From 2d4d91968738fdac560aec4789f021c2bdb7febf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 22 Apr 2022 17:19:04 +0200 Subject: [PATCH 0001/1323] =?UTF-8?q?changelog:=20add=20new=20=E2=80=98unr?= =?UTF-8?q?eleased=E2=80=99=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b83f99f..8201b573 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.12.0](#1-12-0) * [1.11.0](#1-11-0) * [1.10.3](#1-10-3) @@ -37,6 +38,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.12.0 ### Added From e284c764b7c57da0cbd3075e1bdd572a0d7e3958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 22 Apr 2022 18:36:28 +0200 Subject: [PATCH 0002/1323] changelog: replace all bug refs with markdown hyperlinks --- CHANGELOG.md | 438 +++++++++++++++++++++++++-------------------------- 1 file changed, 219 insertions(+), 219 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8201b573..0f604b33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,22 +57,22 @@ * `[key-bindings].scrollback-home|end` options. * Socket activation for `foot --server` and accompanying systemd unit files * Support for re-mapping input, i.e. mapping input to custom escape - sequences (https://codeberg.org/dnkl/foot/issues/325). + sequences ([#325](https://codeberg.org/dnkl/foot/issues/325)). * Support for [DECNKM](https://vt100.net/docs/vt510-rm/DECNKM.html), which allows setting/saving/restoring/querying the keypad mode. * Sixel support can be disabled by setting `[tweak].sixel=no` - (https://codeberg.org/dnkl/foot/issues/950). + ([#950](https://codeberg.org/dnkl/foot/issues/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 - (https://codeberg.org/dnkl/foot/issues/1004). + ([#1004](https://codeberg.org/dnkl/foot/issues/1004)). * `[csd].hide-when-maximized=yes|no` option - (https://codeberg.org/dnkl/foot/issues/1019). + ([#1019](https://codeberg.org/dnkl/foot/issues/1019)). * Scrollback search mode now highlights all matches. * `[key-binding].show-urls-persistent` action. This key binding action is similar to `show-urls-launch`, but does not automatically exit URL mode after activating an URL - (https://codeberg.org/dnkl/foot/issues/964). + ([#964](https://codeberg.org/dnkl/foot/issues/964)). * Support for `CSI > 4 n`, disable _modifyOtherKeys_. Note that since foot only supports level 1 and 2 (and not level 0), this sequence does not disable _modifyOtherKeys_ completely, but simply reverts it @@ -80,24 +80,24 @@ * `-Dtests=false|true` meson command line option. When disabled, test binaries will neither be built, nor will `ninja test` attempt to execute them. Enabled by default - (https://codeberg.org/dnkl/foot/issues/919). + ([#919](https://codeberg.org/dnkl/foot/issues/919)). ### Changed * Minimum required meson version is now 0.58. * Mouse selections are now finalized when the window is resized - (https://codeberg.org/dnkl/foot/issues/922). + ([#922](https://codeberg.org/dnkl/foot/issues/922)). * OSC-4 and OSC-11 replies now uses four digits instead of 2 - (https://codeberg.org/dnkl/foot/issues/971). + ([#971](https://codeberg.org/dnkl/foot/issues/971)). * `\r` is no longer translated to `\n` when pasting clipboard data - (https://codeberg.org/dnkl/foot/issues/980). + ([#980](https://codeberg.org/dnkl/foot/issues/980)). * Use circles for rendering light arc box-drawing characters - (https://codeberg.org/dnkl/foot/issues/988). + ([#988](https://codeberg.org/dnkl/foot/issues/988)). * Example configuration is now installed to `${sysconfdir}/xdg/foot/foot.ini`, typically resolving to `/etc/xdg/foot/foot.ini` - (https://codeberg.org/dnkl/foot/issues/1001). + ([#1001](https://codeberg.org/dnkl/foot/issues/1001)). ### Removed @@ -110,37 +110,37 @@ ### Fixed * Build: missing `wayland_client` dependency in `test-config` - (https://codeberg.org/dnkl/foot/issues/918). + ([#918](https://codeberg.org/dnkl/foot/issues/918)). * “(null)” being logged as font-name (for some fonts) when warning about a non-monospaced primary font. * Rare crash when the window is resized while a mouse selection is - ongoing (https://codeberg.org/dnkl/foot/issues/922). + ongoing ([#922](https://codeberg.org/dnkl/foot/issues/922)). * Large selections crossing the scrollback wrap-around - (https://codeberg.org/dnkl/foot/issues/924). + ([#924](https://codeberg.org/dnkl/foot/issues/924)). * Crash in `pipe-scrollback` - (https://codeberg.org/dnkl/foot/issues/926). + ([#926](https://codeberg.org/dnkl/foot/issues/926)). * Exit code being 0 when a foot server with no open windows terminate due to e.g. a Wayland connection failure - (https://codeberg.org/dnkl/foot/issues/943). + ([#943](https://codeberg.org/dnkl/foot/issues/943)). * Key binding collisions not detected for bindings specified as option overrides on the command line. * Crash when seat has no keyboard - (https://codeberg.org/dnkl/foot/issues/963). + ([#963](https://codeberg.org/dnkl/foot/issues/963)). * Key presses with e.g. `AltGr` triggering key combinations with the - base symbol (https://codeberg.org/dnkl/foot/issues/983). + base symbol ([#983](https://codeberg.org/dnkl/foot/issues/983)). * Underline cursor sometimes being positioned too low, either making it look thinner than what it should be, or being completely - invisible (https://codeberg.org/dnkl/foot/issues/1005). + invisible ([#1005](https://codeberg.org/dnkl/foot/issues/1005)). * Fallback to `/etc/xdg` if `XDG_CONFIG_DIRS` is unset - (https://codeberg.org/dnkl/foot/issues/1008). + ([#1008](https://codeberg.org/dnkl/foot/issues/1008)). * Improved compatibility with XTerm when `modifyOtherKeys=2` - (https://codeberg.org/dnkl/foot/issues/1009). + ([#1009](https://codeberg.org/dnkl/foot/issues/1009)). * Window geometry when CSDs are enabled and CSD border width set to a non-zero value. This fixes window snapping in e.g. GNOME. * Window size “jumping” when starting an interactive resize when CSDs are enabled, and CSD border width set to a non-zero value. * Key binding overrides on the command line having no effect with - `footclient` instances (https://codeberg.org/dnkl/foot/issues/931). + `footclient` instances ([#931](https://codeberg.org/dnkl/foot/issues/931)). * Search prev/next not updating the selection correctly when the previous and new match overlaps. * Various minor fixes to scrollback search, and how it finds the @@ -172,12 +172,12 @@ * _irc://_ and _ircs://_ to the default set of protocols recognized when auto-detecting URLs. * [SGR-Pixels (1016) mouse extended coordinates](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates) is now supported - (https://codeberg.org/dnkl/foot/issues/762). + ([#762](https://codeberg.org/dnkl/foot/issues/762)). * `XTGETTCAP` - builtin terminfo. See [README.md::XTGETTCAP](README.md#xtgettcap) for details - (https://codeberg.org/dnkl/foot/issues/846). + ([#846](https://codeberg.org/dnkl/foot/issues/846)). * `DECRQSS` - _Request Selection or Setting_ - (https://codeberg.org/dnkl/foot/issues/798). Implemented settings + ([#798](https://codeberg.org/dnkl/foot/issues/798)). Implemented settings are: - `DECSTBM` - _Set Top and Bottom Margins_ - `SGR` - _Set Graphic Rendition_ @@ -193,7 +193,7 @@ theme names. * `[scrollback].multiplier` is now applied in “alternate scroll” mode, where scroll events are translated to fake arrow key presses on the - alt screen (https://codeberg.org/dnkl/foot/issues/859). + 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. @@ -204,14 +204,14 @@ * `gettimeofday()` has been replaced with `clock_gettime()`, due to it being marked as obsolete by POSIX. * `alt+tab` now emits `ESC \t` instead of `CSI 27;3;9~` - (https://codeberg.org/dnkl/foot/issues/900). + ([#900](https://codeberg.org/dnkl/foot/issues/900)). * File pasted, or dropped, on the alt screen is no longer quoted - (https://codeberg.org/dnkl/foot/issues/379). + ([#379](https://codeberg.org/dnkl/foot/issues/379)). * Line-based selections now include a trailing newline when copied - (https://codeberg.org/dnkl/foot/issues/869). + ([#869](https://codeberg.org/dnkl/foot/issues/869)). * Foot now clears the signal mask and resets all signal handlers to their default handlers at startup - (https://codeberg.org/dnkl/foot/issues/854). + ([#854](https://codeberg.org/dnkl/foot/issues/854)). * `Copy` and `Paste` keycodes are supported by default for the clipboard. These are useful for keyboards with custom firmware like QMK to enable global copy/paste shortcuts that work inside and @@ -229,15 +229,15 @@ * Font size adjustment (“zooming”) when font is configured with a **pixelsize**, and `dpi-aware=no` - (https://codeberg.org/dnkl/foot/issues/842). + ([#842](https://codeberg.org/dnkl/foot/issues/842)). * Key presses triggering keyboard layout switches also emitting CSI codes in the Kitty keyboard protocol. * Assertion in `shm.c:buffer_release()` - (https://codeberg.org/dnkl/foot/issues/844). + ([#844](https://codeberg.org/dnkl/foot/issues/844)). * Crash when setting a key- or mouse binding to the empty string - (https://codeberg.org/dnkl/foot/issues/851). + ([#851](https://codeberg.org/dnkl/foot/issues/851)). * Crash when maximizing the window and `[csd].size=1` - (https://codeberg.org/dnkl/foot/issues/857). + ([#857](https://codeberg.org/dnkl/foot/issues/857)). * OSC-8 URIs not getting overwritten (erased) by double-width characters (e.g. emojis). * Rendering of CSD borders when `csd.border-width > 0` and desktop @@ -247,14 +247,14 @@ reset the scrollback view to the bottom. * Wrong mouse binding triggered when doing two mouse selections in very quick (< 300ms) succession - (https://codeberg.org/dnkl/foot/issues/883). + ([#883](https://codeberg.org/dnkl/foot/issues/883)). * Bash completion giving an error when completing a list of short options * Sixel: large image resizes (triggered by e.g. large repeat counts in `DECGRI`) are now truncated instead of ignored. * Sixel: a repeat count of 0 in `DECGRI` now emits a single sixel. * LIGHT ARC box drawing characters incorrectly rendered - platforms (https://codeberg.org/dnkl/foot/issues/914). + platforms ([#914](https://codeberg.org/dnkl/foot/issues/914)). ### Contributors @@ -275,7 +275,7 @@ ### Added -* Kitty keyboard protocol (https://codeberg.org/dnkl/foot/issues/319): +* Kitty keyboard protocol ([#319](https://codeberg.org/dnkl/foot/issues/319)): - [Report event types](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-events) (mode `0b10`) - [Report alternate keys](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-alternates) @@ -289,7 +289,7 @@ ### Fixed * Crash when bitmap fonts are scaled down to very small font sizes - (https://codeberg.org/dnkl/foot/issues/830). + ([#830](https://codeberg.org/dnkl/foot/issues/830)). * Crash when overwriting/erasing an OSC-8 URL. @@ -311,15 +311,15 @@ (for example by switching workspace while doing a mouse selection). * OSC-8 URIs in the last column * OSC-8 URIs sometimes being applied to too many, and seemingly - unrelated cells (https://codeberg.org/dnkl/foot/issues/816). + unrelated cells ([#816](https://codeberg.org/dnkl/foot/issues/816)). * OSC-8 URIs incorrectly being dropped when resizing the terminal window with the alternate screen active. * CSD border not being dimmed when window is not focused. * Visual corruption with large CSD borders - (https://codeberg.org/dnkl/foot/issues/823). + ([#823](https://codeberg.org/dnkl/foot/issues/823)). * Mouse cursor shape sometimes not being updated correctly. * Color palette changes (via OSC 4/104) no longer affect RGB colors - (https://codeberg.org/dnkl/foot/issues/678). + ([#678](https://codeberg.org/dnkl/foot/issues/678)). ### Contributors @@ -339,14 +339,14 @@ ### Fixed * Regression: `letter-spacing` resulting in a “not a valid option” - error (https://codeberg.org/dnkl/foot/issues/795). + error ([#795](https://codeberg.org/dnkl/foot/issues/795)). * Regression: bad section name in configuration error messages. * Regression: `pipe-*` key bindings not being parsed correctly, resulting in invalid error messages - (https://codeberg.org/dnkl/foot/issues/809). + ([#809](https://codeberg.org/dnkl/foot/issues/809)). * OSC-8 data not being cleared when cell is overwritten - (https://codeberg.org/dnkl/foot/issues/804, - https://codeberg.org/dnkl/foot/issues/801). + ([#804](https://codeberg.org/dnkl/foot/issues/804), + [#801](https://codeberg.org/dnkl/foot/issues/801)). ### Contributors @@ -368,13 +368,13 @@ foreground and background colors for the scrollback indicator. * `[key-bindings].noop` action. Key combinations assigned to this action will not be sent to the application - (https://codeberg.org/dnkl/foot/issues/765). + ([#765](https://codeberg.org/dnkl/foot/issues/765)). * Color schemes are now installed to `${datadir}/foot/themes`. * `[csd].border-width` and `[csd].border-color`, allowing you to configure the width and color of the CSD border. * Support for `XTMODKEYS` with `Pp=4` and `Pv=2` (_modifyOtherKeys=2_). * `[colors].dim0-7` options, allowing you to configure custom “dim” - colors (https://codeberg.org/dnkl/foot/issues/776). + colors ([#776](https://codeberg.org/dnkl/foot/issues/776)). ### Changed @@ -387,13 +387,13 @@ command line. * Foot now terminates if there are no available seats - for example, due to the compositor not implementing a recent enough version of - the `wl_seat` interface (https://codeberg.org/dnkl/foot/issues/779). + the `wl_seat` interface ([#779](https://codeberg.org/dnkl/foot/issues/779)). * Boolean options in `foot.ini` are now limited to “yes|true|on|1|no|false|off|0”, Previously, anything that did not match “yes|true|on”, or a number greater than 0, was treated as “false”. * `[scrollback].multiplier` is no longer applied when the alternate - screen is in use (https://codeberg.org/dnkl/foot/issues/787). + screen is in use ([#787](https://codeberg.org/dnkl/foot/issues/787)). ### Removed @@ -411,9 +411,9 @@ **effective** modifiers, like it should. * Fix crashes after enabling CSD at runtime when `csd.size` is 0. * Convert `\r` to `\n` when reading clipboard data - (https://codeberg.org/dnkl/foot/issues/752). + ([#752](https://codeberg.org/dnkl/foot/issues/752)). * Clipboard occasionally ceasing to work, until window has been - re-focused (https://codeberg.org/dnkl/foot/issues/753). + re-focused ([#753](https://codeberg.org/dnkl/foot/issues/753)). * Don’t propagate window title updates to the Wayland compositor unless the new title is different from the old title. @@ -436,7 +436,7 @@ * PGO helper scripts no longer set `LC_CTYPE=en_US.UTF-8`. But, note that “full” PGO builds still **require** a UTF-8 locale; you need to set one manually in your build script - (https://codeberg.org/dnkl/foot/issues/728). + ([#728](https://codeberg.org/dnkl/foot/issues/728)). ## 1.9.1 @@ -445,15 +445,15 @@ * Warn when it appears the primary font is not monospaced. Can be disabled by setting `[tweak].font-monospace-warn=no` - (https://codeberg.org/dnkl/foot/issues/704). + ([#704](https://codeberg.org/dnkl/foot/issues/704)). * PGO build scripts, in the `pgo` directory. See INSTALL.md - _Performance optimized, PGO_, for details - (https://codeberg.org/dnkl/foot/issues/701). + ([#701](https://codeberg.org/dnkl/foot/issues/701)). * Braille characters (U+2800 - U+28FF) are now rendered by foot - itself (https://codeberg.org/dnkl/foot/issues/702). + itself ([#702](https://codeberg.org/dnkl/foot/issues/702)). * `-e` command-line option. This option is simply ignored, to appease program launchers that blindly pass `-e` to any terminal emulator - (https://codeberg.org/dnkl/foot/issues/184). + ([#184](https://codeberg.org/dnkl/foot/issues/184)). ### Changed @@ -468,7 +468,7 @@ changed back to `${datadir}/terminfo`. * `dpi-aware=auto`: fonts are now scaled using the monitor’s DPI only when **all** monitors have a scaling factor of one - (https://codeberg.org/dnkl/foot/issues/714). + ([#714](https://codeberg.org/dnkl/foot/issues/714)). * fcft >= 3.0.0 in now required. @@ -476,9 +476,9 @@ * Added workaround for GNOME bug where multiple button press events (for the same button) is sent to the CSDs without any release or - leave events in between (https://codeberg.org/dnkl/foot/issues/709). + leave events in between ([#709](https://codeberg.org/dnkl/foot/issues/709)). * Line-wise selection not taking soft line-wrapping into account - (https://codeberg.org/dnkl/foot/issues/726). + ([#726](https://codeberg.org/dnkl/foot/issues/726)). ### Contributors @@ -492,24 +492,24 @@ ### Added * Window title in the CSDs - (https://codeberg.org/dnkl/foot/issues/638). + ([#638](https://codeberg.org/dnkl/foot/issues/638)). * `-Ddocs=disabled|enabled|auto` meson command line option. * Support for `~`-expansion in the `include` directive - (https://codeberg.org/dnkl/foot/issues/659). + ([#659](https://codeberg.org/dnkl/foot/issues/659)). * Unicode 13 characters U+1FB3C - U+1FB6F, U+1FB9A and U+1FB9B to list of box drawing characters rendered by foot itself (rather than using - font glyphs) (https://codeberg.org/dnkl/foot/issues/474). + font glyphs) ([#474](https://codeberg.org/dnkl/foot/issues/474)). * `XM`+`xm` to terminfo. * Mouse buttons 6/7 (mouse wheel left/right). * `url.uri-characters` option to `foot.ini` - (https://codeberg.org/dnkl/foot/issues/654). + ([#654](https://codeberg.org/dnkl/foot/issues/654)). ### Changed * Terminfo files can now co-exist with the foot terminfo files from ncurses. See `INSTALL.md` for more information - (https://codeberg.org/dnkl/foot/issues/671). + ([#671](https://codeberg.org/dnkl/foot/issues/671)). * `bold-text-in-bright=palette-based` now only brightens colors from palette * Raised grace period between closing the PTY and sending `SIGKILL` (when terminating the client application) from 4 to 60 seconds. @@ -522,7 +522,7 @@ (`tweak.box-drawing-base-thickness`) in box drawing characters are now translated to pixel values using the monitor’s scaling factor when `dpi-aware=no`, or `dpi-aware=auto` and the scaling factor is - larger than 1 (https://codeberg.org/dnkl/foot/issues/680). + larger than 1 ([#680](https://codeberg.org/dnkl/foot/issues/680)). * Spawning a new terminal with a working directory that does not exist is no longer a fatal error. @@ -533,23 +533,23 @@ 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 - (https://codeberg.org/dnkl/foot/issues/670). + ([#670](https://codeberg.org/dnkl/foot/issues/670)). * Keypad application mode keys from terminfo; enabling the keypad application mode is not enough to make foot emit these sequences - you also need to disable private mode 1035 - (https://codeberg.org/dnkl/foot/issues/670). + ([#670](https://codeberg.org/dnkl/foot/issues/670)). ### Fixed * Rendering into the right margin area with `tweak.overflowing-glyphs` enabled. -* PGO builds with clang (https://codeberg.org/dnkl/foot/issues/642). +* PGO builds with clang ([#642](https://codeberg.org/dnkl/foot/issues/642)). * Crash in scrollback search mode when selection has been canceled due to terminal content updates - (https://codeberg.org/dnkl/foot/issues/644). + ([#644](https://codeberg.org/dnkl/foot/issues/644)). * Foot process not terminating when the Wayland connection is broken - (https://codeberg.org/dnkl/foot/issues/651). + ([#651](https://codeberg.org/dnkl/foot/issues/651)). * Output scale being zero on compositors that does not advertise a scaling factor. * Slow-to-terminate client applications causing other footclient instances to @@ -557,10 +557,10 @@ * Underlying cell content showing through in the left-most column of sixels. * `cursor.blink` not working in GNOME - (https://codeberg.org/dnkl/foot/issues/686). + ([#686](https://codeberg.org/dnkl/foot/issues/686)). * Blinking cursor stops blinking, or becoming invisible, when switching focus from, and then back to a terminal window on GNOME - (https://codeberg.org/dnkl/foot/issues/686). + ([#686](https://codeberg.org/dnkl/foot/issues/686)). ### Contributors @@ -575,10 +575,10 @@ ### Added * `locked-title=no|yes` to `foot.ini` - (https://codeberg.org/dnkl/foot/issues/386). + ([#386](https://codeberg.org/dnkl/foot/issues/386)). * `tweak.overflowing-glyphs` option, which can be enabled to fix rendering issues with glyphs of any width that appear cut-off - (https://codeberg.org/dnkl/foot/issues/592). + ([#592](https://codeberg.org/dnkl/foot/issues/592)). ### Changed @@ -586,7 +586,7 @@ * Non-empty lines are now considered to have a hard linebreak, _unless_ an actual word-wrap is inserted. * Setting `DECSDM` now _disables_ sixel scrolling, while resetting it - _enables_ scrolling (https://codeberg.org/dnkl/foot/issues/631). + _enables_ scrolling ([#631](https://codeberg.org/dnkl/foot/issues/631)). ### Removed @@ -601,7 +601,7 @@ * FD exhaustion when repeatedly entering/exiting URL mode with many URLs. * Double free of URL while removing duplicated and/or overlapping URLs - in URL mode (https://codeberg.org/dnkl/foot/issues/627). + in URL mode ([#627](https://codeberg.org/dnkl/foot/issues/627)). * Crash when an unclosed OSC-8 URL ran into un-allocated scrollback rows. * Some box-drawing characters were rendered incorrectly on big-endian @@ -613,7 +613,7 @@ * Reduced memory usage in URL mode. * Crash when the `E3` escape (`\E[3J`) was executed, and there was a selection, or sixel image, in the scrollback - (https://codeberg.org/dnkl/foot/issues/633). + ([#633](https://codeberg.org/dnkl/foot/issues/633)). ### Contributors @@ -629,7 +629,7 @@ * `Tc`, `setrgbf` and `setrgbb` capabilities in `foot` and `foot-direct` terminfo entries. This should make 24-bit RGB colors work in tmux and neovim, without the need for config hacks or detection heuristics - (https://codeberg.org/dnkl/foot/issues/615). + ([#615](https://codeberg.org/dnkl/foot/issues/615)). ### Changed @@ -644,7 +644,7 @@ * Grapheme cluster state being reset between codepoints. * Regression: custom URL key bindings not working - (https://codeberg.org/dnkl/foot/issues/614). + ([#614](https://codeberg.org/dnkl/foot/issues/614)). ### Contributors @@ -721,45 +721,45 @@ supported. * Support for DECSET/DECRST 2026, as an alternative to the existing "synchronized updates" DCS sequences - (https://codeberg.org/dnkl/foot/issues/459). + ([#459](https://codeberg.org/dnkl/foot/issues/459)). * `cursor.beam-thickness` option to `foot.ini` - (https://codeberg.org/dnkl/foot/issues/464). + ([#464](https://codeberg.org/dnkl/foot/issues/464)). * `cursor.underline-thickness` option to `foot.ini` - (https://codeberg.org/dnkl/foot/issues/524). + ([#524](https://codeberg.org/dnkl/foot/issues/524)). * Unicode 13 characters U+1FB70 - U+1FB8B to list of box drawing characters rendered by foot itself (rather than using font glyphs) - (https://codeberg.org/dnkl/foot/issues/471). + ([#471](https://codeberg.org/dnkl/foot/issues/471)). * Dedicated `[bell]` section to config, supporting multiple actions and a new `command` action to run an arbitrary command. (https://codeberg.org/dnkl/foot/pulls/483) * Dedicated `[url]` section to config. * `[url].protocols` option to `foot.ini` - (https://codeberg.org/dnkl/foot/issues/531). + ([#531](https://codeberg.org/dnkl/foot/issues/531)). * Support for setting the full 256 color palette in foot.ini - (https://codeberg.org/dnkl/foot/issues/489) + ([#489](https://codeberg.org/dnkl/foot/issues/489)) * XDG activation support, will be used by `[bell].urgent` when available (falling back to coloring the window margins red when - unavailable) (https://codeberg.org/dnkl/foot/issues/487). + unavailable) ([#487](https://codeberg.org/dnkl/foot/issues/487)). * `ctrl`+`c` as a default key binding; to cancel search/url mode. * `${window-title}` to `notify`. * Support for including files in `foot.ini` - (https://codeberg.org/dnkl/foot/issues/555). + ([#555](https://codeberg.org/dnkl/foot/issues/555)). * `ENVIRONMENT` section in **foot**(1) and **footclient**(1) man pages - (https://codeberg.org/dnkl/foot/issues/556). + ([#556](https://codeberg.org/dnkl/foot/issues/556)). * `tweak.pua-double-width` option to `foot.ini`, letting you force _Private Usage Area_ codepoints to be treated as double-width characters. * OSC 9 desktop notifications (iTerm2 compatible). * Support for LS2 and LS3 (locking shift) escape sequences - (https://codeberg.org/dnkl/foot/issues/581). + ([#581](https://codeberg.org/dnkl/foot/issues/581)). * Support for overriding configuration options on the command line - (https://codeberg.org/dnkl/foot/issues/554, - https://codeberg.org/dnkl/foot/issues/600). + ([#554](https://codeberg.org/dnkl/foot/issues/554), + [#600](https://codeberg.org/dnkl/foot/issues/600)). * `underline-offset` option to `foot.ini` - (https://codeberg.org/dnkl/foot/issues/490). + ([#490](https://codeberg.org/dnkl/foot/issues/490)). * `csd.button-color` option to `foot.ini`. * `-Dterminfo-install-location=disabled|` meson command - line option (https://codeberg.org/dnkl/foot/issues/569). + line option ([#569](https://codeberg.org/dnkl/foot/issues/569)). ### Changed @@ -772,7 +772,7 @@ supported. * The background color of highlighted text is now adjusted, when the foreground and background colors are the same, making the highlighted text legible - (https://codeberg.org/dnkl/foot/issues/455). + ([#455](https://codeberg.org/dnkl/foot/issues/455)). * `cursor.style=bar` to `cursor.style=beam`. `bar` remains a recognized value, but will eventually be deprecated, and removed. * Point values in `line-height`, `letter-spacing`, @@ -783,28 +783,28 @@ supported. 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 - (https://codeberg.org/dnkl/foot/issues/466). + ([#466](https://codeberg.org/dnkl/foot/issues/466)). * Background alpha no longer applied to palette or RGB colors that matches the background color. * Improved performance on compositors that does not release shm buffers immediately, e.g. KWin - (https://codeberg.org/dnkl/foot/issues/478). + ([#478](https://codeberg.org/dnkl/foot/issues/478)). * `ctrl + w` (_extend-to-word-boundary_) can now be used across lines - (https://codeberg.org/dnkl/foot/issues/421). + ([#421](https://codeberg.org/dnkl/foot/issues/421)). * Ignore auto-detected URLs that overlap with OSC-8 URLs. * Default value for the `notify` option to use `-a ${app-id} -i ${app-id} ...` instead of `-a foot -i foot ...`. * `scrollback-*`+`pipe-scrollback` key bindings are now passed through to the client application when the alt screen is active - (https://codeberg.org/dnkl/foot/issues/573). + ([#573](https://codeberg.org/dnkl/foot/issues/573)). * Reverse video (`\E[?5h`) now only swaps the default foreground and background colors. Cells with explicit foreground and/or background colors remain unchanged. * Tabs (`\t`) are now preserved when the window is resized, and when - copying text (https://codeberg.org/dnkl/foot/issues/508). + copying text ([#508](https://codeberg.org/dnkl/foot/issues/508)). * Writing a sixel on top of another sixel no longer erases the first sixel, but the two are instead blended - (https://codeberg.org/dnkl/foot/issues/562). + ([#562](https://codeberg.org/dnkl/foot/issues/562)). * Running foot without a configuration file is no longer an error; it has been demoted to a warning, and is no longer presented as a notification in the terminal window, but only logged on stderr. @@ -832,53 +832,53 @@ supported. * `generate-alt-random-writes.py --sixel` sometimes crashing, resulting in PGO build failures. * Wrong colors in the 256-color cube - (https://codeberg.org/dnkl/foot/issues/479). + ([#479](https://codeberg.org/dnkl/foot/issues/479)). * Memory leak triggered by “opening” an OSC-8 URI and then resetting the terminal without closing the URI - (https://codeberg.org/dnkl/foot/issues/495). + ([#495](https://codeberg.org/dnkl/foot/issues/495)). * Assertion when emitting a sixel occupying the entire scrollback - history (https://codeberg.org/dnkl/foot/issues/494). + history ([#494](https://codeberg.org/dnkl/foot/issues/494)). * Font underlines being positioned below the cell (and thus being invisible) for certain combinations of fonts and font sizes - (https://codeberg.org/dnkl/foot/issues/503). + ([#503](https://codeberg.org/dnkl/foot/issues/503)). * Sixels with transparent bottom border being resized below the size specified in _”Set Raster Attributes”_. * Fonts sometimes not being reloaded with the correct scaling factor when `dpi-aware=no`, or `dpi-aware=auto` with monitor(s) with a - scaling factor > 1 (https://codeberg.org/dnkl/foot/issues/509). + scaling factor > 1 ([#509](https://codeberg.org/dnkl/foot/issues/509)). * Crash caused by certain CSI sequences with very large parameter - values (https://codeberg.org/dnkl/foot/issues/522). + values ([#522](https://codeberg.org/dnkl/foot/issues/522)). * Rare occurrences where the window did not close when the shell exited. Only seen on FreeBSD - (https://codeberg.org/dnkl/foot/issues/534) + ([#534](https://codeberg.org/dnkl/foot/issues/534)) * Foot process(es) sometimes remaining, using 100% CPU, when closing multiple foot windows at the same time - (https://codeberg.org/dnkl/foot/issues/542). + ([#542](https://codeberg.org/dnkl/foot/issues/542)). * Regression where `+shift+tab` always produced `\E[Z` instead of the correct `\E[27;;9~` sequence - (https://codeberg.org/dnkl/foot/issues/547). + ([#547](https://codeberg.org/dnkl/foot/issues/547)). * Crash when a line wrapping OSC-8 URI crossed the scrollback wrap - around (https://codeberg.org/dnkl/foot/issues/552). + around ([#552](https://codeberg.org/dnkl/foot/issues/552)). * Selection incorrectly wrapping rows ending with an explicit newline - (https://codeberg.org/dnkl/foot/issues/565). + ([#565](https://codeberg.org/dnkl/foot/issues/565)). * Off-by-one error in markup of auto-detected URLs when the URL ends in the right-most column. * Multi-column characters being cut in half when resizing the alternate screen. * Restore `SIGHUP` in spawned processes. -* Text reflow performance (https://codeberg.org/dnkl/foot/issues/504). +* Text reflow performance ([#504](https://codeberg.org/dnkl/foot/issues/504)). * IL+DL (`CSI Ps L` + `CSI Ps M`) now moves the cursor to column 0. * SS2 and SS3 (single shift) escape sequences behaving like locking - shifts (https://codeberg.org/dnkl/foot/issues/580). + shifts ([#580](https://codeberg.org/dnkl/foot/issues/580)). * `TEXT`+`STRING`+`UTF8_STRING` mime types not being recognized in - clipboard offers (https://codeberg.org/dnkl/foot/issues/583). + clipboard offers ([#583](https://codeberg.org/dnkl/foot/issues/583)). * Memory leak caused by custom box drawing glyphs not being completely freed when destroying a foot window instance - (https://codeberg.org/dnkl/foot/issues/586). + ([#586](https://codeberg.org/dnkl/foot/issues/586)). * Crash in scrollback search when current XKB layout is missing _compose_ definitions. * Window title not being updated while window is hidden - (https://codeberg.org/dnkl/foot/issues/591). + ([#591](https://codeberg.org/dnkl/foot/issues/591)). * Crash on badly formatted URIs in e.g. OSC-8 URLs. * Window being incorrectly resized on CSD/SSD run-time changes. @@ -893,41 +893,41 @@ supported. ### Added * URxvt OSC-11 extension to set background alpha - (https://codeberg.org/dnkl/foot/issues/436). + ([#436](https://codeberg.org/dnkl/foot/issues/436)). * OSC 17/117/19/119 - change/reset selection background/foreground color. * `box-drawings-uses-font-glyphs=yes|no` option to `foot.ini` - (https://codeberg.org/dnkl/foot/issues/430). + ([#430](https://codeberg.org/dnkl/foot/issues/430)). ### Changed * Underline cursor is now rendered below text underline - (https://codeberg.org/dnkl/foot/issues/415). + ([#415](https://codeberg.org/dnkl/foot/issues/415)). * Foot now tries much harder to keep URL jump labels inside the window - geometry (https://codeberg.org/dnkl/foot/issues/443). + geometry ([#443](https://codeberg.org/dnkl/foot/issues/443)). * `bold-text-in-bright` may now be set to `palette-based`, in which case it will use the corresponding bright palette color when the color to brighten matches one of the base 8 colors, instead of increasing the luminance - (https://codeberg.org/dnkl/foot/issues/449). + ([#449](https://codeberg.org/dnkl/foot/issues/449)). ### Fixed * Reverted _"Consumed modifiers are no longer sent to the client - application"_ (https://codeberg.org/dnkl/foot/issues/425). + application"_ ([#425](https://codeberg.org/dnkl/foot/issues/425)). * Crash caused by a double free originating in `XTSMGRAPHICS` - set number of color registers - (https://codeberg.org/dnkl/foot/issues/427). + ([#427](https://codeberg.org/dnkl/foot/issues/427)). * Wrong action referenced in error message for key binding collisions - (https://codeberg.org/dnkl/foot/issues/432). + ([#432](https://codeberg.org/dnkl/foot/issues/432)). * OSC 4/104 out-of-bounds accesses to the color table. This was the reason pywal turned foot windows transparent - (https://codeberg.org/dnkl/foot/issues/434). + ([#434](https://codeberg.org/dnkl/foot/issues/434)). * PTY not being drained when the client application terminates. * `auto_left_margin` not being limited to `cub1` - (https://codeberg.org/dnkl/foot/issues/441). + ([#441](https://codeberg.org/dnkl/foot/issues/441)). * Crash in scrollback search mode when searching beyond the last output. @@ -941,26 +941,26 @@ supported. ### Changed * Update PGO build instructions in `INSTALL.md` - (https://codeberg.org/dnkl/foot/issues/418). + ([#418](https://codeberg.org/dnkl/foot/issues/418)). * In scrollback search mode, empty cells can now be matched by spaces. ### Fixed * Logic that repairs invalid key bindings ended up breaking valid key - bindings instead (https://codeberg.org/dnkl/foot/issues/407). + bindings instead ([#407](https://codeberg.org/dnkl/foot/issues/407)). * Custom `line-height` settings now scale when increasing or decreasing the font size at run-time. * Newlines sometimes incorrectly inserted into copied text - (https://codeberg.org/dnkl/foot/issues/410). + ([#410](https://codeberg.org/dnkl/foot/issues/410)). * Crash when compositor send `text-input-v3::enter` events without first having sent a `keyboard::enter` event - (https://codeberg.org/dnkl/foot/issues/411). + ([#411](https://codeberg.org/dnkl/foot/issues/411)). * Deadlock when rendering sixel images. * URL labels, scrollback search box or scrollback position indicator sometimes not showing up, caused by invalidly sized surface buffers when output scaling was enabled - (https://codeberg.org/dnkl/foot/issues/409). + ([#409](https://codeberg.org/dnkl/foot/issues/409)). * Empty sixels resulted in non-empty images. @@ -971,39 +971,39 @@ supported. * The `pad` option now accepts an optional third argument, `center` (e.g. `pad=5x5 center`), causing the grid to be centered in the window, with equal amount of padding of the left/right and - top/bottom side (https://codeberg.org/dnkl/foot/issues/273). + top/bottom side ([#273](https://codeberg.org/dnkl/foot/issues/273)). * `line-height`, `letter-spacing`, `horizontal-letter-offset` and `vertical-letter-offset` to `foot.ini`. These options let you tweak cell size and glyph positioning - (https://codeberg.org/dnkl/foot/issues/244). + ([#244](https://codeberg.org/dnkl/foot/issues/244)). * Key/mouse binding `select-extend-character-wise`, which forces the selection mode to 'character-wise' when extending a selection. * `DECSET` `47`, `1047` and `1048`. * URL detection and OSC-8 support. URLs are highlighted and activated using the keyboard (**no** mouse support). See **foot**(1)::URLs, or [README.md](README.md#urls) for details - (https://codeberg.org/dnkl/foot/issues/14). + ([#14](https://codeberg.org/dnkl/foot/issues/14)). * `-d,--log-level={info|warning|error}` to both `foot` and - `footclient` (https://codeberg.org/dnkl/foot/issues/337). + `footclient` ([#337](https://codeberg.org/dnkl/foot/issues/337)). * `-D,--working-directory=DIR` to both `foot` and `footclient` - (https://codeberg.org/dnkl/foot/issues/347) + ([#347](https://codeberg.org/dnkl/foot/issues/347)) * `DECSET 80` - sixel scrolling - (https://codeberg.org/dnkl/foot/issues/361). + ([#361](https://codeberg.org/dnkl/foot/issues/361)). * `DECSET 1070` - sixel private color palette - (https://codeberg.org/dnkl/foot/issues/362). + ([#362](https://codeberg.org/dnkl/foot/issues/362)). * `DECSET 8452` - position cursor to the right of sixels - (https://codeberg.org/dnkl/foot/issues/363). + ([#363](https://codeberg.org/dnkl/foot/issues/363)). * Man page **foot-ctlseqs**(7), documenting all supported escape - sequences (https://codeberg.org/dnkl/foot/issues/235). + sequences ([#235](https://codeberg.org/dnkl/foot/issues/235)). * Support for transparent sixels (DCS parameter `P2=1`) - (https://codeberg.org/dnkl/foot/issues/391). + ([#391](https://codeberg.org/dnkl/foot/issues/391)). * `-N,--no-wait` to `footclient` - (https://codeberg.org/dnkl/foot/issues/395). + ([#395](https://codeberg.org/dnkl/foot/issues/395)). * Completions for Bash shell - (https://codeberg.org/dnkl/foot/issues/10). + ([#10](https://codeberg.org/dnkl/foot/issues/10)). * Implement `XTVERSION` (`CSI > 0q`). Foot will reply with `DCS>|foot(..)ST` - (https://codeberg.org/dnkl/foot/issues/359). + ([#359](https://codeberg.org/dnkl/foot/issues/359)). ### Changed @@ -1012,31 +1012,31 @@ supported. [wrap files](https://mesonbuild.com/Wrap-dependency-system-manual.html) instead of needing to be manually cloned. * Box drawing characters are now rendered by foot, instead of using - font glyphs (https://codeberg.org/dnkl/foot/issues/198) + font glyphs ([#198](https://codeberg.org/dnkl/foot/issues/198)) * Double- or triple clicking then dragging now extends the selection - word- or line-wise (https://codeberg.org/dnkl/foot/issues/267). + word- or line-wise ([#267](https://codeberg.org/dnkl/foot/issues/267)). * The line thickness of box drawing characters now depend on the font - size (https://codeberg.org/dnkl/foot/issues/281). + size ([#281](https://codeberg.org/dnkl/foot/issues/281)). * Extending a word/line-wise selection now uses the original selection mode instead of switching to character-wise. * While doing an interactive resize of a foot window, foot now requires 100ms of idle time (where the window size does not change) before sending the new dimensions to the client application. The timing can be tweaked, or completely disabled, by setting - `resize-delay-ms` (https://codeberg.org/dnkl/foot/issues/301). + `resize-delay-ms` ([#301](https://codeberg.org/dnkl/foot/issues/301)). * `CSI 13 ; 2 t` now reports (0,0). * Key binding matching logic; key combinations like `Control+Shift+C` **must** now be written as either `Control+C` or `Control+Shift+c`, the latter being the preferred - variant. (https://codeberg.org/dnkl/foot/issues/376) + variant. ([#376](https://codeberg.org/dnkl/foot/issues/376)) * Consumed modifiers are no longer sent to the client application - (https://codeberg.org/dnkl/foot/issues/376). + ([#376](https://codeberg.org/dnkl/foot/issues/376)). * The minimum version requirement for the libxkbcommon dependency is now 1.0.0. * Empty pixel rows at the bottom of a sixel is now trimmed. * Sixels with DCS parameter `P2=0|2` now use the _current_ ANSI background color for empty pixels instead of the default background - color (https://codeberg.org/dnkl/foot/issues/391). + color ([#391](https://codeberg.org/dnkl/foot/issues/391)). * Sixel decoding optimized; up to 100% faster in some cases. * Reported sixel “max geometry” from current window size, to the configured maximum size (defaulting to 10000x10000). @@ -1055,7 +1055,7 @@ supported. application. This meant the mouse event was never seen by the application. * Terminals spawned with `ctrl`+`shift`+`n` not terminating when - exiting shell (https://codeberg.org/dnkl/foot/issues/366). + exiting shell ([#366](https://codeberg.org/dnkl/foot/issues/366)). * Default value of `-t,--term` in `--help` output when foot was built without terminfo support. * Drain PTY when the client application terminates. @@ -1077,7 +1077,7 @@ supported. be used to configure which clipboard(s) selected text should be copied to. The default is `primary`, which corresponds to the behavior in older foot releases - (https://codeberg.org/dnkl/foot/issues/288). + ([#288](https://codeberg.org/dnkl/foot/issues/288)). ### Changed @@ -1106,8 +1106,8 @@ supported. ### Added * Completions for fish shell - (https://codeberg.org/dnkl/foot/issues/11) -* FreeBSD support (https://codeberg.org/dnkl/foot/issues/238). + ([#11](https://codeberg.org/dnkl/foot/issues/11)) +* FreeBSD support ([#238](https://codeberg.org/dnkl/foot/issues/238)). * IME popup location support: foot now sends the location of the cursor so any popup can be displayed near the text that is being typed. @@ -1115,7 +1115,7 @@ supported. ### Changed * Trailing comments in `foot.ini` must now be preceded by a space or tab - (https://codeberg.org/dnkl/foot/issues/270) + ([#270](https://codeberg.org/dnkl/foot/issues/270)) * The scrollback search box no longer accepts non-printable characters. * Non-formatting C0 control characters, `BS`, `HT` and `DEL` are now stripped from pasted text. @@ -1126,17 +1126,17 @@ supported. * Exit when the client application terminates, not when the TTY file descriptor is closed. * Crash on compositors not implementing the _text input_ interface - (https://codeberg.org/dnkl/foot/issues/259). + ([#259](https://codeberg.org/dnkl/foot/issues/259)). * Erased, overflowing glyphs (when `tweak.allow-overflowing-double-width-glyphs=yes` - the default) not properly erasing the cell overflowed **into**. * `word-delimiters` option ignores `#` and subsequent characters - (https://codeberg.org/dnkl/foot/issues/270) + ([#270](https://codeberg.org/dnkl/foot/issues/270)) * Combining characters not being rendered when composed with colored bitmap glyphs (i.e. colored emojis). * Pasting URIs from the clipboard when the source has not newline-terminated the last URI - (https://codeberg.org/dnkl/foot/issues/291). + ([#291](https://codeberg.org/dnkl/foot/issues/291)). * Sixel “current geometry” query response not being bounded by the current window dimensions (fixes `lsix` output) * Crash on keyboard input when repeat rate was zero (i.e. no repeat). @@ -1185,7 +1185,7 @@ supported. * Missing dependencies in meson, causing heavily parallelized builds to fail. * Background color when alpha < 1.0 being wrong - (https://codeberg.org/dnkl/foot/issues/249). + ([#249](https://codeberg.org/dnkl/foot/issues/249)). * `generate-alt-random.py` failing in containers. @@ -1209,7 +1209,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * IME support. This is compile-time optional, see [INSTALL.md](INSTALL.md#user-content-options) - (https://codeberg.org/dnkl/foot/issues/134). + ([#134](https://codeberg.org/dnkl/foot/issues/134)). * `DECSET` escape to enable/disable IME: `CSI ? 737769 h` enables IME and `CSI ? 737769 l` disables it. This can be used to e.g. enable/disable IME when entering/leaving insert mode in vim. @@ -1219,11 +1219,11 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See sized using the scaling factor. DPI-only font sizing can be forced by setting `dpi-aware=yes`. Setting `dpi-aware=no` forces font sizing to be based on the scaling factor. - (https://codeberg.org/dnkl/foot/issues/206). + ([#206](https://codeberg.org/dnkl/foot/issues/206)). * Implement reverse auto-wrap (_auto\_left\_margin_, _bw_, in terminfo). This mode can be enabled/disabled with `CSI ? 45 h` and `CSI ? 45 l`. It is **enabled** by default - (https://codeberg.org/dnkl/foot/issues/150). + ([#150](https://codeberg.org/dnkl/foot/issues/150)). * `bell` option to `foot.ini`. Can be set to `set-urgency` to make foot render the margins in red when receiving `BEL` while **not** having keyboard focus. Applications can dynamically enable/disable @@ -1233,24 +1233,24 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See [proposal](https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/9) to add support for this. The value `set-urgency` was chosen for forward-compatibility, in the hopes that this proposal eventualizes - (https://codeberg.org/dnkl/foot/issues/157). + ([#157](https://codeberg.org/dnkl/foot/issues/157)). * `bell` option can also be set to `notify`, in which case a desktop notification is emitted when foot receives `BEL` in an unfocused window. * `word-delimiters` option to `foot.ini` - (https://codeberg.org/dnkl/foot/issues/156). + ([#156](https://codeberg.org/dnkl/foot/issues/156)). * `csd.preferred` can now be set to `none` to disable window decorations. Note that some compositors will render SSDs despite - this option being used (https://codeberg.org/dnkl/foot/issues/163). + this option being used ([#163](https://codeberg.org/dnkl/foot/issues/163)). * Terminal content is now auto-scrolled when moving the mouse above or below the window while selecting - (https://codeberg.org/dnkl/foot/issues/149). + ([#149](https://codeberg.org/dnkl/foot/issues/149)). * `font-bold`, `font-italic` `font-bold-italic` options to `foot.ini`. These options allow custom bold/italic fonts. They are unset by default, meaning the bold/italic version of the regular - font is used (https://codeberg.org/dnkl/foot/issues/169). + font is used ([#169](https://codeberg.org/dnkl/foot/issues/169)). * Drag & drop support; text, files and URLs can now be dropped in a - foot terminal window (https://codeberg.org/dnkl/foot/issues/175). + foot terminal window ([#175](https://codeberg.org/dnkl/foot/issues/175)). * `clipboard-paste` and `primary-paste` scrollback search bindings. By default, they are bound to `ctrl+v ctrl+y` and `shift+insert` respectively, and lets you paste from the clipboard or primary @@ -1258,12 +1258,12 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Support for `pipe-*` actions in mouse bindings. It was previously not possible to add a command to these actions when used in mouse bindings, making them useless - (https://codeberg.org/dnkl/foot/issues/183). + ([#183](https://codeberg.org/dnkl/foot/issues/183)). * `bold-text-in-bright` option to `foot.ini`. When enabled, bold text is rendered in a brighter color - (https://codeberg.org/dnkl/foot/issues/199). + ([#199](https://codeberg.org/dnkl/foot/issues/199)). * `-w,--window-size-pixels` and `-W,--window-size-chars` command line - options to `footclient` (https://codeberg.org/dnkl/foot/issues/189). + options to `footclient` ([#189](https://codeberg.org/dnkl/foot/issues/189)). * Short command line options for `--title`, `--maximized`, `--fullscreen`, `--login-shell`, `--hold` and `--check-config`. * `DECSET` escape to modify the `escape` key to send `\E[27;1;27~` @@ -1271,10 +1271,10 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See 27127 l` disables it (the default). * OSC 777;notify: desktop notifications. Use in combination with the new `notify` option in `foot.ini` - (https://codeberg.org/dnkl/foot/issues/224). + ([#224](https://codeberg.org/dnkl/foot/issues/224)). * Status line terminfo capabilities `hs`, `tsl`, `fsl` and `dsl`. This enables e.g. vim to set the window title - (https://codeberg.org/dnkl/foot/issues/242). + ([#242](https://codeberg.org/dnkl/foot/issues/242)). ### Changed @@ -1285,11 +1285,11 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See window size. * Graphical glitches/flashes when resizing the window while running a fullscreen application, i.e. the 'alt' screen - (https://codeberg.org/dnkl/foot/issues/221). + ([#221](https://codeberg.org/dnkl/foot/issues/221)). * Cursor will now blink if **either** `CSI ? 12 h` or `CSI Ps SP q` has been used to enable blinking. **cursor.blink** in `foot.ini` controls the default state of `CSI Ps SP q` - (https://codeberg.org/dnkl/foot/issues/218). + ([#218](https://codeberg.org/dnkl/foot/issues/218)). * The sub-parameter versions of the SGR RGB color escapes (e.g `\E[38:2...m`) can now be used _without_ the color space ID parameter. @@ -1311,7 +1311,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See ### Fixed * Error when re-assigning a default key binding - (https://codeberg.org/dnkl/foot/issues/233). + ([#233](https://codeberg.org/dnkl/foot/issues/233)). * `\E[s`+`\E[u` (save/restore cursor) now saves and restores attributes and charset configuration, just like `\E7`+`\E8`. * Report mouse motion events to the client application also while @@ -1339,7 +1339,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Num Lock by default overrides the keypad mode. See **foot.ini**(5)::KEYPAD, or [README.md](README.md#user-content-keypad) for details - (https://codeberg.org/dnkl/foot/issues/194). + ([#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 @@ -1348,9 +1348,9 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See ### Fixed * Resize very slow when window is hidden - (https://codeberg.org/dnkl/foot/issues/190). + ([#190](https://codeberg.org/dnkl/foot/issues/190)). * Key mappings for key combinations with `shift`+`tab` - (https://codeberg.org/dnkl/foot/issues/210). + ([#210](https://codeberg.org/dnkl/foot/issues/210)). * Key mappings for key combinations with `alt`+`return`. * `footclient` `-m` (`--maximized`) flag being ignored. * Crash with explicitly sized sixels with a height less than 6 pixels. @@ -1368,18 +1368,18 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Crash when libxkbcommon cannot find a suitable libX11 _compose_ file. Note that foot will run, but without support for dead keys. - (https://codeberg.org/dnkl/foot/issues/170). + ([#170](https://codeberg.org/dnkl/foot/issues/170)). * Restored window size when window is un-tiled. * XCursor shape in CSD corners when window is tiled. * Error handling when processing keyboard input (maybe - https://codeberg.org/dnkl/foot/issues/171). + [#171](https://codeberg.org/dnkl/foot/issues/171)). * Compilation error _"overflow in conversion from long 'unsigned int' to 'int' changes value... "_ seen on platforms where the `request` argument in `ioctl(3)` is an `int` (for example: linux/ppc64). * Crash when using the mouse in alternate scroll mode in an unfocused - window (https://codeberg.org/dnkl/foot/issues/179). + window ([#179](https://codeberg.org/dnkl/foot/issues/179)). * Character dropped from selection when "right-click-hold"-extending a - selection (https://codeberg.org/dnkl/foot/issues/180). + selection ([#180](https://codeberg.org/dnkl/foot/issues/180)). ## 1.5.2 @@ -1387,7 +1387,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See ### Fixed * Regression: middle clicking double pastes in e.g. vim - (https://codeberg.org/dnkl/foot/issues/168) + ([#168](https://codeberg.org/dnkl/foot/issues/168)) ## 1.5.1 @@ -1405,24 +1405,24 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Mouse bindings now match even if the actual click count is larger than specified in the binding. This allows you to, for example, quickly press the middle-button to paste multiple times - (https://codeberg.org/dnkl/foot/issues/146). + ([#146](https://codeberg.org/dnkl/foot/issues/146)). * Color flashes when changing the color palette with OSC 4,10,11 - (https://codeberg.org/dnkl/foot/issues/141). + ([#141](https://codeberg.org/dnkl/foot/issues/141)). * Scrollback position is now retained when resizing the window - (https://codeberg.org/dnkl/foot/issues/142). + ([#142](https://codeberg.org/dnkl/foot/issues/142)). * Trackpad scrolling speed to better match the mouse scrolling speed, and to be consistent with other (Wayland) terminal emulators. Note that it is (much) slower compared to previous foot versions. Use the **scrollback.multiplier** option in `foot.ini` if you find the new - speed too slow (https://codeberg.org/dnkl/foot/issues/144). + speed too slow ([#144](https://codeberg.org/dnkl/foot/issues/144)). * Crash when `foot.ini` contains an invalid section name - (https://codeberg.org/dnkl/foot/issues/159). + ([#159](https://codeberg.org/dnkl/foot/issues/159)). * Background opacity when in _reverse video_ mode. * Crash when writing a sixel image that extends outside the terminal's - right margin (https://codeberg.org/dnkl/foot/issues/151). + right margin ([#151](https://codeberg.org/dnkl/foot/issues/151)). * Sixel image at non-zero column positions getting sheared at seemingly random occasions - (https://codeberg.org/dnkl/foot/issues/151). + ([#151](https://codeberg.org/dnkl/foot/issues/151)). * Crash after either resizing a window or changing the font size if there were sixels present in the scrollback while doing so. * _Send Device Attributes_ to only send a response if `Ps == 0`. @@ -1453,36 +1453,36 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Scrollback position indicator. This feature is optional and controlled by the **scrollback.indicator-position** and **scrollback.indicator-format** options in `foot.ini` - (https://codeberg.org/dnkl/foot/issues/42). + ([#42](https://codeberg.org/dnkl/foot/issues/42)). * Key bindings in _scrollback search_ mode are now configurable. * `--check-config` command line option. * **pipe-selected** key binding. Works like **pipe-visible** and **pipe-scrollback**, but only pipes the currently selected text, if - any (https://codeberg.org/dnkl/foot/issues/51). + any ([#51](https://codeberg.org/dnkl/foot/issues/51)). * **mouse.hide-when-typing** option to `foot.ini`. * **scrollback.multiplier** option to `foot.ini` - (https://codeberg.org/dnkl/foot/issues/54). + ([#54](https://codeberg.org/dnkl/foot/issues/54)). * **colors.selection-foreground** and **colors.selection-background** options to `foot.ini`. * **tweak.render-timer** option to `foot.ini`. * Modifier support in mouse bindings - (https://codeberg.org/dnkl/foot/issues/77). + ([#77](https://codeberg.org/dnkl/foot/issues/77)). * Click count support in mouse bindings, i.e double- and triple-click - (https://codeberg.org/dnkl/foot/issues/78). + ([#78](https://codeberg.org/dnkl/foot/issues/78)). * All mouse actions (begin selection, select word, select row etc) are now configurable, via the new **select-begin**, **select-begin-block**, **select-extend**, **select-word**, **select-word-whitespace** and **select-row** options in the **mouse-bindings** section in `foot.ini` - (https://codeberg.org/dnkl/foot/issues/79). + ([#79](https://codeberg.org/dnkl/foot/issues/79)). * Implement XTSAVE/XTRESTORE escape sequences, `CSI ? Ps s` and `CSI ? - Ps r` (https://codeberg.org/dnkl/foot/issues/91). + Ps r` ([#91](https://codeberg.org/dnkl/foot/issues/91)). * `$COLORTERM` is now set to `truecolor` at startup, to indicate support for 24-bit RGB colors. * Experimental support for rendering double-width glyphs with a character width of 1. Must be explicitly enabled with `tweak.allow-overflowing-double-width-glyphs` - (https://codeberg.org/dnkl/foot/issues/116). + ([#116](https://codeberg.org/dnkl/foot/issues/116)). * **initial-window-size-pixels** options to `foot.ini` and `-w,--window-size-pixels` command line option to `foot`. This option replaces the now deprecated **geometry** and `-g,--geometry` @@ -1493,7 +1493,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See alternative to **initial-window-size-pixels**. * **scrollback-up-half-page** and **scrollback-down-half-page** key bindings. They scroll up/down half of a page in the scrollback - (https://codeberg.org/dnkl/foot/issues/128). + ([#128](https://codeberg.org/dnkl/foot/issues/128)). * **scrollback-up-line** and **scrollback-down-line** key bindings. They scroll up/down a single line in the scrollback. * **mouse.alternate-scroll-mode** option to `foot.ini`. This option @@ -1501,7 +1501,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See defaults to `yes`. When enabled, mouse scroll events are translated to up/down key events in the alternate screen, letting you scroll in e.g. `less` and other applications without enabling native mouse - support in them (https://codeberg.org/dnkl/foot/issues/135). + support in them ([#135](https://codeberg.org/dnkl/foot/issues/135)). ### Changed @@ -1511,7 +1511,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See an error inside the terminal (and of course still log errors on stderr). * Default `--server` socket path to use `$WAYLAND_DISPLAY` instead of - `$XDG_SESSION_ID` (https://codeberg.org/dnkl/foot/issues/55). + `$XDG_SESSION_ID` ([#55](https://codeberg.org/dnkl/foot/issues/55)). * Trailing empty cells are no longer highlighted in mouse selections. * Foot now searches for its configuration in `$XDG_DATA_DIRS/foot/foot.ini`, if no configuration is found in @@ -1532,22 +1532,22 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Compilation errors in 32-bit builds. * Mouse cursor style in top and left margins. * Selection is now **updated** when the cursor moves outside the grid - (https://codeberg.org/dnkl/foot/issues/70). + ([#70](https://codeberg.org/dnkl/foot/issues/70)). * Viewport sometimes not moving when doing a scrollback search. * Crash when canceling a scrollback search and the window had been resized while searching. * Selection start point not moving when the selection changes direction. * OSC 10/11/104/110/111 (modify colors) did not update existing screen - content (https://codeberg.org/dnkl/foot/issues/94). + content ([#94](https://codeberg.org/dnkl/foot/issues/94)). * Extra newlines when copying empty cells - (https://codeberg.org/dnkl/foot/issues/97). + ([#97](https://codeberg.org/dnkl/foot/issues/97)). * Mouse events from being sent to client application when a mouse binding has consumed it. * Input events from getting mixed with paste data - (https://codeberg.org/dnkl/foot/issues/101). + ([#101](https://codeberg.org/dnkl/foot/issues/101)). * Missing DPI values for “some” monitors on Gnome - (https://codeberg.org/dnkl/foot/issues/118). + ([#118](https://codeberg.org/dnkl/foot/issues/118)). * Handling of multi-column composed characters while reflowing. * Escape sequences sent for key combinations with `Return`, that did **not** include `Alt`. @@ -1577,7 +1577,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Crash when starting a selection inside the margins. * Improved font size consistency across multiple monitors with - different DPI (https://codeberg.org/dnkl/foot/issues/47). + different DPI ([#47](https://codeberg.org/dnkl/foot/issues/47)). * Handle trailing comments in `footrc` @@ -1617,7 +1617,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Crash in scrollback search. * Crash when a **pipe-visible** or **pipe-scrollback** command contained an unclosed quote - (https://codeberg.org/dnkl/foot/issues/49). + ([#49](https://codeberg.org/dnkl/foot/issues/49)). ### Contributors @@ -1666,7 +1666,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Implemented `C0::FF` (form feed) * **pipe-visible** and **pipe-scrollback** key bindings. These let you pipe either the currently visible text, or the entire scrollback to - external tools (https://codeberg.org/dnkl/foot/issues/29). Example: + external tools ([#29](https://codeberg.org/dnkl/foot/issues/29)). Example: `pipe-visible=[sh -c "xurls | bemenu | xargs -r firefox] Control+Print` @@ -1713,9 +1713,9 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See select half of a double-width character. * Draw hollow block cursor on top of character. * Set an initial `TIOCSWINSZ`. This ensures clients never read a - `0x0` terminal size (https://codeberg.org/dnkl/foot/issues/20). + `0x0` terminal size ([#20](https://codeberg.org/dnkl/foot/issues/20)). * Glyphs overflowing into surrounding cells - (https://codeberg.org/dnkl/foot/issues/21). + ([#21](https://codeberg.org/dnkl/foot/issues/21)). * Crash when last rendered cursor cell had scrolled off screen and `\E[J3` was executed. * Assert (debug builds) when an `\e]4` OSC escape was not followed by @@ -1735,7 +1735,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Sixel handling when resizing window. * Sixel handling when scrollback wraps around. * Foot now issues much fewer `wl_surface_damage_buffer()` calls - (https://codeberg.org/dnkl/foot/issues/35). + ([#35](https://codeberg.org/dnkl/foot/issues/35)). * `C0::VT` to be processed as `C0::LF`. Previously, `C0::VT` would only move the cursor down, but never scroll. * `C0::HT` (_Horizontal Tab_, or `\t`) no longer clears `LCF` (_Last @@ -1748,7 +1748,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See now printed on the next line, instead of only printing half the character. * Font size can no longer be reduced to negative values - (https://codeberg.org/dnkl/foot/issues/38). + ([#38](https://codeberg.org/dnkl/foot/issues/38)). ## 1.3.0 @@ -1756,7 +1756,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See ### Added * User configurable key- and mouse bindings. See `man 5 foot` and the - example `footrc` (https://codeberg.org/dnkl/foot/issues/1) + example `footrc` ([#1](https://codeberg.org/dnkl/foot/issues/1)) * **initial-window-mode** option to `footrc`, that lets you control the initial mode for each newly spawned window: _windowed_, _maximized_ or _fullscreen_. @@ -1775,7 +1775,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Spaces no longer removed from zsh font name completions. * Default key binding for _spawn-terminal_ to ctrl+shift+n. * Renderer is now much faster with interactive scrolling - (https://codeberg.org/dnkl/foot/issues/4) + ([#4](https://codeberg.org/dnkl/foot/issues/4)) * memfd sealing failures are no longer fatal errors. * Selection to no longer be cleared on resize. * The current monitor's subpixel order (RGB/BGR/V-RGB/V-BGR) is @@ -1836,9 +1836,9 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See ### Fixed * Window size doubling when moving window between outputs with - different scaling factors (https://codeberg.org/dnkl/foot/issues/3). + different scaling factors ([#3](https://codeberg.org/dnkl/foot/issues/3)). * Font being too small on monitors with fractional scaling - (https://codeberg.org/dnkl/foot/issues/5). + ([#5](https://codeberg.org/dnkl/foot/issues/5)). ## 1.2.1 From 6652a836adb26ef55789bbd23113b158090451fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 22 Apr 2022 18:38:32 +0200 Subject: [PATCH 0003/1323] changelog: convert all issue links to reference links in the 1.12.0 release --- CHANGELOG.md | 474 ++++++++++++++++++++++++++------------------------- 1 file changed, 245 insertions(+), 229 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f604b33..4cd2b99b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,47 +57,43 @@ * `[key-bindings].scrollback-home|end` options. * Socket activation for `foot --server` and accompanying systemd unit files * Support for re-mapping input, i.e. mapping input to custom escape - sequences ([#325](https://codeberg.org/dnkl/foot/issues/325)). -* Support for [DECNKM](https://vt100.net/docs/vt510-rm/DECNKM.html), which - allows setting/saving/restoring/querying the keypad mode. + sequences ([#325][325]). +* Support for [DECNKM](https://vt100.net/docs/vt510-rm/DECNKM.html), + which allows setting/saving/restoring/querying the keypad mode. * Sixel support can be disabled by setting `[tweak].sixel=no` - ([#950](https://codeberg.org/dnkl/foot/issues/950)). + ([#950][950]). * footclient: `-E,--client-environment` command line option. When used, the child process in the new terminal instance inherits the environment from the footclient process instead of the server’s - ([#1004](https://codeberg.org/dnkl/foot/issues/1004)). -* `[csd].hide-when-maximized=yes|no` option - ([#1019](https://codeberg.org/dnkl/foot/issues/1019)). + ([#1004][1004]). +* `[csd].hide-when-maximized=yes|no` option ([#1019][1019]). * Scrollback search mode now highlights all matches. * `[key-binding].show-urls-persistent` action. This key binding action is similar to `show-urls-launch`, but does not automatically exit - URL mode after activating an URL - ([#964](https://codeberg.org/dnkl/foot/issues/964)). + URL mode after activating an URL ([#964][964]). * Support for `CSI > 4 n`, disable _modifyOtherKeys_. Note that since foot only supports level 1 and 2 (and not level 0), this sequence does not disable _modifyOtherKeys_ completely, but simply reverts it back to level 1 (the default). * `-Dtests=false|true` meson command line option. When disabled, test binaries will neither be built, nor will `ninja test` attempt to - execute them. Enabled by default - ([#919](https://codeberg.org/dnkl/foot/issues/919)). + execute them. Enabled by default ([#919][919]). ### Changed * Minimum required meson version is now 0.58. * Mouse selections are now finalized when the window is resized - ([#922](https://codeberg.org/dnkl/foot/issues/922)). + ([#922][922]). * OSC-4 and OSC-11 replies now uses four digits instead of 2 - ([#971](https://codeberg.org/dnkl/foot/issues/971)). + ([#971][971]). * `\r` is no longer translated to `\n` when pasting clipboard data - ([#980](https://codeberg.org/dnkl/foot/issues/980)). + ([#980][980]). * Use circles for rendering light arc box-drawing characters - ([#988](https://codeberg.org/dnkl/foot/issues/988)). + ([#988][988]). * Example configuration is now installed to `${sysconfdir}/xdg/foot/foot.ini`, typically resolving to - `/etc/xdg/foot/foot.ini` - ([#1001](https://codeberg.org/dnkl/foot/issues/1001)). + `/etc/xdg/foot/foot.ini` ([#1001][1001]). ### Removed @@ -110,37 +106,33 @@ ### Fixed * Build: missing `wayland_client` dependency in `test-config` - ([#918](https://codeberg.org/dnkl/foot/issues/918)). + ([#918][918]). * “(null)” being logged as font-name (for some fonts) when warning about a non-monospaced primary font. * Rare crash when the window is resized while a mouse selection is - ongoing ([#922](https://codeberg.org/dnkl/foot/issues/922)). -* Large selections crossing the scrollback wrap-around - ([#924](https://codeberg.org/dnkl/foot/issues/924)). -* Crash in `pipe-scrollback` - ([#926](https://codeberg.org/dnkl/foot/issues/926)). + ongoing ([#922][922]). +* Large selections crossing the scrollback wrap-around ([#924][924]). +* Crash in `pipe-scrollback` ([#926][926]). * Exit code being 0 when a foot server with no open windows terminate - due to e.g. a Wayland connection failure - ([#943](https://codeberg.org/dnkl/foot/issues/943)). + due to e.g. a Wayland connection failure ([#943][943]). * Key binding collisions not detected for bindings specified as option overrides on the command line. -* Crash when seat has no keyboard - ([#963](https://codeberg.org/dnkl/foot/issues/963)). +* Crash when seat has no keyboard ([#963][963]). * Key presses with e.g. `AltGr` triggering key combinations with the - base symbol ([#983](https://codeberg.org/dnkl/foot/issues/983)). + base symbol ([#983][983]). * Underline cursor sometimes being positioned too low, either making it look thinner than what it should be, or being completely - invisible ([#1005](https://codeberg.org/dnkl/foot/issues/1005)). + invisible ([#1005][1005]). * Fallback to `/etc/xdg` if `XDG_CONFIG_DIRS` is unset - ([#1008](https://codeberg.org/dnkl/foot/issues/1008)). + ([#1008][1008]). * Improved compatibility with XTerm when `modifyOtherKeys=2` - ([#1009](https://codeberg.org/dnkl/foot/issues/1009)). + ([#1009][1009]). * Window geometry when CSDs are enabled and CSD border width set to a non-zero value. This fixes window snapping in e.g. GNOME. * Window size “jumping” when starting an interactive resize when CSDs are enabled, and CSD border width set to a non-zero value. * Key binding overrides on the command line having no effect with - `footclient` instances ([#931](https://codeberg.org/dnkl/foot/issues/931)). + `footclient` instances ([#931][931]). * Search prev/next not updating the selection correctly when the previous and new match overlaps. * Various minor fixes to scrollback search, and how it finds the @@ -162,6 +154,30 @@ * merkix +[325]: https://codeberg.org/dnkl/foot/issues/325 +[950]: https://codeberg.org/dnkl/foot/issues/950 +[1004]: https://codeberg.org/dnkl/foot/issues/1004 +[1019]: https://codeberg.org/dnkl/foot/issues/1019 +[964]: https://codeberg.org/dnkl/foot/issues/964 +[919]: https://codeberg.org/dnkl/foot/issues/919 +[922]: https://codeberg.org/dnkl/foot/issues/922 +[971]: https://codeberg.org/dnkl/foot/issues/971 +[980]: https://codeberg.org/dnkl/foot/issues/980 +[988]: https://codeberg.org/dnkl/foot/issues/988 +[1001]: https://codeberg.org/dnkl/foot/issues/1001 +[918]: https://codeberg.org/dnkl/foot/issues/918 +[922]: https://codeberg.org/dnkl/foot/issues/922 +[924]: https://codeberg.org/dnkl/foot/issues/924 +[926]: https://codeberg.org/dnkl/foot/issues/926 +[943]: https://codeberg.org/dnkl/foot/issues/943 +[963]: https://codeberg.org/dnkl/foot/issues/963 +[983]: https://codeberg.org/dnkl/foot/issues/983 +[1005]: https://codeberg.org/dnkl/foot/issues/1005 +[1008]: https://codeberg.org/dnkl/foot/issues/1008 +[1009]: https://codeberg.org/dnkl/foot/issues/1009 +[931]: https://codeberg.org/dnkl/foot/issues/931 + + ## 1.11.0 ### Added @@ -172,12 +188,12 @@ * _irc://_ and _ircs://_ to the default set of protocols recognized when auto-detecting URLs. * [SGR-Pixels (1016) mouse extended coordinates](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates) is now supported - ([#762](https://codeberg.org/dnkl/foot/issues/762)). + ([#762][762]). * `XTGETTCAP` - builtin terminfo. See [README.md::XTGETTCAP](README.md#xtgettcap) for details - ([#846](https://codeberg.org/dnkl/foot/issues/846)). + ([#846][846]). * `DECRQSS` - _Request Selection or Setting_ - ([#798](https://codeberg.org/dnkl/foot/issues/798)). Implemented settings + ([#798][798]). Implemented settings are: - `DECSTBM` - _Set Top and Bottom Margins_ - `SGR` - _Set Graphic Rendition_ @@ -193,7 +209,7 @@ theme names. * `[scrollback].multiplier` is now applied in “alternate scroll” mode, where scroll events are translated to fake arrow key presses on the - alt screen ([#859](https://codeberg.org/dnkl/foot/issues/859)). + alt screen ([#859][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. @@ -204,14 +220,14 @@ * `gettimeofday()` has been replaced with `clock_gettime()`, due to it being marked as obsolete by POSIX. * `alt+tab` now emits `ESC \t` instead of `CSI 27;3;9~` - ([#900](https://codeberg.org/dnkl/foot/issues/900)). + ([#900][900]). * File pasted, or dropped, on the alt screen is no longer quoted - ([#379](https://codeberg.org/dnkl/foot/issues/379)). + ([#379][379]). * Line-based selections now include a trailing newline when copied - ([#869](https://codeberg.org/dnkl/foot/issues/869)). + ([#869][869]). * Foot now clears the signal mask and resets all signal handlers to their default handlers at startup - ([#854](https://codeberg.org/dnkl/foot/issues/854)). + ([#854][854]). * `Copy` and `Paste` keycodes are supported by default for the clipboard. These are useful for keyboards with custom firmware like QMK to enable global copy/paste shortcuts that work inside and @@ -229,15 +245,15 @@ * Font size adjustment (“zooming”) when font is configured with a **pixelsize**, and `dpi-aware=no` - ([#842](https://codeberg.org/dnkl/foot/issues/842)). + ([#842][842]). * Key presses triggering keyboard layout switches also emitting CSI codes in the Kitty keyboard protocol. * Assertion in `shm.c:buffer_release()` - ([#844](https://codeberg.org/dnkl/foot/issues/844)). + ([#844][844]). * Crash when setting a key- or mouse binding to the empty string - ([#851](https://codeberg.org/dnkl/foot/issues/851)). + ([#851][851]). * Crash when maximizing the window and `[csd].size=1` - ([#857](https://codeberg.org/dnkl/foot/issues/857)). + ([#857][857]). * OSC-8 URIs not getting overwritten (erased) by double-width characters (e.g. emojis). * Rendering of CSD borders when `csd.border-width > 0` and desktop @@ -247,14 +263,14 @@ reset the scrollback view to the bottom. * Wrong mouse binding triggered when doing two mouse selections in very quick (< 300ms) succession - ([#883](https://codeberg.org/dnkl/foot/issues/883)). + ([#883][883]). * Bash completion giving an error when completing a list of short options * Sixel: large image resizes (triggered by e.g. large repeat counts in `DECGRI`) are now truncated instead of ignored. * Sixel: a repeat count of 0 in `DECGRI` now emits a single sixel. * LIGHT ARC box drawing characters incorrectly rendered - platforms ([#914](https://codeberg.org/dnkl/foot/issues/914)). + platforms ([#914][914]). ### Contributors @@ -275,7 +291,7 @@ ### Added -* Kitty keyboard protocol ([#319](https://codeberg.org/dnkl/foot/issues/319)): +* Kitty keyboard protocol ([#319][319]): - [Report event types](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-events) (mode `0b10`) - [Report alternate keys](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-alternates) @@ -289,7 +305,7 @@ ### Fixed * Crash when bitmap fonts are scaled down to very small font sizes - ([#830](https://codeberg.org/dnkl/foot/issues/830)). + ([#830][830]). * Crash when overwriting/erasing an OSC-8 URL. @@ -311,15 +327,15 @@ (for example by switching workspace while doing a mouse selection). * OSC-8 URIs in the last column * OSC-8 URIs sometimes being applied to too many, and seemingly - unrelated cells ([#816](https://codeberg.org/dnkl/foot/issues/816)). + unrelated cells ([#816][816]). * OSC-8 URIs incorrectly being dropped when resizing the terminal window with the alternate screen active. * CSD border not being dimmed when window is not focused. * Visual corruption with large CSD borders - ([#823](https://codeberg.org/dnkl/foot/issues/823)). + ([#823][823]). * Mouse cursor shape sometimes not being updated correctly. * Color palette changes (via OSC 4/104) no longer affect RGB colors - ([#678](https://codeberg.org/dnkl/foot/issues/678)). + ([#678][678]). ### Contributors @@ -339,14 +355,14 @@ ### Fixed * Regression: `letter-spacing` resulting in a “not a valid option” - error ([#795](https://codeberg.org/dnkl/foot/issues/795)). + error ([#795][795]). * Regression: bad section name in configuration error messages. * Regression: `pipe-*` key bindings not being parsed correctly, resulting in invalid error messages - ([#809](https://codeberg.org/dnkl/foot/issues/809)). + ([#809][809]). * OSC-8 data not being cleared when cell is overwritten - ([#804](https://codeberg.org/dnkl/foot/issues/804), - [#801](https://codeberg.org/dnkl/foot/issues/801)). + ([#804][804], + [#801][801]). ### Contributors @@ -368,13 +384,13 @@ foreground and background colors for the scrollback indicator. * `[key-bindings].noop` action. Key combinations assigned to this action will not be sent to the application - ([#765](https://codeberg.org/dnkl/foot/issues/765)). + ([#765][765]). * Color schemes are now installed to `${datadir}/foot/themes`. * `[csd].border-width` and `[csd].border-color`, allowing you to configure the width and color of the CSD border. * Support for `XTMODKEYS` with `Pp=4` and `Pv=2` (_modifyOtherKeys=2_). * `[colors].dim0-7` options, allowing you to configure custom “dim” - colors ([#776](https://codeberg.org/dnkl/foot/issues/776)). + colors ([#776][776]). ### Changed @@ -387,13 +403,13 @@ command line. * Foot now terminates if there are no available seats - for example, due to the compositor not implementing a recent enough version of - the `wl_seat` interface ([#779](https://codeberg.org/dnkl/foot/issues/779)). + the `wl_seat` interface ([#779][779]). * Boolean options in `foot.ini` are now limited to “yes|true|on|1|no|false|off|0”, Previously, anything that did not match “yes|true|on”, or a number greater than 0, was treated as “false”. * `[scrollback].multiplier` is no longer applied when the alternate - screen is in use ([#787](https://codeberg.org/dnkl/foot/issues/787)). + screen is in use ([#787][787]). ### Removed @@ -411,9 +427,9 @@ **effective** modifiers, like it should. * Fix crashes after enabling CSD at runtime when `csd.size` is 0. * Convert `\r` to `\n` when reading clipboard data - ([#752](https://codeberg.org/dnkl/foot/issues/752)). + ([#752][752]). * Clipboard occasionally ceasing to work, until window has been - re-focused ([#753](https://codeberg.org/dnkl/foot/issues/753)). + re-focused ([#753][753]). * Don’t propagate window title updates to the Wayland compositor unless the new title is different from the old title. @@ -436,7 +452,7 @@ * PGO helper scripts no longer set `LC_CTYPE=en_US.UTF-8`. But, note that “full” PGO builds still **require** a UTF-8 locale; you need to set one manually in your build script - ([#728](https://codeberg.org/dnkl/foot/issues/728)). + ([#728][728]). ## 1.9.1 @@ -445,15 +461,15 @@ * Warn when it appears the primary font is not monospaced. Can be disabled by setting `[tweak].font-monospace-warn=no` - ([#704](https://codeberg.org/dnkl/foot/issues/704)). + ([#704][704]). * PGO build scripts, in the `pgo` directory. See INSTALL.md - _Performance optimized, PGO_, for details - ([#701](https://codeberg.org/dnkl/foot/issues/701)). + ([#701][701]). * Braille characters (U+2800 - U+28FF) are now rendered by foot - itself ([#702](https://codeberg.org/dnkl/foot/issues/702)). + itself ([#702][702]). * `-e` command-line option. This option is simply ignored, to appease program launchers that blindly pass `-e` to any terminal emulator - ([#184](https://codeberg.org/dnkl/foot/issues/184)). + ([#184][184]). ### Changed @@ -468,7 +484,7 @@ changed back to `${datadir}/terminfo`. * `dpi-aware=auto`: fonts are now scaled using the monitor’s DPI only when **all** monitors have a scaling factor of one - ([#714](https://codeberg.org/dnkl/foot/issues/714)). + ([#714][714]). * fcft >= 3.0.0 in now required. @@ -476,9 +492,9 @@ * Added workaround for GNOME bug where multiple button press events (for the same button) is sent to the CSDs without any release or - leave events in between ([#709](https://codeberg.org/dnkl/foot/issues/709)). + leave events in between ([#709][709]). * Line-wise selection not taking soft line-wrapping into account - ([#726](https://codeberg.org/dnkl/foot/issues/726)). + ([#726][726]). ### Contributors @@ -492,24 +508,24 @@ ### Added * Window title in the CSDs - ([#638](https://codeberg.org/dnkl/foot/issues/638)). + ([#638][638]). * `-Ddocs=disabled|enabled|auto` meson command line option. * Support for `~`-expansion in the `include` directive - ([#659](https://codeberg.org/dnkl/foot/issues/659)). + ([#659][659]). * Unicode 13 characters U+1FB3C - U+1FB6F, U+1FB9A and U+1FB9B to list of box drawing characters rendered by foot itself (rather than using - font glyphs) ([#474](https://codeberg.org/dnkl/foot/issues/474)). + font glyphs) ([#474][474]). * `XM`+`xm` to terminfo. * Mouse buttons 6/7 (mouse wheel left/right). * `url.uri-characters` option to `foot.ini` - ([#654](https://codeberg.org/dnkl/foot/issues/654)). + ([#654][654]). ### Changed * Terminfo files can now co-exist with the foot terminfo files from ncurses. See `INSTALL.md` for more information - ([#671](https://codeberg.org/dnkl/foot/issues/671)). + ([#671][671]). * `bold-text-in-bright=palette-based` now only brightens colors from palette * Raised grace period between closing the PTY and sending `SIGKILL` (when terminating the client application) from 4 to 60 seconds. @@ -522,7 +538,7 @@ (`tweak.box-drawing-base-thickness`) in box drawing characters are now translated to pixel values using the monitor’s scaling factor when `dpi-aware=no`, or `dpi-aware=auto` and the scaling factor is - larger than 1 ([#680](https://codeberg.org/dnkl/foot/issues/680)). + larger than 1 ([#680][680]). * Spawning a new terminal with a working directory that does not exist is no longer a fatal error. @@ -533,23 +549,23 @@ 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)). + ([#670][670]). * Keypad application mode keys from terminfo; enabling the keypad application mode is not enough to make foot emit these sequences - you also need to disable private mode 1035 - ([#670](https://codeberg.org/dnkl/foot/issues/670)). + ([#670][670]). ### Fixed * Rendering into the right margin area with `tweak.overflowing-glyphs` enabled. -* PGO builds with clang ([#642](https://codeberg.org/dnkl/foot/issues/642)). +* PGO builds with clang ([#642][642]). * Crash in scrollback search mode when selection has been canceled due to terminal content updates - ([#644](https://codeberg.org/dnkl/foot/issues/644)). + ([#644][644]). * Foot process not terminating when the Wayland connection is broken - ([#651](https://codeberg.org/dnkl/foot/issues/651)). + ([#651][651]). * Output scale being zero on compositors that does not advertise a scaling factor. * Slow-to-terminate client applications causing other footclient instances to @@ -557,10 +573,10 @@ * Underlying cell content showing through in the left-most column of sixels. * `cursor.blink` not working in GNOME - ([#686](https://codeberg.org/dnkl/foot/issues/686)). + ([#686][686]). * Blinking cursor stops blinking, or becoming invisible, when switching focus from, and then back to a terminal window on GNOME - ([#686](https://codeberg.org/dnkl/foot/issues/686)). + ([#686][686]). ### Contributors @@ -575,10 +591,10 @@ ### Added * `locked-title=no|yes` to `foot.ini` - ([#386](https://codeberg.org/dnkl/foot/issues/386)). + ([#386][386]). * `tweak.overflowing-glyphs` option, which can be enabled to fix rendering issues with glyphs of any width that appear cut-off - ([#592](https://codeberg.org/dnkl/foot/issues/592)). + ([#592][592]). ### Changed @@ -586,7 +602,7 @@ * Non-empty lines are now considered to have a hard linebreak, _unless_ an actual word-wrap is inserted. * Setting `DECSDM` now _disables_ sixel scrolling, while resetting it - _enables_ scrolling ([#631](https://codeberg.org/dnkl/foot/issues/631)). + _enables_ scrolling ([#631][631]). ### Removed @@ -601,7 +617,7 @@ * FD exhaustion when repeatedly entering/exiting URL mode with many URLs. * Double free of URL while removing duplicated and/or overlapping URLs - in URL mode ([#627](https://codeberg.org/dnkl/foot/issues/627)). + in URL mode ([#627][627]). * Crash when an unclosed OSC-8 URL ran into un-allocated scrollback rows. * Some box-drawing characters were rendered incorrectly on big-endian @@ -613,7 +629,7 @@ * Reduced memory usage in URL mode. * Crash when the `E3` escape (`\E[3J`) was executed, and there was a selection, or sixel image, in the scrollback - ([#633](https://codeberg.org/dnkl/foot/issues/633)). + ([#633][633]). ### Contributors @@ -629,7 +645,7 @@ * `Tc`, `setrgbf` and `setrgbb` capabilities in `foot` and `foot-direct` terminfo entries. This should make 24-bit RGB colors work in tmux and neovim, without the need for config hacks or detection heuristics - ([#615](https://codeberg.org/dnkl/foot/issues/615)). + ([#615][615]). ### Changed @@ -644,7 +660,7 @@ * Grapheme cluster state being reset between codepoints. * Regression: custom URL key bindings not working - ([#614](https://codeberg.org/dnkl/foot/issues/614)). + ([#614][614]). ### Contributors @@ -721,45 +737,45 @@ supported. * Support for DECSET/DECRST 2026, as an alternative to the existing "synchronized updates" DCS sequences - ([#459](https://codeberg.org/dnkl/foot/issues/459)). + ([#459][459]). * `cursor.beam-thickness` option to `foot.ini` - ([#464](https://codeberg.org/dnkl/foot/issues/464)). + ([#464][464]). * `cursor.underline-thickness` option to `foot.ini` - ([#524](https://codeberg.org/dnkl/foot/issues/524)). + ([#524][524]). * Unicode 13 characters U+1FB70 - U+1FB8B to list of box drawing characters rendered by foot itself (rather than using font glyphs) - ([#471](https://codeberg.org/dnkl/foot/issues/471)). + ([#471][471]). * Dedicated `[bell]` section to config, supporting multiple actions and a new `command` action to run an arbitrary command. (https://codeberg.org/dnkl/foot/pulls/483) * Dedicated `[url]` section to config. * `[url].protocols` option to `foot.ini` - ([#531](https://codeberg.org/dnkl/foot/issues/531)). + ([#531][531]). * Support for setting the full 256 color palette in foot.ini - ([#489](https://codeberg.org/dnkl/foot/issues/489)) + ([#489][489]) * XDG activation support, will be used by `[bell].urgent` when available (falling back to coloring the window margins red when - unavailable) ([#487](https://codeberg.org/dnkl/foot/issues/487)). + unavailable) ([#487][487]). * `ctrl`+`c` as a default key binding; to cancel search/url mode. * `${window-title}` to `notify`. * Support for including files in `foot.ini` - ([#555](https://codeberg.org/dnkl/foot/issues/555)). + ([#555][555]). * `ENVIRONMENT` section in **foot**(1) and **footclient**(1) man pages - ([#556](https://codeberg.org/dnkl/foot/issues/556)). + ([#556][556]). * `tweak.pua-double-width` option to `foot.ini`, letting you force _Private Usage Area_ codepoints to be treated as double-width characters. * OSC 9 desktop notifications (iTerm2 compatible). * Support for LS2 and LS3 (locking shift) escape sequences - ([#581](https://codeberg.org/dnkl/foot/issues/581)). + ([#581][581]). * Support for overriding configuration options on the command line - ([#554](https://codeberg.org/dnkl/foot/issues/554), - [#600](https://codeberg.org/dnkl/foot/issues/600)). + ([#554][554], + [#600][600]). * `underline-offset` option to `foot.ini` - ([#490](https://codeberg.org/dnkl/foot/issues/490)). + ([#490][490]). * `csd.button-color` option to `foot.ini`. * `-Dterminfo-install-location=disabled|` meson command - line option ([#569](https://codeberg.org/dnkl/foot/issues/569)). + line option ([#569][569]). ### Changed @@ -772,7 +788,7 @@ supported. * The background color of highlighted text is now adjusted, when the foreground and background colors are the same, making the highlighted text legible - ([#455](https://codeberg.org/dnkl/foot/issues/455)). + ([#455][455]). * `cursor.style=bar` to `cursor.style=beam`. `bar` remains a recognized value, but will eventually be deprecated, and removed. * Point values in `line-height`, `letter-spacing`, @@ -783,28 +799,28 @@ supported. 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)). + ([#466][466]). * Background alpha no longer applied to palette or RGB colors that matches the background color. * Improved performance on compositors that does not release shm buffers immediately, e.g. KWin - ([#478](https://codeberg.org/dnkl/foot/issues/478)). + ([#478][478]). * `ctrl + w` (_extend-to-word-boundary_) can now be used across lines - ([#421](https://codeberg.org/dnkl/foot/issues/421)). + ([#421][421]). * Ignore auto-detected URLs that overlap with OSC-8 URLs. * Default value for the `notify` option to use `-a ${app-id} -i ${app-id} ...` instead of `-a foot -i foot ...`. * `scrollback-*`+`pipe-scrollback` key bindings are now passed through to the client application when the alt screen is active - ([#573](https://codeberg.org/dnkl/foot/issues/573)). + ([#573][573]). * Reverse video (`\E[?5h`) now only swaps the default foreground and background colors. Cells with explicit foreground and/or background colors remain unchanged. * Tabs (`\t`) are now preserved when the window is resized, and when - copying text ([#508](https://codeberg.org/dnkl/foot/issues/508)). + copying text ([#508][508]). * Writing a sixel on top of another sixel no longer erases the first sixel, but the two are instead blended - ([#562](https://codeberg.org/dnkl/foot/issues/562)). + ([#562][562]). * Running foot without a configuration file is no longer an error; it has been demoted to a warning, and is no longer presented as a notification in the terminal window, but only logged on stderr. @@ -832,53 +848,53 @@ supported. * `generate-alt-random-writes.py --sixel` sometimes crashing, resulting in PGO build failures. * Wrong colors in the 256-color cube - ([#479](https://codeberg.org/dnkl/foot/issues/479)). + ([#479][479]). * Memory leak triggered by “opening” an OSC-8 URI and then resetting the terminal without closing the URI - ([#495](https://codeberg.org/dnkl/foot/issues/495)). + ([#495][495]). * Assertion when emitting a sixel occupying the entire scrollback - history ([#494](https://codeberg.org/dnkl/foot/issues/494)). + history ([#494][494]). * Font underlines being positioned below the cell (and thus being invisible) for certain combinations of fonts and font sizes - ([#503](https://codeberg.org/dnkl/foot/issues/503)). + ([#503][503]). * Sixels with transparent bottom border being resized below the size specified in _”Set Raster Attributes”_. * Fonts sometimes not being reloaded with the correct scaling factor when `dpi-aware=no`, or `dpi-aware=auto` with monitor(s) with a - scaling factor > 1 ([#509](https://codeberg.org/dnkl/foot/issues/509)). + scaling factor > 1 ([#509][509]). * Crash caused by certain CSI sequences with very large parameter - values ([#522](https://codeberg.org/dnkl/foot/issues/522)). + values ([#522][522]). * Rare occurrences where the window did not close when the shell exited. Only seen on FreeBSD - ([#534](https://codeberg.org/dnkl/foot/issues/534)) + ([#534][534]) * Foot process(es) sometimes remaining, using 100% CPU, when closing multiple foot windows at the same time - ([#542](https://codeberg.org/dnkl/foot/issues/542)). + ([#542][542]). * Regression where `+shift+tab` always produced `\E[Z` instead of the correct `\E[27;;9~` sequence - ([#547](https://codeberg.org/dnkl/foot/issues/547)). + ([#547][547]). * Crash when a line wrapping OSC-8 URI crossed the scrollback wrap - around ([#552](https://codeberg.org/dnkl/foot/issues/552)). + around ([#552][552]). * Selection incorrectly wrapping rows ending with an explicit newline - ([#565](https://codeberg.org/dnkl/foot/issues/565)). + ([#565][565]). * Off-by-one error in markup of auto-detected URLs when the URL ends in the right-most column. * Multi-column characters being cut in half when resizing the alternate screen. * Restore `SIGHUP` in spawned processes. -* Text reflow performance ([#504](https://codeberg.org/dnkl/foot/issues/504)). +* Text reflow performance ([#504][504]). * IL+DL (`CSI Ps L` + `CSI Ps M`) now moves the cursor to column 0. * SS2 and SS3 (single shift) escape sequences behaving like locking - shifts ([#580](https://codeberg.org/dnkl/foot/issues/580)). + shifts ([#580][580]). * `TEXT`+`STRING`+`UTF8_STRING` mime types not being recognized in - clipboard offers ([#583](https://codeberg.org/dnkl/foot/issues/583)). + clipboard offers ([#583][583]). * Memory leak caused by custom box drawing glyphs not being completely freed when destroying a foot window instance - ([#586](https://codeberg.org/dnkl/foot/issues/586)). + ([#586][586]). * Crash in scrollback search when current XKB layout is missing _compose_ definitions. * Window title not being updated while window is hidden - ([#591](https://codeberg.org/dnkl/foot/issues/591)). + ([#591][591]). * Crash on badly formatted URIs in e.g. OSC-8 URLs. * Window being incorrectly resized on CSD/SSD run-time changes. @@ -893,41 +909,41 @@ supported. ### Added * URxvt OSC-11 extension to set background alpha - ([#436](https://codeberg.org/dnkl/foot/issues/436)). + ([#436][436]). * OSC 17/117/19/119 - change/reset selection background/foreground color. * `box-drawings-uses-font-glyphs=yes|no` option to `foot.ini` - ([#430](https://codeberg.org/dnkl/foot/issues/430)). + ([#430][430]). ### Changed * Underline cursor is now rendered below text underline - ([#415](https://codeberg.org/dnkl/foot/issues/415)). + ([#415][415]). * Foot now tries much harder to keep URL jump labels inside the window - geometry ([#443](https://codeberg.org/dnkl/foot/issues/443)). + geometry ([#443][443]). * `bold-text-in-bright` may now be set to `palette-based`, in which case it will use the corresponding bright palette color when the color to brighten matches one of the base 8 colors, instead of increasing the luminance - ([#449](https://codeberg.org/dnkl/foot/issues/449)). + ([#449][449]). ### Fixed * Reverted _"Consumed modifiers are no longer sent to the client - application"_ ([#425](https://codeberg.org/dnkl/foot/issues/425)). + application"_ ([#425][425]). * Crash caused by a double free originating in `XTSMGRAPHICS` - set number of color registers - ([#427](https://codeberg.org/dnkl/foot/issues/427)). + ([#427][427]). * Wrong action referenced in error message for key binding collisions - ([#432](https://codeberg.org/dnkl/foot/issues/432)). + ([#432][432]). * OSC 4/104 out-of-bounds accesses to the color table. This was the reason pywal turned foot windows transparent - ([#434](https://codeberg.org/dnkl/foot/issues/434)). + ([#434][434]). * PTY not being drained when the client application terminates. * `auto_left_margin` not being limited to `cub1` - ([#441](https://codeberg.org/dnkl/foot/issues/441)). + ([#441][441]). * Crash in scrollback search mode when searching beyond the last output. @@ -941,26 +957,26 @@ supported. ### Changed * Update PGO build instructions in `INSTALL.md` - ([#418](https://codeberg.org/dnkl/foot/issues/418)). + ([#418][418]). * In scrollback search mode, empty cells can now be matched by spaces. ### Fixed * Logic that repairs invalid key bindings ended up breaking valid key - bindings instead ([#407](https://codeberg.org/dnkl/foot/issues/407)). + bindings instead ([#407][407]). * Custom `line-height` settings now scale when increasing or decreasing the font size at run-time. * Newlines sometimes incorrectly inserted into copied text - ([#410](https://codeberg.org/dnkl/foot/issues/410)). + ([#410][410]). * Crash when compositor send `text-input-v3::enter` events without first having sent a `keyboard::enter` event - ([#411](https://codeberg.org/dnkl/foot/issues/411)). + ([#411][411]). * Deadlock when rendering sixel images. * URL labels, scrollback search box or scrollback position indicator sometimes not showing up, caused by invalidly sized surface buffers when output scaling was enabled - ([#409](https://codeberg.org/dnkl/foot/issues/409)). + ([#409][409]). * Empty sixels resulted in non-empty images. @@ -971,39 +987,39 @@ supported. * The `pad` option now accepts an optional third argument, `center` (e.g. `pad=5x5 center`), causing the grid to be centered in the window, with equal amount of padding of the left/right and - top/bottom side ([#273](https://codeberg.org/dnkl/foot/issues/273)). + top/bottom side ([#273][273]). * `line-height`, `letter-spacing`, `horizontal-letter-offset` and `vertical-letter-offset` to `foot.ini`. These options let you tweak cell size and glyph positioning - ([#244](https://codeberg.org/dnkl/foot/issues/244)). + ([#244][244]). * Key/mouse binding `select-extend-character-wise`, which forces the selection mode to 'character-wise' when extending a selection. * `DECSET` `47`, `1047` and `1048`. * URL detection and OSC-8 support. URLs are highlighted and activated using the keyboard (**no** mouse support). See **foot**(1)::URLs, or [README.md](README.md#urls) for details - ([#14](https://codeberg.org/dnkl/foot/issues/14)). + ([#14][14]). * `-d,--log-level={info|warning|error}` to both `foot` and - `footclient` ([#337](https://codeberg.org/dnkl/foot/issues/337)). + `footclient` ([#337][337]). * `-D,--working-directory=DIR` to both `foot` and `footclient` - ([#347](https://codeberg.org/dnkl/foot/issues/347)) + ([#347][347]) * `DECSET 80` - sixel scrolling - ([#361](https://codeberg.org/dnkl/foot/issues/361)). + ([#361][361]). * `DECSET 1070` - sixel private color palette - ([#362](https://codeberg.org/dnkl/foot/issues/362)). + ([#362][362]). * `DECSET 8452` - position cursor to the right of sixels - ([#363](https://codeberg.org/dnkl/foot/issues/363)). + ([#363][363]). * Man page **foot-ctlseqs**(7), documenting all supported escape - sequences ([#235](https://codeberg.org/dnkl/foot/issues/235)). + sequences ([#235][235]). * Support for transparent sixels (DCS parameter `P2=1`) - ([#391](https://codeberg.org/dnkl/foot/issues/391)). + ([#391][391]). * `-N,--no-wait` to `footclient` - ([#395](https://codeberg.org/dnkl/foot/issues/395)). + ([#395][395]). * Completions for Bash shell - ([#10](https://codeberg.org/dnkl/foot/issues/10)). + ([#10][10]). * Implement `XTVERSION` (`CSI > 0q`). Foot will reply with `DCS>|foot(..)ST` - ([#359](https://codeberg.org/dnkl/foot/issues/359)). + ([#359][359]). ### Changed @@ -1012,31 +1028,31 @@ supported. [wrap files](https://mesonbuild.com/Wrap-dependency-system-manual.html) instead of needing to be manually cloned. * Box drawing characters are now rendered by foot, instead of using - font glyphs ([#198](https://codeberg.org/dnkl/foot/issues/198)) + font glyphs ([#198][198]) * Double- or triple clicking then dragging now extends the selection - word- or line-wise ([#267](https://codeberg.org/dnkl/foot/issues/267)). + word- or line-wise ([#267][267]). * The line thickness of box drawing characters now depend on the font - size ([#281](https://codeberg.org/dnkl/foot/issues/281)). + size ([#281][281]). * Extending a word/line-wise selection now uses the original selection mode instead of switching to character-wise. * While doing an interactive resize of a foot window, foot now requires 100ms of idle time (where the window size does not change) before sending the new dimensions to the client application. The timing can be tweaked, or completely disabled, by setting - `resize-delay-ms` ([#301](https://codeberg.org/dnkl/foot/issues/301)). + `resize-delay-ms` ([#301][301]). * `CSI 13 ; 2 t` now reports (0,0). * Key binding matching logic; key combinations like `Control+Shift+C` **must** now be written as either `Control+C` or `Control+Shift+c`, the latter being the preferred - variant. ([#376](https://codeberg.org/dnkl/foot/issues/376)) + variant. ([#376][376]) * Consumed modifiers are no longer sent to the client application - ([#376](https://codeberg.org/dnkl/foot/issues/376)). + ([#376][376]). * The minimum version requirement for the libxkbcommon dependency is now 1.0.0. * Empty pixel rows at the bottom of a sixel is now trimmed. * Sixels with DCS parameter `P2=0|2` now use the _current_ ANSI background color for empty pixels instead of the default background - color ([#391](https://codeberg.org/dnkl/foot/issues/391)). + color ([#391][391]). * Sixel decoding optimized; up to 100% faster in some cases. * Reported sixel “max geometry” from current window size, to the configured maximum size (defaulting to 10000x10000). @@ -1055,7 +1071,7 @@ supported. application. This meant the mouse event was never seen by the application. * Terminals spawned with `ctrl`+`shift`+`n` not terminating when - exiting shell ([#366](https://codeberg.org/dnkl/foot/issues/366)). + exiting shell ([#366][366]). * Default value of `-t,--term` in `--help` output when foot was built without terminfo support. * Drain PTY when the client application terminates. @@ -1077,7 +1093,7 @@ supported. be used to configure which clipboard(s) selected text should be copied to. The default is `primary`, which corresponds to the behavior in older foot releases - ([#288](https://codeberg.org/dnkl/foot/issues/288)). + ([#288][288]). ### Changed @@ -1106,8 +1122,8 @@ supported. ### Added * Completions for fish shell - ([#11](https://codeberg.org/dnkl/foot/issues/11)) -* FreeBSD support ([#238](https://codeberg.org/dnkl/foot/issues/238)). + ([#11][11]) +* FreeBSD support ([#238][238]). * IME popup location support: foot now sends the location of the cursor so any popup can be displayed near the text that is being typed. @@ -1115,7 +1131,7 @@ supported. ### Changed * Trailing comments in `foot.ini` must now be preceded by a space or tab - ([#270](https://codeberg.org/dnkl/foot/issues/270)) + ([#270][270]) * The scrollback search box no longer accepts non-printable characters. * Non-formatting C0 control characters, `BS`, `HT` and `DEL` are now stripped from pasted text. @@ -1126,17 +1142,17 @@ supported. * Exit when the client application terminates, not when the TTY file descriptor is closed. * Crash on compositors not implementing the _text input_ interface - ([#259](https://codeberg.org/dnkl/foot/issues/259)). + ([#259][259]). * Erased, overflowing glyphs (when `tweak.allow-overflowing-double-width-glyphs=yes` - the default) not properly erasing the cell overflowed **into**. * `word-delimiters` option ignores `#` and subsequent characters - ([#270](https://codeberg.org/dnkl/foot/issues/270)) + ([#270][270]) * Combining characters not being rendered when composed with colored bitmap glyphs (i.e. colored emojis). * Pasting URIs from the clipboard when the source has not newline-terminated the last URI - ([#291](https://codeberg.org/dnkl/foot/issues/291)). + ([#291][291]). * Sixel “current geometry” query response not being bounded by the current window dimensions (fixes `lsix` output) * Crash on keyboard input when repeat rate was zero (i.e. no repeat). @@ -1185,7 +1201,7 @@ supported. * Missing dependencies in meson, causing heavily parallelized builds to fail. * Background color when alpha < 1.0 being wrong - ([#249](https://codeberg.org/dnkl/foot/issues/249)). + ([#249][249]). * `generate-alt-random.py` failing in containers. @@ -1209,7 +1225,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * IME support. This is compile-time optional, see [INSTALL.md](INSTALL.md#user-content-options) - ([#134](https://codeberg.org/dnkl/foot/issues/134)). + ([#134][134]). * `DECSET` escape to enable/disable IME: `CSI ? 737769 h` enables IME and `CSI ? 737769 l` disables it. This can be used to e.g. enable/disable IME when entering/leaving insert mode in vim. @@ -1219,11 +1235,11 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See sized using the scaling factor. DPI-only font sizing can be forced by setting `dpi-aware=yes`. Setting `dpi-aware=no` forces font sizing to be based on the scaling factor. - ([#206](https://codeberg.org/dnkl/foot/issues/206)). + ([#206][206]). * Implement reverse auto-wrap (_auto\_left\_margin_, _bw_, in terminfo). This mode can be enabled/disabled with `CSI ? 45 h` and `CSI ? 45 l`. It is **enabled** by default - ([#150](https://codeberg.org/dnkl/foot/issues/150)). + ([#150][150]). * `bell` option to `foot.ini`. Can be set to `set-urgency` to make foot render the margins in red when receiving `BEL` while **not** having keyboard focus. Applications can dynamically enable/disable @@ -1233,24 +1249,24 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See [proposal](https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/9) to add support for this. The value `set-urgency` was chosen for forward-compatibility, in the hopes that this proposal eventualizes - ([#157](https://codeberg.org/dnkl/foot/issues/157)). + ([#157][157]). * `bell` option can also be set to `notify`, in which case a desktop notification is emitted when foot receives `BEL` in an unfocused window. * `word-delimiters` option to `foot.ini` - ([#156](https://codeberg.org/dnkl/foot/issues/156)). + ([#156][156]). * `csd.preferred` can now be set to `none` to disable window decorations. Note that some compositors will render SSDs despite - this option being used ([#163](https://codeberg.org/dnkl/foot/issues/163)). + this option being used ([#163][163]). * Terminal content is now auto-scrolled when moving the mouse above or below the window while selecting - ([#149](https://codeberg.org/dnkl/foot/issues/149)). + ([#149][149]). * `font-bold`, `font-italic` `font-bold-italic` options to `foot.ini`. These options allow custom bold/italic fonts. They are unset by default, meaning the bold/italic version of the regular - font is used ([#169](https://codeberg.org/dnkl/foot/issues/169)). + font is used ([#169][169]). * Drag & drop support; text, files and URLs can now be dropped in a - foot terminal window ([#175](https://codeberg.org/dnkl/foot/issues/175)). + foot terminal window ([#175][175]). * `clipboard-paste` and `primary-paste` scrollback search bindings. By default, they are bound to `ctrl+v ctrl+y` and `shift+insert` respectively, and lets you paste from the clipboard or primary @@ -1258,12 +1274,12 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Support for `pipe-*` actions in mouse bindings. It was previously not possible to add a command to these actions when used in mouse bindings, making them useless - ([#183](https://codeberg.org/dnkl/foot/issues/183)). + ([#183][183]). * `bold-text-in-bright` option to `foot.ini`. When enabled, bold text is rendered in a brighter color - ([#199](https://codeberg.org/dnkl/foot/issues/199)). + ([#199][199]). * `-w,--window-size-pixels` and `-W,--window-size-chars` command line - options to `footclient` ([#189](https://codeberg.org/dnkl/foot/issues/189)). + options to `footclient` ([#189][189]). * Short command line options for `--title`, `--maximized`, `--fullscreen`, `--login-shell`, `--hold` and `--check-config`. * `DECSET` escape to modify the `escape` key to send `\E[27;1;27~` @@ -1271,10 +1287,10 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See 27127 l` disables it (the default). * OSC 777;notify: desktop notifications. Use in combination with the new `notify` option in `foot.ini` - ([#224](https://codeberg.org/dnkl/foot/issues/224)). + ([#224][224]). * Status line terminfo capabilities `hs`, `tsl`, `fsl` and `dsl`. This enables e.g. vim to set the window title - ([#242](https://codeberg.org/dnkl/foot/issues/242)). + ([#242][242]). ### Changed @@ -1285,11 +1301,11 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See window size. * Graphical glitches/flashes when resizing the window while running a fullscreen application, i.e. the 'alt' screen - ([#221](https://codeberg.org/dnkl/foot/issues/221)). + ([#221][221]). * Cursor will now blink if **either** `CSI ? 12 h` or `CSI Ps SP q` has been used to enable blinking. **cursor.blink** in `foot.ini` controls the default state of `CSI Ps SP q` - ([#218](https://codeberg.org/dnkl/foot/issues/218)). + ([#218][218]). * The sub-parameter versions of the SGR RGB color escapes (e.g `\E[38:2...m`) can now be used _without_ the color space ID parameter. @@ -1311,7 +1327,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See ### Fixed * Error when re-assigning a default key binding - ([#233](https://codeberg.org/dnkl/foot/issues/233)). + ([#233][233]). * `\E[s`+`\E[u` (save/restore cursor) now saves and restores attributes and charset configuration, just like `\E7`+`\E8`. * Report mouse motion events to the client application also while @@ -1339,7 +1355,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Num Lock by default overrides the keypad mode. See **foot.ini**(5)::KEYPAD, or [README.md](README.md#user-content-keypad) for details - ([#194](https://codeberg.org/dnkl/foot/issues/194)). + ([#194][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 @@ -1348,9 +1364,9 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See ### Fixed * Resize very slow when window is hidden - ([#190](https://codeberg.org/dnkl/foot/issues/190)). + ([#190][190]). * Key mappings for key combinations with `shift`+`tab` - ([#210](https://codeberg.org/dnkl/foot/issues/210)). + ([#210][210]). * Key mappings for key combinations with `alt`+`return`. * `footclient` `-m` (`--maximized`) flag being ignored. * Crash with explicitly sized sixels with a height less than 6 pixels. @@ -1368,18 +1384,18 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Crash when libxkbcommon cannot find a suitable libX11 _compose_ file. Note that foot will run, but without support for dead keys. - ([#170](https://codeberg.org/dnkl/foot/issues/170)). + ([#170][170]). * Restored window size when window is un-tiled. * XCursor shape in CSD corners when window is tiled. * Error handling when processing keyboard input (maybe - [#171](https://codeberg.org/dnkl/foot/issues/171)). + [#171][171]). * Compilation error _"overflow in conversion from long 'unsigned int' to 'int' changes value... "_ seen on platforms where the `request` argument in `ioctl(3)` is an `int` (for example: linux/ppc64). * Crash when using the mouse in alternate scroll mode in an unfocused - window ([#179](https://codeberg.org/dnkl/foot/issues/179)). + window ([#179][179]). * Character dropped from selection when "right-click-hold"-extending a - selection ([#180](https://codeberg.org/dnkl/foot/issues/180)). + selection ([#180][180]). ## 1.5.2 @@ -1387,7 +1403,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See ### Fixed * Regression: middle clicking double pastes in e.g. vim - ([#168](https://codeberg.org/dnkl/foot/issues/168)) + ([#168][168]) ## 1.5.1 @@ -1405,24 +1421,24 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Mouse bindings now match even if the actual click count is larger than specified in the binding. This allows you to, for example, quickly press the middle-button to paste multiple times - ([#146](https://codeberg.org/dnkl/foot/issues/146)). + ([#146][146]). * Color flashes when changing the color palette with OSC 4,10,11 - ([#141](https://codeberg.org/dnkl/foot/issues/141)). + ([#141][141]). * Scrollback position is now retained when resizing the window - ([#142](https://codeberg.org/dnkl/foot/issues/142)). + ([#142][142]). * Trackpad scrolling speed to better match the mouse scrolling speed, and to be consistent with other (Wayland) terminal emulators. Note that it is (much) slower compared to previous foot versions. Use the **scrollback.multiplier** option in `foot.ini` if you find the new - speed too slow ([#144](https://codeberg.org/dnkl/foot/issues/144)). + speed too slow ([#144][144]). * Crash when `foot.ini` contains an invalid section name - ([#159](https://codeberg.org/dnkl/foot/issues/159)). + ([#159][159]). * Background opacity when in _reverse video_ mode. * Crash when writing a sixel image that extends outside the terminal's - right margin ([#151](https://codeberg.org/dnkl/foot/issues/151)). + right margin ([#151][151]). * Sixel image at non-zero column positions getting sheared at seemingly random occasions - ([#151](https://codeberg.org/dnkl/foot/issues/151)). + ([#151][151]). * Crash after either resizing a window or changing the font size if there were sixels present in the scrollback while doing so. * _Send Device Attributes_ to only send a response if `Ps == 0`. @@ -1453,36 +1469,36 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Scrollback position indicator. This feature is optional and controlled by the **scrollback.indicator-position** and **scrollback.indicator-format** options in `foot.ini` - ([#42](https://codeberg.org/dnkl/foot/issues/42)). + ([#42][42]). * Key bindings in _scrollback search_ mode are now configurable. * `--check-config` command line option. * **pipe-selected** key binding. Works like **pipe-visible** and **pipe-scrollback**, but only pipes the currently selected text, if - any ([#51](https://codeberg.org/dnkl/foot/issues/51)). + any ([#51][51]). * **mouse.hide-when-typing** option to `foot.ini`. * **scrollback.multiplier** option to `foot.ini` - ([#54](https://codeberg.org/dnkl/foot/issues/54)). + ([#54][54]). * **colors.selection-foreground** and **colors.selection-background** options to `foot.ini`. * **tweak.render-timer** option to `foot.ini`. * Modifier support in mouse bindings - ([#77](https://codeberg.org/dnkl/foot/issues/77)). + ([#77][77]). * Click count support in mouse bindings, i.e double- and triple-click - ([#78](https://codeberg.org/dnkl/foot/issues/78)). + ([#78][78]). * All mouse actions (begin selection, select word, select row etc) are now configurable, via the new **select-begin**, **select-begin-block**, **select-extend**, **select-word**, **select-word-whitespace** and **select-row** options in the **mouse-bindings** section in `foot.ini` - ([#79](https://codeberg.org/dnkl/foot/issues/79)). + ([#79][79]). * Implement XTSAVE/XTRESTORE escape sequences, `CSI ? Ps s` and `CSI ? - Ps r` ([#91](https://codeberg.org/dnkl/foot/issues/91)). + Ps r` ([#91][91]). * `$COLORTERM` is now set to `truecolor` at startup, to indicate support for 24-bit RGB colors. * Experimental support for rendering double-width glyphs with a character width of 1. Must be explicitly enabled with `tweak.allow-overflowing-double-width-glyphs` - ([#116](https://codeberg.org/dnkl/foot/issues/116)). + ([#116][116]). * **initial-window-size-pixels** options to `foot.ini` and `-w,--window-size-pixels` command line option to `foot`. This option replaces the now deprecated **geometry** and `-g,--geometry` @@ -1493,7 +1509,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See alternative to **initial-window-size-pixels**. * **scrollback-up-half-page** and **scrollback-down-half-page** key bindings. They scroll up/down half of a page in the scrollback - ([#128](https://codeberg.org/dnkl/foot/issues/128)). + ([#128][128]). * **scrollback-up-line** and **scrollback-down-line** key bindings. They scroll up/down a single line in the scrollback. * **mouse.alternate-scroll-mode** option to `foot.ini`. This option @@ -1501,7 +1517,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See defaults to `yes`. When enabled, mouse scroll events are translated to up/down key events in the alternate screen, letting you scroll in e.g. `less` and other applications without enabling native mouse - support in them ([#135](https://codeberg.org/dnkl/foot/issues/135)). + support in them ([#135][135]). ### Changed @@ -1511,7 +1527,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See an error inside the terminal (and of course still log errors on stderr). * Default `--server` socket path to use `$WAYLAND_DISPLAY` instead of - `$XDG_SESSION_ID` ([#55](https://codeberg.org/dnkl/foot/issues/55)). + `$XDG_SESSION_ID` ([#55][55]). * Trailing empty cells are no longer highlighted in mouse selections. * Foot now searches for its configuration in `$XDG_DATA_DIRS/foot/foot.ini`, if no configuration is found in @@ -1532,22 +1548,22 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Compilation errors in 32-bit builds. * Mouse cursor style in top and left margins. * Selection is now **updated** when the cursor moves outside the grid - ([#70](https://codeberg.org/dnkl/foot/issues/70)). + ([#70][70]). * Viewport sometimes not moving when doing a scrollback search. * Crash when canceling a scrollback search and the window had been resized while searching. * Selection start point not moving when the selection changes direction. * OSC 10/11/104/110/111 (modify colors) did not update existing screen - content ([#94](https://codeberg.org/dnkl/foot/issues/94)). + content ([#94][94]). * Extra newlines when copying empty cells - ([#97](https://codeberg.org/dnkl/foot/issues/97)). + ([#97][97]). * Mouse events from being sent to client application when a mouse binding has consumed it. * Input events from getting mixed with paste data - ([#101](https://codeberg.org/dnkl/foot/issues/101)). + ([#101][101]). * Missing DPI values for “some” monitors on Gnome - ([#118](https://codeberg.org/dnkl/foot/issues/118)). + ([#118][118]). * Handling of multi-column composed characters while reflowing. * Escape sequences sent for key combinations with `Return`, that did **not** include `Alt`. @@ -1577,7 +1593,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Crash when starting a selection inside the margins. * Improved font size consistency across multiple monitors with - different DPI ([#47](https://codeberg.org/dnkl/foot/issues/47)). + different DPI ([#47][47]). * Handle trailing comments in `footrc` @@ -1617,7 +1633,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Crash in scrollback search. * Crash when a **pipe-visible** or **pipe-scrollback** command contained an unclosed quote - ([#49](https://codeberg.org/dnkl/foot/issues/49)). + ([#49][49]). ### Contributors @@ -1666,7 +1682,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Implemented `C0::FF` (form feed) * **pipe-visible** and **pipe-scrollback** key bindings. These let you pipe either the currently visible text, or the entire scrollback to - external tools ([#29](https://codeberg.org/dnkl/foot/issues/29)). Example: + external tools ([#29][29]). Example: `pipe-visible=[sh -c "xurls | bemenu | xargs -r firefox] Control+Print` @@ -1713,9 +1729,9 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See select half of a double-width character. * Draw hollow block cursor on top of character. * Set an initial `TIOCSWINSZ`. This ensures clients never read a - `0x0` terminal size ([#20](https://codeberg.org/dnkl/foot/issues/20)). + `0x0` terminal size ([#20][20]). * Glyphs overflowing into surrounding cells - ([#21](https://codeberg.org/dnkl/foot/issues/21)). + ([#21][21]). * Crash when last rendered cursor cell had scrolled off screen and `\E[J3` was executed. * Assert (debug builds) when an `\e]4` OSC escape was not followed by @@ -1735,7 +1751,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Sixel handling when resizing window. * Sixel handling when scrollback wraps around. * Foot now issues much fewer `wl_surface_damage_buffer()` calls - ([#35](https://codeberg.org/dnkl/foot/issues/35)). + ([#35][35]). * `C0::VT` to be processed as `C0::LF`. Previously, `C0::VT` would only move the cursor down, but never scroll. * `C0::HT` (_Horizontal Tab_, or `\t`) no longer clears `LCF` (_Last @@ -1748,7 +1764,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See now printed on the next line, instead of only printing half the character. * Font size can no longer be reduced to negative values - ([#38](https://codeberg.org/dnkl/foot/issues/38)). + ([#38][38]). ## 1.3.0 @@ -1756,7 +1772,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See ### Added * User configurable key- and mouse bindings. See `man 5 foot` and the - example `footrc` ([#1](https://codeberg.org/dnkl/foot/issues/1)) + example `footrc` ([#1][1]) * **initial-window-mode** option to `footrc`, that lets you control the initial mode for each newly spawned window: _windowed_, _maximized_ or _fullscreen_. @@ -1775,7 +1791,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Spaces no longer removed from zsh font name completions. * Default key binding for _spawn-terminal_ to ctrl+shift+n. * Renderer is now much faster with interactive scrolling - ([#4](https://codeberg.org/dnkl/foot/issues/4)) + ([#4][4]) * memfd sealing failures are no longer fatal errors. * Selection to no longer be cleared on resize. * The current monitor's subpixel order (RGB/BGR/V-RGB/V-BGR) is @@ -1836,9 +1852,9 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See ### Fixed * Window size doubling when moving window between outputs with - different scaling factors ([#3](https://codeberg.org/dnkl/foot/issues/3)). + different scaling factors ([#3][3]). * Font being too small on monitors with fractional scaling - ([#5](https://codeberg.org/dnkl/foot/issues/5)). + ([#5][5]). ## 1.2.1 From 61446df895b2a663e5bbd8dd53c8ac69fb4a5f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 22 Apr 2022 20:02:15 +0200 Subject: [PATCH 0004/1323] Revert "changelog: convert all issue links to reference links in the 1.12.0 release" This reverts commit 6652a836adb26ef55789bbd23113b158090451fd. We only added the actual links to the 1.12.0 release, meaning all other issue hyperlinks broke. --- CHANGELOG.md | 474 +++++++++++++++++++++++++-------------------------- 1 file changed, 229 insertions(+), 245 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cd2b99b..0f604b33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,43 +57,47 @@ * `[key-bindings].scrollback-home|end` options. * Socket activation for `foot --server` and accompanying systemd unit files * Support for re-mapping input, i.e. mapping input to custom escape - sequences ([#325][325]). -* Support for [DECNKM](https://vt100.net/docs/vt510-rm/DECNKM.html), - which allows setting/saving/restoring/querying the keypad mode. + sequences ([#325](https://codeberg.org/dnkl/foot/issues/325)). +* Support for [DECNKM](https://vt100.net/docs/vt510-rm/DECNKM.html), which + allows setting/saving/restoring/querying the keypad mode. * Sixel support can be disabled by setting `[tweak].sixel=no` - ([#950][950]). + ([#950](https://codeberg.org/dnkl/foot/issues/950)). * footclient: `-E,--client-environment` command line option. When used, the child process in the new terminal instance inherits the environment from the footclient process instead of the server’s - ([#1004][1004]). -* `[csd].hide-when-maximized=yes|no` option ([#1019][1019]). + ([#1004](https://codeberg.org/dnkl/foot/issues/1004)). +* `[csd].hide-when-maximized=yes|no` option + ([#1019](https://codeberg.org/dnkl/foot/issues/1019)). * Scrollback search mode now highlights all matches. * `[key-binding].show-urls-persistent` action. This key binding action is similar to `show-urls-launch`, but does not automatically exit - URL mode after activating an URL ([#964][964]). + URL mode after activating an URL + ([#964](https://codeberg.org/dnkl/foot/issues/964)). * Support for `CSI > 4 n`, disable _modifyOtherKeys_. Note that since foot only supports level 1 and 2 (and not level 0), this sequence does not disable _modifyOtherKeys_ completely, but simply reverts it back to level 1 (the default). * `-Dtests=false|true` meson command line option. When disabled, test binaries will neither be built, nor will `ninja test` attempt to - execute them. Enabled by default ([#919][919]). + execute them. Enabled by default + ([#919](https://codeberg.org/dnkl/foot/issues/919)). ### Changed * Minimum required meson version is now 0.58. * Mouse selections are now finalized when the window is resized - ([#922][922]). + ([#922](https://codeberg.org/dnkl/foot/issues/922)). * OSC-4 and OSC-11 replies now uses four digits instead of 2 - ([#971][971]). + ([#971](https://codeberg.org/dnkl/foot/issues/971)). * `\r` is no longer translated to `\n` when pasting clipboard data - ([#980][980]). + ([#980](https://codeberg.org/dnkl/foot/issues/980)). * Use circles for rendering light arc box-drawing characters - ([#988][988]). + ([#988](https://codeberg.org/dnkl/foot/issues/988)). * Example configuration is now installed to `${sysconfdir}/xdg/foot/foot.ini`, typically resolving to - `/etc/xdg/foot/foot.ini` ([#1001][1001]). + `/etc/xdg/foot/foot.ini` + ([#1001](https://codeberg.org/dnkl/foot/issues/1001)). ### Removed @@ -106,33 +110,37 @@ ### Fixed * Build: missing `wayland_client` dependency in `test-config` - ([#918][918]). + ([#918](https://codeberg.org/dnkl/foot/issues/918)). * “(null)” being logged as font-name (for some fonts) when warning about a non-monospaced primary font. * Rare crash when the window is resized while a mouse selection is - ongoing ([#922][922]). -* Large selections crossing the scrollback wrap-around ([#924][924]). -* Crash in `pipe-scrollback` ([#926][926]). + ongoing ([#922](https://codeberg.org/dnkl/foot/issues/922)). +* Large selections crossing the scrollback wrap-around + ([#924](https://codeberg.org/dnkl/foot/issues/924)). +* Crash in `pipe-scrollback` + ([#926](https://codeberg.org/dnkl/foot/issues/926)). * Exit code being 0 when a foot server with no open windows terminate - due to e.g. a Wayland connection failure ([#943][943]). + due to e.g. a Wayland connection failure + ([#943](https://codeberg.org/dnkl/foot/issues/943)). * Key binding collisions not detected for bindings specified as option overrides on the command line. -* Crash when seat has no keyboard ([#963][963]). +* Crash when seat has no keyboard + ([#963](https://codeberg.org/dnkl/foot/issues/963)). * Key presses with e.g. `AltGr` triggering key combinations with the - base symbol ([#983][983]). + base symbol ([#983](https://codeberg.org/dnkl/foot/issues/983)). * Underline cursor sometimes being positioned too low, either making it look thinner than what it should be, or being completely - invisible ([#1005][1005]). + invisible ([#1005](https://codeberg.org/dnkl/foot/issues/1005)). * Fallback to `/etc/xdg` if `XDG_CONFIG_DIRS` is unset - ([#1008][1008]). + ([#1008](https://codeberg.org/dnkl/foot/issues/1008)). * Improved compatibility with XTerm when `modifyOtherKeys=2` - ([#1009][1009]). + ([#1009](https://codeberg.org/dnkl/foot/issues/1009)). * Window geometry when CSDs are enabled and CSD border width set to a non-zero value. This fixes window snapping in e.g. GNOME. * Window size “jumping” when starting an interactive resize when CSDs are enabled, and CSD border width set to a non-zero value. * Key binding overrides on the command line having no effect with - `footclient` instances ([#931][931]). + `footclient` instances ([#931](https://codeberg.org/dnkl/foot/issues/931)). * Search prev/next not updating the selection correctly when the previous and new match overlaps. * Various minor fixes to scrollback search, and how it finds the @@ -154,30 +162,6 @@ * merkix -[325]: https://codeberg.org/dnkl/foot/issues/325 -[950]: https://codeberg.org/dnkl/foot/issues/950 -[1004]: https://codeberg.org/dnkl/foot/issues/1004 -[1019]: https://codeberg.org/dnkl/foot/issues/1019 -[964]: https://codeberg.org/dnkl/foot/issues/964 -[919]: https://codeberg.org/dnkl/foot/issues/919 -[922]: https://codeberg.org/dnkl/foot/issues/922 -[971]: https://codeberg.org/dnkl/foot/issues/971 -[980]: https://codeberg.org/dnkl/foot/issues/980 -[988]: https://codeberg.org/dnkl/foot/issues/988 -[1001]: https://codeberg.org/dnkl/foot/issues/1001 -[918]: https://codeberg.org/dnkl/foot/issues/918 -[922]: https://codeberg.org/dnkl/foot/issues/922 -[924]: https://codeberg.org/dnkl/foot/issues/924 -[926]: https://codeberg.org/dnkl/foot/issues/926 -[943]: https://codeberg.org/dnkl/foot/issues/943 -[963]: https://codeberg.org/dnkl/foot/issues/963 -[983]: https://codeberg.org/dnkl/foot/issues/983 -[1005]: https://codeberg.org/dnkl/foot/issues/1005 -[1008]: https://codeberg.org/dnkl/foot/issues/1008 -[1009]: https://codeberg.org/dnkl/foot/issues/1009 -[931]: https://codeberg.org/dnkl/foot/issues/931 - - ## 1.11.0 ### Added @@ -188,12 +172,12 @@ * _irc://_ and _ircs://_ to the default set of protocols recognized when auto-detecting URLs. * [SGR-Pixels (1016) mouse extended coordinates](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates) is now supported - ([#762][762]). + ([#762](https://codeberg.org/dnkl/foot/issues/762)). * `XTGETTCAP` - builtin terminfo. See [README.md::XTGETTCAP](README.md#xtgettcap) for details - ([#846][846]). + ([#846](https://codeberg.org/dnkl/foot/issues/846)). * `DECRQSS` - _Request Selection or Setting_ - ([#798][798]). Implemented settings + ([#798](https://codeberg.org/dnkl/foot/issues/798)). Implemented settings are: - `DECSTBM` - _Set Top and Bottom Margins_ - `SGR` - _Set Graphic Rendition_ @@ -209,7 +193,7 @@ theme names. * `[scrollback].multiplier` is now applied in “alternate scroll” mode, where scroll events are translated to fake arrow key presses on the - alt screen ([#859][859]). + 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. @@ -220,14 +204,14 @@ * `gettimeofday()` has been replaced with `clock_gettime()`, due to it being marked as obsolete by POSIX. * `alt+tab` now emits `ESC \t` instead of `CSI 27;3;9~` - ([#900][900]). + ([#900](https://codeberg.org/dnkl/foot/issues/900)). * File pasted, or dropped, on the alt screen is no longer quoted - ([#379][379]). + ([#379](https://codeberg.org/dnkl/foot/issues/379)). * Line-based selections now include a trailing newline when copied - ([#869][869]). + ([#869](https://codeberg.org/dnkl/foot/issues/869)). * Foot now clears the signal mask and resets all signal handlers to their default handlers at startup - ([#854][854]). + ([#854](https://codeberg.org/dnkl/foot/issues/854)). * `Copy` and `Paste` keycodes are supported by default for the clipboard. These are useful for keyboards with custom firmware like QMK to enable global copy/paste shortcuts that work inside and @@ -245,15 +229,15 @@ * Font size adjustment (“zooming”) when font is configured with a **pixelsize**, and `dpi-aware=no` - ([#842][842]). + ([#842](https://codeberg.org/dnkl/foot/issues/842)). * Key presses triggering keyboard layout switches also emitting CSI codes in the Kitty keyboard protocol. * Assertion in `shm.c:buffer_release()` - ([#844][844]). + ([#844](https://codeberg.org/dnkl/foot/issues/844)). * Crash when setting a key- or mouse binding to the empty string - ([#851][851]). + ([#851](https://codeberg.org/dnkl/foot/issues/851)). * Crash when maximizing the window and `[csd].size=1` - ([#857][857]). + ([#857](https://codeberg.org/dnkl/foot/issues/857)). * OSC-8 URIs not getting overwritten (erased) by double-width characters (e.g. emojis). * Rendering of CSD borders when `csd.border-width > 0` and desktop @@ -263,14 +247,14 @@ reset the scrollback view to the bottom. * Wrong mouse binding triggered when doing two mouse selections in very quick (< 300ms) succession - ([#883][883]). + ([#883](https://codeberg.org/dnkl/foot/issues/883)). * Bash completion giving an error when completing a list of short options * Sixel: large image resizes (triggered by e.g. large repeat counts in `DECGRI`) are now truncated instead of ignored. * Sixel: a repeat count of 0 in `DECGRI` now emits a single sixel. * LIGHT ARC box drawing characters incorrectly rendered - platforms ([#914][914]). + platforms ([#914](https://codeberg.org/dnkl/foot/issues/914)). ### Contributors @@ -291,7 +275,7 @@ ### Added -* Kitty keyboard protocol ([#319][319]): +* Kitty keyboard protocol ([#319](https://codeberg.org/dnkl/foot/issues/319)): - [Report event types](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-events) (mode `0b10`) - [Report alternate keys](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-alternates) @@ -305,7 +289,7 @@ ### Fixed * Crash when bitmap fonts are scaled down to very small font sizes - ([#830][830]). + ([#830](https://codeberg.org/dnkl/foot/issues/830)). * Crash when overwriting/erasing an OSC-8 URL. @@ -327,15 +311,15 @@ (for example by switching workspace while doing a mouse selection). * OSC-8 URIs in the last column * OSC-8 URIs sometimes being applied to too many, and seemingly - unrelated cells ([#816][816]). + unrelated cells ([#816](https://codeberg.org/dnkl/foot/issues/816)). * OSC-8 URIs incorrectly being dropped when resizing the terminal window with the alternate screen active. * CSD border not being dimmed when window is not focused. * Visual corruption with large CSD borders - ([#823][823]). + ([#823](https://codeberg.org/dnkl/foot/issues/823)). * Mouse cursor shape sometimes not being updated correctly. * Color palette changes (via OSC 4/104) no longer affect RGB colors - ([#678][678]). + ([#678](https://codeberg.org/dnkl/foot/issues/678)). ### Contributors @@ -355,14 +339,14 @@ ### Fixed * Regression: `letter-spacing` resulting in a “not a valid option” - error ([#795][795]). + error ([#795](https://codeberg.org/dnkl/foot/issues/795)). * Regression: bad section name in configuration error messages. * Regression: `pipe-*` key bindings not being parsed correctly, resulting in invalid error messages - ([#809][809]). + ([#809](https://codeberg.org/dnkl/foot/issues/809)). * OSC-8 data not being cleared when cell is overwritten - ([#804][804], - [#801][801]). + ([#804](https://codeberg.org/dnkl/foot/issues/804), + [#801](https://codeberg.org/dnkl/foot/issues/801)). ### Contributors @@ -384,13 +368,13 @@ foreground and background colors for the scrollback indicator. * `[key-bindings].noop` action. Key combinations assigned to this action will not be sent to the application - ([#765][765]). + ([#765](https://codeberg.org/dnkl/foot/issues/765)). * Color schemes are now installed to `${datadir}/foot/themes`. * `[csd].border-width` and `[csd].border-color`, allowing you to configure the width and color of the CSD border. * Support for `XTMODKEYS` with `Pp=4` and `Pv=2` (_modifyOtherKeys=2_). * `[colors].dim0-7` options, allowing you to configure custom “dim” - colors ([#776][776]). + colors ([#776](https://codeberg.org/dnkl/foot/issues/776)). ### Changed @@ -403,13 +387,13 @@ command line. * Foot now terminates if there are no available seats - for example, due to the compositor not implementing a recent enough version of - the `wl_seat` interface ([#779][779]). + the `wl_seat` interface ([#779](https://codeberg.org/dnkl/foot/issues/779)). * Boolean options in `foot.ini` are now limited to “yes|true|on|1|no|false|off|0”, Previously, anything that did not match “yes|true|on”, or a number greater than 0, was treated as “false”. * `[scrollback].multiplier` is no longer applied when the alternate - screen is in use ([#787][787]). + screen is in use ([#787](https://codeberg.org/dnkl/foot/issues/787)). ### Removed @@ -427,9 +411,9 @@ **effective** modifiers, like it should. * Fix crashes after enabling CSD at runtime when `csd.size` is 0. * Convert `\r` to `\n` when reading clipboard data - ([#752][752]). + ([#752](https://codeberg.org/dnkl/foot/issues/752)). * Clipboard occasionally ceasing to work, until window has been - re-focused ([#753][753]). + re-focused ([#753](https://codeberg.org/dnkl/foot/issues/753)). * Don’t propagate window title updates to the Wayland compositor unless the new title is different from the old title. @@ -452,7 +436,7 @@ * PGO helper scripts no longer set `LC_CTYPE=en_US.UTF-8`. But, note that “full” PGO builds still **require** a UTF-8 locale; you need to set one manually in your build script - ([#728][728]). + ([#728](https://codeberg.org/dnkl/foot/issues/728)). ## 1.9.1 @@ -461,15 +445,15 @@ * Warn when it appears the primary font is not monospaced. Can be disabled by setting `[tweak].font-monospace-warn=no` - ([#704][704]). + ([#704](https://codeberg.org/dnkl/foot/issues/704)). * PGO build scripts, in the `pgo` directory. See INSTALL.md - _Performance optimized, PGO_, for details - ([#701][701]). + ([#701](https://codeberg.org/dnkl/foot/issues/701)). * Braille characters (U+2800 - U+28FF) are now rendered by foot - itself ([#702][702]). + itself ([#702](https://codeberg.org/dnkl/foot/issues/702)). * `-e` command-line option. This option is simply ignored, to appease program launchers that blindly pass `-e` to any terminal emulator - ([#184][184]). + ([#184](https://codeberg.org/dnkl/foot/issues/184)). ### Changed @@ -484,7 +468,7 @@ changed back to `${datadir}/terminfo`. * `dpi-aware=auto`: fonts are now scaled using the monitor’s DPI only when **all** monitors have a scaling factor of one - ([#714][714]). + ([#714](https://codeberg.org/dnkl/foot/issues/714)). * fcft >= 3.0.0 in now required. @@ -492,9 +476,9 @@ * Added workaround for GNOME bug where multiple button press events (for the same button) is sent to the CSDs without any release or - leave events in between ([#709][709]). + leave events in between ([#709](https://codeberg.org/dnkl/foot/issues/709)). * Line-wise selection not taking soft line-wrapping into account - ([#726][726]). + ([#726](https://codeberg.org/dnkl/foot/issues/726)). ### Contributors @@ -508,24 +492,24 @@ ### Added * Window title in the CSDs - ([#638][638]). + ([#638](https://codeberg.org/dnkl/foot/issues/638)). * `-Ddocs=disabled|enabled|auto` meson command line option. * Support for `~`-expansion in the `include` directive - ([#659][659]). + ([#659](https://codeberg.org/dnkl/foot/issues/659)). * Unicode 13 characters U+1FB3C - U+1FB6F, U+1FB9A and U+1FB9B to list of box drawing characters rendered by foot itself (rather than using - font glyphs) ([#474][474]). + font glyphs) ([#474](https://codeberg.org/dnkl/foot/issues/474)). * `XM`+`xm` to terminfo. * Mouse buttons 6/7 (mouse wheel left/right). * `url.uri-characters` option to `foot.ini` - ([#654][654]). + ([#654](https://codeberg.org/dnkl/foot/issues/654)). ### Changed * Terminfo files can now co-exist with the foot terminfo files from ncurses. See `INSTALL.md` for more information - ([#671][671]). + ([#671](https://codeberg.org/dnkl/foot/issues/671)). * `bold-text-in-bright=palette-based` now only brightens colors from palette * Raised grace period between closing the PTY and sending `SIGKILL` (when terminating the client application) from 4 to 60 seconds. @@ -538,7 +522,7 @@ (`tweak.box-drawing-base-thickness`) in box drawing characters are now translated to pixel values using the monitor’s scaling factor when `dpi-aware=no`, or `dpi-aware=auto` and the scaling factor is - larger than 1 ([#680][680]). + larger than 1 ([#680](https://codeberg.org/dnkl/foot/issues/680)). * Spawning a new terminal with a working directory that does not exist is no longer a fatal error. @@ -549,23 +533,23 @@ 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][670]). + ([#670](https://codeberg.org/dnkl/foot/issues/670)). * Keypad application mode keys from terminfo; enabling the keypad application mode is not enough to make foot emit these sequences - you also need to disable private mode 1035 - ([#670][670]). + ([#670](https://codeberg.org/dnkl/foot/issues/670)). ### Fixed * Rendering into the right margin area with `tweak.overflowing-glyphs` enabled. -* PGO builds with clang ([#642][642]). +* PGO builds with clang ([#642](https://codeberg.org/dnkl/foot/issues/642)). * Crash in scrollback search mode when selection has been canceled due to terminal content updates - ([#644][644]). + ([#644](https://codeberg.org/dnkl/foot/issues/644)). * Foot process not terminating when the Wayland connection is broken - ([#651][651]). + ([#651](https://codeberg.org/dnkl/foot/issues/651)). * Output scale being zero on compositors that does not advertise a scaling factor. * Slow-to-terminate client applications causing other footclient instances to @@ -573,10 +557,10 @@ * Underlying cell content showing through in the left-most column of sixels. * `cursor.blink` not working in GNOME - ([#686][686]). + ([#686](https://codeberg.org/dnkl/foot/issues/686)). * Blinking cursor stops blinking, or becoming invisible, when switching focus from, and then back to a terminal window on GNOME - ([#686][686]). + ([#686](https://codeberg.org/dnkl/foot/issues/686)). ### Contributors @@ -591,10 +575,10 @@ ### Added * `locked-title=no|yes` to `foot.ini` - ([#386][386]). + ([#386](https://codeberg.org/dnkl/foot/issues/386)). * `tweak.overflowing-glyphs` option, which can be enabled to fix rendering issues with glyphs of any width that appear cut-off - ([#592][592]). + ([#592](https://codeberg.org/dnkl/foot/issues/592)). ### Changed @@ -602,7 +586,7 @@ * Non-empty lines are now considered to have a hard linebreak, _unless_ an actual word-wrap is inserted. * Setting `DECSDM` now _disables_ sixel scrolling, while resetting it - _enables_ scrolling ([#631][631]). + _enables_ scrolling ([#631](https://codeberg.org/dnkl/foot/issues/631)). ### Removed @@ -617,7 +601,7 @@ * FD exhaustion when repeatedly entering/exiting URL mode with many URLs. * Double free of URL while removing duplicated and/or overlapping URLs - in URL mode ([#627][627]). + in URL mode ([#627](https://codeberg.org/dnkl/foot/issues/627)). * Crash when an unclosed OSC-8 URL ran into un-allocated scrollback rows. * Some box-drawing characters were rendered incorrectly on big-endian @@ -629,7 +613,7 @@ * Reduced memory usage in URL mode. * Crash when the `E3` escape (`\E[3J`) was executed, and there was a selection, or sixel image, in the scrollback - ([#633][633]). + ([#633](https://codeberg.org/dnkl/foot/issues/633)). ### Contributors @@ -645,7 +629,7 @@ * `Tc`, `setrgbf` and `setrgbb` capabilities in `foot` and `foot-direct` terminfo entries. This should make 24-bit RGB colors work in tmux and neovim, without the need for config hacks or detection heuristics - ([#615][615]). + ([#615](https://codeberg.org/dnkl/foot/issues/615)). ### Changed @@ -660,7 +644,7 @@ * Grapheme cluster state being reset between codepoints. * Regression: custom URL key bindings not working - ([#614][614]). + ([#614](https://codeberg.org/dnkl/foot/issues/614)). ### Contributors @@ -737,45 +721,45 @@ supported. * Support for DECSET/DECRST 2026, as an alternative to the existing "synchronized updates" DCS sequences - ([#459][459]). + ([#459](https://codeberg.org/dnkl/foot/issues/459)). * `cursor.beam-thickness` option to `foot.ini` - ([#464][464]). + ([#464](https://codeberg.org/dnkl/foot/issues/464)). * `cursor.underline-thickness` option to `foot.ini` - ([#524][524]). + ([#524](https://codeberg.org/dnkl/foot/issues/524)). * Unicode 13 characters U+1FB70 - U+1FB8B to list of box drawing characters rendered by foot itself (rather than using font glyphs) - ([#471][471]). + ([#471](https://codeberg.org/dnkl/foot/issues/471)). * Dedicated `[bell]` section to config, supporting multiple actions and a new `command` action to run an arbitrary command. (https://codeberg.org/dnkl/foot/pulls/483) * Dedicated `[url]` section to config. * `[url].protocols` option to `foot.ini` - ([#531][531]). + ([#531](https://codeberg.org/dnkl/foot/issues/531)). * Support for setting the full 256 color palette in foot.ini - ([#489][489]) + ([#489](https://codeberg.org/dnkl/foot/issues/489)) * XDG activation support, will be used by `[bell].urgent` when available (falling back to coloring the window margins red when - unavailable) ([#487][487]). + unavailable) ([#487](https://codeberg.org/dnkl/foot/issues/487)). * `ctrl`+`c` as a default key binding; to cancel search/url mode. * `${window-title}` to `notify`. * Support for including files in `foot.ini` - ([#555][555]). + ([#555](https://codeberg.org/dnkl/foot/issues/555)). * `ENVIRONMENT` section in **foot**(1) and **footclient**(1) man pages - ([#556][556]). + ([#556](https://codeberg.org/dnkl/foot/issues/556)). * `tweak.pua-double-width` option to `foot.ini`, letting you force _Private Usage Area_ codepoints to be treated as double-width characters. * OSC 9 desktop notifications (iTerm2 compatible). * Support for LS2 and LS3 (locking shift) escape sequences - ([#581][581]). + ([#581](https://codeberg.org/dnkl/foot/issues/581)). * Support for overriding configuration options on the command line - ([#554][554], - [#600][600]). + ([#554](https://codeberg.org/dnkl/foot/issues/554), + [#600](https://codeberg.org/dnkl/foot/issues/600)). * `underline-offset` option to `foot.ini` - ([#490][490]). + ([#490](https://codeberg.org/dnkl/foot/issues/490)). * `csd.button-color` option to `foot.ini`. * `-Dterminfo-install-location=disabled|` meson command - line option ([#569][569]). + line option ([#569](https://codeberg.org/dnkl/foot/issues/569)). ### Changed @@ -788,7 +772,7 @@ supported. * The background color of highlighted text is now adjusted, when the foreground and background colors are the same, making the highlighted text legible - ([#455][455]). + ([#455](https://codeberg.org/dnkl/foot/issues/455)). * `cursor.style=bar` to `cursor.style=beam`. `bar` remains a recognized value, but will eventually be deprecated, and removed. * Point values in `line-height`, `letter-spacing`, @@ -799,28 +783,28 @@ supported. 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][466]). + ([#466](https://codeberg.org/dnkl/foot/issues/466)). * Background alpha no longer applied to palette or RGB colors that matches the background color. * Improved performance on compositors that does not release shm buffers immediately, e.g. KWin - ([#478][478]). + ([#478](https://codeberg.org/dnkl/foot/issues/478)). * `ctrl + w` (_extend-to-word-boundary_) can now be used across lines - ([#421][421]). + ([#421](https://codeberg.org/dnkl/foot/issues/421)). * Ignore auto-detected URLs that overlap with OSC-8 URLs. * Default value for the `notify` option to use `-a ${app-id} -i ${app-id} ...` instead of `-a foot -i foot ...`. * `scrollback-*`+`pipe-scrollback` key bindings are now passed through to the client application when the alt screen is active - ([#573][573]). + ([#573](https://codeberg.org/dnkl/foot/issues/573)). * Reverse video (`\E[?5h`) now only swaps the default foreground and background colors. Cells with explicit foreground and/or background colors remain unchanged. * Tabs (`\t`) are now preserved when the window is resized, and when - copying text ([#508][508]). + copying text ([#508](https://codeberg.org/dnkl/foot/issues/508)). * Writing a sixel on top of another sixel no longer erases the first sixel, but the two are instead blended - ([#562][562]). + ([#562](https://codeberg.org/dnkl/foot/issues/562)). * Running foot without a configuration file is no longer an error; it has been demoted to a warning, and is no longer presented as a notification in the terminal window, but only logged on stderr. @@ -848,53 +832,53 @@ supported. * `generate-alt-random-writes.py --sixel` sometimes crashing, resulting in PGO build failures. * Wrong colors in the 256-color cube - ([#479][479]). + ([#479](https://codeberg.org/dnkl/foot/issues/479)). * Memory leak triggered by “opening” an OSC-8 URI and then resetting the terminal without closing the URI - ([#495][495]). + ([#495](https://codeberg.org/dnkl/foot/issues/495)). * Assertion when emitting a sixel occupying the entire scrollback - history ([#494][494]). + history ([#494](https://codeberg.org/dnkl/foot/issues/494)). * Font underlines being positioned below the cell (and thus being invisible) for certain combinations of fonts and font sizes - ([#503][503]). + ([#503](https://codeberg.org/dnkl/foot/issues/503)). * Sixels with transparent bottom border being resized below the size specified in _”Set Raster Attributes”_. * Fonts sometimes not being reloaded with the correct scaling factor when `dpi-aware=no`, or `dpi-aware=auto` with monitor(s) with a - scaling factor > 1 ([#509][509]). + scaling factor > 1 ([#509](https://codeberg.org/dnkl/foot/issues/509)). * Crash caused by certain CSI sequences with very large parameter - values ([#522][522]). + values ([#522](https://codeberg.org/dnkl/foot/issues/522)). * Rare occurrences where the window did not close when the shell exited. Only seen on FreeBSD - ([#534][534]) + ([#534](https://codeberg.org/dnkl/foot/issues/534)) * Foot process(es) sometimes remaining, using 100% CPU, when closing multiple foot windows at the same time - ([#542][542]). + ([#542](https://codeberg.org/dnkl/foot/issues/542)). * Regression where `+shift+tab` always produced `\E[Z` instead of the correct `\E[27;;9~` sequence - ([#547][547]). + ([#547](https://codeberg.org/dnkl/foot/issues/547)). * Crash when a line wrapping OSC-8 URI crossed the scrollback wrap - around ([#552][552]). + around ([#552](https://codeberg.org/dnkl/foot/issues/552)). * Selection incorrectly wrapping rows ending with an explicit newline - ([#565][565]). + ([#565](https://codeberg.org/dnkl/foot/issues/565)). * Off-by-one error in markup of auto-detected URLs when the URL ends in the right-most column. * Multi-column characters being cut in half when resizing the alternate screen. * Restore `SIGHUP` in spawned processes. -* Text reflow performance ([#504][504]). +* Text reflow performance ([#504](https://codeberg.org/dnkl/foot/issues/504)). * IL+DL (`CSI Ps L` + `CSI Ps M`) now moves the cursor to column 0. * SS2 and SS3 (single shift) escape sequences behaving like locking - shifts ([#580][580]). + shifts ([#580](https://codeberg.org/dnkl/foot/issues/580)). * `TEXT`+`STRING`+`UTF8_STRING` mime types not being recognized in - clipboard offers ([#583][583]). + clipboard offers ([#583](https://codeberg.org/dnkl/foot/issues/583)). * Memory leak caused by custom box drawing glyphs not being completely freed when destroying a foot window instance - ([#586][586]). + ([#586](https://codeberg.org/dnkl/foot/issues/586)). * Crash in scrollback search when current XKB layout is missing _compose_ definitions. * Window title not being updated while window is hidden - ([#591][591]). + ([#591](https://codeberg.org/dnkl/foot/issues/591)). * Crash on badly formatted URIs in e.g. OSC-8 URLs. * Window being incorrectly resized on CSD/SSD run-time changes. @@ -909,41 +893,41 @@ supported. ### Added * URxvt OSC-11 extension to set background alpha - ([#436][436]). + ([#436](https://codeberg.org/dnkl/foot/issues/436)). * OSC 17/117/19/119 - change/reset selection background/foreground color. * `box-drawings-uses-font-glyphs=yes|no` option to `foot.ini` - ([#430][430]). + ([#430](https://codeberg.org/dnkl/foot/issues/430)). ### Changed * Underline cursor is now rendered below text underline - ([#415][415]). + ([#415](https://codeberg.org/dnkl/foot/issues/415)). * Foot now tries much harder to keep URL jump labels inside the window - geometry ([#443][443]). + geometry ([#443](https://codeberg.org/dnkl/foot/issues/443)). * `bold-text-in-bright` may now be set to `palette-based`, in which case it will use the corresponding bright palette color when the color to brighten matches one of the base 8 colors, instead of increasing the luminance - ([#449][449]). + ([#449](https://codeberg.org/dnkl/foot/issues/449)). ### Fixed * Reverted _"Consumed modifiers are no longer sent to the client - application"_ ([#425][425]). + application"_ ([#425](https://codeberg.org/dnkl/foot/issues/425)). * Crash caused by a double free originating in `XTSMGRAPHICS` - set number of color registers - ([#427][427]). + ([#427](https://codeberg.org/dnkl/foot/issues/427)). * Wrong action referenced in error message for key binding collisions - ([#432][432]). + ([#432](https://codeberg.org/dnkl/foot/issues/432)). * OSC 4/104 out-of-bounds accesses to the color table. This was the reason pywal turned foot windows transparent - ([#434][434]). + ([#434](https://codeberg.org/dnkl/foot/issues/434)). * PTY not being drained when the client application terminates. * `auto_left_margin` not being limited to `cub1` - ([#441][441]). + ([#441](https://codeberg.org/dnkl/foot/issues/441)). * Crash in scrollback search mode when searching beyond the last output. @@ -957,26 +941,26 @@ supported. ### Changed * Update PGO build instructions in `INSTALL.md` - ([#418][418]). + ([#418](https://codeberg.org/dnkl/foot/issues/418)). * In scrollback search mode, empty cells can now be matched by spaces. ### Fixed * Logic that repairs invalid key bindings ended up breaking valid key - bindings instead ([#407][407]). + bindings instead ([#407](https://codeberg.org/dnkl/foot/issues/407)). * Custom `line-height` settings now scale when increasing or decreasing the font size at run-time. * Newlines sometimes incorrectly inserted into copied text - ([#410][410]). + ([#410](https://codeberg.org/dnkl/foot/issues/410)). * Crash when compositor send `text-input-v3::enter` events without first having sent a `keyboard::enter` event - ([#411][411]). + ([#411](https://codeberg.org/dnkl/foot/issues/411)). * Deadlock when rendering sixel images. * URL labels, scrollback search box or scrollback position indicator sometimes not showing up, caused by invalidly sized surface buffers when output scaling was enabled - ([#409][409]). + ([#409](https://codeberg.org/dnkl/foot/issues/409)). * Empty sixels resulted in non-empty images. @@ -987,39 +971,39 @@ supported. * The `pad` option now accepts an optional third argument, `center` (e.g. `pad=5x5 center`), causing the grid to be centered in the window, with equal amount of padding of the left/right and - top/bottom side ([#273][273]). + top/bottom side ([#273](https://codeberg.org/dnkl/foot/issues/273)). * `line-height`, `letter-spacing`, `horizontal-letter-offset` and `vertical-letter-offset` to `foot.ini`. These options let you tweak cell size and glyph positioning - ([#244][244]). + ([#244](https://codeberg.org/dnkl/foot/issues/244)). * Key/mouse binding `select-extend-character-wise`, which forces the selection mode to 'character-wise' when extending a selection. * `DECSET` `47`, `1047` and `1048`. * URL detection and OSC-8 support. URLs are highlighted and activated using the keyboard (**no** mouse support). See **foot**(1)::URLs, or [README.md](README.md#urls) for details - ([#14][14]). + ([#14](https://codeberg.org/dnkl/foot/issues/14)). * `-d,--log-level={info|warning|error}` to both `foot` and - `footclient` ([#337][337]). + `footclient` ([#337](https://codeberg.org/dnkl/foot/issues/337)). * `-D,--working-directory=DIR` to both `foot` and `footclient` - ([#347][347]) + ([#347](https://codeberg.org/dnkl/foot/issues/347)) * `DECSET 80` - sixel scrolling - ([#361][361]). + ([#361](https://codeberg.org/dnkl/foot/issues/361)). * `DECSET 1070` - sixel private color palette - ([#362][362]). + ([#362](https://codeberg.org/dnkl/foot/issues/362)). * `DECSET 8452` - position cursor to the right of sixels - ([#363][363]). + ([#363](https://codeberg.org/dnkl/foot/issues/363)). * Man page **foot-ctlseqs**(7), documenting all supported escape - sequences ([#235][235]). + sequences ([#235](https://codeberg.org/dnkl/foot/issues/235)). * Support for transparent sixels (DCS parameter `P2=1`) - ([#391][391]). + ([#391](https://codeberg.org/dnkl/foot/issues/391)). * `-N,--no-wait` to `footclient` - ([#395][395]). + ([#395](https://codeberg.org/dnkl/foot/issues/395)). * Completions for Bash shell - ([#10][10]). + ([#10](https://codeberg.org/dnkl/foot/issues/10)). * Implement `XTVERSION` (`CSI > 0q`). Foot will reply with `DCS>|foot(..)ST` - ([#359][359]). + ([#359](https://codeberg.org/dnkl/foot/issues/359)). ### Changed @@ -1028,31 +1012,31 @@ supported. [wrap files](https://mesonbuild.com/Wrap-dependency-system-manual.html) instead of needing to be manually cloned. * Box drawing characters are now rendered by foot, instead of using - font glyphs ([#198][198]) + font glyphs ([#198](https://codeberg.org/dnkl/foot/issues/198)) * Double- or triple clicking then dragging now extends the selection - word- or line-wise ([#267][267]). + word- or line-wise ([#267](https://codeberg.org/dnkl/foot/issues/267)). * The line thickness of box drawing characters now depend on the font - size ([#281][281]). + size ([#281](https://codeberg.org/dnkl/foot/issues/281)). * Extending a word/line-wise selection now uses the original selection mode instead of switching to character-wise. * While doing an interactive resize of a foot window, foot now requires 100ms of idle time (where the window size does not change) before sending the new dimensions to the client application. The timing can be tweaked, or completely disabled, by setting - `resize-delay-ms` ([#301][301]). + `resize-delay-ms` ([#301](https://codeberg.org/dnkl/foot/issues/301)). * `CSI 13 ; 2 t` now reports (0,0). * Key binding matching logic; key combinations like `Control+Shift+C` **must** now be written as either `Control+C` or `Control+Shift+c`, the latter being the preferred - variant. ([#376][376]) + variant. ([#376](https://codeberg.org/dnkl/foot/issues/376)) * Consumed modifiers are no longer sent to the client application - ([#376][376]). + ([#376](https://codeberg.org/dnkl/foot/issues/376)). * The minimum version requirement for the libxkbcommon dependency is now 1.0.0. * Empty pixel rows at the bottom of a sixel is now trimmed. * Sixels with DCS parameter `P2=0|2` now use the _current_ ANSI background color for empty pixels instead of the default background - color ([#391][391]). + color ([#391](https://codeberg.org/dnkl/foot/issues/391)). * Sixel decoding optimized; up to 100% faster in some cases. * Reported sixel “max geometry” from current window size, to the configured maximum size (defaulting to 10000x10000). @@ -1071,7 +1055,7 @@ supported. application. This meant the mouse event was never seen by the application. * Terminals spawned with `ctrl`+`shift`+`n` not terminating when - exiting shell ([#366][366]). + exiting shell ([#366](https://codeberg.org/dnkl/foot/issues/366)). * Default value of `-t,--term` in `--help` output when foot was built without terminfo support. * Drain PTY when the client application terminates. @@ -1093,7 +1077,7 @@ supported. be used to configure which clipboard(s) selected text should be copied to. The default is `primary`, which corresponds to the behavior in older foot releases - ([#288][288]). + ([#288](https://codeberg.org/dnkl/foot/issues/288)). ### Changed @@ -1122,8 +1106,8 @@ supported. ### Added * Completions for fish shell - ([#11][11]) -* FreeBSD support ([#238][238]). + ([#11](https://codeberg.org/dnkl/foot/issues/11)) +* FreeBSD support ([#238](https://codeberg.org/dnkl/foot/issues/238)). * IME popup location support: foot now sends the location of the cursor so any popup can be displayed near the text that is being typed. @@ -1131,7 +1115,7 @@ supported. ### Changed * Trailing comments in `foot.ini` must now be preceded by a space or tab - ([#270][270]) + ([#270](https://codeberg.org/dnkl/foot/issues/270)) * The scrollback search box no longer accepts non-printable characters. * Non-formatting C0 control characters, `BS`, `HT` and `DEL` are now stripped from pasted text. @@ -1142,17 +1126,17 @@ supported. * Exit when the client application terminates, not when the TTY file descriptor is closed. * Crash on compositors not implementing the _text input_ interface - ([#259][259]). + ([#259](https://codeberg.org/dnkl/foot/issues/259)). * Erased, overflowing glyphs (when `tweak.allow-overflowing-double-width-glyphs=yes` - the default) not properly erasing the cell overflowed **into**. * `word-delimiters` option ignores `#` and subsequent characters - ([#270][270]) + ([#270](https://codeberg.org/dnkl/foot/issues/270)) * Combining characters not being rendered when composed with colored bitmap glyphs (i.e. colored emojis). * Pasting URIs from the clipboard when the source has not newline-terminated the last URI - ([#291][291]). + ([#291](https://codeberg.org/dnkl/foot/issues/291)). * Sixel “current geometry” query response not being bounded by the current window dimensions (fixes `lsix` output) * Crash on keyboard input when repeat rate was zero (i.e. no repeat). @@ -1201,7 +1185,7 @@ supported. * Missing dependencies in meson, causing heavily parallelized builds to fail. * Background color when alpha < 1.0 being wrong - ([#249][249]). + ([#249](https://codeberg.org/dnkl/foot/issues/249)). * `generate-alt-random.py` failing in containers. @@ -1225,7 +1209,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * IME support. This is compile-time optional, see [INSTALL.md](INSTALL.md#user-content-options) - ([#134][134]). + ([#134](https://codeberg.org/dnkl/foot/issues/134)). * `DECSET` escape to enable/disable IME: `CSI ? 737769 h` enables IME and `CSI ? 737769 l` disables it. This can be used to e.g. enable/disable IME when entering/leaving insert mode in vim. @@ -1235,11 +1219,11 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See sized using the scaling factor. DPI-only font sizing can be forced by setting `dpi-aware=yes`. Setting `dpi-aware=no` forces font sizing to be based on the scaling factor. - ([#206][206]). + ([#206](https://codeberg.org/dnkl/foot/issues/206)). * Implement reverse auto-wrap (_auto\_left\_margin_, _bw_, in terminfo). This mode can be enabled/disabled with `CSI ? 45 h` and `CSI ? 45 l`. It is **enabled** by default - ([#150][150]). + ([#150](https://codeberg.org/dnkl/foot/issues/150)). * `bell` option to `foot.ini`. Can be set to `set-urgency` to make foot render the margins in red when receiving `BEL` while **not** having keyboard focus. Applications can dynamically enable/disable @@ -1249,24 +1233,24 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See [proposal](https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/9) to add support for this. The value `set-urgency` was chosen for forward-compatibility, in the hopes that this proposal eventualizes - ([#157][157]). + ([#157](https://codeberg.org/dnkl/foot/issues/157)). * `bell` option can also be set to `notify`, in which case a desktop notification is emitted when foot receives `BEL` in an unfocused window. * `word-delimiters` option to `foot.ini` - ([#156][156]). + ([#156](https://codeberg.org/dnkl/foot/issues/156)). * `csd.preferred` can now be set to `none` to disable window decorations. Note that some compositors will render SSDs despite - this option being used ([#163][163]). + this option being used ([#163](https://codeberg.org/dnkl/foot/issues/163)). * Terminal content is now auto-scrolled when moving the mouse above or below the window while selecting - ([#149][149]). + ([#149](https://codeberg.org/dnkl/foot/issues/149)). * `font-bold`, `font-italic` `font-bold-italic` options to `foot.ini`. These options allow custom bold/italic fonts. They are unset by default, meaning the bold/italic version of the regular - font is used ([#169][169]). + font is used ([#169](https://codeberg.org/dnkl/foot/issues/169)). * Drag & drop support; text, files and URLs can now be dropped in a - foot terminal window ([#175][175]). + foot terminal window ([#175](https://codeberg.org/dnkl/foot/issues/175)). * `clipboard-paste` and `primary-paste` scrollback search bindings. By default, they are bound to `ctrl+v ctrl+y` and `shift+insert` respectively, and lets you paste from the clipboard or primary @@ -1274,12 +1258,12 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Support for `pipe-*` actions in mouse bindings. It was previously not possible to add a command to these actions when used in mouse bindings, making them useless - ([#183][183]). + ([#183](https://codeberg.org/dnkl/foot/issues/183)). * `bold-text-in-bright` option to `foot.ini`. When enabled, bold text is rendered in a brighter color - ([#199][199]). + ([#199](https://codeberg.org/dnkl/foot/issues/199)). * `-w,--window-size-pixels` and `-W,--window-size-chars` command line - options to `footclient` ([#189][189]). + options to `footclient` ([#189](https://codeberg.org/dnkl/foot/issues/189)). * Short command line options for `--title`, `--maximized`, `--fullscreen`, `--login-shell`, `--hold` and `--check-config`. * `DECSET` escape to modify the `escape` key to send `\E[27;1;27~` @@ -1287,10 +1271,10 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See 27127 l` disables it (the default). * OSC 777;notify: desktop notifications. Use in combination with the new `notify` option in `foot.ini` - ([#224][224]). + ([#224](https://codeberg.org/dnkl/foot/issues/224)). * Status line terminfo capabilities `hs`, `tsl`, `fsl` and `dsl`. This enables e.g. vim to set the window title - ([#242][242]). + ([#242](https://codeberg.org/dnkl/foot/issues/242)). ### Changed @@ -1301,11 +1285,11 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See window size. * Graphical glitches/flashes when resizing the window while running a fullscreen application, i.e. the 'alt' screen - ([#221][221]). + ([#221](https://codeberg.org/dnkl/foot/issues/221)). * Cursor will now blink if **either** `CSI ? 12 h` or `CSI Ps SP q` has been used to enable blinking. **cursor.blink** in `foot.ini` controls the default state of `CSI Ps SP q` - ([#218][218]). + ([#218](https://codeberg.org/dnkl/foot/issues/218)). * The sub-parameter versions of the SGR RGB color escapes (e.g `\E[38:2...m`) can now be used _without_ the color space ID parameter. @@ -1327,7 +1311,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See ### Fixed * Error when re-assigning a default key binding - ([#233][233]). + ([#233](https://codeberg.org/dnkl/foot/issues/233)). * `\E[s`+`\E[u` (save/restore cursor) now saves and restores attributes and charset configuration, just like `\E7`+`\E8`. * Report mouse motion events to the client application also while @@ -1355,7 +1339,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Num Lock by default overrides the keypad mode. See **foot.ini**(5)::KEYPAD, or [README.md](README.md#user-content-keypad) for details - ([#194][194]). + ([#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 @@ -1364,9 +1348,9 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See ### Fixed * Resize very slow when window is hidden - ([#190][190]). + ([#190](https://codeberg.org/dnkl/foot/issues/190)). * Key mappings for key combinations with `shift`+`tab` - ([#210][210]). + ([#210](https://codeberg.org/dnkl/foot/issues/210)). * Key mappings for key combinations with `alt`+`return`. * `footclient` `-m` (`--maximized`) flag being ignored. * Crash with explicitly sized sixels with a height less than 6 pixels. @@ -1384,18 +1368,18 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Crash when libxkbcommon cannot find a suitable libX11 _compose_ file. Note that foot will run, but without support for dead keys. - ([#170][170]). + ([#170](https://codeberg.org/dnkl/foot/issues/170)). * Restored window size when window is un-tiled. * XCursor shape in CSD corners when window is tiled. * Error handling when processing keyboard input (maybe - [#171][171]). + [#171](https://codeberg.org/dnkl/foot/issues/171)). * Compilation error _"overflow in conversion from long 'unsigned int' to 'int' changes value... "_ seen on platforms where the `request` argument in `ioctl(3)` is an `int` (for example: linux/ppc64). * Crash when using the mouse in alternate scroll mode in an unfocused - window ([#179][179]). + window ([#179](https://codeberg.org/dnkl/foot/issues/179)). * Character dropped from selection when "right-click-hold"-extending a - selection ([#180][180]). + selection ([#180](https://codeberg.org/dnkl/foot/issues/180)). ## 1.5.2 @@ -1403,7 +1387,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See ### Fixed * Regression: middle clicking double pastes in e.g. vim - ([#168][168]) + ([#168](https://codeberg.org/dnkl/foot/issues/168)) ## 1.5.1 @@ -1421,24 +1405,24 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Mouse bindings now match even if the actual click count is larger than specified in the binding. This allows you to, for example, quickly press the middle-button to paste multiple times - ([#146][146]). + ([#146](https://codeberg.org/dnkl/foot/issues/146)). * Color flashes when changing the color palette with OSC 4,10,11 - ([#141][141]). + ([#141](https://codeberg.org/dnkl/foot/issues/141)). * Scrollback position is now retained when resizing the window - ([#142][142]). + ([#142](https://codeberg.org/dnkl/foot/issues/142)). * Trackpad scrolling speed to better match the mouse scrolling speed, and to be consistent with other (Wayland) terminal emulators. Note that it is (much) slower compared to previous foot versions. Use the **scrollback.multiplier** option in `foot.ini` if you find the new - speed too slow ([#144][144]). + speed too slow ([#144](https://codeberg.org/dnkl/foot/issues/144)). * Crash when `foot.ini` contains an invalid section name - ([#159][159]). + ([#159](https://codeberg.org/dnkl/foot/issues/159)). * Background opacity when in _reverse video_ mode. * Crash when writing a sixel image that extends outside the terminal's - right margin ([#151][151]). + right margin ([#151](https://codeberg.org/dnkl/foot/issues/151)). * Sixel image at non-zero column positions getting sheared at seemingly random occasions - ([#151][151]). + ([#151](https://codeberg.org/dnkl/foot/issues/151)). * Crash after either resizing a window or changing the font size if there were sixels present in the scrollback while doing so. * _Send Device Attributes_ to only send a response if `Ps == 0`. @@ -1469,36 +1453,36 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Scrollback position indicator. This feature is optional and controlled by the **scrollback.indicator-position** and **scrollback.indicator-format** options in `foot.ini` - ([#42][42]). + ([#42](https://codeberg.org/dnkl/foot/issues/42)). * Key bindings in _scrollback search_ mode are now configurable. * `--check-config` command line option. * **pipe-selected** key binding. Works like **pipe-visible** and **pipe-scrollback**, but only pipes the currently selected text, if - any ([#51][51]). + any ([#51](https://codeberg.org/dnkl/foot/issues/51)). * **mouse.hide-when-typing** option to `foot.ini`. * **scrollback.multiplier** option to `foot.ini` - ([#54][54]). + ([#54](https://codeberg.org/dnkl/foot/issues/54)). * **colors.selection-foreground** and **colors.selection-background** options to `foot.ini`. * **tweak.render-timer** option to `foot.ini`. * Modifier support in mouse bindings - ([#77][77]). + ([#77](https://codeberg.org/dnkl/foot/issues/77)). * Click count support in mouse bindings, i.e double- and triple-click - ([#78][78]). + ([#78](https://codeberg.org/dnkl/foot/issues/78)). * All mouse actions (begin selection, select word, select row etc) are now configurable, via the new **select-begin**, **select-begin-block**, **select-extend**, **select-word**, **select-word-whitespace** and **select-row** options in the **mouse-bindings** section in `foot.ini` - ([#79][79]). + ([#79](https://codeberg.org/dnkl/foot/issues/79)). * Implement XTSAVE/XTRESTORE escape sequences, `CSI ? Ps s` and `CSI ? - Ps r` ([#91][91]). + Ps r` ([#91](https://codeberg.org/dnkl/foot/issues/91)). * `$COLORTERM` is now set to `truecolor` at startup, to indicate support for 24-bit RGB colors. * Experimental support for rendering double-width glyphs with a character width of 1. Must be explicitly enabled with `tweak.allow-overflowing-double-width-glyphs` - ([#116][116]). + ([#116](https://codeberg.org/dnkl/foot/issues/116)). * **initial-window-size-pixels** options to `foot.ini` and `-w,--window-size-pixels` command line option to `foot`. This option replaces the now deprecated **geometry** and `-g,--geometry` @@ -1509,7 +1493,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See alternative to **initial-window-size-pixels**. * **scrollback-up-half-page** and **scrollback-down-half-page** key bindings. They scroll up/down half of a page in the scrollback - ([#128][128]). + ([#128](https://codeberg.org/dnkl/foot/issues/128)). * **scrollback-up-line** and **scrollback-down-line** key bindings. They scroll up/down a single line in the scrollback. * **mouse.alternate-scroll-mode** option to `foot.ini`. This option @@ -1517,7 +1501,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See defaults to `yes`. When enabled, mouse scroll events are translated to up/down key events in the alternate screen, letting you scroll in e.g. `less` and other applications without enabling native mouse - support in them ([#135][135]). + support in them ([#135](https://codeberg.org/dnkl/foot/issues/135)). ### Changed @@ -1527,7 +1511,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See an error inside the terminal (and of course still log errors on stderr). * Default `--server` socket path to use `$WAYLAND_DISPLAY` instead of - `$XDG_SESSION_ID` ([#55][55]). + `$XDG_SESSION_ID` ([#55](https://codeberg.org/dnkl/foot/issues/55)). * Trailing empty cells are no longer highlighted in mouse selections. * Foot now searches for its configuration in `$XDG_DATA_DIRS/foot/foot.ini`, if no configuration is found in @@ -1548,22 +1532,22 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Compilation errors in 32-bit builds. * Mouse cursor style in top and left margins. * Selection is now **updated** when the cursor moves outside the grid - ([#70][70]). + ([#70](https://codeberg.org/dnkl/foot/issues/70)). * Viewport sometimes not moving when doing a scrollback search. * Crash when canceling a scrollback search and the window had been resized while searching. * Selection start point not moving when the selection changes direction. * OSC 10/11/104/110/111 (modify colors) did not update existing screen - content ([#94][94]). + content ([#94](https://codeberg.org/dnkl/foot/issues/94)). * Extra newlines when copying empty cells - ([#97][97]). + ([#97](https://codeberg.org/dnkl/foot/issues/97)). * Mouse events from being sent to client application when a mouse binding has consumed it. * Input events from getting mixed with paste data - ([#101][101]). + ([#101](https://codeberg.org/dnkl/foot/issues/101)). * Missing DPI values for “some” monitors on Gnome - ([#118][118]). + ([#118](https://codeberg.org/dnkl/foot/issues/118)). * Handling of multi-column composed characters while reflowing. * Escape sequences sent for key combinations with `Return`, that did **not** include `Alt`. @@ -1593,7 +1577,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Crash when starting a selection inside the margins. * Improved font size consistency across multiple monitors with - different DPI ([#47][47]). + different DPI ([#47](https://codeberg.org/dnkl/foot/issues/47)). * Handle trailing comments in `footrc` @@ -1633,7 +1617,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Crash in scrollback search. * Crash when a **pipe-visible** or **pipe-scrollback** command contained an unclosed quote - ([#49][49]). + ([#49](https://codeberg.org/dnkl/foot/issues/49)). ### Contributors @@ -1682,7 +1666,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Implemented `C0::FF` (form feed) * **pipe-visible** and **pipe-scrollback** key bindings. These let you pipe either the currently visible text, or the entire scrollback to - external tools ([#29][29]). Example: + external tools ([#29](https://codeberg.org/dnkl/foot/issues/29)). Example: `pipe-visible=[sh -c "xurls | bemenu | xargs -r firefox] Control+Print` @@ -1729,9 +1713,9 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See select half of a double-width character. * Draw hollow block cursor on top of character. * Set an initial `TIOCSWINSZ`. This ensures clients never read a - `0x0` terminal size ([#20][20]). + `0x0` terminal size ([#20](https://codeberg.org/dnkl/foot/issues/20)). * Glyphs overflowing into surrounding cells - ([#21][21]). + ([#21](https://codeberg.org/dnkl/foot/issues/21)). * Crash when last rendered cursor cell had scrolled off screen and `\E[J3` was executed. * Assert (debug builds) when an `\e]4` OSC escape was not followed by @@ -1751,7 +1735,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Sixel handling when resizing window. * Sixel handling when scrollback wraps around. * Foot now issues much fewer `wl_surface_damage_buffer()` calls - ([#35][35]). + ([#35](https://codeberg.org/dnkl/foot/issues/35)). * `C0::VT` to be processed as `C0::LF`. Previously, `C0::VT` would only move the cursor down, but never scroll. * `C0::HT` (_Horizontal Tab_, or `\t`) no longer clears `LCF` (_Last @@ -1764,7 +1748,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See now printed on the next line, instead of only printing half the character. * Font size can no longer be reduced to negative values - ([#38][38]). + ([#38](https://codeberg.org/dnkl/foot/issues/38)). ## 1.3.0 @@ -1772,7 +1756,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See ### Added * User configurable key- and mouse bindings. See `man 5 foot` and the - example `footrc` ([#1][1]) + example `footrc` ([#1](https://codeberg.org/dnkl/foot/issues/1)) * **initial-window-mode** option to `footrc`, that lets you control the initial mode for each newly spawned window: _windowed_, _maximized_ or _fullscreen_. @@ -1791,7 +1775,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Spaces no longer removed from zsh font name completions. * Default key binding for _spawn-terminal_ to ctrl+shift+n. * Renderer is now much faster with interactive scrolling - ([#4][4]) + ([#4](https://codeberg.org/dnkl/foot/issues/4)) * memfd sealing failures are no longer fatal errors. * Selection to no longer be cleared on resize. * The current monitor's subpixel order (RGB/BGR/V-RGB/V-BGR) is @@ -1852,9 +1836,9 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See ### Fixed * Window size doubling when moving window between outputs with - different scaling factors ([#3][3]). + different scaling factors ([#3](https://codeberg.org/dnkl/foot/issues/3)). * Font being too small on monitors with fractional scaling - ([#5][5]). + ([#5](https://codeberg.org/dnkl/foot/issues/5)). ## 1.2.1 From 1383def2a0726ad56126d810ede5afa374587266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 22 Apr 2022 20:05:33 +0200 Subject: [PATCH 0005/1323] changelog: convert all issue links to reference links in the 1.12.0 release --- CHANGELOG.md | 82 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f604b33..f6682adc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,49 +55,46 @@ * OSC-22 - set xcursor pointer. * Add "xterm" as fallback cursor where "text" is not available. * `[key-bindings].scrollback-home|end` options. -* Socket activation for `foot --server` and accompanying systemd unit files +* Socket activation for `foot --server` and accompanying systemd unit + files * Support for re-mapping input, i.e. mapping input to custom escape - sequences ([#325](https://codeberg.org/dnkl/foot/issues/325)). -* Support for [DECNKM](https://vt100.net/docs/vt510-rm/DECNKM.html), which - allows setting/saving/restoring/querying the keypad mode. + sequences ([#325][325]). +* Support for [DECNKM](https://vt100.net/docs/vt510-rm/DECNKM.html), + which allows setting/saving/restoring/querying the keypad mode. * Sixel support can be disabled by setting `[tweak].sixel=no` - ([#950](https://codeberg.org/dnkl/foot/issues/950)). + ([#950][950]). * footclient: `-E,--client-environment` command line option. When used, the child process in the new terminal instance inherits the environment from the footclient process instead of the server’s - ([#1004](https://codeberg.org/dnkl/foot/issues/1004)). -* `[csd].hide-when-maximized=yes|no` option - ([#1019](https://codeberg.org/dnkl/foot/issues/1019)). + ([#1004][1004]). +* `[csd].hide-when-maximized=yes|no` option ([#1019][1019]). * Scrollback search mode now highlights all matches. * `[key-binding].show-urls-persistent` action. This key binding action is similar to `show-urls-launch`, but does not automatically exit - URL mode after activating an URL - ([#964](https://codeberg.org/dnkl/foot/issues/964)). + URL mode after activating an URL ([#964][964]). * Support for `CSI > 4 n`, disable _modifyOtherKeys_. Note that since foot only supports level 1 and 2 (and not level 0), this sequence does not disable _modifyOtherKeys_ completely, but simply reverts it back to level 1 (the default). * `-Dtests=false|true` meson command line option. When disabled, test binaries will neither be built, nor will `ninja test` attempt to - execute them. Enabled by default - ([#919](https://codeberg.org/dnkl/foot/issues/919)). + execute them. Enabled by default ([#919][919]). ### Changed * Minimum required meson version is now 0.58. * Mouse selections are now finalized when the window is resized - ([#922](https://codeberg.org/dnkl/foot/issues/922)). + ([#922][922]). * OSC-4 and OSC-11 replies now uses four digits instead of 2 - ([#971](https://codeberg.org/dnkl/foot/issues/971)). + ([#971][971]). * `\r` is no longer translated to `\n` when pasting clipboard data - ([#980](https://codeberg.org/dnkl/foot/issues/980)). + ([#980][980]). * Use circles for rendering light arc box-drawing characters - ([#988](https://codeberg.org/dnkl/foot/issues/988)). + ([#988][988]). * Example configuration is now installed to `${sysconfdir}/xdg/foot/foot.ini`, typically resolving to - `/etc/xdg/foot/foot.ini` - ([#1001](https://codeberg.org/dnkl/foot/issues/1001)). + `/etc/xdg/foot/foot.ini` ([#1001][1001]). ### Removed @@ -110,37 +107,33 @@ ### Fixed * Build: missing `wayland_client` dependency in `test-config` - ([#918](https://codeberg.org/dnkl/foot/issues/918)). + ([#918][918]). * “(null)” being logged as font-name (for some fonts) when warning about a non-monospaced primary font. * Rare crash when the window is resized while a mouse selection is - ongoing ([#922](https://codeberg.org/dnkl/foot/issues/922)). -* Large selections crossing the scrollback wrap-around - ([#924](https://codeberg.org/dnkl/foot/issues/924)). -* Crash in `pipe-scrollback` - ([#926](https://codeberg.org/dnkl/foot/issues/926)). + ongoing ([#922][922]). +* Large selections crossing the scrollback wrap-around ([#924][924]). +* Crash in `pipe-scrollback` ([#926][926]). * Exit code being 0 when a foot server with no open windows terminate - due to e.g. a Wayland connection failure - ([#943](https://codeberg.org/dnkl/foot/issues/943)). + due to e.g. a Wayland connection failure ([#943][943]). * Key binding collisions not detected for bindings specified as option overrides on the command line. -* Crash when seat has no keyboard - ([#963](https://codeberg.org/dnkl/foot/issues/963)). +* Crash when seat has no keyboard ([#963][963]). * Key presses with e.g. `AltGr` triggering key combinations with the - base symbol ([#983](https://codeberg.org/dnkl/foot/issues/983)). + base symbol ([#983][983]). * Underline cursor sometimes being positioned too low, either making it look thinner than what it should be, or being completely - invisible ([#1005](https://codeberg.org/dnkl/foot/issues/1005)). + invisible ([#1005][1005]). * Fallback to `/etc/xdg` if `XDG_CONFIG_DIRS` is unset - ([#1008](https://codeberg.org/dnkl/foot/issues/1008)). + ([#1008][1008]). * Improved compatibility with XTerm when `modifyOtherKeys=2` - ([#1009](https://codeberg.org/dnkl/foot/issues/1009)). + ([#1009][1009]). * Window geometry when CSDs are enabled and CSD border width set to a non-zero value. This fixes window snapping in e.g. GNOME. * Window size “jumping” when starting an interactive resize when CSDs are enabled, and CSD border width set to a non-zero value. * Key binding overrides on the command line having no effect with - `footclient` instances ([#931](https://codeberg.org/dnkl/foot/issues/931)). + `footclient` instances ([#931][931]). * Search prev/next not updating the selection correctly when the previous and new match overlaps. * Various minor fixes to scrollback search, and how it finds the @@ -161,6 +154,29 @@ * jvoisin * merkix +[325]: https://codeberg.org/dnkl/foot/issues/325 +[950]: https://codeberg.org/dnkl/foot/issues/950 +[1004]: https://codeberg.org/dnkl/foot/issues/1004 +[1019]: https://codeberg.org/dnkl/foot/issues/1019 +[964]: https://codeberg.org/dnkl/foot/issues/964 +[919]: https://codeberg.org/dnkl/foot/issues/919 +[922]: https://codeberg.org/dnkl/foot/issues/922 +[971]: https://codeberg.org/dnkl/foot/issues/971 +[980]: https://codeberg.org/dnkl/foot/issues/980 +[988]: https://codeberg.org/dnkl/foot/issues/988 +[1001]: https://codeberg.org/dnkl/foot/issues/1001 +[918]: https://codeberg.org/dnkl/foot/issues/918 +[922]: https://codeberg.org/dnkl/foot/issues/922 +[924]: https://codeberg.org/dnkl/foot/issues/924 +[926]: https://codeberg.org/dnkl/foot/issues/926 +[943]: https://codeberg.org/dnkl/foot/issues/943 +[963]: https://codeberg.org/dnkl/foot/issues/963 +[983]: https://codeberg.org/dnkl/foot/issues/983 +[1005]: https://codeberg.org/dnkl/foot/issues/1005 +[1008]: https://codeberg.org/dnkl/foot/issues/1008 +[1009]: https://codeberg.org/dnkl/foot/issues/1009 +[931]: https://codeberg.org/dnkl/foot/issues/931 + ## 1.11.0 From 8ceb6e45a49ea414bf779adf1ba484c1dd5b5e7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 23 Apr 2022 00:44:46 +0200 Subject: [PATCH 0006/1323] pgo: add missing stubs for key-binding functions * key_binding_new_for_term() * key_binding_unref_term() --- pgo/pgo.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pgo/pgo.c b/pgo/pgo.c index 26e0c10e..92d97dbf 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -178,6 +178,13 @@ struct key_binding_set * key_binding_for( struct key_binding_manager *mgr, const struct terminal *term, const struct seat *seat) +{ + return &kbd; +} + +void +key_binding_new_for_term( + struct key_binding_manager *mgr, const struct terminal *term) { if (!kbd_initialized) { kbd_initialized = true; @@ -189,8 +196,11 @@ key_binding_for( .selection_overrides = 0, }; } +} - return &kbd; +void +key_binding_unref_term(struct key_binding_manager *mgr, const struct terminal *term) +{ } int From 9483a3a7c031cf6518aff153f07d4996ae86d830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 23 Apr 2022 00:49:52 +0200 Subject: [PATCH 0007/1323] changelog: pgo helper binary build fix (missing key-binding stubs) --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6682adc..477202df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,10 @@ ### Deprecated ### Removed ### Fixed + +* build: missing symbols when linking the `pgo` helper binary. + + ### Security ### Contributors From 18de702aeb89b69cf22f4053670d23b74800ec18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 23 Apr 2022 00:49:52 +0200 Subject: [PATCH 0008/1323] changelog: pgo helper binary build fix (missing key-binding stubs) --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6682adc..477202df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,10 @@ ### Deprecated ### Removed ### Fixed + +* build: missing symbols when linking the `pgo` helper binary. + + ### Security ### Contributors From ae2999740e8b3923685b01dd4878ac240aedc940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 23 Apr 2022 11:10:09 +0200 Subject: [PATCH 0009/1323] readme: default foot.ini is now installed to /etc/xdg/foot/foot.ini --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fdf28828..f12de1c8 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ See [INSTALL.md](INSTALL.md). **foot** can be configured by creating a file `$XDG_CONFIG_HOME/foot/foot.ini` (defaulting to `~/.config/foot/foot.ini`). A template for that can usually be found -in `/usr/share/foot/foot.ini` or +in `/etc/xdg/foot/foot.ini` or [here](https://codeberg.org/dnkl/foot/src/branch/master/foot.ini). Further information can be found in foot's man page `foot.ini(5)`. From ce4fd6df3fe0970db918375145055bd317212b55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 23 Apr 2022 11:10:37 +0200 Subject: [PATCH 0010/1323] readme: add OSC 22 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f12de1c8..9623ffe4 100644 --- a/README.md +++ b/README.md @@ -400,6 +400,7 @@ with the terminal emulator itself. Foot implements the following OSCs: * `OSC 12` - change cursor color * `OSC 17` - change highlight (selection) background color * `OSC 19` - change highlight (selection) foreground color +* `OSC 22` - set the xcursor (mouse) pointer * `OSC 52` - copy/paste clipboard data * `OSC 104` - reset color palette * `OSC 110` - reset default foreground color From 4ca04079459adcf492aebbd9289277401ed1e4e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 23 Apr 2022 11:11:34 +0200 Subject: [PATCH 0011/1323] raedme: add a reference to foot-ctlseq(7) --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 9623ffe4..5b819bf5 100644 --- a/README.md +++ b/README.md @@ -412,6 +412,9 @@ with the terminal emulator itself. Foot implements the following OSCs: * `OSC 777` - desktop notification (only the `;notify` sub-command of OSC 777 is supported.) +See the **foot-ctlseq**(7) man page for a complete list of supported +control sequences. + ## Programmatically checking if running in foot From 1913fb6efdb2dff4c600daa54513366cced38b45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 23 Apr 2022 11:13:25 +0200 Subject: [PATCH 0012/1323] changelog: hyperlink lists under their corresponding sub-section --- CHANGELOG.md | 48 +++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 477202df..a87a63ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,13 @@ binaries will neither be built, nor will `ninja test` attempt to execute them. Enabled by default ([#919][919]). +[325]: https://codeberg.org/dnkl/foot/issues/325 +[950]: https://codeberg.org/dnkl/foot/issues/950 +[1004]: https://codeberg.org/dnkl/foot/issues/1004 +[1019]: https://codeberg.org/dnkl/foot/issues/1019 +[964]: https://codeberg.org/dnkl/foot/issues/964 +[919]: https://codeberg.org/dnkl/foot/issues/919 + ### Changed @@ -100,6 +107,12 @@ `${sysconfdir}/xdg/foot/foot.ini`, typically resolving to `/etc/xdg/foot/foot.ini` ([#1001][1001]). +[922]: https://codeberg.org/dnkl/foot/issues/922 +[971]: https://codeberg.org/dnkl/foot/issues/971 +[980]: https://codeberg.org/dnkl/foot/issues/980 +[988]: https://codeberg.org/dnkl/foot/issues/988 +[1001]: https://codeberg.org/dnkl/foot/issues/1001 + ### Removed @@ -143,6 +156,18 @@ * Various minor fixes to scrollback search, and how it finds the next/prev match. +[918]: https://codeberg.org/dnkl/foot/issues/918 +[922]: https://codeberg.org/dnkl/foot/issues/922 +[924]: https://codeberg.org/dnkl/foot/issues/924 +[926]: https://codeberg.org/dnkl/foot/issues/926 +[943]: https://codeberg.org/dnkl/foot/issues/943 +[963]: https://codeberg.org/dnkl/foot/issues/963 +[983]: https://codeberg.org/dnkl/foot/issues/983 +[1005]: https://codeberg.org/dnkl/foot/issues/1005 +[1008]: https://codeberg.org/dnkl/foot/issues/1008 +[1009]: https://codeberg.org/dnkl/foot/issues/1009 +[931]: https://codeberg.org/dnkl/foot/issues/931 + ### Contributors @@ -158,29 +183,6 @@ * jvoisin * merkix -[325]: https://codeberg.org/dnkl/foot/issues/325 -[950]: https://codeberg.org/dnkl/foot/issues/950 -[1004]: https://codeberg.org/dnkl/foot/issues/1004 -[1019]: https://codeberg.org/dnkl/foot/issues/1019 -[964]: https://codeberg.org/dnkl/foot/issues/964 -[919]: https://codeberg.org/dnkl/foot/issues/919 -[922]: https://codeberg.org/dnkl/foot/issues/922 -[971]: https://codeberg.org/dnkl/foot/issues/971 -[980]: https://codeberg.org/dnkl/foot/issues/980 -[988]: https://codeberg.org/dnkl/foot/issues/988 -[1001]: https://codeberg.org/dnkl/foot/issues/1001 -[918]: https://codeberg.org/dnkl/foot/issues/918 -[922]: https://codeberg.org/dnkl/foot/issues/922 -[924]: https://codeberg.org/dnkl/foot/issues/924 -[926]: https://codeberg.org/dnkl/foot/issues/926 -[943]: https://codeberg.org/dnkl/foot/issues/943 -[963]: https://codeberg.org/dnkl/foot/issues/963 -[983]: https://codeberg.org/dnkl/foot/issues/983 -[1005]: https://codeberg.org/dnkl/foot/issues/1005 -[1008]: https://codeberg.org/dnkl/foot/issues/1008 -[1009]: https://codeberg.org/dnkl/foot/issues/1009 -[931]: https://codeberg.org/dnkl/foot/issues/931 - ## 1.11.0 From 155a2e47905a07810eb6ede94951d20a75ddf79b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 23 Apr 2022 11:24:44 +0200 Subject: [PATCH 0013/1323] ci: enable -Db_pgo=generate on release builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hopefully, this’ll catch missing stubs in pgo/pgo.c in the future. --- .builds/alpine-x64.yml | 2 +- .builds/alpine-x86.yml.disabled | 2 +- .builds/freebsd-x64.yml | 2 +- .gitlab-ci.yml | 4 ++-- .woodpecker.yml | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.builds/alpine-x64.yml b/.builds/alpine-x64.yml index a7782b20..8f341f3d 100644 --- a/.builds/alpine-x64.yml +++ b/.builds/alpine-x64.yml @@ -43,7 +43,7 @@ tasks: meson test -C bld/debug --print-errorlogs - release: | mkdir -p bld/release - meson --buildtype=minsize -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release + meson --buildtype=minsize -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release ninja -C bld/release -k0 meson test -C bld/release --print-errorlogs - codespell: | diff --git a/.builds/alpine-x86.yml.disabled b/.builds/alpine-x86.yml.disabled index 24909eb2..22a9e637 100644 --- a/.builds/alpine-x86.yml.disabled +++ b/.builds/alpine-x86.yml.disabled @@ -38,6 +38,6 @@ tasks: meson test -C bld/debug --print-errorlogs - release: | mkdir -p bld/release - meson --buildtype=minsize -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release + meson --buildtype=minsize -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release ninja -C bld/release -k0 meson test -C bld/release --print-errorlogs diff --git a/.builds/freebsd-x64.yml b/.builds/freebsd-x64.yml index bd4b073c..89803a6e 100644 --- a/.builds/freebsd-x64.yml +++ b/.builds/freebsd-x64.yml @@ -41,7 +41,7 @@ tasks: - release: | mkdir -p bld/release - meson --buildtype=minsize -Dterminfo=disabled -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release + meson --buildtype=minsize -Db_pgo=generate -Dterminfo=disabled -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release ninja -C bld/release -k0 meson test -C bld/release --print-errorlogs bld/release/foot --version diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5857b87d..b2a459dc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -57,7 +57,7 @@ release-x64: - cd .. - mkdir -p bld/release - cd bld/release - - meson --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../../ + - 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 @@ -93,7 +93,7 @@ release-x86: - cd .. - mkdir -p bld/release - cd bld/release - - meson --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../../ + - 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 diff --git a/.woodpecker.yml b/.woodpecker.yml index b790c68f..8493aa47 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -53,7 +53,7 @@ pipeline: # Release - mkdir -p bld/release-x64 - cd bld/release-x64 - - meson --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - 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 @@ -100,7 +100,7 @@ pipeline: # Release - mkdir -p bld/release-x86 - cd bld/release-x86 - - meson --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - 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 From f0f0fac77fd5a60af080e961468619e93e44ed09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 23 Apr 2022 20:08:09 +0200 Subject: [PATCH 0014/1323] doc: foot.ini: drop empty line after *show-urls-launch* --- doc/foot.ini.5.scd | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index d4226e67..8aa63d2f 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -747,7 +747,6 @@ e.g. *search-start=none*. Default: _not bound_ *show-urls-launch* - Enter URL mode, where all currently visible URLs are tagged with a jump label with a key sequence that will open the URL (and exit URL mode). Default: _Control+Shift+u_. From b68d5da71b5014d7570c6d8c097ab4a9905ed287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 24 Apr 2022 12:03:31 +0200 Subject: [PATCH 0015/1323] search: fix debug log This has been broken since the forward/backward search logic was refactored. --- search.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/search.c b/search.c index a1ef8eb4..77492e3f 100644 --- a/search.c +++ b/search.c @@ -467,7 +467,8 @@ search_find_next(struct terminal *term, enum search_direction direction) LOG_DBG( "update: %s: starting at row=%d col=%d " "(offset = %d, view = %d)", - backward ? "backward" : "forward", start.row, start.col, + direction != SEARCH_FORWARD ? "backward" : "forward", + start.row, start.col, grid->offset, grid->view); struct coord end = start; From 2cbcfb315909969c126f9fd322c68236151a17e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 24 Apr 2022 12:04:06 +0200 Subject: [PATCH 0016/1323] render: fix refresh logic of pending csd|search|url MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Our CSDs, the search-box and URL labels are all implemented using sub-surfaces, synchronized with the main grid. This means we *must* commit the main surface as well, when updating one of these sub-surfaces. The logic for doing so in the frame callback was flawed, and only triggered when the main grid was actually dirty. That is, e.g. search box updates that did not also resulted in grid updates (for example - pasting a search criteria that doesn’t match), did not result in a UI refresh. Closes #1040 --- render.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.c b/render.c index 31d5a61f..27f21bd5 100644 --- a/render.c +++ b/render.c @@ -3572,7 +3572,7 @@ frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_da if (urls) render_urls(term); - if (grid && (!term->delayed_render_timer.is_armed | csd | search | urls)) + if ((grid && !term->delayed_render_timer.is_armed) || (csd | search | urls)) grid_render(term); tll_foreach(term->wl->seats, it) { From 47d1ba58e5b2bbdb534ac4993e1ab9d30c229ac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 24 Apr 2022 12:08:23 +0200 Subject: [PATCH 0017/1323] changelog: UI not refreshing when pasting into the scrollback search box --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a87a63ba..c44cc100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,11 @@ ### Fixed * build: missing symbols when linking the `pgo` helper binary. +* UI not refreshing when pasting something into the scrollback search + box, that does not result in a grid update (for example, when the + search criteria did not result in any matches) ([#1040][1040]). + +[1040]: https://codeberg.org/dnkl/foot/issues/1040 ### Security From 8c0fca30db83576f7df5d48c94ecfa9896f8a075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 23 Apr 2022 12:23:27 +0200 Subject: [PATCH 0018/1323] =?UTF-8?q?selection:=20find=5Fword=5Fboundary:?= =?UTF-8?q?=20assert=20=E2=80=98pos=E2=80=99=20is=20valid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- selection.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/selection.c b/selection.c index f279a834..da62d354 100644 --- a/selection.c +++ b/selection.c @@ -270,6 +270,11 @@ void selection_find_word_boundary_left(struct terminal *term, struct coord *pos, bool spaces_only) { + xassert(pos->row >= 0); + xassert(pos->row < term->rows); + xassert(pos->col >= 0); + xassert(pos->col < term->cols); + const struct row *r = grid_row_in_view(term->grid, pos->row); char32_t c = r->cells[pos->col].wc; @@ -343,6 +348,11 @@ void selection_find_word_boundary_right(struct terminal *term, struct coord *pos, bool spaces_only) { + xassert(pos->row >= 0); + xassert(pos->row < term->rows); + xassert(pos->col >= 0); + xassert(pos->col < term->cols); + const struct row *r = grid_row_in_view(term->grid, pos->row); char32_t c = r->cells[pos->col].wc; From f7c29ee39497a23b95e4ef97fc3ed87360014d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 23 Apr 2022 12:24:28 +0200 Subject: [PATCH 0019/1323] search: maches_next: assert match coordinates are valid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * They are within range (i.e. ‘row’ does not exceed term->rows-1) * ‘end’ comes after ‘start’ --- search.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/search.c b/search.c index 77492e3f..330ea791 100644 --- a/search.c +++ b/search.c @@ -561,6 +561,15 @@ search_matches_next(struct search_match_iterator *iter) match.end.row = match.end.row - grid->view + grid->num_rows; match.end.row &= grid->num_rows - 1; + xassert(match.start.row >= 0); + xassert(match.start.row < term->rows); + xassert(match.end.row >= 0); + xassert(match.end.row < term->rows); + + xassert(match.end.row > match.start.row || + (match.end.row == match.start.row && + match.end.col >= match.start.col)); + if (return_primary_match) { iter->start.row = 0; iter->start.col = 0; From d068e821d60dc9d9ae606969010b087040b98b59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 23 Apr 2022 12:25:21 +0200 Subject: [PATCH 0020/1323] =?UTF-8?q?search:=20matches=5Fnext:=20don?= =?UTF-8?q?=E2=80=99t=20wrap=20around=20grid->num=5Frows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When bumping the iter’s start.row, we’re working with view-local coordinates. That is, 0 >= row < term->rows. This means it is wrong to and with grid->num_rows - 1, because a), ‘row’ should **never** be that big. And b), if we do, we’ll just end up in an infinite loop, where the next call to matches_next() just starts over from the beginning again (and eventually hitting the exact same place that got us started). --- search.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/search.c b/search.c index 330ea791..8f99fae7 100644 --- a/search.c +++ b/search.c @@ -580,10 +580,14 @@ search_matches_next(struct search_match_iterator *iter) if (iter->start.col >= term->cols) { iter->start.col = 0; - iter->start.row++; - iter->start.row &= grid->num_rows - 1; + iter->start.row++; /* Overflow is caught in next iteration */ } + xassert(iter->start.row >= 0); + xassert(iter->start.row <= term->rows); + xassert(iter->start.col >= 0); + xassert(iter->start.col < term->cols); + if (match.start.row == term->search.match.row && match.start.col == term->search.match.col) { From 082e242ce5c954c59f52a6878a6d3794694a1182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 23 Apr 2022 12:28:12 +0200 Subject: [PATCH 0021/1323] search: matches_next: stop searching when start.row >= term->rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As this means the last call to sarch_matches_next() found a match at the bottom of the view, and then set the iter’s *next* start position to a row outside the view. This is fine, but we need to handle it, by immediately stopping the iter. --- search.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/search.c b/search.c index 8f99fae7..337db697 100644 --- a/search.c +++ b/search.c @@ -539,7 +539,16 @@ search_matches_next(struct search_match_iterator *iter) /* First, return the primary match */ match = term->selection.coords; found = true; - } else { + } + + else if (iter->start.row >= term->rows) { + goto no_match; + } + + else { + xassert(iter->start.row >= 0); + xassert(iter->start.row < term->rows); + struct coord abs_start = iter->start; abs_start.row = grid_row_absolute_in_view(grid, abs_start.row); From 1d48b7b77c69ca518d0bbdc079ca3736311b9c98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 23 Apr 2022 12:35:07 +0200 Subject: [PATCH 0022/1323] =?UTF-8?q?search:=20matches=5Fnext:=20assert=20?= =?UTF-8?q?start=E2=80=99s=20=E2=80=98col=E2=80=99=20is=20valid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- search.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/search.c b/search.c index 337db697..73f831eb 100644 --- a/search.c +++ b/search.c @@ -548,6 +548,8 @@ search_matches_next(struct search_match_iterator *iter) else { xassert(iter->start.row >= 0); xassert(iter->start.row < term->rows); + xassert(iter->start.col >= 0); + xassert(iter->start.col < term->cols); struct coord abs_start = iter->start; abs_start.row = grid_row_absolute_in_view(grid, abs_start.row); From 312f0dbcfd6e027981b99254421ed5fda6faac34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 23 Apr 2022 15:19:32 +0200 Subject: [PATCH 0023/1323] changelog: scrollback mode freezing, with 100% CPU --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c44cc100..a7ad4371 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,8 +49,10 @@ * UI not refreshing when pasting something into the scrollback search box, that does not result in a grid update (for example, when the search criteria did not result in any matches) ([#1040][1040]). +* foot freezing in scrollback search mode, using 100% CPU ([#1036][1036]). [1040]: https://codeberg.org/dnkl/foot/issues/1040 +[1036]: https://codeberg.org/dnkl/foot/issues/1036 ### Security From 9c0f1a671cb015b650f7ed80e8fcf66d07c9e87a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 23 Apr 2022 15:49:25 +0200 Subject: [PATCH 0024/1323] selection: assert serial is non-zero before copying data to the clipboard --- selection.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/selection.c b/selection.c index da62d354..8a18c6bb 100644 --- a/selection.c +++ b/selection.c @@ -1533,6 +1533,8 @@ static const struct zwp_primary_selection_source_v1_listener primary_selection_s bool text_to_clipboard(struct seat *seat, struct terminal *term, char *text, uint32_t serial) { + xassert(serial != 0); + struct wl_clipboard *clipboard = &seat->clipboard; if (clipboard->data_source != NULL) { @@ -1568,7 +1570,6 @@ text_to_clipboard(struct seat *seat, struct terminal *term, char *text, uint32_t wl_data_device_set_selection(seat->data_device, clipboard->data_source, serial); /* Needed when sending the selection to other client */ - xassert(serial != 0); clipboard->serial = serial; return true; } @@ -1981,6 +1982,8 @@ text_to_primary(struct seat *seat, struct terminal *term, char *text, uint32_t s if (term->wl->primary_selection_device_manager == NULL) return false; + xassert(serial != 0); + struct wl_primary *primary = &seat->primary; /* TODO: somehow share code with the clipboard equivalent */ From a26eb1ea09b9a3c49cb664937c736915e2ab07ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 23 Apr 2022 15:54:37 +0200 Subject: [PATCH 0025/1323] input: assert serial received from compositor is non-zero --- input.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/input.c b/input.c index be495066..961b73ba 100644 --- a/input.c +++ b/input.c @@ -500,6 +500,7 @@ keyboard_enter(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, struct wl_surface *surface, struct wl_array *keys) { xassert(surface != NULL); + xassert(serial != 0); struct seat *seat = data; struct wl_window *win = wl_surface_get_user_data(surface); @@ -1269,6 +1270,8 @@ static void key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, uint32_t key, uint32_t state) { + xassert(serial != 0); + seat->kbd.serial = serial; if (seat->kbd.xkb == NULL || seat->kbd.xkb_keymap == NULL || @@ -1621,6 +1624,7 @@ wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, wl_fixed_t surface_x, wl_fixed_t surface_y) { xassert(surface != NULL); + xassert(serial != 0); if (surface == NULL) { /* Seen on mutter-3.38 */ @@ -1984,6 +1988,8 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, LOG_DBG("BUTTON: pointer=%p, serial=%u, button=%x, state=%u", (void *)wl_pointer, serial, button, state); + xassert(serial != 0); + struct seat *seat = data; struct wayland *wayl = seat->wayl; struct terminal *term = seat->mouse_focus; From b4f666118febfc95bf97e31aa1e39e3df9522dfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 25 Apr 2022 19:57:18 +0200 Subject: [PATCH 0026/1323] grid: add abs-to-sb and sb-to-abs utility function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These functions convert row numbers between absolute coordinates and “scrollback relative” coordinates. Absolute row numbers can be used to index into the grid->rows[] array. Scrollback relative numbers are ordered with the *oldest* row first, and the *newest* row last. That is, in these coordinates, row 0 is the *first* (oldest) row in the scrollback history, and row N is the *last* (newest) row. Scrollback relative numbers are used when we need to sort things after their age, when determining if something has scrolled out, or when limiting an operation to ensure we don’t go past the scrollback wrap-around. --- grid.c | 28 ++++++++++++++++++++++++++++ grid.h | 5 +++++ 2 files changed, 33 insertions(+) diff --git a/grid.c b/grid.c index 5a0b5c7e..4a3995a0 100644 --- a/grid.c +++ b/grid.c @@ -15,6 +15,34 @@ #define TIME_REFLOW 0 +/* + * “sb” (scrollback relative) coordinates + * + * The scrollback relative row number 0 is the *first*, and *oldest* + * row in the scrollback history (and thus the *first* row to be + * scrolled out). Thus, a higher number means further *down* in the + * scrollback, with the *highest* number being at the bottom of the + * screen, where new input appears. + */ +int +grid_row_abs_to_sb(const struct grid *grid, int screen_rows, int abs_row) +{ + const int scrollback_start = grid->offset + screen_rows; + int rebased_row = abs_row - scrollback_start + grid->num_rows; + + rebased_row &= grid->num_rows - 1; + return rebased_row; +} + +int grid_row_sb_to_abs(const struct grid *grid, int screen_rows, int sb_rel_row) +{ + const int scrollback_start = grid->offset + screen_rows; + int abs_row = sb_rel_row + scrollback_start; + + abs_row &= grid->num_rows - 1; + return abs_row; +} + static void ensure_row_has_extra_data(struct row *row) { diff --git a/grid.h b/grid.h index 7819db4d..22bd76bb 100644 --- a/grid.h +++ b/grid.h @@ -21,6 +21,10 @@ void grid_resize_and_reflow( size_t tracking_points_count, struct coord *const _tracking_points[static tracking_points_count]); +/* Convert row numbers between scrollback-relative and absolute coordinates */ +int grid_row_abs_to_sb(const struct grid *grid, int screen_rows, int abs_row); +int grid_row_sb_to_abs(const struct grid *grid, int screen_rows, int sb_rel_row); + static inline int grid_row_absolute(const struct grid *grid, int row_no) { @@ -33,6 +37,7 @@ grid_row_absolute_in_view(const struct grid *grid, int row_no) return (grid->view + row_no) & (grid->num_rows - 1); } + static inline struct row * _grid_row_maybe_alloc(struct grid *grid, int row_no, bool alloc_if_null) { From 6316a5eb0cf2b1fe25ca0e9d60c4e11fce9c22d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 25 Apr 2022 19:59:23 +0200 Subject: [PATCH 0027/1323] selection: add start/end coordinate getters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Internally, selection coordinates are *unbounded* (that is, the row numbers may be larger than grid->num_rows) while a selection is ongoing. Only after it has been finalized are the coordinates bounded. This means it isn’t safe to use term->selection.coords.* directly. --- selection.c | 24 ++++++++++++++++++++++++ selection.h | 3 +++ 2 files changed, 27 insertions(+) diff --git a/selection.c b/selection.c index 8a18c6bb..091d3ed0 100644 --- a/selection.c +++ b/selection.c @@ -38,6 +38,29 @@ static const char *const mime_type_map[] = { [DATA_OFFER_MIME_TEXT_UTF8_STRING] = "UTF8_STRING", }; +static inline struct coord +bounded(const struct grid *grid, struct coord coord) +{ + coord.row &= grid->num_rows - 1; + return coord; +} + +struct coord +selection_get_start(const struct terminal *term) +{ + if (term->selection.coords.start.row < 0) + return term->selection.coords.start; + return bounded(term->grid, term->selection.coords.start); +} + +struct coord +selection_get_end(const struct terminal *term) +{ + if (term->selection.coords.end.row < 0) + return term->selection.coords.end; + return bounded(term->grid, term->selection.coords.end); +} + bool selection_on_rows(const struct terminal *term, int row_start, int row_end) { @@ -2461,3 +2484,4 @@ const struct zwp_primary_selection_device_v1_listener primary_selection_device_l .data_offer = &primary_data_offer, .selection = &primary_selection, }; + diff --git a/selection.h b/selection.h index 295d8d1c..0a6ece91 100644 --- a/selection.h +++ b/selection.h @@ -79,3 +79,6 @@ void selection_find_word_boundary_left( struct terminal *term, struct coord *pos, bool spaces_only); void selection_find_word_boundary_right( struct terminal *term, struct coord *pos, bool spaces_only); + +struct coord selection_get_start(const struct terminal *term); +struct coord selection_get_end(const struct terminal *term); From 1d4e1b921d351da349ccfbc5d587489d7b77e690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 25 Apr 2022 20:00:14 +0200 Subject: [PATCH 0028/1323] sixel/terminal: use the new grid and selection APIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use grid_row_abs_to_sb() instead of manually “rebasing” row numbers. Use selection_get_{start,end}() to retrieve the current selection coordinates. --- sixel.c | 55 ++++++++++++++++++++++++------------------------------ terminal.c | 25 +++++++++++++------------ 2 files changed, 37 insertions(+), 43 deletions(-) diff --git a/sixel.c b/sixel.c index 5316c0ad..0c94117f 100644 --- a/sixel.c +++ b/sixel.c @@ -7,8 +7,9 @@ #define LOG_ENABLE_DBG 0 #include "log.h" #include "debug.h" -#include "render.h" +#include "grid.h" #include "hsl.h" +#include "render.h" #include "util.h" #include "xmalloc.h" #include "xsnprintf.h" @@ -138,25 +139,6 @@ sixel_erase(struct terminal *term, struct sixel *sixel) sixel_destroy(sixel); } -/* - * Calculates the scrollback relative row number, given an absolute row number. - * - * The scrollback relative row number 0 is the *first*, and *oldest* - * row in the scrollback history (and thus the *first* row to be - * scrolled out). Thus, a higher number means further *down* in the - * scrollback, with the *highest* number being at the bottom of the - * screen, where new input appears. - */ -static int -rebase_row(const struct terminal *term, int abs_row) -{ - int scrollback_start = term->grid->offset + term->rows; - int rebased_row = abs_row - scrollback_start + term->grid->num_rows; - - rebased_row &= term->grid->num_rows - 1; - return rebased_row; -} - /* * Verify the sixels are sorted correctly. * @@ -175,7 +157,8 @@ verify_list_order(const struct terminal *term) size_t idx = 0; tll_foreach(term->grid->sixel_images, it) { - int row = rebase_row(term, it->item.pos.row + it->item.rows - 1); + int row = grid_row_abs_to_sb( + term->grid, term->rows, it->item.pos.row + it->item.rows - 1); int col = it->item.pos.col; int col_count = it->item.cols; @@ -232,7 +215,8 @@ verify_scrollback_consistency(const struct terminal *term) int last_row = -1; for (int i = 0; i < six->rows; i++) { - int row_no = rebase_row(term, six->pos.row + i); + int row_no = grid_row_abs_to_sb( + term->grid, term->rows, six->pos.row + i); if (last_row != -1) xassert(last_row < row_no); @@ -295,10 +279,14 @@ verify_sixels(const struct terminal *term) static void sixel_insert(struct terminal *term, struct sixel sixel) { - int end_row = rebase_row(term, sixel.pos.row + sixel.rows - 1); + int end_row = grid_row_abs_to_sb( + term->grid, term->rows, sixel.pos.row + sixel.rows - 1); tll_foreach(term->grid->sixel_images, it) { - if (rebase_row(term, it->item.pos.row + it->item.rows - 1) < end_row) { + int rebased = grid_row_abs_to_sb( + term->grid, term->rows, it->item.pos.row + it->item.rows - 1); + + if (rebased < end_row) { tll_insert_before(term->grid->sixel_images, it, sixel); goto out; } @@ -325,7 +313,7 @@ sixel_scroll_up(struct terminal *term, int rows) tll_rforeach(term->grid->sixel_images, it) { struct sixel *six = &it->item; - int six_start = rebase_row(term, six->pos.row); + int six_start = grid_row_abs_to_sb(term->grid, term->rows, six->pos.row); if (six_start < rows) { sixel_erase(term, six); @@ -358,7 +346,8 @@ sixel_scroll_down(struct terminal *term, int rows) tll_foreach(term->grid->sixel_images, it) { struct sixel *six = &it->item; - int six_end = rebase_row(term, six->pos.row + six->rows - 1); + int six_end = grid_row_abs_to_sb( + term->grid, term->rows, six->pos.row + six->rows - 1); if (six_end >= term->grid->num_rows - rows) { sixel_erase(term, six); tll_remove(term->grid->sixel_images, it); @@ -668,7 +657,8 @@ _sixel_overwrite_by_rectangle( /* We should never generate scrollback wrapping sixels */ xassert(end < term->grid->num_rows); - const int scrollback_rel_start = rebase_row(term, start); + const int scrollback_rel_start = grid_row_abs_to_sb( + term->grid, term->rows, start); bool UNUSED would_have_breaked = false; @@ -677,7 +667,8 @@ _sixel_overwrite_by_rectangle( const int six_start = six->pos.row; const int six_end = (six_start + six->rows - 1); - const int six_scrollback_rel_end = rebase_row(term, six_end); + const int six_scrollback_rel_end = + grid_row_abs_to_sb(term->grid, term->rows, six_end); /* We should never generate scrollback wrapping sixels */ xassert(six_end < term->grid->num_rows); @@ -776,7 +767,7 @@ sixel_overwrite_by_row(struct terminal *term, int _row, int col, int width) width = term->grid->num_cols - col; const int row = (term->grid->offset + _row) & (term->grid->num_rows - 1); - const int scrollback_rel_row = rebase_row(term, row); + const int scrollback_rel_row = grid_row_abs_to_sb(term->grid, term->rows, row); tll_foreach(term->grid->sixel_images, it) { struct sixel *six = &it->item; @@ -786,7 +777,8 @@ sixel_overwrite_by_row(struct terminal *term, int _row, int col, int width) /* We should never generate scrollback wrapping sixels */ xassert(six_end >= six_start); - const int six_scrollback_rel_end = rebase_row(term, six_end); + const int six_scrollback_rel_end = + grid_row_abs_to_sb(term->grid, term->rows, six_end); if (six_scrollback_rel_end < scrollback_rel_row) { /* All remaining sixels are *before* "our" row */ @@ -888,7 +880,8 @@ sixel_reflow(struct terminal *term) int last_row = -1; for (int j = 0; j < six->rows; j++) { - int row_no = rebase_row(term, six->pos.row + j); + int row_no = grid_row_abs_to_sb( + term->grid, term->rows, six->pos.row + j); if (last_row != -1 && last_row >= row_no) { sixel_destroy(six); sixel_destroyed = true; diff --git a/terminal.c b/terminal.c index f1b05f57..7d230cdf 100644 --- a/terminal.c +++ b/terminal.c @@ -2154,18 +2154,18 @@ term_erase(struct terminal *term, int start_row, int start_col, void term_erase_scrollback(struct terminal *term) { - const int num_rows = term->grid->num_rows; + const struct grid *grid = term->grid; + const int num_rows = grid->num_rows; const int mask = num_rows - 1; - const int start = (term->grid->offset + term->rows) & mask; - const int end = (term->grid->offset - 1) & mask; + const int start = (grid->offset + term->rows) & mask; + const int end = (grid->offset - 1) & mask; - const int scrollback_start = term->grid->offset + term->rows; - const int rel_start = (start - scrollback_start + num_rows) & mask; - const int rel_end = (end - scrollback_start + num_rows) & mask; + const int rel_start = grid_row_abs_to_sb(grid, term->rows, start); + const int rel_end = grid_row_abs_to_sb(grid, term->rows, end); - const int sel_start = term->selection.coords.start.row; - const int sel_end = term->selection.coords.end.row; + const int sel_start = selection_get_start(term).row; + const int sel_end = selection_get_end(term).row; if (sel_end >= 0) { /* @@ -2183,8 +2183,8 @@ term_erase_scrollback(struct terminal *term) * closer to the screen bottom. */ - const int rel_sel_start = (sel_start - scrollback_start + num_rows) & mask; - const int rel_sel_end = (sel_end - scrollback_start + num_rows) & mask; + const int rel_sel_start = grid_row_abs_to_sb(grid, term->rows, sel_start); + const int rel_sel_end = grid_row_abs_to_sb(grid, term->rows, sel_end); if ((rel_sel_start <= rel_start && rel_sel_end >= rel_start) || (rel_sel_start <= rel_end && rel_sel_end >= rel_end) || @@ -2196,8 +2196,9 @@ term_erase_scrollback(struct terminal *term) tll_foreach(term->grid->sixel_images, it) { struct sixel *six = &it->item; - const int six_start = (six->pos.row - scrollback_start + num_rows) & mask; - const int six_end = (six->pos.row + six->rows - 1 - scrollback_start + num_rows) & mask; + const int six_start = grid_row_abs_to_sb(grid, term->rows, six->pos.row); + const int six_end = grid_row_abs_to_sb( + grid, term->rows, six->pos.row + six->rows - 1); if ((six_start <= rel_start && six_end >= rel_start) || (six_start <= rel_end && six_end >= rel_end) || From 5c4ddebc3c57724bd89b959a33f5ad0bd97f482b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 25 Apr 2022 20:00:47 +0200 Subject: [PATCH 0029/1323] search: fix multiple crashes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * When extending the selection to the next word boundary, ensure the row numbers are valid: - use selection_get_end() when retrieving the current end coordinate. This alone fixes a crash where we previously would crash in an out-of-bounds array access in grid->rows[], due to term->selection.coords.end being unbounded. - ensure the new end coordinate is bounded before and after calling selection_find_word_boundary_right(). * When moving the viewport (to ensure a new match is visible), make sure we don’t end up with the match outside the new viewport. Under certain, unusual, circumstances, the moved viewport _still_ did not contain the match. This resulted in assertions triggering later, that assumed the match(es) are *always* visible. It’s fairly easy to trigger this one by running foot with e.g. foot -o scrollback.lines=0 --window-size-chars 25x3 and then hitting enter a couple of times, to fill the scrollback history (which should consist of a single row in the example above), and the do a scrollback search for (part of) the prompt, and keep searching backward until it crashes. This would happen if calculated (new) viewport had to be adjusted (for example, to ensure it didn’t go past the scrollback end). This patch changes the logic used when calculating the new viewport. Instead of calculating the wanted viewport (match is vertically centered) and then trying to adjust it to ensure the new viewport is valid, start with a “safe” new viewport value, and then determine how much we can move it, if at all, to center the match. This is done by using scrollback relative coordinates. In this coordinate system, the new viewport must be >= 0, and < grid->num_lines - term->rows This makes it very easy to limit the amount by which the viewport is adjusted. As a side-effect, we can remove all the old re-adjustment logic. * The match iterator no longer special cases the primary match. This was needed before, when the search iterator did not handle overlapping matches correctly. Now that we do, the iterator is guaranteed to find the primary match, and thus we no longer need to special case it. This fixes a bug where the primary match was returned twice, due to the logic checking if a secondary match is the same as the primary match was flawed... Closes #1036 --- search.c | 194 ++++++++++++++++++++++--------------------------------- 1 file changed, 76 insertions(+), 118 deletions(-) diff --git a/search.c b/search.c index 73f831eb..09b59d50 100644 --- a/search.c +++ b/search.c @@ -82,11 +82,7 @@ search_ensure_size(struct terminal *term, size_t wanted_size) static bool has_wrapped_around(const struct terminal *term, int abs_row_no) { - const struct grid *grid = term->grid; - int scrollback_start = grid->offset + term->rows; - int rebased_row = abs_row_no - scrollback_start + grid->num_rows; - rebased_row &= grid->num_rows - 1; - + int rebased_row = grid_row_abs_to_sb(term->grid, term->rows, abs_row_no); return rebased_row == 0; } @@ -182,6 +178,9 @@ search_update_selection(struct terminal *term, const struct range *match) int end_row = match->end.row; int end_col = match->end.col; + xassert(start_row >= 0); + xassert(start_row < grid->num_rows); + bool move_viewport = true; int view_end = (grid->view + term->rows - 1) & (grid->num_rows - 1); @@ -196,24 +195,14 @@ search_update_selection(struct terminal *term, const struct range *match) } if (move_viewport) { - int old_view = grid->view; - int new_view = start_row - term->rows / 2; + int rebased_new_view = grid_row_abs_to_sb(grid, term->rows, start_row); - while (new_view < 0) - new_view += grid->num_rows; + rebased_new_view -= term->rows / 2; + rebased_new_view = + min(max(rebased_new_view, 0), grid->num_rows - term->rows); - new_view = ensure_view_is_allocated(term, new_view); - - /* Don't scroll past scrollback history */ - int end = (grid->offset + term->rows - 1) & (grid->num_rows - 1); - if (end >= grid->offset) { - /* Not wrapped */ - if (new_view >= grid->offset && new_view <= end) - new_view = grid->offset; - } else { - if (new_view >= grid->offset || new_view <= end) - new_view = grid->offset; - } + const int old_view = grid->view; + int new_view = grid_row_sb_to_abs(grid, term->rows, rebased_new_view); #if defined(_DEBUG) /* Verify all to-be-visible rows have been allocated */ @@ -227,23 +216,8 @@ search_update_selection(struct terminal *term, const struct range *match) term_damage_view(term); } -#if 0 - /* Selection endpoint is inclusive */ - if (--end_col < 0) { - end_col = term->cols - 1; - end_row--; - } -#endif - - /* - * Begin a new selection if the start coords changed - * - * Note: check column against selection.coords, since our “old” - * start column isn’t reliable - we modify it when searching - * “next” or “prev”. - */ if (start_row != term->search.match.row || - start_col != term->selection.coords.start.col) + start_col != term->search.match.col) { int selection_row = start_row - grid->view + grid->num_rows; selection_row &= grid->num_rows - 1; @@ -516,7 +490,7 @@ search_matches_new_iter(struct terminal *term) { return (struct search_match_iterator){ .term = term, - .start = {-2, -2}, + .start = {0, 0}, }; } @@ -529,86 +503,63 @@ search_matches_next(struct search_match_iterator *iter) if (term->search.match_len == 0) goto no_match; - struct range match; - bool found; - - const bool return_primary_match = - iter->start.row == -2 && term->selection.coords.end.row >= 0; - - if (return_primary_match) { - /* First, return the primary match */ - match = term->selection.coords; - found = true; - } - - else if (iter->start.row >= term->rows) { + if (iter->start.row >= term->rows) goto no_match; + + xassert(iter->start.row >= 0); + xassert(iter->start.row < term->rows); + xassert(iter->start.col >= 0); + xassert(iter->start.col < term->cols); + + struct coord abs_start = iter->start; + abs_start.row = grid_row_absolute_in_view(grid, abs_start.row); + + struct coord abs_end = { + term->cols - 1, + grid_row_absolute_in_view(grid, term->rows - 1)}; + + struct range match; + bool found = find_next(term, SEARCH_FORWARD, abs_start, abs_end, &match); + if (!found) + goto no_match; + + LOG_DBG("match at (absolute coordinates) %dx%d-%dx%d", + match.start.row, match.start.col, + match.end.row, match.end.col); + + /* Convert absolute row numbers back to view relative */ + match.start.row = match.start.row - grid->view + grid->num_rows; + match.start.row &= grid->num_rows - 1; + match.end.row = match.end.row - grid->view + grid->num_rows; + match.end.row &= grid->num_rows - 1; + + LOG_DBG("match at (view-local coordinates) %dx%d-%dx%d, view=%d", + match.start.row, match.start.col, + match.end.row, match.end.col, grid->view); + + xassert(match.start.row >= 0); + xassert(match.start.row < term->rows); + xassert(match.end.row >= 0); + xassert(match.end.row < term->rows); + + xassert(match.end.row > match.start.row || + (match.end.row == match.start.row && + match.end.col >= match.start.col)); + + /* Continue at next column, next time */ + iter->start.row = match.start.row; + iter->start.col = match.start.col + 1; + + if (iter->start.col >= term->cols) { + iter->start.col = 0; + iter->start.row++; /* Overflow is caught in next iteration */ } - else { - xassert(iter->start.row >= 0); - xassert(iter->start.row < term->rows); - xassert(iter->start.col >= 0); - xassert(iter->start.col < term->cols); - - struct coord abs_start = iter->start; - abs_start.row = grid_row_absolute_in_view(grid, abs_start.row); - - struct coord abs_end = { - term->cols - 1, - grid_row_absolute_in_view(grid, term->rows - 1)}; - - found = find_next(term, SEARCH_FORWARD, abs_start, abs_end, &match); - } - - if (found) { - LOG_DBG("match at %dx%d-%dx%d", - match.start.row, match.start.col, - match.end.row, match.end.col); - - /* Convert absolute row numbers back to view relative */ - match.start.row = match.start.row - grid->view + grid->num_rows; - match.start.row &= grid->num_rows - 1; - match.end.row = match.end.row - grid->view + grid->num_rows; - match.end.row &= grid->num_rows - 1; - - xassert(match.start.row >= 0); - xassert(match.start.row < term->rows); - xassert(match.end.row >= 0); - xassert(match.end.row < term->rows); - - xassert(match.end.row > match.start.row || - (match.end.row == match.start.row && - match.end.col >= match.start.col)); - - if (return_primary_match) { - iter->start.row = 0; - iter->start.col = 0; - } else { - /* Continue at next column, next time */ - iter->start.row = match.start.row; - iter->start.col = match.start.col + 1; - - if (iter->start.col >= term->cols) { - iter->start.col = 0; - iter->start.row++; /* Overflow is caught in next iteration */ - } - - xassert(iter->start.row >= 0); - xassert(iter->start.row <= term->rows); - xassert(iter->start.col >= 0); - xassert(iter->start.col < term->cols); - - if (match.start.row == term->search.match.row && - match.start.col == term->search.match.col) - { - /* Primary match is handled explicitly */ - LOG_DBG("primary match: skipping"); - return search_matches_next(iter); - } - } - return match; - } + xassert(iter->start.row >= 0); + xassert(iter->start.row <= term->rows); + xassert(iter->start.col >= 0); + xassert(iter->start.col < term->cols); + return match; no_match: iter->start.row = -1; @@ -663,15 +614,18 @@ search_match_to_end_of_word(struct terminal *term, bool spaces_only) if (term->search.match_len == 0) return; - xassert(term->selection.coords.end.row != -1); + xassert(term->selection.coords.end.row >= 0); struct grid *grid = term->grid; const bool move_cursor = term->search.cursor == term->search.len; - const struct coord old_end = term->selection.coords.end; + struct coord old_end = selection_get_end(term); struct coord new_end = old_end; struct row *row = NULL; + xassert(new_end.row >= 0); + xassert(new_end.row < grid->num_rows); + /* Advances a coordinate by one column, to the right. Returns * false if we’ve reached the scrollback wrap-around */ #define advance_pos(coord) __extension__ \ @@ -691,12 +645,16 @@ search_match_to_end_of_word(struct terminal *term, bool spaces_only) if (!advance_pos(new_end)) return; + xassert(new_end.row >= 0); + xassert(new_end.row < grid->num_rows); xassert(grid->rows[new_end.row] != NULL); /* Find next word boundary */ - new_end.row -= grid->view; + new_end.row -= grid->view + grid->num_rows; + new_end.row &= grid->num_rows - 1; selection_find_word_boundary_right(term, &new_end, spaces_only); new_end.row += grid->view; + new_end.row &= grid->num_rows - 1; struct coord pos = old_end; row = grid->rows[pos.row]; From 1b5b1d5d925f296619c511a5b12a10f48717efce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 26 Apr 2022 17:40:00 +0200 Subject: [PATCH 0030/1323] changelog: crash when extending selection in search mode --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7ad4371..a03b4c82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,8 @@ box, that does not result in a grid update (for example, when the search criteria did not result in any matches) ([#1040][1040]). * foot freezing in scrollback search mode, using 100% CPU ([#1036][1036]). +* Crash when extending a selection to the next word boundary in + scrollback search mode ([#1036][1036]). [1040]: https://codeberg.org/dnkl/foot/issues/1040 [1036]: https://codeberg.org/dnkl/foot/issues/1036 From b94f540113dd10e4b34f5dda826f732b29b47217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 26 Apr 2022 17:40:20 +0200 Subject: [PATCH 0031/1323] changelog: search mode not always highlighting all matches correctly --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a03b4c82..89c19a0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,8 @@ * foot freezing in scrollback search mode, using 100% CPU ([#1036][1036]). * Crash when extending a selection to the next word boundary in scrollback search mode ([#1036][1036]). +* Scrollback search mode not always highlighting all matches + correctly. [1040]: https://codeberg.org/dnkl/foot/issues/1040 [1036]: https://codeberg.org/dnkl/foot/issues/1036 From 1e87dbc4dc87c9567a7851135d91728796b0696f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 25 Apr 2022 19:55:00 +0200 Subject: [PATCH 0032/1323] search: work around Sway sub-surface unmap bug Unmapping a sub-surface in Sway does not damage the underlying surface. As a result, "committing" a scrollback search will typically leave most of the foot window dimmed. It can be seen when "cancelling" a search as well, but there it's less obvious - only the margins are left dimmed. This is because cancelling a search damaged the current viewport (something that shouldn't be needed). Out of sway, river, weston and mutter, only Sway needs this workaround. This is a workaround for https://github.com/swaywm/sway/issues/6960 --- search.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/search.c b/search.c index 09b59d50..72c589f2 100644 --- a/search.c +++ b/search.c @@ -116,6 +116,11 @@ search_cancel_keep_selection(struct terminal *term) term_xcursor_update(term); render_refresh(term); + + /* Work around Sway bug - unmapping a sub-surface does not damage + * the underlying surface */ + term_damage_margins(term); + term_damage_view(term); } void @@ -795,7 +800,6 @@ execute_binding(struct seat *seat, struct terminal *term, grid->view = ensure_view_is_allocated( term, term->search.original_view); } - term_damage_view(term); search_cancel(term); return true; From 398d96fdb2a18f7652c116639dc17741f5e24404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 26 Apr 2022 17:24:55 +0200 Subject: [PATCH 0033/1323] term: flash: work around Sway sub-surface unmap bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unmapping a sub-surface in Sway does not damage the underlying surface. As a result, the OSC-555 escape (“flash”) will leave yellow margins on ~every second frame. Out of sway, river, weston and mutter, only Sway needs this workaround. This is a workaround for https://github.com/swaywm/sway/issues/6960 Closes #1046 --- terminal.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/terminal.c b/terminal.c index 7d230cdf..9199aa31 100644 --- a/terminal.c +++ b/terminal.c @@ -355,8 +355,12 @@ fdm_flash(struct fdm *fdm, int fd, int events, void *data) (unsigned long long)expiration_count); term->flash.active = false; - term_damage_view(term); render_refresh(term); + + /* Work around Sway bug - unmapping a sub-surface does not damage + * the underlying surface */ + term_damage_margins(term); + term_damage_view(term); return true; } From 3abb23c81c5da902587b5b639eff555fcab5a337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 26 Apr 2022 17:28:36 +0200 Subject: [PATCH 0034/1323] changelog: workaround for Sway bug #6960 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89c19a0e..47ca1107 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,15 @@ ## Unreleased ### Added + +* Workaround for Sway bug [#6960][sway-6960]: scrollback search and + the OSC-555 (“flash”) escape sequence leaves dimmed (search )and + yellow (flash) artifacts ([#1046][1046]). + +[sway-6960]: https://github.com/swaywm/sway/issues/6960 +[1046]: https://codeberg.org/dnkl/foot/issues/1046 + + ### Changed ### Deprecated ### Removed From 93dcb7dc9cf753462bfea40b890a9d68945a5d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 26 Apr 2022 17:52:00 +0200 Subject: [PATCH 0035/1323] changelog: typo: space on the wrong side of the parenthesis --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47ca1107..bb094d93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,7 +42,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]). [sway-6960]: https://github.com/swaywm/sway/issues/6960 From c82c6116ede2706a9d5b28d43e8271ec13be2794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 26 Apr 2022 19:32:08 +0200 Subject: [PATCH 0036/1323] search: regression: crash when moving viewport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5c4ddebc3c57724bd89b959a33f5ad0bd97f482b refactored search_update_selection(), specifically, the logic that moves the viewport. It did so by converting the absolute row number (of the match) to scrollback relative coordinates. This way we could ensure the viewport wasn’t moved “too much” (e.g. beyond the scrollback start). However, grid_row_abs_to_sb() and grid_row_sb_to_abs() doesn’t take a partially filled scrollback into account. This means the row (numbers) it returns may refer to *uninitialized* rows. Since: * The match row itself is valid (we *know* it has text on it) * We *subtract* from it, when setting the new viewport (to center the match on the screen). it’s only the *upper* part of the new viewport that may be uninitialized. I.e. we may have adjusted it too much. So, what we need to do is move the viewport forward until its *first* row is initialized. Then we know the rest will be too. --- search.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/search.c b/search.c index 72c589f2..8f2315a0 100644 --- a/search.c +++ b/search.c @@ -209,6 +209,13 @@ search_update_selection(struct terminal *term, const struct range *match) const int old_view = grid->view; int new_view = grid_row_sb_to_abs(grid, term->rows, rebased_new_view); + /* Scrollback may not be completely filled yet */ + { + const int mask = grid->num_rows - 1; + while (grid->rows[new_view] == NULL) + new_view = (new_view + 1) & mask; + } + #if defined(_DEBUG) /* Verify all to-be-visible rows have been allocated */ for (int r = 0; r < term->rows; r++) From 694938b85bed679c95ac506711475bf06cd80b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 26 Apr 2022 19:47:02 +0200 Subject: [PATCH 0037/1323] search: assert that the match is *inside* the new viewport --- search.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/search.c b/search.c index 8f2315a0..d4460e5b 100644 --- a/search.c +++ b/search.c @@ -222,6 +222,15 @@ search_update_selection(struct terminal *term, const struct range *match) xassert(grid->rows[(new_view + r) & (grid->num_rows - 1)] != NULL); #endif +#if defined(_DEBUG) + { + int rel_start_row = grid_row_abs_to_sb(grid, term->rows, start_row); + int rel_view = grid_row_abs_to_sb(grid, term->rows, new_view); + xassert(rel_view <= rel_start_row); + xassert(rel_start_row < rel_view + term->rows); + } +#endif + /* Update view */ grid->view = new_view; if (new_view != old_view) From 0e9ebf433b589c15e17bd37f73991ecc71e9b3d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 26 Apr 2022 18:24:22 +0200 Subject: [PATCH 0038/1323] search: fix infinite loop when highlighting all matches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit find_next() did not always terminate correctly, causing search_matches_next() to never terminate, which finally leads to an infinite loop when rendering the search overlay surface, while finding all matches to highlight. The problem is that find_next(), after having found the initial matching characters, enters a nested while loop that tries to match the rest of the search criteria. This inner while loop did not check if we’ve reached the last cell, and happily continued past it (eventually wrappping around the scrollback buffer). Closes #1047 --- CHANGELOG.md | 4 +++- search.c | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb094d93..65dd2542 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,7 +58,8 @@ * UI not refreshing when pasting something into the scrollback search box, that does not result in a grid update (for example, when the search criteria did not result in any matches) ([#1040][1040]). -* foot freezing in scrollback search mode, using 100% CPU ([#1036][1036]). +* foot freezing in scrollback search mode, using 100% CPU + ([#1036][1036], [#1047][1047]). * Crash when extending a selection to the next word boundary in scrollback search mode ([#1036][1036]). * Scrollback search mode not always highlighting all matches @@ -66,6 +67,7 @@ [1040]: https://codeberg.org/dnkl/foot/issues/1040 [1036]: https://codeberg.org/dnkl/foot/issues/1036 +[1047]: https://codeberg.org/dnkl/foot/issues/1036 ### Security diff --git a/search.c b/search.c index d4460e5b..b4d06c6c 100644 --- a/search.c +++ b/search.c @@ -375,6 +375,13 @@ find_next(struct terminal *term, enum search_direction direction, if (match_len != term->search.len) { /* Didn't match (completely) */ + + if (match_start_row == abs_end.row && + match_start_col == abs_end.col) + { + break; + } + continue; } @@ -563,10 +570,16 @@ search_matches_next(struct search_match_iterator *iter) xassert(match.end.row >= 0); xassert(match.end.row < term->rows); + /* Assert match end comes *after* the match start */ xassert(match.end.row > match.start.row || (match.end.row == match.start.row && match.end.col >= match.start.col)); + /* Assert the match starts at, or after, the iterator position */ + xassert(match.start.row > iter->start.row || + (match.start.row == iter->start.row && + match.start.col >= iter->start.col)); + /* Continue at next column, next time */ iter->start.row = match.start.row; iter->start.col = match.start.col + 1; From aa4c7c5a30a792a9519b971ff14582dc2704b6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 26 Apr 2022 18:34:18 +0200 Subject: [PATCH 0039/1323] config: add ctrl+shift+v and XF86 paste to SEARCH_CLIPBOARD_PASTE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now bind ctrl+v, ctrl+shift+v, ctrl+y and XF86Paste to pasting from the clipboard into the scrollback search buffer. Why all these? Because we can, and because all are common shortcuts for pasting: * ctrl+v: “normal” apps use this by default * ctrl+shift+v: used in terminals (including foot) * ctrl+y: Emacs * XF86Paste: special keyboard key, for pasting --- CHANGELOG.md | 4 ++++ README.md | 2 +- config.c | 2 ++ doc/foot.1.scd | 2 +- foot.ini | 2 +- 5 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65dd2542..31c05401 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,10 @@ * Workaround for Sway bug [#6960][sway-6960]: scrollback search and the OSC-555 (“flash”) escape sequence leaves dimmed (search) and yellow (flash) artifacts ([#1046][1046]). +* `Control+Shift+v` and `XF86Paste` have been added to the default set + of key bindings that paste from the clipboard into the scrollback + search buffer. This is in addition to the pre-existing `Control+v` + and `Control+y` bindings. [sway-6960]: https://github.com/swaywm/sway/issues/6960 [1046]: https://codeberg.org/dnkl/foot/issues/1046 diff --git a/README.md b/README.md index 5b819bf5..227c528c 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,7 @@ These are the default shortcuts. See `man foot.ini` and the example : Same as ctrl+w, except that the only word separating characters are whitespace characters. -ctrl+v +ctrl+v, ctrl+shift+v, ctrl+y, XF86Paste : Paste from clipboard into the search buffer. shift+insert diff --git a/config.c b/config.c index 10b899e0..c5fad01a 100644 --- a/config.c +++ b/config.c @@ -2686,7 +2686,9 @@ add_default_search_bindings(struct config *conf) {BIND_ACTION_SEARCH_EXTEND_WORD, m_ctrl, {{XKB_KEY_w}}}, {BIND_ACTION_SEARCH_EXTEND_WORD_WS, m_ctrl_shift, {{XKB_KEY_w}}}, {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m_ctrl, {{XKB_KEY_v}}}, + {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m_ctrl_shift, {{XKB_KEY_v}}}, {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m_ctrl, {{XKB_KEY_y}}}, + {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m_none, {{XKB_KEY_XF86Paste}}}, {BIND_ACTION_SEARCH_PRIMARY_PASTE, m_shift, {{XKB_KEY_Insert}}}, }; diff --git a/doc/foot.1.scd b/doc/foot.1.scd index dfbc6651..837da048 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -224,7 +224,7 @@ default) available; see *foot.ini*(5). Same as *ctrl*+*w*, except that the only word separating characters are whitespace characters. -*ctrl*+*v*, *ctrl*+*y* +*ctrl*+*v*, *ctrl*+*shift*+*v*, *ctrl*+*y*, *XF86Paste* Paste from clipboard into the search buffer. *shift*+*insert* diff --git a/foot.ini b/foot.ini index abd3c412..21c49174 100644 --- a/foot.ini +++ b/foot.ini @@ -162,7 +162,7 @@ # delete-next-word=Mod1+d Control+Delete # extend-to-word-boundary=Control+w # extend-to-next-whitespace=Control+Shift+w -# clipboard-paste=Control+v Control+y +# clipboard-paste=Control+v Control+Shift+v Control+y XF86Paste # primary-paste=Shift+Insert [url-bindings] From 32d9895697584988056086cbecdc96375b4b9115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 26 Apr 2022 21:05:17 +0200 Subject: [PATCH 0040/1323] term: reset sixel options when hard resetting the terminal state --- CHANGELOG.md | 2 ++ terminal.c | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31c05401..07c3dfbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,8 @@ scrollback search mode ([#1036][1036]). * Scrollback search mode not always highlighting all matches correctly. +* Sixel options not being reset on hard resets (`\Ec`) + [1040]: https://codeberg.org/dnkl/foot/issues/1040 [1036]: https://codeberg.org/dnkl/foot/issues/1036 diff --git a/terminal.c b/terminal.c index 9199aa31..dcaca033 100644 --- a/terminal.c +++ b/terminal.c @@ -1929,6 +1929,16 @@ term_reset(struct terminal *term, bool hard) tll_free(term->alt.scroll_damage); term->render.last_cursor.row = NULL; term_damage_all(term); + + term->sixel.scrolling = true; + term->sixel.cursor_right_of_graphics = false; + term->sixel.use_private_palette = true; + term->sixel.max_width = SIXEL_MAX_WIDTH; + term->sixel.max_height = SIXEL_MAX_HEIGHT; + term->sixel.palette_size = SIXEL_MAX_COLORS; + free(term->sixel.private_palette); + free(term->sixel.shared_palette); + term->sixel.private_palette = term->sixel.shared_palette = NULL; } static bool From 8356dfac2f8afa854bcf2ceaca26e1b3090deeb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 27 Apr 2022 18:44:17 +0200 Subject: [PATCH 0041/1323] Disable debug logging --- extract.c | 2 +- key-binding.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extract.c b/extract.c index 310d94a3..31c32248 100644 --- a/extract.c +++ b/extract.c @@ -2,7 +2,7 @@ #include #define LOG_MODULE "extract" -#define LOG_ENABLE_DBG 1 +#define LOG_ENABLE_DBG 0 #include "log.h" #include "char32.h" diff --git a/key-binding.c b/key-binding.c index 9133f60c..2135abbc 100644 --- a/key-binding.c +++ b/key-binding.c @@ -3,7 +3,7 @@ #include #define LOG_MODULE "key-binding" -#define LOG_ENABLE_DBG 1 +#define LOG_ENABLE_DBG 0 #include "log.h" #include "config.h" From 76305104486a90c2320dd102c32fe26831ebb658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 27 Apr 2022 18:44:57 +0200 Subject: [PATCH 0042/1323] =?UTF-8?q?selection:=20find=5Fword=5Fboundary?= =?UTF-8?q?=5Fright:=20add=20=E2=80=9Cstop-on-space-to-word-boundary?= =?UTF-8?q?=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When true, selection_find_word_boundary_right() behaves as before - it stops as soon as it encounters a character that isn’t of the same *type* as the “initial” character (the last character in the selection). Take this, for example: The Quick Brown Fox The selection will first stop at the end of “the”, then just *before* “quick”, then at the end of “quick”. Then just *before* “brown”, and then at the end of “brown”, and so on. This suits mouse selections pretty good. But when selection_find_word_boundary_right() is used to extend a search match, it’s better to ignore space-to-word character transitions. That is, we want The Quick Brown Fox to first extend to the end of “the”, then immediately to the end of “quick”, then to the end of “brown”, and so on. Setting the ‘stop_to_space_to_word_boundary’ argument to false results in latter behavior. This is now done by search, when executing the “extend-to-word-boundary” and “extend-to-next-whitespace” key bindings. --- search.c | 2 +- selection.c | 27 +++++++++++++++++++-------- selection.h | 3 ++- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/search.c b/search.c index b4d06c6c..f6d377ea 100644 --- a/search.c +++ b/search.c @@ -686,7 +686,7 @@ search_match_to_end_of_word(struct terminal *term, bool spaces_only) /* Find next word boundary */ new_end.row -= grid->view + grid->num_rows; new_end.row &= grid->num_rows - 1; - selection_find_word_boundary_right(term, &new_end, spaces_only); + selection_find_word_boundary_right(term, &new_end, spaces_only, false); new_end.row += grid->view; new_end.row &= grid->num_rows - 1; diff --git a/selection.c b/selection.c index 091d3ed0..6b57f48c 100644 --- a/selection.c +++ b/selection.c @@ -369,7 +369,8 @@ selection_find_word_boundary_left(struct terminal *term, struct coord *pos, void selection_find_word_boundary_right(struct terminal *term, struct coord *pos, - bool spaces_only) + bool spaces_only, + bool stop_on_space_to_word_boundary) { xassert(pos->row >= 0); xassert(pos->row < term->rows); @@ -395,6 +396,7 @@ selection_find_word_boundary_right(struct terminal *term, struct coord *pos, !initial_is_space && !isword(c, spaces_only, term->conf->word_delimiters); bool initial_is_word = c != 0 && isword(c, spaces_only, term->conf->word_delimiters); + bool have_seen_word = initial_is_word; while (true) { int next_col = pos->col + 1; @@ -435,13 +437,22 @@ selection_find_word_boundary_right(struct terminal *term, struct coord *pos, bool is_word = c != 0 && isword(c, spaces_only, term->conf->word_delimiters); - if (initial_is_space && !is_space) - break; - if (initial_is_delim && !is_delim) - break; + if (stop_on_space_to_word_boundary) { + if (initial_is_space && !is_space) + break; + if (initial_is_delim && !is_delim) + break; + } else { + if (initial_is_space && ((have_seen_word && is_space) || is_delim)) + break; + if (initial_is_delim && ((have_seen_word && is_delim) || is_space)) + break; + } if (initial_is_word && !is_word) break; + have_seen_word = is_word; + pos->col = next_col; pos->row = next_row; } @@ -522,7 +533,7 @@ selection_start(struct terminal *term, int col, int row, case SELECTION_WORD_WISE: { struct coord start = {col, row}, end = {col, row}; selection_find_word_boundary_left(term, &start, spaces_only); - selection_find_word_boundary_right(term, &end, spaces_only); + selection_find_word_boundary_right(term, &end, spaces_only, true); term->selection.coords.start = (struct coord){ start.col, term->grid->view + start.row}; @@ -863,7 +874,7 @@ selection_update(struct terminal *term, int col, int row) case SELECTION_RIGHT: { struct coord end = {col, row}; selection_find_word_boundary_right( - term, &end, term->selection.spaces_only); + term, &end, term->selection.spaces_only, true); new_end = (struct coord){end.col, term->grid->view + end.row}; break; } @@ -1013,7 +1024,7 @@ selection_extend_normal(struct terminal *term, int col, int row, struct coord pivot_end = pivot_start; selection_find_word_boundary_left(term, &pivot_start, spaces_only); - selection_find_word_boundary_right(term, &pivot_end, spaces_only); + selection_find_word_boundary_right(term, &pivot_end, spaces_only, true); term->selection.pivot.start = (struct coord){pivot_start.col, term->grid->view + pivot_start.row}; diff --git a/selection.h b/selection.h index 0a6ece91..3d0c224e 100644 --- a/selection.h +++ b/selection.h @@ -78,7 +78,8 @@ void selection_stop_scroll_timer(struct terminal *term); void selection_find_word_boundary_left( struct terminal *term, struct coord *pos, bool spaces_only); void selection_find_word_boundary_right( - struct terminal *term, struct coord *pos, bool spaces_only); + struct terminal *term, struct coord *pos, bool spaces_only, + bool stop_on_space_to_word_boundary); struct coord selection_get_start(const struct terminal *term); struct coord selection_get_end(const struct terminal *term); From 5308b8cdb8fe3c14c9a48954880b2f858d80df35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 27 Apr 2022 18:52:08 +0200 Subject: [PATCH 0043/1323] =?UTF-8?q?changelog:=20changed=20behavior=20of?= =?UTF-8?q?=20=E2=80=9Cextend-to-word-boundary=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07c3dfbf..7c8c7511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,12 @@ ### Changed + +* Scrollback search’s `extend-to-word-boundary` no longer stops at + space-to-word boundaries, making selection extension feel more + natural. + + ### Deprecated ### Removed ### Fixed From 225f8e659e157fcc72e95ed0617e931d8d89fbba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 27 Apr 2022 20:05:51 +0200 Subject: [PATCH 0044/1323] changelog: prepare for 1.12.1 --- CHANGELOG.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c8c7511..4b964637 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.12.1](#1-12-1) * [1.12.0](#1-12-0) * [1.11.0](#1-11-0) * [1.10.3](#1-10-3) @@ -38,7 +38,8 @@ * [1.2.0](#1-2-0) -## Unreleased +## 1.12.1 + ### Added * Workaround for Sway bug [#6960][sway-6960]: scrollback search and @@ -60,8 +61,6 @@ natural. -### Deprecated -### Removed ### Fixed * build: missing symbols when linking the `pgo` helper binary. @@ -76,16 +75,11 @@ correctly. * Sixel options not being reset on hard resets (`\Ec`) - [1040]: https://codeberg.org/dnkl/foot/issues/1040 [1036]: https://codeberg.org/dnkl/foot/issues/1036 [1047]: https://codeberg.org/dnkl/foot/issues/1036 -### Security -### Contributors - - ## 1.12.0 ### Added From e95269447f1b62384dc64707bb19ebe1006386ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 27 Apr 2022 20:06:09 +0200 Subject: [PATCH 0045/1323] meson: bump version to 1.12.1 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 49611dcd..7280a049 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.12.0', + version: '1.12.1', license: 'MIT', meson_version: '>=0.58.0', default_options: [ From de201ead2e07985fa16d9921f03083053449e932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 27 Apr 2022 20:09:16 +0200 Subject: [PATCH 0046/1323] =?UTF-8?q?changelog:=20add=20new=20=E2=80=98unr?= =?UTF-8?q?eleased=E2=80=99=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b964637..9bf60415 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.12.1](#1-12-1) * [1.12.0](#1-12-0) * [1.11.0](#1-11-0) @@ -38,6 +39,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.12.1 ### Added From 7045c177fdcef280c3d84ed368313c2e13f77915 Mon Sep 17 00:00:00 2001 From: Craig Barnes Date: Wed, 27 Apr 2022 19:18:43 +0100 Subject: [PATCH 0047/1323] commands: fix LOG_DBG() usage in cmd_scrollback_{up,down} The "end" variable was removed from both of these functions in commit cb43c581508d8, but the references to it in the expansion of LOG_DBG() weren't. --- commands.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/commands.c b/commands.c index d6ec9fa0..9d00067d 100644 --- a/commands.c +++ b/commands.c @@ -59,8 +59,8 @@ cmd_scrollback_up(struct terminal *term, int rows) xassert(grid->rows[(new_view + r) & (grid->num_rows - 1)] != NULL); #endif - LOG_DBG("scrollback UP: %d -> %d (offset = %d, end = %d, rows = %d)", - view, new_view, offset, end, grid_rows); + LOG_DBG("scrollback UP: %d -> %d (offset = %d, rows = %d)", + view, new_view, offset, grid_rows); selection_view_up(term, new_view); term->grid->view = new_view; @@ -113,8 +113,8 @@ cmd_scrollback_down(struct terminal *term, int rows) xassert(grid->rows[(new_view + r) & (grid_rows - 1)] != NULL); #endif - LOG_DBG("scrollback DOWN: %d -> %d (offset = %d, end = %d, rows = %d)", - view, new_view, offset, end, grid_rows); + LOG_DBG("scrollback DOWN: %d -> %d (offset = %d, rows = %d)", + view, new_view, offset, grid_rows); selection_view_down(term, new_view); term->grid->view = new_view; From bd8dd9ff7ed34bc44a03f7c559c3a8cf21b856c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 28 Apr 2022 19:29:06 +0200 Subject: [PATCH 0048/1323] changelog: fix URL in #1047 issue link --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bf60415..91646cb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,7 +88,7 @@ [1040]: https://codeberg.org/dnkl/foot/issues/1040 [1036]: https://codeberg.org/dnkl/foot/issues/1036 -[1047]: https://codeberg.org/dnkl/foot/issues/1036 +[1047]: https://codeberg.org/dnkl/foot/issues/1047 ## 1.12.0 From ea1aac88db3a6d9f368406676c3e5a0cedcafd57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 3 May 2022 19:37:04 +0200 Subject: [PATCH 0049/1323] url-mode: add support for XDG activation when opening URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First, add a ‘token’ argument to spawn(). When non-NULL, spawn() will set the ‘XDG_ACTIVATION_TOKEN’ environment variable in the forked process. If DISPLAY is non-NULL, we also set DESKTOP_STARTUP_ID, for compatibility with X11 applications. Note that failing to set either of these environment variables are considered non-fatal - i.e. we ignore failures. Next, add a helper function, wayl_get_activation_token(), to generate an XDG activation token, and call a user-provided callback when it’s ‘done (since token generation is asynchronous). This function takes an optional ‘seat’ and ‘serial’ arguments - when both are non-NULL/zero, we set the serial on the token. ‘win’ is a required argument, used to set the surface on the token. Re-write wayl_win_set_urgent() to use the new helper function. Finally, rewrite activate_url() to first try to get an activation token (and spawn the URL launcher in the token callback). If that fails, or if we don’t have XDG activation support, spawn the URL launcher immediately (like before this patch). Closes #1058 --- CHANGELOG.md | 6 +++ input.c | 3 +- notify.c | 2 +- spawn.c | 15 ++++++- spawn.h | 3 +- terminal.c | 5 ++- url-mode.c | 107 ++++++++++++++++++++++++++++++++++++------------ wayland.c | 113 +++++++++++++++++++++++++++++++++++---------------- wayland.h | 24 ++++++++++- 9 files changed, 209 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91646cb1..68e67049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,12 @@ ## Unreleased ### Added + +* XDG activation support when opening URLs ([#1058][1058]). + +[1058]: https://codeberg.org/dnkl/foot/issues/1058 + + ### Changed ### Deprecated ### Removed diff --git a/input.c b/input.c index 961b73ba..916cbd38 100644 --- a/input.c +++ b/input.c @@ -281,7 +281,8 @@ execute_binding(struct seat *seat, struct terminal *term, } } - if (!spawn(term->reaper, NULL, binding->aux->pipe.args, pipe_fd[0], stdout_fd, stderr_fd)) + if (!spawn(term->reaper, NULL, binding->aux->pipe.args, + pipe_fd[0], stdout_fd, stderr_fd, NULL)) goto pipe_err; /* Close read end */ diff --git a/notify.c b/notify.c index 13cee895..8180477d 100644 --- a/notify.c +++ b/notify.c @@ -48,7 +48,7 @@ notify_notify(const struct terminal *term, const char *title, const char *body) /* Redirect stdin to /dev/null, but ignore failure to open */ int devnull = open("/dev/null", O_RDONLY); - spawn(term->reaper, NULL, argv, devnull, -1, -1); + spawn(term->reaper, NULL, argv, devnull, -1, -1, NULL); if (devnull >= 0) close(devnull); diff --git a/spawn.c b/spawn.c index 8c5c33d2..7c6641da 100644 --- a/spawn.c +++ b/spawn.c @@ -17,7 +17,8 @@ bool spawn(struct reaper *reaper, const char *cwd, char *const argv[], - int stdin_fd, int stdout_fd, int stderr_fd) + int stdin_fd, int stdout_fd, int stderr_fd, + const char *xdg_activation_token) { int pipe_fds[2] = {-1, -1}; if (pipe2(pipe_fds, O_CLOEXEC) < 0) { @@ -47,14 +48,24 @@ spawn(struct reaper *reaper, const char *cwd, char *const argv[], /* Restore ignored (SIG_IGN) signals */ struct sigaction dfl = {.sa_handler = SIG_DFL}; sigemptyset(&dfl.sa_mask); - if (sigaction(SIGHUP, &dfl, NULL) < 0) + if (sigaction(SIGHUP, &dfl, NULL) < 0 || + sigaction(SIGPIPE, &dfl, NULL) < 0) + { goto child_err; + } if (cwd != NULL && chdir(cwd) < 0) { LOG_WARN("failed to change working directory to %s: %s", cwd, strerror(errno)); } + if (xdg_activation_token != NULL) { + setenv("XDG_ACTIVATION_TOKEN", xdg_activation_token, 1); + + if (getenv("DISPLAY") != NULL) + setenv("DESKTOP_STARTUP_ID", xdg_activation_token, 1); + } + bool close_stderr = stderr_fd >= 0; bool close_stdout = stdout_fd >= 0 && stdout_fd != stderr_fd; bool close_stdin = stdin_fd >= 0 && stdin_fd != stdout_fd && stdin_fd != stderr_fd; diff --git a/spawn.h b/spawn.h index c6f9582e..0fc95041 100644 --- a/spawn.h +++ b/spawn.h @@ -5,7 +5,8 @@ #include "reaper.h" bool spawn(struct reaper *reaper, const char *cwd, char *const argv[], - int stdin_fd, int stdout_fd, int stderr_fd); + int stdin_fd, int stdout_fd, int stderr_fd, + const char *xdg_activation_token); bool spawn_expand_template( const struct config_spawn_template *template, diff --git a/terminal.c b/terminal.c index dcaca033..960d6d24 100644 --- a/terminal.c +++ b/terminal.c @@ -3119,7 +3119,8 @@ term_bell(struct terminal *term) (!term->kbd_focus || term->conf->bell.command_focused)) { int devnull = open("/dev/null", O_RDONLY); - spawn(term->reaper, NULL, term->conf->bell.command.argv.args, devnull, -1, -1); + spawn(term->reaper, NULL, term->conf->bell.command.argv.args, + devnull, -1, -1, NULL); if (devnull >= 0) close(devnull); @@ -3131,7 +3132,7 @@ term_spawn_new(const struct terminal *term) { return spawn( term->reaper, term->cwd, (char *const []){term->foot_exe, NULL}, - -1, -1, -1); + -1, -1, -1, NULL); } void diff --git a/url-mode.c b/url-mode.c index 5ae1fd55..538b60f0 100644 --- a/url-mode.c +++ b/url-mode.c @@ -50,8 +50,86 @@ execute_binding(struct seat *seat, struct terminal *term, return true; } +static bool +spawn_url_launcher_with_token(struct terminal *term, + const char *url, + const char *xdg_activation_token) +{ + size_t argc; + char **argv; + + int dev_null = open("/dev/null", O_RDWR); + + if (dev_null < 0) { + LOG_ERRNO("failed to open /dev/null"); + return false; + } + + bool ret = false; + + if (spawn_expand_template( + &term->conf->url.launch, 1, + (const char *[]){"url"}, + (const char *[]){url}, + &argc, &argv)) + { + ret = spawn(term->reaper, term->cwd, argv, + dev_null, dev_null, dev_null, xdg_activation_token); + + for (size_t i = 0; i < argc; i++) + free(argv[i]); + free(argv); + } + + close(dev_null); + return ret; +} + +#if defined(HAVE_XDG_ACTIVATION) +struct spawn_activation_context { + struct terminal *term; + char *url; +}; + static void -activate_url(struct seat *seat, struct terminal *term, const struct url *url) +activation_token_done(const char *token, void *data) +{ + struct spawn_activation_context *ctx = data; + + spawn_url_launcher_with_token(ctx->term, ctx->url, token); + free(ctx->url); + free(ctx); +} +#endif + +static bool +spawn_url_launcher(struct seat *seat, struct terminal *term, const char *url, + uint32_t serial) +{ +#if defined(HAVE_XDG_ACTIVATION) + struct spawn_activation_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct spawn_activation_context){ + .term = term, + .url = xstrdup(url), + }; + + if (wayl_get_activation_token( + seat->wayl, seat, serial, term->window, &activation_token_done, ctx)) + { + /* Context free:d by callback */ + return true; + } + + free(ctx->url); + free(ctx); +#endif + + return spawn_url_launcher_with_token(term, url, NULL); +} + +static void +activate_url(struct seat *seat, struct terminal *term, const struct url *url, + uint32_t serial) { char *url_string = NULL; @@ -87,30 +165,7 @@ activate_url(struct seat *seat, struct terminal *term, const struct url *url) case URL_ACTION_LAUNCH: case URL_ACTION_PERSISTENT: { - size_t argc; - char **argv; - - int dev_null = open("/dev/null", O_RDWR); - - if (dev_null < 0) { - LOG_ERRNO("failed to open /dev/null"); - break; - } - - if (spawn_expand_template( - &term->conf->url.launch, 1, - (const char *[]){"url"}, - (const char *[]){url_string}, - &argc, &argv)) - { - spawn(term->reaper, term->cwd, argv, dev_null, dev_null, dev_null); - - for (size_t i = 0; i < argc; i++) - free(argv[i]); - free(argv); - } - - close(dev_null); + spawn_url_launcher(seat, term, url_string, serial); break; } } @@ -207,7 +262,7 @@ urls_input(struct seat *seat, struct terminal *term, } if (match) { - activate_url(seat, term, match); + activate_url(seat, term, match, serial); switch (match->action) { case URL_ACTION_COPY: diff --git a/wayland.c b/wayland.c index d65a571b..591fa4b7 100644 --- a/wayland.c +++ b/wayland.c @@ -1566,8 +1566,12 @@ wayl_win_destroy(struct wl_window *win) shm_purge(term->render.chains.csd); #if defined(HAVE_XDG_ACTIVATION) - if (win->xdg_activation_token != NULL) - xdg_activation_token_v1_destroy(win->xdg_activation_token); + tll_foreach(win->xdg_tokens, it) { + xdg_activation_token_v1_destroy(it->item->xdg_token); + free(it->item); + + tll_remove(win->xdg_tokens, it); + } #endif if (win->frame_callback != NULL) wl_callback_destroy(win->frame_callback); @@ -1706,51 +1710,21 @@ wayl_roundtrip(struct wayland *wayl) #if defined(HAVE_XDG_ACTIVATION) static void -activation_token_done(void *data, struct xdg_activation_token_v1 *xdg_token, - const char *token) +activation_token_for_urgency_done(const char *token, void *data) { struct wl_window *win = data; struct wayland *wayl = win->term->wl; - LOG_DBG("activation token: %s", token); - xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface); - - xassert(win->xdg_activation_token == xdg_token); - xdg_activation_token_v1_destroy(xdg_token); - win->xdg_activation_token = NULL; } - -static const struct xdg_activation_token_v1_listener activation_token_listener = { - .done = &activation_token_done, -}; #endif /* HAVE_XDG_ACTIVATION */ bool wayl_win_set_urgent(struct wl_window *win) { #if defined(HAVE_XDG_ACTIVATION) - struct wayland *wayl = win->term->wl; - - if (wayl->xdg_activation == NULL) - return false; - - if (win->xdg_activation_token != NULL) - return true; - - struct xdg_activation_token_v1 *token = - xdg_activation_v1_get_activation_token(wayl->xdg_activation); - - if (token == NULL) { - LOG_ERR("failed to retrieve XDG activation token"); - return false; - } - - xdg_activation_token_v1_add_listener(token, &activation_token_listener, win); - xdg_activation_token_v1_set_surface(token, win->surface); - xdg_activation_token_v1_commit(token); - win->xdg_activation_token = token; - return true; + return wayl_get_activation_token( + win->term->wl, NULL, 0, win, &activation_token_for_urgency_done, win); #else return false; #endif @@ -1833,3 +1807,72 @@ wayl_win_subsurface_destroy(struct wl_surf_subsurf *surf) surf->surf = NULL; surf->sub = NULL; } + +#if defined(HAVE_XDG_ACTIVATION) + +static void +activation_token_done(void *data, struct xdg_activation_token_v1 *xdg_token, + const char *token) +{ + LOG_DBG("XDG activation token done: %s", token); + + struct xdg_activation_token_context *ctx = data; + struct wl_window *win = ctx->win; + + ctx->cb(token, ctx->cb_data); + + tll_foreach(win->xdg_tokens, it) { + if (it->item->xdg_token != xdg_token) + continue; + + xassert(win == it->item->win); + + free(ctx); + xdg_activation_token_v1_destroy(xdg_token); + tll_remove(win->xdg_tokens, it); + return; + } + + xassert(false); +} + +static const struct +xdg_activation_token_v1_listener activation_token_listener = { + .done = &activation_token_done, +}; + +bool +wayl_get_activation_token( + struct wayland *wayl, struct seat *seat, uint32_t serial, + struct wl_window *win, + void (*cb)(const char *token, void *data), void *cb_data) +{ + if (wayl->xdg_activation == NULL) + return false; + + struct xdg_activation_token_v1 *token = + xdg_activation_v1_get_activation_token(wayl->xdg_activation); + + if (token == NULL) { + LOG_ERR("failed to retrieve XDG activation token"); + return false; + } + + struct xdg_activation_token_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct xdg_activation_token_context){ + .win = win, + .xdg_token = token, + .cb = cb, + .cb_data = cb_data, + }; + tll_push_back(win->xdg_tokens, ctx); + + if (seat != NULL && serial != 0) + xdg_activation_token_v1_set_serial(token, serial, seat->wl_seat); + + xdg_activation_token_v1_set_surface(token, win->surface); + xdg_activation_token_v1_add_listener(token, &activation_token_listener, ctx); + xdg_activation_token_v1_commit(token); + return true; +} +#endif diff --git a/wayland.h b/wayland.h index 2e8dcd98..8f59eb98 100644 --- a/wayland.h +++ b/wayland.h @@ -295,6 +295,22 @@ struct wl_url { enum csd_mode {CSD_UNKNOWN, CSD_NO, CSD_YES}; +#if defined(HAVE_XDG_ACTIVATION) +typedef void (*activation_token_cb_t)(const char *token, void *data); + +/* + * This context holds data used both in the token::done callback, and + * when cleaning up created, by not-yet-done tokens in + * wayl_win_destroy(). + */ +struct xdg_activation_token_context { + struct wl_window *win; /* Need for win->xdg_tokens */ + struct xdg_activation_token_v1 *xdg_token; /* Used to match token in done() */ + activation_token_cb_t cb; /* User provided callback */ + void *cb_data; /* Callback user pointer */ +}; +#endif + struct wayland; struct wl_window { struct terminal *term; @@ -302,7 +318,7 @@ struct wl_window { struct xdg_surface *xdg_surface; struct xdg_toplevel *xdg_toplevel; #if defined(HAVE_XDG_ACTIVATION) - struct xdg_activation_token_v1 *xdg_activation_token; + tll(struct xdg_activation_token_context *) xdg_tokens; #endif struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration; @@ -417,3 +433,9 @@ bool wayl_win_subsurface_new_with_custom_parent( struct wl_window *win, struct wl_surface *parent, struct wl_surf_subsurf *surf, bool allow_pointer_input); void wayl_win_subsurface_destroy(struct wl_surf_subsurf *surf); + +#if defined(HAVE_XDG_ACTIVATION) +bool wayl_get_activation_token( + struct wayland *wayl, struct seat *seat, uint32_t serial, + struct wl_window *win, activation_token_cb_t cb, void *cb_data); +#endif From f14fc120ad1fc618ac225b26d61f4f8459f84b95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 6 May 2022 10:39:49 +0200 Subject: [PATCH 0050/1323] pgo: add xdg_activation_token parameter to spawn() stub --- pgo/pgo.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pgo/pgo.c b/pgo/pgo.c index 92d97dbf..fec2d26c 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -98,7 +98,8 @@ bool wayl_win_set_urgent(struct wl_window *win) { return true; } bool spawn(struct reaper *reaper, const char *cwd, char *const argv[], - int stdin_fd, int stdout_fd, int stderr_fd) + int stdin_fd, int stdout_fd, int stderr_fd, + const char *xdg_activation_token) { return true; } From 3431619d076644400530e1a7a87d34914436c952 Mon Sep 17 00:00:00 2001 From: Lorenz Date: Mon, 9 May 2022 06:16:05 +0200 Subject: [PATCH 0051/1323] Themes: Add 'Monokai Pro' theme Signed-off-by: Lorenz --- themes/monokai-pro | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 themes/monokai-pro diff --git a/themes/monokai-pro b/themes/monokai-pro new file mode 100644 index 00000000..eecdd3f7 --- /dev/null +++ b/themes/monokai-pro @@ -0,0 +1,21 @@ +# Monokai Pro + +[colors] +background=2D2A2E +foreground=FCFCFA +regular0=403E41 +regular1=FF6188 +regular2=A9DC76 +regular3=FFD866 +regular4=FC9867 +regular5=AB9DF2 +regular6=78DCE8 +regular7=FCFCFA +bright0=727072 +bright1=FF6188 +bright2=A9DC76 +bright3=FFD866 +bright4=FC9867 +bright5=AB9DF2 +bright6=78DCE8 +bright7=FCFCFA From fc67bff9c0732f205b6850e4e6585355fc565482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 11 May 2022 17:58:18 +0200 Subject: [PATCH 0052/1323] terminal: move viewport when part of it is scrolled out If the viewport is at the top of the scrollback, and a program is scrolling (e.g. by emitting newlines), the viewport needs to be moved. Otherwise, the top of the viewport will show the *bottom* of the scrollback (i.e. the newly emitted lines), and the bottom of the viewport shows the top of the scrollback. How do we detect if the viewport needs to be moved? We convert its absolute row number to a scrollback relative row. This number is also the maximum number of rows we can scroll without the viewport being scrolled out. In other words, if the number of rows to scroll is larger than the viewports scrollback relative row number, the viewport needs to moved. How much do we need to move it? The difference between the number of rows to scroll, and the viewports scrollback relative row number. Example: if the viewport is at the very top of the scrollback, its scrollback relative row number is 0. In this case, it needs to be moved the same number of rows as is being scrolled. --- CHANGELOG.md | 5 +++++ terminal.c | 17 +++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68e67049..cf54368d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,11 @@ ### Deprecated ### Removed ### Fixed + +* Graphical corruption when viewport is at the top of the scrollback, + and the output is scrolling. + + ### Security ### Contributors diff --git a/terminal.c b/terminal.c index 960d6d24..09c9caea 100644 --- a/terminal.c +++ b/terminal.c @@ -23,6 +23,7 @@ #include "log.h" #include "async.h" +#include "commands.h" #include "config.h" #include "debug.h" #include "extract.h" @@ -34,9 +35,9 @@ #include "reaper.h" #include "render.h" #include "selection.h" +#include "shm.h" #include "sixel.h" #include "slave.h" -#include "shm.h" #include "spawn.h" #include "url-mode.h" #include "util.h" @@ -2544,13 +2545,22 @@ term_scroll_partial(struct terminal *term, struct scroll_region region, int rows sixel_scroll_up(term, rows); + /* How many lines from the scrollback start is the current viewport? */ + int view_sb_start_distance = grid_row_abs_to_sb( + term->grid, term->rows, term->grid->view); + bool view_follows = term->grid->view == term->grid->offset; term->grid->offset += rows; term->grid->offset &= term->grid->num_rows - 1; - if (view_follows) { + if (likely(view_follows)) { selection_view_down(term, term->grid->offset); term->grid->view = term->grid->offset; + } else if (unlikely(rows > view_sb_start_distance)) { + /* Part of current view is being scrolled out */ + int new_view = grid_row_sb_to_abs(term->grid, term->rows, 0); + selection_view_down(term, new_view); + cmd_scrollback_down(term, rows - view_sb_start_distance); } /* Top non-scrolling region. */ @@ -2611,8 +2621,7 @@ term_scroll_reverse_partial(struct terminal *term, bool view_follows = term->grid->view == term->grid->offset; term->grid->offset -= rows; - while (term->grid->offset < 0) - term->grid->offset += term->grid->num_rows; + term->grid->offset += term->grid->num_rows; term->grid->offset &= term->grid->num_rows - 1; xassert(term->grid->offset >= 0); From 200c5cbc7913f3c9aca45792669c7ede2d087a47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 11 May 2022 21:17:52 +0200 Subject: [PATCH 0053/1323] wayland: throttle xdg activation token requests for window urgency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When XDG activation support was added to URL mode, we introduced a regression, where it is possible to flood the Wayland socket with XDG activation token requests. Start foot with “foot -o bell.urgency=yes”, then run: while true; do echo -en ‘\a’; done Finally, switch keyboard focus to another window. Foot crashes. Throttle the token requests by limiting the number of outstanding urgency token requests to 1. Closes #1065 --- wayland.c | 18 +++++++++++++++--- wayland.h | 1 + 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/wayland.c b/wayland.c index 591fa4b7..b15cd491 100644 --- a/wayland.c +++ b/wayland.c @@ -1715,6 +1715,7 @@ activation_token_for_urgency_done(const char *token, void *data) struct wl_window *win = data; struct wayland *wayl = win->term->wl; + win->urgency_token_is_pending = false; xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface); } #endif /* HAVE_XDG_ACTIVATION */ @@ -1723,11 +1724,22 @@ bool wayl_win_set_urgent(struct wl_window *win) { #if defined(HAVE_XDG_ACTIVATION) - return wayl_get_activation_token( + if (win->urgency_token_is_pending) { + /* We already have a pending token. Don’t request another one, + * to avoid flooding the Wayland socket */ + return true; + } + + bool success = wayl_get_activation_token( win->term->wl, NULL, 0, win, &activation_token_for_urgency_done, win); -#else - return false; + + if (success) { + win->urgency_token_is_pending = true; + return true; + } #endif + + return false; } bool diff --git a/wayland.h b/wayland.h index 8f59eb98..daa3e65d 100644 --- a/wayland.h +++ b/wayland.h @@ -319,6 +319,7 @@ struct wl_window { struct xdg_toplevel *xdg_toplevel; #if defined(HAVE_XDG_ACTIVATION) tll(struct xdg_activation_token_context *) xdg_tokens; + bool urgency_token_is_pending; #endif struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration; From 834beb966ed1f41904c21d4816b6e5e7890d6f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 12 May 2022 11:53:08 +0200 Subject: [PATCH 0054/1323] doc: benchmarks: update with laptop results for 1.12.1 (r9) --- doc/benchmark.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/doc/benchmark.md b/doc/benchmark.md index ceee1b60..531a25c7 100644 --- a/doc/benchmark.md +++ b/doc/benchmark.md @@ -44,7 +44,7 @@ Scrollback: 10000 lines | unicode | 10.19 | 11.27 | 14.72 | 787.77 | 4741.00 | -## 2021-03-20 +## 2022-05-12 ### System @@ -67,15 +67,15 @@ Scrollback=10000 lines ### Results -| Benchmark (times in ms) | Foot (GCC+PGO) 1.9.2 | Foot 1.9.2 | Alacritty 0.9.0 | URxvt 9.26 | XTerm 369 | -|-------------------------------|---------------------:|-----------:|----------------:|-----------:|----------:| -| cursor motion | 13.50 | 16.32 | 27.10 | 23.46 | 1415.38 | -| dense cells | 38.77 | 53.13 | 89.36 | 2007.00 | 2126.60 | -| light cells | 7.73 | 8.72 | 20.35 | 21.06 | 113.34 | -| scrollling | 150.27 | 153.76 | 145.07 | 139.23 | 10088.00 | -| scrolling bottom region | 144.88 | 148.44 | 129.13 | 156.86 | 10166.00 | -| scrolling bottom small region | 142.45 | 137.81 | 167.63 | 183.35 | 9831.50 | -| scrolling fullscreen | 11.23 | 11.91 | 20.12 | 21.21 | 290.80 | -| scrolling top region | 143.80 | 147.37 | 148.63 | 489.57 | 10029.00 | -| scrolling top small region | 139.76 | 144.37 | 165.97 | 308.76 | 9877.00 | -| unicode | 21.94 | 21.50 | 27.72 | 1344.88 | 7402.00 | +| Benchmark (times in ms) | Foot (GCC+PGO) 1.12.1 | Foot 1.12.1 | Alacritty 0.10.1 | URxvt 9.26 | XTerm 372 | +|-------------------------------|----------------------:|------------:|-----------------:|-----------:|----------:| +| cursor motion | 15.03 | 16.74 | 23.22 | 24.14 | 1381.63 | +| dense cells | 43.56 | 54.10 | 89.43 | 1807.17 | 1945.50 | +| light cells | 7.96 | 9.66 | 20.19 | 21.31 | 122.44 | +| scrollling | 146.02 | 150.47 | 129.22 | 129.84 | 10140.00 | +| scrolling bottom region | 138.36 | 137.42 | 117.06 | 141.87 | 10136.00 | +| scrolling bottom small region | 137.40 | 134.66 | 128.97 | 208.77 | 9930.00 | +| scrolling fullscreen | 11.66 | 12.02 | 19.69 | 21.96 | 315.80 | +| scrolling top region | 143.81 | 133.47 | 132.51 | 475.81 | 10267.00 | +| scrolling top small region | 133.72 | 135.32 | 145.10 | 314.13 | 10074.00 | +| unicode | 20.89 | 21.78 | 26.11 | 5687.00 | 15740.00 | From 56e5855fff4b2f9728f851a0b7ab7ae954a732de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 12 May 2022 13:10:24 +0200 Subject: [PATCH 0055/1323] doc: benchmarks: update with desktop results for 1.12.1 (r9) --- doc/benchmark.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/doc/benchmark.md b/doc/benchmark.md index 531a25c7..f77b8268 100644 --- a/doc/benchmark.md +++ b/doc/benchmark.md @@ -8,7 +8,7 @@ All benchmarks are done using [vtebench](https://github.com/alacritty/vtebench): ./target/release/vtebench -b ./benchmarks --dat /tmp/ ``` -## 2021-06-25 +## 2022-05-12 ### System @@ -30,18 +30,18 @@ Scrollback: 10000 lines ### Results -| Benchmark (times in ms) | Foot (GCC+PGO) 1.9.2 | Foot 1.9.2 | Alacritty 0.9.0 | URxvt 9.26 | XTerm 369 | -|-------------------------------|---------------------:|-----------:|----------------:|-----------:|----------:| -| cursor motion | 13.69 | 15.63 | 29.16 | 23.69 | 1341.75 | -| dense cells | 40.77 | 50.76 | 92.39 | 13912.00 | 1959.00 | -| light cells | 5.41 | 6.49 | 12.25 | 16.14 | 66.21 | -| scrollling | 125.43 | 133.00 | 110.90 | 98.29 | 4010.67 | -| scrolling bottom region | 111.90 | 103.95 | 106.35 | 103.65 | 3787.00 | -| scrolling bottom small region | 120.93 | 112.48 | 129.61 | 137.21 | 3796.67 | -| scrolling fullscreen | 5.42 | 5.67 | 11.52 | 12.00 | 124.33 | -| scrolling top region | 110.66 | 107.61 | 100.52 | 340.90 | 3835.33 | -| scrolling top small region | 120.48 | 111.66 | 129.62 | 213.72 | 3805.33 | -| unicode | 10.19 | 11.27 | 14.72 | 787.77 | 4741.00 | +| Benchmark (times in ms) | Foot (GCC+PGO) 1.12.1 | Foot 1.12.1 | Alacritty 0.10.1 | URxvt 9.26 | XTerm 372 | +|-------------------------------|----------------------:|------------:|-----------------:|-----------:|----------:| +| cursor motion | 10.40 | 14.07 | 24.97 | 23.38 | 1622.86 | +| dense cells | 29.58 | 45.46 | 97.45 | 10828.00 | 2323.00 | +| light cells | 4.34 | 4.40 | 12.84 | 12.17 | 49.81 | +| scrollling | 135.31 | 116.35 | 121.69 | 108.30 | 4041.33 | +| scrolling bottom region | 118.19 | 109.70 | 105.26 | 118.80 | 3875.00 | +| scrolling bottom small region | 132.41 | 122.11 | 122.83 | 151.30 | 3839.67 | +| scrolling fullscreen | 5.70 | 5.66 | 10.92 | 12.09 | 124.25 | +| scrolling top region | 144.19 | 121.78 | 135.81 | 159.24 | 3858.33 | +| scrolling top small region | 135.95 | 119.01 | 115.46 | 216.55 | 3872.67 | +| unicode | 11.56 | 10.92 | 15.94 | 1012.27 | 4779.33 | ## 2022-05-12 From 62fe452cc29d47d6e5d8996f85a04ea9c91ce252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 4 May 2022 17:33:34 +0200 Subject: [PATCH 0056/1323] meson: add -Dsystemd-units-dir= meson command line option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows package maintainers to override the location to which our systemd service files are installed. It’s value is an *absolute* path, and *not* relative ${prefix}. The default is ${systemduserunitdir}. --- CHANGELOG.md | 2 ++ INSTALL.md | 19 ++++++++++--------- meson.build | 8 +++++++- meson_options.txt | 3 +++ 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf54368d..b03f3f69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,9 +40,11 @@ ## Unreleased + ### Added * XDG activation support when opening URLs ([#1058][1058]). +* `-Dsystemd-units-dir=` meson command line option. [1058]: https://codeberg.org/dnkl/foot/issues/1058 diff --git a/INSTALL.md b/INSTALL.md index 1ce5260e..ae0598d8 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -140,15 +140,16 @@ mkdir -p bld/release && cd bld/release Available compile-time options: -| Option | Type | Default | Description | Extra dependencies | -|--------------------------------------|---------|-----------------------|------------------------------------------------|--------------------| -| `-Ddocs` | feature | `auto` | Builds and install documentation | scdoc | -| `-Dtests` | bool | `true` | Build tests (adds a `ninja test` build target) | none | -| `-Dime` | bool | `true` | Enables IME support | None | -| `-Dgrapheme-clustering` | feature | `auto` | Enables grapheme clustering | libutf8proc | -| `-Dterminfo` | feature | `enabled` | Build and install terminfo files | tic (ncurses) | -| `-Ddefault-terminfo` | string | `foot` | Default value of `TERM` | none | -| `-Dcustom-terminfo-install-location` | string | `${datadir}/terminfo` | Value to set `TERMINFO` to | None | +| Option | Type | Default | Description | Extra dependencies | +|--------------------------------------|---------|-------------------------|-------------------------------------------------------|--------------------| +| `-Ddocs` | feature | `auto` | Builds and install documentation | scdoc | +| `-Dtests` | bool | `true` | Build tests (adds a `ninja test` build target) | none | +| `-Dime` | bool | `true` | Enables IME support | None | +| `-Dgrapheme-clustering` | feature | `auto` | Enables grapheme clustering | libutf8proc | +| `-Dterminfo` | feature | `enabled` | Build and install terminfo files | tic (ncurses) | +| `-Ddefault-terminfo` | string | `foot` | Default value of `TERM` | none | +| `-Dcustom-terminfo-install-location` | string | `${datadir}/terminfo` | Value to set `TERMINFO` to | None | +| `-Dsystemd-units-dir` | string | `${systemduserunitdir}` | Where to install the systemd service files (absolute) | None | Documentation includes the man pages, readme, changelog and license files. diff --git a/meson.build b/meson.build index 7280a049..e6f4f530 100644 --- a/meson.build +++ b/meson.build @@ -252,7 +252,13 @@ if systemd.found() configuration = configuration_data() configuration.set('bindir', join_paths(get_option('prefix'), get_option('bindir'))) - systemd_units_dir = systemd.get_variable('systemduserunitdir') + custom_units_dir = get_option('systemd-units-dir') + if (custom_units_dir == '') + systemd_units_dir = systemd.get_variable('systemduserunitdir') + else + systemd_units_dir = custom_units_dir + endif + configure_file( configuration: configuration, input: 'foot-server@.service.in', diff --git a/meson_options.txt b/meson_options.txt index 255ca0ae..0c660a75 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -18,3 +18,6 @@ option('default-terminfo', type: 'string', value: 'foot', option('custom-terminfo-install-location', type: 'string', value: '', description: 'Path to foot\'s terminfo, relative to ${prefix}. If set, foot will set $TERMINFO to this value in the client process.') + +option('systemd-units-dir', type: 'string', value: '', + description: 'Where to install the systemd service files (absolute path). Default: ${systemduserunitdir}') From 15d45d5704f3cf4a6fc31fb6204cb5d874aa2bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 11 May 2022 20:58:07 +0200 Subject: [PATCH 0057/1323] meson: -Dsystemd-units-dir installs even if systemd is not found If -Dsystemd-units-dir is explicitly set, then install the systemd service files regardless of whether systemd is available or not. --- meson.build | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/meson.build b/meson.build index e6f4f530..6b50ba03 100644 --- a/meson.build +++ b/meson.build @@ -247,16 +247,16 @@ install_data( install_dir: join_paths(get_option('datadir'), 'applications')) systemd = dependency('systemd', required: false) -if systemd.found() +custom_systemd_units_dir = get_option('systemd-units-dir') +if systemd.found() or custom_systemd_units_dir != '' configuration = configuration_data() configuration.set('bindir', join_paths(get_option('prefix'), get_option('bindir'))) - custom_units_dir = get_option('systemd-units-dir') - if (custom_units_dir == '') + if (custom_systemd_units_dir == '') systemd_units_dir = systemd.get_variable('systemduserunitdir') else - systemd_units_dir = custom_units_dir + systemd_units_dir = custom_systemd_units_dir endif configure_file( From 7e8b5f961037e15f1ccb76c6058616a03b317c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 14 May 2022 09:14:57 +0200 Subject: [PATCH 0058/1323] main: minor rewording of non-UTF8 locale warnings and errors --- main.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/main.c b/main.c index 56fc1a01..4617a3c7 100644 --- a/main.c +++ b/main.c @@ -455,12 +455,12 @@ main(int argc, char *const *argv) const char *const fallback_locale = fallback_locales[i]; if (setlocale(LC_CTYPE, fallback_locale) != NULL) { - LOG_WARN("locale '%s' is not UTF-8, using '%s' instead", + LOG_WARN("'%s' is not a UTF-8 locale, using '%s' instead", locale, fallback_locale); user_notification_add_fmt( &user_notifications, USER_NOTIFICATION_WARNING, - "locale '%s' is not UTF-8, using '%s' instead", + "'%s' is not a UTF-8 locale, using '%s' instead", locale, fallback_locale); bad_locale = false; @@ -469,13 +469,13 @@ main(int argc, char *const *argv) } if (bad_locale) { - LOG_ERR("locale '%s' is not UTF-8, " - "and failed to enable a fallback locale", locale); + LOG_ERR( + "'%s' is not a UTF-8 locale, and failed to find a fallback", + locale); user_notification_add_fmt( &user_notifications, USER_NOTIFICATION_ERROR, - "locale '%s' is not UTF-8, " - "and failed to enable a fallback locale", + "'%s' is not a UTF-8 locale, and failed to find a fallback", locale); } } From bc7214cd885f08306f2fa833af2b5c72a6cc88c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 24 May 2022 18:18:15 +0200 Subject: [PATCH 0059/1323] =?UTF-8?q?config:=20use=20$HOME=20instead=20of?= =?UTF-8?q?=20getpwuid()=20to=20retrieve=20users=E2=80=99s=20home=20dir?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When searching for foot.ini, use $HOME instead of getpwuid() to retrieve the user’s home directory. --- CHANGELOG.md | 5 +++++ config.c | 13 ++----------- doc/foot.1.scd | 6 +++++- doc/foot.ini.5.scd | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b03f3f69..4924572a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,11 @@ ### Changed + +* Use `$HOME` instead of `getpwuid()` to retrieve the user’s home + directory when searching for `foot.ini`. + + ### Deprecated ### Removed ### Fixed diff --git a/config.c b/config.c index c5fad01a..cb05319a 100644 --- a/config.c +++ b/config.c @@ -311,15 +311,6 @@ struct config_file { int fd; /* FD of file, O_RDONLY */ }; -static const char * -get_user_home_dir(void) -{ - const struct passwd *passwd = getpwuid(getuid()); - if (passwd == NULL) - return NULL; - return passwd->pw_dir; -} - static struct config_file open_config(void) { @@ -328,7 +319,7 @@ open_config(void) const char *xdg_config_home = getenv("XDG_CONFIG_HOME"); const char *xdg_config_dirs = getenv("XDG_CONFIG_DIRS"); - const char *home_dir = get_user_home_dir(); + const char *home_dir = getenv("HOME"); char *xdg_config_dirs_copy = NULL; /* First, check XDG_CONFIG_HOME (or .config, if unset) */ @@ -756,7 +747,7 @@ parse_section_main(struct context *ctx) const char *include_path = NULL; if (value[0] == '~' && value[1] == '/') { - const char *home_dir = get_user_home_dir(); + const char *home_dir = getenv("HOME"); if (home_dir == NULL) { LOG_CONTEXTUAL_ERRNO("failed to expand '~'"); diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 837da048..52ccebd1 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -361,7 +361,7 @@ foot will search for a configuration file in the following locations, in this order: - *XDG_CONFIG_HOME/foot/foot.ini* (defaulting to - *~/.config/foot/foot.ini* if unset) + *$HOME/.config/foot/foot.ini* if unset) - *XDG_CONFIG_DIRS/foot/foot.ini* (defaulting to */etc/xdg/foot/foot.ini* if unset) @@ -464,6 +464,10 @@ The following environment variables are used by foot: The default child process to run, when no _command_ argument is specified and the *shell* option in *foot.ini*(5) is not set. +*HOME* + Used to determine the location of the configuration file, see + *foot.ini*(5) for details. + *XDG\_CONFIG\_HOME* Used to determine the location of the configuration file, see *foot.ini*(5) for details. diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 8aa63d2f..9c7ccf29 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -15,7 +15,7 @@ foot will search for a configuration file in the following locations, in this order: - *XDG_CONFIG_HOME/foot/foot.ini* (defaulting to - *~/.config/foot/foot.ini* if unset) + *$HOME/.config/foot/foot.ini* if unset) - *XDG_CONFIG_DIRS/foot/foot.ini* (defaulting to */etc/xdg/foot/foot.ini* if unset) From 8d03652a18dd4e3317552c004ee26f58cd749543 Mon Sep 17 00:00:00 2001 From: Stefan Prosiegel Date: Mon, 23 May 2022 14:40:12 +0200 Subject: [PATCH 0060/1323] themes: add catppuccin --- themes/catppuccin | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 themes/catppuccin diff --git a/themes/catppuccin b/themes/catppuccin new file mode 100644 index 00000000..f873aa3f --- /dev/null +++ b/themes/catppuccin @@ -0,0 +1,25 @@ +# Catppuccin + +[cursor] +color=1A1826 D9E0EE + +[colors] +alpha=1.0 +foreground=D9E0EE +background=1E1D2F +regular0=6E6C7E # black +regular1=F28FAD # red +regular2=ABE9B3 # green +regular3=FAE3B0 # yellow +regular4=96CDFB # blue +regular5=F5C2E7 # magenta +regular6=89DCEB # cyan +regular7=D9E0EE # white +bright0=988BA2 # bright black +bright1=F28FAD # bright red +bright2=ABE9B3 # bright green +bright3=FAE3B0 # bright yellow +bright4=96CDFB # bright blue +bright5=F5C2E7 # bright magenta +bright6=89DCEB # bright cyan +bright7=D9E0EE # bright white From 45b6d91eef99bd14d4b8f4499aa85f24042703c6 Mon Sep 17 00:00:00 2001 From: Lorenz Date: Tue, 31 May 2022 17:09:44 +0200 Subject: [PATCH 0061/1323] Add 'themes/tokyonight-day' --- themes/tokyonight-day | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 themes/tokyonight-day diff --git a/themes/tokyonight-day b/themes/tokyonight-day new file mode 100644 index 00000000..744ef351 --- /dev/null +++ b/themes/tokyonight-day @@ -0,0 +1,19 @@ +[colors] +background=e1e2e7 +foreground=3760bf +regular0=e9e9ed +regular1=f52a65 +regular2=587539 +regular3=8c6c3e +regular4=2e7de9 +regular5=9854f1 +regular6=007197 +regular7=6172b0 +bright0=a1a6c5 +bright1=f52a65 +bright2=587539 +bright3=8c6c3e +bright4=2e7de9 +bright5=9854f1 +bright6=007197 +bright7=3760bf \ No newline at end of file From b4443c7daad99ce85b66736a599cdd58398a5244 Mon Sep 17 00:00:00 2001 From: Lorenz Date: Tue, 31 May 2022 17:12:23 +0200 Subject: [PATCH 0062/1323] Add 'themes/tokyonight-night' --- themes/tokyonight-night | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 themes/tokyonight-night diff --git a/themes/tokyonight-night b/themes/tokyonight-night new file mode 100644 index 00000000..e48d7cd6 --- /dev/null +++ b/themes/tokyonight-night @@ -0,0 +1,19 @@ +[colors] +background=1a1b26 +foreground=c0caf5 +regular0=15161E +regular1=f7768e +regular2=9ece6a +regular3=e0af68 +regular4=7aa2f7 +regular5=bb9af7 +regular6=7dcfff +regular7=a9b1d6 +bright0=414868 +bright1=f7768e +bright2=9ece6a +bright3=e0af68 +bright4=7aa2f7 +bright5=bb9af7 +bright6=7dcfff +bright7=c0caf5 From e521fe5394c90e48a17ecbe0b8a5c620b3bd300f Mon Sep 17 00:00:00 2001 From: Lorenz Date: Tue, 31 May 2022 17:14:34 +0200 Subject: [PATCH 0063/1323] Add 'themes/tokyonight-storm' --- themes/tokyonight-storm | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 themes/tokyonight-storm diff --git a/themes/tokyonight-storm b/themes/tokyonight-storm new file mode 100644 index 00000000..96b90eb8 --- /dev/null +++ b/themes/tokyonight-storm @@ -0,0 +1,19 @@ +[colors] +background=24283b +foreground=c0caf5 +regular0=1D202F +regular1=f7768e +regular2=9ece6a +regular3=e0af68 +regular4=7aa2f7 +regular5=bb9af7 +regular6=7dcfff +regular7=a9b1d6 +bright0=414868 +bright1=f7768e +bright2=9ece6a +bright3=e0af68 +bright4=7aa2f7 +bright5=bb9af7 +bright6=7dcfff +bright7=c0caf5 From cdd46cdf85b1087ac7465c646bb05078f1bbe85b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 1 Jun 2022 19:28:47 +0200 Subject: [PATCH 0064/1323] =?UTF-8?q?grid:=20invert=20the=20default=20valu?= =?UTF-8?q?e=20of=20=E2=80=98linebreak=E2=80=99,=20from=20false=20to=20tru?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- grid.c | 14 +++++++++++--- terminal.c | 4 +--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/grid.c b/grid.c index 4a3995a0..736a9f64 100644 --- a/grid.c +++ b/grid.c @@ -284,7 +284,7 @@ grid_row_alloc(int cols, bool initialize) { struct row *row = xmalloc(sizeof(*row)); row->dirty = false; - row->linebreak = false; + row->linebreak = true; row->extra = NULL; if (initialize) { @@ -502,7 +502,7 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, } else { /* Scrollback is full, need to re-use a row */ grid_row_reset_extra(new_row); - new_row->linebreak = false; + new_row->linebreak = true; tll_foreach(old_grid->sixel_images, it) { if (it->item.pos.row == *row_idx) { @@ -833,6 +833,14 @@ grid_resize_and_reflow( &new_row->cells[new_col_idx], &old_row->cells[from], amount * sizeof(struct cell)); + /* + * We’ve “printed” to this line - reset linebreak. + * + * If the old line ends with a hard linebreak, we’ll + * set linebreak=true on the last new row we print to. + */ + new_row->linebreak = false; + count -= amount; from += amount; new_col_idx += amount; @@ -891,7 +899,7 @@ grid_resize_and_reflow( } - if (old_row->linebreak) { + if (old_row->linebreak && col_count > 0) { /* Erase the remaining cells */ memset(&new_row->cells[new_col_idx], 0, (new_cols - new_col_idx) * sizeof(new_row->cells[0])); diff --git a/terminal.c b/terminal.c index 09c9caea..d8bf7516 100644 --- a/terminal.c +++ b/terminal.c @@ -1811,7 +1811,7 @@ static inline void erase_line(struct terminal *term, struct row *row) { erase_cell_range(term, row, 0, term->cols - 1); - row->linebreak = false; + row->linebreak = true; } void @@ -3297,7 +3297,6 @@ term_print(struct terminal *term, char32_t wc, int width) /* *Must* get current cell *after* linewrap+insert */ struct row *row = grid->cur_row; row->dirty = true; - row->linebreak = true; struct cell *cell = &row->cells[col]; cell->wc = term->vt.last_printed = wc; @@ -3357,7 +3356,6 @@ ascii_printer_fast(struct terminal *term, char32_t wc) struct row *row = grid->cur_row; row->dirty = true; - row->linebreak = true; struct cell *cell = &row->cells[col]; cell->wc = term->vt.last_printed = wc; From 9567694bab7149afbb4e4fcc4c754272a8d25aba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 1 Jun 2022 19:29:30 +0200 Subject: [PATCH 0065/1323] =?UTF-8?q?grid:=20reflow:=20don=E2=80=99t=20tri?= =?UTF-8?q?m=20trailing=20empty=20cells=20from=20logical=20lines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a line in the old grid (the one being reflowed) doesn’t have a hard linebreak, don’t trim trailing empty cells. Doing so means we’ll “compress” (remove) empty cells between text if/when we revert to a larger window size. The output from neofetch suffers from this; it prints a logo to the left, and system information to the right. The logo and the system info column is separated by empty cells (i.e. *not* spaces). If the window is reduced in size such that the system info is pushed to a new line, each logo line ends with a number of empty cells. The next time the window is resized, these empty cells were ignored (i.e. removed). That meant that once the window was enlarged again, the system info column was a) no longer aligned, and b) had been pulled closer to the logo. This patch doesn’t special case trailing empty cells when the line being reflowed doesn’t have a hard linebreak. This means e.g. ‘ls’ output is unaffected. Closes #1055 --- CHANGELOG.md | 3 +++ grid.c | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4924572a..44f0ad20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,9 @@ * Graphical corruption when viewport is at the top of the scrollback, and the output is scrolling. +* Improved text reflow of logical lines with trailing empty cells ([#1055][1055]) + +[1055]: https://codeberg.org/dnkl/foot/issues/1055 ### Security diff --git a/grid.c b/grid.c index 736a9f64..bc1018d9 100644 --- a/grid.c +++ b/grid.c @@ -703,6 +703,11 @@ grid_resize_and_reflow( } } + if (!old_row->linebreak /*&& col_count > 0*/) { + /* Don’t truncate logical lines */ + col_count = old_cols; + } + xassert(col_count >= 0 && col_count <= old_cols); /* Do we have a (at least one) tracking point on this row */ From 497c31d9fc0babf02d2bfdfa2f2633ae80cb24f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 29 May 2022 11:11:52 +0200 Subject: [PATCH 0066/1323] commands: scroll up: simplify viewport clamping logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When scrolling the viewport up, we need to ensure we don’t go past the scrollback wrap around. This was previously done using “custom” logic that tried to calculate many rows away from the scrollback start the current viewport is. But this is *exactly* what grid_row_abs_to_sb() does, except it doesn’t account for uninitialized scrollback. So, copy the logic from grid_row_abs_to_sb(), but ensure scrollback start points to valid scrollback data. The maximum number of rows we’re allowed to scroll is now the same as the current viewport’s sb-relative coordinate. Maybe closes #1074 --- commands.c | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/commands.c b/commands.c index 9d00067d..85e200e2 100644 --- a/commands.c +++ b/commands.c @@ -32,20 +32,12 @@ cmd_scrollback_up(struct terminal *term, int rows) scrollback_start &= grid_rows - 1; } - /* Number of rows to scroll, without going past the scrollback start */ - int max_rows = 0; - if (view + screen_rows >= grid_rows) { - /* View crosses scrollback wrap-around */ - xassert(scrollback_start <= view); - max_rows = view - scrollback_start; - } else { - if (scrollback_start <= view) - max_rows = view - scrollback_start; - else - max_rows = view + (grid_rows - scrollback_start); - } + /* The view row number in scrollback relative coordinates. This is + * the maximum number of rows we’re allowed to scroll */ + int view_sb_rel = view - scrollback_start + grid_rows; + view_sb_rel &= grid_rows - 1; - rows = min(rows, max_rows); + rows = min(rows, view_sb_rel); if (rows == 0) return; From 755f96321ab47f931d11680027d47d1d772f5584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 28 May 2022 19:27:29 +0200 Subject: [PATCH 0067/1323] =?UTF-8?q?config:=20add=20a=20new=20=E2=80=98en?= =?UTF-8?q?vironment=E2=80=99=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This section allows the user to define custom environment variables to be set in the child process: [environment] name=value --- config.c | 40 ++++++++++++++++++++++++++++++++++++++++ config.h | 8 ++++++++ foot.ini | 3 +++ slave.c | 10 ++++++++-- slave.h | 4 +++- terminal.c | 2 +- 6 files changed, 63 insertions(+), 4 deletions(-) diff --git a/config.c b/config.c index cb05319a..d2be087b 100644 --- a/config.c +++ b/config.c @@ -2200,6 +2200,29 @@ err: return false; } +static bool +parse_section_environment(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + const char *value = ctx->value; + + tll_foreach(conf->env_vars, it) { + if (strcmp(it->item.name, key) == 0) { + free(it->item.value); + it->item.value = xstrdup(value); + return true; + } + } + + struct env_var var = { + .name = xstrdup(key), + .value = xstrdup(value), + }; + tll_push_back(conf->env_vars, var); + return true; +} + static bool parse_section_tweak(struct context *ctx) { @@ -2403,6 +2426,7 @@ enum section { SECTION_URL_BINDINGS, SECTION_MOUSE_BINDINGS, SECTION_TEXT_BINDINGS, + SECTION_ENVIRONMENT, SECTION_TWEAK, SECTION_COUNT, }; @@ -2427,6 +2451,7 @@ static const struct { [SECTION_URL_BINDINGS] = {&parse_section_url_bindings, "url-bindings"}, [SECTION_MOUSE_BINDINGS] = {&parse_section_mouse_bindings, "mouse-bindings"}, [SECTION_TEXT_BINDINGS] = {&parse_section_text_bindings, "text-bindings"}, + [SECTION_ENVIRONMENT] = {&parse_section_environment, "environment"}, [SECTION_TWEAK] = {&parse_section_tweak, "tweak"}, }; @@ -2867,6 +2892,7 @@ config_load(struct config *conf, const char *conf_path, .sixel = true, }, + .env_vars = tll_init(), .notifications = tll_init(), }; @@ -3147,6 +3173,14 @@ config_clone(const struct config *old) key_binding_list_clone(&conf->bindings.url, &old->bindings.url); key_binding_list_clone(&conf->bindings.mouse, &old->bindings.mouse); + tll_foreach(old->env_vars, it) { + struct env_var copy = { + .name = xstrdup(it->item.name), + .value = xstrdup(it->item.value), + }; + tll_push_back(conf->env_vars, copy); + } + conf->notifications.length = 0; conf->notifications.head = conf->notifications.tail = 0; tll_foreach(old->notifications, it) { @@ -3207,6 +3241,12 @@ config_free(struct config *conf) free_key_binding_list(&conf->bindings.url); free_key_binding_list(&conf->bindings.mouse); + tll_foreach(conf->env_vars, it) { + free(it->item.name); + free(it->item.value); + tll_remove(conf->env_vars, it); + } + user_notifications_free(&conf->notifications); } diff --git a/config.h b/config.h index 2061415e..de5d8a7b 100644 --- a/config.h +++ b/config.h @@ -104,6 +104,12 @@ struct config_spawn_template { struct argv argv; }; +struct env_var { + char *name; + char *value; +}; +typedef tll(struct env_var) env_var_list_t; + struct config { char *term; char *shell; @@ -296,6 +302,8 @@ struct config { struct config_spawn_template notify; bool notify_focus_inhibit; + env_var_list_t env_vars; + struct { enum fcft_scaling_filter fcft_filter; bool overflowing_glyphs; diff --git a/foot.ini b/foot.ini index 21c49174..f4dc4280 100644 --- a/foot.ini +++ b/foot.ini @@ -33,6 +33,9 @@ # selection-target=primary # workers= +[environment] +# name=value + [bell] # urgent=no # notify=no diff --git a/slave.c b/slave.c index db60d7b5..6473ac7c 100644 --- a/slave.c +++ b/slave.c @@ -305,8 +305,9 @@ err: pid_t slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, - char *const *envp, const char *term_env, const char *conf_shell, - bool login_shell, const user_notifications_t *notifications) + char *const *envp, const env_var_list_t *extra_env_vars, + const char *term_env, const char *conf_shell, bool login_shell, + const user_notifications_t *notifications) { int fork_pipe[2]; if (pipe2(fork_pipe, O_CLOEXEC) < 0) { @@ -356,6 +357,11 @@ slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, setenv("TERMINFO", FOOT_TERMINFO_PATH, 1); #endif + if (extra_env_vars != NULL) { + tll_foreach(*extra_env_vars, it) + setenv(it->item.name, it->item.value, 1); + } + char **_shell_argv = NULL; char **shell_argv = NULL; diff --git a/slave.h b/slave.h index 4b47bfde..b1c08f14 100644 --- a/slave.h +++ b/slave.h @@ -3,9 +3,11 @@ #include +#include "config.h" #include "user-notification.h" pid_t slave_spawn( int ptmx, int argc, const char *cwd, char *const *argv, char *const *envp, - const char *term_env, const char *conf_shell, bool login_shell, + const env_var_list_t *extra_env_vars, const char *term_env, + const char *conf_shell, bool login_shell, const user_notifications_t *notifications); diff --git a/terminal.c b/terminal.c index d8bf7516..4b1e5931 100644 --- a/terminal.c +++ b/terminal.c @@ -1248,7 +1248,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, /* Start the slave/client */ if ((term->slave = slave_spawn( - term->ptmx, argc, term->cwd, argv, envp, + term->ptmx, argc, term->cwd, argv, envp, &conf->env_vars, conf->term, conf->shell, conf->login_shell, &conf->notifications)) == -1) { From 5760bcb3bf168f7c4a695c667ef0316751452803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 28 May 2022 19:28:36 +0200 Subject: [PATCH 0068/1323] =?UTF-8?q?doc:=20document=20the=20new=20?= =?UTF-8?q?=E2=80=98environment=E2=80=99=20config=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/foot.1.scd | 7 +++++-- doc/foot.ini.5.scd | 13 +++++++++++++ doc/footclient.1.scd | 7 +++++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 52ccebd1..6f0ad6db 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -458,7 +458,7 @@ In all other cases, the exit code is that of the client application # ENVIRONMENT -The following environment variables are used by foot: +## Variables used by foot *SHELL* The default child process to run, when no _command_ argument is @@ -492,7 +492,7 @@ The following environment variables are used by foot: The size to use for *Xcursor*(3) pointers (typically set by the Wayland compositor). -The following environment variables are set in the child process: +## Variables set in the child process *TERM* terminfo/termcap identifier. This is used by client applications @@ -504,6 +504,9 @@ The following environment variables are set in the child process: This variable is set to *truecolor*, to indicate to client applications that 24-bit RGB colors are supported. +In addition to the variables listed above, custom environment +variables may be defined in *foot.ini*(5). + # BUGS Please report bugs to https://codeberg.org/dnkl/foot/issues diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 9c7ccf29..bc1b6bdd 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -305,6 +305,19 @@ commented out will usually be installed to */etc/xdg/foot/foot.ini*. (including SMT). Note that this is not always the best value. In some cases, the number of physical _cores_ is better. +# SECTION: environment + +This section is used to define environment variables that will be set +in the client application, in addition to the variables inherited from +the terminal process itself. + +The format is simply: + +*name*=_value_ + +Note: do not set *TERM* here; use the *term* option in the main +(default) section instead. + # SECTION: bell *urgent* diff --git a/doc/footclient.1.scd b/doc/footclient.1.scd index 285ff184..967a4f1b 100644 --- a/doc/footclient.1.scd +++ b/doc/footclient.1.scd @@ -136,7 +136,7 @@ terminfo entries manually, by copying *foot* and *foot-direct* to # ENVIRONMENT -The following environment variables are used by footclient: +## Variables used by footclient *XDG\_RUNTIME\_DIR* Used to construct the default _PATH_ for the *--server-socket* @@ -146,7 +146,7 @@ The following environment variables are used by footclient: Used to construct the default _PATH_ for the *--server-socket* option, when no explicit argument is given (see above). -The following environment variables are set in the child process: +## Variables set in the child process *TERM* terminfo/termcap identifier. This is used by client applications @@ -158,6 +158,9 @@ The following environment variables are set in the child process: This variable is set to *truecolor*, to indicate to client applications that 24-bit RGB colors are supported. +In addition to the variables listed above, custom environment +variables may be defined in *foot.ini*(5). + # SEE ALSO *foot*(1) From 604a3cdd8428afe75318675781cb37aab76c8744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 28 May 2022 19:28:45 +0200 Subject: [PATCH 0069/1323] =?UTF-8?q?changelog:=20new=20=E2=80=98environme?= =?UTF-8?q?nt=E2=80=99=20section=20in=20foot.ini?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44f0ad20..537a70d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,8 +45,12 @@ * XDG activation support when opening URLs ([#1058][1058]). * `-Dsystemd-units-dir=` meson command line option. +* Support for custom environment variables in `foot.ini` + ([#1070][1070]). + [1058]: https://codeberg.org/dnkl/foot/issues/1058 +[1070]: https://codeberg.org/dnkl/foot/issues/1070 ### Changed From 8436e6acea7e45a58bef48772f00dd0b4bbd285f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 28 May 2022 19:34:16 +0200 Subject: [PATCH 0070/1323] =?UTF-8?q?test:=20config:=20new=20test=20for=20?= =?UTF-8?q?the=20new=20=E2=80=98environment=E2=80=99=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test-config.c | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/test-config.c b/tests/test-config.c index 313b3672..874f5b05 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -1132,6 +1132,42 @@ test_section_text_bindings(void) config_free(&conf); } +static void +test_section_environment(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "environment", .path = "unittest"}; + + /* A single variable */ + ctx.key = "FOO"; + ctx.value = "bar"; + xassert(parse_section_environment(&ctx)); + xassert(tll_length(conf.env_vars) == 1); + xassert(strcmp(tll_front(conf.env_vars).name, "FOO") == 0); + xassert(strcmp(tll_front(conf.env_vars).value, "bar") == 0); + + /* 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); + + /* 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); + + config_free(&conf); +} + static void test_section_tweak(void) { @@ -1227,6 +1263,7 @@ main(int argc, const char *const *argv) test_section_mouse_bindings(); test_section_mouse_bindings_collisions(); test_section_text_bindings(); + test_section_environment(); test_section_tweak(); log_deinit(); return 0; From d3c51b51b7d4d8f32962ab634eec85316c1c3621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 13 Jun 2022 16:32:59 +0200 Subject: [PATCH 0071/1323] pgo: slave_spawn(): sync function signature --- pgo/pgo.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pgo/pgo.c b/pgo/pgo.c index fec2d26c..3073c27c 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -107,7 +107,8 @@ spawn(struct reaper *reaper, const char *cwd, char *const argv[], pid_t slave_spawn( int ptmx, int argc, const char *cwd, char *const *argv, char *const *envp, - const char *term_env, const char *conf_shell, bool login_shell, + const env_var_list_t *extra_env_vars, const char *term_env, + const char *conf_shell, bool login_shell, const user_notifications_t *notifications) { return 0; From dbe2c0a068fb0eda9b225f51b1df785d6f076de5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 13 Jun 2022 11:41:54 +0200 Subject: [PATCH 0072/1323] selection: allow HT, VT and FF, disallow NUL in non-bracketed paste mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This syncs foot with more recent versions of XTerm, where it’s “disallowedPasteControls” resource has changed its default value to BS,DEL,ENQ,EOT,ESC,NULL Note that we’re already stripping out ENQ,EOT,ESC in all modes. What does it mean for foot: * HT, VT and FF are now allowed, regardless of paste mode * NUL is now stripped in non-bracketed paste mode Closes #1084 --- CHANGELOG.md | 6 ++++++ selection.c | 19 ++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 537a70d2..5ff030d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,12 @@ * Use `$HOME` instead of `getpwuid()` to retrieve the user’s home directory when searching for `foot.ini`. +* HT, VT and FF are no longer stripped when pasting in non-bracketed + mode ([#1084][1084]). +* NUL is now stripped when pasting in non-bracketed mode + ([#1084][1084]). + +[1084]: https://codeberg.org/dnkl/foot/issues/1084 ### Deprecated diff --git a/selection.c b/selection.c index 6b57f48c..37dd2c8d 100644 --- a/selection.c +++ b/selection.c @@ -1845,9 +1845,22 @@ fdm_receive(struct fdm *fdm, int fd, int events, void *data) skip_one(); goto again; - /* Additional control characters stripped by default (but - * configurable) in XTerm: BS, HT, DEL */ - case '\b': case '\t': case '\v': case '\f': case '\x7f': + /* + * In addition to stripping non-formatting C0 controls, + * XTerm has an option, “disallowedPasteControls”, that + * defines C0 controls that will be replaced with spaces + * when pasted. + * + * It’s default value is BS,DEL,ENQ,EOT,NUL + * + * Instead of replacing them with spaces, we allow them in + * bracketed paste mode, and strip them completely in + * non-bracketed mode. + * + * Note some of the (default) XTerm controls are already + * handled above. + */ + case '\b': case '\x7f': case '\x00': if (!ctx->bracketed) { skip_one(); goto again; From edd68732ad472a7a9ae824fe3be06f8f111754d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 13 Jun 2022 12:14:15 +0200 Subject: [PATCH 0073/1323] vt: prevent potential endless loop when finding a slot for a composed character MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Composed characters are stored in a tree structure, using a key as identifier. The key is calculated from the individual characters that make up the composed character sequence. Since the address space for keys is limited, collisions may occur. In this case, we simply increment the key and try again. It is theoretically possible to saturate the key space, in which case we’ll get stuck in an endless loop. Even if the key space isn’t fully saturated, we fairly easy reach a point where there are so many collisions for each insertion, that performance drops significantly. Since key space is limited (it’s not like a hash table that we can grow), our only option is to limit the number of collisions. If we can’t find a slot within a hard code amount of collisions, the character is simply dropped. --- vt.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/vt.c b/vt.c index a98188be..9746364b 100644 --- a/vt.c +++ b/vt.c @@ -738,8 +738,20 @@ action_utf8_print(struct terminal *term, char32_t wc) xassert(wanted_count <= 255); + size_t collision_count = 0; + /* Look for existing combining chain */ while (true) { + if (collision_count > 128) { + static bool have_warned = false; + if (!have_warned) { + have_warned = true; + LOG_WARN("ignoring composed character: " + "too many collisions in hash table"); + } + return; + } + const struct composed *cc = composed_lookup(term->composed, key); if (cc == NULL) break; @@ -756,6 +768,7 @@ action_utf8_print(struct terminal *term, char32_t wc) cc->chars[wanted_count - 1] != wc) { key++; + collision_count++; continue; } @@ -766,6 +779,7 @@ action_utf8_print(struct terminal *term, char32_t wc) if (!match) { key++; + collision_count++; continue; } From fbcb30bf9870aa2b4d75376b3a02703576a715e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 13 Jun 2022 13:11:56 +0200 Subject: [PATCH 0074/1323] vt: improve key calculation for compose sequences MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Don’t assume 32 bits when rotating the old key. Use the number of actual bits available, as determined by CELL_COMB_CHARS_{HI,LO} * Multiply with magic hash constant This greatly reduces the number of collisions seen. For example, the Emoji test file (from the Unicode specification), now has zero collisions. --- vt.c | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/vt.c b/vt.c index 9746364b..54fd643c 100644 --- a/vt.c +++ b/vt.c @@ -622,8 +622,21 @@ action_put(struct terminal *term, uint8_t c) static inline uint32_t chain_key(uint32_t old_key, uint32_t new_wc) { - /* Rotate left 8 bits, xor with new char */ - return ((old_key << 8) | (old_key >> (32 - 8))) ^ new_wc; + unsigned bits = 32 - __builtin_clz(CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO); + + /* Rotate old key 8 bits */ + uint32_t new_key = (old_key << 8) | (old_key >> (bits - 8)); + + /* xor with new char */ + new_key ^= new_wc; + + /* Multiply with magic hash constant */ + new_key *= 2654435761; + + /* And mask, to ensure the new value is within range */ + new_key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO; + + return new_key; } static void @@ -668,8 +681,6 @@ action_utf8_print(struct terminal *term, char32_t wc) } else key = chain_key(base, wc); - key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO; - #if defined(FOOT_GRAPHEME_CLUSTERING) if (grapheme_clustering) { /* Check if we're on a grapheme cluster break */ @@ -767,6 +778,10 @@ action_utf8_print(struct terminal *term, char32_t wc) cc->count != wanted_count || cc->chars[wanted_count - 1] != wc) { +#if 0 + LOG_WARN("COLLISION: base: %04x/%04x, count: %d/%zu, last: %04x/%04x", + cc->chars[0], base, cc->count, wanted_count, cc->chars[wanted_count - 1], wc); +#endif key++; collision_count++; continue; From 9dc4f48e7a00a2c59f955a4a6cab57ce68a01e4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 13 Jun 2022 13:16:58 +0200 Subject: [PATCH 0075/1323] =?UTF-8?q?vt:=20tag=20collision-count=20check?= =?UTF-8?q?=20with=20=E2=80=98unlikely=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vt.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vt.c b/vt.c index 54fd643c..91f00e6f 100644 --- a/vt.c +++ b/vt.c @@ -753,7 +753,7 @@ action_utf8_print(struct terminal *term, char32_t wc) /* Look for existing combining chain */ while (true) { - if (collision_count > 128) { + if (unlikely(collision_count > 128)) { static bool have_warned = false; if (!have_warned) { have_warned = true; From d852178540bbe98c0b88ecc627b0fcfa208d4b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jun 2022 18:39:46 +0200 Subject: [PATCH 0076/1323] ime: ime_reset_pending_{preedit,commit} is not used outside ime.c --- ime.c | 68 +++++++++++++++++++++++++++++------------------------------ ime.h | 2 -- 2 files changed, 34 insertions(+), 36 deletions(-) diff --git a/ime.c b/ime.c index e7e5937a..a3ca19b2 100644 --- a/ime.c +++ b/ime.c @@ -17,6 +17,40 @@ #include "wayland.h" #include "xmalloc.h" +static void +ime_reset_pending_preedit(struct seat *seat) +{ + free(seat->ime.preedit.pending.text); + seat->ime.preedit.pending.text = NULL; +} + +static void +ime_reset_pending_commit(struct seat *seat) +{ + free(seat->ime.commit.pending.text); + seat->ime.commit.pending.text = NULL; +} + +void +ime_reset_pending(struct seat *seat) +{ + ime_reset_pending_preedit(seat); + ime_reset_pending_commit(seat); +} + +void +ime_reset_preedit(struct seat *seat) +{ + if (seat->ime.preedit.cells == NULL) + return; + + free(seat->ime.preedit.text); + free(seat->ime.preedit.cells); + seat->ime.preedit.text = NULL; + seat->ime.preedit.cells = NULL; + seat->ime.preedit.count = 0; +} + static void enter(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, struct wl_surface *surface) @@ -324,40 +358,6 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, } } -void -ime_reset_pending_preedit(struct seat *seat) -{ - free(seat->ime.preedit.pending.text); - seat->ime.preedit.pending.text = NULL; -} - -void -ime_reset_pending_commit(struct seat *seat) -{ - free(seat->ime.commit.pending.text); - seat->ime.commit.pending.text = NULL; -} - -void -ime_reset_pending(struct seat *seat) -{ - ime_reset_pending_preedit(seat); - ime_reset_pending_commit(seat); -} - -void -ime_reset_preedit(struct seat *seat) -{ - if (seat->ime.preedit.cells == NULL) - return; - - free(seat->ime.preedit.text); - free(seat->ime.preedit.cells); - seat->ime.preedit.text = NULL; - seat->ime.preedit.cells = NULL; - seat->ime.preedit.count = 0; -} - static void ime_send_cursor_rect(struct seat *seat) { diff --git a/ime.h b/ime.h index 2aa4efe4..3127f4d7 100644 --- a/ime.h +++ b/ime.h @@ -15,7 +15,5 @@ void ime_enable(struct seat *seat); void ime_disable(struct seat *seat); void ime_update_cursor_rect(struct seat *seat); -void ime_reset_pending_preedit(struct seat *seat); -void ime_reset_pending_commit(struct seat *seat); void ime_reset_pending(struct seat *seat); void ime_reset_preedit(struct seat *seat); From 96f23b4c6436d314ae4272f5f99c51b8e33556eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jun 2022 18:41:08 +0200 Subject: [PATCH 0077/1323] ime: track IME focus independently from keyboard focus Replace the seat->ime.focused boolean with a terminal instace pointer, seat->ime_focus. Set and reset this on ime::enter() and ime::leave() events, and use this instead of seat->kbd_focus on all other IME events. This fixes two issues: a) buggy compositors that sometimes sends an IME enter event without first having sent a keyboard enter event. b) seats may be IME capable while still lacking the keyboard capability. Such seats will *always* see IME enter events without a corresponding keyboard enter event. --- CHANGELOG.md | 1 + ime.c | 28 +++++++++++++++++----------- render.c | 4 ++-- wayland.h | 2 +- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ff030d5..7737f108 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,7 @@ * Graphical corruption when viewport is at the top of the scrollback, and the output is scrolling. * Improved text reflow of logical lines with trailing empty cells ([#1055][1055]) +* IME focus is now tracked independently from keyboard focus. [1055]: https://codeberg.org/dnkl/foot/issues/1055 diff --git a/ime.c b/ime.c index a3ca19b2..f3a3ec18 100644 --- a/ime.c +++ b/ime.c @@ -56,12 +56,18 @@ enter(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, struct wl_surface *surface) { struct seat *seat = data; + struct wl_window *win = wl_surface_get_user_data(surface); + struct terminal *term = win->term; - LOG_DBG("enter: seat=%s", seat->name); + LOG_DBG("enter: seat=%s, term=%p", seat->name, (const void *)term); + + if (seat->kbd_focus != term) { + LOG_WARN("compositor sent ime::enter() event before the " + "corresponding keyboard_enter() event"); + } /* The main grid is the *only* input-receiving surface we have */ - xassert(seat->kbd_focus != NULL); - seat->ime.focused = true; + seat->ime_focus = term; ime_enable(seat); } @@ -73,7 +79,7 @@ leave(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, LOG_DBG("leave: seat=%s", seat->name); ime_disable(seat); - seat->ime.focused = false; + seat->ime_focus = NULL; } static void @@ -138,7 +144,7 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, LOG_DBG("done: serial=%u", serial); struct seat *seat = data; - struct terminal *term = seat->kbd_focus; + struct terminal *term = seat->ime_focus; if (seat->ime.serial != serial) { LOG_DBG("IME serial mismatch: expected=0x%08x, got 0x%08x", @@ -364,10 +370,10 @@ ime_send_cursor_rect(struct seat *seat) if (unlikely(seat->wayl->text_input_manager == NULL)) return; - if (!seat->ime.focused) + if (seat->ime_focus == NULL) return; - struct terminal *term = seat->kbd_focus; + struct terminal *term = seat->ime_focus; if (!term->ime_enabled) return; @@ -399,10 +405,10 @@ ime_enable(struct seat *seat) if (unlikely(seat->wayl->text_input_manager == NULL)) return; - if (!seat->ime.focused) + if (seat->ime_focus == NULL) return; - struct terminal *term = seat->kbd_focus; + struct terminal *term = seat->ime_focus; if (term == NULL) return; @@ -437,7 +443,7 @@ ime_disable(struct seat *seat) if (unlikely(seat->wayl->text_input_manager == NULL)) return; - if (!seat->ime.focused) + if (seat->ime_focus == NULL) return; ime_reset_pending(seat); @@ -451,7 +457,7 @@ ime_disable(struct seat *seat) void ime_update_cursor_rect(struct seat *seat) { - struct terminal *term = seat->kbd_focus; + struct terminal *term = seat->ime_focus; /* Set in render_ime_preedit() */ if (seat->ime.preedit.cells != NULL) diff --git a/render.c b/render.c index 27f21bd5..3b281a89 100644 --- a/render.c +++ b/render.c @@ -3576,7 +3576,7 @@ frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_da grid_render(term); tll_foreach(term->wl->seats, it) { - if (it->item.kbd_focus == term) + if (it->item.ime_focus == term) ime_update_cursor_rect(&it->item); } @@ -4058,7 +4058,7 @@ fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data) grid_render(term); tll_foreach(term->wl->seats, it) { - if (it->item.kbd_focus == term) + if (it->item.ime_focus == term) ime_update_cursor_rect(&it->item); } diff --git a/wayland.h b/wayland.h index daa3e65d..729a225b 100644 --- a/wayland.h +++ b/wayland.h @@ -81,6 +81,7 @@ struct seat { /* Focused terminals */ struct terminal *kbd_focus; struct terminal *mouse_focus; + struct terminal *ime_focus; /* Keyboard state */ struct wl_keyboard *wl_keyboard; @@ -202,7 +203,6 @@ struct seat { } pending; } surrounding; - bool focused; uint32_t serial; } ime; #endif From bdb79e8b9fb36f627bb204ce2060dd5a877f3712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 15 Jun 2022 18:44:23 +0200 Subject: [PATCH 0078/1323] osc: add support for OSC 133;A (prompt markers) This patch adds support for the OSC-133;A sequence, introduced by FinalTerm and implemented by iTerm2, Kitty and more. See https://iterm2.com/documentation-one-page.html#documentation-escape-codes.html. The shell emits the OSC just before printing the prompt. This lets the terminal know where, in the scrollback, there are prompts. We implement this using a simple boolean in the row struct ("this row has a prompt"). The prompt marker must be reflowed along with the text on window resizes. In an ideal world, erasing, or overwriting the cell where the OSC was emitted, would remove the prompt mark. Since we don't store this information in the cell struct, we can't do that. The best we can do is reset it in erase_line(). This works well enough in the "normal" screen, when used with a "normal" shell. It doesn't really work in fullscreen apps, on the alt screen. But that doesn't matter since we don't support jumping between prompts on the alt screen anyway. To be able to jump between prompts, two new key bindings have been added: prompt-prev and prompt-next, bound to ctrl+shift+z and ctrl+shift+x respectively. prompt-prev will jump to the previous, not currently visible, prompt, by moving the viewport, ensuring the prompt is at the top of the screen. prompt-next jumps to the next prompt, visible or not. Again, by moving the viewport to ensure the prompt is at the top of the screen. If we're at the bottom of the scrollback, the viewport is instead moved as far down as possible. Closes #30 --- CHANGELOG.md | 5 ++- README.md | 54 ++++++++++++++++++++++++++- commands.c | 15 ++------ config.c | 4 ++ doc/foot-ctlseqs.7.scd | 3 ++ doc/foot.1.scd | 39 ++++++++++++++++++++ doc/foot.ini.5.scd | 8 ++++ foot.ini | 2 + grid.c | 40 ++++++++++++++++++++ grid.h | 7 +++- input.c | 83 +++++++++++++++++++++++++++++++++++++++++- key-binding.h | 2 + osc.c | 34 +++++++++++++++++ terminal.c | 1 + terminal.h | 6 ++- 15 files changed, 285 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7737f108..7a84bc13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,10 +47,13 @@ * `-Dsystemd-units-dir=` meson command line option. * Support for custom environment variables in `foot.ini` ([#1070][1070]). - +* Support for jumping to previous/next prompt (requires shell + integration). By default bound to `ctrl`+`shift`+`z` and + `ctrl`+`shift`+`x` respectively ([#30][30]). [1058]: https://codeberg.org/dnkl/foot/issues/1058 [1070]: https://codeberg.org/dnkl/foot/issues/1070 +[30]: https://codeberg.org/dnkl/foot/issues/30 ### Changed diff --git a/README.md b/README.md index 227c528c..348f2e18 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,9 @@ The fast, lightweight and minimalistic Wayland terminal emulator. 1. [Mouse](#mouse) 1. [Server (daemon) mode](#server-daemon-mode) 1. [URLs](#urls) +1. [Shell integration](#shell-integration) + 1. [Current working directory](#current-working-directory) + 1. [Jumping between prompts](#jumping-between-prompts) 1. [Alt/meta](#alt-meta) 1. [Backspace](#backspace) 1. [Keypad](#keypad) @@ -157,13 +160,21 @@ These are the default shortcuts. See `man foot.ini` and the example ctrl+shift+n : Spawn a new terminal. If the shell has been [configured to emit the OSC 7 escape - sequence](https://codeberg.org/dnkl/foot/wiki#user-content-how-to-configure-my-shell-to-emit-the-osc-7-escape-sequence), + sequence](https://codeberg.org/dnkl/foot/wiki#user-content-spawning-new-terminal-instances-in-the-current-working-directory), the new terminal will start in the current working directory. ctrl+shift+u : Enter URL mode, where all currently visible URLs are tagged with a jump label with a key sequence that will open the URL. +ctrl+shift+z +: Jump to the previous, currently not visible, prompt. Requires [shell + integration](https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts). + +ctrl+shift+x +: Jump to the next prompt. Requires [shell + integration](https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts). + #### Scrollback search @@ -296,6 +307,44 @@ Jump label colors, the URL underline color, and the letters used in the jump label key sequences can be configured. +## Shell integration + +### Current working directory + +New foot terminal instances (bound to +ctrl+shift+n by default) will open in +the current working directory, **if** the shell in the “parent” +terminal reports directory changes. + +This is done with the OSC-7 escape sequence. Most shells can be +scripted to do this, if they do not support it natively. See the +[wiki](https://codeberg.org/dnkl/foot/wiki#user-content-spawning-new-terminal-instances-in-the-current-working-directory) +for details. + + +### Jumping between prompts + +Foot can move the current viewport to focus prompts of already +executed commands (bound to +ctrl+shift+z/x by +default). + +For this to work, the shell needs to emit an OSC-133;A +(`\E]133;A\E\\`) sequence before each prompt. + +In zsh, one way to do this is to add a `precmd` hook: + +```zsh +precmd() { + print -Pn "\e]133;A\e\\" +} +``` + +See the +[wiki](https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts) +for details, and examples for other shells. + + ## Alt/meta By default, foot prefixes _Meta characters_ with ESC. This corresponds @@ -392,7 +441,7 @@ with the terminal emulator itself. Foot implements the following OSCs: supported) * `OSC 2` - change window title * `OSC 4` - change color palette -* `OSC 7` - report CWD +* `OSC 7` - report CWD (see [shell integration](#shell-integration)) * `OSC 8` - hyperlink * `OSC 9` - desktop notification * `OSC 10` - change (default) foreground color @@ -408,6 +457,7 @@ with the terminal emulator itself. Foot implements the following OSCs: * `OSC 112` - reset cursor color * `OSC 117` - reset highlight background color * `OSC 119` - reset highlight foreground color +* `OSC 133` - [shell integration](#shell-integration) * `OSC 555` - flash screen (**foot specific**) * `OSC 777` - desktop notification (only the `;notify` sub-command of OSC 777 is supported.) diff --git a/commands.c b/commands.c index 85e200e2..7b93f044 100644 --- a/commands.c +++ b/commands.c @@ -19,23 +19,14 @@ cmd_scrollback_up(struct terminal *term, int rows) return; const struct grid *grid = term->grid; - const int offset = grid->offset; const int view = grid->view; const int grid_rows = grid->num_rows; - const int screen_rows = term->rows; - - int scrollback_start = (offset + screen_rows) & (grid_rows - 1); - - /* Part of the scrollback may be uninitialized */ - while (grid->rows[scrollback_start] == NULL) { - scrollback_start++; - scrollback_start &= grid_rows - 1; - } /* The view row number in scrollback relative coordinates. This is * the maximum number of rows we’re allowed to scroll */ - int view_sb_rel = view - scrollback_start + grid_rows; - view_sb_rel &= grid_rows - 1; + int sb_start = grid_sb_start_ignore_uninitialized(grid, term->rows); + int view_sb_rel = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, view); rows = min(rows, view_sb_rel); if (rows == 0) diff --git a/config.c b/config.c index d2be087b..ce0e27b8 100644 --- a/config.c +++ b/config.c @@ -115,6 +115,8 @@ static const char *const binding_action_map[] = { [BIND_ACTION_SHOW_URLS_LAUNCH] = "show-urls-launch", [BIND_ACTION_SHOW_URLS_PERSISTENT] = "show-urls-persistent", [BIND_ACTION_TEXT_BINDING] = "text-binding", + [BIND_ACTION_PROMPT_PREV] = "prompt-prev", + [BIND_ACTION_PROMPT_NEXT] = "prompt-next", /* Mouse-specific actions */ [BIND_ACTION_SELECT_BEGIN] = "select-begin", @@ -2663,6 +2665,8 @@ add_default_key_bindings(struct config *conf) {BIND_ACTION_FONT_SIZE_RESET, m_ctrl, {{XKB_KEY_KP_0}}}, {BIND_ACTION_SPAWN_TERMINAL, m_ctrl_shift, {{XKB_KEY_n}}}, {BIND_ACTION_SHOW_URLS_LAUNCH, m_ctrl_shift, {{XKB_KEY_u}}}, + {BIND_ACTION_PROMPT_PREV, m_ctrl_shift, {{XKB_KEY_z}}}, + {BIND_ACTION_PROMPT_NEXT, m_ctrl_shift, {{XKB_KEY_x}}}, }; conf->bindings.key.count = ALEN(bindings); diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index 2759b625..1d94219f 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -677,6 +677,9 @@ All _OSC_ sequences begin with *\\E]*, sometimes abbreviated _OSC_. | \\E] 119 \\E\\ : xterm : Reset selection foreground color +| \\E] 133 ; A \\E\\ +: FinalTerm +: Mark start of shell prompt | \\E] 555 \\E\\ : foot : Flash the entire terminal (foot extension) diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 6f0ad6db..afb8faf5 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -205,6 +205,13 @@ default) available; see *foot.ini*(5). *ctrl*+*shift*+*u* Activate URL mode, allowing you to "launch" URLs. +*ctrl*+*shift*+*z* + Jump to the previous, currently not visible, prompt. Requires + shell integration. + +*ctrl*+*shift*+*x* + Jump to the next prompt. Requires shell integration. + ## SCROLLBACK SEARCH *ctrl*+*r* @@ -370,6 +377,38 @@ commented out will usually be installed to */etc/xdg/foot/foot.ini*. For more information, see *foot.ini*(5). +# SHELL INTEGRATION + +## Current working directory + +New foot terminal instances (bound to *ctrl*+*shift*+*n* by default) +will open in the current working directory, if the shell in the +“parent” terminal reports directory changes. + +This is done with the OSC-7 escape sequence. Most shells can be +scripted to do this, if they do not support it natively. See the wiki +(https://codeberg.org/dnkl/foot/wiki#user-content-spawning-new-terminal-instances-in-the-current-working-directory) +for details. + + +## Jumping between prompts + +Foot can move the current viewport to focus prompts of already +executed commands (bound to *ctrl*+*shift*+*z*/*x* by default). + +For this to work, the shell needs to emit an OSC-133;A +(*\\E]133;A\\E\\\\*) sequence before each prompt. + +In zsh, one way to do this is to add a _precmd_ hook: + + *precmd() { + print -Pn "\\e]133;A\\e\\\\" + }* + +See the wiki +(https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts) +for details, and examples for other shells. + # TERMINFO Client applications use the terminfo identifier specified by the diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index bc1b6bdd..bd551442 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -773,6 +773,14 @@ e.g. *search-start=none*. jump label with a key sequence that will place the URL in the clipboard. Default: _none_. +*prompt-prev* + Jump to the previous, currently not visible, prompt (requires + shell integration, see *foot*(1)). Default: _Control+Shift+z_. + +*prompt-next* + Jump the next prompt (requires shell integration, see + *foot*(1)). Default: _Control+Shift+x_. + # SECTION: search-bindings This section lets you override the default key bindings used in diff --git a/foot.ini b/foot.ini index f4dc4280..10eb56e8 100644 --- a/foot.ini +++ b/foot.ini @@ -146,6 +146,8 @@ # show-urls-launch=Control+Shift+u # show-urls-copy=none # show-urls-persistent=none +# prompt-prev=Control+Shift+z +# prompt-next=Control+Shift+x # noop=none [search-bindings] diff --git a/grid.c b/grid.c index bc1018d9..a21f24a2 100644 --- a/grid.c +++ b/grid.c @@ -24,6 +24,7 @@ * scrollback, with the *highest* number being at the bottom of the * screen, where new input appears. */ + int grid_row_abs_to_sb(const struct grid *grid, int screen_rows, int abs_row) { @@ -43,6 +44,38 @@ int grid_row_sb_to_abs(const struct grid *grid, int screen_rows, int sb_rel_row) return abs_row; } +int +grid_sb_start_ignore_uninitialized(const struct grid *grid, int screen_rows) +{ + int scrollback_start = grid->offset + screen_rows; + scrollback_start &= grid->num_rows - 1; + + while (grid->rows[scrollback_start] == NULL) { + scrollback_start++; + scrollback_start &= grid->num_rows - 1; + } + + return scrollback_start; +} + +int +grid_row_abs_to_sb_precalc_sb_start(const struct grid *grid, int sb_start, + int abs_row) +{ + int rebased_row = abs_row - sb_start + grid->num_rows; + rebased_row &= grid->num_rows - 1; + return rebased_row; +} + +int +grid_row_sb_to_abs_precalc_sb_start(const struct grid *grid, int sb_start, + int sb_rel_row) +{ + int abs_row = sb_rel_row + sb_start; + abs_row &= grid->num_rows - 1; + return abs_row; +} + static void ensure_row_has_extra_data(struct row *row) { @@ -196,6 +229,7 @@ grid_snapshot(const struct grid *grid) clone_row->cells = xmalloc(grid->num_cols * sizeof(clone_row->cells[0])); clone_row->linebreak = row->linebreak; clone_row->dirty = row->dirty; + clone_row->prompt_marker = row->prompt_marker; for (int c = 0; c < grid->num_cols; c++) clone_row->cells[c] = row->cells[c]; @@ -286,6 +320,7 @@ grid_row_alloc(int cols, bool initialize) row->dirty = false; row->linebreak = true; row->extra = NULL; + row->prompt_marker = false; if (initialize) { row->cells = xcalloc(cols, sizeof(row->cells[0])); @@ -344,6 +379,7 @@ grid_resize_without_reflow( new_row->dirty = old_row->dirty; new_row->linebreak = false; + new_row->prompt_marker = old_row->prompt_marker; if (new_cols > old_cols) { /* Clear "new" columns */ @@ -503,6 +539,7 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, /* Scrollback is full, need to re-use a row */ grid_row_reset_extra(new_row); new_row->linebreak = true; + new_row->prompt_marker = false; tll_foreach(old_grid->sixel_images, it) { if (it->item.pos.row == *row_idx) { @@ -834,6 +871,9 @@ grid_resize_and_reflow( xassert(new_col_idx + amount <= new_cols); xassert(from + amount <= old_cols); + if (from == 0) + new_row->prompt_marker = old_row->prompt_marker; + memcpy( &new_row->cells[new_col_idx], &old_row->cells[from], amount * sizeof(struct cell)); diff --git a/grid.h b/grid.h index 22bd76bb..0664409c 100644 --- a/grid.h +++ b/grid.h @@ -25,6 +25,12 @@ void grid_resize_and_reflow( int grid_row_abs_to_sb(const struct grid *grid, int screen_rows, int abs_row); int grid_row_sb_to_abs(const struct grid *grid, int screen_rows, int sb_rel_row); +int grid_sb_start_ignore_uninitialized(const struct grid *grid, int screen_rows); +int grid_row_abs_to_sb_precalc_sb_start( + const struct grid *grid, int sb_start, int abs_row); +int grid_row_sb_to_abs_precalc_sb_start( + const struct grid *grid, int sb_start, int sb_rel_row); + static inline int grid_row_absolute(const struct grid *grid, int row_no) { @@ -37,7 +43,6 @@ grid_row_absolute_in_view(const struct grid *grid, int row_no) return (grid->view + row_no) & (grid->num_rows - 1); } - static inline struct row * _grid_row_maybe_alloc(struct grid *grid, int row_no, bool alloc_if_null) { diff --git a/input.c b/input.c index 916cbd38..fca46050 100644 --- a/input.c +++ b/input.c @@ -23,8 +23,9 @@ #define LOG_MODULE "input" #define LOG_ENABLE_DBG 0 #include "log.h" -#include "config.h" #include "commands.h" +#include "config.h" +#include "grid.h" #include "keymap.h" #include "kitty-keymap.h" #include "macros.h" @@ -335,6 +336,86 @@ execute_binding(struct seat *seat, struct terminal *term, term_to_slave(term, binding->aux->text.data, binding->aux->text.len); return true; + case BIND_ACTION_PROMPT_PREV: { + if (term->grid != &term->normal) + return false; + + struct grid *grid = term->grid; + const int sb_start = + grid_sb_start_ignore_uninitialized(grid, term->rows); + + /* Check each row from current view-1 (that is, the first + * currently not visible row), up to, and including, the + * scrollback start */ + for (int r_sb_rel = + grid_row_abs_to_sb_precalc_sb_start( + grid, sb_start, grid->view) - 1; + r_sb_rel >= 0; r_sb_rel--) + { + const int r_abs = + grid_row_sb_to_abs_precalc_sb_start(grid, sb_start, r_sb_rel); + + const struct row *row = grid->rows[r_abs]; + xassert(row != NULL); + + if (!row->prompt_marker) + continue; + + grid->view = r_abs; + term_damage_view(term); + render_refresh(term); + break; + } + + return true; + } + + case BIND_ACTION_PROMPT_NEXT: { + if (term->grid != &term->normal) + return false; + + struct grid *grid = term->grid; + const int num_rows = grid->num_rows; + + if (grid->view == grid->offset) { + /* Already at the bottom */ + return true; + } + + /* Check each row from view+1, to the bottom of the scrollback */ + for (int r_abs = (grid->view + 1) & (num_rows - 1); + ; + r_abs = (r_abs + 1) & (num_rows - 1)) + { + const struct row *row = grid->rows[r_abs]; + xassert(row != NULL); + + if (!row->prompt_marker) { + if (r_abs == grid->offset + term->rows - 1) { + /* We’ve reached the bottom of the scrollback */ + break; + } + continue; + } + + int sb_start = grid_sb_start_ignore_uninitialized(grid, term->rows); + int ofs_sb_rel = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, grid->offset); + int new_view_sb_rel = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, r_abs); + + new_view_sb_rel = min(ofs_sb_rel, new_view_sb_rel); + grid->view = grid_row_sb_to_abs_precalc_sb_start( + grid, sb_start, new_view_sb_rel); + + term_damage_view(term); + render_refresh(term); + break; + } + + return true; + } + case BIND_ACTION_SELECT_BEGIN: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_CHAR_WISE, false); diff --git a/key-binding.h b/key-binding.h index f30184c5..179b978b 100644 --- a/key-binding.h +++ b/key-binding.h @@ -36,6 +36,8 @@ enum bind_action_normal { BIND_ACTION_SHOW_URLS_LAUNCH, BIND_ACTION_SHOW_URLS_PERSISTENT, BIND_ACTION_TEXT_BINDING, + BIND_ACTION_PROMPT_PREV, + BIND_ACTION_PROMPT_NEXT, /* Mouse specific actions - i.e. they require a mouse coordinate */ BIND_ACTION_SELECT_BEGIN, diff --git a/osc.c b/osc.c index 2abd3e86..55cfcf84 100644 --- a/osc.c +++ b/osc.c @@ -869,6 +869,40 @@ osc_dispatch(struct terminal *term) term->colors.use_custom_selection = term->conf->colors.use_custom.selection; break; + case 133: + /* + * Shell integration; see + * https://iterm2.com/documentation-escape-codes.html (Shell + * Integration/FinalTerm) + * + * [PROMPT]prompt% [COMMAND_START] ls -l + * [COMMAND_EXECUTED] + * -rw-r--r-- 1 user group 127 May 1 2016 filename + * [COMMAND_FINISHED] + */ + switch (string[0]) { + case 'A': + LOG_DBG("FTCS_PROMPT: %dx%d", + term->grid->cursor.point.row, + term->grid->cursor.point.col); + + term->grid->cur_row->prompt_marker = true; + break; + + case 'B': + LOG_DBG("FTCS_COMMAND_START"); + break; + + case 'C': + LOG_DBG("FTCS_COMMAND_EXECUTED"); + break; + + case 'D': + LOG_DBG("FTCS_COMMAND_FINISHED"); + break; + } + break; + case 555: osc_flash(term); break; diff --git a/terminal.c b/terminal.c index 4b1e5931..c449aa0e 100644 --- a/terminal.c +++ b/terminal.c @@ -1812,6 +1812,7 @@ erase_line(struct terminal *term, struct row *row) { erase_cell_range(term, row, 0, term->cols - 1); row->linebreak = true; + row->prompt_marker = false; } void diff --git a/terminal.h b/terminal.h index 17a5abf9..bf6e74fe 100644 --- a/terminal.h +++ b/terminal.h @@ -116,9 +116,13 @@ struct row_data { struct row { struct cell *cells; + struct row_data *extra; + bool dirty; bool linebreak; - struct row_data *extra; + + /* Shell integration */ + bool prompt_marker; }; struct sixel { From 86893895235b5e5dd0616c8cbc3c4531a807d0cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 17 Jun 2022 18:42:42 +0200 Subject: [PATCH 0079/1323] key-binding: set BIND_ACTION_KEY_COUNT correctly When the new promp-prev/next bindings were added, we forgot to update BIND_ACTION_KEY_COUNT to reflect this. --- key-binding.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/key-binding.h b/key-binding.h index 179b978b..1c0e2a99 100644 --- a/key-binding.h +++ b/key-binding.h @@ -48,7 +48,7 @@ enum bind_action_normal { BIND_ACTION_SELECT_WORD_WS, BIND_ACTION_SELECT_ROW, - BIND_ACTION_KEY_COUNT = BIND_ACTION_TEXT_BINDING + 1, + BIND_ACTION_KEY_COUNT = BIND_ACTION_PROMPT_NEXT + 1, BIND_ACTION_COUNT = BIND_ACTION_SELECT_ROW + 1, }; From fabffd626b6bdeed624c0980f36283a902b2f864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 16 Jun 2022 18:30:43 +0200 Subject: [PATCH 0080/1323] wayland: log human readable SHM format names in debug builds --- meson.build | 2 +- shm-formats.h | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++ wayland.c | 17 ++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 shm-formats.h diff --git a/meson.build b/meson.build index 6b50ba03..6dc4d098 100644 --- a/meson.build +++ b/meson.build @@ -224,7 +224,7 @@ executable( 'tokenize.c', 'tokenize.h', 'url-mode.c', 'url-mode.h', 'user-notification.c', 'user-notification.h', - 'wayland.c', 'wayland.h', + 'wayland.c', 'wayland.h', 'shm-formats.h', wl_proto_src + wl_proto_headers, version, dependencies: [math, threads, libepoll, pixman, wayland_client, wayland_cursor, xkb, fontconfig, utf8proc, tllist, fcft], diff --git a/shm-formats.h b/shm-formats.h new file mode 100644 index 00000000..bb49bd94 --- /dev/null +++ b/shm-formats.h @@ -0,0 +1,117 @@ +#pragma once + +#if defined(_DEBUG) +static const struct shm_formats { + uint32_t format; + const char *description; +} shm_formats[] = { + {WL_SHM_FORMAT_ARGB8888, "ARGB8888"}, + {WL_SHM_FORMAT_XRGB8888, "XRGB8888"}, + {WL_SHM_FORMAT_C8, "C8"}, + {WL_SHM_FORMAT_RGB332, "RGB332"}, + {WL_SHM_FORMAT_BGR233, "BGR233"}, + {WL_SHM_FORMAT_XRGB4444, "XRGB4444"}, + {WL_SHM_FORMAT_XBGR4444, "XBGR4444"}, + {WL_SHM_FORMAT_RGBX4444, "RGBX4444"}, + {WL_SHM_FORMAT_BGRX4444, "BGRX4444"}, + {WL_SHM_FORMAT_ARGB4444, "ARGB4444"}, + {WL_SHM_FORMAT_ABGR4444, "ABGR4444"}, + {WL_SHM_FORMAT_RGBA4444, "RGBA4444"}, + {WL_SHM_FORMAT_BGRA4444, "BGRA4444"}, + {WL_SHM_FORMAT_XRGB1555, "XRGB1555"}, + {WL_SHM_FORMAT_XBGR1555, "XBGR1555"}, + {WL_SHM_FORMAT_RGBX5551, "RGBX5551"}, + {WL_SHM_FORMAT_BGRX5551, "BGRX5551"}, + {WL_SHM_FORMAT_ARGB1555, "ARGB1555"}, + {WL_SHM_FORMAT_ABGR1555, "ABGR1555"}, + {WL_SHM_FORMAT_RGBA5551, "RGBA5551"}, + {WL_SHM_FORMAT_BGRA5551, "BGRA5551"}, + {WL_SHM_FORMAT_RGB565, "RGB565"}, + {WL_SHM_FORMAT_BGR565, "BGR565"}, + {WL_SHM_FORMAT_RGB888, "RGB888"}, + {WL_SHM_FORMAT_BGR888, "BGR888"}, + {WL_SHM_FORMAT_XBGR8888, "XBGR8888"}, + {WL_SHM_FORMAT_RGBX8888, "RGBX8888"}, + {WL_SHM_FORMAT_BGRX8888, "BGRX8888"}, + {WL_SHM_FORMAT_ABGR8888, "ABGR8888"}, + {WL_SHM_FORMAT_RGBA8888, "RGBA8888"}, + {WL_SHM_FORMAT_BGRA8888, "BGRA8888"}, + {WL_SHM_FORMAT_XRGB2101010, "XRGB2101010"}, + {WL_SHM_FORMAT_XBGR2101010, "XBGR2101010"}, + {WL_SHM_FORMAT_RGBX1010102, "RGBX1010102"}, + {WL_SHM_FORMAT_BGRX1010102, "BGRX1010102"}, + {WL_SHM_FORMAT_ARGB2101010, "ARGB2101010"}, + {WL_SHM_FORMAT_ABGR2101010, "ABGR2101010"}, + {WL_SHM_FORMAT_RGBA1010102, "RGBA1010102"}, + {WL_SHM_FORMAT_BGRA1010102, "BGRA1010102"}, + {WL_SHM_FORMAT_YUYV, "YUYV"}, + {WL_SHM_FORMAT_YVYU, "YVYU"}, + {WL_SHM_FORMAT_UYVY, "UYVY"}, + {WL_SHM_FORMAT_VYUY, "VYUY"}, + {WL_SHM_FORMAT_AYUV, "AYUV"}, + {WL_SHM_FORMAT_NV12, "NV12"}, + {WL_SHM_FORMAT_NV21, "NV21"}, + {WL_SHM_FORMAT_NV16, "NV16"}, + {WL_SHM_FORMAT_NV61, "NV61"}, + {WL_SHM_FORMAT_YUV410, "YUV410"}, + {WL_SHM_FORMAT_YVU410, "YVU410"}, + {WL_SHM_FORMAT_YUV411, "YUV411"}, + {WL_SHM_FORMAT_YVU411, "YVU411"}, + {WL_SHM_FORMAT_YUV420, "YUV420"}, + {WL_SHM_FORMAT_YVU420, "YVU420"}, + {WL_SHM_FORMAT_YUV422, "YUV422"}, + {WL_SHM_FORMAT_YVU422, "YVU422"}, + {WL_SHM_FORMAT_YUV444, "YUV444"}, + {WL_SHM_FORMAT_YVU444, "YVU444"}, + {WL_SHM_FORMAT_R8, "R8"}, + {WL_SHM_FORMAT_R16, "R16"}, + {WL_SHM_FORMAT_RG88, "RG88"}, + {WL_SHM_FORMAT_GR88, "GR88"}, + {WL_SHM_FORMAT_RG1616, "RG1616"}, + {WL_SHM_FORMAT_GR1616, "GR1616"}, + {WL_SHM_FORMAT_XRGB16161616F, "XRGB16161616F"}, + {WL_SHM_FORMAT_XBGR16161616F, "XBGR16161616F"}, + {WL_SHM_FORMAT_ARGB16161616F, "ARGB16161616F"}, + {WL_SHM_FORMAT_ABGR16161616F, "ABGR16161616F"}, + {WL_SHM_FORMAT_XYUV8888, "XYUV8888"}, + {WL_SHM_FORMAT_VUY888, "VUY888"}, + {WL_SHM_FORMAT_VUY101010, "VUY101010"}, + {WL_SHM_FORMAT_Y210, "Y210"}, + {WL_SHM_FORMAT_Y212, "Y212"}, + {WL_SHM_FORMAT_Y216, "Y216"}, + {WL_SHM_FORMAT_Y410, "Y410"}, + {WL_SHM_FORMAT_Y412, "Y412"}, + {WL_SHM_FORMAT_Y416, "Y416"}, + {WL_SHM_FORMAT_XVYU2101010, "XVYU2101010"}, + {WL_SHM_FORMAT_XVYU12_16161616, "XVYU12_16161616"}, + {WL_SHM_FORMAT_XVYU16161616, "XVYU16161616"}, + {WL_SHM_FORMAT_Y0L0, "Y0L0"}, + {WL_SHM_FORMAT_X0L0, "X0L0"}, + {WL_SHM_FORMAT_Y0L2, "Y0L2"}, + {WL_SHM_FORMAT_X0L2, "X0L2"}, + {WL_SHM_FORMAT_YUV420_8BIT, "YUV420_8BIT"}, + {WL_SHM_FORMAT_YUV420_10BIT, "YUV420_10BIT"}, + {WL_SHM_FORMAT_XRGB8888_A8, "XRGB8888_A8"}, + {WL_SHM_FORMAT_XBGR8888_A8, "XBGR8888_A8"}, + {WL_SHM_FORMAT_RGBX8888_A8, "RGBX8888_A8"}, + {WL_SHM_FORMAT_BGRX8888_A8, "BGRX8888_A8"}, + {WL_SHM_FORMAT_RGB888_A8, "RGB888_A8"}, + {WL_SHM_FORMAT_BGR888_A8, "BGR888_A8"}, + {WL_SHM_FORMAT_RGB565_A8, "RGB565_A8"}, + {WL_SHM_FORMAT_BGR565_A8, "BGR565_A8"}, + {WL_SHM_FORMAT_NV24, "NV24"}, + {WL_SHM_FORMAT_NV42, "NV42"}, + {WL_SHM_FORMAT_P210, "P210"}, + {WL_SHM_FORMAT_P010, "P010"}, + {WL_SHM_FORMAT_P012, "P012"}, + {WL_SHM_FORMAT_P016, "P016"}, + {WL_SHM_FORMAT_AXBXGXRX106106106106, "AXBXGXRX106106106106"}, + {WL_SHM_FORMAT_NV15, "NV15"}, + {WL_SHM_FORMAT_Q410, "Q410"}, + {WL_SHM_FORMAT_Q401, "Q401"}, + {WL_SHM_FORMAT_XRGB16161616, "XRGB16161616"}, + {WL_SHM_FORMAT_XBGR16161616, "XBGR16161616"}, + {WL_SHM_FORMAT_ARGB16161616, "ARGB16161616"}, + {WL_SHM_FORMAT_ABGR16161616, "ABGR16161616"}, +}; +#endif diff --git a/wayland.c b/wayland.c index b15cd491..850b657b 100644 --- a/wayland.c +++ b/wayland.c @@ -27,6 +27,7 @@ #include "render.h" #include "selection.h" #include "shm.h" +#include "shm-formats.h" #include "util.h" #include "xmalloc.h" @@ -227,6 +228,22 @@ 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; + + for (size_t i = 0; i < ALEN(shm_formats); i++) { + if (shm_formats[i].format == format) { + LOG_DBG("shm: 0x%08x: %s", format, shm_formats[i].description); + have_description = true; + break; + } + } + + if (!have_description) + LOG_DBG("shm: 0x%08x: unknown", format); +#endif + if (format == WL_SHM_FORMAT_ARGB8888) wayl->have_argb8888 = true; } From bfc53d1e71d2d122a4652382aced9243d448ca21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 20 Jun 2022 19:20:28 +0200 Subject: [PATCH 0081/1323] shm-formats: #ifdef on formats added in 1.20 --- shm-formats.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shm-formats.h b/shm-formats.h index bb49bd94..3ada8266 100644 --- a/shm-formats.h +++ b/shm-formats.h @@ -1,5 +1,7 @@ #pragma once +#include + #if defined(_DEBUG) static const struct shm_formats { uint32_t format; @@ -109,9 +111,11 @@ static const struct shm_formats { {WL_SHM_FORMAT_NV15, "NV15"}, {WL_SHM_FORMAT_Q410, "Q410"}, {WL_SHM_FORMAT_Q401, "Q401"}, +#if WAYLAND_VERSION_MAJOR > 1 || WAYLAND_VERSION_MINOR >= 20 {WL_SHM_FORMAT_XRGB16161616, "XRGB16161616"}, {WL_SHM_FORMAT_XBGR16161616, "XBGR16161616"}, {WL_SHM_FORMAT_ARGB16161616, "ARGB16161616"}, {WL_SHM_FORMAT_ABGR16161616, "ABGR16161616"}, +#endif }; #endif From 2e4da6fbf69eb9edafdd74923fc155d16fde2133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 20 Jun 2022 19:29:57 +0200 Subject: [PATCH 0082/1323] selection: ignore drag-and-drops with unsupported mime-types Specifically, make sure we do *not* call wl_data_offer_receive() with a NULL mime-type, as this causes libwayland to error out, which in turn causes foot to exit. Closes #1092 --- CHANGELOG.md | 7 ++++++- selection.c | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a84bc13..dec74bc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,10 +74,15 @@ * Graphical corruption when viewport is at the top of the scrollback, and the output is scrolling. -* Improved text reflow of logical lines with trailing empty cells ([#1055][1055]) +* Improved text reflow of logical lines with trailing empty cells + ([#1055][1055]) * IME focus is now tracked independently from keyboard focus. +* Workaround for buggy compositors (e.g. some versions of GNOME) + allowing drag-and-drops even though foot has reported it does not + support the offered mime-types ([#1092][1092]). [1055]: https://codeberg.org/dnkl/foot/issues/1055 +[1092]: https://codeberg.org/dnkl/foot/issues/1092 ### Security diff --git a/selection.c b/selection.c index 37dd2c8d..20e2d8d8 100644 --- a/selection.c +++ b/selection.c @@ -2392,6 +2392,12 @@ drop(void *data, struct wl_data_device *wl_data_device) struct wl_clipboard *clipboard = &seat->clipboard; + if (clipboard->mime_type == DATA_OFFER_MIME_UNSET) { + LOG_WARN("compositor called data_device::drop() " + "even though we rejected the drag-and-drop"); + return; + } + struct dnd_context *ctx = xmalloc(sizeof(*ctx)); *ctx = (struct dnd_context){ .term = term, From 0d22e9fa01a8149af453ebe57b5e4ef67f00a4d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 20 Jun 2022 20:57:23 +0200 Subject: [PATCH 0083/1323] selection: explicitly reject *all* dnd offers not targeting the grid --- selection.c | 1 + 1 file changed, 1 insertion(+) diff --git a/selection.c b/selection.c index 20e2d8d8..63170b98 100644 --- a/selection.c +++ b/selection.c @@ -2343,6 +2343,7 @@ enter(void *data, struct wl_data_device *wl_data_device, uint32_t serial, /* Either terminal is already busy sending paste data, or mouse * pointer isn’t over the grid */ seat->clipboard.window = NULL; + wl_data_offer_accept(offer, serial, NULL); wl_data_offer_set_actions(offer, 0, 0); } From 6d4d4502e9188a07751b71b0bde7448e3e9eeef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 20 Jun 2022 20:57:26 +0200 Subject: [PATCH 0084/1323] selection: reject dnd offers with unsupported mime-types We were already doing this, implicitly, *if* the offer was for the main grid. This patch makes it more clear that we do not accept the offer. --- selection.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/selection.c b/selection.c index 63170b98..c62bdb2b 100644 --- a/selection.c +++ b/selection.c @@ -2322,6 +2322,9 @@ enter(void *data, struct wl_data_device *wl_data_device, uint32_t serial, xassert(offer == seat->clipboard.data_offer); + if (seat->clipboard.mime_type == DATA_OFFER_MIME_UNSET) + goto reject_offer; + /* Remember _which_ terminal the current DnD offer is targeting */ xassert(seat->clipboard.window == NULL); tll_foreach(wayl->terms, it) { @@ -2340,6 +2343,7 @@ enter(void *data, struct wl_data_device *wl_data_device, uint32_t serial, } } +reject_offer: /* Either terminal is already busy sending paste data, or mouse * pointer isn’t over the grid */ seat->clipboard.window = NULL; From 206e9a10502b29483b9f7fc35df921716fe8d7c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 21 Jun 2022 19:46:31 +0200 Subject: [PATCH 0085/1323] selection: wl_data_offer_set_action: use enum values instead of magic integers --- selection.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/selection.c b/selection.c index c62bdb2b..d4887382 100644 --- a/selection.c +++ b/selection.c @@ -2348,7 +2348,10 @@ reject_offer: * pointer isn’t over the grid */ seat->clipboard.window = NULL; wl_data_offer_accept(offer, serial, NULL); - wl_data_offer_set_actions(offer, 0, 0); + wl_data_offer_set_actions( + offer, + WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE, + WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE); } static void From d58290ea1213725ddf0f76be129fc5f680d9ccd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 28 Jun 2022 20:57:48 +0200 Subject: [PATCH 0086/1323] =?UTF-8?q?input:=20don=E2=80=99t=20ignore=20key?= =?UTF-8?q?board=20enter/leave=20events=20when=20there=E2=80=99s=20no=20ke?= =?UTF-8?q?ymap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The compositor _usually_ sends the keymap event *before* the enter event. But not always. Not (yet) having a keymap is not a reason to ignore the enter event (now, on the other hand, getting a key press/release event without a keymap is a compositor bug). Closes #1097 --- CHANGELOG.md | 3 +++ input.c | 6 ------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dec74bc1..a052d744 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,9 +80,12 @@ * Workaround for buggy compositors (e.g. some versions of GNOME) allowing drag-and-drops even though foot has reported it does not support the offered mime-types ([#1092][1092]). +* Keyboard enter/leave events being ignored if there is no keymap + ([#1097][1097]). [1055]: https://codeberg.org/dnkl/foot/issues/1055 [1092]: https://codeberg.org/dnkl/foot/issues/1092 +[1097]: https://codeberg.org/dnkl/foot/issues/1097 ### Security diff --git a/input.c b/input.c index fca46050..d4598cf6 100644 --- a/input.c +++ b/input.c @@ -591,9 +591,6 @@ keyboard_enter(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, LOG_DBG("%s: keyboard_enter: keyboard=%p, serial=%u, surface=%p", seat->name, (void *)wl_keyboard, serial, (void *)surface); - if (seat->kbd.xkb == NULL) - return; - term_kbd_focus_in(term); seat->kbd_focus = term; seat->kbd.serial = serial; @@ -653,9 +650,6 @@ keyboard_leave(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, LOG_DBG("keyboard_leave: keyboard=%p, serial=%u, surface=%p", (void *)wl_keyboard, serial, (void *)surface); - if (seat->kbd.xkb == NULL) - return; - xassert( seat->kbd_focus == NULL || surface == NULL || /* Seen on Sway 1.2 */ From 37f094280b06cf207b5720fa1a36ff7199225e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 30 Jun 2022 19:37:01 +0200 Subject: [PATCH 0087/1323] =?UTF-8?q?Revert=20"grid:=20invert=20the=20defa?= =?UTF-8?q?ult=20value=20of=20=E2=80=98linebreak=E2=80=99,=20from=20false?= =?UTF-8?q?=20to=20true"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit cdd46cdf85b1087ac7465c646bb05078f1bbe85b. --- grid.c | 14 +++----------- terminal.c | 4 +++- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/grid.c b/grid.c index a21f24a2..18ca1457 100644 --- a/grid.c +++ b/grid.c @@ -318,7 +318,7 @@ grid_row_alloc(int cols, bool initialize) { struct row *row = xmalloc(sizeof(*row)); row->dirty = false; - row->linebreak = true; + row->linebreak = false; row->extra = NULL; row->prompt_marker = false; @@ -538,7 +538,7 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, } else { /* Scrollback is full, need to re-use a row */ grid_row_reset_extra(new_row); - new_row->linebreak = true; + new_row->linebreak = false; new_row->prompt_marker = false; tll_foreach(old_grid->sixel_images, it) { @@ -878,14 +878,6 @@ grid_resize_and_reflow( &new_row->cells[new_col_idx], &old_row->cells[from], amount * sizeof(struct cell)); - /* - * We’ve “printed” to this line - reset linebreak. - * - * If the old line ends with a hard linebreak, we’ll - * set linebreak=true on the last new row we print to. - */ - new_row->linebreak = false; - count -= amount; from += amount; new_col_idx += amount; @@ -944,7 +936,7 @@ grid_resize_and_reflow( } - if (old_row->linebreak && col_count > 0) { + if (old_row->linebreak) { /* Erase the remaining cells */ memset(&new_row->cells[new_col_idx], 0, (new_cols - new_col_idx) * sizeof(new_row->cells[0])); diff --git a/terminal.c b/terminal.c index c449aa0e..dd84b8cf 100644 --- a/terminal.c +++ b/terminal.c @@ -1811,7 +1811,7 @@ static inline void erase_line(struct terminal *term, struct row *row) { erase_cell_range(term, row, 0, term->cols - 1); - row->linebreak = true; + row->linebreak = false; row->prompt_marker = false; } @@ -3298,6 +3298,7 @@ term_print(struct terminal *term, char32_t wc, int width) /* *Must* get current cell *after* linewrap+insert */ struct row *row = grid->cur_row; row->dirty = true; + row->linebreak = true; struct cell *cell = &row->cells[col]; cell->wc = term->vt.last_printed = wc; @@ -3357,6 +3358,7 @@ ascii_printer_fast(struct terminal *term, char32_t wc) struct row *row = grid->cur_row; row->dirty = true; + row->linebreak = true; struct cell *cell = &row->cells[col]; cell->wc = term->vt.last_printed = wc; From 0c60bb3f295936dff04f56d28ab5dcb4db898a54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 30 Jun 2022 19:47:18 +0200 Subject: [PATCH 0088/1323] grid: reflow: require col count > 0 when skipping line truncation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When reflowing the grid, we truncate lines with a hard linebreak after the last non-empty cell. This way we don’t reflow trailing empty cells to a new line when resizing the window to a smaller size. However, “logical” lines (i.e. those without a hard linebreak) are *not* truncated. This is to ensure we don’t trim empty cells in the middle of a logical line spanning multiple physical lines. Since newly allocated rows are initialized with linebreak=false, we need to ensure _those_ are still truncated - otherwise all that empty space under the current prompt will be reflowed. Note that this is a temporary workaround. The correct solution, I think, is to track whether a line has been printed to or not, and simply ignore (not reflow) lines that haven’t yet been touched. --- grid.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grid.c b/grid.c index 18ca1457..2790d16b 100644 --- a/grid.c +++ b/grid.c @@ -740,7 +740,7 @@ grid_resize_and_reflow( } } - if (!old_row->linebreak /*&& col_count > 0*/) { + if (!old_row->linebreak && col_count > 0) { /* Don’t truncate logical lines */ col_count = old_cols; } From 87e4004960265494fcab21c71fff48b4f8b0af33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 22 Jul 2022 10:44:33 +0200 Subject: [PATCH 0089/1323] =?UTF-8?q?csi:=20clamp=20color=20index=20for=20?= =?UTF-8?q?=E2=80=98CSI=2038/48=20;=205=20;=20idx=20m=E2=80=99=20sequences?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Indexed color values are stored in the cell attributes as color indices (into the 256-color table). However, the index from the CSI was not validated in any way, meaning you can do something like this: echo -e ‘\e[38:5:1024m CRASH \e[m’ and foot will crash on an out-of-bounds access. Fix by clamping the color index. Closes #1111 --- CHANGELOG.md | 4 ++++ csi.c | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a052d744..7e7aa4a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,10 +82,14 @@ support the offered mime-types ([#1092][1092]). * Keyboard enter/leave events being ignored if there is no keymap ([#1097][1097]). +* Crash when application emitted an invalid `CSI 38;5;m`, `CSI + 38:5:m`, `CSI 48;5;m` or `CSI 48:5:m` sequence + ([#1111][1111]). [1055]: https://codeberg.org/dnkl/foot/issues/1055 [1092]: https://codeberg.org/dnkl/foot/issues/1092 [1097]: https://codeberg.org/dnkl/foot/issues/1097 +[1111]: https://codeberg.org/dnkl/foot/issues/1111 ### Security diff --git a/csi.c b/csi.c index 57cae6b3..659839f0 100644 --- a/csi.c +++ b/csi.c @@ -128,7 +128,8 @@ csi_sgr(struct terminal *term) term->vt.params.v[i + 1].value == 5) { src = COLOR_BASE256; - color = term->vt.params.v[i + 2].value; + color = min(term->vt.params.v[i + 2].value, + ALEN(term->colors.table) - 1); i += 2; } @@ -149,7 +150,8 @@ csi_sgr(struct terminal *term) term->vt.params.v[i].sub.value[0] == 5) { src = COLOR_BASE256; - color = term->vt.params.v[i].sub.value[1]; + color = min(term->vt.params.v[i].sub.value[1], + ALEN(term->colors.table) - 1); } /* From 24c2d568042c6fb0533071aaf72b64405ca2c324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 28 Jul 2022 18:55:34 +0200 Subject: [PATCH 0090/1323] =?UTF-8?q?render:=20it=E2=80=99s=20unlikely()?= =?UTF-8?q?=20the=20current=20cell=20is=20where=20the=20cursor=20is?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- render.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.c b/render.c index 3b281a89..0dd251ae 100644 --- a/render.c +++ b/render.c @@ -699,7 +699,7 @@ render_cell(struct terminal *term, pixman_image_t *pix, mtx_unlock(&term->render.workers.lock); } - if (has_cursor && term->cursor_style == CURSOR_BLOCK && term->kbd_focus) + if (unlikely(has_cursor && term->cursor_style == CURSOR_BLOCK && term->kbd_focus)) draw_cursor(term, cell, font, pix, &fg, &bg, x, y, cell_cols); if (cell->wc == 0 || cell->wc >= CELL_SPACER || cell->wc == U'\t' || From 4abf46955f259ed392c5c7fbfbb931cbc7cc2b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 26 Jul 2022 18:19:34 +0200 Subject: [PATCH 0091/1323] keymap: change alt+escape to emit \E\E instead of a CSI 27 sequence Closes #1105 --- CHANGELOG.md | 3 +++ keymap.h | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e7aa4a4..ea0f5803 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,8 +64,11 @@ mode ([#1084][1084]). * NUL is now stripped when pasting in non-bracketed mode ([#1084][1084]). +* `alt`+`escape` now emits `\E\E` instead of a `CSI 27` sequence + ([#1105][1105]). [1084]: https://codeberg.org/dnkl/foot/issues/1084 +[1105]: https://codeberg.org/dnkl/foot/issues/1105 ### Deprecated diff --git a/keymap.h b/keymap.h index 9793d882..79d4b8b3 100644 --- a/keymap.h +++ b/keymap.h @@ -24,7 +24,7 @@ struct key_data { static const struct key_data key_escape[] = { {MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;2;27~"}, - {MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;3;27~"}, + {MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\033"}, {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;4;27~"}, {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;5;27~"}, {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;6;27~"}, From 801970aa332da5786f91d2f9410d4ed9f440c6ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 26 Jul 2022 18:44:29 +0200 Subject: [PATCH 0092/1323] =?UTF-8?q?input:=20kitty:=20always=20treat=20co?= =?UTF-8?q?mposed=20characters=20as=20=E2=80=98printable=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Certain dead key combinations results different escape sequences in foot, compared to kitty, when the kitty keyboard protocol is used. if (composed && is_text) key = utf32; else { key = xkb_keysym_to_utf32(sym_to_use); if (key == 0) return false; /* The *shifted* key. May be the same as the unshifted * key - if so, this is filtered out below, when * emitting the CSI */ alternate = xkb_keysym_to_utf32(sym); } If is_text=false, we’ll fall through to the non-composed logic. is_text is set to true if the character is printable *and* there aren’t any non-consumed modifiers enabled. shift+space is one example where shift is *not* consumed (typically - may be layouts where it is). As a result, pressing ", followed by shift+space with the international english keyboard layout (where " is a dead key) results in different sequences in foot and kitty. This patch fixes this by always treating composed characters as printable. Closes #1120 --- CHANGELOG.md | 4 ++++ input.c | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea0f5803..29eaf084 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,11 +88,15 @@ * Crash when application emitted an invalid `CSI 38;5;m`, `CSI 38:5:m`, `CSI 48;5;m` or `CSI 48:5:m` sequence ([#1111][1111]). +* Certain dead-key combinations resulting in different escape + sequences compared to kitty, when the kitty keyboard protocol is + used ([#1120][1120]). [1055]: https://codeberg.org/dnkl/foot/issues/1055 [1092]: https://codeberg.org/dnkl/foot/issues/1092 [1097]: https://codeberg.org/dnkl/foot/issues/1097 [1111]: https://codeberg.org/dnkl/foot/issues/1111 +[1120]: https://codeberg.org/dnkl/foot/issues/1120 ### Security diff --git a/input.c b/input.c index d4598cf6..e0f21c6e 100644 --- a/input.c +++ b/input.c @@ -1239,7 +1239,7 @@ emit_escapes: ? ctx->level0_syms.syms[0] : sym; - if (composed && is_text) + if (composed) key = utf32; else { key = xkb_keysym_to_utf32(sym_to_use); From 4873004c379f9f06babd65ffc65ba4f99fa62906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 27 Jul 2022 19:13:39 +0200 Subject: [PATCH 0093/1323] test: config: test colors.{jump-labels,scrollback-indicator} --- tests/test-config.c | 54 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/tests/test-config.c b/tests/test-config.c index 874f5b05..5a7cda63 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -401,6 +401,52 @@ test_color(struct context *ctx, bool (*parse_fun)(struct context *ctx), } } +static void +test_two_colors(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, bool alpha_allowed, + uint32_t *ptr1, uint32_t *ptr2) +{ + ctx->key = key; + + const struct { + const char *option_string; + uint32_t color1; + uint32_t color2; + bool invalid; + } input[] = { + {"000000 000000", 0, 0}, + + /* No alpha */ + {"999999 888888", 0x999999, 0x888888}, + {"ffffff aaaaaa", 0xffffff, 0xaaaaaa}, + + /* Both colors have alpha component */ + {"ffffffff 00000000", 0xffffffff, 0x00000000, !alpha_allowed}, + {"aabbccdd, ee112233", 0xaabbccdd, 0xee112233, !alpha_allowed}, + + /* Only one color has alpha component */ + {"ffffffff 112233", 0xffffffff, 0x112233, !alpha_allowed}, + {"ffffff ff112233", 0x00ffffff, 0xff112233, !alpha_allowed}, + + {"unittest-invalid-color", 0, 0, true}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + } + } +} + static void test_section_main(void) { @@ -616,6 +662,12 @@ test_section_colors(void) test_color(&ctx, &parse_section_colors, "selection-foreground", false, &conf.colors.selection_fg); test_color(&ctx, &parse_section_colors, "selection-background", false, &conf.colors.selection_bg); test_color(&ctx, &parse_section_colors, "urls", false, &conf.colors.url); + test_two_colors(&ctx, &parse_section_colors, "jump-labels", false, + &conf.colors.jump_label.fg, + &conf.colors.jump_label.bg); + test_two_colors(&ctx, &parse_section_colors, "scrollback-indicator", false, + &conf.colors.scrollback_indicator.fg, + &conf.colors.scrollback_indicator.bg); for (size_t i = 0; i < 255; i++) { char key_name[4]; @@ -627,8 +679,6 @@ test_section_colors(void) test_invalid_key(&ctx, &parse_section_colors, "256"); /* TODO: alpha (float in range 0-1, converted to uint16_t) */ - /* TODO: jump-labels (two colors) */ - /* TODO: scrollback-indicator (two colors) */ config_free(&conf); } From d79a3b9350ba864464229a1c188e2f696fac5a26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 27 Jul 2022 19:14:27 +0200 Subject: [PATCH 0094/1323] config: add colors.search-box-{no-,}match Closes #1112 --- CHANGELOG.md | 3 +++ config.c | 28 ++++++++++++++++++++++++++++ config.h | 14 ++++++++++++++ doc/foot.ini.5.scd | 10 ++++++++++ foot.ini | 6 ++++-- render.c | 25 ++++++++++++++++++++----- tests/test-config.c | 6 ++++++ 7 files changed, 85 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29eaf084..55961c88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,10 +50,13 @@ * Support for jumping to previous/next prompt (requires shell integration). By default bound to `ctrl`+`shift`+`z` and `ctrl`+`shift`+`x` respectively ([#30][30]). +* `colors.search-box-no-match` and `colors.search-box-match` options + to `foot.ini` ([#1112][1112]). [1058]: https://codeberg.org/dnkl/foot/issues/1058 [1070]: https://codeberg.org/dnkl/foot/issues/1070 [30]: https://codeberg.org/dnkl/foot/issues/30 +[1112]: https://codeberg.org/dnkl/foot/issues/1112 ### Changed diff --git a/config.c b/config.c index ce0e27b8..0e509995 100644 --- a/config.c +++ b/config.c @@ -1171,6 +1171,34 @@ parse_section_colors(struct context *ctx) return true; } + else if (strcmp(key, "search-box-no-match") == 0) { + if (!value_to_two_colors( + ctx, + &conf->colors.search_box.no_match.fg, + &conf->colors.search_box.no_match.bg, + false)) + { + return false; + } + + conf->colors.use_custom.search_box_no_match = true; + return true; + } + + else if (strcmp(key, "search-box-match") == 0) { + if (!value_to_two_colors( + ctx, + &conf->colors.search_box.match.fg, + &conf->colors.search_box.match.bg, + false)) + { + return false; + } + + conf->colors.use_custom.search_box_match = true; + return true; + } + else if (strcmp(key, "urls") == 0) { if (!value_to_color(ctx, &conf->colors.url, false)) return false; diff --git a/config.h b/config.h index de5d8a7b..70e182e3 100644 --- a/config.h +++ b/config.h @@ -217,11 +217,25 @@ struct config { uint32_t bg; } scrollback_indicator; + struct { + struct { + uint32_t fg; + uint32_t bg; + } no_match; + + struct { + uint32_t fg; + uint32_t bg; + } match; + } search_box; + struct { bool selection:1; bool jump_label:1; bool scrollback_indicator:1; bool url:1; + bool search_box_no_match:1; + bool search_box_match:1; uint8_t dim; } use_custom; } colors; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index bd551442..2e62a41f 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -566,6 +566,16 @@ can configure the background transparency with the _alpha_ option. (indicator itself) colors for the scrollback indicator. Default: _regular0 bright4_. +*search-box-no-match* + Two color values specifying the foreground (text) and background + colors for the scrollback search box, when there are no + matches. Default: _regular0 regular1_. + +*search-box-match* + Two color values specifying the foreground (text) and background + colors for the scrollback search box, when the search box is + either empty, or there are matches. Default: _regular0 regular3_. + *urls* Color to use for the underline used to highlight URLs in URL mode. Default: _regular3_. diff --git a/foot.ini b/foot.ini index 10eb56e8..e8ff1870 100644 --- a/foot.ini +++ b/foot.ini @@ -104,9 +104,11 @@ ## Misc colors # selection-foreground= # selection-background= -# jump-labels= +# jump-labels= # black-on-yellow +# scrollback-indicator= # black-on-bright-blue +# search-box-no-match= # black-on-red +# search-box-match= # black-on-yellow # urls= -# scrollback-indicator= [csd] # preferred=server diff --git a/render.c b/render.c index 0dd251ae..9333deb7 100644 --- a/render.c +++ b/render.c @@ -3046,10 +3046,20 @@ render_search_box(struct terminal *term) #define WINDOW_X(x) (margin + x) #define WINDOW_Y(y) (term->height - margin - height + y) - /* Background - yellow on empty/match, red on mismatch */ - pixman_color_t color = color_hex_to_pixman( - term->search.match_len == text_len - ? term->colors.table[3] : term->colors.table[1]); + const bool is_match = term->search.match_len == text_len; + const bool custom_colors = is_match + ? term->conf->colors.use_custom.search_box_match + : term->conf->colors.use_custom.search_box_no_match; + + /* Background - yellow on empty/match, red on mismatch (default) */ + const pixman_color_t color = color_hex_to_pixman( + is_match + ? (custom_colors + ? term->conf->colors.search_box.match.bg + : term->colors.table[3]) + : (custom_colors + ? term->conf->colors.search_box.no_match.bg + : term->colors.table[1])); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &color, @@ -3065,7 +3075,12 @@ render_search_box(struct terminal *term) const int x_ofs = term->font_x_ofs; int x = x_left; int y = margin; - pixman_color_t fg = color_hex_to_pixman(term->colors.table[0]); + pixman_color_t fg = color_hex_to_pixman( + custom_colors + ? (is_match + ? term->conf->colors.search_box.match.fg + : term->conf->colors.search_box.no_match.fg) + : term->colors.table[0]); /* Move offset we start rendering at, to ensure the cursor is visible */ for (size_t i = 0, cell_idx = 0; i <= term->search.cursor; cell_idx += widths[i], i++) { diff --git a/tests/test-config.c b/tests/test-config.c index 5a7cda63..930be6bf 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -668,6 +668,12 @@ test_section_colors(void) test_two_colors(&ctx, &parse_section_colors, "scrollback-indicator", false, &conf.colors.scrollback_indicator.fg, &conf.colors.scrollback_indicator.bg); + test_two_colors(&ctx, &parse_section_colors, "search-box-no-match", false, + &conf.colors.search_box.no_match.fg, + &conf.colors.search_box.no_match.bg); + test_two_colors(&ctx, &parse_section_colors, "search-box-match", false, + &conf.colors.search_box.match.fg, + &conf.colors.search_box.match.bg); for (size_t i = 0; i < 255; i++) { char key_name[4]; From 632c4839cdb1c673bfb3903e1d43802bfdcbf03f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 28 Jul 2022 18:56:28 +0200 Subject: [PATCH 0095/1323] search: find_next(): handle trailing SPACER cells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make sure to increment match_end_col to account for trailing SPACER cells after a match. This fixes an issue where search matches weren’t highlighted correctly when the match *ends* with a double-width character. --- CHANGELOG.md | 2 ++ search.c | 3 +++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55961c88..75950976 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,8 @@ * Certain dead-key combinations resulting in different escape sequences compared to kitty, when the kitty keyboard protocol is used ([#1120][1120]). +* Search matches ending with a double-width character not being + highlighted correctly. [1055]: https://codeberg.org/dnkl/foot/issues/1055 [1092]: https://codeberg.org/dnkl/foot/issues/1092 diff --git a/search.c b/search.c index f6d377ea..88bc88aa 100644 --- a/search.c +++ b/search.c @@ -371,6 +371,9 @@ find_next(struct terminal *term, enum search_direction direction, i += additional_chars; match_len += additional_chars; match_end_col++; + + while (match_row->cells[match_end_col].wc > CELL_SPACER) + match_end_col++; } if (match_len != term->search.len) { From a05eaf28bdeea793829bddb6cc99220b1adb1d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 28 Jul 2022 18:27:13 +0200 Subject: [PATCH 0096/1323] selection: selection_on_rows(): use scrollback relative coords MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When checking if the current selection intersects with the region (passed as parameter to the function), use scrollback relative coordinates. This fixes an issue where selections crossing the scrollback wrap-around being misdetected, resulting in either the selection being canceled while scrolling, even though it wasn’t scrolled out, or the selection _not_ being canceled, when it _was_ scrolled out. --- CHANGELOG.md | 1 + selection.c | 74 +++++++++++++++++++++++++++++++++++++++++----------- selection.h | 2 ++ terminal.c | 12 ++++----- 4 files changed, 68 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75950976..16b2da76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,6 +96,7 @@ used ([#1120][1120]). * Search matches ending with a double-width character not being highlighted correctly. +* Selection not being cancelled correctly when scrolled out. [1055]: https://codeberg.org/dnkl/foot/issues/1055 [1092]: https://codeberg.org/dnkl/foot/issues/1092 diff --git a/selection.c b/selection.c index d4887382..36f4078f 100644 --- a/selection.c +++ b/selection.c @@ -64,41 +64,85 @@ selection_get_end(const struct terminal *term) bool selection_on_rows(const struct terminal *term, int row_start, int row_end) { + xassert(term->selection.coords.end.row >= 0); + LOG_DBG("on rows: %d-%d, range: %d-%d (offset=%d)", term->selection.coords.start.row, term->selection.coords.end.row, row_start, row_end, term->grid->offset); - if (term->selection.coords.end.row < 0) - return false; - - xassert(term->selection.coords.start.row != -1); - row_start += term->grid->offset; row_end += term->grid->offset; + xassert(row_end >= row_start); const struct coord *start = &term->selection.coords.start; const struct coord *end = &term->selection.coords.end; - if ((row_start <= start->row && row_end >= start->row) || - (row_start <= end->row && row_end >= end->row)) + const struct grid *grid = term->grid; + const int sb_start = grid->offset + term->rows; + + /* Use scrollback relative coords when checking for overlap */ + const int rel_row_start = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, row_start); + const int rel_row_end = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, row_start); + int rel_sel_start = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, start->row); + int rel_sel_end = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, end->row); + + if (rel_sel_start > rel_sel_end) { + int tmp = rel_sel_start; + rel_sel_start = rel_sel_end; + rel_sel_end = tmp; + } + + if ((rel_row_start <= rel_sel_start && rel_row_end >= rel_sel_start) || + (rel_row_start <= rel_sel_end && rel_row_end >= rel_sel_end)) { /* The range crosses one of the selection boundaries */ return true; } - /* For the last check we must ensure start <= end */ - if (start->row > end->row) { - const struct coord *tmp = start; - start = end; - end = tmp; - } - - if (row_start >= start->row && row_end <= end->row) + if (rel_row_start >= rel_sel_start && rel_row_end <= rel_sel_end) return true; return false; } +void +selection_scroll_up(struct terminal *term, int rows) +{ + xassert(term->selection.coords.end.row >= 0); + + const int rel_row_start = + grid_row_abs_to_sb(term->grid, term->rows, term->selection.coords.start.row); + const int rel_row_end = + grid_row_abs_to_sb(term->grid, term->rows, term->selection.coords.end.row); + const int actual_start = min(rel_row_start, rel_row_end); + + if (actual_start - rows < 0) { + /* Part of the selection will be scrolled out, cancel it */ + selection_cancel(term); + } +} + +void +selection_scroll_down(struct terminal *term, int rows) +{ + xassert(term->selection.coords.end.row >= 0); + + const int rel_row_start = + grid_row_abs_to_sb(term->grid, term->rows, term->selection.coords.start.row); + const int rel_row_end = + grid_row_abs_to_sb(term->grid, term->rows, term->selection.coords.end.row); + const int actual_end = max(rel_row_start, rel_row_end); + + if (actual_end + rows <= term->grid->num_rows) { + /* Part of the selection will be scrolled out, cancel it */ + selection_cancel(term); + } +} + void selection_view_up(struct terminal *term, int new_view) { diff --git a/selection.h b/selection.h index 3d0c224e..c6d7f968 100644 --- a/selection.h +++ b/selection.h @@ -22,6 +22,8 @@ void selection_extend( bool selection_on_rows(const struct terminal *term, int start, int end); +void selection_scroll_up(struct terminal *term, int rows); +void selection_scroll_down(struct terminal *term, int rows); void selection_view_up(struct terminal *term, int new_view); void selection_view_down(struct terminal *term, int new_view); diff --git a/terminal.c b/terminal.c index dd84b8cf..0763fb52 100644 --- a/terminal.c +++ b/terminal.c @@ -2537,11 +2537,11 @@ term_scroll_partial(struct terminal *term, struct scroll_region region, int rows * scrolled in (i.e. re-used lines). */ if (selection_on_top_region(term, region) || - selection_on_bottom_region(term, region) || - selection_on_rows(term, region.end - rows, region.end - 1)) + selection_on_bottom_region(term, region)) { selection_cancel(term); - } + } else + selection_scroll_up(term, rows); } sixel_scroll_up(term, rows); @@ -2611,11 +2611,11 @@ term_scroll_reverse_partial(struct terminal *term, * scrolled in (i.e. re-used lines). */ if (selection_on_top_region(term, region) || - selection_on_bottom_region(term, region) || - selection_on_rows(term, region.start, region.start + rows - 1)) + selection_on_bottom_region(term, region)) { selection_cancel(term); - } + } else + selection_scroll_down(term, rows); } sixel_scroll_down(term, rows); From 6ebf55572e765394e90c96b5fec0e2c55b3b434e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 28 Jul 2022 18:31:11 +0200 Subject: [PATCH 0097/1323] selection: foreach_selected_*(): refactor: use grid_row_abs_to_sb() --- selection.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/selection.c b/selection.c index 36f4078f..fd821c06 100644 --- a/selection.c +++ b/selection.c @@ -181,14 +181,14 @@ foreach_selected_normal( const struct coord *start = &_start; const struct coord *end = &_end; - const int scrollback_start = term->grid->offset + term->rows; const int grid_rows = term->grid->num_rows; + /* Start/end rows, relative to the scrollback start */ /* Start/end rows, relative to the scrollback start */ const int rel_start_row = - (start->row - scrollback_start + grid_rows) & (grid_rows - 1); + grid_row_abs_to_sb(term->grid, term->rows, start->row); const int rel_end_row = - (end->row - scrollback_start + grid_rows) & (grid_rows - 1); + grid_row_abs_to_sb(term->grid, term->rows, end->row); int start_row, end_row; int start_col, end_col; @@ -244,14 +244,13 @@ foreach_selected_block( const struct coord *start = &_start; const struct coord *end = &_end; - const int scrollback_start = term->grid->offset + term->rows; const int grid_rows = term->grid->num_rows; /* Start/end rows, relative to the scrollback start */ const int rel_start_row = - (start->row - scrollback_start + grid_rows) & (grid_rows - 1); + grid_row_abs_to_sb(term->grid, term->rows, start->row); const int rel_end_row = - (end->row - scrollback_start + grid_rows) & (grid_rows - 1); + grid_row_abs_to_sb(term->grid, term->rows, end->row); struct coord top_left = { .row = (rel_start_row < rel_end_row From b8506bbea049548310552e210e9cb363195fe74b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 28 Jul 2022 18:32:17 +0200 Subject: [PATCH 0098/1323] selection: extend: use scrollback relative coordinates When extending a selection, we determine *how* to extend it (which endpoint to move, and whether to grow or shrink the selection) by comparing the extension point with the old start and end coordinates. For this to work correctly, we need to use scrollback relative coordinates. This fixes an issue where extending a very large selection (covering many pages) sometimes shrunk the selection instead of growing it, or just misbehaving in general. --- CHANGELOG.md | 1 + selection.c | 48 +++++++++++++++++++++++++++++++++--------------- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16b2da76..fc84a398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,6 +97,7 @@ * Search matches ending with a double-width character not being highlighted correctly. * Selection not being cancelled correctly when scrolled out. +* Extending a multi-page selection behaving inconsistently. [1055]: https://codeberg.org/dnkl/foot/issues/1055 [1092]: https://codeberg.org/dnkl/foot/issues/1092 diff --git a/selection.c b/selection.c index fd821c06..54faf49b 100644 --- a/selection.c +++ b/selection.c @@ -1002,27 +1002,37 @@ selection_extend_normal(struct terminal *term, int col, int row, const struct coord *start = &term->selection.coords.start; const struct coord *end = &term->selection.coords.end; - if (start->row > end->row || - (start->row == end->row && start->col > end->col)) + const int rel_row = grid_row_abs_to_sb(term->grid, term->rows, row); + int rel_start_row = grid_row_abs_to_sb(term->grid, term->rows, start->row); + int rel_end_row = grid_row_abs_to_sb(term->grid, term->rows, end->row); + + if (rel_start_row > rel_end_row || + (rel_start_row == rel_end_row && start->col > end->col)) { const struct coord *tmp = start; start = end; end = tmp; - } - xassert(start->row < end->row || start->col < end->col); + int tmp_row = rel_start_row; + rel_start_row = rel_end_row; + rel_end_row = tmp_row; + } struct coord new_start, new_end; enum selection_direction direction; - if (row < start->row || (row == start->row && col < start->col)) { + if (rel_row < rel_start_row || + (rel_row == rel_start_row && col < start->col)) + { /* Extend selection to start *before* current start */ new_start = *end; new_end = (struct coord){col, row}; direction = SELECTION_LEFT; } - else if (row > end->row || (row == end->row && col > end->col)) { + else if (rel_row > rel_end_row || + (rel_row == rel_end_row && col > end->col)) + { /* Extend selection to end *after* current end */ new_start = *start; new_end = (struct coord){col, row}; @@ -1032,10 +1042,10 @@ selection_extend_normal(struct terminal *term, int col, int row, else { /* Shrink selection from start or end, depending on which one is closest */ - const int linear = row * term->cols + col; + const int linear = rel_row * term->cols + col; - if (abs(linear - (start->row * term->cols + start->col)) < - abs(linear - (end->row * term->cols + end->col))) + if (abs(linear - (rel_start_row * term->cols + start->col)) < + abs(linear - (rel_end_row * term->cols + end->col))) { /* Move start point */ new_start = *end; @@ -1110,33 +1120,41 @@ selection_extend_block(struct terminal *term, int col, int row) const struct coord *start = &term->selection.coords.start; const struct coord *end = &term->selection.coords.end; + const int rel_start_row = + grid_row_abs_to_sb(term->grid, term->rows, start->row); + const int rel_end_row = + grid_row_abs_to_sb(term->grid, term->rows, end->row); + struct coord top_left = { - .row = min(start->row, end->row), + .row = rel_start_row < rel_end_row ? start->row : end->row, .col = min(start->col, end->col), }; struct coord top_right = { - .row = min(start->row, end->row), + .row = top_left.row, .col = max(start->col, end->col), }; struct coord bottom_left = { - .row = max(start->row, end->row), + .row = rel_start_row > rel_end_row ? start->row : end->row, .col = min(start->col, end->col), }; struct coord bottom_right = { - .row = max(start->row, end->row), + .row = bottom_left.row, .col = max(start->col, end->col), }; + const int rel_row = grid_row_abs_to_sb(term->grid, term->rows, row); + const int rel_top_row = grid_row_abs_to_sb(term->grid, term->rows, top_left.row); + const int rel_bottom_row = grid_row_abs_to_sb(term->grid, term->rows, bottom_left.row); struct coord new_start; struct coord new_end; enum selection_direction direction = SELECTION_UNDIR; - if (row <= top_left.row || - abs(row - top_left.row) < abs(row - bottom_left.row)) + if (rel_row <= rel_top_row || + abs(rel_row - rel_top_row) < abs(rel_row - rel_bottom_row)) { /* Move one of the top corners */ From fa2d9f86996467ba33cc381f810ea966a4323381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 28 Jul 2022 18:45:25 +0200 Subject: [PATCH 0099/1323] selection: rework how we update a selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this patch, each selection update would result in grid covered by the selection being walked *three* times. First to “premark” the area that *will* be selected after the update, then again to unmark the previous selection (excluding the cells that were premarked - but the cells are still iterated), and then one more time to finalize the selection state in the grid. Furthermore, each time a frame is rendered, the entire selection were iterated again, to ensure all the cells have their ‘selected’ bit set. This quickly gets *very* slow. This patch takes a completely different approach. Instead of looking at the selection as a range of cells to iterate, we view it as an area, or region. Thus, on each update, we have to regions: the region representing the previous selection, and the region representing the to-be selection. By diffing these two regions, we get two new regions: one that represents the cells that were selected, but aren’t any more, and one that represents the cells that previously were not selected, but now will be. We implement the regions using pixman regions. By subtracting the current selection from the previous selection, we get the region representing the cells that are no longer selected, and that should be unmarked. By subtracting the previous selection from the current, we get the region representing the cells that was added to the selection in this update, and that should be marked. selection_dirty_cells() is rewritten in a similar manner. We create pixman regions for the selection, and the current scrollback view. The intersection represents the (selected) cells that are visible. These need to iterated and marked as being selected. Closes #1114 --- CHANGELOG.md | 2 + selection.c | 304 ++++++++++++++++++++++++++++++++------------------- 2 files changed, 196 insertions(+), 110 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc84a398..6e0b3052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,12 +98,14 @@ highlighted correctly. * Selection not being cancelled correctly when scrolled out. * Extending a multi-page selection behaving inconsistently. +* Poor performance when making very large selections ([#1114][1114]). [1055]: https://codeberg.org/dnkl/foot/issues/1055 [1092]: https://codeberg.org/dnkl/foot/issues/1092 [1097]: https://codeberg.org/dnkl/foot/issues/1097 [1111]: https://codeberg.org/dnkl/foot/issues/1111 [1120]: https://codeberg.org/dnkl/foot/issues/1120 +[1114]: https://codeberg.org/dnkl/foot/issues/1114 ### Security diff --git a/selection.c b/selection.c index 54faf49b..8c3a8357 100644 --- a/selection.c +++ b/selection.c @@ -9,6 +9,8 @@ #include #include +#include + #define LOG_MODULE "selection" #define LOG_ENABLE_DBG 0 #include "log.h" @@ -609,111 +611,150 @@ selection_start(struct terminal *term, int col, int row, } -/* Context used while (un)marking selected cells, to be able to - * exclude empty cells */ -struct mark_context { - const struct row *last_row; - int empty_count; - uint8_t **keep_selection; -}; - -static bool -unmark_selected(struct terminal *term, struct row *row, struct cell *cell, - int row_no, int col, void *data) +static pixman_region32_t +pixman_region_for_coords_normal(const struct terminal *term, + const struct coord *start, + const struct coord *end) { - if (!cell->attrs.selected) - return true; + pixman_region32_t region; + pixman_region32_init(®ion); - struct mark_context *ctx = data; - const uint8_t *keep_selection = - ctx->keep_selection != NULL ? ctx->keep_selection[row_no] : NULL; + const int rel_start_row = + grid_row_abs_to_sb(term->grid, term->rows, start->row); + const int rel_end_row = + grid_row_abs_to_sb(term->grid, term->rows, end->row); - if (keep_selection != NULL) { - unsigned idx = (unsigned)col / 8; - unsigned ofs = (unsigned)col % 8; + if (rel_start_row < rel_end_row) { + /* First partial row (start ->)*/ + pixman_region32_union_rect( + ®ion, ®ion, + start->col, rel_start_row, + term->cols - start->col, 1); - if (keep_selection[idx] & (1 << ofs)) { - /* We’re updating the selection, and this cell is still - * going to be selected */ - return true; + /* Full rows between start and end */ + if (rel_start_row + 1 < rel_end_row) { + pixman_region32_union_rect( + ®ion, ®ion, + 0, rel_start_row + 1, + term->cols, rel_end_row - rel_start_row - 1); } + + /* Last partial row (-> end) */ + pixman_region32_union_rect( + ®ion, ®ion, + 0, rel_end_row, + end->col + 1, 1); + + } else if (rel_start_row > rel_end_row) { + /* First partial row (end ->) */ + pixman_region32_union_rect( + ®ion, ®ion, + end->col, rel_end_row, + term->cols - end->col, 1); + + /* Full rows between end and start */ + if (rel_end_row + 1 < rel_start_row) { + pixman_region32_union_rect( + ®ion, ®ion, + 0, rel_end_row + 1, + term->cols, rel_start_row - rel_end_row - 1); + } + + /* Last partial row (-> start) */ + pixman_region32_union_rect( + ®ion, ®ion, + 0, rel_start_row, + start->col + 1, 1); + } else { + const int start_col = min(start->col, end->col); + const int end_col = max(start->col, end->col); + + pixman_region32_union_rect( + ®ion, ®ion, + start_col, rel_start_row, + end_col + 1 - start_col, 1); } - row->dirty = true; - cell->attrs.selected = false; - cell->attrs.clean = false; - return true; + return region; } -static bool -premark_selected(struct terminal *term, struct row *row, struct cell *cell, - int row_no, int col, void *data) +static pixman_region32_t +pixman_region_for_coords_block(const struct terminal *term, + const struct coord *start, const struct coord *end) { - struct mark_context *ctx = data; - xassert(ctx != NULL); + pixman_region32_t region; + pixman_region32_init(®ion); - if (ctx->last_row != row) { - ctx->last_row = row; - ctx->empty_count = 0; - } + const int rel_start_row = + grid_row_abs_to_sb(term->grid, term->rows, start->row); + const int rel_end_row = + grid_row_abs_to_sb(term->grid, term->rows, end->row); - if (cell->wc == 0 && term->selection.kind != SELECTION_BLOCK) { - ctx->empty_count++; - return true; - } + pixman_region32_union_rect( + ®ion, ®ion, + min(start->col, end->col), min(rel_start_row, rel_end_row), + abs(start->col - end->col) + 1, abs(rel_start_row - rel_end_row) + 1); - uint8_t *keep_selection = ctx->keep_selection[row_no]; - if (keep_selection == NULL) { - keep_selection = xcalloc((term->grid->num_cols + 7) / 8, sizeof(keep_selection[0])); - ctx->keep_selection[row_no] = keep_selection; - } - - /* Tell unmark to leave this be */ - for (int i = 0; i < ctx->empty_count + 1; i++) { - unsigned idx = (unsigned)(col - i) / 8; - unsigned ofs = (unsigned)(col - i) % 8; - keep_selection[idx] |= 1 << ofs; - } - - ctx->empty_count = 0; - return true; + return region; } -static bool -mark_selected(struct terminal *term, struct row *row, struct cell *cell, - int row_no, int col, void *data) +/* Returns a pixman region representing the selection between ‘start’ + * and ‘end’ (given the current selection kind), in *scrollback + * relative coordinates* */ +static pixman_region32_t +pixman_region_for_coords(const struct terminal *term, + const struct coord *start, const struct coord *end) { - struct mark_context *ctx = data; - xassert(ctx != NULL); - - if (ctx->last_row != row) { - ctx->last_row = row; - ctx->empty_count = 0; + switch (term->selection.kind) { + default: return pixman_region_for_coords_normal(term, start, end); + case SELECTION_BLOCK: return pixman_region_for_coords_block(term, start, end); } - - if (cell->wc == 0 && term->selection.kind != SELECTION_BLOCK) { - ctx->empty_count++; - return true; - } - - for (int i = 0; i < ctx->empty_count + 1; i++) { - struct cell *c = &row->cells[col - i]; - if (!c->attrs.selected) { - row->dirty = true; - c->attrs.selected = true; - c->attrs.clean = false; - } - } - - ctx->empty_count = 0; - return true; } static void -reset_modify_context(struct mark_context *ctx) +mark_selected_region(struct terminal *term, pixman_box32_t *boxes, + size_t count, bool selected, bool dirty_cells) { - ctx->last_row = NULL; - ctx->empty_count = 0; + for (size_t i = 0; i < count; i++) { + const pixman_box32_t *box = &boxes[i]; + + LOG_DBG("%s selection in region: %dx%d - %dx%d", + selected ? "marking" : "unmarking", + box->x1, box->y1, + box->x2, box->y2); + + int abs_row_start = grid_row_sb_to_abs( + term->grid, term->rows, box->y1); + + for (int r = abs_row_start, rel_r = box->y1; + rel_r < box->y2; + r = (r + 1) & (term->grid->num_rows - 1), rel_r++) + { + struct row *row = term->grid->rows[r]; + xassert(row != NULL); + + if (dirty_cells) + row->dirty = true; + + for (int c = box->x1, empty_count = 0; c < box->x2; c++) { + if (selected && row->cells[c].wc == 0) { + empty_count++; + continue; + } + + for (int j = 0; j < empty_count + 1; j++) { + xassert(c - j >= 0); + struct cell *cell = &row->cells[c - j]; + + if (dirty_cells) + cell->attrs.clean = false; + cell->attrs.selected = selected; + } + + empty_count = 0; + } + } + } } static void @@ -723,33 +764,46 @@ selection_modify(struct terminal *term, struct coord start, struct coord end) xassert(start.row != -1 && start.col != -1); xassert(end.row != -1 && end.col != -1); - uint8_t **keep_selection = - xcalloc(term->grid->num_rows, sizeof(keep_selection[0])); - - struct mark_context ctx = {.keep_selection = keep_selection}; - - /* Premark all cells that *will* be selected */ - foreach_selected(term, start, end, &premark_selected, &ctx); - reset_modify_context(&ctx); - + pixman_region32_t previous_selection; if (term->selection.coords.end.row >= 0) { - /* Unmark previous selection, ignoring cells that are part of - * the new selection */ - foreach_selected(term, term->selection.coords.start, term->selection.coords.end, - &unmark_selected, &ctx); - reset_modify_context(&ctx); - } + previous_selection = pixman_region_for_coords( + term, + &term->selection.coords.start, + &term->selection.coords.end); + } else + pixman_region32_init(&previous_selection); + + pixman_region32_t current_selection = pixman_region_for_coords( + term, &start, &end); + + pixman_region32_t no_longer_selected; + pixman_region32_init(&no_longer_selected); + pixman_region32_subtract( + &no_longer_selected, &previous_selection, ¤t_selection); + + pixman_region32_t newly_selected; + pixman_region32_init(&newly_selected); + pixman_region32_subtract( + &newly_selected, ¤t_selection, &previous_selection); + + /* Clear selection in cells no longer selected */ + int n_rects = -1; + pixman_box32_t *boxes = NULL; + + boxes = pixman_region32_rectangles(&no_longer_selected, &n_rects); + mark_selected_region(term, boxes, n_rects, false, true); + + boxes = pixman_region32_rectangles(&newly_selected, &n_rects); + mark_selected_region(term, boxes, n_rects, true, true); + + pixman_region32_fini(&newly_selected); + pixman_region32_fini(&no_longer_selected); + pixman_region32_fini(¤t_selection); + pixman_region32_fini(&previous_selection); term->selection.coords.start = start; term->selection.coords.end = end; - - /* Mark new selection */ - foreach_selected(term, start, end, &mark_selected, &ctx); render_refresh(term); - - for (size_t i = 0; i < term->grid->num_rows; i++) - free(keep_selection[i]); - free(keep_selection); } static void @@ -990,9 +1044,26 @@ selection_dirty_cells(struct terminal *term) if (term->selection.coords.start.row < 0 || term->selection.coords.end.row < 0) return; - foreach_selected( - term, term->selection.coords.start, term->selection.coords.end, &mark_selected, - &(struct mark_context){0}); + pixman_region32_t selection = pixman_region_for_coords( + term, &term->selection.coords.start, &term->selection.coords.end); + + pixman_region32_t view = pixman_region_for_coords( + term, + &(struct coord){0, term->grid->view}, + &(struct coord){term->cols - 1, term->grid->view + term->rows - 1}); + + pixman_region32_t visible_and_selected; + pixman_region32_init(&visible_and_selected); + pixman_region32_intersect(&visible_and_selected, &selection, &view); + + int n_rects = -1; + pixman_box32_t *boxes = + pixman_region32_rectangles(&visible_and_selected, &n_rects); + mark_selected_region(term, boxes, n_rects, true, false); + + pixman_region32_fini(&visible_and_selected); + pixman_region32_fini(&view); + pixman_region32_fini(&selection); } static void @@ -1270,6 +1341,19 @@ selection_finalize(struct seat *seat, struct terminal *term, uint32_t serial) } } +static bool +unmark_selected(struct terminal *term, struct row *row, struct cell *cell, + int row_no, int col, void *data) +{ + if (!cell->attrs.selected) + return true; + + row->dirty = true; + cell->attrs.selected = false; + cell->attrs.clean = false; + return true; +} + void selection_cancel(struct terminal *term) { @@ -1282,7 +1366,7 @@ selection_cancel(struct terminal *term) if (term->selection.coords.start.row >= 0 && term->selection.coords.end.row >= 0) { foreach_selected( term, term->selection.coords.start, term->selection.coords.end, - &unmark_selected, &(struct mark_context){0}); + &unmark_selected, NULL); render_refresh(term); } From 8967dd9cfe51276d63ef8ff64e8fc76f80a6fa3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 28 Jul 2022 18:09:16 +0200 Subject: [PATCH 0100/1323] input: add new Unicode input mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This mode is activated through the new key-bindings.unicode-input and search-bindings.unicode-input key bindings. When active, the user can “build” a Unicode codepoint by typing its hexadecimal value. Note that there’s no visual feedback in this mode. This is intentional. This mode is intended to be a fallback for users that don’t use an IME. Closes #1116 --- CHANGELOG.md | 5 ++++ config.c | 2 ++ doc/foot.ini.5.scd | 30 ++++++++++++++++++++ foot.ini | 2 ++ input.c | 69 +++++++++++++++++++++++++++++++++++++++++++++- key-binding.h | 4 ++- meson.build | 1 + search.c | 5 ++++ unicode-mode.c | 38 +++++++++++++++++++++++++ unicode-mode.h | 7 +++++ wayland.h | 6 ++++ 11 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 unicode-mode.c create mode 100644 unicode-mode.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e0b3052..8e3cd789 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,11 +52,16 @@ `ctrl`+`shift`+`x` respectively ([#30][30]). * `colors.search-box-no-match` and `colors.search-box-match` options to `foot.ini` ([#1112][1112]). +* Very basic Unicode input mode via the new + `key-bindings.unicode-input` and `search-bindings.unicode-input` key + bindings. Note that there is no visual feedback, as the preferred + way of entering Unicode characters is with an IME ([#1116][1116]). [1058]: https://codeberg.org/dnkl/foot/issues/1058 [1070]: https://codeberg.org/dnkl/foot/issues/1070 [30]: https://codeberg.org/dnkl/foot/issues/30 [1112]: https://codeberg.org/dnkl/foot/issues/1112 +[1116]: https://codeberg.org/dnkl/foot/issues/1116 ### Changed diff --git a/config.c b/config.c index 0e509995..7a746aea 100644 --- a/config.c +++ b/config.c @@ -117,6 +117,7 @@ static const char *const binding_action_map[] = { [BIND_ACTION_TEXT_BINDING] = "text-binding", [BIND_ACTION_PROMPT_PREV] = "prompt-prev", [BIND_ACTION_PROMPT_NEXT] = "prompt-next", + [BIND_ACTION_UNICODE_INPUT] = "unicode-input", /* Mouse-specific actions */ [BIND_ACTION_SELECT_BEGIN] = "select-begin", @@ -148,6 +149,7 @@ static const char *const search_binding_action_map[] = { [BIND_ACTION_SEARCH_EXTEND_WORD_WS] = "extend-to-next-whitespace", [BIND_ACTION_SEARCH_CLIPBOARD_PASTE] = "clipboard-paste", [BIND_ACTION_SEARCH_PRIMARY_PASTE] = "primary-paste", + [BIND_ACTION_SEARCH_UNICODE_INPUT] = "unicode-input", }; static const char *const url_binding_action_map[] = { diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 2e62a41f..d3881274 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -791,6 +791,32 @@ e.g. *search-start=none*. Jump the next prompt (requires shell integration, see *foot*(1)). Default: _Control+Shift+x_. +*unicode-input* + Input a Unicode character by typing its codepoint in hexadecimal, + followed by *Enter* or *Space*. + + For example, to input the character _ö_ (LATIN SMALL LETTER O WITH + DIAERESIS, Unicode codepoint 0xf6), you would first activate this + key binding, then type: *f*, *6*, *Enter*. + + Another example: to input 😍 (SMILING FACE WITH HEART-SHAPED EYES, + Unicode codepoint 0x1f60d), activate this key binding, then type: + *1*, *f*, *6*, *0*, *d*, *Enter*. + + Recognized key bindings in Unicode input mode: + + - Enter, Space: commit the Unicode character, then exit this mode. + - Escape, Ctrl+c, Ctrl+d, Ctrl+g: abort input, then exit this mode. + - 0-9, a-f: append next digit to the Unicode's codepoint. + - Backspace: undo the last digit. + + Note that there is no visual feedback while in this mode. This is + by design; foot's Unicode input mode is considered to be a + fallback. The preferred way of entering Unicode characters, emojis + etc is by using an IME. + + Default: _none_. + # SECTION: search-bindings This section lets you override the default key bindings used in @@ -869,6 +895,10 @@ scrollback search mode. The syntax is exactly the same as the regular Paste from the _primary selection_ into the search buffer. Default: _Shift+Insert_. +*unicode-input* + Unicode input mode. See _key-bindings.unicode-input_ for + details. Default: _none_. + # SECTION: url-bindings This section lets you override the default key bindings used in URL diff --git a/foot.ini b/foot.ini index e8ff1870..4c857296 100644 --- a/foot.ini +++ b/foot.ini @@ -150,6 +150,7 @@ # show-urls-persistent=none # prompt-prev=Control+Shift+z # prompt-next=Control+Shift+x +# unicode-input=none # noop=none [search-bindings] @@ -171,6 +172,7 @@ # extend-to-next-whitespace=Control+Shift+w # clipboard-paste=Control+v Control+Shift+v Control+y XF86Paste # primary-paste=Shift+Insert +# unicode-input=none [url-bindings] # cancel=Control+g Control+c Control+d Escape diff --git a/input.c b/input.c index e0f21c6e..8b47b983 100644 --- a/input.c +++ b/input.c @@ -36,6 +36,7 @@ #include "spawn.h" #include "terminal.h" #include "tokenize.h" +#include "unicode-mode.h" #include "url-mode.h" #include "util.h" #include "vt.h" @@ -416,6 +417,10 @@ execute_binding(struct seat *seat, struct terminal *term, return true; } + case BIND_ACTION_UNICODE_INPUT: + unicode_mode_activate(seat); + return true; + case BIND_ACTION_SELECT_BEGIN: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_CHAR_WISE, false); @@ -1405,7 +1410,69 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, xassert(bindings != NULL); if (pressed) { - if (term->is_searching) { + if (seat->unicode_mode.active) { + if (sym == XKB_KEY_Return || + sym == XKB_KEY_space || + sym == XKB_KEY_KP_Enter || + sym == XKB_KEY_KP_Space) + { + char utf8[MB_CUR_MAX]; + size_t chars = c32rtomb( + utf8, seat->unicode_mode.character, &(mbstate_t){0}); + + LOG_DBG("Unicode input: 0x%06x -> %.*s", + seat->unicode_mode.character, (int)chars, utf8); + + if (chars != (size_t)-1) { + if (term->is_searching) + search_add_chars(term, utf8, chars); + else + term_to_slave(term, utf8, chars); + } + + unicode_mode_deactivate(seat); + } + + else if (sym == XKB_KEY_Escape || + (seat->kbd.ctrl && (sym == XKB_KEY_c || + sym == XKB_KEY_d || + sym == XKB_KEY_g))) + { + unicode_mode_deactivate(seat); + } + + else if (sym == XKB_KEY_BackSpace) { + if (seat->unicode_mode.count > 0) { + seat->unicode_mode.character >>= 4; + seat->unicode_mode.count--; + unicode_mode_updated(seat); + } + } + + else if (seat->unicode_mode.count < 6) { + int digit = -1; + + /* 0-9, a-f, A-F */ + if (sym >= XKB_KEY_0 && sym <= XKB_KEY_9) + digit = sym - XKB_KEY_0; + else if (sym >= XKB_KEY_a && sym <= XKB_KEY_f) + digit = 0xa + (sym - XKB_KEY_a); + else if (sym >= XKB_KEY_A && sym <= XKB_KEY_F) + digit = 0xa + (sym - XKB_KEY_A); + + if (digit >= 0) { + xassert(digit >= 0 && digit <= 0xf); + seat->unicode_mode.character <<= 4; + seat->unicode_mode.character |= digit; + seat->unicode_mode.count++; + unicode_mode_updated(seat); + } + } + + return; + } + + else if (term->is_searching) { if (should_repeat) start_repeater(seat, key); diff --git a/key-binding.h b/key-binding.h index 1c0e2a99..448500c1 100644 --- a/key-binding.h +++ b/key-binding.h @@ -38,6 +38,7 @@ enum bind_action_normal { BIND_ACTION_TEXT_BINDING, BIND_ACTION_PROMPT_PREV, BIND_ACTION_PROMPT_NEXT, + BIND_ACTION_UNICODE_INPUT, /* Mouse specific actions - i.e. they require a mouse coordinate */ BIND_ACTION_SELECT_BEGIN, @@ -48,7 +49,7 @@ enum bind_action_normal { BIND_ACTION_SELECT_WORD_WS, BIND_ACTION_SELECT_ROW, - BIND_ACTION_KEY_COUNT = BIND_ACTION_PROMPT_NEXT + 1, + BIND_ACTION_KEY_COUNT = BIND_ACTION_UNICODE_INPUT + 1, BIND_ACTION_COUNT = BIND_ACTION_SELECT_ROW + 1, }; @@ -72,6 +73,7 @@ enum bind_action_search { BIND_ACTION_SEARCH_EXTEND_WORD_WS, BIND_ACTION_SEARCH_CLIPBOARD_PASTE, BIND_ACTION_SEARCH_PRIMARY_PASTE, + BIND_ACTION_SEARCH_UNICODE_INPUT, BIND_ACTION_SEARCH_COUNT, }; diff --git a/meson.build b/meson.build index 6dc4d098..676d550a 100644 --- a/meson.build +++ b/meson.build @@ -222,6 +222,7 @@ executable( 'slave.c', 'slave.h', 'spawn.c', 'spawn.h', 'tokenize.c', 'tokenize.h', + 'unicode-mode.c', 'unicode-mode.h', 'url-mode.c', 'url-mode.h', 'user-notification.c', 'user-notification.h', 'wayland.c', 'wayland.h', 'shm-formats.h', diff --git a/search.c b/search.c index 88bc88aa..59765c2e 100644 --- a/search.c +++ b/search.c @@ -18,6 +18,7 @@ #include "render.h" #include "selection.h" #include "shm.h" +#include "unicode-mode.h" #include "util.h" #include "xmalloc.h" @@ -993,6 +994,10 @@ execute_binding(struct seat *seat, struct terminal *term, *update_search_result = *redraw = true; return true; + case BIND_ACTION_SEARCH_UNICODE_INPUT: + unicode_mode_activate(seat); + return true; + case BIND_ACTION_SEARCH_COUNT: BUG("Invalid action type"); return true; diff --git a/unicode-mode.c b/unicode-mode.c new file mode 100644 index 00000000..0da86add --- /dev/null +++ b/unicode-mode.c @@ -0,0 +1,38 @@ +#include "unicode-mode.h" + +#include "render.h" + +void +unicode_mode_activate(struct seat *seat) +{ + if (seat->unicode_mode.active) + return; + + seat->unicode_mode.active = true; + seat->unicode_mode.character = u'\0'; + seat->unicode_mode.count = 0; + unicode_mode_updated(seat); +} + +void +unicode_mode_deactivate(struct seat *seat) +{ + if (!seat->unicode_mode.active) + return; + + seat->unicode_mode.active = false; + unicode_mode_updated(seat); +} + +void +unicode_mode_updated(struct seat *seat) +{ + struct terminal *term = seat->kbd_focus; + if (term == NULL) + return; + + if (term->is_searching) + render_refresh_search(term); + else + render_refresh(term); +} diff --git a/unicode-mode.h b/unicode-mode.h new file mode 100644 index 00000000..eadbe06a --- /dev/null +++ b/unicode-mode.h @@ -0,0 +1,7 @@ +#pragma once + +#include "wayland.h" + +void unicode_mode_activate(struct seat *seat); +void unicode_mode_deactivate(struct seat *seat); +void unicode_mode_updated(struct seat *seat); diff --git a/wayland.h b/wayland.h index 729a225b..803fe390 100644 --- a/wayland.h +++ b/wayland.h @@ -206,6 +206,12 @@ struct seat { uint32_t serial; } ime; #endif + + struct { + bool active; + int count; + char32_t character; + } unicode_mode; }; enum csd_surface { From 001f96c4e3ae26a9b2807b549c782a094a36ff3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 28 Jul 2022 19:34:13 +0200 Subject: [PATCH 0101/1323] unicode-input: move input (key press) handling to unicode_mode_input() --- input.c | 59 +------------------------------------------- unicode-mode.c | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++ unicode-mode.h | 4 +++ 3 files changed, 72 insertions(+), 58 deletions(-) diff --git a/input.c b/input.c index 8b47b983..847ff6af 100644 --- a/input.c +++ b/input.c @@ -1411,64 +1411,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, if (pressed) { if (seat->unicode_mode.active) { - if (sym == XKB_KEY_Return || - sym == XKB_KEY_space || - sym == XKB_KEY_KP_Enter || - sym == XKB_KEY_KP_Space) - { - char utf8[MB_CUR_MAX]; - size_t chars = c32rtomb( - utf8, seat->unicode_mode.character, &(mbstate_t){0}); - - LOG_DBG("Unicode input: 0x%06x -> %.*s", - seat->unicode_mode.character, (int)chars, utf8); - - if (chars != (size_t)-1) { - if (term->is_searching) - search_add_chars(term, utf8, chars); - else - term_to_slave(term, utf8, chars); - } - - unicode_mode_deactivate(seat); - } - - else if (sym == XKB_KEY_Escape || - (seat->kbd.ctrl && (sym == XKB_KEY_c || - sym == XKB_KEY_d || - sym == XKB_KEY_g))) - { - unicode_mode_deactivate(seat); - } - - else if (sym == XKB_KEY_BackSpace) { - if (seat->unicode_mode.count > 0) { - seat->unicode_mode.character >>= 4; - seat->unicode_mode.count--; - unicode_mode_updated(seat); - } - } - - else if (seat->unicode_mode.count < 6) { - int digit = -1; - - /* 0-9, a-f, A-F */ - if (sym >= XKB_KEY_0 && sym <= XKB_KEY_9) - digit = sym - XKB_KEY_0; - else if (sym >= XKB_KEY_a && sym <= XKB_KEY_f) - digit = 0xa + (sym - XKB_KEY_a); - else if (sym >= XKB_KEY_A && sym <= XKB_KEY_F) - digit = 0xa + (sym - XKB_KEY_A); - - if (digit >= 0) { - xassert(digit >= 0 && digit <= 0xf); - seat->unicode_mode.character <<= 4; - seat->unicode_mode.character |= digit; - seat->unicode_mode.count++; - unicode_mode_updated(seat); - } - } - + unicode_mode_input(seat, term, sym); return; } diff --git a/unicode-mode.c b/unicode-mode.c index 0da86add..6b4b6050 100644 --- a/unicode-mode.c +++ b/unicode-mode.c @@ -1,6 +1,10 @@ #include "unicode-mode.h" +#define LOG_MODULE "unicode-input" +#define LOG_ENABLE_DBG 0 +#include "log.h" #include "render.h" +#include "search.h" void unicode_mode_activate(struct seat *seat) @@ -36,3 +40,66 @@ unicode_mode_updated(struct seat *seat) else render_refresh(term); } + +void +unicode_mode_input(struct seat *seat, struct terminal *term, + xkb_keysym_t sym) +{ + if (sym == XKB_KEY_Return || + sym == XKB_KEY_space || + sym == XKB_KEY_KP_Enter || + sym == XKB_KEY_KP_Space) + { + char utf8[MB_CUR_MAX]; + size_t chars = c32rtomb( + utf8, seat->unicode_mode.character, &(mbstate_t){0}); + + LOG_DBG("Unicode input: 0x%06x -> %.*s", + seat->unicode_mode.character, (int)chars, utf8); + + if (chars != (size_t)-1) { + if (term->is_searching) + search_add_chars(term, utf8, chars); + else + term_to_slave(term, utf8, chars); + } + + unicode_mode_deactivate(seat); + } + + else if (sym == XKB_KEY_Escape || + (seat->kbd.ctrl && (sym == XKB_KEY_c || + sym == XKB_KEY_d || + sym == XKB_KEY_g))) + { + unicode_mode_deactivate(seat); + } + + else if (sym == XKB_KEY_BackSpace) { + if (seat->unicode_mode.count > 0) { + seat->unicode_mode.character >>= 4; + seat->unicode_mode.count--; + unicode_mode_updated(seat); + } + } + + else if (seat->unicode_mode.count < 6) { + int digit = -1; + + /* 0-9, a-f, A-F */ + if (sym >= XKB_KEY_0 && sym <= XKB_KEY_9) + digit = sym - XKB_KEY_0; + else if (sym >= XKB_KEY_a && sym <= XKB_KEY_f) + digit = 0xa + (sym - XKB_KEY_a); + else if (sym >= XKB_KEY_A && sym <= XKB_KEY_F) + digit = 0xa + (sym - XKB_KEY_A); + + if (digit >= 0) { + xassert(digit >= 0 && digit <= 0xf); + seat->unicode_mode.character <<= 4; + seat->unicode_mode.character |= digit; + seat->unicode_mode.count++; + unicode_mode_updated(seat); + } + } +} diff --git a/unicode-mode.h b/unicode-mode.h index eadbe06a..e7c75b9b 100644 --- a/unicode-mode.h +++ b/unicode-mode.h @@ -1,7 +1,11 @@ #pragma once +#include + #include "wayland.h" void unicode_mode_activate(struct seat *seat); void unicode_mode_deactivate(struct seat *seat); void unicode_mode_updated(struct seat *seat); +void unicode_mode_input(struct seat *seat, struct terminal *term, + xkb_keysym_t sym); From 0cbd99710b9d9ff5116c39e1d989556a6d7c27a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 29 Jul 2022 11:56:41 +0200 Subject: [PATCH 0102/1323] =?UTF-8?q?unicode-mode:=20=E2=80=98q=E2=80=99?= =?UTF-8?q?=20aborts=20Unicode=20input=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/foot.ini.5.scd | 2 +- unicode-mode.c | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index d3881274..995a7e33 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -806,7 +806,7 @@ e.g. *search-start=none*. Recognized key bindings in Unicode input mode: - Enter, Space: commit the Unicode character, then exit this mode. - - Escape, Ctrl+c, Ctrl+d, Ctrl+g: abort input, then exit this mode. + - Escape, q, Ctrl+c, Ctrl+d, Ctrl+g: abort input, then exit this mode. - 0-9, a-f: append next digit to the Unicode's codepoint. - Backspace: undo the last digit. diff --git a/unicode-mode.c b/unicode-mode.c index 6b4b6050..a69601ec 100644 --- a/unicode-mode.c +++ b/unicode-mode.c @@ -68,6 +68,7 @@ unicode_mode_input(struct seat *seat, struct terminal *term, } else if (sym == XKB_KEY_Escape || + sym == XKB_KEY_q || (seat->kbd.ctrl && (sym == XKB_KEY_c || sym == XKB_KEY_d || sym == XKB_KEY_g))) From c0a7c7bf0d4412fd9436962704cb26f9596e529f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 29 Jul 2022 21:27:27 +0200 Subject: [PATCH 0103/1323] config: reset errno before calling getline() again Related to #1107 --- config.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config.c b/config.c index 7a746aea..fbeab3e6 100644 --- a/config.c +++ b/config.c @@ -2639,6 +2639,9 @@ parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_ar if (!section_parser(ctx)) error_or_continue(); + + /* For next iteration of getline() */ + errno = 0; } if (errno != 0) { From ffdac61e2a95aa31a9c69eb5152a7dd79346ddf0 Mon Sep 17 00:00:00 2001 From: Max Gautier Date: Fri, 29 Jul 2022 20:12:47 +0200 Subject: [PATCH 0104/1323] server: Use "normal" socket activation, not inetd Systemd, when doing socket activation, pass file descriptors in a non-stable order when there is multiples ones. But we only use one, so we don't need to identify it, and the file descriptors always start at 3. So use 3 for the systemd service. Source : sd_listen_fds (systemd man pages) We also need to unset variables systemd pass to socket activated process, since we don't need them and sub-process (footclient and theirs forks) could be confused by those. Closes #1107 --- CHANGELOG.md | 3 +++ foot-server@.service.in | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e3cd789..6823e16b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,6 +104,8 @@ * Selection not being cancelled correctly when scrolled out. * Extending a multi-page selection behaving inconsistently. * Poor performance when making very large selections ([#1114][1114]). +* Bogus error message when using systemd socket activation for server + mode ([#1107][1107]) [1055]: https://codeberg.org/dnkl/foot/issues/1055 [1092]: https://codeberg.org/dnkl/foot/issues/1092 @@ -111,6 +113,7 @@ [1111]: https://codeberg.org/dnkl/foot/issues/1111 [1120]: https://codeberg.org/dnkl/foot/issues/1120 [1114]: https://codeberg.org/dnkl/foot/issues/1114 +[1107]: https://codeberg.org/dnkl/foot/issues/1107 ### Security diff --git a/foot-server@.service.in b/foot-server@.service.in index 81c13bb4..c40bb454 100644 --- a/foot-server@.service.in +++ b/foot-server@.service.in @@ -1,8 +1,8 @@ [Service] -ExecStart=@bindir@/foot --server=0 +ExecStart=@bindir@/foot --server=3 Environment=WAYLAND_DISPLAY=%i +UnsetEnvironment=LISTEN_PID LISTEN_FDS LISTEN_FDNAMES NonBlocking=true -StandardInput=socket [Unit] Requires=%N.socket From 2f68b421bfc444c2a713d7418850b67a7e34810f Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Fri, 6 May 2022 20:03:30 +0200 Subject: [PATCH 0105/1323] Add no-op xdg_toplevel.configure_bounds handler Next commit uses v5. --- wayland.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/wayland.c b/wayland.c index 850b657b..2d7c303f 100644 --- a/wayland.c +++ b/wayland.c @@ -706,9 +706,18 @@ xdg_toplevel_close(void *data, struct xdg_toplevel *xdg_toplevel) term_shutdown(term); } +static void +xdg_toplevel_configure_bounds(void *data, + struct xdg_toplevel *xdg_toplevel, + int32_t width, int32_t height) +{ + /* TODO: ensure we don't pick a bigger size */ +} + static const struct xdg_toplevel_listener xdg_toplevel_listener = { .configure = &xdg_toplevel_configure, /*.close = */&xdg_toplevel_close, /* epoll-shim defines a macro ‘close’... */ + .configure_bounds = &xdg_toplevel_configure_bounds, }; static void @@ -902,7 +911,7 @@ handle_global(void *data, struct wl_registry *registry, */ wayl->shell = wl_registry_bind( - wayl->registry, name, &xdg_wm_base_interface, min(version, 2)); + wayl->registry, name, &xdg_wm_base_interface, min(version, 4)); xdg_wm_base_add_listener(wayl->shell, &xdg_wm_base_listener, wayl); } From 129e1a9b8e23007026d19c970941ccbc8ee2f0ca Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Fri, 6 May 2022 20:05:04 +0200 Subject: [PATCH 0106/1323] Add support for xdg_toplevel.wm_capabilities See https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/122 --- CHANGELOG.md | 3 +++ render.c | 6 ++++-- wayland.c | 46 +++++++++++++++++++++++++++++++++++++++++++--- wayland.h | 5 +++++ 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6823e16b..b82cfa00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,12 +56,15 @@ `key-bindings.unicode-input` and `search-bindings.unicode-input` key bindings. Note that there is no visual feedback, as the preferred way of entering Unicode characters is with an IME ([#1116][1116]). +* Support for `xdg_toplevel.wm_capabilities`, to adapt the client-side + decoration buttons to the compositor capabilities ([#1061][1061]). [1058]: https://codeberg.org/dnkl/foot/issues/1058 [1070]: https://codeberg.org/dnkl/foot/issues/1070 [30]: https://codeberg.org/dnkl/foot/issues/30 [1112]: https://codeberg.org/dnkl/foot/issues/1112 [1116]: https://codeberg.org/dnkl/foot/issues/1116 +[1061]: https://codeberg.org/dnkl/foot/pulls/1061 ### Changed diff --git a/render.c b/render.c index 9333deb7..ff82802e 100644 --- a/render.c +++ b/render.c @@ -1726,10 +1726,12 @@ get_csd_data(const struct terminal *term, enum csd_surface surf_idx) const int button_close_width = term->width >= 1 * button_width ? button_width : 0; - const int button_maximize_width = term->width >= 2 * button_width + const int button_maximize_width = + term->width >= 2 * button_width && term->window->wm_capabilities.maximize ? button_width : 0; - const int button_minimize_width = term->width >= 3 * button_width + const int button_minimize_width = + term->width >= 3 * button_width && term->window->wm_capabilities.minimize ? button_width : 0; switch (surf_idx) { diff --git a/wayland.c b/wayland.c index 2d7c303f..fb41cf7d 100644 --- a/wayland.c +++ b/wayland.c @@ -714,10 +714,38 @@ xdg_toplevel_configure_bounds(void *data, /* TODO: ensure we don't pick a bigger size */ } +#if defined(XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION) +static void +xdg_toplevel_wm_capabilities(void *data, + struct xdg_toplevel *xdg_toplevel, + struct wl_array *caps) +{ + struct wl_window *win = data; + + win->wm_capabilities.maximize = false; + win->wm_capabilities.minimize = false; + + uint32_t *cap_ptr; + wl_array_for_each(cap_ptr, caps) { + switch (*cap_ptr) { + case XDG_TOPLEVEL_WM_CAPABILITIES_MAXIMIZE: + win->wm_capabilities.maximize = true; + break; + case XDG_TOPLEVEL_WM_CAPABILITIES_MINIMIZE: + win->wm_capabilities.minimize = true; + break; + } + } +} +#endif + static const struct xdg_toplevel_listener xdg_toplevel_listener = { .configure = &xdg_toplevel_configure, /*.close = */&xdg_toplevel_close, /* epoll-shim defines a macro ‘close’... */ .configure_bounds = &xdg_toplevel_configure_bounds, +#if defined(XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION) + .wm_capabilities = xdg_toplevel_wm_capabilities, +#endif }; static void @@ -905,13 +933,22 @@ handle_global(void *data, struct wl_registry *registry, return; /* - * We *require* version 1, but _can_ use version 2. Version 2 + * We *require* version 1, but _can_ use version 5. Version 2 * adds 'tiled' window states. We use that information to - * restore the window size when window is un-tiled. + * restore the window size when window is un-tiled. Version 5 + * adds 'wm_capabilities'. We use that information to draw + * window decorations. */ +#if defined(XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION) + const uint32_t preferred = XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION; +#elif defined(XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION) + const uint32_t preferred = XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION; +#else + const uint32_t preferred = required; +#endif wayl->shell = wl_registry_bind( - wayl->registry, name, &xdg_wm_base_interface, min(version, 4)); + wayl->registry, name, &xdg_wm_base_interface, min(version, preferred)); xdg_wm_base_add_listener(wayl->shell, &xdg_wm_base_listener, wayl); } @@ -1427,6 +1464,9 @@ wayl_win_init(struct terminal *term, const char *token) win->csd.move_timeout_fd = -1; win->resize_timeout_fd = -1; + win->wm_capabilities.maximize = true; + win->wm_capabilities.minimize = true; + win->surface = wl_compositor_create_surface(wayl->compositor); if (win->surface == NULL) { LOG_ERR("failed to create wayland surface"); diff --git a/wayland.h b/wayland.h index 803fe390..e86c6a3d 100644 --- a/wayland.h +++ b/wayland.h @@ -339,6 +339,11 @@ struct wl_window { uint32_t serial; } csd; + struct { + bool maximize:1; + bool minimize:1; + } wm_capabilities; + struct wl_surf_subsurf search; struct wl_surf_subsurf scrollback_indicator; struct wl_surf_subsurf render_timer; From aaf5894ad9272698cfafeb8e299b925fb9554417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 5 Aug 2022 18:26:37 +0200 Subject: [PATCH 0107/1323] grid: get rid of empty row at the bottom after reflowing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the window is resized and we reflow the text, we ended up inserting an empty row at the bottom. This happens whenever the actual last row has a hard linebreak (which almost always is the case); we then end the reflow with a line break, causing an extra, empty, row to be allocated and inserted. This patch fixes this by detecting when: 1) the last row is empty 2) the next to last row has a hard line break In this case, we roll back the last line break, by adjusting the new offset we just calculated, and free:ing the empty row. TODO: it would be nice if we could detect this in the reflow loop instead, and avoid doing the last line break all together. I haven’t yet been able to find a way to do this correctly. Closes #1108 --- CHANGELOG.md | 2 ++ grid.c | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b82cfa00..1dddd2d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -109,6 +109,7 @@ * Poor performance when making very large selections ([#1114][1114]). * Bogus error message when using systemd socket activation for server mode ([#1107][1107]) +* Empty line at the bottom after a window resize ([#1108][1108]). [1055]: https://codeberg.org/dnkl/foot/issues/1055 [1092]: https://codeberg.org/dnkl/foot/issues/1092 @@ -117,6 +118,7 @@ [1120]: https://codeberg.org/dnkl/foot/issues/1120 [1114]: https://codeberg.org/dnkl/foot/issues/1114 [1107]: https://codeberg.org/dnkl/foot/issues/1107 +[1108]: https://codeberg.org/dnkl/foot/issues/1108 ### Security diff --git a/grid.c b/grid.c index 2790d16b..f40803e1 100644 --- a/grid.c +++ b/grid.c @@ -984,6 +984,26 @@ grid_resize_and_reflow( /* Set offset such that the last reflowed row is at the bottom */ grid->offset = new_row_idx - new_screen_rows + 1; + + if (new_col_idx == 0) { + int next_to_last_new_row_idx = new_row_idx - 1; + next_to_last_new_row_idx += new_rows; + next_to_last_new_row_idx &= new_rows - 1; + + const struct row *next_to_last_row = new_grid[next_to_last_new_row_idx]; + if (next_to_last_row != NULL && next_to_last_row->linebreak) { + /* + * The next to last row is actually the *last* row. But we + * ended the reflow with a line-break, causing an empty + * row to be inserted at the bottom. Undo this. + */ + /* TODO: can we detect this in the reflow loop above instead? */ + grid->offset--; + grid_row_free(new_grid[new_row_idx]); + new_grid[new_row_idx] = NULL; + } + } + while (grid->offset < 0) grid->offset += new_rows; while (new_grid[grid->offset] == NULL) From e3eefdacde0182aa2c5e56cf450e3abac094e852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 7 Aug 2022 09:31:56 +0200 Subject: [PATCH 0108/1323] changelog: prepare for 1.13.0 --- CHANGELOG.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dddd2d3..ca337533 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.13.0](#1-13-0) * [1.12.1](#1-12-1) * [1.12.0](#1-12-0) * [1.11.0](#1-11-0) @@ -39,7 +39,7 @@ * [1.2.0](#1-2-0) -## Unreleased +## 1.13.0 ### Added @@ -82,8 +82,6 @@ [1105]: https://codeberg.org/dnkl/foot/issues/1105 -### Deprecated -### Removed ### Fixed * Graphical corruption when viewport is at the top of the scrollback, @@ -121,9 +119,14 @@ [1108]: https://codeberg.org/dnkl/foot/issues/1108 -### Security ### Contributors +* Craig Barnes +* Lorenz +* Max Gautier +* Simon Ser +* Stefan Prosiegel + ## 1.12.1 From e0465d3a7a0b8d6962462a505f2f8b32c2ee74bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 7 Aug 2022 09:32:02 +0200 Subject: [PATCH 0109/1323] meson: bump version to 1.13.0 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 676d550a..c5597113 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.12.1', + version: '1.13.0', license: 'MIT', meson_version: '>=0.58.0', default_options: [ From a36848a4ad478ebbff3ce4fa27f111f810b9bb2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 7 Aug 2022 09:38:25 +0200 Subject: [PATCH 0110/1323] =?UTF-8?q?changelog:=20add=20new=20=E2=80=98unr?= =?UTF-8?q?eleased=E2=80=99=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca337533..da7f779f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.13.0](#1-13-0) * [1.12.1](#1-12-1) * [1.12.0](#1-12-0) @@ -39,6 +40,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.13.0 ### Added From e249b52abd59024e0a933f93b458d7bf4e780222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 8 Aug 2022 16:31:28 +0200 Subject: [PATCH 0111/1323] render: apply a dimmed overlay while in Unicode input mode --- CHANGELOG.md | 4 ++++ render.c | 33 ++++++++++++++++++++++++++++----- terminal.h | 7 ++++--- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da7f779f..e914e4d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,10 @@ ## Unreleased ### Added ### Changed + +* Window is now dimmed while in Unicode input mode. + + ### Deprecated ### Removed ### Fixed diff --git a/render.c b/render.c index ff82802e..82c9bd13 100644 --- a/render.c +++ b/render.c @@ -1466,10 +1466,21 @@ static void render_overlay(struct terminal *term) { struct wl_surf_subsurf *overlay = &term->window->overlay; + bool unicode_mode_active = false; + + /* Check if unicode mode is active on at least one seat focusing + * this terminal instance */ + tll_foreach(term->wl->seats, it) { + if (it->item.unicode_mode.active) { + unicode_mode_active = true; + break; + } + } const enum overlay_style style = term->is_searching ? OVERLAY_SEARCH : term->flash.active ? OVERLAY_FLASH : + unicode_mode_active ? OVERLAY_UNICODE_MODE : OVERLAY_NONE; if (likely(style == OVERLAY_NONE)) { @@ -1488,9 +1499,21 @@ render_overlay(struct terminal *term) pixman_image_set_clip_region32(buf->pix[0], NULL); - pixman_color_t color = style == OVERLAY_SEARCH - ? (pixman_color_t){0, 0, 0, 0x7fff} - : (pixman_color_t){.red=0x7fff, .green=0x7fff, .blue=0, .alpha=0x7fff}; + pixman_color_t color; + + switch (style) { + case OVERLAY_NONE: + break; + + case OVERLAY_SEARCH: + case OVERLAY_UNICODE_MODE: + color = (pixman_color_t){0, 0, 0, 0x7fff}; + break; + + case OVERLAY_FLASH: + color = (pixman_color_t){.red=0x7fff, .green=0x7fff, .blue=0, .alpha=0x7fff}; + break; + } /* Bounding rectangle of damaged areas - for wl_surface_damage_buffer() */ pixman_box32_t damage_bounds; @@ -1517,7 +1540,7 @@ render_overlay(struct terminal *term) * region that needs to be *cleared* in this frame. * * Finally, the union of the two “diff” regions above, gives - * us the total region affecte by a change, in either way. We + * 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. */ @@ -1605,7 +1628,7 @@ render_overlay(struct terminal *term) else if (buf == term->render.last_overlay_buf && style == term->render.last_overlay_style) { - xassert(style == OVERLAY_FLASH); + xassert(style == OVERLAY_FLASH || style == OVERLAY_UNICODE_MODE); shm_did_not_use_buf(buf); return; } else { diff --git a/terminal.h b/terminal.h index bf6e74fe..0dde6330 100644 --- a/terminal.h +++ b/terminal.h @@ -289,9 +289,10 @@ enum term_surface { }; enum overlay_style { - OVERLAY_NONE = 0, - OVERLAY_SEARCH = 1, - OVERLAY_FLASH = 2, + OVERLAY_NONE, + OVERLAY_SEARCH, + OVERLAY_FLASH, + OVERLAY_UNICODE_MODE, }; typedef tll(struct ptmx_buffer) ptmx_buffer_list_t; From eafff70439c650dba288d0ac05cd009510bf6205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 12 Aug 2022 16:12:36 +0200 Subject: [PATCH 0112/1323] wayland: #ifdef on XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION This enables us to compile against wayland-protocols < 1.25 --- CHANGELOG.md | 4 ++++ wayland.c | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e914e4d3..4c10be50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,10 @@ ### Deprecated ### Removed ### Fixed + +* Compiling against wayland-protocols < 1.25 + + ### Security ### Contributors diff --git a/wayland.c b/wayland.c index fb41cf7d..05de79aa 100644 --- a/wayland.c +++ b/wayland.c @@ -706,6 +706,7 @@ xdg_toplevel_close(void *data, struct xdg_toplevel *xdg_toplevel) term_shutdown(term); } +#if defined(XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION) static void xdg_toplevel_configure_bounds(void *data, struct xdg_toplevel *xdg_toplevel, @@ -713,6 +714,7 @@ xdg_toplevel_configure_bounds(void *data, { /* TODO: ensure we don't pick a bigger size */ } +#endif #if defined(XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION) static void @@ -742,7 +744,9 @@ xdg_toplevel_wm_capabilities(void *data, static const struct xdg_toplevel_listener xdg_toplevel_listener = { .configure = &xdg_toplevel_configure, /*.close = */&xdg_toplevel_close, /* epoll-shim defines a macro ‘close’... */ +#if defined(XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION) .configure_bounds = &xdg_toplevel_configure_bounds, +#endif #if defined(XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION) .wm_capabilities = xdg_toplevel_wm_capabilities, #endif From 45803791cfd466a68968e22d38515474585301ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 12 Aug 2022 16:13:25 +0200 Subject: [PATCH 0113/1323] input: ignore pointer motion events on unknown surfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In some cases, the compositor sends a pointer enter event with a NULL surface. It’s unclear if this is a compositor bug, or a race (where the compositor sends an enter event on a CSD surface at the same time foot unmaps the CSDs). Regardless, this causes seat->mouse_focus to be unset, which triggers a crash in foot on the next pointer motion event. This patch does two things: a) log a warning when we receive a pointer event with a NULL surface b) ignore motion events where seat->mouse_focus is NULL --- CHANGELOG.md | 3 +++ input.c | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c10be50..5fa35a63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,9 @@ ### Fixed * Compiling against wayland-protocols < 1.25 +* Crash on buggy compositors (GNOME) that sometimes send pointer-enter + events with a NULL surface. Foot now ignores these events, and the + subsequent motion and leave events. ### Security diff --git a/input.c b/input.c index 847ff6af..50336679 100644 --- a/input.c +++ b/input.c @@ -1709,11 +1709,9 @@ wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface, wl_fixed_t surface_x, wl_fixed_t surface_y) { - xassert(surface != NULL); - xassert(serial != 0); - - if (surface == NULL) { + if (unlikely(surface == NULL)) { /* Seen on mutter-3.38 */ + LOG_WARN("compositor sent pointer_enter event with a NULL surface"); return; } @@ -1866,6 +1864,16 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, struct seat *seat = data; struct wayland *wayl = seat->wayl; struct terminal *term = seat->mouse_focus; + + if (unlikely(term == NULL)) { + /* Typically happens when the compositor sent a pointer enter + * event with a NULL surface - see wl_pointer_enter(). + * + * In this case, we never set seat->mouse_focus (since we + * can’t map the enter event to a specific window). */ + return; + } + struct wl_window *win = term->window; LOG_DBG("pointer_motion: pointer=%p, x=%d, y=%d", (void *)wl_pointer, From 157b64098a0832d66b5f8d206a14aa2c1342334b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 7 Aug 2022 09:38:25 +0200 Subject: [PATCH 0114/1323] =?UTF-8?q?changelog:=20add=20new=20=E2=80=98unr?= =?UTF-8?q?eleased=E2=80=99=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca337533..da7f779f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.13.0](#1-13-0) * [1.12.1](#1-12-1) * [1.12.0](#1-12-0) @@ -39,6 +40,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.13.0 ### Added From 1cf22846a0e333c61d5db50c260fd897143c6a83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 8 Aug 2022 16:31:28 +0200 Subject: [PATCH 0115/1323] render: apply a dimmed overlay while in Unicode input mode --- CHANGELOG.md | 4 ++++ render.c | 33 ++++++++++++++++++++++++++++----- terminal.h | 7 ++++--- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da7f779f..e914e4d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,10 @@ ## Unreleased ### Added ### Changed + +* Window is now dimmed while in Unicode input mode. + + ### Deprecated ### Removed ### Fixed diff --git a/render.c b/render.c index ff82802e..82c9bd13 100644 --- a/render.c +++ b/render.c @@ -1466,10 +1466,21 @@ static void render_overlay(struct terminal *term) { struct wl_surf_subsurf *overlay = &term->window->overlay; + bool unicode_mode_active = false; + + /* Check if unicode mode is active on at least one seat focusing + * this terminal instance */ + tll_foreach(term->wl->seats, it) { + if (it->item.unicode_mode.active) { + unicode_mode_active = true; + break; + } + } const enum overlay_style style = term->is_searching ? OVERLAY_SEARCH : term->flash.active ? OVERLAY_FLASH : + unicode_mode_active ? OVERLAY_UNICODE_MODE : OVERLAY_NONE; if (likely(style == OVERLAY_NONE)) { @@ -1488,9 +1499,21 @@ render_overlay(struct terminal *term) pixman_image_set_clip_region32(buf->pix[0], NULL); - pixman_color_t color = style == OVERLAY_SEARCH - ? (pixman_color_t){0, 0, 0, 0x7fff} - : (pixman_color_t){.red=0x7fff, .green=0x7fff, .blue=0, .alpha=0x7fff}; + pixman_color_t color; + + switch (style) { + case OVERLAY_NONE: + break; + + case OVERLAY_SEARCH: + case OVERLAY_UNICODE_MODE: + color = (pixman_color_t){0, 0, 0, 0x7fff}; + break; + + case OVERLAY_FLASH: + color = (pixman_color_t){.red=0x7fff, .green=0x7fff, .blue=0, .alpha=0x7fff}; + break; + } /* Bounding rectangle of damaged areas - for wl_surface_damage_buffer() */ pixman_box32_t damage_bounds; @@ -1517,7 +1540,7 @@ render_overlay(struct terminal *term) * region that needs to be *cleared* in this frame. * * Finally, the union of the two “diff” regions above, gives - * us the total region affecte by a change, in either way. We + * 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. */ @@ -1605,7 +1628,7 @@ render_overlay(struct terminal *term) else if (buf == term->render.last_overlay_buf && style == term->render.last_overlay_style) { - xassert(style == OVERLAY_FLASH); + xassert(style == OVERLAY_FLASH || style == OVERLAY_UNICODE_MODE); shm_did_not_use_buf(buf); return; } else { diff --git a/terminal.h b/terminal.h index bf6e74fe..0dde6330 100644 --- a/terminal.h +++ b/terminal.h @@ -289,9 +289,10 @@ enum term_surface { }; enum overlay_style { - OVERLAY_NONE = 0, - OVERLAY_SEARCH = 1, - OVERLAY_FLASH = 2, + OVERLAY_NONE, + OVERLAY_SEARCH, + OVERLAY_FLASH, + OVERLAY_UNICODE_MODE, }; typedef tll(struct ptmx_buffer) ptmx_buffer_list_t; From 5697348b461f4dec4daf67c62336bb357149baf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 12 Aug 2022 16:12:36 +0200 Subject: [PATCH 0116/1323] wayland: #ifdef on XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION This enables us to compile against wayland-protocols < 1.25 --- CHANGELOG.md | 4 ++++ wayland.c | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e914e4d3..4c10be50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,10 @@ ### Deprecated ### Removed ### Fixed + +* Compiling against wayland-protocols < 1.25 + + ### Security ### Contributors diff --git a/wayland.c b/wayland.c index fb41cf7d..05de79aa 100644 --- a/wayland.c +++ b/wayland.c @@ -706,6 +706,7 @@ xdg_toplevel_close(void *data, struct xdg_toplevel *xdg_toplevel) term_shutdown(term); } +#if defined(XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION) static void xdg_toplevel_configure_bounds(void *data, struct xdg_toplevel *xdg_toplevel, @@ -713,6 +714,7 @@ xdg_toplevel_configure_bounds(void *data, { /* TODO: ensure we don't pick a bigger size */ } +#endif #if defined(XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION) static void @@ -742,7 +744,9 @@ xdg_toplevel_wm_capabilities(void *data, static const struct xdg_toplevel_listener xdg_toplevel_listener = { .configure = &xdg_toplevel_configure, /*.close = */&xdg_toplevel_close, /* epoll-shim defines a macro ‘close’... */ +#if defined(XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION) .configure_bounds = &xdg_toplevel_configure_bounds, +#endif #if defined(XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION) .wm_capabilities = xdg_toplevel_wm_capabilities, #endif From 20b8ca1601a8a80770110860ae70d4487c95081b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 12 Aug 2022 16:13:25 +0200 Subject: [PATCH 0117/1323] input: ignore pointer motion events on unknown surfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In some cases, the compositor sends a pointer enter event with a NULL surface. It’s unclear if this is a compositor bug, or a race (where the compositor sends an enter event on a CSD surface at the same time foot unmaps the CSDs). Regardless, this causes seat->mouse_focus to be unset, which triggers a crash in foot on the next pointer motion event. This patch does two things: a) log a warning when we receive a pointer event with a NULL surface b) ignore motion events where seat->mouse_focus is NULL --- CHANGELOG.md | 3 +++ input.c | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c10be50..5fa35a63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,9 @@ ### Fixed * Compiling against wayland-protocols < 1.25 +* Crash on buggy compositors (GNOME) that sometimes send pointer-enter + events with a NULL surface. Foot now ignores these events, and the + subsequent motion and leave events. ### Security diff --git a/input.c b/input.c index 847ff6af..50336679 100644 --- a/input.c +++ b/input.c @@ -1709,11 +1709,9 @@ wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface, wl_fixed_t surface_x, wl_fixed_t surface_y) { - xassert(surface != NULL); - xassert(serial != 0); - - if (surface == NULL) { + if (unlikely(surface == NULL)) { /* Seen on mutter-3.38 */ + LOG_WARN("compositor sent pointer_enter event with a NULL surface"); return; } @@ -1866,6 +1864,16 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, struct seat *seat = data; struct wayland *wayl = seat->wayl; struct terminal *term = seat->mouse_focus; + + if (unlikely(term == NULL)) { + /* Typically happens when the compositor sent a pointer enter + * event with a NULL surface - see wl_pointer_enter(). + * + * In this case, we never set seat->mouse_focus (since we + * can’t map the enter event to a specific window). */ + return; + } + struct wl_window *win = term->window; LOG_DBG("pointer_motion: pointer=%p, x=%d, y=%d", (void *)wl_pointer, From 65ecb7773739dd5dbfcabf905185ba9568f9c346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 20 Aug 2022 18:25:05 +0200 Subject: [PATCH 0118/1323] =?UTF-8?q?ci:=20codespell:=20ignore=20=E2=80=98?= =?UTF-8?q?zar=E2=80=99=20(user=20who=20contributed)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .builds/alpine-x64.yml | 2 +- .gitlab-ci.yml | 2 +- .woodpecker.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.builds/alpine-x64.yml b/.builds/alpine-x64.yml index 8f341f3d..933c7121 100644 --- a/.builds/alpine-x64.yml +++ b/.builds/alpine-x64.yml @@ -49,4 +49,4 @@ tasks: - codespell: | pip install codespell cd foot - ~/.local/bin/codespell -Lser,doas README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd + ~/.local/bin/codespell -Lser,doas,zar README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b2a459dc..28df1ccb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -109,4 +109,4 @@ codespell: - apk add python3 - apk add py3-pip - pip install codespell - - codespell -Lser,doas README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd + - codespell -Lser,doas,zar README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd diff --git a/.woodpecker.yml b/.woodpecker.yml index 8493aa47..284da761 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -9,7 +9,7 @@ pipeline: - apk add python3 - apk add py3-pip - pip install codespell - - codespell -Lser,doas README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd + - codespell -Lser,doas,zar README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd subprojects: when: From a0942f950df3b8826db4b1a3f149b7d0ed45adcf Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 19 Aug 2022 02:54:49 +0200 Subject: [PATCH 0119/1323] config: add setting for underline thickness This adds an "underline-thickness" setting to the "main" section, similar to the existing "underline-offset" setting. This setting is used to specify a custom height for regular (= non-cursor) underlines. Fixes #1136 --- CHANGELOG.md | 6 ++++++ config.c | 4 ++++ config.h | 1 + doc/foot.ini.5.scd | 12 ++++++++++++ foot.ini | 1 + render.c | 5 ++++- tests/test-config.c | 1 + 7 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fa35a63..f0bf2624 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,12 @@ ## Unreleased ### Added + +* Support for adjusting the thickness of regular underlines ([#1136][1136]). + +[1136]: https://codeberg.org/dnkl/foot/issues/1136 + + ### Changed * Window is now dimmed while in Unicode input mode. diff --git a/config.c b/config.c index fbeab3e6..0ee01373 100644 --- a/config.c +++ b/config.c @@ -904,6 +904,9 @@ parse_section_main(struct context *ctx) return true; } + else if (strcmp(key, "underline-thickness") == 0) + return value_to_pt_or_px(ctx, &conf->underline_thickness); + else if (strcmp(key, "dpi-aware") == 0) { if (strcmp(value, "auto") == 0) conf->dpi_aware = DPI_AWARE_AUTO; @@ -2833,6 +2836,7 @@ config_load(struct config *conf, const char *conf_path, .vertical_letter_offset = {.pt = 0, .px = 0}, .use_custom_underline_offset = false, .box_drawings_uses_font_glyphs = false, + .underline_thickness = {.pt = 0., .px = -1}, .dpi_aware = DPI_AWARE_AUTO, /* DPI-aware when scaling-factor == 1 */ .bell = { .urgent = false, diff --git a/config.h b/config.h index 70e182e3..f98e4d35 100644 --- a/config.h +++ b/config.h @@ -150,6 +150,7 @@ struct config { bool use_custom_underline_offset; struct pt_or_px underline_offset; + struct pt_or_px underline_thickness; bool box_drawings_uses_font_glyphs; bool can_shape_grapheme; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 995a7e33..0bc21786 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -132,6 +132,18 @@ commented out will usually be installed to */etc/xdg/foot/foot.ini*. Default: _unset_. +*underline-thickness* + Use a custom thickness (height) for underlines. The thickness is, by + default, in _points_. + + To specify a thickness in _pixels_, append *px*: + *underline-thickness=1px*. + + If left unset (the default), the thickness specified in the font is + used. + + Default: _unset_ + *box-drawings-uses-font-glyphs* Boolean. When disabled, foot generates box/line drawing characters itself. The are several advantages to doing this instead of using font glyphs: diff --git a/foot.ini b/foot.ini index 4c857296..0c19951e 100644 --- a/foot.ini +++ b/foot.ini @@ -17,6 +17,7 @@ # horizontal-letter-offset=0 # vertical-letter-offset=0 # underline-offset= +# underline-thickness= # box-drawings-uses-font-glyphs=no # dpi-aware=auto diff --git a/render.c b/render.c index 82c9bd13..ef698911 100644 --- a/render.c +++ b/render.c @@ -372,7 +372,10 @@ draw_underline(const struct terminal *term, pixman_image_t *pix, const struct fcft_font *font, const pixman_color_t *color, int x, int y, int cols) { - const int thickness = font->underline.thickness; + const int thickness = term->conf->underline_thickness.px >= 0 + ? term_pt_or_px_as_pixels( + term, &term->conf->underline_thickness) + : font->underline.thickness; /* Make sure the line isn't positioned below the cell */ const int y_ofs = min(underline_offset(term, font), diff --git a/tests/test-config.c b/tests/test-config.c index 930be6bf..837c5106 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -470,6 +470,7 @@ test_section_main(void) test_pt_or_px(&ctx, &parse_section_main, "letter-spacing", &conf.letter_spacing); test_pt_or_px(&ctx, &parse_section_main, "horizontal-letter-offset", &conf.horizontal_letter_offset); test_pt_or_px(&ctx, &parse_section_main, "vertical-letter-offset", &conf.vertical_letter_offset); + test_pt_or_px(&ctx, &parse_section_main, "underline-thickness", &conf.underline_thickness); test_uint16(&ctx, &parse_section_main, "resize-delay-ms", &conf.resize_delay_ms); test_uint16(&ctx, &parse_section_main, "workers", &conf.render_worker_count); From 86663522d5915c0fdb2b8b132bacc247fcfd1fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 17 Aug 2022 17:36:10 +0200 Subject: [PATCH 0120/1323] selection: never highlight selected, empty cells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes a regression, where empty cells "between" non-empty cells (i.e. non-trailing empty cells) sometimes were incorrectly highlighted. The idea has always been to highlight exactly those cells that will get extracted when they’re copied. This means we’ve not highlighted trailing empty cells, but we _have_ highlighted other empty cells, since they are converted to spaces when copied (whereas trailing empty cells are skipped). fa2d9f86996467ba33cc381f810ea966a4323381 changed how a selection is updated. That is, which cells gets marked as selected, and which ones gets unmarked. Since we no longer walk all the cells, but instead work with pixman regions representing selection diffs, we can no longer determine (with certainty) which empty cells should be selected and which shouldn’t. Before this patch (but after fa2d9f86996467ba33cc381f810ea966a4323381), we sometimes incorrectly highlighted empty cells that should not have been highlighted. This happened when we’ve first (correctly) highlighted a region of empty cells, but then shrink the selection such that all those empty cells should be de-selected. This patch changes the selection behavior to *never* highlight empty cells. This fixes the regression, but also means slightly different behavior, compared to pre-fa2d9f86996467ba33cc381f810ea966a4323381. The other alternative is to always highlight all empty cells. But, since I personally like the fact that we’re skipping trailing empty cells, I prefer the approach taken by this patch. --- CHANGELOG.md | 4 ++++ selection.c | 23 +++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0bf2624..8e95c9df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,8 @@ ### Changed * Window is now dimmed while in Unicode input mode. +* Selected empty cells are **never** highlighted as being + selected. They used to be, when followed by non-empty cells. ### Deprecated @@ -61,6 +63,8 @@ * Crash on buggy compositors (GNOME) that sometimes send pointer-enter events with a NULL surface. Foot now ignores these events, and the subsequent motion and leave events. +* Regression: “random” selected empty cells being highlighted as + selected when they should not. ### Security diff --git a/selection.c b/selection.c index 8c3a8357..2fd62f8a 100644 --- a/selection.c +++ b/selection.c @@ -737,8 +737,27 @@ mark_selected_region(struct terminal *term, pixman_box32_t *boxes, row->dirty = true; for (int c = box->x1, empty_count = 0; c < box->x2; c++) { - if (selected && row->cells[c].wc == 0) { - empty_count++; + if (row->cells[c].wc == 0) { + /* + * We used to highlight empty cells *if* they were + * followed by non-empty cell(s), since this + * corresponds to what gets extracted when the + * selection is copied (that is, empty cells + * “between” non-empty cells are converted to + * spaces). + * + * However, they way we handle selection updates + * (diffing the “old” selection area against the + * “new” one, using pixman regions), means we + * can’t correctly update the state of empty + * cells. The result is “random” empty cells being + * rendered as selected when they shouldn’t. + * + * “Fix” by *never* highlighting selected empty + * cells (they still get converted to spaces when + * copied, if followed by non-empty cells). + */ + /* empty_count++; */ continue; } From 3cf11bfea9e4787998c538bd312c456fd8287fd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 17 Aug 2022 18:12:51 +0200 Subject: [PATCH 0121/1323] theme: change default color theme to solarized-dark-normal-brights This is my variant of the solarized theme, were only the first eight colors (i.e. the "normal") colors are from the solarized theme. The remaining eight (the "bright" colors) are brightened versions of the "normal" colors. This results in a theme that is usually in all applications, not just those that are "aware" that the terminal color theme is "solarized". --- CHANGELOG.md | 2 ++ config.c | 34 +++++++++++++++++----------------- doc/foot.ini.5.scd | 14 ++++++++------ foot.ini | 34 +++++++++++++++++----------------- 4 files changed, 44 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e95c9df..656c5a46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,8 @@ * Window is now dimmed while in Unicode input mode. * Selected empty cells are **never** highlighted as being selected. They used to be, when followed by non-empty cells. +* Default color theme from a variant of the Zenburn theme, to a + variant of the Solarized dark theme. ### Deprecated diff --git a/config.c b/config.c index 0ee01373..3ecb3db5 100644 --- a/config.c +++ b/config.c @@ -30,8 +30,8 @@ #include "xmalloc.h" #include "xsnprintf.h" -static const uint32_t default_foreground = 0xdcdccc; -static const uint32_t default_background = 0x111111; +static const uint32_t default_foreground = 0x839496; +static const uint32_t default_background = 0x002b36; static const size_t min_csd_border_width = 5; @@ -48,23 +48,23 @@ static const size_t min_csd_border_width = 5; static const uint32_t default_color_table[256] = { // Regular - 0x222222, - 0xcc9393, - 0x7f9f7f, - 0xd0bf8f, - 0x6ca0a3, - 0xdc8cc3, - 0x93e0e3, - 0xdcdccc, + 0x073642, + 0xdc322f, + 0x859900, + 0xb58900, + 0x268bd2, + 0xd33682, + 0x2aa198, + 0xeee8d5, // Bright - 0x666666, - 0xdca3a3, - 0xbfebbf, - 0xf0dfaf, - 0x8cd0d3, - 0xfcace3, - 0xb3ffff, + 0x08404f, + 0xe35f5c, + 0x9fb700, + 0xd9a400, + 0x4ba1de, + 0xdc619d, + 0x32c1b6, 0xffffff, // 6x6x6 RGB cube diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 0bc21786..a0cf69f5 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -507,21 +507,23 @@ can configure the background transparency with the _alpha_ option. *foreground* Default foreground color. This is the color used when no ANSI - color is being used. Default: _dcdccc_. + color is being used. Default: _839496_. *background* Default background color. This is the color used when no ANSI - color is being used. Default: _111111_. + color is being used. Default: _002b36_. *regular0*, *regular1* *..* *regular7* The eight basic ANSI colors (Black, Red, Green, Yellow, Blue, - Magenta, Cyan, White). Default: _222222_, _cc9393_, _7f9f7f_, _d0bf8f_, - _6ca0a3_, _dc8cc3_, _93e0e3_ and _dcdccc_ (a variant of the _zenburn_ theme). + Magenta, Cyan, White). Default: _073642_, _dc322f_, _859900_, + _b58900_, _268bd2_, _d33682_, _2aa198_ and _eee8d5_ (a variant of + the _solarized dark_ theme). *bright0*, *bright1* *..* *bright7* The eight bright ANSI colors (Black, Red, Green, Yellow, Blue, - Magenta, Cyan, White). Default: _666666_, _dca3a3_, _bfebbf_, _f0dfaf_, - _8cd0d3_, _fcace3_, _b3ffff_ and _ffffff_ (a variant of the _zenburn_ theme). + Magenta, Cyan, White). Default: _08404f_, _e35f5c_, _9fb700_, + _d9a400_, _4ba1de_, _dc619d_, _32c1b6_ and _ffffff_ (a variant of + the _solarized dark_ theme). *dim0*, *dim1* *..* *dim7* Custom colors to use with dimmed colors. Dimmed colors do not have diff --git a/foot.ini b/foot.ini index 0c19951e..926ed499 100644 --- a/foot.ini +++ b/foot.ini @@ -69,27 +69,27 @@ [colors] # alpha=1.0 -# foreground=dcdccc -# background=111111 +# background=002b36 +# foreground=839496 ## Normal/regular colors (color palette 0-7) -# regular0=222222 # black -# regular1=cc9393 # red -# regular2=7f9f7f # green -# regular3=d0bf8f # yellow -# regular4=6ca0a3 # blue -# regular5=dc8cc3 # magenta -# regular6=93e0e3 # cyan -# regular7=dcdccc # white +# regular0=073642 # black +# regular1=dc322f # red +# regular2=859900 # green +# regular3=b58900 # yellow +# regular4=268bd2 # blue +# regular5=d33682 # magenta +# regular6=2aa198 # cyan +# regular7=eee8d5 # white ## Bright colors (color palette 8-15) -# bright0=666666 # bright black -# bright1=dca3a3 # bright red -# bright2=bfebbf # bright green -# bright3=f0dfaf # bright yellow -# bright4=8cd0d3 # bright blue -# bright5=fcace3 # bright magenta -# bright6=b3ffff # bright cyan +# bright0=08404f # bright black +# bright1=e35f5c # bright red +# bright2=9fb700 # bright green +# bright3=d9a400 # bright yellow +# bright4=4ba1de # bright blue +# bright5=dc619d # bright magenta +# bright6=32c1b6 # bright cyan # bright7=ffffff # bright white ## dimmed colors (see foot.ini(5) man page) From 21ab16239d87fe8fb4d7651b4bccbe1f611df620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 22 Aug 2022 20:14:06 +0200 Subject: [PATCH 0122/1323] selection: once again highlight non-trailing empty cells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Foot 1.13.0 introduced a regression where non-trailing empty cells were highlighted inconsistently (cells that shouldn’t be highlighted, were, seemingly at random). 86663522d5915c0fdb2b8b132bacc247fcfd1fb8 “fixed” this by never highlighting *any* empty cells. This meant the behavior, compared to foot 1.12 and earlier, changed. In foot 1.12 and older versions, non-trailing empty cells were highlighted, as long as the selection covered at least one of the trailing non-empty cells. This patch restores that behavior. To understand how this works, lets first take a look at how selection works: When a selection is made, and updated (i.e. the mouse is dragged, or the selection is extended through RMB etc), we need to (un)tag and dirty the cells that are a) newly selected, or b) newly deselected. That is, we look at the diff between the “old” and the “new” selection, and only update those cells. This is for performance reasons: iterating the entire selection is not feasible with large selections. However, it also means we cannot reason about empty cells; we simply don’t know if an empty cells is a trailing empty cell, or a non-trailing one. Then, when we render a frame, we iterate all the *visible* and *selected* cells, once again tagging them as selected (this is needed since a selected cell might have lost its selected tag if the cell was written to, by the client application, after the selection was made). At this point, we *can* reason about empty cells. So, to restore the highlighting behavior to that of foot 1.12, we do this: When working with the selection diffs when a selection is updated, we don’t special case empty cells at all. Thus, all empty cells covered by the selection is highlighted, and dirtied. But, when rendering the frame, we _do_ special case them. The only difference (compared to foot 1.12) is that we *must* explicitly *clear* the selection tag, and dirty the empty cells. This is to ensure the empty cells that were incorrectly highlighted by the selection update algorithm, isn’t rendered as that. This does have a slight performance impact, as empty cells are now always re-rendered. The impact should however be small. --- CHANGELOG.md | 2 -- selection.c | 44 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 656c5a46..2eddc3bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,8 +51,6 @@ ### Changed * Window is now dimmed while in Unicode input mode. -* Selected empty cells are **never** highlighted as being - selected. They used to be, when followed by non-empty cells. * Default color theme from a variant of the Zenburn theme, to a variant of the Solarized dark theme. diff --git a/selection.c b/selection.c index 2fd62f8a..5b65310c 100644 --- a/selection.c +++ b/selection.c @@ -713,7 +713,8 @@ pixman_region_for_coords(const struct terminal *term, static void mark_selected_region(struct terminal *term, pixman_box32_t *boxes, - size_t count, bool selected, bool dirty_cells) + size_t count, bool selected, bool dirty_cells, + bool highlight_empty) { for (size_t i = 0; i < count; i++) { const pixman_box32_t *box = &boxes[i]; @@ -737,7 +738,9 @@ mark_selected_region(struct terminal *term, pixman_box32_t *boxes, row->dirty = true; for (int c = box->x1, empty_count = 0; c < box->x2; c++) { - if (row->cells[c].wc == 0) { + struct cell *cell = &row->cells[c]; + + if (cell->wc == 0 && !highlight_empty) { /* * We used to highlight empty cells *if* they were * followed by non-empty cell(s), since this @@ -757,7 +760,36 @@ mark_selected_region(struct terminal *term, pixman_box32_t *boxes, * cells (they still get converted to spaces when * copied, if followed by non-empty cells). */ - /* empty_count++; */ + empty_count++; + + /* + * When the selection is *modified*, empty cells + * are treated just like non-empty cells; they are + * marked as selected, and dirtied. + * + * This is due to how the algorithm for updating + * the selection works; it uses regions to + * calculate the difference between the “old” and + * the “new” selection. This makes it impossible + * to tell if an empty cell is a *trailing* empty + * cell (that should not be highlighted), or an + * empty cells between non-empty cells (that + * *should* be highlighted). + * + * Then, when a frame is rendered, we loop the + * *visibible* cells that belong to the + * selection. At this point, we *can* tell if an + * empty cell is trailing or not. + * + * So, what we need to do is check if a + * ‘selected’, and empty cell has been marked as + * selected, temporarily unmark (forcing it dirty, + * to ensure it gets re-rendered). If it is *not* + * a trailing empty cell, it will get re-tagged as + * selected in the for-loop below. + */ + cell->attrs.clean = false; + cell->attrs.selected = false; continue; } @@ -810,10 +842,10 @@ selection_modify(struct terminal *term, struct coord start, struct coord end) pixman_box32_t *boxes = NULL; boxes = pixman_region32_rectangles(&no_longer_selected, &n_rects); - mark_selected_region(term, boxes, n_rects, false, true); + mark_selected_region(term, boxes, n_rects, false, true, true); boxes = pixman_region32_rectangles(&newly_selected, &n_rects); - mark_selected_region(term, boxes, n_rects, true, true); + mark_selected_region(term, boxes, n_rects, true, true, true); pixman_region32_fini(&newly_selected); pixman_region32_fini(&no_longer_selected); @@ -1078,7 +1110,7 @@ selection_dirty_cells(struct terminal *term) int n_rects = -1; pixman_box32_t *boxes = pixman_region32_rectangles(&visible_and_selected, &n_rects); - mark_selected_region(term, boxes, n_rects, true, false); + mark_selected_region(term, boxes, n_rects, true, false, false); pixman_region32_fini(&visible_and_selected); pixman_region32_fini(&view); From 4f06d413e246747bbe2ce9a84743963e2690b8f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 23 Aug 2022 16:38:39 +0200 Subject: [PATCH 0123/1323] ci (sr.ht): pull directly from git.sr.ht --- .builds/alpine-x64.yml | 2 +- .builds/alpine-x86.yml.disabled | 2 +- .builds/freebsd-x64.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.builds/alpine-x64.yml b/.builds/alpine-x64.yml index 933c7121..2e2ec2c4 100644 --- a/.builds/alpine-x64.yml +++ b/.builds/alpine-x64.yml @@ -24,7 +24,7 @@ packages: - font-noto-emoji sources: - - https://codeberg.org/dnkl/foot + - https://git.sr.ht/~dnkl/foot # triggers: # - action: email diff --git a/.builds/alpine-x86.yml.disabled b/.builds/alpine-x86.yml.disabled index 22a9e637..6d790227 100644 --- a/.builds/alpine-x86.yml.disabled +++ b/.builds/alpine-x86.yml.disabled @@ -23,7 +23,7 @@ packages: - font-noto-emoji sources: - - https://codeberg.org/dnkl/foot + - https://git.sr.ht/~dnkl/foot # triggers: # - action: email diff --git a/.builds/freebsd-x64.yml b/.builds/freebsd-x64.yml index 89803a6e..9642f96d 100644 --- a/.builds/freebsd-x64.yml +++ b/.builds/freebsd-x64.yml @@ -19,7 +19,7 @@ packages: - noto-emoji sources: - - https://codeberg.org/dnkl/foot + - https://git.sr.ht/~dnkl/foot # triggers: # - action: email From db2737b96a2cf82b3c1ced2549e4e795ffe13b04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 26 Aug 2022 17:48:00 +0200 Subject: [PATCH 0124/1323] selection: restore <= 1.12 behavior in block selection wrt empty cells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit That is, highlight empty cells, regardless of whether they’re trailing or not. --- selection.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/selection.c b/selection.c index 5b65310c..5bf72e62 100644 --- a/selection.c +++ b/selection.c @@ -1110,7 +1110,9 @@ selection_dirty_cells(struct terminal *term) int n_rects = -1; pixman_box32_t *boxes = pixman_region32_rectangles(&visible_and_selected, &n_rects); - mark_selected_region(term, boxes, n_rects, true, false, false); + + const bool highlight_empty = term->selection.kind == SELECTION_BLOCK; + mark_selected_region(term, boxes, n_rects, true, false, highlight_empty); pixman_region32_fini(&visible_and_selected); pixman_region32_fini(&view); From d5df86f7853a47aa2de4aea7f4b5a84e918ea433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 26 Aug 2022 21:07:20 +0200 Subject: [PATCH 0125/1323] selection: mark_selected_region(): use an enum to encode how the cells are to be updated --- selection.c | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/selection.c b/selection.c index 5bf72e62..c94686e1 100644 --- a/selection.c +++ b/selection.c @@ -711,11 +711,26 @@ pixman_region_for_coords(const struct terminal *term, } } +enum mark_selection_variant { + MARK_SELECTION_MARK_AND_DIRTY, + MARK_SELECTION_UNMARK_AND_DIRTY, + MARK_SELECTION_MARK_FOR_RENDER, +}; + static void mark_selected_region(struct terminal *term, pixman_box32_t *boxes, - size_t count, bool selected, bool dirty_cells, - bool highlight_empty) + size_t count, enum mark_selection_variant mark_variant) { + const bool selected = + mark_variant == MARK_SELECTION_MARK_AND_DIRTY || + mark_variant == MARK_SELECTION_MARK_FOR_RENDER; + const bool dirty_cells = + mark_variant == MARK_SELECTION_MARK_AND_DIRTY || + mark_variant == MARK_SELECTION_UNMARK_AND_DIRTY; + const bool highlight_empty = + mark_variant != MARK_SELECTION_MARK_FOR_RENDER || + term->selection.kind == SELECTION_BLOCK; + for (size_t i = 0; i < count; i++) { const pixman_box32_t *box = &boxes[i]; @@ -842,10 +857,10 @@ selection_modify(struct terminal *term, struct coord start, struct coord end) pixman_box32_t *boxes = NULL; boxes = pixman_region32_rectangles(&no_longer_selected, &n_rects); - mark_selected_region(term, boxes, n_rects, false, true, true); + mark_selected_region(term, boxes, n_rects, MARK_SELECTION_UNMARK_AND_DIRTY); boxes = pixman_region32_rectangles(&newly_selected, &n_rects); - mark_selected_region(term, boxes, n_rects, true, true, true); + mark_selected_region(term, boxes, n_rects, MARK_SELECTION_MARK_AND_DIRTY); pixman_region32_fini(&newly_selected); pixman_region32_fini(&no_longer_selected); @@ -1110,9 +1125,7 @@ selection_dirty_cells(struct terminal *term) int n_rects = -1; pixman_box32_t *boxes = pixman_region32_rectangles(&visible_and_selected, &n_rects); - - const bool highlight_empty = term->selection.kind == SELECTION_BLOCK; - mark_selected_region(term, boxes, n_rects, true, false, highlight_empty); + mark_selected_region(term, boxes, n_rects, MARK_SELECTION_MARK_FOR_RENDER); pixman_region32_fini(&visible_and_selected); pixman_region32_fini(&view); From 13281f327bcbe4a4799bea2122b77829f0004774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 29 Aug 2022 20:46:19 +0200 Subject: [PATCH 0126/1323] =?UTF-8?q?grid:=20when=20setting=20the=20new=20?= =?UTF-8?q?viewport,=20ensure=20it=E2=80=99s=20correctly=20bounded?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Do this by using scrollback relative coordinates, and ensure the new viewport is not larger than (grid_rows - screen_rows), as that would mean the viewport crosses the scrollback wrap-around. --- grid.c | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/grid.c b/grid.c index f40803e1..f229effa 100644 --- a/grid.c +++ b/grid.c @@ -1016,23 +1016,6 @@ grid_resize_and_reflow( new_grid[idx] = grid_row_alloc(new_cols, true); } - grid->view = view_follows ? grid->offset : viewport.row; - - /* If enlarging the window, the old viewport may be too far down, - * with unallocated rows. Make sure this cannot happen */ - while (true) { - int idx = (grid->view + new_screen_rows - 1) & (new_rows - 1); - if (new_grid[idx] != NULL) - break; - grid->view--; - if (grid->view < 0) - grid->view += new_rows; - } - for (size_t r = 0; r < new_screen_rows; r++) { - int UNUSED idx = (grid->view + r) & (new_rows - 1); - xassert(new_grid[idx] != NULL); - } - /* Free old grid (rows already free:d) */ free(grid->rows); @@ -1040,6 +1023,17 @@ grid_resize_and_reflow( grid->num_rows = new_rows; grid->num_cols = new_cols; + /* + * Set new viewport, making sure it’s not too far down. + * + * This is done by using scrollback-start relative cooardinates, + * and bounding the new viewport to (grid_rows - screen_rows). + */ + int sb_view = grid_row_abs_to_sb( + grid, new_screen_rows, view_follows ? grid->offset : viewport.row); + grid->view = grid_row_sb_to_abs( + grid, new_screen_rows, min(sb_view, new_rows - new_screen_rows)); + /* Convert absolute coordinates to screen relative */ cursor.row -= grid->offset; while (cursor.row < 0) From 5c86358cd1f830c01333326423f862879f9cd6a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 29 Aug 2022 20:47:33 +0200 Subject: [PATCH 0127/1323] =?UTF-8?q?grid:=20reflow:=20don=E2=80=99t=20lin?= =?UTF-8?q?e-wrap=20the=20last=20row?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this patch, we would line-wrap the last row, just like any other row, and then afterwards try to reverse this, by adjusting the offset and free:ing and NULL:ing the "last row". The problem with this is if the scrollback is full. In this case, the row we’re freeing is the first row in the scrollback history. This means we’ll crash as soon as the viewport is moved to the top of the scrollback. The fix is fairly, simple. Skip the post-processing logic, and instead detect when we’re line-wrapping the last row, and skip the call to line_wrap(). This way, the last row in the new grid corresponds to the last row in the old grid. --- grid.c | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/grid.c b/grid.c index f229effa..bbac3395 100644 --- a/grid.c +++ b/grid.c @@ -935,13 +935,14 @@ grid_resize_and_reflow( start += cols; } - if (old_row->linebreak) { /* Erase the remaining cells */ memset(&new_row->cells[new_col_idx], 0, (new_cols - new_col_idx) * sizeof(new_row->cells[0])); new_row->linebreak = true; - line_wrap(); + + if (r + 1 < old_rows) + line_wrap(); } grid_row_free(old_grid[old_row_idx]); @@ -985,25 +986,6 @@ grid_resize_and_reflow( /* Set offset such that the last reflowed row is at the bottom */ grid->offset = new_row_idx - new_screen_rows + 1; - if (new_col_idx == 0) { - int next_to_last_new_row_idx = new_row_idx - 1; - next_to_last_new_row_idx += new_rows; - next_to_last_new_row_idx &= new_rows - 1; - - const struct row *next_to_last_row = new_grid[next_to_last_new_row_idx]; - if (next_to_last_row != NULL && next_to_last_row->linebreak) { - /* - * The next to last row is actually the *last* row. But we - * ended the reflow with a line-break, causing an empty - * row to be inserted at the bottom. Undo this. - */ - /* TODO: can we detect this in the reflow loop above instead? */ - grid->offset--; - grid_row_free(new_grid[new_row_idx]); - new_grid[new_row_idx] = NULL; - } - } - while (grid->offset < 0) grid->offset += new_rows; while (new_grid[grid->offset] == NULL) From 454c82f0f59fea5307fbb570954db26f05ddbd09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 29 Aug 2022 21:03:21 +0200 Subject: [PATCH 0128/1323] =?UTF-8?q?grid:=20reflow:=20assert=20there=20ar?= =?UTF-8?q?en=E2=80=99t=20any=20open=20URIs=20on=20the=20last=20row?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- grid.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/grid.c b/grid.c index bbac3395..7bfef5cb 100644 --- a/grid.c +++ b/grid.c @@ -943,6 +943,21 @@ grid_resize_and_reflow( if (r + 1 < old_rows) line_wrap(); + else if (new_row->extra != NULL && + new_row->extra->uri_ranges.count > 0) + { + /* + * line_wrap() "closes" still-open URIs. Since this is + * the *last* row, and since we’re line-breaking due + * to a hard line-break (rather than running out of + * cells in the "new_row"), there shouldn’t be an open + * URI (it would have been closed when we reached the + * end of the URI while reflowing the last "old" + * row). + */ + uint32_t last_idx = new_row->extra->uri_ranges.count - 1; + xassert(new_row->extra->uri_ranges.v[last_idx].end >= 0); + } } grid_row_free(old_grid[old_row_idx]); From 524474728a5c9a1228500ab39b4974469b156c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 29 Aug 2022 21:04:56 +0200 Subject: [PATCH 0129/1323] changelog: crash when resizing window, or scrolling --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eddc3bf..24403770 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,10 @@ subsequent motion and leave events. * Regression: “random” selected empty cells being highlighted as selected when they should not. +* Crash when either resizing the terminal window, or scrolling in the + scrollback history ([#1074][1074]) + +[1074]: https://codeberg.org/dnkl/foot/pulls/1074 ### Security From c753cf8f45e466165d64a173497ca1f6a3efd4a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 30 Aug 2022 17:48:04 +0200 Subject: [PATCH 0130/1323] url-mode: connect osc-8 links only when both ID and URI matches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this, two OSC-8 links with a matching ID would be connected even if their URIs weren’t the same. This is against the spec: The same id is only used for connecting character cells whose URIs is also the same. Character cells pointing to different URIs should never be underlined together when hovering over. https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda#hover-underlining-and-the-id-parameter --- CHANGELOG.md | 2 ++ url-mode.c | 16 ++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24403770..bcaac033 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,8 @@ selected when they should not. * Crash when either resizing the terminal window, or scrolling in the scrollback history ([#1074][1074]) +* OSC-8 URLs with matching IDs, but mismatching URIs being incorrectly + connected. [1074]: https://codeberg.org/dnkl/foot/pulls/1074 diff --git a/url-mode.c b/url-mode.c index 538b60f0..6fa16623 100644 --- a/url-mode.c +++ b/url-mode.c @@ -677,18 +677,23 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) if (count == 0) return; - uint64_t seen_ids[count]; char32_t *combos[count]; generate_key_combos(conf, count, combos); size_t combo_idx = 0; - size_t id_idx = 0; tll_foreach(*urls, it) { bool id_already_seen = false; - for (size_t i = 0; i < id_idx; i++) { - if (it->item.id == seen_ids[i]) { + /* Look for already processed URLs where both the URI and the + * ID matches */ + tll_foreach(*urls, it2) { + if (&it->item == &it2->item) + break; + + if (it->item.id == it2->item.id && + strcmp(it->item.url, it2->item.url) == 0) + { id_already_seen = true; break; } @@ -696,7 +701,6 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) if (id_already_seen) continue; - seen_ids[id_idx++] = it->item.id; /* * Scan previous URLs, and check if *this* URL matches any of @@ -730,7 +734,7 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) char *key = ac32tombs(it->item.key); xassert(key != NULL); - LOG_DBG("URL: %s (%s)", it->item.url, key); + LOG_DBG("URL: %s (key=%s, id=%"PRIu64")", it->item.url, key, it->item.id); free(key); } #endif From ccef435736a5364c63cfe6c1d64e7c4f5a66a0b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 20 Aug 2022 18:25:05 +0200 Subject: [PATCH 0131/1323] =?UTF-8?q?ci:=20codespell:=20ignore=20=E2=80=98?= =?UTF-8?q?zar=E2=80=99=20(user=20who=20contributed)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .builds/alpine-x64.yml | 2 +- .gitlab-ci.yml | 2 +- .woodpecker.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.builds/alpine-x64.yml b/.builds/alpine-x64.yml index 8f341f3d..933c7121 100644 --- a/.builds/alpine-x64.yml +++ b/.builds/alpine-x64.yml @@ -49,4 +49,4 @@ tasks: - codespell: | pip install codespell cd foot - ~/.local/bin/codespell -Lser,doas README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd + ~/.local/bin/codespell -Lser,doas,zar README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b2a459dc..28df1ccb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -109,4 +109,4 @@ codespell: - apk add python3 - apk add py3-pip - pip install codespell - - codespell -Lser,doas README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd + - codespell -Lser,doas,zar README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd diff --git a/.woodpecker.yml b/.woodpecker.yml index 8493aa47..284da761 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -9,7 +9,7 @@ pipeline: - apk add python3 - apk add py3-pip - pip install codespell - - codespell -Lser,doas README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd + - codespell -Lser,doas,zar README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd subprojects: when: From 736babcdbc10a20e9d620638adc2fec230a81e71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 17 Aug 2022 17:36:10 +0200 Subject: [PATCH 0132/1323] selection: never highlight selected, empty cells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes a regression, where empty cells "between" non-empty cells (i.e. non-trailing empty cells) sometimes were incorrectly highlighted. The idea has always been to highlight exactly those cells that will get extracted when they’re copied. This means we’ve not highlighted trailing empty cells, but we _have_ highlighted other empty cells, since they are converted to spaces when copied (whereas trailing empty cells are skipped). fa2d9f86996467ba33cc381f810ea966a4323381 changed how a selection is updated. That is, which cells gets marked as selected, and which ones gets unmarked. Since we no longer walk all the cells, but instead work with pixman regions representing selection diffs, we can no longer determine (with certainty) which empty cells should be selected and which shouldn’t. Before this patch (but after fa2d9f86996467ba33cc381f810ea966a4323381), we sometimes incorrectly highlighted empty cells that should not have been highlighted. This happened when we’ve first (correctly) highlighted a region of empty cells, but then shrink the selection such that all those empty cells should be de-selected. This patch changes the selection behavior to *never* highlight empty cells. This fixes the regression, but also means slightly different behavior, compared to pre-fa2d9f86996467ba33cc381f810ea966a4323381. The other alternative is to always highlight all empty cells. But, since I personally like the fact that we’re skipping trailing empty cells, I prefer the approach taken by this patch. --- CHANGELOG.md | 4 ++++ selection.c | 23 +++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fa35a63..be617671 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,8 @@ ### Changed * Window is now dimmed while in Unicode input mode. +* Selected empty cells are **never** highlighted as being + selected. They used to be, when followed by non-empty cells. ### Deprecated @@ -55,6 +57,8 @@ * Crash on buggy compositors (GNOME) that sometimes send pointer-enter events with a NULL surface. Foot now ignores these events, and the subsequent motion and leave events. +* Regression: “random” selected empty cells being highlighted as + selected when they should not. ### Security diff --git a/selection.c b/selection.c index 8c3a8357..2fd62f8a 100644 --- a/selection.c +++ b/selection.c @@ -737,8 +737,27 @@ mark_selected_region(struct terminal *term, pixman_box32_t *boxes, row->dirty = true; for (int c = box->x1, empty_count = 0; c < box->x2; c++) { - if (selected && row->cells[c].wc == 0) { - empty_count++; + if (row->cells[c].wc == 0) { + /* + * We used to highlight empty cells *if* they were + * followed by non-empty cell(s), since this + * corresponds to what gets extracted when the + * selection is copied (that is, empty cells + * “between” non-empty cells are converted to + * spaces). + * + * However, they way we handle selection updates + * (diffing the “old” selection area against the + * “new” one, using pixman regions), means we + * can’t correctly update the state of empty + * cells. The result is “random” empty cells being + * rendered as selected when they shouldn’t. + * + * “Fix” by *never* highlighting selected empty + * cells (they still get converted to spaces when + * copied, if followed by non-empty cells). + */ + /* empty_count++; */ continue; } From 6f1cf6fc56b906fb050009f587a5440188a69b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 22 Aug 2022 20:14:06 +0200 Subject: [PATCH 0133/1323] selection: once again highlight non-trailing empty cells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Foot 1.13.0 introduced a regression where non-trailing empty cells were highlighted inconsistently (cells that shouldn’t be highlighted, were, seemingly at random). 86663522d5915c0fdb2b8b132bacc247fcfd1fb8 “fixed” this by never highlighting *any* empty cells. This meant the behavior, compared to foot 1.12 and earlier, changed. In foot 1.12 and older versions, non-trailing empty cells were highlighted, as long as the selection covered at least one of the trailing non-empty cells. This patch restores that behavior. To understand how this works, lets first take a look at how selection works: When a selection is made, and updated (i.e. the mouse is dragged, or the selection is extended through RMB etc), we need to (un)tag and dirty the cells that are a) newly selected, or b) newly deselected. That is, we look at the diff between the “old” and the “new” selection, and only update those cells. This is for performance reasons: iterating the entire selection is not feasible with large selections. However, it also means we cannot reason about empty cells; we simply don’t know if an empty cells is a trailing empty cell, or a non-trailing one. Then, when we render a frame, we iterate all the *visible* and *selected* cells, once again tagging them as selected (this is needed since a selected cell might have lost its selected tag if the cell was written to, by the client application, after the selection was made). At this point, we *can* reason about empty cells. So, to restore the highlighting behavior to that of foot 1.12, we do this: When working with the selection diffs when a selection is updated, we don’t special case empty cells at all. Thus, all empty cells covered by the selection is highlighted, and dirtied. But, when rendering the frame, we _do_ special case them. The only difference (compared to foot 1.12) is that we *must* explicitly *clear* the selection tag, and dirty the empty cells. This is to ensure the empty cells that were incorrectly highlighted by the selection update algorithm, isn’t rendered as that. This does have a slight performance impact, as empty cells are now always re-rendered. The impact should however be small. --- CHANGELOG.md | 2 -- selection.c | 44 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be617671..0ba3aa6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,8 +45,6 @@ ### Changed * Window is now dimmed while in Unicode input mode. -* Selected empty cells are **never** highlighted as being - selected. They used to be, when followed by non-empty cells. ### Deprecated diff --git a/selection.c b/selection.c index 2fd62f8a..5b65310c 100644 --- a/selection.c +++ b/selection.c @@ -713,7 +713,8 @@ pixman_region_for_coords(const struct terminal *term, static void mark_selected_region(struct terminal *term, pixman_box32_t *boxes, - size_t count, bool selected, bool dirty_cells) + size_t count, bool selected, bool dirty_cells, + bool highlight_empty) { for (size_t i = 0; i < count; i++) { const pixman_box32_t *box = &boxes[i]; @@ -737,7 +738,9 @@ mark_selected_region(struct terminal *term, pixman_box32_t *boxes, row->dirty = true; for (int c = box->x1, empty_count = 0; c < box->x2; c++) { - if (row->cells[c].wc == 0) { + struct cell *cell = &row->cells[c]; + + if (cell->wc == 0 && !highlight_empty) { /* * We used to highlight empty cells *if* they were * followed by non-empty cell(s), since this @@ -757,7 +760,36 @@ mark_selected_region(struct terminal *term, pixman_box32_t *boxes, * cells (they still get converted to spaces when * copied, if followed by non-empty cells). */ - /* empty_count++; */ + empty_count++; + + /* + * When the selection is *modified*, empty cells + * are treated just like non-empty cells; they are + * marked as selected, and dirtied. + * + * This is due to how the algorithm for updating + * the selection works; it uses regions to + * calculate the difference between the “old” and + * the “new” selection. This makes it impossible + * to tell if an empty cell is a *trailing* empty + * cell (that should not be highlighted), or an + * empty cells between non-empty cells (that + * *should* be highlighted). + * + * Then, when a frame is rendered, we loop the + * *visibible* cells that belong to the + * selection. At this point, we *can* tell if an + * empty cell is trailing or not. + * + * So, what we need to do is check if a + * ‘selected’, and empty cell has been marked as + * selected, temporarily unmark (forcing it dirty, + * to ensure it gets re-rendered). If it is *not* + * a trailing empty cell, it will get re-tagged as + * selected in the for-loop below. + */ + cell->attrs.clean = false; + cell->attrs.selected = false; continue; } @@ -810,10 +842,10 @@ selection_modify(struct terminal *term, struct coord start, struct coord end) pixman_box32_t *boxes = NULL; boxes = pixman_region32_rectangles(&no_longer_selected, &n_rects); - mark_selected_region(term, boxes, n_rects, false, true); + mark_selected_region(term, boxes, n_rects, false, true, true); boxes = pixman_region32_rectangles(&newly_selected, &n_rects); - mark_selected_region(term, boxes, n_rects, true, true); + mark_selected_region(term, boxes, n_rects, true, true, true); pixman_region32_fini(&newly_selected); pixman_region32_fini(&no_longer_selected); @@ -1078,7 +1110,7 @@ selection_dirty_cells(struct terminal *term) int n_rects = -1; pixman_box32_t *boxes = pixman_region32_rectangles(&visible_and_selected, &n_rects); - mark_selected_region(term, boxes, n_rects, true, false); + mark_selected_region(term, boxes, n_rects, true, false, false); pixman_region32_fini(&visible_and_selected); pixman_region32_fini(&view); From 03d3887e6bb964e9599ce84f5e1548df224371fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 23 Aug 2022 16:38:39 +0200 Subject: [PATCH 0134/1323] ci (sr.ht): pull directly from git.sr.ht --- .builds/alpine-x64.yml | 2 +- .builds/alpine-x86.yml.disabled | 2 +- .builds/freebsd-x64.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.builds/alpine-x64.yml b/.builds/alpine-x64.yml index 933c7121..2e2ec2c4 100644 --- a/.builds/alpine-x64.yml +++ b/.builds/alpine-x64.yml @@ -24,7 +24,7 @@ packages: - font-noto-emoji sources: - - https://codeberg.org/dnkl/foot + - https://git.sr.ht/~dnkl/foot # triggers: # - action: email diff --git a/.builds/alpine-x86.yml.disabled b/.builds/alpine-x86.yml.disabled index 22a9e637..6d790227 100644 --- a/.builds/alpine-x86.yml.disabled +++ b/.builds/alpine-x86.yml.disabled @@ -23,7 +23,7 @@ packages: - font-noto-emoji sources: - - https://codeberg.org/dnkl/foot + - https://git.sr.ht/~dnkl/foot # triggers: # - action: email diff --git a/.builds/freebsd-x64.yml b/.builds/freebsd-x64.yml index 89803a6e..9642f96d 100644 --- a/.builds/freebsd-x64.yml +++ b/.builds/freebsd-x64.yml @@ -19,7 +19,7 @@ packages: - noto-emoji sources: - - https://codeberg.org/dnkl/foot + - https://git.sr.ht/~dnkl/foot # triggers: # - action: email From 2354298ecac8e08ecce8ff8b208cda0f0c6f5257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 26 Aug 2022 17:48:00 +0200 Subject: [PATCH 0135/1323] selection: restore <= 1.12 behavior in block selection wrt empty cells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit That is, highlight empty cells, regardless of whether they’re trailing or not. --- selection.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/selection.c b/selection.c index 5b65310c..5bf72e62 100644 --- a/selection.c +++ b/selection.c @@ -1110,7 +1110,9 @@ selection_dirty_cells(struct terminal *term) int n_rects = -1; pixman_box32_t *boxes = pixman_region32_rectangles(&visible_and_selected, &n_rects); - mark_selected_region(term, boxes, n_rects, true, false, false); + + const bool highlight_empty = term->selection.kind == SELECTION_BLOCK; + mark_selected_region(term, boxes, n_rects, true, false, highlight_empty); pixman_region32_fini(&visible_and_selected); pixman_region32_fini(&view); From d810e4fc8eb3123e427f1453b269ced9608222c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 26 Aug 2022 21:07:20 +0200 Subject: [PATCH 0136/1323] selection: mark_selected_region(): use an enum to encode how the cells are to be updated --- selection.c | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/selection.c b/selection.c index 5bf72e62..c94686e1 100644 --- a/selection.c +++ b/selection.c @@ -711,11 +711,26 @@ pixman_region_for_coords(const struct terminal *term, } } +enum mark_selection_variant { + MARK_SELECTION_MARK_AND_DIRTY, + MARK_SELECTION_UNMARK_AND_DIRTY, + MARK_SELECTION_MARK_FOR_RENDER, +}; + static void mark_selected_region(struct terminal *term, pixman_box32_t *boxes, - size_t count, bool selected, bool dirty_cells, - bool highlight_empty) + size_t count, enum mark_selection_variant mark_variant) { + const bool selected = + mark_variant == MARK_SELECTION_MARK_AND_DIRTY || + mark_variant == MARK_SELECTION_MARK_FOR_RENDER; + const bool dirty_cells = + mark_variant == MARK_SELECTION_MARK_AND_DIRTY || + mark_variant == MARK_SELECTION_UNMARK_AND_DIRTY; + const bool highlight_empty = + mark_variant != MARK_SELECTION_MARK_FOR_RENDER || + term->selection.kind == SELECTION_BLOCK; + for (size_t i = 0; i < count; i++) { const pixman_box32_t *box = &boxes[i]; @@ -842,10 +857,10 @@ selection_modify(struct terminal *term, struct coord start, struct coord end) pixman_box32_t *boxes = NULL; boxes = pixman_region32_rectangles(&no_longer_selected, &n_rects); - mark_selected_region(term, boxes, n_rects, false, true, true); + mark_selected_region(term, boxes, n_rects, MARK_SELECTION_UNMARK_AND_DIRTY); boxes = pixman_region32_rectangles(&newly_selected, &n_rects); - mark_selected_region(term, boxes, n_rects, true, true, true); + mark_selected_region(term, boxes, n_rects, MARK_SELECTION_MARK_AND_DIRTY); pixman_region32_fini(&newly_selected); pixman_region32_fini(&no_longer_selected); @@ -1110,9 +1125,7 @@ selection_dirty_cells(struct terminal *term) int n_rects = -1; pixman_box32_t *boxes = pixman_region32_rectangles(&visible_and_selected, &n_rects); - - const bool highlight_empty = term->selection.kind == SELECTION_BLOCK; - mark_selected_region(term, boxes, n_rects, true, false, highlight_empty); + mark_selected_region(term, boxes, n_rects, MARK_SELECTION_MARK_FOR_RENDER); pixman_region32_fini(&visible_and_selected); pixman_region32_fini(&view); From 649d56a4e5142c9ab8ea24f4d1acbd4a49d534f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 29 Aug 2022 20:46:19 +0200 Subject: [PATCH 0137/1323] =?UTF-8?q?grid:=20when=20setting=20the=20new=20?= =?UTF-8?q?viewport,=20ensure=20it=E2=80=99s=20correctly=20bounded?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Do this by using scrollback relative coordinates, and ensure the new viewport is not larger than (grid_rows - screen_rows), as that would mean the viewport crosses the scrollback wrap-around. --- grid.c | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/grid.c b/grid.c index f40803e1..f229effa 100644 --- a/grid.c +++ b/grid.c @@ -1016,23 +1016,6 @@ grid_resize_and_reflow( new_grid[idx] = grid_row_alloc(new_cols, true); } - grid->view = view_follows ? grid->offset : viewport.row; - - /* If enlarging the window, the old viewport may be too far down, - * with unallocated rows. Make sure this cannot happen */ - while (true) { - int idx = (grid->view + new_screen_rows - 1) & (new_rows - 1); - if (new_grid[idx] != NULL) - break; - grid->view--; - if (grid->view < 0) - grid->view += new_rows; - } - for (size_t r = 0; r < new_screen_rows; r++) { - int UNUSED idx = (grid->view + r) & (new_rows - 1); - xassert(new_grid[idx] != NULL); - } - /* Free old grid (rows already free:d) */ free(grid->rows); @@ -1040,6 +1023,17 @@ grid_resize_and_reflow( grid->num_rows = new_rows; grid->num_cols = new_cols; + /* + * Set new viewport, making sure it’s not too far down. + * + * This is done by using scrollback-start relative cooardinates, + * and bounding the new viewport to (grid_rows - screen_rows). + */ + int sb_view = grid_row_abs_to_sb( + grid, new_screen_rows, view_follows ? grid->offset : viewport.row); + grid->view = grid_row_sb_to_abs( + grid, new_screen_rows, min(sb_view, new_rows - new_screen_rows)); + /* Convert absolute coordinates to screen relative */ cursor.row -= grid->offset; while (cursor.row < 0) From 22989ef9b3c604f5ccc30a6ba3cb11263f7e1a80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 29 Aug 2022 20:47:33 +0200 Subject: [PATCH 0138/1323] =?UTF-8?q?grid:=20reflow:=20don=E2=80=99t=20lin?= =?UTF-8?q?e-wrap=20the=20last=20row?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this patch, we would line-wrap the last row, just like any other row, and then afterwards try to reverse this, by adjusting the offset and free:ing and NULL:ing the "last row". The problem with this is if the scrollback is full. In this case, the row we’re freeing is the first row in the scrollback history. This means we’ll crash as soon as the viewport is moved to the top of the scrollback. The fix is fairly, simple. Skip the post-processing logic, and instead detect when we’re line-wrapping the last row, and skip the call to line_wrap(). This way, the last row in the new grid corresponds to the last row in the old grid. --- grid.c | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/grid.c b/grid.c index f229effa..bbac3395 100644 --- a/grid.c +++ b/grid.c @@ -935,13 +935,14 @@ grid_resize_and_reflow( start += cols; } - if (old_row->linebreak) { /* Erase the remaining cells */ memset(&new_row->cells[new_col_idx], 0, (new_cols - new_col_idx) * sizeof(new_row->cells[0])); new_row->linebreak = true; - line_wrap(); + + if (r + 1 < old_rows) + line_wrap(); } grid_row_free(old_grid[old_row_idx]); @@ -985,25 +986,6 @@ grid_resize_and_reflow( /* Set offset such that the last reflowed row is at the bottom */ grid->offset = new_row_idx - new_screen_rows + 1; - if (new_col_idx == 0) { - int next_to_last_new_row_idx = new_row_idx - 1; - next_to_last_new_row_idx += new_rows; - next_to_last_new_row_idx &= new_rows - 1; - - const struct row *next_to_last_row = new_grid[next_to_last_new_row_idx]; - if (next_to_last_row != NULL && next_to_last_row->linebreak) { - /* - * The next to last row is actually the *last* row. But we - * ended the reflow with a line-break, causing an empty - * row to be inserted at the bottom. Undo this. - */ - /* TODO: can we detect this in the reflow loop above instead? */ - grid->offset--; - grid_row_free(new_grid[new_row_idx]); - new_grid[new_row_idx] = NULL; - } - } - while (grid->offset < 0) grid->offset += new_rows; while (new_grid[grid->offset] == NULL) From 23871d4db58d15e33e0693010373c426fc50688e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 29 Aug 2022 21:03:21 +0200 Subject: [PATCH 0139/1323] =?UTF-8?q?grid:=20reflow:=20assert=20there=20ar?= =?UTF-8?q?en=E2=80=99t=20any=20open=20URIs=20on=20the=20last=20row?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- grid.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/grid.c b/grid.c index bbac3395..7bfef5cb 100644 --- a/grid.c +++ b/grid.c @@ -943,6 +943,21 @@ grid_resize_and_reflow( if (r + 1 < old_rows) line_wrap(); + else if (new_row->extra != NULL && + new_row->extra->uri_ranges.count > 0) + { + /* + * line_wrap() "closes" still-open URIs. Since this is + * the *last* row, and since we’re line-breaking due + * to a hard line-break (rather than running out of + * cells in the "new_row"), there shouldn’t be an open + * URI (it would have been closed when we reached the + * end of the URI while reflowing the last "old" + * row). + */ + uint32_t last_idx = new_row->extra->uri_ranges.count - 1; + xassert(new_row->extra->uri_ranges.v[last_idx].end >= 0); + } } grid_row_free(old_grid[old_row_idx]); From 688bf1bd59df3c692b0891aaf46b6844ea415e65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 29 Aug 2022 21:04:56 +0200 Subject: [PATCH 0140/1323] changelog: crash when resizing window, or scrolling --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ba3aa6b..22267556 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,10 @@ subsequent motion and leave events. * Regression: “random” selected empty cells being highlighted as selected when they should not. +* Crash when either resizing the terminal window, or scrolling in the + scrollback history ([#1074][1074]) + +[1074]: https://codeberg.org/dnkl/foot/pulls/1074 ### Security From 5706141d0a5250af761f29989dad8161ed1cf2b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 30 Aug 2022 17:48:04 +0200 Subject: [PATCH 0141/1323] url-mode: connect osc-8 links only when both ID and URI matches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this, two OSC-8 links with a matching ID would be connected even if their URIs weren’t the same. This is against the spec: The same id is only used for connecting character cells whose URIs is also the same. Character cells pointing to different URIs should never be underlined together when hovering over. https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda#hover-underlining-and-the-id-parameter --- CHANGELOG.md | 2 ++ url-mode.c | 16 ++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22267556..fe6e7fcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,8 @@ selected when they should not. * Crash when either resizing the terminal window, or scrolling in the scrollback history ([#1074][1074]) +* OSC-8 URLs with matching IDs, but mismatching URIs being incorrectly + connected. [1074]: https://codeberg.org/dnkl/foot/pulls/1074 diff --git a/url-mode.c b/url-mode.c index 538b60f0..6fa16623 100644 --- a/url-mode.c +++ b/url-mode.c @@ -677,18 +677,23 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) if (count == 0) return; - uint64_t seen_ids[count]; char32_t *combos[count]; generate_key_combos(conf, count, combos); size_t combo_idx = 0; - size_t id_idx = 0; tll_foreach(*urls, it) { bool id_already_seen = false; - for (size_t i = 0; i < id_idx; i++) { - if (it->item.id == seen_ids[i]) { + /* Look for already processed URLs where both the URI and the + * ID matches */ + tll_foreach(*urls, it2) { + if (&it->item == &it2->item) + break; + + if (it->item.id == it2->item.id && + strcmp(it->item.url, it2->item.url) == 0) + { id_already_seen = true; break; } @@ -696,7 +701,6 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) if (id_already_seen) continue; - seen_ids[id_idx++] = it->item.id; /* * Scan previous URLs, and check if *this* URL matches any of @@ -730,7 +734,7 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) char *key = ac32tombs(it->item.key); xassert(key != NULL); - LOG_DBG("URL: %s (%s)", it->item.url, key); + LOG_DBG("URL: %s (key=%s, id=%"PRIu64")", it->item.url, key, it->item.id); free(key); } #endif From 864de72b171ea6e6e002ec119f017ed57fecc0b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 31 Aug 2022 19:17:38 +0200 Subject: [PATCH 0142/1323] changelog: prepare for 1.13.1 --- CHANGELOG.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe6e7fcb..ff203e04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.13.1](#1-13-1) * [1.13.0](#1-13-0) * [1.12.1](#1-12-1) * [1.12.0](#1-12-0) @@ -40,15 +40,13 @@ * [1.2.0](#1-2-0) -## Unreleased -### Added +## 1.13.1 + ### Changed * Window is now dimmed while in Unicode input mode. -### Deprecated -### Removed ### Fixed * Compiling against wayland-protocols < 1.25 @@ -65,10 +63,6 @@ [1074]: https://codeberg.org/dnkl/foot/pulls/1074 -### Security -### Contributors - - ## 1.13.0 ### Added From cd1933baf12eeef82e04a926f9150ca815d54768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 31 Aug 2022 19:19:15 +0200 Subject: [PATCH 0143/1323] meson: bump version to 1.13.1 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index c5597113..360db8fa 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.13.0', + version: '1.13.1', license: 'MIT', meson_version: '>=0.58.0', default_options: [ From 2d1ded183ac1c79b93f2674fed3ec7c0def65075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 3 Sep 2022 12:16:41 +0200 Subject: [PATCH 0144/1323] =?UTF-8?q?config:=20change=20default=20?= =?UTF-8?q?=E2=80=98pad=E2=80=99=20to=200x0=20(i.e.=20no=20padding)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + config.c | 4 ++-- doc/foot.ini.5.scd | 2 +- foot.ini | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cca3d366..15b0e55c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ * Default color theme from a variant of the Zenburn theme, to a variant of the Solarized dark theme. +* Default `pad` from 2x2 to 0x0 (i.e. no padding at all). ### Deprecated diff --git a/config.c b/config.c index 3ecb3db5..87452cd6 100644 --- a/config.c +++ b/config.c @@ -2821,8 +2821,8 @@ config_load(struct config *conf, const char *conf_path, .width = 700, .height = 500, }, - .pad_x = 2, - .pad_y = 2, + .pad_x = 0, + .pad_y = 0, .resize_delay_ms = 100, .bold_in_bright = { .enabled = false, diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index a0cf69f5..7382a1d2 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -210,7 +210,7 @@ commented out will usually be installed to */etc/xdg/foot/foot.ini*. To instead center the grid content, append *center* (e.g. *pad=5x5 center*). - Default: _2x2_. + Default: _0x0_. *resize-delay-ms* Time, in milliseconds, of "idle time" before foot sends the new diff --git a/foot.ini b/foot.ini index 926ed499..3b8b6f16 100644 --- a/foot.ini +++ b/foot.ini @@ -24,7 +24,7 @@ # initial-window-size-pixels=700x500 # Or, # initial-window-size-chars= # initial-window-mode=windowed -# pad=2x2 # optionally append 'center' +# pad=0x0 # optionally append 'center' # resize-delay-ms=100 # notify=notify-send -a ${app-id} -i ${app-id} ${title} ${body} From c311229b9e4374d81d72e7c20ba51075267e2443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 6 Sep 2022 17:37:19 +0200 Subject: [PATCH 0145/1323] csi: damage *viewport* when exiting the alt screen This fixes a redraw issue when the normal screen were somewhere in the scrollback history when exiting the alt screen. --- csi.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csi.c b/csi.c index 659839f0..4a023554 100644 --- a/csi.c +++ b/csi.c @@ -486,7 +486,7 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) } tll_free(term->alt.scroll_damage); - term_damage_all(term); + term_damage_view(term); } term_update_ascii_printer(term); break; From d2e67689ea0d5ec986b66936188e0557c1f04858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 5 Sep 2022 19:23:40 +0200 Subject: [PATCH 0146/1323] =?UTF-8?q?terminal:=20don=E2=80=99t=20unref=20a?= =?UTF-8?q?=20not-yet-referenced=20key-binding=20set?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Key-binding sets are bound to a seat/configuration pair. The conf reference is done when a new terminal instance is created. When that same terminal instance is destroyed, the key binding set is unref:ed. If the terminal instance is destroyed *before* the key binding set has been referenced, we’ll still unref it. This creates an imbalance. In particular, when the there is exactly one other terminal instance referencing that same key binding set, that terminal instance will trigger a foot server crash as soon as it receives a key press/release event. This happens because the next-to-last terminal instance brought the reference count of the binding set down to 0, causing it to be free:d. Thus, we *must* reference the binding set *before* we can error out (when instantiating a new terminal instance). At this point, we don’t yet have a valid terminal instance. But, that’s ok, because all the key_binding_new_for_term() did with the terminal instance was get the "struct wayland" and "struct config" pointers. So, rename the function and simply pass these pointers explicitly. Similarly, change key_binding_for() to take a "struct config" pointer, rather than a "struct terminal" pointer. Also rename key_binding_unref_term() -> key_binding_unref(). --- CHANGELOG.md | 6 ++++++ input.c | 4 ++-- key-binding.c | 22 +++++++--------------- key-binding.h | 14 ++++++++------ pgo/pgo.c | 9 +++++---- terminal.c | 11 +++++++---- 6 files changed, 35 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15b0e55c..b54016de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,12 @@ ### Deprecated ### Removed ### Fixed + +* Crash in `foot --server` on key press, after another `footclient` + has terminated very early (for example, by trying to launch a + non-existing shell/client). + + ### Security ### Contributors diff --git a/input.c b/input.c index 50336679..3aa8b004 100644 --- a/input.c +++ b/input.c @@ -1406,7 +1406,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, seat->kbd.xkb_keymap, key, layout_idx, 0, &raw_syms); const struct key_binding_set *bindings = key_binding_for( - seat->wayl->key_binding_manager, term, seat); + seat->wayl->key_binding_manager, term->conf, seat); xassert(bindings != NULL); if (pressed) { @@ -2335,7 +2335,7 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, /* Seat has keyboard - use mouse bindings *with* modifiers */ const struct key_binding_set *bindings = key_binding_for( - wayl->key_binding_manager, term, seat); + wayl->key_binding_manager, term->conf, seat); xassert(bindings != NULL); xkb_mod_mask_t mods; diff --git a/key-binding.c b/key-binding.c index 2135abbc..1876a885 100644 --- a/key-binding.c +++ b/key-binding.c @@ -80,17 +80,14 @@ key_binding_new_for_seat(struct key_binding_manager *mgr, } void -key_binding_new_for_term(struct key_binding_manager *mgr, - const struct terminal *term) +key_binding_new_for_conf(struct key_binding_manager *mgr, + const struct wayland *wayl, const struct config *conf) { - const struct config *conf = term->conf; - const struct wayland *wayl = term->wl; - tll_foreach(wayl->seats, it) { struct seat *seat = &it->item; struct key_set *existing = - (struct key_set *)key_binding_for(mgr, term, seat); + (struct key_set *)key_binding_for(mgr, conf, seat); if (existing != NULL) { existing->conf_ref_count++; @@ -116,21 +113,19 @@ key_binding_new_for_term(struct key_binding_manager *mgr, /* Chances are high this set will be requested next */ mgr->last_used_set = &tll_back(mgr->binding_sets); - LOG_DBG("new (term): set=%p, seat=%p, conf=%p, ref-count=1", + LOG_DBG("new (conf): set=%p, seat=%p, conf=%p, ref-count=1", (void *)&tll_back(mgr->binding_sets), (void *)set.seat, (void *)set.conf); } - LOG_DBG("new (term): total number of sets: %zu", + LOG_DBG("new (conf): total number of sets: %zu", tll_length(mgr->binding_sets)); } struct key_binding_set * NOINLINE -key_binding_for(struct key_binding_manager *mgr, const struct terminal *term, +key_binding_for(struct key_binding_manager *mgr, const struct config *conf, const struct seat *seat) { - const struct config *conf = term->conf; - struct key_set *last_used = mgr->last_used_set; if (last_used != NULL && last_used->conf == conf && @@ -192,11 +187,8 @@ key_binding_remove_seat(struct key_binding_manager *mgr, } void -key_binding_unref_term(struct key_binding_manager *mgr, - const struct terminal *term) +key_binding_unref(struct key_binding_manager *mgr, const struct config *conf) { - const struct config *conf = term->conf; - tll_foreach(mgr->binding_sets, it) { struct key_set *set = &it->item; diff --git a/key-binding.h b/key-binding.h index 448500c1..f607644f 100644 --- a/key-binding.h +++ b/key-binding.h @@ -110,6 +110,7 @@ typedef tll(struct key_binding) key_binding_list_t; struct terminal; struct seat; +struct wayland; struct key_binding_set { key_binding_list_t key; @@ -127,20 +128,21 @@ void key_binding_manager_destroy(struct key_binding_manager *mgr); void key_binding_new_for_seat( struct key_binding_manager *mgr, const struct seat *seat); -void key_binding_new_for_term( - struct key_binding_manager *mgr, const struct terminal *term); +void key_binding_new_for_conf( + struct key_binding_manager *mgr, const struct wayland *wayl, + const struct config *conf); -/* Returns the set of key bindings associated with this seat/term pair */ +/* Returns the set of key bindings associated with this seat/conf pair */ struct key_binding_set *key_binding_for( - struct key_binding_manager *mgr, const struct terminal *term, + struct key_binding_manager *mgr, const struct config *conf, const struct seat *seat); /* Remove all key bindings tied to the specified seat */ void key_binding_remove_seat( struct key_binding_manager *mgr, const struct seat *seat); -void key_binding_unref_term( - struct key_binding_manager *mgr, const struct terminal *term); +void key_binding_unref( + struct key_binding_manager *mgr, const struct config *conf); void key_binding_load_keymap( struct key_binding_manager *mgr, const struct seat *seat); diff --git a/pgo/pgo.c b/pgo/pgo.c index 3073c27c..7f6f758b 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -178,15 +178,16 @@ static bool kbd_initialized = false; struct key_binding_set * key_binding_for( - struct key_binding_manager *mgr, const struct terminal *term, + struct key_binding_manager *mgr, const struct config *conf, const struct seat *seat) { return &kbd; } void -key_binding_new_for_term( - struct key_binding_manager *mgr, const struct terminal *term) +key_binding_new_for_conf( + struct key_binding_manager *mgr, const struct wayland *wayl, + const struct config *conf) { if (!kbd_initialized) { kbd_initialized = true; @@ -201,7 +202,7 @@ key_binding_new_for_term( } void -key_binding_unref_term(struct key_binding_manager *mgr, const struct terminal *term) +key_binding_unref(struct key_binding_manager *mgr, const struct config *conf) { } diff --git a/terminal.c b/terminal.c index 0763fb52..3445b89f 100644 --- a/terminal.c +++ b/terminal.c @@ -1089,6 +1089,11 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, goto close_fds; } + /* Need to register *very* early (before the first “goto err”), to + * ensure term_destroy() doesn’t unref a key-binding we haven’t + * yet ref:d */ + key_binding_new_for_conf(wayl->key_binding_manager, wayl, conf); + int ptmx_flags; if ((ptmx_flags = fcntl(ptmx, F_GETFL)) < 0 || fcntl(ptmx, F_SETFL, ptmx_flags | O_NONBLOCK) < 0) @@ -1266,8 +1271,6 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, memcpy(term->colors.table, term->conf->colors.table, sizeof(term->colors.table)); - key_binding_new_for_term(wayl->key_binding_manager, term); - /* Initialize the Wayland window backend */ if ((term->window = wayl_win_init(term, token)) == NULL) goto err; @@ -1583,7 +1586,7 @@ term_destroy(struct terminal *term) if (term == NULL) return 0; - key_binding_unref_term(term->wl->key_binding_manager, term); + key_binding_unref(term->wl->key_binding_manager, term->conf); tll_foreach(term->wl->terms, it) { if (it->item == term) { @@ -2885,7 +2888,7 @@ term_mouse_grabbed(const struct terminal *term, const struct seat *seat) get_current_modifiers(seat, &mods, NULL, 0); const struct key_binding_set *bindings = - key_binding_for(term->wl->key_binding_manager, term, seat); + key_binding_for(term->wl->key_binding_manager, term->conf, seat); const xkb_mod_mask_t override_modmask = bindings->selection_overrides; bool override_mods_pressed = (mods & override_modmask) == override_modmask; From 1c072856ebf12419378c5098ad543c497197c6da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 10 Sep 2022 12:04:39 +0200 Subject: [PATCH 0147/1323] input: pipe-*: set current CWD when spawning the sub-process Closes #1166 --- CHANGELOG.md | 4 ++++ input.c | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b54016de..793f6376 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,10 @@ * Default color theme from a variant of the Zenburn theme, to a variant of the Solarized dark theme. * Default `pad` from 2x2 to 0x0 (i.e. no padding at all). +* Current working directory (as set by OSC-7) is now passed to the + program executed by the `pipe-*` key bindings ([#1166][166]). + +[1166]: https://codeberg.org/dnkl/foot/issues/1166 ### Deprecated diff --git a/input.c b/input.c index 3aa8b004..0a3773bc 100644 --- a/input.c +++ b/input.c @@ -283,7 +283,7 @@ execute_binding(struct seat *seat, struct terminal *term, } } - if (!spawn(term->reaper, NULL, binding->aux->pipe.args, + if (!spawn(term->reaper, term->cwd, binding->aux->pipe.args, pipe_fd[0], stdout_fd, stderr_fd, NULL)) goto pipe_err; From 8dcfa259a2a4596311b9c271043456b49275d0f8 Mon Sep 17 00:00:00 2001 From: Craig Barnes Date: Sat, 17 Sep 2022 06:34:25 +0100 Subject: [PATCH 0148/1323] config: fix "maybe-uninitialized" error when compiling with CFLAGS=-Og MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using GCC 12.2.0 with the following build steps: CFLAGS=-Og meson bld ninja -C bld ...produces the compiler error: ../config.c:2000:18: error: ‘sym_equal’ may be used uninitialized [-Werror=maybe-uninitialized] This commit fixes that by using BUG() to assert that all possible values are accounted for in the offending switch statement. --- config.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config.c b/config.c index 87452cd6..0ba237a0 100644 --- a/config.c +++ b/config.c @@ -2008,6 +2008,9 @@ resolve_key_binding_collisions(struct config *conf, const char *section_name, sym_equal = (binding1->m.button == binding2->m.button && binding1->m.count == binding2->m.count); break; + + default: + BUG("unhandled key binding type"); } if (!mods_equal || !sym_equal) From 46f6bad728efbeee04cfa98d4a31c4201db07ff8 Mon Sep 17 00:00:00 2001 From: Craig Barnes Date: Sat, 17 Sep 2022 07:17:12 +0100 Subject: [PATCH 0149/1323] csi: reply with 4 instead of 2 for DECRQM queries of unsupported modes The status value 4 means "permanently reset", as opposed to 2, which means "reset". The former implies that there's no way to enable the mode because it's unsupported (but recognized). Note: this commit also fixes an unrelated typo in CHANGELOG.md. --- CHANGELOG.md | 7 +++- csi.c | 98 +++++++++++++++++++++++++++++----------------------- 2 files changed, 61 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 793f6376..ffbdd08b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,7 +56,10 @@ variant of the Solarized dark theme. * Default `pad` from 2x2 to 0x0 (i.e. no padding at all). * Current working directory (as set by OSC-7) is now passed to the - program executed by the `pipe-*` key bindings ([#1166][166]). + program executed by the `pipe-*` key bindings ([#1166][1166]). +* `DECRPM` replies (to `DECRQM` queries) now report a value of `4` + ("permanently reset") instead of `2` ("reset") for DEC private + modes that are known but unsupported. [1166]: https://codeberg.org/dnkl/foot/issues/1166 @@ -73,6 +76,8 @@ ### Security ### Contributors +* Craig Barnes + ## 1.13.1 diff --git a/csi.c b/csi.c index 4a023554..2eacaaa9 100644 --- a/csi.c +++ b/csi.c @@ -535,47 +535,65 @@ decrst(struct terminal *term, unsigned param) decset_decrst(term, param, false); } -static bool -decrqm(const struct terminal *term, unsigned param, bool *enabled) +/* + * These values represent the current state of a DEC private mode, + * as returned in the DECRPM reply to a DECRQM query. + */ +enum decrpm_status { + DECRPM_NOT_RECOGNIZED = 0, + DECRPM_SET = 1, + DECRPM_RESET = 2, + DECRPM_PERMANENTLY_SET = 3, + DECRPM_PERMANENTLY_RESET = 4, +}; + +static enum decrpm_status +decrpm(bool enabled) +{ + return enabled ? DECRPM_SET : DECRPM_RESET; +} + +static enum decrpm_status +decrqm(const struct terminal *term, unsigned param) { switch (param) { - case 1: *enabled = term->cursor_keys_mode == CURSOR_KEYS_APPLICATION; return true; - case 3: *enabled = false; return true; - case 4: *enabled = false; return true; - case 5: *enabled = term->reverse; return true; - case 6: *enabled = term->origin; return true; - case 7: *enabled = term->auto_margin; return true; - case 9: *enabled = false; /* term->mouse_tracking == MOUSE_X10; */ return true; - case 12: *enabled = term->cursor_blink.decset; return true; - case 25: *enabled = !term->hide_cursor; return true; - case 45: *enabled = term->reverse_wrap; return true; - case 66: *enabled = term->keypad_keys_mode == KEYPAD_APPLICATION; return true; - case 80: *enabled = !term->sixel.scrolling; return true; - case 1000: *enabled = term->mouse_tracking == MOUSE_CLICK; return true; - case 1001: *enabled = false; return true; - case 1002: *enabled = term->mouse_tracking == MOUSE_DRAG; return true; - case 1003: *enabled = term->mouse_tracking == MOUSE_MOTION; return true; - case 1004: *enabled = term->focus_events; return true; - case 1005: *enabled = false; /* term->mouse_reporting == MOUSE_UTF8; */ return true; - case 1006: *enabled = term->mouse_reporting == MOUSE_SGR; return true; - case 1007: *enabled = term->alt_scrolling; return true; - case 1015: *enabled = term->mouse_reporting == MOUSE_URXVT; return true; - case 1016: *enabled = term->mouse_reporting == MOUSE_SGR_PIXELS; return true; - case 1034: *enabled = term->meta.eight_bit; return true; - case 1035: *enabled = term->num_lock_modifier; return true; - case 1036: *enabled = term->meta.esc_prefix; return true; - case 1042: *enabled = term->bell_action_enabled; return true; + case 1: return decrpm(term->cursor_keys_mode == CURSOR_KEYS_APPLICATION); + case 3: return DECRPM_PERMANENTLY_RESET; + case 4: return DECRPM_PERMANENTLY_RESET; + case 5: return decrpm(term->reverse); + case 6: return decrpm(term->origin); + case 7: return decrpm(term->auto_margin); + case 9: return DECRPM_PERMANENTLY_RESET; /* term->mouse_tracking == MOUSE_X10; */ + case 12: return decrpm(term->cursor_blink.decset); + case 25: return decrpm(!term->hide_cursor); + case 45: return decrpm(term->reverse_wrap); + case 66: return decrpm(term->keypad_keys_mode == KEYPAD_APPLICATION); + case 80: return decrpm(!term->sixel.scrolling); + case 1000: return decrpm(term->mouse_tracking == MOUSE_CLICK); + case 1001: return DECRPM_PERMANENTLY_RESET; + case 1002: return decrpm(term->mouse_tracking == MOUSE_DRAG); + case 1003: return decrpm(term->mouse_tracking == MOUSE_MOTION); + case 1004: return decrpm(term->focus_events); + case 1005: return DECRPM_PERMANENTLY_RESET; /* term->mouse_reporting == MOUSE_UTF8; */ + case 1006: return decrpm(term->mouse_reporting == MOUSE_SGR); + case 1007: return decrpm(term->alt_scrolling); + case 1015: return decrpm(term->mouse_reporting == MOUSE_URXVT); + case 1016: return decrpm(term->mouse_reporting == MOUSE_SGR_PIXELS); + case 1034: return decrpm(term->meta.eight_bit); + case 1035: return decrpm(term->num_lock_modifier); + case 1036: return decrpm(term->meta.esc_prefix); + case 1042: return decrpm(term->bell_action_enabled); case 47: /* FALLTHROUGH */ case 1047: /* FALLTHROUGH */ - case 1049: *enabled = term->grid == &term->alt; return true; - case 1070: *enabled = term->sixel.use_private_palette; return true; - case 2004: *enabled = term->bracketed_paste; return true; - case 2026: *enabled = term->render.app_sync_updates.enabled; return true; - case 8452: *enabled = term->sixel.cursor_right_of_graphics; return true; - case 737769: *enabled = term_ime_is_enabled(term); return true; + case 1049: return decrpm(term->grid == &term->alt); + case 1070: return decrpm(term->sixel.use_private_palette); + case 2004: return decrpm(term->bracketed_paste); + case 2026: return decrpm(term->render.app_sync_updates.enabled); + case 8452: return decrpm(term->sixel.cursor_right_of_graphics); + case 737769: return decrpm(term_ime_is_enabled(term)); } - return false; + return DECRPM_NOT_RECOGNIZED; } static void @@ -1721,15 +1739,9 @@ csi_dispatch(struct terminal *term, uint8_t final) * 3 - permanently set * 4 - permantently reset */ - bool enabled; - unsigned value; - if (decrqm(term, param, &enabled)) - value = enabled ? 1 : 2; - else - value = 0; - + unsigned status = decrqm(term, param); char reply[32]; - size_t n = xsnprintf(reply, sizeof(reply), "\033[?%u;%u$y", param, value); + size_t n = xsnprintf(reply, sizeof(reply), "\033[?%u;%u$y", param, status); term_to_slave(term, reply, n); break; From 335612cfa4ea090874354c7080f492b2b28a136b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Magyar?= Date: Sat, 20 Aug 2022 12:57:38 +0200 Subject: [PATCH 0150/1323] chore: add appstream file Appstream file is usefull for many package management tools to show additional metadata and screenshots. Also it would solve the packaging for flathub. References: https://codeberg.org/dnkl/foot/issues/1129#issuecomment-602089 --- org.codeberg.dnkl.foot.metainfo.xml | 45 +++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 org.codeberg.dnkl.foot.metainfo.xml diff --git a/org.codeberg.dnkl.foot.metainfo.xml b/org.codeberg.dnkl.foot.metainfo.xml new file mode 100644 index 00000000..22512ce8 --- /dev/null +++ b/org.codeberg.dnkl.foot.metainfo.xml @@ -0,0 +1,45 @@ + + + org.codeberg.dnkl.foot + CC0-1.0 + MIT + dnkl + foot + The fast, lightweight and minimalistic Wayland terminal emulator. + +
    +
  • Fast
  • +
  • Lightweight, in dependencies, on-disk and in-memory
  • +
  • Wayland native
  • +
  • DE agnostic
  • +
  • Server/daemon mode
  • +
  • User configurable font fallback
  • +
  • On-the-fly font resize
  • +
  • On-the-fly DPI font size adjustment
  • +
  • Scrollback search
  • +
  • Keyboard driven URL detection
  • +
  • Color emoji support
  • +
  • IME (via text-input-v3)
  • +
  • Multi-seat
  • +
  • True Color (24bpp)
  • +
  • Synchronized Updates support
  • +
  • Sixel image support
  • +
+
+ + + Foot with sixel graphics + https://codeberg.org/dnkl/foot/media/branch/master/doc/sixel-wow.png + + + + + + + + + org.codeberg.dnkl.foot.desktop + https://codeberg.org/dnkl/foot + https://codeberg.org/dnkl/foot/issues + +
From debf1b8453ada57e69ec86fcb3fcb9ebf140d218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Magyar?= Date: Thu, 1 Sep 2022 07:24:09 +0200 Subject: [PATCH 0151/1323] chore: rename desktop files --- meson.build | 2 +- foot-server.desktop => org.codeberg.dnkl.foot-server.desktop | 0 foot.desktop => org.codeberg.dnkl.foot.desktop | 0 footclient.desktop => org.codeberg.dnkl.footclient.desktop | 0 4 files changed, 1 insertion(+), 1 deletion(-) rename foot-server.desktop => org.codeberg.dnkl.foot-server.desktop (100%) rename foot.desktop => org.codeberg.dnkl.foot.desktop (100%) rename footclient.desktop => org.codeberg.dnkl.footclient.desktop (100%) diff --git a/meson.build b/meson.build index 360db8fa..61837459 100644 --- a/meson.build +++ b/meson.build @@ -244,7 +244,7 @@ executable( install: true) install_data( - 'foot.desktop', 'foot-server.desktop', 'footclient.desktop', + 'org.codeberg.dnkl.foot.desktop', 'org.codeberg.dnkl.foot-server.desktop', 'org.codeberg.dnkl.footclient.desktop', install_dir: join_paths(get_option('datadir'), 'applications')) systemd = dependency('systemd', required: false) diff --git a/foot-server.desktop b/org.codeberg.dnkl.foot-server.desktop similarity index 100% rename from foot-server.desktop rename to org.codeberg.dnkl.foot-server.desktop diff --git a/foot.desktop b/org.codeberg.dnkl.foot.desktop similarity index 100% rename from foot.desktop rename to org.codeberg.dnkl.foot.desktop diff --git a/footclient.desktop b/org.codeberg.dnkl.footclient.desktop similarity index 100% rename from footclient.desktop rename to org.codeberg.dnkl.footclient.desktop From 05aedd52da1e89fdb2b25d3cc50f9934f1a1ae3b Mon Sep 17 00:00:00 2001 From: Torsten Trautwein Date: Thu, 22 Sep 2022 08:44:04 +0200 Subject: [PATCH 0152/1323] Add the nightfly theme --- themes/nightfly | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 themes/nightfly diff --git a/themes/nightfly b/themes/nightfly new file mode 100644 index 00000000..3815b42e --- /dev/null +++ b/themes/nightfly @@ -0,0 +1,29 @@ +# nightfly +# Based on https://github.com/bluz71/vim-nightfly-guicolors + +[cursor] +color = 080808 9ca1aa + +[colors] +foreground = acb4c2 +background = 011627 +selection-foreground = 080808 +selection-background = b2ceee + +regular0 = 1d3b53 +regular1 = fc514e +regular2 = a1cd5e +regular3 = e3d18a +regular4 = 82aaff +regular5 = c792ea +regular6 = 7fdbca +regular7 = a1aab8 + +bright0 = 7c8f8f +bright1 = ff5874 +bright2 = 21c7a8 +bright3 = ecc48d +bright4 = 82aaff +bright5 = ae81ff +bright6 = ae81ff +bright7 = d6deeb From cee59bfb3f355fec6488aa98626ee1017272f75e Mon Sep 17 00:00:00 2001 From: Torsten Trautwein Date: Thu, 22 Sep 2022 08:44:21 +0200 Subject: [PATCH 0153/1323] Add the moonfly theme --- themes/moonfly | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 themes/moonfly diff --git a/themes/moonfly b/themes/moonfly new file mode 100644 index 00000000..54d9203b --- /dev/null +++ b/themes/moonfly @@ -0,0 +1,29 @@ +# moonfly +# Based on https://github.com/bluz71/vim-moonfly-colors + +[cursor] +color = 080808 9e9e9e + +[colors] +foreground = b2b2b2 +background = 080808 +selection-foreground = 080808 +selection-background = b2ceee + +regular0 = 323437 +regular1 = ff5454 +regular2 = 8cc85f +regular3 = e3c78a +regular4 = 80a0ff +regular5 = d183e8 +regular6 = 79dac8 +regular7 = c6c6c6 + +bright0 = 949494 +bright1 = ff5189 +bright2 = 36c692 +bright3 = c2c292 +bright4 = 74b2ff +bright5 = ae81ff +bright6 = 85dc85 +bright7 = e4e4e4 From 020148d67ca2f1598dcf4fe3bdb212f3d6f7e521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 23 Sep 2022 20:20:56 +0200 Subject: [PATCH 0154/1323] =?UTF-8?q?install.md:=20add=20=E2=80=98utf8proc?= =?UTF-8?q?=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- INSTALL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/INSTALL.md b/INSTALL.md index ae0598d8..18975503 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -44,6 +44,7 @@ subprojects. * pixman * wayland (_client_ and _cursor_ libraries) * xkbcommon +* utf8proc (_optional_, needed for grapheme clustering) * [fcft](https://codeberg.org/dnkl/fcft) [^1] [^1]: can also be built as subprojects, in which case they are From 4db1dde25c4def9933235093e4720c898559f871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 22 Sep 2022 18:32:28 +0200 Subject: [PATCH 0155/1323] misc: add timespec_add() --- misc.c | 19 ++++++++++++++++++- misc.h | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/misc.c b/misc.c index 7e33e65a..a81aa9e4 100644 --- a/misc.c +++ b/misc.c @@ -13,15 +13,32 @@ isword(char32_t wc, bool spaces_only, const char32_t *delimiters) return isc32graph(wc); } +void +timespec_add(const struct timespec *a, const struct timespec *b, + struct timespec *res) +{ + const long one_sec_in_ns = 1000000000; + + res->tv_sec = a->tv_sec + b->tv_sec; + res->tv_nsec = a->tv_nsec + b->tv_nsec; + /* tv_nsec may be negative */ + if (res->tv_nsec >= one_sec_in_ns) { + res->tv_sec++; + res->tv_nsec -= one_sec_in_ns; + } +} + void timespec_sub(const struct timespec *a, const struct timespec *b, struct timespec *res) { + const long one_sec_in_ns = 1000000000; + res->tv_sec = a->tv_sec - b->tv_sec; res->tv_nsec = a->tv_nsec - b->tv_nsec; /* tv_nsec may be negative */ if (res->tv_nsec < 0) { res->tv_sec--; - res->tv_nsec += 1000 * 1000 * 1000; + res->tv_nsec += one_sec_in_ns; } } diff --git a/misc.h b/misc.h index ba8b2ce7..648bb65f 100644 --- a/misc.h +++ b/misc.h @@ -6,4 +6,5 @@ bool isword(char32_t wc, bool spaces_only, const char32_t *delimiters); +void timespec_add(const struct timespec *a, const struct timespec *b, struct timespec *res); void timespec_sub(const struct timespec *a, const struct timespec *b, struct timespec *res); From b7ba842237a52c3b6b455fc0e8725bed91f45ae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 22 Sep 2022 18:32:54 +0200 Subject: [PATCH 0156/1323] render: timer: print/log *total* rendering time --- render.c | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/render.c b/render.c index ef698911..8775171e 100644 --- a/render.c +++ b/render.c @@ -2913,15 +2913,21 @@ grid_render(struct terminal *term) struct timespec double_buffering_time; timespec_sub(&stop_double_buffering, &start_double_buffering, &double_buffering_time); + struct timespec total_render_time; + timespec_add(&render_time, &double_buffering_time, &total_render_time); + switch (term->conf->tweak.render_timer) { case RENDER_TIMER_LOG: case RENDER_TIMER_BOTH: - LOG_INFO("frame rendered in %lds %ldns " - "(%lds %ldns double buffering)", - (long)render_time.tv_sec, - render_time.tv_nsec, - (long)double_buffering_time.tv_sec, - double_buffering_time.tv_nsec); + LOG_INFO( + "frame rendered in %lds %9ldns " + "(%lds %9ldns rendering, %lds %9ldns double buffering)", + (long)total_render_time.tv_sec, + total_render_time.tv_nsec, + (long)render_time.tv_sec, + render_time.tv_nsec, + (long)double_buffering_time.tv_sec, + double_buffering_time.tv_nsec); break; case RENDER_TIMER_OSD: @@ -2932,7 +2938,7 @@ grid_render(struct terminal *term) switch (term->conf->tweak.render_timer) { case RENDER_TIMER_OSD: case RENDER_TIMER_BOTH: - render_render_timer(term, render_time); + render_render_timer(term, total_render_time); break; case RENDER_TIMER_LOG: From 50ae277d90c1eea6bb0086c31e004042088f794f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 22 Sep 2022 18:33:24 +0200 Subject: [PATCH 0157/1323] =?UTF-8?q?wayland:=20don=E2=80=99t=20crash=20wi?= =?UTF-8?q?th=20a=20division-by-zero=20when=20monitor=20dimensions=20are?= =?UTF-8?q?=20negative?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some compositors set the physical dimensions to -1... --- wayland.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wayland.c b/wayland.c index 05de79aa..cd052532 100644 --- a/wayland.c +++ b/wayland.c @@ -367,7 +367,7 @@ update_terms_on_monitor(struct monitor *mon) static void output_update_ppi(struct monitor *mon) { - if (mon->dim.mm.width == 0 || mon->dim.mm.height == 0) + if (mon->dim.mm.width <= 0 || mon->dim.mm.height <= 0) return; int x_inches = mon->dim.mm.width * 0.03937008; From 4340f8a3b4c5149825d8f1786cfeb7a3af9c0e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 22 Sep 2022 18:34:41 +0200 Subject: [PATCH 0158/1323] render: fix application of old scroll damage when double buffering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On compositors that forces us to double buffer, we need to re-apply the last frame’s damage to the current frame (which uses the buffer from the next-to-last frame). General cell updates are handled by simply copying from the last frame’s pixman buffer to the current frame’s. In an attempt to improve performance, scroll damage were up until now handled by re-playing the last frame’s scroll damage (on the current frame’s buffer). This does not work, and resulted in glitches when scrolling in the scrollback. This patch does the following: * grid_render_scroll{,_reverse}() now update the buffer’s "dirty" region. This means the generic copy-old-frames-buffer handles the scroll damage (albeit in, potentially, a less efficient way). * Tracking of, and re-applying old scroll damage is completely removed. Closes #1173 --- CHANGELOG.md | 5 ++ render.c | 132 +++++++++++++++++++++++++++------------------------ shm.c | 3 -- shm.h | 2 - 4 files changed, 74 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffbdd08b..17430354 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,11 @@ * Crash in `foot --server` on key press, after another `footclient` has terminated very early (for example, by trying to launch a non-existing shell/client). +* Glitchy rendering when scrolling in the scrollback, on compositors + that does not allow Wayland buffer re-use (e.g. KDE/plasma) + ([#1173][1173]) + +[1173]: https://codeberg.org/dnkl/foot/issues/1173 ### Security diff --git a/render.c b/render.c index 8775171e..7f883220 100644 --- a/render.c +++ b/render.c @@ -1014,6 +1014,13 @@ grid_render_scroll(struct terminal *term, struct buffer *buf, wl_surface_damage_buffer( term->window->surface, term->margins.left, dst_y, term->width - term->margins.left - term->margins.right, height); + + /* + * TODO: remove this if re-enabling scroll damage when re-applying + * last frame’s damage (see reapply_old_damage() + */ + pixman_region32_union_rect( + &buf->dirty, &buf->dirty, 0, dst_y, buf->width, height); } static void @@ -1079,6 +1086,13 @@ grid_render_scroll_reverse(struct terminal *term, struct buffer *buf, wl_surface_damage_buffer( term->window->surface, term->margins.left, dst_y, term->width - term->margins.left - term->margins.right, height); + + /* + * TODO: remove this if re-enabling scroll damage when re-applying + * last frame’s damage (see reapply_old_damage() + */ + pixman_region32_union_rect( + &buf->dirty, &buf->dirty, 0, dst_y, buf->width, height); } static void @@ -2538,22 +2552,27 @@ reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old return; } - /* - * TODO: remove this frame’s damage from the region we copy from - * the old frame. - * - * - this frame’s dirty region is only valid *after* we’ve applied - * its scroll damage. - * - last frame’s dirty region is only valid *before* we’ve - * applied this frame’s scroll damage. - * - * Can we transform one of the regions? It’s not trivial, since - * scroll damage isn’t just about counting lines; there may be - * multiple damage records, each with different scrolling regions. - */ pixman_region32_t dirty; pixman_region32_init(&dirty); + /* + * Figure out current frame’s damage region + * + * If current frame doesn’t have any scroll damage, we can simply + * subtract this frame’s damage from the last frame’s damage. That + * way, we don’t have to copy areas from the old frame that’ll + * just get overwritten by current frame. + * + * Note that this is row based. A “half damaged” row is not + * excluded. I.e. the entire row will be copied from the old frame + * to the new, and then when actually rendering the new frame, the + * updated cells will overwrite parts of the copied row. + * + * Since we’re scanning the entire viewport anyway, we also track + * whether *all* cells are to be updated. In this case, just force + * a full re-rendering, and don’t copy anything from the old + * frame. + */ bool full_repaint_needed = true; for (int r = 0; r < term->rows; r++) { @@ -2583,35 +2602,31 @@ reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old return; } - for (size_t i = 0; i < old->scroll_damage_count; i++) { - const struct damage *dmg = &old->scroll_damage[i]; - - switch (dmg->type) { - case DAMAGE_SCROLL: - if (term->grid->view == term->grid->offset) - grid_render_scroll(term, new, dmg); - break; - - case DAMAGE_SCROLL_REVERSE: - if (term->grid->view == term->grid->offset) - grid_render_scroll_reverse(term, new, dmg); - break; - - case DAMAGE_SCROLL_IN_VIEW: - grid_render_scroll(term, new, dmg); - break; - - case DAMAGE_SCROLL_REVERSE_IN_VIEW: - grid_render_scroll_reverse(term, new, dmg); - break; - } - } + /* + * TODO: re-apply last frame’s scroll damage + * + * We used to do this, but it turned out to be buggy. If we decide + * to re-add it, this is where to do it. Note that we’d also have + * to remove the updates to buf->dirty from grid_render_scroll() + * and grid_render_scroll_reverse(). + */ if (tll_length(term->grid->scroll_damage) == 0) { + /* + * We can only subtract current frame’s damage from the old + * frame’s if we don’t have any scroll damage. + * + * If we do have scroll damage, the damage region we + * calculated above is not (yet) valid - we need to apply the + * current frame’s scroll damage *first*. This is done later, + * when rendering the frame. + */ pixman_region32_subtract(&dirty, &old->dirty, &dirty); pixman_image_set_clip_region32(new->pix[0], &dirty); - } else + } else { + /* Copy *all* of last frame’s damaged areas */ pixman_image_set_clip_region32(new->pix[0], &old->dirty); + } pixman_image_composite32( PIXMAN_OP_SRC, old->pix[0], NULL, new->pix[0], @@ -2702,38 +2717,29 @@ grid_render(struct terminal *term) shm_addref(buf); buf->age = 0; - free(term->render.last_buf->scroll_damage); - buf->scroll_damage_count = tll_length(term->grid->scroll_damage); - buf->scroll_damage = xmalloc( - buf->scroll_damage_count * sizeof(buf->scroll_damage[0])); - { - size_t i = 0; - tll_foreach(term->grid->scroll_damage, it) { - buf->scroll_damage[i++] = it->item; - - switch (it->item.type) { - case DAMAGE_SCROLL: - if (term->grid->view == term->grid->offset) - grid_render_scroll(term, buf, &it->item); - break; - - case DAMAGE_SCROLL_REVERSE: - if (term->grid->view == term->grid->offset) - grid_render_scroll_reverse(term, buf, &it->item); - break; - - case DAMAGE_SCROLL_IN_VIEW: + tll_foreach(term->grid->scroll_damage, it) { + switch (it->item.type) { + case DAMAGE_SCROLL: + if (term->grid->view == term->grid->offset) grid_render_scroll(term, buf, &it->item); - break; + break; - case DAMAGE_SCROLL_REVERSE_IN_VIEW: + case DAMAGE_SCROLL_REVERSE: + if (term->grid->view == term->grid->offset) grid_render_scroll_reverse(term, buf, &it->item); - break; - } + break; - tll_remove(term->grid->scroll_damage, it); + case DAMAGE_SCROLL_IN_VIEW: + grid_render_scroll(term, buf, &it->item); + break; + + case DAMAGE_SCROLL_REVERSE_IN_VIEW: + grid_render_scroll_reverse(term, buf, &it->item); + break; } + + tll_remove(term->grid->scroll_damage, it); } /* diff --git a/shm.c b/shm.c index 4c342ddf..4394dbe9 100644 --- a/shm.c +++ b/shm.c @@ -151,7 +151,6 @@ buffer_destroy(struct buffer_private *buf) pool_unref(buf->pool); buf->pool = NULL; - free(buf->public.scroll_damage); pixman_region32_fini(&buf->public.dirty); free(buf); } @@ -581,8 +580,6 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height) LOG_DBG("re-using buffer %p from cache", (void *)cached); cached->busy = true; pixman_region32_clear(&cached->public.dirty); - free(cached->public.scroll_damage); - cached->public.scroll_damage = NULL; xassert(cached->public.pix_instances == chain->pix_instances); return &cached->public; } diff --git a/shm.h b/shm.h index bed4285c..440cfa1d 100644 --- a/shm.h +++ b/shm.h @@ -24,8 +24,6 @@ struct buffer { unsigned age; - struct damage *scroll_damage; - size_t scroll_damage_count; pixman_region32_t dirty; }; From 3be44fb316b166c0b5d5536fdb5bb6c7abdd3a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 22 Sep 2022 18:39:00 +0200 Subject: [PATCH 0159/1323] render: overlay: fix visual glitches when double buffering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When rendering the overlay for scrollback search, the logic assumed buffer re-use. On some compositors this isn’t happening (on e.g. KDE/plasma we’re forced to double buffer). This resulted in matches not being highlighted correctly. The problem is in how we calculated the region for which areas to clear ("un-dim"). It uses the "previous frame’s see-through area" minus the current frame’s see-through area. However, when we’ve detected that the current buffer isn’t the same as the last one, we set the last frame’s see-through region to "the entire buffer". Thus, when calculating the diff, we end up with an empty region, and nothing is highlighted. Fix by simply using the current frame’s see-through region as-is when we’ve detected we’re not re-using the last frame’s buffer. --- CHANGELOG.md | 3 +++ render.c | 29 +++++++++++++++++++---------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17430354..dd0ac80b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,9 @@ * Glitchy rendering when scrolling in the scrollback, on compositors that does not allow Wayland buffer re-use (e.g. KDE/plasma) ([#1173][1173]) +* Scrollback search matches not being highlighted correctly, on + compositors that does now allow Wayland buffer re-use + (e.g. KDE/plasma). [1173]: https://codeberg.org/dnkl/foot/issues/1173 diff --git a/render.c b/render.c index 7f883220..b285b120 100644 --- a/render.c +++ b/render.c @@ -1563,11 +1563,12 @@ render_overlay(struct terminal *term) */ pixman_region32_t *see_through = &term->render.last_overlay_clip; pixman_region32_t old_see_through; + const bool buffer_reuse = + buf == term->render.last_overlay_buf && + style == term->render.last_overlay_style && + buf->age == 0; - if (!(buf == term->render.last_overlay_buf && - style == term->render.last_overlay_style && - buf->age == 0)) - { + if (!buffer_reuse) { /* Can’t re-use last frame’s damage - set to full window, * to ensure *everything* is updated */ pixman_region32_init_rect( @@ -1580,8 +1581,8 @@ render_overlay(struct terminal *term) pixman_region32_clear(see_through); + /* Build region consisting of all current search matches */ struct search_match_iterator iter = search_matches_new_iter(term); - for (struct range match = search_matches_next(&iter); match.start.row >= 0; match = search_matches_next(&iter)) @@ -1609,20 +1610,28 @@ render_overlay(struct terminal *term) } } - /* Current see-through, minus old see-through - aka cells that - * need to be cleared */ + /* Areas that need to be cleared: cells that were dimmed in + * the last frame but is now see-through */ pixman_region32_t new_see_through; pixman_region32_init(&new_see_through); - pixman_region32_subtract(&new_see_through, see_through, &old_see_through); + + if (buffer_reuse) + pixman_region32_subtract(&new_see_through, see_through, &old_see_through); + else { + /* Buffer content is unknown - explicitly clear *all* + * current see-through areas */ + pixman_region32_copy(&new_see_through, see_through); + } pixman_image_set_clip_region32(buf->pix[0], &new_see_through); - /* Old see-through, minus new see-through - aka cells that - * needs to be dimmed */ + /* Areas that need to be dimmed: cells that were cleared in + * the last frame but is not anymore */ pixman_region32_t new_dimmed; pixman_region32_init(&new_dimmed); pixman_region32_subtract(&new_dimmed, &old_see_through, see_through); pixman_region32_fini(&old_see_through); + /* Total affected area */ pixman_region32_t damage; pixman_region32_init(&damage); pixman_region32_union(&damage, &new_see_through, &new_dimmed); From aa10b1d2da3ecf9672ae915a49073bd30ee0a785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 23 Sep 2022 20:24:04 +0200 Subject: [PATCH 0160/1323] Add support for creating utmp records MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch adds support for creating utmp records using the ‘utempter’ helper binary from the ‘libutempter’ package. * New config option ‘main.utempter’ * New meson command line option, -Ddefault-utempter-path. Defaults to auto-detecting the path. The default value of the new ‘main.utempter’ config option depends on the meson command line option ‘-Ddefault-utempter-path’. If ‘main.utempter’ is *not* set to ‘none’, foot will try to execute the utempter helper binary to create utmp records when a new terminal is instantiated. The record is removed when the terminal instance is destroyed. --- CHANGELOG.md | 2 ++ INSTALL.md | 22 ++++++++++++---------- config.c | 19 +++++++++++++++++++ config.h | 2 ++ doc/foot.ini.5.scd | 4 ++++ doc/meson.build | 7 +++++++ foot.ini | 1 + meson.build | 24 +++++++++++++++++++++++- meson_options.txt | 3 +++ terminal.c | 31 +++++++++++++++++++++++++++++++ tests/test-config.c | 1 + 11 files changed, 105 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd0ac80b..1bc41eef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,8 @@ ### Added * Support for adjusting the thickness of regular underlines ([#1136][1136]). +* Support (optional) for utmp logging with libutempter. + [1136]: https://codeberg.org/dnkl/foot/issues/1136 diff --git a/INSTALL.md b/INSTALL.md index 18975503..da3a667e 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -45,6 +45,7 @@ subprojects. * wayland (_client_ and _cursor_ libraries) * xkbcommon * utf8proc (_optional_, needed for grapheme clustering) +* libutempter (_optional_, needed for utmp logging) * [fcft](https://codeberg.org/dnkl/fcft) [^1] [^1]: can also be built as subprojects, in which case they are @@ -141,16 +142,17 @@ mkdir -p bld/release && cd bld/release Available compile-time options: -| Option | Type | Default | Description | Extra dependencies | -|--------------------------------------|---------|-------------------------|-------------------------------------------------------|--------------------| -| `-Ddocs` | feature | `auto` | Builds and install documentation | scdoc | -| `-Dtests` | bool | `true` | Build tests (adds a `ninja test` build target) | none | -| `-Dime` | bool | `true` | Enables IME support | None | -| `-Dgrapheme-clustering` | feature | `auto` | Enables grapheme clustering | libutf8proc | -| `-Dterminfo` | feature | `enabled` | Build and install terminfo files | tic (ncurses) | -| `-Ddefault-terminfo` | string | `foot` | Default value of `TERM` | none | -| `-Dcustom-terminfo-install-location` | string | `${datadir}/terminfo` | Value to set `TERMINFO` to | None | -| `-Dsystemd-units-dir` | string | `${systemduserunitdir}` | Where to install the systemd service files (absolute) | None | +| Option | Type | Default | Description | Extra dependencies | +|--------------------------------------|---------|-------------------------|-----------------------------------------------------------|--------------------| +| `-Ddocs` | feature | `auto` | Builds and install documentation | scdoc | +| `-Dtests` | bool | `true` | Build tests (adds a `ninja test` build target) | none | +| `-Dime` | bool | `true` | Enables IME support | None | +| `-Dgrapheme-clustering` | feature | `auto` | Enables grapheme clustering | libutf8proc | +| `-Dterminfo` | feature | `enabled` | Build and install terminfo files | tic (ncurses) | +| `-Ddefault-terminfo` | string | `foot` | Default value of `TERM` | none | +| `-Dcustom-terminfo-install-location` | string | `${datadir}/terminfo` | Value to set `TERMINFO` to | None | +| `-Dsystemd-units-dir` | string | `${systemduserunitdir}` | Where to install the systemd service files (absolute) | None | +| `-Ddefault-utempter-path` | feature | `auto` | Default path to utempter binary (‘none’ disables default) | libutempter | Documentation includes the man pages, readme, changelog and license files. diff --git a/config.c b/config.c index 0ba237a0..e2c007d2 100644 --- a/config.c +++ b/config.c @@ -944,6 +944,18 @@ parse_section_main(struct context *ctx) else if (strcmp(key, "box-drawings-uses-font-glyphs") == 0) return value_to_bool(ctx, &conf->box_drawings_uses_font_glyphs); + else if (strcmp(key, "utempter") == 0) { + if (!value_to_str(ctx, &conf->utempter_path)) + return false; + + if (strcmp(conf->utempter_path, "none") == 0) { + free(conf->utempter_path); + conf->utempter_path = NULL; + } + + return true; + } + else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; @@ -2937,6 +2949,9 @@ config_load(struct config *conf, const char *conf_path, }, .env_vars = tll_init(), + .utempter_path = (strlen(FOOT_DEFAULT_UTEMPTER_PATH) > 0 + ? xstrdup(FOOT_DEFAULT_UTEMPTER_PATH) + : NULL), .notifications = tll_init(), }; @@ -3225,6 +3240,9 @@ config_clone(const struct config *old) tll_push_back(conf->env_vars, copy); } + conf->utempter_path = + old->utempter_path != NULL ? xstrdup(old->utempter_path) : NULL; + conf->notifications.length = 0; conf->notifications.head = conf->notifications.tail = 0; tll_foreach(old->notifications, it) { @@ -3291,6 +3309,7 @@ config_free(struct config *conf) tll_remove(conf->env_vars, it); } + free(conf->utempter_path); user_notifications_free(&conf->notifications); } diff --git a/config.h b/config.h index f98e4d35..d35abbb2 100644 --- a/config.h +++ b/config.h @@ -319,6 +319,8 @@ struct config { env_var_list_t env_vars; + char *utempter_path; + struct { enum fcft_scaling_filter fcft_filter; bool overflowing_glyphs; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 7382a1d2..46a6d33e 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -317,6 +317,10 @@ commented out will usually be installed to */etc/xdg/foot/foot.ini*. (including SMT). Note that this is not always the best value. In some cases, the number of physical _cores_ is better. +*utempter* + Path to utempter helper binary. Set to *none* to disable utmp + records. Default: _@utempter@_. + # SECTION: environment This section is used to define environment variables that will be set diff --git a/doc/meson.build b/doc/meson.build index 75c3be95..86e75952 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -2,9 +2,16 @@ sh = find_program('sh', native: true) scdoc_prog = find_program(scdoc.get_variable('scdoc'), native: true) +if utempter_path == '' + default_utempter_value = 'not set' +else + default_utempter_value = utempter_path +endif + conf_data = configuration_data( { 'default_terminfo': get_option('default-terminfo'), + 'utempter': default_utempter_value, } ) diff --git a/foot.ini b/foot.ini index 3b8b6f16..62f853fa 100644 --- a/foot.ini +++ b/foot.ini @@ -33,6 +33,7 @@ # word-delimiters=,│`|:"'()[]{}<> # selection-target=primary # workers= +# utempter=/usr/lib/utempter/utempter [environment] # name=value diff --git a/meson.build b/meson.build index 61837459..4d3b6213 100644 --- a/meson.build +++ b/meson.build @@ -16,9 +16,30 @@ if cc.has_function('memfd_create') add_project_arguments('-DMEMFD_CREATE', language: 'c') endif +utempter_path = get_option('default-utempter-path') +if utempter_path == '' + utempter = find_program( + 'utempter', + required: false, + dirs: [join_paths(get_option('prefix'), get_option('libdir'), 'utempter'), + join_paths(get_option('prefix'), get_option('libexecdir'), 'utempter'), + '/usr/lib/utempter', + '/usr/libexec/utempter', + '/lib/utempter'] + ) + if utempter.found() + utempter_path = utempter.full_path() + else + utempter_path = '' + endif +elif utempter_path == 'none' + utempter_path = '' +endif + add_project_arguments( ['-D_GNU_SOURCE=200809L', - '-DFOOT_DEFAULT_TERM="@0@"'.format(get_option('default-terminfo'))] + + '-DFOOT_DEFAULT_TERM="@0@"'.format(get_option('default-terminfo')), + '-DFOOT_DEFAULT_UTEMPTER_PATH="@0@"'.format(utempter_path)] + (is_debug_build ? ['-D_DEBUG'] : [cc.get_supported_arguments('-fno-asynchronous-unwind-tables')]) + @@ -321,6 +342,7 @@ summary( 'Themes': get_option('themes'), 'IME': get_option('ime'), 'Grapheme clustering': utf8proc.found(), + 'Utempter path': utempter_path, 'Build terminfo': tic.found(), 'Terminfo install location': terminfo_install_location, 'Default TERM': get_option('default-terminfo'), diff --git a/meson_options.txt b/meson_options.txt index 0c660a75..c38a8ca8 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -21,3 +21,6 @@ option('custom-terminfo-install-location', type: 'string', value: '', option('systemd-units-dir', type: 'string', value: '', description: 'Where to install the systemd service files (absolute path). Default: ${systemduserunitdir}') + +option('default-utempter-path', type: 'string', value: '', + description: 'Default path to utempter helper binary. Default: auto-detect') diff --git a/terminal.c b/terminal.c index 3445b89f..4c9bf3db 100644 --- a/terminal.c +++ b/terminal.c @@ -203,6 +203,30 @@ fdm_ptmx_out(struct fdm *fdm, int fd, int events, void *data) return true; } +static bool +add_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) +{ + if (ptmx < 0) + return true; + if (conf->utempter_path == NULL) + return true; + + char *const argv[] = {conf->utempter_path, "add", NULL}; + return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL); +} + +static bool +del_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) +{ + if (ptmx < 0) + return true; + if (conf->utempter_path == NULL) + return true; + + char *const argv[] = {conf->utempter_path, "del", NULL}; + return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL); +} + #if PTMX_TIMING static struct timespec last = {0}; #endif @@ -326,6 +350,7 @@ fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) } if (hup) { + del_utmp_record(term->conf, term->reaper, term->ptmx); fdm_del(fdm, fd); term->ptmx = -1; } @@ -1251,6 +1276,8 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, } term->font_line_height = conf->line_height; + 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, @@ -1514,6 +1541,8 @@ term_shutdown(struct terminal *term) fdm_del(term->fdm, term->blink.fd); fdm_del(term->fdm, term->flash.fd); + del_utmp_record(term->conf, term->reaper, term->ptmx); + if (term->window != NULL && term->window->is_configured) fdm_del(term->fdm, term->ptmx); else @@ -1595,6 +1624,8 @@ term_destroy(struct terminal *term) } } + del_utmp_record(term->conf, term->reaper, term->ptmx); + fdm_del(term->fdm, term->selection.auto_scroll.fd); fdm_del(term->fdm, term->render.app_sync_updates.timer_fd); fdm_del(term->fdm, term->render.title.timer_fd); diff --git a/tests/test-config.c b/tests/test-config.c index 837c5106..ae7969dc 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -458,6 +458,7 @@ test_section_main(void) test_string(&ctx, &parse_section_main, "shell", &conf.shell); test_string(&ctx, &parse_section_main, "term", &conf.term); test_string(&ctx, &parse_section_main, "app-id", &conf.app_id); + test_string(&ctx, &parse_section_main, "utempter", &conf.utempter_path); test_c32string(&ctx, &parse_section_main, "word-delimiters", &conf.word_delimiters); From c93eb45b42691e0f13b21668298d2cf4a94e67e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 23 Sep 2022 23:04:10 +0200 Subject: [PATCH 0161/1323] =?UTF-8?q?term:=20utmp:=20set=20=E2=80=98host?= =?UTF-8?q?=E2=80=99=20to=20WAYLAND=5FDISPLAY?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is similar to what XTerm does (setting it to DISPLAY) --- terminal.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/terminal.c b/terminal.c index 4c9bf3db..df17201b 100644 --- a/terminal.c +++ b/terminal.c @@ -211,7 +211,7 @@ add_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) if (conf->utempter_path == NULL) return true; - char *const argv[] = {conf->utempter_path, "add", NULL}; + char *const argv[] = {conf->utempter_path, "add", getenv("WAYLAND_DISPLAY"), NULL}; return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL); } @@ -223,7 +223,7 @@ del_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) if (conf->utempter_path == NULL) return true; - char *const argv[] = {conf->utempter_path, "del", NULL}; + char *const argv[] = {conf->utempter_path, "del", getenv("WAYLAND_DISPLAY"), NULL}; return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL); } From bb02b319d02f986c8f8f2562a77b5cfbd7d51e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 24 Sep 2022 12:32:17 +0200 Subject: [PATCH 0162/1323] terminfo: add kxIN + kxOUT (focus in/out events) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These capabilities are not included in the standard ‘xterm’ or ‘xterm-256color’ terminfos. They’re used in ‘xterm+focus’ -> ‘xterm+sm+1002’ -> ‘xterm-1002|xterm+sm+1003’ -> ‘xterm-1003’ (https://invisible-island.net/ncurses/terminfo.ti.html#tic-xterm_focus) However, as far as I can tell, ncurses doesn’t use these capabilities at all. --- CHANGELOG.md | 2 +- foot.info | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bc41eef..dc56c989 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,7 +47,7 @@ * Support for adjusting the thickness of regular underlines ([#1136][1136]). * Support (optional) for utmp logging with libutempter. - +* `kxIN` and `kxOUT` (focus in/out events) to terminfo. [1136]: https://codeberg.org/dnkl/foot/issues/1136 diff --git a/foot.info b/foot.info index 2dae3eca..f4030b22 100644 --- a/foot.info +++ b/foot.info @@ -218,6 +218,8 @@ knp=\E[6~, kpp=\E[5~, kri=\E[1;2A, + kxIN=\E[I, + kxOUT=\E[O, oc=\E]104\E\\, op=\E[39;49m, rc=\E8, From 88c3128515832e64fa78a814402e30c1470f6d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 28 Sep 2022 21:09:35 +0200 Subject: [PATCH 0163/1323] =?UTF-8?q?scripts:=20generate-builtin-terminfo:?= =?UTF-8?q?=20add=20synthetic=20=E2=80=98name=E2=80=99=20capability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same as ‘TN’; reports the terminfo name. --- CHANGELOG.md | 1 + scripts/generate-builtin-terminfo.py | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc56c989..ea3fac1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ * Support for adjusting the thickness of regular underlines ([#1136][1136]). * Support (optional) for utmp logging with libutempter. * `kxIN` and `kxOUT` (focus in/out events) to terminfo. +* `name` capability to `XTGETTCAP`. [1136]: https://codeberg.org/dnkl/foot/issues/1136 diff --git a/scripts/generate-builtin-terminfo.py b/scripts/generate-builtin-terminfo.py index c8d3be4b..906e2be0 100755 --- a/scripts/generate-builtin-terminfo.py +++ b/scripts/generate-builtin-terminfo.py @@ -166,6 +166,7 @@ def main(): entry.add_capability(IntCapability('Co', 256)) entry.add_capability(StringCapability('TN', target_entry_name)) + entry.add_capability(StringCapability('name', target_entry_name)) entry.add_capability(IntCapability('RGB', 8)) # 8 bits per channel terminfo_parts = [] From 9e5866109374a3181da06ad411b15bab4a728d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Sep 2022 19:00:27 +0200 Subject: [PATCH 0164/1323] slave: spawn: set PWD environment variable This improves handling of symlinks (in CWD) when launching a new terminal instance, either through ctrl+shift+n, or using the --working-directory command line option. Closes #1179 --- CHANGELOG.md | 2 ++ slave.c | 1 + 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea3fac1a..7a6efd08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,8 +63,10 @@ * `DECRPM` replies (to `DECRQM` queries) now report a value of `4` ("permanently reset") instead of `2` ("reset") for DEC private modes that are known but unsupported. +* Set `PWD` environment variable in the slave process ([#1179][1179]). [1166]: https://codeberg.org/dnkl/foot/issues/1166 +[1179]: https://codeberg.org/dnkl/foot/issues/1179 ### Deprecated diff --git a/slave.c b/slave.c index 6473ac7c..4dd80e6f 100644 --- a/slave.c +++ b/slave.c @@ -352,6 +352,7 @@ slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, setenv("TERM", term_env, 1); setenv("COLORTERM", "truecolor", 1); + setenv("PWD", cwd, 1); #if defined(FOOT_TERMINFO_PATH) setenv("TERMINFO", FOOT_TERMINFO_PATH, 1); From 90ce4f3008a7c7f01dcdee4504b9c512c96a41d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Sep 2022 19:09:33 +0200 Subject: [PATCH 0165/1323] main/client: use $PWD for cwd, when $PWD is valid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If $PWD is set, and its resolved path matches the *actual* working directory, use $PWD for cwd when instantiating the terminal. This makes a difference when $PWD refers to a symlink; before this patch, we’d instantiate the terminal in the *resolved* path. Now it’ll use the symlink instead. --- client.c | 22 ++++++++++++++++++++++ main.c | 22 ++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/client.c b/client.c index 7624e7db..2a802d16 100644 --- a/client.c +++ b/client.c @@ -408,6 +408,28 @@ main(int argc, char *const *argv) cwd = _cwd; } + const char *pwd = getenv("PWD"); + if (pwd != NULL) { + char *resolved_path_cwd = realpath(cwd, NULL); + char *resolved_path_pwd = realpath(pwd, NULL); + + if (resolved_path_cwd != NULL && + resolved_path_pwd != NULL && + strcmp(resolved_path_cwd, resolved_path_pwd) == 0) + { + /* + * The resolved path of $PWD matches the resolved path of + * the *actual* working directory - use $PWD. + * + * This makes a difference when $PWD refers to a symlink. + */ + cwd = pwd; + } + + free(resolved_path_cwd); + free(resolved_path_pwd); + } + if (client_environment) { for (char **e = environ; *e != NULL; e++) { if (!push_string(&envp, *e, &total_len)) diff --git a/main.c b/main.c index 4617a3c7..a3ae579d 100644 --- a/main.c +++ b/main.c @@ -594,6 +594,28 @@ main(int argc, char *const *argv) cwd = _cwd; } + const char *pwd = getenv("PWD"); + if (pwd != NULL) { + char *resolved_path_cwd = realpath(cwd, NULL); + char *resolved_path_pwd = realpath(pwd, NULL); + + if (resolved_path_cwd != NULL && + resolved_path_pwd != NULL && + strcmp(resolved_path_cwd, resolved_path_pwd) == 0) + { + /* + * The resolved path of $PWD matches the resolved path of + * the *actual* working directory - use $PWD. + * + * This makes a difference when $PWD refers to a symlink. + */ + cwd = pwd; + } + + free(resolved_path_cwd); + free(resolved_path_pwd); + } + shm_set_max_pool_size(conf.tweak.max_shm_pool_size); if ((fdm = fdm_init()) == NULL) From 332cb90134bf7ba4f5248147aeee9f8f6a99a579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Sep 2022 19:16:40 +0200 Subject: [PATCH 0166/1323] spawn: set $PWD, in addition to calling chdir(cwd) --- spawn.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/spawn.c b/spawn.c index 7c6641da..90b892f3 100644 --- a/spawn.c +++ b/spawn.c @@ -54,9 +54,12 @@ spawn(struct reaper *reaper, const char *cwd, char *const argv[], goto child_err; } - if (cwd != NULL && chdir(cwd) < 0) { - LOG_WARN("failed to change working directory to %s: %s", - cwd, strerror(errno)); + if (cwd != NULL) { + setenv("PWD", cwd, 1); + if (chdir(cwd) < 0) { + LOG_WARN("failed to change working directory to %s: %s", + cwd, strerror(errno)); + } } if (xdg_activation_token != NULL) { From cbebafbfe8d43415fa6baeb59d44bd3be0b593bf Mon Sep 17 00:00:00 2001 From: Nick Hastings Date: Tue, 4 Oct 2022 13:04:03 +0900 Subject: [PATCH 0167/1323] doc: fix tiny typo --- doc/footclient.1.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/footclient.1.scd b/doc/footclient.1.scd index 967a4f1b..93bddd61 100644 --- a/doc/footclient.1.scd +++ b/doc/footclient.1.scd @@ -89,7 +89,7 @@ terminal has terminated. # EXIT STATUS -Footlient will exit with code 220 if there is a failure in footclient +Footclient will exit with code 220 if there is a failure in footclient itself (for example, the server socket does not exist). If *-N*,*--no-wait* is used, footclient exits with code 0 as soon as From 2d4f0535c6507dfb7b7444f6aaff9bfe854cf5a8 Mon Sep 17 00:00:00 2001 From: Hugo Osvaldo Barrera Date: Tue, 4 Oct 2022 20:50:44 +0200 Subject: [PATCH 0168/1323] Add zenburn theme This is the one included by default in previous releases. --- themes/zenburn | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 themes/zenburn diff --git a/themes/zenburn b/themes/zenburn new file mode 100644 index 00000000..3867826f --- /dev/null +++ b/themes/zenburn @@ -0,0 +1,23 @@ +[colors] +foreground=dcdccc +background=111111 + +## Normal/regular colors (color palette 0-7) +regular0=222222 # black +regular1=cc9393 # red +regular2=7f9f7f # green +regular3=d0bf8f # yellow +regular4=6ca0a3 # blue +regular5=dc8cc3 # magenta +regular6=93e0e3 # cyan +regular7=dcdccc # white + +## Bright colors (color palette 8-15) +bright0=666666 # bright black +bright1=dca3a3 # bright red +bright2=bfebbf # bright green +bright3=f0dfaf # bright yellow +bright4=8cd0d3 # bright blue +bright5=fcace3 # bright magenta +bright6=b3ffff # bright cyan +bright7=ffffff # bright white From 37218be64853dccd5574a7f5de7193c0e1a7b5e6 Mon Sep 17 00:00:00 2001 From: Alexey Sakovets Date: Mon, 3 Oct 2022 19:41:13 +0300 Subject: [PATCH 0169/1323] render: fix nanosec "overflow" when calculating timeout value --- CHANGELOG.md | 2 ++ render.c | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a6efd08..52d5b7be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,8 @@ * Scrollback search matches not being highlighted correctly, on compositors that does now allow Wayland buffer re-use (e.g. KDE/plasma). +* Nanosecs "overflow" when calculating timeout value for + `resize-delay-ms` option. [1173]: https://codeberg.org/dnkl/foot/issues/1173 diff --git a/render.c b/render.c index b285b120..f14911d4 100644 --- a/render.c +++ b/render.c @@ -3716,7 +3716,10 @@ send_dimensions_to_client(struct terminal *term) if (fd >= 0) { /* Reset timeout */ const struct itimerspec timeout = { - .it_value = {.tv_sec = 0, .tv_nsec = delay_ms * 1000000}, + .it_value = { + .tv_sec = delay_ms / 1000, + .tv_nsec = (delay_ms % 1000) * 1000000, + }, }; if (timerfd_settime(fd, 0, &timeout, NULL) < 0) { From fd743b51736e99a097063c1f52c242e5592c298e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 27 Sep 2022 19:05:56 +0200 Subject: [PATCH 0170/1323] scripts: generate-builtin-terminfo: double-escape backslash in ST Fixes an issue with XTGETTCAP, where escape sequences terminated with ST, and containing parameters were missing a trailing backslash. --- CHANGELOG.md | 2 ++ scripts/generate-builtin-terminfo.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52d5b7be..1606f3b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,8 @@ (e.g. KDE/plasma). * Nanosecs "overflow" when calculating timeout value for `resize-delay-ms` option. +* Missing backslash in ST terminator in escape sequences in the + built-in terminfo (accessed via XTGETTCAP). [1173]: https://codeberg.org/dnkl/foot/issues/1173 diff --git a/scripts/generate-builtin-terminfo.py b/scripts/generate-builtin-terminfo.py index 906e2be0..035fa1cc 100755 --- a/scripts/generate-builtin-terminfo.py +++ b/scripts/generate-builtin-terminfo.py @@ -55,6 +55,10 @@ class StringCapability(Capability): value = re.sub(r'\\E([0-7])', r'\\033" "\1', value) value = re.sub(r'\\E', r'\\033', value) else: + # Need to double-escape backslashes. These only occur in + # ‘\E\’ combos. Note that \E itself is updated below + value = value.replace('\\E\\\\', '\\E\\\\\\\\') + # Need to double-escape \E in C string literals value = value.replace('\\E', '\\\\E') From f359a8d6bcad8c7f12c6f707caa7480fd193c92a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 4 Oct 2022 21:42:13 +0200 Subject: [PATCH 0171/1323] scripts: generate-builtin-terminfo: escape fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove ‘:’ escaping only in raw (non-parameterized) sequences * Double-escape *all* escape characters in parameterized sequences --- scripts/generate-builtin-terminfo.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/scripts/generate-builtin-terminfo.py b/scripts/generate-builtin-terminfo.py index 035fa1cc..acbf5279 100755 --- a/scripts/generate-builtin-terminfo.py +++ b/scripts/generate-builtin-terminfo.py @@ -52,18 +52,25 @@ class StringCapability(Capability): def __init__(self, name: str, value: str): # Expand \E to literal ESC in non-parameterized capabilities if '%' not in value: + # Ensure e.g. \E7 doesn’t get translated to “\0337”, which + # would be interpreted as octal 337 by the C compiler value = re.sub(r'\\E([0-7])', r'\\033" "\1', value) + + # Replace \E with an actual escape value = re.sub(r'\\E', r'\\033', value) + + # Don’t escape ‘:’ + value = value.replace('\\:', ':') + else: - # Need to double-escape backslashes. These only occur in - # ‘\E\’ combos. Note that \E itself is updated below - value = value.replace('\\E\\\\', '\\E\\\\\\\\') + value = value.replace("\\", "\\\\") + # # Need to double-escape backslashes. These only occur in + # # ‘\E\’ combos. Note that \E itself is updated below + # value = value.replace('\\E\\\\', '\\E\\\\\\\\') - # Need to double-escape \E in C string literals - value = value.replace('\\E', '\\\\E') + # # Need to double-escape \E in C string literals + # value = value.replace('\\E', '\\\\E') - # Don’t escape ‘:’ - value = value.replace('\\:', ':') super().__init__(name, value) From 9937d92c85b5e113c57abae7b74ad55972eaffab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 7 Oct 2022 14:40:22 +0200 Subject: [PATCH 0172/1323] utils: xtgettcap: new utility, to send XTGETTCAP queries --- meson.build | 1 + utils/meson.build | 1 + utils/xtgettcap.c | 175 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 utils/meson.build create mode 100644 utils/xtgettcap.c diff --git a/meson.build b/meson.build index 4d3b6213..68c3bf19 100644 --- a/meson.build +++ b/meson.build @@ -331,6 +331,7 @@ endif subdir('completions') subdir('icons') +subdir('utils') if (get_option('tests')) subdir('tests') diff --git a/utils/meson.build b/utils/meson.build new file mode 100644 index 00000000..2836788c --- /dev/null +++ b/utils/meson.build @@ -0,0 +1 @@ +executable('xtgettcap', 'xtgettcap.c') diff --git a/utils/xtgettcap.c b/utils/xtgettcap.c new file mode 100644 index 00000000..e21de7c0 --- /dev/null +++ b/utils/xtgettcap.c @@ -0,0 +1,175 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static struct termios orig_termios; + +static void +disable_raw_mode(void) +{ + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios) < 0) + exit(__LINE__); +} + +static void +enable_raw_mode(void) +{ + if (tcgetattr(STDIN_FILENO, &orig_termios) < 0) + exit(__LINE__); + + atexit(disable_raw_mode); + + struct termios raw = orig_termios; + raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + raw.c_oflag &= ~(OPOST); + raw.c_cflag |= (CS8); + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + raw.c_cc[VMIN] = 0; + raw.c_cc[VTIME] = 1; + + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) < 0) + exit(__LINE__); +} + +static const char * +hexlify(const char *s) +{ + static char buf[1024]; + + const size_t len = strlen(s); + for (size_t i = 0; i < len; i++) + sprintf(&buf[i * 2], "%02x", s[i]); + buf[len * 2 + 1] = '\0'; + + return buf; +} + +static size_t +unhexlify(char *dst, const char *src) +{ + size_t count = 0; + for (const char *p = src; *p != '\0'; p += 2, dst++, count++) + sscanf(p, "%02hhx", (unsigned char *)dst); + + *dst = '\0'; + return count; +} + +int +main(int argc, const char *const *argv) +{ + enable_raw_mode(); + + const size_t query_count = argc - 1; + + printf("\x1bP+q"); + for (int i = 1; i < argc; i++) + printf("%s%s", i > 1 ? ";" : "", hexlify(argv[i])); + printf("\033\\"); + + fflush(NULL); + + size_t replies = 0; + while (replies < query_count) { + struct pollfd fds[] = {{.fd = STDIN_FILENO, .events = POLLIN}}; + int r = poll(fds, sizeof(fds) / sizeof(fds[0]), -1); + if (r < 0) + exit(__LINE__); + + char buf[1024] = {0}; + ssize_t count = read(STDIN_FILENO, buf, sizeof(buf)); + + if (count < 0) + exit(__LINE__); + + if (count == 1 && buf[0] == 'q') + break; + + printf("reply: (%zd chars): ", count); + + for (size_t i = 0; i < (size_t)count; i++) { + if (isprint(buf[i])) + printf("%c", buf[i]); + else if (buf[i] == '\033') + printf("\033[1;31m\\E\033[m"); + else + printf("%02x", (uint8_t)buf[i]); + } + printf("\r\n"); + + const char *p = buf; + const char *end = buf + count; + + while (p < end) { + + const char *ST = strstr(p, "\033\\"); + if (ST == NULL) + break; + + if (count < 5 || + (strncmp(p, "\033P1+r", 5) != 00 && + strncmp(p, "\033P0+r", 5) != 0)) + { + break; + } + + const bool success = p[2] == '1'; + + char decoded[1024]; + char copy[ST - &p[5] + 1]; + strncpy(copy, &p[5], ST - &p[5]); + copy[ST - &p[5]] = '\0'; + + char *saveptr = NULL; + for (char *key_value = strtok_r(copy, "; ", &saveptr); + key_value != NULL; + key_value = strtok_r(NULL, "; ", &saveptr)) + { + // printf("key-value=%s\n", key_value); + const char *key = strtok(key_value, "="); + const char *value = strtok(NULL, "="); + +#if 0 + assert((success && value != NULL) || + (!success && value == NULL)); +#endif + + //printf("key=%s, value=%s\n", key, value); + size_t len = unhexlify(decoded, key); + + if (value != NULL) { + decoded[len++] = '='; + len += unhexlify(&decoded[len], value); + } + + const int color = success ? 39 : 31; + + printf(" \033[%dm", color); + for (size_t i = 0 ; i < len; i++) { + if (isprint(decoded[i])) + printf("%c", decoded[i]); + else if (decoded[i] == '\033') + printf("\033[1;31m\\E\033[22;%dm", color); + else + printf("\033[1m%02x\033[22m", (uint8_t)decoded[i]); + } + printf("\033[m\r\n"); + replies++; + } + + p = ST + 2; + } + + } + + return 0; +} From 503740f8365aa67197a8ef6f710852b5caec362c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 7 Oct 2022 21:47:56 +0200 Subject: [PATCH 0173/1323] pgo: execute xtgettcap utility, to get profiling data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: ../utils/xtgettcap.c:175:1: error: ‘/home/daniel/src/foot/src/utils/xtgettcap.p/xtgettcap.c.gcda’ profile count data file not found [-Werror=missing-profile] --- pgo/full-inner.sh | 1 + pgo/partial.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/pgo/full-inner.sh b/pgo/full-inner.sh index 599d6aad..425d1ff0 100755 --- a/pgo/full-inner.sh +++ b/pgo/full-inner.sh @@ -15,6 +15,7 @@ rm -f "${blddir}"/pgo-ok # To ensure profiling data is generated in the build directory cd "${blddir}" +"${blddir}"/utils/xtgettcap name "${blddir}"/footclient --version "${blddir}"/foot \ --config=/dev/null \ diff --git a/pgo/partial.sh b/pgo/partial.sh index c16de324..17de7175 100755 --- a/pgo/partial.sh +++ b/pgo/partial.sh @@ -21,6 +21,7 @@ rm -f "${blddir}"/pgo-ok # To ensure profiling data is generated in the build directory cd "${blddir}" +"${blddir}"/utils/xtgettcap name "${blddir}"/footclient --version "${blddir}"/foot --version "${blddir}"/pgo "${pgo_data}" From f747650b770140d7ddf87dcf507157f9681e443e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 8 Oct 2022 16:56:28 +0200 Subject: [PATCH 0174/1323] install.md: add xtgettcap to PGO build instructions --- INSTALL.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index da3a667e..86280079 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -327,6 +327,7 @@ We will use the `pgo` binary along with input corpus generated by `scripts/generate-alt-random-writes.py`: ```sh +./utils/xtgettcap name ./footclient --version ./foot --version tmp_file=$(mktemp) @@ -349,9 +350,10 @@ rm ${tmp_file} ``` The first step, running `./foot --version` and `./footclient ---version` might seem unnecessary, but is needed to ensure we have -_some_ profiling data for functions not covered by the PGO helper -binary. Without this, the final link phase will fail. +--version` etc, might seem unnecessary, but is needed to ensure we +have _some_ profiling data for functions not covered by the PGO helper +binary, for **all** binaries. Without this, the final link phase will +fail. The snippet above then creates an (empty) temporary file. Then, it runs a script that generates random escape sequences (if you cat From 4fca380585b0ccddcef3082b08cbe9e5c3131e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 9 Oct 2022 16:27:10 +0200 Subject: [PATCH 0175/1323] install.md: add `./utils/xtgettcap name` to "full PGO" instructions too --- INSTALL.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 86280079..d2f4e3d4 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -373,6 +373,7 @@ This method requires a running Wayland session. We will use the script `scripts/generate-alt-random-writes.py`: ```sh +./utils/xtgettcap name ./footclient --version foot_tmp_file=$(mktemp) ./foot \ @@ -386,9 +387,10 @@ rm ${foot_tmp_file} You should see a foot window open up, with random colored text. The window should close after ~1-2s. -The first step, `./footclient --version` might seem unnecessary, but -is needed to ensure we have _some_ profiling data for -`footclient`. Without this, the final link phase will fail. +The first step, `./utils/xtgettcap name && ./footclient --version` +might seem unnecessary, but is needed to ensure we have _some_ +profiling data for **all** binaries we build. Without this, the final +link phase will fail. ##### Use the generated PGO data From 807e19385480696ce9fbd8dbdee946df224eaddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 10 Oct 2022 17:17:38 +0200 Subject: [PATCH 0176/1323] xtgettcap: exit immediately when there are no capabilities to query for --- utils/xtgettcap.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/utils/xtgettcap.c b/utils/xtgettcap.c index e21de7c0..b3ab712a 100644 --- a/utils/xtgettcap.c +++ b/utils/xtgettcap.c @@ -67,10 +67,13 @@ unhexlify(char *dst, const char *src) int main(int argc, const char *const *argv) { - enable_raw_mode(); - const size_t query_count = argc - 1; + if (query_count == 0) + return 0; + + enable_raw_mode(); + printf("\x1bP+q"); for (int i = 1; i < argc; i++) printf("%s%s", i > 1 ? ";" : "", hexlify(argv[i])); From a9fc7ce1806d92779da9931c804a88ac444344ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 10 Oct 2022 17:18:04 +0200 Subject: [PATCH 0177/1323] pgo: run xtgettcap without any arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We execute xtgettcap in the parent terminal. Thus we don’t know if it implements XTGETTCAP, and thus it’s not guaranteed to exit - it may hang indefinitely waiting for a reply. Fix by not actually quering anything. --- INSTALL.md | 6 +++--- pgo/full-inner.sh | 2 +- pgo/partial.sh | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index d2f4e3d4..4c15b7b4 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -327,7 +327,7 @@ We will use the `pgo` binary along with input corpus generated by `scripts/generate-alt-random-writes.py`: ```sh -./utils/xtgettcap name +./utils/xtgettcap ./footclient --version ./foot --version tmp_file=$(mktemp) @@ -373,7 +373,7 @@ This method requires a running Wayland session. We will use the script `scripts/generate-alt-random-writes.py`: ```sh -./utils/xtgettcap name +./utils/xtgettcap ./footclient --version foot_tmp_file=$(mktemp) ./foot \ @@ -387,7 +387,7 @@ rm ${foot_tmp_file} You should see a foot window open up, with random colored text. The window should close after ~1-2s. -The first step, `./utils/xtgettcap name && ./footclient --version` +The first step, `./utils/xtgettcap && ./footclient --version` might seem unnecessary, but is needed to ensure we have _some_ profiling data for **all** binaries we build. Without this, the final link phase will fail. diff --git a/pgo/full-inner.sh b/pgo/full-inner.sh index 425d1ff0..c2205e5e 100755 --- a/pgo/full-inner.sh +++ b/pgo/full-inner.sh @@ -15,7 +15,7 @@ rm -f "${blddir}"/pgo-ok # To ensure profiling data is generated in the build directory cd "${blddir}" -"${blddir}"/utils/xtgettcap name +"${blddir}"/utils/xtgettcap "${blddir}"/footclient --version "${blddir}"/foot \ --config=/dev/null \ diff --git a/pgo/partial.sh b/pgo/partial.sh index 17de7175..6d6fdffe 100755 --- a/pgo/partial.sh +++ b/pgo/partial.sh @@ -21,7 +21,7 @@ rm -f "${blddir}"/pgo-ok # To ensure profiling data is generated in the build directory cd "${blddir}" -"${blddir}"/utils/xtgettcap name +"${blddir}"/utils/xtgettcap "${blddir}"/footclient --version "${blddir}"/foot --version "${blddir}"/pgo "${pgo_data}" From 8179d73daa95dc3b3bdd76eaecc78e456bb4686a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 5 Oct 2022 17:05:44 +0200 Subject: [PATCH 0178/1323] =?UTF-8?q?render:=20delay=20reflow=20for=20?= =?UTF-8?q?=E2=80=98resize-delay-ms=E2=80=99=20milliseconds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reflowing a large scrollback is *slow*. During an interactive resize, it can easily take long enough that the compositor fills the Wayland socket with configure events. Eventually, the socket becomes full and the compositor terminates the connection, causing foot to exit. This patch is work-in-progress, and the first step towards alleviating this. It delays the reflow by: * Snapshotting (copying) the original grid when an interactive resize is started. * While resizing, we apply a simple truncation resize of the grid (like we handle the alt screen). * When the resize is done, or paused for ‘resize-delay-ms’, the grid is reflowed. TODO: we *must* not allow any changes to the temporary (truncated) grid during the resize. Any changes to the grid would be lost when the final reflow is applied. That is, we must completely pause the ptmx pipe while a resize is in progress. Future improvements: The initial copy can be slow. We should be able to avoid it by rewriting the reflow algorithm to not free anything. This is complicated by the fact that some resources (e.g. sixel images) are currently *moved* to the new grid. They’d instead have to be copied. --- doc/foot.ini.5.scd | 18 ++++--- grid.c | 2 + render.c | 118 ++++++++++++++++++++++++++++++++++++++++----- terminal.h | 5 ++ 4 files changed, 123 insertions(+), 20 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 46a6d33e..6fe97c09 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -213,15 +213,19 @@ commented out will usually be installed to */etc/xdg/foot/foot.ini*. Default: _0x0_. *resize-delay-ms* - Time, in milliseconds, of "idle time" before foot sends the new - window dimensions to the client application while doing an - interactive resize of a foot window. Idle time in this context is - a period of time where the window size is not changing. + + Time, in milliseconds, of "idle time" before foot performs text + reflow, and sends the new window dimensions to the client + application while doing an interactive resize of a foot + window. Idle time in this context is a period of time where the + window size is not changing. In other words, while you are fiddling with the window size, foot - does not send the updated dimensions to the client. Only when you - pause the fiddling for *resize-delay-ms* milliseconds is the - client updated. + does not send the updated dimensions to the client. It also does a + fast "truncating" resize of the grid, instead of actually + reflowing the contents. Only when you pause the fiddling for + *resize-delay-ms* milliseconds is the client updated, and the + contents properly reflowed. Emphasis is on _while_ here; as soon as the interactive resize ends (i.e. when you let go of the window border), the final diff --git a/grid.c b/grid.c index 7bfef5cb..bccac529 100644 --- a/grid.c +++ b/grid.c @@ -210,6 +210,8 @@ grid_snapshot(const struct grid *grid) clone->offset = grid->offset; clone->view = grid->view; clone->cursor = grid->cursor; + clone->saved_cursor = grid->saved_cursor; + clone->kitty_kbd = grid->kitty_kbd; clone->rows = xcalloc(grid->num_rows, sizeof(clone->rows[0])); memset(&clone->scroll_damage, 0, sizeof(clone->scroll_damage)); memset(&clone->sixel_images, 0, sizeof(clone->sixel_images)); diff --git a/render.c b/render.c index f14911d4..034c9946 100644 --- a/render.c +++ b/render.c @@ -3663,13 +3663,54 @@ tiocswinsz(struct terminal *term) } } +static void +delayed_reflow_of_normal_grid(struct terminal *term) +{ + if (term->render.resizing.grid == NULL) + return; + + struct coord *const tracking_points[] = { + &term->selection.coords.start, + &term->selection.coords.end, + }; + + /* Reflow the original (since before the resize was started) grid, + * to the *current* dimensions */ + grid_resize_and_reflow( + term->render.resizing.grid, + term->grid->num_rows, term->grid->num_cols, + term->render.resizing.screen_rows, term->rows, + term->selection.coords.end.row >= 0 ? ALEN(tracking_points) : 0, + tracking_points); + + /* Replace the current, truncated, “normal” grid with the + * correctly reflowed one */ + grid_free(&term->normal); + term->normal = *term->render.resizing.grid; + free(term->render.resizing.grid); + + /* Reset */ + term->render.resizing.grid = NULL; + term->render.resizing.screen_rows = 0; + + /* Invalidate render pointers */ + shm_unref(term->render.last_buf); + term->render.last_buf = NULL; + term->render.last_cursor.row = NULL; + + if (term->grid == &term->normal) + term_damage_view(term); +} + static bool fdm_tiocswinsz(struct fdm *fdm, int fd, int events, void *data) { struct terminal *term = data; - if (events & EPOLLIN) + if (events & EPOLLIN) { tiocswinsz(term); + delayed_reflow_of_normal_grid(term); + } if (term->window->resize_timeout_fd >= 0) { fdm_del(fdm, term->window->resize_timeout_fd); @@ -3686,6 +3727,7 @@ send_dimensions_to_client(struct terminal *term) if (!win->is_resizing || term->conf->resize_delay_ms == 0) { /* Send new dimensions to client immediately */ tiocswinsz(term); + delayed_reflow_of_normal_grid(term); /* And make sure to reset and deallocate a lingering timer */ if (win->resize_timeout_fd >= 0) { @@ -3846,9 +3888,30 @@ maybe_resize(struct terminal *term, int width, int height, bool force) const uint32_t scrollback_lines = term->render.scrollback_lines; + /* + * Snapshot the “normal” grid. + * + * Since text reflow is slow, don’t do it *while* resizing. Only + * do it when done, or after “pausing” the resize for sufficiently + * long. We re-use the TIOCSWINSZ timer to handle this. See + * send_dimensions_to_client() and fdm_tiocswinsz(). + * + * To be able to do the final reflow correctly, we need a copy of + * the original grid, before the resize started. + */ + if (term->window->is_resizing && term->render.resizing.grid == NULL) { + /* + * TODO: snapshotting a large grid is slow. To improve, move + * normal -> resizing.grid, and instantiate a small (screen + * sized) new “normal” + */ + term->render.resizing.grid = grid_snapshot(&term->normal); + term->render.resizing.screen_rows = term->rows; + } + /* Screen rows/cols before resize */ - const int old_cols = term->cols; - const int old_rows = term->rows; + int old_cols = term->cols; + int old_rows = term->rows; /* Screen rows/cols after resize */ const int new_cols = (term->width - 2 * pad_x) / term->cell_width; @@ -3882,7 +3945,9 @@ maybe_resize(struct terminal *term, int width, int height, bool force) xassert(term->margins.top >= pad_y); xassert(term->margins.bottom >= pad_y); - if (new_cols == old_cols && new_rows == old_rows) { + if (new_cols == old_cols && new_rows == old_rows && + (term->render.resizing.grid == NULL || term->window->is_resizing)) + { LOG_DBG("grid layout unaffected; skipping reflow"); goto damage_view; } @@ -3906,16 +3971,43 @@ maybe_resize(struct terminal *term, int width, int height, bool force) * selection’s pivot point coordinates *must* be added to the * tracking points list. */ - struct coord *const tracking_points[] = { - &term->selection.coords.start, - &term->selection.coords.end, - }; - /* Resize grids */ - grid_resize_and_reflow( - &term->normal, new_normal_grid_rows, new_cols, old_rows, new_rows, - term->selection.coords.end.row >= 0 ? ALEN(tracking_points) : 0, - tracking_points); + if (term->window->is_resizing) { + /* Simple truncating resize, *while* an interactive resize is + * ongoing. */ + xassert(term->render.resizing.grid != NULL); + grid_resize_without_reflow( + &term->normal, + new_normal_grid_rows, new_cols, + old_rows, + new_rows); + } else { + /* Full text reflow */ + + if (term->render.resizing.grid != NULL) { + /* Throw away the current, truncated, “normal” grid, and + * use the original grid instead (from before the resize + * started) */ + grid_free(&term->normal); + term->normal = *term->render.resizing.grid; + free(term->render.resizing.grid); + + old_rows = term->render.resizing.screen_rows; + + term->render.resizing.grid = NULL; + term->render.resizing.screen_rows = 0; + } + + struct coord *const tracking_points[] = { + &term->selection.coords.start, + &term->selection.coords.end, + }; + + grid_resize_and_reflow( + &term->normal, new_normal_grid_rows, new_cols, old_rows, new_rows, + term->selection.coords.end.row >= 0 ? ALEN(tracking_points) : 0, + tracking_points); + } grid_resize_without_reflow( &term->alt, new_alt_grid_rows, new_cols, old_rows, new_rows); diff --git a/terminal.h b/terminal.h index 0dde6330..3afd101d 100644 --- a/terminal.h +++ b/terminal.h @@ -595,6 +595,11 @@ struct terminal { size_t search_glyph_offset; + struct { + struct grid *grid; + int screen_rows; + } resizing; + struct timespec input_time; } render; From 3565cbd636dacf15049504d686219b787783733c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 6 Oct 2022 17:09:32 +0200 Subject: [PATCH 0179/1323] render: performance improvements during interactive resize Instead of copying the entire grid when an interactive resize is started, stash the complete grid (to be used in the final reflow). Copy the current viewport only, to be used during the interactive resize. This gets rid of the initial "pause" when snapshotting the grid when an interactive resize is started. --- render.c | 44 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/render.c b/render.c index 034c9946..d2fce8b0 100644 --- a/render.c +++ b/render.c @@ -3900,13 +3900,45 @@ maybe_resize(struct terminal *term, int width, int height, bool force) * the original grid, before the resize started. */ if (term->window->is_resizing && term->render.resizing.grid == NULL) { - /* - * TODO: snapshotting a large grid is slow. To improve, move - * normal -> resizing.grid, and instantiate a small (screen - * sized) new “normal” - */ - term->render.resizing.grid = grid_snapshot(&term->normal); + /* Stash the current ‘normal’ grid, as-is, to be used when + * doing the final reflow */ term->render.resizing.screen_rows = term->rows; + term->render.resizing.grid = xmalloc(sizeof(*term->render.resizing.grid)); + *term->render.resizing.grid = term->normal; + + + /* + * Copy the current viewport to a new grid that will be used + * during the resize. For now, throw away sixels and OSC-8 + * URLs. They’ll be "restored" when we do the final reflow. + * + * TODO: + * - sixels? + * - OSC-8? + */ + struct grid g = { + .num_rows = 1 << (32 - __builtin_clz(term->rows - 1)), + .num_cols = term->cols, + .offset = 0, + .view = 0, + .cursor = term->normal.cursor, + .saved_cursor = term->normal.saved_cursor, + .rows = xcalloc(g.num_rows, sizeof(g.rows[0])), + .cur_row = NULL, + .scroll_damage = tll_init(), + .sixel_images = tll_init(), + .kitty_kbd = term->normal.kitty_kbd, + }; + + for (size_t i = 0, j = term->normal.view; i < term->rows; + i++, j = (j + 1) & (term->normal.num_rows - 1)) + { + g.rows[i] = grid_row_alloc(term->cols, false); + memcpy(g.rows[i]->cells, term->normal.rows[j]->cells, + term->cols * sizeof(g.rows[i]->cells[0])); + } + + term->normal = g; } /* Screen rows/cols before resize */ From f4f1989b6ef341557cc90a6805d745fef2a7a815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 6 Oct 2022 17:23:56 +0200 Subject: [PATCH 0180/1323] render: resize: ignore ptmx read events during interactive resize --- render.c | 4 ++++ terminal.c | 12 ++++++++++++ terminal.h | 3 +++ 3 files changed, 19 insertions(+) diff --git a/render.c b/render.c index d2fce8b0..3cb544cd 100644 --- a/render.c +++ b/render.c @@ -3700,6 +3700,8 @@ delayed_reflow_of_normal_grid(struct terminal *term) if (term->grid == &term->normal) term_damage_view(term); + + term_ptmx_resume(term); } static bool @@ -3939,6 +3941,7 @@ maybe_resize(struct terminal *term, int width, int height, bool force) } term->normal = g; + term_ptmx_pause(term); } /* Screen rows/cols before resize */ @@ -4028,6 +4031,7 @@ maybe_resize(struct terminal *term, int width, int height, bool force) term->render.resizing.grid = NULL; term->render.resizing.screen_rows = 0; + term_ptmx_resume(term); } struct coord *const tracking_points[] = { diff --git a/terminal.c b/terminal.c index df17201b..ee12ae32 100644 --- a/terminal.c +++ b/terminal.c @@ -233,6 +233,18 @@ static struct timespec last = {0}; static bool cursor_blink_rearm_timer(struct terminal *term); +void +term_ptmx_pause(struct terminal *term) +{ + fdm_event_del(term->fdm, term->ptmx, EPOLLIN); +} + +void +term_ptmx_resume(struct terminal *term) +{ + fdm_event_add(term->fdm, term->ptmx, EPOLLIN); +} + /* Externally visible, but not declared in terminal.h, to enable pgo * to call this function directly */ bool diff --git a/terminal.h b/terminal.h index 3afd101d..7a281e72 100644 --- a/terminal.h +++ b/terminal.h @@ -810,6 +810,9 @@ void term_collect_urls(struct terminal *term); void term_osc8_open(struct terminal *term, uint64_t id, const char *uri); void term_osc8_close(struct terminal *term); +void term_ptmx_pause(struct terminal *term); +void term_ptmx_resume(struct terminal *term); + static inline void term_reset_grapheme_state(struct terminal *term) { #if defined(FOOT_GRAPHEME_CLUSTERING) From b52262da8e422a9b3b62a642bf7108ab03fa4cdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 6 Oct 2022 17:26:38 +0200 Subject: [PATCH 0181/1323] changelog: fixed crash when resizing window with a very large scrollback --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1606f3b4..f97cb252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,8 @@ `resize-delay-ms` option. * Missing backslash in ST terminator in escape sequences in the built-in terminfo (accessed via XTGETTCAP). +* Crash when interactively resizing the window with a very large + scrollback. [1173]: https://codeberg.org/dnkl/foot/issues/1173 From f70c34c5a8c6e89f6591e08fe3e26b9f0f44e6d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 9 Oct 2022 16:01:11 +0200 Subject: [PATCH 0182/1323] sixel: add sixel_reflow_grid() This function reflows all sixels in the specified grid. The pre-existing sixel_reflow() function is a shortcut for sixel_reflow_grid(term, &term->normal) sixel_reflow_grid(term, &term->alt); --- sixel.c | 146 +++++++++++++++++++++++++++++--------------------------- sixel.h | 4 ++ 2 files changed, 79 insertions(+), 71 deletions(-) diff --git a/sixel.c b/sixel.c index 0c94117f..c80a92a3 100644 --- a/sixel.c +++ b/sixel.c @@ -838,85 +838,89 @@ sixel_cell_size_changed(struct terminal *term) } void -sixel_reflow(struct terminal *term) +sixel_reflow_grid(struct terminal *term, struct grid *grid) { - struct grid *g = term->grid; + /* Meh - the sixel functions we call use term->grid... */ + struct grid *active_grid = term->grid; + term->grid = grid; - for (size_t i = 0; i < 2; i++) { - struct grid *grid = i == 0 ? &term->normal : &term->alt; + /* Need the “real” list to be empty from the beginning */ + tll(struct sixel) copy = tll_init(); + tll_foreach(grid->sixel_images, it) + tll_push_back(copy, it->item); + tll_free(grid->sixel_images); - term->grid = grid; + tll_rforeach(copy, it) { + struct sixel *six = &it->item; + int start = six->pos.row; + int end = (start + six->rows - 1) & (grid->num_rows - 1); - /* Need the “real” list to be empty from the beginning */ - tll(struct sixel) copy = tll_init(); - tll_foreach(grid->sixel_images, it) - tll_push_back(copy, it->item); - tll_free(grid->sixel_images); - - tll_rforeach(copy, it) { - struct sixel *six = &it->item; - int start = six->pos.row; - int end = (start + six->rows - 1) & (grid->num_rows - 1); - - if (end < start) { - /* Crosses scrollback wrap-around */ - /* TODO: split image */ - sixel_destroy(six); - continue; - } - - if (six->rows > grid->num_rows) { - /* Image too large */ - /* TODO: keep bottom part? */ - sixel_destroy(six); - continue; - } - - /* Drop sixels that now cross the current scrollback end - * border. This is similar to a sixel that have been - * scrolled out */ - /* TODO: should be possible to optimize this */ - bool sixel_destroyed = false; - int last_row = -1; - - for (int j = 0; j < six->rows; j++) { - int row_no = grid_row_abs_to_sb( - term->grid, term->rows, six->pos.row + j); - if (last_row != -1 && last_row >= row_no) { - sixel_destroy(six); - sixel_destroyed = true; - break; - } - - last_row = row_no; - } - - if (sixel_destroyed) { - LOG_WARN("destroyed sixel that now crossed history"); - continue; - } - - /* Sixels that didn’t overlap may now do so, which isn’t - * allowed of course */ - _sixel_overwrite_by_rectangle( - term, six->pos.row, six->pos.col, six->rows, six->cols, - &it->item.pix, &it->item.opaque); - - if (it->item.data != pixman_image_get_data(it->item.pix)) { - it->item.data = pixman_image_get_data(it->item.pix); - it->item.width = pixman_image_get_width(it->item.pix); - it->item.height = pixman_image_get_height(it->item.pix); - it->item.cols = (it->item.width + term->cell_width - 1) / term->cell_width; - it->item.rows = (it->item.height + term->cell_height - 1) / term->cell_height; - } - - sixel_insert(term, it->item); + if (end < start) { + /* Crosses scrollback wrap-around */ + /* TODO: split image */ + sixel_destroy(six); + continue; } - tll_free(copy); + if (six->rows > grid->num_rows) { + /* Image too large */ + /* TODO: keep bottom part? */ + sixel_destroy(six); + continue; + } + + /* Drop sixels that now cross the current scrollback end + * border. This is similar to a sixel that have been + * scrolled out */ + /* TODO: should be possible to optimize this */ + bool sixel_destroyed = false; + int last_row = -1; + + for (int j = 0; j < six->rows; j++) { + int row_no = grid_row_abs_to_sb( + term->grid, term->rows, six->pos.row + j); + if (last_row != -1 && last_row >= row_no) { + sixel_destroy(six); + sixel_destroyed = true; + break; + } + + last_row = row_no; + } + + if (sixel_destroyed) { + LOG_WARN("destroyed sixel that now crossed history"); + continue; + } + + /* Sixels that didn’t overlap may now do so, which isn’t + * allowed of course */ + _sixel_overwrite_by_rectangle( + term, six->pos.row, six->pos.col, six->rows, six->cols, + &it->item.pix, &it->item.opaque); + + if (it->item.data != pixman_image_get_data(it->item.pix)) { + it->item.data = pixman_image_get_data(it->item.pix); + it->item.width = pixman_image_get_width(it->item.pix); + it->item.height = pixman_image_get_height(it->item.pix); + it->item.cols = (it->item.width + term->cell_width - 1) / term->cell_width; + it->item.rows = (it->item.height + term->cell_height - 1) / term->cell_height; + } + + sixel_insert(term, it->item); } - term->grid = g; + tll_free(copy); + term->grid = active_grid; +} + +void +sixel_reflow(struct terminal *term) +{ + for (size_t i = 0; i < 2; i++) { + struct grid *grid = i == 0 ? &term->normal : &term->alt; + sixel_reflow_grid(term, grid); + } } void diff --git a/sixel.h b/sixel.h index a57957c3..f72b4dc4 100644 --- a/sixel.h +++ b/sixel.h @@ -19,6 +19,10 @@ void sixel_scroll_up(struct terminal *term, int rows); void sixel_scroll_down(struct terminal *term, int rows); void sixel_cell_size_changed(struct terminal *term); + +void sixel_reflow_grid(struct terminal *term, struct grid *grid); + +/* Shortcut for sixel_reflow_grid(normal) + sixel_reflow_grid(alt) */ void sixel_reflow(struct terminal *term); /* From d4b0b0887e73c80cf5ff22fdbac82947897b2b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 9 Oct 2022 16:11:49 +0200 Subject: [PATCH 0183/1323] render: delayed reflow: not enough to damage current view; need to refresh too --- render.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/render.c b/render.c index 3cb544cd..c7174228 100644 --- a/render.c +++ b/render.c @@ -3698,8 +3698,10 @@ delayed_reflow_of_normal_grid(struct terminal *term) term->render.last_buf = NULL; term->render.last_cursor.row = NULL; - if (term->grid == &term->normal) + if (term->grid == &term->normal) { term_damage_view(term); + render_refresh(term); + } term_ptmx_resume(term); } From 18ef36523f55cb8c8a6a9a9c3269274cc08c0a29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 9 Oct 2022 16:12:18 +0200 Subject: [PATCH 0184/1323] grid: resize: assert grid->cur_row is not NULL after a grid resize --- grid.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/grid.c b/grid.c index bccac529..3bcc8a55 100644 --- a/grid.c +++ b/grid.c @@ -485,6 +485,8 @@ grid_resize_without_reflow( grid->saved_cursor.point = saved_cursor; grid->cur_row = new_grid[(grid->offset + cursor.row) & (new_rows - 1)]; + xassert(grid->cur_row != NULL); + grid->cursor.lcf = false; grid->saved_cursor.lcf = false; @@ -1047,6 +1049,8 @@ grid_resize_and_reflow( saved_cursor.col = min(saved_cursor.col, new_cols - 1); grid->cur_row = new_grid[(grid->offset + cursor.row) & (new_rows - 1)]; + xassert(grid->cur_row != NULL); + grid->cursor.point = cursor; grid->saved_cursor.point = saved_cursor; From 66e4592d91312568c8948ddda850ba8b1c31c87d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 9 Oct 2022 16:14:49 +0200 Subject: [PATCH 0185/1323] term: use SIZE_MAX instead of (size_t)-1ll --- terminal.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/terminal.c b/terminal.c index ee12ae32..5dd261bb 100644 --- a/terminal.c +++ b/terminal.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -268,7 +269,7 @@ fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) } uint8_t buf[24 * 1024]; - const size_t max_iterations = !hup ? 10 : (size_t)-1ll; + const size_t max_iterations = !hup ? 10 : SIZE_MAX; for (size_t i = 0; i < max_iterations && pollin; i++) { xassert(pollin); From 54d637e2b47fd55e4abde1e28f7f665e69c2a995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 9 Oct 2022 16:15:29 +0200 Subject: [PATCH 0186/1323] =?UTF-8?q?term:=20ptmx:=20don=E2=80=99t=20consu?= =?UTF-8?q?me=20anything=20while=20doing=20an=20interactive=20resize?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ‘normal’ grid in use during an interactive resize is temporary; all changes done to it will be lost when the resize is finished. --- terminal.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/terminal.c b/terminal.c index 5dd261bb..4fb13f44 100644 --- a/terminal.c +++ b/terminal.c @@ -268,6 +268,16 @@ fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) cursor_blink_rearm_timer(term); } + if (unlikely(term->interactive_resizing.grid != NULL)) { + /* + * Don’t consume PTMX while we’re doing an interactive resize, + * since the ‘normal’ grid we’re currently using is a + * temporary one - all changes done to it will be lost when + * the interactive resize ends. + */ + return 0; + } + uint8_t buf[24 * 1024]; const size_t max_iterations = !hup ? 10 : SIZE_MAX; @@ -291,6 +301,7 @@ fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) break; } + xassert(term->interactive_resizing.grid == NULL); vt_from_slave(term, buf, count); } From c5c97c2fd4a674d0fcce16d40e0e56994195e9b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 9 Oct 2022 16:16:23 +0200 Subject: [PATCH 0187/1323] term_ptmx_{pause,resume}: return success/fail --- terminal.c | 24 ++++++++++++------------ terminal.h | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/terminal.c b/terminal.c index 4fb13f44..16486d82 100644 --- a/terminal.c +++ b/terminal.c @@ -234,18 +234,6 @@ static struct timespec last = {0}; static bool cursor_blink_rearm_timer(struct terminal *term); -void -term_ptmx_pause(struct terminal *term) -{ - fdm_event_del(term->fdm, term->ptmx, EPOLLIN); -} - -void -term_ptmx_resume(struct terminal *term) -{ - fdm_event_add(term->fdm, term->ptmx, EPOLLIN); -} - /* Externally visible, but not declared in terminal.h, to enable pgo * to call this function directly */ bool @@ -382,6 +370,18 @@ fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) return true; } +bool +term_ptmx_pause(struct terminal *term) +{ + return fdm_event_del(term->fdm, term->ptmx, EPOLLIN); +} + +bool +term_ptmx_resume(struct terminal *term) +{ + return fdm_event_add(term->fdm, term->ptmx, EPOLLIN); +} + static bool fdm_flash(struct fdm *fdm, int fd, int events, void *data) { diff --git a/terminal.h b/terminal.h index 7a281e72..16c8e776 100644 --- a/terminal.h +++ b/terminal.h @@ -810,8 +810,8 @@ void term_collect_urls(struct terminal *term); void term_osc8_open(struct terminal *term, uint64_t id, const char *uri); void term_osc8_close(struct terminal *term); -void term_ptmx_pause(struct terminal *term); -void term_ptmx_resume(struct terminal *term); +bool term_ptmx_pause(struct terminal *term); +bool term_ptmx_resume(struct terminal *term); static inline void term_reset_grapheme_state(struct terminal *term) { From c550d67cd856ba806c616ac2f15bdbca283e0a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 9 Oct 2022 16:16:50 +0200 Subject: [PATCH 0188/1323] render: resize: do delayed reflow immediately when failing to arm tiocswinsz timer --- render.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/render.c b/render.c index c7174228..2f79c6c1 100644 --- a/render.c +++ b/render.c @@ -3776,8 +3776,10 @@ send_dimensions_to_client(struct terminal *term) successfully_scheduled = true; } - if (!successfully_scheduled) + if (!successfully_scheduled) { tiocswinsz(term); + delayed_reflow_of_normal_grid(term); + } } } From 298f210ed93a83a531a44f772172da7f64510217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 9 Oct 2022 16:17:22 +0200 Subject: [PATCH 0189/1323] render: rename term->render.resizing -> term->interactive_resizing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit But also, more importantly, logical fixes: * Stash the number of new scrollback lines the stashed ‘normal’ grid should be resized *to*. There’s also a couple of performance changes here: * When doing a delayed reflow (tiocswinsz timer), call sixel_reflow_grid(term, &term->normal) - there’s no need to reflow sixels in the ‘alt’ screen. * When doing a delayed reflow, free all scroll damage. It’s not needed, since we’re damaging the entire window anyway. * Use minimum size for the temporary ‘normal’ grid (that contains the current viewport). We just need it to be large enough to fit the current viewport, and be a valid grid row count (power of 2). This just so happens to be the current ‘alt’ grid’s row count... --- render.c | 68 +++++++++++++++++++++++++++++++----------------------- terminal.h | 11 +++++---- 2 files changed, 45 insertions(+), 34 deletions(-) diff --git a/render.c b/render.c index 2f79c6c1..d50606cb 100644 --- a/render.c +++ b/render.c @@ -3666,9 +3666,11 @@ tiocswinsz(struct terminal *term) static void delayed_reflow_of_normal_grid(struct terminal *term) { - if (term->render.resizing.grid == NULL) + if (term->interactive_resizing.grid == NULL) return; + xassert(term->interactive_resizing.new_rows > 0); + struct coord *const tracking_points[] = { &term->selection.coords.start, &term->selection.coords.end, @@ -3677,27 +3679,31 @@ delayed_reflow_of_normal_grid(struct terminal *term) /* Reflow the original (since before the resize was started) grid, * to the *current* dimensions */ grid_resize_and_reflow( - term->render.resizing.grid, - term->grid->num_rows, term->grid->num_cols, - term->render.resizing.screen_rows, term->rows, + term->interactive_resizing.grid, + term->interactive_resizing.new_rows, term->normal.num_cols, + term->interactive_resizing.old_screen_rows, term->rows, term->selection.coords.end.row >= 0 ? ALEN(tracking_points) : 0, tracking_points); /* Replace the current, truncated, “normal” grid with the * correctly reflowed one */ grid_free(&term->normal); - term->normal = *term->render.resizing.grid; - free(term->render.resizing.grid); + term->normal = *term->interactive_resizing.grid; + free(term->interactive_resizing.grid); /* Reset */ - term->render.resizing.grid = NULL; - term->render.resizing.screen_rows = 0; + term->interactive_resizing.grid = NULL; + term->interactive_resizing.old_screen_rows = 0; + term->interactive_resizing.new_rows = 0; /* Invalidate render pointers */ shm_unref(term->render.last_buf); term->render.last_buf = NULL; term->render.last_cursor.row = NULL; + tll_free(term->normal.scroll_damage); + sixel_reflow_grid(term, &term->normal); + if (term->grid == &term->normal) { term_damage_view(term); render_refresh(term); @@ -3895,8 +3901,6 @@ maybe_resize(struct terminal *term, int width, int height, bool force) const uint32_t scrollback_lines = term->render.scrollback_lines; /* - * Snapshot the “normal” grid. - * * Since text reflow is slow, don’t do it *while* resizing. Only * do it when done, or after “pausing” the resize for sufficiently * long. We re-use the TIOCSWINSZ timer to handle this. See @@ -3905,25 +3909,30 @@ maybe_resize(struct terminal *term, int width, int height, bool force) * To be able to do the final reflow correctly, we need a copy of * the original grid, before the resize started. */ - if (term->window->is_resizing && term->render.resizing.grid == NULL) { + if (term->window->is_resizing && term->interactive_resizing.grid == NULL) { + term_ptmx_pause(term); + /* Stash the current ‘normal’ grid, as-is, to be used when * doing the final reflow */ - term->render.resizing.screen_rows = term->rows; - term->render.resizing.grid = xmalloc(sizeof(*term->render.resizing.grid)); - *term->render.resizing.grid = term->normal; - + term->interactive_resizing.old_screen_rows = term->rows; + term->interactive_resizing.grid = xmalloc(sizeof(*term->interactive_resizing.grid)); + *term->interactive_resizing.grid = term->normal; /* * Copy the current viewport to a new grid that will be used * during the resize. For now, throw away sixels and OSC-8 * URLs. They’ll be "restored" when we do the final reflow. * + * We use the ‘alt’ screen’s row count, since we don’t want to + * instantiate an unnecessarily large grid. + * * TODO: * - sixels? * - OSC-8? */ + xassert(1 << (32 - __builtin_clz(term->rows)) == term->alt.num_rows); struct grid g = { - .num_rows = 1 << (32 - __builtin_clz(term->rows - 1)), + .num_rows = term->alt.num_rows, .num_cols = term->cols, .offset = 0, .view = 0, @@ -3945,7 +3954,6 @@ maybe_resize(struct terminal *term, int width, int height, bool force) } term->normal = g; - term_ptmx_pause(term); } /* Screen rows/cols before resize */ @@ -3985,9 +3993,10 @@ maybe_resize(struct terminal *term, int width, int height, bool force) xassert(term->margins.bottom >= pad_y); if (new_cols == old_cols && new_rows == old_rows && - (term->render.resizing.grid == NULL || term->window->is_resizing)) + (term->interactive_resizing.grid == NULL || term->window->is_resizing)) { LOG_DBG("grid layout unaffected; skipping reflow"); + term->interactive_resizing.new_rows = new_normal_grid_rows; goto damage_view; } @@ -4014,27 +4023,28 @@ maybe_resize(struct terminal *term, int width, int height, bool force) if (term->window->is_resizing) { /* Simple truncating resize, *while* an interactive resize is * ongoing. */ - xassert(term->render.resizing.grid != NULL); + xassert(term->interactive_resizing.grid != NULL); + xassert(new_normal_grid_rows > 0); + term->interactive_resizing.new_rows = new_normal_grid_rows; + grid_resize_without_reflow( - &term->normal, - new_normal_grid_rows, new_cols, - old_rows, - new_rows); + &term->normal, new_alt_grid_rows, new_cols, old_rows, new_rows); } else { /* Full text reflow */ - if (term->render.resizing.grid != NULL) { + if (term->interactive_resizing.grid != NULL) { /* Throw away the current, truncated, “normal” grid, and * use the original grid instead (from before the resize * started) */ grid_free(&term->normal); - term->normal = *term->render.resizing.grid; - free(term->render.resizing.grid); + term->normal = *term->interactive_resizing.grid; + free(term->interactive_resizing.grid); - old_rows = term->render.resizing.screen_rows; + old_rows = term->interactive_resizing.old_screen_rows; - term->render.resizing.grid = NULL; - term->render.resizing.screen_rows = 0; + term->interactive_resizing.grid = NULL; + term->interactive_resizing.old_screen_rows = 0; + term->interactive_resizing.new_rows = 0; term_ptmx_resume(term); } diff --git a/terminal.h b/terminal.h index 16c8e776..d5ed1ba2 100644 --- a/terminal.h +++ b/terminal.h @@ -595,14 +595,15 @@ struct terminal { size_t search_glyph_offset; - struct { - struct grid *grid; - int screen_rows; - } resizing; - struct timespec input_time; } render; + struct { + struct grid *grid; /* Original ‘normal’ grid, before resize started */ + int old_screen_rows; /* term->rows before resize started */ + int new_rows; /* New number of scrollback rows */ + } interactive_resizing; + struct { enum { SIXEL_DECSIXEL, /* DECSIXEL body part ", $, -, ? ... ~ */ From 43a48f53d42a5de21e0a6eb6783863051ad8862a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 13 Oct 2022 17:52:34 +0200 Subject: [PATCH 0190/1323] =?UTF-8?q?sixel:=20don=E2=80=99t=20crash=20when?= =?UTF-8?q?=20sixel=20image=20exceeds=20current=20sixel=20max=20height?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When we try to resize a sixel past the current max height, we set col > image-width to signal this. This means ‘width’ could be smaller than ‘col’. When calculating how many sixels to emit in sixel_add_many(), we didnt’ account for this. The resulting value was -1, converted to ‘unsigned’. I.e. a very large value. This resulted in an assert triggering in sixel_add() in debug builds, and a crash in release builds. --- CHANGELOG.md | 1 + sixel.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f97cb252..cf6a2104 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,7 @@ built-in terminfo (accessed via XTGETTCAP). * Crash when interactively resizing the window with a very large scrollback. +* Crash when a sixel image exceeds the current sixel max height. [1173]: https://codeberg.org/dnkl/foot/issues/1173 diff --git a/sixel.c b/sixel.c index c80a92a3..a824c405 100644 --- a/sixel.c +++ b/sixel.c @@ -1295,7 +1295,7 @@ sixel_add_many(struct terminal *term, uint8_t c, unsigned count) if (unlikely(col + count - 1 >= width)) { resize_horizontally(term, col + count); width = term->sixel.image.width; - count = min(count, width - col); + count = min(count, max(width - col, 0)); } uint32_t color = term->sixel.color; From 3d9a429499841cca83b5c8359ea7e2b78509601f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 14 Oct 2022 18:00:48 +0200 Subject: [PATCH 0191/1323] term: reverse scroll: free scrolled out lines This ensures *everything* in the circular scrollback history, after the bottom of the screen, is either NULL rows, or belong to the scrollback *history* (as opposed to the future history). This fixes an issue when calculating the scrollback start, which for example would trigger a crash when moving the viewport (i.e. scrolling with the mouse, or PgUp/PgDown). Closes #1190 --- terminal.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/terminal.c b/terminal.c index 16486d82..380f0d4a 100644 --- a/terminal.c +++ b/terminal.c @@ -2676,6 +2676,18 @@ term_scroll_reverse_partial(struct terminal *term, selection_scroll_down(term, rows); } + /* Unallocate scrolled out lines */ + for (int r = region.end - rows; r < region.end; r++) { + const int abs_r = grid_row_absolute(term->grid, r); + struct row *row = term->grid->rows[abs_r]; + + grid_row_free(row); + term->grid->rows[abs_r] = NULL; + + if (term->render.last_cursor.row == row) + term->render.last_cursor.row = NULL; + } + sixel_scroll_down(term, rows); bool view_follows = term->grid->view == term->grid->offset; From 89744f8123aadd58b74ea7b5038c3302c1ed75ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 14 Oct 2022 18:03:00 +0200 Subject: [PATCH 0192/1323] selection: scroll down: handle non-full scrollback correctly When determining whether or not to cancel a selection (due to it, or part of it, being "scrolled out"), we assumed the scrollback was full. This patch changes the implementation to compare the scrollback relative selection coordinates with the scrollback relative end-of-the-scrollback coordinates. --- selection.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/selection.c b/selection.c index c94686e1..92f1f85f 100644 --- a/selection.c +++ b/selection.c @@ -133,13 +133,18 @@ selection_scroll_down(struct terminal *term, int rows) { xassert(term->selection.coords.end.row >= 0); + const struct grid *grid = term->grid; + const struct range *sel = &term->selection.coords; + + const int screen_end = + grid_row_abs_to_sb(grid, term->rows, grid->offset + term->rows - 1); const int rel_row_start = - grid_row_abs_to_sb(term->grid, term->rows, term->selection.coords.start.row); + grid_row_abs_to_sb(term->grid, term->rows, sel->start.row); const int rel_row_end = - grid_row_abs_to_sb(term->grid, term->rows, term->selection.coords.end.row); + grid_row_abs_to_sb(term->grid, term->rows, sel->end.row); const int actual_end = max(rel_row_start, rel_row_end); - if (actual_end + rows <= term->grid->num_rows) { + if (actual_end > screen_end - rows) { /* Part of the selection will be scrolled out, cancel it */ selection_cancel(term); } From 3c9a51afa69370efb73e0b8208415fac11a41a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 14 Oct 2022 18:05:12 +0200 Subject: [PATCH 0193/1323] changelog: crash after reverse-scrolling in the normal screen --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf6a2104..3022836d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,8 +89,11 @@ * Crash when interactively resizing the window with a very large scrollback. * Crash when a sixel image exceeds the current sixel max height. +* Crash after reverse-scrolling (`CSI Ps T`) in the ‘normal’ + (non-alternate) screen ([#1190][1190]). [1173]: https://codeberg.org/dnkl/foot/issues/1173 +[1190]: https://codeberg.org/dnkl/foot/issues/1190 ### Security From 0ac0d0647ae996bcef119d885cbd2d22a9940d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 17 Oct 2022 18:49:57 +0200 Subject: [PATCH 0194/1323] interactive resize: improve user experience MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-initialize the temporary ‘normal’ grid instance each time we receive a configure event while doing an interactive resize. This way, window content will not be "erased" when the window is first made smaller, then larger again. And, if the viewport is up in the scrollback history, increasing the window size will reveal more of the scrollback, instead of just being black. The last issue is the cursor; it’s currently not "stuck" where it should be. Instead, it follows the window around. This is due to two things: 1) the temporary grid we create is large enough to contain the current viewport, but not more than that. That means we can’t "scroll up", to hide the cursor. 2) grid_resize_without_reflow() doesn’t know anything about "interactive resizing". As such, it will ensure the cursor is bound to the new grid dimensions. I don’t yet have a solution for this. This patch implements a workaround to at least reduce the impact, by simply hiding the cursor while we’re doing an interactive resize. --- render.c | 141 ++++++++++++++++++++++++++++++----------------------- terminal.h | 2 + 2 files changed, 83 insertions(+), 60 deletions(-) diff --git a/render.c b/render.c index d50606cb..19ab5ef8 100644 --- a/render.c +++ b/render.c @@ -3691,10 +3691,13 @@ delayed_reflow_of_normal_grid(struct terminal *term) term->normal = *term->interactive_resizing.grid; free(term->interactive_resizing.grid); + term->hide_cursor = term->interactive_resizing.old_hide_cursor; + /* Reset */ term->interactive_resizing.grid = NULL; term->interactive_resizing.old_screen_rows = 0; term->interactive_resizing.new_rows = 0; + term->interactive_resizing.old_hide_cursor = false; /* Invalidate render pointers */ shm_unref(term->render.last_buf); @@ -3900,62 +3903,6 @@ maybe_resize(struct terminal *term, int width, int height, bool force) const uint32_t scrollback_lines = term->render.scrollback_lines; - /* - * Since text reflow is slow, don’t do it *while* resizing. Only - * do it when done, or after “pausing” the resize for sufficiently - * long. We re-use the TIOCSWINSZ timer to handle this. See - * send_dimensions_to_client() and fdm_tiocswinsz(). - * - * To be able to do the final reflow correctly, we need a copy of - * the original grid, before the resize started. - */ - if (term->window->is_resizing && term->interactive_resizing.grid == NULL) { - term_ptmx_pause(term); - - /* Stash the current ‘normal’ grid, as-is, to be used when - * doing the final reflow */ - term->interactive_resizing.old_screen_rows = term->rows; - term->interactive_resizing.grid = xmalloc(sizeof(*term->interactive_resizing.grid)); - *term->interactive_resizing.grid = term->normal; - - /* - * Copy the current viewport to a new grid that will be used - * during the resize. For now, throw away sixels and OSC-8 - * URLs. They’ll be "restored" when we do the final reflow. - * - * We use the ‘alt’ screen’s row count, since we don’t want to - * instantiate an unnecessarily large grid. - * - * TODO: - * - sixels? - * - OSC-8? - */ - xassert(1 << (32 - __builtin_clz(term->rows)) == term->alt.num_rows); - struct grid g = { - .num_rows = term->alt.num_rows, - .num_cols = term->cols, - .offset = 0, - .view = 0, - .cursor = term->normal.cursor, - .saved_cursor = term->normal.saved_cursor, - .rows = xcalloc(g.num_rows, sizeof(g.rows[0])), - .cur_row = NULL, - .scroll_damage = tll_init(), - .sixel_images = tll_init(), - .kitty_kbd = term->normal.kitty_kbd, - }; - - for (size_t i = 0, j = term->normal.view; i < term->rows; - i++, j = (j + 1) & (term->normal.num_rows - 1)) - { - g.rows[i] = grid_row_alloc(term->cols, false); - memcpy(g.rows[i]->cells, term->normal.rows[j]->cells, - term->cols * sizeof(g.rows[i]->cells[0])); - } - - term->normal = g; - } - /* Screen rows/cols before resize */ int old_cols = term->cols; int old_rows = term->rows; @@ -3992,14 +3939,84 @@ maybe_resize(struct terminal *term, int width, int height, bool force) xassert(term->margins.top >= pad_y); xassert(term->margins.bottom >= pad_y); - if (new_cols == old_cols && new_rows == old_rows && - (term->interactive_resizing.grid == NULL || term->window->is_resizing)) - { + if (new_cols == old_cols && new_rows == old_rows) { LOG_DBG("grid layout unaffected; skipping reflow"); term->interactive_resizing.new_rows = new_normal_grid_rows; goto damage_view; } + + /* + * Since text reflow is slow, don’t do it *while* resizing. Only + * do it when done, or after “pausing” the resize for sufficiently + * long. We re-use the TIOCSWINSZ timer to handle this. See + * send_dimensions_to_client() and fdm_tiocswinsz(). + * + * To be able to do the final reflow correctly, we need a copy of + * the original grid, before the resize started. + */ + if (term->window->is_resizing) { + if (term->interactive_resizing.grid == NULL) { + term_ptmx_pause(term); + + /* Stash the current ‘normal’ grid, as-is, to be used when + * doing the final reflow */ + term->interactive_resizing.old_screen_rows = term->rows; + term->interactive_resizing.old_cols = term->cols; + term->interactive_resizing.old_hide_cursor = term->hide_cursor; + term->interactive_resizing.grid = xmalloc(sizeof(*term->interactive_resizing.grid)); + *term->interactive_resizing.grid = term->normal; + } else { + /* We’ll replace the current temporary grid, with a new + * one (again based on the original grid) */ + grid_free(&term->normal); + } + + struct grid *orig = term->interactive_resizing.grid; + + /* + * Copy the current viewport (of the original grid) to a new + * grid that will be used during the resize. For now, throw + * away sixels and OSC-8 URLs. They’ll be "restored" when we + * do the final reflow. + * + * Note that OSC-8 URLs are perfectly ok to throw away; they + * cannot be interacted with during the resize. And, even if + * url.osc8-underline=always, the “underline” attribute is + * part of the cell, not the URI struct (and thus our faked + * grid will still render OSC-8 links underlined). + * + * TODO: + * - sixels? + */ + struct grid g = { + .num_rows = 1 << (32 - __builtin_clz(term->interactive_resizing.old_screen_rows)), + .num_cols = term->interactive_resizing.old_cols, + .offset = 0, + .view = 0, + .cursor = orig->cursor, + .saved_cursor = orig->saved_cursor, + .rows = xcalloc(g.num_rows, sizeof(g.rows[0])), + .cur_row = NULL, + .scroll_damage = tll_init(), + .sixel_images = tll_init(), + .kitty_kbd = orig->kitty_kbd, + }; + + for (size_t i = 0, j = orig->view; + i < term->interactive_resizing.old_screen_rows; + i++, j = (j + 1) & (orig->num_rows - 1)) + { + g.rows[i] = grid_row_alloc(g.num_cols, false); + memcpy(g.rows[i]->cells, + orig->rows[j]->cells, + g.num_cols * sizeof(g.rows[i]->cells[0])); + } + + term->normal = g; + term->hide_cursor = true; + } + if (term->grid == &term->alt) selection_cancel(term); else { @@ -4028,7 +4045,8 @@ maybe_resize(struct terminal *term, int width, int height, bool force) term->interactive_resizing.new_rows = new_normal_grid_rows; grid_resize_without_reflow( - &term->normal, new_alt_grid_rows, new_cols, old_rows, new_rows); + &term->normal, new_alt_grid_rows, new_cols, + term->interactive_resizing.old_screen_rows, new_rows); } else { /* Full text reflow */ @@ -4040,11 +4058,14 @@ maybe_resize(struct terminal *term, int width, int height, bool force) term->normal = *term->interactive_resizing.grid; free(term->interactive_resizing.grid); + term->hide_cursor = term->interactive_resizing.old_hide_cursor; + old_rows = term->interactive_resizing.old_screen_rows; term->interactive_resizing.grid = NULL; term->interactive_resizing.old_screen_rows = 0; term->interactive_resizing.new_rows = 0; + term->interactive_resizing.old_hide_cursor = false; term_ptmx_resume(term); } diff --git a/terminal.h b/terminal.h index d5ed1ba2..ec5560cd 100644 --- a/terminal.h +++ b/terminal.h @@ -601,6 +601,8 @@ struct terminal { struct { struct grid *grid; /* Original ‘normal’ grid, before resize started */ int old_screen_rows; /* term->rows before resize started */ + int old_cols; /* term->cols before resize started */ + int old_hide_cursor; /* term->hide_cursor before resize started */ int new_rows; /* New number of scrollback rows */ } interactive_resizing; From b0c30c7ed22acbf4033db18949ea43f50edd1382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 17 Oct 2022 20:16:53 +0200 Subject: [PATCH 0195/1323] doc: foot.ini: improve documentation of cursor.color --- doc/foot.ini.5.scd | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 6fe97c09..ee6aa94c 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -452,14 +452,13 @@ applications can change these at runtime. by applications. Default: _no_. *color* - Two RRGGBB values (i.e. plain old 6-digit hex values, without - prefix) specifying the foreground (text) and background (cursor) - colors for the cursor. + Two space separated RRGGBB values (i.e. plain old 6-digit hex + values, without prefix) specifying the foreground (text) and + background (cursor) colors for the cursor. - Default: _inverse foreground/background colors_. - - Note that this value only applies to the block cursor. The other - cursor styles are always rendered with the foreground color. + Example: *ff0000 00ff00* (green cursor, red text) + + Default: the regular foreground and background colors, reversed. *beam-thickness* Thickness (width) of the beam styled cursor. The value is in From 2e9b3ceb95597e2612b048f20fa006d123090e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 18 Oct 2022 18:28:51 +0200 Subject: [PATCH 0196/1323] =?UTF-8?q?fdm=5Fptmx():=20regression:=20don?= =?UTF-8?q?=E2=80=99t=20return=20false=20when=20an=20interactive=20resize?= =?UTF-8?q?=20is=20in=20progress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- terminal.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal.c b/terminal.c index 380f0d4a..b17731d3 100644 --- a/terminal.c +++ b/terminal.c @@ -263,7 +263,7 @@ fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) * temporary one - all changes done to it will be lost when * the interactive resize ends. */ - return 0; + return true; } uint8_t buf[24 * 1024]; From 09d52d5db6175c272b43d6e132922e14a0c57d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 18 Oct 2022 18:29:20 +0200 Subject: [PATCH 0197/1323] term_destroy(): free interactive_resizing.grid This grid is normally unallocated, but may be allocated if we are exiting (for whatever reason) in the middle of an interactive resize. --- terminal.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/terminal.c b/terminal.c index b17731d3..6cb419be 100644 --- a/terminal.c +++ b/terminal.c @@ -1752,6 +1752,8 @@ term_destroy(struct terminal *term) grid_free(&term->normal); grid_free(&term->alt); + grid_free(term->interactive_resizing.grid); + free(term->interactive_resizing.grid); free(term->foot_exe); free(term->cwd); From c4f08a3b9a7c6d6fc27f68e5a5c273975d42de76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 18 Oct 2022 18:30:02 +0200 Subject: [PATCH 0198/1323] grid_free(): allow being called with grid == NULL --- grid.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/grid.c b/grid.c index 3bcc8a55..7b86e2c1 100644 --- a/grid.c +++ b/grid.c @@ -287,6 +287,9 @@ grid_snapshot(const struct grid *grid) void grid_free(struct grid *grid) { + if (grid == NULL) + return; + for (int r = 0; r < grid->num_rows; r++) grid_row_free(grid->rows[r]); From 3ba03901b893abcdb4433d71d0b6568abceba770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 18 Oct 2022 18:31:18 +0200 Subject: [PATCH 0199/1323] =?UTF-8?q?pgo:=20don=E2=80=99t=20re-use=20the?= =?UTF-8?q?=20rows=20between=20the=20=E2=80=98normal=E2=80=99=20and=20?= =?UTF-8?q?=E2=80=98alt=E2=80=99=20grids?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This used to work because we never free:d any of the rows. Now however, we do free (some of) them when reverse scrolling. This means we can no longer re-use the rows between the two screens. Closes #1196 --- pgo/pgo.c | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/pgo/pgo.c b/pgo/pgo.c index 7f6f758b..b41b5850 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -228,10 +228,14 @@ main(int argc, const char *const *argv) return EXIT_FAILURE; } - struct row **rows = calloc(grid_row_count, sizeof(rows[0])); + struct row **normal_rows = calloc(grid_row_count, sizeof(normal_rows[0])); + struct row **alt_rows = calloc(grid_row_count, sizeof(alt_rows[0])); + for (int i = 0; i < grid_row_count; i++) { - rows[i] = calloc(1, sizeof(*rows[i])); - rows[i]->cells = calloc(col_count, sizeof(rows[i]->cells[0])); + normal_rows[i] = calloc(1, sizeof(*normal_rows[i])); + normal_rows[i]->cells = calloc(col_count, sizeof(normal_rows[i]->cells[0])); + alt_rows[i] = calloc(1, sizeof(*alt_rows[i])); + alt_rows[i]->cells = calloc(col_count, sizeof(alt_rows[i]->cells[0])); } struct config conf = { @@ -254,14 +258,14 @@ main(int argc, const char *const *argv) .normal = { .num_rows = grid_row_count, .num_cols = col_count, - .rows = rows, - .cur_row = rows[0], + .rows = normal_rows, + .cur_row = normal_rows[0], }, .alt = { .num_rows = grid_row_count, .num_cols = col_count, - .rows = rows, - .cur_row = rows[0], + .rows = alt_rows, + .cur_row = alt_rows[0], }, .scale = 1, .width = col_count * 8, @@ -371,11 +375,17 @@ out: tll_free(wayl.terms); for (int i = 0; i < grid_row_count; i++) { - free(rows[i]->cells); - free(rows[i]); + if (normal_rows[i] != NULL) + free(normal_rows[i]->cells); + free(normal_rows[i]); + + if (alt_rows[i] != NULL) + free(alt_rows[i]->cells); + free(alt_rows[i]); } - free(rows); + free(normal_rows); + free(alt_rows); close(lower_fd); close(upper_fd); return ret; From 59c9dfe109f9045b583299d934ac54b8a07749d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 23 Oct 2022 10:34:18 +0200 Subject: [PATCH 0200/1323] render: resize: do full text reflow immediately if resize-delay-ms == 0 That is, skip all custom grid handling when doing an interactive resize, if resize-delay-ms == 0. --- render.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/render.c b/render.c index 19ab5ef8..76f4b7c6 100644 --- a/render.c +++ b/render.c @@ -3955,9 +3955,10 @@ maybe_resize(struct terminal *term, int width, int height, bool force) * To be able to do the final reflow correctly, we need a copy of * the original grid, before the resize started. */ - if (term->window->is_resizing) { + if (term->window->is_resizing && term->conf->resize_delay_ms > 0) { if (term->interactive_resizing.grid == NULL) { term_ptmx_pause(term); + xassert(false); /* Stash the current ‘normal’ grid, as-is, to be used when * doing the final reflow */ @@ -4037,7 +4038,7 @@ maybe_resize(struct terminal *term, int width, int height, bool force) * tracking points list. */ /* Resize grids */ - if (term->window->is_resizing) { + if (term->window->is_resizing && term->conf->resize_delay_ms > 0) { /* Simple truncating resize, *while* an interactive resize is * ongoing. */ xassert(term->interactive_resizing.grid != NULL); From 1313e6352a442bfd7483f7b6e14308f2cabdc684 Mon Sep 17 00:00:00 2001 From: Andrea Pappacoda Date: Sun, 23 Oct 2022 23:56:34 +0200 Subject: [PATCH 0201/1323] build: fix GCC detection in pgo.sh On my system, GCC doesn't output its name when passing the --version flag: $ cc --version cc (Debian 12.2.0-3) 12.2.0 Copyright (C) 2022 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. As the Free Software Foundation is unlikely to write another compiler, I think that searching for the foundation's name instead of GCC is a good enough fix (and I'm almost sure we wouldn't be the first ones to do so). --- pgo/pgo.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgo/pgo.sh b/pgo/pgo.sh index b2ce7fe5..2f409268 100755 --- a/pgo/pgo.sh +++ b/pgo/pgo.sh @@ -30,7 +30,7 @@ do_pgo=no CFLAGS="${CFLAGS-} -O3" case $(${CC-cc} --version) in - *GCC*) + *Free\ Software\ Foundation*) compiler=gcc do_pgo=yes ;; From 49fa75195322e47114d2f5effb0e87f433c275f4 Mon Sep 17 00:00:00 2001 From: Andrea Pappacoda Date: Sun, 23 Oct 2022 21:32:11 +0200 Subject: [PATCH 0202/1323] chore: use MIT license for appstream metadata The Appstream metadata file introduced in commit 335612cfa4ea090874354c7080f492b2b28a136b has been submitted as licensed under the CC0-1.0 license. People generally use the CC0-1.0 to put the file in the "public domain", but this isn't actually possible in lots of countries, so the file ends up being licensed under CC0's fallback permissive license; unfortunately, the fallback license contains some terms that are seen as problematic to some (notably, Fedora has recently decided to consider the license pretty much non-free). As foot is already MIT-licensed, and since this license is in the list of allowed [metadata licenses], this patch changes the license of the metadata file from CC0-1.0 to MIT. [metadata licenses]: https://freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-metadata_license --- org.codeberg.dnkl.foot.metainfo.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org.codeberg.dnkl.foot.metainfo.xml b/org.codeberg.dnkl.foot.metainfo.xml index 22512ce8..1c0b7985 100644 --- a/org.codeberg.dnkl.foot.metainfo.xml +++ b/org.codeberg.dnkl.foot.metainfo.xml @@ -1,7 +1,7 @@ org.codeberg.dnkl.foot - CC0-1.0 + MIT MIT dnkl foot From 2c2a39317be3df4ff9899896067684408526fb29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 30 Oct 2022 19:39:09 +0100 Subject: [PATCH 0203/1323] render: never apply alpha to text color MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When drawing a block cursor using inversed fg/bg colors, we didn’t strip the alpha from the background color. This meant that the text "behind" the cursor was rendered with transparency. If alpha was set to 0, the text was completely invisible. We should never apply alpha to the text color. So, detect this, and force alpha to 1.0. Normally, when selecting the cursor’s color, we don’t really know _where_ the background color is coming from (or more accurately, _what_ it is). However, the *only* background color that can have a non-1.0 alpha is the *default* background color. This is why we can ignore the bg parameter, and use term->colors.fg/bg instead. Closes #1205 --- CHANGELOG.md | 4 ++++ render.c | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3022836d..3546d92e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,9 +91,13 @@ * Crash when a sixel image exceeds the current sixel max height. * Crash after reverse-scrolling (`CSI Ps T`) in the ‘normal’ (non-alternate) screen ([#1190][1190]). +* Background transparency being applied to the text "behind" the + cursor. Only applies to block cursor using inversed fg/bg + colors. ([#1205][1205]). [1173]: https://codeberg.org/dnkl/foot/issues/1173 [1190]: https://codeberg.org/dnkl/foot/issues/1190 +[1205]: https://codeberg.org/dnkl/foot/issues/1205 ### Security diff --git a/render.c b/render.c index 76f4b7c6..822667ae 100644 --- a/render.c +++ b/render.c @@ -419,7 +419,14 @@ cursor_colors_for_cell(const struct terminal *term, const struct cell *cell, } } else { *cursor_color = *fg; - *text_color = *bg; + + if (unlikely(text_color->alpha != 0xffff)) { + /* We *know* this only happens when bg is the default bg + * color */ + *text_color = color_hex_to_pixman( + term->reverse ? term->colors.fg : term->colors.bg); + } else + *text_color = *bg; } } From 30d088376cf8f5c44f393ce876f9a3a10f1dcd82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 1 Nov 2022 17:12:16 +0100 Subject: [PATCH 0204/1323] render: maybe_resize(): remove debug assert This, depending on which compiler being used, caused issues not only in debug builds, but release builds as well (with NDEBUG defined). --- render.c | 1 - 1 file changed, 1 deletion(-) diff --git a/render.c b/render.c index 822667ae..f5e7f627 100644 --- a/render.c +++ b/render.c @@ -3965,7 +3965,6 @@ maybe_resize(struct terminal *term, int width, int height, bool force) if (term->window->is_resizing && term->conf->resize_delay_ms > 0) { if (term->interactive_resizing.grid == NULL) { term_ptmx_pause(term); - xassert(false); /* Stash the current ‘normal’ grid, as-is, to be used when * doing the final reflow */ From 8f2bda67034b1db89be28db1d1f3bfe33cfeb37b Mon Sep 17 00:00:00 2001 From: Craig Barnes Date: Tue, 1 Nov 2022 21:04:22 +0000 Subject: [PATCH 0205/1323] wayland: use BUG() instead of xassert(false) The latter will expand to the former anyway, so we may as well provide an explicit error message instead of "assertion failed: 'false'". --- wayland.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wayland.c b/wayland.c index cd052532..fb103ad1 100644 --- a/wayland.c +++ b/wayland.c @@ -1915,7 +1915,7 @@ activation_token_done(void *data, struct xdg_activation_token_v1 *xdg_token, return; } - xassert(false); + BUG("activation token not found in list"); } static const struct From fa9beae3a69ad1f84e26a5c7528651d7563c0a45 Mon Sep 17 00:00:00 2001 From: Soren A D Date: Tue, 1 Nov 2022 11:32:49 +0530 Subject: [PATCH 0206/1323] added modus themes --- themes/modus-operandi | 24 ++++++++++++++++++++++++ themes/modus-vivendi | 25 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 themes/modus-operandi create mode 100644 themes/modus-vivendi diff --git a/themes/modus-operandi b/themes/modus-operandi new file mode 100644 index 00000000..ca6c9493 --- /dev/null +++ b/themes/modus-operandi @@ -0,0 +1,24 @@ +# +# modus-operandi +# See: https://protesilaos.com/emacs/modus-themes +# +[colors] +alpha=1.0 +background=ffffff +foreground=000000 +regular0=000000 +regular1=a60000 +regular2=005e00 +regular3=813e00 +regular4=0031a9 +regular5=721045 +regular6=00538b +regular7=bfbfbf +bright0=595959 +bright1=972500 +bright2=315b00 +bright3=70480f +bright4=2544bb +bright5=5317ac +bright6=005a5f +bright7=ffffff diff --git a/themes/modus-vivendi b/themes/modus-vivendi new file mode 100644 index 00000000..a95bcec0 --- /dev/null +++ b/themes/modus-vivendi @@ -0,0 +1,25 @@ +# +# modus-vivendi +# See: https://protesilaos.com/emacs/modus-themes +# + +[colors] +alpha=1.0 +background=000000 +foreground=ffffff +regular0=000000 +regular1=ff8059 +regular2=44bc44 +regular3=d0bc00 +regular4=2fafff +regular5=feacd0 +regular6=00d3d0 +regular7=bfbfbf +bright0=595959 +bright1=ef8b50 +bright2=70b900 +bright3=c0c530 +bright4=79a8ff +bright5=b6a0ff +bright6=6ae4b9 +bright7=ffffff From 2910ca354c741cded6656bf15b4d5216d4417ca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 4 Nov 2022 17:42:52 +0100 Subject: [PATCH 0207/1323] wayland: use fp math all the way when calculating DPI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes an FPE when the monitor’s physical width/height is so small that the conversion from mm to inch resulted in inches being zero. --- CHANGELOG.md | 2 ++ wayland.c | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3546d92e..9c41f21a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,10 +94,12 @@ * 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]). [1173]: https://codeberg.org/dnkl/foot/issues/1173 [1190]: https://codeberg.org/dnkl/foot/issues/1190 [1205]: https://codeberg.org/dnkl/foot/issues/1205 +[1209]: https://codeberg.org/dnkl/foot/issues/1209 ### Security diff --git a/wayland.c b/wayland.c index fb103ad1..974dfb39 100644 --- a/wayland.c +++ b/wayland.c @@ -370,8 +370,8 @@ output_update_ppi(struct monitor *mon) if (mon->dim.mm.width <= 0 || mon->dim.mm.height <= 0) return; - int x_inches = mon->dim.mm.width * 0.03937008; - int y_inches = mon->dim.mm.height * 0.03937008; + double x_inches = mon->dim.mm.width * 0.03937008; + double y_inches = mon->dim.mm.height * 0.03937008; mon->ppi.real.x = mon->dim.px_real.width / x_inches; mon->ppi.real.y = mon->dim.px_real.height / y_inches; @@ -407,7 +407,7 @@ output_update_ppi(struct monitor *mon) mon->ppi.scaled.x = scaled_width / x_inches; mon->ppi.scaled.y = scaled_height / y_inches; - float px_diag = sqrt(pow(scaled_width, 2) + pow(scaled_height, 2)); + double px_diag = sqrt(pow(scaled_width, 2) + pow(scaled_height, 2)); mon->dpi = px_diag / mon->inch * mon->scale; } From 42c6af091440e89be9c0d8895a1c5ff1caac5427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 4 Nov 2022 17:45:43 +0100 Subject: [PATCH 0208/1323] =?UTF-8?q?wayland:=20force=20monitor=20DPI=20to?= =?UTF-8?q?=2096=20when=20it=E2=80=99s=20unreasonably=20high?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If an output has a bogus physical width or height, the DPI can become so high that the cell width/height is too large for pixman_image_fill_rectangles(), resulting in a crash in pixman_fill(). Since it doesn’t make any sense to use a DPI that is obviously bogus, don’t. Force it 96 instead. --- CHANGELOG.md | 1 + wayland.c | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c41f21a..f4089b37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ ("permanently reset") instead of `2` ("reset") for DEC private modes that are known but unsupported. * Set `PWD` environment variable in the slave process ([#1179][1179]). +* DPI is now forced to 96 when found to be unreasonably high. [1166]: https://codeberg.org/dnkl/foot/issues/1166 [1179]: https://codeberg.org/dnkl/foot/issues/1179 diff --git a/wayland.c b/wayland.c index 974dfb39..e48d59aa 100644 --- a/wayland.c +++ b/wayland.c @@ -409,6 +409,14 @@ output_update_ppi(struct monitor *mon) double px_diag = sqrt(pow(scaled_width, 2) + pow(scaled_height, 2)); mon->dpi = px_diag / mon->inch * mon->scale; + + if (mon->dpi > 1000) { + if (mon->name != NULL) { + LOG_WARN("%s: DPI=%f is unreasonable, using 96 instead", + mon->name, mon->dpi); + } + mon->dpi = 96; + } } static void From b80c7f75fea0c1a69471d1f67aa97b584d3e48b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= Date: Fri, 18 Nov 2022 11:07:16 -0500 Subject: [PATCH 0209/1323] change default log level to WARNING The default foot output looks like this, in Debian testing "bookworm" at the time of writing: anarcat@angela:pubpaste$ foot true info: main.c:421: version: 1.13.1 +pgo +ime +graphemes -assertions info: main.c:428: arch: Linux x86_64/64-bit info: main.c:440: locale: fr_CA.UTF-8 info: config.c:3003: loading configuration from /home/anarcat/.config/foot/foot.ini info: fcft.c:338: fcft: 3.1.5 +graphemes -runs +svg(nanosvg) -assertions info: fcft.c:377: fontconfig: 2.13.1, freetype: 2.12.1, harfbuzz: 5.2.0 info: fcft.c:838: /home/anarcat/.local/share/fonts/Fira-4.202/otf/FiraMono-Regular.otf: size=8.00pt/8px, dpi=75.00 info: wayland.c:1353: eDP-1: 2256x1504+0x0@60Hz 0x095F 13.32" scale=2 PPI=205x214 (physical) PPI=136x143 (logical), DPI=271.31 info: wayland.c:1509: requesting SSD decorations info: fcft.c:838: /home/anarcat/.local/share/fonts/Fira-4.202/otf/FiraMono-Bold.otf: size=24.00pt/32px, dpi=96.00 info: fcft.c:838: /home/anarcat/.local/share/fonts/Fira-4.202/otf/FiraMono-Regular.otf: size=24.00pt/32px, dpi=96.00 info: fcft.c:838: /home/anarcat/.local/share/fonts/Fira-4.202/otf/FiraMono-Bold.otf: size=24.00pt/32px, dpi=96.00 info: fcft.c:838: /home/anarcat/.local/share/fonts/Fira-4.202/otf/FiraMono-Regular.otf: size=24.00pt/32px, dpi=96.00 info: terminal.c:700: cell width=19, height=39 info: terminal.c:588: using 16 rendering threads info: wayland.c:859: using SSD decorations info: main.c:680: goodbye anarcat@angela:pubpaste$ That's 17 lines of output that are *mostly* useless for most use cases. I might understand having this output during the project's startup, when it's helpful for diagnostics, but now Foot just mostly works everywhere, and I've never had a use for any of that stuff in the (arguably short) time I've been using Foot so far. And if I do, there's the `--log-level` commandline option to tweak this. At first, I looked at tweaking the log level through the config file. But as explained in this issue: https://codeberg.org/dnkl/foot/issues/1142 ... there's a chicken and egg problem there that makes it hard to implement and possibly confusing for users as well. There's also the possibility for users to change the shortcut with which they start foot, for example a `.desktop` file so that menu systems that support those start foot properly. But that only works in that environment, and not through the so many things that will just call `foot` and hope it will do the right thing. In my case, I have `foot` hardcoded in a lot of places now, between sway and waybar, and this is only going to grow. Others have suggested adding the flag to a $TERMINAL global variable, but that won't help .desktop users. So, instead of playing whack-a-mole with the log levels, just make it so that, by default, foot is silent. This is actually one of the [basics of UNIX philosophy][1]: > Rule of Silence: When a program has nothing surprising to say, it > should say nothing. And yes, I am aware I am severely violating that principle by writing a way too long commit log for a one-line patch, but there you go, I figured it was good to document the why of this properly. [1]: https://web.archive.org/web/20031102053334/http://www.faqs.org/docs/artu/ch01s06.html --- CHANGELOG.md | 2 ++ client.c | 4 ++-- completions/fish/foot.fish | 2 +- completions/zsh/_foot | 2 +- completions/zsh/_footclient | 2 +- doc/foot.1.scd | 2 +- doc/footclient.1.scd | 2 +- main.c | 4 ++-- 8 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4089b37..ba339100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,9 +65,11 @@ modes that are known but unsupported. * Set `PWD` environment variable in the slave process ([#1179][1179]). * DPI is now forced to 96 when found to be unreasonably high. +* Set default log level to warning ([#1215][1215]). [1166]: https://codeberg.org/dnkl/foot/issues/1166 [1179]: https://codeberg.org/dnkl/foot/issues/1179 +[1215]: https://codeberg.org/dnkl/foot/pulls/1215 ### Deprecated diff --git a/client.c b/client.c index 2a802d16..6954d17e 100644 --- a/client.c +++ b/client.c @@ -94,7 +94,7 @@ print_usage(const char *prog_name) " -N,--no-wait detach the client process from the running terminal, exiting immediately\n" " -o,--override=[section.]key=value override configuration option\n" " -E, --client-environment exec shell using footclient's environment, instead of the server's\n" - " -d,--log-level={info|warning|error|none} log level (info)\n" + " -d,--log-level={info|warning|error|none} log level (warning)\n" " -l,--log-colorize=[{never|always|auto}] enable/disable colorization of log output on stderr\n" " -v,--version show the version number and quit\n" " -e ignored (for compatibility with xterm -e)\n"; @@ -178,7 +178,7 @@ main(int argc, char *const *argv) const char *custom_cwd = NULL; const char *server_socket_path = NULL; - enum log_class log_level = LOG_CLASS_INFO; + enum log_class log_level = LOG_CLASS_WARNING; enum log_colorize log_colorize = LOG_COLORIZE_AUTO; bool hold = false; bool client_environment = false; diff --git a/completions/fish/foot.fish b/completions/fish/foot.fish index 81c7da61..86f6616d 100644 --- a/completions/fish/foot.fish +++ b/completions/fish/foot.fish @@ -15,7 +15,7 @@ complete -c foot -x -s W -l window-size-chars complete -c foot -F -s s -l server -d "run as server; open terminals by running footclient" complete -c foot -s H -l hold -d "remain open after child process exits" complete -c foot -r -s p -l print-pid -d "print PID to this file or FD when up and running (server mode only)" -complete -c foot -x -s d -l log-level -a "info warning error none" -d "log-level (info)" +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 -s v -l version -d "show the version number and quit" diff --git a/completions/zsh/_foot b/completions/zsh/_foot index 0f184cc0..b9f46cdc 100644 --- a/completions/zsh/_foot +++ b/completions/zsh/_foot @@ -18,7 +18,7 @@ _arguments \ '(-s --server)'{-s,--server}'[run as server; open terminals by running footclient]:server:_files' \ '(-H --hold)'{-H,--hold}'[remain open after child process exits]' \ '(-p --print-pid)'{-p,--print-pid}'[print PID to this file or FD when up and running (server mode only)]:pidfile:_files' \ - '(-d --log-level)'{-d,--log-level}'[log level (info)]:loglevel:(info warning error none)' \ + '(-d --log-level)'{-d,--log-level}'[log level (warning)]:loglevel:(info warning error none)' \ '(-l --log-colorize)'{-l,--log-colorize}'[enable or disable colorization of log output on stderr]:logcolor:(never always auto)' \ '(-S --log-no-syslog)'{-s,--log-no-syslog}'[disable syslog logging (server mode only)]' \ '(-v --version)'{-v,--version}'[show the version number and quit]' \ diff --git a/completions/zsh/_footclient b/completions/zsh/_footclient index b36644c6..c14d65d5 100644 --- a/completions/zsh/_footclient +++ b/completions/zsh/_footclient @@ -16,7 +16,7 @@ _arguments \ '(-N --no-wait)'{-N,--no-wait}'[detach the client process from the running terminal, exiting immediately]' \ '(-o --override)'{-o,--override}'[configuration option to override, in form SECTION.KEY=VALUE]:()' \ '(-E --client-environment)'{-E,--client-environment}"[child process inherits footclient's environment, instead of the server's]" \ - '(-d --log-level)'{-d,--log-level}'[log level (info)]:loglevel:(info warning error none)' \ + '(-d --log-level)'{-d,--log-level}'[log level (warning)]:loglevel:(info warning error none)' \ '(-l --log-colorize)'{-l,--log-colorize}'[enable or disable colorization of log output on stderr]:logcolor:(never always auto)' \ '(-v --version)'{-v,--version}'[show the version number and quit]' \ '(-h --help)'{-h,--help}'[show help message and quit]' \ diff --git a/doc/foot.1.scd b/doc/foot.1.scd index afb8faf5..4ee5df57 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -142,7 +142,7 @@ the foot command line *-d*,*--log-level*={*info*,*warning*,*error*,*none*} Log level, used both for log output on stderr as well as - syslog. Default: _info_. + syslog. Default: _warning_. *-l*,*--log-colorize*=[{*never*,*always*,*auto*}] Enables or disables colorization of log output on stderr. Default: diff --git a/doc/footclient.1.scd b/doc/footclient.1.scd index 93bddd61..63235134 100644 --- a/doc/footclient.1.scd +++ b/doc/footclient.1.scd @@ -75,7 +75,7 @@ terminal has terminated. *-d*,*--log-level*={*info*,*warning*,*error*,*none*} Log level, used both for log output on stderr as well as - syslog. Default: _info_. + syslog. Default: _warning_. *-l*,*--log-colorize*=[{*never*,*always*,*auto*}] Enables or disables colorization of log output on stderr. diff --git a/main.c b/main.c index a3ae579d..c882c825 100644 --- a/main.c +++ b/main.c @@ -83,7 +83,7 @@ print_usage(const char *prog_name) " Without PATH, $XDG_RUNTIME_DIR/foot-$WAYLAND_DISPLAY.sock will be used.\n" " -H,--hold remain open after child process exits\n" " -p,--print-pid=FILE|FD print PID to file or FD (only applicable in server mode)\n" - " -d,--log-level={info|warning|error|none} log level (info)\n" + " -d,--log-level={info|warning|error|none} log level (warning)\n" " -l,--log-colorize=[{never|always|auto}] enable/disable colorization of log output on stderr\n" " -s,--log-no-syslog disable syslog logging (only applicable in server mode)\n" " -v,--version show the version number and quit\n" @@ -236,7 +236,7 @@ main(int argc, char *const *argv) bool fullscreen = false; bool unlink_pid_file = false; const char *pid_file = NULL; - enum log_class log_level = LOG_CLASS_INFO; + enum log_class log_level = LOG_CLASS_WARNING; enum log_colorize log_colorize = LOG_COLORIZE_AUTO; bool log_syslog = true; user_notifications_t user_notifications = tll_init(); From dfabc5d75423c863a7e9a930b9a8aac593784e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 23 Nov 2022 16:27:50 +0100 Subject: [PATCH 0210/1323] =?UTF-8?q?readme/foot.1:=20mention=20that=20we?= =?UTF-8?q?=20now=20need=20=E2=80=9C-d=20info=E2=80=9D=20to=20get=20log=20?= =?UTF-8?q?output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- doc/foot.1.scd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 348f2e18..8d037af3 100644 --- a/README.md +++ b/README.md @@ -565,7 +565,7 @@ reported the same issue. The report should contain the following: - Foot version (`foot --version`). -- Log output from foot (start foot from another terminal). +- Log output from foot (run `foot -d info` from another terminal). - Which Wayland compositor (and version) you are running. - If reporting a crash, please try to provide a `bt full` backtrace with symbols. diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 4ee5df57..e20e7805 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -557,7 +557,7 @@ the same issue. The report should contain the following: - Foot version (*foot --version*). -- Log output from foot (start foot from another terminal). +- Log output from foot (run *foot -d info* from another terminal). - Which Wayland compositor (and version) you are running. - If reporting a crash, please try to provide a *bt full* backtrace with symbols. From 5a54423000ad0cffec0178a6091c87d75110eee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 23 Nov 2022 16:15:32 +0100 Subject: [PATCH 0211/1323] term: adjust user-set line-height by the same percentage as the primary font MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this patch, a user-set line-height was increased/decreased by the exact same amount of pt’s as the font(s). This means, that when there’s a large discrepancy between the line-height and the font size, the proportion between the line’s height and the font size will change as we increase or decrease the font size. This patch changes how the line height is adjusted when the font size is incremented or decremented. We calculate the difference, in percent, between the primary font’s original (default) size, and its current size, and then apply that to the configured line-height. Closes #1218 --- terminal.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/terminal.c b/terminal.c index 6cb419be..ea934df9 100644 --- a/terminal.c +++ b/terminal.c @@ -2031,12 +2031,20 @@ term_font_size_adjust(struct terminal *term, double amount) } if (term->font_line_height.px >= 0) { - float old_pt_size = term->font_line_height.px > 0 - ? term->font_line_height.px * 72. / dpi - : term->font_line_height.pt; + const struct config *conf = term->conf; + + const float font_original_pt_size = + conf->fonts[0].arr[0].px_size > 0 + ? conf->fonts[0].arr[0].px_size * 72. / dpi + : conf->fonts[0].arr[0].pt_size; + + const float change = term->font_sizes[0][0].pt_size / font_original_pt_size; + const float line_original_pt_size = conf->line_height.px > 0 + ? conf->line_height.px * 72. / dpi + : conf->line_height.pt; term->font_line_height.px = 0; - term->font_line_height.pt = fmaxf(old_pt_size + amount, 0.); + term->font_line_height.pt = fmaxf(line_original_pt_size * change, 0.); } return reload_fonts(term); From f31ea4f56d6ff560ad16f9c18528881ee8077dba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 23 Nov 2022 16:23:01 +0100 Subject: [PATCH 0212/1323] changelog: line-height adjustment with user-set line-height --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba339100..499b4275 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,11 +98,14 @@ cursor. Only applies to block cursor using inversed fg/bg colors. ([#1205][1205]). * Crash when monitor’s physical size is "too small" ([#1209][1209]). +* Line-height adjustment when incrementing/decrementing the font size + with a user-set line-height ([#1218][1218]). [1173]: https://codeberg.org/dnkl/foot/issues/1173 [1190]: https://codeberg.org/dnkl/foot/issues/1190 [1205]: https://codeberg.org/dnkl/foot/issues/1205 [1209]: https://codeberg.org/dnkl/foot/issues/1209 +[1218]: https://codeberg.org/dnkl/foot/issues/1218 ### Security From 94bac0513ab3ca78464d8e79c153c8ee4bac835f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 24 Nov 2022 14:34:31 +0100 Subject: [PATCH 0213/1323] term: update user-set line-height just before calculating the cell dimensions This ensures *all* font-size affecting changes (DPI, output scaling, font size increment/decrement) also updates the line-height. --- terminal.c | 49 ++++++++++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/terminal.c b/terminal.c index ea934df9..3af436a4 100644 --- a/terminal.c +++ b/terminal.c @@ -704,6 +704,34 @@ free_custom_glyphs(struct fcft_glyph ***glyphs, size_t count) *glyphs = NULL; } +static void +term_line_height_update(struct terminal *term) +{ + const struct config *conf = term->conf; + + if (term->conf->line_height.px < 0) + return; + + const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; + + const float font_original_pt_size = + conf->fonts[0].arr[0].px_size > 0 + ? conf->fonts[0].arr[0].px_size * 72. / dpi + : conf->fonts[0].arr[0].pt_size; + const float font_current_pt_size = + term->font_sizes[0][0].px_size > 0 + ? term->font_sizes[0][0].px_size * 72. / dpi + : term->font_sizes[0][0].pt_size; + + const float change = font_current_pt_size / font_original_pt_size; + const float line_original_pt_size = conf->line_height.px > 0 + ? conf->line_height.px * 72. / dpi + : conf->line_height.pt; + + term->font_line_height.px = 0; + term->font_line_height.pt = fmaxf(line_original_pt_size * change, 0.); +} + static bool term_set_fonts(struct terminal *term, struct fcft_font *fonts[static 4]) { @@ -730,6 +758,8 @@ term_set_fonts(struct terminal *term, struct fcft_font *fonts[static 4]) fonts[0], U'M', term->font_subpixel); int advance = M != NULL ? M->advance.x : term->fonts[0]->max_advance.x; + term_line_height_update(term); + term->cell_width = advance + term_pt_or_px_as_pixels(term, &conf->letter_spacing); @@ -1078,7 +1108,6 @@ load_fonts_from_conf(struct terminal *term) } } - term->font_line_height = term->conf->line_height; return reload_fonts(term); } @@ -1298,7 +1327,6 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .pt_size = font->pt_size, .px_size = font->px_size}; } } - term->font_line_height = conf->line_height; add_utmp_record(conf, reaper, ptmx); @@ -2030,23 +2058,6 @@ term_font_size_adjust(struct terminal *term, double amount) } } - if (term->font_line_height.px >= 0) { - const struct config *conf = term->conf; - - const float font_original_pt_size = - conf->fonts[0].arr[0].px_size > 0 - ? conf->fonts[0].arr[0].px_size * 72. / dpi - : conf->fonts[0].arr[0].pt_size; - - const float change = term->font_sizes[0][0].pt_size / font_original_pt_size; - const float line_original_pt_size = conf->line_height.px > 0 - ? conf->line_height.px * 72. / dpi - : conf->line_height.pt; - - term->font_line_height.px = 0; - term->font_line_height.pt = fmaxf(line_original_pt_size * change, 0.); - } - return reload_fonts(term); } From e85257bcae116f5e6148e38690e2ec95b586d451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 24 Nov 2022 17:09:31 +0100 Subject: [PATCH 0214/1323] =?UTF-8?q?term:=20initialize=20term->font=5Flin?= =?UTF-8?q?e=5Fheight=20when=20there=E2=80=99s=20no=20user-set=20line-heig?= =?UTF-8?q?ht?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- terminal.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/terminal.c b/terminal.c index 3af436a4..429a4cc3 100644 --- a/terminal.c +++ b/terminal.c @@ -709,8 +709,11 @@ term_line_height_update(struct terminal *term) { const struct config *conf = term->conf; - if (term->conf->line_height.px < 0) + if (term->conf->line_height.px < 0) { + term->font_line_height.pt = 0; + term->font_line_height.px = -1; return; + } const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; From fa6b07abeacfb5b8a70bc762e9c8ff64470c4d2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 24 Nov 2022 17:20:05 +0100 Subject: [PATCH 0215/1323] term: apply scale factor when converting a px value to pt --- terminal.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal.c b/terminal.c index 429a4cc3..1914a229 100644 --- a/terminal.c +++ b/terminal.c @@ -946,7 +946,7 @@ term_pt_or_px_as_pixels(const struct terminal *term, return pt_or_px->px == 0 ? round(pt_or_px->pt * scale * dpi / 72) - : pt_or_px->px; + : pt_or_px->px * scale; } struct font_load_data { From db2627ea26564fc1b8bfc890e42c90a2cde48a6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 24 Nov 2022 17:21:53 +0100 Subject: [PATCH 0216/1323] changelog: scaling factor not being applied when converting px-to-pt --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 499b4275..c5584b45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,8 @@ * Crash when monitor’s physical size is "too small" ([#1209][1209]). * Line-height adjustment when incrementing/decrementing the font size with a user-set line-height ([#1218][1218]). +* Scaling factor not being correctly applied when converting pt-or-px + config values (e.g. letter offsets, line height etc). [1173]: https://codeberg.org/dnkl/foot/issues/1173 [1190]: https://codeberg.org/dnkl/foot/issues/1190 From 0fc8b65a2b77a5c4dd066e4d49be470ee1d1f70a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 24 Nov 2022 17:05:27 +0100 Subject: [PATCH 0217/1323] selection: selection_on_rows(): typo: row_start -> row_end MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes an issue where selections in the scroll margins were not detected correctly. This meant they weren’t canceled as they should have been, which in turn caused a visual glitch where text appeared to be selected, but were in fact not. --- CHANGELOG.md | 1 + csi.c | 4 ++-- selection.c | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5584b45..1f7d033b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,6 +102,7 @@ with a user-set line-height ([#1218][1218]). * Scaling factor not being correctly applied when converting pt-or-px config values (e.g. letter offsets, line height etc). +* Selection being stuck visually when `IL` and `DL`.` [1173]: https://codeberg.org/dnkl/foot/issues/1173 [1190]: https://codeberg.org/dnkl/foot/issues/1190 diff --git a/csi.c b/csi.c index 2eacaaa9..e77ae971 100644 --- a/csi.c +++ b/csi.c @@ -933,7 +933,7 @@ csi_dispatch(struct terminal *term, uint8_t final) break; } - case 'L': { + case 'L': { /* IL */ if (term->grid->cursor.point.row < term->scroll_region.start || term->grid->cursor.point.row >= term->scroll_region.end) break; @@ -953,7 +953,7 @@ csi_dispatch(struct terminal *term, uint8_t final) break; } - case 'M': { + case 'M': { /* DL */ if (term->grid->cursor.point.row < term->scroll_region.start || term->grid->cursor.point.row >= term->scroll_region.end) break; diff --git a/selection.c b/selection.c index 92f1f85f..f6349d7f 100644 --- a/selection.c +++ b/selection.c @@ -86,7 +86,7 @@ selection_on_rows(const struct terminal *term, int row_start, int row_end) const int rel_row_start = grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, row_start); const int rel_row_end = - grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, row_start); + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, row_end); int rel_sel_start = grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, start->row); int rel_sel_end = From 1b24cf4fcb8a32a9c20eb607c2015542eb2753d4 Mon Sep 17 00:00:00 2001 From: Craig Barnes Date: Thu, 24 Nov 2022 20:34:41 +0000 Subject: [PATCH 0218/1323] doc: ctlseq: add trailing space to fix XTGETTCAP entry in DCS table Without the trailing space, both the sequence and the description were getting packed into a single table column. --- doc/foot-ctlseqs.7.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index 1d94219f..e2d66982 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -703,7 +703,7 @@ and are terminated by *\\E\\* (ST). : Begin (_C_=*1*) or end (_C_=*2*) application synchronized updates. This sequence is supported for compatibility reasons, but it's recommended to use private mode 2026 (see above) instead. -| \\EP + q \\E\\ +| \\EP + q \\E\\ : Query builtin terminfo database (XTGETTCAP) From 76d494484f0b26f7785c0964cc412609d09607ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 30 Nov 2022 10:51:45 +0100 Subject: [PATCH 0219/1323] url-mode: tag cells after snapshot:ing the grid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this patch, hyperlinked cells were tagged with the “URL” attribute (thus instructing the renderer to draw an underline) *before* the grid was snapshot. When exiting URL mode, the cells were once again updated, this time removing the URL attribute. But what if an escape sequence had modified the grid _while we were in URL mode_? Depending on the sequence, it could move cells around in such a way, that when exiting URL mode, the affected cells weren’t updated correctly. I.e. we left some cells with the URL attribute still set. The fix is simple: tag cells in the snapshot:ed grid only (which isn’t affected by any escape sequence received while in URL mode). Not in the *actual* grid (which _is_ affected). --- CHANGELOG.md | 1 + url-mode.c | 32 +++++++++++++++++--------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f7d033b..13b87f4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,6 +103,7 @@ * Scaling factor not being correctly applied when converting pt-or-px config values (e.g. letter offsets, line height etc). * Selection being stuck visually when `IL` and `DL`.` +* URL underlines sometimes still being visible after exiting URL mode. [1173]: https://codeberg.org/dnkl/foot/issues/1173 [1190]: https://codeberg.org/dnkl/foot/issues/1190 diff --git a/url-mode.c b/url-mode.c index 6fa16623..7d7ffd81 100644 --- a/url-mode.c +++ b/url-mode.c @@ -746,15 +746,18 @@ tag_cells_for_url(struct terminal *term, const struct url *url, bool value) if (url->url_mode_dont_change_url_attr) return; + struct grid *grid = term->url_grid_snapshot; + xassert(grid != NULL); + const struct coord *start = &url->range.start; const struct coord *end = &url->range.end; - size_t end_r = end->row & (term->grid->num_rows - 1); + size_t end_r = end->row & (grid->num_rows - 1); - size_t r = start->row & (term->grid->num_rows - 1); + size_t r = start->row & (grid->num_rows - 1); size_t c = start->col; - struct row *row = term->grid->rows[r]; + struct row *row = grid->rows[r]; row->dirty = true; while (true) { @@ -766,10 +769,10 @@ tag_cells_for_url(struct terminal *term, const struct url *url, bool value) break; if (++c >= term->cols) { - r = (r + 1) & (term->grid->num_rows - 1); + r = (r + 1) & (grid->num_rows - 1); c = 0; - row = term->grid->rows[r]; + row = grid->rows[r]; if (row == NULL) { /* Un-allocated scrollback. This most likely means a * runaway OSC-8 URL. */ @@ -788,15 +791,6 @@ urls_render(struct terminal *term) if (tll_length(win->term->urls) == 0) return; - xassert(tll_length(win->urls) == 0); - tll_foreach(win->term->urls, it) { - struct wl_url url = {.url = &it->item}; - wayl_win_subsurface_new(win, &url.surf, false); - - tll_push_back(win->urls, url); - tag_cells_for_url(term, &it->item, true); - } - /* Dirty the last cursor, to ensure it is erased */ { struct row *cursor_row = term->render.last_cursor.row; @@ -819,6 +813,15 @@ urls_render(struct terminal *term) /* Snapshot the current grid */ term->url_grid_snapshot = grid_snapshot(term->grid); + xassert(tll_length(win->urls) == 0); + tll_foreach(win->term->urls, it) { + struct wl_url url = {.url = &it->item}; + wayl_win_subsurface_new(win, &url.surf, false); + + tll_push_back(win->urls, url); + tag_cells_for_url(term, &it->item, true); + } + render_refresh_urls(term); render_refresh(term); } @@ -860,7 +863,6 @@ urls_reset(struct terminal *term) } tll_foreach(term->urls, it) { - tag_cells_for_url(term, &it->item, false); url_destroy(&it->item); tll_remove(term->urls, it); } From b43a41df6a5a72b22b6c16d5ac45d0d66a9d5207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 2 Dec 2022 11:45:10 +0100 Subject: [PATCH 0220/1323] =?UTF-8?q?log:=20don=E2=80=99t=20default=20to?= =?UTF-8?q?=20syslog=20enabled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Initialize the global ‘do_syslog’ variable to false. This ensures any log calls done before log_init() has been called (e.g. unit tests) doesn’t syslog anything. As a side effect, such log calls no longer open an implicit syslog file descriptor; this is how this “bug” was found: valgrind detected an unclosed file descriptor at exit. Finally, completely disable syslogging if log-level is “none”. --- log.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/log.c b/log.c index b93b2cde..360ca1c0 100644 --- a/log.c +++ b/log.c @@ -15,7 +15,7 @@ #include "xsnprintf.h" static bool colorize = false; -static bool do_syslog = true; +static bool do_syslog = false; static enum log_class log_level = LOG_CLASS_NONE; static const struct { @@ -45,8 +45,13 @@ log_init(enum log_colorize _colorize, bool _do_syslog, log_level = _log_level; int slvl = log_level_map[_log_level].syslog_equivalent; - if (do_syslog && slvl != -1) { + if (slvl < 0) + do_syslog = false; + + if (do_syslog) { openlog(NULL, /*LOG_PID*/0, facility_map[syslog_facility]); + + xassert(slvl >= 0); setlogmask(LOG_UPTO(slvl)); } } From 1486c57bdb084e5c41f917fd542a3605dcaa90a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 4 Dec 2022 19:49:02 +0100 Subject: [PATCH 0221/1323] doc: foot: add PWD to list of env vars set in child process --- doc/foot.1.scd | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/foot.1.scd b/doc/foot.1.scd index e20e7805..6f63d4c8 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -539,6 +539,9 @@ In all other cases, the exit code is that of the client application set according to either the *--term* command-line option or the *term* config option in *foot.ini*(5). +*PWD* + Current working directory (at the time of launching foot) + *COLORTERM* This variable is set to *truecolor*, to indicate to client applications that 24-bit RGB colors are supported. From 051e86242043cd10dee186343e3f7b661ee30219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 1 Dec 2022 15:00:44 +0100 Subject: [PATCH 0222/1323] config: allow string values to be quoted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Both double and single quotes are recognized. There’s no difference in how they are handled. * The entire string must be quoted: - “a quoted string” - OK - quotes “in the middle” of a string - NOT ok * Two escape characters are regonized: - Backslash - The quote character itself --- CHANGELOG.md | 2 ++ config.c | 42 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13b87f4b..bffe4e11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,8 @@ * Support (optional) for utmp logging with libutempter. * `kxIN` and `kxOUT` (focus in/out events) to terminfo. * `name` capability to `XTGETTCAP`. +* String values in `foot.ini` may now be quoted. This can be used to + set a value to the empty string, for example. [1136]: https://codeberg.org/dnkl/foot/issues/1136 diff --git a/config.c b/config.c index e2c007d2..cbf64abc 100644 --- a/config.c +++ b/config.c @@ -502,8 +502,48 @@ value_to_double(struct context *ctx, float *res) static bool NOINLINE value_to_str(struct context *ctx, char **res) { + char *copy = xstrdup(ctx->value); + char *end = copy + strlen(copy) - 1; + + /* Un-quote + * + * Note: this is very simple; we only support the *entire* value + * being quoted. That is, no mid-value quotes. Both double and + * single quotes are supported. + * + * - key="value" OK + * - key=abc "quote" def NOT OK + * - key=’value’ OK + * + * Finally, we support escaping the quote character, and the + * escape character itself: + * + * - key="value \"quotes\"" + * - key="backslash: \\" + * + * ONLY the "current" quote character can be escaped: + * + * key="value \'" NOt OK (both backslash and single quote is kept) + */ + + if ((copy[0] == '"' && *end == '"') || + (copy[0] == '\'' && *end == '\'')) + { + const char quote = copy[0]; + *end = '\0'; + + memmove(copy, copy + 1, end - copy); + + /* Un-escape */ + for (char *p = copy; *p != '\0'; p++) { + if (p[0] == '\\' && (p[1] == '\\' || p[1] == quote)) { + memmove(p, p + 1, end - p); + } + } + } + free(*res); - *res = xstrdup(ctx->value); + *res = copy; return true; } From 57d9a7451faa62381058575a7dbf11fdb73903d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 1 Dec 2022 15:06:13 +0100 Subject: [PATCH 0223/1323] =?UTF-8?q?foot.ini:=20use=20a=20quoted,=20empty?= =?UTF-8?q?=20string=20for=20=E2=80=9Cindicator-format=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We don’t allow empty values, but we do allow quoted, empty values. --- foot.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foot.ini b/foot.ini index 62f853fa..d0c93519 100644 --- a/foot.ini +++ b/foot.ini @@ -48,7 +48,7 @@ # lines=1000 # multiplier=3.0 # indicator-position=relative -# indicator-format= +# indicator-format="" [url] # launch=xdg-open ${url} From 646314469afd96eda346b6a296037d5430e5f121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 2 Dec 2022 15:03:07 +0100 Subject: [PATCH 0224/1323] doc: foot.ini: add example, and mention string options can be quoted --- doc/foot.ini.5.scd | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index ee6aa94c..e18a6208 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -22,6 +22,15 @@ in this order: An example configuration file containing all options with their default value commented out will usually be installed to */etc/xdg/foot/foot.ini*. +Options are set using KEY=VALUE pairs: + + *\[colors\]*++ +*background=000000*++ +*foreground=ffffff* + +Empty values (*KEY=*) are not supported. String options do allow the +empty string to be set, but it must be quoted: *KEY=""*) + # SECTION: main *shell* From ccfb953bb020a1d229490b8a3e7790ed5c7e1752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 1 Dec 2022 19:43:38 +0100 Subject: [PATCH 0225/1323] slave: unsetenv() env vars that have been set to the empty string MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit That is, users can now *clear* environment variables by doing: [environment] VAR=”” Note that the quotes are required. Closes #1225 --- CHANGELOG.md | 3 +++ config.c | 23 ++++++++++++----------- slave.c | 11 +++++++++-- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bffe4e11..ead014cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,8 +51,11 @@ * `name` capability to `XTGETTCAP`. * String values in `foot.ini` may now be quoted. This can be used to set a value to the empty string, for example. +* Environment variables can now be **unset**, by setting + `[environment].=""` (quotes are required) ([#1225][1225]) [1136]: https://codeberg.org/dnkl/foot/issues/1136 +[1225]: https://codeberg.org/dnkl/foot/issues/1225 ### Changed diff --git a/config.c b/config.c index cbf64abc..68641647 100644 --- a/config.c +++ b/config.c @@ -2295,21 +2295,22 @@ parse_section_environment(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; - const char *value = ctx->value; + /* Check for pre-existing env variable */ tll_foreach(conf->env_vars, it) { - if (strcmp(it->item.name, key) == 0) { - free(it->item.value); - it->item.value = xstrdup(value); - return true; - } + if (strcmp(it->item.name, key) == 0) + return value_to_str(ctx, &it->item.value); } - struct env_var var = { - .name = xstrdup(key), - .value = xstrdup(value), - }; - tll_push_back(conf->env_vars, var); + /* + * No pre-existing variable - allocate a new one + */ + + char *value = NULL; + if (!value_to_str(ctx, &value)) + return false; + + tll_push_back(conf->env_vars, ((struct env_var){xstrdup(key), value})); return true; } diff --git a/slave.c b/slave.c index 4dd80e6f..d4861ad0 100644 --- a/slave.c +++ b/slave.c @@ -359,8 +359,15 @@ slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, #endif if (extra_env_vars != NULL) { - tll_foreach(*extra_env_vars, it) - setenv(it->item.name, it->item.value, 1); + tll_foreach(*extra_env_vars, it) { + const char *name = it->item.name; + const char *value = it->item.value; + + if (strlen(value) == 0) + unsetenv(name); + else + setenv(name, value, 1); + } } char **_shell_argv = NULL; From 3b9aca6a3d0caef3c0a5070f7684687e8c5d0c1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 14 Dec 2022 12:20:52 +0100 Subject: [PATCH 0226/1323] doc: foot-ctlseq: expand last column to fill screen in all tables --- doc/foot-ctlseqs.7.scd | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index e2d66982..f06a9418 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -22,7 +22,7 @@ This document describes all the control sequences supported by foot. [[ *Sequence* :[ *Name* -:[ *Description* +:< *Description* | \\a : BEL : Depends on what *bell* in *foot.ini*(5) is set to. @@ -60,7 +60,7 @@ equivalent to 8-bit C1 controls. [[ *Sequence* :[ *Name* :[ *Origin* -:[ *Description* +:< *Description* | \\E 7 : DECSC : VT100 @@ -140,7 +140,7 @@ single CSI sequence by separating them with semicolons: *\\E[ 1;2;3 m*. [[ *Parameter* -:[ *Description* +:< *Description* | 0 : Reset all attributes | 1 @@ -223,7 +223,7 @@ following 4 escape sequences: [[ *Sequence* :[ *Name* -:[ *Description* +:< *Description* | \\E[ ? _Pm_ h : DECSET : Enable private mode @@ -243,7 +243,7 @@ that corresponds to one of the following modes: [[ *Parameter* :[ *Origin* -:[ *Description* +:< *Description* | 1 : VT100 : Cursor keys mode (DECCKM) @@ -344,7 +344,7 @@ manipulation sequences. The generic format is: [[ *Parameter 1* :[ *Parameter 2* -:[ *Description* +:< *Description* | 11 : - : Report if window is iconified. Foot always reports *1* - not iconified. @@ -394,7 +394,7 @@ manipulation sequences. The generic format is: [[ *Parameter* :[ *Name* :[ *Origin* -:[ *Description* +:< *Description* | \\E[ _Ps_ c : DA : VT100 @@ -595,7 +595,7 @@ All _OSC_ sequences begin with *\\E]*, sometimes abbreviated _OSC_. [[ *Sequence* :[ *Origin* -:[ *Description* +:< *Description* | \\E] 0 ; _Pt_ \\E\\ : xterm : Set window icon and title to _Pt_ (foot does not support setting the @@ -693,7 +693,7 @@ All _DCS_ sequences begin with *\\EP* (sometimes abbreviated _DCS_), and are terminated by *\\E\\* (ST). [[ *Sequence* -:[ *Description* +:< *Description* | \\EP q \\E\\ : Emit a sixel image at the current cursor position | \\P $ q \\E\\ From 7bb5c80d04be5e5297fb6259e487fe0105e85935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Nohlg=C3=A5rd?= Date: Fri, 16 Dec 2022 08:38:37 +0100 Subject: [PATCH 0227/1323] main: Graceful fallback if user has configured an invalid locale --- main.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/main.c b/main.c index c882c825..4af200fd 100644 --- a/main.c +++ b/main.c @@ -433,8 +433,13 @@ main(int argc, char *const *argv) const char *locale = setlocale(LC_CTYPE, ""); if (locale == NULL) { - LOG_ERR("setlocale() failed"); - return ret; + /* + * If the user has configured an invalid locale, or a name of a locale + * that does not exist on this system, then the above call may return + * NULL. We should just continue with the fallback method below. + */ + LOG_WARN("setlocale() failed"); + locale = "C"; } LOG_INFO("locale: %s", locale); From f6ca8c90e11d9b1ecdfe8a5bf69b1b793376a91c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 15 Dec 2022 11:10:32 +0100 Subject: [PATCH 0228/1323] =?UTF-8?q?config:=20add=20=E2=80=98font-size-ad?= =?UTF-8?q?justment=3DN[px|%]=E2=80=99=20option?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch adds a new config option, font-size-adjustment. It lets you configure how much the font size should be incremented/decremented when zooming in or out (ctrl-+, ctrl+-). Values can be specified in points, pixels or percent. Closes #1188 --- CHANGELOG.md | 6 +++- config.c | 26 ++++++++++++++ config.h | 6 ++++ doc/foot.ini.5.scd | 13 +++++++ terminal.c | 87 +++++++++++++++++++++++++++++++++++---------- tests/test-config.c | 1 + 6 files changed, 120 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ead014cf..9538ccca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,10 +52,14 @@ * String values in `foot.ini` may now be quoted. This can be used to set a value to the empty string, for example. * Environment variables can now be **unset**, by setting - `[environment].=""` (quotes are required) ([#1225][1225]) + `[environment].=""` (quotes are required) ([#1225][1225]). +* `font-size-adjustment=N[px]` option, letting you configure how much + to increment/decrement the font size when zooming in or out + ([#1188][1188]). [1136]: https://codeberg.org/dnkl/foot/issues/1136 [1225]: https://codeberg.org/dnkl/foot/issues/1225 +[1188]: https://codeberg.org/dnkl/foot/issues/1188 ### Changed diff --git a/config.c b/config.c index 68641647..b10f42b2 100644 --- a/config.c +++ b/config.c @@ -925,6 +925,31 @@ parse_section_main(struct context *ctx) return true; } + else if (strcmp(key, "font-size-adjustment") == 0) { + const size_t len = strlen(ctx->value); + if (len >= 1 && ctx->value[len - 1] == '%') { + errno = 0; + char *end = NULL; + + float percent = strtof(ctx->value, &end); + if (!(errno == 0 && end == ctx->value + len - 1)) { + LOG_CONTEXTUAL_ERR( + "invalid percent value (must be in the form 10.5%%)"); + return false; + } + + conf->font_size_adjustment.percent = percent / 100.; + conf->font_size_adjustment.pt_or_px.pt = 0; + conf->font_size_adjustment.pt_or_px.px = 0; + return true; + } else { + bool ret = value_to_pt_or_px(ctx, &conf->font_size_adjustment.pt_or_px); + if (ret) + conf->font_size_adjustment.percent = 0.; + return ret; + } + } + else if (strcmp(key, "line-height") == 0) return value_to_pt_or_px(ctx, &conf->line_height); @@ -2886,6 +2911,7 @@ config_load(struct config *conf, const char *conf_path, }, .startup_mode = STARTUP_WINDOWED, .fonts = {{0}}, + .font_size_adjustment = {.percent = 0., .pt_or_px = {.pt = 0.5, .px = 0}}, .line_height = {.pt = 0, .px = -1}, .letter_spacing = {.pt = 0, .px = 0}, .horizontal_letter_offset = {.pt = 0, .px = 0}, diff --git a/config.h b/config.h index d35abbb2..648d92e4 100644 --- a/config.h +++ b/config.h @@ -22,6 +22,11 @@ struct pt_or_px { float pt; }; +struct font_size_adjustment { + struct pt_or_px pt_or_px; + float percent; +}; + enum cursor_style { CURSOR_BLOCK, CURSOR_UNDERLINE, CURSOR_BEAM }; enum conf_size_type {CONF_SIZE_PX, CONF_SIZE_CELLS}; @@ -139,6 +144,7 @@ struct config { enum {DPI_AWARE_AUTO, DPI_AWARE_YES, DPI_AWARE_NO} dpi_aware; struct config_font_list fonts[4]; + struct font_size_adjustment font_size_adjustment; /* Custom font metrics (-1 = use real font metrics) */ struct pt_or_px line_height; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index e18a6208..6443d2ba 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -80,6 +80,19 @@ empty string to be set, but it must be quoted: *KEY=""*) Default: _monospace:size=8_ (*font*), _not set_ (*font-bold*, *font-italic*, *font-bold-italic*). +*font-size-adjustment* + Amount, in _points_, _pixels_ or _percent_, to increment/decrement + the font size when zooming in our out. + + Examples: + ``` + font-size-adjustment=0.5 # Adjust by 0.5 points + font-size-adjustment=10xp # Adjust by 10 pixels + font-size-adjustment=7.5% # Adjust by 7.5 percent + ``` + + Default: _0.5_ + *include* Absolute path to configuration file to import. diff --git a/terminal.c b/terminal.c index 1914a229..78ce4bc3 100644 --- a/terminal.c +++ b/terminal.c @@ -2035,29 +2035,70 @@ term_reset(struct terminal *term, bool hard) } static bool -term_font_size_adjust(struct terminal *term, double amount) +term_font_size_adjust_by_points(struct terminal *term, float amount) { const struct config *conf = term->conf; - const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; for (size_t i = 0; i < 4; i++) { const struct config_font_list *font_list = &conf->fonts[i]; for (size_t j = 0; j < font_list->count; j++) { - float old_pt_size = term->font_sizes[i][j].pt_size; + struct config_font *font = &term->font_sizes[i][j]; + float old_pt_size = font->pt_size; - /* - * To ensure primary and user-configured fallback fonts are - * resizes by the same amount, convert pixel sizes to point - * sizes, and to the adjustment on point sizes only. - */ + if (font->px_size > 0) + old_pt_size = font->px_size * 72. / dpi; - if (term->font_sizes[i][j].px_size > 0) - old_pt_size = term->font_sizes[i][j].px_size * 72. / dpi; + font->pt_size = fmaxf(old_pt_size + amount, 0.); + font->px_size = -1; + } + } - term->font_sizes[i][j].pt_size = fmaxf(old_pt_size + amount, 0.); - term->font_sizes[i][j].px_size = -1; + return reload_fonts(term); +} + +static bool +term_font_size_adjust_by_pixels(struct terminal *term, int amount) +{ + const struct config *conf = term->conf; + const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; + + for (size_t i = 0; i < 4; i++) { + const struct config_font_list *font_list = &conf->fonts[i]; + + for (size_t j = 0; j < font_list->count; j++) { + struct config_font *font = &term->font_sizes[i][j]; + int old_px_size = font->px_size; + + if (font->px_size <= 0) + old_px_size = font->pt_size * dpi / 72.; + + font->px_size = max(old_px_size + amount, 1); + } + } + + return reload_fonts(term); +} + +static bool +term_font_size_adjust_by_percent(struct terminal *term, bool increment, float percent) +{ + const struct config *conf = term->conf; + const float multiplier = increment + ? 1. + percent + : 1. / (1. + percent); + + for (size_t i = 0; i < 4; i++) { + const struct config_font_list *font_list = &conf->fonts[i]; + + for (size_t j = 0; j < font_list->count; j++) { + struct config_font *font = &term->font_sizes[i][j]; + + if (font->px_size > 0) + font->px_size = max(font->px_size * multiplier, 1); + else + font->pt_size = fmax(font->pt_size * multiplier, 0); } } @@ -2067,19 +2108,29 @@ term_font_size_adjust(struct terminal *term, double amount) bool term_font_size_increase(struct terminal *term) { - if (!term_font_size_adjust(term, 0.5)) - return false; + const struct config *conf = term->conf; + const struct font_size_adjustment *inc_dec = &conf->font_size_adjustment; - return true; + if (inc_dec->percent > 0.) + return term_font_size_adjust_by_percent(term, true, inc_dec->percent); + else if (inc_dec->pt_or_px.px > 0) + return term_font_size_adjust_by_pixels(term, inc_dec->pt_or_px.px); + else + return term_font_size_adjust_by_points(term, inc_dec->pt_or_px.pt); } bool term_font_size_decrease(struct terminal *term) { - if (!term_font_size_adjust(term, -0.5)) - return false; + const struct config *conf = term->conf; + const struct font_size_adjustment *inc_dec = &conf->font_size_adjustment; - return true; + if (inc_dec->percent > 0.) + return term_font_size_adjust_by_percent(term, false, inc_dec->percent); + else if (inc_dec->pt_or_px.px > 0) + return term_font_size_adjust_by_pixels(term, -inc_dec->pt_or_px.px); + else + return term_font_size_adjust_by_points(term, -inc_dec->pt_or_px.pt); } bool diff --git a/tests/test-config.c b/tests/test-config.c index ae7969dc..4f8b0c3b 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -467,6 +467,7 @@ test_section_main(void) test_boolean(&ctx, &parse_section_main, "locked-title", &conf.locked_title); test_boolean(&ctx, &parse_section_main, "notify-focus-inhibit", &conf.notify_focus_inhibit); + test_pt_or_px(&ctx, &parse_section_main, "font-size-adjustment", &conf.font_size_adjustment.pt_or_px); /* TODO: test ‘N%’ values too */ test_pt_or_px(&ctx, &parse_section_main, "line-height", &conf.line_height); test_pt_or_px(&ctx, &parse_section_main, "letter-spacing", &conf.letter_spacing); test_pt_or_px(&ctx, &parse_section_main, "horizontal-letter-offset", &conf.horizontal_letter_offset); From 59018446fd8ff616aa80a81dea581c935372b605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 17 Dec 2022 10:18:55 +0100 Subject: [PATCH 0229/1323] foot.ini: add font-size-adjustment --- foot.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/foot.ini b/foot.ini index d0c93519..8266b01b 100644 --- a/foot.ini +++ b/foot.ini @@ -12,6 +12,7 @@ # font-bold= # font-italic= # font-bold-italic= +# font-size-adjustment=0.5 # line-height= # letter-spacing=0 # horizontal-letter-offset=0 From 7bf150c11a6d3b8145b7a3d8b7d0ae2349e0c094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 17 Dec 2022 10:25:16 +0100 Subject: [PATCH 0230/1323] =?UTF-8?q?config:=20value=5Fto=5Fpt=5For=5Fpx()?= =?UTF-8?q?:=20don=E2=80=99t=20allow=20empty=20px=20values=20(key=3Dpx)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.c b/config.c index b10f42b2..af93f419 100644 --- a/config.c +++ b/config.c @@ -651,7 +651,7 @@ value_to_pt_or_px(struct context *ctx, struct pt_or_px *res) char *end = NULL; long value = strtol(s, &end, 10); - if (!(errno == 0 && end == s + len - 2)) { + if (!(len > 2 && errno == 0 && end == s + len - 2)) { LOG_CONTEXTUAL_ERR("invalid px value (must be in the form 12px)"); return false; } From 4ee0b28b02a4633a4f4e6b41b9da57e41bcf9356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 17 Dec 2022 10:25:48 +0100 Subject: [PATCH 0231/1323] =?UTF-8?q?config:=20font-size-adjustment:=20don?= =?UTF-8?q?=E2=80=99t=20allow=20empty=20%-values=20(key=3D%)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.c b/config.c index af93f419..7d849d5c 100644 --- a/config.c +++ b/config.c @@ -932,7 +932,7 @@ parse_section_main(struct context *ctx) char *end = NULL; float percent = strtof(ctx->value, &end); - if (!(errno == 0 && end == ctx->value + len - 1)) { + if (!(len > 1 && errno == 0 && end == ctx->value + len - 1)) { LOG_CONTEXTUAL_ERR( "invalid percent value (must be in the form 10.5%%)"); return false; From 6ebe5cf62115f34d4f9ec1bbf17d3f30447c28f6 Mon Sep 17 00:00:00 2001 From: argosatcore Date: Sun, 25 Dec 2022 05:34:56 +0000 Subject: [PATCH 0232/1323] Add Deus theme. New color palette based on: https://github.com/ajmwagar/vim-deus --- themes/deus | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 themes/deus diff --git a/themes/deus b/themes/deus new file mode 100644 index 00000000..5f863b76 --- /dev/null +++ b/themes/deus @@ -0,0 +1,32 @@ +# Deus +# Color palette based on: https://github.com/ajmwagar/vim-deus + +[cursor] +color=2c323b eaeaea + +[colors] +alpha=1.0 +background=2c323b +foreground=eaeaea +regular0=242a32 +regular1=d54e53 +regular2=98c379 +regular3=e5c07b +regular4=83a598 +regular5=c678dd +regular6=70c0ba +regular7=eaeaea +bright0=666666 +bright1=ec3e45 +bright2=90c966 +bright3=edbf69 +bright4=73ba9f +bright5=c858e9 +bright6=2bcec2 +bright7=ffffff + +## Enable if prefer Deus colors instead of inverterd fg/bg for +## highlighting (mouse selection) +# selection-foreground=2c323b +# selection-background=eaeaea + From da7b393a034dfb36dac8afc9b8d0698e8d37fea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Dec 2022 10:54:02 +0100 Subject: [PATCH 0233/1323] themes: remove alpha MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Alpha isn’t really part of the theme. Leave it up to the user to set alpha. --- themes/catppuccin | 1 - themes/deus | 8 +++----- themes/dracula | 1 - themes/jetbrains-darcula | 1 - themes/modus-operandi | 1 - themes/modus-vivendi | 1 - themes/paper-color-dark | 43 ++++++++++++++++++++-------------------- themes/paper-color-light | 43 ++++++++++++++++++++-------------------- 8 files changed, 45 insertions(+), 54 deletions(-) diff --git a/themes/catppuccin b/themes/catppuccin index f873aa3f..9e46aeed 100644 --- a/themes/catppuccin +++ b/themes/catppuccin @@ -4,7 +4,6 @@ color=1A1826 D9E0EE [colors] -alpha=1.0 foreground=D9E0EE background=1E1D2F regular0=6E6C7E # black diff --git a/themes/deus b/themes/deus index 5f863b76..d9cd82af 100644 --- a/themes/deus +++ b/themes/deus @@ -5,7 +5,6 @@ color=2c323b eaeaea [colors] -alpha=1.0 background=2c323b foreground=eaeaea regular0=242a32 @@ -24,9 +23,8 @@ bright4=73ba9f bright5=c858e9 bright6=2bcec2 bright7=ffffff - -## Enable if prefer Deus colors instead of inverterd fg/bg for -## highlighting (mouse selection) + +# Enable if prefer Deus colors instead of inverterd fg/bg for +# highlighting (mouse selection) # selection-foreground=2c323b # selection-background=eaeaea - diff --git a/themes/dracula b/themes/dracula index cd60e2e6..e116a694 100644 --- a/themes/dracula +++ b/themes/dracula @@ -4,7 +4,6 @@ color=282a36 f8f8f2 [colors] -alpha=1.0 foreground=f8f8f2 background=282a36 regular0=000000 # black diff --git a/themes/jetbrains-darcula b/themes/jetbrains-darcula index a4c811c5..ef4f1ece 100644 --- a/themes/jetbrains-darcula +++ b/themes/jetbrains-darcula @@ -5,7 +5,6 @@ color=202020 ffffff [colors] -#alpha=0.80 background=202020 foreground=adadad regular0=000000 # black diff --git a/themes/modus-operandi b/themes/modus-operandi index ca6c9493..12d884d8 100644 --- a/themes/modus-operandi +++ b/themes/modus-operandi @@ -3,7 +3,6 @@ # See: https://protesilaos.com/emacs/modus-themes # [colors] -alpha=1.0 background=ffffff foreground=000000 regular0=000000 diff --git a/themes/modus-vivendi b/themes/modus-vivendi index a95bcec0..b8176c8e 100644 --- a/themes/modus-vivendi +++ b/themes/modus-vivendi @@ -4,7 +4,6 @@ # [colors] -alpha=1.0 background=000000 foreground=ffffff regular0=000000 diff --git a/themes/paper-color-dark b/themes/paper-color-dark index 17d569ac..7cb1f903 100644 --- a/themes/paper-color-dark +++ b/themes/paper-color-dark @@ -2,27 +2,26 @@ # Palette based on https://github.com/NLKNguyen/papercolor-theme [cursor] - color=1c1c1c eeeeee +color=1c1c1c eeeeee [colors] - alpha=0.80 - background=1c1c1c - foreground=eeeeee - regular0=1c1c1c # black - regular1=af005f # red - regular2=5faf00 # green - regular3=d7af5f # yellow - regular4=5fafd7 # blue - regular5=808080 # magenta - regular6=d7875f # cyan - regular7=d0d0d0 # white - bright0=bcbcbc # bright black - bright1=5faf5f # bright red - bright2=afd700 # bright green - bright3=af87d7 # bright yellow - bright4=ffaf00 # bright blue - bright5=ff5faf # bright magenta - bright6=00afaf # bright cyan - bright7=5f8787 # bright white - #selection-foreground=1c1c1c - #selection-background=af87d7 +background=1c1c1c +foreground=eeeeee +regular0=1c1c1c # black +regular1=af005f # red +regular2=5faf00 # green +regular3=d7af5f # yellow +regular4=5fafd7 # blue +regular5=808080 # magenta +regular6=d7875f # cyan +regular7=d0d0d0 # white +bright0=bcbcbc # bright black +bright1=5faf5f # bright red +bright2=afd700 # bright green +bright3=af87d7 # bright yellow +bright4=ffaf00 # bright blue +bright5=ff5faf # bright magenta +bright6=00afaf # bright cyan +bright7=5f8787 # bright white +# selection-foreground=1c1c1c +# selection-background=af87d7 diff --git a/themes/paper-color-light b/themes/paper-color-light index 6e9f59f6..714ff022 100644 --- a/themes/paper-color-light +++ b/themes/paper-color-light @@ -2,27 +2,26 @@ # Palette based on https://github.com/NLKNguyen/papercolor-theme [cursor] - color=eeeeee 444444 +color=eeeeee 444444 [colors] - alpha=1.0 - background=eeeeee - foreground=444444 - regular0=eeeeee # black - regular1=af0000 # red - regular2=008700 # green - regular3=5f8700 # yellow - regular4=0087af # blue - regular5=878787 # magenta - regular6=005f87 # cyan - regular7=764e37 # white - bright0=bcbcbc # bright black - bright1=d70000 # bright red - bright2=d70087 # bright green - bright3=8700af # bright yellow - bright4=d75f00 # bright blue - bright5=d75f00 # bright magenta - bright6=4c7a5d # bright cyan - bright7=005faf # bright white - #selection-foreground=eeeeee - #selection-background=0087af +background=eeeeee +foreground=444444 +regular0=eeeeee # black +regular1=af0000 # red +regular2=008700 # green +regular3=5f8700 # yellow +regular4=0087af # blue +regular5=878787 # magenta +regular6=005f87 # cyan +regular7=764e37 # white +bright0=bcbcbc # bright black +bright1=d70000 # bright red +bright2=d70087 # bright green +bright3=8700af # bright yellow +bright4=d75f00 # bright blue +bright5=d75f00 # bright magenta +bright6=4c7a5d # bright cyan +bright7=005faf # bright white +# selection-foreground=eeeeee +# selection-background=0087af From 135d4478a156ce2264afd0ee5203510132986381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Dec 2022 10:59:37 +0100 Subject: [PATCH 0234/1323] =?UTF-8?q?themes:=20add=20=E2=80=98conf?= =?UTF-8?q?=E2=80=99=20modeline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- themes/apprentice | 1 + themes/catppuccin | 1 + themes/derp | 1 + themes/deus | 1 + themes/dracula | 1 + themes/gruvbox-dark | 1 + themes/gruvbox-light | 1 + themes/hacktober | 1 + themes/jetbrains-darcula | 1 + themes/kitty | 2 ++ themes/material-design | 1 + themes/modus-operandi | 1 + themes/modus-vivendi | 1 + themes/monokai-pro | 1 + themes/moonfly | 1 + themes/nightfly | 1 + themes/nord | 1 + themes/nordiq | 1 + themes/paper-color-dark | 1 + themes/paper-color-light | 1 + themes/rezza | 1 + themes/selenized-black | 1 + themes/selenized-dark | 1 + themes/selenized-light | 1 + themes/selenized-white | 1 + themes/solarized-dark | 1 + themes/solarized-dark-normal-brights | 1 + themes/solarized-light | 1 + themes/tango | 1 + themes/tempus-autumn | 1 + themes/tempus-classic | 1 + themes/tempus-dawn | 1 + themes/tempus-day | 1 + themes/tempus-dusk | 1 + themes/tempus-fugit | 1 + themes/tempus-future | 1 + themes/tempus-night | 1 + themes/tempus-past | 1 + themes/tempus-rift | 1 + themes/tempus-spring | 1 + themes/tempus-summer | 1 + themes/tempus-tempest | 1 + themes/tempus-totus | 1 + themes/tempus-warp | 1 + themes/tempus-winter | 1 + themes/tokyonight-day | 2 ++ themes/tokyonight-night | 2 ++ themes/tokyonight-storm | 2 ++ themes/visibone | 1 + themes/zenburn | 2 ++ 50 files changed, 55 insertions(+) diff --git a/themes/apprentice b/themes/apprentice index 06d26315..941a27b4 100644 --- a/themes/apprentice +++ b/themes/apprentice @@ -1,3 +1,4 @@ +# -*- conf -*- # https://github.com/romainl/Apprentice [cursor] diff --git a/themes/catppuccin b/themes/catppuccin index 9e46aeed..4ccfabec 100644 --- a/themes/catppuccin +++ b/themes/catppuccin @@ -1,3 +1,4 @@ +# -*- conf -*- # Catppuccin [cursor] diff --git a/themes/derp b/themes/derp index 1d1afcd5..0925d2c2 100644 --- a/themes/derp +++ b/themes/derp @@ -1,3 +1,4 @@ +# -*- conf -*- # Derp [cursor] diff --git a/themes/deus b/themes/deus index d9cd82af..8fb37f75 100644 --- a/themes/deus +++ b/themes/deus @@ -1,3 +1,4 @@ +# -*- conf -*- # Deus # Color palette based on: https://github.com/ajmwagar/vim-deus diff --git a/themes/dracula b/themes/dracula index e116a694..8b6ab542 100644 --- a/themes/dracula +++ b/themes/dracula @@ -1,3 +1,4 @@ +# -*- conf -*- # Dracula [cursor] diff --git a/themes/gruvbox-dark b/themes/gruvbox-dark index 1716647d..73207199 100644 --- a/themes/gruvbox-dark +++ b/themes/gruvbox-dark @@ -1,3 +1,4 @@ +# -*- conf -*- # Gruvbox [colors] diff --git a/themes/gruvbox-light b/themes/gruvbox-light index a0788d0c..6a7a2416 100644 --- a/themes/gruvbox-light +++ b/themes/gruvbox-light @@ -1,3 +1,4 @@ +# -*- conf -*- # Gruvbox - Light [colors] diff --git a/themes/hacktober b/themes/hacktober index 4c7c6233..acb6c0b1 100644 --- a/themes/hacktober +++ b/themes/hacktober @@ -1,3 +1,4 @@ +# -*- conf -*- [cursor] color=141414 c9c9c9 diff --git a/themes/jetbrains-darcula b/themes/jetbrains-darcula index ef4f1ece..306b1e9d 100644 --- a/themes/jetbrains-darcula +++ b/themes/jetbrains-darcula @@ -1,3 +1,4 @@ +# -*- conf -*- # JetBrains Darcula # Palette based on the same theme from https://github.com/dexpota/kitty-themes diff --git a/themes/kitty b/themes/kitty index 670a4559..b5b813cc 100644 --- a/themes/kitty +++ b/themes/kitty @@ -1,3 +1,5 @@ +# -*- conf -*- + [cursor] color=111111 cccccc diff --git a/themes/material-design b/themes/material-design index 4b017391..6d81e339 100644 --- a/themes/material-design +++ b/themes/material-design @@ -1,3 +1,4 @@ +# -*- conf -*- # Material # From https://github.com/MartinSeeler/iterm2-material-design diff --git a/themes/modus-operandi b/themes/modus-operandi index 12d884d8..5e3a9fd6 100644 --- a/themes/modus-operandi +++ b/themes/modus-operandi @@ -1,3 +1,4 @@ +# -*- conf -*- # # modus-operandi # See: https://protesilaos.com/emacs/modus-themes diff --git a/themes/modus-vivendi b/themes/modus-vivendi index b8176c8e..82b1075d 100644 --- a/themes/modus-vivendi +++ b/themes/modus-vivendi @@ -1,3 +1,4 @@ +# -*- conf -*- # # modus-vivendi # See: https://protesilaos.com/emacs/modus-themes diff --git a/themes/monokai-pro b/themes/monokai-pro index eecdd3f7..5d9f31a9 100644 --- a/themes/monokai-pro +++ b/themes/monokai-pro @@ -1,3 +1,4 @@ +# -*- conf -*- # Monokai Pro [colors] diff --git a/themes/moonfly b/themes/moonfly index 54d9203b..c622ab45 100644 --- a/themes/moonfly +++ b/themes/moonfly @@ -1,3 +1,4 @@ +# -*- conf -*- # moonfly # Based on https://github.com/bluz71/vim-moonfly-colors diff --git a/themes/nightfly b/themes/nightfly index 3815b42e..50f95125 100644 --- a/themes/nightfly +++ b/themes/nightfly @@ -1,3 +1,4 @@ +# -*- conf -*- # nightfly # Based on https://github.com/bluz71/vim-nightfly-guicolors diff --git a/themes/nord b/themes/nord index 4f6d8c2b..af09f727 100644 --- a/themes/nord +++ b/themes/nord @@ -1,3 +1,4 @@ +# -*- conf -*- # theme: Nord # author: Arctic Ice Studio , Sven Greb # description: „Nord“ — An arctic, north-bluish color palette diff --git a/themes/nordiq b/themes/nordiq index df45e80a..f309de23 100644 --- a/themes/nordiq +++ b/themes/nordiq @@ -1,3 +1,4 @@ +# -*- conf -*- # Nordiq [cursor] diff --git a/themes/paper-color-dark b/themes/paper-color-dark index 7cb1f903..18cd7f17 100644 --- a/themes/paper-color-dark +++ b/themes/paper-color-dark @@ -1,3 +1,4 @@ +# -*- conf -*- # PaperColorDark # Palette based on https://github.com/NLKNguyen/papercolor-theme diff --git a/themes/paper-color-light b/themes/paper-color-light index 714ff022..b08ea707 100644 --- a/themes/paper-color-light +++ b/themes/paper-color-light @@ -1,3 +1,4 @@ +# -*- conf -*- # PaperColor Light # Palette based on https://github.com/NLKNguyen/papercolor-theme diff --git a/themes/rezza b/themes/rezza index b9ef4a1f..56814a77 100644 --- a/themes/rezza +++ b/themes/rezza @@ -1,3 +1,4 @@ +# -*- conf -*- # theme: rezza # author: Doug Whiteley (rezza) # original URL: http://metawire.org/~rezza/index.php diff --git a/themes/selenized-black b/themes/selenized-black index a75891f9..28392add 100644 --- a/themes/selenized-black +++ b/themes/selenized-black @@ -1,3 +1,4 @@ +# -*- conf -*- # Selenized black [cursor] diff --git a/themes/selenized-dark b/themes/selenized-dark index e3a7b7b4..ed74cdfc 100644 --- a/themes/selenized-dark +++ b/themes/selenized-dark @@ -1,3 +1,4 @@ +# -*- conf -*- # Selenized dark [cursor] diff --git a/themes/selenized-light b/themes/selenized-light index 5f2b7c08..7e599d8e 100644 --- a/themes/selenized-light +++ b/themes/selenized-light @@ -1,3 +1,4 @@ +# -*- conf -*- # Selenized light [cursor] diff --git a/themes/selenized-white b/themes/selenized-white index 492c01f6..b4d25315 100644 --- a/themes/selenized-white +++ b/themes/selenized-white @@ -1,3 +1,4 @@ +# -*- conf -*- # Selenized white [cursor] diff --git a/themes/solarized-dark b/themes/solarized-dark index 0d0a233f..e2395bdc 100644 --- a/themes/solarized-dark +++ b/themes/solarized-dark @@ -1,3 +1,4 @@ +# -*- conf -*- # Solarized dark [cursor] diff --git a/themes/solarized-dark-normal-brights b/themes/solarized-dark-normal-brights index 484e5dc2..405a6e49 100644 --- a/themes/solarized-dark-normal-brights +++ b/themes/solarized-dark-normal-brights @@ -1,3 +1,4 @@ +# -*- conf -*- # Solarized dark [cursor] diff --git a/themes/solarized-light b/themes/solarized-light index fb4b762a..74474573 100644 --- a/themes/solarized-light +++ b/themes/solarized-light @@ -1,3 +1,4 @@ +# -*- conf -*- # Solarized light [cursor] diff --git a/themes/tango b/themes/tango index 3b5c93c4..a326f8ad 100644 --- a/themes/tango +++ b/themes/tango @@ -1,3 +1,4 @@ +# -*- conf -*- # Tango [cursor] diff --git a/themes/tempus-autumn b/themes/tempus-autumn index 9fafb5a2..3f706d0e 100644 --- a/themes/tempus-autumn +++ b/themes/tempus-autumn @@ -1,3 +1,4 @@ +# -*- conf -*- # theme: Tempus Autumn # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a palette inspired by earthly colours (WCAG AA compliant) diff --git a/themes/tempus-classic b/themes/tempus-classic index af86b024..66fb886a 100644 --- a/themes/tempus-classic +++ b/themes/tempus-classic @@ -1,3 +1,4 @@ +# -*- conf -*- # theme: Tempus Classic # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with warm hues (WCAG AA compliant) diff --git a/themes/tempus-dawn b/themes/tempus-dawn index ef599373..fa368b13 100644 --- a/themes/tempus-dawn +++ b/themes/tempus-dawn @@ -1,3 +1,4 @@ +# -*- conf -*- # theme: Tempus Dawn # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme with a soft, slightly desaturated palette (WCAG AA compliant) diff --git a/themes/tempus-day b/themes/tempus-day index 2e4cfdc9..6002a8e2 100644 --- a/themes/tempus-day +++ b/themes/tempus-day @@ -1,3 +1,4 @@ +# -*- conf -*- # theme: Tempus Day # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme with warm colours (WCAG AA compliant) diff --git a/themes/tempus-dusk b/themes/tempus-dusk index 07907efc..e9f94725 100644 --- a/themes/tempus-dusk +++ b/themes/tempus-dusk @@ -1,3 +1,4 @@ +# -*- conf -*- # theme: Tempus Dusk # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a deep blue-ish, slightly desaturated palette (WCAG AA compliant) diff --git a/themes/tempus-fugit b/themes/tempus-fugit index 37484f4c..1a6ae9e7 100644 --- a/themes/tempus-fugit +++ b/themes/tempus-fugit @@ -1,3 +1,4 @@ +# -*- conf -*- # theme: Tempus Fugit # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light, pleasant theme optimised for long writing/coding sessions (WCAG AA compliant) diff --git a/themes/tempus-future b/themes/tempus-future index 462e53dc..c0cdaad9 100644 --- a/themes/tempus-future +++ b/themes/tempus-future @@ -1,3 +1,4 @@ +# -*- conf -*- # theme: Tempus Future # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with colours inspired by concept art of outer space (WCAG AAA compliant) diff --git a/themes/tempus-night b/themes/tempus-night index 72e46bce..a431fbc6 100644 --- a/themes/tempus-night +++ b/themes/tempus-night @@ -1,3 +1,4 @@ +# -*- conf -*- # theme: Tempus Night # author: Protesilaos Stavrou (https://protesilaos.com) # description: High contrast dark theme with bright colours (WCAG AAA compliant) diff --git a/themes/tempus-past b/themes/tempus-past index 5dcee1c2..8f8ddcb1 100644 --- a/themes/tempus-past +++ b/themes/tempus-past @@ -1,3 +1,4 @@ +# -*- conf -*- # theme: Tempus Past # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme inspired by old vaporwave concept art (WCAG AA compliant) diff --git a/themes/tempus-rift b/themes/tempus-rift index b60409e7..6d1ed3a2 100644 --- a/themes/tempus-rift +++ b/themes/tempus-rift @@ -1,3 +1,4 @@ +# -*- conf -*- # theme: Tempus Rift # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a subdued palette on the green side of the spectrum (WCAG AA compliant) diff --git a/themes/tempus-spring b/themes/tempus-spring index dcee15f7..207434e9 100644 --- a/themes/tempus-spring +++ b/themes/tempus-spring @@ -1,3 +1,4 @@ +# -*- conf -*- # theme: Tempus Spring # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a palette inspired by early spring colours (WCAG AA compliant) diff --git a/themes/tempus-summer b/themes/tempus-summer index 3662c04a..3a852e46 100644 --- a/themes/tempus-summer +++ b/themes/tempus-summer @@ -1,3 +1,4 @@ +# -*- conf -*- # theme: Tempus Summer # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with colours inspired by summer evenings by the sea (WCAG AA compliant) diff --git a/themes/tempus-tempest b/themes/tempus-tempest index aab3c44e..15b3d881 100644 --- a/themes/tempus-tempest +++ b/themes/tempus-tempest @@ -1,3 +1,4 @@ +# -*- conf -*- # theme: Tempus Tempest # author: Protesilaos Stavrou (https://protesilaos.com) # description: A green-scale, subtle theme for late night hackers (WCAG AAA compliant) diff --git a/themes/tempus-totus b/themes/tempus-totus index 2673a70c..5ccac91d 100644 --- a/themes/tempus-totus +++ b/themes/tempus-totus @@ -1,3 +1,4 @@ +# -*- conf -*- # theme: Tempus Totus # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme for prose or for coding in an open space (WCAG AAA compliant) diff --git a/themes/tempus-warp b/themes/tempus-warp index 48fd47bc..d4c7a94b 100644 --- a/themes/tempus-warp +++ b/themes/tempus-warp @@ -1,3 +1,4 @@ +# -*- conf -*- # theme: Tempus Warp # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a vibrant palette (WCAG AA compliant) diff --git a/themes/tempus-winter b/themes/tempus-winter index 69c8b867..22cdd6d9 100644 --- a/themes/tempus-winter +++ b/themes/tempus-winter @@ -1,3 +1,4 @@ +# -*- conf -*- # theme: Tempus Winter # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a palette inspired by winter nights at the city (WCAG AA compliant) diff --git a/themes/tokyonight-day b/themes/tokyonight-day index 744ef351..5143aa07 100644 --- a/themes/tokyonight-day +++ b/themes/tokyonight-day @@ -1,3 +1,5 @@ +# -*- conf -*- + [colors] background=e1e2e7 foreground=3760bf diff --git a/themes/tokyonight-night b/themes/tokyonight-night index e48d7cd6..f789e1bd 100644 --- a/themes/tokyonight-night +++ b/themes/tokyonight-night @@ -1,3 +1,5 @@ +# -*- conf -*- + [colors] background=1a1b26 foreground=c0caf5 diff --git a/themes/tokyonight-storm b/themes/tokyonight-storm index 96b90eb8..074b4697 100644 --- a/themes/tokyonight-storm +++ b/themes/tokyonight-storm @@ -1,3 +1,5 @@ +# -*- conf -*- + [colors] background=24283b foreground=c0caf5 diff --git a/themes/visibone b/themes/visibone index a27c815f..3ee665d0 100644 --- a/themes/visibone +++ b/themes/visibone @@ -1,3 +1,4 @@ +# -*- conf -*- # VisiBone [cursor] diff --git a/themes/zenburn b/themes/zenburn index 3867826f..bace080c 100644 --- a/themes/zenburn +++ b/themes/zenburn @@ -1,3 +1,5 @@ +# -*- conf -*- + [colors] foreground=dcdccc background=111111 From 9e4270cd482398361061423de13d7412eecb9f07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Dec 2022 11:03:17 +0100 Subject: [PATCH 0235/1323] themes: comment out selection-{foreground,background} That is, mouse selections should default to inverse fg/bg --- themes/jetbrains-darcula | 4 ++-- themes/material-design | 4 ++-- themes/moonfly | 5 +++-- themes/nightfly | 5 +++-- themes/nord | 5 +++-- themes/solarized-dark | 4 ++-- themes/solarized-dark-normal-brights | 4 ++-- themes/tempus-autumn | 4 ++-- themes/tempus-classic | 4 ++-- themes/tempus-dawn | 4 ++-- themes/tempus-day | 4 ++-- themes/tempus-dusk | 4 ++-- themes/tempus-fugit | 4 ++-- themes/tempus-future | 4 ++-- themes/tempus-night | 4 ++-- themes/tempus-past | 4 ++-- themes/tempus-rift | 4 ++-- themes/tempus-spring | 4 ++-- themes/tempus-summer | 4 ++-- themes/tempus-tempest | 4 ++-- themes/tempus-totus | 4 ++-- themes/tempus-warp | 4 ++-- themes/tempus-winter | 4 ++-- 23 files changed, 49 insertions(+), 46 deletions(-) diff --git a/themes/jetbrains-darcula b/themes/jetbrains-darcula index 306b1e9d..82528498 100644 --- a/themes/jetbrains-darcula +++ b/themes/jetbrains-darcula @@ -24,5 +24,5 @@ bright4=6d9df1 # bright blue bright5=fb82ff # bright magenta bright6=60d3d1 # bright cyan bright7=eeeeee # bright white -selection-foreground=202020 -selection-background=1a3272 +# selection-foreground=202020 +# selection-background=1a3272 diff --git a/themes/material-design b/themes/material-design index 6d81e339..4a9e008a 100644 --- a/themes/material-design +++ b/themes/material-design @@ -5,8 +5,6 @@ [colors] foreground=ECEFF1 background=263238 -selection-foreground=ECEFF1 -selection-background=607D8B regular0=546E7A # black regular1=FF5252 # red regular2=5CF19E # green @@ -23,3 +21,5 @@ bright4=80D8FF # bright blue bright5=FF80AB # bright magenta bright6=A7FDEB # bright cyan bright7=FFFFFF # bright white +# selection-foreground=ECEFF1 +# selection-background=607D8B diff --git a/themes/moonfly b/themes/moonfly index c622ab45..870de9d0 100644 --- a/themes/moonfly +++ b/themes/moonfly @@ -8,8 +8,9 @@ color = 080808 9e9e9e [colors] foreground = b2b2b2 background = 080808 -selection-foreground = 080808 -selection-background = b2ceee + +# selection-foreground = 080808 +# selection-background = b2ceee regular0 = 323437 regular1 = ff5454 diff --git a/themes/nightfly b/themes/nightfly index 50f95125..2a27fb2d 100644 --- a/themes/nightfly +++ b/themes/nightfly @@ -8,8 +8,9 @@ color = 080808 9ca1aa [colors] foreground = acb4c2 background = 011627 -selection-foreground = 080808 -selection-background = b2ceee + +# selection-foreground = 080808 +# selection-background = b2ceee regular0 = 1d3b53 regular1 = fc514e diff --git a/themes/nord b/themes/nord index af09f727..4ce3a53e 100644 --- a/themes/nord +++ b/themes/nord @@ -12,8 +12,9 @@ color = 2e3440 d8dee9 [colors] foreground = d8dee9 background = 2e3440 -#selection-foreground = d8dee9 -#selection-background = 4c566a + +# selection-foreground = d8dee9 +# selection-background = 4c566a regular0 = 3b4252 regular1 = bf616a diff --git a/themes/solarized-dark b/themes/solarized-dark index e2395bdc..cad2945e 100644 --- a/themes/solarized-dark +++ b/themes/solarized-dark @@ -24,7 +24,7 @@ bright5= 6c71c4 bright6= 93a1a1 bright7= fdf6e3 -## Enable if prefer solarized colors instead of inverterd fg/bg for -## highlighting (mouse selection) +# Enable if prefer solarized colors instead of inverterd fg/bg for +# highlighting (mouse selection) # selection-foreground=93a1a1 # selection-background=073642 diff --git a/themes/solarized-dark-normal-brights b/themes/solarized-dark-normal-brights index 405a6e49..1ab7d375 100644 --- a/themes/solarized-dark-normal-brights +++ b/themes/solarized-dark-normal-brights @@ -26,7 +26,7 @@ bright5= dc619d bright6= 32c1b6 bright7= ffffff -## Enable if prefer solarized colors instead of inverterd fg/bg for -## highlighting (mouse selection) +# Enable if prefer solarized colors instead of inverterd fg/bg for +# highlighting (mouse selection) # selection-foreground=93a1a1 # selection-background=073642 diff --git a/themes/tempus-autumn b/themes/tempus-autumn index 3f706d0e..9c1f8797 100644 --- a/themes/tempus-autumn +++ b/themes/tempus-autumn @@ -25,5 +25,5 @@ bright4 = 958fdf bright5 = ce7dc4 bright6 = 2fa6b7 bright7 = a9a2a6 -#selection-foreground = a8948a -#selection-background = 36302a +# selection-foreground = a8948a +# selection-background = 36302a diff --git a/themes/tempus-classic b/themes/tempus-classic index 66fb886a..0164605b 100644 --- a/themes/tempus-classic +++ b/themes/tempus-classic @@ -25,5 +25,5 @@ bright4 = 8e9cc0 bright5 = d58888 bright6 = 7aa880 bright7 = aeadaf -#selection-foreground = 949d9f -#selection-background = 312e30 +# selection-foreground = 949d9f +# selection-background = 312e30 diff --git a/themes/tempus-dawn b/themes/tempus-dawn index fa368b13..cf143fba 100644 --- a/themes/tempus-dawn +++ b/themes/tempus-dawn @@ -25,5 +25,5 @@ bright4 = 5c59b2 bright5 = 8e45a8 bright6 = 3f649c bright7 = eff0f2 -#selection-foreground = 676364 -#selection-background = dee2e0 +# selection-foreground = 676364 +# selection-background = dee2e0 diff --git a/themes/tempus-day b/themes/tempus-day index 6002a8e2..b287d45c 100644 --- a/themes/tempus-day +++ b/themes/tempus-day @@ -25,5 +25,5 @@ bright4 = 0f64c4 bright5 = 8050a7 bright6 = 336c87 bright7 = f8f2e5 -#selection-foreground = 68607d -#selection-background = e7e3d7 +# selection-foreground = 68607d +# selection-background = e7e3d7 diff --git a/themes/tempus-dusk b/themes/tempus-dusk index e9f94725..2c0308e1 100644 --- a/themes/tempus-dusk +++ b/themes/tempus-dusk @@ -25,5 +25,5 @@ bright4 = 9ca5de bright5 = c69ac6 bright6 = 8caeb6 bright7 = a2a8ba -#selection-foreground = a29899 -#selection-background = 2c3150 +# selection-foreground = a29899 +# selection-background = 2c3150 diff --git a/themes/tempus-fugit b/themes/tempus-fugit index 1a6ae9e7..9ebbcee7 100644 --- a/themes/tempus-fugit +++ b/themes/tempus-fugit @@ -25,5 +25,5 @@ bright4 = 485adf bright5 = a234c0 bright6 = 00756a bright7 = fff5f3 -#selection-foreground = 796271 -#selection-background = efe6e4 +# selection-foreground = 796271 +# selection-background = efe6e4 diff --git a/themes/tempus-future b/themes/tempus-future index c0cdaad9..3dd8c7a6 100644 --- a/themes/tempus-future +++ b/themes/tempus-future @@ -25,5 +25,5 @@ bright4 = 8ba7ea bright5 = e08bd6 bright6 = 2cbab6 bright7 = b4abac -#selection-foreground = a7a2c4 -#selection-background = 2b1329 +# selection-foreground = a7a2c4 +# selection-background = 2b1329 diff --git a/themes/tempus-night b/themes/tempus-night index a431fbc6..de7be5ff 100644 --- a/themes/tempus-night +++ b/themes/tempus-night @@ -25,5 +25,5 @@ bright4 = 8cb4f0 bright5 = de99f0 bright6 = 00ca9a bright7 = e0e0e0 -#selection-foreground = c4bdaf -#selection-background = 242536 +# selection-foreground = c4bdaf +# selection-background = 242536 diff --git a/themes/tempus-past b/themes/tempus-past index 8f8ddcb1..8c66f54d 100644 --- a/themes/tempus-past +++ b/themes/tempus-past @@ -25,5 +25,5 @@ bright4 = 5559bb bright5 = b022a7 bright6 = 07707a bright7 = f3f2f4 -#selection-foreground = 80565d -#selection-background = eae2de +# selection-foreground = 80565d +# selection-background = eae2de diff --git a/themes/tempus-rift b/themes/tempus-rift index 6d1ed3a2..3657a7fe 100644 --- a/themes/tempus-rift +++ b/themes/tempus-rift @@ -25,5 +25,5 @@ bright4 = 56bdad bright5 = cca0ba bright6 = 10c480 bright7 = bbbcbc -#selection-foreground = ab9aa9 -#selection-background = 283431 +# selection-foreground = ab9aa9 +# selection-background = 283431 diff --git a/themes/tempus-spring b/themes/tempus-spring index 207434e9..d50e6d06 100644 --- a/themes/tempus-spring +++ b/themes/tempus-spring @@ -25,5 +25,5 @@ bright4 = 70afef bright5 = d095e2 bright6 = 3cbfaf bright7 = b5b8b7 -#selection-foreground = 99afae -#selection-background = 2a453d +# selection-foreground = 99afae +# selection-background = 2a453d diff --git a/themes/tempus-summer b/themes/tempus-summer index 3a852e46..7da1d8c4 100644 --- a/themes/tempus-summer +++ b/themes/tempus-summer @@ -25,5 +25,5 @@ bright4 = 8599ef bright5 = cc82d7 bright6 = 2aacbf bright7 = a0abae -#selection-foreground = 949cbf -#selection-background = 39304f +# selection-foreground = 949cbf +# selection-background = 39304f diff --git a/themes/tempus-tempest b/themes/tempus-tempest index 15b3d881..57c300aa 100644 --- a/themes/tempus-tempest +++ b/themes/tempus-tempest @@ -25,5 +25,5 @@ bright4 = 74e4cd bright5 = d2d4aa bright6 = 9bdfc4 bright7 = b6e0ca -#selection-foreground = b0c8ca -#selection-background = 323535 +# selection-foreground = b0c8ca +# selection-background = 323535 diff --git a/themes/tempus-totus b/themes/tempus-totus index 5ccac91d..01e84692 100644 --- a/themes/tempus-totus +++ b/themes/tempus-totus @@ -25,5 +25,5 @@ bright4 = 2d45b0 bright5 = 700dc9 bright6 = 005289 bright7 = ffffff -#selection-foreground = 5e4b4f -#selection-background = efefef +# selection-foreground = 5e4b4f +# selection-background = efefef diff --git a/themes/tempus-warp b/themes/tempus-warp index d4c7a94b..fa8c21c2 100644 --- a/themes/tempus-warp +++ b/themes/tempus-warp @@ -25,5 +25,5 @@ bright4 = 8887f0 bright5 = d85cf2 bright6 = 1da1af bright7 = a29fa0 -#selection-foreground = 968282 -#selection-background = 261c2c +# selection-foreground = 968282 +# selection-background = 261c2c diff --git a/themes/tempus-winter b/themes/tempus-winter index 22cdd6d9..8db97057 100644 --- a/themes/tempus-winter +++ b/themes/tempus-winter @@ -25,5 +25,5 @@ bright4 = 329fcb bright5 = ca77c5 bright6 = 1ba6a4 bright7 = 8da3b8 -#selection-foreground = 91959b -#selection-background = 2a2e38 +# selection-foreground = 91959b +# selection-background = 2a2e38 From 6259d59b4d82b0e8b7705759e04262130a3426c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 16 Dec 2022 16:56:43 +0100 Subject: [PATCH 0236/1323] config: change default grapheme-width-method from wcswidth to double-width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old default, wcswidth, simply calls wcswidth() on the grapheme cluster. This was supposedly the implementation with the highest application compatibility. Except we never even tried to measure it. It was just assumed. A lot of modern applications have better implementations. Let’s try to push support for better emoji support by changing our default method from wcswith to double-width. While far from correct (it’s not based on the Unicode tables), the ‘double-width’ method produces accurate results anyway. double-width is like wcswidth(), in that it adds together the individual wcwidths of all codepoints in the grapheme cluster. But, it limits the maximum width to 2. --- CHANGELOG.md | 1 + config.c | 2 +- doc/foot.ini.5.scd | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9538ccca..4aadd7bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,7 @@ * Set `PWD` environment variable in the slave process ([#1179][1179]). * DPI is now forced to 96 when found to be unreasonably high. * Set default log level to warning ([#1215][1215]). +* Default `grapheme-width-method` from `wcswidth` to `double-width`. [1166]: https://codeberg.org/dnkl/foot/issues/1166 [1179]: https://codeberg.org/dnkl/foot/issues/1179 diff --git a/config.c b/config.c index 7d849d5c..ea8d062f 100644 --- a/config.c +++ b/config.c @@ -3003,7 +3003,7 @@ config_load(struct config *conf, const char *conf_path, #if defined(FOOT_GRAPHEME_CLUSTERING) && FOOT_GRAPHEME_CLUSTERING .grapheme_shaping = fcft_caps & FCFT_CAPABILITY_GRAPHEME_SHAPING, #endif - .grapheme_width_method = GRAPHEME_WIDTH_WCSWIDTH, + .grapheme_width_method = GRAPHEME_WIDTH_DOUBLE, .delayed_render_lower_ns = 500000, /* 0.5ms */ .delayed_render_upper_ns = 16666666 / 2, /* half a frame period (60Hz) */ .max_shm_pool_size = 512 * 1024 * 1024, diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 6443d2ba..6cdb7db8 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1256,7 +1256,7 @@ any of these options. *max* uses the width of the largest codepoint in the cluster. - Default: _wcswidth_ + Default: _double-width_ *font-monospace-warn* Boolean. When enabled, foot will use heuristics to try to verify From e7c1a93d29f6e6d3962d8444834a8b90f367170d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 1 Jan 2023 10:22:54 +0100 Subject: [PATCH 0237/1323] terminfo: add entries for bracketed paste Ncurses added these in 2022-12-24, but they have been used/supported by vim since 2017. * BE - Bracketed paste Enable * BD - Bracketed paste Disable * PE - Paste Enable (i.e. "begin") * PD - Paste Disable (i.e. "end") --- CHANGELOG.md | 2 ++ foot.info | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aadd7bc..f10f2b75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,8 @@ * `font-size-adjustment=N[px]` option, letting you configure how much to increment/decrement the font size when zooming in or out ([#1188][1188]). +* Bracketed paste terminfo entries (`BD`, `BE`, `PD` and `PE`, added + to ncurses in 2022-12-24). Vim makes use of these. [1136]: https://codeberg.org/dnkl/foot/issues/1136 [1225]: https://codeberg.org/dnkl/foot/issues/1225 diff --git a/foot.info b/foot.info index f4030b22..33769278 100644 --- a/foot.info +++ b/foot.info @@ -28,10 +28,14 @@ it#8, lines#24, pairs#0x10000, + BD=\E[?2004l, + BE=\E[?2004h, Cr=\E]112\E\\, Cs=\E]12;%p1%s\E\\, E3=\E[3J, Ms=\E]52;%p1%s;%p2%s\E\\, + PD=\E[201~, + PE=\E[200~, Se=\E[ q, Ss=\E[%p1%d q, Sync=\E[?2026%?%p1%{1}%-%tl%eh, From 88641005fef7cc72db898ca4baccbca789d09f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 1 Jan 2023 15:21:05 +0100 Subject: [PATCH 0238/1323] terminfo: PD/PE -> PE/PS Ncurses 2022-12-24 had the names wrong. It was corrected on 2022-12-29. --- foot.info | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/foot.info b/foot.info index 33769278..271a00f5 100644 --- a/foot.info +++ b/foot.info @@ -34,8 +34,8 @@ Cs=\E]12;%p1%s\E\\, E3=\E[3J, Ms=\E]52;%p1%s;%p2%s\E\\, - PD=\E[201~, - PE=\E[200~, + PE=\E[201~, + PS=\E[200~, Se=\E[ q, Ss=\E[%p1%d q, Sync=\E[?2026%?%p1%{1}%-%tl%eh, From 1d3023ec5e3a996ab3b0b031292e0fb101ce5900 Mon Sep 17 00:00:00 2001 From: Craig Barnes Date: Sun, 1 Jan 2023 15:21:17 +0000 Subject: [PATCH 0239/1323] changelog: amend terminfo names, in accordance with previous commit --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f10f2b75..ac18a97e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,7 +56,7 @@ * `font-size-adjustment=N[px]` option, letting you configure how much to increment/decrement the font size when zooming in or out ([#1188][1188]). -* Bracketed paste terminfo entries (`BD`, `BE`, `PD` and `PE`, added +* Bracketed paste terminfo entries (`BD`, `BE`, `PE` and `PS`, added to ncurses in 2022-12-24). Vim makes use of these. [1136]: https://codeberg.org/dnkl/foot/issues/1136 From 63bef0dc8cf68e957eeb1f286d813b294c229ac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 2 Jan 2023 13:52:49 +0100 Subject: [PATCH 0240/1323] ci: drop gitlab CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We’re no longer mirroring to gitlab. --- .gitlab-ci.yml | 112 ------------------------------------------------- 1 file changed, 112 deletions(-) delete mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 28df1ccb..00000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,112 +0,0 @@ -stages: - - build - -variables: - GIT_SUBMODULE_STRATEGY: normal - -before_script: - - apk update - - apk add musl-dev linux-headers meson ninja gcc 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-x64: - image: alpine:edge - stage: build - script: - - cd subprojects - - git clone https://codeberg.org/dnkl/fcft.git - - cd .. - - mkdir -p bld/debug - - cd bld/debug - - 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 - artifacts: - reports: - junit: bld/debug/meson-logs/testlog.junit.xml - -debug-x64-no-grapheme-clustering: - image: alpine:edge - stage: build - script: - - apk del harfbuzz harfbuzz-dev utf8proc utf8proc-dev - - cd subprojects - - git clone https://codeberg.org/dnkl/fcft.git - - cd .. - - 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 - artifacts: - reports: - junit: bld/debug/meson-logs/testlog.junit.xml - -release-x64: - image: alpine:edge - stage: build - script: - - cd subprojects - - git clone https://codeberg.org/dnkl/fcft.git - - cd .. - - mkdir -p bld/release - - cd bld/release - - 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 - artifacts: - reports: - junit: bld/release/meson-logs/testlog.junit.xml - -debug-x86: - image: i386/alpine:edge - stage: build - script: - - cd subprojects - - git clone https://codeberg.org/dnkl/fcft.git - - cd .. - - mkdir -p bld/debug - - cd bld/debug - - 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 - artifacts: - reports: - junit: bld/debug/meson-logs/testlog.junit.xml - -release-x86: - image: i386/alpine:edge - stage: build - script: - - cd subprojects - - git clone https://codeberg.org/dnkl/fcft.git - - cd .. - - mkdir -p bld/release - - cd bld/release - - 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 - artifacts: - reports: - junit: bld/release/meson-logs/testlog.junit.xml - -codespell: - image: alpine:edge - stage: build - script: - - apk add python3 - - apk add py3-pip - - pip install codespell - - codespell -Lser,doas,zar README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd From c9465e4c5c81c9bb33886441e266c843b9425752 Mon Sep 17 00:00:00 2001 From: woojiq Date: Wed, 4 Jan 2023 12:35:53 +0200 Subject: [PATCH 0241/1323] themes: add Onedark --- themes/onedark | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 themes/onedark diff --git a/themes/onedark b/themes/onedark new file mode 100644 index 00000000..d7f78a66 --- /dev/null +++ b/themes/onedark @@ -0,0 +1,27 @@ +# OneDark +# Pallete based on the same theme from https://github.com/dexpota/kitty-themes + +[cursor] +color=111111 cccccc + +[colors] +foreground=979eab +background=282c34 +regular0=282c34 # black +regular1=e06c75 # red +regular2=98c379 # green +regular3=e5c07b # yellow +regular4=61afef # blue +regular5=be5046 # magenta +regular6=56b6c2 # cyan +regular7=979eab # white +bright0=393e48 # bright black +bright1=d19a66 # bright red +bright2=56b6c2 # bright green +bright3=e5c07b # bright yellow +bright4=61afef # bright blue +bright5=be5046 # bright magenta +bright6=56b6c2 # bright cyan +bright7=abb2bf # bright white +# selection-foreground=282c34 +# selection-background=979eab From f19768e30406ad154ebdb7f75ce8a656a130db4c Mon Sep 17 00:00:00 2001 From: Craig Barnes Date: Fri, 6 Jan 2023 23:43:51 +0000 Subject: [PATCH 0242/1323] wayland: avoid passing NULL to log_msg() in wayl_reload_xcursor_theme() This pointer ends up being passed to various printf-family functions, where passing a NULL pointer for an "%s" format specifier invokes undefined behaviour. --- wayland.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wayland.c b/wayland.c index e48d59aa..55d45da7 100644 --- a/wayland.c +++ b/wayland.c @@ -1704,7 +1704,8 @@ wayl_reload_xcursor_theme(struct seat *seat, int new_scale) const char *xcursor_theme = getenv("XCURSOR_THEME"); LOG_INFO("cursor theme: %s, size: %d, scale: %d", - xcursor_theme, xcursor_size, new_scale); + xcursor_theme ? xcursor_theme : "(null)", + xcursor_size, new_scale); seat->pointer.theme = wl_cursor_theme_load( xcursor_theme, xcursor_size * new_scale, seat->wayl->shm); From a38b8d02227f13ce1da5125905a262e298875a0d Mon Sep 17 00:00:00 2001 From: Grigory Kirillov Date: Sun, 8 Jan 2023 00:55:01 +0300 Subject: [PATCH 0243/1323] doc: fix a typo --- doc/foot.ini.5.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 6cdb7db8..07a3cb94 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -576,7 +576,7 @@ can configure the background transparency with the _alpha_ option. options are unconfigured). 24-bit RGB colors will typically fall into this category. - Note that applications can change the *regularN* and *brighN* + Note that applications can change the *regularN* and *brightN* colors at runtime. However, they have no way of changing the *dimN* colors. If an application has changed the *regularN* colors, foot will still use the corresponding *dimN* color, as From 7d28da50066601807b6790bbd77e2d8785e46884 Mon Sep 17 00:00:00 2001 From: Craig Barnes Date: Tue, 10 Jan 2023 18:34:25 +0000 Subject: [PATCH 0244/1323] Use "command -v" instead of "which" in bash completion scripts The former is a built-in command in bash, whereas the latter is an external command and isn't always necessarily available. --- completions/bash/foot | 4 ++-- completions/bash/footclient | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/completions/bash/foot b/completions/bash/foot index 1fdab062..b427bc58 100644 --- a/completions/bash/foot +++ b/completions/bash/foot @@ -61,11 +61,11 @@ _foot() compopt -o dirnames elif [[ ${prev} == '--term' ]] ; then # check if toe is available - which toe > /dev/null || return 1 + command -v toe > /dev/null || return 1 COMPREPLY=( $(compgen -W "$(toe -a | awk '$1 ~ /[+]/ {next}; {print $1}')" -- ${cur}) ) elif [[ ${prev} == '--font' ]] ; then # check if fc-list is available - which fc-list > /dev/null || return 1 + command -v fc-list > /dev/null || return 1 COMPREPLY=( $(compgen -W "$(fc-list : family | sed 's/,/\n/g' | uniq | tr -d ' ')" -- ${cur}) ) elif [[ ${prev} == '--log-level' ]] ; then COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) diff --git a/completions/bash/footclient b/completions/bash/footclient index b672c247..0381e8a0 100644 --- a/completions/bash/footclient +++ b/completions/bash/footclient @@ -57,7 +57,7 @@ _footclient() compopt -o dirnames elif [[ ${prev} == '--term' ]] ; then # check if toe is available - which toe > /dev/null || return 1 + command -v toe > /dev/null || return 1 COMPREPLY=( $(compgen -W "$(toe -a | awk '$1 ~ /[+]/ {next}; {print $1}')" -- ${cur}) ) elif [[ ${prev} == '--log-level' ]] ; then COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) From 8acc10b9d4bc1d4c245475e1812093bf8c9e3d78 Mon Sep 17 00:00:00 2001 From: Craig Barnes Date: Tue, 10 Jan 2023 19:44:24 +0000 Subject: [PATCH 0245/1323] completions: bash: use "case" instead of long if/elif/else chain --- completions/bash/foot | 52 ++++++++++++++++++++----------------- completions/bash/footclient | 45 ++++++++++++++++++-------------- 2 files changed, 53 insertions(+), 44 deletions(-) diff --git a/completions/bash/foot b/completions/bash/foot index b427bc58..c1bb20fa 100644 --- a/completions/bash/foot +++ b/completions/bash/foot @@ -31,7 +31,7 @@ _foot() cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} - # check if positional argument is completed + # Check if positional argument is completed previous_words=( "${COMP_WORDS[@]}" ) unset previous_words[-1] commands=$(compgen -c | grep -vFx "$(compgen -k)" | grep -vE '^([.:[]|foot)$' | sort -u) @@ -43,41 +43,45 @@ _foot() (( i++ )) continue fi - # positional argument found + # Positional argument found offset=$i fi (( i++ )) done if [[ ! -z "$offset" ]] ; then - # depends on bash_completion being available + # Depends on bash_completion being available declare -F _command_offset >/dev/null || return 1 _command_offset $offset + return 0 elif [[ ${cur} == --* ]] ; then COMPREPLY=( $(compgen -W "${flags}" -- ${cur}) ) - elif [[ ${prev} =~ ^(--config|--print-pid|--server)$ ]] ; then - compopt -o default - elif [[ ${prev} == '--working-directory' ]] ; then - compopt -o dirnames - elif [[ ${prev} == '--term' ]] ; then - # check if toe is available - command -v toe > /dev/null || return 1 - COMPREPLY=( $(compgen -W "$(toe -a | awk '$1 ~ /[+]/ {next}; {print $1}')" -- ${cur}) ) - elif [[ ${prev} == '--font' ]] ; then - # check if fc-list is available - command -v fc-list > /dev/null || return 1 - COMPREPLY=( $(compgen -W "$(fc-list : family | sed 's/,/\n/g' | uniq | tr -d ' ')" -- ${cur}) ) - elif [[ ${prev} == '--log-level' ]] ; then - COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) - elif [[ ${prev} == '--log-colorize' ]] ; then - COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) - elif [[ ${prev} =~ ^(--app-id|--help|--override|--title|--version|--window-size-chars|--window-size-pixels|--check-config)$ ]] ; then - : # don't autocomplete for these flags - else - # complete commands from $PATH - COMPREPLY=( $(compgen -c -- ${cur}) ) + return 0 fi + case "$prev" in + --config|--print-pid|--server) + compopt -o default ;; + --working-directory) + compopt -o dirnames ;; + --term) + command -v toe > /dev/null || return 1 + COMPREPLY=( $(compgen -W "$(toe -a | awk '$1 ~ /[+]/ {next}; {print $1}')" -- ${cur}) ) ;; + --font) + command -v fc-list > /dev/null || return 1 + COMPREPLY=( $(compgen -W "$(fc-list : family | sed 's/,/\n/g' | uniq | tr -d ' ')" -- ${cur}) ) ;; + --log-level) + COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) ;; + --log-colorize) + COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) ;; + --app-id|--help|--override|--title|--version|--window-size-chars|--window-size-pixels|--check-config) + # Don't autocomplete for these flags + : ;; + *) + # Complete commands from $PATH + COMPREPLY=( $(compgen -c -- ${cur}) ) ;; + esac + return 0 } diff --git a/completions/bash/footclient b/completions/bash/footclient index 0381e8a0..39cb070e 100644 --- a/completions/bash/footclient +++ b/completions/bash/footclient @@ -27,7 +27,7 @@ _footclient() cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} - # check if positional argument is completed + # Check if positional argument is completed previous_words=( "${COMP_WORDS[@]}" ) unset previous_words[-1] commands=$(compgen -c | grep -vFx "$(compgen -k)" | grep -vE '^([.:[]|footclient)$' | sort -u) @@ -39,37 +39,42 @@ _footclient() (( i++ )) continue fi - # positional argument found + # Positional argument found offset=$i fi (( i++ )) done if [[ ! -z "$offset" ]] ; then - # depends on bash_completion being available + # Depends on bash_completion being available declare -F _command_offset >/dev/null || return 1 _command_offset $offset + return 0 elif [[ ${cur} == --* ]] ; then COMPREPLY=( $(compgen -W "${flags}" -- ${cur}) ) - elif [[ ${prev} == '--server-socket' ]] ; then - compopt -o default - elif [[ ${prev} == '--working-directory' ]] ; then - compopt -o dirnames - elif [[ ${prev} == '--term' ]] ; then - # check if toe is available - command -v toe > /dev/null || return 1 - COMPREPLY=( $(compgen -W "$(toe -a | awk '$1 ~ /[+]/ {next}; {print $1}')" -- ${cur}) ) - elif [[ ${prev} == '--log-level' ]] ; then - COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) - elif [[ ${prev} == '--log-colorize' ]] ; then - COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) - elif [[ ${prev} =~ ^(--app-id|--help|--override|--title|--version|--window-size-chars|--window-size-pixels|)$ ]] ; then - : # don't autocomplete for these flags - else - # complete commands from $PATH - COMPREPLY=( $(compgen -c -- ${cur}) ) + return 0 fi + case "$prev" in + --server-socket) + compopt -o default ;; + --working-directory) + compopt -o dirnames ;; + --term) + command -v toe > /dev/null || return 1 + COMPREPLY=( $(compgen -W "$(toe -a | awk '$1 ~ /[+]/ {next}; {print $1}')" -- ${cur}) ) ;; + --log-level) + COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) ;; + --log-colorize) + COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) ;; + --app-id|--help|--override|--title|--version|--window-size-chars|--window-size-pixels) + # Don't autocomplete for these flags + : ;; + *) + # Complete commands from $PATH + COMPREPLY=( $(compgen -c -- ${cur}) ) ;; + esac + return 0 } From becdcd9bb78a552e65f90d3ebb79d286f9496229 Mon Sep 17 00:00:00 2001 From: Craig Barnes Date: Tue, 10 Jan 2023 19:56:12 +0000 Subject: [PATCH 0246/1323] completions: bash: complete option arguments for short options --- completions/bash/foot | 14 +++++++------- completions/bash/footclient | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/completions/bash/foot b/completions/bash/foot index c1bb20fa..71aea97c 100644 --- a/completions/bash/foot +++ b/completions/bash/foot @@ -60,21 +60,21 @@ _foot() fi case "$prev" in - --config|--print-pid|--server) + --config|--print-pid|--server|-[cps]) compopt -o default ;; - --working-directory) + --working-directory|-D) compopt -o dirnames ;; - --term) + --term|-t) command -v toe > /dev/null || return 1 COMPREPLY=( $(compgen -W "$(toe -a | awk '$1 ~ /[+]/ {next}; {print $1}')" -- ${cur}) ) ;; - --font) + --font|-f) command -v fc-list > /dev/null || return 1 COMPREPLY=( $(compgen -W "$(fc-list : family | sed 's/,/\n/g' | uniq | tr -d ' ')" -- ${cur}) ) ;; - --log-level) + --log-level|-d) COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) ;; - --log-colorize) + --log-colorize|-l) COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) ;; - --app-id|--help|--override|--title|--version|--window-size-chars|--window-size-pixels|--check-config) + --app-id|--help|--override|--title|--version|--window-size-chars|--window-size-pixels|--check-config|-[ahoTvWwC]) # Don't autocomplete for these flags : ;; *) diff --git a/completions/bash/footclient b/completions/bash/footclient index 39cb070e..62abdd65 100644 --- a/completions/bash/footclient +++ b/completions/bash/footclient @@ -56,18 +56,18 @@ _footclient() fi case "$prev" in - --server-socket) + --server-socket|-s) compopt -o default ;; - --working-directory) + --working-directory|-D) compopt -o dirnames ;; - --term) + --term|-t) command -v toe > /dev/null || return 1 COMPREPLY=( $(compgen -W "$(toe -a | awk '$1 ~ /[+]/ {next}; {print $1}')" -- ${cur}) ) ;; - --log-level) + --log-level|-d) COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) ;; - --log-colorize) + --log-colorize|-l) COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) ;; - --app-id|--help|--override|--title|--version|--window-size-chars|--window-size-pixels) + --app-id|--help|--override|--title|--version|--window-size-chars|--window-size-pixels|-[ahoTvWw]) # Don't autocomplete for these flags : ;; *) From 3f57afbf60aa622f0ae57ca9aa997b99623d3678 Mon Sep 17 00:00:00 2001 From: EuCaue Date: Mon, 9 Jan 2023 01:17:45 +0000 Subject: [PATCH 0247/1323] add rose-pine theme --- themes/rose-pine | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 themes/rose-pine diff --git a/themes/rose-pine b/themes/rose-pine new file mode 100644 index 00000000..6b58a66c --- /dev/null +++ b/themes/rose-pine @@ -0,0 +1,26 @@ +# -*- conf -*- +# Rose-Piné + +[cursor] +color=191724 e0def4 + +[colors] +background=191724 +foreground=e0def4 +regular0=26233a # black +regular1=eb6f92 # red +regular2=31748f # green +regular3=f6c177 # yellow +regular4=9ccfd8 # blue +regular5=c4a7e7 # magenta +regular6=ebbcba # cyan +regular7=e0def4 # white + +bright0=6e6a86 # bright black +bright1=eb6f92 # bright red +bright2=31748f # bright green +bright3=f6c177 # bright yellow +bright4=9ccfd8 # bright blue +bright5=c4a7e7 # bright magenta +bright6=ebbcba # bright cyan +bright7=e0def4 # bright white \ No newline at end of file From ffaf08e07c37a7fc02040efd4fcbda51b1cf5b02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 15 Jan 2023 10:23:44 +0100 Subject: [PATCH 0248/1323] config: remove unused struct --- config.h | 5 ----- 1 file changed, 5 deletions(-) diff --git a/config.h b/config.h index 648d92e4..31dddc64 100644 --- a/config.h +++ b/config.h @@ -74,11 +74,6 @@ enum key_binding_type { MOUSE_BINDING, }; -struct config_key_binding_text { - char *text; - bool master_copy; -}; - struct config_key_binding { int action; /* One of the varios bind_action_* enums from wayland.h */ struct config_key_modifiers modifiers; From 09f3475ad19753c5ca9dc8693b6937a7f75f7c5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 15 Jan 2023 10:24:01 +0100 Subject: [PATCH 0249/1323] =?UTF-8?q?config:=20don=E2=80=99t=20double-free?= =?UTF-8?q?=20key=20binding=20auxiliary=20data?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Key bindings with multiple key mappings share auxiliary data (e.g. the command to execute in pipe-* bindings, or the escape sequence in text-bindings). The first one is the designated “master” copy. Only that one should be freed. This fixed a double-free on exit, with e.g. [text-bindings] \x1b\x23=Mod4+space Mod4+equal Closes #1259 --- CHANGELOG.md | 3 +++ config.c | 3 +++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac18a97e..593d0692 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,12 +116,15 @@ config values (e.g. letter offsets, line height etc). * Selection being stuck visually when `IL` and `DL`.` * URL underlines sometimes still being visible after exiting URL mode. +* Text-bindings, and pipe-* bindings, with multiple key mappings + causing a crash (double-free) on exit ([#1259][1259]). [1173]: https://codeberg.org/dnkl/foot/issues/1173 [1190]: https://codeberg.org/dnkl/foot/issues/1190 [1205]: https://codeberg.org/dnkl/foot/issues/1205 [1209]: https://codeberg.org/dnkl/foot/issues/1209 [1218]: https://codeberg.org/dnkl/foot/issues/1218 +[1259]: https://codeberg.org/dnkl/foot/issues/1259 ### Security diff --git a/config.c b/config.c index ea8d062f..d77b50b8 100644 --- a/config.c +++ b/config.c @@ -1477,6 +1477,9 @@ parse_section_csd(struct context *ctx) static void free_binding_aux(struct binding_aux *aux) { + if (!aux->master_copy) + return; + switch (aux->type) { case BINDING_AUX_NONE: break; case BINDING_AUX_PIPE: free_argv(&aux->pipe); break; From d1220aebfd5814d53c49c814b8eaee456b5e9cf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 15 Jan 2023 14:00:06 +0100 Subject: [PATCH 0250/1323] terminfo: sync with ncurses 2023-01-14 * RV/rv: report DA2 * XR/xr: report version (XTVERSION) * XF: boolean, focus in/out events available --- CHANGELOG.md | 3 +++ foot.info | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 593d0692..2652f3ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,9 @@ ([#1188][1188]). * Bracketed paste terminfo entries (`BD`, `BE`, `PE` and `PS`, added to ncurses in 2022-12-24). Vim makes use of these. +* “Report version” terminfo entries (`XR`/`xr`). +* “Report DA2” terminfo entries (`RV`/`rv`). +* `XF` terminfo capability (focus in/out events available). [1136]: https://codeberg.org/dnkl/foot/issues/1136 [1225]: https://codeberg.org/dnkl/foot/issues/1225 diff --git a/foot.info b/foot.info index 271a00f5..34b05f23 100644 --- a/foot.info +++ b/foot.info @@ -12,6 +12,10 @@ setaf=\E[%?%p1%{8}%<%t3%p1%d%e38\:2\:\:%p1%{65536}%/%d\:%p1%{256}%/%{255}%&%d\:%p1%{255}%&%d%;m, @default_terminfo@+base|foot base fragment, + AX, + Tc, + XF, + XT, am, bce, bw, @@ -21,9 +25,6 @@ msgr, npc, xenl, - AX, - XT, - Tc, cols#80, it#8, lines#24, @@ -36,10 +37,12 @@ Ms=\E]52;%p1%s;%p2%s\E\\, PE=\E[201~, PS=\E[200~, + RV=\E[>c, Se=\E[ q, Ss=\E[%p1%d q, Sync=\E[?2026%?%p1%{1}%-%tl%eh, XM=\E[?1006;1000%?%p1%{1}%=%th%el%;, + XR=\E[>0q, acsc=``aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~, bel=^G, blink=\E[5m, @@ -242,6 +245,7 @@ rmxx=\E[29m, rs1=\Ec, rs2=\E[!p\E[?3;4l\E[4l\E>, + rv=\E\\[[0-9]+;[0-9]+;[0-9]+c, sc=\E7, setrgbb=\E[48\:2\:\:%p1%d\:%p2%d\:%p3%dm, setrgbf=\E[38\:2\:\:%p1%d\:%p2%d\:%p3%dm, @@ -264,6 +268,7 @@ u9=\E[c, vpa=\E[%i%p1%dd, xm=\E[<%i%p3%d;%p1%d;%p2%d;%?%p4%tM%em%;, + xr=\EP>\\|[ -~]+\E\\\\, # XT, # AX, From a9298959a188013530257a665d839a6343229b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 15 Jan 2023 14:42:48 +0100 Subject: [PATCH 0251/1323] render: fix double-width glyphs glitching when surrounding cells overflow into it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If cells overflowed (for example, by using an italic font that isn’t truly monospaced) into a double-width glyph (that itself is *not* overflowing), then the double-width glyph would glitch when being rendered; typically the second half of it would occasionally disappear. This happened because we tried to rasterize the second cell of the double-width glyph. This cell contains a special “spacer” value. Rasterizing that typically results the font’s “not available” glyph. If _that_ glyph overflows, things broke; we’d later end up forcing a re-render of it (thus erasing half the double-width glyph). But since the double-width glyph _itself_ doesn’t overflow, _it_ wouldn’t be re-rendered, leaving it half erased. Fix by recognizing spacer cells, and not trying to rasterize them (set glyph count to 0, and cell count to 1). Closes #1256 --- CHANGELOG.md | 3 +++ render.c | 16 ++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2652f3ce..95aaab07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -121,6 +121,8 @@ * URL underlines sometimes still being visible after exiting URL mode. * Text-bindings, and pipe-* bindings, with multiple key mappings causing a crash (double-free) on exit ([#1259][1259]). +* Double-width glyphs glitching when surrounded by glyphs overflowing + into the double-width glyph ([#1256][1256]). [1173]: https://codeberg.org/dnkl/foot/issues/1173 [1190]: https://codeberg.org/dnkl/foot/issues/1190 @@ -128,6 +130,7 @@ [1209]: https://codeberg.org/dnkl/foot/issues/1209 [1218]: https://codeberg.org/dnkl/foot/issues/1218 [1259]: https://codeberg.org/dnkl/foot/issues/1259 +[1256]: https://codeberg.org/dnkl/foot/issues/1256 ### Security diff --git a/render.c b/render.c index f5e7f627..92a37d7c 100644 --- a/render.c +++ b/render.c @@ -646,17 +646,21 @@ render_cell(struct terminal *term, pixman_image_t *pix, } } - if (single == NULL && grapheme == NULL) { - xassert(base != 0); - single = fcft_rasterize_char_utf32(font, base, term->font_subpixel); - if (single == NULL) { + if (unlikely(base >= CELL_SPACER)) { glyph_count = 0; cell_cols = 1; } else { - glyph_count = 1; - glyphs = &single; + xassert(base != 0); + single = fcft_rasterize_char_utf32(font, base, term->font_subpixel); + if (single == NULL) { + glyph_count = 0; + cell_cols = 1; + } else { + glyph_count = 1; + glyphs = &single; cell_cols = single->cols; + } } } } From b81b98d47c9570f2ac7356b1ff68a284b03759b5 Mon Sep 17 00:00:00 2001 From: Craig Barnes Date: Tue, 17 Jan 2023 23:49:32 +0000 Subject: [PATCH 0252/1323] render: fix incorrect indent introduced by previous commit --- render.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.c b/render.c index 92a37d7c..1c9dcb16 100644 --- a/render.c +++ b/render.c @@ -659,7 +659,7 @@ render_cell(struct terminal *term, pixman_image_t *pix, } else { glyph_count = 1; glyphs = &single; - cell_cols = single->cols; + cell_cols = single->cols; } } } From 1823fa846ae8205c8bbe78204af599277a743896 Mon Sep 17 00:00:00 2001 From: Craig Barnes Date: Fri, 27 Jan 2023 11:47:12 +0000 Subject: [PATCH 0253/1323] completions: bash: simplify awk command used to filter terminfo names --- completions/bash/foot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/completions/bash/foot b/completions/bash/foot index 71aea97c..eb17dad1 100644 --- a/completions/bash/foot +++ b/completions/bash/foot @@ -66,7 +66,7 @@ _foot() compopt -o dirnames ;; --term|-t) command -v toe > /dev/null || return 1 - COMPREPLY=( $(compgen -W "$(toe -a | awk '$1 ~ /[+]/ {next}; {print $1}')" -- ${cur}) ) ;; + COMPREPLY=( $(compgen -W "$(toe -a | awk '$1 !~ /[+]/ {print $1}')" -- ${cur}) ) ;; --font|-f) command -v fc-list > /dev/null || return 1 COMPREPLY=( $(compgen -W "$(fc-list : family | sed 's/,/\n/g' | uniq | tr -d ' ')" -- ${cur}) ) ;; From 1c16e4a575713581140b81198c0a57dbbf5c0cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 12 Feb 2023 19:09:48 +0100 Subject: [PATCH 0254/1323] Tag a couple variables with UNUSED, to fix warnings with clang-15 Closes #1278 --- sixel.c | 2 +- uri.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sixel.c b/sixel.c index a824c405..592f48f8 100644 --- a/sixel.c +++ b/sixel.c @@ -154,7 +154,7 @@ verify_list_order(const struct terminal *term) int prev_col_count = 0; /* To aid debugging */ - size_t idx = 0; + size_t UNUSED idx = 0; tll_foreach(term->grid->sixel_images, it) { int row = grid_row_abs_to_sb( diff --git a/uri.c b/uri.c index 39073bde..7214a479 100644 --- a/uri.c +++ b/uri.c @@ -159,7 +159,7 @@ uri_parse(const char *uri, size_t len, char *p = decoded; size_t encoded_len = path_len; - size_t decoded_len = 0; + size_t UNUSED decoded_len = 0; while (true) { /* Find next '%' */ From 25154a81509934074934bd71c986d9866103f293 Mon Sep 17 00:00:00 2001 From: Craig Barnes Date: Thu, 16 Feb 2023 08:50:53 +0000 Subject: [PATCH 0255/1323] doc: ctlseq: fix capitalization in description of DA3 sequence --- doc/foot-ctlseqs.7.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index f06a9418..ec970127 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -504,7 +504,7 @@ manipulation sequences. The generic format is: | \\E[ = _Ps_ c : DA3 : VT510 -: send tertiary device attributes. Foot responds with "FOOT", in +: Send tertiary device attributes. Foot responds with "FOOT", in hexadecimal. | \\E[ _Pm_ d : VPA From f2356adee392edefa54009c9932a37e6c5a45b20 Mon Sep 17 00:00:00 2001 From: Craig Barnes Date: Thu, 16 Feb 2023 09:09:51 +0000 Subject: [PATCH 0256/1323] doc: foot.ini: fix spelling mistake in [bell].urgent description --- doc/foot.ini.5.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 07a3cb94..1fbca27a 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -365,7 +365,7 @@ Note: do not set *TERM* here; use the *term* option in the main *urgent* When set to _yes_, foot will signal urgency to the compositor through the XDG activation protocol whenever *BEL* is received, - and the window does NOT have keyboard foccus. + and the window does NOT have keyboard focus. If the compositor does not implement this protocol, the margins will be painted in red instead. From 7f26914583a0114e102e4596763302a5bc6d1730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 29 Dec 2022 11:32:21 +0100 Subject: [PATCH 0257/1323] wayland: ignore configure events for unmapped surfaces Closes #1249 Note that it is still unclear whether ack:ing a configure event for an unmapped surface is a protocol violation, or something that should be handled by the compositor. According to https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/108, Kwin, Mutter and Weston handles it, while wlroots does not. --- CHANGELOG.md | 3 +++ wayland.c | 11 +++++++++++ wayland.h | 1 + 3 files changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95aaab07..0b11bbc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -123,6 +123,8 @@ causing a crash (double-free) on exit ([#1259][1259]). * Double-width glyphs glitching when surrounded by glyphs overflowing into the double-width glyph ([#1256][1256]). +* Wayland protocol violation when ack:ing a configure event for an + unmapped surface ([#1249][1249]). [1173]: https://codeberg.org/dnkl/foot/issues/1173 [1190]: https://codeberg.org/dnkl/foot/issues/1190 @@ -131,6 +133,7 @@ [1218]: https://codeberg.org/dnkl/foot/issues/1218 [1259]: https://codeberg.org/dnkl/foot/issues/1259 [1256]: https://codeberg.org/dnkl/foot/issues/1256 +[1249]: https://codeberg.org/dnkl/foot/issues/1249 ### Security diff --git a/wayland.c b/wayland.c index 55d45da7..68a7a4f1 100644 --- a/wayland.c +++ b/wayland.c @@ -769,6 +769,16 @@ xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, struct wl_window *win = data; struct terminal *term = win->term; + if (win->unmapped) { + /* + * https://codeberg.org/dnkl/foot/issues/1249 + * https://gitlab.freedesktop.org/wlroots/wlroots/-/issues/3487 + * https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/3719 + * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/108 + */ + return; + } + bool wasnt_configured = !win->is_configured; bool was_resizing = win->is_resizing; bool csd_was_enabled = win->csd_mode == CSD_YES && !win->is_fullscreen; @@ -1619,6 +1629,7 @@ wayl_win_destroy(struct wl_window *win) wayl_roundtrip(win->term->wl); /* Main window */ + win->unmapped = true; wl_surface_attach(win->surface, NULL, 0, 0); wl_surface_commit(win->surface); wayl_roundtrip(win->term->wl); diff --git a/wayland.h b/wayland.h index e86c6a3d..4b6939ab 100644 --- a/wayland.h +++ b/wayland.h @@ -327,6 +327,7 @@ struct wl_window { tll(struct xdg_activation_token_context *) xdg_tokens; bool urgency_token_is_pending; #endif + bool unmapped; struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration; From 7a43737745e5e6d8c5ef1cb29ade75b6ca2d6f03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 27 Feb 2023 17:51:29 +0100 Subject: [PATCH 0258/1323] =?UTF-8?q?render:=20fix=20selected=20cursor=20c?= =?UTF-8?q?ell=20being=20=E2=80=98invisble=E2=80=99=20when=20background=20?= =?UTF-8?q?alpha=20is=20used?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ... by taking the cell ‘selected’ state into account when determining whether to use the default fg or bg as the ‘text’ color. --- render.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.c b/render.c index 1c9dcb16..e85d669b 100644 --- a/render.c +++ b/render.c @@ -424,7 +424,7 @@ cursor_colors_for_cell(const struct terminal *term, const struct cell *cell, /* We *know* this only happens when bg is the default bg * color */ *text_color = color_hex_to_pixman( - term->reverse ? term->colors.fg : term->colors.bg); + term->reverse ^ is_selected ? term->colors.fg : term->colors.bg); } else *text_color = *bg; } From 8a849b4b08539766e8251fa9e7d56c557878617b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 28 Feb 2023 17:49:57 +0100 Subject: [PATCH 0259/1323] render: fix inversed cursor fg color when alpha != 1.0, take #2 No need to check if terminal colors have been reversed - this is done by the cell rendering logic. This hopefully fixes all remaining issues with invisible text when background alpha < 1.0 --- render.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/render.c b/render.c index e85d669b..b1c56bbc 100644 --- a/render.c +++ b/render.c @@ -419,14 +419,13 @@ cursor_colors_for_cell(const struct terminal *term, const struct cell *cell, } } else { *cursor_color = *fg; + *text_color = *bg; if (unlikely(text_color->alpha != 0xffff)) { - /* We *know* this only happens when bg is the default bg - * color */ - *text_color = color_hex_to_pixman( - term->reverse ^ is_selected ? term->colors.fg : term->colors.bg); - } else - *text_color = *bg; + /* The *only* color that can have transparency is the + * default background color */ + *text_color = color_hex_to_pixman(term->colors.bg); + } } } From 514fcc20a73feec88f9a912a8dedf7eac40ca55b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 2 Mar 2023 17:22:27 +0100 Subject: [PATCH 0260/1323] render: resize: call xdg_toplevel_set_min_size() This is a hint to the compositor, not to set a smaller size than this. --- CHANGELOG.md | 1 + render.c | 17 ++++++++--------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b11bbc0..4fc80821 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -125,6 +125,7 @@ into the double-width glyph ([#1256][1256]). * Wayland protocol violation when ack:ing a configure event for an unmapped surface ([#1249][1249]). +* `xdg\_toplevel::set_min_size()` not being called. [1173]: https://codeberg.org/dnkl/foot/issues/1173 [1190]: https://codeberg.org/dnkl/foot/issues/1190 diff --git a/render.c b/render.c index b1c56bbc..9c701000 100644 --- a/render.c +++ b/render.c @@ -3885,9 +3885,9 @@ maybe_resize(struct terminal *term, int width, int height, bool force) const int min_cols = 2; const int min_rows = 1; - /* Minimum window size */ - const int min_width = min_cols * term->cell_width; - const int min_height = min_rows * term->cell_height; + /* Minimum window size (must be divisible by the scaling factor)*/ + const int min_width = (min_cols * term->cell_width + scale - 1) / scale * scale; + const int min_height = (min_rows * term->cell_height + scale - 1) / scale * scale; width = max(width, min_width); height = max(height, min_height); @@ -4132,12 +4132,6 @@ damage_view: term->stashed_height = term->height; } -#if 0 - /* TODO: doesn't include CSD title bar */ - xdg_toplevel_set_min_size( - term->window->xdg_toplevel, min_width / scale, min_height / scale); -#endif - { const bool title_shown = wayl_win_csd_titlebar_visible(term->window); const bool border_shown = wayl_win_csd_borders_visible(term->window); @@ -4147,6 +4141,11 @@ damage_view: const int border_width = border_shown ? term->conf->csd.border_width_visible : 0; + xdg_toplevel_set_min_size( + term->window->xdg_toplevel, + min_width / scale + 2 * border_width, + min_height / scale + title_height + 2 * border_width); + xdg_surface_set_window_geometry( term->window->xdg_surface, -border_width, From 9a5a2d9957d8421b815d8ab07e867e73112b9591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 27 Feb 2023 17:56:03 +0100 Subject: [PATCH 0261/1323] key-binding: sort binding lists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sort bindings such that bindings with the same symbol are sorted with the binding having the most modifiers comes first. This fixes an issue where the “wrong” key binding are triggered when used with “consumed” modifiers. For example: if Control+BackSpace is bound before Control+Shift+BackSpace, then the latter binding is never triggered. Why? Because Shift is a consumed modifier. This means Control+BackSpace is “the same” as Control+Shift+BackSpace. By sorting bindings with more modifiers first, we work around the problem. But note that it is *just* a workaround, and I’m not confident there aren’t cases where it doesn’t work. Closes #1280 --- CHANGELOG.md | 3 +++ key-binding.c | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fc80821..40072182 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -126,6 +126,8 @@ * Wayland protocol violation when ack:ing a configure event for an unmapped surface ([#1249][1249]). * `xdg\_toplevel::set_min_size()` not being called. +* Key bindings with consumed modifiers masking other key bindings + ([#1280][1280]). [1173]: https://codeberg.org/dnkl/foot/issues/1173 [1190]: https://codeberg.org/dnkl/foot/issues/1190 @@ -135,6 +137,7 @@ [1259]: https://codeberg.org/dnkl/foot/issues/1259 [1256]: https://codeberg.org/dnkl/foot/issues/1256 [1249]: https://codeberg.org/dnkl/foot/issues/1249 +[1280]: https://codeberg.org/dnkl/foot/issues/1280 ### Security diff --git a/key-binding.c b/key-binding.c index 1876a885..1dffd3ee 100644 --- a/key-binding.c +++ b/key-binding.c @@ -350,6 +350,60 @@ maybe_repair_key_combo(const struct seat *seat, return sym; } +static int +key_cmp(struct key_binding a, struct key_binding b) +{ + xassert(a.type == b.type); + + /* + * Sort bindings such that bindings with the same symbol are + * sorted with the binding having the most modifiers comes first. + * + * This fixes an issue where the “wrong” key binding are triggered + * when used with “consumed” modifiers. + * + * For example: if Control+BackSpace is bound before + * Control+Shift+BackSpace, then the latter binding is never + * triggered. + * + * Why? Because Shift is a consumed modifier. This means + * Control+BackSpace is “the same” as Control+Shift+BackSpace. + * + * By sorting bindings with more modifiers first, we work around + * the problem. But note that it is *just* a workaround, and I’m + * not confident there aren’t cases where it doesn’t work. + * + * See https://codeberg.org/dnkl/foot/issues/1280 + */ + + const int a_mod_count = __builtin_popcount(a.mods); + const int b_mod_count = __builtin_popcount(b.mods); + + switch (a.type) { + case KEY_BINDING: + if (a.k.sym != b.k.sym) + return b.k.sym - a.k.sym; + return b_mod_count - a_mod_count; + + case MOUSE_BINDING: { + if (a.m.button != b.m.button) + return b.m.button - a.m.button; + if (a_mod_count != b_mod_count) + return b_mod_count - a_mod_count; + return b.m.count - a.m.count; + } + } + + BUG("invalid key binding type"); + return 0; +} + +static void NOINLINE +sort_binding_list(key_binding_list_t *list) +{ + tll_sort(*list, key_cmp); +} + static void NOINLINE convert_key_binding(struct key_set *set, const struct config_key_binding *conf_binding, @@ -371,6 +425,7 @@ convert_key_binding(struct key_set *set, }, }; tll_push_back(*bindings, binding); + sort_binding_list(bindings); } static void @@ -421,6 +476,7 @@ convert_mouse_binding(struct key_set *set, }, }; tll_push_back(set->public.mouse, binding); + sort_binding_list(&set->public.mouse); } static void From 9da1b1cec33080890d12946b292511b1f2e17a24 Mon Sep 17 00:00:00 2001 From: jaroeichler Date: Mon, 27 Feb 2023 13:32:13 +0000 Subject: [PATCH 0262/1323] themes: add Material Amber --- themes/material-amber | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 themes/material-amber diff --git a/themes/material-amber b/themes/material-amber new file mode 100644 index 00000000..ee2c21b5 --- /dev/null +++ b/themes/material-amber @@ -0,0 +1,40 @@ +# -*- conf -*- +# Material Amber +# Based on material.io guidelines with Amber 50 background + +# [cursor] +# color=fff8e1 21201d + +[colors] +foreground = 21201d +background = fff8e1 + +regular0 = 21201d # black +regular1 = cd4340 # red +regular2 = 498d49 # green +regular3 = fab32d # yellow +regular4 = 3378c4 # blue +regular5 = b83269 # magenta +regular6 = 21929a # cyan +regular7 = ffd7d7 # white + +bright0 = 66635a # bright black +bright1 = dd7b72 # bright red +bright2 = 82ae78 # bright green +bright3 = fbc870 # bright yellow +bright4 = 73a0cd # bright blue +bright5 = ce6f8e # bright magenta +bright6 = 548c94 # bright cyan +bright7 = ffe1da # bright white + +dim0 = 9e9a8c # dim black +dim1 = e9a99b # dim red +dim2 = b0c99f # dim green +dim3 = fdda9a # dim yellow +dim4 = a6c0d4 # dim blue +dim5 = e0a1ad # dim magenta +dim6 = 3c6064 # dim cyan +dim7 = ffe9dd # dim white + +# selection-foreground=fff8e1 +# selection-background=21201d From 9f3ce9236f9818263437cb6a4f26b20c5506a218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 3 Mar 2023 17:21:11 +0100 Subject: [PATCH 0263/1323] =?UTF-8?q?config:=20apply=20fontconfig=20rules?= =?UTF-8?q?=20if=20user=20didn=E2=80=99t=20set=20an=20explicit=20font=20si?= =?UTF-8?q?ze?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If the user didn’t explicitly set the font size (e.g. font=monospace, instead of font=monospace:size=12), our initial attempt to read the FC_SIZE and FC_PIXEL_SIZE attributes will fail, and we used to fallback to setting the size to 8pt. Change this slightly, so that when we fail to read the FC_*_SIZE attributes, apply the fontconfig rules, but *without expanding* them (i.e. without calling FcDefaultSubstitute()). Then try reading FC_*_SIZE again. If that too fails, _then_ set size to 8pt. This allows us to pick up rules that set a default {pixel}size: 14 Closes #1287 --- CHANGELOG.md | 4 ++++ config.c | 40 ++++++++++++++++++++++++++++++++++------ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40072182..08d7d69f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,10 +81,14 @@ * DPI is now forced to 96 when found to be unreasonably high. * Set default log level to warning ([#1215][1215]). * Default `grapheme-width-method` from `wcswidth` to `double-width`. +* When determining initial font size, do FontConfig config + substitution if the user-provided font pattern has no {pixel}size + option ([#1287][1287]). [1166]: https://codeberg.org/dnkl/foot/issues/1166 [1179]: https://codeberg.org/dnkl/foot/issues/1179 [1215]: https://codeberg.org/dnkl/foot/pulls/1215 +[1287]: https://codeberg.org/dnkl/foot/issues/1287 ### Deprecated diff --git a/config.c b/config.c index d77b50b8..2ede1aa5 100644 --- a/config.c +++ b/config.c @@ -3390,20 +3390,48 @@ config_font_parse(const char *pattern, struct config_font *font) if (pat == NULL) return false; + /* + * First look for user specified {pixel}size option + * e.g. “font-name:size=12” + */ + double pt_size = -1.0; - FcPatternGetDouble(pat, FC_SIZE, 0, &pt_size); - FcPatternRemove(pat, FC_SIZE, 0); + FcResult have_pt_size = FcPatternGetDouble(pat, FC_SIZE, 0, &pt_size); int px_size = -1; - FcPatternGetInteger(pat, FC_PIXEL_SIZE, 0, &px_size); - FcPatternRemove(pat, FC_PIXEL_SIZE, 0); + FcResult have_px_size = FcPatternGetInteger(pat, FC_PIXEL_SIZE, 0, &px_size); - if (pt_size == -1. && px_size == -1) - pt_size = 8.0; + if (have_pt_size != FcResultMatch && have_px_size != FcResultMatch) { + /* + * Apply fontconfig config. Can’t do that until we’ve first + * checked for a user provided size, since we may end up with + * both “size” and “pixelsize” being set, and we don’t know + * which one takes priority. + */ + FcPattern *pat_copy = FcPatternDuplicate(pat); + if (pat_copy == NULL || + !FcConfigSubstitute(NULL, pat_copy, FcMatchPattern)) + { + LOG_WARN("%s: failed to do config substitution", pattern); + } else { + have_pt_size = FcPatternGetDouble(pat_copy, FC_SIZE, 0, &pt_size); + have_px_size = FcPatternGetInteger(pat_copy, FC_PIXEL_SIZE, 0, &px_size); + } + + FcPatternDestroy(pat_copy); + + if (have_pt_size != FcResultMatch && have_px_size != FcResultMatch) + pt_size = 8.0; + } + + FcPatternRemove(pat, FC_SIZE, 0); + FcPatternRemove(pat, FC_PIXEL_SIZE, 0); char *stripped_pattern = (char *)FcNameUnparse(pat); FcPatternDestroy(pat); + LOG_DBG("%s: pt-size=%.2f, px-size=%d", stripped_pattern, pt_size, px_size); + *font = (struct config_font){ .pattern = stripped_pattern, .pt_size = pt_size, From 5b2f02d826d8043b6b89860c56802ea5fed0f3bd Mon Sep 17 00:00:00 2001 From: Craig Barnes Date: Mon, 20 Mar 2023 14:40:36 +0000 Subject: [PATCH 0264/1323] slave: set $TERM_PROGRAM and $TERM_PROGRAM_VERSION environment variables These are already being set by iTerm2, WezTerm, tmux and likely some others. Even though using yet more environment variables seems rather questionable, if we don't set these we run the risk of inheriting them from other terminals. See also: * https://gitlab.com/gnachman/iterm2/-/blob/97a6078df8e822da165e91737a01c0b9e115dd16/sources/PTYSession.m#L2568-2570 * https://github.com/tmux/tmux/blob/1d0f68dee9f71c504e03616fa472a408a6caa49b/environ.c#L263-L264 * https://github.com/search?q=TERM_PROGRAM&type=code --- CHANGELOG.md | 2 ++ doc/foot.1.scd | 11 +++++++++++ doc/footclient.1.scd | 11 +++++++++++ generate-version.sh | 1 + slave.c | 3 +++ 5 files changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08d7d69f..01d664fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,8 @@ * “Report version” terminfo entries (`XR`/`xr`). * “Report DA2” terminfo entries (`RV`/`rv`). * `XF` terminfo capability (focus in/out events available). +* `$TERM_PROGRAM` and `$TERM_PROGRAM_VERSION` environment variables + set in the slave process. [1136]: https://codeberg.org/dnkl/foot/issues/1136 [1225]: https://codeberg.org/dnkl/foot/issues/1225 diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 6f63d4c8..51c53130 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -546,6 +546,17 @@ In all other cases, the exit code is that of the client application This variable is set to *truecolor*, to indicate to client applications that 24-bit RGB colors are supported. +*TERM_PROGRAM* + Always set to *foot*. This can be used by client applications to + check which terminal is in use, but with the caveat that it may + have been inherited from a parent process in other terminals that + aren't known to set the variable. + +*TERM_PROGRAM_VERSION* + Set to the foot version string, in the format _major_*.*_minor_*.*_patch_ + or _major_*.*_minor_*.*_patch_*-*_revision_*-\g*_commit_ for inter-release + builds. The same caveat as for *TERM_PROGRAM* applies. + In addition to the variables listed above, custom environment variables may be defined in *foot.ini*(5). diff --git a/doc/footclient.1.scd b/doc/footclient.1.scd index 63235134..189d9e3c 100644 --- a/doc/footclient.1.scd +++ b/doc/footclient.1.scd @@ -158,6 +158,17 @@ terminfo entries manually, by copying *foot* and *foot-direct* to This variable is set to *truecolor*, to indicate to client applications that 24-bit RGB colors are supported. +*TERM_PROGRAM* + Always set to *foot*. This can be used by client applications to + check which terminal is in use, but with the caveat that it may + have been inherited from a parent process in other terminals that + aren't known to set the variable. + +*TERM_PROGRAM_VERSION* + Set to the foot version string, in the format _major_*.*_minor_*.*_patch_ + or _major_*.*_minor_*.*_patch_*-*_revision_*-\g*_commit_ for inter-release + builds. The same caveat as for *TERM_PROGRAM* applies. + In addition to the variables listed above, custom environment variables may be defined in *foot.ini*(5). diff --git a/generate-version.sh b/generate-version.sh index a030d512..3772008b 100755 --- a/generate-version.sh +++ b/generate-version.sh @@ -41,6 +41,7 @@ patch=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\3/') extra=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+)(-([0-9]+-g[a-z0-9]+) .*)?.*/\5/') new_version="#define FOOT_VERSION \"${new_version}\" +#define FOOT_VERSION_SHORT \"${git_version:-${default_version}}\" #define FOOT_MAJOR ${major} #define FOOT_MINOR ${minor} #define FOOT_PATCH ${patch} diff --git a/slave.c b/slave.c index d4861ad0..2f23e996 100644 --- a/slave.c +++ b/slave.c @@ -21,6 +21,7 @@ #include "macros.h" #include "terminal.h" #include "tokenize.h" +#include "version.h" #include "xmalloc.h" extern char **environ; @@ -351,6 +352,8 @@ slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, } setenv("TERM", term_env, 1); + setenv("TERM_PROGRAM", "foot", 1); + setenv("TERM_PROGRAM_VERSION", FOOT_VERSION_SHORT, 1); setenv("COLORTERM", "truecolor", 1); setenv("PWD", cwd, 1); From 296e75f4f54f56be7d36fcccb032dca9e5be959b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 27 Mar 2023 16:53:41 +0200 Subject: [PATCH 0265/1323] =?UTF-8?q?render:=20fix=20glitchy=20selection?= =?UTF-8?q?=20while=20resizing=20the=20=E2=80=98normal=E2=80=99=20screen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The selection coordinates are in absolute row numbers. As such, selection breaks when interactively resizing the normal grid, since we then instantiate a temporary grid mapping directly to the current viewport (for performance reason, to avoid reflowing the entire grid over and over again). Fix by stashing the actual selection coordinates, and ajusting the "active" ones to the temporary grid. --- render.c | 6 ++++++ terminal.h | 1 + 2 files changed, 7 insertions(+) diff --git a/render.c b/render.c index 9c701000..2aba3237 100644 --- a/render.c +++ b/render.c @@ -3976,6 +3976,7 @@ maybe_resize(struct terminal *term, int width, int height, bool force) term->interactive_resizing.old_hide_cursor = term->hide_cursor; term->interactive_resizing.grid = xmalloc(sizeof(*term->interactive_resizing.grid)); *term->interactive_resizing.grid = term->normal; + term->interactive_resizing.selection_coords = term->selection.coords; } else { /* We’ll replace the current temporary grid, with a new * one (again based on the original grid) */ @@ -4013,6 +4014,9 @@ maybe_resize(struct terminal *term, int width, int height, bool force) .kitty_kbd = orig->kitty_kbd, }; + term->selection.coords.start.row -= orig->view; + term->selection.coords.end.row -= orig->view; + for (size_t i = 0, j = orig->view; i < term->interactive_resizing.old_screen_rows; i++, j = (j + 1) & (orig->num_rows - 1)) @@ -4069,6 +4073,7 @@ maybe_resize(struct terminal *term, int width, int height, bool force) free(term->interactive_resizing.grid); term->hide_cursor = term->interactive_resizing.old_hide_cursor; + term->selection.coords = term->interactive_resizing.selection_coords; old_rows = term->interactive_resizing.old_screen_rows; @@ -4076,6 +4081,7 @@ maybe_resize(struct terminal *term, int width, int height, bool force) term->interactive_resizing.old_screen_rows = 0; term->interactive_resizing.new_rows = 0; term->interactive_resizing.old_hide_cursor = false; + term->interactive_resizing.selection_coords = (struct range){{-1, -1}, {-1, -1}}; term_ptmx_resume(term); } diff --git a/terminal.h b/terminal.h index ec5560cd..21236797 100644 --- a/terminal.h +++ b/terminal.h @@ -604,6 +604,7 @@ struct terminal { int old_cols; /* term->cols before resize started */ int old_hide_cursor; /* term->hide_cursor before resize started */ int new_rows; /* New number of scrollback rows */ + struct range selection_coords; } interactive_resizing; struct { From ae26915916731813edc2aefeb18a5754e8c79cd4 Mon Sep 17 00:00:00 2001 From: Harri Nieminen Date: Wed, 29 Mar 2023 00:31:49 +0300 Subject: [PATCH 0266/1323] fix typos --- INSTALL.md | 2 +- csi.c | 2 +- doc/foot.ini.5.scd | 2 +- grid.c | 2 +- tests/test-config.c | 10 +++++----- themes/onedark | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 4c15b7b4..6cc51750 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -440,7 +440,7 @@ sed 's/@default_terminfo@/foot/g' foot.info | \ 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. +` can simply be omitted. Or, if packaging: diff --git a/csi.c b/csi.c index e77ae971..cef48d43 100644 --- a/csi.c +++ b/csi.c @@ -1518,7 +1518,7 @@ csi_dispatch(struct terminal *term, uint8_t final) break; /* final == 'm' */ case 'n': { - int resource = vt_param_get(term, 0, 2); /* Default is modifyFuncionKeys */ + int resource = vt_param_get(term, 0, 2); /* Default is modifyFunctionKeys */ switch (resource) { case 0: /* modifyKeyboard */ case 1: /* modifyCursorKeys */ diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 1fbca27a..5ef62045 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -960,7 +960,7 @@ Be careful; do not use single-letter keys that are also used in original text. But with e.g. OSC-8 URLs (the terminal version of HTML anchors, - i.e. "links"), the text on the screen can be something completey + i.e. "links"), the text on the screen can be something completely different than the URL. This action toggles between showing and hiding the URL on the jump diff --git a/grid.c b/grid.c index 7b86e2c1..e1c4d28b 100644 --- a/grid.c +++ b/grid.c @@ -793,7 +793,7 @@ grid_resize_and_reflow( /* * Set end-coordinate for this chunk, by finding the next - * point-of-interrest on this row. + * 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, diff --git a/tests/test-config.c b/tests/test-config.c index 4f8b0c3b..6b44e9c2 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -895,7 +895,7 @@ enum collision_test_mode { FAIL_DIFFERENT_ACTION, FAIL_DIFFERENT_ARGV, FAIL_MOUSE_OVERRIDE, - SUCCED_SAME_ACTION_AND_ARGV, + SUCCEED_SAME_ACTION_AND_ARGV, }; static void @@ -949,7 +949,7 @@ _test_binding_collisions(struct context *ctx, break; case FAIL_DIFFERENT_ARGV: - case SUCCED_SAME_ACTION_AND_ARGV: + case SUCCEED_SAME_ACTION_AND_ARGV: bindings.arr[0].aux.type = BINDING_AUX_PIPE; bindings.arr[0].aux.master_copy = true; bindings.arr[0].aux.pipe.args = xcalloc( @@ -965,13 +965,13 @@ _test_binding_collisions(struct context *ctx, bindings.arr[1].aux.pipe.args[0] = xstrdup("/usr/bin/foobar"); bindings.arr[1].aux.pipe.args[1] = xstrdup("hello"); - if (test_mode == SUCCED_SAME_ACTION_AND_ARGV) + if (test_mode == SUCCEED_SAME_ACTION_AND_ARGV) bindings.arr[1].aux.pipe.args[2] = xstrdup("world"); break; } bool expected_result = - test_mode == SUCCED_SAME_ACTION_AND_ARGV ? true : false; + test_mode == SUCCEED_SAME_ACTION_AND_ARGV ? true : false; if (resolve_key_binding_collisions( ctx->conf, ctx->section, map, &bindings, type) != expected_result) @@ -1004,7 +1004,7 @@ test_binding_collisions(struct context *ctx, { _test_binding_collisions(ctx, max_action, map, type, FAIL_DIFFERENT_ACTION); _test_binding_collisions(ctx, max_action, map, type, FAIL_DIFFERENT_ARGV); - _test_binding_collisions(ctx, max_action, map, type, SUCCED_SAME_ACTION_AND_ARGV); + _test_binding_collisions(ctx, max_action, map, type, SUCCEED_SAME_ACTION_AND_ARGV); if (type == MOUSE_BINDING) { _test_binding_collisions( diff --git a/themes/onedark b/themes/onedark index d7f78a66..ac5cc834 100644 --- a/themes/onedark +++ b/themes/onedark @@ -1,5 +1,5 @@ # OneDark -# Pallete based on the same theme from https://github.com/dexpota/kitty-themes +# Palette based on the same theme from https://github.com/dexpota/kitty-themes [cursor] color=111111 cccccc From 3215d54f31c9e339598a34c9fa43f3dc3c5d1e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 27 Mar 2023 16:56:10 +0200 Subject: [PATCH 0267/1323] input: (kitty kbd): the resulting UTF-8 string may translate to multiple UTF-32 codepoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When this happened (for example, by specifying a custom compose sequence), the kitty keyboard protocol didn’t emit any text at all. This was caused by the utf32 codepoint being -1. This in turned was caused by us trying to convert the utf8 sequence to a *single* utf32 codepoint. This patch replaces the use of mbrtoc32() with a call to ambstoc32(), and the utf32 codepoint with an utf32 string. The kitty keyboard protocol is updated: * When determining if we’re dealing with text, check *all* codepoints in the utf32 string. * Add support for multiple codepoints when reporting "associated text". The first codepoint is the actual parameter in the emitted sequence, and the remaining codepoints are sub-parameters. I.e. the codepoints are colon separated. Closes #1288 --- CHANGELOG.md | 3 +++ input.c | 41 +++++++++++++++++++++++++++++------------ 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01d664fc..a02b9346 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -134,6 +134,8 @@ * `xdg\_toplevel::set_min_size()` not being called. * Key bindings with consumed modifiers masking other key bindings ([#1280][1280]). +* Multi-character compose sequences with the kitty keyboard protocol + ([#1288][1288]). [1173]: https://codeberg.org/dnkl/foot/issues/1173 [1190]: https://codeberg.org/dnkl/foot/issues/1190 @@ -144,6 +146,7 @@ [1256]: https://codeberg.org/dnkl/foot/issues/1256 [1249]: https://codeberg.org/dnkl/foot/issues/1249 [1280]: https://codeberg.org/dnkl/foot/issues/1280 +[1288]: https://codeberg.org/dnkl/foot/issues/1288 ### Security diff --git a/input.c b/input.c index 0a3773bc..b7f25670 100644 --- a/input.c +++ b/input.c @@ -898,7 +898,7 @@ struct kbd_ctx { const uint8_t *buf; size_t count; } utf8; - uint32_t utf32; + uint32_t *utf32; enum xkb_compose_status compose_status; enum wl_keyboard_key_state key_state; @@ -1121,12 +1121,18 @@ kitty_kbd_protocol(struct seat *seat, struct terminal *term, (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 uint32_t *utf32 = ctx->utf32; const uint8_t *const utf8 = ctx->utf8.buf; - - const bool is_text = iswprint(utf32) && (effective & ~caps_num) == 0; const size_t count = ctx->utf8.count; + bool is_text = utf32 != NULL && (effective & ~caps_num) == 0; + for (size_t i = 0; utf32[i] != U'\0'; i++) { + if (!iswprint(utf32[i])) { + is_text = false; + break; + } + } + const bool report_associated_text = (flags & KITTY_KBD_REPORT_ASSOCIATED) && is_text && !released; @@ -1245,7 +1251,7 @@ emit_escapes: : sym; if (composed) - key = utf32; + key = utf32[0]; /* TODO: what if there are multiple codepoints? */ else { key = xkb_keysym_to_utf32(sym_to_use); if (key == 0) @@ -1284,7 +1290,7 @@ emit_escapes: } else event[0] = '\0'; - char buf[64], *p = buf; + char buf[128], *p = buf; size_t left = sizeof(buf); size_t bytes; @@ -1316,8 +1322,16 @@ emit_escapes: } if (report_associated_text) { - bytes = snprintf(p, left, "%s;%u", !emit_mods ? ";" : "", utf32); + bytes = snprintf(p, left, "%s;%u", !emit_mods ? ";" : "", utf32[0]); p += bytes; left -= bytes; + + /* Additional text codepoints */ + if (utf32[0] != U'\0') { + for (size_t i = 1; utf32[i] != U'\0'; i++) { + bytes = snprintf(p, left, ":%u", utf32[i]); + p += bytes; left -= bytes; + } + } } bytes = snprintf(p, left, "%c", final); @@ -1514,19 +1528,20 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, * and use a malloc:ed buffer when necessary */ uint8_t buf[32]; uint8_t *utf8 = count < sizeof(buf) ? buf : xmalloc(count + 1); - uint32_t utf32 = (uint32_t)-1; + uint32_t *utf32 = NULL; if (composed) { xkb_compose_state_get_utf8( seat->kbd.xkb_compose_state, (char *)utf8, count + 1); - char32_t wc; - if (mbrtoc32(&wc, (const char *)utf8, count, &(mbstate_t){0}) == count) - utf32 = wc; + if (count > 0) + utf32 = ambstoc32((const char *)utf8); } else { xkb_state_key_get_utf8( seat->kbd.xkb_state, key, (char *)utf8, count + 1); - utf32 = xkb_state_key_get_utf32(seat->kbd.xkb_state, key); + + utf32 = xcalloc(2, sizeof(utf32[0])); + utf32[0] = xkb_state_key_get_utf32(seat->kbd.xkb_state, key); } struct kbd_ctx ctx = { @@ -1563,6 +1578,8 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, selection_cancel(term); } + free(utf32); + maybe_repeat: clock_gettime( term->wl->presentation_clock_id, &term->render.input_time); From 7bc22862fa9536cb2f18b030209a95306e57360d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 28 Mar 2023 18:31:24 +0200 Subject: [PATCH 0268/1323] render: protect against integer underflow when calculating scroll area MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When applying scroll damage, we calculate the affected region’s height (in pixels), by subtracting the number of rows to scroll, from the scrolling region, and finally multiply by the cell height. If the number of rows to scroll is very large, the subtraction may underflow, resulting in a very large height value instead of a negative one. This caused the check for "scrolling too many lines" to fail. That in turn resulted in an integer overflow when calculating the source offset into the rendered surface buffer, which typically triggered a segfault. This bug happened when there was continuous output in the terminal without any new frames being rendered. This caused a buildup of scroll damage, that triggered the underflow+overflow when we finally did render a new frame. For example, a compositor that doesn’t send any frame callbacks (for example because the terminal window is minimized, or on a different workspace/tag) would cause this. Closes #1305 --- CHANGELOG.md | 3 +++ render.c | 22 ++++++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a02b9346..df098c41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -136,6 +136,8 @@ ([#1280][1280]). * Multi-character compose sequences with the kitty keyboard protocol ([#1288][1288]). +* Crash when application output scrolls very fast, e.g. `yes` + ([#1305][1305]). [1173]: https://codeberg.org/dnkl/foot/issues/1173 [1190]: https://codeberg.org/dnkl/foot/issues/1190 @@ -147,6 +149,7 @@ [1249]: https://codeberg.org/dnkl/foot/issues/1249 [1280]: https://codeberg.org/dnkl/foot/issues/1280 [1288]: https://codeberg.org/dnkl/foot/issues/1288 +[1305]: https://codeberg.org/dnkl/foot/issues/1305 ### Security diff --git a/render.c b/render.c index 2aba3237..f16898b4 100644 --- a/render.c +++ b/render.c @@ -929,14 +929,19 @@ static void grid_render_scroll(struct terminal *term, struct buffer *buf, const struct damage *dmg) { - int height = (dmg->region.end - dmg->region.start - dmg->lines) * term->cell_height; - LOG_DBG( "damage: SCROLL: %d-%d by %d lines", dmg->region.start, dmg->region.end, dmg->lines); - if (height <= 0) + const int region_size = dmg->region.end - dmg->region.start; + + if (dmg->lines >= region_size) { + /* The entire scroll region will be scrolled out (i.e. replaced) */ return; + } + + const int height = (region_size - dmg->lines) * term->cell_height; + xassert(height > 0); #if TIME_SCROLL_DAMAGE struct timespec start_time; @@ -1037,14 +1042,19 @@ static void grid_render_scroll_reverse(struct terminal *term, struct buffer *buf, const struct damage *dmg) { - int height = (dmg->region.end - dmg->region.start - dmg->lines) * term->cell_height; - LOG_DBG( "damage: SCROLL REVERSE: %d-%d by %d lines", dmg->region.start, dmg->region.end, dmg->lines); - if (height <= 0) + const int region_size = dmg->region.end - dmg->region.start; + + if (dmg->lines >= region_size) { + /* The entire scroll region will be scrolled out (i.e. replaced) */ return; + } + + const int height = (region_size - dmg->lines) * term->cell_height; + xassert(height > 0); #if TIME_SCROLL_DAMAGE struct timespec start_time; From 981e4b77cb96ab277ea3b7a1e8aebf0e4589c032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 28 Mar 2023 18:37:41 +0200 Subject: [PATCH 0269/1323] term: protect against integer overflow when accumulating scroll damage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When accumulating scroll damage, we check if the last scroll damage’s scrolling region, and type, matches the new/current scroll damage. If so, the number of lines in the last scroll damage is increased, instead of adding a new scroll damage instance to the list. If the scroll damage list isn’t consumed, this build up of scroll damage would eventually overflow. And, even if it didn’t overflow, it could become large enough, that when later used to calculate e.g. the affected surface area, while rendering a frame, would cause an overflow there instead. This patch fixes both issues by: a) do an overflow check before increasing the line count b) limit the line count to UINT16_MAX --- CHANGELOG.md | 1 + terminal.c | 17 +++++++++++------ terminal.h | 2 +- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df098c41..30a34f01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -138,6 +138,7 @@ ([#1288][1288]). * Crash when application output scrolls very fast, e.g. `yes` ([#1305][1305]). +* Crash when application scrolls **many** lines (> ~2³¹). [1173]: https://codeberg.org/dnkl/foot/issues/1173 [1190]: https://codeberg.org/dnkl/foot/issues/1190 diff --git a/terminal.c b/terminal.c index 78ce4bc3..04153513 100644 --- a/terminal.c +++ b/terminal.c @@ -2252,15 +2252,20 @@ void term_damage_scroll(struct terminal *term, enum damage_type damage_type, struct scroll_region region, int lines) { - if (tll_length(term->grid->scroll_damage) > 0) { + if (likely(tll_length(term->grid->scroll_damage) > 0)) { struct damage *dmg = &tll_back(term->grid->scroll_damage); - if (dmg->type == damage_type && - dmg->region.start == region.start && - dmg->region.end == region.end) + if (likely( + dmg->type == damage_type && + dmg->region.start == region.start && + dmg->region.end == region.end)) { - dmg->lines += lines; - return; + /* Make sure we don’t overflow... */ + int new_line_count = (int)dmg->lines + lines; + if (likely(new_line_count <= UINT16_MAX)) { + dmg->lines = new_line_count; + return; + } } } struct damage dmg = { diff --git a/terminal.h b/terminal.h index 21236797..d2762a5a 100644 --- a/terminal.h +++ b/terminal.h @@ -96,7 +96,7 @@ enum damage_type {DAMAGE_SCROLL, DAMAGE_SCROLL_REVERSE, struct damage { enum damage_type type; struct scroll_region region; - int lines; + uint16_t lines; }; struct row_uri_range { From 27c52fb4e38278c381a8d2e58f239cec57249a33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 31 Mar 2023 10:30:58 +0200 Subject: [PATCH 0270/1323] test: config: call FcIni() + FcFini() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some of the config options we’re testing result in calls to FontConfig APIs. Without calling FcIni()+FcFini(), we leak memory: Direct leak of 768 byte(s) in 3 object(s) allocated from: #0 0x7f7e95cbfa89 in __interceptor_malloc /build/gcc/src/gcc/libsanitizer/asan/asan_malloc_linux.cpp:69 #1 0x7f7e95bd1fe5 (/usr/lib/libfontconfig.so.1+0x20fe5) Indirect leak of 96 byte(s) in 3 object(s) allocated from: #0 0x7f7e95cbf411 in __interceptor_calloc /build/gcc/src/gcc/libsanitizer/asan/asan_malloc_linux.cpp:77 #1 0x7f7e95bd63fd (/usr/lib/libfontconfig.so.1+0x253fd) Indirect leak of 19 byte(s) in 2 object(s) allocated from: #0 0x7f7e95c72faa in __interceptor_strdup /build/gcc/src/gcc/libsanitizer/asan/asan_interceptors.cpp:439 #1 0x7f7e95bd1898 in FcValueSave (/usr/lib/libfontconfig.so.1+0x20898) --- tests/test-config.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test-config.c b/tests/test-config.c index 6b44e9c2..4736a46b 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -1304,6 +1304,7 @@ test_section_tweak(void) int main(int argc, const char *const *argv) { + FcInit(); log_init(LOG_COLORIZE_AUTO, false, 0, LOG_CLASS_ERROR); test_section_main(); test_section_bell(); @@ -1325,5 +1326,6 @@ main(int argc, const char *const *argv) test_section_environment(); test_section_tweak(); log_deinit(); + FcFini(); return 0; } From a5dd00362735693faef2c11e7349a42a1a3e3593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 31 Mar 2023 10:41:17 +0200 Subject: [PATCH 0271/1323] changelog: remove trailing back-tick --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30a34f01..bea5c363 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -123,7 +123,7 @@ with a user-set line-height ([#1218][1218]). * Scaling factor not being correctly applied when converting pt-or-px config values (e.g. letter offsets, line height etc). -* Selection being stuck visually when `IL` and `DL`.` +* Selection being stuck visually when `IL` and `DL`. * URL underlines sometimes still being visible after exiting URL mode. * Text-bindings, and pipe-* bindings, with multiple key mappings causing a crash (double-free) on exit ([#1259][1259]). From 03b23ed6e5bd3b9bd0735d577d9ad722e1e40ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 31 Mar 2023 10:42:50 +0200 Subject: [PATCH 0272/1323] changeloge: remove bad escape char --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bea5c363..5fd3acf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -131,7 +131,7 @@ into the double-width glyph ([#1256][1256]). * Wayland protocol violation when ack:ing a configure event for an unmapped surface ([#1249][1249]). -* `xdg\_toplevel::set_min_size()` not being called. +* `xdg_toplevel::set_min_size()` not being called. * Key bindings with consumed modifiers masking other key bindings ([#1280][1280]). * Multi-character compose sequences with the kitty keyboard protocol From deb43c8dc3930954537e8294805aaa71c199d7a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 31 Mar 2023 10:43:39 +0200 Subject: [PATCH 0273/1323] changelog: typo: now -> not --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fd3acf1..4f1da5c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,7 +104,7 @@ that does not allow Wayland buffer re-use (e.g. KDE/plasma) ([#1173][1173]) * Scrollback search matches not being highlighted correctly, on - compositors that does now allow Wayland buffer re-use + compositors that does not allow Wayland buffer re-use (e.g. KDE/plasma). * Nanosecs "overflow" when calculating timeout value for `resize-delay-ms` option. From e71e7f5cf6452e9f14dd93a72ed9afca98ea13b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 31 Mar 2023 11:34:04 +0200 Subject: [PATCH 0274/1323] =?UTF-8?q?input:=20kitty:=20don=E2=80=99t=20tre?= =?UTF-8?q?at=20zero-length=20utf8/utf32=20strings=20as=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a regression introduced in 3215d54f31c9e339598a34c9fa43f3dc3c5d1e42 Symptoms: e.g. arrow keys not working in vim/neovim --- input.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/input.c b/input.c index b7f25670..7e5d204d 100644 --- a/input.c +++ b/input.c @@ -1125,7 +1125,7 @@ kitty_kbd_protocol(struct seat *seat, struct terminal *term, const uint8_t *const utf8 = ctx->utf8.buf; const size_t count = ctx->utf8.count; - bool is_text = utf32 != NULL && (effective & ~caps_num) == 0; + bool is_text = count > 0 && utf32 != NULL && (effective & ~caps_num) == 0; for (size_t i = 0; utf32[i] != U'\0'; i++) { if (!iswprint(utf32[i])) { is_text = false; From 0bc934070c01474e1196ee1964f59cc1c74040b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 31 Mar 2023 13:02:41 +0200 Subject: [PATCH 0275/1323] ci (woodpecker): do a second release build, using clang instead of gcc --- .woodpecker.yml | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 284da761..06631f89 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -33,7 +33,7 @@ pipeline: image: alpine:latest commands: - apk update - - apk add musl-dev linux-headers meson ninja gcc scdoc ncurses + - 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 @@ -50,7 +50,7 @@ pipeline: - ./footclient --version - cd ../.. - # Release + # 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 ../.. @@ -60,6 +60,16 @@ pipeline: - ./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 @@ -80,7 +90,7 @@ pipeline: image: i386/alpine:latest commands: - apk update - - apk add musl-dev linux-headers meson ninja gcc scdoc ncurses + - 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 @@ -97,7 +107,7 @@ pipeline: - ./footclient --version - cd ../.. - # Release + # 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 ../.. @@ -106,3 +116,13 @@ pipeline: - ./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 ../.. From f114068a468f6d6d39603b4ed26049de7b701842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 19 Jan 2023 19:52:57 +0100 Subject: [PATCH 0276/1323] csi: DECCOLM+DECSCLM: remove all support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We don’t support neither 132 column mode, nor smooth scrolling. Thus it makes little sense to recognize these control condes. Note that while XTerm does support 132 columns, it is disabled by default. In this mode, XTerm also doesn’t trigger the side-effects (i.e. clearing the screen). Closes #1265 --- CHANGELOG.md | 2 ++ csi.c | 21 --------------------- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f1da5c1..d50eff6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -139,6 +139,7 @@ * Crash when application output scrolls very fast, e.g. `yes` ([#1305][1305]). * Crash when application scrolls **many** lines (> ~2³¹). +* DECCOLM erasing the screen ([#1265][1265]). [1173]: https://codeberg.org/dnkl/foot/issues/1173 [1190]: https://codeberg.org/dnkl/foot/issues/1190 @@ -151,6 +152,7 @@ [1280]: https://codeberg.org/dnkl/foot/issues/1280 [1288]: https://codeberg.org/dnkl/foot/issues/1288 [1305]: https://codeberg.org/dnkl/foot/issues/1305 +[1265]: https://codeberg.org/dnkl/foot/issues/1265 ### Security diff --git a/csi.c b/csi.c index cef48d43..d22c1da5 100644 --- a/csi.c +++ b/csi.c @@ -276,21 +276,6 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) enable ? CURSOR_KEYS_APPLICATION : CURSOR_KEYS_NORMAL; break; - case 3: - /* DECCOLM */ - if (enable) - LOG_WARN("unimplemented: 132 column mode (DECCOLM)"); - - term_erase(term, 0, 0, term->rows - 1, term->cols - 1); - term_cursor_home(term); - break; - - case 4: - /* DECSCLM - Smooth scroll */ - if (enable) - LOG_WARN("unimplemented: Smooth (Slow) Scroll (DECSCLM)"); - break; - case 5: /* DECSCNM */ term->reverse = enable; @@ -558,8 +543,6 @@ decrqm(const struct terminal *term, unsigned param) { switch (param) { case 1: return decrpm(term->cursor_keys_mode == CURSOR_KEYS_APPLICATION); - case 3: return DECRPM_PERMANENTLY_RESET; - case 4: return DECRPM_PERMANENTLY_RESET; case 5: return decrpm(term->reverse); case 6: return decrpm(term->origin); case 7: return decrpm(term->auto_margin); @@ -601,8 +584,6 @@ xtsave(struct terminal *term, unsigned param) { switch (param) { case 1: term->xtsave.application_cursor_keys = term->cursor_keys_mode == CURSOR_KEYS_APPLICATION; break; - case 3: break; - case 4: break; case 5: term->xtsave.reverse = term->reverse; break; case 6: term->xtsave.origin = term->origin; break; case 7: term->xtsave.auto_margin = term->auto_margin; break; @@ -644,8 +625,6 @@ xtrestore(struct terminal *term, unsigned param) bool enable; switch (param) { case 1: enable = term->xtsave.application_cursor_keys; break; - case 3: return; - case 4: return; case 5: enable = term->xtsave.reverse; break; case 6: enable = term->xtsave.origin; break; case 7: enable = term->xtsave.auto_margin; break; From ae81f5af4caaa8bbad666e028f6fde9c23e58b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 31 Mar 2023 10:36:43 +0200 Subject: [PATCH 0277/1323] terminfo: remove DECRST of DECCOLM+DECSCLM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We’ve never supported neither 132-column mode, nor smooth scrolling. But we _did_ recognize the escape sequences. We don’t, anymore. Thus it makes very little sense to include these escapes in any of our terminfo capabilities. So, remove them. --- CHANGELOG.md | 1 + foot.info | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d50eff6b..8517d163 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,7 @@ * When determining initial font size, do FontConfig config substitution if the user-provided font pattern has no {pixel}size option ([#1287][1287]). +* DECRST of DECCOLM and DECSCLM removed from terminfo. [1166]: https://codeberg.org/dnkl/foot/issues/1166 [1179]: https://codeberg.org/dnkl/foot/issues/1179 diff --git a/foot.info b/foot.info index 34b05f23..cf81d721 100644 --- a/foot.info +++ b/foot.info @@ -86,7 +86,7 @@ indn=\E[%p1%dS, initc=\E]4;%p1%d;rgb\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\, invis=\E[8m, - is2=\E[!p\E[?3;4l\E[4l\E>, + is2=\E[!p\E[4l\E>, kDC3=\E[3;3~, kDC4=\E[3;4~, kDC5=\E[3;5~, @@ -244,7 +244,7 @@ rmul=\E[24m, rmxx=\E[29m, rs1=\Ec, - rs2=\E[!p\E[?3;4l\E[4l\E>, + rs2=\E[!p\E[4l\E>, rv=\E\\[[0-9]+;[0-9]+;[0-9]+c, sc=\E7, setrgbb=\E[48\:2\:\:%p1%d\:%p2%d\:%p3%dm, From 862a003b5b68a08d7ddfc653c3b36aaa21839d6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 3 Apr 2023 18:52:22 +0200 Subject: [PATCH 0278/1323] changelog: prepare for 1.14.0 --- CHANGELOG.md | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8517d163..846735d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.14.0](#1-14-0) * [1.13.1](#1-13-1) * [1.13.0](#1-13-0) * [1.12.1](#1-12-1) @@ -41,7 +41,7 @@ * [1.2.0](#1-2-0) -## Unreleased +## 1.14.0 ### Added @@ -94,8 +94,6 @@ [1287]: https://codeberg.org/dnkl/foot/issues/1287 -### Deprecated -### Removed ### Fixed * Crash in `foot --server` on key press, after another `footclient` @@ -156,10 +154,25 @@ [1265]: https://codeberg.org/dnkl/foot/issues/1265 -### Security ### Contributors +* Alexey Sakovets +* Andrea Pappacoda +* Antoine Beaupré +* argosatcore * Craig Barnes +* EuCaue +* Grigory Kirillov +* Harri Nieminen +* Hugo Osvaldo Barrera +* jaroeichler +* Joakim Nohlgård +* Nick Hastings +* Soren A D +* Torsten Trautwein +* Vladimír Magyar +* woojiq +* Yorick Peterse ## 1.13.1 From ae6bbce6c2527db40177585cc3c3d797d68c104a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 3 Apr 2023 18:52:42 +0200 Subject: [PATCH 0279/1323] meson: bump version to 1.14.0 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 68c3bf19..6e7e7fcf 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.13.1', + version: '1.14.0', license: 'MIT', meson_version: '>=0.58.0', default_options: [ From a858934c042a364110c9e663871722acdfc48aa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 3 Apr 2023 18:57:50 +0200 Subject: [PATCH 0280/1323] =?UTF-8?q?changelog:=20add=20a=20new=20?= =?UTF-8?q?=E2=80=98unreleased=E2=80=99=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 846735d5..74be88cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.14.0](#1-14-0) * [1.13.1](#1-13-1) * [1.13.0](#1-13-0) @@ -41,6 +42,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.14.0 ### Added From 479b3c8ee1816ac8019c202398de9fc93bf109ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 5 Apr 2023 14:39:02 +0200 Subject: [PATCH 0281/1323] *.desktop: add StartupWMClass=foot At least Gnome needs this in order to link running instances of foot to their corresponding .desktop file, used e.g. when determining which icon to display for running applications. Closes #1317 --- CHANGELOG.md | 6 ++++++ org.codeberg.dnkl.foot-server.desktop | 1 + org.codeberg.dnkl.foot.desktop | 1 + org.codeberg.dnkl.footclient.desktop | 1 + 4 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74be88cd..8500195b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,12 @@ ### Deprecated ### Removed ### Fixed + +* Incorrect icon in dock and window switcher on Gnome ([#1317][1317]) + +[1317]: https://codeberg.org/dnkl/foot/issues/1317 + + ### Security ### Contributors diff --git a/org.codeberg.dnkl.foot-server.desktop b/org.codeberg.dnkl.foot-server.desktop index 6e8891c0..a40117c7 100644 --- a/org.codeberg.dnkl.foot-server.desktop +++ b/org.codeberg.dnkl.foot-server.desktop @@ -9,3 +9,4 @@ Keywords=shell;prompt;command;commandline; Name=Foot Server GenericName=Terminal Comment=A wayland native terminal emulator (server) +StartupWMClass=foot diff --git a/org.codeberg.dnkl.foot.desktop b/org.codeberg.dnkl.foot.desktop index f072568d..720d35a9 100644 --- a/org.codeberg.dnkl.foot.desktop +++ b/org.codeberg.dnkl.foot.desktop @@ -9,3 +9,4 @@ Keywords=shell;prompt;command;commandline; Name=Foot GenericName=Terminal Comment=A wayland native terminal emulator +StartupWMClass=foot diff --git a/org.codeberg.dnkl.footclient.desktop b/org.codeberg.dnkl.footclient.desktop index f82f282b..dc8bc5dc 100644 --- a/org.codeberg.dnkl.footclient.desktop +++ b/org.codeberg.dnkl.footclient.desktop @@ -9,3 +9,4 @@ Keywords=shell;prompt;command;commandline; Name=Foot Client GenericName=Terminal Comment=A wayland native terminal emulator (client) +StartupWMClass=foot From 98528da5e5d3715b2778b3a1dd643fdd6dc051e3 Mon Sep 17 00:00:00 2001 From: Vivian Szczepanski Date: Sat, 8 Apr 2023 10:58:27 -0400 Subject: [PATCH 0282/1323] meson: bump tllist dependency version to 1.1.0 The tll_sort() macro was added to tllist in version 1.1.0, and so building with a previous version causes a linking error. --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 6e7e7fcf..a6892cc4 100644 --- a/meson.build +++ b/meson.build @@ -110,7 +110,7 @@ if utf8proc.found() add_project_arguments('-DFOOT_GRAPHEME_CLUSTERING=1', language: 'c') endif -tllist = dependency('tllist', version: '>=1.0.4', fallback: 'tllist') +tllist = dependency('tllist', version: '>=1.1.0', fallback: 'tllist') fcft = dependency('fcft', version: ['>=3.0.1', '<4.0.0'], fallback: 'fcft') wayland_protocols_datadir = wayland_protocols.get_variable('pkgdatadir') From e2baa6523875aaa13b6b7db80b7985978b8ee492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 12 Apr 2023 16:39:54 +0200 Subject: [PATCH 0283/1323] =?UTF-8?q?render:=20ensure=20scroll=20region?= =?UTF-8?q?=E2=80=99s=20endpoint=20is=20valid=20after=20a=20window=20resiz?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If we had a non-empty bottom scroll region, and the window was resized to a smaller size, the scroll region was not reset correctly. This led to a crash when scrolling the screen content. Fix by making sure the scroll region’s endpoint is within range. --- CHANGELOG.md | 2 ++ render.c | 3 +-- terminal.c | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8500195b..ca98ed5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,8 @@ ### Fixed * Incorrect icon in dock and window switcher on Gnome ([#1317][1317]) +* Crash when scrolling after resizing the window with non-zero + scrolling regions. [1317]: https://codeberg.org/dnkl/foot/issues/1317 diff --git a/render.c b/render.c index f16898b4..d39c9489 100644 --- a/render.c +++ b/render.c @@ -4128,8 +4128,7 @@ maybe_resize(struct terminal *term, int width, int height, bool force) if (term->scroll_region.start >= term->rows) term->scroll_region.start = 0; - - if (term->scroll_region.end >= old_rows) + if (term->scroll_region.end > term->rows) term->scroll_region.end = term->rows; term->render.last_cursor.row = NULL; diff --git a/terminal.c b/terminal.c index 04153513..2e62fbb7 100644 --- a/terminal.c +++ b/terminal.c @@ -2716,13 +2716,13 @@ term_scroll_partial(struct terminal *term, struct scroll_region region, int rows erase_line(term, row); } - term_damage_scroll(term, DAMAGE_SCROLL, region, rows); - term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row); - #if defined(_DEBUG) for (int r = 0; r < term->rows; r++) xassert(grid_row(term->grid, r) != NULL); #endif + + term_damage_scroll(term, DAMAGE_SCROLL, region, rows); + term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row); } void From a2db3cdd5b3b6ce0b782b5ee51d174f4b4890f26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 12 Apr 2023 18:09:41 +0200 Subject: [PATCH 0284/1323] render: regression: keep empty bottom scroll margin empty after resize --- render.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/render.c b/render.c index d39c9489..521c8b7f 100644 --- a/render.c +++ b/render.c @@ -4128,8 +4128,11 @@ maybe_resize(struct terminal *term, int width, int height, bool force) if (term->scroll_region.start >= term->rows) term->scroll_region.start = 0; - if (term->scroll_region.end > term->rows) + if (term->scroll_region.end > term->rows || + term->scroll_region.end >= old_rows) + { term->scroll_region.end = term->rows; + } term->render.last_cursor.row = NULL; From dc7642f2a52076aacc04b72e1cb213ac96649d50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 26 Apr 2023 18:30:09 +0200 Subject: [PATCH 0285/1323] csi: implement "CSI ? m" --- CHANGELOG.md | 4 ++++ csi.c | 35 ++++++++++++++++++++++++++++++++++- doc/foot-ctlseqs.7.scd | 4 ++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca98ed5c..0d16a6ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,10 @@ ## Unreleased ### Added + +* VT: implemented `XTQMODKEYS` query (`CSI ? Pp m`). + + ### Changed ### Deprecated ### Removed diff --git a/csi.c b/csi.c index d22c1da5..7b318d0a 100644 --- a/csi.c +++ b/csi.c @@ -1398,6 +1398,39 @@ csi_dispatch(struct terminal *term, uint8_t final) break; } + case 'm': { + int resource = vt_param_get(term, 0, 0); + int value = -1; + + switch (resource) { + case 0: /* modifyKeyboard */ + value = 0; + break; + + case 1: /* modifyCursorKeys */ + case 2: /* modifyFunctionKeys */ + value = 1; + break; + + case 4: /* modifyOtherKeys */ + value = term->modify_other_keys_2 ? 2 : 1; + break; + + default: + LOG_WARN("XTQMODKEYS: invalid resource '%d' in '%s'", + resource, csi_as_string(term, final, -1)); + break; + } + + if (value >= 0) { + char reply[16] = {0}; + int chars = snprintf(reply, sizeof(reply), + "\033[>%d;%dm", resource, value); + term_to_slave(term, reply, chars); + } + break; + } + case 'u': { enum kitty_kbd_flags flags = term->grid->kitty_kbd.flags[term->grid->kitty_kbd.idx]; @@ -1489,7 +1522,7 @@ csi_dispatch(struct terminal *term, uint8_t final) break; default: - LOG_WARN("invalid resource %d in %s", + LOG_WARN("XTMODKEYS: invalid resource '%d' in '%s'", resource, csi_as_string(term, final, -1)); break; } diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index ec970127..3fa03158 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -565,6 +565,10 @@ manipulation sequences. The generic format is: : xterm : Set level of the _modifyOtherKeys_ property to _Pv_. Note that foot only supports level 1 and 2, where level 1 is the default setting. +| \\E[ ? _Pp_ m +: XTQMODKEYS +: xterm +: Query key modifier options | \\E[ > 4 n : : xterm From c13495e26ef7c239b330dccf1afef44430b15543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 26 Apr 2023 18:28:07 +0200 Subject: [PATCH 0286/1323] kitty: F3 is no longer allowed to emit CSI R The original kitty keyboard specification allowed F3 to emit either CSI R, or CSI 13~. Support for CSI R was removed in later revisions of the protocol, since it collides with "Cursor Position Report" sequences. --- CHANGELOG.md | 7 +++++++ kitty-keymap.h | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d16a6ed..d64392a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,13 @@ ### Changed + +* 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”_. + + ### Deprecated ### Removed ### Fixed diff --git a/kitty-keymap.h b/kitty-keymap.h index eba4923a..ae911c4f 100644 --- a/kitty-keymap.h +++ b/kitty-keymap.h @@ -70,7 +70,7 @@ static const struct kitty_key_data kitty_keymap[] = { {XKB_KEY_F1, 1, 'P', false}, {XKB_KEY_F2, 1, 'Q', false}, - {XKB_KEY_F3, 1, 'R', false}, + {XKB_KEY_F3, 13, '~', false}, {XKB_KEY_F4, 1, 'S', false}, {XKB_KEY_F5, 15, '~', false}, {XKB_KEY_F6, 17, '~', false}, From 7eea69df8928ff0f0ea481ff62d8a9166151509f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 12 May 2023 09:42:35 +0200 Subject: [PATCH 0287/1323] term: reset: switch modifyOtherKeys back to level 1 --- CHANGELOG.md | 1 + terminal.c | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d64392a2..5b2625fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ * Incorrect icon in dock and window switcher on Gnome ([#1317][1317]) * Crash when scrolling after resizing the window with non-zero scrolling regions. +* `XTMODKEYS` state not being reset on a terminal reset. [1317]: https://codeberg.org/dnkl/foot/issues/1317 diff --git a/terminal.c b/terminal.c index 2e62fbb7..39d9b406 100644 --- a/terminal.c +++ b/terminal.c @@ -1933,6 +1933,7 @@ term_reset(struct terminal *term, bool hard) term_set_user_mouse_cursor(term, NULL); + term->modify_other_keys_2 = false; memset(term->normal.kitty_kbd.flags, 0, sizeof(term->normal.kitty_kbd.flags)); memset(term->alt.kitty_kbd.flags, 0, sizeof(term->alt.kitty_kbd.flags)); term->normal.kitty_kbd.idx = term->alt.kitty_kbd.idx = 0; From 3b41379be43a21a00a776d0d136c5d1b2fe4007e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 25 Apr 2023 21:33:45 +0200 Subject: [PATCH 0288/1323] quirks: sway does not damage surface beneath sub-surface, when unmapped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When unmapping a sub-surface, Sway <= 1.8 does not damage the surface beneath the sub-surface. https://github.com/swaywm/sway/issues/6960 The workaround is to manually damage the main surface. Previously, this was done when exiting scrollback search, and after the ‘flash’ OSC. But other sub-surfaces, that may also be unmapped, did not. This patch adds a quirk handler that does this, and calls it when: * Exiting scrollback search * Ending the ‘flash’ OSC * Exiting unicode input mode * Clearing URL labels * Removing the scrollback position indicator Closes #1335 --- quirks.c | 25 +++++++++++++++++++++++++ quirks.h | 2 ++ render.c | 11 ++++++++++- search.c | 6 +----- terminal.c | 5 ----- url-mode.c | 5 +++++ 6 files changed, 43 insertions(+), 11 deletions(-) diff --git a/quirks.c b/quirks.c index e4fe4a1f..bf9bc7fb 100644 --- a/quirks.c +++ b/quirks.c @@ -66,3 +66,28 @@ quirk_weston_csd_off(struct terminal *term) for (int i = 0; i < ALEN(term->window->csd.surface); i++) quirk_weston_subsurface_desync_off(term->window->csd.surface[i].sub); } + +static bool +is_sway(void) +{ + static bool is_sway = false; + static bool initialized = false; + + if (!initialized) { + initialized = true; + is_sway = getenv("SWAYSOCK") != NULL; + if (is_sway) + LOG_WARN("applying wl_surface_damage_buffer() workaround for Sway"); + } + + return is_sway; +} + +void +quirk_sway_subsurface_unmap(struct terminal *term) +{ + if (!is_sway()) + return; + + wl_surface_damage_buffer(term->window->surface, 0, 0, INT32_MAX, INT32_MAX); +} diff --git a/quirks.h b/quirks.h index e762bb3e..0e840667 100644 --- a/quirks.h +++ b/quirks.h @@ -21,3 +21,5 @@ void quirk_weston_subsurface_desync_off(struct wl_subsurface *sub); /* Shortcuts to call desync_{on,off} on all CSD subsurfaces */ void quirk_weston_csd_on(struct terminal *term); void quirk_weston_csd_off(struct terminal *term); + +void quirk_sway_subsurface_unmap(struct terminal *term); diff --git a/render.c b/render.c index 521c8b7f..f1e80392 100644 --- a/render.c +++ b/render.c @@ -1527,6 +1527,10 @@ render_overlay(struct terminal *term) wl_surface_commit(overlay->surf); term->render.last_overlay_style = OVERLAY_NONE; term->render.last_overlay_buf = NULL; + + /* Work around Sway bug - unmapping a sub-surface does not + * damage the underlying surface */ + quirk_sway_subsurface_unmap(term); } return; } @@ -2374,8 +2378,13 @@ render_scrollback_position(struct terminal *term) struct wl_window *win = term->window; if (term->grid->view == term->grid->offset) { - if (win->scrollback_indicator.surf != NULL) + if (win->scrollback_indicator.surf != NULL) { wayl_win_subsurface_destroy(&win->scrollback_indicator); + + /* Work around Sway bug - unmapping a sub-surface does not damage + * the underlying surface */ + quirk_sway_subsurface_unmap(term); + } return; } diff --git a/search.c b/search.c index 59765c2e..b4bec057 100644 --- a/search.c +++ b/search.c @@ -15,6 +15,7 @@ #include "input.h" #include "key-binding.h" #include "misc.h" +#include "quirks.h" #include "render.h" #include "selection.h" #include "shm.h" @@ -117,11 +118,6 @@ search_cancel_keep_selection(struct terminal *term) term_xcursor_update(term); render_refresh(term); - - /* Work around Sway bug - unmapping a sub-surface does not damage - * the underlying surface */ - term_damage_margins(term); - term_damage_view(term); } void diff --git a/terminal.c b/terminal.c index 39d9b406..ddc24cb3 100644 --- a/terminal.c +++ b/terminal.c @@ -406,11 +406,6 @@ fdm_flash(struct fdm *fdm, int fd, int events, void *data) term->flash.active = false; render_refresh(term); - - /* Work around Sway bug - unmapping a sub-surface does not damage - * the underlying surface */ - term_damage_margins(term); - term_damage_view(term); return true; } diff --git a/url-mode.c b/url-mode.c index 7d7ffd81..bd9b5157 100644 --- a/url-mode.c +++ b/url-mode.c @@ -14,6 +14,7 @@ #include "char32.h" #include "grid.h" #include "key-binding.h" +#include "quirks.h" #include "render.h" #include "selection.h" #include "spawn.h" @@ -859,6 +860,10 @@ urls_reset(struct terminal *term) tll_foreach(term->window->urls, it) { wayl_win_subsurface_destroy(&it->item.surf); tll_remove(term->window->urls, it); + + /* Work around Sway bug - unmapping a sub-surface does not + * damage the underlying surface */ + quirk_sway_subsurface_unmap(term); } } From 738deb236853ebf1e19843e536b9bda053b69bf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 15 May 2023 20:34:58 +0200 Subject: [PATCH 0289/1323] search: regression: refresh current view when canceling a scrollback search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3b41379be43a21a00a776d0d136c5d1b2fe4007e introduced a regression, where canceling a scrollback search didn’t refresh the viewport correctly; the viewport was changed, but the screen content was not refreshed. This worked before, because the workaround for https://github.com/swaywm/sway/issues/6960 always called term_damage_view() when exiting scrollback search mode. 3b41379be43a21a00a776d0d136c5d1b2fe4007e removed that call since it’s no longer required. *Except* when executing the BIND_ACTION_SEARCH_CANCEL binding, since then the viewport may be moved. Note that this regression affected *all* compositors, not just Sway. Closes #1354 --- search.c | 1 + 1 file changed, 1 insertion(+) diff --git a/search.c b/search.c index b4bec057..6c2a2a7e 100644 --- a/search.c +++ b/search.c @@ -829,6 +829,7 @@ execute_binding(struct seat *seat, struct terminal *term, grid->view = ensure_view_is_allocated( term, term->search.original_view); } + term_damage_view(term); search_cancel(term); return true; From d2f81443f167408318eeaf4e1c1eae6cb74e672e Mon Sep 17 00:00:00 2001 From: locture Date: Tue, 25 Apr 2023 03:43:36 +0000 Subject: [PATCH 0290/1323] customized gnome-like csd buttons --- render.c | 207 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 121 insertions(+), 86 deletions(-) diff --git a/render.c b/render.c index f1e80392..340e0378 100644 --- a/render.c +++ b/render.c @@ -2096,41 +2096,24 @@ render_csd_button_minimize(struct terminal *term, struct buffer *buf) pixman_color_t color = get_csd_button_fg_color(term->conf); pixman_image_t *src = pixman_image_create_solid_fill(&color); - const int max_height = buf->height / 2; - const int max_width = buf->width / 2; + const int max_height = buf->height / 3; + const int max_width = buf->width / 3; - int width = max_width; - int height = max_width / 2; + int width = min(max_height, max_width); + int thick = min(width / 2, 1 * term->scale); - if (height > max_height) { - height = max_height; - width = height * 2; - } + const int x_margin = (buf->width - width) / 2; + const int y_margin = (buf->height - width) / 2; - xassert(width <= max_width); - xassert(height <= max_height); + xassert(x_margin + width - thick >= 0); + xassert(width - 2 * thick >= 0); + xassert(y_margin + width - thick >= 0); + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &color, 1, + (pixman_rectangle16_t[]) { + {x_margin, y_margin + width - thick, width, thick} + }); - int x_margin = (buf->width - width) / 2.; - int y_margin = (buf->height - height) / 2.; - - pixman_triangle_t tri = { - .p1 = { - .x = pixman_int_to_fixed(x_margin), - .y = pixman_int_to_fixed(y_margin), - }, - .p2 = { - .x = pixman_int_to_fixed(x_margin + width), - .y = pixman_int_to_fixed(y_margin), - }, - .p3 = { - .x = pixman_int_to_fixed(buf->width / 2), - .y = pixman_int_to_fixed(y_margin + height), - }, - }; - - pixman_composite_triangles( - PIXMAN_OP_OVER, src, buf->pix[0], PIXMAN_a1, - 0, 0, 0, 0, 1, &tri); pixman_image_unref(src); } @@ -2147,6 +2130,38 @@ render_csd_button_maximize_maximized( int width = min(max_height, max_width); int thick = min(width / 2, 1 * term->scale); + const int x_margin = (buf->width - width) / 2; + const int y_margin = (buf->height - width) / 2; + const int shrink = 1; + xassert(x_margin + width - thick >= 0); + xassert(width - 2 * thick >= 0); + xassert(y_margin + width - thick >= 0); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &color, 4, + (pixman_rectangle16_t[]){ + {x_margin + shrink, y_margin + shrink, width - 2 * shrink, thick}, + { x_margin + shrink, y_margin + thick, thick, width - 2 * thick - shrink }, + { x_margin + width - thick - shrink, y_margin + thick, thick, width - 2 * thick - shrink }, + { x_margin + shrink, y_margin + width - thick - shrink, width - 2 * shrink, thick }}); + + pixman_image_unref(src); + +} + +static void +render_csd_button_maximize_window( + struct terminal *term, struct buffer *buf) +{ + pixman_color_t color = get_csd_button_fg_color(term->conf); + pixman_image_t *src = pixman_image_create_solid_fill(&color); + + const int max_height = buf->height / 3; + const int max_width = buf->width / 3; + + int width = min(max_height, max_width); + int thick = min(width / 2, 1 * term->scale); + const int x_margin = (buf->width - width) / 2; const int y_margin = (buf->height - width) / 2; @@ -2156,58 +2171,12 @@ render_csd_button_maximize_maximized( pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &color, 4, - (pixman_rectangle16_t[]){ + (pixman_rectangle16_t[]) { {x_margin, y_margin, width, thick}, - {x_margin, y_margin + thick, thick, width - 2 * thick}, - {x_margin + width - thick, y_margin + thick, thick, width - 2 * thick}, - {x_margin, y_margin + width - thick, width, thick}}); - - pixman_image_unref(src); - -} - -static void -render_csd_button_maximize_window( - struct terminal *term, struct buffer *buf) -{ - pixman_color_t color = get_csd_button_fg_color(term->conf); - pixman_image_t *src = pixman_image_create_solid_fill(&color); - - const int max_height = buf->height / 2; - const int max_width = buf->width / 2; - - int width = max_width; - int height = max_width / 2; - - if (height > max_height) { - height = max_height; - width = height * 2; - } - - xassert(width <= max_width); - xassert(height <= max_height); - - int x_margin = (buf->width - width) / 2.; - int y_margin = (buf->height - height) / 2.; - - pixman_triangle_t tri = { - .p1 = { - .x = pixman_int_to_fixed(buf->width / 2), - .y = pixman_int_to_fixed(y_margin), - }, - .p2 = { - .x = pixman_int_to_fixed(x_margin), - .y = pixman_int_to_fixed(y_margin + height), - }, - .p3 = { - .x = pixman_int_to_fixed(x_margin + width), - .y = pixman_int_to_fixed(y_margin + height), - }, - }; - - pixman_composite_triangles( - PIXMAN_OP_OVER, src, buf->pix[0], PIXMAN_a1, - 0, 0, 0, 0, 1, &tri); + { x_margin, y_margin + thick, thick, width - 2 * thick }, + { x_margin + width - thick, y_margin + thick, thick, width - 2 * thick }, + { x_margin, y_margin + width - thick, width, thick } + }); pixman_image_unref(src); } @@ -2231,13 +2200,79 @@ render_csd_button_close(struct terminal *term, struct buffer *buf) const int max_width = buf->width / 3; int width = min(max_height, max_width); - + int thick = min(width / 2, 1 * term->scale); const int x_margin = (buf->width - width) / 2; const int y_margin = (buf->height - width) / 2; - pixman_image_fill_rectangles( - PIXMAN_OP_SRC, buf->pix[0], &color, 1, - &(pixman_rectangle16_t){x_margin, y_margin, width, width}); + xassert(x_margin + width - thick >= 0); + xassert(width - 2 * thick >= 0); + xassert(y_margin + width - thick >= 0); + + pixman_triangle_t tri[4] = { + { + .p1 = { + .x = pixman_int_to_fixed(x_margin), + .y = pixman_int_to_fixed(y_margin + thick), + }, + .p2 = { + .x = pixman_int_to_fixed(x_margin + width - thick), + .y = pixman_int_to_fixed(y_margin + width), + }, + .p3 = { + .x = pixman_int_to_fixed(x_margin + thick), + .y = pixman_int_to_fixed(y_margin), + }, + }, + + { + .p1 = { + .x = pixman_int_to_fixed(x_margin + width), + .y = pixman_int_to_fixed(y_margin + width - thick), + }, + .p2 = { + .x = pixman_int_to_fixed(x_margin + thick), + .y = pixman_int_to_fixed(y_margin), + }, + .p3 = { + .x = pixman_int_to_fixed(x_margin + width - thick), + .y = pixman_int_to_fixed(y_margin + width), + }, + }, + + { + .p1 = { + .x = pixman_int_to_fixed(x_margin), + .y = pixman_int_to_fixed(y_margin + width - thick), + }, + .p2 = { + .x = pixman_int_to_fixed(x_margin + width), + .y = pixman_int_to_fixed(y_margin + thick), + }, + .p3 = { + .x = pixman_int_to_fixed(x_margin + thick), + .y = pixman_int_to_fixed(y_margin + width), + }, + }, + + { + .p1 = { + .x = pixman_int_to_fixed(x_margin + width), + .y = pixman_int_to_fixed(y_margin + thick), + }, + .p2 = { + .x = pixman_int_to_fixed(x_margin), + .y = pixman_int_to_fixed(y_margin + width - thick), + }, + .p3 = { + .x = pixman_int_to_fixed(x_margin + width - thick), + .y = pixman_int_to_fixed(y_margin), + }, + }, + }; + + pixman_composite_triangles( + PIXMAN_OP_OVER, src, buf->pix[0], PIXMAN_a1, + 0, 0, 0, 0, 4, tri); pixman_image_unref(src); } From a2f765b72a6612758a034a5b0c24ec08625db1a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 12 May 2023 14:55:55 +0200 Subject: [PATCH 0291/1323] slave: unset TERM_PROGRAM{,_VERSION} MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Foot’s policy is to not set environment variables that identifies it (except the well-known and established `TERM` variable). We encourage applications to use terminfo to determine capabilities, or terminal queries, when available. Or, at least use terminal queries to detect the terminal and its version. Setting environment variables is a bad idea since they are inherited by all applications started by the terminal (which is the whole point). But, this includes other terminal emulators, making it very possible a terminal emulator gets mis-detected just because it was started from another terminal. Since there are a couple of terminal emulators that _do_ set TERM_PROGRAM and TERM_PROGRAM_VERSION, unset these environment variables to avoid being misdetected. Closes #1349 --- CHANGELOG.md | 2 +- doc/foot.1.scd | 11 ----------- doc/footclient.1.scd | 11 ----------- generate-version.sh | 1 - slave.c | 6 +++--- 5 files changed, 4 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b2625fb..e9912729 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,7 +93,7 @@ * “Report DA2” terminfo entries (`RV`/`rv`). * `XF` terminfo capability (focus in/out events available). * `$TERM_PROGRAM` and `$TERM_PROGRAM_VERSION` environment variables - set in the slave process. + unset in the slave process. [1136]: https://codeberg.org/dnkl/foot/issues/1136 [1225]: https://codeberg.org/dnkl/foot/issues/1225 diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 51c53130..6f63d4c8 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -546,17 +546,6 @@ In all other cases, the exit code is that of the client application This variable is set to *truecolor*, to indicate to client applications that 24-bit RGB colors are supported. -*TERM_PROGRAM* - Always set to *foot*. This can be used by client applications to - check which terminal is in use, but with the caveat that it may - have been inherited from a parent process in other terminals that - aren't known to set the variable. - -*TERM_PROGRAM_VERSION* - Set to the foot version string, in the format _major_*.*_minor_*.*_patch_ - or _major_*.*_minor_*.*_patch_*-*_revision_*-\g*_commit_ for inter-release - builds. The same caveat as for *TERM_PROGRAM* applies. - In addition to the variables listed above, custom environment variables may be defined in *foot.ini*(5). diff --git a/doc/footclient.1.scd b/doc/footclient.1.scd index 189d9e3c..63235134 100644 --- a/doc/footclient.1.scd +++ b/doc/footclient.1.scd @@ -158,17 +158,6 @@ terminfo entries manually, by copying *foot* and *foot-direct* to This variable is set to *truecolor*, to indicate to client applications that 24-bit RGB colors are supported. -*TERM_PROGRAM* - Always set to *foot*. This can be used by client applications to - check which terminal is in use, but with the caveat that it may - have been inherited from a parent process in other terminals that - aren't known to set the variable. - -*TERM_PROGRAM_VERSION* - Set to the foot version string, in the format _major_*.*_minor_*.*_patch_ - or _major_*.*_minor_*.*_patch_*-*_revision_*-\g*_commit_ for inter-release - builds. The same caveat as for *TERM_PROGRAM* applies. - In addition to the variables listed above, custom environment variables may be defined in *foot.ini*(5). diff --git a/generate-version.sh b/generate-version.sh index 3772008b..a030d512 100755 --- a/generate-version.sh +++ b/generate-version.sh @@ -41,7 +41,6 @@ patch=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\3/') extra=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+)(-([0-9]+-g[a-z0-9]+) .*)?.*/\5/') new_version="#define FOOT_VERSION \"${new_version}\" -#define FOOT_VERSION_SHORT \"${git_version:-${default_version}}\" #define FOOT_MAJOR ${major} #define FOOT_MINOR ${minor} #define FOOT_PATCH ${patch} diff --git a/slave.c b/slave.c index 2f23e996..ecfce7e6 100644 --- a/slave.c +++ b/slave.c @@ -21,7 +21,6 @@ #include "macros.h" #include "terminal.h" #include "tokenize.h" -#include "version.h" #include "xmalloc.h" extern char **environ; @@ -352,11 +351,12 @@ slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, } setenv("TERM", term_env, 1); - setenv("TERM_PROGRAM", "foot", 1); - setenv("TERM_PROGRAM_VERSION", FOOT_VERSION_SHORT, 1); setenv("COLORTERM", "truecolor", 1); setenv("PWD", cwd, 1); + unsetenv("TERM_PROGRAM"); + unsetenv("TERM_PROGRAM_VERSION"); + #if defined(FOOT_TERMINFO_PATH) setenv("TERMINFO", FOOT_TERMINFO_PATH, 1); #endif From e78319fccd2b8c057deff4b1937b325be1d35140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 17 May 2023 20:51:40 +0200 Subject: [PATCH 0292/1323] utmp: rewrite utmp logging This patch generalizes the utmp support, to not only support libutempter, but also ulog (and in the future, even more interfaces). * Rename config option main.utempter to main.utmp-helper * Add meson option -Dutmp-backend=none|libutempter|ulog|auto * Rename meson option -Ddefault-utempter-path to -Dutmp-default-helper-path * utmp is no longer detected at compile time, but at runtime instead. Meson will configure the following pre-processor macros, based on the selected utmp backend: * UTMP_ADD - argument to pass to utmp helper when adding a record (starting foot) * UTMP_DEL - argument to pass to utmp helper when removing a record (exiting foot) * UTMP_DEL_HAVE_ARGUMENT - if defined, UTMP_DEL expects an extra argument ($WAYLAND_DISPLAY) * UTMP_DEFAULT_HELPER_PATH - path to the default utmp helper binary The documentation has been updated to mention which arguments are passed to the helper binary. Closes #1314 --- CHANGELOG.md | 11 ++++++++ INSTALL.md | 26 ++++++++++--------- config.c | 41 +++++++++++++++++++++-------- config.h | 2 +- doc/foot.ini.5.scd | 21 ++++++++++++--- doc/meson.build | 18 ++++++++++--- foot.ini | 3 ++- meson.build | 63 +++++++++++++++++++++++++++++++-------------- meson_options.txt | 6 +++-- terminal.c | 24 ++++++++++++++--- tests/test-config.c | 2 +- 11 files changed, 159 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9912729..d3b4df58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,9 @@ ### Added * VT: implemented `XTQMODKEYS` query (`CSI ? Pp m`). +* Meson option `utmp-backend=none|libutempter|ulog|auto`. The default + is `auto`, which will select `libutempter` on Linux, `ulog` on + FreeBSD, and `none` for all others. ### Changed @@ -54,9 +57,17 @@ `CSI R`. The kitty keyboard protocol originally allowed F3 to be encoded as `CSI R`, but this was removed from the specification since `CSI R` conflicts with the _”Cursor Position Report”_. +* `[main].utempter` renamed to `[main].utmp-helper`. The old option + name is still recognized, but will log a deprecation warning. +* Meson option `default-utempter-path` renamed to + `utmp-default-helper-path`. ### Deprecated + +* `[main].utempter` option. + + ### Removed ### Fixed diff --git a/INSTALL.md b/INSTALL.md index 6cc51750..9e2da8ec 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -45,7 +45,8 @@ subprojects. * wayland (_client_ and _cursor_ libraries) * xkbcommon * utf8proc (_optional_, needed for grapheme clustering) -* libutempter (_optional_, needed for utmp logging) +* libutempter (_optional_, needed for utmp logging on Linux) +* ulog (_optional_, needed for utmp logging on FreeBSD) * [fcft](https://codeberg.org/dnkl/fcft) [^1] [^1]: can also be built as subprojects, in which case they are @@ -142,17 +143,18 @@ mkdir -p bld/release && cd bld/release Available compile-time options: -| Option | Type | Default | Description | Extra dependencies | -|--------------------------------------|---------|-------------------------|-----------------------------------------------------------|--------------------| -| `-Ddocs` | feature | `auto` | Builds and install documentation | scdoc | -| `-Dtests` | bool | `true` | Build tests (adds a `ninja test` build target) | none | -| `-Dime` | bool | `true` | Enables IME support | None | -| `-Dgrapheme-clustering` | feature | `auto` | Enables grapheme clustering | libutf8proc | -| `-Dterminfo` | feature | `enabled` | Build and install terminfo files | tic (ncurses) | -| `-Ddefault-terminfo` | string | `foot` | Default value of `TERM` | none | -| `-Dcustom-terminfo-install-location` | string | `${datadir}/terminfo` | Value to set `TERMINFO` to | None | -| `-Dsystemd-units-dir` | string | `${systemduserunitdir}` | Where to install the systemd service files (absolute) | None | -| `-Ddefault-utempter-path` | feature | `auto` | Default path to utempter binary (‘none’ disables default) | libutempter | +| Option | Type | Default | Description | Extra dependencies | +|--------------------------------------|---------|-------------------------|---------------------------------------------------------------------------------|---------------------| +| `-Ddocs` | feature | `auto` | Builds and install documentation | scdoc | +| `-Dtests` | bool | `true` | Build tests (adds a `ninja test` build target) | None | +| `-Dime` | bool | `true` | Enables IME support | None | +| `-Dgrapheme-clustering` | feature | `auto` | Enables grapheme clustering | libutf8proc | +| `-Dterminfo` | feature | `enabled` | Build and install terminfo files | tic (ncurses) | +| `-Ddefault-terminfo` | string | `foot` | Default value of `TERM` | None | +| `-Dcustom-terminfo-install-location` | string | `${datadir}/terminfo` | Value to set `TERMINFO` to | None | +| `-Dsystemd-units-dir` | string | `${systemduserunitdir}` | Where to install the systemd service files (absolute) | None | +| `-Dutmp-backend` | combo | `auto` | Which utmp backend to use (`none`, `libutempter`, `ulog` or `auto`) | libutempter or ulog | +| `-Dutmp-default-helper-path` | string | `auto` | Default path to utmp helper binary. `auto` selects path based on `utmp-backend` | None | Documentation includes the man pages, readme, changelog and license files. diff --git a/config.c b/config.c index 2ede1aa5..c3ab61e2 100644 --- a/config.c +++ b/config.c @@ -1009,13 +1009,29 @@ parse_section_main(struct context *ctx) else if (strcmp(key, "box-drawings-uses-font-glyphs") == 0) return value_to_bool(ctx, &conf->box_drawings_uses_font_glyphs); - else if (strcmp(key, "utempter") == 0) { - if (!value_to_str(ctx, &conf->utempter_path)) + else if (strcmp(key, "utmp-helper") == 0 || strcmp(key, "utempter") == 0) { + if (strcmp(key, "utempter") == 0) { + struct user_notification deprecation = { + .kind = USER_NOTIFICATION_DEPRECATED, + .text = xasprintf( + "%s:%d: \033[1m[main].utempter\033[22m, " + "use \033[1m[main].utmp-helper\033[22m instead", + ctx->path, ctx->lineno), + }; + tll_push_back(conf->notifications, deprecation); + + LOG_WARN( + "%s:%d: [main].utempter is deprecated, " + "use [main].utmp-helper instead", + ctx->path, ctx->lineno); + } + + if (!value_to_str(ctx, &conf->utmp_helper_path)) return false; - if (strcmp(conf->utempter_path, "none") == 0) { - free(conf->utempter_path); - conf->utempter_path = NULL; + if (strcmp(conf->utmp_helper_path, "none") == 0) { + free(conf->utmp_helper_path); + conf->utmp_helper_path = NULL; } return true; @@ -3019,9 +3035,12 @@ config_load(struct config *conf, const char *conf_path, }, .env_vars = tll_init(), - .utempter_path = (strlen(FOOT_DEFAULT_UTEMPTER_PATH) > 0 - ? xstrdup(FOOT_DEFAULT_UTEMPTER_PATH) - : NULL), +#if defined(UTMP_DEFAULT_HELPER_PATH) + .utmp_helper_path = ((strlen(UTMP_DEFAULT_HELPER_PATH) > 0 && + access(UTMP_DEFAULT_HELPER_PATH, X_OK) == 0) + ? xstrdup(UTMP_DEFAULT_HELPER_PATH) + : NULL), +#endif .notifications = tll_init(), }; @@ -3310,8 +3329,8 @@ config_clone(const struct config *old) tll_push_back(conf->env_vars, copy); } - conf->utempter_path = - old->utempter_path != NULL ? xstrdup(old->utempter_path) : NULL; + conf->utmp_helper_path = + old->utmp_helper_path != NULL ? xstrdup(old->utmp_helper_path) : NULL; conf->notifications.length = 0; conf->notifications.head = conf->notifications.tail = 0; @@ -3379,7 +3398,7 @@ config_free(struct config *conf) tll_remove(conf->env_vars, it); } - free(conf->utempter_path); + free(conf->utmp_helper_path); user_notifications_free(&conf->notifications); } diff --git a/config.h b/config.h index 31dddc64..34517019 100644 --- a/config.h +++ b/config.h @@ -320,7 +320,7 @@ struct config { env_var_list_t env_vars; - char *utempter_path; + char *utmp_helper_path; struct { enum fcft_scaling_filter fcft_filter; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 5ef62045..6a820892 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -343,9 +343,24 @@ empty string to be set, but it must be quoted: *KEY=""*) (including SMT). Note that this is not always the best value. In some cases, the number of physical _cores_ is better. -*utempter* - Path to utempter helper binary. Set to *none* to disable utmp - records. Default: _@utempter@_. +*utmp-helper* + Path to utmp logging helper binary. + + When starting foot, an utmp record is created by launching the + helper binary with the following arguments: + + ``` + @utmp_add_args@ + ``` + + When foot is closed, the utmp record is removed by launching the + helper binary with the following arguments: + + ``` + @utmp_del_args@ + ``` + + Set to *none* to disable utmp records. Default: _@utmp_helper_path@_. # SECTION: environment diff --git a/doc/meson.build b/doc/meson.build index 86e75952..17f09f39 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -2,16 +2,26 @@ sh = find_program('sh', native: true) scdoc_prog = find_program(scdoc.get_variable('scdoc'), native: true) -if utempter_path == '' - default_utempter_value = 'not set' +if utmp_backend != 'none' + utmp_add_args = '@0@ $WAYLAND_DISPLAY'.format(utmp_add) + utmp_del_args = (utmp_del_have_argument + ? '@0@ $WAYLAND_DISPLAY'.format(utmp_del) + : '@0@'.format(utmp_del)) + utmp_path = utmp_default_helper_path else - default_utempter_value = utempter_path + utmp_add_args = '' + utmp_del_args = '' + utmp_path = 'none' endif + conf_data = configuration_data( { 'default_terminfo': get_option('default-terminfo'), - 'utempter': default_utempter_value, + 'utmp_backend': utmp_backend, + 'utmp_add_args': utmp_add_args, + 'utmp_del_args': utmp_del_args, + 'utmp_helper_path': utmp_path, } ) diff --git a/foot.ini b/foot.ini index 8266b01b..4b2218a4 100644 --- a/foot.ini +++ b/foot.ini @@ -34,7 +34,8 @@ # word-delimiters=,│`|:"'()[]{}<> # selection-target=primary # workers= -# utempter=/usr/lib/utempter/utempter +# utmp-helper=/usr/lib/utempter/utempter # When utmp backend is ‘libutempter’ (Linux) +# utmp-helper=/usr/libexec/ulog-helper # When utmp backend is ‘ulog’ (FreeBSD) [environment] # name=value diff --git a/meson.build b/meson.build index a6892cc4..c5ef1928 100644 --- a/meson.build +++ b/meson.build @@ -16,30 +16,54 @@ if cc.has_function('memfd_create') add_project_arguments('-DMEMFD_CREATE', language: 'c') endif -utempter_path = get_option('default-utempter-path') -if utempter_path == '' - utempter = find_program( - 'utempter', - required: false, - dirs: [join_paths(get_option('prefix'), get_option('libdir'), 'utempter'), - join_paths(get_option('prefix'), get_option('libexecdir'), 'utempter'), - '/usr/lib/utempter', - '/usr/libexec/utempter', - '/lib/utempter'] - ) - if utempter.found() - utempter_path = utempter.full_path() +utmp_backend = get_option('utmp-backend') +if utmp_backend == 'auto' + host_os = host_machine.system() + if host_os == 'linux' + utmp_backend = 'libutempter' + elif host_os == 'freebsd' + utmp_backend = 'ulog' else - utempter_path = '' + utmp_backend = 'none' endif -elif utempter_path == 'none' - utempter_path = '' +endif + +utmp_default_helper_path = get_option('utmp-default-helper-path') + +if utmp_backend == 'none' + utmp_add = '' + utmp_del = '' + utmp_del_have_argument = false + utmp_default_helper_path = '' +elif utmp_backend == 'libutempter' + utmp_add = 'add' + utmp_del = 'del' + utmp_del_have_argument = true + if utmp_default_helper_path == 'auto' + utmp_default_helper_path = join_paths('/usr', get_option('libdir'), 'utempter', 'utempter') + endif +elif utmp_backend == 'ulog' + utmp_add = 'login' + utmp_del = 'logout' + utmp_del_have_argument = false + if utmp_default_helper_path == 'auto' + utmp_default_helper_path = join_paths('/usr', get_option('libexecdir'), 'ulog-helper') + endif +else + error('invalid utmp backend') endif add_project_arguments( ['-D_GNU_SOURCE=200809L', - '-DFOOT_DEFAULT_TERM="@0@"'.format(get_option('default-terminfo')), - '-DFOOT_DEFAULT_UTEMPTER_PATH="@0@"'.format(utempter_path)] + + '-DFOOT_DEFAULT_TERM="@0@"'.format(get_option('default-terminfo'))] + + (utmp_backend != 'none' + ? ['-DUTMP_ADD="@0@"'.format(utmp_add), + '-DUTMP_DEL="@0@"'.format(utmp_del), + '-DUTMP_DEFAULT_HELPER_PATH="@0@"'.format(utmp_default_helper_path)] + : []) + + (utmp_del_have_argument + ? ['-DUTMP_DEL_HAVE_ARGUMENT=1'] + : []) + (is_debug_build ? ['-D_DEBUG'] : [cc.get_supported_arguments('-fno-asynchronous-unwind-tables')]) + @@ -343,7 +367,8 @@ summary( 'Themes': get_option('themes'), 'IME': get_option('ime'), 'Grapheme clustering': utf8proc.found(), - 'Utempter path': utempter_path, + 'utmp backend': utmp_backend, + 'utmp helper default path': utmp_default_helper_path, 'Build terminfo': tic.found(), 'Terminfo install location': terminfo_install_location, 'Default TERM': get_option('default-terminfo'), diff --git a/meson_options.txt b/meson_options.txt index c38a8ca8..76121e60 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -22,5 +22,7 @@ option('custom-terminfo-install-location', type: 'string', value: '', option('systemd-units-dir', type: 'string', value: '', description: 'Where to install the systemd service files (absolute path). Default: ${systemduserunitdir}') -option('default-utempter-path', type: 'string', value: '', - description: 'Default path to utempter helper binary. Default: auto-detect') +option('utmp-backend', type: 'combo', value: 'auto', choices: ['none', 'libutempter', 'ulog', 'auto'], + description: 'Which utmp logging backend to use. This affects how (with what arguments) the utmp helper binary (see \utmp-default-helper-path\')is called. Default: auto (linux=libutempter, freebsd=ulog, others=none)') +option('utmp-default-helper-path', type: 'string', value: 'auto', + description: 'Default path to the utmp helper binary. Default: auto-detect') diff --git a/terminal.c b/terminal.c index ddc24cb3..dd4c627c 100644 --- a/terminal.c +++ b/terminal.c @@ -207,25 +207,41 @@ fdm_ptmx_out(struct fdm *fdm, int fd, int events, void *data) static bool add_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) { +#if defined(UTMP_ADD) if (ptmx < 0) return true; - if (conf->utempter_path == NULL) + if (conf->utmp_helper_path == NULL) return true; - char *const argv[] = {conf->utempter_path, "add", getenv("WAYLAND_DISPLAY"), NULL}; + char *const argv[] = {conf->utmp_helper_path, UTMP_ADD, getenv("WAYLAND_DISPLAY"), NULL}; return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL); +#else + return true; +#endif } static bool del_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) { +#if defined(UTMP_DEL) if (ptmx < 0) return true; - if (conf->utempter_path == NULL) + if (conf->utmp_helper_path == NULL) return true; - char *const argv[] = {conf->utempter_path, "del", getenv("WAYLAND_DISPLAY"), NULL}; + char *del_argument = +#if defined(UTMP_DEL_HAVE_ARGUMENT) + getenv("WAYLAND_DISPLAY") +#else + NULL +#endif + ; + + char *const argv[] = {conf->utmp_helper_path, UTMP_DEL, del_argument, NULL}; return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL); +#else + return true; +#endif } #if PTMX_TIMING diff --git a/tests/test-config.c b/tests/test-config.c index 4736a46b..4b7de298 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -458,7 +458,7 @@ test_section_main(void) test_string(&ctx, &parse_section_main, "shell", &conf.shell); test_string(&ctx, &parse_section_main, "term", &conf.term); test_string(&ctx, &parse_section_main, "app-id", &conf.app_id); - test_string(&ctx, &parse_section_main, "utempter", &conf.utempter_path); + test_string(&ctx, &parse_section_main, "utempter", &conf.utmp_helper_path); test_c32string(&ctx, &parse_section_main, "word-delimiters", &conf.word_delimiters); From f4b8e4f4d675ff30f1be43211ab71b35da7c258f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 17 May 2023 21:05:43 +0200 Subject: [PATCH 0293/1323] test: config: add test for main.utmp-helper option --- tests/test-config.c | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test-config.c b/tests/test-config.c index 4b7de298..da41b2b8 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -459,6 +459,7 @@ test_section_main(void) test_string(&ctx, &parse_section_main, "term", &conf.term); test_string(&ctx, &parse_section_main, "app-id", &conf.app_id); test_string(&ctx, &parse_section_main, "utempter", &conf.utmp_helper_path); + test_string(&ctx, &parse_section_main, "utmp-helper", &conf.utmp_helper_path); test_c32string(&ctx, &parse_section_main, "word-delimiters", &conf.word_delimiters); From 134b54dfe0e4d142ac4b6b7fe25e84485ba63387 Mon Sep 17 00:00:00 2001 From: jdevdevdev Date: Tue, 2 May 2023 01:53:01 +1000 Subject: [PATCH 0294/1323] .desktop: remove StartupWMClass from server, use distinct StartupWMClass for foot and footclient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For this to work, the default app-id of footclient has been changed from ‘foot’ to ‘footclient’. By using distinct StartupWMClasses, the compositor can connect a running foot/footclient instance to the correct .desktop-file. This ensures the correct icon is being used in e.g. docks, and that actions like “open another window” works correctly. Note that the user can override the app-id, either by setting app-id in foot.ini, or with the -a,--app-id command line option. Closes #1355 --- CHANGELOG.md | 5 ++++- config.c | 7 ++++--- config.h | 3 ++- doc/foot.1.scd | 2 +- doc/foot.ini.5.scd | 6 ++++-- doc/footclient.1.scd | 2 +- foot.ini | 2 +- main.c | 2 +- org.codeberg.dnkl.foot-server.desktop | 1 - org.codeberg.dnkl.footclient.desktop | 2 +- 10 files changed, 19 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3b4df58..4c8245b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,7 +62,6 @@ * Meson option `default-utempter-path` renamed to `utmp-default-helper-path`. - ### Deprecated * `[main].utempter` option. @@ -75,8 +74,12 @@ * Crash when scrolling after resizing the window with non-zero scrolling regions. * `XTMODKEYS` state not being reset on a terminal reset. +* In Gnome dock foot always groups under "foot client". Change + instances of footclient and foot to appear as "foot client" and + "foot" respectively. ([#1355][1355]) [1317]: https://codeberg.org/dnkl/foot/issues/1317 +[1355]: https://codeberg.org/dnkl/foot/issues/1355 ### Security diff --git a/config.c b/config.c index c3ab61e2..1d9f200b 100644 --- a/config.c +++ b/config.c @@ -2905,7 +2905,8 @@ config_font_list_clone(struct config_font_list *dst, bool config_load(struct config *conf, const char *conf_path, user_notifications_t *initial_user_notifications, - config_override_t *overrides, bool errors_are_fatal) + config_override_t *overrides, bool errors_are_fatal, + bool as_server) { bool ret = false; enum fcft_capabilities fcft_caps = fcft_capabilities(); @@ -2914,7 +2915,7 @@ config_load(struct config *conf, const char *conf_path, .term = xstrdup(FOOT_DEFAULT_TERM), .shell = get_shell(), .title = xstrdup("foot"), - .app_id = xstrdup("foot"), + .app_id = (as_server ? xstrdup("footclient") : xstrdup("foot")), .word_delimiters = xc32dup(U",│`|:\"'()[]{}<>"), .size = { .type = CONF_SIZE_PX, @@ -3348,7 +3349,7 @@ UNITTEST user_notifications_t nots = tll_init(); config_override_t overrides = tll_init(); - bool ret = config_load(&original, "/dev/null", ¬s, &overrides, false); + bool ret = config_load(&original, "/dev/null", ¬s, &overrides, false, false); xassert(ret); struct config *clone = config_clone(&original); diff --git a/config.h b/config.h index 34517019..ce1ee536 100644 --- a/config.h +++ b/config.h @@ -355,7 +355,8 @@ bool config_override_apply(struct config *conf, config_override_t *overrides, bool config_load( struct config *conf, const char *path, user_notifications_t *initial_user_notifications, - config_override_t *overrides, bool errors_are_fatal); + config_override_t *overrides, bool errors_are_fatal, + bool as_server); void config_free(struct config *conf); struct config *config_clone(const struct config *old); diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 6f63d4c8..60420bef 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -65,7 +65,7 @@ the foot command line *-a*,*--app-id*=_ID_ Value to set the *app-id* property on the Wayland window - to. Default: _foot_. + to. Default: _foot_ (normal mode), or _footclient_ (server mode). *-m*,*--maximized* Start in maximized mode. If both *--maximized* and *--fullscreen* diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 6a820892..32c493be 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -289,7 +289,8 @@ empty string to be set, but it must be quoted: *KEY=""*) *app-id* Value to set the *app-id* property on the Wayland window to. The compositor can use this value to e.g. group multiple windows, or - apply window management rules. Default: _foot_. + apply window management rules. Default: _foot_ (normal mode), or + _footclient_ (server mode). *bold-text-in-bright* Semi-boolean. When enabled, bold text is rendered in a brighter @@ -314,7 +315,8 @@ empty string to be set, but it must be quoted: *KEY=""*) and _body_ (message content). _${app-id}_ is replaced with the value of the command line option - _--app-id_, and defaults to *foot*. + _--app-id_, and defaults to *foot* (normal mode), or + *footclient* (server mode). _${window-title}_ is replaced with the current window title. diff --git a/doc/footclient.1.scd b/doc/footclient.1.scd index 63235134..1464700c 100644 --- a/doc/footclient.1.scd +++ b/doc/footclient.1.scd @@ -31,7 +31,7 @@ terminal has terminated. *-a*,*--app-id*=_ID_ Value to set the *app-id* property on the Wayland window - to. Default: _foot_. + to. Default: _foot_ (normal mode), or _footclient_ (server mode). *-w*,*--window-size-pixels*=_WIDTHxHEIGHT_ Set initial window width and height, in pixels. Default: _700x500_. diff --git a/foot.ini b/foot.ini index 4b2218a4..fcaef4a9 100644 --- a/foot.ini +++ b/foot.ini @@ -4,7 +4,7 @@ # term=foot (or xterm-256color if built with -Dterminfo=disabled) # login-shell=no -# app-id=foot +# app-id=foot # globally set wayland app-id. Default values are "foot" and "footclient" for desktop and server mode # title=foot # locked-title=no diff --git a/main.c b/main.c index 4af200fd..3f9846f3 100644 --- a/main.c +++ b/main.c @@ -487,7 +487,7 @@ main(int argc, char *const *argv) struct config conf = {NULL}; bool conf_successful = config_load( - &conf, conf_path, &user_notifications, &overrides, check_config); + &conf, conf_path, &user_notifications, &overrides, check_config, as_server); tll_free(overrides); if (!conf_successful) { diff --git a/org.codeberg.dnkl.foot-server.desktop b/org.codeberg.dnkl.foot-server.desktop index a40117c7..6e8891c0 100644 --- a/org.codeberg.dnkl.foot-server.desktop +++ b/org.codeberg.dnkl.foot-server.desktop @@ -9,4 +9,3 @@ Keywords=shell;prompt;command;commandline; Name=Foot Server GenericName=Terminal Comment=A wayland native terminal emulator (server) -StartupWMClass=foot diff --git a/org.codeberg.dnkl.footclient.desktop b/org.codeberg.dnkl.footclient.desktop index dc8bc5dc..b65790b4 100644 --- a/org.codeberg.dnkl.footclient.desktop +++ b/org.codeberg.dnkl.footclient.desktop @@ -9,4 +9,4 @@ Keywords=shell;prompt;command;commandline; Name=Foot Client GenericName=Terminal Comment=A wayland native terminal emulator (client) -StartupWMClass=foot +StartupWMClass=footclient From c51050a9bc16b231de964156cd3ffd20ee12569e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 25 May 2023 18:39:32 +0200 Subject: [PATCH 0295/1323] osc: update font subpixel mode, and window opaque compositor hint, on alpha changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When background alpha is changed at runtime (using OSC-11), we (may) have to update the opaque hint we send to the compositor. We must also update the subpixel mode used when rendering font glyphs. Why? When the window is fully opaque, we use wl_surface_set_opaque_region() on the entire surface, to hint to the compositor that it doesn’t have to blend the window content with whatever is behind the window. Obviously, if alpha is changed from opaque, to transparent (or semi-transparent), that hint must be removed. Sub-pixel mode is harder to explain, but in short, we can’t do subpixel hinting with a (semi-)transparent background. Thus, similar to the opaque hint, subpixel antialiasing must be enabled/disabled when background alpha is changed. --- CHANGELOG.md | 4 +++- osc.c | 9 ++++++++- pgo/pgo.c | 1 + wayland.c | 29 +++++++++++++++++++---------- wayland.h | 1 + 5 files changed, 32 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c8245b2..0c44dd13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,7 +76,9 @@ * `XTMODKEYS` state not being reset on a terminal reset. * In Gnome dock foot always groups under "foot client". Change instances of footclient and foot to appear as "foot client" and - "foot" respectively. ([#1355][1355]) + "foot" respectively. ([#1355][1355]). +* Glitchy rendering when alpha (transparency) is changed between + opaque and non-opaque at runtime (using OSC-11). [1317]: https://codeberg.org/dnkl/foot/issues/1317 [1355]: https://codeberg.org/dnkl/foot/issues/1355 diff --git a/osc.c b/osc.c index 55cfcf84..45d114de 100644 --- a/osc.c +++ b/osc.c @@ -729,8 +729,15 @@ osc_dispatch(struct terminal *term) case 11: term->colors.bg = color; - if (have_alpha) + if (have_alpha) { + const bool changed = term->colors.alpha != alpha; term->colors.alpha = alpha; + + if (changed) { + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + } + } break; case 17: diff --git a/pgo/pgo.c b/pgo/pgo.c index b41b5850..d9ee5855 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -94,6 +94,7 @@ wayl_win_init(struct terminal *term, const char *token) } void wayl_win_destroy(struct wl_window *win) {} +void wayl_win_alpha_changed(struct wl_window *win) {} bool wayl_win_set_urgent(struct wl_window *win) { return true; } bool diff --git a/wayland.c b/wayland.c index 68a7a4f1..51161cf0 100644 --- a/wayland.c +++ b/wayland.c @@ -1495,16 +1495,7 @@ wayl_win_init(struct terminal *term, const char *token) goto out; } - if (term->colors.alpha == 0xffff) { - struct wl_region *region = wl_compositor_create_region( - term->wl->compositor); - - if (region != NULL) { - wl_region_add(region, 0, 0, INT32_MAX, INT32_MAX); - wl_surface_set_opaque_region(win->surface, region); - wl_region_destroy(region); - } - } + wayl_win_alpha_changed(win); wl_surface_add_listener(win->surface, &surface_listener, win); @@ -1798,6 +1789,24 @@ wayl_roundtrip(struct wayland *wayl) wayl_flush(wayl); } +void +wayl_win_alpha_changed(struct wl_window *win) +{ + struct terminal *term = win->term; + + if (term->colors.alpha == 0xffff) { + struct wl_region *region = wl_compositor_create_region( + term->wl->compositor); + + if (region != NULL) { + wl_region_add(region, 0, 0, INT32_MAX, INT32_MAX); + wl_surface_set_opaque_region(win->surface, region); + wl_region_destroy(region); + } + } else + wl_surface_set_opaque_region(win->surface, NULL); +} + #if defined(HAVE_XDG_ACTIVATION) static void activation_token_for_urgency_done(const char *token, void *data) diff --git a/wayland.h b/wayland.h index 4b6939ab..0d627052 100644 --- a/wayland.h +++ b/wayland.h @@ -434,6 +434,7 @@ void wayl_roundtrip(struct wayland *wayl); struct wl_window *wayl_win_init(struct terminal *term, const char *token); void wayl_win_destroy(struct wl_window *win); +void wayl_win_alpha_changed(struct wl_window *win); bool wayl_win_set_urgent(struct wl_window *win); bool wayl_win_csd_titlebar_visible(const struct wl_window *win); From b4e418f2518051326ea4805f97740c949140bc83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 26 May 2023 10:20:05 +0200 Subject: [PATCH 0296/1323] ci: try alpine edge instead of latest --- .builds/alpine-x64.yml | 2 +- .woodpecker.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.builds/alpine-x64.yml b/.builds/alpine-x64.yml index 2e2ec2c4..6ec489fc 100644 --- a/.builds/alpine-x64.yml +++ b/.builds/alpine-x64.yml @@ -1,4 +1,4 @@ -image: alpine/latest +image: alpine/edge packages: - musl-dev - eudev-libs diff --git a/.woodpecker.yml b/.woodpecker.yml index 06631f89..acd30fc7 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -4,7 +4,7 @@ pipeline: branch: - master - releases/* - image: alpine:latest + image: alpine:edge commands: - apk add python3 - apk add py3-pip @@ -16,7 +16,7 @@ pipeline: branch: - master - releases/* - image: alpine:latest + image: alpine:edge commands: - apk add git - mkdir -p subprojects && cd subprojects @@ -30,7 +30,7 @@ pipeline: - master - releases/* group: build - image: alpine:latest + image: alpine:edge commands: - apk update - apk add musl-dev linux-headers meson ninja gcc clang scdoc ncurses @@ -87,7 +87,7 @@ pipeline: - master - releases/* group: build - image: i386/alpine:latest + image: i386/alpine:edge commands: - apk update - apk add musl-dev linux-headers meson ninja gcc clang scdoc ncurses From 1433a81c08d696cad8c5460ca739bc0cd97a9a5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 31 May 2023 16:27:48 +0200 Subject: [PATCH 0297/1323] sixel: apply background alpha when P2=0 or P2=2, and current bg color is the default bg color Closes #1360 --- CHANGELOG.md | 6 ++++++ sixel.c | 22 ++++++++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c44dd13..8a26dad6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,12 @@ name is still recognized, but will log a deprecation warning. * Meson option `default-utempter-path` renamed to `utmp-default-helper-path`. +* Opaque sixels now retain the background opacity (when current + background color is the **default** background color) + ([#1360][1360]). + +[1360]: https://codeberg.org/dnkl/foot/issues/1360 + ### Deprecated diff --git a/sixel.c b/sixel.c index 592f48f8..70ec2ee5 100644 --- a/sixel.c +++ b/sixel.c @@ -73,22 +73,36 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) switch (term->vt.attrs.bg_src) { case COLOR_RGB: - bg = term->vt.attrs.bg; + bg = 0xffu << 24 | term->vt.attrs.bg; break; case COLOR_BASE16: case COLOR_BASE256: - bg = term->colors.table[term->vt.attrs.bg]; + bg = 0xffu << 24 | term->colors.table[term->vt.attrs.bg]; break; case COLOR_DEFAULT: - bg = term->colors.bg; + if (term->colors.alpha == 0xffff) + bg = 0xffu << 24 | term->colors.bg; + else { + /* Alpha needs to be pre-multiplied */ + uint32_t r = (term->colors.bg >> 16) & 0xff; + uint32_t g = (term->colors.bg >> 8) & 0xff; + uint32_t b = (term->colors.bg >> 0) & 0xff; + + uint32_t alpha = term->colors.alpha; + r *= alpha; r /= 0xffff; + g *= alpha; g /= 0xffff; + b *= alpha; b /= 0xffff; + + bg = (alpha >> 8) << 24 | (r & 0xff) << 16 | (g & 0xff) << 8 | (b & 0xff); + } break; } term->sixel.default_bg = term->sixel.transparent_bg ? 0x00000000u - : 0xffu << 24 | bg; + : bg; for (size_t i = 0; i < 1 * 6; i++) term->sixel.image.data[i] = term->sixel.default_bg; From 8859e134efa422d50e53c0bbb0e83d07ddf66091 Mon Sep 17 00:00:00 2001 From: Phillip Susi Date: Tue, 30 May 2023 15:49:01 -0400 Subject: [PATCH 0298/1323] Fix non UTF-8 locale complaint If the locale isn't UTF-8, foot tries to fall back to C.UTF-8 and prints a warning. The warning was garbled because the name of the original locale is no longer valid after calling setlocale() a second time. Use strdup to stash the original string. Closes #1362 --- main.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/main.c b/main.c index 3f9846f3..f58e170f 100644 --- a/main.c +++ b/main.c @@ -450,6 +450,7 @@ main(int argc, char *const *argv) "C.UTF-8", "en_US.UTF-8", }; + char *saved_locale = xstrdup(locale); /* * Try to force an UTF-8 locale. If we succeed, launch the @@ -461,12 +462,12 @@ main(int argc, char *const *argv) if (setlocale(LC_CTYPE, fallback_locale) != NULL) { LOG_WARN("'%s' is not a UTF-8 locale, using '%s' instead", - locale, fallback_locale); + saved_locale, fallback_locale); user_notification_add_fmt( &user_notifications, USER_NOTIFICATION_WARNING, "'%s' is not a UTF-8 locale, using '%s' instead", - locale, fallback_locale); + saved_locale, fallback_locale); bad_locale = false; break; @@ -476,13 +477,14 @@ main(int argc, char *const *argv) if (bad_locale) { LOG_ERR( "'%s' is not a UTF-8 locale, and failed to find a fallback", - locale); + saved_locale); user_notification_add_fmt( &user_notifications, USER_NOTIFICATION_ERROR, "'%s' is not a UTF-8 locale, and failed to find a fallback", - locale); + saved_locale); } + free(saved_locale); } struct config conf = {NULL}; From 16872ecc4137a5a58f9da8e62990d5587afbec21 Mon Sep 17 00:00:00 2001 From: sewn Date: Wed, 14 Jun 2023 12:26:19 +0000 Subject: [PATCH 0299/1323] meson: use meson feed feature for scdoc input Removes the need for a shell dependency. --- CHANGELOG.md | 2 ++ doc/meson.build | 5 ++--- meson.build | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a26dad6..692784ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ ### Changed +* Minimum required meson version is now 0.59 ([#1371][1371]). * 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 @@ -65,6 +66,7 @@ background color is the **default** background color) ([#1360][1360]). +[1371]: https://codeberg.org/dnkl/foot/pulls/1371 [1360]: https://codeberg.org/dnkl/foot/issues/1360 diff --git a/doc/meson.build b/doc/meson.build index 17f09f39..37972652 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -1,5 +1,3 @@ -sh = find_program('sh', native: true) - scdoc_prog = find_program(scdoc.get_variable('scdoc'), native: true) if utmp_backend != 'none' @@ -43,8 +41,9 @@ foreach man_src : [{'name': 'foot', 'section' : 1}, out, output: out, input: preprocessed, - command: [sh, '-c', '@0@ < @INPUT@'.format(scdoc_prog.full_path())], + command: scdoc_prog.full_path(), capture: true, + feed: true, install: true, install_dir: join_paths(get_option('mandir'), 'man@0@'.format(section))) endforeach diff --git a/meson.build b/meson.build index c5ef1928..29951541 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,7 @@ project('foot', 'c', version: '1.14.0', license: 'MIT', - meson_version: '>=0.58.0', + meson_version: '>=0.59.0', default_options: [ 'c_std=c11', 'warning_level=1', From 93b6883896f2bd75af15784c1be4b43dfd62b3b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 5 Jun 2023 17:31:35 +0200 Subject: [PATCH 0300/1323] terminfo: XM: add private mode 1004 This was added to ncurses (to the xterm+sm+1006 fragment) in 2023-05-08. --- foot.info | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foot.info b/foot.info index cf81d721..ef8314e9 100644 --- a/foot.info +++ b/foot.info @@ -41,7 +41,7 @@ Se=\E[ q, Ss=\E[%p1%d q, Sync=\E[?2026%?%p1%{1}%-%tl%eh, - XM=\E[?1006;1000%?%p1%{1}%=%th%el%;, + XM=\E[?1006;1004;1000%?%p1%{1}%=%th%el%;, XR=\E[>0q, acsc=``aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~, bel=^G, From b91bde8a651bdf25a510a9b4a6c1f1f5fdf23d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 5 Jun 2023 17:32:28 +0200 Subject: [PATCH 0301/1323] terminfo: add TS capability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a new ‘extended’ capability. ‘TS’ has been around for a while, but was originally not part of foot’s terminfo. Not sure when it was added to ncurses’ foot terminfo. In any case, ncurses has this to say about TS: These building-blocks allow access to the X titlebar and icon name as a status line. There are a few problems in using them in entries: a) tsl should have a parameter to denote the column on which to transfer to the status line. ... But that issue regarding the parameter for tsl means that applications may not rely on it. The SVr4 documentation says tsl will "move to status line, column #1". At the point in time when ESR added DJM's "pseudo-color" entry with the split-up escape sequence for tsl/fsl, there were 65 entries using tsl: 32 used a parameter, matching the documentation (including x10term). 21 used a parameterless control, exiting from the status line on ^M. 6 used parameterless controls for tsl and fsl 6 used a split-up escape sequence, e.g., the same approach. The extension "TS" is preferable, because it does not accept a parameter. However, if you are using a non-extended terminfo, "TS" is not visible. --- foot.info | 1 + 1 file changed, 1 insertion(+) diff --git a/foot.info b/foot.info index ef8314e9..4f95bf7b 100644 --- a/foot.info +++ b/foot.info @@ -41,6 +41,7 @@ Se=\E[ q, Ss=\E[%p1%d q, Sync=\E[?2026%?%p1%{1}%-%tl%eh, + TS=\E]2;, XM=\E[?1006;1004;1000%?%p1%{1}%=%th%el%;, XR=\E[>0q, acsc=``aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~, From 690d78edfa3f468b5c3be56f37fd622e4161dd9b Mon Sep 17 00:00:00 2001 From: Dan Bungert Date: Sat, 10 Jun 2023 20:40:01 -0600 Subject: [PATCH 0302/1323] test: config: add test for url.protocols option --- tests/test-config.c | 46 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/tests/test-config.c b/tests/test-config.c index da41b2b8..b14f28b1 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -106,6 +106,50 @@ test_c32string(struct context *ctx, bool (*parse_fun)(struct context *ctx), } } +static void +test_protocols(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, char32_t **const *ptr) +{ + ctx->key = key; + + static const struct { + const char *option_string; + int count; + const char32_t *value[2]; + bool invalid; + } input[] = { + {""}, + {"http", 1, {U"http://"}}, + {" http", 1, {U"http://"}}, + {"http, https", 2, {U"http://", U"https://"}}, + {"longprotocolislong", 1, {U"longprotocolislong://"}}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, &ctx->value[0]); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, &ctx->value[0]); + } + for (int c = 0; c < input[i].count; c++) { + if (c32cmp((*ptr)[c], input[i].value[c]) != 0) { + BUG("[%s].%s=%s: set value[%d] (%ls) not the expected one (%ls)", + ctx->section, ctx->key, &ctx->value[c], c, + (const wchar_t *)(*ptr)[c], + (const wchar_t *)input[i].value[c]); + } + } + } + } +} + static void test_boolean(struct context *ctx, bool (*parse_fun)(struct context *ctx), const char *key, const bool *ptr) @@ -578,8 +622,8 @@ test_section_url(void) (int []){OSC8_UNDERLINE_URL_MODE, OSC8_UNDERLINE_ALWAYS}, (int *)&conf.url.osc8_underline); test_c32string(&ctx, &parse_section_url, "label-letters", &conf.url.label_letters); + test_protocols(&ctx, &parse_section_url, "protocols", &conf.url.protocols); - /* TODO: protocols (list of wchars) */ /* TODO: uri-characters (wchar string, but sorted) */ config_free(&conf); From d88bea5e22330c36c241ae0d441279799ac5897f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 16 Jun 2023 16:26:13 +0200 Subject: [PATCH 0303/1323] vt: split up action_param() to three separate functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We’re already switching on the next VT input byte in the state machine; no need to if...else if in action_param() too. That is, split up action_param() into three: * action_param_new() * action_param_new_subparam() * action_param() This makes the code cleaner, and hopefully slightly faster. Next, to improve performance further, only check for (sub)parameter overflow in action_param_new() and action_param_subparam(). Add pointers to the VT struct that points to the currently active parameter and sub-parameter. When the number of parameters (or sub-parameters) overflow, warn, and then point the parameter pointer to a "dummy" value in the VT struct. This way, we don’t have to check anything in action_param(). --- terminal.h | 8 +++- vt.c | 135 +++++++++++++++++++++++++++-------------------------- 2 files changed, 75 insertions(+), 68 deletions(-) diff --git a/terminal.h b/terminal.h index d2762a5a..abedf44d 100644 --- a/terminal.h +++ b/terminal.h @@ -180,8 +180,10 @@ struct grid { }; struct vt_subparams { - unsigned value[16]; uint8_t idx; + unsigned *cur; + unsigned value[16]; + unsigned dummy; }; struct vt_param { @@ -197,8 +199,10 @@ struct vt { #endif char32_t utf8; struct { - struct vt_param v[16]; uint8_t idx; + struct vt_param *cur; + struct vt_param v[16]; + struct vt_param dummy; } params; uint32_t private; /* LSB=priv0, MSB=priv3 */ diff --git a/vt.c b/vt.c index 91f00e6f..2ee2dbaf 100644 --- a/vt.c +++ b/vt.c @@ -294,74 +294,31 @@ action_print(struct terminal *term, uint8_t c) } static void -action_param(struct terminal *term, uint8_t c) +action_param_lazy_init(struct terminal *term) { if (term->vt.params.idx == 0) { struct vt_param *param = &term->vt.params.v[0]; + + term->vt.params.cur = param; param->value = 0; param->sub.idx = 0; + param->sub.cur = NULL; term->vt.params.idx = 1; } +} - xassert(term->vt.params.idx > 0); +static void +action_param_new(struct terminal *term, uint8_t c) +{ + xassert(c == ';'); + action_param_lazy_init(term); const size_t max_params = sizeof(term->vt.params.v) / sizeof(term->vt.params.v[0]); - const size_t max_sub_params - = sizeof(term->vt.params.v[0].sub.value) / sizeof(term->vt.params.v[0].sub.value[0]); - /* New parameter */ - if (c == ';') { - if (unlikely(term->vt.params.idx >= max_params)) - goto excess_params; + struct vt_param *param; - struct vt_param *param = &term->vt.params.v[term->vt.params.idx++]; - param->value = 0; - param->sub.idx = 0; - } - - /* New sub-parameter */ - else if (c == ':') { - if (unlikely(term->vt.params.idx - 1 >= max_params)) - goto excess_params; - - struct vt_param *param = &term->vt.params.v[term->vt.params.idx - 1]; - if (unlikely(param->sub.idx >= max_sub_params)) - goto excess_sub_params; - - param->sub.value[param->sub.idx++] = 0; - } - - /* New digit for current parameter/sub-parameter */ - else { - if (unlikely(term->vt.params.idx - 1 >= max_params)) - goto excess_params; - - struct vt_param *param = &term->vt.params.v[term->vt.params.idx - 1]; - unsigned *value; - - if (param->sub.idx > 0) { - if (unlikely(param->sub.idx - 1 >= max_sub_params)) - goto excess_sub_params; - value = ¶m->sub.value[param->sub.idx - 1]; - } else - value = ¶m->value; - - *value *= 10; - *value += c - '0'; - } - -#if defined(_DEBUG) - /* The rest of the code assumes 'idx' *never* points outside the array */ - xassert(term->vt.params.idx <= max_params); - for (size_t i = 0; i < term->vt.params.idx; i++) - xassert(term->vt.params.v[i].sub.idx <= max_sub_params); -#endif - - return; - -excess_params: - { + if (unlikely(term->vt.params.idx >= max_params)) { static bool have_warned = false; if (!have_warned) { have_warned = true; @@ -370,11 +327,29 @@ excess_params: "(will not warn again)", sizeof(term->vt.params.v) / sizeof(term->vt.params.v[0])); } - } - return; + param = &term->vt.params.dummy; + } else + param = &term->vt.params.v[term->vt.params.idx++]; -excess_sub_params: - { + term->vt.params.cur = param; + param->value = 0; + param->sub.idx = 0; + param->sub.cur = NULL; +} + +static void +action_param_new_subparam(struct terminal *term, uint8_t c) +{ + xassert(c == ':'); + action_param_lazy_init(term); + + const size_t max_sub_params + = sizeof(term->vt.params.v[0].sub.value) / sizeof(term->vt.params.v[0].sub.value[0]); + + struct vt_param *param = term->vt.params.cur; + unsigned *sub_param_value; + + if (unlikely(param->sub.idx >= max_sub_params)) { static bool have_warned = false; if (!have_warned) { have_warned = true; @@ -383,8 +358,33 @@ excess_sub_params: "(will not warn again)", sizeof(term->vt.params.v[0].sub.value) / sizeof(term->vt.params.v[0].sub.value[0])); } - } - return; + + sub_param_value = ¶m->sub.dummy; + } else + sub_param_value = ¶m->sub.value[param->sub.idx++]; + + param->sub.cur = sub_param_value; + *sub_param_value = 0; +} + +static void +action_param(struct terminal *term, uint8_t c) +{ + action_param_lazy_init(term); + xassert(term->vt.params.cur != NULL); + + struct vt_param *param = term->vt.params.cur; + unsigned *value; + + if (unlikely(param->sub.cur != NULL)) + value = param->sub.cur; + else + value = ¶m->value; + + unsigned v = *value; + v *= 10; + v += c - '0'; + *value = v; } static void @@ -1024,7 +1024,9 @@ state_csi_entry_switch(struct terminal *term, uint8_t data) case 0x20 ... 0x2f: action_collect(term, data); return STATE_CSI_INTERMEDIATE; case 0x30 ... 0x39: action_param(term, data); return STATE_CSI_PARAM; - case 0x3a ... 0x3b: action_param(term, data); return STATE_CSI_PARAM; + case 0x3a: action_param_new_subparam(term, data); return STATE_CSI_PARAM; + case 0x3b: action_param_new(term, data); return STATE_CSI_PARAM; + case 0x3c ... 0x3f: action_collect(term, data); return STATE_CSI_PARAM; case 0x40 ... 0x7e: action_csi_dispatch(term, data); return STATE_GROUND; case 0x7f: action_ignore(term); return STATE_CSI_ENTRY; @@ -1044,8 +1046,9 @@ state_csi_param_switch(struct terminal *term, uint8_t data) case 0x20 ... 0x2f: action_collect(term, data); return STATE_CSI_INTERMEDIATE; - case 0x30 ... 0x39: - case 0x3a ... 0x3b: action_param(term, data); return STATE_CSI_PARAM; + case 0x30 ... 0x39: action_param(term, data); return STATE_CSI_PARAM; + case 0x3a: action_param_new_subparam(term, data); return STATE_CSI_PARAM; + case 0x3b: action_param_new(term, data); return STATE_CSI_PARAM; case 0x3c ... 0x3f: return STATE_CSI_IGNORE; case 0x40 ... 0x7e: action_csi_dispatch(term, data); return STATE_GROUND; @@ -1126,7 +1129,7 @@ state_dcs_entry_switch(struct terminal *term, uint8_t data) case 0x20 ... 0x2f: action_collect(term, data); return STATE_DCS_INTERMEDIATE; case 0x30 ... 0x39: action_param(term, data); return STATE_DCS_PARAM; case 0x3a: return STATE_DCS_IGNORE; - case 0x3b: action_param(term, data); return STATE_DCS_PARAM; + case 0x3b: action_param_new(term, data); return STATE_DCS_PARAM; case 0x3c ... 0x3f: action_collect(term, data); return STATE_DCS_PARAM; case 0x40 ... 0x7e: action_hook(term, data); return STATE_DCS_PASSTHROUGH; case 0x7f: action_ignore(term); return STATE_DCS_ENTRY; @@ -1147,7 +1150,7 @@ state_dcs_param_switch(struct terminal *term, uint8_t data) case 0x20 ... 0x2f: action_collect(term, data); return STATE_DCS_INTERMEDIATE; case 0x30 ... 0x39: action_param(term, data); return STATE_DCS_PARAM; case 0x3a: return STATE_DCS_IGNORE; - case 0x3b: action_param(term, data); return STATE_DCS_PARAM; + case 0x3b: action_param_new(term, data); return STATE_DCS_PARAM; case 0x3c ... 0x3f: return STATE_DCS_IGNORE; case 0x40 ... 0x7e: action_hook(term, data); return STATE_DCS_PASSTHROUGH; case 0x7f: action_ignore(term); return STATE_DCS_PARAM; From 24f12c7b5e753a5152eac44da9024954d43250e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 16 Jun 2023 16:33:15 +0200 Subject: [PATCH 0304/1323] term: add term_cursor_col() Set cursor column, absolute. term_cursor_to() needs to reload the current row pointer, and is thus not very effective when we only need to modify the column. --- terminal.c | 9 +++++++++ terminal.h | 1 + 2 files changed, 10 insertions(+) diff --git a/terminal.c b/terminal.c index dd4c627c..3beffcf3 100644 --- a/terminal.c +++ b/terminal.c @@ -2557,6 +2557,15 @@ term_cursor_home(struct terminal *term) term_cursor_to(term, term_row_rel_to_abs(term, 0), 0); } +void +term_cursor_col(struct terminal *term, int col) +{ + xassert(col < term->cols); + + term->grid->cursor.lcf = false; + term->grid->cursor.point.col = col; +} + void term_cursor_left(struct terminal *term, int count) { diff --git a/terminal.h b/terminal.h index abedf44d..ec0db44b 100644 --- a/terminal.h +++ b/terminal.h @@ -743,6 +743,7 @@ void term_erase_scrollback(struct terminal *term); int term_row_rel_to_abs(const struct terminal *term, int row); void term_cursor_home(struct terminal *term); void term_cursor_to(struct terminal *term, int row, int col); +void term_cursor_col(struct terminal *term, int col); void term_cursor_left(struct terminal *term, int count); void term_cursor_right(struct terminal *term, int count); void term_cursor_up(struct terminal *term, int count); From 2c0c4ce821b90dea9ebbcdab98c10865de1872fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 16 Jun 2023 16:34:17 +0200 Subject: [PATCH 0305/1323] csi: CHA+HPA (cursor horizontal absolute): use term_cursor_col() --- csi.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csi.c b/csi.c index 7b318d0a..ef1a28f2 100644 --- a/csi.c +++ b/csi.c @@ -815,7 +815,7 @@ csi_dispatch(struct terminal *term, uint8_t final) case 'G': { /* Cursor horizontal absolute */ int col = min(vt_param_get(term, 0, 1), term->cols) - 1; - term_cursor_to(term, term->grid->cursor.point.row, col); + term_cursor_col(term, col); break; } From 67b3663f39146d0f33d93d0f2ac3127e826a9957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= Date: Wed, 14 Jun 2023 14:52:58 -0400 Subject: [PATCH 0306/1323] add srcery theme Based on https://srcery.sh/ --- themes/srcery | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 themes/srcery diff --git a/themes/srcery b/themes/srcery new file mode 100644 index 00000000..54966707 --- /dev/null +++ b/themes/srcery @@ -0,0 +1,26 @@ +# srcery + +[colors] +background= 1c1b19 +foreground= fce8c3 +regular0= 1c1b19 +regular1= ef2f27 +regular2= 519f50 +regular3= fbb829 +regular4= 2c78bf +regular5= e02c6d +regular6= 0aaeb3 +regular7= baa67f +bright0= 918175 +bright1= f75341 +bright2= 98bc37 +bright3= fed06e +bright4= 68a8e4 +bright5= ff5c8f +bright6= 2be4d0 +bright7= fce8c3 + +## Enable if prefer solarized colors instead of inverterd fg/bg for +## highlighting (mouse selection) +# selection-foreground=93a1a1 +# selection-background=073642 From 3a59cbbaa3906da6f9ab73ad949bc598318be7e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 20 Jun 2023 15:59:16 +0200 Subject: [PATCH 0307/1323] render: resize: fix crash when reflowing the alt screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When doing an interactive resize, and `resize-delay-ms` > 0 (the default), we would crash if the original screen size (i.e. the size before the interactive resize started) was larger than the last window size. For example, if we interactively go from 85 rows to 75, and then non-interactively went from 75 to 80, we’d crash. The resizes had to be made in a single go. One way to trigger this was to start an interactive resize on a floating window, and then *while resizing* toggle the window’s floating mode. Closes #1377 --- CHANGELOG.md | 3 +++ render.c | 10 +++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 692784ee..2eab13eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,9 +87,12 @@ "foot" respectively. ([#1355][1355]). * Glitchy rendering when alpha (transparency) is changed between opaque and non-opaque at runtime (using OSC-11). +* Regression: crash when resizing the window when `resize-delay-ms > + 0` ([#1377][1377]). [1317]: https://codeberg.org/dnkl/foot/issues/1317 [1355]: https://codeberg.org/dnkl/foot/issues/1355 +[1377]: https://codeberg.org/dnkl/foot/issues/1377 ### Security diff --git a/render.c b/render.c index 340e0378..1858467d 100644 --- a/render.c +++ b/render.c @@ -4030,7 +4030,9 @@ maybe_resize(struct terminal *term, int width, int height, bool force) term->interactive_resizing.old_hide_cursor = term->hide_cursor; term->interactive_resizing.grid = xmalloc(sizeof(*term->interactive_resizing.grid)); *term->interactive_resizing.grid = term->normal; - term->interactive_resizing.selection_coords = term->selection.coords; + + if (term->grid == &term->normal) + term->interactive_resizing.selection_coords = term->selection.coords; } else { /* We’ll replace the current temporary grid, with a new * one (again based on the original grid) */ @@ -4118,6 +4120,8 @@ maybe_resize(struct terminal *term, int width, int height, bool force) } else { /* Full text reflow */ + int old_normal_rows = old_rows; + if (term->interactive_resizing.grid != NULL) { /* Throw away the current, truncated, “normal” grid, and * use the original grid instead (from before the resize @@ -4129,7 +4133,7 @@ maybe_resize(struct terminal *term, int width, int height, bool force) term->hide_cursor = term->interactive_resizing.old_hide_cursor; term->selection.coords = term->interactive_resizing.selection_coords; - old_rows = term->interactive_resizing.old_screen_rows; + old_normal_rows = term->interactive_resizing.old_screen_rows; term->interactive_resizing.grid = NULL; term->interactive_resizing.old_screen_rows = 0; @@ -4145,7 +4149,7 @@ maybe_resize(struct terminal *term, int width, int height, bool force) }; grid_resize_and_reflow( - &term->normal, new_normal_grid_rows, new_cols, old_rows, new_rows, + &term->normal, new_normal_grid_rows, new_cols, old_normal_rows, new_rows, term->selection.coords.end.row >= 0 ? ALEN(tracking_points) : 0, tracking_points); } From 70ffc2632f508d0298ca53160ce69300861aef5b Mon Sep 17 00:00:00 2001 From: wout Date: Fri, 23 Jun 2023 18:10:19 +0000 Subject: [PATCH 0308/1323] Fixed a type for the pixel fontsize change xp -> px --- doc/foot.ini.5.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 32c493be..f9174fc6 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -87,7 +87,7 @@ empty string to be set, but it must be quoted: *KEY=""*) Examples: ``` font-size-adjustment=0.5 # Adjust by 0.5 points - font-size-adjustment=10xp # Adjust by 10 pixels + font-size-adjustment=10px # Adjust by 10 pixels font-size-adjustment=7.5% # Adjust by 7.5 percent ``` From 8a3620bafaa4119b9f6d3f74189c2dac78614d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 23 Jun 2023 20:20:01 +0200 Subject: [PATCH 0309/1323] term: scroll: only record scroll damage when viewport is at the bottom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We don’t need to record scroll damage if the viewport isn’t at the bottom, since in this case, the renderer ignores the scroll damage anyway. This fixes a performance corner case, when the viewport is at the top of the scrollback history. When application scrolls the terminal contents, and the scrollback history is full, and the viewport is at top of the history, then the viewport needs to be moved (the scrollback history is a circular buffer, and thus the top of the history “moves” when we’re scrolling in new contents). Moving the viewport typically results in another type of scroll damage (DAMAGE_SCROLL_IN_VIEW, instead of the “normal” DAMAGE_SCROLL). Thus, each application triggered scroll, will result in two scroll damage records: one DAMAGE_SCROLL, and one DAMAGE_SCROLL_IN_VIEW. These two are incompatible, meaning they can’t be merged. What’s worse, it also means the DAMAGE_SCROLL records from two application triggered scrolls cannot be merged (since there’s a DAMAGE_SCROLL_IN_VIEW in between). As a result, the renderer will not see one, or “a few” scroll damage events, but a *ton*. _Each_ one typically a single line, or so. And each one resulting in lots of traffic on the wayland socket, as we create and destroy new buffer pools, when doing “shm scrolling”. This eventually leads to the socket not being able to keep up, and the socket is closed on us, forcing us to exit. The fix is really simple: don’t record “normal” scroll damage when scrolling, _unless_ the viewport is at the bottom (and thus “follows” the application output). As soon as the user scrolls up in the history, we’ll stop emitting normal scroll damage records. This is just fine, since, as mentioned above, the renderer ignores them when the viewport isn’t at the bottom. What if the viewport is moved back down again, before the next frame has been rendered? Wont there be “missing” scroll damage records? No, because moving the viewport results in scroll damage records by itself. Closes #1380 --- CHANGELOG.md | 3 +++ terminal.c | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eab13eb..658e0c1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,10 +89,13 @@ opaque and non-opaque at runtime (using OSC-11). * Regression: crash when resizing the window when `resize-delay-ms > 0` ([#1377][1377]). +* Crash when scrolling up while running something that generates a lot + of output (for example, `yes`) ([#1380][1380]). [1317]: https://codeberg.org/dnkl/foot/issues/1317 [1355]: https://codeberg.org/dnkl/foot/issues/1355 [1377]: https://codeberg.org/dnkl/foot/issues/1377 +[1380]: https://codeberg.org/dnkl/foot/issues/1380 ### Security diff --git a/terminal.c b/terminal.c index 3beffcf3..815dc2d4 100644 --- a/terminal.c +++ b/terminal.c @@ -2714,6 +2714,7 @@ term_scroll_partial(struct terminal *term, struct scroll_region region, int rows term->grid->offset &= term->grid->num_rows - 1; if (likely(view_follows)) { + term_damage_scroll(term, DAMAGE_SCROLL, region, rows); selection_view_down(term, term->grid->offset); term->grid->view = term->grid->offset; } else if (unlikely(rows > view_sb_start_distance)) { @@ -2737,13 +2738,12 @@ term_scroll_partial(struct terminal *term, struct scroll_region region, int rows erase_line(term, row); } + term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row); + #if defined(_DEBUG) for (int r = 0; r < term->rows; r++) xassert(grid_row(term->grid, r) != NULL); #endif - - term_damage_scroll(term, DAMAGE_SCROLL, region, rows); - term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row); } void @@ -2800,6 +2800,7 @@ term_scroll_reverse_partial(struct terminal *term, xassert(term->grid->offset < term->grid->num_rows); if (view_follows) { + term_damage_scroll(term, DAMAGE_SCROLL_REVERSE, region, rows); selection_view_up(term, term->grid->offset); term->grid->view = term->grid->offset; } @@ -2818,7 +2819,6 @@ term_scroll_reverse_partial(struct terminal *term, erase_line(term, row); } - term_damage_scroll(term, DAMAGE_SCROLL_REVERSE, region, rows); term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row); #if defined(_DEBUG) From 66d9b8da604e4fd3aa6ee96da3609ff6b9896f50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 16 Jun 2023 16:20:37 +0200 Subject: [PATCH 0310/1323] sixel: fix cursor positioning logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adjusts the logic that positions the text cursor after emitting a sixel, when sixel scrolling mode is *enabled*. We’ve always mimicked XTerm’s behavior. However, XTerm recently changed its behavior, to better match that of an VT382. Now, the cursor is placed *on* the last row of the sixel, instead of on a new row after the sixel. This allows applications to print sixels to the bottom row of the terminal, without causing the content to scroll. Finally, there was a bug in the horizontal positioning of the cursor; it was placed on the *first* column of the row, instead of on the first column of the sixel. --- CHANGELOG.md | 9 +++++++++ sixel.c | 37 +++++++++++++++++-------------------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 658e0c1d..a2fc9ec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,15 @@ * Opaque sixels now retain the background opacity (when current background color is the **default** background color) ([#1360][1360]). +* Text cursor’s vertical position after emitting a sixel, when sixel + scrolling is **enabled** (the default) has been updated to match + XTerm’s, and the VT382’s behavior: the cursor is positioned **on** + the last sixel row, rather than _after_ it. This allows printing + sixels on the last row without scrolling up, but also means + applications may have to explicitly emit a newline to ensure the + sixel is visible. For example, `cat`:ing a sixel in the shell will + typically result in the last row not being visible, unless a newline + is explicitly added. [1371]: https://codeberg.org/dnkl/foot/pulls/1371 [1360]: https://codeberg.org/dnkl/foot/issues/1360 diff --git a/sixel.c b/sixel.c index 70ec2ee5..1af214a9 100644 --- a/sixel.c +++ b/sixel.c @@ -1043,6 +1043,23 @@ sixel_unhook(struct terminal *term) pixel_rows_left -= height; rows_avail -= image.rows; + if (do_scroll) { + /* Yes, truncate last row. This matches XTerm’s, and VT382’s behavior */ + const int linefeed_count = image.height / term->cell_height; + for (size_t i = 0; i < linefeed_count; i++) + term_linefeed(term); + + /* Position text cursor if this is the last image chunk */ + if (rows_avail == 0) { + term_cursor_to( + term, + term->grid->cursor.point.row, + (term->sixel.cursor_right_of_graphics + ? min(image.pos.col + image.cols, term->cols - 1) + : image.pos.col)); + } + } + /* Dirty touched cells, and scroll terminal content if necessary */ for (size_t i = 0; i < image.rows; i++) { struct row *row = term->grid->rows[cur_row + i]; @@ -1055,26 +1072,6 @@ sixel_unhook(struct terminal *term) row->cells[col].attrs.clean = 0; } - if (do_scroll) { - /* - * Linefeed, *unless* we're on the very last row of - * the final image (not just this chunk) and private - * mode 8452 (leave cursor at the right of graphics) - * is enabled. - */ - if (term->sixel.cursor_right_of_graphics && - rows_avail == 0 && - i >= image.rows - 1) - { - term_cursor_to( - term, - term->grid->cursor.point.row, - min(image.pos.col + image.cols, term->cols - 1)); - } else { - term_linefeed(term); - term_carriage_return(term); - } - } } _sixel_overwrite_by_rectangle( From d6d143e2a6916178d1ea74551617cf8a5cddc39e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 19 Jun 2023 19:06:38 +0200 Subject: [PATCH 0311/1323] sixel: respect sixel aspect ratio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit That is, parse P1 when initializing a new sixel, and don’t ignore pad/pad in the raster attributes command. The default aspect ratio is 2:1, but most sixels will override it in the raster attributes command (to 1:1). --- CHANGELOG.md | 3 +++ sixel.c | 54 ++++++++++++++++++++++++++++++++++++++-------------- terminal.h | 9 +++++++++ 3 files changed, 52 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2fc9ec8..663d3983 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ * Meson option `utmp-backend=none|libutempter|ulog|auto`. The default is `auto`, which will select `libutempter` on Linux, `ulog` on FreeBSD, and `none` for all others. +* Sixel aspect ratio. ### Changed @@ -74,6 +75,8 @@ sixel is visible. For example, `cat`:ing a sixel in the shell will typically result in the last row not being visible, unless a newline is explicitly added. +* Default sixel aspect ratio is now 2:1 instead of 1:1. + [1371]: https://codeberg.org/dnkl/foot/pulls/1371 [1360]: https://codeberg.org/dnkl/foot/issues/1360 diff --git a/sixel.c b/sixel.c index 1af214a9..ed165aed 100644 --- a/sixel.c +++ b/sixel.c @@ -38,18 +38,33 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) xassert(term->sixel.image.data == NULL); xassert(term->sixel.palette_size <= SIXEL_MAX_COLORS); + /* Default aspect ratio is 2:1 */ + const int pad = 1; + const int pan = + (p1 == 2) ? 5 : + (p1 == 3 || p1 == 4) ? 3 : + (p1 == 7 || p1 == 8 || p1 == 9) ? 1 : 2; + + LOG_DBG("initializing sixel with " + "p1=%d (pan=%d, pad=%d, AR=%d:%d), " + "p2=%d (transparent=%d), " + "p3=%d (ignored)", + p1, pan, pad, pan, pad, p2, p2 == 1, p3); + term->sixel.state = SIXEL_DECSIXEL; term->sixel.pos = (struct coord){0, 0}; term->sixel.max_non_empty_row_no = -1; term->sixel.row_byte_ofs = 0; term->sixel.color_idx = 0; + term->sixel.pan = pan; + term->sixel.pad = pad; term->sixel.param = 0; term->sixel.param_idx = 0; memset(term->sixel.params, 0, sizeof(term->sixel.params)); term->sixel.transparent_bg = p2 == 1; - term->sixel.image.data = xmalloc(1 * 6 * sizeof(term->sixel.image.data[0])); - term->sixel.image.width = 1; - term->sixel.image.height = 6; + term->sixel.image.data = NULL; + term->sixel.image.width = 0; + term->sixel.image.height = 6 * pan; /* TODO: default palette */ @@ -104,9 +119,6 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) ? 0x00000000u : bg; - for (size_t i = 0; i < 1 * 6; i++) - term->sixel.image.data[i] = term->sixel.default_bg; - count = 0; } @@ -1045,7 +1057,7 @@ sixel_unhook(struct terminal *term) if (do_scroll) { /* Yes, truncate last row. This matches XTerm’s, and VT382’s behavior */ - const int linefeed_count = image.height / term->cell_height; + const int linefeed_count = (image.height - 6 * term->sixel.pan + 1) / term->cell_height; for (size_t i = 0; i < linefeed_count; i++) term_linefeed(term); @@ -1131,7 +1143,8 @@ resize_horizontally(struct terminal *term, int new_width) const int old_width = term->sixel.image.width; const int height = term->sixel.image.height; - int alloc_height = (height + 6 - 1) / 6 * 6; + const int sixel_row_height = 6 * term->sixel.pan; + int alloc_height = (height + sixel_row_height - 1) / sixel_row_height * sixel_row_height; xassert(new_width > 0); xassert(alloc_height > 0); @@ -1176,9 +1189,14 @@ resize_vertically(struct terminal *term, int new_height) int alloc_height = (new_height + 6 - 1) / 6 * 6; - xassert(width > 0); xassert(new_height > 0); + if (unlikely(width == 0)) { + xassert(term->sixel.image.data == NULL); + term->sixel.image.height = new_height; + return true; + } + uint32_t *new_data = realloc( old_data, width * alloc_height * sizeof(uint32_t)); @@ -1283,7 +1301,7 @@ sixel_add(struct terminal *term, int col, int width, uint32_t color, uint8_t six int max_non_empty_row = -1; int row = term->sixel.pos.row; - for (int i = 0; i < 6; i++, sixel >>= 1, data += width) { + for (int i = 0; i < 6 * term->sixel.pan; i++, sixel >>= 1, data += width) { if (sixel & 1) { *data = color; max_non_empty_row = row + i; @@ -1303,6 +1321,8 @@ sixel_add_many(struct terminal *term, uint8_t c, unsigned count) int col = term->sixel.pos.col; int width = term->sixel.image.width; + count *= term->sixel.pad; + if (unlikely(col + count - 1 >= width)) { resize_horizontally(term, col + count); width = term->sixel.image.width; @@ -1352,13 +1372,13 @@ decsixel(struct terminal *term, uint8_t c) break; case '-': - term->sixel.pos.row += 6; + 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.row_byte_ofs += term->sixel.image.width * 6 * term->sixel.pan; if (term->sixel.pos.row >= term->sixel.image.height) { - if (!resize_vertically(term, term->sixel.pos.row + 6)) - term->sixel.pos.col = term->sixel.max_width + 1; + if (!resize_vertically(term, term->sixel.pos.row + 6 * term->sixel.pan)) + term->sixel.pos.col = term->sixel.max_width + 1 * term->sixel.pad; } break; @@ -1415,6 +1435,12 @@ decgra(struct terminal *term, uint8_t c) pan = pan > 0 ? pan : 1; pad = pad > 0 ? pad : 1; + pv *= pan; + ph *= pad; + + term->sixel.pan = pan; + term->sixel.pad = pad; + LOG_DBG("pan=%u, pad=%u (aspect ratio = %u), size=%ux%u", pan, pad, pan / pad, ph, pv); diff --git a/terminal.h b/terminal.h index ec0db44b..0df9c5b6 100644 --- a/terminal.h +++ b/terminal.h @@ -634,6 +634,15 @@ struct terminal { int height; /* Image height, in pixels */ } image; + /* + * Pan is the vertical shape of a pixel + * Pad is the horizontal shape of a pixel + * + * pan/pad is the sixel’s aspect ratio + */ + int pan; + int pad; + bool scrolling:1; /* Private mode 80 */ bool use_private_palette:1; /* Private mode 1070 */ bool cursor_right_of_graphics:1; /* Private mode 8452 */ From 774570ec41a3c585f40b03467eaada786a2e97d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 19 Jun 2023 19:09:58 +0200 Subject: [PATCH 0312/1323] sixel: stop cropping images to the last non-transparent row --- CHANGELOG.md | 1 + sixel.c | 21 --------------------- terminal.h | 1 - 3 files changed, 1 insertion(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 663d3983..943dd411 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,7 @@ typically result in the last row not being visible, unless a newline is explicitly added. * Default sixel aspect ratio is now 2:1 instead of 1:1. +* Sixel images are no longer cropped to the last non-transparent row. [1371]: https://codeberg.org/dnkl/foot/pulls/1371 diff --git a/sixel.c b/sixel.c index ed165aed..71c4f2c0 100644 --- a/sixel.c +++ b/sixel.c @@ -53,7 +53,6 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) term->sixel.state = SIXEL_DECSIXEL; term->sixel.pos = (struct coord){0, 0}; - term->sixel.max_non_empty_row_no = -1; term->sixel.row_byte_ofs = 0; term->sixel.color_idx = 0; term->sixel.pan = pan; @@ -952,13 +951,6 @@ sixel_reflow(struct terminal *term) void sixel_unhook(struct terminal *term) { - if (term->sixel.image.height > term->sixel.max_non_empty_row_no + 1) { - LOG_DBG( - "last row only partially filled, reducing image height: %d -> %d", - term->sixel.image.height, term->sixel.max_non_empty_row_no + 1); - term->sixel.image.height = term->sixel.max_non_empty_row_no + 1; - } - int pixel_row_idx = 0; int pixel_rows_left = term->sixel.image.height; const int stride = term->sixel.image.width * sizeof(uint32_t); @@ -1298,21 +1290,13 @@ sixel_add(struct terminal *term, int col, int width, uint32_t color, uint8_t six size_t ofs = term->sixel.row_byte_ofs + col; uint32_t *data = &term->sixel.image.data[ofs]; - int max_non_empty_row = -1; - int row = term->sixel.pos.row; - for (int i = 0; i < 6 * term->sixel.pan; i++, sixel >>= 1, data += width) { if (sixel & 1) { *data = color; - max_non_empty_row = row + i; } } xassert(sixel == 0); - - term->sixel.max_non_empty_row_no = max( - term->sixel.max_non_empty_row_no, - max_non_empty_row); } static void @@ -1448,11 +1432,6 @@ decgra(struct terminal *term, uint8_t c) ph <= term->sixel.max_height && pv <= term->sixel.max_width) { resize(term, ph, pv); - - /* This ensures the sixel’s final image size is *at least* - * this large */ - term->sixel.max_non_empty_row_no = - min(pv, term->sixel.image.height) - 1; } term->sixel.state = SIXEL_DECSIXEL; diff --git a/terminal.h b/terminal.h index 0df9c5b6..e239e2af 100644 --- a/terminal.h +++ b/terminal.h @@ -620,7 +620,6 @@ struct terminal { } state; struct coord pos; /* Current sixel coordinate */ - int max_non_empty_row_no; size_t row_byte_ofs; /* Byte position into image, for current row */ int color_idx; /* Current palette index */ uint32_t *private_palette; /* Private palette, used when private mode 1070 is enabled */ From 1eb90b2405a010a6e545ef4160cbfdb2fc3e503b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 20 Jun 2023 12:58:35 +0200 Subject: [PATCH 0313/1323] sixel: minor fixes after implementing support for non-1:1 aspect ratios * Lazy initialize image height. This is necessary to prevent garbage from being rendered for "empty" sixels. * Fix plotting of non-1:1 pixels * Fix calculation of height in resize(), for non-1:1 aspect ratios --- sixel.c | 46 +++++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/sixel.c b/sixel.c index 71c4f2c0..a40a54a4 100644 --- a/sixel.c +++ b/sixel.c @@ -46,10 +46,10 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) (p1 == 7 || p1 == 8 || p1 == 9) ? 1 : 2; LOG_DBG("initializing sixel with " - "p1=%d (pan=%d, pad=%d, AR=%d:%d), " - "p2=%d (transparent=%d), " + "p1=%d (pan=%d, pad=%d, aspect-ratio=%d:%d), " + "p2=%d (transparent=%s), " "p3=%d (ignored)", - p1, pan, pad, pan, pad, p2, p2 == 1, p3); + p1, pan, pad, pan, pad, p2, p2 == 1 ? "yes" : "no", p3); term->sixel.state = SIXEL_DECSIXEL; term->sixel.pos = (struct coord){0, 0}; @@ -63,7 +63,7 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) term->sixel.transparent_bg = p2 == 1; term->sixel.image.data = NULL; term->sixel.image.width = 0; - term->sixel.image.height = 6 * pan; + term->sixel.image.height = 0; /* TODO: default palette */ @@ -1119,10 +1119,6 @@ sixel_unhook(struct terminal *term) static void resize_horizontally(struct terminal *term, int new_width) { - LOG_DBG("resizing image horizontally: %dx(%d) -> %dx(%d)", - term->sixel.image.width, term->sixel.image.height, - new_width, term->sixel.image.height); - if (unlikely(new_width > term->sixel.max_width)) { LOG_WARN("maximum image dimensions exceeded, truncating"); new_width = term->sixel.max_width; @@ -1131,11 +1127,23 @@ resize_horizontally(struct terminal *term, int new_width) if (unlikely(term->sixel.image.width == new_width)) return; + const int sixel_row_height = 6 * term->sixel.pan; + uint32_t *old_data = term->sixel.image.data; const int old_width = term->sixel.image.width; - const int height = term->sixel.image.height; - const int sixel_row_height = 6 * term->sixel.pan; + 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; + } else + height = term->sixel.image.height; + + LOG_DBG("resizing image horizontally: %dx(%d) -> %dx(%d)", + term->sixel.image.width, term->sixel.image.height, + new_width, height); + int alloc_height = (height + sixel_row_height - 1) / sixel_row_height * sixel_row_height; xassert(new_width > 0); @@ -1231,10 +1239,11 @@ resize(struct terminal *term, int new_width, int new_height) const int old_width = term->sixel.image.width; const int old_height = term->sixel.image.height; + const int sixel_row_height = 6 * term->sixel.pan; int alloc_new_width = new_width; - int alloc_new_height = (new_height + 6 - 1) / 6 * 6; + 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 < 6); + xassert(alloc_new_height - new_height < sixel_row_height); uint32_t *new_data = NULL; uint32_t bg = term->sixel.default_bg; @@ -1287,13 +1296,16 @@ sixel_add(struct terminal *term, int col, int width, uint32_t color, uint8_t six 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 * term->sixel.pan; i++, sixel >>= 1, data += width) { + for (int i = 0; i < 6; i++, sixel >>= 1) { if (sixel & 1) { - *data = color; - } + for (int r = 0; r < pan; r++, data += width) + *data = color; + } else + data += width * pan; } xassert(sixel == 0); @@ -1425,8 +1437,8 @@ decgra(struct terminal *term, uint8_t c) term->sixel.pan = pan; term->sixel.pad = pad; - LOG_DBG("pan=%u, pad=%u (aspect ratio = %u), size=%ux%u", - pan, pad, pan / pad, ph, pv); + LOG_DBG("pan=%u, pad=%u (aspect ratio = %d:%d), size=%ux%u", + pan, pad, pan, pad, ph, pv); if (ph >= term->sixel.image.height && pv >= term->sixel.image.width && ph <= term->sixel.max_height && pv <= term->sixel.max_width) From 5d576fccbaef3e413bcb5b7a98708d2f1904f5d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 20 Jun 2023 14:52:17 +0200 Subject: [PATCH 0314/1323] sixel: regression: linefeed count for chunked up sixel image All image chunks but the last *should* scroll the screen content. --- sixel.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sixel.c b/sixel.c index a40a54a4..29585d71 100644 --- a/sixel.c +++ b/sixel.c @@ -1049,7 +1049,12 @@ sixel_unhook(struct terminal *term) if (do_scroll) { /* Yes, truncate last row. This matches XTerm’s, and VT382’s behavior */ - const int linefeed_count = (image.height - 6 * term->sixel.pan + 1) / term->cell_height; + const int linefeed_count = rows_avail == 0 + ? (image.height - 6 * term->sixel.pan + 1) / term->cell_height + : image.height / term->cell_height; + + xassert(rows_avail == 0 || image.height % term->cell_height == 0); + for (size_t i = 0; i < linefeed_count; i++) term_linefeed(term); From 425cf894d4db5401e069d5f1c76652eb822c131c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 21 Jun 2023 11:39:54 +0200 Subject: [PATCH 0315/1323] sixel: resize(): handle no size change --- sixel.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sixel.c b/sixel.c index 29585d71..4a8a4e9c 100644 --- a/sixel.c +++ b/sixel.c @@ -1244,6 +1244,9 @@ resize(struct terminal *term, int new_width, int new_height) const int old_width = term->sixel.image.width; const int old_height = term->sixel.image.height; + 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; From c15e75357a3823f264c798e87cf4d7fa802e4c92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 22 Jun 2023 22:01:51 +0200 Subject: [PATCH 0316/1323] sixel: ensure enough rows have been scrolled in, to fit the image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When emitting a sixel, we need to: a) scroll terminal content to ensure the new image fits b) position the text cursor Recent changes in the cursor positioning logic meant we reduced the number of linefeeds, to ensure 1) sixels could be printed to the bottom row without scrolling the terminal contents, and 2) the cursor was positioned on the last sixel row. Except, we’re not actually positioning the cursor on the last sixel row. We’re positioning it on the text row that maps to the *upper* pixel of the last sixel. In most cases, this _is_ the last row of the sixel. But for certain combinations of font and image sizes, it may be higher up. This patch fixes a regression, where the terminal contents weren’t scrolled up enough for certain images, causing a crash when trying to dirty a not-yet scrolled in row. The fix is this: * Always scroll by the number of rows occupied by the image, minus one. This ensures the image "fits". * Adjust the cursor position, if necessary. --- sixel.c | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/sixel.c b/sixel.c index 4a8a4e9c..3334c085 100644 --- a/sixel.c +++ b/sixel.c @@ -1048,10 +1048,17 @@ sixel_unhook(struct terminal *term) rows_avail -= image.rows; if (do_scroll) { - /* Yes, truncate last row. This matches XTerm’s, and VT382’s behavior */ + /* + * Linefeeds - always one less than the number of rows + * occupied by the image. + * + * Unless this is *not* the last chunk. In that case, + * linefeed past the chunk, so that the next chunk + * "starts" at a "new" row. + */ const int linefeed_count = rows_avail == 0 - ? (image.height - 6 * term->sixel.pan + 1) / term->cell_height - : image.height / term->cell_height; + ? max(0, image.rows - 1) + : image.rows; xassert(rows_avail == 0 || image.height % term->cell_height == 0); @@ -1060,9 +1067,28 @@ sixel_unhook(struct terminal *term) /* Position text cursor if this is the last image chunk */ if (rows_avail == 0) { + int row = term->grid->cursor.point.row; + + /* + * Position the text cursor based on the **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. + */ + const int sixel_row_height = 6 * term->sixel.pan; + const int sixel_rows = (image.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; + + row -= (image.rows - term_rows); + term_cursor_to( term, - term->grid->cursor.point.row, + max(0, row), (term->sixel.cursor_right_of_graphics ? min(image.pos.col + image.cols, term->cols - 1) : image.pos.col)); From 2388015b105f420f42161b5a625dda8f6b9ebdb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 22 Jun 2023 22:12:02 +0200 Subject: [PATCH 0317/1323] sixel: assert upper pixel of last sixel maps to last image row, *or lower* --- sixel.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sixel.c b/sixel.c index 3334c085..b0746a9a 100644 --- a/sixel.c +++ b/sixel.c @@ -1084,6 +1084,8 @@ sixel_unhook(struct terminal *term) 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; + xassert(term_rows <= image.rows); + row -= (image.rows - term_rows); term_cursor_to( From d63a00a649c83d602b8914164251296c77029e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Jun 2023 20:15:36 +0200 Subject: [PATCH 0318/1323] config: unittest: explicitly call fcft_init() + fcft_fini() This plugs a memory leak, caused by fontconfig functions being called as part of the unit test implicitly allocating global objects. --- config.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config.c b/config.c index 1d9f200b..aba85db3 100644 --- a/config.c +++ b/config.c @@ -3349,6 +3349,8 @@ UNITTEST user_notifications_t nots = tll_init(); config_override_t overrides = tll_init(); + fcft_init(FCFT_LOG_COLORIZE_NEVER, false, FCFT_LOG_CLASS_NONE); + bool ret = config_load(&original, "/dev/null", ¬s, &overrides, false, false); xassert(ret); @@ -3360,6 +3362,8 @@ UNITTEST config_free(clone); free(clone); + fcft_fini(); + tll_free(overrides); tll_free(nots); } From 1dddb63d9fade867f5493903da67a46f45232ec8 Mon Sep 17 00:00:00 2001 From: Vladimir Bauer Date: Tue, 27 Jun 2023 17:00:31 +0500 Subject: [PATCH 0319/1323] correct csd section entry: hide-when-maximized --- foot.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foot.ini b/foot.ini index fcaef4a9..a4b91ef7 100644 --- a/foot.ini +++ b/foot.ini @@ -119,7 +119,7 @@ # size=26 # font= # color= -# hide-when-typing=no +# hide-when-maximized=no # border-width=0 # border-color= # button-width=26 From 1e6204e1ac3e153fc4eb95a78cb0710803c93130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 8 Mar 2023 10:43:30 +0100 Subject: [PATCH 0320/1323] meson: generate bindings for wp-fractional-scale + wp-viewport --- meson.build | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/meson.build b/meson.build index 29951541..3bad7ab2 100644 --- a/meson.build +++ b/meson.build @@ -158,6 +158,11 @@ if wayland_protocols.version().version_compare('>=1.21') add_project_arguments('-DHAVE_XDG_ACTIVATION', language: 'c') wl_proto_xml += [wayland_protocols_datadir + '/staging/xdg-activation/xdg-activation-v1.xml'] endif +if wayland_protocols.version().version_compare('>=1.31') + add_project_arguments('-DHAVE_FRACTIONAL_SCALE', language: 'c') + wl_proto_xml += [wayland_protocols_datadir + '/stable/viewporter/viewporter.xml'] + wl_proto_xml += [wayland_protocols_datadir + '/staging/fractional-scale/fractional-scale-v1.xml'] +endif foreach prot : wl_proto_xml wl_proto_headers += custom_target( From a9ecf1449e731c2461ced75cc9c16247523aca47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 8 Mar 2023 10:44:03 +0100 Subject: [PATCH 0321/1323] wayland: plumbing for wp-fractional-scale * Bind the wp-viewporter and wp-fractional-scale-manager globals. * Create a viewport and fractional-scale when instantiating a window. * Add fractional-scale listener (that does nothing at the moment). * Destroy everything on teardown. --- wayland.c | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ wayland.h | 16 +++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/wayland.c b/wayland.c index 51161cf0..7aa48e71 100644 --- a/wayland.c +++ b/wayland.c @@ -1121,6 +1121,27 @@ handle_global(void *data, struct wl_registry *registry, } #endif +#if defined(HAVE_FRACTIONAL_SCALE) + else if (strcmp(interface, wp_viewporter_interface.name) == 0) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->viewporter = wl_registry_bind( + wayl->registry, name, &wp_viewporter_interface, required); + } + + else if (strcmp(interface, wp_fractional_scale_manager_v1_interface.name) == 0) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->fractional_scale_manager = wl_registry_bind( + wayl->registry, name, + &wp_fractional_scale_manager_v1_interface, required); + } +#endif + #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED else if (strcmp(interface, zwp_text_input_manager_v3_interface.name) == 0) { const uint32_t required = 1; @@ -1435,6 +1456,12 @@ wayl_destroy(struct wayland *wayl) zwp_text_input_manager_v3_destroy(wayl->text_input_manager); #endif +#if defined(HAVE_FRACTIONAL_SCALE) + if (wayl->fractional_scale_manager != NULL) + wp_fractional_scale_manager_v1_destroy(wayl->fractional_scale_manager); + if (wayl->viewporter != NULL) + wp_viewporter_destroy(wayl->viewporter); +#endif #if defined(HAVE_XDG_ACTIVATION) if (wayl->xdg_activation != NULL) xdg_activation_v1_destroy(wayl->xdg_activation); @@ -1469,6 +1496,21 @@ wayl_destroy(struct wayland *wayl) free(wayl); } +#if defined(HAVE_FRACTIONAL_SCALE) +static void fractional_scale_preferred_scale( + void *data, struct wp_fractional_scale_v1 *wp_fractional_scale_v1, + uint32_t scale) +{ + struct wl_window *win = data; + win->scale = (float)scale / 120.; + LOG_DBG("fractional scale: %.3f", win->scale); +} + +static const struct wp_fractional_scale_v1_listener fractional_scale_listener = { + .preferred_scale = &fractional_scale_preferred_scale, +}; +#endif + struct wl_window * wayl_win_init(struct terminal *term, const char *token) { @@ -1499,6 +1541,19 @@ wayl_win_init(struct terminal *term, const char *token) wl_surface_add_listener(win->surface, &surface_listener, win); +#if defined(HAVE_FRACTIONAL_SCALE) + if (wayl->fractional_scale_manager != NULL && wayl->viewporter != NULL) { + LOG_ERR("LDKJFLDF"); + win->viewport = wp_viewporter_get_viewport(wayl->viewporter, win->surface); + + win->fractional_scale = + wp_fractional_scale_manager_v1_get_fractional_scale( + wayl->fractional_scale_manager, win->surface); + wp_fractional_scale_v1_add_listener( + win->fractional_scale, &fractional_scale_listener, win); + } +#endif + win->xdg_surface = xdg_wm_base_get_xdg_surface(wayl->shell, win->surface); xdg_surface_add_listener(win->xdg_surface, &xdg_surface_listener, win); @@ -1652,6 +1707,12 @@ wayl_win_destroy(struct wl_window *win) tll_remove(win->xdg_tokens, it); } +#endif +#if defined(HAVE_FRACTIONAL_SCALE) + if (win->fractional_scale != NULL) + wp_fractional_scale_v1_destroy(win->fractional_scale); + if (win->viewport != NULL) + wp_viewport_destroy(win->viewport); #endif if (win->frame_callback != NULL) wl_callback_destroy(win->frame_callback); diff --git a/wayland.h b/wayland.h index 0d627052..20edcb68 100644 --- a/wayland.h +++ b/wayland.h @@ -20,6 +20,11 @@ #include #endif +#if defined(HAVE_FRACTIONAL_SCALE) + #include + #include +#endif + #include #include @@ -326,9 +331,15 @@ struct wl_window { #if defined(HAVE_XDG_ACTIVATION) tll(struct xdg_activation_token_context *) xdg_tokens; bool urgency_token_is_pending; +#endif +#if defined(HAVE_FRACTIONAL_SCALE) + struct wp_viewport *viewport; + struct wp_fractional_scale_v1 *fractional_scale; #endif bool unmapped; + float scale; + struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration; enum csd_mode csd_mode; @@ -414,6 +425,11 @@ struct wayland { struct zwp_text_input_manager_v3 *text_input_manager; #endif +#if defined(HAVE_FRACTIONAL_SCALE) + struct wp_viewporter *viewporter; + struct wp_fractional_scale_manager_v1 *fractional_scale_manager; +#endif + bool have_argb8888; tll(struct monitor) monitors; /* All available outputs */ tll(struct seat) seats; From c1f374cc8dab121388669cee6df3e2d1c483d77f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 22 Jun 2023 14:21:51 +0200 Subject: [PATCH 0322/1323] =?UTF-8?q?term:=20convert=20=E2=80=98scale?= =?UTF-8?q?=E2=80=99=20to=20a=20float?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- csi.c | 20 ++++++++++++-------- render.c | 24 ++++++++++++------------ terminal.h | 2 +- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/csi.c b/csi.c index ef1a28f2..153a1099 100644 --- a/csi.c +++ b/csi.c @@ -1206,8 +1206,10 @@ csi_dispatch(struct terminal *term, uint8_t final) if (width >= 0 && height >= 0) { char reply[64]; - size_t n = xsnprintf(reply, sizeof(reply), "\033[4;%d;%dt", - height / term->scale, width / term->scale); + size_t n = xsnprintf( + reply, sizeof(reply), "\033[4;%d;%dt", + (int)round(height / term->scale), + (int)(width / term->scale)); term_to_slave(term, reply, n); } break; @@ -1229,9 +1231,10 @@ csi_dispatch(struct terminal *term, uint8_t final) case 16: { /* report cell size in pixels */ char reply[64]; - size_t n = xsnprintf(reply, sizeof(reply), "\033[6;%d;%dt", - term->cell_height / term->scale, - term->cell_width / term->scale); + size_t n = xsnprintf( + reply, sizeof(reply), "\033[6;%d;%dt", + (int)round(term->cell_height / term->scale), + (int)round(term->cell_width / term->scale)); term_to_slave(term, reply, n); break; } @@ -1247,9 +1250,10 @@ csi_dispatch(struct terminal *term, uint8_t final) case 19: { /* report screen size in chars */ tll_foreach(term->window->on_outputs, it) { char reply[64]; - size_t n = xsnprintf(reply, sizeof(reply), "\033[9;%d;%dt", - it->item->dim.px_real.height / term->cell_height / term->scale, - it->item->dim.px_real.width / term->cell_width / term->scale); + size_t n = xsnprintf( + reply, sizeof(reply), "\033[9;%d;%dt", + (int)round(it->item->dim.px_real.height / term->cell_height / term->scale), + (int)round(it->item->dim.px_real.width / term->cell_width / term->scale)); term_to_slave(term, reply, n); break; } diff --git a/render.c b/render.c index 1858467d..f5438e42 100644 --- a/render.c +++ b/render.c @@ -1830,8 +1830,8 @@ get_csd_data(const struct terminal *term, enum csd_surface surf_idx) static void csd_commit(struct terminal *term, struct wl_surface *surf, struct buffer *buf) { - xassert(buf->width % term->scale == 0); - xassert(buf->height % term->scale == 0); + xassert(buf->width % (int)term->scale == 0); + xassert(buf->height % (int)term->scale == 0); wl_surface_attach(surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer(surf, 0, 0, buf->width, buf->height); @@ -1926,8 +1926,8 @@ render_osd(struct terminal *term, pixman_image_unref(src); pixman_image_set_clip_region32(buf->pix[0], NULL); - xassert(buf->width % term->scale == 0); - xassert(buf->height % term->scale == 0); + xassert(buf->width % (int)term->scale == 0); + xassert(buf->height % (int)term->scale == 0); quirk_weston_subsurface_desync_on(sub_surf); wl_surface_attach(surf, buf->wl_buf, 0, 0); @@ -1955,8 +1955,8 @@ render_csd_title(struct terminal *term, const struct csd_data *info, if (info->width == 0 || info->height == 0) return; - xassert(info->width % term->scale == 0); - xassert(info->height % term->scale == 0); + xassert(info->width % (int)term->scale == 0); + xassert(info->height % (int)term->scale == 0); uint32_t bg = term->conf->csd.color.title_set ? term->conf->csd.color.title @@ -2000,8 +2000,8 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, if (info->width == 0 || info->height == 0) return; - xassert(info->width % term->scale == 0); - xassert(info->height % term->scale == 0); + xassert(info->width % (int)term->scale == 0); + xassert(info->height % (int)term->scale == 0); { pixman_color_t color = color_hex_to_pixman_with_alpha(0, 0); @@ -2289,8 +2289,8 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx, if (info->width == 0 || info->height == 0) return; - xassert(info->width % term->scale == 0); - xassert(info->height % term->scale == 0); + xassert(info->width % (int)term->scale == 0); + xassert(info->height % (int)term->scale == 0); uint32_t _color; uint16_t alpha = 0xffff; @@ -3067,8 +3067,8 @@ grid_render(struct terminal *term) term->window->surface, 0, 0, INT32_MAX, INT32_MAX); } - xassert(buf->width % term->scale == 0); - xassert(buf->height % term->scale == 0); + xassert(buf->width % (int)term->scale == 0); + xassert(buf->height % (int)term->scale == 0); wl_surface_attach(term->window->surface, buf->wl_buf, 0, 0); wl_surface_commit(term->window->surface); diff --git a/terminal.h b/terminal.h index e239e2af..220070e1 100644 --- a/terminal.h +++ b/terminal.h @@ -454,7 +454,7 @@ struct terminal { int fd; } blink; - int scale; + float scale; int width; /* pixels */ int height; /* pixels */ int stashed_width; From 6e2a47287aa5160134a8bb75f562af5c85f3aaf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 22 Jun 2023 14:23:53 +0200 Subject: [PATCH 0323/1323] wayland: pointer.scale: convert to float --- wayland.c | 4 ++-- wayland.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/wayland.c b/wayland.c index 7aa48e71..b5bca9ba 100644 --- a/wayland.c +++ b/wayland.c @@ -1733,7 +1733,7 @@ wayl_win_destroy(struct wl_window *win) } bool -wayl_reload_xcursor_theme(struct seat *seat, int new_scale) +wayl_reload_xcursor_theme(struct seat *seat, float new_scale) { if (seat->pointer.theme != NULL && seat->pointer.scale == new_scale) { /* We already have a theme loaded, and the scale hasn't changed */ @@ -1766,7 +1766,7 @@ wayl_reload_xcursor_theme(struct seat *seat, int new_scale) const char *xcursor_theme = getenv("XCURSOR_THEME"); - LOG_INFO("cursor theme: %s, size: %d, scale: %d", + LOG_INFO("cursor theme: %s, size: %d, scale: %.2f", xcursor_theme ? xcursor_theme : "(null)", xcursor_size, new_scale); diff --git a/wayland.h b/wayland.h index 20edcb68..a5316837 100644 --- a/wayland.h +++ b/wayland.h @@ -135,7 +135,7 @@ struct seat { struct wl_surface *surface; struct wl_cursor_theme *theme; struct wl_cursor *cursor; - int scale; + float scale; bool hidden; const char *xcursor; @@ -442,7 +442,7 @@ struct wayland *wayl_init( bool presentation_timings); void wayl_destroy(struct wayland *wayl); -bool wayl_reload_xcursor_theme(struct seat *seat, int new_scale); +bool wayl_reload_xcursor_theme(struct seat *seat, float new_scale); void wayl_flush(struct wayland *wayl); void wayl_roundtrip(struct wayland *wayl); From 44743b5635aeee2d9cbb2bae33d5dcbc374be8da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 22 Jun 2023 14:27:16 +0200 Subject: [PATCH 0324/1323] render: draw_unfocused_block(): round scale, instead of truncating --- render.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.c b/render.c index f5438e42..e62e000d 100644 --- a/render.c +++ b/render.c @@ -311,7 +311,7 @@ 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) { - const int scale = term->scale; + const int scale = round(term->scale); const int width = min(min(scale, term->cell_width), term->cell_height); pixman_image_fill_rectangles( From b65612479135da9bf888fcd037c86519940e17b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 22 Jun 2023 14:27:37 +0200 Subject: [PATCH 0325/1323] render: csd_border: round scaled border width, instead of truncating --- render.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/render.c b/render.c index e62e000d..f87ef093 100644 --- a/render.c +++ b/render.c @@ -2012,9 +2012,9 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, * The “visible” border. */ - int scale = term->scale; - int bwidth = term->conf->csd.border_width * scale; - int vwidth = term->conf->csd.border_width_visible * scale; /* Visible size */ + float scale = term->scale; + int bwidth = round(term->conf->csd.border_width * scale); + int vwidth = round(term->conf->csd.border_width_visible * scale); /* Visible size */ xassert(bwidth >= vwidth); @@ -2067,7 +2067,6 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, uint16_t alpha = _color >> 24 | (_color >> 24 << 8); pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha); - pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &color, 1, &(pixman_rectangle16_t){x, y, w, h}); From cf280e6655da63dd6d2c2e146ec1ff8e71235779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 22 Jun 2023 14:35:02 +0200 Subject: [PATCH 0326/1323] render: render_timer(): round scaling factor --- render.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.c b/render.c index f87ef093..51f4f1e8 100644 --- a/render.c +++ b/render.c @@ -2568,7 +2568,7 @@ render_render_timer(struct terminal *term, struct timespec render_time) char32_t text[256]; mbstoc32(text, usecs_str, ALEN(text)); - const int scale = term->scale; + const int scale = round(term->scale); const int cell_count = c32len(text); const int margin = 3 * scale; const int width = From 30c8d3e652db1cff7138e5e916cdaf2c9b6026a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 22 Jun 2023 14:35:19 +0200 Subject: [PATCH 0327/1323] render: search_box(): round scaling factor --- render.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.c b/render.c index 51f4f1e8..34648d53 100644 --- a/render.c +++ b/render.c @@ -3132,7 +3132,7 @@ render_search_box(struct terminal *term) const size_t wanted_visible_cells = max(20, total_cells); xassert(term->scale >= 1); - const int scale = term->scale; + const int scale = round(term->scale); const size_t margin = 3 * scale; From d8f64d1047415d1446dad893167d1a756ffa9a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 22 Jun 2023 14:35:29 +0200 Subject: [PATCH 0328/1323] render: urls(): round scaling factor --- render.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.c b/render.c index 34648d53..80025d5e 100644 --- a/render.c +++ b/render.c @@ -3422,7 +3422,7 @@ render_urls(struct terminal *term) struct wl_window *win = term->window; xassert(tll_length(win->urls) > 0); - const int scale = term->scale; + const int scale = round(term->scale); const int x_margin = 2 * scale; const int y_margin = 1 * scale; From 2bb7b28837aac939dad61b7ca58a0dcef08399e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 22 Jun 2023 14:37:31 +0200 Subject: [PATCH 0329/1323] =?UTF-8?q?render:=20xcursor=5Fupdate():=20conve?= =?UTF-8?q?rt=20local=20=E2=80=98scale=E2=80=99=20variable=20to=20float?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- render.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.c b/render.c index 80025d5e..25249218 100644 --- a/render.c +++ b/render.c @@ -4276,7 +4276,7 @@ render_xcursor_update(struct seat *seat) xassert(seat->pointer.cursor != NULL); - const int scale = seat->pointer.scale; + const float scale = seat->pointer.scale; struct wl_cursor_image *image = seat->pointer.cursor->images[0]; wl_surface_attach( From 424d0450840bc3db018457787fcdf93db101b265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 22 Jun 2023 14:39:34 +0200 Subject: [PATCH 0330/1323] =?UTF-8?q?term:=20reload=5Ffonts():=20=E2=80=98?= =?UTF-8?q?scale=E2=80=99=20is=20not=20a=20float?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- terminal.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/terminal.c b/terminal.c index 815dc2d4..43ba157b 100644 --- a/terminal.c +++ b/terminal.c @@ -1000,14 +1000,14 @@ reload_fonts(struct terminal *term) bool use_px_size = term->font_sizes[i][j].px_size > 0; char size[64]; - const int scale = term->font_is_sized_by_dpi ? 1 : term->scale; + const float scale = term->font_is_sized_by_dpi ? 1. : term->scale; if (use_px_size) snprintf(size, sizeof(size), ":pixelsize=%d", - term->font_sizes[i][j].px_size * scale); + (int)round(term->font_sizes[i][j].px_size * scale)); else snprintf(size, sizeof(size), ":size=%.2f", - term->font_sizes[i][j].pt_size * (double)scale); + term->font_sizes[i][j].pt_size * scale); size_t len = strlen(font->pattern) + strlen(size) + 1; names[i][j] = xmalloc(len); @@ -1232,7 +1232,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .reverse_wrap = true, .auto_margin = true, .window_title_stack = tll_init(), - .scale = 1, + .scale = 1., .flash = {.fd = flash_fd}, .blink = {.fd = -1}, .vt = { From 29a14632d369c6a9d2ceea70e75f17dc47ab23ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 22 Jun 2023 14:39:49 +0200 Subject: [PATCH 0331/1323] =?UTF-8?q?wayland:=20csd=5Freload=5Ffont():=20?= =?UTF-8?q?=E2=80=98scale=E2=80=99=20is=20now=20a=20float?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wayland.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/wayland.c b/wayland.c index b5bca9ba..575d2e83 100644 --- a/wayland.c +++ b/wayland.c @@ -32,12 +32,12 @@ #include "xmalloc.h" static void -csd_reload_font(struct wl_window *win, int old_scale) +csd_reload_font(struct wl_window *win, float old_scale) { struct terminal *term = win->term; const struct config *conf = term->conf; - const int scale = term->scale; + const float scale = term->scale; bool enable_csd = win->csd_mode == CSD_YES && !win->is_fullscreen; if (!enable_csd) @@ -52,10 +52,10 @@ csd_reload_font(struct wl_window *win, int old_scale) patterns[i] = conf->csd.font.arr[i].pattern; char pixelsize[32]; - snprintf(pixelsize, sizeof(pixelsize), - "pixelsize=%u", conf->csd.title_height * scale * 1 / 2); + snprintf(pixelsize, sizeof(pixelsize), "pixelsize=%u", + (int)round(conf->csd.title_height * scale * 1 / 2)); - LOG_DBG("loading CSD font \"%s:%s\" (old-scale=%d, scale=%d)", + LOG_DBG("loading CSD font \"%s:%s\" (old-scale=%.2f, scale=%.2f)", patterns[0], pixelsize, old_scale, scale); win->csd.font = fcft_from_name(conf->csd.font.count, patterns, pixelsize); @@ -79,7 +79,7 @@ csd_instantiate(struct wl_window *win) xassert(ret); } - csd_reload_font(win, -1); + csd_reload_font(win, -1.); } static void @@ -334,7 +334,7 @@ update_term_for_output_change(struct terminal *term) if (tll_length(term->window->on_outputs) == 0) return; - int old_scale = term->scale; + float old_scale = term->scale; render_resize(term, term->width / term->scale, term->height / term->scale); term_font_dpi_changed(term, old_scale); From 913ae94cf99cb4a171ea69e6340b804a0e8c6a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 22 Jun 2023 15:01:33 +0200 Subject: [PATCH 0332/1323] wayland: add wayl_fractional_scaling() Returns true if fractional scaling is available. --- wayland.c | 10 ++++++++++ wayland.h | 2 ++ 2 files changed, 12 insertions(+) diff --git a/wayland.c b/wayland.c index 575d2e83..e01a3a50 100644 --- a/wayland.c +++ b/wayland.c @@ -2048,3 +2048,13 @@ wayl_get_activation_token( return true; } #endif + +bool +wayl_fractional_scaling(const struct wayland *wayl) +{ +#if defined(HAVE_FRACTIONAL_SCALE) + return wayl->fractional_scale_manager != NULL; +#else + return false; +#endif +} diff --git a/wayland.h b/wayland.h index a5316837..756da8d2 100644 --- a/wayland.h +++ b/wayland.h @@ -469,3 +469,5 @@ bool wayl_get_activation_token( struct wayland *wayl, struct seat *seat, uint32_t serial, struct wl_window *win, activation_token_cb_t cb, void *cb_data); #endif + +bool wayl_fractional_scaling(const struct wayland *wayl); From 4bd62b10058c626cecb5eb15c9c44aee58bf547a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 22 Jun 2023 15:01:59 +0200 Subject: [PATCH 0333/1323] =?UTF-8?q?render:=20maybe=5Fresize():=20convert?= =?UTF-8?q?=20local=20variable=20=E2=80=98scale=E2=80=99=20to=20float?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- render.c | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/render.c b/render.c index 25249218..3c2e6f18 100644 --- a/render.c +++ b/render.c @@ -3868,13 +3868,13 @@ maybe_resize(struct terminal *term, int width, int height, bool force) if (term->cell_width == 0 && term->cell_height == 0) return false; - int scale = -1; + float scale = -1; tll_foreach(term->window->on_outputs, it) { if (it->item->scale > scale) scale = it->item->scale; } - if (scale < 0) { + if (scale < 0.) { /* Haven't 'entered' an output yet? */ scale = term->scale; } @@ -3922,13 +3922,18 @@ maybe_resize(struct terminal *term, int width, int height, bool force) * Ensure we can scale to logical size, and back to * pixels without truncating. */ - if (width % scale) - width += scale - width % scale; - if (height % scale) - height += scale - height % scale; + if (wayl_fractional_scaling(term->wl)) { + xassert((int)round(scale) == (int)scale); - xassert(width % scale == 0); - xassert(height % scale == 0); + int iscale = scale; + if (width % iscale) + width += iscale - width % iscale; + if (height % iscale) + height += iscale - height % iscale; + + xassert(width % iscale == 0); + xassert(height % iscale == 0); + } break; } } From 0a5073f5703e89cfa4219d5b532cb134f9def6d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Jun 2023 15:51:04 +0200 Subject: [PATCH 0334/1323] wayland: add wayl_surface_scale(), and wayl_win_scale() These functions scale a surface+buffer. For now, only using the legacy scaling method (wl_surface_set_buffer_scale()). --- render.c | 47 +++++++++++++---------------------------------- wayland.c | 44 ++++++++++++++++++++++++++++++++++---------- wayland.h | 7 +++++-- 3 files changed, 52 insertions(+), 46 deletions(-) diff --git a/render.c b/render.c index 3c2e6f18..5663c45a 100644 --- a/render.c +++ b/render.c @@ -1691,8 +1691,8 @@ render_overlay(struct terminal *term) &(pixman_rectangle16_t){0, 0, term->width, term->height}); quirk_weston_subsurface_desync_on(overlay->sub); + wayl_surface_scale(term->wl, overlay->surf, term->scale); wl_subsurface_set_position(overlay->sub, 0, 0); - wl_surface_set_buffer_scale(overlay->surf, term->scale); wl_surface_attach(overlay->surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer( @@ -1830,12 +1830,9 @@ get_csd_data(const struct terminal *term, enum csd_surface surf_idx) static void csd_commit(struct terminal *term, struct wl_surface *surf, struct buffer *buf) { - xassert(buf->width % (int)term->scale == 0); - xassert(buf->height % (int)term->scale == 0); - + wayl_surface_scale(term->wl, surf, term->scale); wl_surface_attach(surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer(surf, 0, 0, buf->width, buf->height); - wl_surface_set_buffer_scale(surf, term->scale); wl_surface_commit(surf); } @@ -1926,13 +1923,10 @@ render_osd(struct terminal *term, pixman_image_unref(src); pixman_image_set_clip_region32(buf->pix[0], NULL); - xassert(buf->width % (int)term->scale == 0); - xassert(buf->height % (int)term->scale == 0); - quirk_weston_subsurface_desync_on(sub_surf); + wayl_surface_scale(term->wl, surf, term->scale); wl_surface_attach(surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer(surf, 0, 0, buf->width, buf->height); - wl_surface_set_buffer_scale(surf, term->scale); struct wl_region *region = wl_compositor_create_region(term->wl->compositor); if (region != NULL) { @@ -1955,9 +1949,6 @@ render_csd_title(struct terminal *term, const struct csd_data *info, if (info->width == 0 || info->height == 0) return; - xassert(info->width % (int)term->scale == 0); - xassert(info->height % (int)term->scale == 0); - uint32_t bg = term->conf->csd.color.title_set ? term->conf->csd.color.title : 0xffu << 24 | term->conf->colors.fg; @@ -2000,9 +1991,6 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, if (info->width == 0 || info->height == 0) return; - xassert(info->width % (int)term->scale == 0); - xassert(info->height % (int)term->scale == 0); - { pixman_color_t color = color_hex_to_pixman_with_alpha(0, 0); render_csd_part(term, surf, buf, info->width, info->height, &color); @@ -2288,9 +2276,6 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx, if (info->width == 0 || info->height == 0) return; - xassert(info->width % (int)term->scale == 0); - xassert(info->height % (int)term->scale == 0); - uint32_t _color; uint16_t alpha = 0xffff; bool is_active = false; @@ -3032,7 +3017,7 @@ grid_render(struct terminal *term) term->window->frame_callback = wl_surface_frame(term->window->surface); wl_callback_add_listener(term->window->frame_callback, &frame_listener, term); - wl_surface_set_buffer_scale(term->window->surface, term->scale); + wayl_win_scale(term->window); if (term->wl->presentation != NULL && term->conf->presentation_timings) { struct timespec commit_time; @@ -3066,9 +3051,6 @@ grid_render(struct terminal *term) term->window->surface, 0, 0, INT32_MAX, INT32_MAX); } - xassert(buf->width % (int)term->scale == 0); - xassert(buf->height % (int)term->scale == 0); - wl_surface_attach(term->window->surface, buf->wl_buf, 0, 0); wl_surface_commit(term->window->surface); } @@ -3132,17 +3114,17 @@ render_search_box(struct terminal *term) const size_t wanted_visible_cells = max(20, total_cells); xassert(term->scale >= 1); - const int scale = round(term->scale); + const int rounded_scale = round(term->scale); - const size_t margin = 3 * scale; + const size_t margin = 3 * rounded_scale; const size_t width = term->width - 2 * margin; const size_t visible_width = min( term->width - 2 * margin, - (2 * margin + wanted_visible_cells * term->cell_width + scale - 1) / scale * scale); + (2 * margin + wanted_visible_cells * term->cell_width + rounded_scale - 1) / rounded_scale * rounded_scale); const size_t height = min( term->height - 2 * margin, - (2 * margin + 1 * term->cell_height + scale - 1) / scale * scale); + (2 * margin + 1 * term->cell_height + rounded_scale - 1) / rounded_scale * rounded_scale); const size_t visible_cells = (visible_width - 2 * margin) / term->cell_width; size_t glyph_offset = term->render.search_glyph_offset; @@ -3389,15 +3371,12 @@ render_search_box(struct terminal *term) /* TODO: this is only necessary on a window resize */ wl_subsurface_set_position( term->window->search.sub, - margin / scale, - max(0, (int32_t)term->height - height - margin) / scale); - - xassert(buf->width % scale == 0); - xassert(buf->height % scale == 0); + margin / term->scale, + max(0, (int32_t)term->height - height - margin) / term->scale); + wayl_surface_scale(term->wl, term->window->search.surf, term->scale); wl_surface_attach(term->window->search.surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer(term->window->search.surf, 0, 0, width, height); - wl_surface_set_buffer_scale(term->window->search.surf, scale); struct wl_region *region = wl_compositor_create_region(term->wl->compositor); if (region != NULL) { @@ -4284,6 +4263,8 @@ render_xcursor_update(struct seat *seat) const float scale = seat->pointer.scale; struct wl_cursor_image *image = seat->pointer.cursor->images[0]; + wayl_surface_scale(seat->wayl, seat->pointer.surface, scale); + wl_surface_attach( seat->pointer.surface, wl_cursor_image_get_buffer(image), 0, 0); @@ -4295,8 +4276,6 @@ render_xcursor_update(struct seat *seat) wl_surface_damage_buffer( seat->pointer.surface, 0, 0, INT32_MAX, INT32_MAX); - wl_surface_set_buffer_scale(seat->pointer.surface, scale); - xassert(seat->pointer.xcursor_callback == NULL); seat->pointer.xcursor_callback = wl_surface_frame(seat->pointer.surface); wl_callback_add_listener(seat->pointer.xcursor_callback, &xcursor_listener, seat); diff --git a/wayland.c b/wayland.c index e01a3a50..5aa10966 100644 --- a/wayland.c +++ b/wayland.c @@ -1850,6 +1850,40 @@ wayl_roundtrip(struct wayland *wayl) wayl_flush(wayl); } + +bool +wayl_fractional_scaling(const struct wayland *wayl) +{ +#if defined(HAVE_FRACTIONAL_SCALE) + return wayl->fractional_scale_manager != NULL; +#else + return false; +#endif +} + +void +wayl_surface_scale(const struct wayland *wayl, struct wl_surface *surf, + float scale) +{ + LOG_WARN("scaling by a factor of %.2f (legacy)", scale); + + if (wayl_fractional_scaling(wayl)) { + BUG("not yet implemented"); + } else { + wl_surface_set_buffer_scale(surf, (int)scale); + } +} + +void +wayl_win_scale(struct wl_window *win) +{ + const struct terminal *term = win->term; + const struct wayland *wayl = term->wl; + const float scale = term->scale; + + wayl_surface_scale(wayl, win->surface, scale); +} + void wayl_win_alpha_changed(struct wl_window *win) { @@ -2048,13 +2082,3 @@ wayl_get_activation_token( return true; } #endif - -bool -wayl_fractional_scaling(const struct wayland *wayl) -{ -#if defined(HAVE_FRACTIONAL_SCALE) - return wayl->fractional_scale_manager != NULL; -#else - return false; -#endif -} diff --git a/wayland.h b/wayland.h index 756da8d2..06150328 100644 --- a/wayland.h +++ b/wayland.h @@ -447,9 +447,14 @@ bool wayl_reload_xcursor_theme(struct seat *seat, float new_scale); void wayl_flush(struct wayland *wayl); void wayl_roundtrip(struct wayland *wayl); +bool wayl_fractional_scaling(const struct wayland *wayl); +void wayl_surface_scale( + const struct wayland *wayl, struct wl_surface *surf, float scale); + struct wl_window *wayl_win_init(struct terminal *term, const char *token); void wayl_win_destroy(struct wl_window *win); +void wayl_win_scale(struct wl_window *win); void wayl_win_alpha_changed(struct wl_window *win); bool wayl_win_set_urgent(struct wl_window *win); @@ -469,5 +474,3 @@ bool wayl_get_activation_token( struct wayland *wayl, struct seat *seat, uint32_t serial, struct wl_window *win, activation_token_cb_t cb, void *cb_data); #endif - -bool wayl_fractional_scaling(const struct wayland *wayl); From c5d533ec71aafe1c842189a7e662f7a7696c56a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Jun 2023 15:55:40 +0200 Subject: [PATCH 0335/1323] wayland: add viewport object to sub-surface struct --- wayland.c | 27 +++++++++++++++++++++++++-- wayland.h | 3 +++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/wayland.c b/wayland.c index 5aa10966..9a3d11e8 100644 --- a/wayland.c +++ b/wayland.c @@ -1543,7 +1543,6 @@ wayl_win_init(struct terminal *term, const char *token) #if defined(HAVE_FRACTIONAL_SCALE) if (wayl->fractional_scale_manager != NULL && wayl->viewporter != NULL) { - LOG_ERR("LDKJFLDF"); win->viewport = wp_viewporter_get_viewport(wayl->viewporter, win->surface); win->fractional_scale = @@ -1965,17 +1964,33 @@ wayl_win_subsurface_new_with_custom_parent( struct wl_surface *main_surface = wl_compositor_create_surface(wayl->compositor); - if (main_surface == NULL) + if (main_surface == NULL) { + LOG_ERR("failed to instantiate surface for sub-surface"); return false; + } struct wl_subsurface *sub = wl_subcompositor_get_subsurface( wayl->sub_compositor, main_surface, parent); if (sub == NULL) { + LOG_ERR("failed to instantiate sub-surface"); wl_surface_destroy(main_surface); return false; } +#if defined(HAVE_FRACTIONAL_SCALE) + struct wp_viewport *viewport = NULL; + if (wayl->fractional_scale_manager != NULL && wayl->viewporter != NULL) { + viewport = wp_viewporter_get_viewport(wayl->viewporter, main_surface); + if (viewport == NULL) { + LOG_ERR("failed to instantiate viewport for sub-surface"); + wl_subsurface_destroy(sub); + wl_surface_destroy(main_surface); + return false; + } + } +#endif + wl_surface_set_user_data(main_surface, win); wl_subsurface_set_sync(sub); @@ -1989,6 +2004,9 @@ wayl_win_subsurface_new_with_custom_parent( surf->surf = main_surface; surf->sub = sub; +#if defined(HAVE_FRACTIONAL_SCALE) + surf->viewport = viewport; +#endif return true; } @@ -2005,6 +2023,11 @@ wayl_win_subsurface_destroy(struct wl_surf_subsurf *surf) { if (surf == NULL) return; + +#if defined(HAVE_FRACTIONAL_SCALE) + if (surf->viewport != NULL) + wp_viewport_destroy(surf->viewport); +#endif if (surf->sub != NULL) wl_subsurface_destroy(surf->sub); if (surf->surf != NULL) diff --git a/wayland.h b/wayland.h index 06150328..bb9bf77f 100644 --- a/wayland.h +++ b/wayland.h @@ -297,6 +297,9 @@ struct monitor { struct wl_surf_subsurf { struct wl_surface *surf; struct wl_subsurface *sub; +#if defined(HAVE_FRACTIONAL_SCALE) + struct wp_viewport *viewport; +#endif }; struct wl_url { From ba46a039aca734bb284c22ba5d5bddf5407e3848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Jun 2023 16:10:40 +0200 Subject: [PATCH 0336/1323] wayland: refactor: wrap wl_surface pointers in a wayl_surface struct And add a viewport object to accompany the surface (to be used when scaling the surface). Also rename the wl_surf_subsurf struct to wayl_sub_surface, and add a wayl_surface object to it, rather than a plain wl_surface pointer (to also get the viewport pointer). --- quirks.c | 2 +- render.c | 100 +++++++++++++++++++++--------------------- terminal.c | 18 ++++---- wayland.c | 125 ++++++++++++++++++++++++++++------------------------- wayland.h | 43 +++++++++--------- 5 files changed, 149 insertions(+), 139 deletions(-) diff --git a/quirks.c b/quirks.c index bf9bc7fb..9769f1ff 100644 --- a/quirks.c +++ b/quirks.c @@ -89,5 +89,5 @@ quirk_sway_subsurface_unmap(struct terminal *term) if (!is_sway()) return; - wl_surface_damage_buffer(term->window->surface, 0, 0, INT32_MAX, INT32_MAX); + wl_surface_damage_buffer(term->window->surface.surf, 0, 0, INT32_MAX, INT32_MAX); } diff --git a/render.c b/render.c index 5663c45a..5ebd69eb 100644 --- a/render.c +++ b/render.c @@ -905,21 +905,21 @@ render_margin(struct terminal *term, struct buffer *buf, if (apply_damage) { /* Top */ wl_surface_damage_buffer( - term->window->surface, 0, 0, term->width, term->margins.top); + term->window->surface.surf, 0, 0, term->width, term->margins.top); /* Bottom */ wl_surface_damage_buffer( - term->window->surface, 0, bmargin, term->width, term->margins.bottom); + term->window->surface.surf, 0, bmargin, term->width, term->margins.bottom); /* Left */ wl_surface_damage_buffer( - term->window->surface, + term->window->surface.surf, 0, term->margins.top + start_line * term->cell_height, term->margins.left, line_count * term->cell_height); /* Right */ wl_surface_damage_buffer( - term->window->surface, + term->window->surface.surf, rmargin, term->margins.top + start_line * term->cell_height, term->margins.right, line_count * term->cell_height); } @@ -1027,7 +1027,7 @@ grid_render_scroll(struct terminal *term, struct buffer *buf, #endif wl_surface_damage_buffer( - term->window->surface, term->margins.left, dst_y, + term->window->surface.surf, term->margins.left, dst_y, term->width - term->margins.left - term->margins.right, height); /* @@ -1104,7 +1104,7 @@ grid_render_scroll_reverse(struct terminal *term, struct buffer *buf, #endif wl_surface_damage_buffer( - term->window->surface, term->margins.left, dst_y, + term->window->surface.surf, term->margins.left, dst_y, term->width - term->margins.left - term->margins.right, height); /* @@ -1153,7 +1153,7 @@ render_sixel_chunk(struct terminal *term, pixman_image_t *pix, const struct sixe x, y, width, height); - wl_surface_damage_buffer(term->window->surface, x, y, width, height); + wl_surface_damage_buffer(term->window->surface.surf, x, y, width, height); } static void @@ -1480,7 +1480,7 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, free(real_cells); wl_surface_damage_buffer( - term->window->surface, + term->window->surface.surf, term->margins.left, term->margins.top + row_idx * term->cell_height, term->width - term->margins.left - term->margins.right, @@ -1502,7 +1502,7 @@ render_ime_preedit(struct terminal *term, struct buffer *buf) static void render_overlay(struct terminal *term) { - struct wl_surf_subsurf *overlay = &term->window->overlay; + struct wayl_sub_surface *overlay = &term->window->overlay; bool unicode_mode_active = false; /* Check if unicode mode is active on at least one seat focusing @@ -1523,8 +1523,8 @@ render_overlay(struct terminal *term) if (likely(style == OVERLAY_NONE)) { if (term->render.last_overlay_style != OVERLAY_NONE) { /* Unmap overlay sub-surface */ - wl_surface_attach(overlay->surf, NULL, 0, 0); - wl_surface_commit(overlay->surf); + wl_surface_attach(overlay->surface.surf, NULL, 0, 0); + wl_surface_commit(overlay->surface.surf); term->render.last_overlay_style = OVERLAY_NONE; term->render.last_overlay_buf = NULL; @@ -1691,17 +1691,17 @@ render_overlay(struct terminal *term) &(pixman_rectangle16_t){0, 0, term->width, term->height}); quirk_weston_subsurface_desync_on(overlay->sub); - wayl_surface_scale(term->wl, overlay->surf, term->scale); + wayl_surface_scale(term->wl, overlay->surface.surf, term->scale); wl_subsurface_set_position(overlay->sub, 0, 0); - wl_surface_attach(overlay->surf, buf->wl_buf, 0, 0); + wl_surface_attach(overlay->surface.surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer( - overlay->surf, + overlay->surface.surf, damage_bounds.x1, damage_bounds.y1, damage_bounds.x2 - damage_bounds.x1, damage_bounds.y2 - damage_bounds.y1); - wl_surface_commit(overlay->surf); + wl_surface_commit(overlay->surface.surf); quirk_weston_subsurface_desync_off(overlay->sub); buf->age = 0; @@ -1945,7 +1945,7 @@ render_csd_title(struct terminal *term, const struct csd_data *info, { xassert(term->window->csd_mode == CSD_YES); - struct wl_surf_subsurf *surf = &term->window->csd.surface[CSD_SURF_TITLE]; + struct wayl_sub_surface *surf = &term->window->csd.surface[CSD_SURF_TITLE]; if (info->width == 0 || info->height == 0) return; @@ -1971,11 +1971,11 @@ render_csd_title(struct terminal *term, const struct csd_data *info, const int margin = M != NULL ? M->advance.x : win->csd.font->max_advance.x; - render_osd(term, surf->surf, surf->sub, win->csd.font, + render_osd(term, surf->surface.surf, surf->sub, win->csd.font, buf, title_text, fg, bg, margin, (buf->height - win->csd.font->height) / 2); - csd_commit(term, surf->surf, buf); + csd_commit(term, surf->surface.surf, buf); free(_title_text); } @@ -1986,7 +1986,7 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, xassert(term->window->csd_mode == CSD_YES); xassert(surf_idx >= CSD_SURF_LEFT && surf_idx <= CSD_SURF_BOTTOM); - struct wl_surface *surf = term->window->csd.surface[surf_idx].surf; + struct wl_surface *surf = term->window->csd.surface[surf_idx].surface.surf; if (info->width == 0 || info->height == 0) return; @@ -2271,7 +2271,7 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx, xassert(term->window->csd_mode == CSD_YES); xassert(surf_idx >= CSD_SURF_MINIMIZE && surf_idx <= CSD_SURF_CLOSE); - struct wl_surface *surf = term->window->csd.surface[surf_idx].surf; + struct wl_surface *surf = term->window->csd.surface[surf_idx].surface.surf; if (info->width == 0 || info->height == 0) return; @@ -2358,7 +2358,7 @@ render_csd(struct terminal *term) const int width = infos[i].width; const int height = infos[i].height; - struct wl_surface *surf = term->window->csd.surface[i].surf; + struct wl_surface *surf = term->window->csd.surface[i].surface.surf; struct wl_subsurface *sub = term->window->csd.surface[i].sub; xassert(surf != NULL); @@ -2397,7 +2397,7 @@ render_scrollback_position(struct terminal *term) struct wl_window *win = term->window; if (term->grid->view == term->grid->offset) { - if (win->scrollback_indicator.surf != NULL) { + if (win->scrollback_indicator.surface.surf != NULL) { wayl_win_subsurface_destroy(&win->scrollback_indicator); /* Work around Sway bug - unmapping a sub-surface does not damage @@ -2407,7 +2407,7 @@ render_scrollback_position(struct terminal *term) return; } - if (win->scrollback_indicator.surf == NULL) { + if (win->scrollback_indicator.surface.surf == NULL) { if (!wayl_win_subsurface_new( win, &win->scrollback_indicator, false)) { @@ -2416,7 +2416,7 @@ render_scrollback_position(struct terminal *term) } } - xassert(win->scrollback_indicator.surf != NULL); + xassert(win->scrollback_indicator.surface.surf != NULL); xassert(win->scrollback_indicator.sub != NULL); /* Find absolute row number of the scrollback start */ @@ -2514,8 +2514,8 @@ render_scrollback_position(struct terminal *term) const int y = (term->margins.top + surf_top) / scale * scale; if (y + height > term->height) { - wl_surface_attach(win->scrollback_indicator.surf, NULL, 0, 0); - wl_surface_commit(win->scrollback_indicator.surf); + wl_surface_attach(win->scrollback_indicator.surface.surf, NULL, 0, 0); + wl_surface_commit(win->scrollback_indicator.surface.surf); return; } @@ -2534,7 +2534,7 @@ render_scrollback_position(struct terminal *term) render_osd( term, - win->scrollback_indicator.surf, + win->scrollback_indicator.surface.surf, win->scrollback_indicator.sub, term->fonts[0], buf, text, fg, 0xffu << 24 | bg, @@ -2571,7 +2571,7 @@ render_render_timer(struct terminal *term, struct timespec render_time) render_osd( term, - win->render_timer.surf, + win->render_timer.surface.surf, win->render_timer.sub, term->fonts[0], buf, text, term->colors.table[0], 0xffu << 24 | term->colors.table[8 + 1], @@ -2919,7 +2919,7 @@ grid_render(struct terminal *term) int height = (r - first_dirty_row) * term->cell_height; wl_surface_damage_buffer( - term->window->surface, x, y, width, height); + term->window->surface.surf, x, y, width, height); pixman_region32_union_rect( &buf->dirty, &buf->dirty, 0, y, buf->width, height); } @@ -2947,7 +2947,7 @@ grid_render(struct terminal *term) int width = term->width - term->margins.left - term->margins.right; int height = (term->rows - first_dirty_row) * term->cell_height; - wl_surface_damage_buffer(term->window->surface, x, y, width, height); + wl_surface_damage_buffer(term->window->surface.surf, x, y, width, height); pixman_region32_union_rect(&buf->dirty, &buf->dirty, 0, y, buf->width, height); } @@ -3014,7 +3014,7 @@ grid_render(struct terminal *term) xassert(term->grid->view >= 0 && term->grid->view < term->grid->num_rows); xassert(term->window->frame_callback == NULL); - term->window->frame_callback = wl_surface_frame(term->window->surface); + term->window->frame_callback = wl_surface_frame(term->window->surface.surf); wl_callback_add_listener(term->window->frame_callback, &frame_listener, term); wayl_win_scale(term->window); @@ -3024,7 +3024,7 @@ grid_render(struct terminal *term) clock_gettime(term->wl->presentation_clock_id, &commit_time); struct wp_presentation_feedback *feedback = wp_presentation_feedback( - term->wl->presentation, term->window->surface); + term->wl->presentation, term->window->surface.surf); if (feedback == NULL) { LOG_WARN("failed to create presentation feedback"); @@ -3048,11 +3048,11 @@ grid_render(struct terminal *term) if (term->conf->tweak.damage_whole_window) { wl_surface_damage_buffer( - term->window->surface, 0, 0, INT32_MAX, INT32_MAX); + term->window->surface.surf, 0, 0, INT32_MAX, INT32_MAX); } - wl_surface_attach(term->window->surface, buf->wl_buf, 0, 0); - wl_surface_commit(term->window->surface); + wl_surface_attach(term->window->surface.surf, buf->wl_buf, 0, 0); + wl_surface_commit(term->window->surface.surf); } static void @@ -3374,18 +3374,18 @@ render_search_box(struct terminal *term) margin / term->scale, max(0, (int32_t)term->height - height - margin) / term->scale); - wayl_surface_scale(term->wl, term->window->search.surf, term->scale); - wl_surface_attach(term->window->search.surf, buf->wl_buf, 0, 0); - wl_surface_damage_buffer(term->window->search.surf, 0, 0, width, height); + wayl_surface_scale(term->wl, term->window->search.surface.surf, term->scale); + wl_surface_attach(term->window->search.surface.surf, buf->wl_buf, 0, 0); + wl_surface_damage_buffer(term->window->search.surface.surf, 0, 0, width, height); struct wl_region *region = wl_compositor_create_region(term->wl->compositor); if (region != NULL) { wl_region_add(region, width - visible_width, 0, visible_width, height); - wl_surface_set_opaque_region(term->window->search.surf, region); + wl_surface_set_opaque_region(term->window->search.surface.surf, region); wl_region_destroy(region); } - wl_surface_commit(term->window->search.surf); + wl_surface_commit(term->window->search.surface.surf); quirk_weston_subsurface_desync_off(term->window->search.sub); #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED @@ -3466,7 +3466,7 @@ render_urls(struct terminal *term) continue; } - struct wl_surface *surf = it->item.surf.surf; + struct wl_surface *surf = it->item.surf.surface.surf; struct wl_subsurface *sub_surf = it->item.surf.sub; if (surf == NULL || sub_surf == NULL) @@ -3601,7 +3601,7 @@ render_urls(struct terminal *term) : term->colors.table[3]; for (size_t i = 0; i < render_count; i++) { - struct wl_surface *surf = info[i].url->surf.surf; + struct wl_surface *surf = info[i].url->surf.surface.surf; struct wl_subsurface *sub_surf = info[i].url->surf.sub; const char32_t *label = info[i].text; @@ -4253,8 +4253,8 @@ render_xcursor_update(struct seat *seat) if (seat->pointer.xcursor == XCURSOR_HIDDEN) { /* Hide cursor */ - wl_surface_attach(seat->pointer.surface, NULL, 0, 0); - wl_surface_commit(seat->pointer.surface); + wl_surface_attach(seat->pointer.surface.surf, NULL, 0, 0); + wl_surface_commit(seat->pointer.surface.surf); return; } @@ -4263,24 +4263,24 @@ render_xcursor_update(struct seat *seat) const float scale = seat->pointer.scale; struct wl_cursor_image *image = seat->pointer.cursor->images[0]; - wayl_surface_scale(seat->wayl, seat->pointer.surface, scale); + wayl_surface_scale(seat->wayl, seat->pointer.surface.surf, scale); wl_surface_attach( - seat->pointer.surface, wl_cursor_image_get_buffer(image), 0, 0); + seat->pointer.surface.surf, wl_cursor_image_get_buffer(image), 0, 0); wl_pointer_set_cursor( seat->wl_pointer, seat->pointer.serial, - seat->pointer.surface, + seat->pointer.surface.surf, image->hotspot_x / scale, image->hotspot_y / scale); wl_surface_damage_buffer( - seat->pointer.surface, 0, 0, INT32_MAX, INT32_MAX); + seat->pointer.surface.surf, 0, 0, INT32_MAX, INT32_MAX); xassert(seat->pointer.xcursor_callback == NULL); - seat->pointer.xcursor_callback = wl_surface_frame(seat->pointer.surface); + seat->pointer.xcursor_callback = wl_surface_frame(seat->pointer.surface.surf); wl_callback_add_listener(seat->pointer.xcursor_callback, &xcursor_listener, seat); - wl_surface_commit(seat->pointer.surface); + wl_surface_commit(seat->pointer.surface.surf); } static void diff --git a/terminal.c b/terminal.c index 43ba157b..825e1550 100644 --- a/terminal.c +++ b/terminal.c @@ -3589,23 +3589,23 @@ term_single_shift(struct terminal *term, enum charset_designator idx) enum term_surface term_surface_kind(const struct terminal *term, const struct wl_surface *surface) { - if (likely(surface == term->window->surface)) + if (likely(surface == term->window->surface.surf)) return TERM_SURF_GRID; - else if (surface == term->window->csd.surface[CSD_SURF_TITLE].surf) + else if (surface == term->window->csd.surface[CSD_SURF_TITLE].surface.surf) return TERM_SURF_TITLE; - else if (surface == term->window->csd.surface[CSD_SURF_LEFT].surf) + else if (surface == term->window->csd.surface[CSD_SURF_LEFT].surface.surf) return TERM_SURF_BORDER_LEFT; - else if (surface == term->window->csd.surface[CSD_SURF_RIGHT].surf) + else if (surface == term->window->csd.surface[CSD_SURF_RIGHT].surface.surf) return TERM_SURF_BORDER_RIGHT; - else if (surface == term->window->csd.surface[CSD_SURF_TOP].surf) + else if (surface == term->window->csd.surface[CSD_SURF_TOP].surface.surf) return TERM_SURF_BORDER_TOP; - else if (surface == term->window->csd.surface[CSD_SURF_BOTTOM].surf) + else if (surface == term->window->csd.surface[CSD_SURF_BOTTOM].surface.surf) return TERM_SURF_BORDER_BOTTOM; - else if (surface == term->window->csd.surface[CSD_SURF_MINIMIZE].surf) + else if (surface == term->window->csd.surface[CSD_SURF_MINIMIZE].surface.surf) return TERM_SURF_BUTTON_MINIMIZE; - else if (surface == term->window->csd.surface[CSD_SURF_MAXIMIZE].surf) + else if (surface == term->window->csd.surface[CSD_SURF_MAXIMIZE].surface.surf) return TERM_SURF_BUTTON_MAXIMIZE; - else if (surface == term->window->csd.surface[CSD_SURF_CLOSE].surf) + else if (surface == term->window->csd.surface[CSD_SURF_CLOSE].surface.surf) return TERM_SURF_BUTTON_CLOSE; else return TERM_SURF_NONE; diff --git a/wayland.c b/wayland.c index 9a3d11e8..3b6833c5 100644 --- a/wayland.c +++ b/wayland.c @@ -74,7 +74,7 @@ csd_instantiate(struct wl_window *win) for (size_t i = CSD_SURF_MINIMIZE; i < CSD_SURF_COUNT; i++) { bool ret = wayl_win_subsurface_new_with_custom_parent( - win, win->csd.surface[CSD_SURF_TITLE].surf, &win->csd.surface[i], + win, win->csd.surface[CSD_SURF_TITLE].surface.surf, &win->csd.surface[i], true); xassert(ret); } @@ -187,8 +187,12 @@ seat_destroy(struct seat *seat) if (seat->pointer.theme != NULL) wl_cursor_theme_destroy(seat->pointer.theme); - if (seat->pointer.surface != NULL) - wl_surface_destroy(seat->pointer.surface); + if (seat->pointer.surface.surf != NULL) + wl_surface_destroy(seat->pointer.surface.surf); +#if defined(HAVE_FRACTIONAL_SCALE) + if (seat->pointer.surface.viewport != NULL) + wp_viewport_destroy(seat->pointer.surface.viewport); +#endif if (seat->pointer.xcursor_callback != NULL) wl_callback_destroy(seat->pointer.xcursor_callback); @@ -288,10 +292,10 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat, if (caps & WL_SEAT_CAPABILITY_POINTER) { if (seat->wl_pointer == NULL) { - xassert(seat->pointer.surface == NULL); - seat->pointer.surface = wl_compositor_create_surface(seat->wayl->compositor); + xassert(seat->pointer.surface.surf == NULL); + seat->pointer.surface.surf = wl_compositor_create_surface(seat->wayl->compositor); - if (seat->pointer.surface == NULL) { + if (seat->pointer.surface.surf == NULL) { LOG_ERR("%s: failed to create pointer surface", seat->name); return; } @@ -302,13 +306,13 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat, } else { if (seat->wl_pointer != NULL) { wl_pointer_release(seat->wl_pointer); - wl_surface_destroy(seat->pointer.surface); + wl_surface_destroy(seat->pointer.surface.surf); if (seat->pointer.theme != NULL) wl_cursor_theme_destroy(seat->pointer.theme); seat->wl_pointer = NULL; - seat->pointer.surface = NULL; + seat->pointer.surface.surf = NULL; seat->pointer.theme = NULL; seat->pointer.cursor = NULL; } @@ -848,7 +852,7 @@ xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, * anytime soon. Some compositors require a commit in * combination with an ack - make them happy. */ - wl_surface_commit(win->surface); + wl_surface_commit(win->surface.surf); } if (wasnt_configured) @@ -1225,7 +1229,7 @@ handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) if (seat->wl_keyboard != NULL) keyboard_listener.leave( - seat, seat->wl_keyboard, -1, seat->kbd_focus->window->surface); + seat, seat->wl_keyboard, -1, seat->kbd_focus->window->surface.surf); } if (seat->mouse_focus != NULL) { @@ -1235,7 +1239,7 @@ handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) if (seat->wl_pointer != NULL) pointer_listener.leave( - seat, seat->wl_pointer, -1, seat->mouse_focus->window->surface); + seat, seat->wl_pointer, -1, seat->mouse_focus->window->surface.surf); } seat_destroy(seat); @@ -1531,29 +1535,29 @@ wayl_win_init(struct terminal *term, const char *token) win->wm_capabilities.maximize = true; win->wm_capabilities.minimize = true; - win->surface = wl_compositor_create_surface(wayl->compositor); - if (win->surface == NULL) { + win->surface.surf = wl_compositor_create_surface(wayl->compositor); + if (win->surface.surf == NULL) { LOG_ERR("failed to create wayland surface"); goto out; } wayl_win_alpha_changed(win); - wl_surface_add_listener(win->surface, &surface_listener, win); + wl_surface_add_listener(win->surface.surf, &surface_listener, win); #if defined(HAVE_FRACTIONAL_SCALE) if (wayl->fractional_scale_manager != NULL && wayl->viewporter != NULL) { - win->viewport = wp_viewporter_get_viewport(wayl->viewporter, win->surface); + win->surface.viewport = wp_viewporter_get_viewport(wayl->viewporter, win->surface.surf); win->fractional_scale = wp_fractional_scale_manager_v1_get_fractional_scale( - wayl->fractional_scale_manager, win->surface); + wayl->fractional_scale_manager, win->surface.surf); wp_fractional_scale_v1_add_listener( win->fractional_scale, &fractional_scale_listener, win); } #endif - win->xdg_surface = xdg_wm_base_get_xdg_surface(wayl->shell, win->surface); + win->xdg_surface = xdg_wm_base_get_xdg_surface(wayl->shell, win->surface.surf); xdg_surface_add_listener(win->xdg_surface, &xdg_surface_listener, win); win->xdg_toplevel = xdg_surface_get_toplevel(win->xdg_surface); @@ -1586,12 +1590,12 @@ wayl_win_init(struct terminal *term, const char *token) LOG_WARN("no decoration manager available - using CSDs unconditionally"); } - wl_surface_commit(win->surface); + wl_surface_commit(win->surface.surf); #if defined(HAVE_XDG_ACTIVATION) /* Complete XDG startup notification */ if (token) - xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface); + xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface.surf); #endif if (!wayl_win_subsurface_new(win, &win->overlay, false)) { @@ -1641,33 +1645,33 @@ wayl_win_destroy(struct wl_window *win) * nor mouse focus). */ - if (win->render_timer.surf != NULL) { - wl_surface_attach(win->render_timer.surf, NULL, 0, 0); - wl_surface_commit(win->render_timer.surf); + if (win->render_timer.surface.surf != NULL) { + wl_surface_attach(win->render_timer.surface.surf, NULL, 0, 0); + wl_surface_commit(win->render_timer.surface.surf); } - if (win->scrollback_indicator.surf != NULL) { - wl_surface_attach(win->scrollback_indicator.surf, NULL, 0, 0); - wl_surface_commit(win->scrollback_indicator.surf); + if (win->scrollback_indicator.surface.surf != NULL) { + wl_surface_attach(win->scrollback_indicator.surface.surf, NULL, 0, 0); + wl_surface_commit(win->scrollback_indicator.surface.surf); } /* Scrollback search */ - if (win->search.surf != NULL) { - wl_surface_attach(win->search.surf, NULL, 0, 0); - wl_surface_commit(win->search.surf); + if (win->search.surface.surf != NULL) { + wl_surface_attach(win->search.surface.surf, NULL, 0, 0); + wl_surface_commit(win->search.surface.surf); } /* URLs */ tll_foreach(win->urls, it) { - wl_surface_attach(it->item.surf.surf, NULL, 0, 0); - wl_surface_commit(it->item.surf.surf); + wl_surface_attach(it->item.surf.surface.surf, NULL, 0, 0); + wl_surface_commit(it->item.surf.surface.surf); } /* CSD */ for (size_t i = 0; i < ALEN(win->csd.surface); i++) { - if (win->csd.surface[i].surf != NULL) { - wl_surface_attach(win->csd.surface[i].surf, NULL, 0, 0); - wl_surface_commit(win->csd.surface[i].surf); + if (win->csd.surface[i].surface.surf != NULL) { + wl_surface_attach(win->csd.surface[i].surface.surf, NULL, 0, 0); + wl_surface_commit(win->csd.surface[i].surface.surf); } } @@ -1675,8 +1679,8 @@ wayl_win_destroy(struct wl_window *win) /* Main window */ win->unmapped = true; - wl_surface_attach(win->surface, NULL, 0, 0); - wl_surface_commit(win->surface); + wl_surface_attach(win->surface.surf, NULL, 0, 0); + wl_surface_commit(win->surface.surf); wayl_roundtrip(win->term->wl); tll_free(win->on_outputs); @@ -1710,8 +1714,8 @@ wayl_win_destroy(struct wl_window *win) #if defined(HAVE_FRACTIONAL_SCALE) if (win->fractional_scale != NULL) wp_fractional_scale_v1_destroy(win->fractional_scale); - if (win->viewport != NULL) - wp_viewport_destroy(win->viewport); + if (win->surface.viewport != NULL) + wp_viewport_destroy(win->surface.viewport); #endif if (win->frame_callback != NULL) wl_callback_destroy(win->frame_callback); @@ -1721,8 +1725,8 @@ wayl_win_destroy(struct wl_window *win) xdg_toplevel_destroy(win->xdg_toplevel); if (win->xdg_surface != NULL) xdg_surface_destroy(win->xdg_surface); - if (win->surface != NULL) - wl_surface_destroy(win->surface); + if (win->surface.surf != NULL) + wl_surface_destroy(win->surface.surf); wayl_roundtrip(win->term->wl); @@ -1880,7 +1884,7 @@ wayl_win_scale(struct wl_window *win) const struct wayland *wayl = term->wl; const float scale = term->scale; - wayl_surface_scale(wayl, win->surface, scale); + wayl_surface_scale(wayl, win->surface.surf, scale); } void @@ -1894,11 +1898,11 @@ wayl_win_alpha_changed(struct wl_window *win) if (region != NULL) { wl_region_add(region, 0, 0, INT32_MAX, INT32_MAX); - wl_surface_set_opaque_region(win->surface, region); + wl_surface_set_opaque_region(win->surface.surf, region); wl_region_destroy(region); } } else - wl_surface_set_opaque_region(win->surface, NULL); + wl_surface_set_opaque_region(win->surface.surf, NULL); } #if defined(HAVE_XDG_ACTIVATION) @@ -1909,7 +1913,7 @@ activation_token_for_urgency_done(const char *token, void *data) struct wayland *wayl = win->term->wl; win->urgency_token_is_pending = false; - xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface); + xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface.surf); } #endif /* HAVE_XDG_ACTIVATION */ @@ -1954,11 +1958,11 @@ wayl_win_csd_borders_visible(const struct wl_window *win) bool wayl_win_subsurface_new_with_custom_parent( struct wl_window *win, struct wl_surface *parent, - struct wl_surf_subsurf *surf, bool allow_pointer_input) + struct wayl_sub_surface *surf, bool allow_pointer_input) { struct wayland *wayl = win->term->wl; - surf->surf = NULL; + surf->surface.surf = NULL; surf->sub = NULL; struct wl_surface *main_surface @@ -2002,39 +2006,42 @@ wayl_win_subsurface_new_with_custom_parent( wl_region_destroy(empty); } - surf->surf = main_surface; + surf->surface.surf = main_surface; surf->sub = sub; #if defined(HAVE_FRACTIONAL_SCALE) - surf->viewport = viewport; + surf->surface.viewport = viewport; #endif return true; } bool -wayl_win_subsurface_new(struct wl_window *win, struct wl_surf_subsurf *surf, +wayl_win_subsurface_new(struct wl_window *win, struct wayl_sub_surface *surf, bool allow_pointer_input) { return wayl_win_subsurface_new_with_custom_parent( - win, win->surface, surf, allow_pointer_input); + win, win->surface.surf, surf, allow_pointer_input); } void -wayl_win_subsurface_destroy(struct wl_surf_subsurf *surf) +wayl_win_subsurface_destroy(struct wayl_sub_surface *surf) { if (surf == NULL) return; #if defined(HAVE_FRACTIONAL_SCALE) - if (surf->viewport != NULL) - wp_viewport_destroy(surf->viewport); + if (surf->surface.viewport != NULL) { + wp_viewport_destroy(surf->surface.viewport); + surf->surface.viewport = NULL; + } #endif - if (surf->sub != NULL) + if (surf->sub != NULL) { wl_subsurface_destroy(surf->sub); - if (surf->surf != NULL) - wl_surface_destroy(surf->surf); - - surf->surf = NULL; - surf->sub = NULL; + surf->sub = NULL; + } + if (surf->surface.surf != NULL) { + wl_surface_destroy(surf->surface.surf); + surf->surface.surf = NULL; + } } #if defined(HAVE_XDG_ACTIVATION) @@ -2099,7 +2106,7 @@ wayl_get_activation_token( if (seat != NULL && serial != 0) xdg_activation_token_v1_set_serial(token, serial, seat->wl_seat); - xdg_activation_token_v1_set_surface(token, win->surface); + xdg_activation_token_v1_set_surface(token, win->surface.surf); xdg_activation_token_v1_add_listener(token, &activation_token_listener, ctx); xdg_activation_token_v1_commit(token); return true; diff --git a/wayland.h b/wayland.h index bb9bf77f..3ad05d33 100644 --- a/wayland.h +++ b/wayland.h @@ -45,6 +45,18 @@ enum data_offer_mime_type { DATA_OFFER_MIME_TEXT_UTF8_STRING, }; +struct wayl_surface { + struct wl_surface *surf; +#if defined(HAVE_FRACTIONAL_SCALE) + struct wp_viewport *viewport; +#endif +}; + +struct wayl_sub_surface { + struct wayl_surface surface; + struct wl_subsurface *sub; +}; + struct wl_window; struct wl_clipboard { struct wl_window *window; /* For DnD */ @@ -132,7 +144,7 @@ struct seat { struct { uint32_t serial; - struct wl_surface *surface; + struct wayl_surface surface; struct wl_cursor_theme *theme; struct wl_cursor *cursor; float scale; @@ -294,17 +306,9 @@ struct monitor { bool use_output_release; }; -struct wl_surf_subsurf { - struct wl_surface *surf; - struct wl_subsurface *sub; -#if defined(HAVE_FRACTIONAL_SCALE) - struct wp_viewport *viewport; -#endif -}; - struct wl_url { const struct url *url; - struct wl_surf_subsurf surf; + struct wayl_sub_surface surf; }; enum csd_mode {CSD_UNKNOWN, CSD_NO, CSD_YES}; @@ -328,7 +332,7 @@ struct xdg_activation_token_context { struct wayland; struct wl_window { struct terminal *term; - struct wl_surface *surface; + struct wayl_surface surface; struct xdg_surface *xdg_surface; struct xdg_toplevel *xdg_toplevel; #if defined(HAVE_XDG_ACTIVATION) @@ -336,7 +340,6 @@ struct wl_window { bool urgency_token_is_pending; #endif #if defined(HAVE_FRACTIONAL_SCALE) - struct wp_viewport *viewport; struct wp_fractional_scale_v1 *fractional_scale; #endif bool unmapped; @@ -348,7 +351,7 @@ struct wl_window { enum csd_mode csd_mode; struct { - struct wl_surf_subsurf surface[CSD_SURF_COUNT]; + struct wayl_sub_surface surface[CSD_SURF_COUNT]; struct fcft_font *font; int move_timeout_fd; uint32_t serial; @@ -359,10 +362,10 @@ struct wl_window { bool minimize:1; } wm_capabilities; - struct wl_surf_subsurf search; - struct wl_surf_subsurf scrollback_indicator; - struct wl_surf_subsurf render_timer; - struct wl_surf_subsurf overlay; + struct wayl_sub_surface search; + struct wayl_sub_surface scrollback_indicator; + struct wayl_sub_surface render_timer; + struct wayl_sub_surface overlay; struct wl_callback *frame_callback; @@ -465,12 +468,12 @@ bool wayl_win_csd_titlebar_visible(const struct wl_window *win); bool wayl_win_csd_borders_visible(const struct wl_window *win); bool wayl_win_subsurface_new( - struct wl_window *win, struct wl_surf_subsurf *surf, + struct wl_window *win, struct wayl_sub_surface *surf, bool allow_pointer_input); bool wayl_win_subsurface_new_with_custom_parent( struct wl_window *win, struct wl_surface *parent, - struct wl_surf_subsurf *surf, bool allow_pointer_input); -void wayl_win_subsurface_destroy(struct wl_surf_subsurf *surf); + struct wayl_sub_surface *surf, bool allow_pointer_input); +void wayl_win_subsurface_destroy(struct wayl_sub_surface *surf); #if defined(HAVE_XDG_ACTIVATION) bool wayl_get_activation_token( From 434fd6aa1f4a21ef6572a772ec891eeb2006a82c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Jun 2023 16:53:16 +0200 Subject: [PATCH 0337/1323] wayland: refactor: wayl_surface_scale(): pass wayl_surface pointer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of passing a raw wl_surface pointer, pass a wayl_surface pointer. This is needed later, when using fractional scaling to scale the surface (since then we need the surface’s viewport object). --- render.c | 61 +++++++++++++++++++++++++------------------------------ wayland.c | 6 +++--- wayland.h | 2 +- 3 files changed, 32 insertions(+), 37 deletions(-) diff --git a/render.c b/render.c index 5ebd69eb..e78f1eeb 100644 --- a/render.c +++ b/render.c @@ -1691,7 +1691,7 @@ render_overlay(struct terminal *term) &(pixman_rectangle16_t){0, 0, term->width, term->height}); quirk_weston_subsurface_desync_on(overlay->sub); - wayl_surface_scale(term->wl, overlay->surface.surf, term->scale); + wayl_surface_scale(term->wl, &overlay->surface, term->scale); wl_subsurface_set_position(overlay->sub, 0, 0); wl_surface_attach(overlay->surface.surf, buf->wl_buf, 0, 0); @@ -1828,12 +1828,12 @@ get_csd_data(const struct terminal *term, enum csd_surface surf_idx) } static void -csd_commit(struct terminal *term, struct wl_surface *surf, struct buffer *buf) +csd_commit(struct terminal *term, struct wayl_surface *surf, struct buffer *buf) { wayl_surface_scale(term->wl, surf, term->scale); - wl_surface_attach(surf, buf->wl_buf, 0, 0); - wl_surface_damage_buffer(surf, 0, 0, buf->width, buf->height); - wl_surface_commit(surf); + wl_surface_attach(surf->surf, buf->wl_buf, 0, 0); + wl_surface_damage_buffer(surf->surf, 0, 0, buf->width, buf->height); + wl_surface_commit(surf->surf); } static void @@ -1849,8 +1849,7 @@ render_csd_part(struct terminal *term, } static void -render_osd(struct terminal *term, - struct wl_surface *surf, struct wl_subsurface *sub_surf, +render_osd(struct terminal *term, const struct wayl_sub_surface *sub_surf, struct fcft_font *font, struct buffer *buf, const char32_t *text, uint32_t _fg, uint32_t _bg, unsigned x, unsigned y) @@ -1923,20 +1922,20 @@ render_osd(struct terminal *term, pixman_image_unref(src); pixman_image_set_clip_region32(buf->pix[0], NULL); - quirk_weston_subsurface_desync_on(sub_surf); - wayl_surface_scale(term->wl, surf, term->scale); - wl_surface_attach(surf, buf->wl_buf, 0, 0); - wl_surface_damage_buffer(surf, 0, 0, buf->width, buf->height); + quirk_weston_subsurface_desync_on(sub_surf->sub); + wayl_surface_scale(term->wl, &sub_surf->surface, term->scale); + wl_surface_attach(sub_surf->surface.surf, buf->wl_buf, 0, 0); + wl_surface_damage_buffer(sub_surf->surface.surf, 0, 0, buf->width, buf->height); struct wl_region *region = wl_compositor_create_region(term->wl->compositor); if (region != NULL) { wl_region_add(region, 0, 0, buf->width, buf->height); - wl_surface_set_opaque_region(surf, region); + wl_surface_set_opaque_region(sub_surf->surface.surf, region); wl_region_destroy(region); } - wl_surface_commit(surf); - quirk_weston_subsurface_desync_off(sub_surf); + wl_surface_commit(sub_surf->surface.surf); + quirk_weston_subsurface_desync_off(sub_surf->sub); } static void @@ -1971,11 +1970,10 @@ render_csd_title(struct terminal *term, const struct csd_data *info, const int margin = M != NULL ? M->advance.x : win->csd.font->max_advance.x; - render_osd(term, surf->surface.surf, surf->sub, win->csd.font, - buf, title_text, fg, bg, margin, + render_osd(term, surf, win->csd.font, buf, title_text, fg, bg, margin, (buf->height - win->csd.font->height) / 2); - csd_commit(term, surf->surface.surf, buf); + csd_commit(term, &surf->surface, buf); free(_title_text); } @@ -1986,14 +1984,14 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, xassert(term->window->csd_mode == CSD_YES); xassert(surf_idx >= CSD_SURF_LEFT && surf_idx <= CSD_SURF_BOTTOM); - struct wl_surface *surf = term->window->csd.surface[surf_idx].surface.surf; + struct wayl_surface *surf = &term->window->csd.surface[surf_idx].surface; if (info->width == 0 || info->height == 0) return; { pixman_color_t color = color_hex_to_pixman_with_alpha(0, 0); - render_csd_part(term, surf, buf, info->width, info->height, &color); + render_csd_part(term, surf->surf, buf, info->width, info->height, &color); } /* @@ -2271,7 +2269,7 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx, xassert(term->window->csd_mode == CSD_YES); xassert(surf_idx >= CSD_SURF_MINIMIZE && surf_idx <= CSD_SURF_CLOSE); - struct wl_surface *surf = term->window->csd.surface[surf_idx].surface.surf; + struct wayl_surface *surf = &term->window->csd.surface[surf_idx].surface; if (info->width == 0 || info->height == 0) return; @@ -2323,7 +2321,7 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx, _color = color_dim(term, _color); pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha); - render_csd_part(term, surf, buf, info->width, info->height, &color); + render_csd_part(term, surf->surf, buf, info->width, info->height, &color); switch (surf_idx) { case CSD_SURF_MINIMIZE: render_csd_button_minimize(term, buf); break; @@ -2534,8 +2532,7 @@ render_scrollback_position(struct terminal *term) render_osd( term, - win->scrollback_indicator.surface.surf, - win->scrollback_indicator.sub, + &win->scrollback_indicator, term->fonts[0], buf, text, fg, 0xffu << 24 | bg, width - margin - c32len(text) * term->cell_width, margin); @@ -2571,8 +2568,7 @@ render_render_timer(struct terminal *term, struct timespec render_time) render_osd( term, - win->render_timer.surface.surf, - win->render_timer.sub, + &win->render_timer, term->fonts[0], buf, text, term->colors.table[0], 0xffu << 24 | term->colors.table[8 + 1], margin, margin); @@ -3374,7 +3370,7 @@ render_search_box(struct terminal *term) margin / term->scale, max(0, (int32_t)term->height - height - margin) / term->scale); - wayl_surface_scale(term->wl, term->window->search.surface.surf, term->scale); + wayl_surface_scale(term->wl, &term->window->search.surface, term->scale); wl_surface_attach(term->window->search.surface.surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer(term->window->search.surface.surf, 0, 0, width, height); @@ -3601,23 +3597,22 @@ render_urls(struct terminal *term) : term->colors.table[3]; for (size_t i = 0; i < render_count; i++) { - struct wl_surface *surf = info[i].url->surf.surface.surf; - struct wl_subsurface *sub_surf = info[i].url->surf.sub; + const struct wayl_sub_surface *sub_surf = &info[i].url->surf; const char32_t *label = info[i].text; const int x = info[i].x; const int y = info[i].y; - xassert(surf != NULL); - xassert(sub_surf != NULL); + xassert(sub_surf->surface.surf != NULL); + xassert(sub_surf->sub != NULL); wl_subsurface_set_position( - sub_surf, + sub_surf->sub, (term->margins.left + x) / term->scale, (term->margins.top + y) / term->scale); render_osd( - term, surf, sub_surf, term->fonts[0], bufs[i], label, + term, sub_surf, term->fonts[0], bufs[i], label, fg, 0xffu << 24 | bg, x_margin, y_margin); free(info[i].text); @@ -4263,7 +4258,7 @@ render_xcursor_update(struct seat *seat) const float scale = seat->pointer.scale; struct wl_cursor_image *image = seat->pointer.cursor->images[0]; - wayl_surface_scale(seat->wayl, seat->pointer.surface.surf, scale); + wayl_surface_scale(seat->wayl, &seat->pointer.surface, scale); wl_surface_attach( seat->pointer.surface.surf, wl_cursor_image_get_buffer(image), 0, 0); diff --git a/wayland.c b/wayland.c index 3b6833c5..cd1a4c68 100644 --- a/wayland.c +++ b/wayland.c @@ -1865,7 +1865,7 @@ wayl_fractional_scaling(const struct wayland *wayl) } void -wayl_surface_scale(const struct wayland *wayl, struct wl_surface *surf, +wayl_surface_scale(const struct wayland *wayl, const struct wayl_surface *surf, float scale) { LOG_WARN("scaling by a factor of %.2f (legacy)", scale); @@ -1873,7 +1873,7 @@ wayl_surface_scale(const struct wayland *wayl, struct wl_surface *surf, if (wayl_fractional_scaling(wayl)) { BUG("not yet implemented"); } else { - wl_surface_set_buffer_scale(surf, (int)scale); + wl_surface_set_buffer_scale(surf->surf, (int)scale); } } @@ -1884,7 +1884,7 @@ wayl_win_scale(struct wl_window *win) const struct wayland *wayl = term->wl; const float scale = term->scale; - wayl_surface_scale(wayl, win->surface.surf, scale); + wayl_surface_scale(wayl, &win->surface, scale); } void diff --git a/wayland.h b/wayland.h index 3ad05d33..0506f82b 100644 --- a/wayland.h +++ b/wayland.h @@ -455,7 +455,7 @@ void wayl_roundtrip(struct wayland *wayl); bool wayl_fractional_scaling(const struct wayland *wayl); void wayl_surface_scale( - const struct wayland *wayl, struct wl_surface *surf, float scale); + const struct wayland *wayl, const struct wayl_surface *surf, float scale); struct wl_window *wayl_win_init(struct terminal *term, const char *token); void wayl_win_destroy(struct wl_window *win); From 5a60bbc119397c2993fc1895bae75c2c1e69fe1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Jun 2023 17:05:16 +0200 Subject: [PATCH 0338/1323] wayland: refactor: add a buffer argument to wayl_*_scale() functions This will be needed later, when using fractional scaling + viewporter to scale. --- render.c | 28 +++++++++++++++++----------- wayland.c | 17 +++++++++++++---- wayland.h | 9 +++++++-- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/render.c b/render.c index e78f1eeb..72d87bff 100644 --- a/render.c +++ b/render.c @@ -1691,7 +1691,8 @@ render_overlay(struct terminal *term) &(pixman_rectangle16_t){0, 0, term->width, term->height}); quirk_weston_subsurface_desync_on(overlay->sub); - wayl_surface_scale(term->wl, &overlay->surface, term->scale); + wayl_surface_scale( + term->wl, &overlay->surface, buf, term->scale); wl_subsurface_set_position(overlay->sub, 0, 0); wl_surface_attach(overlay->surface.surf, buf->wl_buf, 0, 0); @@ -1830,7 +1831,7 @@ get_csd_data(const struct terminal *term, enum csd_surface surf_idx) static void csd_commit(struct terminal *term, struct wayl_surface *surf, struct buffer *buf) { - wayl_surface_scale(term->wl, surf, term->scale); + wayl_surface_scale(term->wl, surf, buf, term->scale); wl_surface_attach(surf->surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer(surf->surf, 0, 0, buf->width, buf->height); wl_surface_commit(surf->surf); @@ -1923,7 +1924,7 @@ render_osd(struct terminal *term, const struct wayl_sub_surface *sub_surf, pixman_image_set_clip_region32(buf->pix[0], NULL); quirk_weston_subsurface_desync_on(sub_surf->sub); - wayl_surface_scale(term->wl, &sub_surf->surface, term->scale); + wayl_surface_scale(term->wl, &sub_surf->surface, buf, term->scale); wl_surface_attach(sub_surf->surface.surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer(sub_surf->surface.surf, 0, 0, buf->width, buf->height); @@ -3013,7 +3014,7 @@ grid_render(struct terminal *term) term->window->frame_callback = wl_surface_frame(term->window->surface.surf); wl_callback_add_listener(term->window->frame_callback, &frame_listener, term); - wayl_win_scale(term->window); + wayl_win_scale(term->window, buf); if (term->wl->presentation != NULL && term->conf->presentation_timings) { struct timespec commit_time; @@ -3370,7 +3371,7 @@ render_search_box(struct terminal *term) margin / term->scale, max(0, (int32_t)term->height - height - margin) / term->scale); - wayl_surface_scale(term->wl, &term->window->search.surface, term->scale); + wayl_surface_scale(term->wl, &term->window->search.surface, buf, term->scale); wl_surface_attach(term->window->search.surface.surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer(term->window->search.surface.surf, 0, 0, width, height); @@ -3843,9 +3844,13 @@ maybe_resize(struct terminal *term, int width, int height, bool force) return false; float scale = -1; - tll_foreach(term->window->on_outputs, it) { - if (it->item->scale > scale) - scale = it->item->scale; + if (wayl_fractional_scaling(term->wl)) { + scale = term->window->scale; + } else { + tll_foreach(term->window->on_outputs, it) { + if (it->item->scale > scale) + scale = it->item->scale; + } } if (scale < 0.) { @@ -4257,11 +4262,12 @@ render_xcursor_update(struct seat *seat) const float scale = seat->pointer.scale; struct wl_cursor_image *image = seat->pointer.cursor->images[0]; + struct wl_buffer *buf = wl_cursor_image_get_buffer(image); - wayl_surface_scale(seat->wayl, &seat->pointer.surface, scale); + wayl_surface_scale_explicit_width_height( + seat->wayl, &seat->pointer.surface, image->width, image->height, scale); - wl_surface_attach( - seat->pointer.surface.surf, wl_cursor_image_get_buffer(image), 0, 0); + wl_surface_attach(seat->pointer.surface.surf, buf, 0, 0); wl_pointer_set_cursor( seat->wl_pointer, seat->pointer.serial, diff --git a/wayland.c b/wayland.c index cd1a4c68..43f85066 100644 --- a/wayland.c +++ b/wayland.c @@ -1865,8 +1865,9 @@ wayl_fractional_scaling(const struct wayland *wayl) } void -wayl_surface_scale(const struct wayland *wayl, const struct wayl_surface *surf, - float scale) +wayl_surface_scale_explicit_width_height( + const struct wayland *wayl, const struct wayl_surface *surf, + int width, int height, float scale) { LOG_WARN("scaling by a factor of %.2f (legacy)", scale); @@ -1878,13 +1879,21 @@ wayl_surface_scale(const struct wayland *wayl, const struct wayl_surface *surf, } void -wayl_win_scale(struct wl_window *win) +wayl_surface_scale(const struct wayland *wayl, const struct wayl_surface *surf, + const struct buffer *buf, float scale) +{ + wayl_surface_scale_explicit_width_height( + wayl, surf, buf->width, buf->height, scale); +} + +void +wayl_win_scale(struct wl_window *win, const struct buffer *buf) { const struct terminal *term = win->term; const struct wayland *wayl = term->wl; const float scale = term->scale; - wayl_surface_scale(wayl, &win->surface, scale); + wayl_surface_scale(wayl, &win->surface, buf, scale); } void diff --git a/wayland.h b/wayland.h index 0506f82b..9736ea4d 100644 --- a/wayland.h +++ b/wayland.h @@ -32,6 +32,7 @@ /* Forward declarations */ struct terminal; +struct buffer; /* Mime-types we support when dealing with data offers (e.g. copy-paste, or DnD) */ enum data_offer_mime_type { @@ -455,12 +456,16 @@ void wayl_roundtrip(struct wayland *wayl); bool wayl_fractional_scaling(const struct wayland *wayl); void wayl_surface_scale( - const struct wayland *wayl, const struct wayl_surface *surf, float scale); + const struct wayland *wayl, const struct wayl_surface *surf, + const struct buffer *buf, float scale); +void wayl_surface_scale_explicit_width_height( + const struct wayland *wayl, const struct wayl_surface *surf, + int width, int height, float scale); struct wl_window *wayl_win_init(struct terminal *term, const char *token); void wayl_win_destroy(struct wl_window *win); -void wayl_win_scale(struct wl_window *win); +void wayl_win_scale(struct wl_window *win, const struct buffer *buf); void wayl_win_alpha_changed(struct wl_window *win); bool wayl_win_set_urgent(struct wl_window *win); From e5989d81b931afb31ac0ac3df9269841adef2d98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Jun 2023 17:31:14 +0200 Subject: [PATCH 0339/1323] wayland: instantiate+destroy viewport for pointer surface --- wayland.c | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/wayland.c b/wayland.c index 43f85066..6dea027d 100644 --- a/wayland.c +++ b/wayland.c @@ -293,13 +293,27 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat, if (caps & WL_SEAT_CAPABILITY_POINTER) { if (seat->wl_pointer == NULL) { xassert(seat->pointer.surface.surf == NULL); - seat->pointer.surface.surf = wl_compositor_create_surface(seat->wayl->compositor); + seat->pointer.surface.surf = + wl_compositor_create_surface(seat->wayl->compositor); if (seat->pointer.surface.surf == NULL) { LOG_ERR("%s: failed to create pointer surface", seat->name); return; } +#if defined(HAVE_FRACTIONAL_SCALE) + xassert(seat->pointer.surface.viewport == NULL); + seat->pointer.surface.viewport = wp_viewporter_get_viewport( + seat->wayl->viewporter, seat->pointer.surface.surf); + + if (seat->pointer.surface.viewport == NULL) { + LOG_ERR("%s: failed to create pointer viewport", seat->name); + wl_surface_destroy(seat->pointer.surface.surf); + seat->pointer.surface.surf = NULL; + return; + } +#endif + seat->wl_pointer = wl_seat_get_pointer(wl_seat); wl_pointer_add_listener(seat->wl_pointer, &pointer_listener, seat); } @@ -308,6 +322,11 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat, wl_pointer_release(seat->wl_pointer); wl_surface_destroy(seat->pointer.surface.surf); +#if defined(HAVE_FRACTIONAL_SCALE) + wp_viewport_destroy(seat->pointer.surface.viewport); + seat->pointer.surface.viewport = NULL; +#endif + if (seat->pointer.theme != NULL) wl_cursor_theme_destroy(seat->pointer.theme); From 36818459e51d99bdb9f48bf0b7a9471c4b8fe67d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Jun 2023 17:31:39 +0200 Subject: [PATCH 0340/1323] wayland: initialize window scale to -1 --- wayland.c | 1 + 1 file changed, 1 insertion(+) diff --git a/wayland.c b/wayland.c index 6dea027d..a45171ff 100644 --- a/wayland.c +++ b/wayland.c @@ -1550,6 +1550,7 @@ wayl_win_init(struct terminal *term, const char *token) win->csd_mode = CSD_UNKNOWN; win->csd.move_timeout_fd = -1; win->resize_timeout_fd = -1; + win->scale = -1.; win->wm_capabilities.maximize = true; win->wm_capabilities.minimize = true; From 8ccabb79745f079417b3a049f52c4779d6efdf07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Jun 2023 17:32:01 +0200 Subject: [PATCH 0341/1323] wayland: surface_scale(): implement fractional scaling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is done by setting the surface’s viewport destination --- wayland.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/wayland.c b/wayland.c index a45171ff..fce6a67f 100644 --- a/wayland.c +++ b/wayland.c @@ -1889,11 +1889,20 @@ wayl_surface_scale_explicit_width_height( const struct wayland *wayl, const struct wayl_surface *surf, int width, int height, float scale) { - LOG_WARN("scaling by a factor of %.2f (legacy)", scale); if (wayl_fractional_scaling(wayl)) { - BUG("not yet implemented"); +#if defined(HAVE_FRACTIONAL_SCALE) + LOG_DBG("scaling by a factor of %.2f (fractional scaling)", scale); + wp_viewport_set_destination( + surf->viewport, + round((float)width / scale), + round((float)height / scale)); +#else + BUG("wayl_fraction_scaling() returned true, " + "but fractional scaling was not available at compile time"). +#endif } else { + LOG_DBG("scaling by a factor of %.2f (legacy)", scale); wl_surface_set_buffer_scale(surf->surf, (int)scale); } } From 0bdb6580bd5f470ca82e771c70a8f7cd4f5a0240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Jun 2023 17:32:33 +0200 Subject: [PATCH 0342/1323] wayland: update terminal when preferred scaling factor changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the window’s preferred scaling factor is changed (through the fractional-scaling protocol), update the terminal; resize font, resize sub-surfaces etc. --- wayland.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wayland.c b/wayland.c index fce6a67f..683338c0 100644 --- a/wayland.c +++ b/wayland.c @@ -1526,7 +1526,9 @@ static void fractional_scale_preferred_scale( { struct wl_window *win = data; win->scale = (float)scale / 120.; + LOG_DBG("fractional scale: %.3f", win->scale); + update_term_for_output_change(win->term); } static const struct wp_fractional_scale_v1_listener fractional_scale_listener = { From 32b8c5c9b6628f3dd1b9f25f9b13a6d016de86cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Jun 2023 17:34:20 +0200 Subject: [PATCH 0343/1323] changelog: mention the newly added support for fractional-scaling-v1 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 943dd411..c3e66216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,9 @@ is `auto`, which will select `libutempter` on Linux, `ulog` on FreeBSD, and `none` for all others. * Sixel aspect ratio. +* Support for the new fractional-scaling-v1 Wayland protocol. This + brings true fractional scaling to Wayland in general, and with this + release, foot. ### Changed From 64b6b5d2a7b81bcad3f0a50a6c1556fd7300c171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Jun 2023 17:55:04 +0200 Subject: [PATCH 0344/1323] =?UTF-8?q?config:=20dpi-aware:=20remove=20?= =?UTF-8?q?=E2=80=98auto=E2=80=99=20value,=20and=20default=20to=20?= =?UTF-8?q?=E2=80=98no=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now default to scaling fonts using the scaling factor, not monitor DPI. The ‘auto’ value for dpi-aware has been removed. Documentation (man pages and README) have been updated to reflect the new default. --- README.md | 52 +++++++++++++++++++++++++++++++++------------ config.c | 15 +++---------- config.h | 2 +- doc/foot.ini.5.scd | 10 ++------- foot.ini | 2 +- terminal.c | 38 +-------------------------------- tests/test-config.c | 12 +---------- wayland.c | 5 ----- 8 files changed, 48 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index 8d037af3..0d6262dc 100644 --- a/README.md +++ b/README.md @@ -411,27 +411,53 @@ This is not how it is meant to be. Fonts are measured in _point sizes_ **for a reason**; a given point size should have the same height on all mediums, be it printers or monitors, regardless of their DPI. -Foot’s default behavior is to use the monitor’s DPI to size fonts when -output scaling has been disabled on **all** monitors. If at least one -monitor has output scaling enabled, fonts will instead by sized using -the scaling factor. +That said, on Wayland, Hi-DPI monitors are typically handled by +configuring a _"scaling factor"_ in the compositor. This is usually +expressed as either a rational value (e.g. _1.5_), or as a percentage +(e.g. _150%_), by which all fonts and window sizes are supposed to be +multiplied. -This can be changed to either **always** use the monitor’s DPI -(regardless of scaling factor), or to **never** use it, with the -`dpi-aware` option in `foot.ini`. See the man page, **foot.ini**(5) -for more information. +For this reason, and because of the new _fractional scaling_ protocol +(see below for details), and because this is how Wayland applications +are expected to behave, foot >= 1.15 will default to scaling fonts +using the compositor’s scaling factor, and **not** the monitor +DPI. -When fonts are sized using the monitor’s DPI, glyphs should always -have the same physical height, regardless of monitor. +This means the (assuming the monitors are at the same viewing +distance) the font size will appear to change when you move the foot +window across different monitors, **unless** you have configured the +monitors’ scaling factors correctly in the compositor. -Furthermore, foot will re-size the fonts on-the-fly when the window is -moved between screens with different DPIs values. If the window covers -multiple screens, with different DPIs, the highest DPI will be used. +This can be changed by setting the `dpi-aware` option to `yes` in +`foot.ini`. When enabled, fonts will **not** be sized using the +scaling factor, but will instead be sized using the monitor’s +DPI. When the foot window is moved across monitors, the font size is +updated for the current monitor’s DPI. + +This means that, assuming the monitors are **at the same viewing +distance**, the font size will appear to be the same, at all times. _Note_: if you configure **pixelsize**, rather than **size**, then DPI changes will **not** change the font size. Pixels are always pixels. +### Fractional scaling on Wayland + +For a long time, there was no **true** support for _fractional +scaling_. That is, values like 1.5 (150%), 1.8 (180%) etc, only +integer values, like 2 (200%). + +Compositors that _did_ support fractional scaling did so using a hack; +all applications were told to scale to 200%, and then the compositor +would down-scale the rendered image to e.g. 150%. This works OK for +everything **except fonts**, which ended up blurry. + +With _wayland-protocols 1.32_, a new protocol was introduced, that +allows compositors to tell applications the _actual_ scaling +factor. Applications can then scale the image using a _viewport_ +object, instead of setting a scale factor on the raw pixel buffer. + + ## Supported OSCs OSC, _Operating System Command_, are escape sequences that interacts diff --git a/config.c b/config.c index aba85db3..3d02355f 100644 --- a/config.c +++ b/config.c @@ -972,17 +972,8 @@ parse_section_main(struct context *ctx) else if (strcmp(key, "underline-thickness") == 0) return value_to_pt_or_px(ctx, &conf->underline_thickness); - else if (strcmp(key, "dpi-aware") == 0) { - if (strcmp(value, "auto") == 0) - conf->dpi_aware = DPI_AWARE_AUTO; - else { - bool value; - if (!value_to_bool(ctx, &value)) - return false; - conf->dpi_aware = value ? DPI_AWARE_YES : DPI_AWARE_NO; - } - return true; - } + else if (strcmp(key, "dpi-aware") == 0) + return value_to_bool(ctx, &conf->dpi_aware); else if (strcmp(key, "workers") == 0) return value_to_uint16(ctx, 10, &conf->render_worker_count); @@ -2939,7 +2930,7 @@ config_load(struct config *conf, const char *conf_path, .use_custom_underline_offset = false, .box_drawings_uses_font_glyphs = false, .underline_thickness = {.pt = 0., .px = -1}, - .dpi_aware = DPI_AWARE_AUTO, /* DPI-aware when scaling-factor == 1 */ + .dpi_aware = false, .bell = { .urgent = false, .notify = false, diff --git a/config.h b/config.h index ce1ee536..2034752f 100644 --- a/config.h +++ b/config.h @@ -137,7 +137,7 @@ struct config { enum { STARTUP_WINDOWED, STARTUP_MAXIMIZED, STARTUP_FULLSCREEN } startup_mode; - enum {DPI_AWARE_AUTO, DPI_AWARE_YES, DPI_AWARE_NO} dpi_aware; + bool dpi_aware; struct config_font_list fonts[4]; struct font_size_adjustment font_size_adjustment; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index f9174fc6..e28cf416 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -185,7 +185,7 @@ empty string to be set, but it must be quoted: *KEY=""*) Default: _no_. *dpi-aware* - *auto*, *yes*, or *no*. + Boolean. When set to *yes*, fonts are sized using the monitor's DPI, making a font of a given size have the same physical size, regardless of @@ -199,12 +199,6 @@ empty string to be set, but it must be quoted: *KEY=""*) instead sized using the monitor's scaling factor; doubling the scaling factor *does* double the font size. - Finally, if set to *auto*, fonts will be sized using the monitor's - DPI if _all_ monitors have a scaling factor of 1. If at least one - monitor as a scaling factor larger than 1 (regardless of whether - the foot window is mapped on that monitor or not), fonts will be - scaled using the scaling factor. - Note that this option typically does not work with bitmap fonts, which only contains a pre-defined set of sizes, and cannot be dynamically scaled. Whichever size (of the available ones) that @@ -217,7 +211,7 @@ empty string to be set, but it must be quoted: *KEY=""*) to size the font (*dpi-aware=no*), the font's pixel size will be multiplied with the scaling factor. - Default: _auto_ + Default: _no_ *pad* Padding between border and glyphs, in pixels (subject to output diff --git a/foot.ini b/foot.ini index a4b91ef7..61e88aec 100644 --- a/foot.ini +++ b/foot.ini @@ -20,7 +20,7 @@ # underline-offset= # underline-thickness= # box-drawings-uses-font-glyphs=no -# dpi-aware=auto +# dpi-aware=no # initial-window-size-pixels=700x500 # Or, # initial-window-size-chars= diff --git a/terminal.c b/terminal.c index 825e1550..090cbb20 100644 --- a/terminal.c +++ b/terminal.c @@ -912,42 +912,6 @@ get_font_subpixel(const struct terminal *term) return FCFT_SUBPIXEL_DEFAULT; } -static bool -term_font_size_by_dpi(const struct terminal *term) -{ - switch (term->conf->dpi_aware) { - case DPI_AWARE_YES: return true; - case DPI_AWARE_NO: return false; - - case DPI_AWARE_AUTO: - /* - * Scale using DPI if all monitors have a scaling factor or 1. - * - * The idea is this: if a user, with multiple monitors, have - * enabled scaling on at least one monitor, then he/she has - * most likely done so to match the size of his/hers other - * monitors. - * - * I.e. if the user has one monitor with a scaling factor of - * one, and another with a scaling factor of two, he/she - * expects things to be twice as large on the second - * monitor. - * - * If we (foot) scale using DPI on the first monitor, and - * using the scaling factor on the second monitor, foot will - * *not* look twice as big on the second monitor. - */ - tll_foreach(term->wl->monitors, it) { - const struct monitor *mon = &it->item; - if (mon->scale > 1) - return false; - } - return true; - } - - BUG("unhandled DPI awareness value"); -} - int term_pt_or_px_as_pixels(const struct terminal *term, const struct pt_or_px *pt_or_px) @@ -2158,7 +2122,7 @@ term_font_dpi_changed(struct terminal *term, int old_scale) xassert(term->scale > 0); bool was_scaled_using_dpi = term->font_is_sized_by_dpi; - bool will_scale_using_dpi = term_font_size_by_dpi(term); + bool will_scale_using_dpi = term->conf->dpi_aware; bool need_font_reload = was_scaled_using_dpi != will_scale_using_dpi || diff --git a/tests/test-config.c b/tests/test-config.c index b14f28b1..c70f7a43 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -511,6 +511,7 @@ test_section_main(void) test_boolean(&ctx, &parse_section_main, "box-drawings-uses-font-glyphs", &conf.box_drawings_uses_font_glyphs); test_boolean(&ctx, &parse_section_main, "locked-title", &conf.locked_title); test_boolean(&ctx, &parse_section_main, "notify-focus-inhibit", &conf.notify_focus_inhibit); + test_boolean(&ctx, &parse_section_main, "dpi-aware", &conf.dpi_aware); test_pt_or_px(&ctx, &parse_section_main, "font-size-adjustment", &conf.font_size_adjustment.pt_or_px); /* TODO: test ‘N%’ values too */ test_pt_or_px(&ctx, &parse_section_main, "line-height", &conf.line_height); @@ -524,17 +525,6 @@ test_section_main(void) test_spawn_template(&ctx, &parse_section_main, "notify", &conf.notify); - test_enum( - &ctx, &parse_section_main, "dpi-aware", - 9, - (const char *[]){"on", "true", "yes", "1", - "off", "false", "no", "0", - "auto"}, - (int []){DPI_AWARE_YES, DPI_AWARE_YES, DPI_AWARE_YES, DPI_AWARE_YES, - DPI_AWARE_NO, DPI_AWARE_NO, DPI_AWARE_NO, DPI_AWARE_NO, - DPI_AWARE_AUTO}, - (int *)&conf.dpi_aware); - test_enum(&ctx, &parse_section_main, "selection-target", 4, (const char *[]){"none", "primary", "clipboard", "both"}, diff --git a/wayland.c b/wayland.c index 683338c0..0f5cad2b 100644 --- a/wayland.c +++ b/wayland.c @@ -373,11 +373,6 @@ update_terms_on_monitor(struct monitor *mon) tll_foreach(wayl->terms, it) { struct terminal *term = it->item; - if (term->conf->dpi_aware == DPI_AWARE_AUTO) { - update_term_for_output_change(term); - continue; - } - tll_foreach(term->window->on_outputs, it2) { if (it2->item == mon) { update_term_for_output_change(term); From 27a92b11588203c90c0ec4c61a1a6c7a741b2935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Jun 2023 17:55:59 +0200 Subject: [PATCH 0345/1323] =?UTF-8?q?changelog:=20dpi-aware=E2=80=99s=20de?= =?UTF-8?q?fault=20value=20is=20now=20=E2=80=98no=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3e66216..ff63610e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,8 @@ is explicitly added. * Default sixel aspect ratio is now 2:1 instead of 1:1. * Sixel images are no longer cropped to the last non-transparent row. +* `dpi-aware` now defaults to `no`, and the `auto` value has been + removed. [1371]: https://codeberg.org/dnkl/foot/pulls/1371 @@ -92,6 +94,10 @@ ### Removed + +* `auto` value for the `dpi-aware` option. + + ### Fixed * Incorrect icon in dock and window switcher on Gnome ([#1317][1317]) From 9db92bd942706d6d0ca0250d6011c49ec88f4051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Jun 2023 18:00:01 +0200 Subject: [PATCH 0346/1323] feature: add a feature flag (for --version) for fractional scaling --- client.c | 4 +++- foot-features.h | 9 +++++++++ main.c | 4 +++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/client.c b/client.c index 6954d17e..84bfb2c3 100644 --- a/client.c +++ b/client.c @@ -66,11 +66,13 @@ static const char * version_and_features(void) { static char buf[256]; - snprintf(buf, sizeof(buf), "version: %s %cpgo %cime %cgraphemes %cassertions", + snprintf(buf, sizeof(buf), + "version: %s %cpgo %cime %cgraphemes %cfractional-scaling %cassertions", FOOT_VERSION, feature_pgo() ? '+' : '-', feature_ime() ? '+' : '-', feature_graphemes() ? '+' : '-', + feature_fractional_scaling() ? '+' : ':', feature_assertions() ? '+' : '-'); return buf; } diff --git a/foot-features.h b/foot-features.h index ad447767..77923aaf 100644 --- a/foot-features.h +++ b/foot-features.h @@ -37,3 +37,12 @@ static inline bool feature_graphemes(void) return false; #endif } + +static inline bool feature_fractional_scaling(void) +{ +#if defined(HAVE_FRACTIONAL_SCALE) + return true; +#else + return false; +#endif +} diff --git a/main.c b/main.c index f58e170f..6dd9e468 100644 --- a/main.c +++ b/main.c @@ -52,11 +52,13 @@ static const char * version_and_features(void) { static char buf[256]; - snprintf(buf, sizeof(buf), "version: %s %cpgo %cime %cgraphemes %cassertions", + snprintf(buf, sizeof(buf), + "version: %s %cpgo %cime %cgraphemes %cfractional-scaling %cassertions", FOOT_VERSION, feature_pgo() ? '+' : '-', feature_ime() ? '+' : '-', feature_graphemes() ? '+' : '-', + feature_fractional_scaling() ? '+' : '-', feature_assertions() ? '+' : '-'); return buf; } From 8a4efb34276e9f15a65304477c40e71aa281f7ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Jun 2023 18:37:49 +0200 Subject: [PATCH 0347/1323] =?UTF-8?q?wayland:=20warn=20when=20fractional?= =?UTF-8?q?=20scaling=20isn=E2=80=99t=20available?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wayland.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/wayland.c b/wayland.c index 0f5cad2b..28bb2b6f 100644 --- a/wayland.c +++ b/wayland.c @@ -1386,6 +1386,14 @@ wayl_init(struct fdm *fdm, struct key_binding_manager *key_binding_manager, "bell.urgent will fall back to coloring the window margins red"); } +#if defined(HAVE_FRACTIONAL_SCALE) + if (wayl->fractional_scale_manager == NULL || wayl->viewporter == NULL) { +#else + if (true) { +#endif + LOG_WARN("fractional scaling not available"); + } + if (presentation_timings && wayl->presentation == NULL) { LOG_ERR("presentation time interface not implemented by compositor"); goto out; From c309c9f572a331881c515d7715d474ef854ea958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Jun 2023 20:25:16 +0200 Subject: [PATCH 0348/1323] wayland: surface_scale(): assert surface width/height is a multiple of scale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When doing legacy scaling (non-fractional scaling), assert the surface’s width and height are multiples of the (integer) scale. --- wayland.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/wayland.c b/wayland.c index 28bb2b6f..ce0e879f 100644 --- a/wayland.c +++ b/wayland.c @@ -1908,6 +1908,13 @@ wayl_surface_scale_explicit_width_height( #endif } else { LOG_DBG("scaling by a factor of %.2f (legacy)", scale); + + xassert(scale == floor(scale)); + + const int iscale = (int)scale; + xassert(width % iscale == 0); + xassert(height % iscale == 0); + wl_surface_set_buffer_scale(surf->surf, (int)scale); } } From d71e588800fea0f7b7815cc5ce51ded752070b95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Jun 2023 21:06:47 +0200 Subject: [PATCH 0349/1323] wayland: refactor: surface_scale(): pass wl_window pointer, instead of wayland global --- render.c | 11 ++++++----- wayland.c | 11 +++++------ wayland.h | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/render.c b/render.c index 72d87bff..11b5223a 100644 --- a/render.c +++ b/render.c @@ -1692,7 +1692,7 @@ render_overlay(struct terminal *term) quirk_weston_subsurface_desync_on(overlay->sub); wayl_surface_scale( - term->wl, &overlay->surface, buf, term->scale); + term->window, &overlay->surface, buf, term->scale); wl_subsurface_set_position(overlay->sub, 0, 0); wl_surface_attach(overlay->surface.surf, buf->wl_buf, 0, 0); @@ -1831,7 +1831,7 @@ get_csd_data(const struct terminal *term, enum csd_surface surf_idx) static void csd_commit(struct terminal *term, struct wayl_surface *surf, struct buffer *buf) { - wayl_surface_scale(term->wl, surf, buf, term->scale); + wayl_surface_scale(term->window, surf, buf, term->scale); wl_surface_attach(surf->surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer(surf->surf, 0, 0, buf->width, buf->height); wl_surface_commit(surf->surf); @@ -1924,7 +1924,7 @@ render_osd(struct terminal *term, const struct wayl_sub_surface *sub_surf, pixman_image_set_clip_region32(buf->pix[0], NULL); quirk_weston_subsurface_desync_on(sub_surf->sub); - wayl_surface_scale(term->wl, &sub_surf->surface, buf, term->scale); + wayl_surface_scale(term->window, &sub_surf->surface, buf, term->scale); wl_surface_attach(sub_surf->surface.surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer(sub_surf->surface.surf, 0, 0, buf->width, buf->height); @@ -3371,7 +3371,7 @@ render_search_box(struct terminal *term) margin / term->scale, max(0, (int32_t)term->height - height - margin) / term->scale); - wayl_surface_scale(term->wl, &term->window->search.surface, buf, term->scale); + wayl_surface_scale(term->window, &term->window->search.surface, buf, term->scale); wl_surface_attach(term->window->search.surface.surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer(term->window->search.surface.surf, 0, 0, width, height); @@ -4265,7 +4265,8 @@ render_xcursor_update(struct seat *seat) struct wl_buffer *buf = wl_cursor_image_get_buffer(image); wayl_surface_scale_explicit_width_height( - seat->wayl, &seat->pointer.surface, image->width, image->height, scale); + seat->mouse_focus->window, + &seat->pointer.surface, image->width, image->height, scale); wl_surface_attach(seat->pointer.surface.surf, buf, 0, 0); diff --git a/wayland.c b/wayland.c index ce0e879f..aa1b298c 100644 --- a/wayland.c +++ b/wayland.c @@ -1891,7 +1891,7 @@ wayl_fractional_scaling(const struct wayland *wayl) void wayl_surface_scale_explicit_width_height( - const struct wayland *wayl, const struct wayl_surface *surf, + const struct wl_window *win, const struct wayl_surface *surf, int width, int height, float scale) { @@ -1915,26 +1915,25 @@ wayl_surface_scale_explicit_width_height( xassert(width % iscale == 0); xassert(height % iscale == 0); - wl_surface_set_buffer_scale(surf->surf, (int)scale); + wl_surface_set_buffer_scale(surf->surf, iscale); } } void -wayl_surface_scale(const struct wayland *wayl, const struct wayl_surface *surf, +wayl_surface_scale(const struct wl_window *win, const struct wayl_surface *surf, const struct buffer *buf, float scale) { wayl_surface_scale_explicit_width_height( - wayl, surf, buf->width, buf->height, scale); + win, surf, buf->width, buf->height, scale); } void wayl_win_scale(struct wl_window *win, const struct buffer *buf) { const struct terminal *term = win->term; - const struct wayland *wayl = term->wl; const float scale = term->scale; - wayl_surface_scale(wayl, &win->surface, buf, scale); + wayl_surface_scale(win, &win->surface, buf, scale); } void diff --git a/wayland.h b/wayland.h index 9736ea4d..7305ade7 100644 --- a/wayland.h +++ b/wayland.h @@ -456,10 +456,10 @@ void wayl_roundtrip(struct wayland *wayl); bool wayl_fractional_scaling(const struct wayland *wayl); void wayl_surface_scale( - const struct wayland *wayl, const struct wayl_surface *surf, + const struct wl_window *win, const struct wayl_surface *surf, const struct buffer *buf, float scale); void wayl_surface_scale_explicit_width_height( - const struct wayland *wayl, const struct wayl_surface *surf, + const struct wl_window *win, const struct wayl_surface *surf, int width, int height, float scale); struct wl_window *wayl_win_init(struct terminal *term, const char *token); From 8f74b1090a6a5297ac7b6710420528ff56ed03d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Jun 2023 21:07:24 +0200 Subject: [PATCH 0350/1323] wayland: use legacy scaling until fractional_scale::preferred_scale() has been called This way, the initial frame is more likely to get scaled correctly; foot will guess the initial (integer) scale from the available monitors, and use that. By using legacy scaling, we force the compositor to down-scale the image to the correct scale factor. If we use the new fraction scaling method with an integer scaling factor, the initial frame gets rendered way too big. --- wayland.c | 3 ++- wayland.h | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/wayland.c b/wayland.c index aa1b298c..bf8f2682 100644 --- a/wayland.c +++ b/wayland.c @@ -1529,6 +1529,7 @@ static void fractional_scale_preferred_scale( { struct wl_window *win = data; win->scale = (float)scale / 120.; + win->have_preferred_scale = true; LOG_DBG("fractional scale: %.3f", win->scale); update_term_for_output_change(win->term); @@ -1895,7 +1896,7 @@ wayl_surface_scale_explicit_width_height( int width, int height, float scale) { - if (wayl_fractional_scaling(wayl)) { + if (wayl_fractional_scaling(win->term->wl) && win->have_preferred_scale) { #if defined(HAVE_FRACTIONAL_SCALE) LOG_DBG("scaling by a factor of %.2f (fractional scaling)", scale); wp_viewport_set_destination( diff --git a/wayland.h b/wayland.h index 7305ade7..e2d22031 100644 --- a/wayland.h +++ b/wayland.h @@ -346,6 +346,7 @@ struct wl_window { bool unmapped; float scale; + bool have_preferred_scale; struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration; From c61247f317b8b0559feeda551e8e7e33b54d3b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Jun 2023 21:09:18 +0200 Subject: [PATCH 0351/1323] wayland: surface_scale(): improve debug logging --- wayland.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/wayland.c b/wayland.c index bf8f2682..cff432bb 100644 --- a/wayland.c +++ b/wayland.c @@ -1898,7 +1898,9 @@ wayl_surface_scale_explicit_width_height( if (wayl_fractional_scaling(win->term->wl) && win->have_preferred_scale) { #if defined(HAVE_FRACTIONAL_SCALE) - LOG_DBG("scaling by a factor of %.2f (fractional scaling)", scale); + LOG_DBG("scaling by a factor of %.2f using fractional scaling " + "(width=%d, height=%d) ", scale, width, height); + wp_viewport_set_destination( surf->viewport, round((float)width / scale), @@ -1908,7 +1910,8 @@ wayl_surface_scale_explicit_width_height( "but fractional scaling was not available at compile time"). #endif } else { - LOG_DBG("scaling by a factor of %.2f (legacy)", scale); + LOG_DBG("scaling by a factor of %.2f using legacy mode " + "(width=%d, height=%d)", scale, width, height); xassert(scale == floor(scale)); From ce31cc518a48d978c84448424d0d2a6bba4a5a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 26 Jun 2023 21:09:30 +0200 Subject: [PATCH 0352/1323] wayland: surface_scale(): reset buffer scale when using fractional scaling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since the first frame uses legacy scaling, the surface may have a buffer scale > 1, which isn’t allowed. --- wayland.c | 1 + 1 file changed, 1 insertion(+) diff --git a/wayland.c b/wayland.c index cff432bb..c9693200 100644 --- a/wayland.c +++ b/wayland.c @@ -1901,6 +1901,7 @@ wayl_surface_scale_explicit_width_height( LOG_DBG("scaling by a factor of %.2f using fractional scaling " "(width=%d, height=%d) ", scale, width, height); + wl_surface_set_buffer_scale(surf->surf, 1); wp_viewport_set_destination( surf->viewport, round((float)width / scale), From 3555e81fee4a069bf4886007082f676b8777f6f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 27 Jun 2023 14:25:55 +0200 Subject: [PATCH 0353/1323] sixel: special case parsing of images with an aspect ratio of 1:1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Images with an aspect ratio of 1:1 are by far the most common (though not the default). It makes a lot of sense, performance wise, to special case them. Specifically, the sixel_add() function benefits greatly from this, as it is the inner most, most heavily executed function when parsing a sixel image. sixel_add_many() also benefits, since allows us to drop a multiplication. Since sixel_add_many() always called first (no other call sites call sixel_add() directly), this has a noticeable effect on performance. Another thing that helps (though not as much), and not specifically with AR 1:1 images, is special casing DECGRI a bit. Up until now, it simply updated the current sixel parameter value. The problem is that the default parameter value is 0. But, a value of 0 should be treated as 1. By adding a special ‘repeat_count’ variable to the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and then simply overwrite it as the parameter value gets updated. This allows us to drop an if..else when emitting the sixel. --- dcs.c | 3 +- sixel.c | 171 ++++++++++++++++++++++++++++++++++++++++++++++------- sixel.h | 5 +- terminal.h | 1 + 4 files changed, 153 insertions(+), 27 deletions(-) diff --git a/dcs.c b/dcs.c index fb4a14b6..7ce1a868 100644 --- a/dcs.c +++ b/dcs.c @@ -427,8 +427,7 @@ dcs_hook(struct terminal *term, uint8_t final) int p2 = vt_param_get(term, 1,0); int p3 = vt_param_get(term, 2, 0); - sixel_init(term, p1, p2, p3); - term->vt.dcs.put_handler = &sixel_put; + term->vt.dcs.put_handler = sixel_init(term, p1, p2, p3); term->vt.dcs.unhook_handler = &sixel_unhook; break; } diff --git a/sixel.c b/sixel.c index b0746a9a..e454032c 100644 --- a/sixel.c +++ b/sixel.c @@ -16,6 +16,9 @@ static size_t count; +static void sixel_put_generic(struct terminal *term, uint8_t c); +static void sixel_put_ar_11(struct terminal *term, uint8_t c); + void sixel_fini(struct terminal *term) { @@ -24,7 +27,7 @@ sixel_fini(struct terminal *term) free(term->sixel.shared_palette); } -void +sixel_put sixel_init(struct terminal *term, int p1, int p2, int p3) { /* @@ -119,6 +122,7 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) : bg; count = 0; + return pan == 1 && pad == 1 ? &sixel_put_ar_11 : &sixel_put_generic; } void @@ -1327,7 +1331,8 @@ resize(struct terminal *term, int new_width, int new_height) } static void -sixel_add(struct terminal *term, int col, int width, uint32_t color, uint8_t sixel) +sixel_add_generic(struct terminal *term, int col, int width, uint32_t color, + uint8_t sixel) { xassert(term->sixel.pos.col < term->sixel.image.width); xassert(term->sixel.pos.row < term->sixel.image.height); @@ -1348,7 +1353,26 @@ sixel_add(struct terminal *term, int col, int width, uint32_t color, uint8_t six } static void -sixel_add_many(struct terminal *term, uint8_t c, unsigned count) +sixel_add_ar_11(struct terminal *term, int col, int width, 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); + + 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, data += width) { + if (sixel & 1) + *data = color; + } + + xassert(sixel == 0); +} + +static void +sixel_add_many_generic(struct terminal *term, uint8_t c, unsigned count) { int col = term->sixel.pos.col; int width = term->sixel.image.width; @@ -1362,14 +1386,38 @@ sixel_add_many(struct terminal *term, uint8_t c, unsigned count) } uint32_t color = term->sixel.color; - for (unsigned i = 0; i < count; i++, col++) - sixel_add(term, col, width, color, c); + 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); + } term->sixel.pos.col = col; } static void -decsixel(struct terminal *term, uint8_t c) +sixel_add_many_ar_11(struct terminal *term, uint8_t c, unsigned count) +{ + xassert(term->sixel.pan == 1); + xassert(term->sixel.pad == 1); + + int col = term->sixel.pos.col; + int width = term->sixel.image.width; + + if (unlikely(col + count - 1 >= width)) { + resize_horizontally(term, col + count); + width = term->sixel.image.width; + count = min(count, max(width - col, 0)); + } + + uint32_t color = term->sixel.color; + for (unsigned i = 0; i < count; i++, col++) + sixel_add_ar_11(term, col, width, color, c); + + term->sixel.pos.col = col; +} + +static void +decsixel_generic(struct terminal *term, uint8_t c) { switch (c) { case '"': @@ -1382,6 +1430,7 @@ decsixel(struct terminal *term, uint8_t c) term->sixel.state = SIXEL_DECGRI; term->sixel.param = 0; term->sixel.param_idx = 0; + term->sixel.repeat_count = 1; break; case '#': @@ -1424,7 +1473,7 @@ decsixel(struct terminal *term, uint8_t c) case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': case '{': case '|': case '}': case '~': - sixel_add_many(term, c - 63, 1); + sixel_add_many_generic(term, c - 63, 1); break; case ' ': @@ -1438,6 +1487,29 @@ decsixel(struct terminal *term, uint8_t c) } } +static void +decsixel_ar_11(struct terminal *term, uint8_t c) +{ + switch (c) { + case '?': case '@': case 'A': case 'B': case 'C': case 'D': case 'E': + case 'F': case 'G': case 'H': case 'I': case 'J': case 'K': case 'L': + case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': + case 'T': case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': + case '[': case '\\': case ']': case '^': case '_': case '`': case 'a': + case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': + case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': + case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': + case 'w': case 'x': case 'y': case 'z': case '{': case '|': case '}': + case '~': + sixel_add_many_ar_11(term, c - 63, 1); + break; + + default: + decsixel_generic(term, c); + break; + } +} + static void decgra(struct terminal *term, uint8_t c) { @@ -1483,21 +1555,34 @@ decgra(struct terminal *term, uint8_t c) } term->sixel.state = SIXEL_DECSIXEL; - decsixel(term, c); + + /* Update DCS put handler, since pan/pad may have changed */ + term->vt.dcs.put_handler = pan == 1 && pad == 1 + ? &sixel_put_ar_11 + : &sixel_put_generic; + + if (likely(pan == 1 && pad == 1)) + decsixel_ar_11(term, c); + else + decsixel_generic(term, c); + break; } } } static void -decgri(struct terminal *term, uint8_t c) +decgri_generic(struct terminal *term, uint8_t c) { switch (c) { case '0': case '1': case '2': case '3': case '4': - case '5': case '6': case '7': case '8': case '9': - term->sixel.param *= 10; - term->sixel.param += c - '0'; + case '5': case '6': case '7': case '8': case '9': { + unsigned param = term->sixel.param; + param *= 10; + param += c - '0'; + term->sixel.repeat_count = term->sixel.param = param; break; + } case '?': case '@': case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': case 'J': case 'K': case 'L': @@ -1509,18 +1594,41 @@ decgri(struct terminal *term, uint8_t c) case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': case '{': case '|': case '}': case '~': { - unsigned count = term->sixel.param; - if (likely(count > 0)) - sixel_add_many(term, c - 63, count); - else if (unlikely(count == 0)) - sixel_add_many(term, c - 63, 1); + const unsigned count = term->sixel.repeat_count; + sixel_add_many_generic(term, c - 63, count); term->sixel.state = SIXEL_DECSIXEL; break; } default: term->sixel.state = SIXEL_DECSIXEL; - sixel_put(term, c); + term->vt.dcs.put_handler(term, c); + break; + } +} + +static void +decgri_ar_11(struct terminal *term, uint8_t c) +{ + switch (c) { + case '?': case '@': case 'A': case 'B': case 'C': case 'D': case 'E': + case 'F': case 'G': case 'H': case 'I': case 'J': case 'K': case 'L': + case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': + case 'T': case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': + case '[': case '\\': case ']': case '^': case '_': case '`': case 'a': + case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': + case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': + case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': + case 'w': case 'x': case 'y': case 'z': case '{': case '|': case '}': + case '~': { + const unsigned count = term->sixel.repeat_count; + sixel_add_many_ar_11(term, c - 63, count); + term->sixel.state = SIXEL_DECSIXEL; + break; + } + + default: + decgri_generic(term, c); break; } } @@ -1601,19 +1709,36 @@ decgci(struct terminal *term, uint8_t c) term->sixel.color = term->sixel.palette[term->sixel.color_idx]; term->sixel.state = SIXEL_DECSIXEL; - decsixel(term, c); + + if (likely(term->sixel.pan == 1 && term->sixel.pad == 1)) + decsixel_ar_11(term, c); + else + decsixel_generic(term, c); break; } } } -void -sixel_put(struct terminal *term, uint8_t c) +static void +sixel_put_generic(struct terminal *term, uint8_t c) { switch (term->sixel.state) { - case SIXEL_DECSIXEL: decsixel(term, c); break; + case SIXEL_DECSIXEL: decsixel_generic(term, c); break; case SIXEL_DECGRA: decgra(term, c); break; - case SIXEL_DECGRI: decgri(term, c); break; + case SIXEL_DECGRI: decgri_generic(term, c); break; + case SIXEL_DECGCI: decgci(term, c); break; + } + + count++; +} + +static void +sixel_put_ar_11(struct terminal *term, uint8_t c) +{ + switch (term->sixel.state) { + case SIXEL_DECSIXEL: decsixel_ar_11(term, c); break; + case SIXEL_DECGRA: decgra(term, c); break; + case SIXEL_DECGRI: decgri_ar_11(term, c); break; case SIXEL_DECGCI: decgci(term, c); break; } diff --git a/sixel.h b/sixel.h index f72b4dc4..efd64908 100644 --- a/sixel.h +++ b/sixel.h @@ -6,10 +6,11 @@ #define SIXEL_MAX_WIDTH 10000u #define SIXEL_MAX_HEIGHT 10000u +typedef void (*sixel_put)(struct terminal *term, uint8_t c); + void sixel_fini(struct terminal *term); -void sixel_init(struct terminal *term, int p1, int p2, int p3); -void sixel_put(struct terminal *term, uint8_t c); +sixel_put sixel_init(struct terminal *term, int p1, int p2, int p3); void sixel_unhook(struct terminal *term); void sixel_destroy(struct sixel *sixel); diff --git a/terminal.h b/terminal.h index 220070e1..1cb62686 100644 --- a/terminal.h +++ b/terminal.h @@ -649,6 +649,7 @@ struct terminal { unsigned params[5]; /* Collected parameters, for RASTER, COLOR_SPEC */ unsigned param; /* Currently collecting parameter, for RASTER, COLOR_SPEC and REPEAT */ unsigned param_idx; /* Parameters seen */ + unsigned repeat_count; bool transparent_bg; uint32_t default_bg; From 75f9bed6b6fc7bc97927d70e9e7df75dcea621c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 27 Jun 2023 15:39:11 +0200 Subject: [PATCH 0354/1323] sixel: refactor: shorten very verbose switch case statements --- sixel.c | 67 ++++++++++++--------------------------------------------- 1 file changed, 14 insertions(+), 53 deletions(-) diff --git a/sixel.c b/sixel.c index e454032c..ec0cbc5a 100644 --- a/sixel.c +++ b/sixel.c @@ -1416,6 +1416,8 @@ sixel_add_many_ar_11(struct terminal *term, uint8_t c, unsigned count) term->sixel.pos.col = col; } +IGNORE_WARNING("-Wpedantic") + static void decsixel_generic(struct terminal *term, uint8_t c) { @@ -1463,16 +1465,7 @@ decsixel_generic(struct terminal *term, uint8_t c) } break; - case '?': case '@': case 'A': case 'B': case 'C': case 'D': case 'E': - case 'F': case 'G': case 'H': case 'I': case 'J': case 'K': case 'L': - case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': - case 'T': case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': - case '[': case '\\': case ']': case '^': case '_': case '`': case 'a': - case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': - case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': - case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': - case 'w': case 'x': case 'y': case 'z': case '{': case '|': case '}': - case '~': + case '?' ... '~': sixel_add_many_generic(term, c - 63, 1); break; @@ -1487,27 +1480,15 @@ decsixel_generic(struct terminal *term, uint8_t c) } } +UNIGNORE_WARNINGS + static void decsixel_ar_11(struct terminal *term, uint8_t c) { - switch (c) { - case '?': case '@': case 'A': case 'B': case 'C': case 'D': case 'E': - case 'F': case 'G': case 'H': case 'I': case 'J': case 'K': case 'L': - case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': - case 'T': case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': - case '[': case '\\': case ']': case '^': case '_': case '`': case 'a': - case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': - case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': - case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': - case 'w': case 'x': case 'y': case 'z': case '{': case '|': case '}': - case '~': + if (likely(c >= '?' && c <= '~')) sixel_add_many_ar_11(term, c - 63, 1); - break; - - default: + else decsixel_generic(term, c); - break; - } } static void @@ -1571,6 +1552,8 @@ decgra(struct terminal *term, uint8_t c) } } +IGNORE_WARNING("-Wpedantic") + static void decgri_generic(struct terminal *term, uint8_t c) { @@ -1584,16 +1567,7 @@ decgri_generic(struct terminal *term, uint8_t c) break; } - case '?': case '@': case 'A': case 'B': case 'C': case 'D': case 'E': - case 'F': case 'G': case 'H': case 'I': case 'J': case 'K': case 'L': - case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': - case 'T': case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': - case '[': case '\\': case ']': case '^': case '_': case '`': case 'a': - case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': - case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': - case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': - case 'w': case 'x': case 'y': case 'z': case '{': case '|': case '}': - case '~': { + case '?' ... '~': { const unsigned count = term->sixel.repeat_count; sixel_add_many_generic(term, c - 63, count); term->sixel.state = SIXEL_DECSIXEL; @@ -1607,30 +1581,17 @@ decgri_generic(struct terminal *term, uint8_t c) } } +UNIGNORE_WARNINGS + static void decgri_ar_11(struct terminal *term, uint8_t c) { - switch (c) { - case '?': case '@': case 'A': case 'B': case 'C': case 'D': case 'E': - case 'F': case 'G': case 'H': case 'I': case 'J': case 'K': case 'L': - case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': - case 'T': case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': - case '[': case '\\': case ']': case '^': case '_': case '`': case 'a': - case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': - case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': - case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': - case 'w': case 'x': case 'y': case 'z': case '{': case '|': case '}': - case '~': { + if (likely(c >= '?' && c <= '~')) { const unsigned count = term->sixel.repeat_count; sixel_add_many_ar_11(term, c - 63, count); term->sixel.state = SIXEL_DECSIXEL; - break; - } - - default: + } else decgri_generic(term, c); - break; - } } static void From fc46087ce9c451acded2692bcb9064ba7a716e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 27 Jun 2023 15:49:47 +0200 Subject: [PATCH 0355/1323] scripts: generate-alt-random: set P2=1 when emitting sixels P2=1 means "empty sixels remain at their current color". This is usually the case with modern sixel encoders. --- scripts/generate-alt-random-writes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/generate-alt-random-writes.py b/scripts/generate-alt-random-writes.py index 812b0213..789d64e0 100755 --- a/scripts/generate-alt-random-writes.py +++ b/scripts/generate-alt-random-writes.py @@ -207,8 +207,8 @@ def main(): six_height, six_width = last_size six_rows = (six_height + 5) // 6 # Round up; each sixel is 6 pixels - # Begin sixel - out.write('\033Pq') + # Begin sixel (with P2=1 - empty sixels are transparent) + out.write('\033P;1q') # Sixel size. Without this, sixels will be # auto-resized on cell-boundaries. From 5e9d68695c05d4be03ac7b83dd0eabc42bb7fefd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 27 Jun 2023 16:21:26 +0200 Subject: [PATCH 0356/1323] sixel: add_ar_11(): manually unroll loop This generates both smaller, and faster code --- sixel.c | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/sixel.c b/sixel.c index ec0cbc5a..31a55a69 100644 --- a/sixel.c +++ b/sixel.c @@ -1360,15 +1360,26 @@ sixel_add_ar_11(struct terminal *term, int col, int width, uint32_t color, xassert(term->sixel.pos.row < term->sixel.image.height); xassert(term->sixel.pan == 1); - size_t ofs = term->sixel.row_byte_ofs + col; + const 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, data += width) { - if (sixel & 1) - *data = color; - } - - xassert(sixel == 0); + if (sixel & 0x01) + *data = color; + data += width; + if (sixel & 0x02) + *data = color; + data += width; + if (sixel & 0x04) + *data = color; + data += width; + if (sixel & 0x08) + *data = color; + data += width; + if (sixel & 0x10) + *data = color; + data += width; + if (sixel & 0x20) + *data = color; } static void From 5e305fa8547bbdeef765813ca4a1c29037248ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 30 Jun 2023 08:24:02 +0200 Subject: [PATCH 0357/1323] =?UTF-8?q?wayland:=20typo:=20=E2=80=98.?= =?UTF-8?q?=E2=80=99=20->=20=E2=80=98;=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1392 --- wayland.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wayland.c b/wayland.c index c9693200..406aba6d 100644 --- a/wayland.c +++ b/wayland.c @@ -1908,7 +1908,7 @@ wayl_surface_scale_explicit_width_height( round((float)height / scale)); #else BUG("wayl_fraction_scaling() returned true, " - "but fractional scaling was not available at compile time"). + "but fractional scaling was not available at compile time"); #endif } else { LOG_DBG("scaling by a factor of %.2f using legacy mode " From 7a37e6891f898e6a26177d8cdd83863bd2671758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 30 Jun 2023 08:28:20 +0200 Subject: [PATCH 0358/1323] meson: log availability of optional wayland protocols --- meson.build | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/meson.build b/meson.build index 3bad7ab2..6e219caf 100644 --- a/meson.build +++ b/meson.build @@ -157,11 +157,17 @@ wl_proto_xml = [ if wayland_protocols.version().version_compare('>=1.21') add_project_arguments('-DHAVE_XDG_ACTIVATION', language: 'c') wl_proto_xml += [wayland_protocols_datadir + '/staging/xdg-activation/xdg-activation-v1.xml'] + xdg_activation = true +else + xdg_activation = false endif if wayland_protocols.version().version_compare('>=1.31') add_project_arguments('-DHAVE_FRACTIONAL_SCALE', language: 'c') wl_proto_xml += [wayland_protocols_datadir + '/stable/viewporter/viewporter.xml'] wl_proto_xml += [wayland_protocols_datadir + '/staging/fractional-scale/fractional-scale-v1.xml'] + fractional_scale = true +else + fractional_scale = false endif foreach prot : wl_proto_xml @@ -372,6 +378,8 @@ summary( 'Themes': get_option('themes'), 'IME': get_option('ime'), 'Grapheme clustering': utf8proc.found(), + 'Wayland: xdg-activation-v1': xdg_activation, + 'Wayland: fractional-scale-v1': fractional_scale, 'utmp backend': utmp_backend, 'utmp helper default path': utmp_default_helper_path, 'Build terminfo': tic.found(), From 49fb0cf359bb2486bbdf58df62c66c890d7e31cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 29 Jun 2023 14:49:54 +0200 Subject: [PATCH 0359/1323] sixel: re-scale images when the cell dimensions change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this patch, when the cell dimensions changed (i.e. when the font size changes), sixel images were either removed (the new cell dimensions are smaller than the old), or simply kept at their original size (new cell dimensions are larger). With this patch, sixels are instead resized. This means a sixel *always* occupies the same number of rows and columns, regardless of how much the font size is changed. This is done by maintaining two sets of image data and pixman images, as well as their dimensions. These two sets are the new ‘original’ and ‘scaled’ members of the sixel struct. The "top-level" pixman image pointer, and the ‘width’ and ‘height’ members either point to the "original", or the "scaled" version. They are invalidated as soon as the cell dimensions change. They, and the ‘scaled’ image is updated on-demand (when we need to render a sixel). Note that the ‘scaled’ image is always NULL when the current cell dimensions matches the ones used when emitting the sixel (to save run-time memory). Closes #1383 --- CHANGELOG.md | 4 +- grid.c | 69 ++++++++++--- render.c | 5 + sixel.c | 266 +++++++++++++++++++++++++++++++++++++-------------- sixel.h | 1 + terminal.c | 26 +---- terminal.h | 36 ++++++- 7 files changed, 295 insertions(+), 112 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff63610e..09b47f09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,12 +80,14 @@ is explicitly added. * Default sixel aspect ratio is now 2:1 instead of 1:1. * Sixel images are no longer cropped to the last non-transparent row. +* Sixel images are now re-scaled when the font size is changed + ([#1383][1383]). * `dpi-aware` now defaults to `no`, and the `auto` value has been removed. - [1371]: https://codeberg.org/dnkl/foot/pulls/1371 [1360]: https://codeberg.org/dnkl/foot/issues/1360 +[1383]: https://codeberg.org/dnkl/foot/issues/1383 ### Deprecated diff --git a/grid.c b/grid.c index e1c4d28b..22d2a89a 100644 --- a/grid.c +++ b/grid.c @@ -255,27 +255,68 @@ grid_snapshot(const struct grid *grid) } tll_foreach(grid->sixel_images, it) { - int width = it->item.width; - int height = it->item.height; - pixman_image_t *pix = it->item.pix; - pixman_format_code_t pix_fmt = pixman_image_get_format(pix); - int stride = stride_for_format_and_width(pix_fmt, width); + int original_width = it->item.original.width; + int original_height = it->item.original.height; + pixman_image_t *original_pix = it->item.original.pix; + pixman_format_code_t original_pix_fmt = pixman_image_get_format(original_pix); + int original_stride = stride_for_format_and_width(original_pix_fmt, original_width); - size_t size = stride * height; - void *new_data = xmalloc(size); - memcpy(new_data, it->item.data, size); + size_t original_size = original_stride * original_height; + void *new_original_data = xmalloc(original_size); + memcpy(new_original_data, it->item.original.data, original_size); - pixman_image_t *new_pix = pixman_image_create_bits_no_clear( - pix_fmt, width, height, new_data, stride); + pixman_image_t *new_original_pix = pixman_image_create_bits_no_clear( + original_pix_fmt, original_width, original_height, + new_original_data, original_stride); + + void *new_scaled_data = NULL; + pixman_image_t *new_scaled_pix = NULL; + int scaled_width = -1; + int scaled_height = -1; + + if (it->item.scaled.data != NULL) { + scaled_width = it->item.scaled.width; + scaled_height = it->item.scaled.height; + + pixman_image_t *scaled_pix = it->item.scaled.pix; + pixman_format_code_t scaled_pix_fmt = pixman_image_get_format(scaled_pix); + int scaled_stride = stride_for_format_and_width(scaled_pix_fmt, scaled_width); + + size_t scaled_size = scaled_stride * scaled_height; + new_scaled_data = xmalloc(scaled_size); + memcpy(new_scaled_data, it->item.scaled.data, scaled_size); + + new_scaled_pix = pixman_image_create_bits_no_clear( + scaled_pix_fmt, scaled_width, scaled_height, new_scaled_data, + scaled_stride); + } struct sixel six = { - .data = new_data, - .pix = new_pix, - .width = width, - .height = height, + .pix = (it->item.pix == it->item.original.pix + ? new_original_pix + : (it->item.pix == it->item.scaled.pix + ? new_scaled_pix + : NULL)), + .width = it->item.width, + .height = it->item.height, .rows = it->item.rows, .cols = it->item.cols, .pos = it->item.pos, + .opaque = it->item.opaque, + .cell_width = it->item.cell_width, + .cell_height = it->item.cell_height, + .original = { + .data = new_original_data, + .pix = new_original_pix, + .width = original_width, + .height = original_height, + }, + .scaled = { + .data = new_scaled_data, + .pix = new_scaled_pix, + .width = scaled_width, + .height = scaled_height, + }, }; tll_push_back(clone->sixel_images, six); diff --git a/render.c b/render.c index 11b5223a..4498290e 100644 --- a/render.c +++ b/render.c @@ -1160,6 +1160,10 @@ static void render_sixel(struct terminal *term, pixman_image_t *pix, const struct coord *cursor, const struct sixel *sixel) { + xassert(sixel->pix != NULL); + xassert(sixel->width >= 0); + xassert(sixel->height >= 0); + const int view_end = (term->grid->view + term->rows - 1) & (term->grid->num_rows - 1); const bool last_row_needs_erase = sixel->height % term->cell_height != 0; const bool last_col_needs_erase = sixel->width % term->cell_width != 0; @@ -1324,6 +1328,7 @@ render_sixel_images(struct terminal *term, pixman_image_t *pix, break; } + sixel_sync_cache(term, &it->item); render_sixel(term, pix, cursor, &it->item); } } diff --git a/sixel.c b/sixel.c index 31a55a69..c7c04b0d 100644 --- a/sixel.c +++ b/sixel.c @@ -125,15 +125,34 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) return pan == 1 && pad == 1 ? &sixel_put_ar_11 : &sixel_put_generic; } +static void +sixel_invalidate_cache(struct sixel *sixel) +{ + if (sixel->scaled.pix != NULL) + pixman_image_unref(sixel->scaled.pix); + + free(sixel->scaled.data); + sixel->scaled.pix = NULL; + sixel->scaled.data = NULL; + sixel->scaled.width = -1; + sixel->scaled.height = -1; + + sixel->pix = NULL; + sixel->width = -1; + sixel->height = -1; +} + void sixel_destroy(struct sixel *sixel) { - if (sixel->pix != NULL) - pixman_image_unref(sixel->pix); + sixel_invalidate_cache(sixel); - free(sixel->data); - sixel->pix = NULL; - sixel->data = NULL; + if (sixel->original.pix != NULL) + pixman_image_unref(sixel->original.pix); + + free(sixel->original.data); + sixel->original.pix = NULL; + sixel->original.data = NULL; } void @@ -396,10 +415,14 @@ blend_new_image_over_old(const struct terminal *term, xassert(pix != NULL); xassert(opaque != NULL); - const int six_ofs_x = six->pos.col * term->cell_width; - const int six_ofs_y = six->pos.row * term->cell_height; - const int img_ofs_x = col * term->cell_width; - const int img_ofs_y = row * term->cell_height; + /* + * TODO: handle images being emitted with different cell dimensions + */ + + const int six_ofs_x = six->pos.col * six->cell_width; + const int six_ofs_y = six->pos.row * six->cell_height; + const int img_ofs_x = col * six->cell_width; + const int img_ofs_y = row * six->cell_height; const int img_width = pixman_image_get_width(*pix); const int img_height = pixman_image_get_height(*pix); @@ -429,7 +452,7 @@ blend_new_image_over_old(const struct terminal *term, */ pixman_image_composite32( PIXMAN_OP_OVER_REVERSE, - six->pix, NULL, *pix, + six->original.pix, NULL, *pix, box->x1 - six_ofs_x, box->y1 - six_ofs_y, 0, 0, box->x1 - img_ofs_x, box->y1 - img_ofs_y, @@ -446,15 +469,15 @@ blend_new_image_over_old(const struct terminal *term, * old image, or the next cell boundary, whichever comes * first. */ - int bounding_x = six_ofs_x + six->width > img_ofs_x + img_width + int bounding_x = six_ofs_x + six->original.width > img_ofs_x + img_width ? min( - six_ofs_x + six->width, - (box->x2 + term->cell_width - 1) / term->cell_width * term->cell_width) + six_ofs_x + six->original.width, + (box->x2 + six->cell_width - 1) / six->cell_width * six->cell_width) : box->x2; - int bounding_y = six_ofs_y + six->height > img_ofs_y + img_height + int bounding_y = six_ofs_y + six->original.height > img_ofs_y + img_height ? min( - six_ofs_y + six->height, - (box->y2 + term->cell_height - 1) / term->cell_height * term->cell_height) + six_ofs_y + six->original.height, + (box->y2 + six->cell_height - 1) / six->cell_height * six->cell_height) : box->y2; /* The required size of the new image */ @@ -494,7 +517,7 @@ blend_new_image_over_old(const struct terminal *term, /* Copy the bottom tile of the old sixel image into the new pixmap */ pixman_image_composite32( PIXMAN_OP_SRC, - six->pix, NULL, pix2, + six->original.pix, NULL, pix2, box->x1 - six_ofs_x, box->y2 - six_ofs_y, 0, 0, box->x1 - img_ofs_x, box->y2 - img_ofs_y, @@ -503,7 +526,7 @@ blend_new_image_over_old(const struct terminal *term, /* Copy the right tile of the old sixel image into the new pixmap */ pixman_image_composite32( PIXMAN_OP_SRC, - six->pix, NULL, pix2, + six->original.pix, NULL, pix2, box->x2 - six_ofs_x, box->y1 - six_ofs_y, 0, 0, box->x2 - img_ofs_x, box->y1 - img_ofs_y, @@ -577,14 +600,14 @@ sixel_overwrite(struct terminal *term, struct sixel *six, pixman_region32_t six_rect; pixman_region32_init_rect( &six_rect, - six->pos.col * term->cell_width, six->pos.row * term->cell_height, - six->width, six->height); + six->pos.col * six->cell_width, six->pos.row * six->cell_height, + six->original.width, six->original.height); pixman_region32_t overwrite_rect; pixman_region32_init_rect( &overwrite_rect, - col * term->cell_width, row * term->cell_height, - width * term->cell_width, height * term->cell_height); + col * six->cell_width, row * six->cell_height, + width * six->cell_width, height * six->cell_height); #if defined(_DEBUG) pixman_region32_t cell_intersection; @@ -597,7 +620,6 @@ sixel_overwrite(struct terminal *term, struct sixel *six, if (pix != NULL) blend_new_image_over_old(term, six, &six_rect, row, col, pix, opaque); - pixman_region32_t diff; pixman_region32_init(&diff); pixman_region32_subtract(&diff, &six_rect, &overwrite_rect); @@ -612,12 +634,12 @@ sixel_overwrite(struct terminal *term, struct sixel *six, LOG_DBG("box #%d: x1=%d, y1=%d, x2=%d, y2=%d", i, boxes[i].x1, boxes[i].y1, boxes[i].x2, boxes[i].y2); - xassert(boxes[i].x1 % term->cell_width == 0); - xassert(boxes[i].y1 % term->cell_height == 0); + xassert(boxes[i].x1 % six->cell_width == 0); + xassert(boxes[i].y1 % six->cell_height == 0); /* New image's position, in cells */ - const int new_col = boxes[i].x1 / term->cell_width; - const int new_row = boxes[i].y1 / term->cell_height; + const int new_col = boxes[i].x1 / six->cell_width; + const int new_row = boxes[i].y1 / six->cell_height; xassert(new_row < term->grid->num_rows); @@ -626,17 +648,17 @@ sixel_overwrite(struct terminal *term, struct sixel *six, const int new_height = boxes[i].y2 - boxes[i].y1; uint32_t *new_data = xmalloc(new_width * new_height * sizeof(uint32_t)); - const uint32_t *old_data = six->data; + const uint32_t *old_data = six->original.data; /* Pixel offsets into old image backing memory */ - const int x_ofs = boxes[i].x1 - six->pos.col * term->cell_width; - const int y_ofs = boxes[i].y1 - six->pos.row * term->cell_height; + const int x_ofs = boxes[i].x1 - six->pos.col * six->cell_width; + const int y_ofs = boxes[i].y1 - six->pos.row * six->cell_height; /* Copy image data, one row at a time */ for (size_t j = 0; j < new_height; j++) { memcpy( &new_data[(0 + j) * new_width], - &old_data[(y_ofs + j) * six->width + x_ofs], + &old_data[(y_ofs + j) * six->original.width + x_ofs], new_width * sizeof(uint32_t)); } @@ -645,14 +667,27 @@ sixel_overwrite(struct terminal *term, struct sixel *six, new_width, new_height, new_data, new_width * sizeof(uint32_t)); struct sixel new_six = { - .data = new_data, - .pix = new_pix, - .width = new_width, - .height = new_height, + .pix = NULL, + .width = -1, + .height = -1, .pos = {.col = new_col, .row = new_row}, - .cols = (new_width + term->cell_width - 1) / term->cell_width, - .rows = (new_height + term->cell_height - 1) / term->cell_height, + .cols = (new_width + six->cell_width - 1) / six->cell_width, + .rows = (new_height + six->cell_height - 1) / six->cell_height, .opaque = six->opaque, + .cell_width = six->cell_width, + .cell_height = six->cell_height, + .original = { + .data = new_data, + .pix = new_pix, + .width = new_width, + .height = new_height, + }, + .scaled = { + .data = NULL, + .pix = NULL, + .width = -1, + .height = -1, + }, }; #if defined(_DEBUG) @@ -847,23 +882,94 @@ sixel_overwrite_at_cursor(struct terminal *term, int width) void sixel_cell_size_changed(struct terminal *term) { - struct grid *g = term->grid; + tll_foreach(term->normal.sixel_images, it) + sixel_invalidate_cache(&it->item); - term->grid = &term->normal; - tll_foreach(term->normal.sixel_images, it) { - struct sixel *six = &it->item; - six->rows = (six->height + term->cell_height - 1) / term->cell_height; - six->cols = (six->width + term->cell_width - 1) / term->cell_width; + tll_foreach(term->alt.sixel_images, it) + sixel_invalidate_cache(&it->item); +} + +void +sixel_sync_cache(const struct terminal *term, struct sixel *six) +{ + if (six->pix != NULL) { +#if defined(_DEBUG) + if (six->cell_width == term->cell_width && + six->cell_height == term->cell_height) + { + xassert(six->pix == six->original.pix); + xassert(six->width == six->original.width); + xassert(six->height == six->original.height); + + xassert(six->scaled.data == NULL); + xassert(six->scaled.pix == NULL); + xassert(six->scaled.width < 0); + xassert(six->scaled.height < 0); + } else { + xassert(six->pix == six->scaled.pix); + xassert(six->width == six->scaled.width); + xassert(six->height == six->scaled.height); + + xassert(six->scaled.data != NULL); + xassert(six->scaled.pix != NULL); + + /* TODO: check ratio */ + xassert(six->scaled.width >= 0); + xassert(six->scaled.height >= 0); + } +#endif + return; } - term->grid = &term->alt; - tll_foreach(term->alt.sixel_images, it) { - struct sixel *six = &it->item; - six->rows = (six->height + term->cell_height - 1) / term->cell_height; - six->cols = (six->width + term->cell_width - 1) / term->cell_width; - } + /* Cache should be invalid */ + xassert(six->scaled.data == NULL); + xassert(six->scaled.pix == NULL); + xassert(six->scaled.width < 0); + xassert(six->scaled.height < 0); - term->grid = g; + if (six->cell_width == term->cell_width && + six->cell_height == term->cell_height) + { + six->pix = six->original.pix; + six->width = six->original.width; + six->height = six->original.height; + } else { + const double width_ratio = (double)term->cell_width / six->cell_width; + const double height_ratio = (double)term->cell_height / six->cell_height; + + struct pixman_f_transform scale; + pixman_f_transform_init_scale( + &scale, 1. / width_ratio, 1. / height_ratio); + + struct pixman_transform _scale; + pixman_transform_from_pixman_f_transform(&_scale, &scale); + pixman_image_set_transform(six->original.pix, &_scale); + pixman_image_set_filter(six->original.pix, PIXMAN_FILTER_BILINEAR, NULL, 0); + + int scaled_width = (double)six->original.width * width_ratio; + int scaled_height = (double)six->original.height * height_ratio; + int scaled_stride = scaled_width * sizeof(uint32_t); + + LOG_DBG("scaling sixel: %dx%d -> %dx%d", + six->original.width, six->original.height, + scaled_width, scaled_height); + + uint8_t *scaled_data = xmalloc(scaled_height * scaled_stride); + pixman_image_t *scaled_pix = pixman_image_create_bits_no_clear( + PIXMAN_a8r8g8b8, scaled_width, scaled_height, + (uint32_t *)scaled_data, scaled_stride); + + pixman_image_composite32( + PIXMAN_OP_SRC, six->original.pix, NULL, scaled_pix, 0, 0, 0, 0, + 0, 0, scaled_width, scaled_height); + + pixman_image_set_transform(six->original.pix, NULL); + + six->scaled.data = scaled_data; + six->scaled.pix = six->pix = scaled_pix; + six->scaled.width = six->width = scaled_width; + six->scaled.height = six->height = scaled_height; + } } void @@ -926,14 +1032,15 @@ sixel_reflow_grid(struct terminal *term, struct grid *grid) * allowed of course */ _sixel_overwrite_by_rectangle( term, six->pos.row, six->pos.col, six->rows, six->cols, - &it->item.pix, &it->item.opaque); + &it->item.original.pix, &it->item.opaque); - if (it->item.data != pixman_image_get_data(it->item.pix)) { - it->item.data = pixman_image_get_data(it->item.pix); - it->item.width = pixman_image_get_width(it->item.pix); - it->item.height = pixman_image_get_height(it->item.pix); - it->item.cols = (it->item.width + term->cell_width - 1) / term->cell_width; - it->item.rows = (it->item.height + term->cell_height - 1) / term->cell_height; + if (it->item.original.data != pixman_image_get_data(it->item.original.pix)) { + it->item.original.data = pixman_image_get_data(it->item.original.pix); + it->item.original.width = pixman_image_get_width(it->item.original.pix); + it->item.original.height = pixman_image_get_height(it->item.original.pix); + it->item.cols = (it->item.original.width + it->item.cell_width - 1) / it->item.cell_width; + it->item.rows = (it->item.original.height + it->item.cell_height - 1) / it->item.cell_height; + sixel_invalidate_cache(&it->item); } sixel_insert(term, it->item); @@ -1027,13 +1134,27 @@ sixel_unhook(struct terminal *term) } struct sixel image = { - .data = img_data, - .width = width, - .height = height, + .pix = NULL, + .width = -1, + .height = -1, .rows = (height + term->cell_height - 1) / term->cell_height, .cols = (width + term->cell_width - 1) / term->cell_width, .pos = (struct coord){start_col, cur_row}, .opaque = !term->sixel.transparent_bg, + .cell_width = term->cell_width, + .cell_height = term->cell_height, + .original = { + .data = img_data, + .pix = NULL, + .width = width, + .height = height, + }, + .scaled = { + .data = NULL, + .pix = NULL, + .width = -1, + .height = -1, + }, }; xassert(image.rows <= term->grid->num_rows); @@ -1044,8 +1165,9 @@ sixel_unhook(struct terminal *term) image.width, image.height, image.pos.row, image.pos.row + image.rows); - image.pix = pixman_image_create_bits_no_clear( - PIXMAN_a8r8g8b8, image.width, image.height, img_data, stride); + image.original.pix = pixman_image_create_bits_no_clear( + PIXMAN_a8r8g8b8, image.original.width, image.original.height, + img_data, stride); pixel_row_idx += height; pixel_rows_left -= height; @@ -1064,7 +1186,8 @@ sixel_unhook(struct terminal *term) ? max(0, image.rows - 1) : image.rows; - xassert(rows_avail == 0 || image.height % term->cell_height == 0); + xassert(rows_avail == 0 || + image.original.height % term->cell_height == 0); for (size_t i = 0; i < linefeed_count; i++) term_linefeed(term); @@ -1084,7 +1207,7 @@ sixel_unhook(struct terminal *term) * higher up. */ const int sixel_row_height = 6 * term->sixel.pan; - const int sixel_rows = (image.height + sixel_row_height - 1) / sixel_row_height; + 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; @@ -1117,14 +1240,15 @@ sixel_unhook(struct terminal *term) _sixel_overwrite_by_rectangle( term, image.pos.row, image.pos.col, image.rows, image.cols, - &image.pix, &image.opaque); + &image.original.pix, &image.opaque); - if (image.data != pixman_image_get_data(image.pix)) { - image.data = pixman_image_get_data(image.pix); - image.width = pixman_image_get_width(image.pix); - image.height = pixman_image_get_height(image.pix); - image.cols = (image.width + term->cell_width - 1) / term->cell_width; - image.rows = (image.height + term->cell_height - 1) / term->cell_height; + if (image.original.data != pixman_image_get_data(image.original.pix)) { + image.original.data = pixman_image_get_data(image.original.pix); + image.original.width = pixman_image_get_width(image.original.pix); + image.original.height = pixman_image_get_height(image.original.pix); + image.cols = (image.original.width + image.cell_width - 1) / image.cell_width; + image.rows = (image.original.height + image.cell_height - 1) / image.cell_height; + sixel_invalidate_cache(&image); } sixel_insert(term, image); diff --git a/sixel.h b/sixel.h index efd64908..ab8a5050 100644 --- a/sixel.h +++ b/sixel.h @@ -20,6 +20,7 @@ void sixel_scroll_up(struct terminal *term, int rows); void sixel_scroll_down(struct terminal *term, int rows); void sixel_cell_size_changed(struct terminal *term); +void sixel_sync_cache(const struct terminal *term, struct sixel *sixel); void sixel_reflow_grid(struct terminal *term, struct grid *grid); diff --git a/terminal.c b/terminal.c index 090cbb20..df77d2b9 100644 --- a/terminal.c +++ b/terminal.c @@ -763,9 +763,6 @@ term_set_fonts(struct terminal *term, struct fcft_font *fonts[static 4]) free_custom_glyphs( &term->custom_glyphs.legacy, GLYPH_LEGACY_COUNT); - const int old_cell_width = term->cell_width; - const int old_cell_height = term->cell_height; - const struct config *conf = term->conf; const struct fcft_glyph *M = fcft_rasterize_char_utf32( @@ -792,28 +789,7 @@ term_set_fonts(struct terminal *term, struct fcft_font *fonts[static 4]) LOG_INFO("cell width=%d, height=%d", term->cell_width, term->cell_height); - if (term->cell_width < old_cell_width || - term->cell_height < old_cell_height) - { - /* - * The cell size has decreased. - * - * This means sixels, which we cannot resize, no longer fit - * into their "allocated" grid space. - * - * To be able to fit them, we would have to change the grid - * content. Inserting empty lines _might_ seem acceptable, but - * we'd also need to insert empty columns, which would break - * existing layout completely. - * - * So we delete them. - */ - sixel_destroy_all(term); - } else if (term->cell_width != old_cell_width || - term->cell_height != old_cell_height) - { - sixel_cell_size_changed(term); - } + sixel_cell_size_changed(term); /* Use force, since cell-width/height may have changed */ render_resize_force(term, term->width / term->scale, term->height / term->scale); diff --git a/terminal.h b/terminal.h index 1cb62686..1ccb3219 100644 --- a/terminal.h +++ b/terminal.h @@ -126,14 +126,48 @@ struct row { }; struct sixel { - void *data; + /* + * These three members reflect the "current", maybe scaled version + * of the image. + * + * The values will either be NULL/-1/-1, or match either the + * values in "original", or "scaled". + * + * They are typically reset when we need to invalidate the cached + * version (e.g. when the cell dimensions change). + */ pixman_image_t *pix; int width; int height; + int rows; int cols; struct coord pos; bool opaque; + + /* + * We store the cell dimensions of the time the sixel was emitted. + * + * If the font size is changed, we rescale the image accordingly, + * to ensure it stays within its cell boundaries. ‘scaled’ is a + * cached, rescaled version of ‘data’ + ‘pix’. + */ + int cell_width; + int cell_height; + + struct { + void *data; + pixman_image_t *pix; + int width; + int height; + } original; + + struct { + void *data; + pixman_image_t *pix; + int width; + int height; + } scaled; }; enum kitty_kbd_flags { From 72bc0acfbd4b002ebf26d65368bb65601452353e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 3 Jul 2023 14:36:03 +0200 Subject: [PATCH 0360/1323] wayland: handle enum value XDG_TOPLEVEL_STATE_SUSPENDED Added in wayland-protocols-1.32 --- wayland.c | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/wayland.c b/wayland.c index 406aba6d..5160240b 100644 --- a/wayland.c +++ b/wayland.c @@ -651,6 +651,7 @@ xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, bool is_tiled_bottom = false; bool is_tiled_left = false; bool is_tiled_right = false; + bool is_suspended UNUSED = false; #if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG char state_str[2048]; @@ -665,29 +666,35 @@ xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, [XDG_TOPLEVEL_STATE_TILED_RIGHT] = "tiled:right", [XDG_TOPLEVEL_STATE_TILED_TOP] = "tiled:top", [XDG_TOPLEVEL_STATE_TILED_BOTTOM] = "tiled:bottom", +#if defined(XDG_TOPLEVEL_STATE_SUSPENDED_SINCE_VERSION) /* wayland-protocols >= 1.32 */ + [XDG_TOPLEVEL_STATE_SUSPENDED] = "suspended", +#endif }; #endif enum xdg_toplevel_state *state; wl_array_for_each(state, states) { switch (*state) { - case XDG_TOPLEVEL_STATE_ACTIVATED: is_activated = true; break; - case XDG_TOPLEVEL_STATE_FULLSCREEN: is_fullscreen = true; break; case XDG_TOPLEVEL_STATE_MAXIMIZED: is_maximized = true; break; + case XDG_TOPLEVEL_STATE_FULLSCREEN: is_fullscreen = true; break; + case XDG_TOPLEVEL_STATE_RESIZING: is_resizing = true; break; + case XDG_TOPLEVEL_STATE_ACTIVATED: is_activated = true; break; case XDG_TOPLEVEL_STATE_TILED_LEFT: is_tiled_left = true; break; case XDG_TOPLEVEL_STATE_TILED_RIGHT: is_tiled_right = true; break; case XDG_TOPLEVEL_STATE_TILED_TOP: is_tiled_top = true; break; case XDG_TOPLEVEL_STATE_TILED_BOTTOM: is_tiled_bottom = true; break; - case XDG_TOPLEVEL_STATE_RESIZING: is_resizing = true; break; - } + +#if defined(XDG_TOPLEVEL_STATE_SUSPENDED_SINCE_VERSION) + case XDG_TOPLEVEL_STATE_SUSPENDED: is_suspended = true; break; +#endif + } #if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG - if (*state >= XDG_TOPLEVEL_STATE_MAXIMIZED && - *state <= XDG_TOPLEVEL_STATE_TILED_BOTTOM) - { + if (*state >= 0 && *state < ALEN(strings)) { state_chars += snprintf( &state_str[state_chars], sizeof(state_str) - state_chars, - "%s, ", strings[*state]); + "%s, ", + strings[*state] != NULL ? strings[*state] : ""); } #endif } From ee794a121e4603d01fb662ad6d1f3b18b6937d2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 27 Jun 2023 16:57:33 +0200 Subject: [PATCH 0361/1323] refactor: track current xcursor using an enum, instead of a char pointer --- cursor-shape.c | 29 +++++++++++++++++++++++++++++ cursor-shape.h | 21 +++++++++++++++++++++ input.c | 22 +++++++++++----------- input.h | 5 +++-- meson.build | 1 + render.c | 24 +++++++++++++++--------- render.h | 2 +- terminal.c | 34 ++++++++++------------------------ terminal.h | 14 -------------- wayland.h | 3 ++- 10 files changed, 93 insertions(+), 62 deletions(-) create mode 100644 cursor-shape.c create mode 100644 cursor-shape.h diff --git a/cursor-shape.c b/cursor-shape.c new file mode 100644 index 00000000..152f176f --- /dev/null +++ b/cursor-shape.c @@ -0,0 +1,29 @@ +#include + +#include "cursor-shape.h" +#include "debug.h" +#include "util.h" + +const char * +cursor_shape_to_string(enum cursor_shape shape) +{ + static const char *const table[] = { + [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", + + }; + + xassert(shape <= ALEN(table)); + return table[shape]; +} diff --git a/cursor-shape.h b/cursor-shape.h new file mode 100644 index 00000000..fb79e45a --- /dev/null +++ b/cursor-shape.h @@ -0,0 +1,21 @@ +#pragma once + +enum cursor_shape { + CURSOR_SHAPE_NONE, + CURSOR_SHAPE_CUSTOM, + + CURSOR_SHAPE_HIDDEN, + 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, + CURSOR_SHAPE_BOTTOM_RIGHT_CORNER, + CURSOR_SHAPE_LEFT_SIDE, + CURSOR_SHAPE_RIGHT_SIDE, + CURSOR_SHAPE_TOP_SIDE, + CURSOR_SHAPE_BOTTOM_SIDE, +}; + +const char *cursor_shape_to_string(enum cursor_shape shape); diff --git a/input.c b/input.c index 7e5d204d..0f638cc1 100644 --- a/input.c +++ b/input.c @@ -1704,20 +1704,20 @@ is_bottom_right(const struct terminal *term, int x, int y) (term->active_surface == TERM_SURF_BORDER_BOTTOM && x > term->width + 1 * csd_border_size * term->scale - 10 * term->scale))); } -const char * +enum cursor_shape xcursor_for_csd_border(struct terminal *term, int x, int y) { - if (is_top_left(term, x, y)) return XCURSOR_TOP_LEFT_CORNER; - else if (is_top_right(term, x, y)) return XCURSOR_TOP_RIGHT_CORNER; - else if (is_bottom_left(term, x, y)) return XCURSOR_BOTTOM_LEFT_CORNER; - else if (is_bottom_right(term, x, y)) return XCURSOR_BOTTOM_RIGHT_CORNER; - else if (term->active_surface == TERM_SURF_BORDER_LEFT) return XCURSOR_LEFT_SIDE; - else if (term->active_surface == TERM_SURF_BORDER_RIGHT) return XCURSOR_RIGHT_SIDE; - else if (term->active_surface == TERM_SURF_BORDER_TOP) return XCURSOR_TOP_SIDE; - else if (term->active_surface == TERM_SURF_BORDER_BOTTOM) return XCURSOR_BOTTOM_SIDE; + if (is_top_left(term, x, y)) return CURSOR_SHAPE_TOP_LEFT_CORNER; + else if (is_top_right(term, x, y)) return CURSOR_SHAPE_TOP_RIGHT_CORNER; + else if (is_bottom_left(term, x, y)) return CURSOR_SHAPE_BOTTOM_LEFT_CORNER; + else if (is_bottom_right(term, x, y)) return CURSOR_SHAPE_BOTTOM_RIGHT_CORNER; + else if (term->active_surface == TERM_SURF_BORDER_LEFT) return CURSOR_SHAPE_LEFT_SIDE; + else if (term->active_surface == TERM_SURF_BORDER_RIGHT) return CURSOR_SHAPE_RIGHT_SIDE; + else if (term->active_surface == TERM_SURF_BORDER_TOP) return CURSOR_SHAPE_TOP_SIDE; + else if (term->active_surface == TERM_SURF_BORDER_BOTTOM) return CURSOR_SHAPE_BOTTOM_SIDE; else { BUG("Unreachable"); - return NULL; + return CURSOR_SHAPE_NONE; } } @@ -1819,7 +1819,7 @@ wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, } /* Reset last-set-xcursor, to ensure we update it on a pointer-enter event */ - seat->pointer.xcursor = NULL; + seat->pointer.shape = CURSOR_SHAPE_NONE; /* Reset mouse state */ seat->mouse.x = seat->mouse.y = 0; diff --git a/input.h b/input.h index ea488a86..825dc3be 100644 --- a/input.h +++ b/input.h @@ -3,8 +3,9 @@ #include #include -#include "wayland.h" +#include "cursor-shape.h" #include "misc.h" +#include "wayland.h" /* * Custom defines for mouse wheel left/right buttons. @@ -33,4 +34,4 @@ void get_current_modifiers(const struct seat *seat, xkb_mod_mask_t *consumed, uint32_t key); -const char *xcursor_for_csd_border(struct terminal *term, int x, int y); +enum cursor_shape xcursor_for_csd_border(struct terminal *term, int x, int y); diff --git a/meson.build b/meson.build index 6e219caf..9560504f 100644 --- a/meson.build +++ b/meson.build @@ -261,6 +261,7 @@ executable( 'box-drawing.c', 'box-drawing.h', 'config.c', 'config.h', 'commands.c', 'commands.h', + 'cursor-shape.c', 'cursor-shape.h', 'extract.c', 'extract.h', 'fdm.c', 'fdm.h', 'foot-features.h', diff --git a/render.c b/render.c index 4498290e..fedc3467 100644 --- a/render.c +++ b/render.c @@ -4254,9 +4254,9 @@ render_xcursor_update(struct seat *seat) if (!seat->mouse_focus) return; - xassert(seat->pointer.xcursor != NULL); + xassert(seat->pointer.shape != CURSOR_SHAPE_NONE); - if (seat->pointer.xcursor == XCURSOR_HIDDEN) { + if (seat->pointer.shape == CURSOR_SHAPE_HIDDEN) { /* Hide cursor */ wl_surface_attach(seat->pointer.surface.surf, NULL, 0, 0); wl_surface_commit(seat->pointer.surface.surf); @@ -4434,13 +4434,13 @@ render_refresh_urls(struct terminal *term) } bool -render_xcursor_set(struct seat *seat, struct terminal *term, const char *xcursor) +render_xcursor_set(struct seat *seat, struct terminal *term, enum cursor_shape shape) { if (seat->pointer.theme == NULL) return false; if (seat->mouse_focus == NULL) { - seat->pointer.xcursor = NULL; + seat->pointer.shape = CURSOR_SHAPE_NONE; return true; } @@ -4449,18 +4449,24 @@ render_xcursor_set(struct seat *seat, struct terminal *term, const char *xcursor return true; } - if (seat->pointer.xcursor == xcursor) + if (seat->pointer.shape == shape) return true; - if (xcursor != XCURSOR_HIDDEN) { + if (shape != CURSOR_SHAPE_HIDDEN) { + const char *const xcursor = 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); if (seat->pointer.cursor == NULL) { seat->pointer.cursor = wl_cursor_theme_get_cursor( - seat->pointer.theme, XCURSOR_TEXT_FALLBACK ); + seat->pointer.theme, fallback); + if (seat->pointer.cursor == NULL) { - LOG_ERR("failed to load xcursor pointer '%s', and fallback '%s'", xcursor, XCURSOR_TEXT_FALLBACK); + LOG_ERR("failed to load xcursor pointer " + "'%s', and fallback '%s'", xcursor, fallback); return false; } } @@ -4468,7 +4474,7 @@ render_xcursor_set(struct seat *seat, struct terminal *term, const char *xcursor seat->pointer.cursor = NULL; /* FDM hook takes care of actual rendering */ - seat->pointer.xcursor = xcursor; + seat->pointer.shape = shape; seat->pointer.xcursor_pending = true; return true; } diff --git a/render.h b/render.h index d2c673ee..f038ffb0 100644 --- a/render.h +++ b/render.h @@ -19,7 +19,7 @@ void render_refresh_search(struct terminal *term); void render_refresh_title(struct terminal *term); void render_refresh_urls(struct terminal *term); bool render_xcursor_set( - struct seat *seat, struct terminal *term, const char *xcursor); + struct seat *seat, struct terminal *term, enum cursor_shape shape); bool render_xcursor_is_valid(const struct seat *seat, const char *cursor); struct render_worker_context { diff --git a/terminal.c b/terminal.c index df77d2b9..3591040b 100644 --- a/terminal.c +++ b/terminal.c @@ -47,20 +47,6 @@ #define PTMX_TIMING 0 -const char *const XCURSOR_HIDDEN = "hidden"; -const char *const XCURSOR_LEFT_PTR = "left_ptr"; -const char *const XCURSOR_TEXT = "text"; -const char *const XCURSOR_TEXT_FALLBACK = "xterm"; -//const char *const XCURSOR_HAND2 = "hand2"; -const char *const XCURSOR_TOP_LEFT_CORNER = "top_left_corner"; -const char *const XCURSOR_TOP_RIGHT_CORNER = "top_right_corner"; -const char *const XCURSOR_BOTTOM_LEFT_CORNER = "bottom_left_corner"; -const char *const XCURSOR_BOTTOM_RIGHT_CORNER = "bottom_right_corner"; -const char *const XCURSOR_LEFT_SIDE = "left_side"; -const char *const XCURSOR_RIGHT_SIDE = "right_side"; -const char *const XCURSOR_TOP_SIDE = "top_side"; -const char *const XCURSOR_BOTTOM_SIDE = "bottom_side"; - static void enqueue_data_for_slave(const void *data, size_t len, size_t offset, ptmx_buffer_list_t *buffer_list) @@ -3137,44 +3123,44 @@ term_mouse_motion(struct terminal *term, int button, int row, int col, void term_xcursor_update_for_seat(struct terminal *term, struct seat *seat) { - const char *xcursor = NULL; + enum cursor_shape shape = CURSOR_SHAPE_NONE; switch (term->active_surface) { case TERM_SURF_GRID: { bool have_custom_cursor = render_xcursor_is_valid(seat, term->mouse_user_cursor); - xcursor = seat->pointer.hidden ? XCURSOR_HIDDEN - : have_custom_cursor ? term->mouse_user_cursor - : term->is_searching ? XCURSOR_LEFT_PTR + shape = seat->pointer.hidden ? CURSOR_SHAPE_HIDDEN + : have_custom_cursor ? CURSOR_SHAPE_CUSTOM //term->mouse_user_cursor + : term->is_searching ? CURSOR_SHAPE_LEFT_PTR : (seat->mouse.col >= 0 && seat->mouse.row >= 0 && - term_mouse_grabbed(term, seat)) ? XCURSOR_TEXT - : XCURSOR_LEFT_PTR; + term_mouse_grabbed(term, seat)) ? CURSOR_SHAPE_TEXT + : CURSOR_SHAPE_LEFT_PTR; break; } case TERM_SURF_TITLE: case TERM_SURF_BUTTON_MINIMIZE: case TERM_SURF_BUTTON_MAXIMIZE: case TERM_SURF_BUTTON_CLOSE: - xcursor = XCURSOR_LEFT_PTR; + shape = CURSOR_SHAPE_LEFT_PTR; break; case TERM_SURF_BORDER_LEFT: case TERM_SURF_BORDER_RIGHT: case TERM_SURF_BORDER_TOP: case TERM_SURF_BORDER_BOTTOM: - xcursor = xcursor_for_csd_border(term, seat->mouse.x, seat->mouse.y); + shape = xcursor_for_csd_border(term, seat->mouse.x, seat->mouse.y); break; case TERM_SURF_NONE: return; } - if (xcursor == NULL) + if (shape == CURSOR_SHAPE_NONE) BUG("xcursor not set"); - render_xcursor_set(seat, term, xcursor); + render_xcursor_set(seat, term, shape); } void diff --git a/terminal.h b/terminal.h index 1ccb3219..6dace7ac 100644 --- a/terminal.h +++ b/terminal.h @@ -718,20 +718,6 @@ struct terminal { char *cwd; }; -extern const char *const XCURSOR_HIDDEN; -extern const char *const XCURSOR_LEFT_PTR; -extern const char *const XCURSOR_TEXT; -extern const char *const XCURSOR_TEXT_FALLBACK; -//extern const char *const XCURSOR_HAND2; -extern const char *const XCURSOR_TOP_LEFT_CORNER; -extern const char *const XCURSOR_TOP_RIGHT_CORNER; -extern const char *const XCURSOR_BOTTOM_LEFT_CORNER; -extern const char *const XCURSOR_BOTTOM_RIGHT_CORNER; -extern const char *const XCURSOR_LEFT_SIDE; -extern const char *const XCURSOR_RIGHT_SIDE; -extern const char *const XCURSOR_TOP_SIDE; -extern const char *const XCURSOR_BOTTOM_SIDE; - struct config; struct terminal *term_init( const struct config *conf, struct fdm *fdm, struct reaper *reaper, diff --git a/wayland.h b/wayland.h index e2d22031..af1bcb3f 100644 --- a/wayland.h +++ b/wayland.h @@ -28,6 +28,7 @@ #include #include +#include "cursor-shape.h" #include "fdm.h" /* Forward declarations */ @@ -151,7 +152,7 @@ struct seat { float scale; bool hidden; - const char *xcursor; + enum cursor_shape shape; struct wl_callback *xcursor_callback; bool xcursor_pending; } pointer; From c8e13ad3938d68635693d0b0f4bf1054628e8387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 27 Jun 2023 17:25:57 +0200 Subject: [PATCH 0362/1323] cursor-shape: add support for server side cursor shapes This implements support for the new cursor-shape-v1 protocol. When available, we use it, instead of client-side cursor surfaces, to select the xcursor shape. Note that we still need to keep client side pointers, for: * backward compatibility * to be able to "hide" the cursor Closes #1379 --- CHANGELOG.md | 4 ++ cursor-shape-v1.xml | 147 ++++++++++++++++++++++++++++++++++++++++++++ cursor-shape.c | 25 +++++++- cursor-shape.h | 13 +++- meson.build | 6 ++ render.c | 47 +++++++++----- wayland.c | 46 ++++++++++++++ wayland.h | 12 +++- 8 files changed, 280 insertions(+), 20 deletions(-) create mode 100644 cursor-shape-v1.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 09b47f09..6625c56d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,10 @@ * Support for the new fractional-scaling-v1 Wayland protocol. This brings true fractional scaling to Wayland in general, and with this release, foot. +* Support for the new `cursor-shape-v1` Wayland protocol, i.e. server + side cursor shapes ([#1379][1379]). + +[1379]: https://codeberg.org/dnkl/foot/issues/1379 ### Changed diff --git a/cursor-shape-v1.xml b/cursor-shape-v1.xml new file mode 100644 index 00000000..56f6a1a6 --- /dev/null +++ b/cursor-shape-v1.xml @@ -0,0 +1,147 @@ + + + + Copyright 2018 The Chromium Authors + Copyright 2023 Simon Ser + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + This global offers an alternative, optional way to set cursor images. This + new way uses enumerated cursors instead of a wl_surface like + wl_pointer.set_cursor does. + + Warning! The protocol described in this file is currently in the testing + phase. Backward compatible changes may be added together with the + corresponding interface version bump. Backward incompatible changes can + only be done by creating a new major version of the extension. + + + + + Destroy the cursor shape manager. + + + + + + Obtain a wp_cursor_shape_device_v1 for a wl_pointer object. + + + + + + + + Obtain a wp_cursor_shape_device_v1 for a zwp_tablet_tool_v2 object. + + + + + + + + + This interface advertises the list of supported cursor shapes for a + device, and allows clients to set the cursor shape. + + + + + This enum describes cursor shapes. + + The names are taken from the CSS W3C specification: + https://w3c.github.io/csswg-drafts/css-ui/#cursor + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Destroy the cursor shape device. + + The device cursor shape remains unchanged. + + + + + + Sets the device cursor to the specified shape. The compositor will + change the cursor image based on the specified shape. + + The cursor actually changes only if the input device focus is one of + the requesting client's surfaces. If any, the previous cursor image + (surface or shape) is replaced. + + The "shape" argument must be a valid enum entry, otherwise the + invalid_shape protocol error is raised. + + This is similar to the wl_pointer.set_cursor and + zwp_tablet_tool_v2.set_cursor requests, but this request accepts a + shape instead of contents in the form of a surface. Clients can mix + set_cursor and set_shape requests. + + The serial parameter must match the latest wl_pointer.enter or + zwp_tablet_tool_v2.proximity_in serial number sent to the client. + Otherwise the request will be ignored. + + + + + + diff --git a/cursor-shape.c b/cursor-shape.c index 152f176f..cd9ba221 100644 --- a/cursor-shape.c +++ b/cursor-shape.c @@ -7,7 +7,7 @@ const char * cursor_shape_to_string(enum cursor_shape shape) { - static const char *const table[] = { + static const char *const table[CURSOR_SHAPE_COUNT] = { [CURSOR_SHAPE_NONE] = NULL, [CURSOR_SHAPE_HIDDEN] = "hidden", [CURSOR_SHAPE_LEFT_PTR] = "left_ptr", @@ -27,3 +27,26 @@ cursor_shape_to_string(enum cursor_shape shape) xassert(shape <= ALEN(table)); return table[shape]; } + +#if defined(HAVE_CURSOR_SHAPE) +enum wp_cursor_shape_device_v1_shape +cursor_shape_to_server_shape(enum cursor_shape shape) +{ + static const enum wp_cursor_shape_device_v1_shape table[CURSOR_SHAPE_COUNT] = { + [CURSOR_SHAPE_LEFT_PTR] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT, + [CURSOR_SHAPE_TEXT] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT, + [CURSOR_SHAPE_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, + [CURSOR_SHAPE_BOTTOM_RIGHT_CORNER] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SE_RESIZE, + [CURSOR_SHAPE_LEFT_SIDE] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_W_RESIZE, + [CURSOR_SHAPE_RIGHT_SIDE] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_E_RESIZE, + [CURSOR_SHAPE_TOP_SIDE] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_N_RESIZE, + [CURSOR_SHAPE_BOTTOM_SIDE] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_S_RESIZE, + }; + + xassert(shape <= ALEN(table)); + return table[shape]; +} +#endif diff --git a/cursor-shape.h b/cursor-shape.h index fb79e45a..0cb0b4d8 100644 --- a/cursor-shape.h +++ b/cursor-shape.h @@ -1,10 +1,14 @@ #pragma once +#if defined(HAVE_CURSOR_SHAPE) +#include +#endif + enum cursor_shape { CURSOR_SHAPE_NONE, CURSOR_SHAPE_CUSTOM, - CURSOR_SHAPE_HIDDEN, + CURSOR_SHAPE_LEFT_PTR, CURSOR_SHAPE_TEXT, CURSOR_SHAPE_TEXT_FALLBACK, @@ -16,6 +20,13 @@ enum cursor_shape { CURSOR_SHAPE_RIGHT_SIDE, CURSOR_SHAPE_TOP_SIDE, CURSOR_SHAPE_BOTTOM_SIDE, + + CURSOR_SHAPE_COUNT, }; const char *cursor_shape_to_string(enum cursor_shape shape); + +#if defined(HAVE_CURSOR_SHAPE) +enum wp_cursor_shape_device_v1_shape cursor_shape_to_server_shape( + enum cursor_shape shape); +#endif diff --git a/meson.build b/meson.build index 9560504f..ed0bf4a0 100644 --- a/meson.build +++ b/meson.build @@ -170,6 +170,12 @@ else fractional_scale = false endif +# TODO: check wayland-protocols version +wl_proto_xml += [wayland_protocols_datadir + '/unstable/tablet/tablet-unstable-v2.xml', # required by cursor-shape-v1 + 'cursor-shape-v1.xml', # TODO: use wayland-protocols + ] +add_project_arguments('-DHAVE_CURSOR_SHAPE', language: 'c') + foreach prot : wl_proto_xml wl_proto_headers += custom_target( prot.underscorify() + '-client-header', diff --git a/render.c b/render.c index fedc3467..ab1ddc51 100644 --- a/render.c +++ b/render.c @@ -31,6 +31,7 @@ #include "box-drawing.h" #include "char32.h" #include "config.h" +#include "cursor-shape.h" #include "grid.h" #include "hsl.h" #include "ime.h" @@ -4259,35 +4260,46 @@ render_xcursor_update(struct seat *seat) if (seat->pointer.shape == CURSOR_SHAPE_HIDDEN) { /* Hide cursor */ wl_surface_attach(seat->pointer.surface.surf, NULL, 0, 0); + wl_pointer_set_cursor( + seat->wl_pointer, seat->pointer.serial, seat->pointer.surface.surf, + 0, 0); wl_surface_commit(seat->pointer.surface.surf); return; } xassert(seat->pointer.cursor != NULL); - const 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); +#if defined(HAVE_CURSOR_SHAPE) + if (seat->pointer.shape_device != NULL) { + wp_cursor_shape_device_v1_set_shape( + seat->pointer.shape_device, + seat->pointer.serial, + cursor_shape_to_server_shape(seat->pointer.shape)); + } else +#endif + { + const int scale = seat->pointer.scale; + struct wl_cursor_image *image = seat->pointer.cursor->images[0]; - wayl_surface_scale_explicit_width_height( - seat->mouse_focus->window, - &seat->pointer.surface, image->width, image->height, scale); + wl_surface_attach( + seat->pointer.surface.surf, wl_cursor_image_get_buffer(image), 0, 0); - wl_surface_attach(seat->pointer.surface.surf, buf, 0, 0); + wl_pointer_set_cursor( + seat->wl_pointer, seat->pointer.serial, + seat->pointer.surface.surf, + image->hotspot_x / scale, image->hotspot_y / scale); - wl_pointer_set_cursor( - seat->wl_pointer, seat->pointer.serial, - seat->pointer.surface.surf, - image->hotspot_x / scale, image->hotspot_y / scale); + wl_surface_damage_buffer( + seat->pointer.surface.surf, 0, 0, INT32_MAX, INT32_MAX); - wl_surface_damage_buffer( - seat->pointer.surface.surf, 0, 0, INT32_MAX, INT32_MAX); + wl_surface_set_buffer_scale(seat->pointer.surface.surf, scale); - xassert(seat->pointer.xcursor_callback == NULL); - seat->pointer.xcursor_callback = wl_surface_frame(seat->pointer.surface.surf); - wl_callback_add_listener(seat->pointer.xcursor_callback, &xcursor_listener, seat); + xassert(seat->pointer.xcursor_callback == NULL); + seat->pointer.xcursor_callback = wl_surface_frame(seat->pointer.surface.surf); + wl_callback_add_listener(seat->pointer.xcursor_callback, &xcursor_listener, seat); - wl_surface_commit(seat->pointer.surface.surf); + wl_surface_commit(seat->pointer.surface.surf); + } } static void @@ -4452,6 +4464,7 @@ render_xcursor_set(struct seat *seat, struct terminal *term, enum cursor_shape s if (seat->pointer.shape == shape) return true; + /* TODO: skip this when using server-side cursors */ if (shape != CURSOR_SHAPE_HIDDEN) { const char *const xcursor = cursor_shape_to_string(shape); const char *const fallback = diff --git a/wayland.c b/wayland.c index 5160240b..f3a5619d 100644 --- a/wayland.c +++ b/wayland.c @@ -14,6 +14,10 @@ #include #include +#if defined(HAVE_CURSOR_SHAPE) +#include +#endif + #include #define LOG_MODULE "wayland" @@ -209,6 +213,11 @@ seat_destroy(struct seat *seat) if (seat->data_device != NULL) wl_data_device_release(seat->data_device); +#if defined(HAVE_CURSOR_SHAPE) + if (seat->pointer.shape_device != NULL) + wp_cursor_shape_device_v1_destroy(seat->pointer.shape_device); +#endif + if (seat->wl_keyboard != NULL) wl_keyboard_release(seat->wl_keyboard); if (seat->wl_pointer != NULL) @@ -316,9 +325,22 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat, seat->wl_pointer = wl_seat_get_pointer(wl_seat); wl_pointer_add_listener(seat->wl_pointer, &pointer_listener, seat); + +#if defined(HAVE_CURSOR_SHAPE) + if (seat->wayl->cursor_shape_manager != NULL) { + xassert(seat->pointer.shape_device == NULL); + seat->pointer.shape_device = wp_cursor_shape_manager_v1_get_pointer( + seat->wayl->cursor_shape_manager, seat->wl_pointer); + } +#endif } } else { if (seat->wl_pointer != NULL) { +#if defined(HAVE_CURSOR_SHAPE) + wp_cursor_shape_device_v1_destroy(seat->pointer.shape_device); + seat->pointer.shape_device = NULL; +#endif + wl_pointer_release(seat->wl_pointer); wl_surface_destroy(seat->pointer.surface.surf); @@ -1167,6 +1189,17 @@ handle_global(void *data, struct wl_registry *registry, } #endif +#if defined(HAVE_CURSOR_SHAPE) + else if (strcmp(interface, wp_cursor_shape_manager_v1_interface.name) == 0) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->cursor_shape_manager = wl_registry_bind( + wayl->registry, name, &wp_cursor_shape_manager_v1_interface, required); + } +#endif + #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED else if (strcmp(interface, zwp_text_input_manager_v3_interface.name) == 0) { const uint32_t required = 1; @@ -1401,6 +1434,15 @@ wayl_init(struct fdm *fdm, struct key_binding_manager *key_binding_manager, LOG_WARN("fractional scaling not available"); } +#if defined(HAVE_CURSOR_SHAPE) + if (wayl->cursor_shape_manager == NULL) { +#else + if (true) { +#endif + LOG_WARN("no server-side cursors available, " + "falling back to client-side cursors"); + } + if (presentation_timings && wayl->presentation == NULL) { LOG_ERR("presentation time interface not implemented by compositor"); goto out; @@ -1495,6 +1537,10 @@ wayl_destroy(struct wayland *wayl) if (wayl->viewporter != NULL) wp_viewporter_destroy(wayl->viewporter); #endif +#if defined(HAVE_CURSOR_SHAPE) + if (wayl->cursor_shape_manager != NULL) + wp_cursor_shape_manager_v1_destroy(wayl->cursor_shape_manager); +#endif #if defined(HAVE_XDG_ACTIVATION) if (wayl->xdg_activation != NULL) xdg_activation_v1_destroy(wayl->xdg_activation); diff --git a/wayland.h b/wayland.h index af1bcb3f..b30ad307 100644 --- a/wayland.h +++ b/wayland.h @@ -146,12 +146,18 @@ struct seat { struct { uint32_t serial; + /* Client-side cursor */ struct wayl_surface surface; struct wl_cursor_theme *theme; struct wl_cursor *cursor; + + /* Server-side cursor */ +#if defined(HAVE_CURSOR_SHAPE) + struct wp_cursor_shape_device_v1 *shape_device; +#endif + float scale; bool hidden; - enum cursor_shape shape; struct wl_callback *xcursor_callback; bool xcursor_pending; @@ -426,6 +432,10 @@ struct wayland { struct xdg_activation_v1 *xdg_activation; #endif +#if defined(HAVE_CURSOR_SHAPE) + struct wp_cursor_shape_manager_v1 *cursor_shape_manager; +#endif + bool presentation_timings; struct wp_presentation *presentation; uint32_t presentation_clock_id; From 6ed5dce5ab5651c1ee345d0315c1538900282f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 27 Jun 2023 17:42:47 +0200 Subject: [PATCH 0363/1323] render: debug log which method we use to set the xcursor --- render.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/render.c b/render.c index ab1ddc51..d7cc9dfb 100644 --- a/render.c +++ b/render.c @@ -4259,6 +4259,7 @@ render_xcursor_update(struct seat *seat) if (seat->pointer.shape == CURSOR_SHAPE_HIDDEN) { /* Hide cursor */ + LOG_DBG("hiding cursor using client-side NULL-surface"); wl_surface_attach(seat->pointer.surface.surf, NULL, 0, 0); wl_pointer_set_cursor( seat->wl_pointer, seat->pointer.serial, seat->pointer.surface.surf, @@ -4271,6 +4272,7 @@ render_xcursor_update(struct seat *seat) #if defined(HAVE_CURSOR_SHAPE) if (seat->pointer.shape_device != NULL) { + LOG_DBG("setting cursor shape using cursor-shape-v1"); wp_cursor_shape_device_v1_set_shape( seat->pointer.shape_device, seat->pointer.serial, @@ -4278,6 +4280,7 @@ render_xcursor_update(struct seat *seat) } else #endif { + LOG_DBG("setting cursor shape using a client-side cursor surface"); const int scale = seat->pointer.scale; struct wl_cursor_image *image = seat->pointer.cursor->images[0]; From 803b250652d766cf08721e334215585f4fd72fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 27 Jun 2023 18:16:33 +0200 Subject: [PATCH 0364/1323] pgo: update xcursor stubs to use enum instead of char pointer --- pgo/pgo.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pgo/pgo.c b/pgo/pgo.c index d9ee5855..6be60363 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -76,15 +76,15 @@ render_xcursor_is_valid(const struct seat *seat, const char *cursor) } bool -render_xcursor_set(struct seat *seat, struct terminal *term, const char *xcursor) +render_xcursor_set(struct seat *seat, struct terminal *term, enum cursor_shape shape) { return true; } -const char * +enum cursor_shape xcursor_for_csd_border(struct terminal *term, int x, int y) { - return XCURSOR_LEFT_PTR; + return CURSOR_SHAPE_LEFT_PTR; } struct wl_window * From 9155948ac8f2902acae107e9ff765630295ca6b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 27 Jun 2023 18:40:25 +0200 Subject: [PATCH 0365/1323] cursor-shape: assert lookup succeeded --- cursor-shape.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cursor-shape.c b/cursor-shape.c index cd9ba221..48f7419f 100644 --- a/cursor-shape.c +++ b/cursor-shape.c @@ -25,6 +25,7 @@ cursor_shape_to_string(enum cursor_shape shape) }; xassert(shape <= ALEN(table)); + xassert(table[shape] != NULL); return table[shape]; } @@ -47,6 +48,7 @@ cursor_shape_to_server_shape(enum cursor_shape shape) }; xassert(shape <= ALEN(table)); + xassert(table[shape] != 0); return table[shape]; } #endif From ddd6004b275dd9a24f26e03dead90f0fffb0cc3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 27 Jun 2023 18:40:44 +0200 Subject: [PATCH 0366/1323] =?UTF-8?q?render:=20don=E2=80=99t=20(can?= =?UTF-8?q?=E2=80=99t)=20use=20cursor-shape-v1=20when=20user=20has=20set?= =?UTF-8?q?=20a=20custom=20cursor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Well, we _could_, but we’d have to reverse map the string to a cursor-shape-v1 enum value. Let’s not do that, for now at least. --- render.c | 11 ++++++++--- terminal.c | 27 ++++++++++++++++----------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/render.c b/render.c index d7cc9dfb..b0b1ec32 100644 --- a/render.c +++ b/render.c @@ -4271,7 +4271,9 @@ render_xcursor_update(struct seat *seat) xassert(seat->pointer.cursor != NULL); #if defined(HAVE_CURSOR_SHAPE) - if (seat->pointer.shape_device != NULL) { + if (seat->pointer.shape_device != NULL && + seat->pointer.shape != CURSOR_SHAPE_CUSTOM) + { LOG_DBG("setting cursor shape using cursor-shape-v1"); wp_cursor_shape_device_v1_set_shape( seat->pointer.shape_device, @@ -4449,7 +4451,8 @@ render_refresh_urls(struct terminal *term) } bool -render_xcursor_set(struct seat *seat, struct terminal *term, enum cursor_shape shape) +render_xcursor_set(struct seat *seat, struct terminal *term, + enum cursor_shape shape) { if (seat->pointer.theme == NULL) return false; @@ -4469,7 +4472,9 @@ render_xcursor_set(struct seat *seat, struct terminal *term, enum cursor_shape s /* TODO: skip this when using server-side cursors */ if (shape != CURSOR_SHAPE_HIDDEN) { - const char *const xcursor = cursor_shape_to_string(shape); + const char *const xcursor = shape == CURSOR_SHAPE_CUSTOM + ? term->mouse_user_cursor + : cursor_shape_to_string(shape); const char *const fallback = cursor_shape_to_string(CURSOR_SHAPE_TEXT_FALLBACK); diff --git a/terminal.c b/terminal.c index 3591040b..3f2c9095 100644 --- a/terminal.c +++ b/terminal.c @@ -3126,19 +3126,24 @@ term_xcursor_update_for_seat(struct terminal *term, struct seat *seat) enum cursor_shape shape = CURSOR_SHAPE_NONE; switch (term->active_surface) { - case TERM_SURF_GRID: { - bool have_custom_cursor = - render_xcursor_is_valid(seat, term->mouse_user_cursor); + case TERM_SURF_GRID: + if (seat->pointer.hidden) + shape = CURSOR_SHAPE_HIDDEN; - shape = seat->pointer.hidden ? CURSOR_SHAPE_HIDDEN - : have_custom_cursor ? CURSOR_SHAPE_CUSTOM //term->mouse_user_cursor - : term->is_searching ? CURSOR_SHAPE_LEFT_PTR - : (seat->mouse.col >= 0 && - seat->mouse.row >= 0 && - term_mouse_grabbed(term, seat)) ? CURSOR_SHAPE_TEXT - : CURSOR_SHAPE_LEFT_PTR; + else if (render_xcursor_is_valid(seat, term->mouse_user_cursor)) + shape = CURSOR_SHAPE_CUSTOM; + + else if (seat->mouse.col >= 0 && + seat->mouse.row >= 0 && + term_mouse_grabbed(term, seat)) + { + shape = CURSOR_SHAPE_TEXT; + } + + else + shape = CURSOR_SHAPE_LEFT_PTR; break; - } + case TERM_SURF_TITLE: case TERM_SURF_BUTTON_MINIMIZE: case TERM_SURF_BUTTON_MAXIMIZE: From bf83a0b2bdb2299c95b6296fe8fd93bf0b99b743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 28 Jun 2023 08:38:20 +0200 Subject: [PATCH 0367/1323] meson: cursor-shape: use .xml from wayland-protocols This patch assumes a git snapshot of wayland-protocols are installed. We need to bump the version number as soon as the next version of wayland-protocols have been released. --- cursor-shape-v1.xml | 147 -------------------------------------------- meson.build | 12 ++-- 2 files changed, 7 insertions(+), 152 deletions(-) delete mode 100644 cursor-shape-v1.xml diff --git a/cursor-shape-v1.xml b/cursor-shape-v1.xml deleted file mode 100644 index 56f6a1a6..00000000 --- a/cursor-shape-v1.xml +++ /dev/null @@ -1,147 +0,0 @@ - - - - Copyright 2018 The Chromium Authors - Copyright 2023 Simon Ser - - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice (including the next - paragraph) shall be included in all copies or substantial portions of the - Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. - - - - - This global offers an alternative, optional way to set cursor images. This - new way uses enumerated cursors instead of a wl_surface like - wl_pointer.set_cursor does. - - Warning! The protocol described in this file is currently in the testing - phase. Backward compatible changes may be added together with the - corresponding interface version bump. Backward incompatible changes can - only be done by creating a new major version of the extension. - - - - - Destroy the cursor shape manager. - - - - - - Obtain a wp_cursor_shape_device_v1 for a wl_pointer object. - - - - - - - - Obtain a wp_cursor_shape_device_v1 for a zwp_tablet_tool_v2 object. - - - - - - - - - This interface advertises the list of supported cursor shapes for a - device, and allows clients to set the cursor shape. - - - - - This enum describes cursor shapes. - - The names are taken from the CSS W3C specification: - https://w3c.github.io/csswg-drafts/css-ui/#cursor - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Destroy the cursor shape device. - - The device cursor shape remains unchanged. - - - - - - Sets the device cursor to the specified shape. The compositor will - change the cursor image based on the specified shape. - - The cursor actually changes only if the input device focus is one of - the requesting client's surfaces. If any, the previous cursor image - (surface or shape) is replaced. - - The "shape" argument must be a valid enum entry, otherwise the - invalid_shape protocol error is raised. - - This is similar to the wl_pointer.set_cursor and - zwp_tablet_tool_v2.set_cursor requests, but this request accepts a - shape instead of contents in the form of a surface. Clients can mix - set_cursor and set_shape requests. - - The serial parameter must match the latest wl_pointer.enter or - zwp_tablet_tool_v2.proximity_in serial number sent to the client. - Otherwise the request will be ignored. - - - - - - diff --git a/meson.build b/meson.build index ed0bf4a0..8535a4d1 100644 --- a/meson.build +++ b/meson.build @@ -170,11 +170,13 @@ else fractional_scale = false endif -# TODO: check wayland-protocols version -wl_proto_xml += [wayland_protocols_datadir + '/unstable/tablet/tablet-unstable-v2.xml', # required by cursor-shape-v1 - 'cursor-shape-v1.xml', # TODO: use wayland-protocols - ] -add_project_arguments('-DHAVE_CURSOR_SHAPE', language: 'c') +if wayland_protocols.version().version_compare('>=1.31') # TODO: 1.32 + wl_proto_xml += [ + wayland_protocols_datadir + '/unstable/tablet/tablet-unstable-v2.xml', # required by cursor-shape-v1 + wayland_protocols_datadir + '/staging/cursor-shape/cursor-shape-v1.xml', + ] + add_project_arguments('-DHAVE_CURSOR_SHAPE', language: 'c') +endif foreach prot : wl_proto_xml wl_proto_headers += custom_target( From c2baaff3c1311335b46e75db27bfd79d4f7b623b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 28 Jun 2023 13:25:08 +0200 Subject: [PATCH 0368/1323] cursor-shape: use server-side cursors for custom (OSC-22), if possible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using a lookup table, try to map the user-provided xcursor string to a cursor-shape-v1 known shape. If we succeed, set the user’s custom cursor using server side cursors (i.e. using cursor-shape-v1). If not, fallback to trying to load the image ourselves (using wl_cursor_theme_get_cursor()), and set it using the legacy wl_pointer_set_cursor(). --- cursor-shape.c | 63 +++++++++++++++++++++++++++++++++- cursor-shape.h | 2 ++ render.c | 92 ++++++++++++++++++++++++++++++++++---------------- terminal.c | 13 +++++-- wayland.c | 1 + wayland.h | 2 ++ 6 files changed, 141 insertions(+), 32 deletions(-) diff --git a/cursor-shape.c b/cursor-shape.c index 48f7419f..aafeae8b 100644 --- a/cursor-shape.c +++ b/cursor-shape.c @@ -1,4 +1,9 @@ #include +#include + +#define LOG_MODULE "cursor-shape" +#define LOG_ENABLE_DBG 0 +#include "log.h" #include "cursor-shape.h" #include "debug.h" @@ -30,6 +35,7 @@ cursor_shape_to_string(enum cursor_shape shape) } #if defined(HAVE_CURSOR_SHAPE) + enum wp_cursor_shape_device_v1_shape cursor_shape_to_server_shape(enum cursor_shape shape) { @@ -51,4 +57,59 @@ cursor_shape_to_server_shape(enum cursor_shape shape) xassert(table[shape] != 0); return table[shape]; } -#endif + +enum wp_cursor_shape_device_v1_shape +cursor_string_to_server_shape(const char *xcursor) +{ + if (xcursor == NULL) + return 0; + + static const char *const table[][2] = { + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT] = {"default", "left_ptr"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CONTEXT_MENU] = {"context-menu"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_HELP] = {"help", "question_arrow"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER] = {"pointer", "hand"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_PROGRESS] = {"progress", "left_ptr_watch"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_WAIT] = {"wait", "watch"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CELL] = {"cell"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CROSSHAIR] = {"crosshair", "cross"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT] = {"text", "xterm"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_VERTICAL_TEXT] = {"vertical-text"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALIAS] = {"alias", "dnd-link"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_COPY] = {"copy", "dnd-copy"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_MOVE] = {"move"}, /* dnd-move? */ + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NO_DROP] = {"no-drop", "dnd-no-drop"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NOT_ALLOWED] = {"not-allowed", "crossed_circle"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_GRAB] = {"grab", "hand1"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_GRABBING] = {"grabbing"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_E_RESIZE] = {"e-resize", "right_side"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_N_RESIZE] = {"n-resize", "top_side"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NE_RESIZE] = {"ne-resize", "top_right_corner"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NW_RESIZE] = {"nw-resize", "top_left_corner"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_S_RESIZE] = {"s-resize", "bottom_side"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SE_RESIZE] = {"se-resize", "bottom_right_corner"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SW_RESIZE] = {"sw-resize", "bottom_left_corner"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_W_RESIZE] = {"w-resize", "left_side"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_EW_RESIZE] = {"ew-resize", "sb_h_double_arrow"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NS_RESIZE] = {"ns-resize", "sb_v_double_arrow"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NESW_RESIZE] = {"nesw-resize", "fd_double_arrow"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NWSE_RESIZE] = {"nwse-resize", "bd_double_arrow"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_COL_RESIZE] = {"col-resize", "sb_h_double_arrow"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ROW_RESIZE] = {"row-resize", "sb_v_double_arrow"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_SCROLL] = {"all-scroll", "fleur"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ZOOM_IN] = {"zoom-in"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ZOOM_OUT] = {"zoom-out"}, + }; + + for (size_t i = 0; i < ALEN(table); i++) { + for (size_t j = 0; j < ALEN(table[i]); j++) { + if (table[i][j] != NULL && strcmp(xcursor, table[i][j]) == 0) { + return i; + } + } + } + + return 0; +} + +#endif /* HAVE_CURSOR_SHAPE */ diff --git a/cursor-shape.h b/cursor-shape.h index 0cb0b4d8..a9619553 100644 --- a/cursor-shape.h +++ b/cursor-shape.h @@ -29,4 +29,6 @@ const char *cursor_shape_to_string(enum cursor_shape shape); #if defined(HAVE_CURSOR_SHAPE) enum wp_cursor_shape_device_v1_shape cursor_shape_to_server_shape( enum cursor_shape shape); +enum wp_cursor_shape_device_v1_shape cursor_string_to_server_shape( + const char *xcursor); #endif diff --git a/render.c b/render.c index b0b1ec32..df7a43f9 100644 --- a/render.c +++ b/render.c @@ -4270,41 +4270,62 @@ render_xcursor_update(struct seat *seat) xassert(seat->pointer.cursor != NULL); + const enum cursor_shape shape = seat->pointer.shape; + const char *const xcursor = seat->pointer.last_custom_xcursor; + #if defined(HAVE_CURSOR_SHAPE) - if (seat->pointer.shape_device != NULL && - seat->pointer.shape != CURSOR_SHAPE_CUSTOM) - { - LOG_DBG("setting cursor shape using cursor-shape-v1"); - wp_cursor_shape_device_v1_set_shape( - seat->pointer.shape_device, - seat->pointer.serial, - cursor_shape_to_server_shape(seat->pointer.shape)); - } else -#endif - { - LOG_DBG("setting cursor shape using a client-side cursor surface"); - const int scale = seat->pointer.scale; - struct wl_cursor_image *image = seat->pointer.cursor->images[0]; + if (seat->pointer.shape_device != NULL) { + xassert(shape != CURSOR_SHAPE_CUSTOM || xcursor != NULL); - wl_surface_attach( - seat->pointer.surface.surf, wl_cursor_image_get_buffer(image), 0, 0); + const enum wp_cursor_shape_device_v1_shape custom_shape = + (shape == CURSOR_SHAPE_CUSTOM && xcursor != NULL + ? cursor_string_to_server_shape(xcursor) + : 0); - wl_pointer_set_cursor( - seat->wl_pointer, seat->pointer.serial, - seat->pointer.surface.surf, - image->hotspot_x / scale, image->hotspot_y / scale); + if (shape != CURSOR_SHAPE_CUSTOM || custom_shape != 0) { + xassert(custom_shape == 0 || shape == CURSOR_SHAPE_CUSTOM); - wl_surface_damage_buffer( - seat->pointer.surface.surf, 0, 0, INT32_MAX, INT32_MAX); + const enum wp_cursor_shape_device_v1_shape wp_shape = custom_shape != 0 + ? custom_shape + : cursor_shape_to_server_shape(shape); - wl_surface_set_buffer_scale(seat->pointer.surface.surf, scale); + LOG_DBG("setting %scursor shape using cursor-shape-v1", + custom_shape != 0 ? "custom " : ""); - xassert(seat->pointer.xcursor_callback == NULL); - seat->pointer.xcursor_callback = wl_surface_frame(seat->pointer.surface.surf); - wl_callback_add_listener(seat->pointer.xcursor_callback, &xcursor_listener, seat); + wp_cursor_shape_device_v1_set_shape( + seat->pointer.shape_device, + seat->pointer.serial, + wp_shape); - wl_surface_commit(seat->pointer.surface.surf); + return; + } } +#endif + + LOG_DBG("setting %scursor shape using a client-side cursor surface", + shape == CURSOR_SHAPE_CUSTOM ? "custom " : ""); + + const int scale = seat->pointer.scale; + struct wl_cursor_image *image = seat->pointer.cursor->images[0]; + + wl_surface_attach( + seat->pointer.surface.surf, wl_cursor_image_get_buffer(image), 0, 0); + + wl_pointer_set_cursor( + seat->wl_pointer, seat->pointer.serial, + seat->pointer.surface.surf, + image->hotspot_x / scale, image->hotspot_y / scale); + + wl_surface_damage_buffer( + seat->pointer.surface.surf, 0, 0, INT32_MAX, INT32_MAX); + + wl_surface_set_buffer_scale(seat->pointer.surface.surf, scale); + + xassert(seat->pointer.xcursor_callback == NULL); + seat->pointer.xcursor_callback = wl_surface_frame(seat->pointer.surface.surf); + wl_callback_add_listener(seat->pointer.xcursor_callback, &xcursor_listener, seat); + + wl_surface_commit(seat->pointer.surface.surf); } static void @@ -4467,8 +4488,13 @@ render_xcursor_set(struct seat *seat, struct terminal *term, return true; } - if (seat->pointer.shape == shape) + if (seat->pointer.shape == shape && + !(shape == CURSOR_SHAPE_CUSTOM && + strcmp(seat->pointer.last_custom_xcursor, + term->mouse_user_cursor) != 0)) + { return true; + } /* TODO: skip this when using server-side cursors */ if (shape != CURSOR_SHAPE_HIDDEN) { @@ -4491,8 +4517,16 @@ render_xcursor_set(struct seat *seat, struct terminal *term, return false; } } - } else + + if (shape == CURSOR_SHAPE_CUSTOM) { + free(seat->pointer.last_custom_xcursor); + seat->pointer.last_custom_xcursor = xstrdup(term->mouse_user_cursor); + } + } else { seat->pointer.cursor = NULL; + free(seat->pointer.last_custom_xcursor); + seat->pointer.last_custom_xcursor = NULL; + } /* FDM hook takes care of actual rendering */ seat->pointer.shape = shape; diff --git a/terminal.c b/terminal.c index 3f2c9095..4a0d99cd 100644 --- a/terminal.c +++ b/terminal.c @@ -3130,8 +3130,15 @@ term_xcursor_update_for_seat(struct terminal *term, struct seat *seat) if (seat->pointer.hidden) shape = CURSOR_SHAPE_HIDDEN; - else if (render_xcursor_is_valid(seat, term->mouse_user_cursor)) +#if defined(HAVE_CURSOR_SHAPE) + else if (cursor_string_to_server_shape(term->mouse_user_cursor) != 0 +#elif + else if (true +#endif + || render_xcursor_is_valid(seat, term->mouse_user_cursor)) + { shape = CURSOR_SHAPE_CUSTOM; + } else if (seat->mouse.col >= 0 && seat->mouse.row >= 0 && @@ -3716,6 +3723,8 @@ void term_set_user_mouse_cursor(struct terminal *term, const char *cursor) { free(term->mouse_user_cursor); - term->mouse_user_cursor = cursor != NULL ? xstrdup(cursor) : NULL; + term->mouse_user_cursor = cursor != NULL && strlen(cursor) > 0 + ? xstrdup(cursor) + : NULL; term_xcursor_update(term); } diff --git a/wayland.c b/wayland.c index f3a5619d..e862b5e8 100644 --- a/wayland.c +++ b/wayland.c @@ -234,6 +234,7 @@ seat_destroy(struct seat *seat) ime_reset_pending(seat); free(seat->clipboard.text); free(seat->primary.text); + free(seat->pointer.last_custom_xcursor); free(seat->name); } diff --git a/wayland.h b/wayland.h index b30ad307..d2c1ead1 100644 --- a/wayland.h +++ b/wayland.h @@ -159,6 +159,8 @@ struct seat { float scale; bool hidden; enum cursor_shape shape; + char *last_custom_xcursor; + struct wl_callback *xcursor_callback; bool xcursor_pending; } pointer; From c2e481fb6af7a2b9c7e3237436aee67019a32878 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 29 Jun 2023 16:06:01 +0200 Subject: [PATCH 0369/1323] meson: bump wayland-protocols version required for cursor-shape to 1.32 --- meson.build | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/meson.build b/meson.build index 8535a4d1..10746d9d 100644 --- a/meson.build +++ b/meson.build @@ -169,8 +169,7 @@ if wayland_protocols.version().version_compare('>=1.31') else fractional_scale = false endif - -if wayland_protocols.version().version_compare('>=1.31') # TODO: 1.32 +if wayland_protocols.version().version_compare('>=1.32') wl_proto_xml += [ wayland_protocols_datadir + '/unstable/tablet/tablet-unstable-v2.xml', # required by cursor-shape-v1 wayland_protocols_datadir + '/staging/cursor-shape/cursor-shape-v1.xml', From 7bfa700c556c529036646e3224c6969e845840f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 29 Jun 2023 16:06:52 +0200 Subject: [PATCH 0370/1323] terminal: #elif -> #else --- terminal.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal.c b/terminal.c index 4a0d99cd..2d1313c1 100644 --- a/terminal.c +++ b/terminal.c @@ -3132,7 +3132,7 @@ term_xcursor_update_for_seat(struct terminal *term, struct seat *seat) #if defined(HAVE_CURSOR_SHAPE) else if (cursor_string_to_server_shape(term->mouse_user_cursor) != 0 -#elif +#else else if (true #endif || render_xcursor_is_valid(seat, term->mouse_user_cursor)) From 6388954e8f908a8cfd2f170c6a4f2c05370de762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 29 Jun 2023 16:07:56 +0200 Subject: [PATCH 0371/1323] =?UTF-8?q?render:=20move=20variables=20inside?= =?UTF-8?q?=20#ifdef,=20as=20they=E2=80=99re=20not=20used=20outside=20of?= =?UTF-8?q?=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- render.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.c b/render.c index df7a43f9..5a757a88 100644 --- a/render.c +++ b/render.c @@ -4270,10 +4270,10 @@ render_xcursor_update(struct seat *seat) xassert(seat->pointer.cursor != NULL); +#if defined(HAVE_CURSOR_SHAPE) const enum cursor_shape shape = seat->pointer.shape; const char *const xcursor = seat->pointer.last_custom_xcursor; -#if defined(HAVE_CURSOR_SHAPE) if (seat->pointer.shape_device != NULL) { xassert(shape != CURSOR_SHAPE_CUSTOM || xcursor != NULL); From a361d7917b311cad2def1ef42e740670cbddd7b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 29 Jun 2023 16:12:54 +0200 Subject: [PATCH 0372/1323] main/client: add a version feature flag for cursor-shape --- client.c | 3 ++- foot-features.h | 9 +++++++++ main.c | 3 ++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/client.c b/client.c index 84bfb2c3..99c7c1e8 100644 --- a/client.c +++ b/client.c @@ -67,12 +67,13 @@ version_and_features(void) { static char buf[256]; snprintf(buf, sizeof(buf), - "version: %s %cpgo %cime %cgraphemes %cfractional-scaling %cassertions", + "version: %s %cpgo %cime %cgraphemes %cfractional-scaling %ccursor-shape %cassertions", FOOT_VERSION, feature_pgo() ? '+' : '-', feature_ime() ? '+' : '-', feature_graphemes() ? '+' : '-', feature_fractional_scaling() ? '+' : ':', + feature_cursor_shape() ? '+' : '-', feature_assertions() ? '+' : '-'); return buf; } diff --git a/foot-features.h b/foot-features.h index 77923aaf..f8043c12 100644 --- a/foot-features.h +++ b/foot-features.h @@ -46,3 +46,12 @@ static inline bool feature_fractional_scaling(void) return false; #endif } + +static inline bool feature_cursor_shape(void) +{ +#if defined(HAVE_CURSOR_SHAPE) + return true; +#else + return false; +#endif +} diff --git a/main.c b/main.c index 6dd9e468..fc329574 100644 --- a/main.c +++ b/main.c @@ -53,12 +53,13 @@ version_and_features(void) { static char buf[256]; snprintf(buf, sizeof(buf), - "version: %s %cpgo %cime %cgraphemes %cfractional-scaling %cassertions", + "version: %s %cpgo %cime %cgraphemes %cfractional-scaling %ccursor-shape %cassertions", FOOT_VERSION, feature_pgo() ? '+' : '-', feature_ime() ? '+' : '-', feature_graphemes() ? '+' : '-', feature_fractional_scaling() ? '+' : '-', + feature_cursor_shape() ? '+' : '-', feature_assertions() ? '+' : '-'); return buf; } From 8fc43ccd2d80488dc3ef1b73e6a1309c4530e7e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 30 Jun 2023 08:29:21 +0200 Subject: [PATCH 0373/1323] meson: log availability of cursor-shape-v1 --- meson.build | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/meson.build b/meson.build index 10746d9d..0d75e1e4 100644 --- a/meson.build +++ b/meson.build @@ -175,6 +175,9 @@ if wayland_protocols.version().version_compare('>=1.32') wayland_protocols_datadir + '/staging/cursor-shape/cursor-shape-v1.xml', ] add_project_arguments('-DHAVE_CURSOR_SHAPE', language: 'c') + cursor_shape = true +else + cursor_shape = false endif foreach prot : wl_proto_xml @@ -388,6 +391,7 @@ summary( 'Grapheme clustering': utf8proc.found(), 'Wayland: xdg-activation-v1': xdg_activation, 'Wayland: fractional-scale-v1': fractional_scale, + 'Wayland: cursor-shape-v1': cursor_shape, 'utmp backend': utmp_backend, 'utmp helper default path': utmp_default_helper_path, 'Build terminfo': tic.found(), From ba09d55aabb8979a305c13afe8d51f98e5118410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 3 Jul 2023 14:26:01 +0200 Subject: [PATCH 0374/1323] term_xcursor_update_for_seat(): fix missing evaluation of render_xcursor_is_valid() When compiling *without* cursor-shape-v1 support, term_xcursor_update_for_seat() would incorrectly set shape=CURSOR_SHAPE_CUSTOM, even though no custom cursor had been set by the user. This resulted in a crash in render_xcursor_set(), when trying to use a NULL-string as custom cursor. --- terminal.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal.c b/terminal.c index 2d1313c1..fff55019 100644 --- a/terminal.c +++ b/terminal.c @@ -3133,7 +3133,7 @@ term_xcursor_update_for_seat(struct terminal *term, struct seat *seat) #if defined(HAVE_CURSOR_SHAPE) else if (cursor_string_to_server_shape(term->mouse_user_cursor) != 0 #else - else if (true + else if (false #endif || render_xcursor_is_valid(seat, term->mouse_user_cursor)) { From 3800b279d668403699e96c3c7974d63bf985d83b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 3 Jul 2023 14:42:22 +0200 Subject: [PATCH 0375/1323] =?UTF-8?q?meson:=20move=20cursor-shape.{c,h}=20?= =?UTF-8?q?from=20=E2=80=98foot=E2=80=99=20binary=20to=20vtlib?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This should fix a build issue when doing partial PGO builds, when cursor-shape-v1 is *available*. --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 0d75e1e4..fc2491d2 100644 --- a/meson.build +++ b/meson.build @@ -233,6 +233,7 @@ vtlib = static_library( 'vtlib', 'base64.c', 'base64.h', 'composed.c', 'composed.h', + 'cursor-shape.c', 'cursor-shape.h', 'csi.c', 'csi.h', 'dcs.c', 'dcs.h', 'macros.h', @@ -271,7 +272,6 @@ executable( 'box-drawing.c', 'box-drawing.h', 'config.c', 'config.h', 'commands.c', 'commands.h', - 'cursor-shape.c', 'cursor-shape.h', 'extract.c', 'extract.h', 'fdm.c', 'fdm.h', 'foot-features.h', From 247035e9e4f8b54ed48e51971bbdd337866038b5 Mon Sep 17 00:00:00 2001 From: Craig Barnes Date: Mon, 3 Jul 2023 20:11:20 +0100 Subject: [PATCH 0376/1323] meson: fix typo in meson_options.txt When using muon[1] instead of meson, this was causing the following error: $ muon setup bld .../meson_options.txt:26:124: error unterminated hex escape .../meson_options.txt:26:223: error unterminated string [1]: https://muon.build/ --- meson_options.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson_options.txt b/meson_options.txt index 76121e60..d16e23ae 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -23,6 +23,6 @@ option('systemd-units-dir', type: 'string', value: '', description: 'Where to install the systemd service files (absolute path). Default: ${systemduserunitdir}') option('utmp-backend', type: 'combo', value: 'auto', choices: ['none', 'libutempter', 'ulog', 'auto'], - description: 'Which utmp logging backend to use. This affects how (with what arguments) the utmp helper binary (see \utmp-default-helper-path\')is called. Default: auto (linux=libutempter, freebsd=ulog, others=none)') + description: 'Which utmp logging backend to use. This affects how (with what arguments) the utmp helper binary (see \'utmp-default-helper-path\')is called. Default: auto (linux=libutempter, freebsd=ulog, others=none)') option('utmp-default-helper-path', type: 'string', value: 'auto', description: 'Default path to the utmp helper binary. Default: auto-detect') From 4a7382891112d898d4ba0f8d445439b8ad86477e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 4 Jul 2023 08:38:07 +0200 Subject: [PATCH 0377/1323] changelog: fractional-scaling-v1 -> fractional-scale-v1 --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6625c56d..7cbc5b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,9 +50,9 @@ is `auto`, which will select `libutempter` on Linux, `ulog` on FreeBSD, and `none` for all others. * Sixel aspect ratio. -* Support for the new fractional-scaling-v1 Wayland protocol. This +* Support for the new `fractional-scale-v1` Wayland protocol. This brings true fractional scaling to Wayland in general, and with this - release, foot. + release, to foot. * Support for the new `cursor-shape-v1` Wayland protocol, i.e. server side cursor shapes ([#1379][1379]). From d2fcb5343f57c132abec9a21ad89d1505a32c47c Mon Sep 17 00:00:00 2001 From: CismonX Date: Wed, 5 Jul 2023 00:19:21 +0800 Subject: [PATCH 0378/1323] input: add basic support for touchscreen input Closes #517 --- CHANGELOG.md | 2 + README.md | 12 ++ config.c | 20 ++++ config.h | 4 + doc/foot.1.scd | 12 ++ doc/foot.ini.5.scd | 8 ++ foot.ini | 3 + input.c | 266 ++++++++++++++++++++++++++++++++++++++++---- input.h | 1 + tests/test-config.c | 16 +++ wayland.c | 23 +++- wayland.h | 19 ++++ 12 files changed, 364 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cbc5b61..87a35bdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,8 +55,10 @@ release, to foot. * Support for the new `cursor-shape-v1` Wayland protocol, i.e. server side cursor shapes ([#1379][1379]). +* Support for touchscreen input ([#517][517]). [1379]: https://codeberg.org/dnkl/foot/issues/1379 +[517]: https://codeberg.org/dnkl/foot/issues/517 ### Changed diff --git a/README.md b/README.md index 0d6262dc..42be5792 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ The fast, lightweight and minimalistic Wayland terminal emulator. 1. [Normal mode](#normal-mode) 1. [Scrollback search](#scrollback-search) 1. [Mouse](#mouse) + 1. [Touchscreen](#touchscreen) 1. [Server (daemon) mode](#server-daemon-mode) 1. [URLs](#urls) 1. [Shell integration](#shell-integration) @@ -246,6 +247,17 @@ These are the default shortcuts. See `man foot.ini` and the example : Scroll up/down in history +### Touchscreen + +tap +: Emulates mouse left button click. + +drag +: Scrolls up/down in history. +: Holding for a while before dragging (time delay can be configured) + emulates mouse dragging with left button held. + + ## Server (daemon) mode When run normally, **foot** is a single-window application; if you diff --git a/config.c b/config.c index 3d02355f..5297bbdc 100644 --- a/config.c +++ b/config.c @@ -2475,6 +2475,20 @@ parse_section_tweak(struct context *ctx) } } +static bool +parse_section_touch(struct context *ctx) { + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (strcmp(key, "long-press-delay") == 0) + return value_to_uint32(ctx, 10, &conf->touch.long_press_delay); + + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + static bool parse_key_value(char *kv, const char **section, const char **key, const char **value) { @@ -2554,6 +2568,7 @@ enum section { SECTION_TEXT_BINDINGS, SECTION_ENVIRONMENT, SECTION_TWEAK, + SECTION_TOUCH, SECTION_COUNT, }; @@ -2579,6 +2594,7 @@ static const struct { [SECTION_TEXT_BINDINGS] = {&parse_section_text_bindings, "text-bindings"}, [SECTION_ENVIRONMENT] = {&parse_section_environment, "environment"}, [SECTION_TWEAK] = {&parse_section_tweak, "tweak"}, + [SECTION_TOUCH] = {&parse_section_touch, "touch"}, }; static_assert(ALEN(section_info) == SECTION_COUNT, "section info array size mismatch"); @@ -3026,6 +3042,10 @@ config_load(struct config *conf, const char *conf_path, .sixel = true, }, + .touch = { + .long_press_delay = 400, + }, + .env_vars = tll_init(), #if defined(UTMP_DEFAULT_HELPER_PATH) .utmp_helper_path = ((strlen(UTMP_DEFAULT_HELPER_PATH) > 0 && diff --git a/config.h b/config.h index 2034752f..20c07f6c 100644 --- a/config.h +++ b/config.h @@ -347,6 +347,10 @@ struct config { bool sixel; } tweak; + struct { + uint32_t long_press_delay; + } touch; + user_notifications_t notifications; }; diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 60420bef..1cdf47e4 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -283,6 +283,18 @@ default) available; see *foot.ini*(5). *wheel* Scroll up/down in history +## TOUCHSCREEN + +*tap* + Emulates mouse left button click. + +*drag* + Scrolls up/down in history. + + Holding for a while before dragging (time delay can be configured) + emulates mouse dragging with left button held. + + # FONT FORMAT The font is specified in FontConfig syntax. That is, a colon-separated diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index e28cf416..ac22ae5a 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -535,6 +535,14 @@ applications can change these at runtime. Default: _yes_. +# SECTION: touch + +*long-press-delay* + Number of milliseconds to distinguish between a short press and + a long press on the touchscreen. + + Default: _400_. + # SECTION: colors This section controls the 16 ANSI colors, the default foreground and diff --git a/foot.ini b/foot.ini index 61e88aec..94d82f6f 100644 --- a/foot.ini +++ b/foot.ini @@ -70,6 +70,9 @@ # hide-when-typing=no # alternate-scroll-mode=yes +[touch] +# long-press-delay=400 + [colors] # alpha=1.0 # background=002b36 diff --git a/input.c b/input.c index 0f638cc1..3bf6535a 100644 --- a/input.c +++ b/input.c @@ -1721,6 +1721,36 @@ xcursor_for_csd_border(struct terminal *term, int x, int y) } } +static void +mouse_button_state_reset(struct seat *seat) +{ + tll_free(seat->mouse.buttons); + seat->mouse.count = 0; + seat->mouse.last_released_button = 0; + memset(&seat->mouse.last_time, 0, sizeof(seat->mouse.last_time)); +} + +static void +mouse_coord_pixel_to_cell(struct seat *seat, const struct terminal *term, + int x, int y) +{ + /* + * Translate x,y pixel coordinate to a cell coordinate, or -1 + * if the cursor is outside the grid. I.e. if it is inside the + * margins. + */ + + if (x < term->margins.left || x >= term->width - term->margins.right) + seat->mouse.col = -1; + else + seat->mouse.col = (x - term->margins.left) / term->cell_width; + + if (y < term->margins.top || y >= term->height - term->margins.bottom) + seat->mouse.row = -1; + else + seat->mouse.row = (y - term->margins.top) / term->cell_height; +} + static void wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface, @@ -1733,6 +1763,24 @@ wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, } struct seat *seat = data; + + if (seat->wl_touch != NULL) { + switch (seat->touch.state) { + case TOUCH_STATE_IDLE: + mouse_button_state_reset(seat); + seat->touch.state = TOUCH_STATE_INHIBITED; + break; + + case TOUCH_STATE_INHIBITED: + break; + + case TOUCH_STATE_HELD: + case TOUCH_STATE_DRAGGING: + case TOUCH_STATE_SCROLLING: + return; + } + } + struct wl_window *win = wl_surface_get_user_data(surface); struct terminal *term = win->term; @@ -1759,22 +1807,7 @@ wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, switch (term->active_surface) { case TERM_SURF_GRID: { - /* - * Translate x,y pixel coordinate to a cell coordinate, or -1 - * if the cursor is outside the grid. I.e. if it is inside the - * margins. - */ - - if (x < term->margins.left || x >= term->width - term->margins.right) - seat->mouse.col = -1; - else - seat->mouse.col = (x - term->margins.left) / term->cell_width; - - if (y < term->margins.top || y >= term->height - term->margins.bottom) - seat->mouse.row = -1; - else - seat->mouse.row = (y - term->margins.top) / term->cell_height; - + mouse_coord_pixel_to_cell(seat, term, x, y); break; } @@ -1802,6 +1835,14 @@ wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface) { struct seat *seat = data; + + if (seat->wl_touch != NULL) { + if (seat->touch.state != TOUCH_STATE_INHIBITED) { + return; + } + seat->touch.state = TOUCH_STATE_IDLE; + } + struct terminal *old_moused = seat->mouse_focus; LOG_DBG( @@ -1824,10 +1865,7 @@ wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, /* Reset mouse state */ seat->mouse.x = seat->mouse.y = 0; seat->mouse.col = seat->mouse.row = 0; - tll_free(seat->mouse.buttons); - seat->mouse.count = 0; - seat->mouse.last_released_button = 0; - memset(&seat->mouse.last_time, 0, sizeof(seat->mouse.last_time)); + mouse_button_state_reset(seat); for (size_t i = 0; i < ALEN(seat->mouse.aggregated); i++) seat->mouse.aggregated[i] = 0.0; seat->mouse.have_discrete = false; @@ -1879,6 +1917,11 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) { struct seat *seat = data; + + /* Touch-emulated pointer events have wl_pointer == NULL. */ + if (wl_pointer != NULL && seat->touch.state != TOUCH_STATE_INHIBITED) + return; + struct wayland *wayl = seat->wayl; struct terminal *term = seat->mouse_focus; @@ -2102,6 +2145,11 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, xassert(serial != 0); struct seat *seat = data; + + /* Touch-emulated pointer events have wl_pointer == NULL. */ + if (wl_pointer != NULL && seat->touch.state != TOUCH_STATE_INHIBITED) + return; + struct wayland *wayl = seat->wayl; struct terminal *term = seat->mouse_focus; @@ -2559,6 +2607,9 @@ wl_pointer_axis(void *data, struct wl_pointer *wl_pointer, { struct seat *seat = data; + if (seat->touch.state != TOUCH_STATE_INHIBITED) + return; + if (seat->mouse.have_discrete) return; @@ -2588,6 +2639,10 @@ wl_pointer_axis_discrete(void *data, struct wl_pointer *wl_pointer, uint32_t axis, int32_t discrete) { struct seat *seat = data; + + if (seat->touch.state != TOUCH_STATE_INHIBITED) + return; + seat->mouse.have_discrete = true; int amount = discrete; @@ -2604,6 +2659,10 @@ static void wl_pointer_frame(void *data, struct wl_pointer *wl_pointer) { struct seat *seat = data; + + if (seat->touch.state != TOUCH_STATE_INHIBITED) + return; + seat->mouse.have_discrete = false; } @@ -2619,6 +2678,9 @@ wl_pointer_axis_stop(void *data, struct wl_pointer *wl_pointer, { struct seat *seat = data; + if (seat->touch.state != TOUCH_STATE_INHIBITED) + return; + xassert(axis < ALEN(seat->mouse.aggregated)); seat->mouse.aggregated[axis] = 0.; } @@ -2634,3 +2696,167 @@ const struct wl_pointer_listener pointer_listener = { .axis_stop = wl_pointer_axis_stop, .axis_discrete = wl_pointer_axis_discrete, }; + +static bool +touch_to_scroll(struct seat *seat, struct terminal *term, + wl_fixed_t surface_x, wl_fixed_t surface_y) +{ + bool coord_updated = false; + + int y = wl_fixed_to_int(surface_y) * term->scale; + int rows = (y - seat->mouse.y) / term->cell_height; + if (rows != 0) { + mouse_scroll(seat, -rows, WL_POINTER_AXIS_VERTICAL_SCROLL); + seat->mouse.y += rows * term->cell_height; + coord_updated = true; + } + + int x = wl_fixed_to_int(surface_x) * term->scale; + int cols = (x - seat->mouse.x) / term->cell_width; + if (cols != 0) { + mouse_scroll(seat, -cols, WL_POINTER_AXIS_HORIZONTAL_SCROLL); + seat->mouse.x += cols * term->cell_width; + coord_updated = true; + } + + return coord_updated; +} + +static void +wl_touch_down(void *data, struct wl_touch *wl_touch, uint32_t serial, + uint32_t time, struct wl_surface *surface, int32_t id, + wl_fixed_t surface_x, wl_fixed_t surface_y) +{ + struct seat *seat = data; + + if (seat->touch.state != TOUCH_STATE_IDLE) + return; + + struct wl_window *win = wl_surface_get_user_data(surface); + struct terminal *term = win->term; + + term->active_surface = term_surface_kind(term, surface); + if (term->active_surface != TERM_SURF_GRID) + return; + + LOG_DBG("touch_down: touch=%p, x=%d, y=%d", (void *)wl_touch, + wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y)); + + int x = wl_fixed_to_int(surface_x) * term->scale; + int y = wl_fixed_to_int(surface_y) * term->scale; + + seat->mouse.x = x; + seat->mouse.y = y; + mouse_coord_pixel_to_cell(seat, term, x, y); + + seat->touch.state = TOUCH_STATE_HELD; + seat->touch.serial = serial; + seat->touch.time = time + term->conf->touch.long_press_delay; + seat->touch.surface = surface; + seat->touch.id = id; +} + +static void +wl_touch_up(void *data, struct wl_touch *wl_touch, uint32_t serial, + uint32_t time, int32_t id) +{ + struct seat *seat = data; + + if (seat->touch.state <= TOUCH_STATE_IDLE || id != seat->touch.id) + return; + + LOG_DBG("touch_up: touch=%p", (void *)wl_touch); + + struct wl_window *win = wl_surface_get_user_data(seat->touch.surface); + struct terminal *term = win->term; + + seat->mouse_focus = term; + + switch (seat->touch.state) { + case TOUCH_STATE_HELD: + wl_pointer_button(seat, NULL, seat->touch.serial, time, BTN_LEFT, + WL_POINTER_BUTTON_STATE_PRESSED); + /* fallthrough */ + case TOUCH_STATE_DRAGGING: + wl_pointer_button(seat, NULL, serial, time, BTN_LEFT, + WL_POINTER_BUTTON_STATE_RELEASED); + /* fallthrough */ + case TOUCH_STATE_SCROLLING: + seat->touch.state = TOUCH_STATE_IDLE; + break; + + case TOUCH_STATE_INHIBITED: + case TOUCH_STATE_IDLE: + BUG("Bad touch state: %d", seat->touch.state); + break; + } + + seat->mouse_focus = NULL; +} + +static void +wl_touch_motion(void *data, struct wl_touch *wl_touch, uint32_t time, + int32_t id, wl_fixed_t surface_x, wl_fixed_t surface_y) +{ + struct seat *seat = data; + if (seat->touch.state <= TOUCH_STATE_IDLE || id != seat->touch.id) + return; + + LOG_DBG("touch_motion: touch=%p, x=%d, y=%d", (void *)wl_touch, + wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y)); + + struct wl_window *win = wl_surface_get_user_data(seat->touch.surface); + struct terminal *term = win->term; + + seat->mouse_focus = term; + + switch (seat->touch.state) { + case TOUCH_STATE_HELD: + if (time <= seat->touch.time) { + if (touch_to_scroll(seat, term, surface_x, surface_y)) + seat->touch.state = TOUCH_STATE_SCROLLING; + break; + } else { + wl_pointer_button(seat, NULL, seat->touch.serial, time, BTN_LEFT, + WL_POINTER_BUTTON_STATE_PRESSED); + seat->touch.state = TOUCH_STATE_DRAGGING; + /* fallthrough */ + } + case TOUCH_STATE_DRAGGING: + wl_pointer_motion(seat, NULL, time, surface_x, surface_y); + break; + case TOUCH_STATE_SCROLLING: + touch_to_scroll(seat, term, surface_x, surface_y); + break; + + case TOUCH_STATE_INHIBITED: + case TOUCH_STATE_IDLE: + BUG("Bad touch state: %d", seat->touch.state); + break; + } + + seat->mouse_focus = NULL; +} + +static void +wl_touch_frame(void *data, struct wl_touch *wl_touch) +{ +} + +static void +wl_touch_cancel(void *data, struct wl_touch *wl_touch) +{ + struct seat *seat = data; + if (seat->touch.state == TOUCH_STATE_INHIBITED) + return; + + seat->touch.state = TOUCH_STATE_IDLE; +} + +const struct wl_touch_listener touch_listener = { + .down = wl_touch_down, + .up = wl_touch_up, + .motion = wl_touch_motion, + .frame = wl_touch_frame, + .cancel = wl_touch_cancel, +}; diff --git a/input.h b/input.h index 825dc3be..906008d5 100644 --- a/input.h +++ b/input.h @@ -26,6 +26,7 @@ extern const struct wl_keyboard_listener keyboard_listener; extern const struct wl_pointer_listener pointer_listener; +extern const struct wl_touch_listener touch_listener; void input_repeat(struct seat *seat, uint32_t key); diff --git a/tests/test-config.c b/tests/test-config.c index c70f7a43..e59c104e 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -662,6 +662,21 @@ test_section_mouse(void) config_free(&conf); } +static void +test_section_touch(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "touch", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_touch, "invalid-key"); + + test_uint32(&ctx, &parse_section_touch, "long-press-delay", + &conf.touch.long_press_delay); + + config_free(&conf); +} + static void test_section_colors(void) { @@ -1347,6 +1362,7 @@ main(int argc, const char *const *argv) test_section_url(); test_section_cursor(); test_section_mouse(); + test_section_touch(); test_section_colors(); test_section_csd(); test_section_key_bindings(); diff --git a/wayland.c b/wayland.c index e862b5e8..a25ec0f6 100644 --- a/wayland.c +++ b/wayland.c @@ -222,6 +222,8 @@ seat_destroy(struct seat *seat) wl_keyboard_release(seat->wl_keyboard); if (seat->wl_pointer != NULL) wl_pointer_release(seat->wl_pointer); + if (seat->wl_touch != NULL) + wl_touch_release(seat->wl_touch); #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED if (seat->wl_text_input != NULL) @@ -284,9 +286,10 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat, struct seat *seat = data; xassert(seat->wl_seat == wl_seat); - LOG_DBG("%s: keyboard=%s, pointer=%s", seat->name, + LOG_DBG("%s: keyboard=%s, pointer=%s, touch=%s", seat->name, (caps & WL_SEAT_CAPABILITY_KEYBOARD) ? "yes" : "no", - (caps & WL_SEAT_CAPABILITY_POINTER) ? "yes" : "no"); + (caps & WL_SEAT_CAPABILITY_POINTER) ? "yes" : "no", + (caps & WL_SEAT_CAPABILITY_TOUCH) ? "yes" : "no"); if (caps & WL_SEAT_CAPABILITY_KEYBOARD) { if (seat->wl_keyboard == NULL) { @@ -359,6 +362,22 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat, seat->pointer.cursor = NULL; } } + + if (caps & WL_SEAT_CAPABILITY_TOUCH) { + if (seat->wl_touch == NULL) { + seat->wl_touch = wl_seat_get_touch(wl_seat); + wl_touch_add_listener(seat->wl_touch, &touch_listener, seat); + + seat->touch.state = TOUCH_STATE_IDLE; + } + } else { + if (seat->wl_touch != NULL) { + wl_touch_release(seat->wl_touch); + seat->wl_touch = NULL; + } + + seat->touch.state = TOUCH_STATE_INHIBITED; + } } static void diff --git a/wayland.h b/wayland.h index d2c1ead1..6d1cd727 100644 --- a/wayland.h +++ b/wayland.h @@ -47,6 +47,14 @@ enum data_offer_mime_type { DATA_OFFER_MIME_TEXT_UTF8_STRING, }; +enum touch_state { + TOUCH_STATE_INHIBITED = -1, + TOUCH_STATE_IDLE, + TOUCH_STATE_HELD, + TOUCH_STATE_DRAGGING, + TOUCH_STATE_SCROLLING, +}; + struct wayl_surface { struct wl_surface *surf; #if defined(HAVE_FRACTIONAL_SCALE) @@ -165,6 +173,17 @@ struct seat { bool xcursor_pending; } pointer; + /* Touch state */ + struct wl_touch *wl_touch; + struct { + enum touch_state state; + + uint32_t serial; + uint32_t time; + struct wl_surface *surface; + int32_t id; + } touch; + struct { int x; int y; From 080a11eb7374af3335bde3841789bd2b659237c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= Date: Thu, 15 Dec 2022 11:49:51 -0500 Subject: [PATCH 0379/1323] bind control-shift-u to unicode-input, move urls to o Having a keybinding to invoke arbitrary unicode characters is very useful. It's often used as a method of last resort to communicate with people outside of your main language. For example, if you want to type the last letter of my real name, you can invoke the latin-1 character 0xe9 or unicode 0x00e9. You can also use this to type special characters, for example, unicode U+1F4A9 is of course, the infamous PILE OF POO, which is sure to produce million laughs everywhere you go. In foot, there's no keybinding by default to invoke the very useful unicode-input command. There is no "standard" (as in "ISO") keybinding this either. But there *is* a de-facto standard currently deployed by *both* GTK and Qt (a rare feat) *and* Chrome OS (an even rarer feat) and it's control-shift-u. Alternatives include Control-x 8 (emacs), Control V u (vim), Alt (Windows, LibreOffice), or Option (Mac). I doubt we want to adopt any of those. So let's use control-shift-u for this. Unfortunately, it's currently assigned to show-urls-launch, which is unfortunate, but insurmountable. We can reassign this keybinding elsewhere. I have picked control-shift-o in my configuration, because "o" is a good mnemonic for "open URLs". Others have suggested "m" instead. Closes: #1183 --- CHANGELOG.md | 4 ++++ config.c | 3 ++- doc/foot.1.scd | 2 +- doc/foot.ini.5.scd | 4 ++-- foot.ini | 4 ++-- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87a35bdd..f02cf039 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,9 @@ ### Changed * Minimum required meson version is now 0.59 ([#1371][1371]). +* `Control+Shift+u` now bound to `unicode-input` to follow the + convention established in GTK and Qt, `show-urls-launch` now bound + to `Control+Shift+o` ([#1183][1183]) * Kitty keyboard protocol: F3 is now encoded as `CSI 13~` instead of `CSI R`. The kitty keyboard protocol originally allowed F3 to be encoded as `CSI R`, but this was removed from the specification @@ -92,6 +95,7 @@ removed. [1371]: https://codeberg.org/dnkl/foot/pulls/1371 +[1183]: https://codeberg.org/dnkl/foot/issues/1183 [1360]: https://codeberg.org/dnkl/foot/issues/1360 [1383]: https://codeberg.org/dnkl/foot/issues/1383 diff --git a/config.c b/config.c index 5297bbdc..1cefba3e 100644 --- a/config.c +++ b/config.c @@ -2807,7 +2807,8 @@ add_default_key_bindings(struct config *conf) {BIND_ACTION_FONT_SIZE_RESET, m_ctrl, {{XKB_KEY_0}}}, {BIND_ACTION_FONT_SIZE_RESET, m_ctrl, {{XKB_KEY_KP_0}}}, {BIND_ACTION_SPAWN_TERMINAL, m_ctrl_shift, {{XKB_KEY_n}}}, - {BIND_ACTION_SHOW_URLS_LAUNCH, m_ctrl_shift, {{XKB_KEY_u}}}, + {BIND_ACTION_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}}}, }; diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 1cdf47e4..6143275c 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -310,7 +310,7 @@ Foot supports URL detection. But, unlike many other terminal emulators, where URLs are highlighted when they are hovered and opened by clicking on them, foot uses a keyboard driven approach. -Pressing *ctrl*+*shift*+*u* enters _“URL mode”_, where all currently +Pressing *ctrl*+*shift*+*o* enters _“Open URL mode”_, where all currently visible URLs are underlined, and is associated with a _“jump-label”_. The jump-label indicates the _key sequence_ (e.g. *”AF”*) to use to activate the URL. diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index ac22ae5a..273a74c2 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -834,7 +834,7 @@ e.g. *search-start=none*. *show-urls-launch* Enter URL mode, where all currently visible URLs are tagged with a jump label with a key sequence that will open the URL (and exit - URL mode). Default: _Control+Shift+u_. + URL mode). Default: _Control+Shift+o_. *show-urls-persistent* Similar to *show-urls-launch*, but does not automatically exit URL @@ -877,7 +877,7 @@ e.g. *search-start=none*. fallback. The preferred way of entering Unicode characters, emojis etc is by using an IME. - Default: _none_. + Default: _Control+Shift+u_. # SECTION: search-bindings diff --git a/foot.ini b/foot.ini index 94d82f6f..2735d370 100644 --- a/foot.ini +++ b/foot.ini @@ -152,12 +152,12 @@ # pipe-visible=[sh -c "xurls | fuzzel | xargs -r firefox"] none # pipe-scrollback=[sh -c "xurls | fuzzel | xargs -r firefox"] none # pipe-selected=[xargs -r firefox] none -# show-urls-launch=Control+Shift+u +# show-urls-launch=Control+Shift+o # show-urls-copy=none # show-urls-persistent=none # prompt-prev=Control+Shift+z # prompt-next=Control+Shift+x -# unicode-input=none +# unicode-input=Control+Shift+u # noop=none [search-bindings] From 19e37b17aa9a636a457f952e292612fe4ed315f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 10 Jul 2023 12:36:18 +0200 Subject: [PATCH 0380/1323] readme: a few more places mentioning the default URL mode shortcut --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 42be5792..5fb7ff4c 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ These are the default shortcuts. See `man foot.ini` and the example sequence](https://codeberg.org/dnkl/foot/wiki#user-content-spawning-new-terminal-instances-in-the-current-working-directory), the new terminal will start in the current working directory. -ctrl+shift+u +ctrl+shift+o : Enter URL mode, where all currently visible URLs are tagged with a jump label with a key sequence that will open the URL. @@ -299,7 +299,7 @@ Foot supports URL detection. But, unlike many other terminal emulators, where URLs are highlighted when they are hovered and opened by clicking on them, foot uses a keyboard driven approach. -Pressing ctrl+shift+u enters _“URL +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. From 87d45c2a01268bddd42d4a43009413af8a391f1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 10 Jul 2023 12:36:41 +0200 Subject: [PATCH 0381/1323] readme: add default shortcut for unicode input --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 5fb7ff4c..b1cfb37d 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,9 @@ These are the default shortcuts. See `man foot.ini` and the example : Enter URL mode, where all currently visible URLs are tagged with a jump label with a key sequence that will open the URL. +ctrl+shift+u +: Enter Unicode input mode. + ctrl+shift+z : Jump to the previous, currently not visible, prompt. Requires [shell integration](https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts). From 5b74808ed0d60241417a040e4b82514cbfebeb2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 10 Jul 2023 12:36:55 +0200 Subject: [PATCH 0382/1323] doc: foot: update default key binding for URL mode --- doc/foot.1.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 6143275c..62ca0374 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -202,7 +202,7 @@ default) available; see *foot.ini*(5). _OSC 7_ escape sequence, the new terminal will start in the current working directory. -*ctrl*+*shift*+*u* +*ctrl*+*shift*+*o* Activate URL mode, allowing you to "launch" URLs. *ctrl*+*shift*+*z* From 0e1dbbbd06caf9bcf978e9ca2cca3359e77b54df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 10 Jul 2023 12:37:10 +0200 Subject: [PATCH 0383/1323] doc: foot: add default key binding for unicode input --- doc/foot.1.scd | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 62ca0374..770c7f32 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -205,6 +205,9 @@ default) available; see *foot.ini*(5). *ctrl*+*shift*+*o* Activate URL mode, allowing you to "launch" URLs. +*ctrl*+*shift*+*u* + Activate Unicode input. + *ctrl*+*shift*+*z* Jump to the previous, currently not visible, prompt. Requires shell integration. From 58898c06339bd1374e975366515d437f22914bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 10 Jul 2023 12:42:10 +0200 Subject: [PATCH 0384/1323] changelog: split up key binding changes for show-urls-launch and unicode-input --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f02cf039..0a36f9c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,9 +64,10 @@ ### Changed * Minimum required meson version is now 0.59 ([#1371][1371]). -* `Control+Shift+u` now bound to `unicode-input` to follow the - convention established in GTK and Qt, `show-urls-launch` now bound - to `Control+Shift+o` ([#1183][1183]) +* `Control+Shift+u` is now bound to `unicode-input` instead of + `show-urls-launch`, to follow the convention established in GTK and + Qt ([#1183][1183]). +* `show-urls-launch` now bound to `Control+Shift+o` ([#1183][1183]). * Kitty keyboard protocol: F3 is now encoded as `CSI 13~` instead of `CSI R`. The kitty keyboard protocol originally allowed F3 to be encoded as `CSI R`, but this was removed from the specification From 3609017c383a74bdb142c5e667fdaa35d11e6da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 10 Jul 2023 12:42:36 +0200 Subject: [PATCH 0385/1323] =?UTF-8?q?changelog:=20mention=20the=20new=20de?= =?UTF-8?q?fault=20key=20binding=20for=20show-urls-launch=20under=20?= =?UTF-8?q?=E2=80=9Cfixed=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It “fixes” the key binding conflict seen on e.g. GNOME, and increases the exposure of the change to, hopefully, more users. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a36f9c7..968dd1a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -126,6 +126,9 @@ 0` ([#1377][1377]). * Crash when scrolling up while running something that generates a lot of output (for example, `yes`) ([#1380][1380]). +* Default key binding for URL mode conflicting with Unicode input on + some DEs; `show-urls-launched` is now mapped to `Control+Shift+o` by + default, instead of `Control+Shift+u` ([#1183][1183]). [1317]: https://codeberg.org/dnkl/foot/issues/1317 [1355]: https://codeberg.org/dnkl/foot/issues/1355 From dbee099eebc46bd6a72f4be1b65c5552f02d471a Mon Sep 17 00:00:00 2001 From: CismonX Date: Tue, 11 Jul 2023 00:51:32 +0800 Subject: [PATCH 0386/1323] sixel: fix regression for DECGRI with a repeat count of 0 --- sixel.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/sixel.c b/sixel.c index c7c04b0d..bd2ebe1d 100644 --- a/sixel.c +++ b/sixel.c @@ -1703,7 +1703,11 @@ decgri_generic(struct terminal *term, uint8_t c) } case '?' ... '~': { - const unsigned count = term->sixel.repeat_count; + unsigned count = term->sixel.repeat_count; + if (unlikely(count == 0)) { + count = 1; + } + sixel_add_many_generic(term, c - 63, count); term->sixel.state = SIXEL_DECSIXEL; break; @@ -1722,7 +1726,11 @@ static void decgri_ar_11(struct terminal *term, uint8_t c) { if (likely(c >= '?' && c <= '~')) { - const unsigned count = term->sixel.repeat_count; + unsigned count = term->sixel.repeat_count; + if (unlikely(count == 0)) { + count = 1; + } + sixel_add_many_ar_11(term, c - 63, count); term->sixel.state = SIXEL_DECSIXEL; } else From efc89a7317fa88d949509cb5542fd6b8c8f0acc0 Mon Sep 17 00:00:00 2001 From: Kyle Gunger Date: Thu, 27 Apr 2023 15:57:57 +0000 Subject: [PATCH 0387/1323] Aero root theme --- themes/aeroroot | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 themes/aeroroot diff --git a/themes/aeroroot b/themes/aeroroot new file mode 100644 index 00000000..77ce7443 --- /dev/null +++ b/themes/aeroroot @@ -0,0 +1,35 @@ +# Aero root theme + +[cursor] +color=1a1a1a 9fd5f5 + +[colors] +foreground=dedeef +background=1a1a1a + +regular0=1a1a1a +regular1=ff3a3a +regular2=3aef3a +regular3=e6e61a +regular4=1a7eff +regular5=df3adf +regular6=3ff0e0 +regular7=dadada + +bright0=5a5a5a +bright1=ffaaaa +bright2=aaf3aa +bright3=f3f35a +bright4=6abaff +bright5=e5aae5 +bright6=aafff0 +bright7=f3f3f3 + +dim0=000000 +dim1=b71a1a +dim2=1ab71a +dim3=b5b50a +dim4=0A4FAA +dim5=a71aa7 +dim6=1AA59F +dim7=a5a5a5 From 50f47dcba9106a89695ce57b021e39a9c24a9e3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 14 Jul 2023 09:10:35 +0200 Subject: [PATCH 0388/1323] themes: aeroroot: disable cursor colors by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Default cursor color is "inversed fg/bg", and themes aren’t supposed to change that. --- themes/aeroroot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/themes/aeroroot b/themes/aeroroot index 77ce7443..204ed46b 100644 --- a/themes/aeroroot +++ b/themes/aeroroot @@ -1,7 +1,7 @@ # Aero root theme [cursor] -color=1a1a1a 9fd5f5 +# color=1a1a1a 9fd5f5 [colors] foreground=dedeef From 98dfeb05abec3513cacf9701750066793dd964f5 Mon Sep 17 00:00:00 2001 From: ShugarSkull Date: Thu, 13 Apr 2023 17:39:45 +0200 Subject: [PATCH 0389/1323] ayu-mirage theme added --- themes/ayu-mirage | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 themes/ayu-mirage diff --git a/themes/ayu-mirage b/themes/ayu-mirage new file mode 100644 index 00000000..64e85a4e --- /dev/null +++ b/themes/ayu-mirage @@ -0,0 +1,28 @@ +# -*- conf -*- +# theme: Ayu Mirage +# description: a theme based on Ayu Mirage for Sublime Text (original: https://github.com/dempfi/ayu) + +[cursor] +color = ffcc66 665a44 + +[colors] +foreground = cccac2 +background = 242936 + +regular0 = 242936 # black +regular1 = f28779 # red +regular2 = d5ff80 # green +regular3 = ffd173 # yellow +regular4 = 73d0ff # blue +regular5 = dfbfff # magenta +regular6 = 5ccfe6 # cyan +regular7 = cccac2 # white + +bright0 = fcfcfc # bright black +bright1 = f07171 # bright red +bright2 = 86b300 # bright gree +bright3 = f2ae49 # bright yellow +bright4 = 399ee6 # bright blue +bright5 = a37acc # bright magenta +bright6 = 55b4d4 # bright cyan +bright7 = 5c6166 # bright white From 66df6fb2f6af5c1ae5e626bcb33adc4ef18b415e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 14 Jul 2023 09:55:32 +0200 Subject: [PATCH 0390/1323] themes: ayu-mirag: disable cursor colors by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Default cursor color is "inversed fg/bg", and themes aren’t supposed to change that. --- themes/ayu-mirage | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/themes/ayu-mirage b/themes/ayu-mirage index 64e85a4e..89877ce9 100644 --- a/themes/ayu-mirage +++ b/themes/ayu-mirage @@ -3,7 +3,7 @@ # description: a theme based on Ayu Mirage for Sublime Text (original: https://github.com/dempfi/ayu) [cursor] -color = ffcc66 665a44 +# color = ffcc66 665a44 [colors] foreground = cccac2 From b3745b31c72042adf523b54be7c2a297a82cca7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 14 Jul 2023 09:51:06 +0200 Subject: [PATCH 0391/1323] =?UTF-8?q?render:=20don=E2=80=99t=20invert=20cu?= =?UTF-8?q?rsor=20colors=20when=20custom=20colors=20are=20being=20used?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the user has configured custom cursor colors (cursor.color is set in foot.ini), don’t invert those colors when the cell is either selected, or has the ‘reverse’ attribute set. This aligns foot’s behavior with Alacritty, Kitty and Wezterm. Contour also behaves similarly, except mouse selections override the cursor colors (turning the cursor invisible). Closes #1347 --- CHANGELOG.md | 5 +++++ render.c | 14 +++----------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 968dd1a3..cbae76c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,11 +94,16 @@ ([#1383][1383]). * `dpi-aware` now defaults to `no`, and the `auto` value has been removed. +* When using custom cursor colors (`cursor.color` is set in + `foot.ini`), the cursor is no longer inverted when the cell is + selected, or when the cell has the `reverse` (SGR 7) attribute set + ([#1347][1347]). [1371]: https://codeberg.org/dnkl/foot/pulls/1371 [1183]: https://codeberg.org/dnkl/foot/issues/1183 [1360]: https://codeberg.org/dnkl/foot/issues/1360 [1383]: https://codeberg.org/dnkl/foot/issues/1383 +[1347]: https://codeberg.org/dnkl/foot/issues/1347 ### Deprecated diff --git a/render.c b/render.c index 5a757a88..677856d8 100644 --- a/render.c +++ b/render.c @@ -405,19 +405,11 @@ cursor_colors_for_cell(const struct terminal *term, const struct cell *cell, const pixman_color_t *fg, const pixman_color_t *bg, pixman_color_t *cursor_color, pixman_color_t *text_color) { - bool is_selected = cell->attrs.selected; - if (term->cursor_color.cursor >> 31) { - *cursor_color = color_hex_to_pixman(term->cursor_color.cursor); - *text_color = color_hex_to_pixman( - term->cursor_color.text >> 31 - ? term->cursor_color.text : term->colors.bg); + xassert(term->cursor_color.text >> 31); - if (cell->attrs.reverse ^ is_selected) { - pixman_color_t swap = *cursor_color; - *cursor_color = *text_color; - *text_color = swap; - } + *cursor_color = color_hex_to_pixman(term->cursor_color.cursor); + *text_color = color_hex_to_pixman(term->cursor_color.text); } else { *cursor_color = *fg; *text_color = *bg; From 28ab41caad46fa8a70e8c65f5eed577ac5a4b5a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 19 Apr 2023 09:35:12 +0200 Subject: [PATCH 0392/1323] =?UTF-8?q?theme:=20add=20new=20theme=20?= =?UTF-8?q?=E2=80=98starlight=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1321 --- themes/starlight | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 themes/starlight diff --git a/themes/starlight b/themes/starlight new file mode 100644 index 00000000..cb850b45 --- /dev/null +++ b/themes/starlight @@ -0,0 +1,23 @@ +# Theme: starlight (https://github.com/CosmicToast/starlight) + +[colors] +foreground = FFFFFF +background = 242424 + +regular0 = 242424 +regular1 = CF1745 +regular2 = 3ECF5B +regular3 = CFCF17 +regular4 = 0BA6DA +regular5 = D926AC +regular6 = 17CFA1 +regular7 = E6E6E6 + +bright0 = 616161 +bright1 = FF1A53 +bright2 = 17E640 +bright3 = ECFF1A +bright4 = 1AC6FF +bright5 = F53DC7 +bright6 = 1AFFC6 +bright7 = FFFFFF From 235e0e9e60a47c9b3b4019ea6dceb5512ecb240f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 14 Jul 2023 10:03:22 +0200 Subject: [PATCH 0393/1323] themes: starlight: add -*- conf -*- header --- themes/starlight | 1 + 1 file changed, 1 insertion(+) diff --git a/themes/starlight b/themes/starlight index cb850b45..9b30b399 100644 --- a/themes/starlight +++ b/themes/starlight @@ -1,3 +1,4 @@ +# -*- conf -*- # Theme: starlight (https://github.com/CosmicToast/starlight) [colors] From f53e7f7478b5011db67e98944a3582c4ed523417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 14 Jul 2023 10:03:56 +0200 Subject: [PATCH 0394/1323] themes: aeroroot: add -*- conf -*- header --- themes/aeroroot | 1 + 1 file changed, 1 insertion(+) diff --git a/themes/aeroroot b/themes/aeroroot index 204ed46b..7e65f909 100644 --- a/themes/aeroroot +++ b/themes/aeroroot @@ -1,3 +1,4 @@ +# -*- conf -*- # Aero root theme [cursor] From efc619b0afc856c976ea8e4d255ab9e014089510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 14 Jul 2023 10:11:30 +0200 Subject: [PATCH 0395/1323] =?UTF-8?q?config:=20make=20=E2=80=98starlight?= =?UTF-8?q?=E2=80=99=20the=20default=20color=20theme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1321 --- CHANGELOG.md | 4 ++++ config.c | 34 +++++++++++++++++----------------- doc/foot.ini.5.scd | 12 ++++++------ 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbae76c5..bc18908a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,9 @@ ### Changed +* Default color theme is now + [starlight](https://github.com/CosmicToast/starlight) + ([#1321][1321]). * Minimum required meson version is now 0.59 ([#1371][1371]). * `Control+Shift+u` is now bound to `unicode-input` instead of `show-urls-launch`, to follow the convention established in GTK and @@ -99,6 +102,7 @@ selected, or when the cell has the `reverse` (SGR 7) attribute set ([#1347][1347]). +[1321]: https://codeberg.org/dnkl/foot/issues/1321 [1371]: https://codeberg.org/dnkl/foot/pulls/1371 [1183]: https://codeberg.org/dnkl/foot/issues/1183 [1360]: https://codeberg.org/dnkl/foot/issues/1360 diff --git a/config.c b/config.c index 1cefba3e..5c38aef5 100644 --- a/config.c +++ b/config.c @@ -30,8 +30,8 @@ #include "xmalloc.h" #include "xsnprintf.h" -static const uint32_t default_foreground = 0x839496; -static const uint32_t default_background = 0x002b36; +static const uint32_t default_foreground = 0xffffff; +static const uint32_t default_background = 0x242424; static const size_t min_csd_border_width = 5; @@ -48,23 +48,23 @@ static const size_t min_csd_border_width = 5; static const uint32_t default_color_table[256] = { // Regular - 0x073642, - 0xdc322f, - 0x859900, - 0xb58900, - 0x268bd2, - 0xd33682, - 0x2aa198, - 0xeee8d5, + 0x242424, + 0xcf1745, + 0x3ecf5b, + 0xcfcf17, + 0x0ba6da, + 0xd926ac, + 0x17cfa1, + 0xe6e6e6, // Bright - 0x08404f, - 0xe35f5c, - 0x9fb700, - 0xd9a400, - 0x4ba1de, - 0xdc619d, - 0x32c1b6, + 0x616161, + 0xff1a53, + 0x17e640, + 0xecff1a, + 0x1ac6ff, + 0xf53dc7, + 0x1affc6, 0xffffff, // 6x6x6 RGB cube diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 273a74c2..32482aa9 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -563,15 +563,15 @@ can configure the background transparency with the _alpha_ option. *regular0*, *regular1* *..* *regular7* The eight basic ANSI colors (Black, Red, Green, Yellow, Blue, - Magenta, Cyan, White). Default: _073642_, _dc322f_, _859900_, - _b58900_, _268bd2_, _d33682_, _2aa198_ and _eee8d5_ (a variant of - the _solarized dark_ theme). + Magenta, Cyan, White). Default: _242424_, _cf1745_, _3ecf5b_, + _cfcf17_, _0ba6da_, _d926ac_, _17cfa1_, _e6e6e6_ (starlight + theme). *bright0*, *bright1* *..* *bright7* The eight bright ANSI colors (Black, Red, Green, Yellow, Blue, - Magenta, Cyan, White). Default: _08404f_, _e35f5c_, _9fb700_, - _d9a400_, _4ba1de_, _dc619d_, _32c1b6_ and _ffffff_ (a variant of - the _solarized dark_ theme). + Magenta, Cyan, White). Default: _616161_, _ff1a53_, _17e640_, + _ecff1a_, _1ac6ff_, _f53dc7_, _1affc6_, _ffffff_ (starlight + theme). *dim0*, *dim1* *..* *dim7* Custom colors to use with dimmed colors. Dimmed colors do not have From 3cd0e2adb0bbdb2e59bdf4f4818f23080c36890e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 14 Jul 2023 10:20:20 +0200 Subject: [PATCH 0396/1323] themes: enable custom cursor colors in all themes that define such colors Not all themes have/define custom cursor colors. But of those that do, nearly all already enabled them (by setting "cursor.color"), except three themes: * aeroroot * ayu-mirage * material-amber This patch makes all themes consistent, by enabling cursor.color in these last three themes too. --- themes/aeroroot | 2 +- themes/ayu-mirage | 2 +- themes/material-amber | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/themes/aeroroot b/themes/aeroroot index 7e65f909..3b887448 100644 --- a/themes/aeroroot +++ b/themes/aeroroot @@ -2,7 +2,7 @@ # Aero root theme [cursor] -# color=1a1a1a 9fd5f5 +color=1a1a1a 9fd5f5 [colors] foreground=dedeef diff --git a/themes/ayu-mirage b/themes/ayu-mirage index 89877ce9..64e85a4e 100644 --- a/themes/ayu-mirage +++ b/themes/ayu-mirage @@ -3,7 +3,7 @@ # description: a theme based on Ayu Mirage for Sublime Text (original: https://github.com/dempfi/ayu) [cursor] -# color = ffcc66 665a44 +color = ffcc66 665a44 [colors] foreground = cccac2 diff --git a/themes/material-amber b/themes/material-amber index ee2c21b5..ad844a9a 100644 --- a/themes/material-amber +++ b/themes/material-amber @@ -2,8 +2,8 @@ # Material Amber # Based on material.io guidelines with Amber 50 background -# [cursor] -# color=fff8e1 21201d +[cursor] +color=fff8e1 21201d [colors] foreground = 21201d From 3f7be59062f00cd99d832eaa5d389b880a3c512e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 14 Jul 2023 12:03:35 +0200 Subject: [PATCH 0397/1323] config: add csd.double-click-to-maximize=no|yes option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When enabled, double-clicking the CSD titlebar will (un)maximize the window. Defaults to ‘yes’ (since this is the old hard-coded behavior). Closes #1293 --- CHANGELOG.md | 3 +++ config.c | 4 ++++ config.h | 1 + doc/foot.ini.5.scd | 4 ++++ foot.ini | 1 + input.c | 5 ++++- tests/test-config.c | 2 ++ 7 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc18908a..891c973a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,9 +56,12 @@ * Support for the new `cursor-shape-v1` Wayland protocol, i.e. server side cursor shapes ([#1379][1379]). * Support for touchscreen input ([#517][517]). +* `csd.double-click-to-maximize` option to `foot.ini`. Defaults to + `yes` ([#1293][1293]). [1379]: https://codeberg.org/dnkl/foot/issues/1379 [517]: https://codeberg.org/dnkl/foot/issues/517 +[1293]: https://codeberg.org/dnkl/foot/issues/1293 ### Changed diff --git a/config.c b/config.c index 5c38aef5..735ccd74 100644 --- a/config.c +++ b/config.c @@ -1475,6 +1475,9 @@ parse_section_csd(struct context *ctx) else if (strcmp(key, "hide-when-maximized") == 0) return value_to_bool(ctx, &conf->csd.hide_when_maximized); + else if (strcmp(key, "double-click-to-maximize") == 0) + return value_to_bool(ctx, &conf->csd.double_click_to_maximize); + else { LOG_CONTEXTUAL_ERR("not a valid action: %s", key); return false; @@ -3009,6 +3012,7 @@ config_load(struct config *conf, const char *conf_path, .preferred = CONF_CSD_PREFER_SERVER, .font = {0}, .hide_when_maximized = false, + .double_click_to_maximize = true, .title_height = 26, .border_width = 5, .border_width_visible = 0, diff --git a/config.h b/config.h index 20c07f6c..8189e56d 100644 --- a/config.h +++ b/config.h @@ -285,6 +285,7 @@ struct config { uint16_t button_width; bool hide_when_maximized; + bool double_click_to_maximize; struct { bool title_set:1; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 32482aa9..7fee8387 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -692,6 +692,10 @@ Examples: is maximized. The completely disable the titlebar, set *size* to 0 instead. Default: _no_. +*double-click-to-maximize* + Boolean. When enabled, double-clicking the CSD titlebar will + (un)maximize the window. Default: _yes_. + *border-width* Width of the border, in pixels (subject to output scaling). Note that the border encompasses the entire window, including the title diff --git a/foot.ini b/foot.ini index 2735d370..b4e4c603 100644 --- a/foot.ini +++ b/foot.ini @@ -123,6 +123,7 @@ # font= # color= # hide-when-maximized=no +# double-click-to-maximize=yes # border-width=0 # border-color= # button-width=26 diff --git a/input.c b/input.c index 3bf6535a..b2b4adf6 100644 --- a/input.c +++ b/input.c @@ -2287,7 +2287,10 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, struct wl_window *win = term->window; /* Toggle maximized state on double-click */ - if (button == BTN_LEFT && seat->mouse.count == 2) { + if (term->conf->csd.double_click_to_maximize && + button == BTN_LEFT && + seat->mouse.count == 2) + { if (win->is_maximized) xdg_toplevel_unset_maximized(win->xdg_toplevel); else diff --git a/tests/test-config.c b/tests/test-config.c index e59c104e..54efd13a 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -777,6 +777,8 @@ test_section_csd(void) &conf.csd.color.quit); test_boolean(&ctx, &parse_section_csd, "hide-when-maximized", &conf.csd.hide_when_maximized); + test_boolean(&ctx, &parse_section_csd, "double-click-to-maximize", + &conf.csd.double_click_to_maximize); /* TODO: verify the ‘set’ bit is actually set for colors */ /* TODO: font */ From 53b0eb8e1b50341d15fcb0957c8dca9af90e6473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 14 Jul 2023 12:25:16 +0200 Subject: [PATCH 0398/1323] changelog: prepare for 1.15.0 --- CHANGELOG.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 891c973a..206b3d52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.15.0](#1-15-0) * [1.14.0](#1-14-0) * [1.13.1](#1-13-1) * [1.13.0](#1-13-0) @@ -42,7 +42,8 @@ * [1.2.0](#1-2-0) -## Unreleased +## 1.15.0 + ### Added * VT: implemented `XTQMODKEYS` query (`CSI ? Pp m`). @@ -148,9 +149,23 @@ [1380]: https://codeberg.org/dnkl/foot/issues/1380 -### Security ### Contributors +* Antoine Beaupré +* CismonX +* Craig Barnes +* Dan Bungert +* jdevdevdev +* Kyle Gunger +* locture +* Phillip Susi +* sewn +* ShugarSkull +* Vivian Szczepanski +* Vladimir Bauer +* wout +* CosmicToast + ## 1.14.0 From 5a3706ac464049baf068d0991d62781b438fad7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 14 Jul 2023 12:26:03 +0200 Subject: [PATCH 0399/1323] meson: bump version to 1.15.0 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index fc2491d2..1a00153c 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.14.0', + version: '1.15.0', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From d1df98e0cac70fb61d68acfe2412f10102e80441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 14 Jul 2023 12:40:55 +0200 Subject: [PATCH 0400/1323] =?UTF-8?q?changelog:=20add=20new=20=E2=80=98unr?= =?UTF-8?q?eleased=E2=80=99=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 206b3d52..b5d21bd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.15.0](#1-15-0) * [1.14.0](#1-14-0) * [1.13.1](#1-13-1) @@ -42,6 +43,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.15.0 ### Added From b7100d57160d2ae7a38d5517f1c2cd7155737d88 Mon Sep 17 00:00:00 2001 From: Ronan Pigott Date: Fri, 14 Jul 2023 16:53:50 -0700 Subject: [PATCH 0401/1323] render: use rounding for fractional scale If we truncate the buffer dimensions we may accidentally submit a buffer with inappropriate size. --- CHANGELOG.md | 3 +++ render.c | 4 ++-- terminal.c | 5 ++++- wayland.c | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5d21bd6..5a1eaaa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,9 @@ ### Deprecated ### Removed ### Fixed + +* Use appropriate rounding when applying fractional scales. + ### Security ### Contributors diff --git a/render.c b/render.c index 677856d8..d084a859 100644 --- a/render.c +++ b/render.c @@ -3856,8 +3856,8 @@ maybe_resize(struct terminal *term, int width, int height, bool force) scale = term->scale; } - width *= scale; - height *= scale; + width = round(width * scale); + height = round(height * scale); if (width == 0 && height == 0) { /* diff --git a/terminal.c b/terminal.c index fff55019..485e8ca3 100644 --- a/terminal.c +++ b/terminal.c @@ -778,7 +778,10 @@ term_set_fonts(struct terminal *term, struct fcft_font *fonts[static 4]) sixel_cell_size_changed(term); /* Use force, since cell-width/height may have changed */ - render_resize_force(term, term->width / term->scale, term->height / term->scale); + render_resize_force( + term, + round(term->width / term->scale), + round(term->height / term->scale)); return true; } diff --git a/wayland.c b/wayland.c index a25ec0f6..9195797e 100644 --- a/wayland.c +++ b/wayland.c @@ -401,7 +401,9 @@ update_term_for_output_change(struct terminal *term) float old_scale = term->scale; - render_resize(term, term->width / term->scale, term->height / term->scale); + render_resize(term, + round(term->width / term->scale), + round(term->height / term->scale)); term_font_dpi_changed(term, old_scale); term_font_subpixel_changed(term); csd_reload_font(term->window, old_scale); From 8b4cb2457aa5b90d265f59207b4b592ba6adebbc Mon Sep 17 00:00:00 2001 From: CismonX Date: Sun, 16 Jul 2023 17:24:55 +0800 Subject: [PATCH 0402/1323] input: do not ignore touch events on the CSDs --- input.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/input.c b/input.c index b2b4adf6..53d7acf8 100644 --- a/input.c +++ b/input.c @@ -2739,8 +2739,6 @@ wl_touch_down(void *data, struct wl_touch *wl_touch, uint32_t serial, struct terminal *term = win->term; term->active_surface = term_surface_kind(term, surface); - if (term->active_surface != TERM_SURF_GRID) - return; LOG_DBG("touch_down: touch=%p, x=%d, y=%d", (void *)wl_touch, wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y)); @@ -2785,6 +2783,7 @@ wl_touch_up(void *data, struct wl_touch *wl_touch, uint32_t serial, WL_POINTER_BUTTON_STATE_RELEASED); /* fallthrough */ case TOUCH_STATE_SCROLLING: + term->active_surface = TERM_SURF_NONE; seat->touch.state = TOUCH_STATE_IDLE; break; @@ -2815,7 +2814,7 @@ wl_touch_motion(void *data, struct wl_touch *wl_touch, uint32_t time, switch (seat->touch.state) { case TOUCH_STATE_HELD: - if (time <= seat->touch.time) { + if (time <= seat->touch.time && term->active_surface == TERM_SURF_GRID) { if (touch_to_scroll(seat, term, surface_x, surface_y)) seat->touch.state = TOUCH_STATE_SCROLLING; break; From 6de69aa9b726c2e270455ac44a7b8d5177b7332e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 17 Jul 2023 20:08:34 +0200 Subject: [PATCH 0403/1323] render: fix xcursor scaling with fractional-scale-v1 This worked just after the fractional-scaling branch was merged, but was then broken by the cursor-shape branch, due to a bad rebase of that branch. --- CHANGELOG.md | 3 +++ render.c | 12 +++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a1eaaa5..812e483a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,9 @@ ### Fixed * Use appropriate rounding when applying fractional scales. +* Xcursor not being scaled correctly on `fractional-scale-v1` capable + compositors. + ### Security ### Contributors diff --git a/render.c b/render.c index d084a859..c40d07d0 100644 --- a/render.c +++ b/render.c @@ -4297,11 +4297,15 @@ render_xcursor_update(struct seat *seat) LOG_DBG("setting %scursor shape using a client-side cursor surface", shape == CURSOR_SHAPE_CUSTOM ? "custom " : ""); - const int scale = seat->pointer.scale; + const float scale = seat->pointer.scale; struct wl_cursor_image *image = seat->pointer.cursor->images[0]; + struct wl_buffer *buf = wl_cursor_image_get_buffer(image); - wl_surface_attach( - seat->pointer.surface.surf, wl_cursor_image_get_buffer(image), 0, 0); + wayl_surface_scale_explicit_width_height( + seat->mouse_focus->window, + &seat->pointer.surface, image->width, image->height, scale); + + wl_surface_attach(seat->pointer.surface.surf, buf, 0, 0); wl_pointer_set_cursor( seat->wl_pointer, seat->pointer.serial, @@ -4311,8 +4315,6 @@ render_xcursor_update(struct seat *seat) wl_surface_damage_buffer( seat->pointer.surface.surf, 0, 0, INT32_MAX, INT32_MAX); - wl_surface_set_buffer_scale(seat->pointer.surface.surf, scale); - xassert(seat->pointer.xcursor_callback == NULL); seat->pointer.xcursor_callback = wl_surface_frame(seat->pointer.surface.surf); wl_callback_add_listener(seat->pointer.xcursor_callback, &xcursor_listener, seat); From da81b63ec0cd51586f896f6e5f55c716cce4d9af Mon Sep 17 00:00:00 2001 From: Ayush Agarwal Date: Sat, 8 Apr 2023 03:11:10 +0530 Subject: [PATCH 0404/1323] themes: add chiba-dark theme --- themes/chiba-dark | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 themes/chiba-dark diff --git a/themes/chiba-dark b/themes/chiba-dark new file mode 100644 index 00000000..bc3b1420 --- /dev/null +++ b/themes/chiba-dark @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Chiba Dark +# author: ayushnix (https://sr.ht/~ayushnix) +# description: A dark theme with bright cyberpunk colors (WCAG AAA compliant) + +[cursor] +color = 181818 cdcdcd + +[colors] +foreground = cdcdcd +background = 181818 +regular0 = 181818 +regular1 = ff8599 +regular2 = 00c545 +regular3 = de9d00 +regular4 = 00b4ff +regular5 = fd71f8 +regular6 = 00bfae +regular7 = cdcdcd +bright0 = 262626 +bright1 = ff9eb2 +bright2 = 19de5e +bright3 = f7b619 +bright4 = 19cdff +bright5 = ff8aff +bright6 = 19d8c7 +bright7 = dadada From 2fd29cbf500a5528445f82c59ac76b1762efa60d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 16 Jul 2023 08:27:12 +0200 Subject: [PATCH 0405/1323] term: (debug): dpi_aware is no longer an enum --- terminal.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/terminal.c b/terminal.c index 485e8ca3..213d8c59 100644 --- a/terminal.c +++ b/terminal.c @@ -2099,8 +2099,7 @@ term_font_dpi_changed(struct terminal *term, int old_scale) LOG_DBG("DPI/scale change: DPI-awareness=%s, " "DPI: %.2f -> %.2f, scale: %d -> %d, " "sizing font based on monitor's %s", - term->conf->dpi_aware == DPI_AWARE_AUTO ? "auto" : - term->conf->dpi_aware == DPI_AWARE_YES ? "yes" : "no", + term->conf->dpi_aware ? "yes" : "no", term->font_dpi, dpi, old_scale, term->scale, will_scale_using_dpi ? "DPI" : "scaling factor"); } From 829353a5dad410ce9b6b1ca78f9d86beedc3a90e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 16 Jul 2023 08:28:21 +0200 Subject: [PATCH 0406/1323] term: font_dpi_changed: scale (and old_scale) are floating point MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This should fix an issue where the font size wasn’t updated when moving the window between outputs whose scaling factors match when truncated. --- terminal.c | 8 ++++---- terminal.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/terminal.c b/terminal.c index 213d8c59..e15e647e 100644 --- a/terminal.c +++ b/terminal.c @@ -1298,7 +1298,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, goto err; /* Load fonts */ - if (!term_font_dpi_changed(term, 0)) + if (!term_font_dpi_changed(term, 0.)) goto err; term->font_subpixel = get_font_subpixel(term); @@ -2081,10 +2081,10 @@ term_font_size_reset(struct terminal *term) } bool -term_font_dpi_changed(struct terminal *term, int old_scale) +term_font_dpi_changed(struct terminal *term, float old_scale) { float dpi = get_font_dpi(term); - xassert(term->scale > 0); + xassert(term->scale > 0.); bool was_scaled_using_dpi = term->font_is_sized_by_dpi; bool will_scale_using_dpi = term->conf->dpi_aware; @@ -2097,7 +2097,7 @@ term_font_dpi_changed(struct terminal *term, int old_scale) if (need_font_reload) { LOG_DBG("DPI/scale change: DPI-awareness=%s, " - "DPI: %.2f -> %.2f, scale: %d -> %d, " + "DPI: %.2f -> %.2f, scale: %.2f -> %.2f, " "sizing font based on monitor's %s", term->conf->dpi_aware ? "yes" : "no", term->font_dpi, dpi, old_scale, term->scale, diff --git a/terminal.h b/terminal.h index 6dace7ac..60d3c3ac 100644 --- a/terminal.h +++ b/terminal.h @@ -739,7 +739,7 @@ bool term_paste_data_to_slave( bool term_font_size_increase(struct terminal *term); bool term_font_size_decrease(struct terminal *term); bool term_font_size_reset(struct terminal *term); -bool term_font_dpi_changed(struct terminal *term, int old_scale); +bool term_font_dpi_changed(struct terminal *term, float old_scale); void term_font_subpixel_changed(struct terminal *term); int term_pt_or_px_as_pixels( From 59f0a721c4663aafd30179e34a38165dd137a0db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 17 Jul 2023 16:12:34 +0200 Subject: [PATCH 0407/1323] wayland: fractional_scale_preferred_scale(): only push update if scale has changed Also, drop wl_window::have_preferred_scale. Check for scale > 0 instead. --- wayland.c | 13 +++++++++---- wayland.h | 1 - 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/wayland.c b/wayland.c index 9195797e..f6ded585 100644 --- a/wayland.c +++ b/wayland.c @@ -1603,10 +1603,15 @@ static void fractional_scale_preferred_scale( uint32_t scale) { struct wl_window *win = data; - win->scale = (float)scale / 120.; - win->have_preferred_scale = true; - LOG_DBG("fractional scale: %.3f", win->scale); + const float new_scale = (float)scale / 120.; + + if (win->scale == new_scale) + return; + + LOG_DBG("fractional scale: %.2f -> %.2f", win->scale, new_scale); + + win->scale = new_scale; update_term_for_output_change(win->term); } @@ -1971,7 +1976,7 @@ wayl_surface_scale_explicit_width_height( int width, int height, float scale) { - if (wayl_fractional_scaling(win->term->wl) && win->have_preferred_scale) { + if (wayl_fractional_scaling(win->term->wl) && win->scale > 0.) { #if defined(HAVE_FRACTIONAL_SCALE) LOG_DBG("scaling by a factor of %.2f using fractional scaling " "(width=%d, height=%d) ", scale, width, height); diff --git a/wayland.h b/wayland.h index 6d1cd727..9e581b20 100644 --- a/wayland.h +++ b/wayland.h @@ -374,7 +374,6 @@ struct wl_window { bool unmapped; float scale; - bool have_preferred_scale; struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration; From b2a29280cbd3fcd825123e7636d511a06b2df242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 17 Jul 2023 16:19:14 +0200 Subject: [PATCH 0408/1323] wayland: use physical DPI on fractional-scale capable compositors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With legacy scaling, we need to use a "scaled", or "logical" DPI value, that is basically the real DPI value scaled by the monitor’s scaling factor. This is necessary to compensate for the compositor downscaling the surface, for "fake" fractional scaling. But with true fractional scaling, *we* scale the surface to the final size. This means we should *not* use the scaled DPI, but the monitor’s actual DPI. To facilitate this, store both the scaled and the unscaled DPI value in the monitor struct. This patch also changes how we pick the DPI value. Before, we would use the highest DPI value from all the monitors we were mapped on. Now, we use the DPI value from the monitor we were *last* mapped on (typically the window we’re dragging the window *to*). --- terminal.c | 55 +++++++++++++++++++++++------------------------------- wayland.c | 46 +++++++++++++++++++++++++++------------------ wayland.h | 5 ++++- 3 files changed, 55 insertions(+), 51 deletions(-) diff --git a/terminal.c b/terminal.c index e15e647e..a557d632 100644 --- a/terminal.c +++ b/terminal.c @@ -796,41 +796,34 @@ get_font_dpi(const struct terminal *term) * Conceptually, we use the physical monitor specs to calculate * the DPI, and we ignore the output's scaling factor. * - * However, to deal with fractional scaling, where we're told to - * render at e.g. 2x, but are then downscaled by the compositor to - * e.g. 1.25, we use the scaled DPI value multiplied by the scale - * factor instead. + * However, to deal with legacy fractional scaling, where we're + * told to render at e.g. 2x, but are then downscaled by the + * compositor to e.g. 1.25, we use the scaled DPI value multiplied + * by the scale factor instead. * * For integral scaling factors the resulting DPI is the same as * if we had used the physical DPI. * - * For fractional scaling factors we'll get a DPI *larger* than - * the physical DPI, that ends up being right when later + * For legacy fractional scaling factors we'll get a DPI *larger* + * than the physical DPI, that ends up being right when later * downscaled by the compositor. + * + * With the newer fractional-scale-v1 protocol, we use the + * monitor’s real DPI, since we scale everything to the correct + * scaling factor (no downscaling done by the compositor). */ - /* Use highest DPI from outputs we're mapped on */ - double dpi = 0.0; - xassert(term->window != NULL); - tll_foreach(term->window->on_outputs, it) { - if (it->item->dpi > dpi) - dpi = it->item->dpi; - } + xassert(tll_length(term->wl->monitors) > 0); - /* If we're not mapped, use DPI from first monitor. Hopefully this is where we'll get mapped later... */ - if (dpi == 0.) { - tll_foreach(term->wl->monitors, it) { - dpi = it->item.dpi; - break; - } - } + 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); - if (dpi == 0) { - /* No monitors? */ - dpi = 96.; - } - - return dpi; + if (wayl_fractional_scaling(term->wl)) + return mon->dpi.physical; + else + return mon->dpi.scaled; } static enum fcft_subpixel @@ -1285,11 +1278,9 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, reaper_add(term->reaper, term->slave, &fdm_client_terminated, term); /* Guess scale; we're not mapped yet, so we don't know on which - * output we'll be. Pick highest scale we find for now */ - tll_foreach(term->wl->monitors, it) { - if (it->item.scale > term->scale) - term->scale = it->item.scale; - } + * output we'll be. Use scaling factor from first monitor */ + xassert(tll_length(term->wl->monitors) > 0); + term->scale = tll_front(term->wl->monitors).scale; memcpy(term->colors.table, term->conf->colors.table, sizeof(term->colors.table)); @@ -2096,7 +2087,7 @@ term_font_dpi_changed(struct terminal *term, float old_scale) : old_scale != term->scale); if (need_font_reload) { - LOG_DBG("DPI/scale change: DPI-awareness=%s, " + LOG_DBG("DPI/scale change: DPI-aware=%s, " "DPI: %.2f -> %.2f, scale: %.2f -> %.2f, " "sizing font based on monitor's %s", term->conf->dpi_aware ? "yes" : "no", diff --git a/wayland.c b/wayland.c index f6ded585..c4f9e402 100644 --- a/wayland.c +++ b/wayland.c @@ -435,6 +435,9 @@ output_update_ppi(struct monitor *mon) double x_inches = mon->dim.mm.width * 0.03937008; double y_inches = mon->dim.mm.height * 0.03937008; + const int width = mon->dim.px_real.width; + const int height = mon->dim.px_real.height; + mon->ppi.real.x = mon->dim.px_real.width / x_inches; mon->ppi.real.y = mon->dim.px_real.height / y_inches; @@ -457,27 +460,36 @@ output_update_ppi(struct monitor *mon) break; } - int scaled_width = mon->dim.px_scaled.width; - int scaled_height = mon->dim.px_scaled.height; - - if (scaled_width == 0 && scaled_height == 0 && mon->scale > 0) { - /* Estimate scaled width/height if none has been provided */ - scaled_width = mon->dim.px_real.width / mon->scale; - scaled_height = mon->dim.px_real.height / mon->scale; - } + const int scaled_width = mon->dim.px_scaled.width; + const int scaled_height = mon->dim.px_scaled.height; mon->ppi.scaled.x = scaled_width / x_inches; mon->ppi.scaled.y = scaled_height / y_inches; - double px_diag = sqrt(pow(scaled_width, 2) + pow(scaled_height, 2)); - mon->dpi = px_diag / mon->inch * mon->scale; + const double px_diag_physical = sqrt(pow(width, 2) + pow(height, 2)); + mon->dpi.physical = width == 0 && height == 0 + ? 96. + : px_diag_physical / mon->inch; - if (mon->dpi > 1000) { + const double px_diag_scaled = sqrt(pow(scaled_width, 2) + pow(scaled_height, 2)); + mon->dpi.scaled = scaled_width == 0 && scaled_height == 0 + ? 96. + : px_diag_scaled / mon->inch * mon->scale; + + if (mon->dpi.physical > 1000) { if (mon->name != NULL) { - LOG_WARN("%s: DPI=%f is unreasonable, using 96 instead", - mon->name, mon->dpi); + LOG_WARN("%s: DPI=%f (physical) is unreasonable, using 96 instead", + mon->name, mon->dpi.physical); } - mon->dpi = 96; + mon->dpi.physical = 96; + } + + if (mon->dpi.scaled > 1000) { + if (mon->name != NULL) { + LOG_WARN("%s: DPI=%f (logical) is unreasonable, using 96 instead", + mon->name, mon->dpi.scaled); + } + mon->dpi.scaled = 96; } } @@ -1487,14 +1499,12 @@ wayl_init(struct fdm *fdm, struct key_binding_manager *key_binding_manager, tll_foreach(wayl->monitors, it) { LOG_INFO( - "%s: %dx%d+%dx%d@%dHz %s %.2f\" scale=%d PPI=%dx%d (physical) PPI=%dx%d (logical), DPI=%.2f", + "%s: %dx%d+%dx%d@%dHz %s %.2f\" scale=%d, DPI=%.2f/%.2f (physical/scaled)", it->item.name, it->item.dim.px_real.width, it->item.dim.px_real.height, it->item.x, it->item.y, (int)round(it->item.refresh), it->item.model != NULL ? it->item.model : it->item.description, it->item.inch, it->item.scale, - it->item.ppi.real.x, it->item.ppi.real.y, - it->item.ppi.scaled.x, it->item.ppi.scaled.y, - it->item.dpi); + it->item.dpi.physical, it->item.dpi.scaled); } wayl->fd = wl_display_get_fd(wayl->display); diff --git a/wayland.h b/wayland.h index 9e581b20..275338a8 100644 --- a/wayland.h +++ b/wayland.h @@ -315,7 +315,10 @@ struct monitor { } scaled; } ppi; - float dpi; + struct { + float scaled; + float physical; + } dpi; int scale; float refresh; From c96863b1882088271dd9867c4fd258a9a5fa6a79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 17 Jul 2023 16:19:21 +0200 Subject: [PATCH 0409/1323] =?UTF-8?q?wayland:=20error=20out=20if=20there?= =?UTF-8?q?=20aren=E2=80=99t=20any=20monitors=20available?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wayland.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/wayland.c b/wayland.c index c4f9e402..85140d6f 100644 --- a/wayland.c +++ b/wayland.c @@ -1447,6 +1447,11 @@ wayl_init(struct fdm *fdm, struct key_binding_manager *key_binding_manager, LOG_ERR("no seats available (wl_seat interface too old?)"); goto out; } + if (tll_length(wayl->monitors) == 0) { + LOG_ERR("no monitors available"); + goto out; + } + if (wayl->primary_selection_device_manager == NULL) LOG_WARN("no primary selection available"); From 21d99f8dced335826964ca96b8ba7ccac059e598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 17 Jul 2023 16:21:16 +0200 Subject: [PATCH 0410/1323] terminal: break out scaling factor updating, and reduce number of calls to render_resize() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Break out the logic that updates the terminal’s scaling factor value, from render_resize(), to a new function, term_update_scale(). This allows us to update the scaling factor without a full grid resize. We also change how we pick the scaling factor (when fractional scaling is not in use). Before, we’d use the highest scaling factor from all monitors we were mapped on. Now, we use the scaling factor from the monitor we were *last* mapped on. Then, add a boolean parameter to term_set_fonts(), and when false, *don’t* call render_resize_force(). Also change term_font_dpi_changed() to only return true if the font was changed in any way. Finally, rewrite update_term_for_output_change() to: * Call term_update_scale() before doing anything else * Call render_resize{,_force} *last*, and *only* if either the scale or the fonts were updated. This fixes several things: * A bug where we failed to update the fonts when fractional scaling was in use, and we guessed the initial scale/DPI wrong. The bug happened because updated the internal "preferred" scale value, and a later call to render_resize() updated the terminal’s scale value, but since that code path didn’t call term_font_dpi_changed() (and it shouldn’t), the fonts weren’t resized properly. * It ensures we only resize the grid *once* when the scaling factor, or DPI is changed. Before this, we’d resize it twice. And this happened when e.g. dragging the window between monitors. --- render.c | 29 ++++++------------------ terminal.c | 65 +++++++++++++++++++++++++++++++++++++++++------------- terminal.h | 1 + wayland.c | 34 +++++++++++++++++++++------- 4 files changed, 84 insertions(+), 45 deletions(-) diff --git a/render.c b/render.c index c40d07d0..11149b16 100644 --- a/render.c +++ b/render.c @@ -3841,21 +3841,7 @@ maybe_resize(struct terminal *term, int width, int height, bool force) if (term->cell_width == 0 && term->cell_height == 0) return false; - float scale = -1; - if (wayl_fractional_scaling(term->wl)) { - scale = term->window->scale; - } else { - tll_foreach(term->window->on_outputs, it) { - if (it->item->scale > scale) - scale = it->item->scale; - } - } - - if (scale < 0.) { - /* Haven't 'entered' an output yet? */ - scale = term->scale; - } - + const float scale = term->scale; width = round(width * scale); height = round(height * scale); @@ -3942,9 +3928,9 @@ maybe_resize(struct terminal *term, int width, int height, bool force) /* Drop out of URL mode */ urls_reset(term); + LOG_DBG("resized: size=%dx%d (scale=%.2f)", width, height, term->scale); term->width = width; term->height = height; - term->scale = scale; const uint32_t scrollback_lines = term->render.scrollback_lines; @@ -4148,12 +4134,11 @@ maybe_resize(struct terminal *term, int width, int height, bool force) sixel_reflow(term); -#if defined(_DEBUG) && LOG_ENABLE_DBG - LOG_DBG("resize: %dx%d, grid: cols=%d, rows=%d " + LOG_DBG("resized: grid: cols=%d, rows=%d " "(left-margin=%d, right-margin=%d, top-margin=%d, bottom-margin=%d)", - term->width, term->height, term->cols, term->rows, - term->margins.left, term->margins.right, term->margins.top, term->margins.bottom); -#endif + term->cols, term->rows, + term->margins.left, term->margins.right, + term->margins.top, term->margins.bottom); if (term->scroll_region.start >= term->rows) term->scroll_region.start = 0; @@ -4295,7 +4280,7 @@ render_xcursor_update(struct seat *seat) #endif LOG_DBG("setting %scursor shape using a client-side cursor surface", - shape == CURSOR_SHAPE_CUSTOM ? "custom " : ""); + seat->pointer.shape == CURSOR_SHAPE_CUSTOM ? "custom " : ""); const float scale = seat->pointer.scale; struct wl_cursor_image *image = seat->pointer.cursor->images[0]; diff --git a/terminal.c b/terminal.c index a557d632..86cb365a 100644 --- a/terminal.c +++ b/terminal.c @@ -733,7 +733,8 @@ term_line_height_update(struct terminal *term) } static bool -term_set_fonts(struct terminal *term, struct fcft_font *fonts[static 4]) +term_set_fonts(struct terminal *term, struct fcft_font *fonts[static 4], + bool resize_grid) { for (size_t i = 0; i < 4; i++) { xassert(fonts[i] != NULL); @@ -777,11 +778,15 @@ term_set_fonts(struct terminal *term, struct fcft_font *fonts[static 4]) sixel_cell_size_changed(term); - /* Use force, since cell-width/height may have changed */ - render_resize_force( - term, - round(term->width / term->scale), - round(term->height / term->scale)); + /* Optimization - some code paths (are forced to) call + * render_resize() after this function */ + if (resize_grid) { + /* Use force, since cell-width/height may have changed */ + render_resize_force( + term, + round(term->width / term->scale), + round(term->height / term->scale)); + } return true; } @@ -899,7 +904,7 @@ font_loader_thread(void *_data) } static bool -reload_fonts(struct terminal *term) +reload_fonts(struct terminal *term, bool resize_grid) { const struct config *conf = term->conf; @@ -1026,7 +1031,7 @@ reload_fonts(struct terminal *term) } } - return success ? term_set_fonts(term, fonts) : success; + return success ? term_set_fonts(term, fonts, resize_grid) : success; } static bool @@ -1044,7 +1049,7 @@ load_fonts_from_conf(struct terminal *term) } } - return reload_fonts(term); + return reload_fonts(term, true); } static void fdm_client_terminated( @@ -1987,7 +1992,7 @@ term_font_size_adjust_by_points(struct terminal *term, float amount) } } - return reload_fonts(term); + return reload_fonts(term, true); } static bool @@ -2010,7 +2015,7 @@ term_font_size_adjust_by_pixels(struct terminal *term, int amount) } } - return reload_fonts(term); + return reload_fonts(term, true); } static bool @@ -2034,7 +2039,7 @@ term_font_size_adjust_by_percent(struct terminal *term, bool increment, float pe } } - return reload_fonts(term); + return reload_fonts(term, true); } bool @@ -2071,6 +2076,36 @@ term_font_size_reset(struct terminal *term) return load_fonts_from_conf(term); } +bool +term_update_scale(struct terminal *term) +{ + const struct wl_window *win = term->window; + + /* + * We have a number of “sources” we can use as scale. We choose + * the scale in the following order: + * + * - “preferred” scale, from the fractional-scale-v1 protocol + * - scaling factor of output we most recently were mapped on + * - if we’re not mapped, use the scaling factor from the first + * available output. + * - if there aren’t any outputs available, use 1.0 + */ + const float new_scale = + (wayl_fractional_scaling(term->wl) && win->scale > 0. + ? win->scale + : (tll_length(win->on_outputs) > 0 + ? tll_back(win->on_outputs)->scale + : 1.)); + + if (new_scale == term->scale) + return false; + + LOG_DBG("scaling factor changed: %.2f -> %.2f", term->scale, new_scale); + term->scale = new_scale; + return true; +} + bool term_font_dpi_changed(struct terminal *term, float old_scale) { @@ -2099,9 +2134,9 @@ term_font_dpi_changed(struct terminal *term, float old_scale) term->font_is_sized_by_dpi = will_scale_using_dpi; if (!need_font_reload) - return true; + return false; - return reload_fonts(term); + return reload_fonts(term, false); } void @@ -3500,7 +3535,7 @@ term_update_ascii_printer(struct terminal *term) #if defined(_DEBUG) && LOG_ENABLE_DBG if (term->ascii_printer != new_printer) { - LOG_DBG("§switching ASCII printer %s -> %s", + LOG_DBG("switching ASCII printer %s -> %s", term->ascii_printer == &ascii_printer_fast ? "fast" : "generic", new_printer == &ascii_printer_fast ? "fast" : "generic"); } diff --git a/terminal.h b/terminal.h index 60d3c3ac..4b1d1d0d 100644 --- a/terminal.h +++ b/terminal.h @@ -736,6 +736,7 @@ bool term_to_slave(struct terminal *term, const void *data, size_t len); bool term_paste_data_to_slave( struct terminal *term, const void *data, size_t len); +bool term_update_scale(struct terminal *term); bool term_font_size_increase(struct terminal *term); bool term_font_size_decrease(struct terminal *term); bool term_font_size_reset(struct terminal *term); diff --git a/wayland.c b/wayland.c index 85140d6f..354a0e78 100644 --- a/wayland.c +++ b/wayland.c @@ -396,17 +396,35 @@ static const struct wl_seat_listener seat_listener = { static void update_term_for_output_change(struct terminal *term) { - if (tll_length(term->window->on_outputs) == 0) - return; + const float old_scale = term->scale; + const float logical_width = term->width / term->scale; + const float logical_height = term->height / term->scale; - float old_scale = term->scale; - - render_resize(term, - round(term->width / term->scale), - round(term->height / term->scale)); - term_font_dpi_changed(term, old_scale); + /* Note: order matters! term_update_scale() must come first */ + bool scale_updated = term_update_scale(term); + bool fonts_updated = term_font_dpi_changed(term, old_scale); term_font_subpixel_changed(term); + csd_reload_font(term->window, old_scale); + + if (fonts_updated) { + /* + * If the fonts have been updated, the cell dimensions have + * changed. This requires a “forced” resize, since the surface + * buffer dimensions may not have been updated (in which case + * render_size() normally shortcuts and returns early). + */ + render_resize_force(term, round(logical_width), round(logical_height)); + } + + else if (scale_updated) { + /* + * A scale update means the surface buffer dimensions have + * been updated, even though the window logical dimensions + * haven’t changed. + */ + render_resize(term, round(logical_width), round(logical_height)); + } } static void From 7fca81dd3fd3cfd5ee6caafacfb61fd19adb0dcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 17 Jul 2023 16:28:10 +0200 Subject: [PATCH 0411/1323] term: get_font_subpixel(): use subpixel from monitor we were *last* mapped on --- terminal.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/terminal.c b/terminal.c index 86cb365a..2f0e632c 100644 --- a/terminal.c +++ b/terminal.c @@ -847,7 +847,8 @@ get_font_subpixel(const struct terminal *term) * output or not. * * Thus, when determining which subpixel mode to use, we can't do - * much but select *an* output. So, we pick the first one. + * much but select *an* output. So, we pick the one we were most + * recently mapped on. * * If we're not mapped at all, we pick the first available * monitor, and hope that's where we'll eventually get mapped. @@ -857,7 +858,7 @@ get_font_subpixel(const struct terminal *term) */ if (tll_length(term->window->on_outputs) > 0) - wl_subpixel = tll_front(term->window->on_outputs)->subpixel; + wl_subpixel = tll_back(term->window->on_outputs)->subpixel; else if (tll_length(term->wl->monitors) > 0) wl_subpixel = tll_front(term->wl->monitors).subpixel; else From 5b3b89cb64cb9fb2731fdb828bdc01086432b002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 17 Jul 2023 16:31:37 +0200 Subject: [PATCH 0412/1323] changelog: monitor metadata is now picked from the one we were last mapped on --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 812e483a..ca9051b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,12 @@ ## Unreleased ### Added ### Changed + +* When window is mapped, use metadata (DPI, scaling factor, subpixel + configuration) from the monitor we were most recently mapped on, + instead of the one least recently. + + ### Deprecated ### Removed ### Fixed From df96b7f4c0bbd69958615b60aaae36611b90a274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 17 Jul 2023 16:31:54 +0200 Subject: [PATCH 0413/1323] changelog: wrong DPI, and wrong initial font size with fractional scaling --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca9051b4..529ddabe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,14 @@ * Use appropriate rounding when applying fractional scales. * Xcursor not being scaled correctly on `fractional-scale-v1` capable compositors. +* `dpi-aware=yes` being broken on `fractional-scale-v1` capable + compositors (and when a fractional sacling factor is being used) + ([#1404][1404]). +* Initial font size being wrong on `fractional-scale-v1` capable + compositors, with multiple monitors with different scaling factors + connected ([#1404][1404]). + +[1404]: https://codeberg.org/dnkl/foot/issues/1404 ### Security From 4a4f2b5dae6f7303e27d45a1d60aaefb7be2d7ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 17 Jul 2023 20:13:50 +0200 Subject: [PATCH 0414/1323] pgo: add stub for wayl_fractional_scaling() --- pgo/pgo.c | 1 + 1 file changed, 1 insertion(+) diff --git a/pgo/pgo.c b/pgo/pgo.c index 6be60363..54618204 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -96,6 +96,7 @@ wayl_win_init(struct terminal *term, const char *token) void wayl_win_destroy(struct wl_window *win) {} void wayl_win_alpha_changed(struct wl_window *win) {} bool wayl_win_set_urgent(struct wl_window *win) { return true; } +bool wayl_fractional_scaling(const struct wayland *wayl) { return true; } bool spawn(struct reaper *reaper, const char *cwd, char *const argv[], From 0b8791d1c5d1bc71daff481874e6c9e44d0dba79 Mon Sep 17 00:00:00 2001 From: xdavidwu Date: Tue, 18 Jul 2023 21:09:24 +0800 Subject: [PATCH 0415/1323] wayland: fix pointer cap lost handling Before this, on compositor without cursor-shape support, a pointer capability lost of the seat makes foot crash. --- wayland.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/wayland.c b/wayland.c index 354a0e78..7e51bfe9 100644 --- a/wayland.c +++ b/wayland.c @@ -341,8 +341,10 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat, } else { if (seat->wl_pointer != NULL) { #if defined(HAVE_CURSOR_SHAPE) - wp_cursor_shape_device_v1_destroy(seat->pointer.shape_device); - seat->pointer.shape_device = NULL; + if (seat->pointer.shape_device != NULL) { + wp_cursor_shape_device_v1_destroy(seat->pointer.shape_device); + seat->pointer.shape_device = NULL; + } #endif wl_pointer_release(seat->wl_pointer); From 023a1b8da65f0f3d0c038dae658c1627e51ac7e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 18 Jul 2023 16:12:43 +0200 Subject: [PATCH 0416/1323] changelog: crash on pointer capability loss --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 529ddabe..31cd9400 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,8 +65,11 @@ * Initial font size being wrong on `fractional-scale-v1` capable compositors, with multiple monitors with different scaling factors connected ([#1404][1404]). +* Crash when _pointer capability_ is removed from a seat, on + compositors without `cursor-shape-v1 support` ([#1411][1411]). [1404]: https://codeberg.org/dnkl/foot/issues/1404 +[1411]: https://codeberg.org/dnkl/foot/pulls/1411 ### Security From 27b4c2ac2da7dc7b82e014786e9a8e9919888e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 18 Jul 2023 16:18:53 +0200 Subject: [PATCH 0417/1323] themes: starlight: update to V4 This also updates the default theme in foot, as well as the documentation. Closes #1409 --- CHANGELOG.md | 3 +++ config.c | 24 ++++++++++++------------ doc/foot.ini.5.scd | 12 ++++++------ foot.ini | 34 +++++++++++++++++----------------- themes/starlight | 30 +++++++++++++++--------------- 5 files changed, 53 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31cd9400..8a7e33bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,9 @@ * When window is mapped, use metadata (DPI, scaling factor, subpixel configuration) from the monitor we were most recently mapped on, instead of the one least recently. +* Starlight theme (the default theme) updated to [V4][starlight-v4] + +[starlight-v4]: https://github.com/CosmicToast/starlight/blob/v4/CHANGELOG.md#v4 ### Deprecated diff --git a/config.c b/config.c index 735ccd74..58a655e6 100644 --- a/config.c +++ b/config.c @@ -49,22 +49,22 @@ static const size_t min_csd_border_width = 5; static const uint32_t default_color_table[256] = { // Regular 0x242424, - 0xcf1745, - 0x3ecf5b, - 0xcfcf17, - 0x0ba6da, - 0xd926ac, - 0x17cfa1, + 0xf62b5a, + 0x47b413, + 0xe3c401, + 0x24acd4, + 0xf2affd, + 0x13c299, 0xe6e6e6, // Bright 0x616161, - 0xff1a53, - 0x17e640, - 0xecff1a, - 0x1ac6ff, - 0xf53dc7, - 0x1affc6, + 0xff4d51, + 0x35d450, + 0xe9e836, + 0x5dc5f8, + 0xfeabf2, + 0x24dfc4, 0xffffff, // 6x6x6 RGB cube diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 7fee8387..ae91ddf5 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -563,15 +563,15 @@ can configure the background transparency with the _alpha_ option. *regular0*, *regular1* *..* *regular7* The eight basic ANSI colors (Black, Red, Green, Yellow, Blue, - Magenta, Cyan, White). Default: _242424_, _cf1745_, _3ecf5b_, - _cfcf17_, _0ba6da_, _d926ac_, _17cfa1_, _e6e6e6_ (starlight - theme). + Magenta, Cyan, White). Default: _242424_, _f62b5a_, _47b413_, + _e3c401_, _24acd4_, _f2affd_, _13c299_, _e6e6e6_ (starlight + theme, V4). *bright0*, *bright1* *..* *bright7* The eight bright ANSI colors (Black, Red, Green, Yellow, Blue, - Magenta, Cyan, White). Default: _616161_, _ff1a53_, _17e640_, - _ecff1a_, _1ac6ff_, _f53dc7_, _1affc6_, _ffffff_ (starlight - theme). + Magenta, Cyan, White). Default: _616161_, _ff4d51_, _35d450_, + _e9e836_, _5dc5f8_, _feabf2_, _24dfc4_, _ffffff_ (starlight + theme, V4). *dim0*, *dim1* *..* *dim7* Custom colors to use with dimmed colors. Dimmed colors do not have diff --git a/foot.ini b/foot.ini index b4e4c603..359b2cf7 100644 --- a/foot.ini +++ b/foot.ini @@ -75,27 +75,27 @@ [colors] # alpha=1.0 -# background=002b36 -# foreground=839496 +# background=242424 +# foreground=ffffff ## Normal/regular colors (color palette 0-7) -# regular0=073642 # black -# regular1=dc322f # red -# regular2=859900 # green -# regular3=b58900 # yellow -# regular4=268bd2 # blue -# regular5=d33682 # magenta -# regular6=2aa198 # cyan -# regular7=eee8d5 # white +# regular0=242424 # black +# regular1=f62b5a # red +# regular2=47b413 # green +# regular3=e3c401 # yellow +# regular4=24acd4 # blue +# regular5=f2affd # magenta +# regular6=13c299 # cyan +# regular7=e6e6e6 # white ## Bright colors (color palette 8-15) -# bright0=08404f # bright black -# bright1=e35f5c # bright red -# bright2=9fb700 # bright green -# bright3=d9a400 # bright yellow -# bright4=4ba1de # bright blue -# bright5=dc619d # bright magenta -# bright6=32c1b6 # bright cyan +# bright0=616161 # bright black +# bright1=ff4d51 # bright red +# bright2=35d450 # bright green +# bright3=e9e836 # bright yellow +# bright4=5dc5f8 # bright blue +# bright5=feabf2 # bright magenta +# bright6=24dfc4 # bright cyan # bright7=ffffff # bright white ## dimmed colors (see foot.ini(5) man page) diff --git a/themes/starlight b/themes/starlight index 9b30b399..ed39f277 100644 --- a/themes/starlight +++ b/themes/starlight @@ -1,24 +1,24 @@ # -*- conf -*- -# Theme: starlight (https://github.com/CosmicToast/starlight) +# Theme: starlight V4 (https://github.com/CosmicToast/starlight) [colors] foreground = FFFFFF background = 242424 regular0 = 242424 -regular1 = CF1745 -regular2 = 3ECF5B -regular3 = CFCF17 -regular4 = 0BA6DA -regular5 = D926AC -regular6 = 17CFA1 -regular7 = E6E6E6 +regular1 = f62b5a +regular2 = 47b413 +regular3 = e3c401 +regular4 = 24acd4 +regular5 = f2affd +regular6 = 13c299 +regular7 = e6e6e6 bright0 = 616161 -bright1 = FF1A53 -bright2 = 17E640 -bright3 = ECFF1A -bright4 = 1AC6FF -bright5 = F53DC7 -bright6 = 1AFFC6 -bright7 = FFFFFF +bright1 = ff4d51 +bright2 = 35d450 +bright3 = e9e836 +bright4 = 5dc5f8 +bright5 = feabf2 +bright6 = 24dfc4 +bright7 = ffffff From fdd753263b3ebe96a83c73fbf560773dce04efd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 18 Jul 2023 16:13:36 +0200 Subject: [PATCH 0418/1323] term: destroy: unref key bindings *after* destroying window This fixes a crash-on-exit on compositors that emit a _"keyboard leave"_ event when a surface is unmapped. In our case, destroying the window (where we unmap it) in term_destroy(), lead to a crash in term_mouse_grabbed(), due to key_binding_for() returning NULL. The call chain in this is case is, roughly: term_destroy() -> wayl_win_destroy() -> keyboard_leave() -> term_xcursor_update_for_seat() -> term_mouse_grabbed() --- CHANGELOG.md | 2 ++ terminal.c | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a7e33bb..9f9d040d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,8 @@ connected ([#1404][1404]). * Crash when _pointer capability_ is removed from a seat, on compositors without `cursor-shape-v1 support` ([#1411][1411]). +* Crash on exit, if the mouse is hovering over the foot window (does + not happen on all compositors) [1404]: https://codeberg.org/dnkl/foot/issues/1404 [1411]: https://codeberg.org/dnkl/foot/pulls/1411 diff --git a/terminal.c b/terminal.c index 2f0e632c..c22646f2 100644 --- a/terminal.c +++ b/terminal.c @@ -1607,8 +1607,6 @@ term_destroy(struct terminal *term) if (term == NULL) return 0; - key_binding_unref(term->wl->key_binding_manager, term->conf); - tll_foreach(term->wl->terms, it) { if (it->item == term) { tll_remove(term->wl->terms, it); @@ -1654,6 +1652,8 @@ term_destroy(struct terminal *term) } mtx_unlock(&term->render.workers.lock); + key_binding_unref(term->wl->key_binding_manager, term->conf); + urls_reset(term); free(term->vt.osc.data); From 648f6016e3cc62ff895c37206680223c531bcc5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 19 Jul 2023 16:37:25 +0200 Subject: [PATCH 0419/1323] changelog: spelling: sacling -> scaling --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f9d040d..5e051081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,7 +63,7 @@ * Xcursor not being scaled correctly on `fractional-scale-v1` capable compositors. * `dpi-aware=yes` being broken on `fractional-scale-v1` capable - compositors (and when a fractional sacling factor is being used) + compositors (and when a fractional scaling factor is being used) ([#1404][1404]). * Initial font size being wrong on `fractional-scale-v1` capable compositors, with multiple monitors with different scaling factors From 899b768b744c74e88e54e6d8eb32f53accea79d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 19 Jul 2023 16:34:42 +0200 Subject: [PATCH 0420/1323] =?UTF-8?q?render:=20disable=20transparency=20wh?= =?UTF-8?q?en=20we=E2=80=99re=20fullscreened?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wayland protocol recommends (or mandates?) that compositors render a black background behind fullscreened transparent windows. I.e. you never see what’s _actually_ behind the window. So, if you have a white, but semi-transparent background in foot, it’ll be rendered in a shade of gray. Given this, it’s better to simply disable transparency while we’re fullscreened. That way, we at least get the "correct" background color. Closes #1416 --- CHANGELOG.md | 3 +++ render.c | 31 +++++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e051081..9958e293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,8 +51,11 @@ configuration) from the monitor we were most recently mapped on, instead of the one least recently. * Starlight theme (the default theme) updated to [V4][starlight-v4] +* Background transparency (alpha) is now disabled in fullscreened + windows ([#1416][1416]). [starlight-v4]: https://github.com/CosmicToast/starlight/blob/v4/CHANGELOG.md#v4 +[1416]: https://codeberg.org/dnkl/foot/issues/1416 ### Deprecated diff --git a/render.c b/render.c index 11149b16..7d7c6348 100644 --- a/render.c +++ b/render.c @@ -526,8 +526,35 @@ render_cell(struct terminal *term, pixman_image_t *pix, uint32_t swap = _fg; _fg = _bg; _bg = swap; - } else if (cell->attrs.bg_src == COLOR_DEFAULT) - alpha = term->colors.alpha; + } + + else if (cell->attrs.bg_src == COLOR_DEFAULT) { + if (term->window->is_fullscreen) { + /* + * Note: disable transparency when fullscreened. + * + * This is because the wayland protocol recommends + * (mandates even?) the compositor render a black + * background behind fullscreened transparent windows. + * + * In other words, transparency does not work when + * fullscreened, in the sense that you don't see + * what's behind the window. + * + * And if we keep our alpha channel, the background + * color will just look weird. For example, if the + * background color is white, and alpha is 0.5, then + * the window will be drawn in a shade of gray while + * fullscreened. + * + * By disabling the alpha channel, the window will at + * least be rendered in the intended background color. + */ + xassert(alpha == 0xffff); + } else { + alpha = term->colors.alpha; + } + } } if (unlikely(is_selected && _fg == _bg)) { From a49281ced3fb27d6fa0f59f7964c5c0cc07b3829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 19 Jul 2023 16:39:56 +0200 Subject: [PATCH 0421/1323] =?UTF-8?q?render:=20OSD:=20don=E2=80=99t=20mark?= =?UTF-8?q?=20surface=20as=20being=20opaque,=20when=20it=E2=80=99s=20not?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + render.c | 15 +++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9958e293..7ef784f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,7 @@ compositors without `cursor-shape-v1 support` ([#1411][1411]). * Crash on exit, if the mouse is hovering over the foot window (does not happen on all compositors) +* Visual glitches when CSD titlebar is transparent. [1404]: https://codeberg.org/dnkl/foot/issues/1404 [1411]: https://codeberg.org/dnkl/foot/pulls/1411 diff --git a/render.c b/render.c index 7d7c6348..11c2456a 100644 --- a/render.c +++ b/render.c @@ -1953,12 +1953,15 @@ render_osd(struct terminal *term, const struct wayl_sub_surface *sub_surf, wl_surface_attach(sub_surf->surface.surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer(sub_surf->surface.surf, 0, 0, buf->width, buf->height); - struct wl_region *region = wl_compositor_create_region(term->wl->compositor); - if (region != NULL) { - wl_region_add(region, 0, 0, buf->width, buf->height); - wl_surface_set_opaque_region(sub_surf->surface.surf, region); - wl_region_destroy(region); - } + if (alpha == 0xffff) { + struct wl_region *region = wl_compositor_create_region(term->wl->compositor); + if (region != NULL) { + wl_region_add(region, 0, 0, buf->width, buf->height); + wl_surface_set_opaque_region(sub_surf->surface.surf, region); + wl_region_destroy(region); + } + } else + wl_surface_set_opaque_region(sub_surf->surface.surf, NULL); wl_surface_commit(sub_surf->surface.surf); quirk_weston_subsurface_desync_off(sub_surf->sub); From c3b119ea81a7b6bf01cf8ba064ab9df8f7c92f1e Mon Sep 17 00:00:00 2001 From: CismonX Date: Thu, 20 Jul 2023 07:00:14 +0800 Subject: [PATCH 0422/1323] vt: improve handling of HTS Do not insert existing positions into the tab stop list. This prevents a performance issue when iterating through an extremely long tab stop list. Also corrects the behaviour of CBT. --- vt.c | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/vt.c b/vt.c index 2ee2dbaf..51b69c7e 100644 --- a/vt.c +++ b/vt.c @@ -436,6 +436,27 @@ UNITTEST xassert(term.vt.private == expected); } +static void +tab_set(struct terminal *term) +{ + int col = term->grid->cursor.point.col; + + if (tll_length(term->tab_stops) == 0 || tll_back(term->tab_stops) < col) { + tll_push_back(term->tab_stops, col); + return; + } + + tll_foreach(term->tab_stops, it) { + if (it->item < col) { + continue; + } + if (it->item > col) { + tll_insert_before(term->tab_stops, it, col); + } + break; + } +} + static void action_esc_dispatch(struct terminal *term, uint8_t final) { @@ -478,14 +499,7 @@ action_esc_dispatch(struct terminal *term, uint8_t final) break; case 'H': - tll_foreach(term->tab_stops, it) { - if (it->item >= term->grid->cursor.point.col) { - tll_insert_before(term->tab_stops, it, term->grid->cursor.point.col); - break; - } - } - - tll_push_back(term->tab_stops, term->grid->cursor.point.col); + tab_set(term); break; case 'M': From c12db68363131d189d8410bf2994d70919229021 Mon Sep 17 00:00:00 2001 From: Max Gautier Date: Thu, 9 Mar 2023 14:13:57 +0100 Subject: [PATCH 0423/1323] footclient: fallback logic when socket paths don't exist Even if WAYLAND_DISPLAY / XDG_RUNTIME_DIR are defined, if we can't find the corresponding socket, we fallback to the path used when they are not defined. --- client.c | 15 +++++++++------ doc/footclient.1.scd | 5 +++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/client.c b/client.c index 99c7c1e8..41be68a9 100644 --- a/client.c +++ b/client.c @@ -374,16 +374,19 @@ main(int argc, char *const *argv) const char *xdg_runtime = getenv("XDG_RUNTIME_DIR"); if (xdg_runtime != NULL) { const char *wayland_display = getenv("WAYLAND_DISPLAY"); - if (wayland_display != NULL) + if (wayland_display != NULL) { snprintf(addr.sun_path, sizeof(addr.sun_path), "%s/foot-%s.sock", xdg_runtime, wayland_display); - else + connected = (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) == 0); + } + if (!connected) { + LOG_WARN("%s: failed to connect, will now try %s/foot.sock", + addr.sun_path, xdg_runtime); snprintf(addr.sun_path, sizeof(addr.sun_path), "%s/foot.sock", xdg_runtime); - - if (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) == 0) - connected = true; - else + connected = (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) == 0); + } + if (!connected) LOG_WARN("%s: failed to connect, will now try /tmp/foot.sock", addr.sun_path); } diff --git a/doc/footclient.1.scd b/doc/footclient.1.scd index 1464700c..7d89b9ed 100644 --- a/doc/footclient.1.scd +++ b/doc/footclient.1.scd @@ -146,6 +146,11 @@ terminfo entries manually, by copying *foot* and *foot-direct* to Used to construct the default _PATH_ for the *--server-socket* option, when no explicit argument is given (see above). +If the socket at default _PATH_ does not exist, *footclient* will +fallback to the less specific path, with the following priority: +*$XDG\_RUNTIME\_DIR/foot-$WAYLAND\_DISPLAY.sock*, +*$XDG\_RUNTIME\_DIR/foot.sock*, */tmp/foot.sock*. + ## Variables set in the child process *TERM* From d3ffb0bde1a8c7a7eff43d1e32a38d314c5adc8a Mon Sep 17 00:00:00 2001 From: Max Gautier Date: Tue, 14 Feb 2023 09:49:32 +0100 Subject: [PATCH 0424/1323] Ties systemd units to graphical-session.target - wayland-instance template target was a mistake. Systemd does not support simultaneous same user session, so stop trying to go against that. - Only start systemd units in Wayland environments. --- foot-server@.service.in => foot-server.service.in | 6 +++--- foot-server.socket | 9 +++++++++ foot-server@.socket | 5 ----- meson.build | 4 ++-- 4 files changed, 14 insertions(+), 10 deletions(-) rename foot-server@.service.in => foot-server.service.in (58%) create mode 100644 foot-server.socket delete mode 100644 foot-server@.socket diff --git a/foot-server@.service.in b/foot-server.service.in similarity index 58% rename from foot-server@.service.in rename to foot-server.service.in index c40bb454..47b81267 100644 --- a/foot-server@.service.in +++ b/foot-server.service.in @@ -1,13 +1,13 @@ [Service] ExecStart=@bindir@/foot --server=3 -Environment=WAYLAND_DISPLAY=%i UnsetEnvironment=LISTEN_PID LISTEN_FDS LISTEN_FDNAMES NonBlocking=true [Unit] Requires=%N.socket -Description=Foot terminal server mode for WAYLAND_DISPLAY=%i +Description=Foot terminal server mode Documentation=man:foot(1) +PartOf=graphical-session.target [Install] -WantedBy=wayland-instance@.target +WantedBy=graphical-session.target diff --git a/foot-server.socket b/foot-server.socket new file mode 100644 index 00000000..997e4363 --- /dev/null +++ b/foot-server.socket @@ -0,0 +1,9 @@ +[Socket] +ListenStream=%t/foot.sock + +[Unit] +PartOf=graphical-session.target +ConditionEnvironment=WAYLAND_DISPLAY + +[Install] +WantedBy=graphical-session.target diff --git a/foot-server@.socket b/foot-server@.socket deleted file mode 100644 index 71db51cb..00000000 --- a/foot-server@.socket +++ /dev/null @@ -1,5 +0,0 @@ -[Socket] -ListenStream=%t/foot-%i.sock - -[Install] -WantedBy=wayland-instance@.target diff --git a/meson.build b/meson.build index 1a00153c..cdccfa7e 100644 --- a/meson.build +++ b/meson.build @@ -329,13 +329,13 @@ if systemd.found() or custom_systemd_units_dir != '' configure_file( configuration: configuration, - input: 'foot-server@.service.in', + input: 'foot-server.service.in', output: '@BASENAME@', install_dir: systemd_units_dir ) install_data( - 'foot-server@.socket', + 'foot-server.socket', install_dir: systemd_units_dir) endif From 555edd60d45b987fb77792356fc2cfdd00a52ce7 Mon Sep 17 00:00:00 2001 From: Max Gautier Date: Tue, 14 Feb 2023 12:11:40 +0100 Subject: [PATCH 0425/1323] Update documentation regarding systemd units --- doc/foot.1.scd | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 770c7f32..3da6fd7e 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -121,14 +121,14 @@ the foot command line of a socket provided by a supervision daemon (such as systemd or s6), and use that socket as it's own. - Two systemd units (foot-server@.{service,socket}) are provided to use that - feature with systemd. They need to be instantiated with the value of - $WAYLAND_DISPLAY (multiples instances can co-exists). + Two systemd units (foot-server.{service,socket}) are provided to use that + feature with systemd. To use socket activation, only enable the + socket unit. Note that starting *foot --server* as a systemd service will use - the environment of the systemd user instance; thus, if you need specific - environment variables, you'll need to import them using *systemctl --user - import-environment* or use a drop-in for the foot-server service. + the environment of the systemd user instance; thus, you'll need + to import *$WAYLAND_DISPLAY* in it using *systemctl --user + import-environment WAYLAND_DISPLAY*. *-H*,*--hold* Remain open after child process exits. From 478474d0ceb10c0151a2ba3684d6e95cd0bf75ff Mon Sep 17 00:00:00 2001 From: Max Gautier Date: Tue, 14 Feb 2023 12:04:26 +0100 Subject: [PATCH 0426/1323] Changelog: standard system target + footclient fallback --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ef784f2..29380684 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,9 +53,16 @@ * Starlight theme (the default theme) updated to [V4][starlight-v4] * Background transparency (alpha) is now disabled in fullscreened windows ([#1416][1416]). +* Foot server systemd units now use the standard + graphical-session.target ([#1281][1281]). +* If `$XDG_RUNTIME_DIR/foot-$WAYLAND_DISPLAY.sock` does not exist, + `footclient` now tries `$XDG_RUNTIME_DIR/foot.sock`, then + `/tmp/foot.sock`, even if `$WAYLAND_DISPLAY` and/or + `$XDG_RUNTIME_DIR` are defined ([#1281][1281]). [starlight-v4]: https://github.com/CosmicToast/starlight/blob/v4/CHANGELOG.md#v4 [1416]: https://codeberg.org/dnkl/foot/issues/1416 +[1281]: https://codeberg.org/dnkl/foot/pulls/1281 ### Deprecated From b3255465f1d4d5a96fbd7acee846db0e14026507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 21 Jul 2023 08:17:32 +0200 Subject: [PATCH 0427/1323] render: change baseline calculation, to center it within the line Before this patch, fonts were anchored to the top of the line. With this patch, it is instead centered. Closes #1302 --- CHANGELOG.md | 3 +++ render.c | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29380684..bda563dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,10 +59,13 @@ `footclient` now tries `$XDG_RUNTIME_DIR/foot.sock`, then `/tmp/foot.sock`, even if `$WAYLAND_DISPLAY` and/or `$XDG_RUNTIME_DIR` are defined ([#1281][1281]). +* Font baseline calculation: try to center the text within the line, + instead of anchoring it at the top ([#1302][1302]). [starlight-v4]: https://github.com/CosmicToast/starlight/blob/v4/CHANGELOG.md#v4 [1416]: https://codeberg.org/dnkl/foot/issues/1416 [1281]: https://codeberg.org/dnkl/foot/pulls/1281 +[1302]: https://codeberg.org/dnkl/foot/issues/1302 ### Deprecated diff --git a/render.c b/render.c index 11c2456a..48957a0a 100644 --- a/render.c +++ b/render.c @@ -305,7 +305,12 @@ color_brighten(const struct terminal *term, uint32_t color) static inline int font_baseline(const struct terminal *term) { - return term->font_y_ofs + term->fonts[0]->ascent; + const struct fcft_font *font = term->fonts[0]; + const int line_height = term->cell_height; + const int font_height = font->ascent + font->descent; + const int glyph_top_y = round((line_height - font_height) / 2.); + + return term->font_y_ofs + glyph_top_y + font->ascent; } static void From fa97df0eabfc17ade72213786ca317664cad9ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 21 Jul 2023 08:56:49 +0200 Subject: [PATCH 0428/1323] changelog: prepare for 1.15.1 --- CHANGELOG.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bda563dd..c55f4eee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.15.1](#1-15-1) * [1.15.0](#1-15-0) * [1.14.0](#1-14-0) * [1.13.1](#1-13-1) @@ -43,8 +43,8 @@ * [1.2.0](#1-2-0) -## Unreleased -### Added +## 1.15.1 + ### Changed * When window is mapped, use metadata (DPI, scaling factor, subpixel @@ -68,8 +68,6 @@ [1302]: https://codeberg.org/dnkl/foot/issues/1302 -### Deprecated -### Removed ### Fixed * Use appropriate rounding when applying fractional scales. @@ -91,9 +89,14 @@ [1411]: https://codeberg.org/dnkl/foot/pulls/1411 -### Security ### Contributors +* Ayush Agarwal +* CismonX +* Max Gautier +* Ronan Pigott +* xdavidwu + ## 1.15.0 From 9e4d82a48411fc79e3a2096527affcc0536eacd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 21 Jul 2023 08:57:03 +0200 Subject: [PATCH 0429/1323] meson: bump version to 1.15.1 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index cdccfa7e..aeb2daa6 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.15.0', + version: '1.15.1', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From 15d7885c785fa0898eb72657494e508aaf40beb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 21 Jul 2023 09:00:57 +0200 Subject: [PATCH 0430/1323] =?UTF-8?q?changelog:=20add=20new=20=E2=80=98unr?= =?UTF-8?q?eleased=E2=80=99=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c55f4eee..84a8aef4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.15.1](#1-15-1) * [1.15.0](#1-15-0) * [1.14.0](#1-14-0) @@ -43,6 +44,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.15.1 ### Changed From fc973a3bb934de7fe6001be7b6c39714e7b5470d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 22 Jul 2023 11:21:12 +0200 Subject: [PATCH 0431/1323] selection: send_clipboard_or_primary(): handle selection text being NULL --- selection.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/selection.c b/selection.c index f6349d7f..d1a7ea28 100644 --- a/selection.c +++ b/selection.c @@ -1662,7 +1662,7 @@ send_clipboard_or_primary(struct seat *seat, int fd, const char *selection, return; } - size_t len = strlen(selection); + size_t len = selection != NULL ? strlen(selection) : 0; size_t async_idx = 0; switch (async_write(fd, selection, len, &async_idx)) { @@ -1701,7 +1701,6 @@ send(void *data, struct wl_data_source *wl_data_source, const char *mime_type, struct seat *seat = data; const struct wl_clipboard *clipboard = &seat->clipboard; - xassert(clipboard->text != NULL); send_clipboard_or_primary(seat, fd, clipboard->text, "clipboard"); } @@ -1756,7 +1755,6 @@ primary_send(void *data, struct seat *seat = data; const struct wl_primary *primary = &seat->primary; - xassert(primary->text != NULL); send_clipboard_or_primary(seat, fd, primary->text, "primary"); } From b59fd7c388c8d59a08e7e30f07e4639d2fd5451f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 22 Jul 2023 11:21:41 +0200 Subject: [PATCH 0432/1323] vt: detect and ignore invalid UTF-8 sequences This patch detects invalid codepoints in the UTF-8 EDxxxx range, and the F4xxxxxx range. Note that we still allow the E0xxxx and F0xxxxxx ranges. These contains overlong encodings. We allow them, because they still decode into correct UTF-32. Closes #1423 --- vt.c | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/vt.c b/vt.c index 51b69c7e..772bd41f 100644 --- a/vt.c +++ b/vt.c @@ -913,6 +913,16 @@ action_utf8_33(struct terminal *term, uint8_t c) { // wc = ((utf8[0] & 0xf) << 12) | ((utf8[1] & 0x3f) << 6) | (utf8[2] & 0x3f) term->vt.utf8 |= c & 0x3f; + + const char32_t utf32 = term->vt.utf8; + if (unlikely(utf32 >= 0xd800 && utf32 <= 0xdfff)) { + /* Invalid sequence - invalid UTF-16 surrogate halves */ + return; + } + + /* Note: the E0 range contains overlong encodings. We don’t try to + detect, as they’ll still decode to valid UTF-32. */ + action_utf8_print(term, term->vt.utf8); } @@ -942,6 +952,17 @@ action_utf8_44(struct terminal *term, uint8_t c) { // wc = ((utf8[0] & 7) << 18) | ((utf8[1] & 0x3f) << 12) | ((utf8[2] & 0x3f) << 6) | (utf8[3] & 0x3f); term->vt.utf8 |= c & 0x3f; + + const char32_t utf32 = term->vt.utf8; + + if (unlikely(utf32 > 0x10FFFF)) { + /* Invalid UTF-8 */ + return; + } + + /* Note: the F0 range contains overlong encodings. We don’t try to + detect, as they’ll still decode to valid UTF-32. */ + action_utf8_print(term, term->vt.utf8); } From 8223b4b76cb6ab0d4320859b2497dc222c9762bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 22 Jul 2023 11:23:22 +0200 Subject: [PATCH 0433/1323] changelog: ignore invalid UTF-8 in input --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84a8aef4..ed0990eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,12 @@ ### Deprecated ### Removed ### Fixed + +* Crash when copying text that contains invalid UTF-8 ([#1423][1423]). + +[1423]: https://codeberg.org/dnkl/foot/issues/1423 + + ### Security ### Contributors From 0a61cfc3beb881fc8942ae99bf3ad3b5de5e6345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 23 Jul 2023 20:03:31 +0200 Subject: [PATCH 0434/1323] wayland: update terminals (fonts etc) on xdg_output_handle_done() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Monitor DPI depends on information from both the wl_output and the xdg_output interfaces. Before this patch, terminals were only updated after changes to the wl_output interfaces (thus depending on xdg output changes being pushed by the compositor before wl_output changes). That assumption (xdg_output happening before wl_output) isn’t always true. This patch fixes the issue by updating the terminals in the xdg_output’s “done” event. Closes #1431 --- CHANGELOG.md | 2 ++ wayland.c | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed0990eb..b6471b92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,8 +52,10 @@ ### Fixed * Crash when copying text that contains invalid UTF-8 ([#1423][1423]). +* Wrong font size after suspending the monitor ([#1431][1431]). [1423]: https://codeberg.org/dnkl/foot/issues/1423 +[1431]: https://codeberg.org/dnkl/foot/issues/1431 ### Security diff --git a/wayland.c b/wayland.c index 7e51bfe9..a297c76a 100644 --- a/wayland.c +++ b/wayland.c @@ -620,6 +620,8 @@ xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, static void xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output) { + struct monitor *mon = data; + update_terms_on_monitor(mon); } static void From a36f67cbe311bebd9321fbf58773dace8128fcd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 23 Jul 2023 17:35:57 +0200 Subject: [PATCH 0435/1323] render: apply new baseline calculation everywhere * URL jump labels * Scrollback position indicator * Line/box drawings characters Closes #1430 --- CHANGELOG.md | 6 ++++++ box-drawing.c | 2 +- render.c | 31 ++++++++++--------------------- terminal.c | 11 +++++++++++ terminal.h | 1 + 5 files changed, 29 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6471b92..7f1ed9f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,9 +53,15 @@ * Crash when copying text that contains invalid UTF-8 ([#1423][1423]). * Wrong font size after suspending the monitor ([#1431][1431]). +* Vertical alignment in URL jump labels, and the scrollback position + indicator ([#1430][1430]). +* Regression: line- and box drawing characters not covering the full + height of the line, when a custom `line-height` is being used + ([#1430][1430]). [1423]: https://codeberg.org/dnkl/foot/issues/1423 [1431]: https://codeberg.org/dnkl/foot/issues/1431 +[1430]: https://codeberg.org/dnkl/foot/issues/1430 ### Security diff --git a/box-drawing.c b/box-drawing.c index 3962e341..07f415cb 100644 --- a/box-drawing.c +++ b/box-drawing.c @@ -3011,7 +3011,7 @@ box_drawing(const struct terminal *term, char32_t wc) .cols = 1, .pix = buf.pix, .x = -term->font_x_ofs, - .y = term->font_y_ofs + term->fonts[0]->ascent, + .y = term_font_baseline(term), .width = width, .height = height, .advance = { diff --git a/render.c b/render.c index 48957a0a..9c37f765 100644 --- a/render.c +++ b/render.c @@ -302,17 +302,6 @@ color_brighten(const struct terminal *term, uint32_t color) return hsl_to_rgb(hue, sat, min(100, lum * 1.3)); } -static inline int -font_baseline(const struct terminal *term) -{ - const struct fcft_font *font = term->fonts[0]; - const int line_height = term->cell_height; - const int font_height = font->ascent + font->descent; - const int glyph_top_y = round((line_height - font_height) / 2.); - - return term->font_y_ofs + glyph_top_y + font->ascent; -} - 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) @@ -335,7 +324,7 @@ draw_beam_cursor(const struct terminal *term, pixman_image_t *pix, const struct fcft_font *font, const pixman_color_t *color, int x, int y) { - int baseline = y + font_baseline(term) - term->fonts[0]->ascent; + int baseline = y + term_font_baseline(term) - term->fonts[0]->ascent; pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, color, 1, &(pixman_rectangle16_t){ @@ -347,7 +336,7 @@ draw_beam_cursor(const struct terminal *term, pixman_image_t *pix, static int underline_offset(const struct terminal *term, const struct fcft_font *font) { - return font_baseline(term) - + return term_font_baseline(term) - (term->conf->use_custom_underline_offset ? -term_pt_or_px_as_pixels(term, &term->conf->underline_offset) : font->underline.position); @@ -401,7 +390,7 @@ draw_strikeout(const struct terminal *term, pixman_image_t *pix, pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, color, 1, &(pixman_rectangle16_t){ - x, y + font_baseline(term) - font->strikeout.position, + x, y + term_font_baseline(term) - font->strikeout.position, cols * term->cell_width, font->strikeout.thickness}); } @@ -767,13 +756,13 @@ render_cell(struct terminal *term, pixman_image_t *pix, if (!(cell->attrs.blink && term->blink.state == BLINK_OFF)) { pixman_image_composite32( PIXMAN_OP_OVER, glyph->pix, NULL, pix, 0, 0, 0, 0, - pen_x + letter_x_ofs + g_x, y + font_baseline(term) - g_y, + pen_x + letter_x_ofs + g_x, y + term_font_baseline(term) - g_y, glyph->width, glyph->height); } } else { pixman_image_composite32( PIXMAN_OP_OVER, clr_pix, glyph->pix, pix, 0, 0, 0, 0, - pen_x + letter_x_ofs + g_x, y + font_baseline(term) - g_y, + pen_x + letter_x_ofs + g_x, y + term_font_baseline(term) - g_y, glyph->width, glyph->height); /* Combining characters */ @@ -813,7 +802,7 @@ render_cell(struct terminal *term, pixman_image_t *pix, /* Some fonts use a negative offset, while others use a * "normal" offset */ pen_x + x_ofs + g->x, - y + font_baseline(term) - g->y, + y + term_font_baseline(term) - g->y, g->width, g->height); } } @@ -1937,12 +1926,12 @@ render_osd(struct terminal *term, const struct wayl_sub_surface *sub_surf, if (pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8) { pixman_image_composite32( PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0, - x + x_ofs + glyph->x, y + term->font_y_ofs + font->ascent - glyph->y, + x + x_ofs + glyph->x, y + /*term->font_y_ofs + font->ascent*/ term_font_baseline(term) - glyph->y, glyph->width, glyph->height); } else { pixman_image_composite32( PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, 0, 0, 0, - x + x_ofs + glyph->x, y + term->font_y_ofs + font->ascent - glyph->y, + x + x_ofs + glyph->x, y + /*term->font_y_ofs + font->ascent*/ term_font_baseline(term) - glyph->y, glyph->width, glyph->height); } @@ -3364,7 +3353,7 @@ render_search_box(struct terminal *term) /* Glyph surface is a pre-rendered image (typically a color emoji...) */ pixman_image_composite32( PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0, - x + x_ofs + glyph->x, y + font_baseline(term) - glyph->y, + x + x_ofs + glyph->x, y + term_font_baseline(term) - glyph->y, glyph->width, glyph->height); } else { int combining_ofs = width == 0 @@ -3376,7 +3365,7 @@ render_search_box(struct terminal *term) pixman_image_composite32( PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, 0, 0, 0, x + x_ofs + combining_ofs + glyph->x, - y + font_baseline(term) - glyph->y, + y + term_font_baseline(term) - glyph->y, glyph->width, glyph->height); pixman_image_unref(src); } diff --git a/terminal.c b/terminal.c index c22646f2..41dc3305 100644 --- a/terminal.c +++ b/terminal.c @@ -2166,6 +2166,17 @@ term_font_subpixel_changed(struct terminal *term) render_refresh(term); } +int +term_font_baseline(const struct terminal *term) +{ + const struct fcft_font *font = term->fonts[0]; + const int line_height = term->cell_height; + const int font_height = font->ascent + font->descent; + const int glyph_top_y = round((line_height - font_height) / 2.); + + return term->font_y_ofs + glyph_top_y + font->ascent; +} + void term_damage_rows(struct terminal *term, int start, int end) { diff --git a/terminal.h b/terminal.h index 4b1d1d0d..00cfcf31 100644 --- a/terminal.h +++ b/terminal.h @@ -742,6 +742,7 @@ bool term_font_size_decrease(struct terminal *term); bool term_font_size_reset(struct terminal *term); bool term_font_dpi_changed(struct terminal *term, float old_scale); void term_font_subpixel_changed(struct terminal *term); +int term_font_baseline(const struct terminal *term); int term_pt_or_px_as_pixels( const struct terminal *term, const struct pt_or_px *pt_or_px); From 76e471c4bc9e80aa25ee6aa125078ed5b92d0cf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 25 Jul 2023 16:45:29 +0200 Subject: [PATCH 0436/1323] ci: alpine no longer allows pip installing to the system installation --- .woodpecker.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.woodpecker.yml b/.woodpecker.yml index acd30fc7..c98ea217 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -8,8 +8,11 @@ pipeline: 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: From 391bc119de711fe2fc8a460d5990f7a8e872738a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 25 Jul 2023 16:47:40 +0200 Subject: [PATCH 0437/1323] ci (sr.ht): alpine no longer allows pip installing to the system installation --- .builds/alpine-x64.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.builds/alpine-x64.yml b/.builds/alpine-x64.yml index 6ec489fc..d1336b64 100644 --- a/.builds/alpine-x64.yml +++ b/.builds/alpine-x64.yml @@ -47,6 +47,9 @@ tasks: ninja -C bld/release -k0 meson test -C bld/release --print-errorlogs - codespell: | + python3 -m venv codespell-venv + source codespell-venv/bin/activate pip install codespell cd foot ~/.local/bin/codespell -Lser,doas,zar README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd + deactivate From 613c61abb44e49236a893376764eb21702c0ef32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 25 Jul 2023 15:56:30 +0200 Subject: [PATCH 0438/1323] scaling: always round the scaling factor when converting to int * In all calls to wl_subsurface_set_position() * (wp_viewport_set_destination() already does this) * Whenever we use the scale to calculate margins (search box, scrollback indicator etc) * Since the scaling factor is stored as a float (and not a double), use roundf() instead of round() --- csi.c | 12 +++--- render.c | 113 ++++++++++++++++++++++++++++++++--------------------- terminal.c | 28 ++++++++----- terminal.h | 1 + wayland.c | 30 +++++--------- 5 files changed, 104 insertions(+), 80 deletions(-) diff --git a/csi.c b/csi.c index 153a1099..959c913d 100644 --- a/csi.c +++ b/csi.c @@ -1208,8 +1208,8 @@ csi_dispatch(struct terminal *term, uint8_t final) char reply[64]; size_t n = xsnprintf( reply, sizeof(reply), "\033[4;%d;%dt", - (int)round(height / term->scale), - (int)(width / term->scale)); + (int)roundf(height / term->scale), + (int)roundf((width / term->scale))); term_to_slave(term, reply, n); } break; @@ -1233,8 +1233,8 @@ csi_dispatch(struct terminal *term, uint8_t final) char reply[64]; size_t n = xsnprintf( reply, sizeof(reply), "\033[6;%d;%dt", - (int)round(term->cell_height / term->scale), - (int)round(term->cell_width / term->scale)); + (int)roundf(term->cell_height / term->scale), + (int)roundf(term->cell_width / term->scale)); term_to_slave(term, reply, n); break; } @@ -1252,8 +1252,8 @@ csi_dispatch(struct terminal *term, uint8_t final) char reply[64]; size_t n = xsnprintf( reply, sizeof(reply), "\033[9;%d;%dt", - (int)round(it->item->dim.px_real.height / term->cell_height / term->scale), - (int)round(it->item->dim.px_real.width / term->cell_width / term->scale)); + (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)); term_to_slave(term, reply, n); break; } diff --git a/render.c b/render.c index 9c37f765..847e57e3 100644 --- a/render.c +++ b/render.c @@ -306,7 +306,7 @@ 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) { - const int scale = round(term->scale); + const int scale = (int)roundf(term->scale); const int width = min(min(scale, term->cell_width), term->cell_height); pixman_image_fill_rectangles( @@ -2022,8 +2022,8 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, */ float scale = term->scale; - int bwidth = round(term->conf->csd.border_width * scale); - int vwidth = round(term->conf->csd.border_width_visible * scale); /* Visible size */ + int bwidth = (int)roundf(term->conf->csd.border_width * scale); + int vwidth = (int)roundf(term->conf->csd.border_width_visible * scale); /* Visible size */ xassert(bwidth >= vwidth); @@ -2396,7 +2396,10 @@ render_csd(struct terminal *term) widths[i] = width; heights[i] = height; - wl_subsurface_set_position(sub, x / term->scale, y / term->scale); + wl_subsurface_set_position( + sub, + (int32_t)roundf(x / term->scale), + (int32_t)roundf(y / term->scale)); } struct buffer *bufs[CSD_SURF_COUNT]; @@ -2487,7 +2490,7 @@ render_scrollback_position(struct terminal *term) char lineno_str[64]; snprintf(lineno_str, sizeof(lineno_str), "%d", rebased_view + 1); mbstoc32(_text, lineno_str, ALEN(_text)); - cell_count = ceil(log10(term->grid->num_rows)); + cell_count = (int)ceilf(log10f(term->grid->num_rows)); break; } @@ -2497,13 +2500,16 @@ render_scrollback_position(struct terminal *term) break; } - const int scale = term->scale; - const int margin = 3 * scale; + const int iscale = (int)ceilf(term->scale); + const int margin = (int)roundf(3. * term->scale); - const int width = - (2 * margin + cell_count * term->cell_width + scale - 1) / scale * scale; - const int height = - (2 * margin + term->cell_height + scale - 1) / scale * scale; + int width = margin + cell_count * term->cell_width + margin; + int height = margin + term->cell_height + margin; + + if (!term_fractional_scaling(term)) { + width = (width + iscale - 1) / iscale * iscale; + height = (height + iscale - 1) / iscale * iscale; + } /* *Where* to render - parent relative coordinates */ int surf_top = 0; @@ -2531,8 +2537,13 @@ render_scrollback_position(struct terminal *term) } } - const int x = (term->width - margin - width) / scale * scale; - const int y = (term->margins.top + surf_top) / scale * scale; + int x = term->width - margin - width; + int y = term->margins.top + surf_top; + + if (!term_fractional_scaling(term)) { + x = (x + iscale - 1) / iscale * iscale; + y = (y + iscale - 1) / iscale * iscale; + } if (y + height > term->height) { wl_surface_attach(win->scrollback_indicator.surface.surf, NULL, 0, 0); @@ -2544,7 +2555,9 @@ render_scrollback_position(struct terminal *term) struct buffer *buf = shm_get_buffer(chain, width, height); wl_subsurface_set_position( - win->scrollback_indicator.sub, x / scale, y / scale); + win->scrollback_indicator.sub, + (int32_t)roundf(x / term->scale), + (int32_t)roundf(y / term->scale)); uint32_t fg = term->colors.table[0]; uint32_t bg = term->colors.table[8 + 4]; @@ -2573,21 +2586,25 @@ render_render_timer(struct terminal *term, struct timespec render_time) char32_t text[256]; mbstoc32(text, usecs_str, ALEN(text)); - const int scale = round(term->scale); + const int iscale = (int)ceilf(term->scale); const int cell_count = c32len(text); - const int margin = 3 * scale; - const int width = - (2 * margin + cell_count * term->cell_width + scale - 1) / scale * scale; - const int height = - (2 * margin + term->cell_height + scale - 1) / scale * scale; + const int margin = (int)roundf(3. * term->scale); + + int width = margin + cell_count * term->cell_width + margin; + int height = margin + term->cell_height + margin; + + if (!term_fractional_scaling(term)) { + width = (width + iscale - 1) / iscale * iscale; + height = (height + iscale - 1) / iscale * iscale; + } struct buffer_chain *chain = term->render.chains.render_timer; struct buffer *buf = shm_get_buffer(chain, width, height); wl_subsurface_set_position( win->render_timer.sub, - margin / term->scale, - (term->margins.top + term->cell_height - margin) / term->scale); + (int32_t)roundf(margin / term->scale), + (int32_t)roundf((term->margins.top + term->cell_height - margin) / term->scale)); render_osd( term, @@ -3132,19 +3149,24 @@ render_search_box(struct terminal *term) const size_t total_cells = c32swidth(text, text_len); const size_t wanted_visible_cells = max(20, total_cells); - xassert(term->scale >= 1); - const int rounded_scale = round(term->scale); - - const size_t margin = 3 * rounded_scale; + xassert(term->scale >= 1.); + const size_t margin = (size_t)roundf(3 * term->scale); const size_t width = term->width - 2 * margin; - const size_t visible_width = min( + size_t visible_width = min( term->width - 2 * margin, - (2 * margin + wanted_visible_cells * term->cell_width + rounded_scale - 1) / rounded_scale * rounded_scale); - const size_t height = min( - term->height - 2 * margin, - (2 * margin + 1 * term->cell_height + rounded_scale - 1) / rounded_scale * rounded_scale); + margin + wanted_visible_cells * term->cell_width + margin); + size_t height = min( + term->height - 2 * margin, + margin + 1 * term->cell_height + margin); + + if (!term_fractional_scaling(term)) { + const int iscale = (int)ceilf(term->scale); + visible_width = (visible_width + iscale - 1) / iscale * iscale; + height = (height + iscale - 1) / iscale * iscale; + } + const size_t visible_cells = (visible_width - 2 * margin) / term->cell_width; size_t glyph_offset = term->render.search_glyph_offset; @@ -3390,8 +3412,8 @@ render_search_box(struct terminal *term) /* TODO: this is only necessary on a window resize */ wl_subsurface_set_position( term->window->search.sub, - margin / term->scale, - max(0, (int32_t)term->height - height - margin) / term->scale); + (int32_t)roundf(margin / term->scale), + (int32_t)roundf(max(0, (int32_t)term->height - height - margin) / term->scale)); wayl_surface_scale(term->window, &term->window->search.surface, buf, term->scale); wl_surface_attach(term->window->search.surface.surf, buf->wl_buf, 0, 0); @@ -3420,9 +3442,8 @@ render_urls(struct terminal *term) struct wl_window *win = term->window; xassert(tll_length(win->urls) > 0); - const int scale = round(term->scale); - const int x_margin = 2 * scale; - const int y_margin = 1 * scale; + const int x_margin = (int)roundf(2 * term->scale); + const int y_margin = (int)roundf(1 * term->scale); /* Calculate view start, counted from the *current* scrollback start */ const int scrollback_end @@ -3592,10 +3613,14 @@ render_urls(struct terminal *term) if (cols == 0) continue; - const int width = - (2 * x_margin + cols * term->cell_width + scale - 1) / scale * scale; - const int height = - (2 * y_margin + term->cell_height + scale - 1) / scale * scale; + int width = x_margin + cols * term->cell_width + x_margin; + int height = y_margin + term->cell_height + y_margin; + + if (!term_fractional_scaling(term)) { + const int iscale = (int)ceilf(term->scale); + width = (width + iscale - 1) / iscale * iscale; + height = (height + iscale - 1) / iscale * iscale; + } info[render_count].url = &it->item; info[render_count].text = xc32dup(label); @@ -3631,8 +3656,8 @@ render_urls(struct terminal *term) wl_subsurface_set_position( sub_surf->sub, - (term->margins.left + x) / term->scale, - (term->margins.top + y) / term->scale); + (int32_t)roundf((term->margins.left + x) / term->scale), + (int32_t)roundf((term->margins.top + y) / term->scale)); render_osd( term, sub_surf, term->fonts[0], bufs[i], label, @@ -3909,8 +3934,8 @@ maybe_resize(struct terminal *term, int width, int height, bool force) * Ensure we can scale to logical size, and back to * pixels without truncating. */ - if (wayl_fractional_scaling(term->wl)) { - xassert((int)round(scale) == (int)scale); + if (!term_fractional_scaling(term)) { + xassert((int)ceilf(scale) == (int)scale); int iscale = scale; if (width % iscale) diff --git a/terminal.c b/terminal.c index 41dc3305..5736400b 100644 --- a/terminal.c +++ b/terminal.c @@ -784,8 +784,8 @@ term_set_fonts(struct terminal *term, struct fcft_font *fonts[static 4], /* Use force, since cell-width/height may have changed */ render_resize_force( term, - round(term->width / term->scale), - round(term->height / term->scale)); + (int)roundf(term->width / term->scale), + (int)roundf(term->height / term->scale)); } return true; } @@ -825,7 +825,7 @@ get_font_dpi(const struct terminal *term) ? tll_back(win->on_outputs) : &tll_front(term->wl->monitors); - if (wayl_fractional_scaling(term->wl)) + if (term_fractional_scaling(term)) return mon->dpi.physical; else return mon->dpi.scaled; @@ -880,12 +880,12 @@ int term_pt_or_px_as_pixels(const struct terminal *term, const struct pt_or_px *pt_or_px) { - double scale = !term->font_is_sized_by_dpi ? term->scale : 1.; - double dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; + float scale = !term->font_is_sized_by_dpi ? term->scale : 1.; + float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; return pt_or_px->px == 0 - ? round(pt_or_px->pt * scale * dpi / 72) - : pt_or_px->px * scale; + ? (int)roundf(pt_or_px->pt * scale * dpi / 72) + : (int)roundf(pt_or_px->px * scale); } struct font_load_data { @@ -932,7 +932,7 @@ reload_fonts(struct terminal *term, bool resize_grid) if (use_px_size) snprintf(size, sizeof(size), ":pixelsize=%d", - (int)round(term->font_sizes[i][j].px_size * scale)); + (int)roundf(term->font_sizes[i][j].px_size * scale)); else snprintf(size, sizeof(size), ":size=%.2f", term->font_sizes[i][j].pt_size * scale); @@ -2077,6 +2077,16 @@ term_font_size_reset(struct terminal *term) return load_fonts_from_conf(term); } +bool +term_fractional_scaling(const struct terminal *term) +{ +#if defined(HAVE_FRACTIONAL_SCALE) + return term->wl->fractional_scale_manager != NULL && term->window->scale > 0.; +#else + return false; +#endif +} + bool term_update_scale(struct terminal *term) { @@ -2093,7 +2103,7 @@ term_update_scale(struct terminal *term) * - if there aren’t any outputs available, use 1.0 */ const float new_scale = - (wayl_fractional_scaling(term->wl) && win->scale > 0. + (term_fractional_scaling(term) ? win->scale : (tll_length(win->on_outputs) > 0 ? tll_back(win->on_outputs)->scale diff --git a/terminal.h b/terminal.h index 00cfcf31..25019ecd 100644 --- a/terminal.h +++ b/terminal.h @@ -736,6 +736,7 @@ bool term_to_slave(struct terminal *term, const void *data, size_t len); bool term_paste_data_to_slave( struct terminal *term, const void *data, size_t len); +bool term_fractional_scaling(const struct terminal *term); bool term_update_scale(struct terminal *term); bool term_font_size_increase(struct terminal *term); bool term_font_size_decrease(struct terminal *term); diff --git a/wayland.c b/wayland.c index a297c76a..acf078aa 100644 --- a/wayland.c +++ b/wayland.c @@ -57,7 +57,7 @@ csd_reload_font(struct wl_window *win, float old_scale) char pixelsize[32]; snprintf(pixelsize, sizeof(pixelsize), "pixelsize=%u", - (int)round(conf->csd.title_height * scale * 1 / 2)); + (int)roundf(conf->csd.title_height * scale * 1 / 2)); LOG_DBG("loading CSD font \"%s:%s\" (old-scale=%.2f, scale=%.2f)", patterns[0], pixelsize, old_scale, scale); @@ -416,7 +416,7 @@ update_term_for_output_change(struct terminal *term) * buffer dimensions may not have been updated (in which case * render_size() normally shortcuts and returns early). */ - render_resize_force(term, round(logical_width), round(logical_height)); + render_resize_force(term, (int)roundf(logical_width), (int)roundf(logical_height)); } else if (scale_updated) { @@ -425,7 +425,7 @@ update_term_for_output_change(struct terminal *term) * been updated, even though the window logical dimensions * haven’t changed. */ - render_resize(term, round(logical_width), round(logical_height)); + render_resize(term, (int)roundf(logical_width), (int)roundf(logical_height)); } } @@ -1528,7 +1528,7 @@ wayl_init(struct fdm *fdm, struct key_binding_manager *key_binding_manager, LOG_INFO( "%s: %dx%d+%dx%d@%dHz %s %.2f\" scale=%d, DPI=%.2f/%.2f (physical/scaled)", it->item.name, it->item.dim.px_real.width, it->item.dim.px_real.height, - it->item.x, it->item.y, (int)round(it->item.refresh), + it->item.x, it->item.y, (int)roundf(it->item.refresh), it->item.model != NULL ? it->item.model : it->item.description, it->item.inch, it->item.scale, it->item.dpi.physical, it->item.dpi.scaled); @@ -1996,24 +1996,12 @@ wayl_roundtrip(struct wayland *wayl) wayl_flush(wayl); } - -bool -wayl_fractional_scaling(const struct wayland *wayl) -{ -#if defined(HAVE_FRACTIONAL_SCALE) - return wayl->fractional_scale_manager != NULL; -#else - return false; -#endif -} - void wayl_surface_scale_explicit_width_height( const struct wl_window *win, const struct wayl_surface *surf, int width, int height, float scale) { - - if (wayl_fractional_scaling(win->term->wl) && win->scale > 0.) { + if (term_fractional_scaling(win->term)) { #if defined(HAVE_FRACTIONAL_SCALE) LOG_DBG("scaling by a factor of %.2f using fractional scaling " "(width=%d, height=%d) ", scale, width, height); @@ -2021,8 +2009,8 @@ wayl_surface_scale_explicit_width_height( wl_surface_set_buffer_scale(surf->surf, 1); wp_viewport_set_destination( surf->viewport, - round((float)width / scale), - round((float)height / scale)); + (int32_t)roundf((float)width / scale), + (int32_t)roundf((float)height / scale)); #else BUG("wayl_fraction_scaling() returned true, " "but fractional scaling was not available at compile time"); @@ -2031,9 +2019,9 @@ wayl_surface_scale_explicit_width_height( LOG_DBG("scaling by a factor of %.2f using legacy mode " "(width=%d, height=%d)", scale, width, height); - xassert(scale == floor(scale)); + xassert(scale == floorf(scale)); - const int iscale = (int)scale; + const int iscale = (int)floorf(scale); xassert(width % iscale == 0); xassert(height % iscale == 0); From fd813d0e6cfff57cf8a8f55e9a249ddb6fbdb6d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 28 Jul 2023 15:36:48 +0200 Subject: [PATCH 0439/1323] font baseline: use max(font->height, font->ascent + font->descent) when calculating font height This is how it's done when calculating the cell height, and we should do the same thing when calculating the font baseline. --- terminal.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal.c b/terminal.c index 5736400b..d4132c24 100644 --- a/terminal.c +++ b/terminal.c @@ -2181,7 +2181,7 @@ term_font_baseline(const struct terminal *term) { const struct fcft_font *font = term->fonts[0]; const int line_height = term->cell_height; - const int font_height = font->ascent + font->descent; + const int font_height = max(font->height, font->ascent + font->descent); const int glyph_top_y = round((line_height - font_height) / 2.); return term->font_y_ofs + glyph_top_y + font->ascent; From e912656682cd6645241fd5ba9ce3c1d1533c03b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 28 Jul 2023 15:37:48 +0200 Subject: [PATCH 0440/1323] render: revert part of a36f67cbe311bebd9321fbf58773dace8128fcd7 render_osd() shouldn't use term_font_baseline(). This is because term_font_baseline() uses the line height to determine the position, while render_osd() renders to surfaces that aren't sized like the grid. This fixes a regression, where the CSD title were sometimes rendered too high up, and sometimes too low. --- render.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/render.c b/render.c index 847e57e3..37c0c97d 100644 --- a/render.c +++ b/render.c @@ -1926,12 +1926,12 @@ render_osd(struct terminal *term, const struct wayl_sub_surface *sub_surf, if (pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8) { pixman_image_composite32( PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0, - x + x_ofs + glyph->x, y + /*term->font_y_ofs + font->ascent*/ term_font_baseline(term) - glyph->y, + x + x_ofs + glyph->x, y + term->font_y_ofs + font->ascent /*term_font_baseline(term)*/ - glyph->y, glyph->width, glyph->height); } else { pixman_image_composite32( PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, 0, 0, 0, - x + x_ofs + glyph->x, y + /*term->font_y_ofs + font->ascent*/ term_font_baseline(term) - glyph->y, + x + x_ofs + glyph->x, y + term->font_y_ofs + font->ascent /* term_font_baseline(term)*/ - glyph->y, glyph->width, glyph->height); } From f3c5b82c8276feda3c9eec522e549062c1589484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 26 Jul 2023 16:12:36 +0200 Subject: [PATCH 0441/1323] config: add tweak.bold-text-in-bright-amount By how much to increase the luminance when brightening bold fonts. This was previously hard-coded to a factor of 1.3, which is now the default value of the new config option. Closes #1434 --- CHANGELOG.md | 6 ++++++ config.c | 14 +++++++++----- config.h | 1 + doc/foot.ini.5.scd | 8 +++++++- hsl.c | 6 +++--- render.c | 4 +++- tests/test-config.c | 9 ++++++--- 7 files changed, 35 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f1ed9f3..85eeae1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,12 @@ ## Unreleased ### Added + +* `[tweak].bold-text-in-bright-amount` option ([#1434][1434]). + +[1434]: https://codeberg.org/dnkl/foot/issues/1434 + + ### Changed ### Deprecated ### Removed diff --git a/config.c b/config.c index 58a655e6..302e30f0 100644 --- a/config.c +++ b/config.c @@ -480,7 +480,7 @@ value_to_dimensions(struct context *ctx, uint32_t *x, uint32_t *y) } static bool NOINLINE -value_to_double(struct context *ctx, float *res) +value_to_float(struct context *ctx, float *res) { const char *s = ctx->value; @@ -659,7 +659,7 @@ value_to_pt_or_px(struct context *ctx, struct pt_or_px *res) res->px = value; } else { float value; - if (!value_to_double(ctx, &value)) + if (!value_to_float(ctx, &value)) return false; res->pt = value; res->px = 0; @@ -1089,7 +1089,7 @@ parse_section_scrollback(struct context *ctx) } else if (strcmp(key, "multiplier") == 0) - return value_to_double(ctx, &conf->scrollback.multiplier); + return value_to_float(ctx, &conf->scrollback.multiplier); else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); @@ -1298,7 +1298,7 @@ parse_section_colors(struct context *ctx) else if (strcmp(key, "alpha") == 0) { float alpha; - if (!value_to_double(ctx, &alpha)) + if (!value_to_float(ctx, &alpha)) return false; if (alpha < 0. || alpha > 1.) { @@ -2461,7 +2461,7 @@ parse_section_tweak(struct context *ctx) } else if (strcmp(key, "box-drawing-base-thickness") == 0) - return value_to_double(ctx, &conf->tweak.box_drawing_base_thickness); + return value_to_float(ctx, &conf->tweak.box_drawing_base_thickness); else if (strcmp(key, "box-drawing-solid-shades") == 0) return value_to_bool(ctx, &conf->tweak.box_drawing_solid_shades); @@ -2472,6 +2472,9 @@ parse_section_tweak(struct context *ctx) else if (strcmp(key, "sixel") == 0) return value_to_bool(ctx, &conf->tweak.sixel); + else if (strcmp(key, "bold-text-in-bright-amount") == 0) + return value_to_float(ctx, &conf->bold_in_bright.amount); + else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; @@ -2939,6 +2942,7 @@ config_load(struct config *conf, const char *conf_path, .bold_in_bright = { .enabled = false, .palette_based = false, + .amount = 1.3, }, .startup_mode = STARTUP_WINDOWED, .fonts = {{0}}, diff --git a/config.h b/config.h index 8189e56d..4d2838c4 100644 --- a/config.h +++ b/config.h @@ -133,6 +133,7 @@ struct config { struct { bool enabled; bool palette_based; + float amount; } bold_in_bright; enum { STARTUP_WINDOWED, STARTUP_MAXIMIZED, STARTUP_FULLSCREEN } startup_mode; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index ae91ddf5..8726da0c 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1337,7 +1337,13 @@ any of these options. Default: _512_. Maximum allowed: _2048_ (2GB). *sixel* - Boolean. When enabled, foot will process sixel images. Default: _yes_ + Boolean. When enabled, foot will process sixel images. Default: + _yes_ + +*bold-text-in-bright-amount* + Amount by which bold fonts are brightened when + *bold-text-in-bright* is set to *yes* (the *palette-based* variant + is not affected by this option). Default: _1.3_. # SEE ALSO diff --git a/hsl.c b/hsl.c index 3ebe4beb..d5d00e67 100644 --- a/hsl.c +++ b/hsl.c @@ -83,7 +83,7 @@ hsl_to_rgb(int hue, int sat, int lum) b += m; return ( - (int)round(r * 255.) << 16 | - (int)round(g * 255.) << 8 | - (int)round(b * 255.) << 0); + (uint8_t)round(r * 255.) << 16 | + (uint8_t)round(g * 255.) << 8 | + (uint8_t)round(b * 255.) << 0); } diff --git a/render.c b/render.c index 37c0c97d..6771fae5 100644 --- a/render.c +++ b/render.c @@ -299,7 +299,9 @@ color_brighten(const struct terminal *term, uint32_t color) int hue, sat, lum; rgb_to_hsl(color, &hue, &sat, &lum); - return hsl_to_rgb(hue, sat, min(100, lum * 1.3)); + + lum = (int)roundf(lum * term->conf->bold_in_bright.amount); + return hsl_to_rgb(hue, sat, min(lum, 100)); } static void diff --git a/tests/test-config.c b/tests/test-config.c index 54efd13a..2ae891e8 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -265,7 +265,7 @@ test_uint32(struct context *ctx, bool (*parse_fun)(struct context *ctx), } static void -test_double(struct context *ctx, bool (*parse_fun)(struct context *ctx), +test_float(struct context *ctx, bool (*parse_fun)(struct context *ctx), const char *key, const float *ptr) { ctx->key = key; @@ -580,7 +580,7 @@ test_section_scrollback(void) test_uint32(&ctx, &parse_section_scrollback, "lines", &conf.scrollback.lines); - test_double(&ctx, parse_section_scrollback, "multiplier", &conf.scrollback.multiplier); + test_float(&ctx, parse_section_scrollback, "multiplier", &conf.scrollback.multiplier); test_enum( &ctx, &parse_section_scrollback, "indicator-position", @@ -1312,7 +1312,7 @@ test_section_tweak(void) RENDER_TIMER_BOTH}, (int *)&conf.tweak.render_timer); - test_double(&ctx, &parse_section_tweak, "box-drawing-base-thickness", + test_float(&ctx, &parse_section_tweak, "box-drawing-base-thickness", &conf.tweak.box_drawing_base_thickness); test_boolean(&ctx, &parse_section_tweak, "box-drawing-solid-shades", &conf.tweak.box_drawing_solid_shades); @@ -1345,6 +1345,9 @@ test_section_tweak(void) test_boolean(&ctx, &parse_section_tweak, "font-monospace-warn", &conf.tweak.font_monospace_warn); + test_float(&ctx, &parse_section_tweak, "bold-text-in-bright-amount", + &conf.bold_in_bright.amount); + #if 0 /* Must be equal to, or less than INT32_MAX */ test_uint32(&ctx, &parse_section_tweak, "max-shm-pool-size-mb", &conf.tweak.max_shm_pool_size); From 139fd6d55cbdd2833be3d3eb7f2c96c87a7fa42d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 26 Jul 2023 16:14:38 +0200 Subject: [PATCH 0442/1323] meson: add -Dterminfo-base-name option This defines the base name of the generated terminfo files. It defaults to the value of -Ddefault-terminfo (i.e. 'foot') Example: meson -Ddefault-terminfo=foot-bananas -Dterminfo-base-name=foot-apples The generated terminfo files will be * terminfo/f/foot-apples * terminfo/f/foot-apples-direct The default value of $TERM will be 'foot-bananas' --- CHANGELOG.md | 5 +++++ INSTALL.md | 15 +++++++++++++-- meson.build | 12 +++++++++--- meson_options.txt | 3 ++- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85eeae1f..26f1d1fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,11 @@ ### Added * `[tweak].bold-text-in-bright-amount` option ([#1434][1434]). +* `-Dterminfo-base-name` meson option, allowing you to name the + terminfo files to something other than `-Ddefault-terminfo`. Use + case: have foot default to using the terminfo from ncurses (`foot`, + `foot-direct`), while still packaging foot's terminfo files, but + under a different name (e.g. `foot-extra`, `foot-extra-direct`). [1434]: https://codeberg.org/dnkl/foot/issues/1434 diff --git a/INSTALL.md b/INSTALL.md index 9e2da8ec..67e6b910 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -151,6 +151,7 @@ Available compile-time options: | `-Dgrapheme-clustering` | feature | `auto` | Enables grapheme clustering | libutf8proc | | `-Dterminfo` | feature | `enabled` | Build and install terminfo files | tic (ncurses) | | `-Ddefault-terminfo` | string | `foot` | Default value of `TERM` | None | +| `-Dterminfo-base-name` | string | `-Ddefault-terminfo` | Base name of the generated terminfo files | None | | `-Dcustom-terminfo-install-location` | string | `${datadir}/terminfo` | Value to set `TERMINFO` to | None | | `-Dsystemd-units-dir` | string | `${systemduserunitdir}` | Where to install the systemd service files (absolute) | None | | `-Dutmp-backend` | combo | `auto` | Which utmp backend to use (`none`, `libutempter`, `ulog` or `auto`) | libutempter or ulog | @@ -165,8 +166,18 @@ under a different name. Setting this changes the default value of `$TERM`, and the names of the terminfo files (if `-Dterminfo=enabled`). -`-Dcustom-terminfo-install-location` enables foot’s terminfo to -co-exist with ncurses’ version, without changing the terminfo +If you want foot to use the terminfo files from ncurses, but still +package foot's own terminfo files under a different name, you can use +the `-Dterminfo-base-name` option. Many distributions use the name +`foot-extra`, and thus it might be a good idea to re-use that: + +```sh +meson ... -Ddefault-terminfo=foot -Dterminfo-base-name=foot-extra +``` +(or just leave out `-Ddefault-terminfo`, since it defaults to `foot` anyway). + +Finally, `-Dcustom-terminfo-install-location` enables foot’s terminfo +to co-exist with ncurses’ version, without changing the terminfo names. The idea is that you install foot’s terminfo to a non-standard location, for example `/usr/share/foot/terminfo`. Use `-Dcustom-terminfo-install-location` to tell foot where the terminfo diff --git a/meson.build b/meson.build index aeb2daa6..87af1edd 100644 --- a/meson.build +++ b/meson.build @@ -352,11 +352,16 @@ if get_option('themes') install_subdir('themes', install_dir: join_paths(get_option('datadir'), 'foot')) endif +terminfo_base_name = get_option('terminfo-base-name') +if terminfo_base_name == '' + terminfo_base_name = get_option('default-terminfo') +endif + tic = find_program('tic', native: true, required: get_option('terminfo')) if tic.found() conf_data = configuration_data( { - 'default_terminfo': get_option('default-terminfo'), + 'default_terminfo': terminfo_base_name } ) @@ -367,9 +372,9 @@ if tic.found() ) custom_target( 'terminfo', - output: get_option('default-terminfo')[0], + output: terminfo_base_name[0], input: preprocessed, - command: [tic, '-x', '-o', '@OUTDIR@', '-e', '@0@,@0@-direct'.format(get_option('default-terminfo')), '@INPUT@'], + command: [tic, '-x', '-o', '@OUTDIR@', '-e', '@0@,@0@-direct'.format(terminfo_base_name), '@INPUT@'], install: true, install_dir: terminfo_install_location ) @@ -395,6 +400,7 @@ summary( 'utmp backend': utmp_backend, 'utmp helper default path': utmp_default_helper_path, 'Build terminfo': tic.found(), + 'Terminfo base name': terminfo_base_name, 'Terminfo install location': terminfo_install_location, 'Default TERM': get_option('default-terminfo'), 'Set TERMINFO': get_option('custom-terminfo-install-location') != '', diff --git a/meson_options.txt b/meson_options.txt index d16e23ae..ab7a07be 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -15,7 +15,8 @@ option('tests', type: 'boolean', value: true, description: 'Build tests') option('terminfo', type: 'feature', value: 'enabled', description: 'Build and install foot\'s terminfo files.') option('default-terminfo', type: 'string', value: 'foot', description: 'Default value of the "term" option in foot.ini.') - +option('terminfo-base-name', type: 'string', + description: 'Base name of the generated terminfo files. Defaults to the value of the \'default-terminfo\' meson option') option('custom-terminfo-install-location', type: 'string', value: '', description: 'Path to foot\'s terminfo, relative to ${prefix}. If set, foot will set $TERMINFO to this value in the client process.') From 9d75c551465fa3dbb3cd20ae87d6de294fcebce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 28 Jul 2023 15:32:42 +0200 Subject: [PATCH 0443/1323] wayland: don't try to use a non-existing viewporter interface When instantiating the viewport for a pointer surface, we didn't first check if the compositor implements the viewporter interface. This triggered a crash when a) foot was compiled with fractional scaling, and b) the compositor did not implement the viewporter interface. Closes #1444 --- CHANGELOG.md | 3 +++ wayland.c | 24 ++++++++++++++---------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26f1d1fe..a60ccef5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,10 +69,13 @@ * Regression: line- and box drawing characters not covering the full height of the line, when a custom `line-height` is being used ([#1430][1430]). +* Crash when compositor does not implement the _viewporter_ interface + ([#1444][1444]). [1423]: https://codeberg.org/dnkl/foot/issues/1423 [1431]: https://codeberg.org/dnkl/foot/issues/1431 [1430]: https://codeberg.org/dnkl/foot/issues/1430 +[1444]: https://codeberg.org/dnkl/foot/issues/1444 ### Security diff --git a/wayland.c b/wayland.c index acf078aa..9a6609f4 100644 --- a/wayland.c +++ b/wayland.c @@ -315,15 +315,17 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat, } #if defined(HAVE_FRACTIONAL_SCALE) - xassert(seat->pointer.surface.viewport == NULL); - seat->pointer.surface.viewport = wp_viewporter_get_viewport( - seat->wayl->viewporter, seat->pointer.surface.surf); + if (seat->wayl->viewporter != NULL) { + xassert(seat->pointer.surface.viewport == NULL); + seat->pointer.surface.viewport = wp_viewporter_get_viewport( + seat->wayl->viewporter, seat->pointer.surface.surf); - if (seat->pointer.surface.viewport == NULL) { - LOG_ERR("%s: failed to create pointer viewport", seat->name); - wl_surface_destroy(seat->pointer.surface.surf); - seat->pointer.surface.surf = NULL; - return; + if (seat->pointer.surface.viewport == NULL) { + LOG_ERR("%s: failed to create pointer viewport", seat->name); + wl_surface_destroy(seat->pointer.surface.surf); + seat->pointer.surface.surf = NULL; + return; + } } #endif @@ -351,8 +353,10 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat, wl_surface_destroy(seat->pointer.surface.surf); #if defined(HAVE_FRACTIONAL_SCALE) - wp_viewport_destroy(seat->pointer.surface.viewport); - seat->pointer.surface.viewport = NULL; + if (seat->pointer.surface.viewport != NULL) { + wp_viewport_destroy(seat->pointer.surface.viewport); + seat->pointer.surface.viewport = NULL; + } #endif if (seat->pointer.theme != NULL) From 753c4b5d4fb51574524af06ba56333f4241b1a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 27 Jul 2023 20:07:05 +0200 Subject: [PATCH 0444/1323] render: round scaled border/title/button widths And calculation of compounded offsets/widths/heights, to compensate for compositor rounding when positioning and scaling/sizing subsurfaces. Closes #1441 --- render.c | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/render.c b/render.c index 6771fae5..ffac3d70 100644 --- a/render.c +++ b/render.c @@ -1808,33 +1808,49 @@ get_csd_data(const struct terminal *term, enum csd_surface surf_idx) const bool borders_visible = wayl_win_csd_borders_visible(term->window); const bool title_visible = wayl_win_csd_titlebar_visible(term->window); - /* Only title bar is rendered in maximized mode */ + const float scale = term->scale; + const int border_width = borders_visible - ? term->conf->csd.border_width * term->scale : 0; + ? roundf(term->conf->csd.border_width * scale) : 0; const int title_height = title_visible - ? term->conf->csd.title_height * term->scale : 0; + ? roundf(term->conf->csd.title_height * scale) : 0; const int button_width = title_visible - ? term->conf->csd.button_width * term->scale : 0; + ? roundf(term->conf->csd.button_width * scale) : 0; const int button_close_width = term->width >= 1 * button_width ? button_width : 0; const int button_maximize_width = term->width >= 2 * button_width && term->window->wm_capabilities.maximize - ? button_width : 0; + ? button_width : 0; const int button_minimize_width = term->width >= 3 * button_width && term->window->wm_capabilities.minimize - ? button_width : 0; + ? button_width : 0; + + /* + * With fractional scaling, we must ensure the offset, when + * divided by the scale (in set_position()), and the scaled back + * (by the compositor), matches the actual pixel count made up by + * the titlebar and the border. + */ + const int top_offset = roundf( + scale * (roundf(-title_height / scale) - roundf(border_width / scale))); + + const int top_bottom_width = roundf( + scale * (roundf(term->width / scale) + 2 * roundf(border_width / scale))); + + const int left_right_height = roundf( + scale * (roundf(title_height / scale) + roundf(term->height / scale))); switch (surf_idx) { - case CSD_SURF_TITLE: return (struct csd_data){ 0, -title_height, term->width, title_height}; - case CSD_SURF_LEFT: return (struct csd_data){-border_width, -title_height, border_width, title_height + term->height}; - case CSD_SURF_RIGHT: return (struct csd_data){ term->width, -title_height, border_width, title_height + term->height}; - case CSD_SURF_TOP: return (struct csd_data){-border_width, -title_height - border_width, term->width + 2 * border_width, border_width}; - case CSD_SURF_BOTTOM: return (struct csd_data){-border_width, term->height, term->width + 2 * border_width, border_width}; + case CSD_SURF_TITLE: return (struct csd_data){ 0, -title_height, term->width, title_height}; + case CSD_SURF_LEFT: return (struct csd_data){-border_width, -title_height, border_width, left_right_height}; + case CSD_SURF_RIGHT: return (struct csd_data){ term->width, -title_height, border_width, left_right_height}; + case CSD_SURF_TOP: return (struct csd_data){-border_width, top_offset, top_bottom_width, border_width}; + case CSD_SURF_BOTTOM: return (struct csd_data){-border_width, term->height, top_bottom_width, border_width}; /* Positioned relative to CSD_SURF_TITLE */ case CSD_SURF_MINIMIZE: return (struct csd_data){term->width - 3 * button_width, 0, button_minimize_width, title_height}; From 17824744812c16560dd214b98f28977b0d2fb47d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 28 Jul 2023 15:28:10 +0200 Subject: [PATCH 0445/1323] fractional scaling: another round(!) of rounding fixes * Ensure buffer sizes are valid. That is, ensure that size / scale * scale == size. * Do size calculation of the window geometry in the same way we calculate the CSD offsets. --- render.c | 132 ++++++++++++++++++++++++++++-------------------------- wayland.c | 7 +-- 2 files changed, 73 insertions(+), 66 deletions(-) diff --git a/render.c b/render.c index ffac3d70..893b461a 100644 --- a/render.c +++ b/render.c @@ -2386,6 +2386,7 @@ render_csd(struct terminal *term) if (term->window->is_fullscreen) return; + const float scale = term->scale; struct csd_data infos[CSD_SURF_COUNT]; int widths[CSD_SURF_COUNT]; int heights[CSD_SURF_COUNT]; @@ -2413,11 +2414,7 @@ render_csd(struct terminal *term) widths[i] = width; heights[i] = height; - - wl_subsurface_set_position( - sub, - (int32_t)roundf(x / term->scale), - (int32_t)roundf(y / term->scale)); + wl_subsurface_set_position(sub, roundf(x / scale), roundf(y / scale)); } struct buffer *bufs[CSD_SURF_COUNT]; @@ -2518,16 +2515,14 @@ render_scrollback_position(struct terminal *term) break; } - const int iscale = (int)ceilf(term->scale); - const int margin = (int)roundf(3. * term->scale); + const float scale = term->scale; + const int margin = (int)roundf(3. * scale); int width = margin + cell_count * term->cell_width + margin; int height = margin + term->cell_height + margin; - if (!term_fractional_scaling(term)) { - width = (width + iscale - 1) / iscale * iscale; - height = (height + iscale - 1) / iscale * iscale; - } + width = roundf(scale * ceilf(width / scale)); + height = roundf(scale * ceilf(height / scale)); /* *Where* to render - parent relative coordinates */ int surf_top = 0; @@ -2558,10 +2553,8 @@ render_scrollback_position(struct terminal *term) int x = term->width - margin - width; int y = term->margins.top + surf_top; - if (!term_fractional_scaling(term)) { - x = (x + iscale - 1) / iscale * iscale; - y = (y + iscale - 1) / iscale * iscale; - } + x = roundf(scale * ceilf(x / scale)); + y = roundf(scale * ceilf(y / scale)); if (y + height > term->height) { wl_surface_attach(win->scrollback_indicator.surface.surf, NULL, 0, 0); @@ -2573,9 +2566,7 @@ render_scrollback_position(struct terminal *term) struct buffer *buf = shm_get_buffer(chain, width, height); wl_subsurface_set_position( - win->scrollback_indicator.sub, - (int32_t)roundf(x / term->scale), - (int32_t)roundf(y / term->scale)); + win->scrollback_indicator.sub, roundf(x / scale), roundf(y / scale)); uint32_t fg = term->colors.table[0]; uint32_t bg = term->colors.table[8 + 4]; @@ -2604,25 +2595,23 @@ render_render_timer(struct terminal *term, struct timespec render_time) char32_t text[256]; mbstoc32(text, usecs_str, ALEN(text)); - const int iscale = (int)ceilf(term->scale); + const float scale = term->scale; const int cell_count = c32len(text); - const int margin = (int)roundf(3. * term->scale); + const int margin = (int)roundf(3. * scale); int width = margin + cell_count * term->cell_width + margin; int height = margin + term->cell_height + margin; - if (!term_fractional_scaling(term)) { - width = (width + iscale - 1) / iscale * iscale; - height = (height + iscale - 1) / iscale * iscale; - } + width = roundf(scale * ceilf(width / scale)); + height = roundf(scale * ceilf(height / scale)); struct buffer_chain *chain = term->render.chains.render_timer; struct buffer *buf = shm_get_buffer(chain, width, height); wl_subsurface_set_position( win->render_timer.sub, - (int32_t)roundf(margin / term->scale), - (int32_t)roundf((term->margins.top + term->cell_height - margin) / term->scale)); + roundf(margin / scale), + roundf((term->margins.top + term->cell_height - margin) / scale)); render_osd( term, @@ -3167,24 +3156,22 @@ render_search_box(struct terminal *term) const size_t total_cells = c32swidth(text, text_len); const size_t wanted_visible_cells = max(20, total_cells); - xassert(term->scale >= 1.); - const size_t margin = (size_t)roundf(3 * term->scale); - - const size_t width = term->width - 2 * margin; - size_t visible_width = min( - term->width - 2 * margin, - margin + wanted_visible_cells * term->cell_width + margin); + const float scale = term->scale; + xassert(scale >= 1.); + const size_t margin = (size_t)roundf(3 * scale); + size_t width = term->width - 2 * margin; size_t height = min( term->height - 2 * margin, margin + 1 * term->cell_height + margin); - if (!term_fractional_scaling(term)) { - const int iscale = (int)ceilf(term->scale); - visible_width = (visible_width + iscale - 1) / iscale * iscale; - height = (height + iscale - 1) / iscale * iscale; - } - + width = roundf(scale * ceilf((term->width - 2 * margin) / scale)); + height = roundf(scale * ceilf(height / scale)); + + size_t visible_width = min( + term->width - 2 * margin, + margin + wanted_visible_cells * term->cell_width + margin); + const size_t visible_cells = (visible_width - 2 * margin) / term->cell_width; size_t glyph_offset = term->render.search_glyph_offset; @@ -3430,10 +3417,10 @@ render_search_box(struct terminal *term) /* TODO: this is only necessary on a window resize */ wl_subsurface_set_position( term->window->search.sub, - (int32_t)roundf(margin / term->scale), - (int32_t)roundf(max(0, (int32_t)term->height - height - margin) / term->scale)); + roundf(margin / scale), + roundf(max(0, (int32_t)term->height - height - margin) / scale)); - wayl_surface_scale(term->window, &term->window->search.surface, buf, term->scale); + wayl_surface_scale(term->window, &term->window->search.surface, buf, scale); wl_surface_attach(term->window->search.surface.surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer(term->window->search.surface.surf, 0, 0, width, height); @@ -3460,8 +3447,9 @@ render_urls(struct terminal *term) struct wl_window *win = term->window; xassert(tll_length(win->urls) > 0); - const int x_margin = (int)roundf(2 * term->scale); - const int y_margin = (int)roundf(1 * term->scale); + const float scale = term->scale; + const int x_margin = (int)roundf(2 * scale); + const int y_margin = (int)roundf(1 * scale); /* Calculate view start, counted from the *current* scrollback start */ const int scrollback_end @@ -3634,11 +3622,8 @@ render_urls(struct terminal *term) int width = x_margin + cols * term->cell_width + x_margin; int height = y_margin + term->cell_height + y_margin; - if (!term_fractional_scaling(term)) { - const int iscale = (int)ceilf(term->scale); - width = (width + iscale - 1) / iscale * iscale; - height = (height + iscale - 1) / iscale * iscale; - } + width = roundf(scale * ceilf(width / scale)); + height = roundf(scale * ceilf(height / scale)); info[render_count].url = &it->item; info[render_count].text = xc32dup(label); @@ -3674,8 +3659,8 @@ render_urls(struct terminal *term) wl_subsurface_set_position( sub_surf->sub, - (int32_t)roundf((term->margins.left + x) / term->scale), - (int32_t)roundf((term->margins.top + y) / term->scale)); + roundf((term->margins.left + x) / scale), + roundf((term->margins.top + y) / scale)); render_osd( term, sub_surf, term->fonts[0], bufs[i], label, @@ -3974,8 +3959,8 @@ maybe_resize(struct terminal *term, int width, int height, bool force) const int min_rows = 1; /* Minimum window size (must be divisible by the scaling factor)*/ - const int min_width = (min_cols * term->cell_width + scale - 1) / scale * scale; - const int min_height = (min_rows * term->cell_height + scale - 1) / scale * scale; + const int min_width = roundf(scale * ceilf((min_cols * term->cell_width) / scale)); + const int min_height = roundf(scale * ceilf((min_rows * term->cell_height) / scale)); width = max(width, min_width); height = max(height, min_height); @@ -4235,22 +4220,43 @@ damage_view: const bool title_shown = wayl_win_csd_titlebar_visible(term->window); const bool border_shown = wayl_win_csd_borders_visible(term->window); - const int title_height = - title_shown ? term->conf->csd.title_height : 0; - const int border_width = - border_shown ? term->conf->csd.border_width_visible : 0; + const int title = title_shown + ? roundf(term->conf->csd.title_height * scale) + : 0; + const int border = border_shown + ? roundf(term->conf->csd.border_width_visible * scale) + : 0; + + /* Must use surface logical coordinates (same calculations as + in get_csd_data(), but with different inputs) */ + const int toplevel_min_width = roundf(border / scale) + + roundf(min_width / scale) + + roundf(border / scale); + + const int toplevel_min_height = roundf(border / scale) + + roundf(title / scale) + + roundf(min_height / scale) + + roundf(border / scale); + + const int toplevel_width = roundf(border / scale) + + roundf(term->width / scale) + + roundf(border / scale); + + const int toplevel_height = roundf(border / scale) + + roundf(title / scale) + + roundf(term->height / scale) + + roundf(border / scale); + + const int x = roundf(-border / scale); + const int y = roundf(-title / scale) - roundf(border / scale); xdg_toplevel_set_min_size( term->window->xdg_toplevel, - min_width / scale + 2 * border_width, - min_height / scale + title_height + 2 * border_width); + toplevel_min_width, toplevel_min_height); xdg_surface_set_window_geometry( term->window->xdg_surface, - -border_width, - -title_height - border_width, - term->width / term->scale + 2 * border_width, - term->height / term->scale + title_height + 2 * border_width); + x, y, toplevel_width, toplevel_height); } tll_free(term->normal.scroll_damage); diff --git a/wayland.c b/wayland.c index 9a6609f4..66e06c10 100644 --- a/wayland.c +++ b/wayland.c @@ -2010,11 +2010,12 @@ wayl_surface_scale_explicit_width_height( LOG_DBG("scaling by a factor of %.2f using fractional scaling " "(width=%d, height=%d) ", scale, width, height); + xassert((int)roundf(scale * (int)roundf(width / scale)) == width); + xassert((int)roundf(scale * (int)roundf(height / scale)) == height); + wl_surface_set_buffer_scale(surf->surf, 1); wp_viewport_set_destination( - surf->viewport, - (int32_t)roundf((float)width / scale), - (int32_t)roundf((float)height / scale)); + surf->viewport, roundf(width / scale), roundf(height / scale)); #else BUG("wayl_fraction_scaling() returned true, " "but fractional scaling was not available at compile time"); From 764248bb0d846f65c20931024c1f4adca57aae29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 29 Jul 2023 08:18:00 +0200 Subject: [PATCH 0446/1323] =?UTF-8?q?wayl=5Fsurface=5Fscale=5Fexplicit=5Fw?= =?UTF-8?q?idth=5Fheight():=20don=E2=80=99t=20assert=20width/height=20are?= =?UTF-8?q?=20valid=20for=20scale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This function is only called directly when scaling the mouse pointer. The mouse pointer is never guaranteed to have a valid width and height, so skip the width/height assertions for it. --- wayland.c | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/wayland.c b/wayland.c index 66e06c10..c338bef8 100644 --- a/wayland.c +++ b/wayland.c @@ -2000,18 +2000,31 @@ wayl_roundtrip(struct wayland *wayl) wayl_flush(wayl); } -void -wayl_surface_scale_explicit_width_height( +static void +surface_scale_explicit_width_height( const struct wl_window *win, const struct wayl_surface *surf, - int width, int height, float scale) + int width, int height, float scale, bool verify) { if (term_fractional_scaling(win->term)) { #if defined(HAVE_FRACTIONAL_SCALE) LOG_DBG("scaling by a factor of %.2f using fractional scaling " "(width=%d, height=%d) ", scale, width, height); - xassert((int)roundf(scale * (int)roundf(width / scale)) == width); - xassert((int)roundf(scale * (int)roundf(height / scale)) == height); + if (verify) { + if ((int)roundf(scale * (int)roundf(width / scale)) != width) { + BUG("width=%d is not valid with scaling factor %.2f (%d != %d)", + width, scale, + (int)roundf(scale * (int)roundf(width / scale)), + width); + } + + if ((int)roundf(scale * (int)roundf(height / scale)) != height) { + BUG("height=%d is not valid with scaling factor %.2f (%d != %d)", + height, scale, + (int)roundf(scale * (int)roundf(height / scale)), + height); + } + } wl_surface_set_buffer_scale(surf->surf, 1); wp_viewport_set_destination( @@ -2034,12 +2047,20 @@ wayl_surface_scale_explicit_width_height( } } +void +wayl_surface_scale_explicit_width_height( + const struct wl_window *win, const struct wayl_surface *surf, + int width, int height, float scale) +{ + surface_scale_explicit_width_height(win, surf, width, height, scale, false); +} + void wayl_surface_scale(const struct wl_window *win, const struct wayl_surface *surf, const struct buffer *buf, float scale) { - wayl_surface_scale_explicit_width_height( - win, surf, buf->width, buf->height, scale); + surface_scale_explicit_width_height( + win, surf, buf->width, buf->height, scale, true); } void From aea687c0a1d6b14a302aeb7916358abb0ba10b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 29 Jul 2023 09:09:59 +0200 Subject: [PATCH 0447/1323] changelog: CSDs with fractional scaling --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a60ccef5..6a5bd8be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,11 +71,13 @@ ([#1430][1430]). * Crash when compositor does not implement the _viewporter_ interface ([#1444][1444]). +* CSD rendering with fractional scaling ([#1441][1441]). [1423]: https://codeberg.org/dnkl/foot/issues/1423 [1431]: https://codeberg.org/dnkl/foot/issues/1431 [1430]: https://codeberg.org/dnkl/foot/issues/1430 [1444]: https://codeberg.org/dnkl/foot/issues/1444 +[1441]: https://codeberg.org/dnkl/foot/issues/1441 ### Security From 1af0277564f6347caa85fda4b46071c0cf03234c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 30 Jul 2023 07:53:33 +0200 Subject: [PATCH 0448/1323] --window-size-chars: ensure width/height are valid for current scaling factor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this patch, we didn’t ensure width and height were valid for the current scaling factor, when fractional scaling _is_ available. That is, we didn’t ensure the width/height values multiplied back to their original values after dividing with the scaling factor. Closes #1446 --- CHANGELOG.md | 4 ++++ render.c | 19 +++---------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a5bd8be..916880c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,12 +72,16 @@ * Crash when compositor does not implement the _viewporter_ interface ([#1444][1444]). * CSD rendering with fractional scaling ([#1441][1441]). +* Regression: crash with certain combinations of + `--window-size-chars=NxM` and desktop scaling factors + ([#1446][1446]). [1423]: https://codeberg.org/dnkl/foot/issues/1423 [1431]: https://codeberg.org/dnkl/foot/issues/1431 [1430]: https://codeberg.org/dnkl/foot/issues/1430 [1444]: https://codeberg.org/dnkl/foot/issues/1444 [1441]: https://codeberg.org/dnkl/foot/issues/1441 +[1446]: https://codeberg.org/dnkl/foot/issues/1446 ### Security diff --git a/render.c b/render.c index 893b461a..5c6eeb92 100644 --- a/render.c +++ b/render.c @@ -3933,22 +3933,9 @@ maybe_resize(struct terminal *term, int width, int height, bool force) width += 2 * term->conf->pad_x * scale; height += 2 * term->conf->pad_y * scale; - /* - * Ensure we can scale to logical size, and back to - * pixels without truncating. - */ - if (!term_fractional_scaling(term)) { - xassert((int)ceilf(scale) == (int)scale); - - int iscale = scale; - if (width % iscale) - width += iscale - width % iscale; - if (height % iscale) - height += iscale - height % iscale; - - xassert(width % iscale == 0); - xassert(height % iscale == 0); - } + /* Ensure width/height is a valid multiple of scale */ + width = roundf(scale * roundf(width / scale)); + height = roundf(scale * roundf(height / scale)); break; } } From 3111bc89e507034df44d46fb64efba224178a38c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 30 Jul 2023 13:18:41 +0200 Subject: [PATCH 0449/1323] changelog: prepare for 1.15.2 --- CHANGELOG.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 916880c0..975a3704 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.15.2](#1-15-2) * [1.15.1](#1-15-1) * [1.15.0](#1-15-0) * [1.14.0](#1-14-0) @@ -44,7 +44,8 @@ * [1.2.0](#1-2-0) -## Unreleased +## 1.15.2 + ### Added * `[tweak].bold-text-in-bright-amount` option ([#1434][1434]). @@ -57,9 +58,6 @@ [1434]: https://codeberg.org/dnkl/foot/issues/1434 -### Changed -### Deprecated -### Removed ### Fixed * Crash when copying text that contains invalid UTF-8 ([#1423][1423]). @@ -84,10 +82,6 @@ [1446]: https://codeberg.org/dnkl/foot/issues/1446 -### Security -### Contributors - - ## 1.15.1 ### Changed From 53a5d62e5a1950ca7caa21ec395a763e65c2dba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 30 Jul 2023 13:18:55 +0200 Subject: [PATCH 0450/1323] meson: bump version to 1.15.2 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 87af1edd..2792533a 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.15.1', + version: '1.15.2', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From 33dcb4d49a76e7736e5548265186e91ef1438423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 30 Jul 2023 13:27:42 +0200 Subject: [PATCH 0451/1323] =?UTF-8?q?changelog:=20add=20new=20=E2=80=98unr?= =?UTF-8?q?eleased=20section=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 975a3704..9ae61a47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.15.2](#1-15-2) * [1.15.1](#1-15-1) * [1.15.0](#1-15-0) @@ -44,6 +45,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.15.2 ### Added From 0b4f1b4af20cea0afae39a2c1c4e42c22962dcde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 31 Jul 2023 16:26:17 +0200 Subject: [PATCH 0452/1323] main: translate command line options to overrides Instead of special casing configuration affecting command line options (like --font, --fullscreen, --maximized etc), translate them to overrides, and let the configuration system handle them. This also fixes an issue where -f,--font did not set csd.font, if csd.font were otherwise unset. --- CHANGELOG.md | 4 ++ main.c | 107 +++++++++------------------------------------------ 2 files changed, 22 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ae61a47..ad90b71b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,10 @@ ### Deprecated ### Removed ### Fixed + +* `-f,--font` command line option not affecting `csd.font` (if unset). + + ### Security ### Contributors diff --git a/main.c b/main.c index fc329574..38e3c148 100644 --- a/main.c +++ b/main.c @@ -222,21 +222,11 @@ main(int argc, char *const *argv) bool check_config = false; const char *conf_path = NULL; - const char *conf_term = NULL; - const char *conf_title = NULL; - const char *conf_app_id = NULL; const char *custom_cwd = NULL; - bool login_shell = false; - tll(char *) conf_fonts = tll_init(); - enum conf_size_type conf_size_type = CONF_SIZE_PX; - int conf_width = -1; - int conf_height = -1; bool as_server = false; const char *conf_server_socket_path = NULL; bool presentation_timings = false; bool hold = false; - bool maximized = false; - bool fullscreen = false; bool unlink_pid_file = false; const char *pid_file = NULL; enum log_class log_level = LOG_CLASS_WARNING; @@ -261,23 +251,23 @@ main(int argc, char *const *argv) break; case 'o': - tll_push_back(overrides, optarg); + tll_push_back(overrides, xstrdup(optarg)); break; case 't': - conf_term = optarg; + tll_push_back(overrides, xasprintf("term=%s", optarg)); break; case 'L': - login_shell = true; + tll_push_back(overrides, xstrdup("login-shell=yes")); break; case 'T': - conf_title = optarg; + tll_push_back(overrides, xasprintf("title%s", optarg)); break; case 'a': - conf_app_id = optarg; + tll_push_back(overrides, xasprintf("app-id=%s", optarg)); break; case 'D': { @@ -290,27 +280,11 @@ main(int argc, char *const *argv) break; } - case 'f': - tll_free_and_free(conf_fonts, free); - for (char *font = strtok(optarg, ","); font != NULL; font = strtok(NULL, ",")) { - - /* Strip leading spaces */ - while (*font != '\0' && isspace(*font)) - font++; - - /* Strip trailing spaces */ - char *end = font + strlen(font); - xassert(*end == '\0'); - end--; - while (end > font && isspace(*end)) - *(end--) = '\0'; - - if (strlen(font) == 0) - continue; - - tll_push_back(conf_fonts, font); - } + case 'f': { + char *font_override = xasprintf("font=%s", optarg); + tll_push_back(overrides, font_override); break; + } case 'w': { unsigned width, height; @@ -319,9 +293,9 @@ main(int argc, char *const *argv) return ret; } - conf_size_type = CONF_SIZE_PX; - conf_width = width; - conf_height = height; + tll_push_back( + overrides, xasprintf("initial-window-size-pixels=%ux%u", + width, height)); break; } @@ -332,9 +306,9 @@ main(int argc, char *const *argv) return ret; } - conf_size_type = CONF_SIZE_CELLS; - conf_width = width; - conf_height = height; + tll_push_back( + overrides, xasprintf("initial-window-size-chars=%ux%u", + width, height)); break; } @@ -353,13 +327,11 @@ main(int argc, char *const *argv) break; case 'm': - maximized = true; - fullscreen = false; + tll_push_back(overrides, xstrdup("initial-window-mode=maximized")); break; case 'F': - fullscreen = true; - maximized = false; + tll_push_back(overrides, xstrdup("initial-window-mode=fullscreen")); break; case 'p': @@ -494,7 +466,7 @@ main(int argc, char *const *argv) bool conf_successful = config_load( &conf, conf_path, &user_notifications, &overrides, check_config, as_server); - tll_free(overrides); + tll_free_and_free(overrides, free); if (!conf_successful) { config_free(&conf); return ret; @@ -515,53 +487,10 @@ main(int argc, char *const *argv) (enum fcft_log_class)log_level); fcft_set_scaling_filter(conf.tweak.fcft_filter); - if (conf_term != NULL) { - free(conf.term); - conf.term = xstrdup(conf_term); - } - if (conf_title != NULL) { - free(conf.title); - conf.title = xstrdup(conf_title); - } - if (conf_app_id != NULL) { - free(conf.app_id); - conf.app_id = xstrdup(conf_app_id); - } - if (login_shell) - conf.login_shell = true; - if (tll_length(conf_fonts) > 0) { - for (size_t i = 0; i < ALEN(conf.fonts); i++) - config_font_list_destroy(&conf.fonts[i]); - - struct config_font_list *font_list = &conf.fonts[0]; - xassert(font_list->count == 0); - xassert(font_list->arr == NULL); - - font_list->arr = xmalloc( - tll_length(conf_fonts) * sizeof(font_list->arr[0])); - - tll_foreach(conf_fonts, it) { - struct config_font font; - if (!config_font_parse(it->item, &font)) { - LOG_ERR("%s: invalid font specification", it->item); - } else - font_list->arr[font_list->count++] = font; - } - tll_free(conf_fonts); - } - if (conf_width > 0 && conf_height > 0) { - conf.size.type = conf_size_type; - conf.size.width = conf_width; - conf.size.height = conf_height; - } if (conf_server_socket_path != NULL) { free(conf.server_socket_path); conf.server_socket_path = xstrdup(conf_server_socket_path); } - if (maximized) - conf.startup_mode = STARTUP_MAXIMIZED; - else if (fullscreen) - conf.startup_mode = STARTUP_FULLSCREEN; conf.presentation_timings = presentation_timings; conf.hold_at_exit = hold; From ddcbf2a7b40bcbd6130339425ab328d6cec96a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 31 Jul 2023 16:28:07 +0200 Subject: [PATCH 0453/1323] config: remove deprecated option 'utempter' --- CHANGELOG.md | 4 ++++ config.c | 18 +----------------- tests/test-config.c | 1 - 3 files changed, 5 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad90b71b..0142a6eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,10 @@ ### Changed ### Deprecated ### Removed + +* `utempter` config (was deprecated in 1.15.0). + + ### Fixed * `-f,--font` command line option not affecting `csd.font` (if unset). diff --git a/config.c b/config.c index 302e30f0..129fe99a 100644 --- a/config.c +++ b/config.c @@ -1000,23 +1000,7 @@ parse_section_main(struct context *ctx) else if (strcmp(key, "box-drawings-uses-font-glyphs") == 0) return value_to_bool(ctx, &conf->box_drawings_uses_font_glyphs); - else if (strcmp(key, "utmp-helper") == 0 || strcmp(key, "utempter") == 0) { - if (strcmp(key, "utempter") == 0) { - struct user_notification deprecation = { - .kind = USER_NOTIFICATION_DEPRECATED, - .text = xasprintf( - "%s:%d: \033[1m[main].utempter\033[22m, " - "use \033[1m[main].utmp-helper\033[22m instead", - ctx->path, ctx->lineno), - }; - tll_push_back(conf->notifications, deprecation); - - LOG_WARN( - "%s:%d: [main].utempter is deprecated, " - "use [main].utmp-helper instead", - ctx->path, ctx->lineno); - } - + else if (strcmp(key, "utmp-helper") == 0) { if (!value_to_str(ctx, &conf->utmp_helper_path)) return false; diff --git a/tests/test-config.c b/tests/test-config.c index 2ae891e8..666f4689 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -502,7 +502,6 @@ test_section_main(void) test_string(&ctx, &parse_section_main, "shell", &conf.shell); test_string(&ctx, &parse_section_main, "term", &conf.term); test_string(&ctx, &parse_section_main, "app-id", &conf.app_id); - test_string(&ctx, &parse_section_main, "utempter", &conf.utmp_helper_path); test_string(&ctx, &parse_section_main, "utmp-helper", &conf.utmp_helper_path); test_c32string(&ctx, &parse_section_main, "word-delimiters", &conf.word_delimiters); From 90ad3d6491670ecc38aa5dcf02b9e1dc626bbf51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 31 Jul 2023 16:29:08 +0200 Subject: [PATCH 0454/1323] render: OSD: center text vertically Rewrite render_osd(), and instead of passing in an y-offset, let render_osd() itself center the text inside the OSD buffer. This is done using the same baseline calculation term_font_baseline() does, except we use the buffer height instead of the line height. Note that most OSDs are sized based on the line height... Closes #1430 --- CHANGELOG.md | 4 ++++ render.c | 25 ++++++++++++++++--------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0142a6eb..c9abe279 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,10 @@ ### Fixed * `-f,--font` command line option not affecting `csd.font` (if unset). +* Vertical alignment in URL jump labels, and the scrollback position + indicator. The fix in 1.15.2 was incorrect, and was reverted in the + last minute. But we forgot to remove the entry from the changelog + ([#1430][1430]). ### Security diff --git a/render.c b/render.c index 5c6eeb92..05d29c25 100644 --- a/render.c +++ b/render.c @@ -1890,7 +1890,7 @@ static void render_osd(struct terminal *term, const struct wayl_sub_surface *sub_surf, struct fcft_font *font, struct buffer *buf, const char32_t *text, uint32_t _fg, uint32_t _bg, - unsigned x, unsigned y) + unsigned x) { pixman_region32_t clip; pixman_region32_init_rect(&clip, 0, 0, buf->width, buf->height); @@ -1938,18 +1938,27 @@ render_osd(struct terminal *term, const struct wayl_sub_surface *sub_surf, pixman_image_t *src = pixman_image_create_solid_fill(&fg); + /* Calculate baseline */ + unsigned y; + { + const int line_height = buf->height; + const int font_height = max(font->height, font->ascent + font->descent); + const int glyph_top_y = round((line_height - font_height) / 2.); + y = term->font_y_ofs + glyph_top_y + font->ascent; + } + for (size_t i = 0; i < glyph_count; i++) { const struct fcft_glyph *glyph = glyphs[i]; if (pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8) { pixman_image_composite32( PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0, - x + x_ofs + glyph->x, y + term->font_y_ofs + font->ascent /*term_font_baseline(term)*/ - glyph->y, + x + x_ofs + glyph->x, y - glyph->y, glyph->width, glyph->height); } else { pixman_image_composite32( PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, 0, 0, 0, - x + x_ofs + glyph->x, y + term->font_y_ofs + font->ascent /* term_font_baseline(term)*/ - glyph->y, + x + x_ofs + glyph->x, y - glyph->y, glyph->width, glyph->height); } @@ -2011,9 +2020,7 @@ render_csd_title(struct terminal *term, const struct csd_data *info, const int margin = M != NULL ? M->advance.x : win->csd.font->max_advance.x; - render_osd(term, surf, win->csd.font, buf, title_text, fg, bg, margin, - (buf->height - win->csd.font->height) / 2); - + render_osd(term, surf, win->csd.font, buf, title_text, fg, bg, margin); csd_commit(term, &surf->surface, buf); free(_title_text); } @@ -2580,7 +2587,7 @@ render_scrollback_position(struct terminal *term) &win->scrollback_indicator, term->fonts[0], buf, text, fg, 0xffu << 24 | bg, - width - margin - c32len(text) * term->cell_width, margin); + width - margin - c32len(text) * term->cell_width); } static void @@ -2618,7 +2625,7 @@ render_render_timer(struct terminal *term, struct timespec render_time) &win->render_timer, term->fonts[0], buf, text, term->colors.table[0], 0xffu << 24 | term->colors.table[8 + 1], - margin, margin); + margin); } static void frame_callback( @@ -3664,7 +3671,7 @@ render_urls(struct terminal *term) render_osd( term, sub_surf, term->fonts[0], bufs[i], label, - fg, 0xffu << 24 | bg, x_margin, y_margin); + fg, 0xffu << 24 | bg, x_margin); free(info[i].text); } From 12e0edd6e16ae4f59ee32d282ed56ab0c7e26a41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 5 Aug 2023 07:19:51 +0200 Subject: [PATCH 0455/1323] vt: fix ASAN UB warning ../vt.c:648:13: runtime error: signed integer overflow: 3924432811 * 2654435761 cannot be represented in type 'long' SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../vt.c:648:13 in Closes #1456 --- vt.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vt.c b/vt.c index 772bd41f..f6e8b79b 100644 --- a/vt.c +++ b/vt.c @@ -645,7 +645,7 @@ chain_key(uint32_t old_key, uint32_t new_wc) new_key ^= new_wc; /* Multiply with magic hash constant */ - new_key *= 2654435761; + new_key *= 2654435761ul; /* And mask, to ensure the new value is within range */ new_key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO; From be22736f230f4aa3d2052e0e7a918365c40ac4b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 5 Aug 2023 07:23:11 +0200 Subject: [PATCH 0456/1323] =?UTF-8?q?main:=20=E2=80=9Ctitle%s=E2=80=9D=20-?= =?UTF-8?q?>=20=E2=80=9Ctitle=3D%s=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix regression of --title,-T option. This broken when command line parsing was switched to using overrides, in 0b4f1b4af20cea0afae39a2c1c4e42c22962dcde. Closes #1457 --- main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.c b/main.c index 38e3c148..dffd2b2b 100644 --- a/main.c +++ b/main.c @@ -263,7 +263,7 @@ main(int argc, char *const *argv) break; case 'T': - tll_push_back(overrides, xasprintf("title%s", optarg)); + tll_push_back(overrides, xasprintf("title=%s", optarg)); break; case 'a': From eea21070eef9a513702cfd292dccae634d736317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 5 Aug 2023 07:25:36 +0200 Subject: [PATCH 0457/1323] =?UTF-8?q?changelog:=20=E2=80=9Cconfig=E2=80=9D?= =?UTF-8?q?=20->=20=E2=80=9Cconfig=20option=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9abe279..0783c3b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,7 +51,7 @@ ### Deprecated ### Removed -* `utempter` config (was deprecated in 1.15.0). +* `utempter` config option (was deprecated in 1.15.0). ### Fixed From e1d66ad0c1a444ed497e614b039d0668dd1c8a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 30 Jul 2023 13:27:42 +0200 Subject: [PATCH 0458/1323] =?UTF-8?q?changelog:=20add=20new=20=E2=80=98unr?= =?UTF-8?q?eleased=20section=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 975a3704..9ae61a47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.15.2](#1-15-2) * [1.15.1](#1-15-1) * [1.15.0](#1-15-0) @@ -44,6 +45,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.15.2 ### Added From e56725044961248006350ea1e8455780c57a3f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 31 Jul 2023 16:26:17 +0200 Subject: [PATCH 0459/1323] main: translate command line options to overrides Instead of special casing configuration affecting command line options (like --font, --fullscreen, --maximized etc), translate them to overrides, and let the configuration system handle them. This also fixes an issue where -f,--font did not set csd.font, if csd.font were otherwise unset. --- CHANGELOG.md | 4 ++ main.c | 107 +++++++++------------------------------------------ 2 files changed, 22 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ae61a47..ad90b71b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,10 @@ ### Deprecated ### Removed ### Fixed + +* `-f,--font` command line option not affecting `csd.font` (if unset). + + ### Security ### Contributors diff --git a/main.c b/main.c index fc329574..38e3c148 100644 --- a/main.c +++ b/main.c @@ -222,21 +222,11 @@ main(int argc, char *const *argv) bool check_config = false; const char *conf_path = NULL; - const char *conf_term = NULL; - const char *conf_title = NULL; - const char *conf_app_id = NULL; const char *custom_cwd = NULL; - bool login_shell = false; - tll(char *) conf_fonts = tll_init(); - enum conf_size_type conf_size_type = CONF_SIZE_PX; - int conf_width = -1; - int conf_height = -1; bool as_server = false; const char *conf_server_socket_path = NULL; bool presentation_timings = false; bool hold = false; - bool maximized = false; - bool fullscreen = false; bool unlink_pid_file = false; const char *pid_file = NULL; enum log_class log_level = LOG_CLASS_WARNING; @@ -261,23 +251,23 @@ main(int argc, char *const *argv) break; case 'o': - tll_push_back(overrides, optarg); + tll_push_back(overrides, xstrdup(optarg)); break; case 't': - conf_term = optarg; + tll_push_back(overrides, xasprintf("term=%s", optarg)); break; case 'L': - login_shell = true; + tll_push_back(overrides, xstrdup("login-shell=yes")); break; case 'T': - conf_title = optarg; + tll_push_back(overrides, xasprintf("title%s", optarg)); break; case 'a': - conf_app_id = optarg; + tll_push_back(overrides, xasprintf("app-id=%s", optarg)); break; case 'D': { @@ -290,27 +280,11 @@ main(int argc, char *const *argv) break; } - case 'f': - tll_free_and_free(conf_fonts, free); - for (char *font = strtok(optarg, ","); font != NULL; font = strtok(NULL, ",")) { - - /* Strip leading spaces */ - while (*font != '\0' && isspace(*font)) - font++; - - /* Strip trailing spaces */ - char *end = font + strlen(font); - xassert(*end == '\0'); - end--; - while (end > font && isspace(*end)) - *(end--) = '\0'; - - if (strlen(font) == 0) - continue; - - tll_push_back(conf_fonts, font); - } + case 'f': { + char *font_override = xasprintf("font=%s", optarg); + tll_push_back(overrides, font_override); break; + } case 'w': { unsigned width, height; @@ -319,9 +293,9 @@ main(int argc, char *const *argv) return ret; } - conf_size_type = CONF_SIZE_PX; - conf_width = width; - conf_height = height; + tll_push_back( + overrides, xasprintf("initial-window-size-pixels=%ux%u", + width, height)); break; } @@ -332,9 +306,9 @@ main(int argc, char *const *argv) return ret; } - conf_size_type = CONF_SIZE_CELLS; - conf_width = width; - conf_height = height; + tll_push_back( + overrides, xasprintf("initial-window-size-chars=%ux%u", + width, height)); break; } @@ -353,13 +327,11 @@ main(int argc, char *const *argv) break; case 'm': - maximized = true; - fullscreen = false; + tll_push_back(overrides, xstrdup("initial-window-mode=maximized")); break; case 'F': - fullscreen = true; - maximized = false; + tll_push_back(overrides, xstrdup("initial-window-mode=fullscreen")); break; case 'p': @@ -494,7 +466,7 @@ main(int argc, char *const *argv) bool conf_successful = config_load( &conf, conf_path, &user_notifications, &overrides, check_config, as_server); - tll_free(overrides); + tll_free_and_free(overrides, free); if (!conf_successful) { config_free(&conf); return ret; @@ -515,53 +487,10 @@ main(int argc, char *const *argv) (enum fcft_log_class)log_level); fcft_set_scaling_filter(conf.tweak.fcft_filter); - if (conf_term != NULL) { - free(conf.term); - conf.term = xstrdup(conf_term); - } - if (conf_title != NULL) { - free(conf.title); - conf.title = xstrdup(conf_title); - } - if (conf_app_id != NULL) { - free(conf.app_id); - conf.app_id = xstrdup(conf_app_id); - } - if (login_shell) - conf.login_shell = true; - if (tll_length(conf_fonts) > 0) { - for (size_t i = 0; i < ALEN(conf.fonts); i++) - config_font_list_destroy(&conf.fonts[i]); - - struct config_font_list *font_list = &conf.fonts[0]; - xassert(font_list->count == 0); - xassert(font_list->arr == NULL); - - font_list->arr = xmalloc( - tll_length(conf_fonts) * sizeof(font_list->arr[0])); - - tll_foreach(conf_fonts, it) { - struct config_font font; - if (!config_font_parse(it->item, &font)) { - LOG_ERR("%s: invalid font specification", it->item); - } else - font_list->arr[font_list->count++] = font; - } - tll_free(conf_fonts); - } - if (conf_width > 0 && conf_height > 0) { - conf.size.type = conf_size_type; - conf.size.width = conf_width; - conf.size.height = conf_height; - } if (conf_server_socket_path != NULL) { free(conf.server_socket_path); conf.server_socket_path = xstrdup(conf_server_socket_path); } - if (maximized) - conf.startup_mode = STARTUP_MAXIMIZED; - else if (fullscreen) - conf.startup_mode = STARTUP_FULLSCREEN; conf.presentation_timings = presentation_timings; conf.hold_at_exit = hold; From a3d54614c7bc0a88b00bb98ad7bb2f730b0b7e72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 31 Jul 2023 16:29:08 +0200 Subject: [PATCH 0460/1323] render: OSD: center text vertically Rewrite render_osd(), and instead of passing in an y-offset, let render_osd() itself center the text inside the OSD buffer. This is done using the same baseline calculation term_font_baseline() does, except we use the buffer height instead of the line height. Note that most OSDs are sized based on the line height... Closes #1430 --- CHANGELOG.md | 4 ++++ render.c | 25 ++++++++++++++++--------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad90b71b..a7e8fb97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,10 @@ ### Fixed * `-f,--font` command line option not affecting `csd.font` (if unset). +* Vertical alignment in URL jump labels, and the scrollback position + indicator. The fix in 1.15.2 was incorrect, and was reverted in the + last minute. But we forgot to remove the entry from the changelog + ([#1430][1430]). ### Security diff --git a/render.c b/render.c index 5c6eeb92..05d29c25 100644 --- a/render.c +++ b/render.c @@ -1890,7 +1890,7 @@ static void render_osd(struct terminal *term, const struct wayl_sub_surface *sub_surf, struct fcft_font *font, struct buffer *buf, const char32_t *text, uint32_t _fg, uint32_t _bg, - unsigned x, unsigned y) + unsigned x) { pixman_region32_t clip; pixman_region32_init_rect(&clip, 0, 0, buf->width, buf->height); @@ -1938,18 +1938,27 @@ render_osd(struct terminal *term, const struct wayl_sub_surface *sub_surf, pixman_image_t *src = pixman_image_create_solid_fill(&fg); + /* Calculate baseline */ + unsigned y; + { + const int line_height = buf->height; + const int font_height = max(font->height, font->ascent + font->descent); + const int glyph_top_y = round((line_height - font_height) / 2.); + y = term->font_y_ofs + glyph_top_y + font->ascent; + } + for (size_t i = 0; i < glyph_count; i++) { const struct fcft_glyph *glyph = glyphs[i]; if (pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8) { pixman_image_composite32( PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0, - x + x_ofs + glyph->x, y + term->font_y_ofs + font->ascent /*term_font_baseline(term)*/ - glyph->y, + x + x_ofs + glyph->x, y - glyph->y, glyph->width, glyph->height); } else { pixman_image_composite32( PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, 0, 0, 0, - x + x_ofs + glyph->x, y + term->font_y_ofs + font->ascent /* term_font_baseline(term)*/ - glyph->y, + x + x_ofs + glyph->x, y - glyph->y, glyph->width, glyph->height); } @@ -2011,9 +2020,7 @@ render_csd_title(struct terminal *term, const struct csd_data *info, const int margin = M != NULL ? M->advance.x : win->csd.font->max_advance.x; - render_osd(term, surf, win->csd.font, buf, title_text, fg, bg, margin, - (buf->height - win->csd.font->height) / 2); - + render_osd(term, surf, win->csd.font, buf, title_text, fg, bg, margin); csd_commit(term, &surf->surface, buf); free(_title_text); } @@ -2580,7 +2587,7 @@ render_scrollback_position(struct terminal *term) &win->scrollback_indicator, term->fonts[0], buf, text, fg, 0xffu << 24 | bg, - width - margin - c32len(text) * term->cell_width, margin); + width - margin - c32len(text) * term->cell_width); } static void @@ -2618,7 +2625,7 @@ render_render_timer(struct terminal *term, struct timespec render_time) &win->render_timer, term->fonts[0], buf, text, term->colors.table[0], 0xffu << 24 | term->colors.table[8 + 1], - margin, margin); + margin); } static void frame_callback( @@ -3664,7 +3671,7 @@ render_urls(struct terminal *term) render_osd( term, sub_surf, term->fonts[0], bufs[i], label, - fg, 0xffu << 24 | bg, x_margin, y_margin); + fg, 0xffu << 24 | bg, x_margin); free(info[i].text); } From d00a2a222ef92a971e9dbc7dab4d2654399f7ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 5 Aug 2023 07:19:51 +0200 Subject: [PATCH 0461/1323] vt: fix ASAN UB warning ../vt.c:648:13: runtime error: signed integer overflow: 3924432811 * 2654435761 cannot be represented in type 'long' SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../vt.c:648:13 in Closes #1456 --- vt.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vt.c b/vt.c index 772bd41f..f6e8b79b 100644 --- a/vt.c +++ b/vt.c @@ -645,7 +645,7 @@ chain_key(uint32_t old_key, uint32_t new_wc) new_key ^= new_wc; /* Multiply with magic hash constant */ - new_key *= 2654435761; + new_key *= 2654435761ul; /* And mask, to ensure the new value is within range */ new_key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO; From 5334e3d1aae017a92cdd858d0e426ecb813afb55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 5 Aug 2023 07:23:11 +0200 Subject: [PATCH 0462/1323] =?UTF-8?q?main:=20=E2=80=9Ctitle%s=E2=80=9D=20-?= =?UTF-8?q?>=20=E2=80=9Ctitle=3D%s=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix regression of --title,-T option. This broken when command line parsing was switched to using overrides, in 0b4f1b4af20cea0afae39a2c1c4e42c22962dcde. Closes #1457 --- main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.c b/main.c index 38e3c148..dffd2b2b 100644 --- a/main.c +++ b/main.c @@ -263,7 +263,7 @@ main(int argc, char *const *argv) break; case 'T': - tll_push_back(overrides, xasprintf("title%s", optarg)); + tll_push_back(overrides, xasprintf("title=%s", optarg)); break; case 'a': From 341a5eeefdd6cfcd2baaea6af5a44dd1135c559b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 7 Aug 2023 16:39:42 +0200 Subject: [PATCH 0463/1323] changelog: prepare for 1.15.3 --- CHANGELOG.md | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7e8fb97..128b7b6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.15.3](#1-15-3) * [1.15.2](#1-15-2) * [1.15.1](#1-15-1) * [1.15.0](#1-15-0) @@ -45,11 +45,8 @@ * [1.2.0](#1-2-0) -## Unreleased -### Added -### Changed -### Deprecated -### Removed +## 1.15.3 + ### Fixed * `-f,--font` command line option not affecting `csd.font` (if unset). @@ -59,10 +56,6 @@ ([#1430][1430]). -### Security -### Contributors - - ## 1.15.2 ### Added From f3146999454b0d28f19f61491bb33f203fe7c53e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 7 Aug 2023 16:39:54 +0200 Subject: [PATCH 0464/1323] meson: bump version to 1.15.3 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 2792533a..a33202b5 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.15.2', + version: '1.15.3', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From e0475a5421b530a6625b603972071698ac226c1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 31 Jul 2023 16:32:06 +0200 Subject: [PATCH 0465/1323] meson: require wayland-protocols >= 1.32 --- CHANGELOG.md | 7 +++++++ meson.build | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abad4d68..0b8ae645 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,13 @@ ## Unreleased ### Added ### Changed + +* Minimum required version of _wayland-protocols_ is now 1.32 + ([#1391][1391]). + +[1391]: https://codeberg.org/dnkl/foot/issues/1391 + + ### Deprecated ### Removed diff --git a/meson.build b/meson.build index a33202b5..d31202d3 100644 --- a/meson.build +++ b/meson.build @@ -123,7 +123,7 @@ math = cc.find_library('m') threads = [dependency('threads'), cc.find_library('stdthreads', required: false)] libepoll = dependency('epoll-shim', required: false) pixman = dependency('pixman-1') -wayland_protocols = dependency('wayland-protocols') +wayland_protocols = dependency('wayland-protocols', version: '>=1.32') wayland_client = dependency('wayland-client') wayland_cursor = dependency('wayland-cursor') xkb = dependency('xkbcommon', version: '>=1.0.0') From d59a4e7a779dd8bd5cea1e0ec18ce2c326dffbd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 31 Jul 2023 16:32:28 +0200 Subject: [PATCH 0466/1323] wayland: xdg-activation is now always available Since we're requiring wayland-protocols >= 1.32 --- meson.build | 9 +-------- url-mode.c | 4 ---- wayland.c | 20 +------------------- wayland.h | 15 +++------------ 4 files changed, 5 insertions(+), 43 deletions(-) diff --git a/meson.build b/meson.build index d31202d3..c6a9228d 100644 --- a/meson.build +++ b/meson.build @@ -152,15 +152,9 @@ wl_proto_xml = [ wayland_protocols_datadir + '/unstable/primary-selection/primary-selection-unstable-v1.xml', wayland_protocols_datadir + '/stable/presentation-time/presentation-time.xml', wayland_protocols_datadir + '/unstable/text-input/text-input-unstable-v3.xml', + wayland_protocols_datadir + '/staging/xdg-activation/xdg-activation-v1.xml', ] -if wayland_protocols.version().version_compare('>=1.21') - add_project_arguments('-DHAVE_XDG_ACTIVATION', language: 'c') - wl_proto_xml += [wayland_protocols_datadir + '/staging/xdg-activation/xdg-activation-v1.xml'] - xdg_activation = true -else - xdg_activation = false -endif if wayland_protocols.version().version_compare('>=1.31') add_project_arguments('-DHAVE_FRACTIONAL_SCALE', language: 'c') wl_proto_xml += [wayland_protocols_datadir + '/stable/viewporter/viewporter.xml'] @@ -394,7 +388,6 @@ summary( 'Themes': get_option('themes'), 'IME': get_option('ime'), 'Grapheme clustering': utf8proc.found(), - 'Wayland: xdg-activation-v1': xdg_activation, 'Wayland: fractional-scale-v1': fractional_scale, 'Wayland: cursor-shape-v1': cursor_shape, 'utmp backend': utmp_backend, diff --git a/url-mode.c b/url-mode.c index bd9b5157..a5da7ca7 100644 --- a/url-mode.c +++ b/url-mode.c @@ -86,7 +86,6 @@ spawn_url_launcher_with_token(struct terminal *term, return ret; } -#if defined(HAVE_XDG_ACTIVATION) struct spawn_activation_context { struct terminal *term; char *url; @@ -101,13 +100,11 @@ activation_token_done(const char *token, void *data) free(ctx->url); free(ctx); } -#endif static bool spawn_url_launcher(struct seat *seat, struct terminal *term, const char *url, uint32_t serial) { -#if defined(HAVE_XDG_ACTIVATION) struct spawn_activation_context *ctx = xmalloc(sizeof(*ctx)); *ctx = (struct spawn_activation_context){ .term = term, @@ -123,7 +120,6 @@ spawn_url_launcher(struct seat *seat, struct terminal *term, const char *url, free(ctx->url); free(ctx); -#endif return spawn_url_launcher_with_token(term, url, NULL); } diff --git a/wayland.c b/wayland.c index c338bef8..3cfc37d0 100644 --- a/wayland.c +++ b/wayland.c @@ -1217,7 +1217,6 @@ handle_global(void *data, struct wl_registry *registry, } } -#if defined(HAVE_XDG_ACTIVATION) else if (strcmp(interface, xdg_activation_v1_interface.name) == 0) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) @@ -1226,7 +1225,6 @@ handle_global(void *data, struct wl_registry *registry, wayl->xdg_activation = wl_registry_bind( wayl->registry, name, &xdg_activation_v1_interface, required); } -#endif #if defined(HAVE_FRACTIONAL_SCALE) else if (strcmp(interface, wp_viewporter_interface.name) == 0) { @@ -1481,11 +1479,7 @@ wayl_init(struct fdm *fdm, struct key_binding_manager *key_binding_manager, if (wayl->primary_selection_device_manager == NULL) LOG_WARN("no primary selection available"); -#if defined(HAVE_XDG_ACTIVATION) if (wayl->xdg_activation == NULL) { -#else - if (true) { -#endif LOG_WARN( "no XDG activation support; " "bell.urgent will fall back to coloring the window margins red"); @@ -1604,10 +1598,8 @@ wayl_destroy(struct wayland *wayl) if (wayl->cursor_shape_manager != NULL) wp_cursor_shape_manager_v1_destroy(wayl->cursor_shape_manager); #endif -#if defined(HAVE_XDG_ACTIVATION) if (wayl->xdg_activation != NULL) xdg_activation_v1_destroy(wayl->xdg_activation); -#endif if (wayl->xdg_output_manager != NULL) zxdg_output_manager_v1_destroy(wayl->xdg_output_manager); if (wayl->shell != NULL) @@ -1739,11 +1731,9 @@ wayl_win_init(struct terminal *term, const char *token) wl_surface_commit(win->surface.surf); -#if defined(HAVE_XDG_ACTIVATION) /* Complete XDG startup notification */ if (token) xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface.surf); -#endif if (!wayl_win_subsurface_new(win, &win->overlay, false)) { LOG_ERR("failed to create overlay surface"); @@ -1850,14 +1840,13 @@ wayl_win_destroy(struct wl_window *win) shm_purge(term->render.chains.url); shm_purge(term->render.chains.csd); -#if defined(HAVE_XDG_ACTIVATION) tll_foreach(win->xdg_tokens, it) { xdg_activation_token_v1_destroy(it->item->xdg_token); free(it->item); tll_remove(win->xdg_tokens, it); } -#endif + #if defined(HAVE_FRACTIONAL_SCALE) if (win->fractional_scale != NULL) wp_fractional_scale_v1_destroy(win->fractional_scale); @@ -2090,7 +2079,6 @@ wayl_win_alpha_changed(struct wl_window *win) wl_surface_set_opaque_region(win->surface.surf, NULL); } -#if defined(HAVE_XDG_ACTIVATION) static void activation_token_for_urgency_done(const char *token, void *data) { @@ -2100,12 +2088,10 @@ activation_token_for_urgency_done(const char *token, void *data) win->urgency_token_is_pending = false; xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface.surf); } -#endif /* HAVE_XDG_ACTIVATION */ bool wayl_win_set_urgent(struct wl_window *win) { -#if defined(HAVE_XDG_ACTIVATION) if (win->urgency_token_is_pending) { /* We already have a pending token. Don’t request another one, * to avoid flooding the Wayland socket */ @@ -2119,7 +2105,6 @@ wayl_win_set_urgent(struct wl_window *win) win->urgency_token_is_pending = true; return true; } -#endif return false; } @@ -2229,8 +2214,6 @@ wayl_win_subsurface_destroy(struct wayl_sub_surface *surf) } } -#if defined(HAVE_XDG_ACTIVATION) - static void activation_token_done(void *data, struct xdg_activation_token_v1 *xdg_token, const char *token) @@ -2296,4 +2279,3 @@ wayl_get_activation_token( xdg_activation_token_v1_commit(token); return true; } -#endif diff --git a/wayland.h b/wayland.h index 275338a8..c200e79f 100644 --- a/wayland.h +++ b/wayland.h @@ -15,10 +15,7 @@ #include #include #include - -#if defined(HAVE_XDG_ACTIVATION) - #include -#endif +#include #if defined(HAVE_FRACTIONAL_SCALE) #include @@ -345,7 +342,6 @@ struct wl_url { enum csd_mode {CSD_UNKNOWN, CSD_NO, CSD_YES}; -#if defined(HAVE_XDG_ACTIVATION) typedef void (*activation_token_cb_t)(const char *token, void *data); /* @@ -359,7 +355,6 @@ struct xdg_activation_token_context { activation_token_cb_t cb; /* User provided callback */ void *cb_data; /* Callback user pointer */ }; -#endif struct wayland; struct wl_window { @@ -367,10 +362,10 @@ struct wl_window { struct wayl_surface surface; struct xdg_surface *xdg_surface; struct xdg_toplevel *xdg_toplevel; -#if defined(HAVE_XDG_ACTIVATION) + tll(struct xdg_activation_token_context *) xdg_tokens; bool urgency_token_is_pending; -#endif + #if defined(HAVE_FRACTIONAL_SCALE) struct wp_fractional_scale_v1 *fractional_scale; #endif @@ -451,9 +446,7 @@ struct wayland { struct wl_data_device_manager *data_device_manager; struct zwp_primary_selection_device_manager_v1 *primary_selection_device_manager; -#if defined(HAVE_XDG_ACTIVATION) struct xdg_activation_v1 *xdg_activation; -#endif #if defined(HAVE_CURSOR_SHAPE) struct wp_cursor_shape_manager_v1 *cursor_shape_manager; @@ -515,8 +508,6 @@ bool wayl_win_subsurface_new_with_custom_parent( struct wayl_sub_surface *surf, bool allow_pointer_input); void wayl_win_subsurface_destroy(struct wayl_sub_surface *surf); -#if defined(HAVE_XDG_ACTIVATION) bool wayl_get_activation_token( struct wayland *wayl, struct seat *seat, uint32_t serial, struct wl_window *win, activation_token_cb_t cb, void *cb_data); -#endif From 7eee415b7573f5a20852c6e29001945149e920a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 31 Jul 2023 16:32:53 +0200 Subject: [PATCH 0467/1323] wayland: fractional-scale-v1 is now always available Since we're requiring wayland-protocols >= 1.32 --- client.c | 3 +-- foot-features.h | 9 --------- main.c | 3 +-- meson.build | 11 ++--------- terminal.c | 4 ---- wayland.c | 39 +++++---------------------------------- wayland.h | 26 ++++++++------------------ 7 files changed, 17 insertions(+), 78 deletions(-) diff --git a/client.c b/client.c index 41be68a9..4b334d5e 100644 --- a/client.c +++ b/client.c @@ -67,12 +67,11 @@ version_and_features(void) { static char buf[256]; snprintf(buf, sizeof(buf), - "version: %s %cpgo %cime %cgraphemes %cfractional-scaling %ccursor-shape %cassertions", + "version: %s %cpgo %cime %cgraphemes %ccursor-shape %cassertions", FOOT_VERSION, feature_pgo() ? '+' : '-', feature_ime() ? '+' : '-', feature_graphemes() ? '+' : '-', - feature_fractional_scaling() ? '+' : ':', feature_cursor_shape() ? '+' : '-', feature_assertions() ? '+' : '-'); return buf; diff --git a/foot-features.h b/foot-features.h index f8043c12..f810a0fc 100644 --- a/foot-features.h +++ b/foot-features.h @@ -38,15 +38,6 @@ static inline bool feature_graphemes(void) #endif } -static inline bool feature_fractional_scaling(void) -{ -#if defined(HAVE_FRACTIONAL_SCALE) - return true; -#else - return false; -#endif -} - static inline bool feature_cursor_shape(void) { #if defined(HAVE_CURSOR_SHAPE) diff --git a/main.c b/main.c index dffd2b2b..95cbf0b9 100644 --- a/main.c +++ b/main.c @@ -53,12 +53,11 @@ version_and_features(void) { static char buf[256]; snprintf(buf, sizeof(buf), - "version: %s %cpgo %cime %cgraphemes %cfractional-scaling %ccursor-shape %cassertions", + "version: %s %cpgo %cime %cgraphemes %ccursor-shape %cassertions", FOOT_VERSION, feature_pgo() ? '+' : '-', feature_ime() ? '+' : '-', feature_graphemes() ? '+' : '-', - feature_fractional_scaling() ? '+' : '-', feature_cursor_shape() ? '+' : '-', feature_assertions() ? '+' : '-'); return buf; diff --git a/meson.build b/meson.build index c6a9228d..2e3c64ac 100644 --- a/meson.build +++ b/meson.build @@ -153,16 +153,10 @@ wl_proto_xml = [ wayland_protocols_datadir + '/stable/presentation-time/presentation-time.xml', wayland_protocols_datadir + '/unstable/text-input/text-input-unstable-v3.xml', wayland_protocols_datadir + '/staging/xdg-activation/xdg-activation-v1.xml', + wayland_protocols_datadir + '/stable/viewporter/viewporter.xml', + wayland_protocols_datadir + '/staging/fractional-scale/fractional-scale-v1.xml', ] -if wayland_protocols.version().version_compare('>=1.31') - add_project_arguments('-DHAVE_FRACTIONAL_SCALE', language: 'c') - wl_proto_xml += [wayland_protocols_datadir + '/stable/viewporter/viewporter.xml'] - wl_proto_xml += [wayland_protocols_datadir + '/staging/fractional-scale/fractional-scale-v1.xml'] - fractional_scale = true -else - fractional_scale = false -endif if wayland_protocols.version().version_compare('>=1.32') wl_proto_xml += [ wayland_protocols_datadir + '/unstable/tablet/tablet-unstable-v2.xml', # required by cursor-shape-v1 @@ -388,7 +382,6 @@ summary( 'Themes': get_option('themes'), 'IME': get_option('ime'), 'Grapheme clustering': utf8proc.found(), - 'Wayland: fractional-scale-v1': fractional_scale, 'Wayland: cursor-shape-v1': cursor_shape, 'utmp backend': utmp_backend, 'utmp helper default path': utmp_default_helper_path, diff --git a/terminal.c b/terminal.c index d4132c24..bbc98efc 100644 --- a/terminal.c +++ b/terminal.c @@ -2080,11 +2080,7 @@ term_font_size_reset(struct terminal *term) bool term_fractional_scaling(const struct terminal *term) { -#if defined(HAVE_FRACTIONAL_SCALE) return term->wl->fractional_scale_manager != NULL && term->window->scale > 0.; -#else - return false; -#endif } bool diff --git a/wayland.c b/wayland.c index 3cfc37d0..2c664fbf 100644 --- a/wayland.c +++ b/wayland.c @@ -193,10 +193,8 @@ seat_destroy(struct seat *seat) wl_cursor_theme_destroy(seat->pointer.theme); if (seat->pointer.surface.surf != NULL) wl_surface_destroy(seat->pointer.surface.surf); -#if defined(HAVE_FRACTIONAL_SCALE) if (seat->pointer.surface.viewport != NULL) wp_viewport_destroy(seat->pointer.surface.viewport); -#endif if (seat->pointer.xcursor_callback != NULL) wl_callback_destroy(seat->pointer.xcursor_callback); @@ -314,7 +312,6 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat, return; } -#if defined(HAVE_FRACTIONAL_SCALE) if (seat->wayl->viewporter != NULL) { xassert(seat->pointer.surface.viewport == NULL); seat->pointer.surface.viewport = wp_viewporter_get_viewport( @@ -327,7 +324,6 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat, return; } } -#endif seat->wl_pointer = wl_seat_get_pointer(wl_seat); wl_pointer_add_listener(seat->wl_pointer, &pointer_listener, seat); @@ -352,12 +348,10 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat, wl_pointer_release(seat->wl_pointer); wl_surface_destroy(seat->pointer.surface.surf); -#if defined(HAVE_FRACTIONAL_SCALE) if (seat->pointer.surface.viewport != NULL) { wp_viewport_destroy(seat->pointer.surface.viewport); seat->pointer.surface.viewport = NULL; } -#endif if (seat->pointer.theme != NULL) wl_cursor_theme_destroy(seat->pointer.theme); @@ -1226,7 +1220,6 @@ handle_global(void *data, struct wl_registry *registry, wayl->registry, name, &xdg_activation_v1_interface, required); } -#if defined(HAVE_FRACTIONAL_SCALE) else if (strcmp(interface, wp_viewporter_interface.name) == 0) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) @@ -1245,7 +1238,6 @@ handle_global(void *data, struct wl_registry *registry, wayl->registry, name, &wp_fractional_scale_manager_v1_interface, required); } -#endif #if defined(HAVE_CURSOR_SHAPE) else if (strcmp(interface, wp_cursor_shape_manager_v1_interface.name) == 0) { @@ -1485,13 +1477,8 @@ wayl_init(struct fdm *fdm, struct key_binding_manager *key_binding_manager, "bell.urgent will fall back to coloring the window margins red"); } -#if defined(HAVE_FRACTIONAL_SCALE) - if (wayl->fractional_scale_manager == NULL || wayl->viewporter == NULL) { -#else - if (true) { -#endif + if (wayl->fractional_scale_manager == NULL || wayl->viewporter == NULL) LOG_WARN("fractional scaling not available"); - } #if defined(HAVE_CURSOR_SHAPE) if (wayl->cursor_shape_manager == NULL) { @@ -1588,12 +1575,11 @@ wayl_destroy(struct wayland *wayl) zwp_text_input_manager_v3_destroy(wayl->text_input_manager); #endif -#if defined(HAVE_FRACTIONAL_SCALE) if (wayl->fractional_scale_manager != NULL) wp_fractional_scale_manager_v1_destroy(wayl->fractional_scale_manager); if (wayl->viewporter != NULL) wp_viewporter_destroy(wayl->viewporter); -#endif + #if defined(HAVE_CURSOR_SHAPE) if (wayl->cursor_shape_manager != NULL) wp_cursor_shape_manager_v1_destroy(wayl->cursor_shape_manager); @@ -1630,8 +1616,8 @@ wayl_destroy(struct wayland *wayl) free(wayl); } -#if defined(HAVE_FRACTIONAL_SCALE) -static void fractional_scale_preferred_scale( +static void +fractional_scale_preferred_scale( void *data, struct wp_fractional_scale_v1 *wp_fractional_scale_v1, uint32_t scale) { @@ -1651,7 +1637,6 @@ static void fractional_scale_preferred_scale( static const struct wp_fractional_scale_v1_listener fractional_scale_listener = { .preferred_scale = &fractional_scale_preferred_scale, }; -#endif struct wl_window * wayl_win_init(struct terminal *term, const char *token) @@ -1684,7 +1669,6 @@ wayl_win_init(struct terminal *term, const char *token) wl_surface_add_listener(win->surface.surf, &surface_listener, win); -#if defined(HAVE_FRACTIONAL_SCALE) if (wayl->fractional_scale_manager != NULL && wayl->viewporter != NULL) { win->surface.viewport = wp_viewporter_get_viewport(wayl->viewporter, win->surface.surf); @@ -1694,7 +1678,6 @@ wayl_win_init(struct terminal *term, const char *token) wp_fractional_scale_v1_add_listener( win->fractional_scale, &fractional_scale_listener, win); } -#endif win->xdg_surface = xdg_wm_base_get_xdg_surface(wayl->shell, win->surface.surf); xdg_surface_add_listener(win->xdg_surface, &xdg_surface_listener, win); @@ -1847,12 +1830,10 @@ wayl_win_destroy(struct wl_window *win) tll_remove(win->xdg_tokens, it); } -#if defined(HAVE_FRACTIONAL_SCALE) if (win->fractional_scale != NULL) wp_fractional_scale_v1_destroy(win->fractional_scale); if (win->surface.viewport != NULL) wp_viewport_destroy(win->surface.viewport); -#endif if (win->frame_callback != NULL) wl_callback_destroy(win->frame_callback); if (win->xdg_toplevel_decoration != NULL) @@ -1995,7 +1976,6 @@ surface_scale_explicit_width_height( int width, int height, float scale, bool verify) { if (term_fractional_scaling(win->term)) { -#if defined(HAVE_FRACTIONAL_SCALE) LOG_DBG("scaling by a factor of %.2f using fractional scaling " "(width=%d, height=%d) ", scale, width, height); @@ -2018,10 +1998,6 @@ surface_scale_explicit_width_height( wl_surface_set_buffer_scale(surf->surf, 1); wp_viewport_set_destination( surf->viewport, roundf(width / scale), roundf(height / scale)); -#else - BUG("wayl_fraction_scaling() returned true, " - "but fractional scaling was not available at compile time"); -#endif } else { LOG_DBG("scaling by a factor of %.2f using legacy mode " "(width=%d, height=%d)", scale, width, height); @@ -2152,7 +2128,6 @@ wayl_win_subsurface_new_with_custom_parent( return false; } -#if defined(HAVE_FRACTIONAL_SCALE) struct wp_viewport *viewport = NULL; if (wayl->fractional_scale_manager != NULL && wayl->viewporter != NULL) { viewport = wp_viewporter_get_viewport(wayl->viewporter, main_surface); @@ -2163,7 +2138,6 @@ wayl_win_subsurface_new_with_custom_parent( return false; } } -#endif wl_surface_set_user_data(main_surface, win); wl_subsurface_set_sync(sub); @@ -2178,9 +2152,7 @@ wayl_win_subsurface_new_with_custom_parent( surf->surface.surf = main_surface; surf->sub = sub; -#if defined(HAVE_FRACTIONAL_SCALE) surf->surface.viewport = viewport; -#endif return true; } @@ -2198,12 +2170,11 @@ wayl_win_subsurface_destroy(struct wayl_sub_surface *surf) if (surf == NULL) return; -#if defined(HAVE_FRACTIONAL_SCALE) if (surf->surface.viewport != NULL) { wp_viewport_destroy(surf->surface.viewport); surf->surface.viewport = NULL; } -#endif + if (surf->sub != NULL) { wl_subsurface_destroy(surf->sub); surf->sub = NULL; diff --git a/wayland.h b/wayland.h index c200e79f..4130bc09 100644 --- a/wayland.h +++ b/wayland.h @@ -9,18 +9,15 @@ #include /* Wayland protocols */ +#include #include #include #include +#include +#include #include #include #include -#include - -#if defined(HAVE_FRACTIONAL_SCALE) - #include - #include -#endif #include #include @@ -54,9 +51,7 @@ enum touch_state { struct wayl_surface { struct wl_surface *surf; -#if defined(HAVE_FRACTIONAL_SCALE) struct wp_viewport *viewport; -#endif }; struct wayl_sub_surface { @@ -362,15 +357,12 @@ struct wl_window { struct wayl_surface surface; struct xdg_surface *xdg_surface; struct xdg_toplevel *xdg_toplevel; + struct wp_fractional_scale_v1 *fractional_scale; tll(struct xdg_activation_token_context *) xdg_tokens; bool urgency_token_is_pending; -#if defined(HAVE_FRACTIONAL_SCALE) - struct wp_fractional_scale_v1 *fractional_scale; -#endif bool unmapped; - float scale; struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration; @@ -448,6 +440,9 @@ struct wayland { struct xdg_activation_v1 *xdg_activation; + struct wp_viewporter *viewporter; + struct wp_fractional_scale_manager_v1 *fractional_scale_manager; + #if defined(HAVE_CURSOR_SHAPE) struct wp_cursor_shape_manager_v1 *cursor_shape_manager; #endif @@ -455,16 +450,11 @@ struct wayland { bool presentation_timings; struct wp_presentation *presentation; uint32_t presentation_clock_id; - + #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED struct zwp_text_input_manager_v3 *text_input_manager; #endif -#if defined(HAVE_FRACTIONAL_SCALE) - struct wp_viewporter *viewporter; - struct wp_fractional_scale_manager_v1 *fractional_scale_manager; -#endif - bool have_argb8888; tll(struct monitor) monitors; /* All available outputs */ tll(struct seat) seats; From 698c5b54f32c885304d5e2ff55580ce7f52b5da7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 31 Jul 2023 16:33:16 +0200 Subject: [PATCH 0468/1323] wayland: cursor-shape-v1 is now always available Since we're requiring wayland-protocols >= 1.32 --- client.c | 3 +-- cursor-shape.c | 4 ---- cursor-shape.h | 4 ---- foot-features.h | 9 --------- main.c | 3 +-- meson.build | 14 ++------------ render.c | 2 -- terminal.c | 8 ++------ wayland.c | 22 +--------------------- wayland.h | 4 ---- 10 files changed, 7 insertions(+), 66 deletions(-) diff --git a/client.c b/client.c index 4b334d5e..e68f71aa 100644 --- a/client.c +++ b/client.c @@ -67,12 +67,11 @@ version_and_features(void) { static char buf[256]; snprintf(buf, sizeof(buf), - "version: %s %cpgo %cime %cgraphemes %ccursor-shape %cassertions", + "version: %s %cpgo %cime %cgraphemes %cassertions", FOOT_VERSION, feature_pgo() ? '+' : '-', feature_ime() ? '+' : '-', feature_graphemes() ? '+' : '-', - feature_cursor_shape() ? '+' : '-', feature_assertions() ? '+' : '-'); return buf; } diff --git a/cursor-shape.c b/cursor-shape.c index aafeae8b..a5402928 100644 --- a/cursor-shape.c +++ b/cursor-shape.c @@ -34,8 +34,6 @@ cursor_shape_to_string(enum cursor_shape shape) return table[shape]; } -#if defined(HAVE_CURSOR_SHAPE) - enum wp_cursor_shape_device_v1_shape cursor_shape_to_server_shape(enum cursor_shape shape) { @@ -111,5 +109,3 @@ cursor_string_to_server_shape(const char *xcursor) return 0; } - -#endif /* HAVE_CURSOR_SHAPE */ diff --git a/cursor-shape.h b/cursor-shape.h index a9619553..58755382 100644 --- a/cursor-shape.h +++ b/cursor-shape.h @@ -1,8 +1,6 @@ #pragma once -#if defined(HAVE_CURSOR_SHAPE) #include -#endif enum cursor_shape { CURSOR_SHAPE_NONE, @@ -26,9 +24,7 @@ enum cursor_shape { const char *cursor_shape_to_string(enum cursor_shape shape); -#if defined(HAVE_CURSOR_SHAPE) enum wp_cursor_shape_device_v1_shape cursor_shape_to_server_shape( enum cursor_shape shape); enum wp_cursor_shape_device_v1_shape cursor_string_to_server_shape( const char *xcursor); -#endif diff --git a/foot-features.h b/foot-features.h index f810a0fc..ad447767 100644 --- a/foot-features.h +++ b/foot-features.h @@ -37,12 +37,3 @@ static inline bool feature_graphemes(void) return false; #endif } - -static inline bool feature_cursor_shape(void) -{ -#if defined(HAVE_CURSOR_SHAPE) - return true; -#else - return false; -#endif -} diff --git a/main.c b/main.c index 95cbf0b9..631ef167 100644 --- a/main.c +++ b/main.c @@ -53,12 +53,11 @@ version_and_features(void) { static char buf[256]; snprintf(buf, sizeof(buf), - "version: %s %cpgo %cime %cgraphemes %ccursor-shape %cassertions", + "version: %s %cpgo %cime %cgraphemes %cassertions", FOOT_VERSION, feature_pgo() ? '+' : '-', feature_ime() ? '+' : '-', feature_graphemes() ? '+' : '-', - feature_cursor_shape() ? '+' : '-', feature_assertions() ? '+' : '-'); return buf; } diff --git a/meson.build b/meson.build index 2e3c64ac..729d8f03 100644 --- a/meson.build +++ b/meson.build @@ -155,19 +155,10 @@ wl_proto_xml = [ wayland_protocols_datadir + '/staging/xdg-activation/xdg-activation-v1.xml', wayland_protocols_datadir + '/stable/viewporter/viewporter.xml', wayland_protocols_datadir + '/staging/fractional-scale/fractional-scale-v1.xml', + wayland_protocols_datadir + '/unstable/tablet/tablet-unstable-v2.xml', # required by cursor-shape-v1 + wayland_protocols_datadir + '/staging/cursor-shape/cursor-shape-v1.xml', ] -if wayland_protocols.version().version_compare('>=1.32') - wl_proto_xml += [ - wayland_protocols_datadir + '/unstable/tablet/tablet-unstable-v2.xml', # required by cursor-shape-v1 - wayland_protocols_datadir + '/staging/cursor-shape/cursor-shape-v1.xml', - ] - add_project_arguments('-DHAVE_CURSOR_SHAPE', language: 'c') - cursor_shape = true -else - cursor_shape = false -endif - foreach prot : wl_proto_xml wl_proto_headers += custom_target( prot.underscorify() + '-client-header', @@ -382,7 +373,6 @@ summary( 'Themes': get_option('themes'), 'IME': get_option('ime'), 'Grapheme clustering': utf8proc.found(), - 'Wayland: cursor-shape-v1': cursor_shape, 'utmp backend': utmp_backend, 'utmp helper default path': utmp_default_helper_path, 'Build terminfo': tic.found(), diff --git a/render.c b/render.c index 05d29c25..70b3fba0 100644 --- a/render.c +++ b/render.c @@ -4314,7 +4314,6 @@ render_xcursor_update(struct seat *seat) xassert(seat->pointer.cursor != NULL); -#if defined(HAVE_CURSOR_SHAPE) const enum cursor_shape shape = seat->pointer.shape; const char *const xcursor = seat->pointer.last_custom_xcursor; @@ -4344,7 +4343,6 @@ render_xcursor_update(struct seat *seat) return; } } -#endif LOG_DBG("setting %scursor shape using a client-side cursor surface", seat->pointer.shape == CURSOR_SHAPE_CUSTOM ? "custom " : ""); diff --git a/terminal.c b/terminal.c index bbc98efc..2f81adc8 100644 --- a/terminal.c +++ b/terminal.c @@ -3176,12 +3176,8 @@ term_xcursor_update_for_seat(struct terminal *term, struct seat *seat) if (seat->pointer.hidden) shape = CURSOR_SHAPE_HIDDEN; -#if defined(HAVE_CURSOR_SHAPE) - else if (cursor_string_to_server_shape(term->mouse_user_cursor) != 0 -#else - else if (false -#endif - || render_xcursor_is_valid(seat, term->mouse_user_cursor)) + else if (cursor_string_to_server_shape(term->mouse_user_cursor) != 0 || + render_xcursor_is_valid(seat, term->mouse_user_cursor)) { shape = CURSOR_SHAPE_CUSTOM; } diff --git a/wayland.c b/wayland.c index 2c664fbf..d558c489 100644 --- a/wayland.c +++ b/wayland.c @@ -10,14 +10,11 @@ #include #include +#include #include #include #include -#if defined(HAVE_CURSOR_SHAPE) -#include -#endif - #include #define LOG_MODULE "wayland" @@ -210,12 +207,8 @@ seat_destroy(struct seat *seat) zwp_primary_selection_device_v1_destroy(seat->primary_selection_device); if (seat->data_device != NULL) wl_data_device_release(seat->data_device); - -#if defined(HAVE_CURSOR_SHAPE) if (seat->pointer.shape_device != NULL) wp_cursor_shape_device_v1_destroy(seat->pointer.shape_device); -#endif - if (seat->wl_keyboard != NULL) wl_keyboard_release(seat->wl_keyboard); if (seat->wl_pointer != NULL) @@ -328,22 +321,18 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat, seat->wl_pointer = wl_seat_get_pointer(wl_seat); wl_pointer_add_listener(seat->wl_pointer, &pointer_listener, seat); -#if defined(HAVE_CURSOR_SHAPE) if (seat->wayl->cursor_shape_manager != NULL) { xassert(seat->pointer.shape_device == NULL); seat->pointer.shape_device = wp_cursor_shape_manager_v1_get_pointer( seat->wayl->cursor_shape_manager, seat->wl_pointer); } -#endif } } else { if (seat->wl_pointer != NULL) { -#if defined(HAVE_CURSOR_SHAPE) if (seat->pointer.shape_device != NULL) { wp_cursor_shape_device_v1_destroy(seat->pointer.shape_device); seat->pointer.shape_device = NULL; } -#endif wl_pointer_release(seat->wl_pointer); wl_surface_destroy(seat->pointer.surface.surf); @@ -1239,7 +1228,6 @@ handle_global(void *data, struct wl_registry *registry, &wp_fractional_scale_manager_v1_interface, required); } -#if defined(HAVE_CURSOR_SHAPE) else if (strcmp(interface, wp_cursor_shape_manager_v1_interface.name) == 0) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) @@ -1248,7 +1236,6 @@ handle_global(void *data, struct wl_registry *registry, wayl->cursor_shape_manager = wl_registry_bind( wayl->registry, name, &wp_cursor_shape_manager_v1_interface, required); } -#endif #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED else if (strcmp(interface, zwp_text_input_manager_v3_interface.name) == 0) { @@ -1480,11 +1467,7 @@ wayl_init(struct fdm *fdm, struct key_binding_manager *key_binding_manager, if (wayl->fractional_scale_manager == NULL || wayl->viewporter == NULL) LOG_WARN("fractional scaling not available"); -#if defined(HAVE_CURSOR_SHAPE) if (wayl->cursor_shape_manager == NULL) { -#else - if (true) { -#endif LOG_WARN("no server-side cursors available, " "falling back to client-side cursors"); } @@ -1579,11 +1562,8 @@ wayl_destroy(struct wayland *wayl) wp_fractional_scale_manager_v1_destroy(wayl->fractional_scale_manager); if (wayl->viewporter != NULL) wp_viewporter_destroy(wayl->viewporter); - -#if defined(HAVE_CURSOR_SHAPE) if (wayl->cursor_shape_manager != NULL) wp_cursor_shape_manager_v1_destroy(wayl->cursor_shape_manager); -#endif if (wayl->xdg_activation != NULL) xdg_activation_v1_destroy(wayl->xdg_activation); if (wayl->xdg_output_manager != NULL) diff --git a/wayland.h b/wayland.h index 4130bc09..6585191f 100644 --- a/wayland.h +++ b/wayland.h @@ -152,9 +152,7 @@ struct seat { struct wl_cursor *cursor; /* Server-side cursor */ -#if defined(HAVE_CURSOR_SHAPE) struct wp_cursor_shape_device_v1 *shape_device; -#endif float scale; bool hidden; @@ -443,9 +441,7 @@ struct wayland { struct wp_viewporter *viewporter; struct wp_fractional_scale_manager_v1 *fractional_scale_manager; -#if defined(HAVE_CURSOR_SHAPE) struct wp_cursor_shape_manager_v1 *cursor_shape_manager; -#endif bool presentation_timings; struct wp_presentation *presentation; From 34520aa16e3b1f284168f614a0bd0ba92f5eccc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 8 Aug 2023 19:32:45 +0200 Subject: [PATCH 0469/1323] meson: allow building with wayland-protocols as a subproject --- CHANGELOG.md | 4 ++++ meson.build | 26 ++++++++++++++------------ subprojects/wayland-protocols.wrap | 3 +++ 3 files changed, 21 insertions(+), 12 deletions(-) create mode 100644 subprojects/wayland-protocols.wrap diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b8ae645..1c4682b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,10 @@ ## Unreleased ### Added + +* Support for building with _wayland-protocols_ as a subproject. + + ### Changed * Minimum required version of _wayland-protocols_ is now 1.32 diff --git a/meson.build b/meson.build index 729d8f03..8f32225e 100644 --- a/meson.build +++ b/meson.build @@ -123,7 +123,9 @@ math = cc.find_library('m') threads = [dependency('threads'), cc.find_library('stdthreads', required: false)] libepoll = dependency('epoll-shim', required: false) pixman = dependency('pixman-1') -wayland_protocols = dependency('wayland-protocols', version: '>=1.32') +wayland_protocols = dependency('wayland-protocols', version: '>=1.32', + fallback: 'wayland-protocols', + default_options: ['tests=false']) wayland_client = dependency('wayland-client') wayland_cursor = dependency('wayland-cursor') xkb = dependency('xkbcommon', version: '>=1.0.0') @@ -146,17 +148,17 @@ wscanner_prog = find_program( wl_proto_headers = [] wl_proto_src = [] wl_proto_xml = [ - wayland_protocols_datadir + '/stable/xdg-shell/xdg-shell.xml', - wayland_protocols_datadir + '/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml', - wayland_protocols_datadir + '/unstable/xdg-output/xdg-output-unstable-v1.xml', - wayland_protocols_datadir + '/unstable/primary-selection/primary-selection-unstable-v1.xml', - wayland_protocols_datadir + '/stable/presentation-time/presentation-time.xml', - wayland_protocols_datadir + '/unstable/text-input/text-input-unstable-v3.xml', - wayland_protocols_datadir + '/staging/xdg-activation/xdg-activation-v1.xml', - wayland_protocols_datadir + '/stable/viewporter/viewporter.xml', - wayland_protocols_datadir + '/staging/fractional-scale/fractional-scale-v1.xml', - wayland_protocols_datadir + '/unstable/tablet/tablet-unstable-v2.xml', # required by cursor-shape-v1 - wayland_protocols_datadir + '/staging/cursor-shape/cursor-shape-v1.xml', + wayland_protocols_datadir / 'stable/xdg-shell/xdg-shell.xml', + wayland_protocols_datadir / 'unstable/xdg-decoration/xdg-decoration-unstable-v1.xml', + wayland_protocols_datadir / 'unstable/xdg-output/xdg-output-unstable-v1.xml', + wayland_protocols_datadir / 'unstable/primary-selection/primary-selection-unstable-v1.xml', + wayland_protocols_datadir / 'stable/presentation-time/presentation-time.xml', + wayland_protocols_datadir / 'unstable/text-input/text-input-unstable-v3.xml', + wayland_protocols_datadir / 'staging/xdg-activation/xdg-activation-v1.xml', + wayland_protocols_datadir / 'stable/viewporter/viewporter.xml', + wayland_protocols_datadir / 'staging/fractional-scale/fractional-scale-v1.xml', + wayland_protocols_datadir / 'unstable/tablet/tablet-unstable-v2.xml', # required by cursor-shape-v1 + wayland_protocols_datadir / 'staging/cursor-shape/cursor-shape-v1.xml', ] foreach prot : wl_proto_xml diff --git a/subprojects/wayland-protocols.wrap b/subprojects/wayland-protocols.wrap new file mode 100644 index 00000000..74e5e913 --- /dev/null +++ b/subprojects/wayland-protocols.wrap @@ -0,0 +1,3 @@ +[wrap-git] +url = https://gitlab.freedesktop.org/wayland/wayland-protocols.git +revision = main From 14ea4322feea0c649cf06feb503d3b8fd59522cd Mon Sep 17 00:00:00 2001 From: Max Gautier Date: Sat, 29 Jul 2023 10:37:26 +0200 Subject: [PATCH 0470/1323] Order the systemd services after graphical-session.target This fixes services in Wayland session where WAYLAND_DISPLAY is only imported into the systemd user instance environment after graphical-session.target is reached (such as GNOME). --- foot-server.service.in | 1 + foot-server.socket | 1 + 2 files changed, 2 insertions(+) diff --git a/foot-server.service.in b/foot-server.service.in index 47b81267..a9447253 100644 --- a/foot-server.service.in +++ b/foot-server.service.in @@ -8,6 +8,7 @@ Requires=%N.socket Description=Foot terminal server mode Documentation=man:foot(1) PartOf=graphical-session.target +After=graphical-session.target [Install] WantedBy=graphical-session.target diff --git a/foot-server.socket b/foot-server.socket index 997e4363..0c7c1b8f 100644 --- a/foot-server.socket +++ b/foot-server.socket @@ -3,6 +3,7 @@ ListenStream=%t/foot.sock [Unit] PartOf=graphical-session.target +After=graphical-session.target ConditionEnvironment=WAYLAND_DISPLAY [Install] From be4797d619d2b8081ba50685a6241be73265e30a Mon Sep 17 00:00:00 2001 From: Max Gautier Date: Sat, 29 Jul 2023 10:39:11 +0200 Subject: [PATCH 0471/1323] systemd: skip foot-server.service when not in a Wayland context --- foot-server.service.in | 1 + 1 file changed, 1 insertion(+) diff --git a/foot-server.service.in b/foot-server.service.in index a9447253..118b19ab 100644 --- a/foot-server.service.in +++ b/foot-server.service.in @@ -9,6 +9,7 @@ Description=Foot terminal server mode Documentation=man:foot(1) PartOf=graphical-session.target After=graphical-session.target +ConditionEnvironment=WAYLAND_DISPLAY [Install] WantedBy=graphical-session.target From f1075377d3f9e7073a15bb651e3dc2d1809bdba1 Mon Sep 17 00:00:00 2001 From: Max Gautier Date: Sat, 29 Jul 2023 20:54:59 +0200 Subject: [PATCH 0472/1323] changelog: systemd units in GNOME --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c4682b9..f2272050 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,8 +56,12 @@ * Minimum required version of _wayland-protocols_ is now 1.32 ([#1391][1391]). +* `foot-server.service` systemd now checks for + `ConditionEnvironment=WAYLAND_DISPLAY` for consistency with the + socket unit ([#1448][1448]) [1391]: https://codeberg.org/dnkl/foot/issues/1391 +[1448]: https://codeberg.org/dnkl/foot/pulls/1448 ### Deprecated @@ -67,6 +71,13 @@ ### Fixed + +* Race condition for systemd units start in GNOME and KDE + ([#1436][1436]). + +[1436]: https://codeberg.org/dnkl/foot/issues/1436 + + ### Security ### Contributors From 50a28fe1e872deadfe988a13306c792de19a7f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 18 Aug 2023 16:46:09 +0200 Subject: [PATCH 0473/1323] ci: replace 'pipeline' with 'steps' Hopefully fixes: failed to parse pipeline: "pipeline:" got removed, use "steps:" instead --- .woodpecker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index c98ea217..484e718f 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -1,4 +1,4 @@ -pipeline: +steps: codespell: when: branch: From 86ef638102a6e03a5f46326162376c964ad353af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 18 Aug 2023 16:39:00 +0200 Subject: [PATCH 0474/1323] term: improve fallback logic when selecting scaling factor while unmapped The foot window may, for various reasons, become completely unmapped (that is, being removed from all outputs) at run time. One example is wlroots based compositors; they unmap all other windows when an opaque window is fullscreened. 21d99f8dced335826964ca96b8ba7ccac059e598 introduced a regression, where instead of picking the scaling factor from one of the available outputs (at random), we started falling back to '1' as soon as we were unmapped. This patch restores the original logic, but also improves upon it. As soon as a scaling factor has been assigned to the window, we store a copy of it in the term struct ('scale_before_unmap'). When unmapped, we check if it has a valid value (the only time it doesn't is before the initial map). If so, we use it. Only if it hasn't been set do we fall back to picking an output at random, and using its scaling factor. Closes #1464 --- CHANGELOG.md | 3 +++ terminal.c | 21 ++++++++++++++------- terminal.h | 1 + 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2272050..00d66919 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,8 +74,11 @@ * Race condition for systemd units start in GNOME and KDE ([#1436][1436]). +* One frame being rendered at the wrong scale after being hidden by + another opaque, maximized window ([#1464][1464]). [1436]: https://codeberg.org/dnkl/foot/issues/1436 +[1464]: https://codeberg.org/dnkl/foot/issues/1464 ### Security diff --git a/terminal.c b/terminal.c index 2f81adc8..f19da873 100644 --- a/terminal.c +++ b/terminal.c @@ -1161,6 +1161,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .auto_margin = true, .window_title_stack = tll_init(), .scale = 1., + .scale_before_unmap = -1, .flash = {.fd = flash_fd}, .blink = {.fd = -1}, .vt = { @@ -2094,21 +2095,27 @@ term_update_scale(struct terminal *term) * * - “preferred” scale, from the fractional-scale-v1 protocol * - scaling factor of output we most recently were mapped on - * - if we’re not mapped, use the scaling factor from the first - * available output. + * - if we're not mapped, use the last known scaling factor + * - if we're not mapped, and we don't have a last known scaling + * factor, use the scaling factor from the first available + * output. * - if there aren’t any outputs available, use 1.0 */ - const float new_scale = - (term_fractional_scaling(term) - ? win->scale - : (tll_length(win->on_outputs) > 0 + const float new_scale = (term_fractional_scaling(term) + ? win->scale + : tll_length(win->on_outputs) > 0 ? tll_back(win->on_outputs)->scale - : 1.)); + : term->scale_before_unmap > 0. + ? term->scale_before_unmap + : tll_length(term->wl->monitors) > 0 + ? tll_front(term->wl->monitors).scale + : 1.); if (new_scale == term->scale) return false; LOG_DBG("scaling factor changed: %.2f -> %.2f", term->scale, new_scale); + term->scale_before_unmap = new_scale; term->scale = new_scale; return true; } diff --git a/terminal.h b/terminal.h index 25019ecd..0bba6945 100644 --- a/terminal.h +++ b/terminal.h @@ -489,6 +489,7 @@ struct terminal { } blink; float scale; + float scale_before_unmap; /* Last scaling factor used */ int width; /* pixels */ int height; /* pixels */ int stashed_width; From 482a032d1a2816b267371b888501123271f49ca4 Mon Sep 17 00:00:00 2001 From: raggedmyth Date: Sun, 18 Jun 2023 01:43:04 +0000 Subject: [PATCH 0475/1323] add panda theme --- themes/panda | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 themes/panda diff --git a/themes/panda b/themes/panda new file mode 100644 index 00000000..b02c7e9f --- /dev/null +++ b/themes/panda @@ -0,0 +1,27 @@ +# -*- conf -*- +# http://panda.siamak.me/ + +[colors] +# alpha=1.0 +background=1D1E20 +foreground=F0F0F0 + +## Normal/regular colors (color palette 0-7) +regular0=1F1F20 # black +regular1=FB055A # red +regular2=26FFD4 # green +regular3=26FFD4 # yellow +regular4=5C9FFF # blue +regular5=FC59A6 # magenta +regular6=26FFD4 # cyan +regular7=F0F0F0 # white + +## Bright colors (color palette 8-15) +bright0=5C6370 # bright black +bright1=FB055A # bright red +bright2=26FFD4 # bright green +bright3=FEBE7E # bright yellow +bright4=55ADFF # bright blue +bright5=FD95D0 # bright magenta +bright6=26FFD4 # bright cyan +bright7=F0F0F0 # bright white \ No newline at end of file From 4f3f61445799827aa88f69b11ebe2b181a58ca36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 21 Aug 2023 16:26:18 +0200 Subject: [PATCH 0476/1323] url-mode: handle wide chars and grapheme clusters when auto-detecting URLs * Skip spacer cells. This fixes an issue where characters following a double-width character weren't detect properly. * Unpack grapheme clusters (i.e. cells with multiple codepoints), and iterate all their codepoints. Closes #1465 --- CHANGELOG.md | 3 + url-mode.c | 205 +++++++++++++++++++++++++++++---------------------- 2 files changed, 118 insertions(+), 90 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00d66919..0bb25068 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,9 +76,12 @@ ([#1436][1436]). * One frame being rendered at the wrong scale after being hidden by another opaque, maximized window ([#1464][1464]). +* Double-width characters, and grapheme clusters breaking URL + auto-detection ([#1465][1465]). [1436]: https://codeberg.org/dnkl/foot/issues/1436 [1464]: https://codeberg.org/dnkl/foot/issues/1464 +[1465]: https://codeberg.org/dnkl/foot/issues/1465 ### Security diff --git a/url-mode.c b/url-mode.c index a5da7ca7..cc5fa2bb 100644 --- a/url-mode.c +++ b/url-mode.c @@ -326,121 +326,138 @@ auto_detected(const struct terminal *term, enum url_action action, for (int c = 0; c < term->cols; c++) { const struct cell *cell = &row->cells[c]; - char32_t wc = cell->wc; - switch (state) { - case STATE_PROTOCOL: - for (size_t i = 0; i < max_prot_len - 1; i++) { + if (cell->wc >= CELL_SPACER) + continue; + + const char32_t *wcs = NULL; + size_t wc_count = 0; + + if (cell->wc >= CELL_COMB_CHARS_LO && cell->wc <= CELL_COMB_CHARS_HI) { + struct composed *composed = + composed_lookup(term->composed, cell->wc - CELL_COMB_CHARS_LO); + wcs = composed->chars; + wc_count = composed->count; + } else { + wcs = &cell->wc; + wc_count = 1; + } + + for (size_t w_idx = 0; w_idx < wc_count; w_idx++) { + char32_t wc = wcs[w_idx]; + + switch (state) { + case STATE_PROTOCOL: + for (size_t i = 0; i < max_prot_len - 1; i++) { proto_chars[i] = proto_chars[i + 1]; proto_start[i] = proto_start[i + 1]; - } + } - if (proto_char_count >= max_prot_len) + if (proto_char_count >= max_prot_len) proto_char_count = max_prot_len - 1; - proto_chars[max_prot_len - 1] = wc; - proto_start[max_prot_len - 1] = (struct coord){c, r}; - proto_char_count++; + proto_chars[max_prot_len - 1] = wc; + proto_start[max_prot_len - 1] = (struct coord){c, r}; + proto_char_count++; - for (size_t i = 0; i < conf->url.prot_count; i++) { + for (size_t i = 0; i < conf->url.prot_count; i++) { size_t prot_len = c32len(conf->url.protocols[i]); if (proto_char_count < prot_len) - continue; + continue; - const char32_t *proto = &proto_chars[max_prot_len - prot_len]; + const char32_t *proto = + &proto_chars[max_prot_len - prot_len]; - if (c32ncasecmp(conf->url.protocols[i], proto, prot_len) == 0) { - state = STATE_URL; - start = proto_start[max_prot_len - prot_len]; + if (c32ncasecmp(conf->url.protocols[i], proto, prot_len) == + 0) { + state = STATE_URL; + start = proto_start[max_prot_len - prot_len]; - c32ncpy(url, proto, prot_len); - len = prot_len; + c32ncpy(url, proto, prot_len); + len = prot_len; - parenthesis = brackets = ltgts = 0; - break; + parenthesis = brackets = ltgts = 0; + break; } - } - break; + } + break; - case STATE_URL: { - const char32_t *match = bsearch( - &wc, - uri_characters, - uri_characters_count, - sizeof(uri_characters[0]), - &c32cmp_single); + case STATE_URL: { + const char32_t *match = + bsearch(&wc, uri_characters, uri_characters_count, + sizeof(uri_characters[0]), &c32cmp_single); - bool emit_url = false; + bool emit_url = false; - if (match == NULL) { + if (match == NULL) { /* * Character is not a valid URI character. Emit * the URL we’ve collected so far, *without* * including _this_ character. */ emit_url = true; - } else { + } else { xassert(*match == wc); switch (wc) { default: - url[len++] = wc; - break; + url[len++] = wc; + break; case U'(': - parenthesis++; - url[len++] = wc; - break; + parenthesis++; + url[len++] = wc; + break; case U'[': - brackets++; - url[len++] = wc; - break; + brackets++; + url[len++] = wc; + break; case U'<': - ltgts++; - url[len++] = wc; - break; + ltgts++; + url[len++] = wc; + break; case U')': - if (--parenthesis < 0) - emit_url = true; - else - url[len++] = wc; - break; + if (--parenthesis < 0) + emit_url = true; + else + url[len++] = wc; + break; case U']': - if (--brackets < 0) - emit_url = true; - else - url[len++] = wc; - break; + if (--brackets < 0) + emit_url = true; + else + url[len++] = wc; + break; case U'>': - if (--ltgts < 0) - emit_url = true; - else - url[len++] = wc; - break; + if (--ltgts < 0) + emit_url = true; + else + url[len++] = wc; + break; } - } + } - if (c >= term->cols - 1 && row->linebreak) { + if (c >= term->cols - 1 && row->linebreak) { /* * Endpoint is inclusive, and we’ll be subtracting * 1 from the column when emitting the URL. */ c++; emit_url = true; - } + } - if (emit_url) { + if (emit_url) { struct coord end = {c, r}; if (--end.col < 0) { - end.row--; - end.col = term->cols - 1; + end.row--; + end.col = term->cols - 1; } /* Heuristic to remove trailing characters that @@ -448,21 +465,28 @@ auto_detected(const struct terminal *term, enum url_action action, * the end of the URL */ bool done = false; do { - switch (url[len - 1]) { - case U'.': case U',': case U':': case U';': case U'?': - case U'!': case U'"': case U'\'': case U'%': - len--; - end.col--; - if (end.col < 0) { - end.row--; - end.col = term->cols - 1; - } - break; - - default: - done = true; - break; + switch (url[len - 1]) { + case U'.': + case U',': + case U':': + case U';': + case U'?': + case U'!': + case U'"': + case U'\'': + case U'%': + len--; + end.col--; + if (end.col < 0) { + end.row--; + end.col = term->cols - 1; } + break; + + default: + done = true; + break; + } } while (!done); url[len] = U'\0'; @@ -472,25 +496,26 @@ auto_detected(const struct terminal *term, enum url_action action, char *url_utf8 = ac32tombs(url); if (url_utf8 != NULL) { - tll_push_back( - *urls, - ((struct url){ - .id = (uint64_t)rand() << 32 | rand(), - .url = url_utf8, - .range = { - .start = start, - .end = end, - }, - .action = action, - .osc8 = false})); + tll_push_back( + *urls, + ((struct url){.id = (uint64_t)rand() << 32 | rand(), + .url = url_utf8, + .range = + { + .start = start, + .end = end, + }, + .action = action, + .osc8 = false})); } state = STATE_PROTOCOL; len = 0; parenthesis = brackets = ltgts = 0; + } + break; + } } - break; - } } } } From f0f0d02bf74b1ba4279b25883db7b48619242377 Mon Sep 17 00:00:00 2001 From: CismonX Date: Sat, 19 Aug 2023 23:01:53 +0800 Subject: [PATCH 0477/1323] input: improve touch handling on pointer presense No longer inhibits touch event handling when terminal window has pointer focus. Instead, inhibit touch event when at least one pointer button is held down. This change improves user experience when using foot with both a mouse and a touchscreen. Closes #1428. --- input.c | 98 +++++++++++++++++++++++++++++++++++++------------------ wayland.c | 6 ++++ wayland.h | 1 + 3 files changed, 73 insertions(+), 32 deletions(-) diff --git a/input.c b/input.c index 53d7acf8..c95d3f28 100644 --- a/input.c +++ b/input.c @@ -1751,6 +1751,28 @@ mouse_coord_pixel_to_cell(struct seat *seat, const struct terminal *term, seat->mouse.row = (y - term->margins.top) / term->cell_height; } +static bool +touch_is_active(const struct seat *seat) +{ + if (seat->wl_touch == NULL) { + return false; + } + + switch (seat->touch.state) { + case TOUCH_STATE_IDLE: + case TOUCH_STATE_INHIBITED: + return false; + + case TOUCH_STATE_HELD: + case TOUCH_STATE_DRAGGING: + case TOUCH_STATE_SCROLLING: + return true; + } + + BUG("Bad touch state: %d", seat->touch.state); + return false; +} + static void wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface, @@ -1764,26 +1786,15 @@ wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, struct seat *seat = data; - if (seat->wl_touch != NULL) { - switch (seat->touch.state) { - case TOUCH_STATE_IDLE: - mouse_button_state_reset(seat); - seat->touch.state = TOUCH_STATE_INHIBITED; - break; - - case TOUCH_STATE_INHIBITED: - break; - - case TOUCH_STATE_HELD: - case TOUCH_STATE_DRAGGING: - case TOUCH_STATE_SCROLLING: - return; - } - } - struct wl_window *win = wl_surface_get_user_data(surface); struct terminal *term = win->term; + seat->mouse_focus = term; + term->active_surface = term_surface_kind(term, surface); + + if (touch_is_active(seat)) + return; + int x = wl_fixed_to_int(surface_x) * term->scale; int y = wl_fixed_to_int(surface_y) * term->scale; @@ -1799,9 +1810,6 @@ wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, xassert(tll_length(seat->mouse.buttons) == 0); - seat->mouse_focus = term; - term->active_surface = term_surface_kind(term, surface); - wayl_reload_xcursor_theme(seat, term->scale); /* Scale may have changed */ term_xcursor_update_for_seat(term, seat); @@ -1837,10 +1845,19 @@ wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, struct seat *seat = data; if (seat->wl_touch != NULL) { - if (seat->touch.state != TOUCH_STATE_INHIBITED) { + switch (seat->touch.state) { + case TOUCH_STATE_IDLE: + break; + + case TOUCH_STATE_INHIBITED: + seat->touch.state = TOUCH_STATE_IDLE; + break; + + case TOUCH_STATE_HELD: + case TOUCH_STATE_DRAGGING: + case TOUCH_STATE_SCROLLING: return; } - seat->touch.state = TOUCH_STATE_IDLE; } struct terminal *old_moused = seat->mouse_focus; @@ -1919,7 +1936,7 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, struct seat *seat = data; /* Touch-emulated pointer events have wl_pointer == NULL. */ - if (wl_pointer != NULL && seat->touch.state != TOUCH_STATE_INHIBITED) + if (wl_pointer != NULL && touch_is_active(seat)) return; struct wayland *wayl = seat->wayl; @@ -2147,7 +2164,7 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, struct seat *seat = data; /* Touch-emulated pointer events have wl_pointer == NULL. */ - if (wl_pointer != NULL && seat->touch.state != TOUCH_STATE_INHIBITED) + if (wl_pointer != NULL && touch_is_active(seat)) return; struct wayland *wayl = seat->wayl; @@ -2162,6 +2179,10 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, bool send_to_client = false; if (state == WL_POINTER_BUTTON_STATE_PRESSED) { + if (seat->wl_touch != NULL && seat->touch.state == TOUCH_STATE_IDLE) { + seat->touch.state = TOUCH_STATE_INHIBITED; + } + /* Time since last click */ struct timespec now, since_last; clock_gettime(CLOCK_MONOTONIC, &now); @@ -2263,6 +2284,12 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, } } + if (seat->wl_touch != NULL && seat->touch.state == TOUCH_STATE_INHIBITED) { + if (tll_length(seat->mouse.buttons) == 0) { + seat->touch.state = TOUCH_STATE_IDLE; + } + } + if (!have_button) { /* * Seen on Sway with slurp @@ -2610,7 +2637,7 @@ wl_pointer_axis(void *data, struct wl_pointer *wl_pointer, { struct seat *seat = data; - if (seat->touch.state != TOUCH_STATE_INHIBITED) + if (touch_is_active(seat)) return; if (seat->mouse.have_discrete) @@ -2643,7 +2670,7 @@ wl_pointer_axis_discrete(void *data, struct wl_pointer *wl_pointer, { struct seat *seat = data; - if (seat->touch.state != TOUCH_STATE_INHIBITED) + if (touch_is_active(seat)) return; seat->mouse.have_discrete = true; @@ -2663,7 +2690,7 @@ wl_pointer_frame(void *data, struct wl_pointer *wl_pointer) { struct seat *seat = data; - if (seat->touch.state != TOUCH_STATE_INHIBITED) + if (touch_is_active(seat)) return; seat->mouse.have_discrete = false; @@ -2681,7 +2708,7 @@ wl_pointer_axis_stop(void *data, struct wl_pointer *wl_pointer, { struct seat *seat = data; - if (seat->touch.state != TOUCH_STATE_INHIBITED) + if (touch_is_active(seat)) return; xassert(axis < ALEN(seat->mouse.aggregated)); @@ -2738,8 +2765,6 @@ wl_touch_down(void *data, struct wl_touch *wl_touch, uint32_t serial, struct wl_window *win = wl_surface_get_user_data(surface); struct terminal *term = win->term; - term->active_surface = term_surface_kind(term, surface); - LOG_DBG("touch_down: touch=%p, x=%d, y=%d", (void *)wl_touch, wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y)); @@ -2754,6 +2779,7 @@ wl_touch_down(void *data, struct wl_touch *wl_touch, uint32_t serial, seat->touch.serial = serial; seat->touch.time = time + term->conf->touch.long_press_delay; seat->touch.surface = surface; + seat->touch.surface_kind = term_surface_kind(term, surface); seat->touch.id = id; } @@ -2771,7 +2797,10 @@ wl_touch_up(void *data, struct wl_touch *wl_touch, uint32_t serial, struct wl_window *win = wl_surface_get_user_data(seat->touch.surface); struct terminal *term = win->term; + struct terminal *old_term = seat->mouse_focus; + enum term_surface old_active_surface = term->active_surface; seat->mouse_focus = term; + term->active_surface = seat->touch.surface_kind; switch (seat->touch.state) { case TOUCH_STATE_HELD: @@ -2793,7 +2822,8 @@ wl_touch_up(void *data, struct wl_touch *wl_touch, uint32_t serial, break; } - seat->mouse_focus = NULL; + seat->mouse_focus = old_term; + term->active_surface = old_active_surface; } static void @@ -2810,7 +2840,10 @@ wl_touch_motion(void *data, struct wl_touch *wl_touch, uint32_t time, struct wl_window *win = wl_surface_get_user_data(seat->touch.surface); struct terminal *term = win->term; + struct terminal *old_term = seat->mouse_focus; + enum term_surface old_active_surface = term->active_surface; seat->mouse_focus = term; + term->active_surface = seat->touch.surface_kind; switch (seat->touch.state) { case TOUCH_STATE_HELD: @@ -2837,7 +2870,8 @@ wl_touch_motion(void *data, struct wl_touch *wl_touch, uint32_t time, break; } - seat->mouse_focus = NULL; + seat->mouse_focus = old_term; + term->active_surface = old_active_surface; } static void diff --git a/wayland.c b/wayland.c index d558c489..7fcb01c1 100644 --- a/wayland.c +++ b/wayland.c @@ -345,6 +345,12 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat, if (seat->pointer.theme != NULL) wl_cursor_theme_destroy(seat->pointer.theme); + if (seat->wl_touch != NULL && + seat->touch.state == TOUCH_STATE_INHIBITED) + { + seat->touch.state = TOUCH_STATE_IDLE; + } + seat->wl_pointer = NULL; seat->pointer.surface.surf = NULL; seat->pointer.theme = NULL; diff --git a/wayland.h b/wayland.h index 6585191f..67aadec2 100644 --- a/wayland.h +++ b/wayland.h @@ -171,6 +171,7 @@ struct seat { uint32_t serial; uint32_t time; struct wl_surface *surface; + int surface_kind; int32_t id; } touch; From fe7aa25ad82f5551f0a84bb98df8ccbac39a8f79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 18 Sep 2023 16:36:39 +0200 Subject: [PATCH 0478/1323] input: make wheel events mappable Un-grabbed wheel events are now passed through the mouse binding matching logic, instead of being hardcoded to scrolling the terminal contents. They are mappable through the BTN_BACK and BTN_FORWARD buttons. Since they're not actually button *presses*, they never generate a click count other than 1. This limitation is documented, but not checked in the config. This means it's possible to create bindings like "BTN_BACK+3" (i.e. triple "click"). They will however never trigger. The old, hardcoded logic is now accessible through the new scrollback-up-mouse and scrollback-down-mouse mouse bindings. They (obiously) default to BTN_BACK and BTN_FORWARD, respectively. Example usage: keep the default of scrolling terminal contents with the wheel, when used without modifiers, but map Control+wheel to font zoom in/out: [mouse-bindings] font-increase=Control+BTN_FORWARD font-decrease=Control+BTN_BACK (this also keeps the default key bindings to zoom in/out; ctrl-+ and ctrl+-) Closes #1077 --- CHANGELOG.md | 7 ++ config.c | 4 + doc/foot.ini.5.scd | 27 +++++- foot.ini | 2 + input.c | 225 +++++++++++++++++++++++++-------------------- key-binding.h | 2 + 6 files changed, 162 insertions(+), 105 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bb25068..31a28a48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,13 @@ ### Added * Support for building with _wayland-protocols_ as a subproject. +* Mouse wheel scrolls can now be used in `mouse-bindings` + ([#1077](1077)). +* New mouse bindings: `scrollback-up-mouse` and + `scrollback-down-mouse`, bound to `BTN_BACK` and `BTN_FORWARD` + respectively. + +[1077]: https://codeberg.org/dnkl/foot/issues/1077 ### Changed diff --git a/config.c b/config.c index 129fe99a..aeea0b32 100644 --- a/config.c +++ b/config.c @@ -120,6 +120,8 @@ static const char *const binding_action_map[] = { [BIND_ACTION_UNICODE_INPUT] = "unicode-input", /* Mouse-specific actions */ + [BIND_ACTION_SCROLLBACK_UP_MOUSE] = "scrollback-up-mouse", + [BIND_ACTION_SCROLLBACK_DOWN_MOUSE] = "scrollback-down-mouse", [BIND_ACTION_SELECT_BEGIN] = "select-begin", [BIND_ACTION_SELECT_BEGIN_BLOCK] = "select-begin-block", [BIND_ACTION_SELECT_EXTEND] = "select-extend", @@ -2871,6 +2873,8 @@ 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}}}, diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 8726da0c..085dd82f 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1032,9 +1032,14 @@ of the modifiers *must* be valid XKB key names, and the button name *must* be a valid libinput name. You can find the button names using *libinput debug-events*. -The trailing *COUNT* is optional and specifies the click count -required to trigger the binding. The default if *COUNT* is omitted is -_1_. +The trailing *COUNT* (number of times the button has to be clicked) is +optional and specifies the click count required to trigger the +binding. The default if *COUNT* is omitted is _1_. + +To map wheel events (i.e. scrolling), use the button names *BTN_BACK* +(up) and *BTN_FORWARD* (down). Note that these events never generate a +*COUNT* larger than 1. That is, *BTN_BACK+2*, for example, will never +trigger. A modifier+button combination can only be mapped to *one* action. Lets say you want to bind *BTN\_MIDDLE* to *fullscreen*. Since @@ -1056,6 +1061,22 @@ _action=none_; e.g. *primary-paste=none*. The actions to which mouse combos can be bound are listed below. All actions listed under *key-bindings* can be used here as well. +*scrollback-up-mouse* + Normal screen: scrolls up the contents. + + Alt screen: send fake _KeyUP_ events to the client application, if + alternate scroll mode is enabled. + + Default: _BTN_BACK_ + +*scrollback-down-mouse* + Normal screen: scrolls down the contents. + + Alt screen: send fake _KeyDOWN_ events to the client application, if + alternate scroll mode is enabled. + + Default: _BTN_FORWARD_ + *select-begin* Begin an interactive selection. The selection is finalized, and copied to the _primary selection_, when the button is diff --git a/foot.ini b/foot.ini index 359b2cf7..262556e7 100644 --- a/foot.ini +++ b/foot.ini @@ -190,6 +190,8 @@ # \x03=Mod4+c # Map Super+c -> Ctrl+c [mouse-bindings] +# scrollback-up-mouse=BTN_BACK +# scrollback-down-mouse=BTN_FORWARD # selection-override-modifiers=Shift # primary-paste=BTN_MIDDLE # select-begin=BTN_LEFT diff --git a/input.c b/input.c index c95d3f28..51b75233 100644 --- a/input.c +++ b/input.c @@ -81,9 +81,11 @@ pipe_closed: return true; } +static void alternate_scroll(struct seat *seat, int amount, int button); + static bool execute_binding(struct seat *seat, struct terminal *term, - const struct key_binding *binding, uint32_t serial) + const struct key_binding *binding, uint32_t serial, int amount) { const enum bind_action_normal action = binding->action; @@ -115,6 +117,14 @@ execute_binding(struct seat *seat, struct terminal *term, } break; + case BIND_ACTION_SCROLLBACK_UP_MOUSE: + if (term->grid == &term->alt) { + if (term->alt_scrolling) + alternate_scroll(seat, amount, BTN_BACK); + } else + cmd_scrollback_up(term, amount); + break; + case BIND_ACTION_SCROLLBACK_DOWN_PAGE: if (term->grid == &term->normal) { cmd_scrollback_down(term, term->rows); @@ -136,6 +146,14 @@ execute_binding(struct seat *seat, struct terminal *term, } break; + case BIND_ACTION_SCROLLBACK_DOWN_MOUSE: + if (term->grid == &term->alt) { + if (term->alt_scrolling) + alternate_scroll(seat, amount, BTN_FORWARD); + } else + cmd_scrollback_down(term, amount); + break; + case BIND_ACTION_SCROLLBACK_HOME: if (term->grid == &term->normal) { cmd_scrollback_up(term, term->grid->num_rows); @@ -1478,7 +1496,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, /* Match translated symbol */ if (bind->k.sym == sym && bind->mods == (bind_mods & ~bind_consumed) && - execute_binding(seat, term, bind, serial)) + execute_binding(seat, term, bind, serial, 1)) { goto maybe_repeat; } @@ -1489,7 +1507,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, /* Match untranslated symbols */ for (size_t i = 0; i < raw_count; i++) { if (bind->k.sym == raw_syms[i] && - execute_binding(seat, term, bind, serial)) + execute_binding(seat, term, bind, serial, 1)) { goto maybe_repeat; } @@ -1498,7 +1516,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, /* Match raw key code */ tll_foreach(bind->k.key_codes, code) { if (code->item == key && - execute_binding(seat, term, bind, serial)) + execute_binding(seat, term, bind, serial, 1)) { goto maybe_repeat; } @@ -2152,6 +2170,95 @@ fdm_csd_move(struct fdm *fdm, int fd, int events, void *data) return true; } +static const struct key_binding * + match_mouse_binding(const struct seat *seat, const struct terminal *term, + int button) +{ + if (seat->wl_keyboard != NULL && seat->kbd.xkb_state != NULL) { + /* Seat has keyboard - use mouse bindings *with* modifiers */ + + const struct key_binding_set *bindings = + key_binding_for(term->wl->key_binding_manager, term->conf, seat); + xassert(bindings != NULL); + + xkb_mod_mask_t mods; + get_current_modifiers(seat, &mods, NULL, 0); + mods &= seat->kbd.bind_significant; + + /* Ignore selection override modifiers when + * matching modifiers */ + mods &= ~bindings->selection_overrides; + + const struct key_binding *match = NULL; + + tll_foreach(bindings->mouse, it) { + const struct key_binding *binding = &it->item; + + if (binding->m.button != button) { + /* Wrong button */ + continue; + } + + if (binding->mods != mods) { + /* Modifier mismatch */ + continue; + } + + if (binding->m.count > seat->mouse.count) { + /* Not correct click count */ + continue; + } + + if (match == NULL || binding->m.count > match->m.count) + match = binding; + } + + return match; + } + + else { + /* Seat does NOT have a keyboard - use mouse bindings *without* + * modifiers */ + const struct config_key_binding *match = NULL; + const struct config *conf = term->conf; + + for (size_t i = 0; i < conf->bindings.mouse.count; i++) { + const struct config_key_binding *binding = + &conf->bindings.mouse.arr[i]; + + if (binding->m.button != button) { + /* Wrong button */ + continue; + } + + if (binding->m.count > seat->mouse.count) { + /* Incorrect click count */ + continue; + } + + const struct config_key_modifiers no_mods = {0}; + if (memcmp(&binding->modifiers, &no_mods, sizeof(no_mods)) != 0) { + /* Binding has modifiers */ + continue; + } + + if (match == NULL || binding->m.count > match->m.count) + match = binding; + } + + if (match != NULL) { + static struct key_binding bind; + bind.action = match->action; + bind.aux = &match->aux; + return &bind; + } + + return NULL; + } + + BUG("should not get here"); +} + static void wl_pointer_button(void *data, struct wl_pointer *wl_pointer, uint32_t serial, uint32_t time, uint32_t button, uint32_t state) @@ -2426,86 +2533,11 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, bool consumed = false; if (cursor_is_on_grid && term_mouse_grabbed(term, seat)) { - if (seat->wl_keyboard != NULL && seat->kbd.xkb_state != NULL) { - /* Seat has keyboard - use mouse bindings *with* modifiers */ + const struct key_binding *match = + match_mouse_binding(seat, term, button); - const struct key_binding_set *bindings = key_binding_for( - wayl->key_binding_manager, term->conf, seat); - xassert(bindings != NULL); - - xkb_mod_mask_t mods; - get_current_modifiers(seat, &mods, NULL, 0); - mods &= seat->kbd.bind_significant; - - /* Ignore selection override modifiers when - * matching modifiers */ - mods &= ~bindings->selection_overrides; - - const struct key_binding *match = NULL; - - tll_foreach(bindings->mouse, it) { - const struct key_binding *binding = &it->item; - - if (binding->m.button != button) { - /* Wrong button */ - continue; - } - - if (binding->mods != mods) { - /* Modifier mismatch */ - continue; - } - - if (binding->m.count > seat->mouse.count) { - /* Not correct click count */ - continue; - } - - if (match == NULL || binding->m.count > match->m.count) - match = binding; - } - - if (match != NULL) - consumed = execute_binding(seat, term, match, serial); - } - - else { - /* Seat does NOT have a keyboard - use mouse bindings *without* modifiers */ - const struct config_key_binding *match = NULL; - const struct config *conf = term->conf; - - for (size_t i = 0; i < conf->bindings.mouse.count; i++) { - const struct config_key_binding *binding = - &conf->bindings.mouse.arr[i]; - - if (binding->m.button != button) { - /* Wrong button */ - continue; - } - - if (binding->m.count > seat->mouse.count) { - /* Incorrect click count */ - continue; - } - - const struct config_key_modifiers no_mods = {0}; - if (memcmp(&binding->modifiers, &no_mods, sizeof(no_mods)) != 0) { - /* Binding has modifiers */ - continue; - } - - if (match == NULL || binding->m.count > match->m.count) - match = binding; - } - - if (match != NULL) { - struct key_binding bind = { - .action = match->action, - .aux = &match->aux, - }; - consumed = execute_binding(seat, term, &bind, serial); - } - } + if (match != NULL) + consumed = execute_binding(seat, term, match, serial, 1); } send_to_client = !consumed && cursor_is_on_grid; @@ -2580,26 +2612,15 @@ mouse_scroll(struct seat *seat, int amount, enum wl_pointer_axis axis) amount = abs(amount); if (term_mouse_grabbed(term, seat)) { - if (term->grid == &term->alt) { - if (term->alt_scrolling) { - switch (button) { - case BTN_BACK: - case BTN_FORWARD: - alternate_scroll(seat, amount, button); - break; - } - } - } else { - switch (button) { - case BTN_BACK: - cmd_scrollback_up(term, amount); - break; + seat->mouse.count = 1; - case BTN_FORWARD: - cmd_scrollback_down(term, amount); - break; - } - } + const struct key_binding *match = + match_mouse_binding(seat, term, button); + + if (match != NULL) + execute_binding(seat, term, match, seat->pointer.serial, amount); + + seat->mouse.last_released_button = button; } else if (seat->mouse.col >= 0 && seat->mouse.row >= 0) { diff --git a/key-binding.h b/key-binding.h index f607644f..4d3ac541 100644 --- a/key-binding.h +++ b/key-binding.h @@ -41,6 +41,8 @@ enum bind_action_normal { BIND_ACTION_UNICODE_INPUT, /* Mouse specific actions - i.e. they require a mouse coordinate */ + BIND_ACTION_SCROLLBACK_UP_MOUSE, + BIND_ACTION_SCROLLBACK_DOWN_MOUSE, BIND_ACTION_SELECT_BEGIN, BIND_ACTION_SELECT_BEGIN_BLOCK, BIND_ACTION_SELECT_EXTEND, From 1719ff93a75ad886ed53be468f7faa12f530e9f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 19 Sep 2023 16:23:34 +0200 Subject: [PATCH 0479/1323] selection: add support for selecting the contents of a quote This patch changes the default of triple clicking, from selecting the current logical row, to first trying to select the contents of the quote under the cursor, and if failing to find a quote, selecting the current row (like before). This is implemented by adding a new key binding, 'select-quote'. It will search for surrounding quote characters, and if one is found on each side of the cursor, the quote is selected. If not, the entire row is selected instead. Subsequent selection operations will behave as if the selection is either a word selection (a quote was found), or a row selection (no quote found). Escaped quote characters are not supported: "foo \" bar" will match 'foo \', and not 'foo " bar'. Mismatched quotes are not custom handled. They will simply not match. Nested quotes ("123 'abc def' 456") are supported. Closes #1364 --- CHANGELOG.md | 8 +++ config.c | 4 +- doc/foot.ini.5.scd | 29 +++++++- foot.ini | 3 +- input.c | 5 ++ key-binding.h | 1 + selection.c | 165 +++++++++++++++++++++++++++++++++++++++++---- terminal.h | 1 + 8 files changed, 199 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31a28a48..a4730e1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,8 +55,12 @@ * New mouse bindings: `scrollback-up-mouse` and `scrollback-down-mouse`, bound to `BTN_BACK` and `BTN_FORWARD` respectively. +* New key binding: `select-quote`. This key binding selects text + between quote characters, and falls back to selecting the entire + row ([#1364][1364]). [1077]: https://codeberg.org/dnkl/foot/issues/1077 +[1364]: https://codeberg.org/dnkl/foot/issues/1364 ### Changed @@ -66,6 +70,10 @@ * `foot-server.service` systemd now checks for `ConditionEnvironment=WAYLAND_DISPLAY` for consistency with the socket unit ([#1448][1448]) +* Default key binding for `select-row` is now `BTN_LEFT+4`. However, + in many cases, triple clicking will still be enough to select the + entire row; see the new key binding `select-quote` (mapped to + `BTN_LEFT+3` by default) ([#1364][1364]). [1391]: https://codeberg.org/dnkl/foot/issues/1391 [1448]: https://codeberg.org/dnkl/foot/pulls/1448 diff --git a/config.c b/config.c index aeea0b32..68dbc679 100644 --- a/config.c +++ b/config.c @@ -128,6 +128,7 @@ static const char *const binding_action_map[] = { [BIND_ACTION_SELECT_EXTEND_CHAR_WISE] = "select-extend-character-wise", [BIND_ACTION_SELECT_WORD] = "select-word", [BIND_ACTION_SELECT_WORD_WS] = "select-word-whitespace", + [BIND_ACTION_SELECT_QUOTE] = "select-quote", [BIND_ACTION_SELECT_ROW] = "select-row", }; @@ -2882,7 +2883,8 @@ add_default_mouse_bindings(struct config *conf) {BIND_ACTION_SELECT_EXTEND_CHAR_WISE, m_ctrl, {.m = {BTN_RIGHT, 1}}}, {BIND_ACTION_SELECT_WORD, m_none, {.m = {BTN_LEFT, 2}}}, {BIND_ACTION_SELECT_WORD_WS, m_ctrl, {.m = {BTN_LEFT, 2}}}, - {BIND_ACTION_SELECT_ROW, m_none, {.m = {BTN_LEFT, 3}}}, + {BIND_ACTION_SELECT_QUOTE, m_none, {.m = {BTN_LEFT, 3}}}, + {BIND_ACTION_SELECT_ROW, m_none, {.m = {BTN_LEFT, 4}}}, }; conf->bindings.mouse.count = ALEN(bindings); diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 085dd82f..6a11d38d 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1101,10 +1101,37 @@ actions listed under *key-bindings* can be used here as well. selection_, when the button is released. Default: _Control+BTN\_LEFT-2_. +*select-quote* + Begin an interactive "quote" selection. This is similar to + *select-word*, except an entire quote is selected (that is, + everything inside the quote, excluding the quote + characters). Recognized quote characters are: *"* and *'*. + + If a complete quote cannot be found on the current logical row + (only one quote character, or none are found), the entire row is + selected. + + The selection is finalized, and copied to the _primary selection_, + when the button is released. + + After the initial selection has been made, it behaves like a + normal word, or row selection, depending on whether a quote was + found or not. This affects what happens when, for example, + extending the selection. + + Notes: + - Escaped quote characters are not supported (*"foo \\"bar"* will + match *'foo \\'*, not *'foo "bar'*). + - Foot does not try to handle mismatched quote characters; they + will simply not match. + - Nested quotes (using different quote characters) are supported. + + Default: _BTN\_LEFT-3_. + *select-row* Begin an interactive row-wise selection. The selection is finalized, and copied to the _primary selection_, when the button - is released. Default: _BTN\_LEFT-3_. + is released. Default: _BTN\_LEFT-4_. *select-extend* Interactively extend an existing selection, using the original diff --git a/foot.ini b/foot.ini index 262556e7..00505165 100644 --- a/foot.ini +++ b/foot.ini @@ -200,6 +200,7 @@ # select-extend-character-wise=Control+BTN_RIGHT # select-word=BTN_LEFT-2 # select-word-whitespace=Control+BTN_LEFT-2 -# select-row=BTN_LEFT-3 +# select-quote = BTN_LEFT-3 +# select-row=BTN_LEFT-4 # vim: ft=dosini diff --git a/input.c b/input.c index 51b75233..e3613a90 100644 --- a/input.c +++ b/input.c @@ -472,6 +472,11 @@ execute_binding(struct seat *seat, struct terminal *term, term, seat->mouse.col, seat->mouse.row, SELECTION_WORD_WISE, true); return true; + case BIND_ACTION_SELECT_QUOTE: + selection_start( + term, seat->mouse.col, seat->mouse.row, SELECTION_QUOTE_WISE, false); + break; + case BIND_ACTION_SELECT_ROW: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_LINE_WISE, false); diff --git a/key-binding.h b/key-binding.h index 4d3ac541..ea2f3d6d 100644 --- a/key-binding.h +++ b/key-binding.h @@ -49,6 +49,7 @@ enum bind_action_normal { BIND_ACTION_SELECT_EXTEND_CHAR_WISE, BIND_ACTION_SELECT_WORD, BIND_ACTION_SELECT_WORD_WS, + BIND_ACTION_SELECT_QUOTE, BIND_ACTION_SELECT_ROW, BIND_ACTION_KEY_COUNT = BIND_ACTION_UNICODE_INPUT + 1, diff --git a/selection.c b/selection.c index d1a7ea28..f03a3d5c 100644 --- a/selection.c +++ b/selection.c @@ -298,6 +298,7 @@ foreach_selected( switch (term->selection.kind) { case SELECTION_CHAR_WISE: case SELECTION_WORD_WISE: + case SELECTION_QUOTE_WISE: case SELECTION_LINE_WISE: foreach_selected_normal(term, start, end, cb, data); return; @@ -508,9 +509,86 @@ selection_find_word_boundary_right(struct terminal *term, struct coord *pos, } } -void -selection_find_line_boundary_left(struct terminal *term, struct coord *pos, - bool spaces_only) +static bool +selection_find_quote_left(struct terminal *term, struct coord *pos, + char32_t *quote_char) +{ + const struct row *row = grid_row_in_view(term->grid, pos->row); + char32_t wc = row->cells[pos->col].wc; + + if ((*quote_char == '\0' && (wc == '"' || wc == '\'')) || + wc == *quote_char) + { + return false; + } + + int next_row = pos->row; + int next_col = pos->col; + + while (true) { + if (--next_col < 0) { + next_col = term->cols - 1; + if (--next_row < 0) + return false; + + row = grid_row_in_view(term->grid, next_row); + if (row->linebreak) + return false; + } + + wc = row->cells[next_col].wc; + + if ((*quote_char == '\0' && (wc == '"' || wc == '\'')) || + wc == *quote_char) + { + pos->row = next_row; + pos->col = next_col + 1; + xassert(pos->col < term->cols); + + *quote_char = wc; + return true; + } + } +} + +static bool +selection_find_quote_right(struct terminal *term, struct coord *pos, char32_t quote_char) +{ + if (quote_char == '\0') + return false; + + const struct row *row = grid_row_in_view(term->grid, pos->row); + char32_t wc = row->cells[pos->col].wc; + if (wc == quote_char) + return false; + + int next_row = pos->row; + int next_col = pos->col; + + while (true) { + if (++next_col >= term->cols) { + next_col = 0; + if (++next_row >= term->rows) + return false; + + if (row->linebreak) + return false; + + row = grid_row_in_view(term->grid, next_row); + } + + wc = row->cells[next_col].wc; + if (wc == quote_char) { + pos->row = next_row; + pos->col = next_col - 1; + xassert(pos->col >= 0); + return true; + } + } +} + +static void +selection_find_line_boundary_left(struct terminal *term, struct coord *pos) { int next_row = pos->row; pos->col = 0; @@ -530,9 +608,8 @@ selection_find_line_boundary_left(struct terminal *term, struct coord *pos, } } -void -selection_find_line_boundary_right(struct terminal *term, struct coord *pos, - bool spaces_only) +static void +selection_find_line_boundary_right(struct terminal *term, struct coord *pos) { int next_row = pos->row; pos->col = term->cols - 1; @@ -562,6 +639,7 @@ selection_start(struct terminal *term, int col, int row, LOG_DBG("%s selection started at %d,%d", kind == SELECTION_CHAR_WISE ? "character-wise" : kind == SELECTION_WORD_WISE ? "word-wise" : + kind == SELECTION_QUOTE_WISE ? "quote-wise" : kind == SELECTION_LINE_WISE ? "line-wise" : kind == SELECTION_BLOCK ? "block" : "", row, col); @@ -595,10 +673,61 @@ selection_start(struct terminal *term, int col, int row, break; } + case SELECTION_QUOTE_WISE: { + struct coord start = {col, row}, end = {col, row}; + + char32_t quote_char = '\0'; + bool found_left = selection_find_quote_left(term, &start, "e_char); + bool found_right = selection_find_quote_right(term, &end, quote_char); + + if (found_left && !found_right) { + xassert(quote_char != '\0'); + + /* + * Try to flip the quote character we're looking for. + * + * This lets us handle things like: + * + * "nested 'quotes are fun', right" + * + * In the example above, starting the selection at + * "right", will otherwise not match. find-left will find + * the single quote, causing find-right to fail. + * + * By flipping the quote-character, and re-trying, we + * find-left will find the starting double quote, letting + * find-right succeed as well. + */ + + if (quote_char == '\'') + quote_char = '"'; + else if (quote_char == '"') + quote_char = '\''; + + found_left = selection_find_quote_left(term, &start, "e_char); + found_right = selection_find_quote_right(term, &end, quote_char); + } + + if (found_left && found_right) { + term->selection.coords.start = (struct coord){ + start.col, term->grid->view + start.row}; + + term->selection.pivot.start = term->selection.coords.start; + term->selection.pivot.end = (struct coord){end.col, term->grid->view + end.row}; + + term->selection.kind = SELECTION_WORD_WISE; + selection_update(term, end.col, end.row); + break; + } else { + term->selection.kind = SELECTION_LINE_WISE; + /* FALLTHROUGH */ + } + } + case SELECTION_LINE_WISE: { struct coord start = {0, row}, end = {term->cols - 1, row}; - selection_find_line_boundary_left(term, &start, spaces_only); - selection_find_line_boundary_right(term, &end, spaces_only); + selection_find_line_boundary_left(term, &start); + selection_find_line_boundary_right(term, &end); term->selection.coords.start = (struct coord){ start.col, term->grid->view + start.row}; @@ -1052,20 +1181,22 @@ selection_update(struct terminal *term, int col, int row) } break; + case SELECTION_QUOTE_WISE: + BUG("quote-wise selection should always be transformed to either word-wise or line-wise"); + break; + case SELECTION_LINE_WISE: switch (term->selection.direction) { case SELECTION_LEFT: { struct coord end = {0, row}; - selection_find_line_boundary_left( - term, &end, term->selection.spaces_only); + selection_find_line_boundary_left(term, &end); new_end = (struct coord){end.col, term->grid->view + end.row}; break; } case SELECTION_RIGHT: { struct coord end = {col, row}; - selection_find_line_boundary_right( - term, &end, term->selection.spaces_only); + selection_find_line_boundary_right(term, &end); new_end = (struct coord){end.col, term->grid->view + end.row}; break; } @@ -1228,6 +1359,11 @@ selection_extend_normal(struct terminal *term, int col, int row, break; } + case SELECTION_QUOTE_WISE: { + BUG("quote-wise selection should always be transformed to either word-wise or line-wise"); + break; + } + case SELECTION_LINE_WISE: { xassert(new_kind == SELECTION_CHAR_WISE || new_kind == SELECTION_LINE_WISE); @@ -1235,8 +1371,8 @@ selection_extend_normal(struct terminal *term, int col, int row, struct coord pivot_start = {new_start.col, new_start.row - term->grid->view}; struct coord pivot_end = pivot_start; - selection_find_line_boundary_left(term, &pivot_start, spaces_only); - selection_find_line_boundary_right(term, &pivot_end, spaces_only); + selection_find_line_boundary_left(term, &pivot_start); + selection_find_line_boundary_right(term, &pivot_end); term->selection.pivot.start = (struct coord){pivot_start.col, term->grid->view + pivot_start.row}; @@ -1362,6 +1498,7 @@ selection_extend(struct seat *seat, struct terminal *term, case SELECTION_CHAR_WISE: case SELECTION_WORD_WISE: + case SELECTION_QUOTE_WISE: case SELECTION_LINE_WISE: selection_extend_normal(term, col, row, new_kind); break; diff --git a/terminal.h b/terminal.h index 0bba6945..624efd5c 100644 --- a/terminal.h +++ b/terminal.h @@ -300,6 +300,7 @@ enum selection_kind { SELECTION_NONE, SELECTION_CHAR_WISE, SELECTION_WORD_WISE, + SELECTION_QUOTE_WISE, SELECTION_LINE_WISE, SELECTION_BLOCK }; From a4843ef4188772b6680d019ba431dca2ce25f9ec Mon Sep 17 00:00:00 2001 From: Sertonix Date: Wed, 20 Sep 2023 13:20:48 +0000 Subject: [PATCH 0480/1323] doc: keybings default none --- doc/foot.ini.5.scd | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 6a11d38d..d9470626 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -751,32 +751,32 @@ e.g. *search-start=none*. *noop* All key combinations listed here will not be sent to the - application. Default: _not bound_. + application. Default: _none_. *scrollback-up-page* Scrolls up/back one page in history. Default: _Shift+Page\_Up_. *scrollback-up-half-page* - Scrolls up/back half of a page in history. Default: _not bound_. + Scrolls up/back half of a page in history. Default: _none_. *scrollback-up-line* - Scrolls up/back a single line in history. Default: _not bound_. + Scrolls up/back a single line in history. Default: _none_. *scrollback-down-page* Scroll down/forward one page in history. Default: _Shift+Page\_Down_. *scrollback-down-half-page* - Scroll down/forward half of a page in history. Default: _not bound_. + Scroll down/forward half of a page in history. Default: _none_. *scrollback-down-line* - Scroll down/forward a single line in history. Default: _not bound_. + Scroll down/forward a single line in history. Default: _none_. *scrollback-home* - Scroll to the beginning of the scrollback. Default: _not bound_. + Scroll to the beginning of the scrollback. Default: _none_. *scrollback-end* - Scroll to the end (bottom) of the scrollback. Default: _not bound_. + Scroll to the end (bottom) of the scrollback. Default: _none_. *clipboard-copy* Copies the current selection into the _clipboard_. Default: _Control+Shift+c_ @@ -809,13 +809,13 @@ e.g. *search-start=none*. current working directory. Default: _Control+Shift+n_. *minimize* - Minimizes the window. Default: _not bound_. + Minimizes the window. Default: _none_. *maximize* - Toggle the maximized state. Default: _not bound_. + Toggle the maximized state. Default: _none_. *fullscreen* - Toggles the fullscreen state. Default: _not bound_. + Toggles the fullscreen state. Default: _none_. *pipe-visible*, *pipe-scrollback*, *pipe-selected* Pipes the currently visible text, the entire scrollback, or the @@ -833,7 +833,7 @@ e.g. *search-start=none*. *pipe-visible=[sh -c "xurls | uniq | tac | fuzzel | xargs -r firefox"] Control+Print* - Default: _not bound_ + Default: _none_ *show-urls-launch* Enter URL mode, where all currently visible URLs are tagged with a From 9257273d84de2ef75ab505c9f954e0af6a2af78d Mon Sep 17 00:00:00 2001 From: Alyssa Ross Date: Thu, 21 Sep 2023 06:59:48 +0000 Subject: [PATCH 0481/1323] wayland: check activation supported before activating It's possible for token to be set when the compositor doesn't support activation, and this caused a segfault. For example, this can happen when overriding WAYLAND_DISPLAY to point to a compositor that doesn't support activation, in a terminal running under one that does, and so has set XDG_ACTIVATION_TOKEN. --- wayland.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wayland.c b/wayland.c index 7fcb01c1..d5dc838f 100644 --- a/wayland.c +++ b/wayland.c @@ -1701,7 +1701,7 @@ wayl_win_init(struct terminal *term, const char *token) wl_surface_commit(win->surface.surf); /* Complete XDG startup notification */ - if (token) + if (token && wayl->xdg_activation) xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface.surf); if (!wayl_win_subsurface_new(win, &win->overlay, false)) { From b2963bbf80973bd23e1d4ff4ead0df917dce7c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 21 Sep 2023 18:31:46 +0200 Subject: [PATCH 0482/1323] changelog: crash when xdg token is set, but compositor does not support activation --- CHANGELOG.md | 3 +++ wayland.c | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4730e1b..fe944218 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,10 +93,13 @@ another opaque, maximized window ([#1464][1464]). * Double-width characters, and grapheme clusters breaking URL auto-detection ([#1465][1465]). +* Crash when `XDG_ACTIVATION_TOKEN` is set, but compositor does not + support XDG activation ([#1493][1493]). [1436]: https://codeberg.org/dnkl/foot/issues/1436 [1464]: https://codeberg.org/dnkl/foot/issues/1464 [1465]: https://codeberg.org/dnkl/foot/issues/1465 +[1493]: https://codeberg.org/dnkl/foot/pulls/1493 ### Security diff --git a/wayland.c b/wayland.c index d5dc838f..d12bfc48 100644 --- a/wayland.c +++ b/wayland.c @@ -1701,7 +1701,7 @@ wayl_win_init(struct terminal *term, const char *token) wl_surface_commit(win->surface.surf); /* Complete XDG startup notification */ - if (token && wayl->xdg_activation) + if (token && wayl->xdg_activation != NULL) xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface.surf); if (!wayl_win_subsurface_new(win, &win->overlay, false)) { From 54722369d8ca0c6b90c6d874b0f712e133cd4925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 20 Sep 2023 15:29:35 +0200 Subject: [PATCH 0483/1323] url-mode: don't strip the file:// prefix from localhost URIs Before this patch, the file:// prefix was stripped from URIs, when the hostname matched the current host (that is, for "local" URLs). Unfortunately, the way this was done caused other parts of the URI to be stripped as well. For example, the 'query' and 'fragment' parts. This patch simply removes all special casing of file:// URIs. Since the URL is passed to a generic opener (i.e. we don't have a special opener application for file:// URIs), the opener helper must handle the file:// prefix anyway. Closes #1474 --- CHANGELOG.md | 3 +++ url-mode.c | 33 ++------------------------------- 2 files changed, 5 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe944218..5e894eed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,9 +74,12 @@ in many cases, triple clicking will still be enough to select the entire row; see the new key binding `select-quote` (mapped to `BTN_LEFT+3` by default) ([#1364][1364]). +* `file://` prefix from URI's are no longer stripped when + opened/activated ([#1474][1474]). [1391]: https://codeberg.org/dnkl/foot/issues/1391 [1448]: https://codeberg.org/dnkl/foot/pulls/1448 +[1474]: https://codeberg.org/dnkl/foot/pulls/1474 ### Deprecated diff --git a/url-mode.c b/url-mode.c index cc5fa2bb..e4ff0d8c 100644 --- a/url-mode.c +++ b/url-mode.c @@ -128,46 +128,17 @@ static void activate_url(struct seat *seat, struct terminal *term, const struct url *url, uint32_t serial) { - char *url_string = NULL; - - char *scheme, *host, *path; - if (uri_parse(url->url, strlen(url->url), &scheme, NULL, NULL, - &host, NULL, &path, NULL, NULL)) - { - if (strcmp(scheme, "file") == 0 && hostname_is_localhost(host)) { - /* - * This is a file in *this* computer. Pass only the - * filename to the URL-launcher. - * - * I.e. strip the ‘file://user@host/’ prefix. - */ - url_string = path; - } else - free(path); - - free(scheme); - free(host); - } - - if (url_string == NULL) - url_string = xstrdup(url->url); - switch (url->action) { case URL_ACTION_COPY: - if (text_to_clipboard(seat, term, url_string, seat->kbd.serial)) { - /* Now owned by our clipboard “manager” */ - url_string = NULL; - } + text_to_clipboard(seat, term, xstrdup(url->url), seat->kbd.serial); break; case URL_ACTION_LAUNCH: case URL_ACTION_PERSISTENT: { - spawn_url_launcher(seat, term, url_string, serial); + spawn_url_launcher(seat, term, url->url, serial); break; } } - - free(url_string); } void From 8a5f2915e9d327d1517d1da49ce7e2303fe61d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 25 Sep 2023 16:37:32 +0200 Subject: [PATCH 0484/1323] dcs: xtgettcap: ignore queries with invalid hex encodings When we receive an XTGETTCAP query, where the capability is not correctly hex encoded, ignore it. Before this patch, we echo:ed it back to the TTY inside an error resonse. --- CHANGELOG.md | 2 ++ dcs.c | 11 ++++------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e894eed..645e6e30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,8 @@ `BTN_LEFT+3` by default) ([#1364][1364]). * `file://` prefix from URI's are no longer stripped when opened/activated ([#1474][1474]). +* `XTGETTCAP` with capabilities that are not properly hex encoded will + be ignored, instead of echo:ed back to the TTY in an error response. [1391]: https://codeberg.org/dnkl/foot/issues/1391 [1448]: https://codeberg.org/dnkl/foot/pulls/1448 diff --git a/dcs.c b/dcs.c index 7ce1a868..601f1172 100644 --- a/dcs.c +++ b/dcs.c @@ -111,14 +111,11 @@ static void xtgettcap_reply(struct terminal *term, const char *hex_cap_name, size_t len) { char *name = hex_decode(hex_cap_name, len); - if (name == NULL) - goto err; + if (name == NULL) { + LOG_WARN("XTGETTCAP: invalid hex encoding, ignoring capability"); + return; + } -#if 0 - const struct foot_terminfo_entry *entry = - bsearch(name, terminfo_capabilities, ALEN(terminfo_capabilities), - sizeof(*entry), &terminfo_entry_compar); -#endif const char *value; bool valid_capability = lookup_capability(name, &value); xassert(!valid_capability || value != NULL); From 4eef001d5855b9b5afb59bbd3b92b3a2828fcca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 20 Sep 2023 13:45:06 +0200 Subject: [PATCH 0485/1323] csi: implement DECSET/DECRST/DECRQM 2027 - grapheme cluster processing This implements private mode 2027 - grapheme cluster processing, as defined in the "Terminal Unicode Core"[1] specification. Internally, we just flip the already existing option "grapheme shaping". Since it's now runtime changeable, we need a copy of it in the terminal struct, rather than referencing the conf object. [1]: https://github.com/contour-terminal/terminal-unicode-core/blob/13fc5a8993a033ff10c1f3433d08cb618cde4ac2/spec/terminal-unicode-core.tex#L50-L53 --- CHANGELOG.md | 2 ++ csi.c | 7 +++++++ doc/foot-ctlseqs.7.scd | 3 +++ doc/foot.ini.5.scd | 2 ++ terminal.c | 3 +++ terminal.h | 3 +++ vt.c | 2 +- 7 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 645e6e30..a064d1ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,8 @@ * New key binding: `select-quote`. This key binding selects text between quote characters, and falls back to selecting the entire row ([#1364][1364]). +* Support for DECSET/DECRST/DECRQM 2027 (_Grapheme cluster + processing_). [1077]: https://codeberg.org/dnkl/foot/issues/1077 [1364]: https://codeberg.org/dnkl/foot/issues/1364 diff --git a/csi.c b/csi.c index 959c913d..2abf08f8 100644 --- a/csi.c +++ b/csi.c @@ -491,6 +491,10 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) term_disable_app_sync_updates(term); break; + case 2027: + term->grapheme_shaping = enable; + break; + case 8452: term->sixel.cursor_right_of_graphics = enable; break; @@ -572,6 +576,7 @@ decrqm(const struct terminal *term, unsigned param) case 1070: return decrpm(term->sixel.use_private_palette); case 2004: return decrpm(term->bracketed_paste); case 2026: return decrpm(term->render.app_sync_updates.enabled); + case 2027: return decrpm(term->grapheme_shaping); case 8452: return decrpm(term->sixel.cursor_right_of_graphics); case 737769: return decrpm(term_ime_is_enabled(term)); } @@ -614,6 +619,7 @@ xtsave(struct terminal *term, unsigned param) case 1070: term->xtsave.sixel_private_palette = term->sixel.use_private_palette; break; case 2004: term->xtsave.bracketed_paste = term->bracketed_paste; break; case 2026: term->xtsave.app_sync_updates = term->render.app_sync_updates.enabled; break; + case 2027: term->xtsave.grapheme_shaping = term->grapheme_shaping; break; case 8452: term->xtsave.sixel_cursor_right_of_graphics = term->sixel.cursor_right_of_graphics; break; case 737769: term->xtsave.ime = term_ime_is_enabled(term); break; } @@ -655,6 +661,7 @@ xtrestore(struct terminal *term, unsigned param) case 1070: enable = term->xtsave.sixel_private_palette; break; case 2004: enable = term->xtsave.bracketed_paste; break; case 2026: enable = term->xtsave.app_sync_updates; break; + case 2027: enable = term->xtsave.grapheme_shaping; break; case 8452: enable = term->xtsave.sixel_cursor_right_of_graphics; break; case 737769: enable = term->xtsave.ime; break; diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index 3fa03158..d26bb39d 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -328,6 +328,9 @@ that corresponds to one of the following modes: | 2026 : terminal-wg : Application synchronized updates mode +| 2027 +: contour +: Grapheme cluster processing | 8452 : xterm : Position cursor to the right of sixels, instead of on the next line diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index d9470626..24c42a5e 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1307,6 +1307,8 @@ any of these options. - foot must have been compiled with utf8proc support - fcft must have been compiled with HarfBuzz support + This option can also be set runtime with DECSET/DECRST 2027. + See also: *grapheme-width-method*. Default: _yes_ diff --git a/terminal.c b/terminal.c index f19da873..1b082100 100644 --- a/terminal.c +++ b/terminal.c @@ -1253,6 +1253,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, }, .foot_exe = xstrdup(foot_exe), .cwd = xstrdup(cwd), + .grapheme_shaping = conf->tweak.grapheme_shaping, #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED .ime_enabled = true, #endif @@ -1903,6 +1904,8 @@ term_reset(struct terminal *term, bool hard) tll_remove(term->alt.sixel_images, it); } + term->grapheme_shaping = term->conf->tweak.grapheme_shaping; + #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED term_ime_enable(term); #endif diff --git a/terminal.h b/terminal.h index 624efd5c..da873ae6 100644 --- a/terminal.h +++ b/terminal.h @@ -469,6 +469,7 @@ struct terminal { bool alt_screen:1; bool ime:1; bool app_sync_updates:1; + bool grapheme_shaping:1; bool sixel_display_mode:1; bool sixel_private_palette:1; @@ -718,6 +719,8 @@ struct terminal { char *foot_exe; char *cwd; + + bool grapheme_shaping; }; struct config; diff --git a/vt.c b/vt.c index f6e8b79b..0f7bfe63 100644 --- a/vt.c +++ b/vt.c @@ -657,7 +657,7 @@ static void action_utf8_print(struct terminal *term, char32_t wc) { int width = c32width(wc); - const bool grapheme_clustering = term->conf->tweak.grapheme_shaping; + const bool grapheme_clustering = term->grapheme_shaping; #if !defined(FOOT_GRAPHEME_CLUSTERING) xassert(!grapheme_clustering); From 7fcbca808b57bd29afbe82dcdacfac6da8ed16da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 21 Sep 2023 18:28:54 +0200 Subject: [PATCH 0486/1323] csi: decrqm: 2027: permanently disabled when grapheme-width-method != double-width --- csi.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/csi.c b/csi.c index 2abf08f8..b83fc9b3 100644 --- a/csi.c +++ b/csi.c @@ -576,7 +576,9 @@ decrqm(const struct terminal *term, unsigned param) case 1070: return decrpm(term->sixel.use_private_palette); case 2004: return decrpm(term->bracketed_paste); case 2026: return decrpm(term->render.app_sync_updates.enabled); - case 2027: return decrpm(term->grapheme_shaping); + case 2027: return term->conf->tweak.grapheme_width_method != GRAPHEME_WIDTH_DOUBLE + ? DECRPM_PERMANENTLY_RESET + : decrpm(term->grapheme_shaping); case 8452: return decrpm(term->sixel.cursor_right_of_graphics); case 737769: return decrpm(term_ime_is_enabled(term)); } From 400a3f5ad284a74da1c9ea5eca7d31f68f222995 Mon Sep 17 00:00:00 2001 From: Alyssa Ross Date: Fri, 22 Sep 2023 08:23:38 +0000 Subject: [PATCH 0487/1323] config: apply overrides even if there's no file Previously, foot -a test wouldn't actually set the app ID if there was no config file and the defaults were used, which was very counterintuitive. Now, load_config() will carry on until the end, even if there's no config file, so overrides still work. --- CHANGELOG.md | 4 ++++ config.c | 39 +++++++++++++++++---------------------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a064d1ee..8f6a26d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,10 +80,14 @@ opened/activated ([#1474][1474]). * `XTGETTCAP` with capabilities that are not properly hex encoded will be ignored, instead of echo:ed back to the TTY in an error response. +* Command line configuration overrides are now applied even if the + configuration file does not exist or can't be + parsed. ([#1495][1495]). [1391]: https://codeberg.org/dnkl/foot/issues/1391 [1448]: https://codeberg.org/dnkl/foot/pulls/1448 [1474]: https://codeberg.org/dnkl/foot/pulls/1474 +[1495]: https://codeberg.org/dnkl/foot/pulls/1495 ### Deprecated diff --git a/config.c b/config.c index 68dbc679..6ba371e8 100644 --- a/config.c +++ b/config.c @@ -2912,7 +2912,7 @@ config_load(struct config *conf, const char *conf_path, config_override_t *overrides, bool errors_are_fatal, bool as_server) { - bool ret = false; + bool ret = true; enum fcft_capabilities fcft_caps = fcft_capabilities(); *conf = (struct config) { @@ -3106,45 +3106,40 @@ config_load(struct config *conf, const char *conf_path, if (fd < 0) { LOG_AND_NOTIFY_ERRNO("%s: failed to open", conf_path); ret = !errors_are_fatal; - goto out; + } else { + conf_file.path = xstrdup(conf_path); + conf_file.fd = fd; } - - conf_file.path = xstrdup(conf_path); - conf_file.fd = fd; } else { conf_file = open_config(); if (conf_file.fd < 0) { LOG_WARN("no configuration found, using defaults"); ret = !errors_are_fatal; - goto out; } } - xassert(conf_file.path != NULL); - xassert(conf_file.fd >= 0); - LOG_INFO("loading configuration from %s", conf_file.path); + if (conf_file.path && conf_file.fd >= 0) { + LOG_INFO("loading configuration from %s", conf_file.path); - FILE *f = fdopen(conf_file.fd, "r"); - if (f == NULL) { - LOG_AND_NOTIFY_ERRNO("%s: failed to open", conf_file.path); - ret = !errors_are_fatal; - goto out; + FILE *f = fdopen(conf_file.fd, "r"); + if (f == NULL) { + LOG_AND_NOTIFY_ERRNO("%s: failed to open", conf_file.path); + ret = !errors_are_fatal; + } else { + if (!parse_config_file(f, conf, conf_file.path, errors_are_fatal)) + ret = !errors_are_fatal; + + fclose(f); + } } - if (!parse_config_file(f, conf, conf_file.path, errors_are_fatal) || - !config_override_apply(conf, overrides, errors_are_fatal)) - { + if (!config_override_apply(conf, overrides, errors_are_fatal)) ret = !errors_are_fatal; - } else - ret = true; - - fclose(f); conf->colors.use_custom.selection = conf->colors.selection_fg >> 24 == 0 && conf->colors.selection_bg >> 24 == 0; -out: if (ret && conf->fonts[0].count == 0) { struct config_font font; if (!config_font_parse("monospace", &font)) { From b95a7cb84f9bebdd99ac68d30fa8256cc60dc7ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 1 Oct 2023 09:20:13 +0200 Subject: [PATCH 0488/1323] term: get_font_dpi(): don't crash when there aren't any available monitors Seen on plasma; monitor is turned off, and then back on again. Before the "new" output global is emitted, the compositor calls fractional_scale::preferred_scale(). This results in a call to get_font_dpi(), where we crash, since it assumes there is at least one monitor available. Fix by falling back to a DPI of 96. Hopefully closes #1498 --- CHANGELOG.md | 4 ++++ terminal.c | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f6a26d5..b1c7bb32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,11 +106,15 @@ auto-detection ([#1465][1465]). * Crash when `XDG_ACTIVATION_TOKEN` is set, but compositor does not support XDG activation ([#1493][1493]). +* Crash when compositor calls `fractional_scale::preferred_scale()` + when there are no monitors (for example, after a monitor has been + turned off and then back on again) ([#1498][1498]). [1436]: https://codeberg.org/dnkl/foot/issues/1436 [1464]: https://codeberg.org/dnkl/foot/issues/1464 [1465]: https://codeberg.org/dnkl/foot/issues/1465 [1493]: https://codeberg.org/dnkl/foot/pulls/1493 +[1498]: https://codeberg.org/dnkl/foot/issues/1498 ### Security diff --git a/terminal.c b/terminal.c index 1b082100..06f47685 100644 --- a/terminal.c +++ b/terminal.c @@ -826,9 +826,9 @@ get_font_dpi(const struct terminal *term) : &tll_front(term->wl->monitors); if (term_fractional_scaling(term)) - return mon->dpi.physical; + return mon != NULL ? mon->dpi.physical : 96.; else - return mon->dpi.scaled; + return mon != NULL ? mon->dpi.scaled : 96.; } static enum fcft_subpixel From 883368572ff3c21653c77fea34fc8ec8a0599b56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 2 Oct 2023 16:34:54 +0200 Subject: [PATCH 0489/1323] wayland: debug: log wm-capabilities as human-readable strings --- wayland.c | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/wayland.c b/wayland.c index d12bfc48..a87bf45d 100644 --- a/wayland.c +++ b/wayland.c @@ -758,7 +758,7 @@ xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, #if defined(XDG_TOPLEVEL_STATE_SUSPENDED_SINCE_VERSION) case XDG_TOPLEVEL_STATE_SUSPENDED: is_suspended = true; break; #endif - } + } #if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG if (*state >= 0 && *state < ALEN(strings)) { @@ -831,17 +831,52 @@ xdg_toplevel_wm_capabilities(void *data, win->wm_capabilities.maximize = false; win->wm_capabilities.minimize = false; - uint32_t *cap_ptr; - wl_array_for_each(cap_ptr, caps) { - switch (*cap_ptr) { +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + char cap_str[2048]; + int cap_chars = 0; + + static const char *const strings[] = { + [XDG_TOPLEVEL_WM_CAPABILITIES_WINDOW_MENU] = "window-menu", + [XDG_TOPLEVEL_WM_CAPABILITIES_MAXIMIZE] = "maximize", + [XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN] = "fullscreen", + [XDG_TOPLEVEL_WM_CAPABILITIES_MINIMIZE] = "minimize", + }; +#endif + + enum xdg_toplevel_wm_capabilities *cap; + wl_array_for_each(cap, caps) { + switch (*cap) { case XDG_TOPLEVEL_WM_CAPABILITIES_MAXIMIZE: win->wm_capabilities.maximize = true; break; + case XDG_TOPLEVEL_WM_CAPABILITIES_MINIMIZE: win->wm_capabilities.minimize = true; break; + + case XDG_TOPLEVEL_WM_CAPABILITIES_WINDOW_MENU: + case XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN: + break; } + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + if (*cap >= 0 && *cap < ALEN(strings)) { + cap_chars += snprintf( + &cap_str[cap_chars], sizeof(cap_str) - cap_chars, + "%s, ", + strings[*cap] != NULL ? strings[*cap] : ""); + } +#endif } + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + if (cap_chars > 2) + cap_str[cap_chars - 2] = '\0'; + else + cap_str[0] = '\0'; + + LOG_DBG("xdg-toplevel: wm-capabilities=[%s]", cap_str); +#endif } #endif From 58d967b2f31ebe1f16bd0915f28ec93919d3fddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 3 Oct 2023 14:11:55 +0200 Subject: [PATCH 0490/1323] Codespell fixes --- CHANGELOG.md | 6 +++--- INSTALL.md | 2 +- client.c | 2 +- grid.c | 4 ++-- main.c | 2 +- render.c | 4 ++-- shm.c | 2 +- terminal.c | 4 ++-- url-mode.c | 2 +- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1c7bb32..603cf5b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -409,10 +409,10 @@ has terminated very early (for example, by trying to launch a non-existing shell/client). * Glitchy rendering when scrolling in the scrollback, on compositors - that does not allow Wayland buffer re-use (e.g. KDE/plasma) + that does not allow Wayland buffer reuse (e.g. KDE/plasma) ([#1173][1173]) * Scrollback search matches not being highlighted correctly, on - compositors that does not allow Wayland buffer re-use + compositors that does not allow Wayland buffer reuse (e.g. KDE/plasma). * Nanosecs "overflow" when calculating timeout value for `resize-delay-ms` option. @@ -2331,7 +2331,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * Window title always being set to "foot" on reset. * Terminfo entry `kb2` (center keypad key); it is now set to `\EOu` (which is what foot emits) instead of the incorrect value `\EOE`. -* Palette re-use in sixel images. Previously, the palette was reset +* Palette reuse in sixel images. Previously, the palette was reset after each image. * Do not auto-resize a sixel image for which the client has specified a size. This fixes an issue where an image would incorrectly diff --git a/INSTALL.md b/INSTALL.md index 67e6b910..22ea8067 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -169,7 +169,7 @@ under a different name. Setting this changes the default value of If you want foot to use the terminfo files from ncurses, but still package foot's own terminfo files under a different name, you can use the `-Dterminfo-base-name` option. Many distributions use the name -`foot-extra`, and thus it might be a good idea to re-use that: +`foot-extra`, and thus it might be a good idea to reuse that: ```sh meson ... -Ddefault-terminfo=foot -Dterminfo-base-name=foot-extra diff --git a/client.c b/client.c index e68f71aa..8456dfd8 100644 --- a/client.c +++ b/client.c @@ -149,7 +149,7 @@ int main(int argc, char *const *argv) { /* Custom exit code, to enable users to differentiate between foot - * itself failing, and the client application failiing */ + * itself failing, and the client application failing */ static const int foot_exit_failure = -36; int ret = foot_exit_failure; diff --git a/grid.c b/grid.c index 22d2a89a..ea103c65 100644 --- a/grid.c +++ b/grid.c @@ -584,7 +584,7 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, new_row = grid_row_alloc(col_count, false); new_grid[*row_idx] = new_row; } else { - /* Scrollback is full, need to re-use a row */ + /* Scrollback is full, need to reuse a row */ grid_row_reset_extra(new_row); new_row->linebreak = false; new_row->prompt_marker = false; @@ -597,7 +597,7 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, } /* - * TODO: detect if the re-used row is covered by the + * TODO: detect if the reused row is covered by the * selection. Of so, cancel the selection. The problem: we * don’t know if we’ve translated the selection coordinates * yet. diff --git a/main.c b/main.c index 631ef167..4fc8f439 100644 --- a/main.c +++ b/main.c @@ -176,7 +176,7 @@ int main(int argc, char *const *argv) { /* Custom exit code, to enable users to differentiate between foot - * itself failing, and the client application failiing */ + * itself failing, and the client application failing */ static const int foot_exit_failure = -26; int ret = foot_exit_failure; diff --git a/render.c b/render.c index 70b3fba0..517c4b31 100644 --- a/render.c +++ b/render.c @@ -1614,7 +1614,7 @@ render_overlay(struct terminal *term) buf->age == 0; if (!buffer_reuse) { - /* Can’t re-use last frame’s damage - set to full window, + /* Can’t reuse last frame’s damage - set to full window, * to ensure *everything* is updated */ pixman_region32_init_rect( &old_see_through, 0, 0, buf->width, buf->height); @@ -4026,7 +4026,7 @@ maybe_resize(struct terminal *term, int width, int height, bool force) /* * Since text reflow is slow, don’t do it *while* resizing. Only * do it when done, or after “pausing” the resize for sufficiently - * long. We re-use the TIOCSWINSZ timer to handle this. See + * long. We reuse the TIOCSWINSZ timer to handle this. See * send_dimensions_to_client() and fdm_tiocswinsz(). * * To be able to do the final reflow correctly, we need a copy of diff --git a/shm.c b/shm.c index 4394dbe9..daa2a47c 100644 --- a/shm.c +++ b/shm.c @@ -555,7 +555,7 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height) cached = buf; else { /* We have multiple buffers eligible for - * re-use. Pick the “youngest” one, and mark the + * reuse. Pick the “youngest” one, and mark the * other one for purging */ if (buf->public.age < cached->public.age) { shm_unref(&cached->public); diff --git a/terminal.c b/terminal.c index 06f47685..b577236f 100644 --- a/terminal.c +++ b/terminal.c @@ -2675,7 +2675,7 @@ term_scroll_partial(struct terminal *term, struct scroll_region region, int rows /* * Selection is (partly) inside either the top or bottom * scrolling regions, or on (at least one) of the lines - * scrolled in (i.e. re-used lines). + * scrolled in (i.e. reused lines). */ if (selection_on_top_region(term, region) || selection_on_bottom_region(term, region)) @@ -2749,7 +2749,7 @@ term_scroll_reverse_partial(struct terminal *term, /* * Selection is (partly) inside either the top or bottom * scrolling regions, or on (at least one) of the lines - * scrolled in (i.e. re-used lines). + * scrolled in (i.e. reused lines). */ if (selection_on_top_region(term, region) || selection_on_bottom_region(term, region)) diff --git a/url-mode.c b/url-mode.c index e4ff0d8c..76e2869d 100644 --- a/url-mode.c +++ b/url-mode.c @@ -697,7 +697,7 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) /* * Scan previous URLs, and check if *this* URL matches any of - * them; if so, re-use the *same* key combo. + * them; if so, reuse the *same* key combo. */ bool url_already_seen = false; tll_foreach(*urls, it2) { From 5e1d73f3cddcd7f21c80abfbc6914e8f8b0c6f3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 3 Oct 2023 14:12:58 +0200 Subject: [PATCH 0491/1323] Codespell fixes --- shm.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shm.c b/shm.c index daa2a47c..123adaf3 100644 --- a/shm.c +++ b/shm.c @@ -527,7 +527,7 @@ struct buffer * shm_get_buffer(struct buffer_chain *chain, int width, int height) { LOG_DBG( - "chain=%p: looking for a re-usable %dx%d buffer " + "chain=%p: looking for a reusable %dx%d buffer " "among %zu potential buffers", (void *)chain, width, height, tll_length(chain->bufs)); From 33a5a369f2054975cf31f6662f4dd82139df762a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 4 Oct 2023 08:23:27 +0200 Subject: [PATCH 0492/1323] term_reset: log hard vs. soft reset --- terminal.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/terminal.c b/terminal.c index b577236f..91c46a11 100644 --- a/terminal.c +++ b/terminal.c @@ -1848,6 +1848,8 @@ erase_line(struct terminal *term, struct row *row) void term_reset(struct terminal *term, bool hard) { + LOG_INFO("%s resetting the terminal", hard ? "hard" : "soft"); + term->cursor_keys_mode = CURSOR_KEYS_NORMAL; term->keypad_keys_mode = KEYPAD_NUMERICAL; term->reverse = false; From 61eb56dfda605427156b52d019be4bf5ce84c3aa Mon Sep 17 00:00:00 2001 From: 6t8k <6t8k@noreply.codeberg.org> Date: Thu, 5 Oct 2023 12:22:44 +0200 Subject: [PATCH 0493/1323] shm: if defined, set MFD_NOEXEC_SEAL flag for memfd_create Effective from Linux 6.3.0 onward, this creates the memfd without execute permissions and prevents that setting from ever being changed. This is a defense-in-depth security measure and prevents a respective kernel warning from being emitted. See https://lwn.net/Articles/918106/ for more information. --- shm.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/shm.c b/shm.c index 123adaf3..d543e1f4 100644 --- a/shm.c +++ b/shm.c @@ -330,8 +330,13 @@ get_new_buffers(struct buffer_chain *chain, size_t count, struct buffer_pool *pool = NULL; /* Backing memory for SHM */ +#if defined(MFD_NOEXEC_SEAL) + #define FOOT_MFD_FLAGS (MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_NOEXEC_SEAL) +#else + #define FOOT_MFD_FLAGS (MFD_CLOEXEC | MFD_ALLOW_SEALING) +#endif #if defined(MEMFD_CREATE) - pool_fd = memfd_create("foot-wayland-shm-buffer-pool", MFD_CLOEXEC | MFD_ALLOW_SEALING); + pool_fd = memfd_create("foot-wayland-shm-buffer-pool", FOOT_MFD_FLAGS); #elif defined(__FreeBSD__) // memfd_create on FreeBSD 13 is SHM_ANON without sealing support pool_fd = shm_open(SHM_ANON, O_RDWR | O_CLOEXEC, 0600); From 56d5d4cc213d422b2f6dba26d94fb8770203abc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 7 Oct 2023 07:58:55 +0200 Subject: [PATCH 0494/1323] render: disable transparency in margins when in fullscreen This amends 899b768b744c74e88e54e6d8eb32f53accea79d8, where we started disabling transparency in fullscreen Closes #1503 --- CHANGELOG.md | 3 +++ render.c | 13 +++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 603cf5b9..f55792d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -109,12 +109,15 @@ * Crash when compositor calls `fractional_scale::preferred_scale()` when there are no monitors (for example, after a monitor has been turned off and then back on again) ([#1498][1498]). +* Transparency in margins (padding) not being disabled in fullscreen + mode ([#1503][1503]). [1436]: https://codeberg.org/dnkl/foot/issues/1436 [1464]: https://codeberg.org/dnkl/foot/issues/1464 [1465]: https://codeberg.org/dnkl/foot/issues/1465 [1493]: https://codeberg.org/dnkl/foot/pulls/1493 [1498]: https://codeberg.org/dnkl/foot/issues/1498 +[1503]: https://codeberg.org/dnkl/foot/issues/1503 ### Security diff --git a/render.c b/render.c index 517c4b31..3101d36d 100644 --- a/render.c +++ b/render.c @@ -545,6 +545,8 @@ render_cell(struct terminal *term, pixman_image_t *pix, * * By disabling the alpha channel, the window will at * least be rendered in the intended background color. + * + * NOTE: if changing this, also update render_margin() */ xassert(alpha == 0xffff); } else { @@ -882,8 +884,15 @@ render_margin(struct terminal *term, struct buffer *buf, const int bmargin = term->height - term->margins.bottom; const int line_count = end_line - start_line; - uint32_t _bg = !term->reverse ? term->colors.bg : term->colors.fg; - pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, term->colors.alpha); + const uint32_t _bg = !term->reverse ? term->colors.bg : term->colors.fg; + uint16_t alpha = term->colors.alpha; + + if (term->window->is_fullscreen) { + /* Disable alpha in fullscreen - see render_cell() for details */ + alpha = 0xffff; + } + + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &bg, 4, From 78665a7e809afd470e1236867761d51de9737eaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 26 Sep 2023 17:54:03 +0200 Subject: [PATCH 0495/1323] search: add more key bindings to extend the current match This patch adds the following new search key bindings: * extend-char (shift+right) * extend-line-down (shift+down) * extend-backward-char (shift+left) * extend-backward-to-word-boundary (ctrl+shift+left) * extend-backward-to-next-whitespace (ctrl+shift+alt+left) * extend-line-up (shift+up) They can be used to extend the search match (i.e. the selection). This patch also adds an initial set of key bindings to scroll in the scrollback history: * scrollback-up-page * scrollback-down-page These work just like the key bindings for the normal mode. Also note that it was already possible to scroll using the mouse. This patch also fixes a couple of search mode bugs: * crashing when a search match ends in the last column * grapheme clusters not being matched correctly * Search match not being "extendable" after a pointer leave event * A few others, related to either large matches, or extending matches after moving the viewport. There are still a couple of (known) issues: * A search match isn't correctly highlighted if its *starting* point is outside the viewport. * Extending the match to end of the scrollback (i.e. the most recent output) is simply buggy. Related to #419 --- CHANGELOG.md | 13 ++ config.c | 27 +++- key-binding.h | 9 ++ search.c | 417 +++++++++++++++++++++++++++++++++++++++++++------- selection.c | 93 ++++++----- selection.h | 4 +- 6 files changed, 457 insertions(+), 106 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f55792d8..912289a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,9 +60,20 @@ row ([#1364][1364]). * Support for DECSET/DECRST/DECRQM 2027 (_Grapheme cluster processing_). +* New search mode key bindings (along with their defaults) + ([#419][419]): + - `extend-char` (shift+right) + - `extend-line-down` (shift+down) + - `extend-backward-char` (shift+left) + - `extend-backward-to-word-boundary` (ctrl+shift+left) + - `extend-backward-to-next-whitespace` (ctrl+alt+shift+left) + - `extend-line-up` (shift+up) + - `scrollback-up-page` (shift+page-up) + - `scrollback-down-page` (shift+page-down) [1077]: https://codeberg.org/dnkl/foot/issues/1077 [1364]: https://codeberg.org/dnkl/foot/issues/1364 +[419]: https://codeberg.org/dnkl/foot/issues/419 ### Changed @@ -111,6 +122,8 @@ turned off and then back on again) ([#1498][1498]). * Transparency in margins (padding) not being disabled in fullscreen mode ([#1503][1503]). +* Crash when a scrollback search match is in the last column. +* Scrollback search: grapheme clusters not matching correctly. [1436]: https://codeberg.org/dnkl/foot/issues/1436 [1464]: https://codeberg.org/dnkl/foot/issues/1464 diff --git a/config.c b/config.c index 6ba371e8..15ec52ff 100644 --- a/config.c +++ b/config.c @@ -148,11 +148,19 @@ static const char *const search_binding_action_map[] = { [BIND_ACTION_SEARCH_DELETE_PREV_WORD] = "delete-prev-word", [BIND_ACTION_SEARCH_DELETE_NEXT] = "delete-next", [BIND_ACTION_SEARCH_DELETE_NEXT_WORD] = "delete-next-word", + [BIND_ACTION_SEARCH_EXTEND_CHAR] = "extend-char", [BIND_ACTION_SEARCH_EXTEND_WORD] = "extend-to-word-boundary", [BIND_ACTION_SEARCH_EXTEND_WORD_WS] = "extend-to-next-whitespace", + [BIND_ACTION_SEARCH_EXTEND_LINE_DOWN] = "extend-line-down", + [BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR] = "extend-backward-char", + [BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD] = "extend-backward-to-word-boundary", + [BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD_WS] = "extend-backward-to-next-whitespace", + [BIND_ACTION_SEARCH_EXTEND_LINE_UP] = "extend-line-up", [BIND_ACTION_SEARCH_CLIPBOARD_PASTE] = "clipboard-paste", [BIND_ACTION_SEARCH_PRIMARY_PASTE] = "primary-paste", [BIND_ACTION_SEARCH_UNICODE_INPUT] = "unicode-input", + [BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE] = "scrollback-up-page", + [BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE] = "scrollback-down-page", }; static const char *const url_binding_action_map[] = { @@ -2774,11 +2782,12 @@ get_server_socket_path(void) 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_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 void add_default_key_bindings(struct config *conf) @@ -2816,6 +2825,8 @@ 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}}}, @@ -2840,8 +2851,14 @@ add_default_search_bindings(struct config *conf) {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_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_BACKWARD_WORD_WS, m_ctrl_shift_alt, {{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}}}, diff --git a/key-binding.h b/key-binding.h index ea2f3d6d..b85fc072 100644 --- a/key-binding.h +++ b/key-binding.h @@ -58,6 +58,9 @@ enum bind_action_normal { enum bind_action_search { BIND_ACTION_SEARCH_NONE, + BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE, + BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE, + // TODO: copy the remaining scrollback key-bindings from normal mode BIND_ACTION_SEARCH_CANCEL, BIND_ACTION_SEARCH_COMMIT, BIND_ACTION_SEARCH_FIND_PREV, @@ -72,8 +75,14 @@ enum bind_action_search { BIND_ACTION_SEARCH_DELETE_PREV_WORD, BIND_ACTION_SEARCH_DELETE_NEXT, BIND_ACTION_SEARCH_DELETE_NEXT_WORD, + BIND_ACTION_SEARCH_EXTEND_CHAR, BIND_ACTION_SEARCH_EXTEND_WORD, BIND_ACTION_SEARCH_EXTEND_WORD_WS, + BIND_ACTION_SEARCH_EXTEND_LINE_DOWN, + BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR, + BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD, + BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD_WS, + BIND_ACTION_SEARCH_EXTEND_LINE_UP, BIND_ACTION_SEARCH_CLIPBOARD_PASTE, BIND_ACTION_SEARCH_PRIMARY_PASTE, BIND_ACTION_SEARCH_UNICODE_INPUT, diff --git a/search.c b/search.c index 6c2a2a7e..1fb6bb3e 100644 --- a/search.c +++ b/search.c @@ -9,6 +9,7 @@ #define LOG_ENABLE_DBG 0 #include "log.h" #include "char32.h" +#include "commands.h" #include "config.h" #include "extract.h" #include "grid.h" @@ -82,7 +83,14 @@ search_ensure_size(struct terminal *term, size_t wanted_size) } static bool -has_wrapped_around(const struct terminal *term, int abs_row_no) +has_wrapped_around_left(const struct terminal *term, int abs_row_no) +{ + int rebased_row = grid_row_abs_to_sb(term->grid, term->rows, abs_row_no); + return rebased_row == term->grid->num_rows - 1 || term->grid->rows[abs_row_no] == NULL; +} + +static bool +has_wrapped_around_right(const struct terminal *term, int abs_row_no) { int rebased_row = grid_row_abs_to_sb(term->grid, term->rows, abs_row_no); return rebased_row == 0; @@ -235,13 +243,19 @@ search_update_selection(struct terminal *term, const struct range *match) } if (start_row != term->search.match.row || - start_col != term->search.match.col) + start_col != term->search.match.col || + + /* Pointer leave events trigger selection_finalize() :/ */ + !term->selection.ongoing) { int selection_row = start_row - grid->view + grid->num_rows; selection_row &= grid->num_rows - 1; selection_start( term, start_col, selection_row, SELECTION_CHAR_WISE, false); + + term->search.match.row = start_row; + term->search.match.col = start_col; } /* Update selection endpoint */ @@ -273,16 +287,16 @@ matches_cell(const struct terminal *term, const struct cell *cell, size_t search return -1; if (composed != NULL) { - if (search_ofs + 1 + composed->count > term->search.len) + if (search_ofs + composed->count > term->search.len) return -1; for (size_t j = 1; j < composed->count; j++) { - if (composed->chars[j] != term->search.buf[search_ofs + 1 + j]) + if (composed->chars[j] != term->search.buf[search_ofs + j]) return -1; } } - return composed != NULL ? 1 + composed->count : 1; + return composed != NULL ? composed->count : 1; } static bool @@ -369,8 +383,11 @@ find_next(struct terminal *term, enum search_direction direction, match_len += additional_chars; match_end_col++; - while (match_row->cells[match_end_col].wc > CELL_SPACER) + while (match_end_col < term->cols && + match_row->cells[match_end_col].wc > CELL_SPACER) + { match_end_col++; + } } if (match_len != term->search.len) { @@ -546,6 +563,7 @@ search_matches_next(struct search_match_iterator *iter) term->cols - 1, grid_row_absolute_in_view(grid, term->rows - 1)}; + /* BUG: matches *starting* outside the view, but ending *inside*, aren't matched */ struct range match; bool found = find_next(term, SEARCH_FORWARD, abs_start, abs_end, &match); if (!found) @@ -565,11 +583,6 @@ search_matches_next(struct search_match_iterator *iter) match.start.row, match.start.col, match.end.row, match.end.col, grid->view); - xassert(match.start.row >= 0); - xassert(match.start.row < term->rows); - xassert(match.end.row >= 0); - xassert(match.end.row < term->rows); - /* Assert match end comes *after* the match start */ xassert(match.end.row > match.start.row || (match.end.row == match.start.row && @@ -642,67 +655,282 @@ search_add_chars(struct terminal *term, const char *src, size_t count) add_wchars(term, c32s, chars); } -static void -search_match_to_end_of_word(struct terminal *term, bool spaces_only) +enum extend_direction {SEARCH_EXTEND_LEFT, SEARCH_EXTEND_RIGHT}; + +static bool +coord_advance_left(const struct terminal *term, struct coord *pos, + const struct row **row) { - if (term->search.match_len == 0) + const struct grid *grid = term->grid; + struct coord new_pos = *pos; + + if (--new_pos.col < 0) { + new_pos.row = (new_pos.row - 1 + grid->num_rows) & (grid->num_rows - 1); + new_pos.col = term->cols - 1; + + if (has_wrapped_around_left(term, new_pos.row)) + return false; + + if (row != NULL) + *row = grid->rows[new_pos.row]; + } + + *pos = new_pos; + return true; +} + +static bool +coord_advance_right(const struct terminal *term, struct coord *pos, + const struct row **row) +{ + const struct grid *grid = term->grid; + struct coord new_pos = *pos; + + if (++new_pos.col >= term->cols) { + new_pos.row = (new_pos.row + 1) & (grid->num_rows - 1); + new_pos.col = 0; + + if (has_wrapped_around_right(term, new_pos.row)) + return false; + + if (row != NULL) + *row = grid->rows[new_pos.row]; + } + + *pos = new_pos; + return true; +} + +static void +search_extend_find_char(const struct terminal *term, struct coord *target, + enum extend_direction direction) +{ + struct coord pos = direction == SEARCH_EXTEND_LEFT + ? selection_get_start(term) : selection_get_end(term); + xassert(pos.row >= 0); + xassert(pos.row < term->grid->num_rows); + + *target = pos; + + const struct row *row = term->grid->rows[pos.row]; + + while (true) { + switch (direction) { + case SEARCH_EXTEND_LEFT: + if (!coord_advance_left(term, &pos, &row)) + return; + break; + + case SEARCH_EXTEND_RIGHT: + if (!coord_advance_right(term, &pos, &row)) + return; + break; + } + + const char32_t wc = row->cells[pos.col].wc; + + if (wc >= CELL_SPACER || wc == U'\0') + continue; + + *target = pos; return; + } +} - xassert(term->selection.coords.end.row >= 0); +static void +search_extend_find_char_left(const struct terminal *term, struct coord *target) +{ + search_extend_find_char(term, target, SEARCH_EXTEND_LEFT); +} +static void +search_extend_find_char_right(const struct terminal *term, struct coord *target) +{ + search_extend_find_char(term, target, SEARCH_EXTEND_RIGHT); +} + +static void +search_extend_find_word(const struct terminal *term, bool spaces_only, + struct coord *target, enum extend_direction direction) +{ struct grid *grid = term->grid; - const bool move_cursor = term->search.cursor == term->search.len; + struct coord pos = direction == SEARCH_EXTEND_LEFT + ? selection_get_start(term) + : selection_get_end(term); + + xassert(pos.row >= 0); + xassert(pos.row < grid->num_rows); - struct coord old_end = selection_get_end(term); - struct coord new_end = old_end; - struct row *row = NULL; - - xassert(new_end.row >= 0); - xassert(new_end.row < grid->num_rows); - - /* Advances a coordinate by one column, to the right. Returns - * false if we’ve reached the scrollback wrap-around */ -#define advance_pos(coord) __extension__ \ - ({ \ - bool wrapped_around = false; \ - if (++(coord).col >= term->cols) { \ - (coord).row = ((coord).row + 1) & (grid->num_rows - 1); \ - (coord).col = 0; \ - row = grid->rows[(coord).row]; \ - if (has_wrapped_around(term, (coord.row))) \ - wrapped_around = true; \ - } \ - !wrapped_around; \ - }) + *target = pos; /* First character to consider is the *next* character */ - if (!advance_pos(new_end)) - return; + switch (direction) { + case SEARCH_EXTEND_LEFT: + if (!coord_advance_left(term, &pos, NULL)) + return; + break; - xassert(new_end.row >= 0); - xassert(new_end.row < grid->num_rows); - xassert(grid->rows[new_end.row] != NULL); + case SEARCH_EXTEND_RIGHT: + if (!coord_advance_right(term, &pos, NULL)) + return; + break; + } + + xassert(pos.row >= 0); + xassert(pos.row < grid->num_rows); + xassert(grid->rows[pos.row] != NULL); /* Find next word boundary */ - new_end.row -= grid->view + grid->num_rows; - new_end.row &= grid->num_rows - 1; - selection_find_word_boundary_right(term, &new_end, spaces_only, false); - new_end.row += grid->view; - new_end.row &= grid->num_rows - 1; + switch (direction) { + case SEARCH_EXTEND_LEFT: + selection_find_word_boundary_left(term, &pos, spaces_only); + break; - struct coord pos = old_end; - row = grid->rows[pos.row]; + case SEARCH_EXTEND_RIGHT: + selection_find_word_boundary_right(term, &pos, spaces_only, false); + break; + } + + *target = pos; +} + +static void +search_extend_find_word_left(const struct terminal *term, bool spaces_only, + struct coord *target) +{ + search_extend_find_word(term, spaces_only, target, SEARCH_EXTEND_LEFT); +} + +static void +search_extend_find_word_right(const struct terminal *term, bool spaces_only, + struct coord *target) +{ + search_extend_find_word(term, spaces_only, target, SEARCH_EXTEND_RIGHT); +} + +static void +search_extend_find_line(const struct terminal *term, struct coord *target, + enum extend_direction direction) +{ + struct coord pos = direction == SEARCH_EXTEND_LEFT + ? selection_get_start(term) : selection_get_end(term); + + xassert(pos.row >= 0); + xassert(pos.row < term->grid->num_rows); + + *target = pos; + + const struct grid *grid = term->grid; + + switch (direction) { + case SEARCH_EXTEND_LEFT: + pos.row = (pos.row - 1 + grid->num_rows) & (grid->num_rows - 1); + if (has_wrapped_around_right(term, pos.row)) + return; + break; + + case SEARCH_EXTEND_RIGHT: + pos.row = (pos.row + 1) & (grid->num_rows - 1); + if (has_wrapped_around_left(term, pos.row)) + return; + break; + } + + *target = pos; +} + +static void +search_extend_find_line_up(const struct terminal *term, struct coord *target) +{ + search_extend_find_line(term, target, SEARCH_EXTEND_LEFT); +} + +static void +search_extend_find_line_down(const struct terminal *term, struct coord *target) +{ + search_extend_find_line(term, target, SEARCH_EXTEND_RIGHT); +} + +static void +search_extend_left(struct terminal *term, const struct coord *target) +{ + const struct coord last_coord = selection_get_start(term); + struct coord pos = *target; + const struct row *row = term->grid->rows[pos.row]; + + const bool move_cursor = term->search.cursor != 0; + + struct extraction_context *ctx = extract_begin(SELECTION_NONE, false); + if (ctx == NULL) + return; + + while (pos.col != last_coord.col || pos.row != last_coord.row) { + if (!extract_one(term, row, &row->cells[pos.col], pos.col, ctx)) + break; + if (!coord_advance_right(term, &pos, &row)) + break; + } + + char32_t *new_text; + size_t new_len; + + if (!extract_finish_wide(ctx, &new_text, &new_len)) + return; + + if (!search_ensure_size(term, term->search.len + new_len)) + return; + + memmove(&term->search.buf[new_len], &term->search.buf[0], + term->search.len * sizeof(term->search.buf[0])); + + size_t actually_copied = 0; + for (size_t i = 0; i < new_len; i++) { + if (new_text[i] == U'\n') { + /* extract() adds newlines, which we never match against */ + continue; + } + + term->search.buf[actually_copied++] = new_text[i]; + term->search.len++; + } + + xassert(actually_copied <= new_len); + if (actually_copied < new_len) { + memmove( + &term->search.buf[actually_copied], &term->search.buf[new_len], + (term->search.len - actually_copied) * sizeof(term->search.buf[0])); + } + + term->search.buf[term->search.len] = U'\0'; + free(new_text); + + if (move_cursor) + term->search.cursor += actually_copied; + + struct range match = {.start = *target, .end = selection_get_end(term)}; + search_update_selection(term, &match); + + term->search.match_len = term->search.len; +} + +static void +search_extend_right(struct terminal *term, const struct coord *target) +{ + struct coord pos = selection_get_end(term); + const struct row *row = term->grid->rows[pos.row]; + + const bool move_cursor = term->search.cursor == term->search.len; struct extraction_context *ctx = extract_begin(SELECTION_NONE, false); if (ctx == NULL) return; do { - if (!advance_pos(pos)) + if (!coord_advance_right(term, &pos, &row)) break; if (!extract_one(term, row, &row->cells[pos.col], pos.col, ctx)) break; - } while (pos.col != new_end.col || pos.row != new_end.row); + } while (pos.col != target->col || pos.row != target->row); char32_t *new_text; size_t new_len; @@ -728,12 +956,9 @@ search_match_to_end_of_word(struct terminal *term, bool spaces_only) if (move_cursor) term->search.cursor = term->search.len; - struct range match = {.start = term->search.match, .end = new_end}; + struct range match = {.start = term->search.match, .end = *target}; search_update_selection(term, &match); - term->search.match_len = term->search.len; - -#undef advance_pos } static size_t @@ -822,6 +1047,20 @@ execute_binding(struct seat *seat, struct terminal *term, case BIND_ACTION_SEARCH_NONE: return false; + case BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, term->rows); + return true; + } + return false; + + case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, term->rows); + return true; + } + return false; + case BIND_ACTION_SEARCH_CANCEL: if (term->search.view_followed_offset) grid->view = grid->offset; @@ -967,17 +1206,77 @@ execute_binding(struct seat *seat, struct terminal *term, return true; } - case BIND_ACTION_SEARCH_EXTEND_WORD: - search_match_to_end_of_word(term, false); + case BIND_ACTION_SEARCH_EXTEND_CHAR: { + struct coord target; + search_extend_find_char_right(term, &target); + search_extend_right(term, &target); *update_search_result = false; *redraw = true; return true; + } - case BIND_ACTION_SEARCH_EXTEND_WORD_WS: - search_match_to_end_of_word(term, true); + case BIND_ACTION_SEARCH_EXTEND_WORD: { + struct coord target; + search_extend_find_word_right(term, false, &target); + search_extend_right(term, &target); *update_search_result = false; *redraw = true; return true; + } + + case BIND_ACTION_SEARCH_EXTEND_WORD_WS: { + struct coord target; + search_extend_find_word_right(term, true, &target); + search_extend_right(term, &target); + *update_search_result = false; + *redraw = true; + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_LINE_DOWN: { + struct coord target; + search_extend_find_line_down(term, &target); + search_extend_right(term, &target); + *update_search_result = false; + *redraw = true; + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR: { + struct coord target; + search_extend_find_char_left(term, &target); + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD: { + struct coord target; + search_extend_find_word_left(term, false, &target); + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD_WS: { + struct coord target; + search_extend_find_word_left(term, true, &target); + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_LINE_UP: { + struct coord target; + search_extend_find_line_up(term, &target); + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + return true; + } case BIND_ACTION_SEARCH_CLIPBOARD_PASTE: text_from_clipboard( diff --git a/selection.c b/selection.c index f03a3d5c..7b508828 100644 --- a/selection.c +++ b/selection.c @@ -340,16 +340,18 @@ selection_to_text(const struct terminal *term) return extract_finish(ctx, &text, NULL) ? text : NULL; } +/* Coordinates are in *absolute* row numbers (NOT view local) */ void -selection_find_word_boundary_left(struct terminal *term, struct coord *pos, +selection_find_word_boundary_left(const struct terminal *term, struct coord *pos, bool spaces_only) { xassert(pos->row >= 0); - xassert(pos->row < term->rows); + xassert(pos->row < term->grid->num_rows); xassert(pos->col >= 0); xassert(pos->col < term->cols); - const struct row *r = grid_row_in_view(term->grid, pos->row); + const struct grid *grid = term->grid; + const struct row *r = grid->rows[pos->row]; char32_t c = r->cells[pos->col].wc; while (c >= CELL_SPACER) { @@ -373,15 +375,22 @@ selection_find_word_boundary_left(struct terminal *term, struct coord *pos, int next_col = pos->col - 1; int next_row = pos->row; - const struct row *row = grid_row_in_view(term->grid, next_row); + const struct row *row = grid->rows[next_row]; /* Linewrap */ if (next_col < 0) { next_col = term->cols - 1; - if (--next_row < 0) - break; - row = grid_row_in_view(term->grid, next_row); + next_row = (next_row - 1 + grid->num_rows) & (grid->num_rows - 1); + + if (grid_row_abs_to_sb(grid, term->rows, next_row) == term->grid->num_rows - 1 || + grid->rows[next_row] == NULL) + { + /* Scrollback wrap-around */ + break; + } + + row = grid->rows[next_row]; if (row->linebreak) { /* Hard linebreak, treat as space. I.e. break selection */ @@ -418,17 +427,19 @@ selection_find_word_boundary_left(struct terminal *term, struct coord *pos, } } +/* Coordinates are in *absolute* row numbers (NOT view local) */ void -selection_find_word_boundary_right(struct terminal *term, struct coord *pos, +selection_find_word_boundary_right(const struct terminal *term, struct coord *pos, bool spaces_only, bool stop_on_space_to_word_boundary) { xassert(pos->row >= 0); - xassert(pos->row < term->rows); + xassert(pos->row < term->grid->num_rows); xassert(pos->col >= 0); xassert(pos->col < term->cols); - const struct row *r = grid_row_in_view(term->grid, pos->row); + const struct grid *grid = term->grid; + const struct row *r = grid->rows[pos->row]; char32_t c = r->cells[pos->col].wc; while (c >= CELL_SPACER) { @@ -453,7 +464,7 @@ selection_find_word_boundary_right(struct terminal *term, struct coord *pos, int next_col = pos->col + 1; int next_row = pos->row; - const struct row *row = grid_row_in_view(term->grid, next_row); + const struct row *row = term->grid->rows[next_row]; /* Linewrap */ if (next_col >= term->cols) { @@ -463,10 +474,14 @@ selection_find_word_boundary_right(struct terminal *term, struct coord *pos, } next_col = 0; - if (++next_row >= term->rows) - break; + next_row = (next_row + 1) & (grid->num_rows - 1); - row = grid_row_in_view(term->grid, next_row); + if (grid_row_abs_to_sb(grid, term->rows, next_row) == 0) { + /* Scrollback wrap-around */ + break; + } + + row = grid->rows[next_row]; } c = row->cells[next_col].wc; @@ -659,15 +674,15 @@ selection_start(struct terminal *term, int col, int row, break; case SELECTION_WORD_WISE: { - struct coord start = {col, row}, end = {col, row}; + struct coord start = {col, term->grid->view + row}; + struct coord end = {col, term->grid->view + row}; selection_find_word_boundary_left(term, &start, spaces_only); selection_find_word_boundary_right(term, &end, spaces_only, true); - term->selection.coords.start = (struct coord){ - start.col, term->grid->view + start.row}; + term->selection.coords.start = start; term->selection.pivot.start = term->selection.coords.start; - term->selection.pivot.end = (struct coord){end.col, term->grid->view + end.row}; + term->selection.pivot.end = end; selection_update(term, end.col, end.row); break; @@ -1085,22 +1100,26 @@ set_pivot_point_for_block_and_char_wise(struct terminal *term, void selection_update(struct terminal *term, int col, int row) { - if (term->selection.coords.start.row < 0) + if (term->selection.coords.start.row < 0) { + LOG_ERR("NO SELECTION"); return; + } - if (!term->selection.ongoing) + if (!term->selection.ongoing) { + LOG_ERR("NOT ON-GOING"); return; - - LOG_DBG("selection updated: start = %d,%d, end = %d,%d -> %d, %d", - term->selection.coords.start.row, term->selection.coords.start.col, - term->selection.coords.end.row, term->selection.coords.end.col, - row, col); + } xassert(term->grid->view + row != -1); struct coord new_start = term->selection.coords.start; struct coord new_end = {col, term->grid->view + row}; + LOG_DBG("selection updated: start = %d,%d, end = %d,%d -> %d, %d", + term->selection.coords.start.row, term->selection.coords.start.col, + term->selection.coords.end.row, term->selection.coords.end.col, + new_end.row, new_end.col); + /* Adjust start point if the selection has changed 'direction' */ if (!(new_end.row == new_start.row && new_end.col == new_start.col)) { enum selection_direction new_direction = term->selection.direction; @@ -1160,21 +1179,17 @@ selection_update(struct terminal *term, int col, int row) case SELECTION_WORD_WISE: switch (term->selection.direction) { - case SELECTION_LEFT: { - struct coord end = {col, row}; + case SELECTION_LEFT: + new_end = (struct coord){col, term->grid->view + row}; selection_find_word_boundary_left( - term, &end, term->selection.spaces_only); - new_end = (struct coord){end.col, term->grid->view + end.row}; + term, &new_end, term->selection.spaces_only); break; - } - case SELECTION_RIGHT: { - struct coord end = {col, row}; + case SELECTION_RIGHT: + new_end = (struct coord){col, term->grid->view + row}; selection_find_word_boundary_right( - term, &end, term->selection.spaces_only, true); - new_end = (struct coord){end.col, term->grid->view + end.row}; + term, &new_end, term->selection.spaces_only, true); break; - } case SELECTION_UNDIR: break; @@ -1346,16 +1361,14 @@ selection_extend_normal(struct terminal *term, int col, int row, xassert(new_kind == SELECTION_CHAR_WISE || new_kind == SELECTION_WORD_WISE); - struct coord pivot_start = {new_start.col, new_start.row - term->grid->view}; + struct coord pivot_start = {new_start.col, new_start.row}; struct coord pivot_end = pivot_start; selection_find_word_boundary_left(term, &pivot_start, spaces_only); selection_find_word_boundary_right(term, &pivot_end, spaces_only, true); - term->selection.pivot.start = - (struct coord){pivot_start.col, term->grid->view + pivot_start.row}; - term->selection.pivot.end = - (struct coord){pivot_end.col, term->grid->view + pivot_end.row}; + term->selection.pivot.start = pivot_start; + term->selection.pivot.end = pivot_end; break; } diff --git a/selection.h b/selection.h index c6d7f968..26298457 100644 --- a/selection.h +++ b/selection.h @@ -78,9 +78,9 @@ void selection_start_scroll_timer( void selection_stop_scroll_timer(struct terminal *term); void selection_find_word_boundary_left( - struct terminal *term, struct coord *pos, bool spaces_only); + const struct terminal *term, struct coord *pos, bool spaces_only); void selection_find_word_boundary_right( - struct terminal *term, struct coord *pos, bool spaces_only, + const struct terminal *term, struct coord *pos, bool spaces_only, bool stop_on_space_to_word_boundary); struct coord selection_get_start(const struct terminal *term); From 5e013cad78a1989f4deb0972a465dfcdad0dc1d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 27 Sep 2023 16:23:30 +0200 Subject: [PATCH 0496/1323] selection: selection_update() uses view-local coordinates --- selection.c | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/selection.c b/selection.c index 7b508828..8a3fd781 100644 --- a/selection.c +++ b/selection.c @@ -684,7 +684,16 @@ selection_start(struct terminal *term, int col, int row, term->selection.pivot.start = term->selection.coords.start; term->selection.pivot.end = end; - selection_update(term, end.col, end.row); + /* + * FIXME: go through selection.c and make sure all public + * functions use the *same* coordinate system... + * + * selection_find_word_boundary*() uses absolute row numbers, + * while selection_update(), and pretty much all others, use + * view-local. + */ + + selection_update(term, end.col, end.row - term->grid->view); break; } @@ -1100,15 +1109,11 @@ set_pivot_point_for_block_and_char_wise(struct terminal *term, void selection_update(struct terminal *term, int col, int row) { - if (term->selection.coords.start.row < 0) { - LOG_ERR("NO SELECTION"); + if (term->selection.coords.start.row < 0) return; - } - if (!term->selection.ongoing) { - LOG_ERR("NOT ON-GOING"); + if (!term->selection.ongoing) return; - } xassert(term->grid->view + row != -1); From ddf4eb3b78ebe590d0dd6675536f9a7e4043ee71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 27 Sep 2023 18:36:52 +0200 Subject: [PATCH 0497/1323] search: don't try to extend a search match when there is none --- search.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/search.c b/search.c index 1fb6bb3e..185a83ca 100644 --- a/search.c +++ b/search.c @@ -705,6 +705,9 @@ static void search_extend_find_char(const struct terminal *term, struct coord *target, enum extend_direction direction) { + if (term->search.match_len == 0) + return; + struct coord pos = direction == SEARCH_EXTEND_LEFT ? selection_get_start(term) : selection_get_end(term); xassert(pos.row >= 0); @@ -753,11 +756,14 @@ static void search_extend_find_word(const struct terminal *term, bool spaces_only, struct coord *target, enum extend_direction direction) { + if (term->search.match_len == 0) + return; + struct grid *grid = term->grid; struct coord pos = direction == SEARCH_EXTEND_LEFT ? selection_get_start(term) : selection_get_end(term); - + xassert(pos.row >= 0); xassert(pos.row < grid->num_rows); @@ -812,6 +818,9 @@ static void search_extend_find_line(const struct terminal *term, struct coord *target, enum extend_direction direction) { + if (term->search.match_len == 0) + return; + struct coord pos = direction == SEARCH_EXTEND_LEFT ? selection_get_start(term) : selection_get_end(term); @@ -854,6 +863,9 @@ search_extend_find_line_down(const struct terminal *term, struct coord *target) static void search_extend_left(struct terminal *term, const struct coord *target) { + if (term->search.match_len == 0) + return; + const struct coord last_coord = selection_get_start(term); struct coord pos = *target; const struct row *row = term->grid->rows[pos.row]; @@ -916,6 +928,9 @@ search_extend_left(struct terminal *term, const struct coord *target) static void search_extend_right(struct terminal *term, const struct coord *target) { + if (term->search.match_len == 0) + return; + struct coord pos = selection_get_end(term); const struct row *row = term->grid->rows[pos.row]; From ca128ae3802dde3b2bafab190a09911589d23e8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 3 Oct 2023 14:07:41 +0200 Subject: [PATCH 0498/1323] selection: find_word_boundary: ensure row number is bounded --- selection.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/selection.c b/selection.c index 8a3fd781..50e41637 100644 --- a/selection.c +++ b/selection.c @@ -345,12 +345,13 @@ void selection_find_word_boundary_left(const struct terminal *term, struct coord *pos, bool spaces_only) { - xassert(pos->row >= 0); - xassert(pos->row < term->grid->num_rows); + const struct grid *grid = term->grid; + xassert(pos->col >= 0); xassert(pos->col < term->cols); + xassert(pos->row >= 0); + pos->row &= grid->num_rows - 1; - const struct grid *grid = term->grid; const struct row *r = grid->rows[pos->row]; char32_t c = r->cells[pos->col].wc; @@ -433,12 +434,13 @@ selection_find_word_boundary_right(const struct terminal *term, struct coord *po bool spaces_only, bool stop_on_space_to_word_boundary) { - xassert(pos->row >= 0); - xassert(pos->row < term->grid->num_rows); + const struct grid *grid = term->grid; + xassert(pos->col >= 0); xassert(pos->col < term->cols); + xassert(pos->row >= 0); + pos->row &= grid->num_rows - 1; - const struct grid *grid = term->grid; const struct row *r = grid->rows[pos->row]; char32_t c = r->cells[pos->col].wc; From 419f0be44106f6be42a2fa9465e92122a535e26f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 8 Oct 2023 10:10:03 +0200 Subject: [PATCH 0499/1323] config: map ctrl+shift+right to extend-to-word-boundary --- config.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.c b/config.c index 15ec52ff..8bbe27cb 100644 --- a/config.c +++ b/config.c @@ -2853,11 +2853,11 @@ add_default_search_bindings(struct config *conf) {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_BACKWARD_WORD_WS, m_ctrl_shift_alt, {{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}}}, From 6a708b35ee62b6e6a53cb61daf397352c33e5fdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 8 Oct 2023 10:16:48 +0200 Subject: [PATCH 0500/1323] foot.ini: document all the new search.extend* bindings --- doc/foot.ini.5.scd | 30 ++++++++++++++++++++++++++---- foot.ini | 8 +++++++- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 24c42a5e..2e0127b1 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -945,13 +945,35 @@ scrollback search mode. The syntax is exactly the same as the regular Deletes the **word after** the cursor. Default: _Mod1+d Control+Delete_. +*extend-char* + Extend current selection to the right, by one character. Default: + _Shift+Right_. + *extend-to-word-boundary* - Extend current selection to the next word boundary. Default: - _Control+w_. + Extend current selection to the right, to the next word + boundary. Default: _Control+w Control+Shift+Right_. *extend-to-next-whitespace* - Extend the current selection to the next whitespace. Default: - _Control+Shift+w_. + Extend the current selection to the right, to the next + whitespace. Default: _Control+Shift+w_. + +*extend-line-down* + Extend current selection down one line. Default: _Shift+Down_. + +*extend-backward-char* + Extend current selection to the left, by one character. Default: + _Shift+Left_. + +*extend-backward-to-word-boundary* + Extend current selection to the left, to the next word + boundary. Default: _Control+Shift+Left_. + +*extend-backward-to-next-whitespace* + Extend the current selection to the left, to the next + whitespace. Default: _none_. + +*extend-line-up* + Extend current selection up one line. Default: _Shift+Up_. *clipboard-paste* Paste from the _clipboard_ into the search buffer. Default: diff --git a/foot.ini b/foot.ini index 00505165..dbd2baab 100644 --- a/foot.ini +++ b/foot.ini @@ -176,8 +176,14 @@ # delete-prev-word=Mod1+BackSpace Control+BackSpace # delete-next=Delete # delete-next-word=Mod1+d Control+Delete -# extend-to-word-boundary=Control+w +# extend-char=Shift+Right +# extend-to-word-boundary=Control+w Control+Shift+Right # extend-to-next-whitespace=Control+Shift+w +# extend-line-down=Shift+Down +# extend-backward-char=Shift+Left +# extend-backward-to-word-boundary=Control+Shift+Left +# extend-backward-to-next-whitespace=none +# extend-line-up=Shift+Up # clipboard-paste=Control+v Control+Shift+v Control+y XF86Paste # primary-paste=Shift+Insert # unicode-input=none From a772179b6c850aad32f52d51c384ad933746c0cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 8 Oct 2023 10:28:17 +0200 Subject: [PATCH 0501/1323] search: fix mixup in search_extend_find_line() The has_wrapped_around_{right,left} functions were mixed up, causing false positives and false negatives, resulting in bad search matches. Also make all search_extend_find* functions return a boolean; false means no change in the selection. In this case, we can skip trying to extend the selection, and updating the UI. --- search.c | 128 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 69 insertions(+), 59 deletions(-) diff --git a/search.c b/search.c index 185a83ca..90dd337c 100644 --- a/search.c +++ b/search.c @@ -701,12 +701,12 @@ coord_advance_right(const struct terminal *term, struct coord *pos, return true; } -static void +static bool search_extend_find_char(const struct terminal *term, struct coord *target, enum extend_direction direction) { if (term->search.match_len == 0) - return; + return false; struct coord pos = direction == SEARCH_EXTEND_LEFT ? selection_get_start(term) : selection_get_end(term); @@ -721,12 +721,12 @@ search_extend_find_char(const struct terminal *term, struct coord *target, switch (direction) { case SEARCH_EXTEND_LEFT: if (!coord_advance_left(term, &pos, &row)) - return; + return false; break; case SEARCH_EXTEND_RIGHT: if (!coord_advance_right(term, &pos, &row)) - return; + return false; break; } @@ -736,28 +736,28 @@ search_extend_find_char(const struct terminal *term, struct coord *target, continue; *target = pos; - return; + return true; } } -static void +static bool search_extend_find_char_left(const struct terminal *term, struct coord *target) { - search_extend_find_char(term, target, SEARCH_EXTEND_LEFT); + return search_extend_find_char(term, target, SEARCH_EXTEND_LEFT); } -static void +static bool search_extend_find_char_right(const struct terminal *term, struct coord *target) { - search_extend_find_char(term, target, SEARCH_EXTEND_RIGHT); + return search_extend_find_char(term, target, SEARCH_EXTEND_RIGHT); } -static void +static bool search_extend_find_word(const struct terminal *term, bool spaces_only, struct coord *target, enum extend_direction direction) { if (term->search.match_len == 0) - return; + return false; struct grid *grid = term->grid; struct coord pos = direction == SEARCH_EXTEND_LEFT @@ -773,12 +773,12 @@ search_extend_find_word(const struct terminal *term, bool spaces_only, switch (direction) { case SEARCH_EXTEND_LEFT: if (!coord_advance_left(term, &pos, NULL)) - return; + return false; break; case SEARCH_EXTEND_RIGHT: if (!coord_advance_right(term, &pos, NULL)) - return; + return false; break; } @@ -798,28 +798,29 @@ search_extend_find_word(const struct terminal *term, bool spaces_only, } *target = pos; + return true; } -static void +static bool search_extend_find_word_left(const struct terminal *term, bool spaces_only, struct coord *target) { - search_extend_find_word(term, spaces_only, target, SEARCH_EXTEND_LEFT); + return search_extend_find_word(term, spaces_only, target, SEARCH_EXTEND_LEFT); } -static void +static bool search_extend_find_word_right(const struct terminal *term, bool spaces_only, struct coord *target) { - search_extend_find_word(term, spaces_only, target, SEARCH_EXTEND_RIGHT); + return search_extend_find_word(term, spaces_only, target, SEARCH_EXTEND_RIGHT); } -static void +static bool search_extend_find_line(const struct terminal *term, struct coord *target, enum extend_direction direction) { if (term->search.match_len == 0) - return; + return false; struct coord pos = direction == SEARCH_EXTEND_LEFT ? selection_get_start(term) : selection_get_end(term); @@ -834,30 +835,31 @@ search_extend_find_line(const struct terminal *term, struct coord *target, switch (direction) { case SEARCH_EXTEND_LEFT: pos.row = (pos.row - 1 + grid->num_rows) & (grid->num_rows - 1); - if (has_wrapped_around_right(term, pos.row)) - return; + if (has_wrapped_around_left(term, pos.row)) + return false; break; case SEARCH_EXTEND_RIGHT: pos.row = (pos.row + 1) & (grid->num_rows - 1); - if (has_wrapped_around_left(term, pos.row)) - return; + if (has_wrapped_around_right(term, pos.row)) + return false; break; } *target = pos; + return true; } -static void +static bool search_extend_find_line_up(const struct terminal *term, struct coord *target) { - search_extend_find_line(term, target, SEARCH_EXTEND_LEFT); + return search_extend_find_line(term, target, SEARCH_EXTEND_LEFT); } -static void +static bool search_extend_find_line_down(const struct terminal *term, struct coord *target) { - search_extend_find_line(term, target, SEARCH_EXTEND_RIGHT); + return search_extend_find_line(term, target, SEARCH_EXTEND_RIGHT); } static void @@ -1223,73 +1225,81 @@ execute_binding(struct seat *seat, struct terminal *term, case BIND_ACTION_SEARCH_EXTEND_CHAR: { struct coord target; - search_extend_find_char_right(term, &target); - search_extend_right(term, &target); - *update_search_result = false; - *redraw = true; + if (search_extend_find_char_right(term, &target)) { + search_extend_right(term, &target); + *update_search_result = false; + *redraw = true; + } return true; } case BIND_ACTION_SEARCH_EXTEND_WORD: { struct coord target; - search_extend_find_word_right(term, false, &target); - search_extend_right(term, &target); - *update_search_result = false; - *redraw = true; + if (search_extend_find_word_right(term, false, &target)) { + search_extend_right(term, &target); + *update_search_result = false; + *redraw = true; + } return true; } case BIND_ACTION_SEARCH_EXTEND_WORD_WS: { struct coord target; - search_extend_find_word_right(term, true, &target); - search_extend_right(term, &target); - *update_search_result = false; - *redraw = true; + if (search_extend_find_word_right(term, true, &target)) { + search_extend_right(term, &target); + *update_search_result = false; + *redraw = true; + } return true; } case BIND_ACTION_SEARCH_EXTEND_LINE_DOWN: { struct coord target; - search_extend_find_line_down(term, &target); - search_extend_right(term, &target); - *update_search_result = false; - *redraw = true; + if (search_extend_find_line_down(term, &target)) { + search_extend_right(term, &target); + *update_search_result = false; + *redraw = true; + } return true; } case BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR: { struct coord target; - search_extend_find_char_left(term, &target); - search_extend_left(term, &target); - *update_search_result = false; - *redraw = true; + if (search_extend_find_char_left(term, &target)) { + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + } return true; } case BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD: { struct coord target; - search_extend_find_word_left(term, false, &target); - search_extend_left(term, &target); - *update_search_result = false; - *redraw = true; + if (search_extend_find_word_left(term, false, &target)) { + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + } return true; } case BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD_WS: { struct coord target; - search_extend_find_word_left(term, true, &target); - search_extend_left(term, &target); - *update_search_result = false; - *redraw = true; + if (search_extend_find_word_left(term, true, &target)) { + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + } return true; } case BIND_ACTION_SEARCH_EXTEND_LINE_UP: { struct coord target; - search_extend_find_line_up(term, &target); - search_extend_left(term, &target); - *update_search_result = false; - *redraw = true; + if (search_extend_find_line_up(term, &target)) { + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + } return true; } From 3e67415e3e7b09c3e10c4ace3008bbaaae54110a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 8 Oct 2023 10:37:16 +0200 Subject: [PATCH 0502/1323] config: add remaining search.scrollback key bindings All scrollback up/down key bindings are now available in search mode. --- config.c | 10 ++++++++-- doc/foot.ini.5.scd | 25 +++++++++++++++++++++++++ foot.ini | 10 ++++++++++ key-binding.h | 7 ++++++- search.c | 42 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 91 insertions(+), 3 deletions(-) diff --git a/config.c b/config.c index 8bbe27cb..e5ebb558 100644 --- a/config.c +++ b/config.c @@ -134,6 +134,14 @@ static const char *const binding_action_map[] = { static const char *const search_binding_action_map[] = { [BIND_ACTION_SEARCH_NONE] = NULL, + [BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE] = "scrollback-up-page", + [BIND_ACTION_SEARCH_SCROLLBACK_UP_HALF_PAGE] = "scrollback-up-half-page", + [BIND_ACTION_SEARCH_SCROLLBACK_UP_LINE] = "scrollback-up-line", + [BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE] = "scrollback-down-page", + [BIND_ACTION_SEARCH_SCROLLBACK_DOWN_HALF_PAGE] = "scrollback-down-half-page", + [BIND_ACTION_SEARCH_SCROLLBACK_DOWN_LINE] = "scrollback-down-line", + [BIND_ACTION_SEARCH_SCROLLBACK_HOME] = "scrollback-home", + [BIND_ACTION_SEARCH_SCROLLBACK_END] = "scrollback-end", [BIND_ACTION_SEARCH_CANCEL] = "cancel", [BIND_ACTION_SEARCH_COMMIT] = "commit", [BIND_ACTION_SEARCH_FIND_PREV] = "find-prev", @@ -159,8 +167,6 @@ static const char *const search_binding_action_map[] = { [BIND_ACTION_SEARCH_CLIPBOARD_PASTE] = "clipboard-paste", [BIND_ACTION_SEARCH_PRIMARY_PASTE] = "primary-paste", [BIND_ACTION_SEARCH_UNICODE_INPUT] = "unicode-input", - [BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE] = "scrollback-up-page", - [BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE] = "scrollback-down-page", }; static const char *const url_binding_action_map[] = { diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 2e0127b1..5ef4dcb4 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -987,6 +987,31 @@ scrollback search mode. The syntax is exactly the same as the regular Unicode input mode. See _key-bindings.unicode-input_ for details. Default: _none_. +*scrollback-up-page* + Scrolls up/back one page in history. Default: _Shift+Page\_Up_. + +*scrollback-up-half-page* + Scrolls up/back half of a page in history. Default: _none_. + +*scrollback-up-line* + Scrolls up/back a single line in history. Default: _none_. + +*scrollback-down-page* + Scroll down/forward one page in history. Default: + _Shift+Page\_Down_. + +*scrollback-down-half-page* + Scroll down/forward half of a page in history. Default: _none_. + +*scrollback-down-line* + Scroll down/forward a single line in history. Default: _none_. + +*scrollback-home* + Scroll to the beginning of the scrollback. Default: _none_. + +*scrollback-end* + Scroll to the end (bottom) of the scrollback. Default: _none_. + # SECTION: url-bindings This section lets you override the default key bindings used in URL diff --git a/foot.ini b/foot.ini index dbd2baab..cdbd8259 100644 --- a/foot.ini +++ b/foot.ini @@ -139,6 +139,8 @@ # scrollback-down-page=Shift+Page_Down # scrollback-down-half-page=none # scrollback-down-line=none +# scrollback-home=none +# scrollback-end=none # clipboard-copy=Control+Shift+c XF86Copy # clipboard-paste=Control+Shift+v XF86Paste # primary-paste=Shift+Insert @@ -187,6 +189,14 @@ # clipboard-paste=Control+v Control+Shift+v Control+y XF86Paste # primary-paste=Shift+Insert # unicode-input=none +# scrollback-up-page=Shift+Page_Up +# scrollback-up-half-page=none +# scrollback-up-line=none +# scrollback-down-page=Shift+Page_Down +# scrollback-down-half-page=none +# scrollback-down-line=none +# scrollback-home=none +# scrollback-end=none [url-bindings] # cancel=Control+g Control+c Control+d Escape diff --git a/key-binding.h b/key-binding.h index b85fc072..050c80a6 100644 --- a/key-binding.h +++ b/key-binding.h @@ -59,8 +59,13 @@ enum bind_action_normal { enum bind_action_search { BIND_ACTION_SEARCH_NONE, BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE, + BIND_ACTION_SEARCH_SCROLLBACK_UP_HALF_PAGE, + BIND_ACTION_SEARCH_SCROLLBACK_UP_LINE, BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE, - // TODO: copy the remaining scrollback key-bindings from normal mode + BIND_ACTION_SEARCH_SCROLLBACK_DOWN_HALF_PAGE, + BIND_ACTION_SEARCH_SCROLLBACK_DOWN_LINE, + BIND_ACTION_SEARCH_SCROLLBACK_HOME, + BIND_ACTION_SEARCH_SCROLLBACK_END, BIND_ACTION_SEARCH_CANCEL, BIND_ACTION_SEARCH_COMMIT, BIND_ACTION_SEARCH_FIND_PREV, diff --git a/search.c b/search.c index 90dd337c..55388577 100644 --- a/search.c +++ b/search.c @@ -1071,6 +1071,20 @@ execute_binding(struct seat *seat, struct terminal *term, } return false; + case BIND_ACTION_SEARCH_SCROLLBACK_UP_HALF_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, max(term->rows / 2, 1)); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_UP_LINE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, 1); + return true; + } + break; + case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE: if (term->grid == &term->normal) { cmd_scrollback_down(term, term->rows); @@ -1078,6 +1092,34 @@ execute_binding(struct seat *seat, struct terminal *term, } return false; + case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_HALF_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, max(term->rows / 2, 1)); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_LINE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, 1); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_HOME: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, term->grid->num_rows); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_END: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, term->grid->num_rows); + return true; + } + break; + case BIND_ACTION_SEARCH_CANCEL: if (term->search.view_followed_offset) grid->view = grid->offset; From 6970055dcae5d98bb78708e248ff09b5f79a4541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 8 Oct 2023 10:39:57 +0200 Subject: [PATCH 0503/1323] changelog: add the remaining scrollback-up/down bindings --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 912289a1..d8a22a71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,10 +66,14 @@ - `extend-line-down` (shift+down) - `extend-backward-char` (shift+left) - `extend-backward-to-word-boundary` (ctrl+shift+left) - - `extend-backward-to-next-whitespace` (ctrl+alt+shift+left) + - `extend-backward-to-next-whitespace` (none) - `extend-line-up` (shift+up) - `scrollback-up-page` (shift+page-up) + - `scrollback-up-half-page` (none) + - `scrollback-up-line` (none) - `scrollback-down-page` (shift+page-down) + - `scrollback-down-half-page` (none) + - `scrollback-down-line` (none) [1077]: https://codeberg.org/dnkl/foot/issues/1077 [1364]: https://codeberg.org/dnkl/foot/issues/1364 From e41555fe0ff0882b79b49c1f00c6b23caa1eaf70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 8 Oct 2023 11:03:13 +0200 Subject: [PATCH 0504/1323] shm: move definition of FOOT_MFD_FLAGS to the top --- shm.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/shm.c b/shm.c index d543e1f4..5bf5b311 100644 --- a/shm.c +++ b/shm.c @@ -27,6 +27,12 @@ #define MAP_UNINITIALIZED 0 #endif +#if defined(MFD_NOEXEC_SEAL) + #define FOOT_MFD_FLAGS (MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_NOEXEC_SEAL) +#else + #define FOOT_MFD_FLAGS (MFD_CLOEXEC | MFD_ALLOW_SEALING) +#endif + #define TIME_SCROLL 0 #define FORCED_DOUBLE_BUFFERING 0 @@ -330,11 +336,6 @@ get_new_buffers(struct buffer_chain *chain, size_t count, struct buffer_pool *pool = NULL; /* Backing memory for SHM */ -#if defined(MFD_NOEXEC_SEAL) - #define FOOT_MFD_FLAGS (MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_NOEXEC_SEAL) -#else - #define FOOT_MFD_FLAGS (MFD_CLOEXEC | MFD_ALLOW_SEALING) -#endif #if defined(MEMFD_CREATE) pool_fd = memfd_create("foot-wayland-shm-buffer-pool", FOOT_MFD_FLAGS); #elif defined(__FreeBSD__) From 1c9d98d57e35a8db3deaa1ba1951b4ee9dc1d149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 8 Oct 2023 16:52:21 +0200 Subject: [PATCH 0505/1323] config: log_contextual_errno(): sync with log_contextual() ... in terms of whether to print section/value separators --- config.c | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/config.c b/config.c index e5ebb558..4722cfdd 100644 --- a/config.c +++ b/config.c @@ -283,10 +283,19 @@ log_contextual_errno(struct context *ctx, const char *file, int lineno, char *formatted_msg = xvasprintf(fmt, va); va_end(va); + bool print_dot = ctx->key != NULL; + bool print_colon = ctx->value != NULL; + + if (!print_dot) + ctx->key = ""; + + if (!print_colon) + ctx->value = ""; + log_and_notify_errno( - ctx->conf, file, lineno, "%s:%d: [%s].%s: %s: %s", - ctx->path, ctx->lineno, ctx->section, ctx->key, ctx->value, - formatted_msg); + ctx->conf, file, lineno, "%s:%d: [%s]%s%s%s%s: %s", + ctx->path, ctx->lineno, ctx->section, print_dot ? "." : "", + ctx->key, print_colon ? ": " : "", ctx->value, formatted_msg); free(formatted_msg); } From c50b1f990093a2a453288b976ef7f6d9a303f793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 7 Oct 2023 16:23:09 +0200 Subject: [PATCH 0506/1323] render: more fine-grained wayland surface damage tracking Before this patch. Wayland surface damage tracking was done on a per-row basis. That is, even if just one cell was updated, the entire row was "damaged". Now, damage is per cell. This hopefully results in lower latencies in many use cases, and especially on high DPI monitors. --- CHANGELOG.md | 3 ++ render.c | 95 +++++++++++++++++++++++++++------------------------- shm.c | 14 ++++++-- shm.h | 13 ++++++- 4 files changed, 75 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8a22a71..667b1662 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,9 @@ * Command line configuration overrides are now applied even if the configuration file does not exist or can't be parsed. ([#1495][1495]). +* Wayland surface damage is now more fine-grained. This should result + in lower latencies in many use cases, especially on high DPI + monitors. [1391]: https://codeberg.org/dnkl/foot/issues/1391 [1448]: https://codeberg.org/dnkl/foot/pulls/1448 diff --git a/render.c b/render.c index 3101d36d..9296223a 100644 --- a/render.c +++ b/render.c @@ -459,8 +459,8 @@ draw_cursor(const struct terminal *term, const struct cell *cell, } static int -render_cell(struct terminal *term, pixman_image_t *pix, - struct row *row, int col, int row_no, bool has_cursor) +render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damage, + struct row *row, int row_no, int col, bool has_cursor) { struct cell *cell = &row->cells[col]; if (cell->attrs.clean) @@ -716,6 +716,12 @@ render_cell(struct terminal *term, pixman_image_t *pix, &clip, x, y, render_width, term->cell_height); pixman_image_set_clip_region32(pix, &clip); + + if (damage != NULL) { + pixman_region32_union_rect( + damage, damage, x, y, render_width, term->cell_height); + } + pixman_region32_fini(&clip); /* Background */ @@ -842,11 +848,11 @@ draw_cursor: } static void -render_row(struct terminal *term, pixman_image_t *pix, struct row *row, - int row_no, int cursor_col) +render_row(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damage, + struct row *row, int row_no, int cursor_col) { for (int col = term->cols - 1; col >= 0; col--) - render_cell(term, pix, row, col, row_no, cursor_col == col); + render_cell(term, pix, damage, row, row_no, col, cursor_col == col); } static void @@ -918,13 +924,13 @@ render_margin(struct terminal *term, struct buffer *buf, /* Ensure the updated regions are copied to the next frame's * buffer when we're double buffering */ pixman_region32_union_rect( - &buf->dirty, &buf->dirty, 0, 0, term->width, term->margins.top); + &buf->dirty[0], &buf->dirty[0], 0, 0, term->width, term->margins.top); pixman_region32_union_rect( - &buf->dirty, &buf->dirty, 0, bmargin, term->width, term->margins.bottom); + &buf->dirty[0], &buf->dirty[0], 0, bmargin, term->width, term->margins.bottom); pixman_region32_union_rect( - &buf->dirty, &buf->dirty, 0, 0, term->margins.left, term->height); + &buf->dirty[0], &buf->dirty[0], 0, 0, term->margins.left, term->height); pixman_region32_union_rect( - &buf->dirty, &buf->dirty, + &buf->dirty[0], &buf->dirty[0], rmargin, 0, term->margins.right, term->height); if (apply_damage) { @@ -1060,7 +1066,7 @@ grid_render_scroll(struct terminal *term, struct buffer *buf, * last frame’s damage (see reapply_old_damage() */ pixman_region32_union_rect( - &buf->dirty, &buf->dirty, 0, dst_y, buf->width, height); + &buf->dirty[0], &buf->dirty[0], 0, dst_y, buf->width, height); } static void @@ -1137,7 +1143,7 @@ grid_render_scroll_reverse(struct terminal *term, struct buffer *buf, * last frame’s damage (see reapply_old_damage() */ pixman_region32_union_rect( - &buf->dirty, &buf->dirty, 0, dst_y, buf->width, height); + &buf->dirty[0], &buf->dirty[0], 0, dst_y, buf->width, height); } static void @@ -1278,7 +1284,7 @@ render_sixel(struct terminal *term, pixman_image_t *pix, if (!sixel->opaque) { /* TODO: multithreading */ int cursor_col = cursor->row == term_row_no ? cursor->col : -1; - render_row(term, pix, row, term_row_no, cursor_col); + render_row(term, pix, NULL, row, term_row_no, cursor_col); } else { for (int col = sixel->pos.col; col < min(sixel->pos.col + sixel->cols, term->cols); @@ -1293,7 +1299,7 @@ render_sixel(struct terminal *term, pixman_image_t *pix, if ((last_row_needs_erase && last_row) || (last_col_needs_erase && last_col)) { - render_cell(term, pix, row, col, term_row_no, cursor_col == col); + render_cell(term, pix, NULL, row, term_row_no, col, cursor_col == col); } else { cell->attrs.clean = 1; cell->attrs.confined = 1; @@ -1464,7 +1470,7 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, break; row->cells[col_idx + i] = *cell; - render_cell(term, buf->pix[0], row, col_idx + i, row_idx, false); + render_cell(term, buf->pix[0], NULL, row, row_idx, col_idx + i, false); } int start = seat->ime.preedit.cursor.start - ime_ofs; @@ -1791,7 +1797,8 @@ render_worker_thread(void *_ctx) struct row *row = grid_row_in_view(term->grid, row_no); int cursor_col = cursor.row == row_no ? cursor.col : -1; - render_row(term, buf->pix[my_id], row, row_no, cursor_col); + render_row(term, buf->pix[my_id], &buf->dirty[my_id], + row, row_no, cursor_col); break; } @@ -2737,11 +2744,11 @@ reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old * current frame’s scroll damage *first*. This is done later, * when rendering the frame. */ - pixman_region32_subtract(&dirty, &old->dirty, &dirty); + pixman_region32_subtract(&dirty, &old->dirty[0], &dirty); pixman_image_set_clip_region32(new->pix[0], &dirty); } else { /* Copy *all* of last frame’s damaged areas */ - pixman_image_set_clip_region32(new->pix[0], &old->dirty); + pixman_image_set_clip_region32(new->pix[0], &old->dirty[0]); } pixman_image_composite32( @@ -2966,28 +2973,14 @@ grid_render(struct terminal *term) xassert(tll_length(term->render.workers.queue) == 0); } - int first_dirty_row = -1; + pixman_region32_t damage; + pixman_region32_init(&damage); + for (int r = 0; r < term->rows; r++) { struct row *row = grid_row_in_view(term->grid, r); - if (!row->dirty) { - if (first_dirty_row >= 0) { - int x = term->margins.left; - int y = term->margins.top + first_dirty_row * term->cell_height; - int width = term->width - term->margins.left - term->margins.right; - int height = (r - first_dirty_row) * term->cell_height; - - wl_surface_damage_buffer( - term->window->surface.surf, x, y, width, height); - pixman_region32_union_rect( - &buf->dirty, &buf->dirty, 0, y, buf->width, height); - } - first_dirty_row = -1; + if (!row->dirty) continue; - } - - if (first_dirty_row < 0) - first_dirty_row = r; row->dirty = false; @@ -2995,21 +2988,12 @@ grid_render(struct terminal *term) tll_push_back(term->render.workers.queue, r); else { + /* TODO: damage region */ int cursor_col = cursor.row == r ? cursor.col : -1; - render_row(term, buf->pix[0], row, r, cursor_col); + render_row(term, buf->pix[0], &damage, row, r, cursor_col); } } - if (first_dirty_row >= 0) { - int x = term->margins.left; - int y = term->margins.top + first_dirty_row * term->cell_height; - int width = term->width - term->margins.left - term->margins.right; - int height = (term->rows - first_dirty_row) * term->cell_height; - - wl_surface_damage_buffer(term->window->surface.surf, x, y, width, height); - pixman_region32_union_rect(&buf->dirty, &buf->dirty, 0, y, buf->width, height); - } - /* Signal workers the frame is done */ if (term->render.workers.count > 0) { for (size_t i = 0; i < term->render.workers.count; i++) @@ -3021,6 +3005,25 @@ grid_render(struct terminal *term) term->render.workers.buf = NULL; } + for (size_t i = 0; i < term->render.workers.count; i++) + pixman_region32_union(&damage, &damage, &buf->dirty[i + 1]); + + pixman_region32_union(&buf->dirty[0], &buf->dirty[0], &damage); + + { + int box_count = 0; + pixman_box32_t *boxes = pixman_region32_rectangles(&damage, &box_count); + + for (size_t i = 0; i < box_count; i++) { + wl_surface_damage_buffer( + term->window->surface.surf, + boxes[i].x1, boxes[i].y1, + boxes[i].x2 - boxes[i].x1, boxes[i].y2 - boxes[i].y1); + } + } + + pixman_region32_fini(&damage); + render_overlay(term); render_ime_preedit(term, buf); render_scrollback_position(term); diff --git a/shm.c b/shm.c index 5bf5b311..8ca0ead0 100644 --- a/shm.c +++ b/shm.c @@ -157,7 +157,9 @@ buffer_destroy(struct buffer_private *buf) pool_unref(buf->pool); buf->pool = NULL; - pixman_region32_fini(&buf->public.dirty); + for (size_t i = 0; i < buf->public.pix_instances; i++) + pixman_region32_fini(&buf->public.dirty[i]); + free(buf->public.dirty); free(buf); } @@ -476,7 +478,12 @@ get_new_buffers(struct buffer_chain *chain, size_t count, else tll_push_front(chain->bufs, buf); - pixman_region32_init(&buf->public.dirty); + buf->public.dirty = malloc( + chain->pix_instances * sizeof(buf->public.dirty[0])); + + for (size_t j = 0; j < chain->pix_instances; j++) + pixman_region32_init(&buf->public.dirty[j]); + pool->ref_count++; offset += buf->size; bufs[i] = &buf->public; @@ -585,7 +592,8 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height) if (cached != NULL) { LOG_DBG("re-using buffer %p from cache", (void *)cached); cached->busy = true; - pixman_region32_clear(&cached->public.dirty); + for (size_t i = 0; i < cached->public.pix_instances; i++) + pixman_region32_clear(&cached->public.dirty[i]); xassert(cached->public.pix_instances == chain->pix_instances); return &cached->public; } diff --git a/shm.h b/shm.h index 440cfa1d..f9e90a23 100644 --- a/shm.h +++ b/shm.h @@ -24,7 +24,18 @@ struct buffer { unsigned age; - pixman_region32_t dirty; + /* + * First item in the array is used to track frame-to-frame + * damage. This is used when re-applying damage from the last + * frame, when the compositor doesn't release buffers immediately + * (forcing us to double buffer) + * + * The remaining items are used to track surface damage. Each + * worker thread adds its own cell damage to "its" region. When + * the frame is done, all damage is converted to a single region, + * which is then used in calls to wl_surface_damage_buffer(). + */ + pixman_region32_t *dirty; }; void shm_fini(void); From 827396237253d3c784c7455a9f3f2c26a8128c51 Mon Sep 17 00:00:00 2001 From: Raimund Sacherer Date: Sat, 7 Oct 2023 19:37:04 +0200 Subject: [PATCH 0507/1323] Enable the use of flash as visual bell With this patch we can configure flash in the bell section. The colors section allow now to configure the color and translucency of the flash. --- CHANGELOG.md | 3 +++ config.c | 21 +++++++++++++++++++++ config.h | 3 +++ doc/foot.ini.5.scd | 16 ++++++++++++++++ foot.ini | 3 +++ render.c | 6 ++++-- terminal.c | 8 ++++++++ terminal.h | 2 ++ 8 files changed, 60 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 667b1662..cc6e57bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,10 +74,13 @@ - `scrollback-down-page` (shift+page-down) - `scrollback-down-half-page` (none) - `scrollback-down-line` (none) +* Support for visual bell which flashes the terminal window. + ([#1508][1508]). [1077]: https://codeberg.org/dnkl/foot/issues/1077 [1364]: https://codeberg.org/dnkl/foot/issues/1364 [419]: https://codeberg.org/dnkl/foot/issues/419 +[1508]: https://codeberg.org/dnkl/foot/issues/1508 ### Changed diff --git a/config.c b/config.c index 4722cfdd..35c847e9 100644 --- a/config.c +++ b/config.c @@ -1054,6 +1054,8 @@ parse_section_bell(struct context *ctx) return value_to_bool(ctx, &conf->bell.urgent); else if (strcmp(key, "notify") == 0) return value_to_bool(ctx, &conf->bell.notify); + else if (strcmp(key, "visual") == 0) + return value_to_bool(ctx, &conf->bell.flash); else if (strcmp(key, "command") == 0) return value_to_spawn_template(ctx, &conf->bell.command); else if (strcmp(key, "command-focused") == 0) @@ -1237,6 +1239,7 @@ parse_section_colors(struct context *ctx) 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; @@ -1320,6 +1323,21 @@ parse_section_colors(struct context *ctx) return true; } + else if (strcmp(key, "flash-alpha") == 0) { + float alpha; + if (!value_to_float(ctx, &alpha)) + return false; + + if (alpha < 0. || alpha > 1.) { + LOG_CONTEXTUAL_ERR("not in range 0.0-1.0"); + return false; + } + + conf->colors.flash_alpha = alpha * 65535.; + return true; + } + + else { LOG_CONTEXTUAL_ERR("not valid option"); return false; @@ -2980,6 +2998,7 @@ config_load(struct config *conf, const char *conf_path, .bell = { .urgent = false, .notify = false, + .flash = false, .command = { .argv = {.args = NULL}, }, @@ -3003,6 +3022,8 @@ config_load(struct config *conf, const char *conf_path, .colors = { .fg = default_foreground, .bg = default_background, + .flash = 0x7f7f00, + .flash_alpha = 0x7fff, .alpha = 0xffff, .selection_fg = 0x80000000, /* Use default bg */ .selection_bg = 0x80000000, /* Use default fg */ diff --git a/config.h b/config.h index 4d2838c4..3c5b3df7 100644 --- a/config.h +++ b/config.h @@ -160,6 +160,7 @@ struct config { struct { bool urgent; bool notify; + bool flash; struct config_spawn_template command; bool command_focused; } bell; @@ -202,6 +203,8 @@ struct config { struct { uint32_t fg; uint32_t bg; + uint32_t flash; + uint32_t flash_alpha; uint32_t table[256]; uint16_t alpha; uint32_t selection_fg; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 5ef4dcb4..84250b40 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -394,6 +394,11 @@ Note: do not set *TERM* here; use the *term* option in the main Default: _no_ +*visual* + When set to _yes_, foot will flash terminal window. + + Default: _no_ + *command* When set, foot will execute this command when *BEL* is received. Default: none @@ -609,6 +614,17 @@ can configure the background transparency with the _alpha_ option. https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit for an explanation of the remainder. +*flash* + Color to use for the terminal window flash. + + Default: _7f7f00_. + +*flash_alpha* + Flash translucency. A value in the range 0.0-1.0, where 0.0 + means completely transparent, and 1.0 is opaque. + + Default: _0.5_. + *alpha* Background translucency. A value in the range 0.0-1.0, where 0.0 means completely transparent, and 1.0 is opaque. Default: _1.0_. diff --git a/foot.ini b/foot.ini index cdbd8259..bdfcd582 100644 --- a/foot.ini +++ b/foot.ini @@ -43,6 +43,7 @@ [bell] # urgent=no # notify=no +# visual=no # command= # command-focused=no @@ -77,6 +78,8 @@ # alpha=1.0 # background=242424 # foreground=ffffff +# flash=7f7f00 +# flash_alpha=0.5 ## Normal/regular colors (color palette 0-7) # regular0=242424 # black diff --git a/render.c b/render.c index 9296223a..9f858bf1 100644 --- a/render.c +++ b/render.c @@ -1588,7 +1588,9 @@ render_overlay(struct terminal *term) break; case OVERLAY_FLASH: - color = (pixman_color_t){.red=0x7fff, .green=0x7fff, .blue=0, .alpha=0x7fff}; + color = color_hex_to_pixman_with_alpha( + term->conf->colors.flash, + term->conf->colors.flash_alpha); break; } @@ -2193,7 +2195,7 @@ render_csd_button_maximize_maximized( { x_margin + shrink, y_margin + thick, thick, width - 2 * thick - shrink }, { x_margin + width - thick - shrink, y_margin + thick, thick, width - 2 * thick - shrink }, { x_margin + shrink, y_margin + width - thick - shrink, width - 2 * shrink, thick }}); - + pixman_image_unref(src); } diff --git a/terminal.c b/terminal.c index 91c46a11..da64740e 100644 --- a/terminal.c +++ b/terminal.c @@ -1170,6 +1170,8 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .colors = { .fg = conf->colors.fg, .bg = conf->colors.bg, + .flash = conf->colors.flash, + .flash_alpha = conf->colors.flash_alpha, .alpha = conf->colors.alpha, .selection_fg = conf->colors.selection_fg, .selection_bg = conf->colors.selection_bg, @@ -1922,7 +1924,9 @@ term_reset(struct terminal *term, bool hard) fdm_del(term->fdm, term->blink.fd); term->blink.fd = -1; term->colors.fg = term->conf->colors.fg; term->colors.bg = term->conf->colors.bg; + term->colors.flash = term->conf->colors.flash; term->colors.alpha = term->conf->colors.alpha; + term->colors.flash_alpha = term->conf->colors.flash_alpha; term->colors.selection_fg = term->conf->colors.selection_fg; term->colors.selection_bg = term->conf->colors.selection_bg; term->colors.use_custom_selection = term->conf->colors.use_custom.selection; @@ -3270,6 +3274,7 @@ term_flash(struct terminal *term, unsigned duration_ms) void term_bell(struct terminal *term) { + if (!term->bell_action_enabled) return; @@ -3288,6 +3293,9 @@ term_bell(struct terminal *term) if (term->conf->bell.notify) notify_notify(term, "Bell", "Bell in terminal"); + if (term->conf->bell.flash) + term_flash(term, 100); + if ((term->conf->bell.command.argv.args != NULL) && (!term->kbd_focus || term->conf->bell.command_focused)) { diff --git a/terminal.h b/terminal.h index da873ae6..4939b91e 100644 --- a/terminal.h +++ b/terminal.h @@ -508,6 +508,8 @@ struct terminal { struct { uint32_t fg; uint32_t bg; + uint32_t flash; + uint32_t flash_alpha; uint32_t table[256]; uint16_t alpha; uint32_t selection_fg; From 8a2a45077812efdf74a0fbcf565ed280a9e37954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 10 Oct 2023 08:07:02 +0200 Subject: [PATCH 0508/1323] doc: foot.ini: flash: tweak grammar, use consistent formatting --- doc/foot.ini.5.scd | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 84250b40..bc63bb27 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -395,9 +395,8 @@ Note: do not set *TERM* here; use the *term* option in the main Default: _no_ *visual* - When set to _yes_, foot will flash terminal window. - - Default: _no_ + When set to _yes_, foot will flash the terminal window. Default: + _no_ *command* When set, foot will execute this command when *BEL* is received. @@ -615,15 +614,11 @@ can configure the background transparency with the _alpha_ option. explanation of the remainder. *flash* - Color to use for the terminal window flash. - - Default: _7f7f00_. + Color to use for the terminal window flash. Default: _7f7f00_. *flash_alpha* - Flash translucency. A value in the range 0.0-1.0, where 0.0 - means completely transparent, and 1.0 is opaque. - - Default: _0.5_. + Flash translucency. A value in the range 0.0-1.0, where 0.0 means + completely transparent, and 1.0 is opaque. Default: _0.5_. *alpha* Background translucency. A value in the range 0.0-1.0, where 0.0 From eea995637d2e65c206734a4d8084a9b500f77f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 10 Oct 2023 08:09:26 +0200 Subject: [PATCH 0509/1323] term: remove unneeded (and mostly unused) term->flash{,_alpha} --- terminal.c | 4 ---- terminal.h | 2 -- 2 files changed, 6 deletions(-) diff --git a/terminal.c b/terminal.c index da64740e..9a76488e 100644 --- a/terminal.c +++ b/terminal.c @@ -1170,8 +1170,6 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .colors = { .fg = conf->colors.fg, .bg = conf->colors.bg, - .flash = conf->colors.flash, - .flash_alpha = conf->colors.flash_alpha, .alpha = conf->colors.alpha, .selection_fg = conf->colors.selection_fg, .selection_bg = conf->colors.selection_bg, @@ -1924,9 +1922,7 @@ term_reset(struct terminal *term, bool hard) fdm_del(term->fdm, term->blink.fd); term->blink.fd = -1; term->colors.fg = term->conf->colors.fg; term->colors.bg = term->conf->colors.bg; - term->colors.flash = term->conf->colors.flash; term->colors.alpha = term->conf->colors.alpha; - term->colors.flash_alpha = term->conf->colors.flash_alpha; term->colors.selection_fg = term->conf->colors.selection_fg; term->colors.selection_bg = term->conf->colors.selection_bg; term->colors.use_custom_selection = term->conf->colors.use_custom.selection; diff --git a/terminal.h b/terminal.h index 4939b91e..da873ae6 100644 --- a/terminal.h +++ b/terminal.h @@ -508,8 +508,6 @@ struct terminal { struct { uint32_t fg; uint32_t bg; - uint32_t flash; - uint32_t flash_alpha; uint32_t table[256]; uint16_t alpha; uint32_t selection_fg; From 9cf22df784a7d081ac9eb52b1d0225714f9cda99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 10 Oct 2023 08:11:13 +0200 Subject: [PATCH 0510/1323] foot.ini: flash_alpha -> flash-alpha --- foot.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foot.ini b/foot.ini index bdfcd582..55eb42de 100644 --- a/foot.ini +++ b/foot.ini @@ -79,7 +79,7 @@ # background=242424 # foreground=ffffff # flash=7f7f00 -# flash_alpha=0.5 +# flash-alpha=0.5 ## Normal/regular colors (color palette 0-7) # regular0=242424 # black From 0c6a3731c3ccb82eeeea92e82578d26d79b3f115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 10 Oct 2023 08:11:22 +0200 Subject: [PATCH 0511/1323] doc: foot.ini.5: flash_alpha -> flash-alpha --- doc/foot.ini.5.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index bc63bb27..e4444c6f 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -616,7 +616,7 @@ can configure the background transparency with the _alpha_ option. *flash* Color to use for the terminal window flash. Default: _7f7f00_. -*flash_alpha* +*flash-alpha* Flash translucency. A value in the range 0.0-1.0, where 0.0 means completely transparent, and 1.0 is opaque. Default: _0.5_. From ce64da2fe1836883323d618664cb0f9e090fbbb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 10 Oct 2023 08:12:04 +0200 Subject: [PATCH 0512/1323] doc: foot.ini.5: move flash{,-alpha} to the bottom of the 'colors' section --- doc/foot.ini.5.scd | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index e4444c6f..b7b79e75 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -613,13 +613,6 @@ can configure the background transparency with the _alpha_ option. https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit for an explanation of the remainder. -*flash* - Color to use for the terminal window flash. Default: _7f7f00_. - -*flash-alpha* - Flash translucency. A value in the range 0.0-1.0, where 0.0 means - completely transparent, and 1.0 is opaque. Default: _0.5_. - *alpha* Background translucency. A value in the range 0.0-1.0, where 0.0 means completely transparent, and 1.0 is opaque. Default: _1.0_. @@ -653,6 +646,13 @@ can configure the background transparency with the _alpha_ option. Color to use for the underline used to highlight URLs in URL mode. Default: _regular3_. +*flash* + Color to use for the terminal window flash. Default: _7f7f00_. + +*flash-alpha* + Flash translucency. A value in the range 0.0-1.0, where 0.0 means + completely transparent, and 1.0 is opaque. Default: _0.5_. + # SECTION: csd This section controls the look of the _CSDs_ (Client Side From af0feed3e5e5061d913b3d34ce500917f13fb71f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 10 Oct 2023 08:14:43 +0200 Subject: [PATCH 0513/1323] changelog: fix issue number for visual bell 1508 refers to the pull request, not the feature request. --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc6e57bb..4ae5f985 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,12 +75,12 @@ - `scrollback-down-half-page` (none) - `scrollback-down-line` (none) * Support for visual bell which flashes the terminal window. - ([#1508][1508]). + ([#1337][1337]). [1077]: https://codeberg.org/dnkl/foot/issues/1077 [1364]: https://codeberg.org/dnkl/foot/issues/1364 [419]: https://codeberg.org/dnkl/foot/issues/419 -[1508]: https://codeberg.org/dnkl/foot/issues/1508 +[1337]: https://codeberg.org/dnkl/foot/issues/1337 ### Changed From 4cf2c45baae517e5a1308bde5ed0c6f3d6b7e096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 10 Oct 2023 09:27:00 +0200 Subject: [PATCH 0514/1323] render: better description of why we disable transparency in fullscreen --- render.c | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/render.c b/render.c index 9f858bf1..231d2b63 100644 --- a/render.c +++ b/render.c @@ -529,22 +529,32 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag /* * Note: disable transparency when fullscreened. * - * This is because the wayland protocol recommends - * (mandates even?) the compositor render a black - * background behind fullscreened transparent windows. + * This is because the wayland protocol mandates no + * screen content is shown behind the fullscreened + * window. * - * In other words, transparency does not work when - * fullscreened, in the sense that you don't see - * what's behind the window. + * The _intent_ of the specification is that a black + * (or other static color) should be used as + * background. * - * And if we keep our alpha channel, the background - * color will just look weird. For example, if the - * background color is white, and alpha is 0.5, then - * the window will be drawn in a shade of gray while - * fullscreened. + * There's a bit of gray area however, and some + * compositors have chosen to interpret the + * specification in a way that allows wallpapers to be + * seen through a fullscreen window. * - * By disabling the alpha channel, the window will at - * least be rendered in the intended background color. + * Given that a) the intent of the specification, and + * b) we don't know what the compositor will do, we + * simply disable transparency while in fullscreen. + * + * To see why, consider what happens if we keep our + * transparency. For example, if the background color + * is white, and alpha is 0.5, then the window will be + * drawn in a shade of gray while fullscreened. + * + * See + * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/116 + * for a discussion on whether transparent, fullscreen + * windows should be allowed in some way or not. * * NOTE: if changing this, also update render_margin() */ From 41932287cf046a263ecaca0d8d140d4f57e180f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 10 Oct 2023 10:52:35 +0200 Subject: [PATCH 0515/1323] Revert "font baseline: use max(font->height, font->ascent + font->descent) when calculating font height" This reverts commit fd813d0e6cfff57cf8a8f55e9a249ddb6fbdb6d6. The intent of the reverted commit was to align font height calculation with cell height calculation. However, it turns out this breaks some fonts. Typically those with large:ish differences in their 'height' attribute, and their ascent+descent value. Closes #1511 --- terminal.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal.c b/terminal.c index 9a76488e..dd92cd67 100644 --- a/terminal.c +++ b/terminal.c @@ -2189,7 +2189,7 @@ term_font_baseline(const struct terminal *term) { const struct fcft_font *font = term->fonts[0]; const int line_height = term->cell_height; - const int font_height = max(font->height, font->ascent + font->descent); + const int font_height = font->ascent + font->descent; const int glyph_top_y = round((line_height - font_height) / 2.); return term->font_y_ofs + glyph_top_y + font->ascent; From 7d126ff41401ff51bc80227eab319e61d64f13e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 10 Oct 2023 10:55:26 +0200 Subject: [PATCH 0516/1323] changelog: fixed font baseline calculation --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ae5f985..0e0cc8c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -134,6 +134,7 @@ mode ([#1503][1503]). * Crash when a scrollback search match is in the last column. * Scrollback search: grapheme clusters not matching correctly. +* Wrong baseline offset for some fonts ([#1511][1511]). [1436]: https://codeberg.org/dnkl/foot/issues/1436 [1464]: https://codeberg.org/dnkl/foot/issues/1464 @@ -141,6 +142,7 @@ [1493]: https://codeberg.org/dnkl/foot/pulls/1493 [1498]: https://codeberg.org/dnkl/foot/issues/1498 [1503]: https://codeberg.org/dnkl/foot/issues/1503 +[1511]: https://codeberg.org/dnkl/foot/issues/1511 ### Security From 34aa979f460c09a481a02af733737a5925c9b1bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 10 Oct 2023 13:52:24 +0200 Subject: [PATCH 0517/1323] term_font_baseline(): only center glyph when a custom line-height is being used When using the font's own line-height, simply set the baseline 'descent' pixels above the bottom of the cell. This fixes an issue where some fonts appeared "glued" to the top of the cell, and sometimes getting partially clipped. --- terminal.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/terminal.c b/terminal.c index dd92cd67..e517253b 100644 --- a/terminal.c +++ b/terminal.c @@ -2190,9 +2190,17 @@ term_font_baseline(const struct terminal *term) const struct fcft_font *font = term->fonts[0]; const int line_height = term->cell_height; const int font_height = font->ascent + font->descent; - const int glyph_top_y = round((line_height - font_height) / 2.); - return term->font_y_ofs + glyph_top_y + font->ascent; + /* + * Center glyph on the line *if* using a custom line height, + * otherwise the baseline is simply 'descent' pixels above the + * bottom of the cell + */ + const int glyph_top_y = term->font_line_height.px >= 0 + ? round((line_height - font_height) / 2.) + : 0; + + return term->font_y_ofs + line_height - glyph_top_y - font->descent; } void From 4449177517c2d96857483acbc8afd7438fcdfadd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 10 Oct 2023 14:23:33 +0200 Subject: [PATCH 0518/1323] term: cache font baseline No need to redo the calculation for every single cell we render, every frame... --- box-drawing.c | 2 +- render.c | 16 ++++++++-------- terminal.c | 2 ++ terminal.h | 1 + 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/box-drawing.c b/box-drawing.c index 07f415cb..cf351b31 100644 --- a/box-drawing.c +++ b/box-drawing.c @@ -3011,7 +3011,7 @@ box_drawing(const struct terminal *term, char32_t wc) .cols = 1, .pix = buf.pix, .x = -term->font_x_ofs, - .y = term_font_baseline(term), + .y = term->font_baseline, .width = width, .height = height, .advance = { diff --git a/render.c b/render.c index 231d2b63..81e9a0a4 100644 --- a/render.c +++ b/render.c @@ -326,7 +326,7 @@ draw_beam_cursor(const struct terminal *term, pixman_image_t *pix, const struct fcft_font *font, const pixman_color_t *color, int x, int y) { - int baseline = y + term_font_baseline(term) - term->fonts[0]->ascent; + int baseline = y + term->font_baseline - term->fonts[0]->ascent; pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, color, 1, &(pixman_rectangle16_t){ @@ -338,7 +338,7 @@ draw_beam_cursor(const struct terminal *term, pixman_image_t *pix, static int underline_offset(const struct terminal *term, const struct fcft_font *font) { - return term_font_baseline(term) - + return term->font_baseline - (term->conf->use_custom_underline_offset ? -term_pt_or_px_as_pixels(term, &term->conf->underline_offset) : font->underline.position); @@ -392,7 +392,7 @@ draw_strikeout(const struct terminal *term, pixman_image_t *pix, pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, color, 1, &(pixman_rectangle16_t){ - x, y + term_font_baseline(term) - font->strikeout.position, + x, y + term->font_baseline - font->strikeout.position, cols * term->cell_width, font->strikeout.thickness}); } @@ -776,13 +776,13 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag if (!(cell->attrs.blink && term->blink.state == BLINK_OFF)) { pixman_image_composite32( PIXMAN_OP_OVER, glyph->pix, NULL, pix, 0, 0, 0, 0, - pen_x + letter_x_ofs + g_x, y + term_font_baseline(term) - g_y, + pen_x + letter_x_ofs + g_x, y + term->font_baseline - g_y, glyph->width, glyph->height); } } else { pixman_image_composite32( PIXMAN_OP_OVER, clr_pix, glyph->pix, pix, 0, 0, 0, 0, - pen_x + letter_x_ofs + g_x, y + term_font_baseline(term) - g_y, + pen_x + letter_x_ofs + g_x, y + term->font_baseline - g_y, glyph->width, glyph->height); /* Combining characters */ @@ -822,7 +822,7 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag /* Some fonts use a negative offset, while others use a * "normal" offset */ pen_x + x_ofs + g->x, - y + term_font_baseline(term) - g->y, + y + term->font_baseline - g->y, g->width, g->height); } } @@ -3411,7 +3411,7 @@ render_search_box(struct terminal *term) /* Glyph surface is a pre-rendered image (typically a color emoji...) */ pixman_image_composite32( PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0, - x + x_ofs + glyph->x, y + term_font_baseline(term) - glyph->y, + x + x_ofs + glyph->x, y + term->font_baseline - glyph->y, glyph->width, glyph->height); } else { int combining_ofs = width == 0 @@ -3423,7 +3423,7 @@ render_search_box(struct terminal *term) pixman_image_composite32( PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, 0, 0, 0, x + x_ofs + combining_ofs + glyph->x, - y + term_font_baseline(term) - glyph->y, + y + term->font_baseline - glyph->y, glyph->width, glyph->height); pixman_image_unref(src); } diff --git a/terminal.c b/terminal.c index e517253b..efbbded1 100644 --- a/terminal.c +++ b/terminal.c @@ -774,6 +774,8 @@ term_set_fonts(struct terminal *term, struct fcft_font *fonts[static 4], term->font_x_ofs = term_pt_or_px_as_pixels(term, &conf->horizontal_letter_offset); term->font_y_ofs = term_pt_or_px_as_pixels(term, &conf->vertical_letter_offset); + term->font_baseline = term_font_baseline(term); + LOG_INFO("cell width=%d, height=%d", term->cell_width, term->cell_height); sixel_cell_size_changed(term); diff --git a/terminal.h b/terminal.h index da873ae6..a8b65198 100644 --- a/terminal.h +++ b/terminal.h @@ -406,6 +406,7 @@ struct terminal { bool font_is_sized_by_dpi; int16_t font_x_ofs; int16_t font_y_ofs; + int16_t font_baseline; enum fcft_subpixel font_subpixel; struct { From 1cafadea6cf5ad9b9d48933cc4fb5ddd02d95c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 11 Oct 2023 18:12:18 +0200 Subject: [PATCH 0519/1323] changelog: emphasize the new key bindings are for search mode --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e0cc8c1..4e621ddc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,7 +60,7 @@ row ([#1364][1364]). * Support for DECSET/DECRST/DECRQM 2027 (_Grapheme cluster processing_). -* New search mode key bindings (along with their defaults) +* New **search mode** key bindings (along with their defaults) ([#419][419]): - `extend-char` (shift+right) - `extend-line-down` (shift+down) From 7131c96b26d9c4cbcba6c9d20bc6e45b68ed303e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 11 Oct 2023 18:14:40 +0200 Subject: [PATCH 0520/1323] changelog: prepare for 1.16.0 --- CHANGELOG.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e621ddc..fa87ff4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.16.0](#1-16-0) * [1.15.3](#1-15-3) * [1.15.2](#1-15-2) * [1.15.1](#1-15-1) @@ -46,7 +46,8 @@ * [1.2.0](#1-2-0) -## Unreleased +## 1.16.0 + ### Added * Support for building with _wayland-protocols_ as a subproject. @@ -111,7 +112,6 @@ [1495]: https://codeberg.org/dnkl/foot/pulls/1495 -### Deprecated ### Removed * `utempter` config option (was deprecated in 1.15.0). @@ -144,10 +144,16 @@ [1503]: https://codeberg.org/dnkl/foot/issues/1503 [1511]: https://codeberg.org/dnkl/foot/issues/1511 - -### Security ### Contributors +* 6t8k +* Alyssa Ross +* CismonX +* Max Gautier +* raggedmyth +* Raimund Sacherer +* Sertonix + ## 1.15.3 From a9d6eaf9376012d0413d20f8a17003cbd82972ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 11 Oct 2023 18:15:01 +0200 Subject: [PATCH 0521/1323] meson: bump version to 1.16.0 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 8f32225e..dd06be2e 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.15.3', + version: '1.16.0', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From 4847cc3bd1c32a7d188b3c2e226435842c51d1fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 11 Oct 2023 18:19:31 +0200 Subject: [PATCH 0522/1323] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa87ff4b..cd46e444 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.16.0](#1-16-0) * [1.15.3](#1-15-3) * [1.15.2](#1-15-2) @@ -46,6 +47,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.16.0 ### Added From 7d7b48f10448cedc4806f77e58b8c89daecee614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 11 Oct 2023 18:39:43 +0200 Subject: [PATCH 0523/1323] changelog: fix link to issue 1077 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd46e444..4a520e29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,7 +63,7 @@ * Support for building with _wayland-protocols_ as a subproject. * Mouse wheel scrolls can now be used in `mouse-bindings` - ([#1077](1077)). + ([#1077][1077]). * New mouse bindings: `scrollback-up-mouse` and `scrollback-down-mouse`, bound to `BTN_BACK` and `BTN_FORWARD` respectively. From c006ac3a079d00e30f9d1cd7986d8112724629b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 12 Oct 2023 16:16:11 +0200 Subject: [PATCH 0524/1323] shm: memfd_create: fallback to not using MFD_NOEXEC_SEAL MFD_NOEXEC_SEAL was introduced in linux 6.3. Kernels before that will *reject* memfd_create() calls that set it. This caused foot to exit (i.e. not start at all), when compiled on linux >= 6.3, but run on linux < 6.3. We _do_ want to use MFD_NOEXEC_SEAL, since a) our memory mapped really shouldn't be executable, and b) to silence a warning on linux >= 6.3. To handle all cases, first try *with* MFD_NOEXEC_SEAL. If that fails with EINVAL, retry *without* it. Closes #1514 --- CHANGELOG.md | 5 +++++ shm.c | 21 ++++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a520e29..39c22d87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,11 @@ ### Deprecated ### Removed ### Fixed +* Foot not starting on linux kernels before 6.3 ([#1514][1514]). + +[1514]: https://codeberg.org/dnkl/foot/issues/1514 + + ### Security ### Contributors diff --git a/shm.c b/shm.c index 8ca0ead0..171459f6 100644 --- a/shm.c +++ b/shm.c @@ -27,10 +27,8 @@ #define MAP_UNINITIALIZED 0 #endif -#if defined(MFD_NOEXEC_SEAL) - #define FOOT_MFD_FLAGS (MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_NOEXEC_SEAL) -#else - #define FOOT_MFD_FLAGS (MFD_CLOEXEC | MFD_ALLOW_SEALING) +#if !defined(MFD_NOEXEC_SEAL) + #define MFD_NOEXEC_SEAL 0 #endif #define TIME_SCROLL 0 @@ -339,7 +337,20 @@ get_new_buffers(struct buffer_chain *chain, size_t count, /* Backing memory for SHM */ #if defined(MEMFD_CREATE) - pool_fd = memfd_create("foot-wayland-shm-buffer-pool", FOOT_MFD_FLAGS); + /* + * Older kernels reject MFD_NOEXEC_SEAL with EINVAL. Try first + * *with* it, and if that fails, try again *without* it. + */ + errno = 0; + pool_fd = memfd_create( + "foot-wayland-shm-buffer-pool", + MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_NOEXEC_SEAL); + + if (pool_fd < 0 && errno == EINVAL) { + pool_fd = memfd_create( + "foot-wayland-shm-buffer-pool", MFD_CLOEXEC | MFD_ALLOW_SEALING); + } + #elif defined(__FreeBSD__) // memfd_create on FreeBSD 13 is SHM_ANON without sealing support pool_fd = shm_open(SHM_ANON, O_RDWR | O_CLOEXEC, 0600); From 4aa67e464a04bf64d30aff70019ce0d8b4d5e136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 12 Oct 2023 16:22:50 +0200 Subject: [PATCH 0525/1323] sixel: erase: fix clearing of cell->attrs.clean When erasing a sixel, the cells underneath it must be marked as 'dirty', in order to be re-rendered. This was not being done correctly; the for loop loops *from* the start col, meaning the *end* col is *not* sixel->pos.col, as that's the *number* of columns, not the *end* column. --- sixel.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sixel.c b/sixel.c index bd2ebe1d..8a0b130f 100644 --- a/sixel.c +++ b/sixel.c @@ -180,7 +180,7 @@ sixel_erase(struct terminal *term, struct sixel *sixel) row->dirty = true; - for (int c = sixel->pos.col; c < min(sixel->cols, term->cols); c++) + for (int c = sixel->pos.col; c < min(sixel->pos.col + sixel->cols, term->cols); c++) row->cells[c].attrs.clean = 0; } From f5f2f5a954ee6cf3532a6aba023860ada183d486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 12 Oct 2023 16:24:15 +0200 Subject: [PATCH 0526/1323] render: fix surface damage when rendering sixels. Pass a damage region to render_row()/render_cell() when rendering partially visible cells underneath a sixel. This ensures the affected regions are later reported as 'damaged' to the Wayland compositor. Closes #1515 --- CHANGELOG.md | 3 +++ render.c | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39c22d87..f276e493 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,9 +53,12 @@ ### Deprecated ### Removed ### Fixed + * Foot not starting on linux kernels before 6.3 ([#1514][1514]). +* Cells underneath erased sixels not being repainted ([#1515][1515]). [1514]: https://codeberg.org/dnkl/foot/issues/1514 +[1515]: https://codeberg.org/dnkl/foot/issues/1515 ### Security diff --git a/render.c b/render.c index 81e9a0a4..11d323d5 100644 --- a/render.c +++ b/render.c @@ -1199,7 +1199,8 @@ render_sixel_chunk(struct terminal *term, pixman_image_t *pix, const struct sixe static void render_sixel(struct terminal *term, pixman_image_t *pix, - const struct coord *cursor, const struct sixel *sixel) + pixman_region32_t *damage, const struct coord *cursor, + const struct sixel *sixel) { xassert(sixel->pix != NULL); xassert(sixel->width >= 0); @@ -1293,8 +1294,7 @@ render_sixel(struct terminal *term, pixman_image_t *pix, */ if (!sixel->opaque) { /* TODO: multithreading */ - int cursor_col = cursor->row == term_row_no ? cursor->col : -1; - render_row(term, pix, NULL, row, term_row_no, cursor_col); + render_row(term, pix, damage, row, term_row_no, cursor_col); } else { for (int col = sixel->pos.col; col < min(sixel->pos.col + sixel->cols, term->cols); @@ -1309,7 +1309,7 @@ render_sixel(struct terminal *term, pixman_image_t *pix, if ((last_row_needs_erase && last_row) || (last_col_needs_erase && last_col)) { - render_cell(term, pix, NULL, row, term_row_no, col, cursor_col == col); + render_cell(term, pix, damage, row, term_row_no, col, cursor_col); } else { cell->attrs.clean = 1; cell->attrs.confined = 1; @@ -1333,6 +1333,7 @@ render_sixel(struct terminal *term, pixman_image_t *pix, static void render_sixel_images(struct terminal *term, pixman_image_t *pix, + pixman_region32_t *damage, const struct coord *cursor) { if (likely(tll_length(term->grid->sixel_images)) == 0) @@ -1370,7 +1371,7 @@ render_sixel_images(struct terminal *term, pixman_image_t *pix, } sixel_sync_cache(term, &it->item); - render_sixel(term, pix, cursor, &it->item); + render_sixel(term, pix, damage, cursor, &it->item); } } @@ -2974,7 +2975,10 @@ grid_render(struct terminal *term) } } - render_sixel_images(term, buf->pix[0], &cursor); + pixman_region32_t damage; + pixman_region32_init(&damage); + + render_sixel_images(term, buf->pix[0], &damage, &cursor); if (term->render.workers.count > 0) { mtx_lock(&term->render.workers.lock); @@ -2985,9 +2989,6 @@ grid_render(struct terminal *term) xassert(tll_length(term->render.workers.queue) == 0); } - pixman_region32_t damage; - pixman_region32_init(&damage); - for (int r = 0; r < term->rows; r++) { struct row *row = grid_row_in_view(term->grid, r); From c26c6e285a8ac191c58254d254ebbc8f72020f3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 12 Oct 2023 16:36:07 +0200 Subject: [PATCH 0527/1323] changelog: prepare for 1.16.1 --- CHANGELOG.md | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f276e493..1d8d50e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.16.1](#1-16-1) * [1.16.0](#1-16-0) * [1.15.3](#1-15-3) * [1.15.2](#1-15-2) @@ -47,11 +47,8 @@ * [1.2.0](#1-2-0) -## Unreleased -### Added -### Changed -### Deprecated -### Removed +## 1.16.1 + ### Fixed * Foot not starting on linux kernels before 6.3 ([#1514][1514]). @@ -61,10 +58,6 @@ [1515]: https://codeberg.org/dnkl/foot/issues/1515 -### Security -### Contributors - - ## 1.16.0 ### Added From 195eb3356a4741c4835384943f9bc64feb2c64e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 12 Oct 2023 16:36:18 +0200 Subject: [PATCH 0528/1323] meson: bump version to 1.16.1 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index dd06be2e..7e11557b 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.16.0', + version: '1.16.1', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From 857ac224c5e461ce09fe11eb58e97f45b6300561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 12 Oct 2023 20:36:28 +0200 Subject: [PATCH 0529/1323] changelog: add new 'unreleased' section --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d8d50e2..a2811945 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.16.1](#1-16-1) * [1.16.0](#1-16-0) * [1.15.3](#1-15-3) @@ -46,6 +47,15 @@ * [1.2.1](#1-2-1) * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + ## 1.16.1 From 3dbb86914cd05143bb0786af5c3756abf5bca73c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 13 Oct 2023 18:44:44 +0200 Subject: [PATCH 0530/1323] render: sixel: regression: wrong cell color behind opaque sixels An opaque sixel that isn't a multiple of the cell size will have some cells partially visible (either the entire last row, the entire last column, or both). These must be rendered before blitting the sixel. f5f2f5a954ee6cf3532a6aba023860ada183d486 introduced a regression, where all such cells were rendered as if the cursor was there, giving them the wrong appearance. Closes #1520 --- CHANGELOG.md | 7 +++++++ render.c | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2811945..5e2ec02e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,13 @@ ### Deprecated ### Removed ### Fixed + +* Last row and/or column of opaque sixels (not having a size that is a + multiple of the cell size) being the wrong color ([#1520][1520]). + +[1520]: https://codeberg.org/dnkl/foot/issues/1520 + + ### Security ### Contributors diff --git a/render.c b/render.c index 11d323d5..d679b5e8 100644 --- a/render.c +++ b/render.c @@ -1309,7 +1309,7 @@ render_sixel(struct terminal *term, pixman_image_t *pix, if ((last_row_needs_erase && last_row) || (last_col_needs_erase && last_col)) { - render_cell(term, pix, damage, row, term_row_no, col, cursor_col); + render_cell(term, pix, damage, row, term_row_no, col, cursor_col == col); } else { cell->attrs.clean = 1; cell->attrs.confined = 1; From 47bc28ce559b0bdcdbc6228fdb64d17d9bb3c2f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 17 Oct 2023 17:24:00 +0200 Subject: [PATCH 0531/1323] changelog: prepare for 1.16.2 --- CHANGELOG.md | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e2ec02e..8ca3746b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.16.2](#1-16-2) * [1.16.1](#1-16-1) * [1.16.0](#1-16-0) * [1.15.3](#1-15-3) @@ -47,11 +47,8 @@ * [1.2.1](#1-2-1) * [1.2.0](#1-2-0) -## Unreleased -### Added -### Changed -### Deprecated -### Removed +## 1.16.2 + ### Fixed * Last row and/or column of opaque sixels (not having a size that is a @@ -60,10 +57,6 @@ [1520]: https://codeberg.org/dnkl/foot/issues/1520 -### Security -### Contributors - - ## 1.16.1 ### Fixed From 8b3dbf09728b5c5478ab5f9593abd75c4c442d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 17 Oct 2023 17:24:12 +0200 Subject: [PATCH 0532/1323] meson: bump version to 1.16.2 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 7e11557b..719352bc 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.16.1', + version: '1.16.2', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From 642f9910c26610e5030c18f88a76e97f7d0945b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 17 Oct 2023 17:25:47 +0200 Subject: [PATCH 0533/1323] changelog: add new 'unreleased' section --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ca3746b..4b68a804 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.16.2](#1-16-2) * [1.16.1](#1-16-1) * [1.16.0](#1-16-0) @@ -47,6 +48,17 @@ * [1.2.1](#1-2-1) * [1.2.0](#1-2-0) + +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.16.2 ### Fixed From 85a4e4ccc15089b58ad8ce4aaec3b301e9025369 Mon Sep 17 00:00:00 2001 From: Fazzi Date: Fri, 20 Oct 2023 10:06:05 +0100 Subject: [PATCH 0534/1323] add CCACHE_DISABLE=1 to pgo.sh to avoid errors when ccache is enabled --- pgo/pgo.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/pgo/pgo.sh b/pgo/pgo.sh index 2f409268..f782a9f8 100755 --- a/pgo/pgo.sh +++ b/pgo/pgo.sh @@ -82,6 +82,7 @@ set -x # echo "CFLAGS: ${CFLAGS}" export CFLAGS +export CCACHE_DISABLE=1 meson setup --buildtype=release -Db_lto=true "${@}" "${blddir}" "${srcdir}" if [ ${do_pgo} = yes ]; then From 02fff24b4fa27c500a964148d54f793ddc1df1cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 26 Oct 2023 16:22:41 +0200 Subject: [PATCH 0535/1323] config: improve validation of color values, default alpha to 0xff Reject color values that aren't in either RGB, or ARGB format. That is, color values that aren't hexadecimal numbers with either 6 or 8 digits. Also, if a color value is allowed to have an alpha component, and the user left it out, default to 0xff (opaque) rather than 0x00 (fully transparent). Closes #1526 --- CHANGELOG.md | 12 ++++++++++++ config.c | 24 ++++++++++++++++++------ tests/test-config.c | 4 ++++ 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b68a804..0696b580 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,9 +52,21 @@ ## Unreleased ### Added ### Changed + +* config: ARGB color values now default to opaque, rather than + transparent, when the alpha component has been left out + ([#1526][1526]). + +[1526]: https://codeberg.org/dnkl/foot/issues/1526 + + ### Deprecated ### Removed ### Fixed + +* config: improved validation of color values. + + ### Security ### Contributors diff --git a/config.c b/config.c index 35c847e9..98d2918b 100644 --- a/config.c +++ b/config.c @@ -618,18 +618,30 @@ value_to_enum(struct context *ctx, const char **value_map, int *res) } 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; } diff --git a/tests/test-config.c b/tests/test-config.c index 666f4689..95abf1a4 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -426,6 +426,10 @@ test_color(struct context *ctx, bool (*parse_fun)(struct context *ctx), {"ffffff", 0xffffff}, {"ffffffff", 0xffffffff, !alpha_allowed}, {"aabbccdd", 0xaabbccdd, !alpha_allowed}, + {"00", 0, true}, + {"0000", 0, true}, + {"00000", 0, true}, + {"000000000", 0, true}, {"unittest-invalid-color", 0, true}, }; From 7ffbb3cc27ab6de7742a448c3726910a164bfa83 Mon Sep 17 00:00:00 2001 From: xnuk Date: Fri, 13 Oct 2023 18:51:10 +0000 Subject: [PATCH 0536/1323] man foo.ini.5: Add 'Comma separated list of fonts' example --- doc/foot.ini.5.scd | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index b7b79e75..e276d186 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -58,6 +58,7 @@ empty string to be set, but it must be quoted: *KEY=""*) - 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 From ee02e7b07d3814e7e20def899599683f19a2ec9d Mon Sep 17 00:00:00 2001 From: Jan Palus Date: Thu, 26 Oct 2023 22:20:38 +0200 Subject: [PATCH 0537/1323] main: correct short option case in help output for disabling syslog --- main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.c b/main.c index 4fc8f439..f7cf8354 100644 --- a/main.c +++ b/main.c @@ -86,7 +86,7 @@ print_usage(const char *prog_name) " -p,--print-pid=FILE|FD print PID to file or FD (only applicable in server mode)\n" " -d,--log-level={info|warning|error|none} log level (warning)\n" " -l,--log-colorize=[{never|always|auto}] enable/disable colorization of log output on stderr\n" - " -s,--log-no-syslog disable syslog logging (only applicable in server mode)\n" + " -S,--log-no-syslog disable syslog logging (only applicable in server mode)\n" " -v,--version show the version number and quit\n" " -e ignored (for compatibility with xterm -e)\n"; From 8e1b51be102dbbfc2f4965e60431eeb60712f069 Mon Sep 17 00:00:00 2001 From: Jan Palus Date: Thu, 26 Oct 2023 23:15:36 +0200 Subject: [PATCH 0538/1323] config: reset conf file descriptor after closing file stream conf file descriptor is closed once again during cleanup at the end of config_load() if descriptor >= 0. avoid double closing by assigning negative value to fd after first close. by the time second close happens some other descriptor might be opened reusing previous number ie it might happen during foot server startup when syslog message is logged between one close and the other. in this particular situation, as of this writing, it considers fd=3 for which following events apply: 1. conf file is opened and fclosed() 2. warning is logged with syslog which leads to opening socket to /dev/log which is kept open by glibc (gets fd=3) 3. second close during config_load() closes /dev/log socket descriptor 4. epoll_create() in fdm.c reuses fd=3 again 5. another message is being logged with syslog. glibc notices sendto() failure on saved /dev/log descriptor hence it closes it and opens new one Due to epoll descriptor closure foot starts a chain of errors that lead to startup failure. Fixes #1531 --- config.c | 1 + 1 file changed, 1 insertion(+) diff --git a/config.c b/config.c index 98d2918b..e5cd6723 100644 --- a/config.c +++ b/config.c @@ -3195,6 +3195,7 @@ config_load(struct config *conf, const char *conf_path, ret = !errors_are_fatal; fclose(f); + conf_file.fd = -1; } } From ca46edfe6f72816c727e3a37476126904807d0bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 27 Oct 2023 16:23:57 +0200 Subject: [PATCH 0539/1323] changelog: double close config file descriptor --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0696b580..e561921c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,10 @@ ### 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]). + +[1531]: https://codeberg.org/dnkl/foot/issues/1531 ### Security From 8b2b65bbbc82c8d63cfa70bc055723ad9708027f Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Mon, 13 Nov 2023 19:10:15 -0600 Subject: [PATCH 0540/1323] terminfo: add terminator to conditional in Sync I'm not sure if this is _strictly_ necessary, but according to the terminfo specification [1] a conditional string should be terminated with `%;`. [1]: https://man7.org/linux/man-pages/man5/terminfo.5.html --- foot.info | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foot.info b/foot.info index 4f95bf7b..89601996 100644 --- a/foot.info +++ b/foot.info @@ -40,7 +40,7 @@ RV=\E[>c, 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%;, XR=\E[>0q, From 242767d373d375a209e75b5a812116d7e1c9bfe0 Mon Sep 17 00:00:00 2001 From: eugenrh Date: Wed, 8 Nov 2023 17:43:44 +0000 Subject: [PATCH 0541/1323] theme: electrophoretic A theme for grayscale electrophoretic displays (e-ink), which aims to maximize the contrast between the text and the white background. --- themes/electrophoretic | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 themes/electrophoretic diff --git a/themes/electrophoretic b/themes/electrophoretic new file mode 100644 index 00000000..d2b67434 --- /dev/null +++ b/themes/electrophoretic @@ -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 From 3b3477d65786a743a2c2336dffb5935f3cb66334 Mon Sep 17 00:00:00 2001 From: Craig Barnes Date: Wed, 6 Dec 2023 16:23:18 +0000 Subject: [PATCH 0542/1323] appstream: update releases list --- org.codeberg.dnkl.foot.metainfo.xml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/org.codeberg.dnkl.foot.metainfo.xml b/org.codeberg.dnkl.foot.metainfo.xml index 1c0b7985..dcb71afd 100644 --- a/org.codeberg.dnkl.foot.metainfo.xml +++ b/org.codeberg.dnkl.foot.metainfo.xml @@ -33,10 +33,16 @@ - - - - + + + + + + + + + + org.codeberg.dnkl.foot.desktop https://codeberg.org/dnkl/foot From 05e6fd969a22b16eeb807481c079228230fb8427 Mon Sep 17 00:00:00 2001 From: Sivecano Date: Fri, 29 Dec 2023 13:37:07 +0100 Subject: [PATCH 0543/1323] support numpad in unicode mode --- CHANGELOG.md | 3 +++ unicode-mode.c | 2 ++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e561921c..2762d4d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,9 @@ ## Unreleased ### Added + +* Unicode input mode now accepts input from the numpad as well, numlock is ignored. + ### Changed * config: ARGB color values now default to opaque, rather than diff --git a/unicode-mode.c b/unicode-mode.c index a69601ec..6290de61 100644 --- a/unicode-mode.c +++ b/unicode-mode.c @@ -90,6 +90,8 @@ unicode_mode_input(struct seat *seat, struct terminal *term, /* 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) From e5f5a74e811e76779ab97091c334d929d9efcfe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 3 Jan 2024 13:31:35 +0100 Subject: [PATCH 0544/1323] changelog: formatting --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2762d4d8..a8f6e059 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,7 +52,9 @@ ## Unreleased ### Added -* Unicode input mode now accepts input from the numpad as well, numlock is ignored. +* Unicode input mode now accepts input from the numpad as well, + numlock is ignored. + ### Changed From 14472cdbd953d5f6891f54098e5d706e45bb0c68 Mon Sep 17 00:00:00 2001 From: LmbMaxim Date: Tue, 12 Dec 2023 12:58:44 +0300 Subject: [PATCH 0545/1323] Add poimandres theme --- themes/poimandres | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 themes/poimandres diff --git a/themes/poimandres b/themes/poimandres new file mode 100644 index 00000000..d8a6b0a7 --- /dev/null +++ b/themes/poimandres @@ -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 From 66f25bb4346eab9b7914213343d22ee67c57724f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 3 Jan 2024 14:07:15 +0100 Subject: [PATCH 0546/1323] slave: chdir to / after spawning the client application With this patch, the terminal process now changes PWD to / after spawning the client application. This ensures the terminal process itself does not "lock" a directory. For example, we may keep a mount point from being unmounted. Closes #1528 --- CHANGELOG.md | 5 +++++ slave.c | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8f6e059..f25814b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,8 +61,13 @@ * 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]). [1526]: https://codeberg.org/dnkl/foot/issues/1526 +[1528]: https://codeberg.org/dnkl/foot/issues/1528 ### Deprecated diff --git a/slave.c b/slave.c index ecfce7e6..1184d0c2 100644 --- a/slave.c +++ b/slave.c @@ -401,6 +401,14 @@ slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, 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. + */ + chdir("/"); + close(fork_pipe[1]); /* Close write end */ LOG_DBG("slave has PID %d", pid); From 0ca46338986becfbbc24160083a9620e8f29a7ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 5 Jan 2024 08:12:32 +0100 Subject: [PATCH 0547/1323] slave: ignore return value of chdir() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It's not critical. Fixes ../slave.c: In function ‘slave_spawn’: ../slave.c:410:9: error: ignoring return value of ‘chdir’ declared with attribute ‘warn_unused_result’ [-Werror=unused-result] 410 | chdir("/"); | ^~~~~~~~~~ --- slave.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slave.c b/slave.c index 1184d0c2..0cf64fa0 100644 --- a/slave.c +++ b/slave.c @@ -407,7 +407,7 @@ slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, * example, it may be a mount point of, say, a thumb drive. Us * keeping it open will prevent the user from unmounting it. */ - chdir("/"); + (void)!!chdir("/"); close(fork_pipe[1]); /* Close write end */ LOG_DBG("slave has PID %d", pid); From a2283c822971cc22db01a6702408aa69e0510448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 9 Jan 2024 16:47:41 +0100 Subject: [PATCH 0548/1323] wayland: surface_scale_explicit_width_height(): dont assert width/height are valid for scale, take 2 764248bb0d846f65c20931024c1f4adca57aae29 modified wayl_surface_scale_explicit_width_height() to not assert the surface size is valid for the given scaling factor. This, since that function is only used when scaling a mouse pointer surface. However, that commit only updated the code path run when fractional scaling is available (i.e. when the compositor implements the fractional-scale-v1 protocol). The legacy code path, that does integer scaling, was still asserting the surface width/height were divisible with the scaling factor. For the same reasons this isn't true with fractional scaling available, it's not true with integer scaling. Fix by skipping the assertions. This patch also converts the assertions to more verbose BUG() calls, that prints more information on the numbers involved. Closes #1573 --- CHANGELOG.md | 4 ++++ wayland.c | 15 ++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f25814b8..6c4f998d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,8 +77,12 @@ * 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]). [1531]: https://codeberg.org/dnkl/foot/issues/1531 +[1573]: https://codeberg.org/dnkl/foot/issues/1573 ### Security diff --git a/wayland.c b/wayland.c index a87bf45d..34050d18 100644 --- a/wayland.c +++ b/wayland.c @@ -2024,10 +2024,19 @@ surface_scale_explicit_width_height( "(width=%d, height=%d)", scale, 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); } From 208008d717567f1a3a096d5775356a67d8dd6517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 10 Jan 2024 16:41:03 +0100 Subject: [PATCH 0549/1323] config: fix cloning of env_vars tllist When cloning a config struct, the env_vars tllist wasn't correctly copied. We did correctly iterate and duplicate all old entries, but we did *not* reset the list in the cloned struct before doing so. This meant the list contained entries shared with the original list, causing double free:s in --server mode. --- CHANGELOG.md | 2 ++ config.c | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c4f998d..d7f41bb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,8 @@ * 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]`. [1531]: https://codeberg.org/dnkl/foot/issues/1531 [1573]: https://codeberg.org/dnkl/foot/issues/1573 diff --git a/config.c b/config.c index e5cd6723..de15add3 100644 --- a/config.c +++ b/config.c @@ -3393,6 +3393,8 @@ config_clone(const struct config *old) 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; tll_foreach(old->env_vars, it) { struct env_var copy = { .name = xstrdup(it->item.name), From 9da7152f834384a354710fcc8f99a6f83e5dfc12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 9 Jan 2024 16:50:47 +0100 Subject: [PATCH 0550/1323] slave: don't skip setting environment variables when using a custom environment When launching footclient with -E,--client-environment the environment variables that should be set by foot, wasn't. Those variables are: * TERM * COLORTERM * PWD * SHELL and all variables defined by the user in the [environment] section in foot.ini. In the same way, we did not *unset* TERM_PROGRAM and TERM_PROGRAM_VERSION. This patch fixes it by "cloning" the custom environment, making it mutable, and then adding/removing the variables above from it. Instead of calling setenv()/unsetenv() directly, we add the wrapper functions add_to_env() and del_from_env(). When *not* using a custom environment, they simply call setenv()/unsetenv(). When we *are* using a custom environment, add_to_env() first loops all existing variables, looking for a match. If a match is found, it's updated with the new value. If it's not found, a new entry is added. del_from_env() loops all entries, and removes it when a match is found. If no match is found, nothing is done. The mutable environment is allocated on the heap, but never free:d. We don't need to free it, since it's only allocated after forking, in the child process. Closes #1568 --- CHANGELOG.md | 3 ++ doc/foot.1.scd | 21 +++++++-- doc/footclient.1.scd | 23 ++++++++++ server.c | 3 +- slave.c | 103 ++++++++++++++++++++++++++++++++++++++----- slave.h | 2 +- terminal.c | 2 +- terminal.h | 2 +- 8 files changed, 141 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7f41bb5..01309223 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,9 +82,12 @@ `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]). [1531]: https://codeberg.org/dnkl/foot/issues/1531 [1573]: https://codeberg.org/dnkl/foot/issues/1573 +[1568]: https://codeberg.org/dnkl/foot/issues/1568 ### Security diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 3da6fd7e..385f9721 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -554,16 +554,31 @@ In all other cases, the exit code is that of the client application set according to either the *--term* command-line option or the *term* config option in *foot.ini*(5). -*PWD* - Current working directory (at the time of launching foot) - *COLORTERM* This variable is set to *truecolor*, to indicate to client applications that 24-bit RGB colors are supported. +*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 --git a/doc/footclient.1.scd b/doc/footclient.1.scd index 7d89b9ed..365689af 100644 --- a/doc/footclient.1.scd +++ b/doc/footclient.1.scd @@ -73,6 +73,11 @@ terminal has terminated. The child process in the new terminal instance will use footclient's environment, instead of the server's. + Environment variables listed in the *Variables set in the child + process* section will be overwritten by the foot server. For + example, the new terminal will use *TERM* from the configuration, + not footclient's environment. + *-d*,*--log-level*={*info*,*warning*,*error*,*none*} Log level, used both for log output on stderr as well as syslog. Default: _warning_. @@ -163,9 +168,27 @@ fallback to the less specific path, with the following priority: 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 --git a/server.c b/server.c index ca55b8f3..7cc87152 100644 --- a/server.c +++ b/server.c @@ -332,7 +332,8 @@ fdm_client(struct fdm *fdm, int fd, int events, void *data) instance->terminal = term_init( conf != NULL ? conf : server->conf, server->fdm, server->reaper, server->wayl, "footclient", cwd, token, - cdata.argc, argv, envp, &term_shutdown_handler, instance); + cdata.argc, argv, (const char *const *)envp, + &term_shutdown_handler, instance); if (instance->terminal == NULL) { LOG_ERR("failed to instantiate new terminal"); diff --git a/slave.c b/slave.c index 0cf64fa0..fa53c515 100644 --- a/slave.c +++ b/slave.c @@ -25,6 +25,11 @@ extern char **environ; +struct environ { + size_t count; + char **envp; +}; + #if defined(__FreeBSD__) static char * find_file_in_path(const char *file) @@ -303,9 +308,69 @@ err: _exit(errno); } +static bool +env_matches_var_name(const char *e, const char *name) +{ + const size_t e_len = strlen(e); + const size_t name_len = strlen(name); + + if (e_len <= name_len) + return false; + if (memcmp(e, name, name_len) != 0) + return false; + if (e[name_len] != '=') + return false; + return true; +} + +static void +add_to_env(struct environ *env, const char *name, const char *value) +{ + if (env->envp == NULL) + setenv(name, value, 1); + else { + char *e = 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 +415,30 @@ slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, _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++; - unsetenv("TERM_PROGRAM"); - unsetenv("TERM_PROGRAM_VERSION"); + custom_env.envp = xcalloc( + custom_env.count + 1, sizeof(custom_env.envp[0])); + + size_t i = 0; + for (const char *const *e = envp; *e != NULL; e++, i++) + custom_env.envp[i] = xstrdup(*e); + xassert(custom_env.envp[custom_env.count] == NULL); + } + + add_to_env(&custom_env, "TERM", term_env); + add_to_env(&custom_env, "COLORTERM", "truecolor"); + add_to_env(&custom_env, "PWD", cwd); + + del_from_env(&custom_env, "TERM_PROGRAM"); + del_from_env(&custom_env, "TERM_PROGRAM_VERSION"); #if defined(FOOT_TERMINFO_PATH) - setenv("TERMINFO", FOOT_TERMINFO_PATH, 1); + add_to_env(&custom_envp, "TERMINFO", FOOT_TERMINFO_PATH); #endif if (extra_env_vars != NULL) { @@ -367,9 +447,9 @@ slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, const char *value = it->item.value; if (strlen(value) == 0) - unsetenv(name); + del_from_env(&custom_env, name); else - setenv(name, value, 1); + add_to_env(&custom_env, name, value); } } @@ -393,9 +473,10 @@ slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, } if (is_valid_shell(shell_argv[0])) - setenv("SHELL", shell_argv[0], 1); + add_to_env(&custom_env, "SHELL", shell_argv[0]); - slave_exec(ptmx, shell_argv, envp != NULL ? envp : environ, + slave_exec(ptmx, shell_argv, + custom_env.envp != NULL ? custom_env.envp : environ, fork_pipe[1], login_shell, notifications); BUG("Unexpected return from slave_exec()"); break; diff --git a/slave.h b/slave.h index b1c08f14..26d93abb 100644 --- a/slave.h +++ b/slave.h @@ -7,7 +7,7 @@ #include "user-notification.h" pid_t slave_spawn( - int ptmx, int argc, const char *cwd, char *const *argv, char *const *envp, + int ptmx, int argc, const char *cwd, char *const *argv, const char *const *envp, const env_var_list_t *extra_env_vars, const char *term_env, const char *conf_shell, bool login_shell, const user_notifications_t *notifications); diff --git a/terminal.c b/terminal.c index efbbded1..5332ada0 100644 --- a/terminal.c +++ b/terminal.c @@ -1061,7 +1061,7 @@ static void fdm_client_terminated( 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, int argc, char *const *argv, const char *const *envp, void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data) { int ptmx = -1; diff --git a/terminal.h b/terminal.h index a8b65198..ddd4f1ea 100644 --- a/terminal.h +++ b/terminal.h @@ -728,7 +728,7 @@ struct config; struct terminal *term_init( const struct config *conf, struct fdm *fdm, struct reaper *reaper, struct wayland *wayl, const char *foot_exe, const char *cwd, - const char *token, int argc, char *const *argv, char *const *envp, + const char *token, 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); From 8073ad352b6f6034a648b1a4331d9ccabbddbf82 Mon Sep 17 00:00:00 2001 From: Artturin Date: Tue, 16 Jan 2024 22:40:35 +0200 Subject: [PATCH 0551/1323] slave: fix typo ``` ../slave.c: In function 'slave_spawn': ../slave.c:441:21: error: 'custom_envp' undeclared (first use in this function); did you mean 'custom_env'? 441 | add_to_env(&custom_envp, "TERMINFO", FOOT_TERMINFO_PATH); | ^~~~~~~~~~~ | custom_env ../slave.c:441:21: note: each undeclared identifier is reported only once for each function it appears in ``` --- slave.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slave.c b/slave.c index fa53c515..47d1c392 100644 --- a/slave.c +++ b/slave.c @@ -438,7 +438,7 @@ slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, del_from_env(&custom_env, "TERM_PROGRAM_VERSION"); #if defined(FOOT_TERMINFO_PATH) - add_to_env(&custom_envp, "TERMINFO", FOOT_TERMINFO_PATH); + add_to_env(&custom_env, "TERMINFO", FOOT_TERMINFO_PATH); #endif if (extra_env_vars != NULL) { From 6ed1c28d2c5209c0149ff222ec2f96369bab7308 Mon Sep 17 00:00:00 2001 From: Craig Barnes Date: Tue, 23 Jan 2024 20:33:46 +0000 Subject: [PATCH 0552/1323] terminal: simplify some string-related code in reload_fonts() --- terminal.c | 36 ++++++++---------------------------- xmalloc.c | 2 -- xmalloc.h | 12 ++++++++++++ 3 files changed, 20 insertions(+), 30 deletions(-) diff --git a/terminal.c b/terminal.c index 5332ada0..5d0dc01d 100644 --- a/terminal.c +++ b/terminal.c @@ -939,11 +939,7 @@ reload_fonts(struct terminal *term, bool resize_grid) 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 +962,14 @@ reload_fonts(struct terminal *term, bool resize_grid) 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] = { diff --git a/xmalloc.c b/xmalloc.c index 5d1fc997..ded7f4e3 100644 --- a/xmalloc.c +++ b/xmalloc.c @@ -1,8 +1,6 @@ #include -#include #include #include -#include #include "xmalloc.h" #include "debug.h" diff --git a/xmalloc.h b/xmalloc.h index 8a3098b8..74282f8f 100644 --- a/xmalloc.h +++ b/xmalloc.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -16,3 +17,14 @@ char *xstrndup(const char *str, size_t n) XSTRDUP; char *xasprintf(const char *format, ...) PRINTF(1) XMALLOC; char *xvasprintf(const char *format, va_list va) VPRINTF(1) XMALLOC; char32_t *xc32dup(const char32_t *str) XSTRDUP; + +static inline 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; +} From 4ee4f47065d7e1a77a18d3b029f94ccab8ddb3d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 22 Jan 2024 16:39:26 +0100 Subject: [PATCH 0553/1323] input: kitty: update to latest version of the spec Starting with kitty 0.32.0, the modifier bits during modifier key events behave differently, compared to before. Or, rather, they have now been spec:ed; before, behavior was different on e.g. MacOS, and Linux. The new behavior is this: On key press, the modifier bits in the kitty key event *includes* the pressed modifier key. On key release, the modifier bits in the kitty key event does *not* include the released modifier key. In other words, The modifier bits reflects the state *after* the key event. This is the exact opposite of what foot did before this patch. The patch is really pretty small: in order to include the key in the modifier set, we simulate a key press to update the XKB state, using xkb_state_uppate_key(). For key pressed, we simulate an XKB_KEY_DOWN event, and for key releases we simulate an XKB_KEY_UP event. Then we re-retrieve the modifers, both the full set, and the consumed set. Closes #1561 --- CHANGELOG.md | 5 ++++ input.c | 80 +++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 71 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01309223..cb62594e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,9 +65,14 @@ 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]). [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 ### Deprecated diff --git a/input.c b/input.c index e3613a90..ba118e67 100644 --- a/input.c +++ b/input.c @@ -1135,19 +1135,77 @@ kitty_kbd_protocol(struct seat *seat, struct terminal *term, 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; + /* 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 consumed = 0; + + 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, ctx->key); + consumed = xkb_state_key_get_consumed_mods2( + seat->kbd.xkb_state, ctx->key, XKB_CONSUMED_MODE_GTK); + +#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 { + mods = ctx->mods; + + /* Re-retrieve the consumed modifiers using the GTK mode, to + better match kitty. */ + consumed = xkb_state_key_get_consumed_mods2( + seat->kbd.xkb_state, ctx->key, XKB_CONSUMED_MODE_GTK); + } + + mods &= seat->kbd.kitty_significant; + consumed &= 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); + bool is_text = count > 0 && utf32 != NULL && (effective & ~caps_num) == 0; for (size_t i = 0; utf32[i] != U'\0'; i++) { if (!iswprint(utf32[i])) { @@ -1159,12 +1217,6 @@ kitty_kbd_protocol(struct seat *seat, struct terminal *term, const bool report_associated_text = (flags & KITTY_KBD_REPORT_ASSOCIATED) && is_text && !released; - /* Lookup sym in the pre-defined keysym table */ - const struct kitty_key_data *info = bsearch( - &sym, kitty_keymap, ALEN(kitty_keymap), sizeof(kitty_keymap[0]), - &kitty_search); - xassert(info == NULL || info->sym == sym); - if (composing) { /* We never emit anything while composing, *except* modifiers * (and only in report-all-keys-as-escape-codes mode) */ From 7e3da3007bb2459900d2c88d3ec11aecd1956849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonardo=20Hern=C3=A1ndez=20Hern=C3=A1ndez?= Date: Tue, 9 Jan 2024 22:51:17 -0600 Subject: [PATCH 0554/1323] wayland: use wl_compositor version 6 when available --- terminal.c | 25 ++++++++++++++++++------- terminal.h | 1 + wayland.c | 47 ++++++++++++++++++++++++++++++++++++++++++++--- wayland.h | 3 +++ 4 files changed, 66 insertions(+), 10 deletions(-) diff --git a/terminal.c b/terminal.c index 5d0dc01d..7a51257d 100644 --- a/terminal.c +++ b/terminal.c @@ -2071,6 +2071,12 @@ term_fractional_scaling(const struct terminal *term) return term->wl->fractional_scale_manager != NULL && term->window->scale > 0.; } +bool +term_preferred_buffer_scale(const struct terminal *term) +{ + return term->wl->has_wl_compositor_v6; +} + bool term_update_scale(struct terminal *term) { @@ -2081,6 +2087,9 @@ term_update_scale(struct terminal *term) * the scale in the following order: * * - “preferred” scale, from the fractional-scale-v1 protocol + * - "preferred" scale, from wl_compositor version 6. + NOTE: if the compositor advertises version 6 we must use 1.0 + until wl_surface.preferred_buffer_scale is sent * - scaling factor of output we most recently were mapped on * - if we're not mapped, use the last known scaling factor * - if we're not mapped, and we don't have a last known scaling @@ -2090,13 +2099,15 @@ term_update_scale(struct terminal *term) */ 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; diff --git a/terminal.h b/terminal.h index ddd4f1ea..0dca0f48 100644 --- a/terminal.h +++ b/terminal.h @@ -743,6 +743,7 @@ bool term_paste_data_to_slave( struct terminal *term, const void *data, size_t len); bool term_fractional_scaling(const struct terminal *term); +bool term_preferred_buffer_scale(const struct terminal *term); bool term_update_scale(struct terminal *term); bool term_font_size_increase(struct terminal *term); bool term_font_size_decrease(struct terminal *term); diff --git a/wayland.c b/wayland.c index 34050d18..24d70235 100644 --- a/wayland.c +++ b/wayland.c @@ -705,9 +705,37 @@ surface_leave(void *data, struct wl_surface *wl_surface, LOG_WARN("unmapped from unknown output"); } +#if defined(WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION) +static void +surface_preferred_buffer_scale(void *data, struct wl_surface *surface, + int32_t scale) +{ + struct wl_window *win = data; + + if (win->preferred_buffer_scale == scale) + return; + + LOG_DBG("wl_surface preferred scale: %d -> %d", win->preferred_buffer_scale, scale); + + win->preferred_buffer_scale = scale; + update_term_for_output_change(win->term); +} + +static void +surface_preferred_buffer_transform(void *data, struct wl_surface *surface, + uint32_t transform) +{ + +} +#endif + static const struct wl_surface_listener surface_listener = { .enter = &surface_enter, .leave = &surface_leave, +#if defined(WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION) + .preferred_buffer_scale = &surface_preferred_buffer_scale, + .preferred_buffer_transform = &surface_preferred_buffer_transform, +#endif }; static void @@ -1057,8 +1085,14 @@ handle_global(void *data, struct wl_registry *registry, 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; + wayl->has_wl_compositor_v6 = version >= 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) { @@ -1700,6 +1734,10 @@ wayl_win_init(struct terminal *term, const char *token) win->fractional_scale, &fractional_scale_listener, win); } + if (wayl->has_wl_compositor_v6) { + win->preferred_buffer_scale = 1; + } + win->xdg_surface = xdg_wm_base_get_xdg_surface(wayl->shell, win->surface.surf); xdg_surface_add_listener(win->xdg_surface, &xdg_surface_listener, win); @@ -2020,8 +2058,11 @@ surface_scale_explicit_width_height( 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 = 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); diff --git a/wayland.h b/wayland.h index 67aadec2..145e480d 100644 --- a/wayland.h +++ b/wayland.h @@ -363,6 +363,7 @@ struct wl_window { bool unmapped; float scale; + int preferred_buffer_scale; struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration; @@ -429,6 +430,8 @@ struct wayland { struct wl_subcompositor *sub_compositor; struct wl_shm *shm; + bool has_wl_compositor_v6; + struct zxdg_output_manager_v1 *xdg_output_manager; struct xdg_wm_base *shell; From 43e27a88434756d6de44a2dde4dbd342ccb387f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 24 Jan 2024 19:59:30 +0100 Subject: [PATCH 0555/1323] wayland: 'mode' is unused when LOG_ENABLE_DBG is not set --- wayland.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wayland.c b/wayland.c index 24d70235..25037c8a 100644 --- a/wayland.c +++ b/wayland.c @@ -2058,7 +2058,7 @@ surface_scale_explicit_width_height( wp_viewport_set_destination( surf->viewport, roundf(width / scale), roundf(height / scale)); } else { - const char *mode = term_preferred_buffer_scale(win->term) + 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 " From 91b22ae21acd0b3171814b47b0dead027f8da779 Mon Sep 17 00:00:00 2001 From: Craig Barnes Date: Thu, 25 Jan 2024 07:03:50 +0000 Subject: [PATCH 0556/1323] Replace unchecked allocations with calls to xmalloc.h functions --- char32.c | 4 ++-- config.c | 2 +- render.c | 2 +- server.c | 2 +- shm.c | 2 +- spawn.c | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/char32.c b/char32.c index e25db3f1..97c599d9 100644 --- a/char32.c +++ b/char32.c @@ -129,11 +129,11 @@ UNITTEST 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 --git a/config.c b/config.c index de15add3..f964f7c1 100644 --- a/config.c +++ b/config.c @@ -3213,7 +3213,7 @@ config_load(struct config *conf, const char *conf_path, 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; } } diff --git a/render.c b/render.c index d679b5e8..a522c24c 100644 --- a/render.c +++ b/render.c @@ -1460,7 +1460,7 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, * from grid), and mark all cells as dirty. This ensures they are * re-rendered when the pre-edit text is modified or removed. */ - struct cell *real_cells = malloc(cells_used * sizeof(real_cells[0])); + struct cell *real_cells = xmalloc(cells_used * sizeof(real_cells[0])); for (int i = 0; i < cells_used; i++) { xassert(col_idx + i < term->cols); real_cells[i] = row->cells[col_idx + i]; diff --git a/server.c b/server.c index 7cc87152..068ca057 100644 --- a/server.c +++ b/server.c @@ -301,7 +301,7 @@ fdm_client(struct fdm *fdm, int fd, int events, void *data) #undef CHECK_BUF_AND_NULL #undef CHECK_BUF - struct terminal_instance *instance = malloc(sizeof(struct terminal_instance)); + struct terminal_instance *instance = xmalloc(sizeof(struct terminal_instance)); const bool need_to_clone_conf = tll_length(overrides)> 0 || diff --git a/shm.c b/shm.c index 171459f6..87bd33a7 100644 --- a/shm.c +++ b/shm.c @@ -489,7 +489,7 @@ get_new_buffers(struct buffer_chain *chain, size_t count, 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++) diff --git a/spawn.c b/spawn.c index 90b892f3..6935a29a 100644 --- a/spawn.c +++ b/spawn.c @@ -145,7 +145,7 @@ spawn_expand_template(const struct config_spawn_template *template, expanded[len] = '\0'; \ } while (0) - *argv = malloc((*argc + 1) * sizeof((*argv)[0])); + *argv = xmalloc((*argc + 1) * sizeof((*argv)[0])); /* Expand the provided keys */ for (size_t i = 0; i < *argc; i++) { From 44c0cf594b4674e92b9dd1646d7a2d43c401e5d8 Mon Sep 17 00:00:00 2001 From: Craig Barnes Date: Thu, 25 Jan 2024 01:07:53 +0000 Subject: [PATCH 0557/1323] csi: simplify handling of Set/Reset Mode (SM/RM) sequences --- csi.c | 51 ++++++++++++++++++--------------------------------- 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/csi.c b/csi.c index b83fc9b3..04f03b05 100644 --- a/csi.c +++ b/csi.c @@ -1081,44 +1081,29 @@ csi_dispatch(struct terminal *term, uint8_t final) break; case 'h': - /* Set mode */ - switch (vt_param_get(term, 0, 0)) { - case 2: /* Keyboard Action Mode - AM */ - LOG_WARN("unimplemented: keyboard action mode (AM)"); - break; - - case 4: /* Insert Mode - IRM */ - term->insert_mode = true; + case 'l': { + /* Set/Reset Mode (SM/RM) */ + int param = vt_param_get(term, 0, 0); + bool sm = final == 'h'; + if (param == 4) { + /* Insertion Replacement Mode (IRM) */ + term->insert_mode = sm; term_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); From e0f3703ae6c4084f9275cfc78d87e5604cc27a10 Mon Sep 17 00:00:00 2001 From: Craig Barnes Date: Wed, 24 Jan 2024 23:17:28 +0000 Subject: [PATCH 0558/1323] util: add streq() function and use in place of strcmp(...) == 0 --- client.c | 8 +- config.c | 220 ++++++++++++++++++++++---------------------- cursor-shape.c | 2 +- input.c | 6 +- log.c | 2 +- main.c | 8 +- osc.c | 4 +- render.c | 4 +- selection.c | 4 +- slave.c | 3 +- terminal.c | 2 +- tests/test-config.c | 24 +++-- uri.c | 6 +- url-mode.c | 4 +- util.h | 8 ++ wayland.c | 32 +++---- 16 files changed, 172 insertions(+), 165 deletions(-) diff --git a/client.c b/client.c index 8456dfd8..8576531f 100644 --- a/client.c +++ b/client.c @@ -315,11 +315,11 @@ main(int argc, char *const *argv) } case 'l': - if (optarg == NULL || strcmp(optarg, "auto") == 0) + if (optarg == NULL || streq(optarg, "auto")) log_colorize = LOG_COLORIZE_AUTO; - else if (strcmp(optarg, "never") == 0) + else if (streq(optarg, "never")) log_colorize = LOG_COLORIZE_NEVER; - else if (strcmp(optarg, "always") == 0) + else if (streq(optarg, "always")) log_colorize = LOG_COLORIZE_ALWAYS; else { fprintf(stderr, "%s: argument must be one of 'never', 'always' or 'auto'\n", optarg); @@ -419,7 +419,7 @@ main(int argc, char *const *argv) if (resolved_path_cwd != NULL && resolved_path_pwd != NULL && - strcmp(resolved_path_cwd, resolved_path_pwd) == 0) + streq(resolved_path_cwd, resolved_path_pwd)) { /* * The resolved path of $PWD matches the resolved path of diff --git a/config.c b/config.c index f964f7c1..ed021569 100644 --- a/config.c +++ b/config.c @@ -824,7 +824,7 @@ parse_section_main(struct context *ctx) const char *value = ctx->value; bool errors_are_fatal = ctx->errors_are_fatal; - if (strcmp(key, "include") == 0) { + if (streq(key, "include")) { char *_include_path = NULL; const char *include_path = NULL; @@ -864,25 +864,25 @@ parse_section_main(struct context *ctx) return ret; } - else if (strcmp(key, "term") == 0) + else if (streq(key, "term")) return value_to_str(ctx, &conf->term); - else if (strcmp(key, "shell") == 0) + else if (streq(key, "shell")) return value_to_str(ctx, &conf->shell); - else if (strcmp(key, "login-shell") == 0) + else if (streq(key, "login-shell")) return value_to_bool(ctx, &conf->login_shell); - else if (strcmp(key, "title") == 0) + else if (streq(key, "title")) return value_to_str(ctx, &conf->title); - else if (strcmp(key, "locked-title") == 0) + else if (streq(key, "locked-title")) return value_to_bool(ctx, &conf->locked_title); - else if (strcmp(key, "app-id") == 0) + else if (streq(key, "app-id")) return value_to_str(ctx, &conf->app_id); - else if (strcmp(key, "initial-window-size-pixels") == 0) { + else if (streq(key, "initial-window-size-pixels")) { if (!value_to_dimensions(ctx, &conf->size.width, &conf->size.height)) return false; @@ -890,7 +890,7 @@ parse_section_main(struct context *ctx) return true; } - else if (strcmp(key, "initial-window-size-chars") == 0) { + else if (streq(key, "initial-window-size-chars")) { if (!value_to_dimensions(ctx, &conf->size.width, &conf->size.height)) return false; @@ -898,7 +898,7 @@ parse_section_main(struct context *ctx) return true; } - else if (strcmp(key, "pad") == 0) { + else if (streq(key, "pad")) { unsigned x, y; char mode[16] = {0}; @@ -918,11 +918,11 @@ parse_section_main(struct context *ctx) 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, "bold-text-in-bright")) { + if (streq(value, "palette-based")) { conf->bold_in_bright.enabled = true; conf->bold_in_bright.palette_based = true; } else { @@ -933,7 +933,7 @@ parse_section_main(struct context *ctx) return true; } - else if (strcmp(key, "initial-window-mode") == 0) { + else if (streq(key, "initial-window-mode")) { _Static_assert(sizeof(conf->startup_mode) == sizeof(int), "enum is not 32-bit"); @@ -943,16 +943,16 @@ parse_section_main(struct context *ctx) (int *)&conf->startup_mode); } - else if (strcmp(key, "font") == 0 || - strcmp(key, "font-bold") == 0 || - strcmp(key, "font-italic") == 0 || - strcmp(key, "font-bold-italic") == 0) + else if (streq(key, "font") || + streq(key, "font-bold") || + streq(key, "font-italic") || + streq(key, "font-bold-italic")) { size_t idx = - strcmp(key, "font") == 0 ? 0 : - strcmp(key, "font-bold") == 0 ? 1 : - strcmp(key, "font-italic") == 0 ? 2 : 3; + streq(key, "font") ? 0 : + streq(key, "font-bold") ? 1 : + streq(key, "font-italic") ? 2 : 3; struct config_font_list new_list = value_to_fonts(ctx); if (new_list.arr == NULL) @@ -963,7 +963,7 @@ parse_section_main(struct context *ctx) return true; } - else if (strcmp(key, "font-size-adjustment") == 0) { + else if (streq(key, "font-size-adjustment")) { const size_t len = strlen(ctx->value); if (len >= 1 && ctx->value[len - 1] == '%') { errno = 0; @@ -988,44 +988,44 @@ parse_section_main(struct context *ctx) } } - else if (strcmp(key, "line-height") == 0) + else if (streq(key, "line-height")) return value_to_pt_or_px(ctx, &conf->line_height); - else if (strcmp(key, "letter-spacing") == 0) + else if (streq(key, "letter-spacing")) return value_to_pt_or_px(ctx, &conf->letter_spacing); - else if (strcmp(key, "horizontal-letter-offset") == 0) + else if (streq(key, "horizontal-letter-offset")) return value_to_pt_or_px(ctx, &conf->horizontal_letter_offset); - else if (strcmp(key, "vertical-letter-offset") == 0) + else if (streq(key, "vertical-letter-offset")) return value_to_pt_or_px(ctx, &conf->vertical_letter_offset); - else if (strcmp(key, "underline-offset") == 0) { + else if (streq(key, "underline-offset")) { if (!value_to_pt_or_px(ctx, &conf->underline_offset)) return false; conf->use_custom_underline_offset = true; return true; } - else if (strcmp(key, "underline-thickness") == 0) + else if (streq(key, "underline-thickness")) return value_to_pt_or_px(ctx, &conf->underline_thickness); - else if (strcmp(key, "dpi-aware") == 0) + 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"); @@ -1035,14 +1035,14 @@ parse_section_main(struct context *ctx) (int *)&conf->selection_target); } - else if (strcmp(key, "box-drawings-uses-font-glyphs") == 0) + else if (streq(key, "box-drawings-uses-font-glyphs")) return value_to_bool(ctx, &conf->box_drawings_uses_font_glyphs); - else if (strcmp(key, "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; } @@ -1062,15 +1062,15 @@ parse_section_bell(struct context *ctx) struct config *conf = ctx->conf; const char *key = ctx->key; - if (strcmp(key, "urgent") == 0) + if (streq(key, "urgent")) return value_to_bool(ctx, &conf->bell.urgent); - else if (strcmp(key, "notify") == 0) + else if (streq(key, "notify")) return value_to_bool(ctx, &conf->bell.notify); - else if (strcmp(key, "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); @@ -1085,10 +1085,10 @@ parse_section_scrollback(struct context *ctx) const char *key = ctx->key; const char *value = ctx->value; - if (strcmp(key, "lines") == 0) + if (streq(key, "lines")) return value_to_uint32(ctx, 10, &conf->scrollback.lines); - else if (strcmp(key, "indicator-position") == 0) { + else if (streq(key, "indicator-position")) { _Static_assert( sizeof(conf->scrollback.indicator.position) == sizeof(int), "enum is not 32-bit"); @@ -1099,12 +1099,12 @@ parse_section_scrollback(struct context *ctx) (int *)&conf->scrollback.indicator.position); } - else if (strcmp(key, "indicator-format") == 0) { - if (strcmp(value, "percentage") == 0) { + else if (streq(key, "indicator-format")) { + if (streq(value, "percentage")) { conf->scrollback.indicator.format = SCROLLBACK_INDICATOR_FORMAT_PERCENTAGE; return true; - } else if (strcmp(value, "line") == 0) { + } else if (streq(value, "line")) { conf->scrollback.indicator.format = SCROLLBACK_INDICATOR_FORMAT_LINENO; return true; @@ -1112,7 +1112,7 @@ parse_section_scrollback(struct context *ctx) 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 { @@ -1128,13 +1128,13 @@ parse_section_url(struct context *ctx) 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"); @@ -1144,7 +1144,7 @@ parse_section_url(struct context *ctx) (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); @@ -1196,7 +1196,7 @@ parse_section_url(struct context *ctx) 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; @@ -1251,13 +1251,13 @@ parse_section_colors(struct context *ctx) 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, @@ -1271,7 +1271,7 @@ parse_section_colors(struct context *ctx) 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, @@ -1285,7 +1285,7 @@ parse_section_colors(struct context *ctx) 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, @@ -1299,7 +1299,7 @@ parse_section_colors(struct context *ctx) 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, @@ -1313,7 +1313,7 @@ parse_section_colors(struct context *ctx) return true; } - else if (strcmp(key, "urls") == 0) { + else if (streq(key, "urls")) { if (!value_to_color(ctx, &conf->colors.url, false)) return false; @@ -1321,7 +1321,7 @@ parse_section_colors(struct context *ctx) return true; } - else if (strcmp(key, "alpha") == 0) { + else if (streq(key, "alpha")) { float alpha; if (!value_to_float(ctx, &alpha)) return false; @@ -1335,7 +1335,7 @@ parse_section_colors(struct context *ctx) 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; @@ -1369,7 +1369,7 @@ parse_section_cursor(struct context *ctx) struct config *conf = ctx->conf; const char *key = ctx->key; - if (strcmp(key, "style") == 0) { + if (streq(key, "style")) { _Static_assert(sizeof(conf->cursor.style) == sizeof(int), "enum is not 32-bit"); @@ -1379,10 +1379,10 @@ parse_section_cursor(struct context *ctx) (int *)&conf->cursor.style); } - else if (strcmp(key, "blink") == 0) + 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, @@ -1397,10 +1397,10 @@ parse_section_cursor(struct context *ctx) 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 { @@ -1415,10 +1415,10 @@ parse_section_mouse(struct context *ctx) struct config *conf = ctx->conf; const char *key = ctx->key; - if (strcmp(key, "hide-when-typing") == 0) + if (streq(key, "hide-when-typing")) return value_to_bool(ctx, &conf->mouse.hide_when_typing); - else if (strcmp(key, "alternate-scroll-mode") == 0) + else if (streq(key, "alternate-scroll-mode")) return value_to_bool(ctx, &conf->mouse.alternate_scroll_mode); else { @@ -1433,7 +1433,7 @@ parse_section_csd(struct context *ctx) struct config *conf = ctx->conf; const char *key = ctx->key; - if (strcmp(key, "preferred") == 0) { + if (streq(key, "preferred")) { _Static_assert(sizeof(conf->csd.preferred) == sizeof(int), "enum is not 32-bit"); @@ -1443,7 +1443,7 @@ parse_section_csd(struct context *ctx) (int *)&conf->csd.preferred); } - else if (strcmp(key, "font") == 0) { + else if (streq(key, "font")) { struct config_font_list new_list = value_to_fonts(ctx); if (new_list.arr == NULL) return false; @@ -1453,7 +1453,7 @@ parse_section_csd(struct context *ctx) return true; } - else if (strcmp(key, "color") == 0) { + else if (streq(key, "color")) { uint32_t color; if (!value_to_color(ctx, &color, true)) return false; @@ -1463,13 +1463,13 @@ parse_section_csd(struct context *ctx) return true; } - else if (strcmp(key, "size") == 0) + else if (streq(key, "size")) return value_to_uint16(ctx, 10, &conf->csd.title_height); - else if (strcmp(key, "button-width") == 0) + else if (streq(key, "button-width")) return value_to_uint16(ctx, 10, &conf->csd.button_width); - else if (strcmp(key, "button-color") == 0) { + else if (streq(key, "button-color")) { if (!value_to_color(ctx, &conf->csd.color.buttons, true)) return false; @@ -1477,7 +1477,7 @@ parse_section_csd(struct context *ctx) return true; } - else if (strcmp(key, "button-minimize-color") == 0) { + else if (streq(key, "button-minimize-color")) { if (!value_to_color(ctx, &conf->csd.color.minimize, true)) return false; @@ -1485,7 +1485,7 @@ parse_section_csd(struct context *ctx) return true; } - else if (strcmp(key, "button-maximize-color") == 0) { + else if (streq(key, "button-maximize-color")) { if (!value_to_color(ctx, &conf->csd.color.maximize, true)) return false; @@ -1493,7 +1493,7 @@ parse_section_csd(struct context *ctx) return true; } - else if (strcmp(key, "button-close-color") == 0) { + else if (streq(key, "button-close-color")) { if (!value_to_color(ctx, &conf->csd.color.quit, true)) return false; @@ -1501,7 +1501,7 @@ parse_section_csd(struct context *ctx) return true; } - else if (strcmp(key, "border-color") == 0) { + else if (streq(key, "border-color")) { if (!value_to_color(ctx, &conf->csd.color.border, true)) return false; @@ -1509,13 +1509,13 @@ parse_section_csd(struct context *ctx) return true; } - else if (strcmp(key, "border-width") == 0) + else if (streq(key, "border-width")) return value_to_uint16(ctx, 10, &conf->csd.border_width_visible); - else if (strcmp(key, "hide-when-maximized") == 0) + else if (streq(key, "hide-when-maximized")) return value_to_bool(ctx, &conf->csd.hide_when_maximized); - else if (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 { @@ -1574,13 +1574,13 @@ parse_modifiers(struct context *ctx, const char *text, size_t len, key != NULL; key = strtok_r(NULL, "+", &tok_ctx)) { - if (strcmp(key, XKB_MOD_NAME_SHIFT) == 0) + if (streq(key, XKB_MOD_NAME_SHIFT)) modifiers->shift = true; - else if (strcmp(key, XKB_MOD_NAME_CTRL) == 0) + else if (streq(key, XKB_MOD_NAME_CTRL)) modifiers->ctrl = true; - else if (strcmp(key, XKB_MOD_NAME_ALT) == 0) + else if (streq(key, XKB_MOD_NAME_ALT)) modifiers->alt = true; - else if (strcmp(key, XKB_MOD_NAME_LOGO) == 0) + else if (streq(key, XKB_MOD_NAME_LOGO)) modifiers->super = true; else { LOG_CONTEXTUAL_ERR("not a valid modifier name: %s", key); @@ -1698,7 +1698,7 @@ static int mouse_button_name_to_code(const char *name) { for (size_t i = 0; i < ALEN(button_map); i++) { - if (strcmp(button_map[i].name, name) == 0) + if (streq(button_map[i].name, name)) return button_map[i].code; } return -1; @@ -1947,7 +1947,7 @@ parse_key_binding_section(struct context *ctx, 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)) { @@ -2248,7 +2248,7 @@ parse_section_mouse_bindings(struct context *ctx) const char *key = ctx->key; const char *value = ctx->value; - if (strcmp(key, "selection-override-modifiers") == 0) { + if (streq(key, "selection-override-modifiers")) { if (!parse_modifiers( ctx, ctx->value, strlen(value), &conf->mouse.selection_override_modifiers)) @@ -2275,7 +2275,7 @@ parse_section_mouse_bindings(struct context *ctx) if (binding_action_map[action] == NULL) continue; - if (strcmp(key, binding_action_map[action]) != 0) + if (!streq(key, binding_action_map[action])) continue; if (!value_to_key_combos( @@ -2376,7 +2376,7 @@ parse_section_environment(struct context *ctx) /* Check for pre-existing env variable */ tll_foreach(conf->env_vars, it) { - if (strcmp(it->item.name, key) == 0) + if (streq(it->item.name, key)) return value_to_str(ctx, &it->item.value); } @@ -2398,7 +2398,7 @@ parse_section_tweak(struct context *ctx) struct config *conf = ctx->conf; const char *key = ctx->key; - if (strcmp(key, "scaling-filter") == 0) { + if (streq(key, "scaling-filter")) { static const char *filters[] = { [FCFT_SCALING_FILTER_NONE] = "none", [FCFT_SCALING_FILTER_NEAREST] = "nearest", @@ -2414,13 +2414,13 @@ parse_section_tweak(struct context *ctx) return value_to_enum(ctx, filters, (int *)&conf->tweak.fcft_filter); } - else if (strcmp(key, "overflowing-glyphs") == 0) + else if (streq(key, "overflowing-glyphs")) return value_to_bool(ctx, &conf->tweak.overflowing_glyphs); - else if (strcmp(key, "damage-whole-window") == 0) + else if (streq(key, "damage-whole-window")) return value_to_bool(ctx, &conf->tweak.damage_whole_window); - else if (strcmp(key, "grapheme-shaping") == 0) { + else if (streq(key, "grapheme-shaping")) { if (!value_to_bool(ctx, &conf->tweak.grapheme_shaping)) return false; @@ -2443,7 +2443,7 @@ parse_section_tweak(struct context *ctx) return true; } - else if (strcmp(key, "grapheme-width-method") == 0) { + else if (streq(key, "grapheme-width-method")) { _Static_assert(sizeof(conf->tweak.grapheme_width_method) == sizeof(int), "enum is not 32-bit"); @@ -2453,7 +2453,7 @@ parse_section_tweak(struct context *ctx) (int *)&conf->tweak.grapheme_width_method); } - else if (strcmp(key, "render-timer") == 0) { + else if (streq(key, "render-timer")) { _Static_assert(sizeof(conf->tweak.render_timer) == sizeof(int), "enum is not 32-bit"); @@ -2463,7 +2463,7 @@ parse_section_tweak(struct context *ctx) (int *)&conf->tweak.render_timer); } - else if (strcmp(key, "delayed-render-lower") == 0) { + else if (streq(key, "delayed-render-lower")) { uint32_t ns; if (!value_to_uint32(ctx, 10, &ns)) return false; @@ -2477,7 +2477,7 @@ parse_section_tweak(struct context *ctx) return true; } - else if (strcmp(key, "delayed-render-upper") == 0) { + else if (streq(key, "delayed-render-upper")) { uint32_t ns; if (!value_to_uint32(ctx, 10, &ns)) return false; @@ -2491,7 +2491,7 @@ parse_section_tweak(struct context *ctx) return true; } - else if (strcmp(key, "max-shm-pool-size-mb") == 0) { + else if (streq(key, "max-shm-pool-size-mb")) { uint32_t mb; if (!value_to_uint32(ctx, 10, &mb)) return false; @@ -2500,19 +2500,19 @@ parse_section_tweak(struct context *ctx) 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 { @@ -2526,7 +2526,7 @@ parse_section_touch(struct context *ctx) { 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 { @@ -2649,7 +2649,7 @@ static enum section 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; diff --git a/cursor-shape.c b/cursor-shape.c index a5402928..131e6f1a 100644 --- a/cursor-shape.c +++ b/cursor-shape.c @@ -101,7 +101,7 @@ cursor_string_to_server_shape(const char *xcursor) 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 --git a/input.c b/input.c index ba118e67..25addf9b 100644 --- a/input.c +++ b/input.c @@ -854,7 +854,7 @@ UNITTEST const struct key_data *info = keymap_lookup(&term, XKB_KEY_ISO_Left_Tab, MOD_SHIFT | MOD_CTRL); xassert(info != NULL); - xassert(strcmp(info->seq, "\033[27;6;9~") == 0); + xassert(streq(info->seq, "\033[27;6;9~")); } UNITTEST @@ -865,12 +865,12 @@ UNITTEST const struct key_data *info = keymap_lookup(&term, XKB_KEY_Return, MOD_ALT); xassert(info != NULL); - xassert(strcmp(info->seq, "\033\r") == 0); + xassert(streq(info->seq, "\033\r")); term.modify_other_keys_2 = true; info = keymap_lookup(&term, XKB_KEY_Return, MOD_ALT); xassert(info != NULL); - xassert(strcmp(info->seq, "\033[27;3;13~") == 0); + xassert(streq(info->seq, "\033[27;3;13~")); } void diff --git a/log.c b/log.c index 360ca1c0..c13b4179 100644 --- a/log.c +++ b/log.c @@ -199,7 +199,7 @@ log_level_from_string(const char *str) return -1; for (int i = 0, n = map_len(); i < n; i++) - if (strcmp(str, log_level_map[i].name) == 0) + if (streq(str, log_level_map[i].name)) return i; return -1; diff --git a/main.c b/main.c index f7cf8354..98db9c38 100644 --- a/main.c +++ b/main.c @@ -351,11 +351,11 @@ main(int argc, char *const *argv) } case 'l': - if (optarg == NULL || strcmp(optarg, "auto") == 0) + if (optarg == NULL || streq(optarg, "auto")) log_colorize = LOG_COLORIZE_AUTO; - else if (strcmp(optarg, "never") == 0) + else if (streq(optarg, "never")) log_colorize = LOG_COLORIZE_NEVER; - else if (strcmp(optarg, "always") == 0) + else if (streq(optarg, "always")) log_colorize = LOG_COLORIZE_ALWAYS; else { fprintf(stderr, "%s: argument must be one of 'never', 'always' or 'auto'\n", optarg); @@ -538,7 +538,7 @@ main(int argc, char *const *argv) if (resolved_path_cwd != NULL && resolved_path_pwd != NULL && - strcmp(resolved_path_cwd, resolved_path_pwd) == 0) + streq(resolved_path_cwd, resolved_path_pwd)) { /* * The resolved path of $PWD matches the resolved path of diff --git a/osc.c b/osc.c index 45d114de..ba08964c 100644 --- a/osc.c +++ b/osc.c @@ -426,7 +426,7 @@ osc_set_pwd(struct terminal *term, char *string) return; } - if (strcmp(scheme, "file") == 0 && hostname_is_localhost(host)) { + if (streq(scheme, "file") && hostname_is_localhost(host)) { LOG_DBG("OSC7: pwd: %s", path); free(term->cwd); term->cwd = path; @@ -483,7 +483,7 @@ osc_uri(struct terminal *term, char *string) const char *value = operator + 1; - if (strcmp(key, "id") == 0) + if (streq(key, "id")) id = sdbm_hash(value); } diff --git a/render.c b/render.c index a522c24c..fcd21472 100644 --- a/render.c +++ b/render.c @@ -4559,8 +4559,8 @@ render_xcursor_set(struct seat *seat, struct terminal *term, 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; } diff --git a/selection.c b/selection.c index 50e41637..9e9bbb10 100644 --- a/selection.c +++ b/selection.c @@ -2080,7 +2080,7 @@ decode_one_uri(struct clipboard_receive *ctx, char *uri, size_t len) ctx->cb(" ", 1, ctx->user); ctx->add_space = true; - if (strcmp(scheme, "file") == 0 && hostname_is_localhost(host)) { + if (streq(scheme, "file") && hostname_is_localhost(host)) { if (ctx->quote_paths) ctx->cb("'", 1, ctx->user); @@ -2534,7 +2534,7 @@ select_mime_type_for_offer(const char *_mime_type, if (mime_type_map[i] == NULL) continue; - if (strcmp(_mime_type, mime_type_map[i]) == 0) { + if (streq(_mime_type, mime_type_map[i])) { mime_type = i; break; } diff --git a/slave.c b/slave.c index 47d1c392..bcd864e1 100644 --- a/slave.c +++ b/slave.c @@ -21,6 +21,7 @@ #include "macros.h" #include "terminal.h" #include "tokenize.h" +#include "util.h" #include "xmalloc.h" extern char **environ; @@ -121,7 +122,7 @@ is_valid_shell(const char *shell) if (line[0] == '#') continue; - if (strcmp(line, shell) == 0) { + if (streq(line, shell)) { fclose(f); return true; } diff --git a/terminal.c b/terminal.c index 7a51257d..787eaf2d 100644 --- a/terminal.c +++ b/terminal.c @@ -3243,7 +3243,7 @@ term_set_window_title(struct terminal *term, const char *title) if (term->conf->locked_title && term->window_title_has_been_set) return; - if (term->window_title != NULL && strcmp(term->window_title, title) == 0) + if (term->window_title != NULL && streq(term->window_title, title)) return; free(term->window_title); diff --git a/tests/test-config.c b/tests/test-config.c index 95abf1a4..182dd4f6 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -60,7 +60,7 @@ test_string(struct context *ctx, bool (*parse_fun)(struct context *ctx), BUG("[%s].%s=%s: failed to parse", ctx->section, ctx->key, ctx->value); } - if (strcmp(*ptr, input[i].value) != 0) { + if (!streq(*ptr, input[i].value)) { BUG("[%s].%s=%s: set value (%s) not the expected one (%s)", ctx->section, ctx->key, ctx->value, *ptr, input[i].value); @@ -357,9 +357,7 @@ test_spawn_template(struct context *ctx, bool (*parse_fun)(struct context *ctx), BUG("[%s].%s=%s: argv is NULL", ctx->section, ctx->key, ctx->value); for (size_t i = 0; i < ALEN(args); i++) { - if (ptr->argv.args[i] == NULL || - strcmp(ptr->argv.args[i], args[i]) != 0) - { + if (ptr->argv.args[i] == NULL || !streq(ptr->argv.args[i], args[i])) { BUG("[%s].%s=%s: set value not the expected one: " "mismatch of arg #%zu: expected=\"%s\", got=\"%s\"", ctx->section, ctx->key, ctx->value, i, @@ -879,7 +877,7 @@ test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), for (size_t i = 0; i < ALEN(args); i++) { if (binding->aux.pipe.args[i] == NULL || - strcmp(binding->aux.pipe.args[i], args[i]) != 0) + !streq(binding->aux.pipe.args[i], args[i])) { BUG("[%s].%s=%s: pipe argv not the expected one: " "mismatch of arg #%zu: expected=\"%s\", got=\"%s\"", @@ -1258,26 +1256,26 @@ test_section_environment(void) ctx.value = "bar"; xassert(parse_section_environment(&ctx)); xassert(tll_length(conf.env_vars) == 1); - xassert(strcmp(tll_front(conf.env_vars).name, "FOO") == 0); - xassert(strcmp(tll_front(conf.env_vars).value, "bar") == 0); + xassert(streq(tll_front(conf.env_vars).name, "FOO")); + xassert(streq(tll_front(conf.env_vars).value, "bar")); /* Add a second variable */ ctx.key = "BAR"; ctx.value = "123"; xassert(parse_section_environment(&ctx)); xassert(tll_length(conf.env_vars) == 2); - xassert(strcmp(tll_back(conf.env_vars).name, "BAR") == 0); - xassert(strcmp(tll_back(conf.env_vars).value, "123") == 0); + xassert(streq(tll_back(conf.env_vars).name, "BAR")); + xassert(streq(tll_back(conf.env_vars).value, "123")); /* Replace the *value* of the first variable */ ctx.key = "FOO"; ctx.value = "456"; xassert(parse_section_environment(&ctx)); xassert(tll_length(conf.env_vars) == 2); - xassert(strcmp(tll_front(conf.env_vars).name, "FOO") == 0); - xassert(strcmp(tll_front(conf.env_vars).value, "456") == 0); - xassert(strcmp(tll_back(conf.env_vars).name, "BAR") == 0); - xassert(strcmp(tll_back(conf.env_vars).value, "123") == 0); + xassert(streq(tll_front(conf.env_vars).name, "FOO")); + xassert(streq(tll_front(conf.env_vars).value, "456")); + xassert(streq(tll_back(conf.env_vars).name, "BAR")); + xassert(streq(tll_back(conf.env_vars).value, "123")); config_free(&conf); } diff --git a/uri.c b/uri.c index 7214a479..4de4bd88 100644 --- a/uri.c +++ b/uri.c @@ -250,7 +250,7 @@ hostname_is_localhost(const char *hostname) this_host[0] = '\0'; return (hostname != NULL && ( - strcmp(hostname, "") == 0 || - strcmp(hostname, "localhost") == 0 || - strcmp(hostname, this_host) == 0)); + streq(hostname, "") || + streq(hostname, "localhost") || + streq(hostname, this_host))); } diff --git a/url-mode.c b/url-mode.c index 76e2869d..1b28f8e0 100644 --- a/url-mode.c +++ b/url-mode.c @@ -685,7 +685,7 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) 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 +704,7 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) 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; diff --git a/util.h b/util.h index 683dbd4a..3746e269 100644 --- a/util.h +++ b/util.h @@ -1,12 +1,20 @@ #pragma once +#include #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 --git a/wayland.c b/wayland.c index 25037c8a..0ef0bdfd 100644 --- a/wayland.c +++ b/wayland.c @@ -1080,7 +1080,7 @@ handle_global(void *data, struct wl_registry *registry, LOG_DBG("global: 0x%08x, interface=%s, version=%u", name, interface, version); struct wayland *wayl = data; - if (strcmp(interface, wl_compositor_interface.name) == 0) { + if (streq(interface, wl_compositor_interface.name)) { const uint32_t required = 4; if (!verify_iface_version(interface, version, required)) return; @@ -1095,7 +1095,7 @@ handle_global(void *data, struct wl_registry *registry, 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; @@ -1104,7 +1104,7 @@ handle_global(void *data, struct wl_registry *registry, wayl->registry, name, &wl_subcompositor_interface, required); } - else if (strcmp(interface, wl_shm_interface.name) == 0) { + else if (streq(interface, wl_shm_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; @@ -1114,7 +1114,7 @@ handle_global(void *data, struct wl_registry *registry, 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; @@ -1139,7 +1139,7 @@ handle_global(void *data, struct wl_registry *registry, xdg_wm_base_add_listener(wayl->shell, &xdg_wm_base_listener, wayl); } - else if (strcmp(interface, zxdg_decoration_manager_v1_interface.name) == 0) { + else if (streq(interface, zxdg_decoration_manager_v1_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; @@ -1148,7 +1148,7 @@ handle_global(void *data, struct wl_registry *registry, wayl->registry, name, &zxdg_decoration_manager_v1_interface, required); } - else if (strcmp(interface, wl_seat_interface.name) == 0) { + else if (streq(interface, wl_seat_interface.name)) { const uint32_t required = 5; if (!verify_iface_version(interface, version, required)) return; @@ -1188,7 +1188,7 @@ handle_global(void *data, struct wl_registry *registry, wl_seat_add_listener(wl_seat, &seat_listener, seat); } - else if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) { + else if (streq(interface, zxdg_output_manager_v1_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; @@ -1205,7 +1205,7 @@ handle_global(void *data, struct wl_registry *registry, } } - else if (strcmp(interface, wl_output_interface.name) == 0) { + else if (streq(interface, wl_output_interface.name)) { const uint32_t required = 2; if (!verify_iface_version(interface, version, required)) return; @@ -1237,7 +1237,7 @@ handle_global(void *data, struct wl_registry *registry, } } - else if (strcmp(interface, wl_data_device_manager_interface.name) == 0) { + else if (streq(interface, wl_data_device_manager_interface.name)) { const uint32_t required = 3; if (!verify_iface_version(interface, version, required)) return; @@ -1249,7 +1249,7 @@ handle_global(void *data, struct wl_registry *registry, seat_add_data_device(&it->item); } - else if (strcmp(interface, zwp_primary_selection_device_manager_v1_interface.name) == 0) { + else if (streq(interface, zwp_primary_selection_device_manager_v1_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; @@ -1262,7 +1262,7 @@ handle_global(void *data, struct wl_registry *registry, seat_add_primary_selection(&it->item); } - else if (strcmp(interface, wp_presentation_interface.name) == 0) { + else if (streq(interface, wp_presentation_interface.name)) { if (wayl->presentation_timings) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) @@ -1275,7 +1275,7 @@ handle_global(void *data, struct wl_registry *registry, } } - 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; @@ -1284,7 +1284,7 @@ handle_global(void *data, struct wl_registry *registry, 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; @@ -1293,7 +1293,7 @@ handle_global(void *data, struct wl_registry *registry, 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; @@ -1303,7 +1303,7 @@ handle_global(void *data, struct wl_registry *registry, &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; @@ -1313,7 +1313,7 @@ handle_global(void *data, struct wl_registry *registry, } #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; From 21a8d832ce12217ddd493cdcaefdc457128f2ada Mon Sep 17 00:00:00 2001 From: "Andrew J. Hesford" Date: Wed, 17 Jan 2024 15:00:14 -0500 Subject: [PATCH 0559/1323] feature: add resize-by-cells option to constrain window sizes... ...to multiples of the cell size, and preserve grid size when changing fonts or display scales in floating windows. --- CHANGELOG.md | 4 +++ config.c | 4 +++ config.h | 3 ++ doc/foot.ini.5.scd | 15 ++++++++ render.c | 87 ++++++++++++++++++++++++++++------------------ render.h | 11 ++++-- terminal.c | 5 +-- wayland.c | 46 +++++++++++++++--------- 8 files changed, 120 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb62594e..14e95557 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,8 @@ * 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. ### Changed @@ -68,6 +70,8 @@ * 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. [1526]: https://codeberg.org/dnkl/foot/issues/1526 [1528]: https://codeberg.org/dnkl/foot/issues/1528 diff --git a/config.c b/config.c index ed021569..ae3255e6 100644 --- a/config.c +++ b/config.c @@ -921,6 +921,9 @@ parse_section_main(struct context *ctx) else if (streq(key, "resize-delay-ms")) return value_to_uint16(ctx, 10, &conf->resize_delay_ms); + else if (streq(key, "resize-by-cells")) + return value_to_bool(ctx, &conf->resize_by_cells); + else if (streq(key, "bold-text-in-bright")) { if (streq(value, "palette-based")) { conf->bold_in_bright.enabled = true; @@ -2990,6 +2993,7 @@ config_load(struct config *conf, const char *conf_path, }, .pad_x = 0, .pad_y = 0, + .resize_by_cells = true, .resize_delay_ms = 100, .bold_in_bright = { .enabled = false, diff --git a/config.h b/config.h index 3c5b3df7..cc4094c4 100644 --- a/config.h +++ b/config.h @@ -128,6 +128,9 @@ struct config { unsigned pad_x; unsigned pad_y; bool center; + + bool resize_by_cells; + uint16_t resize_delay_ms; struct { diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index e276d186..1c87db53 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -252,6 +252,21 @@ empty string to be set, but it must be quoted: *KEY=""*) 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 diff --git a/render.c b/render.c index fcd21472..5d2ec6bc 100644 --- a/render.c +++ b/render.c @@ -3912,9 +3912,25 @@ send_dimensions_to_client(struct terminal *term) } } +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 +3941,29 @@ maybe_resize(struct terminal *term, int width, int height, bool force) 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 +3983,8 @@ maybe_resize(struct terminal *term, int width, int height, bool force) 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 +4007,25 @@ maybe_resize(struct terminal *term, int width, int height, bool force) 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); @@ -4225,10 +4259,7 @@ damage_view: /* Signal TIOCSWINSZ */ send_dimensions_to_client(term); - if (!term->window->is_maximized && - !term->window->is_fullscreen && - !term->window->is_tiled) - { + if (is_floating) { /* Stash current size, to enable us to restore it when we're * being un-maximized/fullscreened/tiled */ term->stashed_width = term->width; @@ -4291,18 +4322,6 @@ damage_view: return true; } -bool -render_resize(struct terminal *term, int width, int height) -{ - return maybe_resize(term, width, height, false); -} - -bool -render_resize_force(struct terminal *term, int width, int height) -{ - return maybe_resize(term, width, height, true); -} - static void xcursor_callback( void *data, struct wl_callback *wl_callback, uint32_t callback_data); static const struct wl_callback_listener xcursor_listener = { diff --git a/render.h b/render.h index f038ffb0..78ebae40 100644 --- a/render.h +++ b/render.h @@ -10,8 +10,15 @@ struct renderer; struct renderer *render_init(struct fdm *fdm, struct wayland *wayl); void render_destroy(struct renderer *renderer); -bool render_resize(struct terminal *term, int width, int height); -bool render_resize_force(struct terminal *term, int width, int height); +enum resize_options { + RESIZE_NORMAL = 0, + RESIZE_FORCE = 1 << 0, + RESIZE_BY_CELLS = 1 << 1, + RESIZE_KEEP_GRID = 1 << 2, +}; + +bool render_resize( + struct terminal *term, int width, int height, uint8_t resize_options); void render_refresh(struct terminal *term); void render_refresh_csd(struct terminal *term); diff --git a/terminal.c b/terminal.c index 787eaf2d..b0120c6a 100644 --- a/terminal.c +++ b/terminal.c @@ -784,10 +784,11 @@ term_set_fonts(struct terminal *term, struct fcft_font *fonts[static 4], * 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; } diff --git a/wayland.c b/wayland.c index 0ef0bdfd..271f950e 100644 --- a/wayland.c +++ b/wayland.c @@ -392,8 +392,6 @@ static void 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; /* Note: order matters! term_update_scale() must come first */ bool scale_updated = term_update_scale(term); @@ -402,24 +400,37 @@ update_term_for_output_change(struct terminal *term) csd_reload_font(term->window, old_scale); + uint8_t resize_opts = RESIZE_KEEP_GRID; + if (fonts_updated) { /* * If the fonts have been updated, the cell dimensions have * changed. This requires a “forced” resize, since the surface * buffer dimensions may not have been updated (in which case - * render_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)); + resize_opts |= RESIZE_FORCE; + } else if (!scale_updated) { + /* No need to resize if neither scale nor fonts have changed */ + return; + } else if (term->conf->dpi_aware) { + /* + * If fonts are sized according to DPI, it is possible for the cell + * size to remain the same when display scale changes. This will not + * change the surface buffer dimensions, but will change the logical + * size of the window. To ensure that the compositor is made aware of + * the proper logical size, force a resize rather than allowing + * render_resize() to shortcut the notification if the buffer + * dimensions remain the same. + */ + resize_opts |= RESIZE_FORCE; } - else if (scale_updated) { - /* - * 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)); - } + render_resize( + term, + (int)roundf(term->width / term->scale), + (int)roundf(term->height / term->scale), + resize_opts); } static void @@ -976,6 +987,8 @@ xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, xdg_surface_ack_configure(xdg_surface, serial); + enum resize_options opts = RESIZE_BY_CELLS; + #if 1 /* * TODO: decide if we should do the last “forced” call when ending @@ -989,13 +1002,12 @@ xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, * Note: if we also disable content centering while resizing, then * the last, forced, resize *is* necessary. */ - bool resized = was_resizing && !win->is_resizing - ? render_resize_force(term, new_width, new_height) - : render_resize(term, new_width, new_height); -#else - bool resized = render_resize(term, new_width, new_height); + if (was_resizing && !win->is_resizing) + opts |= RESIZE_FORCE; #endif + bool resized = render_resize(term, new_width, new_height, opts); + if (win->configure.is_activated) term_visual_focus_in(term); else From 4730ff8d080e82fad0554dfcb957dfc82f6568b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 6 May 2023 11:39:38 +0200 Subject: [PATCH 0560/1323] input/config: support *all* modifier names That is, allow custom modifiers (i.e. other than ctrl/shift/alt etc) in key bindings. This is done by no longer validating/translating modifier names to booleans for a pre-configured set of modifiers (ctrl, shift, alt, super). Instead, we keep the modifier *names* in a list, in the key binding struct. When a keymap is loaded, and we "convert" the key binding, _then_ we do modifier translation. For invalid modifier names, we print an error, and then ignore it. I.e. we no longer fail to load a config due to invalid modifier names. We also need to update how we determine the set of significant modifiers. Any modifier not in this list will be ignored when matching key bindings. Before this patch, we hardcoded this to shift/alt/ctrl/super. Now, to handle custom modifiers as well, we simply treat *all* modifiers defined by the current layout as significant. Typically, the only unwanted modifiers are "locked" modifiers. We are already filtering these out. --- config.c | 363 +++++++++++++++++++++++++------------------- config.h | 13 +- input.c | 33 ++-- key-binding.c | 24 ++- tests/test-config.c | 40 +++-- wayland.h | 5 +- 6 files changed, 291 insertions(+), 187 deletions(-) diff --git a/config.c b/config.c index ae3255e6..e3d531c2 100644 --- a/config.c +++ b/config.c @@ -1544,6 +1544,7 @@ static void free_key_binding(struct config_key_binding *binding) { free_binding_aux(&binding->aux); + tll_free_and_free(binding->modifiers, free); } static void NOINLINE @@ -1559,43 +1560,26 @@ free_key_binding_list(struct config_key_binding_list *bindings) bindings->count = 0; } -static bool NOINLINE -parse_modifiers(struct context *ctx, const char *text, size_t len, - struct config_key_modifiers *modifiers) +static void NOINLINE +parse_modifiers(const char *text, size_t len, config_modifier_list_t *modifiers) { - bool ret = false; - - *modifiers = (struct config_key_modifiers){0}; + tll_free_and_free(*modifiers, free); /* Handle "none" separately because e.g. none+shift is nonsense */ if (strncmp(text, "none", len) == 0) - return true; + return; char *copy = xstrndup(text, len); - for (char *tok_ctx = NULL, *key = strtok_r(copy, "+", &tok_ctx); + for (char *ctx = NULL, *key = strtok_r(copy, "+", &ctx); key != NULL; - key = strtok_r(NULL, "+", &tok_ctx)) + key = strtok_r(NULL, "+", &ctx)) { - if (streq(key, XKB_MOD_NAME_SHIFT)) - modifiers->shift = true; - else if (streq(key, XKB_MOD_NAME_CTRL)) - modifiers->ctrl = true; - else if (streq(key, XKB_MOD_NAME_ALT)) - modifiers->alt = true; - else if (streq(key, XKB_MOD_NAME_LOGO)) - 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 @@ -1731,6 +1715,7 @@ value_to_key_combos(struct context *ctx, int action, /* Count number of combinations */ size_t combo_count = 1; + size_t used_combos = 0; /* For error handling */ for (const char *p = strchr(ctx->value, ' '); p != NULL; p = strchr(p + 1, ' ')) @@ -1746,7 +1731,7 @@ value_to_key_combos(struct context *ctx, int action, for (char *tok_ctx = NULL, *combo = strtok_r(copy, " ", &tok_ctx); combo != NULL; combo = strtok_r(NULL, " ", &tok_ctx), - idx++) + idx++, used_combos++) { struct config_key_binding *new_combo = &new_combos[idx]; new_combo->action = action; @@ -1757,6 +1742,7 @@ value_to_key_combos(struct context *ctx, int action, new_combo->aux.master_copy = idx == 0; new_combo->aux.pipe = *argv; #endif + memset(&new_combo->modifiers, 0, sizeof(new_combo->modifiers)); new_combo->path = ctx->path; new_combo->lineno = ctx->lineno; @@ -1765,11 +1751,9 @@ value_to_key_combos(struct context *ctx, int action, if (key == NULL) { /* No modifiers */ key = combo; - new_combo->modifiers = (struct config_key_modifiers){0}; } else { *key = '\0'; - if (!parse_modifiers(ctx, combo, key - combo, &new_combo->modifiers)) - goto err; + parse_modifiers(combo, key - combo, &new_combo->modifiers); key++; /* Skip past the '+' */ } @@ -1779,6 +1763,7 @@ value_to_key_combos(struct context *ctx, int action, new_combo->k.sym = xkb_keysym_from_name(key, 0); if (new_combo->k.sym == XKB_KEY_NoSymbol) { LOG_CONTEXTUAL_ERR("not a valid XKB key name: %s", key); + free_key_binding(new_combo); goto err; } break; @@ -1799,6 +1784,7 @@ value_to_key_combos(struct context *ctx, int action, LOG_CONTEXTUAL_ERRNO("invalid click count: %s", _count); else LOG_CONTEXTUAL_ERR("invalid click count: %s", _count); + free_key_binding(new_combo); goto err; } @@ -1808,6 +1794,7 @@ value_to_key_combos(struct context *ctx, int action, new_combo->m.button = mouse_button_name_to_code(key); if (new_combo->m.button < 0) { LOG_CONTEXTUAL_ERR("invalid mouse button name: %s", key); + free_key_binding(new_combo); goto err; } @@ -1838,41 +1825,89 @@ value_to_key_combos(struct context *ctx, int action, return true; err: + for (size_t i = 0; i < used_combos; i++) + free_key_binding(&new_combos[i]); free(copy); return false; } static bool -modifiers_equal(const struct config_key_modifiers *mods1, - const struct config_key_modifiers *mods2) +modifiers_equal(const config_modifier_list_t *mods1, + const config_modifier_list_t *mods2) { - bool shift = mods1->shift == mods2->shift; - bool alt = mods1->alt == mods2->alt; - bool ctrl = mods1->ctrl == mods2->ctrl; - bool super = mods1->super == mods2->super; - return shift && alt && ctrl && super; + if (tll_length(*mods1) != tll_length(*mods2)) + return false; + + size_t count = 0; + tll_foreach(*mods1, it1) { + size_t skip = count; + tll_foreach(*mods2, it2) { + if (skip > 0) { + skip--; + continue; + } + + if (strcmp(it1->item, it2->item) != 0) + return false; + break; + } + + count++; + } + + return true; + /* + * bool shift = mods1->shift == mods2->shift; + * bool alt = mods1->alt == mods2->alt; + * bool ctrl = mods1->ctrl == mods2->ctrl; + * bool super = mods1->super == mods2->super; + * return shift && alt && ctrl && super; + */ +} + +UNITTEST +{ + config_modifier_list_t mods1 = tll_init(); + config_modifier_list_t mods2 = tll_init(); + + tll_push_back(mods1, xstrdup("foo")); + tll_push_back(mods1, xstrdup("bar")); + + tll_push_back(mods2, xstrdup("foo")); + xassert(!modifiers_equal(&mods1, &mods2)); + + tll_push_back(mods2, xstrdup("zoo")); + xassert(!modifiers_equal(&mods1, &mods2)); + + free(tll_pop_back(mods2)); + tll_push_back(mods2, xstrdup("bar")); + xassert(modifiers_equal(&mods1, &mods2)); + + tll_free_and_free(mods1, free); + tll_free_and_free(mods2, free); } static bool -modifiers_disjoint(const struct config_key_modifiers *mods1, - const struct config_key_modifiers *mods2) +modifiers_disjoint(const config_modifier_list_t *mods1, + const config_modifier_list_t *mods2) { - bool shift = mods1->shift && mods2->shift; - bool alt = mods1->alt && mods2->alt; - bool ctrl = mods1->ctrl && mods2->ctrl; - bool super = mods1->super && mods2->super; - return !(shift || alt || ctrl || super); + return !modifiers_equal(mods1, mods2); } static char * NOINLINE -modifiers_to_str(const struct config_key_modifiers *mods) +modifiers_to_str(const config_modifier_list_t *mods) { - 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; } @@ -2014,10 +2049,13 @@ UNITTEST xassert(bindings.arr[0].action == TEST_ACTION_FOO); xassert(bindings.arr[1].action == TEST_ACTION_BAR); xassert(bindings.arr[1].k.sym == XKB_KEY_g); - xassert(bindings.arr[1].modifiers.ctrl); + xassert(tll_length(bindings.arr[1].modifiers) == 1); + xassert(strcmp(tll_front(bindings.arr[1].modifiers), XKB_MOD_NAME_CTRL) == 0); xassert(bindings.arr[2].action == TEST_ACTION_BAR); xassert(bindings.arr[2].k.sym == XKB_KEY_x); - xassert(bindings.arr[2].modifiers.ctrl && bindings.arr[2].modifiers.shift); + xassert(tll_length(bindings.arr[2].modifiers) == 2); + xassert(strcmp(tll_front(bindings.arr[2].modifiers), XKB_MOD_NAME_CTRL) == 0); + xassert(strcmp(tll_back(bindings.arr[2].modifiers), XKB_MOD_NAME_SHIFT) == 0); /* * REPLACE foo with foo=Mod+v Shift+q @@ -2033,10 +2071,12 @@ UNITTEST xassert(bindings.arr[1].action == TEST_ACTION_BAR); xassert(bindings.arr[2].action == TEST_ACTION_FOO); xassert(bindings.arr[2].k.sym == XKB_KEY_v); - xassert(bindings.arr[2].modifiers.alt); + xassert(tll_length(bindings.arr[2].modifiers) == 1); + xassert(strcmp(tll_front(bindings.arr[2].modifiers), XKB_MOD_NAME_ALT) == 0); xassert(bindings.arr[3].action == TEST_ACTION_FOO); xassert(bindings.arr[3].k.sym == XKB_KEY_q); - xassert(bindings.arr[3].modifiers.shift); + xassert(tll_length(bindings.arr[3].modifiers) == 1); + xassert(strcmp(tll_front(bindings.arr[3].modifiers), XKB_MOD_NAME_SHIFT) == 0); /* * REMOVE bar @@ -2103,7 +2143,7 @@ resolve_key_binding_collisions(struct config *conf, const char *section_name, struct config_key_binding *binding1 = &bindings->arr[i]; xassert(binding1->action != BIND_ACTION_NONE); - const struct config_key_modifiers *mods1 = &binding1->modifiers; + const config_modifier_list_t *mods1 = &binding1->modifiers; /* Does our modifiers collide with the selection override mods? */ if (type == MOUSE_BINDING && @@ -2127,7 +2167,7 @@ resolve_key_binding_collisions(struct config *conf, const char *section_name, continue; } - const struct config_key_modifiers *mods2 = &binding2->modifiers; + const config_modifier_list_t *mods2 = &binding2->modifiers; bool mods_equal = modifiers_equal(mods1, mods2); bool sym_equal; @@ -2252,13 +2292,9 @@ parse_section_mouse_bindings(struct context *ctx) const char *value = ctx->value; if (streq(key, "selection-override-modifiers")) { - 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; - } + parse_modifiers( + ctx->value, strlen(value), + &conf->mouse.selection_override_modifiers); return true; } @@ -2830,37 +2866,38 @@ get_server_socket_path(void) 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); @@ -2872,46 +2909,47 @@ add_default_key_bindings(struct config *conf) 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); @@ -2922,12 +2960,12 @@ add_default_search_bindings(struct config *conf) 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); @@ -2938,18 +2976,18 @@ add_default_url_bindings(struct config *conf) 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}}}, }; conf->bindings.mouse.count = ALEN(bindings); @@ -3064,12 +3102,7 @@ config_load(struct config *conf, const char *conf_path, .mouse = { .hide_when_typing = false, .alternate_scroll_mode = true, - .selection_override_modifiers = { - .shift = true, - .alt = false, - .ctrl = false, - .super = false, - }, + .selection_override_modifiers = tll_init(), }, .csd = { .preferred = CONF_CSD_PREFER_SERVER, @@ -3125,6 +3158,7 @@ config_load(struct config *conf, const char *conf_path, }; 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); @@ -3324,6 +3358,9 @@ key_binding_list_clone(struct config_key_binding_list *dst, struct config_key_binding *new = &dst->arr[i]; *new = *old; + memset(&new->modifiers, 0, sizeof(new->modifiers)); + tll_foreach(old->modifiers, it) + tll_push_back(new->modifiers, xstrdup(it->item)); switch (old->aux.type) { case BINDING_AUX_NONE: @@ -3399,6 +3436,11 @@ config_clone(const struct config *old) 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), @@ -3431,13 +3473,13 @@ UNITTEST 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(); @@ -3473,6 +3515,7 @@ config_free(struct config *conf) 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); @@ -3603,6 +3646,7 @@ check_if_font_is_monospaced(const char *pattern, return is_monospaced; } +#if 0 xkb_mod_mask_t conf_modifiers_to_mask(const struct seat *seat, const struct config_key_modifiers *modifiers) @@ -3618,3 +3662,4 @@ conf_modifiers_to_mask(const struct seat *seat, mods |= modifiers->super << seat->kbd.mod_super; return mods; } +#endif diff --git a/config.h b/config.h index cc4094c4..39f169f9 100644 --- a/config.h +++ b/config.h @@ -38,12 +38,14 @@ struct config_font { }; DEFINE_LIST(struct config_font); +#if 0 struct config_key_modifiers { bool shift; bool alt; bool ctrl; bool super; }; +#endif struct argv { char **args; @@ -74,9 +76,12 @@ enum key_binding_type { MOUSE_BINDING, }; +typedef tll(char *) config_modifier_list_t; + struct config_key_binding { int action; /* One of the varios bind_action_* enums from wayland.h */ - struct config_key_modifiers modifiers; + //struct config_key_modifiers modifiers; + config_modifier_list_t modifiers; union { /* Key bindings */ struct { @@ -263,7 +268,8 @@ struct config { struct { bool hide_when_typing; bool alternate_scroll_mode; - struct config_key_modifiers selection_override_modifiers; + //struct config_key_modifiers selection_override_modifiers; + config_modifier_list_t selection_override_modifiers; } mouse; struct { @@ -375,10 +381,11 @@ struct config *config_clone(const struct config *old); bool config_font_parse(const char *pattern, struct config_font *font); void config_font_list_destroy(struct config_font_list *font_list); +#if 0 struct seat; xkb_mod_mask_t conf_modifiers_to_mask( const struct seat *seat, const struct config_key_modifiers *modifiers); - +#endif bool check_if_font_is_monospaced( const char *pattern, user_notifications_t *notifications); diff --git a/input.c b/input.c index 25addf9b..97c95a92 100644 --- a/input.c +++ b/input.c @@ -579,22 +579,33 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, seat->kbd.mod_caps = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_CAPS); seat->kbd.mod_num = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_NUM); - seat->kbd.bind_significant = 0; + /* Significant modifiers in the legacy keyboard protocol */ + seat->kbd.legacy_significant = 0; if (seat->kbd.mod_shift != XKB_MOD_INVALID) - seat->kbd.bind_significant |= 1 << seat->kbd.mod_shift; + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_shift; if (seat->kbd.mod_alt != XKB_MOD_INVALID) - seat->kbd.bind_significant |= 1 << seat->kbd.mod_alt; + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_alt; if (seat->kbd.mod_ctrl != XKB_MOD_INVALID) - seat->kbd.bind_significant |= 1 << seat->kbd.mod_ctrl; + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_ctrl; if (seat->kbd.mod_super != XKB_MOD_INVALID) - seat->kbd.bind_significant |= 1 << seat->kbd.mod_super; + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_super; - seat->kbd.kitty_significant = seat->kbd.bind_significant; + /* Significant modifiers in the kitty keyboard protocol */ + seat->kbd.kitty_significant = seat->kbd.legacy_significant; if (seat->kbd.mod_caps != XKB_MOD_INVALID) seat->kbd.kitty_significant |= 1 << seat->kbd.mod_caps; if (seat->kbd.mod_num != XKB_MOD_INVALID) seat->kbd.kitty_significant |= 1 << seat->kbd.mod_num; + /* Significant modifiers when handling shortcuts - use all available */ + seat->kbd.bind_significant = 0; + const xkb_mod_index_t mod_count = xkb_keymap_num_mods(seat->kbd.xkb_keymap); + for (xkb_mod_index_t i = 0; i < mod_count; i++) { + LOG_DBG("significant modifier: %s", + xkb_keymap_mod_get_name(seat->kbd.xkb_keymap, i)); + seat->kbd.bind_significant |= 1 << i; + } + seat->kbd.key_arrow_up = xkb_keymap_key_by_name(seat->kbd.xkb_keymap, "UP"); seat->kbd.key_arrow_down = xkb_keymap_key_by_name(seat->kbd.xkb_keymap, "DOWN"); } @@ -985,7 +996,7 @@ legacy_kbd_protocol(struct seat *seat, struct terminal *term, /* 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 { @@ -1527,7 +1538,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, #if 0 for (size_t i = 0; i < 32; i++) { - if (mods & (1 << i)) { + if (mods & (1u << i)) { LOG_INFO("%s", xkb_keymap_mod_get_name(seat->kbd.xkb_keymap, i)); } } @@ -1555,6 +1566,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, bind->mods == (bind_mods & ~bind_consumed) && execute_binding(seat, term, bind, serial, 1)) { + LOG_WARN("matched translated symbol"); goto maybe_repeat; } @@ -1566,6 +1578,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, if (bind->k.sym == raw_syms[i] && execute_binding(seat, term, bind, serial, 1)) { + LOG_WARN("matched untranslated symbol"); goto maybe_repeat; } } @@ -1575,6 +1588,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, if (code->item == key && execute_binding(seat, term, bind, serial, 1)) { + LOG_WARN("matched raw key code"); goto maybe_repeat; } } @@ -2293,8 +2307,7 @@ static const struct key_binding * 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; } diff --git a/key-binding.c b/key-binding.c index 1dffd3ee..a7738da8 100644 --- a/key-binding.c +++ b/key-binding.c @@ -404,6 +404,24 @@ sort_binding_list(key_binding_list_t *list) tll_sort(*list, key_cmp); } +static xkb_mod_mask_t +mods_to_mask(const struct seat *seat, const config_modifier_list_t *mods) +{ + xkb_mod_mask_t mask = 0; + tll_foreach(*mods, it) { + xkb_mod_index_t idx = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, it->item); + + if (idx == XKB_MOD_INVALID) { + LOG_ERR("%s: invalid modifier name", it->item); + continue; + } + + mask |= 1 << idx; + } + + return mask; +} + static void NOINLINE convert_key_binding(struct key_set *set, const struct config_key_binding *conf_binding, @@ -411,7 +429,7 @@ convert_key_binding(struct key_set *set, { const struct seat *seat = set->seat; - xkb_mod_mask_t mods = conf_modifiers_to_mask(seat, &conf_binding->modifiers); + xkb_mod_mask_t mods = mods_to_mask(seat, &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 @@ convert_mouse_binding(struct key_set *set, .type = MOUSE_BINDING, .action = conf_binding->action, .aux = &conf_binding->aux, - .mods = conf_modifiers_to_mask(set->seat, &conf_binding->modifiers), + .mods = mods_to_mask(set->seat, &conf_binding->modifiers), .m = { .button = conf_binding->m.button, .count = conf_binding->m.count, @@ -509,7 +527,7 @@ load_keymap(struct key_set *set) 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 --git a/tests/test-config.c b/tests/test-config.c index 182dd4f6..b54dd23e 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -787,6 +787,17 @@ test_section_csd(void) config_free(&conf); } +static bool +have_modifier(const config_modifier_list_t *mods, const char *mod) +{ + tll_foreach(*mods, it) { + if (strcmp(it->item, mod) == 0) + return true; + } + + return false; +} + static void test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), int action, int max_action, const char *const *map, @@ -904,17 +915,19 @@ test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), ctx->section, ctx->key, ctx->value, binding->action, action); } - if (binding->modifiers.ctrl != ctrl || - binding->modifiers.alt != alt || - binding->modifiers.shift != shift || - binding->modifiers.super != super) + bool have_ctrl = have_modifier(&binding->modifiers, XKB_MOD_NAME_CTRL); + bool have_alt = have_modifier(&binding->modifiers, XKB_MOD_NAME_ALT); + bool have_shift = have_modifier(&binding->modifiers, XKB_MOD_NAME_SHIFT); + bool have_super = have_modifier(&binding->modifiers, XKB_MOD_NAME_LOGO); + + if (have_ctrl != ctrl || have_alt != alt || + have_shift != shift || have_super != super) { BUG("[%s].%s=%s: modifier mismatch:\n" " have: ctrl=%d, alt=%d, shift=%d, super=%d\n" " expected: ctrl=%d, alt=%d, shift=%d, super=%d", ctx->section, ctx->key, ctx->value, - binding->modifiers.ctrl, binding->modifiers.alt, - binding->modifiers.shift, binding->modifiers.super, + have_ctrl, have_alt, have_shift, have_super, ctrl, alt, shift, super); } @@ -970,14 +983,17 @@ _test_binding_collisions(struct context *ctx, bindings.arr[0] = (struct config_key_binding){ .action = (test_mode == FAIL_DIFFERENT_ACTION ? max_action - 1 : max_action), - .modifiers = {.ctrl = true}, + .modifiers = tll_init(), .path = "unittest", }; + tll_push_back(bindings.arr[0].modifiers, xstrdup(XKB_MOD_NAME_CTRL)); + bindings.arr[1] = (struct config_key_binding){ .action = max_action, - .modifiers = {.ctrl = true}, + .modifiers = tll_init(), .path = "unittest", }; + tll_push_back(bindings.arr[1].modifiers, xstrdup(XKB_MOD_NAME_CTRL)); switch (type) { case KEY_BINDING: @@ -998,7 +1014,8 @@ _test_binding_collisions(struct context *ctx, break; case FAIL_MOUSE_OVERRIDE: - ctx->conf->mouse.selection_override_modifiers.ctrl = true; + tll_free_and_free(ctx->conf->mouse.selection_override_modifiers, free); + tll_push_back(ctx->conf->mouse.selection_override_modifiers, xstrdup(XKB_MOD_NAME_CTRL)); break; case FAIL_DIFFERENT_ARGV: @@ -1237,10 +1254,13 @@ test_section_text_bindings(void) ctx.key = "\\y"; xassert(!parse_section_text_bindings(&ctx)); +#if 0 + /* Invalid modifier and key names are detected later, when a + * layout is applied */ ctx.key = "abcd"; ctx.value = "InvalidMod+y"; xassert(!parse_section_text_bindings(&ctx)); - +#endif config_free(&conf); } diff --git a/wayland.h b/wayland.h index 145e480d..3551dd60 100644 --- a/wayland.h +++ b/wayland.h @@ -128,8 +128,9 @@ struct seat { xkb_mod_index_t mod_caps; xkb_mod_index_t mod_num; - xkb_mod_mask_t bind_significant; - xkb_mod_mask_t kitty_significant; + xkb_mod_mask_t bind_significant; /* Significant modifiers for shortcut handling */ + 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; From 0aefc2c65de8cca39ddbf9d8c12f1a4d4206b42a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 6 Feb 2024 10:41:01 +0100 Subject: [PATCH 0561/1323] input: remove the concept of "significant" modifiers For the purpose of matching key bindings, "significant" modifiers are no more. We're really only interested in filtering out "locked" modifiers. We're already doing this, so there's no need to *also* match against a set of "significant" modifiers. Furthermore, we *never* want to consider locked keys (e.g. when emitting escapes to the client application), thus we can filter those out already when retrieving the set of active modifiers. The exception is the kitty keyboard protocol, which has support for CapsLock and NumLock. Since we're already re-retrieving the "consumed" modifiers (using the GTK style, rather than normal "XKB" style, to better match the kitty terminal), we might as well re-retrieve the effective modifiers as well. --- input.c | 52 +++++++++++++++++++++------------------------------- input.h | 2 +- search.c | 9 ++------- search.h | 1 - terminal.c | 2 +- url-mode.c | 12 +++--------- url-mode.h | 1 - wayland.h | 1 - 8 files changed, 28 insertions(+), 52 deletions(-) diff --git a/input.c b/input.c index 97c95a92..dc0eec93 100644 --- a/input.c +++ b/input.c @@ -597,15 +597,6 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, if (seat->kbd.mod_num != XKB_MOD_INVALID) seat->kbd.kitty_significant |= 1 << seat->kbd.mod_num; - /* Significant modifiers when handling shortcuts - use all available */ - seat->kbd.bind_significant = 0; - const xkb_mod_index_t mod_count = xkb_keymap_num_mods(seat->kbd.xkb_keymap); - for (xkb_mod_index_t i = 0; i < mod_count; i++) { - LOG_DBG("significant modifier: %s", - xkb_keymap_mod_get_name(seat->kbd.xkb_keymap, i)); - seat->kbd.bind_significant |= 1 << i; - } - seat->kbd.key_arrow_up = xkb_keymap_key_by_name(seat->kbd.xkb_keymap, "UP"); seat->kbd.key_arrow_down = xkb_keymap_key_by_name(seat->kbd.xkb_keymap, "DOWN"); } @@ -887,7 +878,8 @@ UNITTEST 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) @@ -897,24 +889,27 @@ get_current_modifiers(const struct seat *seat, } else { + const xkb_mod_mask_t locked = + xkb_state_serialize_mods(seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); + if (effective != NULL) { *effective = xkb_state_serialize_mods( seat->kbd.xkb_state, XKB_STATE_MODS_EFFECTIVE); + + if (filter_locked) + *effective &= ~locked; } if (consumed != NULL) { *consumed = xkb_state_key_get_consumed_mods2( seat->kbd.xkb_state, key, XKB_CONSUMED_MODE_XKB); + + if (filter_locked) + *consumed &= ~locked; } } } -static xkb_mod_mask_t -get_locked_modifiers(const struct seat *seat) -{ - return xkb_state_serialize_mods(seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); -} - struct kbd_ctx { xkb_layout_index_t layout; xkb_keycode_t key; @@ -1184,7 +1179,7 @@ kitty_kbd_protocol(struct seat *seat, struct terminal *term, xkb_state_update_key( seat->kbd.xkb_state, ctx->key, pressed ? XKB_KEY_DOWN : XKB_KEY_UP); - get_current_modifiers(seat, &mods, NULL, ctx->key); + get_current_modifiers(seat, &mods, NULL, ctx->key, false); consumed = xkb_state_key_get_consumed_mods2( seat->kbd.xkb_state, ctx->key, XKB_CONSUMED_MODE_GTK); @@ -1201,7 +1196,9 @@ kitty_kbd_protocol(struct seat *seat, struct terminal *term, seat->kbd.xkb_state, ctx->key, pressed ? XKB_KEY_UP : XKB_KEY_DOWN); #endif } else { - mods = ctx->mods; + /* Same as ctx->mods, but without locked modifiers being + filtered out */ + get_current_modifiers(seat, &mods, NULL, ctx->key, false); /* Re-retrieve the consumed modifiers using the GTK mode, to better match kitty. */ @@ -1490,13 +1487,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, const bool composed = compose_status == XKB_COMPOSE_COMPOSED; xkb_mod_mask_t mods, consumed; - get_current_modifiers(seat, &mods, &consumed, key); - - const xkb_mod_mask_t locked = get_locked_modifiers(seat); - const xkb_mod_mask_t bind_mods - = mods & seat->kbd.bind_significant & ~locked; - const xkb_mod_mask_t bind_consumed = - consumed & seat->kbd.bind_significant & ~locked; + get_current_modifiers(seat, &mods, &consumed, key, true); xkb_layout_index_t layout_idx = xkb_state_key_get_layout(seat->kbd.xkb_state, key); @@ -1520,7 +1511,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, start_repeater(seat, key); search_input( - seat, term, bindings, key, sym, mods, consumed, locked, + seat, term, bindings, key, sym, mods, consumed, raw_syms, raw_count, serial); return; } @@ -1530,7 +1521,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, start_repeater(seat, key); urls_input( - seat, term, bindings, key, sym, mods, consumed, locked, + seat, term, bindings, key, sym, mods, consumed, raw_syms, raw_count, serial); return; } @@ -1563,14 +1554,14 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, /* 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)) { LOG_WARN("matched translated symbol"); goto maybe_repeat; } - if (bind->mods != bind_mods || bind_mods != (mods & ~locked)) + if (bind->mods != mods) continue; /* Match untranslated symbols */ @@ -2253,8 +2244,7 @@ static const struct key_binding * 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 */ diff --git a/input.h b/input.h index 906008d5..001d116f 100644 --- a/input.h +++ b/input.h @@ -33,6 +33,6 @@ void input_repeat(struct seat *seat, uint32_t key); void get_current_modifiers(const struct seat *seat, xkb_mod_mask_t *effective, xkb_mod_mask_t *consumed, - uint32_t key); + uint32_t key, bool filter_locked); enum cursor_shape xcursor_for_csd_border(struct terminal *term, int x, int y); diff --git a/search.c b/search.c index 55388577..10541884 100644 --- a/search.c +++ b/search.c @@ -1374,17 +1374,12 @@ void search_input(struct seat *seat, struct terminal *term, const struct key_binding_set *bindings, uint32_t key, xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed, - xkb_mod_mask_t locked, const xkb_keysym_t *raw_syms, size_t raw_count, uint32_t serial) { LOG_DBG("search: input: sym=%d/0x%x, mods=0x%08x, consumed=0x%08x", sym, sym, mods, consumed); - const xkb_mod_mask_t bind_mods = - mods & seat->kbd.bind_significant & ~locked; - const xkb_mod_mask_t bind_consumed = - consumed & seat->kbd.bind_significant & ~locked; enum xkb_compose_status compose_status = seat->kbd.xkb_compose_state != NULL ? xkb_compose_state_get_status(seat->kbd.xkb_compose_state) : XKB_COMPOSE_NOTHING; @@ -1399,7 +1394,7 @@ search_input(struct seat *seat, struct terminal *term, /* 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 @@ search_input(struct seat *seat, struct terminal *term, return; } - if (bind->mods != bind_mods || bind_mods != (mods & ~locked)) + if (bind->mods != mods) continue; /* Match untranslated symbols */ diff --git a/search.h b/search.h index d5d4162b..ee8ecd76 100644 --- a/search.h +++ b/search.h @@ -11,7 +11,6 @@ void search_input( struct seat *seat, struct terminal *term, const struct key_binding_set *bindings, uint32_t key, xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed, - xkb_mod_mask_t locked, const xkb_keysym_t *raw_syms, size_t raw_count, uint32_t serial); void search_add_chars(struct terminal *term, const char *text, size_t len); diff --git a/terminal.c b/terminal.c index b0120c6a..f63aaaa7 100644 --- a/terminal.c +++ b/terminal.c @@ -3039,7 +3039,7 @@ term_mouse_grabbed(const struct terminal *term, const struct seat *seat) */ xkb_mod_mask_t mods; - get_current_modifiers(seat, &mods, NULL, 0); + get_current_modifiers(seat, &mods, NULL, 0, true); const struct key_binding_set *bindings = key_binding_for(term->wl->key_binding_manager, term->conf, seat); diff --git a/url-mode.c b/url-mode.c index 1b28f8e0..07499794 100644 --- a/url-mode.c +++ b/url-mode.c @@ -145,28 +145,22 @@ void urls_input(struct seat *seat, struct terminal *term, const struct key_binding_set *bindings, uint32_t key, xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed, - xkb_mod_mask_t locked, const xkb_keysym_t *raw_syms, size_t raw_count, uint32_t serial) { - const xkb_mod_mask_t bind_mods = - mods & seat->kbd.bind_significant & ~locked; - const xkb_mod_mask_t bind_consumed = - consumed & seat->kbd.bind_significant & ~locked; - /* Key bindings */ 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,7 +190,7 @@ urls_input(struct seat *seat, struct terminal *term, return; } - if (mods & ~consumed & ~locked) + if (mods & ~consumed) return; char32_t wc = xkb_state_key_get_utf32(seat->kbd.xkb_state, key); diff --git a/url-mode.h b/url-mode.h index abfcb57b..eefe07c0 100644 --- a/url-mode.h +++ b/url-mode.h @@ -23,6 +23,5 @@ void urls_reset(struct terminal *term); void urls_input(struct seat *seat, struct terminal *term, const struct key_binding_set *bindings, uint32_t key, xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed, - xkb_mod_mask_t locked, const xkb_keysym_t *raw_syms, size_t raw_count, uint32_t serial); diff --git a/wayland.h b/wayland.h index 3551dd60..733ebd3f 100644 --- a/wayland.h +++ b/wayland.h @@ -128,7 +128,6 @@ struct seat { xkb_mod_index_t mod_caps; xkb_mod_index_t mod_num; - xkb_mod_mask_t bind_significant; /* Significant modifiers for shortcut handling */ 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 */ From 1685f38ee65972f3768896445afb53d0a6886689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 6 Feb 2024 11:10:36 +0100 Subject: [PATCH 0562/1323] changelog: custom modifiers in key bindings --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14e95557..2233fd9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,10 @@ 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]). + +[1348]: https://codeberg.org/dnkl/foot/issues/1348 ### Changed From f8e875a7cddaac91818b98993d17a96d09a5b4a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 8 Dec 2022 10:35:30 +0100 Subject: [PATCH 0563/1323] term: move row->prompt_marker into new struct, row->shell_integration --- grid.c | 10 +++++----- input.c | 4 ++-- osc.c | 2 +- terminal.c | 2 +- terminal.h | 5 +++-- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/grid.c b/grid.c index ea103c65..19322cbf 100644 --- a/grid.c +++ b/grid.c @@ -231,7 +231,7 @@ grid_snapshot(const struct grid *grid) clone_row->cells = xmalloc(grid->num_cols * sizeof(clone_row->cells[0])); clone_row->linebreak = row->linebreak; clone_row->dirty = row->dirty; - clone_row->prompt_marker = row->prompt_marker; + clone_row->shell_integration = row->shell_integration; for (int c = 0; c < grid->num_cols; c++) clone_row->cells[c] = row->cells[c]; @@ -366,7 +366,7 @@ grid_row_alloc(int cols, bool initialize) row->dirty = false; row->linebreak = false; row->extra = NULL; - row->prompt_marker = false; + row->shell_integration.prompt_marker = false; if (initialize) { row->cells = xcalloc(cols, sizeof(row->cells[0])); @@ -425,7 +425,7 @@ grid_resize_without_reflow( new_row->dirty = old_row->dirty; new_row->linebreak = false; - new_row->prompt_marker = old_row->prompt_marker; + new_row->shell_integration = old_row->shell_integration; if (new_cols > old_cols) { /* Clear "new" columns */ @@ -587,7 +587,7 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, /* Scrollback is full, need to reuse a row */ grid_row_reset_extra(new_row); new_row->linebreak = false; - new_row->prompt_marker = false; + new_row->shell_integration.prompt_marker = false; tll_foreach(old_grid->sixel_images, it) { if (it->item.pos.row == *row_idx) { @@ -920,7 +920,7 @@ grid_resize_and_reflow( xassert(from + amount <= old_cols); if (from == 0) - new_row->prompt_marker = old_row->prompt_marker; + new_row->shell_integration = old_row->shell_integration; memcpy( &new_row->cells[new_col_idx], &old_row->cells[from], diff --git a/input.c b/input.c index dc0eec93..5cdf513f 100644 --- a/input.c +++ b/input.c @@ -377,7 +377,7 @@ execute_binding(struct seat *seat, struct terminal *term, const struct row *row = grid->rows[r_abs]; xassert(row != NULL); - if (!row->prompt_marker) + if (!row->shell_integration.prompt_marker) continue; grid->view = r_abs; @@ -409,7 +409,7 @@ execute_binding(struct seat *seat, struct terminal *term, const struct row *row = grid->rows[r_abs]; xassert(row != NULL); - if (!row->prompt_marker) { + if (!row->shell_integration.prompt_marker) { if (r_abs == grid->offset + term->rows - 1) { /* We’ve reached the bottom of the scrollback */ break; diff --git a/osc.c b/osc.c index ba08964c..a54946ff 100644 --- a/osc.c +++ b/osc.c @@ -893,7 +893,7 @@ osc_dispatch(struct terminal *term) term->grid->cursor.point.row, term->grid->cursor.point.col); - term->grid->cur_row->prompt_marker = true; + term->grid->cur_row->shell_integration.prompt_marker = true; break; case 'B': diff --git a/terminal.c b/terminal.c index f63aaaa7..33666e6a 100644 --- a/terminal.c +++ b/terminal.c @@ -1825,7 +1825,7 @@ erase_line(struct terminal *term, struct row *row) { erase_cell_range(term, row, 0, term->cols - 1); row->linebreak = false; - row->prompt_marker = false; + row->shell_integration.prompt_marker = false; } void diff --git a/terminal.h b/terminal.h index 0dca0f48..76b52566 100644 --- a/terminal.h +++ b/terminal.h @@ -121,8 +121,9 @@ struct row { bool dirty; bool linebreak; - /* Shell integration */ - bool prompt_marker; + struct { + bool prompt_marker; + } shell_integration; }; struct sixel { From e9607de5ae2eaf1a57924bb282a660150eca6eb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 8 Dec 2022 10:46:46 +0100 Subject: [PATCH 0564/1323] osc: store column of FTCS_COMMAND_{EXECUTED,FINISHED} in row struct --- grid.c | 4 ++++ osc.c | 10 ++++++++-- terminal.c | 2 ++ terminal.h | 2 ++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/grid.c b/grid.c index 19322cbf..f14580ad 100644 --- a/grid.c +++ b/grid.c @@ -367,6 +367,8 @@ grid_row_alloc(int cols, bool initialize) row->linebreak = false; row->extra = NULL; row->shell_integration.prompt_marker = false; + row->shell_integration.cmd_start = -1; + row->shell_integration.cmd_end = -1; if (initialize) { row->cells = xcalloc(cols, sizeof(row->cells[0])); @@ -588,6 +590,8 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, grid_row_reset_extra(new_row); new_row->linebreak = 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) { diff --git a/osc.c b/osc.c index a54946ff..5da8666f 100644 --- a/osc.c +++ b/osc.c @@ -901,11 +901,17 @@ osc_dispatch(struct terminal *term) break; case 'C': - LOG_DBG("FTCS_COMMAND_EXECUTED"); + LOG_DBG("FTCS_COMMAND_EXECUTED: %dx%d", + term->grid->cursor.point.row, + term->grid->cursor.point.col); + term->grid->cur_row->shell_integration.cmd_start = term->grid->cursor.point.col; break; case 'D': - LOG_DBG("FTCS_COMMAND_FINISHED"); + LOG_DBG("FTCS_COMMAND_FINISHED: %dx%d", + term->grid->cursor.point.row, + term->grid->cursor.point.col); + term->grid->cur_row->shell_integration.cmd_end = term->grid->cursor.point.col; break; } break; diff --git a/terminal.c b/terminal.c index 33666e6a..4152724a 100644 --- a/terminal.c +++ b/terminal.c @@ -1826,6 +1826,8 @@ erase_line(struct terminal *term, struct row *row) erase_cell_range(term, row, 0, term->cols - 1); row->linebreak = false; row->shell_integration.prompt_marker = false; + row->shell_integration.cmd_start = -1; + row->shell_integration.cmd_end = -1; } void diff --git a/terminal.h b/terminal.h index 76b52566..1e3a724e 100644 --- a/terminal.h +++ b/terminal.h @@ -123,6 +123,8 @@ struct row { struct { bool prompt_marker; + int cmd_start; /* Column, -1 if unset */ + int cmd_end; /* Column, -1 if unset */ } shell_integration; }; From 1c70a84fde80405e0b6658722af8bc3fb54f225d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 8 Dec 2022 11:45:23 +0100 Subject: [PATCH 0565/1323] config: add pipe-command-output key-binding --- config.c | 1 + input.c | 7 +++- key-binding.h | 1 + terminal.c | 93 ++++++++++++++++++++++++++++++++++++++++++++++++--- terminal.h | 2 ++ 5 files changed, 99 insertions(+), 5 deletions(-) diff --git a/config.c b/config.c index e3d531c2..3728826f 100644 --- a/config.c +++ b/config.c @@ -111,6 +111,7 @@ static const char *const binding_action_map[] = { [BIND_ACTION_PIPE_SCROLLBACK] = "pipe-scrollback", [BIND_ACTION_PIPE_VIEW] = "pipe-visible", [BIND_ACTION_PIPE_SELECTED] = "pipe-selected", + [BIND_ACTION_PIPE_COMMAND_OUTPUT] = "pipe-command-output", [BIND_ACTION_SHOW_URLS_COPY] = "show-urls-copy", [BIND_ACTION_SHOW_URLS_LAUNCH] = "show-urls-launch", [BIND_ACTION_SHOW_URLS_PERSISTENT] = "show-urls-persistent", diff --git a/input.c b/input.c index 5cdf513f..166ad70b 100644 --- a/input.c +++ b/input.c @@ -227,7 +227,8 @@ execute_binding(struct seat *seat, struct terminal *term, break; /* FALLTHROUGH */ case BIND_ACTION_PIPE_VIEW: - case BIND_ACTION_PIPE_SELECTED: { + case BIND_ACTION_PIPE_SELECTED: + case BIND_ACTION_PIPE_COMMAND_OUTPUT: { if (binding->aux->type != BINDING_AUX_PIPE) return true; @@ -269,6 +270,10 @@ execute_binding(struct seat *seat, struct terminal *term, len = text != NULL ? strlen(text) : 0; break; + case BIND_ACTION_PIPE_COMMAND_OUTPUT: + success = term_command_output_to_text(term, &text, &len); + break; + default: BUG("Unhandled action type"); success = false; diff --git a/key-binding.h b/key-binding.h index 050c80a6..ba841efa 100644 --- a/key-binding.h +++ b/key-binding.h @@ -32,6 +32,7 @@ enum bind_action_normal { BIND_ACTION_PIPE_SCROLLBACK, BIND_ACTION_PIPE_VIEW, BIND_ACTION_PIPE_SELECTED, + BIND_ACTION_PIPE_COMMAND_OUTPUT, BIND_ACTION_SHOW_URLS_COPY, BIND_ACTION_SHOW_URLS_LAUNCH, BIND_ACTION_SHOW_URLS_PERSISTENT, diff --git a/terminal.c b/terminal.c index 4152724a..a328783f 100644 --- a/terminal.c +++ b/terminal.c @@ -3613,7 +3613,7 @@ term_surface_kind(const struct terminal *term, const struct wl_surface *surface) static bool rows_to_text(const struct terminal *term, int start, int end, - char **text, size_t *len) + int col_start, int col_end, char **text, size_t *len) { struct extraction_context *ctx = extract_begin(SELECTION_NONE, true); if (ctx == NULL) @@ -3626,15 +3626,20 @@ rows_to_text(const struct terminal *term, int start, int end, const struct row *row = term->grid->rows[r]; xassert(row != NULL); - for (int c = 0; c < term->cols; c++) + const int c_end = r == end ? col_end : term->cols; + + for (int c = col_start; c < c_end; c++) { if (!extract_one(term, row, &row->cells[c], c, ctx)) goto out; + } if (r == end) break; r++; r &= grid_rows - 1; + + col_start = 0; } out: @@ -3666,7 +3671,7 @@ term_scrollback_to_text(const struct terminal *term, char **text, size_t *len) end += term->grid->num_rows; } - return rows_to_text(term, start, end, text, len); + return rows_to_text(term, start, end, 0, term->cols, text, len); } bool @@ -3674,7 +3679,87 @@ term_view_to_text(const struct terminal *term, char **text, size_t *len) { int start = grid_row_absolute_in_view(term->grid, 0); int end = grid_row_absolute_in_view(term->grid, term->rows - 1); - return rows_to_text(term, start, end, text, len); + return rows_to_text(term, start, end, 0, term->cols, text, len); +} + +bool +term_command_output_to_text(const struct terminal *term, char **text, size_t *len) +{ + int start_row = -1; + int end_row = -1; + int start_col = -1; + int end_col = -1; + + const struct grid *grid = term->grid; + const int sb_end = grid_row_absolute(grid, term->rows - 1); + int r = (sb_end - 1 + grid->num_rows) & (grid->num_rows - 1); + + while (start_row < 0 && r != sb_end) { + 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; + } + + 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 --git a/terminal.h b/terminal.h index 1e3a724e..35127e3c 100644 --- a/terminal.h +++ b/terminal.h @@ -847,6 +847,8 @@ bool term_scrollback_to_text( const struct terminal *term, char **text, size_t *len); bool term_view_to_text( const struct terminal *term, char **text, size_t *len); +bool term_command_output_to_text( + const struct terminal *term, char **text, size_t *len); bool term_ime_is_enabled(const struct terminal *term); void term_ime_enable(struct terminal *term); From f2a8368759f5968eff460ca2f30d600cbcc26839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 8 Dec 2022 11:45:51 +0100 Subject: [PATCH 0566/1323] foot.ini: add pipe-command-output key binding --- foot.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/foot.ini b/foot.ini index 55eb42de..42a71e58 100644 --- a/foot.ini +++ b/foot.ini @@ -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=[sh -c "cat - > /tmp/foot-cmd-out.txt"] none # Write output of last command to /tmp/foot-cmd-out.txt (requires shell integration) # show-urls-launch=Control+Shift+o # show-urls-copy=none # show-urls-persistent=none From 0fed2451eae01922c8fcbb40ab35b32ac3d655a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 8 Dec 2022 11:50:43 +0100 Subject: [PATCH 0567/1323] doc: foot.ini: document pipe-command-output --- doc/foot.ini.5.scd | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 1c87db53..14999a8b 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -844,11 +844,12 @@ e.g. *search-start=none*. *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. @@ -856,10 +857,17 @@ e.g. *search-start=none*. Note that the command is *not* automatically run inside a shell; use *sh -c "command line"* if you need that. - Example: - *pipe-visible=[sh -c "xurls | uniq | tac | fuzzel | xargs -r + 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: + # Write scrollback content to /tmp/foot-scrollback.txt++ +*pipe-scrollback=[sh -c "cat - > /tmp/foot-scrollback.txt"] + Control+Shift+Print* Default: _none_ *show-urls-launch* From d7dbb91e65d375e2ea496ea7a921bfcd9e3d8c1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 8 Dec 2022 11:51:27 +0100 Subject: [PATCH 0568/1323] changelog: pipe-command-output --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2233fd9e..55ac7e5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ 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. [1348]: https://codeberg.org/dnkl/foot/issues/1348 From 1393942de38fa6ece726fd861cc15e2a5a369fbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 8 Dec 2022 12:17:55 +0100 Subject: [PATCH 0569/1323] readme, doc/foot.1: document shell-integration:command-output tracking --- README.md | 36 ++++++++++++++++++++++++++++++++++++ doc/foot.1.scd | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/README.md b/README.md index b1cfb37d..c1be7476 100644 --- a/README.md +++ b/README.md @@ -359,6 +359,42 @@ See the [wiki](https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts) for details, and examples for other shells. +### Piping last command’s output + +The key binding `pipe-command-output` can pipe the last command’s +output to an application of your choice (similar to the other `pipe-*` +key bindings): + +```ini +[key-bindings] +pipe-command-output=[sh -c "f=$(mktemp); cat - > $f; footclient emacsclient -nw $f; rm $f"] Control+Shift+g +``` + +When pressing ctrl+shift+g, the last +command’s output is written to a temporary file, then an emacsclient +is started in a new footclient instance. The temporary file is removed +after the footclient instance has closed. + +For this to work, the shell must emit an OSC-133;C (`\E]133;C\E\\`) +sequence before command output starts, and an OSC-133;D +(`\E]133;D\E\\`) when the command output ends. + +In fish, one way to do this is to add `preexec` and `postexec` hooks: + +```fish +function foot_cmd_start --on-event fish_preexec + echo -en "\e]133;C\e\\" +end + +function foot_cmd_end --on-event fish_postexec + echo -en "\e]133;D\e\\" +end +``` + +See the +[wiki](https://codeberg.org/dnkl/foot/wiki#user-content-piping-last-commands-output) +for details, and examples for other shells + ## Alt/meta diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 385f9721..8e2fb313 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -424,6 +424,38 @@ See the wiki (https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts) for details, and examples for other shells. +## Piping last command’s output + +The key binding *pipe-command-output* can pipe the last command’s +output to an application of your choice (similar to the other +*pipe-\** key bindings): + + *\[key-bindings\]++ +pipe-command-output=[sh -c "f=$(mktemp); cat - > $f; footclient emacsclient -nw $f; rm $f"] Control+Shift+g* + +When pressing *ctrl*+*shift*+*g*, the last command’s output is written +to a temporary file, then an emacsclient is started in a new +footclient instance. The temporary file is removed after the +footclient instance has closed. + +For this to work, the shell must emit an OSC-133;C (*\\E]133;C\\E\\\\*) +sequence before command output starts, and an OSC-133;D +(*\\E]133;D\\E\\\\*) when the command output ends. + +In fish, one way to do this is to add _preexec_ and _postexec_ hooks: + + *function foot_cmd_start --on-event fish_preexec + echo -en "\\e]133;C\\e\\\\" + end* + + *function foot_cmd_end --on-event fish_postexec + echo -en "\\e]133;D\\e\\\\" + end* + +See the wiki +(https://codeberg.org/dnkl/foot/wiki#user-content-piping-last-commands-output) +for details, and examples for other shells + # TERMINFO Client applications use the terminfo identifier specified by the From d5308a0493bb69167aacf54a8eb98f2e92a046dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 8 Dec 2022 13:06:24 +0100 Subject: [PATCH 0570/1323] =?UTF-8?q?term:=20command=5Foutput=5Fto=5Ftext(?= =?UTF-8?q?):=20don=E2=80=99t=20skip=20FTCS=5FCOMMAND=5FFINISHED=20on=20la?= =?UTF-8?q?st=20row?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- terminal.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/terminal.c b/terminal.c index a328783f..5d6c956d 100644 --- a/terminal.c +++ b/terminal.c @@ -3692,9 +3692,10 @@ term_command_output_to_text(const struct terminal *term, char **text, size_t *le const struct grid *grid = term->grid; const int sb_end = grid_row_absolute(grid, term->rows - 1); - int r = (sb_end - 1 + grid->num_rows) & (grid->num_rows - 1); + const int sb_start = (sb_end + 1) & (grid->num_rows - 1); + int r = sb_end; - while (start_row < 0 && r != sb_end) { + while (start_row < 0) { const struct row *row = grid->rows[r]; if (row == NULL) break; @@ -3709,6 +3710,9 @@ term_command_output_to_text(const struct terminal *term, char **text, size_t *le start_col = row->shell_integration.cmd_start; } + if (r == sb_start) + break; + r = (r - 1 + grid->num_rows) & (grid->num_rows - 1); } From 110a6dd6f080686f85df9dff16fbb06fe2f57a81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 8 Dec 2022 13:49:38 +0100 Subject: [PATCH 0571/1323] grid: resize without reflow: truncate shell_integration.cmd_{start,end} This ensures the cmd start/end columns are valid in the new grid. --- grid.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/grid.c b/grid.c index f14580ad..e0e357dc 100644 --- a/grid.c +++ b/grid.c @@ -427,7 +427,9 @@ grid_resize_without_reflow( new_row->dirty = old_row->dirty; new_row->linebreak = false; - new_row->shell_integration = old_row->shell_integration; + 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 */ From 231e6eb3f10c80ce6fb151d44814504c3ce00e41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 8 Dec 2022 13:50:30 +0100 Subject: [PATCH 0572/1323] grid: resize with reflow: reflow FTCS_COMMAND_{EXECUTED,FINISHED} --- grid.c | 56 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/grid.c b/grid.c index e0e357dc..e7cdedc5 100644 --- a/grid.c +++ b/grid.c @@ -1,5 +1,6 @@ #include "grid.h" +#include #include #include @@ -837,35 +838,26 @@ grid_resize_and_reflow( 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; + /* 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; - if (tp != NULL) { - int tp_col = tp->col + 1; - end = min(tp_col, uri_col); + end = min(col_count, min(min(tp_col, uri_col), ftcs_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; + uri_break = end == uri_col; + tp_break = end == tp_col; + ftcs_break = end == ftcs_col; + } int cols = end - start; xassert(cols > 0); @@ -926,7 +918,7 @@ grid_resize_and_reflow( xassert(from + amount <= old_cols); if (from == 0) - new_row->shell_integration = old_row->shell_integration; + new_row->shell_integration.prompt_marker = old_row->shell_integration.prompt_marker; memcpy( &new_row->cells[new_col_idx], &old_row->cells[from], @@ -985,6 +977,16 @@ grid_resize_and_reflow( } } + 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; } From 84e681f02873a1848dd2de681de49c602e982a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 6 Feb 2024 12:14:48 +0100 Subject: [PATCH 0573/1323] readme: add "Piping last command's output" to index --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c1be7476..f54070ea 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ The fast, lightweight and minimalistic Wayland terminal emulator. 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) From d6939dd6340afd0dd2bc83c235fa192e9dc25ad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 6 Feb 2024 12:26:00 +0100 Subject: [PATCH 0574/1323] readme: shell integration: fix wiki link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f54070ea..1d073dfb 100644 --- a/README.md +++ b/README.md @@ -393,7 +393,7 @@ end ``` See the -[wiki](https://codeberg.org/dnkl/foot/wiki#user-content-piping-last-commands-output) +[wiki](https://codeberg.org/dnkl/foot/wiki#user-content-piping-last-command-s-output) for details, and examples for other shells From 7999975016a57cc0a2c2e20bc55f29de32df7044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 6 Feb 2024 12:36:45 +0100 Subject: [PATCH 0575/1323] Don't use fancy Unicode quotes, stick to ASCII --- CHANGELOG.md | 98 +++++++++++++++++------------------ INSTALL.md | 32 ++++++------ README.md | 34 ++++++------ box-drawing.c | 2 +- commands.c | 2 +- config.c | 10 ++-- config.h | 2 +- csi.c | 8 +-- dcs.c | 8 +-- doc/foot.1.scd | 22 ++++---- grid.c | 32 ++++++------ input.c | 62 +++++++++++----------- input.h | 4 +- key-binding.c | 36 ++++++------- kitty-keymap.h | 2 +- main.c | 2 +- notify.c | 2 +- osc.c | 6 +-- pgo/full-headless-cage.sh | 2 +- pgo/full-headless-sway.sh | 4 +- pgo/pgo.sh | 4 +- render.c | 106 +++++++++++++++++++------------------- selection.c | 38 +++++++------- shm.c | 8 +-- shm.h | 4 +- sixel.c | 16 +++--- terminal.c | 50 +++++++++--------- terminal.h | 12 ++--- url-mode.c | 20 +++---- vt.c | 20 +++---- wayland.c | 8 +-- 31 files changed, 328 insertions(+), 328 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55ac7e5c..096d43a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -378,7 +378,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 @@ -386,9 +386,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 @@ -484,8 +484,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. @@ -538,12 +538,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 @@ -614,7 +614,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]) @@ -654,7 +654,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]). @@ -718,7 +718,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 @@ -731,7 +731,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. @@ -772,7 +772,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. @@ -828,7 +828,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]). @@ -850,7 +850,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]). @@ -916,15 +916,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. @@ -952,7 +952,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 @@ -1026,7 +1026,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. @@ -1063,7 +1063,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, @@ -1098,7 +1098,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)). @@ -1114,9 +1114,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)). @@ -1131,7 +1131,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. @@ -1139,7 +1139,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. @@ -1159,7 +1159,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)). @@ -1187,11 +1187,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. @@ -1240,12 +1240,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 @@ -1255,7 +1255,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)). @@ -1422,10 +1422,10 @@ For full support, the following is required: If `tweak.grapheme-shaping` has **not** been enabled, foot will neither use libutf8proc to do grapheme cluster segmentation, nor will -it use fcft’s grapheme shaping capabilities to shape combining +it use fcft's grapheme shaping capabilities to shape combining characters. -This feature is _experimental_ mostly due to the “wcwidth” problem; +This feature is _experimental_ mostly due to the "wcwidth" problem; how many cells should foot allocate for a grapheme cluster? While the answer may seem simple, the problem is that, whatever the answer is, the client application **must** come up with the **same** @@ -1503,9 +1503,9 @@ supported. * Point values in `line-height`, `letter-spacing`, `horizontal-letter-offset` and `vertical-letter-offset` are now rounded, not truncated, when translated to pixel values. -* Foot’s exit code is now -26/230 when foot itself failed to launch +* Foot's exit code is now -26/230 when foot itself failed to launch (due to invalid command line options, client application/shell not - found etc). Footclient’s exit code is -36/220 when it itself fails + found etc). Footclient's exit code is -36/220 when it itself fails to launch (e.g. bad command line option) and -26/230 when the foot server failed to instantiate a new window ([#466](https://codeberg.org/dnkl/foot/issues/466)). @@ -1558,7 +1558,7 @@ supported. resulting in PGO build failures. * Wrong colors in the 256-color cube ([#479](https://codeberg.org/dnkl/foot/issues/479)). -* Memory leak triggered by “opening” an OSC-8 URI and then resetting +* Memory leak triggered by "opening" an OSC-8 URI and then resetting the terminal without closing the URI ([#495](https://codeberg.org/dnkl/foot/issues/495)). * Assertion when emitting a sixel occupying the entire scrollback @@ -1567,7 +1567,7 @@ supported. invisible) for certain combinations of fonts and font sizes ([#503](https://codeberg.org/dnkl/foot/issues/503)). * Sixels with transparent bottom border being resized below the size - specified in _”Set Raster Attributes”_. + specified in _"Set Raster Attributes"_. * Fonts sometimes not being reloaded with the correct scaling factor when `dpi-aware=no`, or `dpi-aware=auto` with monitor(s) with a scaling factor > 1 ([#509](https://codeberg.org/dnkl/foot/issues/509)). @@ -1763,7 +1763,7 @@ supported. background color for empty pixels instead of the default background color ([#391](https://codeberg.org/dnkl/foot/issues/391)). * Sixel decoding optimized; up to 100% faster in some cases. -* Reported sixel “max geometry” from current window size, to the +* Reported sixel "max geometry" from current window size, to the configured maximum size (defaulting to 10000x10000). @@ -1862,7 +1862,7 @@ supported. * Pasting URIs from the clipboard when the source has not newline-terminated the last URI ([#291](https://codeberg.org/dnkl/foot/issues/291)). -* Sixel “current geometry” query response not being bounded by the +* Sixel "current geometry" query response not being bounded by the current window dimensions (fixes `lsix` output) * Crash on keyboard input when repeat rate was zero (i.e. no repeat). * Wrong button encoding of mouse buttons 6 and 7 in mouse events. @@ -1939,7 +1939,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See and `CSI ? 737769 l` disables it. This can be used to e.g. enable/disable IME when entering/leaving insert mode in vim. * `dpi-aware` option to `foot.ini`. The default, `auto`, sizes fonts - using the monitor’s DPI when output scaling has been + using the monitor's DPI when output scaling has been **disabled**. If output scaling has been **enabled**, fonts are sized using the scaling factor. DPI-only font sizing can be forced by setting `dpi-aware=yes`. Setting `dpi-aware=no` forces font @@ -2019,7 +2019,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See `\E[38:2...m`) can now be used _without_ the color space ID parameter. * SGR 21 no longer disables **bold**. According to ECMA-48, SGR 21 is - _”double underline_”. Foot does not (yet) implement that, but that’s + _"double underline_". Foot does not (yet) implement that, but that's no reason to implement a non-standard behavior. * `DECRQM` now returns actual state of the requested mode, instead of always returning `2`. @@ -2067,7 +2067,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See ([#194](https://codeberg.org/dnkl/foot/issues/194)). * Single-width characters with double-width glyphs are now allowed to overflow into neighboring cells by default. Set - **tweak.allow-overflowing-double-width-glyphs** to ‘no’ to disable + **tweak.allow-overflowing-double-width-glyphs** to 'no' to disable this. ### Fixed @@ -2271,7 +2271,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See binding has consumed it. * Input events from getting mixed with paste data ([#101](https://codeberg.org/dnkl/foot/issues/101)). -* Missing DPI values for “some” monitors on Gnome +* Missing DPI values for "some" monitors on Gnome ([#118](https://codeberg.org/dnkl/foot/issues/118)). * Handling of multi-column composed characters while reflowing. * Escape sequences sent for key combinations with `Return`, that did diff --git a/INSTALL.md b/INSTALL.md index 22ea8067..7df8d0b8 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -94,24 +94,24 @@ A note on terminfo; the terminfo database exposes terminal capabilities to the applications running inside the terminal. As such, it is important that the terminfo used reflects the actual terminal. Using the `xterm-256color` terminfo will, in many cases, -work, but I still recommend using foot’s own terminfo. There are two +work, but I still recommend using foot's own terminfo. There are two reasons for this: -* foot’s terminfo contains a couple of non-standard capabilities, +* foot's terminfo contains a couple of non-standard capabilities, used by e.g. tmux. * New capabilities added to the `xterm-256color` terminfo could potentially break foot. -* There may be future additions or changes to foot’s terminfo. +* There may be future additions or changes to foot's terminfo. -As of ncurses 2021-07-31, ncurses includes a version of foot’s +As of ncurses 2021-07-31, ncurses includes a version of foot's terminfo. **The recommendation is to use those**, and only install the -terminfo definitions from this git repo if the system’s ncurses +terminfo definitions from this git repo if the system's ncurses predates 2021-07-31. -But, note that the foot terminfo definitions in ncurses’ lack the +But, note that the foot terminfo definitions in ncurses' lack the non-standard capabilities. This mostly affects tmux; without them, `terminal-overrides` must be configured to enable truecolor -support. For this reason, it _is_ possible to install “our” terminfo +support. For this reason, it _is_ possible to install "our" terminfo definitions as well, either in a non-default location, or under a different name. @@ -124,10 +124,10 @@ details. Installing them under a different name generally works well, but will break applications that check if `$TERM == foot`. -Hence the recommendation to simply use ncurses’ terminfo definitions +Hence the recommendation to simply use ncurses' terminfo definitions if available. -If packaging “our” terminfo definitions, I recommend doing that as a +If packaging "our" terminfo definitions, I recommend doing that as a separate package, to allow them to be installed on remote systems without having to install foot itself. @@ -176,9 +176,9 @@ meson ... -Ddefault-terminfo=foot -Dterminfo-base-name=foot-extra ``` (or just leave out `-Ddefault-terminfo`, since it defaults to `foot` anyway). -Finally, `-Dcustom-terminfo-install-location` enables foot’s terminfo -to co-exist with ncurses’ version, without changing the terminfo -names. The idea is that you install foot’s terminfo to a non-standard +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 @@ in the meson build. It does **not** change the default value of `TERM`, and it does **not** disable `TERMINFO`, if `-Dcustom-terminfo-install-location` has been set. Use this if packaging the terminfo definitions in a separate package (and the -build script isn’t shared with the ‘foot’ package). +build script isn't shared with the 'foot' package). Example: @@ -269,7 +269,7 @@ reason there are a number of helper scripts available. scripts in the `pgo` directory to do a complete PGO build. This script is intended to be used when doing manual builds. -Note that all “full” PGO builds (which `auto` will prefer, if +Note that all "full" PGO builds (which `auto` will prefer, if possible) **require** `LC_CTYPE` to be set to an UTF-8 locale. This is **not** done automatically. @@ -370,7 +370,7 @@ fail. The snippet above then creates an (empty) temporary file. Then, it runs a script that generates random escape sequences (if you cat -`${tmp_file}` in a terminal, you’ll see random colored characters all +`${tmp_file}` in a terminal, you'll see random colored characters all over the screen). Finally, we feed the randomly generated escape sequences to the PGO helper. This is what generates the profiling data used in the next step. @@ -450,7 +450,7 @@ sed 's/@default_terminfo@/foot/g' foot.info | \ tic -o -x -e foot,foot-direct - ``` -Where _”output-directory”_ **must** match the value passed to +Where _"output-directory"_ **must** match the value passed to `-Dcustom-terminfo-install-location` in the foot build. If `-Dcustom-terminfo-install-location` has not been set, `-o ` can simply be omitted. diff --git a/README.md b/README.md index 1d073dfb..5c8d3878 100644 --- a/README.md +++ b/README.md @@ -303,10 +303,10 @@ Foot supports URL detection. But, unlike many other terminal emulators, where URLs are highlighted when they are hovered and opened by clicking on them, foot uses a keyboard driven approach. -Pressing ctrl+shift+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 @@ -329,7 +329,7 @@ the jump label key sequences can be configured. New foot terminal instances (bound to ctrl+shift+n by default) will open in -the current working directory, **if** the shell in the “parent” +the current working directory, **if** the shell in the "parent" terminal reports directory changes. This is done with the OSC-7 escape sequence. Most shells can be @@ -360,9 +360,9 @@ See the [wiki](https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts) for details, and examples for other shells. -### Piping last command’s output +### Piping last command's output -The key binding `pipe-command-output` can pipe the last command’s +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): @@ -372,7 +372,7 @@ pipe-command-output=[sh -c "f=$(mktemp); cat - > $f; footclient emacsclient -nw ``` When pressing ctrl+shift+g, the last -command’s output is written to a temporary file, then an emacsclient +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. @@ -472,19 +472,19 @@ multiplied. For this reason, and because of the new _fractional scaling_ protocol (see below for details), and because this is how Wayland applications are expected to behave, foot >= 1.15 will default to scaling fonts -using the compositor’s scaling factor, and **not** the monitor +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. @@ -574,7 +574,7 @@ emulator actually responded to. Starting with version 1.7.0, foot also implements `XTVERSION`, to which it will reply with `\EP>|foot(version)\E\\`. Version is -e.g. “1.8.2” for a regular release, or “1.8.2-36-g7db8e06f” for a git +e.g. "1.8.2" for a regular release, or "1.8.2-36-g7db8e06f" for a git build. @@ -587,9 +587,9 @@ It allows querying the terminal for terminfo capabilities. Applications using this feature do not need to use the classic, file-based, terminfo definition. For example, if all applications used this feature, you would no longer have to install -foot’s terminfo on remote hosts you SSH into. +foot's terminfo on remote hosts you SSH into. -XTerm’s implementation (as of XTerm-370) only supports querying key +XTerm's implementation (as of XTerm-370) only supports querying key (as in keyboard keys) capabilities, and three custom capabilities: * `TN` - terminal name @@ -601,7 +601,7 @@ Kitty has extended this, and also supports querying all integer and string capabilities. Foot supports this, and extends it even further, to also include -boolean capabilities. This means foot’s entire terminfo can be queried +boolean capabilities. This means foot's entire terminfo can be queried via `XTGETTCAP`. Note that both Kitty and foot handles **responses** to @@ -613,7 +613,7 @@ capability/value pairs. There are a couple of issues with this: * The success/fail flag in the beginning of the response is always `1` (success), unless the very **first** queried capability is invalid. * XTerm will not respond **at all** to an invalid capability, unless - it’s the first one in the `XTGETTCAP` query. + it's the first one in the `XTGETTCAP` query. * XTerm will end the response at the first invalid capability. In other words, if you send a large multi-capability query, you will diff --git a/box-drawing.c b/box-drawing.c index cf351b31..d1ce1af0 100644 --- a/box-drawing.c +++ b/box-drawing.c @@ -2098,7 +2098,7 @@ draw_braille(struct buf *buf, char32_t wc) if (x_px_left >= 1) { x_spacing++; x_px_left--; } if (y_px_left >= 3) { y_spacing++; y_px_left -= 3; } - /* Fourth, margins (“spacing”, but on the sides) */ + /* Fourth, margins ("spacing", but on the sides) */ if (x_px_left >= 2) { x_margin++; x_px_left -= 2; } if (y_px_left >= 2) { y_margin++; y_px_left -= 2; } diff --git a/commands.c b/commands.c index 7b93f044..a3e48458 100644 --- a/commands.c +++ b/commands.c @@ -23,7 +23,7 @@ cmd_scrollback_up(struct terminal *term, int rows) const int grid_rows = grid->num_rows; /* The view row number in scrollback relative coordinates. This is - * the maximum number of rows we’re allowed to scroll */ + * the maximum number of rows we're allowed to scroll */ int sb_start = grid_sb_start_ignore_uninitialized(grid, term->rows); int view_sb_rel = grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, view); diff --git a/config.c b/config.c index 3728826f..4945485e 100644 --- a/config.c +++ b/config.c @@ -540,7 +540,7 @@ value_to_str(struct context *ctx, char **res) * * - key="value" OK * - key=abc "quote" def NOT OK - * - key=’value’ OK + * - key='value' OK * * Finally, we support escaping the quote character, and the * escape character itself: @@ -1898,7 +1898,7 @@ modifiers_disjoint(const config_modifier_list_t *mods1, static char * NOINLINE modifiers_to_str(const config_modifier_list_t *mods) { - size_t len = tll_length(*mods); /* ‘+’ , and NULL terminator */ + size_t len = tll_length(*mods); /* '+' , and NULL terminator */ tll_foreach(*mods, it) len += strlen(it->item); @@ -3537,7 +3537,7 @@ config_font_parse(const char *pattern, struct config_font *font) /* * First look for user specified {pixel}size option - * e.g. “font-name:size=12” + * e.g. "font-name:size=12" */ double pt_size = -1.0; @@ -3548,9 +3548,9 @@ config_font_parse(const char *pattern, struct config_font *font) if (have_pt_size != FcResultMatch && have_px_size != FcResultMatch) { /* - * Apply fontconfig config. Can’t do that until we’ve first + * Apply fontconfig config. Can't do that until we've first * checked for a user provided size, since we may end up with - * both “size” and “pixelsize” being set, and we don’t know + * both "size" and "pixelsize" being set, and we don't know * which one takes priority. */ FcPattern *pat_copy = FcPatternDuplicate(pat); diff --git a/config.h b/config.h index 39f169f9..30219ff0 100644 --- a/config.h +++ b/config.h @@ -311,7 +311,7 @@ struct config { uint32_t buttons; uint32_t minimize; uint32_t maximize; - uint32_t quit; /* ‘close’ collides with #define in epoll-shim */ + uint32_t quit; /* 'close' collides with #define in epoll-shim */ uint32_t border; } color; diff --git a/csi.c b/csi.c index 04f03b05..fe783b13 100644 --- a/csi.c +++ b/csi.c @@ -1536,8 +1536,8 @@ csi_dispatch(struct terminal *term, uint8_t final) break; case 4: /* modifyOtherKeys */ - /* We don’t support fully disabling modifyOtherKeys, - * but simply revert back to mode ‘1’ */ + /* We don't support fully disabling modifyOtherKeys, + * but simply revert back to mode '1' */ term->modify_other_keys_2 = false; LOG_DBG("modifyOtherKeys=1"); break; @@ -1615,7 +1615,7 @@ csi_dispatch(struct terminal *term, uint8_t final) break; } } - break; /* private[0] == ‘<’ */ + break; /* private[0] == '<' */ } case ' ': { @@ -1762,7 +1762,7 @@ csi_dispatch(struct terminal *term, uint8_t final) break; } - break; /* private[0] == ‘?’ && private[1] == ‘$’ */ + break; /* private[0] == '?' && private[1] == '$' */ default: UNHANDLED(); diff --git a/dcs.c b/dcs.c index 601f1172..c4309459 100644 --- a/dcs.c +++ b/dcs.c @@ -138,12 +138,12 @@ xtgettcap_reply(struct terminal *term, const char *hex_cap_name, size_t len) /* * Reply format: * \EP 1 + r cap=value \E\\ - * Where ‘cap’ and ‘value are hex encoded ascii strings + * Where 'cap' and 'value are hex encoded ascii strings */ char *reply = xmalloc( 5 + /* DCS 1 + r (\EP1+r) */ len + /* capability name, hex encoded */ - 1 + /* ‘=’ */ + 1 + /* '=' */ strlen(value) * 2 + /* capability value, hex encoded */ 2 + /* ST (\E\\) */ 1); @@ -253,8 +253,8 @@ decrqss_unhook(struct terminal *term) /* * A note on the Ps parameter in the reply: many DEC manual * instances (e.g. https://vt100.net/docs/vt510-rm/DECRPSS) claim - * that 0 means “request is valid”, and 1 means “request is - * invalid”. + * that 0 means "request is valid", and 1 means "request is + * invalid". * * However, this appears to be a typo; actual hardware inverts the * response (as does XTerm and mlterm): diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 8e2fb313..73c0c2b1 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -313,10 +313,10 @@ Foot supports URL detection. But, unlike many other terminal emulators, where URLs are highlighted when they are hovered and opened by clicking on them, foot uses a keyboard driven approach. -Pressing *ctrl*+*shift*+*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 +398,7 @@ For more information, see *foot.ini*(5). New foot terminal instances (bound to *ctrl*+*shift*+*n* by default) will open in the current working directory, if the shell in the -“parent” terminal reports directory changes. +"parent" terminal reports directory changes. This is done with the OSC-7 escape sequence. Most shells can be scripted to do this, if they do not support it natively. See the wiki @@ -424,16 +424,16 @@ See the wiki (https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts) for details, and examples for other shells. -## Piping last command’s output +## Piping last command's output -The key binding *pipe-command-output* can pipe the last command’s +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 +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. @@ -496,10 +496,10 @@ also implemented (and extended, to some degree) by Kitty. It allows querying the terminal for terminfo classic, file-based, terminfo definition. For example, if all applications used this -feature, you would no longer have to install foot’s terminfo on remote +feature, you would no longer have to install foot's terminfo on remote hosts you SSH into. -XTerm’s implementation (as of XTerm-370) only supports querying key +XTerm's implementation (as of XTerm-370) only supports querying key (as in keyboard keys) capabilities, and three custom capabilities: - TN - terminal name @@ -511,7 +511,7 @@ Kitty has extended this, and also supports querying all integer and string capabilities. Foot supports this, and extends it even further, to also include -boolean capabilities. This means foot’s entire terminfo can be queried +boolean capabilities. This means foot's entire terminfo can be queried via *XTGETTCAP*. Note that both Kitty and foot handles responses to multi-capability @@ -522,7 +522,7 @@ capability/value pairs. There are a couple of issues with this: - The success/fail flag in the beginning of the response is always 1 (success), unless the very first queried capability is invalid. -- XTerm will not respond at all to an invalid capability, unless it’s +- XTerm will not respond at all to an invalid capability, unless it's the first one in the XTGETTCAP query. - XTerm will end the response at the first invalid capability. diff --git a/grid.c b/grid.c index e7cdedc5..03ceb0ec 100644 --- a/grid.c +++ b/grid.c @@ -17,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 @@ -606,7 +606,7 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, /* * 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. */ } @@ -616,7 +616,7 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, return new_row; /* - * URI ranges are per row. Thus, we need to ‘close’ the still-open + * URI ranges are per row. Thus, we need to 'close' the still-open * ranges on the previous row, and re-open them on the * next/current row. */ @@ -796,7 +796,7 @@ grid_resize_and_reflow( } if (!old_row->linebreak && col_count > 0) { - /* Don’t truncate logical lines */ + /* Don't truncate logical lines */ col_count = old_cols; } @@ -885,8 +885,8 @@ grid_resize_and_reflow( 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; @@ -895,7 +895,7 @@ grid_resize_and_reflow( * 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. */ @@ -1004,9 +1004,9 @@ grid_resize_and_reflow( { /* * 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). @@ -1033,7 +1033,7 @@ grid_resize_and_reflow( xassert(old_rows == 0 || *next_tp == &terminator); #if defined(_DEBUG) - /* Verify all URI ranges have been “closed” */ + /* Verify all URI ranges have been "closed" */ for (int r = 0; r < new_rows; r++) { const struct row *row = new_grid[r]; @@ -1077,7 +1077,7 @@ grid_resize_and_reflow( grid->num_cols = new_cols; /* - * Set new viewport, making sure it’s not too far down. + * Set new viewport, making sure it's not too far down. * * This is done by using scrollback-start relative cooardinates, * and bounding the new viewport to (grid_rows - screen_rows). @@ -1143,7 +1143,7 @@ grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id) 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; } @@ -1182,7 +1182,7 @@ grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id) 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); @@ -1319,7 +1319,7 @@ grid_row_uri_range_erase(struct row *row, int start, int end) 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 */ @@ -1402,11 +1402,11 @@ UNITTEST * The insertion logic typically triggers an xrealloc(), which, in * some cases, *moves* the entire URI vector to a new base * address. grid_row_uri_range_erase() did not account for this, - * and tried to update the ‘end’ member in the URI range we just + * and tried to update the 'end' member in the URI range we just * split. This causes foot to crash when the xrealloc() has moved * the URI range vector. * - * (note: we’re only verifying we don’t crash here, hence the lack + * (note: we're only verifying we don't crash here, hence the lack * of assertions). */ free(row_data.uri_ranges.v); diff --git a/input.c b/input.c index 166ad70b..cfbd63b3 100644 --- a/input.c +++ b/input.c @@ -416,7 +416,7 @@ execute_binding(struct seat *seat, struct terminal *term, 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; @@ -979,19 +979,19 @@ legacy_kbd_protocol(struct seat *seat, struct terminal *term, if (term->modify_other_keys_2) { /* - * Try to mimic XTerm’s behavior, when holding shift: + * Try to mimic XTerm's behavior, when holding shift: * * - if other modifiers are pressed (e.g. Alt), emit a CSI escape * - upper-case symbols A-Z are encoded as an CSI escape - * - other upper-case symbols (e.g ‘Ö’) or emitted as is + * - other upper-case symbols (e.g 'Ö') or emitted as is * - non-upper cased symbols are _mostly_ emitted as is (foot * always emits as is) * * Examples (assuming Swedish layout): - * - Shift-a (‘A’) emits a CSI - * - Shift-, (‘;’) emits ‘;’ + * - Shift-a ('A') emits a CSI + * - Shift-, (';') emits ';' * - Shift-Alt-, (Alt-;) emits a CSI - * - Shift-ö (‘Ö’) emits ‘Ö’ + * - Shift-ö ('Ö') emits 'Ö' */ /* Any modifiers, besides shift active? */ @@ -1004,9 +1004,9 @@ legacy_kbd_protocol(struct seat *seat, struct terminal *term, seat->kbd.xkb_state, ctx->key); /* - * Get pressed key’s base symbol. - * - for ‘A’ (shift-a), that’s ‘a’ - * - for ‘;’ (shift-,), that’s ‘,’ + * Get pressed key's base symbol. + * - for 'A' (shift-a), that's 'a' + * - for ';' (shift-,), that's ',' */ const xkb_keysym_t *base_syms = NULL; size_t base_count = xkb_keymap_key_get_syms_by_level( @@ -1142,7 +1142,7 @@ kitty_kbd_protocol(struct seat *seat, struct terminal *term, 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); @@ -1287,32 +1287,32 @@ emit_escapes: * * 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); @@ -1324,7 +1324,7 @@ emit_escapes: 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++) { @@ -1371,7 +1371,7 @@ emit_escapes: 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'; @@ -2032,7 +2032,7 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, * event with a NULL surface - see wl_pointer_enter(). * * In this case, we never set seat->mouse_focus (since we - * can’t map the enter event to a specific window). */ + * can't map the enter event to a specific window). */ return; } @@ -2141,7 +2141,7 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, if (cursor_is_on_new_cell) { /* Prevent multiple/different mouse bindings from - * triggering if the mouse has moved “too much” (to + * triggering if the mouse has moved "too much" (to * another cell) */ seat->mouse.count = 0; } @@ -2161,14 +2161,14 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, if (!term->is_searching) { if (auto_scroll_direction != SELECTION_SCROLL_NOT) { /* - * Start ‘selection auto-scrolling’ + * Start 'selection auto-scrolling' * * The speed of the scrolling is proportional to the * distance between the mouse and the grid; the * further away the mouse is, the faster we scroll. * - * Note that the speed is measured in ‘intervals (in - * ns) between each timed scroll of a single line’. + * Note that the speed is measured in 'intervals (in + * ns) between each timed scroll of a single line'. * * Thus, the further away the mouse is, the smaller * interval value we use. @@ -2375,7 +2375,7 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, * clicking twice, waiting for the CSD timer, and finally * clicking once more, results in the following sequence * (keyboard and other irrelevant events filtered out, unless - * they’re needed to prove a point): + * they're needed to prove a point): * * dbg: input.c:1551: cancelling drag timer, moving window * dbg: input.c:759: keyboard_leave: keyboard=0x607000003580, serial=873, surface=0x6070000036d0 @@ -2407,12 +2407,12 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, * - GNOME does *not* send a pointer *enter* event after the drag * has stopped * - The second drag does *not* generate a pointer *leave* event - * - The missing leave event means we’re still tracking LMB as + * - The missing leave event means we're still tracking LMB as * being held down in our seat struct. * - This leads to an assert (debug builds) when LMB is clicked - * again (seat’s button list already contains LMB). + * again (seat's button list already contains LMB). * - * Note: I’ve also observed variants of the above + * Note: I've also observed variants of the above */ tll_foreach(seat->mouse.buttons, it) { if (it->item.button == button) { diff --git a/input.h b/input.h index 001d116f..2ea1c6a9 100644 --- a/input.h +++ b/input.h @@ -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. diff --git a/key-binding.c b/key-binding.c index a7738da8..1c131e72 100644 --- a/key-binding.c +++ b/key-binding.c @@ -243,27 +243,27 @@ maybe_repair_key_combo(const struct seat *seat, * modifier, and replace the shifted symbol with its unshifted * variant. * - * For example, the combo is “Control+Shift+U”. In this case, - * Shift is the modifier used to “shift” ‘u’ to ‘U’, after which - * ‘Shift’ will have been “consumed”. Since we filter out consumed + * For example, the combo is "Control+Shift+U". In this case, + * Shift is the modifier used to "shift" 'u' to 'U', after which + * 'Shift' will have been "consumed". Since we filter out consumed * modifiers when matching key combos, this key combo will never - * trigger (we will never be able to match the ‘Shift’ modifier). + * trigger (we will never be able to match the 'Shift' modifier). * * There are two correct variants of the above key combo: - * - “Control+U” (upper case ‘U’) - * - “Control+Shift+u” (lower case ‘u’) + * - "Control+U" (upper case 'U') + * - "Control+Shift+u" (lower case 'u') * * What we do here is, for each key *code*, check if there are any - * (shifted) levels where it produces ‘sym’. If there are, check + * (shifted) levels where it produces 'sym'. If there are, check * *which* sets of modifiers are needed to produce it, and compare - * with ‘mods’. + * with 'mods'. * - * If there is at least one common modifier, it means ‘sym’ is a - * “shifted” symbol, with the corresponding shifting modifier + * If there is at least one common modifier, it means 'sym' is a + * "shifted" symbol, with the corresponding shifting modifier * explicitly included in the key combo. I.e. the key combo will * never trigger. * - * We then proceed and “repair” the key combo by replacing ‘sym’ + * We then proceed and "repair" the key combo by replacing 'sym' * with the corresponding unshifted symbol. * * To reduce the noise, we ignore all key codes where the shifted @@ -283,7 +283,7 @@ maybe_repair_key_combo(const struct seat *seat, seat->kbd.xkb_keymap, code, layout_idx, 0, &base_syms); if (base_count == 0 || sym == base_syms[0]) { - /* No unshifted symbols, or unshifted symbol is same as ‘sym’ */ + /* No unshifted symbols, or unshifted symbol is same as 'sym' */ continue; } @@ -313,7 +313,7 @@ maybe_repair_key_combo(const struct seat *seat, seat->kbd.xkb_keymap, code, layout_idx, level_idx, mod_masks, ALEN(mod_masks)); - /* Check if key combo’s modifier set intersects */ + /* Check if key combo's modifier set intersects */ for (size_t j = 0; j < mod_mask_count; j++) { if ((mod_masks[j] & mods) != mod_masks[j]) continue; @@ -359,19 +359,19 @@ key_cmp(struct key_binding a, struct key_binding b) * Sort bindings such that bindings with the same symbol are * sorted with the binding having the most modifiers comes first. * - * This fixes an issue where the “wrong” key binding are triggered - * when used with “consumed” modifiers. + * This fixes an issue where the "wrong" key binding are triggered + * when used with "consumed" modifiers. * * For example: if Control+BackSpace is bound before * Control+Shift+BackSpace, then the latter binding is never * triggered. * * Why? Because Shift is a consumed modifier. This means - * Control+BackSpace is “the same” as Control+Shift+BackSpace. + * Control+BackSpace is "the same" as Control+Shift+BackSpace. * * By sorting bindings with more modifiers first, we work around - * the problem. But note that it is *just* a workaround, and I’m - * not confident there aren’t cases where it doesn’t work. + * the problem. But note that it is *just* a workaround, and I'm + * not confident there aren't cases where it doesn't work. * * See https://codeberg.org/dnkl/foot/issues/1280 */ diff --git a/kitty-keymap.h b/kitty-keymap.h index ae911c4f..3420d01f 100644 --- a/kitty-keymap.h +++ b/kitty-keymap.h @@ -13,7 +13,7 @@ struct kitty_key_data { _Static_assert(sizeof(struct kitty_key_data) == 7, "bad size"); -/* Note! *Must* Be kept sorted (on ‘sym’) */ +/* Note! *Must* Be kept sorted (on 'sym') */ static const struct kitty_key_data kitty_keymap[] = { {XKB_KEY_ISO_Level3_Shift, 57453, 'u', true}, {XKB_KEY_ISO_Level5_Shift, 57454, 'u', true}, diff --git a/main.c b/main.c index 98db9c38..8b4d2715 100644 --- a/main.c +++ b/main.c @@ -427,7 +427,7 @@ main(int argc, char *const *argv) /* * 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++) { diff --git a/notify.c b/notify.c index 8180477d..04427477 100644 --- a/notify.c +++ b/notify.c @@ -20,7 +20,7 @@ notify_notify(const struct terminal *term, const char *title, const char *body) 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; } diff --git a/osc.c b/osc.c index 5da8666f..1ea61a3e 100644 --- a/osc.c +++ b/osc.c @@ -353,7 +353,7 @@ parse_rgb(const char *string, uint32_t *color, bool *_have_alpha, return false; } - /* Verify prefix is “rgb:” or “rgba:” */ + /* Verify prefix is "rgb:" or "rgba:" */ if (have_alpha) { if (strncmp(string, "rgba:", 5) != 0) return false; @@ -443,9 +443,9 @@ osc_uri(struct terminal *term, char *string) /* * \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 ════╗ diff --git a/pgo/full-headless-cage.sh b/pgo/full-headless-cage.sh index eacb0c33..50fc7509 100755 --- a/pgo/full-headless-cage.sh +++ b/pgo/full-headless-cage.sh @@ -10,5 +10,5 @@ trap "rm -rf '${runtime_dir}'" EXIT INT HUP TERM XDG_RUNTIME_DIR="${runtime_dir}" WLR_RENDERER=pixman WLR_BACKENDS=headless cage "${srcdir}"/pgo/full-inner.sh "${srcdir}" "${blddir}" -# Cage’s exit code doesn’t reflect our script’s exit code +# Cage's exit code doesn't reflect our script's exit code [ -f "${blddir}"/pgo-ok ] || exit 1 diff --git a/pgo/full-headless-sway.sh b/pgo/full-headless-sway.sh index 48dbcb94..524bf42b 100755 --- a/pgo/full-headless-sway.sh +++ b/pgo/full-headless-sway.sh @@ -17,8 +17,8 @@ trap cleanup EXIT INT HUP TERM # Generate a custom config that executes our generate-pgo-data script > "${sway_conf}" echo "exec '${srcdir}'/pgo/full-headless-sway-inner.sh '${srcdir}' '${blddir}'" -# Run Sway. full-headless-sway-inner.sh ends with a ‘swaymsg exit’ +# 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 --git a/pgo/pgo.sh b/pgo/pgo.sh index f782a9f8..b59f5c21 100755 --- a/pgo/pgo.sh +++ b/pgo/pgo.sh @@ -98,8 +98,8 @@ if [ ${do_pgo} = yes ]; then ninja -C "${blddir}" # If fcft/tllist are subprojects, we need to ensure their tests - # have been executed, or we’ll get “profile count data file not - # found” errors. + # have been executed, or we'll get "profile count data file not + # found" errors. ninja -C "${blddir}" test # Run mode-dependent script to generate profiling data diff --git a/render.c b/render.c index 5d2ec6bc..2bfe36e8 100644 --- a/render.c +++ b/render.c @@ -269,12 +269,12 @@ color_dim(const struct terminal *term, uint32_t color) 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]; } } @@ -1073,7 +1073,7 @@ grid_render_scroll(struct terminal *term, struct buffer *buf, /* * 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 +1150,7 @@ grid_render_scroll_reverse(struct terminal *term, struct buffer *buf, /* * 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 +1288,7 @@ render_sixel(struct terminal *term, pixman_image_t *pix, * If the last sixel row only partially covers the cell row, * 'erase' the cell by rendering them. * - * In all cases, do *not* clear the ‘dirty’ bit on the row, to + * In all cases, do *not* clear the 'dirty' bit on the row, to * ensure the regular renderer includes them in the damage * rect. */ @@ -1403,8 +1403,8 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, { /* Cursor will be drawn *after* the pre-edit string, i.e. in * the cell *after*. This means we need to copy, and dirty, - * one extra cell from the original grid, or we’ll leave - * trailing “cursors” after us if the user deletes text while + * one extra cell from the original grid, or we'll leave + * trailing "cursors" after us if the user deletes text while * pre-editing */ cells_needed++; } @@ -1613,23 +1613,23 @@ render_overlay(struct terminal *term) * When possible, we only update the areas that have *changed* * since the last frame. That means: * - * - clearing/erasing cells that are now selected, but weren’t + * - clearing/erasing cells that are now selected, but weren't * in the last frame - * - dimming cells that were selected, but aren’t anymore + * - dimming cells that were selected, but aren't anymore * - * To do this, we save the last frame’s selected cells as a + * To do this, we save the last frame's selected cells as a * pixman region. * * Then, we calculate the corresponding region for this - * frame’s selected cells. + * frame's selected cells. * - * Last frame’s region minus this frame’s region gives us the + * Last frame's region minus this frame's region gives us the * region that needs to be *dimmed* in this frame * - * This frame’s region minus last frame’s region gives us the + * This frame's region minus last frame's region gives us the * region that needs to be *cleared* in this frame. * - * Finally, the union of the two “diff” regions above, gives + * Finally, the union of the two "diff" regions above, gives * us the total region affected by a change, in either way. We * use this as the bounding box for the * wl_surface_damage_buffer() call. @@ -1642,12 +1642,12 @@ render_overlay(struct terminal *term) 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 +2072,7 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, } /* - * The “visible” border. + * The "visible" border. */ float scale = term->scale; @@ -2692,21 +2692,21 @@ reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old pixman_region32_init(&dirty); /* - * Figure out current frame’s damage region + * Figure out current frame's damage region * - * If current frame doesn’t have any scroll damage, we can simply - * subtract this frame’s damage from the last frame’s damage. That - * way, we don’t have to copy areas from the old frame that’ll + * If current frame doesn't have any scroll damage, we can simply + * subtract this frame's damage from the last frame's damage. That + * way, we don't have to copy areas from the old frame that'll * just get overwritten by current frame. * - * Note that this is row based. A “half damaged” row is not + * Note that this is row based. A "half damaged" row is not * excluded. I.e. the entire row will be copied from the old frame * to the new, and then when actually rendering the new frame, the * updated cells will overwrite parts of the copied row. * - * Since we’re scanning the entire viewport anyway, we also track + * Since we're scanning the entire viewport anyway, we also track * whether *all* cells are to be updated. In this case, just force - * a full re-rendering, and don’t copy anything from the old + * a full re-rendering, and don't copy anything from the old * frame. */ bool full_repaint_needed = true; @@ -2739,28 +2739,28 @@ reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old } /* - * TODO: re-apply last frame’s scroll damage + * TODO: re-apply last frame's scroll damage * * We used to do this, but it turned out to be buggy. If we decide - * to re-add it, this is where to do it. Note that we’d also have + * to re-add it, this is where to do it. Note that we'd also have * to remove the updates to buf->dirty from grid_render_scroll() * and grid_render_scroll_reverse(). */ if (tll_length(term->grid->scroll_damage) == 0) { /* - * We can only subtract current frame’s damage from the old - * frame’s if we don’t have any scroll damage. + * We can only subtract current frame's damage from the old + * frame's if we don't have any scroll damage. * * If we do have scroll damage, the damage region we * calculated above is not (yet) valid - we need to apply the - * current frame’s scroll damage *first*. This is done later, + * current frame's scroll damage *first*. This is done later, * when rendering the frame. */ pixman_region32_subtract(&dirty, &old->dirty[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]); } @@ -2819,7 +2819,7 @@ grid_render(struct terminal *term) struct buffer_chain *chain = term->render.chains.grid; struct buffer *buf = shm_get_buffer(chain, term->width, term->height); - /* Dirty old and current cursor cell, to ensure they’re repainted */ + /* Dirty old and current cursor cell, to ensure they're repainted */ dirty_old_cursor(term); dirty_cursor(term); @@ -2938,11 +2938,11 @@ grid_render(struct terminal *term) * they are overflowing. * * As soon as we see a non-overflowing cell we can - * stop, since it isn’t affecting the string of + * stop, since it isn't affecting the string of * overflowing glyphs that follows it. * * As soon as we see a dirty cell, we can stop, since - * that means we’ve already handled it (remember the + * that means we've already handled it (remember the * outer loop goes from left to right). */ for (struct cell *c = cell - 1; c >= &row->cells[0]; c--) { @@ -2960,9 +2960,9 @@ grid_render(struct terminal *term) * Note that the first non-overflowing cell must be * re-rendered as well, but any cell *after* that is * unaffected by the string of overflowing glyphs - * we’re dealing with right now. + * we're dealing with right now. * - * For performance, this iterates the *outer* loop’s + * For performance, this iterates the *outer* loop's * cell pointer - no point in re-checking all these * glyphs again, in the outer loop. */ @@ -3137,9 +3137,9 @@ render_search_box(struct terminal *term) /* * We treat the search box pretty much like a row of cells. That - * is, a glyph is either 1 or 2 (or more) “cells” wide. + * is, a glyph is either 1 or 2 (or more) "cells" wide. * - * The search ‘length’, and ‘cursor’ (position) is in + * The search 'length', and 'cursor' (position) is in * *characters*, not cells. This means we need to translate from * character count to cell count when calculating the length of * the search box, where in the search string we should start @@ -3307,7 +3307,7 @@ render_search_box(struct terminal *term) } /* - * Render the search string, starting at ‘glyph_offset’. Note that + * Render the search string, starting at 'glyph_offset'. Note that * glyph_offset is in cells, not characters */ for (size_t i = 0, @@ -3580,7 +3580,7 @@ render_urls(struct terminal *term) int x = col * term->cell_width - 15 * term->cell_width / 10; int y = row * term->cell_height - 5 * term->cell_height / 10; - /* Don’t position it outside our window */ + /* Don't position it outside our window */ if (x < -term->margins.left) x = -term->margins.left; if (y < -term->margins.top) @@ -3621,12 +3621,12 @@ render_urls(struct terminal *term) label[i] = U' '; /* - * Don’t extend outside our window + * Don't extend outside our window * - * Truncate label so that it doesn’t extend outside our + * Truncate label so that it doesn't extend outside our * window. * - * Do it in a way such that we don’t cut the label in the + * Do it in a way such that we don't cut the label in the * middle of a double-width character. */ @@ -3805,7 +3805,7 @@ delayed_reflow_of_normal_grid(struct terminal *term) 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 +3868,7 @@ send_dimensions_to_client(struct terminal *term) win->resize_timeout_fd = -1; } } else { - /* Send new dimensions to client “in a while” */ + /* Send new dimensions to client "in a while" */ assert(win->is_resizing && term->conf->resize_delay_ms > 0); int fd = win->resize_timeout_fd; @@ -4083,8 +4083,8 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) /* - * 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(). * @@ -4095,7 +4095,7 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) 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; @@ -4106,7 +4106,7 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) 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); } @@ -4116,12 +4116,12 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) /* * 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). * @@ -4163,7 +4163,7 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) 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) { @@ -4175,7 +4175,7 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) /* * 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 */ @@ -4195,7 +4195,7 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) 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); diff --git a/selection.c b/selection.c index 9e9bbb10..7540283b 100644 --- a/selection.c +++ b/selection.c @@ -858,8 +858,8 @@ pixman_region_for_coords_block(const struct terminal *term, return region; } -/* Returns a pixman region representing the selection between ‘start’ - * and ‘end’ (given the current selection kind), in *scrollback +/* Returns a pixman region representing the selection between 'start' + * and 'end' (given the current selection kind), in *scrollback * relative coordinates* */ static pixman_region32_t pixman_region_for_coords(const struct terminal *term, @@ -921,17 +921,17 @@ mark_selected_region(struct terminal *term, pixman_box32_t *boxes, * followed by non-empty cell(s), since this * corresponds to what gets extracted when the * selection is copied (that is, empty cells - * “between” non-empty cells are converted to + * "between" non-empty cells are converted to * spaces). * * However, they way we handle selection updates - * (diffing the “old” selection area against the - * “new” one, using pixman regions), means we - * can’t correctly update the state of empty - * cells. The result is “random” empty cells being - * rendered as selected when they shouldn’t. + * (diffing the "old" selection area against the + * "new" one, using pixman regions), means we + * can't correctly update the state of empty + * cells. The result is "random" empty cells being + * rendered as selected when they shouldn't. * - * “Fix” by *never* highlighting selected empty + * "Fix" by *never* highlighting selected empty * cells (they still get converted to spaces when * copied, if followed by non-empty cells). */ @@ -944,8 +944,8 @@ mark_selected_region(struct terminal *term, pixman_box32_t *boxes, * * This is due to how the algorithm for updating * the selection works; it uses regions to - * calculate the difference between the “old” and - * the “new” selection. This makes it impossible + * calculate the difference between the "old" and + * the "new" selection. This makes it impossible * to tell if an empty cell is a *trailing* empty * cell (that should not be highlighted), or an * empty cells between non-empty cells (that @@ -957,7 +957,7 @@ mark_selected_region(struct terminal *term, pixman_box32_t *boxes, * empty cell is trailing or not. * * So, what we need to do is check if a - * ‘selected’, and empty cell has been marked as + * 'selected', and empty cell has been marked as * selected, temporarily unmark (forcing it dirty, * to ensure it gets re-rendered). If it is *not* * a trailing empty cell, it will get re-tagged as @@ -1042,7 +1042,7 @@ set_pivot_point_for_block_and_char_wise(struct terminal *term, *pivot_start = start; - /* First, make sure ‘start’ isn’t in the middle of a + /* First, make sure 'start' isn't in the middle of a * multi-column character */ while (true) { const struct row *row = term->grid->rows[pivot_start->row & (term->grid->num_rows - 1)]; @@ -1051,7 +1051,7 @@ set_pivot_point_for_block_and_char_wise(struct terminal *term, if (cell->wc < CELL_SPACER) break; - /* Multi-column chars don’t cross rows */ + /* Multi-column chars don't cross rows */ xassert(pivot_start->col > 0); if (pivot_start->col == 0) break; @@ -1876,7 +1876,7 @@ cancelled(void *data, struct wl_data_source *wl_data_source) clipboard->text = NULL; } -/* We don’t support dragging *from* */ +/* We don't support dragging *from* */ static void dnd_drop_performed(void *data, struct wl_data_source *wl_data_source) { @@ -2218,11 +2218,11 @@ fdm_receive(struct fdm *fdm, int fd, int events, void *data) /* * 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 @@ -2716,7 +2716,7 @@ enter(void *data, struct wl_data_device *wl_data_device, uint32_t serial, reject_offer: /* Either terminal is already busy sending paste data, or mouse - * pointer isn’t over the grid */ + * pointer isn't over the grid */ seat->clipboard.window = NULL; wl_data_offer_accept(offer, serial, NULL); wl_data_offer_set_actions( @@ -2812,7 +2812,7 @@ drop(void *data, struct wl_data_device *wl_data_device) 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 --git a/shm.c b/shm.c index 87bd33a7..7959a84b 100644 --- a/shm.c +++ b/shm.c @@ -511,7 +511,7 @@ get_new_buffers(struct buffer_chain *chain, size_t count, #endif if (!(bufs[0] && shm_can_scroll(bufs[0]))) { - /* We only need to keep the pool FD open if we’re going to SHM + /* We only need to keep the pool FD open if we're going to SHM * scroll it */ close(pool_fd); pool->fd = -1; @@ -579,7 +579,7 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height) 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 +589,8 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height) * TODO: I think we _can_ use shm_unref() * here... * - * shm_unref() may remove ‘it’, but that - * should be safe; “our” tll_foreach() already + * shm_unref() may remove 'it', but that + * should be safe; "our" tll_foreach() already * holds the next pointer. */ if (buffer_unref_no_remove_from_chain(buf)) diff --git a/shm.h b/shm.h index f9e90a23..7d1796bf 100644 --- a/shm.h +++ b/shm.h @@ -49,7 +49,7 @@ void shm_chain_free(struct buffer_chain *chain); /* * Returns a single buffer. * - * May returned a cached buffer. If so, the buffer’s age indicates how + * 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. * @@ -57,7 +57,7 @@ void shm_chain_free(struct buffer_chain *chain); */ struct buffer *shm_get_buffer(struct buffer_chain *chain, int width, int height); /* - * Returns many buffers, described by ‘info’, all sharing the same SHM + * Returns many buffers, described by 'info', all sharing the same SHM * buffer pool. * * Never returns cached buffers. However, the newly created buffers diff --git a/sixel.c b/sixel.c index 8a0b130f..c046145f 100644 --- a/sixel.c +++ b/sixel.c @@ -979,7 +979,7 @@ sixel_reflow_grid(struct terminal *term, struct grid *grid) struct grid *active_grid = term->grid; term->grid = grid; - /* Need the “real” list to be empty from the beginning */ + /* Need the "real" list to be empty from the beginning */ tll(struct sixel) copy = tll_init(); tll_foreach(grid->sixel_images, it) tll_push_back(copy, it->item); @@ -1028,7 +1028,7 @@ sixel_reflow_grid(struct terminal *term, struct grid *grid) continue; } - /* Sixels that didn’t overlap may now do so, which isn’t + /* Sixels that didn't overlap may now do so, which isn't * allowed of course */ _sixel_overwrite_by_rectangle( term, six->pos.row, six->pos.col, six->rows, six->cols, @@ -1200,8 +1200,8 @@ sixel_unhook(struct terminal *term) * 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 + * 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. @@ -1580,8 +1580,8 @@ decsixel_generic(struct terminal *term, uint8_t c) case '$': if (likely(term->sixel.pos.col <= term->sixel.max_width)) { /* - * We set, and keep, ‘col’ outside the image boundary when - * we’ve reached the maximum image height, to avoid also + * We set, and keep, 'col' outside the image boundary when + * we've reached the maximum image height, to avoid also * having to check the row vs image height in the common * path in sixel_add(). */ @@ -1775,12 +1775,12 @@ decgci(struct terminal *term, uint8_t c) int sat = min(c3, 100); /* - * Sixel’s HLS use the following primary color hues: + * Sixel's HLS use the following primary color hues: * blue: 0° * red: 120° * green: 240° * - * While “standard” HSL uses: + * While "standard" HSL uses: * red: 0° * green: 120° * blue: 240° diff --git a/terminal.c b/terminal.c index 5d6c956d..ed13073e 100644 --- a/terminal.c +++ b/terminal.c @@ -260,8 +260,8 @@ fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) if (unlikely(term->interactive_resizing.grid != NULL)) { /* - * Don’t consume PTMX while we’re doing an interactive resize, - * since the ‘normal’ grid we’re currently using is a + * Don't consume PTMX while we're doing an interactive resize, + * since the 'normal' grid we're currently using is a * temporary one - all changes done to it will be lost when * the interactive resize ends. */ @@ -817,7 +817,7 @@ get_font_dpi(const struct terminal *term) * 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). */ @@ -1092,8 +1092,8 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, goto close_fds; } - /* Need to register *very* early (before the first “goto err”), to - * ensure term_destroy() doesn’t unref a key-binding we haven’t + /* Need to register *very* early (before the first "goto err"), to + * ensure term_destroy() doesn't unref a key-binding we haven't * yet ref:d */ key_binding_new_for_conf(wayl->key_binding_manager, wayl, conf); @@ -1340,11 +1340,11 @@ term_window_configured(struct terminal *term) * * A foot instance can be terminated in two ways: * - * - the client application terminates (user types ‘exit’, or pressed C-d in the + * - the client application terminates (user types 'exit', or pressed C-d in the * shell, etc) * - the foot window is closed * - * Both variants need to trigger to “other” action. I.e. if the client + * Both variants need to trigger to "other" action. I.e. if the client * application is terminated, then we need to close the window. If the window is * closed, we need to terminate the client application. * @@ -1361,7 +1361,7 @@ term_window_configured(struct terminal *term) * - fdm_client_terminated(): reaper callback, called when the client * application has terminated. * - * + Kills the “terminate” timeout timer + * + Kills the "terminate" timeout timer * + Calls shutdown_maybe_done() if the shutdown procedure has already * started (i.e. the window being closed initiated the shutdown) * -OR- @@ -1369,18 +1369,18 @@ term_window_configured(struct terminal *term) * application termination initiated the shutdown). * * - term_shutdown(): unregisters all FDM callbacks, sends SIGTERM to the client - * application and installs a “terminate” timeout timer (if it hasn’t already + * application and installs a "terminate" timeout timer (if it hasn't already * terminated). Finally registers an event FD with the FDM, which is * immediately triggered. This is done to ensure any pending FDM events are * handled before shutting down. * * - fdm_shutdown(): FDM callback, triggered by the event FD in * term_shutdown(). Unmaps and destroys the window resources, and ensures the - * seats’ focused pointers don’t reference us. Finally calls + * seats' focused pointers don't reference us. Finally calls * shutdown_maybe_done(). * - * - fdm_terminate_timeout(): FDM callback for the “terminate” timeout - * timer. This function is called when the client application hasn’t + * - fdm_terminate_timeout(): FDM callback for the "terminate" timeout + * timer. This function is called when the client application hasn't * terminated after 60 seconds (after the SIGTERM). Sends SIGKILL to the * client application. * @@ -1391,7 +1391,7 @@ term_window_configured(struct terminal *term) * It may however also be called without term_shutdown() having been called * (typically in error code paths - for example, when the Wayland connection * is closed by the compositor). In this case, the client application is - * typically still running, and we can’t assume the FDM is running. To handle + * typically still running, and we can't assume the FDM is running. To handle * this, we install configure a 60 second SIGALRM, send SIGTERM to the client * application, and then enter a blocking waitpid(). * @@ -1715,7 +1715,7 @@ term_destroy(struct terminal *term) 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; @@ -1729,7 +1729,7 @@ term_destroy(struct terminal *term) kill(-term->slave, SIGTERM); /* - * 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 @@ -2086,10 +2086,10 @@ 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 @@ -2098,7 +2098,7 @@ term_update_scale(struct terminal *term) * - 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 @@ -2260,7 +2260,7 @@ term_damage_scroll(struct terminal *term, enum damage_type damage_type, dmg->region.start == region.start && dmg->region.end == region.end)) { - /* Make sure we don’t overflow... */ + /* Make sure we don't overflow... */ int new_line_count = (int)dmg->lines + lines; if (likely(new_line_count <= UINT16_MAX)) { dmg->lines = new_line_count; @@ -2324,14 +2324,14 @@ term_erase_scrollback(struct terminal *term) if (sel_end >= 0) { /* * Cancel selection if it touches any of the rows in the - * scrollback, since we can’t have the selection reference + * scrollback, since we can't have the selection reference * soon-to-be deleted rows. * * This is done by range checking the selection range against * the scrollback range. * * To make this comparison simpler, the start/end absolute row - * numbers are “rebased” against the scrollback start, where + * numbers are "rebased" against the scrollback start, where * row 0 is the *first* row in the scrollback. A high number * thus means the row is further *down* in the scrollback, * closer to the screen bottom. @@ -3282,7 +3282,7 @@ term_bell(struct terminal *term) if (!wayl_win_set_urgent(term->window)) { /* * Urgency (xdg-activation) is relatively new in - * Wayland. Fallback to our old, “faked”, urgency - + * Wayland. Fallback to our old, "faked", urgency - * rendering our window margins in red */ term->render.urgency = true; @@ -3728,20 +3728,20 @@ term_command_output_to_text(const struct terminal *term, char **text, size_t *le * 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 + * 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 + * 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 */ + /* Command output covers partial row - don't append newline */ return true; } diff --git a/terminal.h b/terminal.h index 35127e3c..9e003502 100644 --- a/terminal.h +++ b/terminal.h @@ -152,8 +152,8 @@ struct sixel { * 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; @@ -346,7 +346,7 @@ struct url { char32_t *key; struct range range; enum url_action action; - bool url_mode_dont_change_url_attr; /* Entering/exiting URL mode doesn’t touch the cells’ attr.url */ + bool url_mode_dont_change_url_attr; /* Entering/exiting URL mode doesn't touch the cells' attr.url */ bool osc8; bool duplicate; }; @@ -382,7 +382,7 @@ struct terminal { bool bracketed_paste; bool focus_events; bool alt_scrolling; - bool modify_other_keys_2; /* True when modifyOtherKeys=2 (i.e. “CSI >4;2m”) */ + bool modify_other_keys_2; /* True when modifyOtherKeys=2 (i.e. "CSI >4;2m") */ enum cursor_origin origin; enum cursor_keys cursor_keys_mode; enum keypad_keys keypad_keys_mode; @@ -644,7 +644,7 @@ struct terminal { } render; struct { - struct grid *grid; /* Original ‘normal’ grid, before resize started */ + struct grid *grid; /* Original 'normal' grid, before resize started */ int old_screen_rows; /* term->rows before resize started */ int old_cols; /* term->cols before resize started */ int old_hide_cursor; /* term->hide_cursor before resize started */ @@ -678,7 +678,7 @@ struct terminal { * 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; diff --git a/url-mode.c b/url-mode.c index 07499794..0dc594f5 100644 --- a/url-mode.c +++ b/url-mode.c @@ -196,7 +196,7 @@ urls_input(struct seat *seat, struct terminal *term, 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. */ @@ -358,7 +358,7 @@ auto_detected(const struct terminal *term, enum url_action action, 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; @@ -410,7 +410,7 @@ auto_detected(const struct terminal *term, enum url_action action, 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++; @@ -557,7 +557,7 @@ remove_overlapping(url_list_t *urls, int cols) (in_start >= out_start && in_end <= out_end)) { /* - * OSC-8 URLs can’t overlap with each + * OSC-8 URLs can't overlap with each * other. * * Similarly, auto-detected URLs cannot overlap with @@ -633,7 +633,7 @@ generate_key_combos(const struct config *conf, xassert(hints_count - offset >= count); - /* Copy slice of ‘hints’ array to the caller provided array */ + /* Copy slice of 'hints' array to the caller provided array */ for (size_t i = 0; i < hints_count; i++) { if (i >= offset && i < offset + count) combos[i - offset] = hints[i]; @@ -642,7 +642,7 @@ generate_key_combos(const struct config *conf, } 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); @@ -709,7 +709,7 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) it->item.key = combos[combo_idx++]; } - /* Free combos we didn’t use up */ + /* Free combos we didn't use up */ for (size_t i = combo_idx; i < count; i++) free(combos[i]); @@ -789,7 +789,7 @@ urls_render(struct terminal *term) } term->render.last_cursor.row = NULL; - /* Clear scroll damage, to ensure we don’t apply it twice (once on + /* Clear scroll damage, to ensure we don't apply it twice (once on * the snapshot:ed grid, and then later again on the real grid) */ tll_free(term->grid->scroll_damage); @@ -833,10 +833,10 @@ urls_reset(struct terminal *term) term->url_grid_snapshot = NULL; /* - * Make sure “last cursor” doesn’t point to a row in the just + * Make sure "last cursor" doesn't point to a row in the just * free:d snapshot grid. * - * Note that it will still be erased properly (if hasn’t already), + * Note that it will still be erased properly (if hasn't already), * since we marked the cell as dirty *before* taking the grid * snapshot. */ diff --git a/vt.c b/vt.c index 0f7bfe63..7529f302 100644 --- a/vt.c +++ b/vt.c @@ -137,12 +137,12 @@ action_execute(struct terminal *term, uint8_t c) /* backspace */ #if 0 /* - * This is the “correct” BS behavior. However, it doesn’t play + * This is the "correct" BS behavior. However, it doesn't play * nicely with bw/auto_left_margin, hence the alternative * implementation below. * - * Note that it breaks vttest “1. Test of cursor movements -> - * Test of autowrap” + * Note that it breaks vttest "1. Test of cursor movements -> + * Test of autowrap" */ term_cursor_left(term, 1); #else @@ -154,7 +154,7 @@ action_execute(struct terminal *term, uint8_t c) likely(term->reverse_wrap && term->auto_margin)) { if (term->grid->cursor.point.row <= term->scroll_region.start) { - /* Don’t wrap past, or inside, the scrolling region(?) */ + /* Don't wrap past, or inside, the scrolling region(?) */ } else term_cursor_to( term, @@ -398,7 +398,7 @@ action_collect(struct terminal *term, uint8_t c) * more. * * As such, we optimize *reading* the private(s), and *resetting* - * them (in action_clear()). Writing is ok if it’s a bit slow. + * them (in action_clear()). Writing is ok if it's a bit slow. */ if ((term->vt.private & 0xff) == 0) @@ -783,7 +783,7 @@ action_utf8_print(struct terminal *term, char32_t wc) /* * 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. */ @@ -920,8 +920,8 @@ action_utf8_33(struct terminal *term, uint8_t c) 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 +960,8 @@ action_utf8_44(struct terminal *term, uint8_t c) 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 --git a/wayland.c b/wayland.c index 271f950e..d3d892df 100644 --- a/wayland.c +++ b/wayland.c @@ -405,7 +405,7 @@ update_term_for_output_change(struct terminal *term) 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_resize() normally shortcuts and returns early). */ @@ -921,7 +921,7 @@ xdg_toplevel_wm_capabilities(void *data, 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 @@ -991,7 +991,7 @@ xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, #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 @@ -2152,7 +2152,7 @@ bool wayl_win_set_urgent(struct wl_window *win) { if (win->urgency_token_is_pending) { - /* We already have a pending token. Don’t request another one, + /* We already have a pending token. Don't request another one, * to avoid flooding the Wayland socket */ return true; } From 9f4eb13e9e202a6df61f40bd28f9d86f17d2ab2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 5 Feb 2024 12:01:40 +0100 Subject: [PATCH 0576/1323] terminfo: smm: enable 8-bit Meta mode To enable 8-bit meta mode, we need to: * disable "send ESC when meta modifies a key" (private mode 1036) * enable "8-bit meta mode" (private mode 1034) rmm reverses the above. Closes #1584 --- foot.info | 2 ++ 1 file changed, 2 insertions(+) diff --git a/foot.info b/foot.info index 89601996..3bab3266 100644 --- a/foot.info +++ b/foot.info @@ -241,6 +241,7 @@ 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, @@ -258,6 +259,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, From 756da873465e188f7b5b3ac1a29c9651d0bf9128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 5 Feb 2024 12:07:47 +0100 Subject: [PATCH 0577/1323] changelog: smm+rmm --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 096d43a3..48cea306 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,11 +77,15 @@ 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]). [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 ### Deprecated From 316136f428603481b78d8f6ca747c666e63fd210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 6 Feb 2024 13:08:43 +0100 Subject: [PATCH 0578/1323] term: ignore attempts to set a title that contains an invalid UTF-8 sequence Setting the title ultimately leads to a call to xdg_toplevel::set_title(). It is a protocol violation to try to set a title that contains an invalid UTF-8 sequence: The string must be encoded in UTF-8. Closes #1552 --- CHANGELOG.md | 3 +++ terminal.c | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48cea306..b3255565 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,10 +102,13 @@ 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]). [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 ### Security diff --git a/terminal.c b/terminal.c index ed13073e..f93e7abc 100644 --- a/terminal.c +++ b/terminal.c @@ -3249,6 +3249,13 @@ term_set_window_title(struct terminal *term, const char *title) 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); From 41dc259744675e93239295286795ca4e8191f2df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 6 Feb 2024 13:38:08 +0100 Subject: [PATCH 0579/1323] doc: foot-ctlseq: add OSC 133 C/D (command output start/end) --- doc/foot-ctlseqs.7.scd | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index d26bb39d..68a54beb 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -687,6 +687,12 @@ All _OSC_ sequences begin with *\\E]*, sometimes abbreviated _OSC_. | \\E] 133 ; A \\E\\ : FinalTerm : Mark start of shell prompt +| \\E] 133 ; C \\E\\ +: FinalTerm +: Mark start of command output +| \\E] 133 ; D \\E\\ +: FinalTerm +: Mark end of command output | \\E] 555 \\E\\ : foot : Flash the entire terminal (foot extension) From 4801d3a305e80b449f041d469432780d7b0cc179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 6 Feb 2024 13:41:09 +0100 Subject: [PATCH 0580/1323] term: drop term->render.title.is_armed This boolean isn't needed. The idea was probably to not re-program the timer unnecessarily, or even to prevent it from being moved forward in time indefinitely. However, the logic has (probably) gone through some changes, that now makes it irrelevant. The timer isn't moved forward indefinitely; it is always set to 8ms from the last title update. The closer we get to that point in time, the smaller the timeout we set. Now, is_armed _did_ prevent the timer from being re-programmed. But that tiny performance tweak isn't really necessary, as the title should, in normal cases, not be set that often anyway. --- render.c | 3 --- terminal.c | 2 -- terminal.h | 1 - 3 files changed, 6 deletions(-) diff --git a/render.c b/render.c index 2bfe36e8..13d348f3 100644 --- a/render.c +++ b/render.c @@ -4508,9 +4508,6 @@ fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data) void render_refresh_title(struct terminal *term) { - if (term->render.title.is_armed) - return; - struct timespec now; if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) return; diff --git a/terminal.c b/terminal.c index f93e7abc..7ace10ef 100644 --- a/terminal.c +++ b/terminal.c @@ -622,7 +622,6 @@ fdm_title_update_timeout(struct fdm *fdm, int fd, int events, void *data) struct itimerspec reset = {{0}}; timerfd_settime(term->render.title.timer_fd, 0, &reset, NULL); - term->render.title.is_armed = false; render_refresh_title(term); return true; @@ -1209,7 +1208,6 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .scrollback_lines = conf->scrollback.lines, .app_sync_updates.timer_fd = app_sync_updates_fd, .title = { - .is_armed = false, .timer_fd = title_update_fd, }, .workers = { diff --git a/terminal.h b/terminal.h index 9e003502..7b743c09 100644 --- a/terminal.h +++ b/terminal.h @@ -603,7 +603,6 @@ struct terminal { struct { struct timespec last_update; - bool is_armed; int timer_fd; } title; From 6c56b04b3f312be8dcee307e29515f3c83b3ae87 Mon Sep 17 00:00:00 2001 From: delthas Date: Mon, 4 Sep 2023 14:02:05 +0200 Subject: [PATCH 0581/1323] osc: add support for osc 176 (app ID) This adds support for a new OSC escape sequence: OSC 176, that lets terminal programs tell the terminal the name of the app that is running. foot then sets the app ID of the toplevel to that ID, which lets the compositor know which app is running, and typically sets the appropriate icon, window grouping, ... See: https://gist.github.com/delthas/d451e2cc1573bb2364839849c7117239 --- README.md | 1 + doc/foot-ctlseqs.7.scd | 5 ++++ notify.c | 2 +- osc.c | 4 +++ render.c | 22 +++++++++++++++ render.h | 1 + terminal.c | 61 +++++++++++++++++++++++++++++++++++++++++- terminal.h | 7 +++++ 8 files changed, 101 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5c8d3878..75e10889 100644 --- a/README.md +++ b/README.md @@ -536,6 +536,7 @@ with the terminal emulator itself. Foot implements the following OSCs: * `OSC 117` - reset highlight background color * `OSC 119` - reset highlight foreground color * `OSC 133` - [shell integration](#shell-integration) +* `OSC 176` - set app ID * `OSC 555` - flash screen (**foot specific**) * `OSC 777` - desktop notification (only the `;notify` sub-command of OSC 777 is supported.) diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index 68a54beb..64c56d5d 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -693,6 +693,11 @@ All _OSC_ sequences begin with *\\E]*, sometimes abbreviated _OSC_. | \\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 --git a/notify.c b/notify.c index 04427477..7a208479 100644 --- a/notify.c +++ b/notify.c @@ -36,7 +36,7 @@ notify_notify(const struct terminal *term, const char *title, const char *body) 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 --git a/osc.c b/osc.c index 1ea61a3e..28e93e51 100644 --- a/osc.c +++ b/osc.c @@ -916,6 +916,10 @@ osc_dispatch(struct terminal *term) } break; + case 176: + term_set_app_id(term, string); + break; + case 555: osc_flash(term); break; diff --git a/render.c b/render.c index 13d348f3..f82e54f0 100644 --- a/render.c +++ b/render.c @@ -4529,6 +4529,28 @@ render_refresh_title(struct terminal *term) render_refresh_csd(term); } +void +render_refresh_app_id(struct terminal *term) +{ + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) + return; + + struct timespec diff; + timespec_sub(&now, &term->render.app_id.last_update, &diff); + + if (diff.tv_sec == 0 && diff.tv_nsec < 8333 * 1000) { + const struct itimerspec timeout = { + .it_value = {.tv_nsec = 8333 * 1000 - diff.tv_nsec}, + }; + + timerfd_settime(term->render.app_id.timer_fd, 0, &timeout, NULL); + } 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) { diff --git a/render.h b/render.h index 78ebae40..cfedf311 100644 --- a/render.h +++ b/render.h @@ -21,6 +21,7 @@ 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 --git a/terminal.c b/terminal.c index 7ace10ef..bc7dc428 100644 --- a/terminal.c +++ b/terminal.c @@ -627,6 +627,30 @@ fdm_title_update_timeout(struct fdm *fdm, int fd, int events, void *data) 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) { @@ -1050,6 +1074,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, int delay_upper_fd = -1; int app_sync_updates_fd = -1; int title_update_fd = -1; + int app_id_update_fd = -1; struct terminal *term = malloc(sizeof(*term)); if (unlikely(term == NULL)) { @@ -1084,6 +1109,12 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, 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) { @@ -1114,7 +1145,8 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, !fdm_add(fdm, delay_lower_fd, EPOLLIN, &fdm_delayed_render, term) || !fdm_add(fdm, delay_upper_fd, EPOLLIN, &fdm_delayed_render, term) || !fdm_add(fdm, app_sync_updates_fd, EPOLLIN, &fdm_app_sync_updates_timeout, term) || - !fdm_add(fdm, title_update_fd, EPOLLIN, &fdm_title_update_timeout, term)) + !fdm_add(fdm, title_update_fd, EPOLLIN, &fdm_title_update_timeout, term) || + !fdm_add(fdm, app_id_update_fd, EPOLLIN, &fdm_app_id_update_timeout, term)) { goto err; } @@ -1210,6 +1242,9 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .title = { .timer_fd = title_update_fd, }, + .app_id = { + .timer_fd = app_id_update_fd, + }, .workers = { .count = conf->render_worker_count, .queue = tll_init(), @@ -1318,6 +1353,7 @@ close_fds: 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; @@ -1510,6 +1546,7 @@ term_shutdown(struct terminal *term) fdm_del(term->fdm, term->selection.auto_scroll.fd); fdm_del(term->fdm, term->render.app_sync_updates.timer_fd); + fdm_del(term->fdm, term->render.app_id.timer_fd); fdm_del(term->fdm, term->render.title.timer_fd); fdm_del(term->fdm, term->delayed_render_timer.lower_fd); fdm_del(term->fdm, term->delayed_render_timer.upper_fd); @@ -1548,6 +1585,7 @@ term_shutdown(struct terminal *term) 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; @@ -1601,6 +1639,7 @@ term_destroy(struct terminal *term) fdm_del(term->fdm, term->selection.auto_scroll.fd); fdm_del(term->fdm, term->render.app_sync_updates.timer_fd); + fdm_del(term->fdm, term->render.app_id.timer_fd); fdm_del(term->fdm, term->render.title.timer_fd); fdm_del(term->fdm, term->delayed_render_timer.lower_fd); fdm_del(term->fdm, term->delayed_render_timer.upper_fd); @@ -1644,6 +1683,7 @@ term_destroy(struct terminal *term) composed_free(term->composed); + free(term->app_id); free(term->window_title); tll_free_and_free(term->window_title_stack, free); @@ -3260,6 +3300,25 @@ term_set_window_title(struct terminal *term, const char *title) term->window_title_has_been_set = true; } +void +term_set_app_id(struct terminal *term, const char *app_id) +{ + if (app_id != NULL && *app_id == '\0') + app_id = NULL; + if (term->app_id == NULL && app_id == NULL) + return; + if (term->app_id != NULL && app_id != NULL && 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) { diff --git a/terminal.h b/terminal.h index 7b743c09..49c88926 100644 --- a/terminal.h +++ b/terminal.h @@ -483,6 +483,7 @@ struct terminal { bool window_title_has_been_set; char *window_title; tll(char *) window_title_stack; + char *app_id; struct { bool active; @@ -606,6 +607,11 @@ struct terminal { 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 { @@ -832,6 +838,7 @@ void term_xcursor_update_for_seat(struct terminal *term, struct seat *seat); void term_set_user_mouse_cursor(struct terminal *term, const char *cursor); void term_set_window_title(struct terminal *term, const char *title); +void term_set_app_id(struct terminal *term, const char *app_id); void term_flash(struct terminal *term, unsigned duration_ms); void term_bell(struct terminal *term); bool term_spawn_new(const struct terminal *term); From 22f04f6a84b69937a4b8516f5b45404c4565953d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 6 Feb 2024 13:49:46 +0100 Subject: [PATCH 0582/1323] changelog: OSC-176 - set app-id --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3255565..b2e4a62d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,8 @@ * 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). [1348]: https://codeberg.org/dnkl/foot/issues/1348 From 4b075bb0750754f219704b99486605ee81634c7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 6 Feb 2024 13:55:30 +0100 Subject: [PATCH 0583/1323] osc: 176 (app-id): implement query+reply Applications can now query for the current app-id with: \E] 176 ; ? \E\\ The reply is \E] 176 ; \E\\ --- osc.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osc.c b/osc.c index 28e93e51..06cc7db6 100644 --- a/osc.c +++ b/osc.c @@ -917,6 +917,18 @@ osc_dispatch(struct terminal *term) 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; From af114f81a0cef1d789c354c4d05140400210b136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 6 Feb 2024 14:03:07 +0100 Subject: [PATCH 0584/1323] pgo: fix function prototype for stub function get_current_modifiers() --- pgo/pgo.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pgo/pgo.c b/pgo/pgo.c index 54618204..6d4dab17 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -173,7 +173,8 @@ void search_selection_cancelled(struct terminal *term) {} void get_current_modifiers(const struct seat *seat, xkb_mod_mask_t *effective, - xkb_mod_mask_t *consumed, uint32_t key) {} + xkb_mod_mask_t *consumed, uint32_t key, + bool filter_locked) {} static struct key_binding_set kbd; static bool kbd_initialized = false; From 3b5d83a3ec0b0bba3a8e56d97aaa6d14c46417f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 6 Feb 2024 14:04:22 +0100 Subject: [PATCH 0585/1323] pgo: add missing stub for render_refresh_app_id() --- pgo/pgo.c | 1 + 1 file changed, 1 insertion(+) diff --git a/pgo/pgo.c b/pgo/pgo.c index 6d4dab17..2fd4e837 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -68,6 +68,7 @@ render_resize_force(struct terminal *term, int width, int height) void render_refresh(struct terminal *term) {} void render_refresh_csd(struct terminal *term) {} void render_refresh_title(struct terminal *term) {} +void render_refresh_app_id(struct terminal *term) {} bool render_xcursor_is_valid(const struct seat *seat, const char *cursor) From ebbee61f144e5f3f662d8b2421ab7429145a6637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 6 Feb 2024 14:04:59 +0100 Subject: [PATCH 0586/1323] input: remove debug logging --- input.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/input.c b/input.c index cfbd63b3..2dd1d9e4 100644 --- a/input.c +++ b/input.c @@ -1562,7 +1562,6 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, bind->mods == (mods & ~consumed) && execute_binding(seat, term, bind, serial, 1)) { - LOG_WARN("matched translated symbol"); goto maybe_repeat; } @@ -1574,7 +1573,6 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, if (bind->k.sym == raw_syms[i] && execute_binding(seat, term, bind, serial, 1)) { - LOG_WARN("matched untranslated symbol"); goto maybe_repeat; } } @@ -1584,7 +1582,6 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, if (code->item == key && execute_binding(seat, term, bind, serial, 1)) { - LOG_WARN("matched raw key code"); goto maybe_repeat; } } From bc8c2e01124eea4911df9750583bac11160d4d2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 7 Feb 2024 16:22:33 +0100 Subject: [PATCH 0587/1323] wayland: regression: use correct scaling factor when calling render_resize() When an output property (such as scaling factor) has changed, we need to call render_resize() to ensure the window surface is correct (for example, we may have to change its scale). The width/height parameters are in *logical* pixels (i.e. already scaled). For render_resize() to work correctly when the scale is being changed, it needs to be called with *current* logical size. This means we need to scale our current width/height using the *old* scaling factor. --- wayland.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/wayland.c b/wayland.c index d3d892df..c5feb6a1 100644 --- a/wayland.c +++ b/wayland.c @@ -392,6 +392,8 @@ static void update_term_for_output_change(struct terminal *term) { const float old_scale = term->scale; + const float logical_width = term->width / old_scale; + const float logical_height = term->height / old_scale; /* Note: order matters! term_update_scale() must come first */ bool scale_updated = term_update_scale(term); @@ -400,7 +402,7 @@ update_term_for_output_change(struct terminal *term) csd_reload_font(term->window, old_scale); - uint8_t resize_opts = RESIZE_KEEP_GRID; + enum resize_options resize_opts = RESIZE_KEEP_GRID; if (fonts_updated) { /* @@ -428,8 +430,8 @@ update_term_for_output_change(struct terminal *term) render_resize( term, - (int)roundf(term->width / term->scale), - (int)roundf(term->height / term->scale), + (int)roundf(logical_width), + (int)roundf(logical_height), resize_opts); } From 69df42c51bfed4ac25130751a578d6d5487c908a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 7 Feb 2024 17:09:01 +0100 Subject: [PATCH 0588/1323] doc: better examples for pipe-* commands --- doc/foot.ini.5.scd | 10 ++++------ foot.ini | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 14999a8b..f184eb9a 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -858,16 +858,14 @@ e.g. *search-start=none*. use *sh -c "command line"* if you need that. Example #1: - # Extract currently visible URLs, let user choose one (via fuzzel), then launch firefox with the selected URL++ -*pipe-visible=[sh -c "xurls | uniq | tac | fuzzel | xargs -r - firefox"] Control+Print* +*pipe-visible=[sh -c "xurls | uniq | tac | fuzzel | xargs -r firefox"] Control+Print* Example #2: - # Write scrollback content to /tmp/foot-scrollback.txt++ -*pipe-scrollback=[sh -c "cat - > /tmp/foot-scrollback.txt"] - Control+Shift+Print* + # Open scrollback contents in Emacs running in a new foot instance++ +*pipe-scrollback=[sh -c "f=$(mktemp) && cat - > $f && foot emacsclient -t $f; rm $f"] Control+Shift+Print* + Default: _none_ *show-urls-launch* diff --git a/foot.ini b/foot.ini index 42a71e58..9fd6c9db 100644 --- a/foot.ini +++ b/foot.ini @@ -158,7 +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=[sh -c "cat - > /tmp/foot-cmd-out.txt"] none # Write output of last command to /tmp/foot-cmd-out.txt (requires shell integration) +# 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 From 9d9690410a72e03511bcc19e6f4a848d450fd0cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 8 Feb 2024 16:44:55 +0100 Subject: [PATCH 0589/1323] pgo: render_resize_force() has been removed, adjust stub accordingly Closes #1601 --- pgo/pgo.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pgo/pgo.c b/pgo/pgo.c index 2fd4e837..204c024d 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -60,7 +60,8 @@ fdm_event_del(struct fdm *fdm, int fd, int events) } bool -render_resize_force(struct terminal *term, int width, int height) +render_resize( + struct terminal *term, int width, int height, uint8_t resize_options) { return true; } From fa01bf2b75fd5c74c57552cb351cb291002fc3bb Mon Sep 17 00:00:00 2001 From: Craig Barnes <craigbarnes@protonmail.com> Date: Sun, 11 Feb 2024 15:09:28 +0000 Subject: [PATCH 0590/1323] csi: support DECRQM queries for ECMA-48 (SM/RM) modes This is in addition to the existing support for DECRQM queries of DEC private modes. --- CHANGELOG.md | 1 + csi.c | 17 +++++++++++++++++ doc/foot-ctlseqs.7.scd | 17 ++++++++++++----- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2e4a62d..a11e2b5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ * `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`). [1348]: https://codeberg.org/dnkl/foot/issues/1348 diff --git a/csi.c b/csi.c index fe783b13..78fdf4ef 100644 --- a/csi.c +++ b/csi.c @@ -1429,6 +1429,23 @@ csi_dispatch(struct terminal *term, uint8_t final) 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]; diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index 64c56d5d..0d8d5d79 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -443,13 +443,13 @@ manipulation sequences. The generic format is: | \\E[ _Pm_ h : SM : VT100 -: Set mode. _Pm_=4 -> enable IRM (insert mode). All other values of - _Pm_ are unsupported. +: Set mode. _Pm_=4 -> enable IRM (Insertion Replacement Mode). All + other values of _Pm_ are unsupported. | \\E[ _Pm_ l : RM : VT100 -: Reset mode. _Pm_=4 -> disable IRM (insert mode). All other values of - _Pm_ are unsupported. +: Reset mode. _Pm_=4 -> disable IRM (Insertion Replacement Mode). All + other values of _Pm_ are unsupported. | \\E[ _Ps_ n : DSR : VT100 @@ -487,7 +487,14 @@ manipulation sequences. The generic format is: | \\E[ ? _Ps_ $ p : DECRQM : VT320 -: Request DEC private mode. +: Request status of DEC private mode. The _Ps_ parameter corresponds + to one of the values mentioned in the "Private Modes" section above + (as set with DECSET/DECRST). +| \\E[ _Ps_ $ p +: DECRQM +: VT320 +: Request status of ECMA-48/ANSI mode. See the descriptions for SM/RM + above for recognized _Ps_ values. | \\E[ _Ps_ T : SD : VT420 From c114afadbd5444cb3b783b161970aaa942416974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 13 Feb 2024 16:28:03 +0100 Subject: [PATCH 0591/1323] render: always center grid when fullscreened or maximized --- CHANGELOG.md | 2 ++ render.c | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a11e2b5b..c8d9d781 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,8 @@ * `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. [1526]: https://codeberg.org/dnkl/foot/issues/1526 [1528]: https://codeberg.org/dnkl/foot/issues/1528 diff --git a/render.c b/render.c index f82e54f0..c6fedfbe 100644 --- a/render.c +++ b/render.c @@ -4060,7 +4060,11 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) 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 { From 729bd57caee1cc6fd31419adf8f75ea482c37102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 15 Feb 2024 16:29:02 +0100 Subject: [PATCH 0592/1323] term: erase-scrollback: handle non-existing scrollback history If scrollback.lines == 0, and the window size (number of rows) is a power of two, all rows are always visible. I.e. there is no scrollback history. This threw off the scrollback erase logic, causing visible rows to be erased, and set to NULL. This triggered a crash when trying to update the view. Closes #1610 --- CHANGELOG.md | 5 +++++ terminal.c | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8d9d781..d3f5d119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -109,11 +109,16 @@ -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]). [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 ### Security diff --git a/terminal.c b/terminal.c index bc7dc428..d9d66bac 100644 --- a/terminal.c +++ b/terminal.c @@ -2350,6 +2350,10 @@ term_erase_scrollback(struct terminal *term) const int num_rows = grid->num_rows; const int mask = num_rows - 1; + const int scrollback_history_size = num_rows - term->rows; + if (scrollback_history_size == 0) + return; + const int start = (grid->offset + term->rows) & mask; const int end = (grid->offset - 1) & mask; @@ -2416,6 +2420,13 @@ term_erase_scrollback(struct terminal *term) } term->grid->view = term->grid->offset; + +#if defined(_DEBUG) + for (int i = 0; i < term->rows; i++) { + xassert(grid_row_in_view(term->grid, i) != NULL); + } +#endif + term_damage_view(term); } From 9ca84e6b48dff28aef557b795dd03b1935a1bb14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 15 Feb 2024 16:41:16 +0100 Subject: [PATCH 0593/1323] config: map Control+wheel to font increase/decrease This is in addition to the already existing keyboard shortcuts. Also add missing default keyboard/mouse bindings to readme + foot(1). --- CHANGELOG.md | 3 +++ README.md | 17 ++++++++++++++--- config.c | 2 ++ doc/foot.1.scd | 17 ++++++++++++++--- doc/foot.ini.5.scd | 13 +++++++++++-- 5 files changed, 44 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3f5d119..3763aca0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,9 @@ 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+-`). [1526]: https://codeberg.org/dnkl/foot/issues/1526 [1528]: https://codeberg.org/dnkl/foot/issues/1528 diff --git a/README.md b/README.md index 75e10889..b8d67e96 100644 --- a/README.md +++ b/README.md @@ -151,10 +151,10 @@ These are the default shortcuts. See `man foot.ini` and the example : Start a scrollback search <kbd>ctrl</kbd>+<kbd>+</kbd>, <kbd>ctrl</kbd>+<kbd>=</kbd> -: Increase font size by 0,5pt +: Increase font size <kbd>ctrl</kbd>+<kbd>-</kbd> -: Decrease font size by 0,5pt +: Decrease font size <kbd>ctrl</kbd>+<kbd>0</kbd> : Reset font size @@ -237,7 +237,11 @@ These are the default shortcuts. See `man foot.ini` and the example under the pointer up to, and until, the next space characters. <kbd>left</kbd> - **triple-click** -: Selects the entire row +: Selects the everything between enclosing quotes, or the entire row + if not inside a quote. + +<kbd>left</kbd> - **quad-click** +: Selects the entire row. <kbd>middle</kbd> : Paste from _primary_ selection @@ -247,9 +251,16 @@ These are the default shortcuts. See `man foot.ini` and the example selection, while hold-and-drag allows you to interactively resize the selection. +<kbd>ctrl</kbd>+<kbd>right</kbd> +: Extend the current selection, but force it to be character wise, + rather than depending on the original selection mode. + <kbd>wheel</kbd> : Scroll up/down in history +<kbd>ctrl</kbd>+<kbd>wheel</kbd> +: Increase/decrease font size + ### Touchscreen diff --git a/config.c b/config.c index 4945485e..6953e798 100644 --- a/config.c +++ b/config.c @@ -2989,6 +2989,8 @@ add_default_mouse_bindings(struct config *conf) {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); diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 73c0c2b1..8e2e6a48 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -183,16 +183,16 @@ default) available; see *foot.ini*(5). Paste from _clipboard_ *shift*+*insert* - Paste from the _primary selection_. + Paste from the _primary selection_ *ctrl*+*shift*+*r* Start a scrollback search *ctrl*+*+*, *ctrl*+*=* - Increase font size by 0.5pt + Increase font size *ctrl*+*-* - Decrease font size by 0.5pt + Decrease font size *ctrl*+*0* Reset font size @@ -273,6 +273,10 @@ default) available; see *foot.ini*(5). characters. *left*, triple-click + Selects the everything between enclosing quotes, or the entire row + if not inside a quote. + +*left*, quad-click Selects the entire row *middle* @@ -283,9 +287,16 @@ default) available; see *foot.ini*(5). selection, while hold-and-drag allows you to interactively resize the selection. +*ctrl*+*right* + Extend the current selection, but force it to be character wise, + rather than depending on the original selection mode. + *wheel* Scroll up/down in history +*ctrl*+*wheel* + Increase/decrease font size + ## TOUCHSCREEN *tap* diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index f184eb9a..2c50ca24 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -821,11 +821,11 @@ e.g. *search-start=none*. *font-increase* Increases the font size by 0.5pt. Default: _Control+plus - Control+equal Control+KP\_Add_. + Control+equal Control+KP\_Add_ (also defined in *mouse-bindings*). *font-decrease* Decreases the font size by 0.5pt. Default: _Control+minus - Control+KP\_Subtract_. + Control+KP\_Subtract_ (also defined in *mouse-bindings*). *font-reset* Resets the font size to the default. Default: _Control+0 Control+KP\_0_. @@ -1228,6 +1228,15 @@ actions listed under *key-bindings* can be used here as well. *primary-paste* Pastes from the _primary selection_. Default: _BTN\_MIDDLE_. +*font-increase* + Increases the font size by 0.5pt. Default: _Control+BTN\_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 From aca9af020241a4b7e4c172a009bf8d4fcfc4e16b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 8 Feb 2024 17:09:27 +0100 Subject: [PATCH 0594/1323] vt: VS16 - variation selector 16 (emoji representation) should only affect emojis --- CHANGELOG.md | 2 ++ vt.c | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3763aca0..24bd8a86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,6 +116,8 @@ 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. [1531]: https://codeberg.org/dnkl/foot/issues/1531 [1573]: https://codeberg.org/dnkl/foot/issues/1573 diff --git a/vt.c b/vt.c index 7529f302..4d2bf487 100644 --- a/vt.c +++ b/vt.c @@ -850,8 +850,14 @@ action_utf8_print(struct terminal *term, char32_t wc) break; case GRAPHEME_WIDTH_DOUBLE: - if (unlikely(wc == 0xfe0f)) - width = 2; + if (unlikely(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; + } new_cc->width = min(grapheme_width + width, 2); break; From 0c94bf43f221f81e3aa184d88c31d6dbea1e23f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 16 Feb 2024 07:11:07 +0100 Subject: [PATCH 0595/1323] vt: ignore VS16 (U+FE0F) when grapheme clustering is disabled This fixes: a) a compilation error with -Dgrapheme-clustering=disabled b) ensures U+FE0F does *not* allocate a two cells when grapheme clustering has been disabled (either compile time, in config, or run-time). --- vt.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/vt.c b/vt.c index 4d2bf487..caa53e62 100644 --- a/vt.c +++ b/vt.c @@ -850,7 +850,11 @@ action_utf8_print(struct terminal *term, char32_t wc) break; case GRAPHEME_WIDTH_DOUBLE: - if (unlikely(wc == 0xfe0f && new_cc->count == 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]); @@ -858,6 +862,8 @@ action_utf8_print(struct terminal *term, char32_t wc) if (props->boundclass == UTF8PROC_BOUNDCLASS_EXTENDED_PICTOGRAPHIC) width = 2; } +#endif + new_cc->width = min(grapheme_width + width, 2); break; From 09d856f2ff1f0002457bde6f5f8a469aadb56b4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 20 Feb 2024 16:18:55 +0100 Subject: [PATCH 0596/1323] input: improved debug logging of key press/release * Log which it is: press or release * Include locked modifiers * Include human-readable names of the modifiers --- input.c | 79 +++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 16 deletions(-) diff --git a/input.c b/input.c index 2dd1d9e4..54ba32a7 100644 --- a/input.c +++ b/input.c @@ -969,8 +969,8 @@ legacy_kbd_protocol(struct seat *seat, struct terminal *term, #define is_control_key(x) ((x) >= 0x40 && (x) <= 0x7f) #define IS_CTRL(x) ((x) < 0x20 || ((x) >= 0x7f && (x) <= 0x9f)) - LOG_DBG("term->modify_other_keys=%d, count=%zu, is_ctrl=%d (utf8=0x%02x), sym=%d", - term->modify_other_keys_2, count, IS_CTRL(utf8[0]), utf8[0], sym); + //LOG_DBG("term->modify_other_keys=%d, count=%zu, is_ctrl=%d (utf8=0x%02x), sym=%d", + //term->modify_other_keys_2, count, IS_CTRL(utf8[0]), utf8[0], sym); bool ctrl_is_in_effect = (keymap_mods & MOD_CTRL) != 0; bool ctrl_seq = is_control_key(sym) || (count == 1 && IS_CTRL(utf8[0])); @@ -1449,6 +1449,34 @@ keysym_is_modifier(xkb_keysym_t keysym) keysym == XKB_KEY_Num_Lock; } +#if defined(_DEBUG) +static void +modifier_string(xkb_mod_mask_t mods, size_t sz, char mod_str[static sz], const struct seat *seat) +{ + if (sz == 0) + return; + + mod_str[0] = '\0'; + + for (size_t i = 0; i < sizeof(xkb_mod_mask_t) * 8; i++) { + if (!(mods & (1u << i))) + continue; + + strcat(mod_str, xkb_keymap_mod_get_name(seat->kbd.xkb_keymap, i)); + strcat(mod_str, "+"); + } + + if (mod_str[0] != '\0') { + /* Strip the last '+' */ + mod_str[strlen(mod_str) - 1] = '\0'; + } + + if (mod_str[0] == '\0') { + strcpy(mod_str, "<none>"); + } +} +#endif + static void key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, uint32_t key, uint32_t state) @@ -1532,22 +1560,28 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, } } -#if 0 - for (size_t i = 0; i < 32; i++) { - if (mods & (1u << 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 /* @@ -1685,8 +1719,21 @@ keyboard_modifiers(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, { struct seat *seat = data; - LOG_DBG("modifiers: depressed=0x%x, latched=0x%x, locked=0x%x, group=%u", - mods_depressed, mods_latched, mods_locked, group); +#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( From 67f97cbca1d9c1396537d0f04afb5a336ac46855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 21 Feb 2024 16:29:10 +0100 Subject: [PATCH 0597/1323] shm: use XRGB surfaces when we know we wont be using transparency --- CHANGELOG.md | 2 ++ render.c | 17 ++++++++++------- shm.c | 21 +++++++++++++-------- shm.h | 5 +++-- wayland.c | 10 ---------- wayland.h | 1 - 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24bd8a86..152c7d15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,8 @@ * 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. [1526]: https://codeberg.org/dnkl/foot/issues/1526 [1528]: https://codeberg.org/dnkl/foot/issues/1528 diff --git a/render.c b/render.c index c6fedfbe..4f0d2348 100644 --- a/render.c +++ b/render.c @@ -1583,7 +1583,7 @@ render_overlay(struct terminal *term) } 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); @@ -2454,7 +2454,7 @@ render_csd(struct terminal *term) } 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 +2599,7 @@ render_scrollback_position(struct terminal *term) } 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 +2642,7 @@ render_render_timer(struct terminal *term, struct timespec render_time) 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, @@ -2817,7 +2817,10 @@ grid_render(struct terminal *term) 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_cursor(term); @@ -3208,7 +3211,7 @@ render_search_box(struct terminal *term) 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); @@ -3670,7 +3673,7 @@ render_urls(struct terminal *term) 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 diff --git a/shm.c b/shm.c index 7959a84b..fb868382 100644 --- a/shm.c +++ b/shm.c @@ -82,6 +82,7 @@ struct buffer_private { 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 @@ instantiate_offset(struct buffer_private *buf, off_t new_offset) wl_buf = wl_shm_pool_create_buffer( pool->wl_pool, new_offset, buf->public.width, buf->public.height, buf->public.stride, - WL_SHM_FORMAT_ARGB8888); + buf->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 @@ instantiate_offset(struct buffer_private *buf, off_t new_offset) /* One pixman image for each worker thread (do we really need multiple?) */ for (size_t i = 0; i < buf->public.pix_instances; i++) { pix[i] = pixman_image_create_bits_no_clear( - PIXMAN_a8r8g8b8, buf->public.width, buf->public.height, + buf->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 @@ err: 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 @@ get_new_buffers(struct buffer_chain *chain, size_t count, size_t total_size = 0; for (size_t i = 0; i < count; i++) { - stride[i] = stride_for_format_and_width(PIXMAN_a8r8g8b8, widths[i]); + stride[i] = stride_for_format_and_width( + with_alpha ? PIXMAN_a8r8g8b8 : PIXMAN_x8r8g8b8, widths[i]); sizes[i] = stride[i] * heights[i]; total_size += sizes[i]; } @@ -473,6 +477,7 @@ get_new_buffers(struct buffer_chain *chain, size_t count, .chain = chain, .ref_count = immediate_purge ? 0 : 1, .busy = true, + .with_alpha = with_alpha, .pool = pool, .offset = 0, .size = sizes[i], @@ -542,13 +547,13 @@ shm_did_not_use_buf(struct buffer *_buf) 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 " @@ -610,7 +615,7 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height) } 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 --git a/shm.h b/shm.h index 7d1796bf..b4b075ca 100644 --- a/shm.h +++ b/shm.h @@ -55,7 +55,8 @@ void shm_chain_free(struct buffer_chain *chain); * * 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 * buffer pool. @@ -73,7 +74,7 @@ struct buffer *shm_get_buffer(struct buffer_chain *chain, int width, int height) 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 --git a/wayland.c b/wayland.c index c5feb6a1..fe3cba20 100644 --- a/wayland.c +++ b/wayland.c @@ -234,8 +234,6 @@ seat_destroy(struct seat *seat) 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 @@ shm_format(void *data, struct wl_shm *wl_shm, uint32_t format) if (!have_description) LOG_DBG("shm: 0x%08x: unknown", format); #endif - - if (format == WL_SHM_FORMAT_ARGB8888) - wayl->have_argb8888 = true; } static const struct wl_shm_listener shm_listener = { @@ -1576,11 +1571,6 @@ wayl_init(struct fdm *fdm, struct key_binding_manager *key_binding_manager, /* Trigger listeners registered when handling globals */ wl_display_roundtrip(wayl->display); - if (!wayl->have_argb8888) { - LOG_ERR("compositor does not support ARGB surfaces"); - goto out; - } - tll_foreach(wayl->monitors, it) { LOG_INFO( "%s: %dx%d+%dx%d@%dHz %s %.2f\" scale=%d, DPI=%.2f/%.2f (physical/scaled)", diff --git a/wayland.h b/wayland.h index 733ebd3f..84fcbe48 100644 --- a/wayland.h +++ b/wayland.h @@ -455,7 +455,6 @@ struct wayland { struct zwp_text_input_manager_v3 *text_input_manager; #endif - bool have_argb8888; tll(struct monitor) monitors; /* All available outputs */ tll(struct seat) seats; From 749d36d32127ba146d42d8b13aa628432bc295ea Mon Sep 17 00:00:00 2001 From: Tim Culverhouse <tim@timculverhouse.com> Date: Thu, 22 Feb 2024 15:08:13 -0600 Subject: [PATCH 0598/1323] input: don't clear text selection on modifier keypresses When kitty keyboard is enabled, pressing a modifier key will clear the text selection. This makes it difficult to copy text because the selection clears as soon as the user presses "ctrl". Tested-by: Robin Jarry <robin@jarry.cc> Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- input.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/input.c b/input.c index 54ba32a7..67cb484f 100644 --- a/input.c +++ b/input.c @@ -434,7 +434,7 @@ execute_binding(struct seat *seat, struct terminal *term, term_damage_view(term); render_refresh(term); - break; + break; } return true; @@ -1689,7 +1689,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, if (utf8 != buf) free(utf8); - if (handled) { + if (handled && !keysym_is_modifier(sym)) { term_reset_view(term); selection_cancel(term); } From d31ccf12d0a1f62f9f781186b4b354851fe413e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 23 Feb 2024 17:48:01 +0100 Subject: [PATCH 0599/1323] changelog: kitty modifier no longer clears selection + viewport --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 152c7d15..6ca6d88a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,6 +120,8 @@ 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. [1531]: https://codeberg.org/dnkl/foot/issues/1531 [1573]: https://codeberg.org/dnkl/foot/issues/1573 From 678bdb7c3f0a0a7244905ee7d86b5ffc18f072ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 20 Feb 2024 16:17:52 +0100 Subject: [PATCH 0600/1323] config: on error, correctly free partially parsed key combos --- config.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/config.c b/config.c index 6953e798..74aaca4e 100644 --- a/config.c +++ b/config.c @@ -1716,7 +1716,7 @@ value_to_key_combos(struct context *ctx, int action, /* Count number of combinations */ size_t combo_count = 1; - size_t used_combos = 0; /* For error handling */ + size_t used_combos = 1; /* For error handling */ for (const char *p = strchr(ctx->value, ' '); p != NULL; p = strchr(p + 1, ' ')) @@ -1764,7 +1764,6 @@ value_to_key_combos(struct context *ctx, int action, new_combo->k.sym = xkb_keysym_from_name(key, 0); if (new_combo->k.sym == XKB_KEY_NoSymbol) { LOG_CONTEXTUAL_ERR("not a valid XKB key name: %s", key); - free_key_binding(new_combo); goto err; } break; @@ -1785,7 +1784,6 @@ value_to_key_combos(struct context *ctx, int action, LOG_CONTEXTUAL_ERRNO("invalid click count: %s", _count); else LOG_CONTEXTUAL_ERR("invalid click count: %s", _count); - free_key_binding(new_combo); goto err; } @@ -1795,7 +1793,6 @@ value_to_key_combos(struct context *ctx, int action, new_combo->m.button = mouse_button_name_to_code(key); if (new_combo->m.button < 0) { LOG_CONTEXTUAL_ERR("invalid mouse button name: %s", key); - free_key_binding(new_combo); goto err; } @@ -2390,7 +2387,7 @@ parse_section_text_bindings(struct context *ctx) struct binding_aux aux = { .type = BINDING_AUX_TEXT, .text = { - .data = data, + .data = data, /* data is now owned by value_to_key_combos() */ .len = data_len, }, }; @@ -2398,7 +2395,8 @@ parse_section_text_bindings(struct context *ctx) if (!value_to_key_combos(ctx, BIND_ACTION_TEXT_BINDING, &aux, &conf->bindings.key, KEY_BINDING)) { - goto err; + /* Do *not* free(data) - it is handled by value_to_key_combos() */ + return false; } return true; From 0dc105d0e41a2c280dea1f1471900c84c01832ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 23 Feb 2024 17:59:01 +0100 Subject: [PATCH 0601/1323] ci: rename .woodpecker.yml -> .woodpecker.yaml According to the latest woodpecker docs, this is the only supported extension. --- .woodpecker.yml => .woodpecker.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .woodpecker.yml => .woodpecker.yaml (100%) diff --git a/.woodpecker.yml b/.woodpecker.yaml similarity index 100% rename from .woodpecker.yml rename to .woodpecker.yaml From 1c985537ec0f1efa1608dfb649e077f5c877e5fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 23 Feb 2024 18:02:18 +0100 Subject: [PATCH 0602/1323] ci: 'steps' is a list --- .woodpecker.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 484e718f..8b83ebcd 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -1,5 +1,5 @@ steps: - codespell: + - name: codespell when: branch: - master @@ -14,7 +14,7 @@ steps: - codespell -Lser,doas,zar README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd - deactivate - subprojects: + - name: subprojects when: branch: - master @@ -27,7 +27,7 @@ steps: - git clone https://codeberg.org/dnkl/fcft.git - cd .. - x64: + - name: x64 when: branch: - master @@ -84,7 +84,7 @@ steps: - ./footclient --version - cd ../.. - x86: + - name: x86 when: branch: - master From a5ab490380db9e3fe6dc2e5ebddc7fa07fbe7085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 24 Feb 2024 09:54:51 +0100 Subject: [PATCH 0603/1323] ci: remove deprecated 'group' --- .woodpecker.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 8b83ebcd..29ca226a 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -32,7 +32,6 @@ steps: branch: - master - releases/* - group: build image: alpine:edge commands: - apk update @@ -89,7 +88,6 @@ steps: branch: - master - releases/* - group: build image: i386/alpine:edge commands: - apk update From 3d9aa1c29cae7a493cb87055e6dfee92f0ff88c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 24 Feb 2024 09:58:04 +0100 Subject: [PATCH 0604/1323] config: don't try to free key combos we haven't allocated When erroring out due to a key combo being "empty", we incorrectly tried to free one key binding. This is because 'used_combos' is initialized to '1'. And it should, but an empty key binding is a special case. Fixes a test failure, and closes #1620 --- config.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/config.c b/config.c index 74aaca4e..ab0e2927 100644 --- a/config.c +++ b/config.c @@ -1823,8 +1823,10 @@ value_to_key_combos(struct context *ctx, int action, return true; err: - for (size_t i = 0; i < used_combos; i++) - free_key_binding(&new_combos[i]); + if (idx > 0) { + for (size_t i = 0; i < used_combos; i++) + free_key_binding(&new_combos[i]); + } free(copy); return false; } From 4bb3b5383f98e023c67be02a525e2c93fe32113b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 24 Feb 2024 10:05:05 +0100 Subject: [PATCH 0605/1323] ci: run x86+x64 builds in parallel ... by making them depend on the 'subprojects' step. --- .woodpecker.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 29ca226a..063390be 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -32,6 +32,7 @@ steps: branch: - master - releases/* + depends_on: [subprojects] image: alpine:edge commands: - apk update @@ -88,6 +89,7 @@ steps: branch: - master - releases/* + depends_on: [subprojects] image: i386/alpine:edge commands: - apk update From 6fd533ce138bbb2d026da8175353feb493b12b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 29 Feb 2024 07:49:08 +0100 Subject: [PATCH 0606/1323] render: don't try to set a NULL xcursor image render_xcursor_update() is called when we've loaded a new xcursor image, and needs to display it. The reason it's not pushed to the compositor immediately is to ensure we don't flood the Wayland socket with xcursor updates. Normally, it's only called when we *succeed* to load a new xcursor image. I.e. if we try to load a non-existing xcursor image, we never schedule an update. However, we _can_ still end up in render_xcursor_update() without a valid xcursor image. For example, we have loaded a valid xcursor image, and scheduled an update. But before the update runs, the user moves the cursor, and we try to load a new xcursor image. If it fails, we crash when the previously scheduled update finally runs. Closes #1624 --- CHANGELOG.md | 2 ++ render.c | 19 +++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ca6d88a..0b06bf41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -122,12 +122,14 @@ 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]). [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 ### Security diff --git a/render.c b/render.c index 4f0d2348..3ce8a226 100644 --- a/render.c +++ b/render.c @@ -4363,8 +4363,6 @@ render_xcursor_update(struct seat *seat) return; } - xassert(seat->pointer.cursor != NULL); - const enum cursor_shape shape = seat->pointer.shape; const char *const xcursor = seat->pointer.last_custom_xcursor; @@ -4398,6 +4396,23 @@ render_xcursor_update(struct seat *seat) 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); From d3b348a5b183b0e6295dc985b1d5dcd939cb5c6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 29 Feb 2024 07:54:27 +0100 Subject: [PATCH 0607/1323] cursor-shape: improve xcursor fallback support, and prefer CSS names Before this patch, we used legacy X11 xcursor names, and didn't really have any fallback handling in place (we only tried to fallback to "xterm", regardless of which cursor shape we were trying to load). This patch changes two things: 1. Improved fallback support. cursor_shape_to_string() now returns a list of strings. This allows us to have per-shape fallbacks, and any number of fallbacks. 2. We prefer CSS xcursor names over legacy X11 names. --- CHANGELOG.md | 1 + cursor-shape.c | 31 +++++++++++------------- cursor-shape.h | 3 +-- render.c | 64 +++++++++++++++++++++++++++++--------------------- 4 files changed, 53 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b06bf41..e6b6bcd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,7 @@ 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. [1526]: https://codeberg.org/dnkl/foot/issues/1526 [1528]: https://codeberg.org/dnkl/foot/issues/1528 diff --git a/cursor-shape.c b/cursor-shape.c index 131e6f1a..bbf75ab8 100644 --- a/cursor-shape.c +++ b/cursor-shape.c @@ -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 @@ cursor_shape_to_server_shape(enum cursor_shape shape) static const enum wp_cursor_shape_device_v1_shape table[CURSOR_SHAPE_COUNT] = { [CURSOR_SHAPE_LEFT_PTR] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT, [CURSOR_SHAPE_TEXT] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT, - [CURSOR_SHAPE_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, diff --git a/cursor-shape.h b/cursor-shape.h index 58755382..110dbd2e 100644 --- a/cursor-shape.h +++ b/cursor-shape.h @@ -9,7 +9,6 @@ enum cursor_shape { 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 @@ enum cursor_shape { 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 --git a/render.c b/render.c index 3ce8a226..91472027 100644 --- a/render.c +++ b/render.c @@ -4625,38 +4625,48 @@ render_xcursor_set(struct seat *seat, struct terminal *term, 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 - : 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); - - if (seat->pointer.cursor == NULL) { - seat->pointer.cursor = wl_cursor_theme_get_cursor( - seat->pointer.theme, fallback); - - if (seat->pointer.cursor == NULL) { - LOG_ERR("failed to load xcursor pointer " - "'%s', and fallback '%s'", xcursor, fallback); - return false; - } - } - - if (shape == CURSOR_SHAPE_CUSTOM) { - free(seat->pointer.last_custom_xcursor); - seat->pointer.last_custom_xcursor = xstrdup(term->mouse_user_cursor); - } - } else { + if (shape == CURSOR_SHAPE_HIDDEN) { seat->pointer.cursor = NULL; free(seat->pointer.last_custom_xcursor); seat->pointer.last_custom_xcursor = NULL; } + else if (seat->pointer.shape_device == NULL) { + const char *const custom_xcursors[] = {term->mouse_user_cursor, NULL}; + const char *const *xcursors = shape == CURSOR_SHAPE_CUSTOM + ? custom_xcursors + : cursor_shape_to_string(shape); + + xassert(xcursors[0] != NULL); + + seat->pointer.cursor = NULL; + + for (size_t i = 0; xcursors[i] != NULL; i++) { + seat->pointer.cursor = + wl_cursor_theme_get_cursor(seat->pointer.theme, xcursors[i]); + + if (seat->pointer.cursor != NULL) { + LOG_DBG("loaded xcursor %s", xcursors[i]); + break; + } + } + + if (seat->pointer.cursor == NULL) { + LOG_ERR( + "failed to load xcursor pointer '%s', and all of its fallbacks", + xcursors[0]); + return false; + } + } else { + /* Server-side cursors - no need to load anything */ + } + + if (shape == CURSOR_SHAPE_CUSTOM) { + free(seat->pointer.last_custom_xcursor); + seat->pointer.last_custom_xcursor = + xstrdup(term->mouse_user_cursor); + } + /* FDM hook takes care of actual rendering */ seat->pointer.shape = shape; seat->pointer.xcursor_pending = true; From ec73e4d10d1f3aa899000c1dd4c58f371c7b926f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 29 Feb 2024 09:32:38 +0100 Subject: [PATCH 0608/1323] kitty kbd: switch from GTK to XKB mode for 'consumed' modifiers This fixes an issue where some key combinations resulted in different output (e.g. escape code vs. plain text) depending on the state of e.g. the NumLock key. One such example is Shift+space. Another example is Shift+BackSpace. This patch also removes the hardcoded CapsLock filter, when determining whether a key combo produces text or not, and instead uses the locked modifiers as reported by XKB. --- CHANGELOG.md | 7 +++++++ input.c | 22 ++++++++-------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6b6bcd9..82416455 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,13 @@ * 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. [1526]: https://codeberg.org/dnkl/foot/issues/1526 [1528]: https://codeberg.org/dnkl/foot/issues/1528 diff --git a/input.c b/input.c index 67cb484f..1bdc9b65 100644 --- a/input.c +++ b/input.c @@ -1158,7 +1158,7 @@ kitty_kbd_protocol(struct seat *seat, struct terminal *term, xassert(info == NULL || info->sym == sym); xkb_mod_mask_t mods = 0; - xkb_mod_mask_t consumed = 0; + xkb_mod_mask_t consumed = ctx->consumed; if (info != NULL && info->is_modifier) { /* @@ -1186,7 +1186,7 @@ kitty_kbd_protocol(struct seat *seat, struct terminal *term, get_current_modifiers(seat, &mods, NULL, ctx->key, false); consumed = xkb_state_key_get_consumed_mods2( - seat->kbd.xkb_state, ctx->key, XKB_CONSUMED_MODE_GTK); + seat->kbd.xkb_state, ctx->key, XKB_CONSUMED_MODE_XKB); #if 0 /* @@ -1204,22 +1204,14 @@ kitty_kbd_protocol(struct seat *seat, struct terminal *term, /* Same as ctx->mods, but without locked modifiers being filtered out */ get_current_modifiers(seat, &mods, NULL, ctx->key, false); - - /* Re-retrieve the consumed modifiers using the GTK mode, to - better match kitty. */ - consumed = xkb_state_key_get_consumed_mods2( - seat->kbd.xkb_state, ctx->key, XKB_CONSUMED_MODE_GTK); } mods &= seat->kbd.kitty_significant; consumed &= 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); - - bool is_text = count > 0 && utf32 != NULL && (effective & ~caps_num) == 0; + /* Use ctx->mods, rather than 'mods', since we *do* want locked + modifiers filtered here */ + bool is_text = count > 0 && utf32 != NULL && (ctx->mods & ~consumed) == 0; for (size_t i = 0; utf32[i] != U'\0'; i++) { if (!iswprint(utf32[i])) { is_text = false; @@ -1242,7 +1234,9 @@ kitty_kbd_protocol(struct seat *seat, struct terminal *term, if (report_all_as_escapes) goto emit_escapes; - if (effective == 0) { + /* Use ctx->mods rather than 'mods', since we *do* want locked + modifiers filtered here */ + if ((ctx->mods & ~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; From 702d3ae6ca8e232ba4df9f0ee4e0141e243b49cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 2 Mar 2024 08:16:17 +0100 Subject: [PATCH 0609/1323] kitty kbd: update handling of locked modifiers The kitty keyboard specification has been updated/clarified yet again. Locked modifiers are to be ignored if the key event would result in plain text without the locked modifier being enabled. In short, locked modifiers are included in the set of modifiers reported in a key event. But having a locked modifier enabled doesn't turn all key events into CSIu sequences. For example, with only the disambiguate mode enabled, pressing 'a', or 'shift+a' results in a/A regardless of the state of Caps- or NumLock. But 'ctrl+a', which always results in a CSIu, will have a different modifier list, depending on whether Caps- or NumLock are enabled. --- input.c | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/input.c b/input.c index 1bdc9b65..1a42e5ee 100644 --- a/input.c +++ b/input.c @@ -1158,6 +1158,7 @@ kitty_kbd_protocol(struct seat *seat, struct terminal *term, 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) { @@ -1184,7 +1185,10 @@ kitty_kbd_protocol(struct seat *seat, struct terminal *term, xkb_state_update_key( seat->kbd.xkb_state, ctx->key, pressed ? XKB_KEY_DOWN : XKB_KEY_UP); - get_current_modifiers(seat, &mods, NULL, ctx->key, false); + 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); @@ -1201,17 +1205,33 @@ kitty_kbd_protocol(struct seat *seat, struct terminal *term, seat->kbd.xkb_state, ctx->key, pressed ? XKB_KEY_UP : XKB_KEY_DOWN); #endif } else { - /* Same as ctx->mods, but without locked modifiers being - filtered out */ - get_current_modifiers(seat, &mods, NULL, ctx->key, false); + /* 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; - /* Use ctx->mods, rather than 'mods', since we *do* want locked - modifiers filtered here */ - bool is_text = count > 0 && utf32 != NULL && (ctx->mods & ~consumed) == 0; + /* + * 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; @@ -1234,9 +1254,7 @@ kitty_kbd_protocol(struct seat *seat, struct terminal *term, if (report_all_as_escapes) goto emit_escapes; - /* Use ctx->mods rather than 'mods', since we *do* want locked - modifiers filtered here */ - if ((ctx->mods & ~consumed) == 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; From 8ff8ec5b70da567f28b6e0aba1d09931bd680fb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 4 Mar 2024 16:29:04 +0100 Subject: [PATCH 0610/1323] sixel: fix row height calculation in resize_vertically() In resize_vertically(), we assumed a sixel is 6 pixels tall. This is mostly true, but not for non-1:1 sixels. Or, to be more precise, not for sixels where 'pan' != 1. This caused us to allocate too little backing memory, resulting in a crash when we later tried to write to the image. --- CHANGELOG.md | 2 ++ sixel.c | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82416455..9ceab4d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -131,6 +131,8 @@ * 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. [1531]: https://codeberg.org/dnkl/foot/issues/1531 [1573]: https://codeberg.org/dnkl/foot/issues/1573 diff --git a/sixel.c b/sixel.c index c046145f..8ec72c15 100644 --- a/sixel.c +++ b/sixel.c @@ -1347,8 +1347,9 @@ resize_vertically(struct terminal *term, int new_height) uint32_t *old_data = term->sixel.image.data; const int width = term->sixel.image.width; const int old_height = term->sixel.image.height; + const int sixel_row_height = 6 * term->sixel.pan; - int alloc_height = (new_height + 6 - 1) / 6 * 6; + int alloc_height = (new_height + sixel_row_height - 1) / sixel_row_height * sixel_row_height; xassert(new_height > 0); From 1568518ab3c30ce9d83d6e2c4a9d8e74bbe15051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 4 Mar 2024 16:40:16 +0100 Subject: [PATCH 0611/1323] sixel: performance improvements * Store pointer to current pixel (i.e. pixel we're about to write to), instead of a row-byte-offset. This way, we don't have to calculate the offset into the backing image every time we emit a sixel band. * Pass data pointer directly to sixel_add_*(), to avoid having to calculate an offset into the backing image. * Special case adding a single 1:1 sixel. This removes a for loop, and simplifies state (position) updates. It is likely LTO does this for us, but this way, we get it optimized in non-LTO builds as well. --- sixel.c | 88 +++++++++++++++++++++++++++++++++++++----------------- terminal.h | 2 +- 2 files changed, 61 insertions(+), 29 deletions(-) diff --git a/sixel.c b/sixel.c index 8ec72c15..3e13af23 100644 --- a/sixel.c +++ b/sixel.c @@ -56,7 +56,6 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) 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,6 +64,7 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) 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; @@ -1263,6 +1263,7 @@ sixel_unhook(struct terminal *term) free(term->sixel.image.data); term->sixel.image.data = NULL; + term->sixel.image.p = NULL; term->sixel.image.width = 0; term->sixel.image.height = 0; term->sixel.pos = (struct coord){0, 0}; @@ -1329,7 +1330,9 @@ resize_horizontally(struct terminal *term, int new_width) 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 @@ -1375,8 +1378,14 @@ resize_vertically(struct terminal *term, int new_height) new_data[r * width + c] = bg; } - term->sixel.image.data = new_data; term->sixel.image.height = new_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; } @@ -1450,59 +1459,54 @@ resize(struct terminal *term, int new_width, int new_height) 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.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; } @@ -1522,12 +1526,35 @@ sixel_add_many_generic(struct terminal *term, uint8_t c, unsigned count) } 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; + + for (; data < end; data++) + sixel_add_generic(term, data, width, color, c); + + term->sixel.pos.col = col + count; + term->sixel.image.p = end; +} + +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)); } - term->sixel.pos.col = col; + sixel_add_ar_11(term, term->sixel.image.p, width, term->sixel.color, c); + + term->sixel.pos.col += 1; + term->sixel.image.p += 1; } static void @@ -1546,10 +1573,14 @@ sixel_add_many_ar_11(struct terminal *term, uint8_t c, unsigned count) } 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 = col; + for (; data < end; data++) + sixel_add_ar_11(term, data, width, color, c); + + term->sixel.pos.col += count; + term->sixel.image.p = end; } IGNORE_WARNING("-Wpedantic") @@ -1587,13 +1618,14 @@ decsixel_generic(struct terminal *term, uint8_t c) * path in sixel_add(). */ term->sixel.pos.col = 0; + term->sixel.image.p = &term->sixel.image.data[term->sixel.pos.row * term->sixel.image.width]; } break; case '-': term->sixel.pos.row += 6 * term->sixel.pan; term->sixel.pos.col = 0; - term->sixel.row_byte_ofs += term->sixel.image.width * 6 * term->sixel.pan; + term->sixel.image.p = &term->sixel.image.data[term->sixel.pos.row * term->sixel.image.width]; if (term->sixel.pos.row >= term->sixel.image.height) { if (!resize_vertically(term, term->sixel.pos.row + 6 * term->sixel.pan)) @@ -1622,7 +1654,7 @@ static void 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); } diff --git a/terminal.h b/terminal.h index 49c88926..ec2ff963 100644 --- a/terminal.h +++ b/terminal.h @@ -666,7 +666,6 @@ struct terminal { } 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 */ @@ -675,6 +674,7 @@ struct terminal { struct { uint32_t *data; /* Raw image data, in ARGB */ + uint32_t *p; /* Pointer into data, for current position */ int width; /* Image width, in pixels */ int height; /* Image height, in pixels */ } image; From 1421ba504d58764b7801be72912a9afdddd18cb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 7 Mar 2024 16:18:35 +0100 Subject: [PATCH 0612/1323] sixel: debug: fix logged width/height values when emitting sixel image.width and image.height are the scaled dimensions, and these haven't been set when the log message is printed. --- sixel.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sixel.c b/sixel.c index 3e13af23..c1a1b18c 100644 --- a/sixel.c +++ b/sixel.c @@ -1162,7 +1162,7 @@ sixel_unhook(struct terminal *term) LOG_DBG("generating %s %dx%d pixman image at %d-%d", image.opaque ? "opaque" : "transparent", - image.width, image.height, + image.original.width, image.original.height, image.pos.row, image.pos.row + image.rows); image.original.pix = pixman_image_create_bits_no_clear( From a2fa667f45015f616171eccac9d6f55789b281d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 7 Mar 2024 16:19:56 +0100 Subject: [PATCH 0613/1323] sixel: we no longer need the extra newline Since we never place the cursor *under* the sixel anymore. --- sixel.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sixel.c b/sixel.c index c1a1b18c..605bc8b7 100644 --- a/sixel.c +++ b/sixel.c @@ -1096,10 +1096,9 @@ sixel_unhook(struct terminal *term) int start_row = do_scroll ? term->grid->cursor.point.row : 0; const int start_col = do_scroll ? term->grid->cursor.point.col : 0; - /* Total number of rows needed by image (+ optional newline at the end) */ + /* Total number of rows needed by image */ const int rows_needed = - (term->sixel.image.height + term->cell_height - 1) / term->cell_height + - (term->sixel.cursor_right_of_graphics ? 0 : 1); + (term->sixel.image.height + term->cell_height - 1) / term->cell_height; bool free_image_data = true; From 75fd59df3f27f914729abd69d790e3a2a51c70e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 7 Mar 2024 16:20:29 +0100 Subject: [PATCH 0614/1323] sixel: debug: sixel image _may_ be zero-sized For example, and single GNL (Graphical New Line) will result in a sixel with a non-zero height, but a zero width. --- sixel.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sixel.c b/sixel.c index 605bc8b7..f3d57fb2 100644 --- a/sixel.c +++ b/sixel.c @@ -613,7 +613,8 @@ sixel_overwrite(struct terminal *term, struct sixel *six, 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 From 3e6f0e63f3027f19a83c6a2ddcf52018d30f2751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 7 Mar 2024 16:21:06 +0100 Subject: [PATCH 0615/1323] sixel: don't try to emit a sixel if we're outside the image's boundaries Closes #1634 --- sixel.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sixel.c b/sixel.c index f3d57fb2..7684e657 100644 --- a/sixel.c +++ b/sixel.c @@ -1549,6 +1549,9 @@ sixel_add_one_ar_11(struct terminal *term, uint8_t c) resize_horizontally(term, col + count); width = term->sixel.image.width; count = min(count, max(width - col, 0)); + + if (unlikely(count == 0)) + return; } sixel_add_ar_11(term, term->sixel.image.p, width, term->sixel.color, c); From ea851962c1f4f1d30e350de3f4aa7bfbea135dcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 26 Dec 2021 14:34:17 +0100 Subject: [PATCH 0616/1323] term: add term_put_char() This function prints a single, non-double width, character to the grid. It handles OSC-8 hyperlinks, but does not: * update the cursor location * erase sixels --- terminal.c | 36 ++++++++++++++++++++++++++++++++++++ terminal.h | 1 + 2 files changed, 37 insertions(+) diff --git a/terminal.c b/terminal.c index d9d66bac..6119f68c 100644 --- a/terminal.c +++ b/terminal.c @@ -3498,6 +3498,42 @@ print_spacer(struct terminal *term, int col, int remaining) 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 + * + * Limitiations: + * - double width characters not supported + */ +void +term_put_char(struct terminal *term, int r, int c, wchar_t wc) +{ + struct row *row = grid_row(term->grid, r); + row->dirty = true; + + struct cell *cell = &row->cells[c]; + cell->wc = wc; + cell->attrs = term->vt.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; + } + } +} + void term_print(struct terminal *term, char32_t wc, int width) { diff --git a/terminal.h b/terminal.h index ec2ff963..8abc1c8e 100644 --- a/terminal.h +++ b/terminal.h @@ -797,6 +797,7 @@ void term_cursor_up(struct terminal *term, int count); void term_cursor_down(struct terminal *term, int count); void term_cursor_blink_update(struct terminal *term); +void term_put_char(struct terminal *term, int r, int c, char32_t wc); void term_print(struct terminal *term, char32_t wc, int width); void term_scroll(struct terminal *term, int rows); From e6c372b14fa76799ea3c3ff89258a1b905cc9ec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 26 Dec 2021 14:35:18 +0100 Subject: [PATCH 0617/1323] term: print: spacers may be printed all the way up to the last column --- terminal.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal.c b/terminal.c index 6119f68c..f814de97 100644 --- a/terminal.c +++ b/terminal.c @@ -3602,7 +3602,7 @@ term_print(struct terminal *term, char32_t wc, int width) 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 < term->cols; i++) { col++; print_spacer(term, col, width - i); } From b4dbfb58b883d4bae5ffb794895cbdcf39e5bb0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 26 Dec 2021 14:35:38 +0100 Subject: [PATCH 0618/1323] grid: remove prototype for non-existing function --- grid.h | 1 - 1 file changed, 1 deletion(-) diff --git a/grid.h b/grid.h index 0664409c..8ea5200b 100644 --- a/grid.h +++ b/grid.h @@ -86,7 +86,6 @@ grid_row_in_view(struct grid *grid, int row_no) void grid_row_uri_range_put( struct row *row, int col, const char *uri, uint64_t id); -void grid_row_uri_range_add(struct row *row, struct row_uri_range range); void grid_row_uri_range_erase(struct row *row, int start, int end); static inline void From b30b8a294498ce765698dd40df4a750f8debe689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 26 Dec 2021 15:00:13 +0100 Subject: [PATCH 0619/1323] put_char fixup --- terminal.c | 1 + 1 file changed, 1 insertion(+) diff --git a/terminal.c b/terminal.c index f814de97..bdbcd9bd 100644 --- a/terminal.c +++ b/terminal.c @@ -3506,6 +3506,7 @@ print_spacer(struct terminal *term, int col, int remaining) * - update the cursor * - linewrap * - erase sixels + * - erase URIs (but it _does_ emit them if one is active) * * Limitiations: * - double width characters not supported From 926d88fd3035407057c70a9995bf61e0c47fbe40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 26 Dec 2021 15:00:27 +0100 Subject: [PATCH 0620/1323] csi: implement rectangular edit escapes * DECCARA - change attributes in rectangular area * DECRARA - reverse attributes in rectangular area * DECCRA - copy rectangular area * DECFRA - fill rectangular area * DECERA - erase rectangular area Not implemented: * DECSERA - selective erase rectangular area --- csi.c | 237 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) diff --git a/csi.c b/csi.c index 78fdf4ef..a5987968 100644 --- a/csi.c +++ b/csi.c @@ -1752,6 +1752,243 @@ csi_dispatch(struct terminal *term, uint8_t final) break; /* private[0] == '=' */ } + case '$': { + switch (final) { + case 'r': { /* DECCARA */ + int rel_top_row = vt_param_get(term, 0, 1) - 1; + int left_col = vt_param_get(term, 1, 1) - 1; + int rel_bottom_row = vt_param_get(term, 2, term->rows) - 1; + int right_col = vt_param_get(term, 3, term->cols) - 1; + + int top_row = term_row_rel_to_abs(term, rel_top_row); + int bottom_row = term_row_rel_to_abs(term, rel_bottom_row); + + if (unlikely(top_row > bottom_row || left_col > right_col)) + break; + + for (int r = top_row; r <= bottom_row; r++) { + struct row *row = grid_row(term->grid, r); + + for (int c = left_col; c <= right_col; c++) { + struct attributes *a = &row->cells[c].attrs; + + 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 rel_top_row = vt_param_get(term, 0, 1) - 1; + int left_col = vt_param_get(term, 1, 1) - 1; + int rel_bottom_row = vt_param_get(term, 2, term->rows) - 1; + int right_col = vt_param_get(term, 3, term->cols) - 1; + + int top_row = term_row_rel_to_abs(term, rel_top_row); + int bottom_row = term_row_rel_to_abs(term, rel_bottom_row); + + if (unlikely(top_row > bottom_row || left_col > right_col)) + break; + + for (int r = top_row; r <= bottom_row; r++) { + struct row *row = grid_row(term->grid, r); + + for (int c = left_col; c <= right_col; c++) { + struct attributes *a = &row->cells[c].attrs; + + 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 = !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_rel_top_row = vt_param_get(term, 0, 1) - 1; + int src_left_col = vt_param_get(term, 1, 1) - 1; + int src_rel_bottom_row = vt_param_get(term, 2, term->rows) - 1; + int src_right_col = vt_param_get(term, 3, term->cols) - 1; + int src_page = vt_param_get(term, 4, 1); + + int dst_rel_top_row = vt_param_get(term, 5, 1) - 1; + int dst_left_col = 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_row = + dst_rel_top_row + (src_rel_bottom_row - src_rel_top_row); + int dst_right_col = min( + dst_left_col + (src_right_col - src_left_col), + term->cols - 1); + + /* Source and destination boxes, absolute coordinates */ + int src_top_row = term_row_rel_to_abs(term, src_rel_top_row); + int src_bottom_row = term_row_rel_to_abs(term, src_rel_bottom_row); + int dst_top_row = term_row_rel_to_abs(term, dst_rel_top_row); + int dst_bottom_row = term_row_rel_to_abs(term, dst_rel_bottom_row); + + if (unlikely(src_top_row > src_bottom_row || + src_left_col > src_right_col)) + break; + + /* Target area outside the screen is clipped */ + const size_t row_count = min(src_bottom_row - src_top_row, + dst_bottom_row - dst_top_row) + 1; + const size_t cell_count = min(src_right_col - src_left_col, + dst_right_col - dst_left_col) + 1; + + sixel_overwrite_by_rectangle( + term, dst_top_row, dst_left_col, 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_row + r); + const struct cell *cell = &row->cells[src_left_col]; + 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_row + r); + row->dirty = true; + + struct cell *cell = &row->cells[dst_left_col]; + memcpy(cell, copy[r], cell_count * sizeof(copy[r][0])); + free(copy[r]); + + for (int c = 0; c < cell_count; c++, 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_col, dst_right_col); + } + } + free(copy); + break; + } + + case 'x': { /* DECFRA */ + const char c = vt_param_get(term, 0, 0); + if (likely((c >= 32 && c < 126) || + (c >= 160 && c <= 255))) + { + int rel_top_row = vt_param_get(term, 1, 1) - 1; + int left_col = vt_param_get(term, 2, 1) - 1; + int rel_bottom_row = vt_param_get(term, 3, term->rows) - 1; + int right_col = vt_param_get(term, 4, term->cols) - 1; + + int top_row = term_row_rel_to_abs(term, rel_top_row); + int bottom_row = term_row_rel_to_abs(term, rel_bottom_row); + + if (unlikely(top_row > bottom_row || left_col > right_col)) + 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_row, left_col, + bottom_row - top_row + 1, right_col - left_col + 1); + + for (int r = top_row; r <= bottom_row; r++) { + struct row *row = grid_row(term->grid, r); + + if (unlikely(row->extra != NULL)) + grid_row_uri_range_erase(row, left_col, right_col); + + for (int col = left_col; col <= right_col; col++) + term_put_char(term, r, col, (wchar_t)c); + } + } + break; + } + + case 'z': { /* DECERA */ + int rel_top_row = vt_param_get(term, 0, 1) - 1; + int left_col = vt_param_get(term, 1, 1) - 1; + int rel_bottom_row = vt_param_get(term, 2, term->rows) - 1; + int right_col = vt_param_get(term, 3, term->cols) - 1; + + int top_row = term_row_rel_to_abs(term, rel_top_row); + int bottom_row = term_row_rel_to_abs(term, rel_bottom_row); + + if (unlikely(top_row > bottom_row || left_col > right_col)) + 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_row, left_col, + bottom_row - top_row + 1, right_col - left_col + 1); + + for (int r = top_row; r <= bottom_row; r++) + term_erase(term, r, left_col, r, right_col); + break; + } + } + + break; /* private[0] == ‘$’ */ + } + case 0x243f: /* ?$ */ switch (final) { case 'p': { From 95293f142a1288eedf4d07b09ecb4036ff1db295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 26 Dec 2021 15:03:55 +0100 Subject: [PATCH 0621/1323] =?UTF-8?q?csi:=20add=20=E2=80=9828=E2=80=99=20(?= =?UTF-8?q?rectangular=20edit)=20to=20primary=20DA=20response?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- csi.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/csi.c b/csi.c index a5987968..48c3211b 100644 --- a/csi.c +++ b/csi.c @@ -743,10 +743,10 @@ csi_dispatch(struct terminal *term, uint8_t final) * Note: tertiary DA responds with "FOOT". */ if (term->conf->tweak.sixel) { - static const char reply[] = "\033[?62;4;22c"; + 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; From 4b4fe9d49335265b7287d457c78ce661235c1c92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 26 Dec 2021 15:05:07 +0100 Subject: [PATCH 0622/1323] changelog: rectangular edit functions --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ceab4d5..0d11f3ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -681,6 +681,9 @@ way of entering Unicode characters is with an IME ([#1116][1116]). * Support for `xdg_toplevel.wm_capabilities`, to adapt the client-side decoration buttons to the compositor capabilities ([#1061][1061]). +* Rectangular edit functions: `DECCARA`, `DECRARA`, `DECCRA`, `DECFRA` + and `DECERA`. + [1058]: https://codeberg.org/dnkl/foot/issues/1058 [1070]: https://codeberg.org/dnkl/foot/issues/1070 From df5dd94789291290489e7e875dd7f7862aa6dc35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 26 Dec 2021 15:09:46 +0100 Subject: [PATCH 0623/1323] term: codespell: limitiations -> limitations --- terminal.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal.c b/terminal.c index bdbcd9bd..ad20c737 100644 --- a/terminal.c +++ b/terminal.c @@ -3508,7 +3508,7 @@ print_spacer(struct terminal *term, int col, int remaining) * - erase sixels * - erase URIs (but it _does_ emit them if one is active) * - * Limitiations: + * Limitations: * - double width characters not supported */ void From 1b66c6a3acd9ea80be0903d4860d3b06f6f108d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 26 Dec 2021 15:53:46 +0100 Subject: [PATCH 0624/1323] csi: rectangular: add helper function params_to_rectangular_area() This functions reads the four top/left/bottom/right parameters, validates them, and converts relative row numbers to absolute. Returns true if the params are valid, otherwise false. --- csi.c | 152 +++++++++++++++++++++++++++------------------------------- 1 file changed, 71 insertions(+), 81 deletions(-) diff --git a/csi.c b/csi.c index 48c3211b..43a358a8 100644 --- a/csi.c +++ b/csi.c @@ -673,6 +673,23 @@ xtrestore(struct terminal *term, unsigned param) decset_decrst(term, param, enable); } +static bool +params_to_rectangular_area(const struct terminal *term, int first_idx, + int *top, int *left, int *bottom, int *right) +{ + int rel_top = vt_param_get(term, first_idx + 0, 1) - 1; + *left = vt_param_get(term, first_idx + 1, 1) - 1; + int rel_bottom = vt_param_get(term, first_idx + 2, term->rows) - 1; + *right = vt_param_get(term, first_idx + 3, 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) { @@ -1755,21 +1772,17 @@ csi_dispatch(struct terminal *term, uint8_t final) case '$': { switch (final) { case 'r': { /* DECCARA */ - int rel_top_row = vt_param_get(term, 0, 1) - 1; - int left_col = vt_param_get(term, 1, 1) - 1; - int rel_bottom_row = vt_param_get(term, 2, term->rows) - 1; - int right_col = vt_param_get(term, 3, term->cols) - 1; - - int top_row = term_row_rel_to_abs(term, rel_top_row); - int bottom_row = term_row_rel_to_abs(term, rel_bottom_row); - - if (unlikely(top_row > bottom_row || left_col > right_col)) + int top, left, bottom, right; + if (!params_to_rectangular_area( + term, 0, &top, &left, &bottom, &right)) + { break; + } - for (int r = top_row; r <= bottom_row; r++) { + for (int r = top; r <= bottom; r++) { struct row *row = grid_row(term->grid, r); - for (int c = left_col; c <= right_col; c++) { + for (int c = left; c <= right; c++) { struct attributes *a = &row->cells[c].attrs; for (size_t i = 4; i < term->vt.params.idx; i++) { @@ -1801,21 +1814,17 @@ csi_dispatch(struct terminal *term, uint8_t final) } case 't': { /* DECRARA */ - int rel_top_row = vt_param_get(term, 0, 1) - 1; - int left_col = vt_param_get(term, 1, 1) - 1; - int rel_bottom_row = vt_param_get(term, 2, term->rows) - 1; - int right_col = vt_param_get(term, 3, term->cols) - 1; - - int top_row = term_row_rel_to_abs(term, rel_top_row); - int bottom_row = term_row_rel_to_abs(term, rel_bottom_row); - - if (unlikely(top_row > bottom_row || left_col > right_col)) + int top, left, bottom, right; + if (!params_to_rectangular_area( + term, 0, &top, &left, &bottom, &right)) + { break; + } - for (int r = top_row; r <= bottom_row; r++) { + for (int r = top; r <= bottom; r++) { struct row *row = grid_row(term->grid, r); - for (int c = left_col; c <= right_col; c++) { + for (int c = left; c <= right; c++) { struct attributes *a = &row->cells[c].attrs; for (size_t i = 4; i < term->vt.params.idx; i++) { @@ -1842,14 +1851,17 @@ csi_dispatch(struct terminal *term, uint8_t final) } case 'v': { /* DECCRA */ - int src_rel_top_row = vt_param_get(term, 0, 1) - 1; - int src_left_col = vt_param_get(term, 1, 1) - 1; - int src_rel_bottom_row = vt_param_get(term, 2, term->rows) - 1; - int src_right_col = vt_param_get(term, 3, term->cols) - 1; + 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_row = vt_param_get(term, 5, 1) - 1; - int dst_left_col = vt_param_get(term, 6, 1) - 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)) { @@ -1857,30 +1869,20 @@ csi_dispatch(struct terminal *term, uint8_t final) break; } - int dst_rel_bottom_row = - dst_rel_top_row + (src_rel_bottom_row - src_rel_top_row); - int dst_right_col = min( - dst_left_col + (src_right_col - src_left_col), - term->cols - 1); + int dst_rel_bottom = dst_rel_top + (src_bottom - src_top); + int dst_right = min(dst_left + (src_right - src_left), term->cols - 1); - /* Source and destination boxes, absolute coordinates */ - int src_top_row = term_row_rel_to_abs(term, src_rel_top_row); - int src_bottom_row = term_row_rel_to_abs(term, src_rel_bottom_row); - int dst_top_row = term_row_rel_to_abs(term, dst_rel_top_row); - int dst_bottom_row = term_row_rel_to_abs(term, dst_rel_bottom_row); - - if (unlikely(src_top_row > src_bottom_row || - src_left_col > src_right_col)) - break; + 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_row - src_top_row, - dst_bottom_row - dst_top_row) + 1; - const size_t cell_count = min(src_right_col - src_left_col, - dst_right_col - dst_left_col) + 1; + 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_row, dst_left_col, row_count, cell_count); + term, dst_top, dst_left, row_count, cell_count); /* * Copy source area @@ -1895,17 +1897,17 @@ csi_dispatch(struct terminal *term, uint8_t final) 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_row + r); - const struct cell *cell = &row->cells[src_left_col]; + 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_row + r); + struct row *row = grid_row(term->grid, dst_top + r); row->dirty = true; - struct cell *cell = &row->cells[dst_left_col]; + struct cell *cell = &row->cells[dst_left]; memcpy(cell, copy[r], cell_count * sizeof(copy[r][0])); free(copy[r]); @@ -1914,7 +1916,7 @@ csi_dispatch(struct terminal *term, uint8_t final) if (unlikely(row->extra != NULL)) { /* TODO: technically, we should copy the source URIs... */ - grid_row_uri_range_erase(row, dst_left_col, dst_right_col); + grid_row_uri_range_erase(row, dst_left, dst_right); } } free(copy); @@ -1926,32 +1928,26 @@ csi_dispatch(struct terminal *term, uint8_t final) if (likely((c >= 32 && c < 126) || (c >= 160 && c <= 255))) { - int rel_top_row = vt_param_get(term, 1, 1) - 1; - int left_col = vt_param_get(term, 2, 1) - 1; - int rel_bottom_row = vt_param_get(term, 3, term->rows) - 1; - int right_col = vt_param_get(term, 4, term->cols) - 1; - - int top_row = term_row_rel_to_abs(term, rel_top_row); - int bottom_row = term_row_rel_to_abs(term, rel_bottom_row); - - if (unlikely(top_row > bottom_row || left_col > right_col)) + 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_row, left_col, - bottom_row - top_row + 1, right_col - left_col + 1); + term, top, left, bottom - top + 1, right - left + 1); - for (int r = top_row; r <= bottom_row; r++) { + for (int r = top; r <= bottom; r++) { struct row *row = grid_row(term->grid, r); if (unlikely(row->extra != NULL)) - grid_row_uri_range_erase(row, left_col, right_col); + grid_row_uri_range_erase(row, left, right); - for (int col = left_col; col <= right_col; col++) + for (int col = left; col <= right; col++) term_put_char(term, r, col, (wchar_t)c); } } @@ -1959,16 +1955,12 @@ csi_dispatch(struct terminal *term, uint8_t final) } case 'z': { /* DECERA */ - int rel_top_row = vt_param_get(term, 0, 1) - 1; - int left_col = vt_param_get(term, 1, 1) - 1; - int rel_bottom_row = vt_param_get(term, 2, term->rows) - 1; - int right_col = vt_param_get(term, 3, term->cols) - 1; - - int top_row = term_row_rel_to_abs(term, rel_top_row); - int bottom_row = term_row_rel_to_abs(term, rel_bottom_row); - - if (unlikely(top_row > bottom_row || left_col > right_col)) + 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 @@ -1976,12 +1968,10 @@ csi_dispatch(struct terminal *term, uint8_t final) * entire sixel here is more efficient. */ sixel_overwrite_by_rectangle( - term, - top_row, left_col, - bottom_row - top_row + 1, right_col - left_col + 1); + term, top, left, bottom - top + 1, right - left + 1); - for (int r = top_row; r <= bottom_row; r++) - term_erase(term, r, left_col, r, right_col); + for (int r = top; r <= bottom; r++) + term_erase(term, r, left, r, right); break; } } From 189cfd717fd4670a97995ad9b8e08a2352c09099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 26 Dec 2021 15:59:38 +0100 Subject: [PATCH 0625/1323] term: replace term_put_char() with term_fill() --- csi.c | 11 ++--------- terminal.c | 32 +++++++++++++++++++------------- terminal.h | 2 +- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/csi.c b/csi.c index 43a358a8..ac03825e 100644 --- a/csi.c +++ b/csi.c @@ -1941,15 +1941,8 @@ csi_dispatch(struct terminal *term, uint8_t final) sixel_overwrite_by_rectangle( term, top, left, bottom - top + 1, right - left + 1); - for (int r = top; r <= bottom; r++) { - struct row *row = grid_row(term->grid, r); - - if (unlikely(row->extra != NULL)) - grid_row_uri_range_erase(row, left, right); - - for (int col = left; col <= right; col++) - term_put_char(term, r, col, (wchar_t)c); - } + for (int r = top; r <= bottom; r++) + term_fill(term, r, left, c, right - left + 1); } break; } diff --git a/terminal.c b/terminal.c index ad20c737..8abe61e8 100644 --- a/terminal.c +++ b/terminal.c @@ -3506,33 +3506,39 @@ print_spacer(struct terminal *term, int col, int remaining) * - update the cursor * - linewrap * - erase sixels - * - erase URIs (but it _does_ emit them if one is active) * * Limitations: * - double width characters not supported */ void -term_put_char(struct terminal *term, int r, int c, wchar_t wc) +term_fill(struct terminal *term, int r, int c, char data, size_t count) { struct row *row = grid_row(term->grid, r); row->dirty = true; - struct cell *cell = &row->cells[c]; - cell->wc = wc; - cell->attrs = term->vt.attrs; + xassert(c + count <= term->cols); - if (unlikely(term->vt.osc8.uri != NULL)) { - grid_row_uri_range_put(row, c, term->vt.osc8.uri, term->vt.osc8.id); + const struct cell *last = &row->cells[c + count]; + for (struct cell *cell = &row->cells[c]; cell < last; cell++) { + cell->wc = data; + cell->attrs = term->vt.attrs; - switch (term->conf->url.osc8_underline) { - case OSC8_UNDERLINE_ALWAYS: - cell->attrs.url = true; - break; + if (unlikely(term->vt.osc8.uri != NULL)) { + grid_row_uri_range_put(row, c, term->vt.osc8.uri, term->vt.osc8.id); - case OSC8_UNDERLINE_URL_MODE: - break; + 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 diff --git a/terminal.h b/terminal.h index 8abc1c8e..f56ffdb0 100644 --- a/terminal.h +++ b/terminal.h @@ -797,8 +797,8 @@ void term_cursor_up(struct terminal *term, int count); void term_cursor_down(struct terminal *term, int count); void term_cursor_blink_update(struct terminal *term); -void term_put_char(struct terminal *term, int r, int c, char32_t wc); void term_print(struct terminal *term, char32_t wc, int width); +void term_fill(struct terminal *term, int row, int col, char c, size_t count); void term_scroll(struct terminal *term, int rows); void term_scroll_reverse(struct terminal *term, int rows); From b3a84ba71b021b484e0ddc023480eb6d9f885991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 26 Dec 2021 16:05:18 +0100 Subject: [PATCH 0626/1323] term: modify term_fill() to optionally reset the SGR attributes --- csi.c | 2 +- terminal.c | 9 +++++++-- terminal.h | 3 ++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/csi.c b/csi.c index ac03825e..558c2dc2 100644 --- a/csi.c +++ b/csi.c @@ -1942,7 +1942,7 @@ csi_dispatch(struct terminal *term, uint8_t final) 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); + term_fill(term, r, left, c, right - left + 1, true); } break; } diff --git a/terminal.c b/terminal.c index 8abe61e8..d826282b 100644 --- a/terminal.c +++ b/terminal.c @@ -3511,17 +3511,22 @@ print_spacer(struct terminal *term, int col, int remaining) * - double width characters not supported */ void -term_fill(struct terminal *term, int r, int c, char data, size_t count) +term_fill(struct terminal *term, int r, int c, char data, size_t count, + bool use_sgr_attrs) { struct row *row = grid_row(term->grid, r); row->dirty = true; xassert(c + count <= term->cols); + const 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 = term->vt.attrs; + 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); diff --git a/terminal.h b/terminal.h index f56ffdb0..c8a43c64 100644 --- a/terminal.h +++ b/terminal.h @@ -798,7 +798,8 @@ void term_cursor_down(struct terminal *term, int count); void term_cursor_blink_update(struct terminal *term); void term_print(struct terminal *term, char32_t wc, int width); -void term_fill(struct terminal *term, int row, int col, char c, size_t count); +void term_fill(struct terminal *term, int row, int col, char 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); From 74a1fa9e00058589c30454a87b68527fd27ead9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 26 Dec 2021 16:07:51 +0100 Subject: [PATCH 0627/1323] vt: update DECALN to use term_fill() --- vt.c | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/vt.c b/vt.c index caa53e62..beb26670 100644 --- a/vt.c +++ b/vt.c @@ -560,15 +560,9 @@ action_esc_dispatch(struct terminal *term, uint8_t final) case '#': switch (final) { - case '8': - for (int r = 0; r < term->rows; r++) { - struct row *row = grid_row(term->grid, r); - for (int c = 0; c < term->cols; c++) { - row->cells[c].wc = U'E'; - row->cells[c].attrs = (struct attributes){0}; - } - row->dirty = true; - } + case '8': /* DECALN */ + for (int r = 0; r < term->rows; r++) + term_fill(term, r, 0, 'E', term->cols, false); break; } break; /* private[0] == '#' */ From 1b13deff048fc10620b26ffe0220fae15549f9b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 26 Dec 2021 16:08:42 +0100 Subject: [PATCH 0628/1323] =?UTF-8?q?term=5Ffill():=20make=20sure=20the=20?= =?UTF-8?q?filled=20cells=20have=20their=20=E2=80=98clean=E2=80=99=20bit?= =?UTF-8?q?=20reset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- terminal.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/terminal.c b/terminal.c index d826282b..acde951b 100644 --- a/terminal.c +++ b/terminal.c @@ -3519,10 +3519,12 @@ term_fill(struct terminal *term, int r, int c, char data, size_t count, xassert(c + count <= term->cols); - const struct attributes attrs = use_sgr_attrs + struct attributes attrs = use_sgr_attrs ? term->vt.attrs : (struct attributes){0}; + attrs.clean = 0; + const struct cell *last = &row->cells[c + count]; for (struct cell *cell = &row->cells[c]; cell < last; cell++) { cell->wc = data; From 23908d9277b3c41c294102993875cf485db689cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 26 Dec 2021 16:13:05 +0100 Subject: [PATCH 0629/1323] =?UTF-8?q?term=5Ffill():=20change=20=E2=80=98ch?= =?UTF-8?q?aracter=E2=80=99=20parameter=20from=20char=20->=20uint8=5Ft?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- csi.c | 37 +++++++++++++++++++------------------ terminal.c | 2 +- terminal.h | 2 +- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/csi.c b/csi.c index 558c2dc2..494ac957 100644 --- a/csi.c +++ b/csi.c @@ -1924,26 +1924,27 @@ csi_dispatch(struct terminal *term, uint8_t final) } case 'x': { /* DECFRA */ - const char c = vt_param_get(term, 0, 0); - if (likely((c >= 32 && c < 126) || - (c >= 160 && c <= 255))) + 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)) { - 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; } + + /* 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; } diff --git a/terminal.c b/terminal.c index acde951b..1ca581be 100644 --- a/terminal.c +++ b/terminal.c @@ -3511,7 +3511,7 @@ print_spacer(struct terminal *term, int col, int remaining) * - double width characters not supported */ void -term_fill(struct terminal *term, int r, int c, char data, size_t count, +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); diff --git a/terminal.h b/terminal.h index c8a43c64..0dd40c51 100644 --- a/terminal.h +++ b/terminal.h @@ -798,7 +798,7 @@ void term_cursor_down(struct terminal *term, int count); void term_cursor_blink_update(struct terminal *term); void term_print(struct terminal *term, char32_t wc, int width); -void term_fill(struct terminal *term, int row, int col, char c, size_t count, +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); From f5c574cd94796561a9138a30eae0cbe724033644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 26 Dec 2021 16:25:52 +0100 Subject: [PATCH 0630/1323] csi: DECCRA: no need for a counter here --- csi.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csi.c b/csi.c index 494ac957..b5d58f97 100644 --- a/csi.c +++ b/csi.c @@ -1911,7 +1911,7 @@ csi_dispatch(struct terminal *term, uint8_t final) memcpy(cell, copy[r], cell_count * sizeof(copy[r][0])); free(copy[r]); - for (int c = 0; c < cell_count; c++, cell++) + for (;cell < &row->cells[dst_left + cell_count]; cell++) cell->attrs.clean = 0; if (unlikely(row->extra != NULL)) { From d6c5bc3262594d265cf9a506e9cd4afb033e4f68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 26 Dec 2021 16:37:11 +0100 Subject: [PATCH 0631/1323] csi: DECRARA: fix comment: DECCARA -> DECRARA --- csi.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csi.c b/csi.c index b5d58f97..881a8ce1 100644 --- a/csi.c +++ b/csi.c @@ -1830,7 +1830,7 @@ csi_dispatch(struct terminal *term, uint8_t final) 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 */ + /* DECRARA only supports a sub-set of SGR parameters */ switch (param) { case 0: a->bold = !a->bold; From 60c5d889ecd8b0496e181f587dc2c6368a209b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 26 Dec 2021 16:42:53 +0100 Subject: [PATCH 0632/1323] vt: DECALN: erase sixels, reset margins, home the cursor https://vt100.net/docs/vt510-rm/DECALN.html: Notes on DECALN DECALN sets the margins to the extremes of the page, and moves the cursor to the home position. --- vt.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/vt.c b/vt.c index beb26670..a8a0f0fb 100644 --- a/vt.c +++ b/vt.c @@ -18,6 +18,7 @@ #include "debug.h" #include "grid.h" #include "osc.h" +#include "sixel.h" #include "util.h" #include "xmalloc.h" @@ -561,8 +562,15 @@ action_esc_dispatch(struct terminal *term, uint8_t final) case '#': switch (final) { case '8': /* DECALN */ + sixel_overwrite_by_rectangle(term, 0, 0, term->rows, term->cols); + + term->scroll_region.start = 0; + term->scroll_region.end = term->rows; + for (int r = 0; r < term->rows; r++) term_fill(term, r, 0, 'E', term->cols, false); + + term_cursor_home(term); break; } break; /* private[0] == '#' */ From aac24bfa1b3779c1851bd4dcbfbf8266d16634dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 26 Dec 2021 19:43:47 +0100 Subject: [PATCH 0633/1323] =?UTF-8?q?foot.info:=20add=20non-standard=20cap?= =?UTF-8?q?ability=20=E2=80=98Rect=E2=80=99=20(used=20by=20tmux)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This tells tmux it can use DECFRA to erase rectangular regions. --- CHANGELOG.md | 1 + foot.info | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d11f3ba..9e48118b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -683,6 +683,7 @@ decoration buttons to the compositor capabilities ([#1061][1061]). * Rectangular edit functions: `DECCARA`, `DECRARA`, `DECCRA`, `DECFRA` and `DECERA`. +* `Rect` capability to terminfo. [1058]: https://codeberg.org/dnkl/foot/issues/1058 diff --git a/foot.info b/foot.info index 3bab3266..319d0781 100644 --- a/foot.info +++ b/foot.info @@ -38,6 +38,7 @@ 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%;, From 6a01642a6fef11d5663c9dfcdf212826f7224f64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 28 Dec 2021 17:14:50 +0100 Subject: [PATCH 0634/1323] csi: DECCARA+DECRARA: dirty cells --- csi.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/csi.c b/csi.c index 881a8ce1..7a427f9e 100644 --- a/csi.c +++ b/csi.c @@ -1781,9 +1781,11 @@ csi_dispatch(struct terminal *term, uint8_t final) 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; @@ -1823,9 +1825,11 @@ csi_dispatch(struct terminal *term, uint8_t final) 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; From 8d7ab86182c1439e9aa4c436f0ceebe77c7d05a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 28 Dec 2021 17:15:12 +0100 Subject: [PATCH 0635/1323] term_fill(): no need to set attrs.clean = 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The VT state’s attribute is always 0 --- terminal.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/terminal.c b/terminal.c index 1ca581be..2248bd02 100644 --- a/terminal.c +++ b/terminal.c @@ -3523,8 +3523,6 @@ term_fill(struct terminal *term, int r, int c, uint8_t data, size_t count, ? term->vt.attrs : (struct attributes){0}; - attrs.clean = 0; - const struct cell *last = &row->cells[c + count]; for (struct cell *cell = &row->cells[c]; cell < last; cell++) { cell->wc = data; From 6ff307b3b53c50902b185e0e7504ff5dcc0c0934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 1 Jan 2022 14:32:26 +0100 Subject: [PATCH 0636/1323] doc: ctlseq: DECCARA, DECRARA, DECCRA, DECFRA and DECERA --- doc/foot-ctlseqs.7.scd | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index 0d8d5d79..38742aa3 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -495,6 +495,31 @@ manipulation sequences. The generic format is: : 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 From cbf55ccacf694058ce85a324243ded9cf64bb36c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 7 Mar 2024 16:28:59 +0100 Subject: [PATCH 0637/1323] changeloge: move DECERA to the 'unreleased' section --- CHANGELOG.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e48118b..2f0214a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,9 @@ * 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`. +* `Rect` capability to terminfo. [1348]: https://codeberg.org/dnkl/foot/issues/1348 @@ -681,10 +684,6 @@ way of entering Unicode characters is with an IME ([#1116][1116]). * Support for `xdg_toplevel.wm_capabilities`, to adapt the client-side decoration buttons to the compositor capabilities ([#1061][1061]). -* Rectangular edit functions: `DECCARA`, `DECRARA`, `DECCRA`, `DECFRA` - and `DECERA`. -* `Rect` capability to terminfo. - [1058]: https://codeberg.org/dnkl/foot/issues/1058 [1070]: https://codeberg.org/dnkl/foot/issues/1070 From 4ea4e5da4eb6dca888fdadb76f293c843d719f8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 7 Mar 2024 16:29:39 +0100 Subject: [PATCH 0638/1323] changelog: rectangular functions: add bug ref --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f0214a0..4712a299 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,10 +63,11 @@ (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`. + and `DECERA` ([#1633][1633]). * `Rect` capability to terminfo. [1348]: https://codeberg.org/dnkl/foot/issues/1348 +[1633]: https://codeberg.org/dnkl/foot/issues/1633 ### Changed From e2b3eb91ddfb8690d45d4300f0f41cbb1137f22e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 10 Mar 2024 17:37:11 +0100 Subject: [PATCH 0639/1323] csi: params_to_rectangular_area(): ensure left/right is within bounds --- csi.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/csi.c b/csi.c index 7a427f9e..d03ae40d 100644 --- a/csi.c +++ b/csi.c @@ -678,15 +678,16 @@ 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 = vt_param_get(term, first_idx + 1, 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 = vt_param_get(term, first_idx + 3, term->cols) - 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; } From 712bc95db3bfce96118a3a95dcdaa9779cbeb136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 14 Mar 2024 07:03:51 +0100 Subject: [PATCH 0640/1323] csi: the CSI-t family of queries now report unscaled pixel values Before this patch, we reported scaled pixel values. This was rather useless, since applications have no way of getting the scaling factor, to "scale up" e.g. images. Furthermore, the most common use of these queries are probably to calculate the dimensions to use when emitting sixels. Closes #1643 --- CHANGELOG.md | 3 +++ csi.c | 15 ++++++--------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4712a299..5b898c77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,12 +102,15 @@ * 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]). [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 ### Deprecated diff --git a/csi.c b/csi.c index d03ae40d..0d1ee1f4 100644 --- a/csi.c +++ b/csi.c @@ -1219,9 +1219,7 @@ csi_dispatch(struct terminal *term, uint8_t final) if (width >= 0 && height >= 0) { char reply[64]; size_t n = xsnprintf( - reply, sizeof(reply), "\033[4;%d;%dt", - (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; @@ -1231,8 +1229,8 @@ csi_dispatch(struct terminal *term, uint8_t final) tll_foreach(term->window->on_outputs, it) { char reply[64]; size_t n = xsnprintf(reply, sizeof(reply), "\033[5;%d;%dt", - it->item->dim.px_scaled.height, - it->item->dim.px_scaled.width); + it->item->dim.px_real.height, + it->item->dim.px_real.width); term_to_slave(term, reply, n); break; } @@ -1245,8 +1243,7 @@ csi_dispatch(struct terminal *term, uint8_t final) 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; } @@ -1264,8 +1261,8 @@ csi_dispatch(struct terminal *term, uint8_t final) 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; } From 86894a1cd29f8fb3e5a8487b4ce2d2c1b4cf57a9 Mon Sep 17 00:00:00 2001 From: Alyssa Ross <hi@alyssa.is> Date: Fri, 10 Dec 2021 17:40:59 +0000 Subject: [PATCH 0641/1323] Add support for opening an existing PTY Virtual machine monitor programs (e.g. QEMU, Cloud Hypervisor) expose guest consoles as PTYs. With this patch, foot can access these guest consoles. Usually, the program used for accessing these PTYs is screen, but screen is barely developed, doesn't support resizing, and has a bunch of other unrelated stuff going on. It would be nice to have a terminal emulator that properly supported opening an existing PTY. The VMM controls the master end of the PTY, so to the other end (in this case foot), it just behaves like any application running in a directly-opened PTY, and all that's needed is to change foot's code to support opening an existing PTY rather than creating one. Co-authored-by: tanto <tanto@ccc.ac> --- CHANGELOG.md | 3 ++ completions/bash/foot | 5 +-- completions/fish/foot.fish | 1 + completions/zsh/_foot | 1 + doc/foot.1.scd | 7 ++++ main.c | 19 ++++++++++- server.c | 2 +- terminal.c | 67 +++++++++++++++++++++++--------------- terminal.h | 3 +- 9 files changed, 76 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b898c77..a67ff8fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,8 @@ ## Unreleased ### 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 @@ -69,6 +71,7 @@ [1348]: https://codeberg.org/dnkl/foot/issues/1348 [1633]: https://codeberg.org/dnkl/foot/issues/1633 +[1564]: https://codeberg.org/dnkl/foot/pulls/1564 ### Changed diff --git a/completions/bash/foot b/completions/bash/foot index eb17dad1..25aa2c49 100644 --- a/completions/bash/foot +++ b/completions/bash/foot @@ -19,6 +19,7 @@ _foot() "--maximized" "--override" "--print-pid" + "--pty" "--server" "--term" "--title" @@ -39,7 +40,7 @@ _foot() for word in "${previous_words[@]}" ; do match=$(printf "$commands" | grep -Fx "$word" 2>/dev/null) if [[ ! -z "$match" ]] ; then - if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--config|--font|--log-level|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then + if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--config|--font|--log-level|--pty|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then (( i++ )) continue fi @@ -74,7 +75,7 @@ _foot() COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) ;; --log-colorize|-l) COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) ;; - --app-id|--help|--override|--title|--version|--window-size-chars|--window-size-pixels|--check-config|-[ahoTvWwC]) + --app-id|--help|--override|--pty|--title|--version|--window-size-chars|--window-size-pixels|--check-config|-[ahoTvWwC]) # Don't autocomplete for these flags : ;; *) diff --git a/completions/fish/foot.fish b/completions/fish/foot.fish index 86f6616d..eecb6488 100644 --- a/completions/fish/foot.fish +++ b/completions/fish/foot.fish @@ -18,5 +18,6 @@ complete -c foot -r -s p -l print-pid complete -c foot -x -s d -l log-level -a "info warning error none" -d "log-level (warning)" complete -c foot -x -s l -l log-colorize -a "always never auto" -d "enable or disable colorization of log output on stderr" complete -c foot -s S -l log-no-syslog -d "disable syslog logging (server mode only)" +complete -c foot -x -l pty -a '(__fish_complete_path)' -d "display an existing pty instead of creating one" complete -c foot -s v -l version -d "show the version number and quit" complete -c foot -s h -l help -d "show help message and quit" diff --git a/completions/zsh/_foot b/completions/zsh/_foot index b9f46cdc..2a0dc7b0 100644 --- a/completions/zsh/_foot +++ b/completions/zsh/_foot @@ -18,6 +18,7 @@ _arguments \ '(-s --server)'{-s,--server}'[run as server; open terminals by running footclient]:server:_files' \ '(-H --hold)'{-H,--hold}'[remain open after child process exits]' \ '(-p --print-pid)'{-p,--print-pid}'[print PID to this file or FD when up and running (server mode only)]:pidfile:_files' \ + '--pty=[display an existing pty instead of creating one]:pty:_files' \ '(-d --log-level)'{-d,--log-level}'[log level (warning)]:loglevel:(info warning error none)' \ '(-l --log-colorize)'{-l,--log-colorize}'[enable or disable colorization of log output on stderr]:logcolor:(never always auto)' \ '(-S --log-no-syslog)'{-s,--log-no-syslog}'[disable syslog logging (server mode only)]' \ diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 8e2e6a48..86975464 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -78,6 +78,13 @@ the foot command line *-L*,*--login-shell* Start a login shell, by prepending a '-' to argv[0]. +*--pty* + Display an existing pty instead of creating one. This is useful + for interacting with VM consoles. + + This option is not currently supported in combination with + *-s*,*--server*. + *-D*,*--working-directory*=_DIR_ Initial working directory for the client application. Default: _CWD of foot_. diff --git a/main.c b/main.c index 8b4d2715..24ba45ca 100644 --- a/main.c +++ b/main.c @@ -3,6 +3,7 @@ #include <string.h> #include <ctype.h> #include <stdbool.h> +#include <limits.h> #include <locale.h> #include <getopt.h> #include <signal.h> @@ -77,6 +78,7 @@ print_usage(const char *prog_name) " -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" @@ -172,6 +174,10 @@ sanitize_signals(void) sigaction(i, &dfl, NULL); } +enum { + PTY_OPTION = CHAR_MAX + 1, +}; + int main(int argc, char *const *argv) { @@ -209,6 +215,7 @@ main(int argc, char *const *argv) {"maximized", no_argument, NULL, 'm'}, {"fullscreen", no_argument, NULL, 'F'}, {"presentation-timings", no_argument, NULL, 'P'}, /* Undocumented */ + {"pty", required_argument, NULL, PTY_OPTION}, {"print-pid", required_argument, NULL, 'p'}, {"log-level", required_argument, NULL, 'd'}, {"log-colorize", optional_argument, NULL, 'l'}, @@ -221,6 +228,7 @@ main(int argc, char *const *argv) 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; @@ -316,6 +324,10 @@ main(int argc, char *const *argv) conf_server_socket_path = optarg; break; + case PTY_OPTION: + pty_path = optarg; + break; + case 'P': presentation_timings = true; break; @@ -383,6 +395,11 @@ main(int argc, char *const *argv) } } + if (as_server && pty_path) { + fputs("error: --pty is incompatible with server mode\n", stderr); + return ret; + } + log_init(log_colorize, as_server && log_syslog, as_server ? LOG_FACILITY_DAEMON : LOG_FACILITY_USER, log_level); @@ -574,7 +591,7 @@ main(int argc, char *const *argv) goto out; if (!as_server && (term = term_init( - &conf, fdm, reaper, wayl, "foot", cwd, token, + &conf, fdm, reaper, wayl, "foot", cwd, token, pty_path, argc, argv, NULL, &term_shutdown_cb, &shutdown_ctx)) == NULL) { goto out; diff --git a/server.c b/server.c index 068ca057..53e86088 100644 --- a/server.c +++ b/server.c @@ -332,7 +332,7 @@ fdm_client(struct fdm *fdm, int fd, int events, void *data) instance->terminal = term_init( conf != NULL ? conf : server->conf, server->fdm, server->reaper, server->wayl, "footclient", cwd, token, - cdata.argc, argv, (const char *const *)envp, + NULL, cdata.argc, argv, (const char *const *)envp, &term_shutdown_handler, instance); if (instance->terminal == NULL) { diff --git a/terminal.c b/terminal.c index 2248bd02..3cf44d5a 100644 --- a/terminal.c +++ b/terminal.c @@ -367,6 +367,8 @@ fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) del_utmp_record(term->conf, term->reaper, term->ptmx); fdm_del(fdm, fd); term->ptmx = -1; + if (!term->conf->hold_at_exit) + term_shutdown(term); } return true; @@ -1062,10 +1064,13 @@ load_fonts_from_conf(struct terminal *term) 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, const 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; @@ -1082,7 +1087,8 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, return NULL; } - if ((ptmx = posix_openpt(O_RDWR | O_NOCTTY)) < 0) { + ptmx = pty_path ? open(pty_path, PTY_OPEN_FLAGS) : posix_openpt(PTY_OPEN_FLAGS); + if (ptmx < 0) { LOG_ERRNO("failed to open PTY"); goto close_fds; } @@ -1156,6 +1162,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .fdm = fdm, .reaper = reaper, .conf = conf, + .slave = -1, .ptmx = ptmx, .ptmx_buffers = tll_init(), .ptmx_paste_buffers = tll_init(), @@ -1290,16 +1297,18 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, 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 */ @@ -1561,26 +1570,30 @@ term_shutdown(struct terminal *term) close(term->ptmx); if (!term->shutdown.client_has_terminated) { - LOG_DBG("initiating asynchronous terminate of slave; " - "sending SIGTERM to PID=%u", term->slave); + if (term->slave <= 0) { + term->shutdown.client_has_terminated = true; + } else { + LOG_DBG("initiating asynchronous terminate of slave; " + "sending SIGTERM to PID=%u", term->slave); - kill(-term->slave, SIGTERM); + kill(-term->slave, SIGTERM); - const struct itimerspec timeout = {.it_value = {.tv_sec = 60}}; + 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; } - - xassert(term->shutdown.terminate_timeout_fd < 0); - term->shutdown.terminate_timeout_fd = timeout_fd; } term->selection.auto_scroll.fd = -1; diff --git a/terminal.h b/terminal.h index 0dd40c51..eeff8af0 100644 --- a/terminal.h +++ b/terminal.h @@ -736,7 +736,8 @@ struct config; struct terminal *term_init( const struct config *conf, struct fdm *fdm, struct reaper *reaper, struct wayland *wayl, const char *foot_exe, const char *cwd, - const char *token, int argc, char *const *argv, const 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); From dd3bb13d97b405495465357f7b7b17c9f2bba3c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 14 Mar 2024 07:37:57 +0100 Subject: [PATCH 0642/1323] completions: fish: fix path completion for --pty --- completions/fish/foot.fish | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/completions/fish/foot.fish b/completions/fish/foot.fish index eecb6488..0053d18d 100644 --- a/completions/fish/foot.fish +++ b/completions/fish/foot.fish @@ -18,6 +18,6 @@ complete -c foot -r -s p -l print-pid complete -c foot -x -s d -l log-level -a "info warning error none" -d "log-level (warning)" complete -c foot -x -s l -l log-colorize -a "always never auto" -d "enable or disable colorization of log output on stderr" complete -c foot -s S -l log-no-syslog -d "disable syslog logging (server mode only)" -complete -c foot -x -l pty -a '(__fish_complete_path)' -d "display an existing pty instead of creating one" +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" From 60fd4a262c5a3ce1c3951e305bcade3a88019f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 15 Mar 2024 15:19:43 +0100 Subject: [PATCH 0643/1323] sixel: initialize the color table to colors used by the VT340 --- CHANGELOG.md | 4 ++++ sixel.c | 39 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a67ff8fa..04571604 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -143,6 +143,10 @@ * 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 diff --git a/sixel.c b/sixel.c index 7684e657..f485e4c1 100644 --- a/sixel.c +++ b/sixel.c @@ -4,7 +4,7 @@ #include <limits.h> #define LOG_MODULE "sixel" -#define LOG_ENABLE_DBG 0 +#define LOG_ENABLE_DBG 1 #include "log.h" #include "debug.h" #include "grid.h" @@ -19,6 +19,29 @@ static size_t count; static void sixel_put_generic(struct terminal *term, uint8_t c); static void sixel_put_ar_11(struct terminal *term, uint8_t c); +/* 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) { @@ -68,17 +91,27 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) term->sixel.image.width = 0; term->sixel.image.height = 0; - /* TODO: default palette */ - 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 */ } From f17b989650a00d5b888c9634bded4407198b7c69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 16 Mar 2024 08:57:15 +0100 Subject: [PATCH 0644/1323] sixel: disable debug logging --- sixel.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sixel.c b/sixel.c index f485e4c1..0cb089ed 100644 --- a/sixel.c +++ b/sixel.c @@ -4,7 +4,7 @@ #include <limits.h> #define LOG_MODULE "sixel" -#define LOG_ENABLE_DBG 1 +#define LOG_ENABLE_DBG 0 #include "log.h" #include "debug.h" #include "grid.h" From 27330a5dd6d544acf80272e6173418d5e47d259b Mon Sep 17 00:00:00 2001 From: Craig Barnes <craigbarnes@protonmail.com> Date: Fri, 15 Mar 2024 17:51:51 +0000 Subject: [PATCH 0645/1323] csi: indicate "permanently reset" for DECRQM queries of mode 67 (DECBKM) This allows dynamic querying for the equivalent of terminfo's "kbs" capability. --- CHANGELOG.md | 4 +++- README.md | 6 +++--- csi.c | 8 ++++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04571604..53d0bf39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,14 +64,16 @@ * 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`). +* `DECRQM` queries for private mode 67 ([`DECBKM`]) now reply with mode + value 4 ("permanently reset") instead of 0 ("not recognized"). * Rectangular edit functions: `DECCARA`, `DECRARA`, `DECCRA`, `DECFRA` and `DECERA` ([#1633][1633]). * `Rect` 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 diff --git a/README.md b/README.md index b8d67e96..eb7fe81c 100644 --- a/README.md +++ b/README.md @@ -428,13 +428,13 @@ mode_, `\E[?1034l`), and enabled again with `smm` (_set meta mode_, ## Backspace Foot transmits DEL (`^?`) on <kbd>backspace</kbd>. 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 <kbd>ctrl</kbd>+<kbd>backspace</kbd>. -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 <kbd>alt</kbd> will prefix the transmitted byte with diff --git a/csi.c b/csi.c index 0d1ee1f4..04777310 100644 --- a/csi.c +++ b/csi.c @@ -324,6 +324,11 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) term->keypad_keys_mode = enable ? KEYPAD_APPLICATION : KEYPAD_NUMERICAL; break; + case 67: + if (enable) + LOG_WARN("unimplemented: DECBKM"); + break; + case 80: term->sixel.scrolling = !enable; break; @@ -555,6 +560,7 @@ decrqm(const struct terminal *term, unsigned param) case 25: return decrpm(!term->hide_cursor); case 45: return decrpm(term->reverse_wrap); case 66: return decrpm(term->keypad_keys_mode == KEYPAD_APPLICATION); + case 67: return DECRPM_PERMANENTLY_RESET; /* https://vt100.net/docs/vt510-rm/DECBKM */ case 80: return decrpm(!term->sixel.scrolling); case 1000: return decrpm(term->mouse_tracking == MOUSE_CLICK); case 1001: return DECRPM_PERMANENTLY_RESET; @@ -600,6 +606,7 @@ xtsave(struct terminal *term, unsigned param) case 45: term->xtsave.reverse_wrap = term->reverse_wrap; break; case 47: term->xtsave.alt_screen = term->grid == &term->alt; break; case 66: term->xtsave.application_keypad_keys = term->keypad_keys_mode == KEYPAD_APPLICATION; break; + case 67: break; case 80: term->xtsave.sixel_display_mode = !term->sixel.scrolling; break; case 1000: term->xtsave.mouse_click = term->mouse_tracking == MOUSE_CLICK; break; case 1001: break; @@ -642,6 +649,7 @@ xtrestore(struct terminal *term, unsigned param) case 45: enable = term->xtsave.reverse_wrap; break; case 47: enable = term->xtsave.alt_screen; break; case 66: enable = term->xtsave.application_keypad_keys; break; + case 67: return; case 80: enable = term->xtsave.sixel_display_mode; break; case 1000: enable = term->xtsave.mouse_click; break; case 1001: return; From 578765ad8358b74949ad1296703aec4dec7d004a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 16 Mar 2024 15:30:29 +0100 Subject: [PATCH 0646/1323] wayland: skip loading cursor theme when using server side cursors We don't need the client side cursor theme when using server side cursors. --- render.c | 2 +- wayland.c | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/render.c b/render.c index 91472027..bda43f1a 100644 --- a/render.c +++ b/render.c @@ -4604,7 +4604,7 @@ bool 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) { diff --git a/wayland.c b/wayland.c index fe3cba20..2645c4ab 100644 --- a/wayland.c +++ b/wayland.c @@ -1930,6 +1930,11 @@ wayl_reload_xcursor_theme(struct seat *seat, float new_scale) seat->pointer.cursor = NULL; } + if (seat->pointer.shape_device != NULL) { + /* Using server side cursors */ + return true; + } + int xcursor_size = 24; { From 853be450bb95cb7b022f6dad4263e4a92bf18e4b Mon Sep 17 00:00:00 2001 From: Craig Barnes <craigbarnes@protonmail.com> Date: Sat, 16 Mar 2024 20:17:53 +0000 Subject: [PATCH 0647/1323] main/config: replace some uses of xasprintf() with xstrjoin() --- config.c | 8 ++++---- main.c | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/config.c b/config.c index ab0e2927..ff92cb46 100644 --- a/config.c +++ b/config.c @@ -355,9 +355,9 @@ open_config(void) /* First, check XDG_CONFIG_HOME (or .config, if unset) */ if (xdg_config_home != NULL && xdg_config_home[0] != '\0') - path = xasprintf("%s/foot/foot.ini", xdg_config_home); + path = xstrjoin(xdg_config_home, "/foot/foot.ini"); else if (home_dir != NULL) - path = xasprintf("%s/.config/foot/foot.ini", home_dir); + path = xstrjoin(home_dir, "/.config/foot/foot.ini"); if (path != NULL) { LOG_DBG("checking for %s", path); @@ -382,7 +382,7 @@ open_config(void) conf_dir = strtok(NULL, ":")) { free(path); - path = xasprintf("%s/foot/foot.ini", conf_dir); + path = xstrjoin(conf_dir, "/foot/foot.ini"); LOG_DBG("checking for %s", path); int fd = open(path, O_RDONLY | O_CLOEXEC); @@ -2861,7 +2861,7 @@ get_server_socket_path(void) const char *wayland_display = getenv("WAYLAND_DISPLAY"); if (wayland_display == NULL) { - return xasprintf("%s/foot.sock", xdg_runtime); + return xstrjoin(xdg_runtime, "/foot.sock"); } return xasprintf("%s/foot-%s.sock", xdg_runtime, wayland_display); diff --git a/main.c b/main.c index 24ba45ca..207e6eb7 100644 --- a/main.c +++ b/main.c @@ -261,7 +261,7 @@ main(int argc, char *const *argv) break; case 't': - tll_push_back(overrides, xasprintf("term=%s", optarg)); + tll_push_back(overrides, xstrjoin("term=", optarg)); break; case 'L': @@ -269,11 +269,11 @@ main(int argc, char *const *argv) 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': { @@ -287,7 +287,7 @@ main(int argc, char *const *argv) } case 'f': { - char *font_override = xasprintf("font=%s", optarg); + char *font_override = xstrjoin("font=", optarg); tll_push_back(overrides, font_override); break; } From e8b04e0e2c9e8a4f825193b907d04c3b3165a842 Mon Sep 17 00:00:00 2001 From: Craig Barnes <craigbarnes@protonmail.com> Date: Sat, 16 Mar 2024 20:28:10 +0000 Subject: [PATCH 0648/1323] xmalloc: add xmemdup() and use to replace some uses of xmalloc+memcpy --- config.c | 15 +++++---------- grid.c | 6 ++---- terminal.c | 5 +---- xmalloc.h | 6 ++++++ 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/config.c b/config.c index ff92cb46..d2b75180 100644 --- a/config.c +++ b/config.c @@ -2902,8 +2902,7 @@ add_default_key_bindings(struct config *conf) }; 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)); } @@ -2954,8 +2953,7 @@ add_default_search_bindings(struct config *conf) }; 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 @@ -2970,8 +2968,7 @@ add_default_url_bindings(struct config *conf) }; 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 @@ -2994,8 +2991,7 @@ add_default_mouse_bindings(struct config *conf) }; 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 @@ -3388,8 +3384,7 @@ key_binding_list_clone(struct config_key_binding_list *dst, if (old->aux.master_copy) { const size_t len = old->aux.text.len; new->aux.text.len = len; - new->aux.text.data = xmalloc(len); - memcpy(new->aux.text.data, old->aux.text.data, len); + new->aux.text.data = xmemdup(old->aux.text.data, len); last_master_text_len = len; last_master_text_data = new->aux.text.data; diff --git a/grid.c b/grid.c index 03ceb0ec..85e6183f 100644 --- a/grid.c +++ b/grid.c @@ -263,8 +263,7 @@ grid_snapshot(const struct grid *grid) 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, @@ -284,8 +283,7 @@ grid_snapshot(const struct grid *grid) 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, diff --git a/terminal.c b/terminal.c index 3cf44d5a..e4396585 100644 --- a/terminal.c +++ b/terminal.c @@ -51,11 +51,8 @@ static void enqueue_data_for_slave(const void *data, size_t len, size_t offset, ptmx_buffer_list_t *buffer_list) { - void *copy = xmalloc(len); - memcpy(copy, data, len); - struct ptmx_buffer queued = { - .data = copy, + .data = xmemdup(data, len), .len = len, .idx = offset, }; diff --git a/xmalloc.h b/xmalloc.h index 74282f8f..67fa5c43 100644 --- a/xmalloc.h +++ b/xmalloc.h @@ -18,6 +18,12 @@ 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) { From 5f41eb798b639774d5cb2a7656fbaf4c61a16352 Mon Sep 17 00:00:00 2001 From: Craig Barnes <craigbarnes@protonmail.com> Date: Sun, 17 Mar 2024 15:27:22 +0000 Subject: [PATCH 0649/1323] base64: simplify lookup table initializer --- base64.c | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/base64.c b/base64.c index fe89e9fa..5d01ab07 100644 --- a/base64.c +++ b/base64.c @@ -36,12 +36,9 @@ static const uint8_t reverse_lookup[256] = { }; static const char lookup[64] = { - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', - 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', - 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - '+', '/', + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/" }; char * From 282c55aa4ac9a082392ee13107958fd0b84e7767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 4 Mar 2024 16:18:49 +0100 Subject: [PATCH 0650/1323] sixel: place cursor on the last character row touched by the sixel After emitting a sixel, place the cursor on the character row touched by the last sixel. The last sixel _may_ not be a multiple of 6 pixels, *if* the sixel had an explicit width/height set via raster attributes. This is an intended deviation from the DEC cursor placement algorithm, where the cursor is placed on the character row touched by the last sixel's *upper* pixel. The adjusted algorithm implemented here makes it much easier for applications to handle text-after-sixels, since they can now simply assume a single text newline is required to move the cursor to a character row not touched by the sixel. --- CHANGELOG.md | 4 ++++ sixel.c | 16 ++++------------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53d0bf39..7d3a1662 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -109,6 +109,9 @@ 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]). [1526]: https://codeberg.org/dnkl/foot/issues/1526 [1528]: https://codeberg.org/dnkl/foot/issues/1528 @@ -116,6 +119,7 @@ [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 ### Deprecated diff --git a/sixel.c b/sixel.c index 0cb089ed..586eaf2e 100644 --- a/sixel.c +++ b/sixel.c @@ -1230,19 +1230,11 @@ sixel_unhook(struct terminal *term) 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 term_rows = + (image.original.height + term->cell_height - 1) / term->cell_height; xassert(term_rows <= image.rows); From cc660bc7c1a55e71f8c7a131fbcf8753b447f977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 11 Mar 2024 16:25:45 +0100 Subject: [PATCH 0651/1323] sixel: trim trailing, fully transparent sixel rows See https://github.com/hpjansson/chafa/issues/192 --- CHANGELOG.md | 2 ++ sixel.c | 49 ++++++++++++++++++++++++++++++++++++++----------- terminal.h | 1 + 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d3a1662..960b197c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,8 @@ * 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]). [1526]: https://codeberg.org/dnkl/foot/issues/1526 [1528]: https://codeberg.org/dnkl/foot/issues/1528 diff --git a/sixel.c b/sixel.c index 586eaf2e..a16fee23 100644 --- a/sixel.c +++ b/sixel.c @@ -90,6 +90,7 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) term->sixel.image.p = NULL; term->sixel.image.width = 0; term->sixel.image.height = 0; + term->sixel.image.bottom_pixel = 0; if (term->sixel.use_private_palette) { xassert(term->sixel.private_palette == NULL); @@ -1096,6 +1097,23 @@ sixel_reflow(struct terminal *term) void sixel_unhook(struct terminal *term) { + /* 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); + + term->sixel.image.height -= 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); @@ -1493,9 +1511,6 @@ static void 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; for (int i = 0; i < 6; i++, sixel >>= 1) { @@ -1513,8 +1528,6 @@ 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); if (sixel & 0x01) @@ -1548,17 +1561,22 @@ sixel_add_many_generic(struct terminal *term, uint8_t c, unsigned count) resize_horizontally(term, col + count); width = term->sixel.image.width; count = min(count, max(width - col, 0)); + + if (unlikely(count == 0)) + return; } uint32_t color = term->sixel.color; uint32_t *data = term->sixel.image.p; uint32_t *end = data + count; + term->sixel.pos.col = col + count; + term->sixel.image.p = end; + term->sixel.image.bottom_pixel |= c; + for (; data < end; data++) sixel_add_generic(term, data, width, color, c); - term->sixel.pos.col = col + count; - term->sixel.image.p = end; } static void ALWAYS_INLINE inline @@ -1579,10 +1597,13 @@ sixel_add_one_ar_11(struct terminal *term, uint8_t c) return; } - sixel_add_ar_11(term, term->sixel.image.p, width, term->sixel.color, c); + 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 @@ -1598,17 +1619,22 @@ sixel_add_many_ar_11(struct terminal *term, uint8_t c, unsigned count) resize_horizontally(term, col + count); width = term->sixel.image.width; count = min(count, max(width - col, 0)); + + if (unlikely(count == 0)) + return; } uint32_t color = term->sixel.color; uint32_t *data = term->sixel.image.p; uint32_t *end = data + count; + term->sixel.pos.col += count; + term->sixel.image.p = end; + term->sixel.image.bottom_pixel |= c; + for (; data < end; data++) sixel_add_ar_11(term, data, width, color, c); - term->sixel.pos.col += count; - term->sixel.image.p = end; } IGNORE_WARNING("-Wpedantic") @@ -1650,9 +1676,10 @@ decsixel_generic(struct terminal *term, uint8_t c) } break; - case '-': + case '-': /* GNL - Graphical New Line */ term->sixel.pos.row += 6 * term->sixel.pan; term->sixel.pos.col = 0; + term->sixel.image.bottom_pixel = 0; term->sixel.image.p = &term->sixel.image.data[term->sixel.pos.row * term->sixel.image.width]; if (term->sixel.pos.row >= term->sixel.image.height) { diff --git a/terminal.h b/terminal.h index eeff8af0..a0e3d9d4 100644 --- a/terminal.h +++ b/terminal.h @@ -677,6 +677,7 @@ struct terminal { uint32_t *p; /* Pointer into data, for current position */ int width; /* Image width, in pixels */ int height; /* Image height, in pixels */ + unsigned int bottom_pixel; } image; /* From cb820a498bc8b9c3823f54d0452c2e5d30de1996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 15 Mar 2024 15:11:44 +0100 Subject: [PATCH 0652/1323] sixel: RA region does not affect the text cursor position The raster attributes is, really, just a way to erase an area. And, if the sixel is transparent, it's a nop. The final text cursor position depends, not on our image size (which is based on RA), but on the final graphical cursor position. However, we do want to continue using it as a hint of the final image size, to be able to pre-allocate the backing buffer. So, here's what we do: * When trimming trailing transparent rows, only trim the *image*, if the graphical cursor is positioned on the last sixel row, *and* the sixel is transparent. * Opaque sixels aren't trimmed at all, since RA in this acts as an erase that fills the RA region with the background color. * The graphical cursor position is always adjusted (i.e. trimmed), since it affects the text cursor position. * The text cursor position is now calculated from the graphical cursor position, instead of the image height. --- sixel.c | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- terminal.h | 1 + 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/sixel.c b/sixel.c index a16fee23..1340d875 100644 --- a/sixel.c +++ b/sixel.c @@ -90,6 +90,7 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) term->sixel.image.p = NULL; term->sixel.image.width = 0; term->sixel.image.height = 0; + term->sixel.image.alloc_height = 0; term->sixel.image.bottom_pixel = 0; if (term->sixel.use_private_palette) { @@ -1097,6 +1098,27 @@ sixel_reflow(struct terminal *term) 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 */ @@ -1111,7 +1133,32 @@ sixel_unhook(struct terminal *term) "rows-to-trim=%d*%d", term->sixel.image.bottom_pixel, bits, leading_zeroes, rows_to_trim, term->sixel.pan); - term->sixel.image.height -= 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 && + term->sixel.transparent_bg) + { + LOG_DBG("trimming image"); + term->sixel.image.height = term->sixel.image.alloc_height - rows_to_trim * term->sixel.pan; + } 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; @@ -1422,6 +1469,7 @@ resize_vertically(struct terminal *term, int new_height) } 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; @@ -1502,6 +1550,7 @@ resize(struct terminal *term, int new_width, int new_height) term->sixel.image.data = new_data; term->sixel.image.width = new_width; term->sixel.image.height = new_height; + term->sixel.image.alloc_height = alloc_new_height; term->sixel.image.p = &term->sixel.image.data[term->sixel.pos.row * new_width + term->sixel.pos.col]; return true; @@ -1682,7 +1731,7 @@ decsixel_generic(struct terminal *term, uint8_t c) 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; } @@ -1752,9 +1801,42 @@ decgra(struct terminal *term, uint8_t c) 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); } diff --git a/terminal.h b/terminal.h index a0e3d9d4..953f3329 100644 --- a/terminal.h +++ b/terminal.h @@ -677,6 +677,7 @@ struct terminal { 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; From bce1d7313dfe04f92b7a37c0352637045b4881dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 15 Mar 2024 17:23:21 +0100 Subject: [PATCH 0653/1323] sixel: hopefully handle image height correctly when trimming When trimming trailing empty rows from the final image, handle the RA region correctly: * If image is transparent, trim down to the last non-empty pixel row, regardless of whether it is inside or outside the RA region. * If image is opaque, trim down to either the last non-empty pixel row, or the RA region, whatever is the largest height. --- sixel.c | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/sixel.c b/sixel.c index 1340d875..c9df4ab4 100644 --- a/sixel.c +++ b/sixel.c @@ -1148,11 +1148,23 @@ sixel_unhook(struct terminal *term) * * 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 && - term->sixel.transparent_bg) - { + if (term->sixel.pos.row + 6 * term->sixel.pan >= term->sixel.image.alloc_height) { LOG_DBG("trimming image"); - term->sixel.image.height = term->sixel.image.alloc_height - rows_to_trim * term->sixel.pan; + 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"); } From 9fcf5977c05686d55892d32fa3f980b03ef1b30a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 15 Mar 2024 17:39:43 +0100 Subject: [PATCH 0654/1323] sixel: fix cursor positioning when image is split up into several --- sixel.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sixel.c b/sixel.c index c9df4ab4..01f45d6b 100644 --- a/sixel.c +++ b/sixel.c @@ -1310,8 +1310,11 @@ sixel_unhook(struct terminal *term) * Position the text cursor based on the text row * touched by the last sixel */ + const int pixel_rows = pixel_rows_left > 0 + ? image.original.height + : term->sixel.pos.row; const int term_rows = - (image.original.height + term->cell_height - 1) / term->cell_height; + (pixel_rows + term->cell_height - 1) / term->cell_height; xassert(term_rows <= image.rows); @@ -1324,6 +1327,8 @@ sixel_unhook(struct terminal *term) ? 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 */ From dcd4ab4ab8da8de458acbfc3a4cf2015e88dec3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 25 Mar 2024 16:33:00 +0100 Subject: [PATCH 0655/1323] scripts: generate-alt-random: generate both opaque and transparent sixels --- scripts/generate-alt-random-writes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/generate-alt-random-writes.py b/scripts/generate-alt-random-writes.py index 789d64e0..656a2b9d 100755 --- a/scripts/generate-alt-random-writes.py +++ b/scripts/generate-alt-random-writes.py @@ -207,8 +207,9 @@ def main(): 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. From a99434929ced7673087458d18c4f78ae8a4c962a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 25 Mar 2024 16:33:15 +0100 Subject: [PATCH 0656/1323] sixel: abuse wmemset() when initializing a freshly allocated image buffer wmemset() is heavily optimized, and in some cases, *much* faster than manually initializing the new image pixels. Furthermore, assume calloc() is better at initializing memory to zero, and use that when initializing new pixels in a transparent image. --- sixel.c | 114 ++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 77 insertions(+), 37 deletions(-) diff --git a/sixel.c b/sixel.c index 01f45d6b..a7cbbe18 100644 --- a/sixel.c +++ b/sixel.c @@ -1385,21 +1385,29 @@ sixel_unhook(struct terminal *term) render_refresh(term); } -static void -resize_horizontally(struct terminal *term, int new_width) +static void ALWAYS_INLINE inline +memset_u32(uint32_t *data, uint32_t value, size_t count) { - if (unlikely(new_width > term->sixel.max_width)) { + static_assert(sizeof(wchar_t) == 4, "wchar_t is not 4 bytes"); + wmemset((wchar_t *)data, (wchar_t)value, count); +} + +static void +resize_horizontally(struct terminal *term, int new_width_mutable) +{ + if (unlikely(new_width_mutable > term->sixel.max_width)) { LOG_WARN("maximum image dimensions exceeded, truncating"); - new_width = 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)) { @@ -1424,13 +1432,13 @@ resize_horizontally(struct terminal *term, int new_width) 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); @@ -1443,7 +1451,7 @@ resize_horizontally(struct terminal *term, int new_width) } 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, @@ -1477,13 +1485,11 @@ resize_vertically(struct terminal *term, int new_height) 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.height = new_height; term->sixel.image.alloc_height = alloc_height; @@ -1498,43 +1504,69 @@ resize_vertically(struct terminal *term, int new_height) } 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); - 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; @@ -1545,22 +1577,30 @@ resize(struct terminal *term, int new_width, int new_height) } else { /* Width (and thus stride) change - need to allocate a new buffer */ xassert(new_width > old_width); - new_data = xmalloc(alloc_new_width * alloc_new_height * sizeof(uint32_t)); + const size_t pixels = new_width * alloc_new_height; + + new_data = !initialize_bg + ? xcalloc(pixels, sizeof(uint32_t)) + : xmalloc(pixels * sizeof(uint32_t)); /* Copy old rows, and initialize new columns to background color */ - for (int r = 0; r < min(old_height, new_height); r++) { - memcpy(&new_data[r * new_width], &old_data[r * old_width], old_width * sizeof(uint32_t)); + const int row_copy_count = min(old_height, alloc_new_height); + const uint32_t *end = &new_data[row_copy_count * new_width]; - for (int c = old_width; c < new_width; c++) - new_data[r * new_width + c] = bg; + for (uint32_t *n = new_data, *o = old_data; + n < end; + n += new_width, o += old_width) + { + memcpy(n, o, old_width * sizeof(uint32_t)); + memset_u32(&n[old_width], bg, new_width - old_width); } free(old_data); } - /* Initialize new rows to background color */ - for (int r = old_height; r < new_height; r++) { - for (int c = 0; c < new_width; c++) - new_data[r * new_width + c] = bg; + if (initialize_bg) { + memset_u32(&new_data[old_height * new_width], + bg, + (alloc_new_height - old_height) * new_width); } xassert(new_data != NULL); From a34ae5d527616c33a0f544e82db56cba9a14e0e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 29 Mar 2024 11:40:15 +0100 Subject: [PATCH 0657/1323] terminfo: add fe/fd (focus enable/disable) These are new capabilities, recently added to ncurses. Applications are supposed to use these, instead of XM (to enable focus events). So, remove "CSI ? 1004h" from XM. --- CHANGELOG.md | 5 +++++ foot.info | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 960b197c..6da8ef0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,8 @@ * 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. [1348]: https://codeberg.org/dnkl/foot/issues/1348 [1633]: https://codeberg.org/dnkl/foot/issues/1633 @@ -114,6 +116,9 @@ 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. [1526]: https://codeberg.org/dnkl/foot/issues/1526 [1528]: https://codeberg.org/dnkl/foot/issues/1528 diff --git a/foot.info b/foot.info index 319d0781..fe7e0632 100644 --- a/foot.info +++ b/foot.info @@ -43,7 +43,7 @@ Ss=\E[%p1%d q, 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, @@ -75,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, From 8e79ceba9e6fee212ba53bab444cb504d1cff69b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 29 Mar 2024 11:40:35 +0100 Subject: [PATCH 0658/1323] terminfo: add 'nel' capability --- CHANGELOG.md | 1 + foot.info | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6da8ef0c..4d1f94f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,7 @@ * `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 diff --git a/foot.info b/foot.info index fe7e0632..c8a4da77 100644 --- a/foot.info +++ b/foot.info @@ -231,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, From 3d0d9036fdfa555a442ed9b94224b6b73aa82d44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 29 Mar 2024 11:40:47 +0100 Subject: [PATCH 0659/1323] terminfo: tighten up the rv/xr regular expressions --- CHANGELOG.md | 2 ++ foot.info | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d1f94f9..d3244834 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,6 +120,8 @@ * `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. [1526]: https://codeberg.org/dnkl/foot/issues/1526 [1528]: https://codeberg.org/dnkl/foot/issues/1528 diff --git a/foot.info b/foot.info index c8a4da77..cf5c7b82 100644 --- a/foot.info +++ b/foot.info @@ -251,7 +251,7 @@ 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, @@ -275,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, From 2138273c16dfa51f109f2f624e5294619c6aa61e Mon Sep 17 00:00:00 2001 From: Matheus Afonso Martins Moreira <matheus.a.m.moreira@gmail.com> Date: Sun, 31 Mar 2024 20:36:55 -0300 Subject: [PATCH 0660/1323] themes: add neon theme Add a neon theme file for foot, originally from xcolors.net/neon and also available at terminal.love. --- themes/neon | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 themes/neon diff --git a/themes/neon b/themes/neon new file mode 100644 index 00000000..d11a36d0 --- /dev/null +++ b/themes/neon @@ -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 From 82c1a28e6f8d4290b7de421837b44e6acadcda9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 1 Apr 2024 08:49:27 +0200 Subject: [PATCH 0661/1323] changelog: move DECRQM for private mode 67 from "added" to "changed" --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3244834..6d6ba119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,8 +64,6 @@ * 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`). -* `DECRQM` queries for private mode 67 ([`DECBKM`]) now reply with mode - value 4 ("permanently reset") instead of 0 ("not recognized"). * Rectangular edit functions: `DECCARA`, `DECRARA`, `DECCRA`, `DECFRA` and `DECERA` ([#1633][1633]). * `Rect` capability to terminfo. @@ -122,6 +120,8 @@ 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 From d5dc0b2f49d083a5a23ea93f60010cd9ad190939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 2 Apr 2024 16:27:25 +0200 Subject: [PATCH 0662/1323] changelog: prepare for 1.17.0 --- CHANGELOG.md | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d6ba119..1013add5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.17.0](#1-17-0) * [1.16.2](#1-16-2) * [1.16.1](#1-16-1) * [1.16.0](#1-16-0) @@ -49,7 +49,8 @@ * [1.2.0](#1-2-0) -## Unreleased +## 1.17.0 + ### Added - Support for opening an existing PTY, e.g. a VM console. @@ -76,6 +77,7 @@ [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 @@ -132,8 +134,6 @@ [chafa-192]: https://github.com/hpjansson/chafa/issues/192 -### Deprecated -### Removed ### Fixed * config: improved validation of color values. @@ -172,9 +172,24 @@ [1624]: https://codeberg.org/dnkl/foot/issues/1624 -### Security ### 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 From 21951feb2be7943356345a80a647315f3f1c2721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 2 Apr 2024 16:27:54 +0200 Subject: [PATCH 0663/1323] meson: bump version to 1.17.0 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 719352bc..3db4616e 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.16.2', + version: '1.17.0', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From 51e8b4f533b7f8b4db7ee89b72dc333fc337d92c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 2 Apr 2024 16:34:08 +0200 Subject: [PATCH 0664/1323] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1013add5..f155d116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.17.0](#1-17-0) * [1.16.2](#1-16-2) * [1.16.1](#1-16-1) @@ -49,6 +50,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.17.0 ### Added From 3cc94ab4e89b59013ae374f0516736ec9b648c8b Mon Sep 17 00:00:00 2001 From: tunjan <tunjan@noreply.codeberg.org> Date: Fri, 5 Apr 2024 11:11:21 +0000 Subject: [PATCH 0665/1323] fix typo in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eb7fe81c..dc5e30e6 100644 --- a/README.md +++ b/README.md @@ -304,7 +304,7 @@ when starting your Wayland compositor (i.e. logging in to your desktop), and then run `footclient` instead of `foot` whenever you want to launch a new terminal. -Foot support socket activation, which means `foot --server` will only be +Foot supports socket activation, which means `foot --server` will only be started the first time you'll run `footclient`. (systemd user units are included, but it can work with other supervision suites). From 4f1aaccf812b5ed5f1cf12184ac970b396519f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 5 Apr 2024 16:19:24 +0200 Subject: [PATCH 0666/1323] log: fix syslog not respecting the configured log level --- CHANGELOG.md | 4 ++++ log.c | 3 +++ 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f155d116..4ff49390 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,10 @@ ### Deprecated ### Removed ### Fixed + +* Log-level not respected by syslog. + + ### Security ### Contributors diff --git a/log.c b/log.c index c13b4179..d19adaca 100644 --- a/log.c +++ b/log.c @@ -105,6 +105,9 @@ _sys_log(enum log_class log_class, const char *module, if (!do_syslog) return; + if (log_class > log_level) + return; + /* Map our log level to syslog's level */ int level = log_level_map[log_class].syslog_equivalent; From 88a3b54ca3d0732db8a89b62d3373c66c970327f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 5 Apr 2024 16:19:47 +0200 Subject: [PATCH 0667/1323] wayland: only use the surface preferred scale if set by the compositor Before this patch, we would, in some cases, fallback to the surface preferred (not fractional) scaling, even though the compositor hadn't actually published a preferred buffer scale; the presence of a v6 compositor interface doesn't mean we've actually received a preferred scale yet. --- terminal.c | 2 +- wayland.c | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/terminal.c b/terminal.c index e4396585..6144bacb 100644 --- a/terminal.c +++ b/terminal.c @@ -2125,7 +2125,7 @@ term_fractional_scaling(const struct terminal *term) bool term_preferred_buffer_scale(const struct terminal *term) { - return term->wl->has_wl_compositor_v6; + return term->wl->has_wl_compositor_v6 && term->window->preferred_buffer_scale > 0; } bool diff --git a/wayland.c b/wayland.c index 2645c4ab..1d9d3cdf 100644 --- a/wayland.c +++ b/wayland.c @@ -1738,10 +1738,6 @@ wayl_win_init(struct terminal *term, const char *token) win->fractional_scale, &fractional_scale_listener, win); } - if (wayl->has_wl_compositor_v6) { - win->preferred_buffer_scale = 1; - } - win->xdg_surface = xdg_wm_base_get_xdg_surface(wayl->shell, win->surface.surf); xdg_surface_add_listener(win->xdg_surface, &xdg_surface_listener, win); From c7848c4e75c3cb5704378137c5b5c989135f81b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 5 Apr 2024 16:22:42 +0200 Subject: [PATCH 0668/1323] term: don't shutdown terminal when PTY is closed, unless --pty was used Unless --pty has been used, we do *not* want to shutdown the terminal when the PTY is closed by the client application; we want to wait for the client application to actually terminate. This was the behavior before the --pty patch. This was changed in the --pty patch, since then, we don't *have* a client application. That is, foot has not forked+exec:ed anything. The only way to trigger a shutdown (from the client side) is to close the PTY. This patch restores the old behavior, when --pty is *not* used. Closes #1666 --- CHANGELOG.md | 5 +++++ terminal.c | 14 +++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ff49390..66d6dc42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,11 @@ ### 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]). + +[1666]: https://codeberg.org/dnkl/foot/issues/1666 ### Security diff --git a/terminal.c b/terminal.c index 6144bacb..acb5a639 100644 --- a/terminal.c +++ b/terminal.c @@ -364,8 +364,20 @@ fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) del_utmp_record(term->conf, term->reaper, term->ptmx); fdm_del(fdm, fd); term->ptmx = -1; - if (!term->conf->hold_at_exit) + + /* + * 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; From 7f4328e0b1d5e47aa096c0d3cd8bfea143f80a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 5 Apr 2024 16:27:56 +0200 Subject: [PATCH 0669/1323] term: send SIGHUP before SIGTERM when shutting down If we're the ones initiating shutdown, start by sending SIGHUP. Only if the client application does not terminate, send SIGTERM (and if it still refuses to terminate, send SIGKILL). Also reduce the timeout between the signals from 60s to 30s. --- CHANGELOG.md | 3 +++ terminal.c | 70 ++++++++++++++++++++++++++++++++++++++++++---------- terminal.h | 1 + 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66d6dc42..b0c62d36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,9 @@ * 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`. [1666]: https://codeberg.org/dnkl/foot/issues/1666 diff --git a/terminal.c b/terminal.c index acb5a639..3fb918a4 100644 --- a/terminal.c +++ b/terminal.c @@ -1539,10 +1539,36 @@ fdm_terminate_timeout(struct fdm *fdm, int fd, int events, void *data) struct terminal *term = data; xassert(!term->shutdown.client_has_terminated); - LOG_DBG("slave (PID=%u) has not terminated, sending SIGKILL (%d)", - term->slave, SIGKILL); + LOG_DBG("slave (PID=%u) has not terminated, sending %s (%d)", + term->slave, + term->shutdown.next_signal == SIGTERM ? "SIGTERM" + : term->shutdown.next_signal == SIGKILL ? "SIGKILL" + : "<unknown>", + term->shutdown.next_signal); + + kill(-term->slave, term->shutdown.next_signal); + + switch (term->shutdown.next_signal) { + case SIGTERM: + term->shutdown.next_signal = SIGKILL; + break; + + case SIGKILL: + /* Disarm. Shouldn't be necessary, as we should be able to + shutdown completely after sending SIGKILL, before the next + timeout occurs). But lets play it safe... */ + if (term->shutdown.terminate_timeout_fd >= 0) { + timerfd_settime( + term->shutdown.terminate_timeout_fd, 0, + &(const struct itimerspec){0}, NULL); + } + break; + + default: + BUG("can only handle SIGTERM and SIGKILL"); + return false; + } - kill(-term->slave, SIGKILL); return true; } @@ -1583,11 +1609,18 @@ term_shutdown(struct terminal *term) term->shutdown.client_has_terminated = true; } else { LOG_DBG("initiating asynchronous 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); - const struct itimerspec timeout = {.it_value = {.tv_sec = 60}}; + /* + * Set up a timer, with an interval - on the first timeout + * we'll send SIGTERM. If the the client application still + * isn't terminating, we'll wait an additional interval, + * and then send SIGKILL. + */ + const struct itimerspec timeout = {.it_value = {.tv_sec = 30}, + .it_interval = {.tv_sec = 30}}; int timeout_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); if (timeout_fd < 0 || @@ -1602,6 +1635,7 @@ term_shutdown(struct terminal *term) xassert(term->shutdown.terminate_timeout_fd < 0); term->shutdown.terminate_timeout_fd = timeout_fd; + term->shutdown.next_signal = SIGTERM; } } @@ -1784,9 +1818,9 @@ term_destroy(struct terminal *term) exit_status = term->shutdown.exit_status; else { LOG_DBG("initiating blocking terminate of slave; " - "sending SIGTERM to PID=%u", term->slave); + "sending SIGHUP to PID=%u", term->slave); - kill(-term->slave, SIGTERM); + kill(-term->slave, SIGHUP); /* * we've closed the ptxm, and sent SIGTERM to the client @@ -1807,7 +1841,12 @@ term_destroy(struct terminal *term) struct sigaction action = {.sa_handler = &sig_alarm}; sigemptyset(&action.sa_mask); sigaction(SIGALRM, &action, NULL); - alarm(60); + + /* Wait, then send SIGTERM, wait again, then send SIGKILL */ + int next_signal = SIGTERM; + + alarm_raised = 0; + alarm(30); while (true) { int r = waitpid(term->slave, &exit_status, 0); @@ -1819,11 +1858,16 @@ term_destroy(struct terminal *term) xassert(errno == EINTR); if (alarm_raised) { - LOG_DBG( - "slave (PID=%u) has not terminate yet, " - "sending: SIGKILL (%d)", term->slave, SIGKILL); + LOG_DBG("slave (PID=%u) has not terminated yet, " + "sending: %s (%d)", term->slave, + next_signal == SIGTERM ? "SIGTERM" : "SIGKILL", + next_signal); - kill(-term->slave, SIGKILL); + kill(-term->slave, next_signal); + next_signal = SIGKILL; + + alarm_raised = 0; + alarm(30); } } } diff --git a/terminal.h b/terminal.h index 953f3329..fa80e693 100644 --- a/terminal.h +++ b/terminal.h @@ -723,6 +723,7 @@ struct terminal { bool client_has_terminated; int terminate_timeout_fd; int exit_status; + int next_signal; void (*cb)(void *data, int exit_code); void *cb_data; From ed5717c4cd9dfef6e74232508878a4c02e6d0746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Holger=20Wei=C3=9F?= <holger@zedat.fu-berlin.de> Date: Sat, 6 Apr 2024 13:48:05 +0200 Subject: [PATCH 0670/1323] themes: add xterm theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Include a theme for using xterm’s default palette (based on the XTerm-col.ad file included with the xterm source code, version 390). --- themes/xterm | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 themes/xterm diff --git a/themes/xterm b/themes/xterm new file mode 100644 index 00000000..bf17f5e7 --- /dev/null +++ b/themes/xterm @@ -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 From 09e45794bc0fe734c0b6c41a18be41c028118149 Mon Sep 17 00:00:00 2001 From: Marcin Puc <tranzystorek.io@protonmail.com> Date: Sun, 7 Apr 2024 09:47:19 +0200 Subject: [PATCH 0671/1323] themes: add dracula-iterm --- themes/dracula-iterm | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 themes/dracula-iterm diff --git a/themes/dracula-iterm b/themes/dracula-iterm new file mode 100644 index 00000000..8c2f66c3 --- /dev/null +++ b/themes/dracula-iterm @@ -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 From 9287946b3672d21fabd70ba4aeebc5a63e6259f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 10 Apr 2024 05:44:33 +0200 Subject: [PATCH 0672/1323] dcs: DECRQSS: fix off-by-one when checking for space in the DCS buffer --- CHANGELOG.md | 2 ++ dcs.c | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0c62d36..f555d472 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,8 @@ * 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 diff --git a/dcs.c b/dcs.c index c4309459..19cce3c2 100644 --- a/dcs.c +++ b/dcs.c @@ -239,7 +239,7 @@ decrqss_put(struct terminal *term, uint8_t c) return; struct vt *vt = &term->vt; - if (vt->dcs.idx > 2) + if (vt->dcs.idx >= 2) return; vt->dcs.data[vt->dcs.idx++] = c; } From e5a2ac4b57c18556e2245ecbf2e4faeef46e2b9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 9 Apr 2024 16:28:54 +0200 Subject: [PATCH 0673/1323] config: add cursor.unfocused-style This option controls how we render the cursor when the terminal window is unfocused. Possible values are: * hollow: the default, and how we rendered the cursor before this patch. * unchanged: render the cursor exactly the same way as when the window is focused. * none: do not render any cursor at all Closes #1582 --- CHANGELOG.md | 7 +++++++ config.c | 11 +++++++++++ config.h | 6 ++++++ doc/foot.ini.5.scd | 9 +++++++++ render.c | 27 ++++++++++++++++++++------- tests/test-config.c | 6 ++++++ 6 files changed, 59 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f555d472..3d824c2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,13 @@ ## Unreleased ### Added + +* `cursor.unfocused-style=unchanged|hollow|none` to `foot.ini`. The + default is `hollow` ([#1582][1582]). + +[1582]: https://codeberg.org/dnkl/foot/issues/1582 + + ### Changed ### Deprecated ### Removed diff --git a/config.c b/config.c index d2b75180..6a8bb0a8 100644 --- a/config.c +++ b/config.c @@ -1383,6 +1383,16 @@ parse_section_cursor(struct context *ctx) (int *)&conf->cursor.style); } + else if (streq(key, "unfocused-style")) { + _Static_assert(sizeof(conf->cursor.unfocused_style) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"unchanged", "hollow", "none", NULL}, + (int *)&conf->cursor.unfocused_style); + } + else if (streq(key, "blink")) return value_to_bool(ctx, &conf->cursor.blink); @@ -3090,6 +3100,7 @@ config_load(struct config *conf, const char *conf_path, .cursor = { .style = CURSOR_BLOCK, + .unfocused_style = CURSOR_UNFOCUSED_HOLLOW, .blink = false, .color = { .text = 0, diff --git a/config.h b/config.h index 30219ff0..df3d45f9 100644 --- a/config.h +++ b/config.h @@ -28,6 +28,11 @@ struct font_size_adjustment { }; 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}; @@ -256,6 +261,7 @@ struct config { struct { enum cursor_style style; + enum cursor_unfocused_style unfocused_style; bool blink; struct { uint32_t text; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 2c50ca24..8cd0560f 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -500,6 +500,15 @@ applications can change these at runtime. *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_. diff --git a/render.c b/render.c index bda43f1a..804ccdc3 100644 --- a/render.c +++ b/render.c @@ -305,8 +305,8 @@ color_brighten(const struct terminal *term, uint32_t color) } 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 @@ draw_cursor(const struct terminal *term, const struct cell *cell, 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; - else if (likely(term->cursor_blink.state == CURSOR_BLINK_ON)) { + case CURSOR_UNFOCUSED_HOLLOW: + draw_hollow_block(term, pix, fg, x, y, cols); + return; + + case CURSOR_UNFOCUSED_NONE: + return; + } + } + + 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, @@ -1513,7 +1526,7 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, /* Hollow cursor */ if (start >= 0 && end <= term->cols) { int cols = end - start; - draw_unfocused_block(term, buf->pix[0], &cursor_color, x, y, cols); + draw_hollow_block(term, buf->pix[0], &cursor_color, x, y, cols); } term_ime_set_cursor_rect( @@ -3373,7 +3386,7 @@ render_search_box(struct terminal *term) /* TODO: how do we handle a partially hidden rectangle? */ if (start >= 0 && end <= visible_cells) { - draw_unfocused_block( + draw_hollow_block( term, buf->pix[0], &fg, x + start * term->cell_width, y, end - start); } term_ime_set_cursor_rect(term, diff --git a/tests/test-config.c b/tests/test-config.c index b54dd23e..3d4d345c 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -635,6 +635,12 @@ test_section_cursor(void) (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); From a4046e0c3daf84b028119e1ee28f7500f59f7d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 9 Apr 2024 16:39:18 +0200 Subject: [PATCH 0674/1323] code of conduct: initial code-of-conduct Based on [River's](https://codeberg.org/river/river) code of conduct, which 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 --- CODE_OF_CONDUCT.md | 83 ++++++++++++++++++++++++++++++++++++++++++++++ README.md | 6 ++++ 2 files changed, 89 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..4b652df6 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,83 @@ +# Foot Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +Participants in the foot community are expected to uphold the described +standards not only in official community spaces (issue trackers, IRC channels, +etc.) but in all public spaces. The Code of Conduct however does acknowledge +that people are fallible and that it is possible to 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 --git a/README.md b/README.md index dc5e30e6..c188f76c 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ The fast, lightweight and minimalistic Wayland terminal emulator. 1. [Programmatically checking if running in foot](#programmatically-checking-if-running-in-foot) 1. [XTGETTCAP](#xtgettcap) 1. [Credits](#Credits) +1. [Code of Conduct](#code-of-conduct) 1. [Bugs](#bugs) 1. [Contact](#contact) 1. [IRC](#irc) @@ -644,6 +645,11 @@ queried capabilities are **always** sent. No queries are ever dropped. contributing foot's [logo](icons/hicolor/48x48/apps/foot.png). +# Code of Conduct + +See [Code of Conduct](CODE_OF_CONDUCT.md) + + # Bugs Please report bugs to https://codeberg.org/dnkl/foot/issues From 94f749a40db01a1856d5935a3d894d98b7c0eaf3 Mon Sep 17 00:00:00 2001 From: izmyname <izmyname@noreply.codeberg.org> Date: Wed, 10 Apr 2024 08:02:58 +0000 Subject: [PATCH 0675/1323] Add noirblaze theme --- themes/noirblaze | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 themes/noirblaze diff --git a/themes/noirblaze b/themes/noirblaze new file mode 100644 index 00000000..a06f3350 --- /dev/null +++ b/themes/noirblaze @@ -0,0 +1,31 @@ +# -*- conf -*- +# noirblaze-kitty +# https://github.com/n1ghtmare/noirblaze-kitty + + +[cursor] +color=121212 ff0088 + +[colors] +foreground=d5d5d5 +background=121212 + +# selectrion-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 From fa07c1ec67ef729d136b10f305149551f4f36deb Mon Sep 17 00:00:00 2001 From: izmyname <izmyname@noreply.codeberg.org> Date: Thu, 11 Apr 2024 00:22:07 +0000 Subject: [PATCH 0676/1323] Typo fix --- themes/noirblaze | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/themes/noirblaze b/themes/noirblaze index a06f3350..3cf452e6 100644 --- a/themes/noirblaze +++ b/themes/noirblaze @@ -10,7 +10,7 @@ color=121212 ff0088 foreground=d5d5d5 background=121212 -# selectrion-foreground=121212 +# selection-foreground=121212 # selection-background=b0b0b0 regular0=121212 # black From b400903e25de321dffd310e30d76dad121ccceb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 10 Apr 2024 19:26:23 +0200 Subject: [PATCH 0677/1323] config: add new key-binding 'quit', unbound by default Closes #1475 --- CHANGELOG.md | 2 ++ config.c | 1 + doc/foot.ini.5.scd | 3 +++ foot.ini | 1 + input.c | 4 ++++ key-binding.h | 3 ++- 6 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d824c2a..96c7c8bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,8 +55,10 @@ * `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 ### Changed diff --git a/config.c b/config.c index 6a8bb0a8..d63934b3 100644 --- a/config.c +++ b/config.c @@ -119,6 +119,7 @@ static const char *const binding_action_map[] = { [BIND_ACTION_PROMPT_PREV] = "prompt-prev", [BIND_ACTION_PROMPT_NEXT] = "prompt-next", [BIND_ACTION_UNICODE_INPUT] = "unicode-input", + [BIND_ACTION_QUIT] = "quit", /* Mouse-specific actions */ [BIND_ACTION_SCROLLBACK_UP_MOUSE] = "scrollback-up-mouse", diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 8cd0560f..a3ddb7c3 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -925,6 +925,9 @@ e.g. *search-start=none*. Default: _Control+Shift+u_. +*quit* + Quit foot. Default: _none_. + # SECTION: search-bindings This section lets you override the default key bindings used in diff --git a/foot.ini b/foot.ini index 9fd6c9db..a2c85e97 100644 --- a/foot.ini +++ b/foot.ini @@ -193,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 --git a/input.c b/input.c index 1a42e5ee..26f62629 100644 --- a/input.c +++ b/input.c @@ -444,6 +444,10 @@ execute_binding(struct seat *seat, struct terminal *term, 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); diff --git a/key-binding.h b/key-binding.h index ba841efa..f42dbc48 100644 --- a/key-binding.h +++ b/key-binding.h @@ -40,6 +40,7 @@ enum bind_action_normal { 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, @@ -53,7 +54,7 @@ enum bind_action_normal { 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, }; From accefc3ae14e84d08b57f5523ec05b896dbb5bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 11 Apr 2024 15:28:08 +0200 Subject: [PATCH 0678/1323] changelog: prepare for 1.17.1 --- CHANGELOG.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96c7c8bf..e85730a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.17.1](#1-17-1) * [1.17.0](#1-17-0) * [1.16.2](#1-16-2) * [1.16.1](#1-16-1) @@ -50,7 +50,8 @@ * [1.2.0](#1-2-0) -## Unreleased +## 1.17.1 + ### Added * `cursor.unfocused-style=unchanged|hollow|none` to `foot.ini`. The @@ -61,9 +62,6 @@ [1475]: https://codeberg.org/dnkl/foot/issues/1475 -### Changed -### Deprecated -### Removed ### Fixed * Log-level not respected by syslog. @@ -79,9 +77,13 @@ [1666]: https://codeberg.org/dnkl/foot/issues/1666 -### Security ### Contributors +* Holger Weiß +* izmyname +* Marcin Puc +* tunjan + ## 1.17.0 From 04ebd028747388ffc7c411f2788384b95e49858f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 11 Apr 2024 15:28:17 +0200 Subject: [PATCH 0679/1323] meson: bump version to 1.17.1 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 3db4616e..b5a54187 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.17.0', + version: '1.17.1', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From 4fd26c251cd66029b8aab739ee2e77a6904b5bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 11 Apr 2024 15:32:15 +0200 Subject: [PATCH 0680/1323] changelog: add a new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e85730a7..175403dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.17.1](#1-17-1) * [1.17.0](#1-17-0) * [1.16.2](#1-16-2) @@ -50,6 +51,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.17.1 ### Added From e753bb953bdf2a30d79d637aafa64cf3fdd2f7d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 12 Apr 2024 15:35:25 +0200 Subject: [PATCH 0681/1323] wayland: remove has_wl_compositor_v6 We deviate slightly from the specification, in that we don't assume a preferred buffer scale of 1. Instead, we "guess" the scale *until we receive a surface_preferred_buffer_scale event. Because of this, we don't need the has_wl_compositor_v6 member, as it's enough to check if we have a non-zero 'preferred buffer scale'. --- terminal.c | 2 +- wayland.c | 1 - wayland.h | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/terminal.c b/terminal.c index 3fb918a4..deb9ba81 100644 --- a/terminal.c +++ b/terminal.c @@ -2181,7 +2181,7 @@ term_fractional_scaling(const struct terminal *term) bool term_preferred_buffer_scale(const struct terminal *term) { - return term->wl->has_wl_compositor_v6 && term->window->preferred_buffer_scale > 0; + return term->window->preferred_buffer_scale > 0; } bool diff --git a/wayland.c b/wayland.c index 1d9d3cdf..4add34e3 100644 --- a/wayland.c +++ b/wayland.c @@ -1096,7 +1096,6 @@ handle_global(void *data, struct wl_registry *registry, #if defined (WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION) const uint32_t preferred = WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION; - wayl->has_wl_compositor_v6 = version >= WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION; #else const uint32_t preferred = required; #endif diff --git a/wayland.h b/wayland.h index 84fcbe48..575af1bb 100644 --- a/wayland.h +++ b/wayland.h @@ -430,8 +430,6 @@ struct wayland { struct wl_subcompositor *sub_compositor; struct wl_shm *shm; - bool has_wl_compositor_v6; - struct zxdg_output_manager_v1 *xdg_output_manager; struct xdg_wm_base *shell; From 23ada09d149140b7a38d376dd4fd7e2c3da589aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 15 Apr 2024 16:02:54 +0200 Subject: [PATCH 0682/1323] osc: reject notifications with invalid UTF-8 strings --- CHANGELOG.md | 4 ++++ osc.c | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 175403dc..0f3a0179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,10 @@ ## Unreleased ### Added ### Changed + +* Notifications with invalid UTF-8 strings are now ignored. + + ### Deprecated ### Removed ### Fixed diff --git a/osc.c b/osc.c index 06cc7db6..446461a2 100644 --- a/osc.c +++ b/osc.c @@ -524,6 +524,19 @@ osc_notify(struct terminal *term, char *string) 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 : ""); } From 71ce17d97795419c3fe5d28132e387f27e2ca958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 15 Apr 2024 16:03:30 +0200 Subject: [PATCH 0683/1323] term: don't print outside grid when printing multi-column characters When auto-wrap is disabled, a multi-column character may be printed on a line that doesn't fit the entire character. That is, the "spacers" we print, as place holders in the columns after the first one, may reach outside the grid. We did (try to) check for this, but the check was off by one. Meaning, we could, in some cases, print outside the grid. --- CHANGELOG.md | 3 +++ terminal.c | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f3a0179..68b15356 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,9 @@ ### Deprecated ### Removed ### Fixed + +* Crash when printing double-width (or longer) characters to, or near, + the last column, when auto-wrap (private mode 7) has been disabled. ### Security ### Contributors diff --git a/terminal.c b/terminal.c index deb9ba81..e747009a 100644 --- a/terminal.c +++ b/terminal.c @@ -3680,11 +3680,13 @@ term_print(struct terminal *term, char32_t wc, int width) 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; 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; @@ -3726,6 +3728,7 @@ ascii_printer_fast(struct terminal *term, char32_t wc) /* Advance cursor */ if (unlikely(++col >= term->cols)) { + xassert(col == term->cols); grid->cursor.lcf = true; col--; } else From 0ab05f480789c0ccd452ed4d7f678d3e4fa36874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 15 Apr 2024 16:05:56 +0200 Subject: [PATCH 0684/1323] sixel: also set 'alloc_height', when short-cutting a resize operation In some cases, a sixel may be resized vertically, while still having a zero-width. In this case, the resize operations are short-cutted, and no actual allocations are done. However, we forgot to set 'alloc_height' when doing so. As a result, the trimming code (when the sixel is "done"), trimmed away the entire sixel. --- CHANGELOG.md | 3 +++ sixel.c | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68b15356..cad31efd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,9 @@ * 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. + + ### Security ### Contributors diff --git a/sixel.c b/sixel.c index a7cbbe18..85be3608 100644 --- a/sixel.c +++ b/sixel.c @@ -1400,7 +1400,7 @@ resize_horizontally(struct terminal *term, int new_width_mutable) new_width_mutable = term->sixel.max_width; } - if (unlikely(term->sixel.image.width == new_width_mutable)) + if (unlikely(term->sixel.image.width >= new_width_mutable)) return; const int sixel_row_height = 6 * term->sixel.pan; @@ -1414,6 +1414,7 @@ resize_horizontally(struct terminal *term, int new_width_mutable) /* 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; @@ -1423,6 +1424,7 @@ resize_horizontally(struct terminal *term, int new_width_mutable) 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); @@ -1474,6 +1476,7 @@ resize_vertically(struct terminal *term, const int new_height) 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; } @@ -1508,7 +1511,7 @@ 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_mutable > term->sixel.max_width)) { LOG_WARN("maximum image width exceeded, truncating"); From 3d2588edf8a90a7ba74d75089193a52b69905a4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 15 Apr 2024 16:07:47 +0200 Subject: [PATCH 0685/1323] sixel: don't allow pan/pad changes after sixel data has been emitted Changing pan/pad changes the sixel's aspect ratio. While I don't know for certain what a real VT340 would do, I suspect it would change the aspect ratio of all subsequent sixels, but not those already emitted. The way we implement sixels in foot, makes this behavior hard to implement. We currently don't resize the image properly if the aspect ratio is changed, but not the RA area. We have code that assumes all sixel lines have the same aspect ratio, etc. Since no "normal" applications change the aspect ratio in the middle of a sixel, simply disallow it, and print a warning. This also fixes a crash, when writing sixels after having modified the aspect ratio. --- CHANGELOG.md | 3 +++ sixel.c | 26 +++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cad31efd..f4276f0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,9 @@ ### Removed ### 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. diff --git a/sixel.c b/sixel.c index 85be3608..d6bfa3a4 100644 --- a/sixel.c +++ b/sixel.c @@ -1852,12 +1852,32 @@ decgra(struct terminal *term, uint8_t c) 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); From 3507c724921c5a886336fa481eca30f1bb824064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 17 Apr 2024 08:52:31 +0200 Subject: [PATCH 0686/1323] ci: explicitly install openssl Let's see if this fixes the missing SSL module in Python... --- .woodpecker.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 063390be..c078584b 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -1,3 +1,5 @@ +# -*- yaml -*- + steps: - name: codespell when: @@ -6,6 +8,7 @@ steps: - releases/* image: alpine:edge commands: + - apk add openssl - apk add python3 - apk add py3-pip - python3 -m venv codespell-venv From a5b369ede4fbb74f4e3ef8352a52974deeeeba86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 17 Apr 2024 08:59:27 +0200 Subject: [PATCH 0687/1323] ci: set explicit 'event' filters an all 'when'-statements This should fix the CI warnings we've started to see lately: [bad_habit] Please set an event filter on all when branches --- .woodpecker.yaml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index c078584b..9b121f2a 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -3,9 +3,9 @@ steps: - name: codespell when: - branch: - - master - - releases/* + - event: [manual, pull_request] + - event: [push, tag] + branch: [master, releases/*] image: alpine:edge commands: - apk add openssl @@ -19,9 +19,9 @@ steps: - name: subprojects when: - branch: - - master - - releases/* + - event: [manual, pull_request] + - event: [push, tag] + branch: [master, releases/*] image: alpine:edge commands: - apk add git @@ -32,9 +32,9 @@ steps: - name: x64 when: - branch: - - master - - releases/* + - event: [manual, pull_request] + - event: [push, tag] + branch: [master, releases/*] depends_on: [subprojects] image: alpine:edge commands: @@ -89,9 +89,9 @@ steps: - name: x86 when: - branch: - - master - - releases/* + - event: [manual, pull_request] + - event: [push, tag] + branch: [master, releases/*] depends_on: [subprojects] image: i386/alpine:edge commands: From 7c20fb247c977fe545b1ef0ce9d8935b0424bf70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 17 Apr 2024 08:35:58 +0200 Subject: [PATCH 0688/1323] term: stash last known DPI, and use after a unmapped/mapped sequence A compositor may unmap, and then remap the window, for example when the window is minimized, or if the user switches workspace. With DPI aware rendering, we *need* to know on which output we're mapped, in order to use the correct DPI. This means the first frame we render, before being mapped, always guesses the DPI. In an unmap/map sequence, guessing the wrong DPI means the window will flicker. Fix by stashing the last used DPI value, and use that instead of guessing. This means the *only* time we _actually_ guess the DPI, is the very first frame, when starting up foot. --- CHANGELOG.md | 3 +++ terminal.c | 24 +++++++++++++++++++++--- terminal.h | 1 + 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4276f0a..b4384f0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,9 @@ * 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. ### Security diff --git a/terminal.c b/terminal.c index e747009a..c027cfac 100644 --- a/terminal.c +++ b/terminal.c @@ -858,9 +858,25 @@ get_font_dpi(const struct terminal *term) 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.; @@ -1182,6 +1198,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, xmalloc(sizeof(term->font_sizes[3][0]) * conf->fonts[3].count), }, .font_dpi = 0., + .font_dpi_before_unmap = -1., .font_subpixel = (conf->colors.alpha == 0xffff /* Can't do subpixel rendering on transparent background */ ? FCFT_SUBPIXEL_DEFAULT : FCFT_SUBPIXEL_NONE), @@ -2250,6 +2267,7 @@ term_font_dpi_changed(struct terminal *term, float old_scale) } term->font_dpi = dpi; + term->font_dpi_before_unmap = dpi; term->font_is_sized_by_dpi = will_scale_using_dpi; if (!need_font_reload) diff --git a/terminal.h b/terminal.h index fa80e693..2a0845ef 100644 --- a/terminal.h +++ b/terminal.h @@ -406,6 +406,7 @@ struct terminal { struct config_font *font_sizes[4]; struct pt_or_px font_line_height; float font_dpi; + float font_dpi_before_unmap; bool font_is_sized_by_dpi; int16_t font_x_ofs; int16_t font_y_ofs; From f2fbef1f820c98f38f512db4a2aadf7acb928e79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 17 Apr 2024 11:26:25 +0200 Subject: [PATCH 0689/1323] changelog: prepare for 1.17.2 --- CHANGELOG.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4384f0c..58176b60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.17.2](#1-17-2) * [1.17.1](#1-17-1) * [1.17.0](#1-17-0) * [1.16.2](#1-16-2) @@ -51,15 +51,13 @@ * [1.2.0](#1-2-0) -## Unreleased -### Added +## 1.17.2 + ### Changed * Notifications with invalid UTF-8 strings are now ignored. -### Deprecated -### Removed ### Fixed * Crash when changing aspect ratio of a sixel, in the middle of the @@ -73,10 +71,6 @@ multi-monitor setup with different monitor DPIs. -### Security -### Contributors - - ## 1.17.1 ### Added From b88f0d672f04a835812125ac97ea55240f70de1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 17 Apr 2024 11:26:45 +0200 Subject: [PATCH 0690/1323] meson: bump version to 1.17.2 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index b5a54187..dd698557 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.17.1', + version: '1.17.2', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From a1ac37e771edf9fe907ee09dbec0e90d447679e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 17 Apr 2024 11:28:22 +0200 Subject: [PATCH 0691/1323] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58176b60..feb48a3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.17.2](#1-17-2) * [1.17.1](#1-17-1) * [1.17.0](#1-17-0) @@ -51,6 +52,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.17.2 ### Changed From edb28479cc1f0b72335f309bf87e2522b6751d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 20 Apr 2024 08:17:02 +0200 Subject: [PATCH 0692/1323] doc: foot: simplify example where we run a non-default command To reduce the risk of people mistakenly believing you have to/should use "sh -c". --- doc/foot.1.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 86975464..f5d1686b 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -20,7 +20,7 @@ will start a new terminal window with your default shell. You can override the default shell by appending a custom command to the foot command line - *foot sh -c "echo hello world && sleep 5"* + *foot htop* # OPTIONS From acbb3cbb703e5c20290b677c515ff5f9e2646014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 20 Apr 2024 08:16:15 +0200 Subject: [PATCH 0693/1323] char32: mbsntoc32() returns a size_t, not a char32_t --- char32.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/char32.c b/char32.c index 97c599d9..827cef8d 100644 --- a/char32.c +++ b/char32.c @@ -176,7 +176,7 @@ done: return chars; err: - return (char32_t)-1; + return (size_t)-1; } UNITTEST From 128c5c3efabc212f74a73ce256f8b35ee3ca49a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 20 Apr 2024 08:18:41 +0200 Subject: [PATCH 0694/1323] term: default to DPI 96, if the monitor's DPI is 0 This can happen in virtualized environments, or when running a nested Wayland session. --- CHANGELOG.md | 6 ++++++ terminal.c | 11 +++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index feb48a3e..2851f7ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,12 @@ ### Deprecated ### Removed ### Fixed + +* Crash when zooming in or out, with `dpi-aware=yes`, and the + monitor's DPI is 0 (this is true for, for example, nested Wayland + sessions, or in virtualized environments). + + ### Security ### Contributors diff --git a/terminal.c b/terminal.c index c027cfac..b684030e 100644 --- a/terminal.c +++ b/terminal.c @@ -878,10 +878,13 @@ get_font_dpi(const struct terminal *term) mon = &tll_front(term->wl->monitors); } - if (term_fractional_scaling(term)) - return mon != NULL ? mon->dpi.physical : 96.; - else - return mon != NULL ? mon->dpi.scaled : 96.; + const float monitor_dpi = mon != NULL + ? term_fractional_scaling(term) + ? mon->dpi.physical + : mon->dpi.scaled + : 96.; + + return monitor_dpi > 0. ? monitor_dpi : 96.; } static enum fcft_subpixel From 4d4ef5eed536c373f93c766f4071e14665a9b00e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 20 Apr 2024 08:19:58 +0200 Subject: [PATCH 0695/1323] dcs: XTGETTCAP: handle empty request If the XTGETTCAP request is empty (no capabilities in it), reply with an empty error reply. Closes #1694 --- CHANGELOG.md | 3 +++ dcs.c | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2851f7ba..7d7cd49b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,9 @@ * Crash when zooming in or out, with `dpi-aware=yes`, and the monitor's DPI is 0 (this is true for, for example, nested Wayland sessions, or in virtualized environments). +* No error response for empty `XTGETTCAP` request ([#1694][1694]). + +[1694]: https://codeberg.org/dnkl/foot/issues/1694 ### Security diff --git a/dcs.c b/dcs.c index 19cce3c2..4dccbdee 100644 --- a/dcs.c +++ b/dcs.c @@ -200,6 +200,12 @@ xtgettcap_unhook(struct terminal *term) const char *const end = (const char *)&term->vt.dcs.data[left]; const char *p = (const char *)term->vt.dcs.data; + if (p == NULL) { + /* Request is empty; send an error reply, without any capabilities */ + term_to_slave(term, "\033P0+r\033\\", 7); + return; + } + while (true) { const char *sep = memchr(p, ';', left); size_t cap_len; From a3debf7741d20826bdc6ab786f4ccf0ef7358619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 27 Apr 2024 09:38:55 +0200 Subject: [PATCH 0696/1323] dcs: xtgettcap: always reply with tigetstr(3) formatted "strings" That is, instead of sometimes replying with a "source" encoded string (where e.g. '\E' are returned just like that, and not as an actual ESC), always unescape all string values. This also includes \n \r \t \b \f \s, \^ \\ \ \:, as well as ^x-styled escapes. Closes #1701 --- CHANGELOG.md | 15 +++++++++++ scripts/generate-builtin-terminfo.py | 38 ++++++++++++++++------------ utils/xtgettcap.c | 28 ++++++++++++++++---- 3 files changed, 60 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d7cd49b..10ec7463 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,21 @@ ## Unreleased ### Added ### Changed + +* All `XTGETTCAP` capabilities are now in the `tigetstr()` format: + + - parameterized string capabilities were previously "source + encoded", meaning e.g. `\E` where not "decoded" into `\x1b`. + - Control characters were also "source encoded", meaning they were + returned as e.g. "^G" instead of `\x07` ([#1701][1701]). + + In other words, if, after this change, `XTGETTCAP` returns a string + that is different compared to `tigetstr()`, then it is likely a bug + in foot's implementation of `XTGETTCAP`. + +[1701]: https://codeberg.org/dnkl/foot/issues/1701 + + ### Deprecated ### Removed ### Fixed diff --git a/scripts/generate-builtin-terminfo.py b/scripts/generate-builtin-terminfo.py index acbf5279..61bd4a3c 100755 --- a/scripts/generate-builtin-terminfo.py +++ b/scripts/generate-builtin-terminfo.py @@ -50,27 +50,33 @@ class IntCapability(Capability): class StringCapability(Capability): def __init__(self, name: str, value: str): - # Expand \E to literal ESC in non-parameterized capabilities - if '%' not in value: - # Ensure e.g. \E7 doesn’t get translated to “\0337”, which - # would be interpreted as octal 337 by the C compiler - value = re.sub(r'\\E([0-7])', r'\\033" "\1', value) + # see terminfo(5) for valid escape sequences - # Replace \E with an actual escape - value = re.sub(r'\\E', r'\\033', value) + # Control characters + def translate_ctrl_chr(m): + ctrl = m.group(1) + if ctrl == '?': + return chr(0x7f) + return chr(ord(ctrl) - ord('@')) + value = re.sub('\^([@A-Z[\\\\\]^_?])', translate_ctrl_chr, value) - # Don’t escape ‘:’ - value = value.replace('\\:', ':') + # Ensure e.g. \E7 (or \e7) doesn’t get translated to “\0337”, + # which would be interpreted as octal 337 by the C compiler + value = re.sub(r'(\\E|\\e)([0-7])', r'\\033" "\2', value) - else: - value = value.replace("\\", "\\\\") - # # Need to double-escape backslashes. These only occur in - # # ‘\E\’ combos. Note that \E itself is updated below - # value = value.replace('\\E\\\\', '\\E\\\\\\\\') + # Replace \E and \e with ESC + value = re.sub(r'\\E|\\e', r'\\033', value) - # # Need to double-escape \E in C string literals - # value = value.replace('\\E', '\\\\E') + # Unescape ,:^ + value = re.sub(r'\\(,|:|\^)', r'\1', value) + # Replace \s with space + value = value.replace('\\s', ' ') + + # Let \\, \n, \r, \t, \b and \f "fall through", to the C string literal + + if re.search(r'\\l', value): + raise NotImplementedError('\\l escape sequence') super().__init__(name, value) diff --git a/utils/xtgettcap.c b/utils/xtgettcap.c index b3ab712a..069a9ecb 100644 --- a/utils/xtgettcap.c +++ b/utils/xtgettcap.c @@ -103,7 +103,7 @@ main(int argc, const char *const *argv) if (isprint(buf[i])) printf("%c", buf[i]); else if (buf[i] == '\033') - printf("\033[1;31m\\E\033[m"); + printf("\033[1;31m<ESC>\033[m"); else printf("%02x", (uint8_t)buf[i]); } @@ -158,12 +158,30 @@ main(int argc, const char *const *argv) printf(" \033[%dm", color); for (size_t i = 0 ; i < len; i++) { - if (isprint(decoded[i])) + if (isprint(decoded[i])) { + /* All printable characters */ printf("%c", decoded[i]); - else if (decoded[i] == '\033') - printf("\033[1;31m\\E\033[22;%dm", color); - else + } + + else if (decoded[i] == '\033') { + /* ESC */ + printf("\033[1;31m<ESC>\033[22;%dm", color); + } + + else if (decoded[i] >= '\x00' && decoded[i] <= '\x5f') { + /* Control characters, e.g. ^G etc */ + printf("\033[1m^%c\033[22m", decoded[i] + '@'); + } + + else if (decoded[i] == '\x7f') { + /* Control character ^? */ + printf("\033[1m^?\033[22m"); + } + + else { + /* Unknown: print hex representation */ printf("\033[1m%02x\033[22m", (uint8_t)decoded[i]); + } } printf("\033[m\r\n"); replies++; From 3c4669061baadac105c274bd0d30aadb27b0b642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 30 Apr 2024 10:50:31 +0200 Subject: [PATCH 0697/1323] scripts: generate-builtin-terminfo: use \xNN for control characters Instead of emitting raw control characters (for e.g. bel, cub1 and kbs), use \xNN C string escapes. --- scripts/generate-builtin-terminfo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/generate-builtin-terminfo.py b/scripts/generate-builtin-terminfo.py index 61bd4a3c..4bb16359 100755 --- a/scripts/generate-builtin-terminfo.py +++ b/scripts/generate-builtin-terminfo.py @@ -56,9 +56,9 @@ class StringCapability(Capability): def translate_ctrl_chr(m): ctrl = m.group(1) if ctrl == '?': - return chr(0x7f) - return chr(ord(ctrl) - ord('@')) - value = re.sub('\^([@A-Z[\\\\\]^_?])', translate_ctrl_chr, value) + return '\\x7f' + return f'\\x{ord(ctrl) - ord('@'):02x}' + value = re.sub(r'\^([@A-Z[\\\\\]^_?])', translate_ctrl_chr, value) # Ensure e.g. \E7 (or \e7) doesn’t get translated to “\0337”, # which would be interpreted as octal 337 by the C compiler From 3a7ea1f44b5cac16c4d67cddb75c02638d59c55c Mon Sep 17 00:00:00 2001 From: Artturin <Artturin@artturin.com> Date: Wed, 1 May 2024 21:18:41 +0300 Subject: [PATCH 0698/1323] scripts: generate-builtin-terminfo: fix syntax error --- scripts/generate-builtin-terminfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/generate-builtin-terminfo.py b/scripts/generate-builtin-terminfo.py index 4bb16359..28b31b57 100755 --- a/scripts/generate-builtin-terminfo.py +++ b/scripts/generate-builtin-terminfo.py @@ -57,7 +57,7 @@ class StringCapability(Capability): ctrl = m.group(1) if ctrl == '?': return '\\x7f' - return f'\\x{ord(ctrl) - ord('@'):02x}' + return f'\\x{ord(ctrl) - ord("@"):02x}' value = re.sub(r'\^([@A-Z[\\\\\]^_?])', translate_ctrl_chr, value) # Ensure e.g. \E7 (or \e7) doesn’t get translated to “\0337”, From bc193c7be5b62ec9a395afba753eaf373192ceb0 Mon Sep 17 00:00:00 2001 From: Mariusz Bialonczyk <manio@skyboo.net> Date: Fri, 17 May 2024 10:49:26 +0200 Subject: [PATCH 0699/1323] themes: add onehalf-dark --- themes/onehalf-dark | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 themes/onehalf-dark diff --git a/themes/onehalf-dark b/themes/onehalf-dark new file mode 100644 index 00000000..c37a7984 --- /dev/null +++ b/themes/onehalf-dark @@ -0,0 +1,39 @@ +# theme: One Half - dark version +# author: Son A. Pham <sp@sonpham.me> +# repo: https://github.com/sonph/onehalf +# +# foot theme is based mainly on values from this specific file: +# https://github.com/sonph/onehalf/blob/master/kitty/onehalf-dark.conf +# + cursor colors from: +# https://github.com/sonph/onehalf/blob/master/iterm/OneHalfDark.itermcolors + +[cursor] +color=dcdfe4 a3b3cc + +[colors] +foreground=dcdfe4 +background=282c34 +regular0=282c34 # black +regular1=e06c75 # red +regular2=98c379 # green +regular3=e5c07b # yellow +regular4=61afef # blue +regular5=c678dd # magenta +regular6=56b6c2 # cyan +regular7=dcdfe4 # white +bright0=5d677a # bright black +bright1=e06c75 # bright red +bright2=98c379 # bright green +bright3=e5c07b # bright yellow +bright4=61afef # bright blue +bright5=c678dd # bright magenta +bright6=56b6c2 # bright cyan +bright7=dcdfe4 # bright white + +# Enable if prefer theme color for URL underline +# urls=0087bd + +# Enable if prefer theme colors instead of inverterd fg/bg for +# highlighting (mouse selection) +# selection-foreground=000000 +# selection-background=fffacd From c4f13809430e7086fb6a0f2c80930a7537c7175d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 20 May 2024 09:03:29 +0200 Subject: [PATCH 0700/1323] config: add cursor.blink-rate option The default is 500ms, which corresponds to the old, hardcoded default. Closes #1707 --- CHANGELOG.md | 7 +++++++ config.c | 10 ++++++++-- config.h | 5 ++++- csi.c | 2 +- doc/foot.ini.5.scd | 7 ++++++- terminal.c | 14 +++++++++----- tests/test-config.c | 3 ++- 7 files changed, 37 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10ec7463..45b58778 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,13 @@ ## Unreleased ### Added + +* `cursor.blink-rate` option, allowing you to configure the rate the + cursor blinks with (when `cursor.blink=yes`) ([#1707][1707]); + +[1707]: https://codeberg.org/dnkl/foot/issues/1707 + + ### Changed * All `XTGETTCAP` capabilities are now in the `tigetstr()` format: diff --git a/config.c b/config.c index d63934b3..b7bc1f09 100644 --- a/config.c +++ b/config.c @@ -1395,7 +1395,10 @@ parse_section_cursor(struct context *ctx) } else if (streq(key, "blink")) - return value_to_bool(ctx, &conf->cursor.blink); + return value_to_bool(ctx, &conf->cursor.blink.enabled); + + else if (streq(key, "blink-rate")) + return value_to_uint32(ctx, 10, &conf->cursor.blink.rate_ms); else if (streq(key, "color")) { if (!value_to_two_colors( @@ -3102,7 +3105,10 @@ config_load(struct config *conf, const char *conf_path, .cursor = { .style = CURSOR_BLOCK, .unfocused_style = CURSOR_UNFOCUSED_HOLLOW, - .blink = false, + .blink = { + .enabled = false, + .rate_ms = 500, + }, .color = { .text = 0, .cursor = 0, diff --git a/config.h b/config.h index df3d45f9..e2d94507 100644 --- a/config.h +++ b/config.h @@ -262,7 +262,10 @@ struct config { struct { enum cursor_style style; enum cursor_unfocused_style unfocused_style; - bool blink; + struct { + bool enabled; + uint32_t rate_ms; + } blink; struct { uint32_t text; uint32_t cursor; diff --git a/csi.c b/csi.c index 04777310..3e044908 100644 --- a/csi.c +++ b/csi.c @@ -1665,7 +1665,7 @@ csi_dispatch(struct terminal *term, uint8_t final) switch (param) { case 0: /* blinking block, but we use it to reset to configured default */ term->cursor_style = term->conf->cursor.style; - term->cursor_blink.deccsusr = term->conf->cursor.blink; + term->cursor_blink.deccsusr = term->conf->cursor.blink.enabled; term_cursor_blink_update(term); break; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index a3ddb7c3..93a3120c 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -511,7 +511,12 @@ applications can change these at runtime. *blink* Boolean. Enables blinking cursor. Note that this can be overridden - by applications. Default: _no_. + by applications. Related option: *blink-rate*. Default: _no_. + +*blink-rate* + The rate at which the cursor blink, when cursor blinking has been + enabled. Expressed in milliseconds between each blink. Default: + _500_. *color* Two space separated RRGGBB values (i.e. plain old 6-digit hex diff --git a/terminal.c b/terminal.c index b684030e..9443467c 100644 --- a/terminal.c +++ b/terminal.c @@ -1229,7 +1229,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .cursor_style = conf->cursor.style, .cursor_blink = { .decset = false, - .deccsusr = conf->cursor.blink, + .deccsusr = conf->cursor.blink.enabled, .state = CURSOR_BLINK_ON, .fd = -1, }, @@ -2046,7 +2046,7 @@ term_reset(struct terminal *term, bool hard) term->alt.saved_cursor = (struct cursor){.point = {0, 0}}; term->cursor_style = term->conf->cursor.style; term->cursor_blink.decset = false; - term->cursor_blink.deccsusr = term->conf->cursor.blink; + term->cursor_blink.deccsusr = term->conf->cursor.blink.enabled; term_cursor_blink_update(term); term->cursor_color.text = term->conf->cursor.color.text; term->cursor_color.cursor = term->conf->cursor.color.cursor; @@ -2746,9 +2746,13 @@ cursor_blink_rearm_timer(struct terminal *term) term->cursor_blink.fd = fd; } - static const struct itimerspec timer = { - .it_value = {.tv_sec = 0, .tv_nsec = 500000000}, - .it_interval = {.tv_sec = 0, .tv_nsec = 500000000}, + const int rate_ms = term->conf->cursor.blink.rate_ms; + const long secs = rate_ms / 1000; + const long nsecs = (rate_ms % 1000) * 1000000; + + const struct itimerspec timer = { + .it_value = {.tv_sec = secs, .tv_nsec = nsecs}, + .it_interval = {.tv_sec = secs, .tv_nsec = nsecs}, }; if (timerfd_settime(term->cursor_blink.fd, 0, &timer, NULL) < 0) { diff --git a/tests/test-config.c b/tests/test-config.c index 3d4d345c..4a0fd755 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -641,7 +641,8 @@ test_section_cursor(void) (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_boolean(&ctx, &parse_section_cursor, "blink", &conf.cursor.blink.enabled); + test_uint32(&ctx, &parse_section_cursor, "blink-rate", &conf.cursor.blink.rate_ms); test_pt_or_px(&ctx, &parse_section_cursor, "beam-thickness", &conf.cursor.beam_thickness); test_pt_or_px(&ctx, &parse_section_cursor, "underline-thickness", From 7b983be3d827cb9dc410082fb2642df965c2657a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 20 May 2024 11:00:02 +0200 Subject: [PATCH 0701/1323] foot.ini: add commented out 'blink-rate' --- foot.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/foot.ini b/foot.ini index a2c85e97..f4f104ae 100644 --- a/foot.ini +++ b/foot.ini @@ -64,6 +64,7 @@ # style=block # color=<inverse foreground/background> # blink=no +# blink-rate=500 # beam-thickness=1.5 # underline-thickness=<font underline thickness> From 26e22b74b18027131ca2c02dee1157e64ee8ba8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 21 May 2024 08:11:39 +0200 Subject: [PATCH 0702/1323] forgejo: issue report templates --- .forgejo/issue_template/config.yml | 5 ++ .forgejo/issue_template/issue_template.yml | 69 ++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 .forgejo/issue_template/config.yml create mode 100644 .forgejo/issue_template/issue_template.yml diff --git a/.forgejo/issue_template/config.yml b/.forgejo/issue_template/config.yml new file mode 100644 index 00000000..d519a3ca --- /dev/null +++ b/.forgejo/issue_template/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: IRC + url: https://web.libera.chat/?channels=#foot + about: Join the IRC channel for foot-related discussion and support diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml new file mode 100644 index 00000000..b29370d3 --- /dev/null +++ b/.forgejo/issue_template/issue_template.yml @@ -0,0 +1,69 @@ +name: Bug Report +description: File a bug report +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Please provide as many details as possible, we must be able to + understand the bug in order to fix it. + + Don't forget to search the issue tracker in case there is + already an open issue for the bug you found. + - type: input + id: version + attributes: + label: Foot Version + description: "The output of `foot --version`" + placeholder: "foot version: 1.17.2-11-gc4f13809 (May 20 2024, branch 'master') +pgo +ime +graphemes -assertions" + validations: + required: true + - type: input + id: compositor + attributes: + label: Compositor Version + description: "The name and version of your compositor" + placeholder: "sway version 1.9" + validation: + required: true + - type: textarea + id: repro + attributes: + label: Description of Bug and Steps to Reproduce + description: | + Exactly what steps can someone else take to see the bug + themselves? What happens? + validations: + required: true + - type: markdown + attributes: + value: | + Please provide as many details as possible, we must be able to + understand the bug in order to fix it. + + Have you tested other compositors? Does the issue happen on + all of them, or only your main compositor? + + Use a debug [build](../../INSTALL.md#debug-build) of foot is + possible, to get a better quality stacktrace in case of a + crash. + + Run foot with logging enabled: + ```sh + foot -d info 2> foot.log + ``` + + In many cases, tracing the Wayland communication is extremely helpful: + ```sh + WAYLAND_DEBUG=1 foot -d info 2> foot.wayland.log + ``` + + Reproduce your problem as quickly as possible, and then exit foot. + - type: textarea + id: logs + attributes: + label: Relevant logs, stacktraces, etc. + - type: markdown + attributes: + value: | + Please attach files instead of pasting the logs, if the logs are large From 14b84dd7c5432293d9495db64ca423178a54183f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 21 May 2024 08:13:11 +0200 Subject: [PATCH 0703/1323] issue-template: try to fix link to INSTALL.md --- .forgejo/issue_template/issue_template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index b29370d3..e5a77347 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -44,7 +44,7 @@ body: Have you tested other compositors? Does the issue happen on all of them, or only your main compositor? - Use a debug [build](../../INSTALL.md#debug-build) of foot is + Use a debug [build](INSTALL.md#debug-build) of foot is possible, to get a better quality stacktrace in case of a crash. From 5b0eb7b42d0c9400152bc444a94c769fe854614a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 21 May 2024 08:14:15 +0200 Subject: [PATCH 0704/1323] issue-template: try to fix link to INSTALL.md, attempt 2 --- .forgejo/issue_template/issue_template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index e5a77347..ee3607ba 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -44,7 +44,7 @@ body: Have you tested other compositors? Does the issue happen on all of them, or only your main compositor? - Use a debug [build](INSTALL.md#debug-build) of foot is + Use a debug [build](https://codeberg.org/dnkl/foot/INSTALL.md#debug-build) of foot is possible, to get a better quality stacktrace in case of a crash. From dffe2e0b7c27061610d5486b29b69b2ed45dceae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 21 May 2024 08:14:58 +0200 Subject: [PATCH 0705/1323] issue-template: try to fix link to INSTALL.md, attempt 3 --- .forgejo/issue_template/issue_template.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index ee3607ba..10aebc31 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -44,9 +44,10 @@ body: Have you tested other compositors? Does the issue happen on all of them, or only your main compositor? - Use a debug [build](https://codeberg.org/dnkl/foot/INSTALL.md#debug-build) of foot is - possible, to get a better quality stacktrace in case of a - crash. + Use a debug + [build](https://codeberg.org/dnkl/foot/src/branch/master/INSTALL.md#debug-build) + of foot is possible, to get a better quality stacktrace in + case of a crash. Run foot with logging enabled: ```sh From ad7e0f7f324a4fe9ee90a00c07881442a232b2b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 21 May 2024 08:18:28 +0200 Subject: [PATCH 0706/1323] issue-template: it's validation*s* --- .forgejo/issue_template/issue_template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index 10aebc31..68dae328 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -24,7 +24,7 @@ body: label: Compositor Version description: "The name and version of your compositor" placeholder: "sway version 1.9" - validation: + validations: required: true - type: textarea id: repro From 7982433c71ae35cd611d573b7638b8534691f89c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 21 May 2024 08:27:24 +0200 Subject: [PATCH 0707/1323] issue-template: try to add another template, for feature requests. --- .forgejo/issue_template/issue_template.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .forgejo/issue_template/issue_template.md diff --git a/.forgejo/issue_template/issue_template.md b/.forgejo/issue_template/issue_template.md new file mode 100644 index 00000000..6543c82a --- /dev/null +++ b/.forgejo/issue_template/issue_template.md @@ -0,0 +1,19 @@ +--- +name: 'Feature request' +about: 'Request a new feature' +title: '[FEAT] ' +labels: + - enhancement +--- + +Please search the man page (`foot.ini(5)` and `foot(1)`); maybe the +feature already exists? + +If the feature does not exist in your installed version of foot, +please check the **latest** version of foot; maybe the feature has +already been added? + +Please describe your feature request in as much details as +possible. Describe your use case. Explain why the existing feature set +is not sufficient. Foot is (trying to be) a minimalistic terminal +emulator; explain how your desired feature does not add bloat. From cf65ad49e81eb7fa90ec647964182791fb993800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 21 May 2024 08:31:08 +0200 Subject: [PATCH 0708/1323] issue-template: feature: use yaml instead --- .forgejo/issue_template/issue_template.md | 19 --------------- .forgejo/issue_template/issue_template.yaml | 26 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 19 deletions(-) delete mode 100644 .forgejo/issue_template/issue_template.md create mode 100644 .forgejo/issue_template/issue_template.yaml diff --git a/.forgejo/issue_template/issue_template.md b/.forgejo/issue_template/issue_template.md deleted file mode 100644 index 6543c82a..00000000 --- a/.forgejo/issue_template/issue_template.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: 'Feature request' -about: 'Request a new feature' -title: '[FEAT] ' -labels: - - enhancement ---- - -Please search the man page (`foot.ini(5)` and `foot(1)`); maybe the -feature already exists? - -If the feature does not exist in your installed version of foot, -please check the **latest** version of foot; maybe the feature has -already been added? - -Please describe your feature request in as much details as -possible. Describe your use case. Explain why the existing feature set -is not sufficient. Foot is (trying to be) a minimalistic terminal -emulator; explain how your desired feature does not add bloat. diff --git a/.forgejo/issue_template/issue_template.yaml b/.forgejo/issue_template/issue_template.yaml new file mode 100644 index 00000000..52dd09d8 --- /dev/null +++ b/.forgejo/issue_template/issue_template.yaml @@ -0,0 +1,26 @@ +name: Feature Request +description: Request a new feature +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Please search the man page (`foot.ini(5)` and `foot(1)`); + maybe the feature already exists? + + If the feature does not exist in your installed version of + foot, please check the **latest** version of foot; maybe the + feature has already been added? + + Please describe your feature request in as much details as + possible. Describe your use case. Explain why the existing + feature set is not sufficient. Foot is (trying to be) a + minimalistic terminal emulator; explain how your desired + feature does not add bloat. + - type: textarea + id: request + attributes: + label: Describe your feature request + validations: + required: true + From 18b702b2495d4fb9328930eed9875731cff4cbde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 21 May 2024 07:06:45 +0200 Subject: [PATCH 0709/1323] unicode-mode: move state from seat to term This fixes an issue where entering unicode-mode in one foot client, also enabled unicode-mode on other foot clients. Both visually (although glitchy), and in effect. The reason the state was originally in the seat objects, was to fully support multi-seat. That is, one seat/keyboard entering unicode-mode should not affect other seats/keyboards. The issue with this is that seat objects are Wayland global. Thus, in server mode, all seat objects are shared between the foot clients. There is a similarity with IME, which also keeps state in the seat. There's one big difference, however, and that is IME has Wayland native enter/leave events, that the compositor emits when windows are focused/unfocused. These events allow us to reset IME state. For our own Unicode mode, there is nothing similar. This patch moves the Unicode state from seats, to the terminal struct. This does mean that if one seat/keyboard enters Unicode mode, then *all* seats/keyboards will affect the unicode state. This potential downside is outweighed by the fact that different foot clients no longer affect each other. Closes #1717 --- CHANGELOG.md | 3 +++ input.c | 4 ++-- render.c | 11 +---------- search.c | 2 +- terminal.h | 6 ++++++ unicode-mode.c | 51 +++++++++++++++++++++++++------------------------- unicode-mode.h | 8 ++++---- wayland.h | 6 ------ 8 files changed, 42 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45b58778..0472c8a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,8 +85,11 @@ monitor's DPI is 0 (this is true for, for example, nested Wayland sessions, or in virtualized environments). * No error response for empty `XTGETTCAP` request ([#1694][1694]). +* Unicode-mode in one foot client affecting other clients, in foot + server mode ([#1717][1717]). [1694]: https://codeberg.org/dnkl/foot/issues/1694 +[1717]: https://codeberg.org/dnkl/foot/issues/1717 ### Security diff --git a/input.c b/input.c index 26f62629..33b5446c 100644 --- a/input.c +++ b/input.c @@ -441,7 +441,7 @@ execute_binding(struct seat *seat, struct terminal *term, } case BIND_ACTION_UNICODE_INPUT: - unicode_mode_activate(seat); + unicode_mode_activate(term); return true; case BIND_ACTION_QUIT: @@ -1550,7 +1550,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, xassert(bindings != NULL); if (pressed) { - if (seat->unicode_mode.active) { + if (term->unicode_mode.active) { unicode_mode_input(seat, term, sym); return; } diff --git a/render.c b/render.c index 804ccdc3..3a577482 100644 --- a/render.c +++ b/render.c @@ -1563,16 +1563,7 @@ static void render_overlay(struct terminal *term) { struct wayl_sub_surface *overlay = &term->window->overlay; - bool unicode_mode_active = false; - - /* Check if unicode mode is active on at least one seat focusing - * this terminal instance */ - tll_foreach(term->wl->seats, it) { - if (it->item.unicode_mode.active) { - unicode_mode_active = true; - break; - } - } + const bool unicode_mode_active = term->unicode_mode.active; const enum overlay_style style = term->is_searching ? OVERLAY_SEARCH : diff --git a/search.c b/search.c index 10541884..eaf8c34e 100644 --- a/search.c +++ b/search.c @@ -1358,7 +1358,7 @@ execute_binding(struct seat *seat, struct terminal *term, return true; case BIND_ACTION_SEARCH_UNICODE_INPUT: - unicode_mode_activate(seat); + unicode_mode_activate(term); return true; case BIND_ACTION_SEARCH_COUNT: diff --git a/terminal.h b/terminal.h index 2a0845ef..ba634185 100644 --- a/terminal.h +++ b/terminal.h @@ -719,6 +719,12 @@ struct terminal { bool ime_enabled; #endif + struct { + bool active; + int count; + char32_t character; + } unicode_mode; + struct { bool in_progress; bool client_has_terminated; diff --git a/unicode-mode.c b/unicode-mode.c index 6290de61..b902b5f4 100644 --- a/unicode-mode.c +++ b/unicode-mode.c @@ -1,37 +1,36 @@ #include "unicode-mode.h" #define LOG_MODULE "unicode-input" -#define LOG_ENABLE_DBG 0 +#define LOG_ENABLE_DBG 1 #include "log.h" #include "render.h" #include "search.h" void -unicode_mode_activate(struct seat *seat) +unicode_mode_activate(struct terminal *term) { - if (seat->unicode_mode.active) + if (term->unicode_mode.active) return; - seat->unicode_mode.active = true; - seat->unicode_mode.character = u'\0'; - seat->unicode_mode.count = 0; - unicode_mode_updated(seat); + term->unicode_mode.active = true; + term->unicode_mode.character = u'\0'; + term->unicode_mode.count = 0; + unicode_mode_updated(term); } void -unicode_mode_deactivate(struct seat *seat) +unicode_mode_deactivate(struct terminal *term) { - if (!seat->unicode_mode.active) + if (!term->unicode_mode.active) return; - seat->unicode_mode.active = false; - unicode_mode_updated(seat); + term->unicode_mode.active = false; + unicode_mode_updated(term); } void -unicode_mode_updated(struct seat *seat) +unicode_mode_updated(struct terminal *term) { - struct terminal *term = seat->kbd_focus; if (term == NULL) return; @@ -52,10 +51,10 @@ unicode_mode_input(struct seat *seat, struct terminal *term, { char utf8[MB_CUR_MAX]; size_t chars = c32rtomb( - utf8, seat->unicode_mode.character, &(mbstate_t){0}); + utf8, term->unicode_mode.character, &(mbstate_t){0}); LOG_DBG("Unicode input: 0x%06x -> %.*s", - seat->unicode_mode.character, (int)chars, utf8); + term->unicode_mode.character, (int)chars, utf8); if (chars != (size_t)-1) { if (term->is_searching) @@ -64,7 +63,7 @@ unicode_mode_input(struct seat *seat, struct terminal *term, term_to_slave(term, utf8, chars); } - unicode_mode_deactivate(seat); + unicode_mode_deactivate(term); } else if (sym == XKB_KEY_Escape || @@ -73,18 +72,18 @@ unicode_mode_input(struct seat *seat, struct terminal *term, sym == XKB_KEY_d || sym == XKB_KEY_g))) { - unicode_mode_deactivate(seat); + unicode_mode_deactivate(term); } else if (sym == XKB_KEY_BackSpace) { - if (seat->unicode_mode.count > 0) { - seat->unicode_mode.character >>= 4; - seat->unicode_mode.count--; - unicode_mode_updated(seat); + if (term->unicode_mode.count > 0) { + term->unicode_mode.character >>= 4; + term->unicode_mode.count--; + unicode_mode_updated(term); } } - else if (seat->unicode_mode.count < 6) { + else if (term->unicode_mode.count < 6) { int digit = -1; /* 0-9, a-f, A-F */ @@ -99,10 +98,10 @@ unicode_mode_input(struct seat *seat, struct terminal *term, if (digit >= 0) { xassert(digit >= 0 && digit <= 0xf); - seat->unicode_mode.character <<= 4; - seat->unicode_mode.character |= digit; - seat->unicode_mode.count++; - unicode_mode_updated(seat); + term->unicode_mode.character <<= 4; + term->unicode_mode.character |= digit; + term->unicode_mode.count++; + unicode_mode_updated(term); } } } diff --git a/unicode-mode.h b/unicode-mode.h index e7c75b9b..2f8d2b35 100644 --- a/unicode-mode.h +++ b/unicode-mode.h @@ -2,10 +2,10 @@ #include <xkbcommon/xkbcommon-keysyms.h> -#include "wayland.h" +#include "terminal.h" -void unicode_mode_activate(struct seat *seat); -void unicode_mode_deactivate(struct seat *seat); -void unicode_mode_updated(struct seat *seat); +void unicode_mode_activate(struct terminal *term); +void unicode_mode_deactivate(struct terminal *term); +void unicode_mode_updated(struct terminal *term); void unicode_mode_input(struct seat *seat, struct terminal *term, xkb_keysym_t sym); diff --git a/wayland.h b/wayland.h index 575af1bb..215640aa 100644 --- a/wayland.h +++ b/wayland.h @@ -243,12 +243,6 @@ struct seat { uint32_t serial; } ime; #endif - - struct { - bool active; - int count; - char32_t character; - } unicode_mode; }; enum csd_surface { From 8716ca578495d93ff6fcfa1001ca86413ecf7ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 21 May 2024 06:18:00 +0200 Subject: [PATCH 0710/1323] url-mode: disable IME mode while URL-mode is active This prevents the IME from stealing "our" key-presses, and thus preventing the user from opening URLs. Closes #1718, hopefully. --- CHANGELOG.md | 2 ++ csi.c | 4 +++- terminal.h | 1 + url-mode.c | 12 ++++++++++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0472c8a8..12fe1963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,9 +87,11 @@ * No error response for empty `XTGETTCAP` request ([#1694][1694]). * Unicode-mode in one foot client affecting other clients, in foot server mode ([#1717][1717]). +* IME interfering in URL-mode ([#1718][1718]). [1694]: https://codeberg.org/dnkl/foot/issues/1694 [1717]: https://codeberg.org/dnkl/foot/issues/1717 +[1718]: https://codeberg.org/dnkl/foot/issues/1718 ### Security diff --git a/csi.c b/csi.c index 3e044908..be470e70 100644 --- a/csi.c +++ b/csi.c @@ -507,8 +507,10 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) case 737769: if (enable) term_ime_enable(term); - else + else { term_ime_disable(term); + term->ime_reenable_after_url_mode = false; + } break; default: diff --git a/terminal.h b/terminal.h index ba634185..5033c550 100644 --- a/terminal.h +++ b/terminal.h @@ -714,6 +714,7 @@ struct terminal { char32_t url_keys[5]; bool urls_show_uri_on_jump_label; struct grid *url_grid_snapshot; + bool ime_reenable_after_url_mode; #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED bool ime_enabled; diff --git a/url-mode.c b/url-mode.c index 0dc594f5..9356a362 100644 --- a/url-mode.c +++ b/url-mode.c @@ -778,6 +778,12 @@ urls_render(struct terminal *term) if (tll_length(win->term->urls) == 0) return; + /* Disable IME while in URL-mode */ + if (term_ime_is_enabled(term)) { + term->ime_reenable_after_url_mode = true; + term_ime_disable(term); + } + /* Dirty the last cursor, to ensure it is erased */ { struct row *cursor_row = term->render.last_cursor.row; @@ -861,5 +867,11 @@ urls_reset(struct terminal *term) term->urls_show_uri_on_jump_label = false; memset(term->url_keys, 0, sizeof(term->url_keys)); + /* Re-enable IME, if it was enabled before we entered URL-mode */ + if (term->ime_reenable_after_url_mode) { + term->ime_reenable_after_url_mode = false; + term_ime_enable(term); + } + render_refresh(term); } From 6944d5f9015db00c94d2bdd6196411ddf2baf6fb Mon Sep 17 00:00:00 2001 From: Craig Barnes <craigbarnes@protonmail.com> Date: Tue, 21 May 2024 17:48:04 +0100 Subject: [PATCH 0711/1323] issue-template: fix typo --- .forgejo/issue_template/issue_template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index 68dae328..e3786ac6 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -46,7 +46,7 @@ body: Use a debug [build](https://codeberg.org/dnkl/foot/src/branch/master/INSTALL.md#debug-build) - of foot is possible, to get a better quality stacktrace in + of foot if possible, to get a better quality stacktrace in case of a crash. Run foot with logging enabled: From 3c96d0b68e3f3d270e885c4d7bb82ae8667ba385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 21 May 2024 16:09:34 +0200 Subject: [PATCH 0712/1323] render: use single-pixel buffers for overlays, when possible The unicode-mode, and flash overlays are single color buffers. This means we can use the single-pixel buffer protocol. It's undefined whether the compositor will release the buffer or not; to make things easier, simply destroy the buffer as soon as we've committed it. Note that since compositors don't necessarily release single-pixel buffers, we can't plug them into our own buffer interface. This means we can't use buffer pointers to check if we can re-use the previous buffer (i.e. we can skip comitting a new buffer), or if we have to create a new one. It's _almost_ enough to just check if the last overlay style is the same as the current one. Except that that doesn't take window resizes into account... --- meson.build | 1 + render.c | 78 +++++++++++++++++++++++++++++++++++++++++++++++------ terminal.c | 4 ++- wayland.c | 15 ++++++++++- wayland.h | 5 +++- 5 files changed, 92 insertions(+), 11 deletions(-) diff --git a/meson.build b/meson.build index dd698557..7c714ddf 100644 --- a/meson.build +++ b/meson.build @@ -159,6 +159,7 @@ wl_proto_xml = [ wayland_protocols_datadir / 'staging/fractional-scale/fractional-scale-v1.xml', wayland_protocols_datadir / 'unstable/tablet/tablet-unstable-v2.xml', # required by cursor-shape-v1 wayland_protocols_datadir / 'staging/cursor-shape/cursor-shape-v1.xml', + wayland_protocols_datadir / 'staging/single-pixel-buffer/single-pixel-buffer-v1.xml', ] foreach prot : wl_proto_xml diff --git a/render.c b/render.c index 3a577482..7d9baa1e 100644 --- a/render.c +++ b/render.c @@ -1559,6 +1559,58 @@ render_ime_preedit(struct terminal *term, struct buffer *buf) #endif } +static void +render_overlay_single_pixel(struct terminal *term, enum overlay_style style, + pixman_color_t color) +{ + struct wayland *wayl = term->wl; + struct wayl_sub_surface *overlay = &term->window->overlay; + + /* + * Note: we currently do *not* re-use the overlay buffer + * + * This means we'll re-create the buffer each time we render a new + * frame. This shouldn't be a problem in any of the cases where we + * use single-pixel buffers (unicode-input, and flash). + * + * Note: it's _almost_ enough to just check if + * style' == last_overlay_style + * except that doesn't take window resizes into account... + */ + + assert(style == OVERLAY_UNICODE_MODE || style == OVERLAY_FLASH); + assert(wayl->single_pixel_manager != NULL); + assert(overlay->surface.viewport != NULL); + + struct wl_buffer *buf = + wp_single_pixel_buffer_manager_v1_create_u32_rgba_buffer( + term->wl->single_pixel_manager, + (double)color.red / 0xffff * 0xffffffff, + (double)color.green / 0xffff * 0xffffffff, + (double)color.blue / 0xffff * 0xffffffff, + (double)color.alpha / 0xffff * 0xffffffff); + + wl_surface_set_buffer_scale(overlay->surface.surf, 1); + wp_viewport_set_destination( + overlay->surface.viewport, + roundf(term->width / term->scale), + roundf(term->height / term->scale)); + + quirk_weston_subsurface_desync_on(overlay->sub); + + wl_subsurface_set_position(overlay->sub, 0, 0); + wl_surface_attach(overlay->surface.surf, buf, 0, 0); + + wl_surface_damage_buffer( + overlay->surface.surf, 0, 0, term->width, term->height); + + wl_surface_commit(overlay->surface.surf); + quirk_weston_subsurface_desync_off(overlay->sub); + + term->render.last_overlay_style = style; + wl_buffer_destroy(buf); +} + static void render_overlay(struct terminal *term) { @@ -1586,17 +1638,9 @@ render_overlay(struct terminal *term) return; } - struct buffer *buf = shm_get_buffer( - term->render.chains.overlay, term->width, term->height, true); - - pixman_image_set_clip_region32(buf->pix[0], NULL); - pixman_color_t color; switch (style) { - case OVERLAY_NONE: - break; - case OVERLAY_SEARCH: case OVERLAY_UNICODE_MODE: color = (pixman_color_t){0, 0, 0, 0x7fff}; @@ -1607,8 +1651,26 @@ render_overlay(struct terminal *term) term->conf->colors.flash, term->conf->colors.flash_alpha); break; + + case OVERLAY_NONE: + xassert(false); + break; } + const bool single_pixel = + (style == OVERLAY_UNICODE_MODE || style == OVERLAY_FLASH) && + term->wl->single_pixel_manager != NULL && + overlay->surface.viewport != NULL; + + if (single_pixel) { + render_overlay_single_pixel(term, style, color); + return; + } + + struct buffer *buf = shm_get_buffer( + term->render.chains.overlay, term->width, term->height, true); + pixman_image_set_clip_region32(buf->pix[0], NULL); + /* Bounding rectangle of damaged areas - for wl_surface_damage_buffer() */ pixman_box32_t damage_bounds; diff --git a/terminal.c b/terminal.c index 9443467c..55c387c1 100644 --- a/terminal.c +++ b/terminal.c @@ -2195,7 +2195,9 @@ term_font_size_reset(struct terminal *term) bool term_fractional_scaling(const struct terminal *term) { - return term->wl->fractional_scale_manager != NULL && term->window->scale > 0.; + return term->wl->fractional_scale_manager != NULL && + term->wl->viewporter != NULL && + term->window->scale > 0.; } bool diff --git a/wayland.c b/wayland.c index 4add34e3..d65a404e 100644 --- a/wayland.c +++ b/wayland.c @@ -1320,6 +1320,16 @@ handle_global(void *data, struct wl_registry *registry, wayl->registry, name, &wp_cursor_shape_manager_v1_interface, required); } + else if (streq(interface, wp_single_pixel_buffer_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->single_pixel_manager = wl_registry_bind( + wayl->registry, name, + &wp_single_pixel_buffer_manager_v1_interface, required); + } + #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED else if (streq(interface, zwp_text_input_manager_v3_interface.name)) { const uint32_t required = 1; @@ -1636,6 +1646,8 @@ wayl_destroy(struct wayland *wayl) zwp_text_input_manager_v3_destroy(wayl->text_input_manager); #endif + if (wayl->single_pixel_manager != NULL) + wp_single_pixel_buffer_manager_v1_destroy(wayl->single_pixel_manager); if (wayl->fractional_scale_manager != NULL) wp_fractional_scale_manager_v1_destroy(wayl->fractional_scale_manager); if (wayl->viewporter != NULL) @@ -2058,6 +2070,7 @@ surface_scale_explicit_width_height( } } + xassert(surf->viewport != NULL); wl_surface_set_buffer_scale(surf->surf, 1); wp_viewport_set_destination( surf->viewport, roundf(width / scale), roundf(height / scale)); @@ -2204,7 +2217,7 @@ wayl_win_subsurface_new_with_custom_parent( } struct wp_viewport *viewport = NULL; - if (wayl->fractional_scale_manager != NULL && wayl->viewporter != NULL) { + if (wayl->viewporter != NULL) { viewport = wp_viewporter_get_viewport(wayl->viewporter, main_surface); if (viewport == NULL) { LOG_ERR("failed to instantiate viewport for sub-surface"); diff --git a/wayland.h b/wayland.h index 215640aa..7688fbdb 100644 --- a/wayland.h +++ b/wayland.h @@ -12,6 +12,7 @@ #include <fractional-scale-v1.h> #include <presentation-time.h> #include <primary-selection-unstable-v1.h> +#include <single-pixel-buffer-v1.h> #include <text-input-unstable-v3.h> #include <viewporter.h> #include <xdg-activation-v1.h> @@ -439,10 +440,12 @@ struct wayland { struct wp_cursor_shape_manager_v1 *cursor_shape_manager; + struct wp_single_pixel_buffer_manager_v1 *single_pixel_manager; + bool presentation_timings; struct wp_presentation *presentation; uint32_t presentation_clock_id; - + #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED struct zwp_text_input_manager_v3 *text_input_manager; #endif From 708ca3d650dc16372eeed5f1c6bed62d01c5c408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 22 May 2024 13:38:13 +0200 Subject: [PATCH 0713/1323] render: single-pixel: minor optimization Don't re-create the single-pixel buffer, unless necessary. The buffer itself doesn't have a size. That means we can re-use the buffer if the last frame's overlay style matches the current frame's style. What we *do not* know is whether the current frame's size is the same as the last frame's. This means we still have to set the viewport destination, and commit the surface. --- render.c | 47 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/render.c b/render.c index 7d9baa1e..fd7e743a 100644 --- a/render.c +++ b/render.c @@ -1565,41 +1565,55 @@ render_overlay_single_pixel(struct terminal *term, enum overlay_style style, { struct wayland *wayl = term->wl; struct wayl_sub_surface *overlay = &term->window->overlay; + struct wl_buffer *buf = NULL; /* - * Note: we currently do *not* re-use the overlay buffer + * In an ideal world, we'd only update the surface (i.e. commit + * any changes) if anything has actually changed. * - * This means we'll re-create the buffer each time we render a new - * frame. This shouldn't be a problem in any of the cases where we - * use single-pixel buffers (unicode-input, and flash). + * For technical reasons, we can't do that, since we can't + * determine whether the last committed buffer is still valid + * (i.e. does it correspond to the current overlay style, *and* + * does last frame's size match the current size?) * - * Note: it's _almost_ enough to just check if - * style' == last_overlay_style - * except that doesn't take window resizes into account... + * What we _can_ do is use the fact that single-pixel buffers + * don't have a size; you have to use a viewport to "size" them. + * + * This means we can check if the last frame's overlay style is + * the same as the current size. If so, then we *know* that the + * currently attached buffer is valid, and we *don't* have to + * create a new single-pixel buffer. + * + * What we do *not* know if the *size* is still valid. This means + * we do have to do the viewport calls, and a surface commit. + * + * This is still better than *always* creating a new buffer. */ assert(style == OVERLAY_UNICODE_MODE || style == OVERLAY_FLASH); assert(wayl->single_pixel_manager != NULL); assert(overlay->surface.viewport != NULL); - struct wl_buffer *buf = - wp_single_pixel_buffer_manager_v1_create_u32_rgba_buffer( - term->wl->single_pixel_manager, + quirk_weston_subsurface_desync_on(overlay->sub); + + if (style != term->render.last_overlay_style) { + buf = wp_single_pixel_buffer_manager_v1_create_u32_rgba_buffer( + wayl->single_pixel_manager, (double)color.red / 0xffff * 0xffffffff, (double)color.green / 0xffff * 0xffffffff, (double)color.blue / 0xffff * 0xffffffff, (double)color.alpha / 0xffff * 0xffffffff); - wl_surface_set_buffer_scale(overlay->surface.surf, 1); + wl_surface_set_buffer_scale(overlay->surface.surf, 1); + wl_surface_attach(overlay->surface.surf, buf, 0, 0); + } + wp_viewport_set_destination( overlay->surface.viewport, roundf(term->width / term->scale), roundf(term->height / term->scale)); - quirk_weston_subsurface_desync_on(overlay->sub); - wl_subsurface_set_position(overlay->sub, 0, 0); - wl_surface_attach(overlay->surface.surf, buf, 0, 0); wl_surface_damage_buffer( overlay->surface.surf, 0, 0, term->width, term->height); @@ -1608,7 +1622,10 @@ render_overlay_single_pixel(struct terminal *term, enum overlay_style style, quirk_weston_subsurface_desync_off(overlay->sub); term->render.last_overlay_style = style; - wl_buffer_destroy(buf); + + if (buf != NULL) { + wl_buffer_destroy(buf); + } } static void From fb2ad83d79937a30c23d12634495bf6013e7b687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 22 May 2024 13:43:40 +0200 Subject: [PATCH 0714/1323] changelog: wp-single-pixel-buffer-v1 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12fe1963..10ed8876 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,9 @@ * `cursor.blink-rate` option, allowing you to configure the rate the cursor blinks with (when `cursor.blink=yes`) ([#1707][1707]); +* Support for `wp_single_pixel_buffer_v1`; certain overlay surfaces + will now utilize the new single-pixel buffer protocol. This mainly + reduces the memory usage, but should also be slightly faster. [1707]: https://codeberg.org/dnkl/foot/issues/1707 From f64cc04fe6822d4efe27d85bebb9d46477be4815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 22 May 2024 14:06:15 +0200 Subject: [PATCH 0715/1323] shm: minor optimization Don't retry memfd_create() without MFD_NOEXEC_SEAL is 0. The overall logic is this: * Try memfd_create() with MFD_NOEXEC_SEAL * If that fails (which it does, on older kernels), try without the flag If compiling against an older kernel, or on a system that doesn't support the noexec seal, MFD_NOEXEC_SEAL is 0. In this case, there's little point in retrying memfd_create a second time, with the exact same set of flags. --- shm.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shm.c b/shm.c index fb868382..5b57ace6 100644 --- a/shm.c +++ b/shm.c @@ -350,7 +350,7 @@ get_new_buffers(struct buffer_chain *chain, size_t count, "foot-wayland-shm-buffer-pool", MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_NOEXEC_SEAL); - if (pool_fd < 0 && errno == EINVAL) { + if (pool_fd < 0 && errno == EINVAL && MFD_NOEXEC_SEAL != 0) { pool_fd = memfd_create( "foot-wayland-shm-buffer-pool", MFD_CLOEXEC | MFD_ALLOW_SEALING); } From 713d8d59fbee2328b045069b66ba4a29768c19cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 22 May 2024 14:43:51 +0200 Subject: [PATCH 0716/1323] issue-template: add input-field for TERM --- .forgejo/issue_template/issue_template.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index e3786ac6..e90642d9 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -18,6 +18,14 @@ body: placeholder: "foot version: 1.17.2-11-gc4f13809 (May 20 2024, branch 'master') +pgo +ime +graphemes -assertions" validations: required: true + - type: input + id: term + attributes: + label: TERM environment variable + description: "The output of `echo $TERM`" + placeholder: "foot" + validations: + required: true - type: input id: compositor attributes: From 585fac7af0044f1d3ca1b8f94febb707346a9d85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 22 May 2024 14:44:16 +0200 Subject: [PATCH 0717/1323] issue-template: ask user to provide info on tmux, zellij, IMEs etc --- .forgejo/issue_template/issue_template.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index e90642d9..31b26a8d 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -52,6 +52,11 @@ body: Have you tested other compositors? Does the issue happen on all of them, or only your main compositor? + Are you using tmux, zellij, or any other terminal multiplexer? + Does the bug happen in a plain foot instance? + + Do you use an IME? Does the bug happen if you disable the IME? + Use a debug [build](https://codeberg.org/dnkl/foot/src/branch/master/INSTALL.md#debug-build) of foot if possible, to get a better quality stacktrace in From 5a4af31d184ac8b2880009698c0e2bde2c3f912b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 22 May 2024 14:47:07 +0200 Subject: [PATCH 0718/1323] issue-template: formatting; hopefully makes it easier to read --- .forgejo/issue_template/issue_template.yml | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index 31b26a8d..60c789fa 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -49,13 +49,21 @@ body: Please provide as many details as possible, we must be able to understand the bug in order to fix it. - Have you tested other compositors? Does the issue happen on - all of them, or only your main compositor? + Other software + -------------- - Are you using tmux, zellij, or any other terminal multiplexer? - Does the bug happen in a plain foot instance? + **Compositors**: have you tested other compositors? Does the + issue happen on all of them, or only your main compositor? - Do you use an IME? Does the bug happen if you disable the IME? + **Terminal multiplexers**: are you using tmux, zellij, or any + other terminal multiplexer? Does the bug happen in a plain + foot instance? + + **IME** do you use an IME? Which one? Does the bug happen if + you disable the IME? + + Obtaining logs and stacktraces + ------------------------------ Use a debug [build](https://codeberg.org/dnkl/foot/src/branch/master/INSTALL.md#debug-build) From 91561d7ba7d0cc189e6f425a587b5e6c495c0e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 22 May 2024 14:47:57 +0200 Subject: [PATCH 0719/1323] issue-template: link "debug build", not just "build" --- .forgejo/issue_template/issue_template.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index 60c789fa..cca40dd5 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -65,8 +65,8 @@ body: Obtaining logs and stacktraces ------------------------------ - Use a debug - [build](https://codeberg.org/dnkl/foot/src/branch/master/INSTALL.md#debug-build) + Use a [debug + build](https://codeberg.org/dnkl/foot/src/branch/master/INSTALL.md#debug-build) of foot if possible, to get a better quality stacktrace in case of a crash. From 0bf5a7e90279e87d0418bbce46e3a9b7cc4e699f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 22 May 2024 14:56:10 +0200 Subject: [PATCH 0720/1323] sixel: comment: document the P1 parameter (and no, it's no longer unimplemented) --- sixel.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sixel.c b/sixel.c index d6bfa3a4..279aef05 100644 --- a/sixel.c +++ b/sixel.c @@ -54,7 +54,13 @@ sixel_put sixel_init(struct terminal *term, int p1, int p2, int p3) { /* - * P1: pixel aspect ratio - unimplemented + * P1: pixel aspect ratio + * - 0,1 - 2:1 + * - 2 - 5:1 + * - 3,4 - 3:1 + * - 5,6 - 2:1 + * - 7,8,9 - 1:1 + * * P2: background color mode * - 0|2: empty pixels use current background color * - 1: empty pixels remain at their current color (i.e. transparent) From bb2e0d64e1f85ed8a75b9d9c9fa1cfe5a078c006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 7 Jun 2024 16:18:28 +0200 Subject: [PATCH 0721/1323] doc: foot.ini: document pixelsize People are apparently too lazy to read fontconfig's documentation, and don't understand how to configure font sizes in foot. --- doc/foot.ini.5.scd | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 93a3120c..b9e1e70e 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -50,15 +50,16 @@ empty string to be set, but it must be quoted: *KEY=""*) *font*, *font-bold*, *font-italic*, *font-bold-italic* Comma separated list of fonts to use, in fontconfig format. That is, a font name followed by a list of colon-separated - options. Most noteworthy is *:size=n*, which is used to set the - font size. Note that the font size is also affected by the - *dpi-aware* option. + options. Most noteworthy is *:size=n* (or *:pixelsize=n*), which + is used to set the font size. Note that the font size is also + affected by the *dpi-aware* option. Examples: - Dina:weight=bold:slant=italic - Courier New:size=12 - Fantasque Sans Mono:fontfeatures=ss01 - Meslo LG S:size=12, Noto Color Emoji:size=12 + - Courier New:pixelsize=8 For each option, the first font is the primary font. The remaining fonts are fallback fonts that will be used whenever a glyph cannot @@ -78,6 +79,11 @@ empty string to be set, but it must be quoted: *KEY=""*) To disable bold and/or italic fonts, set e.g. *font-bold* to _exactly_ the same value as *font*. + **size** is in _points_ (as defined by the FontConfig format). To + set a _pixel_ size, use **pixelsize** instead. Note that pixel + sizes are unaffected by DPI aware rendering (see *dpi-aware*), but + are affected by desktop scaling. + Default: _monospace:size=8_ (*font*), _not set_ (*font-bold*, *font-italic*, *font-bold-italic*). From f67449700b169dfab971773530ca8268448d4fbd Mon Sep 17 00:00:00 2001 From: Jan Beich <jbeich@FreeBSD.org> Date: Sat, 25 May 2024 14:40:25 +0200 Subject: [PATCH 0722/1323] meson: auto-detect execvpe on FreeBSD https://github.com/freebsd/freebsd-src/commit/0667d0e0e365 --- meson.build | 5 +++++ slave.c | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/meson.build b/meson.build index 7c714ddf..cb5d4741 100644 --- a/meson.build +++ b/meson.build @@ -16,6 +16,11 @@ if cc.has_function('memfd_create') add_project_arguments('-DMEMFD_CREATE', language: 'c') endif +# Missing on DragonFly, FreeBSD < 14.1 +if cc.has_function('execvpe') + add_project_arguments('-DEXECVPE', language: 'c') +endif + utmp_backend = get_option('utmp-backend') if utmp_backend == 'auto' host_os = host_machine.system() diff --git a/slave.c b/slave.c index bcd864e1..2187eef3 100644 --- a/slave.c +++ b/slave.c @@ -31,7 +31,7 @@ struct environ { char **envp; }; -#if defined(__FreeBSD__) +#if !defined(EXECVPE) static char * find_file_in_path(const char *file) { @@ -82,11 +82,11 @@ foot_execvpe(const char *file, char *const argv[], char *const envp[]) return ret; } -#else /* !__FreeBSD__ */ +#else /* EXECVPE */ #define foot_execvpe(file, argv, envp) execvpe(file, argv, envp) -#endif /* !__FreeBSD__ */ +#endif /* EXECVPE */ static bool is_valid_shell(const char *shell) From 1b4f97d26396a69f5df9995c1b0db6a766639d0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 8 Jun 2024 08:21:55 +0200 Subject: [PATCH 0723/1323] issue-template: let's see if we can disable the default template --- .forgejo/issue_template/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/issue_template/config.yml b/.forgejo/issue_template/config.yml index d519a3ca..a1d59354 100644 --- a/.forgejo/issue_template/config.yml +++ b/.forgejo/issue_template/config.yml @@ -1,4 +1,4 @@ -blank_issues_enabled: true +blank_issues_enabled: false contact_links: - name: IRC url: https://web.libera.chat/?channels=#foot From ba424e84947540f8bcc20e622a8a5e7947178b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 8 Jun 2024 08:49:33 +0200 Subject: [PATCH 0724/1323] ime: codespell: surroundin -> surrounding --- ime.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ime.c b/ime.c index f3a3ec18..54cfa908 100644 --- a/ime.c +++ b/ime.c @@ -175,7 +175,7 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, } /* - * 2. Delete requested surroundin text + * 2. Delete requested surrounding text * * We don't support deleting surrounding text. But, we also never * call set_surrounding_text() so hopefully we should never From 0755aa7e83a8aa084abb32871ba590d59c43a7b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 8 Jun 2024 08:49:43 +0200 Subject: [PATCH 0725/1323] config: codespell: varios -> various --- config.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.h b/config.h index e2d94507..4ce36486 100644 --- a/config.h +++ b/config.h @@ -84,7 +84,7 @@ enum key_binding_type { typedef tll(char *) config_modifier_list_t; struct config_key_binding { - int action; /* One of the varios bind_action_* enums from wayland.h */ + int action; /* One of the various bind_action_* enums from wayland.h */ //struct config_key_modifiers modifiers; config_modifier_list_t modifiers; union { From 4bb68282beaa3558298a0e6d6bbe46133015ee90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 8 Jun 2024 08:54:12 +0200 Subject: [PATCH 0726/1323] foot.ini: add missing option resize-by-cells Closes #1731 --- foot.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/foot.ini b/foot.ini index f4f104ae..23652d5e 100644 --- a/foot.ini +++ b/foot.ini @@ -26,6 +26,7 @@ # initial-window-size-chars=<COLSxROWS> # initial-window-mode=windowed # pad=0x0 # optionally append 'center' +# resize-by-cells=yes # resize-delay-ms=100 # notify=notify-send -a ${app-id} -i ${app-id} ${title} ${body} From f3d848da019c6cc970b078b0b032d609ee83e083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 8 Jun 2024 08:39:47 +0200 Subject: [PATCH 0727/1323] osc: 52: treat OSC-52 replies as paste data (after all, they are) This fixes an issue where other data (such as replies to other requests) being interleaved with the OSC-52 reply. The patch piggy backs on the already existing mechanism for handling regular pastes, where other data is queued up until the paste is done. There's one corner case that won't work; if the user *just* did a normal paste (i.e. at virtually the same time the application requested OSC-52 data), the OSC-52 request will return an empty reply. Likewise, if there are multiple OSC-52 requests at the same time, only the first will return data. Closes #1734 --- CHANGELOG.md | 3 +++ osc.c | 39 +++++++++++++++++++++++++++++++-------- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10ed8876..9cb9b38c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,10 +91,13 @@ * Unicode-mode in one foot client affecting other clients, in foot server mode ([#1717][1717]). * IME interfering in URL-mode ([#1718][1718]). +* OSC-52 reply interleaved with other data sent to the client + ([#1734][1734]). [1694]: https://codeberg.org/dnkl/foot/issues/1694 [1717]: https://codeberg.org/dnkl/foot/issues/1717 [1718]: https://codeberg.org/dnkl/foot/issues/1718 +[1734]: https://codeberg.org/dnkl/foot/issues/1734 ### Security diff --git a/osc.c b/osc.c index 446461a2..546421f5 100644 --- a/osc.c +++ b/osc.c @@ -5,6 +5,8 @@ #include <ctype.h> #include <errno.h> +#include <sys/epoll.h> + #define LOG_MODULE "osc" #define LOG_ENABLE_DBG 0 #include "log.h" @@ -124,7 +126,7 @@ from_clipboard_cb(char *text, size_t size, void *user) xassert(chunk != NULL); xassert(strlen(chunk) == 4); - term_to_slave(term, chunk, 4); + term_paste_data_to_slave(term, chunk, 4); free(chunk); ctx->idx = 0; @@ -144,7 +146,7 @@ from_clipboard_cb(char *text, size_t size, void *user) char *chunk = base64_encode((const uint8_t *)t, left / 3 * 3); xassert(chunk != NULL); xassert(strlen(chunk) % 4 == 0); - term_to_slave(term, chunk, strlen(chunk)); + term_paste_data_to_slave(term, chunk, strlen(chunk)); free(chunk); } @@ -157,13 +159,19 @@ from_clipboard_done(void *user) if (ctx->idx > 0) { char res[4]; base64_encode_final(ctx->buf, ctx->idx, res); - term_to_slave(term, res, 4); + term_paste_data_to_slave(term, res, 4); } if (term->vt.osc.bel) - term_to_slave(term, "\a", 1); + term_paste_data_to_slave(term, "\a", 1); else - term_to_slave(term, "\033\\", 2); + term_paste_data_to_slave(term, "\033\\", 2); + + term->is_sending_paste_data = false; + + /* Make sure we send any queued up non-paste data */ + if (tll_length(term->ptmx_buffers) > 0) + fdm_event_add(term->fdm, term->ptmx, EPOLLOUT); free(ctx); } @@ -214,9 +222,24 @@ osc_from_clipboard(struct terminal *term, const char *source) if (!from_clipboard && !from_primary) return; - term_to_slave(term, "\033]52;", 5); - term_to_slave(term, &src, 1); - term_to_slave(term, ";", 1); + if (term->is_sending_paste_data) { + /* FIXME: we should wait for the paste to end, then continue + with the OSC-52 reply */ + term_to_slave(term, "\033]52;", 5); + term_to_slave(term, &src, 1); + term_to_slave(term, ";", 1); + if (term->vt.osc.bel) + term_to_slave(term, "\a", 1); + else + term_to_slave(term, "\033\\", 2); + return; + } + + term->is_sending_paste_data = true; + + term_paste_data_to_slave(term, "\033]52;", 5); + term_paste_data_to_slave(term, &src, 1); + term_paste_data_to_slave(term, ";", 1); struct clip_context *ctx = xmalloc(sizeof(*ctx)); *ctx = (struct clip_context) {.seat = seat, .term = term}; From aea16ba5d2896ef22bf0bea45e5e8142c0ff1c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 15 Jun 2024 10:17:01 +0200 Subject: [PATCH 0728/1323] input: implement wl_pointer::axis_value120() This implements high resolution mouse wheel scroll events. A "normal" scroll step corresponds to the value 120. Anything less than that is a partial scroll step. This event replaces axis_discrete(), when we bind wl_seat v8 (which we now do, when available). We calculate the number of degrees that is required to scroll a single line, based off of the scrollback.multiplier value. Each high-res event accumulates, until we have at least the number of degress required to scroll one, or more lines. The remaining degrees are kept, and added to in the next scroll event. Closes #1738 --- CHANGELOG.md | 2 ++ input.c | 71 +++++++++++++++++++++++++++++++++++++++++++--------- wayland.c | 8 +++++- wayland.h | 1 + 4 files changed, 69 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cb9b38c..85b8c59d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,8 +60,10 @@ * Support for `wp_single_pixel_buffer_v1`; certain overlay surfaces will now utilize the new single-pixel buffer protocol. This mainly reduces the memory usage, but should also be slightly faster. +* Support for high-res mouse wheel scroll events ([#1738][1738]). [1707]: https://codeberg.org/dnkl/foot/issues/1707 +[1738]: https://codeberg.org/dnkl/foot/issues/1738 ### Changed diff --git a/input.c b/input.c index 33b5446c..13999250 100644 --- a/input.c +++ b/input.c @@ -2768,7 +2768,7 @@ mouse_scroll(struct seat *seat, int amount, enum wl_pointer_axis axis) } } -static float +static double mouse_scroll_multiplier(const struct terminal *term, const struct seat *seat) { return (term->grid == &term->normal || @@ -2812,15 +2812,15 @@ wl_pointer_axis(void *data, struct wl_pointer *wl_pointer, static void wl_pointer_axis_discrete(void *data, struct wl_pointer *wl_pointer, - uint32_t axis, int32_t discrete) + enum wl_pointer_axis axis, int32_t discrete) { + LOG_DBG("axis_discrete: %d", discrete); struct seat *seat = data; if (touch_is_active(seat)) return; seat->mouse.have_discrete = true; - int amount = discrete; if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL) { @@ -2831,6 +2831,50 @@ wl_pointer_axis_discrete(void *data, struct wl_pointer *wl_pointer, mouse_scroll(seat, amount, axis); } +#if defined(WL_POINTER_AXIS_VALUE120_SINCE_VERSION) +static void +wl_pointer_axis_value120(void *data, struct wl_pointer *wl_pointer, + enum wl_pointer_axis axis, int32_t value120) +{ + LOG_DBG("axis_value120: %d -> %.2f", value120, (float)value120 / 120.); + + struct seat *seat = data; + + if (touch_is_active(seat)) + return; + + seat->mouse.have_discrete = true; + + /* + * 120 corresponds to a single "low-res" scroll step. + * + * When doing high-res scrolling, take the scrollback.multiplier, + * and calculate how many degrees there are per line. + * + * For example, with scrollback.multiplier = 3, we have 120 / 3 == 40. + * + * Then, accumulate high-res scroll events, until we have *at + * least* that much. Translate the accumulated value to number of + * lines, and scroll. + * + * Subtract the "used" degrees from the accumulated value, and + * keep what's left (this value will always be less than the + * per-line value). + */ + const double multiplier = mouse_scroll_multiplier(seat->mouse_focus, seat); + const double per_line = 120. / multiplier; + + seat->mouse.aggregated_120[axis] += (double)value120; + + if (fabs(seat->mouse.aggregated_120[axis]) < per_line) + return; + + int lines = (int)(seat->mouse.aggregated_120[axis] / per_line); + mouse_scroll(seat, lines, axis); + seat->mouse.aggregated_120[axis] -= (double)lines * per_line; +} +#endif + static void wl_pointer_frame(void *data, struct wl_pointer *wl_pointer) { @@ -2862,15 +2906,18 @@ wl_pointer_axis_stop(void *data, struct wl_pointer *wl_pointer, } const struct wl_pointer_listener pointer_listener = { - .enter = wl_pointer_enter, - .leave = wl_pointer_leave, - .motion = wl_pointer_motion, - .button = wl_pointer_button, - .axis = wl_pointer_axis, - .frame = wl_pointer_frame, - .axis_source = wl_pointer_axis_source, - .axis_stop = wl_pointer_axis_stop, - .axis_discrete = wl_pointer_axis_discrete, + .enter = &wl_pointer_enter, + .leave = &wl_pointer_leave, + .motion = &wl_pointer_motion, + .button = &wl_pointer_button, + .axis = &wl_pointer_axis, + .frame = &wl_pointer_frame, + .axis_source = &wl_pointer_axis_source, + .axis_stop = &wl_pointer_axis_stop, + .axis_discrete = &wl_pointer_axis_discrete, +#if defined(WL_POINTER_AXIS_VALUE120_SINCE_VERSION) + .axis_value120 = &wl_pointer_axis_value120, +#endif }; static bool diff --git a/wayland.c b/wayland.c index d65a404e..c357f382 100644 --- a/wayland.c +++ b/wayland.c @@ -1161,6 +1161,12 @@ handle_global(void *data, struct wl_registry *registry, if (!verify_iface_version(interface, version, required)) return; +#if defined(WL_POINTER_AXIS_VALUE120_SINCE_VERSION) + const uint32_t preferred = WL_POINTER_AXIS_VALUE120_SINCE_VERSION; +#else + const uint32_t preferred = required; +#endif + int repeat_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); if (repeat_fd == -1) { LOG_ERRNO("failed to create keyboard repeat timer FD"); @@ -1168,7 +1174,7 @@ handle_global(void *data, struct wl_registry *registry, } struct wl_seat *wl_seat = wl_registry_bind( - wayl->registry, name, &wl_seat_interface, required); + wayl->registry, name, &wl_seat_interface, min(version, preferred)); tll_push_back(wayl->seats, ((struct seat){ .wayl = wayl, diff --git a/wayland.h b/wayland.h index 7688fbdb..9577f08f 100644 --- a/wayland.h +++ b/wayland.h @@ -192,6 +192,7 @@ struct seat { /* We used a discrete axis event in the current pointer frame */ double aggregated[2]; + double aggregated_120[2]; bool have_discrete; } mouse; From c45231ef89faeee0674e405e94ef6a92eb6726a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 22 Jun 2024 07:58:43 +0200 Subject: [PATCH 0729/1323] input: don't reset the XKB compose state in keymap() When the compositor sends a new keymap, don't reset the XKB compose state. This is done by initializing the XKB context, along with the compose state, when binding the seat, instead of in keymap(). Then, in keymap(), simply stop destroying the old xkb state. Only destroy, and re-create the keymap state. Closes #1744 --- CHANGELOG.md | 3 +++ input.c | 24 ------------------------ wayland.c | 22 +++++++++++++++++++--- 3 files changed, 22 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85b8c59d..2f3367c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,11 +95,14 @@ * IME interfering in URL-mode ([#1718][1718]). * OSC-52 reply interleaved with other data sent to the client ([#1734][1734]). +* XKB compose state being reset when foot receives a new keymap + ([#1744][1744]). [1694]: https://codeberg.org/dnkl/foot/issues/1694 [1717]: https://codeberg.org/dnkl/foot/issues/1717 [1718]: https://codeberg.org/dnkl/foot/issues/1718 [1734]: https://codeberg.org/dnkl/foot/issues/1734 +[1744]: https://codeberg.org/dnkl/foot/issues/1744 ### Security diff --git a/input.c b/input.c index 13999250..5c7de859 100644 --- a/input.c +++ b/input.c @@ -513,14 +513,6 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, * Free old keymap state */ - if (seat->kbd.xkb_compose_state != NULL) { - xkb_compose_state_unref(seat->kbd.xkb_compose_state); - seat->kbd.xkb_compose_state = NULL; - } - if (seat->kbd.xkb_compose_table != NULL) { - xkb_compose_table_unref(seat->kbd.xkb_compose_table); - seat->kbd.xkb_compose_table = NULL; - } if (seat->kbd.xkb_keymap != NULL) { xkb_keymap_unref(seat->kbd.xkb_keymap); seat->kbd.xkb_keymap = NULL; @@ -529,10 +521,6 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, xkb_state_unref(seat->kbd.xkb_state); seat->kbd.xkb_state = NULL; } - if (seat->kbd.xkb != NULL) { - xkb_context_unref(seat->kbd.xkb); - seat->kbd.xkb = NULL; - } key_binding_unload_keymap(wayl->key_binding_manager, seat); @@ -559,23 +547,11 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, while (map_str[size - 1] == '\0') size--; - seat->kbd.xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS); - if (seat->kbd.xkb != NULL) { seat->kbd.xkb_keymap = xkb_keymap_new_from_buffer( seat->kbd.xkb, map_str, size, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); - /* Compose (dead keys) */ - seat->kbd.xkb_compose_table = xkb_compose_table_new_from_locale( - seat->kbd.xkb, setlocale(LC_CTYPE, NULL), XKB_COMPOSE_COMPILE_NO_FLAGS); - - if (seat->kbd.xkb_compose_table == NULL) { - LOG_WARN("failed to instantiate compose table; dead keys will not work"); - } else { - seat->kbd.xkb_compose_state = xkb_compose_state_new( - seat->kbd.xkb_compose_table, XKB_COMPOSE_STATE_NO_FLAGS); - } } if (seat->kbd.xkb_keymap != NULL) { diff --git a/wayland.c b/wayland.c index c357f382..29ffab60 100644 --- a/wayland.c +++ b/wayland.c @@ -1,11 +1,12 @@ #include "wayland.h" +#include <errno.h> +#include <fcntl.h> +#include <locale.h> +#include <poll.h> #include <stdlib.h> #include <string.h> #include <unistd.h> -#include <errno.h> -#include <poll.h> -#include <fcntl.h> #include <sys/timerfd.h> #include <sys/epoll.h> @@ -13,6 +14,8 @@ #include <cursor-shape-v1.h> #include <wayland-client.h> #include <wayland-cursor.h> +#include <xkbcommon/xkbcommon.h> +#include <xkbcommon/xkbcommon-keysyms.h> #include <xkbcommon/xkbcommon-compose.h> #include <tllist.h> @@ -1195,6 +1198,19 @@ handle_global(void *data, struct wl_registry *registry, return; } + seat->kbd.xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + if (seat->kbd.xkb != NULL) { + seat->kbd.xkb_compose_table = xkb_compose_table_new_from_locale( + seat->kbd.xkb, setlocale(LC_CTYPE, NULL), XKB_COMPOSE_COMPILE_NO_FLAGS); + + if (seat->kbd.xkb_compose_table != NULL) { + seat->kbd.xkb_compose_state = xkb_compose_state_new( + seat->kbd.xkb_compose_table, XKB_COMPOSE_STATE_NO_FLAGS); + } else { + LOG_WARN("failed to instantiate compose table; dead keys (compose) will not work"); + } + } + seat_add_data_device(seat); seat_add_primary_selection(seat); seat_add_text_input(seat); From 795e39de1a187f6a09aad02410f69f19781bd3c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 24 Jun 2024 17:55:07 +0200 Subject: [PATCH 0730/1323] shm: discard shm buffers with mis-matching alpha-setting --- CHANGELOG.md | 2 ++ shm.c | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f3367c5..06225366 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,6 +97,8 @@ ([#1734][1734]). * XKB compose state being reset when foot receives a new keymap ([#1744][1744]). +* Regression: alpha changes through OSC-11 sequences not taking effect + until window is resized. [1694]: https://codeberg.org/dnkl/foot/issues/1694 [1717]: https://codeberg.org/dnkl/foot/issues/1717 diff --git a/shm.c b/shm.c index 5b57ace6..879745d4 100644 --- a/shm.c +++ b/shm.c @@ -564,7 +564,9 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height, bool with_alph tll_foreach(chain->bufs, it) { struct buffer_private *buf = it->item; - if (buf->public.width != width || buf->public.height != height) { + if (buf->public.width != width || buf->public.height != height || + with_alpha != buf->with_alpha) + { LOG_DBG("purging mismatching buffer %p", (void *)buf); if (buffer_unref_no_remove_from_chain(buf)) tll_remove(chain->bufs, it); From 911af53c5c55ca30ba524e32be8deb93769806e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 25 Jun 2024 08:12:40 +0200 Subject: [PATCH 0731/1323] doc: foot.ini: document issues with fractional scaling and initial-window-size-* Since most compositors don't report the correct scaling factor until *after* the window has been mapped, these options often do not work. --- doc/foot.ini.5.scd | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index b9e1e70e..be357c7a 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -277,7 +277,14 @@ empty string to be set, but it must be quoted: *KEY=""*) Initial window width and height in _pixels_ (subject to output scaling), in the form _WIDTHxHEIGHT_. The height _includes_ the titlebar when using CSDs. Mutually exclusive to - *initial-window-size-chars*. Default: _700x500_. + *initial-window-size-chars*. + + Note that this option may not work as expected if fractional + scaling is being used, due to the fact that many compositors do + not report the correct scaling factor until after a window has + been mapped. + + Default: _700x500_. *initial-window-size-chars* Initial window width and height in _characters_, in the form @@ -289,6 +296,10 @@ empty string to be set, but it must be quoted: *KEY=""*) be set correctly. If that is the case, use *initial-window-size-pixels* instead. + And, just like *initial-window-size-pixels*, this option may not + work as expected if fractional scaling is being used (see + *initial-window-size-pixels for details). + Default: _not set_. *initial-window-mode* From 519e9b8b5e4a521f16d35c94331b0ee59238a58e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 25 Jun 2024 08:20:03 +0200 Subject: [PATCH 0732/1323] doc: foot.ini: fix typos --- doc/foot.ini.5.scd | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index be357c7a..58ea9c15 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -283,7 +283,7 @@ empty string to be set, but it must be quoted: *KEY=""*) scaling is being used, due to the fact that many compositors do not report the correct scaling factor until after a window has been mapped. - + Default: _700x500_. *initial-window-size-chars* @@ -298,8 +298,8 @@ empty string to be set, but it must be quoted: *KEY=""*) And, just like *initial-window-size-pixels*, this option may not work as expected if fractional scaling is being used (see - *initial-window-size-pixels for details). - + *initial-window-size-pixels* for details). + Default: _not set_. *initial-window-mode* From 94583703e12fc46a1ca16718a18579f8db2be0f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 24 Jun 2024 20:21:46 +0200 Subject: [PATCH 0733/1323] vt: don't ignore VS-15 (text presentation) When we encounter either VS-15 or VS-16, set the grapheme width to 1 or 2 explicitly. --- CHANGELOG.md | 2 ++ vt.c | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06225366..4615b3e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,12 +99,14 @@ ([#1744][1744]). * Regression: alpha changes through OSC-11 sequences not taking effect until window is resized. +* VS15 being ignored ([#1742][1742]). [1694]: https://codeberg.org/dnkl/foot/issues/1694 [1717]: https://codeberg.org/dnkl/foot/issues/1717 [1718]: https://codeberg.org/dnkl/foot/issues/1718 [1734]: https://codeberg.org/dnkl/foot/issues/1734 [1744]: https://codeberg.org/dnkl/foot/issues/1744 +[1742]: https://codeberg.org/dnkl/foot/issues/1742 ### Security diff --git a/vt.c b/vt.c index a8a0f0fb..41bb305d 100644 --- a/vt.c +++ b/vt.c @@ -852,9 +852,11 @@ action_utf8_print(struct terminal *term, char32_t wc) break; case GRAPHEME_WIDTH_DOUBLE: + new_cc->width = min(grapheme_width + width, 2); + #if defined(FOOT_GRAPHEME_CLUSTERING) if (unlikely(grapheme_clustering && - wc == 0xfe0f && + (wc == 0xfe0e || wc == 0xfe0f) && new_cc->count == 2)) { /* Only emojis should be affected by VS16 */ @@ -862,11 +864,10 @@ action_utf8_print(struct terminal *term, char32_t wc) utf8proc_get_property(new_cc->chars[0]); if (props->boundclass == UTF8PROC_BOUNDCLASS_EXTENDED_PICTOGRAPHIC) - width = 2; + new_cc->width = wc - 0xfe0d; /* 1 for VS-15, 2 for VS-16 */ } #endif - new_cc->width = min(grapheme_width + width, 2); break; case GRAPHEME_WIDTH_WCSWIDTH: From 96656614454f96034d9cc0f0e14c76493b5b7c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 24 Jun 2024 21:18:37 +0200 Subject: [PATCH 0734/1323] vt: only apply VS-15/16 to valid sequences At compile time, build a lookup table from the Unicode data file 'emoji-variation-sequences.txt'. At run-time, when we detect a VS-15/16 sequence, do a lookup in this table, and enforce the variation selector iff the sequence is valid. Closes #1742 --- CHANGELOG.md | 2 + meson.build | 11 +- scripts/generate-emoji-variation-sequences.py | 95 +++ unicode/emoji-variation-sequences.txt | 757 ++++++++++++++++++ vt.c | 44 +- 5 files changed, 903 insertions(+), 6 deletions(-) create mode 100644 scripts/generate-emoji-variation-sequences.py create mode 100644 unicode/emoji-variation-sequences.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 4615b3e2..901f5a6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,8 @@ * Regression: alpha changes through OSC-11 sequences not taking effect until window is resized. * VS15 being ignored ([#1742][1742]). +* VS16 being ignored for a subset of the valid VS16 sequences + ([#1742][1742]). [1694]: https://codeberg.org/dnkl/foot/issues/1694 [1717]: https://codeberg.org/dnkl/foot/issues/1717 diff --git a/meson.build b/meson.build index cb5d4741..b2e2929b 100644 --- a/meson.build +++ b/meson.build @@ -199,6 +199,14 @@ builtin_terminfo = custom_target( '@default_terminfo@', foot_terminfo, 'foot', '@OUTPUT@'] ) +generate_emoji_variation_sequences = files('scripts/generate-emoji-variation-sequences.py') +emoji_variation_sequences = custom_target( + 'generate_emoji_variation_sequences', + input: 'unicode/emoji-variation-sequences.txt', + output: 'emoji-variation-sequences.h', + command: [python, generate_emoji_variation_sequences, '@INPUT@', '@OUTPUT@'] +) + common = static_library( 'common', 'log.c', 'log.h', @@ -227,7 +235,8 @@ vtlib = static_library( 'osc.c', 'osc.h', 'sixel.c', 'sixel.h', 'vt.c', 'vt.h', - builtin_terminfo, wl_proto_src + wl_proto_headers, + builtin_terminfo, emoji_variation_sequences, + wl_proto_src + wl_proto_headers, version, dependencies: [libepoll, pixman, fcft, tllist, wayland_client, xkb, utf8proc], link_with: [common, misc], diff --git a/scripts/generate-emoji-variation-sequences.py b/scripts/generate-emoji-variation-sequences.py new file mode 100644 index 00000000..1a4069ac --- /dev/null +++ b/scripts/generate-emoji-variation-sequences.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 + +import argparse +import sys + + +class Codepoint: + def __init__(self, start: int, end: None|int = None): + self.start = start + self.end = start if end is None else end + self.vs15 = False + self.vs16 = False + + def __repr__(self) -> str: + return f'{self.start:x}-{self.end:x}, vs15={self.vs15}, vs16={self.vs16}' + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('input', type=argparse.FileType('r')) + parser.add_argument('output', type=argparse.FileType('w')) + opts = parser.parse_args() + + codepoints: list[Codepoint] = [] + + for line in opts.input: + line = line.rstrip() + if not line: + continue + if line[0] == '#': + continue + + cp, vs, _ = line.split(' ', maxsplit=2) + cp = int(cp, 16) + vs = int(vs, 16) + + assert vs == 0xfe0e or vs == 0xfe0f + + if len(codepoints) == 0 or codepoints[-1].start != cp: + codepoints.append(Codepoint(cp)) + else: + assert codepoints[-1].start == cp + + if vs == 0xfe0e: + codepoints[-1].vs15 = True + else: + codepoints[-1].vs16 = True + + + compacted_codepoints: list[Codepoint] = [] + for i, cp in enumerate(codepoints): + assert cp.end == cp.start + + if i == 0: + compacted_codepoints.append(cp) + continue + + last_cp = compacted_codepoints[-1] + if last_cp.end == cp.start - 1 and last_cp.vs15 == cp.vs15 and last_cp.vs16 == cp.vs16: + compacted_codepoints[-1].end = cp.start + else: + compacted_codepoints.append(cp) + + opts.output.write('#pragma once\n') + opts.output.write('#include <stdint.h>\n') + opts.output.write('#include <stdbool.h>\n') + opts.output.write('\n') + opts.output.write('struct emoji_vs {\n') + opts.output.write(' uint32_t start;\n') + opts.output.write(' uint32_t end;\n') + opts.output.write(' bool vs15:1;\n') + opts.output.write(' bool vs16:1;\n') + opts.output.write('} __attribute__((packed));\n') + opts.output.write('_Static_assert(sizeof(struct emoji_vs) == 9, "unexpected struct size");\n') + opts.output.write('\n') + opts.output.write('#if defined(FOOT_GRAPHEME_CLUSTERING)\n') + opts.output.write('\n') + + opts.output.write(f'static const struct emoji_vs emoji_vs[{len(compacted_codepoints)}] = {{\n') + + for cp in compacted_codepoints: + opts.output.write(' {\n') + opts.output.write(f' .start = 0x{cp.start:X},\n') + opts.output.write(f' .end = 0x{cp.end:x},\n') + opts.output.write(f' .vs15 = {"true" if cp.vs15 else "false"},\n') + opts.output.write(f' .vs16 = {"true" if cp.vs16 else "false"},\n') + opts.output.write(' },\n') + + opts.output.write('};\n') + opts.output.write('\n') + opts.output.write('#endif /* FOOT_GRAPHEME_CLUSTERING */\n') + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/unicode/emoji-variation-sequences.txt b/unicode/emoji-variation-sequences.txt new file mode 100644 index 00000000..d8a3c9f4 --- /dev/null +++ b/unicode/emoji-variation-sequences.txt @@ -0,0 +1,757 @@ +# emoji-variation-sequences.txt +# Date: 2023-02-01, 02:22:54 GMT +# © 2023 Unicode®, Inc. +# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. +# For terms of use, see https://www.unicode.org/terms_of_use.html +# +# Emoji Variation Sequences for UTS #51 +# Used with Emoji Version 15.1 and subsequent minor revisions (if any) +# +# For documentation and usage, see https://www.unicode.org/reports/tr51 +# +0023 FE0E ; text style; # (1.1) NUMBER SIGN +0023 FE0F ; emoji style; # (1.1) NUMBER SIGN +002A FE0E ; text style; # (1.1) ASTERISK +002A FE0F ; emoji style; # (1.1) ASTERISK +0030 FE0E ; text style; # (1.1) DIGIT ZERO +0030 FE0F ; emoji style; # (1.1) DIGIT ZERO +0031 FE0E ; text style; # (1.1) DIGIT ONE +0031 FE0F ; emoji style; # (1.1) DIGIT ONE +0032 FE0E ; text style; # (1.1) DIGIT TWO +0032 FE0F ; emoji style; # (1.1) DIGIT TWO +0033 FE0E ; text style; # (1.1) DIGIT THREE +0033 FE0F ; emoji style; # (1.1) DIGIT THREE +0034 FE0E ; text style; # (1.1) DIGIT FOUR +0034 FE0F ; emoji style; # (1.1) DIGIT FOUR +0035 FE0E ; text style; # (1.1) DIGIT FIVE +0035 FE0F ; emoji style; # (1.1) DIGIT FIVE +0036 FE0E ; text style; # (1.1) DIGIT SIX +0036 FE0F ; emoji style; # (1.1) DIGIT SIX +0037 FE0E ; text style; # (1.1) DIGIT SEVEN +0037 FE0F ; emoji style; # (1.1) DIGIT SEVEN +0038 FE0E ; text style; # (1.1) DIGIT EIGHT +0038 FE0F ; emoji style; # (1.1) DIGIT EIGHT +0039 FE0E ; text style; # (1.1) DIGIT NINE +0039 FE0F ; emoji style; # (1.1) DIGIT NINE +00A9 FE0E ; text style; # (1.1) COPYRIGHT SIGN +00A9 FE0F ; emoji style; # (1.1) COPYRIGHT SIGN +00AE FE0E ; text style; # (1.1) REGISTERED SIGN +00AE FE0F ; emoji style; # (1.1) REGISTERED SIGN +203C FE0E ; text style; # (1.1) DOUBLE EXCLAMATION MARK +203C FE0F ; emoji style; # (1.1) DOUBLE EXCLAMATION MARK +2049 FE0E ; text style; # (3.0) EXCLAMATION QUESTION MARK +2049 FE0F ; emoji style; # (3.0) EXCLAMATION QUESTION MARK +2122 FE0E ; text style; # (1.1) TRADE MARK SIGN +2122 FE0F ; emoji style; # (1.1) TRADE MARK SIGN +2139 FE0E ; text style; # (3.0) INFORMATION SOURCE +2139 FE0F ; emoji style; # (3.0) INFORMATION SOURCE +2194 FE0E ; text style; # (1.1) LEFT RIGHT ARROW +2194 FE0F ; emoji style; # (1.1) LEFT RIGHT ARROW +2195 FE0E ; text style; # (1.1) UP DOWN ARROW +2195 FE0F ; emoji style; # (1.1) UP DOWN ARROW +2196 FE0E ; text style; # (1.1) NORTH WEST ARROW +2196 FE0F ; emoji style; # (1.1) NORTH WEST ARROW +2197 FE0E ; text style; # (1.1) NORTH EAST ARROW +2197 FE0F ; emoji style; # (1.1) NORTH EAST ARROW +2198 FE0E ; text style; # (1.1) SOUTH EAST ARROW +2198 FE0F ; emoji style; # (1.1) SOUTH EAST ARROW +2199 FE0E ; text style; # (1.1) SOUTH WEST ARROW +2199 FE0F ; emoji style; # (1.1) SOUTH WEST ARROW +21A9 FE0E ; text style; # (1.1) LEFTWARDS ARROW WITH HOOK +21A9 FE0F ; emoji style; # (1.1) LEFTWARDS ARROW WITH HOOK +21AA FE0E ; text style; # (1.1) RIGHTWARDS ARROW WITH HOOK +21AA FE0F ; emoji style; # (1.1) RIGHTWARDS ARROW WITH HOOK +231A FE0E ; text style; # (1.1) WATCH +231A FE0F ; emoji style; # (1.1) WATCH +231B FE0E ; text style; # (1.1) HOURGLASS +231B FE0F ; emoji style; # (1.1) HOURGLASS +2328 FE0E ; text style; # (1.1) KEYBOARD +2328 FE0F ; emoji style; # (1.1) KEYBOARD +23CF FE0E ; text style; # (4.0) EJECT SYMBOL +23CF FE0F ; emoji style; # (4.0) EJECT SYMBOL +23E9 FE0E ; text style; # (6.0) BLACK RIGHT-POINTING DOUBLE TRIANGLE +23E9 FE0F ; emoji style; # (6.0) BLACK RIGHT-POINTING DOUBLE TRIANGLE +23EA FE0E ; text style; # (6.0) BLACK LEFT-POINTING DOUBLE TRIANGLE +23EA FE0F ; emoji style; # (6.0) BLACK LEFT-POINTING DOUBLE TRIANGLE +23EB FE0E ; text style; # (6.0) BLACK UP-POINTING DOUBLE TRIANGLE +23EB FE0F ; emoji style; # (6.0) BLACK UP-POINTING DOUBLE TRIANGLE +23EC FE0E ; text style; # (6.0) BLACK DOWN-POINTING DOUBLE TRIANGLE +23EC FE0F ; emoji style; # (6.0) BLACK DOWN-POINTING DOUBLE TRIANGLE +23ED FE0E ; text style; # (6.0) BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR +23ED FE0F ; emoji style; # (6.0) BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR +23EE FE0E ; text style; # (6.0) BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR +23EE FE0F ; emoji style; # (6.0) BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR +23EF FE0E ; text style; # (6.0) BLACK RIGHT-POINTING TRIANGLE WITH DOUBLE VERTICAL BAR +23EF FE0F ; emoji style; # (6.0) BLACK RIGHT-POINTING TRIANGLE WITH DOUBLE VERTICAL BAR +23F0 FE0E ; text style; # (6.0) ALARM CLOCK +23F0 FE0F ; emoji style; # (6.0) ALARM CLOCK +23F1 FE0E ; text style; # (6.0) STOPWATCH +23F1 FE0F ; emoji style; # (6.0) STOPWATCH +23F2 FE0E ; text style; # (6.0) TIMER CLOCK +23F2 FE0F ; emoji style; # (6.0) TIMER CLOCK +23F3 FE0E ; text style; # (6.0) HOURGLASS WITH FLOWING SAND +23F3 FE0F ; emoji style; # (6.0) HOURGLASS WITH FLOWING SAND +23F8 FE0E ; text style; # (7.0) DOUBLE VERTICAL BAR +23F8 FE0F ; emoji style; # (7.0) DOUBLE VERTICAL BAR +23F9 FE0E ; text style; # (7.0) BLACK SQUARE FOR STOP +23F9 FE0F ; emoji style; # (7.0) BLACK SQUARE FOR STOP +23FA FE0E ; text style; # (7.0) BLACK CIRCLE FOR RECORD +23FA FE0F ; emoji style; # (7.0) BLACK CIRCLE FOR RECORD +24C2 FE0E ; text style; # (1.1) CIRCLED LATIN CAPITAL LETTER M +24C2 FE0F ; emoji style; # (1.1) CIRCLED LATIN CAPITAL LETTER M +25AA FE0E ; text style; # (1.1) BLACK SMALL SQUARE +25AA FE0F ; emoji style; # (1.1) BLACK SMALL SQUARE +25AB FE0E ; text style; # (1.1) WHITE SMALL SQUARE +25AB FE0F ; emoji style; # (1.1) WHITE SMALL SQUARE +25B6 FE0E ; text style; # (1.1) BLACK RIGHT-POINTING TRIANGLE +25B6 FE0F ; emoji style; # (1.1) BLACK RIGHT-POINTING TRIANGLE +25C0 FE0E ; text style; # (1.1) BLACK LEFT-POINTING TRIANGLE +25C0 FE0F ; emoji style; # (1.1) BLACK LEFT-POINTING TRIANGLE +25FB FE0E ; text style; # (3.2) WHITE MEDIUM SQUARE +25FB FE0F ; emoji style; # (3.2) WHITE MEDIUM SQUARE +25FC FE0E ; text style; # (3.2) BLACK MEDIUM SQUARE +25FC FE0F ; emoji style; # (3.2) BLACK MEDIUM SQUARE +25FD FE0E ; text style; # (3.2) WHITE MEDIUM SMALL SQUARE +25FD FE0F ; emoji style; # (3.2) WHITE MEDIUM SMALL SQUARE +25FE FE0E ; text style; # (3.2) BLACK MEDIUM SMALL SQUARE +25FE FE0F ; emoji style; # (3.2) BLACK MEDIUM SMALL SQUARE +2600 FE0E ; text style; # (1.1) BLACK SUN WITH RAYS +2600 FE0F ; emoji style; # (1.1) BLACK SUN WITH RAYS +2601 FE0E ; text style; # (1.1) CLOUD +2601 FE0F ; emoji style; # (1.1) CLOUD +2602 FE0E ; text style; # (1.1) UMBRELLA +2602 FE0F ; emoji style; # (1.1) UMBRELLA +2603 FE0E ; text style; # (1.1) SNOWMAN +2603 FE0F ; emoji style; # (1.1) SNOWMAN +2604 FE0E ; text style; # (1.1) COMET +2604 FE0F ; emoji style; # (1.1) COMET +260E FE0E ; text style; # (1.1) BLACK TELEPHONE +260E FE0F ; emoji style; # (1.1) BLACK TELEPHONE +2611 FE0E ; text style; # (1.1) BALLOT BOX WITH CHECK +2611 FE0F ; emoji style; # (1.1) BALLOT BOX WITH CHECK +2614 FE0E ; text style; # (4.0) UMBRELLA WITH RAIN DROPS +2614 FE0F ; emoji style; # (4.0) UMBRELLA WITH RAIN DROPS +2615 FE0E ; text style; # (4.0) HOT BEVERAGE +2615 FE0F ; emoji style; # (4.0) HOT BEVERAGE +2618 FE0E ; text style; # (4.1) SHAMROCK +2618 FE0F ; emoji style; # (4.1) SHAMROCK +261D FE0E ; text style; # (1.1) WHITE UP POINTING INDEX +261D FE0F ; emoji style; # (1.1) WHITE UP POINTING INDEX +2620 FE0E ; text style; # (1.1) SKULL AND CROSSBONES +2620 FE0F ; emoji style; # (1.1) SKULL AND CROSSBONES +2622 FE0E ; text style; # (1.1) RADIOACTIVE SIGN +2622 FE0F ; emoji style; # (1.1) RADIOACTIVE SIGN +2623 FE0E ; text style; # (1.1) BIOHAZARD SIGN +2623 FE0F ; emoji style; # (1.1) BIOHAZARD SIGN +2626 FE0E ; text style; # (1.1) ORTHODOX CROSS +2626 FE0F ; emoji style; # (1.1) ORTHODOX CROSS +262A FE0E ; text style; # (1.1) STAR AND CRESCENT +262A FE0F ; emoji style; # (1.1) STAR AND CRESCENT +262E FE0E ; text style; # (1.1) PEACE SYMBOL +262E FE0F ; emoji style; # (1.1) PEACE SYMBOL +262F FE0E ; text style; # (1.1) YIN YANG +262F FE0F ; emoji style; # (1.1) YIN YANG +2638 FE0E ; text style; # (1.1) WHEEL OF DHARMA +2638 FE0F ; emoji style; # (1.1) WHEEL OF DHARMA +2639 FE0E ; text style; # (1.1) WHITE FROWNING FACE +2639 FE0F ; emoji style; # (1.1) WHITE FROWNING FACE +263A FE0E ; text style; # (1.1) WHITE SMILING FACE +263A FE0F ; emoji style; # (1.1) WHITE SMILING FACE +2640 FE0E ; text style; # (1.1) FEMALE SIGN +2640 FE0F ; emoji style; # (1.1) FEMALE SIGN +2642 FE0E ; text style; # (1.1) MALE SIGN +2642 FE0F ; emoji style; # (1.1) MALE SIGN +2648 FE0E ; text style; # (1.1) ARIES +2648 FE0F ; emoji style; # (1.1) ARIES +2649 FE0E ; text style; # (1.1) TAURUS +2649 FE0F ; emoji style; # (1.1) TAURUS +264A FE0E ; text style; # (1.1) GEMINI +264A FE0F ; emoji style; # (1.1) GEMINI +264B FE0E ; text style; # (1.1) CANCER +264B FE0F ; emoji style; # (1.1) CANCER +264C FE0E ; text style; # (1.1) LEO +264C FE0F ; emoji style; # (1.1) LEO +264D FE0E ; text style; # (1.1) VIRGO +264D FE0F ; emoji style; # (1.1) VIRGO +264E FE0E ; text style; # (1.1) LIBRA +264E FE0F ; emoji style; # (1.1) LIBRA +264F FE0E ; text style; # (1.1) SCORPIUS +264F FE0F ; emoji style; # (1.1) SCORPIUS +2650 FE0E ; text style; # (1.1) SAGITTARIUS +2650 FE0F ; emoji style; # (1.1) SAGITTARIUS +2651 FE0E ; text style; # (1.1) CAPRICORN +2651 FE0F ; emoji style; # (1.1) CAPRICORN +2652 FE0E ; text style; # (1.1) AQUARIUS +2652 FE0F ; emoji style; # (1.1) AQUARIUS +2653 FE0E ; text style; # (1.1) PISCES +2653 FE0F ; emoji style; # (1.1) PISCES +265F FE0E ; text style; # (1.1) BLACK CHESS PAWN +265F FE0F ; emoji style; # (1.1) BLACK CHESS PAWN +2660 FE0E ; text style; # (1.1) BLACK SPADE SUIT +2660 FE0F ; emoji style; # (1.1) BLACK SPADE SUIT +2663 FE0E ; text style; # (1.1) BLACK CLUB SUIT +2663 FE0F ; emoji style; # (1.1) BLACK CLUB SUIT +2665 FE0E ; text style; # (1.1) BLACK HEART SUIT +2665 FE0F ; emoji style; # (1.1) BLACK HEART SUIT +2666 FE0E ; text style; # (1.1) BLACK DIAMOND SUIT +2666 FE0F ; emoji style; # (1.1) BLACK DIAMOND SUIT +2668 FE0E ; text style; # (1.1) HOT SPRINGS +2668 FE0F ; emoji style; # (1.1) HOT SPRINGS +267B FE0E ; text style; # (3.2) BLACK UNIVERSAL RECYCLING SYMBOL +267B FE0F ; emoji style; # (3.2) BLACK UNIVERSAL RECYCLING SYMBOL +267E FE0E ; text style; # (4.1) PERMANENT PAPER SIGN +267E FE0F ; emoji style; # (4.1) PERMANENT PAPER SIGN +267F FE0E ; text style; # (4.1) WHEELCHAIR SYMBOL +267F FE0F ; emoji style; # (4.1) WHEELCHAIR SYMBOL +2692 FE0E ; text style; # (4.1) HAMMER AND PICK +2692 FE0F ; emoji style; # (4.1) HAMMER AND PICK +2693 FE0E ; text style; # (4.1) ANCHOR +2693 FE0F ; emoji style; # (4.1) ANCHOR +2694 FE0E ; text style; # (4.1) CROSSED SWORDS +2694 FE0F ; emoji style; # (4.1) CROSSED SWORDS +2695 FE0E ; text style; # (4.1) STAFF OF AESCULAPIUS +2695 FE0F ; emoji style; # (4.1) STAFF OF AESCULAPIUS +2696 FE0E ; text style; # (4.1) SCALES +2696 FE0F ; emoji style; # (4.1) SCALES +2697 FE0E ; text style; # (4.1) ALEMBIC +2697 FE0F ; emoji style; # (4.1) ALEMBIC +2699 FE0E ; text style; # (4.1) GEAR +2699 FE0F ; emoji style; # (4.1) GEAR +269B FE0E ; text style; # (4.1) ATOM SYMBOL +269B FE0F ; emoji style; # (4.1) ATOM SYMBOL +269C FE0E ; text style; # (4.1) FLEUR-DE-LIS +269C FE0F ; emoji style; # (4.1) FLEUR-DE-LIS +26A0 FE0E ; text style; # (4.0) WARNING SIGN +26A0 FE0F ; emoji style; # (4.0) WARNING SIGN +26A1 FE0E ; text style; # (4.0) HIGH VOLTAGE SIGN +26A1 FE0F ; emoji style; # (4.0) HIGH VOLTAGE SIGN +26A7 FE0E ; text style; # (4.1) MALE WITH STROKE AND MALE AND FEMALE SIGN +26A7 FE0F ; emoji style; # (4.1) MALE WITH STROKE AND MALE AND FEMALE SIGN +26AA FE0E ; text style; # (4.1) MEDIUM WHITE CIRCLE +26AA FE0F ; emoji style; # (4.1) MEDIUM WHITE CIRCLE +26AB FE0E ; text style; # (4.1) MEDIUM BLACK CIRCLE +26AB FE0F ; emoji style; # (4.1) MEDIUM BLACK CIRCLE +26B0 FE0E ; text style; # (4.1) COFFIN +26B0 FE0F ; emoji style; # (4.1) COFFIN +26B1 FE0E ; text style; # (4.1) FUNERAL URN +26B1 FE0F ; emoji style; # (4.1) FUNERAL URN +26BD FE0E ; text style; # (5.2) SOCCER BALL +26BD FE0F ; emoji style; # (5.2) SOCCER BALL +26BE FE0E ; text style; # (5.2) BASEBALL +26BE FE0F ; emoji style; # (5.2) BASEBALL +26C4 FE0E ; text style; # (5.2) SNOWMAN WITHOUT SNOW +26C4 FE0F ; emoji style; # (5.2) SNOWMAN WITHOUT SNOW +26C5 FE0E ; text style; # (5.2) SUN BEHIND CLOUD +26C5 FE0F ; emoji style; # (5.2) SUN BEHIND CLOUD +26C8 FE0E ; text style; # (5.2) THUNDER CLOUD AND RAIN +26C8 FE0F ; emoji style; # (5.2) THUNDER CLOUD AND RAIN +26CE FE0E ; text style; # (6.0) OPHIUCHUS +26CE FE0F ; emoji style; # (6.0) OPHIUCHUS +26CF FE0E ; text style; # (5.2) PICK +26CF FE0F ; emoji style; # (5.2) PICK +26D1 FE0E ; text style; # (5.2) HELMET WITH WHITE CROSS +26D1 FE0F ; emoji style; # (5.2) HELMET WITH WHITE CROSS +26D3 FE0E ; text style; # (5.2) CHAINS +26D3 FE0F ; emoji style; # (5.2) CHAINS +26D4 FE0E ; text style; # (5.2) NO ENTRY +26D4 FE0F ; emoji style; # (5.2) NO ENTRY +26E9 FE0E ; text style; # (5.2) SHINTO SHRINE +26E9 FE0F ; emoji style; # (5.2) SHINTO SHRINE +26EA FE0E ; text style; # (5.2) CHURCH +26EA FE0F ; emoji style; # (5.2) CHURCH +26F0 FE0E ; text style; # (5.2) MOUNTAIN +26F0 FE0F ; emoji style; # (5.2) MOUNTAIN +26F1 FE0E ; text style; # (5.2) UMBRELLA ON GROUND +26F1 FE0F ; emoji style; # (5.2) UMBRELLA ON GROUND +26F2 FE0E ; text style; # (5.2) FOUNTAIN +26F2 FE0F ; emoji style; # (5.2) FOUNTAIN +26F3 FE0E ; text style; # (5.2) FLAG IN HOLE +26F3 FE0F ; emoji style; # (5.2) FLAG IN HOLE +26F4 FE0E ; text style; # (5.2) FERRY +26F4 FE0F ; emoji style; # (5.2) FERRY +26F5 FE0E ; text style; # (5.2) SAILBOAT +26F5 FE0F ; emoji style; # (5.2) SAILBOAT +26F7 FE0E ; text style; # (5.2) SKIER +26F7 FE0F ; emoji style; # (5.2) SKIER +26F8 FE0E ; text style; # (5.2) ICE SKATE +26F8 FE0F ; emoji style; # (5.2) ICE SKATE +26F9 FE0E ; text style; # (5.2) PERSON WITH BALL +26F9 FE0F ; emoji style; # (5.2) PERSON WITH BALL +26FA FE0E ; text style; # (5.2) TENT +26FA FE0F ; emoji style; # (5.2) TENT +26FD FE0E ; text style; # (5.2) FUEL PUMP +26FD FE0F ; emoji style; # (5.2) FUEL PUMP +2702 FE0E ; text style; # (1.1) BLACK SCISSORS +2702 FE0F ; emoji style; # (1.1) BLACK SCISSORS +2705 FE0E ; text style; # (6.0) WHITE HEAVY CHECK MARK +2705 FE0F ; emoji style; # (6.0) WHITE HEAVY CHECK MARK +2708 FE0E ; text style; # (1.1) AIRPLANE +2708 FE0F ; emoji style; # (1.1) AIRPLANE +2709 FE0E ; text style; # (1.1) ENVELOPE +2709 FE0F ; emoji style; # (1.1) ENVELOPE +270A FE0E ; text style; # (6.0) RAISED FIST +270A FE0F ; emoji style; # (6.0) RAISED FIST +270B FE0E ; text style; # (6.0) RAISED HAND +270B FE0F ; emoji style; # (6.0) RAISED HAND +270C FE0E ; text style; # (1.1) VICTORY HAND +270C FE0F ; emoji style; # (1.1) VICTORY HAND +270D FE0E ; text style; # (1.1) WRITING HAND +270D FE0F ; emoji style; # (1.1) WRITING HAND +270F FE0E ; text style; # (1.1) PENCIL +270F FE0F ; emoji style; # (1.1) PENCIL +2712 FE0E ; text style; # (1.1) BLACK NIB +2712 FE0F ; emoji style; # (1.1) BLACK NIB +2714 FE0E ; text style; # (1.1) HEAVY CHECK MARK +2714 FE0F ; emoji style; # (1.1) HEAVY CHECK MARK +2716 FE0E ; text style; # (1.1) HEAVY MULTIPLICATION X +2716 FE0F ; emoji style; # (1.1) HEAVY MULTIPLICATION X +271D FE0E ; text style; # (1.1) LATIN CROSS +271D FE0F ; emoji style; # (1.1) LATIN CROSS +2721 FE0E ; text style; # (1.1) STAR OF DAVID +2721 FE0F ; emoji style; # (1.1) STAR OF DAVID +2728 FE0E ; text style; # (6.0) SPARKLES +2728 FE0F ; emoji style; # (6.0) SPARKLES +2733 FE0E ; text style; # (1.1) EIGHT SPOKED ASTERISK +2733 FE0F ; emoji style; # (1.1) EIGHT SPOKED ASTERISK +2734 FE0E ; text style; # (1.1) EIGHT POINTED BLACK STAR +2734 FE0F ; emoji style; # (1.1) EIGHT POINTED BLACK STAR +2744 FE0E ; text style; # (1.1) SNOWFLAKE +2744 FE0F ; emoji style; # (1.1) SNOWFLAKE +2747 FE0E ; text style; # (1.1) SPARKLE +2747 FE0F ; emoji style; # (1.1) SPARKLE +274C FE0E ; text style; # (6.0) CROSS MARK +274C FE0F ; emoji style; # (6.0) CROSS MARK +274E FE0E ; text style; # (6.0) NEGATIVE SQUARED CROSS MARK +274E FE0F ; emoji style; # (6.0) NEGATIVE SQUARED CROSS MARK +2753 FE0E ; text style; # (6.0) BLACK QUESTION MARK ORNAMENT +2753 FE0F ; emoji style; # (6.0) BLACK QUESTION MARK ORNAMENT +2754 FE0E ; text style; # (6.0) WHITE QUESTION MARK ORNAMENT +2754 FE0F ; emoji style; # (6.0) WHITE QUESTION MARK ORNAMENT +2755 FE0E ; text style; # (6.0) WHITE EXCLAMATION MARK ORNAMENT +2755 FE0F ; emoji style; # (6.0) WHITE EXCLAMATION MARK ORNAMENT +2757 FE0E ; text style; # (5.2) HEAVY EXCLAMATION MARK SYMBOL +2757 FE0F ; emoji style; # (5.2) HEAVY EXCLAMATION MARK SYMBOL +2763 FE0E ; text style; # (1.1) HEAVY HEART EXCLAMATION MARK ORNAMENT +2763 FE0F ; emoji style; # (1.1) HEAVY HEART EXCLAMATION MARK ORNAMENT +2764 FE0E ; text style; # (1.1) HEAVY BLACK HEART +2764 FE0F ; emoji style; # (1.1) HEAVY BLACK HEART +2795 FE0E ; text style; # (6.0) HEAVY PLUS SIGN +2795 FE0F ; emoji style; # (6.0) HEAVY PLUS SIGN +2796 FE0E ; text style; # (6.0) HEAVY MINUS SIGN +2796 FE0F ; emoji style; # (6.0) HEAVY MINUS SIGN +2797 FE0E ; text style; # (6.0) HEAVY DIVISION SIGN +2797 FE0F ; emoji style; # (6.0) HEAVY DIVISION SIGN +27A1 FE0E ; text style; # (1.1) BLACK RIGHTWARDS ARROW +27A1 FE0F ; emoji style; # (1.1) BLACK RIGHTWARDS ARROW +27B0 FE0E ; text style; # (6.0) CURLY LOOP +27B0 FE0F ; emoji style; # (6.0) CURLY LOOP +27BF FE0E ; text style; # (6.0) DOUBLE CURLY LOOP +27BF FE0F ; emoji style; # (6.0) DOUBLE CURLY LOOP +2934 FE0E ; text style; # (3.2) ARROW POINTING RIGHTWARDS THEN CURVING UPWARDS +2934 FE0F ; emoji style; # (3.2) ARROW POINTING RIGHTWARDS THEN CURVING UPWARDS +2935 FE0E ; text style; # (3.2) ARROW POINTING RIGHTWARDS THEN CURVING DOWNWARDS +2935 FE0F ; emoji style; # (3.2) ARROW POINTING RIGHTWARDS THEN CURVING DOWNWARDS +2B05 FE0E ; text style; # (4.0) LEFTWARDS BLACK ARROW +2B05 FE0F ; emoji style; # (4.0) LEFTWARDS BLACK ARROW +2B06 FE0E ; text style; # (4.0) UPWARDS BLACK ARROW +2B06 FE0F ; emoji style; # (4.0) UPWARDS BLACK ARROW +2B07 FE0E ; text style; # (4.0) DOWNWARDS BLACK ARROW +2B07 FE0F ; emoji style; # (4.0) DOWNWARDS BLACK ARROW +2B1B FE0E ; text style; # (5.1) BLACK LARGE SQUARE +2B1B FE0F ; emoji style; # (5.1) BLACK LARGE SQUARE +2B1C FE0E ; text style; # (5.1) WHITE LARGE SQUARE +2B1C FE0F ; emoji style; # (5.1) WHITE LARGE SQUARE +2B50 FE0E ; text style; # (5.1) WHITE MEDIUM STAR +2B50 FE0F ; emoji style; # (5.1) WHITE MEDIUM STAR +2B55 FE0E ; text style; # (5.2) HEAVY LARGE CIRCLE +2B55 FE0F ; emoji style; # (5.2) HEAVY LARGE CIRCLE +3030 FE0E ; text style; # (1.1) WAVY DASH +3030 FE0F ; emoji style; # (1.1) WAVY DASH +303D FE0E ; text style; # (3.2) PART ALTERNATION MARK +303D FE0F ; emoji style; # (3.2) PART ALTERNATION MARK +3297 FE0E ; text style; # (1.1) CIRCLED IDEOGRAPH CONGRATULATION +3297 FE0F ; emoji style; # (1.1) CIRCLED IDEOGRAPH CONGRATULATION +3299 FE0E ; text style; # (1.1) CIRCLED IDEOGRAPH SECRET +3299 FE0F ; emoji style; # (1.1) CIRCLED IDEOGRAPH SECRET +1F004 FE0E ; text style; # (5.1) MAHJONG TILE RED DRAGON +1F004 FE0F ; emoji style; # (5.1) MAHJONG TILE RED DRAGON +1F170 FE0E ; text style; # (6.0) NEGATIVE SQUARED LATIN CAPITAL LETTER A +1F170 FE0F ; emoji style; # (6.0) NEGATIVE SQUARED LATIN CAPITAL LETTER A +1F171 FE0E ; text style; # (6.0) NEGATIVE SQUARED LATIN CAPITAL LETTER B +1F171 FE0F ; emoji style; # (6.0) NEGATIVE SQUARED LATIN CAPITAL LETTER B +1F17E FE0E ; text style; # (6.0) NEGATIVE SQUARED LATIN CAPITAL LETTER O +1F17E FE0F ; emoji style; # (6.0) NEGATIVE SQUARED LATIN CAPITAL LETTER O +1F17F FE0E ; text style; # (5.2) NEGATIVE SQUARED LATIN CAPITAL LETTER P +1F17F FE0F ; emoji style; # (5.2) NEGATIVE SQUARED LATIN CAPITAL LETTER P +1F202 FE0E ; text style; # (6.0) SQUARED KATAKANA SA +1F202 FE0F ; emoji style; # (6.0) SQUARED KATAKANA SA +1F21A FE0E ; text style; # (5.2) SQUARED CJK UNIFIED IDEOGRAPH-7121 +1F21A FE0F ; emoji style; # (5.2) SQUARED CJK UNIFIED IDEOGRAPH-7121 +1F22F FE0E ; text style; # (5.2) SQUARED CJK UNIFIED IDEOGRAPH-6307 +1F22F FE0F ; emoji style; # (5.2) SQUARED CJK UNIFIED IDEOGRAPH-6307 +1F237 FE0E ; text style; # (6.0) SQUARED CJK UNIFIED IDEOGRAPH-6708 +1F237 FE0F ; emoji style; # (6.0) SQUARED CJK UNIFIED IDEOGRAPH-6708 +1F30D FE0E ; text style; # (6.0) EARTH GLOBE EUROPE-AFRICA +1F30D FE0F ; emoji style; # (6.0) EARTH GLOBE EUROPE-AFRICA +1F30E FE0E ; text style; # (6.0) EARTH GLOBE AMERICAS +1F30E FE0F ; emoji style; # (6.0) EARTH GLOBE AMERICAS +1F30F FE0E ; text style; # (6.0) EARTH GLOBE ASIA-AUSTRALIA +1F30F FE0F ; emoji style; # (6.0) EARTH GLOBE ASIA-AUSTRALIA +1F315 FE0E ; text style; # (6.0) FULL MOON SYMBOL +1F315 FE0F ; emoji style; # (6.0) FULL MOON SYMBOL +1F31C FE0E ; text style; # (6.0) LAST QUARTER MOON WITH FACE +1F31C FE0F ; emoji style; # (6.0) LAST QUARTER MOON WITH FACE +1F321 FE0E ; text style; # (7.0) THERMOMETER +1F321 FE0F ; emoji style; # (7.0) THERMOMETER +1F324 FE0E ; text style; # (7.0) WHITE SUN WITH SMALL CLOUD +1F324 FE0F ; emoji style; # (7.0) WHITE SUN WITH SMALL CLOUD +1F325 FE0E ; text style; # (7.0) WHITE SUN BEHIND CLOUD +1F325 FE0F ; emoji style; # (7.0) WHITE SUN BEHIND CLOUD +1F326 FE0E ; text style; # (7.0) WHITE SUN BEHIND CLOUD WITH RAIN +1F326 FE0F ; emoji style; # (7.0) WHITE SUN BEHIND CLOUD WITH RAIN +1F327 FE0E ; text style; # (7.0) CLOUD WITH RAIN +1F327 FE0F ; emoji style; # (7.0) CLOUD WITH RAIN +1F328 FE0E ; text style; # (7.0) CLOUD WITH SNOW +1F328 FE0F ; emoji style; # (7.0) CLOUD WITH SNOW +1F329 FE0E ; text style; # (7.0) CLOUD WITH LIGHTNING +1F329 FE0F ; emoji style; # (7.0) CLOUD WITH LIGHTNING +1F32A FE0E ; text style; # (7.0) CLOUD WITH TORNADO +1F32A FE0F ; emoji style; # (7.0) CLOUD WITH TORNADO +1F32B FE0E ; text style; # (7.0) FOG +1F32B FE0F ; emoji style; # (7.0) FOG +1F32C FE0E ; text style; # (7.0) WIND BLOWING FACE +1F32C FE0F ; emoji style; # (7.0) WIND BLOWING FACE +1F336 FE0E ; text style; # (7.0) HOT PEPPER +1F336 FE0F ; emoji style; # (7.0) HOT PEPPER +1F378 FE0E ; text style; # (6.0) COCKTAIL GLASS +1F378 FE0F ; emoji style; # (6.0) COCKTAIL GLASS +1F37D FE0E ; text style; # (7.0) FORK AND KNIFE WITH PLATE +1F37D FE0F ; emoji style; # (7.0) FORK AND KNIFE WITH PLATE +1F393 FE0E ; text style; # (6.0) GRADUATION CAP +1F393 FE0F ; emoji style; # (6.0) GRADUATION CAP +1F396 FE0E ; text style; # (7.0) MILITARY MEDAL +1F396 FE0F ; emoji style; # (7.0) MILITARY MEDAL +1F397 FE0E ; text style; # (7.0) REMINDER RIBBON +1F397 FE0F ; emoji style; # (7.0) REMINDER RIBBON +1F399 FE0E ; text style; # (7.0) STUDIO MICROPHONE +1F399 FE0F ; emoji style; # (7.0) STUDIO MICROPHONE +1F39A FE0E ; text style; # (7.0) LEVEL SLIDER +1F39A FE0F ; emoji style; # (7.0) LEVEL SLIDER +1F39B FE0E ; text style; # (7.0) CONTROL KNOBS +1F39B FE0F ; emoji style; # (7.0) CONTROL KNOBS +1F39E FE0E ; text style; # (7.0) FILM FRAMES +1F39E FE0F ; emoji style; # (7.0) FILM FRAMES +1F39F FE0E ; text style; # (7.0) ADMISSION TICKETS +1F39F FE0F ; emoji style; # (7.0) ADMISSION TICKETS +1F3A7 FE0E ; text style; # (6.0) HEADPHONE +1F3A7 FE0F ; emoji style; # (6.0) HEADPHONE +1F3AC FE0E ; text style; # (6.0) CLAPPER BOARD +1F3AC FE0F ; emoji style; # (6.0) CLAPPER BOARD +1F3AD FE0E ; text style; # (6.0) PERFORMING ARTS +1F3AD FE0F ; emoji style; # (6.0) PERFORMING ARTS +1F3AE FE0E ; text style; # (6.0) VIDEO GAME +1F3AE FE0F ; emoji style; # (6.0) VIDEO GAME +1F3C2 FE0E ; text style; # (6.0) SNOWBOARDER +1F3C2 FE0F ; emoji style; # (6.0) SNOWBOARDER +1F3C4 FE0E ; text style; # (6.0) SURFER +1F3C4 FE0F ; emoji style; # (6.0) SURFER +1F3C6 FE0E ; text style; # (6.0) TROPHY +1F3C6 FE0F ; emoji style; # (6.0) TROPHY +1F3CA FE0E ; text style; # (6.0) SWIMMER +1F3CA FE0F ; emoji style; # (6.0) SWIMMER +1F3CB FE0E ; text style; # (7.0) WEIGHT LIFTER +1F3CB FE0F ; emoji style; # (7.0) WEIGHT LIFTER +1F3CC FE0E ; text style; # (7.0) GOLFER +1F3CC FE0F ; emoji style; # (7.0) GOLFER +1F3CD FE0E ; text style; # (7.0) RACING MOTORCYCLE +1F3CD FE0F ; emoji style; # (7.0) RACING MOTORCYCLE +1F3CE FE0E ; text style; # (7.0) RACING CAR +1F3CE FE0F ; emoji style; # (7.0) RACING CAR +1F3D4 FE0E ; text style; # (7.0) SNOW CAPPED MOUNTAIN +1F3D4 FE0F ; emoji style; # (7.0) SNOW CAPPED MOUNTAIN +1F3D5 FE0E ; text style; # (7.0) CAMPING +1F3D5 FE0F ; emoji style; # (7.0) CAMPING +1F3D6 FE0E ; text style; # (7.0) BEACH WITH UMBRELLA +1F3D6 FE0F ; emoji style; # (7.0) BEACH WITH UMBRELLA +1F3D7 FE0E ; text style; # (7.0) BUILDING CONSTRUCTION +1F3D7 FE0F ; emoji style; # (7.0) BUILDING CONSTRUCTION +1F3D8 FE0E ; text style; # (7.0) HOUSE BUILDINGS +1F3D8 FE0F ; emoji style; # (7.0) HOUSE BUILDINGS +1F3D9 FE0E ; text style; # (7.0) CITYSCAPE +1F3D9 FE0F ; emoji style; # (7.0) CITYSCAPE +1F3DA FE0E ; text style; # (7.0) DERELICT HOUSE BUILDING +1F3DA FE0F ; emoji style; # (7.0) DERELICT HOUSE BUILDING +1F3DB FE0E ; text style; # (7.0) CLASSICAL BUILDING +1F3DB FE0F ; emoji style; # (7.0) CLASSICAL BUILDING +1F3DC FE0E ; text style; # (7.0) DESERT +1F3DC FE0F ; emoji style; # (7.0) DESERT +1F3DD FE0E ; text style; # (7.0) DESERT ISLAND +1F3DD FE0F ; emoji style; # (7.0) DESERT ISLAND +1F3DE FE0E ; text style; # (7.0) NATIONAL PARK +1F3DE FE0F ; emoji style; # (7.0) NATIONAL PARK +1F3DF FE0E ; text style; # (7.0) STADIUM +1F3DF FE0F ; emoji style; # (7.0) STADIUM +1F3E0 FE0E ; text style; # (6.0) HOUSE BUILDING +1F3E0 FE0F ; emoji style; # (6.0) HOUSE BUILDING +1F3ED FE0E ; text style; # (6.0) FACTORY +1F3ED FE0F ; emoji style; # (6.0) FACTORY +1F3F3 FE0E ; text style; # (7.0) WAVING WHITE FLAG +1F3F3 FE0F ; emoji style; # (7.0) WAVING WHITE FLAG +1F3F5 FE0E ; text style; # (7.0) ROSETTE +1F3F5 FE0F ; emoji style; # (7.0) ROSETTE +1F3F7 FE0E ; text style; # (7.0) LABEL +1F3F7 FE0F ; emoji style; # (7.0) LABEL +1F408 FE0E ; text style; # (6.0) CAT +1F408 FE0F ; emoji style; # (6.0) CAT +1F415 FE0E ; text style; # (6.0) DOG +1F415 FE0F ; emoji style; # (6.0) DOG +1F41F FE0E ; text style; # (6.0) FISH +1F41F FE0F ; emoji style; # (6.0) FISH +1F426 FE0E ; text style; # (6.0) BIRD +1F426 FE0F ; emoji style; # (6.0) BIRD +1F43F FE0E ; text style; # (7.0) CHIPMUNK +1F43F FE0F ; emoji style; # (7.0) CHIPMUNK +1F441 FE0E ; text style; # (7.0) EYE +1F441 FE0F ; emoji style; # (7.0) EYE +1F442 FE0E ; text style; # (6.0) EAR +1F442 FE0F ; emoji style; # (6.0) EAR +1F446 FE0E ; text style; # (6.0) WHITE UP POINTING BACKHAND INDEX +1F446 FE0F ; emoji style; # (6.0) WHITE UP POINTING BACKHAND INDEX +1F447 FE0E ; text style; # (6.0) WHITE DOWN POINTING BACKHAND INDEX +1F447 FE0F ; emoji style; # (6.0) WHITE DOWN POINTING BACKHAND INDEX +1F448 FE0E ; text style; # (6.0) WHITE LEFT POINTING BACKHAND INDEX +1F448 FE0F ; emoji style; # (6.0) WHITE LEFT POINTING BACKHAND INDEX +1F449 FE0E ; text style; # (6.0) WHITE RIGHT POINTING BACKHAND INDEX +1F449 FE0F ; emoji style; # (6.0) WHITE RIGHT POINTING BACKHAND INDEX +1F44D FE0E ; text style; # (6.0) THUMBS UP SIGN +1F44D FE0F ; emoji style; # (6.0) THUMBS UP SIGN +1F44E FE0E ; text style; # (6.0) THUMBS DOWN SIGN +1F44E FE0F ; emoji style; # (6.0) THUMBS DOWN SIGN +1F453 FE0E ; text style; # (6.0) EYEGLASSES +1F453 FE0F ; emoji style; # (6.0) EYEGLASSES +1F46A FE0E ; text style; # (6.0) FAMILY +1F46A FE0F ; emoji style; # (6.0) FAMILY +1F47D FE0E ; text style; # (6.0) EXTRATERRESTRIAL ALIEN +1F47D FE0F ; emoji style; # (6.0) EXTRATERRESTRIAL ALIEN +1F4A3 FE0E ; text style; # (6.0) BOMB +1F4A3 FE0F ; emoji style; # (6.0) BOMB +1F4B0 FE0E ; text style; # (6.0) MONEY BAG +1F4B0 FE0F ; emoji style; # (6.0) MONEY BAG +1F4B3 FE0E ; text style; # (6.0) CREDIT CARD +1F4B3 FE0F ; emoji style; # (6.0) CREDIT CARD +1F4BB FE0E ; text style; # (6.0) PERSONAL COMPUTER +1F4BB FE0F ; emoji style; # (6.0) PERSONAL COMPUTER +1F4BF FE0E ; text style; # (6.0) OPTICAL DISC +1F4BF FE0F ; emoji style; # (6.0) OPTICAL DISC +1F4CB FE0E ; text style; # (6.0) CLIPBOARD +1F4CB FE0F ; emoji style; # (6.0) CLIPBOARD +1F4DA FE0E ; text style; # (6.0) BOOKS +1F4DA FE0F ; emoji style; # (6.0) BOOKS +1F4DF FE0E ; text style; # (6.0) PAGER +1F4DF FE0F ; emoji style; # (6.0) PAGER +1F4E4 FE0E ; text style; # (6.0) OUTBOX TRAY +1F4E4 FE0F ; emoji style; # (6.0) OUTBOX TRAY +1F4E5 FE0E ; text style; # (6.0) INBOX TRAY +1F4E5 FE0F ; emoji style; # (6.0) INBOX TRAY +1F4E6 FE0E ; text style; # (6.0) PACKAGE +1F4E6 FE0F ; emoji style; # (6.0) PACKAGE +1F4EA FE0E ; text style; # (6.0) CLOSED MAILBOX WITH LOWERED FLAG +1F4EA FE0F ; emoji style; # (6.0) CLOSED MAILBOX WITH LOWERED FLAG +1F4EB FE0E ; text style; # (6.0) CLOSED MAILBOX WITH RAISED FLAG +1F4EB FE0F ; emoji style; # (6.0) CLOSED MAILBOX WITH RAISED FLAG +1F4EC FE0E ; text style; # (6.0) OPEN MAILBOX WITH RAISED FLAG +1F4EC FE0F ; emoji style; # (6.0) OPEN MAILBOX WITH RAISED FLAG +1F4ED FE0E ; text style; # (6.0) OPEN MAILBOX WITH LOWERED FLAG +1F4ED FE0F ; emoji style; # (6.0) OPEN MAILBOX WITH LOWERED FLAG +1F4F7 FE0E ; text style; # (6.0) CAMERA +1F4F7 FE0F ; emoji style; # (6.0) CAMERA +1F4F9 FE0E ; text style; # (6.0) VIDEO CAMERA +1F4F9 FE0F ; emoji style; # (6.0) VIDEO CAMERA +1F4FA FE0E ; text style; # (6.0) TELEVISION +1F4FA FE0F ; emoji style; # (6.0) TELEVISION +1F4FB FE0E ; text style; # (6.0) RADIO +1F4FB FE0F ; emoji style; # (6.0) RADIO +1F4FD FE0E ; text style; # (7.0) FILM PROJECTOR +1F4FD FE0F ; emoji style; # (7.0) FILM PROJECTOR +1F508 FE0E ; text style; # (6.0) SPEAKER +1F508 FE0F ; emoji style; # (6.0) SPEAKER +1F50D FE0E ; text style; # (6.0) LEFT-POINTING MAGNIFYING GLASS +1F50D FE0F ; emoji style; # (6.0) LEFT-POINTING MAGNIFYING GLASS +1F512 FE0E ; text style; # (6.0) LOCK +1F512 FE0F ; emoji style; # (6.0) LOCK +1F513 FE0E ; text style; # (6.0) OPEN LOCK +1F513 FE0F ; emoji style; # (6.0) OPEN LOCK +1F549 FE0E ; text style; # (7.0) OM SYMBOL +1F549 FE0F ; emoji style; # (7.0) OM SYMBOL +1F54A FE0E ; text style; # (7.0) DOVE OF PEACE +1F54A FE0F ; emoji style; # (7.0) DOVE OF PEACE +1F550 FE0E ; text style; # (6.0) CLOCK FACE ONE OCLOCK +1F550 FE0F ; emoji style; # (6.0) CLOCK FACE ONE OCLOCK +1F551 FE0E ; text style; # (6.0) CLOCK FACE TWO OCLOCK +1F551 FE0F ; emoji style; # (6.0) CLOCK FACE TWO OCLOCK +1F552 FE0E ; text style; # (6.0) CLOCK FACE THREE OCLOCK +1F552 FE0F ; emoji style; # (6.0) CLOCK FACE THREE OCLOCK +1F553 FE0E ; text style; # (6.0) CLOCK FACE FOUR OCLOCK +1F553 FE0F ; emoji style; # (6.0) CLOCK FACE FOUR OCLOCK +1F554 FE0E ; text style; # (6.0) CLOCK FACE FIVE OCLOCK +1F554 FE0F ; emoji style; # (6.0) CLOCK FACE FIVE OCLOCK +1F555 FE0E ; text style; # (6.0) CLOCK FACE SIX OCLOCK +1F555 FE0F ; emoji style; # (6.0) CLOCK FACE SIX OCLOCK +1F556 FE0E ; text style; # (6.0) CLOCK FACE SEVEN OCLOCK +1F556 FE0F ; emoji style; # (6.0) CLOCK FACE SEVEN OCLOCK +1F557 FE0E ; text style; # (6.0) CLOCK FACE EIGHT OCLOCK +1F557 FE0F ; emoji style; # (6.0) CLOCK FACE EIGHT OCLOCK +1F558 FE0E ; text style; # (6.0) CLOCK FACE NINE OCLOCK +1F558 FE0F ; emoji style; # (6.0) CLOCK FACE NINE OCLOCK +1F559 FE0E ; text style; # (6.0) CLOCK FACE TEN OCLOCK +1F559 FE0F ; emoji style; # (6.0) CLOCK FACE TEN OCLOCK +1F55A FE0E ; text style; # (6.0) CLOCK FACE ELEVEN OCLOCK +1F55A FE0F ; emoji style; # (6.0) CLOCK FACE ELEVEN OCLOCK +1F55B FE0E ; text style; # (6.0) CLOCK FACE TWELVE OCLOCK +1F55B FE0F ; emoji style; # (6.0) CLOCK FACE TWELVE OCLOCK +1F55C FE0E ; text style; # (6.0) CLOCK FACE ONE-THIRTY +1F55C FE0F ; emoji style; # (6.0) CLOCK FACE ONE-THIRTY +1F55D FE0E ; text style; # (6.0) CLOCK FACE TWO-THIRTY +1F55D FE0F ; emoji style; # (6.0) CLOCK FACE TWO-THIRTY +1F55E FE0E ; text style; # (6.0) CLOCK FACE THREE-THIRTY +1F55E FE0F ; emoji style; # (6.0) CLOCK FACE THREE-THIRTY +1F55F FE0E ; text style; # (6.0) CLOCK FACE FOUR-THIRTY +1F55F FE0F ; emoji style; # (6.0) CLOCK FACE FOUR-THIRTY +1F560 FE0E ; text style; # (6.0) CLOCK FACE FIVE-THIRTY +1F560 FE0F ; emoji style; # (6.0) CLOCK FACE FIVE-THIRTY +1F561 FE0E ; text style; # (6.0) CLOCK FACE SIX-THIRTY +1F561 FE0F ; emoji style; # (6.0) CLOCK FACE SIX-THIRTY +1F562 FE0E ; text style; # (6.0) CLOCK FACE SEVEN-THIRTY +1F562 FE0F ; emoji style; # (6.0) CLOCK FACE SEVEN-THIRTY +1F563 FE0E ; text style; # (6.0) CLOCK FACE EIGHT-THIRTY +1F563 FE0F ; emoji style; # (6.0) CLOCK FACE EIGHT-THIRTY +1F564 FE0E ; text style; # (6.0) CLOCK FACE NINE-THIRTY +1F564 FE0F ; emoji style; # (6.0) CLOCK FACE NINE-THIRTY +1F565 FE0E ; text style; # (6.0) CLOCK FACE TEN-THIRTY +1F565 FE0F ; emoji style; # (6.0) CLOCK FACE TEN-THIRTY +1F566 FE0E ; text style; # (6.0) CLOCK FACE ELEVEN-THIRTY +1F566 FE0F ; emoji style; # (6.0) CLOCK FACE ELEVEN-THIRTY +1F567 FE0E ; text style; # (6.0) CLOCK FACE TWELVE-THIRTY +1F567 FE0F ; emoji style; # (6.0) CLOCK FACE TWELVE-THIRTY +1F56F FE0E ; text style; # (7.0) CANDLE +1F56F FE0F ; emoji style; # (7.0) CANDLE +1F570 FE0E ; text style; # (7.0) MANTELPIECE CLOCK +1F570 FE0F ; emoji style; # (7.0) MANTELPIECE CLOCK +1F573 FE0E ; text style; # (7.0) HOLE +1F573 FE0F ; emoji style; # (7.0) HOLE +1F574 FE0E ; text style; # (7.0) MAN IN BUSINESS SUIT LEVITATING +1F574 FE0F ; emoji style; # (7.0) MAN IN BUSINESS SUIT LEVITATING +1F575 FE0E ; text style; # (7.0) SLEUTH OR SPY +1F575 FE0F ; emoji style; # (7.0) SLEUTH OR SPY +1F576 FE0E ; text style; # (7.0) DARK SUNGLASSES +1F576 FE0F ; emoji style; # (7.0) DARK SUNGLASSES +1F577 FE0E ; text style; # (7.0) SPIDER +1F577 FE0F ; emoji style; # (7.0) SPIDER +1F578 FE0E ; text style; # (7.0) SPIDER WEB +1F578 FE0F ; emoji style; # (7.0) SPIDER WEB +1F579 FE0E ; text style; # (7.0) JOYSTICK +1F579 FE0F ; emoji style; # (7.0) JOYSTICK +1F587 FE0E ; text style; # (7.0) LINKED PAPERCLIPS +1F587 FE0F ; emoji style; # (7.0) LINKED PAPERCLIPS +1F58A FE0E ; text style; # (7.0) LOWER LEFT BALLPOINT PEN +1F58A FE0F ; emoji style; # (7.0) LOWER LEFT BALLPOINT PEN +1F58B FE0E ; text style; # (7.0) LOWER LEFT FOUNTAIN PEN +1F58B FE0F ; emoji style; # (7.0) LOWER LEFT FOUNTAIN PEN +1F58C FE0E ; text style; # (7.0) LOWER LEFT PAINTBRUSH +1F58C FE0F ; emoji style; # (7.0) LOWER LEFT PAINTBRUSH +1F58D FE0E ; text style; # (7.0) LOWER LEFT CRAYON +1F58D FE0F ; emoji style; # (7.0) LOWER LEFT CRAYON +1F590 FE0E ; text style; # (7.0) RAISED HAND WITH FINGERS SPLAYED +1F590 FE0F ; emoji style; # (7.0) RAISED HAND WITH FINGERS SPLAYED +1F5A5 FE0E ; text style; # (7.0) DESKTOP COMPUTER +1F5A5 FE0F ; emoji style; # (7.0) DESKTOP COMPUTER +1F5A8 FE0E ; text style; # (7.0) PRINTER +1F5A8 FE0F ; emoji style; # (7.0) PRINTER +1F5B1 FE0E ; text style; # (7.0) THREE BUTTON MOUSE +1F5B1 FE0F ; emoji style; # (7.0) THREE BUTTON MOUSE +1F5B2 FE0E ; text style; # (7.0) TRACKBALL +1F5B2 FE0F ; emoji style; # (7.0) TRACKBALL +1F5BC FE0E ; text style; # (7.0) FRAME WITH PICTURE +1F5BC FE0F ; emoji style; # (7.0) FRAME WITH PICTURE +1F5C2 FE0E ; text style; # (7.0) CARD INDEX DIVIDERS +1F5C2 FE0F ; emoji style; # (7.0) CARD INDEX DIVIDERS +1F5C3 FE0E ; text style; # (7.0) CARD FILE BOX +1F5C3 FE0F ; emoji style; # (7.0) CARD FILE BOX +1F5C4 FE0E ; text style; # (7.0) FILE CABINET +1F5C4 FE0F ; emoji style; # (7.0) FILE CABINET +1F5D1 FE0E ; text style; # (7.0) WASTEBASKET +1F5D1 FE0F ; emoji style; # (7.0) WASTEBASKET +1F5D2 FE0E ; text style; # (7.0) SPIRAL NOTE PAD +1F5D2 FE0F ; emoji style; # (7.0) SPIRAL NOTE PAD +1F5D3 FE0E ; text style; # (7.0) SPIRAL CALENDAR PAD +1F5D3 FE0F ; emoji style; # (7.0) SPIRAL CALENDAR PAD +1F5DC FE0E ; text style; # (7.0) COMPRESSION +1F5DC FE0F ; emoji style; # (7.0) COMPRESSION +1F5DD FE0E ; text style; # (7.0) OLD KEY +1F5DD FE0F ; emoji style; # (7.0) OLD KEY +1F5DE FE0E ; text style; # (7.0) ROLLED-UP NEWSPAPER +1F5DE FE0F ; emoji style; # (7.0) ROLLED-UP NEWSPAPER +1F5E1 FE0E ; text style; # (7.0) DAGGER KNIFE +1F5E1 FE0F ; emoji style; # (7.0) DAGGER KNIFE +1F5E3 FE0E ; text style; # (7.0) SPEAKING HEAD IN SILHOUETTE +1F5E3 FE0F ; emoji style; # (7.0) SPEAKING HEAD IN SILHOUETTE +1F5E8 FE0E ; text style; # (7.0) LEFT SPEECH BUBBLE +1F5E8 FE0F ; emoji style; # (7.0) LEFT SPEECH BUBBLE +1F5EF FE0E ; text style; # (7.0) RIGHT ANGER BUBBLE +1F5EF FE0F ; emoji style; # (7.0) RIGHT ANGER BUBBLE +1F5F3 FE0E ; text style; # (7.0) BALLOT BOX WITH BALLOT +1F5F3 FE0F ; emoji style; # (7.0) BALLOT BOX WITH BALLOT +1F5FA FE0E ; text style; # (7.0) WORLD MAP +1F5FA FE0F ; emoji style; # (7.0) WORLD MAP +1F610 FE0E ; text style; # (6.0) NEUTRAL FACE +1F610 FE0F ; emoji style; # (6.0) NEUTRAL FACE +1F687 FE0E ; text style; # (6.0) METRO +1F687 FE0F ; emoji style; # (6.0) METRO +1F68D FE0E ; text style; # (6.0) ONCOMING BUS +1F68D FE0F ; emoji style; # (6.0) ONCOMING BUS +1F691 FE0E ; text style; # (6.0) AMBULANCE +1F691 FE0F ; emoji style; # (6.0) AMBULANCE +1F694 FE0E ; text style; # (6.0) ONCOMING POLICE CAR +1F694 FE0F ; emoji style; # (6.0) ONCOMING POLICE CAR +1F698 FE0E ; text style; # (6.0) ONCOMING AUTOMOBILE +1F698 FE0F ; emoji style; # (6.0) ONCOMING AUTOMOBILE +1F6AD FE0E ; text style; # (6.0) NO SMOKING SYMBOL +1F6AD FE0F ; emoji style; # (6.0) NO SMOKING SYMBOL +1F6B2 FE0E ; text style; # (6.0) BICYCLE +1F6B2 FE0F ; emoji style; # (6.0) BICYCLE +1F6B9 FE0E ; text style; # (6.0) MENS SYMBOL +1F6B9 FE0F ; emoji style; # (6.0) MENS SYMBOL +1F6BA FE0E ; text style; # (6.0) WOMENS SYMBOL +1F6BA FE0F ; emoji style; # (6.0) WOMENS SYMBOL +1F6BC FE0E ; text style; # (6.0) BABY SYMBOL +1F6BC FE0F ; emoji style; # (6.0) BABY SYMBOL +1F6CB FE0E ; text style; # (7.0) COUCH AND LAMP +1F6CB FE0F ; emoji style; # (7.0) COUCH AND LAMP +1F6CD FE0E ; text style; # (7.0) SHOPPING BAGS +1F6CD FE0F ; emoji style; # (7.0) SHOPPING BAGS +1F6CE FE0E ; text style; # (7.0) BELLHOP BELL +1F6CE FE0F ; emoji style; # (7.0) BELLHOP BELL +1F6CF FE0E ; text style; # (7.0) BED +1F6CF FE0F ; emoji style; # (7.0) BED +1F6E0 FE0E ; text style; # (7.0) HAMMER AND WRENCH +1F6E0 FE0F ; emoji style; # (7.0) HAMMER AND WRENCH +1F6E1 FE0E ; text style; # (7.0) SHIELD +1F6E1 FE0F ; emoji style; # (7.0) SHIELD +1F6E2 FE0E ; text style; # (7.0) OIL DRUM +1F6E2 FE0F ; emoji style; # (7.0) OIL DRUM +1F6E3 FE0E ; text style; # (7.0) MOTORWAY +1F6E3 FE0F ; emoji style; # (7.0) MOTORWAY +1F6E4 FE0E ; text style; # (7.0) RAILWAY TRACK +1F6E4 FE0F ; emoji style; # (7.0) RAILWAY TRACK +1F6E5 FE0E ; text style; # (7.0) MOTOR BOAT +1F6E5 FE0F ; emoji style; # (7.0) MOTOR BOAT +1F6E9 FE0E ; text style; # (7.0) SMALL AIRPLANE +1F6E9 FE0F ; emoji style; # (7.0) SMALL AIRPLANE +1F6F0 FE0E ; text style; # (7.0) SATELLITE +1F6F0 FE0F ; emoji style; # (7.0) SATELLITE +1F6F3 FE0E ; text style; # (7.0) PASSENGER SHIP +1F6F3 FE0F ; emoji style; # (7.0) PASSENGER SHIP + +#Total sequences: 371 + +#EOF diff --git a/vt.c b/vt.c index 41bb305d..ebc94bc2 100644 --- a/vt.c +++ b/vt.c @@ -16,6 +16,7 @@ #include "csi.h" #include "dcs.h" #include "debug.h" +#include "emoji-variation-sequences.h" #include "grid.h" #include "osc.h" #include "sixel.h" @@ -655,6 +656,24 @@ chain_key(uint32_t old_key, uint32_t new_wc) return new_key; } +#if defined(FOOT_GRAPHEME_CLUSTERING) +static int +emoji_vs_compare(const void *_key, const void *_entry) +{ + const struct emoji_vs *key = _key; + const struct emoji_vs *entry = _entry; + + uint32_t cp = key->start; + + if (cp < entry->start) + return -1; + else if (cp > entry->end) + return 1; + else + return 0; +} +#endif + static void action_utf8_print(struct terminal *term, char32_t wc) { @@ -855,16 +874,31 @@ action_utf8_print(struct terminal *term, char32_t wc) new_cc->width = min(grapheme_width + width, 2); #if defined(FOOT_GRAPHEME_CLUSTERING) + /* Handle VS-15 and VS-16 variation selectors */ if (unlikely(grapheme_clustering && (wc == 0xfe0e || wc == 0xfe0f) && new_cc->count == 2)) { - /* Only emojis should be affected by VS16 */ - const utf8proc_property_t *props = - utf8proc_get_property(new_cc->chars[0]); + const struct emoji_vs *vs = + bsearch( + &(struct emoji_vs){.start = new_cc->chars[0]}, + emoji_vs, sizeof(emoji_vs) / sizeof(emoji_vs[0]), + sizeof(struct emoji_vs), + &emoji_vs_compare); - if (props->boundclass == UTF8PROC_BOUNDCLASS_EXTENDED_PICTOGRAPHIC) - new_cc->width = wc - 0xfe0d; /* 1 for VS-15, 2 for VS-16 */ + if (vs != NULL) { + xassert(new_cc->chars[0] >= vs->start && + new_cc->chars[0] <= vs->end); + + /* Force a grapheme width of 1 for VS-15, and 2 for VS-16 */ + if (wc == 0xfe0e) { + if (vs->vs15) + new_cc->width = 1; + } else if (wc == 0xfe0f) { + if (vs->vs16) + new_cc->width = 2; + } + } } #endif From ecb1ca61afe3c3b3ff676bfc815996b5b2ffd26d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 25 Jun 2024 08:23:27 +0200 Subject: [PATCH 0735/1323] scripts: generate-emoji-variation-sequences: don't assume input is sorted --- scripts/generate-emoji-variation-sequences.py | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/scripts/generate-emoji-variation-sequences.py b/scripts/generate-emoji-variation-sequences.py index 1a4069ac..7324ae3b 100644 --- a/scripts/generate-emoji-variation-sequences.py +++ b/scripts/generate-emoji-variation-sequences.py @@ -21,7 +21,7 @@ def main(): parser.add_argument('output', type=argparse.FileType('w')) opts = parser.parse_args() - codepoints: list[Codepoint] = [] + codepoints: dict[int, Codepoint] = {} for line in opts.input: line = line.rstrip() @@ -36,30 +36,31 @@ def main(): assert vs == 0xfe0e or vs == 0xfe0f - if len(codepoints) == 0 or codepoints[-1].start != cp: - codepoints.append(Codepoint(cp)) - else: - assert codepoints[-1].start == cp + if cp not in codepoints: + codepoints[cp] = Codepoint(cp) + + assert codepoints[cp].start == cp if vs == 0xfe0e: - codepoints[-1].vs15 = True + codepoints[cp].vs15 = True else: - codepoints[-1].vs16 = True + codepoints[cp].vs16 = True + sorted_list = sorted(codepoints.values(), key=lambda cp: cp.start) - compacted_codepoints: list[Codepoint] = [] - for i, cp in enumerate(codepoints): + compacted: list[Codepoint] = [] + for i, cp in enumerate(sorted_list): assert cp.end == cp.start if i == 0: - compacted_codepoints.append(cp) + compacted.append(cp) continue - last_cp = compacted_codepoints[-1] + last_cp = compacted[-1] if last_cp.end == cp.start - 1 and last_cp.vs15 == cp.vs15 and last_cp.vs16 == cp.vs16: - compacted_codepoints[-1].end = cp.start + compacted[-1].end = cp.start else: - compacted_codepoints.append(cp) + compacted.append(cp) opts.output.write('#pragma once\n') opts.output.write('#include <stdint.h>\n') @@ -76,9 +77,9 @@ def main(): opts.output.write('#if defined(FOOT_GRAPHEME_CLUSTERING)\n') opts.output.write('\n') - opts.output.write(f'static const struct emoji_vs emoji_vs[{len(compacted_codepoints)}] = {{\n') + opts.output.write(f'static const struct emoji_vs emoji_vs[{len(compacted)}] = {{\n') - for cp in compacted_codepoints: + for cp in compacted: opts.output.write(' {\n') opts.output.write(f' .start = 0x{cp.start:X},\n') opts.output.write(f' .end = 0x{cp.end:x},\n') From 7378ecf9a76352b90ddc7bf95a582eb9b124acda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 25 Jun 2024 08:23:40 +0200 Subject: [PATCH 0736/1323] vt: unittest: verify emoji_vs list is sorted --- vt.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/vt.c b/vt.c index ebc94bc2..ba78540f 100644 --- a/vt.c +++ b/vt.c @@ -672,6 +672,20 @@ emoji_vs_compare(const void *_key, const void *_entry) else return 0; } + +UNITTEST +{ + /* Verify the emoji_vs list is sorted */ + int64_t last_end = -1; + + for (size_t i = 0; i < sizeof(emoji_vs) / sizeof(emoji_vs[0]); i++) { + const struct emoji_vs *vs = &emoji_vs[i]; + xassert(vs->start <= vs->end); + xassert(vs->start > last_end); + xassert(vs->vs15 || vs->vs16); + last_end = vs->end; + } +} #endif static void From aed9c392ebdf1b030f819ad3e55d21d80dbee2cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 25 Jun 2024 16:22:22 +0200 Subject: [PATCH 0737/1323] scripts: generate-emoji-variation-sequences: compact the C struct Lookups in this table is not performance critical at all. Thus, let's compact it to bring down the binary size of foot. This brings the size of each entry down from 9 bytes to 6, bringing the size of the whole thing down from 1647 bytes to 1098 bytes, saving us 549 bytes. --- scripts/generate-emoji-variation-sequences.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/generate-emoji-variation-sequences.py b/scripts/generate-emoji-variation-sequences.py index 7324ae3b..2c71594c 100644 --- a/scripts/generate-emoji-variation-sequences.py +++ b/scripts/generate-emoji-variation-sequences.py @@ -67,12 +67,12 @@ def main(): opts.output.write('#include <stdbool.h>\n') opts.output.write('\n') opts.output.write('struct emoji_vs {\n') - opts.output.write(' uint32_t start;\n') - opts.output.write(' uint32_t end;\n') + opts.output.write(' uint32_t start:21;\n') + opts.output.write(' uint32_t end:21;\n') opts.output.write(' bool vs15:1;\n') opts.output.write(' bool vs16:1;\n') opts.output.write('} __attribute__((packed));\n') - opts.output.write('_Static_assert(sizeof(struct emoji_vs) == 9, "unexpected struct size");\n') + opts.output.write('_Static_assert(sizeof(struct emoji_vs) == 6, "unexpected struct size");\n') opts.output.write('\n') opts.output.write('#if defined(FOOT_GRAPHEME_CLUSTERING)\n') opts.output.write('\n') From 085c60a33423bf46f64f225e438f62ad12fff8c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 26 Jun 2024 18:30:17 +0200 Subject: [PATCH 0738/1323] scripts: generate-emoji-variation-sequences: don't assume single codepoint sequences Right now (Unicode 15.1), all valid variation sequences consist of a single Unicode codepoint (followed by either VS-15 or VS-16). Don't assume this is the case. We don't actually handle longer sequences. But now we at least catch such escapes, and error out. --- scripts/generate-emoji-variation-sequences.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/scripts/generate-emoji-variation-sequences.py b/scripts/generate-emoji-variation-sequences.py index 2c71594c..e05b6290 100644 --- a/scripts/generate-emoji-variation-sequences.py +++ b/scripts/generate-emoji-variation-sequences.py @@ -30,11 +30,18 @@ def main(): if line[0] == '#': continue - cp, vs, _ = line.split(' ', maxsplit=2) - cp = int(cp, 16) - vs = int(vs, 16) + # Example: "0023 FE0E ; text style; # (1.1) NUMBER SIGN" + cps, _ = line.split(';', maxsplit=1) # cps = "0023 FE0F " + cps = cps.strip().split(' ') # cps = ["0023", "FE0F"] - assert vs == 0xfe0e or vs == 0xfe0f + if len(cps) != 2: + raise NotImplementedError(f'emoji variation sequences with more than one base codepoint: {cps}') + + cp, vs = cps # cp = "0023", vs = "FE0F" + cp = int(cp, 16) # cp = 0x23 + vs = int(vs, 16) # vs = 0xfe0f + + assert vs in [0xfe0e, 0xfe0f] if cp not in codepoints: codepoints[cp] = Codepoint(cp) From 20923bb2e8235c0e3d72f7c6195523ad0b0a2afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 23 Jun 2024 13:29:12 +0200 Subject: [PATCH 0739/1323] grid: refactor: first step towards a more generic range handling --- grid.c | 163 +++++++++++++++++++++++++++++++---------------------- grid.h | 20 ++++++- terminal.h | 27 ++++++--- url-mode.c | 6 +- 4 files changed, 135 insertions(+), 81 deletions(-) diff --git a/grid.c b/grid.c index 85e6183f..cbc00177 100644 --- a/grid.c +++ b/grid.c @@ -85,22 +85,27 @@ ensure_row_has_extra_data(struct row *row) } static void -verify_no_overlapping_uris(const struct row_data *extra) +verify_no_overlapping_ranges_of_type(const struct row_ranges *ranges, + enum row_range_type type) { #if defined(_DEBUG) - for (size_t i = 0; i < extra->uri_ranges.count; i++) { - const struct row_uri_range *r1 = &extra->uri_ranges.v[i]; + for (size_t i = 0; i < ranges->count; i++) { + const struct row_range *r1 = &ranges->v[i]; - for (size_t j = i + 1; j < extra->uri_ranges.count; j++) { - const struct row_uri_range *r2 = &extra->uri_ranges.v[j]; + for (size_t j = i + 1; j < ranges->count; j++) { + const struct row_range *r2 = &ranges->v[j]; xassert(r1 != r2); if ((r1->start <= r2->start && r1->end >= r2->start) || (r1->start <= r2->end && r1->end >= r2->end)) { - BUG("OSC-8 URI overlap: %s: %d-%d: %s: %d-%d", - r1->uri, r1->start, r1->end, - r2->uri, r2->start, r2->end); + switch (type) { + case ROW_RANGE_URI: + BUG("OSC-8 URI overlap: %s: %d-%d: %s: %d-%d", + r1->uri.uri, r1->start, r1->end, + r2->uri.uri, r2->start, r2->end); + break; + } } } } @@ -108,20 +113,31 @@ verify_no_overlapping_uris(const struct row_data *extra) } static void -verify_uris_are_sorted(const struct row_data *extra) +verify_no_overlapping_ranges(const struct row_data *extra) +{ + verify_no_overlapping_ranges_of_type(&extra->uri_ranges, ROW_RANGE_URI); +} + +static void +verify_ranges_of_type_are_sorted(const struct row_ranges *ranges, + enum row_range_type type) { #if defined(_DEBUG) - const struct row_uri_range *last = NULL; + const struct row_range *last = NULL; - for (size_t i = 0; i < extra->uri_ranges.count; i++) { - const struct row_uri_range *r = &extra->uri_ranges.v[i]; + for (size_t i = 0; i < ranges->count; i++) { + const struct row_range *r = &ranges->v[i]; if (last != NULL) { if (last->start >= r->start || last->end >= r->end) { - BUG("OSC-8 URI not sorted correctly: " - "%s: %d-%d came before %s: %d-%d", - last->uri, last->start, last->end, - r->uri, r->start, r->end); + switch (type) { + case ROW_RANGE_URI: + BUG("OSC-8 URI not sorted correctly: " + "%s: %d-%d came before %s: %d-%d", + last->uri.uri, last->start, last->end, + r->uri.uri, r->start, r->end); + break; + } } } @@ -130,6 +146,12 @@ verify_uris_are_sorted(const struct row_data *extra) #endif } +static void +verify_ranges_are_sorted(const struct row_data *extra) +{ + verify_ranges_of_type_are_sorted(&extra->uri_ranges, ROW_RANGE_URI); +} + static void uri_range_ensure_size(struct row_data *extra, uint32_t count_to_add) { @@ -161,11 +183,13 @@ uri_range_insert(struct row_data *extra, size_t idx, int start, int end, move_count * sizeof(extra->uri_ranges.v[0])); extra->uri_ranges.count++; - extra->uri_ranges.v[idx] = (struct row_uri_range){ + extra->uri_ranges.v[idx] = (struct row_range){ .start = start, .end = end, - .id = id, - .uri = xstrdup(uri), + .uri = { + .id = id, + .uri = xstrdup(uri), + }, }; } @@ -174,11 +198,13 @@ uri_range_append_no_strdup(struct row_data *extra, int start, int end, uint64_t id, char *uri) { uri_range_ensure_size(extra, 1); - extra->uri_ranges.v[extra->uri_ranges.count++] = (struct row_uri_range){ + extra->uri_ranges.v[extra->uri_ranges.count++] = (struct row_range){ .start = start, .end = end, - .id = id, - .uri = uri, + .uri = { + .id = id, + .uri = uri, + }, }; } @@ -246,10 +272,10 @@ grid_snapshot(const struct grid *grid) uri_range_ensure_size(clone_extra, extra->uri_ranges.count); for (size_t i = 0; i < extra->uri_ranges.count; i++) { - const struct row_uri_range *range = &extra->uri_ranges.v[i]; + const struct row_range *range = &extra->uri_ranges.v[i]; uri_range_append( clone_extra, - range->start, range->end, range->id, range->uri); + range->start, range->end, range->uri.id, range->uri.uri); } } else clone_row->extra = NULL; @@ -467,7 +493,7 @@ grid_resize_without_reflow( uri_range_ensure_size(new_extra, old_extra->uri_ranges.count); for (size_t i = 0; i < old_extra->uri_ranges.count; i++) { - const struct row_uri_range *range = &old_extra->uri_ranges.v[i]; + const struct row_range *range = &old_extra->uri_ranges.v[i]; if (range->start >= new_cols) { /* The whole range is truncated */ @@ -476,7 +502,7 @@ grid_resize_without_reflow( const int start = range->start; const int end = min(range->end, new_cols - 1); - uri_range_append(new_extra, start, end, range->id, range->uri); + uri_range_append(new_extra, start, end, range->uri.id, range->uri.uri); } } @@ -498,8 +524,8 @@ grid_resize_without_reflow( if (row->extra == NULL) continue; - verify_no_overlapping_uris(row->extra); - verify_uris_are_sorted(row->extra); + verify_no_overlapping_ranges(row->extra); + verify_ranges_are_sorted(row->extra); } #endif @@ -549,26 +575,26 @@ grid_resize_without_reflow( } static void -reflow_uri_range_start(struct row_uri_range *range, struct row *new_row, +reflow_uri_range_start(struct row_range *range, struct row *new_row, int new_col_idx) { ensure_row_has_extra_data(new_row); uri_range_append_no_strdup - (new_row->extra, new_col_idx, -1, range->id, range->uri); - range->uri = NULL; + (new_row->extra, new_col_idx, -1, range->uri.id, range->uri.uri); + range->uri.uri = NULL; } static void -reflow_uri_range_end(struct row_uri_range *range, struct row *new_row, +reflow_uri_range_end(struct row_range *range, struct row *new_row, int new_col_idx) { struct row_data *extra = new_row->extra; xassert(extra->uri_ranges.count > 0); - struct row_uri_range *new_range = + struct row_range *new_range = &extra->uri_ranges.v[extra->uri_ranges.count - 1]; - xassert(new_range->id == range->id); + xassert(new_range->uri.id == range->uri.id); xassert(new_range->end < 0); new_range->end = new_col_idx; } @@ -619,7 +645,7 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, * next/current row. */ if (extra->uri_ranges.count > 0) { - struct row_uri_range *range = + struct row_range *range = &extra->uri_ranges.v[extra->uri_ranges.count - 1]; if (range->end < 0) { @@ -629,7 +655,7 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, /* Open a new range on the new/current row */ ensure_row_has_extra_data(new_row); - uri_range_append(new_row->extra, 0, -1, range->id, range->uri); + uri_range_append(new_row->extra, 0, -1, range->uri.id, range->uri.uri); } } @@ -817,7 +843,7 @@ grid_resize_and_reflow( tp = NULL; /* Does this row have any URIs? */ - struct row_uri_range *range, *range_terminator; + struct row_range *range, *range_terminator; struct row_data *extra = old_row->extra; if (extra != NULL && extra->uri_ranges.count > 0) { @@ -826,7 +852,7 @@ grid_resize_and_reflow( /* Make sure the *last* URI range's end point is included * in the copy */ - const struct row_uri_range *last_on_row = + const struct row_range *last_on_row = &extra->uri_ranges.v[extra->uri_ranges.count - 1]; col_count = max(col_count, last_on_row->end + 1); } else @@ -1043,8 +1069,8 @@ grid_resize_and_reflow( for (size_t i = 0; i < row->extra->uri_ranges.count; i++) xassert(row->extra->uri_ranges.v[i].end >= 0); - verify_no_overlapping_uris(row->extra); - verify_uris_are_sorted(row->extra); + verify_no_overlapping_ranges(row->extra); + verify_ranges_are_sorted(row->extra); } /* Verify all old rows have been free:d */ @@ -1136,9 +1162,9 @@ grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id) struct row_data *extra = row->extra; for (ssize_t i = (ssize_t)extra->uri_ranges.count - 1; i >= 0; i--) { - struct row_uri_range *r = &extra->uri_ranges.v[i]; + struct row_range *r = &extra->uri_ranges.v[i]; - const bool matching_id = r->id == id; + const bool matching_id = r->uri.id == id; if (matching_id && r->end + 1 == col) { /* Extend existing URI's tail */ @@ -1177,7 +1203,7 @@ grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id) xassert(r->start < col); xassert(r->end > col); - uri_range_insert(extra, i + 1, col + 1, r->end, r->id, r->uri); + uri_range_insert(extra, i + 1, col + 1, r->end, r->uri.id, r->uri.uri); /* The insertion may xrealloc() the vector, making our * 'old' pointer invalid */ @@ -1196,21 +1222,23 @@ grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id) if (replace) { grid_row_uri_range_destroy(&extra->uri_ranges.v[insert_idx]); - extra->uri_ranges.v[insert_idx] = (struct row_uri_range){ + extra->uri_ranges.v[insert_idx] = (struct row_range){ .start = col, .end = col, - .id = id, - .uri = xstrdup(uri), + .uri = { + .id = id, + .uri = xstrdup(uri), + }, }; } else uri_range_insert(extra, insert_idx, col, col, id, uri); if (run_merge_pass) { for (size_t i = 1; i < extra->uri_ranges.count; i++) { - struct row_uri_range *r1 = &extra->uri_ranges.v[i - 1]; - struct row_uri_range *r2 = &extra->uri_ranges.v[i]; + struct row_range *r1 = &extra->uri_ranges.v[i - 1]; + struct row_range *r2 = &extra->uri_ranges.v[i]; - if (r1->id == r2->id && r1->end + 1 == r2->start) { + if (r1->uri.id == r2->uri.id && r1->end + 1 == r2->start) { r1->end = r2->end; uri_range_delete(extra, i); i--; @@ -1219,8 +1247,8 @@ grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id) } out: - verify_no_overlapping_uris(extra); - verify_uris_are_sorted(extra); + verify_no_overlapping_ranges(extra); + verify_ranges_are_sorted(extra); } UNITTEST @@ -1233,7 +1261,7 @@ UNITTEST xassert(idx < row_data.uri_ranges.count); \ xassert(row_data.uri_ranges.v[idx].start == _start); \ xassert(row_data.uri_ranges.v[idx].end == _end); \ - xassert(row_data.uri_ranges.v[idx].id == _id); \ + xassert(row_data.uri_ranges.v[idx].uri.id == _id); \ } while (0) grid_row_uri_range_put(&row, 0, "http://foo.bar", 123); @@ -1298,7 +1326,7 @@ grid_row_uri_range_erase(struct row *row, int start, int end) /* Split up, or remove, URI ranges affected by the erase */ for (ssize_t i = (ssize_t)extra->uri_ranges.count - 1; i >= 0; i--) { - struct row_uri_range *old = &extra->uri_ranges.v[i]; + struct row_range *old = &extra->uri_ranges.v[i]; if (old->end < start) return; @@ -1314,7 +1342,7 @@ grid_row_uri_range_erase(struct row *row, int start, int end) else if (start > old->start && end < old->end) { /* Erase range erases a part in the middle of the URI */ uri_range_insert( - extra, i + 1, end + 1, old->end, old->id, old->uri); + extra, i + 1, end + 1, old->end, old->uri.id, old->uri.uri); /* The insertion may xrealloc() the vector, making our * 'old' pointer invalid */ @@ -1352,14 +1380,14 @@ UNITTEST xassert(row_data.uri_ranges.count == 2); xassert(row_data.uri_ranges.v[1].start == 11); xassert(row_data.uri_ranges.v[1].end == 20); - verify_no_overlapping_uris(&row_data); - verify_uris_are_sorted(&row_data); + verify_no_overlapping_ranges(&row_data); + verify_ranges_are_sorted(&row_data); /* Erase both URis */ grid_row_uri_range_erase(&row, 1, 20); xassert(row_data.uri_ranges.count == 0); - verify_no_overlapping_uris(&row_data); - verify_uris_are_sorted(&row_data); + verify_no_overlapping_ranges(&row_data); + verify_ranges_are_sorted(&row_data); /* Two URIs, then erase second half of the first, first half of the second */ @@ -1371,11 +1399,11 @@ UNITTEST xassert(row_data.uri_ranges.v[0].end == 4); xassert(row_data.uri_ranges.v[1].start == 16); xassert(row_data.uri_ranges.v[1].end == 20); - verify_no_overlapping_uris(&row_data); - verify_uris_are_sorted(&row_data); + verify_no_overlapping_ranges(&row_data); + verify_ranges_are_sorted(&row_data); - grid_row_uri_range_destroy(&row_data.uri_ranges.v[0]); - grid_row_uri_range_destroy(&row_data.uri_ranges.v[1]); + grid_row_range_destroy(&row_data.uri_ranges.v[0], ROW_RANGE_URI); + grid_row_range_destroy(&row_data.uri_ranges.v[1], ROW_RANGE_URI); row_data.uri_ranges.count = 0; /* One URI, erase middle part of it */ @@ -1386,11 +1414,11 @@ UNITTEST xassert(row_data.uri_ranges.v[0].end == 4); xassert(row_data.uri_ranges.v[1].start == 7); xassert(row_data.uri_ranges.v[1].end == 10); - verify_no_overlapping_uris(&row_data); - verify_uris_are_sorted(&row_data); + verify_no_overlapping_ranges(&row_data); + verify_ranges_are_sorted(&row_data); - grid_row_uri_range_destroy(&row_data.uri_ranges.v[0]); - grid_row_uri_range_destroy(&row_data.uri_ranges.v[1]); + grid_row_range_destroy(&row_data.uri_ranges.v[0], ROW_RANGE_URI); + grid_row_range_destroy(&row_data.uri_ranges.v[1], ROW_RANGE_URI); row_data.uri_ranges.count = 0; /* @@ -1416,7 +1444,6 @@ UNITTEST grid_row_uri_range_erase(&row, 5, 7); xassert(row_data.uri_ranges.count == 2); - for (size_t i = 0; i < row_data.uri_ranges.count; i++) - grid_row_uri_range_destroy(&row_data.uri_ranges.v[i]); + grid_row_ranges_destroy(&row_data.uri_ranges, ROW_RANGE_URI); free(row_data.uri_ranges.v); } diff --git a/grid.h b/grid.h index 8ea5200b..7c1f14be 100644 --- a/grid.h +++ b/grid.h @@ -89,9 +89,25 @@ void grid_row_uri_range_put( void grid_row_uri_range_erase(struct row *row, int start, int end); static inline void -grid_row_uri_range_destroy(struct row_uri_range *range) +grid_row_uri_range_destroy(struct row_range *range) { - free(range->uri); + free(range->uri.uri); +} + +static inline void +grid_row_range_destroy(struct row_range *range, enum row_range_type type) +{ + switch (type) { + case ROW_RANGE_URI: grid_row_uri_range_destroy(range); break; + } +} + +static inline void +grid_row_ranges_destroy(struct row_ranges *ranges, enum row_range_type type) +{ + for (int i = 0; i < ranges->count; i++) { + grid_row_range_destroy(&ranges->v[i], type); + } } static inline void diff --git a/terminal.h b/terminal.h index 5033c550..fc654702 100644 --- a/terminal.h +++ b/terminal.h @@ -99,19 +99,30 @@ struct damage { uint16_t lines; }; -struct row_uri_range { - int start; - int end; +struct uri_range_data { uint64_t id; char *uri; }; +struct row_range { + int start; + int end; + + union { + struct uri_range_data uri; + }; +}; + +struct row_ranges { + struct row_range *v; + int size; + int count; +}; + +enum row_range_type {ROW_RANGE_URI}; + struct row_data { - struct { - struct row_uri_range *v; - uint32_t size; - uint32_t count; - } uri_ranges; + struct row_ranges uri_ranges; }; struct row { diff --git a/url-mode.c b/url-mode.c index 9356a362..57f47dd0 100644 --- a/url-mode.c +++ b/url-mode.c @@ -509,7 +509,7 @@ osc8_uris(const struct terminal *term, enum url_action action, url_list_t *urls) continue; for (size_t i = 0; i < extra->uri_ranges.count; i++) { - const struct row_uri_range *range = &extra->uri_ranges.v[i]; + const struct row_range *range = &extra->uri_ranges.v[i]; struct coord start = { .col = range->start, @@ -522,8 +522,8 @@ osc8_uris(const struct terminal *term, enum url_action action, url_list_t *urls) tll_push_back( *urls, ((struct url){ - .id = range->id, - .url = xstrdup(range->uri), + .id = range->uri.id, + .url = xstrdup(range->uri.uri), .range = { .start = start, .end = end, From 32effc6657c1802cdf34098748337b0d7bc39dba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 23 Jun 2024 17:39:15 +0200 Subject: [PATCH 0740/1323] csi: wip: styled underlines This is work in progress, and fairly untested. This adds initial tracking of styled underlines. Setting attributes seems to work (both color and underline style). Grid reflow has *not* been tested. When rendering, style is currently ignored (all styles are rendered as a plain, legacy underline). Color however, *is* applied. --- csi.c | 58 ++++++++++- grid.c | 277 ++++++++++++++++++++++++++++++++++++++++++----------- grid.h | 15 ++- render.c | 39 +++++++- terminal.c | 22 ++++- terminal.h | 26 ++++- 6 files changed, 371 insertions(+), 66 deletions(-) diff --git a/csi.c b/csi.c index be470e70..b3583df5 100644 --- a/csi.c +++ b/csi.c @@ -32,7 +32,14 @@ static void sgr_reset(struct terminal *term) { + /* TODO: can we drop this check? */ + const enum curly_style curly_style = term->vt.curly.style; + memset(&term->vt.attrs, 0, sizeof(term->vt.attrs)); + memset(&term->vt.curly, 0, sizeof(term->vt.curly)); + + if (unlikely(curly_style > CURLY_SINGLE)) + term_update_ascii_printer(term); } static const char * @@ -88,7 +95,34 @@ csi_sgr(struct terminal *term) case 1: term->vt.attrs.bold = true; break; case 2: term->vt.attrs.dim = true; break; case 3: term->vt.attrs.italic = true; break; - case 4: term->vt.attrs.underline = true; break; + case 4: { + term->vt.attrs.underline = true; + term->vt.curly.style = CURLY_SINGLE; + + if (unlikely(term->vt.params.v[i].sub.idx == 1)) { + enum curly_style style = term->vt.params.v[i].sub.value[0]; + + switch (style) { + default: + case CURLY_NONE: + term->vt.attrs.underline = false; + term->vt.curly.style = CURLY_NONE; + break; + + case CURLY_SINGLE: + case CURLY_DOUBLE: + case CURLY_CURLY: + case CURLY_DOTTED: + case CURLY_DASHED: + term->vt.curly.style = style; break; + break; + } + + term_update_ascii_printer(term); + } + LOG_WARN("CURLY: %d", term->vt.curly.style); + break; + } case 5: term->vt.attrs.blink = true; break; case 6: LOG_WARN("ignored: rapid blink"); break; case 7: term->vt.attrs.reverse = true; break; @@ -98,7 +132,12 @@ csi_sgr(struct terminal *term) case 21: break; /* double-underline, not implemented */ case 22: term->vt.attrs.bold = term->vt.attrs.dim = false; break; case 23: term->vt.attrs.italic = false; break; - case 24: term->vt.attrs.underline = false; break; + case 24: { + term->vt.attrs.underline = false; + term->vt.curly.style = CURLY_NONE; + term_update_ascii_printer(term); + break; + } case 25: term->vt.attrs.blink = false; break; case 26: break; /* rapid blink, ignored */ case 27: term->vt.attrs.reverse = false; break; @@ -119,7 +158,8 @@ csi_sgr(struct terminal *term) break; case 38: - case 48: { + case 48: + case 58: { uint32_t color; enum color_source src; @@ -194,7 +234,11 @@ csi_sgr(struct terminal *term) break; } - if (param == 38) { + if (unlikely(param == 58)) { + term->vt.curly.color_src = src; + term->vt.curly.color = color; + term_update_ascii_printer(term); + } else if (param == 38) { term->vt.attrs.fg_src = src; term->vt.attrs.fg = color; } else { @@ -226,6 +270,12 @@ csi_sgr(struct terminal *term) term->vt.attrs.bg_src = COLOR_DEFAULT; break; + case 59: + term->vt.curly.color_src = COLOR_DEFAULT; + term->vt.curly.color = 0; + term_update_ascii_printer(term); + break; + /* Bright foreground colors */ case 90: case 91: diff --git a/grid.c b/grid.c index cbc00177..2eade784 100644 --- a/grid.c +++ b/grid.c @@ -105,6 +105,11 @@ verify_no_overlapping_ranges_of_type(const struct row_ranges *ranges, r1->uri.uri, r1->start, r1->end, r2->uri.uri, r2->start, r2->end); break; + + case ROW_RANGE_CURLY: + BUG("curly underline overlap: %d-%d, %d-%d", + r1->start, r1->end, r2->start, r2->end); + break; } } } @@ -116,6 +121,7 @@ static void verify_no_overlapping_ranges(const struct row_data *extra) { verify_no_overlapping_ranges_of_type(&extra->uri_ranges, ROW_RANGE_URI); + verify_no_overlapping_ranges_of_type(&extra->curly_ranges, ROW_RANGE_CURLY); } static void @@ -137,6 +143,12 @@ verify_ranges_of_type_are_sorted(const struct row_ranges *ranges, last->uri.uri, last->start, last->end, r->uri.uri, r->start, r->end); break; + + case ROW_RANGE_CURLY: + BUG("curly ranges not sorted correctly: " + "%d-%d came before %d-%d", + last->start, last->end, r->start, r->end); + break; } } } @@ -150,19 +162,37 @@ static void verify_ranges_are_sorted(const struct row_data *extra) { verify_ranges_of_type_are_sorted(&extra->uri_ranges, ROW_RANGE_URI); + verify_ranges_of_type_are_sorted(&extra->curly_ranges, ROW_RANGE_CURLY); } static void -uri_range_ensure_size(struct row_data *extra, uint32_t count_to_add) +range_ensure_size(struct row_ranges *ranges, int count_to_add) { - if (extra->uri_ranges.count + count_to_add > extra->uri_ranges.size) { - extra->uri_ranges.size = extra->uri_ranges.count + count_to_add; - extra->uri_ranges.v = xrealloc( - extra->uri_ranges.v, - extra->uri_ranges.size * sizeof(extra->uri_ranges.v[0])); + if (ranges->count + count_to_add > ranges->size) { + ranges->size = ranges->count + count_to_add; + ranges->v = xrealloc(ranges->v, ranges->size * sizeof(ranges->v[0])); } - xassert(extra->uri_ranges.count + count_to_add <= extra->uri_ranges.size); + xassert(ranges->count + count_to_add <= ranges->size); +} + +static void +range_insert(struct row_ranges *ranges, size_t idx, int start, int end) +{ + range_ensure_size(ranges, 1); + + xassert(idx <= ranges->count); + + const size_t move_count = ranges->count - idx; + memmove(&ranges->v[idx + 1], + &ranges->v[idx], + move_count * sizeof(ranges->v[0])); + + ranges->count++; + ranges->v[idx] = (struct row_range){ + .start = start, + .end = end, + }; } /* @@ -170,34 +200,27 @@ uri_range_ensure_size(struct row_data *extra, uint32_t count_to_add) * invalidating pointers into it. */ static void -uri_range_insert(struct row_data *extra, size_t idx, int start, int end, +uri_range_insert(struct row_ranges *ranges, size_t idx, int start, int end, uint64_t id, const char *uri) { - uri_range_ensure_size(extra, 1); + range_insert(ranges, idx, start, end); + ranges->v[idx].uri.id = id; + ranges->v[idx].uri.uri = xstrdup(uri); +} - xassert(idx <= extra->uri_ranges.count); - - const size_t move_count = extra->uri_ranges.count - idx; - memmove(&extra->uri_ranges.v[idx + 1], - &extra->uri_ranges.v[idx], - move_count * sizeof(extra->uri_ranges.v[0])); - - extra->uri_ranges.count++; - extra->uri_ranges.v[idx] = (struct row_range){ - .start = start, - .end = end, - .uri = { - .id = id, - .uri = xstrdup(uri), - }, - }; +static void +curly_range_insert(struct row_ranges *ranges, size_t idx, int start, int end, + struct curly_range_data data) +{ + range_insert(ranges, idx, start, end); + ranges->v[idx].curly = data; } static void uri_range_append_no_strdup(struct row_data *extra, int start, int end, uint64_t id, char *uri) { - uri_range_ensure_size(extra, 1); + range_ensure_size(&extra->uri_ranges, 1); extra->uri_ranges.v[extra->uri_ranges.count++] = (struct row_range){ .start = start, .end = end, @@ -216,16 +239,16 @@ uri_range_append(struct row_data *extra, int start, int end, uint64_t id, } static void -uri_range_delete(struct row_data *extra, size_t idx) +range_delete(struct row_ranges *ranges, enum row_range_type type, size_t idx) { - xassert(idx < extra->uri_ranges.count); - grid_row_uri_range_destroy(&extra->uri_ranges.v[idx]); + xassert(idx < ranges->count); + grid_row_range_destroy(&ranges->v[idx], type); - const size_t move_count = extra->uri_ranges.count - idx - 1; - memmove(&extra->uri_ranges.v[idx], - &extra->uri_ranges.v[idx + 1], - move_count * sizeof(extra->uri_ranges.v[0])); - extra->uri_ranges.count--; + const size_t move_count = ranges->count - idx - 1; + memmove(&ranges->v[idx], + &ranges->v[idx + 1], + move_count * sizeof(ranges->v[0])); + ranges->count--; } struct grid * @@ -269,14 +292,20 @@ grid_snapshot(const struct grid *grid) struct row_data *clone_extra = xcalloc(1, sizeof(*clone_extra)); clone_row->extra = clone_extra; - uri_range_ensure_size(clone_extra, extra->uri_ranges.count); + range_ensure_size(&clone_extra->uri_ranges, extra->uri_ranges.count); + range_ensure_size(&clone_extra->curly_ranges, extra->curly_ranges.count); - for (size_t i = 0; i < extra->uri_ranges.count; i++) { + for (int i = 0; i < extra->uri_ranges.count; i++) { const struct row_range *range = &extra->uri_ranges.v[i]; uri_range_append( clone_extra, range->start, range->end, range->uri.id, range->uri.uri); } + + for (int i = 0; i < extra->curly_ranges.count; i++) { + //const struct row_range *range = &extra->curly_ranges.v[i]; + BUG("TODO"); + } } else clone_row->extra = NULL; } @@ -490,9 +519,10 @@ grid_resize_without_reflow( ensure_row_has_extra_data(new_row); struct row_data *new_extra = new_row->extra; - uri_range_ensure_size(new_extra, old_extra->uri_ranges.count); + range_ensure_size(&new_extra->uri_ranges, old_extra->uri_ranges.count); + range_ensure_size(&new_extra->curly_ranges, old_extra->curly_ranges.count); - for (size_t i = 0; i < old_extra->uri_ranges.count; i++) { + for (int i = 0; i < old_extra->uri_ranges.count; i++) { const struct row_range *range = &old_extra->uri_ranges.v[i]; if (range->start >= new_cols) { @@ -504,7 +534,21 @@ grid_resize_without_reflow( const int end = min(range->end, new_cols - 1); uri_range_append(new_extra, start, end, range->uri.id, range->uri.uri); } - } + + for (int i = 0; i < old_extra->uri_ranges.count; i++) { + const struct row_range *range = &old_extra->uri_ranges.v[i]; + + if (range->start >= new_cols) { + /* The whole range is truncated */ + continue; + } + + //const int start = range->start; + //const int end = min(range->end, new_cols - 1); + //uri_range_append(new_extra, start, end, range->uri.id, range->uri.uri); + BUG("TODO"); + } +} /* Clear "new" lines */ for (int r = min(old_screen_rows, new_screen_rows); r < new_screen_rows; r++) { @@ -1161,7 +1205,7 @@ grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id) bool run_merge_pass = false; struct row_data *extra = row->extra; - for (ssize_t i = (ssize_t)extra->uri_ranges.count - 1; i >= 0; i--) { + for (int i = extra->uri_ranges.count - 1; i >= 0; i--) { struct row_range *r = &extra->uri_ranges.v[i]; const bool matching_id = r->uri.id == id; @@ -1203,7 +1247,8 @@ grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id) xassert(r->start < col); xassert(r->end > col); - uri_range_insert(extra, i + 1, col + 1, r->end, r->uri.id, r->uri.uri); + uri_range_insert( + &extra->uri_ranges, i + 1, col + 1, r->end, r->uri.id, r->uri.uri); /* The insertion may xrealloc() the vector, making our * 'old' pointer invalid */ @@ -1231,7 +1276,7 @@ grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id) }, }; } else - uri_range_insert(extra, insert_idx, col, col, id, uri); + uri_range_insert(&extra->uri_ranges, insert_idx, col, col, id, uri); if (run_merge_pass) { for (size_t i = 1; i < extra->uri_ranges.count; i++) { @@ -1240,7 +1285,111 @@ grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id) if (r1->uri.id == r2->uri.id && r1->end + 1 == r2->start) { r1->end = r2->end; - uri_range_delete(extra, i); + range_delete(&extra->uri_ranges, ROW_RANGE_URI, i); + i--; + } + } + } + +out: + verify_no_overlapping_ranges(extra); + verify_ranges_are_sorted(extra); +} + +void +grid_row_curly_range_put(struct row *row, int col, struct curly_range_data data) +{ + ensure_row_has_extra_data(row); + + size_t insert_idx = 0; + bool replace = false; + bool run_merge_pass = false; + + struct row_data *extra = row->extra; + for (int i = extra->curly_ranges.count - 1; i >= 0; i--) { + struct row_range *r = &extra->curly_ranges.v[i]; + + const bool matching = r->curly.style == data.style && + r->curly.color_src == data.color_src && + r->curly.color == data.color; + + if (matching && r->end + 1 == col) { + /* Extend existing curly tail */ + r->end++; + goto out; + } + + else if (r->end < col) { + insert_idx = i + 1; + break; + } + + else if (r->start > col) + continue; + + else { + xassert(r->start <= col); + xassert(r->end >= col); + + if (matching) + goto out; + + if (r->start == r->end) { + replace = true; + run_merge_pass = true; + insert_idx = i; + } else if (r->start == col) { + run_merge_pass = true; + r->start++; + insert_idx = i; + } else if (r->end == col) { + run_merge_pass = true; + r->end--; + insert_idx = i + 1; + } else { + xassert(r->start < col); + xassert(r->end > col); + + curly_range_insert( + &extra->curly_ranges, i + 1, col + 1, r->end, data); + + /* The insertion may xrealloc() the vector, making our + * 'old' pointer invalid */ + r = &extra->curly_ranges.v[i]; + r->end = col - 1; + xassert(r->start <= r->end); + + insert_idx = i + 1; + } + + break; + } + } + + xassert(insert_idx <= extra->curly_ranges.count); + + if (replace) { + grid_row_curly_range_destroy(&extra->curly_ranges.v[insert_idx]); + extra->curly_ranges.v[insert_idx] = (struct row_range){ + .start = col, + .end = col, + .curly = data, + }; + } else + curly_range_insert(&extra->curly_ranges, insert_idx, col, col, data); + + if (run_merge_pass) { + for (size_t i = 1; i < extra->curly_ranges.count; i++) { + struct row_range *r1 = &extra->curly_ranges.v[i - 1]; + struct row_range *r2 = &extra->curly_ranges.v[i]; + + if (r1->curly.style == r2->curly.style && + r1->curly.color_src == r2->curly.color_src && + r1->curly.color == r2->curly.color && + r1->end + 1 == r2->start) + { + r1->end = r2->end; + range_delete(&extra->curly_ranges, ROW_RANGE_CURLY, i); i--; } } @@ -1316,17 +1465,15 @@ UNITTEST #undef verify_range } -void -grid_row_uri_range_erase(struct row *row, int start, int end) +static void +grid_row_range_erase(struct row_ranges *ranges, enum row_range_type type, + int start, int end) { - xassert(row->extra != NULL); xassert(start <= end); - struct row_data *extra = row->extra; - /* Split up, or remove, URI ranges affected by the erase */ - for (ssize_t i = (ssize_t)extra->uri_ranges.count - 1; i >= 0; i--) { - struct row_range *old = &extra->uri_ranges.v[i]; + for (int i = ranges->count - 1; i >= 0; i--) { + struct row_range *old = &ranges->v[i]; if (old->end < start) return; @@ -1336,17 +1483,25 @@ grid_row_uri_range_erase(struct row *row, int start, int end) if (start <= old->start && end >= old->end) { /* Erase range covers URI completely - remove it */ - uri_range_delete(extra, i); + range_delete(ranges, type, i); } else if (start > old->start && end < old->end) { /* Erase range erases a part in the middle of the URI */ - uri_range_insert( - extra, i + 1, end + 1, old->end, old->uri.id, old->uri.uri); + switch (type) { + case ROW_RANGE_URI: + uri_range_insert( + ranges, i + 1, end + 1, old->end, old->uri.id, old->uri.uri); + break; + + case ROW_RANGE_CURLY: + curly_range_insert(ranges, i + 1, end + 1, old->end, old->curly); + break; + } /* The insertion may xrealloc() the vector, making our * 'old' pointer invalid */ - old = &extra->uri_ranges.v[i]; + old = &ranges->v[i]; old->end = start - 1; return; /* There can be no more URIs affected by the erase range */ } @@ -1366,6 +1521,20 @@ grid_row_uri_range_erase(struct row *row, int start, int end) } } +void +grid_row_uri_range_erase(struct row *row, int start, int end) +{ + xassert(row->extra != NULL); + grid_row_range_erase(&row->extra->uri_ranges, ROW_RANGE_URI, start, end); +} + +void +grid_row_curly_range_erase(struct row *row, int start, int end) +{ + xassert(row->extra != NULL); + grid_row_range_erase(&row->extra->curly_ranges, ROW_RANGE_CURLY, start, end); +} + UNITTEST { struct row_data row_data = {.uri_ranges = {0}}; diff --git a/grid.h b/grid.h index 7c1f14be..c5c2b60c 100644 --- a/grid.h +++ b/grid.h @@ -88,17 +88,27 @@ void grid_row_uri_range_put( struct row *row, int col, const char *uri, uint64_t id); void grid_row_uri_range_erase(struct row *row, int start, int end); +void grid_row_curly_range_put( + struct row *row, int col, struct curly_range_data data); +void grid_row_curly_range_erase(struct row *row, int start, int end); + static inline void grid_row_uri_range_destroy(struct row_range *range) { free(range->uri.uri); } +static inline void +grid_row_curly_range_destroy(struct row_range *range) +{ +} + static inline void grid_row_range_destroy(struct row_range *range, enum row_range_type type) { switch (type) { case ROW_RANGE_URI: grid_row_uri_range_destroy(range); break; + case ROW_RANGE_CURLY: grid_row_curly_range_destroy(range); break; } } @@ -118,9 +128,10 @@ grid_row_reset_extra(struct row *row) if (likely(extra == NULL)) return; - for (size_t i = 0; i < extra->uri_ranges.count; i++) - grid_row_uri_range_destroy(&extra->uri_ranges.v[i]); + grid_row_ranges_destroy(&extra->uri_ranges, ROW_RANGE_URI); + grid_row_ranges_destroy(&extra->curly_ranges, ROW_RANGE_CURLY); free(extra->uri_ranges.v); + free(extra->curly_ranges.v); free(extra); row->extra = NULL; diff --git a/render.c b/render.c index fd7e743a..78cc5325 100644 --- a/render.c +++ b/render.c @@ -847,8 +847,43 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag pixman_image_unref(clr_pix); /* Underline */ - if (cell->attrs.underline) - draw_underline(term, pix, font, &fg, x, y, cell_cols); + if (cell->attrs.underline) { + pixman_color_t underline_color = fg; + + /* Check if cell has a styled underline. This lookup is fairly + expensive... */ + if (row->extra != NULL) { + for (int i = 0; i < row->extra->curly_ranges.count; i++) { + const struct row_range *range = &row->extra->curly_ranges.v[i]; + + if (range->start <= col && col <= range->end) { + switch (range->curly.color_src) { + case COLOR_BASE256: + underline_color = color_hex_to_pixman( + term->colors.table[range->curly.color]); + break; + + case COLOR_RGB: + underline_color = + color_hex_to_pixman(range->curly.color); + break; + + case COLOR_DEFAULT: + break; + + case COLOR_BASE16: + BUG("underline color can't be base-16"); + break; + } + + break; + } + } + } + + draw_underline(term, pix, font, &underline_color, x, y, cell_cols); + + } if (cell->attrs.strikethrough) draw_strikeout(term, pix, font, &fg, x, y, cell_cols); diff --git a/terminal.c b/terminal.c index 55c387c1..48c0b298 100644 --- a/terminal.c +++ b/terminal.c @@ -1940,8 +1940,10 @@ erase_cell_range(struct terminal *term, struct row *row, int start, int end) } else memset(&row->cells[start], 0, (end - start + 1) * sizeof(row->cells[0])); - if (unlikely(row->extra != NULL)) + if (unlikely(row->extra != NULL)) { grid_row_uri_range_erase(row, start, end); + grid_row_curly_range_erase(row, start, end); + } } static inline void @@ -3621,6 +3623,7 @@ term_fill(struct terminal *term, int r, int c, uint8_t data, size_t count, cell->wc = data; cell->attrs = attrs; + /* TODO: why do we print the URI here, and then erase it below? */ if (unlikely(term->vt.osc8.uri != NULL)) { grid_row_uri_range_put(row, c, term->vt.osc8.uri, term->vt.osc8.id); @@ -3635,8 +3638,10 @@ term_fill(struct terminal *term, int r, int c, uint8_t data, size_t count, } } - if (unlikely(row->extra != NULL)) + if (unlikely(row->extra != NULL)) { grid_row_uri_range_erase(row, c, c + count - 1); + grid_row_curly_range_erase(row, c, c + count - 1); + } } void @@ -3706,6 +3711,13 @@ term_print(struct terminal *term, char32_t wc, int width) } else if (row->extra != NULL) grid_row_uri_range_erase(row, col, col + width - 1); + if (unlikely(term->vt.curly.style > CURLY_SINGLE || + term->vt.curly.color_src != COLOR_DEFAULT)) + { + grid_row_curly_range_put(row, col, term->vt.curly); + } else if (row->extra != NULL) + grid_row_curly_range_erase(row, col, col + width - 1); + /* Advance cursor the 'additional' columns while dirty:ing the cells */ for (int i = 1; i < width && (col + 1) < term->cols; i++) { col++; @@ -3763,8 +3775,10 @@ ascii_printer_fast(struct terminal *term, char32_t wc) grid->cursor.point.col = col; - if (unlikely(row->extra != NULL)) + if (unlikely(row->extra != NULL)) { grid_row_uri_range_erase(row, uri_start, uri_start); + grid_row_curly_range_erase(row, uri_start, uri_start); + } } static void @@ -3781,6 +3795,8 @@ term_update_ascii_printer(struct terminal *term) void (*new_printer)(struct terminal *term, char32_t wc) = unlikely(tll_length(term->grid->sixel_images) > 0 || term->vt.osc8.uri != NULL || + term->vt.curly.style > CURLY_SINGLE || + term->vt.curly.color_src != COLOR_DEFAULT || term->charsets.set[term->charsets.selected] == CHARSET_GRAPHIC || term->insert_mode) ? &ascii_printer_generic diff --git a/terminal.h b/terminal.h index fc654702..7f6814d3 100644 --- a/terminal.h +++ b/terminal.h @@ -104,12 +104,33 @@ struct uri_range_data { char *uri; }; +enum curly_style { + CURLY_NONE, + CURLY_SINGLE, /* Legacy underline */ + CURLY_DOUBLE, + CURLY_CURLY, + CURLY_DOTTED, + CURLY_DASHED, +}; + +struct curly_range_data { + enum curly_style style; + enum color_source color_src; + uint32_t color; +}; + +union row_range_data { + struct uri_range_data uri; + struct curly_range_data curly; +}; + struct row_range { int start; int end; union { struct uri_range_data uri; + struct curly_range_data curly; }; }; @@ -119,10 +140,11 @@ struct row_ranges { int count; }; -enum row_range_type {ROW_RANGE_URI}; +enum row_range_type {ROW_RANGE_URI, ROW_RANGE_CURLY}; struct row_data { struct row_ranges uri_ranges; + struct row_ranges curly_ranges; }; struct row { @@ -271,6 +293,8 @@ struct vt { char *uri; } osc8; + struct curly_range_data curly; + struct { uint8_t *data; size_t size; From 05f97744164985542b2534c02b817b3ff50f364f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 23 Jun 2024 18:53:51 +0200 Subject: [PATCH 0741/1323] foot.info: add smulx (styled underlines) --- foot.info | 1 + 1 file changed, 1 insertion(+) diff --git a/foot.info b/foot.info index cf5c7b82..e88f07c2 100644 --- a/foot.info +++ b/foot.info @@ -40,6 +40,7 @@ RV=\E[>c, Rect=\E[%p1%d;%p2%d;%p3%d;%p4%d;%p5%d$x, Se=\E[ q, + Smulx=\E[4:%p1%dm, Ss=\E[%p1%d q, Sync=\E[?2026%?%p1%{1}%-%tl%eh%;, TS=\E]2;, From a45ccfaed01b3c2bf52dd436a24b93a27a7c0742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 23 Jun 2024 18:55:29 +0200 Subject: [PATCH 0742/1323] csi: remove debug log --- csi.c | 1 - 1 file changed, 1 deletion(-) diff --git a/csi.c b/csi.c index b3583df5..fe06a31f 100644 --- a/csi.c +++ b/csi.c @@ -120,7 +120,6 @@ csi_sgr(struct terminal *term) term_update_ascii_printer(term); } - LOG_WARN("CURLY: %d", term->vt.curly.style); break; } case 5: term->vt.attrs.blink = true; break; From 8e2402605ec81358cd18c657c9698044598b72f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 23 Jun 2024 18:55:37 +0200 Subject: [PATCH 0743/1323] render: styled underlines This was originally contributed by @kraftwerk28 in https://codeberg.org/dnkl/foot/pulls/1099 Here, we re-use the rendering logic only, as attribute tracking has been completely rewritten. --- render.c | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 1 deletion(-) diff --git a/render.c b/render.c index 78cc5325..471ca467 100644 --- a/render.c +++ b/render.c @@ -384,6 +384,120 @@ draw_underline(const struct terminal *term, pixman_image_t *pix, x, y + y_ofs, cols * term->cell_width, thickness}); } +static void +draw_styled_underline(const struct terminal *term, pixman_image_t *pix, + const struct fcft_font *font, + const pixman_color_t *color, + enum curly_style style, int x, int y, int cols) +{ + xassert(style != CURLY_NONE); + + const int thickness = font->underline.thickness; + + int y_ofs; + + /* Make sure the line isn't positioned below the cell */ + switch (style) { + case CURLY_DOUBLE: + case CURLY_CURLY: + y_ofs = min(underline_offset(term, font), + term->cell_height - thickness * 3); + break; + + default: + y_ofs = min(underline_offset(term, font), + term->cell_height - thickness); + break; + } + + const int ceil_w = cols * term->cell_width; + + switch (style) { + case CURLY_DOUBLE: { + const pixman_rectangle16_t rects[] = { + {x, y + y_ofs, ceil_w, thickness}, + {x, y + y_ofs + thickness * 2, ceil_w, thickness}}; + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 2, rects); + break; + } + + case CURLY_DASHED: { + const int ceil_w = cols * term->cell_width; + const int dash_w = ceil_w / 3 + (ceil_w % 3 > 0); + const pixman_rectangle16_t rects[] = { + {x, y + y_ofs, dash_w, thickness}, + {x + dash_w * 2, y + y_ofs, dash_w, thickness}, + }; + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, color, 2, rects); + break; + } + case CURLY_DOTTED: { + const int ceil_w = cols * term->cell_width; + const int nrects = min(ceil_w / thickness / 2, 16); + pixman_rectangle16_t rects[16] = {0}; + + for (int i = 0; i < nrects; i++) { + rects[i] = (pixman_rectangle16_t){ + x + i * thickness * 2, y + y_ofs, thickness, thickness}; + } + + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, nrects, rects); + break; + } + + case CURLY_CURLY: { + const int top = y + y_ofs; + const int bot = top + thickness * 3; + const int half_x = x + ceil_w / 2.0, full_x = x + ceil_w; + + const double bt_2 = (bot - top) * (bot - top); + const double th_2 = thickness * thickness; + const double hx_2 = ceil_w * ceil_w / 4.0; + const int th = round(sqrt(th_2 + (th_2 * bt_2 / hx_2)) / 2.); + + #define I(x) pixman_int_to_fixed(x) + const pixman_trapezoid_t traps[] = { +#if 0 /* characters sit within the "dips" of the curlies */ + { + I(top), I(bot), + {{I(x), I(top + th)}, {I(half_x), I(bot + th)}}, + {{I(x), I(top - th)}, {I(half_x), I(bot - th)}}, + }, + { + I(top), I(bot), + {{I(half_x), I(bot - th)}, {I(full_x), I(top - th)}}, + {{I(half_x), I(bot + th)}, {I(full_x), I(top + th)}}, + } +#else /* characters sit on top of the curlies */ + { + I(top), I(bot), + {{I(x), I(bot - th)}, {I(half_x), I(top - th)}}, + {{I(x), I(bot + th)}, {I(half_x), I(top + th)}}, + }, + { + I(top), I(bot), + {{I(half_x), I(top + th)}, {I(full_x), I(bot + th)}}, + {{I(half_x), I(top - th)}, {I(full_x), I(bot - th)}}, + } +#endif + }; + + pixman_image_t *fill = pixman_image_create_solid_fill(color); + pixman_composite_trapezoids( + PIXMAN_OP_OVER, fill, pix, PIXMAN_a8, 0, 0, 0, 0, + sizeof(traps) / sizeof(traps[0]), traps); + + pixman_image_unref(fill); + break; + } + + default: + draw_underline(term, pix, font, color, x, y, cols); + break; + } +} + static void draw_strikeout(const struct terminal *term, pixman_image_t *pix, const struct fcft_font *font, @@ -849,6 +963,7 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag /* Underline */ if (cell->attrs.underline) { pixman_color_t underline_color = fg; + enum curly_style underline_style = CURLY_SINGLE; /* Check if cell has a styled underline. This lookup is fairly expensive... */ @@ -876,12 +991,14 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag break; } + underline_style = range->curly.style; break; } } } - draw_underline(term, pix, font, &underline_color, x, y, cell_cols); + draw_styled_underline( + term, pix, font, &underline_color, underline_style, x, y, cell_cols); } From b20302c2a7bcb70844f04c122c640b88be45ff60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 24 Jun 2024 00:57:03 +0200 Subject: [PATCH 0744/1323] grid: reflow: handle styled underlines --- grid.c | 166 ++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 128 insertions(+), 38 deletions(-) diff --git a/grid.c b/grid.c index 2eade784..3cd818a7 100644 --- a/grid.c +++ b/grid.c @@ -238,6 +238,18 @@ uri_range_append(struct row_data *extra, int start, int end, uint64_t id, uri_range_append_no_strdup(extra, start, end, id, xstrdup(uri)); } +static void +curly_range_append(struct row_data *extra, int start, int end, + struct curly_range_data data) +{ + range_ensure_size(&extra->curly_ranges, 1); + extra->curly_ranges.v[extra->curly_ranges.count++] = (struct row_range){ + .start = start, + .end = end, + .curly = data, + }; +} + static void range_delete(struct row_ranges *ranges, enum row_range_type type, size_t idx) { @@ -303,8 +315,8 @@ grid_snapshot(const struct grid *grid) } for (int i = 0; i < extra->curly_ranges.count; i++) { - //const struct row_range *range = &extra->curly_ranges.v[i]; - BUG("TODO"); + const struct row_range *range = &extra->curly_ranges.v[i]; + curly_range_append(clone_extra, range->start, range->end, range->curly); } } else clone_row->extra = NULL; @@ -535,18 +547,17 @@ grid_resize_without_reflow( uri_range_append(new_extra, start, end, range->uri.id, range->uri.uri); } - for (int i = 0; i < old_extra->uri_ranges.count; i++) { - const struct row_range *range = &old_extra->uri_ranges.v[i]; + for (int i = 0; i < old_extra->curly_ranges.count; i++) { + const struct row_range *range = &old_extra->curly_ranges.v[i]; if (range->start >= new_cols) { /* The whole range is truncated */ continue; } - //const int start = range->start; - //const int end = min(range->end, new_cols - 1); - //uri_range_append(new_extra, start, end, range->uri.id, range->uri.uri); - BUG("TODO"); + const int start = range->start; + const int end = min(range->end, new_cols - 1); + curly_range_append(new_extra, start, end, range->curly); } } @@ -623,8 +634,8 @@ reflow_uri_range_start(struct row_range *range, struct row *new_row, int new_col_idx) { ensure_row_has_extra_data(new_row); - uri_range_append_no_strdup - (new_row->extra, new_col_idx, -1, range->uri.id, range->uri.uri); + uri_range_append_no_strdup( + new_row->extra, new_col_idx, -1, range->uri.id, range->uri.uri); range->uri.uri = NULL; } @@ -643,6 +654,33 @@ reflow_uri_range_end(struct row_range *range, struct row *new_row, new_range->end = new_col_idx; } +static void +reflow_curly_range_start(struct row_range *range, struct row *new_row, + int new_col_idx) +{ + ensure_row_has_extra_data(new_row); + curly_range_append(new_row->extra, new_col_idx, -1, range->curly); +} + + +static void +reflow_curly_range_end(struct row_range *range, struct row *new_row, + int new_col_idx) +{ + struct row_data *extra = new_row->extra; + xassert(extra->curly_ranges.count > 0); + + struct row_range *new_range = + &extra->curly_ranges.v[extra->curly_ranges.count - 1]; + + xassert(new_range->curly.style == range->curly.style); + xassert(new_range->curly.color_src == range->curly.color_src); + xassert(new_range->curly.color == range->curly.color); + + xassert(new_range->end < 0); + new_range->end = new_col_idx; +} + static struct row * _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, int *row_idx, int *col_idx, int row_count, int col_count) @@ -703,6 +741,21 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, } } + if (extra->curly_ranges.count > 0) { + struct row_range *range = + &extra->curly_ranges.v[extra->curly_ranges.count - 1]; + + if (range->end < 0) { + + /* Terminate URI range on the previous row */ + range->end = col_count - 1; + + /* Open a new range on the new/current row */ + ensure_row_has_extra_data(new_row); + curly_range_append(new_row->extra, 0, -1, range->curly); + } + } + return new_row; } @@ -887,12 +940,13 @@ grid_resize_and_reflow( tp = NULL; /* Does this row have any URIs? */ - struct row_range *range, *range_terminator; + struct row_range *uri_range, *uri_range_terminator; + struct row_range *curly_range, *curly_range_terminator; struct row_data *extra = old_row->extra; if (extra != NULL && extra->uri_ranges.count > 0) { - range = &extra->uri_ranges.v[0]; - range_terminator = &extra->uri_ranges.v[extra->uri_ranges.count]; + uri_range = &extra->uri_ranges.v[0]; + uri_range_terminator = &extra->uri_ranges.v[extra->uri_ranges.count]; /* Make sure the *last* URI range's end point is included * in the copy */ @@ -900,18 +954,32 @@ grid_resize_and_reflow( &extra->uri_ranges.v[extra->uri_ranges.count - 1]; col_count = max(col_count, last_on_row->end + 1); } else - range = range_terminator = NULL; + uri_range = uri_range_terminator = NULL; + + if (extra != NULL && extra->curly_ranges.count > 0) { + curly_range = &extra->curly_ranges.v[0]; + curly_range_terminator = &extra->curly_ranges.v[extra->curly_ranges.count]; + + const struct row_range *last_on_row = + &extra->curly_ranges.v[extra->curly_ranges.count - 1]; + col_count = max(col_count, last_on_row->end + 1); + } else + curly_range = curly_range_terminator = NULL; for (int start = 0, left = col_count; left > 0;) { int end; bool tp_break = false; bool uri_break = false; + bool curly_break = false; bool ftcs_break = false; /* Figure out where to end this chunk */ { - const int uri_col = range != range_terminator - ? ((range->start >= start ? range->start : range->end) + 1) + const int uri_col = uri_range != uri_range_terminator + ? ((uri_range->start >= start ? uri_range->start : uri_range->end) + 1) + : INT_MAX; + const int curly_col = curly_range != curly_range_terminator + ? ((curly_range->start >= start ? curly_range->start : curly_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 @@ -920,9 +988,10 @@ grid_resize_and_reflow( ? old_row->shell_integration.cmd_end + 1 : INT_MAX; - end = min(col_count, min(min(tp_col, uri_col), ftcs_col)); + end = min(col_count, min(min(tp_col, min(uri_col, curly_col)), ftcs_col)); uri_break = end == uri_col; + curly_break = end == curly_col; tp_break = end == tp_col; ftcs_break = end == ftcs_col; } @@ -1033,15 +1102,28 @@ grid_resize_and_reflow( } if (uri_break) { - xassert(range != NULL); + xassert(uri_range != NULL); - if (range->start == end - 1) - reflow_uri_range_start(range, new_row, new_col_idx - 1); + if (uri_range->start == end - 1) + reflow_uri_range_start(uri_range, new_row, new_col_idx - 1); - if (range->end == end - 1) { - reflow_uri_range_end(range, new_row, new_col_idx - 1); - grid_row_uri_range_destroy(range); - range++; + if (uri_range->end == end - 1) { + reflow_uri_range_end(uri_range, new_row, new_col_idx - 1); + grid_row_uri_range_destroy(uri_range); + uri_range++; + } + } + + if (curly_break) { + xassert(curly_range != NULL); + + if (curly_range->start == end - 1) + reflow_curly_range_start(curly_range, new_row, new_col_idx - 1); + + if (curly_range->end == end - 1) { + reflow_curly_range_end(curly_range, new_row, new_col_idx - 1); + grid_row_curly_range_destroy(curly_range); + curly_range++; } } @@ -1067,20 +1149,26 @@ grid_resize_and_reflow( if (r + 1 < old_rows) line_wrap(); - else if (new_row->extra != NULL && - new_row->extra->uri_ranges.count > 0) - { - /* - * line_wrap() "closes" still-open URIs. Since this is - * the *last* row, and since we're line-breaking due - * to a hard line-break (rather than running out of - * cells in the "new_row"), there shouldn't be an open - * URI (it would have been closed when we reached the - * end of the URI while reflowing the last "old" - * row). - */ - uint32_t last_idx = new_row->extra->uri_ranges.count - 1; - xassert(new_row->extra->uri_ranges.v[last_idx].end >= 0); + else if (new_row->extra != NULL) { + if (new_row->extra->uri_ranges.count > 0) { + /* + * line_wrap() "closes" still-open URIs. Since + * this is the *last* row, and since we're + * line-breaking due to a hard line-break (rather + * than running out of cells in the "new_row"), + * there shouldn't be an open URI (it would have + * been closed when we reached the end of the URI + * while reflowing the last "old" row). + */ + int last_idx = new_row->extra->uri_ranges.count - 1; + xassert(new_row->extra->uri_ranges.v[last_idx].end >= 0); + } + + if (new_row->extra->curly_ranges.count > 0) { + int last_idx = new_row->extra->curly_ranges.count - 1; + xassert(new_row->extra->curly_ranges.v[last_idx].end >= 0); + + } } } @@ -1112,6 +1200,8 @@ grid_resize_and_reflow( for (size_t i = 0; i < row->extra->uri_ranges.count; i++) xassert(row->extra->uri_ranges.v[i].end >= 0); + for (size_t i = 0; i < row->extra->curly_ranges.count; i++) + xassert(row->extra->curly_ranges.v[i].end >= 0); verify_no_overlapping_ranges(row->extra); verify_ranges_are_sorted(row->extra); From 963ce45f3f29bccf7ca5aa0dc8e258a11cfbc552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 24 Jun 2024 00:57:24 +0200 Subject: [PATCH 0745/1323] render: resize: copy styled underlines to temporary grid When doing an interactive resize, we create a small grid copy of the current viewport, and then do a non-reflow resize. When the interactive resize is done, we do a proper reflow. This is for performance reasons. When creating the viewport copy, we also need to copy the styled underlines. Otherwise, styled underlines will be rendered as plain underlines *while resizing*. --- render.c | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/render.c b/render.c index 471ca467..b9a597a9 100644 --- a/render.c +++ b/render.c @@ -4395,6 +4395,29 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) memcpy(g.rows[i]->cells, orig->rows[j]->cells, g.num_cols * sizeof(g.rows[i]->cells[0])); + + if (orig->rows[j]->extra == NULL || + orig->rows[j]->extra->curly_ranges.count == 0) + { + continue; + } + + /* + * Copy undercurly ranges + */ + + const struct row_ranges *curly_src = &orig->rows[j]->extra->curly_ranges; + + const int count = curly_src->count; + g.rows[i]->extra = xcalloc(1, sizeof(*g.rows[i]->extra)); + g.rows[i]->extra->curly_ranges.v = xmalloc( + count * sizeof(g.rows[i]->extra->curly_ranges.v[0])); + + struct row_ranges *curly_dst = &g.rows[i]->extra->curly_ranges; + curly_dst->count = curly_dst->size = count; + + for (int k = 0; k < count; k++) + curly_dst->v[k] = curly_src->v[k]; } term->normal = g; From 3b738c6e683702f25ad8101a686db5917de8e0e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 24 Jun 2024 01:07:17 +0200 Subject: [PATCH 0746/1323] terminal: term_fill(): fix osc8 erase bug + handle styled underlines Only clear OSC-8 hyperlinks at the target columns if we don't have an active OSC-8 URI. This corresponds to normal VT attributes; the currently active attributes are set, and all others are cleared. Handle styled underlines in the same way --- terminal.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/terminal.c b/terminal.c index 48c0b298..771e4878 100644 --- a/terminal.c +++ b/terminal.c @@ -3636,11 +3636,25 @@ term_fill(struct terminal *term, int r, int c, uint8_t data, size_t count, break; } } + + if (unlikely(term->vt.curly.style > CURLY_SINGLE || + term->vt.curly.color_src != COLOR_DEFAULT)) + { + grid_row_curly_range_put(row, c, term->vt.curly); + } } if (unlikely(row->extra != NULL)) { - grid_row_uri_range_erase(row, c, c + count - 1); - grid_row_curly_range_erase(row, c, c + count - 1); + if (likely(term->vt.osc8.uri != NULL)) + grid_row_uri_range_erase(row, c, c + count - 1); + + if (likely(term->vt.curly.style <= CURLY_SINGLE && + term->vt.curly.color_src == COLOR_DEFAULT)) + { + /* No extended/styled underlines active, so erase any such + attributes at the target columns */ + grid_row_curly_range_erase(row, c, c + count - 1); + } } } From 22302d8bccaa76aef9b9dca97ebc35b4ed37afff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 24 Jun 2024 01:09:24 +0200 Subject: [PATCH 0747/1323] term: term_fill(): only set OSC-8 + styled hyperlinks when use_sgr_attrs is set --- terminal.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/terminal.c b/terminal.c index 771e4878..ecd55d77 100644 --- a/terminal.c +++ b/terminal.c @@ -3624,7 +3624,7 @@ term_fill(struct terminal *term, int r, int c, uint8_t data, size_t count, cell->attrs = attrs; /* TODO: why do we print the URI here, and then erase it below? */ - if (unlikely(term->vt.osc8.uri != NULL)) { + if (unlikely(use_sgr_attrs && term->vt.osc8.uri != NULL)) { grid_row_uri_range_put(row, c, term->vt.osc8.uri, term->vt.osc8.id); switch (term->conf->url.osc8_underline) { @@ -3637,8 +3637,9 @@ term_fill(struct terminal *term, int r, int c, uint8_t data, size_t count, } } - if (unlikely(term->vt.curly.style > CURLY_SINGLE || - term->vt.curly.color_src != COLOR_DEFAULT)) + if (unlikely(use_sgr_attrs && + (term->vt.curly.style > CURLY_SINGLE || + term->vt.curly.color_src != COLOR_DEFAULT))) { grid_row_curly_range_put(row, c, term->vt.curly); } From 48cf57818d354625fd1820ee82e31b8bb3227202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 24 Jun 2024 01:26:57 +0200 Subject: [PATCH 0748/1323] term: performance: use a bitfield to track which ascii printer to use The things affecting which ASCII printer we use have grown... Instead of checking everything inside term_update_ascii_printer(), use a bitfield. Anything affecting the printer used, must now set a bit in this bitfield. This makes term_update_ascii_printer() much faster, since all it needs to do is check if the bitfield is zero or not. --- csi.c | 19 ++++++++++++++----- sixel.c | 11 +++++++++++ terminal.c | 23 +++++++++++++++-------- terminal.h | 11 +++++++++++ vt.c | 12 ++++++++++++ 5 files changed, 63 insertions(+), 13 deletions(-) diff --git a/csi.c b/csi.c index fe06a31f..387c37d9 100644 --- a/csi.c +++ b/csi.c @@ -33,13 +33,12 @@ static void sgr_reset(struct terminal *term) { /* TODO: can we drop this check? */ - const enum curly_style curly_style = term->vt.curly.style; - memset(&term->vt.attrs, 0, sizeof(term->vt.attrs)); memset(&term->vt.curly, 0, sizeof(term->vt.curly)); - if (unlikely(curly_style > CURLY_SINGLE)) - term_update_ascii_printer(term); + term->bits_affecting_ascii_printer.curly_style = false; + term->bits_affecting_ascii_printer.curly_color = false; + term_update_ascii_printer(term); } static const char * @@ -107,6 +106,7 @@ csi_sgr(struct terminal *term) case CURLY_NONE: term->vt.attrs.underline = false; term->vt.curly.style = CURLY_NONE; + term->bits_affecting_ascii_printer.curly_style = false; break; case CURLY_SINGLE: @@ -114,7 +114,9 @@ csi_sgr(struct terminal *term) case CURLY_CURLY: case CURLY_DOTTED: case CURLY_DASHED: - term->vt.curly.style = style; break; + term->vt.curly.style = style; + term->bits_affecting_ascii_printer.curly_style = + style > CURLY_SINGLE; break; } @@ -134,6 +136,7 @@ csi_sgr(struct terminal *term) case 24: { term->vt.attrs.underline = false; term->vt.curly.style = CURLY_NONE; + term->bits_affecting_ascii_printer.curly_style = false; term_update_ascii_printer(term); break; } @@ -236,6 +239,7 @@ csi_sgr(struct terminal *term) if (unlikely(param == 58)) { term->vt.curly.color_src = src; term->vt.curly.color = color; + term->bits_affecting_ascii_printer.curly_color = true; term_update_ascii_printer(term); } else if (param == 38) { term->vt.attrs.fg_src = src; @@ -272,6 +276,7 @@ csi_sgr(struct terminal *term) case 59: term->vt.curly.color_src = COLOR_DEFAULT; term->vt.curly.color = 0; + term->bits_affecting_ascii_printer.curly_color = false; term_update_ascii_printer(term); break; @@ -527,6 +532,9 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) tll_free(term->alt.scroll_damage); term_damage_view(term); } + + term->bits_affecting_ascii_printer.sixels = + tll_length(term->grid->sixel_images) > 0; term_update_ascii_printer(term); break; @@ -1165,6 +1173,7 @@ csi_dispatch(struct terminal *term, uint8_t final) if (param == 4) { /* Insertion Replacement Mode (IRM) */ term->insert_mode = sm; + term->bits_affecting_ascii_printer.insert_mode = sm; term_update_ascii_printer(term); break; } diff --git a/sixel.c b/sixel.c index 279aef05..161eaad5 100644 --- a/sixel.c +++ b/sixel.c @@ -420,6 +420,8 @@ sixel_scroll_up(struct terminal *term, int rows) } } + term->bits_affecting_ascii_printer.sixels = + tll_length(term->grid->sixel_images) > 0; term_update_ascii_printer(term); verify_sixels(term); } @@ -444,6 +446,8 @@ sixel_scroll_down(struct terminal *term, int rows) break; } + term->bits_affecting_ascii_printer.sixels = + tll_length(term->grid->sixel_images) > 0; term_update_ascii_printer(term); verify_sixels(term); } @@ -852,6 +856,8 @@ sixel_overwrite_by_rectangle( } else _sixel_overwrite_by_rectangle(term, start, col, height, width, NULL, NULL); + term->bits_affecting_ascii_printer.sixels = + tll_length(term->grid->sixel_images) > 0; term_update_ascii_printer(term); } @@ -908,6 +914,8 @@ sixel_overwrite_by_row(struct terminal *term, int _row, int col, int width) } } + term->bits_affecting_ascii_printer.sixels = + tll_length(term->grid->sixel_images) > 0; term_update_ascii_printer(term); } @@ -1387,6 +1395,9 @@ sixel_unhook(struct terminal *term) LOG_DBG("you now have %zu sixels in current grid", tll_length(term->grid->sixel_images)); + + term->bits_affecting_ascii_printer.sixels = + tll_length(term->grid->sixel_images) > 0; term_update_ascii_printer(term); render_refresh(term); } diff --git a/terminal.c b/terminal.c index ecd55d77..83cbb42b 100644 --- a/terminal.c +++ b/terminal.c @@ -2023,6 +2023,7 @@ term_reset(struct terminal *term, bool hard) term_ime_enable(term); #endif + term->bits_affecting_ascii_printer.value = 0; term_update_ascii_printer(term); if (!hard) @@ -3021,6 +3022,9 @@ term_restore_cursor(struct terminal *term, const struct cursor *cursor) term->vt.attrs = term->vt.saved_attrs; term->charsets = term->saved_charsets; + + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; term_update_ascii_printer(term); } @@ -3801,21 +3805,21 @@ ascii_printer_single_shift(struct terminal *term, char32_t wc) { ascii_printer_generic(term, wc); term->charsets.selected = term->charsets.saved; + + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; term_update_ascii_printer(term); } void term_update_ascii_printer(struct terminal *term) { + _Static_assert(sizeof(term->bits_affecting_ascii_printer) == sizeof(uint8_t), "bad size"); + void (*new_printer)(struct terminal *term, char32_t wc) = - unlikely(tll_length(term->grid->sixel_images) > 0 || - term->vt.osc8.uri != NULL || - term->vt.curly.style > CURLY_SINGLE || - term->vt.curly.color_src != COLOR_DEFAULT || - term->charsets.set[term->charsets.selected] == CHARSET_GRAPHIC || - term->insert_mode) - ? &ascii_printer_generic - : &ascii_printer_fast; + unlikely(term->bits_affecting_ascii_printer.value != 0) + ? &ascii_printer_generic + : &ascii_printer_fast; #if defined(_DEBUG) && LOG_ENABLE_DBG if (term->ascii_printer != new_printer) { @@ -4108,6 +4112,8 @@ term_osc8_open(struct terminal *term, uint64_t id, const char *uri) term->vt.osc8.id = id; term->vt.osc8.uri = xstrdup(uri); + + term->bits_affecting_ascii_printer.osc8 = true; term_update_ascii_printer(term); } @@ -4117,6 +4123,7 @@ term_osc8_close(struct terminal *term) free(term->vt.osc8.uri); term->vt.osc8.uri = NULL; term->vt.osc8.id = 0; + term->bits_affecting_ascii_printer.osc8 = false; term_update_ascii_printer(term); } diff --git a/terminal.h b/terminal.h index 7f6814d3..9fdd3dc0 100644 --- a/terminal.h +++ b/terminal.h @@ -393,6 +393,17 @@ struct terminal { const struct config *conf; void (*ascii_printer)(struct terminal *term, char32_t c); + union { + struct { + bool sixels:1; + bool osc8:1; + bool curly_style:1; + bool curly_color:1; + bool insert_mode:1; + bool charset:1; + }; + uint8_t value; + } bits_affecting_ascii_printer; pid_t slave; int ptmx; diff --git a/vt.c b/vt.c index ba78540f..487c5f5f 100644 --- a/vt.c +++ b/vt.c @@ -243,12 +243,16 @@ action_execute(struct terminal *term, uint8_t c) case '\x0e': /* SO - shift out */ term->charsets.selected = G1; + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; term_update_ascii_printer(term); break; case '\x0f': /* SI - shift in */ term->charsets.selected = G0; + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; term_update_ascii_printer(term); break; @@ -482,12 +486,16 @@ action_esc_dispatch(struct terminal *term, uint8_t final) case 'n': /* LS2 - Locking Shift 2 */ term->charsets.selected = G2; + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; term_update_ascii_printer(term); break; case 'o': /* LS3 - Locking Shift 3 */ term->charsets.selected = G3; + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; term_update_ascii_printer(term); break; @@ -546,6 +554,8 @@ action_esc_dispatch(struct terminal *term, uint8_t final) size_t idx = term->vt.private - '('; xassert(idx <= G3); term->charsets.set[idx] = CHARSET_GRAPHIC; + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; term_update_ascii_printer(term); break; } @@ -554,6 +564,8 @@ action_esc_dispatch(struct terminal *term, uint8_t final) size_t idx = term->vt.private - '('; xassert(idx <= G3); term->charsets.set[idx] = CHARSET_ASCII; + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; term_update_ascii_printer(term); break; } From a33954a8f4f49b1af427edcadb43f40d8086537e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 24 Jun 2024 01:31:42 +0200 Subject: [PATCH 0749/1323] doc: foot-ctlseq: add 58/59 (styled + colored underlines) --- doc/foot-ctlseqs.7.scd | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index 38742aa3..6d324156 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -150,7 +150,7 @@ m*. | 3 : Italic | 4 -: Underline +: Underline, including styled underlines | 5 : Blink | 7 @@ -176,15 +176,19 @@ m*. | 30-37 : Select foreground color (using *regularN* in *foot.ini*(5)) | 38 -: See "indexed and RGB colors" below +: Select foreground color, see "indexed and RGB colors" below | 39 : Use the default foreground color (*foreground* in *foot.ini*(5)) | 40-47 : Select background color (using *regularN* in *foot.ini*(5)) | 48 -: See "indexed and RGB colors" below +: Select background color, see "indexed and RGB colors" below | 49 : Use the default background color (*background* in *foot.ini*(5)) +| 58 +: Select underline color, see "indexed and RGB colors" below +| 59 +: Use the default underline color | 90-97 : Select foreground color (using *brightN* in *foot.ini*(5)) | 100-107 From 0759caec6ee595182c2add37ce38bb2a8b2e15bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 24 Jun 2024 01:33:07 +0200 Subject: [PATCH 0750/1323] changelog: styled + colored underlines --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 901f5a6b..2de1929f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,10 +61,11 @@ will now utilize the new single-pixel buffer protocol. This mainly reduces the memory usage, but should also be slightly faster. * Support for high-res mouse wheel scroll events ([#1738][1738]). +* Styled and colored underlines ([#828][828]). [1707]: https://codeberg.org/dnkl/foot/issues/1707 [1738]: https://codeberg.org/dnkl/foot/issues/1738 - +[828]: https://codeberg.org/dnkl/foot/issues/828 ### Changed From 08138e9546a00736d6dab6197070f5455d4d7f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 24 Jun 2024 01:50:52 +0200 Subject: [PATCH 0751/1323] render: underlines: minor perf-tweak: early break out When looking up the extender underline range, break out early if we see that there can't possibly be any ranges matching the current column. --- render.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/render.c b/render.c index b9a597a9..d66bce65 100644 --- a/render.c +++ b/render.c @@ -971,6 +971,9 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag for (int i = 0; i < row->extra->curly_ranges.count; i++) { const struct row_range *range = &row->extra->curly_ranges.v[i]; + if (range->start > col) + break; + if (range->start <= col && col <= range->end) { switch (range->curly.color_src) { case COLOR_BASE256: From 5e046e6a84badbf21e99799db42712026cae2029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 24 Jun 2024 11:02:47 +0200 Subject: [PATCH 0752/1323] csi: sgr_reset(): avoid using memset() This _should_ be what the compiler does anyway (i.e. it _should_ replace the memset() with inline MOVs). But let's be sure. --- csi.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/csi.c b/csi.c index 387c37d9..10f525ef 100644 --- a/csi.c +++ b/csi.c @@ -32,9 +32,8 @@ static void sgr_reset(struct terminal *term) { - /* TODO: can we drop this check? */ - memset(&term->vt.attrs, 0, sizeof(term->vt.attrs)); - memset(&term->vt.curly, 0, sizeof(term->vt.curly)); + term->vt.attrs = (struct attributes){0}; + term->vt.curly = (struct curly_range_data){0}; term->bits_affecting_ascii_printer.curly_style = false; term->bits_affecting_ascii_printer.curly_color = false; From 1297b13cd2e90d63ce705650f3ae7454e6982679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 25 Jun 2024 08:33:14 +0200 Subject: [PATCH 0753/1323] foot.info: add 'Su' (Styled Underlines) boolean capability --- foot.info | 1 + 1 file changed, 1 insertion(+) diff --git a/foot.info b/foot.info index e88f07c2..0605e39f 100644 --- a/foot.info +++ b/foot.info @@ -13,6 +13,7 @@ @default_terminfo@+base|foot base fragment, AX, + Su, Tc, XF, XT, From 8e4ca9068050c283a60f43f764c4519ade14759d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 25 Jun 2024 08:33:31 +0200 Subject: [PATCH 0754/1323] foot.info: add 'Setulc' (set underline color) capability --- foot.info | 1 + 1 file changed, 1 insertion(+) diff --git a/foot.info b/foot.info index 0605e39f..b87df007 100644 --- a/foot.info +++ b/foot.info @@ -41,6 +41,7 @@ RV=\E[>c, Rect=\E[%p1%d;%p2%d;%p3%d;%p4%d;%p5%d$x, Se=\E[ q, + Setulc=\E[58\:2\:\:%p1%{65536}%/%d\:%p1%{256}%/%{255}%&%d\:%p1%{255}%&%d%;m, Smulx=\E[4:%p1%dm, Ss=\E[%p1%d q, Sync=\E[?2026%?%p1%{1}%-%tl%eh%;, From 6a0110446c1c96379b5b34dfdff5236a9bee311a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 25 Jun 2024 09:55:55 +0200 Subject: [PATCH 0755/1323] grid: grid_row_{uri,curly}_range_put(): share code grid_row_uri_range_put() and grid_row_curly_range_put() now share the same base logic. Range specific data is passed through a union, and range specific checks are done through switched functions. --- grid.c | 250 +++++++++++++++++++++++++++------------------------------ 1 file changed, 117 insertions(+), 133 deletions(-) diff --git a/grid.c b/grid.c index 3cd818a7..30ef3b65 100644 --- a/grid.c +++ b/grid.c @@ -176,8 +176,17 @@ range_ensure_size(struct row_ranges *ranges, int count_to_add) xassert(ranges->count + count_to_add <= ranges->size); } +union range_data_for_insertion { + struct { + uint64_t id; + const char *uri; + } uri; + struct curly_range_data curly; +}; + static void -range_insert(struct row_ranges *ranges, size_t idx, int start, int end) +range_insert(struct row_ranges *ranges, size_t idx, int start, int end, + enum row_range_type type, const union range_data_for_insertion *data) { range_ensure_size(ranges, 1); @@ -193,6 +202,17 @@ range_insert(struct row_ranges *ranges, size_t idx, int start, int end) .start = start, .end = end, }; + + switch (type) { + case ROW_RANGE_URI: + ranges->v[idx].uri.id = data->uri.id; + ranges->v[idx].uri.uri = xstrdup(data->uri.uri); + break; + + case ROW_RANGE_CURLY: + ranges->v[idx].curly = data->curly; + break; + } } /* @@ -203,17 +223,16 @@ static void uri_range_insert(struct row_ranges *ranges, size_t idx, int start, int end, uint64_t id, const char *uri) { - range_insert(ranges, idx, start, end); - ranges->v[idx].uri.id = id; - ranges->v[idx].uri.uri = xstrdup(uri); + range_insert(ranges, idx, start, end, ROW_RANGE_URI, + &(union range_data_for_insertion){.uri = {.id = id, .uri = uri}}); } static void curly_range_insert(struct row_ranges *ranges, size_t idx, int start, int end, struct curly_range_data data) { - range_insert(ranges, idx, start, end); - ranges->v[idx].curly = data; + range_insert(ranges, idx, start, end, ROW_RANGE_CURLY, + &(union range_data_for_insertion){.curly = data}); } static void @@ -1285,128 +1304,62 @@ grid_resize_and_reflow( #endif } -void -grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id) +static bool +ranges_match(const struct row_range *r1, const struct row_range *r2, + enum row_range_type type) { - ensure_row_has_extra_data(row); + switch (type) { + case ROW_RANGE_URI: + /* TODO: also match URI? */ + return r1->uri.id == r2->uri.id; - size_t insert_idx = 0; - bool replace = false; - bool run_merge_pass = false; - - struct row_data *extra = row->extra; - for (int i = extra->uri_ranges.count - 1; i >= 0; i--) { - struct row_range *r = &extra->uri_ranges.v[i]; - - const bool matching_id = r->uri.id == id; - - if (matching_id && r->end + 1 == col) { - /* Extend existing URI's tail */ - r->end++; - goto out; - } - - else if (r->end < col) { - insert_idx = i + 1; - break; - } - - else if (r->start > col) - continue; - - else { - xassert(r->start <= col); - xassert(r->end >= col); - - if (matching_id) - goto out; - - if (r->start == r->end) { - replace = true; - run_merge_pass = true; - insert_idx = i; - } else if (r->start == col) { - run_merge_pass = true; - r->start++; - insert_idx = i; - } else if (r->end == col) { - run_merge_pass = true; - r->end--; - insert_idx = i + 1; - } else { - xassert(r->start < col); - xassert(r->end > col); - - uri_range_insert( - &extra->uri_ranges, i + 1, col + 1, r->end, r->uri.id, r->uri.uri); - - /* The insertion may xrealloc() the vector, making our - * 'old' pointer invalid */ - r = &extra->uri_ranges.v[i]; - r->end = col - 1; - xassert(r->start <= r->end); - - insert_idx = i + 1; - } - - break; - } + case ROW_RANGE_CURLY: + return r1->curly.style == r2->curly.style && + r1->curly.color_src == r2->curly.color_src && + r1->curly.color == r2->curly.color; } - xassert(insert_idx <= extra->uri_ranges.count); - - if (replace) { - grid_row_uri_range_destroy(&extra->uri_ranges.v[insert_idx]); - extra->uri_ranges.v[insert_idx] = (struct row_range){ - .start = col, - .end = col, - .uri = { - .id = id, - .uri = xstrdup(uri), - }, - }; - } else - uri_range_insert(&extra->uri_ranges, insert_idx, col, col, id, uri); - - if (run_merge_pass) { - for (size_t i = 1; i < extra->uri_ranges.count; i++) { - struct row_range *r1 = &extra->uri_ranges.v[i - 1]; - struct row_range *r2 = &extra->uri_ranges.v[i]; - - if (r1->uri.id == r2->uri.id && r1->end + 1 == r2->start) { - r1->end = r2->end; - range_delete(&extra->uri_ranges, ROW_RANGE_URI, i); - i--; - } - } - } - -out: - verify_no_overlapping_ranges(extra); - verify_ranges_are_sorted(extra); + BUG("invalid range type"); + return false; } -void -grid_row_curly_range_put(struct row *row, int col, struct curly_range_data data) +static bool +range_match_data(const struct row_range *r, + const union range_data_for_insertion *data, + enum row_range_type type) { - ensure_row_has_extra_data(row); + switch (type) { + case ROW_RANGE_URI: + return r->uri.id == data->uri.id; + case ROW_RANGE_CURLY: + return r->curly.style == data->curly.style && + r->curly.color_src == data->curly.color_src && + r->curly.color == data->curly.color; + } + + BUG("invalid range type"); + return false; +} + +static void +grid_row_range_put(struct row_ranges *ranges, int col, + const union range_data_for_insertion *data, + enum row_range_type type) +{ size_t insert_idx = 0; bool replace = false; bool run_merge_pass = false; - struct row_data *extra = row->extra; - for (int i = extra->curly_ranges.count - 1; i >= 0; i--) { - struct row_range *r = &extra->curly_ranges.v[i]; + for (int i = ranges->count - 1; i >= 0; i--) { + struct row_range *r = &ranges->v[i]; - const bool matching = r->curly.style == data.style && - r->curly.color_src == data.color_src && - r->curly.color == data.color; + const bool matching = range_match_data(r, data, type); if (matching && r->end + 1 == col) { - /* Extend existing curly tail */ + /* Extend existing range tail */ r->end++; - goto out; + return; } else if (r->end < col) { @@ -1422,7 +1375,7 @@ grid_row_curly_range_put(struct row *row, int col, struct curly_range_data data) xassert(r->end >= col); if (matching) - goto out; + return; if (r->start == r->end) { replace = true; @@ -1440,12 +1393,13 @@ grid_row_curly_range_put(struct row *row, int col, struct curly_range_data data) xassert(r->start < col); xassert(r->end > col); - curly_range_insert( - &extra->curly_ranges, i + 1, col + 1, r->end, data); + range_insert( + ranges, i + 1, col + 1, r->end, type, + &(union range_data_for_insertion){.uri = {.id = r->uri.id, .uri = r->uri.uri}}); /* The insertion may xrealloc() the vector, making our * 'old' pointer invalid */ - r = &extra->curly_ranges.v[i]; + r = &ranges->v[i]; r->end = col - 1; xassert(r->start <= r->end); @@ -1456,38 +1410,68 @@ grid_row_curly_range_put(struct row *row, int col, struct curly_range_data data) } } - xassert(insert_idx <= extra->curly_ranges.count); + xassert(insert_idx <= ranges->count); if (replace) { - grid_row_curly_range_destroy(&extra->curly_ranges.v[insert_idx]); - extra->curly_ranges.v[insert_idx] = (struct row_range){ + grid_row_range_destroy(&ranges->v[insert_idx], type); + ranges->v[insert_idx] = (struct row_range){ .start = col, .end = col, - .curly = data, }; + + switch (type) { + case ROW_RANGE_URI: + ranges->v[insert_idx].uri.id = data->uri.id; + ranges->v[insert_idx].uri.uri = xstrdup(data->uri.uri); + break; + + case ROW_RANGE_CURLY: + ranges->v[insert_idx].curly = data->curly; + break; + } } else - curly_range_insert(&extra->curly_ranges, insert_idx, col, col, data); + range_insert(ranges, insert_idx, col, col, type, data); if (run_merge_pass) { - for (size_t i = 1; i < extra->curly_ranges.count; i++) { - struct row_range *r1 = &extra->curly_ranges.v[i - 1]; - struct row_range *r2 = &extra->curly_ranges.v[i]; + for (size_t i = 1; i < ranges->count; i++) { + struct row_range *r1 = &ranges->v[i - 1]; + struct row_range *r2 = &ranges->v[i]; - if (r1->curly.style == r2->curly.style && - r1->curly.color_src == r2->curly.color_src && - r1->curly.color == r2->curly.color && - r1->end + 1 == r2->start) - { + if (ranges_match(r1, r2, type) && r1->end + 1 == r2->start) { r1->end = r2->end; - range_delete(&extra->curly_ranges, ROW_RANGE_CURLY, i); + range_delete(ranges, ROW_RANGE_URI, i); i--; } } } +} -out: - verify_no_overlapping_ranges(extra); - verify_ranges_are_sorted(extra); +void +grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id) +{ + ensure_row_has_extra_data(row); + + grid_row_range_put( + &row->extra->uri_ranges, col, + &(union range_data_for_insertion){.uri = {.id = id, .uri = uri}}, + ROW_RANGE_URI); + + verify_no_overlapping_ranges(row->extra); + verify_ranges_are_sorted(row->extra); +} + +void +grid_row_curly_range_put(struct row *row, int col, struct curly_range_data data) +{ + ensure_row_has_extra_data(row); + + grid_row_range_put( + &row->extra->curly_ranges, col, + &(union range_data_for_insertion){.curly = data}, + ROW_RANGE_CURLY); + + verify_no_overlapping_ranges(row->extra); + verify_ranges_are_sorted(row->extra); } UNITTEST From 45f4eb48fb6b4b651df6b466936dd425d9647400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 25 Jun 2024 10:26:07 +0200 Subject: [PATCH 0756/1323] grid: refactor: remove union range_data_for_insertion This union is identical to row_range_data, except the URI char pointer is const. Let's ignore that, and re-use row_range_data, casting the URI pointer when necessary. Also remove uri_range_insert() and curly_range_insert(), and use the generic version of range_insert() everywhere. --- grid.c | 74 ++++++++++++++++++------------------------------------ terminal.h | 10 ++++++-- 2 files changed, 33 insertions(+), 51 deletions(-) diff --git a/grid.c b/grid.c index 30ef3b65..9aee69fb 100644 --- a/grid.c +++ b/grid.c @@ -176,17 +176,13 @@ range_ensure_size(struct row_ranges *ranges, int count_to_add) xassert(ranges->count + count_to_add <= ranges->size); } -union range_data_for_insertion { - struct { - uint64_t id; - const char *uri; - } uri; - struct curly_range_data curly; -}; - +/* + * Be careful! This function may xrealloc() the URI range vector, thus + * invalidating pointers into it. + */ static void range_insert(struct row_ranges *ranges, size_t idx, int start, int end, - enum row_range_type type, const union range_data_for_insertion *data) + enum row_range_type type, const union row_range_data *data) { range_ensure_size(ranges, 1); @@ -215,26 +211,6 @@ range_insert(struct row_ranges *ranges, size_t idx, int start, int end, } } -/* - * Be careful! This function may xrealloc() the URI range vector, thus - * invalidating pointers into it. - */ -static void -uri_range_insert(struct row_ranges *ranges, size_t idx, int start, int end, - uint64_t id, const char *uri) -{ - range_insert(ranges, idx, start, end, ROW_RANGE_URI, - &(union range_data_for_insertion){.uri = {.id = id, .uri = uri}}); -} - -static void -curly_range_insert(struct row_ranges *ranges, size_t idx, int start, int end, - struct curly_range_data data) -{ - range_insert(ranges, idx, start, end, ROW_RANGE_CURLY, - &(union range_data_for_insertion){.curly = data}); -} - static void uri_range_append_no_strdup(struct row_data *extra, int start, int end, uint64_t id, char *uri) @@ -1324,8 +1300,7 @@ ranges_match(const struct row_range *r1, const struct row_range *r2, } static bool -range_match_data(const struct row_range *r, - const union range_data_for_insertion *data, +range_match_data(const struct row_range *r, const union row_range_data *data, enum row_range_type type) { switch (type) { @@ -1344,8 +1319,7 @@ range_match_data(const struct row_range *r, static void grid_row_range_put(struct row_ranges *ranges, int col, - const union range_data_for_insertion *data, - enum row_range_type type) + const union row_range_data *data, enum row_range_type type) { size_t insert_idx = 0; bool replace = false; @@ -1393,9 +1367,13 @@ grid_row_range_put(struct row_ranges *ranges, int col, xassert(r->start < col); xassert(r->end > col); - range_insert( - ranges, i + 1, col + 1, r->end, type, - &(union range_data_for_insertion){.uri = {.id = r->uri.id, .uri = r->uri.uri}}); + union row_range_data insert_data; + switch (type) { + case ROW_RANGE_URI: insert_data.uri = r->uri; break; + case ROW_RANGE_CURLY: insert_data.curly = r->curly; break; + } + + range_insert(ranges, i + 1, col + 1, r->end, type, &insert_data); /* The insertion may xrealloc() the vector, making our * 'old' pointer invalid */ @@ -1453,7 +1431,7 @@ grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id) grid_row_range_put( &row->extra->uri_ranges, col, - &(union range_data_for_insertion){.uri = {.id = id, .uri = uri}}, + &(union row_range_data){.uri = {.id = id, .uri = (char *)uri}}, ROW_RANGE_URI); verify_no_overlapping_ranges(row->extra); @@ -1467,7 +1445,7 @@ grid_row_curly_range_put(struct row *row, int col, struct curly_range_data data) grid_row_range_put( &row->extra->curly_ranges, col, - &(union range_data_for_insertion){.curly = data}, + &(union row_range_data){.curly = data}, ROW_RANGE_CURLY); verify_no_overlapping_ranges(row->extra); @@ -1561,17 +1539,15 @@ grid_row_range_erase(struct row_ranges *ranges, enum row_range_type type, } else if (start > old->start && end < old->end) { - /* Erase range erases a part in the middle of the URI */ - switch (type) { - case ROW_RANGE_URI: - uri_range_insert( - ranges, i + 1, end + 1, old->end, old->uri.id, old->uri.uri); - break; - - case ROW_RANGE_CURLY: - curly_range_insert(ranges, i + 1, end + 1, old->end, old->curly); - break; - } + /* + * Erase range erases a part in the middle of the URI + * + * Must copy, since range_insert() may xrealloc() (thus + * causing 'old' to be invalid) before it dereferences + * old->data + */ + union row_range_data data = old->data; + range_insert(ranges, i + 1, end + 1, old->end, type, &data); /* The insertion may xrealloc() the vector, making our * 'old' pointer invalid */ diff --git a/terminal.h b/terminal.h index 9fdd3dc0..21eb7cee 100644 --- a/terminal.h +++ b/terminal.h @@ -129,8 +129,14 @@ struct row_range { int end; union { - struct uri_range_data uri; - struct curly_range_data curly; + /* This is just an expanded union row_range_data, but + * anonymous, so that we don't have to write range->u.uri.id, + * but can instead do range->uri.id */ + union { + struct uri_range_data uri; + struct curly_range_data curly; + }; + union row_range_data data; }; }; From cb4a74e10ba97ad37df3d065ae95a8f8efa34420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 25 Jun 2024 20:01:47 +0200 Subject: [PATCH 0757/1323] grid: row_range_put(): use correct type in call to range_delete() --- grid.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grid.c b/grid.c index 9aee69fb..93e9eaf8 100644 --- a/grid.c +++ b/grid.c @@ -1417,7 +1417,7 @@ grid_row_range_put(struct row_ranges *ranges, int col, if (ranges_match(r1, r2, type) && r1->end + 1 == r2->start) { r1->end = r2->end; - range_delete(ranges, ROW_RANGE_URI, i); + range_delete(ranges, type, i); i--; } } From 0d3f2f27e33bdee58dc1a0e0ff6a3cb39f53056f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 26 Jun 2024 19:01:54 +0200 Subject: [PATCH 0758/1323] grid: refactor: replace {uri,curly}_range_append() with range_append() Also add range_append_by_ref(), with replaces uri_range_append_no_strdup(). --- grid.c | 119 +++++++++++++++++++++++++++++++++------------------------ 1 file changed, 70 insertions(+), 49 deletions(-) diff --git a/grid.c b/grid.c index 93e9eaf8..c9a0d8dc 100644 --- a/grid.c +++ b/grid.c @@ -194,55 +194,62 @@ range_insert(struct row_ranges *ranges, size_t idx, int start, int end, move_count * sizeof(ranges->v[0])); ranges->count++; - ranges->v[idx] = (struct row_range){ - .start = start, - .end = end, - }; + + struct row_range *r = &ranges->v[idx]; + r->start = start; + r->end = end; switch (type) { case ROW_RANGE_URI: - ranges->v[idx].uri.id = data->uri.id; - ranges->v[idx].uri.uri = xstrdup(data->uri.uri); + r->uri.id = data->uri.id; + r->uri.uri = xstrdup(data->uri.uri); break; case ROW_RANGE_CURLY: - ranges->v[idx].curly = data->curly; + r->curly = data->curly; break; } } static void -uri_range_append_no_strdup(struct row_data *extra, int start, int end, - uint64_t id, char *uri) +range_append_by_ref(struct row_ranges *ranges, int start, int end, + enum row_range_type type, const union row_range_data *data) { - range_ensure_size(&extra->uri_ranges, 1); - extra->uri_ranges.v[extra->uri_ranges.count++] = (struct row_range){ - .start = start, - .end = end, - .uri = { - .id = id, - .uri = uri, - }, - }; + range_ensure_size(ranges, 1); + + struct row_range *r = &ranges->v[ranges->count++]; + + r->start = start; + r->end = end; + + switch (type) { + case ROW_RANGE_URI: + r->uri.id = data->uri.id;; + r->uri.uri = data->uri.uri; + break; + + case ROW_RANGE_CURLY: + r->curly = data->curly; + break; + } } static void -uri_range_append(struct row_data *extra, int start, int end, uint64_t id, - const char *uri) +range_append(struct row_ranges *ranges, int start, int end, + enum row_range_type type, const union row_range_data *data) { - uri_range_append_no_strdup(extra, start, end, id, xstrdup(uri)); -} + switch (type) { + case ROW_RANGE_URI: + range_append_by_ref( + ranges, start, end, type, + &(union row_range_data){.uri = {.id = data->uri.id, + .uri = xstrdup(data->uri.uri)}}); + break; -static void -curly_range_append(struct row_data *extra, int start, int end, - struct curly_range_data data) -{ - range_ensure_size(&extra->curly_ranges, 1); - extra->curly_ranges.v[extra->curly_ranges.count++] = (struct row_range){ - .start = start, - .end = end, - .curly = data, - }; + case ROW_RANGE_CURLY: + range_append_by_ref(ranges, start, end, type, data); + break; + } } static void @@ -304,14 +311,16 @@ grid_snapshot(const struct grid *grid) for (int i = 0; i < extra->uri_ranges.count; i++) { const struct row_range *range = &extra->uri_ranges.v[i]; - uri_range_append( - clone_extra, - range->start, range->end, range->uri.id, range->uri.uri); + range_append( + &clone_extra->uri_ranges, + range->start, range->end, ROW_RANGE_URI, &range->data); } for (int i = 0; i < extra->curly_ranges.count; i++) { const struct row_range *range = &extra->curly_ranges.v[i]; - curly_range_append(clone_extra, range->start, range->end, range->curly); + range_append_by_ref( + &clone_extra->curly_ranges, range->start, range->end, + ROW_RANGE_CURLY, &range->data); } } else clone_row->extra = NULL; @@ -539,7 +548,7 @@ grid_resize_without_reflow( const int start = range->start; const int end = min(range->end, new_cols - 1); - uri_range_append(new_extra, start, end, range->uri.id, range->uri.uri); + range_append(&new_extra->uri_ranges, start, end, ROW_RANGE_URI, &range->data); } for (int i = 0; i < old_extra->curly_ranges.count; i++) { @@ -552,7 +561,7 @@ grid_resize_without_reflow( const int start = range->start; const int end = min(range->end, new_cols - 1); - curly_range_append(new_extra, start, end, range->curly); + range_append_by_ref(&new_extra->curly_ranges, start, end, ROW_RANGE_CURLY, &range->data); } } @@ -629,8 +638,11 @@ reflow_uri_range_start(struct row_range *range, struct row *new_row, int new_col_idx) { ensure_row_has_extra_data(new_row); - uri_range_append_no_strdup( - new_row->extra, new_col_idx, -1, range->uri.id, range->uri.uri); + range_append_by_ref( + &new_row->extra->uri_ranges, new_col_idx, -1, + ROW_RANGE_URI, &range->data); + + /* The reflowed range now owns the URI string */ range->uri.uri = NULL; } @@ -654,7 +666,8 @@ reflow_curly_range_start(struct row_range *range, struct row *new_row, int new_col_idx) { ensure_row_has_extra_data(new_row); - curly_range_append(new_row->extra, new_col_idx, -1, range->curly); + range_append_by_ref(&new_row->extra->curly_ranges, new_col_idx, -1, + ROW_RANGE_CURLY, &range->data); } @@ -732,7 +745,8 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, /* Open a new range on the new/current row */ ensure_row_has_extra_data(new_row); - uri_range_append(new_row->extra, 0, -1, range->uri.id, range->uri.uri); + range_append(&new_row->extra->uri_ranges, 0, -1, + ROW_RANGE_URI, &range->data); } } @@ -747,7 +761,8 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, /* Open a new range on the new/current row */ ensure_row_has_extra_data(new_row); - curly_range_append(new_row->extra, 0, -1, range->curly); + range_append(&new_row->extra->curly_ranges, 0, -1, + ROW_RANGE_CURLY, &range->data); } } @@ -1589,13 +1604,19 @@ UNITTEST { struct row_data row_data = {.uri_ranges = {0}}; struct row row = {.extra = &row_data}; + const union row_range_data data = { + .uri = { + .id = 0, + .uri = (char *)"dummy", + }, + }; /* Try erasing a row without any URIs */ grid_row_uri_range_erase(&row, 0, 200); xassert(row_data.uri_ranges.count == 0); - uri_range_append(&row_data, 1, 10, 0, "dummy"); - uri_range_append(&row_data, 11, 20, 0, "dummy"); + range_append(&row_data.uri_ranges, 1, 10, ROW_RANGE_URI, &data); + range_append(&row_data.uri_ranges, 11, 20, ROW_RANGE_URI, &data); xassert(row_data.uri_ranges.count == 2); xassert(row_data.uri_ranges.v[1].start == 11); xassert(row_data.uri_ranges.v[1].end == 20); @@ -1610,8 +1631,8 @@ UNITTEST /* Two URIs, then erase second half of the first, first half of the second */ - uri_range_append(&row_data, 1, 10, 0, "dummy"); - uri_range_append(&row_data, 11, 20, 0, "dummy"); + range_append(&row_data.uri_ranges, 1, 10, ROW_RANGE_URI, &data); + range_append(&row_data.uri_ranges, 11, 20, ROW_RANGE_URI, &data); grid_row_uri_range_erase(&row, 5, 15); xassert(row_data.uri_ranges.count == 2); xassert(row_data.uri_ranges.v[0].start == 1); @@ -1626,7 +1647,7 @@ UNITTEST row_data.uri_ranges.count = 0; /* One URI, erase middle part of it */ - uri_range_append(&row_data, 1, 10, 0, "dummy"); + range_append(&row_data.uri_ranges, 1, 10, ROW_RANGE_URI, &data); grid_row_uri_range_erase(&row, 5, 6); xassert(row_data.uri_ranges.count == 2); xassert(row_data.uri_ranges.v[0].start == 1); @@ -1657,7 +1678,7 @@ UNITTEST free(row_data.uri_ranges.v); row_data.uri_ranges.v = NULL; row_data.uri_ranges.size = 0; - uri_range_append(&row_data, 1, 10, 0, "dummy"); + range_append(&row_data.uri_ranges, 1, 10, ROW_RANGE_URI, &data); xassert(row_data.uri_ranges.size == 1); grid_row_uri_range_erase(&row, 5, 7); From 19bf558e6cee456eabe6b87d9d51e8f220e20cfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 27 Jun 2024 18:36:17 +0200 Subject: [PATCH 0759/1323] render: underlines: improve the appearance of the 'dotted' style Try to make the 'dotted' style appear more even, and less like each cell is rendered separately (even though they are). Algorithm: Each dot is a square; it's sides are that of the font's line thickness. The spacing (gaps) between the dots is initially the same width as the dots themselves. This means the number of dots per cell is the cell width divided by the dots' length/width, divided by two. At this point, there may be "left-over" pixels.I.e. the widths of the dots and the gaps between them may not add up to the width of the cell. These pixels are evenly (as possible) across the gaps. There are still visual inaccuracies at small font sizes. This is impossible to fix without changing the way underlines are rendered, to render an entire line in one go. This is not something we want to do, since it'll make styled underlines, for a specific cell/character, look differently, depending on the surrounding context. --- render.c | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/render.c b/render.c index d66bce65..65fd1739 100644 --- a/render.c +++ b/render.c @@ -432,17 +432,43 @@ draw_styled_underline(const struct terminal *term, pixman_image_t *pix, PIXMAN_OP_SRC, pix, color, 2, rects); break; } - case CURLY_DOTTED: { - const int ceil_w = cols * term->cell_width; - const int nrects = min(ceil_w / thickness / 2, 16); - pixman_rectangle16_t rects[16] = {0}; - for (int i = 0; i < nrects; i++) { + case CURLY_DOTTED: { + /* Number of dots per cell */ + int per_cell = (term->cell_width / thickness) / 2; + if (per_cell == 0) + per_cell = 1; + + xassert(per_cell >= 1); + + /* Spacing between dots; start with the same width as the dots + themselves, then widen them if necessary, to consume unused + pixels */ + int spacing[per_cell]; + for (int i = 0; i < per_cell; i++) + spacing[i] = thickness; + + /* Pixels remaining at the end of the cell */ + int remaining = term->cell_width - (per_cell * 2) * thickness; + + /* Spread out the left-over pixels across the spacing between + the dots */ + for (int i = 0; remaining > 0; i = (i + 1) % per_cell, remaining--) + spacing[i]++; + + xassert(remaining <= 0); + + pixman_rectangle16_t rects[per_cell]; + int dot_x = x; + for (int i = 0; i < per_cell; i++) { rects[i] = (pixman_rectangle16_t){ - x + i * thickness * 2, y + y_ofs, thickness, thickness}; + dot_x, y + y_ofs, thickness, thickness + }; + + dot_x += thickness + spacing[i]; } - pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, nrects, rects); + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, per_cell, rects); break; } From 0c7725217a706f6df1b543c4c85f54872aaf2f21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 27 Jun 2024 18:54:46 +0200 Subject: [PATCH 0760/1323] render: draw_styled_underline(): respect main.underline-thickness Also refactor a bit, and break out early to draw_underline() for legacy underlines. --- render.c | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/render.c b/render.c index 65fd1739..35b7c6ae 100644 --- a/render.c +++ b/render.c @@ -392,7 +392,15 @@ draw_styled_underline(const struct terminal *term, pixman_image_t *pix, { xassert(style != CURLY_NONE); - const int thickness = font->underline.thickness; + if (style == CURLY_SINGLE) { + draw_underline(term, pix, font, color, x, y, cols); + return; + } + + const int thickness = term->conf->underline_thickness.px >= 0 + ? term_pt_or_px_as_pixels( + term, &term->conf->underline_thickness) + : font->underline.thickness; int y_ofs; @@ -404,10 +412,16 @@ draw_styled_underline(const struct terminal *term, pixman_image_t *pix, term->cell_height - thickness * 3); break; - default: + case CURLY_DASHED: + case CURLY_DOTTED: y_ofs = min(underline_offset(term, font), term->cell_height - thickness); break; + + case CURLY_NONE: + case CURLY_SINGLE: + BUG("underline styles not supposed to be handled here"); + break; } const int ceil_w = cols * term->cell_width; @@ -518,8 +532,9 @@ draw_styled_underline(const struct terminal *term, pixman_image_t *pix, break; } - default: - draw_underline(term, pix, font, color, x, y, cols); + case CURLY_NONE: + case CURLY_SINGLE: + BUG("underline styles not supposed to be handled here"); break; } } From 73f6982cbeb17501b097ea6ed6ca3342d46b6c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 27 Jun 2024 19:06:40 +0200 Subject: [PATCH 0761/1323] grid: merge reflow_{uri,curly}_range_start(), and reflow_{uri,curly}_range_end() --- grid.c | 90 +++++++++++++++++++++++++++++++--------------------------- 1 file changed, 48 insertions(+), 42 deletions(-) diff --git a/grid.c b/grid.c index c9a0d8dc..1db71b5c 100644 --- a/grid.c +++ b/grid.c @@ -634,58 +634,60 @@ grid_resize_without_reflow( } static void -reflow_uri_range_start(struct row_range *range, struct row *new_row, - int new_col_idx) +reflow_range_start(struct row_range *range, enum row_range_type type, + struct row *new_row, int new_col_idx) { ensure_row_has_extra_data(new_row); - range_append_by_ref( - &new_row->extra->uri_ranges, new_col_idx, -1, - ROW_RANGE_URI, &range->data); - /* The reflowed range now owns the URI string */ - range->uri.uri = NULL; + struct row_ranges *new_ranges = NULL; + switch (type) { + case ROW_RANGE_URI: new_ranges = &new_row->extra->uri_ranges; break; + case ROW_RANGE_CURLY: new_ranges = &new_row->extra->curly_ranges; break; + } + + if (new_ranges == NULL) + BUG("unhandled range type"); + + range_append_by_ref(new_ranges, new_col_idx, -1, type, &range->data); + + switch (type) { + case ROW_RANGE_URI: range->uri.uri = NULL; break; /* Owned by new_ranges */ + case ROW_RANGE_CURLY: break; + } } static void -reflow_uri_range_end(struct row_range *range, struct row *new_row, - int new_col_idx) +reflow_range_end(struct row_range *range, enum row_range_type type, + struct row *new_row, int new_col_idx) { struct row_data *extra = new_row->extra; - xassert(extra->uri_ranges.count > 0); + struct row_ranges *ranges = NULL; - struct row_range *new_range = - &extra->uri_ranges.v[extra->uri_ranges.count - 1]; + switch (type) { + case ROW_RANGE_URI: ranges = &extra->uri_ranges; break; + case ROW_RANGE_CURLY: ranges = &extra->curly_ranges; break; + } - xassert(new_range->uri.id == range->uri.id); + if (ranges == NULL) + BUG("unhandled range type"); + + xassert(ranges->count > 0); + + struct row_range *new_range = &ranges->v[ranges->count - 1]; xassert(new_range->end < 0); - new_range->end = new_col_idx; -} -static void -reflow_curly_range_start(struct row_range *range, struct row *new_row, - int new_col_idx) -{ - ensure_row_has_extra_data(new_row); - range_append_by_ref(&new_row->extra->curly_ranges, new_col_idx, -1, - ROW_RANGE_CURLY, &range->data); -} + switch (type) { + case ROW_RANGE_URI: + xassert(new_range->uri.id == range->uri.id); + break; + case ROW_RANGE_CURLY: + xassert(new_range->curly.style == range->curly.style); + xassert(new_range->curly.color_src == range->curly.color_src); + xassert(new_range->curly.color == range->curly.color); + break; + } -static void -reflow_curly_range_end(struct row_range *range, struct row *new_row, - int new_col_idx) -{ - struct row_data *extra = new_row->extra; - xassert(extra->curly_ranges.count > 0); - - struct row_range *new_range = - &extra->curly_ranges.v[extra->curly_ranges.count - 1]; - - xassert(new_range->curly.style == range->curly.style); - xassert(new_range->curly.color_src == range->curly.color_src); - xassert(new_range->curly.color == range->curly.color); - - xassert(new_range->end < 0); new_range->end = new_col_idx; } @@ -1115,10 +1117,12 @@ grid_resize_and_reflow( xassert(uri_range != NULL); if (uri_range->start == end - 1) - reflow_uri_range_start(uri_range, new_row, new_col_idx - 1); + reflow_range_start( + uri_range, ROW_RANGE_URI, new_row, new_col_idx - 1); if (uri_range->end == end - 1) { - reflow_uri_range_end(uri_range, new_row, new_col_idx - 1); + reflow_range_end( + uri_range, ROW_RANGE_URI, new_row, new_col_idx - 1); grid_row_uri_range_destroy(uri_range); uri_range++; } @@ -1128,10 +1132,12 @@ grid_resize_and_reflow( xassert(curly_range != NULL); if (curly_range->start == end - 1) - reflow_curly_range_start(curly_range, new_row, new_col_idx - 1); + reflow_range_start( + curly_range, ROW_RANGE_CURLY, new_row, new_col_idx - 1); if (curly_range->end == end - 1) { - reflow_curly_range_end(curly_range, new_row, new_col_idx - 1); + reflow_range_end( + curly_range, ROW_RANGE_CURLY, new_row, new_col_idx - 1); grid_row_curly_range_destroy(curly_range); curly_range++; } From b503c0d6d96dad6cb09db2dea76ecfe857423552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 27 Jun 2024 19:28:04 +0200 Subject: [PATCH 0762/1323] reaadme: add styled and colored underlines to the feature list --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c188f76c..3cb5834b 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ The fast, lightweight and minimalistic Wayland terminal emulator. * IME (via `text-input-v3`) * Multi-seat * True Color (24bpp) +* [Styled and colored underlines](https://sw.kovidgoyal.net/kitty/underlines/) * [Synchronized Updates](https://gitlab.freedesktop.org/terminal-wg/specifications/-/merge_requests/2) support * [Sixel image support](https://en.wikipedia.org/wiki/Sixel) From 7341ba5ee35e95c36fa90fcab912774615f7f2e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 1 Jul 2024 10:53:21 +0200 Subject: [PATCH 0763/1323] render: cursor color: remove assertion that both cursor fg/bg be set Before this patch, we asserted both the cursor foreground, and background colors had been set. This is true in most cases; the config system enforces it. It is however possible to set only the cursor background color, while leaving the foreground (text) color unset: * Use a foot config that does *not* configure the cursor colors. This means foot will invert the default fg/bg colors when rendering the cursor. * Override the cursor color using an OSC-12 sequence. OSC-12 only sets the background color of the cursor, and there is no other OSC sequence to set the cursor's text color. To handle this, remove the assertion, and simply split the logic for the cursor backgound and foreground colors: * Use the configured background color if set (either through config or OSC-12), otherwise use the default foreground color. * Use the configured foreground color if set (through config), otherwise use the default background color. --- CHANGELOG.md | 3 +++ render.c | 11 ++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2de1929f..b18e470a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,6 +103,9 @@ * VS15 being ignored ([#1742][1742]). * VS16 being ignored for a subset of the valid VS16 sequences ([#1742][1742]). +* Crash in debug builds, when using OSC-12 to set the cursor color and + foot config has not set any custom cursor colors (i.e. without + OSC-12, inverted fg/bg would be used). [1694]: https://codeberg.org/dnkl/foot/issues/1694 [1717]: https://codeberg.org/dnkl/foot/issues/1717 diff --git a/render.c b/render.c index 35b7c6ae..1853eb48 100644 --- a/render.c +++ b/render.c @@ -556,13 +556,14 @@ cursor_colors_for_cell(const struct terminal *term, const struct cell *cell, const pixman_color_t *fg, const pixman_color_t *bg, pixman_color_t *cursor_color, pixman_color_t *text_color) { - if (term->cursor_color.cursor >> 31) { - xassert(term->cursor_color.text >> 31); - + if (term->cursor_color.cursor >> 31) *cursor_color = color_hex_to_pixman(term->cursor_color.cursor); - *text_color = color_hex_to_pixman(term->cursor_color.text); - } else { + else *cursor_color = *fg; + + if (term->cursor_color.text >> 31) + *text_color = color_hex_to_pixman(term->cursor_color.text); + else { *text_color = *bg; if (unlikely(text_color->alpha != 0xffff)) { From 64e7f2512481d33fb46b1cd3eff4b4854d634a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 1 Jul 2024 16:25:45 +0200 Subject: [PATCH 0764/1323] dcs: DECRQSS: styled+colored underlines --- dcs.c | 58 ++++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/dcs.c b/dcs.c index 4dccbdee..7856d79c 100644 --- a/dcs.c +++ b/dcs.c @@ -9,6 +9,7 @@ #include "util.h" #include "vt.h" #include "xmalloc.h" +#include "xsnprintf.h" static bool ensure_size(struct terminal *term, size_t required_size) @@ -270,9 +271,9 @@ decrqss_unhook(struct terminal *term) if (n == 1 && query[0] == 'r') { /* DECSTBM - Set Top and Bottom Margins */ char reply[64]; - int len = snprintf(reply, sizeof(reply), "\033P1$r%d;%dr\033\\", - term->scroll_region.start + 1, - term->scroll_region.end); + int len = xsnprintf(reply, sizeof(reply), "\033P1$r%d;%dr\033\\", + term->scroll_region.start + 1, + term->scroll_region.end); term_to_slave(term, reply, len); } @@ -296,8 +297,15 @@ decrqss_unhook(struct terminal *term) append_sgr_attr("2"); if (a->italic) append_sgr_attr("3"); - if (a->underline) - append_sgr_attr("4"); + if (a->underline) { + if (term->vt.curly.style > CURLY_SINGLE) { + char value[4]; + int val_len = + xsnprintf(value, sizeof(value), "4:%d", term->vt.curly.style); + append_sgr_attr_n(&reply, &len, value, val_len); + } else + append_sgr_attr("4"); + } if (a->blink) append_sgr_attr("5"); if (a->reverse) @@ -313,7 +321,7 @@ decrqss_unhook(struct terminal *term) case COLOR_BASE16: { char value[4]; - int val_len = snprintf( + int val_len = xsnprintf( value, sizeof(value), "%u", a->fg >= 8 ? a->fg - 8 + 90 : a->fg + 30); append_sgr_attr_n(&reply, &len, value, val_len); @@ -322,7 +330,7 @@ decrqss_unhook(struct terminal *term) case COLOR_BASE256: { char value[16]; - int val_len = snprintf(value, sizeof(value), "38:5:%u", a->fg); + int val_len = xsnprintf(value, sizeof(value), "38:5:%u", a->fg); append_sgr_attr_n(&reply, &len, value, val_len); break; } @@ -333,7 +341,7 @@ decrqss_unhook(struct terminal *term) uint8_t b = a->fg >> 0; char value[32]; - int val_len = snprintf( + int val_len = xsnprintf( value, sizeof(value), "38:2::%hhu:%hhu:%hhu", r, g, b); append_sgr_attr_n(&reply, &len, value, val_len); break; @@ -346,7 +354,7 @@ decrqss_unhook(struct terminal *term) case COLOR_BASE16: { char value[4]; - int val_len = snprintf( + int val_len = xsnprintf( value, sizeof(value), "%u", a->bg >= 8 ? a->bg - 8 + 100 : a->bg + 40); append_sgr_attr_n(&reply, &len, value, val_len); @@ -355,7 +363,7 @@ decrqss_unhook(struct terminal *term) case COLOR_BASE256: { char value[16]; - int val_len = snprintf(value, sizeof(value), "48:5:%u", a->bg); + int val_len = xsnprintf(value, sizeof(value), "48:5:%u", a->bg); append_sgr_attr_n(&reply, &len, value, val_len); break; } @@ -366,13 +374,39 @@ decrqss_unhook(struct terminal *term) uint8_t b = a->bg >> 0; char value[32]; - int val_len = snprintf( + int val_len = xsnprintf( value, sizeof(value), "48:2::%hhu:%hhu:%hhu", r, g, b); append_sgr_attr_n(&reply, &len, value, val_len); break; } } + switch (term->vt.curly.color_src) { + case COLOR_DEFAULT: + case COLOR_BASE16: + break; + + case COLOR_BASE256: { + char value[16]; + int val_len = xsnprintf( + value, sizeof(value), "58:5:%u", term->vt.curly.color); + append_sgr_attr_n(&reply, &len, value, val_len); + break; + } + + case COLOR_RGB: { + uint8_t r = term->vt.curly.color >> 16; + uint8_t g = term->vt.curly.color >> 8; + uint8_t b = term->vt.curly.color >> 0; + + char value[32]; + int val_len = xsnprintf( + value, sizeof(value), "58:2::%hhu:%hhu:%hhu", r, g, b); + append_sgr_attr_n(&reply, &len, value, val_len); + break; + } + } + #undef append_sgr_attr_n reply[len - 1] = 'm'; @@ -398,7 +432,7 @@ decrqss_unhook(struct terminal *term) mode--; char reply[16]; - int len = snprintf(reply, sizeof(reply), "\033P1$r%d q\033\\", mode); + int len = xsnprintf(reply, sizeof(reply), "\033P1$r%d q\033\\", mode); term_to_slave(term, reply, len); } From 674a535fc3e1260762aaecb37af4a32581d9b0c6 Mon Sep 17 00:00:00 2001 From: Craig Barnes <craigbarnes@protonmail.com> Date: Mon, 1 Jul 2024 20:00:16 +0100 Subject: [PATCH 0765/1323] Rename various uses of "curly" in the source code with "underline" Since "curly" could make it seem as if all underline styles are curled (to people unfamiliar with the codebase), whereas in reality only 1 is. --- csi.c | 48 +++++++++--------- dcs.c | 14 +++--- grid.c | 144 ++++++++++++++++++++++++++--------------------------- grid.h | 14 +++--- render.c | 62 +++++++++++------------ terminal.c | 24 ++++----- terminal.h | 32 ++++++------ 7 files changed, 169 insertions(+), 169 deletions(-) diff --git a/csi.c b/csi.c index 10f525ef..04a15973 100644 --- a/csi.c +++ b/csi.c @@ -33,10 +33,10 @@ static void sgr_reset(struct terminal *term) { term->vt.attrs = (struct attributes){0}; - term->vt.curly = (struct curly_range_data){0}; + term->vt.underline = (struct underline_range_data){0}; - term->bits_affecting_ascii_printer.curly_style = false; - term->bits_affecting_ascii_printer.curly_color = false; + term->bits_affecting_ascii_printer.underline_style = false; + term->bits_affecting_ascii_printer.underline_color = false; term_update_ascii_printer(term); } @@ -95,27 +95,27 @@ csi_sgr(struct terminal *term) case 3: term->vt.attrs.italic = true; break; case 4: { term->vt.attrs.underline = true; - term->vt.curly.style = CURLY_SINGLE; + term->vt.underline.style = UNDERLINE_SINGLE; if (unlikely(term->vt.params.v[i].sub.idx == 1)) { - enum curly_style style = term->vt.params.v[i].sub.value[0]; + enum underline_style style = term->vt.params.v[i].sub.value[0]; switch (style) { default: - case CURLY_NONE: + case UNDERLINE_NONE: term->vt.attrs.underline = false; - term->vt.curly.style = CURLY_NONE; - term->bits_affecting_ascii_printer.curly_style = false; + term->vt.underline.style = UNDERLINE_NONE; + term->bits_affecting_ascii_printer.underline_style = false; break; - case CURLY_SINGLE: - case CURLY_DOUBLE: - case CURLY_CURLY: - case CURLY_DOTTED: - case CURLY_DASHED: - term->vt.curly.style = style; - term->bits_affecting_ascii_printer.curly_style = - style > CURLY_SINGLE; + case UNDERLINE_SINGLE: + case UNDERLINE_DOUBLE: + case UNDERLINE_CURLY: + case UNDERLINE_DOTTED: + case UNDERLINE_DASHED: + term->vt.underline.style = style; + term->bits_affecting_ascii_printer.underline_style = + style > UNDERLINE_SINGLE; break; } @@ -134,8 +134,8 @@ csi_sgr(struct terminal *term) case 23: term->vt.attrs.italic = false; break; case 24: { term->vt.attrs.underline = false; - term->vt.curly.style = CURLY_NONE; - term->bits_affecting_ascii_printer.curly_style = false; + term->vt.underline.style = UNDERLINE_NONE; + term->bits_affecting_ascii_printer.underline_style = false; term_update_ascii_printer(term); break; } @@ -236,9 +236,9 @@ csi_sgr(struct terminal *term) } if (unlikely(param == 58)) { - term->vt.curly.color_src = src; - term->vt.curly.color = color; - term->bits_affecting_ascii_printer.curly_color = true; + term->vt.underline.color_src = src; + term->vt.underline.color = color; + term->bits_affecting_ascii_printer.underline_color = true; term_update_ascii_printer(term); } else if (param == 38) { term->vt.attrs.fg_src = src; @@ -273,9 +273,9 @@ csi_sgr(struct terminal *term) break; case 59: - term->vt.curly.color_src = COLOR_DEFAULT; - term->vt.curly.color = 0; - term->bits_affecting_ascii_printer.curly_color = false; + term->vt.underline.color_src = COLOR_DEFAULT; + term->vt.underline.color = 0; + term->bits_affecting_ascii_printer.underline_color = false; term_update_ascii_printer(term); break; diff --git a/dcs.c b/dcs.c index 7856d79c..6a832aed 100644 --- a/dcs.c +++ b/dcs.c @@ -298,10 +298,10 @@ decrqss_unhook(struct terminal *term) if (a->italic) append_sgr_attr("3"); if (a->underline) { - if (term->vt.curly.style > CURLY_SINGLE) { + if (term->vt.underline.style > UNDERLINE_SINGLE) { char value[4]; int val_len = - xsnprintf(value, sizeof(value), "4:%d", term->vt.curly.style); + xsnprintf(value, sizeof(value), "4:%d", term->vt.underline.style); append_sgr_attr_n(&reply, &len, value, val_len); } else append_sgr_attr("4"); @@ -381,7 +381,7 @@ decrqss_unhook(struct terminal *term) } } - switch (term->vt.curly.color_src) { + switch (term->vt.underline.color_src) { case COLOR_DEFAULT: case COLOR_BASE16: break; @@ -389,15 +389,15 @@ decrqss_unhook(struct terminal *term) case COLOR_BASE256: { char value[16]; int val_len = xsnprintf( - value, sizeof(value), "58:5:%u", term->vt.curly.color); + value, sizeof(value), "58:5:%u", term->vt.underline.color); append_sgr_attr_n(&reply, &len, value, val_len); break; } case COLOR_RGB: { - uint8_t r = term->vt.curly.color >> 16; - uint8_t g = term->vt.curly.color >> 8; - uint8_t b = term->vt.curly.color >> 0; + uint8_t r = term->vt.underline.color >> 16; + uint8_t g = term->vt.underline.color >> 8; + uint8_t b = term->vt.underline.color >> 0; char value[32]; int val_len = xsnprintf( diff --git a/grid.c b/grid.c index 1db71b5c..2f65a1dd 100644 --- a/grid.c +++ b/grid.c @@ -106,8 +106,8 @@ verify_no_overlapping_ranges_of_type(const struct row_ranges *ranges, r2->uri.uri, r2->start, r2->end); break; - case ROW_RANGE_CURLY: - BUG("curly underline overlap: %d-%d, %d-%d", + case ROW_RANGE_UNDERLINE: + BUG("underline overlap: %d-%d, %d-%d", r1->start, r1->end, r2->start, r2->end); break; } @@ -121,7 +121,7 @@ static void verify_no_overlapping_ranges(const struct row_data *extra) { verify_no_overlapping_ranges_of_type(&extra->uri_ranges, ROW_RANGE_URI); - verify_no_overlapping_ranges_of_type(&extra->curly_ranges, ROW_RANGE_CURLY); + verify_no_overlapping_ranges_of_type(&extra->underline_ranges, ROW_RANGE_UNDERLINE); } static void @@ -144,8 +144,8 @@ verify_ranges_of_type_are_sorted(const struct row_ranges *ranges, r->uri.uri, r->start, r->end); break; - case ROW_RANGE_CURLY: - BUG("curly ranges not sorted correctly: " + case ROW_RANGE_UNDERLINE: + BUG("underline ranges not sorted correctly: " "%d-%d came before %d-%d", last->start, last->end, r->start, r->end); break; @@ -162,7 +162,7 @@ static void verify_ranges_are_sorted(const struct row_data *extra) { verify_ranges_of_type_are_sorted(&extra->uri_ranges, ROW_RANGE_URI); - verify_ranges_of_type_are_sorted(&extra->curly_ranges, ROW_RANGE_CURLY); + verify_ranges_of_type_are_sorted(&extra->underline_ranges, ROW_RANGE_UNDERLINE); } static void @@ -205,8 +205,8 @@ range_insert(struct row_ranges *ranges, size_t idx, int start, int end, r->uri.uri = xstrdup(data->uri.uri); break; - case ROW_RANGE_CURLY: - r->curly = data->curly; + case ROW_RANGE_UNDERLINE: + r->underline = data->underline; break; } } @@ -228,8 +228,8 @@ range_append_by_ref(struct row_ranges *ranges, int start, int end, r->uri.uri = data->uri.uri; break; - case ROW_RANGE_CURLY: - r->curly = data->curly; + case ROW_RANGE_UNDERLINE: + r->underline = data->underline; break; } } @@ -246,7 +246,7 @@ range_append(struct row_ranges *ranges, int start, int end, .uri = xstrdup(data->uri.uri)}}); break; - case ROW_RANGE_CURLY: + case ROW_RANGE_UNDERLINE: range_append_by_ref(ranges, start, end, type, data); break; } @@ -307,7 +307,7 @@ grid_snapshot(const struct grid *grid) clone_row->extra = clone_extra; range_ensure_size(&clone_extra->uri_ranges, extra->uri_ranges.count); - range_ensure_size(&clone_extra->curly_ranges, extra->curly_ranges.count); + range_ensure_size(&clone_extra->underline_ranges, extra->underline_ranges.count); for (int i = 0; i < extra->uri_ranges.count; i++) { const struct row_range *range = &extra->uri_ranges.v[i]; @@ -316,11 +316,11 @@ grid_snapshot(const struct grid *grid) range->start, range->end, ROW_RANGE_URI, &range->data); } - for (int i = 0; i < extra->curly_ranges.count; i++) { - const struct row_range *range = &extra->curly_ranges.v[i]; + for (int i = 0; i < extra->underline_ranges.count; i++) { + const struct row_range *range = &extra->underline_ranges.v[i]; range_append_by_ref( - &clone_extra->curly_ranges, range->start, range->end, - ROW_RANGE_CURLY, &range->data); + &clone_extra->underline_ranges, range->start, range->end, + ROW_RANGE_UNDERLINE, &range->data); } } else clone_row->extra = NULL; @@ -536,7 +536,7 @@ grid_resize_without_reflow( struct row_data *new_extra = new_row->extra; range_ensure_size(&new_extra->uri_ranges, old_extra->uri_ranges.count); - range_ensure_size(&new_extra->curly_ranges, old_extra->curly_ranges.count); + range_ensure_size(&new_extra->underline_ranges, old_extra->underline_ranges.count); for (int i = 0; i < old_extra->uri_ranges.count; i++) { const struct row_range *range = &old_extra->uri_ranges.v[i]; @@ -551,8 +551,8 @@ grid_resize_without_reflow( range_append(&new_extra->uri_ranges, start, end, ROW_RANGE_URI, &range->data); } - for (int i = 0; i < old_extra->curly_ranges.count; i++) { - const struct row_range *range = &old_extra->curly_ranges.v[i]; + for (int i = 0; i < old_extra->underline_ranges.count; i++) { + const struct row_range *range = &old_extra->underline_ranges.v[i]; if (range->start >= new_cols) { /* The whole range is truncated */ @@ -561,7 +561,7 @@ grid_resize_without_reflow( const int start = range->start; const int end = min(range->end, new_cols - 1); - range_append_by_ref(&new_extra->curly_ranges, start, end, ROW_RANGE_CURLY, &range->data); + range_append_by_ref(&new_extra->underline_ranges, start, end, ROW_RANGE_UNDERLINE, &range->data); } } @@ -642,7 +642,7 @@ reflow_range_start(struct row_range *range, enum row_range_type type, struct row_ranges *new_ranges = NULL; switch (type) { case ROW_RANGE_URI: new_ranges = &new_row->extra->uri_ranges; break; - case ROW_RANGE_CURLY: new_ranges = &new_row->extra->curly_ranges; break; + case ROW_RANGE_UNDERLINE: new_ranges = &new_row->extra->underline_ranges; break; } if (new_ranges == NULL) @@ -652,7 +652,7 @@ reflow_range_start(struct row_range *range, enum row_range_type type, switch (type) { case ROW_RANGE_URI: range->uri.uri = NULL; break; /* Owned by new_ranges */ - case ROW_RANGE_CURLY: break; + case ROW_RANGE_UNDERLINE: break; } } @@ -665,7 +665,7 @@ reflow_range_end(struct row_range *range, enum row_range_type type, switch (type) { case ROW_RANGE_URI: ranges = &extra->uri_ranges; break; - case ROW_RANGE_CURLY: ranges = &extra->curly_ranges; break; + case ROW_RANGE_UNDERLINE: ranges = &extra->underline_ranges; break; } if (ranges == NULL) @@ -681,10 +681,10 @@ reflow_range_end(struct row_range *range, enum row_range_type type, xassert(new_range->uri.id == range->uri.id); break; - case ROW_RANGE_CURLY: - xassert(new_range->curly.style == range->curly.style); - xassert(new_range->curly.color_src == range->curly.color_src); - xassert(new_range->curly.color == range->curly.color); + case ROW_RANGE_UNDERLINE: + xassert(new_range->underline.style == range->underline.style); + xassert(new_range->underline.color_src == range->underline.color_src); + xassert(new_range->underline.color == range->underline.color); break; } @@ -752,9 +752,9 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, } } - if (extra->curly_ranges.count > 0) { + if (extra->underline_ranges.count > 0) { struct row_range *range = - &extra->curly_ranges.v[extra->curly_ranges.count - 1]; + &extra->underline_ranges.v[extra->underline_ranges.count - 1]; if (range->end < 0) { @@ -763,8 +763,8 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, /* Open a new range on the new/current row */ ensure_row_has_extra_data(new_row); - range_append(&new_row->extra->curly_ranges, 0, -1, - ROW_RANGE_CURLY, &range->data); + range_append(&new_row->extra->underline_ranges, 0, -1, + ROW_RANGE_UNDERLINE, &range->data); } } @@ -953,7 +953,7 @@ grid_resize_and_reflow( /* Does this row have any URIs? */ struct row_range *uri_range, *uri_range_terminator; - struct row_range *curly_range, *curly_range_terminator; + struct row_range *underline_range, *underline_range_terminator; struct row_data *extra = old_row->extra; if (extra != NULL && extra->uri_ranges.count > 0) { @@ -968,21 +968,21 @@ grid_resize_and_reflow( } else uri_range = uri_range_terminator = NULL; - if (extra != NULL && extra->curly_ranges.count > 0) { - curly_range = &extra->curly_ranges.v[0]; - curly_range_terminator = &extra->curly_ranges.v[extra->curly_ranges.count]; + if (extra != NULL && extra->underline_ranges.count > 0) { + underline_range = &extra->underline_ranges.v[0]; + underline_range_terminator = &extra->underline_ranges.v[extra->underline_ranges.count]; const struct row_range *last_on_row = - &extra->curly_ranges.v[extra->curly_ranges.count - 1]; + &extra->underline_ranges.v[extra->underline_ranges.count - 1]; col_count = max(col_count, last_on_row->end + 1); } else - curly_range = curly_range_terminator = NULL; + underline_range = underline_range_terminator = NULL; for (int start = 0, left = col_count; left > 0;) { int end; bool tp_break = false; bool uri_break = false; - bool curly_break = false; + bool underline_break = false; bool ftcs_break = false; /* Figure out where to end this chunk */ @@ -990,8 +990,8 @@ grid_resize_and_reflow( const int uri_col = uri_range != uri_range_terminator ? ((uri_range->start >= start ? uri_range->start : uri_range->end) + 1) : INT_MAX; - const int curly_col = curly_range != curly_range_terminator - ? ((curly_range->start >= start ? curly_range->start : curly_range->end) + 1) + const int underline_col = underline_range != underline_range_terminator + ? ((underline_range->start >= start ? underline_range->start : underline_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 @@ -1000,10 +1000,10 @@ grid_resize_and_reflow( ? old_row->shell_integration.cmd_end + 1 : INT_MAX; - end = min(col_count, min(min(tp_col, min(uri_col, curly_col)), ftcs_col)); + end = min(col_count, min(min(tp_col, min(uri_col, underline_col)), ftcs_col)); uri_break = end == uri_col; - curly_break = end == curly_col; + underline_break = end == underline_col; tp_break = end == tp_col; ftcs_break = end == ftcs_col; } @@ -1128,18 +1128,18 @@ grid_resize_and_reflow( } } - if (curly_break) { - xassert(curly_range != NULL); + if (underline_break) { + xassert(underline_range != NULL); - if (curly_range->start == end - 1) + if (underline_range->start == end - 1) reflow_range_start( - curly_range, ROW_RANGE_CURLY, new_row, new_col_idx - 1); + underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx - 1); - if (curly_range->end == end - 1) { + if (underline_range->end == end - 1) { reflow_range_end( - curly_range, ROW_RANGE_CURLY, new_row, new_col_idx - 1); - grid_row_curly_range_destroy(curly_range); - curly_range++; + underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx - 1); + grid_row_underline_range_destroy(underline_range); + underline_range++; } } @@ -1180,9 +1180,9 @@ grid_resize_and_reflow( xassert(new_row->extra->uri_ranges.v[last_idx].end >= 0); } - if (new_row->extra->curly_ranges.count > 0) { - int last_idx = new_row->extra->curly_ranges.count - 1; - xassert(new_row->extra->curly_ranges.v[last_idx].end >= 0); + if (new_row->extra->underline_ranges.count > 0) { + int last_idx = new_row->extra->underline_ranges.count - 1; + xassert(new_row->extra->underline_ranges.v[last_idx].end >= 0); } } @@ -1216,8 +1216,8 @@ grid_resize_and_reflow( for (size_t i = 0; i < row->extra->uri_ranges.count; i++) xassert(row->extra->uri_ranges.v[i].end >= 0); - for (size_t i = 0; i < row->extra->curly_ranges.count; i++) - xassert(row->extra->curly_ranges.v[i].end >= 0); + for (size_t i = 0; i < row->extra->underline_ranges.count; i++) + xassert(row->extra->underline_ranges.v[i].end >= 0); verify_no_overlapping_ranges(row->extra); verify_ranges_are_sorted(row->extra); @@ -1310,10 +1310,10 @@ ranges_match(const struct row_range *r1, const struct row_range *r2, /* TODO: also match URI? */ return r1->uri.id == r2->uri.id; - case ROW_RANGE_CURLY: - return r1->curly.style == r2->curly.style && - r1->curly.color_src == r2->curly.color_src && - r1->curly.color == r2->curly.color; + case ROW_RANGE_UNDERLINE: + return r1->underline.style == r2->underline.style && + r1->underline.color_src == r2->underline.color_src && + r1->underline.color == r2->underline.color; } BUG("invalid range type"); @@ -1328,10 +1328,10 @@ range_match_data(const struct row_range *r, const union row_range_data *data, case ROW_RANGE_URI: return r->uri.id == data->uri.id; - case ROW_RANGE_CURLY: - return r->curly.style == data->curly.style && - r->curly.color_src == data->curly.color_src && - r->curly.color == data->curly.color; + case ROW_RANGE_UNDERLINE: + return r->underline.style == data->underline.style && + r->underline.color_src == data->underline.color_src && + r->underline.color == data->underline.color; } BUG("invalid range type"); @@ -1391,7 +1391,7 @@ grid_row_range_put(struct row_ranges *ranges, int col, union row_range_data insert_data; switch (type) { case ROW_RANGE_URI: insert_data.uri = r->uri; break; - case ROW_RANGE_CURLY: insert_data.curly = r->curly; break; + case ROW_RANGE_UNDERLINE: insert_data.underline = r->underline; break; } range_insert(ranges, i + 1, col + 1, r->end, type, &insert_data); @@ -1424,8 +1424,8 @@ grid_row_range_put(struct row_ranges *ranges, int col, ranges->v[insert_idx].uri.uri = xstrdup(data->uri.uri); break; - case ROW_RANGE_CURLY: - ranges->v[insert_idx].curly = data->curly; + case ROW_RANGE_UNDERLINE: + ranges->v[insert_idx].underline = data->underline; break; } } else @@ -1460,14 +1460,14 @@ grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id) } void -grid_row_curly_range_put(struct row *row, int col, struct curly_range_data data) +grid_row_underline_range_put(struct row *row, int col, struct underline_range_data data) { ensure_row_has_extra_data(row); grid_row_range_put( - &row->extra->curly_ranges, col, - &(union row_range_data){.curly = data}, - ROW_RANGE_CURLY); + &row->extra->underline_ranges, col, + &(union row_range_data){.underline = data}, + ROW_RANGE_UNDERLINE); verify_no_overlapping_ranges(row->extra); verify_ranges_are_sorted(row->extra); @@ -1600,10 +1600,10 @@ grid_row_uri_range_erase(struct row *row, int start, int end) } void -grid_row_curly_range_erase(struct row *row, int start, int end) +grid_row_underline_range_erase(struct row *row, int start, int end) { xassert(row->extra != NULL); - grid_row_range_erase(&row->extra->curly_ranges, ROW_RANGE_CURLY, start, end); + grid_row_range_erase(&row->extra->underline_ranges, ROW_RANGE_UNDERLINE, start, end); } UNITTEST diff --git a/grid.h b/grid.h index c5c2b60c..de8f98ab 100644 --- a/grid.h +++ b/grid.h @@ -88,9 +88,9 @@ void grid_row_uri_range_put( struct row *row, int col, const char *uri, uint64_t id); void grid_row_uri_range_erase(struct row *row, int start, int end); -void grid_row_curly_range_put( - struct row *row, int col, struct curly_range_data data); -void grid_row_curly_range_erase(struct row *row, int start, int end); +void grid_row_underline_range_put( + struct row *row, int col, struct underline_range_data data); +void grid_row_underline_range_erase(struct row *row, int start, int end); static inline void grid_row_uri_range_destroy(struct row_range *range) @@ -99,7 +99,7 @@ grid_row_uri_range_destroy(struct row_range *range) } static inline void -grid_row_curly_range_destroy(struct row_range *range) +grid_row_underline_range_destroy(struct row_range *range) { } @@ -108,7 +108,7 @@ grid_row_range_destroy(struct row_range *range, enum row_range_type type) { switch (type) { case ROW_RANGE_URI: grid_row_uri_range_destroy(range); break; - case ROW_RANGE_CURLY: grid_row_curly_range_destroy(range); break; + case ROW_RANGE_UNDERLINE: grid_row_underline_range_destroy(range); break; } } @@ -129,9 +129,9 @@ grid_row_reset_extra(struct row *row) return; grid_row_ranges_destroy(&extra->uri_ranges, ROW_RANGE_URI); - grid_row_ranges_destroy(&extra->curly_ranges, ROW_RANGE_CURLY); + grid_row_ranges_destroy(&extra->underline_ranges, ROW_RANGE_UNDERLINE); free(extra->uri_ranges.v); - free(extra->curly_ranges.v); + free(extra->underline_ranges.v); free(extra); row->extra = NULL; diff --git a/render.c b/render.c index 1853eb48..37e7a80f 100644 --- a/render.c +++ b/render.c @@ -388,11 +388,11 @@ static void draw_styled_underline(const struct terminal *term, pixman_image_t *pix, const struct fcft_font *font, const pixman_color_t *color, - enum curly_style style, int x, int y, int cols) + enum underline_style style, int x, int y, int cols) { - xassert(style != CURLY_NONE); + xassert(style != UNDERLINE_NONE); - if (style == CURLY_SINGLE) { + if (style == UNDERLINE_SINGLE) { draw_underline(term, pix, font, color, x, y, cols); return; } @@ -406,20 +406,20 @@ draw_styled_underline(const struct terminal *term, pixman_image_t *pix, /* Make sure the line isn't positioned below the cell */ switch (style) { - case CURLY_DOUBLE: - case CURLY_CURLY: + case UNDERLINE_DOUBLE: + case UNDERLINE_CURLY: y_ofs = min(underline_offset(term, font), term->cell_height - thickness * 3); break; - case CURLY_DASHED: - case CURLY_DOTTED: + case UNDERLINE_DASHED: + case UNDERLINE_DOTTED: y_ofs = min(underline_offset(term, font), term->cell_height - thickness); break; - case CURLY_NONE: - case CURLY_SINGLE: + case UNDERLINE_NONE: + case UNDERLINE_SINGLE: BUG("underline styles not supposed to be handled here"); break; } @@ -427,7 +427,7 @@ draw_styled_underline(const struct terminal *term, pixman_image_t *pix, const int ceil_w = cols * term->cell_width; switch (style) { - case CURLY_DOUBLE: { + case UNDERLINE_DOUBLE: { const pixman_rectangle16_t rects[] = { {x, y + y_ofs, ceil_w, thickness}, {x, y + y_ofs + thickness * 2, ceil_w, thickness}}; @@ -435,7 +435,7 @@ draw_styled_underline(const struct terminal *term, pixman_image_t *pix, break; } - case CURLY_DASHED: { + case UNDERLINE_DASHED: { const int ceil_w = cols * term->cell_width; const int dash_w = ceil_w / 3 + (ceil_w % 3 > 0); const pixman_rectangle16_t rects[] = { @@ -447,7 +447,7 @@ draw_styled_underline(const struct terminal *term, pixman_image_t *pix, break; } - case CURLY_DOTTED: { + case UNDERLINE_DOTTED: { /* Number of dots per cell */ int per_cell = (term->cell_width / thickness) / 2; if (per_cell == 0) @@ -486,7 +486,7 @@ draw_styled_underline(const struct terminal *term, pixman_image_t *pix, break; } - case CURLY_CURLY: { + case UNDERLINE_CURLY: { const int top = y + y_ofs; const int bot = top + thickness * 3; const int half_x = x + ceil_w / 2.0, full_x = x + ceil_w; @@ -532,8 +532,8 @@ draw_styled_underline(const struct terminal *term, pixman_image_t *pix, break; } - case CURLY_NONE: - case CURLY_SINGLE: + case UNDERLINE_NONE: + case UNDERLINE_SINGLE: BUG("underline styles not supposed to be handled here"); break; } @@ -1005,27 +1005,27 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag /* Underline */ if (cell->attrs.underline) { pixman_color_t underline_color = fg; - enum curly_style underline_style = CURLY_SINGLE; + enum underline_style underline_style = UNDERLINE_SINGLE; /* Check if cell has a styled underline. This lookup is fairly expensive... */ if (row->extra != NULL) { - for (int i = 0; i < row->extra->curly_ranges.count; i++) { - const struct row_range *range = &row->extra->curly_ranges.v[i]; + for (int i = 0; i < row->extra->underline_ranges.count; i++) { + const struct row_range *range = &row->extra->underline_ranges.v[i]; if (range->start > col) break; if (range->start <= col && col <= range->end) { - switch (range->curly.color_src) { + switch (range->underline.color_src) { case COLOR_BASE256: underline_color = color_hex_to_pixman( - term->colors.table[range->curly.color]); + term->colors.table[range->underline.color]); break; case COLOR_RGB: underline_color = - color_hex_to_pixman(range->curly.color); + color_hex_to_pixman(range->underline.color); break; case COLOR_DEFAULT: @@ -1036,7 +1036,7 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag break; } - underline_style = range->curly.style; + underline_style = range->underline.style; break; } } @@ -4442,27 +4442,27 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) g.num_cols * sizeof(g.rows[i]->cells[0])); if (orig->rows[j]->extra == NULL || - orig->rows[j]->extra->curly_ranges.count == 0) + orig->rows[j]->extra->underline_ranges.count == 0) { continue; } /* - * Copy undercurly ranges + * Copy underline ranges */ - const struct row_ranges *curly_src = &orig->rows[j]->extra->curly_ranges; + const struct row_ranges *underline_src = &orig->rows[j]->extra->underline_ranges; - const int count = curly_src->count; + const int count = underline_src->count; g.rows[i]->extra = xcalloc(1, sizeof(*g.rows[i]->extra)); - g.rows[i]->extra->curly_ranges.v = xmalloc( - count * sizeof(g.rows[i]->extra->curly_ranges.v[0])); + g.rows[i]->extra->underline_ranges.v = xmalloc( + count * sizeof(g.rows[i]->extra->underline_ranges.v[0])); - struct row_ranges *curly_dst = &g.rows[i]->extra->curly_ranges; - curly_dst->count = curly_dst->size = count; + struct row_ranges *underline_dst = &g.rows[i]->extra->underline_ranges; + underline_dst->count = underline_dst->size = count; for (int k = 0; k < count; k++) - curly_dst->v[k] = curly_src->v[k]; + underline_dst->v[k] = underline_src->v[k]; } term->normal = g; diff --git a/terminal.c b/terminal.c index 83cbb42b..0518e57d 100644 --- a/terminal.c +++ b/terminal.c @@ -1942,7 +1942,7 @@ erase_cell_range(struct terminal *term, struct row *row, int start, int end) if (unlikely(row->extra != NULL)) { grid_row_uri_range_erase(row, start, end); - grid_row_curly_range_erase(row, start, end); + grid_row_underline_range_erase(row, start, end); } } @@ -3642,10 +3642,10 @@ term_fill(struct terminal *term, int r, int c, uint8_t data, size_t count, } if (unlikely(use_sgr_attrs && - (term->vt.curly.style > CURLY_SINGLE || - term->vt.curly.color_src != COLOR_DEFAULT))) + (term->vt.underline.style > UNDERLINE_SINGLE || + term->vt.underline.color_src != COLOR_DEFAULT))) { - grid_row_curly_range_put(row, c, term->vt.curly); + grid_row_underline_range_put(row, c, term->vt.underline); } } @@ -3653,12 +3653,12 @@ term_fill(struct terminal *term, int r, int c, uint8_t data, size_t count, if (likely(term->vt.osc8.uri != NULL)) grid_row_uri_range_erase(row, c, c + count - 1); - if (likely(term->vt.curly.style <= CURLY_SINGLE && - term->vt.curly.color_src == COLOR_DEFAULT)) + if (likely(term->vt.underline.style <= UNDERLINE_SINGLE && + term->vt.underline.color_src == COLOR_DEFAULT)) { /* No extended/styled underlines active, so erase any such attributes at the target columns */ - grid_row_curly_range_erase(row, c, c + count - 1); + grid_row_underline_range_erase(row, c, c + count - 1); } } } @@ -3730,12 +3730,12 @@ term_print(struct terminal *term, char32_t wc, int width) } else if (row->extra != NULL) grid_row_uri_range_erase(row, col, col + width - 1); - if (unlikely(term->vt.curly.style > CURLY_SINGLE || - term->vt.curly.color_src != COLOR_DEFAULT)) + if (unlikely(term->vt.underline.style > UNDERLINE_SINGLE || + term->vt.underline.color_src != COLOR_DEFAULT)) { - grid_row_curly_range_put(row, col, term->vt.curly); + grid_row_underline_range_put(row, col, term->vt.underline); } else if (row->extra != NULL) - grid_row_curly_range_erase(row, col, col + width - 1); + grid_row_underline_range_erase(row, col, col + width - 1); /* Advance cursor the 'additional' columns while dirty:ing the cells */ for (int i = 1; i < width && (col + 1) < term->cols; i++) { @@ -3796,7 +3796,7 @@ ascii_printer_fast(struct terminal *term, char32_t wc) if (unlikely(row->extra != NULL)) { grid_row_uri_range_erase(row, uri_start, uri_start); - grid_row_curly_range_erase(row, uri_start, uri_start); + grid_row_underline_range_erase(row, uri_start, uri_start); } } diff --git a/terminal.h b/terminal.h index 21eb7cee..a148e528 100644 --- a/terminal.h +++ b/terminal.h @@ -104,24 +104,24 @@ struct uri_range_data { char *uri; }; -enum curly_style { - CURLY_NONE, - CURLY_SINGLE, /* Legacy underline */ - CURLY_DOUBLE, - CURLY_CURLY, - CURLY_DOTTED, - CURLY_DASHED, +enum underline_style { + UNDERLINE_NONE, + UNDERLINE_SINGLE, /* Legacy underline */ + UNDERLINE_DOUBLE, + UNDERLINE_CURLY, + UNDERLINE_DOTTED, + UNDERLINE_DASHED, }; -struct curly_range_data { - enum curly_style style; +struct underline_range_data { + enum underline_style style; enum color_source color_src; uint32_t color; }; union row_range_data { struct uri_range_data uri; - struct curly_range_data curly; + struct underline_range_data underline; }; struct row_range { @@ -134,7 +134,7 @@ struct row_range { * but can instead do range->uri.id */ union { struct uri_range_data uri; - struct curly_range_data curly; + struct underline_range_data underline; }; union row_range_data data; }; @@ -146,11 +146,11 @@ struct row_ranges { int count; }; -enum row_range_type {ROW_RANGE_URI, ROW_RANGE_CURLY}; +enum row_range_type {ROW_RANGE_URI, ROW_RANGE_UNDERLINE}; struct row_data { struct row_ranges uri_ranges; - struct row_ranges curly_ranges; + struct row_ranges underline_ranges; }; struct row { @@ -299,7 +299,7 @@ struct vt { char *uri; } osc8; - struct curly_range_data curly; + struct underline_range_data underline; struct { uint8_t *data; @@ -403,8 +403,8 @@ struct terminal { struct { bool sixels:1; bool osc8:1; - bool curly_style:1; - bool curly_color:1; + bool underline_style:1; + bool underline_color:1; bool insert_mode:1; bool charset:1; }; From ab4b3cbd34adeb551dff7e5e827fdc2a2d1faeed Mon Sep 17 00:00:00 2001 From: Craig Barnes <craigbarnes@protonmail.com> Date: Wed, 3 Jul 2024 06:09:59 +0100 Subject: [PATCH 0766/1323] doc: foot-ctlseq: add missing "E" to DECRQSS sequence This omission seems to have been a typo in commit add530e66d770954. --- doc/foot-ctlseqs.7.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index 6d324156..c3a6e163 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -750,7 +750,7 @@ and are terminated by *\\E\\* (ST). :< *Description* | \\EP q <sixel data> \\E\\ : Emit a sixel image at the current cursor position -| \\P $ q <query> \\E\\ +| \\EP $ q <query> \\E\\ : Request selection or setting (DECRQSS). Implemented queries: DECSTBM, SGR and DECSCUSR. | \\EP = _C_ s \\E\\ From 970b95509c0ddb8dcf3733f3f27eb78da792a296 Mon Sep 17 00:00:00 2001 From: Craig Barnes <craigbarnes@protonmail.com> Date: Wed, 3 Jul 2024 06:55:01 +0100 Subject: [PATCH 0767/1323] render: fix "maybe-uninitialized" error in draw_styled_underline() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reproducer: CFLAGS='-Og -g' meson setup --buildtype=debug bld ninja -C bld Error message: ../render.c: In function ‘draw_styled_underline’: ../render.c:490:19: error: ‘y_ofs’ may be used uninitialized [-Werror=maybe-uninitialized] 490 | const int top = y + y_ofs; | ^~~ ../render.c:405:9: note: ‘y_ofs’ was declared here 405 | int y_ofs; | ^~~~~ --- render.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/render.c b/render.c index 37e7a80f..4103be5c 100644 --- a/render.c +++ b/render.c @@ -420,8 +420,9 @@ draw_styled_underline(const struct terminal *term, pixman_image_t *pix, case UNDERLINE_NONE: case UNDERLINE_SINGLE: - BUG("underline styles not supposed to be handled here"); - break; + default: + BUG("unexpected underline style: %d", (int)style); + return; } const int ceil_w = cols * term->cell_width; From e708d19ea3b9319ccff6a305b893fff31d993854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 2 Jul 2024 07:45:04 +0200 Subject: [PATCH 0768/1323] csi: implement SGR 21, double underlines --- CHANGELOG.md | 1 + csi.c | 8 +++++++- doc/foot-ctlseqs.7.scd | 2 ++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b18e470a..a504f77a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ reduces the memory usage, but should also be slightly faster. * Support for high-res mouse wheel scroll events ([#1738][1738]). * Styled and colored underlines ([#828][828]). +* Support for SGR 21 (double underline). [1707]: https://codeberg.org/dnkl/foot/issues/1707 [1738]: https://codeberg.org/dnkl/foot/issues/1738 diff --git a/csi.c b/csi.c index 04a15973..2d0f3c62 100644 --- a/csi.c +++ b/csi.c @@ -129,7 +129,13 @@ csi_sgr(struct terminal *term) case 8: term->vt.attrs.conceal = true; break; case 9: term->vt.attrs.strikethrough = true; break; - case 21: break; /* double-underline, not implemented */ + case 21: + term->vt.attrs.underline = true; + term->vt.underline.style = UNDERLINE_DOUBLE; + term->bits_affecting_ascii_printer.underline_style = true; + term_update_ascii_printer(term); + break; + case 22: term->vt.attrs.bold = term->vt.attrs.dim = false; break; case 23: term->vt.attrs.italic = false; break; case 24: { diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index c3a6e163..3ed26b11 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -159,6 +159,8 @@ m*. : Conceal; text is not visible, but is copiable | 9 : Crossed-out/strike +| 21 +: Double underline | 22 : Disable *bold* and *dim* | 23 From 5d4a002413cdf09aa471e29b0a7ae29f7cf7b89c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 1 Jul 2024 17:24:50 +0200 Subject: [PATCH 0769/1323] osc: merge OSC 10/11/12/17/19 handling 10/11/17/19 were already merged, so this patch just stops special casing 12 (cursor color). In preparation for XTPUSHCOLORS/XTPOPCOLORS, the cursor colors are moved from their own struct, into the 'colors' struct. Also fix a bug where OSC 17/19 queries returned OSC-11 data. --- osc.c | 67 +++++++++++++++++++----------------------------------- render.c | 8 +++---- terminal.c | 10 ++++---- terminal.h | 6 ++--- 4 files changed, 33 insertions(+), 58 deletions(-) diff --git a/osc.c b/osc.c index 546421f5..ff52fbca 100644 --- a/osc.c +++ b/osc.c @@ -712,15 +712,25 @@ osc_dispatch(struct terminal *term) osc_notify(term, string); break; - case 10: - case 11: - case 17: - case 19: { + case 10: /* fg */ + case 11: /* bg */ + case 12: /* cursor */ + case 17: /* highlight (selection) fg */ + case 19: { /* highlight (selection) bg */ /* Set default foreground/background/highlight-bg/highlight-fg color */ /* Client queried for current value */ if (string[0] == '?' && string[1] == '\0') { - uint32_t color = param == 10 ? term->colors.fg : term->colors.bg; + uint32_t color = param == 10 + ? term->colors.fg + : param == 11 + ? term->colors.bg + : param == 12 + ? term->colors.cursor_bg + : param == 17 + ? term->colors.selection_bg + : term->colors.selection_fg; + uint8_t r = (color >> 16) & 0xff; uint8_t g = (color >> 8) & 0xff; uint8_t b = (color >> 0) & 0xff; @@ -754,6 +764,7 @@ osc_dispatch(struct terminal *term) LOG_DBG("change color definition for %s to %06x", param == 10 ? "foreground" : param == 11 ? "background" : + param == 12 ? "cursor" : param == 17 ? "selection background" : "selection foreground", color); @@ -776,6 +787,11 @@ osc_dispatch(struct terminal *term) } break; + case 12: + term->colors.cursor_bg = 1u << 31 | color; + term_damage_cursor(term); + break; + case 17: term->colors.selection_bg = color; term->colors.use_custom_selection = true; @@ -792,43 +808,6 @@ osc_dispatch(struct terminal *term) break; } - case 12: /* Set cursor color */ - - /* Client queried for current value */ - if (string[0] == '?' && string[1] == '\0') { - uint8_t r = (term->cursor_color.cursor >> 16) & 0xff; - uint8_t g = (term->cursor_color.cursor >> 8) & 0xff; - uint8_t b = (term->cursor_color.cursor >> 0) & 0xff; - const char *terminator = term->vt.osc.bel ? "\a" : "\033\\"; - - char reply[32]; - size_t n = xsnprintf( - reply, sizeof(reply), "\033]12;rgb:%02x/%02x/%02x%s", - r, g, b, terminator); - - term_to_slave(term, reply, n); - break; - } - - uint32_t color; - - if (string[0] == '#' || string[0] == '[' - ? !parse_legacy_color(string, &color, NULL, NULL) - : !parse_rgb(string, &color, NULL, NULL)) - { - break; - } - - LOG_DBG("change cursor color to %06x", color); - - if (color == 0) - term->cursor_color.cursor = 0; /* Invert fg/bg */ - else - term->cursor_color.cursor = 1u << 31 | color; - - term_damage_cursor(term); - break; - case 22: /* Set mouse cursor */ term_set_user_mouse_cursor(term, string); break; @@ -895,8 +874,8 @@ osc_dispatch(struct terminal *term) case 112: LOG_DBG("resetting cursor color"); - term->cursor_color.text = term->conf->cursor.color.text; - term->cursor_color.cursor = term->conf->cursor.color.cursor; + term->colors.cursor_fg = term->conf->cursor.color.text; + term->colors.cursor_bg = term->conf->cursor.color.cursor; term_damage_cursor(term); break; diff --git a/render.c b/render.c index 4103be5c..2f18ff88 100644 --- a/render.c +++ b/render.c @@ -557,13 +557,13 @@ cursor_colors_for_cell(const struct terminal *term, const struct cell *cell, const pixman_color_t *fg, const pixman_color_t *bg, pixman_color_t *cursor_color, pixman_color_t *text_color) { - if (term->cursor_color.cursor >> 31) - *cursor_color = color_hex_to_pixman(term->cursor_color.cursor); + if (term->colors.cursor_bg >> 31) + *cursor_color = color_hex_to_pixman(term->colors.cursor_bg); else *cursor_color = *fg; - if (term->cursor_color.text >> 31) - *text_color = color_hex_to_pixman(term->cursor_color.text); + if (term->colors.cursor_fg >> 31) + *text_color = color_hex_to_pixman(term->colors.cursor_fg); else { *text_color = *bg; diff --git a/terminal.c b/terminal.c index 0518e57d..caa8aa5a 100644 --- a/terminal.c +++ b/terminal.c @@ -1221,6 +1221,8 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .fg = conf->colors.fg, .bg = conf->colors.bg, .alpha = conf->colors.alpha, + .cursor_fg = conf->cursor.color.text, + .cursor_bg = conf->cursor.color.cursor, .selection_fg = conf->colors.selection_fg, .selection_bg = conf->colors.selection_bg, .use_custom_selection = conf->colors.use_custom.selection, @@ -1233,10 +1235,6 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .state = CURSOR_BLINK_ON, .fd = -1, }, - .cursor_color = { - .text = conf->cursor.color.text, - .cursor = conf->cursor.color.cursor, - }, .selection = { .coords = { .start = {-1, -1}, @@ -2035,6 +2033,8 @@ term_reset(struct terminal *term, bool hard) term->colors.fg = term->conf->colors.fg; term->colors.bg = term->conf->colors.bg; term->colors.alpha = term->conf->colors.alpha; + term->colors.cursor_fg = term->conf->cursor.color.text; + term->colors.cursor_bg = term->conf->cursor.color.cursor; term->colors.selection_fg = term->conf->colors.selection_fg; term->colors.selection_bg = term->conf->colors.selection_bg; term->colors.use_custom_selection = term->conf->colors.use_custom.selection; @@ -2051,8 +2051,6 @@ term_reset(struct terminal *term, bool hard) term->cursor_blink.decset = false; term->cursor_blink.deccsusr = term->conf->cursor.blink.enabled; term_cursor_blink_update(term); - term->cursor_color.text = term->conf->cursor.color.text; - term->cursor_color.cursor = term->conf->cursor.color.cursor; selection_cancel(term); term->normal.offset = term->normal.view = 0; term->alt.offset = term->alt.view = 0; diff --git a/terminal.h b/terminal.h index a148e528..9fce2396 100644 --- a/terminal.h +++ b/terminal.h @@ -568,6 +568,8 @@ struct terminal { uint32_t bg; uint32_t table[256]; uint16_t alpha; + uint32_t cursor_fg; /* Text color */ + uint32_t cursor_bg; /* cursor color */ uint32_t selection_fg; uint32_t selection_bg; bool use_custom_selection; @@ -580,10 +582,6 @@ struct terminal { int fd; enum { CURSOR_BLINK_ON, CURSOR_BLINK_OFF } state; } cursor_blink; - struct { - uint32_t text; - uint32_t cursor; - } cursor_color; struct { enum selection_kind kind; From dd6fc99ae1badb7963271ed171beea8ef0e39601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 1 Jul 2024 17:40:45 +0200 Subject: [PATCH 0770/1323] csi: implement XTPUSHCOLORS+XTPOPCOLORS+XTREPORTCOLORS The documentation of these sequences are vague and lacking, as is often the case with XTerm invented control sequences. I've tried to replicate what XTerm does (as of xterm-392). The stack represents *stashed/stored* palettes. The currently active palette is *not* stored on the stack. The stack is dynamically allocated, and starts out with zero elements. Now, XTerm has a somewhat weird definition of "pushing" and "popping" in this context, and the documentation is somewhat misleading. What a push does is this: it stores the current palette to the stack at the specified slot. If the specified slot number (Pm) is 0, the slot used is the current slot index incremented by 1. The "current" slot index is then set to the specified slot (which is current slot + 1 if Pm == 0). Thus, "push" (i.e. when Pm == 0 is used) means store to the "next" slot. This is true even if the current slot index points into the middle of stack. Pop works in a similar way. The palette is restored from the specified slot index. If the specified slot number is 0, we use the current slot index. The "current" slot index is then set to the specified slot - 1 (current slot - 1 if Pm == 0). XTREPORTCOLORS return the current slot index, and the number of palettes stored on the stack, on the format CSI ? <slot index> ; <palette count> # Q When XTPUSHCOLORS grows the stack with more than one element (i.e. via a 'CSI N # P' sequence), make sure *all* new slots are initialized (to the current color palette). This avoids uninitialized slots, that could then be popped with XTPOPCOLORS. Closes #856 --- csi.c | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ terminal.c | 10 +++++++ terminal.h | 29 ++++++++++++++------- 3 files changed, 105 insertions(+), 10 deletions(-) diff --git a/csi.c b/csi.c index 2d0f3c62..42f19a91 100644 --- a/csi.c +++ b/csi.c @@ -2048,6 +2048,82 @@ csi_dispatch(struct terminal *term, uint8_t final) break; /* private[0] == ‘$’ */ } + case '#': { + switch (final) { + case 'P': { /* XTPUSHCOLORS */ + int slot = vt_param_get(term, 0, 0); + + /* Pm == 0, "push" (what xterm does is take take the + *current* slot + 1, even if that's in the middle of the + stack, and overwrites whatever is already in that + slot) */ + if (slot == 0) + slot = term->color_stack.idx + 1; + + if (term->color_stack.size < slot) { + const size_t new_size = slot; + term->color_stack.stack = xrealloc( + term->color_stack.stack, + new_size * sizeof(term->color_stack.stack[0])); + + /* Initialize new slots (except the selected slot, + which is done below) */ + xassert(new_size > 0); + for (size_t i = term->color_stack.size; i < new_size - 1; i++) { + memcpy(&term->color_stack.stack[i], &term->colors, + sizeof(term->colors)); + } + term->color_stack.size = new_size; + } + + xassert(slot > 0); + xassert(slot <= term->color_stack.size); + term->color_stack.idx = slot; + memcpy(&term->color_stack.stack[slot - 1], &term->colors, + sizeof(term->colors)); + break; + } + + case 'Q': { /* XTPOPCOLORS */ + int slot = vt_param_get(term, 0, 0); + + /* Pm == 0, "pop" (what xterm does is copy colors from the + *current* slot, *and* decrease the current slot index, + even if that's in the middle of the stack) */ + if (slot == 0) + slot = term->color_stack.idx; + + if (slot > 0 && slot <= term->color_stack.size) { + memcpy(&term->colors, &term->color_stack.stack[slot - 1], + sizeof(term->colors)); + term->color_stack.idx = slot - 1; + + /* TODO: we _could_ iterate all cells and only dirty + those that are affected by the palette change... */ + term_damage_view(term); + } else if (slot == 0) { + LOG_ERR("XTPOPCOLORS: cannot pop beyond the first element"); + } else { + LOG_ERR( + "XTPOPCOLORS: invalid color slot: %d " + "(stack has %zu slots, current slot is %zu)", + vt_param_get(term, 0, 0), + term->color_stack.size, term->color_stack.idx); + } + break; + } + + case 'R': { /* XTREPORTCOLORS */ + char reply[64]; + int n = xsnprintf(reply, sizeof(reply), "\033[?%zu;%zu#Q", + term->color_stack.idx, term->color_stack.size); + term_to_slave(term, reply, n); + break; + } + } + break; /* private[0] == '#' */ + } + case 0x243f: /* ?$ */ switch (final) { case 'p': { diff --git a/terminal.c b/terminal.c index caa8aa5a..ca3d2bc1 100644 --- a/terminal.c +++ b/terminal.c @@ -1227,6 +1227,11 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .selection_bg = conf->colors.selection_bg, .use_custom_selection = conf->colors.use_custom.selection, }, + .color_stack = { + .stack = NULL, + .size = 0, + .idx = 0, + }, .origin = ORIGIN_ABSOLUTE, .cursor_style = conf->cursor.style, .cursor_blink = { @@ -1823,6 +1828,7 @@ term_destroy(struct terminal *term) free(term->foot_exe); free(term->cwd); free(term->mouse_user_cursor); + free(term->color_stack.stack); int ret = EXIT_SUCCESS; @@ -2040,6 +2046,10 @@ term_reset(struct terminal *term, bool hard) term->colors.use_custom_selection = term->conf->colors.use_custom.selection; memcpy(term->colors.table, term->conf->colors.table, sizeof(term->colors.table)); + free(term->color_stack.stack); + term->color_stack.stack = NULL; + term->color_stack.size = 0; + term->color_stack.idx = 0; term->origin = ORIGIN_ABSOLUTE; term->normal.cursor.lcf = false; term->alt.cursor.lcf = false; diff --git a/terminal.h b/terminal.h index 9fce2396..4d628661 100644 --- a/terminal.h +++ b/terminal.h @@ -393,6 +393,19 @@ struct url { }; typedef tll(struct url) url_list_t; + +struct colors { + uint32_t fg; + uint32_t bg; + uint32_t table[256]; + uint16_t alpha; + uint32_t cursor_fg; /* Text color */ + uint32_t cursor_bg; /* cursor color */ + uint32_t selection_fg; + uint32_t selection_bg; + bool use_custom_selection; +}; + struct terminal { struct fdm *fdm; struct reaper *reaper; @@ -563,17 +576,13 @@ struct terminal { int cell_width; /* pixels per cell, x-wise */ int cell_height; /* pixels per cell, y-wise */ + struct colors colors; + struct { - uint32_t fg; - uint32_t bg; - uint32_t table[256]; - uint16_t alpha; - uint32_t cursor_fg; /* Text color */ - uint32_t cursor_bg; /* cursor color */ - uint32_t selection_fg; - uint32_t selection_bg; - bool use_custom_selection; - } colors; + struct colors *stack; + size_t idx; + size_t size; + } color_stack; enum cursor_style cursor_style; struct { From bebd5ce4150a9d43fed8e19f793ade13347c247a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 1 Jul 2024 19:10:50 +0200 Subject: [PATCH 0771/1323] doc: foot-ctlseq: document XTPUSHCOLORS, XTPOPCOLORS and XTREPORTCOLORS --- doc/foot-ctlseqs.7.scd | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index 3ed26b11..f73b5793 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -632,6 +632,19 @@ manipulation sequences. The generic format is: : <unnamed> : kitty : Update current Kitty keyboard flags, according to _mode_. +| \\E[ # P +: XTPUSHCOLORS +: xterm +: Push current color palette onto stack +| \\E[ # Q +: XTPOPCOLORS +: xterm +: Pop color palette from stack +| \\E[ # R +: XTREPORTCOLORS +: xterm +: Report the current entry on the palette stack, and the number of + palettes stored on the stack. # OSC From 5edb0deffe293689274c3e39c1fdbfc06fb7c663 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 1 Jul 2024 19:11:05 +0200 Subject: [PATCH 0772/1323] changelog: XTPUSHCOLORS, XTPOPCOLORS and XTREPORTCOLORS --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a504f77a..39c95231 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,10 +63,14 @@ * Support for high-res mouse wheel scroll events ([#1738][1738]). * Styled and colored underlines ([#828][828]). * Support for SGR 21 (double underline). +* Support for `XTPUSHCOLORS`, `XTPOPCOLORS` and `XTREPORTCOLORS`, + i.e. color palette stack ([#856][856]). [1707]: https://codeberg.org/dnkl/foot/issues/1707 [1738]: https://codeberg.org/dnkl/foot/issues/1738 [828]: https://codeberg.org/dnkl/foot/issues/828 +[856]: https://codeberg.org/dnkl/foot/issues/856 + ### Changed From d440d5aa2c31e673e0b4fe5c8db41486ae706391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 1 Jul 2024 19:29:28 +0200 Subject: [PATCH 0773/1323] osc: 10/11/12/17/19: don't apply overly much damage --- osc.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osc.c b/osc.c index ff52fbca..d9e952c0 100644 --- a/osc.c +++ b/osc.c @@ -772,6 +772,7 @@ osc_dispatch(struct terminal *term) switch (param) { case 10: term->colors.fg = color; + term_damage_view(term); break; case 11: @@ -785,6 +786,8 @@ osc_dispatch(struct terminal *term) term_font_subpixel_changed(term); } } + term_damage_view(term); + term_damage_margins(term); break; case 12: @@ -795,16 +798,16 @@ osc_dispatch(struct terminal *term) case 17: term->colors.selection_bg = color; term->colors.use_custom_selection = true; + term_damage_view(term); break; case 19: term->colors.selection_fg = color; term->colors.use_custom_selection = true; + term_damage_view(term); break; } - term_damage_view(term); - term_damage_margins(term); break; } From dd58dad15ac0b298a25a7a3412240bfaaebe9998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 1 Jul 2024 19:29:54 +0200 Subject: [PATCH 0774/1323] csi: redraw margins after restoring the palette --- csi.c | 1 + 1 file changed, 1 insertion(+) diff --git a/csi.c b/csi.c index 42f19a91..86117c7e 100644 --- a/csi.c +++ b/csi.c @@ -2101,6 +2101,7 @@ csi_dispatch(struct terminal *term, uint8_t final) /* TODO: we _could_ iterate all cells and only dirty those that are affected by the palette change... */ term_damage_view(term); + term_damage_margins(term); } else if (slot == 0) { LOG_ERR("XTPOPCOLORS: cannot pop beyond the first element"); } else { From 22c8637610e15d2c84509a484fee41bccd0ad0fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 3 Jul 2024 10:53:33 +0200 Subject: [PATCH 0775/1323] osc: extend damage-cells-by-color to default fg/bg as well When changing part of the color palette, through either OSC-4, or OSC-10 and OSC-11 (and the corresponding reset OSCs: 104, 110 and 111), only dirty affected cells. We've always done this, but only for OSC-4. This patch breaks out that logic, and extends it to handle default fg/bg too. It also fixes a bug where cells with colored underlines were not dirtied if the underline was the only part of the cell that was affected by a OSC-4 change. --- csi.c | 6 ++-- osc.c | 56 +++++---------------------------- terminal.c | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ terminal.h | 1 + 4 files changed, 102 insertions(+), 51 deletions(-) diff --git a/csi.c b/csi.c index 86117c7e..1f61d3ce 100644 --- a/csi.c +++ b/csi.c @@ -2098,8 +2098,10 @@ csi_dispatch(struct terminal *term, uint8_t final) sizeof(term->colors)); term->color_stack.idx = slot - 1; - /* TODO: we _could_ iterate all cells and only dirty - those that are affected by the palette change... */ + /* Assume a full palette switch *will* affect almost + all cells. The alternative is to call + term_damage_color() for all 256 palette entries + *and* the default fg/bg (256 + 2 calls in total) */ term_damage_view(term); term_damage_margins(term); } else if (slot == 0) { diff --git a/osc.c b/osc.c index d9e952c0..a3dc1715 100644 --- a/osc.c +++ b/osc.c @@ -651,47 +651,7 @@ osc_dispatch(struct terminal *term) idx, term->colors.table[idx], color); term->colors.table[idx] = color; - - /* Dirty visible, affected cells */ - for (int r = 0; r < term->rows; r++) { - struct row *row = grid_row_in_view(term->grid, r); - struct cell *cell = &row->cells[0]; - - for (int c = 0; c < term->cols; c++, cell++) { - bool dirty = false; - - switch (cell->attrs.fg_src) { - case COLOR_BASE16: - case COLOR_BASE256: - if (cell->attrs.fg == idx) - dirty = true; - break; - - case COLOR_DEFAULT: - case COLOR_RGB: - /* Not affected */ - break; - } - - switch (cell->attrs.bg_src) { - case COLOR_BASE16: - case COLOR_BASE256: - if (cell->attrs.bg == idx) - dirty = true; - break; - - case COLOR_DEFAULT: - case COLOR_RGB: - /* Not affected */ - break; - } - - if (dirty) { - cell->attrs.clean = 0; - row->dirty = true; - } - } - } + term_damage_color(term, COLOR_BASE256, idx); } } @@ -772,7 +732,7 @@ osc_dispatch(struct terminal *term) switch (param) { case 10: term->colors.fg = color; - term_damage_view(term); + term_damage_color(term, COLOR_DEFAULT, 0); break; case 11: @@ -786,7 +746,7 @@ osc_dispatch(struct terminal *term) term_font_subpixel_changed(term); } } - term_damage_view(term); + term_damage_color(term, COLOR_DEFAULT, 0); term_damage_margins(term); break; @@ -798,13 +758,11 @@ osc_dispatch(struct terminal *term) case 17: term->colors.selection_bg = color; term->colors.use_custom_selection = true; - term_damage_view(term); break; case 19: term->colors.selection_fg = color; term->colors.use_custom_selection = true; - term_damage_view(term); break; } @@ -829,6 +787,7 @@ osc_dispatch(struct terminal *term) LOG_DBG("resetting all colors"); for (size_t i = 0; i < ALEN(term->colors.table); i++) term->colors.table[i] = term->conf->colors.table[i]; + term_damage_view(term); } else { @@ -850,11 +809,10 @@ osc_dispatch(struct terminal *term) LOG_DBG("resetting color #%u", idx); term->colors.table[idx] = term->conf->colors.table[idx]; + term_damage_color(term, COLOR_BASE256, idx); } } - - term_damage_view(term); break; } @@ -864,14 +822,14 @@ osc_dispatch(struct terminal *term) case 110: /* Reset default text foreground color */ LOG_DBG("resetting foreground color"); term->colors.fg = term->conf->colors.fg; - term_damage_view(term); + term_damage_color(term, COLOR_DEFAULT, 0); break; case 111: /* Reset default text background color */ LOG_DBG("resetting background color"); term->colors.bg = term->conf->colors.bg; term->colors.alpha = term->conf->colors.alpha; - term_damage_view(term); + term_damage_color(term, COLOR_DEFAULT, 0); term_damage_margins(term); break; diff --git a/terminal.c b/terminal.c index ca3d2bc1..d08bb74d 100644 --- a/terminal.c +++ b/terminal.c @@ -2386,6 +2386,96 @@ term_damage_margins(struct terminal *term) term->render.margins = true; } +void +term_damage_color(struct terminal *term, enum color_source src, int idx) +{ + xassert(src == COLOR_DEFAULT || src == COLOR_BASE256); + + for (int r = 0; r < term->rows; r++) { + struct row *row = grid_row_in_view(term->grid, r); + struct cell *cell = &row->cells[0]; + const struct cell *end = &row->cells[term->cols]; + + for (; cell < end; cell++) { + bool dirty = false; + + switch (cell->attrs.fg_src) { + case COLOR_BASE16: + case COLOR_BASE256: + if (src == COLOR_BASE256 && cell->attrs.fg == idx) + dirty = true; + break; + + case COLOR_DEFAULT: + if (src == COLOR_DEFAULT) { + /* Doesn't matter whether we've updated the + default foreground, or background, we still + want to dirty this cell, to be sure we handle + all cases of color inversion/reversal */ + dirty = true; + } + break; + + case COLOR_RGB: + /* Not affected */ + break; + } + + switch (cell->attrs.bg_src) { + case COLOR_BASE16: + case COLOR_BASE256: + if (src == COLOR_BASE256 && cell->attrs.bg == idx) + dirty = true; + break; + + case COLOR_DEFAULT: + if (src == COLOR_DEFAULT) { + /* Doesn't matter whether we've updated the + default foreground, or background, we still + want to dirty this cell, to be sure we handle + all cases of color inversion/reversal */ + dirty = true; + } + break; + + case COLOR_RGB: + /* Not affected */ + break; + } + + if (dirty) { + cell->attrs.clean = 0; + row->dirty = true; + } + } + + /* Colored underlines */ + if (row->extra != NULL) { + const struct row_ranges *underlines = &row->extra->underline_ranges; + + for (int i = 0; i < underlines->count; i++) { + const struct row_range *range = &underlines->v[i]; + + /* Underline colors are either default, or + BASE256/RGB, but never BASE16 */ + xassert(range->underline.color_src == COLOR_DEFAULT || + range->underline.color_src == COLOR_BASE256 || + range->underline.color_src == COLOR_RGB); + + if (range->underline.color_src == src) { + struct cell *c = &row->cells[range->start]; + const struct cell *e = &row->cells[range->end + 1]; + + for (; c < e; c++) + c->attrs.clean = 0; + + row->dirty = true; + } + } + } + } +} + void term_damage_scroll(struct terminal *term, enum damage_type damage_type, struct scroll_region region, int lines) diff --git a/terminal.h b/terminal.h index 4d628661..926870c1 100644 --- a/terminal.h +++ b/terminal.h @@ -845,6 +845,7 @@ void term_damage_view(struct terminal *term); void term_damage_cursor(struct terminal *term); void term_damage_margins(struct terminal *term); +void term_damage_color(struct terminal *term, enum color_source src, int idx); void term_reset_view(struct terminal *term); From 85b2fb1e32d499520e5c33f2357d58d35bcb0b60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 7 Jul 2024 16:31:40 +0200 Subject: [PATCH 0776/1323] doc: foot.ini: line-height: add warning about runtime font size changes --- doc/foot.ini.5.scd | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 58ea9c15..f3c0aea6 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -121,6 +121,12 @@ empty string to be set, but it must be quoted: *KEY=""*) You can specify a height in _pixels_ by using the *px* suffix: e.g. *line-height=12px*. + *Warning*: when changing the font size at runtime (i.e. zooming in + our out), foot will change the line height by the same + percentage. However, due to rounding, it is possible the line + height will be "too small" for some font sizes, causing + e.g. underscores to "disappear". + See also: *vertical-letter-offset*. Default: _not set_. From f066fe47f0583179fdd66efd9f10e1bb8200ae7f Mon Sep 17 00:00:00 2001 From: Nicolas Kolling Ribas <nicolaskribas@gmail.com> Date: Tue, 9 Jul 2024 00:59:59 -0300 Subject: [PATCH 0777/1323] themes: add nvim-dark and nvim-light themes Both based on the new "Nvim branded" default color scheme in Neovim 0.10. --- themes/nvim-dark | 32 ++++++++++++++++++++++++++++++++ themes/nvim-light | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 themes/nvim-dark create mode 100644 themes/nvim-light diff --git a/themes/nvim-dark b/themes/nvim-dark new file mode 100644 index 00000000..4c13770a --- /dev/null +++ b/themes/nvim-dark @@ -0,0 +1,32 @@ +# -*- conf -*- +# Neovim Dark theme +# Uses the dark color palette from the default Neovim color scheme +# See: https://github.com/neovim/neovim/blob/fb6c059dc55c8d594102937be4dd70f5ff51614a/src/nvim/highlight_group.c#L419 + +[cursor] +color=14161b e0e2ea # NvimDarkGrey2 NvimLightGrey2 + +[colors] +foreground=e0e2ea # NvimLightGrey2 +background=14161b # NvimDarkGrey2 + +selection-foreground=e0e2ea # NvimLightGrey2 +selection-background=4f5258 # NvimDarkGrey4 + +regular0=07080d # NvimDarkGrey1 +regular1=ffc0b9 # NvimLightRed +regular2=b3f6c0 # NvimLightGreen +regular3=fce094 # NvimLightYellow +regular4=a6dbff # NvimLightBlue +regular5=ffcaff # NvimLightMagenta +regular6=8cf8f7 # NvimLightCyan +regular7=c4c6cd # NvimLightGrey3 + +bright0=2c2e33 # NvimDarkGrey3 +bright1=ffc0b9 # NvimLightRed +bright2=b3f6c0 # NvimLightGreen +bright3=fce094 # NvimLightYellow +bright4=a6dbff # NvimLightBlue +bright5=ffcaff # NvimLightMagenta +bright6=8cf8f7 # NvimLightCyan +bright7=eef1f8 # NvimLightGrey1 diff --git a/themes/nvim-light b/themes/nvim-light new file mode 100644 index 00000000..5afec9d7 --- /dev/null +++ b/themes/nvim-light @@ -0,0 +1,32 @@ +# -*- conf -*- +# Neovim Light theme +# Uses the light color palette from the default Neovim color scheme +# See: https://github.com/neovim/neovim/blob/fb6c059dc55c8d594102937be4dd70f5ff51614a/src/nvim/highlight_group.c#L334 + +[cursor] +color=e0e2ea 14161b # NvimLightGrey2 NvimDarkGrey2 + +[colors] +foreground=14161b # NvimDarkGrey2 +background=e0e2ea # NvimLightGrey2 + +selection-foreground=14161b # NvimDarkGrey2 +selection-background=9b9ea4 # NvimLightGrey4 + +regular0=eef1f8 # NvimLightGrey1 +regular1=590008 # NvimDarkRed +regular2=005523 # NvimDarkGreen +regular3=6b5300 # NvimDarkYellow +regular4=004c73 # NvimDarkBlue +regular5=470045 # NvimDarkMagenta +regular6=007373 # NvimDarkCyan +regular7=2c2e33 # NvimDarkGrey3 + +bright0=c4c6cd # NvimLightGrey3 +bright1=590008 # NvimDarkRed +bright2=005523 # NvimDarkGreen +bright3=6b5300 # NvimDarkYellow +bright4=004c73 # NvimDarkBlue +bright5=470045 # NvimDarkMagenta +bright6=007373 # NvimDarkCyan +bright7=07080d # NvimDarkGrey1 From c46c124363fad6dabd8328bdf8e15e5da8fabf7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 13 Jul 2024 10:19:53 +0200 Subject: [PATCH 0778/1323] render: cursor: use default fg/bg if cell fg/bg are the same When deciding which colors to use for the cursor, and the cursor text and background colors are the same, use the default fg/bg instead. Closes #1761 --- CHANGELOG.md | 6 ++++++ render.c | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39c95231..12bac7be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,8 +84,14 @@ In other words, if, after this change, `XTGETTCAP` returns a string that is different compared to `tigetstr()`, then it is likely a bug in foot's implementation of `XTGETTCAP`. +* If the cursor foreground and background colors are identical (for + example, when cursor uses inverted colors and the cell's foreground + and background are the same), the cursor will instead be rendered + using the default foreground and background colors, inverted + ([#1761][1761]). [1701]: https://codeberg.org/dnkl/foot/issues/1701 +[1761]: https://codeberg.org/dnkl/foot/issues/1761 ### Deprecated diff --git a/render.c b/render.c index 2f18ff88..fd078184 100644 --- a/render.c +++ b/render.c @@ -573,6 +573,14 @@ cursor_colors_for_cell(const struct terminal *term, const struct cell *cell, *text_color = color_hex_to_pixman(term->colors.bg); } } + + if (text_color->red == cursor_color->red && + text_color->green == cursor_color->green && + text_color->blue == cursor_color->blue) + { + *text_color = color_hex_to_pixman(term->colors.bg); + *cursor_color = color_hex_to_pixman(term->colors.fg); + } } static void From 56556e5f237025619820a3c8595e0de9d2b1c230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 13 Jul 2024 10:22:54 +0200 Subject: [PATCH 0779/1323] render: hollow-cursor: use correct cursor color --- CHANGELOG.md | 3 +++ render.c | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12bac7be..38f8d7a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,6 +117,9 @@ * Crash in debug builds, when using OSC-12 to set the cursor color and foot config has not set any custom cursor colors (i.e. without OSC-12, inverted fg/bg would be used). +* Wrong color used when drawing the unfocused, hollow cursor. +* Encoding of `BTN_BACK` and `BTN_FORWARD`, when sending a mouse input + escape sequence to the terminal application. [1694]: https://codeberg.org/dnkl/foot/issues/1694 [1717]: https://codeberg.org/dnkl/foot/issues/1717 diff --git a/render.c b/render.c index fd078184..53c8c90e 100644 --- a/render.c +++ b/render.c @@ -600,7 +600,7 @@ draw_cursor(const struct terminal *term, const struct cell *cell, break; case CURSOR_UNFOCUSED_HOLLOW: - draw_hollow_block(term, pix, fg, x, y, cols); + draw_hollow_block(term, pix, &cursor_color, x, y, cols); return; case CURSOR_UNFOCUSED_NONE: From 15c0078c2dfcb956fc98d68a7dfec99bea2eb726 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 13 Jul 2024 10:40:37 +0200 Subject: [PATCH 0780/1323] changelog: remove entry for change that hasn't yet been merged --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38f8d7a4..a3f7dffb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -118,8 +118,6 @@ foot config has not set any custom cursor colors (i.e. without OSC-12, inverted fg/bg would be used). * Wrong color used when drawing the unfocused, hollow cursor. -* Encoding of `BTN_BACK` and `BTN_FORWARD`, when sending a mouse input - escape sequence to the terminal application. [1694]: https://codeberg.org/dnkl/foot/issues/1694 [1717]: https://codeberg.org/dnkl/foot/issues/1717 From 1136108c9780eae9fae252a1bca5089f19fcd57d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 13 Jul 2024 10:24:11 +0200 Subject: [PATCH 0781/1323] input: don't map wheel events to BTN_{BACK,FORWARD} BTN_BACK and BTN_FORWARD are separate buttons. The scroll wheel don't have any button mappings in libinput/wayland, so make up our own defines. This allows us to map them in mouse bindings. Also expose BTN_WHEEL_{LEFT,RIGHT}. These were already defined, and used, internally, to handle wheel tilt events. With this, they can also be used in mouse bindings. Finally, fix encoding used for BTN_{BACK,FORWARD} when sending mouse button events to the client application. Before this, they were mapped to buttons 4/5. But, button 4/5 are for the scroll wheel, and as mentioned above, BTN_{BACK,FORWARD} are not the same as scroll wheel "buttons". Closes #1763 --- CHANGELOG.md | 12 ++++++++++++ config.c | 15 +++++++++++---- doc/foot.ini.5.scd | 23 +++++++++++++---------- foot.ini | 6 ++++-- input.c | 2 +- input.h | 2 ++ terminal.c | 26 ++++++++++++++++---------- 7 files changed, 59 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3f7dffb..4a2d530c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,9 +89,19 @@ and background are the same), the cursor will instead be rendered using the default foreground and background colors, inverted ([#1761][1761]). +* Mouse wheel events now generate `BTN_WHEEL_BACK` and + `BTN_WHEEL_FORWARD` "button presses", instead of `BTN_BACK` and + `BTN_FORWARD`. The default bindings have been updated, and + `scrollback-up-mouse`, `scrollback-down-mouse`, `font-increase` and + `font-decrease` now use the new button names. + + This change allow users to separate physical mouse buttons that + _also_ generates `BTN_BACK` and `BTN_FORWARD`, from wheel scrolling + ([#1763][1763]). [1701]: https://codeberg.org/dnkl/foot/issues/1701 [1761]: https://codeberg.org/dnkl/foot/issues/1761 +[1763]: https://codeberg.org/dnkl/foot/issues/1763 ### Deprecated @@ -118,6 +128,8 @@ foot config has not set any custom cursor colors (i.e. without OSC-12, inverted fg/bg would be used). * Wrong color used when drawing the unfocused, hollow cursor. +* Encoding of `BTN_BACK` and `BTN_FORWARD`, when sending a mouse input + escape sequence to the terminal application. [1694]: https://codeberg.org/dnkl/foot/issues/1694 [1717]: https://codeberg.org/dnkl/foot/issues/1717 diff --git a/config.c b/config.c index b7bc1f09..5d9a0b75 100644 --- a/config.c +++ b/config.c @@ -1686,6 +1686,7 @@ static const struct { const char *name; int code; } button_map[] = { + /* System defined */ {"BTN_LEFT", BTN_LEFT}, {"BTN_RIGHT", BTN_RIGHT}, {"BTN_MIDDLE", BTN_MIDDLE}, @@ -1694,6 +1695,12 @@ static const struct { {"BTN_FORWARD", BTN_FORWARD}, {"BTN_BACK", BTN_BACK}, {"BTN_TASK", BTN_TASK}, + + /* Foot custom, to be able to map scroll events to mouse bindings */ + {"BTN_WHEEL_BACK", BTN_WHEEL_BACK}, + {"BTN_WHEEL_FORWARD", BTN_WHEEL_FORWARD}, + {"BTN_WHEEL_LEFT", BTN_WHEEL_LEFT}, + {"BTN_WHEEL_RIGHT", BTN_WHEEL_RIGHT}, }; static int @@ -2989,8 +2996,8 @@ static void add_default_mouse_bindings(struct config *conf) { 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_SCROLLBACK_UP_MOUSE, m("none"), {.m = {BTN_WHEEL_BACK, 1}}}, + {BIND_ACTION_SCROLLBACK_DOWN_MOUSE, m("none"), {.m = {BTN_WHEEL_FORWARD, 1}}}, {BIND_ACTION_PRIMARY_PASTE, m("none"), {.m = {BTN_MIDDLE, 1}}}, {BIND_ACTION_SELECT_BEGIN, m("none"), {.m = {BTN_LEFT, 1}}}, {BIND_ACTION_SELECT_BEGIN_BLOCK, m(XKB_MOD_NAME_CTRL), {.m = {BTN_LEFT, 1}}}, @@ -3000,8 +3007,8 @@ add_default_mouse_bindings(struct config *conf) {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}}}, + {BIND_ACTION_FONT_SIZE_UP, m("Control"), {.m = {BTN_WHEEL_BACK, 1}}}, + {BIND_ACTION_FONT_SIZE_DOWN, m("Control"), {.m = {BTN_WHEEL_FORWARD, 1}}}, }; conf->bindings.mouse.count = ALEN(bindings); diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index f3c0aea6..680768e0 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1156,10 +1156,13 @@ The trailing *COUNT* (number of times the button has to be clicked) is optional and specifies the click count required to trigger the binding. The default if *COUNT* is omitted is _1_. -To map wheel events (i.e. scrolling), use the button names *BTN_BACK* -(up) and *BTN_FORWARD* (down). Note that these events never generate a -*COUNT* larger than 1. That is, *BTN_BACK+2*, for example, will never -trigger. +To map wheel events (i.e. scrolling), use the button names +*BTN_WHEEL_BACK* (up) and *BTN_WHEEL_FORWARD* (down). Note that these +events never generate a *COUNT* larger than 1. That is, +*BTN_WHEEL_BACK+2*, for example, will never trigger. + +Foot also recognizes tiltable wheels; to map these, use +*BTN_WHEEL_LEFT* and *BTN_WHEEL_RIGHT*. A modifier+button combination can only be mapped to *one* action. Lets say you want to bind *BTN\_MIDDLE* to *fullscreen*. Since @@ -1187,7 +1190,7 @@ actions listed under *key-bindings* can be used here as well. Alt screen: send fake _KeyUP_ events to the client application, if alternate scroll mode is enabled. - Default: _BTN_BACK_ + Default: _BTN\_WHEEL\_BACK_ *scrollback-down-mouse* Normal screen: scrolls down the contents. @@ -1195,7 +1198,7 @@ actions listed under *key-bindings* can be used here as well. Alt screen: send fake _KeyDOWN_ events to the client application, if alternate scroll mode is enabled. - Default: _BTN_FORWARD_ + Default: _BTN\_WHEEL\_FORWARD_ *select-begin* Begin an interactive selection. The selection is finalized, and @@ -1269,12 +1272,12 @@ actions listed under *key-bindings* can be used here as well. 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*). + Increases the font size by 0.5pt. Default: + _Control+BTN\_WHEEL\_BACK_ (also defined in *key-bindings*). *font-decrease* - Decreases the font size by 0.5pt. Default: _Control+BTN\_FORWARD_ - (also defined in *key-bindings*). + Decreases the font size by 0.5pt. Default: + _Control+BTN\_WHEEL\_FORWARD_ (also defined in *key-bindings*). # TWEAK diff --git a/foot.ini b/foot.ini index 23652d5e..7ae9ba1b 100644 --- a/foot.ini +++ b/foot.ini @@ -213,8 +213,10 @@ # \x03=Mod4+c # Map Super+c -> Ctrl+c [mouse-bindings] -# scrollback-up-mouse=BTN_BACK -# scrollback-down-mouse=BTN_FORWARD +# scrollback-up-mouse=BTN_WHEEL_BACK +# scrollback-down-mouse=BTN_WHEEL_FORWARD +# font-increase=Control+BTN_WHEEL_BACK +# font-decrease=Control+BTN_WHEEL_FORWARD # selection-override-modifiers=Shift # primary-paste=BTN_MIDDLE # select-begin=BTN_LEFT diff --git a/input.c b/input.c index 5c7de859..68cb9314 100644 --- a/input.c +++ b/input.c @@ -2708,7 +2708,7 @@ mouse_scroll(struct seat *seat, int amount, enum wl_pointer_axis axis) xassert(term != NULL); int button = axis == WL_POINTER_AXIS_VERTICAL_SCROLL - ? amount < 0 ? BTN_BACK : BTN_FORWARD + ? amount < 0 ? BTN_WHEEL_BACK : BTN_WHEEL_FORWARD : amount < 0 ? BTN_WHEEL_LEFT : BTN_WHEEL_RIGHT; amount = abs(amount); diff --git a/input.h b/input.h index 2ea1c6a9..34342bbf 100644 --- a/input.h +++ b/input.h @@ -21,6 +21,8 @@ * Mouse buttons are in the range 0x110 - 0x11f, with joystick defines * starting at 0x120. */ +#define BTN_WHEEL_BACK 0x11c +#define BTN_WHEEL_FORWARD 0x11d #define BTN_WHEEL_LEFT 0x11e #define BTN_WHEEL_RIGHT 0x11f diff --git a/terminal.c b/terminal.c index d08bb74d..6e83d9a0 100644 --- a/terminal.c +++ b/terminal.c @@ -3192,17 +3192,23 @@ term_kbd_focus_out(struct terminal *term) static int linux_mouse_button_to_x(int button) { + /* Note: on X11, scroll events where reported as buttons. Not so + * on Wayland. We manually map scroll events to custom "button" + * defines (BTN_WHEEL_*). + */ switch (button) { - case BTN_LEFT: return 1; - case BTN_MIDDLE: return 2; - case BTN_RIGHT: return 3; - case BTN_BACK: return 4; - case BTN_FORWARD: return 5; - case BTN_WHEEL_LEFT: return 6; /* Foot custom define */ - case BTN_WHEEL_RIGHT: return 7; /* Foot custom define */ - case BTN_SIDE: return 8; - case BTN_EXTRA: return 9; - case BTN_TASK: return -1; /* TODO: ??? */ + case BTN_LEFT: return 1; + case BTN_MIDDLE: return 2; + case BTN_RIGHT: return 3; + case BTN_WHEEL_BACK: return 4; /* Foot custom define */ + case BTN_WHEEL_FORWARD: return 5; /* Foot custom define */ + case BTN_WHEEL_LEFT: return 6; /* Foot custom define */ + case BTN_WHEEL_RIGHT: return 7; /* Foot custom define */ + case BTN_SIDE: return 8; + case BTN_EXTRA: return 9; + case BTN_FORWARD: return 10; + case BTN_BACK: return 11; + case BTN_TASK: return 12; /* Guessing... */ default: LOG_WARN("unrecognized mouse button: %d (0x%x)", button, button); From 1fd40760828ee2e2f159c3a8f7b19c3c348814dd Mon Sep 17 00:00:00 2001 From: abs3nt <abs3nt@asdf.cafe> Date: Sun, 14 Jul 2024 13:26:31 -0700 Subject: [PATCH 0782/1323] themes: catppuccin: replace with updated flavors Pulled from https://github.com/catppuccin/foot --- CHANGELOG.md | 4 +++- themes/catppuccin | 25 ------------------------- themes/catppuccin-frappe | 33 +++++++++++++++++++++++++++++++++ themes/catppuccin-latte | 33 +++++++++++++++++++++++++++++++++ themes/catppuccin-macchiato | 33 +++++++++++++++++++++++++++++++++ themes/catppuccin-mocha | 33 +++++++++++++++++++++++++++++++++ 6 files changed, 135 insertions(+), 26 deletions(-) delete mode 100644 themes/catppuccin create mode 100644 themes/catppuccin-frappe create mode 100644 themes/catppuccin-latte create mode 100644 themes/catppuccin-macchiato create mode 100644 themes/catppuccin-mocha diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a2d530c..7308eda1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,10 +94,12 @@ `BTN_FORWARD`. The default bindings have been updated, and `scrollback-up-mouse`, `scrollback-down-mouse`, `font-increase` and `font-decrease` now use the new button names. - + This change allow users to separate physical mouse buttons that _also_ generates `BTN_BACK` and `BTN_FORWARD`, from wheel scrolling ([#1763][1763]). +* Replaced the old catppuccin theme with updated flavored themes + pulled from [catppuccin/foot](https://github.com/catppuccin/foot) [1701]: https://codeberg.org/dnkl/foot/issues/1701 [1761]: https://codeberg.org/dnkl/foot/issues/1761 diff --git a/themes/catppuccin b/themes/catppuccin deleted file mode 100644 index 4ccfabec..00000000 --- a/themes/catppuccin +++ /dev/null @@ -1,25 +0,0 @@ -# -*- conf -*- -# Catppuccin - -[cursor] -color=1A1826 D9E0EE - -[colors] -foreground=D9E0EE -background=1E1D2F -regular0=6E6C7E # black -regular1=F28FAD # red -regular2=ABE9B3 # green -regular3=FAE3B0 # yellow -regular4=96CDFB # blue -regular5=F5C2E7 # magenta -regular6=89DCEB # cyan -regular7=D9E0EE # white -bright0=988BA2 # bright black -bright1=F28FAD # bright red -bright2=ABE9B3 # bright green -bright3=FAE3B0 # bright yellow -bright4=96CDFB # bright blue -bright5=F5C2E7 # bright magenta -bright6=89DCEB # bright cyan -bright7=D9E0EE # bright white diff --git a/themes/catppuccin-frappe b/themes/catppuccin-frappe new file mode 100644 index 00000000..3b2e0131 --- /dev/null +++ b/themes/catppuccin-frappe @@ -0,0 +1,33 @@ +# _*_ conf _*_ +# Catppuccin Frappe + +[colors] +foreground=c6d0f5 +background=303446 + +regular0=51576d +regular1=e78284 +regular2=a6d189 +regular3=e5c890 +regular4=8caaee +regular5=f4b8e4 +regular6=81c8be +regular7=b5bfe2 + +bright0=626880 +bright1=e78284 +bright2=a6d189 +bright3=e5c890 +bright4=8caaee +bright5=f4b8e4 +bright6=81c8be +bright7=a5adce + +selection-foreground=c6d0f5 +selection-background=4f5369 + +search-box-no-match=232634 e78284 +search-box-match=c6d0f5 414559 + +jump-labels=232634 ef9f76 +urls=8caaee diff --git a/themes/catppuccin-latte b/themes/catppuccin-latte new file mode 100644 index 00000000..8e545f70 --- /dev/null +++ b/themes/catppuccin-latte @@ -0,0 +1,33 @@ +# _*_ conf _*_ +# Catppuccin Latte + +[colors] +foreground=4c4f69 +background=eff1f5 + +regular0=5c5f77 +regular1=d20f39 +regular2=40a02b +regular3=df8e1d +regular4=1e66f5 +regular5=ea76cb +regular6=179299 +regular7=acb0be + +bright0=6c6f85 +bright1=d20f39 +bright2=40a02b +bright3=df8e1d +bright4=1e66f5 +bright5=ea76cb +bright6=179299 +bright7=bcc0cc + +selection-foreground=4c4f69 +selection-background=ccced7 + +search-box-no-match=dce0e8 d20f39 +search-box-match=4c4f69 ccd0da + +jump-labels=dce0e8 fe640b +urls=1e66f5 diff --git a/themes/catppuccin-macchiato b/themes/catppuccin-macchiato new file mode 100644 index 00000000..50aca7da --- /dev/null +++ b/themes/catppuccin-macchiato @@ -0,0 +1,33 @@ +# _*_ conf _*_ +# Catppuccin Macchiato + +[colors] +foreground=cad3f5 +background=24273a + +regular0=494d64 +regular1=ed8796 +regular2=a6da95 +regular3=eed49f +regular4=8aadf4 +regular5=f5bde6 +regular6=8bd5ca +regular7=b8c0e0 + +bright0=5b6078 +bright1=ed8796 +bright2=a6da95 +bright3=eed49f +bright4=8aadf4 +bright5=f5bde6 +bright6=8bd5ca +bright7=a5adcb + +selection-foreground=cad3f5 +selection-background=454a5f + +search-box-no-match=181926 ed8796 +search-box-match=cad3f5 363a4f + +jump-labels=181926 f5a97f +urls=8aadf4 diff --git a/themes/catppuccin-mocha b/themes/catppuccin-mocha new file mode 100644 index 00000000..508ca382 --- /dev/null +++ b/themes/catppuccin-mocha @@ -0,0 +1,33 @@ +# _*_ conf _*_ +# Catppuccin Mocha + +[colors] +foreground=cdd6f4 +background=1e1e2e + +regular0=45475a +regular1=f38ba8 +regular2=a6e3a1 +regular3=f9e2af +regular4=89b4fa +regular5=f5c2e7 +regular6=94e2d5 +regular7=bac2de + +bright0=585b70 +bright1=f38ba8 +bright2=a6e3a1 +bright3=f9e2af +bright4=89b4fa +bright5=f5c2e7 +bright6=94e2d5 +bright7=a6adc8 + +selection-foreground=cdd6f4 +selection-background=414356 + +search-box-no-match=11111b f38ba8 +search-box-match=cdd6f4 313244 + +jump-labels=11111b fab387 +urls=89b4fa From 4f25e1ba9ff136ed237a0c4632c11a65b537cbb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 18 Jul 2024 08:07:32 +0200 Subject: [PATCH 0783/1323] wayland: use wl_shm v2 if available --- wayland.c | 17 ++++++++++++++--- wayland.h | 3 +++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/wayland.c b/wayland.c index 29ffab60..77e00bfd 100644 --- a/wayland.c +++ b/wayland.c @@ -1120,9 +1120,16 @@ handle_global(void *data, struct wl_registry *registry, if (!verify_iface_version(interface, version, required)) return; +#if defined(WL_SHM_RELEASE_SINCE_VERSION) + const uint32_t preferred = WL_SHM_RELEASE_SINCE_VERSION; +#else + const uint32_t preferred = required; +#endif + wayl->shm = wl_registry_bind( - wayl->registry, name, &wl_shm_interface, required); + wayl->registry, name, &wl_shm_interface, min(version, preferred)); wl_shm_add_listener(wayl->shm, &shm_listener, wayl); + wayl->use_shm_release = version >= WL_SHM_RELEASE_SINCE_VERSION; } else if (streq(interface, xdg_wm_base_interface.name)) { @@ -1690,8 +1697,12 @@ wayl_destroy(struct wayland *wayl) wl_data_device_manager_destroy(wayl->data_device_manager); if (wayl->primary_selection_device_manager != NULL) zwp_primary_selection_device_manager_v1_destroy(wayl->primary_selection_device_manager); - if (wayl->shm != NULL) - wl_shm_destroy(wayl->shm); + if (wayl->shm != NULL) { + if (wayl->use_shm_release) + wl_shm_release(wayl->shm); + else + wl_shm_destroy(wayl->shm); + } if (wayl->sub_compositor != NULL) wl_subcompositor_destroy(wayl->sub_compositor); if (wayl->compositor != NULL) diff --git a/wayland.h b/wayland.h index 9577f08f..9db02d89 100644 --- a/wayland.h +++ b/wayland.h @@ -455,6 +455,9 @@ struct wayland { tll(struct seat) seats; tll(struct terminal *) terms; + + /* WL_SHM >= 2 */ + bool use_shm_release; }; struct wayland *wayl_init( From 36e4435bbfbb1a10724e0161f0ca1c0b8bf1bc7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 18 Jul 2024 08:41:44 +0200 Subject: [PATCH 0784/1323] log: respect the NO_COLOR environment variable http://no-color.org/ Closes #1771 --- CHANGELOG.md | 3 +++ log.c | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7308eda1..7b0e30ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,11 +65,14 @@ * Support for SGR 21 (double underline). * Support for `XTPUSHCOLORS`, `XTPOPCOLORS` and `XTREPORTCOLORS`, i.e. color palette stack ([#856][856]). +* Log output now respects the [`NO_COLOR`](http://no-color.org/) + environment variable ([#1771][1771]). [1707]: https://codeberg.org/dnkl/foot/issues/1707 [1738]: https://codeberg.org/dnkl/foot/issues/1738 [828]: https://codeberg.org/dnkl/foot/issues/828 [856]: https://codeberg.org/dnkl/foot/issues/856 +[1771]: https://codeberg.org/dnkl/foot/issues/1771 ### Changed diff --git a/log.c b/log.c index d19adaca..ebf411ec 100644 --- a/log.c +++ b/log.c @@ -40,7 +40,13 @@ log_init(enum log_colorize _colorize, bool _do_syslog, [LOG_FACILITY_DAEMON] = LOG_DAEMON, }; - colorize = _colorize == LOG_COLORIZE_ALWAYS || (_colorize == LOG_COLORIZE_AUTO && isatty(STDERR_FILENO)); + /* Don't use colors if NO_COLOR is defined and not empty */ + const char *no_color_str = getenv("NO_COLOR"); + const bool no_color = no_color_str != NULL && no_color_str[0] != '\0'; + + colorize = _colorize == LOG_COLORIZE_ALWAYS + || (_colorize == LOG_COLORIZE_AUTO + && !no_color && isatty(STDERR_FILENO)); do_syslog = _do_syslog; log_level = _log_level; From 065eb05e3e73034a873b20093388cba3b7f13954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 18 Jul 2024 09:02:42 +0200 Subject: [PATCH 0785/1323] meson/pgo: fix PGO build errors with recent meson(?) versions The back-reference to 'tokenize.c' in the parent directory causes PGO build failures, where gcc can't find the PGO data. Likely due to path/naming issues caused by meson's generated build directories. --- meson.build | 6 ++++++ tests/meson.build | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/meson.build b/meson.build index b2e2929b..868d8e36 100644 --- a/meson.build +++ b/meson.build @@ -252,6 +252,12 @@ pgolib = static_library( link_with: vtlib, ) +tokenize = static_library( + 'tokenizelib', + 'tokenize.c', + link_with: [common], +) + if get_option('b_pgo') == 'generate' executable( 'pgo', diff --git a/tests/meson.build b/tests/meson.build index 2b7768b0..9baa064c 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -1,8 +1,8 @@ config_test = executable( 'test-config', - 'test-config.c', '../tokenize.c', + 'test-config.c', wl_proto_headers, - link_with: [common], + link_with: [common, tokenize], dependencies: [pixman, xkb, fontconfig, wayland_client, fcft, tllist]) test('config', config_test) From 87aac8708d56e53919d8e2a79318293233a49b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 18 Jul 2024 09:04:39 +0200 Subject: [PATCH 0786/1323] foot.info: add setal (colored underlines) This is an alias to Setulc. Upstream (ncurses) terminfo uses setal instead of Setulc :/ --- foot.info | 1 + 1 file changed, 1 insertion(+) diff --git a/foot.info b/foot.info index b87df007..13f4403c 100644 --- a/foot.info +++ b/foot.info @@ -256,6 +256,7 @@ rs2=\E[!p\E[4l\E>, rv=\E\\[>1;[0-9][0-9][0-9][0-9][0-9][0-9];0c, sc=\E7, + setal=\E[58\:2\:\:%p1%{65536}%/%d\:%p1%{256}%/%{255}%&%d\:%p1%{255}%&%d%;m, setrgbb=\E[48\:2\:\:%p1%d\:%p2%d\:%p3%dm, setrgbf=\E[38\:2\:\:%p1%d\:%p2%d\:%p3%dm, sgr0=\E(B\E[m, From e11a4ab6af29cb841feaf2cf4785cfd1a3d709fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 18 Jul 2024 14:27:40 +0200 Subject: [PATCH 0787/1323] wayland: #ifdef guard code related to wl_shm_release() --- wayland.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/wayland.c b/wayland.c index 77e00bfd..04f50bda 100644 --- a/wayland.c +++ b/wayland.c @@ -1129,7 +1129,11 @@ handle_global(void *data, struct wl_registry *registry, wayl->shm = wl_registry_bind( wayl->registry, name, &wl_shm_interface, min(version, preferred)); wl_shm_add_listener(wayl->shm, &shm_listener, wayl); +#if defined(WL_SHM_RELEASE_SINCE_VERSION) wayl->use_shm_release = version >= WL_SHM_RELEASE_SINCE_VERSION; +#else + wayl->use_shm_release = false; +#endif } else if (streq(interface, xdg_wm_base_interface.name)) { @@ -1698,9 +1702,11 @@ wayl_destroy(struct wayland *wayl) if (wayl->primary_selection_device_manager != NULL) zwp_primary_selection_device_manager_v1_destroy(wayl->primary_selection_device_manager); if (wayl->shm != NULL) { +#if defined(WL_SHM_RELEASE_SINCE_VERSION) if (wayl->use_shm_release) wl_shm_release(wayl->shm); else +#endif wl_shm_destroy(wayl->shm); } if (wayl->sub_compositor != NULL) From 38461eef6f3b612277bf48e49b9cc6c3b79e25bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 30 Jun 2024 19:44:17 +0200 Subject: [PATCH 0788/1323] csi: in-band window resize notifications, private mode 2048 This implements https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83, in-band window resize notifications. When user enables private mode 2048 (in-band resize notifications), *always* send current size, even if the mode is already active. This ensures applications can rely on getting a reply from the terminal. --- CHANGELOG.md | 3 +++ csi.c | 10 ++++++++++ doc/foot-ctlseqs.7.scd | 3 +++ render.c | 2 ++ terminal.c | 35 +++++++++++++++++++++++++++++++++++ terminal.h | 7 +++++++ 6 files changed, 60 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b0e30ad..ce993718 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,9 @@ i.e. color palette stack ([#856][856]). * Log output now respects the [`NO_COLOR`](http://no-color.org/) environment variable ([#1771][1771]). +* Support for [in-band window resize + notifications](https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83), + private mode `2048`. [1707]: https://codeberg.org/dnkl/foot/issues/1707 [1738]: https://codeberg.org/dnkl/foot/issues/1738 diff --git a/csi.c b/csi.c index 1f61d3ce..2b9f137d 100644 --- a/csi.c +++ b/csi.c @@ -562,6 +562,13 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) term->grapheme_shaping = enable; break; + case 2048: + if (enable) + term_enable_size_notifications(term); + else + term_disable_size_notifications(term); + break; + case 8452: term->sixel.cursor_right_of_graphics = enable; break; @@ -649,6 +656,7 @@ decrqm(const struct terminal *term, unsigned param) case 2027: return term->conf->tweak.grapheme_width_method != GRAPHEME_WIDTH_DOUBLE ? DECRPM_PERMANENTLY_RESET : decrpm(term->grapheme_shaping); + case 2048: return decrpm(term->size_notifications); case 8452: return decrpm(term->sixel.cursor_right_of_graphics); case 737769: return decrpm(term_ime_is_enabled(term)); } @@ -693,6 +701,7 @@ xtsave(struct terminal *term, unsigned param) case 2004: term->xtsave.bracketed_paste = term->bracketed_paste; break; case 2026: term->xtsave.app_sync_updates = term->render.app_sync_updates.enabled; break; case 2027: term->xtsave.grapheme_shaping = term->grapheme_shaping; break; + case 2048: term->xtsave.size_notifications = term->size_notifications; break; case 8452: term->xtsave.sixel_cursor_right_of_graphics = term->sixel.cursor_right_of_graphics; break; case 737769: term->xtsave.ime = term_ime_is_enabled(term); break; } @@ -736,6 +745,7 @@ xtrestore(struct terminal *term, unsigned param) case 2004: enable = term->xtsave.bracketed_paste; break; case 2026: enable = term->xtsave.app_sync_updates; break; case 2027: enable = term->xtsave.grapheme_shaping; break; + case 2048: enable = term->xtsave.size_notifications; break; case 8452: enable = term->xtsave.sixel_cursor_right_of_graphics; break; case 737769: enable = term->xtsave.ime; break; diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index f73b5793..5c611c92 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -337,6 +337,9 @@ that corresponds to one of the following modes: | 2027 : contour : Grapheme cluster processing +| 2048 +: TODO +: In-band window resize notifications | 8452 : xterm : Position cursor to the right of sixels, instead of on the next line diff --git a/render.c b/render.c index 53c8c90e..ef135b53 100644 --- a/render.c +++ b/render.c @@ -4072,6 +4072,8 @@ tiocswinsz(struct terminal *term) { LOG_ERRNO("TIOCSWINSZ"); } + + term_send_size_notification(term); } } diff --git a/terminal.c b/terminal.c index 6e83d9a0..e1da77f5 100644 --- a/terminal.c +++ b/terminal.c @@ -44,6 +44,7 @@ #include "util.h" #include "vt.h" #include "xmalloc.h" +#include "xsnprintf.h" #define PTMX_TIMING 0 @@ -4240,3 +4241,37 @@ term_set_user_mouse_cursor(struct terminal *term, const char *cursor) : NULL; term_xcursor_update(term); } + +void +term_enable_size_notifications(struct terminal *term) +{ + /* Note: always send current size upon activation, regardless of + previous state */ + term->size_notifications = true; + term_send_size_notification(term); +} + +void +term_disable_size_notifications(struct terminal *term) +{ + if (!term->size_notifications) + return; + + term->size_notifications = false; +} + +void +term_send_size_notification(struct terminal *term) +{ + if (!term->size_notifications) + return; + + const int height = term->height - term->margins.top - term->margins.bottom; + const int width = term->width - term->margins.left - term->margins.right; + + char buf[128]; + const int n = xsnprintf( + buf, sizeof(buf), "\033[48;%d;%d;%d;%dt", + term->rows, term->cols, height, width); + term_to_slave(term, buf, n); +} diff --git a/terminal.h b/terminal.h index 926870c1..44a101eb 100644 --- a/terminal.h +++ b/terminal.h @@ -541,6 +541,8 @@ struct terminal { bool app_sync_updates:1; bool grapheme_shaping:1; + bool size_notifications:1; + bool sixel_display_mode:1; bool sixel_private_palette:1; bool sixel_cursor_right_of_graphics:1; @@ -800,6 +802,7 @@ struct terminal { char *cwd; bool grapheme_shaping; + bool size_notifications; }; struct config; @@ -946,6 +949,10 @@ void term_osc8_close(struct terminal *term); bool term_ptmx_pause(struct terminal *term); bool term_ptmx_resume(struct terminal *term); +void term_enable_size_notifications(struct terminal *term); +void term_disable_size_notifications(struct terminal *term); +void term_send_size_notification(struct terminal *term); + static inline void term_reset_grapheme_state(struct terminal *term) { #if defined(FOOT_GRAPHEME_CLUSTERING) From 45c7cd3f7472b11d50622b10867a329b4d8e2451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 18 Jul 2024 08:08:44 +0200 Subject: [PATCH 0789/1323] input: allow mouse selections to start inside the margins Before this, margins were special cased: * The mouse cursor was always a pointer, and never an I-beam (thus signaling selections cannot be made). * The internal mouse coords where set to -1 when the cursor was inside the margins, causing: - text selections from being made - mouse events being passed to mouse grabbing applications In particular, even with a one-pixel margin, making selections was unnecessarily hard in e.g. fullscreen mode, where you'd expect to be able to throw the cursor into the corner of the screen and then start a selection. With this patch, the cursor is treated as if it was in the first/last column/row, when inside the margin(s). An unintended side-effect of this, initially, was that auto-scrolling selections where way too easy to trigger, since part of its logic is checking if the cursor is inside the margins. That problem has been reduced by two things: * auto-scrolling does not occur unless a selection has been started. That is, just holding down the mouse in the margins and moving up/down doesn't cause scrolling. You have to first select at least one cell in the visible viewport. * A selection isn't fully started (i.e. a cell is actually selected) unless the cursor is inside the actual grid, and *not* in the margins. What does the last point mean? We now allow a selection to be _started_ when clicking in the margin. What this means internally is we set a start coordinate for a selection, but *not* and end coordinate. At this point, we don't have an actual selection. Nothing is selected, and no cells are highlighted, graphically. This happens when we set an end coordinate. Without the last bullet point, that would happen as soon as the cursor was _moved_, even if still inside the margins. Now, we require the cursor to leave the margins and touch an actual cell before we set an end coordinate. Closes #1702 --- CHANGELOG.md | 3 ++ input.c | 82 +++++++++++++++++----------------------------------- terminal.c | 5 +--- 3 files changed, 31 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce993718..dfa2b9ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,10 +106,13 @@ ([#1763][1763]). * Replaced the old catppuccin theme with updated flavored themes pulled from [catppuccin/foot](https://github.com/catppuccin/foot) +* Mouse selections can now be started inside the margins + ([#1702][1702]). [1701]: https://codeberg.org/dnkl/foot/issues/1701 [1761]: https://codeberg.org/dnkl/foot/issues/1761 [1763]: https://codeberg.org/dnkl/foot/issues/1763 +[1702]: https://codeberg.org/dnkl/foot/issues/1702 ### Deprecated diff --git a/input.c b/input.c index 68cb9314..dc588f75 100644 --- a/input.c +++ b/input.c @@ -1860,14 +1860,17 @@ mouse_coord_pixel_to_cell(struct seat *seat, const struct terminal *term, * if the cursor is outside the grid. I.e. if it is inside the * margins. */ - - if (x < term->margins.left || x >= term->width - term->margins.right) - seat->mouse.col = -1; + if (x < term->margins.left) + seat->mouse.col = 0; + else if (x >= term->width - term->margins.right) + seat->mouse.col = term->cols - 1; else seat->mouse.col = (x - term->margins.left) / term->cell_width; - if (y < term->margins.top || y >= term->height - term->margins.bottom) - seat->mouse.row = -1; + if (y < term->margins.top) + seat->mouse.row = 0; + else if (y >= term->height - term->margins.bottom) + seat->mouse.row = term->rows - 1; else seat->mouse.row = (y - term->margins.top) / term->cell_height; } @@ -2127,49 +2130,10 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, int old_col = seat->mouse.col; int old_row = seat->mouse.row; - /* - * While the seat's mouse coordinates must always be on the - * grid, or -1, we allow updating the selection even when the - * mouse is outside the grid (could also be outside the - * terminal window). - */ - int selection_col; - int selection_row; + mouse_coord_pixel_to_cell(seat, term, seat->mouse.x, seat->mouse.y); - if (x < term->margins.left) { - seat->mouse.col = -1; - selection_col = 0; - } else if (x >= term->width - term->margins.right) { - seat->mouse.col = -1; - selection_col = term->cols - 1; - } else { - seat->mouse.col = (x - term->margins.left) / term->cell_width; - selection_col = seat->mouse.col; - } - - if (y < term->margins.top) { - seat->mouse.row = -1; - selection_row = 0; - } else if (y >= term->height - term->margins.bottom) { - seat->mouse.row = -1; - selection_row = term->rows - 1; - } else { - seat->mouse.row = (y - term->margins.top) / term->cell_height; - selection_row = seat->mouse.row; - } - - /* - * If client is receiving events (because the button was - * pressed while the cursor was inside the grid area), then - * make sure it receives valid coordinates. - */ - if (send_to_client) { - seat->mouse.col = selection_col; - seat->mouse.row = selection_row; - } - - xassert(seat->mouse.col == -1 || (seat->mouse.col >= 0 && seat->mouse.col < term->cols)); - xassert(seat->mouse.row == -1 || (seat->mouse.row >= 0 && seat->mouse.row < term->rows)); + xassert(seat->mouse.col >= 0 && seat->mouse.col < term->cols); + xassert(seat->mouse.row >= 0 && seat->mouse.row < term->rows); /* Cursor has moved to a different cell since last time */ bool cursor_is_on_new_cell @@ -2186,9 +2150,13 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, const bool cursor_is_on_grid = seat->mouse.col >= 0 && seat->mouse.row >= 0; enum selection_scroll_direction auto_scroll_direction - = y < term->margins.top ? SELECTION_SCROLL_UP - : y > term->height - term->margins.bottom ? SELECTION_SCROLL_DOWN - : SELECTION_SCROLL_NOT; + = term->selection.coords.end.row < 0 + ? SELECTION_SCROLL_NOT + : y < term->margins.top + ? SELECTION_SCROLL_UP + : y > term->height - term->margins.bottom + ? SELECTION_SCROLL_DOWN + : SELECTION_SCROLL_NOT; if (auto_scroll_direction == SELECTION_SCROLL_NOT) selection_stop_scroll_timer(term); @@ -2220,14 +2188,18 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, selection_start_scroll_timer( term, 400000000 / (divisor > 0 ? divisor : 1), - auto_scroll_direction, selection_col); + auto_scroll_direction, seat->mouse.col); } - if (term->selection.ongoing && ( - cursor_is_on_new_cell || - term->selection.coords.end.row < 0)) + if (term->selection.ongoing && + (cursor_is_on_new_cell || + (term->selection.coords.end.row < 0 && + seat->mouse.x >= term->margins.left && + seat->mouse.x < term->width - term->margins.right && + seat->mouse.y >= term->margins.top && + seat->mouse.y < term->height - term->margins.bottom))) { - selection_update(term, selection_col, selection_row); + selection_update(term, seat->mouse.col, seat->mouse.row); } } diff --git a/terminal.c b/terminal.c index e1da77f5..35cfe8aa 100644 --- a/terminal.c +++ b/terminal.c @@ -3451,10 +3451,7 @@ term_xcursor_update_for_seat(struct terminal *term, struct seat *seat) shape = CURSOR_SHAPE_CUSTOM; } - else if (seat->mouse.col >= 0 && - seat->mouse.row >= 0 && - term_mouse_grabbed(term, seat)) - { + else if (term_mouse_grabbed(term, seat)) { shape = CURSOR_SHAPE_TEXT; } From b0bf8ca5f7034f6d95bc3b58082e719a81d99859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 19 Jul 2024 15:04:28 +0200 Subject: [PATCH 0790/1323] osc/notify: add support for OSC-99, kitty desktop notifications This adds limited support for OSC-99, kitty desktop notifications[^1]. We support everything defined by the "protocol", except: * 'a': action to perform on notification activation. Since we don't trigger the notification ourselves (over D-Bus), we don't know a) which ID the notification got, or b) when it is clicked. * ... and that's it. Everything else is supported To be explicit, we *do* support: * Chunked notifications (d=0|1), allowing the application to append data to a notification in chunks, before it's finally displayed. * Plain UTF-8, or base64-encoded UTF-8 payload (e=0|1). * Notification identifier (i=xyz). * Payload type (p=title|body). * When to honor the notification (o=always|unfocused|invisible), with the following quirks: - we don't know when the window is invisible, thus it's treated as 'unfocused'. - the foot option 'notify-focus-inhibit' overrides 'always' * Urgency (u=0|1|2) [^1]: https://sw.kovidgoyal.net/kitty/desktop-notifications/ --- CHANGELOG.md | 2 + config.c | 5 +- doc/foot.ini.5.scd | 2 +- foot.ini | 2 +- notify.c | 21 +++- notify.h | 28 ++++- osc.c | 249 ++++++++++++++++++++++++++++++++++++++++++++- pgo/pgo.c | 4 +- terminal.c | 21 +++- terminal.h | 3 + 10 files changed, 322 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfa2b9ad..057924fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,8 @@ * Support for [in-band window resize notifications](https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83), private mode `2048`. +* Support for OSC-99 [_"Kitty desktop + notifications"_](https://sw.kovidgoyal.net/kitty/desktop-notifications/). [1707]: https://codeberg.org/dnkl/foot/issues/1707 [1738]: https://codeberg.org/dnkl/foot/issues/1738 diff --git a/config.c b/config.c index 5d9a0b75..d34df132 100644 --- a/config.c +++ b/config.c @@ -3184,8 +3184,9 @@ config_load(struct config *conf, const char *conf_path, 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); + tokenize_cmdline( + "notify-send -a ${app-id} -i ${app-id} -u ${urgency} ${title} ${body}", + &conf->notify.argv.args); tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args); static const char32_t *url_protocols[] = { diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 680768e0..fe6ff57b 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -360,7 +360,7 @@ empty string to be set, but it must be quoted: *KEY=""*) By default, notifications are *inhibited* if the foot window has keyboard focus. See _notify-focus-inhibit_. - Default: _notify-send -a ${app-id} -i ${app-id} ${title} ${body}_. + Default: _notify-send -a ${app-id} -i ${app-id} -u ${urgency} ${title} ${body}_. *notify-focus-inhibit* Boolean. If enabled, foot will not display notifications if the diff --git a/foot.ini b/foot.ini index 7ae9ba1b..7a1db9ba 100644 --- a/foot.ini +++ b/foot.ini @@ -29,7 +29,7 @@ # resize-by-cells=yes # resize-delay-ms=100 -# notify=notify-send -a ${app-id} -i ${app-id} ${title} ${body} +# notify=notify-send -a ${app-id} -i ${app-id} -u ${urgency} ${title} ${body} # bold-text-in-bright=no # word-delimiters=,│`|:"'()[]{}<> diff --git a/notify.c b/notify.c index 7a208479..09e230a0 100644 --- a/notify.c +++ b/notify.c @@ -12,14 +12,18 @@ #include "log.h" #include "config.h" #include "spawn.h" +#include "terminal.h" #include "xmalloc.h" void -notify_notify(const struct terminal *term, const char *title, const char *body) +notify_notify(const struct terminal *term, const char *title, const char *body, + enum notify_when when, enum notify_urgency urgency) { LOG_DBG("notify: title=\"%s\", msg=\"%s\"", title, body); - if (term->conf->notify_focus_inhibit && term->kbd_focus) { + if ((term->conf->notify_focus_inhibit || when != NOTIFY_ALWAYS) + && term->kbd_focus) + { /* No notifications while we're focused */ return; } @@ -33,10 +37,17 @@ notify_notify(const struct terminal *term, const char *title, const char *body) char **argv = NULL; size_t argc = 0; + const char *urgency_str = + urgency == NOTIFY_URGENCY_LOW + ? "low" + : urgency == NOTIFY_URGENCY_NORMAL + ? "normal" : "critical"; + if (!spawn_expand_template( - &term->conf->notify, 4, - (const char *[]){"app-id", "window-title", "title", "body"}, - (const char *[]){term->app_id ? term->app_id : term->conf->app_id, term->window_title, title, body}, + &term->conf->notify, 5, + (const char *[]){"app-id", "window-title", "title", "body", "urgency"}, + (const char *[]){term->app_id ? term->app_id : term->conf->app_id, + term->window_title, title, body, urgency_str}, &argc, &argv)) { return; diff --git a/notify.h b/notify.h index ce60562f..be79b41a 100644 --- a/notify.h +++ b/notify.h @@ -1,6 +1,30 @@ #pragma once +#include <stdbool.h> -#include "terminal.h" +struct terminal; + +enum notify_when { + NOTIFY_ALWAYS, + NOTIFY_UNFOCUSED, + NOTIFY_INVISIBLE +}; + +enum notify_urgency { + NOTIFY_URGENCY_LOW, + NOTIFY_URGENCY_NORMAL, + NOTIFY_URGENCY_CRITICAL, +}; + +struct kitty_notification { + char *id; + char *title; + char *body; + enum notify_when when; + enum notify_urgency urgency; + bool focus; + bool report; +}; void notify_notify( - const struct terminal *term, const char *title, const char *body); + const struct terminal *term, const char *title, const char *body, + enum notify_when when, enum notify_urgency urgency); diff --git a/osc.c b/osc.c index a3dc1715..a9a83df9 100644 --- a/osc.c +++ b/osc.c @@ -560,7 +560,250 @@ osc_notify(struct terminal *term, char *string) return; } - notify_notify(term, title, msg != NULL ? msg : ""); + notify_notify( + term, title, msg != NULL ? msg : "", + NOTIFY_ALWAYS, NOTIFY_URGENCY_NORMAL); +} + +static void +kitty_notification(struct terminal *term, char *string) +{ + /* https://sw.kovidgoyal.net/kitty/desktop-notifications */ + + char *payload = strchr(string, ';'); + if (payload == NULL) { + LOG_ERR("OSC-99: payload missing"); + return; + } + + char *parameters = string; + *payload = '\0'; + payload++; + + char *id = xstrdup("0"); /* The 'i' parameter */ + bool focus = true; /* The 'a' parameter */ + bool report = false; /* The 'a' parameter */ + bool done = true; /* The 'd' parameter */ + bool base64 = false; /* The 'e' parameter */ + bool payload_is_title = true; /* The 'p' parameter */ + + enum notify_when when = NOTIFY_ALWAYS; + enum notify_urgency urgency = NOTIFY_URGENCY_NORMAL; + + bool have_a = false; + bool have_o = false; + bool have_u = false; + + char *ctx = NULL; + for (char *param = strtok_r(parameters, ":", &ctx); + param != NULL; + param = strtok_r(NULL, ":", &ctx)) + { + /* All parameters are on the form X=value, where X is always + exactly one character */ + if (param[0] == '\0' || param[1] != '=') { + LOG_WARN("OSC-99: invalid parameter: \"%s\"", param); + continue; + } + + char *value = ¶m[2]; + + switch (param[0]) { + case 'a': { + /* notification activation action: focus|report|-focus|-report */ + have_a = true; + char *a_ctx = NULL; + for (const char *v = strtok_r(value, ",", &a_ctx); + v != NULL; + v = strtok_r(NULL, ",", &a_ctx)) + { + LOG_WARN(" a: \"%s\"", v); + bool reverse = v[0] == '-'; + if (reverse) + v++; + + if (strcmp(v, "focus") == 0) { + focus = !reverse; + if (focus) + LOG_WARN("unimplemented: OSC-99: focus on notification activation"); + } else if (strcmp(v, "report") == 0) { + report = !reverse; + if (report) + LOG_WARN("unimplemented: OSC-99: report on notification activation"); + } else + LOG_WARN("OSC-99: unrecognized value for 'a': \"%s\", ignoring", v); + } + + break; + } + + case 'd': + /* done: 0|1 */ + if (value[0] == '0' && value[1] == '\0') + done = false; + else if (value[0] == '1' && value[1] == '\0') + done = true; + else + LOG_WARN("OSC-99: unrecognized value for 'd': \"%s\", ignoring", value); + break; + + case 'e': + /* base64: 0=utf8, 1=base64(utf8) */ + if (value[0] == '0' && value[1] == '\0') + base64 = false; + else if (value[0] == '1' && value[1] == '\0') + base64 = true; + else + LOG_WARN("OSC-99: unrecognized value for 'e': \"%s\", ignoring", value); + break; + + case 'i': + /* id */ + free(id); + id = xstrdup(value); + break; + + case 'p': + /* payload content: title|body */ + if (strcmp(value, "title") == 0) + payload_is_title = true; + else if (strcmp(value, "body") == 0) + payload_is_title = false; + else + LOG_WARN("OSC-99: unrecognized value for 'p': \"%s\", ignoring", value); + break; + + case 'o': + /* honor when: always|unfocused|invisible */ + have_o = true; + if (strcmp(value, "always") == 0) + when = NOTIFY_ALWAYS; + else if (strcmp(value, "unfocused") == 0) + when = NOTIFY_UNFOCUSED; + else if (strcmp(value, "invisible") == 0) + when = NOTIFY_INVISIBLE; + else + LOG_WARN("OSC-99: unrecognized value for 'o': \"%s\", ignoring", value); + break; + + case 'u': + /* urgency: 0=low, 1=normal, 2=critical */ + have_u = true; + if (value[0] == '0' && value[1] == '\0') + urgency = NOTIFY_URGENCY_LOW; + else if (value[0] == '1' && value[1] == '\0') + urgency = NOTIFY_URGENCY_NORMAL; + else if (value[0] == '2' && value[1] == '\0') + urgency = NOTIFY_URGENCY_CRITICAL; + else + LOG_WARN("OSC-99: unrecognized value for 'u': \"%s\", ignoring", value); + break; + + default: + LOG_WARN("OSC-99: unrecognized parameter: \"%s\", ignoring", param); + break; + } + } + + if (base64) + payload = base64_decode(payload); + else + payload = xstrdup(payload); + + LOG_DBG("id=%s, done=%d, focus=%d, report=%d, base64=%d, payload: %s, " + "honor: %s, urgency: %s, %s: %s", + id, done, focus, report, base64, + payload_is_title ? "title" : "body", + (when == NOTIFY_ALWAYS + ? "always" + : when == NOTIFY_UNFOCUSED + ? "unfocused" + : "invisible"), + (urgency == NOTIFY_URGENCY_LOW + ? "low" : urgency == NOTIFY_URGENCY_NORMAL + ? "normal" + : "critical"), + payload_is_title ? "title" : "body", payload); + + /* Search for an existing (d=0) notification to update */ + struct kitty_notification *notif = NULL; + tll_foreach(term->kitty_notifications, it) { + if (strcmp(it->item.id, id) == 0) { + /* Found existing notification */ + LOG_WARN("found existing kitty notification"); + notif = &it->item; + break; + } + } + + if (notif == NULL) { + /* Somewhat unoptimized... this will be free:d and removed + immediately if d=1 */ + tll_push_front(term->kitty_notifications, ((struct kitty_notification){ + .id = id, + .title = NULL, + .body = NULL, + .when = when, + .urgency = urgency, + .focus = focus, + .report = report, + })); + + id = NULL; /* Prevent double free */ + notif = &tll_front(term->kitty_notifications); + } + + /* Update notification metadata */ + if (have_a) { + notif->focus = focus; + notif->report = report; + } + + if (have_o) + notif->when = when; + if (have_u) + notif->urgency = urgency; + + if (payload_is_title) { + if (notif->title == NULL) { + notif->title = payload; + payload = NULL; + } else { + char *new_title = xasprintf("%s%s", notif->title, payload); + free(notif->title); + notif->title = new_title; + } + } else { + if (notif->body == NULL) { + notif->body = payload; + payload = NULL; + } else { + char *new_body = xasprintf("%s%s", notif->body, payload); + free(notif->body); + notif->body = new_body; + } + } + + free(id); + free(payload); + + if (done) { + notify_notify( + term, + notif->title != NULL ? notif->title : "", + notif->body != NULL ? notif->body : "", + notif->when, notif->urgency); + + tll_foreach(term->kitty_notifications, it) { + if (&it->item == notif) { + free(it->item.id); + free(it->item.title); + free(it->item.body); + tll_remove(term->kitty_notifications, it); + break; + } + } + } } void @@ -780,6 +1023,10 @@ osc_dispatch(struct terminal *term) osc_selection(term, string); break; + case 99: /* Kitty notifications */ + kitty_notification(term, string); + break; + case 104: { /* Reset Color Number 'c' (whole table if no parameter) */ diff --git a/pgo/pgo.c b/pgo/pgo.c index 204c024d..e8080521 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -152,7 +152,9 @@ void ime_disable(struct seat *seat) {} void ime_reset_preedit(struct seat *seat) {} void -notify_notify(const struct terminal *term, const char *title, const char *body) + notify_notify( + const struct terminal *term, const char *title, const char *body, + enum notify_when when, enum notify_urgency urgency) { } diff --git a/terminal.c b/terminal.c index 35cfe8aa..712b9d4e 100644 --- a/terminal.c +++ b/terminal.c @@ -1313,6 +1313,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED .ime_enabled = true, #endif + .kitty_notifications = tll_init(), }; pixman_region32_init(&term->render.last_overlay_clip); @@ -1817,6 +1818,13 @@ term_destroy(struct terminal *term) tll_remove(term->ptmx_paste_buffers, it); } + tll_foreach(term->kitty_notifications, it) { + free(it->item.id); + free(it->item.body); + free(it->item.title); + tll_remove(term->kitty_notifications, it); + } + sixel_fini(term); term_ime_reset(term); @@ -2022,6 +2030,13 @@ term_reset(struct terminal *term, bool hard) tll_remove(term->alt.sixel_images, it); } + tll_foreach(term->kitty_notifications, it) { + free(it->item.id); + free(it->item.title); + free(it->item.body); + tll_remove(term->kitty_notifications, it); + } + term->grapheme_shaping = term->conf->tweak.grapheme_shaping; #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED @@ -3566,8 +3581,10 @@ term_bell(struct terminal *term) } } - if (term->conf->bell.notify) - notify_notify(term, "Bell", "Bell in terminal"); + if (term->conf->bell.notify) { + notify_notify(term, "Bell", "Bell in terminal", + NOTIFY_ALWAYS, NOTIFY_URGENCY_NORMAL); + } if (term->conf->bell.flash) term_flash(term, 100); diff --git a/terminal.h b/terminal.h index 44a101eb..65dde151 100644 --- a/terminal.h +++ b/terminal.h @@ -20,6 +20,7 @@ #include "fdm.h" #include "key-binding.h" #include "macros.h" +#include "notify.h" #include "reaper.h" #include "shm.h" #include "wayland.h" @@ -798,6 +799,8 @@ struct terminal { void *cb_data; } shutdown; + tll(struct kitty_notification) kitty_notifications; + char *foot_exe; char *cwd; From 57af75f98877220a2e8827bb3cb32c780df25d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 19 Jul 2024 15:26:08 +0200 Subject: [PATCH 0791/1323] osc: kitty notifications: use body as title, if no title is set This mirrors kitty's behavior; if the user didn't set a title, but did set the body/text, use the body as title instead. --- osc.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osc.c b/osc.c index a9a83df9..7485cada 100644 --- a/osc.c +++ b/osc.c @@ -790,8 +790,8 @@ kitty_notification(struct terminal *term, char *string) if (done) { notify_notify( term, - notif->title != NULL ? notif->title : "", - notif->body != NULL ? notif->body : "", + notif->title != NULL ? notif->title : notif->body, + notif->title != NULL && notif->body != NULL ? notif->body : "", notif->when, notif->urgency); tll_foreach(term->kitty_notifications, it) { From a42f99081876dd02e28b931f7c0e70230355bb58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 06:57:30 +0200 Subject: [PATCH 0792/1323] spawn: add optional reaper callback, return pid_t This will allow spawn() callers to do things when the spawned process has terminated. --- input.c | 4 ++-- notify.c | 6 +++++- spawn.c | 14 +++++++------- spawn.h | 8 +++++--- terminal.c | 8 ++++---- url-mode.c | 5 +++-- 6 files changed, 26 insertions(+), 19 deletions(-) diff --git a/input.c b/input.c index dc588f75..98f5c89c 100644 --- a/input.c +++ b/input.c @@ -306,8 +306,8 @@ execute_binding(struct seat *seat, struct terminal *term, } } - if (!spawn(term->reaper, term->cwd, binding->aux->pipe.args, - pipe_fd[0], stdout_fd, stderr_fd, NULL)) + if (spawn(term->reaper, term->cwd, binding->aux->pipe.args, + pipe_fd[0], stdout_fd, stderr_fd, NULL, NULL, NULL) < 0) goto pipe_err; /* Close read end */ diff --git a/notify.c b/notify.c index 09e230a0..7cd22ccb 100644 --- a/notify.c +++ b/notify.c @@ -59,7 +59,11 @@ notify_notify(const struct terminal *term, const char *title, const char *body, /* Redirect stdin to /dev/null, but ignore failure to open */ int devnull = open("/dev/null", O_RDONLY); - spawn(term->reaper, NULL, argv, devnull, -1, -1, NULL); + pid_t pid = spawn( + term->reaper, NULL, argv, devnull, stdout_fds[1], -1, + ¬if_done, (void *)term, NULL); + + if (stdout_fds[1] >= 0) { if (devnull >= 0) close(devnull); diff --git a/spawn.c b/spawn.c index 6935a29a..17c821b5 100644 --- a/spawn.c +++ b/spawn.c @@ -15,9 +15,9 @@ #include "debug.h" #include "xmalloc.h" -bool +pid_t spawn(struct reaper *reaper, const char *cwd, char *const argv[], - int stdin_fd, int stdout_fd, int stderr_fd, + int stdin_fd, int stdout_fd, int stderr_fd, reaper_cb cb, void *cb_data, const char *xdg_activation_token) { int pipe_fds[2] = {-1, -1}; @@ -104,16 +104,16 @@ spawn(struct reaper *reaper, const char *cwd, char *const argv[], close(pipe_fds[0]); if (ret == 0) { - reaper_add(reaper, pid, NULL, NULL); - return true; + reaper_add(reaper, pid, cb, cb_data); + return pid; } else if (ret < 0) { LOG_ERRNO("failed to read from pipe"); - return false; + return -1; } else { LOG_ERRNO_P(errno_copy, "%s: failed to spawn", argv[0]); errno = errno_copy; waitpid(pid, NULL, 0); - return false; + return -1; } err: @@ -121,7 +121,7 @@ err: close(pipe_fds[0]); if (pipe_fds[1] != -1) close(pipe_fds[1]); - return false; + return -1; } bool diff --git a/spawn.h b/spawn.h index 0fc95041..1693f1a8 100644 --- a/spawn.h +++ b/spawn.h @@ -1,12 +1,14 @@ #pragma once #include <stdbool.h> +#include <unistd.h> + #include "config.h" #include "reaper.h" -bool spawn(struct reaper *reaper, const char *cwd, char *const argv[], - int stdin_fd, int stdout_fd, int stderr_fd, - const char *xdg_activation_token); +pid_t spawn(struct reaper *reaper, const char *cwd, char *const argv[], + int stdin_fd, int stdout_fd, int stderr_fd, + reaper_cb cb, void *cb_data, const char *xdg_activation_token); bool spawn_expand_template( const struct config_spawn_template *template, diff --git a/terminal.c b/terminal.c index 712b9d4e..bf0c78dc 100644 --- a/terminal.c +++ b/terminal.c @@ -198,7 +198,7 @@ add_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) return true; char *const argv[] = {conf->utmp_helper_path, UTMP_ADD, getenv("WAYLAND_DISPLAY"), NULL}; - return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL); + return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL, NULL, NULL) >= 0; #else return true; #endif @@ -222,7 +222,7 @@ del_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) ; char *const argv[] = {conf->utmp_helper_path, UTMP_DEL, del_argument, NULL}; - return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL); + return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL, NULL, NULL) >= 0; #else return true; #endif @@ -3594,7 +3594,7 @@ term_bell(struct terminal *term) { int devnull = open("/dev/null", O_RDONLY); spawn(term->reaper, NULL, term->conf->bell.command.argv.args, - devnull, -1, -1, NULL); + devnull, -1, -1, NULL, NULL, NULL); if (devnull >= 0) close(devnull); @@ -3606,7 +3606,7 @@ term_spawn_new(const struct terminal *term) { return spawn( term->reaper, term->cwd, (char *const []){term->foot_exe, NULL}, - -1, -1, -1, NULL); + -1, -1, -1, NULL, NULL, NULL) >= 0; } void diff --git a/url-mode.c b/url-mode.c index 57f47dd0..c6340e94 100644 --- a/url-mode.c +++ b/url-mode.c @@ -74,8 +74,9 @@ spawn_url_launcher_with_token(struct terminal *term, (const char *[]){url}, &argc, &argv)) { - ret = spawn(term->reaper, term->cwd, argv, - dev_null, dev_null, dev_null, xdg_activation_token); + ret = spawn( + term->reaper, term->cwd, argv, + dev_null, dev_null, dev_null, NULL, NULL, xdg_activation_token) >= 0; for (size_t i = 0; i < argc; i++) free(argv[i]); From 69f56b86b7ad30a028917121783ea51e9fe9c3a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 06:58:37 +0200 Subject: [PATCH 0793/1323] wayland: add wayl_activate() wayl_activate() takes an XDG activation token and does an XDG activation request. --- wayland.c | 15 +++++++++++++-- wayland.h | 2 ++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/wayland.c b/wayland.c index 04f50bda..3f65901b 100644 --- a/wayland.c +++ b/wayland.c @@ -1824,8 +1824,7 @@ wayl_win_init(struct terminal *term, const char *token) wl_surface_commit(win->surface.surf); /* Complete XDG startup notification */ - if (token && wayl->xdg_activation != NULL) - xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface.surf); + wayl_activate(wayl, win, token); if (!wayl_win_subsurface_new(win, &win->overlay, false)) { LOG_ERR("failed to create overlay surface"); @@ -2377,3 +2376,15 @@ wayl_get_activation_token( xdg_activation_token_v1_commit(token); return true; } + +void +wayl_activate(struct wayland *wayl, struct wl_window *win, const char *token) +{ + if (wayl->xdg_activation == NULL) + return; + + if (token == NULL) + return; + + xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface.surf); +} diff --git a/wayland.h b/wayland.h index 9db02d89..ca9c05fa 100644 --- a/wayland.h +++ b/wayland.h @@ -499,3 +499,5 @@ void wayl_win_subsurface_destroy(struct wayl_sub_surface *surf); bool wayl_get_activation_token( struct wayland *wayl, struct seat *seat, uint32_t serial, struct wl_window *win, activation_token_cb_t cb, void *cb_data); +void wayl_activate(struct wayland *wayl, struct wl_window *win, const char *token); + From 12152a8ae42951dc58e654b512a7901e0a857eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 06:59:14 +0200 Subject: [PATCH 0794/1323] unicode-mode: disable debug logging --- unicode-mode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unicode-mode.c b/unicode-mode.c index b902b5f4..1acdc664 100644 --- a/unicode-mode.c +++ b/unicode-mode.c @@ -1,7 +1,7 @@ #include "unicode-mode.h" #define LOG_MODULE "unicode-input" -#define LOG_ENABLE_DBG 1 +#define LOG_ENABLE_DBG 0 #include "log.h" #include "render.h" #include "search.h" From 5905ea0d84530c85899292e38be4cd98156f7cbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 06:59:46 +0200 Subject: [PATCH 0795/1323] osc: kitty notifications: implement focus|report This patch adds support for window focusing, and sending events back to the client application when a notification is closed. * Refactor notification related configuration options: - add desktop-notifications sub-section - deprecate 'notify' in favor of 'desktop-notifications.command' - deprecate 'notify-focus-inhibit' in favor of 'desktop-notifications.inhibit-when-focused' * Refactor: rename 'struct kitty_notification' to 'struct notification' * Pass a 'struct notification' to notify_notify(), instead of many arguments. * notify_notify() now registers a reaper callback. When the notifier process has terminated, the notification is considered closed, and we either try to focus (activate) the window, or send an event to the client application, depending on the notification setting. * For the window activation, we need an XDG activation token. For now, assume *everything* written on stdout is part of the token. * Refactor: remove much of the warnings from OSC-99; we don't typically log anything when an OSC/CSI has invalid values. * Add icon support to OSC-99. This isn't part of the upstream spec. Foot's implementation: - uses the 'I' parameter - the value is expected to be a symbolic icon name - a quick check for absolute paths is done, and such icon requests are ignored. * Added ${icon} to the 'desktop-notifications.command' template. Uses the icon specified in the notification, or ${app-id} if not set. --- config.c | 63 +++++++++++--- config.h | 6 +- doc/foot-ctlseqs.7.scd | 7 +- doc/foot.ini.5.scd | 109 +++++++++++++++++------- foot.ini | 7 +- notify.c | 185 +++++++++++++++++++++++++++++++++++++---- notify.h | 21 +++-- osc.c | 103 +++++++++++------------ terminal.c | 23 +++-- terminal.h | 4 +- tests/test-config.c | 19 ++++- 11 files changed, 410 insertions(+), 137 deletions(-) diff --git a/config.c b/config.c index d34df132..a624f95c 100644 --- a/config.c +++ b/config.c @@ -1024,11 +1024,29 @@ parse_section_main(struct context *ctx) else if (streq(key, "word-delimiters")) return value_to_wchars(ctx, &conf->word_delimiters); - else if (streq(key, "notify")) - return value_to_spawn_template(ctx, &conf->notify); + else if (streq(key, "notify")) { + user_notification_add( + &conf->notifications, USER_NOTIFICATION_DEPRECATED, + xstrdup("notify: use desktop-notifications.command instead")); + log_msg( + LOG_CLASS_WARNING, LOG_MODULE, __FILE__, __LINE__, + "deprecated: notify: use desktop-notifications.command instead"); + return value_to_spawn_template( + ctx, &conf->desktop_notifications.command); + } - else if (streq(key, "notify-focus-inhibit")) - return value_to_bool(ctx, &conf->notify_focus_inhibit); + else if (streq(key, "notify-focus-inhibit")) { + user_notification_add( + &conf->notifications, USER_NOTIFICATION_DEPRECATED, + xstrdup("notify-focus-inhibit: " + "use desktop-notifications.inhibit-when-focused instead")); + log_msg( + LOG_CLASS_WARNING, LOG_MODULE, __FILE__, __LINE__, + "deprecrated: notify-focus-inhibit: " + "use desktop-notifications.inhibit-when-focused instead"); + return value_to_bool( + ctx, &conf->desktop_notifications.inhibit_when_focused); + } else if (streq(key, "selection-target")) { _Static_assert(sizeof(conf->selection_target) == sizeof(int), @@ -1083,6 +1101,24 @@ parse_section_bell(struct context *ctx) } } +static bool +parse_section_desktop_notifications(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "command")) + return value_to_spawn_template( + ctx, &conf->desktop_notifications.command); + else if (streq(key, "inhibit-when-focused")) + return value_to_bool( + ctx, &conf->desktop_notifications.inhibit_when_focused); + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + static bool parse_section_scrollback(struct context *ctx) { @@ -2662,6 +2698,7 @@ parse_key_value(char *kv, const char **section, const char **key, const char **v enum section { SECTION_MAIN, SECTION_BELL, + SECTION_DESKTOP_NOTIFICATIONS, SECTION_SCROLLBACK, SECTION_URL, SECTION_COLORS, @@ -2688,6 +2725,7 @@ static const struct { } section_info[] = { [SECTION_MAIN] = {&parse_section_main, "main"}, [SECTION_BELL] = {&parse_section_bell, "bell"}, + [SECTION_DESKTOP_NOTIFICATIONS] = {&parse_section_desktop_notifications, "desktop-notifications"}, [SECTION_SCROLLBACK] = {&parse_section_scrollback, "scrollback"}, [SECTION_URL] = {&parse_section_url, "url"}, [SECTION_COLORS] = {&parse_section_colors, "colors"}, @@ -3144,10 +3182,12 @@ config_load(struct config *conf, const char *conf_path, .presentation_timings = false, .selection_target = SELECTION_TARGET_PRIMARY, .hold_at_exit = false, - .notify = { - .argv = {.args = NULL}, + .desktop_notifications = { + .command = { + .argv = {.args = NULL}, + }, + .inhibit_when_focused = true, }, - .notify_focus_inhibit = true, .tweak = { .fcft_filter = FCFT_SCALING_FILTER_LANCZOS3, @@ -3185,8 +3225,8 @@ config_load(struct config *conf, const char *conf_path, parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers); tokenize_cmdline( - "notify-send -a ${app-id} -i ${app-id} -u ${urgency} ${title} ${body}", - &conf->notify.argv.args); + "notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} -- ${title} ${body}", + &conf->desktop_notifications.command.argv.args); tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args); static const char32_t *url_protocols[] = { @@ -3439,7 +3479,8 @@ config_clone(const struct config *old) conf->scrollback.indicator.text = xc32dup(old->scrollback.indicator.text); conf->server_socket_path = xstrdup(old->server_socket_path); spawn_template_clone(&conf->bell.command, &old->bell.command); - spawn_template_clone(&conf->notify, &old->notify); + spawn_template_clone(&conf->desktop_notifications.command, + &old->desktop_notifications.command); for (size_t i = 0; i < ALEN(conf->fonts); i++) config_font_list_clone(&conf->fonts[i], &old->fonts[i]); @@ -3521,7 +3562,7 @@ config_free(struct config *conf) free(conf->word_delimiters); spawn_template_free(&conf->bell.command); free(conf->scrollback.indicator.text); - spawn_template_free(&conf->notify); + spawn_template_free(&conf->desktop_notifications.command); for (size_t i = 0; i < ALEN(conf->fonts); i++) config_font_list_destroy(&conf->fonts[i]); free(conf->server_socket_path); diff --git a/config.h b/config.h index 4ce36486..b3688f28 100644 --- a/config.h +++ b/config.h @@ -338,8 +338,10 @@ struct config { SELECTION_TARGET_BOTH } selection_target; - struct config_spawn_template notify; - bool notify_focus_inhibit; + struct { + struct config_spawn_template command; + bool inhibit_when_focused; + } desktop_notifications; env_var_list_t env_vars; diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index 5c611c92..998b6843 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -718,6 +718,10 @@ All _OSC_ sequences begin with *\\E]*, sometimes abbreviated _OSC_. : Copy _Pd_ (base64 encoded text) to the clipboard. _Pc_ denotes the target: *c* targets the clipboard and *s* and *p* the primary selection. +| \\E] 99 ; _params_ ; _payload_ \\E\\ +: kitty +: Desktop notification; uses *desktop-notifications.command* in + *foot.ini*(5). | \\E] 104 ; _c_ \\E\\ : xterm : Reset color number _c_ (multiple semicolon separated _c_ values may @@ -757,7 +761,8 @@ All _OSC_ sequences begin with *\\E]*, sometimes abbreviated _OSC_. : Flash the entire terminal (foot extension) | \\E] 777;notify;_title_;_msg_ \\E\\ : urxvt -: Desktop notification, uses *notify* in *foot.ini*(5). +: Desktop notification, uses *desktop-notifications.command* in + *foot.ini*(5). # DCS diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index fe6ff57b..e6b554bb 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -342,32 +342,6 @@ empty string to be set, but it must be quoted: *KEY=""*) text. Note that whitespace characters are _always_ word delimiters, regardless of this setting. Default: _,│`|:"'()[]{}<>_ -*notify* - Command to execute to display a notification. _${title}_ and - _${body}_ will be replaced with the notification's actual _title_ - and _body_ (message content). - - _${app-id}_ is replaced with the value of the command line option - _--app-id_, and defaults to *foot* (normal mode), or - *footclient* (server mode). - - _${window-title}_ is replaced with the current window title. - - Applications can trigger notifications in the following ways: - - - OSC 777: *\\e]777;notify;<title>;<body>\\e\\\\* - - By default, notifications are *inhibited* if the foot window - has keyboard focus. See _notify-focus-inhibit_. - - Default: _notify-send -a ${app-id} -i ${app-id} -u ${urgency} ${title} ${body}_. - -*notify-focus-inhibit* - Boolean. If enabled, foot will not display notifications if the - terminal window has keyboard focus. - - Default: _yes_ - *selection-target* Clipboard target to automatically copy selected text to. One of *none*, *primary*, *clipboard* or *both*. Default: _primary_. @@ -426,10 +400,11 @@ Note: do not set *TERM* here; use the *term* option in the main Default: _no_ *notify* - When set to _yes_, foot will emit a desktop notification using - the command specified in the *notify* option whenever *BEL* is + When set to _yes_, foot will emit a desktop notification using the + command specified in the *notify* option whenever *BEL* is received. By default, bell notifications are shown only when the - window does *not* have keyboard focus. See _notify-focus-inhibit_. + window does *not* have keyboard focus. See + _desktop-notifications.inhibit-when-focused_. Default: _no_ @@ -445,6 +420,82 @@ Note: do not set *TERM* here; use the *term* option in the main Whether to run the command on *BEL* even while focused. Default: _no_ +# SECTION: desktop-notifications + +*command* + Command to execute to display a notification. + + Template arguments + _${title}_ and _${body}_ will be replaced with the + notification's actual _title_ and _body_ (message content). + + _${app-id}_ is replaced with the value of the command line + option _--app-id_, and defaults to *foot* (normal mode), or + *footclient* (server mode). + + _${window-title}_ is replaced with the current window title. + + _${icon}_ is replaced by the icon specified in the + notification request, or _${app_id}_ if the notification did + not set an icon. Note that only symbolic icon names are + supported, not filenames. + + _${urgency}_ is replaced with the notifications urgency; + *low*, *normal* or *critical*. + + Ways to trigger notifications + Applications can trigger notifications in the following ways: + + - OSC 777: *\\e]777;notify;<title>;<body>\\e\\\\* + - OSC 99: *\\e]99;;<title>\\e\\\\* (this is just a bare bones + example; this protocol has lots of features, see + https://sw.kovidgoyal.net/kitty/desktop-notifications) + + By default, notifications are *inhibited* if the foot window + has keyboard focus. See + _desktop-notifications.inhibit-when-focused_. + + Window activation (focusing) + Foot can focus the window when the notification is + "activated". This typically happens when the default action is + invoked, and/or when the notification is clicked, but exact + behavior depends on the notification daemon in use, and how it + has been configured. + + For this to work, foot needs an XDG activation token. To this + end, foot will read the command's stdout; everything printed + there, not including trailing newlines, are assumed to be part + of the activation token. There is no harm in printing + something else on stdout - it will simply result in the + activation failing (i.e. the window will not be focused). + + *Note*: notify-send does not, out of the box, support + reporting the XDG activation token in any way. This means + window activation will not work by default. + + Notification dismissal + The kitty desktop notifications protocol (OSC-99) allows the + terminal application to request an event be sent to it when + the notification has been dismissed (by setting *a=report* in + the notification request). + + To be able to send this event, foot needs to know when the + notification is dismissed. This is handled in a very simple + manner; the command signals notification dismissal by + exiting. That is, as soon as the command returns, foot + considers the notification dismissed. + + For *notify-send*, this can be achieved with the *--wait* + option. + + Default: _notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} -- ${title} ${body}_. + +*inhibit-when-focused* + Boolean. If enabled, foot will not display notifications if the + terminal window has keyboard focus. + + Default: _yes_ + # SECTION: scrollback *lines* diff --git a/foot.ini b/foot.ini index 7a1db9ba..33727dd3 100644 --- a/foot.ini +++ b/foot.ini @@ -29,8 +29,6 @@ # resize-by-cells=yes # resize-delay-ms=100 -# notify=notify-send -a ${app-id} -i ${app-id} -u ${urgency} ${title} ${body} - # bold-text-in-bright=no # word-delimiters=,│`|:"'()[]{}<> # selection-target=primary @@ -48,6 +46,11 @@ # command= # command-focused=no +[desktop-notifications] +# command=notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} -- ${title} ${body} +# inhibit-when-focused=yes + + [scrollback] # lines=1000 # multiplier=3.0 diff --git a/notify.c b/notify.c index 7cd22ccb..043f41a5 100644 --- a/notify.c +++ b/notify.c @@ -1,9 +1,11 @@ #include "notify.h" +#include <errno.h> #include <stdlib.h> #include <string.h> #include <unistd.h> +#include <sys/epoll.h> #include <sys/stat.h> #include <fcntl.h> @@ -13,50 +15,189 @@ #include "config.h" #include "spawn.h" #include "terminal.h" +#include "wayland.h" #include "xmalloc.h" +#include "xsnprintf.h" void -notify_notify(const struct terminal *term, const char *title, const char *body, - enum notify_when when, enum notify_urgency urgency) +notify_free(struct terminal *term, struct notification *notif) { - LOG_DBG("notify: title=\"%s\", msg=\"%s\"", title, body); + fdm_del(term->fdm, notif->stdout_fd); + free(notif->id); + free(notif->title); + free(notif->body); + free(notif->icon); + free(notif->xdg_token); +} - if ((term->conf->notify_focus_inhibit || when != NOTIFY_ALWAYS) +static bool +fdm_notify_stdout(struct fdm *fdm, int fd, int events, void *data) +{ + const struct terminal *term = data; + struct notification *notif = NULL; + + /* Find notification */ + tll_foreach(term->notifications, it) { + if (it->item.stdout_fd == fd) { + notif = &it->item; + break; + } + } + + if (events & EPOLLIN) { + char buf[512]; + ssize_t count = read(fd, buf, sizeof(buf) - 1); + + if (count < 0) { + if (errno == EINTR) + return true; + + LOG_ERRNO("failed to read notification activation token"); + return false; + } + + if (count > 0) { + buf[count - 1] = '\0'; + + if (notif != NULL) { + if (notif->xdg_token == NULL) { + notif->xdg_token = xstrdup(buf); + } else { + char *new_token = xstrjoin(notif->xdg_token, buf); + free(notif->xdg_token); + notif->xdg_token = new_token; + } + } + } + } + + if (events & EPOLLHUP) { + fdm_del(fdm, fd); + if (notif != NULL) + notif->stdout_fd = -1; + + /* Strip trailing newlines */ + if (notif != NULL && notif->xdg_token != NULL) { + size_t len = strlen(notif->xdg_token); + + while (len > 0 && notif->xdg_token[len - 1] == '\n') + len--; + + notif->xdg_token[len] = '\0'; + } + } + + return true; +} + +static void +notif_done(struct reaper *reaper, pid_t pid, int status, void *data) +{ + struct terminal *term = data; + + tll_foreach(term->notifications, it) { + struct notification *notif = &it->item; + if (notif->pid != pid) + continue; + + LOG_DBG("notification %s dismissed", notif->id); + + if (notif->focus) { + LOG_DBG("focus window on notification activation: \"%s\"", notif->xdg_token); + wayl_activate(term->wl, term->window, notif->xdg_token); + } + + if (notif->report) { + xassert(notif->id != NULL); + + LOG_DBG("sending notification report to client"); + + char reply[5 + strlen(notif->id) + 1 + 2 + 1]; + int n = xsnprintf( + reply, sizeof(reply), "\033]99;%s;\033\\", notif->id); + term_to_slave(term, reply, n); + } + + notify_free(term, notif); + tll_remove(term->notifications, it); + return; + } +} + +bool +notify_notify(const struct terminal *term, struct notification *notif) +{ + xassert(notif->xdg_token == NULL); + xassert(notif->pid == 0); + xassert(notif->stdout_fd == 0); + + notif->pid = -1; + notif->stdout_fd = -1; + + /* Use body as title, if title is unset */ + const char *title = notif->title != NULL ? notif->title : notif->body; + const char *body = notif->title != NULL && notif->body != NULL ? notif->body : ""; + + /* Icon: use symbolic name from notification, if present, + otherwise fallback to the application ID */ + const char *icon = notif->icon != NULL + ? notif->icon + : term->app_id != NULL + ? term->app_id + : term->conf->app_id; + + LOG_DBG("notify: title=\"%s\", body=\"%s\"", title, body); + + xassert(title != NULL); + if (title == NULL) + return false; + + if ((term->conf->desktop_notifications.inhibit_when_focused || + notif->when != NOTIFY_ALWAYS) && term->kbd_focus) { /* No notifications while we're focused */ - return; + return false; } - if (title == NULL || body == NULL) - return; - - if (term->conf->notify.argv.args == NULL) - return; + if (term->conf->desktop_notifications.command.argv.args == NULL) + return false; char **argv = NULL; size_t argc = 0; const char *urgency_str = - urgency == NOTIFY_URGENCY_LOW + notif->urgency == NOTIFY_URGENCY_LOW ? "low" - : urgency == NOTIFY_URGENCY_NORMAL + : notif->urgency == NOTIFY_URGENCY_NORMAL ? "normal" : "critical"; if (!spawn_expand_template( - &term->conf->notify, 5, - (const char *[]){"app-id", "window-title", "title", "body", "urgency"}, + &term->conf->desktop_notifications.command, 6, + (const char *[]){"app-id", "window-title", "icon", "title", "body", "urgency"}, (const char *[]){term->app_id ? term->app_id : term->conf->app_id, - term->window_title, title, body, urgency_str}, + term->window_title, icon, title, body, urgency_str}, &argc, &argv)) { - return; + return false; } LOG_DBG("notify command:"); for (size_t i = 0; i < argc; i++) LOG_DBG(" argv[%zu] = \"%s\"", i, argv[i]); + int stdout_fds[2] = {-1, -1}; + if (notif->focus && pipe2(stdout_fds, O_CLOEXEC | O_NONBLOCK) < 0) { + LOG_WARN("failed to create stdout pipe"); + /* Non-fatal */ + } + + if (stdout_fds[0] >= 0) { + xassert(notif->xdg_token == NULL); + fdm_add(term->fdm, stdout_fds[0], EPOLLIN, + &fdm_notify_stdout, (void *)term); + } + /* Redirect stdin to /dev/null, but ignore failure to open */ int devnull = open("/dev/null", O_RDONLY); pid_t pid = spawn( @@ -64,6 +205,14 @@ notify_notify(const struct terminal *term, const char *title, const char *body, ¬if_done, (void *)term, NULL); if (stdout_fds[1] >= 0) { + /* Close write-end of stdout pipe */ + close(stdout_fds[1]); + } + + if (pid < 0 && stdout_fds[0] >= 0) { + /* Remove FDM callback if we failed to spawn */ + fdm_del(term->fdm, stdout_fds[0]); + } if (devnull >= 0) close(devnull); @@ -71,4 +220,8 @@ notify_notify(const struct terminal *term, const char *title, const char *body, for (size_t i = 0; i < argc; i++) free(argv[i]); free(argv); + + notif->pid = pid; + notif->stdout_fd = stdout_fds[0]; + return true; } diff --git a/notify.h b/notify.h index be79b41a..6c43e294 100644 --- a/notify.h +++ b/notify.h @@ -1,30 +1,41 @@ #pragma once #include <stdbool.h> +#include <unistd.h> struct terminal; enum notify_when { + /* First, so that it can be left out of initializer and still be + the default */ NOTIFY_ALWAYS, + NOTIFY_UNFOCUSED, NOTIFY_INVISIBLE }; enum notify_urgency { - NOTIFY_URGENCY_LOW, + /* First, so that it can be left out of initializer and still be + the default */ NOTIFY_URGENCY_NORMAL, + + NOTIFY_URGENCY_LOW, NOTIFY_URGENCY_CRITICAL, }; -struct kitty_notification { +struct notification { char *id; char *title; char *body; + char *icon; + char *xdg_token; enum notify_when when; enum notify_urgency urgency; bool focus; bool report; + + pid_t pid; + int stdout_fd; }; -void notify_notify( - const struct terminal *term, const char *title, const char *body, - enum notify_when when, enum notify_urgency urgency); +bool notify_notify(const struct terminal *term, struct notification *notif); +void notify_free(struct terminal *term, struct notification *notif); diff --git a/osc.c b/osc.c index 7485cada..6926c3ca 100644 --- a/osc.c +++ b/osc.c @@ -560,9 +560,9 @@ osc_notify(struct terminal *term, char *string) return; } - notify_notify( - term, title, msg != NULL ? msg : "", - NOTIFY_ALWAYS, NOTIFY_URGENCY_NORMAL); + notify_notify(term, &(struct notification){ + .title = (char *)title, + .body = (char *)msg}); } static void @@ -571,16 +571,15 @@ kitty_notification(struct terminal *term, char *string) /* https://sw.kovidgoyal.net/kitty/desktop-notifications */ char *payload = strchr(string, ';'); - if (payload == NULL) { - LOG_ERR("OSC-99: payload missing"); + if (payload == NULL) return; - } char *parameters = string; *payload = '\0'; payload++; char *id = xstrdup("0"); /* The 'i' parameter */ + char *icon = NULL; /* The 'I' parameter */ bool focus = true; /* The 'a' parameter */ bool report = false; /* The 'a' parameter */ bool done = true; /* The 'd' parameter */ @@ -601,10 +600,8 @@ kitty_notification(struct terminal *term, char *string) { /* All parameters are on the form X=value, where X is always exactly one character */ - if (param[0] == '\0' || param[1] != '=') { - LOG_WARN("OSC-99: invalid parameter: \"%s\"", param); + if (param[0] == '\0' || param[1] != '=') continue; - } char *value = ¶m[2]; @@ -613,25 +610,19 @@ kitty_notification(struct terminal *term, char *string) /* notification activation action: focus|report|-focus|-report */ have_a = true; char *a_ctx = NULL; + for (const char *v = strtok_r(value, ",", &a_ctx); v != NULL; v = strtok_r(NULL, ",", &a_ctx)) { - LOG_WARN(" a: \"%s\"", v); bool reverse = v[0] == '-'; if (reverse) v++; - if (strcmp(v, "focus") == 0) { + if (strcmp(v, "focus") == 0) focus = !reverse; - if (focus) - LOG_WARN("unimplemented: OSC-99: focus on notification activation"); - } else if (strcmp(v, "report") == 0) { + else if (strcmp(v, "report") == 0) report = !reverse; - if (report) - LOG_WARN("unimplemented: OSC-99: report on notification activation"); - } else - LOG_WARN("OSC-99: unrecognized value for 'a': \"%s\", ignoring", v); } break; @@ -643,8 +634,6 @@ kitty_notification(struct terminal *term, char *string) done = false; else if (value[0] == '1' && value[1] == '\0') done = true; - else - LOG_WARN("OSC-99: unrecognized value for 'd': \"%s\", ignoring", value); break; case 'e': @@ -653,8 +642,6 @@ kitty_notification(struct terminal *term, char *string) base64 = false; else if (value[0] == '1' && value[1] == '\0') base64 = true; - else - LOG_WARN("OSC-99: unrecognized value for 'e': \"%s\", ignoring", value); break; case 'i': @@ -669,8 +656,6 @@ kitty_notification(struct terminal *term, char *string) payload_is_title = true; else if (strcmp(value, "body") == 0) payload_is_title = false; - else - LOG_WARN("OSC-99: unrecognized value for 'p': \"%s\", ignoring", value); break; case 'o': @@ -682,8 +667,6 @@ kitty_notification(struct terminal *term, char *string) when = NOTIFY_UNFOCUSED; else if (strcmp(value, "invisible") == 0) when = NOTIFY_INVISIBLE; - else - LOG_WARN("OSC-99: unrecognized value for 'o': \"%s\", ignoring", value); break; case 'u': @@ -695,13 +678,16 @@ kitty_notification(struct terminal *term, char *string) urgency = NOTIFY_URGENCY_NORMAL; else if (value[0] == '2' && value[1] == '\0') urgency = NOTIFY_URGENCY_CRITICAL; - else - LOG_WARN("OSC-99: unrecognized value for 'u': \"%s\", ignoring", value); break; - default: - LOG_WARN("OSC-99: unrecognized parameter: \"%s\", ignoring", param); - break; + /* + * The options below are not (yet) part of the official spec. + */ + case 'I': + /* icon: only symbolic names allowed; absolute paths are ignored */ + if (value[0] != '/') + icon = xstrdup(value); + break; } } @@ -710,9 +696,9 @@ kitty_notification(struct terminal *term, char *string) else payload = xstrdup(payload); - LOG_DBG("id=%s, done=%d, focus=%d, report=%d, base64=%d, payload: %s, " + LOG_DBG("id=%s, done=%d, focus=%d, report=%d, base64=%d, icon=%s, payload: %s, " "honor: %s, urgency: %s, %s: %s", - id, done, focus, report, base64, + id, done, focus, report, base64, icon != NULL ? icon : "<not set>", payload_is_title ? "title" : "body", (when == NOTIFY_ALWAYS ? "always" @@ -726,11 +712,10 @@ kitty_notification(struct terminal *term, char *string) payload_is_title ? "title" : "body", payload); /* Search for an existing (d=0) notification to update */ - struct kitty_notification *notif = NULL; - tll_foreach(term->kitty_notifications, it) { + struct notification *notif = NULL; + tll_foreach(term->notifications, it) { if (strcmp(it->item.id, id) == 0) { /* Found existing notification */ - LOG_WARN("found existing kitty notification"); notif = &it->item; break; } @@ -739,8 +724,9 @@ kitty_notification(struct terminal *term, char *string) if (notif == NULL) { /* Somewhat unoptimized... this will be free:d and removed immediately if d=1 */ - tll_push_front(term->kitty_notifications, ((struct kitty_notification){ + tll_push_front(term->notifications, ((struct notification){ .id = id, + .icon = NULL, .title = NULL, .body = NULL, .when = when, @@ -749,8 +735,13 @@ kitty_notification(struct terminal *term, char *string) .report = report, })); - id = NULL; /* Prevent double free */ - notif = &tll_front(term->kitty_notifications); + id = NULL; /* Prevent double free */ + notif = &tll_front(term->notifications); + } + + if (notif->pid > 0) { + /* Notification has already been completed, ignore new metadata */ + goto out; } /* Update notification metadata */ @@ -764,6 +755,12 @@ kitty_notification(struct terminal *term, char *string) if (have_u) notif->urgency = urgency; + if (icon != NULL) { + free(notif->icon); + notif->icon = icon; + icon = NULL; /* Prevent double free */ + } + if (payload_is_title) { if (notif->title == NULL) { notif->title = payload; @@ -784,26 +781,22 @@ kitty_notification(struct terminal *term, char *string) } } - free(id); - free(payload); - if (done) { - notify_notify( - term, - notif->title != NULL ? notif->title : notif->body, - notif->title != NULL && notif->body != NULL ? notif->body : "", - notif->when, notif->urgency); - - tll_foreach(term->kitty_notifications, it) { - if (&it->item == notif) { - free(it->item.id); - free(it->item.title); - free(it->item.body); - tll_remove(term->kitty_notifications, it); - break; + if (!notify_notify(term, notif)) { + tll_foreach(term->notifications, it) { + if (&it->item == notif) { + notify_free(term, &it->item); + tll_remove(term->notifications, it); + break; + } } } } + +out: + free(id); + free(icon); + free(payload); } void diff --git a/terminal.c b/terminal.c index bf0c78dc..a0be4518 100644 --- a/terminal.c +++ b/terminal.c @@ -1313,7 +1313,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED .ime_enabled = true, #endif - .kitty_notifications = tll_init(), + .notifications = tll_init(), }; pixman_region32_init(&term->render.last_overlay_clip); @@ -1818,11 +1818,9 @@ term_destroy(struct terminal *term) tll_remove(term->ptmx_paste_buffers, it); } - tll_foreach(term->kitty_notifications, it) { - free(it->item.id); - free(it->item.body); - free(it->item.title); - tll_remove(term->kitty_notifications, it); + tll_foreach(term->notifications, it) { + notify_free(term, &it->item); + tll_remove(term->notifications, it); } sixel_fini(term); @@ -2030,11 +2028,9 @@ term_reset(struct terminal *term, bool hard) tll_remove(term->alt.sixel_images, it); } - tll_foreach(term->kitty_notifications, it) { - free(it->item.id); - free(it->item.title); - free(it->item.body); - tll_remove(term->kitty_notifications, it); + tll_foreach(term->notifications, it) { + notify_free(term, &it->item); + tll_remove(term->notifications, it); } term->grapheme_shaping = term->conf->tweak.grapheme_shaping; @@ -3582,8 +3578,9 @@ term_bell(struct terminal *term) } if (term->conf->bell.notify) { - notify_notify(term, "Bell", "Bell in terminal", - NOTIFY_ALWAYS, NOTIFY_URGENCY_NORMAL); + notify_notify(term, &(struct notification){ + .title = (char *)"Bell", + .body = (char *)"Bell in terminal"}); } if (term->conf->bell.flash) diff --git a/terminal.h b/terminal.h index 65dde151..0967bf14 100644 --- a/terminal.h +++ b/terminal.h @@ -799,7 +799,9 @@ struct terminal { void *cb_data; } shutdown; - tll(struct kitty_notification) kitty_notifications; + /* Notifications that either haven't been sent yet, or have been + sent but not yet dismissed */ + tll(struct notification) notifications; char *foot_exe; char *cwd; diff --git a/tests/test-config.c b/tests/test-config.c index 4a0fd755..ec718c24 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -511,7 +511,7 @@ test_section_main(void) test_boolean(&ctx, &parse_section_main, "login-shell", &conf.login_shell); test_boolean(&ctx, &parse_section_main, "box-drawings-uses-font-glyphs", &conf.box_drawings_uses_font_glyphs); test_boolean(&ctx, &parse_section_main, "locked-title", &conf.locked_title); - test_boolean(&ctx, &parse_section_main, "notify-focus-inhibit", &conf.notify_focus_inhibit); + test_boolean(&ctx, &parse_section_main, "notify-focus-inhibit", &conf.desktop_notifications.inhibit_when_focused); /* Deprecated */ test_boolean(&ctx, &parse_section_main, "dpi-aware", &conf.dpi_aware); test_pt_or_px(&ctx, &parse_section_main, "font-size-adjustment", &conf.font_size_adjustment.pt_or_px); /* TODO: test ‘N%’ values too */ @@ -524,7 +524,7 @@ test_section_main(void) test_uint16(&ctx, &parse_section_main, "resize-delay-ms", &conf.resize_delay_ms); test_uint16(&ctx, &parse_section_main, "workers", &conf.render_worker_count); - test_spawn_template(&ctx, &parse_section_main, "notify", &conf.notify); + test_spawn_template(&ctx, &parse_section_main, "notify", &conf.desktop_notifications.command); /* Deprecated */ test_enum(&ctx, &parse_section_main, "selection-target", 4, @@ -570,6 +570,20 @@ test_section_bell(void) config_free(&conf); } +static void +test_section_desktop_notifications(void) +{ + struct config conf = {0}; + struct context ctx = {.conf = &conf, .section = "desktop-notifications", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_desktop_notifications, "invalid-key"); + + test_boolean(&ctx, &parse_section_desktop_notifications, "inhibit-when-focused", &conf.desktop_notifications.inhibit_when_focused); + test_spawn_template(&ctx, &parse_section_desktop_notifications, "command", &conf.desktop_notifications.command); + + config_free(&conf); +} + static void test_section_scrollback(void) { @@ -1391,6 +1405,7 @@ main(int argc, const char *const *argv) log_init(LOG_COLORIZE_AUTO, false, 0, LOG_CLASS_ERROR); test_section_main(); test_section_bell(); + test_section_desktop_notifications(); test_section_scrollback(); test_section_url(); test_section_cursor(); From 0209458cc0b4fc502492253d3a9e9338e590a97e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 07:09:54 +0200 Subject: [PATCH 0796/1323] changelog: new desktop-notifications config section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 057924fa..861245b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,11 @@ private mode `2048`. * Support for OSC-99 [_"Kitty desktop notifications"_](https://sw.kovidgoyal.net/kitty/desktop-notifications/). +* `desktop-notifications.command` option, replaces `notify`. +* `desktop-notifications.inhibit-when-focused` option, replaces + `notify-focus-inhibit`. +* `${icon}` and `${urgency}` added to the + `desktop-notifications.command` template. [1707]: https://codeberg.org/dnkl/foot/issues/1707 [1738]: https://codeberg.org/dnkl/foot/issues/1738 @@ -118,6 +123,12 @@ ### Deprecated + +* `notify` option; replaced by `desktop-notifications.command`. +* `notify-focus-inhibit` option; replaced by + `desktop-notifications.inhibit-when-focused`. + + ### Removed ### Fixed From 7268ee9078e8c0ce4b7cdaa479b67ea34aa8e88f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 07:43:42 +0200 Subject: [PATCH 0797/1323] pgo: update spawn() prototype --- pgo/pgo.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pgo/pgo.c b/pgo/pgo.c index e8080521..7aabebb4 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -100,12 +100,12 @@ void wayl_win_alpha_changed(struct wl_window *win) {} bool wayl_win_set_urgent(struct wl_window *win) { return true; } bool wayl_fractional_scaling(const struct wayland *wayl) { return true; } -bool +pid_t spawn(struct reaper *reaper, const char *cwd, char *const argv[], int stdin_fd, int stdout_fd, int stderr_fd, - const char *xdg_activation_token) + reaper_cb cb, void *cb_data, const char *xdg_activation_token) { - return true; + return 2; } pid_t From e88ec86c9337b0a2e1b9d3d1d207005a4c084a79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 07:43:56 +0200 Subject: [PATCH 0798/1323] pgo: update notify_notify() prototype, add notify_free() --- pgo/pgo.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pgo/pgo.c b/pgo/pgo.c index 7aabebb4..6c13b72a 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -151,13 +151,18 @@ void ime_enable(struct seat *seat) {} void ime_disable(struct seat *seat) {} void ime_reset_preedit(struct seat *seat) {} +bool +notify_notify(const struct terminal *term, struct notification *notif) +{ + return true; +} + void - notify_notify( - const struct terminal *term, const char *title, const char *body, - enum notify_when when, enum notify_urgency urgency) +notify_free(struct terminal *term, struct notification *notif) { } + void reaper_add(struct reaper *reaper, pid_t pid, reaper_cb cb, void *cb_data) {} void reaper_del(struct reaper *reaper, pid_t pid) {} From e52d6e3fb8648c34e9e859db03ce3c0ad50ca952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 08:05:19 +0200 Subject: [PATCH 0799/1323] osc: kitty notifications: use xstrjoin() instead of xasprintf() --- osc.c | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/osc.c b/osc.c index 6926c3ca..ef54f520 100644 --- a/osc.c +++ b/osc.c @@ -722,8 +722,6 @@ kitty_notification(struct terminal *term, char *string) } if (notif == NULL) { - /* Somewhat unoptimized... this will be free:d and removed - immediately if d=1 */ tll_push_front(term->notifications, ((struct notification){ .id = id, .icon = NULL, @@ -766,18 +764,18 @@ kitty_notification(struct terminal *term, char *string) notif->title = payload; payload = NULL; } else { - char *new_title = xasprintf("%s%s", notif->title, payload); - free(notif->title); - notif->title = new_title; + char *old_title = notif->title; + notif->title = xstrjoin(old_title, payload); + free(old_title); } } else { if (notif->body == NULL) { notif->body = payload; payload = NULL; } else { - char *new_body = xasprintf("%s%s", notif->body, payload); - free(notif->body); - notif->body = new_body; + char *old_body = notif->body; + notif->body = xstrjoin(old_body, payload); + free(old_body); } } From b319618af15b5afd9d0f63271bd09b4a4a27ad52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 09:33:18 +0200 Subject: [PATCH 0800/1323] notify: XDG token is now expected to be prefixed with xdgtoken= This patch modifies our stdout reader to consume input as we go, instead of all at once when stdout is closed. This will make it easier to add support for reading e.g. the daemon assigned notification ID in the future, and also ensures we see the XDG activation token "as soon as possible". Furthermore, to be more future proof, require the XDG activation token to be prefixed with 'xdgtoken=', and ignore other lines. Thus, instead of treating *all* of stdout as the XDG activation token, parse stdout line-by-line, and ignore everything that does not begin with 'xdgtoken='. Everything (on that line) following 'xdgtoken=' is treated as the activation token. --- doc/foot.ini.5.scd | 13 +++++---- notify.c | 73 ++++++++++++++++++++++++++++++++-------------- notify.h | 18 ++++++++++-- 3 files changed, 74 insertions(+), 30 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index e6b554bb..5135e080 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -463,11 +463,14 @@ Note: do not set *TERM* here; use the *term* option in the main has been configured. For this to work, foot needs an XDG activation token. To this - end, foot will read the command's stdout; everything printed - there, not including trailing newlines, are assumed to be part - of the activation token. There is no harm in printing - something else on stdout - it will simply result in the - activation failing (i.e. the window will not be focused). + end, foot will read the command's stdout; a line prefixed with + *xdgtoken=* will be recognized as containing the XDG + activation token: + + xdgtoken=18179adf579a7a904ce73754964b1ec3 + + The expected format of stdout may change at any time. Please + read the changelog when upgrading foot. *Note*: notify-send does not, out of the box, support reporting the XDG activation token in any way. This means diff --git a/notify.c b/notify.c index 043f41a5..d2ff9d07 100644 --- a/notify.c +++ b/notify.c @@ -28,6 +28,40 @@ notify_free(struct terminal *term, struct notification *notif) free(notif->body); free(notif->icon); free(notif->xdg_token); + free(notif->stdout); +} + +static void +consume_stdout(struct notification *notif, bool eof) +{ + char *data = notif->stdout; + const char *line = data; + size_t left = notif->stdout_sz; + + /* Process stdout, line-by-line */ + while (left > 0) { + line = data; + size_t len = left; + char *eol = memchr(line, '\n', left); + + if (eol != NULL) { + *eol = '\0'; + len = strlen(line); + data = eol + 1; + } else if (!eof) + break; + + /* Check for 'xdgtoken=xyz' */ + if (len > 9 && memcmp(line, "xdgtoken=", 9) == 0) { + notif->xdg_token = xstrndup(&line[9], len - 9); + LOG_DBG("XDG token: \"%s\"", notif->xdg_token); + } + + left -= len + (eol != NULL ? 1 : 0); + } + + memmove(notif->stdout, data, left); + notif->stdout_sz = left; } static bool @@ -36,6 +70,7 @@ fdm_notify_stdout(struct fdm *fdm, int fd, int events, void *data) const struct terminal *term = data; struct notification *notif = NULL; + /* Find notification */ tll_foreach(term->notifications, it) { if (it->item.stdout_fd == fd) { @@ -56,34 +91,25 @@ fdm_notify_stdout(struct fdm *fdm, int fd, int events, void *data) return false; } - if (count > 0) { - buf[count - 1] = '\0'; - - if (notif != NULL) { - if (notif->xdg_token == NULL) { - notif->xdg_token = xstrdup(buf); - } else { - char *new_token = xstrjoin(notif->xdg_token, buf); - free(notif->xdg_token); - notif->xdg_token = new_token; - } + if (count > 0 && notif != NULL) { + if (notif->stdout == NULL) { + xassert(notif->stdout_sz == 0); + notif->stdout = xmemdup(buf, count); + } else { + notif->stdout = xrealloc(notif->stdout, notif->stdout_sz + count); + memcpy(¬if->stdout[notif->stdout_sz], buf, count); } + + notif->stdout_sz += count; + consume_stdout(notif, false); } } if (events & EPOLLHUP) { fdm_del(fdm, fd); - if (notif != NULL) + if (notif != NULL) { notif->stdout_fd = -1; - - /* Strip trailing newlines */ - if (notif != NULL && notif->xdg_token != NULL) { - size_t len = strlen(notif->xdg_token); - - while (len > 0 && notif->xdg_token[len - 1] == '\n') - len--; - - notif->xdg_token[len] = '\0'; + consume_stdout(notif, true); } } @@ -130,6 +156,7 @@ notify_notify(const struct terminal *term, struct notification *notif) xassert(notif->xdg_token == NULL); xassert(notif->pid == 0); xassert(notif->stdout_fd == 0); + xassert(notif->stdout == NULL); notif->pid = -1; notif->stdout_fd = -1; @@ -187,7 +214,9 @@ notify_notify(const struct terminal *term, struct notification *notif) LOG_DBG(" argv[%zu] = \"%s\"", i, argv[i]); int stdout_fds[2] = {-1, -1}; - if (notif->focus && pipe2(stdout_fds, O_CLOEXEC | O_NONBLOCK) < 0) { + if ((notif->focus || notif->report) && + pipe2(stdout_fds, O_CLOEXEC | O_NONBLOCK) < 0) + { LOG_WARN("failed to create stdout pipe"); /* Non-fatal */ } diff --git a/notify.h b/notify.h index 6c43e294..cc34ff74 100644 --- a/notify.h +++ b/notify.h @@ -23,18 +23,30 @@ enum notify_urgency { }; struct notification { + /* + * Set by caller of notify_notify() + */ char *id; char *title; char *body; char *icon; - char *xdg_token; + enum notify_when when; enum notify_urgency urgency; bool focus; bool report; - pid_t pid; - int stdout_fd; + /* + * Used internally by notify + */ + + char *xdg_token; /* XDG activation token, from daemon */ + + pid_t pid; /* Notifier command PID */ + int stdout_fd; /* Notifier command's stdout */ + + char *stdout; /* Data we've reado from command's stdout */ + size_t stdout_sz; }; bool notify_notify(const struct terminal *term, struct notification *notif); From c7cffea9ee65dc38a1e87d1254c4097990d7e96b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 09:42:14 +0200 Subject: [PATCH 0801/1323] notify: stdout is a bad name --- notify.c | 16 ++++++++-------- notify.h | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/notify.c b/notify.c index d2ff9d07..d6d1d8a0 100644 --- a/notify.c +++ b/notify.c @@ -28,13 +28,13 @@ notify_free(struct terminal *term, struct notification *notif) free(notif->body); free(notif->icon); free(notif->xdg_token); - free(notif->stdout); + free(notif->stdout_data); } static void consume_stdout(struct notification *notif, bool eof) { - char *data = notif->stdout; + char *data = notif->stdout_data; const char *line = data; size_t left = notif->stdout_sz; @@ -60,7 +60,7 @@ consume_stdout(struct notification *notif, bool eof) left -= len + (eol != NULL ? 1 : 0); } - memmove(notif->stdout, data, left); + memmove(notif->stdout_data, data, left); notif->stdout_sz = left; } @@ -92,12 +92,12 @@ fdm_notify_stdout(struct fdm *fdm, int fd, int events, void *data) } if (count > 0 && notif != NULL) { - if (notif->stdout == NULL) { + if (notif->stdout_data == NULL) { xassert(notif->stdout_sz == 0); - notif->stdout = xmemdup(buf, count); + notif->stdout_data = xmemdup(buf, count); } else { - notif->stdout = xrealloc(notif->stdout, notif->stdout_sz + count); - memcpy(¬if->stdout[notif->stdout_sz], buf, count); + notif->stdout_data = xrealloc(notif->stdout_data, notif->stdout_sz + count); + memcpy(¬if->stdout_data[notif->stdout_sz], buf, count); } notif->stdout_sz += count; @@ -156,7 +156,7 @@ notify_notify(const struct terminal *term, struct notification *notif) xassert(notif->xdg_token == NULL); xassert(notif->pid == 0); xassert(notif->stdout_fd == 0); - xassert(notif->stdout == NULL); + xassert(notif->stdout_data == NULL); notif->pid = -1; notif->stdout_fd = -1; diff --git a/notify.h b/notify.h index cc34ff74..ec62e03e 100644 --- a/notify.h +++ b/notify.h @@ -45,7 +45,7 @@ struct notification { pid_t pid; /* Notifier command PID */ int stdout_fd; /* Notifier command's stdout */ - char *stdout; /* Data we've reado from command's stdout */ + char *stdout_data; /* Data we've reado from command's stdout */ size_t stdout_sz; }; From ccb184ae645cd92a6e3aba429ab88a94a09bbf8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 11:29:05 +0200 Subject: [PATCH 0802/1323] osc: kitty notifications: updated support for icons This implements the suggested protocol discussed in https://github.com/kovidgoyal/kitty/issues/7657. Icons are handled by loading a cache. Both in-band PNG data, and symbolic names are allowed. Applications use a graphical ID to reference the icon both when loading the cache, and when showing a notification. * 'g' is the graphical ID * 'n' is optional, and assigns a symbolic name to the icon * 'p=icon' - the payload is icon PNG data. It needs to be base64 encoded, but this is *not* implied. I.e. the application *must* use e=1 explicitly. To load an icon (in-band PNG data): printf '\e]99;g=123:p=icon;<base64-encoded-png-data>\e\\' or (symbolic name) printf '\e]99;g=123:n=firefox:p=icon;\e\\' Of course, we can combine the two, assigning *both* a symbolic name, *and* PNG data: printf '\e]99;g=123:n=firefox:p=icon;<base64-encoded-png>\e\\' Then, to use the icon in a notification: printf '\e]99;g=123;this is a notification\e\\' Foot also allows a *symbolic* icon to be defined and used at the same time: printf '\e]99;g=123:n=firefox;this is a notification\e\\' This obviously won't work with PNG data, since it uses the payload portion of the escape sequence. --- base64.c | 12 ++++- base64.h | 2 +- notify.c | 126 +++++++++++++++++++++++++++++++++++++++++++++++----- notify.h | 19 +++++++- osc.c | 128 ++++++++++++++++++++++++++++++++++++++--------------- terminal.c | 6 +++ terminal.h | 1 + 7 files changed, 245 insertions(+), 49 deletions(-) diff --git a/base64.c b/base64.c index 5d01ab07..db697cb0 100644 --- a/base64.c +++ b/base64.c @@ -42,7 +42,7 @@ static const char lookup[64] = { }; char * -base64_decode(const char *s) +base64_decode(const char *s, size_t *size) { const size_t len = strlen(s); if (unlikely(len % 4 != 0)) { @@ -54,6 +54,9 @@ base64_decode(const char *s) if (unlikely(ret == NULL)) return NULL; + if (unlikely(size != NULL)) + *size = len / 4 * 3; + for (size_t i = 0, o = 0; i < len; i += 4, o += 3) { unsigned a = reverse_lookup[(unsigned char)s[i + 0]]; unsigned b = reverse_lookup[(unsigned char)s[i + 1]]; @@ -68,6 +71,13 @@ base64_decode(const char *s) if (unlikely(i + 4 != len || (a | b) & P || (c & P && !(d & P)))) goto invalid; + if (unlikely(size != NULL)) { + if (c & P) + *size = len / 4 * 3 - 2; + else + *size = len / 4 * 3 - 1; + } + c &= 63; d &= 63; } diff --git a/base64.h b/base64.h index d4042512..3fa3d078 100644 --- a/base64.h +++ b/base64.h @@ -3,6 +3,6 @@ #include <stdint.h> #include <stddef.h> -char *base64_decode(const char *s); +char *base64_decode(const char *s, size_t *out_len); char *base64_encode(const uint8_t *data, size_t size); void base64_encode_final(const uint8_t *data, size_t size, char result[4]); diff --git a/notify.c b/notify.c index d6d1d8a0..67cf5bd1 100644 --- a/notify.c +++ b/notify.c @@ -10,11 +10,12 @@ #include <fcntl.h> #define LOG_MODULE "notify" -#define LOG_ENABLE_DBG 0 +#define LOG_ENABLE_DBG 1 #include "log.h" #include "config.h" #include "spawn.h" #include "terminal.h" +#include "util.h" #include "wayland.h" #include "xmalloc.h" #include "xsnprintf.h" @@ -26,7 +27,9 @@ notify_free(struct terminal *term, struct notification *notif) free(notif->id); free(notif->title); free(notif->body); - free(notif->icon); + free(notif->icon_id); + free(notif->icon_symbolic_name); + free(notif->icon_data); free(notif->xdg_token); free(notif->stdout_data); } @@ -167,11 +170,22 @@ notify_notify(const struct terminal *term, struct notification *notif) /* Icon: use symbolic name from notification, if present, otherwise fallback to the application ID */ - const char *icon = notif->icon != NULL - ? notif->icon - : term->app_id != NULL - ? term->app_id - : term->conf->app_id; + const char *icon_name_or_path = term->app_id != NULL + ? term->app_id + : term->conf->app_id; + + if (notif->icon_id != NULL) { + for (size_t i = 0; i < ALEN(term->notification_icons); i++) { + const struct notification_icon *icon = &term->notification_icons[i]; + + if (icon->id != NULL && strcmp(icon->id, notif->icon_id) == 0) { + icon_name_or_path = icon->symbolic_name != NULL + ? icon->symbolic_name + : icon->tmp_file_on_disk; + break; + } + } + } LOG_DBG("notify: title=\"%s\", body=\"%s\"", title, body); @@ -201,9 +215,11 @@ notify_notify(const struct terminal *term, struct notification *notif) if (!spawn_expand_template( &term->conf->desktop_notifications.command, 6, - (const char *[]){"app-id", "window-title", "icon", "title", "body", "urgency"}, - (const char *[]){term->app_id ? term->app_id : term->conf->app_id, - term->window_title, icon, title, body, urgency_str}, + (const char *[]){ + "app-id", "window-title", "icon", "title", "body", "urgency"}, + (const char *[]){ + term->app_id ? term->app_id : term->conf->app_id, + term->window_title, icon_name_or_path, title, body, urgency_str}, &argc, &argv)) { return false; @@ -254,3 +270,93 @@ notify_notify(const struct terminal *term, struct notification *notif) notif->stdout_fd = stdout_fds[0]; return true; } + +static void +add_icon(struct notification_icon *icon, const char *id, const char *symbolic_name, + const uint8_t *data, size_t data_sz) +{ + icon->id = xstrdup(id); + icon->symbolic_name = symbolic_name != NULL ? xstrdup(symbolic_name) : NULL; + icon->tmp_file_on_disk = NULL; + + if (data_sz > 0) { + char name[64] = "/tmp/foot-notification-icon-cache-XXXXXX"; + int fd = mkstemp(name); + + if (fd < 0) { + LOG_ERRNO("failed to create temporary file for icon cache"); + return; + } + + write(fd, data, data_sz); + close(fd); + + LOG_DBG("wrote icon data to %s", name); + icon->tmp_file_on_disk = xstrdup(name); + } + + LOG_DBG("added icon to cache: %s: sym=%s, file=%s", + icon->id, icon->symbolic_name, icon->tmp_file_on_disk); +} + +void +notify_icon_add(struct terminal *term, const char *id, + const char *symbolic_name, const uint8_t *data, size_t data_sz) +{ +#if defined(_DEBUG) + for (size_t i = 0; i < ALEN(term->notification_icons); i++) { + struct notification_icon *icon = &term->notification_icons[i]; + if (icon->id != NULL && strcmp(icon->id, id) == 0) { + BUG("notification icon cache already contains \"%s\"", id); + } + } +#endif + + for (size_t i = 0; i < ALEN(term->notification_icons); i++) { + struct notification_icon *icon = &term->notification_icons[i]; + if (icon->id == NULL) { + add_icon(icon, id, symbolic_name, data, data_sz); + return; + } + } + + /* Cache full - throw out first entry, add new entry last */ + notify_icon_free(&term->notification_icons[0]); + memmove(&term->notification_icons[0], + &term->notification_icons[1], + ((ALEN(term->notification_icons) - 1) * + sizeof(term->notification_icons[0]))); + + add_icon( + &term->notification_icons[ALEN(term->notification_icons) - 1], + id, symbolic_name, data, data_sz); +} + +void +notify_icon_del(struct terminal *term, const char *id) +{ + for (size_t i = 0; i < ALEN(term->notification_icons); i++) { + struct notification_icon *icon = &term->notification_icons[i]; + + if (icon->id == NULL || strcmp(icon->id, id) != 0) + continue; + + notify_icon_free(icon); + return; + } +} + +void +notify_icon_free(struct notification_icon *icon) +{ + if (icon->tmp_file_on_disk != NULL) + unlink(icon->tmp_file_on_disk); + + free(icon->id); + free(icon->symbolic_name); + free(icon->tmp_file_on_disk); + + icon->id = NULL; + icon->symbolic_name = NULL; + icon->tmp_file_on_disk = NULL; +} diff --git a/notify.h b/notify.h index ec62e03e..55ace52d 100644 --- a/notify.h +++ b/notify.h @@ -1,5 +1,6 @@ #pragma once #include <stdbool.h> +#include <stdint.h> #include <unistd.h> struct terminal; @@ -29,7 +30,11 @@ struct notification { char *id; char *title; char *body; - char *icon; + + char *icon_id; + char *icon_symbolic_name; + uint8_t *icon_data; + size_t icon_data_sz; enum notify_when when; enum notify_urgency urgency; @@ -49,5 +54,17 @@ struct notification { size_t stdout_sz; }; +struct notification_icon { + char *id; + char *symbolic_name; + char *tmp_file_on_disk; +}; + bool notify_notify(const struct terminal *term, struct notification *notif); void notify_free(struct terminal *term, struct notification *notif); + +void notify_icon_add(struct terminal *term, const char *id, + const char *symbolic_name, const uint8_t *data, + size_t data_sz); +void notify_icon_del(struct terminal *term, const char *id); +void notify_icon_free(struct notification_icon *icon); diff --git a/osc.c b/osc.c index ef54f520..5a001b7a 100644 --- a/osc.c +++ b/osc.c @@ -67,7 +67,7 @@ osc_to_clipboard(struct terminal *term, const char *target, return; } - char *decoded = base64_decode(base64_data); + char *decoded = base64_decode(base64_data, NULL); if (decoded == NULL) { if (errno == EINVAL) LOG_WARN("OSC: invalid clipboard data: %s", base64_data); @@ -579,12 +579,19 @@ kitty_notification(struct terminal *term, char *string) payload++; char *id = xstrdup("0"); /* The 'i' parameter */ - char *icon = NULL; /* The 'I' parameter */ + char *icon_id = NULL; /* The 'g' parameter */ + char *symbolic_icon = NULL; /* The 'n' parameter */ bool focus = true; /* The 'a' parameter */ bool report = false; /* The 'a' parameter */ bool done = true; /* The 'd' parameter */ bool base64 = false; /* The 'e' parameter */ - bool payload_is_title = true; /* The 'p' parameter */ + + size_t payload_size; + enum { + PAYLOAD_TITLE, + PAYLOAD_BODY, + PAYLOAD_ICON, + } payload_type = PAYLOAD_TITLE; /* The 'p' parameter */ enum notify_when when = NOTIFY_ALWAYS; enum notify_urgency urgency = NOTIFY_URGENCY_NORMAL; @@ -653,9 +660,11 @@ kitty_notification(struct terminal *term, char *string) case 'p': /* payload content: title|body */ if (strcmp(value, "title") == 0) - payload_is_title = true; + payload_type = PAYLOAD_TITLE; else if (strcmp(value, "body") == 0) - payload_is_title = false; + payload_type = PAYLOAD_BODY; + else if (strcmp(value, "icon") == 0) + payload_type = PAYLOAD_ICON; break; case 'o': @@ -680,21 +689,26 @@ kitty_notification(struct terminal *term, char *string) urgency = NOTIFY_URGENCY_CRITICAL; break; - /* - * The options below are not (yet) part of the official spec. - */ - case 'I': - /* icon: only symbolic names allowed; absolute paths are ignored */ - if (value[0] != '/') - icon = xstrdup(value); + case 'g': + free(icon_id); + icon_id = xstrdup(value); + break; + + case 'n': + free(symbolic_icon); + symbolic_icon = xstrdup(value); break; } } - if (base64) - payload = base64_decode(payload); - else + if (base64) { + payload = base64_decode(payload, &payload_size); + if (payload == NULL) + goto out; + } else { payload = xstrdup(payload); + payload_size = strlen(payload); + } LOG_DBG("id=%s, done=%d, focus=%d, report=%d, base64=%d, icon=%s, payload: %s, " "honor: %s, urgency: %s, %s: %s", @@ -724,9 +738,6 @@ kitty_notification(struct terminal *term, char *string) if (notif == NULL) { tll_push_front(term->notifications, ((struct notification){ .id = id, - .icon = NULL, - .title = NULL, - .body = NULL, .when = when, .urgency = urgency, .focus = focus, @@ -753,34 +764,78 @@ kitty_notification(struct terminal *term, char *string) if (have_u) notif->urgency = urgency; - if (icon != NULL) { - free(notif->icon); - notif->icon = icon; - icon = NULL; /* Prevent double free */ + if (icon_id != NULL) { + free(notif->icon_id); + notif->icon_id = icon_id; + icon_id = NULL; /* Prevent double free */ } - if (payload_is_title) { - if (notif->title == NULL) { - notif->title = payload; + if (symbolic_icon != NULL) { + free(notif->icon_symbolic_name); + notif->icon_symbolic_name = symbolic_icon; + symbolic_icon = NULL; + } + + /* Handled chunked payload - append to existing metadata */ + switch (payload_type) { + case PAYLOAD_TITLE: + case PAYLOAD_BODY: { + char **ptr = payload_type == PAYLOAD_TITLE + ? ¬if->title + : ¬if->body; + + if (*ptr == NULL) { + *ptr = payload; payload = NULL; } else { - char *old_title = notif->title; - notif->title = xstrjoin(old_title, payload); - free(old_title); + char *old = *ptr; + *ptr = xstrjoin(old, payload); + free(old); } - } else { - if (notif->body == NULL) { - notif->body = payload; + break; + } + + case PAYLOAD_ICON: + if (notif->icon_data == NULL) { + notif->icon_data = (uint8_t *)payload; + notif->icon_data_sz = payload_size; payload = NULL; } else { - char *old_body = notif->body; - notif->body = xstrjoin(old_body, payload); - free(old_body); + notif->icon_data = xrealloc( + notif->icon_data, notif->icon_data_sz + payload_size); + memcpy(¬if->icon_data[notif->icon_data_sz], payload, payload_size); + notif->icon_data_sz += payload_size; } + break; } if (done) { - if (!notify_notify(term, notif)) { + /* Update icon cache, if necessary */ + if (notif->icon_id != NULL && + (notif->icon_symbolic_name != NULL || notif->icon_data != NULL)) + { + notify_icon_del(term, notif->icon_id); + notify_icon_add(term, notif->icon_id, + notif->icon_symbolic_name, + notif->icon_data, notif->icon_data_sz); + + /* Don't need this anymore */ + free(notif->icon_symbolic_name); + free(notif->icon_data); + notif->icon_symbolic_name = NULL; + notif->icon_data = NULL; + notif->icon_data_sz = 0; + } + + /* + * Show notification. + * + * The checks for title|body is to handle notifications that + * only load icon data into the icon cache + */ + if ((notif->title != NULL || notif->body != NULL) && + !notify_notify(term, notif)) + { tll_foreach(term->notifications, it) { if (&it->item == notif) { notify_free(term, &it->item); @@ -793,7 +848,8 @@ kitty_notification(struct terminal *term, char *string) out: free(id); - free(icon); + free(icon_id); + free(symbolic_icon); free(payload); } diff --git a/terminal.c b/terminal.c index a0be4518..eadb9dbc 100644 --- a/terminal.c +++ b/terminal.c @@ -1823,6 +1823,9 @@ term_destroy(struct terminal *term) tll_remove(term->notifications, it); } + for (size_t i = 0; i < ALEN(term->notification_icons); i++) + notify_icon_free(&term->notification_icons[i]); + sixel_fini(term); term_ime_reset(term); @@ -2033,6 +2036,9 @@ term_reset(struct terminal *term, bool hard) tll_remove(term->notifications, it); } + for (size_t i = 0; i < ALEN(term->notification_icons); i++) + notify_icon_free(&term->notification_icons[i]); + term->grapheme_shaping = term->conf->tweak.grapheme_shaping; #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED diff --git a/terminal.h b/terminal.h index 0967bf14..7b726c62 100644 --- a/terminal.h +++ b/terminal.h @@ -802,6 +802,7 @@ struct terminal { /* Notifications that either haven't been sent yet, or have been sent but not yet dismissed */ tll(struct notification) notifications; + struct notification_icon notification_icons[32]; char *foot_exe; char *cwd; From b3108e1ad23387327d6cc7ebfdb20307878dd5c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 11:53:30 +0200 Subject: [PATCH 0803/1323] notify: separate active notifications from unfinished kitty notifications This fixes an issue where it wasn't possible to trigger multiple notifications with the same kitty notification ID. This is something that works in kitty, and there's no reason why it shouldn't work. The issue was that we track stdout, and the notification helper's PID in the notification struct. Thus, when a notification is being displayed, we can't re-use the same notification struct instance for another notification. This patch fixes this by adding a new notification list, 'active_notifications'. Whenever we detect that we need to track the helper (notification want's to either focus the window on activation, or send an event to the application), we add a copy of the notification to the 'active' list. The notification can then be removed from the 'kitty' list, allowing kitty notifications to re-use the same ID over and over again, even if old notifications are still being displayed. --- notify.c | 36 +++++++++++++++++++++++++----------- notify.h | 2 +- osc.c | 25 +++++++++++++------------ terminal.c | 21 ++++++++++++++++----- terminal.h | 3 ++- 5 files changed, 57 insertions(+), 30 deletions(-) diff --git a/notify.c b/notify.c index 67cf5bd1..af89838b 100644 --- a/notify.c +++ b/notify.c @@ -75,7 +75,7 @@ fdm_notify_stdout(struct fdm *fdm, int fd, int events, void *data) /* Find notification */ - tll_foreach(term->notifications, it) { + tll_foreach(term->active_notifications, it) { if (it->item.stdout_fd == fd) { notif = &it->item; break; @@ -124,7 +124,7 @@ notif_done(struct reaper *reaper, pid_t pid, int status, void *data) { struct terminal *term = data; - tll_foreach(term->notifications, it) { + tll_foreach(term->active_notifications, it) { struct notification *notif = &it->item; if (notif->pid != pid) continue; @@ -148,17 +148,17 @@ notif_done(struct reaper *reaper, pid_t pid, int status, void *data) } notify_free(term, notif); - tll_remove(term->notifications, it); + tll_remove(term->active_notifications, it); return; } } bool -notify_notify(const struct terminal *term, struct notification *notif) +notify_notify(struct terminal *term, struct notification *notif) { xassert(notif->xdg_token == NULL); xassert(notif->pid == 0); - xassert(notif->stdout_fd == 0); + xassert(notif->stdout_fd <= 0); xassert(notif->stdout_data == NULL); notif->pid = -1; @@ -187,6 +187,8 @@ notify_notify(const struct terminal *term, struct notification *notif) } } + bool track_notification = notif->focus || notif->report; + LOG_DBG("notify: title=\"%s\", body=\"%s\"", title, body); xassert(title != NULL); @@ -230,13 +232,25 @@ notify_notify(const struct terminal *term, struct notification *notif) LOG_DBG(" argv[%zu] = \"%s\"", i, argv[i]); int stdout_fds[2] = {-1, -1}; - if ((notif->focus || notif->report) && - pipe2(stdout_fds, O_CLOEXEC | O_NONBLOCK) < 0) - { - LOG_WARN("failed to create stdout pipe"); - /* Non-fatal */ + if (track_notification) { + if (pipe2(stdout_fds, O_CLOEXEC | O_NONBLOCK) < 0) { + LOG_WARN("failed to create stdout pipe"); + track_notification = false; + /* Non-fatal */ + } else { + tll_push_back(term->active_notifications, *notif); + notif->id = NULL; + notif->title = NULL; + notif->body = NULL; + notif->icon_id = NULL; + notif->icon_symbolic_name= NULL; + notif->icon_data = NULL; + notif->icon_data_sz = 0; + notif = &tll_back(term->active_notifications); + } } + if (stdout_fds[0] >= 0) { xassert(notif->xdg_token == NULL); fdm_add(term->fdm, stdout_fds[0], EPOLLIN, @@ -247,7 +261,7 @@ notify_notify(const struct terminal *term, struct notification *notif) int devnull = open("/dev/null", O_RDONLY); pid_t pid = spawn( term->reaper, NULL, argv, devnull, stdout_fds[1], -1, - ¬if_done, (void *)term, NULL); + track_notification ? ¬if_done : NULL, (void *)term, NULL); if (stdout_fds[1] >= 0) { /* Close write-end of stdout pipe */ diff --git a/notify.h b/notify.h index 55ace52d..90fbf9fc 100644 --- a/notify.h +++ b/notify.h @@ -60,7 +60,7 @@ struct notification_icon { char *tmp_file_on_disk; }; -bool notify_notify(const struct terminal *term, struct notification *notif); +bool notify_notify(struct terminal *term, struct notification *notif); void notify_free(struct terminal *term, struct notification *notif); void notify_icon_add(struct terminal *term, const char *id, diff --git a/osc.c b/osc.c index 5a001b7a..41a0dc68 100644 --- a/osc.c +++ b/osc.c @@ -727,7 +727,7 @@ kitty_notification(struct terminal *term, char *string) /* Search for an existing (d=0) notification to update */ struct notification *notif = NULL; - tll_foreach(term->notifications, it) { + tll_foreach(term->kitty_notifications, it) { if (strcmp(it->item.id, id) == 0) { /* Found existing notification */ notif = &it->item; @@ -736,16 +736,17 @@ kitty_notification(struct terminal *term, char *string) } if (notif == NULL) { - tll_push_front(term->notifications, ((struct notification){ + tll_push_front(term->kitty_notifications, ((struct notification){ .id = id, .when = when, .urgency = urgency, .focus = focus, .report = report, + .stdout_fd = -1, })); id = NULL; /* Prevent double free */ - notif = &tll_front(term->notifications); + notif = &tll_front(term->kitty_notifications); } if (notif->pid > 0) { @@ -833,15 +834,15 @@ kitty_notification(struct terminal *term, char *string) * The checks for title|body is to handle notifications that * only load icon data into the icon cache */ - if ((notif->title != NULL || notif->body != NULL) && - !notify_notify(term, notif)) - { - tll_foreach(term->notifications, it) { - if (&it->item == notif) { - notify_free(term, &it->item); - tll_remove(term->notifications, it); - break; - } + if (notif->title != NULL || notif->body != NULL) { + notify_notify(term, notif); + } + + tll_foreach(term->kitty_notifications, it) { + if (&it->item == notif) { + notify_free(term, &it->item); + tll_remove(term->kitty_notifications, it); + break; } } } diff --git a/terminal.c b/terminal.c index eadb9dbc..dc4f37b6 100644 --- a/terminal.c +++ b/terminal.c @@ -1313,7 +1313,8 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED .ime_enabled = true, #endif - .notifications = tll_init(), + .kitty_notifications = tll_init(), + .active_notifications = tll_init(), }; pixman_region32_init(&term->render.last_overlay_clip); @@ -1818,9 +1819,14 @@ term_destroy(struct terminal *term) tll_remove(term->ptmx_paste_buffers, it); } - tll_foreach(term->notifications, it) { + tll_foreach(term->kitty_notifications, it) { notify_free(term, &it->item); - tll_remove(term->notifications, it); + tll_remove(term->kitty_notifications, it); + } + + tll_foreach(term->active_notifications, it) { + notify_free(term, &it->item); + tll_remove(term->active_notifications, it); } for (size_t i = 0; i < ALEN(term->notification_icons); i++) @@ -2031,9 +2037,14 @@ term_reset(struct terminal *term, bool hard) tll_remove(term->alt.sixel_images, it); } - tll_foreach(term->notifications, it) { + tll_foreach(term->kitty_notifications, it) { notify_free(term, &it->item); - tll_remove(term->notifications, it); + tll_remove(term->kitty_notifications, it); + } + + tll_foreach(term->active_notifications, it) { + notify_free(term, &it->item); + tll_remove(term->active_notifications, it); } for (size_t i = 0; i < ALEN(term->notification_icons); i++) diff --git a/terminal.h b/terminal.h index 7b726c62..92d1e8f5 100644 --- a/terminal.h +++ b/terminal.h @@ -801,7 +801,8 @@ struct terminal { /* Notifications that either haven't been sent yet, or have been sent but not yet dismissed */ - tll(struct notification) notifications; + tll(struct notification) kitty_notifications; + tll(struct notification) active_notifications; struct notification_icon notification_icons[32]; char *foot_exe; From efa5b9cea639f08ef2b9a9eb0ce6179af98ebd80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 12:12:38 +0200 Subject: [PATCH 0804/1323] osc: cleanup --- osc.c | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/osc.c b/osc.c index 41a0dc68..4fde951c 100644 --- a/osc.c +++ b/osc.c @@ -644,7 +644,7 @@ kitty_notification(struct terminal *term, char *string) break; case 'e': - /* base64: 0=utf8, 1=base64(utf8) */ + /* base64 (payload encoding): 0=utf8, 1=base64(utf8) */ if (value[0] == '0' && value[1] == '\0') base64 = false; else if (value[0] == '1' && value[1] == '\0') @@ -690,11 +690,13 @@ kitty_notification(struct terminal *term, char *string) break; case 'g': + /* graphical ID */ free(icon_id); icon_id = xstrdup(value); break; case 'n': + /* Symbolic icon name, used with 'g' */ free(symbolic_icon); symbolic_icon = xstrdup(value); break; @@ -710,21 +712,6 @@ kitty_notification(struct terminal *term, char *string) payload_size = strlen(payload); } - LOG_DBG("id=%s, done=%d, focus=%d, report=%d, base64=%d, icon=%s, payload: %s, " - "honor: %s, urgency: %s, %s: %s", - id, done, focus, report, base64, icon != NULL ? icon : "<not set>", - payload_is_title ? "title" : "body", - (when == NOTIFY_ALWAYS - ? "always" - : when == NOTIFY_UNFOCUSED - ? "unfocused" - : "invisible"), - (urgency == NOTIFY_URGENCY_LOW - ? "low" : urgency == NOTIFY_URGENCY_NORMAL - ? "normal" - : "critical"), - payload_is_title ? "title" : "body", payload); - /* Search for an existing (d=0) notification to update */ struct notification *notif = NULL; tll_foreach(term->kitty_notifications, it) { From 9814cf57792f43e95755bcf62237ae759c453645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 12:12:50 +0200 Subject: [PATCH 0805/1323] notify: clean up logging messages --- notify.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/notify.c b/notify.c index af89838b..9a9311ee 100644 --- a/notify.c +++ b/notify.c @@ -189,7 +189,8 @@ notify_notify(struct terminal *term, struct notification *notif) bool track_notification = notif->focus || notif->report; - LOG_DBG("notify: title=\"%s\", body=\"%s\"", title, body); + LOG_DBG("notify: title=\"%s\", body=\"%s\", icon=\"%s\" (tracking: %s)", + title, body, icon_name_or_path, track_notification ? "yes" : "no"); xassert(title != NULL); if (title == NULL) @@ -309,7 +310,7 @@ add_icon(struct notification_icon *icon, const char *id, const char *symbolic_na icon->tmp_file_on_disk = xstrdup(name); } - LOG_DBG("added icon to cache: %s: sym=%s, file=%s", + LOG_DBG("added icon to cache: ID=%s: sym=%s, file=%s", icon->id, icon->symbolic_name, icon->tmp_file_on_disk); } @@ -355,6 +356,7 @@ notify_icon_del(struct terminal *term, const char *id) if (icon->id == NULL || strcmp(icon->id, id) != 0) continue; + LOG_DBG("expelled %s from the notification icon cache", icon->id); notify_icon_free(icon); return; } From d0a542515549a71fa2fb7456326ab99c378d929a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 12:15:29 +0200 Subject: [PATCH 0806/1323] notify: add_icon(): check return value of write() --- notify.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/notify.c b/notify.c index 9a9311ee..f8c2bfe1 100644 --- a/notify.c +++ b/notify.c @@ -303,11 +303,14 @@ add_icon(struct notification_icon *icon, const char *id, const char *symbolic_na return; } - write(fd, data, data_sz); - close(fd); + if (write(fd, data, data_sz) != (ssize_t)data_sz) { + LOG_ERRNO("failed to write icon data to temporary file"); + } else { + LOG_DBG("wrote icon data to %s", name); + icon->tmp_file_on_disk = xstrdup(name); + } - LOG_DBG("wrote icon data to %s", name); - icon->tmp_file_on_disk = xstrdup(name); + close(fd); } LOG_DBG("added icon to cache: ID=%s: sym=%s, file=%s", From 50efd9726d041b00d3966431b8e6957a3739b33c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 12:15:37 +0200 Subject: [PATCH 0807/1323] pgo: updated stubs for notification functions --- pgo/pgo.c | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pgo/pgo.c b/pgo/pgo.c index 6c13b72a..6dc0dd10 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -152,7 +152,7 @@ void ime_disable(struct seat *seat) {} void ime_reset_preedit(struct seat *seat) {} bool -notify_notify(const struct terminal *term, struct notification *notif) +notify_notify(struct terminal *term, struct notification *notif) { return true; } @@ -162,6 +162,22 @@ notify_free(struct terminal *term, struct notification *notif) { } +void +notify_icon_add(struct terminal *term, const char *id, + const char *symbolic_name, const uint8_t *data, + size_t data_sz) +{ +} + +void +notify_icon_del(struct terminal *term, const char *id) +{ +} + +void +notify_icon_free(struct notification_icon *icon) +{ +} void reaper_add(struct reaper *reaper, pid_t pid, reaper_cb cb, void *cb_data) {} void reaper_del(struct reaper *reaper, pid_t pid) {} From e59efb12332c8e0427a97d22980376fd9e81e7b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 15:28:47 +0200 Subject: [PATCH 0808/1323] osc: remove unused includes --- osc.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/osc.c b/osc.c index 4fde951c..0e092a9f 100644 --- a/osc.c +++ b/osc.c @@ -12,15 +12,12 @@ #include "log.h" #include "base64.h" #include "config.h" -#include "grid.h" #include "macros.h" #include "notify.h" -#include "render.h" #include "selection.h" #include "terminal.h" #include "uri.h" #include "util.h" -#include "vt.h" #include "xmalloc.h" #include "xsnprintf.h" From fabfef9c82192b04a52cdd092a81d8e10ca20fbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 15:29:08 +0200 Subject: [PATCH 0809/1323] notify: consume_stdout(): fix ASAN warning This is an ASAN false positive; size is always 0 when we're passing a NULL pointer. Still, the warning is easy to avoid, so let's do that to reduce the noise level. --- notify.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/notify.c b/notify.c index f8c2bfe1..b16b941b 100644 --- a/notify.c +++ b/notify.c @@ -63,8 +63,10 @@ consume_stdout(struct notification *notif, bool eof) left -= len + (eol != NULL ? 1 : 0); } - memmove(notif->stdout_data, data, left); - notif->stdout_sz = left; + if (left > 0) { + memmove(notif->stdout_data, data, left); + notif->stdout_sz = left; + } } static bool From 55a4e59ef96227f23fa00e8c772804f67601567a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 15:30:01 +0200 Subject: [PATCH 0810/1323] notify: if there's a symbolic icon name, use it even if there's no graphical ID set. In other words, if there *is* a graphical ID, use the icon cache. Only if there is no graphical ID in the notification request do we fallback to the symbolic name. This means no icon will be displayed if there's no matching icon in the cache. Some examples. You can either pre-load the cache (with inline PNG data, a symbolic name, or both): printf '\e]99;g=123:n=firefox:p=icon:e=1;<PNG data>\e\\' printf '\e]99;g=123;this is a notification\e\\' or printf '\e]99;n=firefox;this is a notification\e\\' --- notify.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/notify.c b/notify.c index b16b941b..4261268d 100644 --- a/notify.c +++ b/notify.c @@ -187,6 +187,8 @@ notify_notify(struct terminal *term, struct notification *notif) break; } } + } else if (notif->icon_symbolic_name != NULL) { + icon_name_or_path = notif->icon_symbolic_name; } bool track_notification = notif->focus || notif->report; From 045ead985c1e320f289704b6a995d48a5eccf59e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 16:41:52 +0200 Subject: [PATCH 0811/1323] notify: don't focus/report on notification dismissal Only do it when the notification was activated. Here, activated means the 'click to activate' notification action was triggered. How do we tie everything together? First, we add a new template parameter, ${action}. It's intended to be used with e.g. notify-send's --action option. When the action is triggered, notify-send prints its name on stdout, on a separate line. Look for this in stdout. Only if we've seen it do we focus/report the notification. --- CHANGELOG.md | 2 +- config.c | 2 +- doc/foot.ini.5.scd | 31 ++++++++++++++++++++++++++----- foot.ini | 2 +- notify.c | 27 ++++++++++++++++----------- notify.h | 1 + 6 files changed, 46 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 861245b2..fe27afc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,7 +75,7 @@ * `desktop-notifications.command` option, replaces `notify`. * `desktop-notifications.inhibit-when-focused` option, replaces `notify-focus-inhibit`. -* `${icon}` and `${urgency}` added to the +* `${icon}`, `${urgency}` and `${action}` added to the `desktop-notifications.command` template. [1707]: https://codeberg.org/dnkl/foot/issues/1707 diff --git a/config.c b/config.c index a624f95c..be465abf 100644 --- a/config.c +++ b/config.c @@ -3225,7 +3225,7 @@ config_load(struct config *conf, const char *conf_path, parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers); tokenize_cmdline( - "notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} -- ${title} ${body}", + "notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action} -- ${title} ${body}", &conf->desktop_notifications.command.argv.args); tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args); diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 5135e080..8e388107 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -462,11 +462,32 @@ Note: do not set *TERM* here; use the *term* option in the main behavior depends on the notification daemon in use, and how it has been configured. - For this to work, foot needs an XDG activation token. To this - end, foot will read the command's stdout; a line prefixed with - *xdgtoken=* will be recognized as containing the XDG - activation token: + For this to work, foot needs to know when the notification was + activated (as opposed to just dismissed), and it needs an XDG + activation token. + There are two parts to handle this. First, the notification + must define an action. For this purpose, foot definse the + template parameter *${action}*. It is intended to be used with + e.g. notify-send's *-A,--action* option. The contents of + *${action}* is not configurable, but will be on the form + 'name=label', where name is a notification internal reference + to the action, and label is what is displayed in the + notification. + + Second, foot needs to know when the notification activated, + and it needs to get hold of the XDG activation token. + + Both are expected to be printed on stdout. + + Foot expects the action name (not label) to be printed on a + single line. No prefix, no postfix. + + Foot expects the activation token to be printed on a single + line, prefixed with *xdgtoken=*. + + Example: + activate-foot xdgtoken=18179adf579a7a904ce73754964b1ec3 The expected format of stdout may change at any time. Please @@ -491,7 +512,7 @@ Note: do not set *TERM* here; use the *term* option in the main For *notify-send*, this can be achieved with the *--wait* option. - Default: _notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} -- ${title} ${body}_. + Default: _notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action} -- ${title} ${body}_. *inhibit-when-focused* Boolean. If enabled, foot will not display notifications if the diff --git a/foot.ini b/foot.ini index 33727dd3..707f09a2 100644 --- a/foot.ini +++ b/foot.ini @@ -47,7 +47,7 @@ # command-focused=no [desktop-notifications] -# command=notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} -- ${title} ${body} +# command=notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action} -- ${title} ${body} # inhibit-when-focused=yes diff --git a/notify.c b/notify.c index 4261268d..d12c3081 100644 --- a/notify.c +++ b/notify.c @@ -54,8 +54,11 @@ consume_stdout(struct notification *notif, bool eof) } else if (!eof) break; + if (strcmp(line, "activate-foot") == 0) + notif->activated = true; + /* Check for 'xdgtoken=xyz' */ - if (len > 9 && memcmp(line, "xdgtoken=", 9) == 0) { + else if (len > 9 && memcmp(line, "xdgtoken=", 9) == 0) { notif->xdg_token = xstrndup(&line[9], len - 9); LOG_DBG("XDG token: \"%s\"", notif->xdg_token); } @@ -133,12 +136,13 @@ notif_done(struct reaper *reaper, pid_t pid, int status, void *data) LOG_DBG("notification %s dismissed", notif->id); - if (notif->focus) { - LOG_DBG("focus window on notification activation: \"%s\"", notif->xdg_token); + if (notif->activated && notif->focus) { + LOG_DBG("focus window on notification activation: \"%s\"", + notif->xdg_token); wayl_activate(term->wl, term->window, notif->xdg_token); } - if (notif->report) { + if (notif->activated && notif->report) { xassert(notif->id != NULL); LOG_DBG("sending notification report to client"); @@ -221,13 +225,14 @@ notify_notify(struct terminal *term, struct notification *notif) ? "normal" : "critical"; if (!spawn_expand_template( - &term->conf->desktop_notifications.command, 6, - (const char *[]){ - "app-id", "window-title", "icon", "title", "body", "urgency"}, - (const char *[]){ - term->app_id ? term->app_id : term->conf->app_id, - term->window_title, icon_name_or_path, title, body, urgency_str}, - &argc, &argv)) + &term->conf->desktop_notifications.command, 7, + (const char *[]){ + "app-id", "window-title", "icon", "title", "body", "urgency", "action"}, + (const char *[]){ + term->app_id ? term->app_id : term->conf->app_id, + term->window_title, icon_name_or_path, title, body, urgency_str, + "activate-foot=Click to activate"}, + &argc, &argv)) { return false; } diff --git a/notify.h b/notify.h index 90fbf9fc..ba017276 100644 --- a/notify.h +++ b/notify.h @@ -45,6 +45,7 @@ struct notification { * Used internally by notify */ + bool activated; /* User 'activated' the notification */ char *xdg_token; /* XDG activation token, from daemon */ pid_t pid; /* Notifier command PID */ From 79832c16e2db2bef79c03aa5eba7cf863436dd67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 16:48:15 +0200 Subject: [PATCH 0812/1323] notify: name the activation action 'default' This is less unique, but also works better with notification daemons that trigger the 'default' action when e.g. clicked. --- notify.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notify.c b/notify.c index d12c3081..9cf44251 100644 --- a/notify.c +++ b/notify.c @@ -54,7 +54,7 @@ consume_stdout(struct notification *notif, bool eof) } else if (!eof) break; - if (strcmp(line, "activate-foot") == 0) + if (strcmp(line, "default") == 0) notif->activated = true; /* Check for 'xdgtoken=xyz' */ @@ -231,7 +231,7 @@ notify_notify(struct terminal *term, struct notification *notif) (const char *[]){ term->app_id ? term->app_id : term->conf->app_id, term->window_title, icon_name_or_path, title, body, urgency_str, - "activate-foot=Click to activate"}, + "default=Click to activate"}, &argc, &argv)) { return false; From 511d4817d307f75ba68640ab1f394bafaae02844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 16:52:18 +0200 Subject: [PATCH 0813/1323] doc: foot.ini: desktop-notification: remove 'notification dismissal' --- doc/foot.ini.5.scd | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 8e388107..40106223 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -457,7 +457,9 @@ Note: do not set *TERM* here; use the *term* option in the main Window activation (focusing) Foot can focus the window when the notification is - "activated". This typically happens when the default action is + 'activated'. It can also send an event back to the client + application, notifying it that the notification has been + 'activated', This typically happens when the default action is invoked, and/or when the notification is clicked, but exact behavior depends on the notification daemon in use, and how it has been configured. @@ -497,21 +499,6 @@ Note: do not set *TERM* here; use the *term* option in the main reporting the XDG activation token in any way. This means window activation will not work by default. - Notification dismissal - The kitty desktop notifications protocol (OSC-99) allows the - terminal application to request an event be sent to it when - the notification has been dismissed (by setting *a=report* in - the notification request). - - To be able to send this event, foot needs to know when the - notification is dismissed. This is handled in a very simple - manner; the command signals notification dismissal by - exiting. That is, as soon as the command returns, foot - considers the notification dismissed. - - For *notify-send*, this can be achieved with the *--wait* - option. - Default: _notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action} -- ${title} ${body}_. *inhibit-when-focused* From 70b4638a754048eeffc877405222d4750c3ea548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 18:32:23 +0200 Subject: [PATCH 0814/1323] osc: kitty notifications: implement query --- osc.c | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/osc.c b/osc.c index 0e092a9f..6a04386c 100644 --- a/osc.c +++ b/osc.c @@ -567,17 +567,19 @@ kitty_notification(struct terminal *term, char *string) { /* https://sw.kovidgoyal.net/kitty/desktop-notifications */ - char *payload = strchr(string, ';'); - if (payload == NULL) + char *payload_raw = strchr(string, ';'); + if (payload_raw == NULL) return; char *parameters = string; - *payload = '\0'; - payload++; + *payload_raw = '\0'; + payload_raw++; char *id = xstrdup("0"); /* The 'i' parameter */ char *icon_id = NULL; /* The 'g' parameter */ char *symbolic_icon = NULL; /* The 'n' parameter */ + char *payload = NULL; + bool focus = true; /* The 'a' parameter */ bool report = false; /* The 'a' parameter */ bool done = true; /* The 'd' parameter */ @@ -662,6 +664,25 @@ kitty_notification(struct terminal *term, char *string) payload_type = PAYLOAD_BODY; else if (strcmp(value, "icon") == 0) payload_type = PAYLOAD_ICON; + else if (strcmp(value, "?") == 0) { + /* Query capabilities */ + + char when_str[64]; + strcpy(when_str, "unfocused"); + if (!term->conf->desktop_notifications.inhibit_when_focused) + strcat(when_str, ",always"); + + const char *terminator = term->vt.osc.bel ? "\a" : "\033\\"; + + char reply[128]; + int n = xsnprintf( + reply, sizeof(reply), + "\033]99;i=%s:p=?;p=title,body,?:a=focus,report:o=%s:u=0,1,2%s", + id, when_str, terminator); + + term_to_slave(term, reply, n); + goto out; + } break; case 'o': @@ -701,11 +722,11 @@ kitty_notification(struct terminal *term, char *string) } if (base64) { - payload = base64_decode(payload, &payload_size); + payload = base64_decode(payload_raw, &payload_size); if (payload == NULL) goto out; } else { - payload = xstrdup(payload); + payload = xstrdup(payload_raw); payload_size = strlen(payload); } From d5c773a58b2cdadf7aee2fe16664c168e750b953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 18:48:45 +0200 Subject: [PATCH 0815/1323] notify: bug: always adjust amount of data left in stdout buffer --- notify.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/notify.c b/notify.c index 9cf44251..2dd303db 100644 --- a/notify.c +++ b/notify.c @@ -66,10 +66,10 @@ consume_stdout(struct notification *notif, bool eof) left -= len + (eol != NULL ? 1 : 0); } - if (left > 0) { + if (left > 0) memmove(notif->stdout_data, data, left); - notif->stdout_sz = left; - } + + notif->stdout_sz = left; } static bool From ecbec57a4749cc4162d2bb55dd14e1890b6dff31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 19:08:21 +0200 Subject: [PATCH 0816/1323] notify: split up the ${action} template parameter Split it up into two, ${action-name} and ${action-label}. Dunstify, for example, has a different syntax compared to notify-send: notify-send: default=foobar dunstify: default,foobar --- CHANGELOG.md | 4 ++-- config.c | 2 +- doc/foot.ini.5.scd | 14 ++++++-------- foot.ini | 2 +- notify.c | 10 ++++------ 5 files changed, 14 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe27afc5..65ddfc3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,8 +75,8 @@ * `desktop-notifications.command` option, replaces `notify`. * `desktop-notifications.inhibit-when-focused` option, replaces `notify-focus-inhibit`. -* `${icon}`, `${urgency}` and `${action}` added to the - `desktop-notifications.command` template. +* `${icon}`, `${urgency}`,`${action-name}` and `${action-label}` added + to the `desktop-notifications.command` template. [1707]: https://codeberg.org/dnkl/foot/issues/1707 [1738]: https://codeberg.org/dnkl/foot/issues/1738 diff --git a/config.c b/config.c index be465abf..307d348d 100644 --- a/config.c +++ b/config.c @@ -3225,7 +3225,7 @@ config_load(struct config *conf, const char *conf_path, parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers); tokenize_cmdline( - "notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action} -- ${title} ${body}", + "notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action-name},${action-label} --print-id -- ${title} ${body}", &conf->desktop_notifications.command.argv.args); tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args); diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 40106223..a6c20e6e 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -470,12 +470,9 @@ Note: do not set *TERM* here; use the *term* option in the main There are two parts to handle this. First, the notification must define an action. For this purpose, foot definse the - template parameter *${action}*. It is intended to be used with - e.g. notify-send's *-A,--action* option. The contents of - *${action}* is not configurable, but will be on the form - 'name=label', where name is a notification internal reference - to the action, and label is what is displayed in the - notification. + template parameters *${action-name}* and + *${action-label}*. They are intended to be used with + e.g. notify-send's *-A,--action* option. Second, foot needs to know when the notification activated, and it needs to get hold of the XDG activation token. @@ -489,7 +486,7 @@ Note: do not set *TERM* here; use the *term* option in the main line, prefixed with *xdgtoken=*. Example: - activate-foot + default xdgtoken=18179adf579a7a904ce73754964b1ec3 The expected format of stdout may change at any time. Please @@ -499,7 +496,8 @@ Note: do not set *TERM* here; use the *term* option in the main reporting the XDG activation token in any way. This means window activation will not work by default. - Default: _notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action} -- ${title} ${body}_. + Default: _notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action-name},${action-label} --print-id -- ${title} ${body}_. + *inhibit-when-focused* Boolean. If enabled, foot will not display notifications if the diff --git a/foot.ini b/foot.ini index 707f09a2..c743182c 100644 --- a/foot.ini +++ b/foot.ini @@ -47,7 +47,7 @@ # command-focused=no [desktop-notifications] -# command=notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action} -- ${title} ${body} +# command=notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action-name},${action-label} --print-id -- ${title} ${body} # inhibit-when-focused=yes diff --git a/notify.c b/notify.c index 2dd303db..71852c6a 100644 --- a/notify.c +++ b/notify.c @@ -78,7 +78,6 @@ fdm_notify_stdout(struct fdm *fdm, int fd, int events, void *data) const struct terminal *term = data; struct notification *notif = NULL; - /* Find notification */ tll_foreach(term->active_notifications, it) { if (it->item.stdout_fd == fd) { @@ -225,13 +224,13 @@ notify_notify(struct terminal *term, struct notification *notif) ? "normal" : "critical"; if (!spawn_expand_template( - &term->conf->desktop_notifications.command, 7, + &term->conf->desktop_notifications.command, 8, (const char *[]){ - "app-id", "window-title", "icon", "title", "body", "urgency", "action"}, + "app-id", "window-title", "icon", "title", "body", "urgency", "action-name", "action-label"}, (const char *[]){ term->app_id ? term->app_id : term->conf->app_id, term->window_title, icon_name_or_path, title, body, urgency_str, - "default=Click to activate"}, + "default", "Click to activate"}, &argc, &argv)) { return false; @@ -253,7 +252,7 @@ notify_notify(struct terminal *term, struct notification *notif) notif->title = NULL; notif->body = NULL; notif->icon_id = NULL; - notif->icon_symbolic_name= NULL; + notif->icon_symbolic_name = NULL; notif->icon_data = NULL; notif->icon_data_sz = 0; notif = &tll_back(term->active_notifications); @@ -262,7 +261,6 @@ notify_notify(struct terminal *term, struct notification *notif) if (stdout_fds[0] >= 0) { - xassert(notif->xdg_token == NULL); fdm_add(term->fdm, stdout_fds[0], EPOLLIN, &fdm_notify_stdout, (void *)term); } From 24168ed86ecf20a44f6dce8bdacfcb2c1471844b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 24 Jul 2024 15:58:19 +0200 Subject: [PATCH 0817/1323] osc: kitty notifications: don't include '?' in the query reply No need to say we support queries, in the query reply... --- osc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osc.c b/osc.c index 6a04386c..c5073ff6 100644 --- a/osc.c +++ b/osc.c @@ -677,7 +677,7 @@ kitty_notification(struct terminal *term, char *string) char reply[128]; int n = xsnprintf( reply, sizeof(reply), - "\033]99;i=%s:p=?;p=title,body,?:a=focus,report:o=%s:u=0,1,2%s", + "\033]99;i=%s:p=?;p=title,body,icon:a=focus,report:o=%s:u=0,1,2%s", id, when_str, terminator); term_to_slave(term, reply, n); From 37ab3b1603ce3697d69d28129b5941dac843e992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 24 Jul 2024 15:59:52 +0200 Subject: [PATCH 0818/1323] notify: don't create icon file on disk when we're not going to use it We always prefer the symbolic name. Thus, there's no need to write raw PNG data to disk if we have a symbolic name. Furthermore, keep the file open until the cache entry is evicted. --- notify.c | 37 +++++++++++++++++++++++++------------ notify.h | 3 ++- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/notify.c b/notify.c index 71852c6a..4efddec2 100644 --- a/notify.c +++ b/notify.c @@ -186,7 +186,7 @@ notify_notify(struct terminal *term, struct notification *notif) if (icon->id != NULL && strcmp(icon->id, notif->icon_id) == 0) { icon_name_or_path = icon->symbolic_name != NULL ? icon->symbolic_name - : icon->tmp_file_on_disk; + : icon->tmp_file_name; break; } } @@ -299,11 +299,21 @@ add_icon(struct notification_icon *icon, const char *id, const char *symbolic_na { icon->id = xstrdup(id); icon->symbolic_name = symbolic_name != NULL ? xstrdup(symbolic_name) : NULL; - icon->tmp_file_on_disk = NULL; + icon->tmp_file_name = NULL; + icon->tmp_file_fd = -1; - if (data_sz > 0) { + /* + * Dump in-line data to a temporary file. This allows us to pass + * the filename as a parameter to notification helpers + * (i.e. notify-send -i <path>). + * + * Optimization: since we always prefer (i.e. use) the symbolic + * name if present, there's no need to create a file on disk if we + * have a symbolic name. + */ + if (symbolic_name == NULL && data_sz > 0) { char name[64] = "/tmp/foot-notification-icon-cache-XXXXXX"; - int fd = mkstemp(name); + int fd = mkostemp(name, O_CLOEXEC); if (fd < 0) { LOG_ERRNO("failed to create temporary file for icon cache"); @@ -312,16 +322,16 @@ add_icon(struct notification_icon *icon, const char *id, const char *symbolic_na if (write(fd, data, data_sz) != (ssize_t)data_sz) { LOG_ERRNO("failed to write icon data to temporary file"); + close(fd); } else { LOG_DBG("wrote icon data to %s", name); - icon->tmp_file_on_disk = xstrdup(name); + icon->tmp_file_name = xstrdup(name); + icon->tmp_file_fd = fd; } - - close(fd); } LOG_DBG("added icon to cache: ID=%s: sym=%s, file=%s", - icon->id, icon->symbolic_name, icon->tmp_file_on_disk); + icon->id, icon->symbolic_name, icon->tmp_file_name); } void @@ -375,14 +385,17 @@ notify_icon_del(struct terminal *term, const char *id) void notify_icon_free(struct notification_icon *icon) { - if (icon->tmp_file_on_disk != NULL) - unlink(icon->tmp_file_on_disk); + if (icon->tmp_file_fd >= 0) + close(icon->tmp_file_fd); + if (icon->tmp_file_name != NULL) + unlink(icon->tmp_file_name); free(icon->id); free(icon->symbolic_name); - free(icon->tmp_file_on_disk); + free(icon->tmp_file_name); icon->id = NULL; icon->symbolic_name = NULL; - icon->tmp_file_on_disk = NULL; + icon->tmp_file_name = NULL; + icon->tmp_file_fd = -1; } diff --git a/notify.h b/notify.h index ba017276..d78af5fa 100644 --- a/notify.h +++ b/notify.h @@ -58,7 +58,8 @@ struct notification { struct notification_icon { char *id; char *symbolic_name; - char *tmp_file_on_disk; + char *tmp_file_name; + int tmp_file_fd; }; bool notify_notify(struct terminal *term, struct notification *notif); From e271027c0c5166994f4f698befefe0b6a5c382f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 24 Jul 2024 16:01:42 +0200 Subject: [PATCH 0819/1323] config: notify-send: it's "action=label", not "action,label" --- config.c | 2 +- doc/foot.ini.5.scd | 2 +- foot.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config.c b/config.c index 307d348d..664474ca 100644 --- a/config.c +++ b/config.c @@ -3225,7 +3225,7 @@ config_load(struct config *conf, const char *conf_path, parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers); tokenize_cmdline( - "notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action-name},${action-label} --print-id -- ${title} ${body}", + "notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action-name}=${action-label} --print-id -- ${title} ${body}", &conf->desktop_notifications.command.argv.args); tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args); diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index a6c20e6e..ef48323f 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -496,7 +496,7 @@ Note: do not set *TERM* here; use the *term* option in the main reporting the XDG activation token in any way. This means window activation will not work by default. - Default: _notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action-name},${action-label} --print-id -- ${title} ${body}_. + Default: _notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action-name}=${action-label} --print-id -- ${title} ${body}_. *inhibit-when-focused* diff --git a/foot.ini b/foot.ini index c743182c..46b2d5f0 100644 --- a/foot.ini +++ b/foot.ini @@ -47,7 +47,7 @@ # command-focused=no [desktop-notifications] -# command=notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action-name},${action-label} --print-id -- ${title} ${body} +# command=notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action-name}=${action-label} --print-id -- ${title} ${body} # inhibit-when-focused=yes From f56da385fe4c4434bfc10c362cbdda58c63d238f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 24 Jul 2024 16:02:19 +0200 Subject: [PATCH 0820/1323] notify: try to read the daemon assigned notification ID from stdout And document the things we recognize in the notification helper's stdout. --- doc/foot.ini.5.scd | 15 +++++++++++++++ notify.c | 43 ++++++++++++++++++++++++++++++++++++++++--- notify.h | 1 + 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index ef48323f..72e6d052 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -496,6 +496,21 @@ Note: do not set *TERM* here; use the *term* option in the main reporting the XDG activation token in any way. This means window activation will not work by default. + Stdout + Foot recognizes the following things from the notification + helper's stdout: + + - _nnn_: integer in base 10, daemon assigned notification ID + - _id=nnn_: same as plain _nnn_. + - _default_: the 'default' action was triggered + - _action=default_: same as _default_ + - _xdgtoken=xyz_: XDG activation token. + + Example: + 17++ +action=default++ +xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 + Default: _notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action-name}=${action-label} --print-id -- ${title} ${body}_. diff --git a/notify.c b/notify.c index 4efddec2..986b3b7b 100644 --- a/notify.c +++ b/notify.c @@ -34,6 +34,27 @@ notify_free(struct terminal *term, struct notification *notif) free(notif->stdout_data); } +static bool +to_integer(const char *line, size_t len, uint32_t *res) +{ + bool is_id = true; + uint32_t maybe_id = 0; + + for (size_t i = 0; i < len; i++) { + char digit = line[i]; + if (digit < '0' || digit > '9') { + is_id = false; + break; + } + + maybe_id *= 10; + maybe_id += digit - '0'; + } + + *res = maybe_id; + return is_id; +} + static void consume_stdout(struct notification *notif, bool eof) { @@ -54,10 +75,26 @@ consume_stdout(struct notification *notif, bool eof) } else if (!eof) break; - if (strcmp(line, "default") == 0) - notif->activated = true; + uint32_t maybe_id = 0; - /* Check for 'xdgtoken=xyz' */ + /* Check for daemon assigned ID, either '123', or 'id=123' */ + if (to_integer(line, len, &maybe_id) || + (len > 3 && memcmp(line, "id=", 3) == 0 && + to_integer(&line[3], len - 3, &maybe_id))) + { + notif->external_id = maybe_id; + LOG_DBG("external ID: %u", notif->external_id); + } + + /* Check for triggered action, either 'default' or 'action=default' */ + else if ((len == 7 && memcmp(line, "default", 7) == 0) || + (len == 7 + 7 && memcmp(line, "action=default", 7 + 7) == 0)) + { + notif->activated = true; + LOG_DBG("notification's default action was triggered"); + } + + /* Check for XDG activation token, 'xdgtoken=xyz' */ else if (len > 9 && memcmp(line, "xdgtoken=", 9) == 0) { notif->xdg_token = xstrndup(&line[9], len - 9); LOG_DBG("XDG token: \"%s\"", notif->xdg_token); diff --git a/notify.h b/notify.h index d78af5fa..231495dc 100644 --- a/notify.h +++ b/notify.h @@ -45,6 +45,7 @@ struct notification { * Used internally by notify */ + uint32_t external_id; /* Daemon assigned notification ID */ bool activated; /* User 'activated' the notification */ char *xdg_token; /* XDG activation token, from daemon */ From a6bc9cafafd2edb2d88286cbdf4e57e331a86347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 24 Jul 2024 16:04:14 +0200 Subject: [PATCH 0821/1323] osc+notify: strcmp() -> streq() --- notify.c | 6 +++--- osc.c | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/notify.c b/notify.c index 986b3b7b..05ef6f58 100644 --- a/notify.c +++ b/notify.c @@ -220,7 +220,7 @@ notify_notify(struct terminal *term, struct notification *notif) for (size_t i = 0; i < ALEN(term->notification_icons); i++) { const struct notification_icon *icon = &term->notification_icons[i]; - if (icon->id != NULL && strcmp(icon->id, notif->icon_id) == 0) { + if (icon->id != NULL && streq(icon->id, notif->icon_id)) { icon_name_or_path = icon->symbolic_name != NULL ? icon->symbolic_name : icon->tmp_file_name; @@ -378,7 +378,7 @@ notify_icon_add(struct terminal *term, const char *id, #if defined(_DEBUG) for (size_t i = 0; i < ALEN(term->notification_icons); i++) { struct notification_icon *icon = &term->notification_icons[i]; - if (icon->id != NULL && strcmp(icon->id, id) == 0) { + if (icon->id != NULL && streq(icon->id, id)) { BUG("notification icon cache already contains \"%s\"", id); } } @@ -410,7 +410,7 @@ notify_icon_del(struct terminal *term, const char *id) for (size_t i = 0; i < ALEN(term->notification_icons); i++) { struct notification_icon *icon = &term->notification_icons[i]; - if (icon->id == NULL || strcmp(icon->id, id) != 0) + if (icon->id == NULL || !streq(icon->id, id)) continue; LOG_DBG("expelled %s from the notification icon cache", icon->id); diff --git a/osc.c b/osc.c index c5073ff6..19d9f097 100644 --- a/osc.c +++ b/osc.c @@ -625,9 +625,9 @@ kitty_notification(struct terminal *term, char *string) if (reverse) v++; - if (strcmp(v, "focus") == 0) + if (streq(v, "focus")) focus = !reverse; - else if (strcmp(v, "report") == 0) + else if (streq(v, "report")) report = !reverse; } @@ -658,13 +658,13 @@ kitty_notification(struct terminal *term, char *string) case 'p': /* payload content: title|body */ - if (strcmp(value, "title") == 0) + if (streq(value, "title")) payload_type = PAYLOAD_TITLE; - else if (strcmp(value, "body") == 0) + else if (streq(value, "body")) payload_type = PAYLOAD_BODY; - else if (strcmp(value, "icon") == 0) + else if (streq(value, "icon")) payload_type = PAYLOAD_ICON; - else if (strcmp(value, "?") == 0) { + else if (streq(value, "?")) { /* Query capabilities */ char when_str[64]; @@ -688,11 +688,11 @@ kitty_notification(struct terminal *term, char *string) case 'o': /* honor when: always|unfocused|invisible */ have_o = true; - if (strcmp(value, "always") == 0) + if (streq(value, "always")) when = NOTIFY_ALWAYS; - else if (strcmp(value, "unfocused") == 0) + else if (streq(value, "unfocused")) when = NOTIFY_UNFOCUSED; - else if (strcmp(value, "invisible") == 0) + else if (streq(value, "invisible")) when = NOTIFY_INVISIBLE; break; @@ -733,7 +733,7 @@ kitty_notification(struct terminal *term, char *string) /* Search for an existing (d=0) notification to update */ struct notification *notif = NULL; tll_foreach(term->kitty_notifications, it) { - if (strcmp(it->item.id, id) == 0) { + if (streq(it->item.id, id)) { /* Found existing notification */ notif = &it->item; break; From 0ce4ef60006b8c2feb855d6b7a992a74bac40ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 25 Jul 2024 18:45:04 +0200 Subject: [PATCH 0822/1323] notify: kitty notifications: fix ID string in activation event We forgot to prefix the notification ID with 'i=' --- notify.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notify.c b/notify.c index 05ef6f58..a417311a 100644 --- a/notify.c +++ b/notify.c @@ -183,9 +183,9 @@ notif_done(struct reaper *reaper, pid_t pid, int status, void *data) LOG_DBG("sending notification report to client"); - char reply[5 + strlen(notif->id) + 1 + 2 + 1]; + char reply[7 + strlen(notif->id) + 1 + 2 + 1]; int n = xsnprintf( - reply, sizeof(reply), "\033]99;%s;\033\\", notif->id); + reply, sizeof(reply), "\033]99;i=%s;\033\\", notif->id); term_to_slave(term, reply, n); } From d53f0aea7560c71c8262c7688bdf7735d5b3fa26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 25 Jul 2024 18:35:15 +0200 Subject: [PATCH 0823/1323] notify: rename 'report' -> 'report_activated' --- notify.c | 4 ++-- notify.h | 2 +- osc.c | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/notify.c b/notify.c index a417311a..60b2bced 100644 --- a/notify.c +++ b/notify.c @@ -178,7 +178,7 @@ notif_done(struct reaper *reaper, pid_t pid, int status, void *data) wayl_activate(term->wl, term->window, notif->xdg_token); } - if (notif->activated && notif->report) { + if (notif->activated && notif->report_activated) { xassert(notif->id != NULL); LOG_DBG("sending notification report to client"); @@ -231,7 +231,7 @@ notify_notify(struct terminal *term, struct notification *notif) icon_name_or_path = notif->icon_symbolic_name; } - bool track_notification = notif->focus || notif->report; + bool track_notification = notif->focus || notif->report_activated; LOG_DBG("notify: title=\"%s\", body=\"%s\", icon=\"%s\" (tracking: %s)", title, body, icon_name_or_path, track_notification ? "yes" : "no"); diff --git a/notify.h b/notify.h index 231495dc..cca70718 100644 --- a/notify.h +++ b/notify.h @@ -39,7 +39,7 @@ struct notification { enum notify_when when; enum notify_urgency urgency; bool focus; - bool report; + bool report_activated; /* * Used internally by notify diff --git a/osc.c b/osc.c index 19d9f097..42dc56ad 100644 --- a/osc.c +++ b/osc.c @@ -746,7 +746,7 @@ kitty_notification(struct terminal *term, char *string) .when = when, .urgency = urgency, .focus = focus, - .report = report, + .report_activated = report, .stdout_fd = -1, })); @@ -762,7 +762,7 @@ kitty_notification(struct terminal *term, char *string) /* Update notification metadata */ if (have_a) { notif->focus = focus; - notif->report = report; + notif->report_activated = report; } if (have_o) From c7972229305cfa76d402507a852af321198fdd2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 25 Jul 2024 18:47:23 +0200 Subject: [PATCH 0824/1323] osc: kitty notification: implement 'close' events Application can now request to receive a 'close' event when the notification is closed (but not necessarily activated), by adding 'c=1' to the notification request. --- notify.c | 11 ++++++++++- notify.h | 1 + osc.c | 24 +++++++++++++++++++----- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/notify.c b/notify.c index 60b2bced..68ba83f3 100644 --- a/notify.c +++ b/notify.c @@ -181,7 +181,7 @@ notif_done(struct reaper *reaper, pid_t pid, int status, void *data) if (notif->activated && notif->report_activated) { xassert(notif->id != NULL); - LOG_DBG("sending notification report to client"); + LOG_DBG("sending notification activation event to client"); char reply[7 + strlen(notif->id) + 1 + 2 + 1]; int n = xsnprintf( @@ -189,6 +189,15 @@ notif_done(struct reaper *reaper, pid_t pid, int status, void *data) term_to_slave(term, reply, n); } + if (notif->report_closed) { + LOG_DBG("sending notification close event to client"); + + char reply[7 + strlen(notif->id) + 1 + 7 + 1 + 2 + 1]; + int n = xsnprintf( + reply, sizeof(reply), "\033]99;i=%s:p=close;\033\\", notif->id); + term_to_slave(term, reply, n); + } + notify_free(term, notif); tll_remove(term->active_notifications, it); return; diff --git a/notify.h b/notify.h index cca70718..e19c4fcd 100644 --- a/notify.h +++ b/notify.h @@ -40,6 +40,7 @@ struct notification { enum notify_urgency urgency; bool focus; bool report_activated; + bool report_closed; /* * Used internally by notify diff --git a/osc.c b/osc.c index 42dc56ad..1da32447 100644 --- a/osc.c +++ b/osc.c @@ -581,7 +581,8 @@ kitty_notification(struct terminal *term, char *string) char *payload = NULL; bool focus = true; /* The 'a' parameter */ - bool report = false; /* The 'a' parameter */ + bool report_activated = false; /* The 'a' parameter */ + bool report_closed = false; /* The 'c' parameter */ bool done = true; /* The 'd' parameter */ bool base64 = false; /* The 'e' parameter */ @@ -596,6 +597,7 @@ kitty_notification(struct terminal *term, char *string) enum notify_urgency urgency = NOTIFY_URGENCY_NORMAL; bool have_a = false; + bool have_c = false; bool have_o = false; bool have_u = false; @@ -628,12 +630,20 @@ kitty_notification(struct terminal *term, char *string) if (streq(v, "focus")) focus = !reverse; else if (streq(v, "report")) - report = !reverse; + report_activated = !reverse; } break; } + case 'c': + if (value[0] == '1' && value[1] == '\0') + report_closed = true; + else if (value[0] == '0' && value[1] == '\0') + report_closed = false; + have_c = true; + break; + case 'd': /* done: 0|1 */ if (value[0] == '0' && value[1] == '\0') @@ -677,7 +687,7 @@ kitty_notification(struct terminal *term, char *string) char reply[128]; int n = xsnprintf( reply, sizeof(reply), - "\033]99;i=%s:p=?;p=title,body,icon:a=focus,report:o=%s:u=0,1,2%s", + "\033]99;i=%s:p=?;p=title,body,icon:a=focus,report:o=%s:u=0,1,2:c=1%s", id, when_str, terminator); term_to_slave(term, reply, n); @@ -746,7 +756,8 @@ kitty_notification(struct terminal *term, char *string) .when = when, .urgency = urgency, .focus = focus, - .report_activated = report, + .report_activated = report_activated, + .report_closed = report_closed, .stdout_fd = -1, })); @@ -762,9 +773,12 @@ kitty_notification(struct terminal *term, char *string) /* Update notification metadata */ if (have_a) { notif->focus = focus; - notif->report_activated = report; + notif->report_activated = report_activated; } + if (have_c) + notif->report_closed = report_closed; + if (have_o) notif->when = when; if (have_u) From c4d9f8a8ff4d65b43defb8d63e775e8b91f82bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 25 Jul 2024 19:24:28 +0200 Subject: [PATCH 0825/1323] osc: kitty notifications: implement the 'close' request Add a new config option, desktop-notifications.close, defining what to execute to close a notification. It has a single template parameter, ${id}, that is expanded to the external notification ID foot may have picked up from the notification helper. notify-send does not support closing notifications, and it appears impossible to pass an *unsigned* integer as argument to gdbus. Hence no default value for the new 'close' option. Example: printf '\e]99;i=123;this is a notification\e\\' printf '\e]99;i=123:p=close;\e\\' --- CHANGELOG.md | 3 +++ config.c | 9 +++++++++ config.h | 1 + doc/foot.ini.5.scd | 12 ++++++++++++ foot.ini | 1 + notify.c | 45 +++++++++++++++++++++++++++++++++++++++++++++ notify.h | 1 + osc.c | 29 ++++++++++++++++++++--------- 8 files changed, 92 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65ddfc3b..3394c0ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,9 @@ `notify-focus-inhibit`. * `${icon}`, `${urgency}`,`${action-name}` and `${action-label}` added to the `desktop-notifications.command` template. +* `desktop-notifications.close` option, defining what to execute when + an application wants to close an existing notification (via an + OSC-99 escape sequence). [1707]: https://codeberg.org/dnkl/foot/issues/1707 [1738]: https://codeberg.org/dnkl/foot/issues/1738 diff --git a/config.c b/config.c index 664474ca..409469ef 100644 --- a/config.c +++ b/config.c @@ -1110,6 +1110,9 @@ parse_section_desktop_notifications(struct context *ctx) if (streq(key, "command")) return value_to_spawn_template( ctx, &conf->desktop_notifications.command); + else if (streq(key, "close")) + return value_to_spawn_template( + ctx, &conf->desktop_notifications.close); else if (streq(key, "inhibit-when-focused")) return value_to_bool( ctx, &conf->desktop_notifications.inhibit_when_focused); @@ -3186,6 +3189,9 @@ config_load(struct config *conf, const char *conf_path, .command = { .argv = {.args = NULL}, }, + .close = { + .argv = {.args = NULL}, + }, .inhibit_when_focused = true, }, @@ -3481,6 +3487,8 @@ config_clone(const struct config *old) spawn_template_clone(&conf->bell.command, &old->bell.command); spawn_template_clone(&conf->desktop_notifications.command, &old->desktop_notifications.command); + spawn_template_clone(&conf->desktop_notifications.close, + &old->desktop_notifications.close); for (size_t i = 0; i < ALEN(conf->fonts); i++) config_font_list_clone(&conf->fonts[i], &old->fonts[i]); @@ -3563,6 +3571,7 @@ config_free(struct config *conf) spawn_template_free(&conf->bell.command); free(conf->scrollback.indicator.text); spawn_template_free(&conf->desktop_notifications.command); + spawn_template_free(&conf->desktop_notifications.close); for (size_t i = 0; i < ALEN(conf->fonts); i++) config_font_list_destroy(&conf->fonts[i]); free(conf->server_socket_path); diff --git a/config.h b/config.h index b3688f28..20fea3fc 100644 --- a/config.h +++ b/config.h @@ -340,6 +340,7 @@ struct config { struct { struct config_spawn_template command; + struct config_spawn_template close; bool inhibit_when_focused; } desktop_notifications; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 72e6d052..195baafe 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -513,6 +513,18 @@ xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 Default: _notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action-name}=${action-label} --print-id -- ${title} ${body}_. +*close* + Command to execute to close an existing notification. + + _${id}_ is expanded to the ID of the notification that should be + closed. For example: + + fyi --close ${id} + + Closing a notification is only supported by the Kitty Desktop + Notification protocol, OSC-99. + + Default: _not set_ *inhibit-when-focused* Boolean. If enabled, foot will not display notifications if the diff --git a/foot.ini b/foot.ini index 46b2d5f0..cead651a 100644 --- a/foot.ini +++ b/foot.ini @@ -48,6 +48,7 @@ [desktop-notifications] # command=notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action-name}=${action-label} --print-id -- ${title} ${body} +# close="" # inhibit-when-focused=yes diff --git a/notify.c b/notify.c index 68ba83f3..fa547126 100644 --- a/notify.c +++ b/notify.c @@ -339,6 +339,51 @@ notify_notify(struct terminal *term, struct notification *notif) return true; } +void +notify_close(struct terminal *term, const char *id) +{ + LOG_DBG("close notification %s", id); + + if (term->conf->desktop_notifications.close.argv.args == NULL) + return; + + tll_foreach(term->active_notifications, it) { + const struct notification *notif = &it->item; + if (notif->id == 0 || !streq(notif->id, id)) + continue; + + if (notif->external_id == 0) + return; + + char **argv = NULL; + size_t argc = 0; + + char external_id[16]; + xsnprintf(external_id, sizeof(external_id), "%u", notif->external_id); + + if (!spawn_expand_template( + &term->conf->desktop_notifications.close, 1, + (const char *[]){"id"}, + (const char *[]){external_id}, + &argc, &argv)) + { + return; + } + + int devnull = open("/dev/null", O_RDONLY); + spawn( + term->reaper, NULL, argv, devnull, -1, -1, + NULL, (void *)term, NULL); + + if (devnull >= 0) + close(devnull); + + for (size_t i = 0; i < argc; i++) + free(argv[i]); + free(argv); + } +} + static void add_icon(struct notification_icon *icon, const char *id, const char *symbolic_name, const uint8_t *data, size_t data_sz) diff --git a/notify.h b/notify.h index e19c4fcd..a20c2e51 100644 --- a/notify.h +++ b/notify.h @@ -65,6 +65,7 @@ struct notification_icon { }; bool notify_notify(struct terminal *term, struct notification *notif); +void notify_close(struct terminal *term, const char *id); void notify_free(struct terminal *term, struct notification *notif); void notify_icon_add(struct terminal *term, const char *id, diff --git a/osc.c b/osc.c index 1da32447..2eaed869 100644 --- a/osc.c +++ b/osc.c @@ -590,6 +590,7 @@ kitty_notification(struct terminal *term, char *string) enum { PAYLOAD_TITLE, PAYLOAD_BODY, + PAYLOAD_CLOSE, PAYLOAD_ICON, } payload_type = PAYLOAD_TITLE; /* The 'p' parameter */ @@ -672,6 +673,8 @@ kitty_notification(struct terminal *term, char *string) payload_type = PAYLOAD_TITLE; else if (streq(value, "body")) payload_type = PAYLOAD_BODY; + else if (streq(value, "close")) + payload_type = PAYLOAD_CLOSE; else if (streq(value, "icon")) payload_type = PAYLOAD_ICON; else if (streq(value, "?")) { @@ -687,7 +690,7 @@ kitty_notification(struct terminal *term, char *string) char reply[128]; int n = xsnprintf( reply, sizeof(reply), - "\033]99;i=%s:p=?;p=title,body,icon:a=focus,report:o=%s:u=0,1,2:c=1%s", + "\033]99;i=%s:p=?;p=title,body,close,icon:a=focus,report:o=%s:u=0,1,2:c=1%s", id, when_str, terminator); term_to_slave(term, reply, n); @@ -815,6 +818,10 @@ kitty_notification(struct terminal *term, char *string) break; } + case PAYLOAD_CLOSE: + /* Ignore payload */ + break; + case PAYLOAD_ICON: if (notif->icon_data == NULL) { notif->icon_data = (uint8_t *)payload; @@ -847,14 +854,18 @@ kitty_notification(struct terminal *term, char *string) notif->icon_data_sz = 0; } - /* - * Show notification. - * - * The checks for title|body is to handle notifications that - * only load icon data into the icon cache - */ - if (notif->title != NULL || notif->body != NULL) { - notify_notify(term, notif); + if (payload_type == PAYLOAD_CLOSE) { + notify_close(term, notif->id); + } else { + /* + * Show notification. + * + * The checks for title|body is to handle notifications that + * only load icon data into the icon cache + */ + if (notif->title != NULL || notif->body != NULL) { + notify_notify(term, notif); + } } tll_foreach(term->kitty_notifications, it) { From 8f16fe54d3cbe2417a0edf9bde585d2844ea4bda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 25 Jul 2024 19:31:27 +0200 Subject: [PATCH 0826/1323] pgo: add notify_close() stub --- pgo/pgo.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pgo/pgo.c b/pgo/pgo.c index 6dc0dd10..f87863c0 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -157,6 +157,11 @@ notify_notify(struct terminal *term, struct notification *notif) return true; } +void +notify_close(struct terminal *term, const char *id) +{ +} + void notify_free(struct terminal *term, struct notification *notif) { From 9cf99ea4bffd667ae59e4328a28b86dcefacb3f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 26 Jul 2024 16:23:17 +0200 Subject: [PATCH 0827/1323] notify: close notification by sending SIGINT to helper If the user hasn't configured a custom 'desktop-notifications.close' command, try to close the notification by sending SIGINT to the notification helper. This is best-effort: * If there's no helper running, we do nothing (except warn) * We don't verify, in any way, the notification is actually closed * We don't send any other signals, under any circumstances. That is, no SIGTERM, no SIGKILL. Ever. --- doc/foot.ini.5.scd | 5 +++ notify.c | 89 ++++++++++++++++++++++++++++++---------------- notify.h | 1 + osc.c | 1 + 4 files changed, 66 insertions(+), 30 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 195baafe..df219e7b 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -524,6 +524,11 @@ xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 Closing a notification is only supported by the Kitty Desktop Notification protocol, OSC-99. + If set to the empty string (the default), foot will instead try to + close the notification by sending SIGINT to the notification + helper process. For example, *notify-send --wait* (libnotify >= + 0.8.0) responds to SIGINT by closing the notification. + Default: _not set_ *inhibit-when-focused* diff --git a/notify.c b/notify.c index fa547126..39e8db4e 100644 --- a/notify.c +++ b/notify.c @@ -170,12 +170,16 @@ notif_done(struct reaper *reaper, pid_t pid, int status, void *data) if (notif->pid != pid) continue; - LOG_DBG("notification %s dismissed", notif->id); + LOG_DBG("notification %s closed", notif->id); if (notif->activated && notif->focus) { LOG_DBG("focus window on notification activation: \"%s\"", notif->xdg_token); - wayl_activate(term->wl, term->window, notif->xdg_token); + + if (notif->xdg_token == NULL) + LOG_WARN("cannot focus window: no activation token available"); + else + wayl_activate(term->wl, term->window, notif->xdg_token); } if (notif->activated && notif->report_activated) { @@ -240,7 +244,9 @@ notify_notify(struct terminal *term, struct notification *notif) icon_name_or_path = notif->icon_symbolic_name; } - bool track_notification = notif->focus || notif->report_activated; + bool track_notification = notif->focus || + notif->report_activated || + notif->may_be_programatically_closed; LOG_DBG("notify: title=\"%s\", body=\"%s\", icon=\"%s\" (tracking: %s)", title, body, icon_name_or_path, track_notification ? "yes" : "no"); @@ -344,44 +350,67 @@ notify_close(struct terminal *term, const char *id) { LOG_DBG("close notification %s", id); - if (term->conf->desktop_notifications.close.argv.args == NULL) - return; - tll_foreach(term->active_notifications, it) { const struct notification *notif = &it->item; if (notif->id == 0 || !streq(notif->id, id)) continue; - if (notif->external_id == 0) - return; + if (term->conf->desktop_notifications.close.argv.args == NULL) { + LOG_DBG( + "trying to close notification \"%s\" by sending SIGINT to %u", + id, notif->pid); - char **argv = NULL; - size_t argc = 0; + if (notif->pid == 0) { + LOG_WARN( + "cannot close notification \"%s\": no helper process running", + id); + } else { + /* Best-effort... */ + kill(notif->pid, SIGINT); + } + } else { + LOG_DBG( + "trying to close notification \"%s\" " + "by running user defined command", id); - char external_id[16]; - xsnprintf(external_id, sizeof(external_id), "%u", notif->external_id); + if (notif->external_id == 0) { + LOG_WARN("cannot close notification \"%s\": " + "no daemon assigned notification ID available", id); + return; + } - if (!spawn_expand_template( - &term->conf->desktop_notifications.close, 1, - (const char *[]){"id"}, - (const char *[]){external_id}, - &argc, &argv)) - { - return; + char **argv = NULL; + size_t argc = 0; + + char external_id[16]; + xsnprintf(external_id, sizeof(external_id), "%u", notif->external_id); + + if (!spawn_expand_template( + &term->conf->desktop_notifications.close, 1, + (const char *[]){"id"}, + (const char *[]){external_id}, + &argc, &argv)) + { + return; + } + + int devnull = open("/dev/null", O_RDONLY); + spawn( + term->reaper, NULL, argv, devnull, -1, -1, + NULL, (void *)term, NULL); + + if (devnull >= 0) + close(devnull); + + for (size_t i = 0; i < argc; i++) + free(argv[i]); + free(argv); } - int devnull = open("/dev/null", O_RDONLY); - spawn( - term->reaper, NULL, argv, devnull, -1, -1, - NULL, (void *)term, NULL); - - if (devnull >= 0) - close(devnull); - - for (size_t i = 0; i < argc; i++) - free(argv[i]); - free(argv); + return; } + + LOG_WARN("cannot close notification \"%s\": no such notification", id); } static void diff --git a/notify.h b/notify.h index a20c2e51..bf19d37c 100644 --- a/notify.h +++ b/notify.h @@ -39,6 +39,7 @@ struct notification { enum notify_when when; enum notify_urgency urgency; bool focus; + bool may_be_programatically_closed; bool report_activated; bool report_closed; diff --git a/osc.c b/osc.c index 2eaed869..34080467 100644 --- a/osc.c +++ b/osc.c @@ -759,6 +759,7 @@ kitty_notification(struct terminal *term, char *string) .when = when, .urgency = urgency, .focus = focus, + .may_be_programatically_closed = true, .report_activated = report_activated, .report_closed = report_closed, .stdout_fd = -1, From 00ec2a8f09f39612cb91a34712ae9a1d2c23181e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 27 Jul 2024 08:37:41 +0200 Subject: [PATCH 0828/1323] readme: update mastodon link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3cb5834b..a6de7e45 100644 --- a/README.md +++ b/README.md @@ -681,7 +681,7 @@ at https://libera.irclog.whitequark.org/foot. ## Mastodon Every now and then I post foot related updates on -[@dnkl@linuxrocks.online](https://linuxrocks.online/@dnkl) +[@dnkl@emacs.ch](https://emacs.ch/@dnkl) # Sponsoring/donations From a3ad74025163dc21939dae8dce0aba530a54521f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 27 Jul 2024 08:48:32 +0200 Subject: [PATCH 0829/1323] readme: IRC: update link to point to the web interface --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a6de7e45..8ebc14b1 100644 --- a/README.md +++ b/README.md @@ -674,8 +674,9 @@ The report should contain the following: ## IRC Ask questions, hang out, sing praise or just say hi in the `#foot` -channel on [irc.libera.chat](https://libera.chat). Logs are available -at https://libera.irclog.whitequark.org/foot. +channel on +[irc.libera.chat](https://web.libera.chat/?channels=#foot). Logs are +available at https://libera.irclog.whitequark.org/foot. ## Mastodon From 0a5ba708e4a8448a0d29d4e263f939c9d1d2b953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 30 Jul 2024 16:33:19 +0200 Subject: [PATCH 0830/1323] notify: don't close FD 0 This fixes a regression where closing a terminal instance, or hard- or soft-resetting a terminal caused FD 0 to be closed. This meant it became re-usable. Usually, by memfd_create() when allocating a new surface buffer. So far nothing _really_ bad has happened. But what if FD 0 is now used by a memfd, and we close _another_ terminal instance? This causes our memfd to be closed. And then, when e.g. trying to scroll the terminal content: fallocate() fails with bad FD. --- notify.c | 9 ++++++--- shm.c | 2 +- terminal.c | 4 ++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/notify.c b/notify.c index 39e8db4e..7c5f0731 100644 --- a/notify.c +++ b/notify.c @@ -505,10 +505,13 @@ notify_icon_del(struct terminal *term, const char *id) void notify_icon_free(struct notification_icon *icon) { - if (icon->tmp_file_fd >= 0) - close(icon->tmp_file_fd); - if (icon->tmp_file_name != NULL) + if (icon->tmp_file_name != NULL) { unlink(icon->tmp_file_name); + if (icon->tmp_file_fd >= 0) { + xassert(icon->tmp_file_fd > 0); // DEBUG + close(icon->tmp_file_fd); + } + } free(icon->id); free(icon->symbolic_name); diff --git a/shm.c b/shm.c index 879745d4..04cec211 100644 --- a/shm.c +++ b/shm.c @@ -255,7 +255,7 @@ instantiate_offset(struct buffer_private *buf, off_t new_offset) void *mmapped = MAP_FAILED; struct wl_buffer *wl_buf = NULL; - pixman_image_t **pix = xcalloc(buf->public.pix_instances, sizeof(*pix)); + pixman_image_t **pix = xcalloc(buf->public.pix_instances, sizeof(pix[0])); mmapped = (uint8_t *)pool->real_mmapped + new_offset; diff --git a/terminal.c b/terminal.c index dc4f37b6..e95a3615 100644 --- a/terminal.c +++ b/terminal.c @@ -1330,6 +1330,10 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, } } + for (size_t i = 0; i < ALEN(term->notification_icons); i++) { + term->notification_icons[i].tmp_file_fd = -1; + } + add_utmp_record(conf, reaper, ptmx); if (!pty_path) { From ba79bf1602c93ebf357f1b9c95f42b2fc0372fc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 30 Jul 2024 17:16:02 +0200 Subject: [PATCH 0831/1323] notify: ${icon}: don't fallback to app-id * Revert --icon to ${app-id} * Use --hint STRING:image-path:${icon} instead * ${icon} no longer expands to ${app-id} if notification has no icon --- config.c | 2 +- doc/foot.ini.5.scd | 2 +- foot.ini | 2 +- notify.c | 7 ++----- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/config.c b/config.c index 409469ef..7b403e3c 100644 --- a/config.c +++ b/config.c @@ -3231,7 +3231,7 @@ config_load(struct config *conf, const char *conf_path, parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers); tokenize_cmdline( - "notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action-name}=${action-label} --print-id -- ${title} ${body}", + "notify-send --wait --app-name ${app-id} --icon ${app-id} --urgency ${urgency} --hint STRING:image-path:${icon} --action ${action-name}=${action-label} --print-id -- ${title} ${body}", &conf->desktop_notifications.command.argv.args); tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args); diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index df219e7b..85063088 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -511,7 +511,7 @@ Note: do not set *TERM* here; use the *term* option in the main action=default++ xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 - Default: _notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action-name}=${action-label} --print-id -- ${title} ${body}_. + Default: _notify-send --wait --app-name ${app-id} --icon ${app-id} --urgency ${urgency} --hint STRING:image-path:${icon} --action ${action-name}=${action-label} --print-id -- ${title} ${body}_. *close* Command to execute to close an existing notification. diff --git a/foot.ini b/foot.ini index cead651a..aeda18df 100644 --- a/foot.ini +++ b/foot.ini @@ -47,7 +47,7 @@ # command-focused=no [desktop-notifications] -# command=notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action-name}=${action-label} --print-id -- ${title} ${body} +# command=notify-send --wait --app-name ${app-id} --icon ${app-id} --urgency ${urgency} --hint STRING:image-path:${icon} --action ${action-name}=${action-label} --print-id -- ${title} ${body} # close="" # inhibit-when-focused=yes diff --git a/notify.c b/notify.c index 7c5f0731..c82c86f0 100644 --- a/notify.c +++ b/notify.c @@ -223,11 +223,8 @@ notify_notify(struct terminal *term, struct notification *notif) const char *title = notif->title != NULL ? notif->title : notif->body; const char *body = notif->title != NULL && notif->body != NULL ? notif->body : ""; - /* Icon: use symbolic name from notification, if present, - otherwise fallback to the application ID */ - const char *icon_name_or_path = term->app_id != NULL - ? term->app_id - : term->conf->app_id; + /* Icon: symbolic name if present, otherwise a filename */ + const char *icon_name_or_path = ""; if (notif->icon_id != NULL) { for (size_t i = 0; i < ALEN(term->notification_icons); i++) { From 259a75e957a8eb0f9d541594e8edf45e77274874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 30 Jul 2024 17:24:48 +0200 Subject: [PATCH 0832/1323] notify: remove debug assertion (FD 0 _is_ valid, after all) --- notify.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/notify.c b/notify.c index c82c86f0..c53a3110 100644 --- a/notify.c +++ b/notify.c @@ -504,10 +504,8 @@ notify_icon_free(struct notification_icon *icon) { if (icon->tmp_file_name != NULL) { unlink(icon->tmp_file_name); - if (icon->tmp_file_fd >= 0) { - xassert(icon->tmp_file_fd > 0); // DEBUG + if (icon->tmp_file_fd >= 0) close(icon->tmp_file_fd); - } } free(icon->id); From d87b81dd523a6235638685873434b5490effcae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 30 Jul 2024 17:31:15 +0200 Subject: [PATCH 0833/1323] notify: disable debug logging --- notify.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notify.c b/notify.c index c53a3110..15cbf531 100644 --- a/notify.c +++ b/notify.c @@ -10,7 +10,7 @@ #include <fcntl.h> #define LOG_MODULE "notify" -#define LOG_ENABLE_DBG 1 +#define LOG_ENABLE_DBG 0 #include "log.h" #include "config.h" #include "spawn.h" From 76ac910b118147beba276748d2e2aaca0ee066de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 31 Jul 2024 16:22:17 +0200 Subject: [PATCH 0834/1323] osc: kitty notifications: buttons, icons, app-name, categories etc First, icons have been finalized in the specification. There were only three things we needed to adjust: * symbolic names are base64 encoded * there are a couple of OSC-99 defined symbolic names that need to be translated to the corresponding XDG icon name. * allow in-band icons without a cache ID (that is, allow applications to use p=icon without having to cache the icon first). Second, add support for the following new additions to the protocol: * 'f': custom app-name, overrides the terminal's app-id * 't': categories * 'p=alive': lets applications poll for currently active notifications * 'id' is now 'unset' by default, rather than "0" * 'w': expire time (i.e. notification timeout) * "buttons": aka actions. This lets applications add additional (to the terminal defined "default" action) actions. The 'activated' event has been updated to report which button/action was used to activate the notification. To support button/actions, desktop-notifications.command had to be reworked a bit. There's now a new config option: desktop-notifications.command-action-arg. It has two template arguments ${action-name} and ${action-label}. command-action-arg gets expanded for *each* action. ${action-name} and ${action-label} has been replaced by ${action-arg} in command. This is a somewhat special template, in that it gets replaced by *all* instances of the expanded actions. --- config.c | 17 ++- config.h | 1 + doc/foot.ini.5.scd | 120 ++++++++++++++--- foot.ini | 3 +- notify.c | 322 +++++++++++++++++++++++++++++++++++++------- notify.h | 13 ++ osc.c | 176 +++++++++++++++++++++++- terminal.c | 4 +- tests/test-config.c | 2 + 9 files changed, 580 insertions(+), 78 deletions(-) diff --git a/config.c b/config.c index 7b403e3c..e45a02a7 100644 --- a/config.c +++ b/config.c @@ -806,6 +806,11 @@ value_to_spawn_template(struct context *ctx, char **argv = NULL; + if (ctx->value[0] == '"' && ctx->value[1] == '"' && ctx->value[2] == '\0') { + template->argv.args = NULL; + return true; + } + if (!tokenize_cmdline(ctx->value, &argv)) { LOG_CONTEXTUAL_ERR("syntax error in command line"); return false; @@ -1110,6 +1115,9 @@ parse_section_desktop_notifications(struct context *ctx) if (streq(key, "command")) return value_to_spawn_template( ctx, &conf->desktop_notifications.command); + else if (streq(key, "command-action-arg")) + return value_to_spawn_template( + ctx, &conf->desktop_notifications.command_action_arg); else if (streq(key, "close")) return value_to_spawn_template( ctx, &conf->desktop_notifications.close); @@ -3189,6 +3197,9 @@ config_load(struct config *conf, const char *conf_path, .command = { .argv = {.args = NULL}, }, + .command_action_arg = { + .argv = {.args = NULL}, + }, .close = { .argv = {.args = NULL}, }, @@ -3231,8 +3242,9 @@ config_load(struct config *conf, const char *conf_path, parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers); tokenize_cmdline( - "notify-send --wait --app-name ${app-id} --icon ${app-id} --urgency ${urgency} --hint STRING:image-path:${icon} --action ${action-name}=${action-label} --print-id -- ${title} ${body}", + "notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --replace-id ${replace-id} ${action-arg} --print-id -- ${title} ${body}", &conf->desktop_notifications.command.argv.args); + tokenize_cmdline("--action ${action-name}=${action-label}", &conf->desktop_notifications.command_action_arg.argv.args); tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args); static const char32_t *url_protocols[] = { @@ -3487,6 +3499,8 @@ config_clone(const struct config *old) spawn_template_clone(&conf->bell.command, &old->bell.command); spawn_template_clone(&conf->desktop_notifications.command, &old->desktop_notifications.command); + spawn_template_clone(&conf->desktop_notifications.command_action_arg, + &old->desktop_notifications.command_action_arg); spawn_template_clone(&conf->desktop_notifications.close, &old->desktop_notifications.close); @@ -3571,6 +3585,7 @@ config_free(struct config *conf) spawn_template_free(&conf->bell.command); free(conf->scrollback.indicator.text); spawn_template_free(&conf->desktop_notifications.command); + spawn_template_free(&conf->desktop_notifications.command_action_arg); spawn_template_free(&conf->desktop_notifications.close); for (size_t i = 0; i < ALEN(conf->fonts); i++) config_font_list_destroy(&conf->fonts[i]); diff --git a/config.h b/config.h index 20fea3fc..246b479f 100644 --- a/config.h +++ b/config.h @@ -340,6 +340,7 @@ struct config { struct { struct config_spawn_template command; + struct config_spawn_template command_action_arg; struct config_spawn_template close; bool inhibit_when_focused; } desktop_notifications; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 85063088..f310e228 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -436,12 +436,37 @@ Note: do not set *TERM* here; use the *term* option in the main _${window-title}_ is replaced with the current window title. _${icon}_ is replaced by the icon specified in the - notification request, or _${app_id}_ if the notification did - not set an icon. Note that only symbolic icon names are - supported, not filenames. + notification request, or the empty string if no icon was + specified. Can be used with e.g. notify-send's *--icon* + option, or preferably, by setting the *image-path* hint (with + e.g. notify-send's *--hint* option). + + _${category}_ is replaced by the notification's catogory. Can + be used together with e.g. notify-send's *--category* option. _${urgency}_ is replaced with the notifications urgency; - *low*, *normal* or *critical*. + *low*, *normal* or *critical*. Can be used together with + e.g. notify-send's *--urgency* option. + + _${expire-time}_ is replaced with the notification specified + notification timeout. Can be used together with + e.g. notify-send's *--expire-time* option. + + _${replace-id}_ is replaced by the notification daemon + assigned ID that the notification replaces/updates. For this + to work, foot needs to know the externally assigned IDs of + previously emitted notifications, see the 'stdout' section + below. Can be used together with e.g. notify-send's + *--replace-id* option. + + _${action-arg}_ will be expanded to the *command-action-arg* + option, for each notification action. There will always be at + least one action, the "default" action. Foot uses this to + enable window focusing, and reporting notification activation + to applications that requested such events. + + Applications can also define their own custom notification + actions. See the *command-action-arg* option for details. Ways to trigger notifications Applications can trigger notifications in the following ways: @@ -469,12 +494,11 @@ Note: do not set *TERM* here; use the *term* option in the main activation token. There are two parts to handle this. First, the notification - must define an action. For this purpose, foot definse the - template parameters *${action-name}* and - *${action-label}*. They are intended to be used with - e.g. notify-send's *-A,--action* option. + must define an action. For this purpose, foot will add a + "default" action to the notification (see the + *command-action-arg* option). - Second, foot needs to know when the notification activated, + Second, foot needs to know when the notification is activated, and it needs to get hold of the XDG activation token. Both are expected to be printed on stdout. @@ -486,8 +510,8 @@ Note: do not set *TERM* here; use the *term* option in the main line, prefixed with *xdgtoken=*. Example: - default - xdgtoken=18179adf579a7a904ce73754964b1ec3 + default++ +xdgtoken=18179adf579a7a904ce73754964b1ec3 The expected format of stdout may change at any time. Please read the changelog when upgrading foot. @@ -501,17 +525,81 @@ Note: do not set *TERM* here; use the *term* option in the main helper's stdout: - _nnn_: integer in base 10, daemon assigned notification ID - - _id=nnn_: same as plain _nnn_. - - _default_: the 'default' action was triggered - - _action=default_: same as _default_ - - _xdgtoken=xyz_: XDG activation token. + - *id=*_nnn_: same as plain _nnn_. + - *default*: the 'default' action was triggered + - *action=*_default_: same as _default_ + - *action=*_n_: application custom action _n_ triggered + - *xdgtoken=*_xyz_: XDG activation token. Example: 17++ action=default++ xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 - Default: _notify-send --wait --app-name ${app-id} --icon ${app-id} --urgency ${urgency} --hint STRING:image-path:${icon} --action ${action-name}=${action-label} --print-id -- ${title} ${body}_. + Default: _notify-send++ + --wait++ + --app-name ${app-id}++ + --icon ${app-id}++ + --category ${category}++ + --urgency ${urgency}++ + --expire-time ${expire-time}++ + --hint STRING:image-path:${icon}++ + --replace-id ${replace-id}++ + ${action-arg}++ + --print-id++ + -- ${title} ${body}_. + +*command-action-arg* + String to use with *command* to enable passing action/button names + to the notification helper. + + Foot will always configure a "default" action that can be used to + "activate" the notification, which in turn can cause the foot + window to be focused, or an escape to be sent to the terminal + application (depending on how the application generated the + notification). + + Furhermore, the OSC-99 notifications protocol allows applications + to define their own actions. Foot uses a combination of the + *command* option, and the *command-action-arg* option to pass the + names of the actions to the notification helper. + + This option has the following template arguments: + + - _${action-name}_: the name of the action; *default* for the + default action configured by foot, and _n_, where _n_ is an + integer >= 1, for application defined actions. + - _${action-label}_: *Click to activate* for the default action, + and a free-form string for application defined actions. + + For each notification action (remember, there will always be at + least one), *command-action-arg* will be expanded with the + action's name and label. + + Then, _${action-arg}_ is expanded *command* to the full list of + actions. + + If *command-action-arg* is set to the empty string, no actions + will be passed to *command*. That is, _${action-arg}_ will be + replaced with the empty string. + + Example: + + *command-action-arg=--action ${action-name}=${action-label}* + *command=notify-send ${action-arg} ...* + + Assume the application defined two custom actions: *OK* and + *Cancel*. + + Given the above, foot will execute: + + notify-send++ + --action default='Click to activate'++ + --action 1=OK++ + --action 2=Cancel++ + ... + + Default: _--action ${action-name}=${action-label}_ *close* Command to execute to close an existing notification. diff --git a/foot.ini b/foot.ini index aeda18df..0ae0b52f 100644 --- a/foot.ini +++ b/foot.ini @@ -47,7 +47,8 @@ # command-focused=no [desktop-notifications] -# command=notify-send --wait --app-name ${app-id} --icon ${app-id} --urgency ${urgency} --hint STRING:image-path:${icon} --action ${action-name}=${action-label} --print-id -- ${title} ${body} +# command=notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --replace-id ${replace-id} ${action-arg} --print-id -- ${title} ${body} +# command-action-arg=--action ${action-name}=${action-label} # close="" # inhibit-when-focused=yes diff --git a/notify.c b/notify.c index 15cbf531..e1d6b9d7 100644 --- a/notify.c +++ b/notify.c @@ -10,7 +10,7 @@ #include <fcntl.h> #define LOG_MODULE "notify" -#define LOG_ENABLE_DBG 0 +#define LOG_ENABLE_DBG 1 #include "log.h" #include "config.h" #include "spawn.h" @@ -27,11 +27,54 @@ notify_free(struct terminal *term, struct notification *notif) free(notif->id); free(notif->title); free(notif->body); + free(notif->category); + free(notif->app_id); free(notif->icon_id); free(notif->icon_symbolic_name); free(notif->icon_data); free(notif->xdg_token); free(notif->stdout_data); + + tll_free_and_free(notif->actions, free); + + if (notif->icon_path != NULL) { + unlink(notif->icon_path); + free(notif->icon_path); + + if (notif->icon_fd >= 0) + close(notif->icon_fd); + } +} + +static bool +write_icon_file(const void *data, size_t data_sz, int *fd, char **filename, + char **symbolic_name) +{ + xassert(*filename == NULL); + xassert(*symbolic_name == NULL); + + char name[64] = "/tmp/foot-notification-icon-XXXXXX"; + + *filename = NULL; + *symbolic_name = NULL; + *fd = mkostemp(name, O_CLOEXEC); + + if (*fd < 0) { + LOG_ERRNO("failed to create temporary file for icon cache"); + return false; + } + + if (write(*fd, data, data_sz) != (ssize_t)data_sz) { + LOG_ERRNO("failed to write icon data to temporary file"); + close(*fd); + *fd = -1; + return false; + } + + LOG_DBG("wrote icon data to %s", name); + *filename = xstrdup(name); + *symbolic_name = xasprintf("file://%s", *filename); + return true; } static bool @@ -94,6 +137,19 @@ consume_stdout(struct notification *notif, bool eof) LOG_DBG("notification's default action was triggered"); } + else if (len > 7 && memcmp(line, "action=", 7) == 0) { + notif->activated = true; + + uint32_t maybe_button_nr; + if (to_integer(&line[7], len - 7, &maybe_button_nr)) { + notif->activated_button = maybe_button_nr; + LOG_DBG("custom action %u triggered", notif->activated_button); + } else { + LOG_DBG("unrecognized action triggered: %.*s", + (int)(len - 7), &line[7]); + } + } + /* Check for XDG activation token, 'xdgtoken=xyz' */ else if (len > 9 && memcmp(line, "xdgtoken=", 9) == 0) { notif->xdg_token = xstrndup(&line[9], len - 9); @@ -170,7 +226,8 @@ notif_done(struct reaper *reaper, pid_t pid, int status, void *data) if (notif->pid != pid) continue; - LOG_DBG("notification %s closed", notif->id); + LOG_DBG("notification %s closed", + notif->id != NULL ? notif->id : "<unset>"); if (notif->activated && notif->focus) { LOG_DBG("focus window on notification activation: \"%s\"", @@ -183,22 +240,29 @@ notif_done(struct reaper *reaper, pid_t pid, int status, void *data) } if (notif->activated && notif->report_activated) { - xassert(notif->id != NULL); - LOG_DBG("sending notification activation event to client"); - char reply[7 + strlen(notif->id) + 1 + 2 + 1]; + const char *id = notif->id != NULL ? notif->id : "0"; + + char button_nr[16] = {0}; + if (notif->activated_button > 0) { + xsnprintf( + button_nr, sizeof(button_nr), "%u", notif->activated_button); + } + + char reply[7 + strlen(id) + 1 + strlen(button_nr) + 2 + 1]; int n = xsnprintf( - reply, sizeof(reply), "\033]99;i=%s;\033\\", notif->id); + reply, sizeof(reply), "\033]99;i=%s;%s\033\\", id, button_nr); term_to_slave(term, reply, n); } if (notif->report_closed) { LOG_DBG("sending notification close event to client"); - char reply[7 + strlen(notif->id) + 1 + 7 + 1 + 2 + 1]; + const char *id = notif->id != NULL ? notif->id : "0"; + char reply[7 + strlen(id) + 1 + 7 + 1 + 2 + 1]; int n = xsnprintf( - reply, sizeof(reply), "\033]99;i=%s:p=close;\033\\", notif->id); + reply, sizeof(reply), "\033]99;i=%s:p=close;\033\\", id); term_to_slave(term, reply, n); } @@ -212,14 +276,33 @@ bool notify_notify(struct terminal *term, struct notification *notif) { xassert(notif->xdg_token == NULL); + xassert(notif->external_id == 0); xassert(notif->pid == 0); xassert(notif->stdout_fd <= 0); xassert(notif->stdout_data == NULL); + xassert(notif->icon_path == NULL); + xassert(notif->icon_fd <= 0); notif->pid = -1; notif->stdout_fd = -1; + notif->icon_fd = -1; - /* Use body as title, if title is unset */ + if (term->conf->desktop_notifications.command.argv.args == NULL) + return false; + + if ((term->conf->desktop_notifications.inhibit_when_focused || + notif->when != NOTIFY_ALWAYS) + && term->kbd_focus) + { + /* No notifications while we're focused */ + return false; + } + + const char *app_id = notif->app_id != NULL + ? notif->app_id + : term->app_id != NULL + ? term->app_id + : term->conf->app_id; const char *title = notif->title != NULL ? notif->title : notif->body; const char *body = notif->title != NULL && notif->body != NULL ? notif->body : ""; @@ -231,40 +314,64 @@ notify_notify(struct terminal *term, struct notification *notif) const struct notification_icon *icon = &term->notification_icons[i]; if (icon->id != NULL && streq(icon->id, notif->icon_id)) { - icon_name_or_path = icon->symbolic_name != NULL - ? icon->symbolic_name - : icon->tmp_file_name; + /* For now, we set the symbolic name to 'file:///path' + * when using a file based icon. */ + xassert(icon->symbolic_name != NULL); + icon_name_or_path = icon->symbolic_name; + + LOG_DBG("using icon from cache (cache ID: %s): %s", + icon->id, icon_name_or_path); break; } } } else if (notif->icon_symbolic_name != NULL) { icon_name_or_path = notif->icon_symbolic_name; + LOG_DBG("using symbolic icon from notification: %s", icon_name_or_path); + } else if (notif->icon_data_sz > 0) { + xassert(notif->icon_data != NULL); + + if (write_icon_file( + notif->icon_data, notif->icon_data_sz, + ¬if->icon_fd, + ¬if->icon_path, + ¬if->icon_symbolic_name)) + icon_name_or_path = notif->icon_symbolic_name; + + LOG_DBG("using icon data from notification: %s", icon_name_or_path); } bool track_notification = notif->focus || notif->report_activated || notif->may_be_programatically_closed; - LOG_DBG("notify: title=\"%s\", body=\"%s\", icon=\"%s\" (tracking: %s)", - title, body, icon_name_or_path, track_notification ? "yes" : "no"); + uint32_t replaces_id = 0; + if (notif->id != NULL) { + tll_foreach(term->active_notifications, it) { + struct notification *existing = &it->item; - xassert(title != NULL); - if (title == NULL) - return false; + if (existing->id == NULL) + continue; - if ((term->conf->desktop_notifications.inhibit_when_focused || - notif->when != NOTIFY_ALWAYS) - && term->kbd_focus) - { - /* No notifications while we're focused */ - return false; + /* + * When replacing/updating a notificaton, we may have + * *multiple* notification helpers running for the "same" + * notification. Make sure only the *last* notification's + * report closed/activated are honored, to avoid sending + * multiple reports. + * + * This also means we cannot 'break' out of the loop - we + * must check *all* notifications. + */ + if (existing->external_id != 0 && streq(existing->id, notif->id)) { + replaces_id = existing->external_id; + existing->report_activated = false; + existing->report_closed = false; + } + } } - if (term->conf->desktop_notifications.command.argv.args == NULL) - return false; - - char **argv = NULL; - size_t argc = 0; + char replaces_id_str[16]; + xsnprintf(replaces_id_str, sizeof(replaces_id_str), "%u", replaces_id); const char *urgency_str = notif->urgency == NOTIFY_URGENCY_LOW @@ -272,19 +379,129 @@ notify_notify(struct terminal *term, struct notification *notif) : notif->urgency == NOTIFY_URGENCY_NORMAL ? "normal" : "critical"; + LOG_DBG("notify: title=\"%s\", body=\"%s\", app-id=\"%s\", category=\"%s\", " + "urgency=\"%s\", icon=\"%s\", expires=%d, replaces=%u (tracking: %s)", + title, body, app_id, notif->category, urgency_str, icon_name_or_path, + notif->expire_time, replaces_id, track_notification ? "yes" : "no"); + + xassert(title != NULL); + if (title == NULL) + return false; + + char **argv = NULL; + size_t argc = 0; + char **action_argv = NULL; + size_t action_argc = 0; + + char expire_time[16]; + xsnprintf(expire_time, sizeof(expire_time), "%d", notif->expire_time); + + if (term->conf->desktop_notifications.command_action_arg.argv.args) { + struct action { + const char *name; + const char *label; + }; + + tll(struct action) actions = tll_init(); + tll_push_back(actions, ((struct action){"default", "Click to activate"})); + + tll_foreach(notif->actions, it) { + tll_push_back(actions, ((struct action){NULL, it->item})); + } + + size_t action_idx = 0; + tll_foreach(actions, it) { + const char *name = it->item.name; + const char *label = it->item.label; + + /* + * Custom actions (buttons) start at 1. + * + * We always insert our own default action first, causing + * all custom actions to start at index 1 in our list. + */ + char numerical_name[16]; + xsnprintf(numerical_name, sizeof(numerical_name), "%zu", action_idx); + + if (name == NULL) + name = numerical_name; + + char **expanded = NULL; + size_t count = 0; + + if (!spawn_expand_template( + &term->conf->desktop_notifications.command_action_arg, 2, + (const char *[]){"action-name", "action-label"}, + (const char *[]){name, label}, + &count, &expanded)) + { + return false; + } + + /* Append to the "global" actions argv */ + action_argv = xrealloc( + action_argv, (action_argc + count) * sizeof(action_argv[0])); + + for (size_t i = 0; i < count; i++) + action_argv[action_argc + i] = expanded[i]; + + action_argc += count; + + free(expanded); + action_idx++; + tll_remove(actions, it); + } + } + if (!spawn_expand_template( - &term->conf->desktop_notifications.command, 8, + &term->conf->desktop_notifications.command, 10, (const char *[]){ - "app-id", "window-title", "icon", "title", "body", "urgency", "action-name", "action-label"}, + "app-id", "window-title", "icon", "title", "body", "category", + "urgency", "expire-time", "replace-id", "action-arg"}, (const char *[]){ - term->app_id ? term->app_id : term->conf->app_id, - term->window_title, icon_name_or_path, title, body, urgency_str, - "default", "Click to activate"}, + app_id, term->window_title, icon_name_or_path, title, body, + notif->category != NULL ? notif->category : "", urgency_str, + expire_time, replaces_id_str, + + /* Custom expansion below, since we need to expand to multiple arguments */ + "${action-arg}"}, &argc, &argv)) { return false; } + for (size_t i = 0; i < argc; i++) { + if (!streq(argv[i], "${action-arg}")) + continue; + + if (action_argc == 0) { + free(argv[i]); + memmove(&argv[i], &argv[i + 1], (argc - i - 1) * sizeof(argv[0])); + argv[argc--] = NULL; + break; + } + + /* Remove the "${action-arg}" entry, add all actions argument from earlier */ + argv = xrealloc(argv, (argc + action_argc - 1) * sizeof(argv[0])); + + /* Move remaining arguments to after the action arguments */ + memmove(&argv[i + action_argc], + &argv[i + 1], + (argc - (i + 1)) * sizeof(argv[0])); + + free(argv[i]); /* Free xstrdup("${action-arg}"); */ + + /* Insert the action arguments */ + for (size_t j = 0; j < action_argc; j++) { + argv[i + j] = action_argv[j]; + action_argv[j] = NULL; + } + + argc += action_argc; + argc--; /* The ${action-arg} option has been removed */ + break; + } + LOG_DBG("notify command:"); for (size_t i = 0; i < argc; i++) LOG_DBG(" argv[%zu] = \"%s\"", i, argv[i]); @@ -300,15 +517,23 @@ notify_notify(struct terminal *term, struct notification *notif) notif->id = NULL; notif->title = NULL; notif->body = NULL; + notif->category = NULL; + notif->app_id = NULL; notif->icon_id = NULL; notif->icon_symbolic_name = NULL; notif->icon_data = NULL; notif->icon_data_sz = 0; - notif = &tll_back(term->active_notifications); + notif->icon_path = NULL; + notif->icon_fd = -1; + notif->stdout_fd = -1; + struct notification *new_notif = &tll_back(term->active_notifications); + + /* We don't need these anymore. They'll be free:d by the caller */ + memset(&new_notif->actions, 0, sizeof(new_notif->actions)); + notif = new_notif; } } - if (stdout_fds[0] >= 0) { fdm_add(term->fdm, stdout_fds[0], EPOLLIN, &fdm_notify_stdout, (void *)term); @@ -336,6 +561,9 @@ notify_notify(struct terminal *term, struct notification *notif) for (size_t i = 0; i < argc; i++) free(argv[i]); free(argv); + for (size_t i = 0; i < action_argc; i++) + free(action_argv[i]); + free(action_argv); notif->pid = pid; notif->stdout_fd = stdout_fds[0]; @@ -345,11 +573,12 @@ notify_notify(struct terminal *term, struct notification *notif) void notify_close(struct terminal *term, const char *id) { + xassert(id != NULL); LOG_DBG("close notification %s", id); tll_foreach(term->active_notifications, it) { const struct notification *notif = &it->item; - if (notif->id == 0 || !streq(notif->id, id)) + if (notif->id == NULL || !streq(notif->id, id)) continue; if (term->conf->desktop_notifications.close.argv.args == NULL) { @@ -429,22 +658,11 @@ add_icon(struct notification_icon *icon, const char *id, const char *symbolic_na * have a symbolic name. */ if (symbolic_name == NULL && data_sz > 0) { - char name[64] = "/tmp/foot-notification-icon-cache-XXXXXX"; - int fd = mkostemp(name, O_CLOEXEC); - - if (fd < 0) { - LOG_ERRNO("failed to create temporary file for icon cache"); - return; - } - - if (write(fd, data, data_sz) != (ssize_t)data_sz) { - LOG_ERRNO("failed to write icon data to temporary file"); - close(fd); - } else { - LOG_DBG("wrote icon data to %s", name); - icon->tmp_file_name = xstrdup(name); - icon->tmp_file_fd = fd; - } + write_icon_file( + data, data_sz, + &icon->tmp_file_fd, + &icon->tmp_file_name, + &icon->symbolic_name); } LOG_DBG("added icon to cache: ID=%s: sym=%s, file=%s", diff --git a/notify.h b/notify.h index bf19d37c..2a7c5ca1 100644 --- a/notify.h +++ b/notify.h @@ -3,6 +3,8 @@ #include <stdint.h> #include <unistd.h> +#include <tllist.h> + struct terminal; enum notify_when { @@ -30,7 +32,9 @@ struct notification { char *id; char *title; char *body; + char *category; + char *app_id; /* Custm app-id, overrides the terminal's app-id */ char *icon_id; char *icon_symbolic_name; uint8_t *icon_data; @@ -38,6 +42,9 @@ struct notification { enum notify_when when; enum notify_urgency urgency; + int32_t expire_time; + tll(char *) actions; + bool focus; bool may_be_programatically_closed; bool report_activated; @@ -49,6 +56,7 @@ struct notification { uint32_t external_id; /* Daemon assigned notification ID */ bool activated; /* User 'activated' the notification */ + uint32_t activated_button; /* User activated one of the custom actions */ char *xdg_token; /* XDG activation token, from daemon */ pid_t pid; /* Notifier command PID */ @@ -56,6 +64,11 @@ struct notification { char *stdout_data; /* Data we've reado from command's stdout */ size_t stdout_sz; + + /* Used when notification provides raw icon data, and it's + bypassing the icon cache */ + char *icon_path; + int icon_fd; }; struct notification_icon { diff --git a/osc.c b/osc.c index 34080467..360ea6ee 100644 --- a/osc.c +++ b/osc.c @@ -559,7 +559,9 @@ osc_notify(struct terminal *term, char *string) notify_notify(term, &(struct notification){ .title = (char *)title, - .body = (char *)msg}); + .body = (char *)msg, + .expire_time = -1, + }); } static void @@ -575,9 +577,11 @@ kitty_notification(struct terminal *term, char *string) *payload_raw = '\0'; payload_raw++; - char *id = xstrdup("0"); /* The 'i' parameter */ + char *id = NULL; /* The 'i' parameter */ + char *app_id = NULL; /* The 'f' parameter */ char *icon_id = NULL; /* The 'g' parameter */ char *symbolic_icon = NULL; /* The 'n' parameter */ + char *category = NULL; /* The 't' parameter */ char *payload = NULL; bool focus = true; /* The 'a' parameter */ @@ -586,12 +590,16 @@ kitty_notification(struct terminal *term, char *string) bool done = true; /* The 'd' parameter */ bool base64 = false; /* The 'e' parameter */ + int32_t expire_time = -1; /* The 'w' parameter */ + size_t payload_size; enum { PAYLOAD_TITLE, PAYLOAD_BODY, PAYLOAD_CLOSE, + PAYLOAD_ALIVE, PAYLOAD_ICON, + PAYLOAD_BUTTON, } payload_type = PAYLOAD_TITLE; /* The 'p' parameter */ enum notify_when when = NOTIFY_ALWAYS; @@ -601,6 +609,7 @@ kitty_notification(struct terminal *term, char *string) bool have_c = false; bool have_o = false; bool have_u = false; + bool have_w = false; char *ctx = NULL; for (char *param = strtok_r(parameters, ":", &ctx); @@ -675,8 +684,12 @@ kitty_notification(struct terminal *term, char *string) payload_type = PAYLOAD_BODY; else if (streq(value, "close")) payload_type = PAYLOAD_CLOSE; + else if (streq(value, "alive")) + payload_type = PAYLOAD_ALIVE; else if (streq(value, "icon")) payload_type = PAYLOAD_ICON; + else if (streq(value, "buttons")) + payload_type = PAYLOAD_BUTTON; else if (streq(value, "?")) { /* Query capabilities */ @@ -690,9 +703,10 @@ kitty_notification(struct terminal *term, char *string) char reply[128]; int n = xsnprintf( reply, sizeof(reply), - "\033]99;i=%s:p=?;p=title,body,close,icon:a=focus,report:o=%s:u=0,1,2:c=1%s", - id, when_str, terminator); + "\033]99;i=%s:p=?;p=title,body,?,close,alive,icon,buttons:a=focus,report:o=%s:u=0,1,2:c=1:w=1%s", + id != NULL ? id : "0", when_str, terminator); + xassert(n < sizeof(reply)); term_to_slave(term, reply, n); goto out; } @@ -720,6 +734,45 @@ kitty_notification(struct terminal *term, char *string) urgency = NOTIFY_URGENCY_CRITICAL; break; + case 'w': { + /* Notification timeout */ + errno = 0; + char *end = NULL; + long timeout = strtol(value, &end, 10); + + if (errno == 0 && *end == '\0' && timeout <= INT32_MAX) { + expire_time = timeout; + have_w = true; + } + break; + } + + case 'f': + free(app_id); + app_id = base64_decode(value, NULL); + break; + + case 't': { + /* Type (category) */ + char *decoded = base64_decode(value, NULL); + if (decoded != NULL) { + if (category == NULL) + category = decoded; + else { + const size_t old_len = strlen(category); + const size_t new_len = strlen(decoded); + + /* Append, comma separated */ + category = xrealloc(category, old_len + 1 + new_len + 1); + category[old_len] = ','; + memcpy(&category[old_len + 1], decoded, new_len); + category[old_len + 1 + new_len] = '\0'; + free(decoded); + } + } + break; + } + case 'g': /* graphical ID */ free(icon_id); @@ -729,7 +782,35 @@ kitty_notification(struct terminal *term, char *string) case 'n': /* Symbolic icon name, used with 'g' */ free(symbolic_icon); - symbolic_icon = xstrdup(value); + symbolic_icon = base64_decode(value, NULL); + + /* Translate OSC-99 "special" names */ + if (symbolic_icon != NULL) { + const char *translated_name = NULL; + + if (streq(symbolic_icon, "error")) + translated_name = "dialog-error"; + else if (streq(symbolic_icon, "warn") || + streq(symbolic_icon, "warning")) + translated_name = "dialog-warning"; + else if (streq(symbolic_icon, "info")) + translated_name = "dialog-information"; + else if (streq(symbolic_icon, "question")) + translated_name = "dialog-question"; + else if (streq(symbolic_icon, "help")) + translated_name = "system-help"; + else if (streq(symbolic_icon, "file-manager")) + translated_name = "system-file-manager"; + else if (streq(symbolic_icon, "system-monitor")) + translated_name = "utilities-system-monitor"; + else if (streq(symbolic_icon, "text-editor")) + translated_name = "text-editor"; + + if (translated_name != NULL) { + free(symbolic_icon); + symbolic_icon = xstrdup(translated_name); + } + } break; } } @@ -746,7 +827,9 @@ kitty_notification(struct terminal *term, char *string) /* Search for an existing (d=0) notification to update */ struct notification *notif = NULL; tll_foreach(term->kitty_notifications, it) { - if (streq(it->item.id, id)) { + if ((id == NULL && it->item.id == NULL) || + (id != NULL && it->item.id != NULL && streq(it->item.id, id))) + { /* Found existing notification */ notif = &it->item; break; @@ -758,6 +841,8 @@ kitty_notification(struct terminal *term, char *string) .id = id, .when = when, .urgency = urgency, + .expire_time = expire_time, + .actions = tll_init(), .focus = focus, .may_be_programatically_closed = true, .report_activated = report_activated, @@ -787,6 +872,8 @@ kitty_notification(struct terminal *term, char *string) notif->when = when; if (have_u) notif->urgency = urgency; + if (have_w) + notif->expire_time = expire_time; if (icon_id != NULL) { free(notif->icon_id); @@ -800,6 +887,29 @@ kitty_notification(struct terminal *term, char *string) symbolic_icon = NULL; } + if (app_id != NULL) { + free(notif->app_id); + notif->app_id = app_id; + app_id = NULL; /* Prevent double free */ + } + + if (category != NULL) { + if (notif->category == NULL) { + notif->category = category; + category = NULL; /* Prevent double free */ + } else { + const size_t old_len = strlen(notif->category); + const size_t new_len = strlen(category); + + /* Append, comma separated */ + notif->category = + xrealloc(notif->category, old_len + 1 + new_len + 1); + notif->category[old_len] = ','; + memcpy(¬if->category[old_len + 1], category, new_len); + notif->category[old_len + 1 + new_len] = '\0'; + } + } + /* Handled chunked payload - append to existing metadata */ switch (payload_type) { case PAYLOAD_TITLE: @@ -820,6 +930,7 @@ kitty_notification(struct terminal *term, char *string) } case PAYLOAD_CLOSE: + case PAYLOAD_ALIVE: /* Ignore payload */ break; @@ -835,6 +946,20 @@ kitty_notification(struct terminal *term, char *string) notif->icon_data_sz += payload_size; } break; + + case PAYLOAD_BUTTON: { + char *ctx = NULL; + for (const char *button = strtok_r(payload, "\u2028", &ctx); + button != NULL; + button = strtok_r(NULL, "\u2028", &ctx)) + { + if (button[0] != '\0') { + tll_push_back(notif->actions, xstrdup(button)); + } + } + + break; + } } if (done) { @@ -856,7 +981,42 @@ kitty_notification(struct terminal *term, char *string) } if (payload_type == PAYLOAD_CLOSE) { - notify_close(term, notif->id); + if (notif->id != NULL) + notify_close(term, notif->id); + } else if (payload_type == PAYLOAD_ALIVE) { + char *alive_ids = NULL; + size_t alive_ids_len = 0; + + tll_foreach(term->active_notifications, it) { + /* TODO: check with kitty: use "0" for all + notifications with no ID? */ + + const char *item_id = it->item.id != NULL ? it->item.id : "0"; + const size_t id_len = strlen(item_id); + + if (alive_ids == NULL) { + alive_ids = xstrdup(item_id); + alive_ids_len = id_len; + } else { + alive_ids = xrealloc(alive_ids, alive_ids_len + 1 + id_len + 1); + + /* Append ",<id>" */ + alive_ids[alive_ids_len] = ','; + memcpy(&alive_ids[alive_ids_len + 1], item_id, id_len); + + alive_ids_len += 1 + id_len; + alive_ids[alive_ids_len] = '\0'; + } + } + + char *reply = xasprintf( + "\033]99;i=%s:p=alive;%s\033\\", + notif->id != NULL ? notif->id : "0", + alive_ids != NULL ? alive_ids : ""); + + term_to_slave(term, reply, strlen(reply)); + free(reply); + free(alive_ids); } else { /* * Show notification. @@ -880,9 +1040,11 @@ kitty_notification(struct terminal *term, char *string) out: free(id); + free(app_id); free(icon_id); free(symbolic_icon); free(payload); + free(category); } void diff --git a/terminal.c b/terminal.c index e95a3615..bcd2651e 100644 --- a/terminal.c +++ b/terminal.c @@ -3601,7 +3601,9 @@ term_bell(struct terminal *term) if (term->conf->bell.notify) { notify_notify(term, &(struct notification){ .title = (char *)"Bell", - .body = (char *)"Bell in terminal"}); + .body = (char *)"Bell in terminal", + .expire_time = -1, + }); } if (term->conf->bell.flash) diff --git a/tests/test-config.c b/tests/test-config.c index ec718c24..ae111446 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -580,6 +580,8 @@ test_section_desktop_notifications(void) test_boolean(&ctx, &parse_section_desktop_notifications, "inhibit-when-focused", &conf.desktop_notifications.inhibit_when_focused); test_spawn_template(&ctx, &parse_section_desktop_notifications, "command", &conf.desktop_notifications.command); + test_spawn_template(&ctx, &parse_section_desktop_notifications, "command-action-arg", &conf.desktop_notifications.command_action_arg); + test_spawn_template(&ctx, &parse_section_desktop_notifications, "close", &conf.desktop_notifications.close); config_free(&conf); } From 18b87b2e206536d2fdda36e8c42cce270c77a54b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 31 Jul 2024 18:31:18 +0200 Subject: [PATCH 0835/1323] notify: don't forget terminating NULL when patching notify helper's argv --- notify.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/notify.c b/notify.c index e1d6b9d7..e109e6a8 100644 --- a/notify.c +++ b/notify.c @@ -476,18 +476,21 @@ notify_notify(struct terminal *term, struct notification *notif) if (action_argc == 0) { free(argv[i]); - memmove(&argv[i], &argv[i + 1], (argc - i - 1) * sizeof(argv[0])); - argv[argc--] = NULL; + + /* Remove ${command-arg}, but include terminating NULL */ + memmove(&argv[i], &argv[i + 1], (argc - i) * sizeof(argv[0])); + argc--; break; } - /* Remove the "${action-arg}" entry, add all actions argument from earlier */ - argv = xrealloc(argv, (argc + action_argc - 1) * sizeof(argv[0])); + /* Remove the "${action-arg}" entry, add all actions argument + from earlier, but include terminating NULL */ + argv = xrealloc(argv, (argc + action_argc) * sizeof(argv[0])); /* Move remaining arguments to after the action arguments */ memmove(&argv[i + action_argc], &argv[i + 1], - (argc - (i + 1)) * sizeof(argv[0])); + (argc - i) * sizeof(argv[0])); /* Include terminating NULL */ free(argv[i]); /* Free xstrdup("${action-arg}"); */ @@ -501,10 +504,11 @@ notify_notify(struct terminal *term, struct notification *notif) argc--; /* The ${action-arg} option has been removed */ break; } - + LOG_DBG("notify command:"); for (size_t i = 0; i < argc; i++) LOG_DBG(" argv[%zu] = \"%s\"", i, argv[i]); + xassert(argv[argc] == NULL); int stdout_fds[2] = {-1, -1}; if (track_notification) { From ea2f0e7c3f29f2ba83ad71e36c6751bb571f826f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 2 Aug 2024 08:07:13 +0200 Subject: [PATCH 0836/1323] osc: kitty notifications: cleanup and update to latest version of spec * Don't store a list of unfinished notifications. Use a single one. If the notification ID of the 'current' notification doesn't match the previous, unfinished one, the 'current' notification replaces the previous one, instead of updating it. * Update xstrjoin() to take an optional delimiter (for example ','), and use that when joining categories and 'alive IDs'. * Rename ${action-arg} to ${action-argument} * Update handling of the 'n' parameter (symbolic icon name); the spec allows it to be used multiple times, and the terminal is supposed to pick the first one it can resolve. Foot can't resolve icons at all, neither can 'notify-send' or 'fyi' (which is what foot typically executes to display a notification); it's the notification daemon that resolves icons. The spec _could_ be interpreted to mean the terminal should lookup .desktop files, and use the value of the 'Icon' key from the first matching .desktop files. But foot doesn't read .desktop files, and I don't intend to implement XDG directory scanning and parsing of .desktop files just to figure out which icon to use. Instead, use a simple heuristics; use the *shortest* symbolic names. The idea is pretty simple: plain icon names are typically shorter than .desktop file IDs. --- CHANGELOG.md | 5 +- config.c | 12 +-- doc/foot.ini.5.scd | 74 ++++++++++----- foot.ini | 4 +- main.c | 23 ++++- notify.c | 135 +++++++++++++++------------ notify.h | 27 +++--- osc.c | 220 +++++++++++++++++++++----------------------- terminal.c | 21 ++--- terminal.h | 8 +- tests/test-config.c | 2 +- xmalloc.h | 12 ++- 12 files changed, 296 insertions(+), 247 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3394c0ce..82ad7f9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,8 +75,11 @@ * `desktop-notifications.command` option, replaces `notify`. * `desktop-notifications.inhibit-when-focused` option, replaces `notify-focus-inhibit`. -* `${icon}`, `${urgency}`,`${action-name}` and `${action-label}` added +* `${icon}`, `${urgency}` and `${action-argument}` added to the `desktop-notifications.command` template. +* `desktop-notifications.command-action-argument` option, defining how + `${action-argument}` (in `desktop-notifications.command`) should be + expanded. * `desktop-notifications.close` option, defining what to execute when an application wants to close an existing notification (via an OSC-99 escape sequence). diff --git a/config.c b/config.c index e45a02a7..46b3d2d2 100644 --- a/config.c +++ b/config.c @@ -356,9 +356,9 @@ open_config(void) /* First, check XDG_CONFIG_HOME (or .config, if unset) */ if (xdg_config_home != NULL && xdg_config_home[0] != '\0') - path = xstrjoin(xdg_config_home, "/foot/foot.ini"); + path = xstrjoin(xdg_config_home, "/foot/foot.ini", 0); else if (home_dir != NULL) - path = xstrjoin(home_dir, "/.config/foot/foot.ini"); + path = xstrjoin(home_dir, "/.config/foot/foot.ini", 0); if (path != NULL) { LOG_DBG("checking for %s", path); @@ -383,7 +383,7 @@ open_config(void) conf_dir = strtok(NULL, ":")) { free(path); - path = xstrjoin(conf_dir, "/foot/foot.ini"); + path = xstrjoin(conf_dir, "/foot/foot.ini", 0); LOG_DBG("checking for %s", path); int fd = open(path, O_RDONLY | O_CLOEXEC); @@ -1115,7 +1115,7 @@ parse_section_desktop_notifications(struct context *ctx) if (streq(key, "command")) return value_to_spawn_template( ctx, &conf->desktop_notifications.command); - else if (streq(key, "command-action-arg")) + else if (streq(key, "command-action-argument")) return value_to_spawn_template( ctx, &conf->desktop_notifications.command_action_arg); else if (streq(key, "close")) @@ -2931,7 +2931,7 @@ get_server_socket_path(void) const char *wayland_display = getenv("WAYLAND_DISPLAY"); if (wayland_display == NULL) { - return xstrjoin(xdg_runtime, "/foot.sock"); + return xstrjoin(xdg_runtime, "/foot.sock", 0); } return xasprintf("%s/foot-%s.sock", xdg_runtime, wayland_display); @@ -3242,7 +3242,7 @@ config_load(struct config *conf, const char *conf_path, parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers); tokenize_cmdline( - "notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --replace-id ${replace-id} ${action-arg} --print-id -- ${title} ${body}", + "notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --replace-id ${replace-id} ${action-argument} --print-id -- ${title} ${body}", &conf->desktop_notifications.command.argv.args); tokenize_cmdline("--action ${action-name}=${action-label}", &conf->desktop_notifications.command_action_arg.argv.args); tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args); diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index f310e228..11114feb 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -459,14 +459,15 @@ Note: do not set *TERM* here; use the *term* option in the main below. Can be used together with e.g. notify-send's *--replace-id* option. - _${action-arg}_ will be expanded to the *command-action-arg* - option, for each notification action. There will always be at - least one action, the "default" action. Foot uses this to - enable window focusing, and reporting notification activation - to applications that requested such events. + _${action-argument}_ will be expanded to the + *command-action-argument* option, for each notification + action. There will always be at least one action, the + "default" action. Foot uses this to enable window focusing, + and reporting notification activation to applications that + requested such events. Applications can also define their own custom notification - actions. See the *command-action-arg* option for details. + actions. See the *command-action-argument* option for details. Ways to trigger notifications Applications can trigger notifications in the following ways: @@ -496,7 +497,7 @@ Note: do not set *TERM* here; use the *term* option in the main There are two parts to handle this. First, the notification must define an action. For this purpose, foot will add a "default" action to the notification (see the - *command-action-arg* option). + *command-action-argument* option). Second, foot needs to know when the notification is activated, and it needs to get hold of the XDG activation token. @@ -524,18 +525,41 @@ xdgtoken=18179adf579a7a904ce73754964b1ec3 Foot recognizes the following things from the notification helper's stdout: - - _nnn_: integer in base 10, daemon assigned notification ID - - *id=*_nnn_: same as plain _nnn_. + - _id_: integer in base 10, daemon assigned notification ID + - *id=*_id_: same as plain _nnn_. - *default*: the 'default' action was triggered - *action=*_default_: same as _default_ - *action=*_n_: application custom action _n_ triggered + - _n_: integer in base 10, appearing after the ID; application + custom action _n_ triggered - *xdgtoken=*_xyz_: XDG activation token. - Example: + Example #1: 17++ action=default++ xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 - + + Foot recognizes this as: + - notification has the daemon assigned ID 17 + - the user triggered the default action + - the notification send an XDG activation token + + Example #2: + 17++ +1 + + Foot recognizes this as: + - notification has the daemon assigned ID 17 + - the user triggered the first custom action, "1" + + Example #3: + id=17++ +1 + + Foot recognizes this as: + - notification has the daemon assigned ID 17 + - the user triggered the first custom action, "1 + Default: _notify-send++ --wait++ --app-name ${app-id}++ @@ -545,11 +569,11 @@ xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 --expire-time ${expire-time}++ --hint STRING:image-path:${icon}++ --replace-id ${replace-id}++ - ${action-arg}++ + ${action-argument}++ --print-id++ -- ${title} ${body}_. -*command-action-arg* +*command-action-argument* String to use with *command* to enable passing action/button names to the notification helper. @@ -561,32 +585,32 @@ xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 Furhermore, the OSC-99 notifications protocol allows applications to define their own actions. Foot uses a combination of the - *command* option, and the *command-action-arg* option to pass the - names of the actions to the notification helper. + *command* option, and the *command-action-argument* option to pass + the names of the actions to the notification helper. This option has the following template arguments: - _${action-name}_: the name of the action; *default* for the default action configured by foot, and _n_, where _n_ is an integer >= 1, for application defined actions. - - _${action-label}_: *Click to activate* for the default action, - and a free-form string for application defined actions. + - _${action-label}_: *Activate* for the default action, and a + free-form string for application defined actions. For each notification action (remember, there will always be at - least one), *command-action-arg* will be expanded with the + least one), *command-action-argument* will be expanded with the action's name and label. - Then, _${action-arg}_ is expanded *command* to the full list of - actions. + Then, _${action-argument}_ is expanded *command* to the full list + of actions. - If *command-action-arg* is set to the empty string, no actions - will be passed to *command*. That is, _${action-arg}_ will be - replaced with the empty string. + If *command-action-argument* is set to the empty string, no + actions will be passed to *command*. That is, _${action-argument}_ + will be replaced with the empty string. Example: - *command-action-arg=--action ${action-name}=${action-label}* - *command=notify-send ${action-arg} ...* + *command-action-argument=--action ${action-name}=${action-label}*++ +*command=notify-send ${action-argument} ...* Assume the application defined two custom actions: *OK* and *Cancel*. diff --git a/foot.ini b/foot.ini index 0ae0b52f..029daa9b 100644 --- a/foot.ini +++ b/foot.ini @@ -47,8 +47,8 @@ # command-focused=no [desktop-notifications] -# command=notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --replace-id ${replace-id} ${action-arg} --print-id -- ${title} ${body} -# command-action-arg=--action ${action-name}=${action-label} +# command=notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --replace-id ${replace-id} ${action-argument} --print-id -- ${title} ${body} +# command-action-argument=--action ${action-name}=${action-label} # close="" # inhibit-when-focused=yes diff --git a/main.c b/main.c index 207e6eb7..47bccf2b 100644 --- a/main.c +++ b/main.c @@ -261,7 +261,7 @@ main(int argc, char *const *argv) break; case 't': - tll_push_back(overrides, xstrjoin("term=", optarg)); + tll_push_back(overrides, xstrjoin("term=", optarg, 0)); break; case 'L': @@ -269,11 +269,11 @@ main(int argc, char *const *argv) break; case 'T': - tll_push_back(overrides, xstrjoin("title=", optarg)); + tll_push_back(overrides, xstrjoin("title=", optarg, 0)); break; case 'a': - tll_push_back(overrides, xstrjoin("app-id=", optarg)); + tll_push_back(overrides, xstrjoin("app-id=", optarg, 0)); break; case 'D': { @@ -287,7 +287,7 @@ main(int argc, char *const *argv) } case 'f': { - char *font_override = xstrjoin("font=", optarg); + char *font_override = xstrjoin("font=", optarg, 0); tll_push_back(overrides, font_override); break; } @@ -658,3 +658,18 @@ out: log_deinit(); return ret == EXIT_SUCCESS && !as_server ? shutdown_ctx.exit_code : ret; } + +UNITTEST +{ + char *s = xstrjoin("foo", "bar", 0); + xassert(streq(s, "foobar")); + free(s); + + s = xstrjoin("foo", "bar", ' '); + xassert(streq(s, "foo bar")); + free(s); + + s = xstrjoin("foo", "bar", ','); + xassert(streq(s, "foo,bar")); + free(s); +} diff --git a/notify.c b/notify.c index e109e6a8..6315e61f 100644 --- a/notify.c +++ b/notify.c @@ -23,13 +23,15 @@ void notify_free(struct terminal *term, struct notification *notif) { - fdm_del(term->fdm, notif->stdout_fd); + if (notif->pid > 0) + fdm_del(term->fdm, notif->stdout_fd); + free(notif->id); free(notif->title); free(notif->body); free(notif->category); free(notif->app_id); - free(notif->icon_id); + free(notif->icon_cache_id); free(notif->icon_symbolic_name); free(notif->icon_data); free(notif->xdg_token); @@ -44,6 +46,8 @@ notify_free(struct terminal *term, struct notification *notif) if (notif->icon_fd >= 0) close(notif->icon_fd); } + + memset(notif, 0, sizeof(*notif)); } static bool @@ -119,9 +123,10 @@ consume_stdout(struct notification *notif, bool eof) break; uint32_t maybe_id = 0; + uint32_t maybe_button_nr = 0; /* Check for daemon assigned ID, either '123', or 'id=123' */ - if (to_integer(line, len, &maybe_id) || + if ((notif->external_id == 0 && to_integer(line, len, &maybe_id)) || (len > 3 && memcmp(line, "id=", 3) == 0 && to_integer(&line[3], len - 3, &maybe_id))) { @@ -140,7 +145,6 @@ consume_stdout(struct notification *notif, bool eof) else if (len > 7 && memcmp(line, "action=", 7) == 0) { notif->activated = true; - uint32_t maybe_button_nr; if (to_integer(&line[7], len - 7, &maybe_button_nr)) { notif->activated_button = maybe_button_nr; LOG_DBG("custom action %u triggered", notif->activated_button); @@ -150,6 +154,18 @@ consume_stdout(struct notification *notif, bool eof) } } + else if (notif->external_id > 0 && + to_integer(line, len, &maybe_button_nr) && + maybe_button_nr > 0 && + maybe_button_nr <= notif->button_count) + { + /* Single integer, appearing *after* the ID, and is within + the custom button/action range */ + notif->activated = true; + notif->activated_button = maybe_button_nr; + LOG_DBG("custom action %u triggered", notif->activated_button); + } + /* Check for XDG activation token, 'xdgtoken=xyz' */ else if (len > 9 && memcmp(line, "xdgtoken=", 9) == 0) { notif->xdg_token = xstrndup(&line[9], len - 9); @@ -272,6 +288,31 @@ notif_done(struct reaper *reaper, pid_t pid, int status, void *data) } } +static bool +expand_action_to_argv(struct terminal *term, const char *name, const char *label, + size_t *argc, char ***argv) +{ + char **expanded = NULL; + size_t count = 0; + + if (!spawn_expand_template( + &term->conf->desktop_notifications.command_action_arg, 2, + (const char *[]){"action-name", "action-label"}, + (const char *[]){name, label}, + &count, &expanded)) + { + return false; + } + + /* Append to the "global" actions argv */ + *argv = xrealloc(*argv, (*argc + count) * sizeof((*argv)[0])); + memcpy(&(*argv)[*argc], expanded, count * sizeof(expanded[0])); + *argc += count; + + free(expanded); + return true; +} + bool notify_notify(struct terminal *term, struct notification *notif) { @@ -309,11 +350,11 @@ notify_notify(struct terminal *term, struct notification *notif) /* Icon: symbolic name if present, otherwise a filename */ const char *icon_name_or_path = ""; - if (notif->icon_id != NULL) { + if (notif->icon_cache_id != NULL) { for (size_t i = 0; i < ALEN(term->notification_icons); i++) { const struct notification_icon *icon = &term->notification_icons[i]; - if (icon->id != NULL && streq(icon->id, notif->icon_id)) { + if (icon->id != NULL && streq(icon->id, notif->icon_cache_id)) { /* For now, we set the symbolic name to 'file:///path' * when using a file based icon. */ xassert(icon->symbolic_name != NULL); @@ -397,59 +438,27 @@ notify_notify(struct terminal *term, struct notification *notif) xsnprintf(expire_time, sizeof(expire_time), "%d", notif->expire_time); if (term->conf->desktop_notifications.command_action_arg.argv.args) { - struct action { - const char *name; - const char *label; - }; - - tll(struct action) actions = tll_init(); - tll_push_back(actions, ((struct action){"default", "Click to activate"})); - - tll_foreach(notif->actions, it) { - tll_push_back(actions, ((struct action){NULL, it->item})); + if (!expand_action_to_argv( + term, "default", "Activate", &action_argc, &action_argv)) + { + return false; } - size_t action_idx = 0; - tll_foreach(actions, it) { - const char *name = it->item.name; - const char *label = it->item.label; + size_t action_idx = 1; + tll_foreach(notif->actions, it) { - /* - * Custom actions (buttons) start at 1. - * - * We always insert our own default action first, causing - * all custom actions to start at index 1 in our list. - */ - char numerical_name[16]; - xsnprintf(numerical_name, sizeof(numerical_name), "%zu", action_idx); + /* Custom actions use a numerical name, starting at 1 */ + char name[16]; + xsnprintf(name, sizeof(name), "%zu", action_idx++); - if (name == NULL) - name = numerical_name; - - char **expanded = NULL; - size_t count = 0; - - if (!spawn_expand_template( - &term->conf->desktop_notifications.command_action_arg, 2, - (const char *[]){"action-name", "action-label"}, - (const char *[]){name, label}, - &count, &expanded)) + if (!expand_action_to_argv( + term, name, it->item, &action_argc, &action_argv)) { + for (size_t i = 0; i < action_argc; i++) + free(action_argv[i]); + free(action_argv); return false; } - - /* Append to the "global" actions argv */ - action_argv = xrealloc( - action_argv, (action_argc + count) * sizeof(action_argv[0])); - - for (size_t i = 0; i < count; i++) - action_argv[action_argc + i] = expanded[i]; - - action_argc += count; - - free(expanded); - action_idx++; - tll_remove(actions, it); } } @@ -457,33 +466,35 @@ notify_notify(struct terminal *term, struct notification *notif) &term->conf->desktop_notifications.command, 10, (const char *[]){ "app-id", "window-title", "icon", "title", "body", "category", - "urgency", "expire-time", "replace-id", "action-arg"}, + "urgency", "expire-time", "replace-id", "action-argument"}, (const char *[]){ app_id, term->window_title, icon_name_or_path, title, body, notif->category != NULL ? notif->category : "", urgency_str, expire_time, replaces_id_str, /* Custom expansion below, since we need to expand to multiple arguments */ - "${action-arg}"}, + "${action-argument}"}, &argc, &argv)) { return false; } + /* Post-process the expanded argv, and patch in all the --action + arguments we expanded earlier */ for (size_t i = 0; i < argc; i++) { - if (!streq(argv[i], "${action-arg}")) + if (!streq(argv[i], "${action-argument}")) continue; if (action_argc == 0) { free(argv[i]); - /* Remove ${command-arg}, but include terminating NULL */ + /* Remove ${command-argument}, but include terminating NULL */ memmove(&argv[i], &argv[i + 1], (argc - i) * sizeof(argv[0])); argc--; break; } - /* Remove the "${action-arg}" entry, add all actions argument + /* Remove the "${action-argument}" entry, add all actions argument from earlier, but include terminating NULL */ argv = xrealloc(argv, (argc + action_argc) * sizeof(argv[0])); @@ -492,7 +503,7 @@ notify_notify(struct terminal *term, struct notification *notif) &argv[i + 1], (argc - i) * sizeof(argv[0])); /* Include terminating NULL */ - free(argv[i]); /* Free xstrdup("${action-arg}"); */ + free(argv[i]); /* Free xstrdup("${action-argument}"); */ /* Insert the action arguments */ for (size_t j = 0; j < action_argc; j++) { @@ -501,7 +512,7 @@ notify_notify(struct terminal *term, struct notification *notif) } argc += action_argc; - argc--; /* The ${action-arg} option has been removed */ + argc--; /* The ${action-argument} option has been removed */ break; } @@ -518,12 +529,15 @@ notify_notify(struct terminal *term, struct notification *notif) /* Non-fatal */ } else { tll_push_back(term->active_notifications, *notif); + + /* We've taken over ownership of all data; clear, so that + notify_free() doesn't double free */ notif->id = NULL; notif->title = NULL; notif->body = NULL; notif->category = NULL; notif->app_id = NULL; - notif->icon_id = NULL; + notif->icon_cache_id = NULL; notif->icon_symbolic_name = NULL; notif->icon_data = NULL; notif->icon_data_sz = 0; @@ -533,6 +547,7 @@ notify_notify(struct terminal *term, struct notification *notif) struct notification *new_notif = &tll_back(term->active_notifications); /* We don't need these anymore. They'll be free:d by the caller */ + new_notif->button_count = tll_length(notif->actions); memset(&new_notif->actions, 0, sizeof(new_notif->actions)); notif = new_notif; } diff --git a/notify.h b/notify.h index 2a7c5ca1..85de209e 100644 --- a/notify.h +++ b/notify.h @@ -29,26 +29,28 @@ struct notification { /* * Set by caller of notify_notify() */ - char *id; - char *title; + char *id; /* Internal notification ID */ + + char *app_id; /* Custom app-id, overrides the terminal's app-id if set */ + char *title; /* Required */ char *body; char *category; - char *app_id; /* Custm app-id, overrides the terminal's app-id */ - char *icon_id; - char *icon_symbolic_name; - uint8_t *icon_data; - size_t icon_data_sz; - enum notify_when when; enum notify_urgency urgency; int32_t expire_time; + tll(char *) actions; - bool focus; - bool may_be_programatically_closed; - bool report_activated; - bool report_closed; + char *icon_cache_id; + char *icon_symbolic_name; + uint8_t *icon_data; + size_t icon_data_sz; + + bool focus; /* Focus the foot window when notification is activated */ + bool may_be_programatically_closed; /* OSC-99: notification may be programatically closed by the client */ + bool report_activated; /* OSC-99: report notification activation to client */ + bool report_closed; /* OSC-99: report notification closed to client */ /* * Used internally by notify @@ -56,6 +58,7 @@ struct notification { uint32_t external_id; /* Daemon assigned notification ID */ bool activated; /* User 'activated' the notification */ + uint32_t button_count; /* Number of buttons (custom actions) in notification */ uint32_t activated_button; /* User activated one of the custom actions */ char *xdg_token; /* XDG activation token, from daemon */ diff --git a/osc.c b/osc.c index 360ea6ee..eb5e9718 100644 --- a/osc.c +++ b/osc.c @@ -579,7 +579,7 @@ kitty_notification(struct terminal *term, char *string) char *id = NULL; /* The 'i' parameter */ char *app_id = NULL; /* The 'f' parameter */ - char *icon_id = NULL; /* The 'g' parameter */ + char *icon_cache_id = NULL; /* The 'g' parameter */ char *symbolic_icon = NULL; /* The 'n' parameter */ char *category = NULL; /* The 't' parameter */ char *payload = NULL; @@ -693,18 +693,24 @@ kitty_notification(struct terminal *term, char *string) else if (streq(value, "?")) { /* Query capabilities */ - char when_str[64]; - strcpy(when_str, "unfocused"); + const char *reply_id = id != NULL ? id : "0"; + + const char *p_caps = "title,body,?,close,alive,icon,buttons"; + const char *a_caps = "focus,report"; + const char *u_caps = "0,1,2"; + + char when_caps[64]; + strcpy(when_caps, "unfocused"); if (!term->conf->desktop_notifications.inhibit_when_focused) - strcat(when_str, ",always"); + strcat(when_caps, ",always"); const char *terminator = term->vt.osc.bel ? "\a" : "\033\\"; char reply[128]; int n = xsnprintf( reply, sizeof(reply), - "\033]99;i=%s:p=?;p=title,body,?,close,alive,icon,buttons:a=focus,report:o=%s:u=0,1,2:c=1:w=1%s", - id != NULL ? id : "0", when_str, terminator); + "\033]99;i=%s:p=?;p=%s:a=%s:o=%s:u=%s:c=1:w=1%s", + reply_id, p_caps, a_caps, when_caps, u_caps, terminator); xassert(n < sizeof(reply)); term_to_slave(term, reply, n); @@ -759,60 +765,79 @@ kitty_notification(struct terminal *term, char *string) if (category == NULL) category = decoded; else { - const size_t old_len = strlen(category); - const size_t new_len = strlen(decoded); - /* Append, comma separated */ - category = xrealloc(category, old_len + 1 + new_len + 1); - category[old_len] = ','; - memcpy(&category[old_len + 1], decoded, new_len); - category[old_len + 1 + new_len] = '\0'; + char *old_category = category; + category = xstrjoin(old_category, decoded, ','); free(decoded); + free(old_category); } } break; } case 'g': - /* graphical ID */ - free(icon_id); - icon_id = xstrdup(value); + /* graphical ID (see 'n' and 'p=icon') */ + free(icon_cache_id); + icon_cache_id = xstrdup(value); break; - case 'n': - /* Symbolic icon name, used with 'g' */ - free(symbolic_icon); - symbolic_icon = base64_decode(value, NULL); + case 'n': { + /* Symbolic icon name, may used with 'g' */ - /* Translate OSC-99 "special" names */ - if (symbolic_icon != NULL) { - const char *translated_name = NULL; + /* + * Sigh, protocol says 'n' can be used multiple times, and + * that the terminal picks the first one that it can + * resolve. + * + * We can't resolve any icons at all. So, enter + * heuristics... let's pick the *shortest* symbolic + * name. The idea is that icon *names* are typically + * shorter than .desktop names, and macOS bundle + * identifiers. + */ + char *maybe_new_symbolic_icon = base64_decode(value, NULL); + if (maybe_new_symbolic_icon == NULL) + break; - if (streq(symbolic_icon, "error")) - translated_name = "dialog-error"; - else if (streq(symbolic_icon, "warn") || - streq(symbolic_icon, "warning")) - translated_name = "dialog-warning"; - else if (streq(symbolic_icon, "info")) - translated_name = "dialog-information"; - else if (streq(symbolic_icon, "question")) - translated_name = "dialog-question"; - else if (streq(symbolic_icon, "help")) - translated_name = "system-help"; - else if (streq(symbolic_icon, "file-manager")) - translated_name = "system-file-manager"; - else if (streq(symbolic_icon, "system-monitor")) - translated_name = "utilities-system-monitor"; - else if (streq(symbolic_icon, "text-editor")) - translated_name = "text-editor"; + if (symbolic_icon == NULL || + strlen(maybe_new_symbolic_icon) < strlen(symbolic_icon)) + { + free(symbolic_icon); + symbolic_icon = maybe_new_symbolic_icon; - if (translated_name != NULL) { - free(symbolic_icon); - symbolic_icon = xstrdup(translated_name); + /* Translate OSC-99 "special" names */ + if (symbolic_icon != NULL) { + const char *translated_name = NULL; + + if (streq(symbolic_icon, "error")) + translated_name = "dialog-error"; + else if (streq(symbolic_icon, "warn") || + streq(symbolic_icon, "warning")) + translated_name = "dialog-warning"; + else if (streq(symbolic_icon, "info")) + translated_name = "dialog-information"; + else if (streq(symbolic_icon, "question")) + translated_name = "dialog-question"; + else if (streq(symbolic_icon, "help")) + translated_name = "system-help"; + else if (streq(symbolic_icon, "file-manager")) + translated_name = "system-file-manager"; + else if (streq(symbolic_icon, "system-monitor")) + translated_name = "utilities-system-monitor"; + else if (streq(symbolic_icon, "text-editor")) + translated_name = "text-editor"; + + if (translated_name != NULL) { + free(symbolic_icon); + symbolic_icon = xstrdup(translated_name); + } } + } else { + free(maybe_new_symbolic_icon); } break; } + } } if (base64) { @@ -824,42 +849,28 @@ kitty_notification(struct terminal *term, char *string) payload_size = strlen(payload); } - /* Search for an existing (d=0) notification to update */ - struct notification *notif = NULL; - tll_foreach(term->kitty_notifications, it) { - if ((id == NULL && it->item.id == NULL) || - (id != NULL && it->item.id != NULL && streq(it->item.id, id))) - { - /* Found existing notification */ - notif = &it->item; - break; - } + /* Append metadata to previous notification chunk */ + struct notification *notif = &term->kitty_notification; + + if (!((id == NULL && notif->id == NULL) || + (id != NULL && notif->id != NULL && streq(id, notif->id))) || + !notif->may_be_programatically_closed) /* Free:d notification has this as false... */ + { + /* ID mismatch, ignore previous notification state */ + notify_free(term, notif); + + notif->id = id; + notif->when = when; + notif->urgency = urgency; + notif->expire_time = expire_time; + notif->focus = focus; + notif->may_be_programatically_closed = true; + notif->report_activated = report_activated; + notif->report_closed = report_closed; + + id = NULL; /* Prevent double free */ } - if (notif == NULL) { - tll_push_front(term->kitty_notifications, ((struct notification){ - .id = id, - .when = when, - .urgency = urgency, - .expire_time = expire_time, - .actions = tll_init(), - .focus = focus, - .may_be_programatically_closed = true, - .report_activated = report_activated, - .report_closed = report_closed, - .stdout_fd = -1, - })); - - id = NULL; /* Prevent double free */ - notif = &tll_front(term->kitty_notifications); - } - - if (notif->pid > 0) { - /* Notification has already been completed, ignore new metadata */ - goto out; - } - - /* Update notification metadata */ if (have_a) { notif->focus = focus; notif->report_activated = report_activated; @@ -875,10 +886,10 @@ kitty_notification(struct terminal *term, char *string) if (have_w) notif->expire_time = expire_time; - if (icon_id != NULL) { - free(notif->icon_id); - notif->icon_id = icon_id; - icon_id = NULL; /* Prevent double free */ + if (icon_cache_id != NULL) { + free(notif->icon_cache_id); + notif->icon_cache_id = icon_cache_id; + icon_cache_id = NULL; /* Prevent double free */ } if (symbolic_icon != NULL) { @@ -898,15 +909,10 @@ kitty_notification(struct terminal *term, char *string) notif->category = category; category = NULL; /* Prevent double free */ } else { - const size_t old_len = strlen(notif->category); - const size_t new_len = strlen(category); - /* Append, comma separated */ - notif->category = - xrealloc(notif->category, old_len + 1 + new_len + 1); - notif->category[old_len] = ','; - memcpy(¬if->category[old_len + 1], category, new_len); - notif->category[old_len + 1 + new_len] = '\0'; + char *new_category = xstrjoin(notif->category, category, ','); + free(notif->category); + notif->category = new_category; } } @@ -923,7 +929,7 @@ kitty_notification(struct terminal *term, char *string) payload = NULL; } else { char *old = *ptr; - *ptr = xstrjoin(old, payload); + *ptr = xstrjoin(old, payload, 0); free(old); } break; @@ -964,11 +970,11 @@ kitty_notification(struct terminal *term, char *string) if (done) { /* Update icon cache, if necessary */ - if (notif->icon_id != NULL && + if (notif->icon_cache_id != NULL && (notif->icon_symbolic_name != NULL || notif->icon_data != NULL)) { - notify_icon_del(term, notif->icon_id); - notify_icon_add(term, notif->icon_id, + notify_icon_del(term, notif->icon_cache_id); + notify_icon_add(term, notif->icon_cache_id, notif->icon_symbolic_name, notif->icon_data, notif->icon_data_sz); @@ -985,27 +991,19 @@ kitty_notification(struct terminal *term, char *string) notify_close(term, notif->id); } else if (payload_type == PAYLOAD_ALIVE) { char *alive_ids = NULL; - size_t alive_ids_len = 0; tll_foreach(term->active_notifications, it) { /* TODO: check with kitty: use "0" for all notifications with no ID? */ const char *item_id = it->item.id != NULL ? it->item.id : "0"; - const size_t id_len = strlen(item_id); - if (alive_ids == NULL) { + if (alive_ids == NULL) alive_ids = xstrdup(item_id); - alive_ids_len = id_len; - } else { - alive_ids = xrealloc(alive_ids, alive_ids_len + 1 + id_len + 1); - - /* Append ",<id>" */ - alive_ids[alive_ids_len] = ','; - memcpy(&alive_ids[alive_ids_len + 1], item_id, id_len); - - alive_ids_len += 1 + id_len; - alive_ids[alive_ids_len] = '\0'; + else { + char *old_alive_ids = alive_ids; + alive_ids = xstrjoin(old_alive_ids, item_id, ','); + free(old_alive_ids); } } @@ -1029,19 +1027,13 @@ kitty_notification(struct terminal *term, char *string) } } - tll_foreach(term->kitty_notifications, it) { - if (&it->item == notif) { - notify_free(term, &it->item); - tll_remove(term->kitty_notifications, it); - break; - } - } + notify_free(term, notif); } out: free(id); free(app_id); - free(icon_id); + free(icon_cache_id); free(symbolic_icon); free(payload); free(category); diff --git a/terminal.c b/terminal.c index bcd2651e..16de2d65 100644 --- a/terminal.c +++ b/terminal.c @@ -994,7 +994,7 @@ reload_fonts(struct terminal *term, bool resize_grid) snprintf(size, sizeof(size), ":size=%.2f", term->font_sizes[i][j].pt_size * scale); - names[i][j] = xstrjoin(font->pattern, size); + names[i][j] = xstrjoin(font->pattern, size, 0); } } @@ -1021,9 +1021,9 @@ reload_fonts(struct terminal *term, bool resize_grid) 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" : ""), + [1] = xstrjoin(dpi, !custom_bold ? ":weight=bold" : "", 0), + [2] = xstrjoin(dpi, !custom_italic ? ":slant=italic" : "", 0), + [3] = xstrjoin(dpi, !custom_bold_italic ? ":weight=bold:slant=italic" : "", 0), }; struct fcft_font *fonts[4]; @@ -1313,7 +1313,6 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED .ime_enabled = true, #endif - .kitty_notifications = tll_init(), .active_notifications = tll_init(), }; @@ -1823,11 +1822,7 @@ term_destroy(struct terminal *term) tll_remove(term->ptmx_paste_buffers, it); } - tll_foreach(term->kitty_notifications, it) { - notify_free(term, &it->item); - tll_remove(term->kitty_notifications, it); - } - + notify_free(term, &term->kitty_notification); tll_foreach(term->active_notifications, it) { notify_free(term, &it->item); tll_remove(term->active_notifications, it); @@ -2041,11 +2036,7 @@ term_reset(struct terminal *term, bool hard) tll_remove(term->alt.sixel_images, it); } - tll_foreach(term->kitty_notifications, it) { - notify_free(term, &it->item); - tll_remove(term->kitty_notifications, it); - } - + notify_free(term, &term->kitty_notification); tll_foreach(term->active_notifications, it) { notify_free(term, &it->item); tll_remove(term->active_notifications, it); diff --git a/terminal.h b/terminal.h index 92d1e8f5..a87a125b 100644 --- a/terminal.h +++ b/terminal.h @@ -799,9 +799,11 @@ struct terminal { void *cb_data; } shutdown; - /* Notifications that either haven't been sent yet, or have been - sent but not yet dismissed */ - tll(struct notification) kitty_notifications; + /* State, to handle chunked notifications */ + struct notification kitty_notification; + + /* Currently active notifications, from foot's perspective (their + notification helper processes are still running) */ tll(struct notification) active_notifications; struct notification_icon notification_icons[32]; diff --git a/tests/test-config.c b/tests/test-config.c index ae111446..a41e8536 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -580,7 +580,7 @@ test_section_desktop_notifications(void) test_boolean(&ctx, &parse_section_desktop_notifications, "inhibit-when-focused", &conf.desktop_notifications.inhibit_when_focused); test_spawn_template(&ctx, &parse_section_desktop_notifications, "command", &conf.desktop_notifications.command); - test_spawn_template(&ctx, &parse_section_desktop_notifications, "command-action-arg", &conf.desktop_notifications.command_action_arg); + test_spawn_template(&ctx, &parse_section_desktop_notifications, "command-action-argument", &conf.desktop_notifications.command_action_arg); test_spawn_template(&ctx, &parse_section_desktop_notifications, "close", &conf.desktop_notifications.close); config_free(&conf); diff --git a/xmalloc.h b/xmalloc.h index 67fa5c43..76db7e1b 100644 --- a/xmalloc.h +++ b/xmalloc.h @@ -25,12 +25,16 @@ xmemdup(const void *ptr, size_t size) } static inline char * -xstrjoin(const char *s1, const char *s2) +xstrjoin(const char *s1, const char *s2, char delim) { size_t n1 = strlen(s1); - size_t n2 = strlen(s2); - char *joined = xmalloc(n1 + n2 + 1); + size_t n2 = delim > 0 ? 1 : 0; + size_t n3 = strlen(s2); + + char *joined = xmalloc(n1 + n2 + n3 + 1); memcpy(joined, s1, n1); - memcpy(joined + n1, s2, n2 + 1); + if (delim > 0) + joined[n1] = delim; + memcpy(joined + n1 + n2, s2, n3 + 1); return joined; } From ebd8ad893710d87f5275e98127f1e70eb9338ea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 2 Aug 2024 08:22:37 +0200 Subject: [PATCH 0837/1323] doc: foot.ini: codespell: furhermore -> furthermore --- doc/foot.ini.5.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 11114feb..8f9ffe57 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -583,7 +583,7 @@ xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 application (depending on how the application generated the notification). - Furhermore, the OSC-99 notifications protocol allows applications + Furthermore, the OSC-99 notifications protocol allows applications to define their own actions. Foot uses a combination of the *command* option, and the *command-action-argument* option to pass the names of the actions to the notification helper. From 1a895387008e99f181dd72e3cb1404f80804ed08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 2 Aug 2024 08:22:57 +0200 Subject: [PATCH 0838/1323] notify: codespell: notificaton -> notification --- notify.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notify.c b/notify.c index 6315e61f..9d2d3c71 100644 --- a/notify.c +++ b/notify.c @@ -394,7 +394,7 @@ notify_notify(struct terminal *term, struct notification *notif) continue; /* - * When replacing/updating a notificaton, we may have + * When replacing/updating a notification, we may have * *multiple* notification helpers running for the "same" * notification. Make sure only the *last* notification's * report closed/activated are honored, to avoid sending From 09ab8c6c7cde19edd21f9e7aec98a724df16c0f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 2 Aug 2024 08:25:29 +0200 Subject: [PATCH 0839/1323] notify: codespell: programatically -> programmatically --- notify.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notify.h b/notify.h index 85de209e..592d2c84 100644 --- a/notify.h +++ b/notify.h @@ -48,7 +48,7 @@ struct notification { size_t icon_data_sz; bool focus; /* Focus the foot window when notification is activated */ - bool may_be_programatically_closed; /* OSC-99: notification may be programatically closed by the client */ + bool may_be_programatically_closed; /* OSC-99: notification may be programmatically closed by the client */ bool report_activated; /* OSC-99: report notification activation to client */ bool report_closed; /* OSC-99: report notification closed to client */ From a9e462d952ca1e0367e9e6eb6ee00a0cda7e8c9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 1 Aug 2024 20:11:04 +0200 Subject: [PATCH 0840/1323] Remove a number of unused includes --- csi.c | 1 - main.c | 3 --- render.c | 1 - server.c | 1 - slave.c | 1 - vt.c | 1 - 6 files changed, 8 deletions(-) diff --git a/csi.c b/csi.c index 2b9f137d..91d42e2a 100644 --- a/csi.c +++ b/csi.c @@ -3,7 +3,6 @@ #include <stdlib.h> #include <string.h> #include <unistd.h> -#include <errno.h> #if defined(_DEBUG) #include <stdio.h> diff --git a/main.c b/main.c index 47bccf2b..a6ec1cb7 100644 --- a/main.c +++ b/main.c @@ -1,7 +1,6 @@ #include <stdlib.h> #include <stdio.h> #include <string.h> -#include <ctype.h> #include <stdbool.h> #include <limits.h> #include <locale.h> @@ -36,8 +35,6 @@ #include "xmalloc.h" #include "xsnprintf.h" -#include "char32.h" - #if !defined(__STDC_UTF_32__) || !__STDC_UTF_32__ #error "char32_t does not use UTF-32" #endif diff --git a/render.c b/render.c index ef135b53..279e51bf 100644 --- a/render.c +++ b/render.c @@ -1,7 +1,6 @@ #include "render.h" #include <string.h> -#include <wctype.h> #include <unistd.h> #include <signal.h> diff --git a/server.c b/server.c index 53e86088..78d98d53 100644 --- a/server.c +++ b/server.c @@ -18,7 +18,6 @@ #include "log.h" #include "client-protocol.h" -#include "shm.h" #include "terminal.h" #include "util.h" #include "wayland.h" diff --git a/slave.c b/slave.c index 2187eef3..00105adb 100644 --- a/slave.c +++ b/slave.c @@ -19,7 +19,6 @@ #include "debug.h" #include "macros.h" -#include "terminal.h" #include "tokenize.h" #include "util.h" #include "xmalloc.h" diff --git a/vt.c b/vt.c index 487c5f5f..572dd2af 100644 --- a/vt.c +++ b/vt.c @@ -17,7 +17,6 @@ #include "dcs.h" #include "debug.h" #include "emoji-variation-sequences.h" -#include "grid.h" #include "osc.h" #include "sixel.h" #include "util.h" From 901daefd960cb666c0da8ca20e574ed40f1f4cf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 2 Aug 2024 10:18:59 +0200 Subject: [PATCH 0841/1323] changelog: more template parameters we've added to desktop-notifications --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82ad7f9d..c30cc534 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,8 +75,9 @@ * `desktop-notifications.command` option, replaces `notify`. * `desktop-notifications.inhibit-when-focused` option, replaces `notify-focus-inhibit`. -* `${icon}`, `${urgency}` and `${action-argument}` added - to the `desktop-notifications.command` template. +* `${category}`, `${urgency}`, `${expire-time}`, `${replace-id}`, + `${icon}` and `${action-argument}` added to the + `desktop-notifications.command` template. * `desktop-notifications.command-action-argument` option, defining how `${action-argument}` (in `desktop-notifications.command`) should be expanded. From aabb239c0f580134ed90bff825e414c5ea2e9ee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 2 Aug 2024 10:33:18 +0200 Subject: [PATCH 0842/1323] readme: xtgettcap: mention tigetstr() compability --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 8ebc14b1..0f962c74 100644 --- a/README.md +++ b/README.md @@ -639,6 +639,10 @@ capability in the multi query. This allows us to send a proper success/fail flag for each queried capability. Responses for **all** queried capabilities are **always** sent. No queries are ever dropped. +All replies are in `tigetstr()` format. That is, given the same +capability name, foot reply is identical to what `tigetstr()` would +have returned. + # Credits From a176d8fbdbeb6dd01eac3da3b6b5828068f92d29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 2 Aug 2024 12:06:08 +0200 Subject: [PATCH 0843/1323] readme: typo: foot -> foot's --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f962c74..0c3fbd1b 100644 --- a/README.md +++ b/README.md @@ -640,7 +640,7 @@ success/fail flag for each queried capability. Responses for **all** queried capabilities are **always** sent. No queries are ever dropped. All replies are in `tigetstr()` format. That is, given the same -capability name, foot reply is identical to what `tigetstr()` would +capability name, foot's reply is identical to what `tigetstr()` would have returned. From 1272632f3b0b6e5e21675374426732d0b5b4a25b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 2 Aug 2024 14:27:57 +0200 Subject: [PATCH 0844/1323] changelog: prepare for 1.18.0 --- CHANGELOG.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c30cc534..a1549a5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.18.0](#1-18-0) * [1.17.2](#1-17-2) * [1.17.1](#1-17-1) * [1.17.0](#1-17-0) @@ -52,7 +52,8 @@ * [1.2.0](#1-2-0) -## Unreleased +## 1.18.0 + ### Added * `cursor.blink-rate` option, allowing you to configure the rate the @@ -136,7 +137,6 @@ `desktop-notifications.inhibit-when-focused`. -### Removed ### Fixed * Crash when zooming in or out, with `dpi-aware=yes`, and the @@ -170,9 +170,15 @@ [1742]: https://codeberg.org/dnkl/foot/issues/1742 -### Security ### Contributors +* abs3nt +* Artturin +* Craig Barnes +* Jan Beich +* Mariusz Bialonczyk +* Nicolas Kolling Ribas + ## 1.17.2 From b5e692ef8ba1627605b351349505f049b4f76b7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 2 Aug 2024 14:28:16 +0200 Subject: [PATCH 0845/1323] meson: bump version to 1.18.0 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 868d8e36..a99d1a48 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.17.2', + version: '1.18.0', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From 62b0b65d4712abd9ecd21ec2fe684c87a868bcc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 2 Aug 2024 14:33:26 +0200 Subject: [PATCH 0846/1323] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1549a5d..ef655a22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.18.0](#1-18-0) * [1.17.2](#1-17-2) * [1.17.1](#1-17-1) @@ -52,6 +53,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.18.0 ### Added From f87c9bb9f70f1a6c8c7081c762c15ba7eaaf971e Mon Sep 17 00:00:00 2001 From: Craig Barnes <craigbarnes@protonmail.com> Date: Sat, 3 Aug 2024 08:12:13 +0100 Subject: [PATCH 0847/1323] xmalloc: remove delim param from xstrjoin() and add separate xstrjoin3() This avoids the need for an unused third argument for most xstrjoin() calls and replaces the cases where it's needed with a more flexible function. Code generation is the same in both cases, when there are 2 string params and a compile-time known delimiter. This commit also converts 4 uses of xasprintf() to use xstrjoin*(). See also: https://godbolt.org/z/xsjrhv9b6 --- config.c | 10 +++++----- main.c | 18 +++++++++++------- notify.c | 2 +- osc.c | 8 ++++---- slave.c | 4 ++-- terminal.c | 8 ++++---- xmalloc.h | 25 +++++++++++++++++-------- 7 files changed, 44 insertions(+), 31 deletions(-) diff --git a/config.c b/config.c index 46b3d2d2..bf3c4175 100644 --- a/config.c +++ b/config.c @@ -356,9 +356,9 @@ open_config(void) /* First, check XDG_CONFIG_HOME (or .config, if unset) */ if (xdg_config_home != NULL && xdg_config_home[0] != '\0') - path = xstrjoin(xdg_config_home, "/foot/foot.ini", 0); + path = xstrjoin(xdg_config_home, "/foot/foot.ini"); else if (home_dir != NULL) - path = xstrjoin(home_dir, "/.config/foot/foot.ini", 0); + path = xstrjoin(home_dir, "/.config/foot/foot.ini"); if (path != NULL) { LOG_DBG("checking for %s", path); @@ -383,7 +383,7 @@ open_config(void) conf_dir = strtok(NULL, ":")) { free(path); - path = xstrjoin(conf_dir, "/foot/foot.ini", 0); + path = xstrjoin(conf_dir, "/foot/foot.ini"); LOG_DBG("checking for %s", path); int fd = open(path, O_RDONLY | O_CLOEXEC); @@ -843,7 +843,7 @@ parse_section_main(struct context *ctx) return false; } - _include_path = xasprintf("%s/%s", home_dir, value + 2); + _include_path = xstrjoin3(home_dir, "/", value + 2); include_path = _include_path; } else include_path = value; @@ -2931,7 +2931,7 @@ get_server_socket_path(void) const char *wayland_display = getenv("WAYLAND_DISPLAY"); if (wayland_display == NULL) { - return xstrjoin(xdg_runtime, "/foot.sock", 0); + return xstrjoin(xdg_runtime, "/foot.sock"); } return xasprintf("%s/foot-%s.sock", xdg_runtime, wayland_display); diff --git a/main.c b/main.c index a6ec1cb7..f46d712e 100644 --- a/main.c +++ b/main.c @@ -258,7 +258,7 @@ main(int argc, char *const *argv) break; case 't': - tll_push_back(overrides, xstrjoin("term=", optarg, 0)); + tll_push_back(overrides, xstrjoin("term=", optarg)); break; case 'L': @@ -266,11 +266,11 @@ main(int argc, char *const *argv) break; case 'T': - tll_push_back(overrides, xstrjoin("title=", optarg, 0)); + tll_push_back(overrides, xstrjoin("title=", optarg)); break; case 'a': - tll_push_back(overrides, xstrjoin("app-id=", optarg, 0)); + tll_push_back(overrides, xstrjoin("app-id=", optarg)); break; case 'D': { @@ -284,7 +284,7 @@ main(int argc, char *const *argv) } case 'f': { - char *font_override = xstrjoin("font=", optarg, 0); + char *font_override = xstrjoin("font=", optarg); tll_push_back(overrides, font_override); break; } @@ -658,15 +658,19 @@ out: UNITTEST { - char *s = xstrjoin("foo", "bar", 0); + char *s = xstrjoin("foo", "bar"); xassert(streq(s, "foobar")); free(s); - s = xstrjoin("foo", "bar", ' '); + s = xstrjoin3("foo", " ", "bar"); xassert(streq(s, "foo bar")); free(s); - s = xstrjoin("foo", "bar", ','); + s = xstrjoin3("foo", ",", "bar"); xassert(streq(s, "foo,bar")); free(s); + + s = xstrjoin3("foo", "bar", "baz"); + xassert(streq(s, "foobarbaz")); + free(s); } diff --git a/notify.c b/notify.c index 9d2d3c71..15139c40 100644 --- a/notify.c +++ b/notify.c @@ -77,7 +77,7 @@ write_icon_file(const void *data, size_t data_sz, int *fd, char **filename, LOG_DBG("wrote icon data to %s", name); *filename = xstrdup(name); - *symbolic_name = xasprintf("file://%s", *filename); + *symbolic_name = xstrjoin("file://", *filename); return true; } diff --git a/osc.c b/osc.c index eb5e9718..92e3cad6 100644 --- a/osc.c +++ b/osc.c @@ -767,7 +767,7 @@ kitty_notification(struct terminal *term, char *string) else { /* Append, comma separated */ char *old_category = category; - category = xstrjoin(old_category, decoded, ','); + category = xstrjoin3(old_category, ",", decoded); free(decoded); free(old_category); } @@ -910,7 +910,7 @@ kitty_notification(struct terminal *term, char *string) category = NULL; /* Prevent double free */ } else { /* Append, comma separated */ - char *new_category = xstrjoin(notif->category, category, ','); + char *new_category = xstrjoin3(notif->category, ",", category); free(notif->category); notif->category = new_category; } @@ -929,7 +929,7 @@ kitty_notification(struct terminal *term, char *string) payload = NULL; } else { char *old = *ptr; - *ptr = xstrjoin(old, payload, 0); + *ptr = xstrjoin(old, payload); free(old); } break; @@ -1002,7 +1002,7 @@ kitty_notification(struct terminal *term, char *string) alive_ids = xstrdup(item_id); else { char *old_alive_ids = alive_ids; - alive_ids = xstrjoin(old_alive_ids, item_id, ','); + alive_ids = xstrjoin3(old_alive_ids, ",", item_id); free(old_alive_ids); } } diff --git a/slave.c b/slave.c index 00105adb..bf1ca8e0 100644 --- a/slave.c +++ b/slave.c @@ -55,7 +55,7 @@ find_file_in_path(const char *file) path != NULL; path = strtok(NULL, ":")) { - char *full = xasprintf("%s/%s", path, file); + char *full = xstrjoin3(path, "/", file); if (access(full, F_OK) == 0) { free(path_list); return full; @@ -329,7 +329,7 @@ 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); + char *e = xstrjoin3(name, "=", value); /* Search for existing variable. If found, replace it with the new value */ diff --git a/terminal.c b/terminal.c index 16de2d65..5699e6b5 100644 --- a/terminal.c +++ b/terminal.c @@ -994,7 +994,7 @@ reload_fonts(struct terminal *term, bool resize_grid) snprintf(size, sizeof(size), ":size=%.2f", term->font_sizes[i][j].pt_size * scale); - names[i][j] = xstrjoin(font->pattern, size, 0); + names[i][j] = xstrjoin(font->pattern, size); } } @@ -1021,9 +1021,9 @@ reload_fonts(struct terminal *term, bool resize_grid) char *attrs[4] = { [0] = dpi, /* Takes ownership */ - [1] = xstrjoin(dpi, !custom_bold ? ":weight=bold" : "", 0), - [2] = xstrjoin(dpi, !custom_italic ? ":slant=italic" : "", 0), - [3] = xstrjoin(dpi, !custom_bold_italic ? ":weight=bold:slant=italic" : "", 0), + [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]; diff --git a/xmalloc.h b/xmalloc.h index 76db7e1b..8a2c208f 100644 --- a/xmalloc.h +++ b/xmalloc.h @@ -25,16 +25,25 @@ xmemdup(const void *ptr, size_t size) } static inline char * -xstrjoin(const char *s1, const char *s2, char delim) +xstrjoin(const char *s1, const char *s2) { size_t n1 = strlen(s1); - size_t n2 = delim > 0 ? 1 : 0; - size_t n3 = strlen(s2); - - char *joined = xmalloc(n1 + n2 + n3 + 1); + size_t n2 = strlen(s2); + char *joined = xmalloc(n1 + n2 + 1); memcpy(joined, s1, n1); - if (delim > 0) - joined[n1] = delim; - memcpy(joined + n1 + n2, s2, n3 + 1); + memcpy(joined + n1, s2, n2 + 1); + return joined; +} + +static inline char * +xstrjoin3(const char *s1, const char *s2, const char *s3) +{ + size_t n1 = strlen(s1); + size_t n2 = strlen(s2); + size_t n3 = strlen(s3); + char *joined = xmalloc(n1 + n2 + n3 + 1); + memcpy(joined, s1, n1); + memcpy(joined + n1, s2, n2); + memcpy(joined + n1 + n2, s3, n3 + 1); return joined; } From 6b72108ee220dd64a3ff5a43a6c84d99078da62e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 3 Aug 2024 11:05:58 +0200 Subject: [PATCH 0848/1323] osc: kitty notifications: ignore invalid IDs Notification IDs must only use characters from [a-zA-Z0-9_-+.] Terminals **must** sanitize ids received from client programs before sending them back in responses, to mitigate input injection based attacks. That is, they must either reject ids containing characters not from the above set, or remove bad characters when reading ids sent to them. Foot implements the first: reject IDs containing characters not from the above set. --- osc.c | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/osc.c b/osc.c index eb5e9718..3ae8a051 100644 --- a/osc.c +++ b/osc.c @@ -564,6 +564,33 @@ osc_notify(struct terminal *term, char *string) }); } +IGNORE_WARNING("-Wpedantic") +static bool +verify_kitty_id_is_valid(const char *id) +{ + const size_t len = strlen(id); + + for (size_t i = 0; i < len; i++) { + switch (id[i]) { + case 'a' ... 'z': + case 'A' ... 'Z': + case '0' ... '9': + case '_': + case '-': + case '+': + case '.': + break; + + default: + return false; + } + } + + return true; +} +UNIGNORE_WARNINGS + + static void kitty_notification(struct terminal *term, char *string) { @@ -672,8 +699,11 @@ kitty_notification(struct terminal *term, char *string) case 'i': /* id */ - free(id); - id = xstrdup(value); + if (verify_kitty_id_is_valid(value)) { + free(id); + id = xstrdup(value); + } else + LOG_WARN("OSC-99: ignoring invalid 'i' identifier"); break; case 'p': @@ -963,7 +993,7 @@ kitty_notification(struct terminal *term, char *string) tll_push_back(notif->actions, xstrdup(button)); } } - + break; } } From a3a35f2c8c6ce3b35ec22627cb3097f621c46373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 3 Aug 2024 09:04:24 +0200 Subject: [PATCH 0849/1323] term: reload_fonts(): don't ignore return value of thrd_join() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This should fix the ‘ret’ may be used uninitialized warning Closes #1789 --- terminal.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/terminal.c b/terminal.c index 5699e6b5..fe97e958 100644 --- a/terminal.c +++ b/terminal.c @@ -1048,8 +1048,10 @@ reload_fonts(struct terminal *term, bool resize_grid) for (size_t i = 0; i < 4; i++) { if (tids[i] != 0) { int ret; - thrd_join(tids[i], &ret); - success = success && ret; + if (thrd_join(tids[i], &ret) != 0) + success = false; + else + success = success && ret; } else success = false; } From 63492624918cd23a5efdd9b8a8b4f057da66d961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 4 Aug 2024 14:16:56 +0200 Subject: [PATCH 0850/1323] osc: kitty notifications: implement s=silent This implements part of the new 's' (sound) parameter; the 'silent' value. When s=silent, we set the ${muted} template argument to "true". It is intended to set the 'suppress-sound' hint: notify-send --hint BOOLEAN:suppress-sound:${muted} --- config.c | 2 +- doc/foot.ini.5.scd | 6 ++++++ foot.ini | 2 +- notify.c | 13 ++++++++----- notify.h | 2 ++ osc.c | 29 +++++++++++++++++++++++++---- 6 files changed, 43 insertions(+), 11 deletions(-) diff --git a/config.c b/config.c index bf3c4175..5dbbacb7 100644 --- a/config.c +++ b/config.c @@ -3242,7 +3242,7 @@ config_load(struct config *conf, const char *conf_path, parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers); tokenize_cmdline( - "notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --replace-id ${replace-id} ${action-argument} --print-id -- ${title} ${body}", + "notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --hint BOOLEAN:suppress-sound:${muted} --replace-id ${replace-id} ${action-argument} --print-id -- ${title} ${body}", &conf->desktop_notifications.command.argv.args); tokenize_cmdline("--action ${action-name}=${action-label}", &conf->desktop_notifications.command_action_arg.argv.args); tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args); diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 8f9ffe57..6db7ffae 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -459,6 +459,11 @@ Note: do not set *TERM* here; use the *term* option in the main below. Can be used together with e.g. notify-send's *--replace-id* option. + _${muted}_ is replaced by either *true* or *false*, depending + on whether the notification has requested all notification + sounds be muted. It is intended to set the *suppress-sound* + hint (with e.g. notify-send's *--hint* option). + _${action-argument}_ will be expanded to the *command-action-argument* option, for each notification action. There will always be at least one action, the @@ -568,6 +573,7 @@ xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 --urgency ${urgency}++ --expire-time ${expire-time}++ --hint STRING:image-path:${icon}++ + --hint BOOLEAN:suppress-sound:${muted}++ --replace-id ${replace-id}++ ${action-argument}++ --print-id++ diff --git a/foot.ini b/foot.ini index 029daa9b..fba9305e 100644 --- a/foot.ini +++ b/foot.ini @@ -47,7 +47,7 @@ # command-focused=no [desktop-notifications] -# command=notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --replace-id ${replace-id} ${action-argument} --print-id -- ${title} ${body} +# command=notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --hint BOOLEAN:suppress-sound:${muted} --replace-id ${replace-id} ${action-argument} --print-id -- ${title} ${body} # command-action-argument=--action ${action-name}=${action-label} # close="" # inhibit-when-focused=yes diff --git a/notify.c b/notify.c index 15139c40..0b37f40a 100644 --- a/notify.c +++ b/notify.c @@ -421,9 +421,12 @@ notify_notify(struct terminal *term, struct notification *notif) ? "normal" : "critical"; LOG_DBG("notify: title=\"%s\", body=\"%s\", app-id=\"%s\", category=\"%s\", " - "urgency=\"%s\", icon=\"%s\", expires=%d, replaces=%u (tracking: %s)", + "urgency=\"%s\", icon=\"%s\", expires=%d, replaces=%u, muted=%s " + "(tracking: %s)", title, body, app_id, notif->category, urgency_str, icon_name_or_path, - notif->expire_time, replaces_id, track_notification ? "yes" : "no"); + notif->expire_time, replaces_id, + notif->muted ? "yes" : "no", + track_notification ? "yes" : "no"); xassert(title != NULL); if (title == NULL) @@ -463,14 +466,14 @@ notify_notify(struct terminal *term, struct notification *notif) } if (!spawn_expand_template( - &term->conf->desktop_notifications.command, 10, + &term->conf->desktop_notifications.command, 11, (const char *[]){ "app-id", "window-title", "icon", "title", "body", "category", - "urgency", "expire-time", "replace-id", "action-argument"}, + "urgency", "muted", "expire-time", "replace-id", "action-argument"}, (const char *[]){ app_id, term->window_title, icon_name_or_path, title, body, notif->category != NULL ? notif->category : "", urgency_str, - expire_time, replaces_id_str, + notif->muted ? "true" : "false", expire_time, replaces_id_str, /* Custom expansion below, since we need to expand to multiple arguments */ "${action-argument}"}, diff --git a/notify.h b/notify.h index 592d2c84..ccdfdea8 100644 --- a/notify.h +++ b/notify.h @@ -52,6 +52,8 @@ struct notification { bool report_activated; /* OSC-99: report notification activation to client */ bool report_closed; /* OSC-99: report notification closed to client */ + bool muted; /* Explicitly mute the notification */ + /* * Used internally by notify */ diff --git a/osc.c b/osc.c index 28d8b7b8..50415570 100644 --- a/osc.c +++ b/osc.c @@ -609,6 +609,7 @@ kitty_notification(struct terminal *term, char *string) char *icon_cache_id = NULL; /* The 'g' parameter */ char *symbolic_icon = NULL; /* The 'n' parameter */ char *category = NULL; /* The 't' parameter */ + char *sound_name = NULL; /* The 's' parameter */ char *payload = NULL; bool focus = true; /* The 'a' parameter */ @@ -739,7 +740,7 @@ kitty_notification(struct terminal *term, char *string) char reply[128]; int n = xsnprintf( reply, sizeof(reply), - "\033]99;i=%s:p=?;p=%s:a=%s:o=%s:u=%s:c=1:w=1%s", + "\033]99;i=%s:p=?;p=%s:a=%s:o=%s:u=%s:c=1:w=1:s=silent%s", reply_id, p_caps, a_caps, when_caps, u_caps, terminator); xassert(n < sizeof(reply)); @@ -783,10 +784,15 @@ kitty_notification(struct terminal *term, char *string) break; } - case 'f': - free(app_id); - app_id = base64_decode(value, NULL); + case 'f': { + /* App-name */ + char *decoded = base64_decode(value, NULL); + if (decoded != NULL) { + free(app_id); + app_id = decoded; + } break; + } case 't': { /* Type (category) */ @@ -805,6 +811,16 @@ kitty_notification(struct terminal *term, char *string) break; } + case 's': { + /* Sound */ + char *decoded = base64_decode(value, NULL); + if (decoded != NULL) { + free(sound_name); + sound_name = decoded; + } + break; + } + case 'g': /* graphical ID (see 'n' and 'p=icon') */ free(icon_cache_id); @@ -946,6 +962,10 @@ kitty_notification(struct terminal *term, char *string) } } + if (sound_name != NULL) { + notif->muted = streq(sound_name, "silent"); + } + /* Handled chunked payload - append to existing metadata */ switch (payload_type) { case PAYLOAD_TITLE: @@ -1067,6 +1087,7 @@ out: free(symbolic_icon); free(payload); free(category); + free(sound_name); } void From 84d36606cbd3e35aae6a68e69101b22da60dbe99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 4 Aug 2024 15:21:06 +0200 Subject: [PATCH 0851/1323] osc: kitty notifications: add support for XDG sound names --- config.c | 2 +- doc/foot.ini.5.scd | 7 +++++++ foot.ini | 2 +- notify.c | 17 +++++++++++------ notify.h | 1 + osc.c | 11 ++++++++++- 6 files changed, 31 insertions(+), 9 deletions(-) diff --git a/config.c b/config.c index 5dbbacb7..1ec73da0 100644 --- a/config.c +++ b/config.c @@ -3242,7 +3242,7 @@ config_load(struct config *conf, const char *conf_path, parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers); tokenize_cmdline( - "notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --hint BOOLEAN:suppress-sound:${muted} --replace-id ${replace-id} ${action-argument} --print-id -- ${title} ${body}", + "notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --hint BOOLEAN:suppress-sound:${muted} --hint STRING:sound-name:${sound-name} --replace-id ${replace-id} ${action-argument} --print-id -- ${title} ${body}", &conf->desktop_notifications.command.argv.args); tokenize_cmdline("--action ${action-name}=${action-label}", &conf->desktop_notifications.command_action_arg.argv.args); tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args); diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 6db7ffae..71992c8f 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -464,6 +464,12 @@ Note: do not set *TERM* here; use the *term* option in the main sounds be muted. It is intended to set the *suppress-sound* hint (with e.g. notify-send's *--hint* option). + _${sound-name}_ is replaced by sound-name requested by the + notification. This should be a name from the freedesktop sound + naming specification, but this is not something that foot + enforces. It is intended to set the *sound-name* hint (with + e.g. notify-send's *--hint* option). + _${action-argument}_ will be expanded to the *command-action-argument* option, for each notification action. There will always be at least one action, the @@ -574,6 +580,7 @@ xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 --expire-time ${expire-time}++ --hint STRING:image-path:${icon}++ --hint BOOLEAN:suppress-sound:${muted}++ + --hint STRING:sound-name:${sound-name}++ --replace-id ${replace-id}++ ${action-argument}++ --print-id++ diff --git a/foot.ini b/foot.ini index fba9305e..9e2f5f29 100644 --- a/foot.ini +++ b/foot.ini @@ -47,7 +47,7 @@ # command-focused=no [desktop-notifications] -# command=notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --hint BOOLEAN:suppress-sound:${muted} --replace-id ${replace-id} ${action-argument} --print-id -- ${title} ${body} +# command=notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --hint BOOLEAN:suppress-sound:${muted} --hint STRING:sound-name:${sound-name} --replace-id ${replace-id} ${action-argument} --print-id -- ${title} ${body} # command-action-argument=--action ${action-name}=${action-label} # close="" # inhibit-when-focused=yes diff --git a/notify.c b/notify.c index 0b37f40a..d1c06fb1 100644 --- a/notify.c +++ b/notify.c @@ -34,6 +34,7 @@ notify_free(struct terminal *term, struct notification *notif) free(notif->icon_cache_id); free(notif->icon_symbolic_name); free(notif->icon_data); + free(notif->sound_name); free(notif->xdg_token); free(notif->stdout_data); @@ -421,11 +422,11 @@ notify_notify(struct terminal *term, struct notification *notif) ? "normal" : "critical"; LOG_DBG("notify: title=\"%s\", body=\"%s\", app-id=\"%s\", category=\"%s\", " - "urgency=\"%s\", icon=\"%s\", expires=%d, replaces=%u, muted=%s " - "(tracking: %s)", + "urgency=\"%s\", icon=\"%s\", expires=%d, replaces=%u, muted=%s, " + "sound-name=%s (tracking: %s)", title, body, app_id, notif->category, urgency_str, icon_name_or_path, notif->expire_time, replaces_id, - notif->muted ? "yes" : "no", + notif->muted ? "yes" : "no", notif->sound_name, track_notification ? "yes" : "no"); xassert(title != NULL); @@ -466,14 +467,17 @@ notify_notify(struct terminal *term, struct notification *notif) } if (!spawn_expand_template( - &term->conf->desktop_notifications.command, 11, + &term->conf->desktop_notifications.command, 12, (const char *[]){ "app-id", "window-title", "icon", "title", "body", "category", - "urgency", "muted", "expire-time", "replace-id", "action-argument"}, + "urgency", "muted", "sound-name", "expire-time", "replace-id", + "action-argument"}, (const char *[]){ app_id, term->window_title, icon_name_or_path, title, body, notif->category != NULL ? notif->category : "", urgency_str, - notif->muted ? "true" : "false", expire_time, replaces_id_str, + notif->muted ? "true" : "false", + notif->sound_name != NULL ? notif->sound_name : "", + expire_time, replaces_id_str, /* Custom expansion below, since we need to expand to multiple arguments */ "${action-argument}"}, @@ -545,6 +549,7 @@ notify_notify(struct terminal *term, struct notification *notif) notif->icon_data = NULL; notif->icon_data_sz = 0; notif->icon_path = NULL; + notif->sound_name = NULL; notif->icon_fd = -1; notif->stdout_fd = -1; struct notification *new_notif = &tll_back(term->active_notifications); diff --git a/notify.h b/notify.h index ccdfdea8..89b51238 100644 --- a/notify.h +++ b/notify.h @@ -53,6 +53,7 @@ struct notification { bool report_closed; /* OSC-99: report notification closed to client */ bool muted; /* Explicitly mute the notification */ + char *sound_name; /* Should be set to NULL if muted == true */ /* * Used internally by notify diff --git a/osc.c b/osc.c index 50415570..eba1850f 100644 --- a/osc.c +++ b/osc.c @@ -740,7 +740,7 @@ kitty_notification(struct terminal *term, char *string) char reply[128]; int n = xsnprintf( reply, sizeof(reply), - "\033]99;i=%s:p=?;p=%s:a=%s:o=%s:u=%s:c=1:w=1:s=silent%s", + "\033]99;i=%s:p=?;p=%s:a=%s:o=%s:u=%s:c=1:w=1:s=silent,xdg-names%s", reply_id, p_caps, a_caps, when_caps, u_caps, terminator); xassert(n < sizeof(reply)); @@ -964,6 +964,15 @@ kitty_notification(struct terminal *term, char *string) if (sound_name != NULL) { notif->muted = streq(sound_name, "silent"); + + if (notif->muted || streq(sound_name, "system")) { + free(notif->sound_name); + notif->sound_name = NULL; + } else { + free(notif->sound_name); + notif->sound_name = sound_name; + sound_name = NULL; /* Prevent double free */ + } } /* Handled chunked payload - append to existing metadata */ From fb74a2df2766f9f43f6ed6b5020aab25925c671b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 4 Aug 2024 15:23:33 +0200 Subject: [PATCH 0852/1323] changelog: osc-99: sound support --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef655a22..258940d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,14 @@ ## Unreleased ### Added + +* OSC-99: support for the `s` parameter. Supported keywords are + `silent`, `system` and names from the freedesktop sound naming + specification. +* `${muted}` and `${sound-name}` added to the + `desktop-notifications.command` template. + + ### Changed ### Deprecated ### Removed From a0b5f79f3297b4774723e63e954aaf859d0058d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 7 Aug 2024 17:13:38 +0200 Subject: [PATCH 0853/1323] readme: repology: use three columns instead of one --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0c3fbd1b..32dfe11f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The fast, lightweight and minimalistic Wayland terminal emulator. [![Pipeline status](https://gitlab.com/dnkl/foot/badges/master/pipeline.svg)](https://gitlab.com/dnkl/foot/commits/master) [![builds.sr.ht status](https://builds.sr.ht/~dnkl/foot.svg)](https://builds.sr.ht/~dnkl/foot?) -[![Packaging status](https://repology.org/badge/vertical-allrepos/foot.svg)](https://repology.org/project/foot/versions) +[![Packaging status](https://repology.org/badge/vertical-allrepos/foot.svg?columns=3)](https://repology.org/project/foot/versions) ## Index From 8607fb6312f1e201b57518da8b67ca99f414f27b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 7 Aug 2024 17:14:26 +0200 Subject: [PATCH 0854/1323] readme: repology: use four columns instead of three --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 32dfe11f..57e401a2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The fast, lightweight and minimalistic Wayland terminal emulator. [![Pipeline status](https://gitlab.com/dnkl/foot/badges/master/pipeline.svg)](https://gitlab.com/dnkl/foot/commits/master) [![builds.sr.ht status](https://builds.sr.ht/~dnkl/foot.svg)](https://builds.sr.ht/~dnkl/foot?) -[![Packaging status](https://repology.org/badge/vertical-allrepos/foot.svg?columns=3)](https://repology.org/project/foot/versions) +[![Packaging status](https://repology.org/badge/vertical-allrepos/foot.svg?columns=4)](https://repology.org/project/foot/versions) ## Index From 01eca82d33b47dffd46ae5a4a2f5085a374636e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 7 Aug 2024 17:15:00 +0200 Subject: [PATCH 0855/1323] readme: remove CI badges for gitlab and sr.ht --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 57e401a2..dc5421cc 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,6 @@ The fast, lightweight and minimalistic Wayland terminal emulator. [![CI status](https://ci.codeberg.org/api/badges/dnkl/foot/status.svg)](https://ci.codeberg.org/dnkl/foot) -[![Pipeline status](https://gitlab.com/dnkl/foot/badges/master/pipeline.svg)](https://gitlab.com/dnkl/foot/commits/master) -[![builds.sr.ht status](https://builds.sr.ht/~dnkl/foot.svg)](https://builds.sr.ht/~dnkl/foot?) [![Packaging status](https://repology.org/badge/vertical-allrepos/foot.svg?columns=4)](https://repology.org/project/foot/versions) From 9d5d84a8350ec73f432d0522c274764ee37a9a50 Mon Sep 17 00:00:00 2001 From: Shogo Yamazaki <pgp@mocknen.net> Date: Wed, 7 Aug 2024 14:12:13 +0900 Subject: [PATCH 0856/1323] meson: fix false positive detection of `memfd_create` Add the arguments to `has_function` to properly detect availability of the function on SDK environments. --- meson.build | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/meson.build b/meson.build index a99d1a48..7ce9d901 100644 --- a/meson.build +++ b/meson.build @@ -12,7 +12,8 @@ is_debug_build = get_option('buildtype').startswith('debug') cc = meson.get_compiler('c') -if cc.has_function('memfd_create') +if cc.has_function('memfd_create', + args: ['-D_GNU_SOURCE'], prefix: '#include <sys/mman.h>') add_project_arguments('-DMEMFD_CREATE', language: 'c') endif From 803f712332e7bcafbb3b41b69ed5787b6b032bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 9 Aug 2024 08:15:13 +0200 Subject: [PATCH 0857/1323] meson: add "prefix: '#include <unistd.h>" to cc.has_function() When checking for execvpe, include <unistd.h> --- meson.build | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/meson.build b/meson.build index 7ce9d901..2bec645e 100644 --- a/meson.build +++ b/meson.build @@ -13,12 +13,15 @@ is_debug_build = get_option('buildtype').startswith('debug') cc = meson.get_compiler('c') if cc.has_function('memfd_create', - args: ['-D_GNU_SOURCE'], prefix: '#include <sys/mman.h>') + args: ['-D_GNU_SOURCE'], + prefix: '#include <sys/mman.h>') add_project_arguments('-DMEMFD_CREATE', language: 'c') endif # Missing on DragonFly, FreeBSD < 14.1 -if cc.has_function('execvpe') +if cc.has_function('execvpe', + args: ['-D_GNU_SOURCE'], + prefix: '#include <unistd.h>') add_project_arguments('-DEXECVPE', language: 'c') endif From 7ec9ca2b9528126bf11dda37cfea9687b6bc66a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 2 Aug 2024 17:12:12 +0200 Subject: [PATCH 0858/1323] input: CSD buttons are now triggered when releasing the mouse button This is how most UIs work. Note that we (at least on River) don't get any surface enter/leave events while a button is held. This means we can't detect if the user pressed the mouse button while on a CSD button, but then moves the mouse outside. Releasing the mouse button will still activate the CSD button. Closes #1787 --- CHANGELOG.md | 5 +++++ input.c | 17 ++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 258940d4..6d7d6614 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,11 @@ ### Changed + +* CSD buttons now activate on mouse button **release**, rather than + press ([#1787][1787]). + + ### Deprecated ### Removed ### Fixed diff --git a/input.c b/input.c index 98f5c89c..4aa410c4 100644 --- a/input.c +++ b/input.c @@ -2577,12 +2577,19 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, } case TERM_SURF_BUTTON_MINIMIZE: - if (button == BTN_LEFT && state == WL_POINTER_BUTTON_STATE_PRESSED) + if (button == BTN_LEFT && + term->active_surface == TERM_SURF_BUTTON_MINIMIZE && + state == WL_POINTER_BUTTON_STATE_RELEASED) + { xdg_toplevel_set_minimized(term->window->xdg_toplevel); + } break; case TERM_SURF_BUTTON_MAXIMIZE: - if (button == BTN_LEFT && state == WL_POINTER_BUTTON_STATE_PRESSED) { + if (button == BTN_LEFT && + term->active_surface == TERM_SURF_BUTTON_MAXIMIZE && + state == WL_POINTER_BUTTON_STATE_RELEASED) + { if (term->window->is_maximized) xdg_toplevel_unset_maximized(term->window->xdg_toplevel); else @@ -2591,8 +2598,12 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, break; case TERM_SURF_BUTTON_CLOSE: - if (button == BTN_LEFT && state == WL_POINTER_BUTTON_STATE_PRESSED) + if (button == BTN_LEFT && + term->active_surface == TERM_SURF_BUTTON_CLOSE && + state == WL_POINTER_BUTTON_STATE_RELEASED) + { term_shutdown(term); + } break; case TERM_SURF_GRID: { From bef613e656a36400577cde295b09267c80f3adce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 9 Aug 2024 08:20:59 +0200 Subject: [PATCH 0859/1323] csd: track pointer when rendering and handling CSD button clicks * Render button as highlighted only when pointer is above them * Releasing the mouse button while *not* on the button does *not* activate the button When pressing, and holding, a mouse button, the compositor keeps sending motion events for the surface where the button was pressed, even if the mouse has moved outside it. We also don't get any surface leave/enter events. This meant that the button was rendered as highlighted, and a click registered, if the user pressed the mouse button while on the button, and then moved the cursor away from the button before releasing the button. This patch fixes it, by checking if the current cursor coordinates fall within the button surface. --- input.c | 75 +++++++++++++++++++++++++++++++++++++++++++++++++------- render.c | 35 +++++++++++++++++++++++--- 2 files changed, 98 insertions(+), 12 deletions(-) diff --git a/input.c b/input.c index 4aa410c4..201f5c69 100644 --- a/input.c +++ b/input.c @@ -2053,6 +2053,25 @@ wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, } } +static bool +pointer_is_on_button(const struct terminal *term, const struct seat *seat, + enum csd_surface csd_surface) +{ + if (seat->mouse.x < 0) + return false; + if (seat->mouse.y < 0) + return false; + + struct csd_data info = get_csd_data(term, csd_surface); + if (seat->mouse.x > info.width) + return false; + + if (seat->mouse.y > info.height) + return false; + + return true; +} + static void wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) @@ -2085,16 +2104,42 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, int x = wl_fixed_to_int(surface_x) * term->scale; int y = wl_fixed_to_int(surface_y) * term->scale; + enum term_surface surf_kind = term->active_surface; + int button = 0; + bool send_to_client = false; + bool is_on_button = false; + + /* If current surface is a button, check if pointer was on it + *before* the motion event */ + switch (surf_kind) { + case TERM_SURF_BUTTON_MINIMIZE: + is_on_button = pointer_is_on_button(term, seat, CSD_SURF_MINIMIZE); + break; + + case TERM_SURF_BUTTON_MAXIMIZE: + is_on_button = pointer_is_on_button(term, seat, CSD_SURF_MAXIMIZE); + break; + + case TERM_SURF_BUTTON_CLOSE: + is_on_button = pointer_is_on_button(term, seat, CSD_SURF_CLOSE); + break; + + case TERM_SURF_NONE: + case TERM_SURF_GRID: + case TERM_SURF_TITLE: + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: + break; + } + seat->pointer.hidden = false; seat->mouse.x = x; seat->mouse.y = y; term_xcursor_update_for_seat(term, seat); - enum term_surface surf_kind = term->active_surface; - int button = 0; - bool send_to_client = false; - if (tll_length(seat->mouse.buttons) > 0) { const struct button_tracker *tracker = &tll_front(seat->mouse.buttons); surf_kind = tracker->surf_kind; @@ -2104,9 +2149,21 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, switch (surf_kind) { case TERM_SURF_NONE: + break; + case TERM_SURF_BUTTON_MINIMIZE: + if (pointer_is_on_button(term, seat, CSD_SURF_MINIMIZE) != is_on_button) + render_refresh_csd(term); + break; + case TERM_SURF_BUTTON_MAXIMIZE: + if (pointer_is_on_button(term, seat, CSD_SURF_MAXIMIZE) != is_on_button) + render_refresh_csd(term); + break; + case TERM_SURF_BUTTON_CLOSE: + if (pointer_is_on_button(term, seat, CSD_SURF_CLOSE) != is_on_button) + render_refresh_csd(term); break; case TERM_SURF_TITLE: @@ -2246,8 +2303,8 @@ fdm_csd_move(struct fdm *fdm, int fd, int events, void *data) } static const struct key_binding * - match_mouse_binding(const struct seat *seat, const struct terminal *term, - int button) +match_mouse_binding(const struct seat *seat, const struct terminal *term, + int button) { if (seat->wl_keyboard != NULL && seat->kbd.xkb_state != NULL) { /* Seat has keyboard - use mouse bindings *with* modifiers */ @@ -2578,7 +2635,7 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, case TERM_SURF_BUTTON_MINIMIZE: if (button == BTN_LEFT && - term->active_surface == TERM_SURF_BUTTON_MINIMIZE && + pointer_is_on_button(term, seat, CSD_SURF_MINIMIZE) && state == WL_POINTER_BUTTON_STATE_RELEASED) { xdg_toplevel_set_minimized(term->window->xdg_toplevel); @@ -2587,7 +2644,7 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, case TERM_SURF_BUTTON_MAXIMIZE: if (button == BTN_LEFT && - term->active_surface == TERM_SURF_BUTTON_MAXIMIZE && + pointer_is_on_button(term, seat, CSD_SURF_MAXIMIZE) && state == WL_POINTER_BUTTON_STATE_RELEASED) { if (term->window->is_maximized) @@ -2599,7 +2656,7 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, case TERM_SURF_BUTTON_CLOSE: if (button == BTN_LEFT && - term->active_surface == TERM_SURF_BUTTON_CLOSE && + pointer_is_on_button(term, seat, CSD_SURF_CLOSE) && state == WL_POINTER_BUTTON_STATE_RELEASED) { term_shutdown(term); diff --git a/render.c b/render.c index 279e51bf..d9630983 100644 --- a/render.c +++ b/render.c @@ -2627,6 +2627,32 @@ render_csd_button_close(struct terminal *term, struct buffer *buf) pixman_image_unref(src); } +static bool +any_pointer_is_on_button(const struct terminal *term, enum csd_surface csd_surface) +{ + if (unlikely(tll_length(term->wl->seats) == 0)) + return false; + + tll_foreach(term->wl->seats, it) { + const struct seat *seat = &it->item; + + if (seat->mouse.x < 0) + continue; + if (seat->mouse.y < 0) + continue; + + struct csd_data info = get_csd_data(term, csd_surface); + if (seat->mouse.x > info.width) + continue; + + if (seat->mouse.y > info.height) + continue; + return true; + } + + return false; +} + static void render_csd_button(struct terminal *term, enum csd_surface surf_idx, const struct csd_data *info, struct buffer *buf) @@ -2650,21 +2676,24 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx, _color = term->conf->colors.table[4]; /* blue */ is_set = term->conf->csd.color.minimize_set; conf_color = &term->conf->csd.color.minimize; - is_active = term->active_surface == TERM_SURF_BUTTON_MINIMIZE; + is_active = term->active_surface == TERM_SURF_BUTTON_MINIMIZE && + any_pointer_is_on_button(term, CSD_SURF_MINIMIZE); break; case CSD_SURF_MAXIMIZE: _color = term->conf->colors.table[2]; /* green */ is_set = term->conf->csd.color.maximize_set; conf_color = &term->conf->csd.color.maximize; - is_active = term->active_surface == TERM_SURF_BUTTON_MAXIMIZE; + is_active = term->active_surface == TERM_SURF_BUTTON_MAXIMIZE && + any_pointer_is_on_button(term, CSD_SURF_MAXIMIZE); break; case CSD_SURF_CLOSE: _color = term->conf->colors.table[1]; /* red */ is_set = term->conf->csd.color.close_set; conf_color = &term->conf->csd.color.quit; - is_active = term->active_surface == TERM_SURF_BUTTON_CLOSE; + is_active = term->active_surface == TERM_SURF_BUTTON_CLOSE && + any_pointer_is_on_button(term, CSD_SURF_CLOSE); break; default: From 481ce82d661e1f3f4a05813bb209f2184b1f61c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 9 Aug 2024 08:24:53 +0200 Subject: [PATCH 0860/1323] doc: foot.ini: document the Unicode ranges covered by the builtin glyphs --- doc/foot.ini.5.scd | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 71992c8f..da887aee 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -179,9 +179,10 @@ empty string to be set, but it must be quoted: *KEY=""*) Default: _unset_ -*box-drawings-uses-font-glyphs* Boolean. When disabled, foot generates - box/line drawing characters itself. The are several advantages to - doing this instead of using font glyphs: +*box-drawings-uses-font-glyphs* + Boolean. When disabled, foot generates box/line drawing characters + itself. The are several advantages to doing this instead of using + font glyphs: - No antialiasing effects where e.g. line endpoints appear dimmed down, or blurred. @@ -195,6 +196,13 @@ empty string to be set, but it must be quoted: *KEY=""*) When enabled, box/line drawing characters are rendered using font glyphs. This may result in a more uniform look, in some use cases. + When disabled, foot will render the following Unicode codepoints + by itself: + + - U+02500 - U+0259F + - U+02800 - U+028FF + - U+1Fb00 - U+1FB9B + Default: _no_. *dpi-aware* From ee9c76ded00de9beac698d2bd4771e8c1d966c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 9 Aug 2024 08:25:36 +0200 Subject: [PATCH 0861/1323] osc: kitty: update 's' to the latest spec * The spec now defines a couple of "standard" names. Translate these to the freedesktop compliant names. * The query response no longer contains 'xdg-names', but instead list the supported standard names. --- osc.c | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/osc.c b/osc.c index eba1850f..53597d0f 100644 --- a/osc.c +++ b/osc.c @@ -740,7 +740,7 @@ kitty_notification(struct terminal *term, char *string) char reply[128]; int n = xsnprintf( reply, sizeof(reply), - "\033]99;i=%s:p=?;p=%s:a=%s:o=%s:u=%s:c=1:w=1:s=silent,xdg-names%s", + "\033]99;i=%s:p=?;p=%s:a=%s:o=%s:u=%s:c=1:w=1:s=system,silent,error,warn,warning,info,question%s", reply_id, p_caps, a_caps, when_caps, u_caps, terminator); xassert(n < sizeof(reply)); @@ -817,6 +817,22 @@ kitty_notification(struct terminal *term, char *string) if (decoded != NULL) { free(sound_name); sound_name = decoded; + + const char *translated_name = NULL; + + if (streq(decoded, "error")) + translated_name = "dialog-error"; + else if (streq(decoded, "warn") || streq(decoded, "warning")) + translated_name = "dialog-warning"; + else if (streq(decoded, "info")) + translated_name = "dialog-information"; + else if (streq(decoded, "question")) + translated_name = "dialog-question"; + + if (translated_name != NULL) { + free(sound_name); + sound_name = xstrdup(translated_name); + } } break; } From 7e1894978fda263f823abbb1e66ea251e96bfd83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 9 Aug 2024 13:55:21 +0200 Subject: [PATCH 0862/1323] slave: prefix user notifications with 'foot' To make it clearer _who_ is emitting the warning/errors --- slave.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/slave.c b/slave.c index bf1ca8e0..47e59e87 100644 --- a/slave.c +++ b/slave.c @@ -158,6 +158,7 @@ emit_one_notification(int fd, const struct user_notification *notif) xassert(prefix != NULL); if (write(fd, prefix, strlen(prefix)) < 0 || + write(fd, "foot: ", 6) < 0 || write(fd, notif->text, strlen(notif->text)) < 0 || write(fd, postfix, strlen(postfix)) < 0) { @@ -180,7 +181,8 @@ emit_one_notification(int fd, const struct user_notification *notif) } static bool -emit_notifications_of_kind(int fd, const user_notifications_t *notifications, +emit_notifications_of_kind(int fd, + const user_notifications_t *notifications, enum user_notification_kind kind) { tll_foreach(*notifications, it) { From 6d351ffc43175b59359258eca83e652d76cfc426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 10 Aug 2024 16:38:35 +0200 Subject: [PATCH 0863/1323] main: invalid locale != non-UTF-8 locale When doing locale fallback, and printing user notifications and log warnings, better separate the case "locale is invalid" from "locale is valid but not UTF-8". Closes #1798 --- main.c | 53 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/main.c b/main.c index f46d712e..b2266d88 100644 --- a/main.c +++ b/main.c @@ -425,19 +425,18 @@ main(int argc, char *const *argv) * that does not exist on this system, then the above call may return * NULL. We should just continue with the fallback method below. */ - LOG_WARN("setlocale() failed"); - locale = "C"; + LOG_ERR("setlocale() failed"); } - LOG_INFO("locale: %s", locale); + LOG_INFO("locale: %s", locale != NULL ? locale : "<invalid>"); - bool bad_locale = !locale_is_utf8(); + bool bad_locale = locale == NULL || !locale_is_utf8(); if (bad_locale) { static const char fallback_locales[][12] = { "C.UTF-8", "en_US.UTF-8", }; - char *saved_locale = xstrdup(locale); + char *saved_locale = locale != NULL ? xstrdup(locale) : NULL; /* * Try to force an UTF-8 locale. If we succeed, launch the @@ -448,13 +447,23 @@ main(int argc, char *const *argv) const char *const fallback_locale = fallback_locales[i]; if (setlocale(LC_CTYPE, fallback_locale) != NULL) { - LOG_WARN("'%s' is not a UTF-8 locale, using '%s' instead", - saved_locale, fallback_locale); + if (saved_locale != NULL) { + LOG_WARN( + "'%s' is not a UTF-8 locale, falling back to '%s'", + saved_locale, fallback_locale); - user_notification_add_fmt( - &user_notifications, USER_NOTIFICATION_WARNING, - "'%s' is not a UTF-8 locale, using '%s' instead", - saved_locale, fallback_locale); + user_notification_add_fmt( + &user_notifications, USER_NOTIFICATION_WARNING, + "'%s' is not a UTF-8 locale, falling back to '%s'", + saved_locale, fallback_locale); + + } else { + LOG_WARN( + "invalid locale, falling back to '%s'", fallback_locale); + user_notification_add_fmt( + &user_notifications, USER_NOTIFICATION_WARNING, + "invalid locale, falling back to '%s'", fallback_locale); + } bad_locale = false; break; @@ -462,14 +471,22 @@ main(int argc, char *const *argv) } if (bad_locale) { - LOG_ERR( - "'%s' is not a UTF-8 locale, and failed to find a fallback", - saved_locale); + if (saved_locale != NULL) { + LOG_ERR( + "'%s' is not a UTF-8 locale, and failed to find a fallback", + saved_locale); - user_notification_add_fmt( - &user_notifications, USER_NOTIFICATION_ERROR, - "'%s' is not a UTF-8 locale, and failed to find a fallback", - saved_locale); + user_notification_add_fmt( + &user_notifications, USER_NOTIFICATION_ERROR, + "'%s' is not a UTF-8 locale, and failed to find a fallback", + saved_locale); + } else { + LOG_ERR("invalid locale, and failed to find a fallback"); + + user_notification_add_fmt( + &user_notifications, USER_NOTIFICATION_ERROR, + "invalid locale, and failed to find a fallback"); + } } free(saved_locale); } From cee0c5423ae1b306fc85960058adfd9574e4c91b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 11 Aug 2024 11:11:38 +0200 Subject: [PATCH 0864/1323] main: spell out the most common reason for setlocale() to fail --- main.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main.c b/main.c index b2266d88..15741012 100644 --- a/main.c +++ b/main.c @@ -425,7 +425,8 @@ main(int argc, char *const *argv) * that does not exist on this system, then the above call may return * NULL. We should just continue with the fallback method below. */ - LOG_ERR("setlocale() failed"); + LOG_ERR("setlocale() failed. The most common cause is that the " + "configured locale is not available, or has been misspelled"); } LOG_INFO("locale: %s", locale != NULL ? locale : "<invalid>"); From eb185bfa47bad124149d8f921825231141d10b20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 14 Aug 2024 08:53:21 +0200 Subject: [PATCH 0865/1323] utils/xtgettcap: fix possible NULL deref, found by -fanalyzer --- utils/xtgettcap.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/utils/xtgettcap.c b/utils/xtgettcap.c index 069a9ecb..82ee0085 100644 --- a/utils/xtgettcap.c +++ b/utils/xtgettcap.c @@ -141,6 +141,9 @@ main(int argc, const char *const *argv) const char *key = strtok(key_value, "="); const char *value = strtok(NULL, "="); + if (key == NULL) + continue; + #if 0 assert((success && value != NULL) || (!success && value == NULL)); From 2896c1898127256193d49c80a56ba7188fad0e33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 14 Aug 2024 08:50:44 +0200 Subject: [PATCH 0866/1323] osc: regression: fix OSC-111 handling of alpha changes When the background alpha changes from fully opaque, to transparent, or vice versa, we need to do more than just repaint the affected cells. For example, we need to create new surfaces with the correct pixel format. OSC-11 (set background color) already does this, but the same alpha checking logic was missing in OSC-111 (reset background color). Fixes #1801 --- CHANGELOG.md | 7 +++++++ osc.c | 11 ++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d7d6614..fe82d2ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,13 @@ ### Deprecated ### Removed ### Fixed + +* Regression: OSC-111 not handling alpha changes correctly, causing + visual glitches ([#1801][1801]). + +[1801]: https://codeberg.org/dnkl/foot/issues/1801 + + ### Security ### Contributors diff --git a/osc.c b/osc.c index 53597d0f..3c5f7616 100644 --- a/osc.c +++ b/osc.c @@ -1381,13 +1381,22 @@ osc_dispatch(struct terminal *term) term_damage_color(term, COLOR_DEFAULT, 0); break; - case 111: /* Reset default text background color */ + case 111: { /* Reset default text background color */ LOG_DBG("resetting background color"); + bool alpha_changed = term->colors.alpha != term->conf->colors.alpha; + term->colors.bg = term->conf->colors.bg; term->colors.alpha = term->conf->colors.alpha; + + if (alpha_changed) { + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + } + term_damage_color(term, COLOR_DEFAULT, 0); term_damage_margins(term); break; + } case 112: LOG_DBG("resetting cursor color"); From 447b02b530e62ff901539131e9c15d4ef6c592cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 14 Aug 2024 12:00:08 +0200 Subject: [PATCH 0867/1323] changelog: prepare for 1.18.1 --- CHANGELOG.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe82d2ff..da697d66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.18.1](#1-18-1) * [1.18.0](#1-18-0) * [1.17.2](#1-17-2) * [1.17.1](#1-17-1) @@ -53,7 +53,8 @@ * [1.2.0](#1-2-0) -## Unreleased +## 1.18.1 + ### Added * OSC-99: support for the `s` parameter. Supported keywords are @@ -69,8 +70,6 @@ press ([#1787][1787]). -### Deprecated -### Removed ### Fixed * Regression: OSC-111 not handling alpha changes correctly, causing @@ -79,9 +78,11 @@ [1801]: https://codeberg.org/dnkl/foot/issues/1801 -### Security ### Contributors +* Craig Barnes +* Shogo Yamazaki + ## 1.18.0 From ce9c9f6be6bc0226741b5e114167a53e135ae885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 14 Aug 2024 12:00:20 +0200 Subject: [PATCH 0868/1323] meson: bump version to 1.18.1 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 2bec645e..2fd1cb4a 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.18.0', + version: '1.18.1', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From dc5ff7db2871de59903c03d4a7b981519918e040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 14 Aug 2024 12:02:56 +0200 Subject: [PATCH 0869/1323] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index da697d66..625ca4dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.18.1](#1-18-1) * [1.18.0](#1-18-0) * [1.17.2](#1-17-2) @@ -53,6 +54,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.18.1 ### Added From 96c30cd410ddd36bbb583caca44d4834851408da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 15 Aug 2024 17:20:12 +0200 Subject: [PATCH 0870/1323] term: thrd_join() returns thrd_success on success, not 0 Closes #1812 --- terminal.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal.c b/terminal.c index fe97e958..e7812e2e 100644 --- a/terminal.c +++ b/terminal.c @@ -1048,7 +1048,7 @@ reload_fonts(struct terminal *term, bool resize_grid) for (size_t i = 0; i < 4; i++) { if (tids[i] != 0) { int ret; - if (thrd_join(tids[i], &ret) != 0) + if (thrd_join(tids[i], &ret) != thrd_success) success = false; else success = success && ret; From a2fc2a986eedc5ac7c83ae21618acedcb7b7aab8 Mon Sep 17 00:00:00 2001 From: tokyo4j <hrak1529@gmail.com> Date: Thu, 15 Aug 2024 01:18:57 +0900 Subject: [PATCH 0871/1323] render: follow cursor.unfocused-style regardless of cursor.style Before this commit, cursor.unfocused-style was effective only with cursor.style=block --- CHANGELOG.md | 5 +++++ render.c | 28 ++++++++++++++-------------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 625ca4dd..00cf0b63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,11 @@ ## Unreleased ### Added ### Changed + +* `cursor.unfocused-style` is now effective even when `cursor.style` + is not `block`. + + ### Deprecated ### Removed ### Fixed diff --git a/render.c b/render.c index d9630983..d9317b68 100644 --- a/render.c +++ b/render.c @@ -591,22 +591,22 @@ draw_cursor(const struct terminal *term, const struct cell *cell, pixman_color_t text_color; cursor_colors_for_cell(term, cell, fg, bg, &cursor_color, &text_color); + if (unlikely(!term->kbd_focus)) { + switch (term->conf->cursor.unfocused_style) { + case CURSOR_UNFOCUSED_UNCHANGED: + break; + + case CURSOR_UNFOCUSED_HOLLOW: + draw_hollow_block(term, pix, &cursor_color, x, y, cols); + return; + + case CURSOR_UNFOCUSED_NONE: + return; + } + } + switch (term->cursor_style) { case CURSOR_BLOCK: - if (unlikely(!term->kbd_focus)) { - switch (term->conf->cursor.unfocused_style) { - case CURSOR_UNFOCUSED_UNCHANGED: - break; - - case CURSOR_UNFOCUSED_HOLLOW: - draw_hollow_block(term, pix, &cursor_color, x, y, cols); - return; - - case CURSOR_UNFOCUSED_NONE: - return; - } - } - if (likely(term->cursor_blink.state == CURSOR_BLINK_ON) || !term->kbd_focus) { From 1969717527a956483ba1c37aab3fcf7e8560e26a Mon Sep 17 00:00:00 2001 From: "Andrew J. Hesford" <ajh@sideband.org> Date: Wed, 14 Aug 2024 10:35:58 -0400 Subject: [PATCH 0872/1323] feature: add resize-keep-grid to allow text reflow on font changes --- config.c | 4 ++++ config.h | 1 + doc/foot.ini.5.scd | 12 ++++++++++++ foot.ini | 1 + terminal.c | 6 +++++- 5 files changed, 23 insertions(+), 1 deletion(-) diff --git a/config.c b/config.c index 1ec73da0..a80a26b4 100644 --- a/config.c +++ b/config.c @@ -931,6 +931,9 @@ parse_section_main(struct context *ctx) else if (streq(key, "resize-by-cells")) return value_to_bool(ctx, &conf->resize_by_cells); + else if (streq(key, "resize-keep-grid")) + return value_to_bool(ctx, &conf->resize_keep_grid); + else if (streq(key, "bold-text-in-bright")) { if (streq(value, "palette-based")) { conf->bold_in_bright.enabled = true; @@ -3101,6 +3104,7 @@ config_load(struct config *conf, const char *conf_path, .pad_x = 0, .pad_y = 0, .resize_by_cells = true, + .resize_keep_grid = true, .resize_delay_ms = 100, .bold_in_bright = { .enabled = false, diff --git a/config.h b/config.h index 246b479f..be99987b 100644 --- a/config.h +++ b/config.h @@ -140,6 +140,7 @@ struct config { bool center; bool resize_by_cells; + bool resize_keep_grid; uint16_t resize_delay_ms; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index da887aee..47c5939e 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -287,6 +287,18 @@ empty string to be set, but it must be quoted: *KEY=""*) Default: _yes_ +*resize-keep-grid* + Boolean. + + When set to *yes*, the window size will be adjusted with changes in font + size to preserve the dimensions of the text grid. When set to *no*, the + window size will remain constant and the text grid will be adjusted as + necessary to fit the window. + + This option only applies to floating windows. + + Default: _yes_ + *initial-window-size-pixels* Initial window width and height in _pixels_ (subject to output scaling), in the form _WIDTHxHEIGHT_. The height _includes_ the diff --git a/foot.ini b/foot.ini index 9e2f5f29..c2c80fa3 100644 --- a/foot.ini +++ b/foot.ini @@ -27,6 +27,7 @@ # initial-window-mode=windowed # pad=0x0 # optionally append 'center' # resize-by-cells=yes +# resize-keep-grid=yes # resize-delay-ms=100 # bold-text-in-bright=no diff --git a/terminal.c b/terminal.c index e7812e2e..dee0b038 100644 --- a/terminal.c +++ b/terminal.c @@ -819,11 +819,15 @@ term_set_fonts(struct terminal *term, struct fcft_font *fonts[static 4], * render_resize() after this function */ if (resize_grid) { /* Use force, since cell-width/height may have changed */ + enum resize_options resize_opts = RESIZE_FORCE; + if (conf->resize_keep_grid) + resize_opts |= RESIZE_KEEP_GRID; + render_resize( term, (int)roundf(term->width / term->scale), (int)roundf(term->height / term->scale), - RESIZE_FORCE | RESIZE_KEEP_GRID); + resize_opts); } return true; } From b3fd994fd35cd6eba776200b309ff124fc23abcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 20 Aug 2024 07:12:09 +0200 Subject: [PATCH 0873/1323] changelog: add missing reference to #1787 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00cf0b63..9881b4a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,8 @@ * CSD buttons now activate on mouse button **release**, rather than press ([#1787][1787]). +[1787]: https://codeberg.org/dnkl/foot/issues/1787 + ### Fixed From be13788a4fdf5275cd7788cc2ef7297d4f7fdb81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 20 Aug 2024 07:12:19 +0200 Subject: [PATCH 0874/1323] changelog: resize-keep-grid --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9881b4a1..8043d9b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,14 @@ ## Unreleased ### Added + +* `resize-keep-grid` option, controlling whether the window is resized + (and the grid reflowed) or not when e.g. zooming in/out + ([#1807][1807]). + +[1807]: https://codeberg.org/dnkl/foot/issues/1807 + + ### Changed * `cursor.unfocused-style` is now effective even when `cursor.style` From 7dd204fd319222c1e500b2c117b4ed62f9e642af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 20 Aug 2024 07:14:53 +0200 Subject: [PATCH 0875/1323] osc: notify: fix bad check for invalid UTF-8 mbsntoc32() returns (size_t)-1 on failure, not (char32_t)-1. --- osc.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osc.c b/osc.c index 3c5f7616..aeb7896c 100644 --- a/osc.c +++ b/osc.c @@ -547,12 +547,12 @@ osc_notify(struct terminal *term, char *string) if (title == NULL) return; - if (mbsntoc32(NULL, title, strlen(title), 0) == (char32_t)-1) { + if (mbsntoc32(NULL, title, strlen(title), 0) == (size_t)-1) { LOG_WARN("%s: notification title is not valid UTF-8, ignoring", title); return; } - if (msg != NULL && mbsntoc32(NULL, msg, strlen(msg), 0) == (char32_t)-1) { + if (msg != NULL && mbsntoc32(NULL, msg, strlen(msg), 0) == (size_t)-1) { LOG_WARN("%s: notification message is not valid UTF-8, ignoring", msg); return; } From 8f9f3dbd9d8cbcc816584c548dff03add3f022e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 20 Aug 2024 07:15:22 +0200 Subject: [PATCH 0876/1323] term_set_window_title(): fix bad check for invalid UTF-8 mbsntoc32() returns (size_t)-1 on failure, not (char32_t)-1. --- terminal.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal.c b/terminal.c index dee0b038..ecf21040 100644 --- a/terminal.c +++ b/terminal.c @@ -3528,7 +3528,7 @@ term_set_window_title(struct terminal *term, const char *title) if (term->window_title != NULL && streq(term->window_title, title)) return; - if (mbsntoc32(NULL, title, strlen(title), 0) == (char32_t)-1) { + if (mbsntoc32(NULL, title, strlen(title), 0) == (size_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); From 01fa59b6b71cc756366905e6ef99cfd42944a292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 20 Aug 2024 07:17:58 +0200 Subject: [PATCH 0877/1323] changelog: mbsntoc32() failure checks --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8043d9b5..c8f5946c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,12 @@ ### Deprecated ### Removed ### Fixed + +* Some invalid UTF-8 strings passing the validity check when setting + the window title, triggering a Wayland protocol error which then + caused foot to shutdown. + + ### Security ### Contributors From b47a4dd2553b7f4db5a385b15bc7cb559c9932db Mon Sep 17 00:00:00 2001 From: Oleh Hushchenkov <o.hushchenkov@gmail.com> Date: Sun, 25 Aug 2024 11:28:21 +0300 Subject: [PATCH 0878/1323] add setting for strikeout thickness --- config.c | 4 ++++ config.h | 2 ++ doc/foot.ini.5.scd | 12 ++++++++++++ foot.ini | 1 + render.c | 14 ++++++++++++-- tests/test-config.c | 1 + 6 files changed, 32 insertions(+), 2 deletions(-) diff --git a/config.c b/config.c index a80a26b4..bda7648b 100644 --- a/config.c +++ b/config.c @@ -1023,6 +1023,9 @@ parse_section_main(struct context *ctx) else if (streq(key, "underline-thickness")) return value_to_pt_or_px(ctx, &conf->underline_thickness); + else if (streq(key, "strikeout-thickness")) + return value_to_pt_or_px(ctx, &conf->strikeout_thickness); + else if (streq(key, "dpi-aware")) return value_to_bool(ctx, &conf->dpi_aware); @@ -3121,6 +3124,7 @@ config_load(struct config *conf, const char *conf_path, .use_custom_underline_offset = false, .box_drawings_uses_font_glyphs = false, .underline_thickness = {.pt = 0., .px = -1}, + .strikeout_thickness = {.pt = 0., .px = -1}, .dpi_aware = false, .bell = { .urgent = false, diff --git a/config.h b/config.h index be99987b..9e983011 100644 --- a/config.h +++ b/config.h @@ -168,6 +168,8 @@ struct config { struct pt_or_px underline_offset; struct pt_or_px underline_thickness; + struct pt_or_px strikeout_thickness; + bool box_drawings_uses_font_glyphs; bool can_shape_grapheme; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 47c5939e..908eb25f 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -179,6 +179,18 @@ empty string to be set, but it must be quoted: *KEY=""*) Default: _unset_ +*strikeout-thickness* + Use a custom thickness (height) for strikeouts. The thickness is, by + default, in _points_. + + To specify a thickness in _pixels_, append *px*: + *strikeout-thickness=1px*. + + If left unset (the default), the thickness specified in the font is + used. + + Default: _unset_ + *box-drawings-uses-font-glyphs* Boolean. When disabled, foot generates box/line drawing characters itself. The are several advantages to doing this instead of using diff --git a/foot.ini b/foot.ini index c2c80fa3..6ad2085f 100644 --- a/foot.ini +++ b/foot.ini @@ -19,6 +19,7 @@ # vertical-letter-offset=0 # underline-offset=<font metrics> # underline-thickness=<font underline thickness> +# strikeout-thickness=<font strikeout thickness> # box-drawings-uses-font-glyphs=no # dpi-aware=no diff --git a/render.c b/render.c index d9317b68..98bc0b69 100644 --- a/render.c +++ b/render.c @@ -544,11 +544,21 @@ draw_strikeout(const struct terminal *term, pixman_image_t *pix, const struct fcft_font *font, const pixman_color_t *color, int x, int y, int cols) { + const int thickness = term->conf->strikeout_thickness.px >= 0 + ? term_pt_or_px_as_pixels( + term, &term->conf->strikeout_thickness) + : font->strikeout.thickness; + + /* Try to center custom strikeout */ + const int position = term->conf->strikeout_thickness.px >= 0 + ? font->strikeout.position - round(font->strikeout.thickness / 2.) + round(thickness / 2.) + : font->strikeout.position; + pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, color, 1, &(pixman_rectangle16_t){ - x, y + term->font_baseline - font->strikeout.position, - cols * term->cell_width, font->strikeout.thickness}); + x, y + term->font_baseline - position, + cols * term->cell_width, thickness}); } static void diff --git a/tests/test-config.c b/tests/test-config.c index a41e8536..61999686 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -520,6 +520,7 @@ test_section_main(void) test_pt_or_px(&ctx, &parse_section_main, "horizontal-letter-offset", &conf.horizontal_letter_offset); test_pt_or_px(&ctx, &parse_section_main, "vertical-letter-offset", &conf.vertical_letter_offset); test_pt_or_px(&ctx, &parse_section_main, "underline-thickness", &conf.underline_thickness); + test_pt_or_px(&ctx, &parse_section_main, "strikeout-thickness", &conf.strikeout_thickness); test_uint16(&ctx, &parse_section_main, "resize-delay-ms", &conf.resize_delay_ms); test_uint16(&ctx, &parse_section_main, "workers", &conf.render_worker_count); From c15ebbfa2eeba7f10d6d86ce344337780cf00b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 26 Aug 2024 19:36:26 +0200 Subject: [PATCH 0879/1323] changelog: strikeout-thickness --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8f5946c..1a2b7aa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ * `resize-keep-grid` option, controlling whether the window is resized (and the grid reflowed) or not when e.g. zooming in/out ([#1807][1807]). +* `strikeout-thickness` option. [1807]: https://codeberg.org/dnkl/foot/issues/1807 From c5bb1fb2ed33c06078bfec5fb2f487f6406254d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 5 Sep 2024 07:13:53 +0200 Subject: [PATCH 0880/1323] notifications: BEL and OSC-777 now focuses the window on notification activation Or put more propertly; if the notification daemon, and the notification helper used by foot has been configured properly (i.e. they both support XDG activation tokens), notifications generated by BEL and OSC-777 will now raise/focus the window when the default action of the notification is activated - typically by clicking the notification. Closes #1822 --- CHANGELOG.md | 8 ++++++++ osc.c | 5 +++-- terminal.c | 5 +++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a2b7aa1..83613505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,14 @@ * `cursor.unfocused-style` is now effective even when `cursor.style` is not `block`. +* Activating a notification triggered with OSC-777, or BEL, now + focuses the foot window, if XDG activation tokens are supported by + the compositor, the notification daemon, and the notification helper + used by foot (i.e. `desktop-notifications.command`). This has been + supported for OSC-99 since 1.18.0, and now we also support it for + BEL and OSC-777 ([#1822][1822]). + +[1822]: https://codeberg.org/dnkl/foot/issues/1822 ### Deprecated diff --git a/osc.c b/osc.c index aeb7896c..63b66dd7 100644 --- a/osc.c +++ b/osc.c @@ -558,9 +558,10 @@ osc_notify(struct terminal *term, char *string) } notify_notify(term, &(struct notification){ - .title = (char *)title, - .body = (char *)msg, + .title = xstrdup(title), + .body = xstrdup(msg), .expire_time = -1, + .focus = true, }); } diff --git a/terminal.c b/terminal.c index ecf21040..7e490418 100644 --- a/terminal.c +++ b/terminal.c @@ -3597,9 +3597,10 @@ term_bell(struct terminal *term) if (term->conf->bell.notify) { notify_notify(term, &(struct notification){ - .title = (char *)"Bell", - .body = (char *)"Bell in terminal", + .title = xstrdup("Bell"), + .body = xstrdup("Bell in terminal"), .expire_time = -1, + .focus = true, }); } From c41f55c3a0994f86aca4d6add49420a25e023ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 5 Sep 2024 07:17:03 +0200 Subject: [PATCH 0881/1323] sixel: default bg color is now taken from the sixel palette, not the ANSI bg color The wording in the original VT340 documentation is vague: Pixel positions specified as 0 are set to the current background color. What does that mean? We _thought_ it meant the current ANSI background color, as set with e.g. CSI 4x m. It's still all a bit vague, but seeing that we have separate palettes for text and graphic (should we?), it doesn't make sense to use the ANSI background color as the default sixel background color. So, use entry 0 from the sixel palette instead. --- CHANGELOG.md | 3 +++ sixel.c | 37 ++++--------------------------------- 2 files changed, 7 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83613505..3fc84d4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,9 @@ used by foot (i.e. `desktop-notifications.command`). This has been supported for OSC-99 since 1.18.0, and now we also support it for BEL and OSC-777 ([#1822][1822]). +* Sixel background color (when `P2=0|2`) is now set to the **sixel** + color palette entry #0, instead of using the current ANSI background + color. This is what a real VT340 does. [1822]: https://codeberg.org/dnkl/foot/issues/1822 diff --git a/sixel.c b/sixel.c index 161eaad5..20385a93 100644 --- a/sixel.c +++ b/sixel.c @@ -127,40 +127,11 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) term->sixel.palette = term->sixel.shared_palette; } - uint32_t bg = 0; + if (term->sixel.transparent_bg) + term->sixel.default_bg = 0x00000000u; + else + term->sixel.default_bg = term->sixel.palette[0]; - switch (term->vt.attrs.bg_src) { - case COLOR_RGB: - bg = 0xffu << 24 | term->vt.attrs.bg; - break; - - case COLOR_BASE16: - case COLOR_BASE256: - bg = 0xffu << 24 | term->colors.table[term->vt.attrs.bg]; - break; - - case COLOR_DEFAULT: - if (term->colors.alpha == 0xffff) - bg = 0xffu << 24 | term->colors.bg; - else { - /* Alpha needs to be pre-multiplied */ - uint32_t r = (term->colors.bg >> 16) & 0xff; - uint32_t g = (term->colors.bg >> 8) & 0xff; - uint32_t b = (term->colors.bg >> 0) & 0xff; - - uint32_t alpha = term->colors.alpha; - r *= alpha; r /= 0xffff; - g *= alpha; g /= 0xffff; - b *= alpha; b /= 0xffff; - - bg = (alpha >> 8) << 24 | (r & 0xff) << 16 | (g & 0xff) << 8 | (b & 0xff); - } - break; - } - - term->sixel.default_bg = term->sixel.transparent_bg - ? 0x00000000u - : bg; count = 0; return pan == 1 && pad == 1 ? &sixel_put_ar_11 : &sixel_put_generic; From a916a6a8ca06e56673eb4f3a806971c15f081d9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 8 Sep 2024 10:21:28 +0200 Subject: [PATCH 0882/1323] metainfo: add recent releases, update feature list --- org.codeberg.dnkl.foot.metainfo.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/org.codeberg.dnkl.foot.metainfo.xml b/org.codeberg.dnkl.foot.metainfo.xml index dcb71afd..1b7c46a7 100644 --- a/org.codeberg.dnkl.foot.metainfo.xml +++ b/org.codeberg.dnkl.foot.metainfo.xml @@ -22,6 +22,7 @@ <li>IME (via text-input-v3)</li> <li>Multi-seat</li> <li>True Color (24bpp)</li> + <li>Styled and colored underlines</li> <li>Synchronized Updates support</li> <li>Sixel image support</li> </ul> @@ -33,6 +34,11 @@ </screenshot> </screenshots> <releases> + <release version="1.18.1" date="2024-08-14"/> + <release version="1.18.0" date="2024-08-02"/> + <release version="1.17.2" date="2024-04-17"/> + <release version="1.17.1" date="2024-04-11"/> + <release version="1.17.0" date="2024-04-02"/> <release version="1.16.2" date="2023-10-17"/> <release version="1.16.1" date="2023-10-12"/> <release version="1.16.0" date="2023-10-11"/> From 11ff9ba7ece4d656bf050052fdd6b7d33f46d21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 8 Sep 2024 10:22:29 +0200 Subject: [PATCH 0883/1323] .desktop: remove StartupWMClass cludge --- org.codeberg.dnkl.foot.desktop | 1 - org.codeberg.dnkl.footclient.desktop | 1 - 2 files changed, 2 deletions(-) diff --git a/org.codeberg.dnkl.foot.desktop b/org.codeberg.dnkl.foot.desktop index 720d35a9..f072568d 100644 --- a/org.codeberg.dnkl.foot.desktop +++ b/org.codeberg.dnkl.foot.desktop @@ -9,4 +9,3 @@ Keywords=shell;prompt;command;commandline; Name=Foot GenericName=Terminal Comment=A wayland native terminal emulator -StartupWMClass=foot diff --git a/org.codeberg.dnkl.footclient.desktop b/org.codeberg.dnkl.footclient.desktop index b65790b4..f82f282b 100644 --- a/org.codeberg.dnkl.footclient.desktop +++ b/org.codeberg.dnkl.footclient.desktop @@ -9,4 +9,3 @@ Keywords=shell;prompt;command;commandline; Name=Foot Client GenericName=Terminal Comment=A wayland native terminal emulator (client) -StartupWMClass=footclient From c8185aec1d36df3aa60001a0b3a0fbd49d11df18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 8 Sep 2024 10:23:26 +0200 Subject: [PATCH 0884/1323] desktop: rename to foot{,client,-server}.desktop That is, skip the reverse DNS naming scheme suggested by the .desktop specification, and directly match our app-ids ("foot", and "footclient"). This simplifies .desktop -> window instance mapping, allowing DEs to match the filenames directly, without having to look at the StartupWMClass key in the .desktop files. These are the original names of the .desktop files. There were renamed (to use the reverse DNS syntax) to please the flathub people, who *required* this scheme to accept the foot package. But, since: * We don't package foot ourselves * We don't go out of our way to support non-distro packaging schemes * Flathub still hasn't merged the foot PR (it's now 2 years old) * There are no know issues in any known DE that prevents a non-reverse DNS .desktop filename from working * There are plenty of other applications that doesn't use reverse DNS names (a very clear majority, in my case) Let's just revert back to the simpler naming scheme. Closes #1607 --- CHANGELOG.md | 4 ++++ org.codeberg.dnkl.foot-server.desktop => foot-server.desktop | 0 org.codeberg.dnkl.foot.desktop => foot.desktop | 0 org.codeberg.dnkl.footclient.desktop => footclient.desktop | 0 meson.build | 2 +- 5 files changed, 5 insertions(+), 1 deletion(-) rename org.codeberg.dnkl.foot-server.desktop => foot-server.desktop (100%) rename org.codeberg.dnkl.foot.desktop => foot.desktop (100%) rename org.codeberg.dnkl.footclient.desktop => footclient.desktop (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fc84d4a..c155f362 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,8 +78,12 @@ * Sixel background color (when `P2=0|2`) is now set to the **sixel** color palette entry #0, instead of using the current ANSI background color. This is what a real VT340 does. +* The `.desktop` files no longer use the reverse DNS naming scheme, + and their names now match the default app-ids used by foot (`foot` + and `footclient`) ([#1607][1607]). [1822]: https://codeberg.org/dnkl/foot/issues/1822 +[1607]: https://codeberg.org/dnkl/foot/issues/1607 ### Deprecated diff --git a/org.codeberg.dnkl.foot-server.desktop b/foot-server.desktop similarity index 100% rename from org.codeberg.dnkl.foot-server.desktop rename to foot-server.desktop diff --git a/org.codeberg.dnkl.foot.desktop b/foot.desktop similarity index 100% rename from org.codeberg.dnkl.foot.desktop rename to foot.desktop diff --git a/org.codeberg.dnkl.footclient.desktop b/footclient.desktop similarity index 100% rename from org.codeberg.dnkl.footclient.desktop rename to footclient.desktop diff --git a/meson.build b/meson.build index 2fd1cb4a..c6e0e2ef 100644 --- a/meson.build +++ b/meson.build @@ -317,7 +317,7 @@ executable( install: true) install_data( - 'org.codeberg.dnkl.foot.desktop', 'org.codeberg.dnkl.foot-server.desktop', 'org.codeberg.dnkl.footclient.desktop', + 'foot.desktop', 'foot-server.desktop', 'footclient.desktop', install_dir: join_paths(get_option('datadir'), 'applications')) systemd = dependency('systemd', required: false) From 8a4bbbf5cb251e34fb81d8fd9c6c864ee2613158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 8 Sep 2024 13:45:20 +0200 Subject: [PATCH 0885/1323] readme: update mastodon link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dc5421cc..117ed298 100644 --- a/README.md +++ b/README.md @@ -684,7 +684,7 @@ available at https://libera.irclog.whitequark.org/foot. ## Mastodon Every now and then I post foot related updates on -[@dnkl@emacs.ch](https://emacs.ch/@dnkl) +[@dnkl@social.treehouse.systems](https://social.treehouse.systems/@dnkl) # Sponsoring/donations From 1925593a374b3e3b4cead86abc4ae352260a372e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 9 Sep 2024 06:51:10 +0200 Subject: [PATCH 0886/1323] render: resize(): don't overflow the number of scrollback lines The config system allows setting the scrollback lines to 2**32-1. However, the total number of grid lines is the scrollback lines plus the window size, and then rounded *up* to the nearest power of two. Furthermore, the number of rows is represented with a plain 'int' throughout the code base. The largest positive integer that fits in an int is 2**31-1. That however, is not a power of two. The largest positive integer, that also is a power of two, that fits in an int is 2**30, or 1073741824. Ideally, we'd just cast the line count to a 64-bit integer, and call __builtin_clzl{,l}() on it, and then take the smallest value of that, or 2**30. But, for some reason, __builtin_clzl(), and __builtin_clzll() appears to ignore bits above 32, despite they being typed to long and long long. Bug? Instead, ensure we never call __builtin_clz() on anything larger than 2**30. Closes #1828 --- CHANGELOG.md | 5 +++++ render.c | 33 +++++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c155f362..d8cadf79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,6 +93,11 @@ * Some invalid UTF-8 strings passing the validity check when setting the window title, triggering a Wayland protocol error which then caused foot to shutdown. +* "Too large" values for `scrollback.lines` causing an integer + overflow, resulting in either visual glitches, crashes, or both + ([#1828][1828]). + +[1828]: https://codeberg.org/dnkl/foot/issues/1828 ### Security diff --git a/render.c b/render.c index 98bc0b69..00ea3445 100644 --- a/render.c +++ b/render.c @@ -4369,8 +4369,6 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) term->width = width; term->height = height; - const uint32_t scrollback_lines = term->render.scrollback_lines; - /* Screen rows/cols before resize */ int old_cols = term->cols; int old_rows = term->rows; @@ -4379,9 +4377,36 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) const int new_cols = (term->width - 2 * pad_x) / term->cell_width; const int new_rows = (term->height - 2 * pad_y) / term->cell_height; + /* + * Requirements for scrollback: + * + * a) total number of rows (visible + scrollback history) must be + * a power of two + * b) must be representable in a plain int (signed) + * + * This means that on a "normal" system, where ints are 32-bit, + * the largest possible scrollback size is 1073741824 (0x40000000, + * 1 << 30). + * + * The largest *signed* int is 2147483647 (0x7fffffff), which is + * *not* a power of two. + * + * Note that these are theoretical limits. Most of the time, + * you'll get a memory allocation failure when trying to allocate + * the grid array. + */ + const unsigned max_scrollback = (INT_MAX >> 1) + 1; + const unsigned scrollback_lines_not_yet_power_of_two = + min((uint64_t)term->render.scrollback_lines + new_rows - 1, max_scrollback); + /* Grid rows/cols after resize */ - const int new_normal_grid_rows = 1 << (32 - __builtin_clz(new_rows + scrollback_lines - 1)); - const int new_alt_grid_rows = 1 << (32 - __builtin_clz(new_rows)); + const int new_normal_grid_rows = + min(1u << (32 - __builtin_clz(scrollback_lines_not_yet_power_of_two)), + max_scrollback); + const int new_alt_grid_rows = + min(1u << (32 - __builtin_clz(new_rows)), max_scrollback); + + LOG_DBG("grid rows: %d", new_normal_grid_rows); xassert(new_cols >= 1); xassert(new_rows >= 1); From 31f88e636c1f4ef6d2cb63d04b42c764d03604a8 Mon Sep 17 00:00:00 2001 From: Craig Barnes <craigbarnes@protonmail.com> Date: Wed, 11 Sep 2024 22:08:45 +0100 Subject: [PATCH 0887/1323] readme: typo: foot-ctlseq -> foot-ctlseqs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 117ed298..3395aff0 100644 --- a/README.md +++ b/README.md @@ -552,7 +552,7 @@ with the terminal emulator itself. Foot implements the following OSCs: * `OSC 777` - desktop notification (only the `;notify` sub-command of OSC 777 is supported.) -See the **foot-ctlseq**(7) man page for a complete list of supported +See the **foot-ctlseqs**(7) man page for a complete list of supported control sequences. From d4a1283797b5a964d92e64c698d65f701a90e1cc Mon Sep 17 00:00:00 2001 From: Craig Barnes <craigbarnes@protonmail.com> Date: Wed, 11 Sep 2024 20:13:30 +0100 Subject: [PATCH 0888/1323] xsnprintf: various improvements related to xvsnprintf() and xsnprintf() Summary of changes: * Make xvsnprintf() static * restrict-qualify pointer arguments (as done by the libc equivalents) * Make comments and spec references more thorough * Remove pointless `n <= INT_MAX` assertion (see comment) * Use FATAL_ERROR() instead of xassert() (since the assertion is inside a shared util function but the caller is responsible for ensuring the condition holds true) * Change some callers to use size_t instead of int for the return value (negative returns are impossible and all subsequent uses are size_t) The updated comments and code were taken (and adapted) from: https://gitlab.com/craigbarnes/dte/-/blob/49260bb154bca0434462a41c061e512540ec2e49/src/util/xsnprintf.c#L6-50 This work was entirely authored by me and I hereby license this contribution under the MIT license (stated explicitly, so that there's no ambiguity w.r.t. the original license). --- csi.c | 2 +- dcs.c | 22 +++++++++++----------- meson.build | 1 + notify.c | 4 ++-- osc.c | 2 +- terminal.c | 2 +- xsnprintf.c | 53 ++++++++++++++++++++++++++++++++++++----------------- xsnprintf.h | 3 +-- 8 files changed, 54 insertions(+), 35 deletions(-) diff --git a/csi.c b/csi.c index 91d42e2a..7c6ea7ca 100644 --- a/csi.c +++ b/csi.c @@ -2127,7 +2127,7 @@ csi_dispatch(struct terminal *term, uint8_t final) case 'R': { /* XTREPORTCOLORS */ char reply[64]; - int n = xsnprintf(reply, sizeof(reply), "\033[?%zu;%zu#Q", + size_t n = xsnprintf(reply, sizeof(reply), "\033[?%zu;%zu#Q", term->color_stack.idx, term->color_stack.size); term_to_slave(term, reply, n); break; diff --git a/dcs.c b/dcs.c index 6a832aed..7518c07c 100644 --- a/dcs.c +++ b/dcs.c @@ -271,7 +271,7 @@ decrqss_unhook(struct terminal *term) if (n == 1 && query[0] == 'r') { /* DECSTBM - Set Top and Bottom Margins */ char reply[64]; - int len = xsnprintf(reply, sizeof(reply), "\033P1$r%d;%dr\033\\", + size_t len = xsnprintf(reply, sizeof(reply), "\033P1$r%d;%dr\033\\", term->scroll_region.start + 1, term->scroll_region.end); term_to_slave(term, reply, len); @@ -300,7 +300,7 @@ decrqss_unhook(struct terminal *term) if (a->underline) { if (term->vt.underline.style > UNDERLINE_SINGLE) { char value[4]; - int val_len = + size_t val_len = xsnprintf(value, sizeof(value), "4:%d", term->vt.underline.style); append_sgr_attr_n(&reply, &len, value, val_len); } else @@ -321,7 +321,7 @@ decrqss_unhook(struct terminal *term) case COLOR_BASE16: { char value[4]; - int val_len = xsnprintf( + size_t val_len = xsnprintf( value, sizeof(value), "%u", a->fg >= 8 ? a->fg - 8 + 90 : a->fg + 30); append_sgr_attr_n(&reply, &len, value, val_len); @@ -330,7 +330,7 @@ decrqss_unhook(struct terminal *term) case COLOR_BASE256: { char value[16]; - int val_len = xsnprintf(value, sizeof(value), "38:5:%u", a->fg); + size_t val_len = xsnprintf(value, sizeof(value), "38:5:%u", a->fg); append_sgr_attr_n(&reply, &len, value, val_len); break; } @@ -341,7 +341,7 @@ decrqss_unhook(struct terminal *term) uint8_t b = a->fg >> 0; char value[32]; - int val_len = xsnprintf( + size_t val_len = xsnprintf( value, sizeof(value), "38:2::%hhu:%hhu:%hhu", r, g, b); append_sgr_attr_n(&reply, &len, value, val_len); break; @@ -354,7 +354,7 @@ decrqss_unhook(struct terminal *term) case COLOR_BASE16: { char value[4]; - int val_len = xsnprintf( + size_t val_len = xsnprintf( value, sizeof(value), "%u", a->bg >= 8 ? a->bg - 8 + 100 : a->bg + 40); append_sgr_attr_n(&reply, &len, value, val_len); @@ -363,7 +363,7 @@ decrqss_unhook(struct terminal *term) case COLOR_BASE256: { char value[16]; - int val_len = xsnprintf(value, sizeof(value), "48:5:%u", a->bg); + size_t val_len = xsnprintf(value, sizeof(value), "48:5:%u", a->bg); append_sgr_attr_n(&reply, &len, value, val_len); break; } @@ -374,7 +374,7 @@ decrqss_unhook(struct terminal *term) uint8_t b = a->bg >> 0; char value[32]; - int val_len = xsnprintf( + size_t val_len = xsnprintf( value, sizeof(value), "48:2::%hhu:%hhu:%hhu", r, g, b); append_sgr_attr_n(&reply, &len, value, val_len); break; @@ -388,7 +388,7 @@ decrqss_unhook(struct terminal *term) case COLOR_BASE256: { char value[16]; - int val_len = xsnprintf( + size_t val_len = xsnprintf( value, sizeof(value), "58:5:%u", term->vt.underline.color); append_sgr_attr_n(&reply, &len, value, val_len); break; @@ -400,7 +400,7 @@ decrqss_unhook(struct terminal *term) uint8_t b = term->vt.underline.color >> 0; char value[32]; - int val_len = xsnprintf( + size_t val_len = xsnprintf( value, sizeof(value), "58:2::%hhu:%hhu:%hhu", r, g, b); append_sgr_attr_n(&reply, &len, value, val_len); break; @@ -432,7 +432,7 @@ decrqss_unhook(struct terminal *term) mode--; char reply[16]; - int len = xsnprintf(reply, sizeof(reply), "\033P1$r%d q\033\\", mode); + size_t len = xsnprintf(reply, sizeof(reply), "\033P1$r%d q\033\\", mode); term_to_slave(term, reply, len); } diff --git a/meson.build b/meson.build index c6e0e2ef..1b3b01aa 100644 --- a/meson.build +++ b/meson.build @@ -216,6 +216,7 @@ common = static_library( 'log.c', 'log.h', 'char32.c', 'char32.h', 'debug.c', 'debug.h', + 'macros.h', 'xmalloc.c', 'xmalloc.h', 'xsnprintf.c', 'xsnprintf.h' ) diff --git a/notify.c b/notify.c index d1c06fb1..58388b1c 100644 --- a/notify.c +++ b/notify.c @@ -268,7 +268,7 @@ notif_done(struct reaper *reaper, pid_t pid, int status, void *data) } char reply[7 + strlen(id) + 1 + strlen(button_nr) + 2 + 1]; - int n = xsnprintf( + size_t n = xsnprintf( reply, sizeof(reply), "\033]99;i=%s;%s\033\\", id, button_nr); term_to_slave(term, reply, n); } @@ -278,7 +278,7 @@ notif_done(struct reaper *reaper, pid_t pid, int status, void *data) const char *id = notif->id != NULL ? notif->id : "0"; char reply[7 + strlen(id) + 1 + 7 + 1 + 2 + 1]; - int n = xsnprintf( + size_t n = xsnprintf( reply, sizeof(reply), "\033]99;i=%s:p=close;\033\\", id); term_to_slave(term, reply, n); } diff --git a/osc.c b/osc.c index 63b66dd7..0db11811 100644 --- a/osc.c +++ b/osc.c @@ -739,7 +739,7 @@ kitty_notification(struct terminal *term, char *string) const char *terminator = term->vt.osc.bel ? "\a" : "\033\\"; char reply[128]; - int n = xsnprintf( + size_t n = xsnprintf( reply, sizeof(reply), "\033]99;i=%s:p=?;p=%s:a=%s:o=%s:u=%s:c=1:w=1:s=system,silent,error,warn,warning,info,question%s", reply_id, p_caps, a_caps, when_caps, u_caps, terminator); diff --git a/terminal.c b/terminal.c index 7e490418..453798e8 100644 --- a/terminal.c +++ b/terminal.c @@ -4302,7 +4302,7 @@ term_send_size_notification(struct terminal *term) const int width = term->width - term->margins.left - term->margins.right; char buf[128]; - const int n = xsnprintf( + const size_t n = xsnprintf( buf, sizeof(buf), "\033[48;%d;%d;%d;%dt", term->rows, term->cols, height, width); term_to_slave(term, buf, n); diff --git a/xsnprintf.c b/xsnprintf.c index b0e17741..2f6f8493 100644 --- a/xsnprintf.c +++ b/xsnprintf.c @@ -1,32 +1,51 @@ #include "xsnprintf.h" +#include <errno.h> #include <limits.h> #include <stdio.h> #include "debug.h" +#include "macros.h" -size_t -xvsnprintf(char *buf, size_t n, const char *format, va_list ap) +/* + * ISO C doesn't require vsnprintf(3) to set errno on failure, but + * POSIX does: + * + * "If an output error was encountered, these functions shall return + * a negative value and set errno to indicate the error." + * + * The mandated errors of interest are: + * + * - EILSEQ: A wide-character code does not correspond to a valid character + * - EOVERFLOW: The value of n is greater than INT_MAX + * - EOVERFLOW: The value to be returned is greater than INT_MAX + * + * ISO C11 states: + * + * "The vsnprintf function returns the number of characters that would + * have been written had n been sufficiently large, not counting the + * terminating null character, or a negative value if an encoding error + * occurred. Thus, the null-terminated output has been completely + * written if and only if the returned value is nonnegative and less + * than n." + * + * See also: + * + * - ISO C11 §7.21.6.12p3 + * - https://pubs.opengroup.org/onlinepubs/9699919799/functions/vsnprintf.html + * - https://pubs.opengroup.org/onlinepubs/9699919799/functions/snprintf.html + */ +static size_t +xvsnprintf(char *restrict buf, size_t n, const char *restrict format, va_list ap) { - xassert(n <= INT_MAX); int len = vsnprintf(buf, n, format, ap); - - /* - * ISO C11 §7.21.6.5 states: - * "The snprintf function returns the number of characters that - * would have been written had n been sufficiently large, not - * counting the terminating null character, or a negative value - * if an encoding error occurred. Thus, the null-terminated output - * has been completely written if and only if the returned value - * is nonnegative and less than n." - */ - xassert(len >= 0); - xassert(len < (int)n); - + if (unlikely(len < 0 || len >= (int)n)) { + FATAL_ERROR(__func__, (len < 0) ? errno : ENOBUFS); + } return (size_t)len; } size_t -xsnprintf(char *buf, size_t n, const char *format, ...) +xsnprintf(char *restrict buf, size_t n, const char *restrict format, ...) { va_list ap; va_start(ap, format); diff --git a/xsnprintf.h b/xsnprintf.h index c745ef3e..9463a34a 100644 --- a/xsnprintf.h +++ b/xsnprintf.h @@ -4,5 +4,4 @@ #include <stddef.h> #include "macros.h" -size_t xsnprintf(char *buf, size_t len, const char *fmt, ...) PRINTF(3) NONNULL_ARGS; -size_t xvsnprintf(char *buf, size_t len, const char *fmt, va_list ap) VPRINTF(3) NONNULL_ARGS; +size_t xsnprintf(char *restrict buf, size_t n, const char *restrict format, ...) PRINTF(3) NONNULL_ARGS; From 5ef69fc591e2fbad2b399e765d697150c97a95a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 8 Sep 2024 10:42:24 +0200 Subject: [PATCH 0889/1323] meson: detect wayland-protocols >= 1.37, and conditionally enable xdg-toplevel-icon-v1 --- meson.build | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/meson.build b/meson.build index 1b3b01aa..91e745bf 100644 --- a/meson.build +++ b/meson.build @@ -171,6 +171,14 @@ wl_proto_xml = [ wayland_protocols_datadir / 'staging/single-pixel-buffer/single-pixel-buffer-v1.xml', ] +if wayland_protocols.version().version_compare('>=1.37') + add_project_arguments('-DHAVE_XDG_TOPLEVEL_ICON', language: 'c') + wl_proto_xml += [wayland_protocols_datadir / 'staging/xdg-toplevel-icon/xdg-toplevel-icon-v1.xml'] + xdg_toplevel_icon = true +else + xdg_toplevel_icon = false +endif + foreach prot : wl_proto_xml wl_proto_headers += custom_target( prot.underscorify() + '-client-header', @@ -401,6 +409,7 @@ summary( 'Themes': get_option('themes'), 'IME': get_option('ime'), 'Grapheme clustering': utf8proc.found(), + 'Wayland: xdg-toplevel-icon-v1': xdg_toplevel_icon, 'utmp backend': utmp_backend, 'utmp helper default path': utmp_default_helper_path, 'Build terminfo': tic.found(), From 28a1c67dd5ecc917ebd69c0552a1ed99165c310f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 8 Sep 2024 11:18:30 +0200 Subject: [PATCH 0890/1323] wayland: bind the xdg-toplevel-icon manager global --- wayland.c | 15 +++++++++++++++ wayland.h | 8 ++++++++ 2 files changed, 23 insertions(+) diff --git a/wayland.c b/wayland.c index 3f65901b..1e98560a 100644 --- a/wayland.c +++ b/wayland.c @@ -1363,6 +1363,17 @@ handle_global(void *data, struct wl_registry *registry, &wp_single_pixel_buffer_manager_v1_interface, required); } +#if defined(HAVE_XDG_TOPLEVEL_ICON) + else if (streq(interface, xdg_toplevel_icon_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->toplevel_icon_manager = wl_registry_bind( + wayl->registry, name, &xdg_toplevel_icon_v1_interface, required); + } +#endif + #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED else if (streq(interface, zwp_text_input_manager_v3_interface.name)) { const uint32_t required = 1; @@ -1679,6 +1690,10 @@ wayl_destroy(struct wayland *wayl) zwp_text_input_manager_v3_destroy(wayl->text_input_manager); #endif +#if defined(HAVE_XDG_TOPLEVEL_ICON) + if (wayl->toplevel_icon_manager != NULL) + xdg_toplevel_icon_manager_v1_destroy(wayl->toplevel_icon_manager); +#endif if (wayl->single_pixel_manager != NULL) wp_single_pixel_buffer_manager_v1_destroy(wayl->single_pixel_manager); if (wayl->fractional_scale_manager != NULL) diff --git a/wayland.h b/wayland.h index ca9c05fa..227e2a68 100644 --- a/wayland.h +++ b/wayland.h @@ -20,6 +20,10 @@ #include <xdg-output-unstable-v1.h> #include <xdg-shell.h> +#if defined(HAVE_XDG_TOPLEVEL_ICON) + #include <xdg-toplevel-icon-v1.h> +#endif + #include <fcft/fcft.h> #include <tllist.h> @@ -443,6 +447,10 @@ struct wayland { struct wp_single_pixel_buffer_manager_v1 *single_pixel_manager; +#if defined(HAVE_XDG_TOPLEVEL_ICON) + struct xdg_toplevel_icon_manager_v1 *toplevel_icon_manager; +#endif + bool presentation_timings; struct wp_presentation *presentation; uint32_t presentation_clock_id; From 0cb07027f2f1e5035cc009140084128d35995c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 8 Sep 2024 13:15:21 +0200 Subject: [PATCH 0891/1323] wayland: set toplevel icon If the xdg-toplevel-icon-v1 protocol is available, and we have the corresponding manager global, set the toplevel icon to "foot". Note: we do *not* provide any pixel data. This is by design; we want to keep things simple. To be able to provide pixel data, we would have to either: * embed the raw pixel data in the foot binary * link against either libpng or/and e.g. nanosvg, locate, at run-time, the paths to our own icons, and load them at run-time. * link against either libpng or/and e.g. nanosvg, and, at run-time, do a full icon lookup. This would also require us to add a config option for which icon theme to use. Of the two, I would prefer the first option. But, let's skip this completely for now. By providing the icon as a name, the compositor will have to lookup the icon itself. Compositors supporting icons is likely to already support this. So what do we gain by implementing this protocol? Compositors no longer has to parse .desktop files and map our app-id to find the icon to use. There's one question remaining. With this patch, the icon name is hardcoded to "foot", just like our .desktop files. But, perhaps we should use the app-id instead? And if so, should we also change the icon when the app-id changes? My gut feeling is, yes, we should use the app-id instead, and yes, we should update the icon when the app-id is changed at run-time. --- wayland.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/wayland.c b/wayland.c index 1e98560a..30ee86c6 100644 --- a/wayland.c +++ b/wayland.c @@ -1811,6 +1811,17 @@ wayl_win_init(struct terminal *term, const char *token) xdg_toplevel_set_app_id(win->xdg_toplevel, conf->app_id); +#if defined(HAVE_XDG_TOPLEVEL_ICON) + if (wayl->toplevel_icon_manager != NULL) { + struct xdg_toplevel_icon_v1 *icon = + xdg_toplevel_icon_manager_v1_create_icon(wayl->toplevel_icon_manager); + xdg_toplevel_icon_v1_set_name(icon, "foot"); + xdg_toplevel_icon_manager_v1_set_icon( + wayl->toplevel_icon_manager, win->xdg_toplevel, icon); + xdg_toplevel_icon_v1_destroy(icon); + } +#endif + if (conf->csd.preferred == CONF_CSD_PREFER_NONE) { /* User specifically do *not* want decorations */ win->csd_mode = CSD_NO; From b34137dde3bd40b5a116b4d8f86196027fc11d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 8 Sep 2024 18:25:07 +0200 Subject: [PATCH 0892/1323] toplevel-icon: set to app-id, instead of hardcoding to "foot" And, special case "footclient", and map it to "foot". --- render.c | 27 ++++++++++++++++++++++++--- wayland.c | 6 +++++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/render.c b/render.c index 00ea3445..93bad7fe 100644 --- a/render.c +++ b/render.c @@ -4944,10 +4944,31 @@ render_refresh_app_id(struct terminal *term) }; 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); + return; } + + const char *app_id = + term->app_id != NULL ? term->app_id : term->conf->app_id; + + xdg_toplevel_set_app_id(term->window->xdg_toplevel, app_id); + +#if defined(HAVE_XDG_TOPLEVEL_ICON) + if (term->wl->toplevel_icon_manager != NULL) { + struct xdg_toplevel_icon_v1 *icon = + xdg_toplevel_icon_manager_v1_create_icon( + term->wl->toplevel_icon_manager); + + xdg_toplevel_icon_v1_set_name( + icon, streq(app_id, "footclient") ? "foot" : app_id); + + xdg_toplevel_icon_manager_v1_set_icon( + term->wl->toplevel_icon_manager, term->window->xdg_toplevel, icon); + + xdg_toplevel_icon_v1_destroy(icon); + } +#endif + + term->render.app_id.last_update = now; } void diff --git a/wayland.c b/wayland.c index 30ee86c6..fd228312 100644 --- a/wayland.c +++ b/wayland.c @@ -1813,9 +1813,13 @@ wayl_win_init(struct terminal *term, const char *token) #if defined(HAVE_XDG_TOPLEVEL_ICON) if (wayl->toplevel_icon_manager != NULL) { + const char *app_id = + term->app_id != NULL ? term->app_id : term->conf->app_id; + struct xdg_toplevel_icon_v1 *icon = xdg_toplevel_icon_manager_v1_create_icon(wayl->toplevel_icon_manager); - xdg_toplevel_icon_v1_set_name(icon, "foot"); + xdg_toplevel_icon_v1_set_name(icon, streq( + app_id, "footclient") ? "foot" : app_id); xdg_toplevel_icon_manager_v1_set_icon( wayl->toplevel_icon_manager, win->xdg_toplevel, icon); xdg_toplevel_icon_v1_destroy(icon); From 3f8a1fc85b933678be59bf05d52652335529545b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 8 Sep 2024 18:26:28 +0200 Subject: [PATCH 0893/1323] changelog: xdg-toplevel-icon-v1 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8cadf79..564b62b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ (and the grid reflowed) or not when e.g. zooming in/out ([#1807][1807]). * `strikeout-thickness` option. +* Implemented the new `xdg-toplevel-icon-v1` protocol. [1807]: https://codeberg.org/dnkl/foot/issues/1807 From 97ec375c67cc7b071b0478b6c887057008dc469e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 10 Sep 2024 18:53:38 +0200 Subject: [PATCH 0894/1323] toplevel-icon: implement OSC-1, CSI 20/21/22/23 t * The toplevel icon is now set to the app-id, unless "overridden" by OSC-1 or OSC-0. * Implemented OSC-1 * OSC-0 extended to also set the icon * Implemented CSI 20 t - report window icon * Implemented CSI 21 t - report window title * Implemented CSI 22 ; 1 t - push window icon * Implemented CS 23 ; 1 t - pop window icon * Extended CSI 22/23 ; 0 t to also push/pop the icon * Verify app-id set by OSC-176 is valid UTF-8 * Verify icon set by OSC-0/1 is valid UTF-8 --- CHANGELOG.md | 5 +++ csi.c | 31 +++++++++++++- doc/foot-ctlseqs.7.scd | 22 ++++++++-- misc.c | 7 ++++ misc.h | 2 + osc.c | 15 +++++-- render.c | 69 ++++++++++++++++++++++---------- render.h | 1 + terminal.c | 91 +++++++++++++++++++++++++++++++++++++++++- terminal.h | 9 +++++ 10 files changed, 220 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 564b62b2..1d0f5454 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,11 @@ ([#1807][1807]). * `strikeout-thickness` option. * Implemented the new `xdg-toplevel-icon-v1` protocol. +* Implemented `CSI 20 t`: report window icon. +* Implemented `CSI 21 t`: report window title. +* Implemented `CSI 22 ; 1 t`: push window icon. +* Implemented `CSI 23 ; 1 t`: pop window icon. +* Implemented `OSC 1`: set window icon. [1807]: https://codeberg.org/dnkl/foot/issues/1807 diff --git a/csi.c b/csi.c index 7c6ea7ca..39821d8d 100644 --- a/csi.c +++ b/csi.c @@ -1249,8 +1249,6 @@ csi_dispatch(struct terminal *term, uint8_t final) case 8: LOG_WARN("unimplemented: resize window in chars"); break; case 9: LOG_WARN("unimplemented: maximize/unmaximize window"); break; case 10: LOG_WARN("unimplemented: to/from full screen"); break; - case 20: LOG_WARN("unimplemented: report icon label"); break; - case 21: LOG_WARN("unimplemented: report window title"); break; case 24: LOG_WARN("unimplemented: resize window (DECSLPP)"); break; case 11: /* report if window is iconified */ @@ -1354,6 +1352,24 @@ csi_dispatch(struct terminal *term, uint8_t final) break; } + case 20: { + const char *icon = term_icon(term); + + char reply[3 + strlen(icon) + 2 + 1]; + int chars = xsnprintf( + reply, sizeof(reply), "\033]L%s\033\\", icon); + term_to_slave(term, reply, chars); + break; + } + + case 21: { + char reply[3 + strlen(term->window_title) + 2 + 1]; + int chars = xsnprintf( + reply, sizeof(reply), "\033]l%s\033\\", term->window_title); + term_to_slave(term, reply, chars); + break; + } + case 22: { /* push window title */ /* 0 - icon + title, 1 - icon, 2 - title */ unsigned what = vt_param_get(term, 1, 0); @@ -1361,6 +1377,10 @@ csi_dispatch(struct terminal *term, uint8_t final) tll_push_back( term->window_title_stack, xstrdup(term->window_title)); } + if (what == 0 || what == 1) { + tll_push_back( + term->window_icon_stack, xstrdup(term->window_icon)); + } break; } @@ -1374,6 +1394,13 @@ csi_dispatch(struct terminal *term, uint8_t final) free(title); } } + if (what == 0 || what == 1) { + if (tll_length(term->window_icon_stack) > 0) { + char *icon = tll_pop_back(term->window_icon_stack); + term_set_icon(term, icon); + free(icon); + } + } break; } diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index 998b6843..60f78d83 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -388,15 +388,27 @@ manipulation sequences. The generic format is: | 19 : - : Report screen size, in characters. +| 20 +: - +: Report icon label. +| 21 +: - +: Report window title. | 22 : - -: Push window title+icon. Foot does not support pushing the icon. +: Push window title+icon. +| 22 +: 1 +: Push window icon. | 22 : 2 : Push window title. | 23 : - -: Pop window title+icon. Foot does not support popping the icon. +: Pop window title+icon. +| 23 +: 1 +: Pop window icon. | 23 : 2 : Pop window title. @@ -659,8 +671,10 @@ All _OSC_ sequences begin with *\\E]*, sometimes abbreviated _OSC_. :< *Description* | \\E] 0 ; _Pt_ \\E\\ : xterm -: Set window icon and title to _Pt_ (foot does not support setting the - icon) +: Set window icon and title to _Pt_. +| \\E] 1 ; _Pt_ \\E\\ +: xterm +: Set window icon to _Pt_. | \\E] 2 ; _Pt_ \\E\\ : xterm : Set window title to _Pt_ diff --git a/misc.c b/misc.c index a81aa9e4..1e5b9328 100644 --- a/misc.c +++ b/misc.c @@ -42,3 +42,10 @@ timespec_sub(const struct timespec *a, const struct timespec *b, res->tv_nsec += one_sec_in_ns; } } + +bool +is_valid_utf8(const char *value) +{ + return value != NULL && + mbsntoc32(NULL, value, strlen(value), 0) != (size_t)-1; +} diff --git a/misc.h b/misc.h index 648bb65f..cce8d2c1 100644 --- a/misc.h +++ b/misc.h @@ -8,3 +8,5 @@ bool isword(char32_t wc, bool spaces_only, const char32_t *delimiters); void timespec_add(const struct timespec *a, const struct timespec *b, struct timespec *res); void timespec_sub(const struct timespec *a, const struct timespec *b, struct timespec *res); + +bool is_valid_utf8(const char *value); diff --git a/osc.c b/osc.c index 0db11811..541a13f4 100644 --- a/osc.c +++ b/osc.c @@ -1145,9 +1145,18 @@ osc_dispatch(struct terminal *term) char *string = (char *)&term->vt.osc.data[data_ofs]; switch (param) { - case 0: term_set_window_title(term, string); break; /* icon + title */ - case 1: break; /* icon */ - case 2: term_set_window_title(term, string); break; /* title */ + case 0: /* icon + title */ + term_set_window_title(term, string); + term_set_icon(term, string); + break; + + case 1: /* icon */ + term_set_icon(term, string); + break; + + case 2: /* title */ + term_set_window_title(term, string); + break; case 4: { /* Set color<idx> */ diff --git a/render.c b/render.c index 93bad7fe..355aa40e 100644 --- a/render.c +++ b/render.c @@ -12,15 +12,19 @@ #include "macros.h" #if HAS_INCLUDE(<pthread_np.h>) -#include <pthread_np.h> -#define pthread_setname_np(thread, name) (pthread_set_name_np(thread, name), 0) + #include <pthread_np.h> + #define pthread_setname_np(thread, name) (pthread_set_name_np(thread, name), 0) #elif defined(__NetBSD__) -#define pthread_setname_np(thread, name) pthread_setname_np(thread, "%s", (void *)name) + #define pthread_setname_np(thread, name) pthread_setname_np(thread, "%s", (void *)name) #endif +#include <presentation-time.h> #include <wayland-cursor.h> #include <xdg-shell.h> -#include <presentation-time.h> + +#if defined(HAVE_XDG_TOPLEVEL_ICON) +#include <xdg-toplevel-icon-v1.h> +#endif #include <fcft/fcft.h> @@ -4951,26 +4955,49 @@ render_refresh_app_id(struct terminal *term) term->app_id != NULL ? term->app_id : term->conf->app_id; xdg_toplevel_set_app_id(term->window->xdg_toplevel, app_id); - -#if defined(HAVE_XDG_TOPLEVEL_ICON) - if (term->wl->toplevel_icon_manager != NULL) { - struct xdg_toplevel_icon_v1 *icon = - xdg_toplevel_icon_manager_v1_create_icon( - term->wl->toplevel_icon_manager); - - xdg_toplevel_icon_v1_set_name( - icon, streq(app_id, "footclient") ? "foot" : app_id); - - xdg_toplevel_icon_manager_v1_set_icon( - term->wl->toplevel_icon_manager, term->window->xdg_toplevel, icon); - - xdg_toplevel_icon_v1_destroy(icon); - } -#endif - term->render.app_id.last_update = now; } +void +render_refresh_icon(struct terminal *term) +{ +#if defined(HAVE_XDG_TOPLEVEL_ICON) + if (term->wl->toplevel_icon_manager == NULL) { + LOG_DBG("compositor does not implement xdg-toplevel-icon: " + "ignoring request to refresh window icon"); + return; + } + + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) + return; + + struct timespec diff; + timespec_sub(&now, &term->render.icon.last_update, &diff); + + if (diff.tv_sec == 0 && diff.tv_nsec < 8333 * 1000) { + const struct itimerspec timeout = { + .it_value = {.tv_nsec = 8333 * 1000 - diff.tv_nsec}, + }; + + timerfd_settime(term->render.icon.timer_fd, 0, &timeout, NULL); + return; + } + + const char *icon_name = term_icon(term); + LOG_DBG("setting toplevel icon: %s", icon_name); + + struct xdg_toplevel_icon_v1 *icon = + xdg_toplevel_icon_manager_v1_create_icon(term->wl->toplevel_icon_manager); + xdg_toplevel_icon_v1_set_name(icon, icon_name); + xdg_toplevel_icon_manager_v1_set_icon( + term->wl->toplevel_icon_manager, term->window->xdg_toplevel, icon); + xdg_toplevel_icon_v1_destroy(icon); + + term->render.icon.last_update = now; +#endif +} + void render_refresh(struct terminal *term) { diff --git a/render.h b/render.h index cfedf311..1898351c 100644 --- a/render.h +++ b/render.h @@ -22,6 +22,7 @@ bool render_resize( void render_refresh(struct terminal *term); void render_refresh_app_id(struct terminal *term); +void render_refresh_icon(struct terminal *term); void render_refresh_csd(struct terminal *term); void render_refresh_search(struct terminal *term); void render_refresh_title(struct terminal *term); diff --git a/terminal.c b/terminal.c index 453798e8..5e9a60a5 100644 --- a/terminal.c +++ b/terminal.c @@ -639,6 +639,30 @@ fdm_title_update_timeout(struct fdm *fdm, int fd, int events, void *data) return true; } +static bool +fdm_icon_update_timeout(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t unused; + ssize_t ret = read(term->render.icon.timer_fd, &unused, sizeof(unused)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + LOG_ERRNO("failed to read icon update throttle timer"); + return false; + } + + struct itimerspec reset = {{0}}; + timerfd_settime(term->render.icon.timer_fd, 0, &reset, NULL); + + render_refresh_icon(term); + return true; +} + static bool fdm_app_id_update_timeout(struct fdm *fdm, int fd, int events, void *data) { @@ -1114,6 +1138,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, int delay_upper_fd = -1; int app_sync_updates_fd = -1; int title_update_fd = -1; + int icon_update_fd = -1; int app_id_update_fd = -1; struct terminal *term = malloc(sizeof(*term)); @@ -1150,6 +1175,12 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, goto close_fds; } + if ((icon_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) + { + LOG_ERRNO("failed to create icon update throttle timer FD"); + goto close_fds; + } + if ((app_id_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) { LOG_ERRNO("failed to create app ID update throttle timer FD"); @@ -1187,6 +1218,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, !fdm_add(fdm, delay_upper_fd, EPOLLIN, &fdm_delayed_render, term) || !fdm_add(fdm, app_sync_updates_fd, EPOLLIN, &fdm_app_sync_updates_timeout, term) || !fdm_add(fdm, title_update_fd, EPOLLIN, &fdm_title_update_timeout, term) || + !fdm_add(fdm, icon_update_fd, EPOLLIN, &fdm_icon_update_timeout, term) || !fdm_add(fdm, app_id_update_fd, EPOLLIN, &fdm_app_id_update_timeout, term)) { goto err; @@ -1288,6 +1320,9 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .title = { .timer_fd = title_update_fd, }, + .icon = { + .timer_fd = icon_update_fd, + }, .app_id = { .timer_fd = app_id_update_fd, }, @@ -1406,6 +1441,7 @@ close_fds: fdm_del(fdm, delay_upper_fd); fdm_del(fdm, app_sync_updates_fd); fdm_del(fdm, title_update_fd); + fdm_del(fdm, icon_update_fd); fdm_del(fdm, app_id_update_fd); free(term); @@ -1626,6 +1662,7 @@ term_shutdown(struct terminal *term) fdm_del(term->fdm, term->selection.auto_scroll.fd); fdm_del(term->fdm, term->render.app_sync_updates.timer_fd); fdm_del(term->fdm, term->render.app_id.timer_fd); + fdm_del(term->fdm, term->render.icon.timer_fd); fdm_del(term->fdm, term->render.title.timer_fd); fdm_del(term->fdm, term->delayed_render_timer.lower_fd); fdm_del(term->fdm, term->delayed_render_timer.upper_fd); @@ -1677,6 +1714,7 @@ term_shutdown(struct terminal *term) term->selection.auto_scroll.fd = -1; term->render.app_sync_updates.timer_fd = -1; term->render.app_id.timer_fd = -1; + term->render.icon.timer_fd = -1; term->render.title.timer_fd = -1; term->delayed_render_timer.lower_fd = -1; term->delayed_render_timer.upper_fd = -1; @@ -1731,6 +1769,7 @@ term_destroy(struct terminal *term) fdm_del(term->fdm, term->selection.auto_scroll.fd); fdm_del(term->fdm, term->render.app_sync_updates.timer_fd); fdm_del(term->fdm, term->render.app_id.timer_fd); + fdm_del(term->fdm, term->render.icon.timer_fd); fdm_del(term->fdm, term->render.title.timer_fd); fdm_del(term->fdm, term->delayed_render_timer.lower_fd); fdm_del(term->fdm, term->delayed_render_timer.upper_fd); @@ -1775,7 +1814,9 @@ term_destroy(struct terminal *term) composed_free(term->composed); free(term->app_id); + free(term->window_icon); free(term->window_title); + tll_free_and_free(term->window_icon_stack, free); tll_free_and_free(term->window_title_stack, free); for (size_t i = 0; i < sizeof(term->fonts) / sizeof(term->fonts[0]); i++) @@ -2007,6 +2048,9 @@ term_reset(struct terminal *term, bool hard) term->saved_charsets = term->charsets; tll_free_and_free(term->window_title_stack, free); term_set_window_title(term, term->conf->title); + tll_free_and_free(term->window_icon_stack, free); + term_set_app_id(term, NULL); + term_set_icon(term, NULL); term_set_user_mouse_cursor(term, NULL); @@ -3528,7 +3572,7 @@ term_set_window_title(struct terminal *term, const char *title) if (term->window_title != NULL && streq(term->window_title, title)) return; - if (mbsntoc32(NULL, title, strlen(title), 0) == (size_t)-1) { + if (!is_valid_utf8(title)) { /* It's an xdg_toplevel::set_title() protocol violation to set a title with an invalid UTF-8 sequence */ LOG_WARN("%s: title is not valid UTF-8, ignoring", title); @@ -3548,9 +3592,14 @@ term_set_app_id(struct terminal *term, const char *app_id) 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) + if (term->app_id != NULL && app_id != NULL && streq(term->app_id, app_id)) return; + if (app_id != NULL && !is_valid_utf8(app_id)) { + LOG_WARN("%s: app-id is not valid UTF-8, ignoring", app_id); + return; + } + free(term->app_id); if (app_id != NULL) { term->app_id = xstrdup(app_id); @@ -3558,6 +3607,44 @@ term_set_app_id(struct terminal *term, const char *app_id) term->app_id = NULL; } render_refresh_app_id(term); + render_refresh_icon(term); +} + +void +term_set_icon(struct terminal *term, const char *icon) +{ + if (icon != NULL && *icon == '\0') + icon = NULL; + if (term->window_icon == NULL && icon == NULL) + return; + if (term->window_icon != NULL && icon != NULL && streq(term->window_icon, icon)) + return; + + if (icon != NULL && !is_valid_utf8(icon)) { + LOG_WARN("%s: icon label is not valid UTF-8, ignoring", icon); + return; + } + + free(term->window_icon); + if (icon != NULL) { + term->window_icon = xstrdup(icon); + } else { + term->window_icon = NULL; + } + render_refresh_icon(term); +} + +const char * +term_icon(const struct terminal *term) +{ + const char *app_id = + term->app_id != NULL ? term->app_id : term->conf->app_id; + + return term->window_icon != NULL + ? term->window_icon + : streq(app_id, "footclient") + ? "foot" + : app_id; } void diff --git a/terminal.h b/terminal.h index a87a125b..28576f23 100644 --- a/terminal.h +++ b/terminal.h @@ -552,6 +552,8 @@ struct terminal { bool window_title_has_been_set; char *window_title; tll(char *) window_title_stack; + char *window_icon; + tll(char *)window_icon_stack; char *app_id; struct { @@ -670,6 +672,11 @@ struct terminal { int timer_fd; } title; + struct { + struct timespec last_update; + int timer_fd; + } icon; + struct { struct timespec last_update; int timer_fd; @@ -925,6 +932,8 @@ 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_set_icon(struct terminal *term, const char *icon); +const char *term_icon(const struct terminal *term); void term_flash(struct terminal *term, unsigned duration_ms); void term_bell(struct terminal *term); bool term_spawn_new(const struct terminal *term); From f5caa2d265f347183864b20929be7f74fcef4da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 10 Sep 2024 19:13:00 +0200 Subject: [PATCH 0895/1323] pgo: add missing stub for render_refresh_icon() --- pgo/pgo.c | 1 + 1 file changed, 1 insertion(+) diff --git a/pgo/pgo.c b/pgo/pgo.c index f87863c0..aab18847 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -70,6 +70,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) {} +void render_refresh_icon(struct terminal *term) {} bool render_xcursor_is_valid(const struct seat *seat, const char *cursor) From c6208a98c80f4b945608b4ce1bd6f35fab5b56d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 13 Sep 2024 08:45:54 +0200 Subject: [PATCH 0896/1323] main: include toplevel-icon support in --version output --- foot-features.h | 9 +++++++++ main.c | 3 ++- wayland.c | 20 +++++++++++++------- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/foot-features.h b/foot-features.h index ad447767..674c1056 100644 --- a/foot-features.h +++ b/foot-features.h @@ -37,3 +37,12 @@ static inline bool feature_graphemes(void) return false; #endif } + +static inline bool feature_xdg_toplevel_icon(void) +{ +#if defined(HAVE_XDG_TOPLEVEL_ICON) + return true; +#else + return false; +#endif +} diff --git a/main.c b/main.c index 15741012..973cbae4 100644 --- a/main.c +++ b/main.c @@ -51,11 +51,12 @@ version_and_features(void) { static char buf[256]; snprintf(buf, sizeof(buf), - "version: %s %cpgo %cime %cgraphemes %cassertions", + "version: %s %cpgo %cime %cgraphemes %ctoplevel-icon %cassertions", FOOT_VERSION, feature_pgo() ? '+' : '-', feature_ime() ? '+' : '-', feature_graphemes() ? '+' : '-', + feature_xdg_toplevel_icon() ? '+' : '-', feature_assertions() ? '+' : '-'); return buf; } diff --git a/wayland.c b/wayland.c index fd228312..9c184adc 100644 --- a/wayland.c +++ b/wayland.c @@ -1592,27 +1592,33 @@ wayl_init(struct fdm *fdm, struct key_binding_manager *key_binding_manager, goto out; } + if (presentation_timings && wayl->presentation == NULL) { + LOG_ERR("compositor does not implement the presentation time interface"); + goto out; + } + if (wayl->primary_selection_device_manager == NULL) - LOG_WARN("no primary selection available"); + LOG_WARN("compositor does not implement the primary selection interface"); if (wayl->xdg_activation == NULL) { LOG_WARN( - "no XDG activation support; " + "compositor does not implement XDG activation, " "bell.urgent will fall back to coloring the window margins red"); } if (wayl->fractional_scale_manager == NULL || wayl->viewporter == NULL) - LOG_WARN("fractional scaling not available"); + LOG_WARN("compositor does not implement fractional scaling"); if (wayl->cursor_shape_manager == NULL) { - LOG_WARN("no server-side cursors available, " + LOG_WARN("compositor does not implement server-side cursors, " "falling back to client-side cursors"); } - if (presentation_timings && wayl->presentation == NULL) { - LOG_ERR("presentation time interface not implemented by compositor"); - goto out; +#if defined(HAVE_XDG_TOPLEVEL_ICON) + if (wayl->toplevel_icon_manager == NULL) { + LOG_WARN("compositor does not implement the XDG toplevel icon protocol"); } +#endif #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED if (wayl->text_input_manager == NULL) { From 7984f08925e18e24e6b54ac4498d65fa26b2a4d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 13 Sep 2024 08:51:12 +0200 Subject: [PATCH 0897/1323] osc: OSC-1 does not set the icon, it sets the icon _label_ In fact, there appears there *is* no escape sequence to set the icon. Keep most of the logic in place, but in practice, we'll always set the icon to the app-id. That is, at startup, we set it to the configured app-id (either from config, or the command line). OSC-176, which sets the app-id, also updates the icon (to the app-id). --- csi.c | 11 ----------- osc.c | 2 -- terminal.c | 36 ++++++------------------------------ terminal.h | 5 ++--- 4 files changed, 8 insertions(+), 46 deletions(-) diff --git a/csi.c b/csi.c index 39821d8d..61e0e44b 100644 --- a/csi.c +++ b/csi.c @@ -1377,10 +1377,6 @@ csi_dispatch(struct terminal *term, uint8_t final) tll_push_back( term->window_title_stack, xstrdup(term->window_title)); } - if (what == 0 || what == 1) { - tll_push_back( - term->window_icon_stack, xstrdup(term->window_icon)); - } break; } @@ -1394,13 +1390,6 @@ csi_dispatch(struct terminal *term, uint8_t final) free(title); } } - if (what == 0 || what == 1) { - if (tll_length(term->window_icon_stack) > 0) { - char *icon = tll_pop_back(term->window_icon_stack); - term_set_icon(term, icon); - free(icon); - } - } break; } diff --git a/osc.c b/osc.c index 541a13f4..5efc7588 100644 --- a/osc.c +++ b/osc.c @@ -1147,11 +1147,9 @@ osc_dispatch(struct terminal *term) switch (param) { case 0: /* icon + title */ term_set_window_title(term, string); - term_set_icon(term, string); break; case 1: /* icon */ - term_set_icon(term, string); break; case 2: /* title */ diff --git a/terminal.c b/terminal.c index 5e9a60a5..6d62a3ed 100644 --- a/terminal.c +++ b/terminal.c @@ -1814,9 +1814,7 @@ term_destroy(struct terminal *term) composed_free(term->composed); free(term->app_id); - free(term->window_icon); free(term->window_title); - tll_free_and_free(term->window_icon_stack, free); tll_free_and_free(term->window_title_stack, free); for (size_t i = 0; i < sizeof(term->fonts) / sizeof(term->fonts[0]); i++) @@ -2048,9 +2046,7 @@ term_reset(struct terminal *term, bool hard) term->saved_charsets = term->charsets; tll_free_and_free(term->window_title_stack, free); term_set_window_title(term, term->conf->title); - tll_free_and_free(term->window_icon_stack, free); term_set_app_id(term, NULL); - term_set_icon(term, NULL); term_set_user_mouse_cursor(term, NULL); @@ -3610,39 +3606,19 @@ term_set_app_id(struct terminal *term, const char *app_id) render_refresh_icon(term); } -void -term_set_icon(struct terminal *term, const char *icon) -{ - if (icon != NULL && *icon == '\0') - icon = NULL; - if (term->window_icon == NULL && icon == NULL) - return; - if (term->window_icon != NULL && icon != NULL && streq(term->window_icon, icon)) - return; - - if (icon != NULL && !is_valid_utf8(icon)) { - LOG_WARN("%s: icon label is not valid UTF-8, ignoring", icon); - return; - } - - free(term->window_icon); - if (icon != NULL) { - term->window_icon = xstrdup(icon); - } else { - term->window_icon = NULL; - } - render_refresh_icon(term); -} - const char * term_icon(const struct terminal *term) { const char *app_id = term->app_id != NULL ? term->app_id : term->conf->app_id; - return term->window_icon != NULL + return +#if 0 +term->window_icon != NULL ? term->window_icon - : streq(app_id, "footclient") + : + #endif + streq(app_id, "footclient") ? "foot" : app_id; } diff --git a/terminal.h b/terminal.h index 28576f23..e87df54c 100644 --- a/terminal.h +++ b/terminal.h @@ -552,8 +552,8 @@ struct terminal { bool window_title_has_been_set; char *window_title; tll(char *) window_title_stack; - char *window_icon; - tll(char *)window_icon_stack; + //char *window_icon; /* No escape sequence available to set the icon */ + //tll(char *)window_icon_stack; char *app_id; struct { @@ -932,7 +932,6 @@ 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_set_icon(struct terminal *term, const char *icon); const char *term_icon(const struct terminal *term); void term_flash(struct terminal *term, unsigned duration_ms); void term_bell(struct terminal *term); From 9151685d04ef3b572f0c574e559143a71f97a854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 13 Sep 2024 08:57:07 +0200 Subject: [PATCH 0898/1323] csi: revert implementation of CSI 20 t --- csi.c | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/csi.c b/csi.c index 61e0e44b..35a39f82 100644 --- a/csi.c +++ b/csi.c @@ -1249,6 +1249,7 @@ csi_dispatch(struct terminal *term, uint8_t final) case 8: LOG_WARN("unimplemented: resize window in chars"); break; case 9: LOG_WARN("unimplemented: maximize/unmaximize window"); break; case 10: LOG_WARN("unimplemented: to/from full screen"); break; + case 20: LOG_WARN("unimplemented: report icon label"); break; case 24: LOG_WARN("unimplemented: resize window (DECSLPP)"); break; case 11: /* report if window is iconified */ @@ -1352,16 +1353,6 @@ csi_dispatch(struct terminal *term, uint8_t final) break; } - case 20: { - const char *icon = term_icon(term); - - char reply[3 + strlen(icon) + 2 + 1]; - int chars = xsnprintf( - reply, sizeof(reply), "\033]L%s\033\\", icon); - term_to_slave(term, reply, chars); - break; - } - case 21: { char reply[3 + strlen(term->window_title) + 2 + 1]; int chars = xsnprintf( From 76b58b566386598765b824458aa3670e064b6f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 13 Sep 2024 08:57:20 +0200 Subject: [PATCH 0899/1323] changelog: remove escape sequences we've reverted --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d0f5454..622617b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,11 +62,7 @@ ([#1807][1807]). * `strikeout-thickness` option. * Implemented the new `xdg-toplevel-icon-v1` protocol. -* Implemented `CSI 20 t`: report window icon. * Implemented `CSI 21 t`: report window title. -* Implemented `CSI 22 ; 1 t`: push window icon. -* Implemented `CSI 23 ; 1 t`: pop window icon. -* Implemented `OSC 1`: set window icon. [1807]: https://codeberg.org/dnkl/foot/issues/1807 From 297cb370aa3f9e9a9093415217be410fae6fccfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 15 Sep 2024 09:56:41 +0200 Subject: [PATCH 0900/1323] render: add missing include, limits.h --- render.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/render.c b/render.c index 355aa40e..3d1ce4ee 100644 --- a/render.c +++ b/render.c @@ -1,8 +1,9 @@ #include "render.h" +#include <limits.h> +#include <signal.h> #include <string.h> #include <unistd.h> -#include <signal.h> #include <sys/ioctl.h> #include <sys/time.h> From e2aeb7f3361b87e61a8bc5f478bb2dab056b1ac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 21 Sep 2024 09:08:40 +0200 Subject: [PATCH 0901/1323] render: xcursor_is_valid(): don't crash when there's no theme loaded When trying to set a custom cursor shape, we first validate it. This is done by checking against known server-side names, and then trying to load the cursor from the client side cursor theme. But, if we're using server side names, there is no theme loaded, and foot crashed. Fix by checking if we have a theme loaded, and if not, fail the cursor shape name validation. --- CHANGELOG.md | 2 ++ render.c | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 622617b2..3a0cdfad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,8 @@ * "Too large" values for `scrollback.lines` causing an integer overflow, resulting in either visual glitches, crashes, or both ([#1828][1828]). +* Crash when trying to set an invalid cursor shape with OSC-22, when + foot uses server-side cursor shapes. [1828]: https://codeberg.org/dnkl/foot/issues/1828 diff --git a/render.c b/render.c index 3d1ce4ee..20c0490b 100644 --- a/render.c +++ b/render.c @@ -4722,6 +4722,8 @@ render_xcursor_is_valid(const struct seat *seat, const char *cursor) { if (cursor == NULL) return false; + if (seat->pointer.theme == NULL) + return false; return wl_cursor_theme_get_cursor(seat->pointer.theme, cursor) != NULL; } From 046d9596575c2dfac5db0d151179711f89b52661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 20 Sep 2024 17:06:47 +0200 Subject: [PATCH 0902/1323] shm: fix compilation when FORCED_DOUBLE_BUFFERING is enabled --- shm.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shm.c b/shm.c index 04cec211..bbf6f933 100644 --- a/shm.c +++ b/shm.c @@ -577,8 +577,8 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height, bool with_alph buf->public.age++; else #if FORCED_DOUBLE_BUFFERING - if (buf->age == 0) - buf->age++; + if (buf->public.age == 0) + buf->public.age++; else #endif { From a9fefcf58b5bb5d1c6a2436c7500be2b45f596d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 20 Sep 2024 17:13:06 +0200 Subject: [PATCH 0903/1323] render: (debug): assert row->dirty vs. cell->attrs.clean consistency --- render.c | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/render.c b/render.c index 20c0490b..0aa38c1a 100644 --- a/render.c +++ b/render.c @@ -3315,6 +3315,30 @@ grid_render(struct terminal *term) render_sixel_images(term, buf->pix[0], &damage, &cursor); + +#if defined(_DEBUG) + for (int r = 0; r < term->rows; r++) { + const struct row *row = grid_row_in_view(term->grid, r); + + if (row->dirty) { + bool all_clean = true; + for (int c = 0; c < term->cols; c++) { + if (!row->cells[c].attrs.clean) { + all_clean = false; + break; + } + } + if (all_clean) + BUG("row #%d is dirty, but all cells are marked as clean", r); + } else { + for (int c = 0; c < term->cols; c++) { + if (!row->cells[c].attrs.clean) + BUG("row #%d is clean, but cell #%d is dirty", r, c); + } + } + } +#endif + if (term->render.workers.count > 0) { mtx_lock(&term->render.workers.lock); term->render.workers.buf = buf; From 798b44934fe36e7fcd738e4395df70fed9246b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 20 Sep 2024 17:14:59 +0200 Subject: [PATCH 0904/1323] render: double-buffer: optimization: skip clean rows When scanning the grid for all-dirty rows (that we can remove from the damage region we're about to memcpy from the old frame), check the row->dirty bit, and skip scanning the cells of that row altogether. We're only looking for rows where all cells are dirty - those rows can be removed from the region we copy from the old frame, since the entire row will be re-rendered anyway. If a row is clean, it *must* be copied from the old frame. --- render.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/render.c b/render.c index 0aa38c1a..d35aa639 100644 --- a/render.c +++ b/render.c @@ -3046,6 +3046,11 @@ reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old for (int r = 0; r < term->rows; r++) { const struct row *row = grid_row_in_view(term->grid, r); + if (!row->dirty) { + full_repaint_needed = false; + continue; + } + bool row_all_dirty = true; for (int c = 0; c < term->cols; c++) { if (row->cells[c].attrs.clean) { From 49ed8b5e21c5a46b9e32ae941ab2dbc2a0f61b88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 20 Sep 2024 17:16:45 +0200 Subject: [PATCH 0905/1323] selection: set row->dirty when clearing the cell->attrs.clean bit Closes #1715 --- selection.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/selection.c b/selection.c index 7540283b..91c851d2 100644 --- a/selection.c +++ b/selection.c @@ -965,6 +965,7 @@ mark_selected_region(struct terminal *term, pixman_box32_t *boxes, */ cell->attrs.clean = false; cell->attrs.selected = false; + row->dirty = true; continue; } @@ -972,8 +973,10 @@ mark_selected_region(struct terminal *term, pixman_box32_t *boxes, xassert(c - j >= 0); struct cell *cell = &row->cells[c - j]; - if (dirty_cells) + if (dirty_cells) { cell->attrs.clean = false; + row->dirty = true; + } cell->attrs.selected = selected; } From 6ad84dab2de90a425bcea84a6cc7b181a517fafc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 21 Sep 2024 09:11:28 +0200 Subject: [PATCH 0906/1323] render: do dirty/clean consistency check before rendering sixels Since the sixels may render some of the cells, we want the check to happen before the sixels. --- render.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/render.c b/render.c index d35aa639..f951cc7d 100644 --- a/render.c +++ b/render.c @@ -3315,12 +3315,6 @@ grid_render(struct terminal *term) } } - pixman_region32_t damage; - pixman_region32_init(&damage); - - render_sixel_images(term, buf->pix[0], &damage, &cursor); - - #if defined(_DEBUG) for (int r = 0; r < term->rows; r++) { const struct row *row = grid_row_in_view(term->grid, r); @@ -3344,6 +3338,12 @@ grid_render(struct terminal *term) } #endif + pixman_region32_t damage; + pixman_region32_init(&damage); + + render_sixel_images(term, buf->pix[0], &damage, &cursor); + + if (term->render.workers.count > 0) { mtx_lock(&term->render.workers.lock); term->render.workers.buf = buf; From 4afb94687c1182cbddef366a66d85cb92d7fe4d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 21 Sep 2024 09:17:38 +0200 Subject: [PATCH 0907/1323] changelog: #1715: "ghost" lines when selecting text --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a0cdfad..109125a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,8 +100,12 @@ ([#1828][1828]). * Crash when trying to set an invalid cursor shape with OSC-22, when foot uses server-side cursor shapes. +* Occasional visual glitches when selecting text, when foot is running + under a compositor that forces foot to double buffer + (e.g. KDE/KWin) ([#1715][1715]). [1828]: https://codeberg.org/dnkl/foot/issues/1828 +[1715]: https://codeberg.org/dnkl/foot/issues/1715 ### Security From ce38f5b41395963bd17f9f998802945db4987a2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 5 Oct 2024 08:53:11 +0200 Subject: [PATCH 0908/1323] render: sixels: update damage region when rendering sixels This fixes flickering when foot is forced to double-buffer (e.g when running under KDE, or smithay based compositors). Since the damage region isn't updated, the sixel images aren't included in the memcpy that is done to transfer the last frame's updated regions to the next frame. As a result, every other frame will have the sixels, while the others don't. Closes #1851 --- CHANGELOG.md | 4 ++++ render.c | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 109125a8..e18f6f95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,9 +103,13 @@ * Occasional visual glitches when selecting text, when foot is running under a compositor that forces foot to double buffer (e.g. KDE/KWin) ([#1715][1715]). +* Sixels flickering when foot is running under a compositor that + forces foot to double buffer (e.g. KDE, or Smithay based + compositors) ([#1851][1851]). [1828]: https://codeberg.org/dnkl/foot/issues/1828 [1715]: https://codeberg.org/dnkl/foot/issues/1715 +[1851]: https://codeberg.org/dnkl/foot/issues/1851 ### Security diff --git a/render.c b/render.c index f951cc7d..9f039f97 100644 --- a/render.c +++ b/render.c @@ -1390,7 +1390,8 @@ grid_render_scroll_reverse(struct terminal *term, struct buffer *buf, } static void -render_sixel_chunk(struct terminal *term, pixman_image_t *pix, const struct sixel *sixel, +render_sixel_chunk(struct terminal *term, pixman_image_t *pix, + pixman_region32_t *damage, const struct sixel *sixel, int term_start_row, int img_start_row, int count) { /* Translate row/column to x/y pixel values */ @@ -1427,6 +1428,9 @@ render_sixel_chunk(struct terminal *term, pixman_image_t *pix, const struct sixe x, y, width, height); + if (damage != NULL) + pixman_region32_union_rect(damage, damage, x, y, width, height); + wl_surface_damage_buffer(term->window->surface.surf, x, y, width, height); } @@ -1450,7 +1454,7 @@ render_sixel(struct terminal *term, pixman_image_t *pix, #define maybe_emit_sixel_chunk_then_reset() \ if (chunk_row_count != 0) { \ render_sixel_chunk( \ - term, pix, sixel, \ + term, pix, damage, sixel, \ chunk_term_start, chunk_img_start, chunk_row_count); \ chunk_term_start = chunk_img_start = -1; \ chunk_row_count = 0; \ From e891abdd6a6652bd46b28c1988700a7f30931210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 6 Oct 2024 11:26:35 +0200 Subject: [PATCH 0909/1323] render: remove unnecessary call to wl_surface_damage_buffer() The damage region(s) are translated to wl_surface_damage_buffer() calls after the entire frame has been rendered. --- render.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/render.c b/render.c index 9f039f97..d14c70ce 100644 --- a/render.c +++ b/render.c @@ -1430,8 +1430,6 @@ render_sixel_chunk(struct terminal *term, pixman_image_t *pix, if (damage != NULL) pixman_region32_union_rect(damage, damage, x, y, width, height); - - wl_surface_damage_buffer(term->window->surface.surf, x, y, width, height); } static void From 511aad419b9c869972902fe88b4f593b0faa6719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 23 Oct 2024 08:35:30 +0200 Subject: [PATCH 0910/1323] config: add color.sixelN options These options allows you to configure the default sixel color palette. --- CHANGELOG.md | 2 ++ config.c | 29 +++++++++++++++++++++++++++++ config.h | 1 + dcs.c | 2 +- doc/foot.ini.5.scd | 6 ++++++ foot.ini | 18 ++++++++++++++++++ sixel.c | 43 +++++++------------------------------------ terminal.h | 1 - 8 files changed, 64 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e18f6f95..f949f1be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,8 @@ * `strikeout-thickness` option. * Implemented the new `xdg-toplevel-icon-v1` protocol. * Implemented `CSI 21 t`: report window title. +* `colors.sixelNN` option, controlling the default sixel color + palette. [1807]: https://codeberg.org/dnkl/foot/issues/1807 diff --git a/config.c b/config.c index bda7648b..7c4ac0d1 100644 --- a/config.c +++ b/config.c @@ -86,6 +86,26 @@ static const uint32_t default_color_table[256] = { 0xd0d0d0, 0xdadada, 0xe4e4e4, 0xeeeeee }; +/* VT330/VT340 Programmer Reference Manual - Table 2-3 VT340 Default Color Map */ +static const uint32_t default_sixel_colors[16] = { + 0xff000000, + 0xff3333cc, + 0xffcc2121, + 0xff33cc33, + 0xffcc33cc, + 0xff33cccc, + 0xffcccc33, + 0xff878787, + 0xff424242, + 0xff545499, + 0xff994242, + 0xff549954, + 0xff995499, + 0xff549999, + 0xff999954, + 0xffcccccc, +}; + static const char *const binding_action_map[] = { [BIND_ACTION_NONE] = NULL, [BIND_ACTION_NOOP] = "noop", @@ -1309,6 +1329,14 @@ parse_section_colors(struct context *ctx) return true; } + else if (str_has_prefix(key, "sixel") && + ((key_len == 6 && last_digit < 10) || + (key_len == 7 && key[5] == '1' && last_digit < 6))) + { + size_t idx = key_len == 6 ? last_digit : 10 + last_digit; + return value_to_color(ctx, &conf->colors.sixel[idx], false); + } + else if (streq(key, "flash")) color = &conf->colors.flash; else if (streq(key, "foreground")) color = &conf->colors.fg; else if (streq(key, "background")) color = &conf->colors.bg; @@ -3247,6 +3275,7 @@ config_load(struct config *conf, const char *conf_path, }; memcpy(conf->colors.table, default_color_table, sizeof(default_color_table)); + memcpy(conf->colors.sixel, default_sixel_colors, sizeof(default_sixel_colors)); parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers); tokenize_cmdline( diff --git a/config.h b/config.h index 9e983011..adb9637c 100644 --- a/config.h +++ b/config.h @@ -228,6 +228,7 @@ struct config { uint32_t url; uint32_t dim[8]; + uint32_t sixel[16]; struct { uint32_t fg; diff --git a/dcs.c b/dcs.c index 7518c07c..ebea9e4c 100644 --- a/dcs.c +++ b/dcs.c @@ -461,7 +461,7 @@ dcs_hook(struct terminal *term, uint8_t final) break; } int p1 = vt_param_get(term, 0, 0); - int p2 = vt_param_get(term, 1,0); + int p2 = vt_param_get(term, 1, 0); int p3 = vt_param_get(term, 2, 0); term->vt.dcs.put_handler = sixel_init(term, p1, p2, p3); diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 908eb25f..f3c8f8d0 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -915,6 +915,12 @@ can configure the background transparency with the _alpha_ option. https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit for an explanation of the remainder. +*sixel0* *..* *sixel15* + The default sixel color palette. Default: _000000_, _3333cc_, + _cc2121_, _33cc33_, _cc33cc_, _33cccc_, _cccc33_, _878787_, + _424242_, _545499_, _994242_, _549954_, _995499_, _549999_, + _999954_, _cccccc_. + *alpha* Background translucency. A value in the range 0.0-1.0, where 0.0 means completely transparent, and 1.0 is opaque. Default: _1.0_. diff --git a/foot.ini b/foot.ini index 6ad2085f..c60f4234 100644 --- a/foot.ini +++ b/foot.ini @@ -120,6 +120,24 @@ # ... # 255 = <256-color palette #255> +## Sixel colors +# sixel0 = 000000 +# sixel1 = 3333cc +# sixel2 = cc2121 +# sixel3 = 33cc33 +# sixel4 = cc33cc +# sixel5 = 33cccc +# sixel6 = cccc33 +# sixel7 = 878787 +# sixel8 = 424242 +# sixel9 = 545499 +# sixel10 = 994242 +# sixel11 = 549954 +# sixel12 = 995499 +# sixel13 = 549999 +# sixel14 = 999954 +# sixel15 = cccccc + ## Misc colors # selection-foreground=<inverse foreground/background> # selection-background=<inverse foreground/background> diff --git a/sixel.c b/sixel.c index 20385a93..44a5995b 100644 --- a/sixel.c +++ b/sixel.c @@ -19,29 +19,6 @@ static size_t count; static void sixel_put_generic(struct terminal *term, uint8_t c); static void sixel_put_ar_11(struct terminal *term, uint8_t c); -/* 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) { @@ -105,8 +82,8 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) 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.private_palette, term->conf->colors.sixel, + min(sizeof(term->conf->colors.sixel), term->sixel.palette_size * sizeof(term->sixel.private_palette[0]))); term->sixel.palette = term->sixel.private_palette; @@ -117,8 +94,8 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) 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.shared_palette, term->conf->colors.sixel, + min(sizeof(term->conf->colors.sixel), term->sixel.palette_size * sizeof(term->sixel.shared_palette[0]))); } else { /* Shared palette - do *not* reset palette for new sixels */ @@ -127,12 +104,6 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) term->sixel.palette = term->sixel.shared_palette; } - if (term->sixel.transparent_bg) - term->sixel.default_bg = 0x00000000u; - else - term->sixel.default_bg = term->sixel.palette[0]; - - count = 0; return pan == 1 && pad == 1 ? &sixel_put_ar_11 : &sixel_put_generic; } @@ -1419,7 +1390,7 @@ resize_horizontally(struct terminal *term, int new_width_mutable) /* Width (and thus stride) change - need to allocate a new buffer */ uint32_t *new_data = xmalloc(new_width * alloc_height * sizeof(uint32_t)); - uint32_t bg = term->sixel.default_bg; + uint32_t bg = term->sixel.transparent_bg ? 0 : term->sixel.palette[0]; /* Copy old rows, and initialize new columns to background color */ const uint32_t *end = &new_data[alloc_height * new_width]; @@ -1476,7 +1447,7 @@ resize_vertically(struct terminal *term, const int new_height) return false; } - const uint32_t bg = term->sixel.default_bg; + const uint32_t bg = term->sixel.transparent_bg ? 0 : term->sixel.palette[0]; memset_u32(&new_data[old_height * width], bg, @@ -1529,7 +1500,7 @@ resize(struct terminal *term, int new_width_mutable, int new_height_mutable) xassert(alloc_new_height - new_height < sixel_row_height); uint32_t *new_data = NULL; - const uint32_t bg = term->sixel.default_bg; + const uint32_t bg = term->sixel.transparent_bg ? 0 : term->sixel.palette[0]; /* * If the image is resized horizontally, or if it's opaque, we diff --git a/terminal.h b/terminal.h index e87df54c..06c40977 100644 --- a/terminal.h +++ b/terminal.h @@ -770,7 +770,6 @@ struct terminal { unsigned repeat_count; bool transparent_bg; - uint32_t default_bg; /* Application configurable */ unsigned palette_size; /* Number of colors in palette */ From 996e5fa630b6275c598d8c2c573b9803b1752f83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 23 Oct 2024 08:46:30 +0200 Subject: [PATCH 0911/1323] Revert "url-mode: don't strip the file:// prefix from localhost URIs" This reverts commit 54722369d8ca0c6b90c6d874b0f712e133cd4925. --- url-mode.c | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/url-mode.c b/url-mode.c index c6340e94..20c9820b 100644 --- a/url-mode.c +++ b/url-mode.c @@ -129,17 +129,46 @@ static void activate_url(struct seat *seat, struct terminal *term, const struct url *url, uint32_t serial) { + char *url_string = NULL; + + char *scheme, *host, *path; + if (uri_parse(url->url, strlen(url->url), &scheme, NULL, NULL, + &host, NULL, &path, NULL, NULL)) + { + if (strcmp(scheme, "file") == 0 && hostname_is_localhost(host)) { + /* + * This is a file in *this* computer. Pass only the + * filename to the URL-launcher. + * + * I.e. strip the ‘file://user@host/’ prefix. + */ + url_string = path; + } else + free(path); + + free(scheme); + free(host); + } + + if (url_string == NULL) + url_string = xstrdup(url->url); + switch (url->action) { case URL_ACTION_COPY: - text_to_clipboard(seat, term, xstrdup(url->url), seat->kbd.serial); + if (text_to_clipboard(seat, term, url_string, seat->kbd.serial)) { + /* Now owned by our clipboard “manager” */ + url_string = NULL; + } break; case URL_ACTION_LAUNCH: case URL_ACTION_PERSISTENT: { - spawn_url_launcher(seat, term, url->url, serial); + spawn_url_launcher(seat, term, url_string, serial); break; } } + + free(url_string); } void From d68da27a7f869cb3c66d0e0b52c5559f1ceb3d3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 23 Oct 2024 08:47:21 +0200 Subject: [PATCH 0912/1323] uri: skip query/fragment parsing when dealing with file:// URIs Also, ignore invalid query/fragments (i.e. if the fragment comes before the query). Closes #1840 --- CHANGELOG.md | 4 ++++ uri.c | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f949f1be..f2bd9442 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,9 +85,13 @@ * The `.desktop` files no longer use the reverse DNS naming scheme, and their names now match the default app-ids used by foot (`foot` and `footclient`) ([#1607][1607]). +* `file://` prefix are now stripped from OSC-8 URIs when + activated/opened, **if** the hostname matches the hostname of the + computer foot is running on ([#1840][1840]). [1822]: https://codeberg.org/dnkl/foot/issues/1822 [1607]: https://codeberg.org/dnkl/foot/issues/1607 +[1840]: https://codeberg.org/dnkl/foot/issues/1840 ### Deprecated diff --git a/uri.c b/uri.c index 4de4bd88..e09f7a97 100644 --- a/uri.c +++ b/uri.c @@ -144,6 +144,21 @@ uri_parse(const char *uri, size_t len, const char *query_start = memchr(start, '?', left); const char *fragment_start = memchr(start, '#', left); + if (streq(*scheme, "file")) { + /* Don't try to parse query/fragment in file URIs, just treat + the remaining text as path */ + query_start = NULL; + fragment_start = NULL; + } + + else if (query_start != NULL && fragment_start != NULL && + fragment_start < query_start) + { + /* Invalid URI - for now, ignore, and treat is as part of path */ + query_start = NULL; + fragment_start = NULL; + } + size_t path_len = query_start != NULL ? query_start - start : fragment_start != NULL ? fragment_start - start : From 6d11e93e2ffc7bbffb444b8f8893b96d402ed34d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 23 Oct 2024 13:50:54 +0200 Subject: [PATCH 0913/1323] changelog: prepare for 1.19.0 --- CHANGELOG.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2bd9442..63299258 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.19.0](#1-19-0) * [1.18.1](#1-18-1) * [1.18.0](#1-18-0) * [1.17.2](#1-17-2) @@ -54,7 +54,8 @@ * [1.2.0](#1-2-0) -## Unreleased +## 1.19.0 + ### Added * `resize-keep-grid` option, controlling whether the window is resized @@ -94,8 +95,6 @@ [1840]: https://codeberg.org/dnkl/foot/issues/1840 -### Deprecated -### Removed ### Fixed * Some invalid UTF-8 strings passing the validity check when setting @@ -118,9 +117,13 @@ [1851]: https://codeberg.org/dnkl/foot/issues/1851 -### Security ### Contributors +* Andrew J. Hesford +* Craig Barnes +* Oleh Hushchenkov +* tokyo4j + ## 1.18.1 From cb91fbb4b6065952094613a1cf8f5bb9405dc007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 23 Oct 2024 13:51:15 +0200 Subject: [PATCH 0914/1323] meson: bump version to 1.19.0 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 91e745bf..7a77ddbc 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.18.1', + version: '1.19.0', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From 8edf273f6e22686477aa32e86c0d45774f921962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 23 Oct 2024 13:55:10 +0200 Subject: [PATCH 0915/1323] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63299258..2bfff2f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.19.0](#1-19-0) * [1.18.1](#1-18-1) * [1.18.0](#1-18-0) @@ -54,6 +55,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.19.0 ### Added From 813b514f63e7e36b6c187b3d6062048c8f8bad63 Mon Sep 17 00:00:00 2001 From: Mark Stosberg <mark@stosberg.com> Date: Fri, 25 Oct 2024 08:58:40 -0400 Subject: [PATCH 0916/1323] docs: document more default bindings in search scrollback mode. These were introduced by #1496 but not fully documented then. --- doc/foot.1.scd | 56 +++++++++++++++++++++++++++++++++++++++++++++- doc/foot.ini.5.scd | 2 +- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/doc/foot.1.scd b/doc/foot.1.scd index f5d1686b..fad44f19 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -224,6 +224,8 @@ default) available; see *foot.ini*(5). ## SCROLLBACK SEARCH +These keyboard shortcuts affect the search selection: + *ctrl*+*r* Search _backward_ for the next match. If the search string is empty, the last searched-for string is used. @@ -232,7 +234,13 @@ default) available; see *foot.ini*(5). Search _forward_ for the next match. If the search string is empty, the last searched-for string is used. -*ctrl*+*w* +*shift*+*right* + Extend current selection to the right by one character. + +*shift*+*left* + Extend current selection to the left by one character. + +*ctrl*+*w*, *ctrl*+*shift*+*right* Extend current selection (and thus the search criteria) to the end of the word, or the next word if currently at a word separating character. @@ -241,6 +249,18 @@ default) available; see *foot.ini*(5). Same as *ctrl*+*w*, except that the only word separating characters are whitespace characters. +*ctrl*+*shift*+*left* + Extend current selection to the left to the last word boundary. + +*ctrl*+*shift*+*w* + Extend the current selection to the right to the last whitespace. + +*shift*+*down* + Extend current selection down one line + +*shift*+*up* + Extend current selection up one line. + *ctrl*+*v*, *ctrl*+*shift*+*v*, *ctrl*+*y*, *XF86Paste* Paste from clipboard into the search buffer. @@ -255,6 +275,40 @@ default) available; see *foot.ini*(5). selection. The terminal selection is kept, allowing you to press *ctrl*+*shift*+*c* to copy it to the clipboard. +These shortcuts affect the search box in scrollback-search mode: + +*ctrl*+*b* + Moves the cursor in the search box one **character** to the left. + +*ctrl*+*left*, *alt*+*b* + Moves the cursor in the search box one **word** to the left. + +*ctrl*+*f* + Moves the cursor in the search box one **character** to the right. + +*ctrl*+*right*, *alt*+*f* + Moves the cursor in the search box one **word** to the right. + +*Home*, *ctrl*+*a* + Moves the cursor in the search box to the beginning of the input. + +*End*, *ctrl*+*e* + Moves the cursor in the search box to the end of the input. + +*alt*+*backspace*, *ctrl*+*backspace* + Deletes the **word before** the cursor. + +*alt*+*delete*, *ctrl*+*delete* + Deletes the **word after** the cursor. + +These shortcuts affect scrolling in scrollback-search mode: + +*shift*+*page-up* + Scrolls up/back one page in history. + +*shift*+*page-down* + Scroll down/forward one page in history. + ## URL MODE *t* diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index f3c8f8d0..2d69f13e 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1305,7 +1305,7 @@ scrollback search mode. The syntax is exactly the same as the regular *clipboard-paste* Paste from the _clipboard_ into the search buffer. Default: - _Control+v Control+y_. + _Control+v Control+y Control+Shift+v XF86Paste_. *primary-paste* Paste from the _primary selection_ into the search From 689549bb1f85f2ff7e1710d527dc2ead4cb3bb5b Mon Sep 17 00:00:00 2001 From: Jack Wilsdon <jack@wilsdon.me> Date: Mon, 28 Oct 2024 17:52:09 +0000 Subject: [PATCH 0917/1323] osc: notify: fix crash with no message Closes #1866 --- notify.c | 3 ++- osc.c | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/notify.c b/notify.c index 58388b1c..c77c0606 100644 --- a/notify.c +++ b/notify.c @@ -473,7 +473,8 @@ notify_notify(struct terminal *term, struct notification *notif) "urgency", "muted", "sound-name", "expire-time", "replace-id", "action-argument"}, (const char *[]){ - app_id, term->window_title, icon_name_or_path, title, body, + app_id, term->window_title, icon_name_or_path, title, + body != NULL ? body : "", notif->category != NULL ? notif->category : "", urgency_str, notif->muted ? "true" : "false", notif->sound_name != NULL ? notif->sound_name : "", diff --git a/osc.c b/osc.c index 5efc7588..535e29c8 100644 --- a/osc.c +++ b/osc.c @@ -557,9 +557,13 @@ osc_notify(struct terminal *term, char *string) return; } + char *msgdup = NULL; + if (msg != NULL) + msgdup = xstrdup(msg); + notify_notify(term, &(struct notification){ .title = xstrdup(title), - .body = xstrdup(msg), + .body = msgdup, .expire_time = -1, .focus = true, }); From 4aae5222fe21173537e162e5183dc02764f590dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 31 Oct 2024 07:02:38 +0100 Subject: [PATCH 0918/1323] changelog: osc-9/777 crash when body is empty --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bfff2f9..4ae3b252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,13 @@ ### Deprecated ### Removed ### Fixed + +* Crash when receiving an OSC-9 or OSC-777 with an empty notification + body ([#1866][1866]). + +[1866]: https://codeberg.org/dnkl/foot/issues/1866 + + ### Security ### Contributors From ab3af2af3741dfc7da62b0ddbc377a018ceec30e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 31 Oct 2024 07:26:07 +0100 Subject: [PATCH 0919/1323] unicode: update data files to 16.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Yup, there's no _actual_ changes 🤷 --- CHANGELOG.md | 6 ++++++ unicode/emoji-variation-sequences.txt | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ae3b252..4d37b6e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,12 @@ ## Unreleased ### Added + +* Unicode data files updated to Unicode 16. Foot uses these to + determine which VS-15 and VS-16 sequences are valid, and which are + not. + + ### Changed ### Deprecated ### Removed diff --git a/unicode/emoji-variation-sequences.txt b/unicode/emoji-variation-sequences.txt index d8a3c9f4..43738353 100644 --- a/unicode/emoji-variation-sequences.txt +++ b/unicode/emoji-variation-sequences.txt @@ -1,11 +1,11 @@ # emoji-variation-sequences.txt -# Date: 2023-02-01, 02:22:54 GMT -# © 2023 Unicode®, Inc. +# Date: 2024-05-01, 21:25:24 GMT +# © 2024 Unicode®, Inc. # Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. -# For terms of use, see https://www.unicode.org/terms_of_use.html +# For terms of use and license, see https://www.unicode.org/terms_of_use.html # # Emoji Variation Sequences for UTS #51 -# Used with Emoji Version 15.1 and subsequent minor revisions (if any) +# Used with Emoji Version 16.0 and subsequent minor revisions (if any) # # For documentation and usage, see https://www.unicode.org/reports/tr51 # From f3e443ea4709f94d5a5865d567f5e3647c8cf870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 2 Nov 2024 08:10:25 +0100 Subject: [PATCH 0920/1323] osc: 9: ignore ConEmu/Windows Terminal sequences OSC-9 sequences starting with "<number>;" are now ignored, as they are ConEmu sequences, and not iTerm notifications. --- CHANGELOG.md | 6 ++++++ osc.c | 14 +++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d37b6e6..d7e9939d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,12 @@ ### Changed + +* OSC-9: sequences beginning with `<number>;` are now ignored. These + sequences are ConEmu/Windows Terminal sequences, and not intended to + be notifications. + + ### Deprecated ### Removed ### Fixed diff --git a/osc.c b/osc.c index 535e29c8..72f3c366 100644 --- a/osc.c +++ b/osc.c @@ -1231,10 +1231,22 @@ osc_dispatch(struct terminal *term) osc_uri(term, string); break; - case 9: + case 9: { /* iTerm2 Growl notifications */ + const char *sep = strchr(string, ';'); + if (sep != NULL) { + errno = 0; + char *end = NULL; + strtoul(string, &end, 10); + if (end == sep && errno == 0) { + /* Ignore ConEmu/Windows Terminal escape */ + break; + } + } + osc_notify(term, string); break; + } case 10: /* fg */ case 11: /* bg */ From d3cd4ad933b033e750aa92801945f2cac213f88e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 31 Oct 2024 07:17:35 +0100 Subject: [PATCH 0921/1323] char32: use utf8proc_charwidth() instead of wcwidth(), when available It appears to be slightly more up-to-date with recent Unicode versions. In particular, it handles the new "Symbols for Legacy Computing Supplement" block, introduced in Unicode 16. Closes #1865 --- CHANGELOG.md | 5 +++++ char32.h | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7e9939d..cc734b81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,11 @@ * OSC-9: sequences beginning with `<number>;` are now ignored. These sequences are ConEmu/Windows Terminal sequences, and not intended to be notifications. +* Use `utf8proc_charwidth()` instead of `wcwidth()`+`wcswidth()` when + calculating character width, when foot has been built with utf8proc + support ([#1865][1865]). + +[1865]: https://codeberg.org/dnkl/foot/issues/1865 ### Deprecated diff --git a/char32.h b/char32.h index 6324c9a0..6a5eb080 100644 --- a/char32.h +++ b/char32.h @@ -8,6 +8,10 @@ #include <wchar.h> #include <wctype.h> +#if defined(FOOT_GRAPHEME_CLUSTERING) + #include <utf8proc.h> +#endif + static inline size_t c32len(const char32_t *s) { return wcslen((const wchar_t *)s); } @@ -69,11 +73,22 @@ static inline bool isc32graph(char32_t c32) { } static inline int c32width(char32_t c) { +#if defined(FOOT_GRAPHEME_CLUSTERING) + return utf8proc_charwidth((utf8proc_int32_t)c); +#else return wcwidth((wchar_t)c); +#endif } static inline int c32swidth(const char32_t *s, size_t n) { +#if defined(FOOT_GRAPHEME_CLUSTERING) + int width = 0; + for (size_t i = 0; i < n; i++) + width += utf8proc_charwidth((utf8proc_int32_t)s[i]); + return width; +#else return wcswidth((const wchar_t *)s, n); +#endif } size_t mbsntoc32(char32_t *dst, const char *src, size_t nms, size_t len); From b43f19cb50d8f617145ab7825e3df1d6a9ff299c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 2 Nov 2024 20:11:14 +0100 Subject: [PATCH 0922/1323] vt: don't call fcft_precompose() if font is NULL This fixes a crash when doing a partial PGO build (where we don't have any fonts available). --- vt.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/vt.c b/vt.c index 572dd2af..95cfdd2e 100644 --- a/vt.c +++ b/vt.c @@ -763,9 +763,11 @@ action_utf8_print(struct terminal *term, char32_t wc) bool comb_from_primary; bool pre_from_primary; - char32_t precomposed = fcft_precompose( - term->fonts[0], base, wc, &base_from_primary, - &comb_from_primary, &pre_from_primary); + char32_t precomposed = term->fonts[0] != NULL + ? fcft_precompose( + term->fonts[0], base, wc, &base_from_primary, + &comb_from_primary, &pre_from_primary) + : (char32_t)-1; int precomposed_width = c32width(precomposed); From 256749c6d0ae0a26ad93c973e2e5623213807bdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 24 Nov 2024 08:01:31 +0100 Subject: [PATCH 0923/1323] term: get_font_dpi(): remove invalid assertion Closes #1874 --- terminal.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/terminal.c b/terminal.c index 6d62a3ed..2eceeda4 100644 --- a/terminal.c +++ b/terminal.c @@ -884,8 +884,6 @@ get_font_dpi(const struct terminal *term) * 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 = NULL; From de305a7e58950d243b1e170cbb3df4ddbc8202e0 Mon Sep 17 00:00:00 2001 From: heather7283 <heather7283@protonmail.com> Date: Tue, 19 Nov 2024 19:59:06 +0400 Subject: [PATCH 0924/1323] pgo: run sway with --unsupported-gpu flag Sway refuses to run if it detects an nvidia GPU on the system, causing pgo build to fail. Adding --unsupported-gpu flag disables this behaviour. --- pgo/full-headless-sway.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgo/full-headless-sway.sh b/pgo/full-headless-sway.sh index 524bf42b..8f6812b3 100755 --- a/pgo/full-headless-sway.sh +++ b/pgo/full-headless-sway.sh @@ -18,7 +18,7 @@ trap cleanup EXIT INT HUP TERM > "${sway_conf}" echo "exec '${srcdir}'/pgo/full-headless-sway-inner.sh '${srcdir}' '${blddir}'" # Run Sway. full-headless-sway-inner.sh ends with a 'swaymsg exit' -XDG_RUNTIME_DIR="${runtime_dir}" WLR_RENDERER=pixman WLR_BACKENDS=headless sway -c "${sway_conf}" +XDG_RUNTIME_DIR="${runtime_dir}" WLR_RENDERER=pixman WLR_BACKENDS=headless sway -c "${sway_conf}" --unsupported-gpu # Sway's exit code doesn't reflect our script's exit code [ -f "${blddir}"/pgo-ok ] || exit 1 From 7e88e0bfdc3b0659bb763fde2cc11726f5b1132d Mon Sep 17 00:00:00 2001 From: heather7283 <heather7283@protonmail.com> Date: Tue, 19 Nov 2024 20:06:32 +0400 Subject: [PATCH 0925/1323] pgo: explicitly set LLVM_PROFILE_FILE envvar When building foot with pgo under gentoo's portage, LLVM tried to generate profile data files directly under system root, triggering sandbox violation and causing build to fail. Setting this envvar fixes the issue by explicitly specifying profiling data location. Reference: https://clang.llvm.org/docs/UsersManual.html#profiling-with-instrumentation --- pgo/pgo.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/pgo/pgo.sh b/pgo/pgo.sh index b59f5c21..24891438 100755 --- a/pgo/pgo.sh +++ b/pgo/pgo.sh @@ -103,6 +103,7 @@ if [ ${do_pgo} = yes ]; then ninja -C "${blddir}" test # Run mode-dependent script to generate profiling data + export LLVM_PROFILE_FILE="${blddir}/default_%m.profraw" "${srcdir}"/pgo/${mode}.sh "${srcdir}" "${blddir}" if [ ${compiler} = clang ]; then From ca13c7b4f5761e3ef258410b60308c7867926feb Mon Sep 17 00:00:00 2001 From: Denis Zharikov <d2718nis@gmail.com> Date: Tue, 26 Nov 2024 23:05:07 +0400 Subject: [PATCH 0926/1323] Update rose-pine, add theme variants --- themes/rose-pine | 38 +++++++++++++++++++++----------------- themes/rose-pine-dawn | 30 ++++++++++++++++++++++++++++++ themes/rose-pine-moon | 30 ++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 17 deletions(-) create mode 100644 themes/rose-pine-dawn create mode 100644 themes/rose-pine-moon diff --git a/themes/rose-pine b/themes/rose-pine index 6b58a66c..78d77dd9 100644 --- a/themes/rose-pine +++ b/themes/rose-pine @@ -1,5 +1,5 @@ # -*- conf -*- -# Rose-Piné +# Rosé Pine [cursor] color=191724 e0def4 @@ -7,20 +7,24 @@ color=191724 e0def4 [colors] background=191724 foreground=e0def4 -regular0=26233a # black -regular1=eb6f92 # red -regular2=31748f # green -regular3=f6c177 # yellow -regular4=9ccfd8 # blue -regular5=c4a7e7 # magenta -regular6=ebbcba # cyan -regular7=e0def4 # white -bright0=6e6a86 # bright black -bright1=eb6f92 # bright red -bright2=31748f # bright green -bright3=f6c177 # bright yellow -bright4=9ccfd8 # bright blue -bright5=c4a7e7 # bright magenta -bright6=ebbcba # bright cyan -bright7=e0def4 # bright white \ No newline at end of file +regular0=26233a # black (Overlay) +regular1=eb6f92 # red (Love) +regular2=9ccfd8 # green (Foam) +regular3=f6c177 # yellow (Gold) +regular4=31748f # blue (Pine) +regular5=c4a7e7 # magenta (Iris) +regular6=ebbcba # cyan (Rose) +regular7=e0def4 # white (Text) + +bright0=47435d # bright black (lighter Overlay) +bright1=ff98ba # bright red (lighter Love) +bright2=c5f9ff # bright green (lighter Foam) +bright3=ffeb9e # bright yellow (lighter Gold) +bright4=5b9ab7 # bright blue (lighter Pine) +bright5=eed0ff # bright magenta (lighter Iris) +bright6=ffe5e3 # bright cyan (lighter Rose) +bright7=fefcff # bright white (lighter Text) + +flash=f6c177 # yellow (Gold) + diff --git a/themes/rose-pine-dawn b/themes/rose-pine-dawn new file mode 100644 index 00000000..52008b44 --- /dev/null +++ b/themes/rose-pine-dawn @@ -0,0 +1,30 @@ +# -*- conf -*- +# Rosé Pine Dawn + +[cursor] +color=faf4ed 575279 + +[colors] +background=faf4ed +foreground=575279 + +regular0=f2e9e1 # black (Overlay) +regular1=b4637a # red (Love) +regular2=56949f # green (Foam) +regular3=ea9d34 # yellow (Gold) +regular4=286983 # blue (Pine) +regular5=907aa9 # magenta (Iris) +regular6=d7827e # cyan (Rose) +regular7=575279 # white (Text) + +bright0=fffdf5 # bright black (lighter Overlay) +bright1=df8aa0 # bright red (lighter Love) +bright2=7ebcc7 # bright green (lighter Foam) +bright3=ffc55c # bright yellow (lighter Gold) +bright4=538faa # bright blue (lighter Pine) +bright5=b8a1d2 # bright magenta (lighter Iris) +bright6=ffaaa5 # bright cyan (lighter Rose) +bright7=7c76a0 # bright white (lighter Text) + +flash=ea9d34 # yellow (Gold) + diff --git a/themes/rose-pine-moon b/themes/rose-pine-moon new file mode 100644 index 00000000..732e5943 --- /dev/null +++ b/themes/rose-pine-moon @@ -0,0 +1,30 @@ +# -*- conf -*- +# Rosé Pine Moon + +[cursor] +color=232136 e0def4 + +[colors] +background=232136 +foreground=e0def4 + +regular0=393552 # black (Overlay) +regular1=eb6f92 # red (Love) +regular2=9ccfd8 # green (Foam) +regular3=f6c177 # yellow (Gold) +regular4=3e8fb0 # blue (Pine) +regular5=c4a7e7 # magenta (Iris) +regular6=ea9a97 # cyan (Rose) +regular7=e0def4 # white (Text) + +bright0=5c5776 # bright black (lighter Overlay) +bright1=ff98ba # bright red (lighter Love) +bright2=c5f9ff # bright green (lighter Foam) +bright3=ffeb9e # bright yellow (lighter Gold) +bright4=6ab7d9 # bright blue (lighter Pine) +bright5=eed0ff # bright magenta (lighter Iris) +bright6=ffc3bf # bright cyan (lighter Rose) +bright7=fefcff # bright white (lighter Text) + +flash=f6c177 # yellow (Gold) + From 5034209087ed09c6e14c940bc4e05b9d47c6f37d Mon Sep 17 00:00:00 2001 From: cy <hi@cything.io> Date: Tue, 3 Dec 2024 00:13:28 -0500 Subject: [PATCH 0927/1323] quit should be in key-bindings --- foot.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foot.ini b/foot.ini index c60f4234..dccc38fe 100644 --- a/foot.ini +++ b/foot.ini @@ -193,6 +193,7 @@ # prompt-next=Control+Shift+x # unicode-input=Control+Shift+u # noop=none +# quit=none [search-bindings] # cancel=Control+g Control+c Escape @@ -220,7 +221,6 @@ # 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 From 768f254286cf1c2b447efc59cf99fabcd940ac48 Mon Sep 17 00:00:00 2001 From: heather7283 <heather7283@protonmail.com> Date: Sun, 8 Dec 2024 13:54:45 +0400 Subject: [PATCH 0928/1323] pgo: prefer full-headless-sway over full-headless-cage --- pgo/pgo.sh | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pgo/pgo.sh b/pgo/pgo.sh index 24891438..24597c82 100755 --- a/pgo/pgo.sh +++ b/pgo/pgo.sh @@ -54,13 +54,10 @@ case ${mode} in ;; auto) - # TODO: once Sway 1.6.2 has been released, prefer - # full-headless-sway - if [ -n "${WAYLAND_DISPLAY+x}" ]; then mode=full-current-session - # elif command -v sway > /dev/null; then # Requires 1.6.2 - # mode=full-headless-sway + elif command -v sway > /dev/null; then + mode=full-headless-sway elif command -v cage > /dev/null; then mode=full-headless-cage else From 9a1b59adaec22434473789953884c9b6db58e9a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 8 Dec 2024 09:05:41 +0100 Subject: [PATCH 0929/1323] box-drawings: implement octants --- CHANGELOG.md | 3 + box-drawing.c | 450 ++++++++++++++++++++++++++++++++++++++++++++++++-- render.c | 16 +- terminal.c | 4 + terminal.h | 6 + 5 files changed, 467 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc734b81..242f3bab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,9 @@ * Unicode data files updated to Unicode 16. Foot uses these to determine which VS-15 and VS-16 sequences are valid, and which are not. +* Box drawing characters U+1CD00...U+1CDE5 (the _"octants"_ from the + _"Symbols for Legacy Computing Supplement"_ codepoint range, added + in Unicode 16.0). ### Changed diff --git a/box-drawing.c b/box-drawing.c index d1ce1af0..1c613051 100644 --- a/box-drawing.c +++ b/box-drawing.c @@ -33,9 +33,12 @@ struct buf { int thickness[2]; - /* For sextants and wedges */ + /* For octants, sextants and wedges */ int x_halfs[2]; int y_thirds[2]; + + /* For octants */ + int y_quads[3]; }; static const pixman_color_t white = {0xffff, 0xffff, 0xffff, 0xffff}; @@ -2213,6 +2216,7 @@ draw_sextant(struct buf *buf, char32_t wc) LOWER_RIGHT = 1 << 5, }; + /* TODO: move this to a separate file? */ static const uint8_t matrix[60] = { /* U+1fb00 - U+1fb0f */ UPPER_LEFT, @@ -2308,6 +2312,398 @@ draw_sextant(struct buf *buf, char32_t wc) sextant_lower_right(buf); } +static void +octant_upper_left(struct buf *buf) +{ + rect(0, 0, buf->x_halfs[0], buf->y_quads[0]); +} + +static void +octant_middle_up_left(struct buf *buf) +{ + rect(0, buf->y_quads[0], buf->x_halfs[0], buf->y_quads[1]); +} + +static void +octant_middle_down_left(struct buf *buf) +{ + rect(0, buf->y_quads[1], buf->x_halfs[0], buf->y_quads[2]); +} + +static void +octant_lower_left(struct buf *buf) +{ + rect(0, buf->y_quads[2], buf->x_halfs[0], buf->height); +} + +static void +octant_upper_right(struct buf *buf) +{ + rect(buf->x_halfs[1], 0, buf->width, buf->y_quads[0]); +} + +static void +octant_middle_up_right(struct buf *buf) +{ + rect(buf->x_halfs[1], buf->y_quads[0], buf->width, buf->y_quads[1]); +} + +static void +octant_middle_down_right(struct buf *buf) +{ + rect(buf->x_halfs[1], buf->y_quads[1], buf->width, buf->y_quads[2]); +} + +static void +octant_lower_right(struct buf *buf) +{ + rect(buf->x_halfs[1], buf->y_quads[2], buf->width, buf->height); +} + +static void NOINLINE +draw_octant(struct buf *buf, char32_t wc) +{ + /* + * Each byte encodes one octant: + * + * Bit octant part + * 0 upper left + * 1 middle, upper left + * 2 middle, lower left + * 3 lower, left + * 4 upper right + * 5 middle, upper right + * 6 middle, lower right + * 7 lower right + */ + enum { + UPPER_LEFT = 1 << 0, + MIDDLE_UP_LEFT = 1 << 1, + MIDDLE_DOWN_LEFT = 1 << 2, + LOWER_LEFT = 1 << 3, + UPPER_RIGHT = 1 << 4, + MIDDLE_UP_RIGHT = 1 << 5, + MIDDLE_DOWN_RIGHT = 1 << 6, + LOWER_RIGHT = 1 << 7, + }; + + /* TODO: move this to a separate file */ + static const uint8_t matrix[230] = { + /* U+1CD00 - U+1CD0F */ + MIDDLE_UP_LEFT, + MIDDLE_UP_LEFT | UPPER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | UPPER_RIGHT, + MIDDLE_UP_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT, + MIDDLE_DOWN_LEFT, + UPPER_LEFT | MIDDLE_DOWN_LEFT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT, + + /* U+1CD10 - U+1CD1F */ + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + MIDDLE_DOWN_RIGHT, + UPPER_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT, + + /* U+1CD20 - U+1CD2F */ + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, + MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + + /* U+1CD30 - U+1CD3F */ + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | LOWER_LEFT, + UPPER_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | LOWER_LEFT, + MIDDLE_UP_LEFT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_LEFT, + MIDDLE_UP_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_LEFT, + + /* U+1CD40 - U+1CD4F */ + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_LEFT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + + /* U+1CD50 - U+1CD5F */ + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + + /* U+1CD60 - U+1CD6F */ + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + + /* U+1CD70 - U+1CD7F */ + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | LOWER_RIGHT, + MIDDLE_UP_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_RIGHT, + + /* U+1CD80 - U+1CD8F */ + MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + + /* U+1CD90 - U+1CD9F */ + UPPER_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + + /* U+1CDA0 - U+1CDAF */ + MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | LOWER_LEFT | LOWER_RIGHT, + + /* U+1CDB0 - U+1CDBF */ + UPPER_LEFT | MIDDLE_UP_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + + /* U+1CDC0 - U+1CDCF */ + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + + /* U+1CDD0 - U+1CDDF */ + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + + /* U+1CDE0 - U+1CDE5 */ + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + }; + + _Static_assert(ALEN(matrix) == 230, "incorrect number of codepoints"); + +#if defined(_DEBUG) + const size_t last_implemented = 0x1cde5; + for (size_t i = 0; i < sizeof(matrix) / sizeof(matrix[0]); i++) { + if (i + 0x1cd00 > last_implemented) + break; + + for (size_t j = 0; j < sizeof(matrix) / sizeof(matrix[0]); j++) { + if (j + 0x1cd00 > last_implemented) + break; + + if (i == j) + continue; + + if (matrix[i] == matrix[j]) { + BUG("octant U+%05x (idx=%zu) is the same as U+%05x (idx=%zu)", + matrix[i], i, matrix[j], j); + } + } + } +#endif + + xassert(wc >= 0x1cd00 && wc <= 0x1cde5); + const size_t idx = wc - 0x1cd00; + + xassert(idx < ALEN(matrix)); + uint8_t encoded = matrix[idx]; + + if (encoded & UPPER_LEFT) + octant_upper_left(buf); + + if (encoded & MIDDLE_UP_LEFT) + octant_middle_up_left(buf); + + if (encoded & MIDDLE_DOWN_LEFT) + octant_middle_down_left(buf); + + if (encoded & LOWER_LEFT) + octant_lower_left(buf); + + if (encoded & UPPER_RIGHT) + octant_upper_right(buf); + + if (encoded & MIDDLE_UP_RIGHT) + octant_middle_up_right(buf); + + if (encoded & MIDDLE_DOWN_RIGHT) + octant_middle_down_right(buf); + + if (encoded & LOWER_RIGHT) + octant_lower_right(buf); +} + static void NOINLINE draw_wedge_triangle(struct buf *buf, char32_t wc) { @@ -2856,6 +3252,7 @@ draw_glyph(struct buf *buf, char32_t wc) case 0x2800 ... 0x28ff: draw_braille(buf, wc); break; + case 0x1cd00 ... 0x1cde5: draw_octant(buf, wc); break; case 0x1fb00 ... 0x1fb3b: draw_sextant(buf, wc); break; case 0x1fb3c ... 0x1fb40: @@ -2957,24 +3354,51 @@ box_drawing(const struct terminal *term, char32_t wc) (double)term->conf->tweak.box_drawing_base_thickness * scale * cell_size * dpi / 72.0; base_thickness = max(base_thickness, 1); - int y0 = 0, y1 = 0; + int y_third_0 = 0, y_third_1 = 0; switch (height % 3) { case 0: - y0 = height / 3; - y1 = 2 * height / 3; + y_third_0 = height / 3; + y_third_1 = 2 * height / 3; break; case 1: - y0 = height / 3; - y1 = 2 * height / 3 + 1; + y_third_0 = height / 3; + y_third_1 = 2 * height / 3 + 1; break; case 2: - y0 = height / 3 + 1; - y1 = y0 + height / 3; + y_third_0 = height / 3 + 1; + y_third_1 = y_third_0 + height / 3; break; } + /* TODO */ + int y_quad_0 = 0, y_quad_1 = 0, y_quad_2 = 0; + switch (height % 4) { + case 0: + y_quad_0 = height / 4; + y_quad_1 = height / 2; + y_quad_2 = 3 * height / 4; + break; + + case 1: + y_quad_0 = height / 4; + y_quad_1 = height / 2; + y_quad_2 = 3 * height / 4; + break; + case 2: + y_quad_0 = height / 4; + y_quad_1 = height / 2; + y_quad_2 = 3 * height / 4; + break; + + case 3: + y_quad_0 = height / 4; + y_quad_1 = height / 2; + y_quad_2 = 3 * height / 4; + break; + } + struct buf buf = { .data = data, .pix = pix, @@ -2996,8 +3420,14 @@ box_drawing(const struct terminal *term, char32_t wc) }, .y_thirds = { - y0, /* Endpoint first third, start point second third */ - y1, /* Endpoint second third, start point last third */ + y_third_0, /* Endpoint first third, start point second third */ + y_third_1, /* Endpoint second third, start point last third */ + }, + + .y_quads = { + y_quad_0, + y_quad_1, + y_quad_2, }, }; diff --git a/render.c b/render.c index d14c70ce..ed6b802e 100644 --- a/render.c +++ b/render.c @@ -801,7 +801,15 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag * Note, the full range is U+1FB00 - U+1FBF9 */ (base >= GLYPH_LEGACY_FIRST && - base <= GLYPH_LEGACY_LAST)) && + base <= GLYPH_LEGACY_LAST) || + + /* + * Unicode 16 "Symbols for Legacy Computing Supplement" + * + * Note, the full range is U+1CC00 - U+1CEAF + */ + (base >= GLYPH_OCTANTS_FIRST && + base <= GLYPH_OCTANTS_LAST)) && likely(!term->conf->box_drawings_uses_font_glyphs)) { @@ -809,7 +817,11 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag size_t count; size_t idx; - if (base >= GLYPH_LEGACY_FIRST) { + if (base >= GLYPH_OCTANTS_FIRST) { + arr = &term->custom_glyphs.octants; + count = GLYPH_OCTANTS_COUNT; + idx = base - GLYPH_OCTANTS_FIRST; + } else if (base >= GLYPH_LEGACY_FIRST) { arr = &term->custom_glyphs.legacy; count = GLYPH_LEGACY_COUNT; idx = base - GLYPH_LEGACY_FIRST; diff --git a/terminal.c b/terminal.c index 2eceeda4..5532972e 100644 --- a/terminal.c +++ b/terminal.c @@ -808,6 +808,8 @@ term_set_fonts(struct terminal *term, struct fcft_font *fonts[static 4], &term->custom_glyphs.braille, GLYPH_BRAILLE_COUNT); free_custom_glyphs( &term->custom_glyphs.legacy, GLYPH_LEGACY_COUNT); + free_custom_glyphs( + &term->custom_glyphs.octants, GLYPH_OCTANTS_COUNT); const struct config *conf = term->conf; @@ -1827,6 +1829,8 @@ term_destroy(struct terminal *term) &term->custom_glyphs.braille, GLYPH_BRAILLE_COUNT); free_custom_glyphs( &term->custom_glyphs.legacy, GLYPH_LEGACY_COUNT); + free_custom_glyphs( + &term->custom_glyphs.octants, GLYPH_OCTANTS_COUNT); free(term->search.buf); free(term->search.last.buf); diff --git a/terminal.h b/terminal.h index 06c40977..d3c7f335 100644 --- a/terminal.h +++ b/terminal.h @@ -483,6 +483,7 @@ struct terminal { struct fcft_glyph **box_drawing; struct fcft_glyph **braille; struct fcft_glyph **legacy; + struct fcft_glyph **octants; #define GLYPH_BOX_DRAWING_FIRST 0x2500 #define GLYPH_BOX_DRAWING_LAST 0x259F @@ -498,6 +499,11 @@ struct terminal { #define GLYPH_LEGACY_LAST 0x1FB9B #define GLYPH_LEGACY_COUNT \ (GLYPH_LEGACY_LAST - GLYPH_LEGACY_FIRST + 1) + + #define GLYPH_OCTANTS_FIRST 0x1CD00 + #define GLYPH_OCTANTS_LAST 0x1CDE5 + #define GLYPH_OCTANTS_COUNT \ + (GLYPH_OCTANTS_LAST - GLYPH_OCTANTS_FIRST + 1) } custom_glyphs; bool is_sending_paste_data; From d523e7a676d6d2cdda0369fac3dc9f953d3a4e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 17 Dec 2024 11:01:17 +0100 Subject: [PATCH 0930/1323] term: set_app_id() + set_window_title(): only allow printable characters --- CHANGELOG.md | 2 ++ misc.c | 17 ++++++++++++++--- misc.h | 2 +- terminal.c | 4 ++-- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 242f3bab..f9c665fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,8 @@ * Use `utf8proc_charwidth()` instead of `wcwidth()`+`wcswidth()` when calculating character width, when foot has been built with utf8proc support ([#1865][1865]). +* Run-time changes to the window title, and the app ID now require the + new value to consist of printable characters only. [1865]: https://codeberg.org/dnkl/foot/issues/1865 diff --git a/misc.c b/misc.c index 1e5b9328..c7abe03b 100644 --- a/misc.c +++ b/misc.c @@ -44,8 +44,19 @@ timespec_sub(const struct timespec *a, const struct timespec *b, } bool -is_valid_utf8(const char *value) +is_valid_utf8_and_printable(const char *value) { - return value != NULL && - mbsntoc32(NULL, value, strlen(value), 0) != (size_t)-1; + char32_t *wide = ambstoc32(value); + if (wide == NULL) + return false; + + for (const char32_t *c = wide; *c != U'\0'; c++) { + if (!isc32print(*c)) { + free(wide); + return false; + } + } + + free(wide); + return true; } diff --git a/misc.h b/misc.h index cce8d2c1..6c77c484 100644 --- a/misc.h +++ b/misc.h @@ -9,4 +9,4 @@ bool isword(char32_t wc, bool spaces_only, const char32_t *delimiters); void timespec_add(const struct timespec *a, const struct timespec *b, struct timespec *res); void timespec_sub(const struct timespec *a, const struct timespec *b, struct timespec *res); -bool is_valid_utf8(const char *value); +bool is_valid_utf8_and_printable(const char *value); diff --git a/terminal.c b/terminal.c index 5532972e..a4963bc8 100644 --- a/terminal.c +++ b/terminal.c @@ -3570,7 +3570,7 @@ term_set_window_title(struct terminal *term, const char *title) if (term->window_title != NULL && streq(term->window_title, title)) return; - if (!is_valid_utf8(title)) { + if (!is_valid_utf8_and_printable(title)) { /* It's an xdg_toplevel::set_title() protocol violation to set a title with an invalid UTF-8 sequence */ LOG_WARN("%s: title is not valid UTF-8, ignoring", title); @@ -3593,7 +3593,7 @@ term_set_app_id(struct terminal *term, const char *app_id) if (term->app_id != NULL && app_id != NULL && streq(term->app_id, app_id)) return; - if (app_id != NULL && !is_valid_utf8(app_id)) { + if (app_id != NULL && !is_valid_utf8_and_printable(app_id)) { LOG_WARN("%s: app-id is not valid UTF-8, ignoring", app_id); return; } From 3b0c2a354376639e9cf84932b7101d84907ea8fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 20 Dec 2024 15:22:14 +0100 Subject: [PATCH 0931/1323] misc: add missing include stdlib.h (for free()) Closes #1887 --- misc.c | 1 + 1 file changed, 1 insertion(+) diff --git a/misc.c b/misc.c index c7abe03b..1369df03 100644 --- a/misc.c +++ b/misc.c @@ -1,5 +1,6 @@ #include "misc.h" #include "char32.h" +#include <stdlib.h> bool isword(char32_t wc, bool spaces_only, const char32_t *delimiters) From 67bd5dd460d646d49b9959ce3190272f2bfe06be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 22 Dec 2024 07:09:37 +0100 Subject: [PATCH 0932/1323] selection: fix crash when tripple clicking in a region containing NUL characters If a cell contains a NUL character, it was incorrectly treated as a quote, later triggering an assertion. Patch by Johannes Altmanninger --- CHANGELOG.md | 1 + selection.c | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9c665fa..b55fe9a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,7 @@ * Crash when receiving an OSC-9 or OSC-777 with an empty notification body ([#1866][1866]). +* Crash when tripple-clicking on region containing `NUL` characters. [1866]: https://codeberg.org/dnkl/foot/issues/1866 diff --git a/selection.c b/selection.c index 91c851d2..d7aa617a 100644 --- a/selection.c +++ b/selection.c @@ -533,8 +533,8 @@ selection_find_quote_left(struct terminal *term, struct coord *pos, const struct row *row = grid_row_in_view(term->grid, pos->row); char32_t wc = row->cells[pos->col].wc; - if ((*quote_char == '\0' && (wc == '"' || wc == '\'')) || - wc == *quote_char) + if (*quote_char == '\0' ? (wc == '"' || wc == '\'') + : wc == *quote_char) { return false; } @@ -555,8 +555,8 @@ selection_find_quote_left(struct terminal *term, struct coord *pos, wc = row->cells[next_col].wc; - if ((*quote_char == '\0' && (wc == '"' || wc == '\'')) || - wc == *quote_char) + if (*quote_char == '\0' ? (wc == '"' || wc == '\'') + : wc == *quote_char) { pos->row = next_row; pos->col = next_col + 1; From e38ec79be15fe355bf50670bc9066210ae0ed211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 21 Dec 2024 06:52:00 +0100 Subject: [PATCH 0933/1323] osc: add option to disable OSC-52, partially or fully Closes #1867 --- CHANGELOG.md | 3 +++ config.c | 24 ++++++++++++++++++++++++ config.h | 9 +++++++++ doc/foot.ini.5.scd | 27 +++++++++++++++++++++++++++ foot.ini | 3 +++ osc.c | 17 ++++++++++++++++- tests/test-config.c | 17 +++++++++++++++++ 7 files changed, 99 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b55fe9a5..bbe24067 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,9 @@ * Box drawing characters U+1CD00...U+1CDE5 (the _"octants"_ from the _"Symbols for Legacy Computing Supplement"_ codepoint range, added in Unicode 16.0). +* `security.osc52` option, allowing you to partially or fully disable + host clipboard access via the OSC-52 escape sequence + ([#1867][1867]). ### Changed diff --git a/config.c b/config.c index 7c4ac0d1..7f1ce055 100644 --- a/config.c +++ b/config.c @@ -1110,6 +1110,25 @@ parse_section_main(struct context *ctx) } } +static bool +parse_section_security(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "osc52")) { + _Static_assert(sizeof(conf->security.osc52) == sizeof(int), + "enum is not 32-bit"); + return value_to_enum( + ctx, + (const char *[]){"disabled", "copy-enabled", "paste-enabled", "enabled", NULL}, + (int *)&conf->security.osc52); + } else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + static bool parse_section_bell(struct context *ctx) { @@ -2742,6 +2761,7 @@ parse_key_value(char *kv, const char **section, const char **key, const char **v enum section { SECTION_MAIN, + SECTION_SECURITY, SECTION_BELL, SECTION_DESKTOP_NOTIFICATIONS, SECTION_SCROLLBACK, @@ -2769,6 +2789,7 @@ static const struct { const char *name; } section_info[] = { [SECTION_MAIN] = {&parse_section_main, "main"}, + [SECTION_SECURITY] = {&parse_section_security, "security"}, [SECTION_BELL] = {&parse_section_bell, "bell"}, [SECTION_DESKTOP_NOTIFICATIONS] = {&parse_section_desktop_notifications, "desktop-notifications"}, [SECTION_SCROLLBACK] = {&parse_section_scrollback, "scrollback"}, @@ -3154,6 +3175,9 @@ config_load(struct config *conf, const char *conf_path, .underline_thickness = {.pt = 0., .px = -1}, .strikeout_thickness = {.pt = 0., .px = -1}, .dpi_aware = false, + .security = { + .osc52 = OSC52_ENABLED, + }, .bell = { .urgent = false, .notify = false, diff --git a/config.h b/config.h index adb9637c..d7192970 100644 --- a/config.h +++ b/config.h @@ -173,6 +173,15 @@ struct config { bool box_drawings_uses_font_glyphs; bool can_shape_grapheme; + struct { + enum { + OSC52_DISABLED, + OSC52_COPY_ENABLED, + OSC52_PASTE_ENABLED, + OSC52_ENABLED, + } osc52; + } security; + struct { bool urgent; bool notify; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 2d69f13e..ba59050e 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -416,6 +416,33 @@ The format is simply: Note: do not set *TERM* here; use the *term* option in the main (default) section instead. +# SECTION: security + +*osc52* + + Whether OSC-52 (clipboard access) is enabled or disabled. One of + *disabled*, *copy-enabled*, *paste-enabled* or *enabled*. + + OSC-52 gives terminal application access to the host clipboard + (i.e. the Wayland clipboard). This is normally not a security + issue, since all applications can access the clipboard directly + over the Wayland socket. + + However, when SSH:ing into a remote system, or accessing a + container etc, the terminal applications may be untrusted, and you + might consider disabling the host clipboard access. + + - *disabled*: disables all clipboard access + - *copy-enabled*: applications can write to the clipboard, but not + read from it. + - *paste-enabled*: applications can read from the clipboard, but + not write to it. + - *enabled*: all applications have full access to the host + clipboard. This is the default. + + Default: _enabled_ + + # SECTION: bell *urgent* diff --git a/foot.ini b/foot.ini index dccc38fe..bd4ac082 100644 --- a/foot.ini +++ b/foot.ini @@ -41,6 +41,9 @@ [environment] # name=value +[security] +# osc52=enabled # disabled|copy-enabled|paste-enabled|enabled + [bell] # urgent=no # notify=no diff --git a/osc.c b/osc.c index 72f3c366..2c02f53a 100644 --- a/osc.c +++ b/osc.c @@ -8,7 +8,7 @@ #include <sys/epoll.h> #define LOG_MODULE "osc" -#define LOG_ENABLE_DBG 0 +#define LOG_ENABLE_DBG 1 #include "log.h" #include "base64.h" #include "config.h" @@ -64,6 +64,14 @@ osc_to_clipboard(struct terminal *term, const char *target, return; } + const bool copy_allowed = term->conf->security.osc52 == OSC52_ENABLED + || term->conf->security.osc52 == OSC52_COPY_ENABLED; + + if (!copy_allowed) { + LOG_DBG("ignoring copy request: disabled in configuration"); + return; + } + char *decoded = base64_decode(base64_data, NULL); if (decoded == NULL) { if (errno == EINVAL) @@ -190,6 +198,13 @@ osc_from_clipboard(struct terminal *term, const char *source) return; } + const bool paste_allowed = term->conf->security.osc52 == OSC52_ENABLED + || term->conf->security.osc52 == OSC52_PASTE_ENABLED; + if (!paste_allowed) { + LOG_DBG("ignoring paste request: disabled in configuration"); + return; + } + /* Use clipboard if no source has been specified */ char src = source[0] == '\0' ? 'c' : 0; bool from_clipboard = src == 'c'; diff --git a/tests/test-config.c b/tests/test-config.c index 61999686..a189d440 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -553,6 +553,22 @@ test_section_main(void) config_free(&conf); } +static void +test_section_security(void) +{ + struct config conf = {0}; + struct context ctx = {.conf = &conf, .section = "security", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_security, "invalid-key"); + test_enum( + &ctx, &parse_section_security, "osc52", 4, + (const char*[]){"disabled", "copy-enabled", "paste-enabled", "enabled"}, + (int []){OSC52_DISABLED, OSC52_COPY_ENABLED, OSC52_PASTE_ENABLED, OSC52_ENABLED}, + (int *)&conf.security.osc52); + + config_free(&conf); +} + static void test_section_bell(void) { @@ -1407,6 +1423,7 @@ main(int argc, const char *const *argv) FcInit(); log_init(LOG_COLORIZE_AUTO, false, 0, LOG_CLASS_ERROR); test_section_main(); + test_section_security(); test_section_bell(); test_section_desktop_notifications(); test_section_scrollback(); From e851d44ac949df533a31b7e40a6ab5752dac79f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 1 Jan 2025 08:06:52 +0100 Subject: [PATCH 0934/1323] kitty kbd: don't generate release events for plain Enter+Tab+Backspace From the specification: The Enter, Tab and Backspace keys will not have release events unless Report all keys as escape codes is also set, so that the user can still type reset at a shell prompt when a program that sets this mode ends without resetting it. Closes #1892 --- CHANGELOG.md | 4 ++++ input.c | 17 ++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbe24067..1eb95c14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,8 +79,12 @@ support ([#1865][1865]). * Run-time changes to the window title, and the app ID now require the new value to consist of printable characters only. +* Kitty keyboard protocol: Enter, Tab and Backspace no longer report + _release_ events unless _"Report all keys as escape codes"_ is + enabled ([#1892][1892]). [1865]: https://codeberg.org/dnkl/foot/issues/1865 +[1892]: https://codeberg.org/dnkl/foot/issues/1892 ### Deprecated diff --git a/input.c b/input.c index 201f5c69..51425a36 100644 --- a/input.c +++ b/input.c @@ -1236,9 +1236,20 @@ kitty_kbd_protocol(struct seat *seat, struct terminal *term, if ((mods & ~locked & ~consumed) == 0) { switch (sym) { - case XKB_KEY_Return: term_to_slave(term, "\r", 1); return true; - case XKB_KEY_BackSpace: term_to_slave(term, "\x7f", 1); return true; - case XKB_KEY_Tab: term_to_slave(term, "\t", 1); return true; + case XKB_KEY_Return: + if (!released) + term_to_slave(term, "\r", 1); + return true; + + case XKB_KEY_BackSpace: + if (!released) + term_to_slave(term, "\x7f", 1); + return true; + + case XKB_KEY_Tab: + if (!released) + term_to_slave(term, "\t", 1); + return true; } } From ded55b7276020032f4bcdb5e5d819b1d2f56c7ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 1 Jan 2025 09:21:26 +0100 Subject: [PATCH 0935/1323] changelog: prepare for 1.20.0 --- CHANGELOG.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eb95c14..9b4af316 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.20.0](#1-20-0) * [1.19.0](#1-19-0) * [1.18.1](#1-18-1) * [1.18.0](#1-18-0) @@ -55,7 +55,8 @@ * [1.2.0](#1-2-0) -## Unreleased +## 1.20.0 + ### Added * Unicode data files updated to Unicode 16. Foot uses these to @@ -87,8 +88,6 @@ [1892]: https://codeberg.org/dnkl/foot/issues/1892 -### Deprecated -### Removed ### Fixed * Crash when receiving an OSC-9 or OSC-777 with an empty notification @@ -98,9 +97,14 @@ [1866]: https://codeberg.org/dnkl/foot/issues/1866 -### Security ### Contributors +* cy +* Denis Zharikov +* heather7283 +* Jack Wilsdon +* Mark Stosberg + ## 1.19.0 From 9b1c31ebcb4d40c8763207eb9e76f2f15f125071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 1 Jan 2025 09:21:51 +0100 Subject: [PATCH 0936/1323] meson: bump version to 1.20.0 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 7a77ddbc..b739e9de 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.19.0', + version: '1.20.0', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From 1dc922e5a43f9613d93921383aab9ffc8980564c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 1 Jan 2025 09:23:33 +0100 Subject: [PATCH 0937/1323] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b4af316..3271c24b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.20.0](#1-20-0) * [1.19.0](#1-19-0) * [1.18.1](#1-18-1) @@ -55,6 +56,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.20.0 ### Added From f8ebe985a8b4daea12a3f9e2630884d70e044e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 1 Jan 2025 09:29:11 +0100 Subject: [PATCH 0938/1323] changelog: add missing issue ref --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3271c24b..c824b82e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,8 @@ host clipboard access via the OSC-52 escape sequence ([#1867][1867]). +[1867]: https://codeberg.org/dnkl/foot/issues/1867 + ### Changed From c7ab7b353996634c61abd7cd66d32459a6e3da4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 2 Jan 2025 08:58:48 +0100 Subject: [PATCH 0939/1323] term: limit app-id to 2048 characters Unsure if the protocol imposes a limit (haven't found any documentation), or if the issue is in the libwayland implementation, or wlroots (triggers in at least sway+river). The issue is that setting a too long app-id causes the compositor (river at least) to peg the CPU at 100%, and stop sending e.g. frame callbacks to foot. Closes #1897 --- CHANGELOG.md | 7 +++++++ terminal.c | 15 +++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c824b82e..bdc1828b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,13 @@ ## Unreleased ### Added ### Changed + +* Runtime changes to the app-id (OSC-176) now limits the app-id string + to 2048 characters ([#1897][1897]). + +[1897]: https://codeberg.org/dnkl/foot/issues/1897 + + ### Deprecated ### Removed ### Fixed diff --git a/terminal.c b/terminal.c index a4963bc8..5a74631a 100644 --- a/terminal.c +++ b/terminal.c @@ -3588,8 +3588,10 @@ term_set_app_id(struct terminal *term, const char *app_id) { if (app_id != NULL && *app_id == '\0') app_id = NULL; + if (term->app_id == NULL && app_id == NULL) return; + if (term->app_id != NULL && app_id != NULL && streq(term->app_id, app_id)) return; @@ -3604,6 +3606,19 @@ term_set_app_id(struct terminal *term, const char *app_id) } else { term->app_id = NULL; } + + const size_t length = strlen(app_id); + if (length > 2048) { + /* + * Not sure if there's a limit in the protocol, or the + * libwayland implementation, or e.g. wlroots, but too long + * app-id's (not e.g. title) causes at least river and sway to + * peg the CPU at 100%, and stop sending e.g. frame callbacks. + * + */ + term->app_id[2048] = '\0'; + } + render_refresh_app_id(term); render_refresh_icon(term); } From 56d2c3e990509d18de31e6009e0d0c6254f99b67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 2 Jan 2025 09:08:24 +0100 Subject: [PATCH 0940/1323] config: don't allow colors.flash-alpha to be 1.0 A compositor will not send a frame callback for our main window if it is fully occluded (for example, by a fully opaque overlay...). This causes the overlay to stuck. For regular buffers, it _should_ be enough to *not* hint the compositor it's opaque. But at least some compositor special cases single-pixel buffers, and actually look at their pixel value. Thus, we have two options: implement frame callback handling for the overlay sub-surface, or ensure we don't use a fully opaque surface. Since no overlays are fully opaque by default, and the flash surface is the only one that can be configured to be opaque (colors.flash-alpha), and since adding frame callback handling adds a lot of boilerplate code... let's go with the simpler solution of --- CHANGELOG.md | 3 +++ config.c | 4 ++-- render.c | 21 +++++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdc1828b..994805dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,9 @@ * Runtime changes to the app-id (OSC-176) now limits the app-id string to 2048 characters ([#1897][1897]). +* `colors.flash-alpha` can no longer be set to 1.0 (i.e. fully + opaque). This fixes an issue where the window would be stuck in the + flash state. [1897]: https://codeberg.org/dnkl/foot/issues/1897 diff --git a/config.c b/config.c index 7f1ce055..3b8ce7e8 100644 --- a/config.c +++ b/config.c @@ -1445,8 +1445,8 @@ parse_section_colors(struct context *ctx) if (!value_to_float(ctx, &alpha)) return false; - if (alpha < 0. || alpha > 1.) { - LOG_CONTEXTUAL_ERR("not in range 0.0-1.0"); + if (alpha < 0. || alpha >= 1.) { + LOG_CONTEXTUAL_ERR("not in range 0.0-0.999"); return false; } diff --git a/render.c b/render.c index ed6b802e..8fc741b5 100644 --- a/render.c +++ b/render.c @@ -1898,6 +1898,27 @@ render_overlay(struct terminal *term) break; case OVERLAY_FLASH: + /* + * A compositor will not send a frame callback for our main + * window if it is fully occluded (for example, by a fully + * opaque overlay...). This causes the overlay to stuck. + * + * For regular buffers, it _should_ be enough to *not* hint + * the compositor it's opaque. But at least some compositor + * special cases single-pixel buffers, and actually look at + * their pixel value. + * + * Thus, we have two options: implement frame callback + * handling for the overlay sub-surface, or ensure we don't + * use a fully opaque surface. Since no overlays are fully + * opaque by default, and the flash surface is the only one + * that can be configured to be opaque (colors.flash-alpha), + * and since adding frame callback handling adds a lot of + * boilerplate code... let's go with the simpler solution of + * not allowing colors.flash-alpha to be 1.0. + */ + xassert(term->conf->colors.flash_alpha != 0xffff); + color = color_hex_to_pixman_with_alpha( term->conf->colors.flash, term->conf->colors.flash_alpha); From 7c1cee0373034b6eba17d12146d340eec68bb5d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 2 Jan 2025 09:12:06 +0100 Subject: [PATCH 0941/1323] notify: disable debug logging --- notify.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notify.c b/notify.c index c77c0606..e8688180 100644 --- a/notify.c +++ b/notify.c @@ -10,7 +10,7 @@ #include <fcntl.h> #define LOG_MODULE "notify" -#define LOG_ENABLE_DBG 1 +#define LOG_ENABLE_DBG 0 #include "log.h" #include "config.h" #include "spawn.h" From 9f5be55d1cdf2a25fa3bb47247420cc658e646f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 2 Jan 2025 09:12:11 +0100 Subject: [PATCH 0942/1323] osc: disable debug logging --- osc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osc.c b/osc.c index 2c02f53a..17639c19 100644 --- a/osc.c +++ b/osc.c @@ -8,7 +8,7 @@ #include <sys/epoll.h> #define LOG_MODULE "osc" -#define LOG_ENABLE_DBG 1 +#define LOG_ENABLE_DBG 0 #include "log.h" #include "base64.h" #include "config.h" From 9cde179034eb42ad032be4492ea6fc8bc91331ef Mon Sep 17 00:00:00 2001 From: evplus <evplus@noreply.codeberg.org> Date: Thu, 2 Jan 2025 11:53:49 +0100 Subject: [PATCH 0943/1323] themes: add iterm theme from alacritty --- themes/iterm | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 themes/iterm diff --git a/themes/iterm b/themes/iterm new file mode 100644 index 00000000..45b1a0bf --- /dev/null +++ b/themes/iterm @@ -0,0 +1,27 @@ +# -*- conf -*- +# this foot theme is based on alacritty iterm theme: +# https://github.com/alacritty/alacritty-theme/blob/master/themes/iterm.toml + +[colors] +foreground=fffbf6 +background=101421 + +## Normal/regular colors (color palette 0-7) +regular0=2e2e2e # black +regular1=eb4129 # red +regular2=abe047 # green +regular3=f6c744 # yellow +regular4=47a0f3 # blue +regular5=7b5cb0 # magenta +regular6=64dbed # cyan +regular7=e5e9f0 # white + +## Bright colors (color palette 8-15) +bright0=565656 # bright black +bright1=ec5357 # bright red +bright2=c0e17d # bright green +bright3=f9da6a # bright yellow +bright4=49a4f8 # bright blue +bright5=a47de9 # bright magenta +bright6=99faf2 # bright cyan +bright7=ffffff # bright white From f5c10a245229fe173d58ecdc71e4a917524eaf7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 3 Jan 2025 07:33:14 +0100 Subject: [PATCH 0944/1323] render: fix order we're checking custom codepoints Fixes a crash when trying to print a "Legacy Computing" symbol (U+1FB00-U+1FB9B). Closes #1901 --- CHANGELOG.md | 7 +++++++ render.c | 10 +++++----- terminal.h | 12 ++++++------ 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 994805dd..1461fa63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,13 @@ ### Deprecated ### Removed ### Fixed + +* Regression: trying to print a Unicode _"Legacy Computing symbol"_, + in the range U+1FB00 - U+1FB9B would crash foot ([#][]). + +[1901]: https://codeberg.org/dnkl/foot/issues/1901 + + ### Security ### Contributors diff --git a/render.c b/render.c index 8fc741b5..5a924743 100644 --- a/render.c +++ b/render.c @@ -817,14 +817,14 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag size_t count; size_t idx; - if (base >= GLYPH_OCTANTS_FIRST) { - arr = &term->custom_glyphs.octants; - count = GLYPH_OCTANTS_COUNT; - idx = base - GLYPH_OCTANTS_FIRST; - } else if (base >= GLYPH_LEGACY_FIRST) { + if (base >= GLYPH_LEGACY_FIRST) { arr = &term->custom_glyphs.legacy; count = GLYPH_LEGACY_COUNT; idx = base - GLYPH_LEGACY_FIRST; + } else if (base >= GLYPH_OCTANTS_FIRST) { + arr = &term->custom_glyphs.octants; + count = GLYPH_OCTANTS_COUNT; + idx = base - GLYPH_OCTANTS_FIRST; } else if (base >= GLYPH_BRAILLE_FIRST) { arr = &term->custom_glyphs.braille; count = GLYPH_BRAILLE_COUNT; diff --git a/terminal.h b/terminal.h index d3c7f335..813510fe 100644 --- a/terminal.h +++ b/terminal.h @@ -482,8 +482,8 @@ struct terminal { struct { struct fcft_glyph **box_drawing; struct fcft_glyph **braille; - struct fcft_glyph **legacy; struct fcft_glyph **octants; + struct fcft_glyph **legacy; #define GLYPH_BOX_DRAWING_FIRST 0x2500 #define GLYPH_BOX_DRAWING_LAST 0x259F @@ -495,15 +495,15 @@ struct terminal { #define GLYPH_BRAILLE_COUNT \ (GLYPH_BRAILLE_LAST - GLYPH_BRAILLE_FIRST + 1) - #define GLYPH_LEGACY_FIRST 0x1FB00 - #define GLYPH_LEGACY_LAST 0x1FB9B - #define GLYPH_LEGACY_COUNT \ - (GLYPH_LEGACY_LAST - GLYPH_LEGACY_FIRST + 1) - #define GLYPH_OCTANTS_FIRST 0x1CD00 #define GLYPH_OCTANTS_LAST 0x1CDE5 #define GLYPH_OCTANTS_COUNT \ (GLYPH_OCTANTS_LAST - GLYPH_OCTANTS_FIRST + 1) + + #define GLYPH_LEGACY_FIRST 0x1FB00 + #define GLYPH_LEGACY_LAST 0x1FB9B + #define GLYPH_LEGACY_COUNT \ + (GLYPH_LEGACY_LAST - GLYPH_LEGACY_FIRST + 1) } custom_glyphs; bool is_sending_paste_data; From 4ed0361b97d137f2cf12f7eff2ede6f885dde945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 3 Jan 2025 08:01:22 +0100 Subject: [PATCH 0945/1323] changelog: prepare for 1.20.1 --- CHANGELOG.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1461fa63..be32f4d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.20.1](#1-20-1) * [1.20.0](#1-20-0) * [1.19.0](#1-19-0) * [1.18.1](#1-18-1) @@ -56,8 +56,8 @@ * [1.2.0](#1-2-0) -## Unreleased -### Added +## 1.20.1 + ### Changed * Runtime changes to the app-id (OSC-176) now limits the app-id string @@ -69,8 +69,6 @@ [1897]: https://codeberg.org/dnkl/foot/issues/1897 -### Deprecated -### Removed ### Fixed * Regression: trying to print a Unicode _"Legacy Computing symbol"_, @@ -79,10 +77,6 @@ [1901]: https://codeberg.org/dnkl/foot/issues/1901 -### Security -### Contributors - - ## 1.20.0 ### Added From b0e6645395934077f11df125452ed47d160f1bc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 3 Jan 2025 08:01:35 +0100 Subject: [PATCH 0946/1323] meson: bump version to 1.20.1 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index b739e9de..359a8ce0 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.20.0', + version: '1.20.1', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From 8414966013ad0b88694697fab2df2d1766366a14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 3 Jan 2025 08:03:06 +0100 Subject: [PATCH 0947/1323] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index be32f4d6..a7e1b169 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.20.1](#1-20-1) * [1.20.0](#1-20-0) * [1.19.0](#1-19-0) @@ -56,6 +57,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.20.1 ### Changed From 9667fe2b263871ea510039178e75b75d247c9e18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 3 Jan 2025 08:08:52 +0100 Subject: [PATCH 0948/1323] changelog: add missing issue ref --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7e1b169..4b5e4b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,7 +83,7 @@ ### Fixed * Regression: trying to print a Unicode _"Legacy Computing symbol"_, - in the range U+1FB00 - U+1FB9B would crash foot ([#][]). + in the range U+1FB00 - U+1FB9B would crash foot ([#1901][1901]). [1901]: https://codeberg.org/dnkl/foot/issues/1901 From 77e30c8b4501e719bc18b4afe06d5fc0eb13d699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 4 Jan 2025 09:50:06 +0100 Subject: [PATCH 0949/1323] ci: sr.ht: disable x64 (rely on codeberg only) --- .builds/{alpine-x64.yml => alpine-x64.yml.disabled} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .builds/{alpine-x64.yml => alpine-x64.yml.disabled} (100%) diff --git a/.builds/alpine-x64.yml b/.builds/alpine-x64.yml.disabled similarity index 100% rename from .builds/alpine-x64.yml rename to .builds/alpine-x64.yml.disabled From 169471cf23ccba1e82f6d5a216c0ca774052c365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 4 Jan 2025 09:50:30 +0100 Subject: [PATCH 0950/1323] ci: sr.ht: try to bring up to date, and pull from codeberg --- .builds/freebsd-x64.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.builds/freebsd-x64.yml b/.builds/freebsd-x64.yml index 9642f96d..497db929 100644 --- a/.builds/freebsd-x64.yml +++ b/.builds/freebsd-x64.yml @@ -19,7 +19,7 @@ packages: - noto-emoji sources: - - https://git.sr.ht/~dnkl/foot + - https://codeberg.org/dnkl/foot.git # triggers: # - action: email @@ -29,6 +29,7 @@ sources: tasks: - fcft: | cd foot/subprojects + git clone https://codeberg.org/dnkl/tllist.git git clone https://codeberg.org/dnkl/fcft.git cd ../.. - debug: | From a2960aa457d867746789ca227259b80e8fe7d1df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 4 Jan 2025 10:06:45 +0100 Subject: [PATCH 0951/1323] meson: fix dependencies (utf8proc missing in lots of places) --- meson.build | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/meson.build b/meson.build index 359a8ce0..ee5af2f3 100644 --- a/meson.build +++ b/meson.build @@ -226,7 +226,8 @@ common = static_library( 'debug.c', 'debug.h', 'macros.h', 'xmalloc.c', 'xmalloc.h', - 'xsnprintf.c', 'xsnprintf.h' + 'xsnprintf.c', 'xsnprintf.h', + dependencies: [utf8proc] ) misc = static_library( @@ -234,7 +235,9 @@ misc = static_library( 'hsl.c', 'hsl.h', 'macros.h', 'misc.c', 'misc.h', - 'uri.c', 'uri.h' + 'uri.c', 'uri.h', + dependencies: [utf8proc], + link_with: [common] ) vtlib = static_library( @@ -268,6 +271,7 @@ pgolib = static_library( tokenize = static_library( 'tokenizelib', 'tokenize.c', + dependencies: [utf8proc], link_with: [common], ) @@ -321,7 +325,7 @@ executable( 'macros.h', 'util.h', version, - dependencies: [tllist], + dependencies: [tllist, utf8proc], link_with: common, install: true) From 6999968ee5ba111946b237bb1bb186a8526bbb23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 4 Jan 2025 10:33:23 +0100 Subject: [PATCH 0952/1323] changelog: utf8proc.h not found --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b5e4b61..f98908fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,8 +84,11 @@ * Regression: trying to print a Unicode _"Legacy Computing symbol"_, in the range U+1FB00 - U+1FB9B would crash foot ([#1901][1901]). +* Build failures (`utf8proc.h` not found) on at least FreeBSD, but + most likely other BSDs, as well as some Linuxes ([#1903][1903]). [1901]: https://codeberg.org/dnkl/foot/issues/1901 +[1903]: https://codeberg.org/dnkl/foot/issues/1903 ## 1.20.0 From 42f78b7f9c755d5fa7e04f0cbbbf88c58dabd44d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 4 Jan 2025 12:06:49 +0100 Subject: [PATCH 0953/1323] ci: "meson [options]" is deprecated (do "meson setup [options]" instead) --- .builds/freebsd-x64.yml | 4 ++-- .woodpecker.yaml | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.builds/freebsd-x64.yml b/.builds/freebsd-x64.yml index 497db929..77775ac3 100644 --- a/.builds/freebsd-x64.yml +++ b/.builds/freebsd-x64.yml @@ -34,7 +34,7 @@ tasks: cd ../.. - debug: | mkdir -p bld/debug - meson --buildtype=debug -Dterminfo=disabled -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/debug + meson setup --buildtype=debug -Dterminfo=disabled -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/debug ninja -C bld/debug -k0 meson test -C bld/debug --print-errorlogs bld/debug/foot --version @@ -42,7 +42,7 @@ tasks: - release: | mkdir -p bld/release - meson --buildtype=minsize -Db_pgo=generate -Dterminfo=disabled -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release + meson setup --buildtype=minsize -Db_pgo=generate -Dterminfo=disabled -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release ninja -C bld/release -k0 meson test -C bld/release --print-errorlogs bld/release/foot --version diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 9b121f2a..340ba241 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -49,7 +49,7 @@ steps: # Debug - mkdir -p bld/debug-x64 - cd bld/debug-x64 - - meson --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - meson setup --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. - ninja -v -k0 - ninja -v test - ./foot --version @@ -59,7 +59,7 @@ steps: # Release (gcc) - mkdir -p bld/release-x64 - cd bld/release-x64 - - meson --buildtype=release -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - meson setup --buildtype=release -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. - ninja -v -k0 - ninja -v test - ./foot --version @@ -69,7 +69,7 @@ steps: # Release (clang) - mkdir -p bld/release-x64-clang - cd bld/release-x64-clang - - CC=clang meson --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - CC=clang meson setup --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. - ninja -v -k0 - ninja -v test - ./foot --version @@ -80,7 +80,7 @@ steps: - apk del harfbuzz harfbuzz-dev utf8proc utf8proc-dev - mkdir -p bld/debug - cd bld/debug - - meson --buildtype=debug -Dgrapheme-clustering=disabled -Dfcft:grapheme-shaping=disabled -Dfcft:run-shaping=disabled -Dfcft:test-text-shaping=false ../.. + - meson setup --buildtype=debug -Dgrapheme-clustering=disabled -Dfcft:grapheme-shaping=disabled -Dfcft:run-shaping=disabled -Dfcft:test-text-shaping=false ../.. - ninja -v -k0 - ninja -v test - ./foot --version @@ -106,7 +106,7 @@ steps: # Debug - mkdir -p bld/debug-x86 - cd bld/debug-x86 - - meson --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - meson setup --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. - ninja -v -k0 - ninja -v test - ./foot --version @@ -116,7 +116,7 @@ steps: # Release (gcc) - mkdir -p bld/release-x86 - cd bld/release-x86 - - meson --buildtype=release -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - meson setup --buildtype=release -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. - ninja -v -k0 - ninja -v test - ./foot --version @@ -126,7 +126,7 @@ steps: # Release (clang) - mkdir -p bld/release-x86-clang - cd bld/release-x86-clang - - CC=clang meson --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - CC=clang meson setup --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. - ninja -v -k0 - ninja -v test - ./foot --version From 8e425c4e976017c09ed2a9b4402067b3595501cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 7 Jan 2025 12:58:44 +0100 Subject: [PATCH 0954/1323] csi: ignore 'CSI 21 t' - report window title It's not widely used (don't know _any_ application that uses it), and can be used to trick users to run unwanted commands. --- csi.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/csi.c b/csi.c index 35a39f82..61cbdced 100644 --- a/csi.c +++ b/csi.c @@ -1354,10 +1354,14 @@ csi_dispatch(struct terminal *term, uint8_t final) } case 21: { +#if 0 /* Disabled for now, see #1894 */ char reply[3 + strlen(term->window_title) + 2 + 1]; int chars = xsnprintf( reply, sizeof(reply), "\033]l%s\033\\", term->window_title); term_to_slave(term, reply, chars); +#else + LOG_WARN("CSI 21 t (report window title) ignored"); +#endif break; } From 06a32d553e8d2613e3b4582199a317219f5cfc8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 7 Jan 2025 13:00:10 +0100 Subject: [PATCH 0955/1323] osc: ignore 'OSC 176 ?' - report app ID It's not widely used (don't know _any_ application that uses it), and can be used to trick users to run unwanted commands. --- osc.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osc.c b/osc.c index 17639c19..e335dc61 100644 --- a/osc.c +++ b/osc.c @@ -1498,6 +1498,7 @@ osc_dispatch(struct terminal *term) case 176: if (string[0] == '?' && string[1] == '\0') { +#if 0 /* Disabled for now, see #1894 */ const char *terminator = term->vt.osc.bel ? "\a" : "\033\\"; char *reply = xasprintf( "\033]176;%s%s", @@ -1506,6 +1507,9 @@ osc_dispatch(struct terminal *term) term_to_slave(term, reply, strlen(reply)); free(reply); +#else + LOG_WARN("OSC-176 app-id query ignored"); +#endif break; } From d9bd9b7ffaf682e66629839bcfe6a8e668cf0822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 7 Jan 2025 13:00:38 +0100 Subject: [PATCH 0956/1323] doc: ctlseqs: remove 'CSI 21 t' --- doc/foot-ctlseqs.7.scd | 3 --- 1 file changed, 3 deletions(-) diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index 60f78d83..f8eb1222 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -391,9 +391,6 @@ manipulation sequences. The generic format is: | 20 : - : Report icon label. -| 21 -: - -: Report window title. | 22 : - : Push window title+icon. From bcc176cdf181b6b3a5b84edea8ce62b5f596b9e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 7 Jan 2025 13:00:50 +0100 Subject: [PATCH 0957/1323] changelog: 'CSI 21 t' and 'OSC 176 ?' disabled --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f98908fe..bc1d3bc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,13 @@ ## Unreleased ### Added ### Changed + +* The `CSI 21 t` (report window title) and `OSC 176 ?` (report app-id) + escape sequences are now ignored ([#1894][1894]). + +[1894]: https://codeberg.org/dnkl/foot/issues/1894 + + ### Deprecated ### Removed ### Fixed From f0253633d3cdde994b48c25692707e1df2f1e1e7 Mon Sep 17 00:00:00 2001 From: Alexander Orzechowski <alex@ozal.ski> Date: Sat, 4 Jan 2025 22:26:00 -0500 Subject: [PATCH 0958/1323] render: Expose render_overlay This function updates the overlay that foot uses. It will be used to update the overlay when the flash effect ends. --- pgo/pgo.c | 2 ++ render.c | 2 +- render.h | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pgo/pgo.c b/pgo/pgo.c index aab18847..24154277 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -72,6 +72,8 @@ void render_refresh_title(struct terminal *term) {} void render_refresh_app_id(struct terminal *term) {} void render_refresh_icon(struct terminal *term) {} +void render_overlay(struct terminal *term) {} + bool render_xcursor_is_valid(const struct seat *seat, const char *cursor) { diff --git a/render.c b/render.c index 5a924743..020dee79 100644 --- a/render.c +++ b/render.c @@ -1862,7 +1862,7 @@ render_overlay_single_pixel(struct terminal *term, enum overlay_style style, } } -static void +void render_overlay(struct terminal *term) { struct wayl_sub_surface *overlay = &term->window->overlay; diff --git a/render.h b/render.h index 1898351c..81d2a905 100644 --- a/render.h +++ b/render.h @@ -31,6 +31,8 @@ bool render_xcursor_set( struct seat *seat, struct terminal *term, enum cursor_shape shape); bool render_xcursor_is_valid(const struct seat *seat, const char *cursor); +void render_overlay(struct terminal *term); + struct render_worker_context { int my_id; struct terminal *term; From c2add346ad5c1673ba0334264300e86eb50b2293 Mon Sep 17 00:00:00 2001 From: Alexander Orzechowski <alex@ozal.ski> Date: Sat, 4 Jan 2025 21:59:19 -0500 Subject: [PATCH 0959/1323] terminal: Refresh only overlay when flash expires If we call render_refresh, that will wait for a callback to the main surface. In the case of a flash, the main surface might not get callbacks if the compositor implements fancy culling optimizations like wlroots wlr_scene compositors such as sway version >=1.10. --- terminal.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/terminal.c b/terminal.c index 5a74631a..7b5a4d2d 100644 --- a/terminal.c +++ b/terminal.c @@ -419,7 +419,12 @@ fdm_flash(struct fdm *fdm, int fd, int events, void *data) (unsigned long long)expiration_count); term->flash.active = false; - render_refresh(term); + render_overlay(term); + + // since the overlay surface is synced with the main window surface, we have + // to commit the main surface for the compositor to acknowledge the new + // overlay state. + wl_surface_commit(term->window->surface.surf); return true; } From 301101e7d9d9675b48b874128a36ecf3005b727e Mon Sep 17 00:00:00 2001 From: Alexander Orzechowski <alex@ozal.ski> Date: Sat, 4 Jan 2025 21:36:33 -0500 Subject: [PATCH 0960/1323] Revert "config: don't allow colors.flash-alpha to be 1.0" This reverts commit 56d2c3e990509d18de31e6009e0d0c6254f99b67. --- CHANGELOG.md | 3 --- config.c | 4 ++-- render.c | 21 --------------------- 3 files changed, 2 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc1d3bc3..508174b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,9 +80,6 @@ * Runtime changes to the app-id (OSC-176) now limits the app-id string to 2048 characters ([#1897][1897]). -* `colors.flash-alpha` can no longer be set to 1.0 (i.e. fully - opaque). This fixes an issue where the window would be stuck in the - flash state. [1897]: https://codeberg.org/dnkl/foot/issues/1897 diff --git a/config.c b/config.c index 3b8ce7e8..7f1ce055 100644 --- a/config.c +++ b/config.c @@ -1445,8 +1445,8 @@ parse_section_colors(struct context *ctx) if (!value_to_float(ctx, &alpha)) return false; - if (alpha < 0. || alpha >= 1.) { - LOG_CONTEXTUAL_ERR("not in range 0.0-0.999"); + if (alpha < 0. || alpha > 1.) { + LOG_CONTEXTUAL_ERR("not in range 0.0-1.0"); return false; } diff --git a/render.c b/render.c index 020dee79..b1791a90 100644 --- a/render.c +++ b/render.c @@ -1898,27 +1898,6 @@ render_overlay(struct terminal *term) break; case OVERLAY_FLASH: - /* - * A compositor will not send a frame callback for our main - * window if it is fully occluded (for example, by a fully - * opaque overlay...). This causes the overlay to stuck. - * - * For regular buffers, it _should_ be enough to *not* hint - * the compositor it's opaque. But at least some compositor - * special cases single-pixel buffers, and actually look at - * their pixel value. - * - * Thus, we have two options: implement frame callback - * handling for the overlay sub-surface, or ensure we don't - * use a fully opaque surface. Since no overlays are fully - * opaque by default, and the flash surface is the only one - * that can be configured to be opaque (colors.flash-alpha), - * and since adding frame callback handling adds a lot of - * boilerplate code... let's go with the simpler solution of - * not allowing colors.flash-alpha to be 1.0. - */ - xassert(term->conf->colors.flash_alpha != 0xffff); - color = color_hex_to_pixman_with_alpha( term->conf->colors.flash, term->conf->colors.flash_alpha); From e136abf1ef394aaa2bc86f9fc4b96d61c0bffbff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 9 Jan 2025 07:56:10 +0100 Subject: [PATCH 0961/1323] changelog: colors.flash-alpha=1.0 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 508174b2..87694630 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,12 @@ ### Deprecated ### Removed ### Fixed + +* 'flash' overlay (triggered by either `tput flash`, or enabling + `bell.visual` and then sending `BEL` to the terminal) stuck when + `colors.flash-alpha=1.0`. + + ### Security ### Contributors From 2c309227f1c20c23d75055935b4d705c275c3c94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 9 Jan 2025 07:49:29 +0100 Subject: [PATCH 0962/1323] term: cursor_refresh(): don't try to dirty the grid if we don't have one If the compositor sends a keyboard enter event before our window has been mapped, foot crashes; the enter event triggers a cursor refresh (hollow -> non-hollow block cursor), which crashes since we haven't yet allocated a grid. Fix by no-op:ing the refresh if the window hasn't been configured yet. Closes #1910 --- CHANGELOG.md | 4 ++++ terminal.c | 3 +++ 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87694630..be321556 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,10 @@ * 'flash' overlay (triggered by either `tput flash`, or enabling `bell.visual` and then sending `BEL` to the terminal) stuck when `colors.flash-alpha=1.0`. +* Crash when compositor sends a keyboard enter event before the foot + window has been mapped ([#1910][1910]). + +[1910]: https://codeberg.org/dnkl/foot/issues/1910 ### Security diff --git a/terminal.c b/terminal.c index 7b5a4d2d..e392c36d 100644 --- a/terminal.c +++ b/terminal.c @@ -515,6 +515,9 @@ term_arm_blink_timer(struct terminal *term) static void cursor_refresh(struct terminal *term) { + if (!term->window->is_configured) + return; + term->grid->cur_row->cells[term->grid->cursor.point.col].attrs.clean = 0; term->grid->cur_row->dirty = true; render_refresh(term); From feb4dd102b606b3b5ccd2d8955d9ac370d9ae9d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 10 Jan 2025 13:05:35 +0100 Subject: [PATCH 0963/1323] forgejo: bugs: add required field 'distro' --- .forgejo/issue_template/issue_template.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index cca40dd5..fa602200 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -34,6 +34,14 @@ body: placeholder: "sway version 1.9" validations: required: true + - type: input + id: distro + attributes: + label: Distribution + description: "The name of the Linux distribution, or BSD flavor, you are running" + placeholder: "Arch Linux" + validations: + required: true - type: textarea id: repro attributes: From b5835cbd589aed06469d4761ad3c8f9214a2bb86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 10 Jan 2025 13:13:05 +0100 Subject: [PATCH 0964/1323] forgejo: bugs: add required field 'config' Require all bug submitters to include their foot configs. --- .forgejo/issue_template/issue_template.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index fa602200..e650d8d0 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -42,6 +42,13 @@ body: placeholder: "Arch Linux" validations: required: true + - type: textarea + id: config + attributes: + label: Foot config + description: paste your entire `foot.ini` here + validations: + required: true - type: textarea id: repro attributes: From 3b96de2aa4c86a968b9239f7351d0eb7d8e60ba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 10 Jan 2025 13:14:02 +0100 Subject: [PATCH 0965/1323] forgejo: bugs: config: uppercase description's first letter --- .forgejo/issue_template/issue_template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index e650d8d0..ca12c4a3 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -46,7 +46,7 @@ body: id: config attributes: label: Foot config - description: paste your entire `foot.ini` here + description: Paste your entire `foot.ini` here validations: required: true - type: textarea From 7e7fd0468d860274c46030dcd43b2eadfb189f64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 10 Jan 2025 13:15:02 +0100 Subject: [PATCH 0966/1323] forgejo: bugs: short explanation of what an IME is --- .forgejo/issue_template/issue_template.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index ca12c4a3..eae2e492 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -74,8 +74,8 @@ body: other terminal multiplexer? Does the bug happen in a plain foot instance? - **IME** do you use an IME? Which one? Does the bug happen if - you disable the IME? + **IME** do you use an IME (e.g. fcitx5, ibus etc)? Which one? + Does the bug happen if you disable the IME? Obtaining logs and stacktraces ------------------------------ From 2a07a2e6b9a9565287996c51ceb2824b37d3fba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 17 Jan 2025 10:10:10 +0100 Subject: [PATCH 0967/1323] Add support for the new Wayland protocol xdg-system-bell From the release notes: system bell - allowing e.g. terminal emulators to hand off system bell alerts to the compositor for among other things accessibility purposes The new protocol is used when the new config option bell.system=yes (and the compositor implements the protocol, obviously). The system bell is rung independent of whether the foot window has keyboard focus or not (thus relying on compositor configuration to determine whether anything should be done or not in response to the bell). The new option is enabled by default. --- CHANGELOG.md | 7 +++++++ config.c | 3 +++ config.h | 1 + doc/foot.ini.5.scd | 29 ++++++++++++++++++----------- foot.ini | 1 + meson.build | 10 ++++++++++ pgo/pgo.c | 1 + terminal.c | 3 +++ tests/test-config.c | 1 + wayland.c | 37 +++++++++++++++++++++++++++++++++++++ wayland.h | 9 +++++++++ 11 files changed, 91 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be321556..3c01193a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,7 +58,14 @@ ## Unreleased + ### Added + +* Support for the new Wayland protocol `xdg-system-bell-v1` protocol + (added in wayland-protocols 1.38), via the new config option + `bell.system=no|yes` (defaults to `yes`). + + ### Changed * The `CSI 21 t` (report window title) and `OSC 176 ?` (report app-id) diff --git a/config.c b/config.c index 7f1ce055..68d49207 100644 --- a/config.c +++ b/config.c @@ -1139,6 +1139,8 @@ parse_section_bell(struct context *ctx) return value_to_bool(ctx, &conf->bell.urgent); else if (streq(key, "notify")) return value_to_bool(ctx, &conf->bell.notify); + else if (streq(key, "system")) + return value_to_bool(ctx, &conf->bell.system_bell); else if (streq(key, "visual")) return value_to_bool(ctx, &conf->bell.flash); else if (streq(key, "command")) @@ -3182,6 +3184,7 @@ config_load(struct config *conf, const char *conf_path, .urgent = false, .notify = false, .flash = false, + .system_bell = true, .command = { .argv = {.args = NULL}, }, diff --git a/config.h b/config.h index d7192970..7d9f88c3 100644 --- a/config.h +++ b/config.h @@ -186,6 +186,7 @@ struct config { bool urgent; bool notify; bool flash; + bool system_bell; struct config_spawn_template command; bool command_focused; } bell; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index ba59050e..733168bf 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -445,10 +445,17 @@ Note: do not set *TERM* here; use the *term* option in the main # SECTION: bell +*system* + Boolean, when set to _yes_, ring the system bell. The bell is rung + independent of whether the foot window has keyboard focus or + not. Exact behavior is compositor dependent. + + Default: _yes_ + *urgent* - When set to _yes_, foot will signal urgency to the compositor - through the XDG activation protocol whenever *BEL* is received, - and the window does NOT have keyboard focus. + Boolean, when set to _yes_, foot will signal urgency to the + compositor through the XDG activation protocol whenever *BEL* is + received, and the window does NOT have keyboard focus. If the compositor does not implement this protocol, the margins will be painted in red instead. @@ -459,25 +466,25 @@ Note: do not set *TERM* here; use the *term* option in the main Default: _no_ *notify* - When set to _yes_, foot will emit a desktop notification using the - command specified in the *notify* option whenever *BEL* is - received. By default, bell notifications are shown only when the - window does *not* have keyboard focus. See + Boolean, when set to _yes_, foot will emit a desktop notification + using the command specified in the *notify* option whenever *BEL* + is received. By default, bell notifications are shown only when + the window does *not* have keyboard focus. See _desktop-notifications.inhibit-when-focused_. Default: _no_ *visual* - When set to _yes_, foot will flash the terminal window. Default: - _no_ + Boolean, when set to _yes_, foot will flash the terminal + window. Default: _no_ *command* When set, foot will execute this command when *BEL* is received. Default: none *command-focused* - Whether to run the command on *BEL* even while focused. Default: - _no_ + Boolean, whether to run the command on *BEL* even while + focused. Default: _no_ # SECTION: desktop-notifications diff --git a/foot.ini b/foot.ini index bd4ac082..580178af 100644 --- a/foot.ini +++ b/foot.ini @@ -45,6 +45,7 @@ # osc52=enabled # disabled|copy-enabled|paste-enabled|enabled [bell] +# system=yes # urgent=no # notify=no # visual=no diff --git a/meson.build b/meson.build index ee5af2f3..f39dbc6c 100644 --- a/meson.build +++ b/meson.build @@ -179,6 +179,15 @@ else xdg_toplevel_icon = false endif +if wayland_protocols.version().version_compare('>=1.38') + add_project_arguments('-DHAVE_XDG_SYSTEM_BELL', language: 'c') + wl_proto_xml += [wayland_protocols_datadir / 'staging/xdg-system-bell/xdg-system-bell-v1.xml'] + xdg_system_bell = true +else + xdg_system_bell = false +endif + + foreach prot : wl_proto_xml wl_proto_headers += custom_target( prot.underscorify() + '-client-header', @@ -414,6 +423,7 @@ summary( 'IME': get_option('ime'), 'Grapheme clustering': utf8proc.found(), 'Wayland: xdg-toplevel-icon-v1': xdg_toplevel_icon, + 'Wayland: xdg-system-bell-v1': xdg_system_bell, 'utmp backend': utmp_backend, 'utmp helper default path': utmp_default_helper_path, 'Build terminfo': tic.found(), diff --git a/pgo/pgo.c b/pgo/pgo.c index 24154277..88e862b8 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -101,6 +101,7 @@ wayl_win_init(struct terminal *term, const char *token) void wayl_win_destroy(struct wl_window *win) {} void wayl_win_alpha_changed(struct wl_window *win) {} bool wayl_win_set_urgent(struct wl_window *win) { return true; } +bool wayl_win_ring_bell(const struct wl_window *win) { return true; } bool wayl_fractional_scaling(const struct wayland *wayl) { return true; } pid_t diff --git a/terminal.c b/terminal.c index e392c36d..6936ff29 100644 --- a/terminal.c +++ b/terminal.c @@ -3683,6 +3683,9 @@ term_bell(struct terminal *term) } } + if (term->conf->bell.system_bell) + wayl_win_ring_bell(term->window); + if (term->conf->bell.notify) { notify_notify(term, &(struct notification){ .title = xstrdup("Bell"), diff --git a/tests/test-config.c b/tests/test-config.c index a189d440..303ddd6f 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -579,6 +579,7 @@ test_section_bell(void) test_boolean(&ctx, &parse_section_bell, "urgent", &conf.bell.urgent); test_boolean(&ctx, &parse_section_bell, "notify", &conf.bell.notify); + test_boolean(&ctx, &parse_section_bell, "system", &conf.bell.system_bell); test_boolean(&ctx, &parse_section_bell, "command-focused", &conf.bell.command_focused); test_spawn_template(&ctx, &parse_section_bell, "command", diff --git a/wayland.c b/wayland.c index 9c184adc..3a46133f 100644 --- a/wayland.c +++ b/wayland.c @@ -1374,6 +1374,17 @@ handle_global(void *data, struct wl_registry *registry, } #endif +#if defined(HAVE_XDG_SYSTEM_BELL) + else if (streq(interface, xdg_system_bell_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->system_bell = wl_registry_bind( + wayl->registry, name, &xdg_system_bell_v1_interface, required); + } +#endif + #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED else if (streq(interface, zwp_text_input_manager_v3_interface.name)) { const uint32_t required = 1; @@ -1696,6 +1707,10 @@ wayl_destroy(struct wayland *wayl) zwp_text_input_manager_v3_destroy(wayl->text_input_manager); #endif +#if defined(HAVE_XDG_SYSTEM_BELL) + if (wayl->system_bell != NULL) + xdg_system_bell_v1_destroy(wayl->system_bell); +#endif #if defined(HAVE_XDG_TOPLEVEL_ICON) if (wayl->toplevel_icon_manager != NULL) xdg_toplevel_icon_manager_v1_destroy(wayl->toplevel_icon_manager); @@ -2247,6 +2262,28 @@ wayl_win_set_urgent(struct wl_window *win) return false; } +bool +wayl_win_ring_bell(const struct wl_window *win) +{ +#if defined(HAVE_XDG_SYSTEM_BELL) + if (win->term->wl->system_bell == NULL) { + static bool have_warned = false; + + if (!have_warned) { + LOG_WARN("compositor does not implement the XDG system bell protocol"); + have_warned = true; + } + + return false; + } + + xdg_system_bell_v1_ring(win->term->wl->system_bell, win->surface.surf); + return true; +#else + return false; +#endif +} + bool wayl_win_csd_titlebar_visible(const struct wl_window *win) { diff --git a/wayland.h b/wayland.h index 227e2a68..b3ef5a2b 100644 --- a/wayland.h +++ b/wayland.h @@ -24,6 +24,10 @@ #include <xdg-toplevel-icon-v1.h> #endif +#if defined(HAVE_XDG_SYSTEM_BELL) + #include <xdg-system-bell-v1.h> +#endif + #include <fcft/fcft.h> #include <tllist.h> @@ -451,6 +455,10 @@ struct wayland { struct xdg_toplevel_icon_manager_v1 *toplevel_icon_manager; #endif +#if defined(HAVE_XDG_SYSTEM_BELL) + struct xdg_system_bell_v1 *system_bell; +#endif + bool presentation_timings; struct wp_presentation *presentation; uint32_t presentation_clock_id; @@ -492,6 +500,7 @@ void wayl_win_destroy(struct wl_window *win); void wayl_win_scale(struct wl_window *win, const struct buffer *buf); void wayl_win_alpha_changed(struct wl_window *win); bool wayl_win_set_urgent(struct wl_window *win); +bool wayl_win_ring_bell(const struct wl_window *win); bool wayl_win_csd_titlebar_visible(const struct wl_window *win); bool wayl_win_csd_borders_visible(const struct wl_window *win); From aeb28e33fa774e9d00ffbc1054c6152ebe54babd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 17 Jan 2025 11:22:23 +0100 Subject: [PATCH 0968/1323] features: add +/-system-bell to version output --- client.c | 6 ++++-- foot-features.h | 9 +++++++++ main.c | 3 ++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/client.c b/client.c index 8576531f..757f4d41 100644 --- a/client.c +++ b/client.c @@ -67,12 +67,14 @@ version_and_features(void) { static char buf[256]; snprintf(buf, sizeof(buf), - "version: %s %cpgo %cime %cgraphemes %cassertions", + "version: %s %cpgo %cime %cgraphemes %ctoplevel-icon %csystem-bell %cassertions", FOOT_VERSION, feature_pgo() ? '+' : '-', feature_ime() ? '+' : '-', feature_graphemes() ? '+' : '-', - feature_assertions() ? '+' : '-'); + feature_xdg_toplevel_icon() ? '+' : '-', + feature_xdg_system_bell() ? '+' : '-', + feature_assertions() ? '+' : '-'); return buf; } diff --git a/foot-features.h b/foot-features.h index 674c1056..0eef5eac 100644 --- a/foot-features.h +++ b/foot-features.h @@ -46,3 +46,12 @@ static inline bool feature_xdg_toplevel_icon(void) return false; #endif } + +static inline bool feature_xdg_system_bell(void) +{ +#if defined(HAVE_XDG_SYSTEM_BELL) + return true; +#else + return false; +#endif +} diff --git a/main.c b/main.c index 973cbae4..c5c11080 100644 --- a/main.c +++ b/main.c @@ -51,12 +51,13 @@ version_and_features(void) { static char buf[256]; snprintf(buf, sizeof(buf), - "version: %s %cpgo %cime %cgraphemes %ctoplevel-icon %cassertions", + "version: %s %cpgo %cime %cgraphemes %ctoplevel-icon %csystem-bell %cassertions", FOOT_VERSION, feature_pgo() ? '+' : '-', feature_ime() ? '+' : '-', feature_graphemes() ? '+' : '-', feature_xdg_toplevel_icon() ? '+' : '-', + feature_xdg_system_bell() ? '+' : '-', feature_assertions() ? '+' : '-'); return buf; } From 45e5a4b024c10cb1834366b33a8d2f04bb44c8d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 3 Jan 2025 08:03:06 +0100 Subject: [PATCH 0969/1323] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index be32f4d6..a7e1b169 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.20.1](#1-20-1) * [1.20.0](#1-20-0) * [1.19.0](#1-19-0) @@ -56,6 +57,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.20.1 ### Changed From c854f35579afa6a841fc795236f9bb2fde2b4126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 3 Jan 2025 08:08:52 +0100 Subject: [PATCH 0970/1323] changelog: add missing issue ref --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7e1b169..4b5e4b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,7 +83,7 @@ ### Fixed * Regression: trying to print a Unicode _"Legacy Computing symbol"_, - in the range U+1FB00 - U+1FB9B would crash foot ([#][]). + in the range U+1FB00 - U+1FB9B would crash foot ([#1901][1901]). [1901]: https://codeberg.org/dnkl/foot/issues/1901 From fc154872c0894044f5c84bd7551529da85bbaae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 4 Jan 2025 09:50:06 +0100 Subject: [PATCH 0971/1323] ci: sr.ht: disable x64 (rely on codeberg only) --- .builds/{alpine-x64.yml => alpine-x64.yml.disabled} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .builds/{alpine-x64.yml => alpine-x64.yml.disabled} (100%) diff --git a/.builds/alpine-x64.yml b/.builds/alpine-x64.yml.disabled similarity index 100% rename from .builds/alpine-x64.yml rename to .builds/alpine-x64.yml.disabled From 2784ae8793f1e4c6b533688672cc6a3b6e4e852f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 4 Jan 2025 09:50:30 +0100 Subject: [PATCH 0972/1323] ci: sr.ht: try to bring up to date, and pull from codeberg --- .builds/freebsd-x64.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.builds/freebsd-x64.yml b/.builds/freebsd-x64.yml index 9642f96d..497db929 100644 --- a/.builds/freebsd-x64.yml +++ b/.builds/freebsd-x64.yml @@ -19,7 +19,7 @@ packages: - noto-emoji sources: - - https://git.sr.ht/~dnkl/foot + - https://codeberg.org/dnkl/foot.git # triggers: # - action: email @@ -29,6 +29,7 @@ sources: tasks: - fcft: | cd foot/subprojects + git clone https://codeberg.org/dnkl/tllist.git git clone https://codeberg.org/dnkl/fcft.git cd ../.. - debug: | From f7031a2161fb8beafc7118f055aaf0b073807682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 4 Jan 2025 10:06:45 +0100 Subject: [PATCH 0973/1323] meson: fix dependencies (utf8proc missing in lots of places) --- meson.build | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/meson.build b/meson.build index 359a8ce0..ee5af2f3 100644 --- a/meson.build +++ b/meson.build @@ -226,7 +226,8 @@ common = static_library( 'debug.c', 'debug.h', 'macros.h', 'xmalloc.c', 'xmalloc.h', - 'xsnprintf.c', 'xsnprintf.h' + 'xsnprintf.c', 'xsnprintf.h', + dependencies: [utf8proc] ) misc = static_library( @@ -234,7 +235,9 @@ misc = static_library( 'hsl.c', 'hsl.h', 'macros.h', 'misc.c', 'misc.h', - 'uri.c', 'uri.h' + 'uri.c', 'uri.h', + dependencies: [utf8proc], + link_with: [common] ) vtlib = static_library( @@ -268,6 +271,7 @@ pgolib = static_library( tokenize = static_library( 'tokenizelib', 'tokenize.c', + dependencies: [utf8proc], link_with: [common], ) @@ -321,7 +325,7 @@ executable( 'macros.h', 'util.h', version, - dependencies: [tllist], + dependencies: [tllist, utf8proc], link_with: common, install: true) From 80ef366bdeaeb2640955ee626c2fd94987df8130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 4 Jan 2025 10:33:23 +0100 Subject: [PATCH 0974/1323] changelog: utf8proc.h not found --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b5e4b61..f98908fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,8 +84,11 @@ * Regression: trying to print a Unicode _"Legacy Computing symbol"_, in the range U+1FB00 - U+1FB9B would crash foot ([#1901][1901]). +* Build failures (`utf8proc.h` not found) on at least FreeBSD, but + most likely other BSDs, as well as some Linuxes ([#1903][1903]). [1901]: https://codeberg.org/dnkl/foot/issues/1901 +[1903]: https://codeberg.org/dnkl/foot/issues/1903 ## 1.20.0 From 87ef8697672b6ea9eb3a4d608a2d9a0fdbd1401c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 4 Jan 2025 12:06:49 +0100 Subject: [PATCH 0975/1323] ci: "meson [options]" is deprecated (do "meson setup [options]" instead) --- .builds/freebsd-x64.yml | 4 ++-- .woodpecker.yaml | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.builds/freebsd-x64.yml b/.builds/freebsd-x64.yml index 497db929..77775ac3 100644 --- a/.builds/freebsd-x64.yml +++ b/.builds/freebsd-x64.yml @@ -34,7 +34,7 @@ tasks: cd ../.. - debug: | mkdir -p bld/debug - meson --buildtype=debug -Dterminfo=disabled -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/debug + meson setup --buildtype=debug -Dterminfo=disabled -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/debug ninja -C bld/debug -k0 meson test -C bld/debug --print-errorlogs bld/debug/foot --version @@ -42,7 +42,7 @@ tasks: - release: | mkdir -p bld/release - meson --buildtype=minsize -Db_pgo=generate -Dterminfo=disabled -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release + meson setup --buildtype=minsize -Db_pgo=generate -Dterminfo=disabled -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release ninja -C bld/release -k0 meson test -C bld/release --print-errorlogs bld/release/foot --version diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 9b121f2a..340ba241 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -49,7 +49,7 @@ steps: # Debug - mkdir -p bld/debug-x64 - cd bld/debug-x64 - - meson --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - meson setup --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. - ninja -v -k0 - ninja -v test - ./foot --version @@ -59,7 +59,7 @@ steps: # Release (gcc) - mkdir -p bld/release-x64 - cd bld/release-x64 - - meson --buildtype=release -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - meson setup --buildtype=release -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. - ninja -v -k0 - ninja -v test - ./foot --version @@ -69,7 +69,7 @@ steps: # Release (clang) - mkdir -p bld/release-x64-clang - cd bld/release-x64-clang - - CC=clang meson --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - CC=clang meson setup --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. - ninja -v -k0 - ninja -v test - ./foot --version @@ -80,7 +80,7 @@ steps: - apk del harfbuzz harfbuzz-dev utf8proc utf8proc-dev - mkdir -p bld/debug - cd bld/debug - - meson --buildtype=debug -Dgrapheme-clustering=disabled -Dfcft:grapheme-shaping=disabled -Dfcft:run-shaping=disabled -Dfcft:test-text-shaping=false ../.. + - meson setup --buildtype=debug -Dgrapheme-clustering=disabled -Dfcft:grapheme-shaping=disabled -Dfcft:run-shaping=disabled -Dfcft:test-text-shaping=false ../.. - ninja -v -k0 - ninja -v test - ./foot --version @@ -106,7 +106,7 @@ steps: # Debug - mkdir -p bld/debug-x86 - cd bld/debug-x86 - - meson --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - meson setup --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. - ninja -v -k0 - ninja -v test - ./foot --version @@ -116,7 +116,7 @@ steps: # Release (gcc) - mkdir -p bld/release-x86 - cd bld/release-x86 - - meson --buildtype=release -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - meson setup --buildtype=release -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. - ninja -v -k0 - ninja -v test - ./foot --version @@ -126,7 +126,7 @@ steps: # Release (clang) - mkdir -p bld/release-x86-clang - cd bld/release-x86-clang - - CC=clang meson --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - CC=clang meson setup --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. - ninja -v -k0 - ninja -v test - ./foot --version From a62194caee739c9e3b60def25008c3c8447aaa8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 7 Jan 2025 12:58:44 +0100 Subject: [PATCH 0976/1323] csi: ignore 'CSI 21 t' - report window title It's not widely used (don't know _any_ application that uses it), and can be used to trick users to run unwanted commands. --- csi.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/csi.c b/csi.c index 35a39f82..61cbdced 100644 --- a/csi.c +++ b/csi.c @@ -1354,10 +1354,14 @@ csi_dispatch(struct terminal *term, uint8_t final) } case 21: { +#if 0 /* Disabled for now, see #1894 */ char reply[3 + strlen(term->window_title) + 2 + 1]; int chars = xsnprintf( reply, sizeof(reply), "\033]l%s\033\\", term->window_title); term_to_slave(term, reply, chars); +#else + LOG_WARN("CSI 21 t (report window title) ignored"); +#endif break; } From 354ba8dad84eaf262c90657f9020c2b482fdd6ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 7 Jan 2025 13:00:10 +0100 Subject: [PATCH 0977/1323] osc: ignore 'OSC 176 ?' - report app ID It's not widely used (don't know _any_ application that uses it), and can be used to trick users to run unwanted commands. --- osc.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osc.c b/osc.c index 17639c19..e335dc61 100644 --- a/osc.c +++ b/osc.c @@ -1498,6 +1498,7 @@ osc_dispatch(struct terminal *term) case 176: if (string[0] == '?' && string[1] == '\0') { +#if 0 /* Disabled for now, see #1894 */ const char *terminator = term->vt.osc.bel ? "\a" : "\033\\"; char *reply = xasprintf( "\033]176;%s%s", @@ -1506,6 +1507,9 @@ osc_dispatch(struct terminal *term) term_to_slave(term, reply, strlen(reply)); free(reply); +#else + LOG_WARN("OSC-176 app-id query ignored"); +#endif break; } From ba81480ebb61b7140aa140ef344a5a78c46a55cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 7 Jan 2025 13:00:38 +0100 Subject: [PATCH 0978/1323] doc: ctlseqs: remove 'CSI 21 t' --- doc/foot-ctlseqs.7.scd | 3 --- 1 file changed, 3 deletions(-) diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index 60f78d83..f8eb1222 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -391,9 +391,6 @@ manipulation sequences. The generic format is: | 20 : - : Report icon label. -| 21 -: - -: Report window title. | 22 : - : Push window title+icon. From ad1e2d7d05863269c9dfd808e6c6752b2595eff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 7 Jan 2025 13:00:50 +0100 Subject: [PATCH 0979/1323] changelog: 'CSI 21 t' and 'OSC 176 ?' disabled --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f98908fe..bc1d3bc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,13 @@ ## Unreleased ### Added ### Changed + +* The `CSI 21 t` (report window title) and `OSC 176 ?` (report app-id) + escape sequences are now ignored ([#1894][1894]). + +[1894]: https://codeberg.org/dnkl/foot/issues/1894 + + ### Deprecated ### Removed ### Fixed From 881eb28134b54b86092fdca426ef51e785a699a0 Mon Sep 17 00:00:00 2001 From: Alexander Orzechowski <alex@ozal.ski> Date: Sat, 4 Jan 2025 22:26:00 -0500 Subject: [PATCH 0980/1323] render: Expose render_overlay This function updates the overlay that foot uses. It will be used to update the overlay when the flash effect ends. --- pgo/pgo.c | 2 ++ render.c | 2 +- render.h | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pgo/pgo.c b/pgo/pgo.c index aab18847..24154277 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -72,6 +72,8 @@ void render_refresh_title(struct terminal *term) {} void render_refresh_app_id(struct terminal *term) {} void render_refresh_icon(struct terminal *term) {} +void render_overlay(struct terminal *term) {} + bool render_xcursor_is_valid(const struct seat *seat, const char *cursor) { diff --git a/render.c b/render.c index 5a924743..020dee79 100644 --- a/render.c +++ b/render.c @@ -1862,7 +1862,7 @@ render_overlay_single_pixel(struct terminal *term, enum overlay_style style, } } -static void +void render_overlay(struct terminal *term) { struct wayl_sub_surface *overlay = &term->window->overlay; diff --git a/render.h b/render.h index 1898351c..81d2a905 100644 --- a/render.h +++ b/render.h @@ -31,6 +31,8 @@ bool render_xcursor_set( struct seat *seat, struct terminal *term, enum cursor_shape shape); bool render_xcursor_is_valid(const struct seat *seat, const char *cursor); +void render_overlay(struct terminal *term); + struct render_worker_context { int my_id; struct terminal *term; From ab5a168dbffb63e17c9067133ab55c97d800ef1d Mon Sep 17 00:00:00 2001 From: Alexander Orzechowski <alex@ozal.ski> Date: Sat, 4 Jan 2025 21:59:19 -0500 Subject: [PATCH 0981/1323] terminal: Refresh only overlay when flash expires If we call render_refresh, that will wait for a callback to the main surface. In the case of a flash, the main surface might not get callbacks if the compositor implements fancy culling optimizations like wlroots wlr_scene compositors such as sway version >=1.10. --- terminal.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/terminal.c b/terminal.c index 5a74631a..7b5a4d2d 100644 --- a/terminal.c +++ b/terminal.c @@ -419,7 +419,12 @@ fdm_flash(struct fdm *fdm, int fd, int events, void *data) (unsigned long long)expiration_count); term->flash.active = false; - render_refresh(term); + render_overlay(term); + + // since the overlay surface is synced with the main window surface, we have + // to commit the main surface for the compositor to acknowledge the new + // overlay state. + wl_surface_commit(term->window->surface.surf); return true; } From de3becef96f30b96f0fceddf73805c3951ccb59f Mon Sep 17 00:00:00 2001 From: Alexander Orzechowski <alex@ozal.ski> Date: Sat, 4 Jan 2025 21:36:33 -0500 Subject: [PATCH 0982/1323] Revert "config: don't allow colors.flash-alpha to be 1.0" This reverts commit 56d2c3e990509d18de31e6009e0d0c6254f99b67. --- CHANGELOG.md | 3 --- config.c | 4 ++-- render.c | 21 --------------------- 3 files changed, 2 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc1d3bc3..508174b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,9 +80,6 @@ * Runtime changes to the app-id (OSC-176) now limits the app-id string to 2048 characters ([#1897][1897]). -* `colors.flash-alpha` can no longer be set to 1.0 (i.e. fully - opaque). This fixes an issue where the window would be stuck in the - flash state. [1897]: https://codeberg.org/dnkl/foot/issues/1897 diff --git a/config.c b/config.c index 3b8ce7e8..7f1ce055 100644 --- a/config.c +++ b/config.c @@ -1445,8 +1445,8 @@ parse_section_colors(struct context *ctx) if (!value_to_float(ctx, &alpha)) return false; - if (alpha < 0. || alpha >= 1.) { - LOG_CONTEXTUAL_ERR("not in range 0.0-0.999"); + if (alpha < 0. || alpha > 1.) { + LOG_CONTEXTUAL_ERR("not in range 0.0-1.0"); return false; } diff --git a/render.c b/render.c index 020dee79..b1791a90 100644 --- a/render.c +++ b/render.c @@ -1898,27 +1898,6 @@ render_overlay(struct terminal *term) break; case OVERLAY_FLASH: - /* - * A compositor will not send a frame callback for our main - * window if it is fully occluded (for example, by a fully - * opaque overlay...). This causes the overlay to stuck. - * - * For regular buffers, it _should_ be enough to *not* hint - * the compositor it's opaque. But at least some compositor - * special cases single-pixel buffers, and actually look at - * their pixel value. - * - * Thus, we have two options: implement frame callback - * handling for the overlay sub-surface, or ensure we don't - * use a fully opaque surface. Since no overlays are fully - * opaque by default, and the flash surface is the only one - * that can be configured to be opaque (colors.flash-alpha), - * and since adding frame callback handling adds a lot of - * boilerplate code... let's go with the simpler solution of - * not allowing colors.flash-alpha to be 1.0. - */ - xassert(term->conf->colors.flash_alpha != 0xffff); - color = color_hex_to_pixman_with_alpha( term->conf->colors.flash, term->conf->colors.flash_alpha); From 39061e0422a850ec5e76cb92cc903c4cec3b4b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 9 Jan 2025 07:56:10 +0100 Subject: [PATCH 0983/1323] changelog: colors.flash-alpha=1.0 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 508174b2..87694630 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,12 @@ ### Deprecated ### Removed ### Fixed + +* 'flash' overlay (triggered by either `tput flash`, or enabling + `bell.visual` and then sending `BEL` to the terminal) stuck when + `colors.flash-alpha=1.0`. + + ### Security ### Contributors From c5529808c4f13c5bd30594055563b83e1db61dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 9 Jan 2025 07:49:29 +0100 Subject: [PATCH 0984/1323] term: cursor_refresh(): don't try to dirty the grid if we don't have one If the compositor sends a keyboard enter event before our window has been mapped, foot crashes; the enter event triggers a cursor refresh (hollow -> non-hollow block cursor), which crashes since we haven't yet allocated a grid. Fix by no-op:ing the refresh if the window hasn't been configured yet. Closes #1910 --- CHANGELOG.md | 4 ++++ terminal.c | 3 +++ 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87694630..be321556 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,10 @@ * 'flash' overlay (triggered by either `tput flash`, or enabling `bell.visual` and then sending `BEL` to the terminal) stuck when `colors.flash-alpha=1.0`. +* Crash when compositor sends a keyboard enter event before the foot + window has been mapped ([#1910][1910]). + +[1910]: https://codeberg.org/dnkl/foot/issues/1910 ### Security diff --git a/terminal.c b/terminal.c index 7b5a4d2d..e392c36d 100644 --- a/terminal.c +++ b/terminal.c @@ -515,6 +515,9 @@ term_arm_blink_timer(struct terminal *term) static void cursor_refresh(struct terminal *term) { + if (!term->window->is_configured) + return; + term->grid->cur_row->cells[term->grid->cursor.point.col].attrs.clean = 0; term->grid->cur_row->dirty = true; render_refresh(term); From b808eb5162bf89edfb548dd4a622081a0f6a8054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 10 Jan 2025 13:05:35 +0100 Subject: [PATCH 0985/1323] forgejo: bugs: add required field 'distro' --- .forgejo/issue_template/issue_template.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index cca40dd5..fa602200 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -34,6 +34,14 @@ body: placeholder: "sway version 1.9" validations: required: true + - type: input + id: distro + attributes: + label: Distribution + description: "The name of the Linux distribution, or BSD flavor, you are running" + placeholder: "Arch Linux" + validations: + required: true - type: textarea id: repro attributes: From 14cd12899261fc8b3a65be1461639889074654f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 10 Jan 2025 13:13:05 +0100 Subject: [PATCH 0986/1323] forgejo: bugs: add required field 'config' Require all bug submitters to include their foot configs. --- .forgejo/issue_template/issue_template.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index fa602200..e650d8d0 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -42,6 +42,13 @@ body: placeholder: "Arch Linux" validations: required: true + - type: textarea + id: config + attributes: + label: Foot config + description: paste your entire `foot.ini` here + validations: + required: true - type: textarea id: repro attributes: From 9361596d024dd2fb191eaef71c40256b9e86d45c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 10 Jan 2025 13:14:02 +0100 Subject: [PATCH 0987/1323] forgejo: bugs: config: uppercase description's first letter --- .forgejo/issue_template/issue_template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index e650d8d0..ca12c4a3 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -46,7 +46,7 @@ body: id: config attributes: label: Foot config - description: paste your entire `foot.ini` here + description: Paste your entire `foot.ini` here validations: required: true - type: textarea From 077177e8a937ce24f2127c403dcc13c6cea8d973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 10 Jan 2025 13:15:02 +0100 Subject: [PATCH 0988/1323] forgejo: bugs: short explanation of what an IME is --- .forgejo/issue_template/issue_template.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index ca12c4a3..eae2e492 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -74,8 +74,8 @@ body: other terminal multiplexer? Does the bug happen in a plain foot instance? - **IME** do you use an IME? Which one? Does the bug happen if - you disable the IME? + **IME** do you use an IME (e.g. fcitx5, ibus etc)? Which one? + Does the bug happen if you disable the IME? Obtaining logs and stacktraces ------------------------------ From 15d9b08307c370c803aedb75a2bf3e806e1cb573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 18 Jan 2025 09:25:19 +0100 Subject: [PATCH 0989/1323] changelog: prepare for 1.20.2 --- CHANGELOG.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be321556..c23a7445 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.20.2](#1-20-2) * [1.20.1](#1-20-1) * [1.20.0](#1-20-0) * [1.19.0](#1-19-0) @@ -57,8 +57,8 @@ * [1.2.0](#1-2-0) -## Unreleased -### Added +## 1.20.2 + ### Changed * The `CSI 21 t` (report window title) and `OSC 176 ?` (report app-id) @@ -67,8 +67,6 @@ [1894]: https://codeberg.org/dnkl/foot/issues/1894 -### Deprecated -### Removed ### Fixed * 'flash' overlay (triggered by either `tput flash`, or enabling @@ -80,9 +78,10 @@ [1910]: https://codeberg.org/dnkl/foot/issues/1910 -### Security ### Contributors +* Alexander Orzechowski + ## 1.20.1 From 771af699f0dfb2b9c2564e93a5b5cc6a31e34de1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 18 Jan 2025 09:25:36 +0100 Subject: [PATCH 0990/1323] meson: bump version to 1.20.2 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index ee5af2f3..0c7f5656 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.20.1', + version: '1.20.2', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From e1d9b57f832cf799a8772fd5fb5790113d7f1186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 18 Jan 2025 09:27:07 +0100 Subject: [PATCH 0991/1323] changelog: add back entry to 1.20.1, removed in de3becef96f30b96f0fceddf73805c3951ccb59f --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c23a7445..db11e8ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,6 +89,9 @@ * Runtime changes to the app-id (OSC-176) now limits the app-id string to 2048 characters ([#1897][1897]). +* `colors.flash-alpha` can no longer be set to 1.0 (i.e. fully + opaque). This fixes an issue where the window would be stuck in the + flash state. [1897]: https://codeberg.org/dnkl/foot/issues/1897 From bb6061894152d2506080bf93ca2055c5fef06ce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 18 Jan 2025 09:30:07 +0100 Subject: [PATCH 0992/1323] changelog: move utf8proc entry to correct release --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db11e8ea..8649e967 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,8 +74,11 @@ `colors.flash-alpha=1.0`. * Crash when compositor sends a keyboard enter event before the foot window has been mapped ([#1910][1910]). +* Build failures (`utf8proc.h` not found) on at least FreeBSD, but + most likely other BSDs, as well as some Linuxes ([#1903][1903]). [1910]: https://codeberg.org/dnkl/foot/issues/1910 +[1903]: https://codeberg.org/dnkl/foot/issues/1903 ### Contributors @@ -100,11 +103,8 @@ * Regression: trying to print a Unicode _"Legacy Computing symbol"_, in the range U+1FB00 - U+1FB9B would crash foot ([#1901][1901]). -* Build failures (`utf8proc.h` not found) on at least FreeBSD, but - most likely other BSDs, as well as some Linuxes ([#1903][1903]). [1901]: https://codeberg.org/dnkl/foot/issues/1901 -[1903]: https://codeberg.org/dnkl/foot/issues/1903 ## 1.20.0 From 22e1b1610f622ca9e2a7e64593025a8d44bc7b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 18 Jan 2025 10:22:24 +0100 Subject: [PATCH 0993/1323] vt: combining chars: ensure 'key' is within range When there's a key collision, we increment the key and check again. When doing this, we need to ensure the key is withing range, and wrap around to 0 if the key value is too large. --- vt.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vt.c b/vt.c index 95cfdd2e..2b5eb27d 100644 --- a/vt.c +++ b/vt.c @@ -845,6 +845,7 @@ action_utf8_print(struct terminal *term, char32_t wc) cc->chars[0], base, cc->count, wanted_count, cc->chars[wanted_count - 1], wc); #endif key++; + key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO; collision_count++; continue; } @@ -856,6 +857,7 @@ action_utf8_print(struct terminal *term, char32_t wc) if (!match) { key++; + key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO; collision_count++; continue; } From 2ff38e86a7fabd8fd90caa31c7eb0aea76fc981b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 20 Jan 2025 09:08:47 +0100 Subject: [PATCH 0994/1323] input: kitty: fix alternate codepoint sometimes not being reported When alternate key reporting is enabled (i.e. when we're supposed to report the shifted key along with the unshifted key), we try to figure out whether the key really is shifted or not (and thus which xkb keysym to use for the unshifted and shifted keys in the escape). This was done by getting the layout's *all* modifier combinations that produce the shifted keysym, and if any of of them contained a modifier that isn't supported by the kitty protocol, the shifted and unshifted keys are derived from the same keysym. This is to ensure we handle things like AltGr-combos correctly. The issue is, since there may be more than one modifier combination generating the shifted keysym, we may end up using the wrong keysym just because _another_ combination set contains modifiers not supported by the kitty protocol. What we're interrested in is whether the *pressed* set of modifiers contains such modifiers. Closes #1918 --- CHANGELOG.md | 7 +++++++ input.c | 25 ++----------------------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cb18305..d4212c68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,13 @@ ### Deprecated ### Removed ### Fixed + +* Kitty keyboard protocol: alternate key reporting failing to report + the alternate codepoint in some corner cases ([#1918][1918]). + +[1918]: https://codeberg.org/dnkl/foot/issues/1918 + + ### Security ### Contributors diff --git a/input.c b/input.c index 51425a36..7b4cb667 100644 --- a/input.c +++ b/input.c @@ -1315,29 +1315,8 @@ emit_escapes: * (yes, this matches what kitty does, as of 0.23.1) */ - /* Get the key's shift level */ - xkb_level_index_t lvl = xkb_state_key_get_level( - seat->kbd.xkb_state, ctx->key, ctx->layout); - - /* And get all modifier combinations that, combined with - * the pressed key, results in the current shift level */ - xkb_mod_mask_t masks[32]; - size_t mask_count = xkb_keymap_key_get_mods_for_level( - seat->kbd.xkb_keymap, ctx->key, ctx->layout, lvl, - masks, ALEN(masks)); - - /* Check modifier combinations - if a combination has - * modifiers not in our set of 'significant' modifiers, - * use key sym as-is */ - bool use_level0_sym = true; - for (size_t i = 0; i < mask_count; i++) { - if ((masks[i] & ~seat->kbd.kitty_significant) > 0) { - use_level0_sym = false; - break; - } - } - - xkb_keysym_t sym_to_use = use_level0_sym && ctx->level0_syms.count > 0 + const bool use_level0_sym = (ctx->mods & ~seat->kbd.kitty_significant) == 0; + const xkb_keysym_t sym_to_use = use_level0_sym && ctx->level0_syms.count > 0 ? ctx->level0_syms.syms[0] : sym; From 786037791c2e775c98287d794277b6ef0752aab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 20 Jan 2025 10:34:45 +0100 Subject: [PATCH 0995/1323] input: kitty: improve handling of alternate+base keys even more * Always do a base key lookup. Before this, we didn't do that if we matched the XKB sym to a lookup table entry (i.e. keypads, cursor keys etc.) * Try to retrieve the unshifted symbol also when we matched the symbol to a lookup table entry. When successful, we now report an alternate key for keypad and cursor keys; before this patch, we only did that for keys that didn't have an entry in the lookup table (i.e. ASCII chars etc). This improves compatibility with kitty (and the kitty keyboard protocol) in more corner cases. One particular example is the neo keyboard layout, where part of the regular keys act as keypad keys when num-lock is active. --- input.c | 74 ++++++++++++++++++++++++++++++++------------------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/input.c b/input.c index 7b4cb667..615eb00d 100644 --- a/input.c +++ b/input.c @@ -1279,75 +1279,83 @@ emit_escapes: int key = -1, alternate = -1, base = -1; char final; + const bool use_level0_sym = + (ctx->mods & ~seat->kbd.kitty_significant) == 0 && ctx->level0_syms.count > 0; + if (info != NULL) { if (!info->is_modifier || report_all_as_escapes) { key = info->key; final = info->final; + + if (use_level0_sym) { + xkb_keysym_t unshifted = xkb_keysym_to_utf32(ctx->level0_syms.syms[0]); + if (unshifted > 0) { + alternate = key; + key = unshifted; + } + } } } else { /* * Use keysym (typically its Unicode codepoint value). * * If the keysym is shifted, use its unshifted codepoint - * instead. In other words, ctrl+a and ctrl+shift+a should - * both use the same value for 'key' (97 - i.a. 'a'). + * instead. In other words, ctrl+a and ctrl+shift+a should both + * use the same value for 'key' (97 - i.a. 'a'). * - * However, don't do this if a non-significant modifier was - * used to generate the symbol. This is needed since we cannot - * encode non-significant modifiers, and thus the "extra" - * modifier(s) would get lost. + * However, don't do this if a non-significant modifier was used + * to generate the symbol. This is needed since we cannot encode + * non-significant modifiers, and thus the "extra" modifier(s) + * would get lost. * * Example: * - * the Swedish layout has '2', QUOTATION MARK ("double - * quote"), '@', and '²' on the same key. '2' is the base - * symbol. + * the Swedish layout has '2', QUOTATION MARK ("double quote"), + * '@', and '²' on the same key. '2' is the base symbol. * * Shift+2 results in QUOTATION MARK * AltGr+2 results in '@' * AltGr+Shift+2 results in '²' * - * The kitty kbd protocol can't encode AltGr. So, if we - * always used the base symbol ('2'), Alt+Shift+2 would - * result in the same escape sequence as - * AltGr+Alt+Shift+2. + * The kitty kbd protocol can't encode AltGr. So, if we always + * used the base symbol ('2'), Alt+Shift+2 would result in the + * same escape sequence as AltGr+Alt+Shift+2. * * (yes, this matches what kitty does, as of 0.23.1) */ - const bool use_level0_sym = (ctx->mods & ~seat->kbd.kitty_significant) == 0; - const xkb_keysym_t sym_to_use = use_level0_sym && ctx->level0_syms.count > 0 - ? ctx->level0_syms.syms[0] - : sym; - if (composed) key = utf32[0]; /* TODO: what if there are multiple codepoints? */ else { - key = xkb_keysym_to_utf32(sym_to_use); + key = xkb_keysym_to_utf32( + use_level0_sym ? ctx->level0_syms.syms[0] : sym); + if (key == 0) return false; - - /* The *shifted* key. May be the same as the unshifted - * key - if so, this is filtered out below, when - * emitting the CSI */ - alternate = xkb_keysym_to_utf32(sym); } - /* Base layout key. I.e the symbol the pressed key produces in - * the base/default layout (layout idx 0) */ - const xkb_keysym_t *base_syms; - int base_sym_count = xkb_keymap_key_get_syms_by_level( - seat->kbd.xkb_keymap, ctx->key, 0, 0, &base_syms); - - if (base_sym_count > 0) - base = xkb_keysym_to_utf32(base_syms[0]); - + /* + * The *shifted* key. May be the same as the unshifted key - + * if so, this is filtered out below, when emitting the CSI. + * + * Note that normally, only the *unshifted* key is emitted - see below + */ + alternate = xkb_keysym_to_utf32(sym); final = 'u'; } if (key < 0) return false; + /* Base layout key. I.e the symbol the pressed key produces in + * the base/default layout (layout idx 0) */ + const xkb_keysym_t *base_syms; + int base_sym_count = xkb_keymap_key_get_syms_by_level( + seat->kbd.xkb_keymap, ctx->key, 0, 0, &base_syms); + + if (base_sym_count > 0) + base = xkb_keysym_to_utf32(base_syms[0]); + xassert(encoded_mods >= 1); char event[4]; From 09f718878f8afa54cb38d046db0af12e1ff679a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 21 Jan 2025 08:37:30 +0100 Subject: [PATCH 0996/1323] input: kitty: add initial unit test Test a couple of key combos from the se and de(neo) layouts. This unit test isn't intended to test _all_ key combinations, for all kitty flag combinations, but to be more of a regression test - whenever we discover a buggy, weird combo, add it here. --- input.c | 229 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) diff --git a/input.c b/input.c index 615eb00d..4f136181 100644 --- a/input.c +++ b/input.c @@ -1752,6 +1752,235 @@ keyboard_modifiers(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, term_xcursor_update_for_seat(seat->kbd_focus, seat); } +UNITTEST +{ + int chan[2]; + pipe2(chan, O_CLOEXEC); + + xassert(chan[0] >= 0); + xassert(chan[1] >= 0); + + struct config conf = {0}; + struct grid grid = {0}; + + struct terminal term = { + .conf = &conf, + .grid = &grid, + .ptmx = chan[1], + .selection = { + .coords = { + .start = {-1, -1}, + .end = {-1, -1}, + }, + .auto_scroll = { + .fd = -1, + }, + }, + }; + + struct key_binding_manager *key_binding_manager = key_binding_manager_new(); + + struct wayland wayl = { + .key_binding_manager = key_binding_manager, + .terms = tll_init(), + }; + + struct seat seat = { + .wayl = &wayl, + .name = "unittest", + }; + + tll_push_back(wayl.terms, &term); + term.wl = &wayl; + + seat.kbd.xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + xassert(seat.kbd.xkb != NULL); + + grid.kitty_kbd.flags[0] = KITTY_KBD_DISAMBIGUATE | KITTY_KBD_REPORT_ALTERNATE; + + /* Swedish keymap */ + { + seat.kbd.xkb_keymap = xkb_keymap_new_from_names( + seat.kbd.xkb, &(struct xkb_rule_names){.layout = "se"}, XKB_KEYMAP_COMPILE_NO_FLAGS); + if (seat.kbd.xkb_keymap == NULL) { + /* Skip test */ + goto no_keymap; + } + + seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap); + xassert(seat.kbd.xkb_state != NULL); + + seat.kbd.mod_shift = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); + seat.kbd.mod_alt = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; + seat.kbd.mod_ctrl = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL); + seat.kbd.mod_super = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO); + seat.kbd.mod_caps = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS); + seat.kbd.mod_num = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM); + + /* Significant modifiers in the legacy keyboard protocol */ + seat.kbd.legacy_significant = 0; + if (seat.kbd.mod_shift != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift; + if (seat.kbd.mod_alt != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt; + if (seat.kbd.mod_ctrl != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl; + if (seat.kbd.mod_super != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super; + + /* Significant modifiers in the kitty keyboard protocol */ + seat.kbd.kitty_significant = seat.kbd.legacy_significant; + if (seat.kbd.mod_caps != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps; + if (seat.kbd.mod_num != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num; + + key_binding_new_for_seat(key_binding_manager, &seat); + key_binding_load_keymap(key_binding_manager, &seat); + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_ctrl; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_A + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key: 97 = 'a', alternate: 65 = 'A', base: N/A, mods: 6 = ctrl+shift */ + const char expected_ctrl_shift_a[] = "\033[97:65;6u"; + xassert(count == strlen(expected_ctrl_shift_a)); + xassert(streq(escape, expected_ctrl_shift_a)); + + key_press_release(&seat, &term, 1337, KEY_A + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key;. 50 = '2', alternate: 34 = '"', base: N/A, 4 = alt+shift */ + const char expected_alt_shift_2[] = "\033[50:34;4u"; + xassert(count == strlen(expected_alt_shift_2)); + xassert(streq(escape, expected_alt_shift_2)); + + key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_index_t alt_gr = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, "Mod5"); + xassert(alt_gr != XKB_MOD_INVALID); + + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt | 1u << alt_gr; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key; 178 = '²', alternate: N/A, base: 50 = '2', 4 = alt+shift (AltGr not part of the protocol) */ + const char expected_altgr_alt_shift_2[] = "\033[178::50;4u"; + xassert(count == strlen(expected_altgr_alt_shift_2)); + xassert(streq(escape, expected_altgr_alt_shift_2)); + + key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + key_binding_unload_keymap(key_binding_manager, &seat); + key_binding_remove_seat(key_binding_manager, &seat); + + xkb_state_unref(seat.kbd.xkb_state); + xkb_keymap_unref(seat.kbd.xkb_keymap); + + seat.kbd.xkb_state = NULL; + seat.kbd.xkb_keymap = NULL; + } + + /* de(neo) keymap */ + { + seat.kbd.xkb_keymap = xkb_keymap_new_from_names( + seat.kbd.xkb, &(struct xkb_rule_names){.layout = "us,de(neo)"}, + XKB_KEYMAP_COMPILE_NO_FLAGS); + + if (seat.kbd.xkb_keymap == NULL) { + /* Skip test */ + goto no_keymap; + } + + seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap); + xassert(seat.kbd.xkb_state != NULL); + + seat.kbd.mod_shift = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); + seat.kbd.mod_alt = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; + seat.kbd.mod_ctrl = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL); + seat.kbd.mod_super = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO); + seat.kbd.mod_caps = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS); + seat.kbd.mod_num = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM); + + /* Significant modifiers in the legacy keyboard protocol */ + seat.kbd.legacy_significant = 0; + if (seat.kbd.mod_shift != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift; + if (seat.kbd.mod_alt != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt; + if (seat.kbd.mod_ctrl != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl; + if (seat.kbd.mod_super != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super; + + /* Significant modifiers in the kitty keyboard protocol */ + seat.kbd.kitty_significant = seat.kbd.legacy_significant; + if (seat.kbd.mod_caps != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps; + if (seat.kbd.mod_num != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num; + + key_binding_new_for_seat(key_binding_manager, &seat); + key_binding_load_keymap(key_binding_manager, &seat); + + { + /* + * In the de(neo) layout, the Y key generates 'k'. This + * means we should get a key+alternate that indiciates + * 'k', but a base key that is 'y'. + */ + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 1); + key_press_release(&seat, &term, 1337, KEY_Y + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key: 107 = 'k', alternate: 75 = 'K', base: 121 = 'y', mods: 4 = alt+shift */ + const char expected_alt_shift_y[] = "\033[107:75:121;4u"; + xassert(count == strlen(expected_alt_shift_y)); + xassert(streq(escape, expected_alt_shift_y)); + + key_press_release(&seat, &term, 1337, KEY_Y + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + key_binding_unload_keymap(key_binding_manager, &seat); + key_binding_remove_seat(key_binding_manager, &seat); + + xkb_state_unref(seat.kbd.xkb_state); + xkb_keymap_unref(seat.kbd.xkb_keymap); + + seat.kbd.xkb_state = NULL; + seat.kbd.xkb_keymap = NULL; + } + +no_keymap: + xkb_context_unref(seat.kbd.xkb); + key_binding_manager_destroy(key_binding_manager); + + tll_free(wayl.terms); + close(chan[0]); + close(chan[1]); +} + static void keyboard_repeat_info(void *data, struct wl_keyboard *wl_keyboard, int32_t rate, int32_t delay) From 5e65f3f07e41065a6fbd1af74e43acd553e1d5b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 22 Jan 2025 07:50:49 +0100 Subject: [PATCH 0997/1323] shm: codespell: re-using -> reusing --- shm.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shm.c b/shm.c index bbf6f933..9a745f6c 100644 --- a/shm.c +++ b/shm.c @@ -608,7 +608,7 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height, bool with_alph } if (cached != NULL) { - LOG_DBG("re-using buffer %p from cache", (void *)cached); + LOG_DBG("reusing buffer %p from cache", (void *)cached); cached->busy = true; for (size_t i = 0; i < cached->public.pix_instances; i++) pixman_region32_clear(&cached->public.dirty[i]); From f62a5ed1ff139b70d6c12ee3033cc956c3d93723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 22 Jan 2025 07:51:00 +0100 Subject: [PATCH 0998/1323] input: codespell: indiciates -> indicates --- input.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/input.c b/input.c index 4f136181..7e0f3e58 100644 --- a/input.c +++ b/input.c @@ -1944,8 +1944,8 @@ UNITTEST { /* * In the de(neo) layout, the Y key generates 'k'. This - * means we should get a key+alternate that indiciates - * 'k', but a base key that is 'y'. + * means we should get a key+alternate that indicates 'k', + * but a base key that is 'y'. */ xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt; keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 1); From 6ca1a2c2dcdcbd3432a953e19f55829b6f07e7f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 22 Jan 2025 10:26:44 +0100 Subject: [PATCH 0999/1323] input: kitty: only set 'alternate' if the "unshifted" code is printable Fixes a regression where alt+backspace was reported as ^[[8:127;3u instead of ^[[127;3u. --- input.c | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/input.c b/input.c index 7e0f3e58..79c5d8a0 100644 --- a/input.c +++ b/input.c @@ -1213,7 +1213,7 @@ kitty_kbd_protocol(struct seat *seat, struct terminal *term, bool is_text = count > 0 && utf32 != NULL && (mods & ~locked & ~consumed) == 0; for (size_t i = 0; utf32[i] != U'\0'; i++) { - if (!iswprint(utf32[i])) { + if (!isc32print(utf32[i])) { is_text = false; break; } @@ -1282,18 +1282,12 @@ emit_escapes: const bool use_level0_sym = (ctx->mods & ~seat->kbd.kitty_significant) == 0 && ctx->level0_syms.count > 0; + int unshifted = use_level0_sym ? xkb_keysym_to_utf32(ctx->level0_syms.syms[0]) : 0; + if (info != NULL) { if (!info->is_modifier || report_all_as_escapes) { key = info->key; final = info->final; - - if (use_level0_sym) { - xkb_keysym_t unshifted = xkb_keysym_to_utf32(ctx->level0_syms.syms[0]); - if (unshifted > 0) { - alternate = key; - key = unshifted; - } - } } } else { /* @@ -1327,26 +1321,23 @@ emit_escapes: if (composed) key = utf32[0]; /* TODO: what if there are multiple codepoints? */ else { - key = xkb_keysym_to_utf32( - use_level0_sym ? ctx->level0_syms.syms[0] : sym); + key = xkb_keysym_to_utf32(sym); if (key == 0) return false; } - /* - * The *shifted* key. May be the same as the unshifted key - - * if so, this is filtered out below, when emitting the CSI. - * - * Note that normally, only the *unshifted* key is emitted - see below - */ - alternate = xkb_keysym_to_utf32(sym); final = 'u'; } if (key < 0) return false; + if (unshifted > 0 && isc32print(unshifted)) { + alternate = key; + key = unshifted; + } + /* Base layout key. I.e the symbol the pressed key produces in * the base/default layout (layout idx 0) */ const xkb_keysym_t *base_syms; @@ -1378,7 +1369,7 @@ emit_escapes: if (report_alternate) { bool emit_alternate = alternate > 0 && alternate != key; - bool emit_base = base > 0 && base != key && base != alternate; + bool emit_base = base > 0 && base != key && base != alternate && isc32print(base); if (emit_alternate) { bytes = snprintf(p, left, ":%u", alternate); @@ -1889,6 +1880,22 @@ UNITTEST key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_RELEASED); } + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_alt; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_BACKSPACE + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key; 127 = <backspace>, alternate: N/A, base: N/A, 3 = alt */ + const char expected_alt_backspace[] = "\033[127;3u"; + xassert(count == strlen(expected_alt_backspace)); + xassert(streq(escape, expected_alt_backspace)); + + key_press_release(&seat, &term, 1337, KEY_BACKSPACE + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + key_binding_unload_keymap(key_binding_manager, &seat); key_binding_remove_seat(key_binding_manager, &seat); From f301f6ecccc4a6be70da793c56814e50d13e93db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 22 Jan 2025 12:24:06 +0100 Subject: [PATCH 1000/1323] input: kitty: add more test cases --- input.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/input.c b/input.c index 79c5d8a0..fc1270c7 100644 --- a/input.c +++ b/input.c @@ -1896,6 +1896,52 @@ UNITTEST key_press_release(&seat, &term, 1337, KEY_BACKSPACE + 8, WL_KEYBOARD_KEY_STATE_RELEASED); } + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_ENTER + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key; 13 = <enter>, alternate: N/A, base: N/A, 5 = ctrl */ + const char expected_ctrl_enter[] = "\033[13;5u"; + xassert(count == strlen(expected_ctrl_enter)); + xassert(streq(escape, expected_ctrl_enter)); + + key_press_release(&seat, &term, 1337, KEY_ENTER + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_TAB + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key; 9 = <tab>, alternate: N/A, base: N/A, 5 = ctrl */ + const char expected_ctrl_tab[] = "\033[9;5u"; + xassert(count == strlen(expected_ctrl_tab)); + xassert(streq(escape, expected_ctrl_tab)); + + key_press_release(&seat, &term, 1337, KEY_TAB + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl | 1u << seat.kbd.mod_shift; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_LEFT + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + const char expected_ctrl_shift_left[] = "\033[1;6D"; + xassert(count == strlen(expected_ctrl_shift_left)); + xassert(streq(escape, expected_ctrl_shift_left)); + + key_press_release(&seat, &term, 1337, KEY_LEFT + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } key_binding_unload_keymap(key_binding_manager, &seat); key_binding_remove_seat(key_binding_manager, &seat); From ba7ecc4669ab0bef850318478cf739016a2fc6a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 22 Jan 2025 12:37:36 +0100 Subject: [PATCH 1001/1323] input: kitty: refactor, try to simplify and be less confusing Use better named variables while juggling the shifted vs. unshifted key codes. Switch to variable names appropriate for the kitty keyboard protocol once we have all the unshifted, shifted and base key codes done. It's not until then we can decide which key code to use as the main key, and whether or not to report the alternate key code. --- input.c | 107 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 60 insertions(+), 47 deletions(-) diff --git a/input.c b/input.c index fc1270c7..f5daf6b4 100644 --- a/input.c +++ b/input.c @@ -1276,68 +1276,48 @@ emit_escapes: encoded_mods |= mods & (1 << seat->kbd.mod_num) ? (1 << 7) : 0; encoded_mods++; - int key = -1, alternate = -1, base = -1; + /* + * Figure out the main, alternate and base key codes. + * + * The main key is the unshifted version of the generated symbol, + * the alternate key is the shifted version, and base is the + * (unshifted) key assuming the default layout. + * + * For example, the user presses shift+a, then: + * - unshifted = 'a' + * - shifted = 'A' + * - base = 'a' + * + * Base will in many cases be the same as the unshifted key, but + * may differ if the active keyboard layout is non-ASCII (examples + * would be russian, or alternative layouts like neo etc). + * + * The shifted key is what we get from XKB, i.e. the resulting key + * from all active modifiers, plus the pressed key. + */ + int unshifted = -1, shifted = -1, base = -1; char final; - const bool use_level0_sym = - (ctx->mods & ~seat->kbd.kitty_significant) == 0 && ctx->level0_syms.count > 0; - - int unshifted = use_level0_sym ? xkb_keysym_to_utf32(ctx->level0_syms.syms[0]) : 0; - if (info != NULL) { + /* Use code from lookup table (cursor keys, enter, tab etc)*/ if (!info->is_modifier || report_all_as_escapes) { - key = info->key; + shifted = info->key; final = info->final; } } else { - /* - * Use keysym (typically its Unicode codepoint value). - * - * If the keysym is shifted, use its unshifted codepoint - * instead. In other words, ctrl+a and ctrl+shift+a should both - * use the same value for 'key' (97 - i.a. 'a'). - * - * However, don't do this if a non-significant modifier was used - * to generate the symbol. This is needed since we cannot encode - * non-significant modifiers, and thus the "extra" modifier(s) - * would get lost. - * - * Example: - * - * the Swedish layout has '2', QUOTATION MARK ("double quote"), - * '@', and '²' on the same key. '2' is the base symbol. - * - * Shift+2 results in QUOTATION MARK - * AltGr+2 results in '@' - * AltGr+Shift+2 results in '²' - * - * The kitty kbd protocol can't encode AltGr. So, if we always - * used the base symbol ('2'), Alt+Shift+2 would result in the - * same escape sequence as AltGr+Alt+Shift+2. - * - * (yes, this matches what kitty does, as of 0.23.1) - */ + /* Use keysym (typically its Unicode codepoint value) */ if (composed) - key = utf32[0]; /* TODO: what if there are multiple codepoints? */ - else { - key = xkb_keysym_to_utf32(sym); - - if (key == 0) - return false; - } + shifted = utf32[0]; /* TODO: what if there are multiple codepoints? */ + else + shifted = xkb_keysym_to_utf32(sym); final = 'u'; } - if (key < 0) + if (shifted <= 0) return false; - if (unshifted > 0 && isc32print(unshifted)) { - alternate = key; - key = unshifted; - } - /* Base layout key. I.e the symbol the pressed key produces in * the base/default layout (layout idx 0) */ const xkb_keysym_t *base_syms; @@ -1347,6 +1327,36 @@ emit_escapes: if (base_sym_count > 0) base = xkb_keysym_to_utf32(base_syms[0]); + /* + * If the keysym is shifted, use its unshifted codepoint + * instead. In other words, ctrl+a and ctrl+shift+a should both + * use the same value for 'key' (97 - i.a. 'a'). + * + * However, don't do this if a non-significant modifier was used + * to generate the symbol. This is needed since we cannot encode + * non-significant modifiers, and thus the "extra" modifier(s) + * would get lost. + * + * Example: + * + * the Swedish layout has '2', QUOTATION MARK ("double quote"), + * '@', and '²' on the same key. '2' is the base symbol. + * + * Shift+2 results in QUOTATION MARK + * AltGr+2 results in '@' + * AltGr+Shift+2 results in '²' + * + * The kitty kbd protocol can't encode AltGr. So, if we always + * used the base symbol ('2'), Alt+Shift+2 would result in the + * same escape sequence as AltGr+Alt+Shift+2. + * + * (yes, this matches what kitty does, as of 0.23.1) + */ + const bool use_level0_sym = + (ctx->mods & ~seat->kbd.kitty_significant) == 0 && ctx->level0_syms.count > 0; + + unshifted = use_level0_sym ? xkb_keysym_to_utf32(ctx->level0_syms.syms[0]) : 0; + xassert(encoded_mods >= 1); char event[4]; @@ -1363,6 +1373,9 @@ emit_escapes: size_t left = sizeof(buf); size_t bytes; + const int key = unshifted > 0 && isc32print(unshifted) ? unshifted : shifted; + const int alternate = shifted; + if (final == 'u' || final == '~') { bytes = snprintf(p, left, "\x1b[%u", key); p += bytes; left -= bytes; From 736328ab6b1d2a8cc90745e6003f4031e1fef5fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 24 Jan 2025 06:38:02 +0100 Subject: [PATCH 1002/1323] config: check for FcNameUnparse() failure --- config.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config.c b/config.c index 68d49207..a0d32869 100644 --- a/config.c +++ b/config.c @@ -3729,6 +3729,11 @@ config_font_parse(const char *pattern, struct config_font *font) LOG_DBG("%s: pt-size=%.2f, px-size=%d", stripped_pattern, pt_size, px_size); + if (stripped_pattern == NULL) { + LOG_ERR("failed to convert font pattern to string"); + return false; + } + *font = (struct config_font){ .pattern = stripped_pattern, .pt_size = pt_size, From bfabc5450b3a01c7d701809b3cf6158220ea289e Mon Sep 17 00:00:00 2001 From: camel-cdr <camel-cdr@noreply.codeberg.org> Date: Wed, 22 Jan 2025 19:38:11 +0000 Subject: [PATCH 1003/1323] fix infinite loop/oom when cwd longer then 1024 The code reads cwd into a buffer, which is expanded while errno is ERANGE, with the intent of growing the buffer until the path fits. While getcwd will set errno on error, it will not reset it once the path fits into the buffer. So to not get an infinite loop once errno is ERANGE, we need to make sure to reset errno, such that the loop behaves as expected. --- main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.c b/main.c index c5c11080..0ba574c9 100644 --- a/main.c +++ b/main.c @@ -552,10 +552,10 @@ main(int argc, char *const *argv) char *_cwd = NULL; if (cwd == NULL) { - errno = 0; size_t buf_len = 1024; do { _cwd = xrealloc(_cwd, buf_len); + errno = 0; if (getcwd(_cwd, buf_len) == NULL && errno != ERANGE) { LOG_ERRNO("failed to get current working directory"); goto out; From 787e886ff0281e53432366cca8456c210d3a919c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 24 Jan 2025 06:51:13 +0100 Subject: [PATCH 1004/1323] client: port bfabc5450b3a01c7d701809b3cf6158220ea289e to footclient --- client.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client.c b/client.c index 757f4d41..dabcc327 100644 --- a/client.c +++ b/client.c @@ -401,10 +401,10 @@ main(int argc, char *const *argv) const char *cwd = custom_cwd; if (cwd == NULL) { - errno = 0; size_t buf_len = 1024; do { _cwd = xrealloc(_cwd, buf_len); + errno = 0; if (getcwd(_cwd, buf_len) == NULL && errno != ERANGE) { LOG_ERRNO("failed to get current working directory"); goto err; From f39b75f2960abf370c9ed455f8f0ca8b87055027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 24 Jan 2025 06:52:52 +0100 Subject: [PATCH 1005/1323] changelog: cwd > 1024 chars --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4212c68..b45aa01f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,9 @@ * Kitty keyboard protocol: alternate key reporting failing to report the alternate codepoint in some corner cases ([#1918][1918]). +* `foot` and `footclient` hanging, or terminating with `SIGABRT`, when + starting inside a directory whose total length is more than 1024 + characters. [1918]: https://codeberg.org/dnkl/foot/issues/1918 From 97385b007f2bb9cd54e701e5745d6e8d607513c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 25 Jan 2025 08:46:21 +0100 Subject: [PATCH 1006/1323] grid: reflow: regression: remove (truncate) SPACER cells at the end of line When printing a double-width glyph at the end of the line, it will get pushed to the next line if there's only one cell left on the current line. That last cell on the current line is filled with a SPACER value. When reflowing the text, the SPACER cell should be "removed", so that the double-width glyph continues directly after the text on the previous line. 9567694bab7149afbb4e4fcc4c754272a8d25aba fixed an issue where reflowing e.g. neofetch output incorrectly removed spaces between the logo, and the system info. But also introduced a regression where SPACER values no longer are removed. This patch tries to fix it, by adding back empty cells, but NOT SPACER cells. --- CHANGELOG.md | 4 ++++ grid.c | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b45aa01f..a249ef2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,10 @@ * `foot` and `footclient` hanging, or terminating with `SIGABRT`, when starting inside a directory whose total length is more than 1024 characters. +* Regression: reflowing (resizing the window) a line that ends with a + double-width glyph that was pushed to the next line due to there + being only one cell left on current line, did not remove the virtual + space inserted at the end of the current line. [1918]: https://codeberg.org/dnkl/foot/issues/1918 diff --git a/grid.c b/grid.c index 2f65a1dd..3f5c617d 100644 --- a/grid.c +++ b/grid.c @@ -930,7 +930,8 @@ grid_resize_and_reflow( if (!old_row->linebreak && col_count > 0) { /* Don't truncate logical lines */ - col_count = old_cols; + while (col_count < old_cols && old_row->cells[col_count].wc == 0) + col_count++; } xassert(col_count >= 0 && col_count <= old_cols); From 846271e8d3056ef3ecd3d2f4b7df6151e17c3c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 26 Jan 2025 09:28:54 +0100 Subject: [PATCH 1007/1323] render: resize: configure with only one dimension being zero The protocol states: If the width or height arguments are zero, it means the client should decide its own window dimension. This may happen when the compositor needs to configure the state of the surface but doesn't have any information about any previous or expected dimension. The wording is a bit ambiguous; does it mean we should set *both* width and height to values we choose, even if only one dimension is zero in the configure event? Or does it mean that we should choose the value for the dimension that is zero in the configure event? Regardless, it's pretty clear that it does *not* mean we should *only* choose width and height if *both* dimensions are zero in the configure event. This is foot's behavior before this patch, meaning if only one of them is zero, foot assumed the compositor wanted us to set the width (or height) to zero... Change this, so that we now choose value for the "missing" dimension, but do use the compositor provided value for the other dimension. Closes #1925 Relevant issues: * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/155 * https://github.com/YaLTeR/niri/issues/1050 --- CHANGELOG.md | 8 ++++++++ render.c | 57 ++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a249ef2d..97becf79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,14 @@ ### Changed + +* Do not try to set a zero width, or height, if the compositor sends a + _configure_ event with only one dimension being zero + ([#1925][1925]). + +[1925]: https://codeberg.org/dnkl/foot/issues/1925 + + ### Deprecated ### Removed ### Fixed diff --git a/render.c b/render.c index b1791a90..0cca0643 100644 --- a/render.c +++ b/render.c @@ -4295,17 +4295,24 @@ send_dimensions_to_client(struct terminal *term) static void set_size_from_grid(struct terminal *term, int *width, int *height, int cols, int rows) { + int new_width, new_height; + /* Nominal grid dimensions */ - *width = cols * term->cell_width; - *height = rows * term->cell_height; + new_width = cols * term->cell_width; + new_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; + new_width += 2 * term->conf->pad_x * term->scale; + new_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)); + new_width = round(term->scale * round(new_width / term->scale)); + new_height = round(term->scale * round(new_height / term->scale)); + + if (width != NULL) + *width = new_width; + if (height != NULL) + *height = new_height; } /* Move to terminal.c? */ @@ -4336,34 +4343,54 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) 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 (term->stashed_width != 0 && term->stashed_height != 0) { + if (width == 0) { + /* The compositor is letting us choose the width */ + if (term->stashed_width != 0) { /* If a default size is requested, prefer the "last used" size */ width = term->stashed_width; - height = term->stashed_height; } else { /* Otherwise, use a user-configured size */ switch (term->conf->size.type) { case CONF_SIZE_PX: width = term->conf->size.width; + + if (wayl_win_csd_borders_visible(term->window)) + width -= 2 * term->conf->csd.border_width_visible; + + width *= scale; + break; + + case CONF_SIZE_CELLS: + set_size_from_grid(term, &width, NULL, + term->conf->size.width, term->conf->size.height); + break; + } + } + } + + if (height == 0) { + /* The compositor is letting us choose the height */ + if (term->stashed_height != 0) { + /* If a default size is requested, prefer the "last used" size */ + height = term->stashed_height; + } else { + /* Otherwise, use a user-configured size */ + switch (term->conf->size.type) { + case CONF_SIZE_PX: height = term->conf->size.height; /* Take CSDs into account */ if (wayl_win_csd_titlebar_visible(term->window)) height -= term->conf->csd.title_height; - if (wayl_win_csd_borders_visible(term->window)) { + if (wayl_win_csd_borders_visible(term->window)) height -= 2 * term->conf->csd.border_width_visible; - width -= 2 * term->conf->csd.border_width_visible; - } - width *= scale; height *= scale; break; case CONF_SIZE_CELLS: - set_size_from_grid(term, &width, &height, + set_size_from_grid(term, NULL, &height, term->conf->size.width, term->conf->size.height); break; } From 43206e66016175407abd11c415c7135c293a0c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 27 Jan 2025 06:34:20 +0100 Subject: [PATCH 1008/1323] config: fix memory leak on e.g. "not a valid XKB key name" errors --- config.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/config.c b/config.c index a0d32869..12336e3d 100644 --- a/config.c +++ b/config.c @@ -1928,7 +1928,8 @@ value_to_key_combos(struct context *ctx, int action, if (idx == 0) { LOG_CONTEXTUAL_ERR( "empty binding not allowed (set to 'none' to unmap)"); - goto err; + free(copy); + return false; } remove_from_key_bindings_list(bindings, action, aux); @@ -1946,10 +1947,8 @@ value_to_key_combos(struct context *ctx, int action, return true; err: - if (idx > 0) { - for (size_t i = 0; i < used_combos; i++) - free_key_binding(&new_combos[i]); - } + for (size_t i = 0; i < used_combos; i++) + free_key_binding(&new_combos[i]); free(copy); return false; } From fda9638edd5e87c52c6a62c781a8f0e6df9deb65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 27 Jan 2025 06:36:54 +0100 Subject: [PATCH 1009/1323] forgejo: add optional field for shell/TUI --- .forgejo/issue_template/issue_template.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index eae2e492..e4ad7944 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -42,6 +42,12 @@ body: placeholder: "Arch Linux" validations: required: true + - type: input + id: application + attributes: + label: Shell, TUI, application + description: "Application in which the problem occurs (list all known)" + placeholder: "e.g. bash, neovim" - type: textarea id: config attributes: From 7a5353d18adcca26ab70ccb97873751d90c51694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 27 Jan 2025 06:38:14 +0100 Subject: [PATCH 1010/1323] forgejo: application -> application(s) --- .forgejo/issue_template/issue_template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index e4ad7944..98b9c919 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -46,7 +46,7 @@ body: id: application attributes: label: Shell, TUI, application - description: "Application in which the problem occurs (list all known)" + description: "Application(s) in which the problem occurs (list all known)" placeholder: "e.g. bash, neovim" - type: textarea id: config From 8d6f0d0583a1cb7c245ff54ec472a03ebcdb6949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 27 Jan 2025 10:51:03 +0100 Subject: [PATCH 1011/1323] key-bindings: try all bindings in translated mode before matching untranslated, and then finally raw When trying to match key bindings, we do three types of matching: * Match the _translated_ symbol (e.g. Control+C) * Match the _untranslated_ symbol (e.g. Control+Shift+c) * Match raw keyboard codes This was done for *each* key binding. This meant we sometimes matched a keybinding in raw mode, even though there was a translated/untranslated binding that would match it too. All depending on the internal order of the key binding list. This patch changes it, so that we first try all bindings in translated mode, then all bindings in untranslated mode, and finally all bindings in raw mode. Closes #1929 --- CHANGELOG.md | 3 +++ input.c | 17 ++++++++++++++--- search.c | 22 ++++++++++++++++++---- url-mode.c | 19 +++++++++++++++++-- 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97becf79..61559359 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,8 +89,11 @@ double-width glyph that was pushed to the next line due to there being only one cell left on current line, did not remove the virtual space inserted at the end of the current line. +* Wrong key bindings executed when using alternative keyboard layouts + ([#1929][1929]). [1918]: https://codeberg.org/dnkl/foot/issues/1918 +[1929]: https://codeberg.org/dnkl/foot/issues/1929 ### Security diff --git a/input.c b/input.c index f5daf6b4..2db696c1 100644 --- a/input.c +++ b/input.c @@ -1582,21 +1582,25 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, * User configurable bindings */ if (pressed) { + /* Match translated symbol */ tll_foreach(bindings->key, it) { const struct key_binding *bind = &it->item; - /* Match translated symbol */ if (bind->k.sym == sym && bind->mods == (mods & ~consumed) && execute_binding(seat, term, bind, serial, 1)) { goto maybe_repeat; } + } + + /* Match untranslated symbols */ + tll_foreach(bindings->key, it) { + const struct key_binding *bind = &it->item; if (bind->mods != mods) continue; - /* Match untranslated symbols */ for (size_t i = 0; i < raw_count; i++) { if (bind->k.sym == raw_syms[i] && execute_binding(seat, term, bind, serial, 1)) @@ -1604,8 +1608,15 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, goto maybe_repeat; } } + } + + /* Match raw key code */ + tll_foreach(bindings->key, it) { + const struct key_binding *bind = &it->item; + + if (bind->mods != mods) + continue; - /* Match raw key code */ tll_foreach(bind->k.key_codes, code) { if (code->item == key && execute_binding(seat, term, bind, serial, 1)) diff --git a/search.c b/search.c index eaf8c34e..bcb354d6 100644 --- a/search.c +++ b/search.c @@ -1388,11 +1388,14 @@ search_input(struct seat *seat, struct terminal *term, bool update_search_result = false; bool redraw = false; - /* Key bindings */ + /* + * Key bindings + */ + + /* Match translated symbol */ tll_foreach(bindings->search, it) { const struct key_binding *bind = &it->item; - /* Match translated symbol */ if (bind->k.sym == sym && bind->mods == (mods & ~consumed)) { @@ -1404,11 +1407,15 @@ search_input(struct seat *seat, struct terminal *term, } return; } + } + + /* Match untranslated symbols */ + tll_foreach(bindings->search, it) { + const struct key_binding *bind = &it->item; if (bind->mods != mods) continue; - /* Match untranslated symbols */ for (size_t i = 0; i < raw_count; i++) { if (bind->k.sym == raw_syms[i]) { if (execute_binding(seat, term, bind, serial, @@ -1420,8 +1427,15 @@ search_input(struct seat *seat, struct terminal *term, return; } } + } + + /* Match raw key code */ + tll_foreach(bindings->search, it) { + const struct key_binding *bind = &it->item; + + if (bind->mods != mods) + continue; - /* Match raw key code */ tll_foreach(bind->k.key_codes, code) { if (code->item == key) { if (execute_binding(seat, term, bind, serial, diff --git a/url-mode.c b/url-mode.c index 20c9820b..cca7bd22 100644 --- a/url-mode.c +++ b/url-mode.c @@ -178,11 +178,14 @@ urls_input(struct seat *seat, struct terminal *term, const xkb_keysym_t *raw_syms, size_t raw_count, uint32_t serial) { - /* Key bindings */ + /* + * Key bindings + */ + + /* Match translated symbol */ tll_foreach(bindings->url, it) { const struct key_binding *bind = &it->item; - /* Match translated symbol */ if (bind->k.sym == sym && bind->mods == (mods & ~consumed)) { @@ -190,6 +193,11 @@ urls_input(struct seat *seat, struct terminal *term, return; } + } + + /* Match untranslated symbols */ + tll_foreach(bindings->url, it) { + const struct key_binding *bind = &it->item; if (bind->mods != mods) continue; @@ -199,6 +207,13 @@ urls_input(struct seat *seat, struct terminal *term, return; } } + } + + /* Match raw key code */ + tll_foreach(bindings->url, it) { + const struct key_binding *bind = &it->item; + if (bind->mods != mods) + continue; /* Match raw key code */ tll_foreach(bind->k.key_codes, code) { From 1c7c9f6c16a55c47ea8f2740cd9d7619c4e0f9a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 27 Jan 2025 12:31:50 +0100 Subject: [PATCH 1012/1323] doc: foot.ini: describe key binding match logic --- doc/foot.ini.5.scd | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 733168bf..35f78674 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1096,7 +1096,36 @@ Note that *Alt* is usually called *Mod1*. *xkbcli interactive-wayland* can be useful for finding keysym names. -A key combination can only be mapped to *one* action. Lets say you +When matching key presses to key bindings, foot uses a couple of +different approaches. + +As an example, let's say you press ctrl+shift+c (assume plain us ASCII +layout). XKB will tell foot *Control+C* was pressed. Note the lack of +the shift modifier, and the upper case 'C'. Internally, this is called +the "translated" form, and is what foot tries to match first. + +If no "translated" key bindings can be found, foot proceeds to +checking the "untranslated" variant. Using the same example as above, +this will match *Control+Shift+c* (shift modifier present, lower case +'c'). + +This means you can use either form in your foot configuration, and +that *Control+C* (and similar) has higher priority than +*Control+Shift+c*. Also note that while foot normally detects when the +same combination is assigned to multiple actions, it will not detect +*Control+C* vs. *Control+Shift+c* collisions. Call it a known bug... + +Finally, foot tries to match the raw key code. Here, the primary +layout is queried for all key codes that generate a particular XKB +symbol, and the pressed key's code is matched against this. For +example, if you use the layouts *"us,de(neo)"*, the 'r' key generates +the symbol 'c' in the neo layout. I.e. to get a 'c', you press +'r'. The match logic described above will only match 'c' key bindings +(e.g. *Control+Shift+c*). The raw mode however, will match 'r' key +bindings (e.g. *Control+Shift+r*). This is useful for non-latin +layouts, where you would otherwise have to customize all key bindings. + +A key combination can only be mapped to *one* action. Let's say you want to bind *Control+Shift+R* to *fullscreen*. Since this is the default shortcut for *search-start*, you first need to unmap the default binding. This can be done by setting _action=none_; @@ -1453,7 +1482,7 @@ events never generate a *COUNT* larger than 1. That is, Foot also recognizes tiltable wheels; to map these, use *BTN_WHEEL_LEFT* and *BTN_WHEEL_RIGHT*. -A modifier+button combination can only be mapped to *one* action. Lets +A modifier+button combination can only be mapped to *one* action. Let's say you want to bind *BTN\_MIDDLE* to *fullscreen*. Since *BTN\_MIDDLE* is the default binding for *primary-paste*, you first need to unmap the default binding. This can be done by setting From 8b408f00397b11cbd47ddf050a23027245d81949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 27 Jan 2025 13:15:59 +0100 Subject: [PATCH 1013/1323] forgejo: add optional field for terminal multiplexers --- .forgejo/issue_template/issue_template.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index 98b9c919..1545fe85 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -42,12 +42,18 @@ body: placeholder: "Arch Linux" validations: required: true + - type: input + id: multiplexer + attributes: + label: Terminal multiplexer + description: "Terminal multiplexers are terminal emulators themselves, therefore the issue may be in the multiplexer, not foot. Please list which multiplexer(s) you use here (and mention in the problem description below if the issue only occurs in the multiplexer, but not in bare metal foot)" + placeholder: "tmux, zellij" - type: input id: application attributes: - label: Shell, TUI, application - description: "Application(s) in which the problem occurs (list all known)" - placeholder: "e.g. bash, neovim" + label: Shell, TUI, application + description: "Application(s) in which the problem occurs (list all known)" + placeholder: "e.g. bash, neovim" - type: textarea id: config attributes: From c2c8d29272f81ef8c468b687f247574a9b62f5a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 27 Jan 2025 13:16:43 +0100 Subject: [PATCH 1014/1323] forgejo: remove "e.g." from placeholder text --- .forgejo/issue_template/issue_template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index 1545fe85..805d15ce 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -53,7 +53,7 @@ body: attributes: label: Shell, TUI, application description: "Application(s) in which the problem occurs (list all known)" - placeholder: "e.g. bash, neovim" + placeholder: "bash, neovim" - type: textarea id: config attributes: From 6e2bdd663aa69176d670e3c8fd8e9a213f5e3e62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 27 Jan 2025 13:18:09 +0100 Subject: [PATCH 1015/1323] forgejo: config: render as .ini, instead of the default markdown --- .forgejo/issue_template/issue_template.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index 805d15ce..fd2e62e5 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -59,6 +59,7 @@ body: attributes: label: Foot config description: Paste your entire `foot.ini` here + render: ini validations: required: true - type: textarea From 5286808b6cd439c0442a76169d7a58b1b7e71e8c Mon Sep 17 00:00:00 2001 From: Attila Fidan <dev@print0.net> Date: Thu, 30 Jan 2025 09:39:31 +0000 Subject: [PATCH 1016/1323] input: close fd on no/unrecognized keymap format --- CHANGELOG.md | 2 ++ input.c | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61559359..a14b0fab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,8 @@ space inserted at the end of the current line. * Wrong key bindings executed when using alternative keyboard layouts ([#1929][1929]). +* Foot not closing file descriptors for unrecognized or `no_keymap` + keymaps. [1918]: https://codeberg.org/dnkl/foot/issues/1918 [1929]: https://codeberg.org/dnkl/foot/issues/1929 diff --git a/input.c b/input.c index 2db696c1..a2f30a0c 100644 --- a/input.c +++ b/input.c @@ -527,6 +527,7 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, /* Verify keymap is in a format we understand */ switch ((enum wl_keyboard_keymap_format)format) { case WL_KEYBOARD_KEYMAP_FORMAT_NO_KEYMAP: + close(fd); return; case WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1: @@ -534,6 +535,7 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, default: LOG_WARN("unrecognized keymap format: %u", format); + close(fd); return; } From d24f700256240ae38505a4ab501e25814133c011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 31 Jan 2025 07:29:16 +0100 Subject: [PATCH 1017/1323] key-bindings: add keypad variants to existing default key-bindings --- config.c | 5 +++++ doc/foot.ini.5.scd | 13 ++++++++----- foot.ini | 10 +++++----- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/config.c b/config.c index 12336e3d..a9db87d5 100644 --- a/config.c +++ b/config.c @@ -3006,7 +3006,9 @@ add_default_key_bindings(struct config *conf) { const struct config_key_binding bindings[] = { {BIND_ACTION_SCROLLBACK_UP_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Prior}}}, + {BIND_ACTION_SCROLLBACK_UP_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_KP_Prior}}}, {BIND_ACTION_SCROLLBACK_DOWN_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Next}}}, + {BIND_ACTION_SCROLLBACK_DOWN_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_KP_Next}}}, {BIND_ACTION_CLIPBOARD_COPY, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_c}}}, {BIND_ACTION_CLIPBOARD_COPY, m("none"), {{XKB_KEY_XF86Copy}}}, {BIND_ACTION_CLIPBOARD_PASTE, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_v}}}, @@ -3037,11 +3039,14 @@ add_default_search_bindings(struct config *conf) { const struct config_key_binding bindings[] = { {BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Prior}}}, + {BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_KP_Prior}}}, {BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Next}}}, + {BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_KP_Next}}}, {BIND_ACTION_SEARCH_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_c}}}, {BIND_ACTION_SEARCH_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_g}}}, {BIND_ACTION_SEARCH_CANCEL, m("none"), {{XKB_KEY_Escape}}}, {BIND_ACTION_SEARCH_COMMIT, m("none"), {{XKB_KEY_Return}}}, + {BIND_ACTION_SEARCH_COMMIT, m("none"), {{XKB_KEY_KP_Enter}}}, {BIND_ACTION_SEARCH_FIND_PREV, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_r}}}, {BIND_ACTION_SEARCH_FIND_NEXT, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_s}}}, {BIND_ACTION_SEARCH_EDIT_LEFT, m("none"), {{XKB_KEY_Left}}}, diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 35f78674..903d3375 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1136,7 +1136,8 @@ e.g. *search-start=none*. application. Default: _none_. *scrollback-up-page* - Scrolls up/back one page in history. Default: _Shift+Page\_Up_. + Scrolls up/back one page in history. Default: _Shift+Page\_Up + Shift+KP\_Page\_Up_. *scrollback-up-half-page* Scrolls up/back half of a page in history. Default: _none_. @@ -1146,7 +1147,7 @@ e.g. *search-start=none*. *scrollback-down-page* Scroll down/forward one page in history. Default: - _Shift+Page\_Down_. + _Shift+Page\_Down Shift+KP\_Page\_Down_. *scrollback-down-half-page* Scroll down/forward half of a page in history. Default: _none_. @@ -1288,7 +1289,8 @@ scrollback search mode. The syntax is exactly the same as the regular *commit* Exit search mode and copy current selection into the _primary selection_. Viewport is **not** restored. To copy the selection to - the regular _clipboard_, use *Control+Shift+c*. Default: _Return_. + the regular _clipboard_, use *Control+Shift+c*. Default: _Return + KP_Enter_. *find-prev* Search **backwards** in the scrollback history for the next @@ -1379,7 +1381,8 @@ scrollback search mode. The syntax is exactly the same as the regular details. Default: _none_. *scrollback-up-page* - Scrolls up/back one page in history. Default: _Shift+Page\_Up_. + Scrolls up/back one page in history. Default: _Shift+Page\_Up + Shift+KP\_Page\_Up_. *scrollback-up-half-page* Scrolls up/back half of a page in history. Default: _none_. @@ -1389,7 +1392,7 @@ scrollback search mode. The syntax is exactly the same as the regular *scrollback-down-page* Scroll down/forward one page in history. Default: - _Shift+Page\_Down_. + _Shift+Page\_Down Shift+KP\_Page\_Down_. *scrollback-down-half-page* Scroll down/forward half of a page in history. Default: _none_. diff --git a/foot.ini b/foot.ini index 580178af..17fabd3d 100644 --- a/foot.ini +++ b/foot.ini @@ -167,10 +167,10 @@ # button-close-color=<regular1> [key-bindings] -# scrollback-up-page=Shift+Page_Up +# scrollback-up-page=Shift+Page_Up Shift+KP_Page_Up # scrollback-up-half-page=none # scrollback-up-line=none -# scrollback-down-page=Shift+Page_Down +# scrollback-down-page=Shift+Page_Down Shift+KP_Page_Down # scrollback-down-half-page=none # scrollback-down-line=none # scrollback-home=none @@ -201,7 +201,7 @@ [search-bindings] # cancel=Control+g Control+c Escape -# commit=Return +# commit=Return KP_Enter # find-prev=Control+r # find-next=Control+s # cursor-left=Left Control+b @@ -225,10 +225,10 @@ # clipboard-paste=Control+v Control+Shift+v Control+y XF86Paste # primary-paste=Shift+Insert # unicode-input=none -# scrollback-up-page=Shift+Page_Up +# scrollback-up-page=Shift+Page_Up Shift+KP_Page_Up # scrollback-up-half-page=none # scrollback-up-line=none -# scrollback-down-page=Shift+Page_Down +# scrollback-down-page=Shift+Page_Down Shift+KP_Page_Down # scrollback-down-half-page=none # scrollback-down-line=none # scrollback-home=none From bee17a95b86a640522c25e650e13aebc82c7353d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 31 Jan 2025 07:35:54 +0100 Subject: [PATCH 1018/1323] input: ignore key-bindings without modifiers when matching untranslated/raw When matching the unshifted symbol, or the raw key code, ignore all key bindings that don't have any modifiers. This fixes an issue where it was impossible to enter (some of the) numbers on the keypad, **if** there was a key-binding for e.g. KP_Page_Up, or KP_Page_Down. --- input.c | 4 ++-- search.c | 4 ++-- url-mode.c | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/input.c b/input.c index a2f30a0c..22566411 100644 --- a/input.c +++ b/input.c @@ -1600,7 +1600,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, tll_foreach(bindings->key, it) { const struct key_binding *bind = &it->item; - if (bind->mods != mods) + if (bind->mods != mods || bind->mods == 0) continue; for (size_t i = 0; i < raw_count; i++) { @@ -1616,7 +1616,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, tll_foreach(bindings->key, it) { const struct key_binding *bind = &it->item; - if (bind->mods != mods) + if (bind->mods != mods || bind->mods == 0) continue; tll_foreach(bind->k.key_codes, code) { diff --git a/search.c b/search.c index bcb354d6..75f12b4a 100644 --- a/search.c +++ b/search.c @@ -1413,7 +1413,7 @@ search_input(struct seat *seat, struct terminal *term, tll_foreach(bindings->search, it) { const struct key_binding *bind = &it->item; - if (bind->mods != mods) + if (bind->mods != mods || bind->mods == 0) continue; for (size_t i = 0; i < raw_count; i++) { @@ -1433,7 +1433,7 @@ search_input(struct seat *seat, struct terminal *term, tll_foreach(bindings->search, it) { const struct key_binding *bind = &it->item; - if (bind->mods != mods) + if (bind->mods != mods || bind->mods == 0) continue; tll_foreach(bind->k.key_codes, code) { diff --git a/url-mode.c b/url-mode.c index cca7bd22..83dbfa70 100644 --- a/url-mode.c +++ b/url-mode.c @@ -198,7 +198,7 @@ urls_input(struct seat *seat, struct terminal *term, /* Match untranslated symbols */ tll_foreach(bindings->url, it) { const struct key_binding *bind = &it->item; - if (bind->mods != mods) + if (bind->mods != mods || bind->mods == 0) continue; for (size_t i = 0; i < raw_count; i++) { @@ -212,7 +212,7 @@ urls_input(struct seat *seat, struct terminal *term, /* Match raw key code */ tll_foreach(bindings->url, it) { const struct key_binding *bind = &it->item; - if (bind->mods != mods) + if (bind->mods != mods || bind->mods == 0) continue; /* Match raw key code */ From 51128a3484049e5f9b9b74276222977d12554d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 31 Jan 2025 09:07:42 +0100 Subject: [PATCH 1019/1323] input: match unshifted key-bindings before shifted That is, try to match e.g. Control+shift+a, before trying to match Control+A. In most cases, order doesn't matter. There are however a couple of symbols where the layout consumes the shift-modifier, and the generated symbol is the same in both the shifted and unshifted form. One such example is backspace. Before this patch, key-bindings with shift-backspace would be ignored, if there were another key-binding with backspace. So, for example, if we had one key-binding with Control+Backspace, and another with Control+Shift+Backspace, the latter would never trigger, as we would always match the first one. By checking for unshifted matches first, we ensure Control+Shift+Backspace does match. --- input.c | 24 ++++++++++++------------ search.c | 34 +++++++++++++++++----------------- url-mode.c | 26 +++++++++++++------------- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/input.c b/input.c index 22566411..c3ddbf13 100644 --- a/input.c +++ b/input.c @@ -1584,18 +1584,6 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, * User configurable bindings */ if (pressed) { - /* Match translated symbol */ - tll_foreach(bindings->key, it) { - const struct key_binding *bind = &it->item; - - if (bind->k.sym == sym && - bind->mods == (mods & ~consumed) && - execute_binding(seat, term, bind, serial, 1)) - { - goto maybe_repeat; - } - } - /* Match untranslated symbols */ tll_foreach(bindings->key, it) { const struct key_binding *bind = &it->item; @@ -1612,6 +1600,18 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, } } + /* Match translated symbol */ + tll_foreach(bindings->key, it) { + const struct key_binding *bind = &it->item; + + if (bind->k.sym == sym && + bind->mods == (mods & ~consumed) && + execute_binding(seat, term, bind, serial, 1)) + { + goto maybe_repeat; + } + } + /* Match raw key code */ tll_foreach(bindings->key, it) { const struct key_binding *bind = &it->item; diff --git a/search.c b/search.c index 75f12b4a..20990c87 100644 --- a/search.c +++ b/search.c @@ -1392,23 +1392,6 @@ search_input(struct seat *seat, struct terminal *term, * Key bindings */ - /* Match translated symbol */ - tll_foreach(bindings->search, it) { - const struct key_binding *bind = &it->item; - - if (bind->k.sym == sym && - bind->mods == (mods & ~consumed)) { - - if (execute_binding(seat, term, bind, serial, - &update_search_result, &search_direction, - &redraw)) - { - goto update_search; - } - return; - } - } - /* Match untranslated symbols */ tll_foreach(bindings->search, it) { const struct key_binding *bind = &it->item; @@ -1429,6 +1412,23 @@ search_input(struct seat *seat, struct terminal *term, } } + /* Match translated symbol */ + tll_foreach(bindings->search, it) { + const struct key_binding *bind = &it->item; + + if (bind->k.sym == sym && + bind->mods == (mods & ~consumed)) { + + if (execute_binding(seat, term, bind, serial, + &update_search_result, &search_direction, + &redraw)) + { + goto update_search; + } + return; + } + } + /* Match raw key code */ tll_foreach(bindings->search, it) { const struct key_binding *bind = &it->item; diff --git a/url-mode.c b/url-mode.c index 83dbfa70..986860af 100644 --- a/url-mode.c +++ b/url-mode.c @@ -182,19 +182,6 @@ urls_input(struct seat *seat, struct terminal *term, * Key bindings */ - /* Match translated symbol */ - tll_foreach(bindings->url, it) { - const struct key_binding *bind = &it->item; - - if (bind->k.sym == sym && - bind->mods == (mods & ~consumed)) - { - execute_binding(seat, term, bind, serial); - return; - } - - } - /* Match untranslated symbols */ tll_foreach(bindings->url, it) { const struct key_binding *bind = &it->item; @@ -209,6 +196,19 @@ urls_input(struct seat *seat, struct terminal *term, } } + /* Match translated symbol */ + tll_foreach(bindings->url, it) { + const struct key_binding *bind = &it->item; + + if (bind->k.sym == sym && + bind->mods == (mods & ~consumed)) + { + execute_binding(seat, term, bind, serial); + return; + } + + } + /* Match raw key code */ tll_foreach(bindings->url, it) { const struct key_binding *bind = &it->item; From dc4e9fc25b541cb19c6e0db297764d3117e39c45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 4 Feb 2025 14:48:02 +0100 Subject: [PATCH 1020/1323] forgejo: ask user to provide distro *version*, when applicable --- .forgejo/issue_template/issue_template.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index fd2e62e5..8ac9edbc 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -38,8 +38,8 @@ body: id: distro attributes: label: Distribution - description: "The name of the Linux distribution, or BSD flavor, you are running" - placeholder: "Arch Linux" + description: "The name of the Linux distribution, or BSD flavor, you are running. And, if applicable, the version" + placeholder: "Fedora Workstation 41" validations: required: true - type: input From 9c882cfdabdd951ff11b5ae0dc4eb3da6e268f7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 4 Feb 2025 14:52:52 +0100 Subject: [PATCH 1021/1323] forgejo: issue happens in foot --server, standalone, or both? --- .forgejo/issue_template/issue_template.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index 8ac9edbc..a52d97b3 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -54,6 +54,14 @@ body: label: Shell, TUI, application description: "Application(s) in which the problem occurs (list all known)" placeholder: "bash, neovim" + - type: checkboxes + id: server + attributes: + label: Server/standalone mode + description: The issue occurs in foot server, or standalone mode, or both. Note that you **cannot** test standalone mode by manually running `foot` from a `footclient` instance, since then the standalone foot will simply inherit the server process' context. + options: + - label: standalone + - label: server - type: textarea id: config attributes: From 230d8b6f70804bb656e6989a868a2db2a6a868a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 4 Feb 2025 14:54:02 +0100 Subject: [PATCH 1022/1323] forgejo: server/standalone: tweak wording --- .forgejo/issue_template/issue_template.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index a52d97b3..51c85c3f 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -58,10 +58,10 @@ body: id: server attributes: label: Server/standalone mode - description: The issue occurs in foot server, or standalone mode, or both. Note that you **cannot** test standalone mode by manually running `foot` from a `footclient` instance, since then the standalone foot will simply inherit the server process' context. + description: Does the issue occur in foot server, or standalone mode, or both? Note that you **cannot** test standalone mode by manually running `foot` from a `footclient` instance, since then the standalone foot will simply inherit the server process' context. options: - - label: standalone - - label: server + - label: Standalone + - label: Server - type: textarea id: config attributes: From fcfdbeebcf17506634b75f763f54388148983a68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 4 Feb 2025 14:55:09 +0100 Subject: [PATCH 1023/1323] forgejo: remind user to sanitize pasted config --- .forgejo/issue_template/issue_template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index 51c85c3f..a5000090 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -66,7 +66,7 @@ body: id: config attributes: label: Foot config - description: Paste your entire `foot.ini` here + description: Paste your entire `foot.ini` here (do not forget to sanitize it!) render: ini validations: required: true From 70aa033d7912b978264d035b095b25090ff00f07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 4 Feb 2025 14:55:49 +0100 Subject: [PATCH 1024/1323] forgejo: server/standalone: what happens when we set required=true? --- .forgejo/issue_template/issue_template.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index a5000090..3c6dee90 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -61,7 +61,9 @@ body: description: Does the issue occur in foot server, or standalone mode, or both? Note that you **cannot** test standalone mode by manually running `foot` from a `footclient` instance, since then the standalone foot will simply inherit the server process' context. options: - label: Standalone + required: true - label: Server + required: true - type: textarea id: config attributes: From 6f9129fa3af94379db59641ebb2e950d65dffdab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 4 Feb 2025 14:56:22 +0100 Subject: [PATCH 1025/1323] Revert "forgejo: server/standalone: what happens when we set required=true?" This reverts commit 70aa033d7912b978264d035b095b25090ff00f07. --- .forgejo/issue_template/issue_template.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/issue_template.yml index 3c6dee90..a5000090 100644 --- a/.forgejo/issue_template/issue_template.yml +++ b/.forgejo/issue_template/issue_template.yml @@ -61,9 +61,7 @@ body: description: Does the issue occur in foot server, or standalone mode, or both? Note that you **cannot** test standalone mode by manually running `foot` from a `footclient` instance, since then the standalone foot will simply inherit the server process' context. options: - label: Standalone - required: true - label: Server - required: true - type: textarea id: config attributes: From 2fe72effa9870ee2d66aad56ce048960f933c96c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 5 Feb 2025 11:39:06 +0100 Subject: [PATCH 1026/1323] term: ptmx pause/resume: don't modify the FDM if ptmx has been closed This fixes error message spam when resizing a terminal window executed with --hold, and where the client application has terminated. --- terminal.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/terminal.c b/terminal.c index 6936ff29..9fd20002 100644 --- a/terminal.c +++ b/terminal.c @@ -387,12 +387,16 @@ fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) bool term_ptmx_pause(struct terminal *term) { + if (term->ptmx < 0) + return false; return fdm_event_del(term->fdm, term->ptmx, EPOLLIN); } bool term_ptmx_resume(struct terminal *term) { + if (term->ptmx < 0) + return false; return fdm_event_add(term->fdm, term->ptmx, EPOLLIN); } From 8de378963b90bca1bc932c094a78c683d01019aa Mon Sep 17 00:00:00 2001 From: sewn <sewn@disroot.org> Date: Wed, 5 Feb 2025 14:23:17 +0300 Subject: [PATCH 1027/1323] server: don't instantiate a client without a monitor --- server.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server.c b/server.c index 78d98d53..5981a14c 100644 --- a/server.c +++ b/server.c @@ -211,6 +211,12 @@ fdm_client(struct fdm *fdm, int fd, int events, void *data) return true; } + if (tll_length(server->wayl->monitors) == 0) { + LOG_ERR("no monitors available for new terminal"); + client_send_exit_code(client, -26); + goto shutdown; + } + /* All initialization data received - time to instantiate a terminal! */ xassert(client->instance == NULL); From 9443ac7e2937bb4f26cf44c73bb8150860c5df45 Mon Sep 17 00:00:00 2001 From: Thomas Bonnefille <thomas.bonnefille@bootlin.com> Date: Tue, 4 Feb 2025 09:48:13 +0100 Subject: [PATCH 1028/1323] box-drawings: handle architecture with soft-float Currently, architecture using soft-floats doesn't support instructions FE_INVALID, FE_DIVBYZERO, FE_OVERFLOW and FE_UNDERFLOW and so building on those architectures results with a build error. As the sqrt math function should set errno to EDOM if an error occurs, fetestexcept shouldn't be mandatory. This commit removes the float environment error handling. Signed-off-by: Thomas Bonnefille <thomas.bonnefille@bootlin.com> --- box-drawing.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/box-drawing.c b/box-drawing.c index 1c613051..421ff54d 100644 --- a/box-drawing.c +++ b/box-drawing.c @@ -1462,14 +1462,12 @@ draw_box_drawings_light_arc(struct buf *buf, char32_t wc) */ for (double i = y_min*16; i <= y_max*16; i++) { errno = 0; - feclearexcept(FE_ALL_EXCEPT); double y = i / 16.; double x = circle_hemisphere * sqrt(c_r2 - (y - c_y) * (y - c_y)) + c_x; /* See math_error(7) */ - if (errno != 0 || - fetestexcept(FE_INVALID | FE_DIVBYZERO | FE_OVERFLOW | FE_UNDERFLOW)) + if (errno != 0) { continue; } From aae794e9bdd5f19019560041479cf7ebc46b4fa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 30 Jan 2025 09:06:24 +0100 Subject: [PATCH 1029/1323] xmalloc: add xreallocarray() --- xmalloc.c | 7 +++++++ xmalloc.h | 1 + 2 files changed, 8 insertions(+) diff --git a/xmalloc.c b/xmalloc.c index ded7f4e3..0a67cdb2 100644 --- a/xmalloc.c +++ b/xmalloc.c @@ -36,6 +36,13 @@ xrealloc(void *ptr, size_t size) return unlikely(size == 0) ? alloc : check_alloc(alloc); } +void * +xreallocarray(void *ptr, size_t n, size_t size) +{ + void *alloc = reallocarray(ptr, n, size); + return unlikely(size == 0) ? alloc : check_alloc(alloc); +} + char * xstrdup(const char *str) { diff --git a/xmalloc.h b/xmalloc.h index 8a2c208f..03e6eb0d 100644 --- a/xmalloc.h +++ b/xmalloc.h @@ -12,6 +12,7 @@ void *xmalloc(size_t size) XMALLOC; void *xcalloc(size_t nmemb, size_t size) XMALLOC; void *xrealloc(void *ptr, size_t size); +void *xreallocarray(void *ptr, size_t n, size_t size); char *xstrdup(const char *str) XSTRDUP; char *xstrndup(const char *str, size_t n) XSTRDUP; char *xasprintf(const char *format, ...) PRINTF(1) XMALLOC; From 32919b1049b2f333208920f6b3c240fe7c637f13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 30 Jan 2025 09:06:40 +0100 Subject: [PATCH 1030/1323] grid: typo --- grid.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/grid.c b/grid.c index 3f5c617d..b7c0447c 100644 --- a/grid.c +++ b/grid.c @@ -36,7 +36,8 @@ grid_row_abs_to_sb(const struct grid *grid, int screen_rows, int abs_row) return rebased_row; } -int grid_row_sb_to_abs(const struct grid *grid, int screen_rows, int sb_rel_row) +int +grid_row_sb_to_abs(const struct grid *grid, int screen_rows, int sb_rel_row) { const int scrollback_start = grid->offset + screen_rows; int abs_row = sb_rel_row + scrollback_start; From 1c15ee940d0063b7cfa11845c0af537d50ca8acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 30 Jan 2025 09:06:47 +0100 Subject: [PATCH 1031/1323] url-mode: wip: convert to regex matching for auto-detection --- url-mode.c | 145 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/url-mode.c b/url-mode.c index 986860af..0ed79a99 100644 --- a/url-mode.c +++ b/url-mode.c @@ -4,6 +4,7 @@ #include <string.h> #include <wctype.h> #include <unistd.h> +#include <regex.h> #include <sys/stat.h> #include <fcntl.h> @@ -291,6 +292,149 @@ urls_input(struct seat *seat, struct terminal *term, } } +struct charmap { + const struct row *row; + int col; +}; + +struct vline { + char *utf8; + size_t len; + size_t sz; + struct charmap *map; +}; + +static void +regex_detected(const struct terminal *term, enum url_action action, url_list_t *urls) +{ + struct vline vlines[term->rows]; + size_t vline_idx = 0; + + memset(vlines, 0, sizeof(vlines)); + struct vline *vline = &vlines[vline_idx]; + + mbstate_t ps = {0}; + + for (int row_no = 0; row_no < term->rows; row_no++) { + const struct row *row = grid_row_in_view(term->grid, row_no); + + for (int c = 0; c < term->cols; c++) { + const struct cell *cell = &row->cells[c]; + const char32_t *wc = &cell->wc; + size_t wc_count = 1; + + if (wc[0] >= CELL_COMB_CHARS_LO && wc[0] <= CELL_COMB_CHARS_HI) { + const struct composed *composed = + composed_lookup(term->composed, wc[0] - CELL_COMB_CHARS_LO); + xassert(composed != NULL); + + wc = composed->chars; + wc_count = composed->count; + } + + for (size_t i = 0; i < wc_count; i++) { + char buf[16]; + size_t char_len = c32rtomb(buf, wc[i], &ps); + if (char_len == (size_t)-1) + continue; + + if (vline->len == 0 && char_len == 1 && buf[0] == 0) + continue; + + for (size_t j = 0; j < char_len; j++) { + if (vline->len + char_len > vline->sz) { + /* TODO: grow dynamically */ + size_t new_count = (vline->len + char_len) * 2; + vline->utf8 = xreallocarray(vline->utf8, new_count, 1); + vline->map = xreallocarray(vline->map, new_count, sizeof(vline->map[0])); + } + + vline->utf8[vline->len + j] = buf[j]; + vline->map[vline->len + j].col = c; + vline->map[vline->len + j].row = row; + } + + vline->len += char_len; + } + } + + if (row->linebreak) { + if (vline->len > 0) { + vline->utf8[vline->len++] = '\0'; + ps = (mbstate_t){0}; + + vline_idx++; + vline = &vlines[vline_idx]; + } + } + } + + // https://gist.github.com/gruber/249502 + regex_t preg; + const char *foo = + "(" + "[a-z][[:alpha:]-]+:" // protocol + "(" + "/{1,3}|[a-z0-9%]" // slashes (what's the OR part for?) + ")" + "|" + "www[:digit:]{0,3}[.]" + "|" + "[a-z0-9.\\-]+[.][a-z]{2,4}/" + ")" + "(" + "[^[:space:]()<>]+" + "|" + "\\(([^[:space:]()<>]+|(\\([^[:space:]()<>]+\\)))*\\)" + ")+" + "(" + "\\(([^[:space:]()<>]+|(\\([^[:space:]()<>]+\\)))*\\)" + "|" + // TODO: figure out how to add \\] to the expression below... + "[^[:space:]`!()\\[{};:'\".,<>?«»“”‘’]" + ")" + ; + + LOG_ERR("foo=%s", foo); + + int r = regcomp(&preg, foo, REG_EXTENDED); + + if (r != 0) { + char err_buf[1024]; + regerror(r, &preg, err_buf, sizeof(err_buf)); + LOG_ERR("regcomp: %s", err_buf); + } else { + size_t i = 0; + while (true) { + const struct vline *v = &vlines[i++]; + if (v->utf8 == NULL) + break; + + + regmatch_t matches[preg.re_nsub + 1]; + r = regexec(&preg, v->utf8, preg.re_nsub + 1, matches, 0); + + if (r == REG_NOMATCH) + continue; + + size_t mlen = matches[0].rm_eo - matches[0].rm_so; + LOG_WARN("MATCH at %d: %.*s (%zu)", matches[0].rm_so, (int)mlen, &v->utf8[matches[0].rm_so], mlen); + } + regfree(&preg); + } + + size_t i = 0; + while (true) { + const struct vline *v = &vlines[i++]; + if (v->utf8 == NULL) + break; + + LOG_WARN("%.*s", (int)v->len, v->utf8); + free(v->utf8); + free(v->map); + } +} + static int c32cmp_single(const void *_a, const void *_b) { @@ -634,6 +778,7 @@ urls_collect(const struct terminal *term, enum url_action action, url_list_t *ur xassert(tll_length(term->urls) == 0); osc8_uris(term, action, urls); auto_detected(term, action, urls); + regex_detected(term, action, urls); remove_overlapping(urls, term->grid->num_cols); } From 859b4c8921f5cde8cfc1f358e865ba417fe68a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 30 Jan 2025 09:51:50 +0100 Subject: [PATCH 1032/1323] url-mode: wip: more work on regex matching Remove the old auto-detection and instead use the regex matches. --- url-mode.c | 371 ++++++++++++----------------------------------------- 1 file changed, 80 insertions(+), 291 deletions(-) diff --git a/url-mode.c b/url-mode.c index 0ed79a99..18085ab5 100644 --- a/url-mode.c +++ b/url-mode.c @@ -292,21 +292,31 @@ urls_input(struct seat *seat, struct terminal *term, } } -struct charmap { - const struct row *row; - int col; -}; - struct vline { char *utf8; - size_t len; - size_t sz; - struct charmap *map; + size_t len; /* Length of utf8[] */ + size_t sz; /* utf8[] allocated size */ + struct coord *map; /* Maps utf8[ofs] to grid coordinates */ }; static void regex_detected(const struct terminal *term, enum url_action action, url_list_t *urls) { + /* + * Use regcomp()+regexec() to find patterns. + * + * Since we can't feed regexec() one character at a time, and + * since it doesn't accept wide characters, we need to build utf8 + * strings. + * + * Each string represents a logical line (i.e. handle line-wrap). + * To be able to map regex matches back to the grid, we store the + * grid coordinates of *each* character, in the line struct as + * well. This is offset based; utf8[ofs] has its grid coordinates + * in map[ofs. + */ + + /* There is *at most* term->rows logical lines */ struct vline vlines[term->rows]; size_t vline_idx = 0; @@ -315,14 +325,15 @@ regex_detected(const struct terminal *term, enum url_action action, url_list_t * mbstate_t ps = {0}; - for (int row_no = 0; row_no < term->rows; row_no++) { - const struct row *row = grid_row_in_view(term->grid, row_no); + for (int r = 0; r < term->rows; r++) { + const struct row *row = grid_row_in_view(term->grid, r); for (int c = 0; c < term->cols; c++) { const struct cell *cell = &row->cells[c]; const char32_t *wc = &cell->wc; size_t wc_count = 1; + /* Expand combining characters */ if (wc[0] >= CELL_COMB_CHARS_LO && wc[0] <= CELL_COMB_CHARS_HI) { const struct composed *composed = composed_lookup(term->composed, wc[0] - CELL_COMB_CHARS_LO); @@ -332,26 +343,26 @@ regex_detected(const struct terminal *term, enum url_action action, url_list_t * wc_count = composed->count; } + /* Convert wide character to utf8 */ for (size_t i = 0; i < wc_count; i++) { char buf[16]; size_t char_len = c32rtomb(buf, wc[i], &ps); + if (char_len == (size_t)-1) continue; - if (vline->len == 0 && char_len == 1 && buf[0] == 0) - continue; - for (size_t j = 0; j < char_len; j++) { - if (vline->len + char_len > vline->sz) { - /* TODO: grow dynamically */ - size_t new_count = (vline->len + char_len) * 2; - vline->utf8 = xreallocarray(vline->utf8, new_count, 1); - vline->map = xreallocarray(vline->map, new_count, sizeof(vline->map[0])); + const size_t requires_size = vline->len + char_len; + + if (requires_size > vline->sz) { + const size_t new_size = requires_size * 2; + vline->utf8 = xreallocarray(vline->utf8, new_size, 1); + vline->map = xreallocarray(vline->map, new_size, sizeof(vline->map[0])); + vline->sz = new_size; } vline->utf8[vline->len + j] = buf[j]; - vline->map[vline->len + j].col = c; - vline->map[vline->len + j].row = row; + vline->map[vline->len + j] = (struct coord){c, term->grid->view + r}; } vline->len += char_len; @@ -371,9 +382,9 @@ regex_detected(const struct terminal *term, enum url_action action, url_list_t * // https://gist.github.com/gruber/249502 regex_t preg; - const char *foo = + const char *regex_string = "(" - "[a-z][[:alpha:]-]+:" // protocol + "[a-z][[:alnum:]-]+:" // protocol "(" "/{1,3}|[a-z0-9%]" // slashes (what's the OR part for?) ")" @@ -390,291 +401,70 @@ regex_detected(const struct terminal *term, enum url_action action, url_list_t * "(" "\\(([^[:space:]()<>]+|(\\([^[:space:]()<>]+\\)))*\\)" "|" - // TODO: figure out how to add \\] to the expression below... - "[^[:space:]`!()\\[{};:'\".,<>?«»“”‘’]" + "[^]\\[[:space:]`!(){};:'\".,<>?«»“”‘’]" ")" ; - LOG_ERR("foo=%s", foo); - - int r = regcomp(&preg, foo, REG_EXTENDED); + int r = regcomp(&preg, regex_string, REG_EXTENDED); if (r != 0) { char err_buf[1024]; regerror(r, &preg, err_buf, sizeof(err_buf)); - LOG_ERR("regcomp: %s", err_buf); - } else { - size_t i = 0; - while (true) { - const struct vline *v = &vlines[i++]; - if (v->utf8 == NULL) - break; + LOG_ERR("failed to compile regular expression: %s", err_buf); - - regmatch_t matches[preg.re_nsub + 1]; - r = regexec(&preg, v->utf8, preg.re_nsub + 1, matches, 0); - - if (r == REG_NOMATCH) - continue; - - size_t mlen = matches[0].rm_eo - matches[0].rm_so; - LOG_WARN("MATCH at %d: %.*s (%zu)", matches[0].rm_so, (int)mlen, &v->utf8[matches[0].rm_so], mlen); + for (size_t i = 0; i < ALEN(vlines); i++) { + const struct vline *v = &vlines[i]; + free(v->utf8); + free(v->map); } - regfree(&preg); + + return; } - size_t i = 0; - while (true) { - const struct vline *v = &vlines[i++]; + for (size_t i = 0; i < ALEN(vlines); i++) { + const struct vline *v = &vlines[i]; if (v->utf8 == NULL) - break; + continue;; + + const char *search_string = v->utf8; + while (true) { + + regmatch_t matches[preg.re_nsub + 1]; + r = regexec(&preg, search_string, preg.re_nsub + 1, matches, 0); + + if (r == REG_NOMATCH) + break; + + const size_t mlen = matches[0].rm_eo - matches[0].rm_so; + const size_t start = &search_string[matches[0].rm_so] - v->utf8; + const size_t end = start + mlen; + + LOG_DBG( + "MATCH at %d: %.*s (%zu) row/col = %dx%d", + matches[0].rm_so, (int)mlen, &search_string[matches[0].rm_so], + mlen, v->map[start].row, v->map[start].col); + + tll_push_back( + *urls, + ((struct url){ + .id = (uint64_t)rand() << 32 | rand(), + .url = xstrndup(&v->utf8[start], mlen), + .range = { + .start = v->map[start], + .end = v->map[end - 1], /* Inclusive */ + }, + .action = action, + .osc8 = false})); + + search_string += matches[0].rm_eo; + } - LOG_WARN("%.*s", (int)v->len, v->utf8); free(v->utf8); free(v->map); } + + regfree(&preg); } - -static int -c32cmp_single(const void *_a, const void *_b) -{ - const char32_t *a = _a; - const char32_t *b = _b; - return *a - *b; -} - -static void -auto_detected(const struct terminal *term, enum url_action action, - url_list_t *urls) -{ - const struct config *conf = term->conf; - - const char32_t *uri_characters = conf->url.uri_characters; - if (uri_characters == NULL) - return; - - const size_t uri_characters_count = c32len(uri_characters); - if (uri_characters_count == 0) - return; - - size_t max_prot_len = conf->url.max_prot_len; - char32_t proto_chars[max_prot_len]; - struct coord proto_start[max_prot_len]; - size_t proto_char_count = 0; - - enum { - STATE_PROTOCOL, - STATE_URL, - } state = STATE_PROTOCOL; - - struct coord start = {-1, -1}; - char32_t url[term->cols * term->rows + 1]; - size_t len = 0; - - ssize_t parenthesis = 0; - ssize_t brackets = 0; - ssize_t ltgts = 0; - - for (int r = 0; r < term->rows; r++) { - const struct row *row = grid_row_in_view(term->grid, r); - - for (int c = 0; c < term->cols; c++) { - const struct cell *cell = &row->cells[c]; - - if (cell->wc >= CELL_SPACER) - continue; - - const char32_t *wcs = NULL; - size_t wc_count = 0; - - if (cell->wc >= CELL_COMB_CHARS_LO && cell->wc <= CELL_COMB_CHARS_HI) { - struct composed *composed = - composed_lookup(term->composed, cell->wc - CELL_COMB_CHARS_LO); - wcs = composed->chars; - wc_count = composed->count; - } else { - wcs = &cell->wc; - wc_count = 1; - } - - for (size_t w_idx = 0; w_idx < wc_count; w_idx++) { - char32_t wc = wcs[w_idx]; - - switch (state) { - case STATE_PROTOCOL: - for (size_t i = 0; i < max_prot_len - 1; i++) { - proto_chars[i] = proto_chars[i + 1]; - proto_start[i] = proto_start[i + 1]; - } - - if (proto_char_count >= max_prot_len) - proto_char_count = max_prot_len - 1; - - proto_chars[max_prot_len - 1] = wc; - proto_start[max_prot_len - 1] = (struct coord){c, r}; - proto_char_count++; - - for (size_t i = 0; i < conf->url.prot_count; i++) { - size_t prot_len = c32len(conf->url.protocols[i]); - - if (proto_char_count < prot_len) - continue; - - const char32_t *proto = - &proto_chars[max_prot_len - prot_len]; - - if (c32ncasecmp(conf->url.protocols[i], proto, prot_len) == - 0) { - state = STATE_URL; - start = proto_start[max_prot_len - prot_len]; - - c32ncpy(url, proto, prot_len); - len = prot_len; - - parenthesis = brackets = ltgts = 0; - break; - } - } - break; - - case STATE_URL: { - const char32_t *match = - bsearch(&wc, uri_characters, uri_characters_count, - sizeof(uri_characters[0]), &c32cmp_single); - - bool emit_url = false; - - if (match == NULL) { - /* - * Character is not a valid URI character. Emit - * the URL we've collected so far, *without* - * including _this_ character. - */ - emit_url = true; - } else { - xassert(*match == wc); - - switch (wc) { - default: - url[len++] = wc; - break; - - case U'(': - parenthesis++; - url[len++] = wc; - break; - - case U'[': - brackets++; - url[len++] = wc; - break; - - case U'<': - ltgts++; - url[len++] = wc; - break; - - case U')': - if (--parenthesis < 0) - emit_url = true; - else - url[len++] = wc; - break; - - case U']': - if (--brackets < 0) - emit_url = true; - else - url[len++] = wc; - break; - - case U'>': - if (--ltgts < 0) - emit_url = true; - else - url[len++] = wc; - break; - } - } - - if (c >= term->cols - 1 && row->linebreak) { - /* - * Endpoint is inclusive, and we'll be subtracting - * 1 from the column when emitting the URL. - */ - c++; - emit_url = true; - } - - if (emit_url) { - struct coord end = {c, r}; - - if (--end.col < 0) { - end.row--; - end.col = term->cols - 1; - } - - /* Heuristic to remove trailing characters that - * are valid URL characters, but typically not at - * the end of the URL */ - bool done = false; - do { - switch (url[len - 1]) { - case U'.': - case U',': - case U':': - case U';': - case U'?': - case U'!': - case U'"': - case U'\'': - case U'%': - len--; - end.col--; - if (end.col < 0) { - end.row--; - end.col = term->cols - 1; - } - break; - - default: - done = true; - break; - } - } while (!done); - - url[len] = U'\0'; - - start.row += term->grid->view; - end.row += term->grid->view; - - char *url_utf8 = ac32tombs(url); - if (url_utf8 != NULL) { - tll_push_back( - *urls, - ((struct url){.id = (uint64_t)rand() << 32 | rand(), - .url = url_utf8, - .range = - { - .start = start, - .end = end, - }, - .action = action, - .osc8 = false})); - } - - state = STATE_PROTOCOL; - len = 0; - parenthesis = brackets = ltgts = 0; - } - break; - } - } - } - } - } -} - static void osc8_uris(const struct terminal *term, enum url_action action, url_list_t *urls) { @@ -777,7 +567,6 @@ urls_collect(const struct terminal *term, enum url_action action, url_list_t *ur { xassert(tll_length(term->urls) == 0); osc8_uris(term, action, urls); - auto_detected(term, action, urls); regex_detected(term, action, urls); remove_overlapping(urls, term->grid->num_cols); } From 031382f428d783cdf692cd17dac9489fb5c0b530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 30 Jan 2025 11:52:18 +0100 Subject: [PATCH 1033/1323] url-mode: wip: regex: don't allow {}, do allow matched [] --- url-mode.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/url-mode.c b/url-mode.c index 18085ab5..83cb4982 100644 --- a/url-mode.c +++ b/url-mode.c @@ -394,12 +394,16 @@ regex_detected(const struct terminal *term, enum url_action action, url_list_t * "[a-z0-9.\\-]+[.][a-z]{2,4}/" ")" "(" - "[^[:space:]()<>]+" + "[^[:space:](){}<>]+" "|" - "\\(([^[:space:]()<>]+|(\\([^[:space:]()<>]+\\)))*\\)" + "\\(([^[:space:](){}<>]+|(\\([^[:space:](){}<>]+\\)))*\\)" + "|" + "\\[([^]\\[[:space:](){}<>]+|(\\[[^]\\[[:space:](){}<>]+\\]))*\\]" ")+" "(" - "\\(([^[:space:]()<>]+|(\\([^[:space:]()<>]+\\)))*\\)" + "\\(([^[:space:](){}<>]+|(\\([^[:space:](){}<>]+\\)))*\\)" + "|" + "\\[([^]\\[[:space:](){}<>]+|(\\[[^]\\[[:space:](){}<>]+\\]))*\\]" "|" "[^]\\[[:space:]`!(){};:'\".,<>?«»“”‘’]" ")" From 6d344f82ee151eb202dd786a63c12492f48fa4b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 30 Jan 2025 11:53:52 +0100 Subject: [PATCH 1034/1323] url-mode: wip: regex: mention changes from original regex --- url-mode.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/url-mode.c b/url-mode.c index 83cb4982..9aac9be0 100644 --- a/url-mode.c +++ b/url-mode.c @@ -380,7 +380,11 @@ regex_detected(const struct terminal *term, enum url_action action, url_list_t * } } - // https://gist.github.com/gruber/249502 + /* + * Based on https://gist.github.com/gruber/249502, but modified: + * - Do not allow {} at all + * - Do allow matched [] + */ regex_t preg; const char *regex_string = "(" From 05207fcde34d742f2e16e8ecfedd90f23a2f8849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 30 Jan 2025 11:55:09 +0100 Subject: [PATCH 1035/1323] url-mode: wip: regex: tweak debug log message --- url-mode.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/url-mode.c b/url-mode.c index 9aac9be0..36dc0250 100644 --- a/url-mode.c +++ b/url-mode.c @@ -448,9 +448,9 @@ regex_detected(const struct terminal *term, enum url_action action, url_list_t * const size_t end = start + mlen; LOG_DBG( - "MATCH at %d: %.*s (%zu) row/col = %dx%d", + "regex match at row %d: %.*srow/col = %dx%d", matches[0].rm_so, (int)mlen, &search_string[matches[0].rm_so], - mlen, v->map[start].row, v->map[start].col); + v->map[start].row, v->map[start].col); tll_push_back( *urls, From e76d8dd7af0cd7b6b2e1b5ed9c395bd75f503e61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 30 Jan 2025 11:58:52 +0100 Subject: [PATCH 1036/1323] config: remove url.{uri-characters,protocols} --- CHANGELOG.md | 7 +++ config.c | 111 -------------------------------------------- config.h | 5 -- doc/foot.ini.5.scd | 13 ------ foot.ini | 2 - tests/test-config.c | 47 ------------------- 6 files changed, 7 insertions(+), 178 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a14b0fab..8b750f3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,12 +72,19 @@ * Do not try to set a zero width, or height, if the compositor sends a _configure_ event with only one dimension being zero ([#1925][1925]). +* Auto-detection of URLs (i.e. not OSC-8 based URLs) are now regex + based. [1925]: https://codeberg.org/dnkl/foot/issues/1925 ### Deprecated ### Removed + +* `url.uri-characters` and `url.protocols`. Both options have been + replaced by `url.regex`. + + ### Fixed * Kitty keyboard protocol: alternate key reporting failing to report diff --git a/config.c b/config.c index a9db87d5..c96e7168 100644 --- a/config.c +++ b/config.c @@ -420,14 +420,6 @@ done: return ret; } -static int -c32cmp_single(const void *_a, const void *_b) -{ - const char32_t *a = _a; - const char32_t *b = _b; - return *a - *b; -} - static bool str_has_prefix(const char *str, const char *prefix) { @@ -1225,7 +1217,6 @@ parse_section_url(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; - const char *value = ctx->value; if (streq(key, "launch")) return value_to_spawn_template(ctx, &conf->url.launch); @@ -1243,70 +1234,6 @@ parse_section_url(struct context *ctx) (int *)&conf->url.osc8_underline); } - 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); - - conf->url.max_prot_len = 0; - conf->url.prot_count = 0; - conf->url.protocols = NULL; - - char *copy = xstrdup(value); - - for (char *prot = strtok(copy, ","); - prot != NULL; - prot = strtok(NULL, ",")) - { - - /* Strip leading whitespace */ - while (isspace(prot[0])) - prot++; - - /* Strip trailing whitespace */ - size_t len = strlen(prot); - while (isspace(prot[len - 1])) - len--; - prot[len] = '\0'; - - size_t chars = mbsntoc32(NULL, prot, len, 0); - if (chars == (size_t)-1) { - ctx->value = prot; - LOG_CONTEXTUAL_ERRNO("invalid protocol"); - return false; - } - - conf->url.prot_count++; - conf->url.protocols = xrealloc( - conf->url.protocols, - conf->url.prot_count * sizeof(conf->url.protocols[0])); - - size_t idx = conf->url.prot_count - 1; - conf->url.protocols[idx] = xmalloc((chars + 1 + 3) * sizeof(char32_t)); - mbsntoc32(conf->url.protocols[idx], prot, len, chars + 1); - c32cpy(&conf->url.protocols[idx][chars], U"://"); - - chars += 3; /* Include the "://" */ - if (chars > conf->url.max_prot_len) - conf->url.max_prot_len = chars; - } - - free(copy); - return true; - } - - else if (streq(key, "uri-characters")) { - if (!value_to_wchars(ctx, &conf->url.uri_characters)) - return false; - - qsort( - conf->url.uri_characters, - c32len(conf->url.uri_characters), - sizeof(conf->url.uri_characters[0]), - &c32cmp_single); - return true; - } - else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; @@ -3196,7 +3123,6 @@ config_load(struct config *conf, const char *conf_path, }, .url = { .label_letters = xc32dup(U"sadfjklewcmpgh"), - .uri_characters = xc32dup(U"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.,~:;/?#@!$&%*+=\"'()[]"), .osc8_underline = OSC8_UNDERLINE_URL_MODE, }, .can_shape_grapheme = fcft_caps & FCFT_CAPABILITY_GRAPHEME_SHAPING, @@ -3315,34 +3241,6 @@ config_load(struct config *conf, const char *conf_path, tokenize_cmdline("--action ${action-name}=${action-label}", &conf->desktop_notifications.command_action_arg.argv.args); tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args); - static const char32_t *url_protocols[] = { - U"http://", - U"https://", - U"ftp://", - U"ftps://", - U"file://", - U"gemini://", - U"gopher://", - U"irc://", - U"ircs://", - }; - conf->url.protocols = xmalloc( - ALEN(url_protocols) * sizeof(conf->url.protocols[0])); - conf->url.prot_count = ALEN(url_protocols); - conf->url.max_prot_len = 0; - - for (size_t i = 0; i < ALEN(url_protocols); i++) { - size_t len = c32len(url_protocols[i]); - if (len > conf->url.max_prot_len) - conf->url.max_prot_len = len; - conf->url.protocols[i] = xc32dup(url_protocols[i]); - } - - qsort( - conf->url.uri_characters, - c32len(conf->url.uri_characters), - sizeof(conf->url.uri_characters[0]), - &c32cmp_single); tll_foreach(*initial_user_notifications, it) { tll_push_back(conf->notifications, it->item); @@ -3577,12 +3475,7 @@ config_clone(const struct config *old) config_font_list_clone(&conf->csd.font, &old->csd.font); conf->url.label_letters = xc32dup(old->url.label_letters); - conf->url.uri_characters = xc32dup(old->url.uri_characters); spawn_template_clone(&conf->url.launch, &old->url.launch); - conf->url.protocols = xmalloc( - old->url.prot_count * sizeof(conf->url.protocols[0])); - for (size_t i = 0; i < old->url.prot_count; i++) - conf->url.protocols[i] = xc32dup(old->url.protocols[i]); key_binding_list_clone(&conf->bindings.key, &old->bindings.key); key_binding_list_clone(&conf->bindings.search, &old->bindings.search); @@ -3663,10 +3556,6 @@ config_free(struct config *conf) free(conf->url.label_letters); spawn_template_free(&conf->url.launch); - for (size_t i = 0; i < conf->url.prot_count; i++) - free(conf->url.protocols[i]); - free(conf->url.protocols); - free(conf->url.uri_characters); free_key_binding_list(&conf->bindings.key); free_key_binding_list(&conf->bindings.search); diff --git a/config.h b/config.h index 7d9f88c3..a7947d9d 100644 --- a/config.h +++ b/config.h @@ -219,11 +219,6 @@ struct config { OSC8_UNDERLINE_URL_MODE, OSC8_UNDERLINE_ALWAYS, } osc8_underline; - - char32_t **protocols; - char32_t *uri_characters; - size_t prot_count; - size_t max_prot_len; } url; struct { diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 903d3375..813348c9 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -782,19 +782,6 @@ xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 Default: _sadfjklewcmpgh_. -*protocols* - Comma separated list of protocols (schemes) that should be - recognized in URL mode. Note that only auto-detected URLs are - affected by this option. OSC-8 URLs are always enabled, regardless - of protocol. Default: _http, https, ftp, ftps, file, gemini, - gopher, irc, ircs_. - -*uri-characters* - Set of characters allowed in auto-detected URLs. Any character not - included in this set constitutes a URL delimiter. - - Default: - _abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-\_.,~:;/?#@!$&%\*+="'()[]_ # SECTION: cursor diff --git a/foot.ini b/foot.ini index 17fabd3d..864c990c 100644 --- a/foot.ini +++ b/foot.ini @@ -69,8 +69,6 @@ # launch=xdg-open ${url} # label-letters=sadfjklewcmpgh # osc8-underline=url-mode -# protocols=http, https, ftp, ftps, file, gemini, gopher -# uri-characters=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.,~:;/?#@!$&%*+="'()[] [cursor] # style=block diff --git a/tests/test-config.c b/tests/test-config.c index 303ddd6f..37cf4f22 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -106,50 +106,6 @@ test_c32string(struct context *ctx, bool (*parse_fun)(struct context *ctx), } } -static void -test_protocols(struct context *ctx, bool (*parse_fun)(struct context *ctx), - const char *key, char32_t **const *ptr) -{ - ctx->key = key; - - static const struct { - const char *option_string; - int count; - const char32_t *value[2]; - bool invalid; - } input[] = { - {""}, - {"http", 1, {U"http://"}}, - {" http", 1, {U"http://"}}, - {"http, https", 2, {U"http://", U"https://"}}, - {"longprotocolislong", 1, {U"longprotocolislong://"}}, - }; - - for (size_t i = 0; i < ALEN(input); i++) { - ctx->value = input[i].option_string; - - if (input[i].invalid) { - if (parse_fun(ctx)) { - BUG("[%s].%s=%s: did not fail to parse as expected", - ctx->section, ctx->key, &ctx->value[0]); - } - } else { - if (!parse_fun(ctx)) { - BUG("[%s].%s=%s: failed to parse", - ctx->section, ctx->key, &ctx->value[0]); - } - for (int c = 0; c < input[i].count; c++) { - if (c32cmp((*ptr)[c], input[i].value[c]) != 0) { - BUG("[%s].%s=%s: set value[%d] (%ls) not the expected one (%ls)", - ctx->section, ctx->key, &ctx->value[c], c, - (const wchar_t *)(*ptr)[c], - (const wchar_t *)input[i].value[c]); - } - } - } - } -} - static void test_boolean(struct context *ctx, bool (*parse_fun)(struct context *ctx), const char *key, const bool *ptr) @@ -647,9 +603,6 @@ test_section_url(void) (int []){OSC8_UNDERLINE_URL_MODE, OSC8_UNDERLINE_ALWAYS}, (int *)&conf.url.osc8_underline); test_c32string(&ctx, &parse_section_url, "label-letters", &conf.url.label_letters); - test_protocols(&ctx, &parse_section_url, "protocols", &conf.url.protocols); - - /* TODO: uri-characters (wchar string, but sorted) */ config_free(&conf); } From d41b28bd027affe67bfd8032a2041241df8be823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 30 Jan 2025 12:26:23 +0100 Subject: [PATCH 1037/1323] url-mode+config: wip: add url.regex option --- config.c | 60 ++++++++++++++++++++++++++++++++++++++++++++++ config.h | 6 ++++- doc/foot.ini.5.scd | 5 ++++ foot.ini | 2 ++ url-mode.c | 57 ++++--------------------------------------- 5 files changed, 76 insertions(+), 54 deletions(-) diff --git a/config.c b/config.c index c96e7168..dd4300e3 100644 --- a/config.c +++ b/config.c @@ -1234,6 +1234,26 @@ parse_section_url(struct context *ctx) (int *)&conf->url.osc8_underline); } + else if (streq(key, "regex")) { + const char *regex = ctx->value; + regex_t preg; + + int r = regcomp(&preg, regex, REG_EXTENDED); + if (r != 0) { + char err_buf[128]; + regerror(r, &preg, err_buf, sizeof(err_buf)); + LOG_CONTEXTUAL_ERR("invalid regex: %s", err_buf); + return false; + } + + regfree(&conf->url.preg); + free(conf->url.regex); + + conf->url.regex = xstrdup(regex); + conf->url.preg = preg; + return true; + } + else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; @@ -3241,6 +3261,42 @@ config_load(struct config *conf, const char *conf_path, tokenize_cmdline("--action ${action-name}=${action-label}", &conf->desktop_notifications.command_action_arg.argv.args); tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args); + { + /* + * Based on https://gist.github.com/gruber/249502, but modified: + * - Do not allow {} at all + * - Do allow matched [] + */ + const char *url_regex_string = + "(" + "[a-z][[:alnum:]-]+:" // protocol + "(" + "/{1,3}|[a-z0-9%]" // slashes (what's the OR part for?) + ")" + "|" + "www[:digit:]{0,3}[.]" + //"|" + //"[a-z0-9.\\-]+[.][a-z]{2,4}/" /* "looks like domain name followed by a slash" - remove? */ + ")" + "(" + "[^[:space:](){}<>]+" + "|" + "\\(([^[:space:](){}<>]+|(\\([^[:space:](){}<>]+\\)))*\\)" + "|" + "\\[([^]\\[[:space:](){}<>]+|(\\[[^]\\[[:space:](){}<>]+\\]))*\\]" + ")+" + "(" + "\\(([^[:space:](){}<>]+|(\\([^[:space:](){}<>]+\\)))*\\)" + "|" + "\\[([^]\\[[:space:](){}<>]+|(\\[[^]\\[[:space:](){}<>]+\\]))*\\]" + "|" + "[^]\\[[:space:]`!(){};:'\".,<>?«»“”‘’]" + ")" + ; + int r = regcomp(&conf->url.preg, url_regex_string, REG_EXTENDED); + xassert(r == 0); + conf->url.regex = xstrdup(url_regex_string); + } tll_foreach(*initial_user_notifications, it) { tll_push_back(conf->notifications, it->item); @@ -3476,6 +3532,8 @@ config_clone(const struct config *old) conf->url.label_letters = xc32dup(old->url.label_letters); spawn_template_clone(&conf->url.launch, &old->url.launch); + conf->url.regex = xstrdup(old->url.regex); + regcomp(&conf->url.preg, conf->url.regex, REG_EXTENDED); key_binding_list_clone(&conf->bindings.key, &old->bindings.key); key_binding_list_clone(&conf->bindings.search, &old->bindings.search); @@ -3556,6 +3614,8 @@ config_free(struct config *conf) free(conf->url.label_letters); spawn_template_free(&conf->url.launch); + regfree(&conf->url.preg); + free(conf->url.regex); free_key_binding_list(&conf->bindings.key); free_key_binding_list(&conf->bindings.search); diff --git a/config.h b/config.h index a7947d9d..ea0bd942 100644 --- a/config.h +++ b/config.h @@ -1,7 +1,8 @@ #pragma once -#include <stdint.h> +#include <regex.h> #include <stdbool.h> +#include <stdint.h> #include <uchar.h> #include <xkbcommon/xkbcommon.h> @@ -219,6 +220,9 @@ struct config { OSC8_UNDERLINE_URL_MODE, OSC8_UNDERLINE_ALWAYS, } osc8_underline; + + char *regex; + regex_t preg; } url; struct { diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 813348c9..ddf5ce84 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -782,6 +782,11 @@ xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 Default: _sadfjklewcmpgh_. +*regex* + URL regex to use when auto-detecting URLs. The format is + "POSIX-Extended Regular Expressions". + + Default: _TODO_ # SECTION: cursor diff --git a/foot.ini b/foot.ini index 864c990c..ae0002d4 100644 --- a/foot.ini +++ b/foot.ini @@ -69,6 +69,8 @@ # launch=xdg-open ${url} # label-letters=sadfjklewcmpgh # osc8-underline=url-mode +# regex=TODO + [cursor] # style=block diff --git a/url-mode.c b/url-mode.c index 36dc0250..9ba07d14 100644 --- a/url-mode.c +++ b/url-mode.c @@ -380,54 +380,7 @@ regex_detected(const struct terminal *term, enum url_action action, url_list_t * } } - /* - * Based on https://gist.github.com/gruber/249502, but modified: - * - Do not allow {} at all - * - Do allow matched [] - */ - regex_t preg; - const char *regex_string = - "(" - "[a-z][[:alnum:]-]+:" // protocol - "(" - "/{1,3}|[a-z0-9%]" // slashes (what's the OR part for?) - ")" - "|" - "www[:digit:]{0,3}[.]" - "|" - "[a-z0-9.\\-]+[.][a-z]{2,4}/" - ")" - "(" - "[^[:space:](){}<>]+" - "|" - "\\(([^[:space:](){}<>]+|(\\([^[:space:](){}<>]+\\)))*\\)" - "|" - "\\[([^]\\[[:space:](){}<>]+|(\\[[^]\\[[:space:](){}<>]+\\]))*\\]" - ")+" - "(" - "\\(([^[:space:](){}<>]+|(\\([^[:space:](){}<>]+\\)))*\\)" - "|" - "\\[([^]\\[[:space:](){}<>]+|(\\[[^]\\[[:space:](){}<>]+\\]))*\\]" - "|" - "[^]\\[[:space:]`!(){};:'\".,<>?«»“”‘’]" - ")" - ; - - int r = regcomp(&preg, regex_string, REG_EXTENDED); - - if (r != 0) { - char err_buf[1024]; - regerror(r, &preg, err_buf, sizeof(err_buf)); - LOG_ERR("failed to compile regular expression: %s", err_buf); - - for (size_t i = 0; i < ALEN(vlines); i++) { - const struct vline *v = &vlines[i]; - free(v->utf8); - free(v->map); - } - - return; - } + const regex_t *preg = &term->conf->url.preg; for (size_t i = 0; i < ALEN(vlines); i++) { const struct vline *v = &vlines[i]; @@ -436,9 +389,8 @@ regex_detected(const struct terminal *term, enum url_action action, url_list_t * const char *search_string = v->utf8; while (true) { - - regmatch_t matches[preg.re_nsub + 1]; - r = regexec(&preg, search_string, preg.re_nsub + 1, matches, 0); + regmatch_t matches[preg->re_nsub + 1]; + int r = regexec(preg, search_string, preg->re_nsub + 1, matches, 0); if (r == REG_NOMATCH) break; @@ -470,9 +422,8 @@ regex_detected(const struct terminal *term, enum url_action action, url_list_t * free(v->utf8); free(v->map); } - - regfree(&preg); } + static void osc8_uris(const struct terminal *term, enum url_action action, url_list_t *urls) { From 130b05f02baaebe533bb00cc2824ffb9902f11d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 30 Jan 2025 12:33:58 +0100 Subject: [PATCH 1038/1323] foot.ini+doc: add default value of url.regex --- config.c | 1 + doc/foot.ini.5.scd | 2 +- foot.ini | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/config.c b/config.c index dd4300e3..1f779724 100644 --- a/config.c +++ b/config.c @@ -1239,6 +1239,7 @@ parse_section_url(struct context *ctx) regex_t preg; int r = regcomp(&preg, regex, REG_EXTENDED); + if (r != 0) { char err_buf[128]; regerror(r, &preg, err_buf, sizeof(err_buf)); diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index ddf5ce84..a23e3977 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -786,7 +786,7 @@ xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 URL regex to use when auto-detecting URLs. The format is "POSIX-Extended Regular Expressions". - Default: _TODO_ + Default: _([a-z][[:alnum:]-]+:(/{1,3}|[a-z0-9%])|www[:digit:]{0,3}[.])([^[:space:](){}<>]+|\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))\*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))\*\])+(\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))\*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))\*\]|[^]\[[:space:]`!(){};:'".,<>?«»“”‘’])_ # SECTION: cursor diff --git a/foot.ini b/foot.ini index ae0002d4..85f3c861 100644 --- a/foot.ini +++ b/foot.ini @@ -69,7 +69,8 @@ # launch=xdg-open ${url} # label-letters=sadfjklewcmpgh # osc8-underline=url-mode -# regex=TODO +# regex=([a-z][[:alnum:]-]+:(/{1,3}|[a-z0-9%])|www[:digit:]{0,3}[.])([^[:space:](){}<>]+|\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))*\])+(\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))*\]|[^]\[[:space:]`!(){};:'".,<>?«»“”‘’]) + [cursor] From ab4426f9873eee6d83871f9bd03d2d64a5f862b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 31 Jan 2025 13:10:58 +0100 Subject: [PATCH 1039/1323] url-mode: regex: make sure there's always room for the NULL terminator --- url-mode.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/url-mode.c b/url-mode.c index 9ba07d14..5b74771a 100644 --- a/url-mode.c +++ b/url-mode.c @@ -354,7 +354,8 @@ regex_detected(const struct terminal *term, enum url_action action, url_list_t * for (size_t j = 0; j < char_len; j++) { const size_t requires_size = vline->len + char_len; - if (requires_size > vline->sz) { + /* Need to grow? Remember to save at least one byte for terminator */ + if (vline->sz == 0 || requires_size > vline->sz - 1) { const size_t new_size = requires_size * 2; vline->utf8 = xreallocarray(vline->utf8, new_size, 1); vline->map = xreallocarray(vline->map, new_size, sizeof(vline->map[0])); From f718cb3fb0d627cab8d952b94891e38238934090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 3 Feb 2025 08:31:31 +0100 Subject: [PATCH 1040/1323] xmalloc: calling xrealloc() or xreallocarray() with a 0-size is UB in C23 And likely to in future POSIX too. --- xmalloc.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/xmalloc.c b/xmalloc.c index 0a67cdb2..ccfb5c48 100644 --- a/xmalloc.c +++ b/xmalloc.c @@ -32,15 +32,17 @@ xcalloc(size_t nmemb, size_t size) void * xrealloc(void *ptr, size_t size) { + xassert(size != 0); void *alloc = realloc(ptr, size); - return unlikely(size == 0) ? alloc : check_alloc(alloc); + return check_alloc(alloc); } void * xreallocarray(void *ptr, size_t n, size_t size) { + xassert(n != 0 && size != 0); void *alloc = reallocarray(ptr, n, size); - return unlikely(size == 0) ? alloc : check_alloc(alloc); + return check_alloc(alloc); } char * From 051cd6ecfc8b98e1d80b399ebea40207fe040750 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 3 Feb 2025 08:55:47 +0100 Subject: [PATCH 1041/1323] config+url: add support for user-defined regex patterns Users can now define their own regex patterns, and use them via key bindings: [regex:foo] regex=foo(bar)? launch=path-to-script-or-application {match} [key-bindings] regex-launch=[foo] Control+Shift+q regex-copy=[foo] Control+Mod1+Shift+q That is, add a section called 'regex:', followed by an identifier. Define a regex and a launcher command line. Add a key-binding, regex-launch and/or regex-copy (similar to show-urls-launch and show-urls-copy), and connect them to the regex with the "[regex-name]" syntax (similar to how the pipe-* bindings work). --- config.c | 263 ++++++++++++++++++++++++++++++++++++++++++++------ config.h | 12 +++ foot.ini | 4 +- input.c | 40 +++++++- key-binding.h | 4 +- terminal.h | 1 + url-mode.c | 29 ++++-- url-mode.h | 5 +- 8 files changed, 310 insertions(+), 48 deletions(-) diff --git a/config.c b/config.c index 1f779724..e35e4233 100644 --- a/config.c +++ b/config.c @@ -140,6 +140,8 @@ static const char *const binding_action_map[] = { [BIND_ACTION_PROMPT_NEXT] = "prompt-next", [BIND_ACTION_UNICODE_INPUT] = "unicode-input", [BIND_ACTION_QUIT] = "quit", + [BIND_ACTION_REGEX_LAUNCH] = "regex-launch", + [BIND_ACTION_REGEX_COPY] = "regex-copy", /* Mouse-specific actions */ [BIND_ACTION_SCROLLBACK_UP_MOUSE] = "scrollback-up-mouse", @@ -207,6 +209,7 @@ static_assert(ALEN(url_binding_action_map) == BIND_ACTION_URL_COUNT, struct context { struct config *conf; const char *section; + const char *section_suffix; const char *key; const char *value; @@ -257,8 +260,9 @@ log_contextual(struct context *ctx, enum log_class log_class, char *formatted_msg = xvasprintf(fmt, va); va_end(va); - bool print_dot = ctx->key != NULL; - bool print_colon = ctx->value != NULL; + const bool print_dot = ctx->key != NULL; + const bool print_colon = ctx->value != NULL; + const bool print_section_suffix = ctx->section_suffix != NULL; if (!print_dot) ctx->key = ""; @@ -266,10 +270,15 @@ log_contextual(struct context *ctx, enum log_class log_class, if (!print_colon) ctx->value = ""; + if (!print_section_suffix) + ctx->section_suffix = ""; + log_and_notify( - ctx->conf, log_class, file, lineno, "%s:%d: [%s]%s%s%s%s: %s", - ctx->path, ctx->lineno, ctx->section, print_dot ? "." : "", - ctx->key, print_colon ? ": " : "", ctx->value, formatted_msg); + ctx->conf, log_class, file, lineno, "%s:%d: [%s%s%s]%s%s%s%s: %s", + ctx->path, ctx->lineno, ctx->section, + print_section_suffix ? ":" : "", ctx->section_suffix, + print_dot ? "." : "", ctx->key, print_colon ? ": " : "", + ctx->value, formatted_msg); free(formatted_msg); } @@ -1261,6 +1270,72 @@ parse_section_url(struct context *ctx) } } +static bool +parse_section_regex(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + const char *regex_name = + ctx->section_suffix != NULL ? ctx->section_suffix : ""; + + struct custom_regex *regex = NULL; + tll_foreach(conf->custom_regexes, it) { + if (streq(it->item.name, regex_name)) { + regex = &it->item; + break; + } + } + + if (streq(key, "regex")) { + const char *regex_string = ctx->value; + regex_t preg; + + int r = regcomp(&preg, regex_string, REG_EXTENDED); + + if (r != 0) { + char err_buf[128]; + regerror(r, &preg, err_buf, sizeof(err_buf)); + LOG_CONTEXTUAL_ERR("invalid regex: %s", err_buf); + return false; + } + + if (regex == NULL) { + tll_push_back(conf->custom_regexes, + ((struct custom_regex){.name = xstrdup(regex_name)})); + regex = &tll_back(conf->custom_regexes); + } + + regfree(®ex->preg); + free(regex->regex); + + regex->regex = xstrdup(regex_string); + regex->preg = preg; + return true; + } + + else if (streq(key, "launch")) { + struct config_spawn_template launch; + if (!value_to_spawn_template(ctx, &launch)) + return false; + + if (regex == NULL) { + tll_push_back(conf->custom_regexes, + ((struct custom_regex){.name = xstrdup(regex_name)})); + regex = &tll_back(conf->custom_regexes); + } + + spawn_template_free(®ex->launch); + regex->launch = launch; + return true; + } + + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + static bool parse_section_colors(struct context *ctx) { @@ -1602,6 +1677,7 @@ free_binding_aux(struct binding_aux *aux) case BINDING_AUX_NONE: break; case BINDING_AUX_PIPE: free_argv(&aux->pipe); break; case BINDING_AUX_TEXT: free(aux->text.data); break; + case BINDING_AUX_REGEX: free(aux->regex_name); break; } } @@ -1691,7 +1767,10 @@ binding_aux_equal(const struct binding_aux *a, case BINDING_AUX_TEXT: return a->text.len == b->text.len && - memcmp(a->text.data, b->text.data, a->text.len) == 0; + memcmp(a->text.data, b->text.data, a->text.len) == 0; + + case BINDING_AUX_REGEX: + return streq(a->regex_name, b->regex_name); } BUG("invalid AUX type: %d", a->type); @@ -1965,19 +2044,23 @@ modifiers_disjoint(const config_modifier_list_t *mods1, } static char * NOINLINE -modifiers_to_str(const config_modifier_list_t *mods) +modifiers_to_str(const config_modifier_list_t *mods, bool strip_last_plus) { - size_t len = tll_length(*mods); /* '+' , and NULL terminator */ + size_t len = tll_length(*mods); /* '+' separator */ tll_foreach(*mods, it) len += strlen(it->item); - char *ret = xmalloc(len); + char *ret = xmalloc(len + 1); size_t idx = 0; tll_foreach(*mods, it) { idx += snprintf(&ret[idx], len - idx, "%s", it->item); ret[idx++] = '+'; } - ret[--idx] = '\0'; + + if (strip_last_plus) + idx--; + + ret[idx] = '\0'; return ret; } @@ -2036,21 +2119,40 @@ pipe_argv_from_value(struct context *ctx, struct argv *argv) return remove_len; } +static ssize_t NOINLINE +regex_name_from_value(struct context *ctx, char **regex_name) +{ + *regex_name = NULL; + + if (ctx->value[0] != '[') + return 0; + + const char *regex_end = strrchr(ctx->value, ']'); + if (regex_end == NULL) { + LOG_CONTEXTUAL_ERR("unclosed '['"); + return -1; + } + + size_t regex_len = regex_end - ctx->value - 1; + *regex_name = xstrndup(&ctx->value[1], regex_len); + + ssize_t remove_len = regex_end + 1 - ctx->value; + ctx->value = regex_end + 1; + while (isspace(*ctx->value)) { + ctx->value++; + remove_len++; + } + + return remove_len; +} + + static bool NOINLINE parse_key_binding_section(struct context *ctx, int action_count, const char *const action_map[static action_count], struct config_key_binding_list *bindings) { - struct binding_aux aux; - - ssize_t pipe_remove_len = pipe_argv_from_value(ctx, &aux.pipe); - if (pipe_remove_len < 0) - return false; - - aux.type = pipe_remove_len == 0 ? BINDING_AUX_NONE : BINDING_AUX_PIPE; - aux.master_copy = true; - for (int action = 0; action < action_count; action++) { if (action_map[action] == NULL) continue; @@ -2058,6 +2160,33 @@ parse_key_binding_section(struct context *ctx, if (!streq(ctx->key, action_map[action])) continue; + struct binding_aux aux = {.type = BINDING_AUX_NONE, .master_copy = true}; + + /* TODO: this is ugly... */ + if (action_map == binding_action_map && + action >= BIND_ACTION_PIPE_SCROLLBACK && + action <= BIND_ACTION_PIPE_COMMAND_OUTPUT) + { + ssize_t pipe_remove_len = pipe_argv_from_value(ctx, &aux.pipe); + if (pipe_remove_len <= 0) + return false; + + aux.type = BINDING_AUX_PIPE; + aux.master_copy = true; + } else if (action_map == binding_action_map && + action >= BIND_ACTION_REGEX_LAUNCH && + action <= BIND_ACTION_REGEX_COPY) + { + char *regex_name = NULL; + ssize_t regex_remove_len = regex_name_from_value(ctx, ®ex_name); + if (regex_remove_len <= 0) + return false; + + aux.type = BINDING_AUX_REGEX; + aux.master_copy = true; + aux.regex_name = regex_name; + } + if (!value_to_key_combos(ctx, action, &aux, bindings, KEY_BINDING)) { free_binding_aux(&aux); return false; @@ -2067,7 +2196,6 @@ parse_key_binding_section(struct context *ctx, } LOG_CONTEXTUAL_ERR("not a valid action: %s", ctx->key); - free_binding_aux(&aux); return false; } @@ -2265,7 +2393,7 @@ resolve_key_binding_collisions(struct config *conf, const char *section_name, } if (collision_type != COLLISION_NONE) { - char *modifier_names = modifiers_to_str(mods1); + char *modifier_names = modifiers_to_str(mods1, false); char sym_name[64]; switch (type){ @@ -2307,7 +2435,7 @@ resolve_key_binding_collisions(struct config *conf, const char *section_name, case COLLISION_OVERRIDE: { char *override_names = modifiers_to_str( - &conf->mouse.selection_override_modifiers); + &conf->mouse.selection_override_modifiers, true); if (override_names[0] != '\0') override_names[strlen(override_names) - 1] = '\0'; @@ -2646,7 +2774,7 @@ parse_section_touch(struct context *ctx) { } static bool -parse_key_value(char *kv, const char **section, const char **key, const char **value) +parse_key_value(char *kv, char **section, const char **key, const char **value) { bool section_is_needed = section != NULL; @@ -2715,6 +2843,7 @@ enum section { SECTION_DESKTOP_NOTIFICATIONS, SECTION_SCROLLBACK, SECTION_URL, + SECTION_REGEX, SECTION_COLORS, SECTION_CURSOR, SECTION_MOUSE, @@ -2736,6 +2865,7 @@ typedef bool (*parser_fun_t)(struct context *ctx); static const struct { parser_fun_t fun; const char *name; + bool allow_colon_suffix; } section_info[] = { [SECTION_MAIN] = {&parse_section_main, "main"}, [SECTION_SECURITY] = {&parse_section_security, "security"}, @@ -2743,6 +2873,7 @@ static const struct { [SECTION_DESKTOP_NOTIFICATIONS] = {&parse_section_desktop_notifications, "desktop-notifications"}, [SECTION_SCROLLBACK] = {&parse_section_scrollback, "scrollback"}, [SECTION_URL] = {&parse_section_url, "url"}, + [SECTION_REGEX] = {&parse_section_regex, "regex", true}, [SECTION_COLORS] = {&parse_section_colors, "colors"}, [SECTION_CURSOR] = {&parse_section_cursor, "cursor"}, [SECTION_MOUSE] = {&parse_section_mouse, "mouse"}, @@ -2760,11 +2891,29 @@ static const struct { static_assert(ALEN(section_info) == SECTION_COUNT, "section info array size mismatch"); static enum section -str_to_section(const char *str) +str_to_section(char *str, char **suffix) { + *suffix = NULL; + for (enum section section = SECTION_MAIN; section < SECTION_COUNT; ++section) { - if (streq(str, section_info[section].name)) + const char *name = section_info[section].name; + + if (streq(str, name)) return section; + + else if (section_info[section].allow_colon_suffix) { + const size_t str_len = strlen(str); + const size_t name_len = strlen(name); + + /* At least "section:" chars? */ + if (str_len > name_len + 1) { + if (strncmp(str, name, name_len) == 0 && str[name_len] == ':') { + str[name_len] = '\0'; + *suffix = &str[name_len + 1]; + return section; + } + } + } } return SECTION_COUNT; } @@ -2788,10 +2937,12 @@ parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_ar } char *section_name = xstrdup("main"); + char *section_suffix = NULL; struct context context = { .conf = conf, .section = section_name, + .section_suffix = section_suffix, .path = path, .lineno = 0, .errors_are_fatal = errors_are_fatal, @@ -2872,7 +3023,8 @@ parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_ar error_or_continue(); } - section = str_to_section(key_value); + char *maybe_section_suffix; + section = str_to_section(key_value, &maybe_section_suffix); if (section == SECTION_COUNT) { context.section = key_value; LOG_CONTEXTUAL_ERR("invalid section name: %s", key_value); @@ -2881,8 +3033,11 @@ parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_ar } free(section_name); + free(section_suffix); section_name = xstrdup(key_value); + section_suffix = maybe_section_suffix != NULL ? xstrdup(maybe_section_suffix) : NULL; context.section = section_name; + context.section_suffix = section_suffix; /* Process next line */ continue; @@ -2922,6 +3077,7 @@ parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_ar done: free(section_name); + free(section_suffix); free(_line); return ret; } @@ -3016,7 +3172,6 @@ add_default_search_bindings(struct config *conf) {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}}}, @@ -3146,6 +3301,7 @@ config_load(struct config *conf, const char *conf_path, .label_letters = xc32dup(U"sadfjklewcmpgh"), .osc8_underline = OSC8_UNDERLINE_URL_MODE, }, + .custom_regexes = tll_init(), .can_shape_grapheme = fcft_caps & FCFT_CAPABILITY_GRAPHEME_SHAPING, .scrollback = { .lines = 1000, @@ -3385,6 +3541,8 @@ bool config_override_apply(struct config *conf, config_override_t *overrides, bool errors_are_fatal) { + char *section_name = NULL; + struct context context = { .conf = conf, .path = "override", @@ -3396,8 +3554,7 @@ config_override_apply(struct config *conf, config_override_t *overrides, tll_foreach(*overrides, it) { context.lineno++; - if (!parse_key_value( - it->item, &context.section, &context.key, &context.value)) + if (!parse_key_value(it->item, §ion_name, &context.key, &context.value)) { LOG_CONTEXTUAL_ERR("syntax error: key/value pair has no %s", context.key == NULL ? "key" : "value"); @@ -3406,20 +3563,28 @@ config_override_apply(struct config *conf, config_override_t *overrides, continue; } - if (context.section[0] == '\0') { + if (section_name[0] == '\0') { LOG_CONTEXTUAL_ERR("empty section name"); if (errors_are_fatal) return false; continue; } - enum section section = str_to_section(context.section); + LOG_ERR("section-name=%s", section_name); + + char *maybe_section_suffix = NULL; + enum section section = str_to_section(section_name, &maybe_section_suffix); + + context.section = section_name; + context.section_suffix = maybe_section_suffix; + if (section == SECTION_COUNT) { - LOG_CONTEXTUAL_ERR("invalid section name: %s", context.section); + LOG_CONTEXTUAL_ERR("invalid section name: %s", section_name); if (errors_are_fatal) return false; continue; } + parser_fun_t section_parser = section_info[section].fun; xassert(section_parser != NULL); @@ -3455,6 +3620,7 @@ key_binding_list_clone(struct config_key_binding_list *dst, struct argv *last_master_argv = NULL; uint8_t *last_master_text_data = NULL; size_t last_master_text_len = 0; + char *last_master_regex_name = NULL; dst->count = src->count; dst->arr = xmalloc(src->count * sizeof(dst->arr[0])); @@ -3502,6 +3668,16 @@ key_binding_list_clone(struct config_key_binding_list *dst, } last_master_argv = NULL; break; + + case BINDING_AUX_REGEX: + if (old->aux.master_copy) { + new->aux.regex_name = xstrdup(old->aux.regex_name); + last_master_regex_name = new->aux.regex_name; + } else { + xassert(last_master_regex_name != NULL); + new->aux.regex_name = last_master_regex_name; + } + break; } } } @@ -3536,6 +3712,20 @@ config_clone(const struct config *old) conf->url.regex = xstrdup(old->url.regex); regcomp(&conf->url.preg, conf->url.regex, REG_EXTENDED); + memset(&conf->custom_regexes, 0, sizeof(conf->custom_regexes)); + tll_foreach(old->custom_regexes, it) { + const struct custom_regex *old_regex = &it->item; + + tll_push_back(conf->custom_regexes, + ((struct custom_regex){.name = xstrdup(old_regex->name), + .regex = xstrdup(old_regex->regex)})); + + + struct custom_regex *new_regex = &tll_back(conf->custom_regexes); + regcomp(&new_regex->preg, new_regex->regex, REG_EXTENDED); + spawn_template_clone(&new_regex->launch, &old_regex->launch); + } + key_binding_list_clone(&conf->bindings.key, &old->bindings.key); key_binding_list_clone(&conf->bindings.search, &old->bindings.search); key_binding_list_clone(&conf->bindings.url, &old->bindings.url); @@ -3618,6 +3808,15 @@ config_free(struct config *conf) regfree(&conf->url.preg); free(conf->url.regex); + tll_foreach(conf->custom_regexes, it) { + struct custom_regex *regex = &it->item; + free(regex->name); + free(regex->regex); + regfree(®ex->preg); + spawn_template_free(®ex->launch); + tll_remove(conf->custom_regexes, it); + } + free_key_binding_list(&conf->bindings.key); free_key_binding_list(&conf->bindings.search); free_key_binding_list(&conf->bindings.url); diff --git a/config.h b/config.h index ea0bd942..3535064e 100644 --- a/config.h +++ b/config.h @@ -61,6 +61,7 @@ enum binding_aux_type { BINDING_AUX_NONE, BINDING_AUX_PIPE, BINDING_AUX_TEXT, + BINDING_AUX_REGEX, }; struct binding_aux { @@ -74,6 +75,8 @@ struct binding_aux { uint8_t *data; size_t len; } text; + + char *regex_name; }; }; @@ -121,6 +124,13 @@ struct env_var { }; typedef tll(struct env_var) env_var_list_t; +struct custom_regex { + char *name; + char *regex; + regex_t preg; + struct config_spawn_template launch; +}; + struct config { char *term; char *shell; @@ -225,6 +235,8 @@ struct config { regex_t preg; } url; + tll(struct custom_regex) custom_regexes; + struct { uint32_t fg; uint32_t bg; diff --git a/foot.ini b/foot.ini index 85f3c861..1fb20e67 100644 --- a/foot.ini +++ b/foot.ini @@ -71,7 +71,9 @@ # osc8-underline=url-mode # regex=([a-z][[:alnum:]-]+:(/{1,3}|[a-z0-9%])|www[:digit:]{0,3}[.])([^[:space:](){}<>]+|\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))*\])+(\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))*\]|[^]\[[:space:]`!(){};:'".,<>?«»“”‘’]) - +# [regex:your-fancy-name] +# regex=<a POSIX-Extended Regular Expression> +# launch=<path to script or application> {match} [cursor] # style=block diff --git a/input.c b/input.c index c3ddbf13..916f30e4 100644 --- a/input.c +++ b/input.c @@ -349,9 +349,9 @@ execute_binding(struct seat *seat, struct terminal *term, action == BIND_ACTION_SHOW_URLS_LAUNCH ? URL_ACTION_LAUNCH : URL_ACTION_PERSISTENT; - urls_collect(term, url_action, &term->urls); + urls_collect(term, url_action, &term->conf->url.preg, true, &term->urls); urls_assign_key_combos(term->conf, &term->urls); - urls_render(term); + urls_render(term, &term->conf->url.launch); return true; } @@ -448,6 +448,42 @@ execute_binding(struct seat *seat, struct terminal *term, term_shutdown(term); return true; + case BIND_ACTION_REGEX_LAUNCH: + case BIND_ACTION_REGEX_COPY: + if (binding->aux->type != BINDING_AUX_REGEX) + return true; + + tll_foreach(term->conf->custom_regexes, it) { + const struct custom_regex *regex = &it->item; + + if (streq(regex->name, binding->aux->regex_name)) { + xassert(!urls_mode_is_active(term)); + + enum url_action url_action = action == BIND_ACTION_REGEX_LAUNCH + ? URL_ACTION_LAUNCH : URL_ACTION_COPY; + + if (regex->regex == NULL) { + LOG_ERR("regex:%s has no regex defined", regex->name); + return true; + } + if (url_action == URL_ACTION_LAUNCH && regex->launch.argv.args == NULL) { + LOG_ERR("regex:%s has no launch command defined", regex->name); + return true; + } + + urls_collect(term, url_action, ®ex->preg, false, &term->urls); + urls_assign_key_combos(term->conf, &term->urls); + urls_render(term, ®ex->launch); + return true; + } + } + + LOG_ERR( + "no regex section named '%s' defined in the configuration", + binding->aux->regex_name); + + return true; + case BIND_ACTION_SELECT_BEGIN: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_CHAR_WISE, false); diff --git a/key-binding.h b/key-binding.h index f42dbc48..5f5bb9d7 100644 --- a/key-binding.h +++ b/key-binding.h @@ -41,6 +41,8 @@ enum bind_action_normal { BIND_ACTION_PROMPT_NEXT, BIND_ACTION_UNICODE_INPUT, BIND_ACTION_QUIT, + BIND_ACTION_REGEX_LAUNCH, + BIND_ACTION_REGEX_COPY, /* Mouse specific actions - i.e. they require a mouse coordinate */ BIND_ACTION_SCROLLBACK_UP_MOUSE, @@ -54,7 +56,7 @@ enum bind_action_normal { BIND_ACTION_SELECT_QUOTE, BIND_ACTION_SELECT_ROW, - BIND_ACTION_KEY_COUNT = BIND_ACTION_QUIT + 1, + BIND_ACTION_KEY_COUNT = BIND_ACTION_REGEX_COPY + 1, BIND_ACTION_COUNT = BIND_ACTION_SELECT_ROW + 1, }; diff --git a/terminal.h b/terminal.h index 813510fe..4242ed1d 100644 --- a/terminal.h +++ b/terminal.h @@ -789,6 +789,7 @@ struct terminal { bool urls_show_uri_on_jump_label; struct grid *url_grid_snapshot; bool ime_reenable_after_url_mode; + const struct config_spawn_template *url_launch; #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED bool ime_enabled; diff --git a/url-mode.c b/url-mode.c index 5b74771a..3108ec12 100644 --- a/url-mode.c +++ b/url-mode.c @@ -67,12 +67,13 @@ spawn_url_launcher_with_token(struct terminal *term, return false; } + xassert(term->url_launch != NULL); bool ret = false; if (spawn_expand_template( - &term->conf->url.launch, 1, - (const char *[]){"url"}, - (const char *[]){url}, + term->url_launch, 2, + (const char *[]){"url", "match"}, + (const char *[]){url, url}, &argc, &argv)) { ret = spawn( @@ -84,6 +85,8 @@ spawn_url_launcher_with_token(struct terminal *term, free(argv); } + term->url_launch = NULL; + close(dev_null); return ret; } @@ -107,6 +110,8 @@ static bool spawn_url_launcher(struct seat *seat, struct terminal *term, const char *url, uint32_t serial) { + xassert(term->url_launch != NULL); + struct spawn_activation_context *ctx = xmalloc(sizeof(*ctx)); *ctx = (struct spawn_activation_context){ .term = term, @@ -300,7 +305,8 @@ struct vline { }; static void -regex_detected(const struct terminal *term, enum url_action action, url_list_t *urls) +regex_detected(const struct terminal *term, enum url_action action, + const regex_t *preg, url_list_t *urls) { /* * Use regcomp()+regexec() to find patterns. @@ -381,8 +387,6 @@ regex_detected(const struct terminal *term, enum url_action action, url_list_t * } } - const regex_t *preg = &term->conf->url.preg; - for (size_t i = 0; i < ALEN(vlines); i++) { const struct vline *v = &vlines[i]; if (v->utf8 == NULL) @@ -523,11 +527,13 @@ remove_overlapping(url_list_t *urls, int cols) } void -urls_collect(const struct terminal *term, enum url_action action, url_list_t *urls) +urls_collect(const struct terminal *term, enum url_action action, + const regex_t *preg, bool osc8, url_list_t *urls) { xassert(tll_length(term->urls) == 0); - osc8_uris(term, action, urls); - regex_detected(term, action, urls); + if (osc8) + osc8_uris(term, action, urls); + regex_detected(term, action, preg, urls); remove_overlapping(urls, term->grid->num_cols); } @@ -710,7 +716,7 @@ tag_cells_for_url(struct terminal *term, const struct url *url, bool value) } void -urls_render(struct terminal *term) +urls_render(struct terminal *term, const struct config_spawn_template *launch) { struct wl_window *win = term->window; @@ -745,6 +751,9 @@ urls_render(struct terminal *term) /* Snapshot the current grid */ term->url_grid_snapshot = grid_snapshot(term->grid); + /* Remember which launcher to use */ + term->url_launch = launch; + xassert(tll_length(win->urls) == 0); tll_foreach(win->term->urls, it) { struct wl_url url = {.url = &it->item}; diff --git a/url-mode.h b/url-mode.h index eefe07c0..758cd92f 100644 --- a/url-mode.h +++ b/url-mode.h @@ -14,10 +14,11 @@ static inline bool urls_mode_is_active(const struct terminal *term) } void urls_collect( - const struct terminal *term, enum url_action action, url_list_t *urls); + const struct terminal *term, enum url_action action, const regex_t *preg, + bool osc8, url_list_t *urls); void urls_assign_key_combos(const struct config *conf, url_list_t *urls); -void urls_render(struct terminal *term); +void urls_render(struct terminal *term, const struct config_spawn_template *launch); void urls_reset(struct terminal *term); void urls_input(struct seat *seat, struct terminal *term, From 9d0f5cbd2ac0457ef825cb1553d68c35f76e3dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 3 Feb 2025 09:05:46 +0100 Subject: [PATCH 1042/1323] foot.ini: improve documentation of custom regex --- foot.ini | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/foot.ini b/foot.ini index 1fb20e67..7514c02b 100644 --- a/foot.ini +++ b/foot.ini @@ -71,9 +71,17 @@ # osc8-underline=url-mode # regex=([a-z][[:alnum:]-]+:(/{1,3}|[a-z0-9%])|www[:digit:]{0,3}[.])([^[:space:](){}<>]+|\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))*\])+(\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))*\]|[^]\[[:space:]`!(){};:'".,<>?«»“”‘’]) +# You can define your own regex's, by adding a section called +# 'regex:<ID>' with a 'regex' and 'launch' key. These can then be tied +# to a key-binding: + # [regex:your-fancy-name] # regex=<a POSIX-Extended Regular Expression> # launch=<path to script or application> {match} +# +# [key-bindings] +# regex-launch=[your-fancy-name] Control+Shift+q +# regex-copy=[your-fancy-name] Control+Alt+Shift+q [cursor] # style=block From 2f902c1f5b5cd3a0bcbc985865a533de58a2a655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 3 Feb 2025 09:15:33 +0100 Subject: [PATCH 1043/1323] doc: foot.ini: document custom regular expressions --- doc/foot.ini.5.scd | 58 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index a23e3977..af355d68 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -755,6 +755,9 @@ xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 # SECTION: url +Note that you can also add custom regular expressions, see the 'regex' +section. + *launch* Command to execute when opening URLs. _${url}_ will be replaced with the actual URL. Default: _xdg-open ${url}_. @@ -783,11 +786,40 @@ xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 Default: _sadfjklewcmpgh_. *regex* - URL regex to use when auto-detecting URLs. The format is + + Regular expression to use when auto-detecting URLs. The format is "POSIX-Extended Regular Expressions". Default: _([a-z][[:alnum:]-]+:(/{1,3}|[a-z0-9%])|www[:digit:]{0,3}[.])([^[:space:](){}<>]+|\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))\*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))\*\])+(\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))\*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))\*\]|[^]\[[:space:]`!(){};:'".,<>?«»“”‘’])_ +# SECTION: regex + +Similar to the 'url' mode, but with custom defined regular expressions +(and launchers). + +To use a custom defined regular expression, you also need to add a key +binding for it. This is done in the *key-binding* section, see below +for details. In short, you need to: + +``` +[regex:foo] +regex=foo(bar)? +launch=path-to-script-or-application {match} + +[key-bindings] +regex-launch=[foo] Control+Shift+q +regex-copy=[foo] Control+Mod1+Shift+q +``` + +*launch* + Command to execute when "launching" a regex match. _${match}_ will + be replaced with the actual URL. Default: _not set_. + +*regex* + Regular expression to use when matching text. The format is + "POSIX-Extended Regular Expressions". Default: _not set_. + + # SECTION: cursor This section controls the cursor style and color. Note that @@ -1230,6 +1262,30 @@ e.g. *search-start=none*. jump label with a key sequence that will place the URL in the clipboard. Default: _none_. +*regex-launch* + Enter regex mode. This works exactly the same as URL mode; all + regex matches are tagged with a jump label with a key sequence + that will "launch" to match (and exit regex mode). + + The name of the regex section must be specified in the key + binding: + + ``` + [regex:foo] + regex=foo(bar)? + launch=path-to-script-or-application {match} + + [key-bindings] + regex-launch=[foo] Control+Shift+q + regex-copy=[foo] Control+Mod1+Shift+q + ``` + + Default: _none_. + +*regex-copy* + Same as *regex-copy*, but the match is placed in the clipboard, + instead of "launched", upon activation. Default: _none_. + *prompt-prev* Jump to the previous, currently not visible, prompt (requires shell integration, see *foot*(1)). Default: _Control+Shift+z_. From cf4324e6c64dc9f19eda8379141e06423d5c38f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 3 Feb 2025 09:29:42 +0100 Subject: [PATCH 1044/1323] tests: config: handle regex key bindings --- tests/test-config.c | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/tests/test-config.c b/tests/test-config.c index 37cf4f22..c9f6586c 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -796,7 +796,7 @@ static void test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), int action, int max_action, const char *const *map, struct config_key_binding_list *bindings, - enum key_binding_type type) + enum key_binding_type type, bool need_argv, bool need_section_id) { xassert(map[action] != NULL); xassert(bindings->count == 0); @@ -808,7 +808,10 @@ test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), const bool alt = action % 3; const bool shift = action % 4; const bool super = action % 5; - const bool argv = action % 6; + const bool argv = need_argv; + const bool section_id = need_section_id; + + xassert(!(argv && section_id)); static const char *const args[] = { "command", "arg1", "arg2", "arg3 has spaces"}; @@ -847,7 +850,7 @@ test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), xkb_keysym_get_name(sym, sym_name, sizeof(sym_name)); snprintf(value, sizeof(value), "%s%s%s", - argv ? "[command arg1 arg2 \"arg3 has spaces\"] " : "", + argv ? "[command arg1 arg2 \"arg3 has spaces\"] " : section_id ? "[foobar]" : "", modifier_string, sym_name); break; } @@ -856,7 +859,7 @@ test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), const char *const button_name = button_map[button_idx].name; int chars = snprintf( value, sizeof(value), "%s%s%s", - argv ? "[command arg1 arg2 \"arg3 has spaces\"] " : "", + argv ? "[command arg1 arg2 \"arg3 has spaces\"] " : section_id ? "[foobar]" : "", modifier_string, button_name); xassert(click_count > 0); @@ -897,6 +900,18 @@ test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), ctx->section, ctx->key, ctx->value, ALEN(args), binding->aux.pipe.args[ALEN(args)]); } + } else if (section_id) { + if (binding->aux.regex_name == NULL) { + BUG("[%s].%s=%s: regex name is NULL", + ctx->section, ctx->key, ctx->value); + } + + if (!streq(binding->aux.regex_name, "foobar")) { + BUG("[%s].%s=%s: regex name not the expected one: " + "expected=\"%s\", got=\"%s\"", + ctx->section, ctx->key, ctx->value, + "foobar", binding->aux.regex_name); + } } else { if (binding->aux.pipe.args != NULL) { BUG("[%s].%s=%s: pipe argv not NULL", @@ -1092,7 +1107,9 @@ test_section_key_bindings(void) test_key_binding( &ctx, &parse_section_key_bindings, action, BIND_ACTION_KEY_COUNT - 1, - binding_action_map, &conf.bindings.key, KEY_BINDING); + binding_action_map, &conf.bindings.key, KEY_BINDING, + action >= BIND_ACTION_PIPE_SCROLLBACK && action <= BIND_ACTION_PIPE_COMMAND_OUTPUT, + action >= BIND_ACTION_REGEX_LAUNCH && action <= BIND_ACTION_REGEX_COPY); } config_free(&conf); @@ -1127,7 +1144,8 @@ test_section_search_bindings(void) test_key_binding( &ctx, &parse_section_search_bindings, action, BIND_ACTION_SEARCH_COUNT - 1, - search_binding_action_map, &conf.bindings.search, KEY_BINDING); + search_binding_action_map, &conf.bindings.search, KEY_BINDING, + false, false); } config_free(&conf); @@ -1163,7 +1181,8 @@ test_section_url_bindings(void) test_key_binding( &ctx, &parse_section_url_bindings, action, BIND_ACTION_URL_COUNT - 1, - url_binding_action_map, &conf.bindings.url, KEY_BINDING); + url_binding_action_map, &conf.bindings.url, KEY_BINDING, + false, false); } config_free(&conf); @@ -1199,7 +1218,8 @@ test_section_mouse_bindings(void) test_key_binding( &ctx, &parse_section_mouse_bindings, action, BIND_ACTION_COUNT - 1, - binding_action_map, &conf.bindings.mouse, MOUSE_BINDING); + binding_action_map, &conf.bindings.mouse, MOUSE_BINDING, + false, false); } config_free(&conf); From 31f536ff8c8cb746de7d4dea2eb1402d44a4b43e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 3 Feb 2025 09:31:34 +0100 Subject: [PATCH 1045/1323] config: remove debug logging --- config.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/config.c b/config.c index e35e4233..82a4811f 100644 --- a/config.c +++ b/config.c @@ -3570,8 +3570,6 @@ config_override_apply(struct config *conf, config_override_t *overrides, continue; } - LOG_ERR("section-name=%s", section_name); - char *maybe_section_suffix = NULL; enum section section = str_to_section(section_name, &maybe_section_suffix); From a984531ce57f30bc9ec7f664e7c2e8c77b3f439e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 3 Feb 2025 13:56:57 +0100 Subject: [PATCH 1046/1323] url-mode: use the first *sub* expression as URL When auto-matching URLs (or custom regular expressions), use the first *subexpression* as URL, rather than the while regex match. This allows us to write custom regular expressions with prefix/suffix strings that should not be included in the presented match. --- config.c | 56 +++++++++++++++++++++++++++++----------------- doc/foot.ini.5.scd | 15 +++++++++---- foot.ini | 2 +- url-mode.c | 6 ++--- 4 files changed, 51 insertions(+), 28 deletions(-) diff --git a/config.c b/config.c index 82a4811f..604c0a76 100644 --- a/config.c +++ b/config.c @@ -1256,6 +1256,12 @@ parse_section_url(struct context *ctx) return false; } + if (preg.re_nsub == 0) { + LOG_CONTEXTUAL_ERR("invalid regex: no marked subexpression(s)"); + regfree(&preg); + return false; + } + regfree(&conf->url.preg); free(conf->url.regex); @@ -1300,6 +1306,12 @@ parse_section_regex(struct context *ctx) return false; } + if (preg.re_nsub == 0) { + LOG_CONTEXTUAL_ERR("invalid regex: no marked subexpression(s)"); + regfree(&preg); + return false; + } + if (regex == NULL) { tll_push_back(conf->custom_regexes, ((struct custom_regex){.name = xstrdup(regex_name)})); @@ -3426,33 +3438,37 @@ config_load(struct config *conf, const char *conf_path, */ const char *url_regex_string = "(" - "[a-z][[:alnum:]-]+:" // protocol "(" - "/{1,3}|[a-z0-9%]" // slashes (what's the OR part for?) + "[a-z][[:alnum:]-]+:" // protocol + "(" + "/{1,3}|[a-z0-9%]" // slashes (what's the OR part for?) + ")" + "|" + "www[:digit:]{0,3}[.]" + //"|" + //"[a-z0-9.\\-]+[.][a-z]{2,4}/" /* "looks like domain name followed by a slash" - remove? */ + ")" + "(" + "[^[:space:](){}<>]+" + "|" + "\\(([^[:space:](){}<>]+|(\\([^[:space:](){}<>]+\\)))*\\)" + "|" + "\\[([^]\\[[:space:](){}<>]+|(\\[[^]\\[[:space:](){}<>]+\\]))*\\]" + ")+" + "(" + "\\(([^[:space:](){}<>]+|(\\([^[:space:](){}<>]+\\)))*\\)" + "|" + "\\[([^]\\[[:space:](){}<>]+|(\\[[^]\\[[:space:](){}<>]+\\]))*\\]" + "|" + "[^]\\[[:space:]`!(){};:'\".,<>?«»“”‘’]" ")" - "|" - "www[:digit:]{0,3}[.]" - //"|" - //"[a-z0-9.\\-]+[.][a-z]{2,4}/" /* "looks like domain name followed by a slash" - remove? */ - ")" - "(" - "[^[:space:](){}<>]+" - "|" - "\\(([^[:space:](){}<>]+|(\\([^[:space:](){}<>]+\\)))*\\)" - "|" - "\\[([^]\\[[:space:](){}<>]+|(\\[[^]\\[[:space:](){}<>]+\\]))*\\]" - ")+" - "(" - "\\(([^[:space:](){}<>]+|(\\([^[:space:](){}<>]+\\)))*\\)" - "|" - "\\[([^]\\[[:space:](){}<>]+|(\\[[^]\\[[:space:](){}<>]+\\]))*\\]" - "|" - "[^]\\[[:space:]`!(){};:'\".,<>?«»“”‘’]" ")" ; + int r = regcomp(&conf->url.preg, url_regex_string, REG_EXTENDED); xassert(r == 0); conf->url.regex = xstrdup(url_regex_string); + xassert(conf->url.preg.re_nsub >= 1); } tll_foreach(*initial_user_notifications, it) { diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index af355d68..742281d4 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -786,11 +786,13 @@ section. Default: _sadfjklewcmpgh_. *regex* - Regular expression to use when auto-detecting URLs. The format is - "POSIX-Extended Regular Expressions". + "POSIX-Extended Regular Expressions". Note that the first marked + subexpression is used a the URL. In other words, if you want the + whole regex matćh to be used as an URL, surround all of it with + parenthesis: *(regex-pattern)*. - Default: _([a-z][[:alnum:]-]+:(/{1,3}|[a-z0-9%])|www[:digit:]{0,3}[.])([^[:space:](){}<>]+|\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))\*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))\*\])+(\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))\*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))\*\]|[^]\[[:space:]`!(){};:'".,<>?«»“”‘’])_ + Default: _(([a-z][[:alnum:]-]+:(/{1,3}|[a-z0-9%])|www[:digit:]{0,3}[.])([^[:space:](){}<>]+|\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))\*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))\*\])+(\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))\*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))\*\]|[^]\[[:space:]`!(){};:'".,<>?«»“”‘’]))_ # SECTION: regex @@ -817,7 +819,12 @@ regex-copy=[foo] Control+Mod1+Shift+q *regex* Regular expression to use when matching text. The format is - "POSIX-Extended Regular Expressions". Default: _not set_. + "POSIX-Extended Regular Expressions". Note that the first marked + subexpression is used a the URL. In other words, if you want the + whole regex matćh to be used as an URL, surround all of it with + parenthesis: *(regex-pattern)*. + + Default: _not set_. # SECTION: cursor diff --git a/foot.ini b/foot.ini index 7514c02b..a9a790ac 100644 --- a/foot.ini +++ b/foot.ini @@ -69,7 +69,7 @@ # launch=xdg-open ${url} # label-letters=sadfjklewcmpgh # osc8-underline=url-mode -# regex=([a-z][[:alnum:]-]+:(/{1,3}|[a-z0-9%])|www[:digit:]{0,3}[.])([^[:space:](){}<>]+|\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))*\])+(\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))*\]|[^]\[[:space:]`!(){};:'".,<>?«»“”‘’]) +# regex=(([a-z][[:alnum:]-]+:(/{1,3}|[a-z0-9%])|www[:digit:]{0,3}[.])([^[:space:](){}<>]+|\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))*\])+(\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))*\]|[^]\[[:space:]`!(){};:'".,<>?«»“”‘’])) # You can define your own regex's, by adding a section called # 'regex:<ID>' with a 'regex' and 'launch' key. These can then be tied diff --git a/url-mode.c b/url-mode.c index 3108ec12..0101de19 100644 --- a/url-mode.c +++ b/url-mode.c @@ -400,13 +400,13 @@ regex_detected(const struct terminal *term, enum url_action action, if (r == REG_NOMATCH) break; - const size_t mlen = matches[0].rm_eo - matches[0].rm_so; - const size_t start = &search_string[matches[0].rm_so] - v->utf8; + const size_t mlen = matches[1].rm_eo - matches[1].rm_so; + const size_t start = &search_string[matches[1].rm_so] - v->utf8; const size_t end = start + mlen; LOG_DBG( "regex match at row %d: %.*srow/col = %dx%d", - matches[0].rm_so, (int)mlen, &search_string[matches[0].rm_so], + matches[1].rm_so, (int)mlen, &search_string[matches[1].rm_so], v->map[start].row, v->map[start].col); tll_push_back( From 0a32dc3820af009aedbb58cfb5817b9462dbb010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 3 Feb 2025 14:08:23 +0100 Subject: [PATCH 1047/1323] spawn template variables are on the form ${}, not {} --- doc/foot.ini.5.scd | 4 ++-- foot.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 742281d4..89984ef5 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -806,7 +806,7 @@ for details. In short, you need to: ``` [regex:foo] regex=foo(bar)? -launch=path-to-script-or-application {match} +launch=path-to-script-or-application ${match} [key-bindings] regex-launch=[foo] Control+Shift+q @@ -1280,7 +1280,7 @@ e.g. *search-start=none*. ``` [regex:foo] regex=foo(bar)? - launch=path-to-script-or-application {match} + launch=path-to-script-or-application ${match} [key-bindings] regex-launch=[foo] Control+Shift+q diff --git a/foot.ini b/foot.ini index a9a790ac..2489887f 100644 --- a/foot.ini +++ b/foot.ini @@ -77,7 +77,7 @@ # [regex:your-fancy-name] # regex=<a POSIX-Extended Regular Expression> -# launch=<path to script or application> {match} +# launch=<path to script or application> ${match} # # [key-bindings] # regex-launch=[your-fancy-name] Control+Shift+q From b1f16c84e0be8d3074139f0be56e0852acceaef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 4 Feb 2025 10:10:10 +0100 Subject: [PATCH 1048/1323] doc: improve regex example --- doc/foot.ini.5.scd | 19 ++++++++++--------- foot.ini | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 89984ef5..68216fcd 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -801,16 +801,17 @@ Similar to the 'url' mode, but with custom defined regular expressions To use a custom defined regular expression, you also need to add a key binding for it. This is done in the *key-binding* section, see below -for details. In short, you need to: +for details. For example, a regex to detect hash digests (e.g. git +commit hashes) could look like: ``` -[regex:foo] -regex=foo(bar)? +[regex:hashes] +regex=([a-fA-f0-9]{7,128}) launch=path-to-script-or-application ${match} [key-bindings] -regex-launch=[foo] Control+Shift+q -regex-copy=[foo] Control+Mod1+Shift+q +regex-launch=[hashes] Control+Shift+q +regex-copy=[hashes] Control+Mod1+Shift+q ``` *launch* @@ -1278,13 +1279,13 @@ e.g. *search-start=none*. binding: ``` - [regex:foo] - regex=foo(bar)? + [regex:hashes] + regex=([a-fA-f0-9]{7,128}) launch=path-to-script-or-application ${match} [key-bindings] - regex-launch=[foo] Control+Shift+q - regex-copy=[foo] Control+Mod1+Shift+q + regex-launch=[hashes] Control+Shift+q + regex-copy=[hashes] Control+Mod1+Shift+q ``` Default: _none_. diff --git a/foot.ini b/foot.ini index 2489887f..a1aa118c 100644 --- a/foot.ini +++ b/foot.ini @@ -73,7 +73,7 @@ # You can define your own regex's, by adding a section called # 'regex:<ID>' with a 'regex' and 'launch' key. These can then be tied -# to a key-binding: +# to a key-binding. See foot.ini(5) for details # [regex:your-fancy-name] # regex=<a POSIX-Extended Regular Expression> From 9e12f791c5df3ac25574894d41d606967d618888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 5 Feb 2025 13:43:11 +0100 Subject: [PATCH 1049/1323] doc: regex: custom regex's aren't URLs --- doc/foot.ini.5.scd | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 68216fcd..61216c75 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -788,8 +788,8 @@ section. *regex* Regular expression to use when auto-detecting URLs. The format is "POSIX-Extended Regular Expressions". Note that the first marked - subexpression is used a the URL. In other words, if you want the - whole regex matćh to be used as an URL, surround all of it with + subexpression is used as the URL. In other words, if you want the + whole regex match to be used as an URL, surround all of it with parenthesis: *(regex-pattern)*. Default: _(([a-z][[:alnum:]-]+:(/{1,3}|[a-z0-9%])|www[:digit:]{0,3}[.])([^[:space:](){}<>]+|\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))\*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))\*\])+(\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))\*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))\*\]|[^]\[[:space:]`!(){};:'".,<>?«»“”‘’]))_ @@ -821,8 +821,8 @@ regex-copy=[hashes] Control+Mod1+Shift+q *regex* Regular expression to use when matching text. The format is "POSIX-Extended Regular Expressions". Note that the first marked - subexpression is used a the URL. In other words, if you want the - whole regex matćh to be used as an URL, surround all of it with + subexpression is used as the match. In other words, if you want + the whole regex match to be used, surround all of it with parenthesis: *(regex-pattern)*. Default: _not set_. From 9d8021de478a82e6ea41a950afcc43388c600137 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 5 Feb 2025 13:46:00 +0100 Subject: [PATCH 1050/1323] changelog: custom regex's --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b750f3d..05bda33c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,11 @@ * Support for the new Wayland protocol `xdg-system-bell-v1` protocol (added in wayland-protocols 1.38), via the new config option `bell.system=no|yes` (defaults to `yes`). +* Added support for custom regex matching ([#1386][1386], + [#1872][1872]) + +[1386]: https://codeberg.org/dnkl/foot/issues/1386 +[1872]: https://codeberg.org/dnkl/foot/issues/1872 ### Changed From 88dcde3ed8b30c14a5d834d0e88f9b4a5f584939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 6 Feb 2025 07:31:30 +0100 Subject: [PATCH 1051/1323] term: insert-mode: handle combining characters correctly When the client application emits combining characters, for example multi-codepoint emojis, in insert-mode, we ended up pushing partial graphemes to the right, for each codepoint, resulting in too many cells (and with the wrong content) being inserted. The fix is fairly simple; don't "insert" when appending characters to an existing grapheme cluster. This isn't something we can detect easily in print_insert() (it would require us to do grapheme clustering again). Fortunately, we do have the required information in action_utf8_print(). So, pass this information as a boolean to term_print(). Closes #1947 --- CHANGELOG.md | 4 ++++ csi.c | 2 +- terminal.c | 7 ++++--- terminal.h | 3 ++- vt.c | 4 +++- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05bda33c..b9b89bf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,9 +105,13 @@ ([#1929][1929]). * Foot not closing file descriptors for unrecognized or `no_keymap` keymaps. +* Combining characters (including emojis consisting of multiple + codepoints) not being handled correctly when _insert mode_ is + enabled ([#1947][1947]). [1918]: https://codeberg.org/dnkl/foot/issues/1918 [1929]: https://codeberg.org/dnkl/foot/issues/1929 +[1947]: https://codeberg.org/dnkl/foot/issues/1947 ### Security diff --git a/csi.c b/csi.c index 61cbdced..b982023c 100644 --- a/csi.c +++ b/csi.c @@ -793,7 +793,7 @@ csi_dispatch(struct terminal *term, uint8_t final) const int width = c32width(term->vt.last_printed); if (width > 0) { for (int i = 0; i < count; i++) - term_print(term, term->vt.last_printed, width); + term_print(term, term->vt.last_printed, width, false); } } break; diff --git a/terminal.c b/terminal.c index 9fd20002..b88a794e 100644 --- a/terminal.c +++ b/terminal.c @@ -3896,7 +3896,7 @@ term_fill(struct terminal *term, int r, int c, uint8_t data, size_t count, } void -term_print(struct terminal *term, char32_t wc, int width) +term_print(struct terminal *term, char32_t wc, int width, bool insert_mode_disable) { xassert(width > 0); @@ -3918,7 +3918,8 @@ term_print(struct terminal *term, char32_t wc, int width) } print_linewrap(term); - print_insert(term, width); + if (!insert_mode_disable) + print_insert(term, width); int col = grid->cursor.point.col; @@ -3990,7 +3991,7 @@ term_print(struct terminal *term, char32_t wc, int width) static void ascii_printer_generic(struct terminal *term, char32_t wc) { - term_print(term, wc, 1); + term_print(term, wc, 1, false); } static void diff --git a/terminal.h b/terminal.h index 4242ed1d..d8e7cf94 100644 --- a/terminal.h +++ b/terminal.h @@ -894,7 +894,8 @@ void term_cursor_up(struct terminal *term, int count); void term_cursor_down(struct terminal *term, int count); void term_cursor_blink_update(struct terminal *term); -void term_print(struct terminal *term, char32_t wc, int width); +void term_print(struct terminal *term, char32_t wc, int width, + bool insert_mode_disable); void term_fill(struct terminal *term, int row, int col, uint8_t c, size_t count, bool use_sgr_attrs); diff --git a/vt.c b/vt.c index 2b5eb27d..bd1cf4ca 100644 --- a/vt.c +++ b/vt.c @@ -703,6 +703,7 @@ static void action_utf8_print(struct terminal *term, char32_t wc) { int width = c32width(wc); + bool insert_mode_disable = false; const bool grapheme_clustering = term->grapheme_shaping; #if !defined(FOOT_GRAPHEME_CLUSTERING) @@ -757,6 +758,7 @@ action_utf8_print(struct terminal *term, char32_t wc) if (base_width > 0) { term->grid->cursor.point.col = col; term->grid->cursor.lcf = false; + insert_mode_disable = true; if (composed == NULL) { bool base_from_primary; @@ -954,7 +956,7 @@ action_utf8_print(struct terminal *term, char32_t wc) out: if (width > 0) - term_print(term, wc, width); + term_print(term, wc, width, insert_mode_disable); } static void From 1181f74d19f6f9e881b539ed9fdb8cc17d03f7bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 24 Jan 2025 09:52:57 +0100 Subject: [PATCH 1052/1323] composed: re-factor: break out key calculation from vt.c --- composed.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++ composed.h | 3 +++ vt.c | 24 ++---------------------- 3 files changed, 51 insertions(+), 22 deletions(-) diff --git a/composed.c b/composed.c index 442325ea..7a36275e 100644 --- a/composed.c +++ b/composed.c @@ -4,6 +4,52 @@ #include <stdbool.h> #include "debug.h" +#include "terminal.h" + +uint32_t +composed_key_from_chars(const uint32_t chars[], size_t count) +{ + if (count == 0) + return 0; + + uint32_t key = chars[0]; + for (size_t i = 1; i < count; i++) + key = composed_key_from_key(key, chars[i]); + + return key; +} + +uint32_t +composed_key_from_key(uint32_t prev_key, uint32_t next_char) +{ + unsigned bits = 32 - __builtin_clz(CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO); + + /* Rotate old key 8 bits */ + uint32_t new_key = (prev_key << 8) | (prev_key >> (bits - 8)); + + /* xor with new char */ + new_key ^= next_char; + + /* Multiply with magic hash constant */ + new_key *= 2654435761ul; + + /* And mask, to ensure the new value is within range */ + new_key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO; + return new_key; +} + +UNITTEST +{ + const char32_t chars[] = U"abcdef"; + + uint32_t k1 = composed_key_from_key(chars[0], chars[1]); + uint32_t k2 = composed_key_from_chars(chars, 2); + xassert(k1 == k2); + + uint32_t k3 = composed_key_from_key(k2, chars[2]); + uint32_t k4 = composed_key_from_chars(chars, 3); + xassert(k3 == k4); +} struct composed * composed_lookup(struct composed *root, uint32_t key) diff --git a/composed.h b/composed.h index 17158407..fcaf87d4 100644 --- a/composed.h +++ b/composed.h @@ -12,6 +12,9 @@ struct composed { uint8_t width; }; +uint32_t composed_key_from_chars(const uint32_t chars[], size_t count); +uint32_t composed_key_from_key(uint32_t prev_key, uint32_t next_char); + struct composed *composed_lookup(struct composed *root, uint32_t key); void composed_insert(struct composed **root, struct composed *node); diff --git a/vt.c b/vt.c index bd1cf4ca..8f5d27d9 100644 --- a/vt.c +++ b/vt.c @@ -647,26 +647,6 @@ action_put(struct terminal *term, uint8_t c) dcs_put(term, c); } -static inline uint32_t -chain_key(uint32_t old_key, uint32_t new_wc) -{ - unsigned bits = 32 - __builtin_clz(CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO); - - /* Rotate old key 8 bits */ - uint32_t new_key = (old_key << 8) | (old_key >> (bits - 8)); - - /* xor with new char */ - new_key ^= new_wc; - - /* Multiply with magic hash constant */ - new_key *= 2654435761ul; - - /* And mask, to ensure the new value is within range */ - new_key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO; - - return new_key; -} - #if defined(FOOT_GRAPHEME_CLUSTERING) static int emoji_vs_compare(const void *_key, const void *_entry) @@ -738,9 +718,9 @@ action_utf8_print(struct terminal *term, char32_t wc) if (composed != NULL) { base = composed->chars[0]; last = composed->chars[composed->count - 1]; - key = chain_key(composed->key, wc); + key = composed_key_from_key(composed->key, wc); } else - key = chain_key(base, wc); + key = composed_key_from_key(base, wc); #if defined(FOOT_GRAPHEME_CLUSTERING) if (grapheme_clustering) { From e248e73753d61bfd24f1af8e824231434db63c53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 24 Jan 2025 14:15:01 +0100 Subject: [PATCH 1053/1323] composed: refactor: break out lookup with collision detection --- composed.c | 37 ++++++++++++++++++++++++++++++- composed.h | 6 ++++- vt.c | 64 +++++++++++------------------------------------------- 3 files changed, 54 insertions(+), 53 deletions(-) diff --git a/composed.c b/composed.c index 7a36275e..2d9ed47d 100644 --- a/composed.c +++ b/composed.c @@ -51,7 +51,7 @@ UNITTEST xassert(k3 == k4); } -struct composed * +const struct composed * composed_lookup(struct composed *root, uint32_t key) { struct composed *node = root; @@ -66,6 +66,41 @@ composed_lookup(struct composed *root, uint32_t key) return NULL; } +const struct composed * +composed_lookup_without_collision(struct composed *root, uint32_t *key, + const char32_t *prefix_text, size_t prefix_len, + char32_t wc, int forced_width) +{ + while (true) { + const struct composed *cc = composed_lookup(root, *key); + if (cc == NULL) + return NULL; + + bool match = cc->count == prefix_len + 1 && + cc->forced_width == forced_width && + cc->chars[prefix_len] == wc; + + if (match) { + for (size_t i = 0; i < prefix_len; i++) { + if (cc->chars[i] != prefix_text[i]) { + match = false; + break; + } + } + } + + if (match) + return cc; + + (*key)++; + *key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO; + + /* TODO: this will loop infinitly if the composed table is full */ + } + + return NULL; +} + void composed_insert(struct composed **root, struct composed *node) { diff --git a/composed.h b/composed.h index fcaf87d4..18afb146 100644 --- a/composed.h +++ b/composed.h @@ -10,12 +10,16 @@ struct composed { uint32_t key; uint8_t count; uint8_t width; + uint8_t forced_width; }; uint32_t composed_key_from_chars(const uint32_t chars[], size_t count); uint32_t composed_key_from_key(uint32_t prev_key, uint32_t next_char); -struct composed *composed_lookup(struct composed *root, uint32_t key); +const struct composed *composed_lookup(struct composed *root, uint32_t key); +const struct composed *composed_lookup_without_collision( + struct composed *root, uint32_t *key, + const char32_t *prefix, size_t prefix_len, char32_t wc, int forced_width); void composed_insert(struct composed **root, struct composed *node); void composed_free(struct composed *root); diff --git a/vt.c b/vt.c index 8f5d27d9..5447493a 100644 --- a/vt.c +++ b/vt.c @@ -793,60 +793,21 @@ action_utf8_print(struct terminal *term, char32_t wc) xassert(wanted_count <= 255); - size_t collision_count = 0; - - /* Look for existing combining chain */ - while (true) { - if (unlikely(collision_count > 128)) { - static bool have_warned = false; - if (!have_warned) { - have_warned = true; - LOG_WARN("ignoring composed character: " - "too many collisions in hash table"); - } - return; - } - - const struct composed *cc = composed_lookup(term->composed, key); - if (cc == NULL) - break; - - /* - * We may have a key collisison, so need to check that - * it's a true match. If not, bump the key and try - * again. - */ - - xassert(key == cc->key); - if (cc->chars[0] != base || - cc->count != wanted_count || - cc->chars[wanted_count - 1] != wc) - { -#if 0 - LOG_WARN("COLLISION: base: %04x/%04x, count: %d/%zu, last: %04x/%04x", - cc->chars[0], base, cc->count, wanted_count, cc->chars[wanted_count - 1], wc); -#endif - key++; - key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO; - collision_count++; - continue; - } - - bool match = composed != NULL - ? memcmp(&cc->chars[1], &composed->chars[1], - (wanted_count - 2) * sizeof(cc->chars[0])) == 0 - : true; - - if (!match) { - key++; - key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO; - collision_count++; - continue; - } + /* Check if we already have a match for the entire compose chain */ + const struct composed *cc = + composed_lookup_without_collision( + term->composed, &key, + composed != NULL ? composed->chars : &(char32_t){base}, + composed != NULL ? composed->count : 1, + wc, 0); + if (cc != NULL) { + /* We *do* have a match! */ wc = CELL_COMB_CHARS_LO + cc->key; width = cc->width; goto out; + } else { + /* No match - allocate a new chain below */ } if (unlikely(term->composed_count >= @@ -867,6 +828,7 @@ action_utf8_print(struct terminal *term, char32_t wc) new_cc->count = wanted_count; new_cc->chars[0] = base; new_cc->chars[wanted_count - 1] = wc; + new_cc->forced_width = 0; if (composed != NULL) { memcpy(&new_cc->chars[1], &composed->chars[1], @@ -923,7 +885,7 @@ action_utf8_print(struct terminal *term, char32_t wc) term->composed_count++; composed_insert(&term->composed, new_cc); - wc = CELL_COMB_CHARS_LO + key; + wc = CELL_COMB_CHARS_LO + new_cc->key; width = new_cc->width; xassert(wc >= CELL_COMB_CHARS_LO); From 1111f7e918a3b41512d11023ef5bf9585fa30eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 25 Jan 2025 14:06:30 +0100 Subject: [PATCH 1054/1323] grid: reflow: handle composed characters longer than 2 cells The logic that tries to ensure we don't break a line in the middle of a multi-cell character was flawed when the number of cells were larger than 2. In particular, if the number of cells to copy were limited by the number of cells left on the current (new) line, and were less than the length of the multi-cell character, then we failed to insert the correct number of spacers, and also ended up misplacing the multi-cell character; instead of pushing it to the next line, it was inserted on the current line, even though it doesn't fit. Also change how trailing SPACER cells are rendered (cells that are "fillers" at then end of a line, when a multi-column character was pushed over to the next line): don't copy the previous cell's attributes (which may be wrong anyway), use default attributes instead. --- grid.c | 8 +++----- terminal.c | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/grid.c b/grid.c index b7c0447c..eb24869d 100644 --- a/grid.c +++ b/grid.c @@ -1052,7 +1052,7 @@ grid_resize_and_reflow( */ while ( unlikely( - amount > 1 && + amount > 0 && from + amount < old_cols && old_row->cells[from + amount].wc >= CELL_SPACER + 1)) { @@ -1061,7 +1061,7 @@ grid_resize_and_reflow( } xassert( - amount == 1 || + amount <= 1 || old_row->cells[from + amount - 1].wc <= CELL_SPACER + 1); } @@ -1084,11 +1084,9 @@ grid_resize_and_reflow( if (unlikely(spacers > 0)) { xassert(new_col_idx + spacers == new_cols); - const struct cell *cell = &old_row->cells[from - 1]; - for (int i = 0; i < spacers; i++, new_col_idx++) { new_row->cells[new_col_idx].wc = CELL_SPACER; - new_row->cells[new_col_idx].attrs = cell->attrs; + new_row->cells[new_col_idx].attrs = (struct attributes){0}; } } } diff --git a/terminal.c b/terminal.c index b88a794e..bf70a37e 100644 --- a/terminal.c +++ b/terminal.c @@ -3826,7 +3826,7 @@ print_spacer(struct terminal *term, int col, int remaining) struct cell *cell = &row->cells[col]; cell->wc = CELL_SPACER + remaining; - cell->attrs = term->vt.attrs; + cell->attrs = (struct attributes){0}; } /* From 7a8d2b5e012636def9545075e01cdf9a6f309355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 25 Jan 2025 14:09:35 +0100 Subject: [PATCH 1055/1323] osc: wip: kitty text size protocol This brings initial support for the new kitty text-sizing protocol. Note hat only the width-parameter ('w') is supported. That is, no font scaling, and no multi-line cells. For now, only explicit widths are supported. That is, w=0 does not yet work. There are a couple of changes to the renderer, to handle e.g. OSC 66 ; w=6 ; foobar ST There are two ways this can get rendered, depending on whether grapheme shaping has been enabled. We either shape it, and get an array of glyphs back that we render. Or, we rasterize each codepoint ourselves, and render each resulting glyph. The two cases ends up in two different renderer loops, that worked somewhat different. In particular, the first case has probably never been tested/used at all... With this patch, both are changed, and now uses some heuristic to differentiate between multi-cell text strings (like in the example above), or single-cell combining characters. The difference is mainly in which offset to use for the secondary glyphs. In a multi-cell string, each glyph is mapped to its own cell, while in the combining case, we try to map all glyphs to the same cell. --- osc.c | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- render.c | 37 +++++++++++++++++--------- 2 files changed, 104 insertions(+), 14 deletions(-) diff --git a/osc.c b/osc.c index e335dc61..6d8bb40c 100644 --- a/osc.c +++ b/osc.c @@ -610,7 +610,6 @@ verify_kitty_id_is_valid(const char *id) } UNIGNORE_WARNINGS - static void kitty_notification(struct terminal *term, char *string) { @@ -1135,6 +1134,82 @@ out: free(sound_name); } +static void +kitty_text_size(struct terminal *term, char *string) +{ + char *text = strchr(string, ';'); + if (text == NULL) + return; + + char *parameters = string; + *text = '\0'; + text++; + + char32_t *wchars = ambstoc32(text); + if (wchars == NULL) + return; + + int width = 0; + + char *ctx = NULL; + for (char *param = strtok_r(parameters, ":", &ctx); + param != NULL; + param = strtok_r(NULL, ":", &ctx)) + { + /* All parameters are on the form X=value, where X is always + exactly one character */ + if (param[0] == '\0' || param[1] != '=') + continue; + + char *value = ¶m[2]; + + switch (param[0]) { + case 'w': { + errno = 0; + char *end = NULL; + unsigned long w = strtoul(value, &end, 10); + + if (*end == '\0' && errno == 0 && w <= 7) { + width = (int)w; + break; + } else + LOG_ERR("OSC-66: invalid 'w' value, ignoring"); + break; + } + + case 's': + case 'n': + case 'd': + case 'v': + LOG_WARN("OSC-66: unsupported: '%c' parameter, ignoring", param[0]); + break; + } + } + + const size_t len = c32len(wchars); + uint32_t key = composed_key_from_chars(wchars, len); + + const struct composed *composed = composed_lookup_without_collision( + term->composed, &key, wchars, len - 1, wchars[len - 1], width); + + if (composed == NULL) { + struct composed *new_cc = xmalloc(sizeof(*new_cc)); + new_cc->chars = wchars; + new_cc->count = len; + new_cc->key = key; + new_cc->width = width; + new_cc->forced_width = width; + + term->composed_count++; + composed_insert(&term->composed, new_cc); + composed = new_cc; + } else if (composed->width == width) { + free(wchars); + } + + term_print(term, CELL_COMB_CHARS_LO + composed->key, composed->forced_width > 0 ? composed->forced_width : composed->width); +} + void osc_dispatch(struct terminal *term) { @@ -1371,6 +1446,10 @@ osc_dispatch(struct terminal *term) osc_selection(term, string); break; + case 66: /* text-size protocol (kitty) */ + kitty_text_size(term, string); + break; + case 99: /* Kitty notifications */ kitty_notification(term, string); break; diff --git a/render.c b/render.c index 0cca0643..13e9d708 100644 --- a/render.c +++ b/render.c @@ -869,11 +869,16 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag } if (grapheme != NULL) { - cell_cols = composed->width; + const int forced_width = composed->forced_width; + + cell_cols = forced_width > 0 ? forced_width : composed->width; composed = NULL; glyphs = grapheme->glyphs; glyph_count = grapheme->count; + + if (forced_width > 0) + glyph_count = min(glyph_count, forced_width); } } @@ -890,7 +895,9 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag } else { glyph_count = 1; glyphs = &single; - cell_cols = single->cols; + + const size_t forced_width = composed != NULL ? composed->forced_width : 0; + cell_cols = forced_width > 0 ? forced_width : single->cols; } } } @@ -972,7 +979,7 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag int g_x = glyph->x; int g_y = glyph->y; - if (i > 0 && glyph->x >= 0) + if (i > 0 && glyph->x >= 0 && cell_cols == 1) g_x -= term->cell_width; if (unlikely(pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8)) { @@ -993,9 +1000,9 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag if (composed != NULL) { assert(glyph_count == 1); - for (size_t i = 1; i < composed->count; i++) { + for (size_t j = 1; j < composed->count; j++) { const struct fcft_glyph *g = fcft_rasterize_char_utf32( - font, composed->chars[i], term->font_subpixel); + font, composed->chars[j], term->font_subpixel); if (g == NULL) continue; @@ -1017,22 +1024,26 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag * somewhat deal with double-width glyphs we use * an offset of *one* cell. */ - int x_ofs = g->x < 0 - ? cell_cols * term->cell_width - : (cell_cols - 1) * term->cell_width; + int x_ofs = cell_cols == 1 + ? g->x < 0 + ? cell_cols * term->cell_width + : (cell_cols - 1) * term->cell_width + : 0; + + if (cell_cols > 1) + pen_x += term->cell_width; pixman_image_composite32( PIXMAN_OP_OVER, clr_pix, g->pix, pix, 0, 0, 0, 0, /* Some fonts use a negative offset, while others use a * "normal" offset */ - pen_x + x_ofs + g->x, - y + term->font_baseline - g->y, - g->width, g->height); + pen_x + letter_x_ofs + x_ofs + g->x, + y + term->font_baseline - g->y, g->width, g->height); } } } - pen_x += glyph->advance.x; + pen_x += cell_cols > 1 ? term->cell_width : glyph->advance.x; } pixman_image_unref(clr_pix); @@ -4398,7 +4409,7 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) } /* Don't shrink grid too much */ - const int min_cols = 2; + const int min_cols = 7; const int min_rows = 1; /* Minimum window size (must be divisible by the scaling factor)*/ From d3f692990ef66f550bb3a0ade2e84107cfbeca47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 26 Jan 2025 07:33:53 +0100 Subject: [PATCH 1056/1323] term+vt: refactor: move "utf8" char processing to term_process_and_print_non_ascii() This function "prints" any non-ascii character (i.e. any character that ends up in the action_utf8_print() function in vt.c) to the grid. This includes grapheme cluster processing etc. action_utf8_print() now simply calls this function. This allows us to re-use the same functionality from other places (like the text-sizing protocol). --- osc.c | 5 +- terminal.c | 255 +++++++++++++++++++++++++++++++++++++++++++++++++++++ terminal.h | 1 + vt.c | 251 +--------------------------------------------------- 4 files changed, 261 insertions(+), 251 deletions(-) diff --git a/osc.c b/osc.c index 6d8bb40c..49bdba67 100644 --- a/osc.c +++ b/osc.c @@ -1207,7 +1207,10 @@ kitty_text_size(struct terminal *term, char *string) free(wchars); } - term_print(term, CELL_COMB_CHARS_LO + composed->key, composed->forced_width > 0 ? composed->forced_width : composed->width); + term_print( + term, CELL_COMB_CHARS_LO + composed->key, + composed->forced_width > 0 ? composed->forced_width : composed->width, + false); } void diff --git a/terminal.c b/terminal.c index bf70a37e..96a215ba 100644 --- a/terminal.c +++ b/terminal.c @@ -27,6 +27,7 @@ #include "commands.h" #include "config.h" #include "debug.h" +#include "emoji-variation-sequences.h" #include "extract.h" #include "grid.h" #include "ime.h" @@ -4073,6 +4074,260 @@ term_single_shift(struct terminal *term, enum charset_designator idx) term->ascii_printer = &ascii_printer_single_shift; } +#if defined(FOOT_GRAPHEME_CLUSTERING) +static int +emoji_vs_compare(const void *_key, const void *_entry) +{ + const struct emoji_vs *key = _key; + const struct emoji_vs *entry = _entry; + + uint32_t cp = key->start; + + if (cp < entry->start) + return -1; + else if (cp > entry->end) + return 1; + else + return 0; +} + +UNITTEST +{ + /* Verify the emoji_vs list is sorted */ + int64_t last_end = -1; + + for (size_t i = 0; i < sizeof(emoji_vs) / sizeof(emoji_vs[0]); i++) { + const struct emoji_vs *vs = &emoji_vs[i]; + xassert(vs->start <= vs->end); + xassert(vs->start > last_end); + xassert(vs->vs15 || vs->vs16); + last_end = vs->end; + } +} +#endif + +void +term_process_and_print_non_ascii(struct terminal *term, char32_t wc) +{ + int width = c32width(wc); + bool insert_mode_disable = false; + const bool grapheme_clustering = term->grapheme_shaping; + +#if !defined(FOOT_GRAPHEME_CLUSTERING) + xassert(!grapheme_clustering); +#endif + + if (term->grid->cursor.point.col > 0 && + (grapheme_clustering || + (!grapheme_clustering && width == 0 && wc >= 0x300))) + { + int col = term->grid->cursor.point.col; + if (!term->grid->cursor.lcf) + col--; + + /* Skip past spacers */ + struct row *row = term->grid->cur_row; + while (row->cells[col].wc >= CELL_SPACER && col > 0) + col--; + + xassert(col >= 0 && col < term->cols); + char32_t base = row->cells[col].wc; + char32_t UNUSED last = base; + + /* Is base cell already a cluster? */ + const struct composed *composed = + (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) + ? composed_lookup(term->composed, base - CELL_COMB_CHARS_LO) + : NULL; + + uint32_t key; + + if (composed != NULL) { + base = composed->chars[0]; + last = composed->chars[composed->count - 1]; + key = composed_key_from_key(composed->key, wc); + } else + key = composed_key_from_key(base, wc); + +#if defined(FOOT_GRAPHEME_CLUSTERING) + if (grapheme_clustering) { + /* Check if we're on a grapheme cluster break */ + if (utf8proc_grapheme_break_stateful( + last, wc, &term->vt.grapheme_state)) + { + term_reset_grapheme_state(term); + goto out; + } + } +#endif + + int base_width = c32width(base); + if (base_width > 0) { + term->grid->cursor.point.col = col; + term->grid->cursor.lcf = false; + insert_mode_disable = true; + + if (composed == NULL) { + bool base_from_primary; + bool comb_from_primary; + bool pre_from_primary; + + char32_t precomposed = term->fonts[0] != NULL + ? fcft_precompose( + term->fonts[0], base, wc, &base_from_primary, + &comb_from_primary, &pre_from_primary) + : (char32_t)-1; + + int precomposed_width = c32width(precomposed); + + /* + * Only use the pre-composed character if: + * + * 1. we *have* a pre-composed character + * 2. the width matches the base characters width + * 3. it's in the primary font, OR one of the base or + * combining characters are *not* from the primary + * font + */ + + if (precomposed != (char32_t)-1 && + precomposed_width == base_width && + (pre_from_primary || + !base_from_primary || + !comb_from_primary)) + { + wc = precomposed; + width = precomposed_width; + term_reset_grapheme_state(term); + goto out; + } + } + + size_t wanted_count = composed != NULL ? composed->count + 1 : 2; + if (wanted_count > 255) { + xassert(composed != NULL); + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + LOG_WARN("combining character overflow:"); + LOG_WARN(" base: 0x%04x", composed->chars[0]); + for (size_t i = 1; i < composed->count; i++) + LOG_WARN(" cc: 0x%04x", composed->chars[i]); + LOG_ERR(" new: 0x%04x", wc); +#endif + /* This is going to break anyway... */ + wanted_count--; + } + + xassert(wanted_count <= 255); + + /* Check if we already have a match for the entire compose chain */ + const struct composed *cc = + composed_lookup_without_collision( + term->composed, &key, + composed != NULL ? composed->chars : &(char32_t){base}, + composed != NULL ? composed->count : 1, + wc, 0); + + if (cc != NULL) { + /* We *do* have a match! */ + wc = CELL_COMB_CHARS_LO + cc->key; + width = cc->width; + goto out; + } else { + /* No match - allocate a new chain below */ + } + + if (unlikely(term->composed_count >= + (CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO))) + { + /* We reached our maximum number of allowed composed + * character chains. Fall through here and print the + * current zero-width character to the current cell */ + LOG_WARN("maximum number of composed characters reached"); + term_reset_grapheme_state(term); + goto out; + } + + /* Allocate new chain */ + struct composed *new_cc = xmalloc(sizeof(*new_cc)); + new_cc->chars = xmalloc(wanted_count * sizeof(new_cc->chars[0])); + new_cc->key = key; + new_cc->count = wanted_count; + new_cc->chars[0] = base; + new_cc->chars[wanted_count - 1] = wc; + new_cc->forced_width = 0; + + if (composed != NULL) { + memcpy(&new_cc->chars[1], &composed->chars[1], + (wanted_count - 2) * sizeof(new_cc->chars[0])); + } + + const int grapheme_width = + composed != NULL ? composed->width : base_width; + + switch (term->conf->tweak.grapheme_width_method) { + case GRAPHEME_WIDTH_MAX: + new_cc->width = max(grapheme_width, width); + break; + + case GRAPHEME_WIDTH_DOUBLE: + new_cc->width = min(grapheme_width + width, 2); + +#if defined(FOOT_GRAPHEME_CLUSTERING) + /* Handle VS-15 and VS-16 variation selectors */ + if (unlikely(grapheme_clustering && + (wc == 0xfe0e || wc == 0xfe0f) && + new_cc->count == 2)) + { + const struct emoji_vs *vs = + bsearch( + &(struct emoji_vs){.start = new_cc->chars[0]}, + emoji_vs, sizeof(emoji_vs) / sizeof(emoji_vs[0]), + sizeof(struct emoji_vs), + &emoji_vs_compare); + + if (vs != NULL) { + xassert(new_cc->chars[0] >= vs->start && + new_cc->chars[0] <= vs->end); + + /* Force a grapheme width of 1 for VS-15, and 2 for VS-16 */ + if (wc == 0xfe0e) { + if (vs->vs15) + new_cc->width = 1; + } else if (wc == 0xfe0f) { + if (vs->vs16) + new_cc->width = 2; + } + } + } +#endif + + break; + + case GRAPHEME_WIDTH_WCSWIDTH: + new_cc->width = grapheme_width + width; + break; + } + + term->composed_count++; + composed_insert(&term->composed, new_cc); + + wc = CELL_COMB_CHARS_LO + new_cc->key; + width = new_cc->width; + + xassert(wc >= CELL_COMB_CHARS_LO); + xassert(wc <= CELL_COMB_CHARS_HI); + goto out; + } + } else + term_reset_grapheme_state(term); + + +out: + if (width > 0) + term_print(term, wc, width, insert_mode_disable); +} + enum term_surface term_surface_kind(const struct terminal *term, const struct wl_surface *surface) { diff --git a/terminal.h b/terminal.h index d8e7cf94..a69a8d0f 100644 --- a/terminal.h +++ b/terminal.h @@ -894,6 +894,7 @@ void term_cursor_up(struct terminal *term, int count); void term_cursor_down(struct terminal *term, int count); void term_cursor_blink_update(struct terminal *term); +void term_process_and_print_non_ascii(struct terminal *term, char32_t wc); void term_print(struct terminal *term, char32_t wc, int width, bool insert_mode_disable); void term_fill(struct terminal *term, int row, int col, uint8_t c, size_t count, diff --git a/vt.c b/vt.c index 5447493a..9c758c55 100644 --- a/vt.c +++ b/vt.c @@ -16,7 +16,6 @@ #include "csi.h" #include "dcs.h" #include "debug.h" -#include "emoji-variation-sequences.h" #include "osc.h" #include "sixel.h" #include "util.h" @@ -647,258 +646,10 @@ action_put(struct terminal *term, uint8_t c) dcs_put(term, c); } -#if defined(FOOT_GRAPHEME_CLUSTERING) -static int -emoji_vs_compare(const void *_key, const void *_entry) -{ - const struct emoji_vs *key = _key; - const struct emoji_vs *entry = _entry; - - uint32_t cp = key->start; - - if (cp < entry->start) - return -1; - else if (cp > entry->end) - return 1; - else - return 0; -} - -UNITTEST -{ - /* Verify the emoji_vs list is sorted */ - int64_t last_end = -1; - - for (size_t i = 0; i < sizeof(emoji_vs) / sizeof(emoji_vs[0]); i++) { - const struct emoji_vs *vs = &emoji_vs[i]; - xassert(vs->start <= vs->end); - xassert(vs->start > last_end); - xassert(vs->vs15 || vs->vs16); - last_end = vs->end; - } -} -#endif - static void action_utf8_print(struct terminal *term, char32_t wc) { - int width = c32width(wc); - bool insert_mode_disable = false; - const bool grapheme_clustering = term->grapheme_shaping; - -#if !defined(FOOT_GRAPHEME_CLUSTERING) - xassert(!grapheme_clustering); -#endif - - if (term->grid->cursor.point.col > 0 && - (grapheme_clustering || - (!grapheme_clustering && width == 0 && wc >= 0x300))) - { - int col = term->grid->cursor.point.col; - if (!term->grid->cursor.lcf) - col--; - - /* Skip past spacers */ - struct row *row = term->grid->cur_row; - while (row->cells[col].wc >= CELL_SPACER && col > 0) - col--; - - xassert(col >= 0 && col < term->cols); - char32_t base = row->cells[col].wc; - char32_t UNUSED last = base; - - /* Is base cell already a cluster? */ - const struct composed *composed = - (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) - ? composed_lookup(term->composed, base - CELL_COMB_CHARS_LO) - : NULL; - - uint32_t key; - - if (composed != NULL) { - base = composed->chars[0]; - last = composed->chars[composed->count - 1]; - key = composed_key_from_key(composed->key, wc); - } else - key = composed_key_from_key(base, wc); - -#if defined(FOOT_GRAPHEME_CLUSTERING) - if (grapheme_clustering) { - /* Check if we're on a grapheme cluster break */ - if (utf8proc_grapheme_break_stateful( - last, wc, &term->vt.grapheme_state)) - { - term_reset_grapheme_state(term); - goto out; - } - } -#endif - - int base_width = c32width(base); - if (base_width > 0) { - term->grid->cursor.point.col = col; - term->grid->cursor.lcf = false; - insert_mode_disable = true; - - if (composed == NULL) { - bool base_from_primary; - bool comb_from_primary; - bool pre_from_primary; - - char32_t precomposed = term->fonts[0] != NULL - ? fcft_precompose( - term->fonts[0], base, wc, &base_from_primary, - &comb_from_primary, &pre_from_primary) - : (char32_t)-1; - - int precomposed_width = c32width(precomposed); - - /* - * Only use the pre-composed character if: - * - * 1. we *have* a pre-composed character - * 2. the width matches the base characters width - * 3. it's in the primary font, OR one of the base or - * combining characters are *not* from the primary - * font - */ - - if (precomposed != (char32_t)-1 && - precomposed_width == base_width && - (pre_from_primary || - !base_from_primary || - !comb_from_primary)) - { - wc = precomposed; - width = precomposed_width; - term_reset_grapheme_state(term); - goto out; - } - } - - size_t wanted_count = composed != NULL ? composed->count + 1 : 2; - if (wanted_count > 255) { - xassert(composed != NULL); - -#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG - LOG_WARN("combining character overflow:"); - LOG_WARN(" base: 0x%04x", composed->chars[0]); - for (size_t i = 1; i < composed->count; i++) - LOG_WARN(" cc: 0x%04x", composed->chars[i]); - LOG_ERR(" new: 0x%04x", wc); -#endif - /* This is going to break anyway... */ - wanted_count--; - } - - xassert(wanted_count <= 255); - - /* Check if we already have a match for the entire compose chain */ - const struct composed *cc = - composed_lookup_without_collision( - term->composed, &key, - composed != NULL ? composed->chars : &(char32_t){base}, - composed != NULL ? composed->count : 1, - wc, 0); - - if (cc != NULL) { - /* We *do* have a match! */ - wc = CELL_COMB_CHARS_LO + cc->key; - width = cc->width; - goto out; - } else { - /* No match - allocate a new chain below */ - } - - if (unlikely(term->composed_count >= - (CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO))) - { - /* We reached our maximum number of allowed composed - * character chains. Fall through here and print the - * current zero-width character to the current cell */ - LOG_WARN("maximum number of composed characters reached"); - term_reset_grapheme_state(term); - goto out; - } - - /* Allocate new chain */ - struct composed *new_cc = xmalloc(sizeof(*new_cc)); - new_cc->chars = xmalloc(wanted_count * sizeof(new_cc->chars[0])); - new_cc->key = key; - new_cc->count = wanted_count; - new_cc->chars[0] = base; - new_cc->chars[wanted_count - 1] = wc; - new_cc->forced_width = 0; - - if (composed != NULL) { - memcpy(&new_cc->chars[1], &composed->chars[1], - (wanted_count - 2) * sizeof(new_cc->chars[0])); - } - - const int grapheme_width = - composed != NULL ? composed->width : base_width; - - switch (term->conf->tweak.grapheme_width_method) { - case GRAPHEME_WIDTH_MAX: - new_cc->width = max(grapheme_width, width); - break; - - case GRAPHEME_WIDTH_DOUBLE: - new_cc->width = min(grapheme_width + width, 2); - -#if defined(FOOT_GRAPHEME_CLUSTERING) - /* Handle VS-15 and VS-16 variation selectors */ - if (unlikely(grapheme_clustering && - (wc == 0xfe0e || wc == 0xfe0f) && - new_cc->count == 2)) - { - const struct emoji_vs *vs = - bsearch( - &(struct emoji_vs){.start = new_cc->chars[0]}, - emoji_vs, sizeof(emoji_vs) / sizeof(emoji_vs[0]), - sizeof(struct emoji_vs), - &emoji_vs_compare); - - if (vs != NULL) { - xassert(new_cc->chars[0] >= vs->start && - new_cc->chars[0] <= vs->end); - - /* Force a grapheme width of 1 for VS-15, and 2 for VS-16 */ - if (wc == 0xfe0e) { - if (vs->vs15) - new_cc->width = 1; - } else if (wc == 0xfe0f) { - if (vs->vs16) - new_cc->width = 2; - } - } - } -#endif - - break; - - case GRAPHEME_WIDTH_WCSWIDTH: - new_cc->width = grapheme_width + width; - break; - } - - term->composed_count++; - composed_insert(&term->composed, new_cc); - - wc = CELL_COMB_CHARS_LO + new_cc->key; - width = new_cc->width; - - xassert(wc >= CELL_COMB_CHARS_LO); - xassert(wc <= CELL_COMB_CHARS_HI); - goto out; - } - } else - term_reset_grapheme_state(term); - - -out: - if (width > 0) - term_print(term, wc, width, insert_mode_disable); + term_process_and_print_non_ascii(term, wc); } static void From 1260004330359619113aac3cb3ea1a7fe2fddb2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 26 Jan 2025 07:36:11 +0100 Subject: [PATCH 1057/1323] osc: text-sizing: implement w=0, plus optimize single-codepoint cases If there's a single codepoint in the text portion of the OSC sequence, and its calculated width matches the forced width, print it directly to the grid instead of emitting a combining character. When w=0, we split up the text string "as we normally would". Since we don't support any other text-sizing parameters, this means simply printing each codepoint to the grid. --- osc.c | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/osc.c b/osc.c index 49bdba67..f6398165 100644 --- a/osc.c +++ b/osc.c @@ -1149,7 +1149,7 @@ kitty_text_size(struct terminal *term, char *string) if (wchars == NULL) return; - int width = 0; + int forced_width = 0; char *ctx = NULL; for (char *param = strtok_r(parameters, ":", &ctx); @@ -1170,7 +1170,7 @@ kitty_text_size(struct terminal *term, char *string) unsigned long w = strtoul(value, &end, 10); if (*end == '\0' && errno == 0 && w <= 7) { - width = (int)w; + forced_width = (int)w; break; } else LOG_ERR("OSC-66: invalid 'w' value, ignoring"); @@ -1187,10 +1187,57 @@ kitty_text_size(struct terminal *term, char *string) } const size_t len = c32len(wchars); + + if (forced_width == 0) { + /* + * w=0 means we split the text up as we'd normally do... Since + * we don't support any other parameters of the text-sizing + * protocol, that means we just process the string as if it + * has been printed without this OSC. + */ + for (size_t i = 0; i < len; i++) + term_process_and_print_non_ascii(term, wchars[i]); + free(wchars); + return; + } + + size_t max_cp_width = 0; + size_t all_cp_width = 0; + + for (size_t i = 0; i < len; i++) { + const size_t cp_width = c32width(wchars[i]); + all_cp_width += cp_width; + max_cp_width = max(max_cp_width, cp_width); + } + + size_t calculated_width = 0; + switch (term->conf->tweak.grapheme_width_method) { + case GRAPHEME_WIDTH_WCSWIDTH: calculated_width = all_cp_width; break; + case GRAPHEME_WIDTH_MAX: calculated_width = max_cp_width; break; + case GRAPHEME_WIDTH_DOUBLE: calculated_width = min(max_cp_width, 2); break; + } + + const size_t width = forced_width == 0 ? calculated_width : forced_width; + + LOG_DBG("len=%zu, forced=%d, calculated=%zu, using=%zu", + len, forced_width, calculated_width, width); + + if (len == 1 && calculated_width == forced_width) { + /* + * Optimization: if there's a single codepoint, and either + * w=0, or the 'w' matches the calculated width, print + * codepoint directly instead of creating a combining + * character. + */ + term_print(term, wchars[0], width); + free(wchars); + return; + } + uint32_t key = composed_key_from_chars(wchars, len); const struct composed *composed = composed_lookup_without_collision( - term->composed, &key, wchars, len - 1, wchars[len - 1], width); + term->composed, &key, wchars, len - 1, wchars[len - 1], forced_width); if (composed == NULL) { struct composed *new_cc = xmalloc(sizeof(*new_cc)); @@ -1198,7 +1245,7 @@ kitty_text_size(struct terminal *term, char *string) new_cc->count = len; new_cc->key = key; new_cc->width = width; - new_cc->forced_width = width; + new_cc->forced_width = forced_width; term->composed_count++; composed_insert(&term->composed, new_cc); From 3998f8570caaf7d92cecc3b29b76c3351087930b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 27 Jan 2025 07:35:10 +0100 Subject: [PATCH 1058/1323] composed: codespell: infinitely --- composed.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composed.c b/composed.c index 2d9ed47d..fc7dfa00 100644 --- a/composed.c +++ b/composed.c @@ -95,7 +95,7 @@ composed_lookup_without_collision(struct composed *root, uint32_t *key, (*key)++; *key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO; - /* TODO: this will loop infinitly if the composed table is full */ + /* TODO: this will loop infinitely if the composed table is full */ } return NULL; From ed35a238d62473d77729748ca6d39ab3f3d42602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 27 Jan 2025 10:12:26 +0100 Subject: [PATCH 1059/1323] doc: ctlseq: add OSC 66 (kitty text sizing) --- doc/foot-ctlseqs.7.scd | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index f8eb1222..6c702738 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -729,7 +729,10 @@ All _OSC_ sequences begin with *\\E]*, sometimes abbreviated _OSC_. : Copy _Pd_ (base64 encoded text) to the clipboard. _Pc_ denotes the target: *c* targets the clipboard and *s* and *p* the primary selection. -| \\E] 99 ; _params_ ; _payload_ \\E\\ +| \\E] 66 ; _params_ ; text \\E\\ +: kitty +: Text sizing protocol (only 'w', width, supported) +| \\E] 99 ; _params_ ; _payload_ \\E\\ : kitty : Desktop notification; uses *desktop-notifications.command* in *foot.ini*(5). From 0f93766614bde055ce61e1b7ffc8f6b5aeef91d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 3 Feb 2025 15:30:00 +0100 Subject: [PATCH 1060/1323] osc: text-size: disable optimization The optimization prevents the forced-width to be set on the new combining character, causing issues when followed by more zero-width codepoints. --- osc.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osc.c b/osc.c index f6398165..eaf6e33e 100644 --- a/osc.c +++ b/osc.c @@ -1222,6 +1222,7 @@ kitty_text_size(struct terminal *term, char *string) LOG_DBG("len=%zu, forced=%d, calculated=%zu, using=%zu", len, forced_width, calculated_width, width); +#if 0 if (len == 1 && calculated_width == forced_width) { /* * Optimization: if there's a single codepoint, and either @@ -1233,6 +1234,7 @@ kitty_text_size(struct terminal *term, char *string) free(wchars); return; } +#endif uint32_t key = composed_key_from_chars(wchars, len); From 98402040977435b0029dbe4952c083e65eb8ef69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 3 Feb 2025 15:31:03 +0100 Subject: [PATCH 1061/1323] term: print-non-ascii: propagate existing forced-width When appending to an existing composed character, "inherit" its forced width, if set. Also make sure to actually _use_ the forced width, if set, rather than the calculated width. This fixes an issue when appending zero-width codepoints to a forced-width combining character. --- terminal.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/terminal.c b/terminal.c index 96a215ba..c8e49663 100644 --- a/terminal.c +++ b/terminal.c @@ -4255,7 +4255,7 @@ term_process_and_print_non_ascii(struct terminal *term, char32_t wc) new_cc->count = wanted_count; new_cc->chars[0] = base; new_cc->chars[wanted_count - 1] = wc; - new_cc->forced_width = 0; + new_cc->forced_width = composed != NULL ? composed->forced_width : 0; if (composed != NULL) { memcpy(&new_cc->chars[1], &composed->chars[1], @@ -4313,7 +4313,7 @@ term_process_and_print_non_ascii(struct terminal *term, char32_t wc) composed_insert(&term->composed, new_cc); wc = CELL_COMB_CHARS_LO + new_cc->key; - width = new_cc->width; + width = new_cc->forced_width > 0 ? new_cc->forced_width : new_cc->width; xassert(wc >= CELL_COMB_CHARS_LO); xassert(wc <= CELL_COMB_CHARS_HI); From d7e8f29ee24365a5aeeca4460afa228a32308e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 5 Feb 2025 11:36:53 +0100 Subject: [PATCH 1062/1323] grid: reflow: get number of spacers to insert from the old grid When checking if we're breaking in the middle of a multi-column character, we counted spacers starting from the break point. But, the character may be wider than that. Use the fact that the spacers cells encode how many *more* there are after them; when we get to the first one, we know exactly how wide the character is. --- grid.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grid.c b/grid.c index eb24869d..2dc4fcd5 100644 --- a/grid.c +++ b/grid.c @@ -1056,8 +1056,8 @@ grid_resize_and_reflow( from + amount < old_cols && old_row->cells[from + amount].wc >= CELL_SPACER + 1)) { + spacers = old_row->cells[from + amount].wc - CELL_SPACER + 1; amount--; - spacers++; } xassert( From a3a404a2570b72636da0583492d9cf87b700ef6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 5 Feb 2025 11:38:29 +0100 Subject: [PATCH 1063/1323] render: resize: note why min_cols=7 --- render.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.c b/render.c index 13e9d708..9ff9c681 100644 --- a/render.c +++ b/render.c @@ -4409,7 +4409,7 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) } /* Don't shrink grid too much */ - const int min_cols = 7; + const int min_cols = 7; /* See OSC-66 */ const int min_rows = 1; /* Minimum window size (must be divisible by the scaling factor)*/ From 8d20b82721ac95cea89a3ec87c8ec32e00f224e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 6 Feb 2025 14:02:04 +0100 Subject: [PATCH 1064/1323] changelog: text-sizing protocol --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9b89bf3..c707d5a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,8 +65,9 @@ * Support for the new Wayland protocol `xdg-system-bell-v1` protocol (added in wayland-protocols 1.38), via the new config option `bell.system=no|yes` (defaults to `yes`). -* Added support for custom regex matching ([#1386][1386], +* Support for custom regex matching ([#1386][1386], [#1872][1872]) +* Support for kitty's text-sizing protocol (`w`, width, only), OSC-66. [1386]: https://codeberg.org/dnkl/foot/issues/1386 [1872]: https://codeberg.org/dnkl/foot/issues/1872 From 325086291b1e2c31322780377abb0759e9870489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 10 Feb 2025 07:43:52 +0100 Subject: [PATCH 1065/1323] config: regex: fix invalid free Zero-initialize the 'launch' spawn template before calling value_to_spawn_template(). This is needed since value_to_spawn_template() tries to free the old value before assigning the new one. Closes #1951 --- config.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.c b/config.c index 604c0a76..7a12cb1a 100644 --- a/config.c +++ b/config.c @@ -1327,7 +1327,7 @@ parse_section_regex(struct context *ctx) } else if (streq(key, "launch")) { - struct config_spawn_template launch; + struct config_spawn_template launch = {NULL}; if (!value_to_spawn_template(ctx, &launch)) return false; From 4e5ad6e013666e4724b4de4273ac445adceb0865 Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger <aclopte@gmail.com> Date: Sun, 9 Feb 2025 09:11:27 +0100 Subject: [PATCH 1066/1323] Fix URL detection regression on lines with NUL bytes Commit 859b4c89 (url-mode: wip: more work on regex matching, 2025-01-30) regressed URL detection in weechat. Some of the URLs still work but others don't. This is because regexec() stops at the first NUL, thus skipping the rest of the line. weechat seems create NUL cells between their UI widgets. Work around this by replacing NUL with space. This is probably correct because selecting and copying those cells also translates to space (not sure where in the code). --- url-mode.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/url-mode.c b/url-mode.c index 0101de19..6fd7f03a 100644 --- a/url-mode.c +++ b/url-mode.c @@ -368,7 +368,8 @@ regex_detected(const struct terminal *term, enum url_action action, vline->sz = new_size; } - vline->utf8[vline->len + j] = buf[j]; + vline->utf8[vline->len + j] = + (buf[j] == '\0') ? ' ' : buf[j]; vline->map[vline->len + j] = (struct coord){c, term->grid->view + r}; } From 98db9658136cfc41b496b005c9df537f59aed6b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 10 Feb 2025 08:54:42 +0100 Subject: [PATCH 1067/1323] url-mode: terminate last virtual line before regex matching If the last line doesn't have a hard linebreak, it was never NULL terminated, causing regexec() to crash on an out-of-bounds access. --- url-mode.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/url-mode.c b/url-mode.c index 6fd7f03a..00d38d75 100644 --- a/url-mode.c +++ b/url-mode.c @@ -388,10 +388,14 @@ regex_detected(const struct terminal *term, enum url_action action, } } + /* Terminate the last line, if necessary */ + if (vline->len > 0 && vline->utf8[vline->len - 1] != '\0') + vline->utf8[vline->len++] = '\0'; + for (size_t i = 0; i < ALEN(vlines); i++) { const struct vline *v = &vlines[i]; if (v->utf8 == NULL) - continue;; + continue; const char *search_string = v->utf8; while (true) { From 26acf41d1391afbded51137e02113c5ef9ab1edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 10 Feb 2025 09:08:14 +0100 Subject: [PATCH 1068/1323] grid: pull in misc.h when TIME_REFLOW is defined --- grid.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/grid.c b/grid.c index 2dc4fcd5..6e52464c 100644 --- a/grid.c +++ b/grid.c @@ -16,6 +16,10 @@ #define TIME_REFLOW 0 +#if defined(TIME_REFLOW) +#include "misc.h" +#endif + /* * "sb" (scrollback relative) coordinates * From fce755aafe3f5fabc417992378c81ea86c22f698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 10 Feb 2025 12:58:35 +0100 Subject: [PATCH 1069/1323] forgejo: better names for templates --- .forgejo/issue_template/{issue_template.yml => bug.yml} | 0 .../issue_template/{issue_template.yaml => feature_request.yml} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename .forgejo/issue_template/{issue_template.yml => bug.yml} (100%) rename .forgejo/issue_template/{issue_template.yaml => feature_request.yml} (100%) diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/bug.yml similarity index 100% rename from .forgejo/issue_template/issue_template.yml rename to .forgejo/issue_template/bug.yml diff --git a/.forgejo/issue_template/issue_template.yaml b/.forgejo/issue_template/feature_request.yml similarity index 100% rename from .forgejo/issue_template/issue_template.yaml rename to .forgejo/issue_template/feature_request.yml From 970d95c5a1e15a6db397984fce5735239fb6fc23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 10 Feb 2025 13:08:33 +0100 Subject: [PATCH 1070/1323] doc: foot.ini: fix 'hashes' regex example It's A-F, not A-f --- doc/foot.ini.5.scd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 61216c75..b38189b1 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -806,7 +806,7 @@ commit hashes) could look like: ``` [regex:hashes] -regex=([a-fA-f0-9]{7,128}) +regex=([a-fA-F0-9]{7,128}) launch=path-to-script-or-application ${match} [key-bindings] @@ -1280,7 +1280,7 @@ e.g. *search-start=none*. ``` [regex:hashes] - regex=([a-fA-f0-9]{7,128}) + regex=([a-fA-F0-9]{7,128}) launch=path-to-script-or-application ${match} [key-bindings] From c63202ee0ee45182ec5ab440262e63b85104e59e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 10 Feb 2025 13:09:07 +0100 Subject: [PATCH 1071/1323] url-mode: regex: don't try to NULL-terminate an invalid vline --- url-mode.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/url-mode.c b/url-mode.c index 00d38d75..f04550f8 100644 --- a/url-mode.c +++ b/url-mode.c @@ -389,8 +389,11 @@ regex_detected(const struct terminal *term, enum url_action action, } /* Terminate the last line, if necessary */ - if (vline->len > 0 && vline->utf8[vline->len - 1] != '\0') + if (vline_idx < ALEN(vlines) && + vline->len > 0 && vline->utf8[vline->len - 1] != '\0') + { vline->utf8[vline->len++] = '\0'; + } for (size_t i = 0; i < ALEN(vlines); i++) { const struct vline *v = &vlines[i]; From 3d66db63ccb1baab2d78e6d104a9c15d3f6684d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 10 Feb 2025 08:57:51 +0100 Subject: [PATCH 1072/1323] grid: refactor reflow We've been trying to performance optimize reflow by "chunking" cells; try to gather as many as possible, and memcpy a chunk at once. The problem is that a) this quickly becomes very complex, and b) is very hard to get right for multi-column characters, especially when we need to truncate long ones due to the window being too small. Refactor, and once again walk and copy all cells one by one. This is slower, but at least it's correct. --- grid.c | 231 ++++++++++++++++++++----------------------------------- grid.h | 2 +- render.c | 6 +- 3 files changed, 88 insertions(+), 151 deletions(-) diff --git a/grid.c b/grid.c index 6e52464c..0e15a151 100644 --- a/grid.c +++ b/grid.c @@ -816,7 +816,7 @@ tp_cmp(const void *_a, const void *_b) void grid_resize_and_reflow( - struct grid *grid, int new_rows, int new_cols, + struct grid *grid, const struct terminal *term, int new_rows, int new_cols, int old_screen_rows, int new_screen_rows, size_t tracking_points_count, struct coord *const _tracking_points[static tracking_points_count]) @@ -960,7 +960,7 @@ grid_resize_and_reflow( /* Does this row have any URIs? */ struct row_range *uri_range, *uri_range_terminator; struct row_range *underline_range, *underline_range_terminator; - struct row_data *extra = old_row->extra; + const struct row_data *extra = old_row->extra; if (extra != NULL && extra->uri_ranges.count > 0) { uri_range = &extra->uri_ranges.v[0]; @@ -984,181 +984,118 @@ grid_resize_and_reflow( } else underline_range = underline_range_terminator = NULL; - for (int start = 0, left = col_count; left > 0;) { - int end; - bool tp_break = false; - bool uri_break = false; - bool underline_break = false; - bool ftcs_break = false; + for (int c = 0; c < col_count; c++) { + const struct cell *old = &old_row->cells[c]; - /* Figure out where to end this chunk */ - { - const int uri_col = uri_range != uri_range_terminator - ? ((uri_range->start >= start ? uri_range->start : uri_range->end) + 1) - : INT_MAX; - const int underline_col = underline_range != underline_range_terminator - ? ((underline_range->start >= start ? underline_range->start : underline_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; + /* Row full, emit newline and get a new, fresh, row */ + xassert(new_col_idx <= new_cols); + if (unlikely(new_col_idx >= new_cols)) + line_wrap(); - end = min(col_count, min(min(tp_col, min(uri_col, underline_col)), ftcs_col)); + char32_t wc = old->wc; + int width = 1; - uri_break = end == uri_col; - underline_break = end == underline_col; - tp_break = end == tp_col; - ftcs_break = end == ftcs_col; + if (unlikely(wc >= CELL_COMB_CHARS_LO && wc <= CELL_COMB_CHARS_HI)) { + const struct composed *composed = + composed_lookup(term->composed, wc - CELL_COMB_CHARS_LO); + + width = composed->forced_width > 0 ? composed->forced_width : composed->width; + } else if (unlikely(c + 1 < col_count && (old + 1)->wc >= CELL_SPACER + 1)) { + /* Wide character, get its width from the next cell's + SPACER value */ + width = (old + 1)->wc - CELL_SPACER + 1; } - int cols = end - start; - xassert(cols > 0); - xassert(start + cols <= old_cols); - /* - * Copy the row chunk to the new grid. Note that there may - * be fewer cells left on the new row than what we have in - * the chunk. I.e. the chunk may have to be split up into - * multiple memcpy:ies. - */ - - for (int count = cols, from = start; count > 0;) { - xassert(new_col_idx <= new_cols); - int new_row_cells_left = new_cols - new_col_idx; - - /* Row full, emit newline and get a new, fresh, row */ - if (new_row_cells_left <= 0) { - line_wrap(); - new_row_cells_left = new_cols; - } - - /* Number of cells we can copy */ - int amount = min(count, new_row_cells_left); - 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 - * multi-column character. - */ - int spacers = 0; - if (new_col_idx + amount >= new_cols) { - /* - * While the cell *after* the last cell is a CELL_SPACER - * - * This means we have a multi-column character - * that doesn't fit on the current row. We need to - * push it to the next row, and insert CELL_SPACER - * cells as padding. - */ - while ( - unlikely( - amount > 0 && - from + amount < old_cols && - old_row->cells[from + amount].wc >= CELL_SPACER + 1)) - { - spacers = old_row->cells[from + amount].wc - CELL_SPACER + 1; - amount--; - } - - xassert( - amount <= 1 || - old_row->cells[from + amount - 1].wc <= CELL_SPACER + 1); - } - - xassert(new_col_idx + amount <= new_cols); - xassert(from + amount <= old_cols); - - if (from == 0) - new_row->shell_integration.prompt_marker = old_row->shell_integration.prompt_marker; - - memcpy( - &new_row->cells[new_col_idx], &old_row->cells[from], - amount * sizeof(struct cell)); - - count -= amount; - from += amount; - new_col_idx += amount; - - xassert(new_col_idx <= new_cols); - - if (unlikely(spacers > 0)) { - xassert(new_col_idx + spacers == new_cols); - - for (int i = 0; i < spacers; i++, new_col_idx++) { - new_row->cells[new_col_idx].wc = CELL_SPACER; - new_row->cells[new_col_idx].attrs = (struct attributes){0}; - } + * Check if character fits, if not, emit spacers, and push + the character to the next row */ + if (unlikely(new_col_idx + width > new_cols && width <= new_cols)) { + for (; new_col_idx < new_cols; new_col_idx++) { + new_row->cells[new_col_idx].wc = CELL_SPACER; + new_row->cells[new_col_idx].attrs = (struct attributes){0}; } + line_wrap(); } - xassert(new_col_idx > 0); + if (unlikely(c == 0)) + new_row->shell_integration.prompt_marker = old_row->shell_integration.prompt_marker; - if (tp_break) { - do { - xassert(tp != NULL); - xassert(tp->row == old_row_idx); - xassert(tp->col == end - 1); + new_row->cells[new_col_idx] = *old; - tp->row = new_row_idx; - tp->col = new_col_idx - 1; - - next_tp++; - tp = *next_tp; - } while (tp->row == old_row_idx && tp->col == end - 1); - - if (tp->row != old_row_idx) - tp = NULL; - - LOG_DBG("next TP (tp=%p): %dx%d", - (void*)tp, (*next_tp)->row, (*next_tp)->col); - } - - if (uri_break) { - xassert(uri_range != NULL); - - if (uri_range->start == end - 1) + if (unlikely(uri_range != uri_range_terminator)) { + if (uri_range->start == c) { reflow_range_start( - uri_range, ROW_RANGE_URI, new_row, new_col_idx - 1); + uri_range, ROW_RANGE_URI, new_row, new_col_idx); + } - if (uri_range->end == end - 1) { + if (uri_range->end == c) { reflow_range_end( - uri_range, ROW_RANGE_URI, new_row, new_col_idx - 1); + uri_range, ROW_RANGE_URI, new_row, new_col_idx); grid_row_uri_range_destroy(uri_range); uri_range++; } } - if (underline_break) { - xassert(underline_range != NULL); - - if (underline_range->start == end - 1) + if (unlikely(underline_range != underline_range_terminator)) { + if (underline_range->start == c) { reflow_range_start( - underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx - 1); + underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx); + } - if (underline_range->end == end - 1) { + if (underline_range->end == c) { reflow_range_end( - underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx - 1); + underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx); grid_row_underline_range_destroy(underline_range); underline_range++; } } - if (ftcs_break) { - xassert(old_row->shell_integration.cmd_start == start + cols - 1 || - old_row->shell_integration.cmd_end == start + cols - 1); + if (unlikely(tp != NULL)) { + if (tp->col == c) { + do { + xassert(tp->row == old_row_idx); - 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; + tp->row = new_row_idx; + tp->col = new_col_idx; + + next_tp++; + tp = *next_tp; + } while (tp->row == old_row_idx && tp->col == c); + + if (tp->row != old_row_idx) + tp = NULL; + + LOG_DBG("next TP (tp=%p): %dx%d", + (void*)tp, (*next_tp)->row, (*next_tp)->col); + } } - left -= cols; - start += cols; + if (unlikely(old_row->shell_integration.cmd_start >= 0)) { + if (old_row->shell_integration.cmd_start == c) { + new_row->shell_integration.cmd_start = new_col_idx; + } else if (old_row->shell_integration.cmd_end == c) { + new_row->shell_integration.cmd_end = new_col_idx; + } + } + + new_col_idx++; + + if (unlikely(width > 1)) { + if (unlikely(width > new_cols)) { + /* Wide character no longer fits on a row, replace + it with a single space */ + new_row->cells[new_col_idx - 1].wc = 0; + + /* Walk past the SPACER cells */ + for (int i = 1; i < width; i++, c++, old++) + ; + } else { + /* Copy spacers */ + xassert(new_col_idx + width - 1 <= new_cols); + for (int i = 1; i < width; i++, c++) + new_row->cells[new_col_idx++] = *(++old); + } + } } if (old_row->linebreak) { diff --git a/grid.h b/grid.h index de8f98ab..71bdc29e 100644 --- a/grid.h +++ b/grid.h @@ -16,7 +16,7 @@ void grid_resize_without_reflow( int old_screen_rows, int new_screen_rows); void grid_resize_and_reflow( - struct grid *grid, int new_rows, int new_cols, + struct grid *grid, const struct terminal *term, int new_rows, int new_cols, int old_screen_rows, int new_screen_rows, size_t tracking_points_count, struct coord *const _tracking_points[static tracking_points_count]); diff --git a/render.c b/render.c index 9ff9c681..3c09f7bb 100644 --- a/render.c +++ b/render.c @@ -4190,7 +4190,7 @@ delayed_reflow_of_normal_grid(struct terminal *term) /* Reflow the original (since before the resize was started) grid, * to the *current* dimensions */ grid_resize_and_reflow( - term->interactive_resizing.grid, + term->interactive_resizing.grid, term, term->interactive_resizing.new_rows, term->normal.num_cols, term->interactive_resizing.old_screen_rows, term->rows, term->selection.coords.end.row >= 0 ? ALEN(tracking_points) : 0, @@ -4409,7 +4409,7 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) } /* Don't shrink grid too much */ - const int min_cols = 7; /* See OSC-66 */ + const int min_cols = 2; const int min_rows = 1; /* Minimum window size (must be divisible by the scaling factor)*/ @@ -4691,7 +4691,7 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) }; grid_resize_and_reflow( - &term->normal, new_normal_grid_rows, new_cols, old_normal_rows, new_rows, + &term->normal, term, new_normal_grid_rows, new_cols, old_normal_rows, new_rows, term->selection.coords.end.row >= 0 ? ALEN(tracking_points) : 0, tracking_points); } From 6a181c9f72e16b629e99dead328bcd3eb10c044d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 10 Feb 2025 12:00:51 +0100 Subject: [PATCH 1073/1323] grid: performance: check for non-NULL before comparing with terminator This should be slightly faster in the normal(?) case (no styled underlines or OSC-8). --- grid.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grid.c b/grid.c index 0e15a151..5888cdc1 100644 --- a/grid.c +++ b/grid.c @@ -1022,7 +1022,7 @@ grid_resize_and_reflow( new_row->cells[new_col_idx] = *old; - if (unlikely(uri_range != uri_range_terminator)) { + if (unlikely(uri_range != NULL && uri_range != uri_range_terminator)) { if (uri_range->start == c) { reflow_range_start( uri_range, ROW_RANGE_URI, new_row, new_col_idx); @@ -1036,7 +1036,7 @@ grid_resize_and_reflow( } } - if (unlikely(underline_range != underline_range_terminator)) { + if (unlikely(underline_range != NULL && underline_range != underline_range_terminator)) { if (underline_range->start == c) { reflow_range_start( underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx); From eced7cf1d6e5ea8cc1f569d022ddea938cd907a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 10 Feb 2025 12:38:11 +0100 Subject: [PATCH 1074/1323] grid: reflow: don't special case the first cell in a multi-column character Wrap *all* cell copying logic in a for-loop for the characters width. This _should_, in theory, mean reflow of e.g. cursor coordinates in the middle of a multi-column character works correctly. Also fix reflow of cmd start/end integration. --- grid.c | 118 +++++++++++++++++++++++++++++---------------------------- 1 file changed, 60 insertions(+), 58 deletions(-) diff --git a/grid.c b/grid.c index 5888cdc1..2b537ba9 100644 --- a/grid.c +++ b/grid.c @@ -984,7 +984,7 @@ grid_resize_and_reflow( } else underline_range = underline_range_terminator = NULL; - for (int c = 0; c < col_count; c++) { + for (int c = 0; c < col_count;) { const struct cell *old = &old_row->cells[c]; /* Row full, emit newline and get a new, fresh, row */ @@ -1017,84 +1017,86 @@ grid_resize_and_reflow( line_wrap(); } - if (unlikely(c == 0)) - new_row->shell_integration.prompt_marker = old_row->shell_integration.prompt_marker; + new_row->shell_integration.prompt_marker = old_row->shell_integration.prompt_marker; - new_row->cells[new_col_idx] = *old; + for (int i = 0; i < width; i++) { + if (unlikely(uri_range != NULL && uri_range != uri_range_terminator)) { + if (unlikely(uri_range->start == c)) { + reflow_range_start( + uri_range, ROW_RANGE_URI, new_row, new_col_idx); + } - if (unlikely(uri_range != NULL && uri_range != uri_range_terminator)) { - if (uri_range->start == c) { - reflow_range_start( - uri_range, ROW_RANGE_URI, new_row, new_col_idx); + if (unlikely(uri_range->end == c)) { + reflow_range_end( + uri_range, ROW_RANGE_URI, new_row, new_col_idx); + grid_row_uri_range_destroy(uri_range); + uri_range++; + } } - if (uri_range->end == c) { - reflow_range_end( - uri_range, ROW_RANGE_URI, new_row, new_col_idx); - grid_row_uri_range_destroy(uri_range); - uri_range++; - } - } + if (unlikely(underline_range != NULL && underline_range != underline_range_terminator)) { + if (unlikely(underline_range->start == c)) { + reflow_range_start( + underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx); + } - if (unlikely(underline_range != NULL && underline_range != underline_range_terminator)) { - if (underline_range->start == c) { - reflow_range_start( - underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx); + if (unlikely(underline_range->end == c)) { + reflow_range_end( + underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx); + grid_row_underline_range_destroy(underline_range); + underline_range++; + } } - if (underline_range->end == c) { - reflow_range_end( - underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx); - grid_row_underline_range_destroy(underline_range); - underline_range++; + if (unlikely(tp != NULL)) { + if (unlikely(tp->col == c)) { + do { + xassert(tp->row == old_row_idx); + + tp->row = new_row_idx; + tp->col = new_col_idx; + + next_tp++; + tp = *next_tp; + } while (tp->row == old_row_idx && tp->col == c); + + if (tp->row != old_row_idx) + tp = NULL; + + LOG_DBG("next TP (tp=%p): %dx%d", + (void*)tp, (*next_tp)->row, (*next_tp)->col); + } } - } - if (unlikely(tp != NULL)) { - if (tp->col == c) { - do { - xassert(tp->row == old_row_idx); - - tp->row = new_row_idx; - tp->col = new_col_idx; - - next_tp++; - tp = *next_tp; - } while (tp->row == old_row_idx && tp->col == c); - - if (tp->row != old_row_idx) - tp = NULL; - - LOG_DBG("next TP (tp=%p): %dx%d", - (void*)tp, (*next_tp)->row, (*next_tp)->col); - } - } - - if (unlikely(old_row->shell_integration.cmd_start >= 0)) { - if (old_row->shell_integration.cmd_start == c) { + if (unlikely(old_row->shell_integration.cmd_start >= 0 && + old_row->shell_integration.cmd_start == c)) + { new_row->shell_integration.cmd_start = new_col_idx; - } else if (old_row->shell_integration.cmd_end == c) { + } + + if (unlikely(old_row->shell_integration.cmd_end >= 0 && + old_row->shell_integration.cmd_end == c)) + { new_row->shell_integration.cmd_end = new_col_idx; } - } - new_col_idx++; - - if (unlikely(width > 1)) { if (unlikely(width > new_cols)) { /* Wide character no longer fits on a row, replace it with a single space */ - new_row->cells[new_col_idx - 1].wc = 0; + new_row->cells[new_col_idx++].wc = 0; + c++; /* Walk past the SPACER cells */ for (int i = 1; i < width; i++, c++, old++) ; - } else { - /* Copy spacers */ - xassert(new_col_idx + width - 1 <= new_cols); - for (int i = 1; i < width; i++, c++) - new_row->cells[new_col_idx++] = *(++old); + + /* Continue with next character in the *old* grid */ + break; } + + new_row->cells[new_col_idx++] = *old; + old++; + c++; } } From 8b63869f5707a771e60ae745d1069860c9056cc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 10 Feb 2025 12:42:29 +0100 Subject: [PATCH 1075/1323] render: minimum window size: 2 cols -> 1 col --- render.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.c b/render.c index 3c09f7bb..cf4f303e 100644 --- a/render.c +++ b/render.c @@ -4409,7 +4409,7 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) } /* Don't shrink grid too much */ - const int min_cols = 2; + const int min_cols = 1; const int min_rows = 1; /* Minimum window size (must be divisible by the scaling factor)*/ From 7445471238bd5b78cdfce82b0e49f5bf876a0c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 10 Feb 2025 12:46:31 +0100 Subject: [PATCH 1076/1323] grid: reflow: shell integration: no need to check for >= 0 --- grid.c | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/grid.c b/grid.c index 2b537ba9..ceaeb230 100644 --- a/grid.c +++ b/grid.c @@ -1068,17 +1068,11 @@ grid_resize_and_reflow( } } - if (unlikely(old_row->shell_integration.cmd_start >= 0 && - old_row->shell_integration.cmd_start == c)) - { + if (unlikely(old_row->shell_integration.cmd_start == c)) new_row->shell_integration.cmd_start = new_col_idx; - } - if (unlikely(old_row->shell_integration.cmd_end >= 0 && - old_row->shell_integration.cmd_end == c)) - { + if (unlikely(old_row->shell_integration.cmd_end == c)) new_row->shell_integration.cmd_end = new_col_idx; - } if (unlikely(width > new_cols)) { /* Wide character no longer fits on a row, replace From 888a6770da4f9f1c138b5d0ed6ec9b1d577337ca Mon Sep 17 00:00:00 2001 From: Ludovico Gerardi <ludovico.gerardi@posteo.it> Date: Thu, 6 Feb 2025 10:13:25 +0100 Subject: [PATCH 1077/1323] themes: update Tokyo Night Light --- CHANGELOG.md | 1 + themes/tokyonight-day | 21 --------------------- themes/tokyonight-light | 25 +++++++++++++++++++++++++ 3 files changed, 26 insertions(+), 21 deletions(-) delete mode 100644 themes/tokyonight-day create mode 100644 themes/tokyonight-light diff --git a/CHANGELOG.md b/CHANGELOG.md index c707d5a6..4b63bdc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,7 @@ ([#1925][1925]). * Auto-detection of URLs (i.e. not OSC-8 based URLs) are now regex based. +* Rename Tokyo Night Day theme to Tokyo Night Light and update colors. [1925]: https://codeberg.org/dnkl/foot/issues/1925 diff --git a/themes/tokyonight-day b/themes/tokyonight-day deleted file mode 100644 index 5143aa07..00000000 --- a/themes/tokyonight-day +++ /dev/null @@ -1,21 +0,0 @@ -# -*- conf -*- - -[colors] -background=e1e2e7 -foreground=3760bf -regular0=e9e9ed -regular1=f52a65 -regular2=587539 -regular3=8c6c3e -regular4=2e7de9 -regular5=9854f1 -regular6=007197 -regular7=6172b0 -bright0=a1a6c5 -bright1=f52a65 -bright2=587539 -bright3=8c6c3e -bright4=2e7de9 -bright5=9854f1 -bright6=007197 -bright7=3760bf \ No newline at end of file diff --git a/themes/tokyonight-light b/themes/tokyonight-light new file mode 100644 index 00000000..ffcae689 --- /dev/null +++ b/themes/tokyonight-light @@ -0,0 +1,25 @@ +# -*- conf -*- + +# Reference: https://github.com/tokyo-night/tokyo-night-vscode-theme/blob/master/themes/tokyo-night-light-color-theme.json + +[colors] +background=d6d8df +foreground=343b58 +regular0=343b58 +regular1=8c4351 +regular2=33635c +regular3=8f5e15 +regular4=2959aa +regular5=7b43ba +regular6=006c86 +regular7=707280 +bright0=343b58 +bright1=8c4351 +bright2=33635c +bright3=8f5e15 +bright4=2959aa +bright5=7b43ba +bright6=006c86 +bright7=707280 + +jump-labels=343b58 e19d37 # brighter yellow than regular3 From d7a4f9e99e7e3517d339df53eee3fe6b9677f9c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 13 Feb 2025 08:00:50 +0100 Subject: [PATCH 1078/1323] grid: reflow: fix cursor reflow when LCF is set When the cursor is at the end of the line, with a pending wrap (LCF set), the lcf flag should be cleared *and* the cursor moved one cell to the right. Before this patch, we cleared LCF, but didn't move the cursor. Closes #1954 --- CHANGELOG.md | 3 +++ grid.c | 17 ++++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b63bdc8..8a090062 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -110,10 +110,13 @@ * Combining characters (including emojis consisting of multiple codepoints) not being handled correctly when _insert mode_ is enabled ([#1947][1947]). +* Reflow of the cursor (active + saved) when at the end of the line + with a pending wrap (LCF set) ([#1954][1954]). [1918]: https://codeberg.org/dnkl/foot/issues/1918 [1929]: https://codeberg.org/dnkl/foot/issues/1929 [1947]: https://codeberg.org/dnkl/foot/issues/1947 +[1954]: https://codeberg.org/dnkl/foot/issues/1954 ### Security diff --git a/grid.c b/grid.c index ceaeb230..2deb9111 100644 --- a/grid.c +++ b/grid.c @@ -1211,15 +1211,26 @@ grid_resize_and_reflow( saved_cursor.row = min(saved_cursor.row, new_screen_rows - 1); saved_cursor.col = min(saved_cursor.col, new_cols - 1); + if (grid->cursor.lcf) { + if (cursor.col + 1 < new_cols) { + cursor.col++; + grid->cursor.lcf = false; + } + } + + if (grid->saved_cursor.lcf) { + if (saved_cursor.col + 1 < new_cols) { + saved_cursor.col++; + grid->saved_cursor.lcf = false; + } + } + grid->cur_row = new_grid[(grid->offset + cursor.row) & (new_rows - 1)]; xassert(grid->cur_row != NULL); grid->cursor.point = cursor; grid->saved_cursor.point = saved_cursor; - grid->cursor.lcf = false; - grid->saved_cursor.lcf = false; - /* Free sixels we failed to "map" to the new grid */ tll_foreach(untranslated_sixels, it) sixel_destroy(&it->item); From 4abbaf134535da823c45949bbe1f3904caf5e085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 16 Feb 2025 09:11:52 +0100 Subject: [PATCH 1079/1323] doc: foot.ini: font: add one more fontfeatures example Add a fontfeatures example where we: * set multiple features * assign a value to the features (as opposed to just enabling a boolean feature) --- doc/foot.ini.5.scd | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index b38189b1..87673233 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -58,9 +58,16 @@ empty string to be set, but it must be quoted: *KEY=""*) - Dina:weight=bold:slant=italic - Courier New:size=12 - Fantasque Sans Mono:fontfeatures=ss01 + - Iosevka:fontfeatures=cv01=1:fontfeatures=cv06=1 - Meslo LG S:size=12, Noto Color Emoji:size=12 - Courier New:pixelsize=8 + Be aware that, depending on your setup, there may be global + FontConfig options that overrides options set here. If an option + appears to have no effect, ensure there is no global configuration + file that sets the same option with *assign* or *assign_replace*; + use one of the many *append* or possibly *prepend* modes. + For each option, the first font is the primary font. The remaining fonts are fallback fonts that will be used whenever a glyph cannot be found in the primary font. From 76503fb86a8b8a6b5c3ce1be87c15a55af38508d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 16 Feb 2025 07:25:25 +0100 Subject: [PATCH 1080/1323] term: append zero-width grapheme breaking characters to previous cell When compiled with grapheme clustering support, zero-width characters that also are grapheme breaks, were ignored (not stored in the grid). When utf8proc says the character is a grapheme break, we try to print the character to the current cell. But this is only done when width > 0. As a result, zero width grapheme breaks were simply discarded. This only happens when grapheme clustering is enabled; when disabled, all zero width characters are appended. Fix this by also requiring the width to be non-zero when if we should append the character or not. Closes #1960 --- CHANGELOG.md | 4 ++++ terminal.c | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a090062..7cc5e13f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,11 +112,15 @@ enabled ([#1947][1947]). * Reflow of the cursor (active + saved) when at the end of the line with a pending wrap (LCF set) ([#1954][1954]). +* Zero-width characters that also are grapheme breaks (e.g. U+200B, + ZERO WIDTH SPACE) being ignored (discarded and never stored in the + grid) ([#1960][1960]). [1918]: https://codeberg.org/dnkl/foot/issues/1918 [1929]: https://codeberg.org/dnkl/foot/issues/1929 [1947]: https://codeberg.org/dnkl/foot/issues/1947 [1954]: https://codeberg.org/dnkl/foot/issues/1954 +[1960]: https://codeberg.org/dnkl/foot/issues/1960 ### Security diff --git a/terminal.c b/terminal.c index c8e49663..2e868793 100644 --- a/terminal.c +++ b/terminal.c @@ -4153,7 +4153,7 @@ term_process_and_print_non_ascii(struct terminal *term, char32_t wc) if (grapheme_clustering) { /* Check if we're on a grapheme cluster break */ if (utf8proc_grapheme_break_stateful( - last, wc, &term->vt.grapheme_state)) + last, wc, &term->vt.grapheme_state) && width > 0) { term_reset_grapheme_state(term); goto out; From d66a00678d434afbdf31dbaad61b9837983ecc6e Mon Sep 17 00:00:00 2001 From: Guillaume Outters <guillaume-github@outters.eu> Date: Thu, 13 Feb 2025 16:16:43 +0100 Subject: [PATCH 1081/1323] server: fix --server=<fd> on OSes returning SO_ACCEPTCONN > 1 Closes #1956 --- server.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server.c b/server.c index 5981a14c..22dd473b 100644 --- a/server.c +++ b/server.c @@ -4,6 +4,7 @@ #include <fcntl.h> #include <unistd.h> #include <errno.h> +#include <limits.h> #include <sys/types.h> #include <sys/socket.h> @@ -23,6 +24,8 @@ #include "wayland.h" #include "xmalloc.h" +#define NON_ZERO_OPT (INT_MIN / 7) + struct client; struct terminal_instance; @@ -474,7 +477,7 @@ prepare_socket(int fd) } int const socket_options[] = { SO_DOMAIN, SO_ACCEPTCONN, SO_TYPE }; - int const socket_options_values[] = { AF_UNIX, 1, SOCK_STREAM}; + int const socket_options_values[] = { AF_UNIX, NON_ZERO_OPT, SOCK_STREAM}; char const * const socket_options_names[] = { "SO_DOMAIN", "SO_ACCEPTCONN", "SO_TYPE" }; xassert(ALEN(socket_options) == ALEN(socket_options_values)); @@ -489,6 +492,8 @@ prepare_socket(int fd) LOG_ERRNO("failed to read socket option from passed file descriptor"); return false; } + if (socket_options_values[i] == NON_ZERO_OPT && socket_option) + socket_option = NON_ZERO_OPT; if (socket_option != socket_options_values[i]) { LOG_ERR("wrong socket value for socket option '%s' on passed file descriptor", socket_options_names[i]); From ba5f4abdd47e572f88e4ccba75cd8af82f6ee507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 16 Feb 2025 13:56:43 +0100 Subject: [PATCH 1082/1323] changelog: --server=FD failing on FreeBSD --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cc5e13f..3eedcb41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,12 +115,14 @@ * Zero-width characters that also are grapheme breaks (e.g. U+200B, ZERO WIDTH SPACE) being ignored (discarded and never stored in the grid) ([#1960][1960]). +* `--server=<FD>` not working on FreeBSD ([#1956][1956]). [1918]: https://codeberg.org/dnkl/foot/issues/1918 [1929]: https://codeberg.org/dnkl/foot/issues/1929 [1947]: https://codeberg.org/dnkl/foot/issues/1947 [1954]: https://codeberg.org/dnkl/foot/issues/1954 [1960]: https://codeberg.org/dnkl/foot/issues/1960 +[1956]: https://codeberg.org/dnkl/foot/issues/1956 ### Security From 9f9ffa94348905509083904f5d542481875b1b25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 18 Feb 2025 15:09:23 +0100 Subject: [PATCH 1083/1323] term: set_app_id(): app_id may be NULL, in which case we can't do strlen() Closes #1963 --- CHANGELOG.md | 3 +++ terminal.c | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eedcb41..f4ed6459 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,6 +116,8 @@ ZERO WIDTH SPACE) being ignored (discarded and never stored in the grid) ([#1960][1960]). * `--server=<FD>` not working on FreeBSD ([#1956][1956]). +* Crash when resetting the terminal and an application had previously + set a custom app ID ([#1963][1963]) [1918]: https://codeberg.org/dnkl/foot/issues/1918 [1929]: https://codeberg.org/dnkl/foot/issues/1929 @@ -123,6 +125,7 @@ [1954]: https://codeberg.org/dnkl/foot/issues/1954 [1960]: https://codeberg.org/dnkl/foot/issues/1960 [1956]: https://codeberg.org/dnkl/foot/issues/1956 +[1963]: https://codeberg.org/dnkl/foot/issues/1963 ### Security diff --git a/terminal.c b/terminal.c index 2e868793..fd1c8937 100644 --- a/terminal.c +++ b/terminal.c @@ -3620,7 +3620,7 @@ term_set_app_id(struct terminal *term, const char *app_id) term->app_id = NULL; } - const size_t length = strlen(app_id); + const size_t length = app_id != NULL ? strlen(app_id) : 0; if (length > 2048) { /* * Not sure if there's a limit in the protocol, or the From 101bc28698177807e997951d046a12dd2c741848 Mon Sep 17 00:00:00 2001 From: Craig Barnes <craigbarnes@protonmail.com> Date: Tue, 18 Feb 2025 17:32:54 +0000 Subject: [PATCH 1084/1323] terminal: add comment/link to cursor::lcf, to clarify its purpose --- terminal.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal.h b/terminal.h index a69a8d0f..e03b7bf7 100644 --- a/terminal.h +++ b/terminal.h @@ -88,7 +88,7 @@ struct range { struct cursor { struct coord point; - bool lcf; + bool lcf; /* Last Column Flag; https://github.com/mattiase/wraptest#basic-vt-line-wrapping-rules */ }; enum damage_type {DAMAGE_SCROLL, DAMAGE_SCROLL_REVERSE, From c41008da31aba42e3d0e25966af9eab27cfd3fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 19 Feb 2025 11:44:38 +0100 Subject: [PATCH 1085/1323] config+render: allow cursor.style=hollow Closes #1965 --- CHANGELOG.md | 2 ++ config.c | 2 +- config.h | 2 +- doc/foot.ini.5.scd | 4 ++-- render.c | 5 +++++ 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4ed6459..8e9ca7d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,9 +68,11 @@ * Support for custom regex matching ([#1386][1386], [#1872][1872]) * Support for kitty's text-sizing protocol (`w`, width, only), OSC-66. +* `cursor.style` can now be set to `hollow` ([#1965][1965]). [1386]: https://codeberg.org/dnkl/foot/issues/1386 [1872]: https://codeberg.org/dnkl/foot/issues/1872 +[1965]: https://codeberg.org/dnkl/foot/issues/1965 ### Changed diff --git a/config.c b/config.c index 7a12cb1a..de2dc07f 100644 --- a/config.c +++ b/config.c @@ -1517,7 +1517,7 @@ parse_section_cursor(struct context *ctx) return value_to_enum( ctx, - (const char *[]){"block", "underline", "beam", NULL}, + (const char *[]){"block", "underline", "beam", "hollow", NULL}, (int *)&conf->cursor.style); } diff --git a/config.h b/config.h index 3535064e..deddcf04 100644 --- a/config.h +++ b/config.h @@ -28,7 +28,7 @@ struct font_size_adjustment { float percent; }; -enum cursor_style { CURSOR_BLOCK, CURSOR_UNDERLINE, CURSOR_BEAM }; +enum cursor_style { CURSOR_BLOCK, CURSOR_UNDERLINE, CURSOR_BEAM, CURSOR_HOLLOW }; enum cursor_unfocused_style { CURSOR_UNFOCUSED_UNCHANGED, CURSOR_UNFOCUSED_HOLLOW, diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 87673233..56004654 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -842,8 +842,8 @@ applications can change these at runtime. *style* Configures the default cursor style, and is one of: *block*, - *beam* or *underline*. Note that this can be overridden by - applications. Default: _block_. + *beam*, *underline* or *hollow*. Note that this can be overridden + by applications. Default: _block_. *unfocused-style* Configures how the cursor is rendered when the terminal window is diff --git a/render.c b/render.c index cf4f303e..701cefae 100644 --- a/render.c +++ b/render.c @@ -647,6 +647,11 @@ draw_cursor(const struct terminal *term, const struct cell *cell, draw_underline_cursor(term, pix, font, &cursor_color, x, y, cols); } break; + + case CURSOR_HOLLOW: + if (likely(term->cursor_blink.state == CURSOR_BLINK_ON)) + draw_hollow_block(term, pix, &cursor_color, x, y, cols); + break; } } From 4f11d6086fd9341e663fafaf2b256981af389eef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 21 Feb 2025 08:03:41 +0100 Subject: [PATCH 1086/1323] DECSCUSR+DECRQSS: treat hollow cursor as a block cursor If the user has configured cursor.style=hollow, make DECSCUSR 1/2 set the cursor to hollow rather than block, and make DECRQSS DECSCUSR respond as if cursor.style=block. The idea is to not expose the hollow variant in DECSCUSR in any way, to avoid having to extend it with custom encodings. Another way to think about it is this is just a slightly more discoverable way of doing: cursor.style=block cursor.block-cursor-is-hollow=yes --- csi.c | 3 ++- dcs.c | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/csi.c b/csi.c index b982023c..81c71e31 100644 --- a/csi.c +++ b/csi.c @@ -1756,7 +1756,8 @@ csi_dispatch(struct terminal *term, uint8_t final) case 1: /* blinking block */ case 2: /* steady block */ - term->cursor_style = CURSOR_BLOCK; + term->cursor_style = term->conf->cursor.style == CURSOR_HOLLOW + ? CURSOR_HOLLOW : CURSOR_BLOCK; break; case 3: /* blinking underline */ diff --git a/dcs.c b/dcs.c index ebea9e4c..376c73bd 100644 --- a/dcs.c +++ b/dcs.c @@ -422,6 +422,7 @@ decrqss_unhook(struct terminal *term) int mode; switch (term->cursor_style) { + case CURSOR_HOLLOW: /* FALLTHROUGH */ case CURSOR_BLOCK: mode = 2; break; case CURSOR_UNDERLINE: mode = 4; break; case CURSOR_BEAM: mode = 6; break; From 882f4b246870d525cf65bb927ec4788010150722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 2 Mar 2025 10:18:00 +0100 Subject: [PATCH 1087/1323] shm-format: add new shm formats --- shm-formats.h | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/shm-formats.h b/shm-formats.h index 3ada8266..a73ba1f2 100644 --- a/shm-formats.h +++ b/shm-formats.h @@ -117,5 +117,22 @@ static const struct shm_formats { {WL_SHM_FORMAT_ARGB16161616, "ARGB16161616"}, {WL_SHM_FORMAT_ABGR16161616, "ABGR16161616"}, #endif +#if WAYLAND_VERSION_MAJOR > 1 || WAYLAND_VERSION_MINOR >= 23 + {WL_SHM_FORMAT_C1, "C1"}, + {WL_SHM_FORMAT_C2, "C2"}, + {WL_SHM_FORMAT_C4, "C4"}, + {WL_SHM_FORMAT_D1, "D1"}, + {WL_SHM_FORMAT_D2, "D2"}, + {WL_SHM_FORMAT_D4, "D4"}, + {WL_SHM_FORMAT_D8, "D8"}, + {WL_SHM_FORMAT_R1, "R1"}, + {WL_SHM_FORMAT_R2, "R2"}, + {WL_SHM_FORMAT_R4, "R4"}, + {WL_SHM_FORMAT_R10, "R10"}, + {WL_SHM_FORMAT_R12, "R12"}, + {WL_SHM_FORMAT_AVUY8888, "AVUY8888"}, + {WL_SHM_FORMAT_XVUY8888, "XVUY8888"}, + {WL_SHM_FORMAT_P030, "P030"}, +#endif }; #endif From 9e6d334bd8d8a565a7255f91ddb4de849563dbc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 4 Mar 2025 07:50:03 +0100 Subject: [PATCH 1088/1323] term: reset the grapheme clustering state on cursor movements --- CHANGELOG.md | 1 + terminal.c | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e9ca7d1..30adad73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,6 +120,7 @@ * `--server=<FD>` not working on FreeBSD ([#1956][1956]). * Crash when resetting the terminal and an application had previously set a custom app ID ([#1963][1963]) +* Grapheme clustering state not reset on cursor movements. [1918]: https://codeberg.org/dnkl/foot/issues/1918 [1929]: https://codeberg.org/dnkl/foot/issues/1929 diff --git a/terminal.c b/terminal.c index fd1c8937..0cd4fbca 100644 --- a/terminal.c +++ b/terminal.c @@ -2860,6 +2860,8 @@ term_cursor_to(struct terminal *term, int row, int col) term->grid->cursor.point.col = col; term->grid->cursor.point.row = row; + term_reset_grapheme_state(term); + term->grid->cur_row = grid_row(term->grid, row); } @@ -2876,6 +2878,7 @@ term_cursor_col(struct terminal *term, int col) term->grid->cursor.lcf = false; term->grid->cursor.point.col = col; + term_reset_grapheme_state(term); } void @@ -2885,6 +2888,7 @@ term_cursor_left(struct terminal *term, int count) term->grid->cursor.point.col -= move_amount; xassert(term->grid->cursor.point.col >= 0); term->grid->cursor.lcf = false; + term_reset_grapheme_state(term); } void @@ -2894,6 +2898,7 @@ term_cursor_right(struct terminal *term, int count) term->grid->cursor.point.col += move_amount; xassert(term->grid->cursor.point.col < term->cols); term->grid->cursor.lcf = false; + term_reset_grapheme_state(term); } void @@ -3165,6 +3170,8 @@ term_linefeed(struct terminal *term) term_scroll(term, 1); else term_cursor_down(term, 1); + + term_reset_grapheme_state(term); } void From 6d39f66eb7c0cafb9e12be7da6831598c6dd4afb Mon Sep 17 00:00:00 2001 From: Adrian fxj9a <af24@tuta.io> Date: Mon, 3 Mar 2025 14:27:30 +0100 Subject: [PATCH 1089/1323] config: add search-bindings.delete-to-{start,end} key bindings Defaults to ctrl+u and ctrl+k respectively. Closes #1972 --- CHANGELOG.md | 4 ++++ config.c | 4 ++++ doc/foot.1.scd | 6 ++++++ doc/foot.ini.5.scd | 6 ++++++ foot.ini | 2 ++ key-binding.h | 2 ++ search.c | 23 +++++++++++++++++++++++ 7 files changed, 47 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30adad73..59d69f3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,10 +69,14 @@ [#1872][1872]) * Support for kitty's text-sizing protocol (`w`, width, only), OSC-66. * `cursor.style` can now be set to `hollow` ([#1965][1965]). +* `search-bindings.delete-to-start` and + `search-bindings.delete-to-end` key bindings, defaulting to + `Control+u` and `Control+k` respectively ([#1972][1972]). [1386]: https://codeberg.org/dnkl/foot/issues/1386 [1872]: https://codeberg.org/dnkl/foot/issues/1872 [1965]: https://codeberg.org/dnkl/foot/issues/1965 +[1972]: https://codeberg.org/dnkl/foot/issues/1972 ### Changed diff --git a/config.c b/config.c index de2dc07f..df7f0031 100644 --- a/config.c +++ b/config.c @@ -180,6 +180,8 @@ static const char *const search_binding_action_map[] = { [BIND_ACTION_SEARCH_DELETE_PREV_WORD] = "delete-prev-word", [BIND_ACTION_SEARCH_DELETE_NEXT] = "delete-next", [BIND_ACTION_SEARCH_DELETE_NEXT_WORD] = "delete-next-word", + [BIND_ACTION_SEARCH_DELETE_TO_START] = "delete-to-start", + [BIND_ACTION_SEARCH_DELETE_TO_END] = "delete-to-end", [BIND_ACTION_SEARCH_EXTEND_CHAR] = "extend-char", [BIND_ACTION_SEARCH_EXTEND_WORD] = "extend-to-word-boundary", [BIND_ACTION_SEARCH_EXTEND_WORD_WS] = "extend-to-next-whitespace", @@ -3183,6 +3185,8 @@ add_default_search_bindings(struct config *conf) {BIND_ACTION_SEARCH_DELETE_NEXT, m("none"), {{XKB_KEY_Delete}}}, {BIND_ACTION_SEARCH_DELETE_NEXT_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Delete}}}, {BIND_ACTION_SEARCH_DELETE_NEXT_WORD, m(XKB_MOD_NAME_ALT), {{XKB_KEY_d}}}, + {BIND_ACTION_SEARCH_DELETE_TO_START, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_u}}}, + {BIND_ACTION_SEARCH_DELETE_TO_END, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_k}}}, {BIND_ACTION_SEARCH_EXTEND_CHAR, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Right}}}, {BIND_ACTION_SEARCH_EXTEND_WORD, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_Right}}}, {BIND_ACTION_SEARCH_EXTEND_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_w}}}, diff --git a/doc/foot.1.scd b/doc/foot.1.scd index fad44f19..f868c12c 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -301,6 +301,12 @@ These shortcuts affect the search box in scrollback-search mode: *alt*+*delete*, *ctrl*+*delete* Deletes the **word after** the cursor. +*ctrl*+*u* + Deletes from the cursor to the start of the input + +*ctrl*+*k* + Deletes from the cursor to the end of the input + These shortcuts affect scrolling in scrollback-search mode: *shift*+*page-up* diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 56004654..c55419d1 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1401,6 +1401,12 @@ scrollback search mode. The syntax is exactly the same as the regular Deletes the **word after** the cursor. Default: _Mod1+d Control+Delete_. +*delete-to-start* + Deletes search input before the cursor. Default: _Ctrl+u_. + +*delete-to-end* + Deletes search input after the cursor. Default: _Ctrl+k_. + *extend-char* Extend current selection to the right, by one character. Default: _Shift+Right_. diff --git a/foot.ini b/foot.ini index a1aa118c..b852da07 100644 --- a/foot.ini +++ b/foot.ini @@ -225,6 +225,8 @@ # delete-prev-word=Mod1+BackSpace Control+BackSpace # delete-next=Delete # delete-next-word=Mod1+d Control+Delete +# delete-to-start=Control+u +# delete-to-end=Control+k # extend-char=Shift+Right # extend-to-word-boundary=Control+w Control+Shift+Right # extend-to-next-whitespace=Control+Shift+w diff --git a/key-binding.h b/key-binding.h index 5f5bb9d7..89398859 100644 --- a/key-binding.h +++ b/key-binding.h @@ -84,6 +84,8 @@ enum bind_action_search { BIND_ACTION_SEARCH_DELETE_PREV_WORD, BIND_ACTION_SEARCH_DELETE_NEXT, BIND_ACTION_SEARCH_DELETE_NEXT_WORD, + BIND_ACTION_SEARCH_DELETE_TO_START, + BIND_ACTION_SEARCH_DELETE_TO_END, BIND_ACTION_SEARCH_EXTEND_CHAR, BIND_ACTION_SEARCH_EXTEND_WORD, BIND_ACTION_SEARCH_EXTEND_WORD_WS, diff --git a/search.c b/search.c index 20990c87..dda84e6d 100644 --- a/search.c +++ b/search.c @@ -1265,6 +1265,29 @@ execute_binding(struct seat *seat, struct terminal *term, return true; } + case BIND_ACTION_SEARCH_DELETE_TO_START: { + if (term->search.cursor > 0) { + memmove(&term->search.buf[0], + &term->search.buf[term->search.cursor], + (term->search.len - term->search.cursor) + * sizeof(char32_t)); + + term->search.len -= term->search.cursor; + term->search.cursor = 0; + *update_search_result = *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_DELETE_TO_END: { + if (term->search.cursor < term->search.len) { + term->search.buf[term->search.cursor] = '\0'; + term->search.len = term->search.cursor; + *update_search_result = *redraw = true; + } + return true; + } + case BIND_ACTION_SEARCH_EXTEND_CHAR: { struct coord target; if (search_extend_find_char_right(term, &target)) { From ccf625b9914917e131beb83379c526bab1927edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 21 Feb 2025 11:01:29 +0100 Subject: [PATCH 1090/1323] render: gamma-correct blending This implements gamma-correct blending, which mainly affects font rendering. The implementation requires compile-time availability of the new color-management protocol (available in wayland-protocols >= 1.41), and run-time support for the same in the compositor (specifically, the EXT_LINEAR TF function and sRGB primaries). How it works: all colors are decoded from sRGB to linear (using a lookup table, generated in the exact same way pixman generates it's internal conversion tables) before being used by pixman. The resulting image buffer is thus in decoded/linear format. We use the color-management protocol to inform the compositor of this, by tagging the wayland surfaces with the 'ext_linear' image attribute. Sixes: all colors are sRGB internally, and decoded to linear before being used in any sixels. Thus, the image buffers will contain linear colors. This is important, since otherwise there would be a decode/encode penalty every time a sixel is blended to the grid. Emojis: we require fcft >= 3.2, which adds support for sRGB decoding color glyphs. Meaning, the emoji pixman surfaces can be blended directly to the grid, just like sixels. Gamma-correct blending is enabled by default *when the compositor supports it*. There's a new option to explicitly enable/disable it: gamma-correct-blending=no|yes. If set to 'yes', and the compositor does not implement the required color-management features, warning logs are emitted. There's a loss of precision when storing linear pixels in 8-bit channels. For this reason, this patch also adds supports for 10-bit surfaces. For now, this is disabled by default since such surfaces only have 2 bits for alpha. It can be enabled with tweak.surface-bit-depth=10-bit. Perhaps, in the future, we can enable it by default if: * gamma-correct blending is enabled * the user has not enabled a transparent background --- CHANGELOG.md | 6 ++ client.c | 3 +- config.c | 38 ++++++++++ config.h | 4 + doc/foot.ini.5.scd | 46 ++++++++++++ foot-features.h | 9 +++ main.c | 3 +- meson.build | 19 ++++- pgo/pgo.c | 10 ++- quirks.c | 1 + render.c | 171 +++++++++++++++++++++++++++--------------- render.h | 2 + scripts/srgb.py | 90 ++++++++++++++++++++++ shm.c | 80 +++++++++++++++++++- shm.h | 5 +- sixel.c | 97 ++++++++++++++++++++---- terminal.c | 54 ++++++++++---- terminal.h | 4 + wayland.c | 182 ++++++++++++++++++++++++++++++++++++++++++++- wayland.h | 23 ++++++ 20 files changed, 746 insertions(+), 101 deletions(-) create mode 100755 scripts/srgb.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 59d69f3e..152b7728 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,11 @@ * `search-bindings.delete-to-start` and `search-bindings.delete-to-end` key bindings, defaulting to `Control+u` and `Control+k` respectively ([#1972][1972]). +* Gamma-correct font rendering. Requires compositor support + (`wp_color_management_v1`, and specifically, the `ext_linear` + transfer function). Enabled by default when compositor support is + available. Can be explicitly enabled or disabled with + `gamma-correct-blending=no|yes`. [1386]: https://codeberg.org/dnkl/foot/issues/1386 [1872]: https://codeberg.org/dnkl/foot/issues/1872 @@ -87,6 +92,7 @@ * Auto-detection of URLs (i.e. not OSC-8 based URLs) are now regex based. * Rename Tokyo Night Day theme to Tokyo Night Light and update colors. +* fcft >= 3.2.0 is now required. [1925]: https://codeberg.org/dnkl/foot/issues/1925 diff --git a/client.c b/client.c index dabcc327..ceee1b29 100644 --- a/client.c +++ b/client.c @@ -67,13 +67,14 @@ version_and_features(void) { static char buf[256]; snprintf(buf, sizeof(buf), - "version: %s %cpgo %cime %cgraphemes %ctoplevel-icon %csystem-bell %cassertions", + "version: %s %cpgo %cime %cgraphemes %ctoplevel-icon %csystem-bell %ccolor-management %cassertions", FOOT_VERSION, feature_pgo() ? '+' : '-', feature_ime() ? '+' : '-', feature_graphemes() ? '+' : '-', feature_xdg_toplevel_icon() ? '+' : '-', feature_xdg_system_bell() ? '+' : '-', + feature_wp_color_management() ? '+' : '-', feature_assertions() ? '+' : '-'); return buf; } diff --git a/config.c b/config.c index df7f0031..1f287250 100644 --- a/config.c +++ b/config.c @@ -1107,6 +1107,28 @@ parse_section_main(struct context *ctx) return true; } + else if (streq(key, "gamma-correct-blending")) { + bool gamma_correct; + if (!value_to_bool(ctx, &gamma_correct)) + return false; + +#if defined(HAVE_WP_COLOR_MANAGEMENT) + conf->gamma_correct = + gamma_correct + ? GAMMA_CORRECT_ENABLED + : GAMMA_CORRECT_DISABLED; + return true; +#else + if (gamma_correct) { + LOG_CONTEXTUAL_WARN( + "ignoring; foot was built without color-management support"); + } + + conf->gamma_correct = GAMMA_CORRECT_DISABLED; + return true; +#endif + } + else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; @@ -2767,6 +2789,16 @@ parse_section_tweak(struct context *ctx) else if (streq(key, "bold-text-in-bright-amount")) return value_to_float(ctx, &conf->bold_in_bright.amount); + else if (streq(key, "surface-bit-depth")) { + _Static_assert(sizeof(conf->tweak.surface_bit_depth) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"8-bit", "10-bit", NULL}, + (int *)&conf->tweak.surface_bit_depth); + } + else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; @@ -3300,6 +3332,11 @@ config_load(struct config *conf, const char *conf_path, .underline_thickness = {.pt = 0., .px = -1}, .strikeout_thickness = {.pt = 0., .px = -1}, .dpi_aware = false, +#if defined(HAVE_WP_COLOR_MANAGEMENT) + .gamma_correct = GAMMA_CORRECT_AUTO, +#else + .gamma_correct = GAMMA_CORRECT_DISABLED, +#endif .security = { .osc52 = OSC52_ENABLED, }, @@ -3408,6 +3445,7 @@ config_load(struct config *conf, const char *conf_path, .box_drawing_solid_shades = true, .font_monospace_warn = true, .sixel = true, + .surface_bit_depth = 8, }, .touch = { diff --git a/config.h b/config.h index deddcf04..fb019d90 100644 --- a/config.h +++ b/config.h @@ -164,6 +164,9 @@ struct config { enum { STARTUP_WINDOWED, STARTUP_MAXIMIZED, STARTUP_FULLSCREEN } startup_mode; bool dpi_aware; + enum {GAMMA_CORRECT_DISABLED, + GAMMA_CORRECT_ENABLED, + GAMMA_CORRECT_AUTO} gamma_correct; struct config_font_list fonts[4]; struct font_size_adjustment font_size_adjustment; @@ -397,6 +400,7 @@ struct config { bool box_drawing_solid_shades; bool font_monospace_warn; bool sixel; + enum { SHM_8_BIT, SHM_10_BIT } surface_bit_depth; } tweak; struct { diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index c55419d1..952c7ae2 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -198,6 +198,35 @@ empty string to be set, but it must be quoted: *KEY=""*) Default: _unset_ +*gamma-correct-blending* + Boolean. When enabled, foot will do gamma-correct blending in + linear color space. This is how font glyphs are supposed to be + rendered, but since nearly no applications or toolkits are doing + it on Linux, the result may not look like you are used to. + + Compared to the default (disabled), bright glyphs on a dark + background will appear thicker, and dark glyphs on a light + background will appear thinner. + + Also be aware that many fonts have been developed on systems that + do not do gamma-correct blending, and may therefore look thicker + than intended when rendered with gamma-correct blending, since the + font designer set the font weight based on incorrect rendering. + + FreeType can limit the effect of the latter, with a technique + called stem darkening. It is only available for CFF fonts + (OpenType, .otf) and disabled by default (in FreeType). You can + enable it by setting the environment variable + *FREETYPE_PROPERTIES="cff:no-stem-darkening=0"* before starting + foot. + + You may also want to enable 10-bit image buffers when + gamma-correct blending is enabled. Though probably only if you do + not use a transparent background (with 10-bit buffers, you only + get 2 bits alpha). See *tweak.surface-bit-depth*. + + Default: enabled when compositor support is available + *box-drawings-uses-font-glyphs* Boolean. When disabled, foot generates box/line drawing characters itself. The are several advantages to doing this instead of using @@ -1917,6 +1946,23 @@ any of these options. *bold-text-in-bright* is set to *yes* (the *palette-based* variant is not affected by this option). Default: _1.3_. +*surface-bit-depth* + Selects which RGB bit depth to use for image buffers. One of + *8-bit*, or *10-bit*. + + The default, *8-bit*, uses 8 bits for all channels, alpha + included. When *gamma-correct-blending* is disabled, this is the + best option. + + When *gamma-correct-blending* is enabled, you may want to enable + 10-bit surfaces, as that improves the color resolution. Be aware + however, that in this mode, the alpha channel is only 2 bits + instead of 8 bits. Thus, if you are using a transparent + background, you may want to use the default, *8-bit*, even if you + have gamma-correct blending enabled. + + Default: _8-bit_ + # SEE ALSO *foot*(1), *footclient*(1) diff --git a/foot-features.h b/foot-features.h index 0eef5eac..c6c9c6f4 100644 --- a/foot-features.h +++ b/foot-features.h @@ -55,3 +55,12 @@ static inline bool feature_xdg_system_bell(void) return false; #endif } + +static inline bool feature_wp_color_management(void) +{ +#if defined(HAVE_WP_COLOR_MANAGEMENT) + return true; +#else + return false; +#endif +} diff --git a/main.c b/main.c index 0ba574c9..1a001186 100644 --- a/main.c +++ b/main.c @@ -51,13 +51,14 @@ version_and_features(void) { static char buf[256]; snprintf(buf, sizeof(buf), - "version: %s %cpgo %cime %cgraphemes %ctoplevel-icon %csystem-bell %cassertions", + "version: %s %cpgo %cime %cgraphemes %ctoplevel-icon %csystem-bell %ccolor-management %cassertions", FOOT_VERSION, feature_pgo() ? '+' : '-', feature_ime() ? '+' : '-', feature_graphemes() ? '+' : '-', feature_xdg_toplevel_icon() ? '+' : '-', feature_xdg_system_bell() ? '+' : '-', + feature_wp_color_management() ? '+' : '-', feature_assertions() ? '+' : '-'); return buf; } diff --git a/meson.build b/meson.build index 27ea3d53..6505460f 100644 --- a/meson.build +++ b/meson.build @@ -146,7 +146,7 @@ if utf8proc.found() endif tllist = dependency('tllist', version: '>=1.1.0', fallback: 'tllist') -fcft = dependency('fcft', version: ['>=3.0.1', '<4.0.0'], fallback: 'fcft') +fcft = dependency('fcft', version: ['>=3.2.0', '<4.0.0'], fallback: 'fcft') wayland_protocols_datadir = wayland_protocols.get_variable('pkgdatadir') @@ -187,6 +187,13 @@ else xdg_system_bell = false endif +if wayland_protocols.version().version_compare('>=1.41') + add_project_arguments('-DHAVE_WP_COLOR_MANAGEMENT', language: 'c') + wl_proto_xml += [wayland_protocols_datadir / 'staging/color-management/color-management-v1.xml'] + wp_color_management = true +else + wp_color_management = false +endif foreach prot : wl_proto_xml wl_proto_headers += custom_target( @@ -228,6 +235,13 @@ emoji_variation_sequences = custom_target( command: [python, generate_emoji_variation_sequences, '@INPUT@', '@OUTPUT@'] ) +generate_srgb_funcs = files('scripts/srgb.py') +srgb_funcs = custom_target( + 'generate_srgb_funcs', + output: ['srgb.c', 'srgb.h'], + command: [python, generate_srgb_funcs, '@OUTPUT0@', '@OUTPUT1@'] +) + common = static_library( 'common', 'log.c', 'log.h', @@ -260,7 +274,7 @@ vtlib = static_library( 'osc.c', 'osc.h', 'sixel.c', 'sixel.h', 'vt.c', 'vt.h', - builtin_terminfo, emoji_variation_sequences, + builtin_terminfo, emoji_variation_sequences, srgb_funcs, wl_proto_src + wl_proto_headers, version, dependencies: [libepoll, pixman, fcft, tllist, wayland_client, xkb, utf8proc], @@ -424,6 +438,7 @@ summary( 'Grapheme clustering': utf8proc.found(), 'Wayland: xdg-toplevel-icon-v1': xdg_toplevel_icon, 'Wayland: xdg-system-bell-v1': xdg_system_bell, + 'Wayland: wp-color-management-v1': wp_color_management, 'utmp backend': utmp_backend, 'utmp helper default path': utmp_default_helper_path, 'Build terminfo': tic.found(), diff --git a/pgo/pgo.c b/pgo/pgo.c index 88e862b8..8a4967ba 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -128,6 +128,12 @@ render_worker_thread(void *_ctx) return 0; } +bool +render_do_linear_blending(const struct terminal *term) +{ + return false; +} + struct extraction_context * extract_begin(enum selection_kind kind, bool strip_trailing_empty) { @@ -197,7 +203,9 @@ void shm_unref(struct buffer *buf) {} void shm_chain_free(struct buffer_chain *chain) {} struct buffer_chain * -shm_chain_new(struct wl_shm *shm, bool scrollable, size_t pix_instances) +shm_chain_new( + struct wayland *wayl, bool scrollable, size_t pix_instances, + bool ten_bit_it_if_capable) { return NULL; } diff --git a/quirks.c b/quirks.c index 9769f1ff..7cc8a8f1 100644 --- a/quirks.c +++ b/quirks.c @@ -86,6 +86,7 @@ is_sway(void) void quirk_sway_subsurface_unmap(struct terminal *term) { + return; if (!is_sway()) return; diff --git a/render.c b/render.c index 701cefae..eea43c10 100644 --- a/render.c +++ b/render.c @@ -44,6 +44,7 @@ #include "selection.h" #include "shm.h" #include "sixel.h" +#include "srgb.h" #include "url-mode.h" #include "util.h" #include "xmalloc.h" @@ -232,22 +233,45 @@ attrs_to_font(const struct terminal *term, const struct attributes *attrs) return term->fonts[idx]; } -static inline pixman_color_t -color_hex_to_pixman_with_alpha(uint32_t color, uint16_t alpha) +static pixman_color_t +color_hex_to_pixman_srgb(uint32_t color, uint16_t alpha) { return (pixman_color_t){ - .red = ((color >> 16 & 0xff) | (color >> 8 & 0xff00)) * alpha / 0xffff, - .green = ((color >> 8 & 0xff) | (color >> 0 & 0xff00)) * alpha / 0xffff, - .blue = ((color >> 0 & 0xff) | (color << 8 & 0xff00)) * alpha / 0xffff, - .alpha = alpha, + .alpha = alpha, /* Consider alpha linear already? */ + .red = srgb_decode_8_to_16((color >> 16) & 0xff), + .green = srgb_decode_8_to_16((color >> 8) & 0xff), + .blue = srgb_decode_8_to_16((color >> 0) & 0xff), }; } static inline pixman_color_t -color_hex_to_pixman(uint32_t color) +color_hex_to_pixman_with_alpha(uint32_t color, uint16_t alpha, bool srgb) +{ + pixman_color_t ret; + + if (srgb) + ret = color_hex_to_pixman_srgb(color, alpha); + else { + ret = (pixman_color_t){ + .red = ((color >> 16 & 0xff) | (color >> 8 & 0xff00)), + .green = ((color >> 8 & 0xff) | (color >> 0 & 0xff00)), + .blue = ((color >> 0 & 0xff) | (color << 8 & 0xff00)), + .alpha = alpha, + }; + } + + ret.red = (uint32_t)ret.red * alpha / 0xffff; + ret.green = (uint32_t)ret.green * alpha / 0xffff; + ret.blue = (uint32_t)ret.blue * alpha / 0xffff; + + return ret; +} + +static inline pixman_color_t +color_hex_to_pixman(uint32_t color, bool srgb) { /* Count on the compiler optimizing this */ - return color_hex_to_pixman_with_alpha(color, 0xffff); + return color_hex_to_pixman_with_alpha(color, 0xffff, srgb); } static inline uint32_t @@ -568,23 +592,24 @@ draw_strikeout(const struct terminal *term, pixman_image_t *pix, static void cursor_colors_for_cell(const struct terminal *term, const struct cell *cell, - const pixman_color_t *fg, const pixman_color_t *bg, - pixman_color_t *cursor_color, pixman_color_t *text_color) + const pixman_color_t *fg, const pixman_color_t *bg, + pixman_color_t *cursor_color, pixman_color_t *text_color, + bool gamma_correct) { if (term->colors.cursor_bg >> 31) - *cursor_color = color_hex_to_pixman(term->colors.cursor_bg); + *cursor_color = color_hex_to_pixman(term->colors.cursor_bg, gamma_correct); else *cursor_color = *fg; if (term->colors.cursor_fg >> 31) - *text_color = color_hex_to_pixman(term->colors.cursor_fg); + *text_color = color_hex_to_pixman(term->colors.cursor_fg, gamma_correct); else { *text_color = *bg; if (unlikely(text_color->alpha != 0xffff)) { /* The *only* color that can have transparency is the * default background color */ - *text_color = color_hex_to_pixman(term->colors.bg); + *text_color = color_hex_to_pixman(term->colors.bg, gamma_correct); } } @@ -592,8 +617,8 @@ cursor_colors_for_cell(const struct terminal *term, const struct cell *cell, text_color->green == cursor_color->green && text_color->blue == cursor_color->blue) { - *text_color = color_hex_to_pixman(term->colors.bg); - *cursor_color = color_hex_to_pixman(term->colors.fg); + *text_color = color_hex_to_pixman(term->colors.bg, gamma_correct); + *cursor_color = color_hex_to_pixman(term->colors.fg, gamma_correct); } } @@ -604,7 +629,8 @@ draw_cursor(const struct terminal *term, const struct cell *cell, { pixman_color_t cursor_color; pixman_color_t text_color; - cursor_colors_for_cell(term, cell, fg, bg, &cursor_color, &text_color); + cursor_colors_for_cell(term, cell, fg, bg, &cursor_color, &text_color, + render_do_linear_blending(term)); if (unlikely(!term->kbd_focus)) { switch (term->conf->cursor.unfocused_style) { @@ -656,8 +682,9 @@ draw_cursor(const struct terminal *term, const struct cell *cell, } static int -render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damage, - struct row *row, int row_no, int col, bool has_cursor) +render_cell(struct terminal *term, pixman_image_t *pix, + pixman_region32_t *damage, struct row *row, int row_no, int col, + bool has_cursor) { struct cell *cell = &row->cells[col]; if (cell->attrs.clean) @@ -776,8 +803,9 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag if (cell->attrs.blink && term->blink.state == BLINK_OFF) _fg = color_decrease_luminance(_fg); - pixman_color_t fg = color_hex_to_pixman(_fg); - pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha); + const bool gamma_correct = render_do_linear_blending(term); + pixman_color_t fg = color_hex_to_pixman(_fg, gamma_correct); + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); struct fcft_font *font = attrs_to_font(term, &cell->attrs); const struct composed *composed = NULL; @@ -987,7 +1015,7 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag if (i > 0 && glyph->x >= 0 && cell_cols == 1) g_x -= term->cell_width; - if (unlikely(pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8)) { + if (unlikely(glyph->is_color_glyph)) { /* Glyph surface is a pre-rendered image (typically a color emoji...) */ if (!(cell->attrs.blink && term->blink.state == BLINK_OFF)) { pixman_image_composite32( @@ -1071,12 +1099,12 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag switch (range->underline.color_src) { case COLOR_BASE256: underline_color = color_hex_to_pixman( - term->colors.table[range->underline.color]); + term->colors.table[range->underline.color], gamma_correct); break; case COLOR_RGB: underline_color = - color_hex_to_pixman(range->underline.color); + color_hex_to_pixman(range->underline.color, gamma_correct); break; case COLOR_DEFAULT: @@ -1105,8 +1133,8 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag pixman_color_t url_color = color_hex_to_pixman( term->conf->colors.use_custom.url ? term->conf->colors.url - : term->colors.table[3] - ); + : term->colors.table[3], + gamma_correct); draw_underline(term, pix, font, &url_color, x, y, cell_cols); } @@ -1119,8 +1147,9 @@ draw_cursor: } static void -render_row(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damage, - struct row *row, int row_no, int cursor_col) +render_row(struct terminal *term, pixman_image_t *pix, + pixman_region32_t *damage, struct row *row, + int row_no, int cursor_col) { for (int col = term->cols - 1; col >= 0; col--) render_cell(term, pix, damage, row, row_no, col, cursor_col == col); @@ -1130,7 +1159,7 @@ static void render_urgency(struct terminal *term, struct buffer *buf) { uint32_t red = term->colors.table[1]; - pixman_color_t bg = color_hex_to_pixman(red); + pixman_color_t bg = color_hex_to_pixman(red, render_do_linear_blending(term)); int width = min(min(term->margins.left, term->margins.right), min(term->margins.top, term->margins.bottom)); @@ -1161,6 +1190,7 @@ render_margin(struct terminal *term, struct buffer *buf, const int bmargin = term->height - term->margins.bottom; const int line_count = end_line - start_line; + const bool gamma_correct = render_do_linear_blending(term); const uint32_t _bg = !term->reverse ? term->colors.bg : term->colors.fg; uint16_t alpha = term->colors.alpha; @@ -1169,7 +1199,7 @@ render_margin(struct terminal *term, struct buffer *buf, alpha = 0xffff; } - pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha); + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &bg, 4, @@ -1596,8 +1626,7 @@ render_sixel(struct terminal *term, pixman_image_t *pix, static void render_sixel_images(struct terminal *term, pixman_image_t *pix, - pixman_region32_t *damage, - const struct coord *cursor) + pixman_region32_t *damage, const struct coord *cursor) { if (likely(tll_length(term->grid->sixel_images)) == 0) return; @@ -1649,6 +1678,8 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, if (unlikely(term->is_searching)) return; + const bool gamma_correct = render_do_linear_blending(term); + /* Adjust cursor position to viewport */ struct coord cursor; cursor = term->grid->cursor.point; @@ -1753,12 +1784,12 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, if (!seat->ime.preedit.cursor.hidden) { const struct cell *start_cell = &seat->ime.preedit.cells[0]; - pixman_color_t fg = color_hex_to_pixman(term->colors.fg); - pixman_color_t bg = color_hex_to_pixman(term->colors.bg); + pixman_color_t fg = color_hex_to_pixman(term->colors.fg, gamma_correct); + pixman_color_t bg = color_hex_to_pixman(term->colors.bg, gamma_correct); pixman_color_t cursor_color, text_color; cursor_colors_for_cell( - term, start_cell, &fg, &bg, &cursor_color, &text_color); + term, start_cell, &fg, &bg, &cursor_color, &text_color, gamma_correct); int x = term->margins.left + (col_idx + start) * term->cell_width; int y = term->margins.top + row_idx * term->cell_height; @@ -1789,12 +1820,14 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, row->cells[col_idx + i] = real_cells[i]; free(real_cells); + const int damage_x = term->margins.left + col_idx * term->cell_width; + const int damage_y = term->margins.top + row_idx * term->cell_height; + const int damage_w = cells_used * term->cell_width; + const int damage_h = term->cell_height; + wl_surface_damage_buffer( term->window->surface.surf, - term->margins.left, - term->margins.top + row_idx * term->cell_height, - term->width - term->margins.left - term->margins.right, - 1 * term->cell_height); + damage_x, damage_y, damage_w, damage_h); } #endif @@ -1916,7 +1949,7 @@ render_overlay(struct terminal *term) case OVERLAY_FLASH: color = color_hex_to_pixman_with_alpha( term->conf->colors.flash, - term->conf->colors.flash_alpha); + term->conf->colors.flash_alpha, render_do_linear_blending(term)); break; case OVERLAY_NONE: @@ -2118,6 +2151,7 @@ render_worker_thread(void *_ctx) sem_wait(start); struct buffer *buf = term->render.workers.buf; + bool frame_done = false; /* Translate offset-relative cursor row to view-relative */ @@ -2138,8 +2172,6 @@ render_worker_thread(void *_ctx) switch (row_no) { default: { - xassert(buf != NULL); - struct row *row = grid_row_in_view(term->grid, row_no); int cursor_col = cursor.row == row_no ? cursor.col : -1; @@ -2259,13 +2291,14 @@ render_osd(struct terminal *term, const struct wayl_sub_surface *sub_surf, pixman_image_set_clip_region32(buf->pix[0], &clip); pixman_region32_fini(&clip); + const bool gamma_correct = render_do_linear_blending(term); uint16_t alpha = _bg >> 24 | (_bg >> 24 << 8); - pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha); + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &bg, 1, &(pixman_rectangle16_t){0, 0, buf->width, buf->height}); - pixman_color_t fg = color_hex_to_pixman(_fg); + pixman_color_t fg = color_hex_to_pixman(_fg, gamma_correct); const int x_ofs = term->font_x_ofs; const size_t len = c32len(text); @@ -2312,7 +2345,7 @@ render_osd(struct terminal *term, const struct wayl_sub_surface *sub_surf, for (size_t i = 0; i < glyph_count; i++) { const struct fcft_glyph *glyph = glyphs[i]; - if (pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8) { + if (unlikely(glyph->is_color_glyph)) { pixman_image_composite32( PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0, x + x_ofs + glyph->x, y - glyph->y, @@ -2399,8 +2432,11 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, if (info->width == 0 || info->height == 0) return; + const bool gamma_correct = render_do_linear_blending(term); + { - pixman_color_t color = color_hex_to_pixman_with_alpha(0, 0); + /* Fully transparent - no need to do a color space transform */ + pixman_color_t color = color_hex_to_pixman_with_alpha(0, 0, gamma_correct); render_csd_part(term, surf->surf, buf, info->width, info->height, &color); } @@ -2461,7 +2497,8 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, _color = color_dim(term, _color); uint16_t alpha = _color >> 24 | (_color >> 24 << 8); - pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha); + pixman_color_t color = + color_hex_to_pixman_with_alpha(_color, alpha, gamma_correct); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &color, 1, @@ -2472,8 +2509,9 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, } static pixman_color_t -get_csd_button_fg_color(const struct config *conf) +get_csd_button_fg_color(const struct terminal *term) { + const struct config *conf = term->conf; uint32_t _color = conf->colors.bg; uint16_t alpha = 0xffff; @@ -2482,13 +2520,14 @@ get_csd_button_fg_color(const struct config *conf) alpha = _color >> 24 | (_color >> 24 << 8); } - return color_hex_to_pixman_with_alpha(_color, alpha); + return color_hex_to_pixman_with_alpha( + _color, alpha, render_do_linear_blending(term)); } static void render_csd_button_minimize(struct terminal *term, struct buffer *buf) { - pixman_color_t color = get_csd_button_fg_color(term->conf); + pixman_color_t color = get_csd_button_fg_color(term); pixman_image_t *src = pixman_image_create_solid_fill(&color); const int max_height = buf->height / 3; @@ -2516,7 +2555,7 @@ static void render_csd_button_maximize_maximized( struct terminal *term, struct buffer *buf) { - pixman_color_t color = get_csd_button_fg_color(term->conf); + pixman_color_t color = get_csd_button_fg_color(term); pixman_image_t *src = pixman_image_create_solid_fill(&color); const int max_height = buf->height / 3; @@ -2548,7 +2587,7 @@ static void render_csd_button_maximize_window( struct terminal *term, struct buffer *buf) { - pixman_color_t color = get_csd_button_fg_color(term->conf); + pixman_color_t color = get_csd_button_fg_color(term); pixman_image_t *src = pixman_image_create_solid_fill(&color); const int max_height = buf->height / 3; @@ -2588,7 +2627,7 @@ render_csd_button_maximize(struct terminal *term, struct buffer *buf) static void render_csd_button_close(struct terminal *term, struct buffer *buf) { - pixman_color_t color = get_csd_button_fg_color(term->conf); + pixman_color_t color = get_csd_button_fg_color(term); pixman_image_t *src = pixman_image_create_solid_fill(&color); const int max_height = buf->height / 3; @@ -2759,14 +2798,14 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx, if (!term->visual_focus) _color = color_dim(term, _color); - pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha); + const bool gamma_correct = render_do_linear_blending(term); + pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha, gamma_correct); render_csd_part(term, surf->surf, buf, info->width, info->height, &color); switch (surf_idx) { case CSD_SURF_MINIMIZE: render_csd_button_minimize(term, buf); break; case CSD_SURF_MAXIMIZE: render_csd_button_maximize(term, buf); break; case CSD_SURF_CLOSE: render_csd_button_close(term, buf); break; - break; default: BUG("unhandled surface type: %u", (unsigned)surf_idx); @@ -3618,6 +3657,7 @@ render_search_box(struct terminal *term) : term->conf->colors.use_custom.search_box_no_match; /* Background - yellow on empty/match, red on mismatch (default) */ + const bool gamma_correct = render_do_linear_blending(term); const pixman_color_t color = color_hex_to_pixman( is_match ? (custom_colors @@ -3625,13 +3665,14 @@ render_search_box(struct terminal *term) : term->colors.table[3]) : (custom_colors ? term->conf->colors.search_box.no_match.bg - : term->colors.table[1])); + : term->colors.table[1]), + gamma_correct); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &color, 1, &(pixman_rectangle16_t){width - visible_width, 0, visible_width, height}); - pixman_color_t transparent = color_hex_to_pixman_with_alpha(0, 0); + pixman_color_t transparent = color_hex_to_pixman_with_alpha(0, 0, gamma_correct); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &transparent, 1, &(pixman_rectangle16_t){0, 0, width - visible_width, height}); @@ -3641,12 +3682,14 @@ render_search_box(struct terminal *term) const int x_ofs = term->font_x_ofs; int x = x_left; int y = margin; + pixman_color_t fg = color_hex_to_pixman( custom_colors ? (is_match ? term->conf->colors.search_box.match.fg : term->conf->colors.search_box.no_match.fg) - : term->colors.table[0]); + : term->colors.table[0], + gamma_correct); /* Move offset we start rendering at, to ensure the cursor is visible */ for (size_t i = 0, cell_idx = 0; i <= term->search.cursor; cell_idx += widths[i], i++) { @@ -3802,8 +3845,7 @@ render_search_box(struct terminal *term) continue; } - if (unlikely(pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8)) { - /* Glyph surface is a pre-rendered image (typically a color emoji...) */ + if (unlikely(glyph->is_color_glyph)) { pixman_image_composite32( PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0, x + x_ofs + glyph->x, y + term->font_baseline - glyph->y, @@ -5186,3 +5228,14 @@ render_xcursor_set(struct seat *seat, struct terminal *term, seat->pointer.xcursor_pending = true; return true; } + +bool +render_do_linear_blending(const struct terminal *term) +{ +#if defined(HAVE_WP_COLOR_MANAGEMENT) + return term->conf->gamma_correct != GAMMA_CORRECT_DISABLED && + term->wl->color_management.img_description != NULL; +#else + return false; +#endif +} diff --git a/render.h b/render.h index 81d2a905..c7b8e4a5 100644 --- a/render.h +++ b/render.h @@ -47,3 +47,5 @@ struct csd_data { }; struct csd_data get_csd_data(const struct terminal *term, enum csd_surface surf_idx); + +bool render_do_linear_blending(const struct terminal *term); diff --git a/scripts/srgb.py b/scripts/srgb.py new file mode 100755 index 00000000..7655dbe4 --- /dev/null +++ b/scripts/srgb.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 + +import argparse +import math +import sys + + +def srgb_to_linear(f: float) -> float: + assert(f >= 0 and f <= 1.0) + + if f <= 0.04045: + return f / 12.92 + + return math.pow((f + 0.055) / 1.055, 2.4) + + +def linear_to_srgb(f: float) -> float: + if f < 0.0031308: + return f * 12.92 + + return 1.055 * math.pow(f, 1 / 2.4) - 0.055 + + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('c_output', type=argparse.FileType('w')) + parser.add_argument('h_output', type=argparse.FileType('w')) + opts = parser.parse_args() + + linear_table: list[int] = [] + srgb_table: list[int] = [] + + for i in range(256): + linear_table.append(int(srgb_to_linear(float(i) / 255) * 65535 + 0.5)) + + for i in range(4096): + srgb_table.append(int(linear_to_srgb(float(i) / 4095) * 255 + 0.5)) + + for i in range(256): + while True: + linear = linear_table[i] + srgb = srgb_table[linear >> 4] + + if i == srgb: + break + + linear_table[i] += 1 + + + opts.h_output.write("#pragma once\n") + opts.h_output.write("#include <stdint.h>\n") + opts.h_output.write("\n") + opts.h_output.write('/* 8-bit input, 16-bit output */\n') + opts.h_output.write("extern const uint16_t srgb_decode_8_to_16_table[256];") + + opts.h_output.write('\n') + opts.h_output.write('static inline uint16_t\n') + opts.h_output.write('srgb_decode_8_to_16(uint8_t v)\n') + opts.h_output.write('{\n') + opts.h_output.write(' return srgb_decode_8_to_16_table[v];\n') + opts.h_output.write('}\n') + + opts.h_output.write('\n') + opts.h_output.write('/* 8-bit input, 8-bit output */\n') + opts.h_output.write("extern const uint8_t srgb_decode_8_to_8_table[256];\n") + + opts.h_output.write('\n') + opts.h_output.write('static inline uint8_t\n') + opts.h_output.write('srgb_decode_8_to_8(uint8_t v)\n') + opts.h_output.write('{\n') + opts.h_output.write(' return srgb_decode_8_to_8_table[v];\n') + opts.h_output.write('}\n') + + opts.c_output.write('#include "srgb.h"\n') + opts.c_output.write('\n') + + opts.c_output.write("const uint16_t srgb_decode_8_to_16_table[256] = {\n") + for i in range(256): + opts.c_output.write(f' {linear_table[i]},\n') + opts.c_output.write('};\n') + + opts.c_output.write("const uint8_t srgb_decode_8_to_8_table[256] = {\n") + for i in range(256): + opts.c_output.write(f' {linear_table[i] >> 8},\n') + opts.c_output.write('};\n') + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/shm.c b/shm.c index 9a745f6c..32e6bdd0 100644 --- a/shm.c +++ b/shm.c @@ -92,6 +92,12 @@ struct buffer_chain { struct wl_shm *shm; size_t pix_instances; bool scrollable; + + pixman_format_code_t pixman_fmt_without_alpha; + enum wl_shm_format shm_format_without_alpha; + + pixman_format_code_t pixman_fmt_with_alpha; + enum wl_shm_format shm_format_with_alpha; }; static tll(struct buffer_private *) deferred; @@ -115,6 +121,7 @@ buffer_destroy_dont_close(struct buffer *buf) if (buf->pix[i] != NULL) pixman_image_unref(buf->pix[i]); } + if (buf->wl_buf != NULL) wl_buffer_destroy(buf->wl_buf); @@ -262,7 +269,9 @@ instantiate_offset(struct buffer_private *buf, off_t new_offset) wl_buf = wl_shm_pool_create_buffer( pool->wl_pool, new_offset, buf->public.width, buf->public.height, buf->public.stride, - buf->with_alpha ? WL_SHM_FORMAT_ARGB8888 : WL_SHM_FORMAT_XRGB8888); + buf->with_alpha + ? buf->chain->shm_format_with_alpha + : buf->chain->shm_format_without_alpha); if (wl_buf == NULL) { LOG_ERR("failed to create SHM buffer"); @@ -272,9 +281,12 @@ instantiate_offset(struct buffer_private *buf, off_t new_offset) /* One pixman image for each worker thread (do we really need multiple?) */ for (size_t i = 0; i < buf->public.pix_instances; i++) { pix[i] = pixman_image_create_bits_no_clear( - buf->with_alpha ? PIXMAN_a8r8g8b8 : PIXMAN_x8r8g8b8, + buf->with_alpha + ? buf->chain->pixman_fmt_with_alpha + : buf->chain->pixman_fmt_without_alpha, buf->public.width, buf->public.height, (uint32_t *)mmapped, buf->public.stride); + if (pix[i] == NULL) { LOG_ERR("failed to create pixman image"); goto err; @@ -959,14 +971,74 @@ shm_unref(struct buffer *_buf) } struct buffer_chain * -shm_chain_new(struct wl_shm *shm, bool scrollable, size_t pix_instances) +shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, + bool ten_bit_if_capable) { + pixman_format_code_t pixman_fmt_without_alpha = PIXMAN_x8r8g8b8; + enum wl_shm_format shm_fmt_without_alpha = WL_SHM_FORMAT_XRGB8888; + + pixman_format_code_t pixman_fmt_with_alpha = PIXMAN_a8r8g8b8; + enum wl_shm_format shm_fmt_with_alpha = WL_SHM_FORMAT_ARGB8888; + + static bool have_logged = false; + + + if (ten_bit_if_capable) { + if (wayl->shm_have_argb2101010 && wayl->shm_have_xrgb2101010) { + pixman_fmt_without_alpha = PIXMAN_x2r10g10b10; + shm_fmt_without_alpha = WL_SHM_FORMAT_XRGB2101010; + + pixman_fmt_with_alpha = PIXMAN_a2r10g10b10; + shm_fmt_with_alpha = WL_SHM_FORMAT_ARGB2101010; + + if (!have_logged) { + have_logged = true; + LOG_INFO("using 10-bit RGB surfaces"); + } + } + + else if (wayl->shm_have_abgr2101010 && wayl->shm_have_xbgr2101010) { + pixman_fmt_without_alpha = PIXMAN_x2b10g10r10; + shm_fmt_without_alpha = WL_SHM_FORMAT_XBGR2101010; + + pixman_fmt_with_alpha = PIXMAN_a2b10g10r10; + shm_fmt_with_alpha = WL_SHM_FORMAT_ABGR2101010; + + if (!have_logged) { + have_logged = true; + LOG_INFO("using 10-bit BGR surfaces"); + } + } + + else { + if (!have_logged) { + have_logged = true; + + LOG_WARN( + "10-bit surfaces requested, but compositor does not " + "implement ARGB2101010+XRGB2101010, or " + "ABGR2101010+XBGR2101010. Falling back to 8-bit surfaces"); + } + } + } else { + if (!have_logged) { + have_logged = true; + LOG_INFO("using 8-bit RGB surfaces"); + } + } + struct buffer_chain *chain = xmalloc(sizeof(*chain)); *chain = (struct buffer_chain){ .bufs = tll_init(), - .shm = shm, + .shm = wayl->shm, .pix_instances = pix_instances, .scrollable = scrollable, + + .pixman_fmt_without_alpha = pixman_fmt_without_alpha, + .shm_format_without_alpha = shm_fmt_without_alpha, + + .pixman_fmt_with_alpha = pixman_fmt_with_alpha, + .shm_format_with_alpha = shm_fmt_with_alpha, }; return chain; } diff --git a/shm.h b/shm.h index b4b075ca..2af185c9 100644 --- a/shm.h +++ b/shm.h @@ -9,6 +9,8 @@ #include <tllist.h> +#include "wayland.h" + struct damage; struct buffer { @@ -43,7 +45,8 @@ void shm_set_max_pool_size(off_t max_pool_size); struct buffer_chain; struct buffer_chain *shm_chain_new( - struct wl_shm *shm, bool scrollable, size_t pix_instances); + struct wayland *wayl, bool scrollable, size_t pix_instances, + bool ten_bit_it_if_capable); void shm_chain_free(struct buffer_chain *chain); /* diff --git a/sixel.c b/sixel.c index 44a5995b..dd933d7a 100644 --- a/sixel.c +++ b/sixel.c @@ -10,6 +10,7 @@ #include "grid.h" #include "hsl.h" #include "render.h" +#include "srgb.h" #include "util.h" #include "xmalloc.h" #include "xsnprintf.h" @@ -19,6 +20,40 @@ static size_t count; static void sixel_put_generic(struct terminal *term, uint8_t c); static void sixel_put_ar_11(struct terminal *term, uint8_t c); +static uint32_t +color_decode_srgb(const struct terminal *term, uint16_t r, uint16_t g, uint16_t b) +{ + if (term->sixel.linear_blending) { + if (term->sixel.use_10bit) { + r = srgb_decode_8_to_16(r) >> 6; + g = srgb_decode_8_to_16(g) >> 6; + b = srgb_decode_8_to_16(b) >> 6; + } else { + r = srgb_decode_8_to_8(r); + g = srgb_decode_8_to_8(g); + b = srgb_decode_8_to_8(b); + } + } else { + if (term->sixel.use_10bit) { + r <<= 2; + g <<= 2; + b <<= 2; + } + } + + uint32_t color; + + if (term->sixel.use_10bit) { + if (PIXMAN_FORMAT_TYPE(term->sixel.pixman_fmt) == PIXMAN_TYPE_ARGB) + color = 0x3u << 30 | r << 20 | g << 10 | b; + else + color = 0x3u << 30 | b << 20 | g << 10 | r; + } else + color = 0xffu << 24 | r << 16 | g << 8 | b; + + return color; +} + void sixel_fini(struct terminal *term) { @@ -75,6 +110,23 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) term->sixel.image.height = 0; term->sixel.image.alloc_height = 0; term->sixel.image.bottom_pixel = 0; + term->sixel.linear_blending = render_do_linear_blending(term); + term->sixel.pixman_fmt = PIXMAN_a8r8g8b8; + + if (term->conf->tweak.surface_bit_depth == SHM_10_BIT) { + if (term->wl->shm_have_argb2101010 && term->wl->shm_have_xrgb2101010) { + term->sixel.use_10bit = true; + term->sixel.pixman_fmt = PIXMAN_a2r10g10b10; + } + + else if (term->wl->shm_have_abgr2101010 && term->wl->shm_have_xbgr2101010) { + term->sixel.use_10bit = true; + term->sixel.pixman_fmt = PIXMAN_a2b10g10r10; + } + } + + const size_t active_palette_entries = min( + ALEN(term->conf->colors.sixel), term->sixel.palette_size); if (term->sixel.use_private_palette) { xassert(term->sixel.private_palette == NULL); @@ -83,11 +135,18 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) memcpy( term->sixel.private_palette, term->conf->colors.sixel, - min(sizeof(term->conf->colors.sixel), - term->sixel.palette_size * sizeof(term->sixel.private_palette[0]))); + active_palette_entries * sizeof(term->sixel.private_palette[0])); + + if (term->sixel.linear_blending || term->sixel.use_10bit) { + for (size_t i = 0; i < active_palette_entries; i++) { + uint8_t r = (term->sixel.private_palette[i] >> 16) & 0xff; + uint8_t g = (term->sixel.private_palette[i] >> 8) & 0xff; + uint8_t b = (term->sixel.private_palette[i] >> 0) & 0xff; + term->sixel.private_palette[i] = color_decode_srgb(term, r, g, b); + } + } term->sixel.palette = term->sixel.private_palette; - } else { if (term->sixel.shared_palette == NULL) { term->sixel.shared_palette = xcalloc( @@ -95,8 +154,16 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) memcpy( term->sixel.shared_palette, term->conf->colors.sixel, - min(sizeof(term->conf->colors.sixel), - term->sixel.palette_size * sizeof(term->sixel.shared_palette[0]))); + active_palette_entries * sizeof(term->sixel.shared_palette[0])); + + if (term->sixel.linear_blending || term->sixel.use_10bit) { + for (size_t i = 0; i < active_palette_entries; i++) { + uint8_t r = (term->sixel.private_palette[i] >> 16) & 0xff; + uint8_t g = (term->sixel.private_palette[i] >> 8) & 0xff; + uint8_t b = (term->sixel.private_palette[i] >> 0) & 0xff; + term->sixel.private_palette[i] = color_decode_srgb(term, r, g, b); + } + } } else { /* Shared palette - do *not* reset palette for new sixels */ } @@ -488,7 +555,7 @@ blend_new_image_over_old(const struct terminal *term, int stride = new_width * sizeof(uint32_t); uint32_t *new_data = xmalloc(stride * new_height); pixman_image_t *pix2 = pixman_image_create_bits_no_clear( - PIXMAN_a8r8g8b8, new_width, new_height, new_data, stride); + term->sixel.pixman_fmt, new_width, new_height, new_data, stride); #if defined(_DEBUG) /* Fill new image with an easy-to-recognize color (green) */ @@ -651,8 +718,7 @@ sixel_overwrite(struct terminal *term, struct sixel *six, } pixman_image_t *new_pix = pixman_image_create_bits_no_clear( - PIXMAN_a8r8g8b8, - new_width, new_height, new_data, new_width * sizeof(uint32_t)); + term->sixel.pixman_fmt, new_width, new_height, new_data, new_width * sizeof(uint32_t)); struct sixel new_six = { .pix = NULL, @@ -948,7 +1014,7 @@ sixel_sync_cache(const struct terminal *term, struct sixel *six) uint8_t *scaled_data = xmalloc(scaled_height * scaled_stride); pixman_image_t *scaled_pix = pixman_image_create_bits_no_clear( - PIXMAN_a8r8g8b8, scaled_width, scaled_height, + term->sixel.pixman_fmt, scaled_width, scaled_height, (uint32_t *)scaled_data, scaled_stride); pixman_image_composite32( @@ -1232,7 +1298,7 @@ sixel_unhook(struct terminal *term) image.pos.row, image.pos.row + image.rows); image.original.pix = pixman_image_create_bits_no_clear( - PIXMAN_a8r8g8b8, image.original.width, image.original.height, + term->sixel.pixman_fmt, image.original.width, image.original.height, img_data, stride); pixel_row_idx += height; @@ -2006,15 +2072,14 @@ decgci(struct terminal *term, uint8_t c) } case 2: { /* RGB */ - uint8_t r = 255 * min(c1, 100) / 100; - uint8_t g = 255 * min(c2, 100) / 100; - uint8_t b = 255 * min(c3, 100) / 100; + uint16_t r = 255 * min(c1, 100) / 100; + uint16_t g = 255 * min(c2, 100) / 100; + uint16_t b = 255 * min(c3, 100) / 100; - LOG_DBG("setting palette #%d = RGB %hhu/%hhu/%hhu", + LOG_DBG("setting palette #%d = RGB %hu/%hu/%hu", term->sixel.color_idx, r, g, b); - term->sixel.palette[term->sixel.color_idx] = - 0xffu << 24 | r << 16 | g << 8 | b; + term->sixel.palette[term->sixel.color_idx] = color_decode_srgb(term, r, g, b); break; } } diff --git a/terminal.c b/terminal.c index 0cd4fbca..1c607787 100644 --- a/terminal.c +++ b/terminal.c @@ -991,6 +991,7 @@ struct font_load_data { const char **names; const char *attrs; + const struct fcft_font_options *options; struct fcft_font **font; }; @@ -998,7 +999,8 @@ static int font_loader_thread(void *_data) { struct font_load_data *data = _data; - *data->font = fcft_from_name(data->count, data->names, data->attrs); + *data->font = fcft_from_name2( + data->count, data->names, data->attrs, data->options); return *data->font != NULL; } @@ -1065,14 +1067,32 @@ reload_fonts(struct terminal *term, bool resize_grid) [1] = xstrjoin(dpi, !custom_bold ? ":weight=bold" : ""), [2] = xstrjoin(dpi, !custom_italic ? ":slant=italic" : ""), [3] = xstrjoin(dpi, !custom_bold_italic ? ":weight=bold:slant=italic" : ""), - }; + }; + + struct fcft_font_options *options = fcft_font_options_create(); + + options->color_glyphs.format = PIXMAN_a8r8g8b8; + options->color_glyphs.srgb_decode = render_do_linear_blending(term); + + if (conf->tweak.surface_bit_depth == SHM_10_BIT) { + if ((term->wl->shm_have_argb2101010 && term->wl->shm_have_xrgb2101010) || + (term->wl->shm_have_abgr2101010 && term->wl->shm_have_xbgr2101010)) + { + /* + * Use a high-res buffer type for emojis. We don't want to + * use an a2r10g0b10 type of surface, since we need more + * than 2 bits for alpha. + */ + options->color_glyphs.format = PIXMAN_rgba_float; + } + } struct fcft_font *fonts[4]; struct font_load_data data[4] = { - {count_regular, names_regular, attrs[0], &fonts[0]}, - {count_bold, names_bold, attrs[1], &fonts[1]}, - {count_italic, names_italic, attrs[2], &fonts[2]}, - {count_bold_italic, names_bold_italic, attrs[3], &fonts[3]}, + {count_regular, names_regular, attrs[0], options, &fonts[0]}, + {count_bold, names_bold, attrs[1], options, &fonts[1]}, + {count_italic, names_italic, attrs[2], options, &fonts[2]}, + {count_bold_italic, names_bold_italic, attrs[3], options, &fonts[3]}, }; thrd_t tids[4] = {0}; @@ -1097,6 +1117,8 @@ reload_fonts(struct terminal *term, bool resize_grid) success = false; } + fcft_font_options_destroy(options); + for (size_t i = 0; i < 4; i++) { for (size_t j = 0; j < counts[i]; j++) free(names[i][j]); @@ -1237,6 +1259,8 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, goto err; } + const bool ten_bit_surfaces = conf->tweak.surface_bit_depth == SHM_10_BIT; + /* Initialize configure-based terminal attributes */ *term = (struct terminal) { .fdm = fdm, @@ -1320,13 +1344,14 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .wl = wayl, .render = { .chains = { - .grid = shm_chain_new(wayl->shm, true, 1 + conf->render_worker_count), - .search = shm_chain_new(wayl->shm, false, 1), - .scrollback_indicator = shm_chain_new(wayl->shm, false, 1), - .render_timer = shm_chain_new(wayl->shm, false, 1), - .url = shm_chain_new(wayl->shm, false, 1), - .csd = shm_chain_new(wayl->shm, false, 1), - .overlay = shm_chain_new(wayl->shm, false, 1), + .grid = shm_chain_new(wayl, true, 1 + conf->render_worker_count, + ten_bit_surfaces), + .search = shm_chain_new(wayl, false, 1 ,ten_bit_surfaces), + .scrollback_indicator = shm_chain_new(wayl, false, 1, ten_bit_surfaces), + .render_timer = shm_chain_new(wayl, false, 1, ten_bit_surfaces), + .url = shm_chain_new(wayl, false, 1, ten_bit_surfaces), + .csd = shm_chain_new(wayl, false, 1, ten_bit_surfaces), + .overlay = shm_chain_new(wayl, false, 1, ten_bit_surfaces), }, .scrollback_lines = conf->scrollback.lines, .app_sync_updates.timer_fd = app_sync_updates_fd, @@ -1468,6 +1493,9 @@ term_window_configured(struct terminal *term) if (!term->shutdown.in_progress) { xassert(term->window->is_configured); fdm_add(term->fdm, term->ptmx, EPOLLIN, &fdm_ptmx, term); + + const bool gamma_correct = render_do_linear_blending(term); + LOG_INFO("gamma-correct blending: %s", gamma_correct ? "enabled" : "disabled"); } } diff --git a/terminal.h b/terminal.h index e03b7bf7..518e36ef 100644 --- a/terminal.h +++ b/terminal.h @@ -777,6 +777,10 @@ struct terminal { bool transparent_bg; + bool linear_blending; + bool use_10bit; + pixman_format_code_t pixman_fmt; + /* Application configurable */ unsigned palette_size; /* Number of colors in palette */ unsigned max_width; /* Maximum image width, in pixels */ diff --git a/wayland.c b/wayland.c index 3a46133f..1c083a9e 100644 --- a/wayland.c +++ b/wayland.c @@ -237,6 +237,15 @@ seat_destroy(struct seat *seat) static void shm_format(void *data, struct wl_shm *wl_shm, uint32_t format) { + struct wayland *wayl = data; + + switch (format) { + case WL_SHM_FORMAT_XRGB2101010: wayl->shm_have_xrgb2101010 = true; break; + case WL_SHM_FORMAT_ARGB2101010: wayl->shm_have_argb2101010 = true; break; + case WL_SHM_FORMAT_XBGR2101010: wayl->shm_have_xbgr2101010 = true; break; + case WL_SHM_FORMAT_ABGR2101010: wayl->shm_have_abgr2101010 = true; break; + } + #if defined(_DEBUG) bool have_description = false; @@ -666,6 +675,91 @@ static const struct wp_presentation_listener presentation_listener = { .clock_id = &clock_id, }; +#if defined(HAVE_WP_COLOR_MANAGEMENT) + +static void +color_manager_create_image_description(struct wayland *wayl) +{ + if (!wayl->color_management.have_feat_parametric) + return; + + if (!wayl->color_management.have_primaries_srgb) + return; + + if (!wayl->color_management.have_tf_ext_linear) + return; + + struct wp_image_description_creator_params_v1 *params = + wp_color_manager_v1_create_parametric_creator(wayl->color_management.manager); + + wp_image_description_creator_params_v1_set_tf_named( + params, WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR); + + wp_image_description_creator_params_v1_set_primaries_named( + params, WP_COLOR_MANAGER_V1_PRIMARIES_SRGB); + + wayl->color_management.img_description = + wp_image_description_creator_params_v1_create(params); +} + +static void +color_manager_supported_intent(void *data, + struct wp_color_manager_v1 *wp_color_manager_v1, + uint32_t render_intent) +{ + struct wayland *wayl = data; + if (render_intent == WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL) + wayl->color_management.have_intent_perceptual = true; +} + +static void +color_manager_supported_feature(void *data, + struct wp_color_manager_v1 *wp_color_manager_v1, + uint32_t feature) +{ + struct wayland *wayl = data; + + if (feature == WP_COLOR_MANAGER_V1_FEATURE_PARAMETRIC) + wayl->color_management.have_feat_parametric = true; +} + +static void +color_manager_supported_tf_named(void *data, + struct wp_color_manager_v1 *wp_color_manager_v1, + uint32_t tf) +{ + struct wayland *wayl = data; + if (tf == WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR) + wayl->color_management.have_tf_ext_linear = true; +} + +static void +color_manager_supported_primaries_named(void *data, + struct wp_color_manager_v1 *wp_color_manager_v1, + uint32_t primaries) +{ + struct wayland *wayl = data; + if (primaries == WP_COLOR_MANAGER_V1_PRIMARIES_SRGB) + wayl->color_management.have_primaries_srgb = true; +} + +static void +color_manager_done(void *data, + struct wp_color_manager_v1 *wp_color_manager_v1) +{ + struct wayland *wayl = data; + color_manager_create_image_description(wayl); +} + +static const struct wp_color_manager_v1_listener color_manager_listener = { + .supported_intent = &color_manager_supported_intent, + .supported_feature = &color_manager_supported_feature, + .supported_primaries_named = &color_manager_supported_primaries_named, + .supported_tf_named = &color_manager_supported_tf_named, + .done = &color_manager_done, +}; +#endif + static bool verify_iface_version(const char *iface, uint32_t version, uint32_t wanted) { @@ -1385,6 +1479,20 @@ handle_global(void *data, struct wl_registry *registry, } #endif +#if defined(HAVE_WP_COLOR_MANAGEMENT) + else if (streq(interface, wp_color_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->color_management.manager = wl_registry_bind( + wayl->registry, name, &wp_color_manager_v1_interface, required); + + wp_color_manager_v1_add_listener( + wayl->color_management.manager, &color_manager_listener, wayl); + } +#endif + #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED else if (streq(interface, zwp_text_input_manager_v3_interface.name)) { const uint32_t required = 1; @@ -1707,6 +1815,13 @@ wayl_destroy(struct wayland *wayl) zwp_text_input_manager_v3_destroy(wayl->text_input_manager); #endif +#if defined(HAVE_WP_COLOR_MANAGEMENT) + if (wayl->color_management.img_description != NULL) + wp_image_description_v1_destroy(wayl->color_management.img_description); + if (wayl->color_management.manager != NULL) + wp_color_manager_v1_destroy(wayl->color_management.manager); +#endif + #if defined(HAVE_XDG_SYSTEM_BELL) if (wayl->system_bell != NULL) xdg_system_bell_v1_destroy(wayl->system_bell); @@ -1847,6 +1962,38 @@ wayl_win_init(struct terminal *term, const char *token) } #endif +#if defined(HAVE_WP_COLOR_MANAGEMENT) + if (term->conf->gamma_correct != GAMMA_CORRECT_DISABLED) { + if (wayl->color_management.img_description != NULL) { + xassert(wayl->color_management.manager != NULL); + + win->surface.color_management = wp_color_manager_v1_get_surface( + term->wl->color_management.manager, win->surface.surf); + + wp_color_management_surface_v1_set_image_description( + win->surface.color_management, wayl->color_management.img_description, + WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL); + } else if (term->conf->gamma_correct == GAMMA_CORRECT_ENABLED) { + if (wayl->color_management.manager == NULL) { + LOG_WARN( + "gamma-corrected-blending: disabling; " + "compositor does not implement the color-management protocol"); + } else { + LOG_WARN( + "gamma-corrected-blending: disabling; " + "compositor does not implement all required color-management features"); + LOG_WARN("use e.g. 'wayland-info' and verify the compositor implements:"); + LOG_WARN(" - feature: parametric"); + LOG_WARN(" - render intent: perceptual"); + LOG_WARN(" - TF: ext_linear"); + LOG_WARN(" - primaries: sRGB"); + } + } else { + /* "auto" - don't warn */ + } + } +#endif + if (conf->csd.preferred == CONF_CSD_PREFER_NONE) { /* User specifically do *not* want decorations */ win->csd_mode = CSD_NO; @@ -1861,8 +2008,8 @@ wayl_win_init(struct terminal *term, const char *token) zxdg_toplevel_decoration_v1_set_mode( win->xdg_toplevel_decoration, (conf->csd.preferred == CONF_CSD_PREFER_SERVER - ? ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE - : ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE)); + ? ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE + : ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE)); zxdg_toplevel_decoration_v1_add_listener( win->xdg_toplevel_decoration, &xdg_toplevel_decoration_listener, win); @@ -1987,7 +2134,12 @@ wayl_win_destroy(struct wl_window *win) free(it->item); tll_remove(win->xdg_tokens, it); - } +} + +#if defined(HAVE_WP_COLOR_MANAGEMENT) + if (win->surface.color_management != NULL) + wp_color_management_surface_v1_destroy(win->surface.color_management); +#endif if (win->fractional_scale != NULL) wp_fractional_scale_v1_destroy(win->fractional_scale); @@ -2308,6 +2460,7 @@ wayl_win_subsurface_new_with_custom_parent( struct wayland *wayl = win->term->wl; surf->surface.surf = NULL; + surf->surface.viewport = NULL; surf->sub = NULL; struct wl_surface *main_surface @@ -2318,6 +2471,22 @@ wayl_win_subsurface_new_with_custom_parent( return false; } +#if defined(HAVE_WP_COLOR_MANAGEMENT) + surf->surface.color_management = NULL; + if (win->term->conf->gamma_correct && + wayl->color_management.img_description != NULL) + { + xassert(wayl->color_management.manager != NULL); + + surf->surface.color_management = wp_color_manager_v1_get_surface( + wayl->color_management.manager, main_surface); + + wp_color_management_surface_v1_set_image_description( + surf->surface.color_management, wayl->color_management.img_description, + WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL); + } +#endif + struct wl_subsurface *sub = wl_subcompositor_get_subsurface( wayl->sub_compositor, main_surface, parent); @@ -2369,6 +2538,13 @@ wayl_win_subsurface_destroy(struct wayl_sub_surface *surf) if (surf == NULL) return; +#if defined(HAVE_WP_COLOR_MANAGEMENT) + if (surf->surface.color_management != NULL) { + wp_color_management_surface_v1_destroy(surf->surface.color_management); + surf->surface.color_management = NULL; + } +#endif + if (surf->surface.viewport != NULL) { wp_viewport_destroy(surf->surface.viewport); surf->surface.viewport = NULL; diff --git a/wayland.h b/wayland.h index b3ef5a2b..ec27281a 100644 --- a/wayland.h +++ b/wayland.h @@ -28,6 +28,10 @@ #include <xdg-system-bell-v1.h> #endif +#if defined(HAVE_WP_COLOR_MANAGEMENT) + #include <color-management-v1.h> +#endif + #include <fcft/fcft.h> #include <tllist.h> @@ -61,6 +65,9 @@ enum touch_state { struct wayl_surface { struct wl_surface *surf; struct wp_viewport *viewport; +#if defined(HAVE_WP_COLOR_MANAGEMENT) + struct wp_color_management_surface_v1 *color_management; +#endif }; struct wayl_sub_surface { @@ -459,6 +466,17 @@ struct wayland { struct xdg_system_bell_v1 *system_bell; #endif +#if defined(HAVE_WP_COLOR_MANAGEMENT) + struct { + struct wp_color_manager_v1 *manager; + struct wp_image_description_v1 *img_description; + bool have_intent_perceptual; + bool have_feat_parametric; + bool have_tf_ext_linear; + bool have_primaries_srgb; + } color_management; +#endif + bool presentation_timings; struct wp_presentation *presentation; uint32_t presentation_clock_id; @@ -474,6 +492,11 @@ struct wayland { /* WL_SHM >= 2 */ bool use_shm_release; + + bool shm_have_argb2101010:1; + bool shm_have_xrgb2101010:1; + bool shm_have_abgr2101010:1; + bool shm_have_xbgr2101010:1; }; struct wayland *wayl_init( From a80b32d006b24512bb8bd6c4d362f90abb4388d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 28 Feb 2025 08:11:50 +0100 Subject: [PATCH 1091/1323] term: tweak linebreaking Don't set linebreak on linefeed. Instead, rely on the default value of true, and that it is only cleared when a character is printed while LCF=1. Note that printing to a row that has linebreak cleared, will set the linebreak flag again. --- CHANGELOG.md | 6 ++++++ terminal.c | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 152b7728..cb3ae470 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,8 +93,14 @@ based. * Rename Tokyo Night Day theme to Tokyo Night Light and update colors. * fcft >= 3.2.0 is now required. +* Linefeed:ing control characters (e.g. `\n`) no longer **clears** a + row's internal linebreak flag. This fixes an issue where + e.g. multi-line prompt input in fish is treated as separate lines, + rather than one logical, when selecting and copying it + ([#1487][1487]). [1925]: https://codeberg.org/dnkl/foot/issues/1925 +[1487]: https://codeberg.org/dnkl/foot/issues/1487 ### Deprecated diff --git a/terminal.c b/terminal.c index 1c607787..988716f4 100644 --- a/terminal.c +++ b/terminal.c @@ -3191,7 +3191,6 @@ term_carriage_return(struct terminal *term) void term_linefeed(struct terminal *term) { - term->grid->cur_row->linebreak = true; term->grid->cursor.lcf = false; if (term->grid->cursor.point.row == term->scroll_region.end - 1) From 7b6efcf19a10794dd7b41e30ce706eae3a290388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 4 Mar 2025 08:34:18 +0100 Subject: [PATCH 1092/1323] grid: change default value of linebreak to true This way, all lines are treated as having a hard linebreak, until it's cleared when we do an auto-wrap. This change alone causes issues when reflowing text, as now all trailing lines in an otherwise empty window are treated as hard linebreaks, causing the new grid to insert lots of unwanted, empty lines. Fix by doing two things: * *clear* the linebreak flag when we pull in new lines for the new grid. We only want to set it explicitly, when an old row has its linebreak flag set. * Coalesce empty lines with linebreak=true, and only "emit" them as new liens in the new grid if they are followed by non-empty lines. --- grid.c | 87 +++++++++++++++++++++++++++++++++++++----------------- terminal.c | 2 +- 2 files changed, 61 insertions(+), 28 deletions(-) diff --git a/grid.c b/grid.c index 2deb9111..4b9a7fb6 100644 --- a/grid.c +++ b/grid.c @@ -439,7 +439,7 @@ grid_row_alloc(int cols, bool initialize) { struct row *row = xmalloc(sizeof(*row)); row->dirty = false; - row->linebreak = false; + row->linebreak = true; row->extra = NULL; row->shell_integration.prompt_marker = false; row->shell_integration.cmd_start = -1; @@ -709,14 +709,21 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, /* Scrollback not yet full, allocate a completely new row */ new_row = grid_row_alloc(col_count, false); new_grid[*row_idx] = new_row; + + /* *clear* linebreak, since we only want to set it when we + reach the end of an old row, with linebreak=true */ + new_row->linebreak = false; } else { /* Scrollback is full, need to reuse a row */ grid_row_reset_extra(new_row); - new_row->linebreak = false; new_row->shell_integration.prompt_marker = false; new_row->shell_integration.cmd_start = -1; new_row->shell_integration.cmd_end = -1; + /* *clear* linebreak, since we only want to set it when we + reach the end of an old row, with linebreak=true */ + new_row->linebreak = false; + tll_foreach(old_grid->sixel_images, it) { if (it->item.pos.row == *row_idx) { sixel_destroy(&it->item); @@ -894,6 +901,8 @@ grid_resize_and_reflow( i, tracking_points[i]->row, tracking_points[i]->col); } + int coalesced_linebreaks = 0; + /* * Walk the old grid */ @@ -984,6 +993,20 @@ grid_resize_and_reflow( } else underline_range = underline_range_terminator = NULL; + if (unlikely(col_count > 0 && coalesced_linebreaks > 0)) { + for (size_t apa = 0; apa < coalesced_linebreaks; apa++) { + /* Erase the remaining cells */ + memset(&new_row->cells[new_col_idx], 0, + (new_cols - new_col_idx) * sizeof(new_row->cells[0])); + new_row->linebreak = true; + + if (r + 1 < old_rows) + line_wrap(); + } + + coalesced_linebreaks = 0; + } + for (int c = 0; c < col_count;) { const struct cell *old = &old_row->cells[c]; @@ -1095,33 +1118,43 @@ grid_resize_and_reflow( } if (old_row->linebreak) { - /* Erase the remaining cells */ - memset(&new_row->cells[new_col_idx], 0, - (new_cols - new_col_idx) * sizeof(new_row->cells[0])); - new_row->linebreak = true; - - if (r + 1 < old_rows) - line_wrap(); - else if (new_row->extra != NULL) { - if (new_row->extra->uri_ranges.count > 0) { - /* - * line_wrap() "closes" still-open URIs. Since - * this is the *last* row, and since we're - * line-breaking due to a hard line-break (rather - * than running out of cells in the "new_row"), - * there shouldn't be an open URI (it would have - * been closed when we reached the end of the URI - * while reflowing the last "old" row). - */ - int last_idx = new_row->extra->uri_ranges.count - 1; - xassert(new_row->extra->uri_ranges.v[last_idx].end >= 0); - } - - if (new_row->extra->underline_ranges.count > 0) { - int last_idx = new_row->extra->underline_ranges.count - 1; - xassert(new_row->extra->underline_ranges.v[last_idx].end >= 0); + if (col_count > 0) { + /* Erase the remaining cells */ + memset(&new_row->cells[new_col_idx], 0, + (new_cols - new_col_idx) * sizeof(new_row->cells[0])); + new_row->linebreak = true; + if (r + 1 < old_rows) { + /* Not the last (old) row */ + line_wrap(); + } else if (new_row->extra != NULL) { + if (new_row->extra->uri_ranges.count > 0) { + /* + * line_wrap() "closes" still-open URIs. Since + * this is the *last* row, and since we're + * line-breaking due to a hard line-break (rather + * than running out of cells in the "new_row"), + * there shouldn't be an open URI (it would have + * been closed when we reached the end of the URI + * while reflowing the last "old" row). + */ + int last_idx = new_row->extra->uri_ranges.count - 1; + xassert(new_row->extra->uri_ranges.v[last_idx].end >= 0); + } + + if (new_row->extra->underline_ranges.count > 0) { + int last_idx = new_row->extra->underline_ranges.count - 1; + xassert(new_row->extra->underline_ranges.v[last_idx].end >= 0); + } } + } else { + /* + * rows have linebreak=true by default. But we don't + * want trailing empty lines to result in actual lines + * in the new grid (think: empty window with prompt at + * the top) + */ + coalesced_linebreaks++; } } diff --git a/terminal.c b/terminal.c index 988716f4..2ba85276 100644 --- a/terminal.c +++ b/terminal.c @@ -2057,7 +2057,7 @@ static inline void erase_line(struct terminal *term, struct row *row) { erase_cell_range(term, row, 0, term->cols - 1); - row->linebreak = false; + row->linebreak = true; row->shell_integration.prompt_marker = false; row->shell_integration.cmd_start = -1; row->shell_integration.cmd_end = -1; From 605694bc938ace9a7501d6730c1865ea531870df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 5 Mar 2025 07:38:44 +0100 Subject: [PATCH 1093/1323] grid: set linebreak=false when printing to a line, not when allocating it This ensures empty lines are treated correctly, and is also more in line with how lines are handled at runtime, when filling the scrollback. For now, set linebreak=false as soon as something is printed on a line. It will remain like that *until* we reach the end of an old row with linebreak=true, at which point we set linebreak=true on the current new line. --- grid.c | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/grid.c b/grid.c index 4b9a7fb6..cb6e1a91 100644 --- a/grid.c +++ b/grid.c @@ -501,7 +501,6 @@ grid_resize_without_reflow( sizeof(struct cell) * min(old_cols, new_cols)); new_row->dirty = old_row->dirty; - new_row->linebreak = false; 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); @@ -709,10 +708,6 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, /* Scrollback not yet full, allocate a completely new row */ new_row = grid_row_alloc(col_count, false); new_grid[*row_idx] = new_row; - - /* *clear* linebreak, since we only want to set it when we - reach the end of an old row, with linebreak=true */ - new_row->linebreak = false; } else { /* Scrollback is full, need to reuse a row */ grid_row_reset_extra(new_row); @@ -720,10 +715,6 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, new_row->shell_integration.cmd_start = -1; new_row->shell_integration.cmd_end = -1; - /* *clear* linebreak, since we only want to set it when we - reach the end of an old row, with linebreak=true */ - new_row->linebreak = false; - tll_foreach(old_grid->sixel_images, it) { if (it->item.pos.row == *row_idx) { sixel_destroy(&it->item); @@ -1112,6 +1103,17 @@ grid_resize_and_reflow( } new_row->cells[new_col_idx++] = *old; + + /* + * TODO: simulate LCF instead? + * + * Rows have linebreak=true by default. This is needed + * for a number of reasons. However, we want non-empty + * rows to have linebreak=false, *until* we reach the + * end of an old row with linebreak=true, at which + * point we set linebreak=true on the new row. + */ + new_row->linebreak = false; old++; c++; } From 8d2627b1ef8fd7f8313e4441de3c76b873575a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 10 Mar 2025 15:47:20 +0100 Subject: [PATCH 1094/1323] input: kitty: always use shifted key when it's the result of a compose Closes #1987 --- input.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/input.c b/input.c index 916f30e4..a2867ac6 100644 --- a/input.c +++ b/input.c @@ -1411,7 +1411,7 @@ emit_escapes: size_t left = sizeof(buf); size_t bytes; - const int key = unshifted > 0 && isc32print(unshifted) ? unshifted : shifted; + const int key = unshifted > 0 && isc32print(unshifted) && !composed ? unshifted : shifted; const int alternate = shifted; if (final == 'u' || final == '~') { From 04fcc5f5b5d0a3f45d752e987a0c87baa4e50779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 11 Mar 2025 08:23:23 +0100 Subject: [PATCH 1095/1323] input: kitty: regression test for #1987 --- input.c | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/input.c b/input.c index a2867ac6..0507d8bd 100644 --- a/input.c +++ b/input.c @@ -2087,6 +2087,103 @@ UNITTEST seat.kbd.xkb_keymap = NULL; } + /* us(intl) keymap */ + { + seat.kbd.xkb_keymap = xkb_keymap_new_from_names( + seat.kbd.xkb, &(struct xkb_rule_names){.layout = "us", .variant = "intl"}, + XKB_KEYMAP_COMPILE_NO_FLAGS); + + if (seat.kbd.xkb_keymap == NULL) { + /* Skip test */ + goto no_keymap; + } + + seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap); + xassert(seat.kbd.xkb_state != NULL); + + seat.kbd.xkb_compose_table = xkb_compose_table_new_from_locale( + seat.kbd.xkb, setlocale(LC_CTYPE, NULL), XKB_COMPOSE_COMPILE_NO_FLAGS); + xassert(seat.kbd.xkb_compose_table != NULL); + + seat.kbd.xkb_compose_state = xkb_compose_state_new( + seat.kbd.xkb_compose_table, XKB_COMPOSE_STATE_NO_FLAGS); + xassert(seat.kbd.xkb_compose_state != NULL); + + seat.kbd.mod_shift = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); + seat.kbd.mod_alt = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; + seat.kbd.mod_ctrl = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL); + seat.kbd.mod_super = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO); + seat.kbd.mod_caps = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS); + seat.kbd.mod_num = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM); + + /* Significant modifiers in the legacy keyboard protocol */ + seat.kbd.legacy_significant = 0; + if (seat.kbd.mod_shift != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift; + if (seat.kbd.mod_alt != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt; + if (seat.kbd.mod_ctrl != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl; + if (seat.kbd.mod_super != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super; + + /* Significant modifiers in the kitty keyboard protocol */ + seat.kbd.kitty_significant = seat.kbd.legacy_significant; + if (seat.kbd.mod_caps != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps; + if (seat.kbd.mod_num != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num; + + key_binding_new_for_seat(key_binding_manager, &seat); + key_binding_load_keymap(key_binding_manager, &seat); + + { + /* + * Test the compose sequence "shift+', shift+space" + * + * Should result in a double quote, but a regression + * caused it to instead emit a space. See #1987 + * + * Note: "shift+', space" also results in a double quote, + * but never regressed to a space. + */ + grid.kitty_kbd.flags[0] = KITTY_KBD_DISAMBIGUATE; + xkb_compose_state_reset(seat.kbd.xkb_compose_state); + + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 1); + + key_press_release(&seat, &term, 1337, KEY_APOSTROPHE + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + key_press_release(&seat, &term, 1337, KEY_APOSTROPHE + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + + key_press_release(&seat, &term, 1337, KEY_SPACE + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key: 34 = '"', alternate: N/A, base: N/A, mods: 2 = shift */ + const char expected_shift_apostrophe[] = "\033[34;2u"; + xassert(count == strlen(expected_shift_apostrophe)); + xassert(streq(escape, expected_shift_apostrophe)); + + key_press_release(&seat, &term, 1337, KEY_SPACE + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + + grid.kitty_kbd.flags[0] = KITTY_KBD_DISAMBIGUATE | KITTY_KBD_REPORT_ALTERNATE; + } + + key_binding_unload_keymap(key_binding_manager, &seat); + key_binding_remove_seat(key_binding_manager, &seat); + + xkb_compose_state_unref(seat.kbd.xkb_compose_state); + xkb_compose_table_unref(seat.kbd.xkb_compose_table); + + xkb_state_unref(seat.kbd.xkb_state); + xkb_keymap_unref(seat.kbd.xkb_keymap); + + seat.kbd.xkb_state = NULL; + seat.kbd.xkb_keymap = NULL; + } + no_keymap: xkb_context_unref(seat.kbd.xkb); key_binding_manager_destroy(key_binding_manager); From 7976975a8a6382a1e81a5d6c2749bc5beb23ff41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 11 Mar 2025 08:36:37 +0100 Subject: [PATCH 1096/1323] input: kitty: send release events for composed keys --- input.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/input.c b/input.c index 0507d8bd..c0b2323f 100644 --- a/input.c +++ b/input.c @@ -1157,9 +1157,6 @@ kitty_kbd_protocol(struct seat *seat, struct terminal *term, if (!report_events && released) return false; - if (composed && released) - return false; - /* TODO: should we even bother with this, or just say it's not supported? */ if (!disambiguate && !report_all_as_escapes && pressed) return legacy_kbd_protocol(seat, term, ctx); From edbfdd51508ce6e4a44078db49f62151707eb928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 11 Mar 2025 08:37:42 +0100 Subject: [PATCH 1097/1323] changelog: kitty: release events for composed keys --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb3ae470..b86c826e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -137,6 +137,8 @@ * Crash when resetting the terminal and an application had previously set a custom app ID ([#1963][1963]) * Grapheme clustering state not reset on cursor movements. +* Kitty keyboard protocol: no release events emitted for composed + keys. [1918]: https://codeberg.org/dnkl/foot/issues/1918 [1929]: https://codeberg.org/dnkl/foot/issues/1929 From cfa178ab259528a106a2855bdd2e07c34a822224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 11 Mar 2025 08:42:03 +0100 Subject: [PATCH 1098/1323] input: kitty: unittest: don't fail if system has no compose tables --- input.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/input.c b/input.c index c0b2323f..cbe8e1c7 100644 --- a/input.c +++ b/input.c @@ -2100,11 +2100,15 @@ UNITTEST seat.kbd.xkb_compose_table = xkb_compose_table_new_from_locale( seat.kbd.xkb, setlocale(LC_CTYPE, NULL), XKB_COMPOSE_COMPILE_NO_FLAGS); - xassert(seat.kbd.xkb_compose_table != NULL); + if (seat.kbd.xkb_compose_table == NULL) + goto no_keymap; seat.kbd.xkb_compose_state = xkb_compose_state_new( seat.kbd.xkb_compose_table, XKB_COMPOSE_STATE_NO_FLAGS); - xassert(seat.kbd.xkb_compose_state != NULL); + if (seat.kbd.xkb_compose_state == NULL) { + xkb_compose_table_unref(seat.kbd.xkb_compose_table); + goto no_keymap; + } seat.kbd.mod_shift = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); seat.kbd.mod_alt = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; From 7f11ba59efad352b1ff0bb11c0286942ae3d1edb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 12 Mar 2025 10:03:06 +0100 Subject: [PATCH 1099/1323] fcft: require fcft >= 3.3.0, add support for new scaling-filters Update tweak.scaling-filter to recognize the new scaling filters added in fcft-3.3.0. Since fcft_set_scaling_filter() is deprecated in 3.3.0, don't use it anymore, and set the scaling filter via fcft_font_options instead. --- CHANGELOG.md | 2 +- config.c | 7 +++++++ doc/foot.ini.5.scd | 5 ++--- main.c | 1 - meson.build | 2 +- terminal.c | 1 + 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b86c826e..0843c29d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,7 +92,7 @@ * Auto-detection of URLs (i.e. not OSC-8 based URLs) are now regex based. * Rename Tokyo Night Day theme to Tokyo Night Light and update colors. -* fcft >= 3.2.0 is now required. +* fcft >= 3.3.0 is now required. * Linefeed:ing control characters (e.g. `\n`) no longer **clears** a row's internal linebreak flag. This fixes an issue where e.g. multi-line prompt input in fish is treated as separate lines, diff --git a/config.c b/config.c index 1f287250..b141f564 100644 --- a/config.c +++ b/config.c @@ -2677,8 +2677,15 @@ parse_section_tweak(struct context *ctx) [FCFT_SCALING_FILTER_NONE] = "none", [FCFT_SCALING_FILTER_NEAREST] = "nearest", [FCFT_SCALING_FILTER_BILINEAR] = "bilinear", + + [FCFT_SCALING_FILTER_IMPULSE] = "impulse", + [FCFT_SCALING_FILTER_BOX] = "box", + [FCFT_SCALING_FILTER_LINEAR] = "linear", [FCFT_SCALING_FILTER_CUBIC] = "cubic", + [FCFT_SCALING_FILTER_GAUSSIAN] = "gaussian", + [FCFT_SCALING_FILTER_LANCZOS2] = "lanczos2", [FCFT_SCALING_FILTER_LANCZOS3] = "lanczos3", + [FCFT_SCALING_FILTER_LANCZOS3_STRETCHED] = "lanczos3-stretched", NULL, }; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 952c7ae2..a75ffc6e 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1716,9 +1716,8 @@ any of these options. *scaling-filter* Overrides the default scaling filter used when down-scaling bitmap fonts (e.g. emoji fonts). Possible values are *none*, *nearest*, - *bilinear*, *cubic* or *lanczos3*. *cubic* and *lanczos3* produce - the best results, but are slower (with *lanczos3* being the best - _and_ slowest). + *bilinear*, *impulse*, *box*, *linear*, *cubic* *gaussian*, + *lanczos2*, *lanczos3* or *lanczos3-stretched*. Default: _lanczos3_. diff --git a/main.c b/main.c index 1a001186..e7183238 100644 --- a/main.c +++ b/main.c @@ -518,7 +518,6 @@ main(int argc, char *const *argv) (enum fcft_log_colorize)log_colorize, as_server && log_syslog, (enum fcft_log_class)log_level); - fcft_set_scaling_filter(conf.tweak.fcft_filter); if (conf_server_socket_path != NULL) { free(conf.server_socket_path); diff --git a/meson.build b/meson.build index 6505460f..251e6fde 100644 --- a/meson.build +++ b/meson.build @@ -146,7 +146,7 @@ if utf8proc.found() endif tllist = dependency('tllist', version: '>=1.1.0', fallback: 'tllist') -fcft = dependency('fcft', version: ['>=3.2.0', '<4.0.0'], fallback: 'fcft') +fcft = dependency('fcft', version: ['>=3.3.0', '<4.0.0'], fallback: 'fcft') wayland_protocols_datadir = wayland_protocols.get_variable('pkgdatadir') diff --git a/terminal.c b/terminal.c index 2ba85276..ae1adb1a 100644 --- a/terminal.c +++ b/terminal.c @@ -1071,6 +1071,7 @@ reload_fonts(struct terminal *term, bool resize_grid) struct fcft_font_options *options = fcft_font_options_create(); + options->scaling_filter = conf->tweak.fcft_filter; options->color_glyphs.format = PIXMAN_a8r8g8b8; options->color_glyphs.srgb_decode = render_do_linear_blending(term); From 16c384b707757e69f3d01ae666f81e7d65a282b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 12 Mar 2025 10:06:13 +0100 Subject: [PATCH 1100/1323] changelog: mention some of the side-effects the new fcft requirement brings --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0843c29d..466e3a91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,6 +93,9 @@ based. * Rename Tokyo Night Day theme to Tokyo Night Light and update colors. * fcft >= 3.3.0 is now required. + - `tweak.scaling-filter` now supports more scaling-filters + - scaled bitmap fonts (when enabled in FontConfig) no longer have a + scaling-filter applied * Linefeed:ing control characters (e.g. `\n`) no longer **clears** a row's internal linebreak flag. This fixes an issue where e.g. multi-line prompt input in fish is treated as separate lines, From a79fd6a7cf585209e3204b87098f34e0c0f5aa45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 13 Mar 2025 13:23:25 +0100 Subject: [PATCH 1101/1323] meson: require fcft-3.3.1 fcft-3.3.0 is not binary compatible with 3.2.x, and earlier. --- CHANGELOG.md | 2 +- meson.build | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 466e3a91..cda805ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,7 +92,7 @@ * Auto-detection of URLs (i.e. not OSC-8 based URLs) are now regex based. * Rename Tokyo Night Day theme to Tokyo Night Light and update colors. -* fcft >= 3.3.0 is now required. +* fcft >= 3.3.1 is now required. - `tweak.scaling-filter` now supports more scaling-filters - scaled bitmap fonts (when enabled in FontConfig) no longer have a scaling-filter applied diff --git a/meson.build b/meson.build index 251e6fde..d84a848c 100644 --- a/meson.build +++ b/meson.build @@ -146,7 +146,7 @@ if utf8proc.found() endif tllist = dependency('tllist', version: '>=1.1.0', fallback: 'tllist') -fcft = dependency('fcft', version: ['>=3.3.0', '<4.0.0'], fallback: 'fcft') +fcft = dependency('fcft', version: ['>=3.3.1', '<4.0.0'], fallback: 'fcft') wayland_protocols_datadir = wayland_protocols.get_variable('pkgdatadir') From d48a1c53f59e8b7c99c18f4e4a5693ca0b10bd52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 12 Mar 2025 17:53:04 +0100 Subject: [PATCH 1102/1323] meson: require wayland-protocols >= 1.41 --- CHANGELOG.md | 1 + client.c | 5 +---- config.c | 14 -------------- foot-features.h | 27 --------------------------- main.c | 5 +---- meson.build | 32 ++++---------------------------- render.c | 9 --------- wayland.c | 33 --------------------------------- wayland.h | 23 +++-------------------- 9 files changed, 10 insertions(+), 139 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cda805ca..541096ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,7 @@ e.g. multi-line prompt input in fish is treated as separate lines, rather than one logical, when selecting and copying it ([#1487][1487]). +* wayland-protocols >= 1.41 is now required. [1925]: https://codeberg.org/dnkl/foot/issues/1925 [1487]: https://codeberg.org/dnkl/foot/issues/1487 diff --git a/client.c b/client.c index ceee1b29..e7df3768 100644 --- a/client.c +++ b/client.c @@ -67,14 +67,11 @@ version_and_features(void) { static char buf[256]; snprintf(buf, sizeof(buf), - "version: %s %cpgo %cime %cgraphemes %ctoplevel-icon %csystem-bell %ccolor-management %cassertions", + "version: %s %cpgo %cime %cgraphemes %cassertions", FOOT_VERSION, feature_pgo() ? '+' : '-', feature_ime() ? '+' : '-', feature_graphemes() ? '+' : '-', - feature_xdg_toplevel_icon() ? '+' : '-', - feature_xdg_system_bell() ? '+' : '-', - feature_wp_color_management() ? '+' : '-', feature_assertions() ? '+' : '-'); return buf; } diff --git a/config.c b/config.c index b141f564..6c8e147f 100644 --- a/config.c +++ b/config.c @@ -1112,21 +1112,11 @@ parse_section_main(struct context *ctx) if (!value_to_bool(ctx, &gamma_correct)) return false; -#if defined(HAVE_WP_COLOR_MANAGEMENT) conf->gamma_correct = gamma_correct ? GAMMA_CORRECT_ENABLED : GAMMA_CORRECT_DISABLED; return true; -#else - if (gamma_correct) { - LOG_CONTEXTUAL_WARN( - "ignoring; foot was built without color-management support"); - } - - conf->gamma_correct = GAMMA_CORRECT_DISABLED; - return true; -#endif } else { @@ -3339,11 +3329,7 @@ config_load(struct config *conf, const char *conf_path, .underline_thickness = {.pt = 0., .px = -1}, .strikeout_thickness = {.pt = 0., .px = -1}, .dpi_aware = false, -#if defined(HAVE_WP_COLOR_MANAGEMENT) .gamma_correct = GAMMA_CORRECT_AUTO, -#else - .gamma_correct = GAMMA_CORRECT_DISABLED, -#endif .security = { .osc52 = OSC52_ENABLED, }, diff --git a/foot-features.h b/foot-features.h index c6c9c6f4..ad447767 100644 --- a/foot-features.h +++ b/foot-features.h @@ -37,30 +37,3 @@ static inline bool feature_graphemes(void) return false; #endif } - -static inline bool feature_xdg_toplevel_icon(void) -{ -#if defined(HAVE_XDG_TOPLEVEL_ICON) - return true; -#else - return false; -#endif -} - -static inline bool feature_xdg_system_bell(void) -{ -#if defined(HAVE_XDG_SYSTEM_BELL) - return true; -#else - return false; -#endif -} - -static inline bool feature_wp_color_management(void) -{ -#if defined(HAVE_WP_COLOR_MANAGEMENT) - return true; -#else - return false; -#endif -} diff --git a/main.c b/main.c index e7183238..7e07038f 100644 --- a/main.c +++ b/main.c @@ -51,14 +51,11 @@ version_and_features(void) { static char buf[256]; snprintf(buf, sizeof(buf), - "version: %s %cpgo %cime %cgraphemes %ctoplevel-icon %csystem-bell %ccolor-management %cassertions", + "version: %s %cpgo %cime %cgraphemes %cassertions", FOOT_VERSION, feature_pgo() ? '+' : '-', feature_ime() ? '+' : '-', feature_graphemes() ? '+' : '-', - feature_xdg_toplevel_icon() ? '+' : '-', - feature_xdg_system_bell() ? '+' : '-', - feature_wp_color_management() ? '+' : '-', feature_assertions() ? '+' : '-'); return buf; } diff --git a/meson.build b/meson.build index d84a848c..3d2ce91a 100644 --- a/meson.build +++ b/meson.build @@ -132,7 +132,7 @@ math = cc.find_library('m') threads = [dependency('threads'), cc.find_library('stdthreads', required: false)] libepoll = dependency('epoll-shim', required: false) pixman = dependency('pixman-1') -wayland_protocols = dependency('wayland-protocols', version: '>=1.32', +wayland_protocols = dependency('wayland-protocols', version: '>=1.41', fallback: 'wayland-protocols', default_options: ['tests=false']) wayland_client = dependency('wayland-client') @@ -169,32 +169,11 @@ wl_proto_xml = [ wayland_protocols_datadir / 'unstable/tablet/tablet-unstable-v2.xml', # required by cursor-shape-v1 wayland_protocols_datadir / 'staging/cursor-shape/cursor-shape-v1.xml', wayland_protocols_datadir / 'staging/single-pixel-buffer/single-pixel-buffer-v1.xml', + wayland_protocols_datadir / 'staging/xdg-toplevel-icon/xdg-toplevel-icon-v1.xml', + wayland_protocols_datadir / 'staging/xdg-system-bell/xdg-system-bell-v1.xml', + wayland_protocols_datadir / 'staging/color-management/color-management-v1.xml', ] -if wayland_protocols.version().version_compare('>=1.37') - add_project_arguments('-DHAVE_XDG_TOPLEVEL_ICON', language: 'c') - wl_proto_xml += [wayland_protocols_datadir / 'staging/xdg-toplevel-icon/xdg-toplevel-icon-v1.xml'] - xdg_toplevel_icon = true -else - xdg_toplevel_icon = false -endif - -if wayland_protocols.version().version_compare('>=1.38') - add_project_arguments('-DHAVE_XDG_SYSTEM_BELL', language: 'c') - wl_proto_xml += [wayland_protocols_datadir / 'staging/xdg-system-bell/xdg-system-bell-v1.xml'] - xdg_system_bell = true -else - xdg_system_bell = false -endif - -if wayland_protocols.version().version_compare('>=1.41') - add_project_arguments('-DHAVE_WP_COLOR_MANAGEMENT', language: 'c') - wl_proto_xml += [wayland_protocols_datadir / 'staging/color-management/color-management-v1.xml'] - wp_color_management = true -else - wp_color_management = false -endif - foreach prot : wl_proto_xml wl_proto_headers += custom_target( prot.underscorify() + '-client-header', @@ -436,9 +415,6 @@ summary( 'Themes': get_option('themes'), 'IME': get_option('ime'), 'Grapheme clustering': utf8proc.found(), - 'Wayland: xdg-toplevel-icon-v1': xdg_toplevel_icon, - 'Wayland: xdg-system-bell-v1': xdg_system_bell, - 'Wayland: wp-color-management-v1': wp_color_management, 'utmp backend': utmp_backend, 'utmp helper default path': utmp_default_helper_path, 'Build terminfo': tic.found(), diff --git a/render.c b/render.c index eea43c10..4975394f 100644 --- a/render.c +++ b/render.c @@ -22,10 +22,7 @@ #include <presentation-time.h> #include <wayland-cursor.h> #include <xdg-shell.h> - -#if defined(HAVE_XDG_TOPLEVEL_ICON) #include <xdg-toplevel-icon-v1.h> -#endif #include <fcft/fcft.h> @@ -5092,7 +5089,6 @@ render_refresh_app_id(struct terminal *term) void render_refresh_icon(struct terminal *term) { -#if defined(HAVE_XDG_TOPLEVEL_ICON) if (term->wl->toplevel_icon_manager == NULL) { LOG_DBG("compositor does not implement xdg-toplevel-icon: " "ignoring request to refresh window icon"); @@ -5126,7 +5122,6 @@ render_refresh_icon(struct terminal *term) xdg_toplevel_icon_v1_destroy(icon); term->render.icon.last_update = now; -#endif } void @@ -5232,10 +5227,6 @@ render_xcursor_set(struct seat *seat, struct terminal *term, bool render_do_linear_blending(const struct terminal *term) { -#if defined(HAVE_WP_COLOR_MANAGEMENT) return term->conf->gamma_correct != GAMMA_CORRECT_DISABLED && term->wl->color_management.img_description != NULL; -#else - return false; -#endif } diff --git a/wayland.c b/wayland.c index 1c083a9e..14d9bed9 100644 --- a/wayland.c +++ b/wayland.c @@ -675,8 +675,6 @@ static const struct wp_presentation_listener presentation_listener = { .clock_id = &clock_id, }; -#if defined(HAVE_WP_COLOR_MANAGEMENT) - static void color_manager_create_image_description(struct wayland *wayl) { @@ -758,7 +756,6 @@ static const struct wp_color_manager_v1_listener color_manager_listener = { .supported_tf_named = &color_manager_supported_tf_named, .done = &color_manager_done, }; -#endif static bool verify_iface_version(const char *iface, uint32_t version, uint32_t wanted) @@ -1457,7 +1454,6 @@ handle_global(void *data, struct wl_registry *registry, &wp_single_pixel_buffer_manager_v1_interface, required); } -#if defined(HAVE_XDG_TOPLEVEL_ICON) else if (streq(interface, xdg_toplevel_icon_v1_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) @@ -1466,9 +1462,7 @@ handle_global(void *data, struct wl_registry *registry, wayl->toplevel_icon_manager = wl_registry_bind( wayl->registry, name, &xdg_toplevel_icon_v1_interface, required); } -#endif -#if defined(HAVE_XDG_SYSTEM_BELL) else if (streq(interface, xdg_system_bell_v1_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) @@ -1477,9 +1471,7 @@ handle_global(void *data, struct wl_registry *registry, wayl->system_bell = wl_registry_bind( wayl->registry, name, &xdg_system_bell_v1_interface, required); } -#endif -#if defined(HAVE_WP_COLOR_MANAGEMENT) else if (streq(interface, wp_color_manager_v1_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) @@ -1491,7 +1483,6 @@ handle_global(void *data, struct wl_registry *registry, wp_color_manager_v1_add_listener( wayl->color_management.manager, &color_manager_listener, wayl); } -#endif #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED else if (streq(interface, zwp_text_input_manager_v3_interface.name)) { @@ -1733,11 +1724,9 @@ wayl_init(struct fdm *fdm, struct key_binding_manager *key_binding_manager, "falling back to client-side cursors"); } -#if defined(HAVE_XDG_TOPLEVEL_ICON) if (wayl->toplevel_icon_manager == NULL) { LOG_WARN("compositor does not implement the XDG toplevel icon protocol"); } -#endif #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED if (wayl->text_input_manager == NULL) { @@ -1815,21 +1804,14 @@ wayl_destroy(struct wayland *wayl) zwp_text_input_manager_v3_destroy(wayl->text_input_manager); #endif -#if defined(HAVE_WP_COLOR_MANAGEMENT) if (wayl->color_management.img_description != NULL) wp_image_description_v1_destroy(wayl->color_management.img_description); if (wayl->color_management.manager != NULL) wp_color_manager_v1_destroy(wayl->color_management.manager); -#endif - -#if defined(HAVE_XDG_SYSTEM_BELL) if (wayl->system_bell != NULL) xdg_system_bell_v1_destroy(wayl->system_bell); -#endif -#if defined(HAVE_XDG_TOPLEVEL_ICON) if (wayl->toplevel_icon_manager != NULL) xdg_toplevel_icon_manager_v1_destroy(wayl->toplevel_icon_manager); -#endif if (wayl->single_pixel_manager != NULL) wp_single_pixel_buffer_manager_v1_destroy(wayl->single_pixel_manager); if (wayl->fractional_scale_manager != NULL) @@ -1947,7 +1929,6 @@ wayl_win_init(struct terminal *term, const char *token) xdg_toplevel_set_app_id(win->xdg_toplevel, conf->app_id); -#if defined(HAVE_XDG_TOPLEVEL_ICON) if (wayl->toplevel_icon_manager != NULL) { const char *app_id = term->app_id != NULL ? term->app_id : term->conf->app_id; @@ -1960,9 +1941,7 @@ wayl_win_init(struct terminal *term, const char *token) wayl->toplevel_icon_manager, win->xdg_toplevel, icon); xdg_toplevel_icon_v1_destroy(icon); } -#endif -#if defined(HAVE_WP_COLOR_MANAGEMENT) if (term->conf->gamma_correct != GAMMA_CORRECT_DISABLED) { if (wayl->color_management.img_description != NULL) { xassert(wayl->color_management.manager != NULL); @@ -1992,7 +1971,6 @@ wayl_win_init(struct terminal *term, const char *token) /* "auto" - don't warn */ } } -#endif if (conf->csd.preferred == CONF_CSD_PREFER_NONE) { /* User specifically do *not* want decorations */ @@ -2136,11 +2114,8 @@ wayl_win_destroy(struct wl_window *win) tll_remove(win->xdg_tokens, it); } -#if defined(HAVE_WP_COLOR_MANAGEMENT) if (win->surface.color_management != NULL) wp_color_management_surface_v1_destroy(win->surface.color_management); -#endif - if (win->fractional_scale != NULL) wp_fractional_scale_v1_destroy(win->fractional_scale); if (win->surface.viewport != NULL) @@ -2417,7 +2392,6 @@ wayl_win_set_urgent(struct wl_window *win) bool wayl_win_ring_bell(const struct wl_window *win) { -#if defined(HAVE_XDG_SYSTEM_BELL) if (win->term->wl->system_bell == NULL) { static bool have_warned = false; @@ -2431,9 +2405,6 @@ wayl_win_ring_bell(const struct wl_window *win) xdg_system_bell_v1_ring(win->term->wl->system_bell, win->surface.surf); return true; -#else - return false; -#endif } bool @@ -2471,7 +2442,6 @@ wayl_win_subsurface_new_with_custom_parent( return false; } -#if defined(HAVE_WP_COLOR_MANAGEMENT) surf->surface.color_management = NULL; if (win->term->conf->gamma_correct && wayl->color_management.img_description != NULL) @@ -2485,7 +2455,6 @@ wayl_win_subsurface_new_with_custom_parent( surf->surface.color_management, wayl->color_management.img_description, WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL); } -#endif struct wl_subsurface *sub = wl_subcompositor_get_subsurface( wayl->sub_compositor, main_surface, parent); @@ -2538,12 +2507,10 @@ wayl_win_subsurface_destroy(struct wayl_sub_surface *surf) if (surf == NULL) return; -#if defined(HAVE_WP_COLOR_MANAGEMENT) if (surf->surface.color_management != NULL) { wp_color_management_surface_v1_destroy(surf->surface.color_management); surf->surface.color_management = NULL; } -#endif if (surf->surface.viewport != NULL) { wp_viewport_destroy(surf->surface.viewport); diff --git a/wayland.h b/wayland.h index ec27281a..6215d708 100644 --- a/wayland.h +++ b/wayland.h @@ -9,6 +9,7 @@ #include <xkbcommon/xkbcommon.h> /* Wayland protocols */ +#include <color-management-v1.h> #include <fractional-scale-v1.h> #include <presentation-time.h> #include <primary-selection-unstable-v1.h> @@ -19,18 +20,8 @@ #include <xdg-decoration-unstable-v1.h> #include <xdg-output-unstable-v1.h> #include <xdg-shell.h> - -#if defined(HAVE_XDG_TOPLEVEL_ICON) - #include <xdg-toplevel-icon-v1.h> -#endif - -#if defined(HAVE_XDG_SYSTEM_BELL) - #include <xdg-system-bell-v1.h> -#endif - -#if defined(HAVE_WP_COLOR_MANAGEMENT) - #include <color-management-v1.h> -#endif +#include <xdg-system-bell-v1.h> +#include <xdg-toplevel-icon-v1.h> #include <fcft/fcft.h> #include <tllist.h> @@ -65,9 +56,7 @@ enum touch_state { struct wayl_surface { struct wl_surface *surf; struct wp_viewport *viewport; -#if defined(HAVE_WP_COLOR_MANAGEMENT) struct wp_color_management_surface_v1 *color_management; -#endif }; struct wayl_sub_surface { @@ -458,15 +447,10 @@ struct wayland { struct wp_single_pixel_buffer_manager_v1 *single_pixel_manager; -#if defined(HAVE_XDG_TOPLEVEL_ICON) struct xdg_toplevel_icon_manager_v1 *toplevel_icon_manager; -#endif -#if defined(HAVE_XDG_SYSTEM_BELL) struct xdg_system_bell_v1 *system_bell; -#endif -#if defined(HAVE_WP_COLOR_MANAGEMENT) struct { struct wp_color_manager_v1 *manager; struct wp_image_description_v1 *img_description; @@ -475,7 +459,6 @@ struct wayland { bool have_tf_ext_linear; bool have_primaries_srgb; } color_management; -#endif bool presentation_timings; struct wp_presentation *presentation; From eb9357709bfd521a87f5fc4232bf37ce360a0317 Mon Sep 17 00:00:00 2001 From: Craig Barnes <craigbarnes@protonmail.com> Date: Fri, 14 Mar 2025 20:15:11 +0000 Subject: [PATCH 1103/1323] main/client: simplify code for printing --version string --- client.c | 17 +---------------- foot-features.c | 30 ++++++++++++++++++++++++++++++ foot-features.h | 40 +++++++--------------------------------- main.c | 19 ++----------------- meson.build | 4 ++-- 5 files changed, 42 insertions(+), 68 deletions(-) create mode 100644 foot-features.c diff --git a/client.c b/client.c index e7df3768..e76f2d51 100644 --- a/client.c +++ b/client.c @@ -22,7 +22,6 @@ #include "foot-features.h" #include "macros.h" #include "util.h" -#include "version.h" #include "xmalloc.h" extern char **environ; @@ -62,20 +61,6 @@ sendall(int sock, const void *_buf, size_t len) return len; } -static const char * -version_and_features(void) -{ - static char buf[256]; - snprintf(buf, sizeof(buf), - "version: %s %cpgo %cime %cgraphemes %cassertions", - FOOT_VERSION, - feature_pgo() ? '+' : '-', - feature_ime() ? '+' : '-', - feature_graphemes() ? '+' : '-', - feature_assertions() ? '+' : '-'); - return buf; -} - static void print_usage(const char *prog_name) { @@ -328,7 +313,7 @@ main(int argc, char *const *argv) break; case 'v': - printf("footclient %s\n", version_and_features()); + print_version_and_features("footclient "); ret = EXIT_SUCCESS; goto err; diff --git a/foot-features.c b/foot-features.c new file mode 100644 index 00000000..1b5bf7fd --- /dev/null +++ b/foot-features.c @@ -0,0 +1,30 @@ +#include "foot-features.h" +#include "version.h" + +const char version_and_features[] = + "version: " FOOT_VERSION + +#if defined(FOOT_PGO_ENABLED) && FOOT_PGO_ENABLED + " +pgo" +#else + " -pgo" +#endif + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + " +ime" +#else + " -ime" +#endif + +#if defined(FOOT_GRAPHEME_CLUSTERING) && FOOT_GRAPHEME_CLUSTERING + " +graphemes" +#else + " -graphemes" +#endif + +#if !defined(NDEBUG) + " +assertions" +#else + " -assertions" +#endif +; diff --git a/foot-features.h b/foot-features.h index ad447767..49cc56ed 100644 --- a/foot-features.h +++ b/foot-features.h @@ -1,39 +1,13 @@ #pragma once -#include <stdbool.h> +#include <stdio.h> -static inline bool feature_assertions(void) -{ -#if defined(NDEBUG) - return false; -#else - return true; -#endif -} +extern const char version_and_features[]; -static inline bool feature_ime(void) +static inline void +print_version_and_features(const char *prefix) { -#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED - return true; -#else - return false; -#endif -} - -static inline bool feature_pgo(void) -{ -#if defined(FOOT_PGO_ENABLED) && FOOT_PGO_ENABLED - return true; -#else - return false; -#endif -} - -static inline bool feature_graphemes(void) -{ -#if defined(FOOT_GRAPHEME_CLUSTERING) && FOOT_GRAPHEME_CLUSTERING - return true; -#else - return false; -#endif + fputs(prefix, stdout); + fputs(version_and_features, stdout); + fputc('\n', stdout); } diff --git a/main.c b/main.c index 7e07038f..b9404503 100644 --- a/main.c +++ b/main.c @@ -31,7 +31,6 @@ #include "shm.h" #include "terminal.h" #include "util.h" -#include "version.h" #include "xmalloc.h" #include "xsnprintf.h" @@ -46,20 +45,6 @@ fdm_sigint(struct fdm *fdm, int signo, void *data) return true; } -static const char * -version_and_features(void) -{ - static char buf[256]; - snprintf(buf, sizeof(buf), - "version: %s %cpgo %cime %cgraphemes %cassertions", - FOOT_VERSION, - feature_pgo() ? '+' : '-', - feature_ime() ? '+' : '-', - feature_graphemes() ? '+' : '-', - feature_assertions() ? '+' : '-'); - return buf; -} - static void print_usage(const char *prog_name) { @@ -377,7 +362,7 @@ main(int argc, char *const *argv) break; case 'v': - printf("foot %s\n", version_and_features()); + print_version_and_features("foot "); return EXIT_SUCCESS; case 'h': @@ -405,7 +390,7 @@ main(int argc, char *const *argv) argv += optind; } - LOG_INFO("%s", version_and_features()); + LOG_INFO("%s", version_and_features); { struct utsname name; diff --git a/meson.build b/meson.build index 3d2ce91a..c8f23dfc 100644 --- a/meson.build +++ b/meson.build @@ -295,7 +295,7 @@ executable( 'commands.c', 'commands.h', 'extract.c', 'extract.h', 'fdm.c', 'fdm.h', - 'foot-features.h', + 'foot-features.c', 'foot-features.h', 'ime.c', 'ime.h', 'input.c', 'input.h', 'key-binding.c', 'key-binding.h', @@ -323,7 +323,7 @@ executable( executable( 'footclient', 'client.c', 'client-protocol.h', - 'foot-features.h', + 'foot-features.c', 'foot-features.h', 'macros.h', 'util.h', version, From cd4ee8ae49f7c3ba1441917decd2e4ebb804e4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 17 Mar 2025 08:43:12 +0100 Subject: [PATCH 1104/1323] ime: fix initial cursor rectangle being reported as 0,0,0,0 Closes #1994 --- CHANGELOG.md | 3 +++ ime.c | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 541096ec..8b55c42f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -143,6 +143,8 @@ * Grapheme clustering state not reset on cursor movements. * Kitty keyboard protocol: no release events emitted for composed keys. +* IME: the initial cursor position was reported as 0,0,0,0 + ([#1994][1994]). [1918]: https://codeberg.org/dnkl/foot/issues/1918 [1929]: https://codeberg.org/dnkl/foot/issues/1929 @@ -151,6 +153,7 @@ [1960]: https://codeberg.org/dnkl/foot/issues/1960 [1956]: https://codeberg.org/dnkl/foot/issues/1956 [1963]: https://codeberg.org/dnkl/foot/issues/1963 +[1994]: https://codeberg.org/dnkl/foot/issues/1994 ### Security diff --git a/ime.c b/ime.c index 54cfa908..c6ccb479 100644 --- a/ime.c +++ b/ime.c @@ -68,6 +68,16 @@ enter(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, /* The main grid is the *only* input-receiving surface we have */ seat->ime_focus = term; + + const struct coord *cursor = &term->grid->cursor.point; + + term_ime_set_cursor_rect( + term, + term->margins.left + cursor->col * term->cell_width, + term->margins.top + cursor->row * term->cell_height, + term->cell_width, + term->cell_height); + ime_enable(seat); } From 7dbfdc73b66a65956b4ca15e3ad7d88ae5a2bac9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 17 Mar 2025 08:51:27 +0100 Subject: [PATCH 1105/1323] doc: foot.init: surface-bit-depth: mention 10-bit surfaces are slow --- doc/foot.ini.5.scd | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index a75ffc6e..a1d3bfc1 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1960,6 +1960,9 @@ any of these options. background, you may want to use the default, *8-bit*, even if you have gamma-correct blending enabled. + You shouuld also note that 10-bit surface is slower. This will + increase input latency and decrease rendering throughput. + Default: _8-bit_ # SEE ALSO From d2ede697f9cb40269399bd2f72bcac11a2b918b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 17 Mar 2025 12:02:57 +0100 Subject: [PATCH 1106/1323] config: remove deprecated options 'notify' and 'notify-focus-inhibit' They've been deprecated since 1.18.0 --- CHANGELOG.md | 2 ++ config.c | 24 ------------------------ tests/test-config.c | 3 --- 3 files changed, 2 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b55c42f..3abc2552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,8 @@ * `url.uri-characters` and `url.protocols`. Both options have been replaced by `url.regex`. +* `notify` option (has been deprecated since 1.18.0). +* `notify-focus-inhibit` option (has been deprecated since 1.18.0). ### Fixed diff --git a/config.c b/config.c index 6c8e147f..222b1b60 100644 --- a/config.c +++ b/config.c @@ -1058,30 +1058,6 @@ parse_section_main(struct context *ctx) else if (streq(key, "word-delimiters")) return value_to_wchars(ctx, &conf->word_delimiters); - else if (streq(key, "notify")) { - user_notification_add( - &conf->notifications, USER_NOTIFICATION_DEPRECATED, - xstrdup("notify: use desktop-notifications.command instead")); - log_msg( - LOG_CLASS_WARNING, LOG_MODULE, __FILE__, __LINE__, - "deprecated: notify: use desktop-notifications.command instead"); - return value_to_spawn_template( - ctx, &conf->desktop_notifications.command); - } - - else if (streq(key, "notify-focus-inhibit")) { - user_notification_add( - &conf->notifications, USER_NOTIFICATION_DEPRECATED, - xstrdup("notify-focus-inhibit: " - "use desktop-notifications.inhibit-when-focused instead")); - log_msg( - LOG_CLASS_WARNING, LOG_MODULE, __FILE__, __LINE__, - "deprecrated: notify-focus-inhibit: " - "use desktop-notifications.inhibit-when-focused instead"); - return value_to_bool( - ctx, &conf->desktop_notifications.inhibit_when_focused); - } - else if (streq(key, "selection-target")) { _Static_assert(sizeof(conf->selection_target) == sizeof(int), "enum is not 32-bit"); diff --git a/tests/test-config.c b/tests/test-config.c index c9f6586c..f431f4ab 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -467,7 +467,6 @@ test_section_main(void) test_boolean(&ctx, &parse_section_main, "login-shell", &conf.login_shell); test_boolean(&ctx, &parse_section_main, "box-drawings-uses-font-glyphs", &conf.box_drawings_uses_font_glyphs); test_boolean(&ctx, &parse_section_main, "locked-title", &conf.locked_title); - test_boolean(&ctx, &parse_section_main, "notify-focus-inhibit", &conf.desktop_notifications.inhibit_when_focused); /* Deprecated */ test_boolean(&ctx, &parse_section_main, "dpi-aware", &conf.dpi_aware); test_pt_or_px(&ctx, &parse_section_main, "font-size-adjustment", &conf.font_size_adjustment.pt_or_px); /* TODO: test ‘N%’ values too */ @@ -481,8 +480,6 @@ test_section_main(void) test_uint16(&ctx, &parse_section_main, "resize-delay-ms", &conf.resize_delay_ms); test_uint16(&ctx, &parse_section_main, "workers", &conf.render_worker_count); - test_spawn_template(&ctx, &parse_section_main, "notify", &conf.desktop_notifications.command); /* Deprecated */ - test_enum(&ctx, &parse_section_main, "selection-target", 4, (const char *[]){"none", "primary", "clipboard", "both"}, From 3eef3ec877fa55e3faf2b6e0c5b7fffc92cab8e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 17 Mar 2025 12:04:46 +0100 Subject: [PATCH 1107/1323] changelog: prepare for 1.21.0 --- CHANGELOG.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3abc2552..5e598010 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.21.0](#1-21-0) * [1.20.2](#1-20-2) * [1.20.1](#1-20-1) * [1.20.0](#1-20-0) @@ -58,7 +58,7 @@ * [1.2.0](#1-2-0) -## Unreleased +## 1.21.0 ### Added @@ -107,7 +107,6 @@ [1487]: https://codeberg.org/dnkl/foot/issues/1487 -### Deprecated ### Removed * `url.uri-characters` and `url.protocols`. Both options have been @@ -158,9 +157,19 @@ [1994]: https://codeberg.org/dnkl/foot/issues/1994 -### Security ### Contributors +* Adrian fxj9a +* Alexander Orzechowski +* Attila Fidan +* camel-cdr +* Craig Barnes +* Guillaume Outters +* Johannes Altmanninger +* Ludovico Gerardi +* sewn +* Thomas Bonnefille + ## 1.20.2 From df32cd0504c59243f06904bb9a6e1b29ccedfbe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 17 Mar 2025 12:04:59 +0100 Subject: [PATCH 1108/1323] meson: bump version to 1.21.0 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index c8f23dfc..e85d95e5 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.20.2', + version: '1.21.0', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From 49d2c08912c6e15ff39f2c7a9e69727e348f7e7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 17 Mar 2025 12:08:27 +0100 Subject: [PATCH 1109/1323] doc: foot.ini: codespell: shouuld -> should --- doc/foot.ini.5.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index a1d3bfc1..7fad2b9c 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1960,7 +1960,7 @@ any of these options. background, you may want to use the default, *8-bit*, even if you have gamma-correct blending enabled. - You shouuld also note that 10-bit surface is slower. This will + You should also note that 10-bit surface is slower. This will increase input latency and decrease rendering throughput. Default: _8-bit_ From 68f5eab0b0fa08becebbed412947ba19246c2518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 17 Mar 2025 12:08:27 +0100 Subject: [PATCH 1110/1323] doc: foot.ini: codespell: shouuld -> should --- doc/foot.ini.5.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index a1d3bfc1..7fad2b9c 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1960,7 +1960,7 @@ any of these options. background, you may want to use the default, *8-bit*, even if you have gamma-correct blending enabled. - You shouuld also note that 10-bit surface is slower. This will + You should also note that 10-bit surface is slower. This will increase input latency and decrease rendering throughput. Default: _8-bit_ From 6813b321f5bd0661eea0af0b9104d615e3d6d0b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 17 Mar 2025 12:15:36 +0100 Subject: [PATCH 1111/1323] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e598010..30f4dc75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.21.0](#1-21-0) * [1.20.2](#1-20-2) * [1.20.1](#1-20-1) @@ -58,6 +59,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.21.0 ### Added From 878e07da59855d62e89eba5f5f479a5ee598998e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 18 Mar 2025 14:37:28 +0100 Subject: [PATCH 1112/1323] vt: utf8: don't discard current byte when an invalid UTF-8 sequence is detected Example: printf "pok\xe9mon\n" would result in 'pokon' - the 'm' has been discarded along with E9. While correct, in some sense, it's perhaps not intuitive. This patch changes the VT parser to instead discard everything up to the invalid byte, but then try the invalid byte from the ground state. This way, invalid UTF-8 sequences followed by both plain ASCII, or longer (and valid) UTF-8 sequences are printed as expected instead of being discarded. --- CHANGELOG.md | 4 ++++ vt.c | 12 ++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30f4dc75..ac022c4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,10 @@ ## Unreleased ### Added ### Changed + +* UTF-8 error recovery now discards fewer bytes. + + ### Deprecated ### Removed ### Fixed diff --git a/vt.c b/vt.c index 9c758c55..173b59a6 100644 --- a/vt.c +++ b/vt.c @@ -1041,7 +1041,7 @@ state_utf8_21_switch(struct terminal *term, uint8_t data) switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_22(term, data); return STATE_GROUND; - default: return STATE_GROUND; + default: return state_ground_switch(term, data); } } @@ -1051,7 +1051,7 @@ state_utf8_31_switch(struct terminal *term, uint8_t data) switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_32(term, data); return STATE_UTF8_32; - default: return STATE_GROUND; + default: return state_ground_switch(term, data); } } @@ -1061,7 +1061,7 @@ state_utf8_32_switch(struct terminal *term, uint8_t data) switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_33(term, data); return STATE_GROUND; - default: return STATE_GROUND; + default: return state_ground_switch(term, data); } } @@ -1071,7 +1071,7 @@ state_utf8_41_switch(struct terminal *term, uint8_t data) switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_42(term, data); return STATE_UTF8_42; - default: return STATE_GROUND; + default: return state_ground_switch(term, data); } } @@ -1081,7 +1081,7 @@ state_utf8_42_switch(struct terminal *term, uint8_t data) switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_43(term, data); return STATE_UTF8_43; - default: return STATE_GROUND; + default: return state_ground_switch(term, data); } } @@ -1091,7 +1091,7 @@ state_utf8_43_switch(struct terminal *term, uint8_t data) switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_44(term, data); return STATE_GROUND; - default: return STATE_GROUND; + default: return state_ground_switch(term, data); } } From a02c0c8d4de96832f2bb3e8c16b8e6e4cf6a9662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 18 Mar 2025 18:28:09 +0100 Subject: [PATCH 1113/1323] vt: utf8: insert a REPLACEMENT CHARACTER when an invalid UTF-8 sequence is detected --- vt.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/vt.c b/vt.c index 173b59a6..1d8297be 100644 --- a/vt.c +++ b/vt.c @@ -1041,7 +1041,7 @@ state_utf8_21_switch(struct terminal *term, uint8_t data) switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_22(term, data); return STATE_GROUND; - default: return state_ground_switch(term, data); + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); } } @@ -1051,7 +1051,7 @@ state_utf8_31_switch(struct terminal *term, uint8_t data) switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_32(term, data); return STATE_UTF8_32; - default: return state_ground_switch(term, data); + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); } } @@ -1061,7 +1061,7 @@ state_utf8_32_switch(struct terminal *term, uint8_t data) switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_33(term, data); return STATE_GROUND; - default: return state_ground_switch(term, data); + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); } } @@ -1071,7 +1071,7 @@ state_utf8_41_switch(struct terminal *term, uint8_t data) switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_42(term, data); return STATE_UTF8_42; - default: return state_ground_switch(term, data); + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); } } @@ -1081,7 +1081,7 @@ state_utf8_42_switch(struct terminal *term, uint8_t data) switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_43(term, data); return STATE_UTF8_43; - default: return state_ground_switch(term, data); + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); } } @@ -1091,7 +1091,7 @@ state_utf8_43_switch(struct terminal *term, uint8_t data) switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_44(term, data); return STATE_GROUND; - default: return state_ground_switch(term, data); + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); } } From cc99db5bc4f02fffe0dde7483fb91a8056e112d8 Mon Sep 17 00:00:00 2001 From: llyyr <llyyr.public@gmail.com> Date: Wed, 19 Mar 2025 10:06:38 +0530 Subject: [PATCH 1114/1323] url-mode: fix crash when opening multiple urls with persist mode Fixes: 051cd6ecfc8b98e1d80b399ebea40207fe040750 Closes #2000 --- url-mode.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/url-mode.c b/url-mode.c index f04550f8..ed260597 100644 --- a/url-mode.c +++ b/url-mode.c @@ -85,8 +85,6 @@ spawn_url_launcher_with_token(struct terminal *term, free(argv); } - term->url_launch = NULL; - close(dev_null); return ret; } From 5f72f51ae8671d232f7fdc8c1ce42b537d73c409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 20 Mar 2025 08:51:43 +0100 Subject: [PATCH 1115/1323] changelog: url-mode: show-urls-persistent regression fix --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac022c4f..7ed4bd7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,13 @@ ### Deprecated ### Removed ### Fixed + +* Regression: assertion in `url-mode.c` when activating a second URL + via `show-urls-persistent` ([#2000][2000]). + +[2000]: https://codeberg.org/dnkl/foot/issues/2000 + + ### Security ### Contributors From 663c9082db4dfe43bb329a09fd5b93a6ba99fdb6 Mon Sep 17 00:00:00 2001 From: Sam McCall <sam.mccall@gmail.com> Date: Sat, 22 Mar 2025 20:11:23 +0100 Subject: [PATCH 1116/1323] render: dim and brighten using linear rgb interpolation Adds setting tweak.dim-amount, similar to bold-text-in-bright-amount. Closes #2006 --- CHANGELOG.md | 6 +++++ config.c | 4 ++++ config.h | 4 ++++ doc/foot.ini.5.scd | 11 +++++---- hsl.c | 35 ----------------------------- hsl.h | 1 - render.c | 56 +++++++++++++++++++++++++--------------------- 7 files changed, 51 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ed4bd7a..671b6dad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,12 @@ ### Changed * UTF-8 error recovery now discards fewer bytes. +* Auto-calculated dimmed and brightened colors (e.g. when custom dim + colors has not configured) is now done by linear RGB interpolation, + rather than converting to HSL and adjusting the luminance + ([#2006][2006]). + +[2006]: https://codeberg.org/dnkl/foot/issues/2006 ### Deprecated diff --git a/config.c b/config.c index 222b1b60..a8cdb34a 100644 --- a/config.c +++ b/config.c @@ -2759,6 +2759,9 @@ parse_section_tweak(struct context *ctx) else if (streq(key, "sixel")) return value_to_bool(ctx, &conf->tweak.sixel); + else if (streq(key, "dim-amount")) + return value_to_float(ctx, &conf->dim.amount); + else if (streq(key, "bold-text-in-bright-amount")) return value_to_float(ctx, &conf->bold_in_bright.amount); @@ -3288,6 +3291,7 @@ config_load(struct config *conf, const char *conf_path, .resize_by_cells = true, .resize_keep_grid = true, .resize_delay_ms = 100, + .dim = { .amount = 1.5 }, .bold_in_bright = { .enabled = false, .palette_based = false, diff --git a/config.h b/config.h index fb019d90..a08fae31 100644 --- a/config.h +++ b/config.h @@ -155,6 +155,10 @@ struct config { uint16_t resize_delay_ms; + struct { + float amount; + } dim; + struct { bool enabled; bool palette_based; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 7fad2b9c..2a065a0c 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -396,7 +396,7 @@ empty string to be set, but it must be quoted: *KEY=""*) *bold-text-in-bright* Semi-boolean. When enabled, bold text is rendered in a brighter color (in addition to using a bold font). The color is brightened - by increasing its luminance. + by blending it with white. If set to *palette-based*, rather than a simple *yes|true*, colors matching one of the 8 regular palette colors will be brightened @@ -986,8 +986,8 @@ can configure the background transparency with the _alpha_ option. an entry in the color palette. Applications emit them by combining a color value, and a "dim" attribute. - By default, foot implements this by reducing the luminance of the - current color. This is a generic approach that applies to both + By default, foot implements this by blending the current color + with black. This is a generic approach that applies to both colors from the 256-color palette, as well as 24-bit RGB colors. You can change this behavior by setting the *dimN* options. When @@ -999,7 +999,7 @@ can configure the background transparency with the _alpha_ option. the corresponding *regularN* color will be used. If the current color does not match any known color, it is dimmed - by reducing the luminance (i.e. the same behavior as if the *dimN* + by blending with black (i.e. the same behavior as if the *dimN* options are unconfigured). 24-bit RGB colors will typically fall into this category. @@ -1940,6 +1940,9 @@ any of these options. Boolean. When enabled, foot will process sixel images. Default: _yes_ +*dim-amount* + Amount by which dimmed text is darkened. Default: _1.5_. + *bold-text-in-bright-amount* Amount by which bold fonts are brightened when *bold-text-in-bright* is set to *yes* (the *palette-based* variant diff --git a/hsl.c b/hsl.c index d5d00e67..1a8c919e 100644 --- a/hsl.c +++ b/hsl.c @@ -2,41 +2,6 @@ #include <math.h> -#include "util.h" - -void -rgb_to_hsl(uint32_t rgb, int *hue, int *sat, int *lum) -{ - double r = (double)((rgb >> 16) & 0xff) / 255.; - double g = (double)((rgb >> 8) & 0xff) / 255.; - double b = (double)((rgb >> 0) & 0xff) / 255.; - - double x_max = max(max(r, g), b); - double x_min = min(min(r, g), b); - double V = x_max; - - double C = x_max - x_min; - double L = (x_max + x_min) / 2.; - - *lum = 100 * L; - - if (C == 0.0) - *hue = 0; - else if (V == r) - *hue = 60. * (0. + (g - b) / C); - else if (V == g) - *hue = 60. * (2. + (b - r) / C); - else if (V == b) - *hue = 60. * (4. + (r - g) / C); - if (*hue < 0) - *hue += 360; - - double S = C == 0.0 - ? 0 - : C / (1. - fabs(2. * L - 1.)); - *sat = 100 * S; -} - uint32_t hsl_to_rgb(int hue, int sat, int lum) { diff --git a/hsl.h b/hsl.h index 2a46c117..1aaf7e66 100644 --- a/hsl.h +++ b/hsl.h @@ -2,5 +2,4 @@ #include <stdint.h> -void rgb_to_hsl(uint32_t rgb, int *hue, int *sat, int *lum); uint32_t hsl_to_rgb(int hue, int sat, int lum); diff --git a/render.c b/render.c index 4975394f..d2202468 100644 --- a/render.c +++ b/render.c @@ -34,7 +34,6 @@ #include "config.h" #include "cursor-shape.h" #include "grid.h" -#include "hsl.h" #include "ime.h" #include "quirks.h" #include "search.h" @@ -271,13 +270,23 @@ color_hex_to_pixman(uint32_t color, bool srgb) return color_hex_to_pixman_with_alpha(color, 0xffff, srgb); } +static inline int i_lerp(int from, int to, float t) { + return from + (to - from) * t; +} + static inline uint32_t -color_decrease_luminance(uint32_t color) +color_blend_towards(uint32_t from, uint32_t to, float amount) { - uint32_t alpha = color & 0xff000000; - int hue, sat, lum; - rgb_to_hsl(color, &hue, &sat, &lum); - return alpha | hsl_to_rgb(hue, sat, lum / 1.5); + if (unlikely(amount == 0)) + return from; + float t = 1 - 1/amount; + + uint32_t alpha = from & 0xff000000; + uint8_t r = i_lerp((from>>16)&0xff, (to>>16)&0xff, t); + uint8_t g = i_lerp((from>>8)&0xff, (to>>8)&0xff, t); + uint8_t b = i_lerp((from>>0)&0xff, (to>>0)&0xff, t); + + return alpha | (r<<16) | (g<<8) | (b<<0); } static inline uint32_t @@ -286,25 +295,24 @@ color_dim(const struct terminal *term, uint32_t color) const struct config *conf = term->conf; const uint8_t custom_dim = conf->colors.use_custom.dim; - if (likely(custom_dim == 0)) - return color_decrease_luminance(color); + if (unlikely(custom_dim != 0)) { + for (size_t i = 0; i < 8; i++) { + if (((custom_dim >> i) & 1) == 0) + continue; - for (size_t i = 0; i < 8; i++) { - if (((custom_dim >> i) & 1) == 0) - continue; + if (term->colors.table[0 + i] == color) { + /* "Regular" color, return the corresponding "dim" */ + return conf->colors.dim[i]; + } - if (term->colors.table[0 + i] == color) { - /* "Regular" color, return the corresponding "dim" */ - return conf->colors.dim[i]; - } - - else if (term->colors.table[8 + i] == color) { - /* "Bright" color, return the corresponding "regular" */ - return term->colors.table[i]; + else if (term->colors.table[8 + i] == color) { + /* "Bright" color, return the corresponding "regular" */ + return term->colors.table[i]; + } } } - return color_decrease_luminance(color); + return color_blend_towards(color, 0x00000000, conf->dim.amount); } static inline uint32_t @@ -322,11 +330,7 @@ color_brighten(const struct terminal *term, uint32_t color) return color; } - int hue, sat, lum; - rgb_to_hsl(color, &hue, &sat, &lum); - - lum = (int)roundf(lum * term->conf->bold_in_bright.amount); - return hsl_to_rgb(hue, sat, min(lum, 100)); + return color_blend_towards(color, 0x00ffffff, term->conf->bold_in_bright.amount); } static void @@ -798,7 +802,7 @@ render_cell(struct terminal *term, pixman_image_t *pix, _fg = color_brighten(term, _fg); if (cell->attrs.blink && term->blink.state == BLINK_OFF) - _fg = color_decrease_luminance(_fg); + _fg = color_blend_towards(_fg, 0x00000000, term->conf->dim.amount); const bool gamma_correct = render_do_linear_blending(term); pixman_color_t fg = color_hex_to_pixman(_fg, gamma_correct); From 6922ab2b8efa1422f88730b6bc7ba09fff39996e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 23 Mar 2025 17:00:19 +0100 Subject: [PATCH 1117/1323] doc: foot.ini: gamma-correct: move section --- doc/foot.ini.5.scd | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 2a065a0c..c663223d 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -208,11 +208,6 @@ empty string to be set, but it must be quoted: *KEY=""*) background will appear thicker, and dark glyphs on a light background will appear thinner. - Also be aware that many fonts have been developed on systems that - do not do gamma-correct blending, and may therefore look thicker - than intended when rendered with gamma-correct blending, since the - font designer set the font weight based on incorrect rendering. - FreeType can limit the effect of the latter, with a technique called stem darkening. It is only available for CFF fonts (OpenType, .otf) and disabled by default (in FreeType). You can @@ -220,6 +215,11 @@ empty string to be set, but it must be quoted: *KEY=""*) *FREETYPE_PROPERTIES="cff:no-stem-darkening=0"* before starting foot. + Also be aware that many fonts have been developed on systems that + do not do gamma-correct blending, and may therefore look thicker + than intended when rendered with gamma-correct blending, since the + font designer set the font weight based on incorrect rendering. + You may also want to enable 10-bit image buffers when gamma-correct blending is enabled. Though probably only if you do not use a transparent background (with 10-bit buffers, you only From 9b776f2d6de39569670dbd76f635c11a383b8971 Mon Sep 17 00:00:00 2001 From: "Alex Xu (Hello71)" <alex_y_xu@yahoo.ca> Date: Mon, 17 Mar 2025 16:51:53 -0400 Subject: [PATCH 1118/1323] meson: add foot (render.c) -> srgb.h dep otherwise, depending on ninja dependency resolution order and parallel build, srgb.h may not be built in time Fixes: ccf625b991 ("render: gamma-correct blending") --- CHANGELOG.md | 1 + meson.build | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 671b6dad..1b36e86b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,7 @@ * Regression: assertion in `url-mode.c` when activating a second URL via `show-urls-persistent` ([#2000][2000]). +* Build failure (`srgb.h` not found) when doing a parallel build. [2000]: https://codeberg.org/dnkl/foot/issues/2000 diff --git a/meson.build b/meson.build index e85d95e5..a9e47b3b 100644 --- a/meson.build +++ b/meson.build @@ -314,7 +314,7 @@ executable( 'url-mode.c', 'url-mode.h', 'user-notification.c', 'user-notification.h', 'wayland.c', 'wayland.h', 'shm-formats.h', - wl_proto_src + wl_proto_headers, version, + srgb_funcs, wl_proto_src + wl_proto_headers, version, dependencies: [math, threads, libepoll, pixman, wayland_client, wayland_cursor, xkb, fontconfig, utf8proc, tllist, fcft], link_with: pgolib, From c8470f40c1f5a850048a5b296a944e07abae716a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 29 Mar 2025 10:15:13 +0100 Subject: [PATCH 1119/1323] grid: reflow: fix empty line coalescing If a range of empty lines ended with a non-empty line at the very bottom of the to-be-resized grid, all those empty lines were removed. Closes #2011 --- CHANGELOG.md | 3 +++ grid.c | 6 ++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b36e86b..466947cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,8 +79,11 @@ * Regression: assertion in `url-mode.c` when activating a second URL via `show-urls-persistent` ([#2000][2000]). * Build failure (`srgb.h` not found) when doing a parallel build. +* Regression: reflowing (changing the window size) removing empty + lines ([#2011][2011]). [2000]: https://codeberg.org/dnkl/foot/issues/2000 +[2011]: https://codeberg.org/dnkl/foot/issues/2011 ### Security diff --git a/grid.c b/grid.c index cb6e1a91..df7ef61c 100644 --- a/grid.c +++ b/grid.c @@ -985,14 +985,12 @@ grid_resize_and_reflow( underline_range = underline_range_terminator = NULL; if (unlikely(col_count > 0 && coalesced_linebreaks > 0)) { - for (size_t apa = 0; apa < coalesced_linebreaks; apa++) { + for (size_t line_no = 0; line_no < coalesced_linebreaks; line_no++) { /* Erase the remaining cells */ memset(&new_row->cells[new_col_idx], 0, (new_cols - new_col_idx) * sizeof(new_row->cells[0])); new_row->linebreak = true; - - if (r + 1 < old_rows) - line_wrap(); + line_wrap(); } coalesced_linebreaks = 0; From 58910856c8a86095883625009a089cef464edc44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 29 Mar 2025 10:34:40 +0100 Subject: [PATCH 1120/1323] input: xkb: ignore virtual modifiers Some compositors (mutter/GNOME is one) adds _virtual_ modifiers to the set of active modifiers when e.g. Alt, Meta, Super or Hyper is pressed. For example, pressing Alt+b would result in *both* the Alt *and* the Mod1 modifier being set. Since foot makes close to zero assumptions on how the modifiers should be interpreted, this causes various breakages. For example, a foot shortcut defined as Mod1+b will not match, since the Alt modifiers is also set. This has forced users to redefine/override some of the default key bindings to include the additional modifiers. It also causes issues with the kitty keyboard protocol, for some key combinations. Mainly whether or not to use unshifted key or not, resulting in incorrect escape sequences. Since all the "real" modifiers are always set as well, we can safely ignore the virtual modifiers. Closes #2009 --- CHANGELOG.md | 12 ++++++++++++ input.c | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ wayland.h | 2 ++ 3 files changed, 66 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 466947cd..92b7524f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,8 +68,20 @@ colors has not configured) is now done by linear RGB interpolation, rather than converting to HSL and adjusting the luminance ([#2006][2006]). +* XKB: virtual modifiers are now ignored. This works around various + issues seen when running foot under mutter (GNOME) ([#2009][2009]): + - Some key combinations generating the wrong escape sequence in the + kitty keyboard protocol. + - some of foot's default shortcuts not working (mainly those using + `Mod1`) out of the box. + - **Note: if you have custom key bindings in `foot.ini` that + includes one or more of the `Alt`, `Meta`, `Super`, `Hyper`, + `NumLock`, `ScrollLock`, `LevelThree` or `LevelFive` modifiers, + you need to update them; i.e. remove the virtual modifier(s), + leaving only the real modifiers (`Mod1`, `Mod2` etc).** [2006]: https://codeberg.org/dnkl/foot/issues/2006 +[2009]: https://codeberg.org/dnkl/foot/issues/2009 ### Deprecated diff --git a/input.c b/input.c index cbe8e1c7..abaac8eb 100644 --- a/input.c +++ b/input.c @@ -620,6 +620,54 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, if (seat->kbd.mod_num != XKB_MOD_INVALID) seat->kbd.kitty_significant |= 1 << seat->kbd.mod_num; + + /* + * Create a mask of all "virtual" modifiers. Some compositors + * add these *in addition* to the "real" modifiers (Mod1, + * Mod2, etc). + * + * Since our modifier logic (both for internal shortcut + * processing, and e.g. the kitty keyboard protocol) makes + * very few assumptions on available modifiers, which keys map + * to which modifier etc, the presence of virtual modifiers + * causes various things to break. + * + * For example, if a foot shortcut is Mod1+b (i.e. Alt+b), it + * won't match if the compositor _also_ sets the Alt modifier + * (the corresponding shortcut in foot would be Alt+Mod1+b). + * + * See https://codeberg.org/dnkl/foot/issues/2009 + * + * Mutter (GNOME) is known to set the virtual modifiers in + * addtiion to the real modifiers. + * + * As far as I know, there's no compositor that _only_ sets + * virtual modifiers (don't think that's even legal...?) + */ + { + xkb_mod_index_t alt = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_ALT); + xkb_mod_index_t meta = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_META); + xkb_mod_index_t super = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_SUPER); + xkb_mod_index_t hyper = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_HYPER); + xkb_mod_index_t num_lock = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_NUM); + xkb_mod_index_t scroll_lock = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_SCROLL); + xkb_mod_index_t level_three = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_LEVEL3); + xkb_mod_index_t level_five = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_LEVEL5); + + xkb_mod_index_t ignore = 0; + + if (alt != XKB_MOD_INVALID) ignore |= 1 << alt; + if (meta != XKB_MOD_INVALID) ignore |= 1 << meta; + if (super != XKB_MOD_INVALID) ignore |= 1 << super; + if (hyper != XKB_MOD_INVALID) ignore |= 1 << hyper; + if (num_lock != XKB_MOD_INVALID) ignore |= 1 << num_lock; + if (scroll_lock != XKB_MOD_INVALID) ignore |= 1 << scroll_lock; + if (level_three != XKB_MOD_INVALID) ignore |= 1 << level_three; + if (level_five != XKB_MOD_INVALID) ignore |= 1 << level_five; + + seat->kbd.virtual_modifiers = ignore; + } + seat->kbd.key_arrow_up = xkb_keymap_key_by_name(seat->kbd.xkb_keymap, "UP"); seat->kbd.key_arrow_down = xkb_keymap_key_by_name(seat->kbd.xkb_keymap, "DOWN"); } @@ -1759,6 +1807,10 @@ keyboard_modifiers(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, { struct seat *seat = data; + mods_depressed &= ~seat->kbd.virtual_modifiers; + mods_latched &= ~seat->kbd.virtual_modifiers; + mods_locked &= ~seat->kbd.virtual_modifiers; + #if defined(_DEBUG) char depressed[256]; char latched[256]; diff --git a/wayland.h b/wayland.h index 6215d708..37dd7860 100644 --- a/wayland.h +++ b/wayland.h @@ -136,6 +136,8 @@ struct seat { xkb_mod_mask_t legacy_significant; /* Significant modifiers for the legacy keyboard protocol */ xkb_mod_mask_t kitty_significant; /* Significant modifiers for the kitty keyboard protocol */ + xkb_mod_mask_t virtual_modifiers; /* Set of modifiers to completely ignore */ + xkb_keycode_t key_arrow_up; xkb_keycode_t key_arrow_down; From dc99cf735888d0aba31194b8c59e92eed7e1bc4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 31 Mar 2025 10:11:30 +0200 Subject: [PATCH 1121/1323] key-binding: recognize virtual modifiers, and translate to the corresponding real modifier. --- CHANGELOG.md | 9 ++++-- key-binding.c | 90 +++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 90 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92b7524f..7dcc8372 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,8 +68,9 @@ colors has not configured) is now done by linear RGB interpolation, rather than converting to HSL and adjusting the luminance ([#2006][2006]). -* XKB: virtual modifiers are now ignored. This works around various - issues seen when running foot under mutter (GNOME) ([#2009][2009]): +* XKB: virtual modifiers in keyboard events from the compositor are + now ignored. This works around various issues seen when running foot + under mutter (GNOME) ([#2009][2009]): - Some key combinations generating the wrong escape sequence in the kitty keyboard protocol. - some of foot's default shortcuts not working (mainly those using @@ -79,6 +80,10 @@ `NumLock`, `ScrollLock`, `LevelThree` or `LevelFive` modifiers, you need to update them; i.e. remove the virtual modifier(s), leaving only the real modifiers (`Mod1`, `Mod2` etc).** +* Virtual modifiers (e.g. `Alt` instead of `Mod1`, `Super` instead of + `Mod4` etc) in key bindings are now recognized as being virtual, and + are automatically mapped to the corresponding real modifier. This + means you can use e.g. `Alt+b` instead of `Mod1+b`. [2006]: https://codeberg.org/dnkl/foot/issues/2006 [2009]: https://codeberg.org/dnkl/foot/issues/2009 diff --git a/key-binding.c b/key-binding.c index 1c131e72..e5b7ac81 100644 --- a/key-binding.c +++ b/key-binding.c @@ -13,12 +13,21 @@ #include "wayland.h" #include "xmalloc.h" +struct vmod_map { + const char *name; + xkb_mod_mask_t virtual_mask; + xkb_mod_mask_t real_mask; +}; + struct key_set { struct key_binding_set public; const struct config *conf; const struct seat *seat; size_t conf_ref_count; + + /* Virtual to real modifier mappings */ + struct vmod_map vmods[8]; }; typedef tll(struct key_set) bind_set_list_t; @@ -44,6 +53,50 @@ key_binding_manager_destroy(struct key_binding_manager *mgr) free(mgr); } +static void +initialize_vmod_mappings(struct key_set *set) +{ + if (set->seat == NULL || set->seat->kbd.xkb_keymap == NULL) + return; + + set->vmods[0].name = XKB_VMOD_NAME_ALT; + set->vmods[1].name = XKB_VMOD_NAME_HYPER; + set->vmods[2].name = XKB_VMOD_NAME_LEVEL3; + set->vmods[3].name = XKB_VMOD_NAME_LEVEL5; + set->vmods[4].name = XKB_VMOD_NAME_META; + set->vmods[5].name = XKB_VMOD_NAME_NUM; + set->vmods[6].name = XKB_VMOD_NAME_SCROLL; + set->vmods[7].name = XKB_VMOD_NAME_SUPER; + + struct xkb_state *scratch_state = xkb_state_new(set->seat->kbd.xkb_keymap); + xassert(scratch_state != NULL); + + for (size_t i = 0; i < ALEN(set->vmods); i++) { + xkb_mod_index_t virt_idx = xkb_keymap_mod_get_index( + set->seat->kbd.xkb_keymap, set->vmods[i].name); + + if (virt_idx != XKB_MOD_INVALID) { + xkb_mod_mask_t vmask = 1 << virt_idx; + xkb_state_update_mask(scratch_state, vmask, 0, 0, 0, 0, 0); + set->vmods[i].real_mask = xkb_state_serialize_mods( + scratch_state, XKB_STATE_MODS_DEPRESSED) & ~vmask; + set->vmods[i].virtual_mask = vmask; + + LOG_DBG("%s: 0x%04x -> 0x%04x", + set->vmods[i].name, + set->vmods[i].virtual_mask, + set->vmods[i].real_mask); + } else { + set->vmods[i].virtual_mask = 0; + set->vmods[i].real_mask = 0; + + LOG_DBG("%s: virtual modifier not available", set->vmods[i].name); + } + } + + xkb_state_unref(scratch_state); +} + void key_binding_new_for_seat(struct key_binding_manager *mgr, const struct seat *seat) @@ -67,6 +120,7 @@ key_binding_new_for_seat(struct key_binding_manager *mgr, }; tll_push_back(mgr->binding_sets, set); + initialize_vmod_mappings(&tll_back(mgr->binding_sets)); LOG_DBG("new (seat): set=%p, seat=%p, conf=%p, ref-count=1", (void *)&tll_back(mgr->binding_sets), @@ -107,6 +161,7 @@ key_binding_new_for_conf(struct key_binding_manager *mgr, }; tll_push_back(mgr->binding_sets, set); + initialize_vmod_mappings(&tll_back(mgr->binding_sets)); load_keymap(&tll_back(mgr->binding_sets)); @@ -405,18 +460,35 @@ sort_binding_list(key_binding_list_t *list) } static xkb_mod_mask_t -mods_to_mask(const struct seat *seat, const config_modifier_list_t *mods) +mods_to_mask(const struct seat *seat, + const struct vmod_map *vmods, size_t vmod_count, + const config_modifier_list_t *mods) { xkb_mod_mask_t mask = 0; tll_foreach(*mods, it) { - xkb_mod_index_t idx = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, it->item); + const xkb_mod_index_t idx = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, it->item); if (idx == XKB_MOD_INVALID) { LOG_ERR("%s: invalid modifier name", it->item); continue; } - mask |= 1 << idx; + xkb_mod_mask_t mod = 1 << idx; + + /* Check if this is a virtual modifier, and if so, use the + real modifier it maps to instead */ + for (size_t i = 0; i < vmod_count; i++) { + if (vmods[i].virtual_mask == mod) { + mask |= vmods[i].real_mask; + mod = 0; + + LOG_DBG("%s: virtual modifier, mapped to 0x%04x", + it->item, vmods[i].real_mask); + break; + } + } + + mask |= mod; } return mask; @@ -429,7 +501,8 @@ convert_key_binding(struct key_set *set, { const struct seat *seat = set->seat; - xkb_mod_mask_t mods = mods_to_mask(seat, &conf_binding->modifiers); + xkb_mod_mask_t mods = mods_to_mask( + seat, set->vmods, ALEN(set->vmods), &conf_binding->modifiers); xkb_keysym_t sym = maybe_repair_key_combo(seat, conf_binding->k.sym, mods); struct key_binding binding = { @@ -487,7 +560,7 @@ convert_mouse_binding(struct key_set *set, .type = MOUSE_BINDING, .action = conf_binding->action, .aux = &conf_binding->aux, - .mods = mods_to_mask(set->seat, &conf_binding->modifiers), + .mods = mods_to_mask(set->seat, set->vmods, ALEN(set->vmods), &conf_binding->modifiers), .m = { .button = conf_binding->m.button, .count = conf_binding->m.count, @@ -528,7 +601,8 @@ load_keymap(struct key_set *set) convert_mouse_bindings(set); set->public.selection_overrides = mods_to_mask( - set->seat, &set->conf->mouse.selection_override_modifiers); + set->seat, set->vmods, ALEN(set->vmods), + &set->conf->mouse.selection_override_modifiers); } void @@ -538,8 +612,10 @@ key_binding_load_keymap(struct key_binding_manager *mgr, tll_foreach(mgr->binding_sets, it) { struct key_set *set = &it->item; - if (set->seat == seat) + if (set->seat == seat) { + initialize_vmod_mappings(set); load_keymap(set); + } } } From a43614f098236adf3b876cf8d6fbf1e72a1297ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 31 Mar 2025 10:13:19 +0200 Subject: [PATCH 1122/1323] doc: foot.ini: mention virtual modifiers are allowed --- doc/foot.ini.5.scd | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index c663223d..043600d2 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1160,7 +1160,8 @@ Note that if *Shift* is one of the modifiers, the _key_ *must not* be in upper case. For example, *Control+Shift+V* will never trigger, but *Control+Shift+v* will. -Note that *Alt* is usually called *Mod1*. +The default key bindings all use "real" modifiers (*Mod1*, *Mod4* +etc), but "virtual" modifiers (*Alt*, *Super* etc) are allowed. *xkbcli interactive-wayland* can be useful for finding keysym names. From 0d8c7db962f43255ed0c2e98d9a36253bdddbd3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 31 Mar 2025 11:08:22 +0200 Subject: [PATCH 1123/1323] changelog: reword, and remove section that no longer applies --- CHANGELOG.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dcc8372..bc297e6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,18 +68,13 @@ colors has not configured) is now done by linear RGB interpolation, rather than converting to HSL and adjusting the luminance ([#2006][2006]). -* XKB: virtual modifiers in keyboard events from the compositor are - now ignored. This works around various issues seen when running foot +* Virtual modifiers in keyboard events from the compositor are now + supported. This works around various issues seen when running foot under mutter (GNOME) ([#2009][2009]): - Some key combinations generating the wrong escape sequence in the kitty keyboard protocol. - some of foot's default shortcuts not working (mainly those using `Mod1`) out of the box. - - **Note: if you have custom key bindings in `foot.ini` that - includes one or more of the `Alt`, `Meta`, `Super`, `Hyper`, - `NumLock`, `ScrollLock`, `LevelThree` or `LevelFive` modifiers, - you need to update them; i.e. remove the virtual modifier(s), - leaving only the real modifiers (`Mod1`, `Mod2` etc).** * Virtual modifiers (e.g. `Alt` instead of `Mod1`, `Super` instead of `Mod4` etc) in key bindings are now recognized as being virtual, and are automatically mapped to the corresponding real modifier. This From 1760cb6ab82b355a0751f614f3b45c7446e23e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 2 Apr 2025 08:41:46 +0200 Subject: [PATCH 1124/1323] config: update default URL regex The old one is in some cases too liberal. The new one is stricter in two ways: 1. The protocol list is now explicit, rather than matching anything:// 2. Allowed characters are now limited to the "safe character set", the "reserved character set", and some from the "unsafe character set" Furthermore, some of the characters are restricted in how/when they are allowed: 1. Periods, commas, question marks etc are allowed inside an URL, but not at the end. 2. [ ], ( ), " " and ' ' are allowed but only when balanced. This allows us to match e.g. [http://foo.bar/foo[bar]] correctly. Closes #2016 --- CHANGELOG.md | 5 ++++ config.c | 69 +++++++++++++++++++++++++--------------------- doc/foot.ini.5.scd | 2 +- foot.ini | 2 +- 4 files changed, 45 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc297e6c..3e421014 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,9 +79,14 @@ `Mod4` etc) in key bindings are now recognized as being virtual, and are automatically mapped to the corresponding real modifier. This means you can use e.g. `Alt+b` instead of `Mod1+b`. +* Default URL regex changed to a much more strict variant + ([#2016][2016]). You can manually set the [old + one](https://codeberg.org/dnkl/foot/src/tag/1.21.0/foot.ini#L72), if + you prefer it over the new regex. [2006]: https://codeberg.org/dnkl/foot/issues/2006 [2009]: https://codeberg.org/dnkl/foot/issues/2009 +[2016]: https://codeberg.org/dnkl/foot/issues/2016 ### Deprecated diff --git a/config.c b/config.c index a8cdb34a..bfd3ffed 100644 --- a/config.c +++ b/config.c @@ -3446,39 +3446,46 @@ config_load(struct config *conf, const char *conf_path, tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args); { - /* - * Based on https://gist.github.com/gruber/249502, but modified: - * - Do not allow {} at all - * - Do allow matched [] - */ - const char *url_regex_string = + const char *url_regex_string = + "(" "(" - "(" - "[a-z][[:alnum:]-]+:" // protocol - "(" - "/{1,3}|[a-z0-9%]" // slashes (what's the OR part for?) - ")" - "|" - "www[:digit:]{0,3}[.]" - //"|" - //"[a-z0-9.\\-]+[.][a-z]{2,4}/" /* "looks like domain name followed by a slash" - remove? */ - ")" - "(" - "[^[:space:](){}<>]+" - "|" - "\\(([^[:space:](){}<>]+|(\\([^[:space:](){}<>]+\\)))*\\)" - "|" - "\\[([^]\\[[:space:](){}<>]+|(\\[[^]\\[[:space:](){}<>]+\\]))*\\]" - ")+" - "(" - "\\(([^[:space:](){}<>]+|(\\([^[:space:](){}<>]+\\)))*\\)" - "|" - "\\[([^]\\[[:space:](){}<>]+|(\\[[^]\\[[:space:](){}<>]+\\]))*\\]" - "|" - "[^]\\[[:space:]`!(){};:'\".,<>?«»“”‘’]" - ")" + "(https?://|mailto:|ftp://|file:|ssh:|ssh://|git://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news:)" + "|" + "www\\." ")" - ; + "(" + /* Safe + reserved + some unsafe characters parenthesis and double quotes omitted (we only allow them when balanced) */ + "[0-9a-zA-Z:/?#@!$&*+,;=.~_%^\\-]+" + "|" + /* Balanced "(...)". Content is same as above, plus all _other_ characters we require to be balanced */ + "\\([]\\[\"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\\)" + "|" + /* Balanced "[...]". Content is same as above, plus all _other_ characters we require to be balanced */ + "\\[[\\(\\)\"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\\]" + "|" + /* Balanced '"..."'. Content is same as above, plus all _other_ characters we require to be balanced */ + "\"[]\\[\\(\\)0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\"" + "|" + /* Balanced "'...'". Content is same as above, plus all _other_ characters we require to be balanced */ + "'[]\\[\\(\\)0-9a-zA-Z:/?#@!$&*+,;=.~_%^\\-]*'" + ")+" + "(" + /* Same as above, except :?!,;. are excluded */ + "[0-9a-zA-Z/#@$&*+=~_%^\\-]" + "|" + /* Balanced "(...)". Content is same as above, plus all _other_ characters we require to be balanced */ + "\\([]\\[\"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\\)" + "|" + /* Balanced "[...]". Content is same as above, plus all _other_ characters we require to be balanced */ + "\\[[\\(\\)\"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\\]" + "|" + /* Balanced '"..."'. Content is same as above, plus all _other_ characters we require to be balanced */ + "\"[]\\[\\(\\)0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\"" + "|" + /* Balanced "'...'". Content is same as above, plus all _other_ characters we require to be balanced */ + "'[]\\[\\(\\)0-9a-zA-Z:/?#@!$&*+,;=.~_%^\\-]*'" + ")" + ")"; int r = regcomp(&conf->url.preg, url_regex_string, REG_EXTENDED); xassert(r == 0); diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 043600d2..c32a8e06 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -828,7 +828,7 @@ section. whole regex match to be used as an URL, surround all of it with parenthesis: *(regex-pattern)*. - Default: _(([a-z][[:alnum:]-]+:(/{1,3}|[a-z0-9%])|www[:digit:]{0,3}[.])([^[:space:](){}<>]+|\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))\*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))\*\])+(\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))\*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))\*\]|[^]\[[:space:]`!(){};:'".,<>?«»“”‘’]))_ + Default: _(((https?://|mailto:|ftp://|file:|ssh:|ssh://|git://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news:)|www\.)([0-9a-zA-Z:/?#@!$&\*+,;=.~\_%^\-]+|\([]\["0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*\)|\[[\(\)"0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*\]|"[]\[\(\)0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*"|'[]\[\(\)0-9a-zA-Z:/?#@!$&\*+,;=.~\_%^\-]\*')+([0-9a-zA-Z/#@$&\*+=~\_%^\-]|\([]\["0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*\)|\[[\(\)"0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*\]|"[]\[\(\)0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*"|'[]\[\(\)0-9a-zA-Z:/?#@!$&\*+,;=.~\_%^\-]\*'))_ # SECTION: regex diff --git a/foot.ini b/foot.ini index b852da07..b170dc34 100644 --- a/foot.ini +++ b/foot.ini @@ -69,7 +69,7 @@ # launch=xdg-open ${url} # label-letters=sadfjklewcmpgh # osc8-underline=url-mode -# regex=(([a-z][[:alnum:]-]+:(/{1,3}|[a-z0-9%])|www[:digit:]{0,3}[.])([^[:space:](){}<>]+|\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))*\])+(\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))*\]|[^]\[[:space:]`!(){};:'".,<>?«»“”‘’])) +# regex=(((https?://|mailto:|ftp://|file:|ssh:|ssh://|git://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news:)|www\.)([0-9a-zA-Z:/?#@!$&*+,;=.~_%^\-]+|\([]\["0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*\)|\[[\(\)"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*\]|"[]\[\(\)0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*"|'[]\[\(\)0-9a-zA-Z:/?#@!$&*+,;=.~_%^\-]*')+([0-9a-zA-Z/#@$&*+=~_%^\-]|\([]\["0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*\)|\[[\(\)"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*\]|"[]\[\(\)0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*"|'[]\[\(\)0-9a-zA-Z:/?#@!$&*+,;=.~_%^\-]*')) # You can define your own regex's, by adding a section called # 'regex:<ID>' with a 'regex' and 'launch' key. These can then be tied From bdf65672c0d7567b78d065583bac3c2473690a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wojni=C5=82owicz?= <lukasz.wojnilowicz@gmail.com> Date: Thu, 3 Apr 2025 18:09:53 +0200 Subject: [PATCH 1125/1323] Themes: Add 'Molokai' theme --- themes/molokai | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 themes/molokai diff --git a/themes/molokai b/themes/molokai new file mode 100644 index 00000000..c3935f69 --- /dev/null +++ b/themes/molokai @@ -0,0 +1,23 @@ +# -*- conf -*- +# Molokai +# Based on zhou13's at https://github.com/zhou13/molokai-terminal/blob/master/xterm/Xresources + +[colors] +background=1B1D1E +foreground=CCCCCC +regular0=1B1D1E +regular1=FF0044 +regular2=82B414 +regular3=FD971F +regular4=266C98 +regular5=AC0CB1 +regular6=AE81FF +regular7=CCCCCC +bright0=808080 +bright1=F92672 +bright2=A6E22E +bright3=E6DB74 +bright4=7070F0 +bright5=D63AE1 +bright6=66D9EF +bright7=F8F8F2 From 34d3f4664b93d42ec3e1eef9a11e78756465d25a Mon Sep 17 00:00:00 2001 From: Dominique Martinet <asmadeus@codewreck.org> Date: Sun, 6 Apr 2025 15:35:54 +0900 Subject: [PATCH 1126/1323] xkbcommon: require libxkbcommon >= 1.8.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trying to build with an older libxkbcommon fails as follow: ``` ../input.c: In function ‘keyboard_keymap’: ../input.c:648:82: error: ‘XKB_VMOD_NAME_ALT’ undeclared (first use in this function); did you mean ‘XKB_MOD_NAME_ALT’? 648 | xkb_mod_index_t alt = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_ALT); | ^~~~~~~~~~~~~~~~~ | XKB_MOD_NAME_ALT ../input.c:648:82: note: each undeclared identifier is reported only once for each function it appears in ../input.c:649:83: error: ‘XKB_VMOD_NAME_META’ undeclared (first use in this function); did you mean XKB_MOD_NAME_ALT’? 649 | xkb_mod_index_t meta = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_META); | ^~~~~~~~~~~~~~~~~~ | XKB_MOD_NAME_ALT ../input.c:650:84: error: ‘XKB_VMOD_NAME_SUPER’ undeclared (first use in this function); did you mean ‘XKB_MOD_NAME_NUM’? 650 | xkb_mod_index_t super = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_SUPER); | ^~~~~~~~~~~~~~~~~~~ | XKB_MOD_NAME_NUM ../input.c:651:84: error: ‘XKB_VMOD_NAME_HYPER’ undeclared (first use in this function); did you mean ‘XKB_MOD_NAME_CAPS’? 651 | xkb_mod_index_t hyper = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_HYPER); | ^~~~~~~~~~~~~~~~~~~ | XKB_MOD_NAME_CAPS ../input.c:652:87: error: ‘XKB_VMOD_NAME_NUM’ undeclared (first use in this function); did you mean ‘XKB_MOD_NAME_NUM’? 652 | xkb_mod_index_t num_lock = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_NUM); | ^~~~~~~~~~~~~~~~~ | XKB_MOD_NAME_NUM ../input.c:653:90: error: ‘XKB_VMOD_NAME_SCROLL’ undeclared (first use in this function); did you mean ‘XKB_LED_NAME_SCROLL’? 653 | xkb_mod_index_t scroll_lock = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_SCROLL); | ^~~~~~~~~~~~~~~~~~~~ | XKB_LED_NAME_SCROLL ../input.c:654:90: error: ‘XKB_VMOD_NAME_LEVEL3’ undeclared (first use in this function); did you mean ‘XKB_MOD_NAME_CTRL’? 654 | xkb_mod_index_t level_three = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_LEVEL3); | ^~~~~~~~~~~~~~~~~~~~ | XKB_MOD_NAME_CTRL ../input.c:655:89: error: ‘XKB_VMOD_NAME_LEVEL5’ undeclared (first use in this function); did you mean ‘XKB_MOD_NAME_CTRL’? 655 | xkb_mod_index_t level_five = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_LEVEL5); | ^~~~~~~~~~~~~~~~~~~~ | XKB_MOD_NAME_CTRL ``` --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index a9e47b3b..77869cc7 100644 --- a/meson.build +++ b/meson.build @@ -137,7 +137,7 @@ wayland_protocols = dependency('wayland-protocols', version: '>=1.41', default_options: ['tests=false']) wayland_client = dependency('wayland-client') wayland_cursor = dependency('wayland-cursor') -xkb = dependency('xkbcommon', version: '>=1.0.0') +xkb = dependency('xkbcommon', version: '>=1.8.0') fontconfig = dependency('fontconfig') utf8proc = dependency('libutf8proc', required: get_option('grapheme-clustering')) From 091aa90f1a726507803108b217f52224904115be Mon Sep 17 00:00:00 2001 From: Dominique Martinet <asmadeus@codewreck.org> Date: Sun, 6 Apr 2025 15:48:29 +0900 Subject: [PATCH 1127/1323] wayland: handle xdg-shell edge constraints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wayland-protocols commit 86750c99ed06 ("xdg-shell: Add edge constraints") added a few more enums to handle, making the build fail with -Werror: ../wayland.c: In function ‘xdg_toplevel_configure’: ../wayland.c:878:9: error: enumeration value ‘XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT’ not handled in switch [-Werror=switch] 878 | switch (*state) { | ^~~~~~ ../wayland.c:878:9: error: enumeration value ‘XDG_TOPLEVEL_STATE_CONSTRAINED_RIGHT’ not handled in switch [-Werror=switch] ../wayland.c:878:9: error: enumeration value ‘XDG_TOPLEVEL_STATE_CONSTRAINED_TOP’ not handled in switch [-Werror=switch] ../wayland.c:878:9: error: enumeration value ‘XDG_TOPLEVEL_STATE_CONSTRAINED_BOTTOM’ not handled in switch [-Werror=switch] (This is not part of any release yet, but can be used when building with the submodule) From a quick look it sounds like the meaning is the same as tiling as far as we are concerned so handle these as we do of tiling. --- wayland.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/wayland.c b/wayland.c index 14d9bed9..b13e801d 100644 --- a/wayland.c +++ b/wayland.c @@ -887,6 +887,12 @@ xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, #if defined(XDG_TOPLEVEL_STATE_SUSPENDED_SINCE_VERSION) case XDG_TOPLEVEL_STATE_SUSPENDED: is_suspended = true; break; +#endif +#if defined(XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT_SINCE_VERSION) + case XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT: is_tiled_left = true; break; + case XDG_TOPLEVEL_STATE_CONSTRAINED_RIGHT: is_tiled_right = true; break; + case XDG_TOPLEVEL_STATE_CONSTRAINED_TOP: is_tiled_top = true; break; + case XDG_TOPLEVEL_STATE_CONSTRAINED_BOTTOM: is_tiled_bottom = true; break; #endif } From 23431e3ecfb59f71627b332852b906dc3d0bfad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 7 Apr 2025 13:32:30 +0200 Subject: [PATCH 1128/1323] wayland+input: add support for toplevel edge constraints Edge constraints are new (not yet available in a wayland-protocols release) toplevel states, acting as a complement to the existing tiled states. Tiled tells us we shouldn't draw shadows etc *outside our window geometry*. Constrained tells us the window cannot be resized in the constrained direction. This patch does a couple of things: * Recognize the new states when debug logging * Change is_top_left() etc to look at the new constrained state instead of the tiled state. These functions are used when both choosing cursor shape, and when determining if/how to resize a window on a CSD edge click-and-drag. * Update cursor shape selection to use the default (left_ptr) shape when on a constrained edge (or corner). * Update CSD resize triggering, to not trigger a resize when attempted on a constrained edge (or corner). See https://gitlab.freedesktop.org/wayland/wayland-protocols/-/commit/86750c99ed062c306e837f11bb9492df572ad677: An edge constraint is an complementery state to the tiled state, meaning that it's not only tiled, but constrained in a way that it can't resize in that direction. This typically means that the constrained edge is tiled against a monitor edge. An example configuration is two windows tiled next to each other on a single monitor. Together they cover the whole work area. The left window would have the following tiled and edge constraint state: [ tiled_top, tiled_right, tiled_bottom, tiled_left, constrained_top, constrained_bottom, constrained_left ] while the right window would have the following: [ tiled_top, tiled_right, tiled_bottom, tiled_left, constrained_top, constrained_bottom, constrained_right ] This aims to replace and deprecate the `gtk_surface1.configure_edges` event and the `gtk_surface1.edge_constraint` enum. --- CHANGELOG.md | 8 ++++++ input.c | 71 +++++++++++++++++++++++++++++++++++++--------------- wayland.c | 52 ++++++++++++++++++++++++++++++-------- wayland.h | 13 ++++++++++ 4 files changed, 114 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e421014..bf01bb7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,12 @@ ## Unreleased ### Added + +* Support for toplevel edge constraints. When the compositor indicates + the toplevel has edge constraints, foot will not allow the window to + be resized (via CSDs) in the constrained directions. + + ### Changed * UTF-8 error recovery now discards fewer bytes. @@ -83,6 +89,8 @@ ([#2016][2016]). You can manually set the [old one](https://codeberg.org/dnkl/foot/src/tag/1.21.0/foot.ini#L72), if you prefer it over the new regex. +* A tiled window can now be resized in the corners (via CSDs), unless + the compositor has indicated the toplevel has edge constraints. [2006]: https://codeberg.org/dnkl/foot/issues/2006 [2009]: https://codeberg.org/dnkl/foot/issues/2009 diff --git a/input.c b/input.c index abaac8eb..0f2a8446 100644 --- a/input.c +++ b/input.c @@ -2280,7 +2280,7 @@ is_top_left(const struct terminal *term, int x, int y) { int csd_border_size = term->conf->csd.border_width; return ( - (!term->window->is_tiled_top && !term->window->is_tiled_left) && + (!term->window->is_constrained_top && !term->window->is_constrained_left) && ((term->active_surface == TERM_SURF_BORDER_LEFT && y < 10 * term->scale) || (term->active_surface == TERM_SURF_BORDER_TOP && x < (10 + csd_border_size) * term->scale))); } @@ -2290,7 +2290,7 @@ is_top_right(const struct terminal *term, int x, int y) { int csd_border_size = term->conf->csd.border_width; return ( - (!term->window->is_tiled_top && !term->window->is_tiled_right) && + (!term->window->is_constrained_top && !term->window->is_constrained_right) && ((term->active_surface == TERM_SURF_BORDER_RIGHT && y < 10 * term->scale) || (term->active_surface == TERM_SURF_BORDER_TOP && x > term->width + 1 * csd_border_size * term->scale - 10 * term->scale))); } @@ -2301,7 +2301,7 @@ is_bottom_left(const struct terminal *term, int x, int y) int csd_title_size = term->conf->csd.title_height; int csd_border_size = term->conf->csd.border_width; return ( - (!term->window->is_tiled_bottom && !term->window->is_tiled_left) && + (!term->window->is_constrained_bottom && !term->window->is_constrained_left) && ((term->active_surface == TERM_SURF_BORDER_LEFT && y > csd_title_size * term->scale + term->height) || (term->active_surface == TERM_SURF_BORDER_BOTTOM && x < (10 + csd_border_size) * term->scale))); } @@ -2312,7 +2312,7 @@ is_bottom_right(const struct terminal *term, int x, int y) int csd_title_size = term->conf->csd.title_height; int csd_border_size = term->conf->csd.border_width; return ( - (!term->window->is_tiled_bottom && !term->window->is_tiled_right) && + (!term->window->is_constrained_bottom && !term->window->is_constrained_right) && ((term->active_surface == TERM_SURF_BORDER_RIGHT && y > csd_title_size * term->scale + term->height) || (term->active_surface == TERM_SURF_BORDER_BOTTOM && x > term->width + 1 * csd_border_size * term->scale - 10 * term->scale))); } @@ -2324,10 +2324,23 @@ xcursor_for_csd_border(struct terminal *term, int x, int y) else if (is_top_right(term, x, y)) return CURSOR_SHAPE_TOP_RIGHT_CORNER; else if (is_bottom_left(term, x, y)) return CURSOR_SHAPE_BOTTOM_LEFT_CORNER; else if (is_bottom_right(term, x, y)) return CURSOR_SHAPE_BOTTOM_RIGHT_CORNER; - else if (term->active_surface == TERM_SURF_BORDER_LEFT) return CURSOR_SHAPE_LEFT_SIDE; - else if (term->active_surface == TERM_SURF_BORDER_RIGHT) return CURSOR_SHAPE_RIGHT_SIDE; - else if (term->active_surface == TERM_SURF_BORDER_TOP) return CURSOR_SHAPE_TOP_SIDE; - else if (term->active_surface == TERM_SURF_BORDER_BOTTOM) return CURSOR_SHAPE_BOTTOM_SIDE; + + else if (term->active_surface == TERM_SURF_BORDER_LEFT) + return !term->window->is_constrained_left + ? CURSOR_SHAPE_LEFT_SIDE : CURSOR_SHAPE_LEFT_PTR; + + else if (term->active_surface == TERM_SURF_BORDER_RIGHT) + return !term->window->is_constrained_right + ? CURSOR_SHAPE_RIGHT_SIDE : CURSOR_SHAPE_LEFT_PTR; + + else if (term->active_surface == TERM_SURF_BORDER_TOP) + return !term->window->is_constrained_top + ? CURSOR_SHAPE_TOP_SIDE : CURSOR_SHAPE_LEFT_PTR; + + else if (term->active_surface == TERM_SURF_BORDER_BOTTOM) + return !term->window->is_constrained_bottom + ? CURSOR_SHAPE_BOTTOM_SIDE : CURSOR_SHAPE_LEFT_PTR; + else { BUG("Unreachable"); return CURSOR_SHAPE_NONE; @@ -3095,15 +3108,8 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, case TERM_SURF_BORDER_RIGHT: case TERM_SURF_BORDER_TOP: case TERM_SURF_BORDER_BOTTOM: { - static const enum xdg_toplevel_resize_edge map[] = { - [TERM_SURF_BORDER_LEFT] = XDG_TOPLEVEL_RESIZE_EDGE_LEFT, - [TERM_SURF_BORDER_RIGHT] = XDG_TOPLEVEL_RESIZE_EDGE_RIGHT, - [TERM_SURF_BORDER_TOP] = XDG_TOPLEVEL_RESIZE_EDGE_TOP, - [TERM_SURF_BORDER_BOTTOM] = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM, - }; - if (button == BTN_LEFT && state == WL_POINTER_BUTTON_STATE_PRESSED) { - enum xdg_toplevel_resize_edge resize_type; + enum xdg_toplevel_resize_edge resize_type = XDG_TOPLEVEL_RESIZE_EDGE_NONE; int x = seat->mouse.x; int y = seat->mouse.y; @@ -3116,11 +3122,36 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, resize_type = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT; else if (is_bottom_right(term, x, y)) resize_type = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT; - else - resize_type = map[term->active_surface]; + else { + if (term->active_surface == TERM_SURF_BORDER_LEFT && + !term->window->is_constrained_left) + { + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_LEFT; + } - xdg_toplevel_resize( - term->window->xdg_toplevel, seat->wl_seat, serial, resize_type); + else if (term->active_surface == TERM_SURF_BORDER_RIGHT && + !term->window->is_constrained_right) + { + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_RIGHT; + } + + else if (term->active_surface == TERM_SURF_BORDER_TOP && + !term->window->is_constrained_top) + { + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_TOP; + } + + else if (term->active_surface == TERM_SURF_BORDER_BOTTOM && + !term->window->is_constrained_bottom) + { + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM; + } + } + + if (resize_type != XDG_TOPLEVEL_RESIZE_EDGE_NONE) { + xdg_toplevel_resize( + term->window->xdg_toplevel, seat->wl_seat, serial, resize_type); + } } return; } diff --git a/wayland.c b/wayland.c index b13e801d..853124be 100644 --- a/wayland.c +++ b/wayland.c @@ -852,6 +852,10 @@ xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, bool is_tiled_bottom = false; bool is_tiled_left = false; bool is_tiled_right = false; + bool is_constrained_top = false; + bool is_constrained_bottom = false; + bool is_constrained_left = false; + bool is_constrained_right = false; bool is_suspended UNUSED = false; #if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG @@ -869,6 +873,12 @@ xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, [XDG_TOPLEVEL_STATE_TILED_BOTTOM] = "tiled:bottom", #if defined(XDG_TOPLEVEL_STATE_SUSPENDED_SINCE_VERSION) /* wayland-protocols >= 1.32 */ [XDG_TOPLEVEL_STATE_SUSPENDED] = "suspended", +#endif +#if defined(XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT_SINCE_VERSION) + [XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT] = "constrained:left", + [XDG_TOPLEVEL_STATE_CONSTRAINED_RIGHT] = "constrained:right", + [XDG_TOPLEVEL_STATE_CONSTRAINED_TOP] = "constrained:top", + [XDG_TOPLEVEL_STATE_CONSTRAINED_BOTTOM] = "constrained:bottom", #endif }; #endif @@ -889,10 +899,10 @@ xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, case XDG_TOPLEVEL_STATE_SUSPENDED: is_suspended = true; break; #endif #if defined(XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT_SINCE_VERSION) - case XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT: is_tiled_left = true; break; - case XDG_TOPLEVEL_STATE_CONSTRAINED_RIGHT: is_tiled_right = true; break; - case XDG_TOPLEVEL_STATE_CONSTRAINED_TOP: is_tiled_top = true; break; - case XDG_TOPLEVEL_STATE_CONSTRAINED_BOTTOM: is_tiled_bottom = true; break; + case XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT: is_constrained_left = true; break; + case XDG_TOPLEVEL_STATE_CONSTRAINED_RIGHT: is_constrained_right = true; break; + case XDG_TOPLEVEL_STATE_CONSTRAINED_TOP: is_constrained_top = true; break; + case XDG_TOPLEVEL_STATE_CONSTRAINED_BOTTOM: is_constrained_bottom = true; break; #endif } @@ -933,6 +943,10 @@ xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, win->configure.is_tiled_bottom = is_tiled_bottom; win->configure.is_tiled_left = is_tiled_left; win->configure.is_tiled_right = is_tiled_right; + win->configure.is_constrained_top = is_constrained_top; + win->configure.is_constrained_bottom = is_constrained_bottom; + win->configure.is_constrained_left = is_constrained_left; + win->configure.is_constrained_right = is_constrained_right; win->configure.width = width; win->configure.height = height; } @@ -1056,14 +1070,22 @@ xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, win->is_maximized = win->configure.is_maximized; win->is_fullscreen = win->configure.is_fullscreen; win->is_resizing = win->configure.is_resizing; + win->is_tiled_top = win->configure.is_tiled_top; win->is_tiled_bottom = win->configure.is_tiled_bottom; win->is_tiled_left = win->configure.is_tiled_left; win->is_tiled_right = win->configure.is_tiled_right; + + win->is_constrained_top = win->configure.is_constrained_top; + win->is_constrained_bottom = win->configure.is_constrained_bottom; + win->is_constrained_left = win->configure.is_constrained_left; + win->is_constrained_right = win->configure.is_constrained_right; + win->is_tiled = (win->is_tiled_top || win->is_tiled_bottom || win->is_tiled_left || win->is_tiled_right); + win->csd_mode = win->configure.csd_mode; bool enable_csd = win->csd_mode == CSD_YES && !win->is_fullscreen; @@ -1239,13 +1261,23 @@ handle_global(void *data, struct wl_registry *registry, return; /* - * We *require* version 1, but _can_ use version 5. Version 2 - * adds 'tiled' window states. We use that information to - * restore the window size when window is un-tiled. Version 5 - * adds 'wm_capabilities'. We use that information to draw - * window decorations. + * We *require* version 1, but _can_ use version 2, 5 or 7, if + * available. + * + * Version 2 adds 'tiled' window states. We use this + * information to restore the window size when window is + * un-tiled. + * + * Version 5 adds 'wm_capabilities'. We use this information + * to draw window decorations. + * + * Version 7 adds 'constrained' window states. We use this + * information to determine whether to allow window resize + * (via CSDs) or not. */ -#if defined(XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION) +#if defined(XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT_SINCE_VERSION) + const uint32_t preferred = XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT_SINCE_VERSION; +#elif defined(XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION) const uint32_t preferred = XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION; #elif defined(XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION) const uint32_t preferred = XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION; diff --git a/wayland.h b/wayland.h index 37dd7860..a9d6858c 100644 --- a/wayland.h +++ b/wayland.h @@ -402,6 +402,12 @@ struct wl_window { bool is_tiled_left; bool is_tiled_right; bool is_tiled; /* At least one of is_tiled_{top,bottom,left,right} is true */ + + bool is_constrained_top; + bool is_constrained_bottom; + bool is_constrained_left; + bool is_constrained_right; + struct { int width; int height; @@ -409,10 +415,17 @@ struct wl_window { bool is_fullscreen:1; bool is_maximized:1; bool is_resizing:1; + bool is_tiled_top:1; bool is_tiled_bottom:1; bool is_tiled_left:1; bool is_tiled_right:1; + + bool is_constrained_top:1; + bool is_constrained_bottom:1; + bool is_constrained_left:1; + bool is_constrained_right:1; + enum csd_mode csd_mode; } configure; From bc2e0a29bba936728cd2033fcb795351b738e8a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 10 Apr 2025 12:18:34 +0200 Subject: [PATCH 1129/1323] changelog: move vmod support in config from "changed" to "added" --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf01bb7c..c2f18c5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,10 @@ * Support for toplevel edge constraints. When the compositor indicates the toplevel has edge constraints, foot will not allow the window to be resized (via CSDs) in the constrained directions. +* Virtual modifiers (e.g. `Alt` instead of `Mod1`, `Super` instead of + `Mod4` etc) in key bindings are now recognized as being virtual, and + are automatically mapped to the corresponding real modifier. This + means you can use e.g. `Alt+b` instead of `Mod1+b`. ### Changed @@ -81,10 +85,6 @@ kitty keyboard protocol. - some of foot's default shortcuts not working (mainly those using `Mod1`) out of the box. -* Virtual modifiers (e.g. `Alt` instead of `Mod1`, `Super` instead of - `Mod4` etc) in key bindings are now recognized as being virtual, and - are automatically mapped to the corresponding real modifier. This - means you can use e.g. `Alt+b` instead of `Mod1+b`. * Default URL regex changed to a much more strict variant ([#2016][2016]). You can manually set the [old one](https://codeberg.org/dnkl/foot/src/tag/1.21.0/foot.ini#L72), if From b93d2f042c378e9a2a6d0882c79331492d5dee09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 13 Apr 2025 08:26:20 +0200 Subject: [PATCH 1130/1323] url-mode: fix double-width characters not being handled correctly When a regex matches a string containing double-width characters, the CELL_SPACER values were included in the URL string. This meant the final URL (either launched, or copied) weren't handled correctly, as invalid UTF-8 sequences were inserted in the middle of the string. Closes #2027 --- CHANGELOG.md | 3 +++ url-mode.c | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2f18c5f..e9f82999 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,9 +106,12 @@ * Build failure (`srgb.h` not found) when doing a parallel build. * Regression: reflowing (changing the window size) removing empty lines ([#2011][2011]). +* `url/regex-copy` not handling double-width characters correctly + ([#2027][2027]). [2000]: https://codeberg.org/dnkl/foot/issues/2000 [2011]: https://codeberg.org/dnkl/foot/issues/2011 +[2027]: https://codeberg.org/dnkl/foot/issues/2027 ### Security diff --git a/url-mode.c b/url-mode.c index ed260597..d0f7fc53 100644 --- a/url-mode.c +++ b/url-mode.c @@ -347,6 +347,9 @@ regex_detected(const struct terminal *term, enum url_action action, wc_count = composed->count; } + else if (wc[0] >= CELL_SPACER) + continue; + /* Convert wide character to utf8 */ for (size_t i = 0; i < wc_count; i++) { char buf[16]; @@ -355,6 +358,7 @@ regex_detected(const struct terminal *term, enum url_action action, if (char_len == (size_t)-1) continue; + for (size_t j = 0; j < char_len; j++) { const size_t requires_size = vline->len + char_len; @@ -411,9 +415,9 @@ regex_detected(const struct terminal *term, enum url_action action, const size_t end = start + mlen; LOG_DBG( - "regex match at row %d: %.*srow/col = %dx%d", + "regex match at row %d: %.*s (%zu bytes), row/col = %dx%d", matches[1].rm_so, (int)mlen, &search_string[matches[1].rm_so], - v->map[start].row, v->map[start].col); + mlen, v->map[start].row, v->map[start].col); tll_push_back( *urls, From 9a6227acb354255aec507329d29b3f4a31429ba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 14 Apr 2025 07:03:37 +0200 Subject: [PATCH 1131/1323] doc: foot.ini: workers: "if you have a ridiculous number of cores" --- doc/foot.ini.5.scd | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index c32a8e06..24df3cbb 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -419,6 +419,10 @@ empty string to be set, but it must be quoted: *KEY=""*) multithreading. Default: the number of available logical CPUs (including SMT). Note that this is not always the best value. In some cases, the number of physical _cores_ is better. + + In case you have a ridiculous amount of cores and/or threads, + consider limiting the number of *workers*, since foot cannot + parallelize more than the number of visible rows. *utmp-helper* Path to utmp logging helper binary. From 5f83278afd0530c323d4192e1095b3d1dea644c9 Mon Sep 17 00:00:00 2001 From: Fazzi <faaris.ansari@proton.me> Date: Mon, 9 Oct 2023 18:47:09 +0100 Subject: [PATCH 1132/1323] config: add alpha_mode option --- CHANGELOG.md | 3 +++ config.c | 10 ++++++++++ config.h | 2 ++ foot.ini | 2 ++ render.c | 21 +++++++++++++++++++++ 5 files changed, 38 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9f82999..f3ef0622 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -788,6 +788,9 @@ ### Added +* `alpha-mode` option to `foot.ini`. Defaults to `default`. This + config changes how alpha is handled on background colours not set by + the terminal.(e.g. vim) ([#1510](1510)) * Support for building with _wayland-protocols_ as a subproject. * Mouse wheel scrolls can now be used in `mouse-bindings` ([#1077][1077]). diff --git a/config.c b/config.c index bfd3ffed..aa52b89b 100644 --- a/config.c +++ b/config.c @@ -1095,6 +1095,15 @@ parse_section_main(struct context *ctx) return true; } + else if (strcmp(key, "alpha-mode") == 0) { + _Static_assert(sizeof(conf->alpha_mode) == sizeof(int), + "enum is not 32-bit"); + return value_to_enum( + ctx, + (const char *[]){"default", "matching", "all", NULL}, + (int *)&conf->alpha_mode); + } + else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; @@ -3338,6 +3347,7 @@ config_load(struct config *conf, const char *conf_path, }, .multiplier = 3., }, + .alpha_mode = ALPHA_MODE_DEFAULT, .colors = { .fg = default_foreground, .bg = default_background, diff --git a/config.h b/config.h index a08fae31..18d1a477 100644 --- a/config.h +++ b/config.h @@ -167,6 +167,8 @@ struct config { enum { STARTUP_WINDOWED, STARTUP_MAXIMIZED, STARTUP_FULLSCREEN } startup_mode; + enum { ALPHA_MODE_DEFAULT, ALPHA_MODE_MATCHING, ALPHA_MODE_ALL } alpha_mode; + bool dpi_aware; enum {GAMMA_CORRECT_DISABLED, GAMMA_CORRECT_ENABLED, diff --git a/foot.ini b/foot.ini index b170dc34..0981e180 100644 --- a/foot.ini +++ b/foot.ini @@ -38,6 +38,8 @@ # utmp-helper=/usr/lib/utempter/utempter # When utmp backend is ‘libutempter’ (Linux) # utmp-helper=/usr/libexec/ulog-helper # When utmp backend is ‘ulog’ (FreeBSD) +# alpha-mode=default # Can be `default`, `matching` or `all` + [environment] # name=value diff --git a/render.c b/render.c index d2202468..2766e5ee 100644 --- a/render.c +++ b/render.c @@ -788,6 +788,27 @@ render_cell(struct terminal *term, pixman_image_t *pix, alpha = term->colors.alpha; } } + + if (!term->window->is_fullscreen) { + switch (term->conf->alpha_mode) { + case ALPHA_MODE_DEFAULT: { + if (cell->attrs.bg_src == COLOR_DEFAULT) { + alpha = term->colors.alpha; + } + break; + } + case ALPHA_MODE_MATCHING: { + if (cell->attrs.bg == term->colors.bg) { + alpha = term->colors.alpha; + } + break; + } + case ALPHA_MODE_ALL: { + alpha = term->colors.alpha; + break; + } + } + } } if (unlikely(is_selected && _fg == _bg)) { From bacfba135da5326d72508ba788a5cd8db3dcd671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 14 Apr 2025 16:48:44 +0200 Subject: [PATCH 1133/1323] changelog: move 'alpha-mode' to next-release --- CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3ef0622..d28f567a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,11 @@ `Mod4` etc) in key bindings are now recognized as being virtual, and are automatically mapped to the corresponding real modifier. This means you can use e.g. `Alt+b` instead of `Mod1+b`. +* `alpha-mode` option to `foot.ini`. Defaults to `default`. This + config changes how alpha is handled on background colours not set by + the terminal.(e.g. vim) ([#2026](2026)) + +[2026]: https://codeberg.org/dnkl/foot/issues/2026 ### Changed @@ -788,9 +793,6 @@ ### Added -* `alpha-mode` option to `foot.ini`. Defaults to `default`. This - config changes how alpha is handled on background colours not set by - the terminal.(e.g. vim) ([#1510](1510)) * Support for building with _wayland-protocols_ as a subproject. * Mouse wheel scrolls can now be used in `mouse-bindings` ([#1077][1077]). From d2d4f538619177bc3af6c09e2465272f564ee780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 14 Apr 2025 16:58:23 +0200 Subject: [PATCH 1134/1323] config+render: move alpha-mode to colors.alpha-mode, fix cursor handling Move main.alpha-mode to colors.alpha-mode. Fix (inverted) cursor handling, by always using the bg color without alpha. Do a minor optimization, where we don't even lock at colors.alpha-mode if there's no transparency configured. --- config.c | 20 ++++----- config.h | 8 +++- render.c | 124 ++++++++++++++++++++++++++----------------------------- 3 files changed, 74 insertions(+), 78 deletions(-) diff --git a/config.c b/config.c index aa52b89b..347cc1ec 100644 --- a/config.c +++ b/config.c @@ -1095,15 +1095,6 @@ parse_section_main(struct context *ctx) return true; } - else if (strcmp(key, "alpha-mode") == 0) { - _Static_assert(sizeof(conf->alpha_mode) == sizeof(int), - "enum is not 32-bit"); - return value_to_enum( - ctx, - (const char *[]){"default", "matching", "all", NULL}, - (int *)&conf->alpha_mode); - } - else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; @@ -1490,6 +1481,15 @@ parse_section_colors(struct context *ctx) return true; } + else if (strcmp(key, "alpha-mode") == 0) { + _Static_assert(sizeof(conf->colors.alpha_mode) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"default", "matching", "all", NULL}, + (int *)&conf->colors.alpha_mode); + } else { LOG_CONTEXTUAL_ERR("not valid option"); @@ -3347,13 +3347,13 @@ config_load(struct config *conf, const char *conf_path, }, .multiplier = 3., }, - .alpha_mode = ALPHA_MODE_DEFAULT, .colors = { .fg = default_foreground, .bg = default_background, .flash = 0x7f7f00, .flash_alpha = 0x7fff, .alpha = 0xffff, + .alpha_mode = ALPHA_MODE_DEFAULT, .selection_fg = 0x80000000, /* Use default bg */ .selection_bg = 0x80000000, /* Use default fg */ .use_custom = { diff --git a/config.h b/config.h index 18d1a477..2dec82c1 100644 --- a/config.h +++ b/config.h @@ -167,8 +167,6 @@ struct config { enum { STARTUP_WINDOWED, STARTUP_MAXIMIZED, STARTUP_FULLSCREEN } startup_mode; - enum { ALPHA_MODE_DEFAULT, ALPHA_MODE_MATCHING, ALPHA_MODE_ALL } alpha_mode; - bool dpi_aware; enum {GAMMA_CORRECT_DISABLED, GAMMA_CORRECT_ENABLED, @@ -260,6 +258,12 @@ struct config { uint32_t dim[8]; uint32_t sixel[16]; + enum { + ALPHA_MODE_DEFAULT, + ALPHA_MODE_MATCHING, + ALPHA_MODE_ALL + } alpha_mode; + struct { uint32_t fg; uint32_t bg; diff --git a/render.c b/render.c index 2766e5ee..fdf7015c 100644 --- a/render.c +++ b/render.c @@ -605,13 +605,8 @@ cursor_colors_for_cell(const struct terminal *term, const struct cell *cell, if (term->colors.cursor_fg >> 31) *text_color = color_hex_to_pixman(term->colors.cursor_fg, gamma_correct); else { + xassert(bg->alpha == 0xffff); *text_color = *bg; - - if (unlikely(text_color->alpha != 0xffff)) { - /* The *only* color that can have transparency is the - * default background color */ - *text_color = color_hex_to_pixman(term->colors.bg, gamma_correct); - } } if (text_color->red == cursor_color->red && @@ -749,65 +744,58 @@ render_cell(struct terminal *term, pixman_image_t *pix, _bg = swap; } - else if (cell->attrs.bg_src == COLOR_DEFAULT) { - if (term->window->is_fullscreen) { - /* - * Note: disable transparency when fullscreened. - * - * This is because the wayland protocol mandates no - * screen content is shown behind the fullscreened - * window. - * - * The _intent_ of the specification is that a black - * (or other static color) should be used as - * background. - * - * There's a bit of gray area however, and some - * compositors have chosen to interpret the - * specification in a way that allows wallpapers to be - * seen through a fullscreen window. - * - * Given that a) the intent of the specification, and - * b) we don't know what the compositor will do, we - * simply disable transparency while in fullscreen. - * - * To see why, consider what happens if we keep our - * transparency. For example, if the background color - * is white, and alpha is 0.5, then the window will be - * drawn in a shade of gray while fullscreened. - * - * See - * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/116 - * for a discussion on whether transparent, fullscreen - * windows should be allowed in some way or not. - * - * NOTE: if changing this, also update render_margin() - */ - xassert(alpha == 0xffff); - } else { - alpha = term->colors.alpha; - } - } - - if (!term->window->is_fullscreen) { - switch (term->conf->alpha_mode) { - case ALPHA_MODE_DEFAULT: { - if (cell->attrs.bg_src == COLOR_DEFAULT) { - alpha = term->colors.alpha; - } - break; - } - case ALPHA_MODE_MATCHING: { - if (cell->attrs.bg == term->colors.bg) { - alpha = term->colors.alpha; - } - break; - } - case ALPHA_MODE_ALL: { + if (!term->window->is_fullscreen && term->colors.alpha != 0xffff) { + switch (term->conf->colors.alpha_mode) { + case ALPHA_MODE_DEFAULT: { + if (cell->attrs.bg_src == COLOR_DEFAULT) { alpha = term->colors.alpha; - break; } + break; } + + case ALPHA_MODE_MATCHING: { + if (cell->attrs.bg == term->colors.bg) + alpha = term->colors.alpha; + break; + } + + case ALPHA_MODE_ALL: { + alpha = term->colors.alpha; + break; + } + } + } else { + /* + * Note: disable transparency when fullscreened. + * + * This is because the wayland protocol mandates no screen + * content is shown behind the fullscreened window. + * + * The _intent_ of the specification is that a black (or + * other static color) should be used as background. + * + * There's a bit of gray area however, and some + * compositors have chosen to interpret the specification + * in a way that allows wallpapers to be seen through a + * fullscreen window. + * + * Given that a) the intent of the specification, and b) + * we don't know what the compositor will do, we simply + * disable transparency while in fullscreen. + * + * To see why, consider what happens if we keep our + * transparency. For example, if the background color is + * white, and alpha is 0.5, then the window will be drawn + * in a shade of gray while fullscreened. + * + * See + * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/116 + * for a discussion on whether transparent, fullscreen + * windows should be allowed in some way or not. + * + * NOTE: if changing this, also update render_margin() + */ + xassert(alpha == 0xffff); } } @@ -1012,8 +1000,10 @@ render_cell(struct terminal *term, pixman_image_t *pix, mtx_unlock(&term->render.workers.lock); } - if (unlikely(has_cursor && term->cursor_style == CURSOR_BLOCK && term->kbd_focus)) - draw_cursor(term, cell, font, pix, &fg, &bg, x, y, cell_cols); + if (unlikely(has_cursor && term->cursor_style == CURSOR_BLOCK && term->kbd_focus)) { + const pixman_color_t bg_without_alpha = color_hex_to_pixman(_bg, gamma_correct); + draw_cursor(term, cell, font, pix, &fg, &bg_without_alpha, x, y, cell_cols); + } if (cell->wc == 0 || cell->wc >= CELL_SPACER || cell->wc == U'\t' || (unlikely(cell->attrs.conceal) && !is_selected)) @@ -1161,8 +1151,10 @@ render_cell(struct terminal *term, pixman_image_t *pix, } draw_cursor: - if (has_cursor && (term->cursor_style != CURSOR_BLOCK || !term->kbd_focus)) - draw_cursor(term, cell, font, pix, &fg, &bg, x, y, cell_cols); + if (has_cursor && (term->cursor_style != CURSOR_BLOCK || !term->kbd_focus)) { + const pixman_color_t bg_without_alpha = color_hex_to_pixman(_bg, gamma_correct); + draw_cursor(term, cell, font, pix, &fg, &bg_without_alpha, x, y, cell_cols); + } pixman_image_set_clip_region32(pix, NULL); return cell_cols; From f7807c0f4c5f1e01b72157bbad650d20a1a9d4db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 14 Apr 2025 17:00:07 +0200 Subject: [PATCH 1135/1323] tests: config: test colors.alpha-mode --- tests/test-config.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test-config.c b/tests/test-config.c index f431f4ab..69d349b4 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -720,6 +720,11 @@ test_section_colors(void) &conf.colors.search_box.match.fg, &conf.colors.search_box.match.bg); + test_enum(&ctx, &parse_section_colors, "alpha-mode", 3, + (const char *[]){"default", "matching", "all"}, + (int []){ALPHA_MODE_DEFAULT, ALPHA_MODE_MATCHING, ALPHA_MODE_ALL}, + (int *)&conf.colors.alpha_mode); + for (size_t i = 0; i < 255; i++) { char key_name[4]; sprintf(key_name, "%zu", i); From 9ba8caf30b9d042260e4125b30a17a0f3c96382e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 14 Apr 2025 17:02:45 +0200 Subject: [PATCH 1136/1323] doc: foot.ini: add colors.alpha-mode --- doc/foot.ini.5.scd | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 24df3cbb..083a1087 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1031,6 +1031,21 @@ can configure the background transparency with the _alpha_ option. Background translucency. A value in the range 0.0-1.0, where 0.0 means completely transparent, and 1.0 is opaque. Default: _1.0_. +*alpha-mode* + Specifies when *alpha* is applied. One of *default*, *matching* or + *all*. + + *default* applies *alpha* to cells with the default background + color, excluding cells with the same RGB value as the default + background color. + + *matching* is the same as *default*, but also applies *alpha* to + cells with the same RGB value as the default background color. + + *all* applies *alpha* to all cells, regardless of background color. + + Default: _default_ + *selection-foreground*, *selection-background* Foreground (text) and background color to use in selected text. Note that *both* options must be set, or the default will be From b46a9aa6d7b76712ea0dca4f3799ae4a504843db Mon Sep 17 00:00:00 2001 From: datsudo <76833632+datsudo@users.noreply.github.com> Date: Mon, 14 Apr 2025 22:01:54 +0800 Subject: [PATCH 1137/1323] themes: add "Night Owl" theme --- themes/night-owl | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 themes/night-owl diff --git a/themes/night-owl b/themes/night-owl new file mode 100644 index 00000000..03e1d8f7 --- /dev/null +++ b/themes/night-owl @@ -0,0 +1,30 @@ +# _*_ conf _*_ +# Night Owl + +[cursor] +color=011627 80a4c2 + +[colors] +foreground=d6deeb +background=011627 + +regular0=011627 +regular1=ef5350 +regular2=22da6e +regular3=addb67 +regular4=82aaff +regular5=c792ea +regular6=21c7a8 +regular7=ffffff + +bright0=575656 +bright1=ef5350 +bright2=22da6e +bright3=ffeb95 +bright4=82aaff +bright5=c792ea +bright6=7fdbca +bright7=ffffff + +selection-background=5f7e97 +selection-foreground=dfe5ee From 2c8214f6eac2ad8def58ecce76e806db18b4a4ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 17 Apr 2025 14:41:13 +0200 Subject: [PATCH 1138/1323] changelog: prepare for 1.22.0 --- CHANGELOG.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d28f567a..374d0c2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.22.0](#1-22-0) * [1.21.0](#1-21-0) * [1.20.2](#1-20-2) * [1.20.1](#1-20-1) @@ -59,7 +59,8 @@ * [1.2.0](#1-2-0) -## Unreleased +## 1.22.0 + ### Added * Support for toplevel edge constraints. When the compositor indicates @@ -102,8 +103,6 @@ [2016]: https://codeberg.org/dnkl/foot/issues/2016 -### Deprecated -### Removed ### Fixed * Regression: assertion in `url-mode.c` when activating a second URL @@ -119,9 +118,16 @@ [2027]: https://codeberg.org/dnkl/foot/issues/2027 -### Security ### Contributors +* Alex Xu (Hello71) +* datsudo +* Dominique Martinet +* Fazzi +* llyyr +* Łukasz Wojniłowicz +* Sam McCall + ## 1.21.0 From 95f7b7105841f81788229dbc29055b0a98fff041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 17 Apr 2025 14:41:32 +0200 Subject: [PATCH 1139/1323] meson: bump version to 1.22.0 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 77869cc7..ed2dc7e4 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.21.0', + version: '1.22.0', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From 6e5a602f67a1c95fbec07a1e19027c4454e8ab81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 17 Apr 2025 14:44:05 +0200 Subject: [PATCH 1140/1323] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 374d0c2c..d8787775 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.22.0](#1-22-0) * [1.21.0](#1-21-0) * [1.20.2](#1-20-2) @@ -59,6 +60,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.22.0 ### Added From 30aafce82d344e0ec77e4409945b733c66011433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 18 Apr 2025 13:59:43 +0200 Subject: [PATCH 1141/1323] foot.ini: move alpha-mode to colors section This is where the config parser expects it --- foot.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/foot.ini b/foot.ini index 0981e180..7d96ca0f 100644 --- a/foot.ini +++ b/foot.ini @@ -38,8 +38,6 @@ # utmp-helper=/usr/lib/utempter/utempter # When utmp backend is ‘libutempter’ (Linux) # utmp-helper=/usr/libexec/ulog-helper # When utmp backend is ‘ulog’ (FreeBSD) -# alpha-mode=default # Can be `default`, `matching` or `all` - [environment] # name=value @@ -102,6 +100,7 @@ [colors] # alpha=1.0 +# alpha-mode=default # Can be `default`, `matching` or `all` # background=242424 # foreground=ffffff # flash=7f7f00 From 155c7c96b7a384913d62021b1fa039a1e9d01b9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 18 Apr 2025 14:43:36 +0200 Subject: [PATCH 1142/1323] doc: foot.ini: key-bindings: untranslated symbols are tried before translated --- doc/foot.ini.5.scd | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 083a1087..d51409b8 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1190,17 +1190,18 @@ different approaches. As an example, let's say you press ctrl+shift+c (assume plain us ASCII layout). XKB will tell foot *Control+C* was pressed. Note the lack of the shift modifier, and the upper case 'C'. Internally, this is called -the "translated" form, and is what foot tries to match first. +the "translated" form. -If no "translated" key bindings can be found, foot proceeds to -checking the "untranslated" variant. Using the same example as above, -this will match *Control+Shift+c* (shift modifier present, lower case -'c'). +The "untranslated" form (*Control+Shift+c*) is derived from the +translated form, and is what foot tries to match first. + +If no "untranslated" key bindings can be found, foot proceeds to +checking the "translated" variant. This means you can use either form in your foot configuration, and -that *Control+C* (and similar) has higher priority than -*Control+Shift+c*. Also note that while foot normally detects when the -same combination is assigned to multiple actions, it will not detect +that *Control+Shift+c* (and similar) has higher priority than +*Control+C*. Also note that while foot normally detects when the same +combination is assigned to multiple actions, it will not detect *Control+C* vs. *Control+Shift+c* collisions. Call it a known bug... Finally, foot tries to match the raw key code. Here, the primary From 179e14e0a1792ebf7a5c2774a66b822ce2017a81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 19 Apr 2025 09:16:28 +0200 Subject: [PATCH 1143/1323] doc: foot.ini: gamma-correct-blending: mention colors being off --- doc/foot.ini.5.scd | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index d51409b8..26eb1780 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -220,11 +220,12 @@ empty string to be set, but it must be quoted: *KEY=""*) than intended when rendered with gamma-correct blending, since the font designer set the font weight based on incorrect rendering. - You may also want to enable 10-bit image buffers when - gamma-correct blending is enabled. Though probably only if you do - not use a transparent background (with 10-bit buffers, you only - get 2 bits alpha). See *tweak.surface-bit-depth*. - + Note that some colors (especially dark ones) will look a bit + off. The reason for this is loss of color precision, due to foot + using 8-bit surfaces (i.e. each color channel is 8 bits). The + amount of errors can be reduced by using 10-bit surfaces; see + *tweak.surface-bit-depth*. + Default: enabled when compositor support is available *box-drawings-uses-font-glyphs* @@ -1978,13 +1979,13 @@ any of these options. best option. When *gamma-correct-blending* is enabled, you may want to enable - 10-bit surfaces, as that improves the color resolution. Be aware + 10-bit surfaces, as that improves color precision. Be aware however, that in this mode, the alpha channel is only 2 bits instead of 8 bits. Thus, if you are using a transparent background, you may want to use the default, *8-bit*, even if you have gamma-correct blending enabled. - You should also note that 10-bit surface is slower. This will + You should also note that 10-bit surface is much slower. This will increase input latency and decrease rendering throughput. Default: _8-bit_ From 1bf91566287904664f5c7f68ad09082148eceab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 19 Apr 2025 11:59:50 +0200 Subject: [PATCH 1144/1323] doc: foot.ini: spaces -> tab (for indentation) --- doc/foot.ini.5.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 26eb1780..cdfc65a0 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -225,7 +225,7 @@ empty string to be set, but it must be quoted: *KEY=""*) using 8-bit surfaces (i.e. each color channel is 8 bits). The amount of errors can be reduced by using 10-bit surfaces; see *tweak.surface-bit-depth*. - + Default: enabled when compositor support is available *box-drawings-uses-font-glyphs* From 1a2e5f4932a17c9804ca6d201b7d8cf84d7f19d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 19 Apr 2025 07:46:06 +0200 Subject: [PATCH 1145/1323] render: fix colors.alpha-mode=matching Before this patch, it only matched RGB color sources. It did not match the default bg color, or indexed colors. That is, e.g. CSI 43m didn't apply alpha, even if the color3 matched the default background color. --- CHANGELOG.md | 4 ++++ render.c | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8787775..6958c869 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,10 @@ ### Deprecated ### Removed ### Fixed + +* `colors.alpha-mode=matching` not working as intended. + + ### Security ### Contributors diff --git a/render.c b/render.c index fdf7015c..0e403949 100644 --- a/render.c +++ b/render.c @@ -754,8 +754,15 @@ render_cell(struct terminal *term, pixman_image_t *pix, } case ALPHA_MODE_MATCHING: { - if (cell->attrs.bg == term->colors.bg) + if (cell->attrs.bg_src == COLOR_DEFAULT || + ((cell->attrs.bg_src == COLOR_BASE16 || + cell->attrs.bg_src == COLOR_BASE256) && + term->colors.table[cell->attrs.bg] == term->colors.bg) || + (cell->attrs.bg_src == COLOR_RGB && + cell->attrs.bg == term->colors.bg)) + { alpha = term->colors.alpha; + } break; } From cb2a64c5854205092150afe80f96177764bd4038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 19 Apr 2025 12:16:48 +0200 Subject: [PATCH 1146/1323] csi: don't allow client app to enable grapheme-shaping when disabled at compile-time Closes #2039 --- CHANGELOG.md | 5 +++++ csi.c | 2 ++ 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6958c869..c15e05bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,11 @@ ### Fixed * `colors.alpha-mode=matching` not working as intended. +* Grapheme shaping was allowed to be "enabled" at runtime, even though + disabled at compile time. This caused mis-rendering of certain + codepoints ([#2039][2039]). + +[2039]: https://codeberg.org/dnkl/foot/issues/2039 ### Security diff --git a/csi.c b/csi.c index 81c71e31..b66fda21 100644 --- a/csi.c +++ b/csi.c @@ -558,7 +558,9 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) break; case 2027: +#if defined(FOOT_GRAPHEME_CLUSTERING) term->grapheme_shaping = enable; +#endif break; case 2048: From ef4a680ae813b23e7b9d1b6dd67413dcad377655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 19 Apr 2025 08:05:15 +0200 Subject: [PATCH 1147/1323] input: reset modifiers in keyboard_leave() Closes #2034 --- CHANGELOG.md | 3 +++ input.c | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c15e05bf..35669f03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,8 +71,11 @@ * Grapheme shaping was allowed to be "enabled" at runtime, even though disabled at compile time. This caused mis-rendering of certain codepoints ([#2039][2039]). +* Keyboard modifiers not being reset on keyboard leave events + ([#2034][2034]). [2039]: https://codeberg.org/dnkl/foot/issues/2039 +[2034]: https://codeberg.org/dnkl/foot/issues/2034 ### Security diff --git a/input.c b/input.c index 0f2a8446..d7a7975a 100644 --- a/input.c +++ b/input.c @@ -765,9 +765,17 @@ keyboard_leave(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, seat->kbd.alt = false; seat->kbd.ctrl = false; seat->kbd.super = false; + if (seat->kbd.xkb_compose_state != NULL) xkb_compose_state_reset(seat->kbd.xkb_compose_state); + if (seat->kbd.xkb_state != NULL && seat->kbd.xkb_keymap != NULL) { + const xkb_layout_index_t layout_count = xkb_keymap_num_layouts(seat->kbd.xkb_keymap); + + for (xkb_layout_index_t i = 0; i < layout_count; i++) + xkb_state_update_mask(seat->kbd.xkb_state, 0, 0, 0, i, i, i); + } + if (old_focused != NULL) { seat->pointer.hidden = false; term_xcursor_update_for_seat(old_focused, seat); From 8bded8ce8cf22db51dd42bb71a2a5d39624df402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 19 Apr 2025 17:10:52 +0200 Subject: [PATCH 1148/1323] doc: foot.ini: add newish Unicode range to 'box-drawings-uses-font-glyphs' --- doc/foot.ini.5.scd | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index cdfc65a0..cffbf9c5 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -250,6 +250,7 @@ empty string to be set, but it must be quoted: *KEY=""*) - U+02500 - U+0259F - U+02800 - U+028FF + - U+1CD00 - U+1CDE5 - U+1Fb00 - U+1FB9B Default: _no_. From bc8d6d1ff350672cae7d7e6572eab2d10b1b415e Mon Sep 17 00:00:00 2001 From: Jan Palus <jpalus@fastmail.com> Date: Wed, 23 Apr 2025 11:44:41 +0200 Subject: [PATCH 1149/1323] build: fix race when generating emoji-variation-sequences.h d3f692990ef6 moved emoji-variation-sequences.h header inclusion from vt.c to terminal.c. these two files are part of different libraries hence target for generating emoji-variation-sequences.h needs to be moved too. --- meson.build | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/meson.build b/meson.build index ed2dc7e4..3d97040d 100644 --- a/meson.build +++ b/meson.build @@ -253,7 +253,7 @@ vtlib = static_library( 'osc.c', 'osc.h', 'sixel.c', 'sixel.h', 'vt.c', 'vt.h', - builtin_terminfo, emoji_variation_sequences, srgb_funcs, + builtin_terminfo, srgb_funcs, wl_proto_src + wl_proto_headers, version, dependencies: [libepoll, pixman, fcft, tllist, wayland_client, xkb, utf8proc], @@ -265,6 +265,7 @@ pgolib = static_library( 'grid.c', 'grid.h', 'selection.c', 'selection.h', 'terminal.c', 'terminal.h', + emoji_variation_sequences, wl_proto_src + wl_proto_headers, dependencies: [libepoll, pixman, fcft, tllist, wayland_client, xkb, utf8proc], link_with: vtlib, From b2dfd339e4478ab4d16e249bf491c45f7f4ae587 Mon Sep 17 00:00:00 2001 From: valoq <valoq@noreply.codeberg.org> Date: Mon, 21 Apr 2025 13:35:05 +0000 Subject: [PATCH 1150/1323] Add alacritty theme This adds the default colors from alacritty as an additional theme --- themes/alacritty | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 themes/alacritty diff --git a/themes/alacritty b/themes/alacritty new file mode 100644 index 00000000..a5e4d2c1 --- /dev/null +++ b/themes/alacritty @@ -0,0 +1,59 @@ +# -*- conf -*- +# Alacritty + +[cursor] +color = 181818 56d8c9 + +[colors] +background= 181818 +foreground= d8d8d8 + +#black +regular0= 181818 + +#red +regular1= ac4242 + +#green +regular2= 90a959 + +#yellow +regular3= f4bf75 + +#blue +regular4= 6a9fb5 + +#magenta +regular5= aa759f + +#cyan +regular6= 75b5aa + +#white/grey +regular7= d8d8d8 + + + +#grey/black +bright0= 6b6b6b + +#red +bright1= c55555 + +#green +bright2= aac474 + +#yellow +bright3= feca88 + +#blue +bright4= 82b8c8 + +#pink +bright5= c28cb8 + +#cyan +bright6= 93d3c3 + +#grey +bright7= f8f8f8 \ No newline at end of file From 70b324b24c86473a99528feb6f1f91ca70e11a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 24 Apr 2025 08:23:56 +0200 Subject: [PATCH 1151/1323] term: ignore LTR+RTL markers (U+200E + U+200F) Foot doesn't implement RTL, and explicit LTR markers is neither needed, nor used in anyway. In fact, they cause issues with font lookup, as fcft often fails to find the marker codepoint in the primary font, causing a fallback font to be used instead. Closes #2049 --- CHANGELOG.md | 9 +++++++++ terminal.c | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35669f03..62dc7e03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,13 @@ ## Unreleased ### Added ### Changed + +* Left-to-right and right-to-left markers (U+200E and U+200F) are now + ignored ([#2049][2049]). + +[2049]: https://codeberg.org/dnkl/foot/issues/2049 + + ### Deprecated ### Removed ### Fixed @@ -73,6 +80,8 @@ codepoints ([#2039][2039]). * Keyboard modifiers not being reset on keyboard leave events ([#2034][2034]). +* Last character in a `remind` calendar event being in the wrong font + and color ([#2049][2049]). [2039]: https://codeberg.org/dnkl/foot/issues/2039 [2034]: https://codeberg.org/dnkl/foot/issues/2034 diff --git a/terminal.c b/terminal.c index ae1adb1a..59c39760 100644 --- a/terminal.c +++ b/terminal.c @@ -4156,6 +4156,14 @@ term_process_and_print_non_ascii(struct terminal *term, char32_t wc) (grapheme_clustering || (!grapheme_clustering && width == 0 && wc >= 0x300))) { + if (unlikely(wc == 0x200e || wc == 0x200f)) { + /* + * Ignore left-to-right and right-to-left markers + * see https://codeberg.org/dnkl/foot/issues/2049 + */ + return; + } + int col = term->grid->cursor.point.col; if (!term->grid->cursor.lcf) col--; From 1b15cc5f3d633809114d9d569b34abf934426c69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 24 Apr 2025 18:20:18 +0200 Subject: [PATCH 1152/1323] Revert "term: ignore LTR+RTL markers (U+200E + U+200F)" This reverts commit 70b324b24c86473a99528feb6f1f91ca70e11a02. --- CHANGELOG.md | 9 --------- terminal.c | 8 -------- 2 files changed, 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62dc7e03..35669f03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,13 +63,6 @@ ## Unreleased ### Added ### Changed - -* Left-to-right and right-to-left markers (U+200E and U+200F) are now - ignored ([#2049][2049]). - -[2049]: https://codeberg.org/dnkl/foot/issues/2049 - - ### Deprecated ### Removed ### Fixed @@ -80,8 +73,6 @@ codepoints ([#2039][2039]). * Keyboard modifiers not being reset on keyboard leave events ([#2034][2034]). -* Last character in a `remind` calendar event being in the wrong font - and color ([#2049][2049]). [2039]: https://codeberg.org/dnkl/foot/issues/2039 [2034]: https://codeberg.org/dnkl/foot/issues/2034 diff --git a/terminal.c b/terminal.c index 59c39760..ae1adb1a 100644 --- a/terminal.c +++ b/terminal.c @@ -4156,14 +4156,6 @@ term_process_and_print_non_ascii(struct terminal *term, char32_t wc) (grapheme_clustering || (!grapheme_clustering && width == 0 && wc >= 0x300))) { - if (unlikely(wc == 0x200e || wc == 0x200f)) { - /* - * Ignore left-to-right and right-to-left markers - * see https://codeberg.org/dnkl/foot/issues/2049 - */ - return; - } - int col = term->grid->cursor.point.col; if (!term->grid->cursor.lcf) col--; From 1fec0cf5ea0c3fe3be92a49eaee5eef5aafa9c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 24 Apr 2025 18:22:37 +0200 Subject: [PATCH 1153/1323] Revert "term: append zero-width grapheme breaking characters to previous cell" This reverts commit 76503fb86a8b8a6b5c3ce1be87c15a55af38508d. --- CHANGELOG.md | 4 ++-- terminal.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35669f03..1aedf331 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -229,9 +229,9 @@ enabled ([#1947][1947]). * Reflow of the cursor (active + saved) when at the end of the line with a pending wrap (LCF set) ([#1954][1954]). -* Zero-width characters that also are grapheme breaks (e.g. U+200B, +* ~~Zero-width characters that also are grapheme breaks (e.g. U+200B, ZERO WIDTH SPACE) being ignored (discarded and never stored in the - grid) ([#1960][1960]). + grid) ([#1960][1960]).~~ (reverted) * `--server=<FD>` not working on FreeBSD ([#1956][1956]). * Crash when resetting the terminal and an application had previously set a custom app ID ([#1963][1963]) diff --git a/terminal.c b/terminal.c index ae1adb1a..f2d03e77 100644 --- a/terminal.c +++ b/terminal.c @@ -4188,7 +4188,7 @@ term_process_and_print_non_ascii(struct terminal *term, char32_t wc) if (grapheme_clustering) { /* Check if we're on a grapheme cluster break */ if (utf8proc_grapheme_break_stateful( - last, wc, &term->vt.grapheme_state) && width > 0) + last, wc, &term->vt.grapheme_state)) { term_reset_grapheme_state(term); goto out; From d43326d2b5775f832c50c9cb297c25c5950d6a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 24 Apr 2025 18:40:22 +0200 Subject: [PATCH 1154/1323] changelog: zero-width grapheme breaking codepoints causing fallback font to be used --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1aedf331..3edd8212 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,9 +73,13 @@ codepoints ([#2039][2039]). * Keyboard modifiers not being reset on keyboard leave events ([#2034][2034]). +* Fallback font (and possibly wrong color) being used when a character + was followed by a zero-width grapheme breaking codepoint (for + example, _LEFT-TO-RIGHT MARK_) ([#2049][2049]). [2039]: https://codeberg.org/dnkl/foot/issues/2039 [2034]: https://codeberg.org/dnkl/foot/issues/2034 +[2049]: https://codeberg.org/dnkl/foot/issues/2049 ### Security From cb1b7ba0c5752eeb92c72a80640fbd1f09ad4b7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 25 Apr 2025 19:20:36 +0200 Subject: [PATCH 1155/1323] render: regression: alpha applied to inversed text/selections Introduced by 5f83278afd0530c323d4192e1095b3d1dea644c9 Closes #2073 --- CHANGELOG.md | 2 ++ render.c | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3edd8212..3e9ce91c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,8 @@ * Fallback font (and possibly wrong color) being used when a character was followed by a zero-width grapheme breaking codepoint (for example, _LEFT-TO-RIGHT MARK_) ([#2049][2049]). +* Regression: alpha applied to inversed text/selections + ([#2073][2073]). [2039]: https://codeberg.org/dnkl/foot/issues/2039 [2034]: https://codeberg.org/dnkl/foot/issues/2034 diff --git a/render.c b/render.c index 0e403949..b0d21d18 100644 --- a/render.c +++ b/render.c @@ -744,7 +744,7 @@ render_cell(struct terminal *term, pixman_image_t *pix, _bg = swap; } - if (!term->window->is_fullscreen && term->colors.alpha != 0xffff) { + else if (!term->window->is_fullscreen && term->colors.alpha != 0xffff) { switch (term->conf->colors.alpha_mode) { case ALPHA_MODE_DEFAULT: { if (cell->attrs.bg_src == COLOR_DEFAULT) { From 0020ef12b472f8a57dc67fc912d64872f05b3760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 26 Apr 2025 10:31:09 +0200 Subject: [PATCH 1156/1323] changelog: add missing bug ref --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e9ce91c..2f2e9d88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,7 @@ [2039]: https://codeberg.org/dnkl/foot/issues/2039 [2034]: https://codeberg.org/dnkl/foot/issues/2034 [2049]: https://codeberg.org/dnkl/foot/issues/2049 +[2073]: https://codeberg.org/dnkl/foot/issues/2073 ### Security From 89bfac00e7cc4f36a17d91208678412a6cba4f7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 26 Apr 2025 10:36:13 +0200 Subject: [PATCH 1157/1323] changelog: prepare for 1.22.1 --- CHANGELOG.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f2e9d88..aeabbe14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.22.1](#1-22-1) * [1.22.0](#1-22-0) * [1.21.0](#1-21-0) * [1.20.2](#1-20-2) @@ -60,11 +60,8 @@ * [1.2.0](#1-2-0) -## Unreleased -### Added -### Changed -### Deprecated -### Removed +## 1.22.1 + ### Fixed * `colors.alpha-mode=matching` not working as intended. @@ -85,9 +82,11 @@ [2073]: https://codeberg.org/dnkl/foot/issues/2073 -### Security ### Contributors +* Jan Palus +* valoq + ## 1.22.0 From c85d5d50965d7fdc5718b3d1b69b9e09e90e44f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 26 Apr 2025 10:36:23 +0200 Subject: [PATCH 1158/1323] meson: bump version to 1.22.1 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 3d97040d..7ac033b5 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.22.0', + version: '1.22.1', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From 79f6b4b1deefac678665a7420cd68fd410c80f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 26 Apr 2025 10:41:14 +0200 Subject: [PATCH 1159/1323] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aeabbe14..3c48c41c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.22.1](#1-22-1) * [1.22.0](#1-22-0) * [1.21.0](#1-21-0) @@ -60,6 +61,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.22.1 ### Fixed From a7276d9dff31f5a8d889a9a0c5da974019e1a623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 20 Apr 2025 06:54:58 +0200 Subject: [PATCH 1160/1323] config: refactor: break out 'colors' to a color_theme struct --- config.h | 106 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 54 insertions(+), 52 deletions(-) diff --git a/config.h b/config.h index 2dec82c1..4a4e4ae9 100644 --- a/config.h +++ b/config.h @@ -131,6 +131,59 @@ struct custom_regex { struct config_spawn_template launch; }; +struct color_theme { + uint32_t fg; + uint32_t bg; + uint32_t flash; + uint32_t flash_alpha; + uint32_t table[256]; + uint16_t alpha; + uint32_t selection_fg; + uint32_t selection_bg; + uint32_t url; + + uint32_t dim[8]; + uint32_t sixel[16]; + + enum { + ALPHA_MODE_DEFAULT, + ALPHA_MODE_MATCHING, + ALPHA_MODE_ALL + } alpha_mode; + + struct { + uint32_t fg; + uint32_t bg; + } jump_label; + + struct { + uint32_t fg; + uint32_t bg; + } scrollback_indicator; + + struct { + struct { + uint32_t fg; + uint32_t bg; + } no_match; + + struct { + uint32_t fg; + uint32_t bg; + } match; + } search_box; + + struct { + bool selection:1; + bool jump_label:1; + bool scrollback_indicator:1; + bool url:1; + bool search_box_no_match:1; + bool search_box_match:1; + uint8_t dim; + } use_custom; +}; + struct config { char *term; char *shell; @@ -244,58 +297,7 @@ struct config { tll(struct custom_regex) custom_regexes; - struct { - uint32_t fg; - uint32_t bg; - uint32_t flash; - uint32_t flash_alpha; - uint32_t table[256]; - uint16_t alpha; - uint32_t selection_fg; - uint32_t selection_bg; - uint32_t url; - - uint32_t dim[8]; - uint32_t sixel[16]; - - enum { - ALPHA_MODE_DEFAULT, - ALPHA_MODE_MATCHING, - ALPHA_MODE_ALL - } alpha_mode; - - struct { - uint32_t fg; - uint32_t bg; - } jump_label; - - struct { - uint32_t fg; - uint32_t bg; - } scrollback_indicator; - - struct { - struct { - uint32_t fg; - uint32_t bg; - } no_match; - - struct { - uint32_t fg; - uint32_t bg; - } match; - } search_box; - - struct { - bool selection:1; - bool jump_label:1; - bool scrollback_indicator:1; - bool url:1; - bool search_box_no_match:1; - bool search_box_match:1; - uint8_t dim; - } use_custom; - } colors; + struct color_theme colors; struct { enum cursor_style style; From 624c383a1f45a4fbd5ebf687613c509e6c22d909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 20 Apr 2025 07:16:18 +0200 Subject: [PATCH 1161/1323] config: move cursor.color to colors.cursor --- CHANGELOG.md | 8 ++++++++ config.c | 41 +++++++++++++++++++++++++++++++---------- config.h | 12 +++++++----- doc/foot.ini.5.scd | 18 +++++++++--------- foot.ini | 3 ++- osc.c | 10 ++++++++-- terminal.c | 8 ++++---- 7 files changed, 69 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c48c41c..7b8348ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,7 +64,15 @@ ## Unreleased ### Added ### Changed + +* `cursor.color` moved to `colors.cursor`. + + ### Deprecated + +* `cursor.color` config option; use `colors.cursor` instead. + + ### Removed ### Fixed ### Security diff --git a/config.c b/config.c index 347cc1ec..4337d001 100644 --- a/config.c +++ b/config.c @@ -1445,6 +1445,20 @@ parse_section_colors(struct context *ctx) return true; } + else if (streq(key, "cursor")) { + if (!value_to_two_colors( + ctx, + &conf->colors.cursor.text, + &conf->colors.cursor.cursor, + false)) + { + return false; + } + + conf->colors.use_custom.cursor = true; + return true; + } + else if (streq(key, "urls")) { if (!value_to_color(ctx, &conf->colors.url, false)) return false; @@ -1537,17 +1551,24 @@ parse_section_cursor(struct context *ctx) return value_to_uint32(ctx, 10, &conf->cursor.blink.rate_ms); else if (streq(key, "color")) { + LOG_WARN("%s:%d: cursor.color: deprecated; use colors.cursor instead", + ctx->path, ctx->lineno); + + user_notification_add( + &conf->notifications, + USER_NOTIFICATION_DEPRECATED, + xstrdup("cursor.color: use colors.cursor instead")); + if (!value_to_two_colors( - ctx, - &conf->cursor.color.text, - &conf->cursor.color.cursor, - false)) + ctx, + &conf->colors.cursor.text, + &conf->colors.cursor.cursor, + false)) { return false; } - conf->cursor.color.text |= 1u << 31; - conf->cursor.color.cursor |= 1u << 31; + conf->colors.use_custom.cursor = true; return true; } @@ -3356,6 +3377,10 @@ config_load(struct config *conf, const char *conf_path, .alpha_mode = ALPHA_MODE_DEFAULT, .selection_fg = 0x80000000, /* Use default bg */ .selection_bg = 0x80000000, /* Use default fg */ + .cursor = { + .text = 0, + .cursor = 0, + }, .use_custom = { .selection = false, .jump_label = false, @@ -3371,10 +3396,6 @@ config_load(struct config *conf, const char *conf_path, .enabled = false, .rate_ms = 500, }, - .color = { - .text = 0, - .cursor = 0, - }, .beam_thickness = {.pt = 1.5}, .underline_thickness = {.pt = 0., .px = -1}, }, diff --git a/config.h b/config.h index 4a4e4ae9..89740db3 100644 --- a/config.h +++ b/config.h @@ -149,7 +149,12 @@ struct color_theme { ALPHA_MODE_DEFAULT, ALPHA_MODE_MATCHING, ALPHA_MODE_ALL - } alpha_mode; + } alpha_mode; + + struct { + uint32_t text; + uint32_t cursor; + } cursor; struct { uint32_t fg; @@ -174,6 +179,7 @@ struct color_theme { } search_box; struct { + bool cursor:1; bool selection:1; bool jump_label:1; bool scrollback_indicator:1; @@ -306,10 +312,6 @@ struct config { bool enabled; uint32_t rate_ms; } blink; - struct { - uint32_t text; - uint32_t cursor; - } color; struct pt_or_px beam_thickness; struct pt_or_px underline_thickness; } cursor; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index cffbf9c5..13f768c2 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -898,15 +898,6 @@ applications can change these at runtime. enabled. Expressed in milliseconds between each blink. Default: _500_. -*color* - Two space separated RRGGBB values (i.e. plain old 6-digit hex - values, without prefix) specifying the foreground (text) and - background (cursor) colors for the cursor. - - Example: *ff0000 00ff00* (green cursor, red text) - - Default: the regular foreground and background colors, reversed. - *beam-thickness* Thickness (width) of the beam styled cursor. The value is in points, and its exact value thus depends on the monitor's DPI. To @@ -967,6 +958,15 @@ The colors are in RRGGBB format (i.e. plain old 6-digit hex values, without prefix). That is, they do *not* have an alpha component. You can configure the background transparency with the _alpha_ option. +*cursor* + Two space separated RRGGBB values (i.e. plain old 6-digit hex + values, without prefix) specifying the foreground (text) and + background (cursor) colors for the cursor. + + Example: *ff0000 00ff00* (green cursor, red text) + + Default: the regular foreground and background colors, reversed. + *foreground* Default foreground color. This is the color used when no ANSI color is being used. Default: _839496_. diff --git a/foot.ini b/foot.ini index 7d96ca0f..dc2ad6cd 100644 --- a/foot.ini +++ b/foot.ini @@ -85,7 +85,6 @@ [cursor] # style=block -# color=<inverse foreground/background> # blink=no # blink-rate=500 # beam-thickness=1.5 @@ -106,6 +105,8 @@ # flash=7f7f00 # flash-alpha=0.5 +# cursor=<inverse foreground/background> + ## Normal/regular colors (color palette 0-7) # regular0=242424 # black # regular1=f62b5a # red diff --git a/osc.c b/osc.c index eaf6e33e..7e3e6376 100644 --- a/osc.c +++ b/osc.c @@ -1570,8 +1570,14 @@ osc_dispatch(struct terminal *term) case 112: LOG_DBG("resetting cursor color"); - term->colors.cursor_fg = term->conf->cursor.color.text; - term->colors.cursor_bg = term->conf->cursor.color.cursor; + term->colors.cursor_fg = term->conf->colors.cursor.text; + term->colors.cursor_bg = term->conf->colors.cursor.cursor; + + if (term->conf->colors.use_custom.cursor) { + term->colors.cursor_fg |= 1u << 31; + term->colors.cursor_bg |= 1u << 31; + } + term_damage_cursor(term); break; diff --git a/terminal.c b/terminal.c index f2d03e77..16663647 100644 --- a/terminal.c +++ b/terminal.c @@ -1298,8 +1298,8 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .fg = conf->colors.fg, .bg = conf->colors.bg, .alpha = conf->colors.alpha, - .cursor_fg = conf->cursor.color.text, - .cursor_bg = conf->cursor.color.cursor, + .cursor_fg = (conf->colors.use_custom.cursor ? 1u << 31 : 0) | conf->colors.cursor.text, + .cursor_bg = (conf->colors.use_custom.cursor ? 1u << 31 : 0) | conf->colors.cursor.cursor, .selection_fg = conf->colors.selection_fg, .selection_bg = conf->colors.selection_bg, .use_custom_selection = conf->colors.use_custom.selection, @@ -2153,8 +2153,8 @@ term_reset(struct terminal *term, bool hard) term->colors.fg = term->conf->colors.fg; term->colors.bg = term->conf->colors.bg; term->colors.alpha = term->conf->colors.alpha; - term->colors.cursor_fg = term->conf->cursor.color.text; - term->colors.cursor_bg = term->conf->cursor.color.cursor; + term->colors.cursor_fg = (term->conf->colors.use_custom.cursor ? 1u << 31 : 0) | term->conf->colors.cursor.text; + term->colors.cursor_bg = (term->conf->colors.use_custom.cursor ? 1u << 31 : 0) | term->conf->colors.cursor.cursor; term->colors.selection_fg = term->conf->colors.selection_fg; term->colors.selection_bg = term->conf->colors.selection_bg; term->colors.use_custom_selection = term->conf->colors.use_custom.selection; From 5406ae335530e37f2e6ead3132b15cb5cb71499d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 20 Apr 2025 07:16:37 +0200 Subject: [PATCH 1162/1323] themes: cursor.color -> colors.cursor --- themes/aeroroot | 4 +--- themes/alacritty | 4 +--- themes/apprentice | 4 +--- themes/ayu-mirage | 4 +--- themes/chiba-dark | 4 +--- themes/derp | 4 +--- themes/deus | 4 +--- themes/dracula | 4 +--- themes/dracula-iterm | 4 +--- themes/electrophoretic | 4 +--- themes/hacktober | 3 +-- themes/jetbrains-darcula | 4 +--- themes/kitty | 4 +--- themes/material-amber | 4 +--- themes/moonfly | 4 +--- themes/night-owl | 4 +--- themes/nightfly | 4 +--- themes/noirblaze | 4 +--- themes/nord | 4 +--- themes/nordiq | 4 +--- themes/nvim-dark | 4 +--- themes/nvim-light | 4 +--- themes/onedark | 4 +--- themes/onehalf-dark | 4 +--- themes/paper-color-dark | 4 +--- themes/paper-color-light | 4 +--- themes/poimandres | 4 +--- themes/rose-pine | 4 +--- themes/rose-pine-dawn | 4 +--- themes/rose-pine-moon | 4 +--- themes/selenized-black | 4 +--- themes/selenized-dark | 4 +--- themes/selenized-light | 4 +--- themes/selenized-white | 4 +--- themes/solarized-dark | 4 +--- themes/solarized-dark-normal-brights | 4 +--- themes/solarized-light | 4 +--- themes/tango | 4 +--- themes/tempus-autumn | 4 +--- themes/tempus-classic | 4 +--- themes/tempus-dawn | 4 +--- themes/tempus-day | 4 +--- themes/tempus-dusk | 4 +--- themes/tempus-fugit | 4 +--- themes/tempus-future | 4 +--- themes/tempus-night | 4 +--- themes/tempus-past | 4 +--- themes/tempus-rift | 4 +--- themes/tempus-spring | 4 +--- themes/tempus-summer | 4 +--- themes/tempus-tempest | 4 +--- themes/tempus-totus | 4 +--- themes/tempus-warp | 4 +--- themes/tempus-winter | 4 +--- themes/visibone | 4 +--- 55 files changed, 55 insertions(+), 164 deletions(-) diff --git a/themes/aeroroot b/themes/aeroroot index 3b887448..2a0e0985 100644 --- a/themes/aeroroot +++ b/themes/aeroroot @@ -1,10 +1,8 @@ # -*- conf -*- # Aero root theme -[cursor] -color=1a1a1a 9fd5f5 - [colors] +cursor=1a1a1a 9fd5f5 foreground=dedeef background=1a1a1a diff --git a/themes/alacritty b/themes/alacritty index a5e4d2c1..14503887 100644 --- a/themes/alacritty +++ b/themes/alacritty @@ -1,10 +1,8 @@ # -*- conf -*- # Alacritty -[cursor] -color = 181818 56d8c9 - [colors] +cursor = 181818 56d8c9 background= 181818 foreground= d8d8d8 diff --git a/themes/apprentice b/themes/apprentice index 941a27b4..6b67d21d 100644 --- a/themes/apprentice +++ b/themes/apprentice @@ -1,10 +1,8 @@ # -*- conf -*- # https://github.com/romainl/Apprentice -[cursor] -color=262626 6c6c6c - [colors] +cursor=262626 6c6c6c foreground=bcbcbc background=262626 regular0=1c1c1c diff --git a/themes/ayu-mirage b/themes/ayu-mirage index 64e85a4e..4646e418 100644 --- a/themes/ayu-mirage +++ b/themes/ayu-mirage @@ -2,10 +2,8 @@ # theme: Ayu Mirage # description: a theme based on Ayu Mirage for Sublime Text (original: https://github.com/dempfi/ayu) -[cursor] -color = ffcc66 665a44 - [colors] +cursor = ffcc66 665a44 foreground = cccac2 background = 242936 diff --git a/themes/chiba-dark b/themes/chiba-dark index bc3b1420..8727f684 100644 --- a/themes/chiba-dark +++ b/themes/chiba-dark @@ -3,10 +3,8 @@ # author: ayushnix (https://sr.ht/~ayushnix) # description: A dark theme with bright cyberpunk colors (WCAG AAA compliant) -[cursor] -color = 181818 cdcdcd - [colors] +cursor = 181818 cdcdcd foreground = cdcdcd background = 181818 regular0 = 181818 diff --git a/themes/derp b/themes/derp index 0925d2c2..45eed752 100644 --- a/themes/derp +++ b/themes/derp @@ -1,10 +1,8 @@ # -*- conf -*- # Derp -[cursor] -color=000000 ffffff - [colors] +cursor=000000 ffffff foreground=ffffff background=000000 regular0=111111 diff --git a/themes/deus b/themes/deus index 8fb37f75..0d52e55b 100644 --- a/themes/deus +++ b/themes/deus @@ -2,10 +2,8 @@ # Deus # Color palette based on: https://github.com/ajmwagar/vim-deus -[cursor] -color=2c323b eaeaea - [colors] +cursor=2c323b eaeaea background=2c323b foreground=eaeaea regular0=242a32 diff --git a/themes/dracula b/themes/dracula index 8b6ab542..008fc150 100644 --- a/themes/dracula +++ b/themes/dracula @@ -1,10 +1,8 @@ # -*- conf -*- # Dracula -[cursor] -color=282a36 f8f8f2 - [colors] +cursor=282a36 f8f8f2 foreground=f8f8f2 background=282a36 regular0=000000 # black diff --git a/themes/dracula-iterm b/themes/dracula-iterm index 8c2f66c3..249bb6ab 100644 --- a/themes/dracula-iterm +++ b/themes/dracula-iterm @@ -1,10 +1,8 @@ # -*- conf -*- # Dracula iTerm2 variant -[cursor] -color=ffffff bbbbbb - [colors] +cursor=ffffff bbbbbb foreground=f8f8f2 background=1e1f29 regular0=000000 # black diff --git a/themes/electrophoretic b/themes/electrophoretic index d2b67434..e0bf6e79 100644 --- a/themes/electrophoretic +++ b/themes/electrophoretic @@ -5,10 +5,8 @@ # text and the white background. # author: Eugen Rahaian <eugen@rah.ro> -[cursor] -color=ffffff 515151 - [colors] +cursor=ffffff 515151 background= ffffff foreground= 000000 diff --git a/themes/hacktober b/themes/hacktober index acb6c0b1..dfcc4c7e 100644 --- a/themes/hacktober +++ b/themes/hacktober @@ -1,8 +1,7 @@ # -*- conf -*- -[cursor] -color=141414 c9c9c9 [colors] +cursor=141414 c9c9c9 foreground=c9c9c9 background=141414 regular0=191918 # black diff --git a/themes/jetbrains-darcula b/themes/jetbrains-darcula index 82528498..e6997848 100644 --- a/themes/jetbrains-darcula +++ b/themes/jetbrains-darcula @@ -2,10 +2,8 @@ # JetBrains Darcula # Palette based on the same theme from https://github.com/dexpota/kitty-themes -[cursor] -color=202020 ffffff - [colors] +cursor=202020 ffffff background=202020 foreground=adadad regular0=000000 # black diff --git a/themes/kitty b/themes/kitty index b5b813cc..f43eea9d 100644 --- a/themes/kitty +++ b/themes/kitty @@ -1,9 +1,7 @@ # -*- conf -*- -[cursor] -color=111111 cccccc - [colors] +cursor=111111 cccccc foreground=dddddd background=000000 regular0=000000 # black diff --git a/themes/material-amber b/themes/material-amber index ad844a9a..27983833 100644 --- a/themes/material-amber +++ b/themes/material-amber @@ -2,10 +2,8 @@ # Material Amber # Based on material.io guidelines with Amber 50 background -[cursor] -color=fff8e1 21201d - [colors] +cursor=fff8e1 21201d foreground = 21201d background = fff8e1 diff --git a/themes/moonfly b/themes/moonfly index 870de9d0..0dbe0e95 100644 --- a/themes/moonfly +++ b/themes/moonfly @@ -2,10 +2,8 @@ # moonfly # Based on https://github.com/bluz71/vim-moonfly-colors -[cursor] -color = 080808 9e9e9e - [colors] +cursor = 080808 9e9e9e foreground = b2b2b2 background = 080808 diff --git a/themes/night-owl b/themes/night-owl index 03e1d8f7..43a5c054 100644 --- a/themes/night-owl +++ b/themes/night-owl @@ -1,10 +1,8 @@ # _*_ conf _*_ # Night Owl -[cursor] -color=011627 80a4c2 - [colors] +cursor=011627 80a4c2 foreground=d6deeb background=011627 diff --git a/themes/nightfly b/themes/nightfly index 2a27fb2d..37205f0f 100644 --- a/themes/nightfly +++ b/themes/nightfly @@ -2,10 +2,8 @@ # nightfly # Based on https://github.com/bluz71/vim-nightfly-guicolors -[cursor] -color = 080808 9ca1aa - [colors] +cursor = 080808 9ca1aa foreground = acb4c2 background = 011627 diff --git a/themes/noirblaze b/themes/noirblaze index 3cf452e6..42daf11b 100644 --- a/themes/noirblaze +++ b/themes/noirblaze @@ -3,10 +3,8 @@ # https://github.com/n1ghtmare/noirblaze-kitty -[cursor] -color=121212 ff0088 - [colors] +cursor=121212 ff0088 foreground=d5d5d5 background=121212 diff --git a/themes/nord b/themes/nord index 4ce3a53e..9b988ad6 100644 --- a/themes/nord +++ b/themes/nord @@ -6,10 +6,8 @@ # this specific foot theme is based on nord-alacritty: # https://github.com/arcticicestudio/nord-alacritty/blob/develop/src/nord.yml -[cursor] -color = 2e3440 d8dee9 - [colors] +cursor = 2e3440 d8dee9 foreground = d8dee9 background = 2e3440 diff --git a/themes/nordiq b/themes/nordiq index f309de23..0df5c7de 100644 --- a/themes/nordiq +++ b/themes/nordiq @@ -1,10 +1,8 @@ # -*- conf -*- # Nordiq -[cursor] -color=eeeeee 9f515a - [colors] +cursor=eeeeee 9f515a foreground=dbdee9 background=0e1420 regular0=5b6272 diff --git a/themes/nvim-dark b/themes/nvim-dark index 4c13770a..9a177770 100644 --- a/themes/nvim-dark +++ b/themes/nvim-dark @@ -3,10 +3,8 @@ # Uses the dark color palette from the default Neovim color scheme # See: https://github.com/neovim/neovim/blob/fb6c059dc55c8d594102937be4dd70f5ff51614a/src/nvim/highlight_group.c#L419 -[cursor] -color=14161b e0e2ea # NvimDarkGrey2 NvimLightGrey2 - [colors] +cursor=14161b e0e2ea # NvimDarkGrey2 NvimLightGrey2 foreground=e0e2ea # NvimLightGrey2 background=14161b # NvimDarkGrey2 diff --git a/themes/nvim-light b/themes/nvim-light index 5afec9d7..aca4e156 100644 --- a/themes/nvim-light +++ b/themes/nvim-light @@ -3,10 +3,8 @@ # Uses the light color palette from the default Neovim color scheme # See: https://github.com/neovim/neovim/blob/fb6c059dc55c8d594102937be4dd70f5ff51614a/src/nvim/highlight_group.c#L334 -[cursor] -color=e0e2ea 14161b # NvimLightGrey2 NvimDarkGrey2 - [colors] +cursor=e0e2ea 14161b # NvimLightGrey2 NvimDarkGrey2 foreground=14161b # NvimDarkGrey2 background=e0e2ea # NvimLightGrey2 diff --git a/themes/onedark b/themes/onedark index ac5cc834..0932960b 100644 --- a/themes/onedark +++ b/themes/onedark @@ -1,10 +1,8 @@ # OneDark # Palette based on the same theme from https://github.com/dexpota/kitty-themes -[cursor] -color=111111 cccccc - [colors] +cursor=111111 cccccc foreground=979eab background=282c34 regular0=282c34 # black diff --git a/themes/onehalf-dark b/themes/onehalf-dark index c37a7984..1adc9e23 100644 --- a/themes/onehalf-dark +++ b/themes/onehalf-dark @@ -7,10 +7,8 @@ # + cursor colors from: # https://github.com/sonph/onehalf/blob/master/iterm/OneHalfDark.itermcolors -[cursor] -color=dcdfe4 a3b3cc - [colors] +cursor=dcdfe4 a3b3cc foreground=dcdfe4 background=282c34 regular0=282c34 # black diff --git a/themes/paper-color-dark b/themes/paper-color-dark index 18cd7f17..991bcc9d 100644 --- a/themes/paper-color-dark +++ b/themes/paper-color-dark @@ -2,10 +2,8 @@ # PaperColorDark # Palette based on https://github.com/NLKNguyen/papercolor-theme -[cursor] -color=1c1c1c eeeeee - [colors] +cursor=1c1c1c eeeeee background=1c1c1c foreground=eeeeee regular0=1c1c1c # black diff --git a/themes/paper-color-light b/themes/paper-color-light index b08ea707..b8a6ceec 100644 --- a/themes/paper-color-light +++ b/themes/paper-color-light @@ -2,10 +2,8 @@ # PaperColor Light # Palette based on https://github.com/NLKNguyen/papercolor-theme -[cursor] -color=eeeeee 444444 - [colors] +cursor=eeeeee 444444 background=eeeeee foreground=444444 regular0=eeeeee # black diff --git a/themes/poimandres b/themes/poimandres index d8a6b0a7..b4edc175 100644 --- a/themes/poimandres +++ b/themes/poimandres @@ -1,10 +1,8 @@ # Based on Poimandres color theme for kitti terminal emulator # https://github.com/ubmit/poimandres-kitty -[cursor] -color=1b1e28 ffffff - [colors] +cursor=1b1e28 ffffff foreground=a6accd background=1b1e28 diff --git a/themes/rose-pine b/themes/rose-pine index 78d77dd9..2cae00e8 100644 --- a/themes/rose-pine +++ b/themes/rose-pine @@ -1,10 +1,8 @@ # -*- conf -*- # Rosé Pine -[cursor] -color=191724 e0def4 - [colors] +cursor=191724 e0def4 background=191724 foreground=e0def4 diff --git a/themes/rose-pine-dawn b/themes/rose-pine-dawn index 52008b44..674c7a21 100644 --- a/themes/rose-pine-dawn +++ b/themes/rose-pine-dawn @@ -1,10 +1,8 @@ # -*- conf -*- # Rosé Pine Dawn -[cursor] -color=faf4ed 575279 - [colors] +cursor=faf4ed 575279 background=faf4ed foreground=575279 diff --git a/themes/rose-pine-moon b/themes/rose-pine-moon index 732e5943..cbc81451 100644 --- a/themes/rose-pine-moon +++ b/themes/rose-pine-moon @@ -1,10 +1,8 @@ # -*- conf -*- # Rosé Pine Moon -[cursor] -color=232136 e0def4 - [colors] +cursor=232136 e0def4 background=232136 foreground=e0def4 diff --git a/themes/selenized-black b/themes/selenized-black index 28392add..591751f0 100644 --- a/themes/selenized-black +++ b/themes/selenized-black @@ -1,10 +1,8 @@ # -*- conf -*- # Selenized black -[cursor] -color = 181818 56d8c9 - [colors] +cursor = 181818 56d8c9 background= 181818 foreground= b9b9b9 diff --git a/themes/selenized-dark b/themes/selenized-dark index ed74cdfc..5d062dec 100644 --- a/themes/selenized-dark +++ b/themes/selenized-dark @@ -1,10 +1,8 @@ # -*- conf -*- # Selenized dark -[cursor] -color = 103c48 53d6c7 - [colors] +cursor = 103c48 53d6c7 background= 103c48 foreground= adbcbc diff --git a/themes/selenized-light b/themes/selenized-light index 7e599d8e..04dffbea 100644 --- a/themes/selenized-light +++ b/themes/selenized-light @@ -1,10 +1,8 @@ # -*- conf -*- # Selenized light -[cursor] -color=fbf3db 00978a - [colors] +cursor=fbf3db 00978a background= fbf3db foreground= 53676d diff --git a/themes/selenized-white b/themes/selenized-white index b4d25315..5a7d68b2 100644 --- a/themes/selenized-white +++ b/themes/selenized-white @@ -1,10 +1,8 @@ # -*- conf -*- # Selenized white -[cursor] -color=ffffff 009a8a - [colors] +cursor=ffffff 009a8a background= ffffff foreground= 474747 diff --git a/themes/solarized-dark b/themes/solarized-dark index cad2945e..4997eb4a 100644 --- a/themes/solarized-dark +++ b/themes/solarized-dark @@ -1,10 +1,8 @@ # -*- conf -*- # Solarized dark -[cursor] -color= 002b36 93a1a1 - [colors] +cursor= 002b36 93a1a1 background= 002b36 foreground= 839496 regular0= 073642 diff --git a/themes/solarized-dark-normal-brights b/themes/solarized-dark-normal-brights index 1ab7d375..f0c2172d 100644 --- a/themes/solarized-dark-normal-brights +++ b/themes/solarized-dark-normal-brights @@ -1,10 +1,8 @@ # -*- conf -*- # Solarized dark -[cursor] -color= 002b36 93a1a1 - [colors] +cursor= 002b36 93a1a1 background= 002b36 foreground= 839496 regular0= 073642 diff --git a/themes/solarized-light b/themes/solarized-light index 74474573..3d750277 100644 --- a/themes/solarized-light +++ b/themes/solarized-light @@ -1,10 +1,8 @@ # -*- conf -*- # Solarized light -[cursor] -color=fdf6e3 586e75 - [colors] +cursor= fdf6e3 586e75 background= fdf6e3 foreground= 657b83 regular0= eee8d5 diff --git a/themes/tango b/themes/tango index a326f8ad..a93d29cb 100644 --- a/themes/tango +++ b/themes/tango @@ -1,10 +1,8 @@ # -*- conf -*- # Tango -[cursor] -color=000000 babdb6 - [colors] +cursor=000000 babdb6 foreground=babdb6 background=000000 regular0=2e3436 diff --git a/themes/tempus-autumn b/themes/tempus-autumn index 9c1f8797..74228e90 100644 --- a/themes/tempus-autumn +++ b/themes/tempus-autumn @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a palette inspired by earthly colours (WCAG AA compliant) -#[cursor] -#color = 302420 a9a2a6 - [colors] +#cursor = 302420 a9a2a6 foreground = a9a2a6 background = 302420 regular0 = 302420 diff --git a/themes/tempus-classic b/themes/tempus-classic index 0164605b..b35dc5e5 100644 --- a/themes/tempus-classic +++ b/themes/tempus-classic @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with warm hues (WCAG AA compliant) -#[cursor] -#color = 232323 aeadaf - [colors] +#cursor = 232323 aeadaf foreground = aeadaf background = 232323 regular0 = 232323 diff --git a/themes/tempus-dawn b/themes/tempus-dawn index cf143fba..dc45f29d 100644 --- a/themes/tempus-dawn +++ b/themes/tempus-dawn @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme with a soft, slightly desaturated palette (WCAG AA compliant) -#[cursor] -#color = eff0f2 4a4b4e - [colors] +#cursor = eff0f2 4a4b4e foreground = 4a4b4e background = eff0f2 regular0 = 4a4b4e diff --git a/themes/tempus-day b/themes/tempus-day index b287d45c..1df70137 100644 --- a/themes/tempus-day +++ b/themes/tempus-day @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme with warm colours (WCAG AA compliant) -#[cursor] -#color = f8f2e5 464340 - [colors] +#cursor = f8f2e5 464340 foreground = 464340 background = f8f2e5 regular0 = 464340 diff --git a/themes/tempus-dusk b/themes/tempus-dusk index 2c0308e1..5b4d1bea 100644 --- a/themes/tempus-dusk +++ b/themes/tempus-dusk @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a deep blue-ish, slightly desaturated palette (WCAG AA compliant) -#[cursor] -#color = 1f252d a2a8ba - [colors] +#cursor = 1f252d a2a8ba foreground = a2a8ba background = 1f252d regular0 = 1f252d diff --git a/themes/tempus-fugit b/themes/tempus-fugit index 9ebbcee7..ebd082fe 100644 --- a/themes/tempus-fugit +++ b/themes/tempus-fugit @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light, pleasant theme optimised for long writing/coding sessions (WCAG AA compliant) -#[cursor] -#color = fff5f3 4d595f - [colors] +#cursor = fff5f3 4d595f foreground = 4d595f background = fff5f3 regular0 = 4d595f diff --git a/themes/tempus-future b/themes/tempus-future index 3dd8c7a6..c97d379d 100644 --- a/themes/tempus-future +++ b/themes/tempus-future @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with colours inspired by concept art of outer space (WCAG AAA compliant) -#[cursor] -#color = 090a18 b4abac - [colors] +#cursor = 090a18 b4abac foreground = b4abac background = 090a18 regular0 = 090a18 diff --git a/themes/tempus-night b/themes/tempus-night index de7be5ff..7c97681d 100644 --- a/themes/tempus-night +++ b/themes/tempus-night @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: High contrast dark theme with bright colours (WCAG AAA compliant) -#[cursor] -#color = 1a1a1a e0e0e0 - [colors] +#cursor = 1a1a1a e0e0e0 foreground = e0e0e0 background = 1a1a1a regular0 = 1a1a1a diff --git a/themes/tempus-past b/themes/tempus-past index 8c66f54d..af408b00 100644 --- a/themes/tempus-past +++ b/themes/tempus-past @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme inspired by old vaporwave concept art (WCAG AA compliant) -#[cursor] -#color = f3f2f4 53545b - [colors] +#cursor = f3f2f4 53545b foreground = 53545b background = f3f2f4 regular0 = 53545b diff --git a/themes/tempus-rift b/themes/tempus-rift index 3657a7fe..e0cea4da 100644 --- a/themes/tempus-rift +++ b/themes/tempus-rift @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a subdued palette on the green side of the spectrum (WCAG AA compliant) -#[cursor] -#color = 162c22 bbbcbc - [colors] +#cursor = 162c22 bbbcbc foreground = bbbcbc background = 162c22 regular0 = 162c22 diff --git a/themes/tempus-spring b/themes/tempus-spring index d50e6d06..b98be3b4 100644 --- a/themes/tempus-spring +++ b/themes/tempus-spring @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a palette inspired by early spring colours (WCAG AA compliant) -#[cursor] -#color = 283a37 b5b8b7 - [colors] +#cursor = 283a37 b5b8b7 foreground = b5b8b7 background = 283a37 regular0 = 283a37 diff --git a/themes/tempus-summer b/themes/tempus-summer index 7da1d8c4..cd904010 100644 --- a/themes/tempus-summer +++ b/themes/tempus-summer @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with colours inspired by summer evenings by the sea (WCAG AA compliant) -#[cursor] -#color = 202c3d a0abae - [colors] +#cursor = 202c3d a0abae foreground = a0abae background = 202c3d regular0 = 202c3d diff --git a/themes/tempus-tempest b/themes/tempus-tempest index 57c300aa..2c84454e 100644 --- a/themes/tempus-tempest +++ b/themes/tempus-tempest @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: A green-scale, subtle theme for late night hackers (WCAG AAA compliant) -#[cursor] -#color = 282b2b b6e0ca - [colors] +#cursor = 282b2b b6e0ca foreground = b6e0ca background = 282b2b regular0 = 282b2b diff --git a/themes/tempus-totus b/themes/tempus-totus index 01e84692..3eb21644 100644 --- a/themes/tempus-totus +++ b/themes/tempus-totus @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme for prose or for coding in an open space (WCAG AAA compliant) -#[cursor] -#color = ffffff 4a484d - [colors] +#cursor = ffffff 4a484d foreground = 4a484d background = ffffff regular0 = 4a484d diff --git a/themes/tempus-warp b/themes/tempus-warp index fa8c21c2..911fb266 100644 --- a/themes/tempus-warp +++ b/themes/tempus-warp @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a vibrant palette (WCAG AA compliant) -#[cursor] -#color = 001514 a29fa0 - [colors] +#cursor = 001514 a29fa0 foreground = a29fa0 background = 001514 regular0 = 001514 diff --git a/themes/tempus-winter b/themes/tempus-winter index 8db97057..e4307142 100644 --- a/themes/tempus-winter +++ b/themes/tempus-winter @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a palette inspired by winter nights at the city (WCAG AA compliant) -#[cursor] -#color = 202427 8da3b8 - [colors] +#cursor = 202427 8da3b8 foreground = 8da3b8 background = 202427 regular0 = 202427 diff --git a/themes/visibone b/themes/visibone index 3ee665d0..9979bee0 100644 --- a/themes/visibone +++ b/themes/visibone @@ -1,10 +1,8 @@ # -*- conf -*- # VisiBone -[cursor] -color=010101 ffffff - [colors] +cursor=010101 ffffff foreground=ffffff background=010101 regular0=666666 From b24a9a59b9db95fe9a5195a30d982d9ed2150a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 20 Apr 2025 07:29:54 +0200 Subject: [PATCH 1163/1323] tests: config: colors: verify loaded color is correct --- tests/test-config.c | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test-config.c b/tests/test-config.c index 69d349b4..7dfb8556 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -399,6 +399,16 @@ test_color(struct context *ctx, bool (*parse_fun)(struct context *ctx), BUG("[%s].%s=%s: failed to parse", ctx->section, ctx->key, ctx->value); } + + uint32_t color = input[i].color; + if (alpha_allowed && strlen(input[i].option_string) == 6) + color |= 0xff000000; + + if (*ptr != color) { + BUG("[%s].%s=%s: expected 0x%08x, got 0x%08x", + ctx->section, ctx->key, ctx->value, + color, *ptr); + } } } } @@ -445,6 +455,18 @@ test_two_colors(struct context *ctx, bool (*parse_fun)(struct context *ctx), BUG("[%s].%s=%s: failed to parse", ctx->section, ctx->key, ctx->value); } + + if (*ptr1 != input[i].color1) { + BUG("[%s].%s=%s: expected 0x%08x, got 0x%08x", + ctx->section, ctx->key, ctx->value, + input[i].color1, *ptr1); + } + + if (*ptr2 != input[i].color2) { + BUG("[%s].%s=%s: expected 0x%08x, got 0x%08x", + ctx->section, ctx->key, ctx->value, + input[i].color2, *ptr2); + } } } } @@ -720,6 +742,10 @@ test_section_colors(void) &conf.colors.search_box.match.fg, &conf.colors.search_box.match.bg); + test_two_colors(&ctx, &parse_section_colors, "cursor", false, + &conf.colors.cursor.text, + &conf.colors.cursor.cursor); + test_enum(&ctx, &parse_section_colors, "alpha-mode", 3, (const char *[]){"default", "matching", "all"}, (int []){ALPHA_MODE_DEFAULT, ALPHA_MODE_MATCHING, ALPHA_MODE_ALL}, From 01c43f1644a691a01266cc4ec013dafae9eaf984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 20 Apr 2025 07:32:48 +0200 Subject: [PATCH 1164/1323] config: refactor: break out color theme parsing to a separate function --- config.c | 79 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/config.c b/config.c index 4337d001..ce88d6ce 100644 --- a/config.c +++ b/config.c @@ -1339,9 +1339,8 @@ parse_section_regex(struct context *ctx) } static bool -parse_section_colors(struct context *ctx) +parse_color_theme(struct context *ctx, struct color_theme *theme) { - struct config *conf = ctx->conf; const char *key = ctx->key; size_t key_len = strlen(key); @@ -1350,28 +1349,26 @@ parse_section_colors(struct context *ctx) if (isdigit(key[0])) { unsigned long index; - if (!str_to_ulong(key, 0, &index) || - index >= ALEN(conf->colors.table)) - { + if (!str_to_ulong(key, 0, &index) || index >= ALEN(theme->table)) { LOG_CONTEXTUAL_ERR( "invalid color palette index: %s (not in range 0-%zu)", - key, ALEN(conf->colors.table)); + key, ALEN(theme->table)); return false; } - color = &conf->colors.table[index]; + color = &theme->table[index]; } else if (key_len == 8 && str_has_prefix(key, "regular") && last_digit < 8) - color = &conf->colors.table[last_digit]; + color = &theme->table[last_digit]; else if (key_len == 7 && str_has_prefix(key, "bright") && last_digit < 8) - color = &conf->colors.table[8 + last_digit]; + color = &theme->table[8 + last_digit]; else if (key_len == 4 && str_has_prefix(key, "dim") && last_digit < 8) { - if (!value_to_color(ctx, &conf->colors.dim[last_digit], false)) + if (!value_to_color(ctx, &theme->dim[last_digit], false)) return false; - conf->colors.use_custom.dim |= 1 << last_digit; + theme->use_custom.dim |= 1 << last_digit; return true; } @@ -1380,90 +1377,90 @@ parse_section_colors(struct context *ctx) (key_len == 7 && key[5] == '1' && last_digit < 6))) { size_t idx = key_len == 6 ? last_digit : 10 + last_digit; - return value_to_color(ctx, &conf->colors.sixel[idx], false); + return value_to_color(ctx, &theme->sixel[idx], false); } - else if (streq(key, "flash")) color = &conf->colors.flash; - else if (streq(key, "foreground")) color = &conf->colors.fg; - else if (streq(key, "background")) color = &conf->colors.bg; - else if (streq(key, "selection-foreground")) color = &conf->colors.selection_fg; - else if (streq(key, "selection-background")) color = &conf->colors.selection_bg; + else if (streq(key, "flash")) color = &theme->flash; + else if (streq(key, "foreground")) color = &theme->fg; + else if (streq(key, "background")) color = &theme->bg; + else if (streq(key, "selection-foreground")) color = &theme->selection_fg; + else if (streq(key, "selection-background")) color = &theme->selection_bg; else if (streq(key, "jump-labels")) { if (!value_to_two_colors( ctx, - &conf->colors.jump_label.fg, - &conf->colors.jump_label.bg, + &theme->jump_label.fg, + &theme->jump_label.bg, false)) { return false; } - conf->colors.use_custom.jump_label = true; + theme->use_custom.jump_label = true; return true; } else if (streq(key, "scrollback-indicator")) { if (!value_to_two_colors( ctx, - &conf->colors.scrollback_indicator.fg, - &conf->colors.scrollback_indicator.bg, + &theme->scrollback_indicator.fg, + &theme->scrollback_indicator.bg, false)) { return false; } - conf->colors.use_custom.scrollback_indicator = true; + theme->use_custom.scrollback_indicator = true; return true; } else if (streq(key, "search-box-no-match")) { if (!value_to_two_colors( ctx, - &conf->colors.search_box.no_match.fg, - &conf->colors.search_box.no_match.bg, + &theme->search_box.no_match.fg, + &theme->search_box.no_match.bg, false)) { return false; } - conf->colors.use_custom.search_box_no_match = true; + theme->use_custom.search_box_no_match = true; return true; } else if (streq(key, "search-box-match")) { if (!value_to_two_colors( ctx, - &conf->colors.search_box.match.fg, - &conf->colors.search_box.match.bg, + &theme->search_box.match.fg, + &theme->search_box.match.bg, false)) { return false; } - conf->colors.use_custom.search_box_match = true; + theme->use_custom.search_box_match = true; return true; } else if (streq(key, "cursor")) { if (!value_to_two_colors( ctx, - &conf->colors.cursor.text, - &conf->colors.cursor.cursor, + &theme->cursor.text, + &theme->cursor.cursor, false)) { return false; } - conf->colors.use_custom.cursor = true; + theme->use_custom.cursor = true; return true; } else if (streq(key, "urls")) { - if (!value_to_color(ctx, &conf->colors.url, false)) + if (!value_to_color(ctx, &theme->url, false)) return false; - conf->colors.use_custom.url = true; + theme->use_custom.url = true; return true; } @@ -1477,7 +1474,7 @@ parse_section_colors(struct context *ctx) return false; } - conf->colors.alpha = alpha * 65535.; + theme->alpha = alpha * 65535.; return true; } @@ -1491,18 +1488,18 @@ parse_section_colors(struct context *ctx) return false; } - conf->colors.flash_alpha = alpha * 65535.; + theme->flash_alpha = alpha * 65535.; return true; } else if (strcmp(key, "alpha-mode") == 0) { - _Static_assert(sizeof(conf->colors.alpha_mode) == sizeof(int), + _Static_assert(sizeof(theme->alpha_mode) == sizeof(int), "enum is not 32-bit"); return value_to_enum( ctx, (const char *[]){"default", "matching", "all", NULL}, - (int *)&conf->colors.alpha_mode); + (int *)&theme->alpha_mode); } else { @@ -1518,6 +1515,12 @@ parse_section_colors(struct context *ctx) return true; } +static bool +parse_section_colors(struct context *ctx) +{ + return parse_color_theme(ctx, &ctx->conf->colors); +} + static bool parse_section_cursor(struct context *ctx) { From 1423babc35e2896b602d124b634816ebfaeae243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 20 Apr 2025 07:36:58 +0200 Subject: [PATCH 1165/1323] config: add new section 'colors2' This section defines an alternative color theme. The keys are the same as in the 'colors' section, as are the default values. Values are *not* inherited from 'colors'. That is, if you set a value in 'colors', but not in 'colors2', it is *not* inherited by 'colors2'. --- config.c | 8 ++++++++ config.h | 1 + doc/foot.ini.5.scd | 9 +++++++++ foot.ini | 3 +++ 4 files changed, 21 insertions(+) diff --git a/config.c b/config.c index ce88d6ce..1b73fbcf 100644 --- a/config.c +++ b/config.c @@ -1521,6 +1521,12 @@ parse_section_colors(struct context *ctx) return parse_color_theme(ctx, &ctx->conf->colors); } +static bool +parse_section_colors2(struct context *ctx) +{ + return parse_color_theme(ctx, &ctx->conf->colors2); +} + static bool parse_section_cursor(struct context *ctx) { @@ -2900,6 +2906,7 @@ enum section { SECTION_URL, SECTION_REGEX, SECTION_COLORS, + SECTION_COLORS2, SECTION_CURSOR, SECTION_MOUSE, SECTION_CSD, @@ -2930,6 +2937,7 @@ static const struct { [SECTION_URL] = {&parse_section_url, "url"}, [SECTION_REGEX] = {&parse_section_regex, "regex", true}, [SECTION_COLORS] = {&parse_section_colors, "colors"}, + [SECTION_COLORS2] = {&parse_section_colors2, "colors2"}, [SECTION_CURSOR] = {&parse_section_cursor, "cursor"}, [SECTION_MOUSE] = {&parse_section_mouse, "mouse"}, [SECTION_CSD] = {&parse_section_csd, "csd"}, diff --git a/config.h b/config.h index 89740db3..8954b270 100644 --- a/config.h +++ b/config.h @@ -304,6 +304,7 @@ struct config { tll(struct custom_regex) custom_regexes; struct color_theme colors; + struct color_theme colors2; struct { enum cursor_style style; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 13f768c2..45084ab6 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1084,6 +1084,15 @@ can configure the background transparency with the _alpha_ option. Flash translucency. A value in the range 0.0-1.0, where 0.0 means completely transparent, and 1.0 is opaque. Default: _0.5_. +# SECTION: colors2 + +This section defines an alternative color theme. It has the exact same +keys as the *colors* section. The default values are the same. + +Note that values are not inherited. That is, if you set a value in +*colors*, but not in *colors2*, the value from *colors* is not +inherited by *colors2*. + # SECTION: csd This section controls the look of the _CSDs_ (Client Side diff --git a/foot.ini b/foot.ini index dc2ad6cd..8482af6b 100644 --- a/foot.ini +++ b/foot.ini @@ -164,6 +164,9 @@ # search-box-match=<regular0> <regular3> # black-on-yellow # urls=<regular3> +[colors2] +# Alternative color theme, see man page foot.ini(5) + [csd] # preferred=server # size=26 From 6bc91b5e288ef16613a4e6f9d0a9f6077cedc267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 20 Apr 2025 07:58:02 +0200 Subject: [PATCH 1166/1323] key-bindings: add bindings to switch between color themes * color-theme-switch-1: select the primary color theme * color-theme-switch-2: select the alternative color theme * color-theme-toggle: toggle between the primary and alternative color themes --- CHANGELOG.md | 10 ++++++++++ config.c | 4 ++++ doc/foot.ini.5.scd | 16 ++++++++++++++++ foot.ini | 3 +++ input.c | 45 +++++++++++++++++++++++++++++++++++++++++++++ key-binding.h | 5 ++++- terminal.c | 27 +++++++++++++++++---------- terminal.h | 3 +++ 8 files changed, 102 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b8348ed..4296da09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,16 @@ ## Unreleased ### Added + +* `colors2` config section. This section duplicates the `colors` + section, and lets you define an alternative color theme. +* `key-bindings.color-theme-switch-1`, + `key-bindings.color-theme-switch-2` and + `key-bindings.color-theme-toggle` key bindings. These can be used to + switch between the primary and alternative color themes. They are + not bound by default. + + ### Changed * `cursor.color` moved to `colors.cursor`. diff --git a/config.c b/config.c index 1b73fbcf..921b2d68 100644 --- a/config.c +++ b/config.c @@ -142,6 +142,9 @@ static const char *const binding_action_map[] = { [BIND_ACTION_QUIT] = "quit", [BIND_ACTION_REGEX_LAUNCH] = "regex-launch", [BIND_ACTION_REGEX_COPY] = "regex-copy", + [BIND_ACTION_THEME_SWITCH_1] = "color-theme-switch-1", + [BIND_ACTION_THEME_SWITCH_2] = "color-theme-switch-2", + [BIND_ACTION_THEME_TOGGLE] = "color-theme-toggle", /* Mouse-specific actions */ [BIND_ACTION_SCROLLBACK_UP_MOUSE] = "scrollback-up-mouse", @@ -3479,6 +3482,7 @@ config_load(struct config *conf, const char *conf_path, memcpy(conf->colors.table, default_color_table, sizeof(default_color_table)); memcpy(conf->colors.sixel, default_sixel_colors, sizeof(default_sixel_colors)); + memcpy(&conf->colors2, &conf->colors, sizeof(conf->colors)); parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers); tokenize_cmdline( diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 45084ab6..af6f7875 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1396,6 +1396,22 @@ e.g. *search-start=none*. Default: _Control+Shift+u_. +*color-theme-switch-1*, *color-theme-switch-2*, *color-theme-toggle* + Switch between the primary color theme (defined in the *colors* + section), and the alternative color theme (defined in the + *colors2* section). + + *color-theme-switch-1* applies the primary color theme regardless + of which color theme is currently active. + + *color-theme-switch-2* applies the alternative color theme regardless + of which color theme is currently active. + + *color-theme-toggle* toggles between the primary and alternative + color themes. + + Default: _none_ + *quit* Quit foot. Default: _none_. diff --git a/foot.ini b/foot.ini index 8482af6b..ebbc8ca7 100644 --- a/foot.ini +++ b/foot.ini @@ -212,6 +212,9 @@ # prompt-prev=Control+Shift+z # prompt-next=Control+Shift+x # unicode-input=Control+Shift+u +# color-theme-switch-1=none +# color-theme-switch-2=none +# color-theme-toggle=none # noop=none # quit=none diff --git a/input.c b/input.c index d7a7975a..ebb646c6 100644 --- a/input.c +++ b/input.c @@ -484,6 +484,51 @@ execute_binding(struct seat *seat, struct terminal *term, return true; + case BIND_ACTION_THEME_SWITCH_1: + if (term->colors.active_theme != COLOR_THEME1) { + term_theme_apply(term, &term->conf->colors); + term->colors.active_theme = COLOR_THEME1; + + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + + term_damage_view(term); + term_damage_margins(term); + render_refresh(term); + } + return true; + + case BIND_ACTION_THEME_SWITCH_2: + if (term->colors.active_theme != COLOR_THEME2) { + term_theme_apply(term, &term->conf->colors2); + term->colors.active_theme = COLOR_THEME2; + + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + + term_damage_view(term); + term_damage_margins(term); + render_refresh(term); + } + return true; + + case BIND_ACTION_THEME_TOGGLE: + if (term->colors.active_theme == COLOR_THEME1) { + term_theme_apply(term, &term->conf->colors2); + term->colors.active_theme = COLOR_THEME2; + } else { + term_theme_apply(term, &term->conf->colors); + term->colors.active_theme = COLOR_THEME1; + } + + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + + term_damage_view(term); + term_damage_margins(term); + render_refresh(term); + return true; + case BIND_ACTION_SELECT_BEGIN: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_CHAR_WISE, false); diff --git a/key-binding.h b/key-binding.h index 89398859..5f0c1f1e 100644 --- a/key-binding.h +++ b/key-binding.h @@ -43,6 +43,9 @@ enum bind_action_normal { BIND_ACTION_QUIT, BIND_ACTION_REGEX_LAUNCH, BIND_ACTION_REGEX_COPY, + BIND_ACTION_THEME_SWITCH_1, + BIND_ACTION_THEME_SWITCH_2, + BIND_ACTION_THEME_TOGGLE, /* Mouse specific actions - i.e. they require a mouse coordinate */ BIND_ACTION_SCROLLBACK_UP_MOUSE, @@ -56,7 +59,7 @@ enum bind_action_normal { BIND_ACTION_SELECT_QUOTE, BIND_ACTION_SELECT_ROW, - BIND_ACTION_KEY_COUNT = BIND_ACTION_REGEX_COPY + 1, + BIND_ACTION_KEY_COUNT = BIND_ACTION_THEME_TOGGLE + 1, BIND_ACTION_COUNT = BIND_ACTION_SELECT_ROW + 1, }; diff --git a/terminal.c b/terminal.c index 16663647..29e03b8e 100644 --- a/terminal.c +++ b/terminal.c @@ -1303,6 +1303,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .selection_fg = conf->colors.selection_fg, .selection_bg = conf->colors.selection_bg, .use_custom_selection = conf->colors.use_custom.selection, + .active_theme = COLOR_THEME1, }, .color_stack = { .stack = NULL, @@ -2150,16 +2151,8 @@ term_reset(struct terminal *term, bool hard) term->flash.active = false; term->blink.state = BLINK_ON; fdm_del(term->fdm, term->blink.fd); term->blink.fd = -1; - term->colors.fg = term->conf->colors.fg; - term->colors.bg = term->conf->colors.bg; - term->colors.alpha = term->conf->colors.alpha; - term->colors.cursor_fg = (term->conf->colors.use_custom.cursor ? 1u << 31 : 0) | term->conf->colors.cursor.text; - term->colors.cursor_bg = (term->conf->colors.use_custom.cursor ? 1u << 31 : 0) | term->conf->colors.cursor.cursor; - term->colors.selection_fg = term->conf->colors.selection_fg; - term->colors.selection_bg = term->conf->colors.selection_bg; - term->colors.use_custom_selection = term->conf->colors.use_custom.selection; - memcpy(term->colors.table, term->conf->colors.table, - sizeof(term->colors.table)); + term_theme_apply(term, &term->conf->colors); + term->colors.active_theme = COLOR_THEME1; free(term->color_stack.stack); term->color_stack.stack = NULL; term->color_stack.size = 0; @@ -4693,3 +4686,17 @@ term_send_size_notification(struct terminal *term) term->rows, term->cols, height, width); term_to_slave(term, buf, n); } + +void +term_theme_apply(struct terminal *term, const struct color_theme *theme) +{ + term->colors.fg = theme->fg; + term->colors.bg = theme->bg; + term->colors.alpha = theme->alpha; + term->colors.cursor_fg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.text; + term->colors.cursor_bg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.cursor; + term->colors.selection_fg = theme->selection_fg; + term->colors.selection_bg = theme->selection_bg; + term->colors.use_custom_selection = theme->use_custom.selection; + memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); +} diff --git a/terminal.h b/terminal.h index 518e36ef..45e13925 100644 --- a/terminal.h +++ b/terminal.h @@ -405,6 +405,7 @@ struct colors { uint32_t selection_fg; uint32_t selection_bg; bool use_custom_selection; + enum { COLOR_THEME1, COLOR_THEME2 } active_theme; }; struct terminal { @@ -982,6 +983,8 @@ void term_enable_size_notifications(struct terminal *term); void term_disable_size_notifications(struct terminal *term); void term_send_size_notification(struct terminal *term); +void term_theme_apply(struct terminal *term, const struct color_theme *theme); + static inline void term_reset_grapheme_state(struct terminal *term) { #if defined(FOOT_GRAPHEME_CLUSTERING) From 10e7f291498a6600cfecc3cde604ea51de5f0f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 20 Apr 2025 12:48:37 +0200 Subject: [PATCH 1167/1323] csi: implement private mode 2031 (dark/light mode detection) * Recognize 'CSI ? 996 n', and respond with - 'CSI ? 997 ; 1 n' if the primary theme is active - 'CSI ? 997 ; 2 n' if the alternative theme is actice * Implement private mode 2031, where changing the color theme (currently only possible via key bindings) causes the terminal to send the same CSI sequences as above. In this context, foot's primary theme is considered dark, and the alternative theme light (since the default theme is dark). Closes #2025 --- CHANGELOG.md | 5 +++++ csi.c | 33 +++++++++++++++++++++++++++++++++ doc/foot-ctlseqs.7.scd | 10 ++++++++++ doc/foot.ini.5.scd | 8 ++++++++ input.c | 12 ++++++++++++ terminal.h | 2 ++ 6 files changed, 70 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4296da09..5c40b8cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,11 @@ `key-bindings.color-theme-toggle` key bindings. These can be used to switch between the primary and alternative color themes. They are not bound by default. +* Support for private mode 2031 - [_Dark and Light Mode + Detection_](https://contour-terminal.org/vt-extensions/color-palette-update-notifications/) + ([#2025][2025]) + +[2025]: https://codeberg.org/dnkl/foot/issues/2025 ### Changed diff --git a/csi.c b/csi.c index b66fda21..e8b2c492 100644 --- a/csi.c +++ b/csi.c @@ -563,6 +563,10 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) #endif break; + case 2031: + term->report_theme_changes = enable; + break; + case 2048: if (enable) term_enable_size_notifications(term); @@ -657,6 +661,7 @@ decrqm(const struct terminal *term, unsigned param) case 2027: return term->conf->tweak.grapheme_width_method != GRAPHEME_WIDTH_DOUBLE ? DECRPM_PERMANENTLY_RESET : decrpm(term->grapheme_shaping); + case 2031: return decrpm(term->report_theme_changes); case 2048: return decrpm(term->size_notifications); case 8452: return decrpm(term->sixel.cursor_right_of_graphics); case 737769: return decrpm(term_ime_is_enabled(term)); @@ -702,6 +707,7 @@ xtsave(struct terminal *term, unsigned param) case 2004: term->xtsave.bracketed_paste = term->bracketed_paste; break; case 2026: term->xtsave.app_sync_updates = term->render.app_sync_updates.enabled; break; case 2027: term->xtsave.grapheme_shaping = term->grapheme_shaping; break; + case 2031: term->xtsave.report_theme_changes = term->report_theme_changes; break; case 2048: term->xtsave.size_notifications = term->size_notifications; break; case 8452: term->xtsave.sixel_cursor_right_of_graphics = term->sixel.cursor_right_of_graphics; break; case 737769: term->xtsave.ime = term_ime_is_enabled(term); break; @@ -746,6 +752,7 @@ xtrestore(struct terminal *term, unsigned param) case 2004: enable = term->xtsave.bracketed_paste; break; case 2026: enable = term->xtsave.app_sync_updates; break; case 2027: enable = term->xtsave.grapheme_shaping; break; + case 2031: enable = term->xtsave.report_theme_changes; break; case 2048: enable = term->xtsave.size_notifications; break; case 8452: enable = term->xtsave.sixel_cursor_right_of_graphics; break; case 737769: enable = term->xtsave.ime; break; @@ -1539,6 +1546,32 @@ csi_dispatch(struct terminal *term, uint8_t final) break; } + case 'n': { + const int param = vt_param_get(term, 0, 0); + + switch (param) { + case 996: { /* Query current theme mode (see private mode 2031) */ + /* + * 1 - dark mode + * 2 - light mode + * + * In foot, the themes aren't necessarily light/dark, + * but by convention, the primary theme is dark, and + * the alternative theme is light. + */ + char reply[16] = {0}; + int chars = snprintf( + reply, sizeof(reply), + "\033[?997;%dn", + term->colors.active_theme == COLOR_THEME1 ? 1 : 2); + + term_to_slave(term, reply, chars); + break; + } + } + break; + } + case 'p': { /* * Request status of ECMA-48/"ANSI" private mode (DECRQM diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index 6c702738..40906ebf 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -337,6 +337,9 @@ that corresponds to one of the following modes: | 2027 : contour : Grapheme cluster processing +| 2031 +: contour +: Request color theme updates | 2048 : TODO : In-band window resize notifications @@ -657,6 +660,13 @@ manipulation sequences. The generic format is: : xterm : Report the current entry on the palette stack, and the number of palettes stored on the stack. +| \\E[ ? 996 n +: Query the current (color) theme mode +: contour +: The current color theme mode (light or dark) is reported as *CSI ? + 997 ; 1|2 n*, where *1* means dark and *2* light. By convention, the + primary theme in foot is considered dark, and the alternative theme + light. # OSC diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index af6f7875..85a7cf7b 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -958,6 +958,10 @@ The colors are in RRGGBB format (i.e. plain old 6-digit hex values, without prefix). That is, they do *not* have an alpha component. You can configure the background transparency with the _alpha_ option. +In the context of private mode 2031 (Dark and Light Mode detection), +the primary theme (i.e. the *colors* section) is considered to be the +dark theme (since the default theme is dark). + *cursor* Two space separated RRGGBB values (i.e. plain old 6-digit hex values, without prefix) specifying the foreground (text) and @@ -1093,6 +1097,10 @@ Note that values are not inherited. That is, if you set a value in *colors*, but not in *colors2*, the value from *colors* is not inherited by *colors2*. +In the context of private mode 2031 (Dark and Light Mode detection), +the primary theme (i.e. the *colors2* section) is considered to be the +light theme (since the default theme is dark). + # SECTION: csd This section controls the look of the _CSDs_ (Client Side diff --git a/input.c b/input.c index ebb646c6..b6c56fde 100644 --- a/input.c +++ b/input.c @@ -492,6 +492,9 @@ execute_binding(struct seat *seat, struct terminal *term, wayl_win_alpha_changed(term->window); term_font_subpixel_changed(term); + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;1n", 9); + term_damage_view(term); term_damage_margins(term); render_refresh(term); @@ -506,6 +509,9 @@ execute_binding(struct seat *seat, struct terminal *term, wayl_win_alpha_changed(term->window); term_font_subpixel_changed(term); + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;2n", 9); + term_damage_view(term); term_damage_margins(term); render_refresh(term); @@ -516,9 +522,15 @@ execute_binding(struct seat *seat, struct terminal *term, if (term->colors.active_theme == COLOR_THEME1) { term_theme_apply(term, &term->conf->colors2); term->colors.active_theme = COLOR_THEME2; + + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;2n", 9); } else { term_theme_apply(term, &term->conf->colors); term->colors.active_theme = COLOR_THEME1; + + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;1n", 9); } wayl_win_alpha_changed(term->window); diff --git a/terminal.h b/terminal.h index 45e13925..e6499ef7 100644 --- a/terminal.h +++ b/terminal.h @@ -518,6 +518,7 @@ struct terminal { bool num_lock_modifier; bool bell_action_enabled; + bool report_theme_changes; /* Saved DECSET modes - we save the SET state */ struct { @@ -548,6 +549,7 @@ struct terminal { bool ime:1; bool app_sync_updates:1; bool grapheme_shaping:1; + bool report_theme_changes:1; bool size_notifications:1; From bc5b71666867c00e7dc850740bb5cc2a13a9c2a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 21 Apr 2025 12:19:11 +0200 Subject: [PATCH 1168/1323] config: add initial-color-theme=1|2 This option selects which color theme to use by default. I.e. at startup, and after a reset. This is useful with combined theme files, where a single file defines e.g. both a dark and light version of the theme. --- CHANGELOG.md | 2 ++ config.c | 11 ++++++++++- config.h | 6 ++++++ doc/foot.ini.5.scd | 12 ++++++++++++ foot.ini | 1 + terminal.c | 39 ++++++++++++++++++++++++++------------- terminal.h | 2 +- 7 files changed, 58 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c40b8cf..41457205 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,8 @@ * Support for private mode 2031 - [_Dark and Light Mode Detection_](https://contour-terminal.org/vt-extensions/color-palette-update-notifications/) ([#2025][2025]) +* Added `initial-color-theme=1|2` config option. `1` uses colors from + the `[colors]` section, `2` uses `[colors2]`. [2025]: https://codeberg.org/dnkl/foot/issues/2025 diff --git a/config.c b/config.c index 921b2d68..7dc41aa6 100644 --- a/config.c +++ b/config.c @@ -1098,6 +1098,15 @@ parse_section_main(struct context *ctx) return true; } + else if (streq(key, "initial-color-theme")) { + _Static_assert( + sizeof(conf->initial_color_theme) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum(ctx, (const char*[]){"1", "2", NULL}, + (int *)&conf->initial_color_theme); + } + else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; @@ -3402,7 +3411,7 @@ config_load(struct config *conf, const char *conf_path, .url = false, }, }, - + .initial_color_theme = COLOR_THEME1, .cursor = { .style = CURSOR_BLOCK, .unfocused_style = CURSOR_UNFOCUSED_HOLLOW, diff --git a/config.h b/config.h index 8954b270..cbdf11b1 100644 --- a/config.h +++ b/config.h @@ -190,6 +190,11 @@ struct color_theme { } use_custom; }; +enum which_color_theme { + COLOR_THEME1, + COLOR_THEME2, +}; + struct config { char *term; char *shell; @@ -305,6 +310,7 @@ struct config { struct color_theme colors; struct color_theme colors2; + enum which_color_theme initial_color_theme; struct { enum cursor_style style; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 85a7cf7b..f7da2c53 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -349,6 +349,18 @@ empty string to be set, but it must be quoted: *KEY=""*) Default: _yes_ +*initial-color-theme* + Selects which color theme to use, *1*, or *2*. + + *1* uses the colors defined in the *colors* section, while *2* + uses the colors from the *colors2* section. + + Use the *color-theme-switch-1*, *color-theme-switch-2* and + *color-theme-toggle* key bindings to switch between the two themes + at runtime. + + Default: _1_ + *initial-window-size-pixels* Initial window width and height in _pixels_ (subject to output scaling), in the form _WIDTHxHEIGHT_. The height _includes_ the diff --git a/foot.ini b/foot.ini index ebbc8ca7..563558db 100644 --- a/foot.ini +++ b/foot.ini @@ -23,6 +23,7 @@ # box-drawings-uses-font-glyphs=no # dpi-aware=no +# initial-color-theme=1 # initial-window-size-pixels=700x500 # Or, # initial-window-size-chars=<COLSxROWS> # initial-window-mode=windowed diff --git a/terminal.c b/terminal.c index 29e03b8e..d25516cb 100644 --- a/terminal.c +++ b/terminal.c @@ -1262,6 +1262,12 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, const bool ten_bit_surfaces = conf->tweak.surface_bit_depth == SHM_10_BIT; + const struct color_theme *theme = NULL; + switch (conf->initial_color_theme) { + case COLOR_THEME1: theme = &conf->colors; break; + case COLOR_THEME2: theme = &conf->colors2; break; + } + /* Initialize configure-based terminal attributes */ *term = (struct terminal) { .fdm = fdm, @@ -1279,7 +1285,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, }, .font_dpi = 0., .font_dpi_before_unmap = -1., - .font_subpixel = (conf->colors.alpha == 0xffff /* Can't do subpixel rendering on transparent background */ + .font_subpixel = (theme->alpha == 0xffff /* Can't do subpixel rendering on transparent background */ ? FCFT_SUBPIXEL_DEFAULT : FCFT_SUBPIXEL_NONE), .cursor_keys_mode = CURSOR_KEYS_NORMAL, @@ -1295,15 +1301,15 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .state = 0, /* STATE_GROUND */ }, .colors = { - .fg = conf->colors.fg, - .bg = conf->colors.bg, - .alpha = conf->colors.alpha, - .cursor_fg = (conf->colors.use_custom.cursor ? 1u << 31 : 0) | conf->colors.cursor.text, - .cursor_bg = (conf->colors.use_custom.cursor ? 1u << 31 : 0) | conf->colors.cursor.cursor, - .selection_fg = conf->colors.selection_fg, - .selection_bg = conf->colors.selection_bg, - .use_custom_selection = conf->colors.use_custom.selection, - .active_theme = COLOR_THEME1, + .fg = theme->fg, + .bg = theme->bg, + .alpha = theme->alpha, + .cursor_fg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.text, + .cursor_bg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.cursor, + .selection_fg = theme->selection_fg, + .selection_bg = theme->selection_bg, + .use_custom_selection = theme->use_custom.selection, + .active_theme = conf->initial_color_theme, }, .color_stack = { .stack = NULL, @@ -1434,7 +1440,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, xassert(tll_length(term->wl->monitors) > 0); term->scale = tll_front(term->wl->monitors).scale; - memcpy(term->colors.table, term->conf->colors.table, sizeof(term->colors.table)); + memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); /* Initialize the Wayland window backend */ if ((term->window = wayl_win_init(term, token)) == NULL) @@ -2148,11 +2154,18 @@ term_reset(struct terminal *term, bool hard) if (!hard) return; + const struct color_theme *theme = NULL; + + switch (term->conf->initial_color_theme) { + case COLOR_THEME1: theme = &term->conf->colors; break; + case COLOR_THEME2: theme = &term->conf->colors2; break; + } + term->flash.active = false; term->blink.state = BLINK_ON; fdm_del(term->fdm, term->blink.fd); term->blink.fd = -1; - term_theme_apply(term, &term->conf->colors); - term->colors.active_theme = COLOR_THEME1; + term_theme_apply(term, theme); + term->colors.active_theme = term->conf->initial_color_theme; free(term->color_stack.stack); term->color_stack.stack = NULL; term->color_stack.size = 0; diff --git a/terminal.h b/terminal.h index e6499ef7..4639fa69 100644 --- a/terminal.h +++ b/terminal.h @@ -405,7 +405,7 @@ struct colors { uint32_t selection_fg; uint32_t selection_bg; bool use_custom_selection; - enum { COLOR_THEME1, COLOR_THEME2 } active_theme; + enum which_color_theme active_theme; }; struct terminal { From 537092e6434361307f2c7d88b327920dc176c8d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 21 Apr 2025 12:20:28 +0200 Subject: [PATCH 1169/1323] themes: solarized: add dark/light combined theme file These themes uses the 'colors' section to define the dark variant, and 'colors2' to define the light variant. --- themes/solarized | 47 ++++++++++++++++++++++++++++ themes/solarized-normal-brights | 54 +++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 themes/solarized create mode 100644 themes/solarized-normal-brights diff --git a/themes/solarized b/themes/solarized new file mode 100644 index 00000000..335c738e --- /dev/null +++ b/themes/solarized @@ -0,0 +1,47 @@ +# -*- conf -*- +# Solarized dark+light + +# Dark +[colors] +cursor= 002b36 93a1a1 +background= 002b36 +foreground= 839496 +regular0= 073642 +regular1= dc322f +regular2= 859900 +regular3= b58900 +regular4= 268bd2 +regular5= d33682 +regular6= 2aa198 +regular7= eee8d5 +bright0= 002b36 +bright1= cb4b16 +bright2= 586e75 +bright3= 657b83 +bright4= 839496 +bright5= 6c71c4 +bright6= 93a1a1 +bright7= fdf6e3 + + +# Light +[colors2] +cursor= fdf6e3 586e75 +background= fdf6e3 +foreground= 657b83 +regular0= eee8d5 +regular1= dc322f +regular2= 859900 +regular3= b58900 +regular4= 268bd2 +regular5= d33682 +regular6= 2aa198 +regular7= 073642 +bright0= cb4b16 +bright1= fdf6e3 +bright2= 93a1a1 +bright3= 839496 +bright4= 657b83 +bright5= 6c71c4 +bright6= 586e75 +bright7= 002b36 diff --git a/themes/solarized-normal-brights b/themes/solarized-normal-brights new file mode 100644 index 00000000..a7724cd3 --- /dev/null +++ b/themes/solarized-normal-brights @@ -0,0 +1,54 @@ +# -*- conf -*- +# Solarized dark+light +# +# Bright colors are brighter versions of the regular colors, instead +# of the normal solarized palette. +# +# They were generated by taking the regular colors, decoding from sRGB +# to linear, multiplying the linear RGB values by 1.8, and then +# encoding to sRGB again. + +# Dark +[colors] +cursor= 002b36 93a1a1 +background= 002b36 +foreground= 839496 +regular0= 073642 +regular1= dc322f +regular2= 859900 +regular3= b58900 +regular4= 268bd2 +regular5= d33682 +regular6= 2aa198 +regular7= eee8d5 +bright0= 0c4958 +bright1= ff4440 +bright2= aec700 +bright3= ebb300 +bright4= 34b5ff +bright5= ff49aa +bright6= 3ad2c6 +bright7= ffffff + + +# Light +[colors2] +cursor= fdf6e3 586e75 +background= fdf6e3 +foreground= 657b83 +regular0= eee8d5 +regular1= dc322f +regular2= 859900 +regular3= b58900 +regular4= 268bd2 +regular5= d33682 +regular6= 2aa198 +regular7= 073642 +bright0= ffffff +bright1= ff4440 +bright2= aec700 +bright3= ebb300 +bright4= 34b5ff +bright5= ff49aa +bright6= 3ad2c6 +bright7= 0c4958 From 1dc14a300107e13b87662a010968763f64ed9321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 26 Apr 2025 15:23:44 +0200 Subject: [PATCH 1170/1323] themes: selenized: add dark/light combined theme file --- themes/selenized | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 themes/selenized diff --git a/themes/selenized b/themes/selenized new file mode 100644 index 00000000..cde35723 --- /dev/null +++ b/themes/selenized @@ -0,0 +1,48 @@ +# -*- conf -*- +# Selenized dark + +[colors] +cursor = 103c48 53d6c7 +background= 103c48 +foreground= adbcbc + +regular0= 184956 +regular1= fa5750 +regular2= 75b938 +regular3= dbb32d +regular4= 4695f7 +regular5= f275be +regular6= 41c7b9 +regular7= 72898f + +bright0= 2d5b69 +bright1= ff665c +bright2= 84c747 +bright3= ebc13d +bright4= 58a3ff +bright5= ff84cd +bright6= 53d6c7 +bright7= cad8d9 + +[colors2] +cursor=fbf3db 00978a +background= fbf3db +foreground= 53676d + +regular0= ece3cc +regular1= d2212d +regular2= 489100 +regular3= ad8900 +regular4= 0072d4 +regular5= ca4898 +regular6= 009c8f +regular7= 909995 + +bright0= d5cdb6 +bright1= cc1729 +bright2= 428b00 +bright3= a78300 +bright4= 006dce +bright5= c44392 +bright6= 00978a +bright7= 3a4d53 From 6a1c3b89c2f0b5f34d7c2f9957846eb90ca1f62c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 26 Apr 2025 15:26:22 +0200 Subject: [PATCH 1171/1323] themes: gruvbox: add dark/light combined theme file --- themes/gruvbox | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 themes/gruvbox diff --git a/themes/gruvbox b/themes/gruvbox new file mode 100644 index 00000000..6bc97352 --- /dev/null +++ b/themes/gruvbox @@ -0,0 +1,42 @@ +# -*- conf -*- +# Gruvbox + +[colors] +background=282828 +foreground=ebdbb2 +regular0=282828 +regular1=cc241d +regular2=98971a +regular3=d79921 +regular4=458588 +regular5=b16286 +regular6=689d6a +regular7=a89984 +bright0=928374 +bright1=fb4934 +bright2=b8bb26 +bright3=fabd2f +bright4=83a598 +bright5=d3869b +bright6=8ec07c +bright7=ebdbb2 + +[colors2] +background=fbf1c7 +foreground=3c3836 +regular0=fbf1c7 +regular1=cc241d +regular2=98971a +regular3=d79921 +regular4=458588 +regular5=b16286 +regular6=689d6a +regular7=7c6f64 +bright0=928374 +bright1=9d0006 +bright2=79740e +bright3=b57614 +bright4=076678 +bright5=8f3f71 +bright6=427b58 +bright7=3c3836 From d3e45791bde9af9c6ae8e0380f4f810778f59a57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 26 Apr 2025 15:26:31 +0200 Subject: [PATCH 1172/1323] themes: nvim: add dark/light combined theme file --- themes/nvim | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 themes/nvim diff --git a/themes/nvim b/themes/nvim new file mode 100644 index 00000000..bf629c0a --- /dev/null +++ b/themes/nvim @@ -0,0 +1,56 @@ +# -*- conf -*- +# Neovim Dark theme +# Uses the dark color palette from the default Neovim color scheme +# See: https://github.com/neovim/neovim/blob/fb6c059dc55c8d594102937be4dd70f5ff51614a/src/nvim/highlight_group.c#L419 + +[colors] +cursor=14161b e0e2ea # NvimDarkGrey2 NvimLightGrey2 +foreground=e0e2ea # NvimLightGrey2 +background=14161b # NvimDarkGrey2 + +selection-foreground=e0e2ea # NvimLightGrey2 +selection-background=4f5258 # NvimDarkGrey4 + +regular0=07080d # NvimDarkGrey1 +regular1=ffc0b9 # NvimLightRed +regular2=b3f6c0 # NvimLightGreen +regular3=fce094 # NvimLightYellow +regular4=a6dbff # NvimLightBlue +regular5=ffcaff # NvimLightMagenta +regular6=8cf8f7 # NvimLightCyan +regular7=c4c6cd # NvimLightGrey3 + +bright0=2c2e33 # NvimDarkGrey3 +bright1=ffc0b9 # NvimLightRed +bright2=b3f6c0 # NvimLightGreen +bright3=fce094 # NvimLightYellow +bright4=a6dbff # NvimLightBlue +bright5=ffcaff # NvimLightMagenta +bright6=8cf8f7 # NvimLightCyan +bright7=eef1f8 # NvimLightGrey1 + +[colors2] +cursor=e0e2ea 14161b # NvimLightGrey2 NvimDarkGrey2 +foreground=14161b # NvimDarkGrey2 +background=e0e2ea # NvimLightGrey2 + +selection-foreground=14161b # NvimDarkGrey2 +selection-background=9b9ea4 # NvimLightGrey4 + +regular0=eef1f8 # NvimLightGrey1 +regular1=590008 # NvimDarkRed +regular2=005523 # NvimDarkGreen +regular3=6b5300 # NvimDarkYellow +regular4=004c73 # NvimDarkBlue +regular5=470045 # NvimDarkMagenta +regular6=007373 # NvimDarkCyan +regular7=2c2e33 # NvimDarkGrey3 + +bright0=c4c6cd # NvimLightGrey3 +bright1=590008 # NvimDarkRed +bright2=005523 # NvimDarkGreen +bright3=6b5300 # NvimDarkYellow +bright4=004c73 # NvimDarkBlue +bright5=470045 # NvimDarkMagenta +bright6=007373 # NvimDarkCyan +bright7=07080d # NvimDarkGrey1 From 8273514d3c98108fbd02316fb5f44a721cca8e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 26 Apr 2025 15:26:36 +0200 Subject: [PATCH 1173/1323] themes: paper-color: add dark/light combined theme file --- themes/paper-color | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 themes/paper-color diff --git a/themes/paper-color b/themes/paper-color new file mode 100644 index 00000000..f158c148 --- /dev/null +++ b/themes/paper-color @@ -0,0 +1,49 @@ +# -*- conf -*- +# PaperColorDark +# Palette based on https://github.com/NLKNguyen/papercolor-theme + +[colors] +cursor=1c1c1c eeeeee +background=1c1c1c +foreground=eeeeee +regular0=1c1c1c # black +regular1=af005f # red +regular2=5faf00 # green +regular3=d7af5f # yellow +regular4=5fafd7 # blue +regular5=808080 # magenta +regular6=d7875f # cyan +regular7=d0d0d0 # white +bright0=bcbcbc # bright black +bright1=5faf5f # bright red +bright2=afd700 # bright green +bright3=af87d7 # bright yellow +bright4=ffaf00 # bright blue +bright5=ff5faf # bright magenta +bright6=00afaf # bright cyan +bright7=5f8787 # bright white +# selection-foreground=1c1c1c +# selection-background=af87d7 + +[colors2] +cursor=eeeeee 444444 +background=eeeeee +foreground=444444 +regular0=eeeeee # black +regular1=af0000 # red +regular2=008700 # green +regular3=5f8700 # yellow +regular4=0087af # blue +regular5=878787 # magenta +regular6=005f87 # cyan +regular7=764e37 # white +bright0=bcbcbc # bright black +bright1=d70000 # bright red +bright2=d70087 # bright green +bright3=8700af # bright yellow +bright4=d75f00 # bright blue +bright5=d75f00 # bright magenta +bright6=4c7a5d # bright cyan +bright7=005faf # bright white +# selection-foreground=eeeeee +# selection-background=0087af From 4d70bb7b420c748ed6fc33262a3b0cf0bc0865eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 26 Apr 2025 18:15:31 +0200 Subject: [PATCH 1174/1323] changelog: mention the new combined dark/light theme files --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41457205..91627d0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,13 @@ ([#2025][2025]) * Added `initial-color-theme=1|2` config option. `1` uses colors from the `[colors]` section, `2` uses `[colors2]`. +* Combined dark/light theme files for (dark variant is the default, + set `initial-color-theme=2` to use the light variant by default): + - gruvbox + - nvim + - paper-color + - selenized + - solarized [2025]: https://codeberg.org/dnkl/foot/issues/2025 From d20fbc68078fe5b3bc8782e8e7827caf406708e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 27 Apr 2025 07:46:09 +0200 Subject: [PATCH 1175/1323] config: parse_color_theme(): make NOINLINE --- config.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.c b/config.c index 7dc41aa6..04a9c9d4 100644 --- a/config.c +++ b/config.c @@ -1350,7 +1350,7 @@ parse_section_regex(struct context *ctx) } } -static bool +static bool NOINLINE parse_color_theme(struct context *ctx, struct color_theme *theme) { const char *key = ctx->key; From 97910a5cbac84bb99254849ee1b5c00b00b983c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 27 Apr 2025 10:14:45 +0200 Subject: [PATCH 1176/1323] scripts: srgb: use 2.2 gamma TF instead of piece-wise sRGB TF --- CHANGELOG.md | 12 ++++++++++++ scripts/srgb.py | 29 +++++------------------------ 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91627d0d..f231a1be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,9 @@ ### Changed * `cursor.color` moved to `colors.cursor`. +* `gamma-correct-blending=yes` now uses a pure gamma 2.2 transfer + function, instead of the piece-wise sRGB transfer function, to match + what compositors do. ### Deprecated @@ -99,6 +102,15 @@ ### Removed ### Fixed + +* Wrong colors when `gamma-correct-blending=yes` (the default when + there is compositor support). Note that some colors will still be + off by a **very** small amount, due to loss of precision when + converting to a linear color space. ([#2035][2035]). + +[2035]: https://codeberg.org/dnkl/foot/issues/2035 + + ### Security ### Contributors diff --git a/scripts/srgb.py b/scripts/srgb.py index 7655dbe4..12056956 100755 --- a/scripts/srgb.py +++ b/scripts/srgb.py @@ -5,21 +5,16 @@ import math import sys +# Note: we use a pure gamma 2.2 function, rather than the piece-wise +# sRGB transfer function, since that is what all compositors do. + def srgb_to_linear(f: float) -> float: assert(f >= 0 and f <= 1.0) - - if f <= 0.04045: - return f / 12.92 - - return math.pow((f + 0.055) / 1.055, 2.4) + return math.pow(f, 2.2) def linear_to_srgb(f: float) -> float: - if f < 0.0031308: - return f * 12.92 - - return 1.055 * math.pow(f, 1 / 2.4) - 0.055 - + return math.pow(f, 1 / 2.2) def main(): @@ -29,24 +24,10 @@ def main(): opts = parser.parse_args() linear_table: list[int] = [] - srgb_table: list[int] = [] for i in range(256): linear_table.append(int(srgb_to_linear(float(i) / 255) * 65535 + 0.5)) - for i in range(4096): - srgb_table.append(int(linear_to_srgb(float(i) / 4095) * 255 + 0.5)) - - for i in range(256): - while True: - linear = linear_table[i] - srgb = srgb_table[linear >> 4] - - if i == srgb: - break - - linear_table[i] += 1 - opts.h_output.write("#pragma once\n") opts.h_output.write("#include <stdint.h>\n") From d7b48d3924eac9013fbd6b1f2220f44d2f392b9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 28 Apr 2025 12:32:40 +0200 Subject: [PATCH 1177/1323] doc: foot.ini: gamma-correct: tweak wording of 8- vs. 10-bit surfaces --- doc/foot.ini.5.scd | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index f7da2c53..215809f8 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -220,11 +220,12 @@ empty string to be set, but it must be quoted: *KEY=""*) than intended when rendered with gamma-correct blending, since the font designer set the font weight based on incorrect rendering. - Note that some colors (especially dark ones) will look a bit + Note that some colors (especially dark ones) may be slightly off. The reason for this is loss of color precision, due to foot - using 8-bit surfaces (i.e. each color channel is 8 bits). The - amount of errors can be reduced by using 10-bit surfaces; see - *tweak.surface-bit-depth*. + using 8-bit surfaces (i.e. each color channel is 8 bits). In all + known cases, the difference is small enough not to be noticed + though. The amount of errors can be reduced by using 10-bit + surfaces; see *tweak.surface-bit-depth*. Default: enabled when compositor support is available From eb79a27900603aeb171e3c43c00452a11d695a86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 30 Apr 2025 09:28:35 +0200 Subject: [PATCH 1178/1323] readme: donations: add liberapay --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3395aff0..95f43fb4 100644 --- a/README.md +++ b/README.md @@ -689,6 +689,7 @@ Every now and then I post foot related updates on # Sponsoring/donations +* Liberapay: https://liberapay.com/dnkl * GitHub Sponsors: https://github.com/sponsors/dnkl From 1ea20b1b707ded204e6239f2704ae97ec9cd8852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 26 Apr 2025 10:41:14 +0200 Subject: [PATCH 1179/1323] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aeabbe14..3c48c41c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.22.1](#1-22-1) * [1.22.0](#1-22-0) * [1.21.0](#1-21-0) @@ -60,6 +61,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.22.1 ### Fixed From ce424e0990f9bf20995cfde36f3ec635512d64fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 27 Apr 2025 10:14:45 +0200 Subject: [PATCH 1180/1323] scripts: srgb: use 2.2 gamma TF instead of piece-wise sRGB TF --- CHANGELOG.md | 15 +++++++++++++++ scripts/srgb.py | 29 +++++------------------------ 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c48c41c..645c6c38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,9 +64,24 @@ ## Unreleased ### Added ### Changed + +* `gamma-correct-blending=yes` now uses a pure gamma 2.2 transfer + function, instead of the piece-wise sRGB transfer function, to match + what compositors do. + + ### Deprecated ### Removed ### Fixed + +* Wrong colors when `gamma-correct-blending=yes` (the default when + there is compositor support). Note that some colors will still be + off by a **very** small amount, due to loss of precision when + converting to a linear color space. ([#2035][2035]). + +[2035]: https://codeberg.org/dnkl/foot/issues/2035 + + ### Security ### Contributors diff --git a/scripts/srgb.py b/scripts/srgb.py index 7655dbe4..12056956 100755 --- a/scripts/srgb.py +++ b/scripts/srgb.py @@ -5,21 +5,16 @@ import math import sys +# Note: we use a pure gamma 2.2 function, rather than the piece-wise +# sRGB transfer function, since that is what all compositors do. + def srgb_to_linear(f: float) -> float: assert(f >= 0 and f <= 1.0) - - if f <= 0.04045: - return f / 12.92 - - return math.pow((f + 0.055) / 1.055, 2.4) + return math.pow(f, 2.2) def linear_to_srgb(f: float) -> float: - if f < 0.0031308: - return f * 12.92 - - return 1.055 * math.pow(f, 1 / 2.4) - 0.055 - + return math.pow(f, 1 / 2.2) def main(): @@ -29,24 +24,10 @@ def main(): opts = parser.parse_args() linear_table: list[int] = [] - srgb_table: list[int] = [] for i in range(256): linear_table.append(int(srgb_to_linear(float(i) / 255) * 65535 + 0.5)) - for i in range(4096): - srgb_table.append(int(linear_to_srgb(float(i) / 4095) * 255 + 0.5)) - - for i in range(256): - while True: - linear = linear_table[i] - srgb = srgb_table[linear >> 4] - - if i == srgb: - break - - linear_table[i] += 1 - opts.h_output.write("#pragma once\n") opts.h_output.write("#include <stdint.h>\n") From 172f67a8df71d859e3f610dabffabff0623775a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 28 Apr 2025 12:32:40 +0200 Subject: [PATCH 1181/1323] doc: foot.ini: gamma-correct: tweak wording of 8- vs. 10-bit surfaces --- doc/foot.ini.5.scd | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index cffbf9c5..95e491eb 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -220,11 +220,12 @@ empty string to be set, but it must be quoted: *KEY=""*) than intended when rendered with gamma-correct blending, since the font designer set the font weight based on incorrect rendering. - Note that some colors (especially dark ones) will look a bit + Note that some colors (especially dark ones) may be slightly off. The reason for this is loss of color precision, due to foot - using 8-bit surfaces (i.e. each color channel is 8 bits). The - amount of errors can be reduced by using 10-bit surfaces; see - *tweak.surface-bit-depth*. + using 8-bit surfaces (i.e. each color channel is 8 bits). In all + known cases, the difference is small enough not to be noticed + though. The amount of errors can be reduced by using 10-bit + surfaces; see *tweak.surface-bit-depth*. Default: enabled when compositor support is available From fc293bad5e3989a7aef778ad8d0dc054794a990c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 30 Apr 2025 10:23:20 +0200 Subject: [PATCH 1182/1323] changelog: prepare 1.22.2 --- CHANGELOG.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 645c6c38..8ef3ae43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.22.2](#1-22-2) * [1.22.1](#1-22-1) * [1.22.0](#1-22-0) * [1.21.0](#1-21-0) @@ -61,8 +61,8 @@ * [1.2.0](#1-2-0) -## Unreleased -### Added +## 1.22.2 + ### Changed * `gamma-correct-blending=yes` now uses a pure gamma 2.2 transfer @@ -70,8 +70,6 @@ what compositors do. -### Deprecated -### Removed ### Fixed * Wrong colors when `gamma-correct-blending=yes` (the default when @@ -82,10 +80,6 @@ [2035]: https://codeberg.org/dnkl/foot/issues/2035 -### Security -### Contributors - - ## 1.22.1 ### Fixed From 513e91c33a0b1593f80b887e42c38ef4ba981634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 30 Apr 2025 10:23:51 +0200 Subject: [PATCH 1183/1323] meson: bump version to 1.22.2 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 7ac033b5..b3163586 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.22.1', + version: '1.22.2', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From 1dc8354534c9b1f1c7ae7e1bbe1cbd3df0cd1260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 30 Apr 2025 11:43:13 +0200 Subject: [PATCH 1184/1323] readme: add liberapay donation button --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 95f43fb4..e8f3c8cd 100644 --- a/README.md +++ b/README.md @@ -692,6 +692,8 @@ Every now and then I post foot related updates on * Liberapay: https://liberapay.com/dnkl * GitHub Sponsors: https://github.com/sponsors/dnkl +[![Donate using Liberapay](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/dnkl/donate) + # License From b07ce56321404a34fc02635ecf4d5b63ddddf1ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 1 May 2025 08:09:08 +0200 Subject: [PATCH 1185/1323] config: gamma-correct-blending: disable by default --- CHANGELOG.md | 1 + config.c | 15 +++------------ config.h | 4 +--- doc/foot.ini.5.scd | 2 +- foot.ini | 1 + render.c | 2 +- tests/test-config.c | 1 + wayland.c | 6 ++---- 8 files changed, 11 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f57ca9fb..8521fd86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,7 @@ ### Changed * `cursor.color` moved to `colors.cursor`. +* `gamma-correct-blending` now defaults to `no` instead of `yes`. ### Deprecated diff --git a/config.c b/config.c index 04a9c9d4..f182241c 100644 --- a/config.c +++ b/config.c @@ -1086,17 +1086,8 @@ parse_section_main(struct context *ctx) return true; } - else if (streq(key, "gamma-correct-blending")) { - bool gamma_correct; - if (!value_to_bool(ctx, &gamma_correct)) - return false; - - conf->gamma_correct = - gamma_correct - ? GAMMA_CORRECT_ENABLED - : GAMMA_CORRECT_DISABLED; - return true; - } + else if (streq(key, "gamma-correct-blending")) + return value_to_bool(ctx, &conf->gamma_correct); else if (streq(key, "initial-color-theme")) { _Static_assert( @@ -3362,7 +3353,7 @@ config_load(struct config *conf, const char *conf_path, .underline_thickness = {.pt = 0., .px = -1}, .strikeout_thickness = {.pt = 0., .px = -1}, .dpi_aware = false, - .gamma_correct = GAMMA_CORRECT_AUTO, + .gamma_correct = false, .security = { .osc52 = OSC52_ENABLED, }, diff --git a/config.h b/config.h index cbdf11b1..be465d68 100644 --- a/config.h +++ b/config.h @@ -232,9 +232,7 @@ struct config { enum { STARTUP_WINDOWED, STARTUP_MAXIMIZED, STARTUP_FULLSCREEN } startup_mode; bool dpi_aware; - enum {GAMMA_CORRECT_DISABLED, - GAMMA_CORRECT_ENABLED, - GAMMA_CORRECT_AUTO} gamma_correct; + bool gamma_correct; struct config_font_list fonts[4]; struct font_size_adjustment font_size_adjustment; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 215809f8..0f06d0ca 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -227,7 +227,7 @@ empty string to be set, but it must be quoted: *KEY=""*) though. The amount of errors can be reduced by using 10-bit surfaces; see *tweak.surface-bit-depth*. - Default: enabled when compositor support is available + Default: _no_. *box-drawings-uses-font-glyphs* Boolean. When disabled, foot generates box/line drawing characters diff --git a/foot.ini b/foot.ini index 563558db..f3ef6d85 100644 --- a/foot.ini +++ b/foot.ini @@ -22,6 +22,7 @@ # strikeout-thickness=<font strikeout thickness> # box-drawings-uses-font-glyphs=no # dpi-aware=no +# gamma-correct-blending=no # initial-color-theme=1 # initial-window-size-pixels=700x500 # Or, diff --git a/render.c b/render.c index b0d21d18..0ee60d65 100644 --- a/render.c +++ b/render.c @@ -5251,6 +5251,6 @@ render_xcursor_set(struct seat *seat, struct terminal *term, bool render_do_linear_blending(const struct terminal *term) { - return term->conf->gamma_correct != GAMMA_CORRECT_DISABLED && + return term->conf->gamma_correct && term->wl->color_management.img_description != NULL; } diff --git a/tests/test-config.c b/tests/test-config.c index 7dfb8556..bab57788 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -490,6 +490,7 @@ test_section_main(void) test_boolean(&ctx, &parse_section_main, "box-drawings-uses-font-glyphs", &conf.box_drawings_uses_font_glyphs); test_boolean(&ctx, &parse_section_main, "locked-title", &conf.locked_title); test_boolean(&ctx, &parse_section_main, "dpi-aware", &conf.dpi_aware); + test_boolean(&ctx, &parse_section_main, "gamma-correct-blending", &conf.gamma_correct); test_pt_or_px(&ctx, &parse_section_main, "font-size-adjustment", &conf.font_size_adjustment.pt_or_px); /* TODO: test ‘N%’ values too */ test_pt_or_px(&ctx, &parse_section_main, "line-height", &conf.line_height); diff --git a/wayland.c b/wayland.c index 853124be..9b143508 100644 --- a/wayland.c +++ b/wayland.c @@ -1980,7 +1980,7 @@ wayl_win_init(struct terminal *term, const char *token) xdg_toplevel_icon_v1_destroy(icon); } - if (term->conf->gamma_correct != GAMMA_CORRECT_DISABLED) { + if (term->conf->gamma_correct) { if (wayl->color_management.img_description != NULL) { xassert(wayl->color_management.manager != NULL); @@ -1990,7 +1990,7 @@ wayl_win_init(struct terminal *term, const char *token) wp_color_management_surface_v1_set_image_description( win->surface.color_management, wayl->color_management.img_description, WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL); - } else if (term->conf->gamma_correct == GAMMA_CORRECT_ENABLED) { + } else { if (wayl->color_management.manager == NULL) { LOG_WARN( "gamma-corrected-blending: disabling; " @@ -2005,8 +2005,6 @@ wayl_win_init(struct terminal *term, const char *token) LOG_WARN(" - TF: ext_linear"); LOG_WARN(" - primaries: sRGB"); } - } else { - /* "auto" - don't warn */ } } From e5a0755451b736c635be58764799ef8d7b536e8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 1 May 2025 08:34:49 +0200 Subject: [PATCH 1186/1323] config: tweak.surface-bit-depth now defaults to 'auto' When set to 'auto', use 10-bit surfaces if gamma-correct blending is enabled, and 8-bit surfaces otherwise. Note that we may still fallback to 8-bit surfaces (without disabling gamma-correct blending) if the compositor does not support 10-bit surfaces. Closes #2082 --- CHANGELOG.md | 10 ++++++++++ config.c | 4 ++-- config.h | 8 +++++++- doc/foot.ini.5.scd | 41 ++++++++++++++++++++++------------------- pgo/pgo.c | 5 +++-- render.c | 31 +++++++++++++------------------ render.h | 2 -- shm.c | 15 ++++++++++++--- shm.h | 5 ++++- sixel.c | 4 ++-- terminal.c | 42 +++++++++++++++++++++--------------------- wayland.c | 7 +++++++ wayland.h | 2 ++ 13 files changed, 105 insertions(+), 71 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8521fd86..aada556e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,7 @@ - paper-color - selenized - solarized +* `auto` to the `tweak.surface-bit-depth` option. [2025]: https://codeberg.org/dnkl/foot/issues/2025 @@ -92,6 +93,9 @@ * `cursor.color` moved to `colors.cursor`. * `gamma-correct-blending` now defaults to `no` instead of `yes`. +* `tweak.surface-bit-depth` default value changed to `auto`; uses + 10-bit surfaces when `gamma-correct-blending=yes`, and 8-bit + surfaces otherwise. ### Deprecated @@ -101,6 +105,12 @@ ### Removed ### Fixed + +* Inaccurate colors when `gamma-correct-blending=yes` ([#2082][2082]). + +[2082]: https://codeberg.org/dnkl/foot/issues/2082 + + ### Security ### Contributors diff --git a/config.c b/config.c index f182241c..64e45135 100644 --- a/config.c +++ b/config.c @@ -2813,7 +2813,7 @@ parse_section_tweak(struct context *ctx) return value_to_enum( ctx, - (const char *[]){"8-bit", "10-bit", NULL}, + (const char *[]){"auto", "8-bit", "10-bit", NULL}, (int *)&conf->tweak.surface_bit_depth); } @@ -3463,7 +3463,7 @@ config_load(struct config *conf, const char *conf_path, .box_drawing_solid_shades = true, .font_monospace_warn = true, .sixel = true, - .surface_bit_depth = 8, + .surface_bit_depth = SHM_BITS_AUTO, }, .touch = { diff --git a/config.h b/config.h index be465d68..80081906 100644 --- a/config.h +++ b/config.h @@ -195,6 +195,12 @@ enum which_color_theme { COLOR_THEME2, }; +enum shm_bit_depth { + SHM_BITS_AUTO, + SHM_BITS_8, + SHM_BITS_10 +}; + struct config { char *term; char *shell; @@ -419,7 +425,7 @@ struct config { bool box_drawing_solid_shades; bool font_monospace_warn; bool sixel; - enum { SHM_8_BIT, SHM_10_BIT } surface_bit_depth; + enum shm_bit_depth surface_bit_depth; } tweak; struct { diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 0f06d0ca..b9ab9c6a 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -220,12 +220,13 @@ empty string to be set, but it must be quoted: *KEY=""*) than intended when rendered with gamma-correct blending, since the font designer set the font weight based on incorrect rendering. - Note that some colors (especially dark ones) may be slightly - off. The reason for this is loss of color precision, due to foot - using 8-bit surfaces (i.e. each color channel is 8 bits). In all - known cases, the difference is small enough not to be noticed - though. The amount of errors can be reduced by using 10-bit - surfaces; see *tweak.surface-bit-depth*. + In order to represent colors faithfully, higher precision image + buffers are required. By default, foot will use 10-bit color + channels, if available, when gamma-correct blending is + enabled. However, the high precision buffers are slow; if you want + to use gamma-correct blending, but prefer speed (throughput and + input latency) over accurate colors, you can force 8-bit color + channels by setting *tweak.surface-bit-depth=8-bit*. Default: _no_. @@ -2019,23 +2020,25 @@ any of these options. *surface-bit-depth* Selects which RGB bit depth to use for image buffers. One of - *8-bit*, or *10-bit*. + *auto*, *8-bit*, or *10-bit*. - The default, *8-bit*, uses 8 bits for all channels, alpha - included. When *gamma-correct-blending* is disabled, this is the - best option. + *auto* chooses bit depth depending on other settings, and + availability. - When *gamma-correct-blending* is enabled, you may want to enable - 10-bit surfaces, as that improves color precision. Be aware - however, that in this mode, the alpha channel is only 2 bits - instead of 8 bits. Thus, if you are using a transparent - background, you may want to use the default, *8-bit*, even if you - have gamma-correct blending enabled. + *8-bit*, uses 8 bits for each color channel, alpha included. This + is the default when *gamma-correct-blending=no*. - You should also note that 10-bit surface is much slower. This will - increase input latency and decrease rendering throughput. + *10-bit* uses 10 bits for each RGB channel, and 2 bits for the + alpha channel. Thus, it provides higher precision color channels, + but a lower precision alpha channel. It is the default when + *gamma-correct-blending=yes*, if supported by the compositor. - Default: _8-bit_ + Note that *10-bit* is much slower than *8-bit*; if you want to use + gamma-correct blending, and if you prefer speed (throughput and + input latency) over accurate colors, you can set + *surface-bit-depth=8-bit* explicitly. + + Default: _auto_ # SEE ALSO diff --git a/pgo/pgo.c b/pgo/pgo.c index 8a4967ba..757dcd06 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -129,7 +129,7 @@ render_worker_thread(void *_ctx) } bool -render_do_linear_blending(const struct terminal *term) +wayl_do_linear_blending(const struct wayland *wayl, const struct config *conf) { return false; } @@ -201,11 +201,12 @@ void urls_reset(struct terminal *term) {} void shm_unref(struct buffer *buf) {} void shm_chain_free(struct buffer_chain *chain) {} +enum shm_bit_depth shm_chain_bit_depth(const struct buffer_chain *chain) { return SHM_BITS_8; } struct buffer_chain * shm_chain_new( struct wayland *wayl, bool scrollable, size_t pix_instances, - bool ten_bit_it_if_capable) + enum shm_bit_depth desired_bit_depth) { return NULL; } diff --git a/render.c b/render.c index 0ee60d65..55c2ec4d 100644 --- a/render.c +++ b/render.c @@ -626,7 +626,7 @@ draw_cursor(const struct terminal *term, const struct cell *cell, pixman_color_t cursor_color; pixman_color_t text_color; cursor_colors_for_cell(term, cell, fg, bg, &cursor_color, &text_color, - render_do_linear_blending(term)); + wayl_do_linear_blending(term->wl, term->conf)); if (unlikely(!term->kbd_focus)) { switch (term->conf->cursor.unfocused_style) { @@ -820,7 +820,7 @@ render_cell(struct terminal *term, pixman_image_t *pix, if (cell->attrs.blink && term->blink.state == BLINK_OFF) _fg = color_blend_towards(_fg, 0x00000000, term->conf->dim.amount); - const bool gamma_correct = render_do_linear_blending(term); + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); pixman_color_t fg = color_hex_to_pixman(_fg, gamma_correct); pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); @@ -1180,7 +1180,8 @@ static void render_urgency(struct terminal *term, struct buffer *buf) { uint32_t red = term->colors.table[1]; - pixman_color_t bg = color_hex_to_pixman(red, render_do_linear_blending(term)); + pixman_color_t bg = color_hex_to_pixman( + red, wayl_do_linear_blending(term->wl, term->conf)); int width = min(min(term->margins.left, term->margins.right), min(term->margins.top, term->margins.bottom)); @@ -1211,7 +1212,7 @@ render_margin(struct terminal *term, struct buffer *buf, const int bmargin = term->height - term->margins.bottom; const int line_count = end_line - start_line; - const bool gamma_correct = render_do_linear_blending(term); + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); const uint32_t _bg = !term->reverse ? term->colors.bg : term->colors.fg; uint16_t alpha = term->colors.alpha; @@ -1699,7 +1700,7 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, if (unlikely(term->is_searching)) return; - const bool gamma_correct = render_do_linear_blending(term); + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); /* Adjust cursor position to viewport */ struct coord cursor; @@ -1970,7 +1971,8 @@ render_overlay(struct terminal *term) case OVERLAY_FLASH: color = color_hex_to_pixman_with_alpha( term->conf->colors.flash, - term->conf->colors.flash_alpha, render_do_linear_blending(term)); + term->conf->colors.flash_alpha, + wayl_do_linear_blending(term->wl, term->conf)); break; case OVERLAY_NONE: @@ -2312,7 +2314,7 @@ render_osd(struct terminal *term, const struct wayl_sub_surface *sub_surf, pixman_image_set_clip_region32(buf->pix[0], &clip); pixman_region32_fini(&clip); - const bool gamma_correct = render_do_linear_blending(term); + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); uint16_t alpha = _bg >> 24 | (_bg >> 24 << 8); pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); pixman_image_fill_rectangles( @@ -2453,7 +2455,7 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, if (info->width == 0 || info->height == 0) return; - const bool gamma_correct = render_do_linear_blending(term); + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); { /* Fully transparent - no need to do a color space transform */ @@ -2542,7 +2544,7 @@ get_csd_button_fg_color(const struct terminal *term) } return color_hex_to_pixman_with_alpha( - _color, alpha, render_do_linear_blending(term)); + _color, alpha, wayl_do_linear_blending(term->wl, term->conf)); } static void @@ -2819,7 +2821,7 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx, if (!term->visual_focus) _color = color_dim(term, _color); - const bool gamma_correct = render_do_linear_blending(term); + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha, gamma_correct); render_csd_part(term, surf->surf, buf, info->width, info->height, &color); @@ -3678,7 +3680,7 @@ render_search_box(struct terminal *term) : term->conf->colors.use_custom.search_box_no_match; /* Background - yellow on empty/match, red on mismatch (default) */ - const bool gamma_correct = render_do_linear_blending(term); + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); const pixman_color_t color = color_hex_to_pixman( is_match ? (custom_colors @@ -5247,10 +5249,3 @@ render_xcursor_set(struct seat *seat, struct terminal *term, seat->pointer.xcursor_pending = true; return true; } - -bool -render_do_linear_blending(const struct terminal *term) -{ - return term->conf->gamma_correct && - term->wl->color_management.img_description != NULL; -} diff --git a/render.h b/render.h index c7b8e4a5..81d2a905 100644 --- a/render.h +++ b/render.h @@ -47,5 +47,3 @@ struct csd_data { }; struct csd_data get_csd_data(const struct terminal *term, enum csd_surface surf_idx); - -bool render_do_linear_blending(const struct terminal *term); diff --git a/shm.c b/shm.c index 32e6bdd0..38944020 100644 --- a/shm.c +++ b/shm.c @@ -972,7 +972,7 @@ shm_unref(struct buffer *_buf) struct buffer_chain * shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, - bool ten_bit_if_capable) + enum shm_bit_depth desired_bit_depth) { pixman_format_code_t pixman_fmt_without_alpha = PIXMAN_x8r8g8b8; enum wl_shm_format shm_fmt_without_alpha = WL_SHM_FORMAT_XRGB8888; @@ -982,8 +982,7 @@ shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, static bool have_logged = false; - - if (ten_bit_if_capable) { + if (desired_bit_depth == SHM_BITS_10) { if (wayl->shm_have_argb2101010 && wayl->shm_have_xrgb2101010) { pixman_fmt_without_alpha = PIXMAN_x2r10g10b10; shm_fmt_without_alpha = WL_SHM_FORMAT_XRGB2101010; @@ -1058,3 +1057,13 @@ shm_chain_free(struct buffer_chain *chain) free(chain); } + +enum shm_bit_depth +shm_chain_bit_depth(const struct buffer_chain *chain) +{ + const pixman_format_code_t fmt = chain->pixman_fmt_with_alpha; + + return (fmt == PIXMAN_a2r10g10b10 || fmt == PIXMAN_a2b10g10r10) + ? SHM_BITS_10 + : SHM_BITS_8; +} diff --git a/shm.h b/shm.h index 2af185c9..8f8c406a 100644 --- a/shm.h +++ b/shm.h @@ -9,6 +9,7 @@ #include <tllist.h> +#include "config.h" #include "wayland.h" struct damage; @@ -46,9 +47,11 @@ void shm_set_max_pool_size(off_t max_pool_size); struct buffer_chain; struct buffer_chain *shm_chain_new( struct wayland *wayl, bool scrollable, size_t pix_instances, - bool ten_bit_it_if_capable); + enum shm_bit_depth desired_bit_depth); void shm_chain_free(struct buffer_chain *chain); +enum shm_bit_depth shm_chain_bit_depth(const struct buffer_chain *chain); + /* * Returns a single buffer. * diff --git a/sixel.c b/sixel.c index dd933d7a..680c258f 100644 --- a/sixel.c +++ b/sixel.c @@ -110,10 +110,10 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) term->sixel.image.height = 0; term->sixel.image.alloc_height = 0; term->sixel.image.bottom_pixel = 0; - term->sixel.linear_blending = render_do_linear_blending(term); + term->sixel.linear_blending = wayl_do_linear_blending(term->wl, term->conf); term->sixel.pixman_fmt = PIXMAN_a8r8g8b8; - if (term->conf->tweak.surface_bit_depth == SHM_10_BIT) { + if (term->conf->tweak.surface_bit_depth == SHM_BITS_10) { if (term->wl->shm_have_argb2101010 && term->wl->shm_have_xrgb2101010) { term->sixel.use_10bit = true; term->sixel.pixman_fmt = PIXMAN_a2r10g10b10; diff --git a/terminal.c b/terminal.c index d25516cb..793a1616 100644 --- a/terminal.c +++ b/terminal.c @@ -1073,19 +1073,16 @@ reload_fonts(struct terminal *term, bool resize_grid) options->scaling_filter = conf->tweak.fcft_filter; options->color_glyphs.format = PIXMAN_a8r8g8b8; - options->color_glyphs.srgb_decode = render_do_linear_blending(term); + options->color_glyphs.srgb_decode = + wayl_do_linear_blending(term->wl, term->conf); - if (conf->tweak.surface_bit_depth == SHM_10_BIT) { - if ((term->wl->shm_have_argb2101010 && term->wl->shm_have_xrgb2101010) || - (term->wl->shm_have_abgr2101010 && term->wl->shm_have_xbgr2101010)) - { - /* - * Use a high-res buffer type for emojis. We don't want to - * use an a2r10g0b10 type of surface, since we need more - * than 2 bits for alpha. - */ - options->color_glyphs.format = PIXMAN_rgba_float; - } + if (shm_chain_bit_depth(term->render.chains.grid) >= SHM_BITS_10) { + /* + * Use a high-res buffer type for emojis. We don't want to use + * an a2r10g0b10 type of surface, since we need more than 2 + * bits for alpha. + */ + options->color_glyphs.format = PIXMAN_rgba_float; } struct fcft_font *fonts[4]; @@ -1260,7 +1257,10 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, goto err; } - const bool ten_bit_surfaces = conf->tweak.surface_bit_depth == SHM_10_BIT; + const enum shm_bit_depth desired_bit_depth = + conf->tweak.surface_bit_depth == SHM_BITS_AUTO + ? wayl_do_linear_blending(wayl, conf) ? SHM_BITS_10 : SHM_BITS_8 + : conf->tweak.surface_bit_depth; const struct color_theme *theme = NULL; switch (conf->initial_color_theme) { @@ -1353,13 +1353,13 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .render = { .chains = { .grid = shm_chain_new(wayl, true, 1 + conf->render_worker_count, - ten_bit_surfaces), - .search = shm_chain_new(wayl, false, 1 ,ten_bit_surfaces), - .scrollback_indicator = shm_chain_new(wayl, false, 1, ten_bit_surfaces), - .render_timer = shm_chain_new(wayl, false, 1, ten_bit_surfaces), - .url = shm_chain_new(wayl, false, 1, ten_bit_surfaces), - .csd = shm_chain_new(wayl, false, 1, ten_bit_surfaces), - .overlay = shm_chain_new(wayl, false, 1, ten_bit_surfaces), + desired_bit_depth), + .search = shm_chain_new(wayl, false, 1 ,desired_bit_depth), + .scrollback_indicator = shm_chain_new(wayl, false, 1, desired_bit_depth), + .render_timer = shm_chain_new(wayl, false, 1, desired_bit_depth), + .url = shm_chain_new(wayl, false, 1, desired_bit_depth), + .csd = shm_chain_new(wayl, false, 1, desired_bit_depth), + .overlay = shm_chain_new(wayl, false, 1, desired_bit_depth), }, .scrollback_lines = conf->scrollback.lines, .app_sync_updates.timer_fd = app_sync_updates_fd, @@ -1502,7 +1502,7 @@ term_window_configured(struct terminal *term) xassert(term->window->is_configured); fdm_add(term->fdm, term->ptmx, EPOLLIN, &fdm_ptmx, term); - const bool gamma_correct = render_do_linear_blending(term); + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); LOG_INFO("gamma-correct blending: %s", gamma_correct ? "enabled" : "disabled"); } } diff --git a/wayland.c b/wayland.c index 9b143508..320f03aa 100644 --- a/wayland.c +++ b/wayland.c @@ -2640,3 +2640,10 @@ wayl_activate(struct wayland *wayl, struct wl_window *win, const char *token) xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface.surf); } + +bool +wayl_do_linear_blending(const struct wayland *wayl, const struct config *conf) +{ + return conf->gamma_correct && + wayl->color_management.img_description != NULL; +} diff --git a/wayland.h b/wayland.h index a9d6858c..044b217f 100644 --- a/wayland.h +++ b/wayland.h @@ -26,6 +26,7 @@ #include <fcft/fcft.h> #include <tllist.h> +#include "config.h" #include "cursor-shape.h" #include "fdm.h" @@ -539,3 +540,4 @@ bool wayl_get_activation_token( struct wl_window *win, activation_token_cb_t cb, void *cb_data); void wayl_activate(struct wayland *wayl, struct wl_window *win, const char *token); +bool wayl_do_linear_blending(const struct wayland *wayl, const struct config *conf); From 9ff0151055e4883c588c8ce8c9f683c8b5d475bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 1 May 2025 10:17:20 +0200 Subject: [PATCH 1187/1323] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ef3ae43..6bc5b9df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.22.2](#1-22-2) * [1.22.1](#1-22-1) * [1.22.0](#1-22-0) @@ -61,6 +62,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.22.2 ### Changed From 7ced397089fb5724fb2021bea8491f361b289366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 1 May 2025 08:09:08 +0200 Subject: [PATCH 1188/1323] config: gamma-correct-blending: disable by default --- CHANGELOG.md | 4 ++++ config.c | 15 +++------------ config.h | 4 +--- doc/foot.ini.5.scd | 2 +- foot.ini | 1 + render.c | 2 +- tests/test-config.c | 1 + wayland.c | 6 ++---- 8 files changed, 14 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bc5b9df..dd6300ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,10 @@ ## Unreleased ### Added ### Changed + +* `gamma-correct-blending` now defaults to `no` instead of `yes`. + + ### Deprecated ### Removed ### Fixed diff --git a/config.c b/config.c index 347cc1ec..c7cf03f9 100644 --- a/config.c +++ b/config.c @@ -1083,17 +1083,8 @@ parse_section_main(struct context *ctx) return true; } - else if (streq(key, "gamma-correct-blending")) { - bool gamma_correct; - if (!value_to_bool(ctx, &gamma_correct)) - return false; - - conf->gamma_correct = - gamma_correct - ? GAMMA_CORRECT_ENABLED - : GAMMA_CORRECT_DISABLED; - return true; - } + else if (streq(key, "gamma-correct-blending")) + return value_to_bool(ctx, &conf->gamma_correct); else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); @@ -3318,7 +3309,7 @@ config_load(struct config *conf, const char *conf_path, .underline_thickness = {.pt = 0., .px = -1}, .strikeout_thickness = {.pt = 0., .px = -1}, .dpi_aware = false, - .gamma_correct = GAMMA_CORRECT_AUTO, + .gamma_correct = false, .security = { .osc52 = OSC52_ENABLED, }, diff --git a/config.h b/config.h index 2dec82c1..fe7a0331 100644 --- a/config.h +++ b/config.h @@ -168,9 +168,7 @@ struct config { enum { STARTUP_WINDOWED, STARTUP_MAXIMIZED, STARTUP_FULLSCREEN } startup_mode; bool dpi_aware; - enum {GAMMA_CORRECT_DISABLED, - GAMMA_CORRECT_ENABLED, - GAMMA_CORRECT_AUTO} gamma_correct; + bool gamma_correct; struct config_font_list fonts[4]; struct font_size_adjustment font_size_adjustment; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 95e491eb..52a14524 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -227,7 +227,7 @@ empty string to be set, but it must be quoted: *KEY=""*) though. The amount of errors can be reduced by using 10-bit surfaces; see *tweak.surface-bit-depth*. - Default: enabled when compositor support is available + Default: _no_. *box-drawings-uses-font-glyphs* Boolean. When disabled, foot generates box/line drawing characters diff --git a/foot.ini b/foot.ini index 7d96ca0f..2ac0c05e 100644 --- a/foot.ini +++ b/foot.ini @@ -22,6 +22,7 @@ # strikeout-thickness=<font strikeout thickness> # box-drawings-uses-font-glyphs=no # dpi-aware=no +# gamma-correct-blending=no # initial-window-size-pixels=700x500 # Or, # initial-window-size-chars=<COLSxROWS> diff --git a/render.c b/render.c index b0d21d18..0ee60d65 100644 --- a/render.c +++ b/render.c @@ -5251,6 +5251,6 @@ render_xcursor_set(struct seat *seat, struct terminal *term, bool render_do_linear_blending(const struct terminal *term) { - return term->conf->gamma_correct != GAMMA_CORRECT_DISABLED && + return term->conf->gamma_correct && term->wl->color_management.img_description != NULL; } diff --git a/tests/test-config.c b/tests/test-config.c index 69d349b4..99398fec 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -468,6 +468,7 @@ test_section_main(void) test_boolean(&ctx, &parse_section_main, "box-drawings-uses-font-glyphs", &conf.box_drawings_uses_font_glyphs); test_boolean(&ctx, &parse_section_main, "locked-title", &conf.locked_title); test_boolean(&ctx, &parse_section_main, "dpi-aware", &conf.dpi_aware); + test_boolean(&ctx, &parse_section_main, "gamma-correct-blending", &conf.gamma_correct); test_pt_or_px(&ctx, &parse_section_main, "font-size-adjustment", &conf.font_size_adjustment.pt_or_px); /* TODO: test ‘N%’ values too */ test_pt_or_px(&ctx, &parse_section_main, "line-height", &conf.line_height); diff --git a/wayland.c b/wayland.c index 853124be..9b143508 100644 --- a/wayland.c +++ b/wayland.c @@ -1980,7 +1980,7 @@ wayl_win_init(struct terminal *term, const char *token) xdg_toplevel_icon_v1_destroy(icon); } - if (term->conf->gamma_correct != GAMMA_CORRECT_DISABLED) { + if (term->conf->gamma_correct) { if (wayl->color_management.img_description != NULL) { xassert(wayl->color_management.manager != NULL); @@ -1990,7 +1990,7 @@ wayl_win_init(struct terminal *term, const char *token) wp_color_management_surface_v1_set_image_description( win->surface.color_management, wayl->color_management.img_description, WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL); - } else if (term->conf->gamma_correct == GAMMA_CORRECT_ENABLED) { + } else { if (wayl->color_management.manager == NULL) { LOG_WARN( "gamma-corrected-blending: disabling; " @@ -2005,8 +2005,6 @@ wayl_win_init(struct terminal *term, const char *token) LOG_WARN(" - TF: ext_linear"); LOG_WARN(" - primaries: sRGB"); } - } else { - /* "auto" - don't warn */ } } From 2a8948a3f32eaa6894457dcbf5aced57bdd91853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 1 May 2025 08:34:49 +0200 Subject: [PATCH 1189/1323] config: tweak.surface-bit-depth now defaults to 'auto' When set to 'auto', use 10-bit surfaces if gamma-correct blending is enabled, and 8-bit surfaces otherwise. Note that we may still fallback to 8-bit surfaces (without disabling gamma-correct blending) if the compositor does not support 10-bit surfaces. Closes #2082 --- CHANGELOG.md | 13 +++++++++++++ config.c | 4 ++-- config.h | 8 +++++++- doc/foot.ini.5.scd | 41 ++++++++++++++++++++++------------------- pgo/pgo.c | 5 +++-- render.c | 31 +++++++++++++------------------ render.h | 2 -- shm.c | 15 ++++++++++++--- shm.h | 5 ++++- sixel.c | 4 ++-- terminal.c | 42 +++++++++++++++++++++--------------------- wayland.c | 7 +++++++ wayland.h | 2 ++ 13 files changed, 108 insertions(+), 71 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd6300ae..9ea93304 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,14 +64,27 @@ ## Unreleased ### Added + +* `auto` to the `tweak.surface-bit-depth` option. + + ### Changed * `gamma-correct-blending` now defaults to `no` instead of `yes`. +* `tweak.surface-bit-depth` default value changed to `auto`; uses + 10-bit surfaces when `gamma-correct-blending=yes`, and 8-bit + surfaces otherwise. ### Deprecated ### Removed ### Fixed + +* Inaccurate colors when `gamma-correct-blending=yes` ([#2082][2082]). + +[2082]: https://codeberg.org/dnkl/foot/issues/2082 + + ### Security ### Contributors diff --git a/config.c b/config.c index c7cf03f9..3f7ffa28 100644 --- a/config.c +++ b/config.c @@ -2771,7 +2771,7 @@ parse_section_tweak(struct context *ctx) return value_to_enum( ctx, - (const char *[]){"8-bit", "10-bit", NULL}, + (const char *[]){"auto", "8-bit", "10-bit", NULL}, (int *)&conf->tweak.surface_bit_depth); } @@ -3419,7 +3419,7 @@ config_load(struct config *conf, const char *conf_path, .box_drawing_solid_shades = true, .font_monospace_warn = true, .sixel = true, - .surface_bit_depth = 8, + .surface_bit_depth = SHM_BITS_AUTO, }, .touch = { diff --git a/config.h b/config.h index fe7a0331..ea0160bf 100644 --- a/config.h +++ b/config.h @@ -131,6 +131,12 @@ struct custom_regex { struct config_spawn_template launch; }; +enum shm_bit_depth { + SHM_BITS_AUTO, + SHM_BITS_8, + SHM_BITS_10 +}; + struct config { char *term; char *shell; @@ -408,7 +414,7 @@ struct config { bool box_drawing_solid_shades; bool font_monospace_warn; bool sixel; - enum { SHM_8_BIT, SHM_10_BIT } surface_bit_depth; + enum shm_bit_depth surface_bit_depth; } tweak; struct { diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 52a14524..650242a2 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -220,12 +220,13 @@ empty string to be set, but it must be quoted: *KEY=""*) than intended when rendered with gamma-correct blending, since the font designer set the font weight based on incorrect rendering. - Note that some colors (especially dark ones) may be slightly - off. The reason for this is loss of color precision, due to foot - using 8-bit surfaces (i.e. each color channel is 8 bits). In all - known cases, the difference is small enough not to be noticed - though. The amount of errors can be reduced by using 10-bit - surfaces; see *tweak.surface-bit-depth*. + In order to represent colors faithfully, higher precision image + buffers are required. By default, foot will use 10-bit color + channels, if available, when gamma-correct blending is + enabled. However, the high precision buffers are slow; if you want + to use gamma-correct blending, but prefer speed (throughput and + input latency) over accurate colors, you can force 8-bit color + channels by setting *tweak.surface-bit-depth=8-bit*. Default: _no_. @@ -1974,23 +1975,25 @@ any of these options. *surface-bit-depth* Selects which RGB bit depth to use for image buffers. One of - *8-bit*, or *10-bit*. + *auto*, *8-bit*, or *10-bit*. - The default, *8-bit*, uses 8 bits for all channels, alpha - included. When *gamma-correct-blending* is disabled, this is the - best option. + *auto* chooses bit depth depending on other settings, and + availability. - When *gamma-correct-blending* is enabled, you may want to enable - 10-bit surfaces, as that improves color precision. Be aware - however, that in this mode, the alpha channel is only 2 bits - instead of 8 bits. Thus, if you are using a transparent - background, you may want to use the default, *8-bit*, even if you - have gamma-correct blending enabled. + *8-bit*, uses 8 bits for each color channel, alpha included. This + is the default when *gamma-correct-blending=no*. - You should also note that 10-bit surface is much slower. This will - increase input latency and decrease rendering throughput. + *10-bit* uses 10 bits for each RGB channel, and 2 bits for the + alpha channel. Thus, it provides higher precision color channels, + but a lower precision alpha channel. It is the default when + *gamma-correct-blending=yes*, if supported by the compositor. - Default: _8-bit_ + Note that *10-bit* is much slower than *8-bit*; if you want to use + gamma-correct blending, and if you prefer speed (throughput and + input latency) over accurate colors, you can set + *surface-bit-depth=8-bit* explicitly. + + Default: _auto_ # SEE ALSO diff --git a/pgo/pgo.c b/pgo/pgo.c index 8a4967ba..757dcd06 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -129,7 +129,7 @@ render_worker_thread(void *_ctx) } bool -render_do_linear_blending(const struct terminal *term) +wayl_do_linear_blending(const struct wayland *wayl, const struct config *conf) { return false; } @@ -201,11 +201,12 @@ void urls_reset(struct terminal *term) {} void shm_unref(struct buffer *buf) {} void shm_chain_free(struct buffer_chain *chain) {} +enum shm_bit_depth shm_chain_bit_depth(const struct buffer_chain *chain) { return SHM_BITS_8; } struct buffer_chain * shm_chain_new( struct wayland *wayl, bool scrollable, size_t pix_instances, - bool ten_bit_it_if_capable) + enum shm_bit_depth desired_bit_depth) { return NULL; } diff --git a/render.c b/render.c index 0ee60d65..55c2ec4d 100644 --- a/render.c +++ b/render.c @@ -626,7 +626,7 @@ draw_cursor(const struct terminal *term, const struct cell *cell, pixman_color_t cursor_color; pixman_color_t text_color; cursor_colors_for_cell(term, cell, fg, bg, &cursor_color, &text_color, - render_do_linear_blending(term)); + wayl_do_linear_blending(term->wl, term->conf)); if (unlikely(!term->kbd_focus)) { switch (term->conf->cursor.unfocused_style) { @@ -820,7 +820,7 @@ render_cell(struct terminal *term, pixman_image_t *pix, if (cell->attrs.blink && term->blink.state == BLINK_OFF) _fg = color_blend_towards(_fg, 0x00000000, term->conf->dim.amount); - const bool gamma_correct = render_do_linear_blending(term); + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); pixman_color_t fg = color_hex_to_pixman(_fg, gamma_correct); pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); @@ -1180,7 +1180,8 @@ static void render_urgency(struct terminal *term, struct buffer *buf) { uint32_t red = term->colors.table[1]; - pixman_color_t bg = color_hex_to_pixman(red, render_do_linear_blending(term)); + pixman_color_t bg = color_hex_to_pixman( + red, wayl_do_linear_blending(term->wl, term->conf)); int width = min(min(term->margins.left, term->margins.right), min(term->margins.top, term->margins.bottom)); @@ -1211,7 +1212,7 @@ render_margin(struct terminal *term, struct buffer *buf, const int bmargin = term->height - term->margins.bottom; const int line_count = end_line - start_line; - const bool gamma_correct = render_do_linear_blending(term); + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); const uint32_t _bg = !term->reverse ? term->colors.bg : term->colors.fg; uint16_t alpha = term->colors.alpha; @@ -1699,7 +1700,7 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, if (unlikely(term->is_searching)) return; - const bool gamma_correct = render_do_linear_blending(term); + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); /* Adjust cursor position to viewport */ struct coord cursor; @@ -1970,7 +1971,8 @@ render_overlay(struct terminal *term) case OVERLAY_FLASH: color = color_hex_to_pixman_with_alpha( term->conf->colors.flash, - term->conf->colors.flash_alpha, render_do_linear_blending(term)); + term->conf->colors.flash_alpha, + wayl_do_linear_blending(term->wl, term->conf)); break; case OVERLAY_NONE: @@ -2312,7 +2314,7 @@ render_osd(struct terminal *term, const struct wayl_sub_surface *sub_surf, pixman_image_set_clip_region32(buf->pix[0], &clip); pixman_region32_fini(&clip); - const bool gamma_correct = render_do_linear_blending(term); + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); uint16_t alpha = _bg >> 24 | (_bg >> 24 << 8); pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); pixman_image_fill_rectangles( @@ -2453,7 +2455,7 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, if (info->width == 0 || info->height == 0) return; - const bool gamma_correct = render_do_linear_blending(term); + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); { /* Fully transparent - no need to do a color space transform */ @@ -2542,7 +2544,7 @@ get_csd_button_fg_color(const struct terminal *term) } return color_hex_to_pixman_with_alpha( - _color, alpha, render_do_linear_blending(term)); + _color, alpha, wayl_do_linear_blending(term->wl, term->conf)); } static void @@ -2819,7 +2821,7 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx, if (!term->visual_focus) _color = color_dim(term, _color); - const bool gamma_correct = render_do_linear_blending(term); + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha, gamma_correct); render_csd_part(term, surf->surf, buf, info->width, info->height, &color); @@ -3678,7 +3680,7 @@ render_search_box(struct terminal *term) : term->conf->colors.use_custom.search_box_no_match; /* Background - yellow on empty/match, red on mismatch (default) */ - const bool gamma_correct = render_do_linear_blending(term); + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); const pixman_color_t color = color_hex_to_pixman( is_match ? (custom_colors @@ -5247,10 +5249,3 @@ render_xcursor_set(struct seat *seat, struct terminal *term, seat->pointer.xcursor_pending = true; return true; } - -bool -render_do_linear_blending(const struct terminal *term) -{ - return term->conf->gamma_correct && - term->wl->color_management.img_description != NULL; -} diff --git a/render.h b/render.h index c7b8e4a5..81d2a905 100644 --- a/render.h +++ b/render.h @@ -47,5 +47,3 @@ struct csd_data { }; struct csd_data get_csd_data(const struct terminal *term, enum csd_surface surf_idx); - -bool render_do_linear_blending(const struct terminal *term); diff --git a/shm.c b/shm.c index 32e6bdd0..38944020 100644 --- a/shm.c +++ b/shm.c @@ -972,7 +972,7 @@ shm_unref(struct buffer *_buf) struct buffer_chain * shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, - bool ten_bit_if_capable) + enum shm_bit_depth desired_bit_depth) { pixman_format_code_t pixman_fmt_without_alpha = PIXMAN_x8r8g8b8; enum wl_shm_format shm_fmt_without_alpha = WL_SHM_FORMAT_XRGB8888; @@ -982,8 +982,7 @@ shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, static bool have_logged = false; - - if (ten_bit_if_capable) { + if (desired_bit_depth == SHM_BITS_10) { if (wayl->shm_have_argb2101010 && wayl->shm_have_xrgb2101010) { pixman_fmt_without_alpha = PIXMAN_x2r10g10b10; shm_fmt_without_alpha = WL_SHM_FORMAT_XRGB2101010; @@ -1058,3 +1057,13 @@ shm_chain_free(struct buffer_chain *chain) free(chain); } + +enum shm_bit_depth +shm_chain_bit_depth(const struct buffer_chain *chain) +{ + const pixman_format_code_t fmt = chain->pixman_fmt_with_alpha; + + return (fmt == PIXMAN_a2r10g10b10 || fmt == PIXMAN_a2b10g10r10) + ? SHM_BITS_10 + : SHM_BITS_8; +} diff --git a/shm.h b/shm.h index 2af185c9..8f8c406a 100644 --- a/shm.h +++ b/shm.h @@ -9,6 +9,7 @@ #include <tllist.h> +#include "config.h" #include "wayland.h" struct damage; @@ -46,9 +47,11 @@ void shm_set_max_pool_size(off_t max_pool_size); struct buffer_chain; struct buffer_chain *shm_chain_new( struct wayland *wayl, bool scrollable, size_t pix_instances, - bool ten_bit_it_if_capable); + enum shm_bit_depth desired_bit_depth); void shm_chain_free(struct buffer_chain *chain); +enum shm_bit_depth shm_chain_bit_depth(const struct buffer_chain *chain); + /* * Returns a single buffer. * diff --git a/sixel.c b/sixel.c index dd933d7a..680c258f 100644 --- a/sixel.c +++ b/sixel.c @@ -110,10 +110,10 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) term->sixel.image.height = 0; term->sixel.image.alloc_height = 0; term->sixel.image.bottom_pixel = 0; - term->sixel.linear_blending = render_do_linear_blending(term); + term->sixel.linear_blending = wayl_do_linear_blending(term->wl, term->conf); term->sixel.pixman_fmt = PIXMAN_a8r8g8b8; - if (term->conf->tweak.surface_bit_depth == SHM_10_BIT) { + if (term->conf->tweak.surface_bit_depth == SHM_BITS_10) { if (term->wl->shm_have_argb2101010 && term->wl->shm_have_xrgb2101010) { term->sixel.use_10bit = true; term->sixel.pixman_fmt = PIXMAN_a2r10g10b10; diff --git a/terminal.c b/terminal.c index f2d03e77..1ebe067e 100644 --- a/terminal.c +++ b/terminal.c @@ -1073,19 +1073,16 @@ reload_fonts(struct terminal *term, bool resize_grid) options->scaling_filter = conf->tweak.fcft_filter; options->color_glyphs.format = PIXMAN_a8r8g8b8; - options->color_glyphs.srgb_decode = render_do_linear_blending(term); + options->color_glyphs.srgb_decode = + wayl_do_linear_blending(term->wl, term->conf); - if (conf->tweak.surface_bit_depth == SHM_10_BIT) { - if ((term->wl->shm_have_argb2101010 && term->wl->shm_have_xrgb2101010) || - (term->wl->shm_have_abgr2101010 && term->wl->shm_have_xbgr2101010)) - { - /* - * Use a high-res buffer type for emojis. We don't want to - * use an a2r10g0b10 type of surface, since we need more - * than 2 bits for alpha. - */ - options->color_glyphs.format = PIXMAN_rgba_float; - } + if (shm_chain_bit_depth(term->render.chains.grid) >= SHM_BITS_10) { + /* + * Use a high-res buffer type for emojis. We don't want to use + * an a2r10g0b10 type of surface, since we need more than 2 + * bits for alpha. + */ + options->color_glyphs.format = PIXMAN_rgba_float; } struct fcft_font *fonts[4]; @@ -1260,7 +1257,10 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, goto err; } - const bool ten_bit_surfaces = conf->tweak.surface_bit_depth == SHM_10_BIT; + const enum shm_bit_depth desired_bit_depth = + conf->tweak.surface_bit_depth == SHM_BITS_AUTO + ? wayl_do_linear_blending(wayl, conf) ? SHM_BITS_10 : SHM_BITS_8 + : conf->tweak.surface_bit_depth; /* Initialize configure-based terminal attributes */ *term = (struct terminal) { @@ -1346,13 +1346,13 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .render = { .chains = { .grid = shm_chain_new(wayl, true, 1 + conf->render_worker_count, - ten_bit_surfaces), - .search = shm_chain_new(wayl, false, 1 ,ten_bit_surfaces), - .scrollback_indicator = shm_chain_new(wayl, false, 1, ten_bit_surfaces), - .render_timer = shm_chain_new(wayl, false, 1, ten_bit_surfaces), - .url = shm_chain_new(wayl, false, 1, ten_bit_surfaces), - .csd = shm_chain_new(wayl, false, 1, ten_bit_surfaces), - .overlay = shm_chain_new(wayl, false, 1, ten_bit_surfaces), + desired_bit_depth), + .search = shm_chain_new(wayl, false, 1 ,desired_bit_depth), + .scrollback_indicator = shm_chain_new(wayl, false, 1, desired_bit_depth), + .render_timer = shm_chain_new(wayl, false, 1, desired_bit_depth), + .url = shm_chain_new(wayl, false, 1, desired_bit_depth), + .csd = shm_chain_new(wayl, false, 1, desired_bit_depth), + .overlay = shm_chain_new(wayl, false, 1, desired_bit_depth), }, .scrollback_lines = conf->scrollback.lines, .app_sync_updates.timer_fd = app_sync_updates_fd, @@ -1495,7 +1495,7 @@ term_window_configured(struct terminal *term) xassert(term->window->is_configured); fdm_add(term->fdm, term->ptmx, EPOLLIN, &fdm_ptmx, term); - const bool gamma_correct = render_do_linear_blending(term); + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); LOG_INFO("gamma-correct blending: %s", gamma_correct ? "enabled" : "disabled"); } } diff --git a/wayland.c b/wayland.c index 9b143508..320f03aa 100644 --- a/wayland.c +++ b/wayland.c @@ -2640,3 +2640,10 @@ wayl_activate(struct wayland *wayl, struct wl_window *win, const char *token) xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface.surf); } + +bool +wayl_do_linear_blending(const struct wayland *wayl, const struct config *conf) +{ + return conf->gamma_correct && + wayl->color_management.img_description != NULL; +} diff --git a/wayland.h b/wayland.h index a9d6858c..044b217f 100644 --- a/wayland.h +++ b/wayland.h @@ -26,6 +26,7 @@ #include <fcft/fcft.h> #include <tllist.h> +#include "config.h" #include "cursor-shape.h" #include "fdm.h" @@ -539,3 +540,4 @@ bool wayl_get_activation_token( struct wl_window *win, activation_token_cb_t cb, void *cb_data); void wayl_activate(struct wayland *wayl, struct wl_window *win, const char *token); +bool wayl_do_linear_blending(const struct wayland *wayl, const struct config *conf); From acea863fbef16bdd72e14f8fdbbf51db49bf0fba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 1 May 2025 10:20:22 +0200 Subject: [PATCH 1190/1323] changelog: prepare for 1.22.3 --- CHANGELOG.md | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ea93304..0f08c500 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.22.3](#1-22-3) * [1.22.2](#1-22-2) * [1.22.1](#1-22-1) * [1.22.0](#1-22-0) @@ -62,7 +62,8 @@ * [1.2.0](#1-2-0) -## Unreleased +## 1.22.3 + ### Added * `auto` to the `tweak.surface-bit-depth` option. @@ -76,8 +77,6 @@ surfaces otherwise. -### Deprecated -### Removed ### Fixed * Inaccurate colors when `gamma-correct-blending=yes` ([#2082][2082]). @@ -85,10 +84,6 @@ [2082]: https://codeberg.org/dnkl/foot/issues/2082 -### Security -### Contributors - - ## 1.22.2 ### Changed From 85c81042d2b16094677ccb383dd527a0df52e755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 1 May 2025 10:20:38 +0200 Subject: [PATCH 1191/1323] meson: bump version to 1.22.3 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index b3163586..4bf4993c 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.22.2', + version: '1.22.3', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From 0ea572dc63083e0d415382da2df8050a70f8cd07 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent <git@rrc.codes> Date: Mon, 28 Apr 2025 19:56:32 -0400 Subject: [PATCH 1192/1323] Paste URL/regex selection to prompt if key is uppercase. In copy-regex/show-urls-copy mode, if the last input character was uppercase, copy the selection to the clipboard _and_ paste it. This is useful for taking a file path from a command output:(log, git, test failure, etc.) and using it in another command. This is inspired by the behavior of copy mode in wezterm: https://wezterm.org/quickselect.html I could have made it check every character in the hint, but it seemed fine to assume that if the last character was uppercase, the user wanted this behavior. Closes #1975. --- CHANGELOG.md | 3 +++ doc/foot.ini.5.scd | 9 ++++++--- url-mode.c | 14 ++++++++++++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfe3b8bc..533f601c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,8 +85,11 @@ - paper-color - selenized - solarized +* `regex-copy`/`show-urls-copy` will copy and paste the selected text if the hint + is completed with an uppercase character ([#1975][1975]). [2025]: https://codeberg.org/dnkl/foot/issues/2025 +[1975]: https://codeberg.org/dnkl/foot/issues/1975 ### Changed diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index b9ab9c6a..3e70074e 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1358,7 +1358,8 @@ e.g. *search-start=none*. *show-urls-copy* Enter URL mode, where all currently visible URLs are tagged with a jump label with a key sequence that will place the URL in the - clipboard. Default: _none_. + clipboard. If the hint is completed with an uppercase character, + the match will also be pasted. Default: _none_. *regex-launch* Enter regex mode. This works exactly the same as URL mode; all @@ -1381,8 +1382,10 @@ e.g. *search-start=none*. Default: _none_. *regex-copy* - Same as *regex-copy*, but the match is placed in the clipboard, - instead of "launched", upon activation. Default: _none_. + Same as *regex-launch*, but the match is placed in the clipboard, + instead of "launched", upon activation. If the hint is completed + with an uppercase character, the match will also be pasted. + Default: _none_. *prompt-prev* Jump to the previous, currently not visible, prompt (requires diff --git a/url-mode.c b/url-mode.c index d0f7fc53..199ff3f1 100644 --- a/url-mode.c +++ b/url-mode.c @@ -131,7 +131,7 @@ spawn_url_launcher(struct seat *seat, struct terminal *term, const char *url, static void activate_url(struct seat *seat, struct terminal *term, const struct url *url, - uint32_t serial) + uint32_t serial, bool paste_url_to_self) { char *url_string = NULL; @@ -159,6 +159,15 @@ activate_url(struct seat *seat, struct terminal *term, const struct url *url, switch (url->action) { case URL_ACTION_COPY: + if (paste_url_to_self) { + if (term->bracketed_paste) + term_to_slave(term, "\033[200~", 6); + + term_to_slave(term, url_string, strlen(url_string)); + + if (term->bracketed_paste) + term_to_slave(term, "\033[201~", 6); + } if (text_to_clipboard(seat, term, url_string, seat->kbd.serial)) { /* Now owned by our clipboard “manager” */ url_string = NULL; @@ -273,7 +282,8 @@ urls_input(struct seat *seat, struct terminal *term, } if (match) { - activate_url(seat, term, match, serial); + // If the last hint character was uppercase, copy and paste + activate_url(seat, term, match, serial, wc == toc32upper(wc)); switch (match->action) { case URL_ACTION_COPY: From 237db6e771293689d6739de5aac7358d35dce5d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 2 May 2025 13:43:59 +0200 Subject: [PATCH 1193/1323] wayland: always call wl_display_dispatch_pending() at least once, after reading This fixes an issue where protocol errors aren't reported. I'm guessing the read succeeds, but that prepare_read() _also_ succeeds immediately, since there aren't any events to dispatch (only log the protocol error). By calling dispatch unconditionally, we ensure any error messages are printed. Then we proceed to loop prepare_read() + dispatch_pending() until the queue is empty. --- wayland.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wayland.c b/wayland.c index 320f03aa..a41b5060 100644 --- a/wayland.c +++ b/wayland.c @@ -1650,6 +1650,8 @@ fdm_wayl(struct fdm *fdm, int fd, int events, void *data) return false; } + wl_display_dispatch_pending(wayl->display); + while (wl_display_prepare_read(wayl->display) != 0) { if (wl_display_dispatch_pending(wayl->display) < 0) { LOG_ERRNO("failed to dispatch pending Wayland events"); From 5080e271c2fa5899ccb37384c26491e025b225ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 2 May 2025 13:46:18 +0200 Subject: [PATCH 1194/1323] wayland: attempt to log protocol errors on failure to flush When failing to flush, and the error is EPIPE, attempt to read and dispatch events. This ensures protocol errors are logged. --- wayland.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/wayland.c b/wayland.c index a41b5060..08994202 100644 --- a/wayland.c +++ b/wayland.c @@ -2247,7 +2247,14 @@ wayl_flush(struct wayland *wayl) } if (errno != EAGAIN) { - LOG_ERRNO("failed to flush wayland socket"); + const int saved_errno = errno; + + if (errno == EPIPE) { + wl_display_read_events(wayl->display); + wl_display_dispatch_pending(wayl->display); + } + + LOG_ERRNO_P(saved_errno, "failed to flush wayland socket"); return; } From 7354b94f737c9f589803b23714e7eddfb2f0fd69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 2 May 2025 08:53:43 +0200 Subject: [PATCH 1195/1323] osc: restore configured alpha if OSC-11 has no alpha value When parsing an OSC-11 without an alpha value (i.e. standard OSC-11, not rxvt's extended variant), restore the alpha value from the configuration, rather than keeping whatever the current alpha is. --- CHANGELOG.md | 3 +++ osc.c | 21 +++++++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 533f601c..54ad3a25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,6 +95,9 @@ ### Changed * `cursor.color` moved to `colors.cursor`. +* OSC-11 without an alpha value will now restore the configured + (i.e. from `foot.ini`) alpha, rather than keeping whatever the + current alpha value is, unchanged. ### Deprecated diff --git a/osc.c b/osc.c index 7e3e6376..78f335e1 100644 --- a/osc.c +++ b/osc.c @@ -1455,15 +1455,20 @@ osc_dispatch(struct terminal *term) case 11: term->colors.bg = color; - if (have_alpha) { - const bool changed = term->colors.alpha != alpha; - term->colors.alpha = alpha; - - if (changed) { - wayl_win_alpha_changed(term->window); - term_font_subpixel_changed(term); - } + if (!have_alpha) { + alpha = term->colors.active_theme == COLOR_THEME1 + ? term->conf->colors.alpha + : term->conf->colors2.alpha; } + + const bool changed = term->colors.alpha != alpha; + term->colors.alpha = alpha; + + if (changed) { + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + } + term_damage_color(term, COLOR_DEFAULT, 0); term_damage_margins(term); break; From 970e13db8deebf6c7542c7aede4f34703fa86769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 1 May 2025 09:37:47 +0200 Subject: [PATCH 1196/1323] config: tweak.surface-bit-depth: add support for 16-bit surfaces This adds supports for 16-bit surfaces, using the new PIXMAN_a16b16g16r16 buffer format. This maps to WL_SHM_FORMAT_ABGR16161616 (little-endian). Use the new 16-bit surfaces by default, when gamma-correct-blending=yes. --- CHANGELOG.md | 5 +++++ config.c | 9 +++++++- config.h | 3 ++- doc/foot.ini.5.scd | 29 +++++++++++++------------ meson.build | 4 ++++ shm.c | 53 +++++++++++++++++++++++++++++++++++++++------- sixel.c | 13 +++++++++++- terminal.c | 6 +++++- wayland.c | 2 ++ wayland.h | 2 ++ 10 files changed, 101 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54ad3a25..1a9917b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,9 @@ - solarized * `regex-copy`/`show-urls-copy` will copy and paste the selected text if the hint is completed with an uppercase character ([#1975][1975]). +* `16-bit` to `tweak.surface-bit-depth`. Makes foot use 16-bit image + buffers. They provide the necessary color precision required by + `gamma-correct-blending=yes`. [2025]: https://codeberg.org/dnkl/foot/issues/2025 [1975]: https://codeberg.org/dnkl/foot/issues/1975 @@ -98,6 +101,8 @@ * OSC-11 without an alpha value will now restore the configured (i.e. from `foot.ini`) alpha, rather than keeping whatever the current alpha value is, unchanged. +* `gamma-correct-blending=yes` now defaults to `16-bit` image buffers, + instead of `10-bit`. ### Deprecated diff --git a/config.c b/config.c index 64e45135..d0aae6a5 100644 --- a/config.c +++ b/config.c @@ -2809,12 +2809,19 @@ parse_section_tweak(struct context *ctx) else if (streq(key, "surface-bit-depth")) { _Static_assert(sizeof(conf->tweak.surface_bit_depth) == sizeof(int), - "enum is not 32-bit"); + "enum is not 32-bit"); +#if defined(HAVE_PIXMAN_RGBA_16) + return value_to_enum( + ctx, + (const char *[]){"auto", "8-bit", "10-bit", "16-bit", NULL}, + (int *)&conf->tweak.surface_bit_depth); +#else return value_to_enum( ctx, (const char *[]){"auto", "8-bit", "10-bit", NULL}, (int *)&conf->tweak.surface_bit_depth); +#endif } else { diff --git a/config.h b/config.h index 80081906..197b67cd 100644 --- a/config.h +++ b/config.h @@ -198,7 +198,8 @@ enum which_color_theme { enum shm_bit_depth { SHM_BITS_AUTO, SHM_BITS_8, - SHM_BITS_10 + SHM_BITS_10, + SHM_BITS_16, }; struct config { diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 3e70074e..c1847932 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -207,7 +207,7 @@ empty string to be set, but it must be quoted: *KEY=""*) Compared to the default (disabled), bright glyphs on a dark background will appear thicker, and dark glyphs on a light background will appear thinner. - + FreeType can limit the effect of the latter, with a technique called stem darkening. It is only available for CFF fonts (OpenType, .otf) and disabled by default (in FreeType). You can @@ -221,12 +221,13 @@ empty string to be set, but it must be quoted: *KEY=""*) font designer set the font weight based on incorrect rendering. In order to represent colors faithfully, higher precision image - buffers are required. By default, foot will use 10-bit color - channels, if available, when gamma-correct blending is - enabled. However, the high precision buffers are slow; if you want - to use gamma-correct blending, but prefer speed (throughput and - input latency) over accurate colors, you can force 8-bit color - channels by setting *tweak.surface-bit-depth=8-bit*. + buffers are required. By default, foot will use either 16-bit, or + 10-bit color channels, depending on availability, when + gamma-correct blending is enabled. However, the high precision + buffers are slow; if you want to use gamma-correct blending, but + prefer speed (throughput and input latency) over accurate colors, + you can force 8-bit color channels by setting + *tweak.surface-bit-depth=8-bit*. Default: _no_. @@ -2023,7 +2024,7 @@ any of these options. *surface-bit-depth* Selects which RGB bit depth to use for image buffers. One of - *auto*, *8-bit*, or *10-bit*. + *auto*, *8-bit*, *10-bit* or *16-bit*. *auto* chooses bit depth depending on other settings, and availability. @@ -2033,12 +2034,14 @@ any of these options. *10-bit* uses 10 bits for each RGB channel, and 2 bits for the alpha channel. Thus, it provides higher precision color channels, - but a lower precision alpha channel. It is the default when - *gamma-correct-blending=yes*, if supported by the compositor. + but a lower precision alpha channel. - Note that *10-bit* is much slower than *8-bit*; if you want to use - gamma-correct blending, and if you prefer speed (throughput and - input latency) over accurate colors, you can set + *16-bit* 16 bits for each color channel, alpha included. If + available, this is the default when *gamma-correct-blending=yes*. + + Note that both *10-bit* and *16-bit* are much slower than *8-bit*; + if you want to use gamma-correct blending, and if you prefer speed + (throughput and input latency) over accurate colors, you can set *surface-bit-depth=8-bit* explicitly. Default: _auto_ diff --git a/meson.build b/meson.build index 4bf4993c..a884e533 100644 --- a/meson.build +++ b/meson.build @@ -145,6 +145,10 @@ if utf8proc.found() add_project_arguments('-DFOOT_GRAPHEME_CLUSTERING=1', language: 'c') endif +if pixman.version().version_compare('>=0.46.0') + add_project_arguments('-DHAVE_PIXMAN_RGBA_16', language: 'c') +endif + tllist = dependency('tllist', version: '>=1.1.0', fallback: 'tllist') fcft = dependency('fcft', version: ['>=3.3.1', '<4.0.0'], fallback: 'fcft') diff --git a/shm.c b/shm.c index 38944020..b586b504 100644 --- a/shm.c +++ b/shm.c @@ -338,7 +338,10 @@ get_new_buffers(struct buffer_chain *chain, size_t count, size_t total_size = 0; for (size_t i = 0; i < count; i++) { stride[i] = stride_for_format_and_width( - with_alpha ? PIXMAN_a8r8g8b8 : PIXMAN_x8r8g8b8, widths[i]); + with_alpha + ? chain->pixman_fmt_with_alpha + : chain->pixman_fmt_without_alpha, + widths[i]); sizes[i] = stride[i] * heights[i]; total_size += sizes[i]; } @@ -981,8 +984,38 @@ shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, enum wl_shm_format shm_fmt_with_alpha = WL_SHM_FORMAT_ARGB8888; static bool have_logged = false; + static bool have_logged_10_fallback = false; - if (desired_bit_depth == SHM_BITS_10) { +#if defined(HAVE_PIXMAN_RGBA_16) + static bool have_logged_16_fallback = false; + + if (desired_bit_depth == SHM_BITS_16) { + if (wayl->shm_have_abgr161616 && wayl->shm_have_xbgr161616) { + pixman_fmt_without_alpha = PIXMAN_a16b16g16r16; + shm_fmt_without_alpha = WL_SHM_FORMAT_XBGR16161616; + + pixman_fmt_without_alpha = PIXMAN_a16b16g16r16; + shm_fmt_with_alpha = WL_SHM_FORMAT_ABGR16161616; + + if (!have_logged) { + have_logged = true; + LOG_INFO("using 16-bit BGR surfaces"); + } + } else { + if (!have_logged_16_fallback) { + have_logged_16_fallback = true; + + LOG_WARN( + "16-bit surfaces requested, but compositor does not " + "implement ABGR161616+XBGR161616"); + } + } + } +#endif + + if (desired_bit_depth >= SHM_BITS_10 && + pixman_fmt_with_alpha == PIXMAN_a8r8g8b8) + { if (wayl->shm_have_argb2101010 && wayl->shm_have_xrgb2101010) { pixman_fmt_without_alpha = PIXMAN_x2r10g10b10; shm_fmt_without_alpha = WL_SHM_FORMAT_XRGB2101010; @@ -1010,13 +1043,13 @@ shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, } else { - if (!have_logged) { - have_logged = true; + if (!have_logged_10_fallback) { + have_logged_10_fallback = true; LOG_WARN( "10-bit surfaces requested, but compositor does not " "implement ARGB2101010+XRGB2101010, or " - "ABGR2101010+XBGR2101010. Falling back to 8-bit surfaces"); + "ABGR2101010+XBGR2101010"); } } } else { @@ -1063,7 +1096,11 @@ shm_chain_bit_depth(const struct buffer_chain *chain) { const pixman_format_code_t fmt = chain->pixman_fmt_with_alpha; - return (fmt == PIXMAN_a2r10g10b10 || fmt == PIXMAN_a2b10g10r10) - ? SHM_BITS_10 - : SHM_BITS_8; + return fmt == PIXMAN_a8r8g8b8 + ? SHM_BITS_8 +#if defined(HAVE_PIXMAN_RGBA_16) + : fmt == PIXMAN_a16b16g16r16 + ? SHM_BITS_16 +#endif + : SHM_BITS_10; } diff --git a/sixel.c b/sixel.c index 680c258f..c5ef01a1 100644 --- a/sixel.c +++ b/sixel.c @@ -113,7 +113,18 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) term->sixel.linear_blending = wayl_do_linear_blending(term->wl, term->conf); term->sixel.pixman_fmt = PIXMAN_a8r8g8b8; - if (term->conf->tweak.surface_bit_depth == SHM_BITS_10) { + /* + * Use higher-precision sixel surfaces if we're using + * higher-precision window surfaces. + * + * This is to a) get more accurate colors when doing gamma-correct + * blending, and b) use the same pixman format as the main + * surfaces, for (hopefully) better performance. + * + * For now, don't support 16-bit surfaces (too much sixel logic + * that assumes 32-bit pixels). + */ + if (shm_chain_bit_depth(term->render.chains.grid) >= SHM_BITS_10) { if (term->wl->shm_have_argb2101010 && term->wl->shm_have_xrgb2101010) { term->sixel.use_10bit = true; term->sixel.pixman_fmt = PIXMAN_a2r10g10b10; diff --git a/terminal.c b/terminal.c index 793a1616..f3a4b7d0 100644 --- a/terminal.c +++ b/terminal.c @@ -1082,7 +1082,11 @@ reload_fonts(struct terminal *term, bool resize_grid) * an a2r10g0b10 type of surface, since we need more than 2 * bits for alpha. */ +#if defined(HAVE_PIXMAN_RGBA_16) + options->color_glyphs.format = PIXMAN_a16b16g16r16; +#else options->color_glyphs.format = PIXMAN_rgba_float; +#endif } struct fcft_font *fonts[4]; @@ -1259,7 +1263,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, const enum shm_bit_depth desired_bit_depth = conf->tweak.surface_bit_depth == SHM_BITS_AUTO - ? wayl_do_linear_blending(wayl, conf) ? SHM_BITS_10 : SHM_BITS_8 + ? wayl_do_linear_blending(wayl, conf) ? SHM_BITS_16 : SHM_BITS_8 : conf->tweak.surface_bit_depth; const struct color_theme *theme = NULL; diff --git a/wayland.c b/wayland.c index 08994202..368b3be7 100644 --- a/wayland.c +++ b/wayland.c @@ -244,6 +244,8 @@ shm_format(void *data, struct wl_shm *wl_shm, uint32_t format) case WL_SHM_FORMAT_ARGB2101010: wayl->shm_have_argb2101010 = true; break; case WL_SHM_FORMAT_XBGR2101010: wayl->shm_have_xbgr2101010 = true; break; case WL_SHM_FORMAT_ABGR2101010: wayl->shm_have_abgr2101010 = true; break; + case WL_SHM_FORMAT_XBGR16161616: wayl->shm_have_xbgr161616 = true; break; + case WL_SHM_FORMAT_ABGR16161616: wayl->shm_have_abgr161616 = true; break; } #if defined(_DEBUG) diff --git a/wayland.h b/wayland.h index 044b217f..b7e8e79f 100644 --- a/wayland.h +++ b/wayland.h @@ -496,6 +496,8 @@ struct wayland { bool shm_have_xrgb2101010:1; bool shm_have_abgr2101010:1; bool shm_have_xbgr2101010:1; + bool shm_have_abgr161616:1; + bool shm_have_xbgr161616:1; }; struct wayland *wayl_init( From c6db0bed42d85776e5e74d2f56e355da2962b886 Mon Sep 17 00:00:00 2001 From: Chen Mulong <chenmulong@gmail.com> Date: Sat, 3 May 2025 09:31:24 +0800 Subject: [PATCH 1197/1323] Update catppuccin themes From https://github.com/catppuccin/foot Without the 'cursor.color', those themes have problems with cursor display problems in the zsh vi normal mode. --- themes/catppuccin-frappe | 5 +++++ themes/catppuccin-latte | 5 +++++ themes/catppuccin-macchiato | 5 +++++ themes/catppuccin-mocha | 5 +++++ 4 files changed, 20 insertions(+) diff --git a/themes/catppuccin-frappe b/themes/catppuccin-frappe index 3b2e0131..44bef16c 100644 --- a/themes/catppuccin-frappe +++ b/themes/catppuccin-frappe @@ -23,6 +23,11 @@ bright5=f4b8e4 bright6=81c8be bright7=a5adce +cursor=232634 f2d5cf + +16=ef9f76 +17=f2d5cf + selection-foreground=c6d0f5 selection-background=4f5369 diff --git a/themes/catppuccin-latte b/themes/catppuccin-latte index 8e545f70..d0b90e64 100644 --- a/themes/catppuccin-latte +++ b/themes/catppuccin-latte @@ -23,6 +23,11 @@ bright5=ea76cb bright6=179299 bright7=bcc0cc +cursor=eff1f5 dc8a78 + +16=fe640b +17=dc8a78 + selection-foreground=4c4f69 selection-background=ccced7 diff --git a/themes/catppuccin-macchiato b/themes/catppuccin-macchiato index 50aca7da..ae8adab8 100644 --- a/themes/catppuccin-macchiato +++ b/themes/catppuccin-macchiato @@ -23,6 +23,11 @@ bright5=f5bde6 bright6=8bd5ca bright7=a5adcb +cursor=181926 f4dbd6 + +16=f5a97f +17=f4dbd6 + selection-foreground=cad3f5 selection-background=454a5f diff --git a/themes/catppuccin-mocha b/themes/catppuccin-mocha index 508ca382..d29eb0ec 100644 --- a/themes/catppuccin-mocha +++ b/themes/catppuccin-mocha @@ -23,6 +23,11 @@ bright5=f5c2e7 bright6=94e2d5 bright7=a6adc8 +cursor=11111b f5e0dc + +16=fab387 +17=f5e0dc + selection-foreground=cdd6f4 selection-background=414356 From c037836bbd8415ff0fcd428cbadea3bed7dbe022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 5 May 2025 13:02:04 +0200 Subject: [PATCH 1198/1323] doc: foot.ini: fix description of dark/light themes --- doc/foot.ini.5.scd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index c1847932..1cc45231 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1113,8 +1113,8 @@ Note that values are not inherited. That is, if you set a value in inherited by *colors2*. In the context of private mode 2031 (Dark and Light Mode detection), -the primary theme (i.e. the *colors2* section) is considered to be the -light theme (since the default theme is dark). +the alternative theme (i.e. the *colors2* section) is considered to be +the light theme (since the default, the primary theme, is dark). # SECTION: csd From 073b637d4535cd0dfe92ca1ce954b552ae9f1a4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 5 May 2025 12:43:02 +0200 Subject: [PATCH 1199/1323] render: refactor to allow setting only selection bg or fg Before this, we only applied custom selection colors, if *both* the selection bg and fg had been set. Since the options are already split up into two separate options, and since it makes sense to at least be able to keep the foreground colors unchanged (i.e. only setting the selection background), let's allow only having one of the selection colors set. Closes #1846 --- CHANGELOG.md | 4 ++ config.c | 5 --- config.h | 1 - doc/foot.ini.5.scd | 3 +- osc.c | 4 -- render.c | 108 ++++++++++++++++++++++++++------------------- terminal.c | 2 - terminal.h | 1 - 8 files changed, 68 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a9917b5..ea818d53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,6 +103,10 @@ current alpha value is, unchanged. * `gamma-correct-blending=yes` now defaults to `16-bit` image buffers, instead of `10-bit`. +* Allow setting either selection background, or selection foreground, + only ([#1846][1846]). + +[1846]: https://codeberg.org/dnkl/foot/issues/1846 ### Deprecated diff --git a/config.c b/config.c index d0aae6a5..07f781d6 100644 --- a/config.c +++ b/config.c @@ -3403,7 +3403,6 @@ config_load(struct config *conf, const char *conf_path, .cursor = 0, }, .use_custom = { - .selection = false, .jump_label = false, .scrollback_indicator = false, .url = false, @@ -3593,10 +3592,6 @@ config_load(struct config *conf, const char *conf_path, if (!config_override_apply(conf, overrides, errors_are_fatal)) ret = !errors_are_fatal; - conf->colors.use_custom.selection = - conf->colors.selection_fg >> 24 == 0 && - conf->colors.selection_bg >> 24 == 0; - if (ret && conf->fonts[0].count == 0) { struct config_font font; if (!config_font_parse("monospace", &font)) { diff --git a/config.h b/config.h index 197b67cd..7cf6f6f5 100644 --- a/config.h +++ b/config.h @@ -180,7 +180,6 @@ struct color_theme { struct { bool cursor:1; - bool selection:1; bool jump_label:1; bool scrollback_indicator:1; bool url:1; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 1cc45231..81b88f64 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1069,8 +1069,7 @@ dark theme (since the default theme is dark). *selection-foreground*, *selection-background* Foreground (text) and background color to use in selected - text. Note that *both* options must be set, or the default will be - used. Default: _inverse foreground/background_. + text. Default: _inverse foreground/background_. *jump-labels* Two color values specifying the foreground (text) and background diff --git a/osc.c b/osc.c index 78f335e1..d59adc5a 100644 --- a/osc.c +++ b/osc.c @@ -1480,12 +1480,10 @@ osc_dispatch(struct terminal *term) case 17: term->colors.selection_bg = color; - term->colors.use_custom_selection = true; break; case 19: term->colors.selection_fg = color; - term->colors.use_custom_selection = true; break; } @@ -1589,13 +1587,11 @@ osc_dispatch(struct terminal *term) case 117: LOG_DBG("resetting selection background color"); term->colors.selection_bg = term->conf->colors.selection_bg; - term->colors.use_custom_selection = term->conf->colors.use_custom.selection; break; case 119: LOG_DBG("resetting selection foreground color"); term->colors.selection_fg = term->conf->colors.selection_fg; - term->colors.use_custom_selection = term->conf->colors.use_custom.selection; break; case 133: diff --git a/render.c b/render.c index 55c2ec4d..83a160bc 100644 --- a/render.c +++ b/render.c @@ -694,51 +694,75 @@ render_cell(struct terminal *term, pixman_image_t *pix, const int x = term->margins.left + col * width; const int y = term->margins.top + row_no * height; - bool is_selected = cell->attrs.selected; - uint32_t _fg = 0; uint32_t _bg = 0; uint16_t alpha = 0xffff; + const bool is_selected = cell->attrs.selected; + + /* Use cell specific color, if set, otherwise the default colors (possible reversed) */ + switch (cell->attrs.fg_src) { + case COLOR_RGB: + _fg = cell->attrs.fg; + break; + + case COLOR_BASE16: + case COLOR_BASE256: + xassert(cell->attrs.fg < ALEN(term->colors.table)); + _fg = term->colors.table[cell->attrs.fg]; + break; + + case COLOR_DEFAULT: + _fg = term->reverse ? term->colors.bg : term->colors.fg; + break; + } + + switch (cell->attrs.bg_src) { + case COLOR_RGB: + _bg = cell->attrs.bg; + break; + + case COLOR_BASE16: + case COLOR_BASE256: + xassert(cell->attrs.bg < ALEN(term->colors.table)); + _bg = term->colors.table[cell->attrs.bg]; + break; + + case COLOR_DEFAULT: + _bg = term->reverse ? term->colors.fg : term->colors.bg; + break; + } + + if (unlikely(is_selected)) { + const uint32_t cell_fg = _fg; + const uint32_t cell_bg = _bg; + + const bool custom_fg = term->colors.selection_fg >> 24 == 0; + const bool custom_bg = term->colors.selection_bg >> 24 == 0; + const bool custom_both = custom_fg && custom_bg; + + if (custom_both) { + _fg = term->colors.selection_fg; + _bg = term->colors.selection_bg; + } else if (custom_bg) { + _bg = term->colors.selection_bg; + _fg = cell->attrs.reverse ? cell_bg : cell_fg; + } else if (custom_fg) { + _fg = term->colors.selection_fg; + _bg = cell->attrs.reverse ? cell_fg : cell_bg; + } else { + _bg = cell_fg; + _fg = cell_bg; + } + + if (unlikely(_fg == _bg)) { + /* Invert bg when selected/highlighted text has same fg/bg */ + _bg = ~_bg; + alpha = 0xffff; + } - if (is_selected && term->colors.use_custom_selection) { - _fg = term->colors.selection_fg; - _bg = term->colors.selection_bg; } else { - /* Use cell specific color, if set, otherwise the default colors (possible reversed) */ - switch (cell->attrs.fg_src) { - case COLOR_RGB: - _fg = cell->attrs.fg; - break; - - case COLOR_BASE16: - case COLOR_BASE256: - xassert(cell->attrs.fg < ALEN(term->colors.table)); - _fg = term->colors.table[cell->attrs.fg]; - break; - - case COLOR_DEFAULT: - _fg = term->reverse ? term->colors.bg : term->colors.fg; - break; - } - - switch (cell->attrs.bg_src) { - case COLOR_RGB: - _bg = cell->attrs.bg; - break; - - case COLOR_BASE16: - case COLOR_BASE256: - xassert(cell->attrs.bg < ALEN(term->colors.table)); - _bg = term->colors.table[cell->attrs.bg]; - break; - - case COLOR_DEFAULT: - _bg = term->reverse ? term->colors.fg : term->colors.bg; - break; - } - - if (cell->attrs.reverse ^ is_selected) { + if (unlikely(cell->attrs.reverse)) { uint32_t swap = _fg; _fg = _bg; _bg = swap; @@ -806,12 +830,6 @@ render_cell(struct terminal *term, pixman_image_t *pix, } } - if (unlikely(is_selected && _fg == _bg)) { - /* Invert bg when selected/highlighted text has same fg/bg */ - _bg = ~_bg; - alpha = 0xffff; - } - if (cell->attrs.dim) _fg = color_dim(term, _fg); if (term->conf->bold_in_bright.enabled && cell->attrs.bold) diff --git a/terminal.c b/terminal.c index f3a4b7d0..9ec538c6 100644 --- a/terminal.c +++ b/terminal.c @@ -1312,7 +1312,6 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .cursor_bg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.cursor, .selection_fg = theme->selection_fg, .selection_bg = theme->selection_bg, - .use_custom_selection = theme->use_custom.selection, .active_theme = conf->initial_color_theme, }, .color_stack = { @@ -4714,6 +4713,5 @@ term_theme_apply(struct terminal *term, const struct color_theme *theme) term->colors.cursor_bg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.cursor; term->colors.selection_fg = theme->selection_fg; term->colors.selection_bg = theme->selection_bg; - term->colors.use_custom_selection = theme->use_custom.selection; memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); } diff --git a/terminal.h b/terminal.h index 4639fa69..3122cef3 100644 --- a/terminal.h +++ b/terminal.h @@ -404,7 +404,6 @@ struct colors { uint32_t cursor_bg; /* cursor color */ uint32_t selection_fg; uint32_t selection_bg; - bool use_custom_selection; enum which_color_theme active_theme; }; From 9b0d5e7c96f75d7ca81cd2e452cda54688ecc259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 8 May 2025 10:22:45 +0200 Subject: [PATCH 1200/1323] term: unittest: auto-scroll timer FD is created on-demand nowadays --- terminal.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/terminal.c b/terminal.c index 9ec538c6..18f3bc9f 100644 --- a/terminal.c +++ b/terminal.c @@ -2769,13 +2769,11 @@ UNITTEST }, .kind = SELECTION_NONE, .auto_scroll = { - .fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK), + .fd = -1, }, }, }; - xassert(term.selection.auto_scroll.fd >= 0); - #define populate_scrollback() do { \ for (int i = 0; i < scrollback_rows; i++) { \ if (term.normal.rows[i] == NULL) { \ @@ -2865,7 +2863,7 @@ UNITTEST /* Cleanup */ tll_free(term.normal.sixel_images); - close(term.selection.auto_scroll.fd); + xassert(term.selection.auto_scroll.fd == -1); for (int i = 0; i < scrollback_rows; i++) grid_row_free(term.normal.rows[i]); free(term.normal.rows); From ebd1614316ea541cd1ac0cc0029f771be333176e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 16 May 2025 10:46:25 +0200 Subject: [PATCH 1201/1323] csi: when REP:ing a "combining" character, use correct width Before this patch, we just called c32width(), which only works on actual codepoints. If the last printed character is a "combining" character, i.e. a key into our lookup table for multi-codepoint graphemes, we need to lookup the grapheme and pick the width from there. See https://gitlab.com/AutumnMeowMeow/jexer/-/issues/119#note_2499712901 --- CHANGELOG.md | 4 ++++ csi.c | 12 +++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea818d53..00c1f0a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,6 +116,10 @@ ### Removed ### Fixed + +* `REP`: wrong width of repeated multi-codepoint graphemes. + + ### Security ### Contributors diff --git a/csi.c b/csi.c index e8b2c492..6d4845be 100644 --- a/csi.c +++ b/csi.c @@ -799,7 +799,17 @@ csi_dispatch(struct terminal *term, uint8_t final) int count = vt_param_get(term, 0, 1); LOG_DBG("REP: '%lc' %d times", (wint_t)term->vt.last_printed, count); - const int width = c32width(term->vt.last_printed); + int width; + + if (term->vt.last_printed >= CELL_COMB_CHARS_LO) { + const struct composed *comp = composed_lookup( + term->composed, term->vt.last_printed - CELL_COMB_CHARS_LO); + + xassert(comp != NULL); + width = comp->forced_width > 0 ? comp->forced_width : comp->width; + } else + width = c32width(term->vt.last_printed); + if (width > 0) { for (int i = 0; i < count; i++) term_print(term, term->vt.last_printed, width, false); From 8bd39b32cd87abc65a30d23a83a0564f6419025e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 18 May 2025 11:29:50 +0200 Subject: [PATCH 1202/1323] Revert "xkbcommon: require libxkbcommon >= 1.8.0" This reverts commit 34d3f4664b93d42ec3e1eef9a11e78756465d25a. --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index a884e533..ae263851 100644 --- a/meson.build +++ b/meson.build @@ -137,7 +137,7 @@ wayland_protocols = dependency('wayland-protocols', version: '>=1.41', default_options: ['tests=false']) wayland_client = dependency('wayland-client') wayland_cursor = dependency('wayland-cursor') -xkb = dependency('xkbcommon', version: '>=1.8.0') +xkb = dependency('xkbcommon', version: '>=1.0.0') fontconfig = dependency('fontconfig') utf8proc = dependency('libutf8proc', required: get_option('grapheme-clustering')) From 3e1e3ea38ce8ed43ee61ddb584298f45dd72f1d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 18 May 2025 11:35:27 +0200 Subject: [PATCH 1203/1323] libxkbcommon: don't require 1.8.0 The version bump was done since we now use XKB_VMOD_NAME_*; macros added in libxkbcommon 1.8.0. Not all distros have updated libxkbcommon yet (read: Debian). Since it's fairly easy to work around, let's do that. Closes #2103 --- CHANGELOG.md | 3 +++ input.c | 1 + key-binding.c | 1 + meson.build | 1 + xkbcommon-vmod.h | 18 ++++++++++++++++++ 5 files changed, 24 insertions(+) create mode 100644 xkbcommon-vmod.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 00c1f0a0..cb478f87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,8 +105,11 @@ instead of `10-bit`. * Allow setting either selection background, or selection foreground, only ([#1846][1846]). +* Drop required version of libxkbcommon from 1.8.0 back to 1.0.0 + ([#2103][2103]). [1846]: https://codeberg.org/dnkl/foot/issues/1846 +[2103]: https://codeberg.org/dnkl/foot/issues/2103 ### Deprecated diff --git a/input.c b/input.c index b6c56fde..271ffb88 100644 --- a/input.c +++ b/input.c @@ -40,6 +40,7 @@ #include "url-mode.h" #include "util.h" #include "vt.h" +#include "xkbcommon-vmod.h" #include "xmalloc.h" #include "xsnprintf.h" diff --git a/key-binding.c b/key-binding.c index e5b7ac81..a2883ed5 100644 --- a/key-binding.c +++ b/key-binding.c @@ -11,6 +11,7 @@ #include "terminal.h" #include "util.h" #include "wayland.h" +#include "xkbcommon-vmod.h" #include "xmalloc.h" struct vmod_map { diff --git a/meson.build b/meson.build index ae263851..7b9490d9 100644 --- a/meson.build +++ b/meson.build @@ -319,6 +319,7 @@ executable( 'url-mode.c', 'url-mode.h', 'user-notification.c', 'user-notification.h', 'wayland.c', 'wayland.h', 'shm-formats.h', + 'xkbcommon-vmod.h', srgb_funcs, wl_proto_src + wl_proto_headers, version, dependencies: [math, threads, libepoll, pixman, wayland_client, wayland_cursor, xkb, fontconfig, utf8proc, tllist, fcft], diff --git a/xkbcommon-vmod.h b/xkbcommon-vmod.h new file mode 100644 index 00000000..44d818ec --- /dev/null +++ b/xkbcommon-vmod.h @@ -0,0 +1,18 @@ +#pragma once + +#include <xkbcommon/xkbcommon-names.h> + +/* Added in libxkbcommon 1.8.0 */ +#if !defined(XKB_VMOD_NAME_ALT) +/* Common *virtual* modifiers, encoded in xkeyboard-config in the compat and + * symbols files. They have been stable since the beginning of the project and + * are unlikely to ever change. */ +#define XKB_VMOD_NAME_ALT "Alt" +#define XKB_VMOD_NAME_HYPER "Hyper" +#define XKB_VMOD_NAME_LEVEL3 "LevelThree" +#define XKB_VMOD_NAME_LEVEL5 "LevelFive" +#define XKB_VMOD_NAME_META "Meta" +#define XKB_VMOD_NAME_NUM "NumLock" +#define XKB_VMOD_NAME_SCROLL "ScrollLock" +#define XKB_VMOD_NAME_SUPER "Super" +#endif From 456ac5d79f46a670479fcdaa75df0942a1571c78 Mon Sep 17 00:00:00 2001 From: Kirill Primak <vyivel@eclair.cafe> Date: Tue, 20 May 2025 15:01:25 +0300 Subject: [PATCH 1204/1323] render: improve CSD button positioning This commit fixes titlebar button positioning when maximization isn't available but minimization is. --- render.c | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/render.c b/render.c index 83a160bc..e0f32575 100644 --- a/render.c +++ b/render.c @@ -2254,16 +2254,21 @@ get_csd_data(const struct terminal *term, enum csd_surface surf_idx) const int button_width = title_visible ? roundf(term->conf->csd.button_width * scale) : 0; - const int button_close_width = term->width >= 1 * button_width - ? button_width : 0; + int remaining_width = term->width; - const int button_maximize_width = - term->width >= 2 * button_width && term->window->wm_capabilities.maximize - ? button_width : 0; + const int button_close_width = remaining_width >= button_width ? button_width : 0; + remaining_width -= button_close_width; + const int button_close_start = remaining_width; - const int button_minimize_width = - term->width >= 3 * button_width && term->window->wm_capabilities.minimize - ? button_width : 0; + const int button_maximize_width = remaining_width >= button_width && + term->window->wm_capabilities.maximize ? button_width : 0; + remaining_width -= button_maximize_width; + const int button_maximize_start = remaining_width; + + const int button_minimize_width = remaining_width >= button_width && + term->window->wm_capabilities.minimize ? button_width : 0; + remaining_width -= button_minimize_width; + const int button_minimize_start = remaining_width; /* * With fractional scaling, we must ensure the offset, when @@ -2288,9 +2293,9 @@ get_csd_data(const struct terminal *term, enum csd_surface surf_idx) case CSD_SURF_BOTTOM: return (struct csd_data){-border_width, term->height, top_bottom_width, border_width}; /* Positioned relative to CSD_SURF_TITLE */ - case CSD_SURF_MINIMIZE: return (struct csd_data){term->width - 3 * button_width, 0, button_minimize_width, title_height}; - case CSD_SURF_MAXIMIZE: return (struct csd_data){term->width - 2 * button_width, 0, button_maximize_width, title_height}; - case CSD_SURF_CLOSE: return (struct csd_data){term->width - 1 * button_width, 0, button_close_width, title_height}; + case CSD_SURF_MINIMIZE: return (struct csd_data){button_minimize_start, 0, button_minimize_width, title_height}; + case CSD_SURF_MAXIMIZE: return (struct csd_data){button_maximize_start, 0, button_maximize_width, title_height}; + case CSD_SURF_CLOSE: return (struct csd_data){ button_close_start, 0, button_close_width, title_height}; case CSD_SURF_COUNT: break; From d26659988113662d1500f600f5c10899920a5e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 21 May 2025 13:01:30 +0200 Subject: [PATCH 1205/1323] wayland: configure: don't commit if we have a pending refresh Currently, if the following occurs: 1. foot has AxB size 2. Compositor sends CxD size 3. foot detects a resize, acks and saves CxD, but doesn't redraw immediately 4. Compositor sends CxD size again (due to a toplevel state array change, for example) Then foot will detect no resize occurred, and will do an "empty" commit immediately. In this particular case that's wrong, since we're effectively acking+committing the initial AxB size. Fix by only doing the immediate commit if there's no size change **and** there's no pending refresh. Note: normally, we'd resize and refresh+commit immediately, but if we're waiting for a frame callback, then the refresh+commit will be delayed (i.e. scheduled). This is what we're checking here. Closes #2105 --- CHANGELOG.md | 4 ++++ wayland.c | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb478f87..fe149600 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -121,6 +121,10 @@ ### Fixed * `REP`: wrong width of repeated multi-codepoint graphemes. +* Incorrect surface commit after a configure event, under certain + conditions ([#2105][2105]). + +[2105]: https://codeberg.org/dnkl/foot/issues/2105 ### Security diff --git a/wayland.c b/wayland.c index 368b3be7..37fefb29 100644 --- a/wayland.c +++ b/wayland.c @@ -1134,7 +1134,7 @@ xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, else term_visual_focus_out(term); - if (!resized) { + if (!resized && !term->render.pending.grid) { /* * If we didn't resize, we won't be committing a new surface * anytime soon. Some compositors require a commit in From 664cdcc65cf42ab269ea4a2c4962c3ee65d7e92f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 21 May 2025 15:25:28 +0200 Subject: [PATCH 1206/1323] cursor-shape: add 'dnd-ask' and 'all-resize' These (non-css) cursor shapes were added to the cursor-shape-v1 protocol in wayland-protocols 1.42. We don't need (or use them at all) internally, but add them to the list we use to translate from shape names to shape enums. This allows users to set a custom shape (via OSC-22), while still using server side cursors (i.e. no need to fallback to client-side cursors). If we try to set a shape not implemented by the server, we get a protocol error and foot exits. This is bad. So, make sure we don't do that: 1. First, we need to explicitly bind v2 if implemented by the server 2. Track the bound version number in the wayland struct 3. When matching shape enum, skip shapes not supported in the currently bound version of the cursor-shape protocol --- CHANGELOG.md | 1 + cursor-shape.c | 22 +++++++++++++++++++++- cursor-shape.h | 2 +- render.c | 5 +++-- terminal.c | 4 +++- wayland.c | 10 +++++++++- wayland.h | 1 + 7 files changed, 39 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe149600..66fa7c93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,7 @@ * `16-bit` to `tweak.surface-bit-depth`. Makes foot use 16-bit image buffers. They provide the necessary color precision required by `gamma-correct-blending=yes`. +* New cursor shapes, from `cursor-shape-v1` version 2. [2025]: https://codeberg.org/dnkl/foot/issues/2025 [1975]: https://codeberg.org/dnkl/foot/issues/1975 diff --git a/cursor-shape.c b/cursor-shape.c index bbf75ab8..6e859259 100644 --- a/cursor-shape.c +++ b/cursor-shape.c @@ -54,7 +54,7 @@ cursor_shape_to_server_shape(enum cursor_shape shape) } enum wp_cursor_shape_device_v1_shape -cursor_string_to_server_shape(const char *xcursor) +cursor_string_to_server_shape(const char *xcursor, int bound_version) { if (xcursor == NULL) return 0; @@ -94,9 +94,29 @@ cursor_string_to_server_shape(const char *xcursor) [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_SCROLL] = {"all-scroll", "fleur"}, [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ZOOM_IN] = {"zoom-in"}, [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ZOOM_OUT] = {"zoom-out"}, +#if defined(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK_SINCE_VERSION) /* 1.42 */ + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK] = {"dnd-ask"}, +#endif +#if defined(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_RESIZE_SINCE_VERSION) /* 1.42 */ + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_RESIZE] = {"all-resize"}, +#endif }; for (size_t i = 0; i < ALEN(table); i++) { +#if defined(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK_SINCE_VERSION) + if (i == WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK && + bound_version < WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK_SINCE_VERSION) + { + continue; + } +#endif +#if defined(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_RESIZE_SINCE_VERSION) + if (i == WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_RESIZE && + bound_version < WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_RESIZE_SINCE_VERSION) + { + continue; + } +#endif for (size_t j = 0; j < ALEN(table[i]); j++) { if (table[i][j] != NULL && streq(xcursor, table[i][j])) { return i; diff --git a/cursor-shape.h b/cursor-shape.h index 110dbd2e..13690588 100644 --- a/cursor-shape.h +++ b/cursor-shape.h @@ -26,4 +26,4 @@ const char *const *cursor_shape_to_string(enum cursor_shape shape); enum wp_cursor_shape_device_v1_shape cursor_shape_to_server_shape( enum cursor_shape shape); enum wp_cursor_shape_device_v1_shape cursor_string_to_server_shape( - const char *xcursor); + const char *xcursor, int bound_version); diff --git a/render.c b/render.c index e0f32575..a41eee0c 100644 --- a/render.c +++ b/render.c @@ -4929,8 +4929,9 @@ render_xcursor_update(struct seat *seat) const enum wp_cursor_shape_device_v1_shape custom_shape = (shape == CURSOR_SHAPE_CUSTOM && xcursor != NULL - ? cursor_string_to_server_shape(xcursor) - : 0); + ? cursor_string_to_server_shape( + xcursor, seat->wayl->shape_manager_version) + : 0); if (shape != CURSOR_SHAPE_CUSTOM || custom_shape != 0) { xassert(custom_shape == 0 || shape == CURSOR_SHAPE_CUSTOM); diff --git a/terminal.c b/terminal.c index 18f3bc9f..6f66f65b 100644 --- a/terminal.c +++ b/terminal.c @@ -3571,7 +3571,9 @@ term_xcursor_update_for_seat(struct terminal *term, struct seat *seat) if (seat->pointer.hidden) shape = CURSOR_SHAPE_HIDDEN; - else if (cursor_string_to_server_shape(term->mouse_user_cursor) != 0 || + else if (cursor_string_to_server_shape( + term->mouse_user_cursor, + term->wl->shape_manager_version) != 0 || render_xcursor_is_valid(seat, term->mouse_user_cursor)) { shape = CURSOR_SHAPE_CUSTOM; diff --git a/wayland.c b/wayland.c index 37fefb29..7d3c7c67 100644 --- a/wayland.c +++ b/wayland.c @@ -1480,8 +1480,16 @@ handle_global(void *data, struct wl_registry *registry, if (!verify_iface_version(interface, version, required)) return; +#if defined(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK_SINCE_VERSION) /* 1.42 */ + const uint32_t preferred = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK_SINCE_VERSION; +#else + const uint32_t preferred = required; +#endif + + wayl->shape_manager_version = min(required, preferred); wayl->cursor_shape_manager = wl_registry_bind( - wayl->registry, name, &wp_cursor_shape_manager_v1_interface, required); + wayl->registry, name, &wp_cursor_shape_manager_v1_interface, + min(required, preferred)); } else if (streq(interface, wp_single_pixel_buffer_manager_v1_interface.name)) { diff --git a/wayland.h b/wayland.h index b7e8e79f..eb1c35a3 100644 --- a/wayland.h +++ b/wayland.h @@ -460,6 +460,7 @@ struct wayland { struct wp_fractional_scale_manager_v1 *fractional_scale_manager; struct wp_cursor_shape_manager_v1 *cursor_shape_manager; + int shape_manager_version; struct wp_single_pixel_buffer_manager_v1 *single_pixel_manager; From 5621829bb00deea6c187c0c328e2560b84f809d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 23 May 2025 13:31:53 +0200 Subject: [PATCH 1207/1323] cursor-shape: map "dnd-move" to WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_MOVE --- cursor-shape.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cursor-shape.c b/cursor-shape.c index 6e859259..c195a554 100644 --- a/cursor-shape.c +++ b/cursor-shape.c @@ -72,7 +72,7 @@ cursor_string_to_server_shape(const char *xcursor, int bound_version) [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_VERTICAL_TEXT] = {"vertical-text"}, [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALIAS] = {"alias", "dnd-link"}, [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_COPY] = {"copy", "dnd-copy"}, - [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_MOVE] = {"move"}, /* dnd-move? */ + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_MOVE] = {"move", "dnd-move"}, [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NO_DROP] = {"no-drop", "dnd-no-drop"}, [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NOT_ALLOWED] = {"not-allowed", "crossed_circle"}, [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_GRAB] = {"grab", "hand1"}, From 5a84f8d841d09e9aa91befbbb8e8b634b7f8959a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 23 May 2025 08:38:00 +0200 Subject: [PATCH 1208/1323] conf: pad: add center-when-fullscreen and center-when-maximized-and-fullscreen Before this patch, the grid content was *always* centered when the window was maximized or fullscreened, regardless of how the user had configured padding. Now, the behavior is controlled by the 'pad' option. Before this patch, the syntax was pad MxN [center] Now it is pad MxN [center|center-when-fullscreen|center-when-maximized-and-fullscreen] The default is "pad 0x0 center-when-maximized-and-fullscreen", to match current behavior. Closes #2111 --- CHANGELOG.md | 4 ++++ config.c | 28 +++++++++++++++++++++------- config.h | 10 +++++++++- doc/foot.ini.5.scd | 27 +++++++++++++++++++-------- foot.ini | 2 +- render.c | 9 ++++++--- 6 files changed, 60 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66fa7c93..9ade3b6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,9 +91,13 @@ buffers. They provide the necessary color precision required by `gamma-correct-blending=yes`. * New cursor shapes, from `cursor-shape-v1` version 2. +* `center-when-fullscreen` and `center-when-maximized-and-fullscreen` + to the `pad` option. This allows you to configure when the grid is + centered in more detail ([#2111][2111]). [2025]: https://codeberg.org/dnkl/foot/issues/2025 [1975]: https://codeberg.org/dnkl/foot/issues/1975 +[2111]: https://codeberg.org/dnkl/foot/issues/2111 ### Changed diff --git a/config.c b/config.c index 07f781d6..d8f1c0ed 100644 --- a/config.c +++ b/config.c @@ -933,21 +933,34 @@ parse_section_main(struct context *ctx) else if (streq(key, "pad")) { unsigned x, y; - char mode[16] = {0}; + char mode[64] = {0}; + int ret = sscanf(value, "%ux%u %63s", &x, &y, mode); - int ret = sscanf(value, "%ux%u %15s", &x, &y, mode); - bool center = strcasecmp(mode, "center") == 0; - bool invalid_mode = !center && mode[0] != '\0'; + enum center_when center = CENTER_NEVER; - if ((ret != 2 && ret != 3) || invalid_mode) { + if (ret == 3) { + if (strcasecmp(mode, "center") == 0) + center = CENTER_ALWAYS; + else if (strcasecmp(mode, "center-when-fullscreen") == 0) + center = CENTER_FULLSCREEN; + else if (strcasecmp(mode, "center-when-maximized-and-fullscreen") == 0) + center = CENTER_MAXIMIZED_AND_FULLSCREEN; + else + center = CENTER_INVALID; + } + + if ((ret != 2 && ret != 3) || center == CENTER_INVALID) { LOG_CONTEXTUAL_ERR( - "invalid padding (must be in the form PAD_XxPAD_Y [center])"); + "invalid padding (must be in the form PAD_XxPAD_Y " + "[center|" + "center-when-fullscreen|" + "center-when-maximized-and-fullscreen])"); return false; } conf->pad_x = x; conf->pad_y = y; - conf->center = center; + conf->center_when = ret == 2 ? CENTER_NEVER : center; return true; } @@ -3339,6 +3352,7 @@ config_load(struct config *conf, const char *conf_path, }, .pad_x = 0, .pad_y = 0, + .center_when = CENTER_MAXIMIZED_AND_FULLSCREEN, .resize_by_cells = true, .resize_keep_grid = true, .resize_delay_ms = 100, diff --git a/config.h b/config.h index 7cf6f6f5..315f7e24 100644 --- a/config.h +++ b/config.h @@ -201,6 +201,14 @@ enum shm_bit_depth { SHM_BITS_16, }; +enum center_when { + CENTER_INVALID, + CENTER_NEVER, + CENTER_FULLSCREEN, + CENTER_MAXIMIZED_AND_FULLSCREEN, + CENTER_ALWAYS, +}; + struct config { char *term; char *shell; @@ -218,7 +226,7 @@ struct config { unsigned pad_x; unsigned pad_y; - bool center; + enum center_when center_when; bool resize_by_cells; bool resize_keep_grid; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 81b88f64..74b3f35b 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -289,18 +289,29 @@ empty string to be set, but it must be quoted: *KEY=""*) *pad* Padding between border and glyphs, in pixels (subject to output - scaling), in the form _XxY_. + scaling), in the form + + ``` + _XxY_ [center | center-when-fullscreen | center-when-maximized-and-fullscreen] + ``` This will add _at least_ X pixels on both the left and right - sides, and Y pixels on the top and bottom sides. The grid content - will be anchored in the top left corner. I.e. if the window - manager forces an odd window size on foot, the additional pixels - will be added to the right and bottom sides. + sides, and Y pixels on the top and bottom sides. - To instead center the grid content, append *center* (e.g. *pad=5x5 - center*). + When no centering is specified, the grid content is anchored to + the top left corner. I.e. if the window manager forces an odd + window size on foot, the additional pixels will be added to the + right and bottom sides. - Default: _0x0_. + If *center* is specified, the grid content is instead + centered. This may cause "jumpiness" when resizing the window. + + With *center-when-fullscreen* and + *center-when-maximized-and-fullscreen*, the grid is anchored to + the top left corner, unless the window is maximized, or + fullscreened. + + Default: _0x0_ center-when-maximized-and-fullscreen. *resize-delay-ms* diff --git a/foot.ini b/foot.ini index f3ef6d85..73fdb7ab 100644 --- a/foot.ini +++ b/foot.ini @@ -28,7 +28,7 @@ # initial-window-size-pixels=700x500 # Or, # initial-window-size-chars=<COLSxROWS> # initial-window-mode=windowed -# pad=0x0 # optionally append 'center' +# pad=0x0 center-when-maximized-and-fullscreen # resize-by-cells=yes # resize-keep-grid=yes # resize-delay-ms=100 diff --git a/render.c b/render.c index a41eee0c..244152ef 100644 --- a/render.c +++ b/render.c @@ -4596,9 +4596,12 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) const int total_x_pad = term->width - grid_width; const int total_y_pad = term->height - grid_height; - const bool centered_padding = term->conf->center - || term->window->is_fullscreen - || term->window->is_maximized; + const enum center_when center = term->conf->center_when; + const bool centered_padding = + center == CENTER_ALWAYS || + (center == CENTER_MAXIMIZED_AND_FULLSCREEN && + (term->window->is_fullscreen || term->window->is_maximized)) || + (center == CENTER_FULLSCREEN && term->window->is_fullscreen); if (centered_padding && !term->window->is_resizing) { term->margins.left = total_x_pad / 2; From eeaecba7238fb3a501cb25b55b7d2ada2bc4d023 Mon Sep 17 00:00:00 2001 From: tokyo4j <hrak1529@gmail.com> Date: Sat, 24 May 2025 19:06:29 +0900 Subject: [PATCH 1209/1323] wayland: fix global listener for xdg_toplevel_icon_manager_v1 --- wayland.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wayland.c b/wayland.c index 7d3c7c67..5f68ecf7 100644 --- a/wayland.c +++ b/wayland.c @@ -1502,13 +1502,13 @@ handle_global(void *data, struct wl_registry *registry, &wp_single_pixel_buffer_manager_v1_interface, required); } - else if (streq(interface, xdg_toplevel_icon_v1_interface.name)) { + else if (streq(interface, xdg_toplevel_icon_manager_v1_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; wayl->toplevel_icon_manager = wl_registry_bind( - wayl->registry, name, &xdg_toplevel_icon_v1_interface, required); + wayl->registry, name, &xdg_toplevel_icon_manager_v1_interface, required); } else if (streq(interface, xdg_system_bell_v1_interface.name)) { From 7347f4beb1314df12e834a260f18feae8b775a5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 9 Jun 2025 07:08:24 +0200 Subject: [PATCH 1210/1323] quirks: remove subsurface unmap quirk for Sway Sway used to have an issue where unmapping a subsurface did not damage the surface below (https://github.com/swaywm/sway/issues/6960). This has been fixed for quite some time now, so let's remove the quirk. --- CHANGELOG.md | 5 +++++ quirks.c | 12 ++---------- quirks.h | 2 -- render.c | 11 +---------- url-mode.c | 4 ---- 5 files changed, 8 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ade3b6f..b048bda3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -123,6 +123,11 @@ ### Removed + +* Subsurface unmap quirk for Sway. This was a workaround added in + 1.12.1, for Sway issue [#6960][sway-6960]. + + ### Fixed * `REP`: wrong width of repeated multi-codepoint graphemes. diff --git a/quirks.c b/quirks.c index 7cc8a8f1..67cb587e 100644 --- a/quirks.c +++ b/quirks.c @@ -67,6 +67,7 @@ quirk_weston_csd_off(struct terminal *term) quirk_weston_subsurface_desync_off(term->window->csd.surface[i].sub); } +#if 0 static bool is_sway(void) { @@ -82,13 +83,4 @@ is_sway(void) return is_sway; } - -void -quirk_sway_subsurface_unmap(struct terminal *term) -{ - return; - if (!is_sway()) - return; - - wl_surface_damage_buffer(term->window->surface.surf, 0, 0, INT32_MAX, INT32_MAX); -} +#endif diff --git a/quirks.h b/quirks.h index 0e840667..e762bb3e 100644 --- a/quirks.h +++ b/quirks.h @@ -21,5 +21,3 @@ void quirk_weston_subsurface_desync_off(struct wl_subsurface *sub); /* Shortcuts to call desync_{on,off} on all CSD subsurfaces */ void quirk_weston_csd_on(struct terminal *term); void quirk_weston_csd_off(struct terminal *term); - -void quirk_sway_subsurface_unmap(struct terminal *term); diff --git a/render.c b/render.c index 244152ef..1c24bafa 100644 --- a/render.c +++ b/render.c @@ -1970,10 +1970,6 @@ render_overlay(struct terminal *term) wl_surface_commit(overlay->surface.surf); term->render.last_overlay_style = OVERLAY_NONE; term->render.last_overlay_buf = NULL; - - /* Work around Sway bug - unmapping a sub-surface does not - * damage the underlying surface */ - quirk_sway_subsurface_unmap(term); } return; } @@ -2919,13 +2915,8 @@ render_scrollback_position(struct terminal *term) struct wl_window *win = term->window; if (term->grid->view == term->grid->offset) { - if (win->scrollback_indicator.surface.surf != NULL) { + if (win->scrollback_indicator.surface.surf != NULL) wayl_win_subsurface_destroy(&win->scrollback_indicator); - - /* Work around Sway bug - unmapping a sub-surface does not damage - * the underlying surface */ - quirk_sway_subsurface_unmap(term); - } return; } diff --git a/url-mode.c b/url-mode.c index 199ff3f1..35843219 100644 --- a/url-mode.c +++ b/url-mode.c @@ -820,10 +820,6 @@ urls_reset(struct terminal *term) tll_foreach(term->window->urls, it) { wayl_win_subsurface_destroy(&it->item.surf); tll_remove(term->window->urls, it); - - /* Work around Sway bug - unmapping a sub-surface does not - * damage the underlying surface */ - quirk_sway_subsurface_unmap(term); } } From 33eefa7b45c2d20750f5788dfdcc1008cf33ca47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 9 Jun 2025 07:37:29 +0200 Subject: [PATCH 1211/1323] term+input: refactor: move theme switching into term_theme_* functions --- input.c | 51 ++------------------------------- terminal.c | 83 ++++++++++++++++++++++++++++++++++++++++++++++++------ terminal.h | 4 ++- 3 files changed, 80 insertions(+), 58 deletions(-) diff --git a/input.c b/input.c index 271ffb88..fe90a7f0 100644 --- a/input.c +++ b/input.c @@ -486,60 +486,15 @@ execute_binding(struct seat *seat, struct terminal *term, return true; case BIND_ACTION_THEME_SWITCH_1: - if (term->colors.active_theme != COLOR_THEME1) { - term_theme_apply(term, &term->conf->colors); - term->colors.active_theme = COLOR_THEME1; - - wayl_win_alpha_changed(term->window); - term_font_subpixel_changed(term); - - if (term->report_theme_changes) - term_to_slave(term, "\033[?997;1n", 9); - - term_damage_view(term); - term_damage_margins(term); - render_refresh(term); - } + term_theme_switch_to_1(term); return true; case BIND_ACTION_THEME_SWITCH_2: - if (term->colors.active_theme != COLOR_THEME2) { - term_theme_apply(term, &term->conf->colors2); - term->colors.active_theme = COLOR_THEME2; - - wayl_win_alpha_changed(term->window); - term_font_subpixel_changed(term); - - if (term->report_theme_changes) - term_to_slave(term, "\033[?997;2n", 9); - - term_damage_view(term); - term_damage_margins(term); - render_refresh(term); - } + term_theme_switch_to_2(term); return true; case BIND_ACTION_THEME_TOGGLE: - if (term->colors.active_theme == COLOR_THEME1) { - term_theme_apply(term, &term->conf->colors2); - term->colors.active_theme = COLOR_THEME2; - - if (term->report_theme_changes) - term_to_slave(term, "\033[?997;2n", 9); - } else { - term_theme_apply(term, &term->conf->colors); - term->colors.active_theme = COLOR_THEME1; - - if (term->report_theme_changes) - term_to_slave(term, "\033[?997;1n", 9); - } - - wayl_win_alpha_changed(term->window); - term_font_subpixel_changed(term); - - term_damage_view(term); - term_damage_margins(term); - render_refresh(term); + term_theme_toggle(term); return true; case BIND_ACTION_SELECT_BEGIN: diff --git a/terminal.c b/terminal.c index 6f66f65b..135f21ce 100644 --- a/terminal.c +++ b/terminal.c @@ -2074,6 +2074,19 @@ erase_line(struct terminal *term, struct row *row) row->shell_integration.cmd_end = -1; } +static void +term_theme_apply(struct terminal *term, const struct color_theme *theme) +{ + term->colors.fg = theme->fg; + term->colors.bg = theme->bg; + term->colors.alpha = theme->alpha; + term->colors.cursor_fg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.text; + term->colors.cursor_bg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.cursor; + term->colors.selection_fg = theme->selection_fg; + term->colors.selection_bg = theme->selection_bg; + memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); +} + void term_reset(struct terminal *term, bool hard) { @@ -4704,14 +4717,66 @@ term_send_size_notification(struct terminal *term) } void -term_theme_apply(struct terminal *term, const struct color_theme *theme) +term_theme_switch_to_1(struct terminal *term) { - term->colors.fg = theme->fg; - term->colors.bg = theme->bg; - term->colors.alpha = theme->alpha; - term->colors.cursor_fg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.text; - term->colors.cursor_bg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.cursor; - term->colors.selection_fg = theme->selection_fg; - term->colors.selection_bg = theme->selection_bg; - memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); + if (term->colors.active_theme == COLOR_THEME1) + return; + + term_theme_apply(term, &term->conf->colors); + term->colors.active_theme = COLOR_THEME1; + + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;1n", 9); + + term_damage_view(term); + term_damage_margins(term); + render_refresh(term); +} + +void +term_theme_switch_to_2(struct terminal *term) +{ + if (term->colors.active_theme == COLOR_THEME2) + return; + + term_theme_apply(term, &term->conf->colors2); + term->colors.active_theme = COLOR_THEME2; + + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;2n", 9); + + term_damage_view(term); + term_damage_margins(term); + render_refresh(term); +} + +void +term_theme_toggle(struct terminal *term) +{ + if (term->colors.active_theme == COLOR_THEME1) { + term_theme_apply(term, &term->conf->colors2); + term->colors.active_theme = COLOR_THEME2; + + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;2n", 9); + } else { + term_theme_apply(term, &term->conf->colors); + term->colors.active_theme = COLOR_THEME1; + + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;1n", 9); + } + + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + + term_damage_view(term); + term_damage_margins(term); + render_refresh(term); } diff --git a/terminal.h b/terminal.h index 3122cef3..88371b07 100644 --- a/terminal.h +++ b/terminal.h @@ -984,7 +984,9 @@ void term_enable_size_notifications(struct terminal *term); void term_disable_size_notifications(struct terminal *term); void term_send_size_notification(struct terminal *term); -void term_theme_apply(struct terminal *term, const struct color_theme *theme); +void term_theme_switch_to_1(struct terminal *term); +void term_theme_switch_to_2(struct terminal *term); +void term_theme_toggle(struct terminal *term); static inline void term_reset_grapheme_state(struct terminal *term) { From d9675a714016553264ae3b77fee785c6c49d2b6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 9 Jun 2025 07:38:26 +0200 Subject: [PATCH 1212/1323] main: do a theme toggle upon receiving SIGUSR1 Caveat: in server mode, *all* instances toggle their themes. --- CHANGELOG.md | 2 ++ main.c | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b048bda3..82b4238b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,8 @@ `key-bindings.color-theme-toggle` key bindings. These can be used to switch between the primary and alternative color themes. They are not bound by default. +* Sending `SIGUSR1` to the foot process now triggers a theme switch + (in server mode, **all** instances toggles their themes). * Support for private mode 2031 - [_Dark and Light Mode Detection_](https://contour-terminal.org/vt-extensions/color-palette-update-notifications/) ([#2025][2025]) diff --git a/main.c b/main.c index b9404503..37bbb6a7 100644 --- a/main.c +++ b/main.c @@ -45,6 +45,19 @@ fdm_sigint(struct fdm *fdm, int signo, void *data) return true; } +static bool +fdm_sigusr1(struct fdm *fdm, int signo, void *data) +{ + struct wayland *wayl = data; + + tll_foreach(wayl->terms, it) { + struct terminal *term = it->item; + term_theme_toggle(term); + } + + return true; +} + static void print_usage(const char *prog_name) { @@ -608,6 +621,9 @@ main(int argc, char *const *argv) goto out; } + if (!fdm_signal_add(fdm, SIGUSR1, &fdm_sigusr1, wayl)) + goto out; + struct sigaction sig_ign = {.sa_handler = SIG_IGN}; sigemptyset(&sig_ign.sa_mask); if (sigaction(SIGHUP, &sig_ign, NULL) < 0 || @@ -643,6 +659,7 @@ out: wayl_destroy(wayl); key_binding_manager_destroy(key_binding_manager); reaper_destroy(reaper); + fdm_signal_del(fdm, SIGUSR1); fdm_signal_del(fdm, SIGTERM); fdm_signal_del(fdm, SIGINT); fdm_destroy(fdm); From 499f019deaf6e7b7e7bb6bbb43b93a7296d9b4fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 9 Jun 2025 09:12:08 +0200 Subject: [PATCH 1213/1323] osc: 52: clear selection if the payload is the empty string --- CHANGELOG.md | 1 + osc.c | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82b4238b..329a7650 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -114,6 +114,7 @@ only ([#1846][1846]). * Drop required version of libxkbcommon from 1.8.0 back to 1.0.0 ([#2103][2103]). +* OSC-52: an empty payload now clears the clipboard. [1846]: https://codeberg.org/dnkl/foot/issues/1846 [2103]: https://codeberg.org/dnkl/foot/issues/2103 diff --git a/osc.c b/osc.c index d59adc5a..834029c1 100644 --- a/osc.c +++ b/osc.c @@ -73,16 +73,19 @@ osc_to_clipboard(struct terminal *term, const char *target, } char *decoded = base64_decode(base64_data, NULL); - if (decoded == NULL) { - if (errno == EINVAL) - LOG_WARN("OSC: invalid clipboard data: %s", base64_data); - else - LOG_ERRNO("base64_decode() failed"); + if (decoded == NULL || decoded[0] == '\0') { + if (decoded == NULL) { + if (errno == EINVAL) + LOG_WARN("OSC: invalid clipboard data: %s", base64_data); + else + LOG_ERRNO("base64_decode() failed"); + } if (to_clipboard) selection_clipboard_unset(seat); if (to_primary) selection_primary_unset(seat); + free(decoded); return; } From 968bc05c32a2e68282fd28e840b06ac63556d82e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 9 Jun 2025 09:19:07 +0200 Subject: [PATCH 1214/1323] csi: add '52' to the DA reply, to indicate PSC-52 support Note: only *copy* is required to be enabled in security.osc52; paste is optional, see https://github.com/contour-terminal/contour/issues/1761#issuecomment-2944492097 --- CHANGELOG.md | 3 +++ csi.c | 17 ++++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 329a7650..5a6cf6a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,6 +115,9 @@ * Drop required version of libxkbcommon from 1.8.0 back to 1.0.0 ([#2103][2103]). * OSC-52: an empty payload now clears the clipboard. +* DA (Device Attributes): include `52` in the reply, to indicate + OSC-52 support (when at least _copy_ has been enabled in + `security.osc52`). [1846]: https://codeberg.org/dnkl/foot/issues/1846 [2103]: https://codeberg.org/dnkl/foot/issues/2103 diff --git a/csi.c b/csi.c index 6d4845be..437fd8bc 100644 --- a/csi.c +++ b/csi.c @@ -850,6 +850,7 @@ csi_dispatch(struct terminal *term, uint8_t final) * - 22 ANSI color, e.g., VT525. * - 28 Rectangular editing. * - 29 ANSI text locator (i.e., DEC Locator mode). + * - 52 Clipboard access * * Note: we report ourselves as a VT220, mainly to be able * to pass parameters, to indicate we support sixel, and @@ -860,13 +861,15 @@ csi_dispatch(struct terminal *term, uint8_t final) * * Note: tertiary DA responds with "FOOT". */ - if (term->conf->tweak.sixel) { - static const char reply[] = "\033[?62;4;22;28c"; - term_to_slave(term, reply, sizeof(reply) - 1); - } else { - static const char reply[] = "\033[?62;22;28c"; - term_to_slave(term, reply, sizeof(reply) - 1); - } + char reply[32]; + + int len = snprintf( + reply, sizeof(reply), "\033[?62%s;22;28%sc", + term->conf->tweak.sixel ? ";4" : "", + (term->conf->security.osc52 == OSC52_ENABLED || + term->conf->security.osc52 == OSC52_COPY_ENABLED ? ";52" : "")); + + term_to_slave(term, reply, len); break; } From aa579acd6ecd76941ca5bb8821396cf9c06ca8ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 11 Jul 2025 16:30:18 +0200 Subject: [PATCH 1215/1323] issue template: compositor version -> compositor name and version The existing hints and descriptions are apparently not enough; some people still only mention the version, which is rather useless. --- .forgejo/issue_template/bug.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/issue_template/bug.yml b/.forgejo/issue_template/bug.yml index a5000090..921bd68f 100644 --- a/.forgejo/issue_template/bug.yml +++ b/.forgejo/issue_template/bug.yml @@ -29,7 +29,7 @@ body: - type: input id: compositor attributes: - label: Compositor Version + label: Compositor Name and Version description: "The name and version of your compositor" placeholder: "sway version 1.9" validations: From 693aefa96a74b0e4b9117a0c4ffe8c95b35b0c8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 11 Jul 2025 16:47:51 +0200 Subject: [PATCH 1216/1323] config: silence valgrind-detected leak in config_font_parse() --- config.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config.c b/config.c index d8f1c0ed..77dc3a73 100644 --- a/config.c +++ b/config.c @@ -3956,9 +3956,10 @@ config_font_parse(const char *pattern, struct config_font *font) * both "size" and "pixelsize" being set, and we don't know * which one takes priority. */ + FcConfig *fc_conf = FcConfigCreate(); FcPattern *pat_copy = FcPatternDuplicate(pat); if (pat_copy == NULL || - !FcConfigSubstitute(NULL, pat_copy, FcMatchPattern)) + !FcConfigSubstitute(fc_conf, pat_copy, FcMatchPattern)) { LOG_WARN("%s: failed to do config substitution", pattern); } else { @@ -3967,6 +3968,7 @@ config_font_parse(const char *pattern, struct config_font *font) } FcPatternDestroy(pat_copy); + FcConfigDestroy(fc_conf); if (have_pt_size != FcResultMatch && have_px_size != FcResultMatch) pt_size = 8.0; From e72e08625d9f085755f476e6deb327de913031c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 16 Jul 2025 08:14:54 +0200 Subject: [PATCH 1217/1323] changelog: prepare for 1.23.0 --- CHANGELOG.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a6cf6a9..8985905e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.23.0](#1-23-0) * [1.22.3](#1-22-3) * [1.22.2](#1-22-2) * [1.22.1](#1-22-1) @@ -63,7 +63,8 @@ * [1.2.0](#1-2-0) -## Unreleased +## 1.23.0 + ### Added * `colors2` config section. This section duplicates the `colors` @@ -143,9 +144,13 @@ [2105]: https://codeberg.org/dnkl/foot/issues/2105 -### Security ### Contributors +* Chen Mulong +* Kirill Primak +* Ryan Roden-Corrent +* tokyo4j + ## 1.22.3 From d62bff1440472f33b5350b3146f691ce906a3588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 16 Jul 2025 08:15:34 +0200 Subject: [PATCH 1218/1323] meson: bump to 1.23.0 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 7b9490d9..b9994c11 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.22.3', + version: '1.23.0', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From 692b22cbbb24efa91cb24446d189bc8c2535c1ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 16 Jul 2025 08:31:42 +0200 Subject: [PATCH 1219/1323] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8985905e..6121955f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.23.0](#1-23-0) * [1.22.3](#1-22-3) * [1.22.2](#1-22-2) @@ -63,6 +64,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.23.0 ### Added From cc290fa9b0c36e90f6af978f839c083d05f22a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 17 Jul 2025 10:40:20 +0200 Subject: [PATCH 1220/1323] url-mode: assign label keys in reverse order The _last_ URL is often the one you are interested in, and with this change, it is always assigned the first (and thus the same) key. Closes #2140 --- CHANGELOG.md | 8 ++++++++ url-mode.c | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6121955f..df52e27d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,14 @@ ## Unreleased ### Added ### Changed + +* URL labels are now assigned in reverse order, from bottom to + top. This ensures the **last** URL (which is often the one you are + interested in) is always assigned the same key ([#2140][2140]). + +[2140]: https://codeberg.org/dnkl/foot/issues/2140 + + ### Deprecated ### Removed ### Fixed diff --git a/url-mode.c b/url-mode.c index 35843219..c25396cd 100644 --- a/url-mode.c +++ b/url-mode.c @@ -634,12 +634,12 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) size_t combo_idx = 0; - tll_foreach(*urls, it) { + tll_rforeach(*urls, it) { bool id_already_seen = false; /* Look for already processed URLs where both the URI and the * ID matches */ - tll_foreach(*urls, it2) { + tll_rforeach(*urls, it2) { if (&it->item == &it2->item) break; @@ -659,7 +659,7 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) * them; if so, reuse the *same* key combo. */ bool url_already_seen = false; - tll_foreach(*urls, it2) { + tll_rforeach(*urls, it2) { if (&it->item == &it2->item) break; @@ -679,7 +679,7 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) free(combos[i]); #if defined(_DEBUG) && LOG_ENABLE_DBG - tll_foreach(*urls, it) { + tll_rforeach(*urls, it) { if (it->item.key == NULL) continue; From 01387f9593294f4eb467a162f245e3c712635f35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 17 Jul 2025 10:18:17 +0200 Subject: [PATCH 1221/1323] main: SIGUSR1 selects the first color theme, SIGUSR2 the second Before this patch, SIGUSR1 toggled between [colors] and [colors2]. Now, SIGUSR1 changes to [colors], regardless of what the current color theme is, and SIGUSR2 changes to [colors2]. Closes #2144 --- CHANGELOG.md | 4 ++++ main.c | 22 +++++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df52e27d..af7fe73a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,8 +71,12 @@ * URL labels are now assigned in reverse order, from bottom to top. This ensures the **last** URL (which is often the one you are interested in) is always assigned the same key ([#2140][2140]). +* Sending `SIGUSR1` no longer **toggles** between `[colors]` and + `[colors2]`, but explicitly changes to `[colors]`. `SIGUSR2` changes + to `[colors2]` ([#2144][2144]). [2140]: https://codeberg.org/dnkl/foot/issues/2140 +[2144]: https://codeberg.org/dnkl/foot/issues/2144 ### Deprecated diff --git a/main.c b/main.c index 37bbb6a7..517d8460 100644 --- a/main.c +++ b/main.c @@ -46,13 +46,22 @@ fdm_sigint(struct fdm *fdm, int signo, void *data) } static bool -fdm_sigusr1(struct fdm *fdm, int signo, void *data) +fdm_sigusr(struct fdm *fdm, int signo, void *data) { struct wayland *wayl = data; - tll_foreach(wayl->terms, it) { - struct terminal *term = it->item; - term_theme_toggle(term); + xassert(signo == SIGUSR1 || signo == SIGUSR2); + + if (signo == SIGUSR1) { + tll_foreach(wayl->terms, it) { + struct terminal *term = it->item; + term_theme_switch_to_1(term); + } + } else { + tll_foreach(wayl->terms, it) { + struct terminal *term = it->item; + term_theme_switch_to_2(term); + } } return true; @@ -621,8 +630,11 @@ main(int argc, char *const *argv) goto out; } - if (!fdm_signal_add(fdm, SIGUSR1, &fdm_sigusr1, wayl)) + if (!fdm_signal_add(fdm, SIGUSR1, &fdm_sigusr, wayl) || + !fdm_signal_add(fdm, SIGUSR2, &fdm_sigusr, wayl)) + { goto out; + } struct sigaction sig_ign = {.sa_handler = SIG_IGN}; sigemptyset(&sig_ign.sa_mask); From 57ae3bb89c6bf50e70e831e3658e59a6877e9817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 18 Jul 2025 17:24:18 +0200 Subject: [PATCH 1222/1323] main: unregister SIGUSR2 on exit --- main.c | 1 + 1 file changed, 1 insertion(+) diff --git a/main.c b/main.c index 517d8460..f97f21b5 100644 --- a/main.c +++ b/main.c @@ -672,6 +672,7 @@ out: key_binding_manager_destroy(key_binding_manager); reaper_destroy(reaper); fdm_signal_del(fdm, SIGUSR1); + fdm_signal_del(fdm, SIGUSR2); fdm_signal_del(fdm, SIGTERM); fdm_signal_del(fdm, SIGINT); fdm_destroy(fdm); From 7ab43ebf7464ab72e2041de0a1112feaa81ea89e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 21 Jul 2025 13:49:57 +0200 Subject: [PATCH 1223/1323] shm: don't set pixman_fmt_without_alpha twice When selecting 16-bit surfaces, we set pixman_fmt_without_alpha twice, and never set pixman_fmt_with_alpha. This caused 10-bit surfaces to be used instead, since it checks if pixman_fmt_with_alpha has been overridden or not. --- CHANGELOG.md | 4 ++++ shm.c | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af7fe73a..cfbbde53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,10 @@ ### Deprecated ### Removed ### Fixed + +* 10-bit surfaces sometimes used instead of 16-bit. + + ### Security ### Contributors diff --git a/shm.c b/shm.c index b586b504..4680c12c 100644 --- a/shm.c +++ b/shm.c @@ -994,7 +994,7 @@ shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, pixman_fmt_without_alpha = PIXMAN_a16b16g16r16; shm_fmt_without_alpha = WL_SHM_FORMAT_XBGR16161616; - pixman_fmt_without_alpha = PIXMAN_a16b16g16r16; + pixman_fmt_with_alpha = PIXMAN_a16b16g16r16; shm_fmt_with_alpha = WL_SHM_FORMAT_ABGR16161616; if (!have_logged) { From 21db6a6cdc68a5a8b9561d07d3ef69e11d2a077e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 21 Jul 2025 15:28:52 +0200 Subject: [PATCH 1224/1323] fdm: when logging signal related errors, include the signal name Since sigabbrev_np() is GNU only, provide a fallback function that returns "SIG<signo>" when sigabbrev_np() doesn't exist (for example, on FreeBSD). --- fdm.c | 30 ++++++++++++++++++++++++------ meson.build | 6 ++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/fdm.c b/fdm.c index ea30443b..4822cd97 100644 --- a/fdm.c +++ b/fdm.c @@ -18,6 +18,18 @@ #include "debug.h" #include "xmalloc.h" +#if !defined(SIGABBREV_NP) +#include <stdio.h> + +static const char * +sigabbrev_np(int sig) +{ + static char buf[16]; + snprintf(buf, sizeof(buf), "<%d>", sig); + return buf; +} +#endif + struct fd_handler { int fd; int events; @@ -113,7 +125,8 @@ fdm_destroy(struct fdm *fdm) for (int i = 0; i < SIGRTMAX; i++) { if (fdm->signal_handlers[i].callback != NULL) - LOG_WARN("handler for signal %d not removed", i); + LOG_WARN("handler for signal %d (SIG%s) not removed", + i, sigabbrev_np(i)); } if (tll_length(fdm->hooks_low) > 0 || @@ -338,7 +351,8 @@ bool fdm_signal_add(struct fdm *fdm, int signo, fdm_signal_handler_t handler, void *data) { if (fdm->signal_handlers[signo].callback != NULL) { - LOG_ERR("signal %d already has a handler", signo); + LOG_ERR("signal %d (SIG%s) already has a handler", + signo, sigabbrev_np(signo)); return false; } @@ -347,14 +361,16 @@ fdm_signal_add(struct fdm *fdm, int signo, fdm_signal_handler_t handler, void *d sigaddset(&mask, signo); if (sigprocmask(SIG_BLOCK, &mask, &original) < 0) { - LOG_ERRNO("failed to block signal %d", signo); + LOG_ERRNO("failed to block signal %d (SIG%s)", + signo, sigabbrev_np(signo)); return false; } struct sigaction action = {.sa_handler = &signal_handler}; sigemptyset(&action.sa_mask); if (sigaction(signo, &action, NULL) < 0) { - LOG_ERRNO("failed to set signal handler for signal %d", signo); + LOG_ERRNO("failed to set signal handler for signal %d (SIG%s)", + signo, sigabbrev_np(signo)); sigprocmask(SIG_SETMASK, &original, NULL); return false; } @@ -374,7 +390,8 @@ fdm_signal_del(struct fdm *fdm, int signo) struct sigaction action = {.sa_handler = SIG_DFL}; sigemptyset(&action.sa_mask); if (sigaction(signo, &action, NULL) < 0) { - LOG_ERRNO("failed to restore signal handler for signal %d", signo); + LOG_ERRNO("failed to restore signal handler for signal %d (SIG%s)", + signo, sigabbrev_np(signo)); return false; } @@ -386,7 +403,8 @@ fdm_signal_del(struct fdm *fdm, int signo) sigemptyset(&mask); sigaddset(&mask, signo); if (sigprocmask(SIG_UNBLOCK, &mask, NULL) < 0) { - LOG_ERRNO("failed to unblock signal %d", signo); + LOG_ERRNO("failed to unblock signal %d (SIG%s)", + signo, sigabbrev_np(signo)); return false; } diff --git a/meson.build b/meson.build index b9994c11..8a072dcf 100644 --- a/meson.build +++ b/meson.build @@ -25,6 +25,12 @@ if cc.has_function('execvpe', add_project_arguments('-DEXECVPE', language: 'c') endif +if cc.has_function('sigabbrev_np', + args: ['-D_GNU_SOURCE'], + prefix: '#include <string.h>') + add_project_arguments('-DSIGABBREV_NP', language: 'c') +endif + utmp_backend = get_option('utmp-backend') if utmp_backend == 'auto' host_os = host_machine.system() From 42be74214a0b2cd1b4245b262c99cbd5030a0e0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 22 Jul 2025 13:30:00 +0200 Subject: [PATCH 1225/1323] term: make sure the color table is populated *before* the slave process is spawned --- terminal.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/terminal.c b/terminal.c index 135f21ce..2e23f749 100644 --- a/terminal.c +++ b/terminal.c @@ -1409,6 +1409,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, pixman_region32_init(&term->render.last_overlay_clip); term_update_ascii_printer(term); + memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); for (size_t i = 0; i < 4; i++) { const struct config_font_list *font_list = &conf->fonts[i]; @@ -1443,8 +1444,6 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, xassert(tll_length(term->wl->monitors) > 0); term->scale = tll_front(term->wl->monitors).scale; - memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); - /* Initialize the Wayland window backend */ if ((term->window = wayl_win_init(term, token)) == NULL) goto err; From fcde74a18150a1722f2be2b7990c2f9b45268854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 22 Jul 2025 13:30:28 +0200 Subject: [PATCH 1226/1323] osc: color reset: read default color from currently active theme --- CHANGELOG.md | 2 ++ osc.c | 69 ++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfbbde53..96040e86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,8 @@ ### Fixed * 10-bit surfaces sometimes used instead of 16-bit. +* OSC-104/110/111/112/117/119 (reset colors) not taking the currently + active theme into account. ### Security diff --git a/osc.c b/osc.c index 834029c1..0b492564 100644 --- a/osc.c +++ b/osc.c @@ -1515,10 +1515,14 @@ osc_dispatch(struct terminal *term) case 104: { /* Reset Color Number 'c' (whole table if no parameter) */ + const struct color_theme *theme = + term->colors.active_theme == COLOR_THEME1 + ? &term->conf->colors + : &term->conf->colors2; + if (string[0] == '\0') { LOG_DBG("resetting all colors"); - for (size_t i = 0; i < ALEN(term->colors.table); i++) - term->colors.table[i] = term->conf->colors.table[i]; + memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); term_damage_view(term); } @@ -1540,7 +1544,7 @@ osc_dispatch(struct terminal *term) } LOG_DBG("resetting color #%u", idx); - term->colors.table[idx] = term->conf->colors.table[idx]; + term->colors.table[idx] = theme->table[idx]; term_damage_color(term, COLOR_BASE256, idx); } @@ -1553,16 +1557,28 @@ osc_dispatch(struct terminal *term) case 110: /* Reset default text foreground color */ LOG_DBG("resetting foreground color"); - term->colors.fg = term->conf->colors.fg; + + const struct color_theme *theme = + term->colors.active_theme == COLOR_THEME1 + ? &term->conf->colors + : &term->conf->colors2; + + term->colors.fg = theme->fg; term_damage_color(term, COLOR_DEFAULT, 0); break; case 111: { /* Reset default text background color */ LOG_DBG("resetting background color"); - bool alpha_changed = term->colors.alpha != term->conf->colors.alpha; - term->colors.bg = term->conf->colors.bg; - term->colors.alpha = term->conf->colors.alpha; + const struct color_theme *theme = + term->colors.active_theme == COLOR_THEME1 + ? &term->conf->colors + : &term->conf->colors2; + + bool alpha_changed = term->colors.alpha != theme->alpha; + + term->colors.bg = theme->bg; + term->colors.alpha = theme->alpha; if (alpha_changed) { wayl_win_alpha_changed(term->window); @@ -1574,10 +1590,16 @@ osc_dispatch(struct terminal *term) break; } - case 112: + case 112: { LOG_DBG("resetting cursor color"); - term->colors.cursor_fg = term->conf->colors.cursor.text; - term->colors.cursor_bg = term->conf->colors.cursor.cursor; + + const struct color_theme *theme = + term->colors.active_theme == COLOR_THEME1 + ? &term->conf->colors + : &term->conf->colors2; + + term->colors.cursor_fg = theme->cursor.text; + term->colors.cursor_bg = theme->cursor.cursor; if (term->conf->colors.use_custom.cursor) { term->colors.cursor_fg |= 1u << 31; @@ -1586,16 +1608,31 @@ osc_dispatch(struct terminal *term) term_damage_cursor(term); break; + } - case 117: + case 117: { LOG_DBG("resetting selection background color"); - term->colors.selection_bg = term->conf->colors.selection_bg; - break; - case 119: - LOG_DBG("resetting selection foreground color"); - term->colors.selection_fg = term->conf->colors.selection_fg; + const struct color_theme *theme = + term->colors.active_theme == COLOR_THEME1 + ? &term->conf->colors + : &term->conf->colors2; + + term->colors.selection_bg = theme->selection_bg; break; + } + + case 119: { + LOG_DBG("resetting selection foreground color"); + + const struct color_theme *theme = + term->colors.active_theme == COLOR_THEME1 + ? &term->conf->colors + : &term->conf->colors2; + + term->colors.selection_fg = theme->selection_fg; + break; + } case 133: /* From 95e8b18c125c1ad2835ae59204dd0f8b5feaf64b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 23 Jul 2025 08:27:59 +0200 Subject: [PATCH 1227/1323] changelog: prepare for 1.23.1 --- CHANGELOG.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96040e86..6d720f0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.23.1](#1-23-1) * [1.23.0](#1-23-0) * [1.22.3](#1-22-3) * [1.22.2](#1-22-2) @@ -64,8 +64,8 @@ * [1.2.0](#1-2-0) -## Unreleased -### Added +## 1.23.1 + ### Changed * URL labels are now assigned in reverse order, from bottom to @@ -79,8 +79,6 @@ [2144]: https://codeberg.org/dnkl/foot/issues/2144 -### Deprecated -### Removed ### Fixed * 10-bit surfaces sometimes used instead of 16-bit. @@ -88,10 +86,6 @@ active theme into account. -### Security -### Contributors - - ## 1.23.0 ### Added From 43620935a169c03ab5744d12f5917269ba92567e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 23 Jul 2025 08:28:13 +0200 Subject: [PATCH 1228/1323] meson: bump version to 1.23.1 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 8a072dcf..0b7bbc17 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.23.0', + version: '1.23.1', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From 86d63f08ba118f0051b6941353d0a888f4987d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 23 Jul 2025 08:31:30 +0200 Subject: [PATCH 1229/1323] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d720f0a..c628155c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.23.1](#1-23-1) * [1.23.0](#1-23-0) * [1.22.3](#1-22-3) @@ -64,6 +65,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.23.1 ### Changed From f873aa904db36ed9477deaba5ee2f4906c4d7831 Mon Sep 17 00:00:00 2001 From: Tobias Mock <mail@tmock.de> Date: Mon, 21 Jul 2025 23:28:02 +0200 Subject: [PATCH 1230/1323] Add tinted variant of modus-vivendi theme --- themes/modus-vivendi-tinted | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 themes/modus-vivendi-tinted diff --git a/themes/modus-vivendi-tinted b/themes/modus-vivendi-tinted new file mode 100644 index 00000000..67cf02a0 --- /dev/null +++ b/themes/modus-vivendi-tinted @@ -0,0 +1,25 @@ +# -*- conf -*- +# +# modus-vivendi-tinted +# See: https://protesilaos.com/emacs/modus-themes +# + +[colors] +background=0d0e1c +foreground=ffffff +regular0=000000 +regular1=ff5f59 +regular2=44bc44 +regular3=d0bc00 +regular4=2fafff +regular5=feacd0 +regular6=00d3d0 +regular7=a6a6a6 +bright0=595959 +bright1=ff6b55 +bright2=ff6b55 +bright3=fec43f +bright4=fec43f +bright5=b6a0ff +bright6=6ae4b9 +bright7=777777 From 83303bd2a461b5917de51d8740999cbdb46e7986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 29 Jul 2025 11:18:49 +0200 Subject: [PATCH 1231/1323] url-mode: for some reason we sorted the label letters before assigning them Don't do this. Now that we **don't** sort them, the first letter chosen by the user is always assigned to the bottom most URL. Closes #2140 (again) --- CHANGELOG.md | 5 +++++ url-mode.c | 12 ------------ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c628155c..e405459e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,11 @@ ## Unreleased ### Added ### Changed + +* The label letters are no longer sorted before being assigned to URLs + ([#2140]2140[]). + + ### Deprecated ### Removed ### Fixed diff --git a/url-mode.c b/url-mode.c index c25396cd..19d95356 100644 --- a/url-mode.c +++ b/url-mode.c @@ -557,14 +557,6 @@ urls_collect(const struct terminal *term, enum url_action action, remove_overlapping(urls, term->grid->num_cols); } -static int -c32cmp_qsort_wrapper(const void *_a, const void *_b) -{ - const char32_t *a = *(const char32_t **)_a; - const char32_t *b = *(const char32_t **)_b; - return c32cmp(a, b); -} - static void generate_key_combos(const struct config *conf, size_t count, char32_t *combos[static count]) @@ -607,10 +599,6 @@ generate_key_combos(const struct config *conf, } free(hints); - /* Sorting is a kind of shuffle, since we're sorting on the - * *reversed* strings */ - qsort(combos, count, sizeof(char32_t *), &c32cmp_qsort_wrapper); - /* Reverse all strings */ for (size_t i = 0; i < count; i++) { const size_t len = c32len(combos[i]); From 7636f264a818eb11b155be051d012a616984a5a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 26 Jul 2025 12:21:51 +0200 Subject: [PATCH 1232/1323] slave: remove more environment variables set by other terminals This ensures applications don't mistake foot for another terminal emulator. Not that applications _should_ rely on environment variables, but some do anyway... --- slave.c | 50 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/slave.c b/slave.c index 47e59e87..62899372 100644 --- a/slave.c +++ b/slave.c @@ -436,8 +436,54 @@ slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, add_to_env(&custom_env, "COLORTERM", "truecolor"); add_to_env(&custom_env, "PWD", cwd); - del_from_env(&custom_env, "TERM_PROGRAM"); - del_from_env(&custom_env, "TERM_PROGRAM_VERSION"); + del_from_env(&custom_env, "TERM_PROGRAM"); /* Wezterm, Ghostty */ + del_from_env(&custom_env, "TERM_PROGRAM_VERSION"); /* Wezterm, Ghostty */ + del_from_env(&custom_env, "TERMINAL_NAME"); /* Contour */ + del_from_env(&custom_env, "TERMINAL_VERSION_STRING"); /* Contour */ + del_from_env(&custom_env, "TERMINAL_VERSION_TRIPLE"); /* Contour */ + + /* XTerm specific */ + del_from_env(&custom_env, "XTERM_SHELL"); + del_from_env(&custom_env, "XTERM_VERSION"); + del_from_env(&custom_env, "XTERM_LOCALE"); + + /* Mlterm specific */ + del_from_env(&custom_env, "MLTERM"); + + /* Zutty specific */ + del_from_env(&custom_env, "ZUTTY_VERSION"); + + /* Ghostty specific */ + del_from_env(&custom_env, "GHOSTTY_BIN_DIR"); + del_from_env(&custom_env, "GHOSTTY_SHELL_INTEGRATION_NO_SUDO"); + del_from_env(&custom_env, "GHOSTTY_RESOURCES_DIR"); + + /* Kitty specific */ + del_from_env(&custom_env, "KITTY_WINDOW_ID"); + del_from_env(&custom_env, "KITTY_PID"); + del_from_env(&custom_env, "KITTY_PUBLIC_KEY"); + del_from_env(&custom_env, "KITTY_INSTALLATION_DIR"); + + /* Contour specific */ + del_from_env(&custom_env, "CONTOUR_PROFILE"); + + /* Wezterm specific */ + del_from_env(&custom_env, "WEZTERM_PANE"); + del_from_env(&custom_env, "WEZTERM_EXECUTABLE"); + del_from_env(&custom_env, "WEZTERM_CONFIG_FILE"); + del_from_env(&custom_env, "WEZTERM_EXECUTABLE_DIR"); + del_from_env(&custom_env, "WEZTERM_UNIX_SOCKET"); + del_from_env(&custom_env, "WEZTERM_CONFIG_DIR"); + + /* Alacritty specific */ + del_from_env(&custom_env, "ALACRITTY_LOG"); + del_from_env(&custom_env, "ALACRITTY_WINDOW_ID"); + del_from_env(&custom_env, "ALACRITTY_SOCKET"); + + /* VTE, gnome-terminal, kgx etc */ + del_from_env(&custom_env, "VTE_VERSION"); + del_from_env(&custom_env, "GNOME_TERMINAL_SERVICE"); + del_from_env(&custom_env, "GNOME_TERMINAL_SCREEN"); #if defined(FOOT_TERMINFO_PATH) add_to_env(&custom_env, "TERMINFO", FOOT_TERMINFO_PATH); From 6eedc88d70520456fbb89db2600e762e722fe313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 30 Jul 2025 12:23:39 +0200 Subject: [PATCH 1233/1323] server: sigusr1/2: update conf object with the "new" theme When sending SIGUSR1/SIGUSR2 to a server process, all currently running client instances change their theme. But before this patch, all future instances used the original theme. With this patch, the server owned config object is updated with the selected theme, thus making new instances use the same theme as well. --- main.c | 36 +++++++++++++++++++++++------------- server.c | 24 ++++++++++++++++++++++-- server.h | 5 ++++- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/main.c b/main.c index f97f21b5..1afbd16d 100644 --- a/main.c +++ b/main.c @@ -45,23 +45,28 @@ fdm_sigint(struct fdm *fdm, int signo, void *data) return true; } +struct sigusr_context { + struct terminal *term; + struct server *server; +}; + static bool fdm_sigusr(struct fdm *fdm, int signo, void *data) { - struct wayland *wayl = data; - xassert(signo == SIGUSR1 || signo == SIGUSR2); - if (signo == SIGUSR1) { - tll_foreach(wayl->terms, it) { - struct terminal *term = it->item; - term_theme_switch_to_1(term); - } + struct sigusr_context *ctx = data; + + if (ctx->server != NULL) { + if (signo == SIGUSR1) + server_global_theme_switch_to_1(ctx->server); + else + server_global_theme_switch_to_2(ctx->server); } else { - tll_foreach(wayl->terms, it) { - struct terminal *term = it->item; - term_theme_switch_to_2(term); - } + if (signo == SIGUSR1) + term_theme_switch_to_1(ctx->term); + else + term_theme_switch_to_2(ctx->term); } return true; @@ -630,8 +635,13 @@ main(int argc, char *const *argv) goto out; } - if (!fdm_signal_add(fdm, SIGUSR1, &fdm_sigusr, wayl) || - !fdm_signal_add(fdm, SIGUSR2, &fdm_sigusr, wayl)) + struct sigusr_context sigusr_context = { + .term = term, + .server = server, + }; + + if (!fdm_signal_add(fdm, SIGUSR1, &fdm_sigusr, &sigusr_context) || + !fdm_signal_add(fdm, SIGUSR2, &fdm_sigusr, &sigusr_context)) { goto out; } diff --git a/server.c b/server.c index 22dd473b..97c1915d 100644 --- a/server.c +++ b/server.c @@ -30,7 +30,7 @@ struct client; struct terminal_instance; struct server { - const struct config *conf; + struct config *conf; struct fdm *fdm; struct reaper *reaper; struct wayland *wayl; @@ -505,7 +505,7 @@ prepare_socket(int fd) } struct server * -server_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, +server_init(struct config *conf, struct fdm *fdm, struct reaper *reaper, struct wayland *wayl) { int fd; @@ -617,3 +617,23 @@ server_destroy(struct server *server) unlink(server->sock_path); free(server); } + +void +server_global_theme_switch_to_1(struct server *server) +{ + server->conf->initial_color_theme = COLOR_THEME1; + tll_foreach(server->clients, it) + term_theme_switch_to_1(it->item->instance->terminal); + tll_foreach(server->terminals, it) + term_theme_switch_to_1(it->item->terminal); +} + +void +server_global_theme_switch_to_2(struct server *server) +{ + server->conf->initial_color_theme = COLOR_THEME2; + tll_foreach(server->clients, it) + term_theme_switch_to_2(it->item->instance->terminal); + tll_foreach(server->terminals, it) + term_theme_switch_to_2(it->item->terminal); +} diff --git a/server.h b/server.h index 50797540..6adfe7c6 100644 --- a/server.h +++ b/server.h @@ -6,6 +6,9 @@ #include "wayland.h" struct server; -struct server *server_init(const struct config *conf, struct fdm *fdm, +struct server *server_init(struct config *conf, struct fdm *fdm, struct reaper *reaper, struct wayland *wayl); void server_destroy(struct server *server); + +void server_global_theme_switch_to_1(struct server *server); +void server_global_theme_switch_to_2(struct server *server); From 3b8d59f4760070801e197aea1c7d6dc6392aafdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 30 Jul 2025 12:25:13 +0200 Subject: [PATCH 1234/1323] doc: foot: document SIGUSR1/SIGUSR2 --- doc/foot.1.scd | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/doc/foot.1.scd b/doc/foot.1.scd index f868c12c..2aef83ca 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -689,6 +689,21 @@ variables may be defined in *foot.ini*(5). In addition to the variables listed above, custom environment variables to unset may be defined in *foot.ini*(5). +# Signals + +The following signals have special meaning in foot: + +- SIGUSR1: switch to color theme 1 (i.e. use the *[colors]* section). +- SIGUSR2: switch to color theme 2 (i.e. use the *[colors2]* section)- + +Note: you can send SIGUSR1/SIGUSR2 to a *foot --server* process too, +in which case all client instances will switch theme. Furthermore, all +future client instances will also use the selected theme. + +Sending SIGUSR1/SIGUSR2 to a footclient instance is currently not +supported. + + # BUGS Please report bugs to https://codeberg.org/dnkl/foot/issues From b1b2162416cc0633b6432b0af48e79de6a5540d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 30 Jul 2025 12:25:21 +0200 Subject: [PATCH 1235/1323] doc: foot.ini: mention SIGUSR1/SIGUSR2 and reference foot(1) --- doc/foot.ini.5.scd | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 74b3f35b..7a7fb781 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -371,7 +371,8 @@ empty string to be set, but it must be quoted: *KEY=""*) Use the *color-theme-switch-1*, *color-theme-switch-2* and *color-theme-toggle* key bindings to switch between the two themes - at runtime. + at runtime, or send SIGUSR1/SIGUSR2 to the foot process (see + *foot*(1) for details). Default: _1_ @@ -1446,6 +1447,9 @@ e.g. *search-start=none*. *color-theme-toggle* toggles between the primary and alternative color themes. + Note: you can also send SIGUSR1/SIGUSR2 to the foot process to + change the theme (see *foot*(1) for details.) + Default: _none_ *quit* From 70d99a80513f574ac6d74989282b3481572513ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 30 Jul 2025 12:38:14 +0200 Subject: [PATCH 1236/1323] changelog: SIGUSR changes in the server --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e405459e..dfd2b9db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,9 @@ * The label letters are no longer sorted before being assigned to URLs ([#2140]2140[]). +* Sending SIGUSR1/SIGUSR2 to a `foot --server` process now causes + newly spawned client instances to use the selected theme, instead of + the original one. ### Deprecated From b13a8f12d2d360e0923ee30ff40f25b245576bb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 31 Jul 2025 17:37:19 +0200 Subject: [PATCH 1237/1323] server/client: add support for sending SIGUSR to footclient This patch adds the IPC infrastructure necessary to propagate SIGUSR1/SIGUSR2 from a footclient process to the server process. By targeting a particular footclient instance, only that particular instance changes theme. This is different from when targeting the server process, where all instances change theme. Closes #2156 --- CHANGELOG.md | 4 +++ client-protocol.h | 14 ++++++++++ client.c | 65 ++++++++++++++++++++++++++++++++++++++++---- doc/foot.1.scd | 6 ++-- doc/footclient.1.scd | 15 ++++++++++ server.c | 59 +++++++++++++++++++++++++++++++++++++--- 6 files changed, 151 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfd2b9db..dd5730a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,10 @@ * Sending SIGUSR1/SIGUSR2 to a `foot --server` process now causes newly spawned client instances to use the selected theme, instead of the original one. +* SIGUSR1/SIGUSR2 can now be sent to `footclient` processes, to change + the theme of that particular instance ([#2156][2156]). + +[2156]: https://codeberg.org/dnkl/foot/issues/2156 ### Deprecated diff --git a/client-protocol.h b/client-protocol.h index 505825f6..efd601d7 100644 --- a/client-protocol.h +++ b/client-protocol.h @@ -29,3 +29,17 @@ struct client_data { } __attribute__((packed)); _Static_assert(sizeof(struct client_data) == 10, "protocol struct size error"); + +enum client_ipc_code { + FOOT_IPC_SIGUSR, +}; + +struct client_ipc_hdr { + enum client_ipc_code ipc_code; + uint8_t size; +} __attribute__((packed)); + + +struct client_ipc_sigusr { + int signo; +} __attribute__((packed)); diff --git a/client.c b/client.c index e76f2d51..aa5302be 100644 --- a/client.c +++ b/client.c @@ -33,13 +33,20 @@ struct string { typedef tll(struct string) string_list_t; static volatile sig_atomic_t aborted = 0; +static volatile sig_atomic_t sigusr = 0; static void -sig_handler(int signo) +sigint_handler(int signo) { aborted = 1; } +static void +sigusr_handler(int signo) +{ + sigusr = signo; +} + static ssize_t sendall(int sock, const void *_buf, size_t len) { @@ -507,15 +514,63 @@ main(int argc, char *const *argv) if (!send_string_list(fd, &envp)) goto err; - struct sigaction sa = {.sa_handler = &sig_handler}; - sigemptyset(&sa.sa_mask); - if (sigaction(SIGINT, &sa, NULL) < 0 || sigaction(SIGTERM, &sa, NULL) < 0) { + struct sigaction sa_int = {.sa_handler = &sigint_handler}; + struct sigaction sa_usr = {.sa_handler = &sigusr_handler}; + sigemptyset(&sa_int.sa_mask); + sigemptyset(&sa_usr.sa_mask); + + if (sigaction(SIGINT, &sa_int, NULL) < 0 || + sigaction(SIGTERM, &sa_int, NULL) < 0 || + sigaction(SIGUSR1, &sa_usr, NULL) < 0 || + sigaction(SIGUSR2, &sa_usr, NULL) < 0) + { LOG_ERRNO("failed to register signal handlers"); goto err; } int exit_code; - ssize_t rcvd = recv(fd, &exit_code, sizeof(exit_code), 0); + ssize_t rcvd = -1; + + while (true) { + rcvd = recv(fd, &exit_code, sizeof(exit_code), 0); + + const int got_sigusr = sigusr; + sigusr = 0; + + if (rcvd < 0 && errno == EINTR) { + if (aborted) + break; + else if (got_sigusr != 0) { + LOG_DBG("sending sigusr %d to server", got_sigusr); + + struct { + struct client_ipc_hdr hdr; + struct client_ipc_sigusr sigusr; + } ipc = { + .hdr = { + .ipc_code = FOOT_IPC_SIGUSR, + .size = sizeof(struct client_ipc_sigusr), + }, + .sigusr = { + .signo = got_sigusr, + }, + }; + + ssize_t count = send(fd, &ipc, sizeof(ipc), 0); + if (count < 0) { + LOG_ERRNO("failed to send SIGUSR IPC to server"); + goto err; + } else if ((size_t)count != sizeof(ipc)) { + LOG_ERR("failed to send SIGUSR IPC to server"); + goto err; + } + } + + continue; + } + + break; + } if (rcvd == -1 && errno == EINTR) xassert(aborted); diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 2aef83ca..8d968a6e 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -694,14 +694,14 @@ variables to unset may be defined in *foot.ini*(5). The following signals have special meaning in foot: - SIGUSR1: switch to color theme 1 (i.e. use the *[colors]* section). -- SIGUSR2: switch to color theme 2 (i.e. use the *[colors2]* section)- +- SIGUSR2: switch to color theme 2 (i.e. use the *[colors2]* section). Note: you can send SIGUSR1/SIGUSR2 to a *foot --server* process too, in which case all client instances will switch theme. Furthermore, all future client instances will also use the selected theme. -Sending SIGUSR1/SIGUSR2 to a footclient instance is currently not -supported. +You can also send SIGUSR1/SIGUSR2 to a footclient instance, see +*footclient*(1) for details. # BUGS diff --git a/doc/footclient.1.scd b/doc/footclient.1.scd index 365689af..e4f6d350 100644 --- a/doc/footclient.1.scd +++ b/doc/footclient.1.scd @@ -189,6 +189,21 @@ variables may be defined in *foot.ini*(5). In addition to the variables listed above, custom environment variables to unset may be defined in *foot.ini*(5). +# Signals + +The following signals have special meaning in footclient: + +- SIGUSR1: switch to color theme 1 (i.e. use the *[colors]* section). +- SIGUSR2: switch to color theme 2 (i.e. use the *[colors2]* section). + +When sending SIGUSR1/SIGUSR2 to a footclient instance, the theme is +changed in that instance only. This is different from when you send +SIGUSR1/SIGUSR2 to the server process, where all instances change the +theme. + +Note: for obvious reasons, this is not supported when footclient is +started with *--no-wait*. + # SEE ALSO *foot*(1) diff --git a/server.c b/server.c index 97c1915d..3d4b5725 100644 --- a/server.c +++ b/server.c @@ -156,10 +156,61 @@ fdm_client(struct fdm *fdm, int fd, int events, void *data) xassert(events & EPOLLIN); if (client->instance != NULL) { - uint8_t dummy[128]; - ssize_t count = read(fd, dummy, sizeof(dummy)); - LOG_WARN("client unexpectedly sent %zd bytes", count); - return true; /* TODO: shutdown instead? */ + struct client_ipc_hdr ipc_hdr; + ssize_t count = read(fd, &ipc_hdr, sizeof(ipc_hdr)); + + if (count != sizeof(ipc_hdr)) { + LOG_WARN("client unexpectedly sent %zd bytes", count); + return true; /* TODO: shutdown instead? */ + } + + switch (ipc_hdr.ipc_code) { + case FOOT_IPC_SIGUSR: { + xassert(ipc_hdr.size == sizeof(struct client_ipc_sigusr)); + + struct client_ipc_sigusr sigusr; + count = read(fd, &sigusr, sizeof(sigusr)); + if (count < 0) { + LOG_ERRNO("failed to read SIGUSR IPC data from client"); + return true; /* TODO: shutdown instead? */ + } + + if ((size_t)count != sizeof(sigusr)) { + LOG_ERR("failed to read SIGUSR IPC data from client"); + return true; /* TODO: shutdown instead? */ + } + + switch (sigusr.signo) { + case SIGUSR1: + term_theme_switch_to_1(client->instance->terminal); + break; + + case SIGUSR2: + term_theme_switch_to_2(client->instance->terminal); + break; + + default: + LOG_ERR( + "client sent bad SIGUSR number: %d " + "(expected SIGUSR1=%d or SIGUSR2=%d)", + sigusr.signo, SIGUSR1, SIGUSR2); + break; + } + + return true; + } + + default: + LOG_WARN( + "client sent unrecognized IPC (0x%04x), ignoring %hhu bytes", + ipc_hdr.ipc_code, ipc_hdr.size); + + /* TODO: slightly broken, since not all data is guaranteed + to be readable yet */ + uint8_t dummy[ipc_hdr.size]; + read(fd, dummy, ipc_hdr.size); + return true; + } } if (client->buffer.data == NULL) { From 72d9a13c0c6b6ee4b56a38f508c2e8d5c56616b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 1 Aug 2025 09:41:37 +0200 Subject: [PATCH 1238/1323] server: fix compilation error: return value ignored --- server.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.c b/server.c index 3d4b5725..6b3e5094 100644 --- a/server.c +++ b/server.c @@ -208,7 +208,7 @@ fdm_client(struct fdm *fdm, int fd, int events, void *data) /* TODO: slightly broken, since not all data is guaranteed to be readable yet */ uint8_t dummy[ipc_hdr.size]; - read(fd, dummy, ipc_hdr.size); + (void)!!read(fd, dummy, ipc_hdr.size); return true; } } From ed7652db5056c3658afbc11c04e164e77da18650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 25 Aug 2025 14:26:44 +0200 Subject: [PATCH 1239/1323] config: value_to_*(): don't overwrite result variable on error Some of the value_to_*() functions wrote directly to the output variable, even when the value was invalid. This often resulted in the an actual configuration option (i.e. a member in the config struct) to be overwritten by an invalid value. For example, -o initial-color-theme=0 would set conf->initial_color_theme to -1, resulting in a crash later, when initializing a terminal instance. --- CHANGELOG.md | 7 ++++++- config.c | 20 ++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd5730a1..2a2e3347 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,7 +70,7 @@ ### Changed * The label letters are no longer sorted before being assigned to URLs - ([#2140]2140[]). + ([#2140][2140]). * Sending SIGUSR1/SIGUSR2 to a `foot --server` process now causes newly spawned client instances to use the selected theme, instead of the original one. @@ -83,6 +83,11 @@ ### Deprecated ### Removed ### Fixed + +* Invalid configuration values overriding valid ones in surprising + ways. + + ### Security ### Contributors diff --git a/config.c b/config.c index 77dc3a73..1d539bcb 100644 --- a/config.c +++ b/config.c @@ -474,8 +474,12 @@ str_to_ulong(const char *s, int base, unsigned long *res) errno = 0; char *end = NULL; - *res = strtoul(s, &end, base); - return errno == 0 && *end == '\0'; + unsigned long v = strtoul(s, &end, base); + if (!(errno == 0 && *end == '\0')) + return false; + + *res = v; + return true; } static bool NOINLINE @@ -544,12 +548,13 @@ value_to_float(struct context *ctx, float *res) errno = 0; char *end = NULL; - *res = strtof(s, &end); + float v = strtof(s, &end); if (!(errno == 0 && *end == '\0')) { LOG_CONTEXTUAL_ERR("invalid decimal value"); return false; } + *res = v; return true; } @@ -641,7 +646,6 @@ value_to_enum(struct context *ctx, const char **value_map, int *res) valid_values[idx - 2] = '\0'; LOG_CONTEXTUAL_ERR("not one of %s", valid_values); - *res = -1; return false; } @@ -690,14 +694,18 @@ value_to_two_colors(struct context *ctx, goto out; } + uint32_t a, b; + ctx->value = first_as_str; - if (!value_to_color(ctx, first, allow_alpha)) + if (!value_to_color(ctx, &a, allow_alpha)) goto out; ctx->value = second_as_str; - if (!value_to_color(ctx, second, allow_alpha)) + if (!value_to_color(ctx, &b, allow_alpha)) goto out; + *first = a; + *second = b; ret = true; out: From f0e36e35cb65bdb92dbe24d00a9cc6bc8d72a458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 30 Aug 2025 08:18:31 +0200 Subject: [PATCH 1240/1323] input: unit test: check pipe2() return value Fixes compilation failures with clang, in release mode. --- input.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/input.c b/input.c index fe90a7f0..fe5a8001 100644 --- a/input.c +++ b/input.c @@ -1878,7 +1878,7 @@ keyboard_modifiers(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, UNITTEST { int chan[2]; - pipe2(chan, O_CLOEXEC); + xassert(pipe2(chan, O_CLOEXEC) == 0); xassert(chan[0] >= 0); xassert(chan[1] >= 0); From 298196365c06c13dd16a74e34e03d9de93670473 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent <git@rrc.codes> Date: Thu, 7 Aug 2025 08:18:38 -0400 Subject: [PATCH 1241/1323] config: add 'uppercase-regex-insert' This makes the "uppercase hint character inserts selected text" behavior added in #1975 configurable, as it can have unexpected behavior for some users. It defaults to "on", preserving the new behavior of `foot`, after Fixes #2159. --- CHANGELOG.md | 7 +++++++ config.c | 4 ++++ config.h | 1 + doc/foot.ini.5.scd | 7 +++++++ foot.ini | 2 ++ tests/test-config.c | 1 + url-mode.c | 3 ++- 7 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a2e3347..57144549 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,13 @@ ## Unreleased ### Added + +* The `uppercase-regex-insert` option controls whether an uppercase hint + character will insert the selected text into the prompt in `regex-copy` + or `show-urls-copy` mode. It defaults to `true`. ([#2159][2159]). + +[2159]: https://codeberg.org/dnkl/foot/issues/2159 + ### Changed * The label letters are no longer sorted before being assigned to URLs diff --git a/config.c b/config.c index 1d539bcb..0de1a1be 100644 --- a/config.c +++ b/config.c @@ -1119,6 +1119,9 @@ parse_section_main(struct context *ctx) (int *)&conf->initial_color_theme); } + else if (streq(key, "uppercase-regex-insert")) + return value_to_bool(ctx, &conf->uppercase_regex_insert); + else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; @@ -3383,6 +3386,7 @@ config_load(struct config *conf, const char *conf_path, .strikeout_thickness = {.pt = 0., .px = -1}, .dpi_aware = false, .gamma_correct = false, + .uppercase_regex_insert = true, .security = { .osc52 = OSC52_ENABLED, }, diff --git a/config.h b/config.h index 315f7e24..86fb2a8f 100644 --- a/config.h +++ b/config.h @@ -247,6 +247,7 @@ struct config { bool dpi_aware; bool gamma_correct; + bool uppercase_regex_insert; struct config_font_list fonts[4]; struct font_size_adjustment font_size_adjustment; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 7a7fb781..3a057d88 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -231,6 +231,13 @@ empty string to be set, but it must be quoted: *KEY=""*) Default: _no_. +*upppercase-regex-insert* + Boolean. When enabled, inputting an uppercase hint character in + *show-urls-copy* or *regex-copy* mode will insert the selected + text into the prompt in addition to copying it to the clipboard. + + Default: _yes_ + *box-drawings-uses-font-glyphs* Boolean. When disabled, foot generates box/line drawing characters itself. The are several advantages to doing this instead of using diff --git a/foot.ini b/foot.ini index 73fdb7ab..44ed5785 100644 --- a/foot.ini +++ b/foot.ini @@ -40,6 +40,8 @@ # utmp-helper=/usr/lib/utempter/utempter # When utmp backend is ‘libutempter’ (Linux) # utmp-helper=/usr/libexec/ulog-helper # When utmp backend is ‘ulog’ (FreeBSD) +# uppercase-regex-insert=yes + [environment] # name=value diff --git a/tests/test-config.c b/tests/test-config.c index bab57788..8c0805f4 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -491,6 +491,7 @@ test_section_main(void) test_boolean(&ctx, &parse_section_main, "locked-title", &conf.locked_title); test_boolean(&ctx, &parse_section_main, "dpi-aware", &conf.dpi_aware); test_boolean(&ctx, &parse_section_main, "gamma-correct-blending", &conf.gamma_correct); + test_boolean(&ctx, &parse_section_main, "uppercase-regex-insert", &conf.uppercase_regex_insert); test_pt_or_px(&ctx, &parse_section_main, "font-size-adjustment", &conf.font_size_adjustment.pt_or_px); /* TODO: test ‘N%’ values too */ test_pt_or_px(&ctx, &parse_section_main, "line-height", &conf.line_height); diff --git a/url-mode.c b/url-mode.c index 19d95356..44809f5f 100644 --- a/url-mode.c +++ b/url-mode.c @@ -283,7 +283,8 @@ urls_input(struct seat *seat, struct terminal *term, if (match) { // If the last hint character was uppercase, copy and paste - activate_url(seat, term, match, serial, wc == toc32upper(wc)); + bool insert = term->conf->uppercase_regex_insert && wc == toc32upper(wc); + activate_url(seat, term, match, serial, insert); switch (match->action) { case URL_ACTION_COPY: From 1d9ac3f611f1b81983b095a2f96e36dba4d24da9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 31 Aug 2025 11:42:56 +0200 Subject: [PATCH 1242/1323] doc: foot.ini: typo: upppercase -> uppercase --- doc/foot.ini.5.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 3a057d88..7550fd13 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -231,7 +231,7 @@ empty string to be set, but it must be quoted: *KEY=""*) Default: _no_. -*upppercase-regex-insert* +*uppercase-regex-insert* Boolean. When enabled, inputting an uppercase hint character in *show-urls-copy* or *regex-copy* mode will insert the selected text into the prompt in addition to copying it to the clipboard. From 65528f455d0d7753da73365fb39b39473a87d8b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 9 Sep 2025 17:34:02 +0200 Subject: [PATCH 1243/1323] meson: utempter del has no argument This fixes an issue where we didn't record a logout record when using the libutempter backend. --- CHANGELOG.md | 3 ++- meson.build | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57144549..4536f6ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,7 +93,8 @@ * Invalid configuration values overriding valid ones in surprising ways. - +* Bug where the libutempter utmp backend did not record logouts + correctly. ### Security ### Contributors diff --git a/meson.build b/meson.build index 0b7bbc17..56f4a31c 100644 --- a/meson.build +++ b/meson.build @@ -53,7 +53,7 @@ if utmp_backend == 'none' elif utmp_backend == 'libutempter' utmp_add = 'add' utmp_del = 'del' - utmp_del_have_argument = true + utmp_del_have_argument = false if utmp_default_helper_path == 'auto' utmp_default_helper_path = join_paths('/usr', get_option('libdir'), 'utempter', 'utempter') endif From efc39097e5a939c4e6f54bf80d24871bfeeaf80b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 9 Sep 2025 17:34:54 +0200 Subject: [PATCH 1244/1323] term: no need to pass ptmx as stdout to utempter --- terminal.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/terminal.c b/terminal.c index 2e23f749..421a3e65 100644 --- a/terminal.c +++ b/terminal.c @@ -199,7 +199,7 @@ add_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) return true; char *const argv[] = {conf->utmp_helper_path, UTMP_ADD, getenv("WAYLAND_DISPLAY"), NULL}; - return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL, NULL, NULL) >= 0; + return spawn(reaper, NULL, argv, ptmx, -1, -1, NULL, NULL, NULL) >= 0; #else return true; #endif @@ -223,7 +223,7 @@ del_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) ; char *const argv[] = {conf->utmp_helper_path, UTMP_DEL, del_argument, NULL}; - return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL, NULL, NULL) >= 0; + return spawn(reaper, NULL, argv, ptmx, -1, -1, NULL, NULL, NULL) >= 0; #else return true; #endif From f715f3b55fb094e6ab1fdc18dcf8956227c495c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 12 Sep 2025 10:18:06 +0200 Subject: [PATCH 1245/1323] changelog: prepare for 1.24.0 --- CHANGELOG.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4536f6ea..7fa3f0ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.24.0](#1-24-0) * [1.23.1](#1-23-1) * [1.23.0](#1-23-0) * [1.22.3](#1-22-3) @@ -65,7 +65,8 @@ * [1.2.0](#1-2-0) -## Unreleased +## 1.24.0 + ### Added * The `uppercase-regex-insert` option controls whether an uppercase hint @@ -87,8 +88,6 @@ [2156]: https://codeberg.org/dnkl/foot/issues/2156 -### Deprecated -### Removed ### Fixed * Invalid configuration values overriding valid ones in surprising @@ -96,9 +95,11 @@ * Bug where the libutempter utmp backend did not record logouts correctly. -### Security ### Contributors +* Ryan Roden-Corrent +* Tobias Mock + ## 1.23.1 From fa0fd2f50f41d8fc47241dd576be42a2f29d6530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 12 Sep 2025 10:18:33 +0200 Subject: [PATCH 1246/1323] meson: bump version to 1.24.0 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 56f4a31c..305df13c 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.23.1', + version: '1.24.0', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From c34f0633075769a2564e10d359feeca604373ae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 12 Sep 2025 10:22:21 +0200 Subject: [PATCH 1247/1323] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fa3f0ea..3144a125 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.24.0](#1-24-0) * [1.23.1](#1-23-1) * [1.23.0](#1-23-0) @@ -65,6 +66,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.24.0 ### Added From 44a674edb86f2f8db3304d76faf567346dcd7d94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 25 Sep 2025 16:57:41 +0200 Subject: [PATCH 1248/1323] term: erase: use erase_line() whenever a range corresponds to a full line --- terminal.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/terminal.c b/terminal.c index 421a3e65..e27a8fd9 100644 --- a/terminal.c +++ b/terminal.c @@ -2648,7 +2648,10 @@ term_erase(struct terminal *term, int start_row, int start_col, if (start_row == end_row) { struct row *row = grid_row(term->grid, start_row); - erase_cell_range(term, row, start_col, end_col); + if (unlikely(start_col == 0 && end_col == term->cols - 1)) + erase_line(term, row); + else + erase_cell_range(term, row, start_col, end_col); sixel_overwrite_by_row(term, start_row, start_col, end_col - start_col + 1); return; } @@ -2664,7 +2667,10 @@ term_erase(struct terminal *term, int start_row, int start_col, sixel_overwrite_by_rectangle( term, start_row + 1, 0, end_row - start_row, term->cols); - erase_cell_range(term, grid_row(term->grid, end_row), 0, end_col); + if (unlikely(end_col == term->cols - 1)) + erase_line(term, grid_row(term->grid, end_row)); + else + erase_cell_range(term, grid_row(term->grid, end_row), 0, end_col); sixel_overwrite_by_row(term, end_row, 0, end_col + 1); } From 1dfa86c93ac3585b1b8b6ff3d8617176a57942c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 4 Oct 2025 07:21:15 +0200 Subject: [PATCH 1249/1323] Revert "term: erase: use erase_line() whenever a range corresponds to a full line" This reverts commit 44a674edb86f2f8db3304d76faf567346dcd7d94. It caused a regression with prompt markers, in at least fish+starship. --- terminal.c | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/terminal.c b/terminal.c index e27a8fd9..421a3e65 100644 --- a/terminal.c +++ b/terminal.c @@ -2648,10 +2648,7 @@ term_erase(struct terminal *term, int start_row, int start_col, if (start_row == end_row) { struct row *row = grid_row(term->grid, start_row); - if (unlikely(start_col == 0 && end_col == term->cols - 1)) - erase_line(term, row); - else - erase_cell_range(term, row, start_col, end_col); + erase_cell_range(term, row, start_col, end_col); sixel_overwrite_by_row(term, start_row, start_col, end_col - start_col + 1); return; } @@ -2667,10 +2664,7 @@ term_erase(struct terminal *term, int start_row, int start_col, sixel_overwrite_by_rectangle( term, start_row + 1, 0, end_row - start_row, term->cols); - if (unlikely(end_col == term->cols - 1)) - erase_line(term, grid_row(term->grid, end_row)); - else - erase_cell_range(term, grid_row(term->grid, end_row), 0, end_col); + erase_cell_range(term, grid_row(term->grid, end_row), 0, end_col); sixel_overwrite_by_row(term, end_row, 0, end_col + 1); } From 80951ab7a6b3bcc4dd3c30a5454bf648d3d9ed93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 4 Oct 2025 09:24:47 +0200 Subject: [PATCH 1250/1323] term: osc8: tag *all* cells in a multi-column character as an URI When we print a character to the grid, we must also update its OSC-8 state if an OSC-8 URI is currently active. For double-width characters, this was only being done for the first cell. This causes the labels in URL mode to be off, as the link was effectively chopped up into multiple pieces. Closes #2179 --- CHANGELOG.md | 7 +++++++ terminal.c | 8 +++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3144a125..2b0a7247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,13 @@ ### Deprecated ### Removed ### Fixed + +* URL labels misplaces when URL contains double-width characters + ([#2179][2179]). + +[2179]: https://codeberg.org/dnkl/foot/issues/2179 + + ### Security ### Contributors diff --git a/terminal.c b/terminal.c index 421a3e65..60506d07 100644 --- a/terminal.c +++ b/terminal.c @@ -4005,9 +4005,11 @@ term_print(struct terminal *term, char32_t wc, int width, bool insert_mode_disab cell->wc = term->vt.last_printed = wc; cell->attrs = term->vt.attrs; - if (term->vt.osc8.uri != NULL) { - grid_row_uri_range_put( - row, col, term->vt.osc8.uri, term->vt.osc8.id); + if (unlikely(term->vt.osc8.uri != NULL)) { + for (int i = 0; i < width && (col + i) < term->cols; i++) { + grid_row_uri_range_put( + row, col + i, term->vt.osc8.uri, term->vt.osc8.id); + } switch (term->conf->url.osc8_underline) { case OSC8_UNDERLINE_ALWAYS: From fac399415452edba6f20986e5f8fa479863582fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 4 Oct 2025 09:29:56 +0200 Subject: [PATCH 1251/1323] config: add tweak.min-stride-alignment This allows the user to configure the value by which a surface buffer's stride must be an even multiple of. This can be used to ensure the stride meets the GPU driver's requirements for direct import. Defaults to 256. Set to 0 to disable. Closes #2182 --- config.c | 5 +++++ config.h | 1 + main.c | 1 + shm.c | 17 ++++++++++++++++- shm.h | 3 +++ tests/test-config.c | 3 +++ 6 files changed, 29 insertions(+), 1 deletion(-) diff --git a/config.c b/config.c index 0de1a1be..459a1de9 100644 --- a/config.c +++ b/config.c @@ -2848,6 +2848,10 @@ parse_section_tweak(struct context *ctx) #endif } + else if (streq(key, "min-stride-alignment")) { + return value_to_uint32(ctx, 10, &conf->tweak.min_stride_alignment); + } + else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; @@ -3496,6 +3500,7 @@ config_load(struct config *conf, const char *conf_path, .font_monospace_warn = true, .sixel = true, .surface_bit_depth = SHM_BITS_AUTO, + .min_stride_alignment = 256, }, .touch = { diff --git a/config.h b/config.h index 86fb2a8f..11439d3a 100644 --- a/config.h +++ b/config.h @@ -435,6 +435,7 @@ struct config { bool font_monospace_warn; bool sixel; enum shm_bit_depth surface_bit_depth; + uint32_t min_stride_alignment; } tweak; struct { diff --git a/main.c b/main.c index 1afbd16d..b6a0d825 100644 --- a/main.c +++ b/main.c @@ -597,6 +597,7 @@ main(int argc, char *const *argv) } shm_set_max_pool_size(conf.tweak.max_shm_pool_size); + shm_set_min_stride_alignment(conf.tweak.min_stride_alignment); if ((fdm = fdm_init()) == NULL) goto out; diff --git a/shm.c b/shm.c index 4680c12c..7b13db9b 100644 --- a/shm.c +++ b/shm.c @@ -13,7 +13,6 @@ #include <pixman.h> -#include <fcft/stride.h> #include <tllist.h> #define LOG_MODULE "shm" @@ -21,6 +20,7 @@ #include "log.h" #include "debug.h" #include "macros.h" +#include "stride.h" #include "xmalloc.h" #if !defined(MAP_UNINITIALIZED) @@ -61,6 +61,8 @@ static off_t max_pool_size = 512 * 1024 * 1024; static bool can_punch_hole = false; static bool can_punch_hole_initialized = false; +static size_t min_stride_alignment = 0; + struct buffer_pool { int fd; /* memfd */ struct wl_shm_pool *wl_pool; @@ -113,6 +115,12 @@ shm_set_max_pool_size(off_t _max_pool_size) max_pool_size = _max_pool_size; } +void +shm_set_min_stride_alignment(size_t _min_stride_alignment) +{ + min_stride_alignment = _min_stride_alignment; +} + static void buffer_destroy_dont_close(struct buffer *buf) { @@ -342,6 +350,13 @@ get_new_buffers(struct buffer_chain *chain, size_t count, ? chain->pixman_fmt_with_alpha : chain->pixman_fmt_without_alpha, widths[i]); + + if (min_stride_alignment > 0) { + const size_t m = min_stride_alignment; + stride[i] = (stride[i] + m - 1) / m * m; + } + + xassert(min_stride_alignment == 0 || stride[i] % min_stride_alignment == 0); sizes[i] = stride[i] * heights[i]; total_size += sizes[i]; } diff --git a/shm.h b/shm.h index 8f8c406a..6050f1c7 100644 --- a/shm.h +++ b/shm.h @@ -42,7 +42,10 @@ struct buffer { }; void shm_fini(void); + +/* TODO: combine into shm_init() */ void shm_set_max_pool_size(off_t max_pool_size); +void shm_set_min_stride_alignment(size_t min_stride_alignment); struct buffer_chain; struct buffer_chain *shm_chain_new( diff --git a/tests/test-config.c b/tests/test-config.c index 8c0805f4..64b61540 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -1413,6 +1413,9 @@ test_section_tweak(void) test_float(&ctx, &parse_section_tweak, "bold-text-in-bright-amount", &conf.bold_in_bright.amount); + test_uint32(&ctx, &parse_section_tweak, "min-stride-alignment", + &conf.tweak.min_stride_alignment); + #if 0 /* Must be equal to, or less than INT32_MAX */ test_uint32(&ctx, &parse_section_tweak, "max-shm-pool-size-mb", &conf.tweak.max_shm_pool_size); From bd994eda1c816d5430a9c4cdb7089e43c47d74ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 4 Oct 2025 10:50:38 +0200 Subject: [PATCH 1252/1323] shm: page-align the memfd size (also needed for GPU direct import) --- shm.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/shm.c b/shm.c index 7b13db9b..31ea67ed 100644 --- a/shm.c +++ b/shm.c @@ -239,7 +239,6 @@ static const struct wl_buffer_listener buffer_listener = { .release = &buffer_release, }; -#if __SIZEOF_POINTER__ == 8 static size_t page_size(void) { @@ -256,7 +255,6 @@ page_size(void) xassert(size > 0); return size; } -#endif static bool instantiate_offset(struct buffer_private *buf, off_t new_offset) @@ -398,9 +396,11 @@ get_new_buffers(struct buffer_chain *chain, size_t count, goto err; } + const size_t page_sz = page_size(); + #if __SIZEOF_POINTER__ == 8 off_t offset = chain->scrollable && max_pool_size > 0 - ? (max_pool_size / 4) & ~(page_size() - 1) + ? (max_pool_size / 4) & ~(page_sz - 1) : 0; off_t memfd_size = chain->scrollable && max_pool_size > 0 ? max_pool_size @@ -410,7 +410,8 @@ get_new_buffers(struct buffer_chain *chain, size_t count, off_t memfd_size = total_size; #endif - xassert(chain->scrollable || (offset == 0 && memfd_size == total_size)); + /* Page align */ + memfd_size = (memfd_size + page_sz - 1) & ~(page_sz - 1); LOG_DBG("memfd-size: %lu, initial offset: %lu", memfd_size, offset); @@ -442,6 +443,9 @@ get_new_buffers(struct buffer_chain *chain, size_t count, memfd_size = total_size; chain->scrollable = false; + /* Page align */ + memfd_size = (memfd_size + page_sz - 1) & ~(page_sz - 1); + if (ftruncate(pool_fd, memfd_size) < 0) { LOG_ERRNO("failed to set size of SHM backing memory file"); goto err; From e43ea3676fe359864a8a22a31beed5bf289f367c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 4 Oct 2025 15:38:35 +0200 Subject: [PATCH 1253/1323] doc: foot.ini: document tweak.min-stride-alignment --- doc/foot.ini.5.scd | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 7550fd13..56b76be7 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -2031,6 +2031,32 @@ any of these options. Default: _512_. Maximum allowed: _2048_ (2GB). +*min-stride-alignment* + This option controls the minimum stride alignment, in bytes, when + allocating SHM buffers. + + In some circumstances, a compositor can import foot's SHM buffers + directly to the GPU, without copying the buffer to GPU memory + (typically on integrated graphics). Different drivers have + different requirements for this, and one of those requirements is + typically the stride alignment. At the time of writing, AMD GPUs + require 256-byte alignment. + + Note that doing a direct import typically disables immediate + buffer release (if the compositor supports that), which means foot + has to double buffer. This adds a performance penalty in foot, but + the overall system performance should still be better. + + If you are not using integrated graphics, or if the compositor + does not support GPU direct imports, this option has close to zero + impact. You can save a small amount of memory by setting this to + 0. + + Ultimately, it is up to the compositor to decide whether to do + immediate buffer releases, or try to optimize GPU imports. + + Default: _256_ + *sixel* Boolean. When enabled, foot will process sixel images. Default: _yes_ From bb314425ef9c2069e3b17122b7f81ecc7d527a81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 4 Oct 2025 15:40:20 +0200 Subject: [PATCH 1254/1323] changelog: shm buffer stride alignment --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b0a7247..a2aaf488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,16 @@ ## Unreleased ### Added ### Changed + +* SHM buffer sizes are now rounded up to nearest page size, and their + stride is always an even multiple of 256 bytes (by default, + configurable by setting `tweak.min-stride-alignment`). This allows + compositor to directly import foot's SHM buffers to the GPU, with + e.g. integrated graphics ([#2182][2182]). + +[2182]: https://codeberg.org/dnkl/foot/issues/2182 + + ### Deprecated ### Removed ### Fixed From 299186a6547f6e038ee6f3822caf9a0fdfabceef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 5 Oct 2025 10:48:36 +0200 Subject: [PATCH 1255/1323] render: when double-buffering, pre-apply previous frame's damage early Foot likes it when compositor releases buffer immediately, as that means we only have to re-render the cells that have changed since the last frame. For various reasons, not all compositors do this. In this case, foot is typically forced to switch between two buffers, i.e. double-buffer. In this case, each frame starts with copying over the damage from the previous frame, to the new frame. Then we start rendering the updated cells. Bringing over the previous frame's damage can be slow, if the changed area was large (e.g. when scrolling one or a few lines, or on full screen updates). It's also done single-threaded. Thus it not only slows down frame rendering, but pauses everything else (i.e. input processing). All in all, it reduces performance and increases input latency. But we don't have to wait until it's time to render a frame to copy over the previous frame's damage. We can do that as soon as the compositor has released the buffer (for the frame _before_ the previous frame). And we can do this in a thread. This frees up foot to continue processing input, and reduces frame rendering time since we can now start rendering the modified cells immediately, without first doing a large memcpy(3). In worst case scenarios (or perhaps we should consider them best case scenarios...), I've seen up to a 10x performance increase in frame rendering times (this obviously does *not* include the time it takes to copy over the previous frame's damage, since that doesn't affect neither input processing nor frame rendering). Implemented by adding a callback mechanism to the shm abstraction layer. Use it for the grid buffers, and kick off a thread that copies the previous frame's damage, and resets the buffers age to 0 (so that foot understands it can start render to it immediately when it later needs to render a frame). Since we have certain way of knowing if a compositor releases buffers immediately or not, use a bit of heuristics; if we see 10 consecutive non-immediate releases (that is, we reset the counter as soon as we do see an immediate release), this new "pre-apply damage" logic is enabled. It can be force-disabled with tweak.pre-apply-damage=no. We also need to take care to wait for the thread before resetting the render's "last_buf" pointer (or we'll SEGFAULT in the thread...). We must also ensure we wait for the thread to finish before we start rendering a new frame. Under normal circumstances, the wait time is always 0, the thread has almost always finished long before we need to render the next frame. But it _can_ happen. Closes #2188 --- CHANGELOG.md | 7 ++ config.c | 7 +- config.h | 1 + doc/foot.ini.5.scd | 35 ++++++++ pgo/pgo.c | 5 +- render.c | 200 ++++++++++++++++++++++++++++++++++++++++++--- render.h | 2 + shm.c | 24 +++++- shm.h | 3 +- terminal.c | 19 +++-- terminal.h | 10 +++ 11 files changed, 287 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2aaf488..65f0bbab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,13 @@ ## Unreleased ### Added + +* Performance increased and input latency decreased on compositors + that do not release SHM buffers immediately ([#2188][2188]). + +[2188]: https://codeberg.org/dnkl/foot/issues/2188 + + ### Changed * SHM buffer sizes are now rounded up to nearest page size, and their diff --git a/config.c b/config.c index 459a1de9..06817247 100644 --- a/config.c +++ b/config.c @@ -2848,9 +2848,11 @@ parse_section_tweak(struct context *ctx) #endif } - else if (streq(key, "min-stride-alignment")) { + else if (streq(key, "min-stride-alignment")) return value_to_uint32(ctx, 10, &conf->tweak.min_stride_alignment); - } + + else if (streq(key, "pre-apply-damage")) + return value_to_bool(ctx, &conf->tweak.preapply_damage); else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); @@ -3501,6 +3503,7 @@ config_load(struct config *conf, const char *conf_path, .sixel = true, .surface_bit_depth = SHM_BITS_AUTO, .min_stride_alignment = 256, + .preapply_damage = true, }, .touch = { diff --git a/config.h b/config.h index 11439d3a..5b7ff11e 100644 --- a/config.h +++ b/config.h @@ -436,6 +436,7 @@ struct config { bool sixel; enum shm_bit_depth surface_bit_depth; uint32_t min_stride_alignment; + bool preapply_damage; } tweak; struct { diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 56b76be7..7b08d5d4 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -2093,6 +2093,41 @@ any of these options. Default: _auto_ +*pre-apply-damage* + Boolean. When enabled, foot will attempt to "pre-apply" the damage + from the last frame when foot is forced to double-buffer + (i.e. when the compositor does not release SHM buffers + immediately). All text after this assumes the compositor is not + releasing buffers immediately. + + When this option is disabled, each time foot needs to render a + frame, it has to first copy over areas that changed in the last + frame (i.e. all changes between the last two frames). This is + basically a *memcpy*(3), which can be slow if the changed area is + large. It is also done on the main thread, which means foot cannot + do anything else at the same time; no other rendering, no VT + parsing. After the changes have been brought over to the new + frame, foot proceeds with rendering the cells that has changed + between the last frame and the new frame. + + When this open is enabled, the changes between the last two frames + are brought over to what will become the next frame before foot + starts rendering the next frame. As soon as the compositor + releases the previous buffer (typically right after foot has + pushed a new frame), foot kicks off a thread that copies over the + changes to the newly released buffer. Since this is done in a + thread, foot can continue processing input at the same + time. Later, when it is time to render a new frame, the changes + have already been transferred, and foot can immediately start with + the actual rendering. + + Thus, having this option enabled improves both performance + (copying the last two frames' changes is threaded), and improves + input latency (rending the next frame no longer has to first bring + over the changes between the last two frames). + + Default: _yes_ + # SEE ALSO *foot*(1), *footclient*(1) diff --git a/pgo/pgo.c b/pgo/pgo.c index 757dcd06..4ff4111c 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -74,6 +74,8 @@ void render_refresh_icon(struct terminal *term) {} void render_overlay(struct terminal *term) {} +void render_buffer_release_callback(struct buffer *buf, void *data) {} + bool render_xcursor_is_valid(const struct seat *seat, const char *cursor) { @@ -206,7 +208,8 @@ enum shm_bit_depth shm_chain_bit_depth(const struct buffer_chain *chain) { retur struct buffer_chain * shm_chain_new( struct wayland *wayl, bool scrollable, size_t pix_instances, - enum shm_bit_depth desired_bit_depth) + enum shm_bit_depth desired_bit_depth, + void (*release_cb)(struct buffer *buf, void *data), void *cb_data) { return NULL; } diff --git a/render.c b/render.c index 1c24bafa..35752125 100644 --- a/render.c +++ b/render.c @@ -2224,6 +2224,56 @@ render_worker_thread(void *_ctx) case -2: return 0; + + case -3: { + if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) + clock_gettime(CLOCK_MONOTONIC, &term->render.workers.preapplied_damage.start); + + mtx_lock(&term->render.workers.preapplied_damage.lock); + buf = term->render.workers.preapplied_damage.buf; + xassert(buf != NULL); + + if (likely(term->render.last_buf != NULL)) { + mtx_unlock(&term->render.workers.preapplied_damage.lock); + + pixman_region32_t dmg; + pixman_region32_init(&dmg); + + if (buf->age == 0) + ; /* No need to do anything */ + else if (buf->age == 1) + pixman_region32_copy(&dmg, + &term->render.last_buf->dirty[0]); + else + pixman_region32_init_rect(&dmg, 0, 0, buf->width, + buf->height); + + pixman_image_set_clip_region32(buf->pix[my_id], &dmg); + pixman_image_composite32(PIXMAN_OP_SRC, + term->render.last_buf->pix[my_id], + NULL, buf->pix[my_id], 0, 0, 0, 0, 0, + 0, buf->width, buf->height); + + pixman_region32_fini(&dmg); + + buf->age = 0; + shm_unref(term->render.last_buf); + shm_addref(buf); + term->render.last_buf = buf; + + mtx_lock(&term->render.workers.preapplied_damage.lock); + } + + term->render.workers.preapplied_damage.buf = NULL; + cnd_signal(&term->render.workers.preapplied_damage.cond); + mtx_unlock(&term->render.workers.preapplied_damage.lock); + + if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) + clock_gettime(CLOCK_MONOTONIC, &term->render.workers.preapplied_damage.stop); + + frame_done = true; + break; + } } } }; @@ -2231,6 +2281,22 @@ render_worker_thread(void *_ctx) return -1; } +static void +wait_for_preapply_damage(struct terminal *term) +{ + if (!term->render.preapply_last_frame_damage) + return; + if (term->render.workers.preapplied_damage.buf == NULL) + return; + + mtx_lock(&term->render.workers.preapplied_damage.lock); + while (term->render.workers.preapplied_damage.buf != NULL) { + cnd_wait(&term->render.workers.preapplied_damage.cond, + &term->render.workers.preapplied_damage.lock); + } + mtx_unlock(&term->render.workers.preapplied_damage.lock); +} + struct csd_data get_csd_data(const struct terminal *term, enum csd_surface surf_idx) { @@ -3113,14 +3179,6 @@ force_full_repaint(struct terminal *term, struct buffer *buf) static void reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old) { - static int counter = 0; - static bool have_warned = false; - if (!have_warned && ++counter > 5) { - LOG_WARN("compositor is not releasing buffers immediately; " - "expect lower rendering performance"); - have_warned = true; - } - if (new->age > 1) { memcpy(new->data, old->data, new->height * new->stride); return; @@ -3251,7 +3309,18 @@ grid_render(struct terminal *term) if (term->shutdown.in_progress) return; - struct timespec start_time, start_double_buffering = {0}, stop_double_buffering = {0}; + struct timespec start_time; + struct timespec start_wait_preapply = {0}, stop_wait_preapply = {0}; + struct timespec start_double_buffering = {0}, stop_double_buffering = {0}; + + /* Might be a thread doing pre-applied damage */ + if (unlikely(term->render.preapply_last_frame_damage && + term->render.workers.preapplied_damage.buf != NULL)) + { + clock_gettime(CLOCK_MONOTONIC, &start_wait_preapply); + wait_for_preapply_damage(term); + clock_gettime(CLOCK_MONOTONIC, &stop_wait_preapply); + } if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) clock_gettime(CLOCK_MONOTONIC, &start_time); @@ -3269,6 +3338,8 @@ grid_render(struct terminal *term) dirty_old_cursor(term); dirty_cursor(term); + LOG_DBG("buffer age: %u (%p)", buf->age, (void *)buf); + if (term->render.last_buf == NULL || term->render.last_buf->width != buf->width || term->render.last_buf->height != buf->height || @@ -3285,9 +3356,27 @@ grid_render(struct terminal *term) xassert(term->render.last_buf->width == buf->width); xassert(term->render.last_buf->height == buf->height); + if (++term->render.frames_since_last_immediate_release > 10) { + static bool have_warned = false; + + if (!term->render.preapply_last_frame_damage && + term->conf->tweak.preapply_damage && + term->render.workers.count > 0) + { + LOG_INFO("enabling pre-applied frame damage"); + term->render.preapply_last_frame_damage = true; + } else if (!have_warned) { + LOG_WARN("compositor is not releasing buffers immediately; " + "expect lower rendering performance"); + have_warned = true; + } + } + clock_gettime(CLOCK_MONOTONIC, &start_double_buffering); reapply_old_damage(term, buf, term->render.last_buf); clock_gettime(CLOCK_MONOTONIC, &stop_double_buffering); + } else if (!term->render.preapply_last_frame_damage) { + term->render.frames_since_last_immediate_release = 0; } if (term->render.last_buf != NULL) { @@ -3515,27 +3604,40 @@ grid_render(struct terminal *term) struct timespec end_time; clock_gettime(CLOCK_MONOTONIC, &end_time); + struct timespec wait_time; + timespec_sub(&stop_wait_preapply, &start_wait_preapply, &wait_time); + struct timespec render_time; timespec_sub(&end_time, &start_time, &render_time); struct timespec double_buffering_time; timespec_sub(&stop_double_buffering, &start_double_buffering, &double_buffering_time); + struct timespec preapply_damage; + timespec_sub(&term->render.workers.preapplied_damage.stop, + &term->render.workers.preapplied_damage.start, + &preapply_damage); + struct timespec total_render_time; timespec_add(&render_time, &double_buffering_time, &total_render_time); + timespec_add(&wait_time, &total_render_time, &total_render_time); switch (term->conf->tweak.render_timer) { case RENDER_TIMER_LOG: case RENDER_TIMER_BOTH: LOG_INFO( "frame rendered in %lds %9ldns " - "(%lds %9ldns rendering, %lds %9ldns double buffering)", + "(%lds %9ldns wait, %lds %9ldns rendering, %lds %9ldns double buffering) not included: %lds %ldns pre-apply damage", (long)total_render_time.tv_sec, total_render_time.tv_nsec, + (long)wait_time.tv_sec, + wait_time.tv_nsec, (long)render_time.tv_sec, render_time.tv_nsec, (long)double_buffering_time.tv_sec, - double_buffering_time.tv_nsec); + double_buffering_time.tv_nsec, + (long)preapply_damage.tv_sec, + preapply_damage.tv_nsec); break; case RENDER_TIMER_OSD: @@ -4295,6 +4397,7 @@ delayed_reflow_of_normal_grid(struct terminal *term) term->interactive_resizing.old_hide_cursor = false; /* Invalidate render pointers */ + wait_for_preapply_damage(term); shm_unref(term->render.last_buf); term->render.last_buf = NULL; term->render.last_cursor.row = NULL; @@ -4869,6 +4972,7 @@ damage_view: tll_free(term->normal.scroll_damage); tll_free(term->alt.scroll_damage); + wait_for_preapply_damage(term); shm_unref(term->render.last_buf); term->render.last_buf = NULL; term_damage_view(term); @@ -5267,3 +5371,77 @@ render_xcursor_set(struct seat *seat, struct terminal *term, seat->pointer.xcursor_pending = true; return true; } + +void +render_buffer_release_callback(struct buffer *buf, void *data) +{ + /* + * Called from shm.c when a buffer is released + * + * We use it to pre-apply last-frame's damage to it, when we're + * forced to double buffer (compositor doesn't release buffers + * immediately). + * + * The timeline is thus: + * 1. We render and push a new frame + * 2. Some (hopefully short) time after that, the compositor releases the previous buffer + * 3. We're called, and kick off the thread that copies the changes from (1) to the just freed buffer + * 4. Time passes.... + * 5. The compositor calls our frame callback, signalling to us that it's time to start rendering the next frame + * 6. Hopefully, our thread is already done with copying the changes, otherwise we stall, waiting for it + * 7. We render the frame as if the compositor does immediate releases. + * + * What's the gain? Reduced latency, by applying the previous + * frame's damage as soon as possible, we shorten the time it + * takes to render the frame after the frame callback. + * + * This means the compositor can, in theory, push the frame + * callback closer to the vblank deadline, and thus reduce input + * latency. Not all compositors (most, in fact?) don't adapt like + * this, unfortunately. But some allows the user to manually + * configure the deadline. + */ + struct terminal *term = data; + + if (likely(buf->age != 1)) + return; + + if (likely(!term->render.preapply_last_frame_damage)) + return; + + if (term->render.last_buf == NULL) + return; + + if (term->render.last_buf->age != 0) + return; + + if (buf->width != term->render.last_buf->width) + return; + + if (buf->height != term->render.last_buf->height) + return; + + xassert(term->render.workers.count > 0); + xassert(term->render.last_buf != NULL); + + xassert(term->render.last_buf->age == 0); + xassert(term->render.last_buf != buf); + + mtx_lock(&term->render.workers.preapplied_damage.lock); + if (term->render.workers.preapplied_damage.buf != NULL) { + mtx_unlock(&term->render.workers.preapplied_damage.lock); + return; + } + + xassert(term->render.workers.preapplied_damage.buf == NULL); + term->render.workers.preapplied_damage.buf = buf; + term->render.workers.preapplied_damage.start = (struct timespec){0}; + term->render.workers.preapplied_damage.stop = (struct timespec){0}; + mtx_unlock(&term->render.workers.preapplied_damage.lock); + + mtx_lock(&term->render.workers.lock); + sem_post(&term->render.workers.start); + xassert(tll_length(term->render.workers.queue) == 0); + tll_push_back(term->render.workers.queue, -3); + mtx_unlock(&term->render.workers.lock); +} diff --git a/render.h b/render.h index 81d2a905..e21eaca8 100644 --- a/render.h +++ b/render.h @@ -47,3 +47,5 @@ struct csd_data { }; struct csd_data get_csd_data(const struct terminal *term, enum csd_surface surf_idx); + +void render_buffer_release_callback(struct buffer *buf, void *data); diff --git a/shm.c b/shm.c index 31ea67ed..72b32f16 100644 --- a/shm.c +++ b/shm.c @@ -87,6 +87,9 @@ struct buffer_private { bool with_alpha; bool scrollable; + + void (*release_cb)(struct buffer *buf, void *data); + void *cb_data; }; struct buffer_chain { @@ -100,6 +103,9 @@ struct buffer_chain { pixman_format_code_t pixman_fmt_with_alpha; enum wl_shm_format shm_format_with_alpha; + + void (*release_cb)(struct buffer *buf, void *data); + void *cb_data; }; static tll(struct buffer_private *) deferred; @@ -232,6 +238,10 @@ buffer_release(void *data, struct wl_buffer *wl_buffer) xassert(found); if (!found) LOG_WARN("deferred delete: buffer not on the 'deferred' list"); + } else { + if (buffer->release_cb != NULL) { + buffer->release_cb(&buffer->public, buffer->cb_data); + } } } @@ -516,6 +526,8 @@ get_new_buffers(struct buffer_chain *chain, size_t count, .offset = 0, .size = sizes[i], .scrollable = chain->scrollable, + .release_cb = chain->release_cb, + .cb_data = chain->cb_data, }; if (!instantiate_offset(buf, offset)) { @@ -623,7 +635,7 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height, bool with_alph * reuse. Pick the "youngest" one, and mark the * other one for purging */ if (buf->public.age < cached->public.age) { - shm_unref(&cached->public); + //shm_unref(&cached->public); cached = buf; } else { /* @@ -634,8 +646,8 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height, bool with_alph * should be safe; "our" tll_foreach() already * holds the next pointer. */ - if (buffer_unref_no_remove_from_chain(buf)) - tll_remove(chain->bufs, it); + //if (buffer_unref_no_remove_from_chain(buf)) + // tll_remove(chain->bufs, it); } } } @@ -994,7 +1006,8 @@ shm_unref(struct buffer *_buf) struct buffer_chain * shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, - enum shm_bit_depth desired_bit_depth) + enum shm_bit_depth desired_bit_depth, + void (*release_cb)(struct buffer *buf, void *data), void *cb_data) { pixman_format_code_t pixman_fmt_without_alpha = PIXMAN_x8r8g8b8; enum wl_shm_format shm_fmt_without_alpha = WL_SHM_FORMAT_XRGB8888; @@ -1090,6 +1103,9 @@ shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, .pixman_fmt_with_alpha = pixman_fmt_with_alpha, .shm_format_with_alpha = shm_fmt_with_alpha, + + .release_cb = release_cb, + .cb_data = cb_data, }; return chain; } diff --git a/shm.h b/shm.h index 6050f1c7..84eb4386 100644 --- a/shm.h +++ b/shm.h @@ -50,7 +50,8 @@ void shm_set_min_stride_alignment(size_t min_stride_alignment); struct buffer_chain; struct buffer_chain *shm_chain_new( struct wayland *wayl, bool scrollable, size_t pix_instances, - enum shm_bit_depth desired_bit_depth); + enum shm_bit_depth desired_bit_depth, + void (*release_cb)(struct buffer *buf, void *data), void *cb_data); void shm_chain_free(struct buffer_chain *chain); enum shm_bit_depth shm_chain_bit_depth(const struct buffer_chain *chain); diff --git a/terminal.c b/terminal.c index 60506d07..36f8513b 100644 --- a/terminal.c +++ b/terminal.c @@ -719,6 +719,9 @@ initialize_render_workers(struct terminal *term) goto err_sem_destroy; } + mtx_init(&term->render.workers.preapplied_damage.lock, mtx_plain); + cnd_init(&term->render.workers.preapplied_damage.cond); + term->render.workers.threads = xcalloc( term->render.workers.count, sizeof(term->render.workers.threads[0])); @@ -1356,13 +1359,13 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .render = { .chains = { .grid = shm_chain_new(wayl, true, 1 + conf->render_worker_count, - desired_bit_depth), - .search = shm_chain_new(wayl, false, 1 ,desired_bit_depth), - .scrollback_indicator = shm_chain_new(wayl, false, 1, desired_bit_depth), - .render_timer = shm_chain_new(wayl, false, 1, desired_bit_depth), - .url = shm_chain_new(wayl, false, 1, desired_bit_depth), - .csd = shm_chain_new(wayl, false, 1, desired_bit_depth), - .overlay = shm_chain_new(wayl, false, 1, desired_bit_depth), + desired_bit_depth, &render_buffer_release_callback, term), + .search = shm_chain_new(wayl, false, 1 ,desired_bit_depth, NULL, NULL), + .scrollback_indicator = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .render_timer = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .url = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .csd = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .overlay = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), }, .scrollback_lines = conf->scrollback.lines, .app_sync_updates.timer_fd = app_sync_updates_fd, @@ -1893,6 +1896,8 @@ term_destroy(struct terminal *term) } } free(term->render.workers.threads); + mtx_destroy(&term->render.workers.preapplied_damage.lock); + cnd_destroy(&term->render.workers.preapplied_damage.cond); mtx_destroy(&term->render.workers.lock); sem_destroy(&term->render.workers.start); sem_destroy(&term->render.workers.done); diff --git a/terminal.h b/terminal.h index 88371b07..364d57b3 100644 --- a/terminal.h +++ b/terminal.h @@ -706,6 +706,14 @@ struct terminal { tll(int) queue; thrd_t *threads; struct buffer *buf; + + struct { + mtx_t lock; + cnd_t cond; + struct buffer *buf; + struct timespec start; + struct timespec stop; + } preapplied_damage; } workers; /* Last rendered cursor position */ @@ -716,6 +724,8 @@ struct terminal { } last_cursor; struct buffer *last_buf; /* Buffer we rendered to last time */ + size_t frames_since_last_immediate_release; + bool preapply_last_frame_damage; enum overlay_style last_overlay_style; struct buffer *last_overlay_buf; From fd88c6c61c52b75789c7f9d723815b67aed2984f Mon Sep 17 00:00:00 2001 From: Charalampos Mitrodimas <charmitro@posteo.net> Date: Thu, 2 Oct 2025 00:29:34 +0300 Subject: [PATCH 1256/1323] wayland: restore opacity after exiting fullscreen When exiting fullscreen mode, the window's transparency was not being restored, leaving it opaque until another window was fullscreened. This occurred because the Wayland opaque region was set based only on the configured alpha value, without considering the fullscreen state. Since commit 899b768b74 ("render: disable transparency when we're fullscreened") transparency is disabled during fullscreen to avoid compositor-mandated black backgrounds affecting the intended colors. However, the opaque region was not being updated when the fullscreen state changed. Fixes: https://codeberg.org/dnkl/foot/issues/2180 Signed-off-by: Charalampos Mitrodimas <charmitro@posteo.net> --- wayland.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/wayland.c b/wayland.c index 5f68ecf7..59b2a33e 100644 --- a/wayland.c +++ b/wayland.c @@ -1064,6 +1064,7 @@ xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, bool wasnt_configured = !win->is_configured; bool was_resizing = win->is_resizing; + bool was_fullscreen = win->is_fullscreen; bool csd_was_enabled = win->csd_mode == CSD_YES && !win->is_fullscreen; int new_width = win->configure.width; int new_height = win->configure.height; @@ -1096,6 +1097,10 @@ xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, else if (csd_was_enabled && !enable_csd) csd_destroy(win); + /* Update opaque region if fullscreen state changed */ + if (was_fullscreen != win->is_fullscreen) + wayl_win_alpha_changed(win); + if (enable_csd && new_width > 0 && new_height > 0) { if (wayl_win_csd_titlebar_visible(win)) new_height -= win->term->conf->csd.title_height; @@ -2401,7 +2406,13 @@ wayl_win_alpha_changed(struct wl_window *win) { struct terminal *term = win->term; - if (term->colors.alpha == 0xffff) { + /* + * When fullscreened, transparency is disabled (see render.c). + * Update the opaque region to match. + */ + bool is_opaque = term->colors.alpha == 0xffff || win->is_fullscreen; + + if (is_opaque) { struct wl_region *region = wl_compositor_create_region( term->wl->compositor); From e308a4733e4149b661457c9a275e48d9dbeaf23f Mon Sep 17 00:00:00 2001 From: Matthias Heyman <matthias.heyman@ebo-enterprises.com> Date: Fri, 26 Sep 2025 10:59:48 +0200 Subject: [PATCH 1257/1323] fix: jump labels are more readable --- themes/modus-operandi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/themes/modus-operandi b/themes/modus-operandi index 5e3a9fd6..2d417bb5 100644 --- a/themes/modus-operandi +++ b/themes/modus-operandi @@ -22,3 +22,5 @@ bright4=2544bb bright5=5317ac bright6=005a5f bright7=ffffff + +jump-labels=dce0e8 0000ff From 371837ef7b23b3e9f574069671d06a96e73526aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 10 Oct 2025 10:36:41 +0200 Subject: [PATCH 1258/1323] changelog: updated jump label colors in modus-operandi --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65f0bbab..314f7b38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,8 @@ configurable by setting `tweak.min-stride-alignment`). This allows compositor to directly import foot's SHM buffers to the GPU, with e.g. integrated graphics ([#2182][2182]). +* Jump label colors in the modus-operandi theme, for improved + readability. [2182]: https://codeberg.org/dnkl/foot/issues/2182 From 7ed36c10334c0b2480fa026238f8f1791d2f9439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 10 Oct 2025 11:10:38 +0200 Subject: [PATCH 1259/1323] config: add colors.dim-blend-towards=black|white Before this patch, we always blended towards black when dimming text. However, with light color themes, it usually looks better if we dim towards white instead. This option allows you to choose which color to blend towards. The default is 'black' in '[colors]', and 'white' in '[colors2]'. Closes #2187 --- CHANGELOG.md | 4 ++++ config.c | 15 ++++++++++++++- config.h | 5 +++++ doc/foot.ini.5.scd | 14 ++++++++++++-- foot.ini | 3 +++ render.c | 9 ++++++++- tests/test-config.c | 5 +++++ 7 files changed, 51 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 314f7b38..76183459 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,8 +71,12 @@ * Performance increased and input latency decreased on compositors that do not release SHM buffers immediately ([#2188][2188]). +* `colors{,2}.dim-blend-towards=black|white` option, allowing you to + select towards which color to blend when dimming text. Defaults to + `black` in `[colors]`, and `white` in `[colors2]` ([#2187][2187]). [2188]: https://codeberg.org/dnkl/foot/issues/2188 +[2187]: https://codeberg.org/dnkl/foot/issues/2187 ### Changed diff --git a/config.c b/config.c index 06817247..4449d9c2 100644 --- a/config.c +++ b/config.c @@ -1519,7 +1519,7 @@ parse_color_theme(struct context *ctx, struct color_theme *theme) return true; } - else if (strcmp(key, "alpha-mode") == 0) { + else if (streq(key, "alpha-mode")) { _Static_assert(sizeof(theme->alpha_mode) == sizeof(int), "enum is not 32-bit"); @@ -1529,6 +1529,16 @@ parse_color_theme(struct context *ctx, struct color_theme *theme) (int *)&theme->alpha_mode); } + else if (streq(key, "dim-blend-towards")) { + _Static_assert(sizeof(theme->dim_blend_towards) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"black", "white", NULL}, + (int *)&theme->dim_blend_towards); + } + else { LOG_CONTEXTUAL_ERR("not valid option"); return false; @@ -3428,6 +3438,7 @@ config_load(struct config *conf, const char *conf_path, .flash_alpha = 0x7fff, .alpha = 0xffff, .alpha_mode = ALPHA_MODE_DEFAULT, + .dim_blend_towards = DIM_BLEND_TOWARDS_BLACK, .selection_fg = 0x80000000, /* Use default bg */ .selection_bg = 0x80000000, /* Use default fg */ .cursor = { @@ -3523,6 +3534,8 @@ config_load(struct config *conf, const char *conf_path, memcpy(conf->colors.table, default_color_table, sizeof(default_color_table)); memcpy(conf->colors.sixel, default_sixel_colors, sizeof(default_sixel_colors)); memcpy(&conf->colors2, &conf->colors, sizeof(conf->colors)); + conf->colors2.dim_blend_towards = DIM_BLEND_TOWARDS_WHITE; + parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers); tokenize_cmdline( diff --git a/config.h b/config.h index 5b7ff11e..37b3259f 100644 --- a/config.h +++ b/config.h @@ -145,6 +145,11 @@ struct color_theme { uint32_t dim[8]; uint32_t sixel[16]; + enum { + DIM_BLEND_TOWARDS_BLACK, + DIM_BLEND_TOWARDS_WHITE, + } dim_blend_towards; + enum { ALPHA_MODE_DEFAULT, ALPHA_MODE_MATCHING, diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 7b08d5d4..8697add2 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1031,7 +1031,8 @@ dark theme (since the default theme is dark). a color value, and a "dim" attribute. By default, foot implements this by blending the current color - with black. This is a generic approach that applies to both + with black or white, depending on what the *dim-blend-towards* + option is set to . This is a generic approach that applies to both colors from the 256-color palette, as well as 24-bit RGB colors. You can change this behavior by setting the *dimN* options. When @@ -1086,6 +1087,14 @@ dark theme (since the default theme is dark). Default: _default_ +*dim-blend-towards* + Which color to blend towards when "auto" dimming a color (see + *dim0*..*dim7* above). One of *black* or *white*. Blending towards + black makes the text darker, while blending towards white makes it + whiter (but still dimmer than normal text). + + Default: _black_ (*colors*), _white_ (*colors2*) + *selection-foreground*, *selection-background* Foreground (text) and background color to use in selected text. Default: _inverse foreground/background_. @@ -1124,7 +1133,8 @@ dark theme (since the default theme is dark). # SECTION: colors2 This section defines an alternative color theme. It has the exact same -keys as the *colors* section. The default values are the same. +keys as the *colors* section. The default values are the same, except +for *dim-blend-towards*, which defaults to *white* instead. Note that values are not inherited. That is, if you set a value in *colors*, but not in *colors2*, the value from *colors* is not diff --git a/foot.ini b/foot.ini index 44ed5785..2d170489 100644 --- a/foot.ini +++ b/foot.ini @@ -132,6 +132,7 @@ # bright7=ffffff # bright white ## dimmed colors (see foot.ini(5) man page) +# dim-blend-towards=black # dim0=<not set> # ... # dim7=<not-set> @@ -170,6 +171,8 @@ [colors2] # Alternative color theme, see man page foot.ini(5) +# Same builtin defaults as [color], except for: +# dim-blend-towards=white [csd] # preferred=server diff --git a/render.c b/render.c index 35752125..fd721395 100644 --- a/render.c +++ b/render.c @@ -312,7 +312,14 @@ color_dim(const struct terminal *term, uint32_t color) } } - return color_blend_towards(color, 0x00000000, conf->dim.amount); + const struct color_theme *theme = term->colors.active_theme == COLOR_THEME1 + ? &conf->colors + : &conf->colors2; + + return color_blend_towards( + color, + theme->dim_blend_towards == DIM_BLEND_TOWARDS_BLACK ? 0x00000000 : 0x00ffffff, + conf->dim.amount); } static inline uint32_t diff --git a/tests/test-config.c b/tests/test-config.c index 64b61540..c442e700 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -753,6 +753,11 @@ test_section_colors(void) (int []){ALPHA_MODE_DEFAULT, ALPHA_MODE_MATCHING, ALPHA_MODE_ALL}, (int *)&conf.colors.alpha_mode); + test_enum(&ctx, &parse_section_colors, "dim-blend-towards", 2, + (const char *[]){"black", "white"}, + (int []){DIM_BLEND_TOWARDS_BLACK, DIM_BLEND_TOWARDS_WHITE}, + (int *)&conf.colors.dim_blend_towards); + for (size_t i = 0; i < 255; i++) { char key_name[4]; sprintf(key_name, "%zu", i); From 96605bf52fa3794ee2f13739831ad92229b4b3a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 11 Oct 2025 10:05:26 +0200 Subject: [PATCH 1260/1323] extract: number of spaces after the tab shouldn't include the tab cell itself This fixes an off by one, where we sometimes "ate" an extra space when extracting contents with tabs. This happened if the tab (and its subsequent spaces) were followed by an additional space. Closes #2194 --- CHANGELOG.md | 3 +++ extract.c | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76183459..3935889a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,8 +98,11 @@ * URL labels misplaces when URL contains double-width characters ([#2179][2179]). +* One space too much consumed when copying (or pipe:ing) contents with + tabs ([#2194][2194]) [2179]: https://codeberg.org/dnkl/foot/issues/2179 +[2194]: https://codeberg.org/dnkl/foot/issues/2194 ### Security diff --git a/extract.c b/extract.c index 31c32248..cd9a0c95 100644 --- a/extract.c +++ b/extract.c @@ -256,8 +256,8 @@ extract_one(const struct terminal *term, const struct row *row, } } - xassert(next_tab_stop >= col); - ctx->tab_spaces_left = next_tab_stop - col; + if (next_tab_stop > col) + ctx->tab_spaces_left = next_tab_stop - col - 1; } } From dbf18ba444e728ecddcc0e15fc44cc1fc0590b53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 15 Oct 2025 09:41:52 +0200 Subject: [PATCH 1261/1323] wayland: always render a new frame after a fullscreen change This is needed, since we disable alpha in fullscreen, and since we use different image buffer formats (XRGB vs. ARGB) when we have alpha vs. when we don't (and fullscreen always disables alpha). Normally, this happens anyway, as the window is resized when going in or out from fullscreen. But, it's technically possible for a compositor to change an application's fullscreen state without resizing the window. --- CHANGELOG.md | 4 ++++ wayland.c | 27 +++++++++++++++++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3935889a..ed8dff69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,10 @@ ([#2179][2179]). * One space too much consumed when copying (or pipe:ing) contents with tabs ([#2194][2194]) +* Ensure we render a new frame when changing fullscreen state. Before, + this was automatically done if the window was also resized. But, it + is possible for a compositor to change an application's fullscreen + state without resizing the window. [2179]: https://codeberg.org/dnkl/foot/issues/2179 [2194]: https://codeberg.org/dnkl/foot/issues/2194 diff --git a/wayland.c b/wayland.c index 59b2a33e..bac087fb 100644 --- a/wayland.c +++ b/wayland.c @@ -1097,10 +1097,6 @@ xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, else if (csd_was_enabled && !enable_csd) csd_destroy(win); - /* Update opaque region if fullscreen state changed */ - if (was_fullscreen != win->is_fullscreen) - wayl_win_alpha_changed(win); - if (enable_csd && new_width > 0 && new_height > 0) { if (wayl_win_csd_titlebar_visible(win)) new_height -= win->term->conf->csd.title_height; @@ -1139,11 +1135,26 @@ xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, else term_visual_focus_out(term); - if (!resized && !term->render.pending.grid) { + /* + * Update opaque region if fullscreen state changed, also need to + * render, since we use different buffer types with and without + * alpha + */ + if (was_fullscreen != win->is_fullscreen) { + wayl_win_alpha_changed(win); + render_refresh(term); + } + + const bool will_render_soon = resized || + term->render.refresh.grid || + term->render.pending.grid; + + if (!will_render_soon) { /* - * If we didn't resize, we won't be committing a new surface - * anytime soon. Some compositors require a commit in - * combination with an ack - make them happy. + * If we didn't resize, and aren't refreshing for other + * reasons, we won't be committing a new surface anytime + * soon. Some compositors require a commit in combination with + * an ack - make them happy. */ wl_surface_commit(win->surface.surf); } From 612adda3842fcdbbc9b7e1e2f46be02a224da1eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 16 Oct 2025 08:45:07 +0200 Subject: [PATCH 1262/1323] render: don't warn about immediate buffer release if pre-apply-damage has been activated --- render.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.c b/render.c index fd721395..1d0f08af 100644 --- a/render.c +++ b/render.c @@ -3372,7 +3372,7 @@ grid_render(struct terminal *term) { LOG_INFO("enabling pre-applied frame damage"); term->render.preapply_last_frame_damage = true; - } else if (!have_warned) { + } else if (!have_warned && !term->render.preapply_last_frame_damage) { LOG_WARN("compositor is not releasing buffers immediately; " "expect lower rendering performance"); have_warned = true; From dc5a921d2c5561f6d8f857ab3483e2fd1c59026f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 16 Oct 2025 08:46:36 +0200 Subject: [PATCH 1263/1323] changelog: prepare for 1.25.0 --- CHANGELOG.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed8dff69..ce7d318d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.25.0](#1-25-0) * [1.24.0](#1-24-0) * [1.23.1](#1-23-1) * [1.23.0](#1-23-0) @@ -66,7 +66,8 @@ * [1.2.0](#1-2-0) -## Unreleased +## 1.25.0 + ### Added * Performance increased and input latency decreased on compositors @@ -92,8 +93,6 @@ [2182]: https://codeberg.org/dnkl/foot/issues/2182 -### Deprecated -### Removed ### Fixed * URL labels misplaces when URL contains double-width characters @@ -109,9 +108,11 @@ [2194]: https://codeberg.org/dnkl/foot/issues/2194 -### Security ### Contributors +* Charalampos Mitrodimas +* Matthias Heyman + ## 1.24.0 From b44a62724cd51c7fecdfd9d1b41a3691b11a4c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 16 Oct 2025 08:46:58 +0200 Subject: [PATCH 1264/1323] meson: bump version to 1.25.0 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 305df13c..a1d0104d 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.24.0', + version: '1.25.0', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From 82e75851e41fd245dc08e71f1618ae4aeede7fb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 16 Oct 2025 08:50:31 +0200 Subject: [PATCH 1265/1323] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce7d318d..d2eebef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.25.0](#1-25-0) * [1.24.0](#1-24-0) * [1.23.1](#1-23-1) @@ -66,6 +67,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.25.0 ### Added From 558760446932f0f22c794084285cf998462dd706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 18 Oct 2025 08:23:53 +0200 Subject: [PATCH 1266/1323] input: keymap(): use a goto-label on error, to ensure we always close the keymap FD --- input.c | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/input.c b/input.c index fe5a8001..44a99e3b 100644 --- a/input.c +++ b/input.c @@ -576,23 +576,20 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, /* Verify keymap is in a format we understand */ switch ((enum wl_keyboard_keymap_format)format) { case WL_KEYBOARD_KEYMAP_FORMAT_NO_KEYMAP: - close(fd); - return; + goto err; case WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1: break; default: LOG_WARN("unrecognized keymap format: %u", format); - close(fd); - return; + goto err; } char *map_str = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); if (map_str == MAP_FAILED) { LOG_ERRNO("failed to mmap keyboard keymap"); - close(fd); - return; + goto err; } while (map_str[size - 1] == '\0') @@ -605,6 +602,8 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, } + munmap(map_str, size); + if (seat->kbd.xkb_keymap != NULL) { seat->kbd.xkb_state = xkb_state_new(seat->kbd.xkb_keymap); @@ -685,10 +684,10 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, seat->kbd.key_arrow_down = xkb_keymap_key_by_name(seat->kbd.xkb_keymap, "DOWN"); } - munmap(map_str, size); - close(fd); - key_binding_load_keymap(wayl->key_binding_manager, seat); + +err: + close(fd); } static void From 19466a21d8b7e580ef611ca56b81e7568fc44b37 Mon Sep 17 00:00:00 2001 From: Andrei <andreisva2023@gmail.com> Date: Fri, 24 Oct 2025 11:08:57 -0700 Subject: [PATCH 1267/1323] doc: foot.ini: fix typo --- doc/foot.ini.5.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 8697add2..2b57c467 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -2120,7 +2120,7 @@ any of these options. frame, foot proceeds with rendering the cells that has changed between the last frame and the new frame. - When this open is enabled, the changes between the last two frames + When this option is enabled, the changes between the last two frames are brought over to what will become the next frame before foot starts rendering the next frame. As soon as the compositor releases the previous buffer (typically right after foot has From 71de0c45bc38ee773f86bb4d48f96719c8abb568 Mon Sep 17 00:00:00 2001 From: c4llv07e <igor@c4llv07e.xyz> Date: Mon, 27 Oct 2025 13:24:07 +0300 Subject: [PATCH 1268/1323] char32: add helper functions to work with c32 case --- char32.c | 22 ++++++++++++++++++++++ char32.h | 15 +++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/char32.c b/char32.c index 827cef8d..3d6c2c78 100644 --- a/char32.c +++ b/char32.c @@ -53,6 +53,14 @@ UNITTEST xassert(c32cmp(U"b", U"a") > 0); } +UNITTEST +{ + xassert(c32ncmp(U"foo", U"foot", 3) == 0); + xassert(c32ncmp(U"foot", U"FOOT", 4) > 0); + xassert(c32ncmp(U"a", U"b", 1) < 0); + xassert(c32ncmp(U"bb", U"aa", 2) > 0); +} + UNITTEST { char32_t copy[16]; @@ -127,6 +135,20 @@ UNITTEST xassert(c32cmp(dst, U"foobar12345678") == 0); } +UNITTEST +{ + xassert(!isc32upper(U'a')); + xassert(isc32upper(U'A')); + xassert(!isc32upper(U'a')); +} + +UNITTEST +{ + xassert(hasc32upper(U"abc1A")); + xassert(!hasc32upper(U"abc1_aaa")); + xassert(!hasc32upper(U"")); +} + UNITTEST { char32_t *c = xc32dup(U"foobar"); diff --git a/char32.h b/char32.h index 6a5eb080..dcb412ce 100644 --- a/char32.h +++ b/char32.h @@ -20,6 +20,10 @@ static inline int c32cmp(const char32_t *s1, const char32_t *s2) { return wcscmp((const wchar_t *)s1, (const wchar_t *)s2); } +static inline int c32ncmp(const char32_t *s1, const char32_t *s2, size_t n) { + return wcsncmp((const wchar_t *)s1, (const wchar_t *)s2, n); +} + static inline char32_t *c32ncpy(char32_t *dst, const char32_t *src, size_t n) { return (char32_t *)wcsncpy((wchar_t *)dst, (const wchar_t *)src, n); } @@ -60,6 +64,10 @@ static inline char32_t toc32upper(char32_t c) { return (char32_t)towupper((wint_t)c); } +static inline bool isc32upper(char32_t c32) { + return iswupper((wint_t)c32); +} + static inline bool isc32space(char32_t c32) { return iswspace((wint_t)c32); } @@ -72,6 +80,13 @@ static inline bool isc32graph(char32_t c32) { return iswgraph((wint_t)c32); } +static inline bool hasc32upper(const char32_t *s) { + for (int i = 0; s[i] != '\0'; i++) { + if (isc32upper(s[i])) return true; + } + return false; +} + static inline int c32width(char32_t c) { #if defined(FOOT_GRAPHEME_CLUSTERING) return utf8proc_charwidth((utf8proc_int32_t)c); From 5ae4955e834831b5680baaad7c4f974e407ffb7a Mon Sep 17 00:00:00 2001 From: c4llv07e <igor@c4llv07e.xyz> Date: Mon, 27 Oct 2025 13:25:48 +0300 Subject: [PATCH 1269/1323] search: use case insensitive search only if there's no uppercase in search --- CHANGELOG.md | 2 ++ search.c | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2eebef8..81f7a168 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,8 @@ e.g. integrated graphics ([#2182][2182]). * Jump label colors in the modus-operandi theme, for improved readability. +* Scrollback search is now case sensitive when the search string + contains at least one upper case character. [2182]: https://codeberg.org/dnkl/foot/issues/2182 diff --git a/search.c b/search.c index dda84e6d..5a2b6236 100644 --- a/search.c +++ b/search.c @@ -283,8 +283,13 @@ matches_cell(const struct terminal *term, const struct cell *cell, size_t search if (composed == NULL && base == 0 && term->search.buf[search_ofs] == U' ') return 1; - if (c32ncasecmp(&base, &term->search.buf[search_ofs], 1) != 0) - return -1; + if (hasc32upper(term->search.buf)) { + if (c32ncmp(&base, &term->search.buf[search_ofs], 1) != 0) + return -1; + } else { + if (c32ncasecmp(&base, &term->search.buf[search_ofs], 1) != 0) + return -1; + } if (composed != NULL) { if (search_ofs + composed->count > term->search.len) From 143f220527a0cffebcecbd5eaa0dccd5e009122b Mon Sep 17 00:00:00 2001 From: Ronan Pigott <ronan@rjp.ie> Date: Fri, 31 Oct 2025 15:11:53 -0700 Subject: [PATCH 1270/1323] search: do not emit composing keys When we are in the composing state for XCompose key sequences, we should not add the compose component keys to the search buffer. --- CHANGELOG.md | 4 ++++ search.c | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81f7a168..f00b5d15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,10 @@ ### Deprecated ### Removed ### Fixed + +* Search mode: composing keys not ignored. + + ### Security ### Contributors diff --git a/search.c b/search.c index 5a2b6236..5228bf61 100644 --- a/search.c +++ b/search.c @@ -1484,7 +1484,8 @@ search_input(struct seat *seat, struct terminal *term, count = xkb_compose_state_get_utf8( seat->kbd.xkb_compose_state, (char *)buf, sizeof(buf)); xkb_compose_state_reset(seat->kbd.xkb_compose_state); - } else if (compose_status == XKB_COMPOSE_CANCELLED) { + } else if (compose_status == XKB_COMPOSE_CANCELLED || + compose_status == XKB_COMPOSE_COMPOSING) { count = 0; } else { count = xkb_state_key_get_utf8( From 9728ada0289c446585325a74ded9c94958d9ab24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 30 Oct 2025 06:29:51 +0100 Subject: [PATCH 1271/1323] csi: focus mode (private mode 1004): send focus event immediate, when enabled This lets the application now the current state, without having to wait for the user to switch focus. Fixes #2202 --- CHANGELOG.md | 8 ++++++++ csi.c | 2 ++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f00b5d15..21d2c5f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,14 @@ ## Unreleased ### Added ### Changed + +* When enabling _"focus mode"_ (private mode 1004), foot now sends a + focus event immediately, to inform the application what the current + state is ([#2202][2202]). + +[2202]: https://codeberg.org/dnkl/foot/issues/2202 + + ### Deprecated ### Removed ### Fixed diff --git a/csi.c b/csi.c index 437fd8bc..c5f616ac 100644 --- a/csi.c +++ b/csi.c @@ -422,6 +422,8 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) case 1004: term->focus_events = enable; + if (enable) + term_to_slave(term, term->kbd_focus ? "\033[I" : "\033[O", 3); break; case 1005: From 1fce0e69f5ec2c24d518917c0538a4d762b8a1e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 1 Nov 2025 08:12:52 +0100 Subject: [PATCH 1272/1323] changelog: case sensitive scrollback search: move to correct release --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21d2c5f5..e7f71ed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,8 @@ * When enabling _"focus mode"_ (private mode 1004), foot now sends a focus event immediately, to inform the application what the current state is ([#2202][2202]). +* Scrollback search is now case sensitive when the search string + contains at least one upper case character. [2202]: https://codeberg.org/dnkl/foot/issues/2202 @@ -112,8 +114,6 @@ e.g. integrated graphics ([#2182][2182]). * Jump label colors in the modus-operandi theme, for improved readability. -* Scrollback search is now case sensitive when the search string - contains at least one upper case character. [2182]: https://codeberg.org/dnkl/foot/issues/2182 From 5cb8ff2e9c51c528589e5e1ef78b9a6bd554fc0d Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger <aclopte@gmail.com> Date: Fri, 7 Nov 2025 07:32:11 +0100 Subject: [PATCH 1273/1323] Fix assertion failure triple-clicking line with quote in last column By default, triple-click tries to select quoted strings within a logical line. This also works if the line spans multiple screen lines. If there is a quote character in the last column: printf %"$COLUMNS"s \'; printf wrapped; sleep inf and I triple-click on the following soft-wrapped line, there's an assertion failure because the column next to the quote is out of range. The quote position has been found by walking at least one cell backwards from "pos". This means that if the quote position is in the very last column, there must be a row below. Also move the assertion to be a pre-condition, though that's debatable. --- selection.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/selection.c b/selection.c index d7aa617a..f07396a5 100644 --- a/selection.c +++ b/selection.c @@ -19,6 +19,7 @@ #include "char32.h" #include "commands.h" #include "config.h" +#include "debug.h" #include "extract.h" #include "grid.h" #include "misc.h" @@ -558,9 +559,15 @@ selection_find_quote_left(struct terminal *term, struct coord *pos, if (*quote_char == '\0' ? (wc == '"' || wc == '\'') : wc == *quote_char) { - pos->row = next_row; - pos->col = next_col + 1; - xassert(pos->col < term->cols); + xassert(next_col + 1 <= term->cols); + if (next_col + 1 == term->cols) { + xassert(next_row < pos->row); + pos->row = next_row + 1; + pos->col = 0; + } else { + pos->row = next_row; + pos->col = next_col + 1; + } *quote_char = wc; return true; From c9abab08079a0b6eeb4f3fc9beb80eb5fd621730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 12 Nov 2025 07:46:34 +0100 Subject: [PATCH 1274/1323] changelog: triple-click when there's a quote in the last column --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7f71ed5..654ba94f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,8 @@ ### Fixed * Search mode: composing keys not ignored. +* Crash when triple-clicking a soft-wrapped line and there is a quote + character in the last column. ### Security From fc9625678fc7e295e7a7e03a5d47db21d4be3010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 12 Nov 2025 11:04:25 +0100 Subject: [PATCH 1275/1323] config: add toplevel-tag=TAG Add support for the new xdg-toplevel-tag-v1 Wayland protocol, by exposing a new config option, `toplevel-tag`, and a corresponding command option, `--toplevel-tag` (in both `foot` and `footclient`). This can help the compositor with session management, or custom window rules. Closes #2212 --- CHANGELOG.md | 9 +++++++++ client.c | 12 ++++++++++++ completions/bash/foot | 5 +++-- completions/bash/footclient | 5 +++-- completions/fish/foot.fish | 1 + completions/fish/footclient.fish | 1 + completions/zsh/_foot | 1 + completions/zsh/_footclient | 1 + config.c | 6 ++++++ config.h | 1 + doc/foot.1.scd | 5 +++++ doc/foot.ini.5.scd | 5 +++++ doc/footclient.1.scd | 5 +++++ foot-features.c | 6 ++++++ main.c | 7 +++++++ meson.build | 7 ++++++- tests/test-config.c | 1 + wayland.c | 33 +++++++++++++++++++++++++++++++- wayland.h | 8 ++++++++ 19 files changed, 113 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 654ba94f..a293c721 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,15 @@ ## Unreleased ### Added + +* `toplevel-tag` option (and `--toplevel-tag` command line options to + `foot` and `footclient`), allowing you to set a custom toplevel + tag. The compositor must implement the new `xdg-toplevel-tag-v1` + Wayland protocol ([#2212][2212]). + +[2212]: https://codeberg.org/dnkl/foot/issues/2212 + + ### Changed * When enabling _"focus mode"_ (private mode 1004), foot now sends a diff --git a/client.c b/client.c index aa5302be..85bc7fda 100644 --- a/client.c +++ b/client.c @@ -76,6 +76,7 @@ print_usage(const char *prog_name) " -t,--term=TERM value to set the environment variable TERM to (" FOOT_DEFAULT_TERM ")\n" " -T,--title=TITLE initial window title (foot)\n" " -a,--app-id=ID window application ID (foot)\n" + " --toplevel-tag=TAG set a custom toplevel tag\n" " -w,--window-size-pixels=WIDTHxHEIGHT initial width and height, in pixels\n" " -W,--window-size-chars=WIDTHxHEIGHT initial width and height, in characters\n" " -m,--maximized start in maximized mode\n" @@ -137,6 +138,10 @@ send_string_list(int fd, const string_list_t *string_list) return true; } +enum { + TOPLEVEL_TAG_OPTION = CHAR_MAX + 1, +}; + int main(int argc, char *const *argv) { @@ -151,6 +156,7 @@ main(int argc, char *const *argv) {"term", required_argument, NULL, 't'}, {"title", required_argument, NULL, 'T'}, {"app-id", required_argument, NULL, 'a'}, + {"toplevel-tag", required_argument, NULL, TOPLEVEL_TAG_OPTION}, {"window-size-pixels", required_argument, NULL, 'w'}, {"window-size-chars", required_argument, NULL, 'W'}, {"maximized", no_argument, NULL, 'm'}, @@ -220,6 +226,12 @@ main(int argc, char *const *argv) goto err; break; + case TOPLEVEL_TAG_OPTION: + snprintf(buf, sizeof(buf), "toplevel-tag=%s", optarg); + if (!push_string(&overrides, buf, &total_len)) + goto err; + break; + case 'L': if (!push_string(&overrides, "login-shell=yes", &total_len)) goto err; diff --git a/completions/bash/foot b/completions/bash/foot index 25aa2c49..e27be2fa 100644 --- a/completions/bash/foot +++ b/completions/bash/foot @@ -6,6 +6,7 @@ _foot() local cur prev flags word commands match previous_words i offset flags=( "--app-id" + "--toplevel-tag" "--check-config" "--config" "--font" @@ -40,7 +41,7 @@ _foot() for word in "${previous_words[@]}" ; do match=$(printf "$commands" | grep -Fx "$word" 2>/dev/null) if [[ ! -z "$match" ]] ; then - if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--config|--font|--log-level|--pty|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then + if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--toplevel-tag|--config|--font|--log-level|--pty|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then (( i++ )) continue fi @@ -75,7 +76,7 @@ _foot() COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) ;; --log-colorize|-l) COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) ;; - --app-id|--help|--override|--pty|--title|--version|--window-size-chars|--window-size-pixels|--check-config|-[ahoTvWwC]) + --app-id|--toplevel-tag|--help|--override|--pty|--title|--version|--window-size-chars|--window-size-pixels|--check-config|-[ahoTvWwC]) # Don't autocomplete for these flags : ;; *) diff --git a/completions/bash/footclient b/completions/bash/footclient index 62abdd65..c7f1df4e 100644 --- a/completions/bash/footclient +++ b/completions/bash/footclient @@ -6,6 +6,7 @@ _footclient() local cur prev flags word commands match previous_words i offset flags=( "--app-id" + "--toplevel-tag" "--fullscreen" "--help" "--hold" @@ -35,7 +36,7 @@ _footclient() for word in "${previous_words[@]}" ; do match=$(printf "$commands" | grep -Fx "$word" 2>/dev/null) if [[ ! -z "$match" ]] ; then - if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--log-level|--server-socket|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then + if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--toplevel-tag|--log-level|--server-socket|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then (( i++ )) continue fi @@ -67,7 +68,7 @@ _footclient() COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) ;; --log-colorize|-l) COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) ;; - --app-id|--help|--override|--title|--version|--window-size-chars|--window-size-pixels|-[ahoTvWw]) + --app-id|--toplevel-tag|--help|--override|--title|--version|--window-size-chars|--window-size-pixels|-[ahoTvWw]) # Don't autocomplete for these flags : ;; *) diff --git a/completions/fish/foot.fish b/completions/fish/foot.fish index 0053d18d..21b42d3d 100644 --- a/completions/fish/foot.fish +++ b/completions/fish/foot.fish @@ -6,6 +6,7 @@ complete -c foot -x -s f -l font -a "(fc-list : family | sed 's/,/ complete -c foot -x -s t -l term -a '(find /usr/share/terminfo -type f -printf "%f\n")' -d "value to set the environment variable TERM to (foot)" complete -c foot -x -s T -l title -d "initial window title" complete -c foot -x -s a -l app-id -d "value to set the app-id property on the Wayland window to (foot)" +complete -c foot -x -l toplevel-tag -d "value to set the toplevel-tag property on the Wayland window to" complete -c foot -s m -l maximized -d "start in maximized mode" complete -c foot -s F -l fullscreen -d "start in fullscreen mode" complete -c foot -s L -l login-shell -d "start shell as a login shell" diff --git a/completions/fish/footclient.fish b/completions/fish/footclient.fish index df3e1273..03624796 100644 --- a/completions/fish/footclient.fish +++ b/completions/fish/footclient.fish @@ -2,6 +2,7 @@ complete -c footclient -x -a "(__fish_complete_subcom complete -c footclient -x -s t -l term -a '(find /usr/share/terminfo -type f -printf "%f\n")' -d "value to set the environment variable TERM to (foot)" complete -c footclient -x -s T -l title -d "initial window title" complete -c footclient -x -s a -l app-id -d "value to set the app-id property on the Wayland window to (foot)" +complete -c footclient -x -l toplevel-tag -d "value to set the toplevel-tag property on the Wayland window to" complete -c footclient -s m -l maximized -d "start in maximized mode" complete -c footclient -s F -l fullscreen -d "start in fullscreen mode" complete -c footclient -s L -l login-shell -d "start shell as a login shell" diff --git a/completions/zsh/_foot b/completions/zsh/_foot index 2a0dc7b0..0fd83b3c 100644 --- a/completions/zsh/_foot +++ b/completions/zsh/_foot @@ -9,6 +9,7 @@ _arguments \ '(-t --term)'{-t,--term}'[value to set the environment variable TERM to (foot)]:term:->terms' \ '(-T --title)'{-T,--title}'[initial window title]:()' \ '(-a --app-id)'{-a,--app-id}'[value to set the app-id property on the Wayland window to (foot)]:()' \ + '--toplevel-tag=[value to set the toplevel-tag property on the Wayland window to]:()' \ '(-m --maximized)'{-m,--maximized}'[start in maximized mode]' \ '(-F --fullscreen)'{-F,--fullscreen}'[start in fullscreen mode]' \ '(-L --login-shell)'{-L,--login-shell}'[start shell as a login shell]' \ diff --git a/completions/zsh/_footclient b/completions/zsh/_footclient index c14d65d5..12f29d7a 100644 --- a/completions/zsh/_footclient +++ b/completions/zsh/_footclient @@ -5,6 +5,7 @@ _arguments \ '(-t --term)'{-t,--term}'[value to set the environment variable TERM to (foot)]:term:->terms' \ '(-T --title)'{-T,--title}'[initial window title]:()' \ '(-a --app-id)'{-a,--app-id}'[value to set the app-id property on the Wayland window to (foot)]:()' \ + '--toplevel-tag=[value to set the toplevel-tag property on the Wayland window to]:()' \ '(-m --maximized)'{-m,--maximized}'[start in maximized mode]' \ '(-F --fullscreen)'{-F,--fullscreen}'[start in fullscreen mode]' \ '(-L --login-shell)'{-L,--login-shell}'[start shell as a login shell]' \ diff --git a/config.c b/config.c index 4449d9c2..515b088c 100644 --- a/config.c +++ b/config.c @@ -923,6 +923,9 @@ parse_section_main(struct context *ctx) else if (streq(key, "app-id")) return value_to_str(ctx, &conf->app_id); + else if (streq(key, "toplevel-tag")) + return value_to_str(ctx, &conf->toplevel_tag); + else if (streq(key, "initial-window-size-pixels")) { if (!value_to_dimensions(ctx, &conf->size.width, &conf->size.height)) return false; @@ -3371,6 +3374,7 @@ config_load(struct config *conf, const char *conf_path, .shell = get_shell(), .title = xstrdup("foot"), .app_id = (as_server ? xstrdup("footclient") : xstrdup("foot")), + .toplevel_tag = xstrdup(""), .word_delimiters = xc32dup(U",│`|:\"'()[]{}<>"), .size = { .type = CONF_SIZE_PX, @@ -3823,6 +3827,7 @@ config_clone(const struct config *old) conf->shell = xstrdup(old->shell); conf->title = xstrdup(old->title); conf->app_id = xstrdup(old->app_id); + conf->toplevel_tag = xstrdup(old->toplevel_tag); conf->word_delimiters = xc32dup(old->word_delimiters); conf->scrollback.indicator.text = xc32dup(old->scrollback.indicator.text); conf->server_socket_path = xstrdup(old->server_socket_path); @@ -3922,6 +3927,7 @@ config_free(struct config *conf) free(conf->shell); free(conf->title); free(conf->app_id); + free(conf->toplevel_tag); free(conf->word_delimiters); spawn_template_free(&conf->bell.command); free(conf->scrollback.indicator.text); diff --git a/config.h b/config.h index 37b3259f..fc5e290e 100644 --- a/config.h +++ b/config.h @@ -219,6 +219,7 @@ struct config { char *shell; char *title; char *app_id; + char *toplevel_tag; char32_t *word_delimiters; bool login_shell; bool locked_title; diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 8d968a6e..cbf22f5b 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -67,6 +67,11 @@ the foot command line Value to set the *app-id* property on the Wayland window to. Default: _foot_ (normal mode), or _footclient_ (server mode). +*toplevel-tag*=_TAG_ + Value to set the *toplevel-tag* property on the Wayland window + to. The compositor can use this value for session management, + window rules etc. Default: _not set_ + *-m*,*--maximized* Start in maximized mode. If both *--maximized* and *--fullscreen* are specified, the _last_ one takes precedence. diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 2b57c467..c9782895 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -429,6 +429,11 @@ empty string to be set, but it must be quoted: *KEY=""*) apply window management rules. Default: _foot_ (normal mode), or _footclient_ (server mode). +*toplevel-tag* + Value to set the *toplevel-tag* property on the Wayland window + to. The compositor can use this value for session management, + window rules etc. Default: _not set_ + *bold-text-in-bright* Semi-boolean. When enabled, bold text is rendered in a brighter color (in addition to using a bold font). The color is brightened diff --git a/doc/footclient.1.scd b/doc/footclient.1.scd index e4f6d350..edf3e9f3 100644 --- a/doc/footclient.1.scd +++ b/doc/footclient.1.scd @@ -33,6 +33,11 @@ terminal has terminated. Value to set the *app-id* property on the Wayland window to. Default: _foot_ (normal mode), or _footclient_ (server mode). +*toplevel-tag*=_TAG_ + Value to set the *toplevel-tag* property on the Wayland window + to. The compositor can use this value for session management, + window rules etc. Default: _not set_ + *-w*,*--window-size-pixels*=_WIDTHxHEIGHT_ Set initial window width and height, in pixels. Default: _700x500_. diff --git a/foot-features.c b/foot-features.c index 1b5bf7fd..f701533c 100644 --- a/foot-features.c +++ b/foot-features.c @@ -22,6 +22,12 @@ const char version_and_features[] = " -graphemes" #endif +#if defined(HAVE_XDG_TOPLEVEL_TAG) + " +toplevel-tag" +#else + " -toplevel-tag" +#endif + #if !defined(NDEBUG) " +assertions" #else diff --git a/main.c b/main.c index b6a0d825..b933e1c1 100644 --- a/main.c +++ b/main.c @@ -84,6 +84,7 @@ print_usage(const char *prog_name) " -t,--term=TERM value to set the environment variable TERM to (" FOOT_DEFAULT_TERM ")\n" " -T,--title=TITLE initial window title (foot)\n" " -a,--app-id=ID window application ID (foot)\n" + " --toplevel-tag=TAG set a custom toplevel tag\n" " -m,--maximized start in maximized mode\n" " -F,--fullscreen start in fullscreen mode\n" " -L,--login-shell start shell as a login shell\n" @@ -185,6 +186,7 @@ sanitize_signals(void) enum { PTY_OPTION = CHAR_MAX + 1, + TOPLEVEL_TAG_OPTION = CHAR_MAX + 2, }; int @@ -214,6 +216,7 @@ main(int argc, char *const *argv) {"term", required_argument, NULL, 't'}, {"title", required_argument, NULL, 'T'}, {"app-id", required_argument, NULL, 'a'}, + {"toplevel-tag", required_argument, NULL, TOPLEVEL_TAG_OPTION}, {"login-shell", no_argument, NULL, 'L'}, {"working-directory", required_argument, NULL, 'D'}, {"font", required_argument, NULL, 'f'}, @@ -285,6 +288,10 @@ main(int argc, char *const *argv) tll_push_back(overrides, xstrjoin("app-id=", optarg)); break; + case TOPLEVEL_TAG_OPTION: + tll_push_back(overrides, xstrjoin("toplevel-tag=", optarg)); + break; + case 'D': { struct stat st; if (stat(optarg, &st) < 0 || !(st.st_mode & S_IFDIR)) { diff --git a/meson.build b/meson.build index a1d0104d..aa8342ab 100644 --- a/meson.build +++ b/meson.build @@ -182,7 +182,12 @@ wl_proto_xml = [ wayland_protocols_datadir / 'staging/xdg-toplevel-icon/xdg-toplevel-icon-v1.xml', wayland_protocols_datadir / 'staging/xdg-system-bell/xdg-system-bell-v1.xml', wayland_protocols_datadir / 'staging/color-management/color-management-v1.xml', - ] +] + +if (wayland_protocols.version().version_compare('>=1.43')) + wl_proto_xml += [wayland_protocols_datadir / 'staging/xdg-toplevel-tag/xdg-toplevel-tag-v1.xml'] + add_project_arguments('-DHAVE_XDG_TOPLEVEL_TAG=1', language: 'c') +endif foreach prot : wl_proto_xml wl_proto_headers += custom_target( diff --git a/tests/test-config.c b/tests/test-config.c index c442e700..268733db 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -482,6 +482,7 @@ test_section_main(void) test_string(&ctx, &parse_section_main, "shell", &conf.shell); test_string(&ctx, &parse_section_main, "term", &conf.term); test_string(&ctx, &parse_section_main, "app-id", &conf.app_id); + test_string(&ctx, &parse_section_main, "toplevel-tag", &conf.toplevel_tag); test_string(&ctx, &parse_section_main, "utmp-helper", &conf.utmp_helper_path); test_c32string(&ctx, &parse_section_main, "word-delimiters", &conf.word_delimiters); diff --git a/wayland.c b/wayland.c index bac087fb..6785c52d 100644 --- a/wayland.c +++ b/wayland.c @@ -1548,6 +1548,17 @@ handle_global(void *data, struct wl_registry *registry, wayl->color_management.manager, &color_manager_listener, wayl); } +#if defined(HAVE_XDG_TOPLEVEL_TAG) + else if (streq(interface, xdg_toplevel_tag_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->toplevel_tag_manager = wl_registry_bind( + wayl->registry, name, &xdg_toplevel_tag_manager_v1_interface, required); + } +#endif + #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED else if (streq(interface, zwp_text_input_manager_v3_interface.name)) { const uint32_t required = 1; @@ -1791,7 +1802,7 @@ wayl_init(struct fdm *fdm, struct key_binding_manager *key_binding_manager, } if (wayl->toplevel_icon_manager == NULL) { - LOG_WARN("compositor does not implement the XDG toplevel icon protocol"); + LOG_WARN("compositor does not implement the xdg-toplevel-icon protocol"); } #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED @@ -1870,6 +1881,11 @@ wayl_destroy(struct wayland *wayl) zwp_text_input_manager_v3_destroy(wayl->text_input_manager); #endif +#if defined(HAVE_XDG_TOPLEVEL_TAG) + if (wayl->toplevel_tag_manager != NULL) + xdg_toplevel_tag_manager_v1_destroy(wayl->toplevel_tag_manager); +#endif + if (wayl->color_management.img_description != NULL) wp_image_description_v1_destroy(wayl->color_management.img_description); if (wayl->color_management.manager != NULL) @@ -1995,6 +2011,21 @@ wayl_win_init(struct terminal *term, const char *token) xdg_toplevel_set_app_id(win->xdg_toplevel, conf->app_id); +#if defined(HAVE_XDG_TOPLEVEL_TAG) + if (conf->toplevel_tag != NULL && conf->toplevel_tag[0] != '\0') { + if (wayl->toplevel_tag_manager != NULL) { + xdg_toplevel_tag_manager_v1_set_toplevel_tag( + wayl->toplevel_tag_manager, win->xdg_toplevel, conf->toplevel_tag); + + /* TODO: the description is recommended to be the tag, but translated */ + xdg_toplevel_tag_manager_v1_set_toplevel_description( + wayl->toplevel_tag_manager, win->xdg_toplevel, conf->toplevel_tag); + } else { + LOG_WARN("compositor does not implement the xdg-toplevel-tag protocol"); + } + } +#endif + if (wayl->toplevel_icon_manager != NULL) { const char *app_id = term->app_id != NULL ? term->app_id : term->conf->app_id; diff --git a/wayland.h b/wayland.h index eb1c35a3..140c2058 100644 --- a/wayland.h +++ b/wayland.h @@ -23,6 +23,10 @@ #include <xdg-system-bell-v1.h> #include <xdg-toplevel-icon-v1.h> +#if defined(HAVE_XDG_TOPLEVEL_TAG) + #include <xdg-toplevel-tag-v1.h> +#endif + #include <fcft/fcft.h> #include <tllist.h> @@ -481,6 +485,10 @@ struct wayland { struct wp_presentation *presentation; uint32_t presentation_clock_id; +#if defined(HAVE_XDG_TOPLEVEL_TAG) + struct xdg_toplevel_tag_manager_v1 *toplevel_tag_manager; +#endif + #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED struct zwp_text_input_manager_v3 *text_input_manager; #endif From be19ca2b2074c522be489d0f4760d48a09265045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 29 Nov 2025 09:47:22 +0100 Subject: [PATCH 1276/1323] client: add missing <limits.h> (for CHAR_MAX) Closes #2221 --- client.c | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/client.c b/client.c index 85bc7fda..befd3ab0 100644 --- a/client.c +++ b/client.c @@ -1,12 +1,13 @@ -#include <stdlib.h> -#include <stdio.h> -#include <string.h> -#include <stdint.h> -#include <stdbool.h> -#include <unistd.h> -#include <getopt.h> -#include <signal.h> #include <errno.h> +#include <getopt.h> +#include <limits.h> +#include <signal.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> #include <sys/socket.h> #include <sys/un.h> From 55f838869443bb34a502c238301e22b4d6f52073 Mon Sep 17 00:00:00 2001 From: Whyme Lyu <callme5long@gmail.com> Date: Mon, 1 Dec 2025 18:38:58 +0800 Subject: [PATCH 1277/1323] doc: remove duplicated ctrl+shift+w in foot(1) --- doc/foot.1.scd | 3 --- 1 file changed, 3 deletions(-) diff --git a/doc/foot.1.scd b/doc/foot.1.scd index cbf22f5b..60ba622b 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -257,9 +257,6 @@ These keyboard shortcuts affect the search selection: *ctrl*+*shift*+*left* Extend current selection to the left to the last word boundary. -*ctrl*+*shift*+*w* - Extend the current selection to the right to the last whitespace. - *shift*+*down* Extend current selection down one line From 65bd79b77d0acf3fcf6be1ecfe5a4a3ce2e1151a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 10 Dec 2025 08:48:41 +0100 Subject: [PATCH 1278/1323] term: reverse-scroll: fix crash when viewport ends up outside the (new) scrollback If the viewport has been scrolled up, it is possible for a reverse-scroll (rin) to cause the viewport to point to lines outside the scrollback. This is an issue if the scrollback isn't full, since in that case, the viewport will contain NULL lines. This will potentially trigger assertions in a couple of different places. Example backtrace: #2 0x555555cd230c in bug ../../debug.c:44 #3 0x555555ad485e in grid_row_in_view ../../grid.h:83 #4 0x555555b15a89 in grid_render ../../render.c:3465 #5 0x555555b3b0ab in fdm_hook_refresh_pending_terminals ../../render.c:5165 #6 0x555555a74980 in fdm_poll ../../fdm.c:435 #7 0x555555ac2b85 in main ../../main.c:676 Detect when this happens, and force-move the viewport to ensure it is valid. Closes #2232 --- CHANGELOG.md | 4 ++++ terminal.c | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a293c721..77d7d772 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,6 +96,10 @@ * Search mode: composing keys not ignored. * Crash when triple-clicking a soft-wrapped line and there is a quote character in the last column. +* Crash when reverse-scrolling (terminfo capability `rin`) such that + the current viewport ends up outside the scrollback ([#2232][2232]). + +[2232]: https://codeberg.org/dnkl/foot/issues/2232 ### Security diff --git a/terminal.c b/terminal.c index 36f8513b..e70250d8 100644 --- a/terminal.c +++ b/terminal.c @@ -3165,11 +3165,17 @@ term_scroll_reverse_partial(struct terminal *term, sixel_scroll_down(term, rows); - bool view_follows = term->grid->view == term->grid->offset; + const bool view_follows = term->grid->view == term->grid->offset; term->grid->offset -= rows; term->grid->offset += term->grid->num_rows; term->grid->offset &= term->grid->num_rows - 1; + /* How many lines from the scrollback start is the current viewport? */ + const int view_sb_start_distance = grid_row_abs_to_sb( + term->grid, term->rows, term->grid->view); + const int offset_sb_start_distance = grid_row_abs_to_sb( + term->grid, term->rows, term->grid->offset); + xassert(term->grid->offset >= 0); xassert(term->grid->offset < term->grid->num_rows); @@ -3177,6 +3183,11 @@ term_scroll_reverse_partial(struct terminal *term, term_damage_scroll(term, DAMAGE_SCROLL_REVERSE, region, rows); selection_view_up(term, term->grid->offset); term->grid->view = term->grid->offset; + } else if (unlikely(view_sb_start_distance > offset_sb_start_distance)) { + /* Part of current view is being scrolled out */ + int new_view = term->grid->offset; + selection_view_up(term, new_view); + term->grid->view = new_view; } /* Bottom non-scrolling region */ @@ -3193,11 +3204,16 @@ term_scroll_reverse_partial(struct terminal *term, erase_line(term, row); } + if (unlikely(view_sb_start_distance > offset_sb_start_distance)) + term_damage_view(term); + term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row); #if defined(_DEBUG) for (int r = 0; r < term->rows; r++) xassert(grid_row(term->grid, r) != NULL); + for (int r = 0; r < term->rows; r++) + xassert(grid_row_in_view(term->grid, r) != NULL); #endif } From ac6d7660dd77c5418593a35cd623bf198ac93fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 10 Dec 2025 08:54:24 +0100 Subject: [PATCH 1279/1323] ci: codespell: ignore 'rin' --- .woodpecker.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 340ba241..843c9afc 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -14,7 +14,7 @@ steps: - 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 + - codespell -Lser,doas,zar,rin README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd - deactivate - name: subprojects From 6e533231b016684a32a1975ce2e33ae3ae38b4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 10 Dec 2025 09:39:51 +0100 Subject: [PATCH 1280/1323] term: mouse SGR mode: don't emit negative CSI values When reporting the column/row pixel value in mouse SGR mode, we emitted negative values when the cursor was being dragged outside the window. Unfortunately, negative values aren't allowed in CSI parameters, as '-' is an intermediate value. It was done this way, to be consistent with XTerm behavior. Allegedly, XTerm has changed its behavior in patch 404. With that in mind, and seeing that foot has never emitted negative values in any other mouse mode, let's stop emitting negative values in SGR mode too. Closes #2226 --- CHANGELOG.md | 3 +++ terminal.c | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77d7d772..85dc3762 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,8 +85,11 @@ state is ([#2202][2202]). * Scrollback search is now case sensitive when the search string contains at least one upper case character. +* Mouse tracking in SGR pixel mode no longer emits negative column/row + pixel values ([#2226][2226]). [2202]: https://codeberg.org/dnkl/foot/issues/2202 +[2226]: https://codeberg.org/dnkl/foot/issues/2226 ### Deprecated diff --git a/terminal.c b/terminal.c index e70250d8..3749416b 100644 --- a/terminal.c +++ b/terminal.c @@ -3420,10 +3420,13 @@ report_mouse_click(struct terminal *term, int encoded_button, int row, int col, encoded_button, col + 1, row + 1, release ? 'm' : 'M'); break; - case MOUSE_SGR_PIXELS: + case MOUSE_SGR_PIXELS: { + const int bounded_col = max(col_pixels, 0); + const int bounded_row = max(row_pixels, 0); snprintf(response, sizeof(response), "\033[<%d;%d;%d%c", - encoded_button, col_pixels + 1, row_pixels + 1, release ? 'm' : 'M'); + encoded_button, bounded_col + 1, bounded_row + 1, release ? 'm' : 'M'); break; + } case MOUSE_URXVT: snprintf(response, sizeof(response), "\033[%d;%d;%dM", From 15ebc433baabd799252609e28bf79afd892b33a4 Mon Sep 17 00:00:00 2001 From: Yaakov Selkowitz <yselkowi@redhat.com> Date: Tue, 16 Dec 2025 22:10:39 -0500 Subject: [PATCH 1281/1323] Fix discarded const qualifiers from string functions This is a new warning in GCC 15 that is being promoted to an error due to the werror=true in meson.build. --- notify.c | 2 +- osc.c | 2 +- tokenize.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/notify.c b/notify.c index e8688180..e454b03b 100644 --- a/notify.c +++ b/notify.c @@ -114,7 +114,7 @@ consume_stdout(struct notification *notif, bool eof) while (left > 0) { line = data; size_t len = left; - char *eol = memchr(line, '\n', left); + char *eol = (char *)memchr(line, '\n', left); if (eol != NULL) { *eol = '\0'; diff --git a/osc.c b/osc.c index 0b492564..375eae5c 100644 --- a/osc.c +++ b/osc.c @@ -513,7 +513,7 @@ osc_uri(struct terminal *term, char *string) key_value = strtok_r(NULL, ":", &ctx)) { const char *key = key_value; - char *operator = strchr(key_value, '='); + char *operator = (char *)strchr(key_value, '='); if (operator == NULL) continue; diff --git a/tokenize.c b/tokenize.c index 77cc3f1a..70ceb39b 100644 --- a/tokenize.c +++ b/tokenize.c @@ -45,7 +45,7 @@ tokenize_cmdline(const char *cmdline, char ***argv) size_t idx = 0; while (*p != '\0') { - char *end = strchr(search_start, delim); + char *end = (char *)strchr(search_start, delim); if (end == NULL) { if (delim != ' ') { LOG_ERR("unterminated %s quote", delim == '"' ? "double" : "single"); From 4e96780eef048baa7370499c64a33210b4e0406d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 16 Dec 2025 14:56:42 +0100 Subject: [PATCH 1282/1323] shm: revert part of 299186a6547f6e038ee6f3822caf9a0fdfabceef 299186a6547f6e038ee6f3822caf9a0fdfabceef introduced a regression, where we don't handle SHM buffer "hiccups" correctly. If foot, for some reason is forced to render a frame "too soon", we might end up having multiple buffers "in flight" (i.e. committed to the compositor). This could happen if the compositor pushes multiple configure events rapidly, for example. Or anything else that forces foot to render something "immediately", without waiting for a frame callback. The compositor typically releases both buffers at the same time (or close to it), so the _next_ time we want to render a frame, we have *two* buffers to pick between. The problem here is that after 299186a6547f6e038ee6f3822caf9a0fdfabceef, foot no longer purges the additional buffer(s), but keeps all of them around. This messes up foot's age tracking, and the _next_ time we're forced to pull two buffers (without the compositor releasing the first one in between), we try to apply damage tracking that is no longer valid. This results in visual glitches. This never self-repairs, and we're stuck with visual glitches until the window is resized, and we're forced to allocate completely new buffers. It is unclear why 299186a6547f6e038ee6f3822caf9a0fdfabceef stopped removing the buffers. It was likely done early in the development, and is no longer needed. So far, I haven't noticed any bugs by re-introducing the buffer purging, but further testing is needed. --- CHANGELOG.md | 1 + shm.c | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85dc3762..70ab43b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,7 @@ character in the last column. * Crash when reverse-scrolling (terminfo capability `rin`) such that the current viewport ends up outside the scrollback ([#2232][2232]). +* Regression: visual glitches in rare circumstances. [2232]: https://codeberg.org/dnkl/foot/issues/2232 diff --git a/shm.c b/shm.c index 72b32f16..f488d6b6 100644 --- a/shm.c +++ b/shm.c @@ -628,14 +628,14 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height, bool with_alph else #endif { - if (cached == NULL) + if (cached == NULL) { cached = buf; - else { + } else { /* We have multiple buffers eligible for * reuse. Pick the "youngest" one, and mark the * other one for purging */ if (buf->public.age < cached->public.age) { - //shm_unref(&cached->public); + shm_unref(&cached->public); cached = buf; } else { /* @@ -646,8 +646,8 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height, bool with_alph * should be safe; "our" tll_foreach() already * holds the next pointer. */ - //if (buffer_unref_no_remove_from_chain(buf)) - // tll_remove(chain->bufs, it); + if (buffer_unref_no_remove_from_chain(buf)) + tll_remove(chain->bufs, it); } } } From cf2b390f6e096e7a2ca93d4dece153eb13261a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 19 Dec 2025 09:29:06 +0100 Subject: [PATCH 1283/1323] config: add [colors-dark] and [colors-light], replacing [colors] and [colors2] The main reason for having two color sections is to be able to switch between dark and light. Thus, it's better if the section names reflect this, rather than the more generic 'colors' and 'colors2' (which was the dark one and which was the light one, now again?) When the second color section was added, we kept the original name, colors, to make sure we didn't break existing configurations, and third-party themes. However, in the long run, it's probably better to be specific in the section naming, to avoid confusion. So, add 'colors-dark', and 'colors-light'. Keep 'colors' and 'colors2' as aliases for now, but mark them as deprecated. They WILL be removed in a future release. Also rename the option values for initial-color-theme, from 1/2, to dark/light. Keep the old ones for now, marked as deprecated. Update all bundled themes to use the new names. In the light-only themes (i.e. themes that define a single, light, theme), use colors-light, and set initial-color-theme=light. Possible improvements: disable color switching if only one color section has been explicitly configured (todo: figure out how to handle the default color theme values...) --- CHANGELOG.md | 7 + config.c | 128 ++++++++++++++--- config.h | 10 +- csi.c | 2 +- doc/foot.1.scd | 4 +- doc/foot.ini.5.scd | 57 ++++---- doc/footclient.1.scd | 4 +- foot.ini | 6 +- input.c | 6 +- key-binding.h | 2 + main.c | 8 +- osc.c | 44 +++--- render.c | 60 ++++---- server.c | 20 +-- server.h | 4 +- sixel.c | 6 +- terminal.c | 38 +++--- terminal.h | 4 +- tests/test-config.c | 196 +++++++++++++++++++-------- themes/aeroroot | 2 +- themes/alacritty | 2 +- themes/apprentice | 2 +- themes/ayu-mirage | 2 +- themes/catppuccin-frappe | 2 +- themes/catppuccin-latte | 5 +- themes/catppuccin-macchiato | 2 +- themes/catppuccin-mocha | 2 +- themes/chiba-dark | 2 +- themes/derp | 2 +- themes/deus | 2 +- themes/dracula | 2 +- themes/dracula-iterm | 2 +- themes/electrophoretic | 5 +- themes/gruvbox | 4 +- themes/gruvbox-dark | 2 +- themes/gruvbox-light | 5 +- themes/hacktober | 2 +- themes/iterm | 2 +- themes/jetbrains-darcula | 2 +- themes/kitty | 2 +- themes/material-amber | 5 +- themes/material-design | 2 +- themes/modus-operandi | 6 +- themes/modus-vivendi | 2 +- themes/modus-vivendi-tinted | 2 +- themes/molokai | 2 +- themes/monokai-pro | 2 +- themes/moonfly | 2 +- themes/neon | 2 +- themes/night-owl | 2 +- themes/nightfly | 2 +- themes/noirblaze | 2 +- themes/nord | 2 +- themes/nordiq | 2 +- themes/nvim | 4 +- themes/nvim-dark | 2 +- themes/nvim-light | 5 +- themes/onedark | 2 +- themes/onehalf-dark | 2 +- themes/panda | 2 +- themes/paper-color | 4 +- themes/paper-color-dark | 2 +- themes/paper-color-light | 5 +- themes/poimandres | 2 +- themes/rezza | 2 +- themes/rose-pine | 2 +- themes/rose-pine-dawn | 6 +- themes/rose-pine-moon | 2 +- themes/selenized | 4 +- themes/selenized-black | 2 +- themes/selenized-dark | 2 +- themes/selenized-light | 5 +- themes/selenized-white | 5 +- themes/solarized | 4 +- themes/solarized-dark | 2 +- themes/solarized-dark-normal-brights | 2 +- themes/solarized-light | 5 +- themes/solarized-normal-brights | 4 +- themes/srcery | 2 +- themes/starlight | 2 +- themes/tango | 2 +- themes/tempus-autumn | 2 +- themes/tempus-classic | 2 +- themes/tempus-dawn | 6 +- themes/tempus-day | 5 +- themes/tempus-dusk | 2 +- themes/tempus-fugit | 5 +- themes/tempus-future | 2 +- themes/tempus-night | 2 +- themes/tempus-past | 5 +- themes/tempus-rift | 2 +- themes/tempus-spring | 2 +- themes/tempus-summer | 2 +- themes/tempus-tempest | 2 +- themes/tempus-totus | 5 +- themes/tempus-warp | 2 +- themes/tempus-winter | 2 +- themes/tokyonight-light | 5 +- themes/tokyonight-night | 2 +- themes/tokyonight-storm | 2 +- themes/visibone | 2 +- themes/xterm | 2 +- themes/zenburn | 2 +- 103 files changed, 542 insertions(+), 298 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70ab43b3..ae9feb54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,8 @@ `foot` and `footclient`), allowing you to set a custom toplevel tag. The compositor must implement the new `xdg-toplevel-tag-v1` Wayland protocol ([#2212][2212]). +* `[colors-dark]` section to `foot.ini`. Replaces `[colors]`. +* `[colors-light]` section to `foot.ini`. Replaces `[colors2]`. [2212]: https://codeberg.org/dnkl/foot/issues/2212 @@ -93,6 +95,11 @@ ### Deprecated + +* `[colors]` section in `foot.ini`. Use `[colors-dark]` instead. +* `[colors2]` section in `foot.ini`. Use `[colors-light]` instead. + + ### Removed ### Fixed diff --git a/config.c b/config.c index 515b088c..0340d418 100644 --- a/config.c +++ b/config.c @@ -144,6 +144,8 @@ static const char *const binding_action_map[] = { [BIND_ACTION_REGEX_COPY] = "regex-copy", [BIND_ACTION_THEME_SWITCH_1] = "color-theme-switch-1", [BIND_ACTION_THEME_SWITCH_2] = "color-theme-switch-2", + [BIND_ACTION_THEME_SWITCH_DARK] = "color-theme-switch-dark", + [BIND_ACTION_THEME_SWITCH_LIGHT] = "color-theme-switch-light", [BIND_ACTION_THEME_TOGGLE] = "color-theme-toggle", /* Mouse-specific actions */ @@ -1118,8 +1120,40 @@ parse_section_main(struct context *ctx) sizeof(conf->initial_color_theme) == sizeof(int), "enum is not 32-bit"); - return value_to_enum(ctx, (const char*[]){"1", "2", NULL}, - (int *)&conf->initial_color_theme); + if (!value_to_enum(ctx, (const char*[]){ + "dark", "light", "1", "2", NULL}, + (int *)&conf->initial_color_theme)) + return false; + + if (streq(ctx->value, "1")) { + LOG_WARN("%s:%d: [main].initial-color-theme=1 deprecated, " + "use [main].initial-color-theme=dark instead", + ctx->path, ctx->lineno); + + user_notification_add( + &ctx->conf->notifications, + USER_NOTIFICATION_DEPRECATED, + xstrdup("[main].initial-color-theme=1: " + "use [main].initial-color-theme=dark instead")); + + conf->initial_color_theme = COLOR_THEME_DARK; + } + + else if (streq(ctx->value, "2")) { + LOG_WARN("%s:%d: [main].initial-color-theme=2 deprecated, " + "use [main].initial-color-theme=light instead", + ctx->path, ctx->lineno); + + user_notification_add( + &ctx->conf->notifications, + USER_NOTIFICATION_DEPRECATED, + xstrdup("[main].initial-color-theme=2: " + "use [main].initial-color-theme=light instead")); + + conf->initial_color_theme = COLOR_THEME_LIGHT; + } + + return true; } else if (streq(key, "uppercase-regex-insert")) @@ -1555,16 +1589,44 @@ parse_color_theme(struct context *ctx, struct color_theme *theme) return true; } +static bool +parse_section_colors_dark(struct context *ctx) +{ + return parse_color_theme(ctx, &ctx->conf->colors_dark); +} + +static bool +parse_section_colors_light(struct context *ctx) +{ + return parse_color_theme(ctx, &ctx->conf->colors_light); +} + static bool parse_section_colors(struct context *ctx) { - return parse_color_theme(ctx, &ctx->conf->colors); + LOG_WARN("%s:%d: [colors]: deprecated; use [colors-dark] instead", + ctx->path, ctx->lineno); + + user_notification_add( + &ctx->conf->notifications, + USER_NOTIFICATION_DEPRECATED, + xstrdup("[colors]: use [colors-dark] instead")); + + return parse_color_theme(ctx, &ctx->conf->colors_dark); } static bool parse_section_colors2(struct context *ctx) { - return parse_color_theme(ctx, &ctx->conf->colors2); + LOG_WARN("%s:%d: [colors2]: deprecated; use [colors-light] instead", + ctx->path, ctx->lineno); + + user_notification_add( + &ctx->conf->notifications, + USER_NOTIFICATION_DEPRECATED, + xstrdup("[colors2]: use [colors-light] instead")); + + return parse_color_theme(ctx, &ctx->conf->colors_light); } static bool @@ -1610,14 +1672,14 @@ parse_section_cursor(struct context *ctx) if (!value_to_two_colors( ctx, - &conf->colors.cursor.text, - &conf->colors.cursor.cursor, + &conf->colors_dark.cursor.text, + &conf->colors_dark.cursor.cursor, false)) { return false; } - conf->colors.use_custom.cursor = true; + conf->colors_dark.use_custom.cursor = true; return true; } @@ -2268,6 +2330,29 @@ parse_key_binding_section(struct context *ctx, aux.regex_name = regex_name; } + if (action_map == binding_action_map && + action >= BIND_ACTION_THEME_SWITCH_1 && + action <= BIND_ACTION_THEME_SWITCH_2) + { + const char *use_instead = + action_map[action == BIND_ACTION_THEME_SWITCH_1 + ? BIND_ACTION_THEME_SWITCH_DARK + : BIND_ACTION_THEME_SWITCH_LIGHT]; + + const char *notif = action == BIND_ACTION_THEME_SWITCH_1 + ? "[key-bindings].color-theme-switch-1: use [key-bindings].color-theme-switch-dark instead" + : "[key-bindings].color-theme-switch-2: use [key-bindings].color-theme-switch-light instead"; + + LOG_WARN("%s:%d: [key-bindings].%s: deprecated, use %s instead", + ctx->path, ctx->lineno, + action_map[action], use_instead); + + user_notification_add( + &ctx->conf->notifications, + USER_NOTIFICATION_DEPRECATED, + xstrdup(notif)); + } + if (!value_to_key_combos(ctx, action, &aux, bindings, KEY_BINDING)) { free_binding_aux(&aux); return false; @@ -2958,8 +3043,8 @@ enum section { SECTION_SCROLLBACK, SECTION_URL, SECTION_REGEX, - SECTION_COLORS, - SECTION_COLORS2, + SECTION_COLORS_DARK, + SECTION_COLORS_LIGHT, SECTION_CURSOR, SECTION_MOUSE, SECTION_CSD, @@ -2971,6 +3056,11 @@ enum section { SECTION_ENVIRONMENT, SECTION_TWEAK, SECTION_TOUCH, + + /* Deprecated */ + SECTION_COLORS, + SECTION_COLORS2, + SECTION_COUNT, }; @@ -2989,8 +3079,8 @@ static const struct { [SECTION_SCROLLBACK] = {&parse_section_scrollback, "scrollback"}, [SECTION_URL] = {&parse_section_url, "url"}, [SECTION_REGEX] = {&parse_section_regex, "regex", true}, - [SECTION_COLORS] = {&parse_section_colors, "colors"}, - [SECTION_COLORS2] = {&parse_section_colors2, "colors2"}, + [SECTION_COLORS_DARK] = {&parse_section_colors_dark, "colors-dark"}, + [SECTION_COLORS_LIGHT] = {&parse_section_colors_light, "colors-light"}, [SECTION_CURSOR] = {&parse_section_cursor, "cursor"}, [SECTION_MOUSE] = {&parse_section_mouse, "mouse"}, [SECTION_CSD] = {&parse_section_csd, "csd"}, @@ -3002,6 +3092,10 @@ static const struct { [SECTION_ENVIRONMENT] = {&parse_section_environment, "environment"}, [SECTION_TWEAK] = {&parse_section_tweak, "tweak"}, [SECTION_TOUCH] = {&parse_section_touch, "touch"}, + + /* Deprecated */ + [SECTION_COLORS] = {&parse_section_colors, "colors"}, + [SECTION_COLORS2] = {&parse_section_colors2, "colors2"}, }; static_assert(ALEN(section_info) == SECTION_COUNT, "section info array size mismatch"); @@ -3435,7 +3529,7 @@ config_load(struct config *conf, const char *conf_path, }, .multiplier = 3., }, - .colors = { + .colors_dark = { .fg = default_foreground, .bg = default_background, .flash = 0x7f7f00, @@ -3455,7 +3549,7 @@ config_load(struct config *conf, const char *conf_path, .url = false, }, }, - .initial_color_theme = COLOR_THEME1, + .initial_color_theme = COLOR_THEME_DARK, .cursor = { .style = CURSOR_BLOCK, .unfocused_style = CURSOR_UNFOCUSED_HOLLOW, @@ -3535,10 +3629,10 @@ config_load(struct config *conf, const char *conf_path, .notifications = tll_init(), }; - memcpy(conf->colors.table, default_color_table, sizeof(default_color_table)); - memcpy(conf->colors.sixel, default_sixel_colors, sizeof(default_sixel_colors)); - memcpy(&conf->colors2, &conf->colors, sizeof(conf->colors)); - conf->colors2.dim_blend_towards = DIM_BLEND_TOWARDS_WHITE; + memcpy(conf->colors_dark.table, default_color_table, sizeof(default_color_table)); + memcpy(conf->colors_dark.sixel, default_sixel_colors, sizeof(default_sixel_colors)); + memcpy(&conf->colors_light, &conf->colors_dark, sizeof(conf->colors_dark)); + conf->colors_light.dim_blend_towards = DIM_BLEND_TOWARDS_WHITE; parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers); diff --git a/config.h b/config.h index fc5e290e..9ca47753 100644 --- a/config.h +++ b/config.h @@ -195,8 +195,10 @@ struct color_theme { }; enum which_color_theme { - COLOR_THEME1, - COLOR_THEME2, + COLOR_THEME_DARK, + COLOR_THEME_LIGHT, + COLOR_THEME_1, /* Deprecated */ + COLOR_THEME_2, /* Deprecated */ }; enum shm_bit_depth { @@ -327,8 +329,8 @@ struct config { tll(struct custom_regex) custom_regexes; - struct color_theme colors; - struct color_theme colors2; + struct color_theme colors_dark; + struct color_theme colors_light; enum which_color_theme initial_color_theme; struct { diff --git a/csi.c b/csi.c index c5f616ac..7e0cf464 100644 --- a/csi.c +++ b/csi.c @@ -1578,7 +1578,7 @@ csi_dispatch(struct terminal *term, uint8_t final) int chars = snprintf( reply, sizeof(reply), "\033[?997;%dn", - term->colors.active_theme == COLOR_THEME1 ? 1 : 2); + term->colors.active_theme == COLOR_THEME_DARK ? 1 : 2); term_to_slave(term, reply, chars); break; diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 60ba622b..7058e96f 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -695,8 +695,8 @@ variables to unset may be defined in *foot.ini*(5). The following signals have special meaning in foot: -- SIGUSR1: switch to color theme 1 (i.e. use the *[colors]* section). -- SIGUSR2: switch to color theme 2 (i.e. use the *[colors2]* section). +- SIGUSR1: switch to the dark color theme (*[colors-dark]*). +- SIGUSR2: switch to the light color theme (*[colors-light]*). Note: you can send SIGUSR1/SIGUSR2 to a *foot --server* process too, in which case all client instances will switch theme. Furthermore, all diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index c9782895..8bff9629 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -24,7 +24,7 @@ commented out will usually be installed to */etc/xdg/foot/foot.ini*. Options are set using KEY=VALUE pairs: - *\[colors\]*++ + *\[colors-dark\]*++ *background=000000*++ *foreground=ffffff* @@ -371,12 +371,12 @@ empty string to be set, but it must be quoted: *KEY=""*) Default: _yes_ *initial-color-theme* - Selects which color theme to use, *1*, or *2*. + Selects which color theme to use, *dark*, or *light*. - *1* uses the colors defined in the *colors* section, while *2* - uses the colors from the *colors2* section. + *dark* uses the colors defined in the *colors-dark* section, while + *light* uses the colors from the *colors-light* section. - Use the *color-theme-switch-1*, *color-theme-switch-2* and + Use the *color-theme-switch-dark*, *color-theme-switch-light* and *color-theme-toggle* key bindings to switch between the two themes at runtime, or send SIGUSR1/SIGUSR2 to the foot process (see *foot*(1) for details). @@ -987,19 +987,24 @@ applications can change these at runtime. Default: _400_. -# SECTION: colors +# SECTION: colors-dark, colors-light -This section controls the 16 ANSI colors, the default foreground and -background colors, and the extended 256 color palette. Note that +These two sections controls the 16 ANSI colors, the default foreground +and background colors, and the extended 256 color palette. Note that applications can change these at runtime. The colors are in RRGGBB format (i.e. plain old 6-digit hex values, without prefix). That is, they do *not* have an alpha component. You can configure the background transparency with the _alpha_ option. -In the context of private mode 2031 (Dark and Light Mode detection), -the primary theme (i.e. the *colors* section) is considered to be the -dark theme (since the default theme is dark). +*colors-dark* is intended to define a dark color theme, and +*colors-light* is intended to define a light color theme. You can +switch between them using the *color-theme-switch-dark*, +*color-theme-switch-light* and *color-theme-toggle* key bindings, or +by sending SIGUSR1/SIGUSR2 to the foot process. + +The default theme used is *colors-dark*, unless +*initial-color-theme=light* has been set. *cursor* Two space separated RRGGBB values (i.e. plain old 6-digit hex @@ -1098,7 +1103,7 @@ dark theme (since the default theme is dark). black makes the text darker, while blending towards white makes it whiter (but still dimmer than normal text). - Default: _black_ (*colors*), _white_ (*colors2*) + Default: _black_ (*colors-dark*), _white_ (*colors-light*) *selection-foreground*, *selection-background* Foreground (text) and background color to use in selected @@ -1135,20 +1140,6 @@ dark theme (since the default theme is dark). Flash translucency. A value in the range 0.0-1.0, where 0.0 means completely transparent, and 1.0 is opaque. Default: _0.5_. -# SECTION: colors2 - -This section defines an alternative color theme. It has the exact same -keys as the *colors* section. The default values are the same, except -for *dim-blend-towards*, which defaults to *white* instead. - -Note that values are not inherited. That is, if you set a value in -*colors*, but not in *colors2*, the value from *colors* is not -inherited by *colors2*. - -In the context of private mode 2031 (Dark and Light Mode detection), -the alternative theme (i.e. the *colors2* section) is considered to be -the light theme (since the default, the primary theme, is dark). - # SECTION: csd This section controls the look of the _CSDs_ (Client Side @@ -1455,16 +1446,16 @@ e.g. *search-start=none*. Default: _Control+Shift+u_. -*color-theme-switch-1*, *color-theme-switch-2*, *color-theme-toggle* - Switch between the primary color theme (defined in the *colors* - section), and the alternative color theme (defined in the - *colors2* section). +*color-theme-switch-dark*, *color-theme-switch-dark*, *color-theme-toggle* + Switch between the dark color theme (defined in the *colors-dark* + section), and the light color theme (defined in the *colors-light* + section). - *color-theme-switch-1* applies the primary color theme regardless + *color-theme-switch-dark* applies the dark color theme regardless of which color theme is currently active. - *color-theme-switch-2* applies the alternative color theme regardless - of which color theme is currently active. + *color-theme-switch-light* applies the light color theme + regardless of which color theme is currently active. *color-theme-toggle* toggles between the primary and alternative color themes. diff --git a/doc/footclient.1.scd b/doc/footclient.1.scd index edf3e9f3..ad865913 100644 --- a/doc/footclient.1.scd +++ b/doc/footclient.1.scd @@ -198,8 +198,8 @@ variables to unset may be defined in *foot.ini*(5). The following signals have special meaning in footclient: -- SIGUSR1: switch to color theme 1 (i.e. use the *[colors]* section). -- SIGUSR2: switch to color theme 2 (i.e. use the *[colors2]* section). +- SIGUSR1: switch to the dark color theme (*[colors-dark]*). +- SIGUSR2: switch to the light color theme (*[colors-light]*). When sending SIGUSR1/SIGUSR2 to a footclient instance, the theme is changed in that instance only. This is different from when you send diff --git a/foot.ini b/foot.ini index 2d170489..a9b4b83d 100644 --- a/foot.ini +++ b/foot.ini @@ -24,7 +24,7 @@ # dpi-aware=no # gamma-correct-blending=no -# initial-color-theme=1 +# initial-color-theme=dark # initial-window-size-pixels=700x500 # Or, # initial-window-size-chars=<COLSxROWS> # initial-window-mode=windowed @@ -101,7 +101,7 @@ [touch] # long-press-delay=400 -[colors] +[colors-dark] # alpha=1.0 # alpha-mode=default # Can be `default`, `matching` or `all` # background=242424 @@ -169,7 +169,7 @@ # search-box-match=<regular0> <regular3> # black-on-yellow # urls=<regular3> -[colors2] +[colors-light] # Alternative color theme, see man page foot.ini(5) # Same builtin defaults as [color], except for: # dim-blend-towards=white diff --git a/input.c b/input.c index 44a99e3b..80b028ac 100644 --- a/input.c +++ b/input.c @@ -486,11 +486,13 @@ execute_binding(struct seat *seat, struct terminal *term, return true; case BIND_ACTION_THEME_SWITCH_1: - term_theme_switch_to_1(term); + case BIND_ACTION_THEME_SWITCH_DARK: + term_theme_switch_to_dark(term); return true; case BIND_ACTION_THEME_SWITCH_2: - term_theme_switch_to_2(term); + case BIND_ACTION_THEME_SWITCH_LIGHT: + term_theme_switch_to_light(term); return true; case BIND_ACTION_THEME_TOGGLE: diff --git a/key-binding.h b/key-binding.h index 5f0c1f1e..c4a04e99 100644 --- a/key-binding.h +++ b/key-binding.h @@ -45,6 +45,8 @@ enum bind_action_normal { BIND_ACTION_REGEX_COPY, BIND_ACTION_THEME_SWITCH_1, BIND_ACTION_THEME_SWITCH_2, + BIND_ACTION_THEME_SWITCH_DARK, + BIND_ACTION_THEME_SWITCH_LIGHT, BIND_ACTION_THEME_TOGGLE, /* Mouse specific actions - i.e. they require a mouse coordinate */ diff --git a/main.c b/main.c index b933e1c1..9db77d0c 100644 --- a/main.c +++ b/main.c @@ -59,14 +59,14 @@ fdm_sigusr(struct fdm *fdm, int signo, void *data) if (ctx->server != NULL) { if (signo == SIGUSR1) - server_global_theme_switch_to_1(ctx->server); + server_global_theme_switch_to_dark(ctx->server); else - server_global_theme_switch_to_2(ctx->server); + server_global_theme_switch_to_light(ctx->server); } else { if (signo == SIGUSR1) - term_theme_switch_to_1(ctx->term); + term_theme_switch_to_dark(ctx->term); else - term_theme_switch_to_2(ctx->term); + term_theme_switch_to_light(ctx->term); } return true; diff --git a/osc.c b/osc.c index 375eae5c..9407e7b8 100644 --- a/osc.c +++ b/osc.c @@ -1459,9 +1459,9 @@ osc_dispatch(struct terminal *term) case 11: term->colors.bg = color; if (!have_alpha) { - alpha = term->colors.active_theme == COLOR_THEME1 - ? term->conf->colors.alpha - : term->conf->colors2.alpha; + alpha = term->colors.active_theme == COLOR_THEME_DARK + ? term->conf->colors_dark.alpha + : term->conf->colors_light.alpha; } const bool changed = term->colors.alpha != alpha; @@ -1516,9 +1516,9 @@ osc_dispatch(struct terminal *term) /* Reset Color Number 'c' (whole table if no parameter) */ const struct color_theme *theme = - term->colors.active_theme == COLOR_THEME1 - ? &term->conf->colors - : &term->conf->colors2; + term->colors.active_theme == COLOR_THEME_DARK + ? &term->conf->colors_dark + : &term->conf->colors_light; if (string[0] == '\0') { LOG_DBG("resetting all colors"); @@ -1559,9 +1559,9 @@ osc_dispatch(struct terminal *term) LOG_DBG("resetting foreground color"); const struct color_theme *theme = - term->colors.active_theme == COLOR_THEME1 - ? &term->conf->colors - : &term->conf->colors2; + term->colors.active_theme == COLOR_THEME_DARK + ? &term->conf->colors_dark + : &term->conf->colors_light; term->colors.fg = theme->fg; term_damage_color(term, COLOR_DEFAULT, 0); @@ -1571,9 +1571,9 @@ osc_dispatch(struct terminal *term) LOG_DBG("resetting background color"); const struct color_theme *theme = - term->colors.active_theme == COLOR_THEME1 - ? &term->conf->colors - : &term->conf->colors2; + term->colors.active_theme == COLOR_THEME_DARK + ? &term->conf->colors_dark + : &term->conf->colors_light; bool alpha_changed = term->colors.alpha != theme->alpha; @@ -1594,14 +1594,14 @@ osc_dispatch(struct terminal *term) LOG_DBG("resetting cursor color"); const struct color_theme *theme = - term->colors.active_theme == COLOR_THEME1 - ? &term->conf->colors - : &term->conf->colors2; + term->colors.active_theme == COLOR_THEME_DARK + ? &term->conf->colors_dark + : &term->conf->colors_light; term->colors.cursor_fg = theme->cursor.text; term->colors.cursor_bg = theme->cursor.cursor; - if (term->conf->colors.use_custom.cursor) { + if (term->conf->colors_dark.use_custom.cursor) { term->colors.cursor_fg |= 1u << 31; term->colors.cursor_bg |= 1u << 31; } @@ -1614,9 +1614,9 @@ osc_dispatch(struct terminal *term) LOG_DBG("resetting selection background color"); const struct color_theme *theme = - term->colors.active_theme == COLOR_THEME1 - ? &term->conf->colors - : &term->conf->colors2; + term->colors.active_theme == COLOR_THEME_DARK + ? &term->conf->colors_dark + : &term->conf->colors_light; term->colors.selection_bg = theme->selection_bg; break; @@ -1626,9 +1626,9 @@ osc_dispatch(struct terminal *term) LOG_DBG("resetting selection foreground color"); const struct color_theme *theme = - term->colors.active_theme == COLOR_THEME1 - ? &term->conf->colors - : &term->conf->colors2; + term->colors.active_theme == COLOR_THEME_DARK + ? &term->conf->colors_dark + : &term->conf->colors_light; term->colors.selection_fg = theme->selection_fg; break; diff --git a/render.c b/render.c index 1d0f08af..ac8ece37 100644 --- a/render.c +++ b/render.c @@ -293,7 +293,7 @@ static inline uint32_t color_dim(const struct terminal *term, uint32_t color) { const struct config *conf = term->conf; - const uint8_t custom_dim = conf->colors.use_custom.dim; + const uint8_t custom_dim = conf->colors_dark.use_custom.dim; if (unlikely(custom_dim != 0)) { for (size_t i = 0; i < 8; i++) { @@ -302,7 +302,7 @@ color_dim(const struct terminal *term, uint32_t color) if (term->colors.table[0 + i] == color) { /* "Regular" color, return the corresponding "dim" */ - return conf->colors.dim[i]; + return conf->colors_dark.dim[i]; } else if (term->colors.table[8 + i] == color) { @@ -312,9 +312,9 @@ color_dim(const struct terminal *term, uint32_t color) } } - const struct color_theme *theme = term->colors.active_theme == COLOR_THEME1 - ? &conf->colors - : &conf->colors2; + const struct color_theme *theme = term->colors.active_theme == COLOR_THEME_DARK + ? &conf->colors_dark + : &conf->colors_light; return color_blend_towards( color, @@ -776,7 +776,7 @@ render_cell(struct terminal *term, pixman_image_t *pix, } else if (!term->window->is_fullscreen && term->colors.alpha != 0xffff) { - switch (term->conf->colors.alpha_mode) { + switch (term->conf->colors_dark.alpha_mode) { case ALPHA_MODE_DEFAULT: { if (cell->attrs.bg_src == COLOR_DEFAULT) { alpha = term->colors.alpha; @@ -1175,8 +1175,8 @@ render_cell(struct terminal *term, pixman_image_t *pix, if (unlikely(cell->attrs.url)) { pixman_color_t url_color = color_hex_to_pixman( - term->conf->colors.use_custom.url - ? term->conf->colors.url + term->conf->colors_dark.use_custom.url + ? term->conf->colors_dark.url : term->colors.table[3], gamma_correct); draw_underline(term, pix, font, &url_color, x, y, cell_cols); @@ -1991,8 +1991,8 @@ render_overlay(struct terminal *term) case OVERLAY_FLASH: color = color_hex_to_pixman_with_alpha( - term->conf->colors.flash, - term->conf->colors.flash_alpha, + term->conf->colors_dark.flash, + term->conf->colors_dark.flash_alpha, wayl_do_linear_blending(term->wl, term->conf)); break; @@ -2510,10 +2510,10 @@ render_csd_title(struct terminal *term, const struct csd_data *info, uint32_t bg = term->conf->csd.color.title_set ? term->conf->csd.color.title - : 0xffu << 24 | term->conf->colors.fg; + : 0xffu << 24 | term->conf->colors_dark.fg; uint32_t fg = term->conf->csd.color.buttons_set ? term->conf->csd.color.buttons - : term->conf->colors.bg; + : term->conf->colors_dark.bg; if (!term->visual_focus) { bg = color_dim(term, bg); @@ -2607,7 +2607,7 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, uint32_t _color = conf->csd.color.border_set ? conf->csd.color.border : conf->csd.color.title_set ? conf->csd.color.title : - 0xffu << 24 | term->conf->colors.fg; + 0xffu << 24 | term->conf->colors_dark.fg; if (!term->visual_focus) _color = color_dim(term, _color); @@ -2627,7 +2627,7 @@ static pixman_color_t get_csd_button_fg_color(const struct terminal *term) { const struct config *conf = term->conf; - uint32_t _color = conf->colors.bg; + uint32_t _color = conf->colors_dark.bg; uint16_t alpha = 0xffff; if (conf->csd.color.buttons_set) { @@ -2872,7 +2872,7 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx, switch (surf_idx) { case CSD_SURF_MINIMIZE: - _color = term->conf->colors.table[4]; /* blue */ + _color = term->conf->colors_dark.table[4]; /* blue */ is_set = term->conf->csd.color.minimize_set; conf_color = &term->conf->csd.color.minimize; is_active = term->active_surface == TERM_SURF_BUTTON_MINIMIZE && @@ -2880,7 +2880,7 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx, break; case CSD_SURF_MAXIMIZE: - _color = term->conf->colors.table[2]; /* green */ + _color = term->conf->colors_dark.table[2]; /* green */ is_set = term->conf->csd.color.maximize_set; conf_color = &term->conf->csd.color.maximize; is_active = term->active_surface == TERM_SURF_BUTTON_MAXIMIZE && @@ -2888,7 +2888,7 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx, break; case CSD_SURF_CLOSE: - _color = term->conf->colors.table[1]; /* red */ + _color = term->conf->colors_dark.table[1]; /* red */ is_set = term->conf->csd.color.close_set; conf_color = &term->conf->csd.color.quit; is_active = term->active_surface == TERM_SURF_BUTTON_CLOSE && @@ -3117,9 +3117,9 @@ render_scrollback_position(struct terminal *term) uint32_t fg = term->colors.table[0]; uint32_t bg = term->colors.table[8 + 4]; - if (term->conf->colors.use_custom.scrollback_indicator) { - fg = term->conf->colors.scrollback_indicator.fg; - bg = term->conf->colors.scrollback_indicator.bg; + if (term->conf->colors_dark.use_custom.scrollback_indicator) { + fg = term->conf->colors_dark.scrollback_indicator.fg; + bg = term->conf->colors_dark.scrollback_indicator.bg; } render_osd( @@ -3799,18 +3799,18 @@ render_search_box(struct terminal *term) const bool is_match = term->search.match_len == text_len; const bool custom_colors = is_match - ? term->conf->colors.use_custom.search_box_match - : term->conf->colors.use_custom.search_box_no_match; + ? term->conf->colors_dark.use_custom.search_box_match + : term->conf->colors_dark.use_custom.search_box_no_match; /* Background - yellow on empty/match, red on mismatch (default) */ const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); const pixman_color_t color = color_hex_to_pixman( is_match ? (custom_colors - ? term->conf->colors.search_box.match.bg + ? term->conf->colors_dark.search_box.match.bg : term->colors.table[3]) : (custom_colors - ? term->conf->colors.search_box.no_match.bg + ? term->conf->colors_dark.search_box.no_match.bg : term->colors.table[1]), gamma_correct); @@ -3832,8 +3832,8 @@ render_search_box(struct terminal *term) pixman_color_t fg = color_hex_to_pixman( custom_colors ? (is_match - ? term->conf->colors.search_box.match.fg - : term->conf->colors.search_box.no_match.fg) + ? term->conf->colors_dark.search_box.match.fg + : term->conf->colors_dark.search_box.no_match.fg) : term->colors.table[0], gamma_correct); @@ -4254,11 +4254,11 @@ render_urls(struct terminal *term) struct buffer *bufs[render_count]; shm_get_many(chain, render_count, widths, heights, bufs, false); - uint32_t fg = term->conf->colors.use_custom.jump_label - ? term->conf->colors.jump_label.fg + uint32_t fg = term->conf->colors_dark.use_custom.jump_label + ? term->conf->colors_dark.jump_label.fg : term->colors.table[0]; - uint32_t bg = term->conf->colors.use_custom.jump_label - ? term->conf->colors.jump_label.bg + uint32_t bg = term->conf->colors_dark.use_custom.jump_label + ? term->conf->colors_dark.jump_label.bg : term->colors.table[3]; for (size_t i = 0; i < render_count; i++) { diff --git a/server.c b/server.c index 6b3e5094..25963325 100644 --- a/server.c +++ b/server.c @@ -182,11 +182,11 @@ fdm_client(struct fdm *fdm, int fd, int events, void *data) switch (sigusr.signo) { case SIGUSR1: - term_theme_switch_to_1(client->instance->terminal); + term_theme_switch_to_dark(client->instance->terminal); break; case SIGUSR2: - term_theme_switch_to_2(client->instance->terminal); + term_theme_switch_to_light(client->instance->terminal); break; default: @@ -670,21 +670,21 @@ server_destroy(struct server *server) } void -server_global_theme_switch_to_1(struct server *server) +server_global_theme_switch_to_dark(struct server *server) { - server->conf->initial_color_theme = COLOR_THEME1; + server->conf->initial_color_theme = COLOR_THEME_DARK; tll_foreach(server->clients, it) - term_theme_switch_to_1(it->item->instance->terminal); + term_theme_switch_to_dark(it->item->instance->terminal); tll_foreach(server->terminals, it) - term_theme_switch_to_1(it->item->terminal); + term_theme_switch_to_dark(it->item->terminal); } void -server_global_theme_switch_to_2(struct server *server) +server_global_theme_switch_to_light(struct server *server) { - server->conf->initial_color_theme = COLOR_THEME2; + server->conf->initial_color_theme = COLOR_THEME_LIGHT; tll_foreach(server->clients, it) - term_theme_switch_to_2(it->item->instance->terminal); + term_theme_switch_to_light(it->item->instance->terminal); tll_foreach(server->terminals, it) - term_theme_switch_to_2(it->item->terminal); + term_theme_switch_to_light(it->item->terminal); } diff --git a/server.h b/server.h index 6adfe7c6..683ad74d 100644 --- a/server.h +++ b/server.h @@ -10,5 +10,5 @@ struct server *server_init(struct config *conf, struct fdm *fdm, struct reaper *reaper, struct wayland *wayl); void server_destroy(struct server *server); -void server_global_theme_switch_to_1(struct server *server); -void server_global_theme_switch_to_2(struct server *server); +void server_global_theme_switch_to_dark(struct server *server); +void server_global_theme_switch_to_light(struct server *server); diff --git a/sixel.c b/sixel.c index c5ef01a1..07b97f46 100644 --- a/sixel.c +++ b/sixel.c @@ -137,7 +137,7 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) } const size_t active_palette_entries = min( - ALEN(term->conf->colors.sixel), term->sixel.palette_size); + ALEN(term->conf->colors_dark.sixel), term->sixel.palette_size); if (term->sixel.use_private_palette) { xassert(term->sixel.private_palette == NULL); @@ -145,7 +145,7 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) term->sixel.palette_size, sizeof(term->sixel.private_palette[0])); memcpy( - term->sixel.private_palette, term->conf->colors.sixel, + term->sixel.private_palette, term->conf->colors_dark.sixel, active_palette_entries * sizeof(term->sixel.private_palette[0])); if (term->sixel.linear_blending || term->sixel.use_10bit) { @@ -164,7 +164,7 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) term->sixel.palette_size, sizeof(term->sixel.shared_palette[0])); memcpy( - term->sixel.shared_palette, term->conf->colors.sixel, + term->sixel.shared_palette, term->conf->colors_dark.sixel, active_palette_entries * sizeof(term->sixel.shared_palette[0])); if (term->sixel.linear_blending || term->sixel.use_10bit) { diff --git a/terminal.c b/terminal.c index 3749416b..b670d606 100644 --- a/terminal.c +++ b/terminal.c @@ -1271,8 +1271,10 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, const struct color_theme *theme = NULL; switch (conf->initial_color_theme) { - case COLOR_THEME1: theme = &conf->colors; break; - case COLOR_THEME2: theme = &conf->colors2; break; + case COLOR_THEME_DARK: theme = &conf->colors_dark; break; + case COLOR_THEME_LIGHT: theme = &conf->colors_light; break; + case COLOR_THEME_1: BUG("COLOR_THEME_1 should not be used"); break; + case COLOR_THEME_2: BUG("COLOR_THEME_2 should not be used"); break; } /* Initialize configure-based terminal attributes */ @@ -2177,8 +2179,10 @@ term_reset(struct terminal *term, bool hard) const struct color_theme *theme = NULL; switch (term->conf->initial_color_theme) { - case COLOR_THEME1: theme = &term->conf->colors; break; - case COLOR_THEME2: theme = &term->conf->colors2; break; + case COLOR_THEME_DARK: theme = &term->conf->colors_dark; break; + case COLOR_THEME_LIGHT: theme = &term->conf->colors_light; break; + case COLOR_THEME_1: BUG("COLOR_THEME_1 should not be used"); break; + case COLOR_THEME_2: BUG("COLOR_THEME_2 should not be used"); break; } term->flash.active = false; @@ -4742,13 +4746,13 @@ term_send_size_notification(struct terminal *term) } void -term_theme_switch_to_1(struct terminal *term) +term_theme_switch_to_dark(struct terminal *term) { - if (term->colors.active_theme == COLOR_THEME1) + if (term->colors.active_theme == COLOR_THEME_DARK) return; - term_theme_apply(term, &term->conf->colors); - term->colors.active_theme = COLOR_THEME1; + term_theme_apply(term, &term->conf->colors_dark); + term->colors.active_theme = COLOR_THEME_DARK; wayl_win_alpha_changed(term->window); term_font_subpixel_changed(term); @@ -4762,13 +4766,13 @@ term_theme_switch_to_1(struct terminal *term) } void -term_theme_switch_to_2(struct terminal *term) +term_theme_switch_to_light(struct terminal *term) { - if (term->colors.active_theme == COLOR_THEME2) + if (term->colors.active_theme == COLOR_THEME_LIGHT) return; - term_theme_apply(term, &term->conf->colors2); - term->colors.active_theme = COLOR_THEME2; + term_theme_apply(term, &term->conf->colors_light); + term->colors.active_theme = COLOR_THEME_LIGHT; wayl_win_alpha_changed(term->window); term_font_subpixel_changed(term); @@ -4784,15 +4788,15 @@ term_theme_switch_to_2(struct terminal *term) void term_theme_toggle(struct terminal *term) { - if (term->colors.active_theme == COLOR_THEME1) { - term_theme_apply(term, &term->conf->colors2); - term->colors.active_theme = COLOR_THEME2; + if (term->colors.active_theme == COLOR_THEME_DARK) { + term_theme_apply(term, &term->conf->colors_light); + term->colors.active_theme = COLOR_THEME_LIGHT; if (term->report_theme_changes) term_to_slave(term, "\033[?997;2n", 9); } else { - term_theme_apply(term, &term->conf->colors); - term->colors.active_theme = COLOR_THEME1; + term_theme_apply(term, &term->conf->colors_dark); + term->colors.active_theme = COLOR_THEME_DARK; if (term->report_theme_changes) term_to_slave(term, "\033[?997;1n", 9); diff --git a/terminal.h b/terminal.h index 364d57b3..fe39341d 100644 --- a/terminal.h +++ b/terminal.h @@ -994,8 +994,8 @@ void term_enable_size_notifications(struct terminal *term); void term_disable_size_notifications(struct terminal *term); void term_send_size_notification(struct terminal *term); -void term_theme_switch_to_1(struct terminal *term); -void term_theme_switch_to_2(struct terminal *term); +void term_theme_switch_to_dark(struct terminal *term); +void term_theme_switch_to_light(struct terminal *term); void term_theme_toggle(struct terminal *term); static inline void term_reset_grapheme_state(struct terminal *term) diff --git a/tests/test-config.c b/tests/test-config.c index 268733db..f83a9beb 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -521,6 +521,14 @@ test_section_main(void) (int []){STARTUP_WINDOWED, STARTUP_MAXIMIZED, STARTUP_FULLSCREEN}, (int *)&conf.startup_mode); + test_enum( + &ctx, &parse_section_main, "initial-color-theme", + 2, + (const char *[]){"dark", "light", "1", "2"}, + (int []){COLOR_THEME_DARK, COLOR_THEME_LIGHT, + COLOR_THEME_DARK, COLOR_THEME_LIGHT}, + (int *)&conf.initial_color_theme); + /* TODO: font (custom) */ /* TODO: include (custom) */ /* TODO: bold-text-in-bright (enum/boolean) */ @@ -695,78 +703,157 @@ test_section_touch(void) } static void -test_section_colors(void) +test_section_colors_dark(void) { struct config conf = {0}; struct context ctx = { - .conf = &conf, .section = "colors", .path = "unittest"}; + .conf = &conf, .section = "colors-dark", .path = "unittest"}; test_invalid_key(&ctx, &parse_section_colors, "invalid-key"); - test_color(&ctx, &parse_section_colors, "foreground", false, &conf.colors.fg); - test_color(&ctx, &parse_section_colors, "background", false, &conf.colors.bg); - test_color(&ctx, &parse_section_colors, "regular0", false, &conf.colors.table[0]); - test_color(&ctx, &parse_section_colors, "regular1", false, &conf.colors.table[1]); - test_color(&ctx, &parse_section_colors, "regular2", false, &conf.colors.table[2]); - test_color(&ctx, &parse_section_colors, "regular3", false, &conf.colors.table[3]); - test_color(&ctx, &parse_section_colors, "regular4", false, &conf.colors.table[4]); - test_color(&ctx, &parse_section_colors, "regular5", false, &conf.colors.table[5]); - test_color(&ctx, &parse_section_colors, "regular6", false, &conf.colors.table[6]); - test_color(&ctx, &parse_section_colors, "regular7", false, &conf.colors.table[7]); - test_color(&ctx, &parse_section_colors, "bright0", false, &conf.colors.table[8]); - test_color(&ctx, &parse_section_colors, "bright1", false, &conf.colors.table[9]); - test_color(&ctx, &parse_section_colors, "bright2", false, &conf.colors.table[10]); - test_color(&ctx, &parse_section_colors, "bright3", false, &conf.colors.table[11]); - test_color(&ctx, &parse_section_colors, "bright4", false, &conf.colors.table[12]); - test_color(&ctx, &parse_section_colors, "bright5", false, &conf.colors.table[13]); - test_color(&ctx, &parse_section_colors, "bright6", false, &conf.colors.table[14]); - test_color(&ctx, &parse_section_colors, "bright7", false, &conf.colors.table[15]); - test_color(&ctx, &parse_section_colors, "dim0", false, &conf.colors.dim[0]); - test_color(&ctx, &parse_section_colors, "dim1", false, &conf.colors.dim[1]); - test_color(&ctx, &parse_section_colors, "dim2", false, &conf.colors.dim[2]); - test_color(&ctx, &parse_section_colors, "dim3", false, &conf.colors.dim[3]); - test_color(&ctx, &parse_section_colors, "dim4", false, &conf.colors.dim[4]); - test_color(&ctx, &parse_section_colors, "dim5", false, &conf.colors.dim[5]); - test_color(&ctx, &parse_section_colors, "dim6", false, &conf.colors.dim[6]); - test_color(&ctx, &parse_section_colors, "dim7", false, &conf.colors.dim[7]); - test_color(&ctx, &parse_section_colors, "selection-foreground", false, &conf.colors.selection_fg); - test_color(&ctx, &parse_section_colors, "selection-background", false, &conf.colors.selection_bg); - test_color(&ctx, &parse_section_colors, "urls", false, &conf.colors.url); - test_two_colors(&ctx, &parse_section_colors, "jump-labels", false, - &conf.colors.jump_label.fg, - &conf.colors.jump_label.bg); - test_two_colors(&ctx, &parse_section_colors, "scrollback-indicator", false, - &conf.colors.scrollback_indicator.fg, - &conf.colors.scrollback_indicator.bg); - test_two_colors(&ctx, &parse_section_colors, "search-box-no-match", false, - &conf.colors.search_box.no_match.fg, - &conf.colors.search_box.no_match.bg); - test_two_colors(&ctx, &parse_section_colors, "search-box-match", false, - &conf.colors.search_box.match.fg, - &conf.colors.search_box.match.bg); + test_color(&ctx, &parse_section_colors_dark, "foreground", false, &conf.colors_dark.fg); + test_color(&ctx, &parse_section_colors_dark, "background", false, &conf.colors_dark.bg); + test_color(&ctx, &parse_section_colors_dark, "regular0", false, &conf.colors_dark.table[0]); + test_color(&ctx, &parse_section_colors_dark, "regular1", false, &conf.colors_dark.table[1]); + test_color(&ctx, &parse_section_colors_dark, "regular2", false, &conf.colors_dark.table[2]); + test_color(&ctx, &parse_section_colors_dark, "regular3", false, &conf.colors_dark.table[3]); + test_color(&ctx, &parse_section_colors_dark, "regular4", false, &conf.colors_dark.table[4]); + test_color(&ctx, &parse_section_colors_dark, "regular5", false, &conf.colors_dark.table[5]); + test_color(&ctx, &parse_section_colors_dark, "regular6", false, &conf.colors_dark.table[6]); + test_color(&ctx, &parse_section_colors_dark, "regular7", false, &conf.colors_dark.table[7]); + test_color(&ctx, &parse_section_colors_dark, "bright0", false, &conf.colors_dark.table[8]); + test_color(&ctx, &parse_section_colors_dark, "bright1", false, &conf.colors_dark.table[9]); + test_color(&ctx, &parse_section_colors_dark, "bright2", false, &conf.colors_dark.table[10]); + test_color(&ctx, &parse_section_colors_dark, "bright3", false, &conf.colors_dark.table[11]); + test_color(&ctx, &parse_section_colors_dark, "bright4", false, &conf.colors_dark.table[12]); + test_color(&ctx, &parse_section_colors_dark, "bright5", false, &conf.colors_dark.table[13]); + test_color(&ctx, &parse_section_colors_dark, "bright6", false, &conf.colors_dark.table[14]); + test_color(&ctx, &parse_section_colors_dark, "bright7", false, &conf.colors_dark.table[15]); + test_color(&ctx, &parse_section_colors_dark, "dim0", false, &conf.colors_dark.dim[0]); + test_color(&ctx, &parse_section_colors_dark, "dim1", false, &conf.colors_dark.dim[1]); + test_color(&ctx, &parse_section_colors_dark, "dim2", false, &conf.colors_dark.dim[2]); + test_color(&ctx, &parse_section_colors_dark, "dim3", false, &conf.colors_dark.dim[3]); + test_color(&ctx, &parse_section_colors_dark, "dim4", false, &conf.colors_dark.dim[4]); + test_color(&ctx, &parse_section_colors_dark, "dim5", false, &conf.colors_dark.dim[5]); + test_color(&ctx, &parse_section_colors_dark, "dim6", false, &conf.colors_dark.dim[6]); + test_color(&ctx, &parse_section_colors_dark, "dim7", false, &conf.colors_dark.dim[7]); + test_color(&ctx, &parse_section_colors_dark, "selection-foreground", false, &conf.colors_dark.selection_fg); + test_color(&ctx, &parse_section_colors_dark, "selection-background", false, &conf.colors_dark.selection_bg); + test_color(&ctx, &parse_section_colors_dark, "urls", false, &conf.colors_dark.url); + test_two_colors(&ctx, &parse_section_colors_dark, "jump-labels", false, + &conf.colors_dark.jump_label.fg, + &conf.colors_dark.jump_label.bg); + test_two_colors(&ctx, &parse_section_colors_dark, "scrollback-indicator", false, + &conf.colors_dark.scrollback_indicator.fg, + &conf.colors_dark.scrollback_indicator.bg); + test_two_colors(&ctx, &parse_section_colors_dark, "search-box-no-match", false, + &conf.colors_dark.search_box.no_match.fg, + &conf.colors_dark.search_box.no_match.bg); + test_two_colors(&ctx, &parse_section_colors_dark, "search-box-match", false, + &conf.colors_dark.search_box.match.fg, + &conf.colors_dark.search_box.match.bg); - test_two_colors(&ctx, &parse_section_colors, "cursor", false, - &conf.colors.cursor.text, - &conf.colors.cursor.cursor); + test_two_colors(&ctx, &parse_section_colors_dark, "cursor", false, + &conf.colors_dark.cursor.text, + &conf.colors_dark.cursor.cursor); - test_enum(&ctx, &parse_section_colors, "alpha-mode", 3, + test_enum(&ctx, &parse_section_colors_dark, "alpha-mode", 3, (const char *[]){"default", "matching", "all"}, (int []){ALPHA_MODE_DEFAULT, ALPHA_MODE_MATCHING, ALPHA_MODE_ALL}, - (int *)&conf.colors.alpha_mode); + (int *)&conf.colors_dark.alpha_mode); - test_enum(&ctx, &parse_section_colors, "dim-blend-towards", 2, + test_enum(&ctx, &parse_section_colors_dark, "dim-blend-towards", 2, (const char *[]){"black", "white"}, (int []){DIM_BLEND_TOWARDS_BLACK, DIM_BLEND_TOWARDS_WHITE}, - (int *)&conf.colors.dim_blend_towards); + (int *)&conf.colors_dark.dim_blend_towards); for (size_t i = 0; i < 255; i++) { char key_name[4]; sprintf(key_name, "%zu", i); - test_color(&ctx, &parse_section_colors, key_name, false, - &conf.colors.table[i]); + test_color(&ctx, &parse_section_colors_dark, key_name, false, + &conf.colors_dark.table[i]); } - test_invalid_key(&ctx, &parse_section_colors, "256"); + test_invalid_key(&ctx, &parse_section_colors_dark, "256"); + + /* TODO: alpha (float in range 0-1, converted to uint16_t) */ + + config_free(&conf); +} + +static void +test_section_colors_light(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "colors-light", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_colors, "invalid-key"); + + test_color(&ctx, &parse_section_colors_light, "foreground", false, &conf.colors_light.fg); + test_color(&ctx, &parse_section_colors_light, "background", false, &conf.colors_light.bg); + test_color(&ctx, &parse_section_colors_light, "regular0", false, &conf.colors_light.table[0]); + test_color(&ctx, &parse_section_colors_light, "regular1", false, &conf.colors_light.table[1]); + test_color(&ctx, &parse_section_colors_light, "regular2", false, &conf.colors_light.table[2]); + test_color(&ctx, &parse_section_colors_light, "regular3", false, &conf.colors_light.table[3]); + test_color(&ctx, &parse_section_colors_light, "regular4", false, &conf.colors_light.table[4]); + test_color(&ctx, &parse_section_colors_light, "regular5", false, &conf.colors_light.table[5]); + test_color(&ctx, &parse_section_colors_light, "regular6", false, &conf.colors_light.table[6]); + test_color(&ctx, &parse_section_colors_light, "regular7", false, &conf.colors_light.table[7]); + test_color(&ctx, &parse_section_colors_light, "bright0", false, &conf.colors_light.table[8]); + test_color(&ctx, &parse_section_colors_light, "bright1", false, &conf.colors_light.table[9]); + test_color(&ctx, &parse_section_colors_light, "bright2", false, &conf.colors_light.table[10]); + test_color(&ctx, &parse_section_colors_light, "bright3", false, &conf.colors_light.table[11]); + test_color(&ctx, &parse_section_colors_light, "bright4", false, &conf.colors_light.table[12]); + test_color(&ctx, &parse_section_colors_light, "bright5", false, &conf.colors_light.table[13]); + test_color(&ctx, &parse_section_colors_light, "bright6", false, &conf.colors_light.table[14]); + test_color(&ctx, &parse_section_colors_light, "bright7", false, &conf.colors_light.table[15]); + test_color(&ctx, &parse_section_colors_light, "dim0", false, &conf.colors_light.dim[0]); + test_color(&ctx, &parse_section_colors_light, "dim1", false, &conf.colors_light.dim[1]); + test_color(&ctx, &parse_section_colors_light, "dim2", false, &conf.colors_light.dim[2]); + test_color(&ctx, &parse_section_colors_light, "dim3", false, &conf.colors_light.dim[3]); + test_color(&ctx, &parse_section_colors_light, "dim4", false, &conf.colors_light.dim[4]); + test_color(&ctx, &parse_section_colors_light, "dim5", false, &conf.colors_light.dim[5]); + test_color(&ctx, &parse_section_colors_light, "dim6", false, &conf.colors_light.dim[6]); + test_color(&ctx, &parse_section_colors_light, "dim7", false, &conf.colors_light.dim[7]); + test_color(&ctx, &parse_section_colors_light, "selection-foreground", false, &conf.colors_light.selection_fg); + test_color(&ctx, &parse_section_colors_light, "selection-background", false, &conf.colors_light.selection_bg); + test_color(&ctx, &parse_section_colors_light, "urls", false, &conf.colors_light.url); + test_two_colors(&ctx, &parse_section_colors_light, "jump-labels", false, + &conf.colors_light.jump_label.fg, + &conf.colors_light.jump_label.bg); + test_two_colors(&ctx, &parse_section_colors_light, "scrollback-indicator", false, + &conf.colors_light.scrollback_indicator.fg, + &conf.colors_light.scrollback_indicator.bg); + test_two_colors(&ctx, &parse_section_colors_light, "search-box-no-match", false, + &conf.colors_light.search_box.no_match.fg, + &conf.colors_light.search_box.no_match.bg); + test_two_colors(&ctx, &parse_section_colors_light, "search-box-match", false, + &conf.colors_light.search_box.match.fg, + &conf.colors_light.search_box.match.bg); + + test_two_colors(&ctx, &parse_section_colors_light, "cursor", false, + &conf.colors_light.cursor.text, + &conf.colors_light.cursor.cursor); + + test_enum(&ctx, &parse_section_colors_light, "alpha-mode", 3, + (const char *[]){"default", "matching", "all"}, + (int []){ALPHA_MODE_DEFAULT, ALPHA_MODE_MATCHING, ALPHA_MODE_ALL}, + (int *)&conf.colors_light.alpha_mode); + + test_enum(&ctx, &parse_section_colors_light, "dim-blend-towards", 2, + (const char *[]){"black", "white"}, + (int []){DIM_BLEND_TOWARDS_BLACK, DIM_BLEND_TOWARDS_WHITE}, + (int *)&conf.colors_light.dim_blend_towards); + + for (size_t i = 0; i < 255; i++) { + char key_name[4]; + sprintf(key_name, "%zu", i); + test_color(&ctx, &parse_section_colors_light, key_name, false, + &conf.colors_light.table[i]); + } + + test_invalid_key(&ctx, &parse_section_colors_light, "256"); /* TODO: alpha (float in range 0-1, converted to uint16_t) */ @@ -1444,7 +1531,8 @@ main(int argc, const char *const *argv) test_section_cursor(); test_section_mouse(); test_section_touch(); - test_section_colors(); + test_section_colors_dark(); + test_section_colors_light(); test_section_csd(); test_section_key_bindings(); test_section_key_bindings_collisions(); diff --git a/themes/aeroroot b/themes/aeroroot index 2a0e0985..dbeb2e81 100644 --- a/themes/aeroroot +++ b/themes/aeroroot @@ -1,7 +1,7 @@ # -*- conf -*- # Aero root theme -[colors] +[colors-dark] cursor=1a1a1a 9fd5f5 foreground=dedeef background=1a1a1a diff --git a/themes/alacritty b/themes/alacritty index 14503887..68d1c68c 100644 --- a/themes/alacritty +++ b/themes/alacritty @@ -1,7 +1,7 @@ # -*- conf -*- # Alacritty -[colors] +[colors-dark] cursor = 181818 56d8c9 background= 181818 foreground= d8d8d8 diff --git a/themes/apprentice b/themes/apprentice index 6b67d21d..291ab8db 100644 --- a/themes/apprentice +++ b/themes/apprentice @@ -1,7 +1,7 @@ # -*- conf -*- # https://github.com/romainl/Apprentice -[colors] +[colors-dark] cursor=262626 6c6c6c foreground=bcbcbc background=262626 diff --git a/themes/ayu-mirage b/themes/ayu-mirage index 4646e418..2d9b6b54 100644 --- a/themes/ayu-mirage +++ b/themes/ayu-mirage @@ -2,7 +2,7 @@ # theme: Ayu Mirage # description: a theme based on Ayu Mirage for Sublime Text (original: https://github.com/dempfi/ayu) -[colors] +[colors-dark] cursor = ffcc66 665a44 foreground = cccac2 background = 242936 diff --git a/themes/catppuccin-frappe b/themes/catppuccin-frappe index 44bef16c..3acae600 100644 --- a/themes/catppuccin-frappe +++ b/themes/catppuccin-frappe @@ -1,7 +1,7 @@ # _*_ conf _*_ # Catppuccin Frappe -[colors] +[colors-dark] foreground=c6d0f5 background=303446 diff --git a/themes/catppuccin-latte b/themes/catppuccin-latte index d0b90e64..ca7a7aae 100644 --- a/themes/catppuccin-latte +++ b/themes/catppuccin-latte @@ -1,7 +1,10 @@ # _*_ conf _*_ # Catppuccin Latte -[colors] +[main] +initial-color-theme=light + +[colors-light] foreground=4c4f69 background=eff1f5 diff --git a/themes/catppuccin-macchiato b/themes/catppuccin-macchiato index ae8adab8..8f5ea36e 100644 --- a/themes/catppuccin-macchiato +++ b/themes/catppuccin-macchiato @@ -1,7 +1,7 @@ # _*_ conf _*_ # Catppuccin Macchiato -[colors] +[colors-dark] foreground=cad3f5 background=24273a diff --git a/themes/catppuccin-mocha b/themes/catppuccin-mocha index d29eb0ec..7d98dc0f 100644 --- a/themes/catppuccin-mocha +++ b/themes/catppuccin-mocha @@ -1,7 +1,7 @@ # _*_ conf _*_ # Catppuccin Mocha -[colors] +[colors-dark] foreground=cdd6f4 background=1e1e2e diff --git a/themes/chiba-dark b/themes/chiba-dark index 8727f684..ffaf6cb2 100644 --- a/themes/chiba-dark +++ b/themes/chiba-dark @@ -3,7 +3,7 @@ # author: ayushnix (https://sr.ht/~ayushnix) # description: A dark theme with bright cyberpunk colors (WCAG AAA compliant) -[colors] +[colors-dark] cursor = 181818 cdcdcd foreground = cdcdcd background = 181818 diff --git a/themes/derp b/themes/derp index 45eed752..42af3377 100644 --- a/themes/derp +++ b/themes/derp @@ -1,7 +1,7 @@ # -*- conf -*- # Derp -[colors] +[colors-dark] cursor=000000 ffffff foreground=ffffff background=000000 diff --git a/themes/deus b/themes/deus index 0d52e55b..69c44944 100644 --- a/themes/deus +++ b/themes/deus @@ -2,7 +2,7 @@ # Deus # Color palette based on: https://github.com/ajmwagar/vim-deus -[colors] +[colors-dark] cursor=2c323b eaeaea background=2c323b foreground=eaeaea diff --git a/themes/dracula b/themes/dracula index 008fc150..82994203 100644 --- a/themes/dracula +++ b/themes/dracula @@ -1,7 +1,7 @@ # -*- conf -*- # Dracula -[colors] +[colors-dark] cursor=282a36 f8f8f2 foreground=f8f8f2 background=282a36 diff --git a/themes/dracula-iterm b/themes/dracula-iterm index 249bb6ab..b75ddd9c 100644 --- a/themes/dracula-iterm +++ b/themes/dracula-iterm @@ -1,7 +1,7 @@ # -*- conf -*- # Dracula iTerm2 variant -[colors] +[colors-dark] cursor=ffffff bbbbbb foreground=f8f8f2 background=1e1f29 diff --git a/themes/electrophoretic b/themes/electrophoretic index e0bf6e79..8bc022ea 100644 --- a/themes/electrophoretic +++ b/themes/electrophoretic @@ -5,7 +5,10 @@ # text and the white background. # author: Eugen Rahaian <eugen@rah.ro> -[colors] +[main] +initial-color-theme=light + +[colors-light] cursor=ffffff 515151 background= ffffff foreground= 000000 diff --git a/themes/gruvbox b/themes/gruvbox index 6bc97352..e44f3ea9 100644 --- a/themes/gruvbox +++ b/themes/gruvbox @@ -1,7 +1,7 @@ # -*- conf -*- # Gruvbox -[colors] +[colors-dark] background=282828 foreground=ebdbb2 regular0=282828 @@ -21,7 +21,7 @@ bright5=d3869b bright6=8ec07c bright7=ebdbb2 -[colors2] +[colors-light] background=fbf1c7 foreground=3c3836 regular0=fbf1c7 diff --git a/themes/gruvbox-dark b/themes/gruvbox-dark index 73207199..c5dadcc5 100644 --- a/themes/gruvbox-dark +++ b/themes/gruvbox-dark @@ -1,7 +1,7 @@ # -*- conf -*- # Gruvbox -[colors] +[colors-dark] background=282828 foreground=ebdbb2 regular0=282828 diff --git a/themes/gruvbox-light b/themes/gruvbox-light index 6a7a2416..6b616612 100644 --- a/themes/gruvbox-light +++ b/themes/gruvbox-light @@ -1,7 +1,10 @@ # -*- conf -*- # Gruvbox - Light -[colors] +[main] +initial-color-theme=light + +[colors-light] background=fbf1c7 foreground=3c3836 regular0=fbf1c7 diff --git a/themes/hacktober b/themes/hacktober index dfcc4c7e..ecdb18fb 100644 --- a/themes/hacktober +++ b/themes/hacktober @@ -1,6 +1,6 @@ # -*- conf -*- -[colors] +[colors-dark] cursor=141414 c9c9c9 foreground=c9c9c9 background=141414 diff --git a/themes/iterm b/themes/iterm index 45b1a0bf..c5ffc190 100644 --- a/themes/iterm +++ b/themes/iterm @@ -2,7 +2,7 @@ # this foot theme is based on alacritty iterm theme: # https://github.com/alacritty/alacritty-theme/blob/master/themes/iterm.toml -[colors] +[colors-dark] foreground=fffbf6 background=101421 diff --git a/themes/jetbrains-darcula b/themes/jetbrains-darcula index e6997848..0092b795 100644 --- a/themes/jetbrains-darcula +++ b/themes/jetbrains-darcula @@ -2,7 +2,7 @@ # JetBrains Darcula # Palette based on the same theme from https://github.com/dexpota/kitty-themes -[colors] +[colors-dark] cursor=202020 ffffff background=202020 foreground=adadad diff --git a/themes/kitty b/themes/kitty index f43eea9d..81fd003e 100644 --- a/themes/kitty +++ b/themes/kitty @@ -1,6 +1,6 @@ # -*- conf -*- -[colors] +[colors-dark] cursor=111111 cccccc foreground=dddddd background=000000 diff --git a/themes/material-amber b/themes/material-amber index 27983833..69126aa0 100644 --- a/themes/material-amber +++ b/themes/material-amber @@ -2,7 +2,10 @@ # Material Amber # Based on material.io guidelines with Amber 50 background -[colors] +[main] +initial-color-theme=light + +[colors-light] cursor=fff8e1 21201d foreground = 21201d background = fff8e1 diff --git a/themes/material-design b/themes/material-design index 4a9e008a..bf1d0a6b 100644 --- a/themes/material-design +++ b/themes/material-design @@ -2,7 +2,7 @@ # Material # From https://github.com/MartinSeeler/iterm2-material-design -[colors] +[colors-dark] foreground=ECEFF1 background=263238 regular0=546E7A # black diff --git a/themes/modus-operandi b/themes/modus-operandi index 2d417bb5..6baca2f7 100644 --- a/themes/modus-operandi +++ b/themes/modus-operandi @@ -3,7 +3,11 @@ # modus-operandi # See: https://protesilaos.com/emacs/modus-themes # -[colors] + +[main] +initial-color-theme=light + +[colors-light] background=ffffff foreground=000000 regular0=000000 diff --git a/themes/modus-vivendi b/themes/modus-vivendi index 82b1075d..9ee670ec 100644 --- a/themes/modus-vivendi +++ b/themes/modus-vivendi @@ -4,7 +4,7 @@ # See: https://protesilaos.com/emacs/modus-themes # -[colors] +[colors-dark] background=000000 foreground=ffffff regular0=000000 diff --git a/themes/modus-vivendi-tinted b/themes/modus-vivendi-tinted index 67cf02a0..6a61fc79 100644 --- a/themes/modus-vivendi-tinted +++ b/themes/modus-vivendi-tinted @@ -4,7 +4,7 @@ # See: https://protesilaos.com/emacs/modus-themes # -[colors] +[colors-dark] background=0d0e1c foreground=ffffff regular0=000000 diff --git a/themes/molokai b/themes/molokai index c3935f69..19e1b6fa 100644 --- a/themes/molokai +++ b/themes/molokai @@ -2,7 +2,7 @@ # Molokai # Based on zhou13's at https://github.com/zhou13/molokai-terminal/blob/master/xterm/Xresources -[colors] +[colors-dark] background=1B1D1E foreground=CCCCCC regular0=1B1D1E diff --git a/themes/monokai-pro b/themes/monokai-pro index 5d9f31a9..3044da91 100644 --- a/themes/monokai-pro +++ b/themes/monokai-pro @@ -1,7 +1,7 @@ # -*- conf -*- # Monokai Pro -[colors] +[colors-dark] background=2D2A2E foreground=FCFCFA regular0=403E41 diff --git a/themes/moonfly b/themes/moonfly index 0dbe0e95..b30e3156 100644 --- a/themes/moonfly +++ b/themes/moonfly @@ -2,7 +2,7 @@ # moonfly # Based on https://github.com/bluz71/vim-moonfly-colors -[colors] +[colors-dark] cursor = 080808 9e9e9e foreground = b2b2b2 background = 080808 diff --git a/themes/neon b/themes/neon index d11a36d0..74884e03 100644 --- a/themes/neon +++ b/themes/neon @@ -6,7 +6,7 @@ # https://xcolors.net/neon # -[colors] +[colors-dark] foreground=f8f8f8 background=171717 regular0=171717 diff --git a/themes/night-owl b/themes/night-owl index 43a5c054..e9e40404 100644 --- a/themes/night-owl +++ b/themes/night-owl @@ -1,7 +1,7 @@ # _*_ conf _*_ # Night Owl -[colors] +[colors-dark] cursor=011627 80a4c2 foreground=d6deeb background=011627 diff --git a/themes/nightfly b/themes/nightfly index 37205f0f..ccdd183a 100644 --- a/themes/nightfly +++ b/themes/nightfly @@ -2,7 +2,7 @@ # nightfly # Based on https://github.com/bluz71/vim-nightfly-guicolors -[colors] +[colors-dark] cursor = 080808 9ca1aa foreground = acb4c2 background = 011627 diff --git a/themes/noirblaze b/themes/noirblaze index 42daf11b..b21055a4 100644 --- a/themes/noirblaze +++ b/themes/noirblaze @@ -3,7 +3,7 @@ # https://github.com/n1ghtmare/noirblaze-kitty -[colors] +[colors-dark] cursor=121212 ff0088 foreground=d5d5d5 background=121212 diff --git a/themes/nord b/themes/nord index 9b988ad6..eb2fdf0f 100644 --- a/themes/nord +++ b/themes/nord @@ -6,7 +6,7 @@ # this specific foot theme is based on nord-alacritty: # https://github.com/arcticicestudio/nord-alacritty/blob/develop/src/nord.yml -[colors] +[colors-dark] cursor = 2e3440 d8dee9 foreground = d8dee9 background = 2e3440 diff --git a/themes/nordiq b/themes/nordiq index 0df5c7de..1efccba6 100644 --- a/themes/nordiq +++ b/themes/nordiq @@ -1,7 +1,7 @@ # -*- conf -*- # Nordiq -[colors] +[colors-dark] cursor=eeeeee 9f515a foreground=dbdee9 background=0e1420 diff --git a/themes/nvim b/themes/nvim index bf629c0a..74dd1ac6 100644 --- a/themes/nvim +++ b/themes/nvim @@ -3,7 +3,7 @@ # Uses the dark color palette from the default Neovim color scheme # See: https://github.com/neovim/neovim/blob/fb6c059dc55c8d594102937be4dd70f5ff51614a/src/nvim/highlight_group.c#L419 -[colors] +[colors-dark] cursor=14161b e0e2ea # NvimDarkGrey2 NvimLightGrey2 foreground=e0e2ea # NvimLightGrey2 background=14161b # NvimDarkGrey2 @@ -29,7 +29,7 @@ bright5=ffcaff # NvimLightMagenta bright6=8cf8f7 # NvimLightCyan bright7=eef1f8 # NvimLightGrey1 -[colors2] +[colors-light] cursor=e0e2ea 14161b # NvimLightGrey2 NvimDarkGrey2 foreground=14161b # NvimDarkGrey2 background=e0e2ea # NvimLightGrey2 diff --git a/themes/nvim-dark b/themes/nvim-dark index 9a177770..fe3afb74 100644 --- a/themes/nvim-dark +++ b/themes/nvim-dark @@ -3,7 +3,7 @@ # Uses the dark color palette from the default Neovim color scheme # See: https://github.com/neovim/neovim/blob/fb6c059dc55c8d594102937be4dd70f5ff51614a/src/nvim/highlight_group.c#L419 -[colors] +[colors-dark] cursor=14161b e0e2ea # NvimDarkGrey2 NvimLightGrey2 foreground=e0e2ea # NvimLightGrey2 background=14161b # NvimDarkGrey2 diff --git a/themes/nvim-light b/themes/nvim-light index aca4e156..fd8943b1 100644 --- a/themes/nvim-light +++ b/themes/nvim-light @@ -3,7 +3,10 @@ # Uses the light color palette from the default Neovim color scheme # See: https://github.com/neovim/neovim/blob/fb6c059dc55c8d594102937be4dd70f5ff51614a/src/nvim/highlight_group.c#L334 -[colors] +[main] +initial-color-theme=light + +[colors-light] cursor=e0e2ea 14161b # NvimLightGrey2 NvimDarkGrey2 foreground=14161b # NvimDarkGrey2 background=e0e2ea # NvimLightGrey2 diff --git a/themes/onedark b/themes/onedark index 0932960b..6d66e87e 100644 --- a/themes/onedark +++ b/themes/onedark @@ -1,7 +1,7 @@ # OneDark # Palette based on the same theme from https://github.com/dexpota/kitty-themes -[colors] +[colors-dark] cursor=111111 cccccc foreground=979eab background=282c34 diff --git a/themes/onehalf-dark b/themes/onehalf-dark index 1adc9e23..1faca455 100644 --- a/themes/onehalf-dark +++ b/themes/onehalf-dark @@ -7,7 +7,7 @@ # + cursor colors from: # https://github.com/sonph/onehalf/blob/master/iterm/OneHalfDark.itermcolors -[colors] +[colors-dark] cursor=dcdfe4 a3b3cc foreground=dcdfe4 background=282c34 diff --git a/themes/panda b/themes/panda index b02c7e9f..2c1dc7c5 100644 --- a/themes/panda +++ b/themes/panda @@ -1,7 +1,7 @@ # -*- conf -*- # http://panda.siamak.me/ -[colors] +[colors-dark] # alpha=1.0 background=1D1E20 foreground=F0F0F0 diff --git a/themes/paper-color b/themes/paper-color index f158c148..09934925 100644 --- a/themes/paper-color +++ b/themes/paper-color @@ -2,7 +2,7 @@ # PaperColorDark # Palette based on https://github.com/NLKNguyen/papercolor-theme -[colors] +[colors-dark] cursor=1c1c1c eeeeee background=1c1c1c foreground=eeeeee @@ -25,7 +25,7 @@ bright7=5f8787 # bright white # selection-foreground=1c1c1c # selection-background=af87d7 -[colors2] +[colors-light] cursor=eeeeee 444444 background=eeeeee foreground=444444 diff --git a/themes/paper-color-dark b/themes/paper-color-dark index 991bcc9d..26260c6f 100644 --- a/themes/paper-color-dark +++ b/themes/paper-color-dark @@ -2,7 +2,7 @@ # PaperColorDark # Palette based on https://github.com/NLKNguyen/papercolor-theme -[colors] +[colors-dark] cursor=1c1c1c eeeeee background=1c1c1c foreground=eeeeee diff --git a/themes/paper-color-light b/themes/paper-color-light index b8a6ceec..2f7a8003 100644 --- a/themes/paper-color-light +++ b/themes/paper-color-light @@ -2,7 +2,10 @@ # PaperColor Light # Palette based on https://github.com/NLKNguyen/papercolor-theme -[colors] +[main] +initial-color-theme=light +xs +[colors-light] cursor=eeeeee 444444 background=eeeeee foreground=444444 diff --git a/themes/poimandres b/themes/poimandres index b4edc175..a2123ac5 100644 --- a/themes/poimandres +++ b/themes/poimandres @@ -1,7 +1,7 @@ # Based on Poimandres color theme for kitti terminal emulator # https://github.com/ubmit/poimandres-kitty -[colors] +[colors-dark] cursor=1b1e28 ffffff foreground=a6accd background=1b1e28 diff --git a/themes/rezza b/themes/rezza index 56814a77..62a08cc2 100644 --- a/themes/rezza +++ b/themes/rezza @@ -13,7 +13,7 @@ # and also posted here: # https://forums.debian.net/viewtopic.php?t=29981 -[colors] +[colors-dark] foreground = cccccc background = 191911 diff --git a/themes/rose-pine b/themes/rose-pine index 2cae00e8..b9aa7e2a 100644 --- a/themes/rose-pine +++ b/themes/rose-pine @@ -1,7 +1,7 @@ # -*- conf -*- # Rosé Pine -[colors] +[colors-dark] cursor=191724 e0def4 background=191724 foreground=e0def4 diff --git a/themes/rose-pine-dawn b/themes/rose-pine-dawn index 674c7a21..d2742c72 100644 --- a/themes/rose-pine-dawn +++ b/themes/rose-pine-dawn @@ -1,7 +1,11 @@ # -*- conf -*- # Rosé Pine Dawn -[colors] +[main] +initial-color-theme=light + + +[colors-light] cursor=faf4ed 575279 background=faf4ed foreground=575279 diff --git a/themes/rose-pine-moon b/themes/rose-pine-moon index cbc81451..51b9a33a 100644 --- a/themes/rose-pine-moon +++ b/themes/rose-pine-moon @@ -1,7 +1,7 @@ # -*- conf -*- # Rosé Pine Moon -[colors] +[colors-dark] cursor=232136 e0def4 background=232136 foreground=e0def4 diff --git a/themes/selenized b/themes/selenized index cde35723..83fea617 100644 --- a/themes/selenized +++ b/themes/selenized @@ -1,7 +1,7 @@ # -*- conf -*- # Selenized dark -[colors] +[colors-dark] cursor = 103c48 53d6c7 background= 103c48 foreground= adbcbc @@ -24,7 +24,7 @@ bright5= ff84cd bright6= 53d6c7 bright7= cad8d9 -[colors2] +[colors-light] cursor=fbf3db 00978a background= fbf3db foreground= 53676d diff --git a/themes/selenized-black b/themes/selenized-black index 591751f0..8a93187e 100644 --- a/themes/selenized-black +++ b/themes/selenized-black @@ -1,7 +1,7 @@ # -*- conf -*- # Selenized black -[colors] +[colors-dark] cursor = 181818 56d8c9 background= 181818 foreground= b9b9b9 diff --git a/themes/selenized-dark b/themes/selenized-dark index 5d062dec..8ace1c05 100644 --- a/themes/selenized-dark +++ b/themes/selenized-dark @@ -1,7 +1,7 @@ # -*- conf -*- # Selenized dark -[colors] +[colors-dark] cursor = 103c48 53d6c7 background= 103c48 foreground= adbcbc diff --git a/themes/selenized-light b/themes/selenized-light index 04dffbea..c842fc3c 100644 --- a/themes/selenized-light +++ b/themes/selenized-light @@ -1,7 +1,10 @@ # -*- conf -*- # Selenized light -[colors] +[main] +initial-color-theme=light + +[colors-light] cursor=fbf3db 00978a background= fbf3db foreground= 53676d diff --git a/themes/selenized-white b/themes/selenized-white index 5a7d68b2..659bf814 100644 --- a/themes/selenized-white +++ b/themes/selenized-white @@ -1,7 +1,10 @@ # -*- conf -*- # Selenized white -[colors] +[main] +initial-color-theme=light + +[colors-light] cursor=ffffff 009a8a background= ffffff foreground= 474747 diff --git a/themes/solarized b/themes/solarized index 335c738e..f1844b3c 100644 --- a/themes/solarized +++ b/themes/solarized @@ -2,7 +2,7 @@ # Solarized dark+light # Dark -[colors] +[colors-dark] cursor= 002b36 93a1a1 background= 002b36 foreground= 839496 @@ -25,7 +25,7 @@ bright7= fdf6e3 # Light -[colors2] +[colors-light] cursor= fdf6e3 586e75 background= fdf6e3 foreground= 657b83 diff --git a/themes/solarized-dark b/themes/solarized-dark index 4997eb4a..6335fa0f 100644 --- a/themes/solarized-dark +++ b/themes/solarized-dark @@ -1,7 +1,7 @@ # -*- conf -*- # Solarized dark -[colors] +[colors-dark] cursor= 002b36 93a1a1 background= 002b36 foreground= 839496 diff --git a/themes/solarized-dark-normal-brights b/themes/solarized-dark-normal-brights index f0c2172d..7b608110 100644 --- a/themes/solarized-dark-normal-brights +++ b/themes/solarized-dark-normal-brights @@ -1,7 +1,7 @@ # -*- conf -*- # Solarized dark -[colors] +[colors-dark] cursor= 002b36 93a1a1 background= 002b36 foreground= 839496 diff --git a/themes/solarized-light b/themes/solarized-light index 3d750277..db27be43 100644 --- a/themes/solarized-light +++ b/themes/solarized-light @@ -1,7 +1,10 @@ # -*- conf -*- # Solarized light -[colors] +[main] +initial-color-theme=light + +[colors-light] cursor= fdf6e3 586e75 background= fdf6e3 foreground= 657b83 diff --git a/themes/solarized-normal-brights b/themes/solarized-normal-brights index a7724cd3..3bd3c189 100644 --- a/themes/solarized-normal-brights +++ b/themes/solarized-normal-brights @@ -9,7 +9,7 @@ # encoding to sRGB again. # Dark -[colors] +[colors-dark] cursor= 002b36 93a1a1 background= 002b36 foreground= 839496 @@ -32,7 +32,7 @@ bright7= ffffff # Light -[colors2] +[colors-light] cursor= fdf6e3 586e75 background= fdf6e3 foreground= 657b83 diff --git a/themes/srcery b/themes/srcery index 54966707..612c82cc 100644 --- a/themes/srcery +++ b/themes/srcery @@ -1,6 +1,6 @@ # srcery -[colors] +[colors-dark] background= 1c1b19 foreground= fce8c3 regular0= 1c1b19 diff --git a/themes/starlight b/themes/starlight index ed39f277..81ce1a5f 100644 --- a/themes/starlight +++ b/themes/starlight @@ -1,7 +1,7 @@ # -*- conf -*- # Theme: starlight V4 (https://github.com/CosmicToast/starlight) -[colors] +[colors-dark] foreground = FFFFFF background = 242424 diff --git a/themes/tango b/themes/tango index a93d29cb..5ea43f63 100644 --- a/themes/tango +++ b/themes/tango @@ -1,7 +1,7 @@ # -*- conf -*- # Tango -[colors] +[colors-dark] cursor=000000 babdb6 foreground=babdb6 background=000000 diff --git a/themes/tempus-autumn b/themes/tempus-autumn index 74228e90..214478bb 100644 --- a/themes/tempus-autumn +++ b/themes/tempus-autumn @@ -3,7 +3,7 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a palette inspired by earthly colours (WCAG AA compliant) -[colors] +[colors-dark] #cursor = 302420 a9a2a6 foreground = a9a2a6 background = 302420 diff --git a/themes/tempus-classic b/themes/tempus-classic index b35dc5e5..95b37b76 100644 --- a/themes/tempus-classic +++ b/themes/tempus-classic @@ -3,7 +3,7 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with warm hues (WCAG AA compliant) -[colors] +[colors-dark] #cursor = 232323 aeadaf foreground = aeadaf background = 232323 diff --git a/themes/tempus-dawn b/themes/tempus-dawn index dc45f29d..c288544e 100644 --- a/themes/tempus-dawn +++ b/themes/tempus-dawn @@ -3,7 +3,11 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme with a soft, slightly desaturated palette (WCAG AA compliant) -[colors] +[main] +initial-color-theme=light + + +[colors-light] #cursor = eff0f2 4a4b4e foreground = 4a4b4e background = eff0f2 diff --git a/themes/tempus-day b/themes/tempus-day index 1df70137..03454f04 100644 --- a/themes/tempus-day +++ b/themes/tempus-day @@ -3,7 +3,10 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme with warm colours (WCAG AA compliant) -[colors] +[main] +initial-color-theme=light + +[colors-light] #cursor = f8f2e5 464340 foreground = 464340 background = f8f2e5 diff --git a/themes/tempus-dusk b/themes/tempus-dusk index 5b4d1bea..cd27aaaa 100644 --- a/themes/tempus-dusk +++ b/themes/tempus-dusk @@ -3,7 +3,7 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a deep blue-ish, slightly desaturated palette (WCAG AA compliant) -[colors] +[colors-dark] #cursor = 1f252d a2a8ba foreground = a2a8ba background = 1f252d diff --git a/themes/tempus-fugit b/themes/tempus-fugit index ebd082fe..b9dce351 100644 --- a/themes/tempus-fugit +++ b/themes/tempus-fugit @@ -3,7 +3,10 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light, pleasant theme optimised for long writing/coding sessions (WCAG AA compliant) -[colors] +[main] +initial-color-theme=light + +[colors-light] #cursor = fff5f3 4d595f foreground = 4d595f background = fff5f3 diff --git a/themes/tempus-future b/themes/tempus-future index c97d379d..1f8c3c79 100644 --- a/themes/tempus-future +++ b/themes/tempus-future @@ -3,7 +3,7 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with colours inspired by concept art of outer space (WCAG AAA compliant) -[colors] +[colors-dark] #cursor = 090a18 b4abac foreground = b4abac background = 090a18 diff --git a/themes/tempus-night b/themes/tempus-night index 7c97681d..aae80f02 100644 --- a/themes/tempus-night +++ b/themes/tempus-night @@ -3,7 +3,7 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: High contrast dark theme with bright colours (WCAG AAA compliant) -[colors] +[colors-dark] #cursor = 1a1a1a e0e0e0 foreground = e0e0e0 background = 1a1a1a diff --git a/themes/tempus-past b/themes/tempus-past index af408b00..5f90ddf1 100644 --- a/themes/tempus-past +++ b/themes/tempus-past @@ -3,7 +3,10 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme inspired by old vaporwave concept art (WCAG AA compliant) -[colors] +[main] +initial-color-theme=light + +[colors-light] #cursor = f3f2f4 53545b foreground = 53545b background = f3f2f4 diff --git a/themes/tempus-rift b/themes/tempus-rift index e0cea4da..8add657a 100644 --- a/themes/tempus-rift +++ b/themes/tempus-rift @@ -3,7 +3,7 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a subdued palette on the green side of the spectrum (WCAG AA compliant) -[colors] +[colors-dark] #cursor = 162c22 bbbcbc foreground = bbbcbc background = 162c22 diff --git a/themes/tempus-spring b/themes/tempus-spring index b98be3b4..eb15a1be 100644 --- a/themes/tempus-spring +++ b/themes/tempus-spring @@ -3,7 +3,7 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a palette inspired by early spring colours (WCAG AA compliant) -[colors] +[colors-dark] #cursor = 283a37 b5b8b7 foreground = b5b8b7 background = 283a37 diff --git a/themes/tempus-summer b/themes/tempus-summer index cd904010..74c8faa2 100644 --- a/themes/tempus-summer +++ b/themes/tempus-summer @@ -3,7 +3,7 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with colours inspired by summer evenings by the sea (WCAG AA compliant) -[colors] +[colors-dark] #cursor = 202c3d a0abae foreground = a0abae background = 202c3d diff --git a/themes/tempus-tempest b/themes/tempus-tempest index 2c84454e..f1cf55bf 100644 --- a/themes/tempus-tempest +++ b/themes/tempus-tempest @@ -3,7 +3,7 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: A green-scale, subtle theme for late night hackers (WCAG AAA compliant) -[colors] +[colors-dark] #cursor = 282b2b b6e0ca foreground = b6e0ca background = 282b2b diff --git a/themes/tempus-totus b/themes/tempus-totus index 3eb21644..fae6ede3 100644 --- a/themes/tempus-totus +++ b/themes/tempus-totus @@ -3,7 +3,10 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme for prose or for coding in an open space (WCAG AAA compliant) -[colors] +[main] +initial-color-theme=light + +[colors-light] #cursor = ffffff 4a484d foreground = 4a484d background = ffffff diff --git a/themes/tempus-warp b/themes/tempus-warp index 911fb266..906b3f37 100644 --- a/themes/tempus-warp +++ b/themes/tempus-warp @@ -3,7 +3,7 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a vibrant palette (WCAG AA compliant) -[colors] +[colors-dark] #cursor = 001514 a29fa0 foreground = a29fa0 background = 001514 diff --git a/themes/tempus-winter b/themes/tempus-winter index e4307142..dc95128b 100644 --- a/themes/tempus-winter +++ b/themes/tempus-winter @@ -3,7 +3,7 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a palette inspired by winter nights at the city (WCAG AA compliant) -[colors] +[colors-dark] #cursor = 202427 8da3b8 foreground = 8da3b8 background = 202427 diff --git a/themes/tokyonight-light b/themes/tokyonight-light index ffcae689..359a31b9 100644 --- a/themes/tokyonight-light +++ b/themes/tokyonight-light @@ -2,7 +2,10 @@ # Reference: https://github.com/tokyo-night/tokyo-night-vscode-theme/blob/master/themes/tokyo-night-light-color-theme.json -[colors] +[main] +initial-color-theme=light + +[colors-light] background=d6d8df foreground=343b58 regular0=343b58 diff --git a/themes/tokyonight-night b/themes/tokyonight-night index f789e1bd..58037f72 100644 --- a/themes/tokyonight-night +++ b/themes/tokyonight-night @@ -1,6 +1,6 @@ # -*- conf -*- -[colors] +[colors-dark] background=1a1b26 foreground=c0caf5 regular0=15161E diff --git a/themes/tokyonight-storm b/themes/tokyonight-storm index 074b4697..4dbbf6c6 100644 --- a/themes/tokyonight-storm +++ b/themes/tokyonight-storm @@ -1,6 +1,6 @@ # -*- conf -*- -[colors] +[colors-dark] background=24283b foreground=c0caf5 regular0=1D202F diff --git a/themes/visibone b/themes/visibone index 9979bee0..b989b36b 100644 --- a/themes/visibone +++ b/themes/visibone @@ -1,7 +1,7 @@ # -*- conf -*- # VisiBone -[colors] +[colors-dark] cursor=010101 ffffff foreground=ffffff background=010101 diff --git a/themes/xterm b/themes/xterm index bf17f5e7..a9382fd8 100644 --- a/themes/xterm +++ b/themes/xterm @@ -1,7 +1,7 @@ # -*- conf -*- # The default palette of xterm. -[colors] +[colors-dark] foreground=e5e5e5 background=000000 regular0=000000 # black diff --git a/themes/zenburn b/themes/zenburn index bace080c..37a26812 100644 --- a/themes/zenburn +++ b/themes/zenburn @@ -1,6 +1,6 @@ # -*- conf -*- -[colors] +[colors-dark] foreground=dcdccc background=111111 From 1caba0d993c7bac68b555efb5d6dfab2577fcd02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 20 Dec 2025 15:56:32 +0100 Subject: [PATCH 1284/1323] config: remove deprecated config option cursor.color This option was deprecated in 1.23.0. Use colors-{dark,light}.cursor instead. --- CHANGELOG.md | 5 +++++ config.c | 22 ---------------------- 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae9feb54..3a73893d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,11 @@ ### Removed + +* `cursor.color` config option (deprecated in 1.23.0). Use + `colors-{dark,light}.cursor` instead. + + ### Fixed * Search mode: composing keys not ignored. diff --git a/config.c b/config.c index 0340d418..14e836c1 100644 --- a/config.c +++ b/config.c @@ -1661,28 +1661,6 @@ parse_section_cursor(struct context *ctx) else if (streq(key, "blink-rate")) return value_to_uint32(ctx, 10, &conf->cursor.blink.rate_ms); - else if (streq(key, "color")) { - LOG_WARN("%s:%d: cursor.color: deprecated; use colors.cursor instead", - ctx->path, ctx->lineno); - - user_notification_add( - &conf->notifications, - USER_NOTIFICATION_DEPRECATED, - xstrdup("cursor.color: use colors.cursor instead")); - - if (!value_to_two_colors( - ctx, - &conf->colors_dark.cursor.text, - &conf->colors_dark.cursor.cursor, - false)) - { - return false; - } - - conf->colors_dark.use_custom.cursor = true; - return true; - } - else if (streq(key, "beam-thickness")) return value_to_pt_or_px(ctx, &conf->cursor.beam_thickness); From aa26676c43edf2cfcc3543a443a25e881e34473e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 12 Nov 2025 10:22:17 +0100 Subject: [PATCH 1285/1323] builtin terminfo: add custom 'query-os-name' Inspired by Kitty's 'kitty-query-os_name'. Notable changes: * Drop kitty prefix * os_name -> os-name * Use "uname -s" without any transformations (e.g. no lower-casing) $ ./utils/xtgettcap query-os-name reply: (44 chars): <ESC>P1+r71756572792d6f732d6e616d65=4C696E7578<ESC>\ query-os-name=Linux Closes #2209 --- CHANGELOG.md | 3 +++ README.md | 4 ++++ scripts/generate-builtin-terminfo.py | 2 ++ 3 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a73893d..6fa7439e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,8 +76,11 @@ Wayland protocol ([#2212][2212]). * `[colors-dark]` section to `foot.ini`. Replaces `[colors]`. * `[colors-light]` section to `foot.ini`. Replaces `[colors2]`. +* `XTGETTCAP`: added `query-os-name`, returning the OS foot is + compiled for (e.g. _'Linux'_) ([#2209][2209]). [2212]: https://codeberg.org/dnkl/foot/issues/2212 +[2209]: https://codeberg.org/dnkl/foot/issues/2209 ### Changed diff --git a/README.md b/README.md index e8f3c8cd..7ee771ba 100644 --- a/README.md +++ b/README.md @@ -641,6 +641,10 @@ All replies are in `tigetstr()` format. That is, given the same capability name, foot's reply is identical to what `tigetstr()` would have returned. +In addition to queries for terminfo entries, the `query-os-name` query +will be answered with a response of the form `uname=$(uname -s)`, +where `$(uname -s)` is the name of the OS foot was compiled for. + # Credits diff --git a/scripts/generate-builtin-terminfo.py b/scripts/generate-builtin-terminfo.py index 28b31b57..6a6ba68c 100755 --- a/scripts/generate-builtin-terminfo.py +++ b/scripts/generate-builtin-terminfo.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import argparse +import os import re import sys @@ -185,6 +186,7 @@ def main(): entry.add_capability(StringCapability('TN', target_entry_name)) entry.add_capability(StringCapability('name', target_entry_name)) entry.add_capability(IntCapability('RGB', 8)) # 8 bits per channel + entry.add_capability(StringCapability('query-os-name', os.uname().sysname)) terminfo_parts = [] for cap in sorted(entry.caps.values()): From 4cb17f5ae65e129765794986d36fbfc33c0c65bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 24 Dec 2025 11:33:28 +0100 Subject: [PATCH 1286/1323] csi: make sure the ASCII printer function is updated on plain underlines Otherwise, a sequence like \E[4:2;4m # Enable double-underline, then immediately switch to single Will switch to the slow printer, and then get stuck there even though we immediately switch to plain underlines (which don't need the slow printer). --- csi.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/csi.c b/csi.c index 7e0cf464..0cf8e203 100644 --- a/csi.c +++ b/csi.c @@ -117,9 +117,9 @@ csi_sgr(struct terminal *term) style > UNDERLINE_SINGLE; break; } - - term_update_ascii_printer(term); - } + } else + term->bits_affecting_ascii_printer.underline_style = false; + term_update_ascii_printer(term); break; } case 5: term->vt.attrs.blink = true; break; From ca278398b101bc6c44ad6df0c48e068660ac5323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 26 Dec 2025 12:33:03 +0100 Subject: [PATCH 1287/1323] pyproject.toml: add initial mypy configuration --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..52106805 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.mypy] +files = '$MYPY_CONFIG_FILE_DIR/scripts' +strict = true From cb1e152d995d2430c73bf322ced5e23f40fcb73f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 26 Dec 2025 13:12:43 +0100 Subject: [PATCH 1288/1323] pyproject.toml: add initial pyright configuration --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 52106805..d561b976 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,6 @@ +[tool.pyright] +strict = ['scripts'] + [tool.mypy] files = '$MYPY_CONFIG_FILE_DIR/scripts' strict = true From bbebe0f330fc3606aeb1896af70175d2fa81716d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 26 Dec 2025 13:13:01 +0100 Subject: [PATCH 1289/1323] scripts: mypy fixes --- scripts/benchmark.py | 8 +- scripts/generate-alt-random-writes.py | 23 +++-- scripts/generate-builtin-terminfo.py | 85 +++++++++++-------- scripts/generate-emoji-variation-sequences.py | 7 +- scripts/srgb.py | 5 +- 5 files changed, 77 insertions(+), 51 deletions(-) diff --git a/scripts/benchmark.py b/scripts/benchmark.py index 5483dac1..fe820d9b 100755 --- a/scripts/benchmark.py +++ b/scripts/benchmark.py @@ -11,7 +11,7 @@ import termios from datetime import datetime -def main(): +def main() -> None: parser = argparse.ArgumentParser() parser.add_argument('files', type=argparse.FileType('rb'), nargs='+') parser.add_argument('--iterations', type=int, default=20) @@ -24,12 +24,12 @@ def main(): termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))) - times = {name: [] for name in [f.name for f in args.files]} + times: dict[str, list[float]] = {name: [] for name in [f.name for f in args.files]} for f in args.files: bench_bytes = f.read() - for i in range(args.iterations): + for _ in range(args.iterations): start = datetime.now() sys.stdout.buffer.write(bench_bytes) stop = datetime.now() @@ -48,4 +48,4 @@ def main(): if __name__ == '__main__': - sys.exit(main()) + main() diff --git a/scripts/generate-alt-random-writes.py b/scripts/generate-alt-random-writes.py index 656a2b9d..7ad1460c 100755 --- a/scripts/generate-alt-random-writes.py +++ b/scripts/generate-alt-random-writes.py @@ -8,6 +8,8 @@ import struct import sys import termios +from typing import Any + class ColorVariant(enum.IntEnum): NONE = enum.auto() @@ -17,7 +19,7 @@ class ColorVariant(enum.IntEnum): RGB = enum.auto() -def main(): +def main() -> None: parser = argparse.ArgumentParser() parser.add_argument( 'out', type=argparse.FileType(mode='w'), nargs='?', help='name of output file') @@ -38,10 +40,16 @@ def main(): opts = parser.parse_args() out = opts.out if opts.out is not None else sys.stdout + lines: int | None = None + cols: int | None = None + width: int | None = None + height: int | None = None + if opts.rows is None or opts.cols is None: try: - def dummy(*args): + def dummy(*args: Any) -> None: """Need a handler installed for sigwait() to trigger.""" + _ = args pass signal.signal(signal.SIGWINCH, dummy) @@ -53,6 +61,9 @@ def main(): termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))) + assert width is not None + assert height is not None + if width > 0 and height > 0: break @@ -71,9 +82,11 @@ def main(): if opts.rows is not None: lines = opts.rows + assert lines is not None height = 15 * lines # PGO helper binary hardcodes cell height to 15px if opts.cols is not None: cols = opts.cols + assert cols is not None width = 8 * cols # PGO help binary hardcodes cell width to 8px if lines is None or cols is None or height is None or width is None: @@ -190,8 +203,8 @@ def main(): # The sixel 'alphabet' sixels = '?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~' - last_pos = None - last_size = None + last_pos: tuple[int, int] | None = None + last_size: tuple[int, int] = 0, 0 for _ in range(20): if last_pos is not None and random.randrange(2): @@ -254,4 +267,4 @@ def main(): if __name__ == '__main__': - sys.exit(main()) + main() diff --git a/scripts/generate-builtin-terminfo.py b/scripts/generate-builtin-terminfo.py index 6a6ba68c..515153a2 100755 --- a/scripts/generate-builtin-terminfo.py +++ b/scripts/generate-builtin-terminfo.py @@ -3,13 +3,10 @@ import argparse import os import re -import sys - -from typing import Dict, Union class Capability: - def __init__(self, name: str, value: Union[bool, int, str]): + def __init__(self, name: str, value: bool | int | str) -> None: self._name = name self._value = value @@ -18,30 +15,42 @@ class Capability: return self._name @property - def value(self) -> Union[bool, int, str]: + def value(self) -> bool | int | str: return self._value - def __lt__(self, other): + def __lt__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented return self._name < other._name - def __le__(self, other): + def __le__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented return self._name <= other._name - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented return self._name == other._name - def __ne__(self, other): - return self._name != other._name + def __ne__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented + return bool(self._name != other._name) - def __gt__(self, other): - return self._name > other._name + def __gt__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented + return bool(self._name > other._name) - def __ge__(self, other): + def __ge__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented return self._name >= other._name class BoolCapability(Capability): - def __init__(self, name: str): + def __init__(self, name: str) -> None: super().__init__(name, True) @@ -50,11 +59,11 @@ class IntCapability(Capability): class StringCapability(Capability): - def __init__(self, name: str, value: str): + def __init__(self, name: str, value: str) -> None: # see terminfo(5) for valid escape sequences # Control characters - def translate_ctrl_chr(m): + def translate_ctrl_chr(m: re.Match[str]) -> str: ctrl = m.group(1) if ctrl == '?': return '\\x7f' @@ -83,10 +92,10 @@ class StringCapability(Capability): class Fragment: - def __init__(self, name: str, description: str): + def __init__(self, name: str, description: str) -> None: self._name = name self._description = description - self._caps = {} + self._caps = dict[str, Capability]() @property def name(self) -> str: @@ -97,18 +106,18 @@ class Fragment: return self._description @property - def caps(self) -> Dict[str, Capability]: + def caps(self) -> dict[str, Capability]: return self._caps - def add_capability(self, cap: Capability): + def add_capability(self, cap: Capability) -> None: assert cap.name not in self._caps self._caps[cap.name] = cap - def del_capability(self, name: str): + def del_capability(self, name: str) -> None: del self._caps[name] -def main(): +def main() -> None: parser = argparse.ArgumentParser() parser.add_argument('source_entry_name') parser.add_argument('source', type=argparse.FileType('r')) @@ -121,15 +130,15 @@ def main(): source = opts.source target = opts.target - lines = [] - for l in source.readlines(): - l = l.strip() - if l.startswith('#'): + lines = list[str]() + for line in source.readlines(): + line = line.strip() + if line.startswith('#'): continue - lines.append(l) + lines.append(line) - fragments = {} - cur_fragment = None + fragments = dict[str, Fragment]() + cur_fragment: Fragment | None = None for m in re.finditer( r'(?P<name>(?P<entry_name>[-+\w@]+)\|(?P<entry_desc>.+?),)|' @@ -148,17 +157,20 @@ def main(): elif m.group('bool_cap') is not None: name = m.group('bool_name') + assert cur_fragment is not None cur_fragment.add_capability(BoolCapability(name)) elif m.group('int_cap') is not None: name = m.group('int_name') - value = int(m.group('int_val'), 0) - cur_fragment.add_capability(IntCapability(name, value)) + int_value = int(m.group('int_val'), 0) + assert cur_fragment is not None + cur_fragment.add_capability(IntCapability(name, int_value)) elif m.group('str_cap') is not None: name = m.group('str_name') - value = m.group('str_val') - cur_fragment.add_capability(StringCapability(name, value)) + str_value = m.group('str_val') + assert cur_fragment is not None + cur_fragment.add_capability(StringCapability(name, str_value)) else: assert False @@ -167,6 +179,9 @@ def main(): for frag in fragments.values(): for cap in frag.caps.values(): if cap.name == 'use': + assert isinstance(cap, StringCapability) + assert isinstance(cap.value, str) + use_frag = fragments[cap.value] for use_cap in use_frag.caps.values(): frag.add_capability(use_cap) @@ -188,7 +203,7 @@ def main(): entry.add_capability(IntCapability('RGB', 8)) # 8 bits per channel entry.add_capability(StringCapability('query-os-name', os.uname().sysname)) - terminfo_parts = [] + terminfo_parts = list[str]() for cap in sorted(entry.caps.values()): name = cap.name value = str(cap.value) @@ -212,4 +227,4 @@ def main(): if __name__ == '__main__': - sys.exit(main()) + main() diff --git a/scripts/generate-emoji-variation-sequences.py b/scripts/generate-emoji-variation-sequences.py index e05b6290..17172d20 100644 --- a/scripts/generate-emoji-variation-sequences.py +++ b/scripts/generate-emoji-variation-sequences.py @@ -1,11 +1,10 @@ #!/usr/bin/env python3 import argparse -import sys class Codepoint: - def __init__(self, start: int, end: None|int = None): + def __init__(self, start: int, end: None | int = None) -> None: self.start = start self.end = start if end is None else end self.vs15 = False @@ -15,7 +14,7 @@ class Codepoint: return f'{self.start:x}-{self.end:x}, vs15={self.vs15}, vs16={self.vs16}' -def main(): +def main() -> None: parser = argparse.ArgumentParser() parser.add_argument('input', type=argparse.FileType('r')) parser.add_argument('output', type=argparse.FileType('w')) @@ -100,4 +99,4 @@ def main(): if __name__ == '__main__': - sys.exit(main()) + main() diff --git a/scripts/srgb.py b/scripts/srgb.py index 12056956..a6aa0f4a 100755 --- a/scripts/srgb.py +++ b/scripts/srgb.py @@ -2,7 +2,6 @@ import argparse import math -import sys # Note: we use a pure gamma 2.2 function, rather than the piece-wise @@ -17,7 +16,7 @@ def linear_to_srgb(f: float) -> float: return math.pow(f, 1 / 2.2) -def main(): +def main() -> None: parser = argparse.ArgumentParser() parser.add_argument('c_output', type=argparse.FileType('w')) parser.add_argument('h_output', type=argparse.FileType('w')) @@ -68,4 +67,4 @@ def main(): if __name__ == '__main__': - sys.exit(main()) + main() From 6ab2e2d9ebc247b4c4112d12a16ebc2b7b0f7c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 26 Dec 2025 13:15:01 +0100 Subject: [PATCH 1290/1323] ci: run mypy + ruff check --- .woodpecker.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 843c9afc..ef96efcb 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -17,6 +17,24 @@ steps: - codespell -Lser,doas,zar,rin README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd - deactivate + - name: mypy + 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 mypy-venv + - source mypy-venv/bin/activate + - pip install mypy + - pip install ruff + - mypy + - ruff check + - deactivate + - name: subprojects when: - event: [manual, pull_request] From ee682abac876cf558627e39415ebc92d2431c964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 26 Dec 2025 14:13:14 +0100 Subject: [PATCH 1291/1323] mypy: no need to declare None as return type for __init__ --- scripts/generate-builtin-terminfo.py | 8 ++++---- scripts/generate-emoji-variation-sequences.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/generate-builtin-terminfo.py b/scripts/generate-builtin-terminfo.py index 515153a2..c10373d3 100755 --- a/scripts/generate-builtin-terminfo.py +++ b/scripts/generate-builtin-terminfo.py @@ -6,7 +6,7 @@ import re class Capability: - def __init__(self, name: str, value: bool | int | str) -> None: + def __init__(self, name: str, value: bool | int | str): self._name = name self._value = value @@ -50,7 +50,7 @@ class Capability: class BoolCapability(Capability): - def __init__(self, name: str) -> None: + def __init__(self, name: str): super().__init__(name, True) @@ -59,7 +59,7 @@ class IntCapability(Capability): class StringCapability(Capability): - def __init__(self, name: str, value: str) -> None: + def __init__(self, name: str, value: str): # see terminfo(5) for valid escape sequences # Control characters @@ -92,7 +92,7 @@ class StringCapability(Capability): class Fragment: - def __init__(self, name: str, description: str) -> None: + def __init__(self, name: str, description: str): self._name = name self._description = description self._caps = dict[str, Capability]() diff --git a/scripts/generate-emoji-variation-sequences.py b/scripts/generate-emoji-variation-sequences.py index 17172d20..57e881c7 100644 --- a/scripts/generate-emoji-variation-sequences.py +++ b/scripts/generate-emoji-variation-sequences.py @@ -4,7 +4,7 @@ import argparse class Codepoint: - def __init__(self, start: int, end: None | int = None) -> None: + def __init__(self, start: int, end: None | int = None): self.start = start self.end = start if end is None else end self.vs15 = False From b3cb180e443ce108b3f051d5eccd80474a058ac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 26 Dec 2025 14:42:51 +0100 Subject: [PATCH 1292/1323] codespell: use pyproject.toml to define options and exceptions --- .woodpecker.yaml | 2 +- CODE_OF_CONDUCT.md | 2 +- pyproject.toml | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index ef96efcb..35d52d67 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -14,7 +14,7 @@ steps: - python3 -m venv codespell-venv - source codespell-venv/bin/activate - pip install codespell - - codespell -Lser,doas,zar,rin README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd + - codespell - deactivate - name: mypy diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 4b652df6..26ab32a5 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -53,7 +53,7 @@ decisions when appropriate. 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 +that people are fallible and that it is possible to truly correct a past pattern of unacceptable behavior. That is to say, the scope of the Code of Conduct does not necessarily extend into the distant past. diff --git a/pyproject.toml b/pyproject.toml index d561b976..654e632d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,3 +4,7 @@ strict = ['scripts'] [tool.mypy] files = '$MYPY_CONFIG_FILE_DIR/scripts' strict = true + +[tool.codespell] +skip = 'pyproject.toml,./subprojects,./pkg,./src,./bld,foot.info,./unicode,.*-venv' +ignore-regex = 'terminfo capability `rin`|\* Simon Ser|\* \[zar\]\(https://codeberg.org/zar\)|iterm theme|iterm.toml|iterm/OneHalfDark.itermcolors' \ No newline at end of file From 41679e64a84977225e193f4eb734ab0a0eab1e09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 26 Dec 2025 15:00:18 +0100 Subject: [PATCH 1293/1323] box-drawing: fenv.h: remove, not needed anymore --- box-drawing.c | 1 - 1 file changed, 1 deletion(-) diff --git a/box-drawing.c b/box-drawing.c index 421ff54d..e69d9648 100644 --- a/box-drawing.c +++ b/box-drawing.c @@ -2,7 +2,6 @@ #include <stdio.h> #include <math.h> -#include <fenv.h> #include <errno.h> #define LOG_MODULE "box-drawing" From bb6968c2847e99ba89a7597ff5b8a31fb5dd9434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 26 Dec 2025 17:23:46 +0100 Subject: [PATCH 1294/1323] ci: combine the codespell and mypy stages They both need python and a venv, so let's combine them, to avoid having to install the same things twice. --- .woodpecker.yaml | 22 ++++------------------ pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 35d52d67..5b4fa587 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -1,7 +1,7 @@ # -*- yaml -*- steps: - - name: codespell + - name: pychecks when: - event: [manual, pull_request] - event: [push, tag] @@ -11,26 +11,12 @@ steps: - apk add openssl - apk add python3 - apk add py3-pip - - python3 -m venv codespell-venv - - source codespell-venv/bin/activate + - python3 -m venv venv + - source venv/bin/activate - pip install codespell - - codespell - - deactivate - - - name: mypy - 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 mypy-venv - - source mypy-venv/bin/activate - pip install mypy - pip install ruff + - codespell - mypy - ruff check - deactivate diff --git a/pyproject.toml b/pyproject.toml index 654e632d..f5fc08a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,5 +6,5 @@ files = '$MYPY_CONFIG_FILE_DIR/scripts' strict = true [tool.codespell] -skip = 'pyproject.toml,./subprojects,./pkg,./src,./bld,foot.info,./unicode,.*-venv' +skip = 'pyproject.toml,./subprojects,./pkg,./src,./bld,foot.info,./unicode,./venv' ignore-regex = 'terminfo capability `rin`|\* Simon Ser|\* \[zar\]\(https://codeberg.org/zar\)|iterm theme|iterm.toml|iterm/OneHalfDark.itermcolors' \ No newline at end of file From 53e8fbbdec5779d59341b37e0b630b06ecf6951a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Fri, 26 Dec 2025 17:25:28 +0100 Subject: [PATCH 1295/1323] ci: python: upgrade pip before installing python packages --- .woodpecker.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 5b4fa587..900251a7 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -13,6 +13,7 @@ steps: - apk add py3-pip - python3 -m venv venv - source venv/bin/activate + - python -m pip install --upgrade pip - pip install codespell - pip install mypy - pip install ruff From 42e04c5c8741f739295f7da0b1bb1effda95547f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 28 Dec 2025 11:37:54 +0100 Subject: [PATCH 1296/1323] csi: secondary DA: fix comment; we don't use an XTerm version number --- csi.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/csi.c b/csi.c index 0cf8e203..87af215e 100644 --- a/csi.c +++ b/csi.c @@ -1644,10 +1644,10 @@ csi_dispatch(struct terminal *term, uint8_t final) * 64 - vt520 * 65 - vt525 * - * Param 2 - firmware version - * xterm uses its version number. We use an xterm - * version number too, since e.g. Emacs uses this to - * determine level of support. + * Param 2 - firmware version xterm uses its version + * number. We do to, in the format "MAJORMINORPATCH", + * where all three version numbers are always two + * digits. So e.g. 1.25.0 is reported as 012500. * * We report ourselves as a VT220. This must be * synchronized with the primary DA response. From b78cc92322dd2ae431dbbb70bc681b4d603d844d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sun, 4 Jan 2026 07:57:25 +0100 Subject: [PATCH 1297/1323] shm: don't bother with xrgb surfaces, always use argb Before this patch, foot used xrgb surfaces for all fully opaque surfaces, and only used argb surfaces for the main window when the user enabled translucency. However, several compositors have damage-like issues when we switch between opaque and non-opaque surfaces (for example, when switching color theme, or when toggling fullscreen). Since the performance benefit of using non-alpha aware surfaces are likely minor (if there's any measurable performance difference at all!), lets workaround these compositor issues by always using argb surfaces. --- CHANGELOG.md | 5 ++++ render.c | 17 +++++------ shm.c | 84 ++++++++++++++++------------------------------------ shm.h | 5 ++-- sixel.c | 4 +-- wayland.c | 3 -- wayland.h | 3 -- 7 files changed, 42 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fa7439e..0462666c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,11 @@ contains at least one upper case character. * Mouse tracking in SGR pixel mode no longer emits negative column/row pixel values ([#2226][2226]). +* Foot now always uses ARGB SHM surfaces. In earlier versions, XRGB + surfaces were used for opaque surfaces. Unfortunately, several + compositors had issues when foot switched between ARGB and XRGB + surfaces (for example when switching color theme, or toggling + fullscreen). [2202]: https://codeberg.org/dnkl/foot/issues/2202 [2226]: https://codeberg.org/dnkl/foot/issues/2226 diff --git a/render.c b/render.c index ac8ece37..86244434 100644 --- a/render.c +++ b/render.c @@ -2012,7 +2012,7 @@ render_overlay(struct terminal *term) } struct buffer *buf = shm_get_buffer( - term->render.chains.overlay, term->width, term->height, true); + term->render.chains.overlay, term->width, term->height); pixman_image_set_clip_region32(buf->pix[0], NULL); /* Bounding rectangle of damaged areas - for wl_surface_damage_buffer() */ @@ -2970,7 +2970,7 @@ render_csd(struct terminal *term) } struct buffer *bufs[CSD_SURF_COUNT]; - shm_get_many(term->render.chains.csd, CSD_SURF_COUNT, widths, heights, bufs, true); + shm_get_many(term->render.chains.csd, CSD_SURF_COUNT, widths, heights, bufs); for (size_t i = CSD_SURF_LEFT; i <= CSD_SURF_BOTTOM; i++) render_csd_border(term, i, &infos[i], bufs[i]); @@ -3110,7 +3110,7 @@ render_scrollback_position(struct terminal *term) } struct buffer_chain *chain = term->render.chains.scrollback_indicator; - struct buffer *buf = shm_get_buffer(chain, width, height, false); + struct buffer *buf = shm_get_buffer(chain, width, height); wl_subsurface_set_position( win->scrollback_indicator.sub, roundf(x / scale), roundf(y / scale)); @@ -3153,7 +3153,7 @@ render_render_timer(struct terminal *term, struct timespec render_time) height = roundf(scale * ceilf(height / scale)); struct buffer_chain *chain = term->render.chains.render_timer; - struct buffer *buf = shm_get_buffer(chain, width, height, false); + struct buffer *buf = shm_get_buffer(chain, width, height); wl_subsurface_set_position( win->render_timer.sub, @@ -3336,10 +3336,7 @@ grid_render(struct terminal *term) xassert(term->height > 0); struct buffer_chain *chain = term->render.chains.grid; - bool use_alpha = !term->window->is_fullscreen && - term->colors.alpha != 0xffff; - struct buffer *buf = shm_get_buffer( - chain, term->width, term->height, use_alpha); + struct buffer *buf = shm_get_buffer(chain, term->width, term->height); /* Dirty old and current cursor cell, to ensure they're repainted */ dirty_old_cursor(term); @@ -3787,7 +3784,7 @@ render_search_box(struct terminal *term) size_t glyph_offset = term->render.search_glyph_offset; struct buffer_chain *chain = term->render.chains.search; - struct buffer *buf = shm_get_buffer(chain, width, height, true); + struct buffer *buf = shm_get_buffer(chain, width, height); pixman_region32_t clip; pixman_region32_init_rect(&clip, 0, 0, width, height); @@ -4252,7 +4249,7 @@ render_urls(struct terminal *term) struct buffer_chain *chain = term->render.chains.url; struct buffer *bufs[render_count]; - shm_get_many(chain, render_count, widths, heights, bufs, false); + shm_get_many(chain, render_count, widths, heights, bufs); uint32_t fg = term->conf->colors_dark.use_custom.jump_label ? term->conf->colors_dark.jump_label.fg diff --git a/shm.c b/shm.c index f488d6b6..5c1573ad 100644 --- a/shm.c +++ b/shm.c @@ -84,7 +84,6 @@ struct buffer_private { struct buffer_pool *pool; off_t offset; /* Offset into memfd where data begins */ size_t size; - bool with_alpha; bool scrollable; @@ -98,11 +97,8 @@ struct buffer_chain { size_t pix_instances; bool scrollable; - pixman_format_code_t pixman_fmt_without_alpha; - enum wl_shm_format shm_format_without_alpha; - - pixman_format_code_t pixman_fmt_with_alpha; - enum wl_shm_format shm_format_with_alpha; + pixman_format_code_t pixman_fmt; + enum wl_shm_format shm_format; void (*release_cb)(struct buffer *buf, void *data); void *cb_data; @@ -285,9 +281,7 @@ instantiate_offset(struct buffer_private *buf, off_t new_offset) wl_buf = wl_shm_pool_create_buffer( pool->wl_pool, new_offset, buf->public.width, buf->public.height, buf->public.stride, - buf->with_alpha - ? buf->chain->shm_format_with_alpha - : buf->chain->shm_format_without_alpha); + buf->chain->shm_format); if (wl_buf == NULL) { LOG_ERR("failed to create SHM buffer"); @@ -297,9 +291,7 @@ instantiate_offset(struct buffer_private *buf, off_t new_offset) /* One pixman image for each worker thread (do we really need multiple?) */ for (size_t i = 0; i < buf->public.pix_instances; i++) { pix[i] = pixman_image_create_bits_no_clear( - buf->with_alpha - ? buf->chain->pixman_fmt_with_alpha - : buf->chain->pixman_fmt_without_alpha, + buf->chain->pixman_fmt, buf->public.width, buf->public.height, (uint32_t *)mmapped, buf->public.stride); @@ -334,8 +326,7 @@ err: static void NOINLINE get_new_buffers(struct buffer_chain *chain, size_t count, int widths[static count], int heights[static count], - struct buffer *bufs[static count], bool with_alpha, - bool immediate_purge) + struct buffer *bufs[static count], bool immediate_purge) { xassert(count == 1 || !chain->scrollable); /* @@ -354,10 +345,7 @@ get_new_buffers(struct buffer_chain *chain, size_t count, size_t total_size = 0; for (size_t i = 0; i < count; i++) { stride[i] = stride_for_format_and_width( - with_alpha - ? chain->pixman_fmt_with_alpha - : chain->pixman_fmt_without_alpha, - widths[i]); + chain->pixman_fmt, widths[i]); if (min_stride_alignment > 0) { const size_t m = min_stride_alignment; @@ -521,7 +509,6 @@ get_new_buffers(struct buffer_chain *chain, size_t count, .chain = chain, .ref_count = immediate_purge ? 0 : 1, .busy = true, - .with_alpha = with_alpha, .pool = pool, .offset = 0, .size = sizes[i], @@ -593,13 +580,13 @@ shm_did_not_use_buf(struct buffer *_buf) void shm_get_many(struct buffer_chain *chain, size_t count, int widths[static count], int heights[static count], - struct buffer *bufs[static count], bool with_alpha) + struct buffer *bufs[static count]) { - get_new_buffers(chain, count, widths, heights, bufs, with_alpha, true); + get_new_buffers(chain, count, widths, heights, bufs, true); } struct buffer * -shm_get_buffer(struct buffer_chain *chain, int width, int height, bool with_alpha) +shm_get_buffer(struct buffer_chain *chain, int width, int height) { LOG_DBG( "chain=%p: looking for a reusable %dx%d buffer " @@ -610,9 +597,7 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height, bool with_alph tll_foreach(chain->bufs, it) { struct buffer_private *buf = it->item; - if (buf->public.width != width || buf->public.height != height || - with_alpha != buf->with_alpha) - { + if (buf->public.width != width || buf->public.height != height) { LOG_DBG("purging mismatching buffer %p", (void *)buf); if (buffer_unref_no_remove_from_chain(buf)) tll_remove(chain->bufs, it); @@ -663,7 +648,7 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height, bool with_alph } struct buffer *ret; - get_new_buffers(chain, 1, &width, &height, &ret, with_alpha, false); + get_new_buffers(chain, 1, &width, &height, &ret, false); return ret; } @@ -1009,11 +994,8 @@ shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, enum shm_bit_depth desired_bit_depth, void (*release_cb)(struct buffer *buf, void *data), void *cb_data) { - pixman_format_code_t pixman_fmt_without_alpha = PIXMAN_x8r8g8b8; - enum wl_shm_format shm_fmt_without_alpha = WL_SHM_FORMAT_XRGB8888; - - pixman_format_code_t pixman_fmt_with_alpha = PIXMAN_a8r8g8b8; - enum wl_shm_format shm_fmt_with_alpha = WL_SHM_FORMAT_ARGB8888; + pixman_format_code_t pixman_fmt = PIXMAN_a8r8g8b8; + enum wl_shm_format shm_fmt = WL_SHM_FORMAT_ARGB8888; static bool have_logged = false; static bool have_logged_10_fallback = false; @@ -1022,12 +1004,9 @@ shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, static bool have_logged_16_fallback = false; if (desired_bit_depth == SHM_BITS_16) { - if (wayl->shm_have_abgr161616 && wayl->shm_have_xbgr161616) { - pixman_fmt_without_alpha = PIXMAN_a16b16g16r16; - shm_fmt_without_alpha = WL_SHM_FORMAT_XBGR16161616; - - pixman_fmt_with_alpha = PIXMAN_a16b16g16r16; - shm_fmt_with_alpha = WL_SHM_FORMAT_ABGR16161616; + if (wayl->shm_have_abgr161616) { + pixman_fmt = PIXMAN_a16b16g16r16; + shm_fmt = WL_SHM_FORMAT_ABGR16161616; if (!have_logged) { have_logged = true; @@ -1045,15 +1024,10 @@ shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, } #endif - if (desired_bit_depth >= SHM_BITS_10 && - pixman_fmt_with_alpha == PIXMAN_a8r8g8b8) - { - if (wayl->shm_have_argb2101010 && wayl->shm_have_xrgb2101010) { - pixman_fmt_without_alpha = PIXMAN_x2r10g10b10; - shm_fmt_without_alpha = WL_SHM_FORMAT_XRGB2101010; - - pixman_fmt_with_alpha = PIXMAN_a2r10g10b10; - shm_fmt_with_alpha = WL_SHM_FORMAT_ARGB2101010; + if (desired_bit_depth >= SHM_BITS_10 && pixman_fmt == PIXMAN_a8r8g8b8) { + if (wayl->shm_have_argb2101010) { + pixman_fmt = PIXMAN_a2r10g10b10; + shm_fmt = WL_SHM_FORMAT_ARGB2101010; if (!have_logged) { have_logged = true; @@ -1061,12 +1035,9 @@ shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, } } - else if (wayl->shm_have_abgr2101010 && wayl->shm_have_xbgr2101010) { - pixman_fmt_without_alpha = PIXMAN_x2b10g10r10; - shm_fmt_without_alpha = WL_SHM_FORMAT_XBGR2101010; - - pixman_fmt_with_alpha = PIXMAN_a2b10g10r10; - shm_fmt_with_alpha = WL_SHM_FORMAT_ABGR2101010; + else if (wayl->shm_have_abgr2101010) { + pixman_fmt = PIXMAN_a2b10g10r10; + shm_fmt = WL_SHM_FORMAT_ABGR2101010; if (!have_logged) { have_logged = true; @@ -1098,11 +1069,8 @@ shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, .pix_instances = pix_instances, .scrollable = scrollable, - .pixman_fmt_without_alpha = pixman_fmt_without_alpha, - .shm_format_without_alpha = shm_fmt_without_alpha, - - .pixman_fmt_with_alpha = pixman_fmt_with_alpha, - .shm_format_with_alpha = shm_fmt_with_alpha, + .pixman_fmt = pixman_fmt, + .shm_format = shm_fmt, .release_cb = release_cb, .cb_data = cb_data, @@ -1129,7 +1097,7 @@ shm_chain_free(struct buffer_chain *chain) enum shm_bit_depth shm_chain_bit_depth(const struct buffer_chain *chain) { - const pixman_format_code_t fmt = chain->pixman_fmt_with_alpha; + const pixman_format_code_t fmt = chain->pixman_fmt; return fmt == PIXMAN_a8r8g8b8 ? SHM_BITS_8 diff --git a/shm.h b/shm.h index 84eb4386..c58a8531 100644 --- a/shm.h +++ b/shm.h @@ -65,8 +65,7 @@ enum shm_bit_depth shm_chain_bit_depth(const struct buffer_chain *chain); * * A newly allocated buffer has an age of 1234. */ -struct buffer *shm_get_buffer( - struct buffer_chain *chain, int width, int height, bool with_alpha); +struct buffer *shm_get_buffer(struct buffer_chain *chain, int width, int height); /* * Returns many buffers, described by 'info', all sharing the same SHM * buffer pool. @@ -84,7 +83,7 @@ struct buffer *shm_get_buffer( void shm_get_many( struct buffer_chain *chain, size_t count, int widths[static count], int heights[static count], - struct buffer *bufs[static count], bool with_alpha); + struct buffer *bufs[static count]); void shm_did_not_use_buf(struct buffer *buf); diff --git a/sixel.c b/sixel.c index 07b97f46..294864fd 100644 --- a/sixel.c +++ b/sixel.c @@ -125,12 +125,12 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) * that assumes 32-bit pixels). */ if (shm_chain_bit_depth(term->render.chains.grid) >= SHM_BITS_10) { - if (term->wl->shm_have_argb2101010 && term->wl->shm_have_xrgb2101010) { + if (term->wl->shm_have_argb2101010) { term->sixel.use_10bit = true; term->sixel.pixman_fmt = PIXMAN_a2r10g10b10; } - else if (term->wl->shm_have_abgr2101010 && term->wl->shm_have_xbgr2101010) { + else if (term->wl->shm_have_abgr2101010) { term->sixel.use_10bit = true; term->sixel.pixman_fmt = PIXMAN_a2b10g10r10; } diff --git a/wayland.c b/wayland.c index 6785c52d..958e535d 100644 --- a/wayland.c +++ b/wayland.c @@ -240,11 +240,8 @@ shm_format(void *data, struct wl_shm *wl_shm, uint32_t format) struct wayland *wayl = data; switch (format) { - case WL_SHM_FORMAT_XRGB2101010: wayl->shm_have_xrgb2101010 = true; break; case WL_SHM_FORMAT_ARGB2101010: wayl->shm_have_argb2101010 = true; break; - case WL_SHM_FORMAT_XBGR2101010: wayl->shm_have_xbgr2101010 = true; break; case WL_SHM_FORMAT_ABGR2101010: wayl->shm_have_abgr2101010 = true; break; - case WL_SHM_FORMAT_XBGR16161616: wayl->shm_have_xbgr161616 = true; break; case WL_SHM_FORMAT_ABGR16161616: wayl->shm_have_abgr161616 = true; break; } diff --git a/wayland.h b/wayland.h index 140c2058..1b1c1f4c 100644 --- a/wayland.h +++ b/wayland.h @@ -502,11 +502,8 @@ struct wayland { bool use_shm_release; bool shm_have_argb2101010:1; - bool shm_have_xrgb2101010:1; bool shm_have_abgr2101010:1; - bool shm_have_xbgr2101010:1; bool shm_have_abgr161616:1; - bool shm_have_xbgr161616:1; }; struct wayland *wayl_init( From e2a989785ab32a39aaa8d49a45b8a94b12a12fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 10 Jan 2026 07:35:25 +0100 Subject: [PATCH 1298/1323] input: execute: add missing 'return true' to a couple of switch cases Without this, the input handling code won't understand the key/mouse event was consumed (i.e. triggered a shortcut), and will continue processing normally (e.g. sending event to the client application). --- input.c | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/input.c b/input.c index 80b028ac..e8e84fb7 100644 --- a/input.c +++ b/input.c @@ -120,10 +120,14 @@ execute_binding(struct seat *seat, struct terminal *term, case BIND_ACTION_SCROLLBACK_UP_MOUSE: if (term->grid == &term->alt) { - if (term->alt_scrolling) + if (term->alt_scrolling) { alternate_scroll(seat, amount, BTN_BACK); - } else - cmd_scrollback_up(term, amount); + return true; + } + } else { + cmd_scrollback_up(term, amount); + return true; + } break; case BIND_ACTION_SCROLLBACK_DOWN_PAGE: @@ -149,10 +153,14 @@ execute_binding(struct seat *seat, struct terminal *term, case BIND_ACTION_SCROLLBACK_DOWN_MOUSE: if (term->grid == &term->alt) { - if (term->alt_scrolling) + if (term->alt_scrolling) { alternate_scroll(seat, amount, BTN_FORWARD); - } else + return true; + } + } else { cmd_scrollback_down(term, amount); + return true; + } break; case BIND_ACTION_SCROLLBACK_HOME: @@ -535,7 +543,7 @@ execute_binding(struct seat *seat, struct terminal *term, case BIND_ACTION_SELECT_QUOTE: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_QUOTE_WISE, false); - break; + return true; case BIND_ACTION_SELECT_ROW: selection_start( From 3a2eb80d83d59d194a3d07da227431c634892ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 10 Jan 2026 07:36:17 +0100 Subject: [PATCH 1299/1323] input: ignore release events after a keyboard shortcut was triggered This fixes an issue with the kitty keyboard protocol, where 'release' events associated with a shortcut was sent to the client application. Example: user triggers "scroll up". We scroll up. No key event(s) are sent to the client application. Then the user releases the keys. we don't do any shortcut handling on release events, and so we continue with the normal input processing. If the kitty keyboard protocol has been enabled (and specifically, release event reporting has been enabled), then we'll emit a 'release' escape sequence. This in itself is wrong, since the client application never saw the corresponding press event. But we _also_ reset the viewport. The effect (in this example), is that it's impossible to scroll up in the scrollback history. Note that we don't ignore _any_ release event, only the release event for the (final) symbol that triggered the shortcut. This should allow e.g. modifier keys release events to be processed normally, if released before the shortcut key. This is somewhat important, since the client application will have received press events for the modifier keys leading up to the shortcut (if modifier press/release events have been enabled in the kitty keyboard protocol - _Report all keys as escape codes_). Closes #2257 --- CHANGELOG.md | 3 +++ input.c | 24 ++++++++++++++++++++++++ wayland.h | 2 ++ 3 files changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0462666c..a69f58b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -122,8 +122,11 @@ * Crash when reverse-scrolling (terminfo capability `rin`) such that the current viewport ends up outside the scrollback ([#2232][2232]). * Regression: visual glitches in rare circumstances. +* Key release events for shortcuts being sent to the client + application (kitty keyboard protocol only) ([#2257][2257]). [2232]: https://codeberg.org/dnkl/foot/issues/2232 +[2257]: https://codeberg.org/dnkl/foot/issues/2257 ### Security diff --git a/input.c b/input.c index e8e84fb7..aa6b7f1d 100644 --- a/input.c +++ b/input.c @@ -1605,6 +1605,9 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, if (released) stop_repeater(seat, key); + if (pressed) + seat->kbd.last_shortcut_sym = XKB_KEYSYM_MAX + 1; + bool should_repeat = pressed && xkb_keymap_key_repeats(seat->kbd.xkb_keymap, key); @@ -1706,6 +1709,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, if (bind->k.sym == raw_syms[i] && execute_binding(seat, term, bind, serial, 1)) { + seat->kbd.last_shortcut_sym = sym; goto maybe_repeat; } } @@ -1719,6 +1723,7 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, bind->mods == (mods & ~consumed) && execute_binding(seat, term, bind, serial, 1)) { + seat->kbd.last_shortcut_sym = sym; goto maybe_repeat; } } @@ -1734,12 +1739,31 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, if (code->item == key && execute_binding(seat, term, bind, serial, 1)) { + seat->kbd.last_shortcut_sym = sym; goto maybe_repeat; } } } } + if (released && seat->kbd.last_shortcut_sym == sym) { + /* + * Don't process a release event, if it corresponds to a + * triggered shortcut. + * + * 1. If we consumed a key (press) event, we shouldn't emit an + * escape for its release event. + * 2. Ignoring the incorrectness of doing so; this also caused + * us to reset the viewport. + * + * Background: if the kitty keyboard protocol was enabled, + * then the viewport was instantly reset to the bottom, after + * scrolling up. + */ + //seat->kbd.last_shortcut_sym = XKB_KEYSYM_MAX + 1; + goto maybe_repeat; + } + /* * Keys generating escape sequences */ diff --git a/wayland.h b/wayland.h index 1b1c1f4c..6247875a 100644 --- a/wayland.h +++ b/wayland.h @@ -151,6 +151,8 @@ struct seat { bool alt; bool ctrl; bool super; + + xkb_keysym_t last_shortcut_sym; } kbd; /* Pointer state */ From 6fbb9b7d3b2b43e69ca3ce56823fdb9f65a74890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 28 Jan 2026 09:21:57 +0100 Subject: [PATCH 1300/1323] sixel: force a height of at least one sixel when explicitly resizing Applications often prefix the sixel with a raster attributes (RA) sequence, where they tell us how large the sixel is. Strictly speaking, this just tells us the size of the area to clear, but we use it as a hint and pre-allocates the image buffer. It's important to stress that it is valid to emit a MxN RA, and then write sixel data outside of that area. Foot handles this, in _most_ cases. We didn't handle the corner case Mx0. I.e. a width > 0, but height == 0. No image buffer was allocated, and we also failed to detect a resize was necessary when the application started printing sixel data. Much of this is for performance reason; we only check the minimum necessary. For example, we only check if going outside the pre-allocated *column* while printing sixels. *Rows* are checked on a graphical newline. In other words, the *current* row has to be valid when writing sixels. And in case of Mx0, it wasn't. Fix by forcing a height of at least one sixel (typically 6 pixels). Closes #2267 --- CHANGELOG.md | 3 +++ sixel.c | 3 +++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a69f58b8..ce1a7d8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -124,9 +124,12 @@ * Regression: visual glitches in rare circumstances. * Key release events for shortcuts being sent to the client application (kitty keyboard protocol only) ([#2257][2257]). +* Crash when application emits sixel RA with a height of 0, a width > + 0, and then starts writing sixel data ([#2267][2267]). [2232]: https://codeberg.org/dnkl/foot/issues/2232 [2257]: https://codeberg.org/dnkl/foot/issues/2257 +[2267]: https://codeberg.org/dnkl/foot/issues/2267 ### Security diff --git a/sixel.c b/sixel.c index 294864fd..187f1348 100644 --- a/sixel.c +++ b/sixel.c @@ -1559,6 +1559,9 @@ resize(struct terminal *term, int new_width_mutable, int new_height_mutable) new_height_mutable = term->sixel.max_height; } + if (unlikely(new_height_mutable == 0)) { + new_height_mutable = 6 * term->sixel.pan; + } uint32_t *old_data = term->sixel.image.data; const int old_width = term->sixel.image.width; From 0bf193ef8122b2fd412438c0e405fb085cd62a1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 2 Feb 2026 11:19:07 +0100 Subject: [PATCH 1301/1323] osc-8: don't log URL + ID when closing --- osc.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osc.c b/osc.c index 9407e7b8..909fd484 100644 --- a/osc.c +++ b/osc.c @@ -525,12 +525,14 @@ osc_uri(struct terminal *term, char *string) id = sdbm_hash(value); } - LOG_DBG("OSC-8: URL=%s, id=%" PRIu64, uri, id); - if (uri[0] == '\0') + if (uri[0] == '\0') { + LOG_DBG("OSC-8: close"); term_osc8_close(term); - else + } else { + LOG_DBG("OSC-8: URL=%s, id=%" PRIu64, uri, id); term_osc8_open(term, id, uri); + } } static void From c291194a4e593bbbb91420e81fa0111508084448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 28 Jan 2026 09:44:57 +0100 Subject: [PATCH 1302/1323] wayland: wait for pre-apply damage thread before destroying a terminal instance It's possible, but unlikely, that we've pushed a "pre-apply damage" job to the renderer thread queue (or that we've pushed it, and the a thread is now working on it) when we shutdown a terminal instance. This is sometimes caught in an assertion in term_destroy(), where we check the queue length is 0. Other times, or in release builds, we might crash in the thread, or in the shutdown logic when freeing the buffer chains associated with the terminal instance. Fix by ensuring there's no pre-apply damage operation queued, or running, before shutting down a terminal instance. Closes #2263 --- CHANGELOG.md | 3 +++ render.c | 10 +++++----- render.h | 1 + wayland.c | 2 ++ 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce1a7d8c..cee4ddef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -126,10 +126,13 @@ application (kitty keyboard protocol only) ([#2257][2257]). * Crash when application emits sixel RA with a height of 0, a width > 0, and then starts writing sixel data ([#2267][2267]). +* Crash if shutting down terminal instance while a "pre-apply damage" + thread is running ([#2263][2263]). [2232]: https://codeberg.org/dnkl/foot/issues/2232 [2257]: https://codeberg.org/dnkl/foot/issues/2257 [2267]: https://codeberg.org/dnkl/foot/issues/2267 +[2263]: https://codeberg.org/dnkl/foot/issues/2263 ### Security diff --git a/render.c b/render.c index 86244434..3aa7d543 100644 --- a/render.c +++ b/render.c @@ -2288,8 +2288,8 @@ render_worker_thread(void *_ctx) return -1; } -static void -wait_for_preapply_damage(struct terminal *term) +void +render_wait_for_preapply_damage(struct terminal *term) { if (!term->render.preapply_last_frame_damage) return; @@ -3325,7 +3325,7 @@ grid_render(struct terminal *term) term->render.workers.preapplied_damage.buf != NULL)) { clock_gettime(CLOCK_MONOTONIC, &start_wait_preapply); - wait_for_preapply_damage(term); + render_wait_for_preapply_damage(term); clock_gettime(CLOCK_MONOTONIC, &stop_wait_preapply); } @@ -4401,7 +4401,7 @@ delayed_reflow_of_normal_grid(struct terminal *term) term->interactive_resizing.old_hide_cursor = false; /* Invalidate render pointers */ - wait_for_preapply_damage(term); + render_wait_for_preapply_damage(term); shm_unref(term->render.last_buf); term->render.last_buf = NULL; term->render.last_cursor.row = NULL; @@ -4976,7 +4976,7 @@ damage_view: tll_free(term->normal.scroll_damage); tll_free(term->alt.scroll_damage); - wait_for_preapply_damage(term); + render_wait_for_preapply_damage(term); shm_unref(term->render.last_buf); term->render.last_buf = NULL; term_damage_view(term); diff --git a/render.h b/render.h index e21eaca8..e6674ab2 100644 --- a/render.h +++ b/render.h @@ -49,3 +49,4 @@ struct csd_data { struct csd_data get_csd_data(const struct terminal *term, enum csd_surface surf_idx); void render_buffer_release_callback(struct buffer *buf, void *data); +void render_wait_for_preapply_damage(struct terminal *term); diff --git a/wayland.c b/wayland.c index 958e535d..59df991a 100644 --- a/wayland.c +++ b/wayland.c @@ -2129,6 +2129,8 @@ wayl_win_destroy(struct wl_window *win) struct terminal *term = win->term; + render_wait_for_preapply_damage(term); + if (win->csd.move_timeout_fd != -1) close(win->csd.move_timeout_fd); From e24334a8dfdd43a5a26ad59bba0d76409b1de1ea Mon Sep 17 00:00:00 2001 From: valoq <valoq@noreply.codeberg.org> Date: Mon, 23 Feb 2026 16:46:36 +0100 Subject: [PATCH 1303/1323] Fix cursor color Default cursor color in alacritty is white (foreground) and not cyan. --- themes/alacritty | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/themes/alacritty b/themes/alacritty index 68d1c68c..f05683ba 100644 --- a/themes/alacritty +++ b/themes/alacritty @@ -2,7 +2,7 @@ # Alacritty [colors-dark] -cursor = 181818 56d8c9 +cursor = 181818 d8d8d8 background= 181818 foreground= d8d8d8 From 1f31b43db7acd5ea5f7e88d0408f043a78df17fe Mon Sep 17 00:00:00 2001 From: Barinderpreet Singh <64461700+knownasnaffy@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:58:35 +0530 Subject: [PATCH 1304/1323] doc: fix typos in foot.ini.5.scd --- doc/foot.ini.5.scd | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 8bff9629..33ebfec8 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -29,7 +29,7 @@ Options are set using KEY=VALUE pairs: *foreground=ffffff* Empty values (*KEY=*) are not supported. String options do allow the -empty string to be set, but it must be quoted: *KEY=""*) +empty string to be set, but it must be quoted: *KEY=""* # SECTION: main @@ -96,7 +96,7 @@ empty string to be set, but it must be quoted: *KEY=""*) *font-size-adjustment* Amount, in _points_, _pixels_ or _percent_, to increment/decrement - the font size when zooming in our out. + the font size when zooming in or out. Examples: ``` @@ -129,7 +129,7 @@ empty string to be set, but it must be quoted: *KEY=""*) e.g. *line-height=12px*. *Warning*: when changing the font size at runtime (i.e. zooming in - our out), foot will change the line height by the same + or out), foot will change the line height by the same percentage. However, due to rounding, it is possible the line height will be "too small" for some font sizes, causing e.g. underscores to "disappear". @@ -161,7 +161,7 @@ empty string to be set, but it must be quoted: *KEY=""*) *underline-offset* Use a custom offset for underlines. The offset is, by default, in - _points_ and relative the font's baseline. A positive value + _points_ and relative to the font's baseline. A positive value positions the underline under the baseline, while a negative value positions it above the baseline. @@ -240,7 +240,7 @@ empty string to be set, but it must be quoted: *KEY=""*) *box-drawings-uses-font-glyphs* Boolean. When disabled, foot generates box/line drawing characters - itself. The are several advantages to doing this instead of using + itself. There are several advantages to doing this instead of using font glyphs: - No antialiasing effects where e.g. line endpoints appear @@ -281,7 +281,7 @@ empty string to be set, but it must be quoted: *KEY=""*) scaling factor *does* double the font size. Note that this option typically does not work with bitmap fonts, - which only contains a pre-defined set of sizes, and cannot be + which only contain a pre-defined set of sizes, and cannot be dynamically scaled. Whichever size (of the available ones) that best matches the DPI or scaling factor, will be used. @@ -337,7 +337,7 @@ empty string to be set, but it must be quoted: *KEY=""*) Emphasis is on _while_ here; as soon as the interactive resize ends (i.e. when you let go of the window border), the final - dimensions is sent to the client, without any delays. + dimensions are sent to the client, without any delays. Setting it to 0 disables the delay completely. @@ -352,7 +352,7 @@ empty string to be set, but it must be quoted: *KEY=""*) 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 + This option only applies to floating windows. Sizes of maximized, tiled or fullscreen windows will not be constrained to multiples of the cell size. @@ -399,7 +399,7 @@ empty string to be set, but it must be quoted: *KEY=""*) *initial-window-size-chars* Initial window width and height in _characters_, in the form _WIDTHxHEIGHT_. Mutually exclusive to - *initial-window-size-pixels*.' + *initial-window-size-pixels*. Note that if you have a multi-monitor setup, with different scaling factors, there is a possibility the window size will not @@ -588,7 +588,7 @@ Note: do not set *TERM* here; use the *term* option in the main option, or preferably, by setting the *image-path* hint (with e.g. notify-send's *--hint* option). - _${category}_ is replaced by the notification's catogory. Can + _${category}_ is replaced by the notification's category. Can be used together with e.g. notify-send's *--category* option. _${urgency}_ is replaced with the notifications urgency; @@ -700,7 +700,7 @@ xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 Foot recognizes this as: - notification has the daemon assigned ID 17 - the user triggered the default action - - the notification send an XDG activation token + - the notification sent an XDG activation token Example #2: 17++ @@ -716,7 +716,7 @@ xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 Foot recognizes this as: - notification has the daemon assigned ID 17 - - the user triggered the first custom action, "1 + - the user triggered the first custom action, "1" Default: _notify-send++ --wait++ @@ -760,7 +760,7 @@ xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 least one), *command-action-argument* will be expanded with the action's name and label. - Then, _${action-argument}_ is expanded *command* to the full list + Then, _${action-argument}_ is expanded in *command* to the full list of actions. If *command-action-argument* is set to the empty string, no @@ -933,7 +933,7 @@ applications can change these at runtime. by applications. Related option: *blink-rate*. Default: _no_. *blink-rate* - The rate at which the cursor blink, when cursor blinking has been + The rate at which the cursor blinks, when cursor blinking has been enabled. Expressed in milliseconds between each blink. Default: _500_. @@ -1187,7 +1187,7 @@ Examples: *hide-when-maximized* Boolean. When enabled, the CSD titlebar is hidden when the window - is maximized. The completely disable the titlebar, set *size* to 0 + is maximized. To completely disable the titlebar, set *size* to 0 instead. Default: _no_. *double-click-to-maximize* @@ -1417,7 +1417,7 @@ e.g. *search-start=none*. shell integration, see *foot*(1)). Default: _Control+Shift+z_. *prompt-next* - Jump the next prompt (requires shell integration, see + Jump to the next prompt (requires shell integration, see *foot*(1)). Default: _Control+Shift+x_. *unicode-input* @@ -1446,7 +1446,7 @@ e.g. *search-start=none*. Default: _Control+Shift+u_. -*color-theme-switch-dark*, *color-theme-switch-dark*, *color-theme-toggle* +*color-theme-switch-dark*, *color-theme-switch-light*, *color-theme-toggle* Switch between the dark color theme (defined in the *colors-dark* section), and the light color theme (defined in the *colors-light* section). @@ -1817,7 +1817,7 @@ any of these options. *scaling-filter* Overrides the default scaling filter used when down-scaling bitmap fonts (e.g. emoji fonts). Possible values are *none*, *nearest*, - *bilinear*, *impulse*, *box*, *linear*, *cubic* *gaussian*, + *bilinear*, *impulse*, *box*, *linear*, *cubic*, *gaussian*, *lanczos2*, *lanczos3* or *lanczos3-stretched*. Default: _lanczos3_. @@ -1886,8 +1886,8 @@ any of these options. must be patched to use it. Until this has happened, foot offers an interim workaround; an - attempt to mitigate the screen flicker *without* affecting neither - performance nor latency. + attempt to mitigate the screen flicker *without* affecting either + performance or latency. It is based on the fact that the screen is updated at a fixed interval (typically 60Hz). For us, this means it does not matter @@ -2129,7 +2129,7 @@ any of these options. Thus, having this option enabled improves both performance (copying the last two frames' changes is threaded), and improves - input latency (rending the next frame no longer has to first bring + input latency (rendering the next frame no longer has to first bring over the changes between the last two frames). Default: _yes_ From fbf430473146bdb6f9ab6afefa4aab4c57a3d9a4 Mon Sep 17 00:00:00 2001 From: nariby <nariby@noreply.codeberg.org> Date: Sun, 8 Feb 2026 14:12:32 +0000 Subject: [PATCH 1305/1323] doc: foot.ini: mention titlebar text color in button-color --- doc/foot.ini.5.scd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 33ebfec8..a9e4f045 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1209,8 +1209,8 @@ Examples: minimize/maximize/close buttons. Default: _26_. *button-color* - Foreground color on the minimize/maximize/close buttons. Default: - use the default _background_ color. + Foreground color on the minimize/maximize/close buttons and the + titlebar text. Default: use the default _background_ color. *button-minimize-color* Minimize button's background color. Default: use the default From 21485fa66d9b7026bfd1dd2764431805cdb17cf1 Mon Sep 17 00:00:00 2001 From: pi66 <pixel2176@proton.me> Date: Fri, 19 Dec 2025 12:17:29 +0100 Subject: [PATCH 1306/1323] support four-sided padding (left/top/right/bottom) --- CHANGELOG.md | 2 ++ config.c | 41 ++++++++++++++++++++++++++++++----------- config.h | 6 ++++-- doc/foot.ini.5.scd | 15 +++++++++++++-- render.c | 34 ++++++++++++++++++++-------------- 5 files changed, 69 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cee4ddef..3e4c2a6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,8 @@ * `[colors-light]` section to `foot.ini`. Replaces `[colors2]`. * `XTGETTCAP`: added `query-os-name`, returning the OS foot is compiled for (e.g. _'Linux'_) ([#2209][2209]). +* `pad` option now supports 4-directional padding format: + `LEFTxTOPxRIGHTxBOTTOM` (e.g., `20x10x20x10`). [2212]: https://codeberg.org/dnkl/foot/issues/2212 [2209]: https://codeberg.org/dnkl/foot/issues/2209 diff --git a/config.c b/config.c index 14e836c1..b1ff329c 100644 --- a/config.c +++ b/config.c @@ -945,13 +945,12 @@ parse_section_main(struct context *ctx) } else if (streq(key, "pad")) { - unsigned x, y; + unsigned x, y, left, top, right, bottom; char mode[64] = {0}; - int ret = sscanf(value, "%ux%u %63s", &x, &y, mode); - + int ret = sscanf(value, "%ux%ux%ux%u %63s", &left, &top, &right, &bottom, mode); enum center_when center = CENTER_NEVER; - if (ret == 3) { + if (ret == 5) { if (strcasecmp(mode, "center") == 0) center = CENTER_ALWAYS; else if (strcasecmp(mode, "center-when-fullscreen") == 0) @@ -960,20 +959,38 @@ parse_section_main(struct context *ctx) center = CENTER_MAXIMIZED_AND_FULLSCREEN; else center = CENTER_INVALID; + } else if (ret < 4) { + ret = sscanf(value, "%ux%u %63s", &x, &y, mode); + if (ret >= 2) { + left = right = x; + top = bottom = y; + if (ret == 3) { + if (strcasecmp(mode, "center") == 0) + center = CENTER_ALWAYS; + else if (strcasecmp(mode, "center-when-fullscreen") == 0) + center = CENTER_FULLSCREEN; + else if (strcasecmp(mode, "center-when-maximized-and-fullscreen") == 0) + center = CENTER_MAXIMIZED_AND_FULLSCREEN; + else + center = CENTER_INVALID; + } + } } - if ((ret != 2 && ret != 3) || center == CENTER_INVALID) { + if ((ret < 2 || ret > 5) || center == CENTER_INVALID) { LOG_CONTEXTUAL_ERR( - "invalid padding (must be in the form PAD_XxPAD_Y " + "invalid padding (must be in the form RIGHTxTOPxLEFTxBOTTOM or XxY " "[center|" "center-when-fullscreen|" "center-when-maximized-and-fullscreen])"); return false; } - conf->pad_x = x; - conf->pad_y = y; - conf->center_when = ret == 2 ? CENTER_NEVER : center; + conf->pad_left = left; + conf->pad_top = top; + conf->pad_right = right; + conf->pad_bottom = bottom; + conf->center_when = (ret == 4 || ret == 2) ? CENTER_NEVER : center; return true; } @@ -3453,8 +3470,10 @@ config_load(struct config *conf, const char *conf_path, .width = 700, .height = 500, }, - .pad_x = 0, - .pad_y = 0, + .pad_left = 0, + .pad_top = 0, + .pad_right = 0, + .pad_bottom = 0, .center_when = CENTER_MAXIMIZED_AND_FULLSCREEN, .resize_by_cells = true, .resize_keep_grid = true, diff --git a/config.h b/config.h index 9ca47753..d7db5ecc 100644 --- a/config.h +++ b/config.h @@ -232,8 +232,10 @@ struct config { uint32_t height; } size; - unsigned pad_x; - unsigned pad_y; + unsigned pad_left; + unsigned pad_top; + unsigned pad_right; + unsigned pad_bottom; enum center_when center_when; bool resize_by_cells; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index a9e4f045..be8c131e 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -301,9 +301,20 @@ empty string to be set, but it must be quoted: *KEY=""* ``` _XxY_ [center | center-when-fullscreen | center-when-maximized-and-fullscreen] ``` + or + ``` + RIGHTxTOPxLEFTxBOTTOM [center | center-when-fullscreen | center-when-maximized-and-fullscreen] + ``` - This will add _at least_ X pixels on both the left and right - sides, and Y pixels on the top and bottom sides. + - `_XxY_` adds _at least_: + - X pixels on the left and right sides. + - Y pixels on the top and bottom sides. + + - `LEFTxTOPxRIGHTxBOTTOM` adds **at least**: + - LEFT pixels to the left + - TOP pixels to the top + - RIGHT pixels to the right + - BOTTOM pixels to the bottom When no centering is specified, the grid content is anchored to the top left corner. I.e. if the window manager forces an odd diff --git a/render.c b/render.c index 3aa7d543..627da5d6 100644 --- a/render.c +++ b/render.c @@ -4504,8 +4504,8 @@ set_size_from_grid(struct terminal *term, int *width, int *height, int cols, int new_height = rows * term->cell_height; /* Include any configured padding */ - new_width += 2 * term->conf->pad_x * term->scale; - new_height += 2 * term->conf->pad_y * term->scale; + new_width += (term->conf->pad_left + term->conf->pad_right) * term->scale; + new_height += (term->conf->pad_top + term->conf->pad_bottom) * term->scale; /* Round to multiples of scale */ new_width = round(term->scale * round(new_width / term->scale)); @@ -4613,18 +4613,22 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) /* Padding */ const int max_pad_x = (width - min_width) / 2; const int max_pad_y = (height - min_height) / 2; - const int pad_x = min(max_pad_x, scale * term->conf->pad_x); - const int pad_y = min(max_pad_y, scale * term->conf->pad_y); + const int pad_left = min(max_pad_x, scale * term->conf->pad_left); + const int pad_right = min(max_pad_x, scale * term->conf->pad_right); + const int pad_top = min(max_pad_y, scale * term->conf->pad_top); + const int pad_bottom= min(max_pad_y, scale * term->conf->pad_bottom); if (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 = ((width - (pad_left + pad_right)) / term->cell_width) + * term->cell_width + (pad_left + pad_right); width = max(min_width, roundf(scale * roundf(width / scale))); - height = ((height - 2 * pad_y) / term->cell_height) * term->cell_height + 2 * pad_y; + height = ((height - (pad_top + pad_bottom)) / term->cell_height) + * term->cell_height + (pad_top + pad_bottom); height = max(min_height, roundf(scale * roundf(height / scale))); } @@ -4651,8 +4655,10 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) int old_rows = term->rows; /* Screen rows/cols after resize */ - const int new_cols = (term->width - 2 * pad_x) / term->cell_width; - const int new_rows = (term->height - 2 * pad_y) / term->cell_height; + const int new_cols = + (term->width - (pad_left + pad_right)) / term->cell_width; + const int new_rows = + (term->height - (pad_top + pad_bottom)) / term->cell_height; /* * Requirements for scrollback: @@ -4705,16 +4711,16 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts) term->margins.left = total_x_pad / 2; term->margins.top = total_y_pad / 2; } else { - term->margins.left = pad_x; - term->margins.top = pad_y; + term->margins.left = pad_left; + term->margins.top = pad_top; } term->margins.right = total_x_pad - term->margins.left; term->margins.bottom = total_y_pad - term->margins.top; - xassert(term->margins.left >= pad_x); - xassert(term->margins.right >= pad_x); - xassert(term->margins.top >= pad_y); - xassert(term->margins.bottom >= pad_y); + xassert(term->margins.left >= pad_left); + xassert(term->margins.right >= pad_right); + xassert(term->margins.top >= pad_top); + xassert(term->margins.bottom >= pad_bottom); if (new_cols == old_cols && new_rows == old_rows) { LOG_DBG("grid layout unaffected; skipping reflow"); From dc0c8550c38ffd0983789573a1a0d4417016c2c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Klein?= <contact@stephane-klein.info> Date: Fri, 9 Jan 2026 00:31:04 +0100 Subject: [PATCH 1307/1323] Spawning new terminal with --config from parent instance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reference: https://codeberg.org/dnkl/foot/issues/1622 Signed-off-by: Stéphane Klein <contact@stephane-klein.info> --- CHANGELOG.md | 3 +++ config.c | 3 +++ config.h | 1 + doc/foot.1.scd | 3 +++ terminal.c | 12 +++++++++++- 5 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e4c2a6f..c3760f10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,9 +80,12 @@ compiled for (e.g. _'Linux'_) ([#2209][2209]). * `pad` option now supports 4-directional padding format: `LEFTxTOPxRIGHTxBOTTOM` (e.g., `20x10x20x10`). +* `--config=PATH` option is now automatically passed to new + terminals spawned via `spawn-terminal` action ([#2259][2259]). [2212]: https://codeberg.org/dnkl/foot/issues/2212 [2209]: https://codeberg.org/dnkl/foot/issues/2209 +[2259]: https://codeberg.org/dnkl/foot/pulls/2259 ### Changed diff --git a/config.c b/config.c index b1ff329c..bbac7fb6 100644 --- a/config.c +++ b/config.c @@ -3459,6 +3459,7 @@ config_load(struct config *conf, const char *conf_path, enum fcft_capabilities fcft_caps = fcft_capabilities(); *conf = (struct config) { + .conf_path = (conf_path ? xstrdup(conf_path) : NULL), .term = xstrdup(FOOT_DEFAULT_TERM), .shell = get_shell(), .title = xstrdup("foot"), @@ -3914,6 +3915,7 @@ config_clone(const struct config *old) struct config *conf = xmalloc(sizeof(*conf)); *conf = *old; + conf->conf_path = (old->conf_path ? xstrdup(old->conf_path) : NULL); conf->term = xstrdup(old->term); conf->shell = xstrdup(old->shell); conf->title = xstrdup(old->title); @@ -4014,6 +4016,7 @@ UNITTEST void config_free(struct config *conf) { + free(conf->conf_path); free(conf->term); free(conf->shell); free(conf->title); diff --git a/config.h b/config.h index d7db5ecc..47743a9c 100644 --- a/config.h +++ b/config.h @@ -217,6 +217,7 @@ enum center_when { }; struct config { + char *conf_path; char *term; char *shell; char *title; diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 7058e96f..a190db9b 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -27,6 +27,9 @@ the foot command line *-c*,*--config*=_PATH_ Path to configuration file, see *foot.ini*(5) for details. + The configuration file is automatically passed to new terminals + spawned via *spawn-terminal* (see *foot.ini*(5)). + *-C*,*--check-config* Verify configuration and then exit with 0 if ok, otherwise exit with 230 (see *EXIT STATUS*). diff --git a/terminal.c b/terminal.c index b670d606..8ce9bd4b 100644 --- a/terminal.c +++ b/terminal.c @@ -3802,8 +3802,18 @@ term_bell(struct terminal *term) bool term_spawn_new(const struct terminal *term) { + char *argv[4]; + int argc = 0; + + argv[argc++] = term->foot_exe; + if (term->conf->conf_path != NULL) { + argv[argc++] = "--config"; + argv[argc++] = term->conf->conf_path; + } + argv[argc] = NULL; + return spawn( - term->reaper, term->cwd, (char *const []){term->foot_exe, NULL}, + term->reaper, term->cwd, argv, -1, -1, -1, NULL, NULL, NULL) >= 0; } From dea10e2e48162c6ab48ca564ceeca88dea886a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 16 Oct 2025 13:43:33 +0200 Subject: [PATCH 1308/1323] Add support for background blur This patch adds a new config option: colors{,2}.blur=no|yes. When enabled, transparent background are also blurred. Note that this requires the brand new ext-background-effect-v1 protocol, and specifically, that the compositor implements the blur effect. --- CHANGELOG.md | 6 +++ config.c | 4 ++ config.h | 2 + doc/foot.ini.5.scd | 8 ++++ foot-features.c | 6 +++ meson.build | 4 ++ osc.c | 42 ++++---------------- render.c | 4 +- terminal.c | 8 ++++ terminal.h | 1 + tests/test-config.c | 4 ++ wayland.c | 97 ++++++++++++++++++++++++++++++++++++++++++--- wayland.h | 11 +++++ 13 files changed, 154 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3760f10..3ada2084 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,12 @@ `LEFTxTOPxRIGHTxBOTTOM` (e.g., `20x10x20x10`). * `--config=PATH` option is now automatically passed to new terminals spawned via `spawn-terminal` action ([#2259][2259]). +* Preliminary (untested) support for background blur via the new + `ext-background-effect-v1` protocol. Enable by setting + `colors-{dark,light}.blur=yes`. Foot needs to have been **built** + against `wayland-protocols >= 1.45`, and the compositor **must** + implement the `ext-background-effect-v1` protocol, **and** the + `blur` effect. [2212]: https://codeberg.org/dnkl/foot/issues/2212 [2209]: https://codeberg.org/dnkl/foot/issues/2209 diff --git a/config.c b/config.c index bbac7fb6..12c594bc 100644 --- a/config.c +++ b/config.c @@ -1593,6 +1593,9 @@ parse_color_theme(struct context *ctx, struct color_theme *theme) (int *)&theme->dim_blend_towards); } + else if (streq(key, "blur")) + return value_to_bool(ctx, &theme->blur); + else { LOG_CONTEXTUAL_ERR("not valid option"); return false; @@ -3546,6 +3549,7 @@ config_load(struct config *conf, const char *conf_path, .scrollback_indicator = false, .url = false, }, + .blur = false, }, .initial_color_theme = COLOR_THEME_DARK, .cursor = { diff --git a/config.h b/config.h index 47743a9c..a3522f44 100644 --- a/config.h +++ b/config.h @@ -192,6 +192,8 @@ struct color_theme { bool search_box_match:1; uint8_t dim; } use_custom; + + bool blur; }; enum which_color_theme { diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index be8c131e..2f5fc38c 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1108,6 +1108,14 @@ The default theme used is *colors-dark*, unless Default: _default_ +*blur* + Boolean. When enabled, foot will blur the background (main window + only, not CSDs etc), when it is transparent. This feature requires + the compositor to implement the _ext-background-effect-v1_ + protocol (and specifically, the _blur_ effect). + + Default: _no_ + *dim-blend-towards* Which color to blend towards when "auto" dimming a color (see *dim0*..*dim7* above). One of *black* or *white*. Blending towards diff --git a/foot-features.c b/foot-features.c index f701533c..8e332517 100644 --- a/foot-features.c +++ b/foot-features.c @@ -28,6 +28,12 @@ const char version_and_features[] = " -toplevel-tag" #endif +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + " +blur" +#else + " -blur" +#endif + #if !defined(NDEBUG) " +assertions" #else diff --git a/meson.build b/meson.build index aa8342ab..b7377652 100644 --- a/meson.build +++ b/meson.build @@ -188,6 +188,10 @@ if (wayland_protocols.version().version_compare('>=1.43')) wl_proto_xml += [wayland_protocols_datadir / 'staging/xdg-toplevel-tag/xdg-toplevel-tag-v1.xml'] add_project_arguments('-DHAVE_XDG_TOPLEVEL_TAG=1', language: 'c') endif +if (wayland_protocols.version().version_compare('>=1.45')) + wl_proto_xml += [wayland_protocols_datadir / 'staging/ext-background-effect/ext-background-effect-v1.xml'] + add_project_arguments('-DHAVE_EXT_BACKGROUND_EFFECT=1', language: 'c') +endif foreach prot : wl_proto_xml wl_proto_headers += custom_target( diff --git a/osc.c b/osc.c index 909fd484..4dc47172 100644 --- a/osc.c +++ b/osc.c @@ -1460,11 +1460,8 @@ osc_dispatch(struct terminal *term) case 11: term->colors.bg = color; - if (!have_alpha) { - alpha = term->colors.active_theme == COLOR_THEME_DARK - ? term->conf->colors_dark.alpha - : term->conf->colors_light.alpha; - } + if (!have_alpha) + alpha = term_theme_get(term)->alpha; const bool changed = term->colors.alpha != alpha; term->colors.alpha = alpha; @@ -1517,10 +1514,7 @@ osc_dispatch(struct terminal *term) case 104: { /* Reset Color Number 'c' (whole table if no parameter) */ - const struct color_theme *theme = - term->colors.active_theme == COLOR_THEME_DARK - ? &term->conf->colors_dark - : &term->conf->colors_light; + const struct color_theme *theme = term_theme_get(term); if (string[0] == '\0') { LOG_DBG("resetting all colors"); @@ -1560,11 +1554,7 @@ osc_dispatch(struct terminal *term) case 110: /* Reset default text foreground color */ LOG_DBG("resetting foreground color"); - const struct color_theme *theme = - term->colors.active_theme == COLOR_THEME_DARK - ? &term->conf->colors_dark - : &term->conf->colors_light; - + const struct color_theme *theme = term_theme_get(term); term->colors.fg = theme->fg; term_damage_color(term, COLOR_DEFAULT, 0); break; @@ -1572,11 +1562,7 @@ osc_dispatch(struct terminal *term) case 111: { /* Reset default text background color */ LOG_DBG("resetting background color"); - const struct color_theme *theme = - term->colors.active_theme == COLOR_THEME_DARK - ? &term->conf->colors_dark - : &term->conf->colors_light; - + const struct color_theme *theme = term_theme_get(term); bool alpha_changed = term->colors.alpha != theme->alpha; term->colors.bg = theme->bg; @@ -1595,11 +1581,7 @@ osc_dispatch(struct terminal *term) case 112: { LOG_DBG("resetting cursor color"); - const struct color_theme *theme = - term->colors.active_theme == COLOR_THEME_DARK - ? &term->conf->colors_dark - : &term->conf->colors_light; - + const struct color_theme *theme = term_theme_get(term); term->colors.cursor_fg = theme->cursor.text; term->colors.cursor_bg = theme->cursor.cursor; @@ -1615,11 +1597,7 @@ osc_dispatch(struct terminal *term) case 117: { LOG_DBG("resetting selection background color"); - const struct color_theme *theme = - term->colors.active_theme == COLOR_THEME_DARK - ? &term->conf->colors_dark - : &term->conf->colors_light; - + const struct color_theme *theme = term_theme_get(term); term->colors.selection_bg = theme->selection_bg; break; } @@ -1627,11 +1605,7 @@ osc_dispatch(struct terminal *term) case 119: { LOG_DBG("resetting selection foreground color"); - const struct color_theme *theme = - term->colors.active_theme == COLOR_THEME_DARK - ? &term->conf->colors_dark - : &term->conf->colors_light; - + const struct color_theme *theme = term_theme_get(term); term->colors.selection_fg = theme->selection_fg; break; } diff --git a/render.c b/render.c index 627da5d6..c47133b3 100644 --- a/render.c +++ b/render.c @@ -312,9 +312,7 @@ color_dim(const struct terminal *term, uint32_t color) } } - const struct color_theme *theme = term->colors.active_theme == COLOR_THEME_DARK - ? &conf->colors_dark - : &conf->colors_light; + const struct color_theme *theme = term_theme_get(term); return color_blend_towards( color, diff --git a/terminal.c b/terminal.c index 8ce9bd4b..ac7922a7 100644 --- a/terminal.c +++ b/terminal.c @@ -4819,3 +4819,11 @@ term_theme_toggle(struct terminal *term) term_damage_margins(term); render_refresh(term); } + +const struct color_theme * +term_theme_get(const struct terminal *term) +{ + return term->colors.active_theme == COLOR_THEME_DARK + ? &term->conf->colors_dark + : &term->conf->colors_light; +} diff --git a/terminal.h b/terminal.h index fe39341d..5a2a57aa 100644 --- a/terminal.h +++ b/terminal.h @@ -997,6 +997,7 @@ void term_send_size_notification(struct terminal *term); void term_theme_switch_to_dark(struct terminal *term); void term_theme_switch_to_light(struct terminal *term); void term_theme_toggle(struct terminal *term); +const struct color_theme *term_theme_get(const struct terminal *term); static inline void term_reset_grapheme_state(struct terminal *term) { diff --git a/tests/test-config.c b/tests/test-config.c index f83a9beb..05c70990 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -774,6 +774,8 @@ test_section_colors_dark(void) &conf.colors_dark.table[i]); } + test_boolean(&ctx, &parse_section_colors, "blur", &conf.colors_dark.blur); + test_invalid_key(&ctx, &parse_section_colors_dark, "256"); /* TODO: alpha (float in range 0-1, converted to uint16_t) */ @@ -853,6 +855,8 @@ test_section_colors_light(void) &conf.colors_light.table[i]); } + test_boolean(&ctx, &parse_section_colors, "blur", &conf.colors_light.blur); + test_invalid_key(&ctx, &parse_section_colors_light, "256"); /* TODO: alpha (float in range 0-1, converted to uint16_t) */ diff --git a/wayland.c b/wayland.c index 59df991a..1d258213 100644 --- a/wayland.c +++ b/wayland.c @@ -1193,6 +1193,27 @@ static const struct zxdg_toplevel_decoration_v1_listener xdg_toplevel_decoration .configure = &xdg_toplevel_decoration_configure, }; +#if defined(HAVE_EXT_BACKGROUND_EFFECT) +static void +ext_background_capabilities( + void *data, + struct ext_background_effect_manager_v1 *ext_background_effect_manager_v1, + uint32_t flags) +{ + struct wayland *wayl = data; + + wayl->have_background_blur = + !!(flags & EXT_BACKGROUND_EFFECT_MANAGER_V1_CAPABILITY_BLUR); + + LOG_DBG("compositor supports background blur: %s", + wayl->have_background_blur ? "yes" : "no"); +} + +static const struct ext_background_effect_manager_v1_listener background_manager_listener = { + .capabilities = &ext_background_capabilities, +}; +#endif /* HAVE_EXT_BACKGROUND_EFFECT */ + static bool fdm_repeat(struct fdm *fdm, int fd, int events, void *data) { @@ -1555,6 +1576,20 @@ handle_global(void *data, struct wl_registry *registry, wayl->registry, name, &xdg_toplevel_tag_manager_v1_interface, required); } #endif +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + else if (streq(interface, ext_background_effect_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->background_effect_manager = wl_registry_bind( + wayl->registry, name, + &ext_background_effect_manager_v1_interface, required); + + ext_background_effect_manager_v1_add_listener( + wayl->background_effect_manager, &background_manager_listener, wayl); + } +#endif #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED else if (streq(interface, zwp_text_input_manager_v3_interface.name)) { @@ -1569,6 +1604,7 @@ handle_global(void *data, struct wl_registry *registry, seat_add_text_input(&it->item); } #endif + } static void @@ -1882,6 +1918,10 @@ wayl_destroy(struct wayland *wayl) if (wayl->toplevel_tag_manager != NULL) xdg_toplevel_tag_manager_v1_destroy(wayl->toplevel_tag_manager); #endif +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + if (wayl->background_effect_manager != NULL) + ext_background_effect_manager_v1_destroy(wayl->background_effect_manager); +#endif if (wayl->color_management.img_description != NULL) wp_image_description_v1_destroy(wayl->color_management.img_description); @@ -1986,8 +2026,6 @@ wayl_win_init(struct terminal *term, const char *token) goto out; } - wayl_win_alpha_changed(win); - wl_surface_add_listener(win->surface.surf, &surface_listener, win); if (wayl->fractional_scale_manager != NULL && wayl->viewporter != NULL) { @@ -2000,6 +2038,16 @@ wayl_win_init(struct terminal *term, const char *token) win->fractional_scale, &fractional_scale_listener, win); } +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + if (wayl->background_effect_manager != NULL) { + win->surface.background_effect = + ext_background_effect_manager_v1_get_background_effect( + wayl->background_effect_manager, win->surface.surf); + } +#endif + + wayl_win_alpha_changed(win); + win->xdg_surface = xdg_wm_base_get_xdg_surface(wayl->shell, win->surface.surf); xdg_surface_add_listener(win->xdg_surface, &xdg_surface_listener, win); @@ -2206,7 +2254,12 @@ wayl_win_destroy(struct wl_window *win) free(it->item); tll_remove(win->xdg_tokens, it); -} + } + +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + if (win->surface.background_effect != NULL) + ext_background_effect_surface_v1_destroy(win->surface.background_effect); +#endif if (win->surface.color_management != NULL) wp_color_management_surface_v1_destroy(win->surface.color_management); @@ -2446,16 +2499,16 @@ void wayl_win_alpha_changed(struct wl_window *win) { struct terminal *term = win->term; + struct wayland *wayl = term->wl; /* * When fullscreened, transparency is disabled (see render.c). * Update the opaque region to match. */ - bool is_opaque = term->colors.alpha == 0xffff || win->is_fullscreen; + const bool is_opaque = term->colors.alpha == 0xffff || win->is_fullscreen; if (is_opaque) { - struct wl_region *region = wl_compositor_create_region( - term->wl->compositor); + struct wl_region *region = wl_compositor_create_region(wayl->compositor); if (region != NULL) { wl_region_add(region, 0, 0, INT32_MAX, INT32_MAX); @@ -2464,6 +2517,38 @@ wayl_win_alpha_changed(struct wl_window *win) } } else wl_surface_set_opaque_region(win->surface.surf, NULL); + +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + if (term_theme_get(term)->blur) { + if (wayl->have_background_blur) { + xassert(win->surface.background_effect != NULL); + + if (is_opaque) { + /* No transparency, disable blur */ + LOG_DBG("disabling background blur"); + ext_background_effect_surface_v1_set_blur_region( + win->surface.background_effect, NULL); + } else { + /* We have transparency, enable blur if user has enabled it */ + struct wl_region *region = wl_compositor_create_region(wayl->compositor); + if (region != NULL) { + LOG_DBG("enabling background blur"); + + wl_region_add(region, 0, 0, INT32_MAX, INT32_MAX); + ext_background_effect_surface_v1_set_blur_region( + win->surface.background_effect, region); + wl_region_destroy(region); + } + } + } else { + static bool have_warned = false; + if (!have_warned) { + LOG_WARN("background blur requested, but compositor does not support it"); + have_warned = true; + } + } + } +#endif /* HAVE_EXT_BACKGROUND_EFFECT */ } static void diff --git a/wayland.h b/wayland.h index 6247875a..9cbd1023 100644 --- a/wayland.h +++ b/wayland.h @@ -26,6 +26,9 @@ #if defined(HAVE_XDG_TOPLEVEL_TAG) #include <xdg-toplevel-tag-v1.h> #endif +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + #include <ext-background-effect-v1.h> +#endif #include <fcft/fcft.h> #include <tllist.h> @@ -62,6 +65,10 @@ struct wayl_surface { struct wl_surface *surf; struct wp_viewport *viewport; struct wp_color_management_surface_v1 *color_management; + +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + struct ext_background_effect_surface_v1 *background_effect; +#endif }; struct wayl_sub_surface { @@ -490,6 +497,10 @@ struct wayland { #if defined(HAVE_XDG_TOPLEVEL_TAG) struct xdg_toplevel_tag_manager_v1 *toplevel_tag_manager; #endif +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + struct ext_background_effect_manager_v1 *background_effect_manager; + bool have_background_blur; +#endif #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED struct zwp_text_input_manager_v3 *text_input_manager; From 046898f1b8e89b7ad1677905cc468daa98546b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 2 Mar 2026 09:42:31 +0100 Subject: [PATCH 1309/1323] test: config: blur: fix test failure; use the correct parsing function --- tests/test-config.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test-config.c b/tests/test-config.c index 05c70990..9774cba9 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -774,7 +774,7 @@ test_section_colors_dark(void) &conf.colors_dark.table[i]); } - test_boolean(&ctx, &parse_section_colors, "blur", &conf.colors_dark.blur); + test_boolean(&ctx, &parse_section_colors_dark, "blur", &conf.colors_dark.blur); test_invalid_key(&ctx, &parse_section_colors_dark, "256"); @@ -855,7 +855,7 @@ test_section_colors_light(void) &conf.colors_light.table[i]); } - test_boolean(&ctx, &parse_section_colors, "blur", &conf.colors_light.blur); + test_boolean(&ctx, &parse_section_colors_light, "blur", &conf.colors_light.blur); test_invalid_key(&ctx, &parse_section_colors_light, "256"); From e48178bec3c5fd6bd1b6aa25c28cce2c9ff6874e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 2 Mar 2026 11:48:14 +0100 Subject: [PATCH 1310/1323] readme: update sixel screenshot Closes #2215 --- README.md | 2 +- doc/sixel-tux-foot.png | Bin 0 -> 297553 bytes doc/sixel-wow.png | Bin 119970 -> 0 bytes doc/tux-foot-ok.png | Bin 0 -> 404008 bytes 4 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 doc/sixel-tux-foot.png delete mode 100644 doc/sixel-wow.png create mode 100644 doc/tux-foot-ok.png diff --git a/README.md b/README.md index 7ee771ba..985c7e33 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ The fast, lightweight and minimalistic Wayland terminal emulator. * [Synchronized Updates](https://gitlab.freedesktop.org/terminal-wg/specifications/-/merge_requests/2) support * [Sixel image support](https://en.wikipedia.org/wiki/Sixel) - ![wow](doc/sixel-wow.png "Sixel screenshot") + ![tux-with-foot](doc/sixel-tux-foot.png "Sixel screenshot") # Installing diff --git a/doc/sixel-tux-foot.png b/doc/sixel-tux-foot.png new file mode 100644 index 0000000000000000000000000000000000000000..ce30fe8fff52aa58817ded0ddf44350cea73d87d GIT binary patch literal 297553 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�__<#$&4>} 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%0DCnlyh9awGq?&TSK7Bgvw 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 F33DAeRbB|^YQ7o_IcM@ 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 ?5g`|;*WBU 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 D_T*7t literal 0 HcmV?d00001 diff --git a/doc/sixel-wow.png b/doc/sixel-wow.png deleted file mode 100644 index da481ac8dce811601d377086651bf0d5e62514be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 119970 zcmeAS@N?(olHy`uVBq!ia0y~yU@~K1U{vE^VqjqS_sTbgfq{W7$=lt9;Xep2*t>i( z1A_vCr;B4q#hf>L9s8f0n5p{X(3##XCd)-7*K~Y2cjwJ1U0<hHPTYsMoXlHh+Mdxd zI3V}T<iMGK+_KM%YF|0e|8=E(-`PJcc4y>YM4r(T<2F$-*&<+a;hR#lQ|6}4x3rTa zQZC+|RKh*${X^qfvoG5!ZQitMiPlnk77n(#e#<A9RDav|{$_gm|5yJ1nEfmswg~%K zJbc|Q+3>vn|LObRGUpT=;#8kga7g6M_F5(o1H_Y2@3MlZ%<d9)oeNg-!Srb7B8UM$ zW<BC?{rhPBzdvjFzjyEdRs3sr{rvOQXMV^3Qd0kO^nTrS!}x01cUJA|AJu?$?CYE% zoA>L*{eLHK%f1$`|N8vl-jBbgm;TA;y7$5A$d5h0=Yy=+!@a}b)z{?Il5+VFryF(Q zx2-M;yD#<sqnW?LeuZg&f7yA?_UU}~<}y##mCMe%`uF$#X~}&0cQ1an2f4N4c%h5y z(T?bXsD<Cm<95D|o&2@aZpZ74{Dt;>_e(1deV=nO`=eNT>+YAY=6Rc2J_LnBOS#PY zjovNKKAl}$R&KhfU3#T!-EOy^Tjv*jdHl}mgp0@B|AP5D{=5JG=HK_O?EFtiU<o`n z)q3^$(T2!=)3efY4^P`J1)C+jZ{ZIk-{;>oW>=>if1-NJsXqSE&b^9!!NK_(|97VP zgTj8FNKIf@Rc>+3bw0!ScU=>{%Y1&D)cjpW<s9=~PrnzR-~GS!*%IU%i^QhwJYRfu zI1_)n@INeO`}Jt?-mQxt`kwoFw*QY<dTGi3!+WkjpOg)bvG)q+{#~8_r(Hkd<sa!p zr{7ico?QRD1`_gW6{j8Uul)W0nnmm`urtk%i1U7Vm@@C*9E8Ypffv?bTOYQpXIk#{ z9Bit3jc?(i6<|HW`<BgMtoY`?@9S;5UWka_j3l?t6;JE`_3wXc3~}<q7TY;YwJ^OO zmj+((ulsb}{^!Z~`qKO*ckjJZX8L!!{?}#w+VlDy@_&!^@B1hJW%2$W7sV@oi|@&f z_m}w_6n`qX>JX<oOU=ot=k{CuxhVhl^y2-04ygWr6#xHb{<ofYM?hxQe{ui+$^3(3 zquIYt_y0Z(|K(W!^Zmb1pM$Q17Ygn>lH8X5(%a_b{TsFR4_nIF*DqfIw(XMGhweYC z{)3!wz~|$y*otDQ>g+>{%U+$0UcG(awvV-yldHdkUcO<@@$qhDcAVwLi=66z*gm+1 zUWt9!x501!laqOc%!W6MzPw%UDl>iY*ZPm^|K~VGCH}rxes}-#*~-tBUpK2)@lE=~ z5v{gHxLe{@o%mCksbTK+FEkF<YM#nD#F%J#)8gT$-|v6N?kW1h@%{bJoj0Doi1gie zj#GWk2hoS7J6HVP{rq9O)z7t;ra2c`-Kp53YPIXRt;H4p#`*7*7FH<#-CK5k@sG{( z|K)6I>X;gTyG3^Iw#NBP(HHiADc|v5@#cm5imUgEmGAxAt+3(HLuVn8^gR>*-BX(D z>|XtP`~Gh;K7QLh?{=`#s}|vXF)|BR+>c=TKfBNV_sjGDzWl!5eNyv*`OLmu-F5Gl zFLiK=uTuD)J+HN^|EH?lH$64Ghb_YUINou8*<1Sf>rv}T_ZO_w*mz}r-T&&u)A<wU zCw={_c5~%n<(g(Ijr_W~{qKH0OZwg-?Dr%0A&0AM?#_EMg){!Go%yVItJGwJyZJng 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�bIpf++}=HfPWAto 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%0Dw`QB;&u3LZpoM$xo|8xE0 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 FngFqdq+kF5 diff --git a/doc/tux-foot-ok.png b/doc/tux-foot-ok.png new file mode 100644 index 0000000000000000000000000000000000000000..5d814623eb7d968a03620ef279bfa8d1cda07903 GIT binary patch literal 404008 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�G?$4t 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%0AF6fRy8SQD&noFSPI;3D6L0Wu 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 literal 0 HcmV?d00001 From c9c5c55745ba067809b26fb98063f46203611955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Mon, 2 Mar 2026 11:38:10 +0100 Subject: [PATCH 1311/1323] Remove metainfo.xml It hasn't been kept up to date for a long time, and no one seems to care, so lets remove it. --- org.codeberg.dnkl.foot.metainfo.xml | 57 ----------------------------- 1 file changed, 57 deletions(-) delete mode 100644 org.codeberg.dnkl.foot.metainfo.xml diff --git a/org.codeberg.dnkl.foot.metainfo.xml b/org.codeberg.dnkl.foot.metainfo.xml deleted file mode 100644 index 1b7c46a7..00000000 --- a/org.codeberg.dnkl.foot.metainfo.xml +++ /dev/null @@ -1,57 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<component type="desktop-application"> - <id>org.codeberg.dnkl.foot</id> - <metadata_license>MIT</metadata_license> - <project_license>MIT</project_license> - <developer_name>dnkl</developer_name> - <name>foot</name> - <summary>The fast, lightweight and minimalistic Wayland terminal emulator.</summary> - <description> - <ul> - <li>Fast</li> - <li>Lightweight, in dependencies, on-disk and in-memory</li> - <li>Wayland native</li> - <li>DE agnostic</li> - <li>Server/daemon mode</li> - <li>User configurable font fallback</li> - <li>On-the-fly font resize</li> - <li>On-the-fly DPI font size adjustment</li> - <li>Scrollback search</li> - <li>Keyboard driven URL detection</li> - <li>Color emoji support</li> - <li>IME (via text-input-v3)</li> - <li>Multi-seat</li> - <li>True Color (24bpp)</li> - <li>Styled and colored underlines</li> - <li>Synchronized Updates support</li> - <li>Sixel image support</li> - </ul> - </description> - <screenshots> - <screenshot type="default"> - <caption>Foot with sixel graphics</caption> - <image>https://codeberg.org/dnkl/foot/media/branch/master/doc/sixel-wow.png</image> - </screenshot> - </screenshots> - <releases> - <release version="1.18.1" date="2024-08-14"/> - <release version="1.18.0" date="2024-08-02"/> - <release version="1.17.2" date="2024-04-17"/> - <release version="1.17.1" date="2024-04-11"/> - <release version="1.17.0" date="2024-04-02"/> - <release version="1.16.2" date="2023-10-17"/> - <release version="1.16.1" date="2023-10-12"/> - <release version="1.16.0" date="2023-10-11"/> - <release version="1.15.3" date="2023-08-07"/> - <release version="1.15.2" date="2023-07-30"/> - <release version="1.15.1" date="2023-07-21"/> - <release version="1.15.0" date="2023-07-14"/> - <release version="1.14.0" date="2023-04-03"/> - <release version="1.13.1" date="2022-08-31"/> - <release version="1.13.0" date="2022-08-07"/> - </releases> - <launchable type="desktop-id">org.codeberg.dnkl.foot.desktop</launchable> - <url type="homepage">https://codeberg.org/dnkl/foot</url> - <url type="bugtracker">https://codeberg.org/dnkl/foot/issues</url> - <content_rating type="oars-1.1"/> -</component> From 3bbaa64caef285727ff63dcaef614a01c231935c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 3 Mar 2026 17:35:02 +0100 Subject: [PATCH 1312/1323] changelog: prepare for 1.26.0 --- CHANGELOG.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ada2084..c9319875 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.26.0](#1-26-0) * [1.25.0](#1-25-0) * [1.24.0](#1-24-0) * [1.23.1](#1-23-1) @@ -67,7 +67,8 @@ * [1.2.0](#1-2-0) -## Unreleased +## 1.26.0 + ### Added * `toplevel-tag` option (and `--toplevel-tag` command line options to @@ -146,9 +147,20 @@ [2263]: https://codeberg.org/dnkl/foot/issues/2263 -### Security ### Contributors +* Andrei +* Barinderpreet Singh +* c4llv07e +* Johannes Altmanninger +* nariby +* pi66 +* Ronan Pigott +* Stéphane Klein +* valoq +* Whyme Lyu +* Yaakov Selkowitz + ## 1.25.0 From 739cf115e6bc014b895bf945afccb88be6971be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 3 Mar 2026 17:35:20 +0100 Subject: [PATCH 1313/1323] meson: bump version to 1.26.0 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index b7377652..66b3d6bc 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.25.0', + version: '1.26.0', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From ebacb14be80d7d10bc4f0a2c8eb9238c283abecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 3 Mar 2026 17:38:46 +0100 Subject: [PATCH 1314/1323] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9319875..0f02e550 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.26.0](#1-26-0) * [1.25.0](#1-25-0) * [1.24.0](#1-24-0) @@ -67,6 +68,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.26.0 ### Added From c05bd55029b8a9bb57df246fec9acc786d71d16f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Thu, 5 Mar 2026 16:17:09 +0100 Subject: [PATCH 1315/1323] doc: foot.ini: fix default value of initial-color-theme Closes #2292 --- CHANGELOG.md | 7 +++++++ doc/foot.ini.5.scd | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f02e550..30a73369 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,13 @@ ### Deprecated ### Removed ### Fixed + +* Wrong documented default value for `initial-color-theme` in + `foot.ini(5)` ([#2292][2292]). + +[2292]: https://codeberg.org/dnkl/foot/issues/2292 + + ### Security ### Contributors diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 2f5fc38c..a1ee326f 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -392,7 +392,7 @@ empty string to be set, but it must be quoted: *KEY=""* at runtime, or send SIGUSR1/SIGUSR2 to the foot process (see *foot*(1) for details). - Default: _1_ + Default: _dark_ *initial-window-size-pixels* Initial window width and height in _pixels_ (subject to output From f49fdf7ca3a69d987407c4c1965e32697fb671d7 Mon Sep 17 00:00:00 2001 From: Roshless <me@roshless.com> Date: Thu, 5 Mar 2026 19:11:44 +0100 Subject: [PATCH 1316/1323] themes: paper-color-light: fix newline --- themes/paper-color-light | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/themes/paper-color-light b/themes/paper-color-light index 2f7a8003..554aabc0 100644 --- a/themes/paper-color-light +++ b/themes/paper-color-light @@ -4,7 +4,7 @@ [main] initial-color-theme=light -xs + [colors-light] cursor=eeeeee 444444 background=eeeeee From 4fd682b4e8d985ce25d2bd599c1d855bc1489650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 10 Mar 2026 07:59:40 +0100 Subject: [PATCH 1317/1323] meson: clang: add -Wno-wc2y-extensions Recent clang versions warn on __COUNTER__, unless compiling with -std=c2y (which breaks other things). "Fixes" '__COUNTER__' is a C2y extension (__COUNTER__ is used by our UNITTEST macro). --- meson.build | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/meson.build b/meson.build index 66b3d6bc..16e8e3c0 100644 --- a/meson.build +++ b/meson.build @@ -12,6 +12,11 @@ is_debug_build = get_option('buildtype').startswith('debug') cc = meson.get_compiler('c') +# Newer clang versions warns when using __COUNTER__ without -std=c2y +if cc.has_argument('-Wc2y-extensions') + add_project_arguments('-Wno-c2y-extensions', language: 'c') +endif + if cc.has_function('memfd_create', args: ['-D_GNU_SOURCE'], prefix: '#include <sys/mman.h>') From 657db18a4ec4df93689c3eaae03b70f851724001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 10 Mar 2026 07:46:03 +0100 Subject: [PATCH 1318/1323] wayland: do all surface unmap and roundtrips before waiting for pre-apply damage The pre-apply damage thread may be running when we destroy a terminal instance, and we need to wait for it to finish before destroying the underlying buffer. c291194a4e593bbbb91420e81fa0111508084448 did this, but failed to realize the thread may get re-started in the roundtrips done later in wayl_win_destroy(); after the wait added in c291194a4e593bbbb91420e81fa0111508084448, we unmap all surfaces (including the main grid), and roundtrip. This means the compositor will release the currently active buffer, and that means _we_ will trigger the pre-apply damage thread on it. This introduces a race, where wayl_win_destroy() may reach its shm_purge() calls before the pre-apply damage thread has finished. That typically causes foot to crash. Closes #2288 --- CHANGELOG.md | 3 +++ wayland.c | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30a73369..30b3e1e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,8 +77,11 @@ * Wrong documented default value for `initial-color-theme` in `foot.ini(5)` ([#2292][2292]). +* Occasional crashes when closing a window and + `tweak.pre-apply-damage=yes` (the default) ([#2288][2288]). [2292]: https://codeberg.org/dnkl/foot/issues/2292 +[2288]: https://codeberg.org/dnkl/foot/issues/2288 ### Security diff --git a/wayland.c b/wayland.c index 1d258213..1ffd62a6 100644 --- a/wayland.c +++ b/wayland.c @@ -2177,8 +2177,6 @@ wayl_win_destroy(struct wl_window *win) struct terminal *term = win->term; - render_wait_for_preapply_damage(term); - if (win->csd.move_timeout_fd != -1) close(win->csd.move_timeout_fd); @@ -2236,6 +2234,8 @@ wayl_win_destroy(struct wl_window *win) tll_remove(win->urls, it); } + render_wait_for_preapply_damage(term); + csd_destroy(win); wayl_win_subsurface_destroy(&win->search); wayl_win_subsurface_destroy(&win->scrollback_indicator); From eed2d668ecdb0705142d27950a6d8c1923df32f1 Mon Sep 17 00:00:00 2001 From: vlkrs <vlkrs@noreply.codeberg.org> Date: Thu, 12 Mar 2026 18:47:50 +0100 Subject: [PATCH 1319/1323] OpenBSD has UTF-32 --- char32.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/char32.c b/char32.c index 3d6c2c78..be5bf229 100644 --- a/char32.c +++ b/char32.c @@ -34,7 +34,7 @@ _Static_assert( #if !defined(__STDC_UTF_32__) || !__STDC_UTF_32__ #error "char32_t does not use UTF-32" #endif -#if (!defined(__STDC_ISO_10646__) || !__STDC_ISO_10646__) && !defined(__FreeBSD__) +#if (!defined(__STDC_ISO_10646__) || !__STDC_ISO_10646__) && !defined(__FreeBSD__) && !defined(__OpenBSD__) #error "wchar_t does not use UTF-32" #endif From 370adaf6975c7128107d187928c7f9cdff247930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 14 Mar 2026 08:35:15 +0100 Subject: [PATCH 1320/1323] changelog: prepare for 1.26.1 --- CHANGELOG.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30b3e1e4..b62f5c92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -* [Unreleased](#unreleased) +* [1.26.1](#1-26-1) * [1.26.0](#1-26-0) * [1.25.0](#1-25-0) * [1.24.0](#1-24-0) @@ -68,11 +68,8 @@ * [1.2.0](#1-2-0) -## Unreleased -### Added -### Changed -### Deprecated -### Removed +## 1.26.1 + ### Fixed * Wrong documented default value for `initial-color-theme` in @@ -84,9 +81,11 @@ [2288]: https://codeberg.org/dnkl/foot/issues/2288 -### Security ### Contributors +* Roshless +* vlkrs + ## 1.26.0 From ef15414b301513a75193fd872de79d6379f41a79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 14 Mar 2026 08:35:28 +0100 Subject: [PATCH 1321/1323] meson: bump version to 1.26.1 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 16e8e3c0..a0e602bb 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.26.0', + version: '1.26.1', license: 'MIT', meson_version: '>=0.59.0', default_options: [ From 2fb7bb0ea4a240c6a8d921698d89d6044dea16e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 14 Mar 2026 08:38:15 +0100 Subject: [PATCH 1322/1323] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b62f5c92..a0654b08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.26.1](#1-26-1) * [1.26.0](#1-26-0) * [1.25.0](#1-25-0) @@ -68,6 +69,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.26.1 ### Fixed From 037a2f4fa2c6fab014248d62efa8a6e14f617832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Sat, 21 Mar 2026 14:43:27 +0100 Subject: [PATCH 1323/1323] term: enqueue data to slave if there are queued paste data buffers When writing paste data to the terminal (either interactively, or as an OSC-52 reply), we enqueue other data (key presses, for examples, or query replies) while the paste is happening. The idea is to send the key press _after_ all paste data has been written, to ensure consistency. Unfortunately, we only checked for an on-going paste. I.e. where the paste itself hasn't yet finished. It is also possible the paste itself has finished, but we haven't yet flushed all the paste buffers. That is, if we were able to *receive* paste data faster than the terminal client was able to *consume* it. In this case, we've queued up paste data in the terminal. These are in separate queues, and when emitting e.g. a key press, we didn't check if all _those_ queues had been flushed yet. Closes #2307 --- CHANGELOG.md | 7 +++++++ terminal.c | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0654b08..f554124b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,13 @@ ### Deprecated ### Removed ### Fixed + +* Other output (key presses, query replies etc) being mixed with paste + data, both interactive pastes and OSC-52 ([#2307][2307]). + +[2307]: https://codeberg.org/dnkl/foot/issues/2307 + + ### Security ### Contributors diff --git a/terminal.c b/terminal.c index ac7922a7..8eafbcbe 100644 --- a/terminal.c +++ b/terminal.c @@ -120,7 +120,10 @@ term_to_slave(struct terminal *term, const void *data, size_t len) return false; } - if (tll_length(term->ptmx_buffers) > 0 || term->is_sending_paste_data) { + if (unlikely(tll_length(term->ptmx_buffers) > 0 || + term->is_sending_paste_data || + tll_length(term->ptmx_paste_buffers) > 0)) + { /* * Don't even try to send data *now* if there's queued up * data, since that would result in events arriving out of