diff --git a/.builds/alpine-x64.yml b/.builds/alpine-x64.yml index 8f341f3d..2e2ec2c4 100644 --- a/.builds/alpine-x64.yml +++ b/.builds/alpine-x64.yml @@ -24,7 +24,7 @@ packages: - font-noto-emoji sources: - - https://codeberg.org/dnkl/foot + - https://git.sr.ht/~dnkl/foot # triggers: # - action: email @@ -49,4 +49,4 @@ tasks: - codespell: | pip install codespell cd foot - ~/.local/bin/codespell -Lser,doas README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd + ~/.local/bin/codespell -Lser,doas,zar README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd diff --git a/.builds/alpine-x86.yml.disabled b/.builds/alpine-x86.yml.disabled index 22a9e637..6d790227 100644 --- a/.builds/alpine-x86.yml.disabled +++ b/.builds/alpine-x86.yml.disabled @@ -23,7 +23,7 @@ packages: - font-noto-emoji sources: - - https://codeberg.org/dnkl/foot + - https://git.sr.ht/~dnkl/foot # triggers: # - action: email diff --git a/.builds/freebsd-x64.yml b/.builds/freebsd-x64.yml index 89803a6e..9642f96d 100644 --- a/.builds/freebsd-x64.yml +++ b/.builds/freebsd-x64.yml @@ -19,7 +19,7 @@ packages: - noto-emoji sources: - - https://codeberg.org/dnkl/foot + - https://git.sr.ht/~dnkl/foot # triggers: # - action: email diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b2a459dc..28df1ccb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -109,4 +109,4 @@ codespell: - apk add python3 - apk add py3-pip - pip install codespell - - codespell -Lser,doas README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd + - codespell -Lser,doas,zar README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd diff --git a/.woodpecker.yml b/.woodpecker.yml index 8493aa47..284da761 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -9,7 +9,7 @@ pipeline: - apk add python3 - apk add py3-pip - pip install codespell - - codespell -Lser,doas README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd + - codespell -Lser,doas,zar README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd subprojects: when: diff --git a/CHANGELOG.md b/CHANGELOG.md index dec74bc1..7a6efd08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog * [Unreleased](#unreleased) +* [1.13.1](#1-13-1) +* [1.13.0](#1-13-0) * [1.12.1](#1-12-1) * [1.12.0](#1-12-0) * [1.11.0](#1-11-0) @@ -43,6 +45,80 @@ ### Added +* Support for adjusting the thickness of regular underlines ([#1136][1136]). +* Support (optional) for utmp logging with libutempter. +* `kxIN` and `kxOUT` (focus in/out events) to terminfo. +* `name` capability to `XTGETTCAP`. + +[1136]: https://codeberg.org/dnkl/foot/issues/1136 + + +### Changed + +* Default color theme from a variant of the Zenburn theme, to a + variant of the Solarized dark theme. +* Default `pad` from 2x2 to 0x0 (i.e. no padding at all). +* Current working directory (as set by OSC-7) is now passed to the + program executed by the `pipe-*` key bindings ([#1166][1166]). +* `DECRPM` replies (to `DECRQM` queries) now report a value of `4` + ("permanently reset") instead of `2` ("reset") for DEC private + modes that are known but unsupported. +* Set `PWD` environment variable in the slave process ([#1179][1179]). + +[1166]: https://codeberg.org/dnkl/foot/issues/1166 +[1179]: https://codeberg.org/dnkl/foot/issues/1179 + + +### Deprecated +### Removed +### Fixed + +* Crash in `foot --server` on key press, after another `footclient` + has terminated very early (for example, by trying to launch a + non-existing shell/client). +* Glitchy rendering when scrolling in the scrollback, on compositors + that does not allow Wayland buffer re-use (e.g. KDE/plasma) + ([#1173][1173]) +* Scrollback search matches not being highlighted correctly, on + compositors that does now allow Wayland buffer re-use + (e.g. KDE/plasma). + +[1173]: https://codeberg.org/dnkl/foot/issues/1173 + + +### Security +### Contributors + +* Craig Barnes + + +## 1.13.1 + +### Changed + +* Window is now dimmed while in Unicode input mode. + + +### Fixed + +* Compiling against wayland-protocols < 1.25 +* Crash on buggy compositors (GNOME) that sometimes send pointer-enter + events with a NULL surface. Foot now ignores these events, and the + subsequent motion and leave events. +* Regression: “random” selected empty cells being highlighted as + selected when they should not. +* Crash when either resizing the terminal window, or scrolling in the + scrollback history ([#1074][1074]) +* OSC-8 URLs with matching IDs, but mismatching URIs being incorrectly + connected. + +[1074]: https://codeberg.org/dnkl/foot/pulls/1074 + + +## 1.13.0 + +### Added + * XDG activation support when opening URLs ([#1058][1058]). * `-Dsystemd-units-dir=` meson command line option. * Support for custom environment variables in `foot.ini` @@ -50,10 +126,21 @@ * Support for jumping to previous/next prompt (requires shell integration). By default bound to `ctrl`+`shift`+`z` and `ctrl`+`shift`+`x` respectively ([#30][30]). +* `colors.search-box-no-match` and `colors.search-box-match` options + to `foot.ini` ([#1112][1112]). +* Very basic Unicode input mode via the new + `key-bindings.unicode-input` and `search-bindings.unicode-input` key + bindings. Note that there is no visual feedback, as the preferred + way of entering Unicode characters is with an IME ([#1116][1116]). +* Support for `xdg_toplevel.wm_capabilities`, to adapt the client-side + decoration buttons to the compositor capabilities ([#1061][1061]). [1058]: https://codeberg.org/dnkl/foot/issues/1058 [1070]: https://codeberg.org/dnkl/foot/issues/1070 [30]: https://codeberg.org/dnkl/foot/issues/30 +[1112]: https://codeberg.org/dnkl/foot/issues/1112 +[1116]: https://codeberg.org/dnkl/foot/issues/1116 +[1061]: https://codeberg.org/dnkl/foot/pulls/1061 ### Changed @@ -64,12 +151,13 @@ mode ([#1084][1084]). * NUL is now stripped when pasting in non-bracketed mode ([#1084][1084]). +* `alt`+`escape` now emits `\E\E` instead of a `CSI 27` sequence + ([#1105][1105]). [1084]: https://codeberg.org/dnkl/foot/issues/1084 +[1105]: https://codeberg.org/dnkl/foot/issues/1105 -### Deprecated -### Removed ### Fixed * Graphical corruption when viewport is at the top of the scrollback, @@ -80,14 +168,41 @@ * Workaround for buggy compositors (e.g. some versions of GNOME) allowing drag-and-drops even though foot has reported it does not support the offered mime-types ([#1092][1092]). +* Keyboard enter/leave events being ignored if there is no keymap + ([#1097][1097]). +* Crash when application emitted an invalid `CSI 38;5;m`, `CSI + 38:5:m`, `CSI 48;5;m` or `CSI 48:5:m` sequence + ([#1111][1111]). +* Certain dead-key combinations resulting in different escape + sequences compared to kitty, when the kitty keyboard protocol is + used ([#1120][1120]). +* Search matches ending with a double-width character not being + highlighted correctly. +* Selection not being cancelled correctly when scrolled out. +* Extending a multi-page selection behaving inconsistently. +* Poor performance when making very large selections ([#1114][1114]). +* Bogus error message when using systemd socket activation for server + mode ([#1107][1107]) +* Empty line at the bottom after a window resize ([#1108][1108]). [1055]: https://codeberg.org/dnkl/foot/issues/1055 [1092]: https://codeberg.org/dnkl/foot/issues/1092 +[1097]: https://codeberg.org/dnkl/foot/issues/1097 +[1111]: https://codeberg.org/dnkl/foot/issues/1111 +[1120]: https://codeberg.org/dnkl/foot/issues/1120 +[1114]: https://codeberg.org/dnkl/foot/issues/1114 +[1107]: https://codeberg.org/dnkl/foot/issues/1107 +[1108]: https://codeberg.org/dnkl/foot/issues/1108 -### Security ### Contributors +* Craig Barnes +* Lorenz +* Max Gautier +* Simon Ser +* Stefan Prosiegel + ## 1.12.1 diff --git a/INSTALL.md b/INSTALL.md index ae0598d8..da3a667e 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -44,6 +44,8 @@ subprojects. * pixman * wayland (_client_ and _cursor_ libraries) * xkbcommon +* utf8proc (_optional_, needed for grapheme clustering) +* libutempter (_optional_, needed for utmp logging) * [fcft](https://codeberg.org/dnkl/fcft) [^1] [^1]: can also be built as subprojects, in which case they are @@ -140,16 +142,17 @@ mkdir -p bld/release && cd bld/release Available compile-time options: -| Option | Type | Default | Description | Extra dependencies | -|--------------------------------------|---------|-------------------------|-------------------------------------------------------|--------------------| -| `-Ddocs` | feature | `auto` | Builds and install documentation | scdoc | -| `-Dtests` | bool | `true` | Build tests (adds a `ninja test` build target) | none | -| `-Dime` | bool | `true` | Enables IME support | None | -| `-Dgrapheme-clustering` | feature | `auto` | Enables grapheme clustering | libutf8proc | -| `-Dterminfo` | feature | `enabled` | Build and install terminfo files | tic (ncurses) | -| `-Ddefault-terminfo` | string | `foot` | Default value of `TERM` | none | -| `-Dcustom-terminfo-install-location` | string | `${datadir}/terminfo` | Value to set `TERMINFO` to | None | -| `-Dsystemd-units-dir` | string | `${systemduserunitdir}` | Where to install the systemd service files (absolute) | None | +| Option | Type | Default | Description | Extra dependencies | +|--------------------------------------|---------|-------------------------|-----------------------------------------------------------|--------------------| +| `-Ddocs` | feature | `auto` | Builds and install documentation | scdoc | +| `-Dtests` | bool | `true` | Build tests (adds a `ninja test` build target) | none | +| `-Dime` | bool | `true` | Enables IME support | None | +| `-Dgrapheme-clustering` | feature | `auto` | Enables grapheme clustering | libutf8proc | +| `-Dterminfo` | feature | `enabled` | Build and install terminfo files | tic (ncurses) | +| `-Ddefault-terminfo` | string | `foot` | Default value of `TERM` | none | +| `-Dcustom-terminfo-install-location` | string | `${datadir}/terminfo` | Value to set `TERMINFO` to | None | +| `-Dsystemd-units-dir` | string | `${systemduserunitdir}` | Where to install the systemd service files (absolute) | None | +| `-Ddefault-utempter-path` | feature | `auto` | Default path to utempter binary (‘none’ disables default) | libutempter | Documentation includes the man pages, readme, changelog and license files. diff --git a/client.c b/client.c index 7624e7db..2a802d16 100644 --- a/client.c +++ b/client.c @@ -408,6 +408,28 @@ main(int argc, char *const *argv) cwd = _cwd; } + const char *pwd = getenv("PWD"); + if (pwd != NULL) { + char *resolved_path_cwd = realpath(cwd, NULL); + char *resolved_path_pwd = realpath(pwd, NULL); + + if (resolved_path_cwd != NULL && + resolved_path_pwd != NULL && + strcmp(resolved_path_cwd, resolved_path_pwd) == 0) + { + /* + * The resolved path of $PWD matches the resolved path of + * the *actual* working directory - use $PWD. + * + * This makes a difference when $PWD refers to a symlink. + */ + cwd = pwd; + } + + free(resolved_path_cwd); + free(resolved_path_pwd); + } + if (client_environment) { for (char **e = environ; *e != NULL; e++) { if (!push_string(&envp, *e, &total_len)) diff --git a/config.c b/config.c index e14dd343..95a06936 100644 --- a/config.c +++ b/config.c @@ -30,8 +30,8 @@ #include "xmalloc.h" #include "xsnprintf.h" -static const uint32_t default_foreground = 0xdcdccc; -static const uint32_t default_background = 0x111111; +static const uint32_t default_foreground = 0x839496; +static const uint32_t default_background = 0x002b36; static const size_t min_csd_border_width = 5; @@ -48,23 +48,23 @@ static const size_t min_csd_border_width = 5; static const uint32_t default_color_table[256] = { // Regular - 0x222222, - 0xcc9393, - 0x7f9f7f, - 0xd0bf8f, - 0x6ca0a3, - 0xdc8cc3, - 0x93e0e3, - 0xdcdccc, + 0x073642, + 0xdc322f, + 0x859900, + 0xb58900, + 0x268bd2, + 0xd33682, + 0x2aa198, + 0xeee8d5, // Bright - 0x666666, - 0xdca3a3, - 0xbfebbf, - 0xf0dfaf, - 0x8cd0d3, - 0xfcace3, - 0xb3ffff, + 0x08404f, + 0xe35f5c, + 0x9fb700, + 0xd9a400, + 0x4ba1de, + 0xdc619d, + 0x32c1b6, 0xffffff, // 6x6x6 RGB cube @@ -117,6 +117,7 @@ static const char *const binding_action_map[] = { [BIND_ACTION_TEXT_BINDING] = "text-binding", [BIND_ACTION_PROMPT_PREV] = "prompt-prev", [BIND_ACTION_PROMPT_NEXT] = "prompt-next", + [BIND_ACTION_UNICODE_INPUT] = "unicode-input", /* Mouse-specific actions */ [BIND_ACTION_SELECT_BEGIN] = "select-begin", @@ -149,6 +150,7 @@ static const char *const search_binding_action_map[] = { [BIND_ACTION_SEARCH_EXTEND_LINE] = "extend-to-end-line", [BIND_ACTION_SEARCH_CLIPBOARD_PASTE] = "clipboard-paste", [BIND_ACTION_SEARCH_PRIMARY_PASTE] = "primary-paste", + [BIND_ACTION_SEARCH_UNICODE_INPUT] = "unicode-input", }; static const char *const url_binding_action_map[] = { @@ -903,6 +905,9 @@ parse_section_main(struct context *ctx) return true; } + else if (strcmp(key, "underline-thickness") == 0) + return value_to_pt_or_px(ctx, &conf->underline_thickness); + else if (strcmp(key, "dpi-aware") == 0) { if (strcmp(value, "auto") == 0) conf->dpi_aware = DPI_AWARE_AUTO; @@ -940,6 +945,18 @@ parse_section_main(struct context *ctx) else if (strcmp(key, "box-drawings-uses-font-glyphs") == 0) return value_to_bool(ctx, &conf->box_drawings_uses_font_glyphs); + else if (strcmp(key, "utempter") == 0) { + if (!value_to_str(ctx, &conf->utempter_path)) + return false; + + if (strcmp(conf->utempter_path, "none") == 0) { + free(conf->utempter_path); + conf->utempter_path = NULL; + } + + return true; + } + else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; @@ -1172,6 +1189,34 @@ parse_section_colors(struct context *ctx) return true; } + else if (strcmp(key, "search-box-no-match") == 0) { + if (!value_to_two_colors( + ctx, + &conf->colors.search_box.no_match.fg, + &conf->colors.search_box.no_match.bg, + false)) + { + return false; + } + + conf->colors.use_custom.search_box_no_match = true; + return true; + } + + else if (strcmp(key, "search-box-match") == 0) { + if (!value_to_two_colors( + ctx, + &conf->colors.search_box.match.fg, + &conf->colors.search_box.match.bg, + false)) + { + return false; + } + + conf->colors.use_custom.search_box_match = true; + return true; + } + else if (strcmp(key, "urls") == 0) { if (!value_to_color(ctx, &conf->colors.url, false)) return false; @@ -1976,6 +2021,9 @@ resolve_key_binding_collisions(struct config *conf, const char *section_name, sym_equal = (binding1->m.button == binding2->m.button && binding1->m.count == binding2->m.count); break; + + default: + BUG("unhandled key binding type"); } if (!mods_equal || !sym_equal) @@ -2610,6 +2658,9 @@ parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_ar if (!section_parser(ctx)) error_or_continue(); + + /* For next iteration of getline() */ + errno = 0; } if (errno != 0) { @@ -2787,8 +2838,8 @@ config_load(struct config *conf, const char *conf_path, .width = 700, .height = 500, }, - .pad_x = 2, - .pad_y = 2, + .pad_x = 0, + .pad_y = 0, .resize_delay_ms = 100, .bold_in_bright = { .enabled = false, @@ -2802,6 +2853,7 @@ config_load(struct config *conf, const char *conf_path, .vertical_letter_offset = {.pt = 0, .px = 0}, .use_custom_underline_offset = false, .box_drawings_uses_font_glyphs = false, + .underline_thickness = {.pt = 0., .px = -1}, .dpi_aware = DPI_AWARE_AUTO, /* DPI-aware when scaling-factor == 1 */ .bell = { .urgent = false, @@ -2899,6 +2951,9 @@ config_load(struct config *conf, const char *conf_path, }, .env_vars = tll_init(), + .utempter_path = (strlen(FOOT_DEFAULT_UTEMPTER_PATH) > 0 + ? xstrdup(FOOT_DEFAULT_UTEMPTER_PATH) + : NULL), .notifications = tll_init(), }; @@ -3187,6 +3242,9 @@ config_clone(const struct config *old) tll_push_back(conf->env_vars, copy); } + conf->utempter_path = + old->utempter_path != NULL ? xstrdup(old->utempter_path) : NULL; + conf->notifications.length = 0; conf->notifications.head = conf->notifications.tail = 0; tll_foreach(old->notifications, it) { @@ -3253,6 +3311,7 @@ config_free(struct config *conf) tll_remove(conf->env_vars, it); } + free(conf->utempter_path); user_notifications_free(&conf->notifications); } diff --git a/config.h b/config.h index de5d8a7b..d35abbb2 100644 --- a/config.h +++ b/config.h @@ -150,6 +150,7 @@ struct config { bool use_custom_underline_offset; struct pt_or_px underline_offset; + struct pt_or_px underline_thickness; bool box_drawings_uses_font_glyphs; bool can_shape_grapheme; @@ -217,11 +218,25 @@ struct config { uint32_t bg; } scrollback_indicator; + struct { + struct { + uint32_t fg; + uint32_t bg; + } no_match; + + struct { + uint32_t fg; + uint32_t bg; + } match; + } search_box; + struct { bool selection:1; bool jump_label:1; bool scrollback_indicator:1; bool url:1; + bool search_box_no_match:1; + bool search_box_match:1; uint8_t dim; } use_custom; } colors; @@ -304,6 +319,8 @@ struct config { env_var_list_t env_vars; + char *utempter_path; + struct { enum fcft_scaling_filter fcft_filter; bool overflowing_glyphs; diff --git a/csi.c b/csi.c index 57cae6b3..2eacaaa9 100644 --- a/csi.c +++ b/csi.c @@ -128,7 +128,8 @@ csi_sgr(struct terminal *term) term->vt.params.v[i + 1].value == 5) { src = COLOR_BASE256; - color = term->vt.params.v[i + 2].value; + color = min(term->vt.params.v[i + 2].value, + ALEN(term->colors.table) - 1); i += 2; } @@ -149,7 +150,8 @@ csi_sgr(struct terminal *term) term->vt.params.v[i].sub.value[0] == 5) { src = COLOR_BASE256; - color = term->vt.params.v[i].sub.value[1]; + color = min(term->vt.params.v[i].sub.value[1], + ALEN(term->colors.table) - 1); } /* @@ -484,7 +486,7 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) } tll_free(term->alt.scroll_damage); - term_damage_all(term); + term_damage_view(term); } term_update_ascii_printer(term); break; @@ -533,47 +535,65 @@ decrst(struct terminal *term, unsigned param) decset_decrst(term, param, false); } -static bool -decrqm(const struct terminal *term, unsigned param, bool *enabled) +/* + * These values represent the current state of a DEC private mode, + * as returned in the DECRPM reply to a DECRQM query. + */ +enum decrpm_status { + DECRPM_NOT_RECOGNIZED = 0, + DECRPM_SET = 1, + DECRPM_RESET = 2, + DECRPM_PERMANENTLY_SET = 3, + DECRPM_PERMANENTLY_RESET = 4, +}; + +static enum decrpm_status +decrpm(bool enabled) +{ + return enabled ? DECRPM_SET : DECRPM_RESET; +} + +static enum decrpm_status +decrqm(const struct terminal *term, unsigned param) { switch (param) { - case 1: *enabled = term->cursor_keys_mode == CURSOR_KEYS_APPLICATION; return true; - case 3: *enabled = false; return true; - case 4: *enabled = false; return true; - case 5: *enabled = term->reverse; return true; - case 6: *enabled = term->origin; return true; - case 7: *enabled = term->auto_margin; return true; - case 9: *enabled = false; /* term->mouse_tracking == MOUSE_X10; */ return true; - case 12: *enabled = term->cursor_blink.decset; return true; - case 25: *enabled = !term->hide_cursor; return true; - case 45: *enabled = term->reverse_wrap; return true; - case 66: *enabled = term->keypad_keys_mode == KEYPAD_APPLICATION; return true; - case 80: *enabled = !term->sixel.scrolling; return true; - case 1000: *enabled = term->mouse_tracking == MOUSE_CLICK; return true; - case 1001: *enabled = false; return true; - case 1002: *enabled = term->mouse_tracking == MOUSE_DRAG; return true; - case 1003: *enabled = term->mouse_tracking == MOUSE_MOTION; return true; - case 1004: *enabled = term->focus_events; return true; - case 1005: *enabled = false; /* term->mouse_reporting == MOUSE_UTF8; */ return true; - case 1006: *enabled = term->mouse_reporting == MOUSE_SGR; return true; - case 1007: *enabled = term->alt_scrolling; return true; - case 1015: *enabled = term->mouse_reporting == MOUSE_URXVT; return true; - case 1016: *enabled = term->mouse_reporting == MOUSE_SGR_PIXELS; return true; - case 1034: *enabled = term->meta.eight_bit; return true; - case 1035: *enabled = term->num_lock_modifier; return true; - case 1036: *enabled = term->meta.esc_prefix; return true; - case 1042: *enabled = term->bell_action_enabled; return true; + case 1: return decrpm(term->cursor_keys_mode == CURSOR_KEYS_APPLICATION); + case 3: return DECRPM_PERMANENTLY_RESET; + case 4: return DECRPM_PERMANENTLY_RESET; + case 5: return decrpm(term->reverse); + case 6: return decrpm(term->origin); + case 7: return decrpm(term->auto_margin); + case 9: return DECRPM_PERMANENTLY_RESET; /* term->mouse_tracking == MOUSE_X10; */ + case 12: return decrpm(term->cursor_blink.decset); + case 25: return decrpm(!term->hide_cursor); + case 45: return decrpm(term->reverse_wrap); + case 66: return decrpm(term->keypad_keys_mode == KEYPAD_APPLICATION); + case 80: return decrpm(!term->sixel.scrolling); + case 1000: return decrpm(term->mouse_tracking == MOUSE_CLICK); + case 1001: return DECRPM_PERMANENTLY_RESET; + case 1002: return decrpm(term->mouse_tracking == MOUSE_DRAG); + case 1003: return decrpm(term->mouse_tracking == MOUSE_MOTION); + case 1004: return decrpm(term->focus_events); + case 1005: return DECRPM_PERMANENTLY_RESET; /* term->mouse_reporting == MOUSE_UTF8; */ + case 1006: return decrpm(term->mouse_reporting == MOUSE_SGR); + case 1007: return decrpm(term->alt_scrolling); + case 1015: return decrpm(term->mouse_reporting == MOUSE_URXVT); + case 1016: return decrpm(term->mouse_reporting == MOUSE_SGR_PIXELS); + case 1034: return decrpm(term->meta.eight_bit); + case 1035: return decrpm(term->num_lock_modifier); + case 1036: return decrpm(term->meta.esc_prefix); + case 1042: return decrpm(term->bell_action_enabled); case 47: /* FALLTHROUGH */ case 1047: /* FALLTHROUGH */ - case 1049: *enabled = term->grid == &term->alt; return true; - case 1070: *enabled = term->sixel.use_private_palette; return true; - case 2004: *enabled = term->bracketed_paste; return true; - case 2026: *enabled = term->render.app_sync_updates.enabled; return true; - case 8452: *enabled = term->sixel.cursor_right_of_graphics; return true; - case 737769: *enabled = term_ime_is_enabled(term); return true; + case 1049: return decrpm(term->grid == &term->alt); + case 1070: return decrpm(term->sixel.use_private_palette); + case 2004: return decrpm(term->bracketed_paste); + case 2026: return decrpm(term->render.app_sync_updates.enabled); + case 8452: return decrpm(term->sixel.cursor_right_of_graphics); + case 737769: return decrpm(term_ime_is_enabled(term)); } - return false; + return DECRPM_NOT_RECOGNIZED; } static void @@ -1719,15 +1739,9 @@ csi_dispatch(struct terminal *term, uint8_t final) * 3 - permanently set * 4 - permantently reset */ - bool enabled; - unsigned value; - if (decrqm(term, param, &enabled)) - value = enabled ? 1 : 2; - else - value = 0; - + unsigned status = decrqm(term, param); char reply[32]; - size_t n = xsnprintf(reply, sizeof(reply), "\033[?%u;%u$y", param, value); + size_t n = xsnprintf(reply, sizeof(reply), "\033[?%u;%u$y", param, status); term_to_slave(term, reply, n); break; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 9a9e3a11..c9e1051d 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -132,6 +132,18 @@ commented out will usually be installed to */etc/xdg/foot/foot.ini*. Default: _unset_. +*underline-thickness* + Use a custom thickness (height) for underlines. The thickness is, by + default, in _points_. + + To specify a thickness in _pixels_, append *px*: + *underline-thickness=1px*. + + If left unset (the default), the thickness specified in the font is + used. + + Default: _unset_ + *box-drawings-uses-font-glyphs* Boolean. When disabled, foot generates box/line drawing characters itself. The are several advantages to doing this instead of using font glyphs: @@ -198,7 +210,7 @@ commented out will usually be installed to */etc/xdg/foot/foot.ini*. To instead center the grid content, append *center* (e.g. *pad=5x5 center*). - Default: _2x2_. + Default: _0x0_. *resize-delay-ms* Time, in milliseconds, of "idle time" before foot sends the new @@ -305,6 +317,10 @@ commented out will usually be installed to */etc/xdg/foot/foot.ini*. (including SMT). Note that this is not always the best value. In some cases, the number of physical _cores_ is better. +*utempter* + Path to utempter helper binary. Set to *none* to disable utmp + records. Default: _@utempter@_. + # SECTION: environment This section is used to define environment variables that will be set @@ -495,21 +511,23 @@ can configure the background transparency with the _alpha_ option. *foreground* Default foreground color. This is the color used when no ANSI - color is being used. Default: _dcdccc_. + color is being used. Default: _839496_. *background* Default background color. This is the color used when no ANSI - color is being used. Default: _111111_. + color is being used. Default: _002b36_. *regular0*, *regular1* *..* *regular7* The eight basic ANSI colors (Black, Red, Green, Yellow, Blue, - Magenta, Cyan, White). Default: _222222_, _cc9393_, _7f9f7f_, _d0bf8f_, - _6ca0a3_, _dc8cc3_, _93e0e3_ and _dcdccc_ (a variant of the _zenburn_ theme). + Magenta, Cyan, White). Default: _073642_, _dc322f_, _859900_, + _b58900_, _268bd2_, _d33682_, _2aa198_ and _eee8d5_ (a variant of + the _solarized dark_ theme). *bright0*, *bright1* *..* *bright7* The eight bright ANSI colors (Black, Red, Green, Yellow, Blue, - Magenta, Cyan, White). Default: _666666_, _dca3a3_, _bfebbf_, _f0dfaf_, - _8cd0d3_, _fcace3_, _b3ffff_ and _ffffff_ (a variant of the _zenburn_ theme). + Magenta, Cyan, White). Default: _08404f_, _e35f5c_, _9fb700_, + _d9a400_, _4ba1de_, _dc619d_, _32c1b6_ and _ffffff_ (a variant of + the _solarized dark_ theme). *dim0*, *dim1* *..* *dim7* Custom colors to use with dimmed colors. Dimmed colors do not have @@ -566,6 +584,16 @@ can configure the background transparency with the _alpha_ option. (indicator itself) colors for the scrollback indicator. Default: _regular0 bright4_. +*search-box-no-match* + Two color values specifying the foreground (text) and background + colors for the scrollback search box, when there are no + matches. Default: _regular0 regular1_. + +*search-box-match* + Two color values specifying the foreground (text) and background + colors for the scrollback search box, when the search box is + either empty, or there are matches. Default: _regular0 regular3_. + *urls* Color to use for the underline used to highlight URLs in URL mode. Default: _regular3_. @@ -781,6 +809,32 @@ e.g. *search-start=none*. Jump the next prompt (requires shell integration, see *foot*(1)). Default: _Control+Shift+x_. +*unicode-input* + Input a Unicode character by typing its codepoint in hexadecimal, + followed by *Enter* or *Space*. + + For example, to input the character _ö_ (LATIN SMALL LETTER O WITH + DIAERESIS, Unicode codepoint 0xf6), you would first activate this + key binding, then type: *f*, *6*, *Enter*. + + Another example: to input 😍 (SMILING FACE WITH HEART-SHAPED EYES, + Unicode codepoint 0x1f60d), activate this key binding, then type: + *1*, *f*, *6*, *0*, *d*, *Enter*. + + Recognized key bindings in Unicode input mode: + + - Enter, Space: commit the Unicode character, then exit this mode. + - Escape, q, Ctrl+c, Ctrl+d, Ctrl+g: abort input, then exit this mode. + - 0-9, a-f: append next digit to the Unicode's codepoint. + - Backspace: undo the last digit. + + Note that there is no visual feedback while in this mode. This is + by design; foot's Unicode input mode is considered to be a + fallback. The preferred way of entering Unicode characters, emojis + etc is by using an IME. + + Default: _none_. + # SECTION: search-bindings This section lets you override the default key bindings used in @@ -863,6 +917,10 @@ scrollback search mode. The syntax is exactly the same as the regular Paste from the _primary selection_ into the search buffer. Default: _Shift+Insert_. +*unicode-input* + Unicode input mode. See _key-bindings.unicode-input_ for + details. Default: _none_. + # SECTION: url-bindings This section lets you override the default key bindings used in URL diff --git a/doc/meson.build b/doc/meson.build index 75c3be95..86e75952 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -2,9 +2,16 @@ sh = find_program('sh', native: true) scdoc_prog = find_program(scdoc.get_variable('scdoc'), native: true) +if utempter_path == '' + default_utempter_value = 'not set' +else + default_utempter_value = utempter_path +endif + conf_data = configuration_data( { 'default_terminfo': get_option('default-terminfo'), + 'utempter': default_utempter_value, } ) diff --git a/foot-server@.service.in b/foot-server@.service.in index 81c13bb4..c40bb454 100644 --- a/foot-server@.service.in +++ b/foot-server@.service.in @@ -1,8 +1,8 @@ [Service] -ExecStart=@bindir@/foot --server=0 +ExecStart=@bindir@/foot --server=3 Environment=WAYLAND_DISPLAY=%i +UnsetEnvironment=LISTEN_PID LISTEN_FDS LISTEN_FDNAMES NonBlocking=true -StandardInput=socket [Unit] Requires=%N.socket diff --git a/foot.info b/foot.info index 2dae3eca..f4030b22 100644 --- a/foot.info +++ b/foot.info @@ -218,6 +218,8 @@ knp=\E[6~, kpp=\E[5~, kri=\E[1;2A, + kxIN=\E[I, + kxOUT=\E[O, oc=\E]104\E\\, op=\E[39;49m, rc=\E8, diff --git a/foot.ini b/foot.ini index 7220294c..b2f71fc7 100644 --- a/foot.ini +++ b/foot.ini @@ -17,13 +17,14 @@ # horizontal-letter-offset=0 # vertical-letter-offset=0 # underline-offset= +# underline-thickness= # box-drawings-uses-font-glyphs=no # dpi-aware=auto # initial-window-size-pixels=700x500 # Or, # initial-window-size-chars= # initial-window-mode=windowed -# pad=2x2 # optionally append 'center' +# pad=0x0 # optionally append 'center' # resize-delay-ms=100 # notify=notify-send -a ${app-id} -i ${app-id} ${title} ${body} @@ -32,6 +33,7 @@ # word-delimiters=,│`|:"'()[]{}<> # selection-target=primary # workers= +# utempter=/usr/lib/utempter/utempter [environment] # name=value @@ -68,27 +70,27 @@ [colors] # alpha=1.0 -# foreground=dcdccc -# background=111111 +# background=002b36 +# foreground=839496 ## Normal/regular colors (color palette 0-7) -# regular0=222222 # black -# regular1=cc9393 # red -# regular2=7f9f7f # green -# regular3=d0bf8f # yellow -# regular4=6ca0a3 # blue -# regular5=dc8cc3 # magenta -# regular6=93e0e3 # cyan -# regular7=dcdccc # white +# regular0=073642 # black +# regular1=dc322f # red +# regular2=859900 # green +# regular3=b58900 # yellow +# regular4=268bd2 # blue +# regular5=d33682 # magenta +# regular6=2aa198 # cyan +# regular7=eee8d5 # white ## Bright colors (color palette 8-15) -# bright0=666666 # bright black -# bright1=dca3a3 # bright red -# bright2=bfebbf # bright green -# bright3=f0dfaf # bright yellow -# bright4=8cd0d3 # bright blue -# bright5=fcace3 # bright magenta -# bright6=b3ffff # bright cyan +# bright0=08404f # bright black +# bright1=e35f5c # bright red +# bright2=9fb700 # bright green +# bright3=d9a400 # bright yellow +# bright4=4ba1de # bright blue +# bright5=dc619d # bright magenta +# bright6=32c1b6 # bright cyan # bright7=ffffff # bright white ## dimmed colors (see foot.ini(5) man page) @@ -104,9 +106,11 @@ ## Misc colors # selection-foreground= # selection-background= -# jump-labels= +# jump-labels= # black-on-yellow +# scrollback-indicator= # black-on-bright-blue +# search-box-no-match= # black-on-red +# search-box-match= # black-on-yellow # urls= -# scrollback-indicator= [csd] # preferred=server @@ -148,6 +152,7 @@ # show-urls-persistent=none # prompt-prev=Control+Shift+z # prompt-next=Control+Shift+x +# unicode-input=none # noop=none [search-bindings] @@ -170,6 +175,7 @@ # extend-to-end-line=Control+l # clipboard-paste=Control+v Control+Shift+v Control+y XF86Paste # primary-paste=Shift+Insert +# unicode-input=none [url-bindings] # cancel=Control+g Control+c Control+d Escape diff --git a/grid.c b/grid.c index a21f24a2..7bfef5cb 100644 --- a/grid.c +++ b/grid.c @@ -318,7 +318,7 @@ grid_row_alloc(int cols, bool initialize) { struct row *row = xmalloc(sizeof(*row)); row->dirty = false; - row->linebreak = true; + row->linebreak = false; row->extra = NULL; row->prompt_marker = false; @@ -538,7 +538,7 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, } else { /* Scrollback is full, need to re-use a row */ grid_row_reset_extra(new_row); - new_row->linebreak = true; + new_row->linebreak = false; new_row->prompt_marker = false; tll_foreach(old_grid->sixel_images, it) { @@ -740,7 +740,7 @@ grid_resize_and_reflow( } } - if (!old_row->linebreak /*&& col_count > 0*/) { + if (!old_row->linebreak && col_count > 0) { /* Don’t truncate logical lines */ col_count = old_cols; } @@ -878,14 +878,6 @@ grid_resize_and_reflow( &new_row->cells[new_col_idx], &old_row->cells[from], amount * sizeof(struct cell)); - /* - * We’ve “printed” to this line - reset linebreak. - * - * If the old line ends with a hard linebreak, we’ll - * set linebreak=true on the last new row we print to. - */ - new_row->linebreak = false; - count -= amount; from += amount; new_col_idx += amount; @@ -943,13 +935,29 @@ grid_resize_and_reflow( start += cols; } - - if (old_row->linebreak && col_count > 0) { + if (old_row->linebreak) { /* Erase the remaining cells */ memset(&new_row->cells[new_col_idx], 0, (new_cols - new_col_idx) * sizeof(new_row->cells[0])); new_row->linebreak = true; - line_wrap(); + + if (r + 1 < old_rows) + line_wrap(); + else if (new_row->extra != NULL && + new_row->extra->uri_ranges.count > 0) + { + /* + * line_wrap() "closes" still-open URIs. Since this is + * the *last* row, and since we’re line-breaking due + * to a hard line-break (rather than running out of + * cells in the "new_row"), there shouldn’t be an open + * URI (it would have been closed when we reached the + * end of the URI while reflowing the last "old" + * row). + */ + uint32_t last_idx = new_row->extra->uri_ranges.count - 1; + xassert(new_row->extra->uri_ranges.v[last_idx].end >= 0); + } } grid_row_free(old_grid[old_row_idx]); @@ -992,6 +1000,7 @@ grid_resize_and_reflow( /* Set offset such that the last reflowed row is at the bottom */ grid->offset = new_row_idx - new_screen_rows + 1; + while (grid->offset < 0) grid->offset += new_rows; while (new_grid[grid->offset] == NULL) @@ -1004,23 +1013,6 @@ grid_resize_and_reflow( new_grid[idx] = grid_row_alloc(new_cols, true); } - grid->view = view_follows ? grid->offset : viewport.row; - - /* If enlarging the window, the old viewport may be too far down, - * with unallocated rows. Make sure this cannot happen */ - while (true) { - int idx = (grid->view + new_screen_rows - 1) & (new_rows - 1); - if (new_grid[idx] != NULL) - break; - grid->view--; - if (grid->view < 0) - grid->view += new_rows; - } - for (size_t r = 0; r < new_screen_rows; r++) { - int UNUSED idx = (grid->view + r) & (new_rows - 1); - xassert(new_grid[idx] != NULL); - } - /* Free old grid (rows already free:d) */ free(grid->rows); @@ -1028,6 +1020,17 @@ grid_resize_and_reflow( grid->num_rows = new_rows; grid->num_cols = new_cols; + /* + * Set new viewport, making sure it’s not too far down. + * + * This is done by using scrollback-start relative cooardinates, + * and bounding the new viewport to (grid_rows - screen_rows). + */ + int sb_view = grid_row_abs_to_sb( + grid, new_screen_rows, view_follows ? grid->offset : viewport.row); + grid->view = grid_row_sb_to_abs( + grid, new_screen_rows, min(sb_view, new_rows - new_screen_rows)); + /* Convert absolute coordinates to screen relative */ cursor.row -= grid->offset; while (cursor.row < 0) diff --git a/input.c b/input.c index fca46050..0a3773bc 100644 --- a/input.c +++ b/input.c @@ -36,6 +36,7 @@ #include "spawn.h" #include "terminal.h" #include "tokenize.h" +#include "unicode-mode.h" #include "url-mode.h" #include "util.h" #include "vt.h" @@ -282,7 +283,7 @@ execute_binding(struct seat *seat, struct terminal *term, } } - if (!spawn(term->reaper, NULL, binding->aux->pipe.args, + if (!spawn(term->reaper, term->cwd, binding->aux->pipe.args, pipe_fd[0], stdout_fd, stderr_fd, NULL)) goto pipe_err; @@ -416,6 +417,10 @@ execute_binding(struct seat *seat, struct terminal *term, return true; } + case BIND_ACTION_UNICODE_INPUT: + unicode_mode_activate(seat); + return true; + case BIND_ACTION_SELECT_BEGIN: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_CHAR_WISE, false); @@ -591,9 +596,6 @@ keyboard_enter(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, LOG_DBG("%s: keyboard_enter: keyboard=%p, serial=%u, surface=%p", seat->name, (void *)wl_keyboard, serial, (void *)surface); - if (seat->kbd.xkb == NULL) - return; - term_kbd_focus_in(term); seat->kbd_focus = term; seat->kbd.serial = serial; @@ -653,9 +655,6 @@ keyboard_leave(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, LOG_DBG("keyboard_leave: keyboard=%p, serial=%u, surface=%p", (void *)wl_keyboard, serial, (void *)surface); - if (seat->kbd.xkb == NULL) - return; - xassert( seat->kbd_focus == NULL || surface == NULL || /* Seen on Sway 1.2 */ @@ -1245,7 +1244,7 @@ emit_escapes: ? ctx->level0_syms.syms[0] : sym; - if (composed && is_text) + if (composed) key = utf32; else { key = xkb_keysym_to_utf32(sym_to_use); @@ -1407,11 +1406,16 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, seat->kbd.xkb_keymap, key, layout_idx, 0, &raw_syms); const struct key_binding_set *bindings = key_binding_for( - seat->wayl->key_binding_manager, term, seat); + seat->wayl->key_binding_manager, term->conf, seat); xassert(bindings != NULL); if (pressed) { - if (term->is_searching) { + if (seat->unicode_mode.active) { + unicode_mode_input(seat, term, sym); + return; + } + + else if (term->is_searching) { if (should_repeat) start_repeater(seat, key); @@ -1705,11 +1709,9 @@ wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface, wl_fixed_t surface_x, wl_fixed_t surface_y) { - xassert(surface != NULL); - xassert(serial != 0); - - if (surface == NULL) { + if (unlikely(surface == NULL)) { /* Seen on mutter-3.38 */ + LOG_WARN("compositor sent pointer_enter event with a NULL surface"); return; } @@ -1862,6 +1864,16 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, struct seat *seat = data; struct wayland *wayl = seat->wayl; struct terminal *term = seat->mouse_focus; + + if (unlikely(term == NULL)) { + /* Typically happens when the compositor sent a pointer enter + * event with a NULL surface - see wl_pointer_enter(). + * + * In this case, we never set seat->mouse_focus (since we + * can’t map the enter event to a specific window). */ + return; + } + struct wl_window *win = term->window; LOG_DBG("pointer_motion: pointer=%p, x=%d, y=%d", (void *)wl_pointer, @@ -2323,7 +2335,7 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, /* Seat has keyboard - use mouse bindings *with* modifiers */ const struct key_binding_set *bindings = key_binding_for( - wayl->key_binding_manager, term, seat); + wayl->key_binding_manager, term->conf, seat); xassert(bindings != NULL); xkb_mod_mask_t mods; diff --git a/key-binding.c b/key-binding.c index 2135abbc..1876a885 100644 --- a/key-binding.c +++ b/key-binding.c @@ -80,17 +80,14 @@ key_binding_new_for_seat(struct key_binding_manager *mgr, } void -key_binding_new_for_term(struct key_binding_manager *mgr, - const struct terminal *term) +key_binding_new_for_conf(struct key_binding_manager *mgr, + const struct wayland *wayl, const struct config *conf) { - const struct config *conf = term->conf; - const struct wayland *wayl = term->wl; - tll_foreach(wayl->seats, it) { struct seat *seat = &it->item; struct key_set *existing = - (struct key_set *)key_binding_for(mgr, term, seat); + (struct key_set *)key_binding_for(mgr, conf, seat); if (existing != NULL) { existing->conf_ref_count++; @@ -116,21 +113,19 @@ key_binding_new_for_term(struct key_binding_manager *mgr, /* Chances are high this set will be requested next */ mgr->last_used_set = &tll_back(mgr->binding_sets); - LOG_DBG("new (term): set=%p, seat=%p, conf=%p, ref-count=1", + LOG_DBG("new (conf): set=%p, seat=%p, conf=%p, ref-count=1", (void *)&tll_back(mgr->binding_sets), (void *)set.seat, (void *)set.conf); } - LOG_DBG("new (term): total number of sets: %zu", + LOG_DBG("new (conf): total number of sets: %zu", tll_length(mgr->binding_sets)); } struct key_binding_set * NOINLINE -key_binding_for(struct key_binding_manager *mgr, const struct terminal *term, +key_binding_for(struct key_binding_manager *mgr, const struct config *conf, const struct seat *seat) { - const struct config *conf = term->conf; - struct key_set *last_used = mgr->last_used_set; if (last_used != NULL && last_used->conf == conf && @@ -192,11 +187,8 @@ key_binding_remove_seat(struct key_binding_manager *mgr, } void -key_binding_unref_term(struct key_binding_manager *mgr, - const struct terminal *term) +key_binding_unref(struct key_binding_manager *mgr, const struct config *conf) { - const struct config *conf = term->conf; - tll_foreach(mgr->binding_sets, it) { struct key_set *set = &it->item; diff --git a/key-binding.h b/key-binding.h index d4bb9f51..a661aa6d 100644 --- a/key-binding.h +++ b/key-binding.h @@ -38,6 +38,7 @@ enum bind_action_normal { BIND_ACTION_TEXT_BINDING, BIND_ACTION_PROMPT_PREV, BIND_ACTION_PROMPT_NEXT, + BIND_ACTION_UNICODE_INPUT, /* Mouse specific actions - i.e. they require a mouse coordinate */ BIND_ACTION_SELECT_BEGIN, @@ -48,7 +49,7 @@ enum bind_action_normal { BIND_ACTION_SELECT_WORD_WS, BIND_ACTION_SELECT_ROW, - BIND_ACTION_KEY_COUNT = BIND_ACTION_PROMPT_NEXT + 1, + BIND_ACTION_KEY_COUNT = BIND_ACTION_UNICODE_INPUT + 1, BIND_ACTION_COUNT = BIND_ACTION_SELECT_ROW + 1, }; @@ -73,6 +74,7 @@ enum bind_action_search { BIND_ACTION_SEARCH_EXTEND_LINE, BIND_ACTION_SEARCH_CLIPBOARD_PASTE, BIND_ACTION_SEARCH_PRIMARY_PASTE, + BIND_ACTION_SEARCH_UNICODE_INPUT, BIND_ACTION_SEARCH_COUNT, }; @@ -109,6 +111,7 @@ typedef tll(struct key_binding) key_binding_list_t; struct terminal; struct seat; +struct wayland; struct key_binding_set { key_binding_list_t key; @@ -126,20 +129,21 @@ void key_binding_manager_destroy(struct key_binding_manager *mgr); void key_binding_new_for_seat( struct key_binding_manager *mgr, const struct seat *seat); -void key_binding_new_for_term( - struct key_binding_manager *mgr, const struct terminal *term); +void key_binding_new_for_conf( + struct key_binding_manager *mgr, const struct wayland *wayl, + const struct config *conf); -/* Returns the set of key bindings associated with this seat/term pair */ +/* Returns the set of key bindings associated with this seat/conf pair */ struct key_binding_set *key_binding_for( - struct key_binding_manager *mgr, const struct terminal *term, + struct key_binding_manager *mgr, const struct config *conf, const struct seat *seat); /* Remove all key bindings tied to the specified seat */ void key_binding_remove_seat( struct key_binding_manager *mgr, const struct seat *seat); -void key_binding_unref_term( - struct key_binding_manager *mgr, const struct terminal *term); +void key_binding_unref( + struct key_binding_manager *mgr, const struct config *conf); void key_binding_load_keymap( struct key_binding_manager *mgr, const struct seat *seat); diff --git a/keymap.h b/keymap.h index 9793d882..79d4b8b3 100644 --- a/keymap.h +++ b/keymap.h @@ -24,7 +24,7 @@ struct key_data { static const struct key_data key_escape[] = { {MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;2;27~"}, - {MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;3;27~"}, + {MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\033"}, {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;4;27~"}, {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;5;27~"}, {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;6;27~"}, diff --git a/main.c b/main.c index 4617a3c7..a3ae579d 100644 --- a/main.c +++ b/main.c @@ -594,6 +594,28 @@ main(int argc, char *const *argv) cwd = _cwd; } + const char *pwd = getenv("PWD"); + if (pwd != NULL) { + char *resolved_path_cwd = realpath(cwd, NULL); + char *resolved_path_pwd = realpath(pwd, NULL); + + if (resolved_path_cwd != NULL && + resolved_path_pwd != NULL && + strcmp(resolved_path_cwd, resolved_path_pwd) == 0) + { + /* + * The resolved path of $PWD matches the resolved path of + * the *actual* working directory - use $PWD. + * + * This makes a difference when $PWD refers to a symlink. + */ + cwd = pwd; + } + + free(resolved_path_cwd); + free(resolved_path_pwd); + } + shm_set_max_pool_size(conf.tweak.max_shm_pool_size); if ((fdm = fdm_init()) == NULL) diff --git a/meson.build b/meson.build index 6dc4d098..4d3b6213 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.12.1', + version: '1.13.1', license: 'MIT', meson_version: '>=0.58.0', default_options: [ @@ -16,9 +16,30 @@ if cc.has_function('memfd_create') add_project_arguments('-DMEMFD_CREATE', language: 'c') endif +utempter_path = get_option('default-utempter-path') +if utempter_path == '' + utempter = find_program( + 'utempter', + required: false, + dirs: [join_paths(get_option('prefix'), get_option('libdir'), 'utempter'), + join_paths(get_option('prefix'), get_option('libexecdir'), 'utempter'), + '/usr/lib/utempter', + '/usr/libexec/utempter', + '/lib/utempter'] + ) + if utempter.found() + utempter_path = utempter.full_path() + else + utempter_path = '' + endif +elif utempter_path == 'none' + utempter_path = '' +endif + add_project_arguments( ['-D_GNU_SOURCE=200809L', - '-DFOOT_DEFAULT_TERM="@0@"'.format(get_option('default-terminfo'))] + + '-DFOOT_DEFAULT_TERM="@0@"'.format(get_option('default-terminfo')), + '-DFOOT_DEFAULT_UTEMPTER_PATH="@0@"'.format(utempter_path)] + (is_debug_build ? ['-D_DEBUG'] : [cc.get_supported_arguments('-fno-asynchronous-unwind-tables')]) + @@ -222,6 +243,7 @@ executable( 'slave.c', 'slave.h', 'spawn.c', 'spawn.h', 'tokenize.c', 'tokenize.h', + 'unicode-mode.c', 'unicode-mode.h', 'url-mode.c', 'url-mode.h', 'user-notification.c', 'user-notification.h', 'wayland.c', 'wayland.h', 'shm-formats.h', @@ -243,7 +265,7 @@ executable( install: true) install_data( - 'foot.desktop', 'foot-server.desktop', 'footclient.desktop', + 'org.codeberg.dnkl.foot.desktop', 'org.codeberg.dnkl.foot-server.desktop', 'org.codeberg.dnkl.footclient.desktop', install_dir: join_paths(get_option('datadir'), 'applications')) systemd = dependency('systemd', required: false) @@ -320,6 +342,7 @@ summary( 'Themes': get_option('themes'), 'IME': get_option('ime'), 'Grapheme clustering': utf8proc.found(), + 'Utempter path': utempter_path, 'Build terminfo': tic.found(), 'Terminfo install location': terminfo_install_location, 'Default TERM': get_option('default-terminfo'), diff --git a/meson_options.txt b/meson_options.txt index 0c660a75..c38a8ca8 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -21,3 +21,6 @@ option('custom-terminfo-install-location', type: 'string', value: '', option('systemd-units-dir', type: 'string', value: '', description: 'Where to install the systemd service files (absolute path). Default: ${systemduserunitdir}') + +option('default-utempter-path', type: 'string', value: '', + description: 'Default path to utempter helper binary. Default: auto-detect') diff --git a/misc.c b/misc.c index 7e33e65a..a81aa9e4 100644 --- a/misc.c +++ b/misc.c @@ -13,15 +13,32 @@ isword(char32_t wc, bool spaces_only, const char32_t *delimiters) return isc32graph(wc); } +void +timespec_add(const struct timespec *a, const struct timespec *b, + struct timespec *res) +{ + const long one_sec_in_ns = 1000000000; + + res->tv_sec = a->tv_sec + b->tv_sec; + res->tv_nsec = a->tv_nsec + b->tv_nsec; + /* tv_nsec may be negative */ + if (res->tv_nsec >= one_sec_in_ns) { + res->tv_sec++; + res->tv_nsec -= one_sec_in_ns; + } +} + void timespec_sub(const struct timespec *a, const struct timespec *b, struct timespec *res) { + const long one_sec_in_ns = 1000000000; + res->tv_sec = a->tv_sec - b->tv_sec; res->tv_nsec = a->tv_nsec - b->tv_nsec; /* tv_nsec may be negative */ if (res->tv_nsec < 0) { res->tv_sec--; - res->tv_nsec += 1000 * 1000 * 1000; + res->tv_nsec += one_sec_in_ns; } } diff --git a/misc.h b/misc.h index ba8b2ce7..648bb65f 100644 --- a/misc.h +++ b/misc.h @@ -6,4 +6,5 @@ bool isword(char32_t wc, bool spaces_only, const char32_t *delimiters); +void timespec_add(const struct timespec *a, const struct timespec *b, struct timespec *res); void timespec_sub(const struct timespec *a, const struct timespec *b, struct timespec *res); diff --git a/foot-server.desktop b/org.codeberg.dnkl.foot-server.desktop similarity index 100% rename from foot-server.desktop rename to org.codeberg.dnkl.foot-server.desktop diff --git a/foot.desktop b/org.codeberg.dnkl.foot.desktop similarity index 100% rename from foot.desktop rename to org.codeberg.dnkl.foot.desktop diff --git a/org.codeberg.dnkl.foot.metainfo.xml b/org.codeberg.dnkl.foot.metainfo.xml new file mode 100644 index 00000000..22512ce8 --- /dev/null +++ b/org.codeberg.dnkl.foot.metainfo.xml @@ -0,0 +1,45 @@ + + + org.codeberg.dnkl.foot + CC0-1.0 + MIT + dnkl + foot + The fast, lightweight and minimalistic Wayland terminal emulator. + +
    +
  • Fast
  • +
  • Lightweight, in dependencies, on-disk and in-memory
  • +
  • Wayland native
  • +
  • DE agnostic
  • +
  • Server/daemon mode
  • +
  • User configurable font fallback
  • +
  • On-the-fly font resize
  • +
  • On-the-fly DPI font size adjustment
  • +
  • Scrollback search
  • +
  • Keyboard driven URL detection
  • +
  • Color emoji support
  • +
  • IME (via text-input-v3)
  • +
  • Multi-seat
  • +
  • True Color (24bpp)
  • +
  • Synchronized Updates support
  • +
  • Sixel image support
  • +
+
+ + + Foot with sixel graphics + https://codeberg.org/dnkl/foot/media/branch/master/doc/sixel-wow.png + + + + + + + + + org.codeberg.dnkl.foot.desktop + https://codeberg.org/dnkl/foot + https://codeberg.org/dnkl/foot/issues + +
diff --git a/footclient.desktop b/org.codeberg.dnkl.footclient.desktop similarity index 100% rename from footclient.desktop rename to org.codeberg.dnkl.footclient.desktop diff --git a/pgo/pgo.c b/pgo/pgo.c index 3073c27c..7f6f758b 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -178,15 +178,16 @@ static bool kbd_initialized = false; struct key_binding_set * key_binding_for( - struct key_binding_manager *mgr, const struct terminal *term, + struct key_binding_manager *mgr, const struct config *conf, const struct seat *seat) { return &kbd; } void -key_binding_new_for_term( - struct key_binding_manager *mgr, const struct terminal *term) +key_binding_new_for_conf( + struct key_binding_manager *mgr, const struct wayland *wayl, + const struct config *conf) { if (!kbd_initialized) { kbd_initialized = true; @@ -201,7 +202,7 @@ key_binding_new_for_term( } void -key_binding_unref_term(struct key_binding_manager *mgr, const struct terminal *term) +key_binding_unref(struct key_binding_manager *mgr, const struct config *conf) { } diff --git a/render.c b/render.c index 3b281a89..b285b120 100644 --- a/render.c +++ b/render.c @@ -372,7 +372,10 @@ draw_underline(const struct terminal *term, pixman_image_t *pix, const struct fcft_font *font, const pixman_color_t *color, int x, int y, int cols) { - const int thickness = font->underline.thickness; + const int thickness = term->conf->underline_thickness.px >= 0 + ? term_pt_or_px_as_pixels( + term, &term->conf->underline_thickness) + : font->underline.thickness; /* Make sure the line isn't positioned below the cell */ const int y_ofs = min(underline_offset(term, font), @@ -699,7 +702,7 @@ render_cell(struct terminal *term, pixman_image_t *pix, mtx_unlock(&term->render.workers.lock); } - if (has_cursor && term->cursor_style == CURSOR_BLOCK && term->kbd_focus) + if (unlikely(has_cursor && term->cursor_style == CURSOR_BLOCK && term->kbd_focus)) draw_cursor(term, cell, font, pix, &fg, &bg, x, y, cell_cols); if (cell->wc == 0 || cell->wc >= CELL_SPACER || cell->wc == U'\t' || @@ -1011,6 +1014,13 @@ grid_render_scroll(struct terminal *term, struct buffer *buf, wl_surface_damage_buffer( term->window->surface, term->margins.left, dst_y, term->width - term->margins.left - term->margins.right, height); + + /* + * TODO: remove this if re-enabling scroll damage when re-applying + * last frame’s damage (see reapply_old_damage() + */ + pixman_region32_union_rect( + &buf->dirty, &buf->dirty, 0, dst_y, buf->width, height); } static void @@ -1076,6 +1086,13 @@ grid_render_scroll_reverse(struct terminal *term, struct buffer *buf, wl_surface_damage_buffer( term->window->surface, term->margins.left, dst_y, term->width - term->margins.left - term->margins.right, height); + + /* + * TODO: remove this if re-enabling scroll damage when re-applying + * last frame’s damage (see reapply_old_damage() + */ + pixman_region32_union_rect( + &buf->dirty, &buf->dirty, 0, dst_y, buf->width, height); } static void @@ -1466,10 +1483,21 @@ static void render_overlay(struct terminal *term) { struct wl_surf_subsurf *overlay = &term->window->overlay; + bool unicode_mode_active = false; + + /* Check if unicode mode is active on at least one seat focusing + * this terminal instance */ + tll_foreach(term->wl->seats, it) { + if (it->item.unicode_mode.active) { + unicode_mode_active = true; + break; + } + } const enum overlay_style style = term->is_searching ? OVERLAY_SEARCH : term->flash.active ? OVERLAY_FLASH : + unicode_mode_active ? OVERLAY_UNICODE_MODE : OVERLAY_NONE; if (likely(style == OVERLAY_NONE)) { @@ -1488,9 +1516,21 @@ render_overlay(struct terminal *term) pixman_image_set_clip_region32(buf->pix[0], NULL); - pixman_color_t color = style == OVERLAY_SEARCH - ? (pixman_color_t){0, 0, 0, 0x7fff} - : (pixman_color_t){.red=0x7fff, .green=0x7fff, .blue=0, .alpha=0x7fff}; + pixman_color_t color; + + switch (style) { + case OVERLAY_NONE: + break; + + case OVERLAY_SEARCH: + case OVERLAY_UNICODE_MODE: + color = (pixman_color_t){0, 0, 0, 0x7fff}; + break; + + case OVERLAY_FLASH: + color = (pixman_color_t){.red=0x7fff, .green=0x7fff, .blue=0, .alpha=0x7fff}; + break; + } /* Bounding rectangle of damaged areas - for wl_surface_damage_buffer() */ pixman_box32_t damage_bounds; @@ -1517,17 +1557,18 @@ render_overlay(struct terminal *term) * region that needs to be *cleared* in this frame. * * Finally, the union of the two “diff” regions above, gives - * us the total region affecte by a change, in either way. We + * us the total region affected by a change, in either way. We * use this as the bounding box for the * wl_surface_damage_buffer() call. */ pixman_region32_t *see_through = &term->render.last_overlay_clip; pixman_region32_t old_see_through; + const bool buffer_reuse = + buf == term->render.last_overlay_buf && + style == term->render.last_overlay_style && + buf->age == 0; - if (!(buf == term->render.last_overlay_buf && - style == term->render.last_overlay_style && - buf->age == 0)) - { + if (!buffer_reuse) { /* Can’t re-use last frame’s damage - set to full window, * to ensure *everything* is updated */ pixman_region32_init_rect( @@ -1540,8 +1581,8 @@ render_overlay(struct terminal *term) pixman_region32_clear(see_through); + /* Build region consisting of all current search matches */ struct search_match_iterator iter = search_matches_new_iter(term); - for (struct range match = search_matches_next(&iter); match.start.row >= 0; match = search_matches_next(&iter)) @@ -1569,20 +1610,28 @@ render_overlay(struct terminal *term) } } - /* Current see-through, minus old see-through - aka cells that - * need to be cleared */ + /* Areas that need to be cleared: cells that were dimmed in + * the last frame but is now see-through */ pixman_region32_t new_see_through; pixman_region32_init(&new_see_through); - pixman_region32_subtract(&new_see_through, see_through, &old_see_through); + + if (buffer_reuse) + pixman_region32_subtract(&new_see_through, see_through, &old_see_through); + else { + /* Buffer content is unknown - explicitly clear *all* + * current see-through areas */ + pixman_region32_copy(&new_see_through, see_through); + } pixman_image_set_clip_region32(buf->pix[0], &new_see_through); - /* Old see-through, minus new see-through - aka cells that - * needs to be dimmed */ + /* Areas that need to be dimmed: cells that were cleared in + * the last frame but is not anymore */ pixman_region32_t new_dimmed; pixman_region32_init(&new_dimmed); pixman_region32_subtract(&new_dimmed, &old_see_through, see_through); pixman_region32_fini(&old_see_through); + /* Total affected area */ pixman_region32_t damage; pixman_region32_init(&damage); pixman_region32_union(&damage, &new_see_through, &new_dimmed); @@ -1605,7 +1654,7 @@ render_overlay(struct terminal *term) else if (buf == term->render.last_overlay_buf && style == term->render.last_overlay_style) { - xassert(style == OVERLAY_FLASH); + xassert(style == OVERLAY_FLASH || style == OVERLAY_UNICODE_MODE); shm_did_not_use_buf(buf); return; } else { @@ -1726,10 +1775,12 @@ get_csd_data(const struct terminal *term, enum csd_surface surf_idx) const int button_close_width = term->width >= 1 * button_width ? button_width : 0; - const int button_maximize_width = term->width >= 2 * button_width + const int button_maximize_width = + term->width >= 2 * button_width && term->window->wm_capabilities.maximize ? button_width : 0; - const int button_minimize_width = term->width >= 3 * button_width + const int button_minimize_width = + term->width >= 3 * button_width && term->window->wm_capabilities.minimize ? button_width : 0; switch (surf_idx) { @@ -2510,22 +2561,27 @@ reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old return; } - /* - * TODO: remove this frame’s damage from the region we copy from - * the old frame. - * - * - this frame’s dirty region is only valid *after* we’ve applied - * its scroll damage. - * - last frame’s dirty region is only valid *before* we’ve - * applied this frame’s scroll damage. - * - * Can we transform one of the regions? It’s not trivial, since - * scroll damage isn’t just about counting lines; there may be - * multiple damage records, each with different scrolling regions. - */ pixman_region32_t dirty; pixman_region32_init(&dirty); + /* + * Figure out current frame’s damage region + * + * If current frame doesn’t have any scroll damage, we can simply + * subtract this frame’s damage from the last frame’s damage. That + * way, we don’t have to copy areas from the old frame that’ll + * just get overwritten by current frame. + * + * Note that this is row based. A “half damaged” row is not + * excluded. I.e. the entire row will be copied from the old frame + * to the new, and then when actually rendering the new frame, the + * updated cells will overwrite parts of the copied row. + * + * Since we’re scanning the entire viewport anyway, we also track + * whether *all* cells are to be updated. In this case, just force + * a full re-rendering, and don’t copy anything from the old + * frame. + */ bool full_repaint_needed = true; for (int r = 0; r < term->rows; r++) { @@ -2555,35 +2611,31 @@ reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old return; } - for (size_t i = 0; i < old->scroll_damage_count; i++) { - const struct damage *dmg = &old->scroll_damage[i]; - - switch (dmg->type) { - case DAMAGE_SCROLL: - if (term->grid->view == term->grid->offset) - grid_render_scroll(term, new, dmg); - break; - - case DAMAGE_SCROLL_REVERSE: - if (term->grid->view == term->grid->offset) - grid_render_scroll_reverse(term, new, dmg); - break; - - case DAMAGE_SCROLL_IN_VIEW: - grid_render_scroll(term, new, dmg); - break; - - case DAMAGE_SCROLL_REVERSE_IN_VIEW: - grid_render_scroll_reverse(term, new, dmg); - break; - } - } + /* + * TODO: re-apply last frame’s scroll damage + * + * We used to do this, but it turned out to be buggy. If we decide + * to re-add it, this is where to do it. Note that we’d also have + * to remove the updates to buf->dirty from grid_render_scroll() + * and grid_render_scroll_reverse(). + */ if (tll_length(term->grid->scroll_damage) == 0) { + /* + * We can only subtract current frame’s damage from the old + * frame’s if we don’t have any scroll damage. + * + * If we do have scroll damage, the damage region we + * calculated above is not (yet) valid - we need to apply the + * current frame’s scroll damage *first*. This is done later, + * when rendering the frame. + */ pixman_region32_subtract(&dirty, &old->dirty, &dirty); pixman_image_set_clip_region32(new->pix[0], &dirty); - } else + } else { + /* Copy *all* of last frame’s damaged areas */ pixman_image_set_clip_region32(new->pix[0], &old->dirty); + } pixman_image_composite32( PIXMAN_OP_SRC, old->pix[0], NULL, new->pix[0], @@ -2674,38 +2726,29 @@ grid_render(struct terminal *term) shm_addref(buf); buf->age = 0; - free(term->render.last_buf->scroll_damage); - buf->scroll_damage_count = tll_length(term->grid->scroll_damage); - buf->scroll_damage = xmalloc( - buf->scroll_damage_count * sizeof(buf->scroll_damage[0])); - { - size_t i = 0; - tll_foreach(term->grid->scroll_damage, it) { - buf->scroll_damage[i++] = it->item; - - switch (it->item.type) { - case DAMAGE_SCROLL: - if (term->grid->view == term->grid->offset) - grid_render_scroll(term, buf, &it->item); - break; - - case DAMAGE_SCROLL_REVERSE: - if (term->grid->view == term->grid->offset) - grid_render_scroll_reverse(term, buf, &it->item); - break; - - case DAMAGE_SCROLL_IN_VIEW: + tll_foreach(term->grid->scroll_damage, it) { + switch (it->item.type) { + case DAMAGE_SCROLL: + if (term->grid->view == term->grid->offset) grid_render_scroll(term, buf, &it->item); - break; + break; - case DAMAGE_SCROLL_REVERSE_IN_VIEW: + case DAMAGE_SCROLL_REVERSE: + if (term->grid->view == term->grid->offset) grid_render_scroll_reverse(term, buf, &it->item); - break; - } + break; - tll_remove(term->grid->scroll_damage, it); + case DAMAGE_SCROLL_IN_VIEW: + grid_render_scroll(term, buf, &it->item); + break; + + case DAMAGE_SCROLL_REVERSE_IN_VIEW: + grid_render_scroll_reverse(term, buf, &it->item); + break; } + + tll_remove(term->grid->scroll_damage, it); } /* @@ -2885,15 +2928,21 @@ grid_render(struct terminal *term) struct timespec double_buffering_time; timespec_sub(&stop_double_buffering, &start_double_buffering, &double_buffering_time); + struct timespec total_render_time; + timespec_add(&render_time, &double_buffering_time, &total_render_time); + switch (term->conf->tweak.render_timer) { case RENDER_TIMER_LOG: case RENDER_TIMER_BOTH: - LOG_INFO("frame rendered in %lds %ldns " - "(%lds %ldns double buffering)", - (long)render_time.tv_sec, - render_time.tv_nsec, - (long)double_buffering_time.tv_sec, - double_buffering_time.tv_nsec); + LOG_INFO( + "frame rendered in %lds %9ldns " + "(%lds %9ldns rendering, %lds %9ldns double buffering)", + (long)total_render_time.tv_sec, + total_render_time.tv_nsec, + (long)render_time.tv_sec, + render_time.tv_nsec, + (long)double_buffering_time.tv_sec, + double_buffering_time.tv_nsec); break; case RENDER_TIMER_OSD: @@ -2904,7 +2953,7 @@ grid_render(struct terminal *term) switch (term->conf->tweak.render_timer) { case RENDER_TIMER_OSD: case RENDER_TIMER_BOTH: - render_render_timer(term, render_time); + render_render_timer(term, total_render_time); break; case RENDER_TIMER_LOG: @@ -3046,10 +3095,20 @@ render_search_box(struct terminal *term) #define WINDOW_X(x) (margin + x) #define WINDOW_Y(y) (term->height - margin - height + y) - /* Background - yellow on empty/match, red on mismatch */ - pixman_color_t color = color_hex_to_pixman( - term->search.match_len == text_len - ? term->colors.table[3] : term->colors.table[1]); + const bool is_match = term->search.match_len == text_len; + const bool custom_colors = is_match + ? term->conf->colors.use_custom.search_box_match + : term->conf->colors.use_custom.search_box_no_match; + + /* Background - yellow on empty/match, red on mismatch (default) */ + const pixman_color_t color = color_hex_to_pixman( + is_match + ? (custom_colors + ? term->conf->colors.search_box.match.bg + : term->colors.table[3]) + : (custom_colors + ? term->conf->colors.search_box.no_match.bg + : term->colors.table[1])); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &color, @@ -3065,7 +3124,12 @@ render_search_box(struct terminal *term) const int x_ofs = term->font_x_ofs; int x = x_left; int y = margin; - pixman_color_t fg = color_hex_to_pixman(term->colors.table[0]); + pixman_color_t fg = color_hex_to_pixman( + custom_colors + ? (is_match + ? term->conf->colors.search_box.match.fg + : term->conf->colors.search_box.no_match.fg) + : term->colors.table[0]); /* Move offset we start rendering at, to ensure the cursor is visible */ for (size_t i = 0, cell_idx = 0; i <= term->search.cursor; cell_idx += widths[i], i++) { diff --git a/scripts/generate-builtin-terminfo.py b/scripts/generate-builtin-terminfo.py index c8d3be4b..906e2be0 100755 --- a/scripts/generate-builtin-terminfo.py +++ b/scripts/generate-builtin-terminfo.py @@ -166,6 +166,7 @@ def main(): entry.add_capability(IntCapability('Co', 256)) entry.add_capability(StringCapability('TN', target_entry_name)) + entry.add_capability(StringCapability('name', target_entry_name)) entry.add_capability(IntCapability('RGB', 8)) # 8 bits per channel terminfo_parts = [] diff --git a/search.c b/search.c index d8f1abb0..45220a71 100644 --- a/search.c +++ b/search.c @@ -18,6 +18,7 @@ #include "render.h" #include "selection.h" #include "shm.h" +#include "unicode-mode.h" #include "util.h" #include "xmalloc.h" @@ -371,6 +372,9 @@ find_next(struct terminal *term, enum search_direction direction, i += additional_chars; match_len += additional_chars; match_end_col++; + + while (match_row->cells[match_end_col].wc > CELL_SPACER) + match_end_col++; } if (match_len != term->search.len) { @@ -1000,6 +1004,10 @@ execute_binding(struct seat *seat, struct terminal *term, *update_search_result = *redraw = true; return true; + case BIND_ACTION_SEARCH_UNICODE_INPUT: + unicode_mode_activate(seat); + return true; + case BIND_ACTION_SEARCH_COUNT: BUG("Invalid action type"); return true; diff --git a/selection.c b/selection.c index 2c626656..329b2ae3 100644 --- a/selection.c +++ b/selection.c @@ -9,6 +9,8 @@ #include #include +#include + #define LOG_MODULE "selection" #define LOG_ENABLE_DBG 0 #include "log.h" @@ -64,41 +66,85 @@ selection_get_end(const struct terminal *term) bool selection_on_rows(const struct terminal *term, int row_start, int row_end) { + xassert(term->selection.coords.end.row >= 0); + LOG_DBG("on rows: %d-%d, range: %d-%d (offset=%d)", term->selection.coords.start.row, term->selection.coords.end.row, row_start, row_end, term->grid->offset); - if (term->selection.coords.end.row < 0) - return false; - - xassert(term->selection.coords.start.row != -1); - row_start += term->grid->offset; row_end += term->grid->offset; + xassert(row_end >= row_start); const struct coord *start = &term->selection.coords.start; const struct coord *end = &term->selection.coords.end; - if ((row_start <= start->row && row_end >= start->row) || - (row_start <= end->row && row_end >= end->row)) + const struct grid *grid = term->grid; + const int sb_start = grid->offset + term->rows; + + /* Use scrollback relative coords when checking for overlap */ + const int rel_row_start = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, row_start); + const int rel_row_end = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, row_start); + int rel_sel_start = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, start->row); + int rel_sel_end = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, end->row); + + if (rel_sel_start > rel_sel_end) { + int tmp = rel_sel_start; + rel_sel_start = rel_sel_end; + rel_sel_end = tmp; + } + + if ((rel_row_start <= rel_sel_start && rel_row_end >= rel_sel_start) || + (rel_row_start <= rel_sel_end && rel_row_end >= rel_sel_end)) { /* The range crosses one of the selection boundaries */ return true; } - /* For the last check we must ensure start <= end */ - if (start->row > end->row) { - const struct coord *tmp = start; - start = end; - end = tmp; - } - - if (row_start >= start->row && row_end <= end->row) + if (rel_row_start >= rel_sel_start && rel_row_end <= rel_sel_end) return true; return false; } +void +selection_scroll_up(struct terminal *term, int rows) +{ + xassert(term->selection.coords.end.row >= 0); + + const int rel_row_start = + grid_row_abs_to_sb(term->grid, term->rows, term->selection.coords.start.row); + const int rel_row_end = + grid_row_abs_to_sb(term->grid, term->rows, term->selection.coords.end.row); + const int actual_start = min(rel_row_start, rel_row_end); + + if (actual_start - rows < 0) { + /* Part of the selection will be scrolled out, cancel it */ + selection_cancel(term); + } +} + +void +selection_scroll_down(struct terminal *term, int rows) +{ + xassert(term->selection.coords.end.row >= 0); + + const int rel_row_start = + grid_row_abs_to_sb(term->grid, term->rows, term->selection.coords.start.row); + const int rel_row_end = + grid_row_abs_to_sb(term->grid, term->rows, term->selection.coords.end.row); + const int actual_end = max(rel_row_start, rel_row_end); + + if (actual_end + rows <= term->grid->num_rows) { + /* Part of the selection will be scrolled out, cancel it */ + selection_cancel(term); + } +} + void selection_view_up(struct terminal *term, int new_view) { @@ -137,14 +183,14 @@ foreach_selected_normal( const struct coord *start = &_start; const struct coord *end = &_end; - const int scrollback_start = term->grid->offset + term->rows; const int grid_rows = term->grid->num_rows; + /* Start/end rows, relative to the scrollback start */ /* Start/end rows, relative to the scrollback start */ const int rel_start_row = - (start->row - scrollback_start + grid_rows) & (grid_rows - 1); + grid_row_abs_to_sb(term->grid, term->rows, start->row); const int rel_end_row = - (end->row - scrollback_start + grid_rows) & (grid_rows - 1); + grid_row_abs_to_sb(term->grid, term->rows, end->row); int start_row, end_row; int start_col, end_col; @@ -200,14 +246,13 @@ foreach_selected_block( const struct coord *start = &_start; const struct coord *end = &_end; - const int scrollback_start = term->grid->offset + term->rows; const int grid_rows = term->grid->num_rows; /* Start/end rows, relative to the scrollback start */ const int rel_start_row = - (start->row - scrollback_start + grid_rows) & (grid_rows - 1); + grid_row_abs_to_sb(term->grid, term->rows, start->row); const int rel_end_row = - (end->row - scrollback_start + grid_rows) & (grid_rows - 1); + grid_row_abs_to_sb(term->grid, term->rows, end->row); struct coord top_left = { .row = (rel_start_row < rel_end_row @@ -564,111 +609,216 @@ selection_start(struct terminal *term, int col, int row, } -/* Context used while (un)marking selected cells, to be able to - * exclude empty cells */ -struct mark_context { - const struct row *last_row; - int empty_count; - uint8_t **keep_selection; +static pixman_region32_t +pixman_region_for_coords_normal(const struct terminal *term, + const struct coord *start, + const struct coord *end) +{ + pixman_region32_t region; + pixman_region32_init(®ion); + + const int rel_start_row = + grid_row_abs_to_sb(term->grid, term->rows, start->row); + const int rel_end_row = + grid_row_abs_to_sb(term->grid, term->rows, end->row); + + if (rel_start_row < rel_end_row) { + /* First partial row (start ->)*/ + pixman_region32_union_rect( + ®ion, ®ion, + start->col, rel_start_row, + term->cols - start->col, 1); + + /* Full rows between start and end */ + if (rel_start_row + 1 < rel_end_row) { + pixman_region32_union_rect( + ®ion, ®ion, + 0, rel_start_row + 1, + term->cols, rel_end_row - rel_start_row - 1); + } + + /* Last partial row (-> end) */ + pixman_region32_union_rect( + ®ion, ®ion, + 0, rel_end_row, + end->col + 1, 1); + + } else if (rel_start_row > rel_end_row) { + /* First partial row (end ->) */ + pixman_region32_union_rect( + ®ion, ®ion, + end->col, rel_end_row, + term->cols - end->col, 1); + + /* Full rows between end and start */ + if (rel_end_row + 1 < rel_start_row) { + pixman_region32_union_rect( + ®ion, ®ion, + 0, rel_end_row + 1, + term->cols, rel_start_row - rel_end_row - 1); + } + + /* Last partial row (-> start) */ + pixman_region32_union_rect( + ®ion, ®ion, + 0, rel_start_row, + start->col + 1, 1); + } else { + const int start_col = min(start->col, end->col); + const int end_col = max(start->col, end->col); + + pixman_region32_union_rect( + ®ion, ®ion, + start_col, rel_start_row, + end_col + 1 - start_col, 1); + } + + return region; +} + +static pixman_region32_t +pixman_region_for_coords_block(const struct terminal *term, + const struct coord *start, const struct coord *end) +{ + pixman_region32_t region; + pixman_region32_init(®ion); + + const int rel_start_row = + grid_row_abs_to_sb(term->grid, term->rows, start->row); + const int rel_end_row = + grid_row_abs_to_sb(term->grid, term->rows, end->row); + + pixman_region32_union_rect( + ®ion, ®ion, + min(start->col, end->col), min(rel_start_row, rel_end_row), + abs(start->col - end->col) + 1, abs(rel_start_row - rel_end_row) + 1); + + return region; +} + +/* Returns a pixman region representing the selection between ‘start’ + * and ‘end’ (given the current selection kind), in *scrollback + * relative coordinates* */ +static pixman_region32_t +pixman_region_for_coords(const struct terminal *term, + const struct coord *start, const struct coord *end) +{ + switch (term->selection.kind) { + default: return pixman_region_for_coords_normal(term, start, end); + case SELECTION_BLOCK: return pixman_region_for_coords_block(term, start, end); + } +} + +enum mark_selection_variant { + MARK_SELECTION_MARK_AND_DIRTY, + MARK_SELECTION_UNMARK_AND_DIRTY, + MARK_SELECTION_MARK_FOR_RENDER, }; -static bool -unmark_selected(struct terminal *term, struct row *row, struct cell *cell, - int row_no, int col, void *data) -{ - if (!cell->attrs.selected) - return true; - - struct mark_context *ctx = data; - const uint8_t *keep_selection = - ctx->keep_selection != NULL ? ctx->keep_selection[row_no] : NULL; - - if (keep_selection != NULL) { - unsigned idx = (unsigned)col / 8; - unsigned ofs = (unsigned)col % 8; - - if (keep_selection[idx] & (1 << ofs)) { - /* We’re updating the selection, and this cell is still - * going to be selected */ - return true; - } - } - - row->dirty = true; - cell->attrs.selected = false; - cell->attrs.clean = false; - return true; -} - -static bool -premark_selected(struct terminal *term, struct row *row, struct cell *cell, - int row_no, int col, void *data) -{ - struct mark_context *ctx = data; - xassert(ctx != NULL); - - if (ctx->last_row != row) { - ctx->last_row = row; - ctx->empty_count = 0; - } - - if (cell->wc == 0 && term->selection.kind != SELECTION_BLOCK) { - ctx->empty_count++; - return true; - } - - uint8_t *keep_selection = ctx->keep_selection[row_no]; - if (keep_selection == NULL) { - keep_selection = xcalloc((term->grid->num_cols + 7) / 8, sizeof(keep_selection[0])); - ctx->keep_selection[row_no] = keep_selection; - } - - /* Tell unmark to leave this be */ - for (int i = 0; i < ctx->empty_count + 1; i++) { - unsigned idx = (unsigned)(col - i) / 8; - unsigned ofs = (unsigned)(col - i) % 8; - keep_selection[idx] |= 1 << ofs; - } - - ctx->empty_count = 0; - return true; -} - -static bool -mark_selected(struct terminal *term, struct row *row, struct cell *cell, - int row_no, int col, void *data) -{ - struct mark_context *ctx = data; - xassert(ctx != NULL); - - if (ctx->last_row != row) { - ctx->last_row = row; - ctx->empty_count = 0; - } - - if (cell->wc == 0 && term->selection.kind != SELECTION_BLOCK) { - ctx->empty_count++; - return true; - } - - for (int i = 0; i < ctx->empty_count + 1; i++) { - struct cell *c = &row->cells[col - i]; - if (!c->attrs.selected) { - row->dirty = true; - c->attrs.selected = true; - c->attrs.clean = false; - } - } - - ctx->empty_count = 0; - return true; -} - static void -reset_modify_context(struct mark_context *ctx) +mark_selected_region(struct terminal *term, pixman_box32_t *boxes, + size_t count, enum mark_selection_variant mark_variant) { - ctx->last_row = NULL; - ctx->empty_count = 0; + const bool selected = + mark_variant == MARK_SELECTION_MARK_AND_DIRTY || + mark_variant == MARK_SELECTION_MARK_FOR_RENDER; + const bool dirty_cells = + mark_variant == MARK_SELECTION_MARK_AND_DIRTY || + mark_variant == MARK_SELECTION_UNMARK_AND_DIRTY; + const bool highlight_empty = + mark_variant != MARK_SELECTION_MARK_FOR_RENDER || + term->selection.kind == SELECTION_BLOCK; + + for (size_t i = 0; i < count; i++) { + const pixman_box32_t *box = &boxes[i]; + + LOG_DBG("%s selection in region: %dx%d - %dx%d", + selected ? "marking" : "unmarking", + box->x1, box->y1, + box->x2, box->y2); + + int abs_row_start = grid_row_sb_to_abs( + term->grid, term->rows, box->y1); + + for (int r = abs_row_start, rel_r = box->y1; + rel_r < box->y2; + r = (r + 1) & (term->grid->num_rows - 1), rel_r++) + { + struct row *row = term->grid->rows[r]; + xassert(row != NULL); + + if (dirty_cells) + row->dirty = true; + + for (int c = box->x1, empty_count = 0; c < box->x2; c++) { + struct cell *cell = &row->cells[c]; + + if (cell->wc == 0 && !highlight_empty) { + /* + * We used to highlight empty cells *if* they were + * followed by non-empty cell(s), since this + * corresponds to what gets extracted when the + * selection is copied (that is, empty cells + * “between” non-empty cells are converted to + * spaces). + * + * However, they way we handle selection updates + * (diffing the “old” selection area against the + * “new” one, using pixman regions), means we + * can’t correctly update the state of empty + * cells. The result is “random” empty cells being + * rendered as selected when they shouldn’t. + * + * “Fix” by *never* highlighting selected empty + * cells (they still get converted to spaces when + * copied, if followed by non-empty cells). + */ + empty_count++; + + /* + * When the selection is *modified*, empty cells + * are treated just like non-empty cells; they are + * marked as selected, and dirtied. + * + * This is due to how the algorithm for updating + * the selection works; it uses regions to + * calculate the difference between the “old” and + * the “new” selection. This makes it impossible + * to tell if an empty cell is a *trailing* empty + * cell (that should not be highlighted), or an + * empty cells between non-empty cells (that + * *should* be highlighted). + * + * Then, when a frame is rendered, we loop the + * *visibible* cells that belong to the + * selection. At this point, we *can* tell if an + * empty cell is trailing or not. + * + * So, what we need to do is check if a + * ‘selected’, and empty cell has been marked as + * selected, temporarily unmark (forcing it dirty, + * to ensure it gets re-rendered). If it is *not* + * a trailing empty cell, it will get re-tagged as + * selected in the for-loop below. + */ + cell->attrs.clean = false; + cell->attrs.selected = false; + continue; + } + + for (int j = 0; j < empty_count + 1; j++) { + xassert(c - j >= 0); + struct cell *cell = &row->cells[c - j]; + + if (dirty_cells) + cell->attrs.clean = false; + cell->attrs.selected = selected; + } + + empty_count = 0; + } + } + } } static void @@ -678,33 +828,46 @@ selection_modify(struct terminal *term, struct coord start, struct coord end) xassert(start.row != -1 && start.col != -1); xassert(end.row != -1 && end.col != -1); - uint8_t **keep_selection = - xcalloc(term->grid->num_rows, sizeof(keep_selection[0])); - - struct mark_context ctx = {.keep_selection = keep_selection}; - - /* Premark all cells that *will* be selected */ - foreach_selected(term, start, end, &premark_selected, &ctx); - reset_modify_context(&ctx); - + pixman_region32_t previous_selection; if (term->selection.coords.end.row >= 0) { - /* Unmark previous selection, ignoring cells that are part of - * the new selection */ - foreach_selected(term, term->selection.coords.start, term->selection.coords.end, - &unmark_selected, &ctx); - reset_modify_context(&ctx); - } + previous_selection = pixman_region_for_coords( + term, + &term->selection.coords.start, + &term->selection.coords.end); + } else + pixman_region32_init(&previous_selection); + + pixman_region32_t current_selection = pixman_region_for_coords( + term, &start, &end); + + pixman_region32_t no_longer_selected; + pixman_region32_init(&no_longer_selected); + pixman_region32_subtract( + &no_longer_selected, &previous_selection, ¤t_selection); + + pixman_region32_t newly_selected; + pixman_region32_init(&newly_selected); + pixman_region32_subtract( + &newly_selected, ¤t_selection, &previous_selection); + + /* Clear selection in cells no longer selected */ + int n_rects = -1; + pixman_box32_t *boxes = NULL; + + boxes = pixman_region32_rectangles(&no_longer_selected, &n_rects); + mark_selected_region(term, boxes, n_rects, MARK_SELECTION_UNMARK_AND_DIRTY); + + boxes = pixman_region32_rectangles(&newly_selected, &n_rects); + mark_selected_region(term, boxes, n_rects, MARK_SELECTION_MARK_AND_DIRTY); + + pixman_region32_fini(&newly_selected); + pixman_region32_fini(&no_longer_selected); + pixman_region32_fini(¤t_selection); + pixman_region32_fini(&previous_selection); term->selection.coords.start = start; term->selection.coords.end = end; - - /* Mark new selection */ - foreach_selected(term, start, end, &mark_selected, &ctx); render_refresh(term); - - for (size_t i = 0; i < term->grid->num_rows; i++) - free(keep_selection[i]); - free(keep_selection); } static void @@ -945,9 +1108,26 @@ selection_dirty_cells(struct terminal *term) if (term->selection.coords.start.row < 0 || term->selection.coords.end.row < 0) return; - foreach_selected( - term, term->selection.coords.start, term->selection.coords.end, &mark_selected, - &(struct mark_context){0}); + pixman_region32_t selection = pixman_region_for_coords( + term, &term->selection.coords.start, &term->selection.coords.end); + + pixman_region32_t view = pixman_region_for_coords( + term, + &(struct coord){0, term->grid->view}, + &(struct coord){term->cols - 1, term->grid->view + term->rows - 1}); + + pixman_region32_t visible_and_selected; + pixman_region32_init(&visible_and_selected); + pixman_region32_intersect(&visible_and_selected, &selection, &view); + + int n_rects = -1; + pixman_box32_t *boxes = + pixman_region32_rectangles(&visible_and_selected, &n_rects); + mark_selected_region(term, boxes, n_rects, MARK_SELECTION_MARK_FOR_RENDER); + + pixman_region32_fini(&visible_and_selected); + pixman_region32_fini(&view); + pixman_region32_fini(&selection); } static void @@ -957,27 +1137,37 @@ selection_extend_normal(struct terminal *term, int col, int row, const struct coord *start = &term->selection.coords.start; const struct coord *end = &term->selection.coords.end; - if (start->row > end->row || - (start->row == end->row && start->col > end->col)) + const int rel_row = grid_row_abs_to_sb(term->grid, term->rows, row); + int rel_start_row = grid_row_abs_to_sb(term->grid, term->rows, start->row); + int rel_end_row = grid_row_abs_to_sb(term->grid, term->rows, end->row); + + if (rel_start_row > rel_end_row || + (rel_start_row == rel_end_row && start->col > end->col)) { const struct coord *tmp = start; start = end; end = tmp; - } - xassert(start->row < end->row || start->col < end->col); + int tmp_row = rel_start_row; + rel_start_row = rel_end_row; + rel_end_row = tmp_row; + } struct coord new_start, new_end; enum selection_direction direction; - if (row < start->row || (row == start->row && col < start->col)) { + if (rel_row < rel_start_row || + (rel_row == rel_start_row && col < start->col)) + { /* Extend selection to start *before* current start */ new_start = *end; new_end = (struct coord){col, row}; direction = SELECTION_LEFT; } - else if (row > end->row || (row == end->row && col > end->col)) { + else if (rel_row > rel_end_row || + (rel_row == rel_end_row && col > end->col)) + { /* Extend selection to end *after* current end */ new_start = *start; new_end = (struct coord){col, row}; @@ -987,10 +1177,10 @@ selection_extend_normal(struct terminal *term, int col, int row, else { /* Shrink selection from start or end, depending on which one is closest */ - const int linear = row * term->cols + col; + const int linear = rel_row * term->cols + col; - if (abs(linear - (start->row * term->cols + start->col)) < - abs(linear - (end->row * term->cols + end->col))) + if (abs(linear - (rel_start_row * term->cols + start->col)) < + abs(linear - (rel_end_row * term->cols + end->col))) { /* Move start point */ new_start = *end; @@ -1065,33 +1255,41 @@ selection_extend_block(struct terminal *term, int col, int row) const struct coord *start = &term->selection.coords.start; const struct coord *end = &term->selection.coords.end; + const int rel_start_row = + grid_row_abs_to_sb(term->grid, term->rows, start->row); + const int rel_end_row = + grid_row_abs_to_sb(term->grid, term->rows, end->row); + struct coord top_left = { - .row = min(start->row, end->row), + .row = rel_start_row < rel_end_row ? start->row : end->row, .col = min(start->col, end->col), }; struct coord top_right = { - .row = min(start->row, end->row), + .row = top_left.row, .col = max(start->col, end->col), }; struct coord bottom_left = { - .row = max(start->row, end->row), + .row = rel_start_row > rel_end_row ? start->row : end->row, .col = min(start->col, end->col), }; struct coord bottom_right = { - .row = max(start->row, end->row), + .row = bottom_left.row, .col = max(start->col, end->col), }; + const int rel_row = grid_row_abs_to_sb(term->grid, term->rows, row); + const int rel_top_row = grid_row_abs_to_sb(term->grid, term->rows, top_left.row); + const int rel_bottom_row = grid_row_abs_to_sb(term->grid, term->rows, bottom_left.row); struct coord new_start; struct coord new_end; enum selection_direction direction = SELECTION_UNDIR; - if (row <= top_left.row || - abs(row - top_left.row) < abs(row - bottom_left.row)) + if (rel_row <= rel_top_row || + abs(rel_row - rel_top_row) < abs(rel_row - rel_bottom_row)) { /* Move one of the top corners */ @@ -1207,6 +1405,19 @@ selection_finalize(struct seat *seat, struct terminal *term, uint32_t serial) } } +static bool +unmark_selected(struct terminal *term, struct row *row, struct cell *cell, + int row_no, int col, void *data) +{ + if (!cell->attrs.selected) + return true; + + row->dirty = true; + cell->attrs.selected = false; + cell->attrs.clean = false; + return true; +} + void selection_cancel(struct terminal *term) { @@ -1219,7 +1430,7 @@ selection_cancel(struct terminal *term) if (term->selection.coords.start.row >= 0 && term->selection.coords.end.row >= 0) { foreach_selected( term, term->selection.coords.start, term->selection.coords.end, - &unmark_selected, &(struct mark_context){0}); + &unmark_selected, NULL); render_refresh(term); } diff --git a/selection.h b/selection.h index 5776ac48..250140b1 100644 --- a/selection.h +++ b/selection.h @@ -22,6 +22,8 @@ void selection_extend( bool selection_on_rows(const struct terminal *term, int start, int end); +void selection_scroll_up(struct terminal *term, int rows); +void selection_scroll_down(struct terminal *term, int rows); void selection_view_up(struct terminal *term, int new_view); void selection_view_down(struct terminal *term, int new_view); diff --git a/shm.c b/shm.c index 4c342ddf..4394dbe9 100644 --- a/shm.c +++ b/shm.c @@ -151,7 +151,6 @@ buffer_destroy(struct buffer_private *buf) pool_unref(buf->pool); buf->pool = NULL; - free(buf->public.scroll_damage); pixman_region32_fini(&buf->public.dirty); free(buf); } @@ -581,8 +580,6 @@ shm_get_buffer(struct buffer_chain *chain, int width, int height) LOG_DBG("re-using buffer %p from cache", (void *)cached); cached->busy = true; pixman_region32_clear(&cached->public.dirty); - free(cached->public.scroll_damage); - cached->public.scroll_damage = NULL; xassert(cached->public.pix_instances == chain->pix_instances); return &cached->public; } diff --git a/shm.h b/shm.h index bed4285c..440cfa1d 100644 --- a/shm.h +++ b/shm.h @@ -24,8 +24,6 @@ struct buffer { unsigned age; - struct damage *scroll_damage; - size_t scroll_damage_count; pixman_region32_t dirty; }; diff --git a/slave.c b/slave.c index 6473ac7c..4dd80e6f 100644 --- a/slave.c +++ b/slave.c @@ -352,6 +352,7 @@ slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, setenv("TERM", term_env, 1); setenv("COLORTERM", "truecolor", 1); + setenv("PWD", cwd, 1); #if defined(FOOT_TERMINFO_PATH) setenv("TERMINFO", FOOT_TERMINFO_PATH, 1); diff --git a/spawn.c b/spawn.c index 7c6641da..90b892f3 100644 --- a/spawn.c +++ b/spawn.c @@ -54,9 +54,12 @@ spawn(struct reaper *reaper, const char *cwd, char *const argv[], goto child_err; } - if (cwd != NULL && chdir(cwd) < 0) { - LOG_WARN("failed to change working directory to %s: %s", - cwd, strerror(errno)); + if (cwd != NULL) { + setenv("PWD", cwd, 1); + if (chdir(cwd) < 0) { + LOG_WARN("failed to change working directory to %s: %s", + cwd, strerror(errno)); + } } if (xdg_activation_token != NULL) { diff --git a/terminal.c b/terminal.c index c449aa0e..df17201b 100644 --- a/terminal.c +++ b/terminal.c @@ -203,6 +203,30 @@ fdm_ptmx_out(struct fdm *fdm, int fd, int events, void *data) return true; } +static bool +add_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) +{ + if (ptmx < 0) + return true; + if (conf->utempter_path == NULL) + return true; + + char *const argv[] = {conf->utempter_path, "add", getenv("WAYLAND_DISPLAY"), NULL}; + return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL); +} + +static bool +del_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) +{ + if (ptmx < 0) + return true; + if (conf->utempter_path == NULL) + return true; + + char *const argv[] = {conf->utempter_path, "del", getenv("WAYLAND_DISPLAY"), NULL}; + return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL); +} + #if PTMX_TIMING static struct timespec last = {0}; #endif @@ -326,6 +350,7 @@ fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) } if (hup) { + del_utmp_record(term->conf, term->reaper, term->ptmx); fdm_del(fdm, fd); term->ptmx = -1; } @@ -1089,6 +1114,11 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, goto close_fds; } + /* Need to register *very* early (before the first “goto err”), to + * ensure term_destroy() doesn’t unref a key-binding we haven’t + * yet ref:d */ + key_binding_new_for_conf(wayl->key_binding_manager, wayl, conf); + int ptmx_flags; if ((ptmx_flags = fcntl(ptmx, F_GETFL)) < 0 || fcntl(ptmx, F_SETFL, ptmx_flags | O_NONBLOCK) < 0) @@ -1246,6 +1276,8 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, } term->font_line_height = conf->line_height; + add_utmp_record(conf, reaper, ptmx); + /* Start the slave/client */ if ((term->slave = slave_spawn( term->ptmx, argc, term->cwd, argv, envp, &conf->env_vars, @@ -1266,8 +1298,6 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, memcpy(term->colors.table, term->conf->colors.table, sizeof(term->colors.table)); - key_binding_new_for_term(wayl->key_binding_manager, term); - /* Initialize the Wayland window backend */ if ((term->window = wayl_win_init(term, token)) == NULL) goto err; @@ -1511,6 +1541,8 @@ term_shutdown(struct terminal *term) fdm_del(term->fdm, term->blink.fd); fdm_del(term->fdm, term->flash.fd); + del_utmp_record(term->conf, term->reaper, term->ptmx); + if (term->window != NULL && term->window->is_configured) fdm_del(term->fdm, term->ptmx); else @@ -1583,7 +1615,7 @@ term_destroy(struct terminal *term) if (term == NULL) return 0; - key_binding_unref_term(term->wl->key_binding_manager, term); + key_binding_unref(term->wl->key_binding_manager, term->conf); tll_foreach(term->wl->terms, it) { if (it->item == term) { @@ -1592,6 +1624,8 @@ term_destroy(struct terminal *term) } } + del_utmp_record(term->conf, term->reaper, term->ptmx); + fdm_del(term->fdm, term->selection.auto_scroll.fd); fdm_del(term->fdm, term->render.app_sync_updates.timer_fd); fdm_del(term->fdm, term->render.title.timer_fd); @@ -1811,7 +1845,7 @@ static inline void erase_line(struct terminal *term, struct row *row) { erase_cell_range(term, row, 0, term->cols - 1); - row->linebreak = true; + row->linebreak = false; row->prompt_marker = false; } @@ -2537,11 +2571,11 @@ term_scroll_partial(struct terminal *term, struct scroll_region region, int rows * scrolled in (i.e. re-used lines). */ if (selection_on_top_region(term, region) || - selection_on_bottom_region(term, region) || - selection_on_rows(term, region.end - rows, region.end - 1)) + selection_on_bottom_region(term, region)) { selection_cancel(term); - } + } else + selection_scroll_up(term, rows); } sixel_scroll_up(term, rows); @@ -2611,11 +2645,11 @@ term_scroll_reverse_partial(struct terminal *term, * scrolled in (i.e. re-used lines). */ if (selection_on_top_region(term, region) || - selection_on_bottom_region(term, region) || - selection_on_rows(term, region.start, region.start + rows - 1)) + selection_on_bottom_region(term, region)) { selection_cancel(term); - } + } else + selection_scroll_down(term, rows); } sixel_scroll_down(term, rows); @@ -2885,7 +2919,7 @@ term_mouse_grabbed(const struct terminal *term, const struct seat *seat) get_current_modifiers(seat, &mods, NULL, 0); const struct key_binding_set *bindings = - key_binding_for(term->wl->key_binding_manager, term, seat); + key_binding_for(term->wl->key_binding_manager, term->conf, seat); const xkb_mod_mask_t override_modmask = bindings->selection_overrides; bool override_mods_pressed = (mods & override_modmask) == override_modmask; @@ -3298,6 +3332,7 @@ term_print(struct terminal *term, char32_t wc, int width) /* *Must* get current cell *after* linewrap+insert */ struct row *row = grid->cur_row; row->dirty = true; + row->linebreak = true; struct cell *cell = &row->cells[col]; cell->wc = term->vt.last_printed = wc; @@ -3357,6 +3392,7 @@ ascii_printer_fast(struct terminal *term, char32_t wc) struct row *row = grid->cur_row; row->dirty = true; + row->linebreak = true; struct cell *cell = &row->cells[col]; cell->wc = term->vt.last_printed = wc; diff --git a/terminal.h b/terminal.h index bf6e74fe..0dde6330 100644 --- a/terminal.h +++ b/terminal.h @@ -289,9 +289,10 @@ enum term_surface { }; enum overlay_style { - OVERLAY_NONE = 0, - OVERLAY_SEARCH = 1, - OVERLAY_FLASH = 2, + OVERLAY_NONE, + OVERLAY_SEARCH, + OVERLAY_FLASH, + OVERLAY_UNICODE_MODE, }; typedef tll(struct ptmx_buffer) ptmx_buffer_list_t; diff --git a/tests/test-config.c b/tests/test-config.c index 874f5b05..ae7969dc 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -401,6 +401,52 @@ test_color(struct context *ctx, bool (*parse_fun)(struct context *ctx), } } +static void +test_two_colors(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, bool alpha_allowed, + uint32_t *ptr1, uint32_t *ptr2) +{ + ctx->key = key; + + const struct { + const char *option_string; + uint32_t color1; + uint32_t color2; + bool invalid; + } input[] = { + {"000000 000000", 0, 0}, + + /* No alpha */ + {"999999 888888", 0x999999, 0x888888}, + {"ffffff aaaaaa", 0xffffff, 0xaaaaaa}, + + /* Both colors have alpha component */ + {"ffffffff 00000000", 0xffffffff, 0x00000000, !alpha_allowed}, + {"aabbccdd, ee112233", 0xaabbccdd, 0xee112233, !alpha_allowed}, + + /* Only one color has alpha component */ + {"ffffffff 112233", 0xffffffff, 0x112233, !alpha_allowed}, + {"ffffff ff112233", 0x00ffffff, 0xff112233, !alpha_allowed}, + + {"unittest-invalid-color", 0, 0, true}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + } + } +} + static void test_section_main(void) { @@ -412,6 +458,7 @@ test_section_main(void) test_string(&ctx, &parse_section_main, "shell", &conf.shell); test_string(&ctx, &parse_section_main, "term", &conf.term); test_string(&ctx, &parse_section_main, "app-id", &conf.app_id); + test_string(&ctx, &parse_section_main, "utempter", &conf.utempter_path); test_c32string(&ctx, &parse_section_main, "word-delimiters", &conf.word_delimiters); @@ -424,6 +471,7 @@ test_section_main(void) test_pt_or_px(&ctx, &parse_section_main, "letter-spacing", &conf.letter_spacing); test_pt_or_px(&ctx, &parse_section_main, "horizontal-letter-offset", &conf.horizontal_letter_offset); test_pt_or_px(&ctx, &parse_section_main, "vertical-letter-offset", &conf.vertical_letter_offset); + test_pt_or_px(&ctx, &parse_section_main, "underline-thickness", &conf.underline_thickness); test_uint16(&ctx, &parse_section_main, "resize-delay-ms", &conf.resize_delay_ms); test_uint16(&ctx, &parse_section_main, "workers", &conf.render_worker_count); @@ -616,6 +664,18 @@ test_section_colors(void) test_color(&ctx, &parse_section_colors, "selection-foreground", false, &conf.colors.selection_fg); test_color(&ctx, &parse_section_colors, "selection-background", false, &conf.colors.selection_bg); test_color(&ctx, &parse_section_colors, "urls", false, &conf.colors.url); + test_two_colors(&ctx, &parse_section_colors, "jump-labels", false, + &conf.colors.jump_label.fg, + &conf.colors.jump_label.bg); + test_two_colors(&ctx, &parse_section_colors, "scrollback-indicator", false, + &conf.colors.scrollback_indicator.fg, + &conf.colors.scrollback_indicator.bg); + test_two_colors(&ctx, &parse_section_colors, "search-box-no-match", false, + &conf.colors.search_box.no_match.fg, + &conf.colors.search_box.no_match.bg); + test_two_colors(&ctx, &parse_section_colors, "search-box-match", false, + &conf.colors.search_box.match.fg, + &conf.colors.search_box.match.bg); for (size_t i = 0; i < 255; i++) { char key_name[4]; @@ -627,8 +687,6 @@ test_section_colors(void) test_invalid_key(&ctx, &parse_section_colors, "256"); /* TODO: alpha (float in range 0-1, converted to uint16_t) */ - /* TODO: jump-labels (two colors) */ - /* TODO: scrollback-indicator (two colors) */ config_free(&conf); } diff --git a/themes/moonfly b/themes/moonfly new file mode 100644 index 00000000..54d9203b --- /dev/null +++ b/themes/moonfly @@ -0,0 +1,29 @@ +# moonfly +# Based on https://github.com/bluz71/vim-moonfly-colors + +[cursor] +color = 080808 9e9e9e + +[colors] +foreground = b2b2b2 +background = 080808 +selection-foreground = 080808 +selection-background = b2ceee + +regular0 = 323437 +regular1 = ff5454 +regular2 = 8cc85f +regular3 = e3c78a +regular4 = 80a0ff +regular5 = d183e8 +regular6 = 79dac8 +regular7 = c6c6c6 + +bright0 = 949494 +bright1 = ff5189 +bright2 = 36c692 +bright3 = c2c292 +bright4 = 74b2ff +bright5 = ae81ff +bright6 = 85dc85 +bright7 = e4e4e4 diff --git a/themes/nightfly b/themes/nightfly new file mode 100644 index 00000000..3815b42e --- /dev/null +++ b/themes/nightfly @@ -0,0 +1,29 @@ +# nightfly +# Based on https://github.com/bluz71/vim-nightfly-guicolors + +[cursor] +color = 080808 9ca1aa + +[colors] +foreground = acb4c2 +background = 011627 +selection-foreground = 080808 +selection-background = b2ceee + +regular0 = 1d3b53 +regular1 = fc514e +regular2 = a1cd5e +regular3 = e3d18a +regular4 = 82aaff +regular5 = c792ea +regular6 = 7fdbca +regular7 = a1aab8 + +bright0 = 7c8f8f +bright1 = ff5874 +bright2 = 21c7a8 +bright3 = ecc48d +bright4 = 82aaff +bright5 = ae81ff +bright6 = ae81ff +bright7 = d6deeb diff --git a/unicode-mode.c b/unicode-mode.c new file mode 100644 index 00000000..a69601ec --- /dev/null +++ b/unicode-mode.c @@ -0,0 +1,106 @@ +#include "unicode-mode.h" + +#define LOG_MODULE "unicode-input" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "render.h" +#include "search.h" + +void +unicode_mode_activate(struct seat *seat) +{ + if (seat->unicode_mode.active) + return; + + seat->unicode_mode.active = true; + seat->unicode_mode.character = u'\0'; + seat->unicode_mode.count = 0; + unicode_mode_updated(seat); +} + +void +unicode_mode_deactivate(struct seat *seat) +{ + if (!seat->unicode_mode.active) + return; + + seat->unicode_mode.active = false; + unicode_mode_updated(seat); +} + +void +unicode_mode_updated(struct seat *seat) +{ + struct terminal *term = seat->kbd_focus; + if (term == NULL) + return; + + if (term->is_searching) + render_refresh_search(term); + else + render_refresh(term); +} + +void +unicode_mode_input(struct seat *seat, struct terminal *term, + xkb_keysym_t sym) +{ + if (sym == XKB_KEY_Return || + sym == XKB_KEY_space || + sym == XKB_KEY_KP_Enter || + sym == XKB_KEY_KP_Space) + { + char utf8[MB_CUR_MAX]; + size_t chars = c32rtomb( + utf8, seat->unicode_mode.character, &(mbstate_t){0}); + + LOG_DBG("Unicode input: 0x%06x -> %.*s", + seat->unicode_mode.character, (int)chars, utf8); + + if (chars != (size_t)-1) { + if (term->is_searching) + search_add_chars(term, utf8, chars); + else + term_to_slave(term, utf8, chars); + } + + unicode_mode_deactivate(seat); + } + + else if (sym == XKB_KEY_Escape || + sym == XKB_KEY_q || + (seat->kbd.ctrl && (sym == XKB_KEY_c || + sym == XKB_KEY_d || + sym == XKB_KEY_g))) + { + unicode_mode_deactivate(seat); + } + + else if (sym == XKB_KEY_BackSpace) { + if (seat->unicode_mode.count > 0) { + seat->unicode_mode.character >>= 4; + seat->unicode_mode.count--; + unicode_mode_updated(seat); + } + } + + else if (seat->unicode_mode.count < 6) { + int digit = -1; + + /* 0-9, a-f, A-F */ + if (sym >= XKB_KEY_0 && sym <= XKB_KEY_9) + digit = sym - XKB_KEY_0; + else if (sym >= XKB_KEY_a && sym <= XKB_KEY_f) + digit = 0xa + (sym - XKB_KEY_a); + else if (sym >= XKB_KEY_A && sym <= XKB_KEY_F) + digit = 0xa + (sym - XKB_KEY_A); + + if (digit >= 0) { + xassert(digit >= 0 && digit <= 0xf); + seat->unicode_mode.character <<= 4; + seat->unicode_mode.character |= digit; + seat->unicode_mode.count++; + unicode_mode_updated(seat); + } + } +} diff --git a/unicode-mode.h b/unicode-mode.h new file mode 100644 index 00000000..e7c75b9b --- /dev/null +++ b/unicode-mode.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include "wayland.h" + +void unicode_mode_activate(struct seat *seat); +void unicode_mode_deactivate(struct seat *seat); +void unicode_mode_updated(struct seat *seat); +void unicode_mode_input(struct seat *seat, struct terminal *term, + xkb_keysym_t sym); diff --git a/url-mode.c b/url-mode.c index 538b60f0..6fa16623 100644 --- a/url-mode.c +++ b/url-mode.c @@ -677,18 +677,23 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) if (count == 0) return; - uint64_t seen_ids[count]; char32_t *combos[count]; generate_key_combos(conf, count, combos); size_t combo_idx = 0; - size_t id_idx = 0; tll_foreach(*urls, it) { bool id_already_seen = false; - for (size_t i = 0; i < id_idx; i++) { - if (it->item.id == seen_ids[i]) { + /* Look for already processed URLs where both the URI and the + * ID matches */ + tll_foreach(*urls, it2) { + if (&it->item == &it2->item) + break; + + if (it->item.id == it2->item.id && + strcmp(it->item.url, it2->item.url) == 0) + { id_already_seen = true; break; } @@ -696,7 +701,6 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) if (id_already_seen) continue; - seen_ids[id_idx++] = it->item.id; /* * Scan previous URLs, and check if *this* URL matches any of @@ -730,7 +734,7 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) char *key = ac32tombs(it->item.key); xassert(key != NULL); - LOG_DBG("URL: %s (%s)", it->item.url, key); + LOG_DBG("URL: %s (key=%s, id=%"PRIu64")", it->item.url, key, it->item.id); free(key); } #endif diff --git a/wayland.c b/wayland.c index 850b657b..cd052532 100644 --- a/wayland.c +++ b/wayland.c @@ -367,7 +367,7 @@ update_terms_on_monitor(struct monitor *mon) static void output_update_ppi(struct monitor *mon) { - if (mon->dim.mm.width == 0 || mon->dim.mm.height == 0) + if (mon->dim.mm.width <= 0 || mon->dim.mm.height <= 0) return; int x_inches = mon->dim.mm.width * 0.03937008; @@ -706,9 +706,50 @@ xdg_toplevel_close(void *data, struct xdg_toplevel *xdg_toplevel) term_shutdown(term); } +#if defined(XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION) +static void +xdg_toplevel_configure_bounds(void *data, + struct xdg_toplevel *xdg_toplevel, + int32_t width, int32_t height) +{ + /* TODO: ensure we don't pick a bigger size */ +} +#endif + +#if defined(XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION) +static void +xdg_toplevel_wm_capabilities(void *data, + struct xdg_toplevel *xdg_toplevel, + struct wl_array *caps) +{ + struct wl_window *win = data; + + win->wm_capabilities.maximize = false; + win->wm_capabilities.minimize = false; + + uint32_t *cap_ptr; + wl_array_for_each(cap_ptr, caps) { + switch (*cap_ptr) { + case XDG_TOPLEVEL_WM_CAPABILITIES_MAXIMIZE: + win->wm_capabilities.maximize = true; + break; + case XDG_TOPLEVEL_WM_CAPABILITIES_MINIMIZE: + win->wm_capabilities.minimize = true; + break; + } + } +} +#endif + static const struct xdg_toplevel_listener xdg_toplevel_listener = { .configure = &xdg_toplevel_configure, /*.close = */&xdg_toplevel_close, /* epoll-shim defines a macro ‘close’... */ +#if defined(XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION) + .configure_bounds = &xdg_toplevel_configure_bounds, +#endif +#if defined(XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION) + .wm_capabilities = xdg_toplevel_wm_capabilities, +#endif }; static void @@ -896,13 +937,22 @@ handle_global(void *data, struct wl_registry *registry, return; /* - * We *require* version 1, but _can_ use version 2. Version 2 + * We *require* version 1, but _can_ use version 5. Version 2 * adds 'tiled' window states. We use that information to - * restore the window size when window is un-tiled. + * restore the window size when window is un-tiled. Version 5 + * adds 'wm_capabilities'. We use that information to draw + * window decorations. */ +#if defined(XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION) + const uint32_t preferred = XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION; +#elif defined(XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION) + const uint32_t preferred = XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION; +#else + const uint32_t preferred = required; +#endif wayl->shell = wl_registry_bind( - wayl->registry, name, &xdg_wm_base_interface, min(version, 2)); + wayl->registry, name, &xdg_wm_base_interface, min(version, preferred)); xdg_wm_base_add_listener(wayl->shell, &xdg_wm_base_listener, wayl); } @@ -1418,6 +1468,9 @@ wayl_win_init(struct terminal *term, const char *token) win->csd.move_timeout_fd = -1; win->resize_timeout_fd = -1; + win->wm_capabilities.maximize = true; + win->wm_capabilities.minimize = true; + win->surface = wl_compositor_create_surface(wayl->compositor); if (win->surface == NULL) { LOG_ERR("failed to create wayland surface"); diff --git a/wayland.h b/wayland.h index 729a225b..e86c6a3d 100644 --- a/wayland.h +++ b/wayland.h @@ -206,6 +206,12 @@ struct seat { uint32_t serial; } ime; #endif + + struct { + bool active; + int count; + char32_t character; + } unicode_mode; }; enum csd_surface { @@ -333,6 +339,11 @@ struct wl_window { uint32_t serial; } csd; + struct { + bool maximize:1; + bool minimize:1; + } wm_capabilities; + struct wl_surf_subsurf search; struct wl_surf_subsurf scrollback_indicator; struct wl_surf_subsurf render_timer;