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:
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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")?;
|
||||
|
||||
Reference in New Issue
Block a user