Skip to main content

atmos/os_lib/web_engine/
omnibox.rs

1// src/os_lib/web_engine/omnibox.rs - Omnibox (Smart Address Bar) Suggestions & Bookmarks
2#![allow(dead_code)]
3
4extern crate alloc;
5use alloc::format;
6use alloc::string::String;
7use alloc::vec::Vec;
8
9/// Omnibox 候補の種別
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum OmniboxItemKind {
12    /// ブックマーク(お気に入り登録済み)
13    Bookmark,
14    /// 閲覧履歴
15    History,
16    /// Web 検索候補 (DuckDuckGo)
17    Search,
18}
19
20impl OmniboxItemKind {
21    pub fn badge_label(&self) -> &'static str {
22        match self {
23            OmniboxItemKind::Bookmark => "Bookmark",
24            OmniboxItemKind::History => "History",
25            OmniboxItemKind::Search => "DuckDuckGo",
26        }
27    }
28
29    pub fn icon_char(&self) -> &'static str {
30        match self {
31            OmniboxItemKind::Bookmark => "*",
32            OmniboxItemKind::History => ">",
33            OmniboxItemKind::Search => "?",
34        }
35    }
36}
37
38/// Omnibox サジェスト項目
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct OmniboxItem {
41    pub title: String,
42    pub url: String,
43    pub kind: OmniboxItemKind,
44}
45
46/// ブックマーク項目
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct Bookmark {
49    pub title: String,
50    pub url: String,
51}
52
53/// 閲覧履歴項目
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct HistoryEntry {
56    pub title: String,
57    pub url: String,
58    pub visit_count: u32,
59}
60
61/// Omnibox サジェスト・ブックマーク・履歴管理エンジン(純粋ロジック)
62#[derive(Debug, Clone)]
63pub struct OmniboxEngine {
64    pub bookmarks: Vec<Bookmark>,
65    pub history: Vec<HistoryEntry>,
66}
67
68impl Default for OmniboxEngine {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74impl OmniboxEngine {
75    pub fn new() -> Self {
76        let mut engine = Self {
77            bookmarks: Vec::new(),
78            history: Vec::new(),
79        };
80
81        // デフォルトの組み込みブックマーク
82        engine.add_bookmark("AtmOS Home", "http://localhost/home");
83        engine.add_bookmark("DuckDuckGo", "https://duckduckgo.com/");
84        engine.add_bookmark("Sugi Lab (Nagoya Univ)", "https://www.sugi-lab.net/");
85        engine.add_bookmark("YouTube", "https://www.youtube.com/");
86        engine.add_bookmark("AtmOS GitHub", "https://github.com/sugi-lab/AtmOS");
87
88        engine
89    }
90
91    /// ブックマーク追加(既に同一 URL があればタイトル更新して true を返す)
92    pub fn add_bookmark(&mut self, title: &str, url: &str) -> bool {
93        let clean_url = url.trim();
94        if clean_url.is_empty() {
95            return false;
96        }
97
98        if let Some(existing) = self.bookmarks.iter_mut().find(|b| b.url == clean_url) {
99            existing.title = title.trim().into();
100            return false;
101        }
102
103        self.bookmarks.push(Bookmark {
104            title: title.trim().into(),
105            url: clean_url.into(),
106        });
107        true
108    }
109
110    /// ブックマーク削除
111    pub fn remove_bookmark(&mut self, url: &str) -> bool {
112        let clean_url = url.trim();
113        let initial_len = self.bookmarks.len();
114        self.bookmarks.retain(|b| b.url != clean_url);
115        self.bookmarks.len() < initial_len
116    }
117
118    /// ブックマーク登録状態の確認
119    pub fn is_bookmarked(&self, url: &str) -> bool {
120        let clean = url.trim();
121        self.bookmarks.iter().any(|b| b.url == clean)
122    }
123
124    /// ブックマーク登録のトグル(未登録なら追加、登録済みなら削除)
125    pub fn toggle_bookmark(&mut self, title: &str, url: &str) -> bool {
126        if self.is_bookmarked(url) {
127            self.remove_bookmark(url);
128            false
129        } else {
130            self.add_bookmark(title, url);
131            true
132        }
133    }
134
135    /// ページ訪問履歴を記録
136    pub fn record_visit(&mut self, title: &str, url: &str) {
137        let clean_url = url.trim();
138        if clean_url.is_empty() || clean_url == "about:blank" {
139            return;
140        }
141
142        // 既存エントリがあれば最前列に移動してカウント増加
143        if let Some(pos) = self.history.iter().position(|h| h.url == clean_url) {
144            let mut entry = self.history.remove(pos);
145            entry.visit_count = entry.visit_count.saturating_add(1);
146            if !title.trim().is_empty() {
147                entry.title = title.trim().into();
148            }
149            self.history.insert(0, entry);
150            return;
151        }
152
153        // 新規エントリを先頭に挿入
154        self.history.insert(0, HistoryEntry {
155            title: title.trim().into(),
156            url: clean_url.into(),
157            visit_count: 1,
158        });
159
160        // 履歴上限(最新 100 件)
161        if self.history.len() > 100 {
162            self.history.truncate(100);
163        }
164    }
165
166    /// 入力クエリに対する統合サジェスト候補を生成
167    ///
168    /// 優先順位:
169    /// 1. ブックマーク(タイトルまたは URL にクエリが含まれるもの)
170    /// 2. 閲覧履歴(訪問回数および直近順)
171    /// 3. Web 検索候補 (DuckDuckGo サジェスト)
172    pub fn get_suggestions(
173        &self,
174        query: &str,
175        search_suggestions: &[String],
176        max_items: usize,
177    ) -> Vec<OmniboxItem> {
178        let q = query.trim().to_ascii_lowercase();
179        let mut results = Vec::new();
180
181        if q.is_empty() {
182            // 空クエリ時は主要ブックマークを提示
183            for b in self.bookmarks.iter().take(max_items) {
184                results.push(OmniboxItem {
185                    title: b.title.clone(),
186                    url: b.url.clone(),
187                    kind: OmniboxItemKind::Bookmark,
188                });
189            }
190            return results;
191        }
192
193        // 1. ブックマーク検索
194        for b in &self.bookmarks {
195            let t_lower = b.title.to_ascii_lowercase();
196            let u_lower = b.url.to_ascii_lowercase();
197            if t_lower.contains(&q) || u_lower.contains(&q) {
198                results.push(OmniboxItem {
199                    title: b.title.clone(),
200                    url: b.url.clone(),
201                    kind: OmniboxItemKind::Bookmark,
202                });
203                if results.len() >= max_items {
204                    return results;
205                }
206            }
207        }
208
209        // 2. 閲覧履歴検索(ブックマークと重複しないもの)
210        for h in &self.history {
211            let t_lower = h.title.to_ascii_lowercase();
212            let u_lower = h.url.to_ascii_lowercase();
213            if t_lower.contains(&q) || u_lower.contains(&q) {
214                if !results.iter().any(|r| r.url == h.url) {
215                    let disp_title = if h.title.is_empty() {
216                        h.url.clone()
217                    } else {
218                        h.title.clone()
219                    };
220                    results.push(OmniboxItem {
221                        title: disp_title,
222                        url: h.url.clone(),
223                        kind: OmniboxItemKind::History,
224                    });
225                    if results.len() >= max_items {
226                        return results;
227                    }
228                }
229            }
230        }
231
232        // 3. Web 検索サジェスト
233        for s in search_suggestions {
234            if !results.iter().any(|r| r.title.eq_ignore_ascii_case(s)) {
235                let search_url = format!("https://duckduckgo.com/?q={}", super::duckduckgo::url_encode(s));
236                results.push(OmniboxItem {
237                    title: s.clone(),
238                    url: search_url,
239                    kind: OmniboxItemKind::Search,
240                });
241                if results.len() >= max_items {
242                    return results;
243                }
244            }
245        }
246
247        results
248    }
249}