v0.27.2 5960dd8

app.go RawHistoryFolder

1// Package cli provides CLI commands using Bubble Tea TUI.
2package cli
3
4import (
5	"context"
6	"database/sql"
7	"fmt"
8	"os"
9	"path/filepath"
10
11	"github.com/bnema/dumber/internal/application/port"
12	"github.com/bnema/dumber/internal/application/usecase"
13	"github.com/bnema/dumber/internal/cli/styles"
14	"github.com/bnema/dumber/internal/domain/build"
15	"github.com/bnema/dumber/internal/domain/repository"
16	"github.com/bnema/dumber/internal/infrastructure/config"
17	"github.com/bnema/dumber/internal/infrastructure/favicon"
18	infralogging "github.com/bnema/dumber/internal/infrastructure/logging"
19	"github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"
20	"github.com/bnema/dumber/internal/logging"
21)
22
23// App holds CLI dependencies.
24type App struct {
25	Config    *config.Config
26	Theme     *styles.Theme
27	BuildInfo build.Info
28	db        *sql.DB
29	History   repository.HistoryRepository
30
31	// Use cases
32	SearchHistoryUC *usecase.SearchHistoryUseCase
33	SessionUC       *usecase.ManageSessionUseCase
34	ListSessionsUC  *usecase.ListSessionsUseCase
35	RestoreUC       *usecase.RestoreSessionUseCase
36	DeleteSessionUC *usecase.DeleteSessionUseCase
37
38	// Services
39	FaviconService *favicon.Service
40
41	// Context with logger
42	ctx        context.Context
43	logCleanup func()
44}
45
46// NewApp creates a new CLI application with all dependencies.
47func NewApp() (*App, error) {
48	const dataDirPerm = 0o755
49	var err error
50	// Load config
51	cfg := loadConfig()
52
53	// Create theme from config
54	theme := styles.NewTheme(cfg)
55
56	// Start with a quiet logger. If a browser session is active, we'll attach to it.
57	logLevel := cfg.Logging.Level
58	if envLevel := os.Getenv("DUMBER_LOG_LEVEL"); envLevel != "" {
59		logLevel = envLevel
60	}
61
62	logger, logCleanup, _ := logging.NewWithFile(
63		logging.Config{Level: logging.ParseLevel(logLevel), Format: cfg.Logging.Format, TimeFormat: "15:04:05"},
64		logging.FileConfig{Enabled: false, WriteToStderr: false},
65	)
66	ctx := logging.WithContext(context.Background(), logger)
67
68	// Determine database path - use share directory per XDG spec (user data)
69	home, _ := os.UserHomeDir()
70	dbFile := cfg.Database.Path
71	if dbFile == "" {
72		dbFile = filepath.Join(home, ".local", "share", "dumber", "dumber.db")
73	}
74	dbPath := filepath.Dir(dbFile)
75
76	// Ensure data directory exists
77	if err = os.MkdirAll(dbPath, dataDirPerm); err != nil {
78		return nil, fmt.Errorf("create data dir: %w", err)
79	}
80
81	// Open database connection
82	db, err := sqlite.NewConnection(ctx, dbFile)
83	if err != nil {
84		return nil, fmt.Errorf("open database: %w", err)
85	}
86
87	// Attach CLI logs to the active browser session (if any).
88	// This avoids creating extra tiny session log files for each CLI invocation.
89	sessionRepo := sqlite.NewSessionRepository(db)
90	sessionLoggerAdapter := infralogging.NewSessionLoggerAdapter()
91	sessionUC := usecase.NewManageSessionUseCase(sessionRepo, sessionLoggerAdapter)
92	if active, activeErr := sessionUC.GetActiveSession(ctx); activeErr == nil && active != nil {
93		attachedLogger, attachedCleanup, _ := sessionLoggerAdapter.CreateLogger(ctx, active.ID, port.SessionLogConfig{
94			Level:         logLevel,
95			Format:        cfg.Logging.Format,
96			TimeFormat:    "15:04:05",
97			LogDir:        cfg.Logging.LogDir,
98			WriteToStderr: false,
99			EnableFileLog: cfg.Logging.EnableFileLog,
100		})
101		logger = attachedLogger
102		logCleanup = attachedCleanup
103		ctx = logging.WithContext(context.Background(), logger)
104		logger.Info().Str("session_id", string(active.ID)).Msg("attached to active session")
105	}
106
107	logger.Debug().Str("db_path", dbFile).Msg("database connected")
108
109	// Create repositories
110	historyRepo := sqlite.NewHistoryRepository(db)
111	sessionStateRepo := sqlite.NewSessionStateRepository(db)
112
113	// Create use cases
114	searchHistoryUC := usecase.NewSearchHistoryUseCase(historyRepo)
115	listSessionsUC := usecase.NewListSessionsUseCase(sessionRepo, sessionStateRepo, cfg.Logging.LogDir)
116	restoreUC := usecase.NewRestoreSessionUseCase(sessionStateRepo, sessionRepo)
117	deleteSessionUC := usecase.NewDeleteSessionUseCase(sessionStateRepo, sessionRepo, cfg.Logging.LogDir)
118
119	// Create favicon service for CLI (path resolution for dmenu/fuzzel)
120	faviconCacheDir, _ := config.GetFaviconCacheDir()
121	faviconService := favicon.NewService(faviconCacheDir)
122
123	return &App{
124		Config:          cfg,
125		Theme:           theme,
126		db:              db,
127		History:         historyRepo,
128		SearchHistoryUC: searchHistoryUC,
129		SessionUC:       sessionUC,
130		ListSessionsUC:  listSessionsUC,
131		RestoreUC:       restoreUC,
132		DeleteSessionUC: deleteSessionUC,
133		FaviconService:  faviconService,
134		ctx:             ctx,
135		logCleanup:      logCleanup,
136	}, nil
137}
138
139// Close releases all resources.
140func (a *App) Close() error {
141	if a.logCleanup != nil {
142		a.logCleanup()
143	}
144	if a.FaviconService != nil {
145		a.FaviconService.Close()
146	}
147	if a.db != nil {
148		return a.db.Close()
149	}
150	return nil
151}
152
153// Ctx returns the application context with logger.
154func (a *App) Ctx() context.Context {
155	return a.ctx
156}
157
158// loadConfig loads configuration from standard locations.
159func loadConfig() *config.Config {
160	mgr, err := config.NewManager()
161	if err != nil {
162		// Return default config if manager fails
163		return config.DefaultConfig()
164	}
165
166	if err := mgr.Load(); err != nil {
167		// Return default config if loading fails
168		return config.DefaultConfig()
169	}
170
171	return mgr.Get()
172}

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