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