Skip to content

Commit a1114f5

Browse files
committed
feat(ie-shell): add navigation service, tab model, and bookmarks #7
- NavigationService trait (object-safe via async-trait) with InProcessNavigator wrapping ie-net::Client - TabManager with tab lifecycle, cycling, back/forward history - BookmarkStore with JSON persistence - 27 new tests (7 navigation, 15 tab/history, 5 bookmark)
1 parent ea468c0 commit a1114f5

7 files changed

Lines changed: 761 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 60 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,6 @@ serde_json = "1"
5353
clap = { version = "4", features = ["derive"] }
5454
softbuffer = "0.4"
5555
bytes = "1"
56+
async-trait = "0.1"
57+
chrono = { version = "0.4", features = ["serde"] }
58+
tempfile = "3"

crates/ie-shell/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ winit.workspace = true
1717
url.workspace = true
1818
clap.workspace = true
1919
softbuffer.workspace = true
20+
async-trait.workspace = true
21+
serde.workspace = true
22+
serde_json.workspace = true
23+
chrono.workspace = true
2024
ie-net.workspace = true
2125
ie-dom.workspace = true
2226
ie-sandbox.workspace = true
27+
28+
[dev-dependencies]
29+
tempfile.workspace = true
30+
bytes.workspace = true
31+
http-body-util.workspace = true
32+
hyper.workspace = true
33+
hyper-util.workspace = true

crates/ie-shell/src/bookmarks.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
use std::path::{Path, PathBuf};
2+
3+
use anyhow::Result;
4+
use chrono::{DateTime, Utc};
5+
use serde::{Deserialize, Serialize};
6+
7+
#[derive(Debug, Clone, Serialize, Deserialize)]
8+
pub struct Bookmark {
9+
pub url: String,
10+
pub title: String,
11+
pub created: DateTime<Utc>,
12+
}
13+
14+
pub struct BookmarkStore {
15+
bookmarks: Vec<Bookmark>,
16+
path: PathBuf,
17+
}
18+
19+
impl BookmarkStore {
20+
pub fn new(data_dir: &Path) -> Result<Self> {
21+
let path = data_dir.join("bookmarks.json");
22+
let bookmarks = if path.exists() {
23+
let data = std::fs::read_to_string(&path)?;
24+
serde_json::from_str(&data)?
25+
} else {
26+
Vec::new()
27+
};
28+
Ok(Self { bookmarks, path })
29+
}
30+
31+
pub fn add(&mut self, url: &str, title: &str) -> Result<()> {
32+
self.bookmarks.push(Bookmark {
33+
url: url.to_string(),
34+
title: title.to_string(),
35+
created: Utc::now(),
36+
});
37+
self.save()
38+
}
39+
40+
pub fn remove(&mut self, url: &str) -> Result<()> {
41+
self.bookmarks.retain(|b| b.url != url);
42+
self.save()
43+
}
44+
45+
pub fn list(&self) -> &[Bookmark] {
46+
&self.bookmarks
47+
}
48+
49+
fn save(&self) -> Result<()> {
50+
if let Some(parent) = self.path.parent() {
51+
std::fs::create_dir_all(parent)?;
52+
}
53+
let json = serde_json::to_string_pretty(&self.bookmarks)?;
54+
std::fs::write(&self.path, json)?;
55+
Ok(())
56+
}
57+
}
58+
59+
#[cfg(test)]
60+
mod tests {
61+
use super::*;
62+
63+
#[test]
64+
fn new_with_empty_dir() {
65+
let dir = tempfile::tempdir().unwrap();
66+
let store = BookmarkStore::new(dir.path()).unwrap();
67+
assert!(store.list().is_empty());
68+
}
69+
70+
#[test]
71+
fn add_then_list() {
72+
let dir = tempfile::tempdir().unwrap();
73+
let mut store = BookmarkStore::new(dir.path()).unwrap();
74+
store.add("https://example.com", "Example").unwrap();
75+
assert_eq!(store.list().len(), 1);
76+
assert_eq!(store.list()[0].url, "https://example.com");
77+
assert_eq!(store.list()[0].title, "Example");
78+
}
79+
80+
#[test]
81+
fn remove_bookmark() {
82+
let dir = tempfile::tempdir().unwrap();
83+
let mut store = BookmarkStore::new(dir.path()).unwrap();
84+
store.add("https://a.com", "A").unwrap();
85+
store.add("https://b.com", "B").unwrap();
86+
store.remove("https://a.com").unwrap();
87+
assert_eq!(store.list().len(), 1);
88+
assert_eq!(store.list()[0].url, "https://b.com");
89+
}
90+
91+
#[test]
92+
fn round_trip_persistence() {
93+
let dir = tempfile::tempdir().unwrap();
94+
{
95+
let mut store = BookmarkStore::new(dir.path()).unwrap();
96+
store.add("https://example.com", "Example").unwrap();
97+
store.add("https://rust-lang.org", "Rust").unwrap();
98+
}
99+
let store = BookmarkStore::new(dir.path()).unwrap();
100+
assert_eq!(store.list().len(), 2);
101+
assert_eq!(store.list()[0].url, "https://example.com");
102+
assert_eq!(store.list()[1].url, "https://rust-lang.org");
103+
}
104+
105+
#[test]
106+
fn add_duplicate_url_keeps_both() {
107+
let dir = tempfile::tempdir().unwrap();
108+
let mut store = BookmarkStore::new(dir.path()).unwrap();
109+
store.add("https://example.com", "First").unwrap();
110+
store.add("https://example.com", "Second").unwrap();
111+
assert_eq!(store.list().len(), 2);
112+
}
113+
}

crates/ie-shell/src/main.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,14 @@
1212
//! - No spell checking
1313
1414
mod app;
15+
#[allow(dead_code)]
16+
mod bookmarks;
1517
mod cli;
1618
mod headless;
19+
#[allow(dead_code)]
20+
mod navigation;
21+
#[allow(dead_code)]
22+
mod tab;
1723

1824
use anyhow::Result;
1925
use clap::Parser;

0 commit comments

Comments
 (0)