forked from entailz/toes
Compare commits
8 commits
gradient-t
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 1962186ce1 | |||
| 3f137482ce | |||
| 1566f14a08 | |||
|
|
cabddb26e6 | ||
| 4826aa5e80 | |||
| 6ba4c5ff7a | |||
|
|
05ee680778 | ||
| 3a4814e1fa |
19 changed files with 22982 additions and 21328 deletions
2
config.h
2
config.h
|
|
@ -495,6 +495,8 @@ struct config {
|
|||
uint32_t active_bg;
|
||||
uint32_t active_fg;
|
||||
uint32_t unread_fg;
|
||||
uint32_t overview_active_border; /* ring around the currently-active tab in the overview */
|
||||
uint32_t overview_select_border; /* ring around keyboard/hover-selected tab in the overview */
|
||||
} colors;
|
||||
} tabs;
|
||||
|
||||
|
|
|
|||
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
39
flake.nix
Normal file
39
flake.nix
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
description = "toes terminal emulator";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{ nixpkgs, ... }:
|
||||
let
|
||||
systems = [
|
||||
"aarch64-linux"
|
||||
"x86_64-linux"
|
||||
];
|
||||
|
||||
forAllSystems = nixpkgs.lib.genAttrs systems;
|
||||
in
|
||||
{
|
||||
packages = forAllSystems (
|
||||
system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
|
||||
toes = pkgs.foot.overrideAttrs (prev: {
|
||||
pname = "toes";
|
||||
version = "1.26.1";
|
||||
src = ./.;
|
||||
buildInputs = (prev.buildInputs or [ ]) ++ [
|
||||
pkgs.libsodium
|
||||
];
|
||||
});
|
||||
in
|
||||
{
|
||||
inherit toes;
|
||||
default = toes;
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
5
foot.ini
5
foot.ini
|
|
@ -193,6 +193,8 @@ height=26
|
|||
# inherit-cwd=no (new tabs open in the active tab's cwd; requires OSC 7 shell support)
|
||||
# unread-indicator=● (string drawn before label when tab has unseen output; empty disables)
|
||||
# unread-color=fabd2f (color of the unread-indicator)
|
||||
# overview-active-border=fabd2f (ring around the currently-active tab in tab overview)
|
||||
# overview-select-border=ffffff (ring around the keyboard/hover-selected tab in tab overview)
|
||||
|
||||
[csd]
|
||||
# preferred=server
|
||||
|
|
@ -249,6 +251,9 @@ 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
|
||||
|
|
|
|||
|
|
@ -65,6 +65,11 @@ 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,
|
||||
|
|
@ -77,7 +82,7 @@ enum bind_action_normal {
|
|||
BIND_ACTION_SELECT_QUOTE,
|
||||
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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ 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')
|
||||
|
||||
|
|
@ -330,6 +331,8 @@ 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',
|
||||
|
|
@ -342,7 +345,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],
|
||||
tllist, fcft, libsodium],
|
||||
link_with: pgolib,
|
||||
install: true)
|
||||
|
||||
|
|
|
|||
1
search.h
1
search.h
|
|
@ -6,6 +6,7 @@
|
|||
#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,
|
||||
|
|
|
|||
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);
|
||||
8907
terminal.c
8907
terminal.c
File diff suppressed because it is too large
Load diff
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 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;
|
||||
|
|
@ -626,6 +635,7 @@ struct terminal {
|
|||
size_t len;
|
||||
size_t sz;
|
||||
size_t cursor;
|
||||
enum search_mode mode;
|
||||
|
||||
int original_view;
|
||||
bool view_followed_offset;
|
||||
|
|
@ -865,6 +875,24 @@ 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;
|
||||
};
|
||||
|
|
@ -882,6 +910,15 @@ 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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue