use std::collections::{BTreeMap, HashMap, VecDeque};
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,
Prompt,
Permissions,
}
#[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,
pub zoom: f64,
}
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,
zoom: 1.0,
}
}
}
#[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 get(&self, id: TabId) -> Option<&Tab> {
self.tabs.iter().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>,
pub search: Option<SearchStatus>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SearchStatus {
pub total: Option<usize>,
}
impl SearchStatus {
pub fn label(&self) -> String {
match self.total {
None => "searching".to_string(),
Some(0) => "no matches".to_string(),
Some(1) => "1 match".to_string(),
Some(n) => format!("{n} matches"),
}
}
}
#[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, serde::Deserialize)]
#[serde(default)]
pub struct Zoom {
pub default: f64,
}
impl Default for Zoom {
fn default() -> Self {
Self { default: 1.0 }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PermissionPolicy {
#[default]
Ask,
Allow,
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, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Capability {
Geolocation,
Notifications,
Camera,
Microphone,
}
impl Capability {
pub const ALL: [Capability; 4] = [
Capability::Geolocation,
Capability::Notifications,
Capability::Camera,
Capability::Microphone,
];
pub fn as_str(self) -> &'static str {
match self {
Capability::Geolocation => "geolocation",
Capability::Notifications => "notifications",
Capability::Camera => "camera",
Capability::Microphone => "microphone",
}
}
pub fn parse(value: &str) -> Option<Self> {
Capability::ALL.into_iter().find(|c| c.as_str() == value)
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum SiteRules {
All(PermissionPolicy),
PerCapability(BTreeMap<Capability, PermissionPolicy>),
}
impl SiteRules {
fn get(&self, cap: Capability) -> Option<PermissionPolicy> {
match self {
SiteRules::All(p) => Some(*p),
SiteRules::PerCapability(m) => m.get(&cap).copied(),
}
}
fn set(&mut self, cap: Capability, policy: PermissionPolicy) {
if let SiteRules::All(p) = *self {
let mut m: BTreeMap<Capability, PermissionPolicy> =
Capability::ALL.into_iter().map(|c| (c, p)).collect();
m.insert(cap, policy);
*self = SiteRules::PerCapability(m);
} else if let SiteRules::PerCapability(m) = self {
m.insert(cap, policy);
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct Permissions {
pub default: PermissionPolicy,
pub sites: BTreeMap<String, SiteRules>,
}
impl Permissions {
pub fn policy_for(&self, host: &str, cap: Capability) -> PermissionPolicy {
for (site, rules) in &self.sites {
if (host == site.as_str() || host.ends_with(&format!(".{site}")))
&& let Some(p) = rules.get(cap)
{
return p;
}
}
self.default
}
pub fn set_capability(&mut self, host: &str, cap: Capability, policy: PermissionPolicy) {
self.sites
.entry(host.to_string())
.or_insert_with(|| SiteRules::PerCapability(BTreeMap::new()))
.set(cap, policy);
}
pub fn set_all(&mut self, host: &str, policy: PermissionPolicy) {
self.sites.insert(host.to_string(), SiteRules::All(policy));
}
pub fn rows(&self) -> Vec<PermissionRow> {
let mut rows = Vec::new();
for (host, rules) in &self.sites {
match rules {
SiteRules::All(p) => rows.push(PermissionRow {
host: host.clone(),
capability: None,
policy: *p,
}),
SiteRules::PerCapability(m) => {
for (cap, p) in m {
rows.push(PermissionRow {
host: host.clone(),
capability: Some(*cap),
policy: *p,
});
}
}
}
}
rows
}
pub fn set_row(&mut self, row: &PermissionRow, policy: PermissionPolicy) {
match row.capability {
Some(cap) => self.set_capability(&row.host, cap, policy),
None => self.set_all(&row.host, policy),
}
}
pub fn revoke_row(&mut self, row: &PermissionRow) {
match row.capability {
None => {
self.sites.remove(&row.host);
}
Some(cap) => {
if let Some(SiteRules::PerCapability(m)) = self.sites.get_mut(&row.host) {
m.remove(&cap);
if m.is_empty() {
self.sites.remove(&row.host);
}
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PermissionRow {
pub host: String,
pub capability: Option<Capability>,
pub policy: PermissionPolicy,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PermissionPrompt {
pub id: u64,
pub host: String,
pub capability: Capability,
}
#[derive(Debug, Default)]
pub struct PermissionViewState {
pub rows: Vec<PermissionRow>,
pub selected: usize,
}
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(default)]
pub struct Config {
pub homepage: String,
pub colors: Colors,
pub font: Font,
pub zoom: Zoom,
pub permissions: Permissions,
}
impl Default for Config {
fn default() -> Self {
Self {
homepage: "https://duckduckgo.com".to_string(),
colors: Colors::default(),
font: Font::default(),
zoom: Zoom::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}"))?
}
"zoom.default" => {
self.zoom.default = value
.parse()
.map_err(|_| format!("invalid zoom.default: {value}"))?
}
"permissions.default" => self.permissions.default = PermissionPolicy::parse(value)?,
key if key.starts_with("permissions.") => {
let rest = &key["permissions.".len()..];
let policy = PermissionPolicy::parse(value)?;
match rest.rsplit_once('.').and_then(|(host, last)| {
Capability::parse(last).map(|cap| (host, cap))
}) {
Some((host, cap)) => self.permissions.set_capability(host, cap, policy),
None => self.permissions.set_all(rest, policy),
}
}
_ => 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 prompts: VecDeque<PermissionPrompt>,
pub perm_view: PermissionViewState,
pub last_search: Option<Search>,
pub downloads: BTreeMap<u64, String>,
pub running: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Search {
pub text: String,
}
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
}
}
#[cfg(test)]
mod permission_tests {
use super::*;
#[test]
fn capabilities_are_independent() {
let mut p = Permissions::default();
p.set_capability("example.com", Capability::Notifications, PermissionPolicy::Allow);
assert_eq!(
p.policy_for("example.com", Capability::Notifications),
PermissionPolicy::Allow
);
assert_eq!(
p.policy_for("example.com", Capability::Camera),
PermissionPolicy::default()
);
}
#[test]
fn set_all_applies_to_every_capability() {
let mut p = Permissions::default();
p.set_all("example.com", PermissionPolicy::Allow);
for cap in Capability::ALL {
assert_eq!(p.policy_for("example.com", cap), PermissionPolicy::Allow);
}
}
#[test]
fn revoke_reverts_to_default() {
let mut p = Permissions::default();
p.set_capability("example.com", Capability::Camera, PermissionPolicy::Allow);
let row = p.rows().into_iter().next().unwrap();
p.revoke_row(&row);
assert_eq!(
p.policy_for("example.com", Capability::Camera),
PermissionPolicy::default()
);
assert!(p.rows().is_empty());
}
#[test]
fn old_bare_string_config_parses_as_all() {
let toml = "default = \"ask\"\n[sites]\n\"example.com\" = \"allow\"\n";
let p: Permissions = toml::from_str(toml).unwrap();
assert_eq!(p.default, PermissionPolicy::Ask);
assert_eq!(
p.policy_for("example.com", Capability::Camera),
PermissionPolicy::Allow
);
}
#[test]
fn per_capability_config_parses() {
let toml =
"[sites]\n\"example.com\" = { geolocation = \"allow\", camera = \"deny\" }\n";
let p: Permissions = toml::from_str(toml).unwrap();
assert_eq!(
p.policy_for("example.com", Capability::Geolocation),
PermissionPolicy::Allow
);
assert_eq!(
p.policy_for("example.com", Capability::Camera),
PermissionPolicy::Deny
);
}
#[test]
fn permissions_round_trip_through_toml() {
let mut p = Permissions::default();
p.set_all("a.test", PermissionPolicy::Allow);
p.set_capability("b.test", Capability::Geolocation, PermissionPolicy::Deny);
let text = toml::to_string_pretty(&p).unwrap();
let back: Permissions = toml::from_str(&text).unwrap();
assert_eq!(p, back);
}
}