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
5
config.c
5
config.c
|
|
@ -130,6 +130,11 @@ static const char *const binding_action_map[] = {
|
||||||
[BIND_ACTION_TAB_9] = "tab-9",
|
[BIND_ACTION_TAB_9] = "tab-9",
|
||||||
[BIND_ACTION_TAB_OVERVIEW] = "tab-overview",
|
[BIND_ACTION_TAB_OVERVIEW] = "tab-overview",
|
||||||
|
|
||||||
|
/* Session actions */
|
||||||
|
[BIND_ACTION_SESSION_SAVE] = "session-save",
|
||||||
|
[BIND_ACTION_SESSION_LOAD] = "session-load",
|
||||||
|
[BIND_ACTION_SESSION_SAVE_SECURE] = "session-save-secure",
|
||||||
|
|
||||||
/* Mouse-specific actions */
|
/* Mouse-specific actions */
|
||||||
[BIND_ACTION_SCROLLBACK_UP_MOUSE] = "scrollback-up-mouse",
|
[BIND_ACTION_SCROLLBACK_UP_MOUSE] = "scrollback-up-mouse",
|
||||||
[BIND_ACTION_SCROLLBACK_DOWN_MOUSE] = "scrollback-down-mouse",
|
[BIND_ACTION_SCROLLBACK_DOWN_MOUSE] = "scrollback-down-mouse",
|
||||||
|
|
|
||||||
5
csi.c
5
csi.c
|
|
@ -1583,7 +1583,10 @@ void csi_dispatch(struct terminal *term, uint8_t final) {
|
||||||
/* 0 - icon + title, 1 - icon, 2 - title */
|
/* 0 - icon + title, 1 - icon, 2 - title */
|
||||||
unsigned what = vt_param_get(term, 1, 0);
|
unsigned what = vt_param_get(term, 1, 0);
|
||||||
if (what == 0 || what == 2) {
|
if (what == 0 || what == 2) {
|
||||||
tll_push_back(term->window_title_stack, xstrdup(term->window_title));
|
tll_push_back(term->window_title_stack,
|
||||||
|
xstrdup(term->window_title != NULL
|
||||||
|
? term->window_title
|
||||||
|
: ""));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
3
foot.ini
3
foot.ini
|
|
@ -251,6 +251,9 @@ height=26
|
||||||
# tab-next=Control+Tab
|
# tab-next=Control+Tab
|
||||||
# tab-prev=Control+Shift+Tab
|
# tab-prev=Control+Shift+Tab
|
||||||
# tab-overview=Control+Shift+space
|
# tab-overview=Control+Shift+space
|
||||||
|
# session-save=Control+Shift+s
|
||||||
|
# session-load=Control+Shift+l
|
||||||
|
# session-save-secure=Control+Shift+Alt+s
|
||||||
|
|
||||||
[search-bindings]
|
[search-bindings]
|
||||||
# cancel=Control+g Control+c Escape
|
# cancel=Control+g Control+c Escape
|
||||||
|
|
|
||||||
12
input.c
12
input.c
|
|
@ -491,6 +491,18 @@ static bool execute_binding(struct seat *seat, struct terminal *term,
|
||||||
tab_overview_toggle(term->window);
|
tab_overview_toggle(term->window);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case BIND_ACTION_SESSION_SAVE:
|
||||||
|
search_begin_session(term, SEARCH_MODE_SESSION_SAVE);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case BIND_ACTION_SESSION_LOAD:
|
||||||
|
search_begin_session(term, SEARCH_MODE_SESSION_LOAD);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case BIND_ACTION_SESSION_SAVE_SECURE:
|
||||||
|
search_begin_session(term, SEARCH_MODE_SESSION_SAVE_SECURE_NAME);
|
||||||
|
return true;
|
||||||
|
|
||||||
case BIND_ACTION_REGEX_LAUNCH:
|
case BIND_ACTION_REGEX_LAUNCH:
|
||||||
case BIND_ACTION_REGEX_COPY:
|
case BIND_ACTION_REGEX_COPY:
|
||||||
if (binding->aux->type != BINDING_AUX_REGEX)
|
if (binding->aux->type != BINDING_AUX_REGEX)
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,11 @@ enum bind_action_normal {
|
||||||
BIND_ACTION_TAB_9,
|
BIND_ACTION_TAB_9,
|
||||||
BIND_ACTION_TAB_OVERVIEW,
|
BIND_ACTION_TAB_OVERVIEW,
|
||||||
|
|
||||||
|
/* Session actions */
|
||||||
|
BIND_ACTION_SESSION_SAVE,
|
||||||
|
BIND_ACTION_SESSION_LOAD,
|
||||||
|
BIND_ACTION_SESSION_SAVE_SECURE,
|
||||||
|
|
||||||
/* Mouse specific actions - i.e. they require a mouse coordinate */
|
/* Mouse specific actions - i.e. they require a mouse coordinate */
|
||||||
BIND_ACTION_SCROLLBACK_UP_MOUSE,
|
BIND_ACTION_SCROLLBACK_UP_MOUSE,
|
||||||
BIND_ACTION_SCROLLBACK_DOWN_MOUSE,
|
BIND_ACTION_SCROLLBACK_DOWN_MOUSE,
|
||||||
|
|
@ -77,7 +82,7 @@ enum bind_action_normal {
|
||||||
BIND_ACTION_SELECT_QUOTE,
|
BIND_ACTION_SELECT_QUOTE,
|
||||||
BIND_ACTION_SELECT_ROW,
|
BIND_ACTION_SELECT_ROW,
|
||||||
|
|
||||||
BIND_ACTION_KEY_COUNT = BIND_ACTION_TAB_OVERVIEW + 1,
|
BIND_ACTION_KEY_COUNT = BIND_ACTION_SESSION_SAVE_SECURE + 1,
|
||||||
BIND_ACTION_COUNT = BIND_ACTION_SELECT_ROW + 1,
|
BIND_ACTION_COUNT = BIND_ACTION_SELECT_ROW + 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,7 @@ endif
|
||||||
|
|
||||||
tllist = dependency('tllist', version: '>=1.1.0', fallback: 'tllist')
|
tllist = dependency('tllist', version: '>=1.1.0', fallback: 'tllist')
|
||||||
fcft = dependency('fcft', version: ['>=3.3.1', '<4.0.0'], fallback: 'fcft')
|
fcft = dependency('fcft', version: ['>=3.3.1', '<4.0.0'], fallback: 'fcft')
|
||||||
|
libsodium = dependency('libsodium')
|
||||||
|
|
||||||
wayland_protocols_datadir = wayland_protocols.get_variable('pkgdatadir')
|
wayland_protocols_datadir = wayland_protocols.get_variable('pkgdatadir')
|
||||||
|
|
||||||
|
|
@ -330,6 +331,8 @@ executable(
|
||||||
'reaper.c', 'reaper.h',
|
'reaper.c', 'reaper.h',
|
||||||
'render.c', 'render.h',
|
'render.c', 'render.h',
|
||||||
'search.c', 'search.h',
|
'search.c', 'search.h',
|
||||||
|
'session.c', 'session.h', 'session-prompt.c',
|
||||||
|
'session-crypto.c', 'session-crypto.h',
|
||||||
'server.c', 'server.h', 'client-protocol.h',
|
'server.c', 'server.h', 'client-protocol.h',
|
||||||
'shm.c', 'shm.h',
|
'shm.c', 'shm.h',
|
||||||
'slave.c', 'slave.h',
|
'slave.c', 'slave.h',
|
||||||
|
|
@ -342,7 +345,7 @@ executable(
|
||||||
'xkbcommon-vmod.h',
|
'xkbcommon-vmod.h',
|
||||||
srgb_funcs, wl_proto_src + wl_proto_headers, version,
|
srgb_funcs, wl_proto_src + wl_proto_headers, version,
|
||||||
dependencies: [math, threads, libepoll, pixman, wayland_client, wayland_cursor, xkb, fontconfig, utf8proc,
|
dependencies: [math, threads, libepoll, pixman, wayland_client, wayland_cursor, xkb, fontconfig, utf8proc,
|
||||||
tllist, fcft],
|
tllist, fcft, libsodium],
|
||||||
link_with: pgolib,
|
link_with: pgolib,
|
||||||
install: true)
|
install: true)
|
||||||
|
|
||||||
|
|
|
||||||
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;
|
const size_t text_len = term->search.len;
|
||||||
#endif
|
#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 */
|
/* Calculate the width of each character */
|
||||||
int widths[text_len + 1];
|
int widths[text_len + 1];
|
||||||
for (size_t i = 0; i < text_len; i++)
|
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;
|
widths[text_len] = 0;
|
||||||
|
|
||||||
const size_t total_cells = c32swidth(text, text_len);
|
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 ↻"
|
* Build status string: " Aa Wb Re 3/17 ↻"
|
||||||
|
|
@ -3655,36 +3679,70 @@ static void render_search_box(struct terminal *term) {
|
||||||
char32_t status[64];
|
char32_t status[64];
|
||||||
size_t st_len = 0;
|
size_t st_len = 0;
|
||||||
|
|
||||||
if (term->search.case_mode == SEARCH_CASE_SENSITIVE) {
|
if (term->search.mode != SEARCH_MODE_NORMAL) {
|
||||||
status[st_len++] = U'A';
|
char label_buf[128];
|
||||||
status[st_len++] = U'a';
|
const char *label;
|
||||||
status[st_len++] = U' ';
|
switch (term->search.mode) {
|
||||||
} else if (term->search.case_mode == SEARCH_CASE_INSENSITIVE) {
|
case SEARCH_MODE_SESSION_OVERWRITE_CONFIRM:
|
||||||
status[st_len++] = U'a';
|
snprintf(label_buf, sizeof(label_buf), "Overwrite '%s'? (y/n) ",
|
||||||
status[st_len++] = U'a';
|
term->session_pending_name != NULL
|
||||||
status[st_len++] = U' ';
|
? term->session_pending_name
|
||||||
}
|
: "");
|
||||||
if (term->search.whole_word) {
|
label = label_buf;
|
||||||
status[st_len++] = U'W';
|
break;
|
||||||
status[st_len++] = U'b';
|
case SEARCH_MODE_SESSION_SAVE:
|
||||||
status[st_len++] = U' ';
|
label = "Save State As: ";
|
||||||
}
|
break;
|
||||||
if (term->search.regex) {
|
case SEARCH_MODE_SESSION_SAVE_SECURE_NAME:
|
||||||
status[st_len++] = U'.';
|
label = "Secure Save As: ";
|
||||||
status[st_len++] = U'*';
|
break;
|
||||||
status[st_len++] = U' ';
|
case SEARCH_MODE_SESSION_SAVE_SECURE_PASSWORD:
|
||||||
}
|
label = "Password (new): ";
|
||||||
if (term->search.wrapped) {
|
break;
|
||||||
status[st_len++] = U'~';
|
case SEARCH_MODE_SESSION_LOAD:
|
||||||
status[st_len++] = U' ';
|
label = "Load State: ";
|
||||||
}
|
break;
|
||||||
if (term->search.len > 0 && term->search.total_count > 0) {
|
case SEARCH_MODE_SESSION_LOAD_PASSWORD:
|
||||||
char tmp[32];
|
label = "Decrypt Password: ";
|
||||||
snprintf(tmp, sizeof(tmp), "%zu/%zu", term->search.current_idx,
|
break;
|
||||||
term->search.total_count);
|
default:
|
||||||
for (size_t i = 0; tmp[i] != '\0' && st_len < ALEN(status) - 1; i++)
|
label = "";
|
||||||
status[st_len++] = (char32_t)tmp[i];
|
break;
|
||||||
status[st_len++] = U' ';
|
}
|
||||||
|
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';
|
status[st_len] = U'\0';
|
||||||
const size_t status_cells = c32swidth(status, st_len);
|
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;
|
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 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));
|
width = roundf(scale * ceilf(width / scale));
|
||||||
height = roundf(scale * ceilf(height / 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 bg_hex = term->colors.bg;
|
||||||
uint32_t fg_hex = term->colors.fg;
|
uint32_t fg_hex = term->colors.fg;
|
||||||
|
|
||||||
if (term->search.len > 0 && term->search.match_len == 0) {
|
if (term->search.mode == SEARCH_MODE_NORMAL) {
|
||||||
bg_hex = term->colors.table[1];
|
if (term->search.len > 0 && term->search.match_len == 0) {
|
||||||
fg_hex = term->colors.table[0];
|
bg_hex = term->colors.table[1];
|
||||||
} else if (term->search.wrapped) {
|
fg_hex = term->colors.table[0];
|
||||||
bg_hex = term->colors.table[3];
|
} else if (term->search.wrapped) {
|
||||||
fg_hex = term->colors.table[0];
|
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);
|
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);
|
quirk_weston_subsurface_desync_on(term->window->search.sub);
|
||||||
|
|
||||||
/* TODO: this is only necessary on a window resize */
|
/* 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);
|
wl_surface_commit(term->window->search.surface.surf);
|
||||||
quirk_weston_subsurface_desync_off(term->window->search.sub);
|
quirk_weston_subsurface_desync_off(term->window->search.sub);
|
||||||
|
|
||||||
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
|
free(ime_alloc);
|
||||||
free(text);
|
free(masked_text);
|
||||||
#endif
|
|
||||||
#undef WINDOW_X
|
#undef WINDOW_X
|
||||||
#undef WINDOW_Y
|
#undef WINDOW_Y
|
||||||
}
|
}
|
||||||
|
|
|
||||||
63
search.c
63
search.c
|
|
@ -20,6 +20,7 @@
|
||||||
#include "quirks.h"
|
#include "quirks.h"
|
||||||
#include "render.h"
|
#include "render.h"
|
||||||
#include "selection.h"
|
#include "selection.h"
|
||||||
|
#include "session.h"
|
||||||
#include "shm.h"
|
#include "shm.h"
|
||||||
#include "unicode-mode.h"
|
#include "unicode-mode.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
|
@ -155,6 +156,9 @@ static void search_cancel_keep_selection(struct terminal *term) {
|
||||||
term->search.wrapped = false;
|
term->search.wrapped = false;
|
||||||
term->search.history_pos = NULL;
|
term->search.history_pos = NULL;
|
||||||
term->is_searching = false;
|
term->is_searching = false;
|
||||||
|
if (term->search.mode == SEARCH_MODE_SESSION_LOAD)
|
||||||
|
session_picker_free(term);
|
||||||
|
term->search.mode = SEARCH_MODE_NORMAL;
|
||||||
term->render.search_glyph_offset = 0;
|
term->render.search_glyph_offset = 0;
|
||||||
|
|
||||||
/* Reset IME state */
|
/* Reset IME state */
|
||||||
|
|
@ -193,11 +197,20 @@ void search_begin(struct terminal *term) {
|
||||||
term->search.sz = 64;
|
term->search.sz = 64;
|
||||||
term->search.buf = xmalloc(term->search.sz * sizeof(term->search.buf[0]));
|
term->search.buf = xmalloc(term->search.sz * sizeof(term->search.buf[0]));
|
||||||
term->search.buf[0] = U'\0';
|
term->search.buf[0] = U'\0';
|
||||||
|
term->search.mode = SEARCH_MODE_NORMAL;
|
||||||
|
|
||||||
term_xcursor_update(term);
|
term_xcursor_update(term);
|
||||||
render_refresh_search(term);
|
render_refresh_search(term);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void search_begin_session(struct terminal *term, enum search_mode mode) {
|
||||||
|
search_begin(term);
|
||||||
|
term->search.mode = mode;
|
||||||
|
if (mode == SEARCH_MODE_SESSION_LOAD)
|
||||||
|
session_picker_init(term);
|
||||||
|
render_refresh_search(term);
|
||||||
|
}
|
||||||
|
|
||||||
void search_cancel(struct terminal *term) {
|
void search_cancel(struct terminal *term) {
|
||||||
if (!term->is_searching)
|
if (!term->is_searching)
|
||||||
return;
|
return;
|
||||||
|
|
@ -1539,6 +1552,10 @@ static bool execute_binding(struct seat *seat, struct terminal *term,
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case BIND_ACTION_SEARCH_COMMIT:
|
case BIND_ACTION_SEARCH_COMMIT:
|
||||||
|
if (term->search.mode != SEARCH_MODE_NORMAL) {
|
||||||
|
session_prompt_commit(term);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
selection_finalize(seat, term, serial);
|
selection_finalize(seat, term, serial);
|
||||||
search_cancel_keep_selection(term);
|
search_cancel_keep_selection(term);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -1881,6 +1898,48 @@ void search_input(struct seat *seat, struct terminal *term,
|
||||||
LOG_DBG("search: input: sym=%d/0x%x, mods=0x%08x, consumed=0x%08x", sym, sym,
|
LOG_DBG("search: input: sym=%d/0x%x, mods=0x%08x, consumed=0x%08x", sym, sym,
|
||||||
mods, consumed);
|
mods, consumed);
|
||||||
|
|
||||||
|
/* Overwrite-confirm: only y/Y proceeds; anything else cancels. */
|
||||||
|
if (term->search.mode == SEARCH_MODE_SESSION_OVERWRITE_CONFIRM) {
|
||||||
|
if (sym == XKB_KEY_y || sym == XKB_KEY_Y || sym == XKB_KEY_Return ||
|
||||||
|
sym == XKB_KEY_KP_Enter) {
|
||||||
|
session_prompt_confirm_overwrite(term);
|
||||||
|
} else {
|
||||||
|
session_prompt_cancel(term);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* In session-load mode, hijack a few keys for picker navigation. We do this
|
||||||
|
* before binding dispatch so configured search-bindings (e.g. history-prev
|
||||||
|
* on Up) don't fire. */
|
||||||
|
if (term->search.mode == SEARCH_MODE_SESSION_LOAD) {
|
||||||
|
bool handled = true;
|
||||||
|
switch (sym) {
|
||||||
|
case XKB_KEY_Up:
|
||||||
|
session_picker_move(term, -1);
|
||||||
|
break;
|
||||||
|
case XKB_KEY_Down:
|
||||||
|
session_picker_move(term, +1);
|
||||||
|
break;
|
||||||
|
case XKB_KEY_Page_Up:
|
||||||
|
session_picker_move(term, -5);
|
||||||
|
break;
|
||||||
|
case XKB_KEY_Page_Down:
|
||||||
|
session_picker_move(term, +5);
|
||||||
|
break;
|
||||||
|
case XKB_KEY_Delete:
|
||||||
|
session_picker_delete_selected(term);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
handled = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (handled) {
|
||||||
|
render_refresh_search(term);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum xkb_compose_status compose_status =
|
enum xkb_compose_status compose_status =
|
||||||
seat->kbd.xkb_compose_state != NULL
|
seat->kbd.xkb_compose_state != NULL
|
||||||
? xkb_compose_state_get_status(seat->kbd.xkb_compose_state)
|
? xkb_compose_state_get_status(seat->kbd.xkb_compose_state)
|
||||||
|
|
@ -1969,8 +2028,10 @@ void search_input(struct seat *seat, struct terminal *term,
|
||||||
|
|
||||||
update_search:
|
update_search:
|
||||||
LOG_DBG("search: buffer: %ls", (const wchar_t *)term->search.buf);
|
LOG_DBG("search: buffer: %ls", (const wchar_t *)term->search.buf);
|
||||||
if (update_search_result)
|
if (update_search_result && term->search.mode == SEARCH_MODE_NORMAL)
|
||||||
search_find_next(term, search_direction);
|
search_find_next(term, search_direction);
|
||||||
|
if (term->search.mode == SEARCH_MODE_SESSION_LOAD)
|
||||||
|
session_picker_refilter(term);
|
||||||
if (redraw)
|
if (redraw)
|
||||||
render_refresh_search(term);
|
render_refresh_search(term);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
search.h
1
search.h
|
|
@ -6,6 +6,7 @@
|
||||||
#include "terminal.h"
|
#include "terminal.h"
|
||||||
|
|
||||||
void search_begin(struct terminal *term);
|
void search_begin(struct terminal *term);
|
||||||
|
void search_begin_session(struct terminal *term, enum search_mode mode);
|
||||||
void search_cancel(struct terminal *term);
|
void search_cancel(struct terminal *term);
|
||||||
void search_input(
|
void search_input(
|
||||||
struct seat *seat, struct terminal *term,
|
struct seat *seat, struct terminal *term,
|
||||||
|
|
|
||||||
160
session-crypto.c
Normal file
160
session-crypto.c
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
#include "session-crypto.h"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <sodium.h>
|
||||||
|
|
||||||
|
#define LOG_MODULE "session-crypto"
|
||||||
|
#define LOG_ENABLE_DBG 0
|
||||||
|
#include "log.h"
|
||||||
|
#include "xmalloc.h"
|
||||||
|
|
||||||
|
#define MAGIC "FOOT-ENC1\0"
|
||||||
|
#define MAGIC_LEN 10
|
||||||
|
|
||||||
|
static bool sodium_ready = false;
|
||||||
|
|
||||||
|
bool
|
||||||
|
session_crypto_init(void)
|
||||||
|
{
|
||||||
|
if (sodium_ready)
|
||||||
|
return true;
|
||||||
|
if (sodium_init() < 0) {
|
||||||
|
LOG_ERR("libsodium init failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
sodium_ready = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
derive_key(const char *password, const unsigned char *salt,
|
||||||
|
unsigned char key[crypto_aead_xchacha20poly1305_ietf_KEYBYTES])
|
||||||
|
{
|
||||||
|
return crypto_pwhash(
|
||||||
|
key, crypto_aead_xchacha20poly1305_ietf_KEYBYTES,
|
||||||
|
password, strlen(password), salt,
|
||||||
|
crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
||||||
|
crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
||||||
|
crypto_pwhash_ALG_ARGON2ID13) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
session_crypto_encrypt(const char *password,
|
||||||
|
const unsigned char *plaintext, size_t plaintext_len,
|
||||||
|
unsigned char **out, size_t *out_len)
|
||||||
|
{
|
||||||
|
if (!session_crypto_init())
|
||||||
|
return false;
|
||||||
|
*out = NULL;
|
||||||
|
*out_len = 0;
|
||||||
|
|
||||||
|
const size_t salt_len = crypto_pwhash_SALTBYTES;
|
||||||
|
const size_t nonce_len = crypto_aead_xchacha20poly1305_ietf_NPUBBYTES;
|
||||||
|
const size_t tag_len = crypto_aead_xchacha20poly1305_ietf_ABYTES;
|
||||||
|
|
||||||
|
const size_t total = MAGIC_LEN + salt_len + nonce_len + plaintext_len + tag_len;
|
||||||
|
unsigned char *buf = xmalloc(total);
|
||||||
|
memcpy(buf, MAGIC, MAGIC_LEN);
|
||||||
|
|
||||||
|
unsigned char *salt = buf + MAGIC_LEN;
|
||||||
|
unsigned char *nonce = salt + salt_len;
|
||||||
|
unsigned char *cipher = nonce + nonce_len;
|
||||||
|
|
||||||
|
randombytes_buf(salt, salt_len);
|
||||||
|
randombytes_buf(nonce, nonce_len);
|
||||||
|
|
||||||
|
unsigned char key[crypto_aead_xchacha20poly1305_ietf_KEYBYTES];
|
||||||
|
if (!derive_key(password, salt, key)) {
|
||||||
|
LOG_ERR("crypto_pwhash failed (out of memory?)");
|
||||||
|
sodium_memzero(key, sizeof(key));
|
||||||
|
free(buf);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bind the file header into the AEAD so any tampering breaks decryption. */
|
||||||
|
unsigned long long clen = 0;
|
||||||
|
int rc = crypto_aead_xchacha20poly1305_ietf_encrypt(
|
||||||
|
cipher, &clen,
|
||||||
|
plaintext, plaintext_len,
|
||||||
|
buf, MAGIC_LEN + salt_len + nonce_len, /* AAD = full header */
|
||||||
|
NULL,
|
||||||
|
nonce,
|
||||||
|
key);
|
||||||
|
sodium_memzero(key, sizeof(key));
|
||||||
|
|
||||||
|
if (rc != 0) {
|
||||||
|
LOG_ERR("xchacha20poly1305 encrypt failed");
|
||||||
|
free(buf);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/* Sanity */
|
||||||
|
if (clen != plaintext_len + tag_len) {
|
||||||
|
free(buf);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*out = buf;
|
||||||
|
*out_len = total;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
session_crypto_decrypt(const char *password,
|
||||||
|
const unsigned char *blob, size_t blob_len,
|
||||||
|
unsigned char **out, size_t *out_len)
|
||||||
|
{
|
||||||
|
if (!session_crypto_init())
|
||||||
|
return false;
|
||||||
|
*out = NULL;
|
||||||
|
*out_len = 0;
|
||||||
|
|
||||||
|
const size_t salt_len = crypto_pwhash_SALTBYTES;
|
||||||
|
const size_t nonce_len = crypto_aead_xchacha20poly1305_ietf_NPUBBYTES;
|
||||||
|
const size_t tag_len = crypto_aead_xchacha20poly1305_ietf_ABYTES;
|
||||||
|
const size_t header_len = MAGIC_LEN + salt_len + nonce_len;
|
||||||
|
|
||||||
|
if (blob_len < header_len + tag_len) {
|
||||||
|
LOG_ERR("encrypted blob too small (%zu bytes)", blob_len);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (memcmp(blob, MAGIC, MAGIC_LEN) != 0) {
|
||||||
|
LOG_ERR("bad magic in encrypted scrollback");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsigned char *salt = blob + MAGIC_LEN;
|
||||||
|
const unsigned char *nonce = salt + salt_len;
|
||||||
|
const unsigned char *cipher = nonce + nonce_len;
|
||||||
|
const size_t cipher_len = blob_len - header_len;
|
||||||
|
|
||||||
|
unsigned char key[crypto_aead_xchacha20poly1305_ietf_KEYBYTES];
|
||||||
|
if (!derive_key(password, salt, key)) {
|
||||||
|
LOG_ERR("crypto_pwhash failed during decrypt");
|
||||||
|
sodium_memzero(key, sizeof(key));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned char *plain = xmalloc(cipher_len); /* upper bound */
|
||||||
|
unsigned long long plen = 0;
|
||||||
|
int rc = crypto_aead_xchacha20poly1305_ietf_decrypt(
|
||||||
|
plain, &plen,
|
||||||
|
NULL,
|
||||||
|
cipher, cipher_len,
|
||||||
|
blob, header_len, /* AAD must match what was used in encrypt */
|
||||||
|
nonce,
|
||||||
|
key);
|
||||||
|
sodium_memzero(key, sizeof(key));
|
||||||
|
|
||||||
|
if (rc != 0) {
|
||||||
|
LOG_ERR("decryption failed (wrong password or corrupted file)");
|
||||||
|
sodium_memzero(plain, cipher_len);
|
||||||
|
free(plain);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*out = plain;
|
||||||
|
*out_len = (size_t)plen;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
39
session-crypto.h
Normal file
39
session-crypto.h
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Encrypt and decrypt session scrollback with a password.
|
||||||
|
*
|
||||||
|
* File format (binary):
|
||||||
|
* [magic : 10 bytes "FOOT-ENC1\0"]
|
||||||
|
* [salt : crypto_pwhash_SALTBYTES (16)]
|
||||||
|
* [nonce : crypto_aead_xchacha20poly1305_ietf_NPUBBYTES (24)]
|
||||||
|
* [ciphertext : plaintext_len + crypto_aead_xchacha20poly1305_ietf_ABYTES (16)]
|
||||||
|
*
|
||||||
|
* KDF: argon2id at INTERACTIVE ops/mem limits (≈100ms on modern hardware).
|
||||||
|
* AEAD: XChaCha20-Poly1305 (authenticates the magic+salt+nonce as AAD).
|
||||||
|
*/
|
||||||
|
|
||||||
|
bool session_crypto_init(void);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Encrypts plaintext with the given password. Allocates and returns a buffer
|
||||||
|
* in *out (caller frees); *out_len receives its size. Returns true on success.
|
||||||
|
*/
|
||||||
|
bool session_crypto_encrypt(
|
||||||
|
const char *password,
|
||||||
|
const unsigned char *plaintext, size_t plaintext_len,
|
||||||
|
unsigned char **out, size_t *out_len);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Decrypts an encrypted blob (as produced by session_crypto_encrypt) with
|
||||||
|
* the given password. Allocates *out (caller frees) on success. Returns
|
||||||
|
* false on bad magic, truncated file, or authentication failure (wrong
|
||||||
|
* password / corrupted file).
|
||||||
|
*/
|
||||||
|
bool session_crypto_decrypt(
|
||||||
|
const char *password,
|
||||||
|
const unsigned char *blob, size_t blob_len,
|
||||||
|
unsigned char **out, size_t *out_len);
|
||||||
460
session-prompt.c
Normal file
460
session-prompt.c
Normal file
|
|
@ -0,0 +1,460 @@
|
||||||
|
#include "session.h"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <sodium.h>
|
||||||
|
|
||||||
|
#define LOG_MODULE "session-prompt"
|
||||||
|
#define LOG_ENABLE_DBG 0
|
||||||
|
#include "char32.h"
|
||||||
|
#include "extract.h"
|
||||||
|
#include "grid.h"
|
||||||
|
#include "log.h"
|
||||||
|
#include "render.h"
|
||||||
|
#include "search.h"
|
||||||
|
#include "session-crypto.h"
|
||||||
|
#include "terminal.h"
|
||||||
|
#include "vt.h"
|
||||||
|
#include "xmalloc.h"
|
||||||
|
|
||||||
|
#define SCROLLBACK_MAX_BYTES (1u << 20) /* 1 MB */
|
||||||
|
|
||||||
|
static char *
|
||||||
|
buf_to_utf8(const struct terminal *term)
|
||||||
|
{
|
||||||
|
if (term->search.len == 0)
|
||||||
|
return NULL;
|
||||||
|
/* Make a NUL-terminated char32_t copy for ac32tombs */
|
||||||
|
char32_t *tmp = calloc(term->search.len + 1, sizeof(char32_t));
|
||||||
|
if (tmp == NULL)
|
||||||
|
return NULL;
|
||||||
|
memcpy(tmp, term->search.buf, term->search.len * sizeof(char32_t));
|
||||||
|
tmp[term->search.len] = U'\0';
|
||||||
|
char *utf8 = ac32tombs(tmp);
|
||||||
|
free(tmp);
|
||||||
|
return utf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
session_picker_init(struct terminal *term)
|
||||||
|
{
|
||||||
|
session_picker_free(term);
|
||||||
|
term->session_picker.all = session_list(&term->session_picker.all_count);
|
||||||
|
term->session_picker.filtered_cap = term->session_picker.all_count;
|
||||||
|
term->session_picker.filtered = term->session_picker.filtered_cap > 0
|
||||||
|
? xmalloc(sizeof(size_t) * term->session_picker.filtered_cap)
|
||||||
|
: NULL;
|
||||||
|
session_picker_refilter(term);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
session_picker_free(struct terminal *term)
|
||||||
|
{
|
||||||
|
session_free_names(term->session_picker.all,
|
||||||
|
term->session_picker.all_count);
|
||||||
|
free(term->session_picker.filtered);
|
||||||
|
term->session_picker.all = NULL;
|
||||||
|
term->session_picker.all_count = 0;
|
||||||
|
term->session_picker.filtered = NULL;
|
||||||
|
term->session_picker.filtered_count = 0;
|
||||||
|
term->session_picker.filtered_cap = 0;
|
||||||
|
term->session_picker.sel = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
session_picker_refilter(struct terminal *term)
|
||||||
|
{
|
||||||
|
char *filter = buf_to_utf8(term);
|
||||||
|
term->session_picker.filtered_count = 0;
|
||||||
|
for (size_t i = 0; i < term->session_picker.all_count; i++) {
|
||||||
|
const char *name = term->session_picker.all[i];
|
||||||
|
if (filter == NULL || strstr(name, filter) != NULL)
|
||||||
|
term->session_picker.filtered[term->session_picker.filtered_count++] = i;
|
||||||
|
}
|
||||||
|
free(filter);
|
||||||
|
if (term->session_picker.sel >= term->session_picker.filtered_count)
|
||||||
|
term->session_picker.sel = term->session_picker.filtered_count > 0
|
||||||
|
? term->session_picker.filtered_count - 1
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
session_picker_move(struct terminal *term, int delta)
|
||||||
|
{
|
||||||
|
if (term->session_picker.filtered_count == 0)
|
||||||
|
return;
|
||||||
|
long n = (long)term->session_picker.filtered_count;
|
||||||
|
long cur = (long)term->session_picker.sel + delta;
|
||||||
|
/* Wrap */
|
||||||
|
cur = ((cur % n) + n) % n;
|
||||||
|
term->session_picker.sel = (size_t)cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
session_picker_delete_selected(struct terminal *term)
|
||||||
|
{
|
||||||
|
if (term->session_picker.filtered_count == 0)
|
||||||
|
return false;
|
||||||
|
size_t idx = term->session_picker.filtered[term->session_picker.sel];
|
||||||
|
const char *name = term->session_picker.all[idx];
|
||||||
|
if (!session_delete(name))
|
||||||
|
return false;
|
||||||
|
/* Rebuild the list */
|
||||||
|
session_picker_init(term);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
dismiss(struct terminal *term)
|
||||||
|
{
|
||||||
|
session_picker_free(term);
|
||||||
|
if (term->session_pending_name != NULL)
|
||||||
|
sodium_memzero(term->session_pending_name,
|
||||||
|
strlen(term->session_pending_name));
|
||||||
|
free(term->session_pending_name);
|
||||||
|
term->session_pending_name = NULL;
|
||||||
|
term->session_pending_secure = false;
|
||||||
|
if (term->session_pending_load_state != NULL) {
|
||||||
|
struct session_state *st = term->session_pending_load_state;
|
||||||
|
session_state_free(st);
|
||||||
|
free(st);
|
||||||
|
term->session_pending_load_state = NULL;
|
||||||
|
}
|
||||||
|
/* Zero any residual password bytes in the input buffer */
|
||||||
|
if (term->search.buf != NULL && term->search.len > 0)
|
||||||
|
sodium_memzero(term->search.buf,
|
||||||
|
term->search.len * sizeof(term->search.buf[0]));
|
||||||
|
term->search.mode = SEARCH_MODE_NORMAL;
|
||||||
|
/* search_cancel clears the search bar; reuse it. We deliberately do not
|
||||||
|
* leave selection state behind (no commit semantics for sessions). */
|
||||||
|
search_cancel(term);
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *
|
||||||
|
extract_scrollback_text(struct terminal *term, size_t *out_len)
|
||||||
|
{
|
||||||
|
*out_len = 0;
|
||||||
|
struct grid *grid = &term->normal;
|
||||||
|
if (grid->rows == NULL)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
const int total = grid->num_rows;
|
||||||
|
const int visible = term->rows;
|
||||||
|
const int scrollback = total > visible ? total - visible : 0;
|
||||||
|
|
||||||
|
struct extraction_context *ctx = extract_begin(SELECTION_NONE, true);
|
||||||
|
if (ctx == NULL)
|
||||||
|
return NULL;
|
||||||
|
for (int row_no = -scrollback; row_no < visible; row_no++) {
|
||||||
|
int idx = ((grid->offset + row_no) % total + total) % total;
|
||||||
|
const struct row *row = grid->rows[idx];
|
||||||
|
if (row == NULL)
|
||||||
|
continue;
|
||||||
|
for (int col = 0; col < grid->num_cols; col++) {
|
||||||
|
if (!extract_one(term, row, &row->cells[col], col, ctx)) {
|
||||||
|
char *junk = NULL;
|
||||||
|
size_t jl = 0;
|
||||||
|
extract_finish(ctx, &junk, &jl);
|
||||||
|
free(junk);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
char *text = NULL;
|
||||||
|
size_t len = 0;
|
||||||
|
if (!extract_finish(ctx, &text, &len))
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
/* Trim leading newlines (uninitialized scrollback) */
|
||||||
|
size_t start = 0;
|
||||||
|
while (start < len && (text[start] == '\n' || text[start] == ' '))
|
||||||
|
start++;
|
||||||
|
if (start > 0) {
|
||||||
|
memmove(text, text + start, len - start + 1);
|
||||||
|
len -= start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cap at SCROLLBACK_MAX_BYTES, keeping the newest content. */
|
||||||
|
if (len > SCROLLBACK_MAX_BYTES) {
|
||||||
|
size_t drop = len - SCROLLBACK_MAX_BYTES;
|
||||||
|
const char *nl = memchr(text + drop, '\n', len - drop);
|
||||||
|
if (nl != NULL)
|
||||||
|
drop = (size_t)(nl + 1 - text);
|
||||||
|
memmove(text, text + drop, len - drop + 1);
|
||||||
|
len -= drop;
|
||||||
|
}
|
||||||
|
|
||||||
|
*out_len = len;
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void
|
||||||
|
do_save(struct terminal *term, const char *name)
|
||||||
|
{
|
||||||
|
struct session_state st = {0};
|
||||||
|
if (!session_capture(term->ptmx, term->cwd, &st)) {
|
||||||
|
LOG_ERR("session_save: could not capture state");
|
||||||
|
session_state_free(&st);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!session_save(name, &st))
|
||||||
|
LOG_ERR("session_save: write failed for '%s'", name);
|
||||||
|
session_state_free(&st);
|
||||||
|
|
||||||
|
/* Plain save deliberately drops any stale encrypted scrollback so the
|
||||||
|
* session reflects the current save. */
|
||||||
|
session_delete_enc_scrollback(name);
|
||||||
|
session_delete_scrollback(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
do_save_secure(struct terminal *term, const char *name, const char *password)
|
||||||
|
{
|
||||||
|
struct session_state st = {0};
|
||||||
|
if (!session_capture(term->ptmx, term->cwd, &st)) {
|
||||||
|
LOG_ERR("session_save_secure: could not capture state");
|
||||||
|
session_state_free(&st);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool ok = session_save(name, &st);
|
||||||
|
session_state_free(&st);
|
||||||
|
if (!ok) {
|
||||||
|
LOG_ERR("session_save_secure: JSON write failed for '%s'", name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* Capture scrollback as plain text, encrypt, write sidecar. */
|
||||||
|
size_t plain_len = 0;
|
||||||
|
char *plain = extract_scrollback_text(term, &plain_len);
|
||||||
|
if (plain == NULL || plain_len == 0) {
|
||||||
|
/* No scrollback to encrypt — clear any stale ciphertext. */
|
||||||
|
free(plain);
|
||||||
|
session_delete_enc_scrollback(name);
|
||||||
|
session_delete_scrollback(name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unsigned char *blob = NULL;
|
||||||
|
size_t blob_len = 0;
|
||||||
|
bool enc_ok = session_crypto_encrypt(
|
||||||
|
password, (const unsigned char *)plain, plain_len, &blob, &blob_len);
|
||||||
|
sodium_memzero(plain, plain_len);
|
||||||
|
free(plain);
|
||||||
|
if (!enc_ok) {
|
||||||
|
LOG_ERR("session_save_secure: encryption failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!session_write_enc_blob(name, blob, blob_len))
|
||||||
|
LOG_ERR("session_save_secure: sidecar write failed");
|
||||||
|
/* Remove any legacy plain sidecar so we never load it accidentally. */
|
||||||
|
session_delete_scrollback(name);
|
||||||
|
sodium_memzero(blob, blob_len);
|
||||||
|
free(blob);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Replay a saved scrollback into a freshly created terminal. Walks the bytes
|
||||||
|
* once to convert bare LF into CRLF (so each saved line lands at column 0),
|
||||||
|
* then runs them through vt_from_slave. Called from the LOAD path right after
|
||||||
|
* the new tab is spawned, before any output from the new slave has arrived.
|
||||||
|
*/
|
||||||
|
static void
|
||||||
|
replay_scrollback(struct terminal *new_term, const char *text, size_t len)
|
||||||
|
{
|
||||||
|
if (text == NULL || len == 0 || new_term == NULL)
|
||||||
|
return;
|
||||||
|
|
||||||
|
char *buf = xmalloc(len * 2);
|
||||||
|
size_t out = 0;
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
char c = text[i];
|
||||||
|
if (c == '\n')
|
||||||
|
buf[out++] = '\r';
|
||||||
|
buf[out++] = c;
|
||||||
|
}
|
||||||
|
if (out > 0)
|
||||||
|
buf[out++] = '\n';
|
||||||
|
|
||||||
|
/* Visually separate replayed content from the upcoming shell prompt */
|
||||||
|
/* (single trailing newline is enough; the shell will emit its own CRLF) */
|
||||||
|
|
||||||
|
vt_from_slave(new_term, (const uint8_t *)buf, out);
|
||||||
|
free(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
session_prompt_confirm_overwrite(struct terminal *term)
|
||||||
|
{
|
||||||
|
if (term->session_pending_name == NULL) {
|
||||||
|
dismiss(term);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (term->session_pending_secure) {
|
||||||
|
/* Don't save yet — transition to password prompt. */
|
||||||
|
term->search.mode = SEARCH_MODE_SESSION_SAVE_SECURE_PASSWORD;
|
||||||
|
term->search.len = 0;
|
||||||
|
term->search.cursor = 0;
|
||||||
|
if (term->search.buf != NULL)
|
||||||
|
term->search.buf[0] = U'\0';
|
||||||
|
render_refresh_search(term);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
do_save(term, term->session_pending_name);
|
||||||
|
dismiss(term);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
session_prompt_cancel(struct terminal *term)
|
||||||
|
{
|
||||||
|
dismiss(term);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
spawn_and_replay(struct terminal *term, struct session_state *st,
|
||||||
|
const char *name, const char *password)
|
||||||
|
{
|
||||||
|
struct terminal *nt = term_tab_new_with_cwd(
|
||||||
|
term, st->cwd, st->argc, st->argv, NULL,
|
||||||
|
term->shutdown.cb, term->shutdown.cb_data);
|
||||||
|
if (nt == NULL) {
|
||||||
|
LOG_ERR("session_load: could not spawn new tab");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* Encrypted sidecar takes priority if present. */
|
||||||
|
if (password != NULL && session_has_enc_scrollback(name)) {
|
||||||
|
unsigned char *blob = NULL;
|
||||||
|
size_t blob_len = 0;
|
||||||
|
if (session_read_enc_blob(name, &blob, &blob_len)) {
|
||||||
|
unsigned char *plain = NULL;
|
||||||
|
size_t plain_len = 0;
|
||||||
|
if (session_crypto_decrypt(password, blob, blob_len,
|
||||||
|
&plain, &plain_len)) {
|
||||||
|
replay_scrollback(nt, (const char *)plain, plain_len);
|
||||||
|
sodium_memzero(plain, plain_len);
|
||||||
|
free(plain);
|
||||||
|
} else {
|
||||||
|
LOG_ERR("session_load: decryption failed for '%s'", name);
|
||||||
|
}
|
||||||
|
free(blob);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* Plain sidecar (legacy or never-encrypted). */
|
||||||
|
char *sb_text = NULL;
|
||||||
|
size_t sb_len = 0;
|
||||||
|
if (session_load_scrollback(name, &sb_text, &sb_len))
|
||||||
|
replay_scrollback(nt, sb_text, sb_len);
|
||||||
|
free(sb_text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
session_prompt_commit(struct terminal *term)
|
||||||
|
{
|
||||||
|
enum search_mode mode = term->search.mode;
|
||||||
|
|
||||||
|
if (mode == SEARCH_MODE_SESSION_SAVE ||
|
||||||
|
mode == SEARCH_MODE_SESSION_SAVE_SECURE_NAME) {
|
||||||
|
const bool secure = (mode == SEARCH_MODE_SESSION_SAVE_SECURE_NAME);
|
||||||
|
char *name = buf_to_utf8(term);
|
||||||
|
if (name == NULL || !session_name_is_valid(name)) {
|
||||||
|
LOG_ERR("session name must be non-empty [A-Za-z0-9._-]");
|
||||||
|
free(name);
|
||||||
|
dismiss(term);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (session_exists(name)) {
|
||||||
|
free(term->session_pending_name);
|
||||||
|
term->session_pending_name = name;
|
||||||
|
term->session_pending_secure = secure;
|
||||||
|
term->search.mode = SEARCH_MODE_SESSION_OVERWRITE_CONFIRM;
|
||||||
|
render_refresh_search(term);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (secure) {
|
||||||
|
/* No overwrite needed, go straight to password prompt. */
|
||||||
|
free(term->session_pending_name);
|
||||||
|
term->session_pending_name = name;
|
||||||
|
term->session_pending_secure = true;
|
||||||
|
term->search.mode = SEARCH_MODE_SESSION_SAVE_SECURE_PASSWORD;
|
||||||
|
term->search.len = 0;
|
||||||
|
term->search.cursor = 0;
|
||||||
|
if (term->search.buf != NULL)
|
||||||
|
term->search.buf[0] = U'\0';
|
||||||
|
render_refresh_search(term);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
do_save(term, name);
|
||||||
|
free(name);
|
||||||
|
} else if (mode == SEARCH_MODE_SESSION_SAVE_SECURE_PASSWORD) {
|
||||||
|
char *password = buf_to_utf8(term);
|
||||||
|
if (password == NULL || password[0] == '\0') {
|
||||||
|
LOG_ERR("session_save_secure: empty password");
|
||||||
|
free(password);
|
||||||
|
dismiss(term);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const char *name = term->session_pending_name;
|
||||||
|
if (name == NULL) {
|
||||||
|
sodium_memzero(password, strlen(password));
|
||||||
|
free(password);
|
||||||
|
dismiss(term);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
do_save_secure(term, name, password);
|
||||||
|
sodium_memzero(password, strlen(password));
|
||||||
|
free(password);
|
||||||
|
} else if (mode == SEARCH_MODE_SESSION_LOAD) {
|
||||||
|
if (term->session_picker.filtered_count == 0) {
|
||||||
|
LOG_ERR("session_load: no matching session");
|
||||||
|
dismiss(term);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
size_t idx = term->session_picker.filtered[term->session_picker.sel];
|
||||||
|
const char *picked = term->session_picker.all[idx];
|
||||||
|
|
||||||
|
struct session_state *st = xmalloc(sizeof(*st));
|
||||||
|
memset(st, 0, sizeof(*st));
|
||||||
|
if (!session_load(picked, st)) {
|
||||||
|
LOG_ERR("session_load: could not read '%s'", picked);
|
||||||
|
free(st);
|
||||||
|
dismiss(term);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (session_has_enc_scrollback(picked)) {
|
||||||
|
/* Stash state, transition to password mode. */
|
||||||
|
free(term->session_pending_name);
|
||||||
|
term->session_pending_name = xstrdup(picked);
|
||||||
|
term->session_pending_load_state = st;
|
||||||
|
term->search.mode = SEARCH_MODE_SESSION_LOAD_PASSWORD;
|
||||||
|
term->search.len = 0;
|
||||||
|
term->search.cursor = 0;
|
||||||
|
if (term->search.buf != NULL)
|
||||||
|
term->search.buf[0] = U'\0';
|
||||||
|
session_picker_free(term);
|
||||||
|
render_refresh_search(term);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* No encrypted scrollback — spawn now. */
|
||||||
|
spawn_and_replay(term, st, picked, NULL);
|
||||||
|
session_state_free(st);
|
||||||
|
free(st);
|
||||||
|
} else if (mode == SEARCH_MODE_SESSION_LOAD_PASSWORD) {
|
||||||
|
char *password = buf_to_utf8(term);
|
||||||
|
struct session_state *st = term->session_pending_load_state;
|
||||||
|
const char *name = term->session_pending_name;
|
||||||
|
if (st != NULL && name != NULL && password != NULL) {
|
||||||
|
spawn_and_replay(term, st, name, password);
|
||||||
|
}
|
||||||
|
if (st != NULL) {
|
||||||
|
session_state_free(st);
|
||||||
|
free(st);
|
||||||
|
term->session_pending_load_state = NULL;
|
||||||
|
}
|
||||||
|
if (password != NULL) {
|
||||||
|
sodium_memzero(password, strlen(password));
|
||||||
|
free(password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss(term);
|
||||||
|
}
|
||||||
854
session.c
Normal file
854
session.c
Normal file
|
|
@ -0,0 +1,854 @@
|
||||||
|
#include "session.h"
|
||||||
|
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
#include <termios.h>
|
||||||
|
|
||||||
|
#define LOG_MODULE "session"
|
||||||
|
#define LOG_ENABLE_DBG 0
|
||||||
|
#include "log.h"
|
||||||
|
#include "xmalloc.h"
|
||||||
|
|
||||||
|
#define JSON_SUFFIX ".json"
|
||||||
|
#define JSON_SUFFIX_LEN 5
|
||||||
|
#define SCROLLBACK_SUFFIX ".scrollback.txt"
|
||||||
|
#define SCROLLBACK_ENC_SUFFIX ".scrollback.enc"
|
||||||
|
#define SCROLLBACK_MAX_BYTES (1u << 20) /* 1 MB cap */
|
||||||
|
|
||||||
|
static int
|
||||||
|
state_dir(char *buf, size_t buf_sz)
|
||||||
|
{
|
||||||
|
const char *xdg = getenv("XDG_DATA_HOME");
|
||||||
|
int n;
|
||||||
|
if (xdg != NULL && xdg[0] == '/')
|
||||||
|
n = snprintf(buf, buf_sz, "%s/foot/state", xdg);
|
||||||
|
else {
|
||||||
|
const char *home = getenv("HOME");
|
||||||
|
if (home == NULL)
|
||||||
|
return -1;
|
||||||
|
n = snprintf(buf, buf_sz, "%s/.local/share/foot/state", home);
|
||||||
|
}
|
||||||
|
if (n < 0 || (size_t)n >= buf_sz)
|
||||||
|
return -1;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
mkdir_p(const char *path)
|
||||||
|
{
|
||||||
|
char tmp[4096];
|
||||||
|
size_t len = strlen(path);
|
||||||
|
if (len >= sizeof(tmp))
|
||||||
|
return false;
|
||||||
|
memcpy(tmp, path, len + 1);
|
||||||
|
|
||||||
|
for (size_t i = 1; i < len; i++) {
|
||||||
|
if (tmp[i] == '/') {
|
||||||
|
tmp[i] = '\0';
|
||||||
|
if (mkdir(tmp, 0700) < 0 && errno != EEXIST) {
|
||||||
|
LOG_ERRNO("mkdir(%s)", tmp);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
tmp[i] = '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mkdir(tmp, 0700) < 0 && errno != EEXIST) {
|
||||||
|
LOG_ERRNO("mkdir(%s)", tmp);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
session_path(const char *name, char *buf, size_t buf_sz)
|
||||||
|
{
|
||||||
|
char dir[1024];
|
||||||
|
if (state_dir(dir, sizeof(dir)) < 0)
|
||||||
|
return -1;
|
||||||
|
int n = snprintf(buf, buf_sz, "%s/%s.json", dir, name);
|
||||||
|
if (n < 0 || (size_t)n >= buf_sz)
|
||||||
|
return -1;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
session_name_is_valid(const char *name)
|
||||||
|
{
|
||||||
|
if (name == NULL || name[0] == '\0')
|
||||||
|
return false;
|
||||||
|
for (const char *p = name; *p != '\0'; p++) {
|
||||||
|
char c = *p;
|
||||||
|
bool ok = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
||||||
|
(c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-';
|
||||||
|
if (!ok)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
session_state_free(struct session_state *st)
|
||||||
|
{
|
||||||
|
if (st == NULL)
|
||||||
|
return;
|
||||||
|
free(st->cwd);
|
||||||
|
if (st->argv != NULL) {
|
||||||
|
for (int i = 0; i < st->argc; i++)
|
||||||
|
free(st->argv[i]);
|
||||||
|
free(st->argv);
|
||||||
|
}
|
||||||
|
st->cwd = NULL;
|
||||||
|
st->argv = NULL;
|
||||||
|
st->argc = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(__linux__)
|
||||||
|
static char *
|
||||||
|
read_link(const char *path)
|
||||||
|
{
|
||||||
|
char *buf = NULL;
|
||||||
|
size_t cap = 256;
|
||||||
|
for (;;) {
|
||||||
|
buf = xrealloc(buf, cap);
|
||||||
|
ssize_t n = readlink(path, buf, cap - 1);
|
||||||
|
if (n < 0) {
|
||||||
|
free(buf);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if ((size_t)n < cap - 1) {
|
||||||
|
buf[n] = '\0';
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
cap *= 2;
|
||||||
|
if (cap > 1 << 16) {
|
||||||
|
free(buf);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
read_cmdline(pid_t pid, int *out_argc, char ***out_argv)
|
||||||
|
{
|
||||||
|
char path[64];
|
||||||
|
snprintf(path, sizeof(path), "/proc/%d/cmdline", (int)pid);
|
||||||
|
|
||||||
|
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
||||||
|
if (fd < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
char *buf = NULL;
|
||||||
|
size_t len = 0;
|
||||||
|
size_t cap = 0;
|
||||||
|
char chunk[1024];
|
||||||
|
for (;;) {
|
||||||
|
ssize_t n = read(fd, chunk, sizeof(chunk));
|
||||||
|
if (n < 0) {
|
||||||
|
if (errno == EINTR)
|
||||||
|
continue;
|
||||||
|
close(fd);
|
||||||
|
free(buf);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (n == 0)
|
||||||
|
break;
|
||||||
|
if (len + (size_t)n + 1 > cap) {
|
||||||
|
cap = (cap == 0 ? 1024 : cap * 2);
|
||||||
|
while (len + (size_t)n + 1 > cap)
|
||||||
|
cap *= 2;
|
||||||
|
buf = xrealloc(buf, cap);
|
||||||
|
}
|
||||||
|
memcpy(buf + len, chunk, n);
|
||||||
|
len += n;
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
|
||||||
|
if (buf == NULL || len == 0) {
|
||||||
|
free(buf);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/* /proc/<pid>/cmdline is NUL-separated; ensure trailing NUL */
|
||||||
|
if (buf[len - 1] != '\0') {
|
||||||
|
buf = xrealloc(buf, len + 1);
|
||||||
|
buf[len] = '\0';
|
||||||
|
len++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Count args */
|
||||||
|
int argc = 0;
|
||||||
|
for (size_t i = 0; i < len; i++)
|
||||||
|
if (buf[i] == '\0' && (i == 0 || buf[i - 1] != '\0'))
|
||||||
|
argc++;
|
||||||
|
|
||||||
|
if (argc <= 0) {
|
||||||
|
free(buf);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char **argv = xmalloc(sizeof(char *) * (argc + 1));
|
||||||
|
int idx = 0;
|
||||||
|
size_t start = 0;
|
||||||
|
for (size_t i = 0; i < len && idx < argc; i++) {
|
||||||
|
if (buf[i] == '\0') {
|
||||||
|
argv[idx++] = xstrdup(buf + start);
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
argv[argc] = NULL;
|
||||||
|
free(buf);
|
||||||
|
|
||||||
|
*out_argc = argc;
|
||||||
|
*out_argv = argv;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool
|
||||||
|
session_capture(int ptmx, const char *fallback_cwd, struct session_state *out)
|
||||||
|
{
|
||||||
|
memset(out, 0, sizeof(*out));
|
||||||
|
|
||||||
|
#if defined(__linux__)
|
||||||
|
pid_t fg = tcgetpgrp(ptmx);
|
||||||
|
if (fg > 0) {
|
||||||
|
char path[64];
|
||||||
|
snprintf(path, sizeof(path), "/proc/%d/cwd", (int)fg);
|
||||||
|
char *cwd = read_link(path);
|
||||||
|
if (cwd != NULL)
|
||||||
|
out->cwd = cwd;
|
||||||
|
|
||||||
|
int argc = 0;
|
||||||
|
char **argv = NULL;
|
||||||
|
if (read_cmdline(fg, &argc, &argv)) {
|
||||||
|
/* If the foreground process is a shell (login or otherwise), don't
|
||||||
|
* try to resurrect it via argv — let the user's normal shell
|
||||||
|
* launch take over. We only restore non-shell processes. */
|
||||||
|
const char *base = argv[0];
|
||||||
|
const char *slash = strrchr(base, '/');
|
||||||
|
if (slash != NULL)
|
||||||
|
base = slash + 1;
|
||||||
|
const char *shells[] = {
|
||||||
|
"bash", "zsh", "fish", "sh", "dash", "ksh", "tcsh", "csh",
|
||||||
|
"-bash", "-zsh", "-fish", "-sh", "-dash"
|
||||||
|
};
|
||||||
|
bool is_shell = false;
|
||||||
|
for (size_t i = 0; i < sizeof(shells) / sizeof(shells[0]); i++) {
|
||||||
|
if (strcmp(base, shells[i]) == 0) {
|
||||||
|
is_shell = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (is_shell) {
|
||||||
|
for (int i = 0; i < argc; i++)
|
||||||
|
free(argv[i]);
|
||||||
|
free(argv);
|
||||||
|
} else {
|
||||||
|
out->argc = argc;
|
||||||
|
out->argv = argv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (out->cwd == NULL && fallback_cwd != NULL)
|
||||||
|
out->cwd = xstrdup(fallback_cwd);
|
||||||
|
|
||||||
|
return out->cwd != NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------- JSON encoding/decoding (minimal, hand-rolled) ------------- */
|
||||||
|
|
||||||
|
static void
|
||||||
|
json_escape(FILE *fp, const char *s)
|
||||||
|
{
|
||||||
|
fputc('"', fp);
|
||||||
|
for (const char *p = s; *p != '\0'; p++) {
|
||||||
|
unsigned char c = (unsigned char)*p;
|
||||||
|
switch (c) {
|
||||||
|
case '"': fputs("\\\"", fp); break;
|
||||||
|
case '\\': fputs("\\\\", fp); break;
|
||||||
|
case '\b': fputs("\\b", fp); break;
|
||||||
|
case '\f': fputs("\\f", fp); break;
|
||||||
|
case '\n': fputs("\\n", fp); break;
|
||||||
|
case '\r': fputs("\\r", fp); break;
|
||||||
|
case '\t': fputs("\\t", fp); break;
|
||||||
|
default:
|
||||||
|
if (c < 0x20)
|
||||||
|
fprintf(fp, "\\u%04x", c);
|
||||||
|
else
|
||||||
|
fputc(c, fp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fputc('"', fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
session_save(const char *name, const struct session_state *st)
|
||||||
|
{
|
||||||
|
if (!session_name_is_valid(name)) {
|
||||||
|
LOG_ERR("invalid session name: %s", name == NULL ? "(null)" : name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (st == NULL || st->cwd == NULL) {
|
||||||
|
LOG_ERR("session_save: missing cwd");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char dir[1024];
|
||||||
|
if (state_dir(dir, sizeof(dir)) < 0) {
|
||||||
|
LOG_ERR("could not resolve state directory");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!mkdir_p(dir))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
char path[1280];
|
||||||
|
if (session_path(name, path, sizeof(path)) < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
char tmp_path[1408];
|
||||||
|
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path);
|
||||||
|
|
||||||
|
FILE *fp = fopen(tmp_path, "we");
|
||||||
|
if (fp == NULL) {
|
||||||
|
LOG_ERRNO("fopen(%s)", tmp_path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fputs("{\n \"cwd\": ", fp);
|
||||||
|
json_escape(fp, st->cwd);
|
||||||
|
if (st->argc > 0 && st->argv != NULL) {
|
||||||
|
fputs(",\n \"argv\": [", fp);
|
||||||
|
for (int i = 0; i < st->argc; i++) {
|
||||||
|
if (i > 0)
|
||||||
|
fputs(", ", fp);
|
||||||
|
json_escape(fp, st->argv[i]);
|
||||||
|
}
|
||||||
|
fputc(']', fp);
|
||||||
|
}
|
||||||
|
fputs("\n}\n", fp);
|
||||||
|
|
||||||
|
if (fflush(fp) != 0 || fsync(fileno(fp)) != 0) {
|
||||||
|
LOG_ERRNO("fsync(%s)", tmp_path);
|
||||||
|
fclose(fp);
|
||||||
|
unlink(tmp_path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
fclose(fp);
|
||||||
|
|
||||||
|
if (rename(tmp_path, path) < 0) {
|
||||||
|
LOG_ERRNO("rename(%s -> %s)", tmp_path, path);
|
||||||
|
unlink(tmp_path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("session saved: %s", path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------- JSON parsing (just enough for our schema) ------------- */
|
||||||
|
|
||||||
|
struct parser {
|
||||||
|
const char *p;
|
||||||
|
const char *end;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void
|
||||||
|
skip_ws(struct parser *pa)
|
||||||
|
{
|
||||||
|
while (pa->p < pa->end) {
|
||||||
|
char c = *pa->p;
|
||||||
|
if (c == ' ' || c == '\t' || c == '\n' || c == '\r')
|
||||||
|
pa->p++;
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
parse_string(struct parser *pa, char **out)
|
||||||
|
{
|
||||||
|
skip_ws(pa);
|
||||||
|
if (pa->p >= pa->end || *pa->p != '"')
|
||||||
|
return false;
|
||||||
|
pa->p++;
|
||||||
|
|
||||||
|
size_t cap = 64;
|
||||||
|
size_t len = 0;
|
||||||
|
char *buf = xmalloc(cap);
|
||||||
|
|
||||||
|
while (pa->p < pa->end && *pa->p != '"') {
|
||||||
|
char c = *pa->p++;
|
||||||
|
char decoded;
|
||||||
|
if (c == '\\') {
|
||||||
|
if (pa->p >= pa->end) { free(buf); return false; }
|
||||||
|
char esc = *pa->p++;
|
||||||
|
switch (esc) {
|
||||||
|
case '"': decoded = '"'; break;
|
||||||
|
case '\\': decoded = '\\'; break;
|
||||||
|
case '/': decoded = '/'; break;
|
||||||
|
case 'b': decoded = '\b'; break;
|
||||||
|
case 'f': decoded = '\f'; break;
|
||||||
|
case 'n': decoded = '\n'; break;
|
||||||
|
case 'r': decoded = '\r'; break;
|
||||||
|
case 't': decoded = '\t'; break;
|
||||||
|
case 'u': {
|
||||||
|
if (pa->end - pa->p < 4) { free(buf); return false; }
|
||||||
|
unsigned val = 0;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
char h = pa->p[i];
|
||||||
|
val <<= 4;
|
||||||
|
if (h >= '0' && h <= '9') val |= h - '0';
|
||||||
|
else if (h >= 'a' && h <= 'f') val |= h - 'a' + 10;
|
||||||
|
else if (h >= 'A' && h <= 'F') val |= h - 'A' + 10;
|
||||||
|
else { free(buf); return false; }
|
||||||
|
}
|
||||||
|
pa->p += 4;
|
||||||
|
if (val < 0x80) {
|
||||||
|
decoded = (char)val;
|
||||||
|
} else {
|
||||||
|
/* UTF-8 encode codepoint (no surrogate-pair handling) */
|
||||||
|
char enc[4];
|
||||||
|
int n;
|
||||||
|
if (val < 0x800) {
|
||||||
|
enc[0] = (char)(0xc0 | (val >> 6));
|
||||||
|
enc[1] = (char)(0x80 | (val & 0x3f));
|
||||||
|
n = 2;
|
||||||
|
} else {
|
||||||
|
enc[0] = (char)(0xe0 | (val >> 12));
|
||||||
|
enc[1] = (char)(0x80 | ((val >> 6) & 0x3f));
|
||||||
|
enc[2] = (char)(0x80 | (val & 0x3f));
|
||||||
|
n = 3;
|
||||||
|
}
|
||||||
|
if (len + (size_t)n + 1 > cap) {
|
||||||
|
while (len + (size_t)n + 1 > cap) cap *= 2;
|
||||||
|
buf = xrealloc(buf, cap);
|
||||||
|
}
|
||||||
|
memcpy(buf + len, enc, n);
|
||||||
|
len += n;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: free(buf); return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
decoded = c;
|
||||||
|
}
|
||||||
|
if (len + 2 > cap) {
|
||||||
|
cap *= 2;
|
||||||
|
buf = xrealloc(buf, cap);
|
||||||
|
}
|
||||||
|
buf[len++] = decoded;
|
||||||
|
}
|
||||||
|
if (pa->p >= pa->end) { free(buf); return false; }
|
||||||
|
pa->p++; /* consume closing quote */
|
||||||
|
buf[len] = '\0';
|
||||||
|
*out = buf;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
expect_char(struct parser *pa, char c)
|
||||||
|
{
|
||||||
|
skip_ws(pa);
|
||||||
|
if (pa->p >= pa->end || *pa->p != c)
|
||||||
|
return false;
|
||||||
|
pa->p++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
session_load(const char *name, struct session_state *out)
|
||||||
|
{
|
||||||
|
memset(out, 0, sizeof(*out));
|
||||||
|
|
||||||
|
if (!session_name_is_valid(name)) {
|
||||||
|
LOG_ERR("invalid session name: %s", name == NULL ? "(null)" : name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char path[1280];
|
||||||
|
if (session_path(name, path, sizeof(path)) < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
||||||
|
if (fd < 0) {
|
||||||
|
LOG_ERRNO("open(%s)", path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
struct stat sb;
|
||||||
|
if (fstat(fd, &sb) < 0 || sb.st_size <= 0 || sb.st_size > (off_t)(1 << 20)) {
|
||||||
|
LOG_ERR("session file %s has unexpected size", path);
|
||||||
|
close(fd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
char *data = xmalloc(sb.st_size + 1);
|
||||||
|
ssize_t got = 0;
|
||||||
|
while (got < sb.st_size) {
|
||||||
|
ssize_t n = read(fd, data + got, sb.st_size - got);
|
||||||
|
if (n < 0) {
|
||||||
|
if (errno == EINTR) continue;
|
||||||
|
free(data);
|
||||||
|
close(fd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (n == 0) break;
|
||||||
|
got += n;
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
data[got] = '\0';
|
||||||
|
|
||||||
|
struct parser pa = { .p = data, .end = data + got };
|
||||||
|
bool ok = false;
|
||||||
|
|
||||||
|
if (!expect_char(&pa, '{')) goto done;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
skip_ws(&pa);
|
||||||
|
if (pa.p < pa.end && *pa.p == '}') { pa.p++; break; }
|
||||||
|
|
||||||
|
char *key = NULL;
|
||||||
|
if (!parse_string(&pa, &key)) goto done;
|
||||||
|
if (!expect_char(&pa, ':')) { free(key); goto done; }
|
||||||
|
|
||||||
|
if (strcmp(key, "cwd") == 0) {
|
||||||
|
free(key);
|
||||||
|
if (!parse_string(&pa, &out->cwd)) goto done;
|
||||||
|
} else if (strcmp(key, "argv") == 0) {
|
||||||
|
free(key);
|
||||||
|
if (!expect_char(&pa, '[')) goto done;
|
||||||
|
int cap = 4;
|
||||||
|
out->argv = xmalloc(sizeof(char *) * cap);
|
||||||
|
out->argc = 0;
|
||||||
|
for (;;) {
|
||||||
|
skip_ws(&pa);
|
||||||
|
if (pa.p < pa.end && *pa.p == ']') { pa.p++; break; }
|
||||||
|
if (out->argc + 1 >= cap) {
|
||||||
|
cap *= 2;
|
||||||
|
out->argv = xrealloc(out->argv, sizeof(char *) * cap);
|
||||||
|
}
|
||||||
|
if (!parse_string(&pa, &out->argv[out->argc])) goto done;
|
||||||
|
out->argc++;
|
||||||
|
skip_ws(&pa);
|
||||||
|
if (pa.p < pa.end && *pa.p == ',') { pa.p++; continue; }
|
||||||
|
}
|
||||||
|
out->argv[out->argc] = NULL;
|
||||||
|
} else {
|
||||||
|
free(key);
|
||||||
|
/* unknown key: skip a string value */
|
||||||
|
char *junk = NULL;
|
||||||
|
if (!parse_string(&pa, &junk)) goto done;
|
||||||
|
free(junk);
|
||||||
|
}
|
||||||
|
|
||||||
|
skip_ws(&pa);
|
||||||
|
if (pa.p < pa.end && *pa.p == ',') { pa.p++; continue; }
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = out->cwd != NULL;
|
||||||
|
|
||||||
|
done:
|
||||||
|
free(data);
|
||||||
|
if (!ok)
|
||||||
|
session_state_free(out);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------- listing ------------- */
|
||||||
|
|
||||||
|
static int
|
||||||
|
cmp_names(const void *a, const void *b)
|
||||||
|
{
|
||||||
|
return strcmp(*(const char *const *)a, *(const char *const *)b);
|
||||||
|
}
|
||||||
|
|
||||||
|
char **
|
||||||
|
session_list(size_t *out_count)
|
||||||
|
{
|
||||||
|
*out_count = 0;
|
||||||
|
|
||||||
|
char dir[1024];
|
||||||
|
if (state_dir(dir, sizeof(dir)) < 0)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
DIR *d = opendir(dir);
|
||||||
|
if (d == NULL)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
size_t cap = 8;
|
||||||
|
size_t count = 0;
|
||||||
|
char **names = xmalloc(sizeof(char *) * cap);
|
||||||
|
|
||||||
|
struct dirent *de;
|
||||||
|
while ((de = readdir(d)) != NULL) {
|
||||||
|
const char *n = de->d_name;
|
||||||
|
size_t nlen = strlen(n);
|
||||||
|
if (nlen <= JSON_SUFFIX_LEN)
|
||||||
|
continue;
|
||||||
|
if (strcmp(n + nlen - JSON_SUFFIX_LEN, JSON_SUFFIX) != 0)
|
||||||
|
continue;
|
||||||
|
char *stripped = xmalloc(nlen - JSON_SUFFIX_LEN + 1);
|
||||||
|
memcpy(stripped, n, nlen - JSON_SUFFIX_LEN);
|
||||||
|
stripped[nlen - JSON_SUFFIX_LEN] = '\0';
|
||||||
|
if (!session_name_is_valid(stripped)) {
|
||||||
|
free(stripped);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (count >= cap) {
|
||||||
|
cap *= 2;
|
||||||
|
names = xrealloc(names, sizeof(char *) * cap);
|
||||||
|
}
|
||||||
|
names[count++] = stripped;
|
||||||
|
}
|
||||||
|
closedir(d);
|
||||||
|
|
||||||
|
qsort(names, count, sizeof(char *), cmp_names);
|
||||||
|
|
||||||
|
*out_count = count;
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
session_exists(const char *name)
|
||||||
|
{
|
||||||
|
if (!session_name_is_valid(name))
|
||||||
|
return false;
|
||||||
|
char path[1280];
|
||||||
|
if (session_path(name, path, sizeof(path)) < 0)
|
||||||
|
return false;
|
||||||
|
return access(path, F_OK) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
session_delete(const char *name)
|
||||||
|
{
|
||||||
|
if (!session_name_is_valid(name))
|
||||||
|
return false;
|
||||||
|
char path[1280];
|
||||||
|
if (session_path(name, path, sizeof(path)) < 0)
|
||||||
|
return false;
|
||||||
|
if (unlink(path) < 0) {
|
||||||
|
LOG_ERRNO("unlink(%s)", path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
session_delete_scrollback(name);
|
||||||
|
session_delete_enc_scrollback(name);
|
||||||
|
LOG_INFO("session deleted: %s", path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
scrollback_path(const char *name, char *buf, size_t buf_sz)
|
||||||
|
{
|
||||||
|
char dir[1024];
|
||||||
|
if (state_dir(dir, sizeof(dir)) < 0)
|
||||||
|
return -1;
|
||||||
|
int n = snprintf(buf, buf_sz, "%s/%s%s", dir, name, SCROLLBACK_SUFFIX);
|
||||||
|
if (n < 0 || (size_t)n >= buf_sz)
|
||||||
|
return -1;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
session_load_scrollback(const char *name, char **out_text, size_t *out_len)
|
||||||
|
{
|
||||||
|
*out_text = NULL;
|
||||||
|
*out_len = 0;
|
||||||
|
if (!session_name_is_valid(name))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
char path[1280];
|
||||||
|
if (scrollback_path(name, path, sizeof(path)) < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
||||||
|
if (fd < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
struct stat sb;
|
||||||
|
if (fstat(fd, &sb) < 0 || sb.st_size < 0 ||
|
||||||
|
(size_t)sb.st_size > SCROLLBACK_MAX_BYTES * 4 /* generous on load */) {
|
||||||
|
close(fd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (sb.st_size == 0) {
|
||||||
|
close(fd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *data = xmalloc(sb.st_size + 1);
|
||||||
|
ssize_t got = 0;
|
||||||
|
while (got < sb.st_size) {
|
||||||
|
ssize_t n = read(fd, data + got, sb.st_size - got);
|
||||||
|
if (n < 0) {
|
||||||
|
if (errno == EINTR) continue;
|
||||||
|
free(data);
|
||||||
|
close(fd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (n == 0) break;
|
||||||
|
got += n;
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
data[got] = '\0';
|
||||||
|
|
||||||
|
*out_text = data;
|
||||||
|
*out_len = (size_t)got;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
session_delete_scrollback(const char *name)
|
||||||
|
{
|
||||||
|
if (!session_name_is_valid(name))
|
||||||
|
return;
|
||||||
|
char path[1280];
|
||||||
|
if (scrollback_path(name, path, sizeof(path)) < 0)
|
||||||
|
return;
|
||||||
|
unlink(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
enc_path(const char *name, char *buf, size_t buf_sz)
|
||||||
|
{
|
||||||
|
char dir[1024];
|
||||||
|
if (state_dir(dir, sizeof(dir)) < 0)
|
||||||
|
return -1;
|
||||||
|
int n = snprintf(buf, buf_sz, "%s/%s%s", dir, name, SCROLLBACK_ENC_SUFFIX);
|
||||||
|
if (n < 0 || (size_t)n >= buf_sz)
|
||||||
|
return -1;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
session_has_enc_scrollback(const char *name)
|
||||||
|
{
|
||||||
|
if (!session_name_is_valid(name))
|
||||||
|
return false;
|
||||||
|
char path[1280];
|
||||||
|
if (enc_path(name, path, sizeof(path)) < 0)
|
||||||
|
return false;
|
||||||
|
return access(path, F_OK) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
session_delete_enc_scrollback(const char *name)
|
||||||
|
{
|
||||||
|
if (!session_name_is_valid(name))
|
||||||
|
return;
|
||||||
|
char path[1280];
|
||||||
|
if (enc_path(name, path, sizeof(path)) < 0)
|
||||||
|
return;
|
||||||
|
unlink(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
session_write_enc_blob(const char *name, const unsigned char *blob, size_t len)
|
||||||
|
{
|
||||||
|
if (!session_name_is_valid(name))
|
||||||
|
return false;
|
||||||
|
char dir[1024];
|
||||||
|
if (state_dir(dir, sizeof(dir)) < 0)
|
||||||
|
return false;
|
||||||
|
if (!mkdir_p(dir))
|
||||||
|
return false;
|
||||||
|
char path[1280];
|
||||||
|
if (enc_path(name, path, sizeof(path)) < 0)
|
||||||
|
return false;
|
||||||
|
char tmp_path[1408];
|
||||||
|
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path);
|
||||||
|
|
||||||
|
/* 0600 — encrypted but still reduces accidental exposure. */
|
||||||
|
int fd = open(tmp_path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600);
|
||||||
|
if (fd < 0) {
|
||||||
|
LOG_ERRNO("open(%s)", tmp_path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
size_t written = 0;
|
||||||
|
while (written < len) {
|
||||||
|
ssize_t n = write(fd, blob + written, len - written);
|
||||||
|
if (n < 0) {
|
||||||
|
if (errno == EINTR) continue;
|
||||||
|
LOG_ERRNO("write(%s)", tmp_path);
|
||||||
|
close(fd);
|
||||||
|
unlink(tmp_path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
written += n;
|
||||||
|
}
|
||||||
|
if (fsync(fd) < 0) {
|
||||||
|
LOG_ERRNO("fsync(%s)", tmp_path);
|
||||||
|
close(fd);
|
||||||
|
unlink(tmp_path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
if (rename(tmp_path, path) < 0) {
|
||||||
|
LOG_ERRNO("rename(%s -> %s)", tmp_path, path);
|
||||||
|
unlink(tmp_path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
session_read_enc_blob(const char *name, unsigned char **out, size_t *out_len)
|
||||||
|
{
|
||||||
|
*out = NULL;
|
||||||
|
*out_len = 0;
|
||||||
|
if (!session_name_is_valid(name))
|
||||||
|
return false;
|
||||||
|
char path[1280];
|
||||||
|
if (enc_path(name, path, sizeof(path)) < 0)
|
||||||
|
return false;
|
||||||
|
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
||||||
|
if (fd < 0)
|
||||||
|
return false;
|
||||||
|
struct stat sb;
|
||||||
|
if (fstat(fd, &sb) < 0 || sb.st_size <= 0 ||
|
||||||
|
(size_t)sb.st_size > SCROLLBACK_MAX_BYTES * 4) {
|
||||||
|
close(fd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
unsigned char *buf = xmalloc(sb.st_size);
|
||||||
|
ssize_t got = 0;
|
||||||
|
while (got < sb.st_size) {
|
||||||
|
ssize_t n = read(fd, buf + got, sb.st_size - got);
|
||||||
|
if (n < 0) {
|
||||||
|
if (errno == EINTR) continue;
|
||||||
|
free(buf);
|
||||||
|
close(fd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (n == 0) break;
|
||||||
|
got += n;
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
if (got != sb.st_size) {
|
||||||
|
free(buf);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*out = buf;
|
||||||
|
*out_len = (size_t)got;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
session_free_names(char **names, size_t count)
|
||||||
|
{
|
||||||
|
if (names == NULL)
|
||||||
|
return;
|
||||||
|
for (size_t i = 0; i < count; i++)
|
||||||
|
free(names[i]);
|
||||||
|
free(names);
|
||||||
|
}
|
||||||
92
session.h
Normal file
92
session.h
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Persistent terminal session state.
|
||||||
|
*
|
||||||
|
* Sessions are stored as JSON files under $XDG_DATA_HOME/foot/state/<name>.json
|
||||||
|
* (defaulting to ~/.local/share/foot/state). Each file records the cwd of the
|
||||||
|
* foreground process on the pty plus its argv, so a session can be resurrected
|
||||||
|
* in a new tab.
|
||||||
|
*/
|
||||||
|
|
||||||
|
struct session_state {
|
||||||
|
char *cwd;
|
||||||
|
int argc;
|
||||||
|
char **argv; /* NULL-terminated for convenience; argc does not count the terminator */
|
||||||
|
};
|
||||||
|
|
||||||
|
void session_state_free(struct session_state *st);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inspect the pty for its foreground process. On Linux, walks /proc to grab
|
||||||
|
* cmdline + cwd. Falls back to fallback_cwd and a NULL argv on other platforms
|
||||||
|
* or when introspection fails (the caller will then save cwd only and let the
|
||||||
|
* shell start fresh on resume). Returns true if at least cwd is populated.
|
||||||
|
*/
|
||||||
|
bool session_capture(int ptmx, const char *fallback_cwd,
|
||||||
|
struct session_state *out);
|
||||||
|
|
||||||
|
/* Persist to <state-dir>/<name>.json. Creates the directory if missing. */
|
||||||
|
bool session_save(const char *name, const struct session_state *st);
|
||||||
|
|
||||||
|
/* Read <state-dir>/<name>.json. */
|
||||||
|
bool session_load(const char *name, struct session_state *out);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* List existing sessions. Returns a malloc'd array of malloc'd names
|
||||||
|
* (no .json suffix), sorted. Caller frees with session_free_names().
|
||||||
|
*/
|
||||||
|
char **session_list(size_t *out_count);
|
||||||
|
void session_free_names(char **names, size_t count);
|
||||||
|
|
||||||
|
/* Returns true if name is non-empty and contains only [A-Za-z0-9._-]. */
|
||||||
|
bool session_name_is_valid(const char *name);
|
||||||
|
|
||||||
|
/* Remove <state-dir>/<name>.json. Returns true on success. */
|
||||||
|
bool session_delete(const char *name);
|
||||||
|
|
||||||
|
/* True if <state-dir>/<name>.json exists. */
|
||||||
|
bool session_exists(const char *name);
|
||||||
|
|
||||||
|
struct terminal;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Read <state-dir>/<name>.scrollback.txt into out_text and out_len. Returns
|
||||||
|
* false (with out_text set to NULL) if the file is missing or fails sanity
|
||||||
|
* checks. Caller frees out_text.
|
||||||
|
*/
|
||||||
|
bool session_load_scrollback(const char *name, char **out_text, size_t *out_len);
|
||||||
|
|
||||||
|
/* Best-effort delete of the scrollback sidecar (used when removing a session). */
|
||||||
|
void session_delete_scrollback(const char *name);
|
||||||
|
|
||||||
|
/* Encrypted-sidecar I/O. <state-dir>/<name>.scrollback.enc */
|
||||||
|
bool session_write_enc_blob(const char *name,
|
||||||
|
const unsigned char *blob, size_t len);
|
||||||
|
bool session_read_enc_blob(const char *name,
|
||||||
|
unsigned char **out, size_t *out_len);
|
||||||
|
bool session_has_enc_scrollback(const char *name);
|
||||||
|
void session_delete_enc_scrollback(const char *name);
|
||||||
|
|
||||||
|
struct terminal;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Called from search.c when the user presses Enter while the search bar is
|
||||||
|
* in a session prompt mode. Reads the typed name from term->search.buf and
|
||||||
|
* performs the requested save or load, then dismisses the prompt.
|
||||||
|
*/
|
||||||
|
void session_prompt_commit(struct terminal *term);
|
||||||
|
void session_prompt_cancel(struct terminal *term);
|
||||||
|
|
||||||
|
/* Finalize a save after the user has confirmed overwrite (y). */
|
||||||
|
void session_prompt_confirm_overwrite(struct terminal *term);
|
||||||
|
|
||||||
|
/* Picker (session-load) helpers. */
|
||||||
|
void session_picker_init(struct terminal *term);
|
||||||
|
void session_picker_free(struct terminal *term);
|
||||||
|
void session_picker_refilter(struct terminal *term);
|
||||||
|
void session_picker_move(struct terminal *term, int delta); /* +1/-1 etc */
|
||||||
|
bool session_picker_delete_selected(struct terminal *term);
|
||||||
24
terminal.c
24
terminal.c
|
|
@ -1511,6 +1511,17 @@ struct terminal *term_tab_new(struct terminal *primary, int argc,
|
||||||
char *const *argv, const char *const *envp,
|
char *const *argv, const char *const *envp,
|
||||||
void (*shutdown_cb)(void *data, int exit_code),
|
void (*shutdown_cb)(void *data, int exit_code),
|
||||||
void *shutdown_data) {
|
void *shutdown_data) {
|
||||||
|
return term_tab_new_with_cwd(primary, NULL, argc, argv, envp, shutdown_cb,
|
||||||
|
shutdown_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct terminal *term_tab_new_with_cwd(struct terminal *primary,
|
||||||
|
const char *override_cwd, int argc,
|
||||||
|
char *const *argv,
|
||||||
|
const char *const *envp,
|
||||||
|
void (*shutdown_cb)(void *data,
|
||||||
|
int exit_code),
|
||||||
|
void *shutdown_data) {
|
||||||
struct wl_window *win = primary->window;
|
struct wl_window *win = primary->window;
|
||||||
|
|
||||||
if (win->tab_count >= TAB_MAX) {
|
if (win->tab_count >= TAB_MAX) {
|
||||||
|
|
@ -1725,9 +1736,12 @@ struct terminal *term_tab_new(struct terminal *primary, int argc,
|
||||||
.cb_data = shutdown_data,
|
.cb_data = shutdown_data,
|
||||||
},
|
},
|
||||||
.foot_exe = xstrdup(primary->foot_exe),
|
.foot_exe = xstrdup(primary->foot_exe),
|
||||||
.cwd = xstrdup(conf->tabs.inherit_cwd
|
.cwd = xstrdup(override_cwd != NULL
|
||||||
? win->term->cwd
|
? override_cwd
|
||||||
: (getenv("HOME") != NULL ? getenv("HOME") : "/")),
|
: (conf->tabs.inherit_cwd
|
||||||
|
? win->term->cwd
|
||||||
|
: (getenv("HOME") != NULL ? getenv("HOME")
|
||||||
|
: "/"))),
|
||||||
.grapheme_shaping = conf->tweak.grapheme_shaping,
|
.grapheme_shaping = conf->tweak.grapheme_shaping,
|
||||||
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
|
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
|
||||||
.ime_enabled = true,
|
.ime_enabled = true,
|
||||||
|
|
@ -1764,6 +1778,10 @@ struct terminal *term_tab_new(struct terminal *primary, int argc,
|
||||||
|
|
||||||
add_utmp_record(conf, reaper, ptmx);
|
add_utmp_record(conf, reaper, ptmx);
|
||||||
|
|
||||||
|
/* Initialize window title to the configured default; otherwise a freshly
|
||||||
|
* spawned child sending CSI 22 t (push title) sees a NULL title. */
|
||||||
|
term_set_window_title(term, conf->title);
|
||||||
|
|
||||||
if ((term->slave = slave_spawn(
|
if ((term->slave = slave_spawn(
|
||||||
term->ptmx, argc, term->cwd, argv, envp, &conf->env_vars, conf->term,
|
term->ptmx, argc, term->cwd, argv, envp, &conf->env_vars, conf->term,
|
||||||
conf->shell, conf->login_shell, &conf->notifications)) == -1) {
|
conf->shell, conf->login_shell, &conf->notifications)) == -1) {
|
||||||
|
|
|
||||||
37
terminal.h
37
terminal.h
|
|
@ -344,6 +344,15 @@ enum selection_direction {SELECTION_UNDIR, SELECTION_LEFT, SELECTION_RIGHT};
|
||||||
enum selection_scroll_direction {SELECTION_SCROLL_NOT, SELECTION_SCROLL_UP, SELECTION_SCROLL_DOWN};
|
enum selection_scroll_direction {SELECTION_SCROLL_NOT, SELECTION_SCROLL_UP, SELECTION_SCROLL_DOWN};
|
||||||
enum search_direction { SEARCH_BACKWARD_SAME_POSITION, SEARCH_BACKWARD, SEARCH_FORWARD };
|
enum search_direction { SEARCH_BACKWARD_SAME_POSITION, SEARCH_BACKWARD, SEARCH_FORWARD };
|
||||||
enum search_case_mode { SEARCH_CASE_SMART, SEARCH_CASE_SENSITIVE, SEARCH_CASE_INSENSITIVE };
|
enum search_case_mode { SEARCH_CASE_SMART, SEARCH_CASE_SENSITIVE, SEARCH_CASE_INSENSITIVE };
|
||||||
|
enum search_mode {
|
||||||
|
SEARCH_MODE_NORMAL,
|
||||||
|
SEARCH_MODE_SESSION_SAVE,
|
||||||
|
SEARCH_MODE_SESSION_LOAD,
|
||||||
|
SEARCH_MODE_SESSION_OVERWRITE_CONFIRM,
|
||||||
|
SEARCH_MODE_SESSION_SAVE_SECURE_NAME,
|
||||||
|
SEARCH_MODE_SESSION_SAVE_SECURE_PASSWORD,
|
||||||
|
SEARCH_MODE_SESSION_LOAD_PASSWORD,
|
||||||
|
};
|
||||||
|
|
||||||
struct ptmx_buffer {
|
struct ptmx_buffer {
|
||||||
void *data;
|
void *data;
|
||||||
|
|
@ -626,6 +635,7 @@ struct terminal {
|
||||||
size_t len;
|
size_t len;
|
||||||
size_t sz;
|
size_t sz;
|
||||||
size_t cursor;
|
size_t cursor;
|
||||||
|
enum search_mode mode;
|
||||||
|
|
||||||
int original_view;
|
int original_view;
|
||||||
bool view_followed_offset;
|
bool view_followed_offset;
|
||||||
|
|
@ -865,6 +875,24 @@ struct terminal {
|
||||||
char *foot_exe;
|
char *foot_exe;
|
||||||
char *cwd;
|
char *cwd;
|
||||||
|
|
||||||
|
/* Active only while term->search.mode == SEARCH_MODE_SESSION_LOAD */
|
||||||
|
struct {
|
||||||
|
char **all; /* All session names from disk */
|
||||||
|
size_t all_count;
|
||||||
|
size_t *filtered; /* indices into all[] matching current input */
|
||||||
|
size_t filtered_count;
|
||||||
|
size_t filtered_cap;
|
||||||
|
size_t sel; /* index within filtered[] */
|
||||||
|
} session_picker;
|
||||||
|
|
||||||
|
/* Name held across a multi-step session flow (overwrite confirm, secure
|
||||||
|
* save name->password, load password). */
|
||||||
|
char *session_pending_name;
|
||||||
|
bool session_pending_secure; /* set when overwrite-confirm precedes a secure save */
|
||||||
|
/* For LOAD_PASSWORD: heap-allocated session_state pulled from JSON. The
|
||||||
|
* new tab isn't spawned until decryption succeeds, so we hold it here. */
|
||||||
|
void *session_pending_load_state;
|
||||||
|
|
||||||
bool grapheme_shaping;
|
bool grapheme_shaping;
|
||||||
bool size_notifications;
|
bool size_notifications;
|
||||||
};
|
};
|
||||||
|
|
@ -882,6 +910,15 @@ struct terminal *term_tab_new(
|
||||||
int argc, char *const *argv, const char *const *envp,
|
int argc, char *const *argv, const char *const *envp,
|
||||||
void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data);
|
void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Like term_tab_new but with an explicit override for the new tab's cwd.
|
||||||
|
* If override_cwd is NULL behaves identically to term_tab_new.
|
||||||
|
*/
|
||||||
|
struct terminal *term_tab_new_with_cwd(
|
||||||
|
struct terminal *primary, const char *override_cwd,
|
||||||
|
int argc, char *const *argv, const char *const *envp,
|
||||||
|
void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data);
|
||||||
|
|
||||||
void term_tab_switch(struct wl_window *win, size_t idx);
|
void term_tab_switch(struct wl_window *win, size_t idx);
|
||||||
void term_tab_close(struct terminal *term);
|
void term_tab_close(struct terminal *term);
|
||||||
size_t term_tab_bar_hit_test(const struct terminal *term, int x, int y);
|
size_t term_tab_bar_hit_test(const struct terminal *term, int x, int y);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue