Merge branch 'scrollback-search'

This commit is contained in:
Daniel Eklöf 2019-08-30 20:16:12 +02:00
commit e673bd4ab0
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
12 changed files with 940 additions and 88 deletions

171
README.md
View file

@ -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
* <kbd>shift</kbd>+<kbd>page up</kbd>/<kbd>page down</kbd>
Scroll up/down in history
* <kbd>ctrl</kbd>+<kbd>shift</kbd>+<kbd>c</kbd>
Copy selected text to the _clipboard_
* <kbd>ctrl</kbd>+<kbd>shift</kbd>+<kbd>v</kbd>
Paste from _clipboard_
* <kbd>ctrl</kbd>+<kbd>shift</kbd>+<kbd>r</kbd>
Start a scrollback search
While doing a scrollback search, the following shortcuts are
available:
* <kbd>ctrl</kbd>+<kbd>r</kbd>
Search for next match
* <kbd>escape</kbd>
Cancel the search
* <kbd>ctrl</kbd>+<kbd>g</kbd>
Cancel the search (same as <kbd>escape</kbd>)
* <kbd>return</kbd>
Finish the search and put the current match to the primary selection
### Mouse
* <kbd>left</kbd> - **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 <kbd>shift</kbd>.
* <kbd>left</kbd> - **double-click**
Selects the _word_ (separated by spaces, period, comma, parenthesis
etc) under the pointer. Hold <kbd>ctrl</kbd> to select everything
under the pointer up to, and until, the next space characters.
* <kbd>left</kbd> - **triple-click**
Selects the entire row
* <kbd>middle</kbd>
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.

16
grid.h
View file

@ -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);

33
input.c
View file

@ -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 */

122
main.c
View file

@ -14,6 +14,7 @@
#include <sys/sysinfo.h>
#include <sys/prctl.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <freetype/tttables.h>
#include <wayland-client.h>
@ -22,6 +23,7 @@
#include <xkbcommon/xkbcommon-compose.h>
#include <xdg-output-unstable-v1.h>
#include <xdg-decoration-unstable-v1.h>
#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)

View file

@ -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',

127
render.c
View file

@ -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)

View file

@ -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);

496
search.c Normal file
View file

@ -0,0 +1,496 @@
#include "search.h"
#include <wchar.h>
#include <wctype.h>
#include <wayland-client.h>
#include <xkbcommon/xkbcommon-compose.h>
#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);
}

8
search.h Normal file
View file

@ -0,0 +1,8 @@
#pragma once
#include <xkbcommon/xkbcommon.h>
#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);

View file

@ -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

View file

@ -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)
{

View file

@ -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);