v0.20.0 c741a39

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
35	// Services
36	FaviconService *favicon.Service
37
38	// Context with logger
39	ctx        context.Context
40	logCleanup func()
41}
42
43// NewApp creates a new CLI application with all dependencies.
44func NewApp() (*App, error) {
45	const dataDirPerm = 0o755
46	var err error
47	// Load config
48	cfg := loadConfig()
49
50	// Create theme from config
51	theme := styles.NewTheme(cfg)
52
53	// Start with a quiet logger. If a browser session is active, we'll attach to it.
54	logLevel := cfg.Logging.Level
55	if envLevel := os.Getenv("DUMBER_LOG_LEVEL"); envLevel != "" {
56		logLevel = envLevel
57	}
58
59	logger, logCleanup, _ := logging.NewWithFile(
60		logging.Config{Level: logging.ParseLevel(logLevel), Format: cfg.Logging.Format, TimeFormat: "15:04:05"},
61		logging.FileConfig{Enabled: false, WriteToStderr: false},
62	)
63	ctx := logging.WithContext(context.Background(), logger)
64
65	// Determine database path - use share directory per XDG spec (user data)
66	home, _ := os.UserHomeDir()
67	dbFile := cfg.Database.Path
68	if dbFile == "" {
69		dbFile = filepath.Join(home, ".local", "share", "dumber", "dumber.db")
70	}
71	dbPath := filepath.Dir(dbFile)
72
73	// Ensure data directory exists
74	if err = os.MkdirAll(dbPath, dataDirPerm); err != nil {
75		return nil, fmt.Errorf("create data dir: %w", err)
76	}
77
78	// Open database connection
79	db, err := sqlite.NewConnection(ctx, dbFile)
80	if err != nil {
81		return nil, fmt.Errorf("open database: %w", err)
82	}
83
84	// Attach CLI logs to the active browser session (if any).
85	// This avoids creating extra tiny session log files for each CLI invocation.
86	sessionRepo := sqlite.NewSessionRepository(db)
87	sessionLoggerAdapter := infralogging.NewSessionLoggerAdapter()
88	sessionUC := usecase.NewManageSessionUseCase(sessionRepo, sessionLoggerAdapter)
89	if active, activeErr := sessionUC.GetActiveSession(ctx); activeErr == nil && active != nil {
90		attachedLogger, attachedCleanup, _ := sessionLoggerAdapter.CreateLogger(ctx, active.ID, port.SessionLogConfig{
91			Level:         logLevel,
92			Format:        cfg.Logging.Format,
93			TimeFormat:    "15:04:05",
94			LogDir:        cfg.Logging.LogDir,
95			WriteToStderr: false,
96			EnableFileLog: cfg.Logging.EnableFileLog,
97		})
98		logger = attachedLogger
99		logCleanup = attachedCleanup
100		ctx = logging.WithContext(context.Background(), logger)
101		logger.Info().Str("session_id", string(active.ID)).Msg("attached to active session")
102	}
103
104	logger.Debug().Str("db_path", dbFile).Msg("database connected")
105
106	// Create repositories
107	historyRepo := sqlite.NewHistoryRepository(db)
108
109	// Create use cases
110	searchHistoryUC := usecase.NewSearchHistoryUseCase(historyRepo)
111
112	// Create favicon service for CLI (path resolution for dmenu/fuzzel)
113	faviconCacheDir, _ := config.GetFaviconCacheDir()
114	faviconService := favicon.NewService(faviconCacheDir)
115
116	return &App{
117		Config:          cfg,
118		Theme:           theme,
119		db:              db,
120		History:         historyRepo,
121		SearchHistoryUC: searchHistoryUC,
122		SessionUC:       sessionUC,
123		FaviconService:  faviconService,
124		ctx:             ctx,
125		logCleanup:      logCleanup,
126	}, nil
127}
128
129// Close releases all resources.
130func (a *App) Close() error {
131	if a.logCleanup != nil {
132		a.logCleanup()
133	}
134	if a.FaviconService != nil {
135		a.FaviconService.Close()
136	}
137	if a.db != nil {
138		return a.db.Close()
139	}
140	return nil
141}
142
143// Ctx returns the application context with logger.
144func (a *App) Ctx() context.Context {
145	return a.ctx
146}
147
148// loadConfig loads configuration from standard locations.
149func loadConfig() *config.Config {
150	mgr, err := config.NewManager()
151	if err != nil {
152		// Return default config if manager fails
153		return config.DefaultConfig()
154	}
155
156	if err := mgr.Load(); err != nil {
157		// Return default config if loading fails
158		return config.DefaultConfig()
159	}
160
161	return mgr.Get()
162}

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