detached 8523928

feat(domain): add repository interfaces and domain entities

commit
8523928edad5f67d09e8e75bfa0ed5b1648a4e45
author
bnema · · 2025-12-09 21:32 UTC
parent
513db12
signature
unsigned
10 files · +765 -0
A internal/domain/entity/favorite.go +117 -0
@@ -0,0 +1,117 @@
1+package entity
2+
3+import "time"
4+
5+// FavoriteID uniquely identifies a favorite/bookmark.
6+type FavoriteID int64
7+
8+// FolderID uniquely identifies a bookmark folder.
9+type FolderID int64
10+
11+// TagID uniquely identifies a tag.
12+type TagID int64
13+
14+// Favorite represents a bookmarked URL.
15+type Favorite struct {
16+ ID FavoriteID
17+ URL string
18+ Title string
19+ FaviconURL string
20+ FolderID *FolderID // nil = root level
21+ ShortcutKey *int // 1-9 for quick access (Alt+1 through Alt+9)
22+ Position int // Order within folder
23+ Tags []Tag
24+ CreatedAt time.Time
25+ UpdatedAt time.Time
26+}
27+
28+// NewFavorite creates a new favorite for a URL.
29+func NewFavorite(url, title string) *Favorite {
30+ now := time.Now()
31+ return &Favorite{
32+ URL: url,
33+ Title: title,
34+ CreatedAt: now,
35+ UpdatedAt: now,
36+ }
37+}
38+
39+// HasShortcut returns true if this favorite has a keyboard shortcut.
40+func (f *Favorite) HasShortcut() bool {
41+ return f.ShortcutKey != nil && *f.ShortcutKey >= 1 && *f.ShortcutKey <= 9
42+}
43+
44+// InFolder returns true if this favorite is in a folder.
45+func (f *Favorite) InFolder() bool {
46+ return f.FolderID != nil
47+}
48+
49+// HasTag returns true if this favorite has the given tag.
50+func (f *Favorite) HasTag(tagID TagID) bool {
51+ for _, t := range f.Tags {
52+ if t.ID == tagID {
53+ return true
54+ }
55+ }
56+ return false
57+}
58+
59+// Folder represents a container for organizing favorites.
60+type Folder struct {
61+ ID FolderID
62+ Name string
63+ Icon string // Optional icon identifier
64+ ParentID *FolderID // nil = root level
65+ Position int // Order within parent
66+ CreatedAt time.Time
67+}
68+
69+// NewFolder creates a new folder.
70+func NewFolder(name string) *Folder {
71+ return &Folder{
72+ Name: name,
73+ CreatedAt: time.Now(),
74+ }
75+}
76+
77+// IsRoot returns true if this folder is at root level.
78+func (f *Folder) IsRoot() bool {
79+ return f.ParentID == nil
80+}
81+
82+// Tag represents a label that can be applied to favorites.
83+type Tag struct {
84+ ID TagID
85+ Name string
86+ Color string // Hex color code (e.g., "#FF5733")
87+ CreatedAt time.Time
88+}
89+
90+// NewTag creates a new tag with default color.
91+func NewTag(name string) *Tag {
92+ return &Tag{
93+ Name: name,
94+ Color: "#808080", // Default gray
95+ CreatedAt: time.Now(),
96+ }
97+}
98+
99+// FavoriteTree represents a hierarchical view of folders and favorites.
100+type FavoriteTree struct {
101+ RootFolders []*Folder
102+ RootFavorites []*Favorite
103+ FolderMap map[FolderID]*Folder // Quick lookup
104+ ChildFolders map[FolderID][]*Folder // Children of each folder
105+ ChildFavorites map[FolderID][]*Favorite // Favorites in each folder
106+}
107+
108+// NewFavoriteTree creates an empty favorite tree.
109+func NewFavoriteTree() *FavoriteTree {
110+ return &FavoriteTree{
111+ RootFolders: make([]*Folder, 0),
112+ RootFavorites: make([]*Favorite, 0),
113+ FolderMap: make(map[FolderID]*Folder),
114+ ChildFolders: make(map[FolderID][]*Folder),
115+ ChildFavorites: make(map[FolderID][]*Favorite),
116+ }
117+}
@@ -0,0 +1,117 @@
1+package entity
2+
3+import "time"
4+
5+// FavoriteID uniquely identifies a favorite/bookmark.
6+type FavoriteID int64
7+
8+// FolderID uniquely identifies a bookmark folder.
9+type FolderID int64
10+
11+// TagID uniquely identifies a tag.
12+type TagID int64
13+
14+// Favorite represents a bookmarked URL.
15+type Favorite struct {
16+ ID FavoriteID
17+ URL string
18+ Title string
19+ FaviconURL string
20+ FolderID *FolderID // nil = root level
21+ ShortcutKey *int // 1-9 for quick access (Alt+1 through Alt+9)
22+ Position int // Order within folder
23+ Tags []Tag
24+ CreatedAt time.Time
25+ UpdatedAt time.Time
26+}
27+
28+// NewFavorite creates a new favorite for a URL.
29+func NewFavorite(url, title string) *Favorite {
30+ now := time.Now()
31+ return &Favorite{
32+ URL: url,
33+ Title: title,
34+ CreatedAt: now,
35+ UpdatedAt: now,
36+ }
37+}
38+
39+// HasShortcut returns true if this favorite has a keyboard shortcut.
40+func (f *Favorite) HasShortcut() bool {
41+ return f.ShortcutKey != nil && *f.ShortcutKey >= 1 && *f.ShortcutKey <= 9
42+}
43+
44+// InFolder returns true if this favorite is in a folder.
45+func (f *Favorite) InFolder() bool {
46+ return f.FolderID != nil
47+}
48+
49+// HasTag returns true if this favorite has the given tag.
50+func (f *Favorite) HasTag(tagID TagID) bool {
51+ for _, t := range f.Tags {
52+ if t.ID == tagID {
53+ return true
54+ }
55+ }
56+ return false
57+}
58+
59+// Folder represents a container for organizing favorites.
60+type Folder struct {
61+ ID FolderID
62+ Name string
63+ Icon string // Optional icon identifier
64+ ParentID *FolderID // nil = root level
65+ Position int // Order within parent
66+ CreatedAt time.Time
67+}
68+
69+// NewFolder creates a new folder.
70+func NewFolder(name string) *Folder {
71+ return &Folder{
72+ Name: name,
73+ CreatedAt: time.Now(),
74+ }
75+}
76+
77+// IsRoot returns true if this folder is at root level.
78+func (f *Folder) IsRoot() bool {
79+ return f.ParentID == nil
80+}
81+
82+// Tag represents a label that can be applied to favorites.
83+type Tag struct {
84+ ID TagID
85+ Name string
86+ Color string // Hex color code (e.g., "#FF5733")
87+ CreatedAt time.Time
88+}
89+
90+// NewTag creates a new tag with default color.
91+func NewTag(name string) *Tag {
92+ return &Tag{
93+ Name: name,
94+ Color: "#808080", // Default gray
95+ CreatedAt: time.Now(),
96+ }
97+}
98+
99+// FavoriteTree represents a hierarchical view of folders and favorites.
100+type FavoriteTree struct {
101+ RootFolders []*Folder
102+ RootFavorites []*Favorite
103+ FolderMap map[FolderID]*Folder // Quick lookup
104+ ChildFolders map[FolderID][]*Folder // Children of each folder
105+ ChildFavorites map[FolderID][]*Favorite // Favorites in each folder
106+}
107+
108+// NewFavoriteTree creates an empty favorite tree.
109+func NewFavoriteTree() *FavoriteTree {
110+ return &FavoriteTree{
111+ RootFolders: make([]*Folder, 0),
112+ RootFavorites: make([]*Favorite, 0),
113+ FolderMap: make(map[FolderID]*Folder),
114+ ChildFolders: make(map[FolderID][]*Folder),
115+ ChildFavorites: make(map[FolderID][]*Favorite),
116+ }
117+}
A internal/domain/entity/history.go +39 -0
@@ -0,0 +1,39 @@
1+package entity
2+
3+import "time"
4+
5+// HistoryEntry represents a visited URL in browsing history.
6+type HistoryEntry struct {
7+ ID int64
8+ URL string
9+ Title string
10+ FaviconURL string
11+ VisitCount int64
12+ LastVisited time.Time
13+ CreatedAt time.Time
14+}
15+
16+// NewHistoryEntry creates a new history entry for a URL.
17+func NewHistoryEntry(url, title string) *HistoryEntry {
18+ now := time.Now()
19+ return &HistoryEntry{
20+ URL: url,
21+ Title: title,
22+ VisitCount: 1,
23+ LastVisited: now,
24+ CreatedAt: now,
25+ }
26+}
27+
28+// IncrementVisit updates the entry for a new visit.
29+func (h *HistoryEntry) IncrementVisit() {
30+ h.VisitCount++
31+ h.LastVisited = time.Now()
32+}
33+
34+// HistoryMatch represents a history entry that matched a search query.
35+// Used for fuzzy search results with scoring.
36+type HistoryMatch struct {
37+ Entry *HistoryEntry
38+ Score float64 // Match score (higher is better)
39+}
@@ -0,0 +1,39 @@
1+package entity
2+
3+import "time"
4+
5+// HistoryEntry represents a visited URL in browsing history.
6+type HistoryEntry struct {
7+ ID int64
8+ URL string
9+ Title string
10+ FaviconURL string
11+ VisitCount int64
12+ LastVisited time.Time
13+ CreatedAt time.Time
14+}
15+
16+// NewHistoryEntry creates a new history entry for a URL.
17+func NewHistoryEntry(url, title string) *HistoryEntry {
18+ now := time.Now()
19+ return &HistoryEntry{
20+ URL: url,
21+ Title: title,
22+ VisitCount: 1,
23+ LastVisited: now,
24+ CreatedAt: now,
25+ }
26+}
27+
28+// IncrementVisit updates the entry for a new visit.
29+func (h *HistoryEntry) IncrementVisit() {
30+ h.VisitCount++
31+ h.LastVisited = time.Now()
32+}
33+
34+// HistoryMatch represents a history entry that matched a search query.
35+// Used for fuzzy search results with scoring.
36+type HistoryMatch struct {
37+ Entry *HistoryEntry
38+ Score float64 // Match score (higher is better)
39+}
A internal/domain/entity/pane.go +158 -0
@@ -0,0 +1,158 @@
1+// Package entity contains domain entities representing core business concepts.
2+// These entities are pure Go types with no infrastructure dependencies.
3+package entity
4+
5+import "time"
6+
7+// PaneID uniquely identifies a pane within the browser.
8+type PaneID string
9+
10+// SplitDirection indicates how a pane container splits its children.
11+type SplitDirection int
12+
13+const (
14+ SplitNone SplitDirection = iota // Leaf node or stacked container
15+ SplitHorizontal // Left/right split
16+ SplitVertical // Top/bottom split
17+)
18+
19+// WindowType indicates the type of browser window.
20+type WindowType int
21+
22+const (
23+ WindowMain WindowType = iota // Regular browser tab
24+ WindowPopup // Popup window (OAuth, feature-restricted)
25+)
26+
27+// Pane represents a single browsing context (a WebView container).
28+// This is the leaf-level entity that holds navigation state.
29+type Pane struct {
30+ ID PaneID
31+ URI string
32+ Title string
33+ FaviconURL string
34+ WindowType WindowType
35+ ZoomFactor float64
36+ CanGoBack bool
37+ CanForward bool
38+ IsLoading bool
39+ CreatedAt time.Time
40+
41+ // Popup-specific fields
42+ IsRelated bool // Shares context with parent
43+ ParentPaneID *PaneID // Parent pane if this is a related popup
44+ AutoClose bool // Auto-close on OAuth success
45+ RequestID string // Request ID for popup tracking
46+}
47+
48+// NewPane creates a new pane with default values.
49+func NewPane(id PaneID) *Pane {
50+ return &Pane{
51+ ID: id,
52+ WindowType: WindowMain,
53+ ZoomFactor: 1.0,
54+ CreatedAt: time.Now(),
55+ }
56+}
57+
58+// PaneNode represents a node in the workspace pane tree structure.
59+// It can be either:
60+// - Leaf node: Contains a single Pane
61+// - Split node: Contains two children (left/right or top/bottom)
62+// - Stacked node: Contains multiple panes in a tabbed view
63+type PaneNode struct {
64+ ID string
65+ Pane *Pane // Non-nil for leaf nodes
66+ Parent *PaneNode // nil for root
67+ Children []*PaneNode
68+
69+ // Layout
70+ SplitDir SplitDirection
71+ SplitRatio float64 // 0.0-1.0, position of divider
72+
73+ // Stacked panes (alternative to split)
74+ IsStacked bool
75+ ActiveStackIndex int
76+}
77+
78+// IsLeaf returns true if this node contains a pane (no children).
79+func (n *PaneNode) IsLeaf() bool {
80+ return n.Pane != nil && len(n.Children) == 0 && !n.IsStacked
81+}
82+
83+// IsContainer returns true if this is a split or stacked container.
84+func (n *PaneNode) IsContainer() bool {
85+ return !n.IsLeaf()
86+}
87+
88+// IsSplit returns true if this node splits into two children.
89+func (n *PaneNode) IsSplit() bool {
90+ return n.SplitDir != SplitNone && len(n.Children) == 2
91+}
92+
93+// Left returns the left/top child in a split node.
94+func (n *PaneNode) Left() *PaneNode {
95+ if len(n.Children) > 0 {
96+ return n.Children[0]
97+ }
98+ return nil
99+}
100+
101+// Right returns the right/bottom child in a split node.
102+func (n *PaneNode) Right() *PaneNode {
103+ if len(n.Children) > 1 {
104+ return n.Children[1]
105+ }
106+ return nil
107+}
108+
109+// StackedPanes returns the list of panes if this is a stacked container.
110+func (n *PaneNode) StackedPanes() []*PaneNode {
111+ if n.IsStacked {
112+ return n.Children
113+ }
114+ return nil
115+}
116+
117+// ActivePane returns the currently visible pane in a stacked container.
118+func (n *PaneNode) ActivePane() *PaneNode {
119+ if n.IsStacked && n.ActiveStackIndex >= 0 && n.ActiveStackIndex < len(n.Children) {
120+ return n.Children[n.ActiveStackIndex]
121+ }
122+ return nil
123+}
124+
125+// Walk traverses the tree calling fn for each node. Returns early if fn returns false.
126+func (n *PaneNode) Walk(fn func(*PaneNode) bool) {
127+ if !fn(n) {
128+ return
129+ }
130+ for _, child := range n.Children {
131+ child.Walk(fn)
132+ }
133+}
134+
135+// FindPane searches the tree for a pane with the given ID.
136+func (n *PaneNode) FindPane(id PaneID) *PaneNode {
137+ var found *PaneNode
138+ n.Walk(func(node *PaneNode) bool {
139+ if node.Pane != nil && node.Pane.ID == id {
140+ found = node
141+ return false
142+ }
143+ return true
144+ })
145+ return found
146+}
147+
148+// LeafCount returns the number of leaf nodes (panes) in the tree.
149+func (n *PaneNode) LeafCount() int {
150+ count := 0
151+ n.Walk(func(node *PaneNode) bool {
152+ if node.IsLeaf() {
153+ count++
154+ }
155+ return true
156+ })
157+ return count
158+}
@@ -0,0 +1,158 @@
1+// Package entity contains domain entities representing core business concepts.
2+// These entities are pure Go types with no infrastructure dependencies.
3+package entity
4+
5+import "time"
6+
7+// PaneID uniquely identifies a pane within the browser.
8+type PaneID string
9+
10+// SplitDirection indicates how a pane container splits its children.
11+type SplitDirection int
12+
13+const (
14+ SplitNone SplitDirection = iota // Leaf node or stacked container
15+ SplitHorizontal // Left/right split
16+ SplitVertical // Top/bottom split
17+)
18+
19+// WindowType indicates the type of browser window.
20+type WindowType int
21+
22+const (
23+ WindowMain WindowType = iota // Regular browser tab
24+ WindowPopup // Popup window (OAuth, feature-restricted)
25+)
26+
27+// Pane represents a single browsing context (a WebView container).
28+// This is the leaf-level entity that holds navigation state.
29+type Pane struct {
30+ ID PaneID
31+ URI string
32+ Title string
33+ FaviconURL string
34+ WindowType WindowType
35+ ZoomFactor float64
36+ CanGoBack bool
37+ CanForward bool
38+ IsLoading bool
39+ CreatedAt time.Time
40+
41+ // Popup-specific fields
42+ IsRelated bool // Shares context with parent
43+ ParentPaneID *PaneID // Parent pane if this is a related popup
44+ AutoClose bool // Auto-close on OAuth success
45+ RequestID string // Request ID for popup tracking
46+}
47+
48+// NewPane creates a new pane with default values.
49+func NewPane(id PaneID) *Pane {
50+ return &Pane{
51+ ID: id,
52+ WindowType: WindowMain,
53+ ZoomFactor: 1.0,
54+ CreatedAt: time.Now(),
55+ }
56+}
57+
58+// PaneNode represents a node in the workspace pane tree structure.
59+// It can be either:
60+// - Leaf node: Contains a single Pane
61+// - Split node: Contains two children (left/right or top/bottom)
62+// - Stacked node: Contains multiple panes in a tabbed view
63+type PaneNode struct {
64+ ID string
65+ Pane *Pane // Non-nil for leaf nodes
66+ Parent *PaneNode // nil for root
67+ Children []*PaneNode
68+
69+ // Layout
70+ SplitDir SplitDirection
71+ SplitRatio float64 // 0.0-1.0, position of divider
72+
73+ // Stacked panes (alternative to split)
74+ IsStacked bool
75+ ActiveStackIndex int
76+}
77+
78+// IsLeaf returns true if this node contains a pane (no children).
79+func (n *PaneNode) IsLeaf() bool {
80+ return n.Pane != nil && len(n.Children) == 0 && !n.IsStacked
81+}
82+
83+// IsContainer returns true if this is a split or stacked container.
84+func (n *PaneNode) IsContainer() bool {
85+ return !n.IsLeaf()
86+}
87+
88+// IsSplit returns true if this node splits into two children.
89+func (n *PaneNode) IsSplit() bool {
90+ return n.SplitDir != SplitNone && len(n.Children) == 2
91+}
92+
93+// Left returns the left/top child in a split node.
94+func (n *PaneNode) Left() *PaneNode {
95+ if len(n.Children) > 0 {
96+ return n.Children[0]
97+ }
98+ return nil
99+}
100+
101+// Right returns the right/bottom child in a split node.
102+func (n *PaneNode) Right() *PaneNode {
103+ if len(n.Children) > 1 {
104+ return n.Children[1]
105+ }
106+ return nil
107+}
108+
109+// StackedPanes returns the list of panes if this is a stacked container.
110+func (n *PaneNode) StackedPanes() []*PaneNode {
111+ if n.IsStacked {
112+ return n.Children
113+ }
114+ return nil
115+}
116+
117+// ActivePane returns the currently visible pane in a stacked container.
118+func (n *PaneNode) ActivePane() *PaneNode {
119+ if n.IsStacked && n.ActiveStackIndex >= 0 && n.ActiveStackIndex < len(n.Children) {
120+ return n.Children[n.ActiveStackIndex]
121+ }
122+ return nil
123+}
124+
125+// Walk traverses the tree calling fn for each node. Returns early if fn returns false.
126+func (n *PaneNode) Walk(fn func(*PaneNode) bool) {
127+ if !fn(n) {
128+ return
129+ }
130+ for _, child := range n.Children {
131+ child.Walk(fn)
132+ }
133+}
134+
135+// FindPane searches the tree for a pane with the given ID.
136+func (n *PaneNode) FindPane(id PaneID) *PaneNode {
137+ var found *PaneNode
138+ n.Walk(func(node *PaneNode) bool {
139+ if node.Pane != nil && node.Pane.ID == id {
140+ found = node
141+ return false
142+ }
143+ return true
144+ })
145+ return found
146+}
147+
148+// LeafCount returns the number of leaf nodes (panes) in the tree.
149+func (n *PaneNode) LeafCount() int {
150+ count := 0
151+ n.Walk(func(node *PaneNode) bool {
152+ if node.IsLeaf() {
153+ count++
154+ }
155+ return true
156+ })
157+ return count
158+}
A internal/domain/entity/tab.go +147 -0
@@ -0,0 +1,147 @@
1+package entity
2+
3+import "time"
4+
5+// TabID uniquely identifies a tab.
6+type TabID string
7+
8+// Tab represents a browser tab containing a workspace.
9+// Tabs are the top-level container in the browser's tab bar.
10+type Tab struct {
11+ ID TabID
12+ Name string // Display name (often derived from active pane title)
13+ Workspace *Workspace // The workspace this tab contains
14+ Position int // Position in the tab bar (0-indexed)
15+ IsPinned bool // Pinned tabs stay at the left
16+ CreatedAt time.Time
17+}
18+
19+// NewTab creates a new tab with an initial pane.
20+func NewTab(tabID TabID, workspaceID WorkspaceID, initialPane *Pane) *Tab {
21+ return &Tab{
22+ ID: tabID,
23+ Workspace: NewWorkspace(workspaceID, initialPane),
24+ Position: 0,
25+ CreatedAt: time.Now(),
26+ }
27+}
28+
29+// Title returns the display title for the tab.
30+// Uses the active pane's title, falling back to URL or "New Tab".
31+func (t *Tab) Title() string {
32+ if t.Name != "" {
33+ return t.Name
34+ }
35+ if t.Workspace != nil {
36+ if active := t.Workspace.ActivePane(); active != nil && active.Pane != nil {
37+ if active.Pane.Title != "" {
38+ return active.Pane.Title
39+ }
40+ if active.Pane.URI != "" {
41+ return active.Pane.URI
42+ }
43+ }
44+ }
45+ return "New Tab"
46+}
47+
48+// PaneCount returns the number of panes in this tab's workspace.
49+func (t *Tab) PaneCount() int {
50+ if t.Workspace == nil {
51+ return 0
52+ }
53+ return t.Workspace.PaneCount()
54+}
55+
56+// TabList manages an ordered collection of tabs.
57+type TabList struct {
58+ Tabs []*Tab
59+ ActiveTabID TabID
60+}
61+
62+// NewTabList creates an empty tab list.
63+func NewTabList() *TabList {
64+ return &TabList{
65+ Tabs: make([]*Tab, 0),
66+ }
67+}
68+
69+// Add appends a tab to the list.
70+func (tl *TabList) Add(tab *Tab) {
71+ tab.Position = len(tl.Tabs)
72+ tl.Tabs = append(tl.Tabs, tab)
73+ if tl.ActiveTabID == "" {
74+ tl.ActiveTabID = tab.ID
75+ }
76+}
77+
78+// Remove removes a tab by ID and reindexes positions.
79+func (tl *TabList) Remove(id TabID) bool {
80+ for i, tab := range tl.Tabs {
81+ if tab.ID == id {
82+ tl.Tabs = append(tl.Tabs[:i], tl.Tabs[i+1:]...)
83+ // Reindex positions
84+ for j := i; j < len(tl.Tabs); j++ {
85+ tl.Tabs[j].Position = j
86+ }
87+ // Update active tab if needed
88+ if tl.ActiveTabID == id && len(tl.Tabs) > 0 {
89+ if i < len(tl.Tabs) {
90+ tl.ActiveTabID = tl.Tabs[i].ID
91+ } else {
92+ tl.ActiveTabID = tl.Tabs[len(tl.Tabs)-1].ID
93+ }
94+ }
95+ return true
96+ }
97+ }
98+ return false
99+}
100+
101+// Find returns a tab by ID.
102+func (tl *TabList) Find(id TabID) *Tab {
103+ for _, tab := range tl.Tabs {
104+ if tab.ID == id {
105+ return tab
106+ }
107+ }
108+ return nil
109+}
110+
111+// ActiveTab returns the currently active tab.
112+func (tl *TabList) ActiveTab() *Tab {
113+ return tl.Find(tl.ActiveTabID)
114+}
115+
116+// Count returns the number of tabs.
117+func (tl *TabList) Count() int {
118+ return len(tl.Tabs)
119+}
120+
121+// Move moves a tab to a new position.
122+func (tl *TabList) Move(id TabID, newPos int) bool {
123+ if newPos < 0 || newPos >= len(tl.Tabs) {
124+ return false
125+ }
126+ var tab *Tab
127+ var oldPos int
128+ for i, t := range tl.Tabs {
129+ if t.ID == id {
130+ tab = t
131+ oldPos = i
132+ break
133+ }
134+ }
135+ if tab == nil {
136+ return false
137+ }
138+ // Remove from old position
139+ tl.Tabs = append(tl.Tabs[:oldPos], tl.Tabs[oldPos+1:]...)
140+ // Insert at new position
141+ tl.Tabs = append(tl.Tabs[:newPos], append([]*Tab{tab}, tl.Tabs[newPos:]...)...)
142+ // Reindex all
143+ for i := range tl.Tabs {
144+ tl.Tabs[i].Position = i
145+ }
146+ return true
147+}
@@ -0,0 +1,147 @@
1+package entity
2+
3+import "time"
4+
5+// TabID uniquely identifies a tab.
6+type TabID string
7+
8+// Tab represents a browser tab containing a workspace.
9+// Tabs are the top-level container in the browser's tab bar.
10+type Tab struct {
11+ ID TabID
12+ Name string // Display name (often derived from active pane title)
13+ Workspace *Workspace // The workspace this tab contains
14+ Position int // Position in the tab bar (0-indexed)
15+ IsPinned bool // Pinned tabs stay at the left
16+ CreatedAt time.Time
17+}
18+
19+// NewTab creates a new tab with an initial pane.
20+func NewTab(tabID TabID, workspaceID WorkspaceID, initialPane *Pane) *Tab {
21+ return &Tab{
22+ ID: tabID,
23+ Workspace: NewWorkspace(workspaceID, initialPane),
24+ Position: 0,
25+ CreatedAt: time.Now(),
26+ }
27+}
28+
29+// Title returns the display title for the tab.
30+// Uses the active pane's title, falling back to URL or "New Tab".
31+func (t *Tab) Title() string {
32+ if t.Name != "" {
33+ return t.Name
34+ }
35+ if t.Workspace != nil {
36+ if active := t.Workspace.ActivePane(); active != nil && active.Pane != nil {
37+ if active.Pane.Title != "" {
38+ return active.Pane.Title
39+ }
40+ if active.Pane.URI != "" {
41+ return active.Pane.URI
42+ }
43+ }
44+ }
45+ return "New Tab"
46+}
47+
48+// PaneCount returns the number of panes in this tab's workspace.
49+func (t *Tab) PaneCount() int {
50+ if t.Workspace == nil {
51+ return 0
52+ }
53+ return t.Workspace.PaneCount()
54+}
55+
56+// TabList manages an ordered collection of tabs.
57+type TabList struct {
58+ Tabs []*Tab
59+ ActiveTabID TabID
60+}
61+
62+// NewTabList creates an empty tab list.
63+func NewTabList() *TabList {
64+ return &TabList{
65+ Tabs: make([]*Tab, 0),
66+ }
67+}
68+
69+// Add appends a tab to the list.
70+func (tl *TabList) Add(tab *Tab) {
71+ tab.Position = len(tl.Tabs)
72+ tl.Tabs = append(tl.Tabs, tab)
73+ if tl.ActiveTabID == "" {
74+ tl.ActiveTabID = tab.ID
75+ }
76+}
77+
78+// Remove removes a tab by ID and reindexes positions.
79+func (tl *TabList) Remove(id TabID) bool {
80+ for i, tab := range tl.Tabs {
81+ if tab.ID == id {
82+ tl.Tabs = append(tl.Tabs[:i], tl.Tabs[i+1:]...)
83+ // Reindex positions
84+ for j := i; j < len(tl.Tabs); j++ {
85+ tl.Tabs[j].Position = j
86+ }
87+ // Update active tab if needed
88+ if tl.ActiveTabID == id && len(tl.Tabs) > 0 {
89+ if i < len(tl.Tabs) {
90+ tl.ActiveTabID = tl.Tabs[i].ID
91+ } else {
92+ tl.ActiveTabID = tl.Tabs[len(tl.Tabs)-1].ID
93+ }
94+ }
95+ return true
96+ }
97+ }
98+ return false
99+}
100+
101+// Find returns a tab by ID.
102+func (tl *TabList) Find(id TabID) *Tab {
103+ for _, tab := range tl.Tabs {
104+ if tab.ID == id {
105+ return tab
106+ }
107+ }
108+ return nil
109+}
110+
111+// ActiveTab returns the currently active tab.
112+func (tl *TabList) ActiveTab() *Tab {
113+ return tl.Find(tl.ActiveTabID)
114+}
115+
116+// Count returns the number of tabs.
117+func (tl *TabList) Count() int {
118+ return len(tl.Tabs)
119+}
120+
121+// Move moves a tab to a new position.
122+func (tl *TabList) Move(id TabID, newPos int) bool {
123+ if newPos < 0 || newPos >= len(tl.Tabs) {
124+ return false
125+ }
126+ var tab *Tab
127+ var oldPos int
128+ for i, t := range tl.Tabs {
129+ if t.ID == id {
130+ tab = t
131+ oldPos = i
132+ break
133+ }
134+ }
135+ if tab == nil {
136+ return false
137+ }
138+ // Remove from old position
139+ tl.Tabs = append(tl.Tabs[:oldPos], tl.Tabs[oldPos+1:]...)
140+ // Insert at new position
141+ tl.Tabs = append(tl.Tabs[:newPos], append([]*Tab{tab}, tl.Tabs[newPos:]...)...)
142+ // Reindex all
143+ for i := range tl.Tabs {
144+ tl.Tabs[i].Position = i
145+ }
146+ return true
147+}
A internal/domain/entity/workspace.go +66 -0
@@ -0,0 +1,66 @@
1+package entity
2+
3+import "time"
4+
5+// WorkspaceID uniquely identifies a workspace.
6+type WorkspaceID string
7+
8+// Workspace represents a collection of panes arranged in a tree layout.
9+// Each tab contains exactly one workspace.
10+type Workspace struct {
11+ ID WorkspaceID
12+ Name string
13+ Root *PaneNode // Root of the pane tree
14+ ActivePaneID PaneID // Currently focused pane
15+ CreatedAt time.Time
16+}
17+
18+// NewWorkspace creates a new workspace with an initial pane.
19+func NewWorkspace(id WorkspaceID, initialPane *Pane) *Workspace {
20+ root := &PaneNode{
21+ ID: string(initialPane.ID),
22+ Pane: initialPane,
23+ }
24+ return &Workspace{
25+ ID: id,
26+ Root: root,
27+ ActivePaneID: initialPane.ID,
28+ CreatedAt: time.Now(),
29+ }
30+}
31+
32+// PaneCount returns the number of panes in the workspace.
33+func (w *Workspace) PaneCount() int {
34+ if w.Root == nil {
35+ return 0
36+ }
37+ return w.Root.LeafCount()
38+}
39+
40+// FindPane searches for a pane by ID in the workspace.
41+func (w *Workspace) FindPane(id PaneID) *PaneNode {
42+ if w.Root == nil {
43+ return nil
44+ }
45+ return w.Root.FindPane(id)
46+}
47+
48+// ActivePane returns the currently active pane node.
49+func (w *Workspace) ActivePane() *PaneNode {
50+ return w.FindPane(w.ActivePaneID)
51+}
52+
53+// AllPanes returns all leaf panes in the workspace.
54+func (w *Workspace) AllPanes() []*Pane {
55+ var panes []*Pane
56+ if w.Root == nil {
57+ return panes
58+ }
59+ w.Root.Walk(func(node *PaneNode) bool {
60+ if node.Pane != nil {
61+ panes = append(panes, node.Pane)
62+ }
63+ return true
64+ })
65+ return panes
66+}
@@ -0,0 +1,66 @@
1+package entity
2+
3+import "time"
4+
5+// WorkspaceID uniquely identifies a workspace.
6+type WorkspaceID string
7+
8+// Workspace represents a collection of panes arranged in a tree layout.
9+// Each tab contains exactly one workspace.
10+type Workspace struct {
11+ ID WorkspaceID
12+ Name string
13+ Root *PaneNode // Root of the pane tree
14+ ActivePaneID PaneID // Currently focused pane
15+ CreatedAt time.Time
16+}
17+
18+// NewWorkspace creates a new workspace with an initial pane.
19+func NewWorkspace(id WorkspaceID, initialPane *Pane) *Workspace {
20+ root := &PaneNode{
21+ ID: string(initialPane.ID),
22+ Pane: initialPane,
23+ }
24+ return &Workspace{
25+ ID: id,
26+ Root: root,
27+ ActivePaneID: initialPane.ID,
28+ CreatedAt: time.Now(),
29+ }
30+}
31+
32+// PaneCount returns the number of panes in the workspace.
33+func (w *Workspace) PaneCount() int {
34+ if w.Root == nil {
35+ return 0
36+ }
37+ return w.Root.LeafCount()
38+}
39+
40+// FindPane searches for a pane by ID in the workspace.
41+func (w *Workspace) FindPane(id PaneID) *PaneNode {
42+ if w.Root == nil {
43+ return nil
44+ }
45+ return w.Root.FindPane(id)
46+}
47+
48+// ActivePane returns the currently active pane node.
49+func (w *Workspace) ActivePane() *PaneNode {
50+ return w.FindPane(w.ActivePaneID)
51+}
52+
53+// AllPanes returns all leaf panes in the workspace.
54+func (w *Workspace) AllPanes() []*Pane {
55+ var panes []*Pane
56+ if w.Root == nil {
57+ return panes
58+ }
59+ w.Root.Walk(func(node *PaneNode) bool {
60+ if node.Pane != nil {
61+ panes = append(panes, node.Pane)
62+ }
63+ return true
64+ })
65+ return panes
66+}
A internal/domain/entity/zoom.go +70 -0
@@ -0,0 +1,70 @@
1+package entity
2+
3+import "time"
4+
5+// ZoomLevel represents the zoom factor for a specific domain.
6+// Allows users to set persistent zoom levels per-site.
7+type ZoomLevel struct {
8+ Domain string // Domain name (e.g., "github.com")
9+ ZoomFactor float64 // Zoom factor (1.0 = 100%, 1.5 = 150%)
10+ UpdatedAt time.Time
11+}
12+
13+// Default zoom constants
14+const (
15+ ZoomDefault = 1.0
16+ ZoomMin = 0.25 // 25%
17+ ZoomMax = 5.0 // 500%
18+ ZoomStep = 0.1 // 10% increments
19+)
20+
21+// NewZoomLevel creates a new zoom level for a domain.
22+func NewZoomLevel(domain string, factor float64) *ZoomLevel {
23+ return &ZoomLevel{
24+ Domain: domain,
25+ ZoomFactor: clampZoom(factor),
26+ UpdatedAt: time.Now(),
27+ }
28+}
29+
30+// SetFactor updates the zoom factor, clamping to valid range.
31+func (z *ZoomLevel) SetFactor(factor float64) {
32+ z.ZoomFactor = clampZoom(factor)
33+ z.UpdatedAt = time.Now()
34+}
35+
36+// ZoomIn increases the zoom factor by one step.
37+func (z *ZoomLevel) ZoomIn() {
38+ z.SetFactor(z.ZoomFactor + ZoomStep)
39+}
40+
41+// ZoomOut decreases the zoom factor by one step.
42+func (z *ZoomLevel) ZoomOut() {
43+ z.SetFactor(z.ZoomFactor - ZoomStep)
44+}
45+
46+// Reset restores the zoom factor to default.
47+func (z *ZoomLevel) Reset() {
48+ z.SetFactor(ZoomDefault)
49+}
50+
51+// IsDefault returns true if the zoom is at default level.
52+func (z *ZoomLevel) IsDefault() bool {
53+ return z.ZoomFactor == ZoomDefault
54+}
55+
56+// Percentage returns the zoom factor as a percentage (e.g., 150 for 1.5).
57+func (z *ZoomLevel) Percentage() int {
58+ return int(z.ZoomFactor * 100)
59+}
60+
61+// clampZoom constrains a zoom factor to the valid range.
62+func clampZoom(factor float64) float64 {
63+ if factor < ZoomMin {
64+ return ZoomMin
65+ }
66+ if factor > ZoomMax {
67+ return ZoomMax
68+ }
69+ return factor
70+}
@@ -0,0 +1,70 @@
1+package entity
2+
3+import "time"
4+
5+// ZoomLevel represents the zoom factor for a specific domain.
6+// Allows users to set persistent zoom levels per-site.
7+type ZoomLevel struct {
8+ Domain string // Domain name (e.g., "github.com")
9+ ZoomFactor float64 // Zoom factor (1.0 = 100%, 1.5 = 150%)
10+ UpdatedAt time.Time
11+}
12+
13+// Default zoom constants
14+const (
15+ ZoomDefault = 1.0
16+ ZoomMin = 0.25 // 25%
17+ ZoomMax = 5.0 // 500%
18+ ZoomStep = 0.1 // 10% increments
19+)
20+
21+// NewZoomLevel creates a new zoom level for a domain.
22+func NewZoomLevel(domain string, factor float64) *ZoomLevel {
23+ return &ZoomLevel{
24+ Domain: domain,
25+ ZoomFactor: clampZoom(factor),
26+ UpdatedAt: time.Now(),
27+ }
28+}
29+
30+// SetFactor updates the zoom factor, clamping to valid range.
31+func (z *ZoomLevel) SetFactor(factor float64) {
32+ z.ZoomFactor = clampZoom(factor)
33+ z.UpdatedAt = time.Now()
34+}
35+
36+// ZoomIn increases the zoom factor by one step.
37+func (z *ZoomLevel) ZoomIn() {
38+ z.SetFactor(z.ZoomFactor + ZoomStep)
39+}
40+
41+// ZoomOut decreases the zoom factor by one step.
42+func (z *ZoomLevel) ZoomOut() {
43+ z.SetFactor(z.ZoomFactor - ZoomStep)
44+}
45+
46+// Reset restores the zoom factor to default.
47+func (z *ZoomLevel) Reset() {
48+ z.SetFactor(ZoomDefault)
49+}
50+
51+// IsDefault returns true if the zoom is at default level.
52+func (z *ZoomLevel) IsDefault() bool {
53+ return z.ZoomFactor == ZoomDefault
54+}
55+
56+// Percentage returns the zoom factor as a percentage (e.g., 150 for 1.5).
57+func (z *ZoomLevel) Percentage() int {
58+ return int(z.ZoomFactor * 100)
59+}
60+
61+// clampZoom constrains a zoom factor to the valid range.
62+func clampZoom(factor float64) float64 {
63+ if factor < ZoomMin {
64+ return ZoomMin
65+ }
66+ if factor > ZoomMax {
67+ return ZoomMax
68+ }
69+ return factor
70+}
A internal/domain/repository/favorite.go +91 -0
@@ -0,0 +1,91 @@
1+package repository
2+
3+import (
4+ "context"
5+
6+ "github.com/bnema/dumber/internal/domain/entity"
7+)
8+
9+// FavoriteRepository defines operations for bookmark persistence.
10+type FavoriteRepository interface {
11+ // Save creates or updates a favorite.
12+ Save(ctx context.Context, fav *entity.Favorite) error
13+
14+ // FindByID retrieves a favorite by its ID.
15+ FindByID(ctx context.Context, id entity.FavoriteID) (*entity.Favorite, error)
16+
17+ // FindByURL retrieves a favorite by its URL.
18+ FindByURL(ctx context.Context, url string) (*entity.Favorite, error)
19+
20+ // GetAll retrieves all favorites.
21+ GetAll(ctx context.Context) ([]*entity.Favorite, error)
22+
23+ // GetByFolder retrieves favorites in a specific folder.
24+ // Pass nil for root-level favorites.
25+ GetByFolder(ctx context.Context, folderID *entity.FolderID) ([]*entity.Favorite, error)
26+
27+ // GetByShortcut retrieves the favorite with the given shortcut key (1-9).
28+ GetByShortcut(ctx context.Context, key int) (*entity.Favorite, error)
29+
30+ // UpdatePosition updates a favorite's position within its folder.
31+ UpdatePosition(ctx context.Context, id entity.FavoriteID, position int) error
32+
33+ // SetFolder moves a favorite to a different folder.
34+ SetFolder(ctx context.Context, id entity.FavoriteID, folderID *entity.FolderID) error
35+
36+ // SetShortcut assigns a keyboard shortcut (1-9) or nil to remove.
37+ SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error
38+
39+ // Delete removes a favorite by ID.
40+ Delete(ctx context.Context, id entity.FavoriteID) error
41+}
42+
43+// FolderRepository defines operations for bookmark folder persistence.
44+type FolderRepository interface {
45+ // Save creates or updates a folder.
46+ Save(ctx context.Context, folder *entity.Folder) error
47+
48+ // FindByID retrieves a folder by its ID.
49+ FindByID(ctx context.Context, id entity.FolderID) (*entity.Folder, error)
50+
51+ // GetAll retrieves all folders.
52+ GetAll(ctx context.Context) ([]*entity.Folder, error)
53+
54+ // GetChildren retrieves child folders of a parent.
55+ // Pass nil for root-level folders.
56+ GetChildren(ctx context.Context, parentID *entity.FolderID) ([]*entity.Folder, error)
57+
58+ // UpdatePosition updates a folder's position within its parent.
59+ UpdatePosition(ctx context.Context, id entity.FolderID, position int) error
60+
61+ // Delete removes a folder by ID.
62+ // Favorites in the folder should be moved to root.
63+ Delete(ctx context.Context, id entity.FolderID) error
64+}
65+
66+// TagRepository defines operations for tag persistence.
67+type TagRepository interface {
68+ // Save creates or updates a tag.
69+ Save(ctx context.Context, tag *entity.Tag) error
70+
71+ // FindByID retrieves a tag by its ID.
72+ FindByID(ctx context.Context, id entity.TagID) (*entity.Tag, error)
73+
74+ // FindByName retrieves a tag by its name.
75+ FindByName(ctx context.Context, name string) (*entity.Tag, error)
76+
77+ // GetAll retrieves all tags.
78+ GetAll(ctx context.Context) ([]*entity.Tag, error)
79+
80+ // AssignToFavorite associates a tag with a favorite.
81+ AssignToFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error
82+
83+ // RemoveFromFavorite removes a tag association from a favorite.
84+ RemoveFromFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error
85+
86+ // GetForFavorite retrieves all tags associated with a favorite.
87+ GetForFavorite(ctx context.Context, favID entity.FavoriteID) ([]*entity.Tag, error)
88+
89+ // Delete removes a tag by ID.
90+ Delete(ctx context.Context, id entity.TagID) error
91+}
@@ -0,0 +1,91 @@
1+package repository
2+
3+import (
4+ "context"
5+
6+ "github.com/bnema/dumber/internal/domain/entity"
7+)
8+
9+// FavoriteRepository defines operations for bookmark persistence.
10+type FavoriteRepository interface {
11+ // Save creates or updates a favorite.
12+ Save(ctx context.Context, fav *entity.Favorite) error
13+
14+ // FindByID retrieves a favorite by its ID.
15+ FindByID(ctx context.Context, id entity.FavoriteID) (*entity.Favorite, error)
16+
17+ // FindByURL retrieves a favorite by its URL.
18+ FindByURL(ctx context.Context, url string) (*entity.Favorite, error)
19+
20+ // GetAll retrieves all favorites.
21+ GetAll(ctx context.Context) ([]*entity.Favorite, error)
22+
23+ // GetByFolder retrieves favorites in a specific folder.
24+ // Pass nil for root-level favorites.
25+ GetByFolder(ctx context.Context, folderID *entity.FolderID) ([]*entity.Favorite, error)
26+
27+ // GetByShortcut retrieves the favorite with the given shortcut key (1-9).
28+ GetByShortcut(ctx context.Context, key int) (*entity.Favorite, error)
29+
30+ // UpdatePosition updates a favorite's position within its folder.
31+ UpdatePosition(ctx context.Context, id entity.FavoriteID, position int) error
32+
33+ // SetFolder moves a favorite to a different folder.
34+ SetFolder(ctx context.Context, id entity.FavoriteID, folderID *entity.FolderID) error
35+
36+ // SetShortcut assigns a keyboard shortcut (1-9) or nil to remove.
37+ SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error
38+
39+ // Delete removes a favorite by ID.
40+ Delete(ctx context.Context, id entity.FavoriteID) error
41+}
42+
43+// FolderRepository defines operations for bookmark folder persistence.
44+type FolderRepository interface {
45+ // Save creates or updates a folder.
46+ Save(ctx context.Context, folder *entity.Folder) error
47+
48+ // FindByID retrieves a folder by its ID.
49+ FindByID(ctx context.Context, id entity.FolderID) (*entity.Folder, error)
50+
51+ // GetAll retrieves all folders.
52+ GetAll(ctx context.Context) ([]*entity.Folder, error)
53+
54+ // GetChildren retrieves child folders of a parent.
55+ // Pass nil for root-level folders.
56+ GetChildren(ctx context.Context, parentID *entity.FolderID) ([]*entity.Folder, error)
57+
58+ // UpdatePosition updates a folder's position within its parent.
59+ UpdatePosition(ctx context.Context, id entity.FolderID, position int) error
60+
61+ // Delete removes a folder by ID.
62+ // Favorites in the folder should be moved to root.
63+ Delete(ctx context.Context, id entity.FolderID) error
64+}
65+
66+// TagRepository defines operations for tag persistence.
67+type TagRepository interface {
68+ // Save creates or updates a tag.
69+ Save(ctx context.Context, tag *entity.Tag) error
70+
71+ // FindByID retrieves a tag by its ID.
72+ FindByID(ctx context.Context, id entity.TagID) (*entity.Tag, error)
73+
74+ // FindByName retrieves a tag by its name.
75+ FindByName(ctx context.Context, name string) (*entity.Tag, error)
76+
77+ // GetAll retrieves all tags.
78+ GetAll(ctx context.Context) ([]*entity.Tag, error)
79+
80+ // AssignToFavorite associates a tag with a favorite.
81+ AssignToFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error
82+
83+ // RemoveFromFavorite removes a tag association from a favorite.
84+ RemoveFromFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error
85+
86+ // GetForFavorite retrieves all tags associated with a favorite.
87+ GetForFavorite(ctx context.Context, favID entity.FavoriteID) ([]*entity.Tag, error)
88+
89+ // Delete removes a tag by ID.
90+ Delete(ctx context.Context, id entity.TagID) error
91+}
A internal/domain/repository/filter.go +19 -0
@@ -0,0 +1,19 @@
1+package repository
2+
3+import "context"
4+
5+// ContentWhitelistRepository defines operations for content filter whitelist persistence.
6+// Whitelisted domains bypass content filtering.
7+type ContentWhitelistRepository interface {
8+ // Add adds a domain to the whitelist.
9+ Add(ctx context.Context, domain string) error
10+
11+ // Remove removes a domain from the whitelist.
12+ Remove(ctx context.Context, domain string) error
13+
14+ // Contains checks if a domain is whitelisted.
15+ Contains(ctx context.Context, domain string) (bool, error)
16+
17+ // GetAll retrieves all whitelisted domains.
18+ GetAll(ctx context.Context) ([]string, error)
19+}
@@ -0,0 +1,19 @@
1+package repository
2+
3+import "context"
4+
5+// ContentWhitelistRepository defines operations for content filter whitelist persistence.
6+// Whitelisted domains bypass content filtering.
7+type ContentWhitelistRepository interface {
8+ // Add adds a domain to the whitelist.
9+ Add(ctx context.Context, domain string) error
10+
11+ // Remove removes a domain from the whitelist.
12+ Remove(ctx context.Context, domain string) error
13+
14+ // Contains checks if a domain is whitelisted.
15+ Contains(ctx context.Context, domain string) (bool, error)
16+
17+ // GetAll retrieves all whitelisted domains.
18+ GetAll(ctx context.Context) ([]string, error)
19+}
A internal/domain/repository/history.go +35 -0
@@ -0,0 +1,35 @@
1+package repository
2+
3+import (
4+ "context"
5+ "time"
6+
7+ "github.com/bnema/dumber/internal/domain/entity"
8+)
9+
10+// HistoryRepository defines operations for browsing history persistence.
11+type HistoryRepository interface {
12+ // Save creates or updates a history entry (upsert).
13+ Save(ctx context.Context, entry *entity.HistoryEntry) error
14+
15+ // FindByURL retrieves a history entry by its URL.
16+ FindByURL(ctx context.Context, url string) (*entity.HistoryEntry, error)
17+
18+ // Search performs a fuzzy search on history entries.
19+ Search(ctx context.Context, query string, limit int) ([]entity.HistoryMatch, error)
20+
21+ // GetRecent retrieves recent history entries with pagination.
22+ GetRecent(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error)
23+
24+ // IncrementVisitCount increments the visit count for a URL.
25+ IncrementVisitCount(ctx context.Context, url string) error
26+
27+ // Delete removes a single history entry by ID.
28+ Delete(ctx context.Context, id int64) error
29+
30+ // DeleteOlderThan removes entries older than the given time.
31+ DeleteOlderThan(ctx context.Context, before time.Time) error
32+
33+ // DeleteAll removes all history entries.
34+ DeleteAll(ctx context.Context) error
35+}
@@ -0,0 +1,35 @@
1+package repository
2+
3+import (
4+ "context"
5+ "time"
6+
7+ "github.com/bnema/dumber/internal/domain/entity"
8+)
9+
10+// HistoryRepository defines operations for browsing history persistence.
11+type HistoryRepository interface {
12+ // Save creates or updates a history entry (upsert).
13+ Save(ctx context.Context, entry *entity.HistoryEntry) error
14+
15+ // FindByURL retrieves a history entry by its URL.
16+ FindByURL(ctx context.Context, url string) (*entity.HistoryEntry, error)
17+
18+ // Search performs a fuzzy search on history entries.
19+ Search(ctx context.Context, query string, limit int) ([]entity.HistoryMatch, error)
20+
21+ // GetRecent retrieves recent history entries with pagination.
22+ GetRecent(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error)
23+
24+ // IncrementVisitCount increments the visit count for a URL.
25+ IncrementVisitCount(ctx context.Context, url string) error
26+
27+ // Delete removes a single history entry by ID.
28+ Delete(ctx context.Context, id int64) error
29+
30+ // DeleteOlderThan removes entries older than the given time.
31+ DeleteOlderThan(ctx context.Context, before time.Time) error
32+
33+ // DeleteAll removes all history entries.
34+ DeleteAll(ctx context.Context) error
35+}
A internal/domain/repository/zoom.go +23 -0
@@ -0,0 +1,23 @@
1+package repository
2+
3+import (
4+ "context"
5+
6+ "github.com/bnema/dumber/internal/domain/entity"
7+)
8+
9+// ZoomRepository defines operations for per-domain zoom level persistence.
10+type ZoomRepository interface {
11+ // Get retrieves the zoom level for a domain.
12+ // Returns nil if no custom zoom is set.
13+ Get(ctx context.Context, domain string) (*entity.ZoomLevel, error)
14+
15+ // Set saves or updates the zoom level for a domain.
16+ Set(ctx context.Context, level *entity.ZoomLevel) error
17+
18+ // Delete removes the custom zoom level for a domain.
19+ Delete(ctx context.Context, domain string) error
20+
21+ // GetAll retrieves all custom zoom levels.
22+ GetAll(ctx context.Context) ([]*entity.ZoomLevel, error)
23+}
@@ -0,0 +1,23 @@
1+package repository
2+
3+import (
4+ "context"
5+
6+ "github.com/bnema/dumber/internal/domain/entity"
7+)
8+
9+// ZoomRepository defines operations for per-domain zoom level persistence.
10+type ZoomRepository interface {
11+ // Get retrieves the zoom level for a domain.
12+ // Returns nil if no custom zoom is set.
13+ Get(ctx context.Context, domain string) (*entity.ZoomLevel, error)
14+
15+ // Set saves or updates the zoom level for a domain.
16+ Set(ctx context.Context, level *entity.ZoomLevel) error
17+
18+ // Delete removes the custom zoom level for a domain.
19+ Delete(ctx context.Context, domain string) error
20+
21+ // GetAll retrieves all custom zoom levels.
22+ GetAll(ctx context.Context) ([]*entity.ZoomLevel, error)
23+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close