add session save and session load functionality
New [key-bindings]:
- session-save: captures cwd and foreground process argv to ~/.local/share/foot/state/{name}.json
- session-save-secure: prompts for a password, encrypts the scrollback with argon2id + XChaCha20-Poly1305 (libsodium) and writes it to {name}.scrollback.enc(stores up to 1Mb scrollback buffer).
- session-load: a minimal fuzzy picker that displays saved sessions (both secure and vanilla), UI piggybacks on search bar subsurface. use arrows to navigate and delete to delete a previously saved session.
This commit is contained in:
parent
05ee680778
commit
cabddb26e6
16 changed files with 1947 additions and 49 deletions
229
render.c
229
render.c
|
|
@ -3635,6 +3635,21 @@ static void render_search_box(struct terminal *term) {
|
|||
const size_t text_len = term->search.len;
|
||||
#endif
|
||||
|
||||
/* Password modes: replace every codepoint with a mask glyph for display. */
|
||||
char32_t *masked_text = NULL;
|
||||
char32_t *ime_alloc = NULL;
|
||||
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
|
||||
ime_alloc = text; /* remember the IME-allocated buffer for cleanup */
|
||||
#endif
|
||||
if (term->search.mode == SEARCH_MODE_SESSION_SAVE_SECURE_PASSWORD ||
|
||||
term->search.mode == SEARCH_MODE_SESSION_LOAD_PASSWORD) {
|
||||
masked_text = xmalloc((text_len + 1) * sizeof(char32_t));
|
||||
for (size_t i = 0; i < text_len; i++)
|
||||
masked_text[i] = U'•';
|
||||
masked_text[text_len] = U'\0';
|
||||
text = masked_text;
|
||||
}
|
||||
|
||||
/* Calculate the width of each character */
|
||||
int widths[text_len + 1];
|
||||
for (size_t i = 0; i < text_len; i++)
|
||||
|
|
@ -3642,7 +3657,16 @@ static void render_search_box(struct terminal *term) {
|
|||
widths[text_len] = 0;
|
||||
|
||||
const size_t total_cells = c32swidth(text, text_len);
|
||||
const size_t wanted_visible_cells = max(20, total_cells);
|
||||
size_t wanted_visible_cells = max(20, total_cells);
|
||||
if (term->search.mode == SEARCH_MODE_SESSION_LOAD) {
|
||||
/* Make room for the longest session name */
|
||||
for (size_t i = 0; i < term->session_picker.all_count; i++) {
|
||||
size_t n = strlen(term->session_picker.all[i]);
|
||||
if (n > wanted_visible_cells)
|
||||
wanted_visible_cells = n;
|
||||
}
|
||||
wanted_visible_cells = max(wanted_visible_cells, 30);
|
||||
}
|
||||
|
||||
/*
|
||||
* Build status string: " Aa Wb Re 3/17 ↻"
|
||||
|
|
@ -3655,36 +3679,70 @@ static void render_search_box(struct terminal *term) {
|
|||
char32_t status[64];
|
||||
size_t st_len = 0;
|
||||
|
||||
if (term->search.case_mode == SEARCH_CASE_SENSITIVE) {
|
||||
status[st_len++] = U'A';
|
||||
status[st_len++] = U'a';
|
||||
status[st_len++] = U' ';
|
||||
} else if (term->search.case_mode == SEARCH_CASE_INSENSITIVE) {
|
||||
status[st_len++] = U'a';
|
||||
status[st_len++] = U'a';
|
||||
status[st_len++] = U' ';
|
||||
}
|
||||
if (term->search.whole_word) {
|
||||
status[st_len++] = U'W';
|
||||
status[st_len++] = U'b';
|
||||
status[st_len++] = U' ';
|
||||
}
|
||||
if (term->search.regex) {
|
||||
status[st_len++] = U'.';
|
||||
status[st_len++] = U'*';
|
||||
status[st_len++] = U' ';
|
||||
}
|
||||
if (term->search.wrapped) {
|
||||
status[st_len++] = U'~';
|
||||
status[st_len++] = U' ';
|
||||
}
|
||||
if (term->search.len > 0 && term->search.total_count > 0) {
|
||||
char tmp[32];
|
||||
snprintf(tmp, sizeof(tmp), "%zu/%zu", term->search.current_idx,
|
||||
term->search.total_count);
|
||||
for (size_t i = 0; tmp[i] != '\0' && st_len < ALEN(status) - 1; i++)
|
||||
status[st_len++] = (char32_t)tmp[i];
|
||||
status[st_len++] = U' ';
|
||||
if (term->search.mode != SEARCH_MODE_NORMAL) {
|
||||
char label_buf[128];
|
||||
const char *label;
|
||||
switch (term->search.mode) {
|
||||
case SEARCH_MODE_SESSION_OVERWRITE_CONFIRM:
|
||||
snprintf(label_buf, sizeof(label_buf), "Overwrite '%s'? (y/n) ",
|
||||
term->session_pending_name != NULL
|
||||
? term->session_pending_name
|
||||
: "");
|
||||
label = label_buf;
|
||||
break;
|
||||
case SEARCH_MODE_SESSION_SAVE:
|
||||
label = "Save State As: ";
|
||||
break;
|
||||
case SEARCH_MODE_SESSION_SAVE_SECURE_NAME:
|
||||
label = "Secure Save As: ";
|
||||
break;
|
||||
case SEARCH_MODE_SESSION_SAVE_SECURE_PASSWORD:
|
||||
label = "Password (new): ";
|
||||
break;
|
||||
case SEARCH_MODE_SESSION_LOAD:
|
||||
label = "Load State: ";
|
||||
break;
|
||||
case SEARCH_MODE_SESSION_LOAD_PASSWORD:
|
||||
label = "Decrypt Password: ";
|
||||
break;
|
||||
default:
|
||||
label = "";
|
||||
break;
|
||||
}
|
||||
for (const char *p = label; *p != '\0' && st_len < ALEN(status) - 1; p++)
|
||||
status[st_len++] = (char32_t)(unsigned char)*p;
|
||||
} else {
|
||||
if (term->search.case_mode == SEARCH_CASE_SENSITIVE) {
|
||||
status[st_len++] = U'A';
|
||||
status[st_len++] = U'a';
|
||||
status[st_len++] = U' ';
|
||||
} else if (term->search.case_mode == SEARCH_CASE_INSENSITIVE) {
|
||||
status[st_len++] = U'a';
|
||||
status[st_len++] = U'a';
|
||||
status[st_len++] = U' ';
|
||||
}
|
||||
if (term->search.whole_word) {
|
||||
status[st_len++] = U'W';
|
||||
status[st_len++] = U'b';
|
||||
status[st_len++] = U' ';
|
||||
}
|
||||
if (term->search.regex) {
|
||||
status[st_len++] = U'.';
|
||||
status[st_len++] = U'*';
|
||||
status[st_len++] = U' ';
|
||||
}
|
||||
if (term->search.wrapped) {
|
||||
status[st_len++] = U'~';
|
||||
status[st_len++] = U' ';
|
||||
}
|
||||
if (term->search.len > 0 && term->search.total_count > 0) {
|
||||
char tmp[32];
|
||||
snprintf(tmp, sizeof(tmp), "%zu/%zu", term->search.current_idx,
|
||||
term->search.total_count);
|
||||
for (size_t i = 0; tmp[i] != '\0' && st_len < ALEN(status) - 1; i++)
|
||||
status[st_len++] = (char32_t)tmp[i];
|
||||
status[st_len++] = U' ';
|
||||
}
|
||||
}
|
||||
status[st_len] = U'\0';
|
||||
const size_t status_cells = c32swidth(status, st_len);
|
||||
|
|
@ -3704,8 +3762,20 @@ static void render_search_box(struct terminal *term) {
|
|||
const size_t want_box_w = chrome_w + wanted_visible_cells * term->cell_width;
|
||||
|
||||
size_t width = min(want_box_w, max_box_w);
|
||||
size_t height =
|
||||
min(term->height - 2 * outer_margin, margin + term->cell_height + margin);
|
||||
|
||||
/* Session-load picker: grow box to fit a few rows of session names */
|
||||
size_t list_rows = 0;
|
||||
if (term->search.mode == SEARCH_MODE_SESSION_LOAD) {
|
||||
const size_t max_visible = 10;
|
||||
list_rows = min(max_visible, term->session_picker.filtered_count);
|
||||
/* Reserve at least one row so empty state is visible */
|
||||
if (list_rows == 0)
|
||||
list_rows = 1;
|
||||
}
|
||||
|
||||
size_t height = min(
|
||||
term->height - 2 * outer_margin,
|
||||
margin + (1 + list_rows) * term->cell_height + margin);
|
||||
|
||||
width = roundf(scale * ceilf(width / scale));
|
||||
height = roundf(scale * ceilf(height / scale));
|
||||
|
|
@ -3732,12 +3802,14 @@ static void render_search_box(struct terminal *term) {
|
|||
uint32_t bg_hex = term->colors.bg;
|
||||
uint32_t fg_hex = term->colors.fg;
|
||||
|
||||
if (term->search.len > 0 && term->search.match_len == 0) {
|
||||
bg_hex = term->colors.table[1];
|
||||
fg_hex = term->colors.table[0];
|
||||
} else if (term->search.wrapped) {
|
||||
bg_hex = term->colors.table[3];
|
||||
fg_hex = term->colors.table[0];
|
||||
if (term->search.mode == SEARCH_MODE_NORMAL) {
|
||||
if (term->search.len > 0 && term->search.match_len == 0) {
|
||||
bg_hex = term->colors.table[1];
|
||||
fg_hex = term->colors.table[0];
|
||||
} else if (term->search.wrapped) {
|
||||
bg_hex = term->colors.table[3];
|
||||
fg_hex = term->colors.table[0];
|
||||
}
|
||||
}
|
||||
|
||||
const pixman_color_t color = color_hex_to_pixman(bg_hex, gamma_correct);
|
||||
|
|
@ -3973,6 +4045,80 @@ static void render_search_box(struct terminal *term) {
|
|||
}
|
||||
}
|
||||
|
||||
/* Session-load list */
|
||||
if (term->search.mode == SEARCH_MODE_SESSION_LOAD) {
|
||||
const pixman_color_t sel_bg = color_hex_to_pixman(
|
||||
term->colors.table[4], gamma_correct);
|
||||
const size_t name_x = margin;
|
||||
const size_t name_w = width > 2 * margin ? width - 2 * margin : 0;
|
||||
|
||||
if (term->session_picker.filtered_count == 0) {
|
||||
const char32_t *msg = U"(no sessions)";
|
||||
int rx = (int)name_x;
|
||||
const int ry = (int)margin + (int)term->cell_height;
|
||||
for (size_t i = 0; msg[i] != U'\0'; i++) {
|
||||
const struct fcft_glyph *g =
|
||||
fcft_rasterize_char_utf32(font, msg[i], term->font_subpixel);
|
||||
int gw = max(1, c32width(msg[i]));
|
||||
if (g != NULL && !g->is_color_glyph) {
|
||||
pixman_image_t *src = pixman_image_create_solid_fill(&fg);
|
||||
pixman_image_composite32(PIXMAN_OP_OVER, src, g->pix, buf->pix[0], 0,
|
||||
0, 0, 0, rx + x_ofs + g->x,
|
||||
ry + term->font_baseline - g->y, g->width,
|
||||
g->height);
|
||||
pixman_image_unref(src);
|
||||
}
|
||||
rx += gw * (int)term->cell_width;
|
||||
}
|
||||
} else {
|
||||
const size_t shown = min(list_rows, term->session_picker.filtered_count);
|
||||
/* Scroll the list so the selection is visible */
|
||||
size_t first = 0;
|
||||
if (term->session_picker.sel >= shown)
|
||||
first = term->session_picker.sel - shown + 1;
|
||||
for (size_t r = 0; r < shown; r++) {
|
||||
size_t fi = first + r;
|
||||
if (fi >= term->session_picker.filtered_count)
|
||||
break;
|
||||
const char *name =
|
||||
term->session_picker.all[term->session_picker.filtered[fi]];
|
||||
|
||||
const int ry = (int)margin + (int)(1 + r) * (int)term->cell_height;
|
||||
const bool selected = fi == term->session_picker.sel;
|
||||
|
||||
if (selected) {
|
||||
pixman_image_fill_rectangles(
|
||||
PIXMAN_OP_SRC, buf->pix[0], &sel_bg, 1,
|
||||
&(pixman_rectangle16_t){(int16_t)name_x, (int16_t)ry,
|
||||
(uint16_t)name_w,
|
||||
(uint16_t)term->cell_height});
|
||||
}
|
||||
|
||||
const pixman_color_t row_fg = selected
|
||||
? color_hex_to_pixman(term->colors.bg, gamma_correct)
|
||||
: fg;
|
||||
|
||||
int rx = (int)name_x;
|
||||
for (const char *p = name; *p != '\0'; p++) {
|
||||
char32_t ch = (char32_t)(unsigned char)*p;
|
||||
const struct fcft_glyph *g =
|
||||
fcft_rasterize_char_utf32(font, ch, term->font_subpixel);
|
||||
if (g != NULL && !g->is_color_glyph) {
|
||||
pixman_image_t *src = pixman_image_create_solid_fill(&row_fg);
|
||||
pixman_image_composite32(PIXMAN_OP_OVER, src, g->pix, buf->pix[0],
|
||||
0, 0, 0, 0, rx + x_ofs + g->x,
|
||||
ry + term->font_baseline - g->y,
|
||||
g->width, g->height);
|
||||
pixman_image_unref(src);
|
||||
}
|
||||
rx += (int)term->cell_width;
|
||||
if (rx > (int)(name_x + name_w))
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
quirk_weston_subsurface_desync_on(term->window->search.sub);
|
||||
|
||||
/* TODO: this is only necessary on a window resize */
|
||||
|
|
@ -3998,9 +4144,8 @@ static void render_search_box(struct terminal *term) {
|
|||
wl_surface_commit(term->window->search.surface.surf);
|
||||
quirk_weston_subsurface_desync_off(term->window->search.sub);
|
||||
|
||||
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
|
||||
free(text);
|
||||
#endif
|
||||
free(ime_alloc);
|
||||
free(masked_text);
|
||||
#undef WINDOW_X
|
||||
#undef WINDOW_Y
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue