diff --git a/.forgejo/issue_template/issue_template.yml b/.forgejo/issue_template/bug.yml similarity index 70% rename from .forgejo/issue_template/issue_template.yml rename to .forgejo/issue_template/bug.yml index eae2e492..921bd68f 100644 --- a/.forgejo/issue_template/issue_template.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: @@ -38,15 +38,36 @@ 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 + id: multiplexer + attributes: + label: Terminal multiplexer + description: "Terminal multiplexers are terminal emulators themselves, therefore the issue may be in the multiplexer, not foot. Please list which multiplexer(s) you use here (and mention in the problem description below if the issue only occurs in the multiplexer, but not in bare metal foot)" + placeholder: "tmux, zellij" + - type: input + id: application + attributes: + label: Shell, TUI, application + description: "Application(s) in which the problem occurs (list all known)" + placeholder: "bash, neovim" + - type: checkboxes + id: server + attributes: + label: Server/standalone mode + description: Does the issue occur in foot server, or standalone mode, or both? Note that you **cannot** test standalone mode by manually running `foot` from a `footclient` instance, since then the standalone foot will simply inherit the server process' context. + options: + - label: Standalone + - label: Server - type: textarea id: config attributes: label: Foot config - description: Paste your entire `foot.ini` here + description: Paste your entire `foot.ini` here (do not forget to sanitize it!) + render: ini validations: required: true - type: textarea 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 diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 340ba241..900251a7 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,10 +11,15 @@ 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 + - python -m pip install --upgrade pip - pip install codespell - - codespell -Lser,doas,zar README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd + - pip install mypy + - pip install ruff + - codespell + - mypy + - ruff check - deactivate - name: subprojects diff --git a/CHANGELOG.md b/CHANGELOG.md index 8649e967..f554124b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +* [Unreleased](#unreleased) +* [1.26.1](#1-26-1) +* [1.26.0](#1-26-0) +* [1.25.0](#1-25-0) +* [1.24.0](#1-24-0) +* [1.23.1](#1-23-1) +* [1.23.0](#1-23-0) +* [1.22.3](#1-22-3) +* [1.22.2](#1-22-2) +* [1.22.1](#1-22-1) +* [1.22.0](#1-22-0) +* [1.21.0](#1-21-0) * [1.20.2](#1-20-2) * [1.20.1](#1-20-1) * [1.20.0](#1-20-0) @@ -57,6 +69,584 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed + +* Other output (key presses, query replies etc) being mixed with paste + data, both interactive pastes and OSC-52 ([#2307][2307]). + +[2307]: https://codeberg.org/dnkl/foot/issues/2307 + + +### Security +### Contributors + + +## 1.26.1 + +### Fixed + +* Wrong documented default value for `initial-color-theme` in + `foot.ini(5)` ([#2292][2292]). +* Occasional crashes when closing a window and + `tweak.pre-apply-damage=yes` (the default) ([#2288][2288]). + +[2292]: https://codeberg.org/dnkl/foot/issues/2292 +[2288]: https://codeberg.org/dnkl/foot/issues/2288 + + +### Contributors + +* Roshless +* vlkrs + + +## 1.26.0 + +### Added + +* `toplevel-tag` option (and `--toplevel-tag` command line options to + `foot` and `footclient`), allowing you to set a custom toplevel + tag. The compositor must implement the new `xdg-toplevel-tag-v1` + Wayland protocol ([#2212][2212]). +* `[colors-dark]` section to `foot.ini`. Replaces `[colors]`. +* `[colors-light]` section to `foot.ini`. Replaces `[colors2]`. +* `XTGETTCAP`: added `query-os-name`, returning the OS foot is + compiled for (e.g. _'Linux'_) ([#2209][2209]). +* `pad` option now supports 4-directional padding format: + `LEFTxTOPxRIGHTxBOTTOM` (e.g., `20x10x20x10`). +* `--config=PATH` option is now automatically passed to new + terminals spawned via `spawn-terminal` action ([#2259][2259]). +* Preliminary (untested) support for background blur via the new + `ext-background-effect-v1` protocol. Enable by setting + `colors-{dark,light}.blur=yes`. Foot needs to have been **built** + against `wayland-protocols >= 1.45`, and the compositor **must** + implement the `ext-background-effect-v1` protocol, **and** the + `blur` effect. + +[2212]: https://codeberg.org/dnkl/foot/issues/2212 +[2209]: https://codeberg.org/dnkl/foot/issues/2209 +[2259]: https://codeberg.org/dnkl/foot/pulls/2259 + + +### Changed + +* When enabling _"focus mode"_ (private mode 1004), foot now sends a + focus event immediately, to inform the application what the current + state is ([#2202][2202]). +* Scrollback search is now case sensitive when the search string + contains at least one upper case character. +* Mouse tracking in SGR pixel mode no longer emits negative column/row + pixel values ([#2226][2226]). +* Foot now always uses ARGB SHM surfaces. In earlier versions, XRGB + surfaces were used for opaque surfaces. Unfortunately, several + compositors had issues when foot switched between ARGB and XRGB + surfaces (for example when switching color theme, or toggling + fullscreen). + +[2202]: https://codeberg.org/dnkl/foot/issues/2202 +[2226]: https://codeberg.org/dnkl/foot/issues/2226 + + +### Deprecated + +* `[colors]` section in `foot.ini`. Use `[colors-dark]` instead. +* `[colors2]` section in `foot.ini`. Use `[colors-light]` instead. + + +### Removed + +* `cursor.color` config option (deprecated in 1.23.0). Use + `colors-{dark,light}.cursor` instead. + + +### Fixed + +* Search mode: composing keys not ignored. +* Crash when triple-clicking a soft-wrapped line and there is a quote + character in the last column. +* Crash when reverse-scrolling (terminfo capability `rin`) such that + the current viewport ends up outside the scrollback ([#2232][2232]). +* Regression: visual glitches in rare circumstances. +* Key release events for shortcuts being sent to the client + application (kitty keyboard protocol only) ([#2257][2257]). +* Crash when application emits sixel RA with a height of 0, a width > + 0, and then starts writing sixel data ([#2267][2267]). +* Crash if shutting down terminal instance while a "pre-apply damage" + thread is running ([#2263][2263]). + +[2232]: https://codeberg.org/dnkl/foot/issues/2232 +[2257]: https://codeberg.org/dnkl/foot/issues/2257 +[2267]: https://codeberg.org/dnkl/foot/issues/2267 +[2263]: https://codeberg.org/dnkl/foot/issues/2263 + + +### Contributors + +* Andrei +* Barinderpreet Singh +* c4llv07e +* Johannes Altmanninger +* nariby +* pi66 +* Ronan Pigott +* Stéphane Klein +* valoq +* Whyme Lyu +* Yaakov Selkowitz + + +## 1.25.0 + +### Added + +* Performance increased and input latency decreased on compositors + that do not release SHM buffers immediately ([#2188][2188]). +* `colors{,2}.dim-blend-towards=black|white` option, allowing you to + select towards which color to blend when dimming text. Defaults to + `black` in `[colors]`, and `white` in `[colors2]` ([#2187][2187]). + +[2188]: https://codeberg.org/dnkl/foot/issues/2188 +[2187]: https://codeberg.org/dnkl/foot/issues/2187 + + +### Changed + +* SHM buffer sizes are now rounded up to nearest page size, and their + stride is always an even multiple of 256 bytes (by default, + configurable by setting `tweak.min-stride-alignment`). This allows + compositor to directly import foot's SHM buffers to the GPU, with + e.g. integrated graphics ([#2182][2182]). +* Jump label colors in the modus-operandi theme, for improved + readability. + +[2182]: https://codeberg.org/dnkl/foot/issues/2182 + + +### Fixed + +* URL labels misplaces when URL contains double-width characters + ([#2179][2179]). +* One space too much consumed when copying (or pipe:ing) contents with + tabs ([#2194][2194]) +* Ensure we render a new frame when changing fullscreen state. Before, + this was automatically done if the window was also resized. But, it + is possible for a compositor to change an application's fullscreen + state without resizing the window. + +[2179]: https://codeberg.org/dnkl/foot/issues/2179 +[2194]: https://codeberg.org/dnkl/foot/issues/2194 + + +### Contributors + +* Charalampos Mitrodimas +* Matthias Heyman + + +## 1.24.0 + +### Added + +* The `uppercase-regex-insert` option controls whether an uppercase hint + character will insert the selected text into the prompt in `regex-copy` + or `show-urls-copy` mode. It defaults to `true`. ([#2159][2159]). + +[2159]: https://codeberg.org/dnkl/foot/issues/2159 + +### Changed + +* The label letters are no longer sorted before being assigned to URLs + ([#2140][2140]). +* Sending SIGUSR1/SIGUSR2 to a `foot --server` process now causes + newly spawned client instances to use the selected theme, instead of + the original one. +* SIGUSR1/SIGUSR2 can now be sent to `footclient` processes, to change + the theme of that particular instance ([#2156][2156]). + +[2156]: https://codeberg.org/dnkl/foot/issues/2156 + + +### Fixed + +* Invalid configuration values overriding valid ones in surprising + ways. +* Bug where the libutempter utmp backend did not record logouts + correctly. + +### Contributors + +* Ryan Roden-Corrent +* Tobias Mock + + +## 1.23.1 + +### Changed + +* URL labels are now assigned in reverse order, from bottom to + top. This ensures the **last** URL (which is often the one you are + interested in) is always assigned the same key ([#2140][2140]). +* Sending `SIGUSR1` no longer **toggles** between `[colors]` and + `[colors2]`, but explicitly changes to `[colors]`. `SIGUSR2` changes + to `[colors2]` ([#2144][2144]). + +[2140]: https://codeberg.org/dnkl/foot/issues/2140 +[2144]: https://codeberg.org/dnkl/foot/issues/2144 + + +### Fixed + +* 10-bit surfaces sometimes used instead of 16-bit. +* OSC-104/110/111/112/117/119 (reset colors) not taking the currently + active theme into account. + + +## 1.23.0 + +### Added + +* `colors2` config section. This section duplicates the `colors` + section, and lets you define an alternative color theme. +* `key-bindings.color-theme-switch-1`, + `key-bindings.color-theme-switch-2` and + `key-bindings.color-theme-toggle` key bindings. These can be used to + switch between the primary and alternative color themes. They are + not bound by default. +* Sending `SIGUSR1` to the foot process now triggers a theme switch + (in server mode, **all** instances toggles their themes). +* Support for private mode 2031 - [_Dark and Light Mode + Detection_](https://contour-terminal.org/vt-extensions/color-palette-update-notifications/) + ([#2025][2025]) +* Added `initial-color-theme=1|2` config option. `1` uses colors from + the `[colors]` section, `2` uses `[colors2]`. +* Combined dark/light theme files for (dark variant is the default, + set `initial-color-theme=2` to use the light variant by default): + - gruvbox + - nvim + - paper-color + - selenized + - solarized +* `regex-copy`/`show-urls-copy` will copy and paste the selected text if the hint + is completed with an uppercase character ([#1975][1975]). +* `16-bit` to `tweak.surface-bit-depth`. Makes foot use 16-bit image + buffers. They provide the necessary color precision required by + `gamma-correct-blending=yes`. +* New cursor shapes, from `cursor-shape-v1` version 2. +* `center-when-fullscreen` and `center-when-maximized-and-fullscreen` + to the `pad` option. This allows you to configure when the grid is + centered in more detail ([#2111][2111]). + +[2025]: https://codeberg.org/dnkl/foot/issues/2025 +[1975]: https://codeberg.org/dnkl/foot/issues/1975 +[2111]: https://codeberg.org/dnkl/foot/issues/2111 + + +### Changed + +* `cursor.color` moved to `colors.cursor`. +* OSC-11 without an alpha value will now restore the configured + (i.e. from `foot.ini`) alpha, rather than keeping whatever the + current alpha value is, unchanged. +* `gamma-correct-blending=yes` now defaults to `16-bit` image buffers, + instead of `10-bit`. +* Allow setting either selection background, or selection foreground, + only ([#1846][1846]). +* Drop required version of libxkbcommon from 1.8.0 back to 1.0.0 + ([#2103][2103]). +* OSC-52: an empty payload now clears the clipboard. +* DA (Device Attributes): include `52` in the reply, to indicate + OSC-52 support (when at least _copy_ has been enabled in + `security.osc52`). + +[1846]: https://codeberg.org/dnkl/foot/issues/1846 +[2103]: https://codeberg.org/dnkl/foot/issues/2103 + + +### Deprecated + +* `cursor.color` config option; use `colors.cursor` instead. + + +### Removed + +* Subsurface unmap quirk for Sway. This was a workaround added in + 1.12.1, for Sway issue [#6960][sway-6960]. + + +### Fixed + +* `REP`: wrong width of repeated multi-codepoint graphemes. +* Incorrect surface commit after a configure event, under certain + conditions ([#2105][2105]). + +[2105]: https://codeberg.org/dnkl/foot/issues/2105 + + +### Contributors + +* Chen Mulong +* Kirill Primak +* Ryan Roden-Corrent +* tokyo4j + + +## 1.22.3 + +### Added + +* `auto` to the `tweak.surface-bit-depth` option. + + +### Changed + +* `gamma-correct-blending` now defaults to `no` instead of `yes`. +* `tweak.surface-bit-depth` default value changed to `auto`; uses + 10-bit surfaces when `gamma-correct-blending=yes`, and 8-bit + surfaces otherwise. + + +### Fixed + +* Inaccurate colors when `gamma-correct-blending=yes` ([#2082][2082]). + +[2082]: https://codeberg.org/dnkl/foot/issues/2082 + + +## 1.22.2 + +### Changed + +* `gamma-correct-blending=yes` now uses a pure gamma 2.2 transfer + function, instead of the piece-wise sRGB transfer function, to match + what compositors do. + + +### Fixed + +* Wrong colors when `gamma-correct-blending=yes` (the default when + there is compositor support). Note that some colors will still be + off by a **very** small amount, due to loss of precision when + converting to a linear color space. ([#2035][2035]). + +[2035]: https://codeberg.org/dnkl/foot/issues/2035 + + +## 1.22.1 + +### Fixed + +* `colors.alpha-mode=matching` not working as intended. +* Grapheme shaping was allowed to be "enabled" at runtime, even though + disabled at compile time. This caused mis-rendering of certain + codepoints ([#2039][2039]). +* Keyboard modifiers not being reset on keyboard leave events + ([#2034][2034]). +* Fallback font (and possibly wrong color) being used when a character + was followed by a zero-width grapheme breaking codepoint (for + example, _LEFT-TO-RIGHT MARK_) ([#2049][2049]). +* Regression: alpha applied to inversed text/selections + ([#2073][2073]). + +[2039]: https://codeberg.org/dnkl/foot/issues/2039 +[2034]: https://codeberg.org/dnkl/foot/issues/2034 +[2049]: https://codeberg.org/dnkl/foot/issues/2049 +[2073]: https://codeberg.org/dnkl/foot/issues/2073 + + +### Contributors + +* Jan Palus +* valoq + + +## 1.22.0 + +### Added + +* Support for toplevel edge constraints. When the compositor indicates + the toplevel has edge constraints, foot will not allow the window to + be resized (via CSDs) in the constrained directions. +* Virtual modifiers (e.g. `Alt` instead of `Mod1`, `Super` instead of + `Mod4` etc) in key bindings are now recognized as being virtual, and + are automatically mapped to the corresponding real modifier. This + means you can use e.g. `Alt+b` instead of `Mod1+b`. +* `alpha-mode` option to `foot.ini`. Defaults to `default`. This + config changes how alpha is handled on background colours not set by + the terminal.(e.g. vim) ([#2026](2026)) + +[2026]: https://codeberg.org/dnkl/foot/issues/2026 + + +### Changed + +* UTF-8 error recovery now discards fewer bytes. +* Auto-calculated dimmed and brightened colors (e.g. when custom dim + colors has not configured) is now done by linear RGB interpolation, + rather than converting to HSL and adjusting the luminance + ([#2006][2006]). +* Virtual modifiers in keyboard events from the compositor are now + supported. This works around various issues seen when running foot + under mutter (GNOME) ([#2009][2009]): + - Some key combinations generating the wrong escape sequence in the + kitty keyboard protocol. + - some of foot's default shortcuts not working (mainly those using + `Mod1`) out of the box. +* Default URL regex changed to a much more strict variant + ([#2016][2016]). You can manually set the [old + one](https://codeberg.org/dnkl/foot/src/tag/1.21.0/foot.ini#L72), if + you prefer it over the new regex. +* A tiled window can now be resized in the corners (via CSDs), unless + the compositor has indicated the toplevel has edge constraints. + +[2006]: https://codeberg.org/dnkl/foot/issues/2006 +[2009]: https://codeberg.org/dnkl/foot/issues/2009 +[2016]: https://codeberg.org/dnkl/foot/issues/2016 + + +### Fixed + +* Regression: assertion in `url-mode.c` when activating a second URL + via `show-urls-persistent` ([#2000][2000]). +* Build failure (`srgb.h` not found) when doing a parallel build. +* Regression: reflowing (changing the window size) removing empty + lines ([#2011][2011]). +* `url/regex-copy` not handling double-width characters correctly + ([#2027][2027]). + +[2000]: https://codeberg.org/dnkl/foot/issues/2000 +[2011]: https://codeberg.org/dnkl/foot/issues/2011 +[2027]: https://codeberg.org/dnkl/foot/issues/2027 + + +### Contributors + +* Alex Xu (Hello71) +* datsudo +* Dominique Martinet +* Fazzi +* llyyr +* Łukasz Wojniłowicz +* Sam McCall + + +## 1.21.0 + +### Added + +* Support for the new Wayland protocol `xdg-system-bell-v1` protocol + (added in wayland-protocols 1.38), via the new config option + `bell.system=no|yes` (defaults to `yes`). +* Support for custom regex matching ([#1386][1386], + [#1872][1872]) +* Support for kitty's text-sizing protocol (`w`, width, only), OSC-66. +* `cursor.style` can now be set to `hollow` ([#1965][1965]). +* `search-bindings.delete-to-start` and + `search-bindings.delete-to-end` key bindings, defaulting to + `Control+u` and `Control+k` respectively ([#1972][1972]). +* Gamma-correct font rendering. Requires compositor support + (`wp_color_management_v1`, and specifically, the `ext_linear` + transfer function). Enabled by default when compositor support is + available. Can be explicitly enabled or disabled with + `gamma-correct-blending=no|yes`. + +[1386]: https://codeberg.org/dnkl/foot/issues/1386 +[1872]: https://codeberg.org/dnkl/foot/issues/1872 +[1965]: https://codeberg.org/dnkl/foot/issues/1965 +[1972]: https://codeberg.org/dnkl/foot/issues/1972 + + +### Changed + +* Do not try to set a zero width, or height, if the compositor sends a + _configure_ event with only one dimension being zero + ([#1925][1925]). +* Auto-detection of URLs (i.e. not OSC-8 based URLs) are now regex + based. +* Rename Tokyo Night Day theme to Tokyo Night Light and update colors. +* fcft >= 3.3.1 is now required. + - `tweak.scaling-filter` now supports more scaling-filters + - scaled bitmap fonts (when enabled in FontConfig) no longer have a + scaling-filter applied +* Linefeed:ing control characters (e.g. `\n`) no longer **clears** a + row's internal linebreak flag. This fixes an issue where + e.g. multi-line prompt input in fish is treated as separate lines, + rather than one logical, when selecting and copying it + ([#1487][1487]). +* wayland-protocols >= 1.41 is now required. + +[1925]: https://codeberg.org/dnkl/foot/issues/1925 +[1487]: https://codeberg.org/dnkl/foot/issues/1487 + + +### Removed + +* `url.uri-characters` and `url.protocols`. Both options have been + replaced by `url.regex`. +* `notify` option (has been deprecated since 1.18.0). +* `notify-focus-inhibit` option (has been deprecated since 1.18.0). + + +### Fixed + +* Kitty keyboard protocol: alternate key reporting failing to report + the alternate codepoint in some corner cases ([#1918][1918]). +* `foot` and `footclient` hanging, or terminating with `SIGABRT`, when + starting inside a directory whose total length is more than 1024 + characters. +* Regression: reflowing (resizing the window) a line that ends with a + double-width glyph that was pushed to the next line due to there + being only one cell left on current line, did not remove the virtual + space inserted at the end of the current line. +* Wrong key bindings executed when using alternative keyboard layouts + ([#1929][1929]). +* Foot not closing file descriptors for unrecognized or `no_keymap` + keymaps. +* Combining characters (including emojis consisting of multiple + codepoints) not being handled correctly when _insert mode_ is + enabled ([#1947][1947]). +* Reflow of the cursor (active + saved) when at the end of the line + with a pending wrap (LCF set) ([#1954][1954]). +* ~~Zero-width characters that also are grapheme breaks (e.g. U+200B, + ZERO WIDTH SPACE) being ignored (discarded and never stored in the + grid) ([#1960][1960]).~~ (reverted) +* `--server=` not working on FreeBSD ([#1956][1956]). +* Crash when resetting the terminal and an application had previously + set a custom app ID ([#1963][1963]) +* Grapheme clustering state not reset on cursor movements. +* Kitty keyboard protocol: no release events emitted for composed + keys. +* IME: the initial cursor position was reported as 0,0,0,0 + ([#1994][1994]). + +[1918]: https://codeberg.org/dnkl/foot/issues/1918 +[1929]: https://codeberg.org/dnkl/foot/issues/1929 +[1947]: https://codeberg.org/dnkl/foot/issues/1947 +[1954]: https://codeberg.org/dnkl/foot/issues/1954 +[1960]: https://codeberg.org/dnkl/foot/issues/1960 +[1956]: https://codeberg.org/dnkl/foot/issues/1956 +[1963]: https://codeberg.org/dnkl/foot/issues/1963 +[1994]: https://codeberg.org/dnkl/foot/issues/1994 + + +### Contributors + +* Adrian fxj9a +* Alexander Orzechowski +* Attila Fidan +* camel-cdr +* Craig Barnes +* Guillaume Outters +* Johannes Altmanninger +* Ludovico Gerardi +* sewn +* Thomas Bonnefille + + ## 1.20.2 ### Changed 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/README.md b/README.md index 3395aff0..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 @@ -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 @@ -689,8 +693,11 @@ Every now and then I post foot related updates on # Sponsoring/donations +* Liberapay: https://liberapay.com/dnkl * GitHub Sponsors: https://github.com/sponsors/dnkl +[![Donate using Liberapay](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/dnkl/donate) + # License diff --git a/box-drawing.c b/box-drawing.c index 1c613051..e69d9648 100644 --- a/box-drawing.c +++ b/box-drawing.c @@ -2,7 +2,6 @@ #include #include -#include #include #define LOG_MODULE "box-drawing" @@ -1462,14 +1461,12 @@ draw_box_drawings_light_arc(struct buf *buf, char32_t wc) */ for (double i = y_min*16; i <= y_max*16; i++) { errno = 0; - feclearexcept(FE_ALL_EXCEPT); double y = i / 16.; double x = circle_hemisphere * sqrt(c_r2 - (y - c_y) * (y - c_y)) + c_x; /* See math_error(7) */ - if (errno != 0 || - fetestexcept(FE_INVALID | FE_DIVBYZERO | FE_OVERFLOW | FE_UNDERFLOW)) + if (errno != 0) { continue; } diff --git a/char32.c b/char32.c index 827cef8d..be5bf229 100644 --- a/char32.c +++ b/char32.c @@ -34,7 +34,7 @@ _Static_assert( #if !defined(__STDC_UTF_32__) || !__STDC_UTF_32__ #error "char32_t does not use UTF-32" #endif -#if (!defined(__STDC_ISO_10646__) || !__STDC_ISO_10646__) && !defined(__FreeBSD__) +#if (!defined(__STDC_ISO_10646__) || !__STDC_ISO_10646__) && !defined(__FreeBSD__) && !defined(__OpenBSD__) #error "wchar_t does not use UTF-32" #endif @@ -53,6 +53,14 @@ UNITTEST xassert(c32cmp(U"b", U"a") > 0); } +UNITTEST +{ + xassert(c32ncmp(U"foo", U"foot", 3) == 0); + xassert(c32ncmp(U"foot", U"FOOT", 4) > 0); + xassert(c32ncmp(U"a", U"b", 1) < 0); + xassert(c32ncmp(U"bb", U"aa", 2) > 0); +} + UNITTEST { char32_t copy[16]; @@ -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); 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 8576531f..befd3ab0 100644 --- a/client.c +++ b/client.c @@ -1,12 +1,13 @@ -#include -#include -#include -#include -#include -#include -#include -#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include @@ -22,7 +23,6 @@ #include "foot-features.h" #include "macros.h" #include "util.h" -#include "version.h" #include "xmalloc.h" extern char **environ; @@ -34,13 +34,20 @@ struct string { typedef tll(struct string) string_list_t; static volatile sig_atomic_t aborted = 0; +static volatile sig_atomic_t sigusr = 0; static void -sig_handler(int signo) +sigint_handler(int signo) { aborted = 1; } +static void +sigusr_handler(int signo) +{ + sigusr = signo; +} + static ssize_t sendall(int sock, const void *_buf, size_t len) { @@ -62,20 +69,6 @@ sendall(int sock, const void *_buf, size_t len) return len; } -static const char * -version_and_features(void) -{ - static char buf[256]; - snprintf(buf, sizeof(buf), - "version: %s %cpgo %cime %cgraphemes %cassertions", - FOOT_VERSION, - feature_pgo() ? '+' : '-', - feature_ime() ? '+' : '-', - feature_graphemes() ? '+' : '-', - feature_assertions() ? '+' : '-'); - return buf; -} - static void print_usage(const char *prog_name) { @@ -84,6 +77,7 @@ print_usage(const char *prog_name) " -t,--term=TERM value to set the environment variable TERM to (" FOOT_DEFAULT_TERM ")\n" " -T,--title=TITLE initial window title (foot)\n" " -a,--app-id=ID window application ID (foot)\n" + " --toplevel-tag=TAG set a custom toplevel tag\n" " -w,--window-size-pixels=WIDTHxHEIGHT initial width and height, in pixels\n" " -W,--window-size-chars=WIDTHxHEIGHT initial width and height, in characters\n" " -m,--maximized start in maximized mode\n" @@ -145,6 +139,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) { @@ -159,6 +157,7 @@ main(int argc, char *const *argv) {"term", required_argument, NULL, 't'}, {"title", required_argument, NULL, 'T'}, {"app-id", required_argument, NULL, 'a'}, + {"toplevel-tag", required_argument, NULL, TOPLEVEL_TAG_OPTION}, {"window-size-pixels", required_argument, NULL, 'w'}, {"window-size-chars", required_argument, NULL, 'W'}, {"maximized", no_argument, NULL, 'm'}, @@ -228,6 +227,12 @@ main(int argc, char *const *argv) goto err; break; + case TOPLEVEL_TAG_OPTION: + snprintf(buf, sizeof(buf), "toplevel-tag=%s", optarg); + if (!push_string(&overrides, buf, &total_len)) + goto err; + break; + case 'L': if (!push_string(&overrides, "login-shell=yes", &total_len)) goto err; @@ -328,7 +333,7 @@ main(int argc, char *const *argv) break; case 'v': - printf("footclient %s\n", version_and_features()); + print_version_and_features("footclient "); ret = EXIT_SUCCESS; goto err; @@ -399,10 +404,10 @@ main(int argc, char *const *argv) const char *cwd = custom_cwd; if (cwd == NULL) { - errno = 0; size_t buf_len = 1024; do { _cwd = xrealloc(_cwd, buf_len); + errno = 0; if (getcwd(_cwd, buf_len) == NULL && errno != ERANGE) { LOG_ERRNO("failed to get current working directory"); goto err; @@ -522,15 +527,63 @@ main(int argc, char *const *argv) if (!send_string_list(fd, &envp)) goto err; - struct sigaction sa = {.sa_handler = &sig_handler}; - sigemptyset(&sa.sa_mask); - if (sigaction(SIGINT, &sa, NULL) < 0 || sigaction(SIGTERM, &sa, NULL) < 0) { + struct sigaction sa_int = {.sa_handler = &sigint_handler}; + struct sigaction sa_usr = {.sa_handler = &sigusr_handler}; + sigemptyset(&sa_int.sa_mask); + sigemptyset(&sa_usr.sa_mask); + + if (sigaction(SIGINT, &sa_int, NULL) < 0 || + sigaction(SIGTERM, &sa_int, NULL) < 0 || + sigaction(SIGUSR1, &sa_usr, NULL) < 0 || + sigaction(SIGUSR2, &sa_usr, NULL) < 0) + { LOG_ERRNO("failed to register signal handlers"); goto err; } int exit_code; - ssize_t rcvd = recv(fd, &exit_code, sizeof(exit_code), 0); + ssize_t rcvd = -1; + + while (true) { + rcvd = recv(fd, &exit_code, sizeof(exit_code), 0); + + const int got_sigusr = sigusr; + sigusr = 0; + + if (rcvd < 0 && errno == EINTR) { + if (aborted) + break; + else if (got_sigusr != 0) { + LOG_DBG("sending sigusr %d to server", got_sigusr); + + struct { + struct client_ipc_hdr hdr; + struct client_ipc_sigusr sigusr; + } ipc = { + .hdr = { + .ipc_code = FOOT_IPC_SIGUSR, + .size = sizeof(struct client_ipc_sigusr), + }, + .sigusr = { + .signo = got_sigusr, + }, + }; + + ssize_t count = send(fd, &ipc, sizeof(ipc), 0); + if (count < 0) { + LOG_ERRNO("failed to send SIGUSR IPC to server"); + goto err; + } else if ((size_t)count != sizeof(ipc)) { + LOG_ERR("failed to send SIGUSR IPC to server"); + goto err; + } + } + + continue; + } + + break; + } if (rcvd == -1 && errno == EINTR) xassert(aborted); diff --git a/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/composed.c b/composed.c index 442325ea..fc7dfa00 100644 --- a/composed.c +++ b/composed.c @@ -4,8 +4,54 @@ #include #include "debug.h" +#include "terminal.h" -struct composed * +uint32_t +composed_key_from_chars(const uint32_t chars[], size_t count) +{ + if (count == 0) + return 0; + + uint32_t key = chars[0]; + for (size_t i = 1; i < count; i++) + key = composed_key_from_key(key, chars[i]); + + return key; +} + +uint32_t +composed_key_from_key(uint32_t prev_key, uint32_t next_char) +{ + unsigned bits = 32 - __builtin_clz(CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO); + + /* Rotate old key 8 bits */ + uint32_t new_key = (prev_key << 8) | (prev_key >> (bits - 8)); + + /* xor with new char */ + new_key ^= next_char; + + /* Multiply with magic hash constant */ + new_key *= 2654435761ul; + + /* And mask, to ensure the new value is within range */ + new_key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO; + return new_key; +} + +UNITTEST +{ + const char32_t chars[] = U"abcdef"; + + uint32_t k1 = composed_key_from_key(chars[0], chars[1]); + uint32_t k2 = composed_key_from_chars(chars, 2); + xassert(k1 == k2); + + uint32_t k3 = composed_key_from_key(k2, chars[2]); + uint32_t k4 = composed_key_from_chars(chars, 3); + xassert(k3 == k4); +} + +const struct composed * composed_lookup(struct composed *root, uint32_t key) { struct composed *node = root; @@ -20,6 +66,41 @@ composed_lookup(struct composed *root, uint32_t key) return NULL; } +const struct composed * +composed_lookup_without_collision(struct composed *root, uint32_t *key, + const char32_t *prefix_text, size_t prefix_len, + char32_t wc, int forced_width) +{ + while (true) { + const struct composed *cc = composed_lookup(root, *key); + if (cc == NULL) + return NULL; + + bool match = cc->count == prefix_len + 1 && + cc->forced_width == forced_width && + cc->chars[prefix_len] == wc; + + if (match) { + for (size_t i = 0; i < prefix_len; i++) { + if (cc->chars[i] != prefix_text[i]) { + match = false; + break; + } + } + } + + if (match) + return cc; + + (*key)++; + *key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO; + + /* TODO: this will loop infinitely if the composed table is full */ + } + + return NULL; +} + void composed_insert(struct composed **root, struct composed *node) { diff --git a/composed.h b/composed.h index 17158407..18afb146 100644 --- a/composed.h +++ b/composed.h @@ -10,9 +10,16 @@ struct composed { uint32_t key; uint8_t count; uint8_t width; + uint8_t forced_width; }; -struct composed *composed_lookup(struct composed *root, uint32_t key); +uint32_t composed_key_from_chars(const uint32_t chars[], size_t count); +uint32_t composed_key_from_key(uint32_t prev_key, uint32_t next_char); + +const struct composed *composed_lookup(struct composed *root, uint32_t key); +const struct composed *composed_lookup_without_collision( + struct composed *root, uint32_t *key, + const char32_t *prefix, size_t prefix_len, char32_t wc, int forced_width); void composed_insert(struct composed **root, struct composed *node); void composed_free(struct composed *root); diff --git a/config.c b/config.c index 7f1ce055..12c594bc 100644 --- a/config.c +++ b/config.c @@ -140,6 +140,13 @@ 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", + [BIND_ACTION_THEME_SWITCH_1] = "color-theme-switch-1", + [BIND_ACTION_THEME_SWITCH_2] = "color-theme-switch-2", + [BIND_ACTION_THEME_SWITCH_DARK] = "color-theme-switch-dark", + [BIND_ACTION_THEME_SWITCH_LIGHT] = "color-theme-switch-light", + [BIND_ACTION_THEME_TOGGLE] = "color-theme-toggle", /* Mouse-specific actions */ [BIND_ACTION_SCROLLBACK_UP_MOUSE] = "scrollback-up-mouse", @@ -178,6 +185,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", @@ -207,6 +216,7 @@ static_assert(ALEN(url_binding_action_map) == BIND_ACTION_URL_COUNT, struct context { struct config *conf; const char *section; + const char *section_suffix; const char *key; const char *value; @@ -257,8 +267,9 @@ log_contextual(struct context *ctx, enum log_class log_class, char *formatted_msg = xvasprintf(fmt, va); va_end(va); - bool print_dot = ctx->key != NULL; - bool print_colon = ctx->value != NULL; + const bool print_dot = ctx->key != NULL; + const bool print_colon = ctx->value != NULL; + const bool print_section_suffix = ctx->section_suffix != NULL; if (!print_dot) ctx->key = ""; @@ -266,10 +277,15 @@ log_contextual(struct context *ctx, enum log_class log_class, if (!print_colon) ctx->value = ""; + if (!print_section_suffix) + ctx->section_suffix = ""; + log_and_notify( - ctx->conf, log_class, file, lineno, "%s:%d: [%s]%s%s%s%s: %s", - ctx->path, ctx->lineno, ctx->section, print_dot ? "." : "", - ctx->key, print_colon ? ": " : "", ctx->value, formatted_msg); + ctx->conf, log_class, file, lineno, "%s:%d: [%s%s%s]%s%s%s%s: %s", + ctx->path, ctx->lineno, ctx->section, + print_section_suffix ? ":" : "", ctx->section_suffix, + print_dot ? "." : "", ctx->key, print_colon ? ": " : "", + ctx->value, formatted_msg); free(formatted_msg); } @@ -420,14 +436,6 @@ done: return ret; } -static int -c32cmp_single(const void *_a, const void *_b) -{ - const char32_t *a = _a; - const char32_t *b = _b; - return *a - *b; -} - static bool str_has_prefix(const char *str, const char *prefix) { @@ -468,8 +476,12 @@ str_to_ulong(const char *s, int base, unsigned long *res) errno = 0; char *end = NULL; - *res = strtoul(s, &end, base); - return errno == 0 && *end == '\0'; + unsigned long v = strtoul(s, &end, base); + if (!(errno == 0 && *end == '\0')) + return false; + + *res = v; + return true; } static bool NOINLINE @@ -538,12 +550,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; } @@ -635,7 +648,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; } @@ -684,14 +696,18 @@ value_to_two_colors(struct context *ctx, goto out; } + uint32_t a, b; + ctx->value = first_as_str; - if (!value_to_color(ctx, first, allow_alpha)) + if (!value_to_color(ctx, &a, allow_alpha)) goto out; ctx->value = second_as_str; - if (!value_to_color(ctx, second, allow_alpha)) + if (!value_to_color(ctx, &b, allow_alpha)) goto out; + *first = a; + *second = b; ret = true; out: @@ -909,6 +925,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; @@ -926,22 +945,52 @@ parse_section_main(struct context *ctx) } else if (streq(key, "pad")) { - unsigned x, y; - char mode[16] = {0}; + unsigned x, y, left, top, right, bottom; + char mode[64] = {0}; + int ret = sscanf(value, "%ux%ux%ux%u %63s", &left, &top, &right, &bottom, mode); + enum center_when center = CENTER_NEVER; - int ret = sscanf(value, "%ux%u %15s", &x, &y, mode); - bool center = strcasecmp(mode, "center") == 0; - bool invalid_mode = !center && mode[0] != '\0'; + if (ret == 5) { + if (strcasecmp(mode, "center") == 0) + center = CENTER_ALWAYS; + else if (strcasecmp(mode, "center-when-fullscreen") == 0) + center = CENTER_FULLSCREEN; + else if (strcasecmp(mode, "center-when-maximized-and-fullscreen") == 0) + center = CENTER_MAXIMIZED_AND_FULLSCREEN; + else + center = CENTER_INVALID; + } else if (ret < 4) { + ret = sscanf(value, "%ux%u %63s", &x, &y, mode); + if (ret >= 2) { + left = right = x; + top = bottom = y; + if (ret == 3) { + if (strcasecmp(mode, "center") == 0) + center = CENTER_ALWAYS; + else if (strcasecmp(mode, "center-when-fullscreen") == 0) + center = CENTER_FULLSCREEN; + else if (strcasecmp(mode, "center-when-maximized-and-fullscreen") == 0) + center = CENTER_MAXIMIZED_AND_FULLSCREEN; + else + center = CENTER_INVALID; + } + } + } - if ((ret != 2 && ret != 3) || invalid_mode) { + if ((ret < 2 || ret > 5) || center == CENTER_INVALID) { LOG_CONTEXTUAL_ERR( - "invalid padding (must be in the form PAD_XxPAD_Y [center])"); + "invalid padding (must be in the form RIGHTxTOPxLEFTxBOTTOM or XxY " + "[center|" + "center-when-fullscreen|" + "center-when-maximized-and-fullscreen])"); return false; } - conf->pad_x = x; - conf->pad_y = y; - conf->center = center; + conf->pad_left = left; + conf->pad_top = top; + conf->pad_right = right; + conf->pad_bottom = bottom; + conf->center_when = (ret == 4 || ret == 2) ? CENTER_NEVER : center; return true; } @@ -1055,30 +1104,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"); @@ -1104,6 +1129,53 @@ parse_section_main(struct context *ctx) return true; } + else if (streq(key, "gamma-correct-blending")) + return value_to_bool(ctx, &conf->gamma_correct); + + else if (streq(key, "initial-color-theme")) { + _Static_assert( + sizeof(conf->initial_color_theme) == sizeof(int), + "enum is not 32-bit"); + + if (!value_to_enum(ctx, (const char*[]){ + "dark", "light", "1", "2", NULL}, + (int *)&conf->initial_color_theme)) + return false; + + if (streq(ctx->value, "1")) { + LOG_WARN("%s:%d: [main].initial-color-theme=1 deprecated, " + "use [main].initial-color-theme=dark instead", + ctx->path, ctx->lineno); + + user_notification_add( + &ctx->conf->notifications, + USER_NOTIFICATION_DEPRECATED, + xstrdup("[main].initial-color-theme=1: " + "use [main].initial-color-theme=dark instead")); + + conf->initial_color_theme = COLOR_THEME_DARK; + } + + else if (streq(ctx->value, "2")) { + LOG_WARN("%s:%d: [main].initial-color-theme=2 deprecated, " + "use [main].initial-color-theme=light instead", + ctx->path, ctx->lineno); + + user_notification_add( + &ctx->conf->notifications, + USER_NOTIFICATION_DEPRECATED, + xstrdup("[main].initial-color-theme=2: " + "use [main].initial-color-theme=light instead")); + + conf->initial_color_theme = COLOR_THEME_LIGHT; + } + + return true; + } + + else if (streq(key, "uppercase-regex-insert")) + return value_to_bool(ctx, &conf->uppercase_regex_insert); + else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; @@ -1139,6 +1211,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")) @@ -1223,7 +1297,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); @@ -1241,67 +1314,30 @@ 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); + else if (streq(key, "regex")) { + const char *regex = ctx->value; + regex_t preg; - conf->url.max_prot_len = 0; - conf->url.prot_count = 0; - conf->url.protocols = NULL; + int r = regcomp(&preg, regex, REG_EXTENDED); - char *copy = xstrdup(value); - - for (char *prot = strtok(copy, ","); - prot != NULL; - prot = strtok(NULL, ",")) - { - - /* Strip leading whitespace */ - while (isspace(prot[0])) - prot++; - - /* Strip trailing whitespace */ - size_t len = strlen(prot); - while (isspace(prot[len - 1])) - len--; - prot[len] = '\0'; - - size_t chars = mbsntoc32(NULL, prot, len, 0); - if (chars == (size_t)-1) { - ctx->value = prot; - LOG_CONTEXTUAL_ERRNO("invalid protocol"); - return false; - } - - conf->url.prot_count++; - conf->url.protocols = xrealloc( - conf->url.protocols, - conf->url.prot_count * sizeof(conf->url.protocols[0])); - - size_t idx = conf->url.prot_count - 1; - conf->url.protocols[idx] = xmalloc((chars + 1 + 3) * sizeof(char32_t)); - mbsntoc32(conf->url.protocols[idx], prot, len, chars + 1); - c32cpy(&conf->url.protocols[idx][chars], U"://"); - - chars += 3; /* Include the "://" */ - if (chars > conf->url.max_prot_len) - conf->url.max_prot_len = chars; + if (r != 0) { + char err_buf[128]; + regerror(r, &preg, err_buf, sizeof(err_buf)); + LOG_CONTEXTUAL_ERR("invalid regex: %s", err_buf); + return false; } - free(copy); - return true; - } - - else if (streq(key, "uri-characters")) { - if (!value_to_wchars(ctx, &conf->url.uri_characters)) + if (preg.re_nsub == 0) { + LOG_CONTEXTUAL_ERR("invalid regex: no marked subexpression(s)"); + regfree(&preg); return false; + } - qsort( - conf->url.uri_characters, - c32len(conf->url.uri_characters), - sizeof(conf->url.uri_characters[0]), - &c32cmp_single); + regfree(&conf->url.preg); + free(conf->url.regex); + + conf->url.regex = xstrdup(regex); + conf->url.preg = preg; return true; } @@ -1312,39 +1348,108 @@ parse_section_url(struct context *ctx) } static bool -parse_section_colors(struct context *ctx) +parse_section_regex(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; + const char *regex_name = + ctx->section_suffix != NULL ? ctx->section_suffix : ""; + + struct custom_regex *regex = NULL; + tll_foreach(conf->custom_regexes, it) { + if (streq(it->item.name, regex_name)) { + regex = &it->item; + break; + } + } + + if (streq(key, "regex")) { + const char *regex_string = ctx->value; + regex_t preg; + + int r = regcomp(&preg, regex_string, REG_EXTENDED); + + if (r != 0) { + char err_buf[128]; + regerror(r, &preg, err_buf, sizeof(err_buf)); + LOG_CONTEXTUAL_ERR("invalid regex: %s", err_buf); + return false; + } + + if (preg.re_nsub == 0) { + LOG_CONTEXTUAL_ERR("invalid regex: no marked subexpression(s)"); + regfree(&preg); + return false; + } + + if (regex == NULL) { + tll_push_back(conf->custom_regexes, + ((struct custom_regex){.name = xstrdup(regex_name)})); + regex = &tll_back(conf->custom_regexes); + } + + regfree(®ex->preg); + free(regex->regex); + + regex->regex = xstrdup(regex_string); + regex->preg = preg; + return true; + } + + else if (streq(key, "launch")) { + struct config_spawn_template launch = {NULL}; + if (!value_to_spawn_template(ctx, &launch)) + return false; + + if (regex == NULL) { + tll_push_back(conf->custom_regexes, + ((struct custom_regex){.name = xstrdup(regex_name)})); + regex = &tll_back(conf->custom_regexes); + } + + spawn_template_free(®ex->launch); + regex->launch = launch; + return true; + } + + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool NOINLINE +parse_color_theme(struct context *ctx, struct color_theme *theme) +{ + const char *key = ctx->key; + size_t key_len = strlen(key); uint8_t last_digit = (unsigned char)key[key_len - 1] - '0'; uint32_t *color = NULL; if (isdigit(key[0])) { unsigned long index; - if (!str_to_ulong(key, 0, &index) || - index >= ALEN(conf->colors.table)) - { + if (!str_to_ulong(key, 0, &index) || index >= ALEN(theme->table)) { LOG_CONTEXTUAL_ERR( "invalid color palette index: %s (not in range 0-%zu)", - key, ALEN(conf->colors.table)); + key, ALEN(theme->table)); return false; } - color = &conf->colors.table[index]; + color = &theme->table[index]; } else if (key_len == 8 && str_has_prefix(key, "regular") && last_digit < 8) - color = &conf->colors.table[last_digit]; + color = &theme->table[last_digit]; else if (key_len == 7 && str_has_prefix(key, "bright") && last_digit < 8) - color = &conf->colors.table[8 + last_digit]; + color = &theme->table[8 + last_digit]; else if (key_len == 4 && str_has_prefix(key, "dim") && last_digit < 8) { - if (!value_to_color(ctx, &conf->colors.dim[last_digit], false)) + if (!value_to_color(ctx, &theme->dim[last_digit], false)) return false; - conf->colors.use_custom.dim |= 1 << last_digit; + theme->use_custom.dim |= 1 << last_digit; return true; } @@ -1353,76 +1458,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, + &theme->cursor.text, + &theme->cursor.cursor, + false)) + { + return false; + } + + theme->use_custom.cursor = true; return true; } else if (streq(key, "urls")) { - if (!value_to_color(ctx, &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; } @@ -1436,7 +1555,7 @@ parse_section_colors(struct context *ctx) return false; } - conf->colors.alpha = alpha * 65535.; + theme->alpha = alpha * 65535.; return true; } @@ -1450,10 +1569,32 @@ parse_section_colors(struct context *ctx) return false; } - conf->colors.flash_alpha = alpha * 65535.; + theme->flash_alpha = alpha * 65535.; return true; } + else if (streq(key, "alpha-mode")) { + _Static_assert(sizeof(theme->alpha_mode) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"default", "matching", "all", NULL}, + (int *)&theme->alpha_mode); + } + + else if (streq(key, "dim-blend-towards")) { + _Static_assert(sizeof(theme->dim_blend_towards) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"black", "white", NULL}, + (int *)&theme->dim_blend_towards); + } + + else if (streq(key, "blur")) + return value_to_bool(ctx, &theme->blur); else { LOG_CONTEXTUAL_ERR("not valid option"); @@ -1468,6 +1609,46 @@ parse_section_colors(struct context *ctx) return true; } +static bool +parse_section_colors_dark(struct context *ctx) +{ + return parse_color_theme(ctx, &ctx->conf->colors_dark); +} + +static bool +parse_section_colors_light(struct context *ctx) +{ + return parse_color_theme(ctx, &ctx->conf->colors_light); +} + +static bool +parse_section_colors(struct context *ctx) +{ + LOG_WARN("%s:%d: [colors]: deprecated; use [colors-dark] instead", + ctx->path, ctx->lineno); + + user_notification_add( + &ctx->conf->notifications, + USER_NOTIFICATION_DEPRECATED, + xstrdup("[colors]: use [colors-dark] instead")); + + return parse_color_theme(ctx, &ctx->conf->colors_dark); +} + +static bool +parse_section_colors2(struct context *ctx) +{ + LOG_WARN("%s:%d: [colors2]: deprecated; use [colors-light] instead", + ctx->path, ctx->lineno); + + user_notification_add( + &ctx->conf->notifications, + USER_NOTIFICATION_DEPRECATED, + xstrdup("[colors2]: use [colors-light] instead")); + + return parse_color_theme(ctx, &ctx->conf->colors_light); +} + static bool parse_section_cursor(struct context *ctx) { @@ -1480,7 +1661,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); } @@ -1500,21 +1681,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")) { - if (!value_to_two_colors( - ctx, - &conf->cursor.color.text, - &conf->cursor.color.cursor, - false)) - { - return false; - } - - conf->cursor.color.text |= 1u << 31; - conf->cursor.color.cursor |= 1u << 31; - return true; - } - else if (streq(key, "beam-thickness")) return value_to_pt_or_px(ctx, &conf->cursor.beam_thickness); @@ -1652,6 +1818,7 @@ free_binding_aux(struct binding_aux *aux) case BINDING_AUX_NONE: break; case BINDING_AUX_PIPE: free_argv(&aux->pipe); break; case BINDING_AUX_TEXT: free(aux->text.data); break; + case BINDING_AUX_REGEX: free(aux->regex_name); break; } } @@ -1741,7 +1908,10 @@ binding_aux_equal(const struct binding_aux *a, case BINDING_AUX_TEXT: return a->text.len == b->text.len && - memcmp(a->text.data, b->text.data, a->text.len) == 0; + memcmp(a->text.data, b->text.data, a->text.len) == 0; + + case BINDING_AUX_REGEX: + return streq(a->regex_name, b->regex_name); } BUG("invalid AUX type: %d", a->type); @@ -1926,7 +2096,8 @@ value_to_key_combos(struct context *ctx, int action, if (idx == 0) { LOG_CONTEXTUAL_ERR( "empty binding not allowed (set to 'none' to unmap)"); - goto err; + free(copy); + return false; } remove_from_key_bindings_list(bindings, action, aux); @@ -1944,10 +2115,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; } @@ -2016,19 +2185,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; } @@ -2087,21 +2260,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; @@ -2109,6 +2301,56 @@ 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 (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; @@ -2118,7 +2360,6 @@ parse_key_binding_section(struct context *ctx, } LOG_CONTEXTUAL_ERR("not a valid action: %s", ctx->key); - free_binding_aux(&aux); return false; } @@ -2316,7 +2557,7 @@ resolve_key_binding_collisions(struct config *conf, const char *section_name, } if (collision_type != COLLISION_NONE) { - char *modifier_names = modifiers_to_str(mods1); + char *modifier_names = modifiers_to_str(mods1, false); char sym_name[64]; switch (type){ @@ -2358,7 +2599,7 @@ resolve_key_binding_collisions(struct config *conf, const char *section_name, case COLLISION_OVERRIDE: { char *override_names = modifiers_to_str( - &conf->mouse.selection_override_modifiers); + &conf->mouse.selection_override_modifiers, true); if (override_names[0] != '\0') override_names[strlen(override_names) - 1] = '\0'; @@ -2564,8 +2805,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, }; @@ -2673,9 +2921,35 @@ 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); + else if (streq(key, "surface-bit-depth")) { + _Static_assert(sizeof(conf->tweak.surface_bit_depth) == sizeof(int), + "enum is not 32-bit"); + +#if defined(HAVE_PIXMAN_RGBA_16) + return value_to_enum( + ctx, + (const char *[]){"auto", "8-bit", "10-bit", "16-bit", NULL}, + (int *)&conf->tweak.surface_bit_depth); +#else + return value_to_enum( + ctx, + (const char *[]){"auto", "8-bit", "10-bit", NULL}, + (int *)&conf->tweak.surface_bit_depth); +#endif + } + + else if (streq(key, "min-stride-alignment")) + return value_to_uint32(ctx, 10, &conf->tweak.min_stride_alignment); + + else if (streq(key, "pre-apply-damage")) + return value_to_bool(ctx, &conf->tweak.preapply_damage); + else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; @@ -2697,7 +2971,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; @@ -2766,7 +3040,9 @@ enum section { SECTION_DESKTOP_NOTIFICATIONS, SECTION_SCROLLBACK, SECTION_URL, - SECTION_COLORS, + SECTION_REGEX, + SECTION_COLORS_DARK, + SECTION_COLORS_LIGHT, SECTION_CURSOR, SECTION_MOUSE, SECTION_CSD, @@ -2778,6 +3054,11 @@ enum section { SECTION_ENVIRONMENT, SECTION_TWEAK, SECTION_TOUCH, + + /* Deprecated */ + SECTION_COLORS, + SECTION_COLORS2, + SECTION_COUNT, }; @@ -2787,6 +3068,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"}, @@ -2794,7 +3076,9 @@ 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_COLORS] = {&parse_section_colors, "colors"}, + [SECTION_REGEX] = {&parse_section_regex, "regex", true}, + [SECTION_COLORS_DARK] = {&parse_section_colors_dark, "colors-dark"}, + [SECTION_COLORS_LIGHT] = {&parse_section_colors_light, "colors-light"}, [SECTION_CURSOR] = {&parse_section_cursor, "cursor"}, [SECTION_MOUSE] = {&parse_section_mouse, "mouse"}, [SECTION_CSD] = {&parse_section_csd, "csd"}, @@ -2806,16 +3090,38 @@ 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"); 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; } @@ -2839,10 +3145,12 @@ parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_ar } char *section_name = xstrdup("main"); + char *section_suffix = NULL; struct context context = { .conf = conf, .section = section_name, + .section_suffix = section_suffix, .path = path, .lineno = 0, .errors_are_fatal = errors_are_fatal, @@ -2923,7 +3231,8 @@ parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_ar error_or_continue(); } - section = str_to_section(key_value); + char *maybe_section_suffix; + section = str_to_section(key_value, &maybe_section_suffix); if (section == SECTION_COUNT) { context.section = key_value; LOG_CONTEXTUAL_ERR("invalid section name: %s", key_value); @@ -2932,8 +3241,11 @@ parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_ar } free(section_name); + free(section_suffix); section_name = xstrdup(key_value); + section_suffix = maybe_section_suffix != NULL ? xstrdup(maybe_section_suffix) : NULL; context.section = section_name; + context.section_suffix = section_suffix; /* Process next line */ continue; @@ -2973,6 +3285,7 @@ parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_ar done: free(section_name); + free(section_suffix); free(_line); return ret; } @@ -3005,7 +3318,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}}}, @@ -3036,11 +3351,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}}}, @@ -3061,8 +3379,9 @@ 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_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}}}, @@ -3143,21 +3462,27 @@ 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"), .app_id = (as_server ? xstrdup("footclient") : xstrdup("foot")), + .toplevel_tag = xstrdup(""), .word_delimiters = xc32dup(U",│`|:\"'()[]{}<>"), .size = { .type = CONF_SIZE_PX, .width = 700, .height = 500, }, - .pad_x = 0, - .pad_y = 0, + .pad_left = 0, + .pad_top = 0, + .pad_right = 0, + .pad_bottom = 0, + .center_when = CENTER_MAXIMIZED_AND_FULLSCREEN, .resize_by_cells = true, .resize_keep_grid = true, .resize_delay_ms = 100, + .dim = { .amount = 1.5 }, .bold_in_bright = { .enabled = false, .palette_based = false, @@ -3175,6 +3500,8 @@ 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 = false, + .uppercase_regex_insert = true, .security = { .osc52 = OSC52_ENABLED, }, @@ -3182,6 +3509,7 @@ config_load(struct config *conf, const char *conf_path, .urgent = false, .notify = false, .flash = false, + .system_bell = true, .command = { .argv = {.args = NULL}, }, @@ -3189,9 +3517,9 @@ config_load(struct config *conf, const char *conf_path, }, .url = { .label_letters = xc32dup(U"sadfjklewcmpgh"), - .uri_characters = xc32dup(U"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.,~:;/?#@!$&%*+=\"'()[]"), .osc8_underline = OSC8_UNDERLINE_URL_MODE, }, + .custom_regexes = tll_init(), .can_shape_grapheme = fcft_caps & FCFT_CAPABILITY_GRAPHEME_SHAPING, .scrollback = { .lines = 1000, @@ -3202,22 +3530,28 @@ config_load(struct config *conf, const char *conf_path, }, .multiplier = 3., }, - .colors = { + .colors_dark = { .fg = default_foreground, .bg = default_background, .flash = 0x7f7f00, .flash_alpha = 0x7fff, .alpha = 0xffff, + .alpha_mode = ALPHA_MODE_DEFAULT, + .dim_blend_towards = DIM_BLEND_TOWARDS_BLACK, .selection_fg = 0x80000000, /* Use default bg */ .selection_bg = 0x80000000, /* Use default fg */ + .cursor = { + .text = 0, + .cursor = 0, + }, .use_custom = { - .selection = false, .jump_label = false, .scrollback_indicator = false, .url = false, }, + .blur = false, }, - + .initial_color_theme = COLOR_THEME_DARK, .cursor = { .style = CURSOR_BLOCK, .unfocused_style = CURSOR_UNFOCUSED_HOLLOW, @@ -3225,10 +3559,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}, }, @@ -3282,6 +3612,9 @@ config_load(struct config *conf, const char *conf_path, .box_drawing_solid_shades = true, .font_monospace_warn = true, .sixel = true, + .surface_bit_depth = SHM_BITS_AUTO, + .min_stride_alignment = 256, + .preapply_damage = true, }, .touch = { @@ -3298,8 +3631,11 @@ 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->colors_dark.table, default_color_table, sizeof(default_color_table)); + memcpy(conf->colors_dark.sixel, default_sixel_colors, sizeof(default_sixel_colors)); + memcpy(&conf->colors_light, &conf->colors_dark, sizeof(conf->colors_dark)); + conf->colors_light.dim_blend_towards = DIM_BLEND_TOWARDS_WHITE; + parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers); tokenize_cmdline( @@ -3308,35 +3644,54 @@ 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; + { + const char *url_regex_string = + "(" + "(" + "(https?://|mailto:|ftp://|file:|ssh:|ssh://|git://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news:)" + "|" + "www\\." + ")" + "(" + /* Safe + reserved + some unsafe characters parenthesis and double quotes omitted (we only allow them when balanced) */ + "[0-9a-zA-Z:/?#@!$&*+,;=.~_%^\\-]+" + "|" + /* Balanced "(...)". Content is same as above, plus all _other_ characters we require to be balanced */ + "\\([]\\[\"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\\)" + "|" + /* Balanced "[...]". Content is same as above, plus all _other_ characters we require to be balanced */ + "\\[[\\(\\)\"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\\]" + "|" + /* Balanced '"..."'. Content is same as above, plus all _other_ characters we require to be balanced */ + "\"[]\\[\\(\\)0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\"" + "|" + /* Balanced "'...'". Content is same as above, plus all _other_ characters we require to be balanced */ + "'[]\\[\\(\\)0-9a-zA-Z:/?#@!$&*+,;=.~_%^\\-]*'" + ")+" + "(" + /* Same as above, except :?!,;. are excluded */ + "[0-9a-zA-Z/#@$&*+=~_%^\\-]" + "|" + /* Balanced "(...)". Content is same as above, plus all _other_ characters we require to be balanced */ + "\\([]\\[\"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\\)" + "|" + /* Balanced "[...]". Content is same as above, plus all _other_ characters we require to be balanced */ + "\\[[\\(\\)\"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\\]" + "|" + /* Balanced '"..."'. Content is same as above, plus all _other_ characters we require to be balanced */ + "\"[]\\[\\(\\)0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\"" + "|" + /* Balanced "'...'". Content is same as above, plus all _other_ characters we require to be balanced */ + "'[]\\[\\(\\)0-9a-zA-Z:/?#@!$&*+,;=.~_%^\\-]*'" + ")" + ")"; - for (size_t i = 0; i < ALEN(url_protocols); i++) { - size_t len = c32len(url_protocols[i]); - if (len > conf->url.max_prot_len) - conf->url.max_prot_len = len; - conf->url.protocols[i] = xc32dup(url_protocols[i]); + int r = regcomp(&conf->url.preg, url_regex_string, REG_EXTENDED); + xassert(r == 0); + conf->url.regex = xstrdup(url_regex_string); + xassert(conf->url.preg.re_nsub >= 1); } - qsort( - conf->url.uri_characters, - c32len(conf->url.uri_characters), - sizeof(conf->url.uri_characters[0]), - &c32cmp_single); - tll_foreach(*initial_user_notifications, it) { tll_push_back(conf->notifications, it->item); tll_remove(*initial_user_notifications, it); @@ -3384,10 +3739,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)) { @@ -3423,6 +3774,8 @@ bool config_override_apply(struct config *conf, config_override_t *overrides, bool errors_are_fatal) { + char *section_name = NULL; + struct context context = { .conf = conf, .path = "override", @@ -3434,8 +3787,7 @@ config_override_apply(struct config *conf, config_override_t *overrides, tll_foreach(*overrides, it) { context.lineno++; - if (!parse_key_value( - it->item, &context.section, &context.key, &context.value)) + if (!parse_key_value(it->item, §ion_name, &context.key, &context.value)) { LOG_CONTEXTUAL_ERR("syntax error: key/value pair has no %s", context.key == NULL ? "key" : "value"); @@ -3444,20 +3796,26 @@ config_override_apply(struct config *conf, config_override_t *overrides, continue; } - if (context.section[0] == '\0') { + if (section_name[0] == '\0') { LOG_CONTEXTUAL_ERR("empty section name"); if (errors_are_fatal) return false; continue; } - enum section section = str_to_section(context.section); + char *maybe_section_suffix = NULL; + enum section section = str_to_section(section_name, &maybe_section_suffix); + + context.section = section_name; + context.section_suffix = maybe_section_suffix; + if (section == SECTION_COUNT) { - LOG_CONTEXTUAL_ERR("invalid section name: %s", context.section); + LOG_CONTEXTUAL_ERR("invalid section name: %s", section_name); if (errors_are_fatal) return false; continue; } + parser_fun_t section_parser = section_info[section].fun; xassert(section_parser != NULL); @@ -3493,6 +3851,7 @@ key_binding_list_clone(struct config_key_binding_list *dst, struct argv *last_master_argv = NULL; uint8_t *last_master_text_data = NULL; size_t last_master_text_len = 0; + char *last_master_regex_name = NULL; dst->count = src->count; dst->arr = xmalloc(src->count * sizeof(dst->arr[0])); @@ -3540,6 +3899,16 @@ key_binding_list_clone(struct config_key_binding_list *dst, } last_master_argv = NULL; break; + + case BINDING_AUX_REGEX: + if (old->aux.master_copy) { + new->aux.regex_name = xstrdup(old->aux.regex_name); + last_master_regex_name = new->aux.regex_name; + } else { + xassert(last_master_regex_name != NULL); + new->aux.regex_name = last_master_regex_name; + } + break; } } } @@ -3550,10 +3919,12 @@ config_clone(const struct config *old) struct config *conf = xmalloc(sizeof(*conf)); *conf = *old; + conf->conf_path = (old->conf_path ? xstrdup(old->conf_path) : NULL); conf->term = xstrdup(old->term); conf->shell = xstrdup(old->shell); conf->title = xstrdup(old->title); conf->app_id = xstrdup(old->app_id); + conf->toplevel_tag = xstrdup(old->toplevel_tag); conf->word_delimiters = xc32dup(old->word_delimiters); conf->scrollback.indicator.text = xc32dup(old->scrollback.indicator.text); conf->server_socket_path = xstrdup(old->server_socket_path); @@ -3570,12 +3941,23 @@ 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]); + 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); @@ -3638,10 +4020,12 @@ UNITTEST void config_free(struct config *conf) { + free(conf->conf_path); free(conf->term); free(conf->shell); free(conf->title); free(conf->app_id); + free(conf->toplevel_tag); free(conf->word_delimiters); spawn_template_free(&conf->bell.command); free(conf->scrollback.indicator.text); @@ -3656,10 +4040,17 @@ config_free(struct config *conf) free(conf->url.label_letters); spawn_template_free(&conf->url.launch); - for (size_t i = 0; i < conf->url.prot_count; i++) - free(conf->url.protocols[i]); - free(conf->url.protocols); - free(conf->url.uri_characters); + regfree(&conf->url.preg); + free(conf->url.regex); + + tll_foreach(conf->custom_regexes, it) { + struct custom_regex *regex = &it->item; + free(regex->name); + free(regex->regex); + regfree(®ex->preg); + spawn_template_free(®ex->launch); + tll_remove(conf->custom_regexes, it); + } free_key_binding_list(&conf->bindings.key); free_key_binding_list(&conf->bindings.search); @@ -3702,9 +4093,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 { @@ -3713,6 +4105,7 @@ config_font_parse(const char *pattern, struct config_font *font) } FcPatternDestroy(pat_copy); + FcConfigDestroy(fc_conf); if (have_pt_size != FcResultMatch && have_px_size != FcResultMatch) pt_size = 8.0; @@ -3726,6 +4119,11 @@ config_font_parse(const char *pattern, struct config_font *font) LOG_DBG("%s: pt-size=%.2f, px-size=%d", stripped_pattern, pt_size, px_size); + if (stripped_pattern == NULL) { + LOG_ERR("failed to convert font pattern to string"); + return false; + } + *font = (struct config_font){ .pattern = stripped_pattern, .pt_size = pt_size, diff --git a/config.h b/config.h index d7192970..a3522f44 100644 --- a/config.h +++ b/config.h @@ -1,7 +1,8 @@ #pragma once -#include +#include #include +#include #include #include @@ -27,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, @@ -60,6 +61,7 @@ enum binding_aux_type { BINDING_AUX_NONE, BINDING_AUX_PIPE, BINDING_AUX_TEXT, + BINDING_AUX_REGEX, }; struct binding_aux { @@ -73,6 +75,8 @@ struct binding_aux { uint8_t *data; size_t len; } text; + + char *regex_name; }; }; @@ -120,11 +124,107 @@ struct env_var { }; typedef tll(struct env_var) env_var_list_t; +struct custom_regex { + char *name; + char *regex; + regex_t preg; + struct config_spawn_template launch; +}; + +struct color_theme { + uint32_t fg; + uint32_t bg; + uint32_t flash; + uint32_t flash_alpha; + uint32_t table[256]; + uint16_t alpha; + uint32_t selection_fg; + uint32_t selection_bg; + uint32_t url; + + uint32_t dim[8]; + uint32_t sixel[16]; + + enum { + DIM_BLEND_TOWARDS_BLACK, + DIM_BLEND_TOWARDS_WHITE, + } dim_blend_towards; + + enum { + ALPHA_MODE_DEFAULT, + ALPHA_MODE_MATCHING, + ALPHA_MODE_ALL + } alpha_mode; + + struct { + uint32_t text; + uint32_t cursor; + } cursor; + + struct { + uint32_t fg; + uint32_t bg; + } jump_label; + + struct { + uint32_t fg; + uint32_t bg; + } scrollback_indicator; + + struct { + struct { + uint32_t fg; + uint32_t bg; + } no_match; + + struct { + uint32_t fg; + uint32_t bg; + } match; + } search_box; + + struct { + bool cursor:1; + bool jump_label:1; + bool scrollback_indicator:1; + bool url:1; + bool search_box_no_match:1; + bool search_box_match:1; + uint8_t dim; + } use_custom; + + bool blur; +}; + +enum which_color_theme { + COLOR_THEME_DARK, + COLOR_THEME_LIGHT, + COLOR_THEME_1, /* Deprecated */ + COLOR_THEME_2, /* Deprecated */ +}; + +enum shm_bit_depth { + SHM_BITS_AUTO, + SHM_BITS_8, + SHM_BITS_10, + SHM_BITS_16, +}; + +enum center_when { + CENTER_INVALID, + CENTER_NEVER, + CENTER_FULLSCREEN, + CENTER_MAXIMIZED_AND_FULLSCREEN, + CENTER_ALWAYS, +}; + struct config { + char *conf_path; char *term; char *shell; char *title; char *app_id; + char *toplevel_tag; char32_t *word_delimiters; bool login_shell; bool locked_title; @@ -135,15 +235,21 @@ struct config { uint32_t height; } size; - unsigned pad_x; - unsigned pad_y; - bool center; + unsigned pad_left; + unsigned pad_top; + unsigned pad_right; + unsigned pad_bottom; + enum center_when center_when; bool resize_by_cells; bool resize_keep_grid; uint16_t resize_delay_ms; + struct { + float amount; + } dim; + struct { bool enabled; bool palette_based; @@ -153,6 +259,8 @@ struct config { enum { STARTUP_WINDOWED, STARTUP_MAXIMIZED, STARTUP_FULLSCREEN } startup_mode; bool dpi_aware; + bool gamma_correct; + bool uppercase_regex_insert; struct config_font_list fonts[4]; struct font_size_adjustment font_size_adjustment; @@ -186,6 +294,7 @@ struct config { bool urgent; bool notify; bool flash; + bool system_bell; struct config_spawn_template command; bool command_focused; } bell; @@ -219,58 +328,15 @@ struct config { OSC8_UNDERLINE_ALWAYS, } osc8_underline; - char32_t **protocols; - char32_t *uri_characters; - size_t prot_count; - size_t max_prot_len; + char *regex; + regex_t preg; } url; - struct { - uint32_t fg; - uint32_t bg; - uint32_t flash; - uint32_t flash_alpha; - uint32_t table[256]; - uint16_t alpha; - uint32_t selection_fg; - uint32_t selection_bg; - uint32_t url; + tll(struct custom_regex) custom_regexes; - uint32_t dim[8]; - uint32_t sixel[16]; - - struct { - uint32_t fg; - uint32_t bg; - } jump_label; - - struct { - uint32_t fg; - uint32_t bg; - } scrollback_indicator; - - struct { - struct { - uint32_t fg; - uint32_t bg; - } no_match; - - struct { - uint32_t fg; - uint32_t bg; - } match; - } search_box; - - struct { - bool selection:1; - bool jump_label:1; - bool scrollback_indicator:1; - bool url:1; - bool search_box_no_match:1; - bool search_box_match:1; - uint8_t dim; - } use_custom; - } colors; + struct color_theme colors_dark; + struct color_theme colors_light; + enum which_color_theme initial_color_theme; struct { enum cursor_style style; @@ -279,10 +345,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; @@ -385,6 +447,9 @@ struct config { bool box_drawing_solid_shades; bool font_monospace_warn; bool sixel; + enum shm_bit_depth surface_bit_depth; + uint32_t min_stride_alignment; + bool preapply_damage; } tweak; struct { diff --git a/csi.c b/csi.c index 61cbdced..87af215e 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; @@ -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: @@ -558,7 +560,13 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) break; case 2027: +#if defined(FOOT_GRAPHEME_CLUSTERING) term->grapheme_shaping = enable; +#endif + break; + + case 2031: + term->report_theme_changes = enable; break; case 2048: @@ -655,6 +663,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)); @@ -700,6 +709,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; @@ -744,6 +754,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; @@ -790,10 +801,20 @@ csi_dispatch(struct terminal *term, uint8_t final) int count = vt_param_get(term, 0, 1); LOG_DBG("REP: '%lc' %d times", (wint_t)term->vt.last_printed, count); - const int width = c32width(term->vt.last_printed); + int width; + + if (term->vt.last_printed >= CELL_COMB_CHARS_LO) { + const struct composed *comp = composed_lookup( + term->composed, term->vt.last_printed - CELL_COMB_CHARS_LO); + + xassert(comp != NULL); + width = comp->forced_width > 0 ? comp->forced_width : comp->width; + } else + width = c32width(term->vt.last_printed); + if (width > 0) { for (int i = 0; i < count; i++) - term_print(term, term->vt.last_printed, width); + term_print(term, term->vt.last_printed, width, false); } } break; @@ -831,6 +852,7 @@ csi_dispatch(struct terminal *term, uint8_t final) * - 22 ANSI color, e.g., VT525. * - 28 Rectangular editing. * - 29 ANSI text locator (i.e., DEC Locator mode). + * - 52 Clipboard access * * Note: we report ourselves as a VT220, mainly to be able * to pass parameters, to indicate we support sixel, and @@ -841,13 +863,15 @@ csi_dispatch(struct terminal *term, uint8_t final) * * Note: tertiary DA responds with "FOOT". */ - if (term->conf->tweak.sixel) { - static const char reply[] = "\033[?62;4;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; } @@ -1537,6 +1561,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_THEME_DARK ? 1 : 2); + + term_to_slave(term, reply, chars); + break; + } + } + break; + } + case 'p': { /* * Request status of ECMA-48/"ANSI" private mode (DECRQM @@ -1594,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. @@ -1756,7 +1806,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/cursor-shape.c b/cursor-shape.c index bbf75ab8..c195a554 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; @@ -72,7 +72,7 @@ cursor_string_to_server_shape(const char *xcursor) [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"}, @@ -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/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; diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index f8eb1222..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 @@ -729,7 +739,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). diff --git a/doc/foot.1.scd b/doc/foot.1.scd index fad44f19..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*). @@ -67,6 +70,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. @@ -252,9 +260,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 @@ -301,6 +306,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* @@ -683,6 +694,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 the dark color theme (*[colors-dark]*). +- SIGUSR2: switch to the light color theme (*[colors-light]*). + +Note: you can send SIGUSR1/SIGUSR2 to a *foot --server* process too, +in which case all client instances will switch theme. Furthermore, all +future client instances will also use the selected theme. + +You can also send SIGUSR1/SIGUSR2 to a footclient instance, see +*footclient*(1) for details. + + # BUGS Please report bugs to https://codeberg.org/dnkl/foot/issues diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index ba59050e..a1ee326f 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -24,12 +24,12 @@ commented out will usually be installed to */etc/xdg/foot/foot.ini*. Options are set using KEY=VALUE pairs: - *\[colors\]*++ + *\[colors-dark\]*++ *background=000000*++ *foreground=ffffff* Empty values (*KEY=*) are not supported. String options do allow the -empty string to be set, but it must be quoted: *KEY=""*) +empty string to be set, but it must be quoted: *KEY=""* # SECTION: main @@ -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. @@ -89,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: ``` @@ -122,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". @@ -154,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. @@ -191,9 +198,49 @@ 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. + + FreeType can limit the effect of the latter, with a technique + called stem darkening. It is only available for CFF fonts + (OpenType, .otf) and disabled by default (in FreeType). You can + enable it by setting the environment variable + *FREETYPE_PROPERTIES="cff:no-stem-darkening=0"* before starting + foot. + + Also be aware that many fonts have been developed on systems that + do not do gamma-correct blending, and may therefore look thicker + than intended when rendered with gamma-correct blending, since the + font designer set the font weight based on incorrect rendering. + + In order to represent colors faithfully, higher precision image + buffers are required. By default, foot will use either 16-bit, or + 10-bit color channels, depending on availability, when + gamma-correct blending is enabled. However, the high precision + buffers are slow; if you want to use gamma-correct blending, but + prefer speed (throughput and input latency) over accurate colors, + you can force 8-bit color channels by setting + *tweak.surface-bit-depth=8-bit*. + + Default: _no_. + +*uppercase-regex-insert* + Boolean. When enabled, inputting an uppercase hint character in + *show-urls-copy* or *regex-copy* mode will insert the selected + text into the prompt in addition to copying it to the clipboard. + + Default: _yes_ + *box-drawings-uses-font-glyphs* Boolean. When disabled, foot generates box/line drawing characters - itself. 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 @@ -213,6 +260,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_. @@ -233,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. @@ -248,18 +296,40 @@ 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 - This will add _at least_ X pixels on both the left and right - sides, and Y pixels on the top and bottom sides. The grid content - will be anchored in the top left corner. I.e. if the window - manager forces an odd window size on foot, the additional pixels - will be added to the right and bottom sides. + ``` + _XxY_ [center | center-when-fullscreen | center-when-maximized-and-fullscreen] + ``` + or + ``` + RIGHTxTOPxLEFTxBOTTOM [center | center-when-fullscreen | center-when-maximized-and-fullscreen] + ``` - To instead center the grid content, append *center* (e.g. *pad=5x5 - center*). + - `_XxY_` adds _at least_: + - X pixels on the left and right sides. + - Y pixels on the top and bottom sides. - Default: _0x0_. + - `LEFTxTOPxRIGHTxBOTTOM` adds **at least**: + - LEFT pixels to the left + - TOP pixels to the top + - RIGHT pixels to the right + - BOTTOM pixels to the bottom + + When no centering is specified, the grid content is anchored to + the top left corner. I.e. if the window manager forces an odd + window size on foot, the additional pixels will be added to the + right and bottom sides. + + If *center* is specified, the grid content is instead + centered. This may cause "jumpiness" when resizing the window. + + With *center-when-fullscreen* and + *center-when-maximized-and-fullscreen*, the grid is anchored to + the top left corner, unless the window is maximized, or + fullscreened. + + Default: _0x0_ center-when-maximized-and-fullscreen. *resize-delay-ms* @@ -278,7 +348,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. @@ -293,7 +363,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. @@ -311,6 +381,19 @@ empty string to be set, but it must be quoted: *KEY=""*) Default: _yes_ +*initial-color-theme* + Selects which color theme to use, *dark*, or *light*. + + *dark* uses the colors defined in the *colors-dark* section, while + *light* uses the colors from the *colors-light* section. + + Use the *color-theme-switch-dark*, *color-theme-switch-light* and + *color-theme-toggle* key bindings to switch between the two themes + at runtime, or send SIGUSR1/SIGUSR2 to the foot process (see + *foot*(1) for details). + + Default: _dark_ + *initial-window-size-pixels* Initial window width and height in _pixels_ (subject to output scaling), in the form _WIDTHxHEIGHT_. The height _includes_ the @@ -327,7 +410,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 @@ -357,10 +440,15 @@ 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 - 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 @@ -383,6 +471,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. @@ -445,10 +537,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 +558,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 @@ -500,7 +599,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; @@ -612,7 +711,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++ @@ -628,7 +727,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++ @@ -672,7 +771,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 @@ -748,6 +847,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}_. @@ -775,19 +877,48 @@ 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. +*regex* + Regular expression to use when auto-detecting URLs. The format is + "POSIX-Extended Regular Expressions". Note that the first marked + subexpression is used as the URL. In other words, if you want the + whole regex match to be used as an URL, surround all of it with + parenthesis: *(regex-pattern)*. - Default: - _abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-\_.,~:;/?#@!$&%\*+="'()[]_ + Default: _(((https?://|mailto:|ftp://|file:|ssh:|ssh://|git://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news:)|www\.)([0-9a-zA-Z:/?#@!$&\*+,;=.~\_%^\-]+|\([]\["0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*\)|\[[\(\)"0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*\]|"[]\[\(\)0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*"|'[]\[\(\)0-9a-zA-Z:/?#@!$&\*+,;=.~\_%^\-]\*')+([0-9a-zA-Z/#@$&\*+=~\_%^\-]|\([]\["0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*\)|\[[\(\)"0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*\]|"[]\[\(\)0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*"|'[]\[\(\)0-9a-zA-Z:/?#@!$&\*+,;=.~\_%^\-]\*'))_ + +# SECTION: regex + +Similar to the 'url' mode, but with custom defined regular expressions +(and launchers). + +To use a custom defined regular expression, you also need to add a key +binding for it. This is done in the *key-binding* section, see below +for details. For example, a regex to detect hash digests (e.g. git +commit hashes) could look like: + +``` +[regex:hashes] +regex=([a-fA-F0-9]{7,128}) +launch=path-to-script-or-application ${match} + +[key-bindings] +regex-launch=[hashes] Control+Shift+q +regex-copy=[hashes] Control+Mod1+Shift+q +``` + +*launch* + Command to execute when "launching" a regex match. _${match}_ will + be replaced with the actual URL. Default: _not set_. + +*regex* + Regular expression to use when matching text. The format is + "POSIX-Extended Regular Expressions". Note that the first marked + subexpression is used as the match. In other words, if you want + the whole regex match to be used, surround all of it with + parenthesis: *(regex-pattern)*. + + Default: _not set_. + # SECTION: cursor @@ -796,8 +927,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 @@ -813,19 +944,10 @@ 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_. -*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 @@ -876,16 +998,34 @@ 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. +*colors-dark* is intended to define a dark color theme, and +*colors-light* is intended to define a light color theme. You can +switch between them using the *color-theme-switch-dark*, +*color-theme-switch-light* and *color-theme-toggle* key bindings, or +by sending SIGUSR1/SIGUSR2 to the foot process. + +The default theme used is *colors-dark*, unless +*initial-color-theme=light* has been set. + +*cursor* + Two space separated RRGGBB values (i.e. plain old 6-digit hex + values, without prefix) specifying the foreground (text) and + background (cursor) colors for the cursor. + + Example: *ff0000 00ff00* (green cursor, red text) + + Default: the regular foreground and background colors, reversed. + *foreground* Default foreground color. This is the color used when no ANSI color is being used. Default: _839496_. @@ -911,8 +1051,9 @@ 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 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 @@ -924,7 +1065,7 @@ can configure the background transparency with the _alpha_ option. the corresponding *regularN* color will be used. If the current color does not match any known color, it is dimmed - by reducing the luminance (i.e. the same behavior as if the *dimN* + by blending with black (i.e. the same behavior as if the *dimN* options are unconfigured). 24-bit RGB colors will typically fall into this category. @@ -952,10 +1093,40 @@ 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_ + +*blur* + Boolean. When enabled, foot will blur the background (main window + only, not CSDs etc), when it is transparent. This feature requires + the compositor to implement the _ext-background-effect-v1_ + protocol (and specifically, the _blur_ effect). + + Default: _no_ + +*dim-blend-towards* + Which color to blend towards when "auto" dimming a color (see + *dim0*..*dim7* above). One of *black* or *white*. Blending towards + black makes the text darker, while blending towards white makes it + whiter (but still dimmer than normal text). + + Default: _black_ (*colors-dark*), _white_ (*colors-light*) + *selection-foreground*, *selection-background* Foreground (text) and background color to use in selected - text. Note that *both* options must be set, or the default will be - used. Default: _inverse foreground/background_. + text. Default: _inverse foreground/background_. *jump-labels* Two color values specifying the foreground (text) and background @@ -1035,7 +1206,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* @@ -1057,8 +1228,8 @@ Examples: minimize/maximize/close buttons. Default: _26_. *button-color* - Foreground color on the minimize/maximize/close buttons. Default: - use the default _background_ color. + Foreground color on the minimize/maximize/close buttons and the + titlebar text. Default: use the default _background_ color. *button-minimize-color* Minimize button's background color. Default: use the default @@ -1085,11 +1256,42 @@ Note that if *Shift* is one of the modifiers, the _key_ *must not* be in upper case. For example, *Control+Shift+V* will never trigger, but *Control+Shift+v* will. -Note that *Alt* is usually called *Mod1*. +The default key bindings all use "real" modifiers (*Mod1*, *Mod4* +etc), but "virtual" modifiers (*Alt*, *Super* etc) are allowed. *xkbcli interactive-wayland* can be useful for finding keysym names. -A key combination can only be mapped to *one* action. Lets say you +When matching key presses to key bindings, foot uses a couple of +different approaches. + +As an example, let's say you press ctrl+shift+c (assume plain us ASCII +layout). XKB will tell foot *Control+C* was pressed. Note the lack of +the shift modifier, and the upper case 'C'. Internally, this is called +the "translated" form. + +The "untranslated" form (*Control+Shift+c*) is derived from the +translated form, and is what foot tries to match first. + +If no "untranslated" key bindings can be found, foot proceeds to +checking the "translated" variant. + +This means you can use either form in your foot configuration, and +that *Control+Shift+c* (and similar) has higher priority than +*Control+C*. Also note that while foot normally detects when the same +combination is assigned to multiple actions, it will not detect +*Control+C* vs. *Control+Shift+c* collisions. Call it a known bug... + +Finally, foot tries to match the raw key code. Here, the primary +layout is queried for all key codes that generate a particular XKB +symbol, and the pressed key's code is matched against this. For +example, if you use the layouts *"us,de(neo)"*, the 'r' key generates +the symbol 'c' in the neo layout. I.e. to get a 'c', you press +'r'. The match logic described above will only match 'c' key bindings +(e.g. *Control+Shift+c*). The raw mode however, will match 'r' key +bindings (e.g. *Control+Shift+r*). This is useful for non-latin +layouts, where you would otherwise have to customize all key bindings. + +A key combination can only be mapped to *one* action. Let's say you want to bind *Control+Shift+R* to *fullscreen*. Since this is the default shortcut for *search-start*, you first need to unmap the default binding. This can be done by setting _action=none_; @@ -1100,7 +1302,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_. @@ -1110,7 +1313,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_. @@ -1199,14 +1402,41 @@ e.g. *search-start=none*. *show-urls-copy* Enter URL mode, where all currently visible URLs are tagged with a jump label with a key sequence that will place the URL in the - clipboard. Default: _none_. + clipboard. If the hint is completed with an uppercase character, + the match will also be pasted. Default: _none_. + +*regex-launch* + Enter regex mode. This works exactly the same as URL mode; all + regex matches are tagged with a jump label with a key sequence + that will "launch" to match (and exit regex mode). + + The name of the regex section must be specified in the key + binding: + + ``` + [regex:hashes] + regex=([a-fA-F0-9]{7,128}) + launch=path-to-script-or-application ${match} + + [key-bindings] + regex-launch=[hashes] Control+Shift+q + regex-copy=[hashes] Control+Mod1+Shift+q + ``` + + Default: _none_. + +*regex-copy* + Same as *regex-launch*, but the match is placed in the clipboard, + instead of "launched", upon activation. If the hint is completed + with an uppercase character, the match will also be pasted. + Default: _none_. *prompt-prev* Jump to the previous, currently not visible, prompt (requires shell integration, see *foot*(1)). Default: _Control+Shift+z_. *prompt-next* - Jump the next prompt (requires shell integration, see + Jump to the next prompt (requires shell integration, see *foot*(1)). Default: _Control+Shift+x_. *unicode-input* @@ -1235,6 +1465,25 @@ e.g. *search-start=none*. Default: _Control+Shift+u_. +*color-theme-switch-dark*, *color-theme-switch-light*, *color-theme-toggle* + Switch between the dark color theme (defined in the *colors-dark* + section), and the light color theme (defined in the *colors-light* + section). + + *color-theme-switch-dark* applies the dark color theme regardless + of which color theme is currently active. + + *color-theme-switch-light* applies the light color theme + regardless of which color theme is currently active. + + *color-theme-toggle* toggles between the primary and alternative + color themes. + + Note: you can also send SIGUSR1/SIGUSR2 to the foot process to + change the theme (see *foot*(1) for details.) + + Default: _none_ + *quit* Quit foot. Default: _none_. @@ -1252,7 +1501,8 @@ scrollback search mode. The syntax is exactly the same as the regular *commit* Exit search mode and copy current selection into the _primary selection_. Viewport is **not** restored. To copy the selection to - the regular _clipboard_, use *Control+Shift+c*. Default: _Return_. + the regular _clipboard_, use *Control+Shift+c*. Default: _Return + KP_Enter_. *find-prev* Search **backwards** in the scrollback history for the next @@ -1300,6 +1550,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_. @@ -1343,7 +1599,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_. @@ -1353,7 +1610,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_. @@ -1446,7 +1703,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 @@ -1579,9 +1836,8 @@ any of these options. *scaling-filter* Overrides the default scaling filter used when down-scaling bitmap fonts (e.g. emoji fonts). Possible values are *none*, *nearest*, - *bilinear*, *cubic* or *lanczos3*. *cubic* and *lanczos3* produce - the best results, but are slower (with *lanczos3* being the best - _and_ slowest). + *bilinear*, *impulse*, *box*, *linear*, *cubic*, *gaussian*, + *lanczos2*, *lanczos3* or *lanczos3-stretched*. Default: _lanczos3_. @@ -1649,8 +1905,8 @@ any of these options. must be patched to use it. Until this has happened, foot offers an interim workaround; an - attempt to mitigate the screen flicker *without* affecting neither - performance nor latency. + attempt to mitigate the screen flicker *without* affecting either + performance or latency. It is based on the fact that the screen is updated at a fixed interval (typically 60Hz). For us, this means it does not matter @@ -1800,15 +2056,103 @@ 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_ +*dim-amount* + Amount by which dimmed text is darkened. Default: _1.5_. + *bold-text-in-bright-amount* Amount by which bold fonts are brightened when *bold-text-in-bright* is set to *yes* (the *palette-based* variant is not affected by this option). Default: _1.3_. +*surface-bit-depth* + Selects which RGB bit depth to use for image buffers. One of + *auto*, *8-bit*, *10-bit* or *16-bit*. + + *auto* chooses bit depth depending on other settings, and + availability. + + *8-bit*, uses 8 bits for each color channel, alpha included. This + is the default when *gamma-correct-blending=no*. + + *10-bit* uses 10 bits for each RGB channel, and 2 bits for the + alpha channel. Thus, it provides higher precision color channels, + but a lower precision alpha channel. + + *16-bit* 16 bits for each color channel, alpha included. If + available, this is the default when *gamma-correct-blending=yes*. + + Note that both *10-bit* and *16-bit* are much slower than *8-bit*; + if you want to use gamma-correct blending, and if you prefer speed + (throughput and input latency) over accurate colors, you can set + *surface-bit-depth=8-bit* explicitly. + + Default: _auto_ + +*pre-apply-damage* + Boolean. When enabled, foot will attempt to "pre-apply" the damage + from the last frame when foot is forced to double-buffer + (i.e. when the compositor does not release SHM buffers + immediately). All text after this assumes the compositor is not + releasing buffers immediately. + + When this option is disabled, each time foot needs to render a + frame, it has to first copy over areas that changed in the last + frame (i.e. all changes between the last two frames). This is + basically a *memcpy*(3), which can be slow if the changed area is + large. It is also done on the main thread, which means foot cannot + do anything else at the same time; no other rendering, no VT + parsing. After the changes have been brought over to the new + frame, foot proceeds with rendering the cells that has changed + between the last frame and the new frame. + + When this option is enabled, the changes between the last two frames + are brought over to what will become the next frame before foot + starts rendering the next frame. As soon as the compositor + releases the previous buffer (typically right after foot has + pushed a new frame), foot kicks off a thread that copies over the + changes to the newly released buffer. Since this is done in a + thread, foot can continue processing input at the same + time. Later, when it is time to render a new frame, the changes + have already been transferred, and foot can immediately start with + the actual rendering. + + Thus, having this option enabled improves both performance + (copying the last two frames' changes is threaded), and improves + input latency (rendering the next frame no longer has to first bring + over the changes between the last two frames). + + Default: _yes_ + # SEE ALSO *foot*(1), *footclient*(1) diff --git a/doc/footclient.1.scd b/doc/footclient.1.scd index 365689af..ad865913 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_. @@ -189,6 +194,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 the dark color theme (*[colors-dark]*). +- SIGUSR2: switch to the light color theme (*[colors-light]*). + +When sending SIGUSR1/SIGUSR2 to a footclient instance, the theme is +changed in that instance only. This is different from when you send +SIGUSR1/SIGUSR2 to the server process, where all instances change the +theme. + +Note: for obvious reasons, this is not supported when footclient is +started with *--no-wait*. + # SEE ALSO *foot*(1) diff --git a/doc/sixel-tux-foot.png b/doc/sixel-tux-foot.png new file mode 100644 index 00000000..ce30fe8f Binary files /dev/null and b/doc/sixel-tux-foot.png differ diff --git a/doc/sixel-wow.png b/doc/sixel-wow.png deleted file mode 100644 index da481ac8..00000000 Binary files a/doc/sixel-wow.png and /dev/null differ diff --git a/doc/tux-foot-ok.png b/doc/tux-foot-ok.png new file mode 100644 index 00000000..5d814623 Binary files /dev/null and b/doc/tux-foot-ok.png differ diff --git a/extract.c b/extract.c index 31c32248..cd9a0c95 100644 --- a/extract.c +++ b/extract.c @@ -256,8 +256,8 @@ extract_one(const struct terminal *term, const struct row *row, } } - xassert(next_tab_stop >= col); - ctx->tab_spaces_left = next_tab_stop - col; + if (next_tab_stop > col) + ctx->tab_spaces_left = next_tab_stop - col - 1; } } diff --git a/fdm.c b/fdm.c index ea30443b..4822cd97 100644 --- a/fdm.c +++ b/fdm.c @@ -18,6 +18,18 @@ #include "debug.h" #include "xmalloc.h" +#if !defined(SIGABBREV_NP) +#include + +static const char * +sigabbrev_np(int sig) +{ + static char buf[16]; + snprintf(buf, sizeof(buf), "<%d>", sig); + return buf; +} +#endif + struct fd_handler { int fd; int events; @@ -113,7 +125,8 @@ fdm_destroy(struct fdm *fdm) for (int i = 0; i < SIGRTMAX; i++) { if (fdm->signal_handlers[i].callback != NULL) - LOG_WARN("handler for signal %d not removed", i); + LOG_WARN("handler for signal %d (SIG%s) not removed", + i, sigabbrev_np(i)); } if (tll_length(fdm->hooks_low) > 0 || @@ -338,7 +351,8 @@ bool fdm_signal_add(struct fdm *fdm, int signo, fdm_signal_handler_t handler, void *data) { if (fdm->signal_handlers[signo].callback != NULL) { - LOG_ERR("signal %d already has a handler", signo); + LOG_ERR("signal %d (SIG%s) already has a handler", + signo, sigabbrev_np(signo)); return false; } @@ -347,14 +361,16 @@ fdm_signal_add(struct fdm *fdm, int signo, fdm_signal_handler_t handler, void *d sigaddset(&mask, signo); if (sigprocmask(SIG_BLOCK, &mask, &original) < 0) { - LOG_ERRNO("failed to block signal %d", signo); + LOG_ERRNO("failed to block signal %d (SIG%s)", + signo, sigabbrev_np(signo)); return false; } struct sigaction action = {.sa_handler = &signal_handler}; sigemptyset(&action.sa_mask); if (sigaction(signo, &action, NULL) < 0) { - LOG_ERRNO("failed to set signal handler for signal %d", signo); + LOG_ERRNO("failed to set signal handler for signal %d (SIG%s)", + signo, sigabbrev_np(signo)); sigprocmask(SIG_SETMASK, &original, NULL); return false; } @@ -374,7 +390,8 @@ fdm_signal_del(struct fdm *fdm, int signo) struct sigaction action = {.sa_handler = SIG_DFL}; sigemptyset(&action.sa_mask); if (sigaction(signo, &action, NULL) < 0) { - LOG_ERRNO("failed to restore signal handler for signal %d", signo); + LOG_ERRNO("failed to restore signal handler for signal %d (SIG%s)", + signo, sigabbrev_np(signo)); return false; } @@ -386,7 +403,8 @@ fdm_signal_del(struct fdm *fdm, int signo) sigemptyset(&mask); sigaddset(&mask, signo); if (sigprocmask(SIG_UNBLOCK, &mask, NULL) < 0) { - LOG_ERRNO("failed to unblock signal %d", signo); + LOG_ERRNO("failed to unblock signal %d (SIG%s)", + signo, sigabbrev_np(signo)); return false; } diff --git a/foot-features.c b/foot-features.c new file mode 100644 index 00000000..8e332517 --- /dev/null +++ b/foot-features.c @@ -0,0 +1,42 @@ +#include "foot-features.h" +#include "version.h" + +const char version_and_features[] = + "version: " FOOT_VERSION + +#if defined(FOOT_PGO_ENABLED) && FOOT_PGO_ENABLED + " +pgo" +#else + " -pgo" +#endif + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + " +ime" +#else + " -ime" +#endif + +#if defined(FOOT_GRAPHEME_CLUSTERING) && FOOT_GRAPHEME_CLUSTERING + " +graphemes" +#else + " -graphemes" +#endif + +#if defined(HAVE_XDG_TOPLEVEL_TAG) + " +toplevel-tag" +#else + " -toplevel-tag" +#endif + +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + " +blur" +#else + " -blur" +#endif + +#if !defined(NDEBUG) + " +assertions" +#else + " -assertions" +#endif +; diff --git a/foot-features.h b/foot-features.h index 674c1056..49cc56ed 100644 --- a/foot-features.h +++ b/foot-features.h @@ -1,48 +1,13 @@ #pragma once -#include +#include -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 -} - -static inline bool feature_xdg_toplevel_icon(void) -{ -#if defined(HAVE_XDG_TOPLEVEL_ICON) - return true; -#else - return false; -#endif + fputs(prefix, stdout); + fputs(version_and_features, stdout); + fputc('\n', stdout); } diff --git a/foot.ini b/foot.ini index bd4ac082..a9b4b83d 100644 --- a/foot.ini +++ b/foot.ini @@ -22,11 +22,13 @@ # strikeout-thickness= # box-drawings-uses-font-glyphs=no # dpi-aware=no +# gamma-correct-blending=no +# initial-color-theme=dark # initial-window-size-pixels=700x500 # Or, # initial-window-size-chars= # 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 @@ -38,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 @@ -45,6 +49,7 @@ # osc52=enabled # disabled|copy-enabled|paste-enabled|enabled [bell] +# system=yes # urgent=no # notify=no # visual=no @@ -68,12 +73,22 @@ # launch=xdg-open ${url} # label-letters=sadfjklewcmpgh # osc8-underline=url-mode -# protocols=http, https, ftp, ftps, file, gemini, gopher -# uri-characters=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.,~:;/?#@!$&%*+="'()[] +# regex=(((https?://|mailto:|ftp://|file:|ssh:|ssh://|git://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news:)|www\.)([0-9a-zA-Z:/?#@!$&*+,;=.~_%^\-]+|\([]\["0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*\)|\[[\(\)"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*\]|"[]\[\(\)0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*"|'[]\[\(\)0-9a-zA-Z:/?#@!$&*+,;=.~_%^\-]*')+([0-9a-zA-Z/#@$&*+=~_%^\-]|\([]\["0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*\)|\[[\(\)"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*\]|"[]\[\(\)0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*"|'[]\[\(\)0-9a-zA-Z:/?#@!$&*+,;=.~_%^\-]*')) + +# You can define your own regex's, by adding a section called +# 'regex:' with a 'regex' and 'launch' key. These can then be tied +# to a key-binding. See foot.ini(5) for details + +# [regex:your-fancy-name] +# regex= +# launch= ${match} +# +# [key-bindings] +# regex-launch=[your-fancy-name] Control+Shift+q +# regex-copy=[your-fancy-name] Control+Alt+Shift+q [cursor] # style=block -# color= # blink=no # blink-rate=500 # beam-thickness=1.5 @@ -86,13 +101,16 @@ [touch] # long-press-delay=400 -[colors] +[colors-dark] # alpha=1.0 +# alpha-mode=default # Can be `default`, `matching` or `all` # background=242424 # foreground=ffffff # flash=7f7f00 # flash-alpha=0.5 +# cursor= + ## Normal/regular colors (color palette 0-7) # regular0=242424 # black # regular1=f62b5a # red @@ -114,6 +132,7 @@ # bright7=ffffff # bright white ## dimmed colors (see foot.ini(5) man page) +# dim-blend-towards=black # dim0= # ... # dim7= @@ -150,6 +169,11 @@ # search-box-match= # black-on-yellow # urls= +[colors-light] +# Alternative color theme, see man page foot.ini(5) +# Same builtin defaults as [color], except for: +# dim-blend-towards=white + [csd] # preferred=server # size=26 @@ -166,10 +190,10 @@ # button-close-color= [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 @@ -195,12 +219,15 @@ # 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 [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 @@ -213,6 +240,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 @@ -224,10 +253,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 diff --git a/grid.c b/grid.c index 2f65a1dd..df7ef61c 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 * @@ -36,7 +40,8 @@ grid_row_abs_to_sb(const struct grid *grid, int screen_rows, int abs_row) return rebased_row; } -int grid_row_sb_to_abs(const struct grid *grid, int screen_rows, int sb_rel_row) +int +grid_row_sb_to_abs(const struct grid *grid, int screen_rows, int sb_rel_row) { const int scrollback_start = grid->offset + screen_rows; int abs_row = sb_rel_row + scrollback_start; @@ -434,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; @@ -496,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); @@ -707,7 +711,6 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, } 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; @@ -811,7 +814,7 @@ tp_cmp(const void *_a, const void *_b) void grid_resize_and_reflow( - struct grid *grid, int new_rows, int new_cols, + struct grid *grid, const struct terminal *term, int new_rows, int new_cols, int old_screen_rows, int new_screen_rows, size_t tracking_points_count, struct coord *const _tracking_points[static tracking_points_count]) @@ -889,6 +892,8 @@ grid_resize_and_reflow( i, tracking_points[i]->row, tracking_points[i]->col); } + int coalesced_linebreaks = 0; + /* * Walk the old grid */ @@ -930,7 +935,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); @@ -954,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]; @@ -978,213 +984,177 @@ 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; - - /* 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; - - end = min(col_count, min(min(tp_col, min(uri_col, underline_col)), ftcs_col)); - - uri_break = end == uri_col; - underline_break = end == underline_col; - tp_break = end == tp_col; - ftcs_break = end == ftcs_col; + if (unlikely(col_count > 0 && coalesced_linebreaks > 0)) { + for (size_t line_no = 0; line_no < coalesced_linebreaks; line_no++) { + /* Erase the remaining cells */ + memset(&new_row->cells[new_col_idx], 0, + (new_cols - new_col_idx) * sizeof(new_row->cells[0])); + new_row->linebreak = true; + line_wrap(); } - int cols = end - start; - xassert(cols > 0); - xassert(start + cols <= old_cols); + coalesced_linebreaks = 0; + } + + for (int c = 0; c < col_count;) { + const struct cell *old = &old_row->cells[c]; + + /* Row full, emit newline and get a new, fresh, row */ + xassert(new_col_idx <= new_cols); + if (unlikely(new_col_idx >= new_cols)) + line_wrap(); + + char32_t wc = old->wc; + int width = 1; + + if (unlikely(wc >= CELL_COMB_CHARS_LO && wc <= CELL_COMB_CHARS_HI)) { + const struct composed *composed = + composed_lookup(term->composed, wc - CELL_COMB_CHARS_LO); + + width = composed->forced_width > 0 ? composed->forced_width : composed->width; + } else if (unlikely(c + 1 < col_count && (old + 1)->wc >= CELL_SPACER + 1)) { + /* Wide character, get its width from the next cell's + SPACER value */ + width = (old + 1)->wc - CELL_SPACER + 1; + } /* - * 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. - */ + * 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(); + } - for (int count = cols, from = start; count > 0;) { - xassert(new_col_idx <= new_cols); - int new_row_cells_left = new_cols - new_col_idx; + new_row->shell_integration.prompt_marker = old_row->shell_integration.prompt_marker; - /* Row full, emit newline and get a new, fresh, row */ - if (new_row_cells_left <= 0) { - line_wrap(); - new_row_cells_left = new_cols; + for (int i = 0; i < width; i++) { + if (unlikely(uri_range != NULL && uri_range != uri_range_terminator)) { + if (unlikely(uri_range->start == c)) { + reflow_range_start( + uri_range, ROW_RANGE_URI, new_row, new_col_idx); + } + + if (unlikely(uri_range->end == c)) { + reflow_range_end( + uri_range, ROW_RANGE_URI, new_row, new_col_idx); + grid_row_uri_range_destroy(uri_range); + uri_range++; + } } - /* Number of cells we can copy */ - int amount = min(count, new_row_cells_left); - xassert(amount > 0); + if (unlikely(underline_range != NULL && underline_range != underline_range_terminator)) { + if (unlikely(underline_range->start == c)) { + reflow_range_start( + underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx); + } + + if (unlikely(underline_range->end == c)) { + reflow_range_end( + underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx); + grid_row_underline_range_destroy(underline_range); + underline_range++; + } + } + + if (unlikely(tp != NULL)) { + if (unlikely(tp->col == c)) { + do { + xassert(tp->row == old_row_idx); + + tp->row = new_row_idx; + tp->col = new_col_idx; + + next_tp++; + tp = *next_tp; + } while (tp->row == old_row_idx && tp->col == c); + + if (tp->row != old_row_idx) + tp = NULL; + + LOG_DBG("next TP (tp=%p): %dx%d", + (void*)tp, (*next_tp)->row, (*next_tp)->col); + } + } + + if (unlikely(old_row->shell_integration.cmd_start == c)) + new_row->shell_integration.cmd_start = new_col_idx; + + if (unlikely(old_row->shell_integration.cmd_end == c)) + new_row->shell_integration.cmd_end = new_col_idx; + + if (unlikely(width > new_cols)) { + /* Wide character no longer fits on a row, replace + it with a single space */ + new_row->cells[new_col_idx++].wc = 0; + c++; + + /* Walk past the SPACER cells */ + for (int i = 1; i < width; i++, c++, old++) + ; + + /* Continue with next character in the *old* grid */ + break; + } + + new_row->cells[new_col_idx++] = *old; /* - * If we're going to reach the end of the new row, we - * need to make sure we don't end in the middle of a - * multi-column character. + * TODO: simulate LCF instead? + * + * Rows have linebreak=true by default. This is needed + * for a number of reasons. However, we want non-empty + * rows to have linebreak=false, *until* we reach the + * end of an old row with linebreak=true, at which + * point we set linebreak=true on the new row. */ - int spacers = 0; - if (new_col_idx + amount >= new_cols) { - /* - * While the cell *after* the last cell is a CELL_SPACER - * - * This means we have a multi-column character - * that doesn't fit on the current row. We need to - * push it to the next row, and insert CELL_SPACER - * cells as padding. - */ - while ( - unlikely( - amount > 1 && - from + amount < old_cols && - old_row->cells[from + amount].wc >= CELL_SPACER + 1)) - { - amount--; - spacers++; - } - - xassert( - amount == 1 || - old_row->cells[from + amount - 1].wc <= CELL_SPACER + 1); - } - - xassert(new_col_idx + amount <= new_cols); - xassert(from + amount <= old_cols); - - if (from == 0) - new_row->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); - - const struct cell *cell = &old_row->cells[from - 1]; - - for (int i = 0; i < spacers; i++, new_col_idx++) { - new_row->cells[new_col_idx].wc = CELL_SPACER; - new_row->cells[new_col_idx].attrs = cell->attrs; - } - } + new_row->linebreak = false; + old++; + c++; } - - xassert(new_col_idx > 0); - - if (tp_break) { - do { - xassert(tp != NULL); - xassert(tp->row == old_row_idx); - xassert(tp->col == end - 1); - - tp->row = new_row_idx; - tp->col = new_col_idx - 1; - - next_tp++; - tp = *next_tp; - } while (tp->row == old_row_idx && tp->col == end - 1); - - if (tp->row != old_row_idx) - tp = NULL; - - LOG_DBG("next TP (tp=%p): %dx%d", - (void*)tp, (*next_tp)->row, (*next_tp)->col); - } - - if (uri_break) { - xassert(uri_range != NULL); - - if (uri_range->start == end - 1) - reflow_range_start( - uri_range, ROW_RANGE_URI, new_row, new_col_idx - 1); - - if (uri_range->end == end - 1) { - reflow_range_end( - uri_range, ROW_RANGE_URI, new_row, new_col_idx - 1); - grid_row_uri_range_destroy(uri_range); - uri_range++; - } - } - - if (underline_break) { - xassert(underline_range != NULL); - - if (underline_range->start == end - 1) - reflow_range_start( - underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx - 1); - - if (underline_range->end == end - 1) { - reflow_range_end( - underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx - 1); - 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 (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; } 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++; } } @@ -1274,15 +1244,26 @@ grid_resize_and_reflow( saved_cursor.row = min(saved_cursor.row, new_screen_rows - 1); saved_cursor.col = min(saved_cursor.col, new_cols - 1); + if (grid->cursor.lcf) { + if (cursor.col + 1 < new_cols) { + cursor.col++; + grid->cursor.lcf = false; + } + } + + if (grid->saved_cursor.lcf) { + if (saved_cursor.col + 1 < new_cols) { + saved_cursor.col++; + grid->saved_cursor.lcf = false; + } + } + grid->cur_row = new_grid[(grid->offset + cursor.row) & (new_rows - 1)]; xassert(grid->cur_row != NULL); grid->cursor.point = cursor; grid->saved_cursor.point = saved_cursor; - grid->cursor.lcf = false; - grid->saved_cursor.lcf = false; - /* Free sixels we failed to "map" to the new grid */ tll_foreach(untranslated_sixels, it) sixel_destroy(&it->item); 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/hsl.c b/hsl.c index d5d00e67..1a8c919e 100644 --- a/hsl.c +++ b/hsl.c @@ -2,41 +2,6 @@ #include -#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 -void rgb_to_hsl(uint32_t rgb, int *hue, int *sat, int *lum); uint32_t hsl_to_rgb(int hue, int sat, int lum); diff --git a/ime.c b/ime.c index 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); } diff --git a/input.c b/input.c index 51425a36..aa6b7f1d 100644 --- a/input.c +++ b/input.c @@ -40,6 +40,7 @@ #include "url-mode.h" #include "util.h" #include "vt.h" +#include "xkbcommon-vmod.h" #include "xmalloc.h" #include "xsnprintf.h" @@ -119,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: @@ -148,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: @@ -349,9 +358,9 @@ execute_binding(struct seat *seat, struct terminal *term, action == BIND_ACTION_SHOW_URLS_LAUNCH ? URL_ACTION_LAUNCH : URL_ACTION_PERSISTENT; - urls_collect(term, url_action, &term->urls); + urls_collect(term, url_action, &term->conf->url.preg, true, &term->urls); urls_assign_key_combos(term->conf, &term->urls); - urls_render(term); + urls_render(term, &term->conf->url.launch); return true; } @@ -448,6 +457,56 @@ 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_THEME_SWITCH_1: + case BIND_ACTION_THEME_SWITCH_DARK: + term_theme_switch_to_dark(term); + return true; + + case BIND_ACTION_THEME_SWITCH_2: + case BIND_ACTION_THEME_SWITCH_LIGHT: + term_theme_switch_to_light(term); + return true; + + case BIND_ACTION_THEME_TOGGLE: + term_theme_toggle(term); + return true; + case BIND_ACTION_SELECT_BEGIN: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_CHAR_WISE, false); @@ -484,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( @@ -527,21 +586,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: - return; + goto err; case WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1: break; default: LOG_WARN("unrecognized keymap format: %u", format); - return; + goto err; } char *map_str = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); if (map_str == MAP_FAILED) { LOG_ERRNO("failed to mmap keyboard keymap"); - close(fd); - return; + goto err; } while (map_str[size - 1] == '\0') @@ -554,6 +612,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); @@ -582,14 +642,62 @@ 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"); } - munmap(map_str, size); - close(fd); - key_binding_load_keymap(wayl->key_binding_manager, seat); + +err: + close(fd); } static void @@ -679,9 +787,17 @@ keyboard_leave(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, seat->kbd.alt = false; seat->kbd.ctrl = false; seat->kbd.super = false; + if (seat->kbd.xkb_compose_state != NULL) xkb_compose_state_reset(seat->kbd.xkb_compose_state); + if (seat->kbd.xkb_state != NULL && seat->kbd.xkb_keymap != NULL) { + const xkb_layout_index_t layout_count = xkb_keymap_num_layouts(seat->kbd.xkb_keymap); + + for (xkb_layout_index_t i = 0; i < layout_count; i++) + xkb_state_update_mask(seat->kbd.xkb_state, 0, 0, 0, i, i, i); + } + if (old_focused != NULL) { seat->pointer.hidden = false; term_xcursor_update_for_seat(old_focused, seat); @@ -1119,9 +1235,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); @@ -1213,7 +1326,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; } @@ -1276,99 +1389,87 @@ emit_escapes: encoded_mods |= mods & (1 << seat->kbd.mod_num) ? (1 << 7) : 0; encoded_mods++; - int key = -1, alternate = -1, base = -1; + /* + * Figure out the main, alternate and base key codes. + * + * The main key is the unshifted version of the generated symbol, + * the alternate key is the shifted version, and base is the + * (unshifted) key assuming the default layout. + * + * For example, the user presses shift+a, then: + * - unshifted = 'a' + * - shifted = 'A' + * - base = 'a' + * + * Base will in many cases be the same as the unshifted key, but + * may differ if the active keyboard layout is non-ASCII (examples + * would be russian, or alternative layouts like neo etc). + * + * The shifted key is what we get from XKB, i.e. the resulting key + * from all active modifiers, plus the pressed key. + */ + int unshifted = -1, shifted = -1, base = -1; char final; if (info != NULL) { + /* Use code from lookup table (cursor keys, enter, tab etc)*/ if (!info->is_modifier || report_all_as_escapes) { - key = info->key; + shifted = info->key; final = info->final; } } else { - /* - * Use keysym (typically its Unicode codepoint value). - * - * If the keysym is shifted, use its unshifted codepoint - * instead. In other words, ctrl+a and ctrl+shift+a should - * both use the same value for 'key' (97 - i.a. 'a'). - * - * However, don't do this if a non-significant modifier was - * used to generate the symbol. This is needed since we cannot - * encode non-significant modifiers, and thus the "extra" - * modifier(s) would get lost. - * - * Example: - * - * the Swedish layout has '2', QUOTATION MARK ("double - * quote"), '@', and '²' on the same key. '2' is the base - * symbol. - * - * Shift+2 results in QUOTATION MARK - * AltGr+2 results in '@' - * AltGr+Shift+2 results in '²' - * - * The kitty kbd protocol can't encode AltGr. So, if we - * always used the base symbol ('2'), Alt+Shift+2 would - * result in the same escape sequence as - * AltGr+Alt+Shift+2. - * - * (yes, this matches what kitty does, as of 0.23.1) - */ - - /* Get the key's shift level */ - xkb_level_index_t lvl = xkb_state_key_get_level( - seat->kbd.xkb_state, ctx->key, ctx->layout); - - /* And get all modifier combinations that, combined with - * the pressed key, results in the current shift level */ - xkb_mod_mask_t masks[32]; - size_t mask_count = xkb_keymap_key_get_mods_for_level( - seat->kbd.xkb_keymap, ctx->key, ctx->layout, lvl, - masks, ALEN(masks)); - - /* Check modifier combinations - if a combination has - * modifiers not in our set of 'significant' modifiers, - * use key sym as-is */ - bool use_level0_sym = true; - for (size_t i = 0; i < mask_count; i++) { - if ((masks[i] & ~seat->kbd.kitty_significant) > 0) { - use_level0_sym = false; - break; - } - } - - xkb_keysym_t sym_to_use = use_level0_sym && ctx->level0_syms.count > 0 - ? ctx->level0_syms.syms[0] - : sym; + /* Use keysym (typically its Unicode codepoint value) */ if (composed) - key = utf32[0]; /* TODO: what if there are multiple codepoints? */ - else { - key = xkb_keysym_to_utf32(sym_to_use); - if (key == 0) - return false; - - /* The *shifted* key. May be the same as the unshifted - * key - if so, this is filtered out below, when - * emitting the CSI */ - alternate = xkb_keysym_to_utf32(sym); - } - - /* Base layout key. I.e the symbol the pressed key produces in - * the base/default layout (layout idx 0) */ - const xkb_keysym_t *base_syms; - int base_sym_count = xkb_keymap_key_get_syms_by_level( - seat->kbd.xkb_keymap, ctx->key, 0, 0, &base_syms); - - if (base_sym_count > 0) - base = xkb_keysym_to_utf32(base_syms[0]); + shifted = utf32[0]; /* TODO: what if there are multiple codepoints? */ + else + shifted = xkb_keysym_to_utf32(sym); final = 'u'; } - if (key < 0) + if (shifted <= 0) return false; + /* Base layout key. I.e the symbol the pressed key produces in + * the base/default layout (layout idx 0) */ + const xkb_keysym_t *base_syms; + int base_sym_count = xkb_keymap_key_get_syms_by_level( + seat->kbd.xkb_keymap, ctx->key, 0, 0, &base_syms); + + if (base_sym_count > 0) + base = xkb_keysym_to_utf32(base_syms[0]); + + /* + * If the keysym is shifted, use its unshifted codepoint + * instead. In other words, ctrl+a and ctrl+shift+a should both + * use the same value for 'key' (97 - i.a. 'a'). + * + * However, don't do this if a non-significant modifier was used + * to generate the symbol. This is needed since we cannot encode + * non-significant modifiers, and thus the "extra" modifier(s) + * would get lost. + * + * Example: + * + * the Swedish layout has '2', QUOTATION MARK ("double quote"), + * '@', and '²' on the same key. '2' is the base symbol. + * + * Shift+2 results in QUOTATION MARK + * AltGr+2 results in '@' + * AltGr+Shift+2 results in '²' + * + * The kitty kbd protocol can't encode AltGr. So, if we always + * used the base symbol ('2'), Alt+Shift+2 would result in the + * same escape sequence as AltGr+Alt+Shift+2. + * + * (yes, this matches what kitty does, as of 0.23.1) + */ + const bool use_level0_sym = + (ctx->mods & ~seat->kbd.kitty_significant) == 0 && ctx->level0_syms.count > 0; + + unshifted = use_level0_sym ? xkb_keysym_to_utf32(ctx->level0_syms.syms[0]) : 0; + xassert(encoded_mods >= 1); char event[4]; @@ -1385,13 +1486,16 @@ emit_escapes: size_t left = sizeof(buf); size_t bytes; + const int key = unshifted > 0 && isc32print(unshifted) && !composed ? unshifted : shifted; + const int alternate = shifted; + if (final == 'u' || final == '~') { bytes = snprintf(p, left, "\x1b[%u", key); p += bytes; left -= bytes; if (report_alternate) { bool emit_alternate = alternate > 0 && alternate != key; - bool emit_base = base > 0 && base != key && base != alternate; + bool emit_base = base > 0 && base != key && base != alternate && isc32print(base); if (emit_alternate) { bytes = snprintf(p, left, ":%u", alternate); @@ -1501,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); @@ -1591,38 +1698,70 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, * User configurable bindings */ if (pressed) { + /* Match untranslated symbols */ tll_foreach(bindings->key, it) { const struct key_binding *bind = &it->item; - /* Match translated symbol */ - if (bind->k.sym == sym && - bind->mods == (mods & ~consumed) && - execute_binding(seat, term, bind, serial, 1)) - { - goto maybe_repeat; - } - - if (bind->mods != mods) + if (bind->mods != mods || bind->mods == 0) continue; - /* Match untranslated symbols */ for (size_t i = 0; i < raw_count; i++) { if (bind->k.sym == raw_syms[i] && execute_binding(seat, term, bind, serial, 1)) { - goto maybe_repeat; - } - } - - /* Match raw key code */ - tll_foreach(bind->k.key_codes, code) { - if (code->item == key && - execute_binding(seat, term, bind, serial, 1)) - { + seat->kbd.last_shortcut_sym = sym; goto maybe_repeat; } } } + + /* Match translated symbol */ + tll_foreach(bindings->key, it) { + const struct key_binding *bind = &it->item; + + if (bind->k.sym == sym && + bind->mods == (mods & ~consumed) && + execute_binding(seat, term, bind, serial, 1)) + { + seat->kbd.last_shortcut_sym = sym; + goto maybe_repeat; + } + } + + /* Match raw key code */ + tll_foreach(bindings->key, it) { + const struct key_binding *bind = &it->item; + + if (bind->mods != mods || bind->mods == 0) + continue; + + tll_foreach(bind->k.key_codes, code) { + if (code->item == key && + execute_binding(seat, term, bind, serial, 1)) + { + seat->kbd.last_shortcut_sym = sym; + goto maybe_repeat; + } + } + } + } + + if (released && seat->kbd.last_shortcut_sym == sym) { + /* + * Don't process a release event, if it corresponds to a + * triggered shortcut. + * + * 1. If we consumed a key (press) event, we shouldn't emit an + * escape for its release event. + * 2. Ignoring the incorrectness of doing so; this also caused + * us to reset the viewport. + * + * Background: if the kitty keyboard protocol was enabled, + * then the viewport was instantly reset to the bottom, after + * scrolling up. + */ + //seat->kbd.last_shortcut_sym = XKB_KEYSYM_MAX + 1; + goto maybe_repeat; } /* @@ -1722,6 +1861,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]; @@ -1765,6 +1908,398 @@ keyboard_modifiers(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, term_xcursor_update_for_seat(seat->kbd_focus, seat); } +UNITTEST +{ + int chan[2]; + xassert(pipe2(chan, O_CLOEXEC) == 0); + + xassert(chan[0] >= 0); + xassert(chan[1] >= 0); + + struct config conf = {0}; + struct grid grid = {0}; + + struct terminal term = { + .conf = &conf, + .grid = &grid, + .ptmx = chan[1], + .selection = { + .coords = { + .start = {-1, -1}, + .end = {-1, -1}, + }, + .auto_scroll = { + .fd = -1, + }, + }, + }; + + struct key_binding_manager *key_binding_manager = key_binding_manager_new(); + + struct wayland wayl = { + .key_binding_manager = key_binding_manager, + .terms = tll_init(), + }; + + struct seat seat = { + .wayl = &wayl, + .name = "unittest", + }; + + tll_push_back(wayl.terms, &term); + term.wl = &wayl; + + seat.kbd.xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + xassert(seat.kbd.xkb != NULL); + + grid.kitty_kbd.flags[0] = KITTY_KBD_DISAMBIGUATE | KITTY_KBD_REPORT_ALTERNATE; + + /* Swedish keymap */ + { + seat.kbd.xkb_keymap = xkb_keymap_new_from_names( + seat.kbd.xkb, &(struct xkb_rule_names){.layout = "se"}, XKB_KEYMAP_COMPILE_NO_FLAGS); + if (seat.kbd.xkb_keymap == NULL) { + /* Skip test */ + goto no_keymap; + } + + seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap); + xassert(seat.kbd.xkb_state != NULL); + + seat.kbd.mod_shift = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); + seat.kbd.mod_alt = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; + seat.kbd.mod_ctrl = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL); + seat.kbd.mod_super = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO); + seat.kbd.mod_caps = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS); + seat.kbd.mod_num = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM); + + /* Significant modifiers in the legacy keyboard protocol */ + seat.kbd.legacy_significant = 0; + if (seat.kbd.mod_shift != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift; + if (seat.kbd.mod_alt != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt; + if (seat.kbd.mod_ctrl != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl; + if (seat.kbd.mod_super != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super; + + /* Significant modifiers in the kitty keyboard protocol */ + seat.kbd.kitty_significant = seat.kbd.legacy_significant; + if (seat.kbd.mod_caps != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps; + if (seat.kbd.mod_num != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num; + + key_binding_new_for_seat(key_binding_manager, &seat); + key_binding_load_keymap(key_binding_manager, &seat); + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_ctrl; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_A + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key: 97 = 'a', alternate: 65 = 'A', base: N/A, mods: 6 = ctrl+shift */ + const char expected_ctrl_shift_a[] = "\033[97:65;6u"; + xassert(count == strlen(expected_ctrl_shift_a)); + xassert(streq(escape, expected_ctrl_shift_a)); + + key_press_release(&seat, &term, 1337, KEY_A + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key;. 50 = '2', alternate: 34 = '"', base: N/A, 4 = alt+shift */ + const char expected_alt_shift_2[] = "\033[50:34;4u"; + xassert(count == strlen(expected_alt_shift_2)); + xassert(streq(escape, expected_alt_shift_2)); + + key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_index_t alt_gr = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, "Mod5"); + xassert(alt_gr != XKB_MOD_INVALID); + + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt | 1u << alt_gr; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key; 178 = '²', alternate: N/A, base: 50 = '2', 4 = alt+shift (AltGr not part of the protocol) */ + const char expected_altgr_alt_shift_2[] = "\033[178::50;4u"; + xassert(count == strlen(expected_altgr_alt_shift_2)); + xassert(streq(escape, expected_altgr_alt_shift_2)); + + key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_alt; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_BACKSPACE + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key; 127 = , alternate: N/A, base: N/A, 3 = alt */ + const char expected_alt_backspace[] = "\033[127;3u"; + xassert(count == strlen(expected_alt_backspace)); + xassert(streq(escape, expected_alt_backspace)); + + key_press_release(&seat, &term, 1337, KEY_BACKSPACE + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_ENTER + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key; 13 = , alternate: N/A, base: N/A, 5 = ctrl */ + const char expected_ctrl_enter[] = "\033[13;5u"; + xassert(count == strlen(expected_ctrl_enter)); + xassert(streq(escape, expected_ctrl_enter)); + + key_press_release(&seat, &term, 1337, KEY_ENTER + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_TAB + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key; 9 = , alternate: N/A, base: N/A, 5 = ctrl */ + const char expected_ctrl_tab[] = "\033[9;5u"; + xassert(count == strlen(expected_ctrl_tab)); + xassert(streq(escape, expected_ctrl_tab)); + + key_press_release(&seat, &term, 1337, KEY_TAB + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl | 1u << seat.kbd.mod_shift; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_LEFT + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + const char expected_ctrl_shift_left[] = "\033[1;6D"; + xassert(count == strlen(expected_ctrl_shift_left)); + xassert(streq(escape, expected_ctrl_shift_left)); + + key_press_release(&seat, &term, 1337, KEY_LEFT + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + key_binding_unload_keymap(key_binding_manager, &seat); + key_binding_remove_seat(key_binding_manager, &seat); + + xkb_state_unref(seat.kbd.xkb_state); + xkb_keymap_unref(seat.kbd.xkb_keymap); + + seat.kbd.xkb_state = NULL; + seat.kbd.xkb_keymap = NULL; + } + + /* de(neo) keymap */ + { + seat.kbd.xkb_keymap = xkb_keymap_new_from_names( + seat.kbd.xkb, &(struct xkb_rule_names){.layout = "us,de(neo)"}, + XKB_KEYMAP_COMPILE_NO_FLAGS); + + if (seat.kbd.xkb_keymap == NULL) { + /* Skip test */ + goto no_keymap; + } + + seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap); + xassert(seat.kbd.xkb_state != NULL); + + seat.kbd.mod_shift = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); + seat.kbd.mod_alt = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; + seat.kbd.mod_ctrl = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL); + seat.kbd.mod_super = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO); + seat.kbd.mod_caps = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS); + seat.kbd.mod_num = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM); + + /* Significant modifiers in the legacy keyboard protocol */ + seat.kbd.legacy_significant = 0; + if (seat.kbd.mod_shift != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift; + if (seat.kbd.mod_alt != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt; + if (seat.kbd.mod_ctrl != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl; + if (seat.kbd.mod_super != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super; + + /* Significant modifiers in the kitty keyboard protocol */ + seat.kbd.kitty_significant = seat.kbd.legacy_significant; + if (seat.kbd.mod_caps != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps; + if (seat.kbd.mod_num != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num; + + key_binding_new_for_seat(key_binding_manager, &seat); + key_binding_load_keymap(key_binding_manager, &seat); + + { + /* + * In the de(neo) layout, the Y key generates 'k'. This + * means we should get a key+alternate that indicates 'k', + * but a base key that is 'y'. + */ + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 1); + key_press_release(&seat, &term, 1337, KEY_Y + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key: 107 = 'k', alternate: 75 = 'K', base: 121 = 'y', mods: 4 = alt+shift */ + const char expected_alt_shift_y[] = "\033[107:75:121;4u"; + xassert(count == strlen(expected_alt_shift_y)); + xassert(streq(escape, expected_alt_shift_y)); + + key_press_release(&seat, &term, 1337, KEY_Y + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + key_binding_unload_keymap(key_binding_manager, &seat); + key_binding_remove_seat(key_binding_manager, &seat); + + xkb_state_unref(seat.kbd.xkb_state); + xkb_keymap_unref(seat.kbd.xkb_keymap); + + seat.kbd.xkb_state = NULL; + seat.kbd.xkb_keymap = NULL; + } + + /* us(intl) keymap */ + { + seat.kbd.xkb_keymap = xkb_keymap_new_from_names( + seat.kbd.xkb, &(struct xkb_rule_names){.layout = "us", .variant = "intl"}, + XKB_KEYMAP_COMPILE_NO_FLAGS); + + if (seat.kbd.xkb_keymap == NULL) { + /* Skip test */ + goto no_keymap; + } + + seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap); + xassert(seat.kbd.xkb_state != NULL); + + seat.kbd.xkb_compose_table = xkb_compose_table_new_from_locale( + seat.kbd.xkb, setlocale(LC_CTYPE, NULL), XKB_COMPOSE_COMPILE_NO_FLAGS); + if (seat.kbd.xkb_compose_table == NULL) + goto no_keymap; + + seat.kbd.xkb_compose_state = xkb_compose_state_new( + seat.kbd.xkb_compose_table, XKB_COMPOSE_STATE_NO_FLAGS); + if (seat.kbd.xkb_compose_state == NULL) { + xkb_compose_table_unref(seat.kbd.xkb_compose_table); + goto no_keymap; + } + + seat.kbd.mod_shift = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); + seat.kbd.mod_alt = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; + seat.kbd.mod_ctrl = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL); + seat.kbd.mod_super = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO); + seat.kbd.mod_caps = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS); + seat.kbd.mod_num = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM); + + /* Significant modifiers in the legacy keyboard protocol */ + seat.kbd.legacy_significant = 0; + if (seat.kbd.mod_shift != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift; + if (seat.kbd.mod_alt != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt; + if (seat.kbd.mod_ctrl != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl; + if (seat.kbd.mod_super != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super; + + /* Significant modifiers in the kitty keyboard protocol */ + seat.kbd.kitty_significant = seat.kbd.legacy_significant; + if (seat.kbd.mod_caps != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps; + if (seat.kbd.mod_num != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num; + + key_binding_new_for_seat(key_binding_manager, &seat); + key_binding_load_keymap(key_binding_manager, &seat); + + { + /* + * Test the compose sequence "shift+', shift+space" + * + * Should result in a double quote, but a regression + * caused it to instead emit a space. See #1987 + * + * Note: "shift+', space" also results in a double quote, + * but never regressed to a space. + */ + grid.kitty_kbd.flags[0] = KITTY_KBD_DISAMBIGUATE; + xkb_compose_state_reset(seat.kbd.xkb_compose_state); + + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 1); + + key_press_release(&seat, &term, 1337, KEY_APOSTROPHE + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + key_press_release(&seat, &term, 1337, KEY_APOSTROPHE + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + + key_press_release(&seat, &term, 1337, KEY_SPACE + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key: 34 = '"', alternate: N/A, base: N/A, mods: 2 = shift */ + const char expected_shift_apostrophe[] = "\033[34;2u"; + xassert(count == strlen(expected_shift_apostrophe)); + xassert(streq(escape, expected_shift_apostrophe)); + + key_press_release(&seat, &term, 1337, KEY_SPACE + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + + grid.kitty_kbd.flags[0] = KITTY_KBD_DISAMBIGUATE | KITTY_KBD_REPORT_ALTERNATE; + } + + key_binding_unload_keymap(key_binding_manager, &seat); + key_binding_remove_seat(key_binding_manager, &seat); + + xkb_compose_state_unref(seat.kbd.xkb_compose_state); + xkb_compose_table_unref(seat.kbd.xkb_compose_table); + + xkb_state_unref(seat.kbd.xkb_state); + xkb_keymap_unref(seat.kbd.xkb_keymap); + + seat.kbd.xkb_state = NULL; + seat.kbd.xkb_keymap = NULL; + } + +no_keymap: + xkb_context_unref(seat.kbd.xkb); + key_binding_manager_destroy(key_binding_manager); + + tll_free(wayl.terms); + close(chan[0]); + close(chan[1]); +} + static void keyboard_repeat_info(void *data, struct wl_keyboard *wl_keyboard, int32_t rate, int32_t delay) @@ -1799,7 +2334,7 @@ is_top_left(const struct terminal *term, int x, int y) { int csd_border_size = term->conf->csd.border_width; return ( - (!term->window->is_tiled_top && !term->window->is_tiled_left) && + (!term->window->is_constrained_top && !term->window->is_constrained_left) && ((term->active_surface == TERM_SURF_BORDER_LEFT && y < 10 * term->scale) || (term->active_surface == TERM_SURF_BORDER_TOP && x < (10 + csd_border_size) * term->scale))); } @@ -1809,7 +2344,7 @@ is_top_right(const struct terminal *term, int x, int y) { int csd_border_size = term->conf->csd.border_width; return ( - (!term->window->is_tiled_top && !term->window->is_tiled_right) && + (!term->window->is_constrained_top && !term->window->is_constrained_right) && ((term->active_surface == TERM_SURF_BORDER_RIGHT && y < 10 * term->scale) || (term->active_surface == TERM_SURF_BORDER_TOP && x > term->width + 1 * csd_border_size * term->scale - 10 * term->scale))); } @@ -1820,7 +2355,7 @@ is_bottom_left(const struct terminal *term, int x, int y) int csd_title_size = term->conf->csd.title_height; int csd_border_size = term->conf->csd.border_width; return ( - (!term->window->is_tiled_bottom && !term->window->is_tiled_left) && + (!term->window->is_constrained_bottom && !term->window->is_constrained_left) && ((term->active_surface == TERM_SURF_BORDER_LEFT && y > csd_title_size * term->scale + term->height) || (term->active_surface == TERM_SURF_BORDER_BOTTOM && x < (10 + csd_border_size) * term->scale))); } @@ -1831,7 +2366,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))); } @@ -1843,10 +2378,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; @@ -2614,15 +3162,8 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, case TERM_SURF_BORDER_RIGHT: case TERM_SURF_BORDER_TOP: case TERM_SURF_BORDER_BOTTOM: { - static const enum xdg_toplevel_resize_edge map[] = { - [TERM_SURF_BORDER_LEFT] = XDG_TOPLEVEL_RESIZE_EDGE_LEFT, - [TERM_SURF_BORDER_RIGHT] = XDG_TOPLEVEL_RESIZE_EDGE_RIGHT, - [TERM_SURF_BORDER_TOP] = XDG_TOPLEVEL_RESIZE_EDGE_TOP, - [TERM_SURF_BORDER_BOTTOM] = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM, - }; - if (button == BTN_LEFT && state == WL_POINTER_BUTTON_STATE_PRESSED) { - enum xdg_toplevel_resize_edge resize_type; + enum xdg_toplevel_resize_edge resize_type = XDG_TOPLEVEL_RESIZE_EDGE_NONE; int x = seat->mouse.x; int y = seat->mouse.y; @@ -2635,11 +3176,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/key-binding.c b/key-binding.c index 1c131e72..a2883ed5 100644 --- a/key-binding.c +++ b/key-binding.c @@ -11,14 +11,24 @@ #include "terminal.h" #include "util.h" #include "wayland.h" +#include "xkbcommon-vmod.h" #include "xmalloc.h" +struct vmod_map { + const char *name; + xkb_mod_mask_t virtual_mask; + xkb_mod_mask_t real_mask; +}; + struct key_set { struct key_binding_set public; const struct config *conf; const struct seat *seat; size_t conf_ref_count; + + /* Virtual to real modifier mappings */ + struct vmod_map vmods[8]; }; typedef tll(struct key_set) bind_set_list_t; @@ -44,6 +54,50 @@ key_binding_manager_destroy(struct key_binding_manager *mgr) free(mgr); } +static void +initialize_vmod_mappings(struct key_set *set) +{ + if (set->seat == NULL || set->seat->kbd.xkb_keymap == NULL) + return; + + set->vmods[0].name = XKB_VMOD_NAME_ALT; + set->vmods[1].name = XKB_VMOD_NAME_HYPER; + set->vmods[2].name = XKB_VMOD_NAME_LEVEL3; + set->vmods[3].name = XKB_VMOD_NAME_LEVEL5; + set->vmods[4].name = XKB_VMOD_NAME_META; + set->vmods[5].name = XKB_VMOD_NAME_NUM; + set->vmods[6].name = XKB_VMOD_NAME_SCROLL; + set->vmods[7].name = XKB_VMOD_NAME_SUPER; + + struct xkb_state *scratch_state = xkb_state_new(set->seat->kbd.xkb_keymap); + xassert(scratch_state != NULL); + + for (size_t i = 0; i < ALEN(set->vmods); i++) { + xkb_mod_index_t virt_idx = xkb_keymap_mod_get_index( + set->seat->kbd.xkb_keymap, set->vmods[i].name); + + if (virt_idx != XKB_MOD_INVALID) { + xkb_mod_mask_t vmask = 1 << virt_idx; + xkb_state_update_mask(scratch_state, vmask, 0, 0, 0, 0, 0); + set->vmods[i].real_mask = xkb_state_serialize_mods( + scratch_state, XKB_STATE_MODS_DEPRESSED) & ~vmask; + set->vmods[i].virtual_mask = vmask; + + LOG_DBG("%s: 0x%04x -> 0x%04x", + set->vmods[i].name, + set->vmods[i].virtual_mask, + set->vmods[i].real_mask); + } else { + set->vmods[i].virtual_mask = 0; + set->vmods[i].real_mask = 0; + + LOG_DBG("%s: virtual modifier not available", set->vmods[i].name); + } + } + + xkb_state_unref(scratch_state); +} + void key_binding_new_for_seat(struct key_binding_manager *mgr, const struct seat *seat) @@ -67,6 +121,7 @@ key_binding_new_for_seat(struct key_binding_manager *mgr, }; tll_push_back(mgr->binding_sets, set); + initialize_vmod_mappings(&tll_back(mgr->binding_sets)); LOG_DBG("new (seat): set=%p, seat=%p, conf=%p, ref-count=1", (void *)&tll_back(mgr->binding_sets), @@ -107,6 +162,7 @@ key_binding_new_for_conf(struct key_binding_manager *mgr, }; tll_push_back(mgr->binding_sets, set); + initialize_vmod_mappings(&tll_back(mgr->binding_sets)); load_keymap(&tll_back(mgr->binding_sets)); @@ -405,18 +461,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 +502,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 +561,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 +602,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 +613,10 @@ key_binding_load_keymap(struct key_binding_manager *mgr, tll_foreach(mgr->binding_sets, it) { struct key_set *set = &it->item; - if (set->seat == seat) + if (set->seat == seat) { + initialize_vmod_mappings(set); load_keymap(set); + } } } diff --git a/key-binding.h b/key-binding.h index f42dbc48..c4a04e99 100644 --- a/key-binding.h +++ b/key-binding.h @@ -41,6 +41,13 @@ enum bind_action_normal { BIND_ACTION_PROMPT_NEXT, BIND_ACTION_UNICODE_INPUT, BIND_ACTION_QUIT, + BIND_ACTION_REGEX_LAUNCH, + BIND_ACTION_REGEX_COPY, + BIND_ACTION_THEME_SWITCH_1, + BIND_ACTION_THEME_SWITCH_2, + BIND_ACTION_THEME_SWITCH_DARK, + BIND_ACTION_THEME_SWITCH_LIGHT, + BIND_ACTION_THEME_TOGGLE, /* Mouse specific actions - i.e. they require a mouse coordinate */ BIND_ACTION_SCROLLBACK_UP_MOUSE, @@ -54,7 +61,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_THEME_TOGGLE + 1, BIND_ACTION_COUNT = BIND_ACTION_SELECT_ROW + 1, }; @@ -82,6 +89,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/main.c b/main.c index 973cbae4..9db77d0c 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,19 +45,31 @@ fdm_sigint(struct fdm *fdm, int signo, void *data) return true; } -static const char * -version_and_features(void) +struct sigusr_context { + struct terminal *term; + struct server *server; +}; + +static bool +fdm_sigusr(struct fdm *fdm, int signo, void *data) { - static char buf[256]; - snprintf(buf, sizeof(buf), - "version: %s %cpgo %cime %cgraphemes %ctoplevel-icon %cassertions", - FOOT_VERSION, - feature_pgo() ? '+' : '-', - feature_ime() ? '+' : '-', - feature_graphemes() ? '+' : '-', - feature_xdg_toplevel_icon() ? '+' : '-', - feature_assertions() ? '+' : '-'); - return buf; + xassert(signo == SIGUSR1 || signo == SIGUSR2); + + struct sigusr_context *ctx = data; + + if (ctx->server != NULL) { + if (signo == SIGUSR1) + server_global_theme_switch_to_dark(ctx->server); + else + server_global_theme_switch_to_light(ctx->server); + } else { + if (signo == SIGUSR1) + term_theme_switch_to_dark(ctx->term); + else + term_theme_switch_to_light(ctx->term); + } + + return true; } static void @@ -73,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" @@ -174,6 +186,7 @@ sanitize_signals(void) enum { PTY_OPTION = CHAR_MAX + 1, + TOPLEVEL_TAG_OPTION = CHAR_MAX + 2, }; int @@ -203,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'}, @@ -274,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)) { @@ -378,7 +396,7 @@ main(int argc, char *const *argv) break; case 'v': - printf("foot %s\n", version_and_features()); + print_version_and_features("foot "); return EXIT_SUCCESS; case 'h': @@ -406,7 +424,7 @@ main(int argc, char *const *argv) argv += optind; } - LOG_INFO("%s", version_and_features()); + LOG_INFO("%s", version_and_features); { struct utsname name; @@ -516,7 +534,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); @@ -551,10 +568,10 @@ main(int argc, char *const *argv) char *_cwd = NULL; if (cwd == NULL) { - errno = 0; size_t buf_len = 1024; do { _cwd = xrealloc(_cwd, buf_len); + errno = 0; if (getcwd(_cwd, buf_len) == NULL && errno != ERANGE) { LOG_ERRNO("failed to get current working directory"); goto out; @@ -587,6 +604,7 @@ main(int argc, char *const *argv) } shm_set_max_pool_size(conf.tweak.max_shm_pool_size); + shm_set_min_stride_alignment(conf.tweak.min_stride_alignment); if ((fdm = fdm_init()) == NULL) goto out; @@ -625,6 +643,17 @@ main(int argc, char *const *argv) goto out; } + struct sigusr_context sigusr_context = { + .term = term, + .server = server, + }; + + if (!fdm_signal_add(fdm, SIGUSR1, &fdm_sigusr, &sigusr_context) || + !fdm_signal_add(fdm, SIGUSR2, &fdm_sigusr, &sigusr_context)) + { + goto out; + } + struct sigaction sig_ign = {.sa_handler = SIG_IGN}; sigemptyset(&sig_ign.sa_mask); if (sigaction(SIGHUP, &sig_ign, NULL) < 0 || @@ -660,6 +689,8 @@ out: wayl_destroy(wayl); key_binding_manager_destroy(key_binding_manager); reaper_destroy(reaper); + fdm_signal_del(fdm, SIGUSR1); + fdm_signal_del(fdm, SIGUSR2); fdm_signal_del(fdm, SIGTERM); fdm_signal_del(fdm, SIGINT); fdm_destroy(fdm); diff --git a/meson.build b/meson.build index 0c7f5656..a0e602bb 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.20.2', + version: '1.26.1', license: 'MIT', meson_version: '>=0.59.0', default_options: [ @@ -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 ') @@ -25,6 +30,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 ') + add_project_arguments('-DSIGABBREV_NP', language: 'c') +endif + utmp_backend = get_option('utmp-backend') if utmp_backend == 'auto' host_os = host_machine.system() @@ -47,7 +58,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 @@ -132,7 +143,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') @@ -145,8 +156,12 @@ 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.0.1', '<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') @@ -169,14 +184,18 @@ 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 +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 @@ -219,6 +238,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', @@ -251,7 +277,7 @@ vtlib = static_library( 'osc.c', 'osc.h', 'sixel.c', 'sixel.h', 'vt.c', 'vt.h', - builtin_terminfo, emoji_variation_sequences, + builtin_terminfo, srgb_funcs, wl_proto_src + wl_proto_headers, version, dependencies: [libepoll, pixman, fcft, tllist, wayland_client, xkb, utf8proc], @@ -263,6 +289,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, @@ -293,7 +320,7 @@ executable( 'commands.c', 'commands.h', 'extract.c', 'extract.h', 'fdm.c', 'fdm.h', - 'foot-features.h', + 'foot-features.c', 'foot-features.h', 'ime.c', 'ime.h', 'input.c', 'input.h', 'key-binding.c', 'key-binding.h', @@ -312,7 +339,8 @@ executable( 'url-mode.c', 'url-mode.h', 'user-notification.c', 'user-notification.h', 'wayland.c', 'wayland.h', 'shm-formats.h', - wl_proto_src + wl_proto_headers, version, + 'xkbcommon-vmod.h', + srgb_funcs, wl_proto_src + wl_proto_headers, version, dependencies: [math, threads, libepoll, pixman, wayland_client, wayland_cursor, xkb, fontconfig, utf8proc, tllist, fcft], link_with: pgolib, @@ -321,7 +349,7 @@ executable( executable( 'footclient', 'client.c', 'client-protocol.h', - 'foot-features.h', + 'foot-features.c', 'foot-features.h', 'macros.h', 'util.h', version, @@ -413,7 +441,6 @@ 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(), 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/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 @@ - - - org.codeberg.dnkl.foot - MIT - MIT - dnkl - foot - The fast, lightweight and minimalistic Wayland terminal emulator. - -
    -
  • Fast
  • -
  • Lightweight, in dependencies, on-disk and in-memory
  • -
  • Wayland native
  • -
  • DE agnostic
  • -
  • Server/daemon mode
  • -
  • User configurable font fallback
  • -
  • On-the-fly font resize
  • -
  • On-the-fly DPI font size adjustment
  • -
  • Scrollback search
  • -
  • Keyboard driven URL detection
  • -
  • Color emoji support
  • -
  • IME (via text-input-v3)
  • -
  • Multi-seat
  • -
  • True Color (24bpp)
  • -
  • Styled and colored underlines
  • -
  • Synchronized Updates support
  • -
  • Sixel image support
  • -
-
- - - Foot with sixel graphics - https://codeberg.org/dnkl/foot/media/branch/master/doc/sixel-wow.png - - - - - - - - - - - - - - - - - - - - org.codeberg.dnkl.foot.desktop - https://codeberg.org/dnkl/foot - https://codeberg.org/dnkl/foot/issues - -
diff --git a/osc.c b/osc.c index e335dc61..4dc47172 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; } @@ -510,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; @@ -522,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 @@ -610,7 +615,6 @@ verify_kitty_id_is_valid(const char *id) } UNIGNORE_WARNINGS - static void kitty_notification(struct terminal *term, char *string) { @@ -1135,6 +1139,134 @@ 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 forced_width = 0; + + char *ctx = NULL; + for (char *param = strtok_r(parameters, ":", &ctx); + param != NULL; + param = strtok_r(NULL, ":", &ctx)) + { + /* All parameters are on the form X=value, where X is always + exactly one character */ + if (param[0] == '\0' || param[1] != '=') + continue; + + char *value = ¶m[2]; + + switch (param[0]) { + case 'w': { + errno = 0; + char *end = NULL; + unsigned long w = strtoul(value, &end, 10); + + if (*end == '\0' && errno == 0 && w <= 7) { + forced_width = (int)w; + break; + } else + LOG_ERR("OSC-66: invalid 'w' value, ignoring"); + break; + } + + case 's': + case 'n': + case 'd': + case 'v': + LOG_WARN("OSC-66: unsupported: '%c' parameter, ignoring", param[0]); + break; + } + } + + const size_t len = c32len(wchars); + + if (forced_width == 0) { + /* + * w=0 means we split the text up as we'd normally do... Since + * we don't support any other parameters of the text-sizing + * protocol, that means we just process the string as if it + * has been printed without this OSC. + */ + for (size_t i = 0; i < len; i++) + term_process_and_print_non_ascii(term, wchars[i]); + free(wchars); + return; + } + + size_t max_cp_width = 0; + size_t all_cp_width = 0; + + for (size_t i = 0; i < len; i++) { + const size_t cp_width = c32width(wchars[i]); + all_cp_width += cp_width; + max_cp_width = max(max_cp_width, cp_width); + } + + size_t calculated_width = 0; + switch (term->conf->tweak.grapheme_width_method) { + case GRAPHEME_WIDTH_WCSWIDTH: calculated_width = all_cp_width; break; + case GRAPHEME_WIDTH_MAX: calculated_width = max_cp_width; break; + case GRAPHEME_WIDTH_DOUBLE: calculated_width = min(max_cp_width, 2); break; + } + + const size_t width = forced_width == 0 ? calculated_width : forced_width; + + LOG_DBG("len=%zu, forced=%d, calculated=%zu, using=%zu", + len, forced_width, calculated_width, width); + +#if 0 + if (len == 1 && calculated_width == forced_width) { + /* + * Optimization: if there's a single codepoint, and either + * w=0, or the 'w' matches the calculated width, print + * codepoint directly instead of creating a combining + * character. + */ + term_print(term, wchars[0], width); + free(wchars); + return; + } +#endif + + uint32_t key = composed_key_from_chars(wchars, len); + + const struct composed *composed = composed_lookup_without_collision( + term->composed, &key, wchars, len - 1, wchars[len - 1], forced_width); + + if (composed == NULL) { + struct composed *new_cc = xmalloc(sizeof(*new_cc)); + new_cc->chars = wchars; + new_cc->count = len; + new_cc->key = key; + new_cc->width = width; + new_cc->forced_width = forced_width; + + term->composed_count++; + composed_insert(&term->composed, new_cc); + composed = new_cc; + } else if (composed->width == width) { + free(wchars); + } + + term_print( + term, CELL_COMB_CHARS_LO + composed->key, + composed->forced_width > 0 ? composed->forced_width : composed->width, + false); +} + void osc_dispatch(struct terminal *term) { @@ -1328,15 +1460,17 @@ 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 (!have_alpha) + alpha = term_theme_get(term)->alpha; - if (changed) { - wayl_win_alpha_changed(term->window); - term_font_subpixel_changed(term); - } + 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; @@ -1348,12 +1482,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; } @@ -1371,6 +1503,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; @@ -1378,10 +1514,11 @@ osc_dispatch(struct terminal *term) case 104: { /* Reset Color Number 'c' (whole table if no parameter) */ + const struct color_theme *theme = term_theme_get(term); + if (string[0] == '\0') { LOG_DBG("resetting all colors"); - for (size_t i = 0; i < ALEN(term->colors.table); i++) - term->colors.table[i] = term->conf->colors.table[i]; + memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); term_damage_view(term); } @@ -1403,7 +1540,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); } @@ -1416,16 +1553,20 @@ 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_theme_get(term); + term->colors.fg = theme->fg; term_damage_color(term, COLOR_DEFAULT, 0); break; case 111: { /* Reset default text background color */ LOG_DBG("resetting background color"); - 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_theme_get(term); + bool alpha_changed = term->colors.alpha != theme->alpha; + + term->colors.bg = theme->bg; + term->colors.alpha = theme->alpha; if (alpha_changed) { wayl_win_alpha_changed(term->window); @@ -1437,24 +1578,37 @@ osc_dispatch(struct terminal *term) break; } - case 112: + 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; + + const struct color_theme *theme = term_theme_get(term); + term->colors.cursor_fg = theme->cursor.text; + term->colors.cursor_bg = theme->cursor.cursor; + + if (term->conf->colors_dark.use_custom.cursor) { + term->colors.cursor_fg |= 1u << 31; + term->colors.cursor_bg |= 1u << 31; + } + term_damage_cursor(term); break; + } - case 117: + case 117: { LOG_DBG("resetting selection background color"); - term->colors.selection_bg = term->conf->colors.selection_bg; - term->colors.use_custom_selection = term->conf->colors.use_custom.selection; - break; - case 119: - LOG_DBG("resetting selection foreground color"); - term->colors.selection_fg = term->conf->colors.selection_fg; - term->colors.use_custom_selection = term->conf->colors.use_custom.selection; + const struct color_theme *theme = term_theme_get(term); + term->colors.selection_bg = theme->selection_bg; break; + } + + case 119: { + LOG_DBG("resetting selection foreground color"); + + const struct color_theme *theme = term_theme_get(term); + term->colors.selection_fg = theme->selection_fg; + break; + } case 133: /* diff --git a/pgo/pgo.c b/pgo/pgo.c index 24154277..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) { @@ -101,6 +103,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 @@ -127,6 +130,12 @@ render_worker_thread(void *_ctx) return 0; } +bool +wayl_do_linear_blending(const struct wayland *wayl, const struct config *conf) +{ + return false; +} + struct extraction_context * extract_begin(enum selection_kind kind, bool strip_trailing_empty) { @@ -194,9 +203,13 @@ void urls_reset(struct terminal *term) {} void shm_unref(struct buffer *buf) {} void shm_chain_free(struct buffer_chain *chain) {} +enum shm_bit_depth shm_chain_bit_depth(const struct buffer_chain *chain) { return SHM_BITS_8; } struct buffer_chain * -shm_chain_new(struct wl_shm *shm, bool scrollable, size_t pix_instances) +shm_chain_new( + struct wayland *wayl, bool scrollable, size_t pix_instances, + enum shm_bit_depth desired_bit_depth, + void (*release_cb)(struct buffer *buf, void *data), void *cb_data) { return NULL; } diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..f5fc08a2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[tool.pyright] +strict = ['scripts'] + +[tool.mypy] +files = '$MYPY_CONFIG_FILE_DIR/scripts' +strict = true + +[tool.codespell] +skip = 'pyproject.toml,./subprojects,./pkg,./src,./bld,foot.info,./unicode,./venv' +ignore-regex = 'terminfo capability `rin`|\* Simon Ser|\* \[zar\]\(https://codeberg.org/zar\)|iterm theme|iterm.toml|iterm/OneHalfDark.itermcolors' \ No newline at end of file diff --git a/quirks.c b/quirks.c index 9769f1ff..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,12 +83,4 @@ is_sway(void) return is_sway; } - -void -quirk_sway_subsurface_unmap(struct terminal *term) -{ - 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 b1791a90..c47133b3 100644 --- a/render.c +++ b/render.c @@ -22,10 +22,7 @@ #include #include #include - -#if defined(HAVE_XDG_TOPLEVEL_ICON) #include -#endif #include @@ -37,13 +34,13 @@ #include "config.h" #include "cursor-shape.h" #include "grid.h" -#include "hsl.h" #include "ime.h" #include "quirks.h" #include "search.h" #include "selection.h" #include "shm.h" #include "sixel.h" +#include "srgb.h" #include "url-mode.h" #include "util.h" #include "xmalloc.h" @@ -232,58 +229,95 @@ attrs_to_font(const struct terminal *term, const struct attributes *attrs) return term->fonts[idx]; } -static inline pixman_color_t -color_hex_to_pixman_with_alpha(uint32_t color, uint16_t alpha) +static pixman_color_t +color_hex_to_pixman_srgb(uint32_t color, uint16_t alpha) { return (pixman_color_t){ - .red = ((color >> 16 & 0xff) | (color >> 8 & 0xff00)) * alpha / 0xffff, - .green = ((color >> 8 & 0xff) | (color >> 0 & 0xff00)) * alpha / 0xffff, - .blue = ((color >> 0 & 0xff) | (color << 8 & 0xff00)) * alpha / 0xffff, - .alpha = alpha, + .alpha = alpha, /* Consider alpha linear already? */ + .red = srgb_decode_8_to_16((color >> 16) & 0xff), + .green = srgb_decode_8_to_16((color >> 8) & 0xff), + .blue = srgb_decode_8_to_16((color >> 0) & 0xff), }; } static inline pixman_color_t -color_hex_to_pixman(uint32_t color) +color_hex_to_pixman_with_alpha(uint32_t color, uint16_t alpha, bool srgb) +{ + pixman_color_t ret; + + if (srgb) + ret = color_hex_to_pixman_srgb(color, alpha); + else { + ret = (pixman_color_t){ + .red = ((color >> 16 & 0xff) | (color >> 8 & 0xff00)), + .green = ((color >> 8 & 0xff) | (color >> 0 & 0xff00)), + .blue = ((color >> 0 & 0xff) | (color << 8 & 0xff00)), + .alpha = alpha, + }; + } + + ret.red = (uint32_t)ret.red * alpha / 0xffff; + ret.green = (uint32_t)ret.green * alpha / 0xffff; + ret.blue = (uint32_t)ret.blue * alpha / 0xffff; + + return ret; +} + +static inline pixman_color_t +color_hex_to_pixman(uint32_t color, bool srgb) { /* Count on the compiler optimizing this */ - return color_hex_to_pixman_with_alpha(color, 0xffff); + return color_hex_to_pixman_with_alpha(color, 0xffff, srgb); +} + +static inline int i_lerp(int from, int to, float t) { + return from + (to - from) * t; } static inline uint32_t -color_decrease_luminance(uint32_t color) +color_blend_towards(uint32_t from, uint32_t to, float amount) { - uint32_t alpha = color & 0xff000000; - int hue, sat, lum; - rgb_to_hsl(color, &hue, &sat, &lum); - return alpha | hsl_to_rgb(hue, sat, lum / 1.5); + if (unlikely(amount == 0)) + return from; + float t = 1 - 1/amount; + + uint32_t alpha = from & 0xff000000; + uint8_t r = i_lerp((from>>16)&0xff, (to>>16)&0xff, t); + uint8_t g = i_lerp((from>>8)&0xff, (to>>8)&0xff, t); + uint8_t b = i_lerp((from>>0)&0xff, (to>>0)&0xff, t); + + return alpha | (r<<16) | (g<<8) | (b<<0); } static inline uint32_t color_dim(const struct terminal *term, uint32_t color) { const struct config *conf = term->conf; - const uint8_t custom_dim = conf->colors.use_custom.dim; + const uint8_t custom_dim = conf->colors_dark.use_custom.dim; - if (likely(custom_dim == 0)) - return color_decrease_luminance(color); + if (unlikely(custom_dim != 0)) { + for (size_t i = 0; i < 8; i++) { + if (((custom_dim >> i) & 1) == 0) + continue; - for (size_t i = 0; i < 8; i++) { - if (((custom_dim >> i) & 1) == 0) - continue; + if (term->colors.table[0 + i] == color) { + /* "Regular" color, return the corresponding "dim" */ + return conf->colors_dark.dim[i]; + } - if (term->colors.table[0 + i] == color) { - /* "Regular" color, return the corresponding "dim" */ - return conf->colors.dim[i]; - } - - else if (term->colors.table[8 + i] == color) { - /* "Bright" color, return the corresponding "regular" */ - return term->colors.table[i]; + else if (term->colors.table[8 + i] == color) { + /* "Bright" color, return the corresponding "regular" */ + return term->colors.table[i]; + } } } - return color_decrease_luminance(color); + const struct color_theme *theme = term_theme_get(term); + + return color_blend_towards( + color, + theme->dim_blend_towards == DIM_BLEND_TOWARDS_BLACK ? 0x00000000 : 0x00ffffff, + conf->dim.amount); } static inline uint32_t @@ -301,11 +335,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 @@ -568,32 +598,28 @@ 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 { + 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); - } } 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); + *text_color = color_hex_to_pixman(term->colors.bg, gamma_correct); + *cursor_color = color_hex_to_pixman(term->colors.fg, gamma_correct); } } @@ -604,7 +630,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, + wayl_do_linear_blending(term->wl, term->conf)); if (unlikely(!term->kbd_focus)) { switch (term->conf->cursor.unfocused_style) { @@ -647,12 +674,18 @@ draw_cursor(const struct terminal *term, const struct cell *cell, draw_underline_cursor(term, pix, font, &cursor_color, x, y, cols); } break; + + case CURSOR_HOLLOW: + if (likely(term->cursor_blink.state == CURSOR_BLINK_ON)) + draw_hollow_block(term, pix, &cursor_color, x, y, cols); + break; } } static int -render_cell(struct terminal *term, pixman_image_t *pix, 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) @@ -666,101 +699,140 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag const int x = term->margins.left + col * width; const int y = term->margins.top + row_no * height; - bool is_selected = cell->attrs.selected; - uint32_t _fg = 0; uint32_t _bg = 0; uint16_t alpha = 0xffff; + const bool is_selected = cell->attrs.selected; + + /* Use cell specific color, if set, otherwise the default colors (possible reversed) */ + switch (cell->attrs.fg_src) { + case COLOR_RGB: + _fg = cell->attrs.fg; + break; + + case COLOR_BASE16: + case COLOR_BASE256: + xassert(cell->attrs.fg < ALEN(term->colors.table)); + _fg = term->colors.table[cell->attrs.fg]; + break; + + case COLOR_DEFAULT: + _fg = term->reverse ? term->colors.bg : term->colors.fg; + break; + } + + switch (cell->attrs.bg_src) { + case COLOR_RGB: + _bg = cell->attrs.bg; + break; + + case COLOR_BASE16: + case COLOR_BASE256: + xassert(cell->attrs.bg < ALEN(term->colors.table)); + _bg = term->colors.table[cell->attrs.bg]; + break; + + case COLOR_DEFAULT: + _bg = term->reverse ? term->colors.fg : term->colors.bg; + break; + } + + if (unlikely(is_selected)) { + const uint32_t cell_fg = _fg; + const uint32_t cell_bg = _bg; + + const bool custom_fg = term->colors.selection_fg >> 24 == 0; + const bool custom_bg = term->colors.selection_bg >> 24 == 0; + const bool custom_both = custom_fg && custom_bg; + + if (custom_both) { + _fg = term->colors.selection_fg; + _bg = term->colors.selection_bg; + } else if (custom_bg) { + _bg = term->colors.selection_bg; + _fg = cell->attrs.reverse ? cell_bg : cell_fg; + } else if (custom_fg) { + _fg = term->colors.selection_fg; + _bg = cell->attrs.reverse ? cell_fg : cell_bg; + } else { + _bg = cell_fg; + _fg = cell_bg; + } + + if (unlikely(_fg == _bg)) { + /* Invert bg when selected/highlighted text has same fg/bg */ + _bg = ~_bg; + alpha = 0xffff; + } - if (is_selected && term->colors.use_custom_selection) { - _fg = term->colors.selection_fg; - _bg = term->colors.selection_bg; } else { - /* Use cell specific color, if set, otherwise the default colors (possible reversed) */ - switch (cell->attrs.fg_src) { - case COLOR_RGB: - _fg = cell->attrs.fg; - break; - - case COLOR_BASE16: - case COLOR_BASE256: - xassert(cell->attrs.fg < ALEN(term->colors.table)); - _fg = term->colors.table[cell->attrs.fg]; - break; - - case COLOR_DEFAULT: - _fg = term->reverse ? term->colors.bg : term->colors.fg; - break; - } - - switch (cell->attrs.bg_src) { - case COLOR_RGB: - _bg = cell->attrs.bg; - break; - - case COLOR_BASE16: - case COLOR_BASE256: - xassert(cell->attrs.bg < ALEN(term->colors.table)); - _bg = term->colors.table[cell->attrs.bg]; - break; - - case COLOR_DEFAULT: - _bg = term->reverse ? term->colors.fg : term->colors.bg; - break; - } - - if (cell->attrs.reverse ^ is_selected) { + if (unlikely(cell->attrs.reverse)) { uint32_t swap = _fg; _fg = _bg; _bg = swap; } - else if (cell->attrs.bg_src == COLOR_DEFAULT) { - 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; + else if (!term->window->is_fullscreen && term->colors.alpha != 0xffff) { + switch (term->conf->colors_dark.alpha_mode) { + case ALPHA_MODE_DEFAULT: { + if (cell->attrs.bg_src == COLOR_DEFAULT) { + alpha = term->colors.alpha; + } + break; } - } - } - if (unlikely(is_selected && _fg == _bg)) { - /* Invert bg when selected/highlighted text has same fg/bg */ - _bg = ~_bg; - alpha = 0xffff; + case ALPHA_MODE_MATCHING: { + if (cell->attrs.bg_src == COLOR_DEFAULT || + ((cell->attrs.bg_src == COLOR_BASE16 || + cell->attrs.bg_src == COLOR_BASE256) && + term->colors.table[cell->attrs.bg] == term->colors.bg) || + (cell->attrs.bg_src == COLOR_RGB && + cell->attrs.bg == term->colors.bg)) + { + alpha = term->colors.alpha; + } + break; + } + + case ALPHA_MODE_ALL: { + alpha = term->colors.alpha; + break; + } + } + } else { + /* + * Note: disable transparency when fullscreened. + * + * This is because the wayland protocol mandates no screen + * content is shown behind the fullscreened window. + * + * The _intent_ of the specification is that a black (or + * other static color) should be used as background. + * + * There's a bit of gray area however, and some + * compositors have chosen to interpret the specification + * in a way that allows wallpapers to be seen through a + * fullscreen window. + * + * Given that a) the intent of the specification, and b) + * we don't know what the compositor will do, we simply + * disable transparency while in fullscreen. + * + * To see why, consider what happens if we keep our + * transparency. For example, if the background color is + * white, and alpha is 0.5, then the window will be drawn + * in a shade of gray while fullscreened. + * + * See + * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/116 + * for a discussion on whether transparent, fullscreen + * windows should be allowed in some way or not. + * + * NOTE: if changing this, also update render_margin() + */ + xassert(alpha == 0xffff); + } } if (cell->attrs.dim) @@ -769,10 +841,11 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag _fg = color_brighten(term, _fg); if (cell->attrs.blink && term->blink.state == BLINK_OFF) - _fg = color_decrease_luminance(_fg); + _fg = color_blend_towards(_fg, 0x00000000, term->conf->dim.amount); - pixman_color_t fg = color_hex_to_pixman(_fg); - pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha); + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + pixman_color_t fg = color_hex_to_pixman(_fg, gamma_correct); + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); struct fcft_font *font = attrs_to_font(term, &cell->attrs); const struct composed *composed = NULL; @@ -869,11 +942,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 +968,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; } } } @@ -950,8 +1030,10 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag 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)) @@ -972,10 +1054,10 @@ 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)) { + 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( @@ -993,9 +1075,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 +1099,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); @@ -1055,12 +1141,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: @@ -1087,24 +1173,27 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag if (unlikely(cell->attrs.url)) { pixman_color_t url_color = color_hex_to_pixman( - term->conf->colors.use_custom.url - ? term->conf->colors.url - : term->colors.table[3] - ); + term->conf->colors_dark.use_custom.url + ? term->conf->colors_dark.url + : term->colors.table[3], + gamma_correct); draw_underline(term, pix, font, &url_color, x, y, cell_cols); } draw_cursor: - if (has_cursor && (term->cursor_style != CURSOR_BLOCK || !term->kbd_focus)) - draw_cursor(term, cell, font, pix, &fg, &bg, x, y, cell_cols); + if (has_cursor && (term->cursor_style != CURSOR_BLOCK || !term->kbd_focus)) { + const pixman_color_t bg_without_alpha = color_hex_to_pixman(_bg, gamma_correct); + draw_cursor(term, cell, font, pix, &fg, &bg_without_alpha, x, y, cell_cols); + } pixman_image_set_clip_region32(pix, NULL); return cell_cols; } static void -render_row(struct terminal *term, pixman_image_t *pix, 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); @@ -1114,7 +1203,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); + 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)); @@ -1145,6 +1235,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 = 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; @@ -1153,7 +1244,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, @@ -1580,8 +1671,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; @@ -1633,6 +1723,8 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, if (unlikely(term->is_searching)) return; + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + /* Adjust cursor position to viewport */ struct coord cursor; cursor = term->grid->cursor.point; @@ -1737,12 +1829,12 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, if (!seat->ime.preedit.cursor.hidden) { const struct cell *start_cell = &seat->ime.preedit.cells[0]; - pixman_color_t fg = color_hex_to_pixman(term->colors.fg); - pixman_color_t bg = color_hex_to_pixman(term->colors.bg); + pixman_color_t fg = color_hex_to_pixman(term->colors.fg, gamma_correct); + pixman_color_t bg = color_hex_to_pixman(term->colors.bg, gamma_correct); pixman_color_t cursor_color, text_color; cursor_colors_for_cell( - term, start_cell, &fg, &bg, &cursor_color, &text_color); + term, start_cell, &fg, &bg, &cursor_color, &text_color, gamma_correct); int x = term->margins.left + (col_idx + start) * term->cell_width; int y = term->margins.top + row_idx * term->cell_height; @@ -1773,12 +1865,14 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, row->cells[col_idx + i] = real_cells[i]; free(real_cells); + const int damage_x = term->margins.left + col_idx * term->cell_width; + const int damage_y = term->margins.top + row_idx * term->cell_height; + const int damage_w = cells_used * term->cell_width; + const int damage_h = term->cell_height; + wl_surface_damage_buffer( term->window->surface.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 @@ -1881,10 +1975,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; } @@ -1899,8 +1989,9 @@ 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; case OVERLAY_NONE: @@ -1919,7 +2010,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() */ @@ -2102,6 +2193,7 @@ render_worker_thread(void *_ctx) sem_wait(start); struct buffer *buf = term->render.workers.buf; + bool frame_done = false; /* Translate offset-relative cursor row to view-relative */ @@ -2122,8 +2214,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; @@ -2139,6 +2229,56 @@ render_worker_thread(void *_ctx) case -2: return 0; + + case -3: { + if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) + clock_gettime(CLOCK_MONOTONIC, &term->render.workers.preapplied_damage.start); + + mtx_lock(&term->render.workers.preapplied_damage.lock); + buf = term->render.workers.preapplied_damage.buf; + xassert(buf != NULL); + + if (likely(term->render.last_buf != NULL)) { + mtx_unlock(&term->render.workers.preapplied_damage.lock); + + pixman_region32_t dmg; + pixman_region32_init(&dmg); + + if (buf->age == 0) + ; /* No need to do anything */ + else if (buf->age == 1) + pixman_region32_copy(&dmg, + &term->render.last_buf->dirty[0]); + else + pixman_region32_init_rect(&dmg, 0, 0, buf->width, + buf->height); + + pixman_image_set_clip_region32(buf->pix[my_id], &dmg); + pixman_image_composite32(PIXMAN_OP_SRC, + term->render.last_buf->pix[my_id], + NULL, buf->pix[my_id], 0, 0, 0, 0, 0, + 0, buf->width, buf->height); + + pixman_region32_fini(&dmg); + + buf->age = 0; + shm_unref(term->render.last_buf); + shm_addref(buf); + term->render.last_buf = buf; + + mtx_lock(&term->render.workers.preapplied_damage.lock); + } + + term->render.workers.preapplied_damage.buf = NULL; + cnd_signal(&term->render.workers.preapplied_damage.cond); + mtx_unlock(&term->render.workers.preapplied_damage.lock); + + if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) + clock_gettime(CLOCK_MONOTONIC, &term->render.workers.preapplied_damage.stop); + + frame_done = true; + break; + } } } }; @@ -2146,6 +2286,22 @@ render_worker_thread(void *_ctx) return -1; } +void +render_wait_for_preapply_damage(struct terminal *term) +{ + if (!term->render.preapply_last_frame_damage) + return; + if (term->render.workers.preapplied_damage.buf == NULL) + return; + + mtx_lock(&term->render.workers.preapplied_damage.lock); + while (term->render.workers.preapplied_damage.buf != NULL) { + cnd_wait(&term->render.workers.preapplied_damage.cond, + &term->render.workers.preapplied_damage.lock); + } + mtx_unlock(&term->render.workers.preapplied_damage.lock); +} + struct csd_data get_csd_data(const struct terminal *term, enum csd_surface surf_idx) { @@ -2165,16 +2321,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 @@ -2199,9 +2360,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; @@ -2243,13 +2404,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 = wayl_do_linear_blending(term->wl, term->conf); uint16_t alpha = _bg >> 24 | (_bg >> 24 << 8); - pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha); + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &bg, 1, &(pixman_rectangle16_t){0, 0, buf->width, buf->height}); - pixman_color_t fg = color_hex_to_pixman(_fg); + pixman_color_t fg = color_hex_to_pixman(_fg, gamma_correct); const int x_ofs = term->font_x_ofs; const size_t len = c32len(text); @@ -2296,7 +2458,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, @@ -2346,10 +2508,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); @@ -2383,8 +2545,11 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, if (info->width == 0 || info->height == 0) return; + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + { - 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); } @@ -2440,12 +2605,13 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, uint32_t _color = conf->csd.color.border_set ? conf->csd.color.border : conf->csd.color.title_set ? conf->csd.color.title : - 0xffu << 24 | term->conf->colors.fg; + 0xffu << 24 | term->conf->colors_dark.fg; if (!term->visual_focus) _color = color_dim(term, _color); uint16_t alpha = _color >> 24 | (_color >> 24 << 8); - pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha); + pixman_color_t color = + color_hex_to_pixman_with_alpha(_color, alpha, gamma_correct); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &color, 1, @@ -2456,9 +2622,10 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, } static pixman_color_t -get_csd_button_fg_color(const struct config *conf) +get_csd_button_fg_color(const struct terminal *term) { - uint32_t _color = conf->colors.bg; + const struct config *conf = term->conf; + uint32_t _color = conf->colors_dark.bg; uint16_t alpha = 0xffff; if (conf->csd.color.buttons_set) { @@ -2466,13 +2633,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, wayl_do_linear_blending(term->wl, term->conf)); } static void render_csd_button_minimize(struct terminal *term, struct buffer *buf) { - pixman_color_t color = get_csd_button_fg_color(term->conf); + pixman_color_t color = get_csd_button_fg_color(term); pixman_image_t *src = pixman_image_create_solid_fill(&color); const int max_height = buf->height / 3; @@ -2500,7 +2668,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; @@ -2532,7 +2700,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; @@ -2572,7 +2740,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; @@ -2702,7 +2870,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 && @@ -2710,7 +2878,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 && @@ -2718,7 +2886,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 && @@ -2743,14 +2911,14 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx, if (!term->visual_focus) _color = color_dim(term, _color); - pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha); + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha, gamma_correct); render_csd_part(term, surf->surf, buf, info->width, info->height, &color); switch (surf_idx) { case CSD_SURF_MINIMIZE: render_csd_button_minimize(term, buf); break; case CSD_SURF_MAXIMIZE: render_csd_button_maximize(term, buf); break; case CSD_SURF_CLOSE: render_csd_button_close(term, buf); break; - break; default: BUG("unhandled surface type: %u", (unsigned)surf_idx); @@ -2800,7 +2968,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]); @@ -2818,13 +2986,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; } @@ -2945,16 +3108,16 @@ 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)); 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( @@ -2988,7 +3151,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, @@ -3021,14 +3184,6 @@ force_full_repaint(struct terminal *term, struct buffer *buf) static void reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old) { - static int counter = 0; - static bool have_warned = false; - if (!have_warned && ++counter > 5) { - LOG_WARN("compositor is not releasing buffers immediately; " - "expect lower rendering performance"); - have_warned = true; - } - if (new->age > 1) { memcpy(new->data, old->data, new->height * new->stride); return; @@ -3159,7 +3314,18 @@ grid_render(struct terminal *term) if (term->shutdown.in_progress) return; - struct timespec start_time, start_double_buffering = {0}, stop_double_buffering = {0}; + struct timespec start_time; + struct timespec start_wait_preapply = {0}, stop_wait_preapply = {0}; + struct timespec start_double_buffering = {0}, stop_double_buffering = {0}; + + /* Might be a thread doing pre-applied damage */ + if (unlikely(term->render.preapply_last_frame_damage && + term->render.workers.preapplied_damage.buf != NULL)) + { + clock_gettime(CLOCK_MONOTONIC, &start_wait_preapply); + render_wait_for_preapply_damage(term); + clock_gettime(CLOCK_MONOTONIC, &stop_wait_preapply); + } if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) clock_gettime(CLOCK_MONOTONIC, &start_time); @@ -3168,15 +3334,14 @@ 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); 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 || @@ -3193,9 +3358,27 @@ grid_render(struct terminal *term) xassert(term->render.last_buf->width == buf->width); xassert(term->render.last_buf->height == buf->height); + if (++term->render.frames_since_last_immediate_release > 10) { + static bool have_warned = false; + + if (!term->render.preapply_last_frame_damage && + term->conf->tweak.preapply_damage && + term->render.workers.count > 0) + { + LOG_INFO("enabling pre-applied frame damage"); + term->render.preapply_last_frame_damage = true; + } else if (!have_warned && !term->render.preapply_last_frame_damage) { + LOG_WARN("compositor is not releasing buffers immediately; " + "expect lower rendering performance"); + have_warned = true; + } + } + clock_gettime(CLOCK_MONOTONIC, &start_double_buffering); reapply_old_damage(term, buf, term->render.last_buf); clock_gettime(CLOCK_MONOTONIC, &stop_double_buffering); + } else if (!term->render.preapply_last_frame_damage) { + term->render.frames_since_last_immediate_release = 0; } if (term->render.last_buf != NULL) { @@ -3423,27 +3606,40 @@ grid_render(struct terminal *term) struct timespec end_time; clock_gettime(CLOCK_MONOTONIC, &end_time); + struct timespec wait_time; + timespec_sub(&stop_wait_preapply, &start_wait_preapply, &wait_time); + struct timespec render_time; timespec_sub(&end_time, &start_time, &render_time); struct timespec double_buffering_time; timespec_sub(&stop_double_buffering, &start_double_buffering, &double_buffering_time); + struct timespec preapply_damage; + timespec_sub(&term->render.workers.preapplied_damage.stop, + &term->render.workers.preapplied_damage.start, + &preapply_damage); + struct timespec total_render_time; timespec_add(&render_time, &double_buffering_time, &total_render_time); + timespec_add(&wait_time, &total_render_time, &total_render_time); switch (term->conf->tweak.render_timer) { case RENDER_TIMER_LOG: case RENDER_TIMER_BOTH: LOG_INFO( "frame rendered in %lds %9ldns " - "(%lds %9ldns rendering, %lds %9ldns double buffering)", + "(%lds %9ldns wait, %lds %9ldns rendering, %lds %9ldns double buffering) not included: %lds %ldns pre-apply damage", (long)total_render_time.tv_sec, total_render_time.tv_nsec, + (long)wait_time.tv_sec, + wait_time.tv_nsec, (long)render_time.tv_sec, render_time.tv_nsec, (long)double_buffering_time.tv_sec, - double_buffering_time.tv_nsec); + double_buffering_time.tv_nsec, + (long)preapply_damage.tv_sec, + preapply_damage.tv_nsec); break; case RENDER_TIMER_OSD: @@ -3586,7 +3782,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); @@ -3598,24 +3794,26 @@ render_search_box(struct terminal *term) const bool is_match = term->search.match_len == text_len; const bool custom_colors = is_match - ? term->conf->colors.use_custom.search_box_match - : term->conf->colors.use_custom.search_box_no_match; + ? term->conf->colors_dark.use_custom.search_box_match + : term->conf->colors_dark.use_custom.search_box_no_match; /* Background - yellow on empty/match, red on mismatch (default) */ + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); const pixman_color_t color = color_hex_to_pixman( is_match ? (custom_colors - ? term->conf->colors.search_box.match.bg + ? term->conf->colors_dark.search_box.match.bg : term->colors.table[3]) : (custom_colors - ? term->conf->colors.search_box.no_match.bg - : term->colors.table[1])); + ? term->conf->colors_dark.search_box.no_match.bg + : term->colors.table[1]), + gamma_correct); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &color, 1, &(pixman_rectangle16_t){width - visible_width, 0, visible_width, height}); - pixman_color_t transparent = color_hex_to_pixman_with_alpha(0, 0); + pixman_color_t transparent = color_hex_to_pixman_with_alpha(0, 0, gamma_correct); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &transparent, 1, &(pixman_rectangle16_t){0, 0, width - visible_width, height}); @@ -3625,12 +3823,14 @@ render_search_box(struct terminal *term) const int x_ofs = term->font_x_ofs; int x = x_left; int y = margin; + pixman_color_t fg = color_hex_to_pixman( custom_colors ? (is_match - ? term->conf->colors.search_box.match.fg - : term->conf->colors.search_box.no_match.fg) - : term->colors.table[0]); + ? term->conf->colors_dark.search_box.match.fg + : term->conf->colors_dark.search_box.no_match.fg) + : term->colors.table[0], + gamma_correct); /* Move offset we start rendering at, to ensure the cursor is visible */ for (size_t i = 0, cell_idx = 0; i <= term->search.cursor; cell_idx += widths[i], i++) { @@ -3786,8 +3986,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, @@ -4048,13 +4247,13 @@ 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.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++) { @@ -4179,7 +4378,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, @@ -4200,6 +4399,7 @@ delayed_reflow_of_normal_grid(struct terminal *term) term->interactive_resizing.old_hide_cursor = false; /* Invalidate render pointers */ + render_wait_for_preapply_damage(term); shm_unref(term->render.last_buf); term->render.last_buf = NULL; term->render.last_cursor.row = NULL; @@ -4295,17 +4495,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 += (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 */ - *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 +4543,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; } @@ -4371,7 +4598,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)*/ @@ -4384,18 +4611,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))); } @@ -4422,8 +4653,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: @@ -4465,24 +4698,27 @@ 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; 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"); @@ -4653,7 +4889,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); } @@ -4744,6 +4980,7 @@ damage_view: tll_free(term->normal.scroll_damage); tll_free(term->alt.scroll_damage); + render_wait_for_preapply_damage(term); shm_unref(term->render.last_buf); term->render.last_buf = NULL; term_damage_view(term); @@ -4798,8 +5035,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); @@ -5007,7 +5245,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"); @@ -5041,7 +5278,6 @@ render_refresh_icon(struct terminal *term) xdg_toplevel_icon_v1_destroy(icon); term->render.icon.last_update = now; -#endif } void @@ -5143,3 +5379,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..e6674ab2 100644 --- a/render.h +++ b/render.h @@ -47,3 +47,6 @@ struct csd_data { }; struct csd_data get_csd_data(const struct terminal *term, enum csd_surface surf_idx); + +void render_buffer_release_callback(struct buffer *buf, void *data); +void render_wait_for_preapply_damage(struct terminal *term); diff --git a/scripts/benchmark.py b/scripts/benchmark.py index 5483dac1..fe820d9b 100755 --- a/scripts/benchmark.py +++ b/scripts/benchmark.py @@ -11,7 +11,7 @@ import termios from datetime import datetime -def main(): +def main() -> None: parser = argparse.ArgumentParser() parser.add_argument('files', type=argparse.FileType('rb'), nargs='+') parser.add_argument('--iterations', type=int, default=20) @@ -24,12 +24,12 @@ def main(): termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))) - times = {name: [] for name in [f.name for f in args.files]} + times: dict[str, list[float]] = {name: [] for name in [f.name for f in args.files]} for f in args.files: bench_bytes = f.read() - for i in range(args.iterations): + for _ in range(args.iterations): start = datetime.now() sys.stdout.buffer.write(bench_bytes) stop = datetime.now() @@ -48,4 +48,4 @@ def main(): if __name__ == '__main__': - sys.exit(main()) + main() diff --git a/scripts/generate-alt-random-writes.py b/scripts/generate-alt-random-writes.py index 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 28b31b57..c10373d3 100755 --- a/scripts/generate-builtin-terminfo.py +++ b/scripts/generate-builtin-terminfo.py @@ -1,14 +1,12 @@ #!/usr/bin/env python3 import argparse +import os import re -import sys - -from typing import Dict, Union class Capability: - def __init__(self, name: str, value: Union[bool, int, str]): + def __init__(self, name: str, value: bool | int | str): self._name = name self._value = value @@ -17,25 +15,37 @@ class Capability: return self._name @property - def value(self) -> Union[bool, int, str]: + def value(self) -> bool | int | str: return self._value - def __lt__(self, other): + def __lt__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented return self._name < other._name - def __le__(self, other): + def __le__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented return self._name <= other._name - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented return self._name == other._name - def __ne__(self, other): - return self._name != other._name + def __ne__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented + return bool(self._name != other._name) - def __gt__(self, other): - return self._name > other._name + def __gt__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented + return bool(self._name > other._name) - def __ge__(self, other): + def __ge__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented return self._name >= other._name @@ -53,7 +63,7 @@ class StringCapability(Capability): # 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' @@ -85,7 +95,7 @@ class Fragment: def __init__(self, name: str, description: str): self._name = name self._description = description - self._caps = {} + self._caps = dict[str, Capability]() @property def name(self) -> str: @@ -96,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')) @@ -120,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(?P[-+\w@]+)\|(?P.+?),)|' @@ -147,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 @@ -166,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) @@ -185,8 +201,9 @@ def main(): entry.add_capability(StringCapability('TN', target_entry_name)) entry.add_capability(StringCapability('name', target_entry_name)) entry.add_capability(IntCapability('RGB', 8)) # 8 bits per channel + entry.add_capability(StringCapability('query-os-name', os.uname().sysname)) - terminfo_parts = [] + terminfo_parts = list[str]() for cap in sorted(entry.caps.values()): name = cap.name value = str(cap.value) @@ -210,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..57e881c7 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): 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 new file mode 100755 index 00000000..a6aa0f4a --- /dev/null +++ b/scripts/srgb.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 + +import argparse +import math + + +# Note: we use a pure gamma 2.2 function, rather than the piece-wise +# sRGB transfer function, since that is what all compositors do. + +def srgb_to_linear(f: float) -> float: + assert(f >= 0 and f <= 1.0) + return math.pow(f, 2.2) + + +def linear_to_srgb(f: float) -> float: + return math.pow(f, 1 / 2.2) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument('c_output', type=argparse.FileType('w')) + parser.add_argument('h_output', type=argparse.FileType('w')) + opts = parser.parse_args() + + linear_table: list[int] = [] + + for i in range(256): + linear_table.append(int(srgb_to_linear(float(i) / 255) * 65535 + 0.5)) + + + opts.h_output.write("#pragma once\n") + opts.h_output.write("#include \n") + opts.h_output.write("\n") + opts.h_output.write('/* 8-bit input, 16-bit output */\n') + opts.h_output.write("extern const uint16_t srgb_decode_8_to_16_table[256];") + + opts.h_output.write('\n') + opts.h_output.write('static inline uint16_t\n') + opts.h_output.write('srgb_decode_8_to_16(uint8_t v)\n') + opts.h_output.write('{\n') + opts.h_output.write(' return srgb_decode_8_to_16_table[v];\n') + opts.h_output.write('}\n') + + opts.h_output.write('\n') + opts.h_output.write('/* 8-bit input, 8-bit output */\n') + opts.h_output.write("extern const uint8_t srgb_decode_8_to_8_table[256];\n") + + opts.h_output.write('\n') + opts.h_output.write('static inline uint8_t\n') + opts.h_output.write('srgb_decode_8_to_8(uint8_t v)\n') + opts.h_output.write('{\n') + opts.h_output.write(' return srgb_decode_8_to_8_table[v];\n') + opts.h_output.write('}\n') + + opts.c_output.write('#include "srgb.h"\n') + opts.c_output.write('\n') + + opts.c_output.write("const uint16_t srgb_decode_8_to_16_table[256] = {\n") + for i in range(256): + opts.c_output.write(f' {linear_table[i]},\n') + opts.c_output.write('};\n') + + opts.c_output.write("const uint8_t srgb_decode_8_to_8_table[256] = {\n") + for i in range(256): + opts.c_output.write(f' {linear_table[i] >> 8},\n') + opts.c_output.write('};\n') + + +if __name__ == '__main__': + main() diff --git a/search.c b/search.c index eaf8c34e..5228bf61 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) @@ -1265,6 +1270,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)) { @@ -1388,27 +1416,17 @@ search_input(struct seat *seat, struct terminal *term, bool update_search_result = false; bool redraw = false; - /* Key bindings */ + /* + * Key bindings + */ + + /* Match untranslated symbols */ tll_foreach(bindings->search, it) { const struct key_binding *bind = &it->item; - /* Match translated symbol */ - if (bind->k.sym == sym && - bind->mods == (mods & ~consumed)) { - - if (execute_binding(seat, term, bind, serial, - &update_search_result, &search_direction, - &redraw)) - { - goto update_search; - } - return; - } - - if (bind->mods != mods) + if (bind->mods != mods || bind->mods == 0) continue; - /* Match untranslated symbols */ for (size_t i = 0; i < raw_count; i++) { if (bind->k.sym == raw_syms[i]) { if (execute_binding(seat, term, bind, serial, @@ -1420,8 +1438,32 @@ search_input(struct seat *seat, struct terminal *term, return; } } + } + + /* Match translated symbol */ + tll_foreach(bindings->search, it) { + const struct key_binding *bind = &it->item; + + if (bind->k.sym == sym && + bind->mods == (mods & ~consumed)) { + + if (execute_binding(seat, term, bind, serial, + &update_search_result, &search_direction, + &redraw)) + { + goto update_search; + } + return; + } + } + + /* Match raw key code */ + tll_foreach(bindings->search, it) { + const struct key_binding *bind = &it->item; + + if (bind->mods != mods || bind->mods == 0) + continue; - /* Match raw key code */ tll_foreach(bind->k.key_codes, code) { if (code->item == key) { if (execute_binding(seat, term, bind, serial, @@ -1442,7 +1484,8 @@ search_input(struct seat *seat, struct terminal *term, count = xkb_compose_state_get_utf8( seat->kbd.xkb_compose_state, (char *)buf, sizeof(buf)); xkb_compose_state_reset(seat->kbd.xkb_compose_state); - } else if (compose_status == XKB_COMPOSE_CANCELLED) { + } else if (compose_status == XKB_COMPOSE_CANCELLED || + compose_status == XKB_COMPOSE_COMPOSING) { count = 0; } else { count = xkb_state_key_get_utf8( diff --git a/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; diff --git a/server.c b/server.c index 78d98d53..25963325 100644 --- a/server.c +++ b/server.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -23,11 +24,13 @@ #include "wayland.h" #include "xmalloc.h" +#define NON_ZERO_OPT (INT_MIN / 7) + struct client; struct terminal_instance; struct server { - const struct config *conf; + struct config *conf; struct fdm *fdm; struct reaper *reaper; struct wayland *wayl; @@ -153,10 +156,61 @@ fdm_client(struct fdm *fdm, int fd, int events, void *data) xassert(events & EPOLLIN); if (client->instance != NULL) { - uint8_t dummy[128]; - ssize_t count = read(fd, dummy, sizeof(dummy)); - LOG_WARN("client unexpectedly sent %zd bytes", count); - return true; /* TODO: shutdown instead? */ + struct client_ipc_hdr ipc_hdr; + ssize_t count = read(fd, &ipc_hdr, sizeof(ipc_hdr)); + + if (count != sizeof(ipc_hdr)) { + LOG_WARN("client unexpectedly sent %zd bytes", count); + return true; /* TODO: shutdown instead? */ + } + + switch (ipc_hdr.ipc_code) { + case FOOT_IPC_SIGUSR: { + xassert(ipc_hdr.size == sizeof(struct client_ipc_sigusr)); + + struct client_ipc_sigusr sigusr; + count = read(fd, &sigusr, sizeof(sigusr)); + if (count < 0) { + LOG_ERRNO("failed to read SIGUSR IPC data from client"); + return true; /* TODO: shutdown instead? */ + } + + if ((size_t)count != sizeof(sigusr)) { + LOG_ERR("failed to read SIGUSR IPC data from client"); + return true; /* TODO: shutdown instead? */ + } + + switch (sigusr.signo) { + case SIGUSR1: + term_theme_switch_to_dark(client->instance->terminal); + break; + + case SIGUSR2: + term_theme_switch_to_light(client->instance->terminal); + break; + + default: + LOG_ERR( + "client sent bad SIGUSR number: %d " + "(expected SIGUSR1=%d or SIGUSR2=%d)", + sigusr.signo, SIGUSR1, SIGUSR2); + break; + } + + return true; + } + + default: + LOG_WARN( + "client sent unrecognized IPC (0x%04x), ignoring %hhu bytes", + ipc_hdr.ipc_code, ipc_hdr.size); + + /* TODO: slightly broken, since not all data is guaranteed + to be readable yet */ + uint8_t dummy[ipc_hdr.size]; + (void)!!read(fd, dummy, ipc_hdr.size); + return true; + } } if (client->buffer.data == NULL) { @@ -211,6 +265,12 @@ fdm_client(struct fdm *fdm, int fd, int events, void *data) return true; } + if (tll_length(server->wayl->monitors) == 0) { + LOG_ERR("no monitors available for new terminal"); + client_send_exit_code(client, -26); + goto shutdown; + } + /* All initialization data received - time to instantiate a terminal! */ xassert(client->instance == NULL); @@ -468,7 +528,7 @@ prepare_socket(int fd) } int const socket_options[] = { SO_DOMAIN, SO_ACCEPTCONN, SO_TYPE }; - int const socket_options_values[] = { AF_UNIX, 1, SOCK_STREAM}; + int const socket_options_values[] = { AF_UNIX, NON_ZERO_OPT, SOCK_STREAM}; char const * const socket_options_names[] = { "SO_DOMAIN", "SO_ACCEPTCONN", "SO_TYPE" }; xassert(ALEN(socket_options) == ALEN(socket_options_values)); @@ -483,6 +543,8 @@ prepare_socket(int fd) LOG_ERRNO("failed to read socket option from passed file descriptor"); return false; } + if (socket_options_values[i] == NON_ZERO_OPT && socket_option) + socket_option = NON_ZERO_OPT; if (socket_option != socket_options_values[i]) { LOG_ERR("wrong socket value for socket option '%s' on passed file descriptor", socket_options_names[i]); @@ -494,7 +556,7 @@ prepare_socket(int fd) } struct server * -server_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, +server_init(struct config *conf, struct fdm *fdm, struct reaper *reaper, struct wayland *wayl) { int fd; @@ -606,3 +668,23 @@ server_destroy(struct server *server) unlink(server->sock_path); free(server); } + +void +server_global_theme_switch_to_dark(struct server *server) +{ + server->conf->initial_color_theme = COLOR_THEME_DARK; + tll_foreach(server->clients, it) + term_theme_switch_to_dark(it->item->instance->terminal); + tll_foreach(server->terminals, it) + term_theme_switch_to_dark(it->item->terminal); +} + +void +server_global_theme_switch_to_light(struct server *server) +{ + server->conf->initial_color_theme = COLOR_THEME_LIGHT; + tll_foreach(server->clients, it) + term_theme_switch_to_light(it->item->instance->terminal); + tll_foreach(server->terminals, it) + term_theme_switch_to_light(it->item->terminal); +} diff --git a/server.h b/server.h index 50797540..683ad74d 100644 --- a/server.h +++ b/server.h @@ -6,6 +6,9 @@ #include "wayland.h" struct server; -struct server *server_init(const struct config *conf, struct fdm *fdm, +struct server *server_init(struct config *conf, struct fdm *fdm, struct reaper *reaper, struct wayland *wayl); void server_destroy(struct server *server); + +void server_global_theme_switch_to_dark(struct server *server); +void server_global_theme_switch_to_light(struct server *server); diff --git a/shm-formats.h b/shm-formats.h index 3ada8266..a73ba1f2 100644 --- a/shm-formats.h +++ b/shm-formats.h @@ -117,5 +117,22 @@ static const struct shm_formats { {WL_SHM_FORMAT_ARGB16161616, "ARGB16161616"}, {WL_SHM_FORMAT_ABGR16161616, "ABGR16161616"}, #endif +#if WAYLAND_VERSION_MAJOR > 1 || WAYLAND_VERSION_MINOR >= 23 + {WL_SHM_FORMAT_C1, "C1"}, + {WL_SHM_FORMAT_C2, "C2"}, + {WL_SHM_FORMAT_C4, "C4"}, + {WL_SHM_FORMAT_D1, "D1"}, + {WL_SHM_FORMAT_D2, "D2"}, + {WL_SHM_FORMAT_D4, "D4"}, + {WL_SHM_FORMAT_D8, "D8"}, + {WL_SHM_FORMAT_R1, "R1"}, + {WL_SHM_FORMAT_R2, "R2"}, + {WL_SHM_FORMAT_R4, "R4"}, + {WL_SHM_FORMAT_R10, "R10"}, + {WL_SHM_FORMAT_R12, "R12"}, + {WL_SHM_FORMAT_AVUY8888, "AVUY8888"}, + {WL_SHM_FORMAT_XVUY8888, "XVUY8888"}, + {WL_SHM_FORMAT_P030, "P030"}, +#endif }; #endif diff --git a/shm.c b/shm.c index bbf6f933..5c1573ad 100644 --- a/shm.c +++ b/shm.c @@ -13,7 +13,6 @@ #include -#include #include #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; @@ -82,9 +84,11 @@ struct buffer_private { struct buffer_pool *pool; off_t offset; /* Offset into memfd where data begins */ size_t size; - bool with_alpha; bool scrollable; + + void (*release_cb)(struct buffer *buf, void *data); + void *cb_data; }; struct buffer_chain { @@ -92,6 +96,12 @@ struct buffer_chain { struct wl_shm *shm; size_t pix_instances; bool scrollable; + + pixman_format_code_t pixman_fmt; + enum wl_shm_format shm_format; + + void (*release_cb)(struct buffer *buf, void *data); + void *cb_data; }; static tll(struct buffer_private *) deferred; @@ -107,6 +117,12 @@ shm_set_max_pool_size(off_t _max_pool_size) max_pool_size = _max_pool_size; } +void +shm_set_min_stride_alignment(size_t _min_stride_alignment) +{ + min_stride_alignment = _min_stride_alignment; +} + static void buffer_destroy_dont_close(struct buffer *buf) { @@ -115,6 +131,7 @@ buffer_destroy_dont_close(struct buffer *buf) if (buf->pix[i] != NULL) pixman_image_unref(buf->pix[i]); } + if (buf->wl_buf != NULL) wl_buffer_destroy(buf->wl_buf); @@ -217,6 +234,10 @@ buffer_release(void *data, struct wl_buffer *wl_buffer) xassert(found); if (!found) LOG_WARN("deferred delete: buffer not on the 'deferred' list"); + } else { + if (buffer->release_cb != NULL) { + buffer->release_cb(&buffer->public, buffer->cb_data); + } } } @@ -224,7 +245,6 @@ static const struct wl_buffer_listener buffer_listener = { .release = &buffer_release, }; -#if __SIZEOF_POINTER__ == 8 static size_t page_size(void) { @@ -241,7 +261,6 @@ page_size(void) xassert(size > 0); return size; } -#endif static bool instantiate_offset(struct buffer_private *buf, off_t new_offset) @@ -262,7 +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 ? WL_SHM_FORMAT_ARGB8888 : WL_SHM_FORMAT_XRGB8888); + buf->chain->shm_format); if (wl_buf == NULL) { LOG_ERR("failed to create SHM buffer"); @@ -272,9 +291,10 @@ instantiate_offset(struct buffer_private *buf, off_t new_offset) /* One pixman image for each worker thread (do we really need multiple?) */ for (size_t i = 0; i < buf->public.pix_instances; i++) { pix[i] = pixman_image_create_bits_no_clear( - buf->with_alpha ? PIXMAN_a8r8g8b8 : PIXMAN_x8r8g8b8, + buf->chain->pixman_fmt, buf->public.width, buf->public.height, (uint32_t *)mmapped, buf->public.stride); + if (pix[i] == NULL) { LOG_ERR("failed to create pixman image"); goto err; @@ -306,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); /* @@ -326,7 +345,14 @@ 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]); + chain->pixman_fmt, widths[i]); + + if (min_stride_alignment > 0) { + const size_t m = min_stride_alignment; + stride[i] = (stride[i] + m - 1) / m * m; + } + + xassert(min_stride_alignment == 0 || stride[i] % min_stride_alignment == 0); sizes[i] = stride[i] * heights[i]; total_size += sizes[i]; } @@ -368,9 +394,11 @@ get_new_buffers(struct buffer_chain *chain, size_t count, goto err; } + const size_t page_sz = page_size(); + #if __SIZEOF_POINTER__ == 8 off_t offset = chain->scrollable && max_pool_size > 0 - ? (max_pool_size / 4) & ~(page_size() - 1) + ? (max_pool_size / 4) & ~(page_sz - 1) : 0; off_t memfd_size = chain->scrollable && max_pool_size > 0 ? max_pool_size @@ -380,7 +408,8 @@ get_new_buffers(struct buffer_chain *chain, size_t count, off_t memfd_size = total_size; #endif - xassert(chain->scrollable || (offset == 0 && memfd_size == total_size)); + /* Page align */ + memfd_size = (memfd_size + page_sz - 1) & ~(page_sz - 1); LOG_DBG("memfd-size: %lu, initial offset: %lu", memfd_size, offset); @@ -412,6 +441,9 @@ get_new_buffers(struct buffer_chain *chain, size_t count, memfd_size = total_size; chain->scrollable = false; + /* Page align */ + memfd_size = (memfd_size + page_sz - 1) & ~(page_sz - 1); + if (ftruncate(pool_fd, memfd_size) < 0) { LOG_ERRNO("failed to set size of SHM backing memory file"); goto err; @@ -477,11 +509,12 @@ 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], .scrollable = chain->scrollable, + .release_cb = chain->release_cb, + .cb_data = chain->cb_data, }; if (!instantiate_offset(buf, offset)) { @@ -547,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 " @@ -564,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); @@ -582,9 +613,9 @@ 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 */ @@ -608,7 +639,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]); @@ -617,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; } @@ -959,14 +990,90 @@ shm_unref(struct buffer *_buf) } struct buffer_chain * -shm_chain_new(struct wl_shm *shm, bool scrollable, size_t pix_instances) +shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, + enum shm_bit_depth desired_bit_depth, + void (*release_cb)(struct buffer *buf, void *data), void *cb_data) { + pixman_format_code_t pixman_fmt = PIXMAN_a8r8g8b8; + enum wl_shm_format shm_fmt = WL_SHM_FORMAT_ARGB8888; + + static bool have_logged = false; + static bool have_logged_10_fallback = false; + +#if defined(HAVE_PIXMAN_RGBA_16) + static bool have_logged_16_fallback = false; + + if (desired_bit_depth == SHM_BITS_16) { + if (wayl->shm_have_abgr161616) { + pixman_fmt = PIXMAN_a16b16g16r16; + shm_fmt = WL_SHM_FORMAT_ABGR16161616; + + if (!have_logged) { + have_logged = true; + LOG_INFO("using 16-bit BGR surfaces"); + } + } else { + if (!have_logged_16_fallback) { + have_logged_16_fallback = true; + + LOG_WARN( + "16-bit surfaces requested, but compositor does not " + "implement ABGR161616+XBGR161616"); + } + } + } +#endif + + if (desired_bit_depth >= SHM_BITS_10 && pixman_fmt == PIXMAN_a8r8g8b8) { + if (wayl->shm_have_argb2101010) { + pixman_fmt = PIXMAN_a2r10g10b10; + shm_fmt = WL_SHM_FORMAT_ARGB2101010; + + if (!have_logged) { + have_logged = true; + LOG_INFO("using 10-bit RGB surfaces"); + } + } + + else if (wayl->shm_have_abgr2101010) { + pixman_fmt = PIXMAN_a2b10g10r10; + shm_fmt = WL_SHM_FORMAT_ABGR2101010; + + if (!have_logged) { + have_logged = true; + LOG_INFO("using 10-bit BGR surfaces"); + } + } + + else { + if (!have_logged_10_fallback) { + have_logged_10_fallback = true; + + LOG_WARN( + "10-bit surfaces requested, but compositor does not " + "implement ARGB2101010+XRGB2101010, or " + "ABGR2101010+XBGR2101010"); + } + } + } else { + if (!have_logged) { + have_logged = true; + LOG_INFO("using 8-bit RGB surfaces"); + } + } + struct buffer_chain *chain = xmalloc(sizeof(*chain)); *chain = (struct buffer_chain){ .bufs = tll_init(), - .shm = shm, + .shm = wayl->shm, .pix_instances = pix_instances, .scrollable = scrollable, + + .pixman_fmt = pixman_fmt, + .shm_format = shm_fmt, + + .release_cb = release_cb, + .cb_data = cb_data, }; return chain; } @@ -986,3 +1093,17 @@ shm_chain_free(struct buffer_chain *chain) free(chain); } + +enum shm_bit_depth +shm_chain_bit_depth(const struct buffer_chain *chain) +{ + const pixman_format_code_t fmt = chain->pixman_fmt; + + return fmt == PIXMAN_a8r8g8b8 + ? SHM_BITS_8 +#if defined(HAVE_PIXMAN_RGBA_16) + : fmt == PIXMAN_a16b16g16r16 + ? SHM_BITS_16 +#endif + : SHM_BITS_10; +} diff --git a/shm.h b/shm.h index b4b075ca..c58a8531 100644 --- a/shm.h +++ b/shm.h @@ -9,6 +9,9 @@ #include +#include "config.h" +#include "wayland.h" + struct damage; struct buffer { @@ -39,13 +42,20 @@ 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( - struct wl_shm *shm, bool scrollable, size_t pix_instances); + struct wayland *wayl, bool scrollable, size_t pix_instances, + enum shm_bit_depth desired_bit_depth, + void (*release_cb)(struct buffer *buf, void *data), void *cb_data); void shm_chain_free(struct buffer_chain *chain); +enum shm_bit_depth shm_chain_bit_depth(const struct buffer_chain *chain); + /* * Returns a single buffer. * @@ -55,8 +65,7 @@ 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, 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. @@ -74,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 44a5995b..187f1348 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,34 @@ 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 = wayl_do_linear_blending(term->wl, term->conf); + term->sixel.pixman_fmt = PIXMAN_a8r8g8b8; + + /* + * Use higher-precision sixel surfaces if we're using + * higher-precision window surfaces. + * + * This is to a) get more accurate colors when doing gamma-correct + * blending, and b) use the same pixman format as the main + * surfaces, for (hopefully) better performance. + * + * For now, don't support 16-bit surfaces (too much sixel logic + * that assumes 32-bit pixels). + */ + if (shm_chain_bit_depth(term->render.chains.grid) >= SHM_BITS_10) { + if (term->wl->shm_have_argb2101010) { + term->sixel.use_10bit = true; + term->sixel.pixman_fmt = PIXMAN_a2r10g10b10; + } + + else if (term->wl->shm_have_abgr2101010) { + term->sixel.use_10bit = true; + term->sixel.pixman_fmt = PIXMAN_a2b10g10r10; + } + } + + const size_t active_palette_entries = min( + ALEN(term->conf->colors_dark.sixel), term->sixel.palette_size); if (term->sixel.use_private_palette) { xassert(term->sixel.private_palette == NULL); @@ -82,21 +145,36 @@ 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, - min(sizeof(term->conf->colors.sixel), - term->sixel.palette_size * sizeof(term->sixel.private_palette[0]))); + term->sixel.private_palette, term->conf->colors_dark.sixel, + active_palette_entries * sizeof(term->sixel.private_palette[0])); + + if (term->sixel.linear_blending || term->sixel.use_10bit) { + for (size_t i = 0; i < active_palette_entries; i++) { + uint8_t r = (term->sixel.private_palette[i] >> 16) & 0xff; + uint8_t g = (term->sixel.private_palette[i] >> 8) & 0xff; + uint8_t b = (term->sixel.private_palette[i] >> 0) & 0xff; + term->sixel.private_palette[i] = color_decode_srgb(term, r, g, b); + } + } term->sixel.palette = term->sixel.private_palette; - } else { if (term->sixel.shared_palette == NULL) { term->sixel.shared_palette = xcalloc( term->sixel.palette_size, sizeof(term->sixel.shared_palette[0])); memcpy( - term->sixel.shared_palette, term->conf->colors.sixel, - min(sizeof(term->conf->colors.sixel), - term->sixel.palette_size * sizeof(term->sixel.shared_palette[0]))); + term->sixel.shared_palette, term->conf->colors_dark.sixel, + active_palette_entries * sizeof(term->sixel.shared_palette[0])); + + if (term->sixel.linear_blending || term->sixel.use_10bit) { + for (size_t i = 0; i < active_palette_entries; i++) { + uint8_t r = (term->sixel.private_palette[i] >> 16) & 0xff; + uint8_t g = (term->sixel.private_palette[i] >> 8) & 0xff; + uint8_t b = (term->sixel.private_palette[i] >> 0) & 0xff; + term->sixel.private_palette[i] = color_decode_srgb(term, r, g, b); + } + } } else { /* Shared palette - do *not* reset palette for new sixels */ } @@ -488,7 +566,7 @@ blend_new_image_over_old(const struct terminal *term, int stride = new_width * sizeof(uint32_t); uint32_t *new_data = xmalloc(stride * new_height); pixman_image_t *pix2 = pixman_image_create_bits_no_clear( - PIXMAN_a8r8g8b8, new_width, new_height, new_data, stride); + term->sixel.pixman_fmt, new_width, new_height, new_data, stride); #if defined(_DEBUG) /* Fill new image with an easy-to-recognize color (green) */ @@ -651,8 +729,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 +1025,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 +1309,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; @@ -1482,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; @@ -2006,15 +2086,14 @@ decgci(struct terminal *term, uint8_t c) } case 2: { /* RGB */ - uint8_t r = 255 * min(c1, 100) / 100; - uint8_t g = 255 * min(c2, 100) / 100; - uint8_t b = 255 * min(c3, 100) / 100; + uint16_t r = 255 * min(c1, 100) / 100; + uint16_t g = 255 * min(c2, 100) / 100; + uint16_t b = 255 * min(c3, 100) / 100; - LOG_DBG("setting palette #%d = RGB %hhu/%hhu/%hhu", + LOG_DBG("setting palette #%d = RGB %hu/%hu/%hu", term->sixel.color_idx, r, g, b); - term->sixel.palette[term->sixel.color_idx] = - 0xffu << 24 | r << 16 | g << 8 | b; + term->sixel.palette[term->sixel.color_idx] = color_decode_srgb(term, r, g, b); break; } } 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); diff --git a/terminal.c b/terminal.c index e392c36d..8eafbcbe 100644 --- a/terminal.c +++ b/terminal.c @@ -27,6 +27,7 @@ #include "commands.h" #include "config.h" #include "debug.h" +#include "emoji-variation-sequences.h" #include "extract.h" #include "grid.h" #include "ime.h" @@ -119,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 @@ -198,7 +202,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 @@ -222,7 +226,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 @@ -387,12 +391,16 @@ fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) bool term_ptmx_pause(struct terminal *term) { + if (term->ptmx < 0) + return false; return fdm_event_del(term->fdm, term->ptmx, EPOLLIN); } bool term_ptmx_resume(struct terminal *term) { + if (term->ptmx < 0) + return false; return fdm_event_add(term->fdm, term->ptmx, EPOLLIN); } @@ -714,6 +722,9 @@ initialize_render_workers(struct terminal *term) goto err_sem_destroy; } + mtx_init(&term->render.workers.preapplied_damage.lock, mtx_plain); + cnd_init(&term->render.workers.preapplied_damage.cond); + term->render.workers.threads = xcalloc( term->render.workers.count, sizeof(term->render.workers.threads[0])); @@ -986,6 +997,7 @@ struct font_load_data { const char **names; const char *attrs; + const struct fcft_font_options *options; struct fcft_font **font; }; @@ -993,7 +1005,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; } @@ -1060,14 +1073,34 @@ 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->scaling_filter = conf->tweak.fcft_filter; + options->color_glyphs.format = PIXMAN_a8r8g8b8; + options->color_glyphs.srgb_decode = + wayl_do_linear_blending(term->wl, term->conf); + + if (shm_chain_bit_depth(term->render.chains.grid) >= SHM_BITS_10) { + /* + * Use a high-res buffer type for emojis. We don't want to use + * an a2r10g0b10 type of surface, since we need more than 2 + * bits for alpha. + */ +#if defined(HAVE_PIXMAN_RGBA_16) + options->color_glyphs.format = PIXMAN_a16b16g16r16; +#else + options->color_glyphs.format = PIXMAN_rgba_float; +#endif + } struct fcft_font *fonts[4]; struct font_load_data data[4] = { - {count_regular, names_regular, attrs[0], &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}; @@ -1092,6 +1125,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]); @@ -1232,6 +1267,19 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, goto err; } + const enum shm_bit_depth desired_bit_depth = + conf->tweak.surface_bit_depth == SHM_BITS_AUTO + ? wayl_do_linear_blending(wayl, conf) ? SHM_BITS_16 : SHM_BITS_8 + : conf->tweak.surface_bit_depth; + + const struct color_theme *theme = NULL; + switch (conf->initial_color_theme) { + case COLOR_THEME_DARK: theme = &conf->colors_dark; break; + case COLOR_THEME_LIGHT: theme = &conf->colors_light; break; + case COLOR_THEME_1: BUG("COLOR_THEME_1 should not be used"); break; + case COLOR_THEME_2: BUG("COLOR_THEME_2 should not be used"); break; + } + /* Initialize configure-based terminal attributes */ *term = (struct terminal) { .fdm = fdm, @@ -1249,7 +1297,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, @@ -1265,14 +1313,14 @@ 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->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, + .fg = theme->fg, + .bg = theme->bg, + .alpha = theme->alpha, + .cursor_fg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.text, + .cursor_bg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.cursor, + .selection_fg = theme->selection_fg, + .selection_bg = theme->selection_bg, + .active_theme = conf->initial_color_theme, }, .color_stack = { .stack = NULL, @@ -1315,13 +1363,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, + 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, @@ -1368,6 +1417,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]; @@ -1402,8 +1452,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, term->conf->colors.table, sizeof(term->colors.table)); - /* Initialize the Wayland window backend */ if ((term->window = wayl_win_init(term, token)) == NULL) goto err; @@ -1463,6 +1511,9 @@ term_window_configured(struct terminal *term) if (!term->shutdown.in_progress) { xassert(term->window->is_configured); fdm_add(term->fdm, term->ptmx, EPOLLIN, &fdm_ptmx, term); + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + LOG_INFO("gamma-correct blending: %s", gamma_correct ? "enabled" : "disabled"); } } @@ -1850,6 +1901,8 @@ term_destroy(struct terminal *term) } } free(term->render.workers.threads); + mtx_destroy(&term->render.workers.preapplied_damage.lock); + cnd_destroy(&term->render.workers.preapplied_damage.cond); mtx_destroy(&term->render.workers.lock); sem_destroy(&term->render.workers.start); sem_destroy(&term->render.workers.done); @@ -2024,12 +2077,25 @@ 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; } +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) { @@ -2113,19 +2179,20 @@ term_reset(struct terminal *term, bool hard) if (!hard) return; + const struct color_theme *theme = NULL; + + switch (term->conf->initial_color_theme) { + case COLOR_THEME_DARK: theme = &term->conf->colors_dark; break; + case COLOR_THEME_LIGHT: theme = &term->conf->colors_light; break; + case COLOR_THEME_1: BUG("COLOR_THEME_1 should not be used"); break; + case COLOR_THEME_2: BUG("COLOR_THEME_2 should not be used"); break; + } + term->flash.active = false; term->blink.state = BLINK_ON; fdm_del(term->fdm, term->blink.fd); term->blink.fd = -1; - term->colors.fg = term->conf->colors.fg; - term->colors.bg = term->conf->colors.bg; - term->colors.alpha = term->conf->colors.alpha; - term->colors.cursor_fg = term->conf->cursor.color.text; - term->colors.cursor_bg = term->conf->cursor.color.cursor; - term->colors.selection_fg = term->conf->colors.selection_fg; - term->colors.selection_bg = term->conf->colors.selection_bg; - term->colors.use_custom_selection = term->conf->colors.use_custom.selection; - memcpy(term->colors.table, term->conf->colors.table, - sizeof(term->colors.table)); + 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; @@ -2726,13 +2793,11 @@ UNITTEST }, .kind = SELECTION_NONE, .auto_scroll = { - .fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK), + .fd = -1, }, }, }; - xassert(term.selection.auto_scroll.fd >= 0); - #define populate_scrollback() do { \ for (int i = 0; i < scrollback_rows; i++) { \ if (term.normal.rows[i] == NULL) { \ @@ -2822,7 +2887,7 @@ UNITTEST /* Cleanup */ tll_free(term.normal.sixel_images); - close(term.selection.auto_scroll.fd); + xassert(term.selection.auto_scroll.fd == -1); for (int i = 0; i < scrollback_rows; i++) grid_row_free(term.normal.rows[i]); free(term.normal.rows); @@ -2855,6 +2920,8 @@ term_cursor_to(struct terminal *term, int row, int col) term->grid->cursor.point.col = col; term->grid->cursor.point.row = row; + term_reset_grapheme_state(term); + term->grid->cur_row = grid_row(term->grid, row); } @@ -2871,6 +2938,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 @@ -2880,6 +2948,7 @@ term_cursor_left(struct terminal *term, int count) term->grid->cursor.point.col -= move_amount; xassert(term->grid->cursor.point.col >= 0); term->grid->cursor.lcf = false; + term_reset_grapheme_state(term); } void @@ -2889,6 +2958,7 @@ term_cursor_right(struct terminal *term, int count) term->grid->cursor.point.col += move_amount; xassert(term->grid->cursor.point.col < term->cols); term->grid->cursor.lcf = false; + term_reset_grapheme_state(term); } void @@ -3102,11 +3172,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); @@ -3114,6 +3190,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 */ @@ -3130,11 +3211,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 } @@ -3153,13 +3239,14 @@ term_carriage_return(struct terminal *term) void term_linefeed(struct terminal *term) { - term->grid->cur_row->linebreak = true; term->grid->cursor.lcf = false; if (term->grid->cursor.point.row == term->scroll_region.end - 1) term_scroll(term, 1); else term_cursor_down(term, 1); + + term_reset_grapheme_state(term); } void @@ -3340,10 +3427,13 @@ report_mouse_click(struct terminal *term, int encoded_button, int row, int col, encoded_button, col + 1, row + 1, release ? 'm' : 'M'); break; - case MOUSE_SGR_PIXELS: + case MOUSE_SGR_PIXELS: { + const int bounded_col = max(col_pixels, 0); + const int bounded_row = max(row_pixels, 0); snprintf(response, sizeof(response), "\033[<%d;%d;%d%c", - encoded_button, col_pixels + 1, row_pixels + 1, release ? 'm' : 'M'); + encoded_button, bounded_col + 1, bounded_row + 1, release ? 'm' : 'M'); break; + } case MOUSE_URXVT: snprintf(response, sizeof(response), "\033[%d;%d;%dM", @@ -3524,7 +3614,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; @@ -3615,7 +3707,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 @@ -3683,6 +3775,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"), @@ -3710,8 +3805,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; } @@ -3819,7 +3924,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}; } /* @@ -3889,7 +3994,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); @@ -3911,7 +4016,8 @@ term_print(struct terminal *term, char32_t wc, int width) } print_linewrap(term); - print_insert(term, width); + if (!insert_mode_disable) + print_insert(term, width); int col = grid->cursor.point.col; @@ -3940,9 +4046,11 @@ term_print(struct terminal *term, char32_t wc, int width) cell->wc = term->vt.last_printed = wc; cell->attrs = term->vt.attrs; - if (term->vt.osc8.uri != NULL) { - grid_row_uri_range_put( - row, col, term->vt.osc8.uri, term->vt.osc8.id); + if (unlikely(term->vt.osc8.uri != NULL)) { + for (int i = 0; i < width && (col + i) < term->cols; i++) { + grid_row_uri_range_put( + row, col + i, term->vt.osc8.uri, term->vt.osc8.id); + } switch (term->conf->url.osc8_underline) { case OSC8_UNDERLINE_ALWAYS: @@ -3983,7 +4091,7 @@ term_print(struct terminal *term, char32_t wc, int width) static void ascii_printer_generic(struct terminal *term, char32_t wc) { - term_print(term, wc, 1); + term_print(term, wc, 1, false); } static void @@ -4065,6 +4173,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 = composed != NULL ? composed->forced_width : 0; + + if (composed != NULL) { + memcpy(&new_cc->chars[1], &composed->chars[1], + (wanted_count - 2) * sizeof(new_cc->chars[0])); + } + + const int grapheme_width = + composed != NULL ? composed->width : base_width; + + switch (term->conf->tweak.grapheme_width_method) { + case GRAPHEME_WIDTH_MAX: + new_cc->width = max(grapheme_width, width); + break; + + case GRAPHEME_WIDTH_DOUBLE: + new_cc->width = min(grapheme_width + width, 2); + +#if defined(FOOT_GRAPHEME_CLUSTERING) + /* Handle VS-15 and VS-16 variation selectors */ + if (unlikely(grapheme_clustering && + (wc == 0xfe0e || wc == 0xfe0f) && + new_cc->count == 2)) + { + const struct emoji_vs *vs = + bsearch( + &(struct emoji_vs){.start = new_cc->chars[0]}, + emoji_vs, sizeof(emoji_vs) / sizeof(emoji_vs[0]), + sizeof(struct emoji_vs), + &emoji_vs_compare); + + if (vs != NULL) { + xassert(new_cc->chars[0] >= vs->start && + new_cc->chars[0] <= vs->end); + + /* Force a grapheme width of 1 for VS-15, and 2 for VS-16 */ + if (wc == 0xfe0e) { + if (vs->vs15) + new_cc->width = 1; + } else if (wc == 0xfe0f) { + if (vs->vs16) + new_cc->width = 2; + } + } + } +#endif + + break; + + case GRAPHEME_WIDTH_WCSWIDTH: + new_cc->width = grapheme_width + width; + break; + } + + term->composed_count++; + composed_insert(&term->composed, new_cc); + + wc = CELL_COMB_CHARS_LO + new_cc->key; + width = new_cc->forced_width > 0 ? new_cc->forced_width : new_cc->width; + + xassert(wc >= CELL_COMB_CHARS_LO); + xassert(wc <= CELL_COMB_CHARS_HI); + goto out; + } + } else + term_reset_grapheme_state(term); + + +out: + if (width > 0) + term_print(term, wc, width, insert_mode_disable); +} + enum term_surface term_surface_kind(const struct terminal *term, const struct wl_surface *surface) { @@ -4395,3 +4757,76 @@ term_send_size_notification(struct terminal *term) term->rows, term->cols, height, width); term_to_slave(term, buf, n); } + +void +term_theme_switch_to_dark(struct terminal *term) +{ + if (term->colors.active_theme == COLOR_THEME_DARK) + return; + + term_theme_apply(term, &term->conf->colors_dark); + term->colors.active_theme = COLOR_THEME_DARK; + + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;1n", 9); + + term_damage_view(term); + term_damage_margins(term); + render_refresh(term); +} + +void +term_theme_switch_to_light(struct terminal *term) +{ + if (term->colors.active_theme == COLOR_THEME_LIGHT) + return; + + term_theme_apply(term, &term->conf->colors_light); + term->colors.active_theme = COLOR_THEME_LIGHT; + + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;2n", 9); + + term_damage_view(term); + term_damage_margins(term); + render_refresh(term); +} + +void +term_theme_toggle(struct terminal *term) +{ + if (term->colors.active_theme == COLOR_THEME_DARK) { + term_theme_apply(term, &term->conf->colors_light); + term->colors.active_theme = COLOR_THEME_LIGHT; + + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;2n", 9); + } else { + term_theme_apply(term, &term->conf->colors_dark); + term->colors.active_theme = COLOR_THEME_DARK; + + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;1n", 9); + } + + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + + term_damage_view(term); + term_damage_margins(term); + render_refresh(term); +} + +const struct color_theme * +term_theme_get(const struct terminal *term) +{ + return term->colors.active_theme == COLOR_THEME_DARK + ? &term->conf->colors_dark + : &term->conf->colors_light; +} diff --git a/terminal.h b/terminal.h index 813510fe..5a2a57aa 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, @@ -404,7 +404,7 @@ 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; }; struct terminal { @@ -517,6 +517,7 @@ struct terminal { bool num_lock_modifier; bool bell_action_enabled; + bool report_theme_changes; /* Saved DECSET modes - we save the SET state */ struct { @@ -547,6 +548,7 @@ struct terminal { bool ime:1; bool app_sync_updates:1; bool grapheme_shaping:1; + bool report_theme_changes:1; bool size_notifications:1; @@ -704,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 */ @@ -714,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; @@ -777,6 +789,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 */ @@ -789,6 +805,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; @@ -893,7 +910,9 @@ void term_cursor_up(struct terminal *term, int count); void term_cursor_down(struct terminal *term, int count); void term_cursor_blink_update(struct terminal *term); -void term_print(struct terminal *term, char32_t wc, int width); +void term_process_and_print_non_ascii(struct terminal *term, char32_t wc); +void term_print(struct terminal *term, char32_t wc, int width, + bool insert_mode_disable); void term_fill(struct terminal *term, int row, int col, uint8_t c, size_t count, bool use_sgr_attrs); @@ -975,6 +994,11 @@ void term_enable_size_notifications(struct terminal *term); void term_disable_size_notifications(struct terminal *term); void term_send_size_notification(struct terminal *term); +void term_theme_switch_to_dark(struct terminal *term); +void term_theme_switch_to_light(struct terminal *term); +void term_theme_toggle(struct terminal *term); +const struct color_theme *term_theme_get(const struct terminal *term); + static inline void term_reset_grapheme_state(struct terminal *term) { #if defined(FOOT_GRAPHEME_CLUSTERING) diff --git a/tests/test-config.c b/tests/test-config.c index a189d440..9774cba9 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) @@ -443,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); + } } } } @@ -489,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); + } } } } @@ -504,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); @@ -511,8 +490,9 @@ 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_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); @@ -525,8 +505,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"}, @@ -543,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) */ @@ -579,6 +565,7 @@ test_section_bell(void) test_boolean(&ctx, &parse_section_bell, "urgent", &conf.bell.urgent); test_boolean(&ctx, &parse_section_bell, "notify", &conf.bell.notify); + test_boolean(&ctx, &parse_section_bell, "system", &conf.bell.system_bell); test_boolean(&ctx, &parse_section_bell, "command-focused", &conf.bell.command_focused); test_spawn_template(&ctx, &parse_section_bell, "command", @@ -646,9 +633,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); } @@ -719,64 +703,161 @@ 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_dark, "cursor", false, + &conf.colors_dark.cursor.text, + &conf.colors_dark.cursor.cursor); + + test_enum(&ctx, &parse_section_colors_dark, "alpha-mode", 3, + (const char *[]){"default", "matching", "all"}, + (int []){ALPHA_MODE_DEFAULT, ALPHA_MODE_MATCHING, ALPHA_MODE_ALL}, + (int *)&conf.colors_dark.alpha_mode); + + test_enum(&ctx, &parse_section_colors_dark, "dim-blend-towards", 2, + (const char *[]){"black", "white"}, + (int []){DIM_BLEND_TOWARDS_BLACK, DIM_BLEND_TOWARDS_WHITE}, + (int *)&conf.colors_dark.dim_blend_towards); for (size_t i = 0; i < 255; i++) { char key_name[4]; sprintf(key_name, "%zu", i); - test_color(&ctx, &parse_section_colors, key_name, false, - &conf.colors.table[i]); + test_color(&ctx, &parse_section_colors_dark, key_name, false, + &conf.colors_dark.table[i]); } - test_invalid_key(&ctx, &parse_section_colors, "256"); + test_boolean(&ctx, &parse_section_colors_dark, "blur", &conf.colors_dark.blur); + + test_invalid_key(&ctx, &parse_section_colors_dark, "256"); + + /* TODO: alpha (float in range 0-1, converted to uint16_t) */ + + config_free(&conf); +} + +static void +test_section_colors_light(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "colors-light", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_colors, "invalid-key"); + + test_color(&ctx, &parse_section_colors_light, "foreground", false, &conf.colors_light.fg); + test_color(&ctx, &parse_section_colors_light, "background", false, &conf.colors_light.bg); + test_color(&ctx, &parse_section_colors_light, "regular0", false, &conf.colors_light.table[0]); + test_color(&ctx, &parse_section_colors_light, "regular1", false, &conf.colors_light.table[1]); + test_color(&ctx, &parse_section_colors_light, "regular2", false, &conf.colors_light.table[2]); + test_color(&ctx, &parse_section_colors_light, "regular3", false, &conf.colors_light.table[3]); + test_color(&ctx, &parse_section_colors_light, "regular4", false, &conf.colors_light.table[4]); + test_color(&ctx, &parse_section_colors_light, "regular5", false, &conf.colors_light.table[5]); + test_color(&ctx, &parse_section_colors_light, "regular6", false, &conf.colors_light.table[6]); + test_color(&ctx, &parse_section_colors_light, "regular7", false, &conf.colors_light.table[7]); + test_color(&ctx, &parse_section_colors_light, "bright0", false, &conf.colors_light.table[8]); + test_color(&ctx, &parse_section_colors_light, "bright1", false, &conf.colors_light.table[9]); + test_color(&ctx, &parse_section_colors_light, "bright2", false, &conf.colors_light.table[10]); + test_color(&ctx, &parse_section_colors_light, "bright3", false, &conf.colors_light.table[11]); + test_color(&ctx, &parse_section_colors_light, "bright4", false, &conf.colors_light.table[12]); + test_color(&ctx, &parse_section_colors_light, "bright5", false, &conf.colors_light.table[13]); + test_color(&ctx, &parse_section_colors_light, "bright6", false, &conf.colors_light.table[14]); + test_color(&ctx, &parse_section_colors_light, "bright7", false, &conf.colors_light.table[15]); + test_color(&ctx, &parse_section_colors_light, "dim0", false, &conf.colors_light.dim[0]); + test_color(&ctx, &parse_section_colors_light, "dim1", false, &conf.colors_light.dim[1]); + test_color(&ctx, &parse_section_colors_light, "dim2", false, &conf.colors_light.dim[2]); + test_color(&ctx, &parse_section_colors_light, "dim3", false, &conf.colors_light.dim[3]); + test_color(&ctx, &parse_section_colors_light, "dim4", false, &conf.colors_light.dim[4]); + test_color(&ctx, &parse_section_colors_light, "dim5", false, &conf.colors_light.dim[5]); + test_color(&ctx, &parse_section_colors_light, "dim6", false, &conf.colors_light.dim[6]); + test_color(&ctx, &parse_section_colors_light, "dim7", false, &conf.colors_light.dim[7]); + test_color(&ctx, &parse_section_colors_light, "selection-foreground", false, &conf.colors_light.selection_fg); + test_color(&ctx, &parse_section_colors_light, "selection-background", false, &conf.colors_light.selection_bg); + test_color(&ctx, &parse_section_colors_light, "urls", false, &conf.colors_light.url); + test_two_colors(&ctx, &parse_section_colors_light, "jump-labels", false, + &conf.colors_light.jump_label.fg, + &conf.colors_light.jump_label.bg); + test_two_colors(&ctx, &parse_section_colors_light, "scrollback-indicator", false, + &conf.colors_light.scrollback_indicator.fg, + &conf.colors_light.scrollback_indicator.bg); + test_two_colors(&ctx, &parse_section_colors_light, "search-box-no-match", false, + &conf.colors_light.search_box.no_match.fg, + &conf.colors_light.search_box.no_match.bg); + test_two_colors(&ctx, &parse_section_colors_light, "search-box-match", false, + &conf.colors_light.search_box.match.fg, + &conf.colors_light.search_box.match.bg); + + test_two_colors(&ctx, &parse_section_colors_light, "cursor", false, + &conf.colors_light.cursor.text, + &conf.colors_light.cursor.cursor); + + test_enum(&ctx, &parse_section_colors_light, "alpha-mode", 3, + (const char *[]){"default", "matching", "all"}, + (int []){ALPHA_MODE_DEFAULT, ALPHA_MODE_MATCHING, ALPHA_MODE_ALL}, + (int *)&conf.colors_light.alpha_mode); + + test_enum(&ctx, &parse_section_colors_light, "dim-blend-towards", 2, + (const char *[]){"black", "white"}, + (int []){DIM_BLEND_TOWARDS_BLACK, DIM_BLEND_TOWARDS_WHITE}, + (int *)&conf.colors_light.dim_blend_towards); + + for (size_t i = 0; i < 255; i++) { + char key_name[4]; + sprintf(key_name, "%zu", i); + test_color(&ctx, &parse_section_colors_light, key_name, false, + &conf.colors_light.table[i]); + } + + test_boolean(&ctx, &parse_section_colors_light, "blur", &conf.colors_light.blur); + + test_invalid_key(&ctx, &parse_section_colors_light, "256"); /* TODO: alpha (float in range 0-1, converted to uint16_t) */ @@ -842,7 +923,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); @@ -854,7 +935,10 @@ test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), const bool alt = action % 3; const bool shift = action % 4; const bool super = action % 5; - const bool argv = action % 6; + const bool argv = need_argv; + const bool section_id = need_section_id; + + xassert(!(argv && section_id)); static const char *const args[] = { "command", "arg1", "arg2", "arg3 has spaces"}; @@ -893,7 +977,7 @@ test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), xkb_keysym_get_name(sym, sym_name, sizeof(sym_name)); snprintf(value, sizeof(value), "%s%s%s", - argv ? "[command arg1 arg2 \"arg3 has spaces\"] " : "", + argv ? "[command arg1 arg2 \"arg3 has spaces\"] " : section_id ? "[foobar]" : "", modifier_string, sym_name); break; } @@ -902,7 +986,7 @@ test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), const char *const button_name = button_map[button_idx].name; int chars = snprintf( value, sizeof(value), "%s%s%s", - argv ? "[command arg1 arg2 \"arg3 has spaces\"] " : "", + argv ? "[command arg1 arg2 \"arg3 has spaces\"] " : section_id ? "[foobar]" : "", modifier_string, button_name); xassert(click_count > 0); @@ -943,6 +1027,18 @@ test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), ctx->section, ctx->key, ctx->value, ALEN(args), binding->aux.pipe.args[ALEN(args)]); } + } else if (section_id) { + if (binding->aux.regex_name == NULL) { + BUG("[%s].%s=%s: regex name is NULL", + ctx->section, ctx->key, ctx->value); + } + + if (!streq(binding->aux.regex_name, "foobar")) { + BUG("[%s].%s=%s: regex name not the expected one: " + "expected=\"%s\", got=\"%s\"", + ctx->section, ctx->key, ctx->value, + "foobar", binding->aux.regex_name); + } } else { if (binding->aux.pipe.args != NULL) { BUG("[%s].%s=%s: pipe argv not NULL", @@ -1138,7 +1234,9 @@ test_section_key_bindings(void) test_key_binding( &ctx, &parse_section_key_bindings, action, BIND_ACTION_KEY_COUNT - 1, - binding_action_map, &conf.bindings.key, KEY_BINDING); + binding_action_map, &conf.bindings.key, KEY_BINDING, + action >= BIND_ACTION_PIPE_SCROLLBACK && action <= BIND_ACTION_PIPE_COMMAND_OUTPUT, + action >= BIND_ACTION_REGEX_LAUNCH && action <= BIND_ACTION_REGEX_COPY); } config_free(&conf); @@ -1173,7 +1271,8 @@ test_section_search_bindings(void) test_key_binding( &ctx, &parse_section_search_bindings, action, BIND_ACTION_SEARCH_COUNT - 1, - search_binding_action_map, &conf.bindings.search, KEY_BINDING); + search_binding_action_map, &conf.bindings.search, KEY_BINDING, + false, false); } config_free(&conf); @@ -1209,7 +1308,8 @@ test_section_url_bindings(void) test_key_binding( &ctx, &parse_section_url_bindings, action, BIND_ACTION_URL_COUNT - 1, - url_binding_action_map, &conf.bindings.url, KEY_BINDING); + url_binding_action_map, &conf.bindings.url, KEY_BINDING, + false, false); } config_free(&conf); @@ -1245,7 +1345,8 @@ test_section_mouse_bindings(void) test_key_binding( &ctx, &parse_section_mouse_bindings, action, BIND_ACTION_COUNT - 1, - binding_action_map, &conf.bindings.mouse, MOUSE_BINDING); + binding_action_map, &conf.bindings.mouse, MOUSE_BINDING, + false, false); } config_free(&conf); @@ -1409,6 +1510,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); @@ -1431,7 +1535,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 3b887448..dbeb2e81 100644 --- a/themes/aeroroot +++ b/themes/aeroroot @@ -1,10 +1,8 @@ # -*- conf -*- # Aero root theme -[cursor] -color=1a1a1a 9fd5f5 - -[colors] +[colors-dark] +cursor=1a1a1a 9fd5f5 foreground=dedeef background=1a1a1a diff --git a/themes/alacritty b/themes/alacritty new file mode 100644 index 00000000..f05683ba --- /dev/null +++ b/themes/alacritty @@ -0,0 +1,57 @@ +# -*- conf -*- +# Alacritty + +[colors-dark] +cursor = 181818 d8d8d8 +background= 181818 +foreground= d8d8d8 + +#black +regular0= 181818 + +#red +regular1= ac4242 + +#green +regular2= 90a959 + +#yellow +regular3= f4bf75 + +#blue +regular4= 6a9fb5 + +#magenta +regular5= aa759f + +#cyan +regular6= 75b5aa + +#white/grey +regular7= d8d8d8 + + + +#grey/black +bright0= 6b6b6b + +#red +bright1= c55555 + +#green +bright2= aac474 + +#yellow +bright3= feca88 + +#blue +bright4= 82b8c8 + +#pink +bright5= c28cb8 + +#cyan +bright6= 93d3c3 + +#grey +bright7= f8f8f8 \ No newline at end of file diff --git a/themes/apprentice b/themes/apprentice index 941a27b4..291ab8db 100644 --- a/themes/apprentice +++ b/themes/apprentice @@ -1,10 +1,8 @@ # -*- conf -*- # https://github.com/romainl/Apprentice -[cursor] -color=262626 6c6c6c - -[colors] +[colors-dark] +cursor=262626 6c6c6c foreground=bcbcbc background=262626 regular0=1c1c1c diff --git a/themes/ayu-mirage b/themes/ayu-mirage index 64e85a4e..2d9b6b54 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] +[colors-dark] +cursor = ffcc66 665a44 foreground = cccac2 background = 242936 diff --git a/themes/catppuccin-frappe b/themes/catppuccin-frappe index 3b2e0131..3acae600 100644 --- a/themes/catppuccin-frappe +++ b/themes/catppuccin-frappe @@ -1,7 +1,7 @@ # _*_ conf _*_ # Catppuccin Frappe -[colors] +[colors-dark] foreground=c6d0f5 background=303446 @@ -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..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 @@ -23,6 +26,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..8f5ea36e 100644 --- a/themes/catppuccin-macchiato +++ b/themes/catppuccin-macchiato @@ -1,7 +1,7 @@ # _*_ conf _*_ # Catppuccin Macchiato -[colors] +[colors-dark] foreground=cad3f5 background=24273a @@ -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..7d98dc0f 100644 --- a/themes/catppuccin-mocha +++ b/themes/catppuccin-mocha @@ -1,7 +1,7 @@ # _*_ conf _*_ # Catppuccin Mocha -[colors] +[colors-dark] foreground=cdd6f4 background=1e1e2e @@ -23,6 +23,11 @@ bright5=f5c2e7 bright6=94e2d5 bright7=a6adc8 +cursor=11111b f5e0dc + +16=fab387 +17=f5e0dc + selection-foreground=cdd6f4 selection-background=414356 diff --git a/themes/chiba-dark b/themes/chiba-dark index bc3b1420..ffaf6cb2 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] +[colors-dark] +cursor = 181818 cdcdcd foreground = cdcdcd background = 181818 regular0 = 181818 diff --git a/themes/derp b/themes/derp index 0925d2c2..42af3377 100644 --- a/themes/derp +++ b/themes/derp @@ -1,10 +1,8 @@ # -*- conf -*- # Derp -[cursor] -color=000000 ffffff - -[colors] +[colors-dark] +cursor=000000 ffffff foreground=ffffff background=000000 regular0=111111 diff --git a/themes/deus b/themes/deus index 8fb37f75..69c44944 100644 --- a/themes/deus +++ b/themes/deus @@ -2,10 +2,8 @@ # Deus # Color palette based on: https://github.com/ajmwagar/vim-deus -[cursor] -color=2c323b eaeaea - -[colors] +[colors-dark] +cursor=2c323b eaeaea background=2c323b foreground=eaeaea regular0=242a32 diff --git a/themes/dracula b/themes/dracula index 8b6ab542..82994203 100644 --- a/themes/dracula +++ b/themes/dracula @@ -1,10 +1,8 @@ # -*- conf -*- # Dracula -[cursor] -color=282a36 f8f8f2 - -[colors] +[colors-dark] +cursor=282a36 f8f8f2 foreground=f8f8f2 background=282a36 regular0=000000 # black diff --git a/themes/dracula-iterm b/themes/dracula-iterm index 8c2f66c3..b75ddd9c 100644 --- a/themes/dracula-iterm +++ b/themes/dracula-iterm @@ -1,10 +1,8 @@ # -*- conf -*- # Dracula iTerm2 variant -[cursor] -color=ffffff bbbbbb - -[colors] +[colors-dark] +cursor=ffffff bbbbbb foreground=f8f8f2 background=1e1f29 regular0=000000 # black diff --git a/themes/electrophoretic b/themes/electrophoretic index d2b67434..8bc022ea 100644 --- a/themes/electrophoretic +++ b/themes/electrophoretic @@ -5,10 +5,11 @@ # text and the white background. # author: Eugen Rahaian -[cursor] -color=ffffff 515151 +[main] +initial-color-theme=light -[colors] +[colors-light] +cursor=ffffff 515151 background= ffffff foreground= 000000 diff --git a/themes/gruvbox b/themes/gruvbox new file mode 100644 index 00000000..e44f3ea9 --- /dev/null +++ b/themes/gruvbox @@ -0,0 +1,42 @@ +# -*- conf -*- +# Gruvbox + +[colors-dark] +background=282828 +foreground=ebdbb2 +regular0=282828 +regular1=cc241d +regular2=98971a +regular3=d79921 +regular4=458588 +regular5=b16286 +regular6=689d6a +regular7=a89984 +bright0=928374 +bright1=fb4934 +bright2=b8bb26 +bright3=fabd2f +bright4=83a598 +bright5=d3869b +bright6=8ec07c +bright7=ebdbb2 + +[colors-light] +background=fbf1c7 +foreground=3c3836 +regular0=fbf1c7 +regular1=cc241d +regular2=98971a +regular3=d79921 +regular4=458588 +regular5=b16286 +regular6=689d6a +regular7=7c6f64 +bright0=928374 +bright1=9d0006 +bright2=79740e +bright3=b57614 +bright4=076678 +bright5=8f3f71 +bright6=427b58 +bright7=3c3836 diff --git a/themes/gruvbox-dark b/themes/gruvbox-dark index 73207199..c5dadcc5 100644 --- a/themes/gruvbox-dark +++ b/themes/gruvbox-dark @@ -1,7 +1,7 @@ # -*- conf -*- # Gruvbox -[colors] +[colors-dark] background=282828 foreground=ebdbb2 regular0=282828 diff --git a/themes/gruvbox-light b/themes/gruvbox-light index 6a7a2416..6b616612 100644 --- a/themes/gruvbox-light +++ b/themes/gruvbox-light @@ -1,7 +1,10 @@ # -*- conf -*- # Gruvbox - Light -[colors] +[main] +initial-color-theme=light + +[colors-light] background=fbf1c7 foreground=3c3836 regular0=fbf1c7 diff --git a/themes/hacktober b/themes/hacktober index acb6c0b1..ecdb18fb 100644 --- a/themes/hacktober +++ b/themes/hacktober @@ -1,8 +1,7 @@ # -*- conf -*- -[cursor] -color=141414 c9c9c9 -[colors] +[colors-dark] +cursor=141414 c9c9c9 foreground=c9c9c9 background=141414 regular0=191918 # black diff --git a/themes/iterm b/themes/iterm 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 82528498..0092b795 100644 --- a/themes/jetbrains-darcula +++ b/themes/jetbrains-darcula @@ -2,10 +2,8 @@ # JetBrains Darcula # Palette based on the same theme from https://github.com/dexpota/kitty-themes -[cursor] -color=202020 ffffff - -[colors] +[colors-dark] +cursor=202020 ffffff background=202020 foreground=adadad regular0=000000 # black diff --git a/themes/kitty b/themes/kitty index b5b813cc..81fd003e 100644 --- a/themes/kitty +++ b/themes/kitty @@ -1,9 +1,7 @@ # -*- conf -*- -[cursor] -color=111111 cccccc - -[colors] +[colors-dark] +cursor=111111 cccccc foreground=dddddd background=000000 regular0=000000 # black diff --git a/themes/material-amber b/themes/material-amber index ad844a9a..69126aa0 100644 --- a/themes/material-amber +++ b/themes/material-amber @@ -2,10 +2,11 @@ # Material Amber # Based on material.io guidelines with Amber 50 background -[cursor] -color=fff8e1 21201d +[main] +initial-color-theme=light -[colors] +[colors-light] +cursor=fff8e1 21201d foreground = 21201d background = fff8e1 diff --git a/themes/material-design b/themes/material-design index 4a9e008a..bf1d0a6b 100644 --- a/themes/material-design +++ b/themes/material-design @@ -2,7 +2,7 @@ # Material # From https://github.com/MartinSeeler/iterm2-material-design -[colors] +[colors-dark] foreground=ECEFF1 background=263238 regular0=546E7A # black diff --git a/themes/modus-operandi b/themes/modus-operandi index 5e3a9fd6..6baca2f7 100644 --- a/themes/modus-operandi +++ b/themes/modus-operandi @@ -3,7 +3,11 @@ # modus-operandi # See: https://protesilaos.com/emacs/modus-themes # -[colors] + +[main] +initial-color-theme=light + +[colors-light] background=ffffff foreground=000000 regular0=000000 @@ -22,3 +26,5 @@ bright4=2544bb bright5=5317ac bright6=005a5f bright7=ffffff + +jump-labels=dce0e8 0000ff diff --git a/themes/modus-vivendi b/themes/modus-vivendi index 82b1075d..9ee670ec 100644 --- a/themes/modus-vivendi +++ b/themes/modus-vivendi @@ -4,7 +4,7 @@ # See: https://protesilaos.com/emacs/modus-themes # -[colors] +[colors-dark] background=000000 foreground=ffffff regular0=000000 diff --git a/themes/modus-vivendi-tinted b/themes/modus-vivendi-tinted new file mode 100644 index 00000000..6a61fc79 --- /dev/null +++ b/themes/modus-vivendi-tinted @@ -0,0 +1,25 @@ +# -*- conf -*- +# +# modus-vivendi-tinted +# See: https://protesilaos.com/emacs/modus-themes +# + +[colors-dark] +background=0d0e1c +foreground=ffffff +regular0=000000 +regular1=ff5f59 +regular2=44bc44 +regular3=d0bc00 +regular4=2fafff +regular5=feacd0 +regular6=00d3d0 +regular7=a6a6a6 +bright0=595959 +bright1=ff6b55 +bright2=ff6b55 +bright3=fec43f +bright4=fec43f +bright5=b6a0ff +bright6=6ae4b9 +bright7=777777 diff --git a/themes/molokai b/themes/molokai new file mode 100644 index 00000000..19e1b6fa --- /dev/null +++ b/themes/molokai @@ -0,0 +1,23 @@ +# -*- conf -*- +# Molokai +# Based on zhou13's at https://github.com/zhou13/molokai-terminal/blob/master/xterm/Xresources + +[colors-dark] +background=1B1D1E +foreground=CCCCCC +regular0=1B1D1E +regular1=FF0044 +regular2=82B414 +regular3=FD971F +regular4=266C98 +regular5=AC0CB1 +regular6=AE81FF +regular7=CCCCCC +bright0=808080 +bright1=F92672 +bright2=A6E22E +bright3=E6DB74 +bright4=7070F0 +bright5=D63AE1 +bright6=66D9EF +bright7=F8F8F2 diff --git a/themes/monokai-pro b/themes/monokai-pro index 5d9f31a9..3044da91 100644 --- a/themes/monokai-pro +++ b/themes/monokai-pro @@ -1,7 +1,7 @@ # -*- conf -*- # Monokai Pro -[colors] +[colors-dark] background=2D2A2E foreground=FCFCFA regular0=403E41 diff --git a/themes/moonfly b/themes/moonfly index 870de9d0..b30e3156 100644 --- a/themes/moonfly +++ b/themes/moonfly @@ -2,10 +2,8 @@ # moonfly # Based on https://github.com/bluz71/vim-moonfly-colors -[cursor] -color = 080808 9e9e9e - -[colors] +[colors-dark] +cursor = 080808 9e9e9e foreground = b2b2b2 background = 080808 diff --git a/themes/neon b/themes/neon 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 new file mode 100644 index 00000000..e9e40404 --- /dev/null +++ b/themes/night-owl @@ -0,0 +1,28 @@ +# _*_ conf _*_ +# Night Owl + +[colors-dark] +cursor=011627 80a4c2 +foreground=d6deeb +background=011627 + +regular0=011627 +regular1=ef5350 +regular2=22da6e +regular3=addb67 +regular4=82aaff +regular5=c792ea +regular6=21c7a8 +regular7=ffffff + +bright0=575656 +bright1=ef5350 +bright2=22da6e +bright3=ffeb95 +bright4=82aaff +bright5=c792ea +bright6=7fdbca +bright7=ffffff + +selection-background=5f7e97 +selection-foreground=dfe5ee diff --git a/themes/nightfly b/themes/nightfly index 2a27fb2d..ccdd183a 100644 --- a/themes/nightfly +++ b/themes/nightfly @@ -2,10 +2,8 @@ # nightfly # Based on https://github.com/bluz71/vim-nightfly-guicolors -[cursor] -color = 080808 9ca1aa - -[colors] +[colors-dark] +cursor = 080808 9ca1aa foreground = acb4c2 background = 011627 diff --git a/themes/noirblaze b/themes/noirblaze index 3cf452e6..b21055a4 100644 --- a/themes/noirblaze +++ b/themes/noirblaze @@ -3,10 +3,8 @@ # https://github.com/n1ghtmare/noirblaze-kitty -[cursor] -color=121212 ff0088 - -[colors] +[colors-dark] +cursor=121212 ff0088 foreground=d5d5d5 background=121212 diff --git a/themes/nord b/themes/nord index 4ce3a53e..eb2fdf0f 100644 --- a/themes/nord +++ b/themes/nord @@ -6,10 +6,8 @@ # this specific foot theme is based on nord-alacritty: # https://github.com/arcticicestudio/nord-alacritty/blob/develop/src/nord.yml -[cursor] -color = 2e3440 d8dee9 - -[colors] +[colors-dark] +cursor = 2e3440 d8dee9 foreground = d8dee9 background = 2e3440 diff --git a/themes/nordiq b/themes/nordiq index f309de23..1efccba6 100644 --- a/themes/nordiq +++ b/themes/nordiq @@ -1,10 +1,8 @@ # -*- conf -*- # Nordiq -[cursor] -color=eeeeee 9f515a - -[colors] +[colors-dark] +cursor=eeeeee 9f515a foreground=dbdee9 background=0e1420 regular0=5b6272 diff --git a/themes/nvim b/themes/nvim new file mode 100644 index 00000000..74dd1ac6 --- /dev/null +++ b/themes/nvim @@ -0,0 +1,56 @@ +# -*- conf -*- +# Neovim Dark theme +# Uses the dark color palette from the default Neovim color scheme +# See: https://github.com/neovim/neovim/blob/fb6c059dc55c8d594102937be4dd70f5ff51614a/src/nvim/highlight_group.c#L419 + +[colors-dark] +cursor=14161b e0e2ea # NvimDarkGrey2 NvimLightGrey2 +foreground=e0e2ea # NvimLightGrey2 +background=14161b # NvimDarkGrey2 + +selection-foreground=e0e2ea # NvimLightGrey2 +selection-background=4f5258 # NvimDarkGrey4 + +regular0=07080d # NvimDarkGrey1 +regular1=ffc0b9 # NvimLightRed +regular2=b3f6c0 # NvimLightGreen +regular3=fce094 # NvimLightYellow +regular4=a6dbff # NvimLightBlue +regular5=ffcaff # NvimLightMagenta +regular6=8cf8f7 # NvimLightCyan +regular7=c4c6cd # NvimLightGrey3 + +bright0=2c2e33 # NvimDarkGrey3 +bright1=ffc0b9 # NvimLightRed +bright2=b3f6c0 # NvimLightGreen +bright3=fce094 # NvimLightYellow +bright4=a6dbff # NvimLightBlue +bright5=ffcaff # NvimLightMagenta +bright6=8cf8f7 # NvimLightCyan +bright7=eef1f8 # NvimLightGrey1 + +[colors-light] +cursor=e0e2ea 14161b # NvimLightGrey2 NvimDarkGrey2 +foreground=14161b # NvimDarkGrey2 +background=e0e2ea # NvimLightGrey2 + +selection-foreground=14161b # NvimDarkGrey2 +selection-background=9b9ea4 # NvimLightGrey4 + +regular0=eef1f8 # NvimLightGrey1 +regular1=590008 # NvimDarkRed +regular2=005523 # NvimDarkGreen +regular3=6b5300 # NvimDarkYellow +regular4=004c73 # NvimDarkBlue +regular5=470045 # NvimDarkMagenta +regular6=007373 # NvimDarkCyan +regular7=2c2e33 # NvimDarkGrey3 + +bright0=c4c6cd # NvimLightGrey3 +bright1=590008 # NvimDarkRed +bright2=005523 # NvimDarkGreen +bright3=6b5300 # NvimDarkYellow +bright4=004c73 # NvimDarkBlue +bright5=470045 # NvimDarkMagenta +bright6=007373 # NvimDarkCyan +bright7=07080d # NvimDarkGrey1 diff --git a/themes/nvim-dark b/themes/nvim-dark index 4c13770a..fe3afb74 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] +[colors-dark] +cursor=14161b e0e2ea # NvimDarkGrey2 NvimLightGrey2 foreground=e0e2ea # NvimLightGrey2 background=14161b # NvimDarkGrey2 diff --git a/themes/nvim-light b/themes/nvim-light index 5afec9d7..fd8943b1 100644 --- a/themes/nvim-light +++ b/themes/nvim-light @@ -3,10 +3,11 @@ # 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 +[main] +initial-color-theme=light -[colors] +[colors-light] +cursor=e0e2ea 14161b # NvimLightGrey2 NvimDarkGrey2 foreground=14161b # NvimDarkGrey2 background=e0e2ea # NvimLightGrey2 diff --git a/themes/onedark b/themes/onedark index ac5cc834..6d66e87e 100644 --- a/themes/onedark +++ b/themes/onedark @@ -1,10 +1,8 @@ # OneDark # Palette based on the same theme from https://github.com/dexpota/kitty-themes -[cursor] -color=111111 cccccc - -[colors] +[colors-dark] +cursor=111111 cccccc foreground=979eab background=282c34 regular0=282c34 # black diff --git a/themes/onehalf-dark b/themes/onehalf-dark index c37a7984..1faca455 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] +[colors-dark] +cursor=dcdfe4 a3b3cc foreground=dcdfe4 background=282c34 regular0=282c34 # black 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 new file mode 100644 index 00000000..09934925 --- /dev/null +++ b/themes/paper-color @@ -0,0 +1,49 @@ +# -*- conf -*- +# PaperColorDark +# Palette based on https://github.com/NLKNguyen/papercolor-theme + +[colors-dark] +cursor=1c1c1c eeeeee +background=1c1c1c +foreground=eeeeee +regular0=1c1c1c # black +regular1=af005f # red +regular2=5faf00 # green +regular3=d7af5f # yellow +regular4=5fafd7 # blue +regular5=808080 # magenta +regular6=d7875f # cyan +regular7=d0d0d0 # white +bright0=bcbcbc # bright black +bright1=5faf5f # bright red +bright2=afd700 # bright green +bright3=af87d7 # bright yellow +bright4=ffaf00 # bright blue +bright5=ff5faf # bright magenta +bright6=00afaf # bright cyan +bright7=5f8787 # bright white +# selection-foreground=1c1c1c +# selection-background=af87d7 + +[colors-light] +cursor=eeeeee 444444 +background=eeeeee +foreground=444444 +regular0=eeeeee # black +regular1=af0000 # red +regular2=008700 # green +regular3=5f8700 # yellow +regular4=0087af # blue +regular5=878787 # magenta +regular6=005f87 # cyan +regular7=764e37 # white +bright0=bcbcbc # bright black +bright1=d70000 # bright red +bright2=d70087 # bright green +bright3=8700af # bright yellow +bright4=d75f00 # bright blue +bright5=d75f00 # bright magenta +bright6=4c7a5d # bright cyan +bright7=005faf # bright white +# selection-foreground=eeeeee +# selection-background=0087af diff --git a/themes/paper-color-dark b/themes/paper-color-dark index 18cd7f17..26260c6f 100644 --- a/themes/paper-color-dark +++ b/themes/paper-color-dark @@ -2,10 +2,8 @@ # PaperColorDark # Palette based on https://github.com/NLKNguyen/papercolor-theme -[cursor] -color=1c1c1c eeeeee - -[colors] +[colors-dark] +cursor=1c1c1c eeeeee background=1c1c1c foreground=eeeeee regular0=1c1c1c # black diff --git a/themes/paper-color-light b/themes/paper-color-light index b08ea707..554aabc0 100644 --- a/themes/paper-color-light +++ b/themes/paper-color-light @@ -2,10 +2,11 @@ # PaperColor Light # Palette based on https://github.com/NLKNguyen/papercolor-theme -[cursor] -color=eeeeee 444444 +[main] +initial-color-theme=light -[colors] +[colors-light] +cursor=eeeeee 444444 background=eeeeee foreground=444444 regular0=eeeeee # black diff --git a/themes/poimandres b/themes/poimandres index d8a6b0a7..a2123ac5 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] +[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 78d77dd9..b9aa7e2a 100644 --- a/themes/rose-pine +++ b/themes/rose-pine @@ -1,10 +1,8 @@ # -*- conf -*- # Rosé Pine -[cursor] -color=191724 e0def4 - -[colors] +[colors-dark] +cursor=191724 e0def4 background=191724 foreground=e0def4 diff --git a/themes/rose-pine-dawn b/themes/rose-pine-dawn index 52008b44..d2742c72 100644 --- a/themes/rose-pine-dawn +++ b/themes/rose-pine-dawn @@ -1,10 +1,12 @@ # -*- conf -*- # Rosé Pine Dawn -[cursor] -color=faf4ed 575279 +[main] +initial-color-theme=light -[colors] + +[colors-light] +cursor=faf4ed 575279 background=faf4ed foreground=575279 diff --git a/themes/rose-pine-moon b/themes/rose-pine-moon index 732e5943..51b9a33a 100644 --- a/themes/rose-pine-moon +++ b/themes/rose-pine-moon @@ -1,10 +1,8 @@ # -*- conf -*- # Rosé Pine Moon -[cursor] -color=232136 e0def4 - -[colors] +[colors-dark] +cursor=232136 e0def4 background=232136 foreground=e0def4 diff --git a/themes/selenized b/themes/selenized new file mode 100644 index 00000000..83fea617 --- /dev/null +++ b/themes/selenized @@ -0,0 +1,48 @@ +# -*- conf -*- +# Selenized dark + +[colors-dark] +cursor = 103c48 53d6c7 +background= 103c48 +foreground= adbcbc + +regular0= 184956 +regular1= fa5750 +regular2= 75b938 +regular3= dbb32d +regular4= 4695f7 +regular5= f275be +regular6= 41c7b9 +regular7= 72898f + +bright0= 2d5b69 +bright1= ff665c +bright2= 84c747 +bright3= ebc13d +bright4= 58a3ff +bright5= ff84cd +bright6= 53d6c7 +bright7= cad8d9 + +[colors-light] +cursor=fbf3db 00978a +background= fbf3db +foreground= 53676d + +regular0= ece3cc +regular1= d2212d +regular2= 489100 +regular3= ad8900 +regular4= 0072d4 +regular5= ca4898 +regular6= 009c8f +regular7= 909995 + +bright0= d5cdb6 +bright1= cc1729 +bright2= 428b00 +bright3= a78300 +bright4= 006dce +bright5= c44392 +bright6= 00978a +bright7= 3a4d53 diff --git a/themes/selenized-black b/themes/selenized-black index 28392add..8a93187e 100644 --- a/themes/selenized-black +++ b/themes/selenized-black @@ -1,10 +1,8 @@ # -*- conf -*- # Selenized black -[cursor] -color = 181818 56d8c9 - -[colors] +[colors-dark] +cursor = 181818 56d8c9 background= 181818 foreground= b9b9b9 diff --git a/themes/selenized-dark b/themes/selenized-dark index ed74cdfc..8ace1c05 100644 --- a/themes/selenized-dark +++ b/themes/selenized-dark @@ -1,10 +1,8 @@ # -*- conf -*- # Selenized dark -[cursor] -color = 103c48 53d6c7 - -[colors] +[colors-dark] +cursor = 103c48 53d6c7 background= 103c48 foreground= adbcbc diff --git a/themes/selenized-light b/themes/selenized-light index 7e599d8e..c842fc3c 100644 --- a/themes/selenized-light +++ b/themes/selenized-light @@ -1,10 +1,11 @@ # -*- conf -*- # Selenized light -[cursor] -color=fbf3db 00978a +[main] +initial-color-theme=light -[colors] +[colors-light] +cursor=fbf3db 00978a background= fbf3db foreground= 53676d diff --git a/themes/selenized-white b/themes/selenized-white index b4d25315..659bf814 100644 --- a/themes/selenized-white +++ b/themes/selenized-white @@ -1,10 +1,11 @@ # -*- conf -*- # Selenized white -[cursor] -color=ffffff 009a8a +[main] +initial-color-theme=light -[colors] +[colors-light] +cursor=ffffff 009a8a background= ffffff foreground= 474747 diff --git a/themes/solarized b/themes/solarized new file mode 100644 index 00000000..f1844b3c --- /dev/null +++ b/themes/solarized @@ -0,0 +1,47 @@ +# -*- conf -*- +# Solarized dark+light + +# Dark +[colors-dark] +cursor= 002b36 93a1a1 +background= 002b36 +foreground= 839496 +regular0= 073642 +regular1= dc322f +regular2= 859900 +regular3= b58900 +regular4= 268bd2 +regular5= d33682 +regular6= 2aa198 +regular7= eee8d5 +bright0= 002b36 +bright1= cb4b16 +bright2= 586e75 +bright3= 657b83 +bright4= 839496 +bright5= 6c71c4 +bright6= 93a1a1 +bright7= fdf6e3 + + +# Light +[colors-light] +cursor= fdf6e3 586e75 +background= fdf6e3 +foreground= 657b83 +regular0= eee8d5 +regular1= dc322f +regular2= 859900 +regular3= b58900 +regular4= 268bd2 +regular5= d33682 +regular6= 2aa198 +regular7= 073642 +bright0= cb4b16 +bright1= fdf6e3 +bright2= 93a1a1 +bright3= 839496 +bright4= 657b83 +bright5= 6c71c4 +bright6= 586e75 +bright7= 002b36 diff --git a/themes/solarized-dark b/themes/solarized-dark index cad2945e..6335fa0f 100644 --- a/themes/solarized-dark +++ b/themes/solarized-dark @@ -1,10 +1,8 @@ # -*- conf -*- # Solarized dark -[cursor] -color= 002b36 93a1a1 - -[colors] +[colors-dark] +cursor= 002b36 93a1a1 background= 002b36 foreground= 839496 regular0= 073642 diff --git a/themes/solarized-dark-normal-brights b/themes/solarized-dark-normal-brights index 1ab7d375..7b608110 100644 --- a/themes/solarized-dark-normal-brights +++ b/themes/solarized-dark-normal-brights @@ -1,10 +1,8 @@ # -*- conf -*- # Solarized dark -[cursor] -color= 002b36 93a1a1 - -[colors] +[colors-dark] +cursor= 002b36 93a1a1 background= 002b36 foreground= 839496 regular0= 073642 diff --git a/themes/solarized-light b/themes/solarized-light index 74474573..db27be43 100644 --- a/themes/solarized-light +++ b/themes/solarized-light @@ -1,10 +1,11 @@ # -*- conf -*- # Solarized light -[cursor] -color=fdf6e3 586e75 +[main] +initial-color-theme=light -[colors] +[colors-light] +cursor= fdf6e3 586e75 background= fdf6e3 foreground= 657b83 regular0= eee8d5 diff --git a/themes/solarized-normal-brights b/themes/solarized-normal-brights new file mode 100644 index 00000000..3bd3c189 --- /dev/null +++ b/themes/solarized-normal-brights @@ -0,0 +1,54 @@ +# -*- conf -*- +# Solarized dark+light +# +# Bright colors are brighter versions of the regular colors, instead +# of the normal solarized palette. +# +# They were generated by taking the regular colors, decoding from sRGB +# to linear, multiplying the linear RGB values by 1.8, and then +# encoding to sRGB again. + +# Dark +[colors-dark] +cursor= 002b36 93a1a1 +background= 002b36 +foreground= 839496 +regular0= 073642 +regular1= dc322f +regular2= 859900 +regular3= b58900 +regular4= 268bd2 +regular5= d33682 +regular6= 2aa198 +regular7= eee8d5 +bright0= 0c4958 +bright1= ff4440 +bright2= aec700 +bright3= ebb300 +bright4= 34b5ff +bright5= ff49aa +bright6= 3ad2c6 +bright7= ffffff + + +# Light +[colors-light] +cursor= fdf6e3 586e75 +background= fdf6e3 +foreground= 657b83 +regular0= eee8d5 +regular1= dc322f +regular2= 859900 +regular3= b58900 +regular4= 268bd2 +regular5= d33682 +regular6= 2aa198 +regular7= 073642 +bright0= ffffff +bright1= ff4440 +bright2= aec700 +bright3= ebb300 +bright4= 34b5ff +bright5= ff49aa +bright6= 3ad2c6 +bright7= 0c4958 diff --git a/themes/srcery b/themes/srcery 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 a326f8ad..5ea43f63 100644 --- a/themes/tango +++ b/themes/tango @@ -1,10 +1,8 @@ # -*- conf -*- # Tango -[cursor] -color=000000 babdb6 - -[colors] +[colors-dark] +cursor=000000 babdb6 foreground=babdb6 background=000000 regular0=2e3436 diff --git a/themes/tempus-autumn b/themes/tempus-autumn index 9c1f8797..214478bb 100644 --- a/themes/tempus-autumn +++ b/themes/tempus-autumn @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a palette inspired by earthly colours (WCAG AA compliant) -#[cursor] -#color = 302420 a9a2a6 - -[colors] +[colors-dark] +#cursor = 302420 a9a2a6 foreground = a9a2a6 background = 302420 regular0 = 302420 diff --git a/themes/tempus-classic b/themes/tempus-classic index 0164605b..95b37b76 100644 --- a/themes/tempus-classic +++ b/themes/tempus-classic @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with warm hues (WCAG AA compliant) -#[cursor] -#color = 232323 aeadaf - -[colors] +[colors-dark] +#cursor = 232323 aeadaf foreground = aeadaf background = 232323 regular0 = 232323 diff --git a/themes/tempus-dawn b/themes/tempus-dawn index cf143fba..c288544e 100644 --- a/themes/tempus-dawn +++ b/themes/tempus-dawn @@ -3,10 +3,12 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme with a soft, slightly desaturated palette (WCAG AA compliant) -#[cursor] -#color = eff0f2 4a4b4e +[main] +initial-color-theme=light -[colors] + +[colors-light] +#cursor = eff0f2 4a4b4e foreground = 4a4b4e background = eff0f2 regular0 = 4a4b4e diff --git a/themes/tempus-day b/themes/tempus-day index b287d45c..03454f04 100644 --- a/themes/tempus-day +++ b/themes/tempus-day @@ -3,10 +3,11 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme with warm colours (WCAG AA compliant) -#[cursor] -#color = f8f2e5 464340 +[main] +initial-color-theme=light -[colors] +[colors-light] +#cursor = f8f2e5 464340 foreground = 464340 background = f8f2e5 regular0 = 464340 diff --git a/themes/tempus-dusk b/themes/tempus-dusk index 2c0308e1..cd27aaaa 100644 --- a/themes/tempus-dusk +++ b/themes/tempus-dusk @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a deep blue-ish, slightly desaturated palette (WCAG AA compliant) -#[cursor] -#color = 1f252d a2a8ba - -[colors] +[colors-dark] +#cursor = 1f252d a2a8ba foreground = a2a8ba background = 1f252d regular0 = 1f252d diff --git a/themes/tempus-fugit b/themes/tempus-fugit index 9ebbcee7..b9dce351 100644 --- a/themes/tempus-fugit +++ b/themes/tempus-fugit @@ -3,10 +3,11 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light, pleasant theme optimised for long writing/coding sessions (WCAG AA compliant) -#[cursor] -#color = fff5f3 4d595f +[main] +initial-color-theme=light -[colors] +[colors-light] +#cursor = fff5f3 4d595f foreground = 4d595f background = fff5f3 regular0 = 4d595f diff --git a/themes/tempus-future b/themes/tempus-future index 3dd8c7a6..1f8c3c79 100644 --- a/themes/tempus-future +++ b/themes/tempus-future @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with colours inspired by concept art of outer space (WCAG AAA compliant) -#[cursor] -#color = 090a18 b4abac - -[colors] +[colors-dark] +#cursor = 090a18 b4abac foreground = b4abac background = 090a18 regular0 = 090a18 diff --git a/themes/tempus-night b/themes/tempus-night index de7be5ff..aae80f02 100644 --- a/themes/tempus-night +++ b/themes/tempus-night @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: High contrast dark theme with bright colours (WCAG AAA compliant) -#[cursor] -#color = 1a1a1a e0e0e0 - -[colors] +[colors-dark] +#cursor = 1a1a1a e0e0e0 foreground = e0e0e0 background = 1a1a1a regular0 = 1a1a1a diff --git a/themes/tempus-past b/themes/tempus-past index 8c66f54d..5f90ddf1 100644 --- a/themes/tempus-past +++ b/themes/tempus-past @@ -3,10 +3,11 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme inspired by old vaporwave concept art (WCAG AA compliant) -#[cursor] -#color = f3f2f4 53545b +[main] +initial-color-theme=light -[colors] +[colors-light] +#cursor = f3f2f4 53545b foreground = 53545b background = f3f2f4 regular0 = 53545b diff --git a/themes/tempus-rift b/themes/tempus-rift index 3657a7fe..8add657a 100644 --- a/themes/tempus-rift +++ b/themes/tempus-rift @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a subdued palette on the green side of the spectrum (WCAG AA compliant) -#[cursor] -#color = 162c22 bbbcbc - -[colors] +[colors-dark] +#cursor = 162c22 bbbcbc foreground = bbbcbc background = 162c22 regular0 = 162c22 diff --git a/themes/tempus-spring b/themes/tempus-spring index d50e6d06..eb15a1be 100644 --- a/themes/tempus-spring +++ b/themes/tempus-spring @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a palette inspired by early spring colours (WCAG AA compliant) -#[cursor] -#color = 283a37 b5b8b7 - -[colors] +[colors-dark] +#cursor = 283a37 b5b8b7 foreground = b5b8b7 background = 283a37 regular0 = 283a37 diff --git a/themes/tempus-summer b/themes/tempus-summer index 7da1d8c4..74c8faa2 100644 --- a/themes/tempus-summer +++ b/themes/tempus-summer @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with colours inspired by summer evenings by the sea (WCAG AA compliant) -#[cursor] -#color = 202c3d a0abae - -[colors] +[colors-dark] +#cursor = 202c3d a0abae foreground = a0abae background = 202c3d regular0 = 202c3d diff --git a/themes/tempus-tempest b/themes/tempus-tempest index 57c300aa..f1cf55bf 100644 --- a/themes/tempus-tempest +++ b/themes/tempus-tempest @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: A green-scale, subtle theme for late night hackers (WCAG AAA compliant) -#[cursor] -#color = 282b2b b6e0ca - -[colors] +[colors-dark] +#cursor = 282b2b b6e0ca foreground = b6e0ca background = 282b2b regular0 = 282b2b diff --git a/themes/tempus-totus b/themes/tempus-totus index 01e84692..fae6ede3 100644 --- a/themes/tempus-totus +++ b/themes/tempus-totus @@ -3,10 +3,11 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme for prose or for coding in an open space (WCAG AAA compliant) -#[cursor] -#color = ffffff 4a484d +[main] +initial-color-theme=light -[colors] +[colors-light] +#cursor = ffffff 4a484d foreground = 4a484d background = ffffff regular0 = 4a484d diff --git a/themes/tempus-warp b/themes/tempus-warp index fa8c21c2..906b3f37 100644 --- a/themes/tempus-warp +++ b/themes/tempus-warp @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a vibrant palette (WCAG AA compliant) -#[cursor] -#color = 001514 a29fa0 - -[colors] +[colors-dark] +#cursor = 001514 a29fa0 foreground = a29fa0 background = 001514 regular0 = 001514 diff --git a/themes/tempus-winter b/themes/tempus-winter index 8db97057..dc95128b 100644 --- a/themes/tempus-winter +++ b/themes/tempus-winter @@ -3,10 +3,8 @@ # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a palette inspired by winter nights at the city (WCAG AA compliant) -#[cursor] -#color = 202427 8da3b8 - -[colors] +[colors-dark] +#cursor = 202427 8da3b8 foreground = 8da3b8 background = 202427 regular0 = 202427 diff --git a/themes/tokyonight-day b/themes/tokyonight-day deleted file mode 100644 index 5143aa07..00000000 --- a/themes/tokyonight-day +++ /dev/null @@ -1,21 +0,0 @@ -# -*- conf -*- - -[colors] -background=e1e2e7 -foreground=3760bf -regular0=e9e9ed -regular1=f52a65 -regular2=587539 -regular3=8c6c3e -regular4=2e7de9 -regular5=9854f1 -regular6=007197 -regular7=6172b0 -bright0=a1a6c5 -bright1=f52a65 -bright2=587539 -bright3=8c6c3e -bright4=2e7de9 -bright5=9854f1 -bright6=007197 -bright7=3760bf \ No newline at end of file diff --git a/themes/tokyonight-light b/themes/tokyonight-light new file mode 100644 index 00000000..359a31b9 --- /dev/null +++ b/themes/tokyonight-light @@ -0,0 +1,28 @@ +# -*- conf -*- + +# Reference: https://github.com/tokyo-night/tokyo-night-vscode-theme/blob/master/themes/tokyo-night-light-color-theme.json + +[main] +initial-color-theme=light + +[colors-light] +background=d6d8df +foreground=343b58 +regular0=343b58 +regular1=8c4351 +regular2=33635c +regular3=8f5e15 +regular4=2959aa +regular5=7b43ba +regular6=006c86 +regular7=707280 +bright0=343b58 +bright1=8c4351 +bright2=33635c +bright3=8f5e15 +bright4=2959aa +bright5=7b43ba +bright6=006c86 +bright7=707280 + +jump-labels=343b58 e19d37 # brighter yellow than regular3 diff --git a/themes/tokyonight-night b/themes/tokyonight-night index f789e1bd..58037f72 100644 --- a/themes/tokyonight-night +++ b/themes/tokyonight-night @@ -1,6 +1,6 @@ # -*- conf -*- -[colors] +[colors-dark] background=1a1b26 foreground=c0caf5 regular0=15161E diff --git a/themes/tokyonight-storm b/themes/tokyonight-storm index 074b4697..4dbbf6c6 100644 --- a/themes/tokyonight-storm +++ b/themes/tokyonight-storm @@ -1,6 +1,6 @@ # -*- conf -*- -[colors] +[colors-dark] background=24283b foreground=c0caf5 regular0=1D202F diff --git a/themes/visibone b/themes/visibone index 3ee665d0..b989b36b 100644 --- a/themes/visibone +++ b/themes/visibone @@ -1,10 +1,8 @@ # -*- conf -*- # VisiBone -[cursor] -color=010101 ffffff - -[colors] +[colors-dark] +cursor=010101 ffffff foreground=ffffff background=010101 regular0=666666 diff --git a/themes/xterm b/themes/xterm 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 diff --git a/tokenize.c b/tokenize.c index 77cc3f1a..70ceb39b 100644 --- a/tokenize.c +++ b/tokenize.c @@ -45,7 +45,7 @@ tokenize_cmdline(const char *cmdline, char ***argv) size_t idx = 0; while (*p != '\0') { - char *end = strchr(search_start, delim); + char *end = (char *)strchr(search_start, delim); if (end == NULL) { if (delim != ' ') { LOG_ERR("unterminated %s quote", delim == '"' ? "double" : "single"); diff --git a/url-mode.c b/url-mode.c index 20c9820b..44809f5f 100644 --- a/url-mode.c +++ b/url-mode.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -66,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( @@ -106,6 +108,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, @@ -127,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; @@ -155,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; @@ -178,19 +191,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 untranslated symbols */ tll_foreach(bindings->url, 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); - return; - } - - if (bind->mods != mods) + if (bind->mods != mods || bind->mods == 0) continue; for (size_t i = 0; i < raw_count; i++) { @@ -199,6 +207,26 @@ urls_input(struct seat *seat, struct terminal *term, return; } } + } + + /* Match translated symbol */ + tll_foreach(bindings->url, it) { + const struct key_binding *bind = &it->item; + + if (bind->k.sym == sym && + bind->mods == (mods & ~consumed)) + { + execute_binding(seat, term, bind, serial); + return; + } + + } + + /* Match raw key code */ + tll_foreach(bindings->url, it) { + const struct key_binding *bind = &it->item; + if (bind->mods != mods || bind->mods == 0) + continue; /* Match raw key code */ tll_foreach(bind->k.key_codes, code) { @@ -254,7 +282,9 @@ urls_input(struct seat *seat, struct terminal *term, } if (match) { - activate_url(seat, term, match, serial); + // If the last hint character was uppercase, copy and paste + bool insert = term->conf->uppercase_regex_insert && wc == toc32upper(wc); + activate_url(seat, term, match, serial, insert); switch (match->action) { case URL_ACTION_COPY: @@ -276,243 +306,147 @@ urls_input(struct seat *seat, struct terminal *term, } } -static int -c32cmp_single(const void *_a, const void *_b) -{ - const char32_t *a = _a; - const char32_t *b = _b; - return *a - *b; -} +struct vline { + char *utf8; + size_t len; /* Length of utf8[] */ + size_t sz; /* utf8[] allocated size */ + struct coord *map; /* Maps utf8[ofs] to grid coordinates */ +}; static void -auto_detected(const struct terminal *term, enum url_action action, - url_list_t *urls) +regex_detected(const struct terminal *term, enum url_action action, + const regex_t *preg, url_list_t *urls) { - const struct config *conf = term->conf; + /* + * Use regcomp()+regexec() to find patterns. + * + * Since we can't feed regexec() one character at a time, and + * since it doesn't accept wide characters, we need to build utf8 + * strings. + * + * Each string represents a logical line (i.e. handle line-wrap). + * To be able to map regex matches back to the grid, we store the + * grid coordinates of *each* character, in the line struct as + * well. This is offset based; utf8[ofs] has its grid coordinates + * in map[ofs. + */ - const char32_t *uri_characters = conf->url.uri_characters; - if (uri_characters == NULL) - return; + /* There is *at most* term->rows logical lines */ + struct vline vlines[term->rows]; + size_t vline_idx = 0; - const size_t uri_characters_count = c32len(uri_characters); - if (uri_characters_count == 0) - return; + memset(vlines, 0, sizeof(vlines)); + struct vline *vline = &vlines[vline_idx]; - size_t max_prot_len = conf->url.max_prot_len; - char32_t proto_chars[max_prot_len]; - struct coord proto_start[max_prot_len]; - size_t proto_char_count = 0; - - enum { - STATE_PROTOCOL, - STATE_URL, - } state = STATE_PROTOCOL; - - struct coord start = {-1, -1}; - char32_t url[term->cols * term->rows + 1]; - size_t len = 0; - - ssize_t parenthesis = 0; - ssize_t brackets = 0; - ssize_t ltgts = 0; + mbstate_t ps = {0}; for (int r = 0; r < term->rows; r++) { const struct row *row = grid_row_in_view(term->grid, r); for (int c = 0; c < term->cols; c++) { const struct cell *cell = &row->cells[c]; + const char32_t *wc = &cell->wc; + size_t wc_count = 1; - if (cell->wc >= CELL_SPACER) + /* Expand combining characters */ + if (wc[0] >= CELL_COMB_CHARS_LO && wc[0] <= CELL_COMB_CHARS_HI) { + const struct composed *composed = + composed_lookup(term->composed, wc[0] - CELL_COMB_CHARS_LO); + xassert(composed != NULL); + + wc = composed->chars; + wc_count = composed->count; + } + + else if (wc[0] >= CELL_SPACER) continue; - const char32_t *wcs = NULL; - size_t wc_count = 0; + /* 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 (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; - } + if (char_len == (size_t)-1) + continue; - 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]; - } + for (size_t j = 0; j < char_len; j++) { + const size_t requires_size = vline->len + char_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++; - - 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; + /* Need to grow? Remember to save at least one byte for terminator */ + if (vline->sz == 0 || requires_size > vline->sz - 1) { + const size_t new_size = requires_size * 2; + vline->utf8 = xreallocarray(vline->utf8, new_size, 1); + vline->map = xreallocarray(vline->map, new_size, sizeof(vline->map[0])); + vline->sz = new_size; } - /* 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; - } + vline->utf8[vline->len + j] = + (buf[j] == '\0') ? ' ' : buf[j]; + vline->map[vline->len + j] = (struct coord){c, term->grid->view + r}; } + + vline->len += char_len; } } + + if (row->linebreak) { + if (vline->len > 0) { + vline->utf8[vline->len++] = '\0'; + ps = (mbstate_t){0}; + + vline_idx++; + vline = &vlines[vline_idx]; + } + } + } + + /* Terminate the last line, if necessary */ + if (vline_idx < ALEN(vlines) && + vline->len > 0 && vline->utf8[vline->len - 1] != '\0') + { + vline->utf8[vline->len++] = '\0'; + } + + for (size_t i = 0; i < ALEN(vlines); i++) { + const struct vline *v = &vlines[i]; + if (v->utf8 == NULL) + continue; + + const char *search_string = v->utf8; + while (true) { + regmatch_t matches[preg->re_nsub + 1]; + int r = regexec(preg, search_string, preg->re_nsub + 1, matches, 0); + + if (r == REG_NOMATCH) + break; + + const size_t mlen = matches[1].rm_eo - matches[1].rm_so; + const size_t start = &search_string[matches[1].rm_so] - v->utf8; + const size_t end = start + mlen; + + LOG_DBG( + "regex match at row %d: %.*s (%zu bytes), row/col = %dx%d", + matches[1].rm_so, (int)mlen, &search_string[matches[1].rm_so], + mlen, v->map[start].row, v->map[start].col); + + tll_push_back( + *urls, + ((struct url){ + .id = (uint64_t)rand() << 32 | rand(), + .url = xstrndup(&v->utf8[start], mlen), + .range = { + .start = v->map[start], + .end = v->map[end - 1], /* Inclusive */ + }, + .action = action, + .osc8 = false})); + + search_string += matches[0].rm_eo; + } + + free(v->utf8); + free(v->map); } } @@ -614,22 +548,16 @@ remove_overlapping(url_list_t *urls, int cols) } void -urls_collect(const struct terminal *term, enum url_action action, url_list_t *urls) +urls_collect(const struct terminal *term, enum url_action action, + const regex_t *preg, bool osc8, url_list_t *urls) { xassert(tll_length(term->urls) == 0); - osc8_uris(term, action, urls); - auto_detected(term, action, urls); + if (osc8) + osc8_uris(term, action, urls); + regex_detected(term, action, preg, urls); remove_overlapping(urls, term->grid->num_cols); } -static int -c32cmp_qsort_wrapper(const void *_a, const void *_b) -{ - const char32_t *a = *(const char32_t **)_a; - const char32_t *b = *(const char32_t **)_b; - return c32cmp(a, b); -} - static void generate_key_combos(const struct config *conf, size_t count, char32_t *combos[static count]) @@ -672,10 +600,6 @@ generate_key_combos(const struct config *conf, } free(hints); - /* Sorting is a kind of shuffle, since we're sorting on the - * *reversed* strings */ - qsort(combos, count, sizeof(char32_t *), &c32cmp_qsort_wrapper); - /* Reverse all strings */ for (size_t i = 0; i < count; i++) { const size_t len = c32len(combos[i]); @@ -699,12 +623,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; @@ -724,7 +648,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; @@ -744,7 +668,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; @@ -801,7 +725,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; @@ -836,6 +760,9 @@ urls_render(struct terminal *term) /* Snapshot the current grid */ term->url_grid_snapshot = grid_snapshot(term->grid); + /* Remember which launcher to use */ + term->url_launch = launch; + xassert(tll_length(win->urls) == 0); tll_foreach(win->term->urls, it) { struct wl_url url = {.url = &it->item}; @@ -882,10 +809,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); } } 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, diff --git a/vt.c b/vt.c index 95cfdd2e..1d8297be 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,312 +646,10 @@ action_put(struct terminal *term, uint8_t c) dcs_put(term, c); } -static inline uint32_t -chain_key(uint32_t old_key, uint32_t new_wc) -{ - unsigned bits = 32 - __builtin_clz(CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO); - - /* Rotate old key 8 bits */ - uint32_t new_key = (old_key << 8) | (old_key >> (bits - 8)); - - /* xor with new char */ - new_key ^= new_wc; - - /* Multiply with magic hash constant */ - new_key *= 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) -{ - 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); - 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 = chain_key(composed->key, wc); - } else - key = chain_key(base, wc); - -#if defined(FOOT_GRAPHEME_CLUSTERING) - if (grapheme_clustering) { - /* Check if we're on a grapheme cluster break */ - if (utf8proc_grapheme_break_stateful( - last, wc, &term->vt.grapheme_state)) - { - term_reset_grapheme_state(term); - goto out; - } - } -#endif - - int base_width = c32width(base); - if (base_width > 0) { - term->grid->cursor.point.col = col; - term->grid->cursor.lcf = false; - - if (composed == NULL) { - bool base_from_primary; - bool comb_from_primary; - bool pre_from_primary; - - char32_t precomposed = 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); - - size_t collision_count = 0; - - /* Look for existing combining chain */ - while (true) { - if (unlikely(collision_count > 128)) { - static bool have_warned = false; - if (!have_warned) { - have_warned = true; - LOG_WARN("ignoring composed character: " - "too many collisions in hash table"); - } - return; - } - - const struct composed *cc = composed_lookup(term->composed, key); - if (cc == NULL) - break; - - /* - * We may have a key collisison, so need to check that - * it's a true match. If not, bump the key and try - * again. - */ - - xassert(key == cc->key); - if (cc->chars[0] != base || - cc->count != wanted_count || - cc->chars[wanted_count - 1] != wc) - { -#if 0 - LOG_WARN("COLLISION: base: %04x/%04x, count: %d/%zu, last: %04x/%04x", - cc->chars[0], base, cc->count, wanted_count, cc->chars[wanted_count - 1], wc); -#endif - key++; - collision_count++; - continue; - } - - bool match = composed != NULL - ? memcmp(&cc->chars[1], &composed->chars[1], - (wanted_count - 2) * sizeof(cc->chars[0])) == 0 - : true; - - if (!match) { - key++; - collision_count++; - continue; - } - - wc = CELL_COMB_CHARS_LO + cc->key; - width = cc->width; - goto out; - } - - if (unlikely(term->composed_count >= - (CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO))) - { - /* We reached our maximum number of allowed composed - * character chains. Fall through here and print the - * current zero-width character to the current cell */ - LOG_WARN("maximum number of composed characters reached"); - term_reset_grapheme_state(term); - goto out; - } - - /* Allocate new chain */ - struct composed *new_cc = xmalloc(sizeof(*new_cc)); - new_cc->chars = xmalloc(wanted_count * sizeof(new_cc->chars[0])); - new_cc->key = key; - new_cc->count = wanted_count; - new_cc->chars[0] = base; - new_cc->chars[wanted_count - 1] = wc; - - if (composed != NULL) { - memcpy(&new_cc->chars[1], &composed->chars[1], - (wanted_count - 2) * sizeof(new_cc->chars[0])); - } - - const int grapheme_width = - composed != NULL ? composed->width : base_width; - - switch (term->conf->tweak.grapheme_width_method) { - case GRAPHEME_WIDTH_MAX: - new_cc->width = max(grapheme_width, width); - break; - - case GRAPHEME_WIDTH_DOUBLE: - 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 + key; - width = new_cc->width; - - xassert(wc >= CELL_COMB_CHARS_LO); - xassert(wc <= CELL_COMB_CHARS_HI); - goto out; - } - } else - term_reset_grapheme_state(term); - - -out: - if (width > 0) - term_print(term, wc, width); + term_process_and_print_non_ascii(term, wc); } static void @@ -1344,7 +1041,7 @@ state_utf8_21_switch(struct terminal *term, uint8_t data) switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_22(term, data); return STATE_GROUND; - default: return STATE_GROUND; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); } } @@ -1354,7 +1051,7 @@ state_utf8_31_switch(struct terminal *term, uint8_t data) switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_32(term, data); return STATE_UTF8_32; - default: return STATE_GROUND; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); } } @@ -1364,7 +1061,7 @@ state_utf8_32_switch(struct terminal *term, uint8_t data) switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_33(term, data); return STATE_GROUND; - default: return STATE_GROUND; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); } } @@ -1374,7 +1071,7 @@ state_utf8_41_switch(struct terminal *term, uint8_t data) switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_42(term, data); return STATE_UTF8_42; - default: return STATE_GROUND; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); } } @@ -1384,7 +1081,7 @@ state_utf8_42_switch(struct terminal *term, uint8_t data) switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_43(term, data); return STATE_UTF8_43; - default: return STATE_GROUND; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); } } @@ -1394,7 +1091,7 @@ state_utf8_43_switch(struct terminal *term, uint8_t data) switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_44(term, data); return STATE_GROUND; - default: return STATE_GROUND; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); } } diff --git a/wayland.c b/wayland.c index 9c184adc..1ffd62a6 100644 --- a/wayland.c +++ b/wayland.c @@ -237,6 +237,14 @@ 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_ARGB2101010: wayl->shm_have_argb2101010 = true; break; + case WL_SHM_FORMAT_ABGR2101010: wayl->shm_have_abgr2101010 = true; break; + case WL_SHM_FORMAT_ABGR16161616: wayl->shm_have_abgr161616 = true; break; + } + #if defined(_DEBUG) bool have_description = false; @@ -666,6 +674,88 @@ static const struct wp_presentation_listener presentation_listener = { .clock_id = &clock_id, }; +static void +color_manager_create_image_description(struct wayland *wayl) +{ + if (!wayl->color_management.have_feat_parametric) + return; + + if (!wayl->color_management.have_primaries_srgb) + return; + + if (!wayl->color_management.have_tf_ext_linear) + return; + + struct wp_image_description_creator_params_v1 *params = + wp_color_manager_v1_create_parametric_creator(wayl->color_management.manager); + + wp_image_description_creator_params_v1_set_tf_named( + params, WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR); + + wp_image_description_creator_params_v1_set_primaries_named( + params, WP_COLOR_MANAGER_V1_PRIMARIES_SRGB); + + wayl->color_management.img_description = + wp_image_description_creator_params_v1_create(params); +} + +static void +color_manager_supported_intent(void *data, + struct wp_color_manager_v1 *wp_color_manager_v1, + uint32_t render_intent) +{ + struct wayland *wayl = data; + if (render_intent == WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL) + wayl->color_management.have_intent_perceptual = true; +} + +static void +color_manager_supported_feature(void *data, + struct wp_color_manager_v1 *wp_color_manager_v1, + uint32_t feature) +{ + struct wayland *wayl = data; + + if (feature == WP_COLOR_MANAGER_V1_FEATURE_PARAMETRIC) + wayl->color_management.have_feat_parametric = true; +} + +static void +color_manager_supported_tf_named(void *data, + struct wp_color_manager_v1 *wp_color_manager_v1, + uint32_t tf) +{ + struct wayland *wayl = data; + if (tf == WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR) + wayl->color_management.have_tf_ext_linear = true; +} + +static void +color_manager_supported_primaries_named(void *data, + struct wp_color_manager_v1 *wp_color_manager_v1, + uint32_t primaries) +{ + struct wayland *wayl = data; + if (primaries == WP_COLOR_MANAGER_V1_PRIMARIES_SRGB) + wayl->color_management.have_primaries_srgb = true; +} + +static void +color_manager_done(void *data, + struct wp_color_manager_v1 *wp_color_manager_v1) +{ + struct wayland *wayl = data; + color_manager_create_image_description(wayl); +} + +static const struct wp_color_manager_v1_listener color_manager_listener = { + .supported_intent = &color_manager_supported_intent, + .supported_feature = &color_manager_supported_feature, + .supported_primaries_named = &color_manager_supported_primaries_named, + .supported_tf_named = &color_manager_supported_tf_named, + .done = &color_manager_done, +}; + static bool verify_iface_version(const char *iface, uint32_t version, uint32_t wanted) { @@ -761,6 +851,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 @@ -778,6 +872,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 @@ -796,6 +896,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_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 } @@ -836,6 +942,10 @@ xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, win->configure.is_tiled_bottom = is_tiled_bottom; win->configure.is_tiled_left = is_tiled_left; win->configure.is_tiled_right = is_tiled_right; + win->configure.is_constrained_top = is_constrained_top; + win->configure.is_constrained_bottom = is_constrained_bottom; + win->configure.is_constrained_left = is_constrained_left; + win->configure.is_constrained_right = is_constrained_right; win->configure.width = width; win->configure.height = height; } @@ -951,6 +1061,7 @@ xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, bool wasnt_configured = !win->is_configured; bool was_resizing = win->is_resizing; + bool was_fullscreen = win->is_fullscreen; bool csd_was_enabled = win->csd_mode == CSD_YES && !win->is_fullscreen; int new_width = win->configure.width; int new_height = win->configure.height; @@ -959,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; @@ -1013,11 +1132,26 @@ xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, else term_visual_focus_out(term); - if (!resized) { + /* + * Update opaque region if fullscreen state changed, also need to + * render, since we use different buffer types with and without + * alpha + */ + if (was_fullscreen != win->is_fullscreen) { + wayl_win_alpha_changed(win); + render_refresh(term); + } + + const bool will_render_soon = resized || + term->render.refresh.grid || + term->render.pending.grid; + + if (!will_render_soon) { /* - * If we didn't resize, we won't be committing a new surface - * anytime soon. Some compositors require a commit in - * combination with an ack - make them happy. + * If we didn't resize, and aren't refreshing for other + * reasons, we won't be committing a new surface anytime + * soon. Some compositors require a commit in combination with + * an ack - make them happy. */ wl_surface_commit(win->surface.surf); } @@ -1059,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) { @@ -1142,13 +1297,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; @@ -1349,8 +1514,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)) { @@ -1363,14 +1536,58 @@ 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)) { + 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)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->system_bell = wl_registry_bind( + wayl->registry, name, &xdg_system_bell_v1_interface, required); + } + + else if (streq(interface, wp_color_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->color_management.manager = wl_registry_bind( + wayl->registry, name, &wp_color_manager_v1_interface, required); + + wp_color_manager_v1_add_listener( + wayl->color_management.manager, &color_manager_listener, wayl); + } + +#if defined(HAVE_XDG_TOPLEVEL_TAG) + else if (streq(interface, xdg_toplevel_tag_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->toplevel_tag_manager = wl_registry_bind( + wayl->registry, name, &xdg_toplevel_tag_manager_v1_interface, required); + } +#endif +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + else if (streq(interface, ext_background_effect_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->background_effect_manager = wl_registry_bind( + wayl->registry, name, + &ext_background_effect_manager_v1_interface, required); + + ext_background_effect_manager_v1_add_listener( + wayl->background_effect_manager, &background_manager_listener, wayl); } #endif @@ -1387,6 +1604,7 @@ handle_global(void *data, struct wl_registry *registry, seat_add_text_input(&it->item); } #endif + } static void @@ -1502,6 +1720,8 @@ fdm_wayl(struct fdm *fdm, int fd, int events, void *data) return false; } + wl_display_dispatch_pending(wayl->display); + while (wl_display_prepare_read(wayl->display) != 0) { if (wl_display_dispatch_pending(wayl->display) < 0) { LOG_ERRNO("failed to dispatch pending Wayland events"); @@ -1614,11 +1834,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"); + 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) { @@ -1696,10 +1914,23 @@ wayl_destroy(struct wayland *wayl) zwp_text_input_manager_v3_destroy(wayl->text_input_manager); #endif -#if defined(HAVE_XDG_TOPLEVEL_ICON) +#if defined(HAVE_XDG_TOPLEVEL_TAG) + if (wayl->toplevel_tag_manager != NULL) + xdg_toplevel_tag_manager_v1_destroy(wayl->toplevel_tag_manager); +#endif +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + if (wayl->background_effect_manager != NULL) + ext_background_effect_manager_v1_destroy(wayl->background_effect_manager); +#endif + + if (wayl->color_management.img_description != NULL) + wp_image_description_v1_destroy(wayl->color_management.img_description); + if (wayl->color_management.manager != NULL) + wp_color_manager_v1_destroy(wayl->color_management.manager); + if (wayl->system_bell != NULL) + xdg_system_bell_v1_destroy(wayl->system_bell); if (wayl->toplevel_icon_manager != NULL) xdg_toplevel_icon_manager_v1_destroy(wayl->toplevel_icon_manager); -#endif if (wayl->single_pixel_manager != NULL) wp_single_pixel_buffer_manager_v1_destroy(wayl->single_pixel_manager); if (wayl->fractional_scale_manager != NULL) @@ -1795,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) { @@ -1809,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); @@ -1817,7 +2056,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_ICON) +#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; @@ -1830,7 +2083,34 @@ 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 (term->conf->gamma_correct) { + if (wayl->color_management.img_description != NULL) { + xassert(wayl->color_management.manager != NULL); + + win->surface.color_management = wp_color_manager_v1_get_surface( + term->wl->color_management.manager, win->surface.surf); + + wp_color_management_surface_v1_set_image_description( + win->surface.color_management, wayl->color_management.img_description, + WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL); + } else { + if (wayl->color_management.manager == NULL) { + LOG_WARN( + "gamma-corrected-blending: disabling; " + "compositor does not implement the color-management protocol"); + } else { + LOG_WARN( + "gamma-corrected-blending: disabling; " + "compositor does not implement all required color-management features"); + LOG_WARN("use e.g. 'wayland-info' and verify the compositor implements:"); + LOG_WARN(" - feature: parametric"); + LOG_WARN(" - render intent: perceptual"); + LOG_WARN(" - TF: ext_linear"); + LOG_WARN(" - primaries: sRGB"); + } + } + } if (conf->csd.preferred == CONF_CSD_PREFER_NONE) { /* User specifically do *not* want decorations */ @@ -1846,8 +2126,8 @@ wayl_win_init(struct terminal *term, const char *token) zxdg_toplevel_decoration_v1_set_mode( win->xdg_toplevel_decoration, (conf->csd.preferred == CONF_CSD_PREFER_SERVER - ? ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE - : ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE)); + ? ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE + : ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE)); zxdg_toplevel_decoration_v1_add_listener( win->xdg_toplevel_decoration, &xdg_toplevel_decoration_listener, win); @@ -1954,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); @@ -1974,6 +2256,13 @@ wayl_win_destroy(struct wl_window *win) tll_remove(win->xdg_tokens, it); } +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + if (win->surface.background_effect != NULL) + ext_background_effect_surface_v1_destroy(win->surface.background_effect); +#endif + + if (win->surface.color_management != NULL) + wp_color_management_surface_v1_destroy(win->surface.color_management); if (win->fractional_scale != NULL) wp_fractional_scale_v1_destroy(win->fractional_scale); if (win->surface.viewport != NULL) @@ -2067,7 +2356,14 @@ wayl_flush(struct wayland *wayl) } if (errno != EAGAIN) { - LOG_ERRNO("failed to flush wayland socket"); + const int saved_errno = errno; + + if (errno == EPIPE) { + wl_display_read_events(wayl->display); + wl_display_dispatch_pending(wayl->display); + } + + LOG_ERRNO_P(saved_errno, "failed to flush wayland socket"); return; } @@ -2203,10 +2499,16 @@ void wayl_win_alpha_changed(struct wl_window *win) { struct terminal *term = win->term; + struct wayland *wayl = term->wl; - if (term->colors.alpha == 0xffff) { - struct wl_region *region = wl_compositor_create_region( - term->wl->compositor); + /* + * When fullscreened, transparency is disabled (see render.c). + * Update the opaque region to match. + */ + const bool is_opaque = term->colors.alpha == 0xffff || win->is_fullscreen; + + if (is_opaque) { + struct wl_region *region = wl_compositor_create_region(wayl->compositor); if (region != NULL) { wl_region_add(region, 0, 0, INT32_MAX, INT32_MAX); @@ -2215,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 @@ -2247,6 +2581,24 @@ wayl_win_set_urgent(struct wl_window *win) return false; } +bool +wayl_win_ring_bell(const struct wl_window *win) +{ + if (win->term->wl->system_bell == NULL) { + static bool have_warned = false; + + if (!have_warned) { + LOG_WARN("compositor does not implement the XDG system bell protocol"); + have_warned = true; + } + + return false; + } + + xdg_system_bell_v1_ring(win->term->wl->system_bell, win->surface.surf); + return true; +} + bool wayl_win_csd_titlebar_visible(const struct wl_window *win) { @@ -2271,6 +2623,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 @@ -2281,6 +2634,20 @@ wayl_win_subsurface_new_with_custom_parent( return false; } + surf->surface.color_management = NULL; + if (win->term->conf->gamma_correct && + wayl->color_management.img_description != NULL) + { + xassert(wayl->color_management.manager != NULL); + + surf->surface.color_management = wp_color_manager_v1_get_surface( + wayl->color_management.manager, main_surface); + + wp_color_management_surface_v1_set_image_description( + surf->surface.color_management, wayl->color_management.img_description, + WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL); + } + struct wl_subsurface *sub = wl_subcompositor_get_subsurface( wayl->sub_compositor, main_surface, parent); @@ -2332,6 +2699,11 @@ wayl_win_subsurface_destroy(struct wayl_sub_surface *surf) if (surf == NULL) return; + if (surf->surface.color_management != NULL) { + wp_color_management_surface_v1_destroy(surf->surface.color_management); + surf->surface.color_management = NULL; + } + if (surf->surface.viewport != NULL) { wp_viewport_destroy(surf->surface.viewport); surf->surface.viewport = NULL; @@ -2424,3 +2796,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 227e2a68..9cbd1023 100644 --- a/wayland.h +++ b/wayland.h @@ -9,6 +9,7 @@ #include /* Wayland protocols */ +#include #include #include #include @@ -19,14 +20,20 @@ #include #include #include +#include +#include -#if defined(HAVE_XDG_TOPLEVEL_ICON) - #include +#if defined(HAVE_XDG_TOPLEVEL_TAG) + #include +#endif +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + #include #endif #include #include +#include "config.h" #include "cursor-shape.h" #include "fdm.h" @@ -57,6 +64,11 @@ enum touch_state { 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 { @@ -136,6 +148,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; @@ -144,6 +158,8 @@ struct seat { bool alt; bool ctrl; bool super; + + xkb_keysym_t last_shortcut_sym; } kbd; /* Pointer state */ @@ -400,6 +416,12 @@ struct wl_window { bool is_tiled_left; bool is_tiled_right; bool is_tiled; /* At least one of is_tiled_{top,bottom,left,right} is true */ + + bool is_constrained_top; + bool is_constrained_bottom; + bool is_constrained_left; + bool is_constrained_right; + struct { int width; int height; @@ -407,10 +429,17 @@ struct wl_window { bool is_fullscreen:1; bool is_maximized:1; bool is_resizing:1; + bool is_tiled_top:1; bool is_tiled_bottom:1; bool is_tiled_left:1; bool is_tiled_right:1; + + bool is_constrained_top:1; + bool is_constrained_bottom:1; + bool is_constrained_left:1; + bool is_constrained_right:1; + enum csd_mode csd_mode; } configure; @@ -444,17 +473,35 @@ 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; -#if defined(HAVE_XDG_TOPLEVEL_ICON) struct xdg_toplevel_icon_manager_v1 *toplevel_icon_manager; -#endif + + struct xdg_system_bell_v1 *system_bell; + + struct { + struct wp_color_manager_v1 *manager; + struct wp_image_description_v1 *img_description; + bool have_intent_perceptual; + bool have_feat_parametric; + bool have_tf_ext_linear; + bool have_primaries_srgb; + } color_management; bool presentation_timings; struct wp_presentation *presentation; uint32_t presentation_clock_id; +#if defined(HAVE_XDG_TOPLEVEL_TAG) + struct xdg_toplevel_tag_manager_v1 *toplevel_tag_manager; +#endif +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + struct ext_background_effect_manager_v1 *background_effect_manager; + bool have_background_blur; +#endif + #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED struct zwp_text_input_manager_v3 *text_input_manager; #endif @@ -466,6 +513,10 @@ struct wayland { /* WL_SHM >= 2 */ bool use_shm_release; + + bool shm_have_argb2101010:1; + bool shm_have_abgr2101010:1; + bool shm_have_abgr161616:1; }; struct wayland *wayl_init( @@ -492,6 +543,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); @@ -509,3 +561,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); 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 + +/* Added in libxkbcommon 1.8.0 */ +#if !defined(XKB_VMOD_NAME_ALT) +/* Common *virtual* modifiers, encoded in xkeyboard-config in the compat and + * symbols files. They have been stable since the beginning of the project and + * are unlikely to ever change. */ +#define XKB_VMOD_NAME_ALT "Alt" +#define XKB_VMOD_NAME_HYPER "Hyper" +#define XKB_VMOD_NAME_LEVEL3 "LevelThree" +#define XKB_VMOD_NAME_LEVEL5 "LevelFive" +#define XKB_VMOD_NAME_META "Meta" +#define XKB_VMOD_NAME_NUM "NumLock" +#define XKB_VMOD_NAME_SCROLL "ScrollLock" +#define XKB_VMOD_NAME_SUPER "Super" +#endif diff --git a/xmalloc.c b/xmalloc.c index ded7f4e3..ccfb5c48 100644 --- a/xmalloc.c +++ b/xmalloc.c @@ -32,8 +32,17 @@ xcalloc(size_t nmemb, size_t size) void * xrealloc(void *ptr, size_t size) { + xassert(size != 0); void *alloc = realloc(ptr, size); - return unlikely(size == 0) ? alloc : check_alloc(alloc); + return check_alloc(alloc); +} + +void * +xreallocarray(void *ptr, size_t n, size_t size) +{ + xassert(n != 0 && size != 0); + void *alloc = reallocarray(ptr, n, size); + return check_alloc(alloc); } char * diff --git a/xmalloc.h b/xmalloc.h index 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;