mirror of
https://codeberg.org/dnkl/foot.git
synced 2026-02-05 04:06:08 -05:00
While most compositors handle instantiated but not-yet-mapped sub-surfaces correctly, e.g. kwin does not. For example, it will incorrectly offset the main surface both horizontally and vertically with a couple of pixels, leaving two transparent areas at the top and left, between the SSDs and the main surface. Note that a workaround is to position the sub-surfaces inside the main surface while they're unmapped. However, since the surfaces may be larger than the main surface (the CSDs, for examples, always are), this doesn't quite work since kwin, at least, resizes the window to include the sub-surfaces, even when unmapped. So, instead we instantiate all sub-surfaces on demand, when we know where to position them, and when they should be mapped.
624 lines
19 KiB
C
624 lines
19 KiB
C
#include "search.h"
|
|
|
|
#include <string.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 "misc.h"
|
|
#include "render.h"
|
|
#include "selection.h"
|
|
#include "shm.h"
|
|
|
|
#define max(x, y) ((x) > (y) ? (x) : (y))
|
|
|
|
static bool
|
|
search_ensure_size(struct terminal *term, size_t wanted_size)
|
|
{
|
|
while (wanted_size >= 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 false;
|
|
}
|
|
|
|
term->search.buf = new_buf;
|
|
term->search.sz = new_sz;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
search_cancel_keep_selection(struct terminal *term)
|
|
{
|
|
struct wl_window *win = term->window;
|
|
if (win->search_sub_surface != NULL)
|
|
wl_subsurface_destroy(win->search_sub_surface);
|
|
if (win->search_surface != NULL)
|
|
wl_surface_destroy(win->search_surface);
|
|
|
|
win->search_surface = NULL;
|
|
win->search_sub_surface = NULL;
|
|
|
|
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;
|
|
term->render.search_glyph_offset = 0;
|
|
|
|
term_xcursor_update(term);
|
|
render_refresh(term);
|
|
}
|
|
|
|
void
|
|
search_begin(struct terminal *term)
|
|
{
|
|
LOG_DBG("search: begin");
|
|
|
|
search_cancel_keep_selection(term);
|
|
selection_cancel(term);
|
|
|
|
/* On-demand instantiate wayland surface */
|
|
struct wl_window *win = term->window;
|
|
struct wayland *wayl = term->wl;
|
|
win->search_surface = wl_compositor_create_surface(wayl->compositor);
|
|
win->search_sub_surface = wl_subcompositor_get_subsurface(
|
|
wayl->sub_compositor, win->search_surface, win->surface);
|
|
wl_subsurface_set_desync(win->search_sub_surface);
|
|
|
|
struct wl_region *region = wl_compositor_create_region(term->wl->compositor);
|
|
wl_region_add(region, 0, 0, INT32_MAX, INT32_MAX);
|
|
wl_surface_set_opaque_region(win->search_surface, region);
|
|
wl_region_destroy(region);
|
|
|
|
term->search.original_view = term->grid->view;
|
|
term->search.view_followed_offset = term->grid->view == term->grid->offset;
|
|
term->is_searching = true;
|
|
|
|
term_xcursor_update(term);
|
|
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_selection(struct terminal *term,
|
|
int start_row, int start_col,
|
|
int end_row, int end_col)
|
|
{
|
|
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;
|
|
end_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, SELECTION_NORMAL);
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
}
|
|
|
|
static void
|
|
search_find_next(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++)
|
|
{
|
|
for (;
|
|
backward ? start_col >= 0 : start_col < term->cols;
|
|
backward ? start_col-- : start_col++)
|
|
{
|
|
const struct row *row = term->grid->rows[start_row];
|
|
if (row == NULL)
|
|
continue;
|
|
|
|
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 (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 (wcsncasecmp(&row->cells[end_col].wc, &term->search.buf[i], 1) != 0)
|
|
break;
|
|
|
|
end_col++;
|
|
}
|
|
|
|
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.
|
|
*/
|
|
search_update_selection(term, start_row, start_col, end_row, end_col);
|
|
|
|
/* 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 void
|
|
search_match_to_end_of_word(struct terminal *term, bool spaces_only)
|
|
{
|
|
if (term->search.match_len == 0)
|
|
return;
|
|
|
|
assert(term->search.match.row != -1);
|
|
assert(term->search.match.col != -1);
|
|
|
|
int end_row = term->search.match.row;
|
|
int end_col = term->search.match.col;
|
|
size_t len = term->search.match_len;
|
|
|
|
/* Calculate end coord - note: assumed to be valid */
|
|
for (size_t i = 0; i < len; i++) {
|
|
if (++end_col >= term->cols) {
|
|
end_row = (end_row + 1) % term->grid->num_rows;
|
|
end_col = 0;
|
|
}
|
|
}
|
|
|
|
tll(wchar_t) new_chars = tll_init();
|
|
|
|
/* Always append at least one character *if* possible */
|
|
bool first = true;
|
|
|
|
for (size_t r = 0;
|
|
r < term->grid->num_rows;
|
|
end_row = (end_row + 1) % term->grid->num_rows, r++)
|
|
{
|
|
const struct row *row = term->grid->rows[end_row];
|
|
if (row == NULL)
|
|
break;
|
|
|
|
bool done = false;
|
|
for (; end_col < term->cols; end_col++) {
|
|
wchar_t wc = row->cells[end_col].wc;
|
|
if (wc == 0 || (!first && !isword(wc, spaces_only))) {
|
|
done = true;
|
|
break;
|
|
}
|
|
|
|
first = false;
|
|
tll_push_back(new_chars, wc);
|
|
}
|
|
|
|
if (done)
|
|
break;
|
|
|
|
end_col = 0;
|
|
}
|
|
|
|
if (tll_length(new_chars) == 0)
|
|
return;
|
|
|
|
if (!search_ensure_size(term, term->search.len + tll_length(new_chars)))
|
|
return;
|
|
|
|
/* Keep cursor at the end, but don't move it if not */
|
|
bool move_cursor = term->search.cursor == term->search.len;
|
|
|
|
/* Append newly found characters to the search buffer */
|
|
tll_foreach(new_chars, it)
|
|
term->search.buf[term->search.len++] = it->item;
|
|
term->search.buf[term->search.len] = L'\0';
|
|
|
|
if (move_cursor)
|
|
term->search.cursor += tll_length(new_chars);
|
|
|
|
tll_free(new_chars);
|
|
|
|
search_update_selection(
|
|
term, term->search.match.row, term->search.match.col, end_row, end_col);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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->wl->kbd.mod_ctrl;
|
|
const xkb_mod_mask_t alt = 1 << term->wl->kbd.mod_alt;
|
|
const xkb_mod_mask_t shift = 1 << term->wl->kbd.mod_shift;
|
|
//const xkb_mod_mask_t meta = 1 << term->wl->kbd.mod_meta;
|
|
|
|
enum xkb_compose_status compose_status = xkb_compose_state_get_status(
|
|
term->wl->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->wl->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) ||
|
|
(mods == ctrl && sym == XKB_KEY_b))
|
|
{
|
|
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) ||
|
|
(mods == ctrl && sym == XKB_KEY_f))
|
|
{
|
|
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 if (mods == ctrl && sym == XKB_KEY_w)
|
|
search_match_to_end_of_word(term, false);
|
|
|
|
else if (mods == (ctrl | shift) && sym == XKB_KEY_W)
|
|
search_match_to_end_of_word(term, true);
|
|
|
|
else {
|
|
uint8_t buf[64] = {0};
|
|
int count = 0;
|
|
|
|
if (compose_status == XKB_COMPOSE_COMPOSED) {
|
|
count = xkb_compose_state_get_utf8(
|
|
term->wl->kbd.xkb_compose_state, (char *)buf, sizeof(buf));
|
|
xkb_compose_state_reset(term->wl->kbd.xkb_compose_state);
|
|
} else if (compose_status == XKB_COMPOSE_CANCELLED) {
|
|
count = 0;
|
|
} else {
|
|
count = xkb_state_key_get_utf8(
|
|
term->wl->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;
|
|
}
|
|
|
|
if (!search_ensure_size(term, term->search.len + wchars))
|
|
return;
|
|
|
|
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_find_next(term);
|
|
render_refresh(term);
|
|
render_search_box(term);
|
|
}
|