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}