Archive closed browser tabs

This commit is contained in:
2026-05-07 19:53:32 -04:00
parent 82df2b4502
commit e0eb5b51b2
9 changed files with 150 additions and 5 deletions
+3
View File
@@ -12,6 +12,9 @@ pub enum CoreError {
#[error("favorite limit reached: {limit}")]
FavoriteLimitReached { limit: usize },
#[error("browser state has no archived tabs")]
NoArchivedTabs,
#[error("browser state has no active tab")]
MissingActiveTab,
}
+27 -3
View File
@@ -1,6 +1,6 @@
use ely_domain::{
BrowserTab, CommandIntent, CommandScope, DomainError, Profile, ProfileId, ProfileKind, Space,
SpaceId, TabId, UrlText,
ArchiveSource, ArchivedTab, BrowserTab, CommandIntent, CommandScope, DomainError, Profile,
ProfileId, ProfileKind, Space, SpaceId, TabId, UrlText,
};
use url::Url;
@@ -33,6 +33,7 @@ pub struct BrowserSnapshot {
pub tabs: Vec<BrowserTab>,
pub favorites: Vec<BrowserTab>,
pub pinned_tabs: Vec<BrowserTab>,
pub archived_tabs: Vec<ArchivedTab>,
pub active_tab_id: TabId,
pub active_space_name: String,
pub active_profile_name: String,
@@ -44,6 +45,7 @@ pub struct BrowserCore {
spaces: Vec<Space>,
profiles: Vec<Profile>,
tabs: Vec<BrowserTab>,
archived_tabs: Vec<ArchivedTab>,
active_space_id: SpaceId,
active_profile_id: ProfileId,
active_tab_id: TabId,
@@ -71,6 +73,7 @@ impl BrowserCore {
spaces: vec![space],
profiles: vec![profile],
tabs: vec![tab],
archived_tabs: Vec::new(),
command_query: String::new(),
new_tab_url,
})
@@ -102,7 +105,8 @@ impl BrowserCore {
.ok_or_else(|| CoreError::TabNotFound { id: tab_id.clone() })?;
let was_active = &self.active_tab_id == tab_id;
self.tabs.remove(close_index);
let closed_tab = self.tabs.remove(close_index);
self.archived_tabs.push(ArchivedTab::new(closed_tab, ArchiveSource::ManualClose));
if self.tabs.is_empty() {
let tab = self.build_tab(self.new_tab_url.clone());
@@ -121,6 +125,21 @@ impl BrowserCore {
Ok(self.active_tab_id.clone())
}
pub fn restore_last_archived_tab(&mut self) -> Result<TabId, CoreError> {
let archived_tab = self.archived_tabs.pop().ok_or(CoreError::NoArchivedTabs)?;
let tab = archived_tab.into_tab();
let tab_id = tab.id().clone();
let insert_index = self
.tabs
.iter()
.position(|existing| existing.id() == &self.active_tab_id)
.map_or(self.tabs.len(), |index| index + 1);
self.tabs.insert(insert_index, tab);
self.select_tab(&tab_id)?;
Ok(tab_id)
}
pub fn select_tab(&mut self, tab_id: &TabId) -> Result<(), CoreError> {
let tab = self
.tabs
@@ -221,6 +240,7 @@ impl BrowserCore {
Ok(BrowserSnapshot {
favorites: self.favorites(),
pinned_tabs: self.pinned_tabs(),
archived_tabs: self.archived_tabs.clone(),
tabs: self.tabs.clone(),
active_tab_id: self.active_tab_id.clone(),
active_space_name: active_space.name().to_string(),
@@ -263,6 +283,10 @@ impl BrowserCore {
self.toggle_active_tab_pinned()?;
Ok(true)
}
"restore-tab" | "reopen-tab" => {
self.restore_last_archived_tab()?;
Ok(true)
}
_ => Ok(false),
}
}
+17
View File
@@ -67,6 +67,23 @@ fn close_tab_command_closes_active_tab() -> Result<(), Box<dyn Error>> {
Ok(())
}
#[test]
fn restore_tab_command_reopens_last_archived_tab() -> Result<(), Box<dyn Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
let closed_tab_id = core.open_tab(UrlText::parse("https://example.com")?);
core.close_active_tab()?;
core.set_command_query(">restore-tab");
let intent = core.submit_command()?;
let snapshot = core.snapshot()?;
assert_eq!(intent, Some(CommandIntent::Command("restore-tab".to_string())));
assert_eq!(snapshot.active_tab_id, closed_tab_id);
assert!(snapshot.archived_tabs.is_empty());
assert_eq!(snapshot.command_query, "");
Ok(())
}
#[test]
fn unknown_command_preserves_query() -> Result<(), Box<dyn Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
+32
View File
@@ -36,6 +36,8 @@ fn closes_active_tab_and_selects_next_neighbor() -> Result<(), Box<dyn Error>> {
assert_eq!(active_tab_id, third_tab_id);
assert_eq!(ordered_ids, vec![first_tab_id, active_tab_id.clone()]);
assert_eq!(snapshot.active_tab_id, active_tab_id);
assert_eq!(snapshot.archived_tabs.len(), 1);
assert_eq!(snapshot.archived_tabs[0].tab().id(), &second_tab_id);
Ok(())
}
@@ -49,11 +51,41 @@ fn closing_last_tab_replaces_it_with_new_tab() -> Result<(), Box<dyn Error>> {
let snapshot = core.snapshot()?;
assert_eq!(snapshot.tabs.len(), 1);
assert_eq!(snapshot.active_tab_id, active_tab_id);
assert_eq!(snapshot.archived_tabs.len(), 1);
let replacement_tab = snapshot.tabs.first().ok_or(CoreError::MissingActiveTab)?;
assert_eq!(replacement_tab.url().as_str(), "ely://new-tab");
Ok(())
}
#[test]
fn restores_last_archived_tab() -> Result<(), Box<dyn Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
let closed_tab_id = core.open_tab(UrlText::parse("https://example.com")?);
core.close_active_tab()?;
let restored_tab_id = core.restore_last_archived_tab()?;
let snapshot = core.snapshot()?;
assert_eq!(restored_tab_id, closed_tab_id);
assert_eq!(snapshot.active_tab_id, closed_tab_id);
assert!(snapshot.archived_tabs.is_empty());
assert!(snapshot.tabs.iter().any(|tab| tab.id() == &closed_tab_id));
Ok(())
}
#[test]
fn restore_without_archived_tabs_returns_error() -> Result<(), Box<dyn Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
let error = match core.restore_last_archived_tab() {
Err(error) => error,
Ok(_) => return Err("restore should require an archived tab".into()),
};
assert_eq!(error, CoreError::NoArchivedTabs);
Ok(())
}
#[test]
fn selects_next_tab_with_wraparound() -> Result<(), Box<dyn Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;