1package cli
2
3import (
4 "context"
5 "database/sql"
6 "fmt"
7 "log"
8 "os"
9 "strconv"
10 "strings"
11 "text/tabwriter"
12 "time"
13
14 "github.com/spf13/cobra"
15)
16
17const (
18 unknownValue = "Unknown"
19 defaultHistoryLimit = 20
20 defaultSearchLimit = 10
21 maxURLDisplay = 50
22 maxTitleDisplay = 40
23 maxURLCompact = 60
24 maxTitleCompact = 30
25 tabSpacing = 2
26)
27
28// NewHistoryCmd creates the history command
29func NewHistoryCmd() *cobra.Command {
30 cmd := &cobra.Command{
31 Use: "history",
32 Short: "Manage browsing history",
33 Long: `Manage browsing history with various subcommands:
34 list - Show recent browsing history
35 search - Search through history
36 clear - Clear history (with confirmation)`,
37 RunE: func(cmd *cobra.Command, args []string) error {
38 // Default to list if no subcommand
39 return listHistory(cmd, args)
40 },
41 }
42
43 // List subcommand (also default behavior)
44 listCmd := &cobra.Command{
45 Use: "list",
46 Short: "List recent browsing history",
47 Long: `Display recent browsing history with visit counts and timestamps.`,
48 RunE: listHistory,
49 }
50 listCmd.Flags().IntP("limit", "n", defaultHistoryLimit, "Number of history entries to show")
51 listCmd.Flags().BoolP("verbose", "v", false, "Show detailed information")
52
53 // Search subcommand
54 searchCmd := &cobra.Command{
55 Use: "search <query>",
56 Short: "Search through browsing history",
57 Long: `Search for URLs and titles in browsing history.`,
58 Args: cobra.ExactArgs(1),
59 RunE: searchHistory,
60 }
61 searchCmd.Flags().IntP("limit", "n", defaultSearchLimit, "Number of search results to show")
62
63 // Clear subcommand
64 clearCmd := &cobra.Command{
65 Use: "clear",
66 Short: "Clear browsing history",
67 Long: `Clear all browsing history. This action cannot be undone.`,
68 RunE: clearHistory,
69 }
70 clearCmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
71
72 // Stats subcommand
73 statsCmd := &cobra.Command{
74 Use: "stats",
75 Short: "Show history statistics",
76 Long: `Display statistics about your browsing history.`,
77 RunE: showStats,
78 }
79
80 // Add global flags
81 cmd.Flags().IntP("limit", "n", defaultHistoryLimit, "Number of history entries to show")
82 cmd.Flags().BoolP("verbose", "v", false, "Show detailed information")
83
84 // Add subcommands
85 cmd.AddCommand(listCmd)
86 cmd.AddCommand(searchCmd)
87 cmd.AddCommand(clearCmd)
88 cmd.AddCommand(statsCmd)
89
90 return cmd
91}
92
93// listHistory displays recent browsing history
94func listHistory(cmd *cobra.Command, _ []string) error {
95 cli, err := NewCLI()
96 if err != nil {
97 return fmt.Errorf("failed to initialize CLI: %w", err)
98 }
99 defer func() {
100 if err := cli.Close(); err != nil {
101 fmt.Fprintf(os.Stderr, "Warning: failed to close database: %v\n", err)
102 }
103 }()
104
105 ctx := context.Background()
106
107 limit, _ := cmd.Flags().GetInt("limit")
108 verbose, _ := cmd.Flags().GetBool("verbose")
109
110 history, err := cli.Queries.GetHistory(ctx, int64(limit))
111 if err != nil {
112 return fmt.Errorf("failed to get history: %w", err)
113 }
114
115 if len(history) == 0 {
116 fmt.Println("No browsing history found.")
117 return nil
118 }
119
120 // Create tabwriter for aligned output
121 w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
122 defer func() {
123 if err := w.Flush(); err != nil {
124 log.Printf("Warning: failed to flush output: %v", err)
125 }
126 }()
127
128 if verbose {
129 if _, err := fmt.Fprintln(w, "ID\tVISITS\tLAST VISITED\tURL\tTITLE"); err != nil {
130 log.Printf("Warning: failed to write header: %v", err)
131 }
132 if _, err := fmt.Fprintln(w, "--\t------\t------------\t---\t-----"); err != nil {
133 log.Printf("Warning: failed to write separator: %v", err)
134 }
135
136 for _, entry := range history {
137 visits := "1"
138 if entry.VisitCount.Valid {
139 visits = strconv.FormatInt(entry.VisitCount.Int64, 10)
140 }
141
142 lastVisited := unknownValue
143 if entry.LastVisited.Valid {
144 lastVisited = entry.LastVisited.Time.Format("2006-01-02 15:04")
145 }
146
147 title := entry.Title.String
148 if !entry.Title.Valid || title == "" {
149 title = "-"
150 }
151
152 url := truncateString(entry.Url, maxURLDisplay)
153 title = truncateString(title, maxTitleDisplay)
154
155 if _, err := fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\n",
156 entry.ID, visits, lastVisited, url, title); err != nil {
157 log.Printf("Warning: failed to write history entry: %v", err)
158 }
159 }
160 } else {
161 if _, err := fmt.Fprintln(w, "VISITS\tLAST VISITED\tURL"); err != nil {
162 log.Printf("Warning: failed to write header: %v", err)
163 }
164 if _, err := fmt.Fprintln(w, "------\t------------\t---"); err != nil {
165 log.Printf("Warning: failed to write separator: %v", err)
166 }
167
168 for _, entry := range history {
169 visits := "1"
170 if entry.VisitCount.Valid {
171 visits = strconv.FormatInt(entry.VisitCount.Int64, 10)
172 }
173
174 lastVisited := unknownValue
175 if entry.LastVisited.Valid {
176 // Show relative time for recent entries
177 if time.Since(entry.LastVisited.Time) < 24*time.Hour {
178 lastVisited = entry.LastVisited.Time.Format("15:04")
179 } else {
180 lastVisited = entry.LastVisited.Time.Format("Jan 02")
181 }
182 }
183
184 url := entry.Url
185 if len(url) > maxURLCompact {
186 url = url[:57] + "..."
187 }
188
189 if _, err := fmt.Fprintf(w, "%s\t%s\t%s\n", visits, lastVisited, url); err != nil {
190 log.Printf("Warning: failed to write history entry: %v", err)
191 }
192 }
193 }
194
195 return nil
196}
197
198// searchHistory searches through browsing history
199func searchHistory(cmd *cobra.Command, args []string) error {
200 cli, err := NewCLI()
201 if err != nil {
202 return fmt.Errorf("failed to initialize CLI: %w", err)
203 }
204 defer func() {
205 if err := cli.Close(); err != nil {
206 fmt.Fprintf(os.Stderr, "Warning: failed to close database: %v\n", err)
207 }
208 }()
209
210 ctx := context.Background()
211 query := args[0]
212 limit, _ := cmd.Flags().GetInt("limit")
213
214 // Search both URL and title
215 searchQuery := sql.NullString{String: query, Valid: true}
216 results, err := cli.Queries.SearchHistory(ctx, searchQuery, searchQuery, int64(limit))
217 if err != nil {
218 return fmt.Errorf("failed to search history: %w", err)
219 }
220
221 if len(results) == 0 {
222 fmt.Printf("No history entries found matching '%s'.\n", query)
223 return nil
224 }
225
226 fmt.Printf("Found %d result(s) for '%s':\n\n", len(results), query)
227
228 // Create tabwriter for aligned output
229 w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
230 defer func() {
231 if err := w.Flush(); err != nil {
232 log.Printf("Warning: failed to flush output: %v", err)
233 }
234 }()
235
236 if _, err := fmt.Fprintln(w, "VISITS\tLAST VISITED\tURL\tTITLE"); err != nil {
237 log.Printf("Warning: failed to write header: %v", err)
238 }
239 if _, err := fmt.Fprintln(w, "------\t------------\t---\t-----"); err != nil {
240 log.Printf("Warning: failed to write separator: %v", err)
241 }
242
243 for _, entry := range results {
244 visits := "1"
245 if entry.VisitCount.Valid {
246 visits = strconv.FormatInt(entry.VisitCount.Int64, 10)
247 }
248
249 lastVisited := "Unknown"
250 if entry.LastVisited.Valid {
251 lastVisited = entry.LastVisited.Time.Format("Jan 02 15:04")
252 }
253
254 title := entry.Title.String
255 if !entry.Title.Valid || title == "" {
256 title = "-"
257 }
258
259 url := truncateString(entry.Url, maxURLDisplay)
260 title = truncateString(title, maxTitleCompact)
261
262 if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", visits, lastVisited, url, title); err != nil {
263 log.Printf("Warning: failed to write history entry: %v", err)
264 }
265 }
266
267 return nil
268}
269
270// clearHistory clears all browsing history
271func clearHistory(cmd *cobra.Command, _ []string) error {
272 force, _ := cmd.Flags().GetBool("force")
273
274 // Confirmation unless --force is used
275 if !force {
276 fmt.Print("This will permanently delete all browsing history. Continue? [y/N]: ")
277 var response string
278 _, _ = fmt.Scanln(&response)
279 response = strings.ToLower(strings.TrimSpace(response))
280
281 if response != "y" && response != "yes" {
282 fmt.Println("Operation cancelled.")
283 return nil
284 }
285 }
286
287 cli, err := NewCLI()
288 if err != nil {
289 return fmt.Errorf("failed to initialize CLI: %w", err)
290 }
291 defer func() {
292 if err := cli.Close(); err != nil {
293 fmt.Fprintf(os.Stderr, "Warning: failed to close database: %v\n", err)
294 }
295 }()
296
297 // Execute DELETE query
298 result, err := cli.DB.Exec("DELETE FROM history")
299 if err != nil {
300 return fmt.Errorf("failed to clear history: %w", err)
301 }
302
303 rowsAffected, err := result.RowsAffected()
304 if err != nil {
305 return fmt.Errorf("failed to get affected rows: %w", err)
306 }
307
308 fmt.Printf("Successfully cleared %d history entries.\n", rowsAffected)
309 return nil
310}
311
312// showStats displays statistics about browsing history
313func showStats(_ *cobra.Command, _ []string) error {
314 cli, err := NewCLI()
315 if err != nil {
316 return fmt.Errorf("failed to initialize CLI: %w", err)
317 }
318 defer func() {
319 if err := cli.Close(); err != nil {
320 fmt.Fprintf(os.Stderr, "Warning: failed to close database: %v\n", err)
321 }
322 }()
323
324 ctx := context.Background()
325
326 // Get total entries
327 var totalEntries int64
328 err = cli.DB.QueryRowContext(ctx, "SELECT COUNT(*) FROM history").Scan(&totalEntries)
329 if err != nil {
330 return fmt.Errorf("failed to get total entries: %w", err)
331 }
332
333 // Get total visits
334 var totalVisits sql.NullInt64
335 err = cli.DB.QueryRowContext(ctx, "SELECT SUM(visit_count) FROM history").Scan(&totalVisits)
336 if err != nil {
337 return fmt.Errorf("failed to get total visits: %w", err)
338 }
339
340 // Get date range
341 var oldestDate, newestDate sql.NullTime
342 err = cli.DB.QueryRowContext(ctx, "SELECT MIN(created_at), MAX(last_visited) FROM history").Scan(&oldestDate, &newestDate)
343 if err != nil {
344 return fmt.Errorf("failed to get date range: %w", err)
345 }
346
347 // Get top domains
348 rows, err := cli.DB.QueryContext(ctx, `
349 SELECT
350 CASE
351 WHEN url LIKE 'http://%' THEN substr(url, 8)
352 WHEN url LIKE 'https://%' THEN substr(url, 9)
353 ELSE url
354 END as domain,
355 COUNT(*) as count,
356 SUM(visit_count) as total_visits
357 FROM history
358 GROUP BY domain
359 ORDER BY total_visits DESC
360 LIMIT 5
361 `)
362 if err != nil {
363 return fmt.Errorf("failed to get top domains: %w", err)
364 }
365 defer func() {
366 if err := rows.Close(); err != nil {
367 log.Printf("Warning: failed to close rows: %v", err)
368 }
369 }()
370
371 fmt.Println("Browsing History Statistics")
372 fmt.Println("==========================")
373 fmt.Printf("Total unique URLs: %d\n", totalEntries)
374
375 if totalVisits.Valid {
376 fmt.Printf("Total visits: %d\n", totalVisits.Int64)
377 }
378
379 if oldestDate.Valid && newestDate.Valid {
380 fmt.Printf("Date range: %s to %s\n",
381 oldestDate.Time.Format("2006-01-02"),
382 newestDate.Time.Format("2006-01-02"))
383 }
384
385 fmt.Println("\nTop 5 domains:")
386 fmt.Println("--------------")
387
388 w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
389 defer func() {
390 if err := w.Flush(); err != nil {
391 log.Printf("Warning: failed to flush output: %v", err)
392 }
393 }()
394
395 if _, err := fmt.Fprintln(w, "DOMAIN\tURLs\tVISITS"); err != nil {
396 log.Printf("Warning: failed to write header: %v", err)
397 }
398 if _, err := fmt.Fprintln(w, "------\t----\t------"); err != nil {
399 log.Printf("Warning: failed to write separator: %v", err)
400 }
401
402 for rows.Next() {
403 var domain string
404 var urlCount, visitCount int64
405
406 if err := rows.Scan(&domain, &urlCount, &visitCount); err != nil {
407 continue
408 }
409
410 // Extract just the domain part
411 if idx := strings.Index(domain, "/"); idx > 0 {
412 domain = domain[:idx]
413 }
414
415 if _, err := fmt.Fprintf(w, "%s\t%d\t%d\n", domain, urlCount, visitCount); err != nil {
416 log.Printf("Warning: failed to write domain entry: %v", err)
417 }
418 }
419
420 return nil
421}