v0.28.0 3cb3770

main_window.go RawHistoryFolder

1// Package window provides GTK window implementations.
2package window
3
4import (
5	"context"
6
7	"github.com/bnema/dumber/internal/logging"
8	"github.com/bnema/dumber/internal/ui/component"
9	"github.com/bnema/puregotk/v4/gtk"
10	"github.com/rs/zerolog"
11)
12
13const (
14	defaultWidth  = 1280
15	defaultHeight = 800
16	windowTitle   = "Dumber"
17)
18
19// MainWindow represents the main browser window.
20type MainWindow struct {
21	window         *gtk.ApplicationWindow
22	rootBox        *gtk.Box // Vertical: tab bar + content
23	tabBar         *component.TabBar
24	contentOverlay *gtk.Overlay // Overlay for content + omnibox
25	contentArea    *gtk.Box     // Container for workspace content
26	currentContent *gtk.Widget  // Track current content for removal on tab switch
27
28	tabBarPosition string // "top" or "bottom"
29	logger         zerolog.Logger
30}
31
32// New creates a new main browser window.
33// tabBarPosition controls whether the tab bar is at the "top" or "bottom" (default: "top").
34func New(ctx context.Context, app *gtk.Application, tabBarPosition string) (*MainWindow, error) {
35	log := logging.FromContext(ctx)
36
37	mw := &MainWindow{
38		tabBarPosition: tabBarPosition,
39		logger:         log.With().Str("component", "main-window").Logger(),
40	}
41
42	mw.window = gtk.NewApplicationWindow(app)
43	if mw.window == nil {
44		return nil, ErrWindowCreationFailed
45	}
46
47	title := windowTitle
48	mw.window.SetTitle(&title)
49	mw.window.SetDefaultSize(defaultWidth, defaultHeight)
50
51	mw.rootBox = gtk.NewBox(gtk.OrientationVerticalValue, 0)
52	if mw.rootBox == nil {
53		mw.window.Unref()
54		return nil, ErrWidgetCreationFailed("rootBox")
55	}
56	mw.rootBox.SetHexpand(true)
57	mw.rootBox.SetVexpand(true)
58	mw.rootBox.SetVisible(true)
59
60	mw.tabBar = component.NewTabBar()
61	if mw.tabBar == nil {
62		mw.rootBox.Unref()
63		mw.window.Unref()
64		return nil, ErrWidgetCreationFailed("tabBar")
65	}
66
67	mw.contentOverlay = gtk.NewOverlay()
68	if mw.contentOverlay == nil {
69		mw.tabBar.Destroy()
70		mw.rootBox.Unref()
71		mw.window.Unref()
72		return nil, ErrWidgetCreationFailed("contentOverlay")
73	}
74	mw.contentOverlay.SetHexpand(true)
75	mw.contentOverlay.SetVexpand(true)
76	mw.contentOverlay.SetVisible(true)
77
78	mw.contentArea = gtk.NewBox(gtk.OrientationVerticalValue, 0)
79	if mw.contentArea == nil {
80		mw.contentOverlay.Unref()
81		mw.tabBar.Destroy()
82		mw.rootBox.Unref()
83		mw.window.Unref()
84		return nil, ErrWidgetCreationFailed("contentArea")
85	}
86	mw.contentArea.SetHexpand(true)
87	mw.contentArea.SetVexpand(true)
88	mw.contentArea.SetVisible(true)
89	mw.contentArea.AddCssClass("content-area")
90
91	mw.contentOverlay.SetChild(&mw.contentArea.Widget)
92
93	mw.assembleLayout()
94
95	mw.window.SetChild(&mw.rootBox.Widget)
96
97	return mw, nil
98}
99
100func (mw *MainWindow) assembleLayout() {
101	tabBarPos := "top" // default
102	if mw.tabBarPosition != "" {
103		tabBarPos = mw.tabBarPosition
104	}
105
106	if tabBarPos == "bottom" {
107		mw.rootBox.Append(&mw.contentOverlay.Widget)
108		mw.rootBox.Append(mw.tabBar.Widget())
109	} else {
110		mw.rootBox.Append(mw.tabBar.Widget())
111		mw.rootBox.Append(&mw.contentOverlay.Widget)
112	}
113
114	mw.logger.Debug().
115		Str("tab_bar_position", tabBarPos).
116		Msg("window layout assembled")
117}
118
119// Show presents the window.
120func (mw *MainWindow) Show() {
121	mw.window.Present()
122}
123
124// Close closes the window.
125func (mw *MainWindow) Close() {
126	mw.window.Close()
127}
128
129// TabBar returns the window's tab bar component.
130func (mw *MainWindow) TabBar() *component.TabBar {
131	return mw.tabBar
132}
133
134// ContentArea returns the main content container widget.
135func (mw *MainWindow) ContentArea() *gtk.Box {
136	return mw.contentArea
137}
138
139// SetContent replaces the current content widget with the given widget.
140func (mw *MainWindow) SetContent(widget *gtk.Widget) {
141	if mw.currentContent != nil {
142		mw.contentArea.Remove(mw.currentContent)
143		mw.currentContent = nil
144	}
145
146	if widget != nil {
147		widget.SetVisible(true)
148		mw.contentArea.Append(widget)
149		mw.currentContent = widget
150	}
151}
152
153// Window returns the underlying GTK application window.
154func (mw *MainWindow) Window() *gtk.ApplicationWindow {
155	return mw.window
156}
157
158// SetTitle sets the window title.
159func (mw *MainWindow) SetTitle(title string) {
160	if mw.window == nil {
161		return
162	}
163	const maxTitleLen = 255
164	runes := []rune(title)
165	if len(runes) > maxTitleLen {
166		title = string(runes[:maxTitleLen-3]) + "..."
167	}
168	mw.window.SetTitle(&title)
169}
170
171// ContentOverlay returns the overlay container for the content area.
172func (mw *MainWindow) ContentOverlay() *gtk.Overlay {
173	return mw.contentOverlay
174}
175
176// AddOverlay adds a widget as an overlay above the content area.
177func (mw *MainWindow) AddOverlay(widget *gtk.Widget) {
178	if mw.contentOverlay != nil && widget != nil {
179		mw.contentOverlay.AddOverlay(widget)
180	}
181}
182
183// Destroy releases all GTK resources held by the window.
184func (mw *MainWindow) Destroy() {
185	if mw.tabBar != nil {
186		mw.tabBar.Destroy()
187		mw.tabBar = nil
188	}
189	if mw.contentArea != nil {
190		mw.contentArea.Unref()
191		mw.contentArea = nil
192	}
193	if mw.rootBox != nil {
194		mw.rootBox.Unref()
195		mw.rootBox = nil
196	}
197	if mw.window != nil {
198		mw.window.Destroy()
199		mw.window = nil
200	}
201}
202
203// WindowError is a window-related error.
204type WindowError struct {
205	Message string
206}
207
208// Error implements the error interface.
209func (e WindowError) Error() string {
210	return e.Message
211}
212
213var (
214	// ErrWindowCreationFailed is returned when the GTK application window cannot be created.
215	ErrWindowCreationFailed = WindowError{Message: "failed to create application window"}
216)
217
218// ErrWidgetCreationFailed returns an error for a failed widget creation.
219func ErrWidgetCreationFailed(name string) error {
220	return WindowError{Message: "failed to create widget: " + name}
221}

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