diff --git a/README.md b/README.md index 0fafc587..923bb34d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,127 @@ **foot** is a fast Wayland terminal emulator. +## Index + +1. [Features](#features) +1. [Non-features](#non-features) +1. [Fonts](#fonts) +1. [Shortcuts](#shortcuts) + 1. [Keyboard](#keyboard) + 1. [Mouse](#mouse) +1. [Requirements](#requirements) + 1. [Running](#running) + 1. [Building](#building) +1. [Installing](#installing) + 1. [Arch Linux](#arch-linux) + 1. [Other](#other) + + +## Features + +* Fast (**TODO** insert benchmark results here) +* Wayland native +* DE agnostic +* User configurable font fallback +* Scrollback search +* Color emoji support + + +## Non-features + +This is a non-exhaustive list of things some people might consider +being important features (i.e. _"must-haves"_), that are unlikely to +ever be supported by foot. + +* Tabs +* Multiple windows + + +## Fonts + +**foot** supports all fonts that can be loaded by _freetype_, +including **bitmap** fonts and **color emoji** fonts. + +Foot uses _fontconfig_ to locate and configure the font(s) to +use. Since fontconfig's fallback mechanism is imperfect, especially +for monospace fonts (it doesn't prefer monospace fonts even though the +requested font is one), foot allows you, the user, to configure the +fallback fonts to use. + +This also means you can configure _each_ fallback font individually; +you want _that_ fallback font to use _this_ size, and you want that +_other_ fallback font to be _italic_? No problem! + +If a glyph cannot be found in _any_ of the user configured fallback +fonts, _then_ fontconfig's list is used. + + +## Shortcuts + +At the moment, all shortcuts are hard coded and cannot be changed. It +is **not** possible to define new key bindings. + + +### Keyboard + +* shift+page up/page down + + Scroll up/down in history + +* ctrl+shift+c + + Copy selected text to the _clipboard_ + +* ctrl+shift+v + + Paste from _clipboard_ + +* ctrl+shift+r + + Start a scrollback search + +While doing a scrollback search, the following shortcuts are +available: + +* ctrl+r + + Search for next match + +* escape + + Cancel the search + +* ctrl+g + + Cancel the search (same as escape) + +* return + + Finish the search and put the current match to the primary selection + +### Mouse + +* left - **single-click** + + Drag to select; when released, the selected text is copied to the + _primary_ selection. Note that this feature is normally **disabled** + whenever the client has enabled _mouse tracking_, but can be forced + by holding shift. + +* left - **double-click** + + Selects the _word_ (separated by spaces, period, comma, parenthesis + etc) under the pointer. Hold ctrl to select everything + under the pointer up to, and until, the next space characters. + +* left - **triple-click** + + Selects the entire row + +* middle + + Paste from _primary_ selection + ## Requirements @@ -25,46 +146,24 @@ In addition to the dev variant of the packages above, you need: * scdoc -## Fonts +## Installing -**foot** supports all fonts that can be loaded by freetype, including -**bitmap** fonts and **color emoji** fonts. +### Arch Linux -Foot uses its own font fallback mechanism, rather than relying on -fontconfig's fallback. This is because fontconfig is quite bad at -selecting fallback fonts suitable for a terminal (i.e. monospaced -fonts). +Use [makepkg](https://wiki.archlinux.org/index.php/Makepkg) to build +the bundled `PKGBUILD` (just run `makepkg` in the source root +directory).. -Instead, foot allows you to specify a font fallback list, where _each_ -font can be configured independently (for example, you can configure -the size for each font individually). - -If a glyph cannot be found in _any_ of the user configured fallback -fonts, _then_ fontconfig's list is used. +Note that it will do a profiling-guided build, and that this requires +a running wayland session since it needs to run an intermediate build +of foot. -## Shortcuts +### Other -At the moment, all shortcuts are hard coded and cannot be changed. It -is **not** possible to define new key bindings. +Foot uses _meson_. If you are unfamiliar with it, the official +[tutorial](https://mesonbuild.com/Tutorial.html) might be a good +starting point. - -### Keyboard - -* `shift+page up/down` - scroll up/down in history -* `ctrl+shift+c` - copy selected text to the _clipboard_ -* `ctrl+shift+v` - paste from _clipboard_ - - -### Mouse - -* `left` - single-click: drag to select; when released, the selected - text is copied to the _primary_ selection. Note that this feature is - normally disabled whenever the client has enabled mouse tracking, - but can be forced by holding `shift`. -* `left` - double-click: selects the _word_ (separated by spaces, - period, comma, parenthesis etc) under the pointer. Hold `ctrl` to - select everything under the pointer up to, and until, the next space - characters. -* `left` - triple-click: selects the entire row -* `middle` - paste from _primary_ selection +I also recommend taking a look at that bundled Arch `PKGBUILD` file, +to see how it builds foot. diff --git a/grid.h b/grid.h index a421a5c0..96ef4181 100644 --- a/grid.h +++ b/grid.h @@ -7,12 +7,24 @@ void grid_swap_row(struct grid *grid, int row_a, int row_b, bool initialize); struct row *grid_row_alloc(int cols, bool initialize); void grid_row_free(struct row *row); +static inline int +grid_row_absolute(const struct grid *grid, int row_no) +{ + return (grid->offset + row_no) & (grid->num_rows - 1); +} + +static inline int +grid_row_absolute_in_view(const struct grid *grid, int row_no) +{ + return (grid->view + row_no) & (grid->num_rows - 1); +} + static inline struct row * _grid_row_maybe_alloc(struct grid *grid, int row_no, bool alloc_if_null) { assert(grid->offset >= 0); - int real_row = (grid->offset + row_no) & (grid->num_rows - 1); + int real_row = grid_row_absolute(grid, row_no); struct row *row = grid->rows[real_row]; if (row == NULL && alloc_if_null) { @@ -41,7 +53,7 @@ grid_row_in_view(struct grid *grid, int row_no) { assert(grid->view >= 0); - int real_row = (grid->view + row_no) & (grid->num_rows - 1); + int real_row = grid_row_absolute_in_view(grid, row_no); struct row *row = grid->rows[real_row]; assert(row != NULL); diff --git a/input.c b/input.c index dc48b44d..76bc5812 100644 --- a/input.c +++ b/input.c @@ -18,11 +18,12 @@ #define LOG_MODULE "input" #define LOG_ENABLE_DBG 0 #include "log.h" -#include "terminal.h" -#include "render.h" -#include "keymap.h" #include "commands.h" +#include "keymap.h" +#include "render.h" +#include "search.h" #include "selection.h" +#include "terminal.h" #include "vt.h" static void @@ -178,6 +179,12 @@ keyboard_key(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, xkb_mod_mask_t significant = ctrl | alt | shift | meta; xkb_mod_mask_t effective_mods = mods & ~consumed & significant; + if (term->is_searching) { + start_repeater(term, key - 8); + search_input(term, key, sym, effective_mods); + return; + } + #if 0 for (size_t i = 0; i < 32; i++) { if (mods & (1 << i)) { @@ -218,6 +225,12 @@ keyboard_key(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, else if (sym == XKB_KEY_V) { selection_from_clipboard(term, serial); + term_reset_view(term); + found_map = true; + } + + else if (sym == XKB_KEY_R) { + search_begin(term); found_map = true; } } @@ -243,11 +256,7 @@ keyboard_key(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, vt_to_slave(term, info->seq, strlen(info->seq)); found_map = true; - if (term->grid->view != term->grid->offset) { - term->grid->view = term->grid->offset; - term_damage_all(term); - } - + term_reset_view(term); selection_cancel(term); break; } @@ -308,13 +317,9 @@ keyboard_key(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, vt_to_slave(term, "\x1b", 1); vt_to_slave(term, buf, count); - - if (term->grid->view != term->grid->offset) { - term->grid->view = term->grid->offset; - term_damage_all(term); - } } + term_reset_view(term); selection_cancel(term); } } @@ -436,6 +441,8 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, struct terminal *term = data; + search_cancel(term); + switch (state) { case WL_POINTER_BUTTON_STATE_PRESSED: { /* Time since last click */ diff --git a/main.c b/main.c index 2a3b3daa..afa213cb 100644 --- a/main.c +++ b/main.c @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -22,6 +23,7 @@ #include #include +#include #define LOG_MODULE "main" #define LOG_ENABLE_DBG 0 @@ -129,8 +131,10 @@ output_scale(void *data, struct wl_output *wl_output, int32_t factor) { struct monitor *mon = data; mon->scale = factor; + + int old_scale = mon->term->scale >= 1 ? mon->term->scale : 1; render_reload_cursor_theme(mon->term); - render_resize(mon->term, mon->term->width, mon->term->height); + render_resize(mon->term, mon->term->width / old_scale, mon->term->height / old_scale); } static const struct wl_output_listener output_listener = { @@ -197,6 +201,11 @@ handle_global(void *data, struct wl_registry *registry, term->wl.registry, name, &wl_compositor_interface, 4); } + else if (strcmp(interface, wl_subcompositor_interface.name) == 0) { + term->wl.sub_compositor = wl_registry_bind( + term->wl.registry, name, &wl_subcompositor_interface, 1); + } + else if (strcmp(interface, wl_shm_interface.name) == 0) { term->wl.shm = wl_registry_bind( term->wl.registry, name, &wl_shm_interface, 1); @@ -210,6 +219,10 @@ handle_global(void *data, struct wl_registry *registry, xdg_wm_base_add_listener(term->wl.shell, &xdg_wm_base_listener, term); } + else if (strcmp(interface, zxdg_decoration_manager_v1_interface.name) == 0) + term->wl.xdg_decoration_manager = wl_registry_bind( + term->wl.registry, name, &zxdg_decoration_manager_v1_interface, 1); + else if (strcmp(interface, wl_seat_interface.name) == 0) { term->wl.seat = wl_registry_bind( term->wl.registry, name, &wl_seat_interface, 5); @@ -261,8 +274,9 @@ surface_enter(void *data, struct wl_surface *wl_surface, tll_push_back(term->wl.on_outputs, &it->item); /* Resize, since scale-to-use may have changed */ + int scale = term->scale >= 1 ? term->scale : 1; render_reload_cursor_theme(term); - render_resize(term, term->width, term->height); + render_resize(term, term->width / scale, term->height / scale); return; } } @@ -283,8 +297,9 @@ surface_leave(void *data, struct wl_surface *wl_surface, tll_remove(term->wl.on_outputs, it); /* Resize, since scale-to-use may have changed */ + int scale = term->scale >= 1 ? term->scale : 1; render_reload_cursor_theme(term); - render_resize(term, term->width, term->height); + render_resize(term, term->width / scale, term->height / scale); return; } @@ -335,6 +350,30 @@ static const struct xdg_surface_listener xdg_surface_listener = { .configure = &xdg_surface_configure, }; +static void +xdg_toplevel_decoration_configure(void *data, + struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, + uint32_t mode) +{ + switch (mode) { + case ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE: + LOG_ERR("unimplemented: client-side decorations"); + break; + + case ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE: + LOG_DBG("using server-side decorations"); + break; + + default: + LOG_ERR("unimplemented: unknown XDG toplevel decoration mode: %u", mode); + break; + } +} + +static const struct zxdg_toplevel_decoration_v1_listener xdg_toplevel_decoration_listener = { + .configure = &xdg_toplevel_decoration_configure, +}; + static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) { @@ -630,7 +669,7 @@ main(int argc, char *const *argv) term.cell_width = (int)ceil(term.fextents.max_x_advance); term.cell_height = (int)ceil(term.fextents.height); - LOG_DBG("cell width=%d, height=%d", term.cell_width, term.cell_height); + LOG_INFO("cell width=%d, height=%d", term.cell_width, term.cell_height); term.wl.display = wl_display_connect(NULL); if (term.wl.display == NULL) { @@ -718,6 +757,7 @@ main(int argc, char *const *argv) goto out; } + /* Main window */ term.wl.surface = wl_compositor_create_surface(term.wl.compositor); if (term.wl.surface == NULL) { LOG_ERR("failed to create wayland surface"); @@ -735,6 +775,20 @@ main(int argc, char *const *argv) xdg_toplevel_set_app_id(term.wl.xdg_toplevel, "foot"); term_set_window_title(&term, "foot"); + /* Request server-side decorations */ + term.wl.xdg_toplevel_decoration = zxdg_decoration_manager_v1_get_toplevel_decoration( + term.wl.xdg_decoration_manager, term.wl.xdg_toplevel); + zxdg_toplevel_decoration_v1_set_mode( + term.wl.xdg_toplevel_decoration, ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE); + zxdg_toplevel_decoration_v1_add_listener( + term.wl.xdg_toplevel_decoration, &xdg_toplevel_decoration_listener, &term); + + /* Scrollback search box */ + term.wl.search_surface = wl_compositor_create_surface(term.wl.compositor); + term.wl.search_sub_surface = wl_subcompositor_get_subsurface( + term.wl.sub_compositor, term.wl.search_surface, term.wl.surface); + wl_subsurface_set_desync(term.wl.search_sub_surface); + wl_surface_commit(term.wl.surface); wl_display_roundtrip(term.wl.display); @@ -835,8 +889,9 @@ main(int argc, char *const *argv) } } + bool timeout_is_armed = false; + int timeout_fd = timerfd_create(CLOCK_REALTIME, TFD_NONBLOCK | TFD_CLOEXEC); - int timeout_ms = -1; while (true) { struct pollfd fds[] = { {.fd = wl_display_get_fd(term.wl.display), .events = POLLIN}, @@ -844,10 +899,11 @@ main(int argc, char *const *argv) {.fd = term.kbd.repeat.fd, .events = POLLIN}, {.fd = term.flash.fd, .events = POLLIN}, {.fd = term.blink.fd, .events = POLLIN}, + {.fd = timeout_fd, .events = POLLIN}, }; wl_display_flush(term.wl.display); - int pret = poll(fds, sizeof(fds) / sizeof(fds[0]), timeout_ms); + int pret = poll(fds, sizeof(fds) / sizeof(fds[0]), -1); if (pret == -1) { if (errno == EINTR) @@ -857,13 +913,19 @@ main(int argc, char *const *argv) break; } - if (pret == 0 || (timeout_ms != -1 && !(fds[1].revents & POLLIN))) { - /* Delayed rendering */ - render_refresh(&term); - } + if (fds[5].revents & POLLIN) { + assert(timeout_is_armed); - /* Reset poll timeout to infinity */ - timeout_ms = -1; + uint64_t unused; + ssize_t ret = read(timeout_fd, &unused, sizeof(unused)); + + if (ret < 0 && errno != EAGAIN) + LOG_ERRNO("failed to read timeout timer"); + else if (ret > 0) { + timeout_is_armed = false; + render_refresh(&term); + } + } if (fds[0].revents & POLLIN) { wl_display_dispatch(term.wl.display); @@ -915,7 +977,10 @@ main(int argc, char *const *argv) * ourselves we just received keyboard input, and in * this case *not* delay rendering? */ - timeout_ms = 1; + if (!timeout_is_armed) { + timerfd_settime(timeout_fd, 0, &(struct itimerspec){.it_value = {.tv_nsec = 1000000}}, NULL); + timeout_is_armed = true; + } } } @@ -986,9 +1051,10 @@ main(int argc, char *const *argv) render_refresh(&term); } } - } + close(timeout_fd); + out: mtx_lock(&term.render.workers.lock); assert(tll_length(term.render.workers.queue) == 0); @@ -1014,12 +1080,6 @@ out: if (term.wl.xdg_output_manager != NULL) zxdg_output_manager_v1_destroy(term.wl.xdg_output_manager); - if (term.render.frame_callback != NULL) - wl_callback_destroy(term.render.frame_callback); - if (term.wl.xdg_toplevel != NULL) - xdg_toplevel_destroy(term.wl.xdg_toplevel); - if (term.wl.xdg_surface != NULL) - xdg_surface_destroy(term.wl.xdg_surface); free(term.wl.pointer.theme_name); if (term.wl.pointer.theme != NULL) wl_cursor_theme_destroy(term.wl.pointer.theme); @@ -1049,12 +1109,28 @@ out: zwp_primary_selection_device_manager_v1_destroy(term.wl.primary_selection_device_manager); if (term.wl.seat != NULL) wl_seat_destroy(term.wl.seat); - if (term.wl.surface != NULL) - wl_surface_destroy(term.wl.surface); + if (term.wl.search_sub_surface != NULL) + wl_subsurface_destroy(term.wl.search_sub_surface); + if (term.wl.search_surface != NULL) + wl_surface_destroy(term.wl.search_surface); + if (term.render.frame_callback != NULL) + wl_callback_destroy(term.render.frame_callback); + if (term.wl.xdg_toplevel_decoration != NULL) + zxdg_toplevel_decoration_v1_destroy(term.wl.xdg_toplevel_decoration); + if (term.wl.xdg_decoration_manager != NULL) + zxdg_decoration_manager_v1_destroy(term.wl.xdg_decoration_manager); + if (term.wl.xdg_toplevel != NULL) + xdg_toplevel_destroy(term.wl.xdg_toplevel); + if (term.wl.xdg_surface != NULL) + xdg_surface_destroy(term.wl.xdg_surface); if (term.wl.shell != NULL) xdg_wm_base_destroy(term.wl.shell); + if (term.wl.surface != NULL) + wl_surface_destroy(term.wl.surface); if (term.wl.shm != NULL) wl_shm_destroy(term.wl.shm); + if (term.wl.sub_compositor != NULL) + wl_subcompositor_destroy(term.wl.sub_compositor); if (term.wl.compositor != NULL) wl_compositor_destroy(term.wl.compositor); if (term.wl.registry != NULL) @@ -1086,6 +1162,8 @@ out: for (size_t i = 0; i < sizeof(term.fonts) / sizeof(term.fonts[0]); i++) font_destroy(&term.fonts[i]); + free(term.search.buf); + if (term.flash.fd != -1) close(term.flash.fd); if (term.blink.fd != -1) diff --git a/meson.build b/meson.build index edf354b2..632da62d 100644 --- a/meson.build +++ b/meson.build @@ -60,6 +60,7 @@ wl_proto_headers = [] wl_proto_src = [] foreach prot : [ wayland_protocols_datadir + '/stable/xdg-shell/xdg-shell.xml', + wayland_protocols_datadir + '/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml', wayland_protocols_datadir + '/unstable/xdg-output/xdg-output-unstable-v1.xml', wayland_protocols_datadir + '/unstable/primary-selection/primary-selection-unstable-v1.xml', ] @@ -90,6 +91,7 @@ executable( 'main.c', 'osc.c', 'osc.h', 'render.c', 'render.h', + 'search.c', 'search.h', 'selection.c', 'selection.h', 'shm.c', 'shm.h', 'slave.c', 'slave.h', diff --git a/render.c b/render.c index 84c86232..e5ec9c90 100644 --- a/render.c +++ b/render.c @@ -72,14 +72,44 @@ pixman_color_dim(pixman_color_t *color) color->blue /= 2; } +static inline void +pixman_color_dim_for_search(pixman_color_t *color) +{ + color->red /= 3; + color->green /= 3; + color->blue /= 3; +} + +static inline int +font_baseline(const struct terminal *term) +{ + assert(term->fextents.ascent >= 0); + assert(term->fextents.descent >= 0); + + int diff = term->fextents.height - (term->fextents.ascent + term->fextents.descent); + assert(diff >= 0); + +#if 0 + LOG_INFO("height=%d, ascent=%d, descent=%d, diff=%d", + term->fextents.height, + term->fextents.ascent, term->fextents.descent, + diff); +#endif + + return term->fextents.height - diff / 2 - term->fextents.descent; +} + static void draw_bar(const struct terminal *term, pixman_image_t *pix, + const struct font *font, const pixman_color_t *color, int x, int y) { - /* TODO: investigate if this is the best way */ + int baseline = y + font_baseline(term) - term->fextents.ascent; pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, color, - 1, &(pixman_rectangle16_t){x, y, 1, term->cell_height}); + 1, &(pixman_rectangle16_t){ + x, baseline, + font->underline.thickness, term->fextents.ascent + term->fextents.descent}); } static void @@ -87,7 +117,7 @@ draw_underline(const struct terminal *term, pixman_image_t *pix, const struct font *font, const pixman_color_t *color, int x, int y, int cols) { - int baseline = y + term->fextents.height - term->fextents.descent; + int baseline = y + font_baseline(term); int width = font->underline.thickness; int y_under = baseline - font->underline.position - width / 2; @@ -101,7 +131,7 @@ draw_strikeout(const struct terminal *term, pixman_image_t *pix, const struct font *font, const pixman_color_t *color, int x, int y, int cols) { - int baseline = y + term->fextents.height - term->fextents.descent; + int baseline = y + font_baseline(term); int width = font->strikeout.thickness; int y_strike = baseline - font->strikeout.position - width / 2; @@ -204,6 +234,11 @@ render_cell(struct terminal *term, pixman_image_t *pix, bg = color_hex_to_pixman(term->cursor_color.cursor); } + if (term->is_searching && !is_selected) { + pixman_color_dim_for_search(&fg); + pixman_color_dim_for_search(&bg); + } + struct font *font = attrs_to_font(term, &cell->attrs); const struct glyph *glyph = font_glyph_for_wc(font, cell->wc); @@ -216,12 +251,16 @@ render_cell(struct terminal *term, pixman_image_t *pix, /* Non-block cursors */ if (has_cursor && !block_cursor) { - pixman_color_t cursor_color = term->cursor_color.text >> 31 - ? color_hex_to_pixman(term->cursor_color.cursor) - : fg; + pixman_color_t cursor_color; + if (term->cursor_color.text >> 31) { + cursor_color = color_hex_to_pixman(term->cursor_color.cursor); + if (term->is_searching) + pixman_color_dim(&cursor_color); + } else + cursor_color = fg; if (term->cursor_style == CURSOR_BAR) - draw_bar(term, pix, &cursor_color, x, y); + draw_bar(term, pix, font, &cursor_color, x, y); else if (term->cursor_style == CURSOR_UNDERLINE) draw_underline( term, pix, attrs_to_font(term, &cell->attrs), &cursor_color, @@ -435,9 +474,10 @@ grid_render(struct terminal *term) term_damage_view(term); /* If we resized the window, or is flashing, or just stopped flashing */ - if (term->render.last_buf != buf || term->flash.active || term->render.was_flashing) { - LOG_DBG("new buffer"); - + if (term->render.last_buf != buf || + term->flash.active || term->render.was_flashing || + term->is_searching != term->render.was_searching) + { /* Fill area outside the cell grid with the default background color */ int rmargin = term->x_margin + term->cols * term->cell_width; int bmargin = term->y_margin + term->rows * term->cell_height; @@ -445,8 +485,10 @@ grid_render(struct terminal *term) int bmargin_height = term->height - bmargin; uint32_t _bg = !term->reverse ? term->colors.bg : term->colors.fg; - pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, term->colors.alpha); + if (term->is_searching) + pixman_color_dim(&bg); + pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, &bg, 4, (pixman_rectangle16_t[]){ @@ -469,6 +511,7 @@ grid_render(struct terminal *term) term->render.last_buf = buf; term->render.was_flashing = term->flash.active; + term->render.was_searching = term->is_searching; } tll_foreach(term->grid->scroll_damage, it) { @@ -626,6 +669,7 @@ grid_render(struct terminal *term) if (term->flash.active) { /* Note: alpha is pre-computed in each color component */ + /* TODO: dim while searching */ pixman_image_fill_rectangles( PIXMAN_OP_OVER, pix, &(pixman_color_t){.red=0x7fff, .green=0x7fff, .blue=0, .alpha=0x7fff}, @@ -669,6 +713,65 @@ frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_da grid_render(term); } +void +render_search_box(struct terminal *term) +{ + assert(term->wl.search_sub_surface != NULL); + + /* TODO: at least sway allows the subsurface to extend outside the + * main window. Do we want that? */ + const int scale = term->scale >= 1 ? term->scale : 1; + const int margin = scale * 3; + const int width = 2 * margin + max(20, term->search.len) * term->cell_width; + const int height = 2 * margin + 1 * term->cell_height; + + struct buffer *buf = shm_get_buffer(term->wl.shm, width, height, 1); + + /* Background - yellow on empty/match, red on mismatch */ + pixman_color_t color = color_hex_to_pixman( + term->search.match_len == term->search.len ? 0xffff00 : 0xff0000); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix, &color, + 1, &(pixman_rectangle16_t){0, 0, width, height}); + + struct font *font = &term->fonts[0]; + int x = margin; + int y = margin; + pixman_color_t fg = color_hex_to_pixman(0x000000); + + /* Text (what the user entered - *not* match(es)) */ + for (size_t i = 0; i < term->search.len; i++) { + if (i == term->search.cursor) + draw_bar(term, buf->pix, font, &fg, x, y); + + const struct glyph *glyph = font_glyph_for_wc(font, term->search.buf[i]); + if (glyph == NULL) + continue; + + pixman_image_t *src = pixman_image_create_solid_fill(&fg); + pixman_image_composite32( + PIXMAN_OP_OVER, src, glyph->pix, buf->pix, 0, 0, 0, 0, + x + glyph->x, y + term->fextents.ascent - glyph->y, + glyph->width, glyph->height); + pixman_image_unref(src); + + x += term->cell_width; + } + + if (term->search.cursor >= term->search.len) + draw_bar(term, buf->pix, font, &fg, x, y); + + wl_subsurface_set_position( + term->wl.search_sub_surface, + term->width - width - margin, term->height - height - margin); + + wl_surface_damage_buffer(term->wl.search_surface, 0, 0, width, height); + wl_surface_attach(term->wl.search_surface, buf->wl_buf, 0, 0); + wl_surface_set_buffer_scale(term->wl.search_surface, scale); + wl_surface_commit(term->wl.search_surface); +} + static void reflow(struct row **new_grid, int new_cols, int new_rows, struct row *const *old_grid, int old_cols, int old_rows) diff --git a/render.h b/render.h index a3620665..c2fa489e 100644 --- a/render.h +++ b/render.h @@ -10,6 +10,7 @@ void render_resize(struct terminal *term, int width, int height); void render_set_title(struct terminal *term, const char *title); void render_refresh(struct terminal *term); +void render_search_box(struct terminal *term); bool render_reload_cursor_theme(struct terminal *term); void render_update_cursor_surface(struct terminal *term); diff --git a/search.c b/search.c new file mode 100644 index 00000000..28a24367 --- /dev/null +++ b/search.c @@ -0,0 +1,496 @@ +#include "search.h" + +#include +#include + +#include +#include + +#define LOG_MODULE "search" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "grid.h" +#include "render.h" +#include "selection.h" +#include "shm.h" + +#define max(x, y) ((x) > (y) ? (x) : (y)) + +static void +search_cancel_keep_selection(struct terminal *term) +{ + wl_surface_attach(term->wl.search_surface, NULL, 0, 0); + wl_surface_commit(term->wl.search_surface); + + free(term->search.buf); + term->search.buf = NULL; + term->search.len = 0; + term->search.sz = 0; + term->search.cursor = 0; + term->search.match = (struct coord){-1, -1}; + term->search.match_len = 0; + term->is_searching = false; + + render_refresh(term); +} + +void +search_begin(struct terminal *term) +{ + LOG_DBG("search: begin"); + + search_cancel_keep_selection(term); + selection_cancel(term); + + term->search.original_view = term->grid->view; + term->search.view_followed_offset = term->grid->view == term->grid->offset; + term->is_searching = true; + + render_search_box(term); + render_refresh(term); +} + +void +search_cancel(struct terminal *term) +{ + if (!term->is_searching) + return; + + search_cancel_keep_selection(term); + selection_cancel(term); +} + +static void +search_update(struct terminal *term) +{ + bool backward = term->search.direction == SEARCH_BACKWARD; + term->search.direction = SEARCH_BACKWARD; + + if (term->search.len == 0) { + term->search.match = (struct coord){-1, -1}; + term->search.match_len = 0; + selection_cancel(term); + return; + } + + int start_row = term->search.match.row; + int start_col = term->search.match.col; + size_t len __attribute__((unused)) = term->search.match_len; + + assert((len == 0 && start_row == -1 && start_col == -1) || + (len > 0 && start_row >= 0 && start_col >= 0)); + + if (len == 0) { + if (backward) { + start_row = grid_row_absolute_in_view(term->grid, term->rows - 1); + start_col = term->cols - 1; + } else { + start_row = grid_row_absolute_in_view(term->grid, 0); + start_col = 0; + } + } + + LOG_DBG("search: update: %s: starting at row=%d col=%d (offset = %d, view = %d)", + backward ? "backward" : "forward", start_row, start_col, + term->grid->offset, term->grid->view); + +#define ROW_DEC(_r) ((_r) = ((_r) - 1 + term->grid->num_rows) % term->grid->num_rows) +#define ROW_INC(_r) ((_r) = ((_r) + 1) % term->grid->num_rows) + + /* Scan backward from current end-of-output */ + /* TODO: don't search "scrollback" in alt screen? */ + for (size_t r = 0; + r < term->grid->num_rows; + backward ? ROW_DEC(start_row) : ROW_INC(start_row), r++) + { + const struct row *row = term->grid->rows[start_row]; + if (row == NULL) + continue; + + for (; + backward ? start_col >= 0 : start_col < term->cols; + backward ? start_col-- : start_col++) + { + if (wcsncasecmp(&row->cells[start_col].wc, term->search.buf, 1) != 0) + continue; + + /* + * Got a match on the first letter. Now we'll see if the + * rest of the search buffer matches. + */ + + LOG_DBG("search: initial match at row=%d, col=%d", start_row, start_col); + + int end_row = start_row; + int end_col = start_col; + size_t match_len = 0; + + for (size_t i = 0; i < term->search.len; i++, match_len++) { + if (wcsncasecmp(&row->cells[end_col].wc, &term->search.buf[i], 1) != 0) + break; + + if (++end_col >= term->cols) { + if (end_row + 1 > grid_row_absolute(term->grid, term->grid->offset + term->rows - 1)) { + /* Don't continue past end of the world */ + break; + } + + end_row++; + end_col = 0; + row = term->grid->rows[end_row]; + } + } + + if (match_len != term->search.len) { + /* Didn't match (completely) */ + continue; + } + + /* + * We matched the entire buffer. Move view to ensure the + * match is visible, create a selection and return. + */ + + int old_view = term->grid->view; + int new_view = start_row; + + /* Prevent scrolling in uninitialized rows */ + bool all_initialized = false; + do { + all_initialized = true; + + for (int i = 0; i < term->rows; i++) { + int row_no = (new_view + i) % term->grid->num_rows; + if (term->grid->rows[row_no] == NULL) { + all_initialized = false; + new_view--; + break; + } + } + } while (!all_initialized); + + /* Don't scroll past scrollback history */ + int end = (term->grid->offset + term->rows - 1) % term->grid->num_rows; + if (end >= term->grid->offset) { + /* Not wrapped */ + if (new_view >= term->grid->offset && new_view <= end) + new_view = term->grid->offset; + } else { + if (new_view >= term->grid->offset || new_view <= end) + new_view = term->grid->offset; + } + + /* Update view */ + term->grid->view = new_view; + if (new_view != old_view) + term_damage_view(term); + + /* Selection endpoint is inclusive */ + if (--end_col < 0) { + end_col = term->cols - 1; + start_row--; + } + + /* Begin a new selection if the start coords changed */ + if (start_row != term->search.match.row || + start_col != term->search.match.col) + { + int selection_row = start_row - term->grid->view; + while (selection_row < 0) + selection_row += term->grid->num_rows; + + assert(selection_row >= 0 && + selection_row < term->grid->num_rows); + selection_start(term, start_col, selection_row); + } + + /* Update selection endpoint */ + { + int selection_row = end_row - term->grid->view; + while (selection_row < 0) + selection_row += term->grid->num_rows; + + assert(selection_row >= 0 && + selection_row < term->grid->num_rows); + selection_update(term, end_col, selection_row); + } + + /* Update match state */ + term->search.match.row = start_row; + term->search.match.col = start_col; + term->search.match_len = match_len; + + return; + } + + start_col = backward ? term->cols - 1 : 0; + } + + /* No match */ + LOG_DBG("no match"); + term->search.match = (struct coord){-1, -1}; + term->search.match_len = 0; + selection_cancel(term); +#undef ROW_DEC +} + +static size_t +distance_next_word(const struct terminal *term) +{ + size_t cursor = term->search.cursor; + + /* First eat non-whitespace. This is the word we're skipping past */ + while (cursor < term->search.len) { + if (iswspace(term->search.buf[cursor++])) + break; + } + + assert(cursor == term->search.len || iswspace(term->search.buf[cursor - 1])); + + /* Now skip past whitespace, so that we end up at the beginning of + * the next word */ + while (cursor < term->search.len) { + if (!iswspace(term->search.buf[cursor++])) + break; + } + + LOG_INFO("cursor = %zu, iswspace() = %d", cursor, iswspace(term->search.buf[cursor - 1])); + assert(cursor == term->search.len || !iswspace(term->search.buf[cursor - 1])); + + if (cursor < term->search.len && !iswspace(term->search.buf[cursor])) + cursor--; + + return cursor - term->search.cursor; +} + +static size_t +distance_prev_word(const struct terminal *term) +{ + int cursor = term->search.cursor; + + /* First, eat whitespace prefix */ + while (cursor > 0) { + if (!iswspace(term->search.buf[--cursor])) + break; + } + + assert(cursor == 0 || !iswspace(term->search.buf[cursor])); + + /* Now eat non-whitespace. This is the word we're skipping past */ + while (cursor > 0) { + if (iswspace(term->search.buf[--cursor])) + break; + } + + assert(cursor == 0 || iswspace(term->search.buf[cursor])); + if (cursor > 0 && iswspace(term->search.buf[cursor])) + cursor++; + + return term->search.cursor - cursor; +} + +void +search_input(struct terminal *term, uint32_t key, xkb_keysym_t sym, xkb_mod_mask_t mods) +{ + LOG_DBG("search: input: sym=%d/0x%x, mods=0x%08x", sym, sym, mods); + + const xkb_mod_mask_t ctrl = 1 << term->kbd.mod_ctrl; + const xkb_mod_mask_t alt = 1 << term->kbd.mod_alt; + //const xkb_mod_mask_t shift = 1 << term->kbd.mod_shift; + //const xkb_mod_mask_t meta = 1 << term->kbd.mod_meta; + + enum xkb_compose_status compose_status = xkb_compose_state_get_status( + term->kbd.xkb_compose_state); + + /* Cancel search */ + if ((mods == 0 && sym == XKB_KEY_Escape) || + (mods == ctrl && sym == XKB_KEY_g)) + { + if (term->search.view_followed_offset) + term->grid->view = term->grid->offset; + else + term->grid->view = term->search.original_view; + term_damage_view(term); + search_cancel(term); + return; + } + + /* "Commit" search - copy selection to primary and cancel search */ + else if (mods == 0 && sym == XKB_KEY_Return) { + selection_finalize(term, term->input_serial); + search_cancel_keep_selection(term); + return; + } + + else if (mods == ctrl && sym == XKB_KEY_r) { + if (term->search.match_len > 0) { + int new_col = term->search.match.col - 1; + int new_row = term->search.match.row; + + if (new_col < 0) { + new_col = term->cols - 1; + new_row--; + } + + if (new_row >= 0) { + term->search.match.col = new_col; + term->search.match.row = new_row; + } + } + } + + else if (mods == ctrl && sym == XKB_KEY_s) { + if (term->search.match_len > 0) { + int new_col = term->search.match.col + 1; + int new_row = term->search.match.row; + + if (new_col >= term->cols) { + new_col = 0; + new_row++; + } + + if (new_row < term->grid->num_rows) { + term->search.match.col = new_col; + term->search.match.row = new_row; + term->search.direction = SEARCH_FORWARD; + } + } + } + + else if (mods == 0 && sym == XKB_KEY_Left) { + if (term->search.cursor > 0) + term->search.cursor--; + } + + else if ((mods == ctrl && sym == XKB_KEY_Left) || + (mods == alt && sym == XKB_KEY_b)) + { + size_t diff = distance_prev_word(term); + term->search.cursor -= diff; + assert(term->search.cursor >= 0); + assert(term->search.cursor <= term->search.len); + } + + else if (mods == 0 && sym == XKB_KEY_Right) { + if (term->search.cursor < term->search.len) + term->search.cursor++; + } + + else if ((mods == ctrl && sym == XKB_KEY_Right) || + (mods == alt && sym == XKB_KEY_f)) + { + size_t diff = distance_next_word(term); + term->search.cursor += diff; + assert(term->search.cursor >= 0); + assert(term->search.cursor <= term->search.len); + } + + else if ((mods == 0 && sym == XKB_KEY_Home) || + (mods == ctrl && sym == XKB_KEY_a)) + term->search.cursor = 0; + + else if ((mods == 0 && sym == XKB_KEY_End) || + (mods == ctrl && sym == XKB_KEY_e)) + term->search.cursor = term->search.len; + + else if (mods == 0 && sym == XKB_KEY_BackSpace) { + if (term->search.cursor > 0) { + memmove( + &term->search.buf[term->search.cursor - 1], + &term->search.buf[term->search.cursor], + (term->search.len - term->search.cursor) * sizeof(wchar_t)); + term->search.cursor--; + term->search.buf[--term->search.len] = L'\0'; + } + } + + else if ((mods == alt || mods == ctrl) && sym == XKB_KEY_BackSpace) { + size_t diff = distance_prev_word(term); + size_t old_cursor = term->search.cursor; + size_t new_cursor = old_cursor - diff; + + memmove(&term->search.buf[new_cursor], + &term->search.buf[old_cursor], + (term->search.len - old_cursor) * sizeof(wchar_t)); + + term->search.len -= diff; + term->search.cursor = new_cursor; + } + + else if ((mods == alt && sym == XKB_KEY_d) || + (mods == ctrl && sym == XKB_KEY_Delete)) { + size_t diff = distance_next_word(term); + size_t cursor = term->search.cursor; + + memmove(&term->search.buf[cursor], + &term->search.buf[cursor + diff], + (term->search.len - (cursor + diff)) * sizeof(wchar_t)); + + term->search.len -= diff; + } + + else if (mods == 0 && sym == XKB_KEY_Delete) { + if (term->search.cursor < term->search.len) { + memmove( + &term->search.buf[term->search.cursor], + &term->search.buf[term->search.cursor + 1], + (term->search.len - term->search.cursor - 1) * sizeof(wchar_t)); + term->search.buf[--term->search.len] = L'\0'; + } + } + + else { + uint8_t buf[64] = {0}; + int count = 0; + + if (compose_status == XKB_COMPOSE_COMPOSED) { + count = xkb_compose_state_get_utf8( + term->kbd.xkb_compose_state, (char *)buf, sizeof(buf)); + xkb_compose_state_reset(term->kbd.xkb_compose_state); + } else { + count = xkb_state_key_get_utf8( + term->kbd.xkb_state, key, (char *)buf, sizeof(buf)); + } + + const char *src = (const char *)buf; + mbstate_t ps = {0}; + size_t wchars = mbsnrtowcs(NULL, &src, count, 0, &ps); + + if (wchars == -1) { + LOG_ERRNO("failed to convert %.*s to wchars", count, buf); + return; + } + + while (term->search.len + wchars >= term->search.sz) { + size_t new_sz = term->search.sz == 0 ? 64 : term->search.sz * 2; + wchar_t *new_buf = realloc(term->search.buf, new_sz * sizeof(term->search.buf[0])); + + if (new_buf == NULL) { + LOG_ERRNO("failed to resize search buffer"); + return; + } + + term->search.buf = new_buf; + term->search.sz = new_sz; + } + + assert(term->search.len + wchars < term->search.sz); + + memmove(&term->search.buf[term->search.cursor + wchars], + &term->search.buf[term->search.cursor], + (term->search.len - term->search.cursor) * sizeof(wchar_t)); + + memset(&ps, 0, sizeof(ps)); + mbsnrtowcs(&term->search.buf[term->search.cursor], &src, count, + wchars, &ps); + + term->search.len += wchars; + term->search.cursor += wchars; + term->search.buf[term->search.len] = L'\0'; + } + + LOG_DBG("search: buffer: %S", term->search.buf); + search_update(term); + render_refresh(term); + render_search_box(term); +} diff --git a/search.h b/search.h new file mode 100644 index 00000000..4ea59c1f --- /dev/null +++ b/search.h @@ -0,0 +1,8 @@ +#pragma once + +#include +#include "terminal.h" + +void search_begin(struct terminal *term); +void search_cancel(struct terminal *term); +void search_input(struct terminal *term, uint32_t key, xkb_keysym_t sym, xkb_mod_mask_t mods); diff --git a/selection.c b/selection.c index 45f970f2..c4991906 100644 --- a/selection.c +++ b/selection.c @@ -20,7 +20,10 @@ bool selection_enabled(const struct terminal *term) { - return term->mouse_tracking == MOUSE_NONE || term->kbd.shift; + return + term->mouse_tracking == MOUSE_NONE || + term->kbd.shift || + term->is_searching; } bool diff --git a/terminal.c b/terminal.c index b9d2f0f3..95a60682 100644 --- a/terminal.c +++ b/terminal.c @@ -362,6 +362,16 @@ term_reverse_index(struct terminal *term) term_cursor_up(term, 1); } +void +term_reset_view(struct terminal *term) +{ + if (term->grid->view == term->grid->offset) + return; + + term->grid->view = term->grid->offset; + term_damage_view(term); +} + void term_restore_cursor(struct terminal *term) { diff --git a/terminal.h b/terminal.h index 6f373a14..f435ab3b 100644 --- a/terminal.h +++ b/terminal.h @@ -41,15 +41,20 @@ struct wayland { struct wl_display *display; struct wl_registry *registry; struct wl_compositor *compositor; - struct wl_surface *surface; + struct wl_subcompositor *sub_compositor; struct wl_shm *shm; + struct wl_seat *seat; + struct wl_keyboard *keyboard; struct zxdg_output_manager_v1 *xdg_output_manager; + + /* Clipboard */ struct wl_data_device_manager *data_device_manager; struct wl_data_device *data_device; struct zwp_primary_selection_device_manager_v1 *primary_selection_device_manager; struct zwp_primary_selection_device_v1 *primary_selection_device; - struct wl_keyboard *keyboard; + + /* Cursor */ struct { struct wl_pointer *pointer; uint32_t serial; @@ -60,9 +65,20 @@ struct wayland { int size; char *theme_name; } pointer; + + /* Main window */ + struct wl_surface *surface; struct xdg_wm_base *shell; struct xdg_surface *xdg_surface; struct xdg_toplevel *xdg_toplevel; + + struct zxdg_decoration_manager_v1 *xdg_decoration_manager; + struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration; + + /* Scrollback search */ + struct wl_surface *search_surface; + struct wl_subsurface *search_sub_surface; + bool have_argb8888; tll(struct monitor) monitors; /* All available outputs */ tll(const struct monitor *) on_outputs; /* Outputs we're mapped on */ @@ -331,6 +347,20 @@ struct terminal { struct primary primary; } selection; + bool is_searching; + struct { + wchar_t *buf; + size_t len; + size_t sz; + size_t cursor; + enum { SEARCH_BACKWARD, SEARCH_FORWARD} direction; + + int original_view; + bool view_followed_offset; + struct coord match; + size_t match_len; + } search; + struct grid normal; struct grid alt; struct grid *grid; @@ -368,6 +398,7 @@ struct terminal { struct buffer *last_buf; /* Buffer we rendered to last time */ bool was_flashing; /* Flash was active last time we rendered */ + bool was_searching; } render; }; @@ -379,6 +410,8 @@ void term_damage_rows_in_view(struct terminal *term, int start, int end); void term_damage_all(struct terminal *term); void term_damage_view(struct terminal *term); +void term_reset_view(struct terminal *term); + void term_damage_scroll( struct terminal *term, enum damage_type damage_type, struct scroll_region region, int lines);