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