use std::collections::{BTreeMap, HashMap};
use crate::core::bindings::default_bindings;
use crate::core::command::HintTarget;
use crate::core::completion::CompletionState;
use crate::core::key::Key;
use crate::core::msg::{JsPurpose, RequestId};
use crate::core::trie::BindingTrie;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Normal,
Insert,
Command,
Hint,
}
#[derive(Debug, Clone, Copy)]
pub struct ModeState {
pub current: Mode,
pub previous: Mode,
}
impl Default for ModeState {
fn default() -> Self {
Self {
current: Mode::Normal,
previous: Mode::Normal,
}
}
}
impl ModeState {
pub fn enter(&mut self, mode: Mode) {
if mode != self.current {
self.previous = self.current;
self.current = mode;
}
}
pub fn leave(&mut self) {
self.previous = self.current;
self.current = Mode::Normal;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TabId(pub u64);
#[derive(Debug, Clone)]
pub struct Tab {
pub id: TabId,
pub url: String,
pub title: String,
pub loading: bool,
pub progress: f64,
pub crashed: bool,
}
impl Tab {
fn new(id: TabId, url: &str) -> Self {
Self {
id,
url: url.to_string(),
title: String::new(),
loading: false,
progress: 0.0,
crashed: false,
}
}
}
#[derive(Debug, Clone)]
pub struct ClosedTab {
pub url: String,
}
const UNDO_LIMIT: usize = 100;
#[derive(Debug, Default)]
pub struct Tabs {
tabs: Vec<Tab>,
active: usize,
next_id: u64,
undo_stack: Vec<ClosedTab>,
}
impl Tabs {
pub fn open(&mut self, url: &str) -> TabId {
let id = TabId(self.next_id);
self.next_id += 1;
self.tabs.push(Tab::new(id, url));
id
}
pub fn len(&self) -> usize {
self.tabs.len()
}
pub fn active(&self) -> Option<&Tab> {
self.tabs.get(self.active)
}
pub fn active_id(&self) -> Option<TabId> {
self.active().map(|t| t.id)
}
pub fn active_index(&self) -> usize {
self.active
}
pub fn get_mut(&mut self, id: TabId) -> Option<&mut Tab> {
self.tabs.iter_mut().find(|t| t.id == id)
}
pub fn focus_last(&mut self) {
if !self.tabs.is_empty() {
self.active = self.tabs.len() - 1;
}
}
pub fn focus_index_1based(&mut self, index: usize) -> Option<TabId> {
let idx = index.checked_sub(1)?;
let tab = self.tabs.get(idx)?;
self.active = idx;
Some(tab.id)
}
pub fn next(&mut self, count: u32) -> Option<TabId> {
if self.tabs.is_empty() {
return None;
}
self.active = (self.active + count as usize) % self.tabs.len();
self.active_id()
}
pub fn prev(&mut self, count: u32) -> Option<TabId> {
if self.tabs.is_empty() {
return None;
}
let len = self.tabs.len();
let back = (count as usize) % len;
self.active = (self.active + len - back) % len;
self.active_id()
}
pub fn close_active(&mut self) -> Option<(TabId, Option<TabId>)> {
if self.tabs.is_empty() {
return None;
}
let closed = self.tabs.remove(self.active);
self.push_undo(&closed);
if self.active >= self.tabs.len() && !self.tabs.is_empty() {
self.active = self.tabs.len() - 1;
}
let next = self.active_id();
Some((closed.id, next))
}
pub fn close_others(&mut self) -> Vec<TabId> {
if self.tabs.len() < 2 {
return Vec::new();
}
let kept = self.tabs.swap_remove(self.active);
let removed = std::mem::take(&mut self.tabs);
let closed_ids = removed.iter().map(|t| t.id).collect();
for tab in &removed {
self.push_undo(tab);
}
self.tabs = vec![kept];
self.active = 0;
closed_ids
}
pub fn move_active(&mut self, delta: i32) -> bool {
let len = self.tabs.len();
if len < 2 {
return false;
}
let target = (self.active as i32 + delta).clamp(0, len as i32 - 1) as usize;
if target == self.active {
return false;
}
let tab = self.tabs.remove(self.active);
self.tabs.insert(target, tab);
self.active = target;
true
}
pub fn undo(&mut self) -> Option<ClosedTab> {
self.undo_stack.pop()
}
pub fn urls(&self) -> Vec<String> {
self.tabs.iter().map(|t| t.url.clone()).collect()
}
fn push_undo(&mut self, tab: &Tab) {
self.undo_stack.push(ClosedTab {
url: tab.url.clone(),
});
if self.undo_stack.len() > UNDO_LIMIT {
self.undo_stack.remove(0);
}
}
}
#[derive(Debug, Default)]
pub struct CommandLine {
pub text: String,
pub active: bool,
}
#[derive(Debug, Default)]
pub struct InputState {
pub pending: Vec<Key>,
pub count: String,
}
#[derive(Debug, Default)]
pub struct StatusLine {
pub scroll_percent: Option<u8>,
}
#[derive(Debug, Default)]
pub struct HintState {
pub target: HintTarget,
pub labels: Vec<String>,
pub input: String,
}
impl HintState {
pub fn reset(&mut self) {
self.labels.clear();
self.input.clear();
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Bookmark {
pub url: String,
pub title: String,
}
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(default)]
pub struct Colors {
pub background: String,
pub foreground: String,
pub accent: String,
}
impl Default for Colors {
fn default() -> Self {
Self {
background: "#1a1a2e".to_string(),
foreground: "#e0e0e0".to_string(),
accent: "#ffd76e".to_string(),
}
}
}
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(default)]
pub struct Font {
pub family: String,
pub size: u32,
}
impl Default for Font {
fn default() -> Self {
Self {
family: "monospace".to_string(),
size: 11,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PermissionPolicy {
Ask,
Allow,
#[default]
Deny,
}
impl PermissionPolicy {
pub fn parse(value: &str) -> Result<Self, String> {
match value {
"ask" => Ok(Self::Ask),
"allow" => Ok(Self::Allow),
"deny" => Ok(Self::Deny),
other => Err(format!("invalid permission policy: {other}")),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Deserialize)]
#[serde(default)]
pub struct Permissions {
pub default: PermissionPolicy,
pub sites: BTreeMap<String, PermissionPolicy>,
}
impl Permissions {
pub fn policy_for(&self, host: &str) -> PermissionPolicy {
self.sites
.iter()
.find(|(site, _)| host == site.as_str() || host.ends_with(&format!(".{site}")))
.map(|(_, p)| *p)
.unwrap_or(self.default)
}
}
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(default)]
pub struct Config {
pub homepage: String,
pub colors: Colors,
pub font: Font,
pub permissions: Permissions,
}
impl Default for Config {
fn default() -> Self {
Self {
homepage: "https://duckduckgo.com".to_string(),
colors: Colors::default(),
font: Font::default(),
permissions: Permissions::default(),
}
}
}
impl Config {
pub fn set(&mut self, key: &str, value: &str) -> Result<(), String> {
match key {
"homepage" | "general.homepage" => self.homepage = value.to_string(),
"colors.background" => self.colors.background = value.to_string(),
"colors.foreground" => self.colors.foreground = value.to_string(),
"colors.accent" => self.colors.accent = value.to_string(),
"font.family" => self.font.family = value.to_string(),
"font.size" => {
self.font.size = value
.parse()
.map_err(|_| format!("invalid font.size: {value}"))?
}
"permissions.default" => self.permissions.default = PermissionPolicy::parse(value)?,
key if key.starts_with("permissions.") => {
let host = &key["permissions.".len()..];
self.permissions
.sites
.insert(host.to_string(), PermissionPolicy::parse(value)?);
}
_ => return Err(format!("unknown setting: {key}")),
}
Ok(())
}
}
#[derive(Debug, Default)]
pub struct State {
pub mode: ModeState,
pub tabs: Tabs,
pub input: InputState,
pub command_line: CommandLine,
pub status: StatusLine,
pub hints: HintState,
pub completion: CompletionState,
pub quickmarks: BTreeMap<String, String>,
pub bookmarks: Vec<Bookmark>,
pub config: Config,
pub bindings: BindingTrie,
pub pending_js: HashMap<RequestId, JsPurpose>,
next_request_id: u64,
pub dark_mode: bool,
pub running: bool,
}
impl State {
pub fn new(config: Config) -> Self {
Self {
config,
bindings: default_bindings(),
running: true,
..Self::default()
}
}
pub fn alloc_request_id(&mut self) -> RequestId {
let id = RequestId(self.next_request_id);
self.next_request_id += 1;
id
}
}