use std::path::{Path, PathBuf};
use crate::app::{App, Mode};
use crate::session::{SavedSession, SavedTab};
impl App {
pub(crate) fn save_session(&self) {
if !self.cfg.ui.restore_tabs {
return;
}
let Some(store) = &self.session_store else {
return;
};
let _ = store.write(self.session_snapshot());
}
fn session_snapshot(&self) -> SavedSession {
let mut tabs = Vec::with_capacity(self.tabs.len());
for (i, slot) in self.tabs.iter().enumerate() {
let (mode, root, cursor, preview) = if i == self.active_tab {
(
self.mode,
&self.root,
self.entries.get(self.selected),
self.preview_path.as_deref(),
)
} else {
(
slot.mode,
&slot.root,
slot.entries.get(slot.selected),
slot.preview_path.as_deref(),
)
};
tabs.push(SavedTab {
root: root.to_string_lossy().to_string(),
cursor: cursor.map(|e| e.path.to_string_lossy().to_string()),
preview: if mode == Mode::Preview {
preview.map(|p| p.to_string_lossy().to_string())
} else {
None
},
});
}
SavedSession {
dir: String::new(), active: self.active_tab,
tabs,
}
}
pub fn restore_session(&mut self) {
if !self.cfg.ui.restore_tabs {
return;
}
let Some(store) = &self.session_store else {
return;
};
let Some(sess) = store.read() else {
return;
};
let tabs: Vec<SavedTab> = sess
.tabs
.into_iter()
.filter(|t| Path::new(&t.root).is_dir())
.collect();
if tabs.is_empty() {
return;
}
for (i, t) in tabs.iter().enumerate() {
if i > 0 && self.tab_new().is_err() {
break;
}
self.apply_saved_tab(t);
}
let want = sess.active.min(self.tabs.len() - 1);
if want != self.active_tab {
self.tab_goto(want);
}
self.save_session();
}
fn apply_saved_tab(&mut self, t: &SavedTab) {
let root = PathBuf::from(&t.root);
if root != self.root {
self.clear_for_root_change();
self.root = root;
self.entries.clear();
self.selected = 0;
if self.rebuild_tree().is_err() {
return; }
}
if let Some(cur) = &t.cursor {
let p = Path::new(cur);
if p.exists() {
let _ = self.reveal_path_deep(p);
}
}
if let Some(pv) = &t.preview {
let p = Path::new(pv);
if p.is_file() {
let _ = self.reveal_path_deep(p);
self.enter_preview(p);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use crate::session::SessionStore;
use std::fs;
fn setup(name: &str) -> (PathBuf, PathBuf) {
let dir = std::env::temp_dir().join(name);
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(dir.join("sub")).unwrap();
fs::write(dir.join("a.txt"), "alpha\n").unwrap();
fs::write(dir.join("b.txt"), "beta\n").unwrap();
fs::write(dir.join("sub/c.txt"), "gamma\n").unwrap();
let base = std::env::temp_dir().join(format!("{name}_base"));
let _ = fs::remove_dir_all(&base);
(dir, base)
}
#[test]
fn session_restore_rebuilds_tabs_cursor_and_preview() {
let (dir, base) = setup("konoma_sess_restore_test");
let mut app = App::new(dir.clone(), Config::default()).unwrap();
app.attach_session_store(SessionStore::with_base(base.clone(), &dir));
let a = dir.join("a.txt");
let b = dir.join("b.txt");
let _ = app.reveal_path_deep(&a);
app.enter_preview(&a);
app.tab_new().unwrap();
let _ = app.reveal_path_deep(&b);
app.save_session(); drop(app);
let mut app2 = App::new(dir.clone(), Config::default()).unwrap();
app2.attach_session_store(SessionStore::with_base(base.clone(), &dir));
app2.restore_session();
assert_eq!(app2.tab_count(), 2, "タブ数を復元");
assert_eq!(app2.active_tab_index(), 1, "アクティブタブも復元");
assert_eq!(app2.mode, Mode::Tree);
assert_eq!(app2.entries[app2.selected].path, b, "カーソル位置を復元");
app2.tab_goto(0);
assert_eq!(app2.mode, Mode::Preview, "プレビュー面ごと復元");
assert_eq!(app2.preview_path.as_deref(), Some(a.as_path()));
fs::remove_dir_all(&dir).ok();
fs::remove_dir_all(&base).ok();
}
#[test]
fn session_restore_skips_stale_roots_and_previews() {
let (dir, base) = setup("konoma_sess_stale_test");
let store = SessionStore::with_base(base.clone(), &dir);
store
.write(SavedSession {
dir: String::new(),
active: 5, tabs: vec![
SavedTab {
root: dir.join("no_such_dir").to_string_lossy().into(),
cursor: None,
preview: None,
},
SavedTab {
root: dir.to_string_lossy().into(),
cursor: Some(dir.join("b.txt").to_string_lossy().into()),
preview: Some(dir.join("gone.txt").to_string_lossy().into()),
},
],
})
.unwrap();
let mut app = App::new(dir.clone(), Config::default()).unwrap();
app.attach_session_store(store);
app.restore_session();
assert_eq!(app.tab_count(), 1, "存在しない root のタブは捨てる");
assert_eq!(
app.mode,
Mode::Tree,
"プレビュー先が消えていたらツリーで開く"
);
assert_eq!(app.entries[app.selected].path, dir.join("b.txt"));
fs::remove_dir_all(&dir).ok();
fs::remove_dir_all(&base).ok();
}
#[test]
fn restore_tabs_off_neither_saves_nor_restores() {
let (dir, base) = setup("konoma_sess_off_test");
let mut cfg = Config::default();
cfg.ui.restore_tabs = false;
let mut app = App::new(dir.clone(), cfg.clone()).unwrap();
app.attach_session_store(SessionStore::with_base(base.clone(), &dir));
app.tab_new().unwrap();
app.save_session();
assert!(!base.exists(), "restore_tabs=false は書き込みもしない");
SessionStore::with_base(base.clone(), &dir)
.write(SavedSession {
dir: String::new(),
active: 0,
tabs: vec![
SavedTab {
root: dir.to_string_lossy().into(),
cursor: None,
preview: None,
},
SavedTab {
root: dir.to_string_lossy().into(),
cursor: None,
preview: None,
},
],
})
.unwrap();
let mut app2 = App::new(dir.clone(), cfg).unwrap();
app2.attach_session_store(SessionStore::with_base(base.clone(), &dir));
app2.restore_session();
assert_eq!(app2.tab_count(), 1, "restore_tabs=false は復元しない");
fs::remove_dir_all(&dir).ok();
fs::remove_dir_all(&base).ok();
}
}