feat: restore tabs and splits at launch

The tab layout (tab order and names, pins, the active tab, split trees
with their ratios) is saved whenever it changes — checked once a second,
written only on change, so it survives a crash — and reopened at the
next launch: saved profiles reconnect and local shells start in the
directory they were in (on Linux the live /proc cwd, so it isn't one
command behind). Quick Connect tabs and placeholder panes are skipped.
A General setting turns it off; it never runs when launched with
--connect, --run-local or --open, and screenshot runs never save.

Verified: a local tab left in /usr/share came back there with a
'Restored 1 tab(s)' toast.
This commit is contained in:
Andy Dixon
2026-10-12 18:53:00 +01:00
parent 469acb1203
commit 1990fb5da9
11 changed files with 247 additions and 1 deletions
+6
View File
@@ -147,6 +147,12 @@ impl LocalShell {
/// The last directory the shell was seen in (used to restart in the same place).
pub fn working_directory(&self) -> PathBuf {
// On Linux the live directory is a cheap readlink; elsewhere the last capture.
if cfg!(target_os = "linux")
&& let Some(dir) = self.state.lock().0.as_ref().and_then(|r| r.pid).and_then(process_cwd).filter(|d| d.is_dir())
{
return dir;
}
self.state.lock().1.clone()
}
+3
View File
@@ -621,6 +621,8 @@ pub struct AppPreferences {
pub allow_remote_clipboard: bool,
/// Ask before pasting multi-line text into a terminal that isn't in bracketed-paste mode.
pub confirm_multiline_paste: bool,
/// Reopen the previous session's tabs and splits at launch.
pub restore_sessions: bool,
}
impl Default for AppPreferences {
@@ -642,6 +644,7 @@ impl Default for AppPreferences {
check_for_updates: true,
allow_remote_clipboard: true,
confirm_multiline_paste: true,
restore_sessions: true,
}
}
}
+5
View File
@@ -127,6 +127,11 @@ impl Service {
self.sessions.connect_local(&shell)
}
/// A local shell with an explicit shell and start directory (restored tabs).
pub fn connect_local_in(&self, shell: &str, cwd: Option<PathBuf>) -> String {
self.sessions.connect_local_in(shell, cwd)
}
pub fn reconnect(&self, id: &str) -> Result<String> {
self.sessions.reconnect(id)
}
+20
View File
@@ -6,6 +6,11 @@ impl Manager {
// --- local shell --------------------------------------------------------------------
pub fn connect_local(self: &Arc<Self>, shell: &str) -> String {
self.connect_local_in(shell, None)
}
/// A local shell starting in `cwd` (when it still exists), e.g. a restored tab.
pub fn connect_local_in(self: &Arc<Self>, shell: &str, cwd: Option<PathBuf>) -> String {
let id = new_id();
let info = SessionInfo {
id: id.clone(),
@@ -21,6 +26,9 @@ impl Manager {
error: String::new(),
};
let epoch = self.new_session(info, None, shell.into(), SessionSecrets::default());
if let Some(dir) = cwd.filter(|d| d.is_dir()) {
let _ = self.with(&id, |s| s.local_cwd = Some(dir));
}
self.emit_state(&id, SessionState::Connecting, "");
let me = self.clone();
let id2 = id.clone();
@@ -28,6 +36,18 @@ impl Manager {
id
}
/// A local session's shell and its current directory.
pub fn local_state(&self, id: &str) -> Option<(String, Option<PathBuf>)> {
self.with(id, |s| {
let cwd = match &s.client {
Client::Local(c) => Some(c.working_directory()),
_ => s.local_cwd.clone(),
};
(s.local_shell.clone(), cwd)
})
.ok()
}
/// Starts a fresh shell in an exited local tab, keeping its scrollback and last directory.
pub fn restart_local(self: &Arc<Self>, id: &str, cols: u16, rows: u16) -> Result<()> {
let epoch = self.with(id, |s| {
+10
View File
@@ -395,6 +395,16 @@ impl Store {
Ok(())
}
/// A value saved under `key` (app state such as the restorable tab layout).
pub fn setting(&self, key: &str) -> Option<String> {
self.db.lock().query_row("SELECT value FROM preferences WHERE key = ?", [key], |r| r.get(0)).optional().ok().flatten()
}
pub fn set_setting(&self, key: &str, value: &str) -> Result<()> {
self.db.lock().execute("INSERT INTO preferences (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value", [key, value])?;
Ok(())
}
pub fn list_snippets(&self) -> Result<Vec<Snippet>> {
let db = self.db.lock();
let mut st = db.prepare("SELECT id, name, command, description, folder, tags, created_at, updated_at FROM snippets ORDER BY name ASC")?;