Initialize Rust GPUI browser shell

This commit is contained in:
2026-05-07 18:27:15 -04:00
commit d9a27f3dc4
41 changed files with 11573 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "ely_domain"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
[dependencies]
thiserror.workspace = true
url.workspace = true
uuid.workspace = true
[lints]
workspace = true
+62
View File
@@ -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())
}
+13
View File
@@ -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,
}
+40
View File
@@ -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");
+17
View File
@@ -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;
+42
View File
@@ -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
}
}
+54
View File
@@ -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
}
}
+60
View File
@@ -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
}
}
+92
View File
@@ -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()
}
}
+60
View File
@@ -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)
}
}