use anyhow::Result;use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};use serde::{Deserialize, Serialize};use std::sync::mpsc::{channel, Receiver, Sender};use std::sync::Arc;use std::time::Duration; use crate::ai::Summarizer;use crate::browser::{Browser, Page, RenderMode};use crate::chat::ChatMessage;use crate::config::Config;use crate::mascot::{Mascot, MascotState};use crate::search::{self, QueryTarget, SearchManager};use crate::scrape;use crate::storage::{Bookmark, Database, HistoryEntry};use crate::tabs::TabManager; #[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum Focus { Content, Sidebar, URLBar, TabBar, Bookmarks, History,} impl Focus { pub fn next(&self) -> Self { match self { Focus::Content => Focus::Sidebar, Focus::Sidebar => Focus::URLBar, Focus::URLBar => Focus::Content, Focus::TabBar => Focus::Content, Focus::Bookmarks => Focus::Content, Focus::History => Focus::Content, } } pub fn previous(&self) -> Self { match self { Focus::Content => Focus::URLBar, Focus::Sidebar => Focus::Content, Focus::URLBar => Focus::Sidebar, Focus::TabBar => Focus::Content, Focus::Bookmarks => Focus::Content, Focus::History => Focus::Content, } }} pub enum AppMessage { PageLoaded(usize, Page), // tab_id, page LoadError(usize, String), AiSummary(String), AiError(String), ChatResponse(String), ToolAction(ToolAction), RagResponse(Vec<String>), RagError(String),} /// Actions that can be triggered by AI tool calls#[derive(Debug, Clone)]pub enum ToolAction { Navigate(String), FollowLink(usize), Scroll { direction: String, amount: usize },} /// Panel mode for overlays#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum PanelMode { None, Bookmarks, History, Help, Chat, Settings, Rag, Memory,} pub struct App { pub config: Config, pub focus: Focus, pub should_quit: bool, pub animation_tick: usize, pub status_message: String, pub view_mode: ViewMode, pub format_text: bool, pub ai_summary: Option<String>, pub panel_mode: PanelMode, // Tabs pub tabs: TabManager, pub url_input: String, // Mascot pub mascot: Mascot, // Storage pub db: Option<Database>, pub bookmarks_list: Vec<Bookmark>, pub history_list: Vec<HistoryEntry>, pub bookmarks_selected: usize, pub history_selected: usize, // Chat pub chat_session: Option<crate::chat::ChatSession>, pub chat_input: String, pub chat_selected_model: usize, pub chat_scroll: usize, pub chat_focused: bool, // Settings pub settings_selected: usize, pub settings_model_input: String, pub settings_editing_model: bool, // RAG & Memory pub rag_client: Option<crate::rag::RagClient>, pub rag_results: Vec<String>, pub rag_query: String, pub rag_scroll: usize, pub rag_loading: bool, pub memory_client: Option<crate::memory::MemoryClient>, pub memory_nodes: Vec<String>, // Message passing page_rx: Receiver<AppMessage>, page_tx: Sender<AppMessage>, summarizer: Option<Arc<Summarizer>>,} #[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum ViewMode { Rendered, Raw,} impl App { pub fn new() -> Result<Self> { let config = Config::load()?; let (page_tx, page_rx) = channel(); // Try to open database, but don't fail if it errors let db = Database::open().ok(); // Initialize chat session with available models let chat_session = if let Some(api_key) = config.get_api_key() { if !api_key.is_empty() { let model = config.get_ai_model().unwrap_or("qwen/qwen3-235b-a22b:free").to_string(); let mut models = vec![model.clone()]; if let Some(fallback) = config.get_fallback_models() { models.extend(fallback); } Some(crate::chat::ChatSession::new(model, models)) } else { None } } else { None }; // Initialize RAG and memory clients let rag_client = crate::rag::RagClient::from_config(&config); let memory_client = crate::memory::MemoryClient::from_config(&config); let mut app = Self { config, focus: Focus::Content, should_quit: false, animation_tick: 0, status_message: "Ready - / search | t new tab | b bookmarks | c chat | r rag | m memory | ? help".to_string(), view_mode: ViewMode::Rendered, format_text: true, ai_summary: None, panel_mode: PanelMode::None, // Chat is always visible, not a panel mode tabs: TabManager::new(), url_input: String::new(), mascot: Mascot::new(), db, bookmarks_list: Vec::new(), history_list: Vec::new(), bookmarks_selected: 0, history_selected: 0, chat_session, chat_input: String::new(), chat_selected_model: 0, chat_scroll: 0, chat_focused: false, // Start in content mode, press 'c' or '3' for chat settings_selected: 0, settings_model_input: String::new(), settings_editing_model: false, rag_client, rag_results: Vec::new(), rag_query: String::new(), rag_scroll: 0, rag_loading: false, memory_client, memory_nodes: Vec::new(), page_rx, page_tx, summarizer: Summarizer::from_env().map(Arc::new), }; // Load bookmarks and history app.refresh_bookmarks(); app.refresh_history(); Ok(app) } // ═══════════════════════════════════════════════════════════════════════════ // CONVENIENCE ACCESSORS // ═══════════════════════════════════════════════════════════════════════════ /// Get current page from active tab pub fn current_page(&self) -> Option<&Page> { self.tabs.active_tab().and_then(|t| t.page.as_ref()) } /// Get current scroll offset from active tab pub fn scroll_offset(&self) -> usize { self.tabs.active_tab().map(|t| t.scroll_offset).unwrap_or(0) } /// Set scroll offset on active tab fn set_scroll_offset(&mut self, offset: usize) { if let Some(tab) = self.tabs.active_tab_mut() { tab.scroll_offset = offset; } } /// Get sidebar selected from active tab pub fn sidebar_selected(&self) -> usize { self.tabs.active_tab().map(|t| t.sidebar_selected).unwrap_or(0) } /// Set sidebar selected on active tab fn set_sidebar_selected(&mut self, idx: usize) { if let Some(tab) = self.tabs.active_tab_mut() { tab.sidebar_selected = idx; } } /// Check if current tab is loading pub fn loading(&self) -> bool { self.tabs.active_tab().map(|t| t.loading).unwrap_or(false) } /// Get current render mode from active tab pub fn render_mode(&self) -> RenderMode { self.tabs.active_tab().map(|t| t.render_mode).unwrap_or(RenderMode::Auto) } // ═══════════════════════════════════════════════════════════════════════════ // UPDATE LOOP // ═══════════════════════════════════════════════════════════════════════════ pub fn update(&mut self) { self.animation_tick = self.animation_tick.wrapping_add(1); self.mascot.tick(); // Check for loaded pages while let Ok(msg) = self.page_rx.try_recv() { match msg { AppMessage::PageLoaded(tab_id, page) => { // Find and update the tab for i in 0..self.tabs.count() { if let Some(tab) = self.tabs.get_tab_mut(i) { if tab.id == tab_id { // Add to history if let Some(db) = &self.db { let _ = db.add_history(&page.url, &page.title); } tab.set_page(page); self.mascot.set_state(MascotState::Success); self.status_message = format!("Loaded: {}", tab.url); break; } } } self.ai_summary = None; } AppMessage::LoadError(tab_id, err) => { for i in 0..self.tabs.count() { if let Some(tab) = self.tabs.get_tab_mut(i) { if tab.id == tab_id { tab.set_error(err.clone()); self.mascot.set_state(MascotState::Error); self.status_message = format!("Error: {}", err); break; } } } } AppMessage::AiSummary(summary) => { self.ai_summary = Some(summary); self.status_message = "AI summary ready".to_string(); self.mascot.set_state(MascotState::Success); } AppMessage::AiError(err) => { self.status_message = format!("AI error: {}", err); self.mascot.set_state(MascotState::Error); // Also show error in chat so it's visible if let Some(session) = &mut self.chat_session { session.add_assistant_message(format!("❌ Error: {}", err)); self.chat_scroll = 0; } } AppMessage::ChatResponse(response) => { if let Some(session) = &mut self.chat_session { session.add_assistant_message(response); self.chat_scroll = 0; // Auto-scroll to bottom (newest messages) self.mascot.set_state(MascotState::Success); self.status_message = "Response received".to_string(); } } AppMessage::ToolAction(action) => { match action { ToolAction::Navigate(url) => { self.url_input = url; self.navigate(); self.status_message = "AI navigated to URL".to_string(); } ToolAction::FollowLink(idx) => { if let Some(page) = self.current_page() { if let Some(link) = page.links.get(idx) { self.url_input = link.url.clone(); self.navigate(); self.status_message = format!("AI followed link {}", idx + 1); } } } ToolAction::Scroll { direction, amount } => { if let Some(tab) = self.tabs.active_tab_mut() { match direction.as_str() { "up" => tab.scroll_offset = tab.scroll_offset.saturating_sub(amount), "down" => tab.scroll_offset += amount, "top" => tab.scroll_offset = 0, "bottom" => tab.scroll_offset = usize::MAX / 2, _ => {} } self.status_message = format!("AI scrolled {}", direction); } } } } AppMessage::RagResponse(results) => { self.rag_loading = false; self.rag_results = results; self.rag_scroll = 0; self.mascot.set_state(MascotState::Success); self.status_message = format!("RAG: {} results", self.rag_results.len()); } AppMessage::RagError(err) => { self.rag_loading = false; self.rag_results = vec![format!("Error: {}", err)]; self.mascot.set_state(MascotState::Error); self.status_message = format!("RAG error: {}", err); } } } } pub fn quit(&mut self) { self.should_quit = true; } pub fn focus_next(&mut self) { self.focus = self.focus.next(); self.update_status(); } pub fn focus_previous(&mut self) { self.focus = self.focus.previous(); self.update_status(); } // ═══════════════════════════════════════════════════════════════════════════ // KEY HANDLING // ═══════════════════════════════════════════════════════════════════════════ pub fn handle_key(&mut self, key: KeyEvent) -> Result<()> { // Overlay panel handling first (these take full focus) match self.panel_mode { PanelMode::Bookmarks => return self.handle_bookmarks_keys(key), PanelMode::History => return self.handle_history_keys(key), PanelMode::Help => { // Any key closes help self.panel_mode = PanelMode::None; return Ok(()); } PanelMode::Settings => return self.handle_settings_keys(key), PanelMode::Rag => return self.handle_rag_keys(key), PanelMode::Memory => return self.handle_memory_keys(key), PanelMode::Chat | PanelMode::None => {} // Chat is always visible, handled below } // Chat input handling when chat is focused if self.chat_focused { match (key.code, key.modifiers) { // Esc unfocuses chat (KeyCode::Esc, _) => { self.chat_focused = false; self.status_message = "Content mode - 'c' for chat, '/' for URL".to_string(); return Ok(()); } // These pass through to global shortcuts (KeyCode::Tab, _) | (KeyCode::BackTab, _) => {} // Handle chat input _ => return self.handle_chat_keys(key), } } // Global shortcuts match (key.code, key.modifiers) { (KeyCode::Char('q'), KeyModifiers::NONE) | (KeyCode::Char('Q'), KeyModifiers::NONE) => { if self.focus != Focus::URLBar { self.quit(); return Ok(()); } } (KeyCode::Char('c'), KeyModifiers::CONTROL) => { self.quit(); return Ok(()); } (KeyCode::Char('?'), KeyModifiers::NONE) => { self.panel_mode = PanelMode::Help; return Ok(()); } (KeyCode::Tab, KeyModifiers::NONE) => { self.focus_next(); return Ok(()); } (KeyCode::BackTab, _) => { self.focus_previous(); return Ok(()); } (KeyCode::Char('/'), KeyModifiers::NONE) => { if self.focus != Focus::URLBar { self.focus = Focus::URLBar; self.url_input.clear(); self.update_status(); return Ok(()); } } // Tab management (KeyCode::Char('t'), KeyModifiers::NONE) if self.focus != Focus::URLBar => { if self.tabs.new_tab(None).is_some() { self.status_message = format!("New tab {} created", self.tabs.count()); } else { self.status_message = "Max tabs reached (9)".to_string(); } return Ok(()); } (KeyCode::Char('w'), KeyModifiers::CONTROL) => { if self.tabs.count() > 1 { self.tabs.close_current_tab(); self.status_message = "Tab closed".to_string(); } else { self.status_message = "Can't close last tab".to_string(); } return Ok(()); } (KeyCode::Char(n), KeyModifiers::ALT) if n.is_ascii_digit() => { let num = n.to_digit(10).unwrap_or(0) as usize; if num > 0 { self.tabs.go_to_tab(num); self.status_message = format!("Switched to tab {}", num); } return Ok(()); } (KeyCode::Char('['), KeyModifiers::CONTROL) => { self.tabs.prev_tab(); self.status_message = format!("Tab {}/{}", self.tabs.active_index() + 1, self.tabs.count()); return Ok(()); } (KeyCode::Char(']'), KeyModifiers::CONTROL) => { self.tabs.next_tab(); self.status_message = format!("Tab {}/{}", self.tabs.active_index() + 1, self.tabs.count()); return Ok(()); } // Bookmarks & History (KeyCode::Char('b'), KeyModifiers::NONE) if self.focus != Focus::URLBar => { self.panel_mode = PanelMode::Bookmarks; self.refresh_bookmarks(); self.bookmarks_selected = 0; return Ok(()); } (KeyCode::Char('H'), KeyModifiers::NONE) => { self.panel_mode = PanelMode::History; self.refresh_history(); self.history_selected = 0; return Ok(()); } (KeyCode::Char('B'), KeyModifiers::NONE) => { self.toggle_bookmark();Lines 1–500 of 1828. Load more
use anyhow::Result; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use serde::{Deserialize, Serialize}; use std::sync::mpsc::{channel, Receiver, Sender}; use std::sync::Arc; use std::time::Duration; use crate::ai::Summarizer; use crate::browser::{Browser, Page, RenderMode}; use crate::chat::ChatMessage; use crate::config::Config; use crate::mascot::{Mascot, MascotState}; use crate::search::{self, QueryTarget, SearchManager}; use crate::scrape; use crate::storage::{Bookmark, Database, HistoryEntry}; use crate::tabs::TabManager; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Focus { Content, Sidebar, URLBar, TabBar, Bookmarks, History, } impl Focus { pub fn next(&self) -> Self { match self { Focus::Content => Focus::Sidebar, Focus::Sidebar => Focus::URLBar, Focus::URLBar => Focus::Content, Focus::TabBar => Focus::Content, Focus::Bookmarks => Focus::Content, Focus::History => Focus::Content, } } pub fn previous(&self) -> Self { match self { Focus::Content => Focus::URLBar, Focus::Sidebar => Focus::Content, Focus::URLBar => Focus::Sidebar, Focus::TabBar => Focus::Content, Focus::Bookmarks => Focus::Content, Focus::History => Focus::Content, } } } pub enum AppMessage { PageLoaded(usize, Page), // tab_id, page LoadError(usize, String), AiSummary(String), AiError(String), ChatResponse(String), ToolAction(ToolAction), RagResponse(Vec<String>), RagError(String), } /// Actions that can be triggered by AI tool calls #[derive(Debug, Clone)] pub enum ToolAction { Navigate(String), FollowLink(usize), Scroll { direction: String, amount: usize }, } /// Panel mode for overlays #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PanelMode { None, Bookmarks, History, Help, Chat, Settings, Rag, Memory, } pub struct App { pub config: Config, pub focus: Focus, pub should_quit: bool, pub animation_tick: usize, pub status_message: String, pub view_mode: ViewMode, pub format_text: bool, pub ai_summary: Option<String>, pub panel_mode: PanelMode, // Tabs pub tabs: TabManager, pub url_input: String, // Mascot pub mascot: Mascot, // Storage pub db: Option<Database>, pub bookmarks_list: Vec<Bookmark>, pub history_list: Vec<HistoryEntry>, pub bookmarks_selected: usize, pub history_selected: usize, // Chat pub chat_session: Option<crate::chat::ChatSession>, pub chat_input: String, pub chat_selected_model: usize, pub chat_scroll: usize, pub chat_focused: bool, // Settings pub settings_selected: usize, pub settings_model_input: String, pub settings_editing_model: bool, // RAG & Memory pub rag_client: Option<crate::rag::RagClient>, pub rag_results: Vec<String>, pub rag_query: String, pub rag_scroll: usize, pub rag_loading: bool, pub memory_client: Option<crate::memory::MemoryClient>, pub memory_nodes: Vec<String>, // Message passing page_rx: Receiver<AppMessage>, page_tx: Sender<AppMessage>, summarizer: Option<Arc<Summarizer>>, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ViewMode { Rendered, Raw, } impl App { pub fn new() -> Result<Self> { let config = Config::load()?; let (page_tx, page_rx) = channel(); // Try to open database, but don't fail if it errors let db = Database::open().ok(); // Initialize chat session with available models let chat_session = if let Some(api_key) = config.get_api_key() { if !api_key.is_empty() { let model = config.get_ai_model().unwrap_or("qwen/qwen3-235b-a22b:free").to_string(); let mut models = vec![model.clone()]; if let Some(fallback) = config.get_fallback_models() { models.extend(fallback); } Some(crate::chat::ChatSession::new(model, models)) } else { None } } else { None }; // Initialize RAG and memory clients let rag_client = crate::rag::RagClient::from_config(&config); let memory_client = crate::memory::MemoryClient::from_config(&config); let mut app = Self { config, focus: Focus::Content, should_quit: false, animation_tick: 0, status_message: "Ready - / search | t new tab | b bookmarks | c chat | r rag | m memory | ? help".to_string(), view_mode: ViewMode::Rendered, format_text: true, ai_summary: None, panel_mode: PanelMode::None, // Chat is always visible, not a panel mode tabs: TabManager::new(), url_input: String::new(), mascot: Mascot::new(), db, bookmarks_list: Vec::new(), history_list: Vec::new(), bookmarks_selected: 0, history_selected: 0, chat_session, chat_input: String::new(), chat_selected_model: 0, chat_scroll: 0, chat_focused: false, // Start in content mode, press 'c' or '3' for chat settings_selected: 0, settings_model_input: String::new(), settings_editing_model: false, rag_client, rag_results: Vec::new(), rag_query: String::new(), rag_scroll: 0, rag_loading: false, memory_client, memory_nodes: Vec::new(), page_rx, page_tx, summarizer: Summarizer::from_env().map(Arc::new), }; // Load bookmarks and history app.refresh_bookmarks(); app.refresh_history(); Ok(app) } // ═══════════════════════════════════════════════════════════════════════════ // CONVENIENCE ACCESSORS // ═══════════════════════════════════════════════════════════════════════════ /// Get current page from active tab pub fn current_page(&self) -> Option<&Page> { self.tabs.active_tab().and_then(|t| t.page.as_ref()) } /// Get current scroll offset from active tab pub fn scroll_offset(&self) -> usize { self.tabs.active_tab().map(|t| t.scroll_offset).unwrap_or(0) } /// Set scroll offset on active tab fn set_scroll_offset(&mut self, offset: usize) { if let Some(tab) = self.tabs.active_tab_mut() { tab.scroll_offset = offset; } } /// Get sidebar selected from active tab pub fn sidebar_selected(&self) -> usize { self.tabs.active_tab().map(|t| t.sidebar_selected).unwrap_or(0) } /// Set sidebar selected on active tab fn set_sidebar_selected(&mut self, idx: usize) { if let Some(tab) = self.tabs.active_tab_mut() { tab.sidebar_selected = idx; } } /// Check if current tab is loading pub fn loading(&self) -> bool { self.tabs.active_tab().map(|t| t.loading).unwrap_or(false) } /// Get current render mode from active tab pub fn render_mode(&self) -> RenderMode { self.tabs.active_tab().map(|t| t.render_mode).unwrap_or(RenderMode::Auto) } // ═══════════════════════════════════════════════════════════════════════════ // UPDATE LOOP // ═══════════════════════════════════════════════════════════════════════════ pub fn update(&mut self) { self.animation_tick = self.animation_tick.wrapping_add(1); self.mascot.tick(); // Check for loaded pages while let Ok(msg) = self.page_rx.try_recv() { match msg { AppMessage::PageLoaded(tab_id, page) => { // Find and update the tab for i in 0..self.tabs.count() { if let Some(tab) = self.tabs.get_tab_mut(i) { if tab.id == tab_id { // Add to history if let Some(db) = &self.db { let _ = db.add_history(&page.url, &page.title); } tab.set_page(page); self.mascot.set_state(MascotState::Success); self.status_message = format!("Loaded: {}", tab.url); break; } } } self.ai_summary = None; } AppMessage::LoadError(tab_id, err) => { for i in 0..self.tabs.count() { if let Some(tab) = self.tabs.get_tab_mut(i) { if tab.id == tab_id { tab.set_error(err.clone()); self.mascot.set_state(MascotState::Error); self.status_message = format!("Error: {}", err); break; } } } } AppMessage::AiSummary(summary) => { self.ai_summary = Some(summary); self.status_message = "AI summary ready".to_string(); self.mascot.set_state(MascotState::Success); } AppMessage::AiError(err) => { self.status_message = format!("AI error: {}", err); self.mascot.set_state(MascotState::Error); // Also show error in chat so it's visible if let Some(session) = &mut self.chat_session { session.add_assistant_message(format!("❌ Error: {}", err)); self.chat_scroll = 0; } } AppMessage::ChatResponse(response) => { if let Some(session) = &mut self.chat_session { session.add_assistant_message(response); self.chat_scroll = 0; // Auto-scroll to bottom (newest messages) self.mascot.set_state(MascotState::Success); self.status_message = "Response received".to_string(); } } AppMessage::ToolAction(action) => { match action { ToolAction::Navigate(url) => { self.url_input = url; self.navigate(); self.status_message = "AI navigated to URL".to_string(); } ToolAction::FollowLink(idx) => { if let Some(page) = self.current_page() { if let Some(link) = page.links.get(idx) { self.url_input = link.url.clone(); self.navigate(); self.status_message = format!("AI followed link {}", idx + 1); } } } ToolAction::Scroll { direction, amount } => { if let Some(tab) = self.tabs.active_tab_mut() { match direction.as_str() { "up" => tab.scroll_offset = tab.scroll_offset.saturating_sub(amount), "down" => tab.scroll_offset += amount, "top" => tab.scroll_offset = 0, "bottom" => tab.scroll_offset = usize::MAX / 2, _ => {} } self.status_message = format!("AI scrolled {}", direction); } } } } AppMessage::RagResponse(results) => { self.rag_loading = false; self.rag_results = results; self.rag_scroll = 0; self.mascot.set_state(MascotState::Success); self.status_message = format!("RAG: {} results", self.rag_results.len()); } AppMessage::RagError(err) => { self.rag_loading = false; self.rag_results = vec![format!("Error: {}", err)]; self.mascot.set_state(MascotState::Error); self.status_message = format!("RAG error: {}", err); } } } } pub fn quit(&mut self) { self.should_quit = true; } pub fn focus_next(&mut self) { self.focus = self.focus.next(); self.update_status(); } pub fn focus_previous(&mut self) { self.focus = self.focus.previous(); self.update_status(); } // ═══════════════════════════════════════════════════════════════════════════ // KEY HANDLING // ═══════════════════════════════════════════════════════════════════════════ pub fn handle_key(&mut self, key: KeyEvent) -> Result<()> { // Overlay panel handling first (these take full focus) match self.panel_mode { PanelMode::Bookmarks => return self.handle_bookmarks_keys(key), PanelMode::History => return self.handle_history_keys(key), PanelMode::Help => { // Any key closes help self.panel_mode = PanelMode::None; return Ok(()); } PanelMode::Settings => return self.handle_settings_keys(key), PanelMode::Rag => return self.handle_rag_keys(key), PanelMode::Memory => return self.handle_memory_keys(key), PanelMode::Chat | PanelMode::None => {} // Chat is always visible, handled below } // Chat input handling when chat is focused if self.chat_focused { match (key.code, key.modifiers) { // Esc unfocuses chat (KeyCode::Esc, _) => { self.chat_focused = false; self.status_message = "Content mode - 'c' for chat, '/' for URL".to_string(); return Ok(()); } // These pass through to global shortcuts (KeyCode::Tab, _) | (KeyCode::BackTab, _) => {} // Handle chat input _ => return self.handle_chat_keys(key), } } // Global shortcuts match (key.code, key.modifiers) { (KeyCode::Char('q'), KeyModifiers::NONE) | (KeyCode::Char('Q'), KeyModifiers::NONE) => { if self.focus != Focus::URLBar { self.quit(); return Ok(()); } } (KeyCode::Char('c'), KeyModifiers::CONTROL) => { self.quit(); return Ok(()); } (KeyCode::Char('?'), KeyModifiers::NONE) => { self.panel_mode = PanelMode::Help; return Ok(()); } (KeyCode::Tab, KeyModifiers::NONE) => { self.focus_next(); return Ok(()); } (KeyCode::BackTab, _) => { self.focus_previous(); return Ok(()); } (KeyCode::Char('/'), KeyModifiers::NONE) => { if self.focus != Focus::URLBar { self.focus = Focus::URLBar; self.url_input.clear(); self.update_status(); return Ok(()); } } // Tab management (KeyCode::Char('t'), KeyModifiers::NONE) if self.focus != Focus::URLBar => { if self.tabs.new_tab(None).is_some() { self.status_message = format!("New tab {} created", self.tabs.count()); } else { self.status_message = "Max tabs reached (9)".to_string(); } return Ok(()); } (KeyCode::Char('w'), KeyModifiers::CONTROL) => { if self.tabs.count() > 1 { self.tabs.close_current_tab(); self.status_message = "Tab closed".to_string(); } else { self.status_message = "Can't close last tab".to_string(); } return Ok(()); } (KeyCode::Char(n), KeyModifiers::ALT) if n.is_ascii_digit() => { let num = n.to_digit(10).unwrap_or(0) as usize; if num > 0 { self.tabs.go_to_tab(num); self.status_message = format!("Switched to tab {}", num); } return Ok(()); } (KeyCode::Char('['), KeyModifiers::CONTROL) => { self.tabs.prev_tab(); self.status_message = format!("Tab {}/{}", self.tabs.active_index() + 1, self.tabs.count()); return Ok(()); } (KeyCode::Char(']'), KeyModifiers::CONTROL) => { self.tabs.next_tab(); self.status_message = format!("Tab {}/{}", self.tabs.active_index() + 1, self.tabs.count()); return Ok(()); } // Bookmarks & History (KeyCode::Char('b'), KeyModifiers::NONE) if self.focus != Focus::URLBar => { self.panel_mode = PanelMode::Bookmarks; self.refresh_bookmarks(); self.bookmarks_selected = 0; return Ok(()); } (KeyCode::Char('H'), KeyModifiers::NONE) => { self.panel_mode = PanelMode::History; self.refresh_history(); self.history_selected = 0; return Ok(()); } (KeyCode::Char('B'), KeyModifiers::NONE) => { self.toggle_bookmark();