diff --git a/config.c b/config.c index 7ea297e..2a552a6 100644 --- a/config.c +++ b/config.c @@ -130,11 +130,6 @@ static const char *const binding_action_map[] = { [BIND_ACTION_TAB_9] = "tab-9", [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 */ [BIND_ACTION_SCROLLBACK_UP_MOUSE] = "scrollback-up-mouse", [BIND_ACTION_SCROLLBACK_DOWN_MOUSE] = "scrollback-down-mouse", diff --git a/csi.c b/csi.c index f4f16fd..9bdda99 100644 --- a/csi.c +++ b/csi.c @@ -1583,10 +1583,7 @@ void csi_dispatch(struct terminal *term, uint8_t final) { /* 0 - icon + title, 1 - icon, 2 - title */ unsigned what = vt_param_get(term, 1, 0); if (what == 0 || what == 2) { - tll_push_back(term->window_title_stack, - xstrdup(term->window_title != NULL - ? term->window_title - : "")); + tll_push_back(term->window_title_stack, xstrdup(term->window_title)); } break; } diff --git a/flake.lock b/flake.lock deleted file mode 100644 index c98e08e..0000000 --- a/flake.lock +++ /dev/null @@ -1,27 +0,0 @@ -{ - "nodes": { - "nixpkgs": { - "locked": { - "lastModified": 1779259093, - "narHash": "sha256-7DKWmH23hL2eYdkxCKeqj2i+yljTKuU+3Nk1UPHOnxc=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "d99b013d5d1931ad77fe3912ed218170dec5d9a4", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "nixpkgs": "nixpkgs" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix index abcd22c..b308e52 100644 --- a/flake.nix +++ b/flake.nix @@ -21,14 +21,12 @@ let pkgs = import nixpkgs { inherit system; }; - toes = pkgs.foot.overrideAttrs (prev: { + toes = pkgs.foot.overrideAttrs { pname = "toes"; version = "1.26.1"; src = ./.; - buildInputs = (prev.buildInputs or [ ]) ++ [ - pkgs.libsodium - ]; - }); + meta.mainProgram = "foot"; + }; in { inherit toes; diff --git a/foot.ini b/foot.ini index 3381f20..3d4183d 100644 --- a/foot.ini +++ b/foot.ini @@ -251,9 +251,6 @@ height=26 # tab-next=Control+Tab # tab-prev=Control+Shift+Tab # tab-overview=Control+Shift+space -# session-save=Control+Shift+s -# session-load=Control+Shift+l -# session-save-secure=Control+Shift+Alt+s [search-bindings] # cancel=Control+g Control+c Escape diff --git a/input.c b/input.c index fb02113..969952a 100644 --- a/input.c +++ b/input.c @@ -491,18 +491,6 @@ static bool execute_binding(struct seat *seat, struct terminal *term, tab_overview_toggle(term->window); 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_COPY: if (binding->aux->type != BINDING_AUX_REGEX) diff --git a/key-binding.h b/key-binding.h index e008ecc..45702ee 100644 --- a/key-binding.h +++ b/key-binding.h @@ -65,11 +65,6 @@ enum bind_action_normal { BIND_ACTION_TAB_9, 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 */ BIND_ACTION_SCROLLBACK_UP_MOUSE, BIND_ACTION_SCROLLBACK_DOWN_MOUSE, @@ -82,7 +77,7 @@ enum bind_action_normal { BIND_ACTION_SELECT_QUOTE, BIND_ACTION_SELECT_ROW, - BIND_ACTION_KEY_COUNT = BIND_ACTION_SESSION_SAVE_SECURE + 1, + BIND_ACTION_KEY_COUNT = BIND_ACTION_TAB_OVERVIEW + 1, BIND_ACTION_COUNT = BIND_ACTION_SELECT_ROW + 1, }; diff --git a/meson.build b/meson.build index 9a1efa1..a0e602b 100644 --- a/meson.build +++ b/meson.build @@ -162,7 +162,6 @@ endif tllist = dependency('tllist', version: '>=1.1.0', fallback: 'tllist') fcft = dependency('fcft', version: ['>=3.3.1', '<4.0.0'], fallback: 'fcft') -libsodium = dependency('libsodium') wayland_protocols_datadir = wayland_protocols.get_variable('pkgdatadir') @@ -331,8 +330,6 @@ executable( 'reaper.c', 'reaper.h', 'render.c', 'render.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', 'shm.c', 'shm.h', 'slave.c', 'slave.h', @@ -345,7 +342,7 @@ executable( 'xkbcommon-vmod.h', srgb_funcs, wl_proto_src + wl_proto_headers, version, dependencies: [math, threads, libepoll, pixman, wayland_client, wayland_cursor, xkb, fontconfig, utf8proc, - tllist, fcft, libsodium], + tllist, fcft], link_with: pgolib, install: true) diff --git a/render.c b/render.c index 2e2d16b..2b8971a 100644 --- a/render.c +++ b/render.c @@ -3635,21 +3635,6 @@ 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++) @@ -3657,16 +3642,7 @@ static void render_search_box(struct terminal *term) { widths[text_len] = 0; const size_t total_cells = c32swidth(text, text_len); - 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); - } + const size_t wanted_visible_cells = max(20, total_cells); /* * Build status string: " Aa Wb Re 3/17 ↻" @@ -3679,70 +3655,36 @@ static void render_search_box(struct terminal *term) { char32_t status[64]; size_t st_len = 0; - 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' '; - } + 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); @@ -3762,20 +3704,8 @@ 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); - - /* 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); + size_t height = + min(term->height - 2 * outer_margin, margin + term->cell_height + margin); width = roundf(scale * ceilf(width / scale)); height = roundf(scale * ceilf(height / scale)); @@ -3802,14 +3732,12 @@ 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.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]; - } + 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); @@ -4045,80 +3973,6 @@ 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 */ @@ -4144,8 +3998,9 @@ 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); - free(ime_alloc); - free(masked_text); +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + free(text); +#endif #undef WINDOW_X #undef WINDOW_Y } diff --git a/search.c b/search.c index 3d6d632..e50d964 100644 --- a/search.c +++ b/search.c @@ -20,7 +20,6 @@ #include "quirks.h" #include "render.h" #include "selection.h" -#include "session.h" #include "shm.h" #include "unicode-mode.h" #include "util.h" @@ -156,9 +155,6 @@ static void search_cancel_keep_selection(struct terminal *term) { term->search.wrapped = false; term->search.history_pos = NULL; 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; /* Reset IME state */ @@ -197,20 +193,11 @@ void search_begin(struct terminal *term) { term->search.sz = 64; term->search.buf = xmalloc(term->search.sz * sizeof(term->search.buf[0])); term->search.buf[0] = U'\0'; - term->search.mode = SEARCH_MODE_NORMAL; term_xcursor_update(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) { if (!term->is_searching) return; @@ -1552,10 +1539,6 @@ static bool execute_binding(struct seat *seat, struct terminal *term, return true; case BIND_ACTION_SEARCH_COMMIT: - if (term->search.mode != SEARCH_MODE_NORMAL) { - session_prompt_commit(term); - return true; - } selection_finalize(seat, term, serial); search_cancel_keep_selection(term); return true; @@ -1898,48 +1881,6 @@ 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, 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 = seat->kbd.xkb_compose_state != NULL ? xkb_compose_state_get_status(seat->kbd.xkb_compose_state) @@ -2028,10 +1969,8 @@ void search_input(struct seat *seat, struct terminal *term, update_search: LOG_DBG("search: buffer: %ls", (const wchar_t *)term->search.buf); - if (update_search_result && term->search.mode == SEARCH_MODE_NORMAL) + if (update_search_result) search_find_next(term, search_direction); - if (term->search.mode == SEARCH_MODE_SESSION_LOAD) - session_picker_refilter(term); if (redraw) render_refresh_search(term); } diff --git a/search.h b/search.h index c8fba13..ee8ecd7 100644 --- a/search.h +++ b/search.h @@ -6,7 +6,6 @@ #include "terminal.h" 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_input( struct seat *seat, struct terminal *term, diff --git a/session-crypto.c b/session-crypto.c deleted file mode 100644 index f2af313..0000000 --- a/session-crypto.c +++ /dev/null @@ -1,160 +0,0 @@ -#include "session-crypto.h" - -#include -#include - -#include - -#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; -} diff --git a/session-crypto.h b/session-crypto.h deleted file mode 100644 index 16aae8f..0000000 --- a/session-crypto.h +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once - -#include -#include - -/* - * 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); diff --git a/session-prompt.c b/session-prompt.c deleted file mode 100644 index 640bd3b..0000000 --- a/session-prompt.c +++ /dev/null @@ -1,460 +0,0 @@ -#include "session.h" - -#include -#include - -#include - -#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); -} diff --git a/session.c b/session.c deleted file mode 100644 index 3bbcac7..0000000 --- a/session.c +++ /dev/null @@ -1,854 +0,0 @@ -#include "session.h" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include - -#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//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); -} diff --git a/session.h b/session.h deleted file mode 100644 index 2b1921a..0000000 --- a/session.h +++ /dev/null @@ -1,92 +0,0 @@ -#pragma once - -#include -#include - -/* - * Persistent terminal session state. - * - * Sessions are stored as JSON files under $XDG_DATA_HOME/foot/state/.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 /.json. Creates the directory if missing. */ -bool session_save(const char *name, const struct session_state *st); - -/* Read /.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 /.json. Returns true on success. */ -bool session_delete(const char *name); - -/* True if /.json exists. */ -bool session_exists(const char *name); - -struct terminal; - -/* - * Read /.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. /.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); diff --git a/terminal.c b/terminal.c index 2ed756f..aada5fb 100644 --- a/terminal.c +++ b/terminal.c @@ -1511,17 +1511,6 @@ struct terminal *term_tab_new(struct terminal *primary, int argc, char *const *argv, const char *const *envp, void (*shutdown_cb)(void *data, int exit_code), 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; if (win->tab_count >= TAB_MAX) { @@ -1736,12 +1725,9 @@ struct terminal *term_tab_new_with_cwd(struct terminal *primary, .cb_data = shutdown_data, }, .foot_exe = xstrdup(primary->foot_exe), - .cwd = xstrdup(override_cwd != NULL - ? override_cwd - : (conf->tabs.inherit_cwd - ? win->term->cwd - : (getenv("HOME") != NULL ? getenv("HOME") - : "/"))), + .cwd = xstrdup(conf->tabs.inherit_cwd + ? win->term->cwd + : (getenv("HOME") != NULL ? getenv("HOME") : "/")), .grapheme_shaping = conf->tweak.grapheme_shaping, #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED .ime_enabled = true, @@ -1778,10 +1764,6 @@ struct terminal *term_tab_new_with_cwd(struct terminal *primary, 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( term->ptmx, argc, term->cwd, argv, envp, &conf->env_vars, conf->term, conf->shell, conf->login_shell, &conf->notifications)) == -1) { diff --git a/terminal.h b/terminal.h index e06b068..985d2ae 100644 --- a/terminal.h +++ b/terminal.h @@ -344,15 +344,6 @@ enum selection_direction {SELECTION_UNDIR, SELECTION_LEFT, SELECTION_RIGHT}; 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_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 { void *data; @@ -635,7 +626,6 @@ struct terminal { size_t len; size_t sz; size_t cursor; - enum search_mode mode; int original_view; bool view_followed_offset; @@ -875,24 +865,6 @@ struct terminal { char *foot_exe; 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 size_notifications; }; @@ -910,15 +882,6 @@ struct terminal *term_tab_new( int argc, char *const *argv, const char *const *envp, 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_close(struct terminal *term); size_t term_tab_bar_hit_test(const struct terminal *term, int x, int y);