Initialize Rust GPUI browser shell
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
use crate::{DomainError, UrlText};
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum CommandScope {
|
||||
Tabs,
|
||||
Bookmarks,
|
||||
History,
|
||||
Settings,
|
||||
Plugins,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum CommandIntent {
|
||||
Navigate(UrlText),
|
||||
Search(String),
|
||||
Command(String),
|
||||
ScopedSearch { scope: CommandScope, query: String },
|
||||
}
|
||||
|
||||
impl CommandIntent {
|
||||
pub fn parse(input: &str) -> Result<Self, DomainError> {
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(DomainError::InvalidCommand);
|
||||
}
|
||||
|
||||
if let Some(command) = trimmed.strip_prefix('>') {
|
||||
return non_empty_text(command).map(Self::Command);
|
||||
}
|
||||
|
||||
if let Some(query) = trimmed.strip_prefix('?') {
|
||||
return non_empty_text(query).map(Self::Search);
|
||||
}
|
||||
|
||||
if let Some((scope, query)) = parse_scope(trimmed) {
|
||||
return non_empty_text(query).map(|query| Self::ScopedSearch { scope, query });
|
||||
}
|
||||
|
||||
UrlText::from_address_text(trimmed).map(Self::Navigate)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_scope(value: &str) -> Option<(CommandScope, &str)> {
|
||||
let (scope, query) = value.split_once(' ')?;
|
||||
let scope = match scope {
|
||||
"@tabs" => CommandScope::Tabs,
|
||||
"@bookmarks" => CommandScope::Bookmarks,
|
||||
"@history" => CommandScope::History,
|
||||
"@settings" => CommandScope::Settings,
|
||||
"@plugins" => CommandScope::Plugins,
|
||||
_ => return None,
|
||||
};
|
||||
Some((scope, query))
|
||||
}
|
||||
|
||||
fn non_empty_text(value: &str) -> Result<String, DomainError> {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(DomainError::InvalidCommand);
|
||||
}
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Debug, Error, Eq, PartialEq)]
|
||||
pub enum DomainError {
|
||||
#[error("{field} cannot be empty")]
|
||||
EmptyField { field: &'static str },
|
||||
|
||||
#[error("invalid URL: {value}")]
|
||||
InvalidUrl { value: String },
|
||||
|
||||
#[error("invalid command query")]
|
||||
InvalidCommand,
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use std::fmt;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
macro_rules! entity_id {
|
||||
($name:ident, $prefix:literal) => {
|
||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct $name(String);
|
||||
|
||||
impl $name {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self(format!("{}_{}", $prefix, Uuid::now_v7().simple()))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for $name {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for $name {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
entity_id!(TabId, "tab");
|
||||
entity_id!(SpaceId, "space");
|
||||
entity_id!(ProfileId, "profile");
|
||||
entity_id!(SplitId, "split");
|
||||
entity_id!(WebViewId, "webview");
|
||||
@@ -0,0 +1,17 @@
|
||||
mod command;
|
||||
mod error;
|
||||
mod identifiers;
|
||||
mod profile;
|
||||
mod space;
|
||||
mod split;
|
||||
mod tab;
|
||||
mod url_text;
|
||||
|
||||
pub use command::{CommandIntent, CommandScope};
|
||||
pub use error::DomainError;
|
||||
pub use identifiers::{ProfileId, SpaceId, SplitId, TabId, WebViewId};
|
||||
pub use profile::{Profile, ProfileKind};
|
||||
pub use space::{ArchivePolicy, Space};
|
||||
pub use split::{SplitAxis, SplitLayout, SplitPane};
|
||||
pub use tab::{BrowserTab, TabFlags, TabState};
|
||||
pub use url_text::UrlText;
|
||||
@@ -0,0 +1,42 @@
|
||||
use crate::ProfileId;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ProfileKind {
|
||||
Standard,
|
||||
Private,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Profile {
|
||||
id: ProfileId,
|
||||
name: String,
|
||||
color_hex: u32,
|
||||
kind: ProfileKind,
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
#[must_use]
|
||||
pub fn new(name: impl Into<String>, color_hex: u32, kind: ProfileKind) -> Self {
|
||||
Self { id: ProfileId::new(), name: name.into(), color_hex, kind }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn id(&self) -> &ProfileId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn color_hex(&self) -> u32 {
|
||||
self.color_hex
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn kind(&self) -> &ProfileKind {
|
||||
&self.kind
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use crate::SpaceId;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ArchivePolicy {
|
||||
Manual,
|
||||
IdleDays(u16),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Space {
|
||||
id: SpaceId,
|
||||
name: String,
|
||||
icon: String,
|
||||
accent_hex: u32,
|
||||
archive_policy: ArchivePolicy,
|
||||
}
|
||||
|
||||
impl Space {
|
||||
#[must_use]
|
||||
pub fn new(name: impl Into<String>, icon: impl Into<String>, accent_hex: u32) -> Self {
|
||||
Self {
|
||||
id: SpaceId::new(),
|
||||
name: name.into(),
|
||||
icon: icon.into(),
|
||||
accent_hex,
|
||||
archive_policy: ArchivePolicy::Manual,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn id(&self) -> &SpaceId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn icon(&self) -> &str {
|
||||
&self.icon
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn accent_hex(&self) -> u32 {
|
||||
self.accent_hex
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn archive_policy(&self) -> &ArchivePolicy {
|
||||
&self.archive_policy
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use crate::{SplitId, TabId};
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum SplitAxis {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
Grid,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SplitPane {
|
||||
tab_id: TabId,
|
||||
weight: u16,
|
||||
}
|
||||
|
||||
impl SplitPane {
|
||||
#[must_use]
|
||||
pub fn new(tab_id: TabId, weight: u16) -> Self {
|
||||
Self { tab_id, weight }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn tab_id(&self) -> &TabId {
|
||||
&self.tab_id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn weight(&self) -> u16 {
|
||||
self.weight
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SplitLayout {
|
||||
id: SplitId,
|
||||
axis: SplitAxis,
|
||||
panes: Vec<SplitPane>,
|
||||
}
|
||||
|
||||
impl SplitLayout {
|
||||
#[must_use]
|
||||
pub fn new(axis: SplitAxis, panes: Vec<SplitPane>) -> Self {
|
||||
Self { id: SplitId::new(), axis, panes }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn id(&self) -> &SplitId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn axis(&self) -> &SplitAxis {
|
||||
&self.axis
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn panes(&self) -> &[SplitPane] {
|
||||
&self.panes
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
use crate::{ProfileId, SpaceId, SplitId, TabId, UrlText};
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum TabState {
|
||||
Loading,
|
||||
Ready,
|
||||
Crashed,
|
||||
Discarded,
|
||||
Archived,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct TabFlags {
|
||||
pub pinned: bool,
|
||||
pub favorite: bool,
|
||||
pub muted: bool,
|
||||
pub unread: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct BrowserTab {
|
||||
id: TabId,
|
||||
space_id: SpaceId,
|
||||
profile_id: ProfileId,
|
||||
title: String,
|
||||
url: UrlText,
|
||||
state: TabState,
|
||||
flags: TabFlags,
|
||||
split_id: Option<SplitId>,
|
||||
}
|
||||
|
||||
impl BrowserTab {
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
id: TabId,
|
||||
space_id: SpaceId,
|
||||
profile_id: ProfileId,
|
||||
title: impl Into<String>,
|
||||
url: UrlText,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
space_id,
|
||||
profile_id,
|
||||
title: title.into(),
|
||||
url,
|
||||
state: TabState::Ready,
|
||||
flags: TabFlags::default(),
|
||||
split_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn id(&self) -> &TabId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn space_id(&self) -> &SpaceId {
|
||||
&self.space_id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn profile_id(&self) -> &ProfileId {
|
||||
&self.profile_id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn title(&self) -> &str {
|
||||
&self.title
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn url(&self) -> &UrlText {
|
||||
&self.url
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn state(&self) -> &TabState {
|
||||
&self.state
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn flags(&self) -> &TabFlags {
|
||||
&self.flags
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn split_id(&self) -> Option<&SplitId> {
|
||||
self.split_id.as_ref()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use std::fmt;
|
||||
|
||||
use url::Url;
|
||||
|
||||
use crate::DomainError;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct UrlText {
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl UrlText {
|
||||
pub fn parse(value: impl Into<String>) -> Result<Self, DomainError> {
|
||||
let value = value.into();
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(DomainError::EmptyField { field: "url" });
|
||||
}
|
||||
|
||||
Url::parse(trimmed).map_err(|_| DomainError::InvalidUrl { value: trimmed.to_string() })?;
|
||||
|
||||
Ok(Self { value: trimmed.to_string() })
|
||||
}
|
||||
|
||||
pub fn from_address_text(value: &str) -> Result<Self, DomainError> {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(DomainError::EmptyField { field: "url" });
|
||||
}
|
||||
|
||||
if Url::parse(trimmed).is_ok() {
|
||||
return Self::parse(trimmed);
|
||||
}
|
||||
|
||||
if trimmed.contains('.') && !trimmed.contains(char::is_whitespace) {
|
||||
return Self::parse(format!("https://{trimmed}"));
|
||||
}
|
||||
|
||||
Err(DomainError::InvalidUrl { value: trimmed.to_string() })
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.value
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn display_host(&self) -> String {
|
||||
Url::parse(&self.value)
|
||||
.ok()
|
||||
.and_then(|url| url.host_str().map(str::to_string))
|
||||
.unwrap_or_else(|| self.value.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for UrlText {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.value)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user