Skip to content

Commit f4bf733

Browse files
committed
feat: query activities by day
1 parent caa60fd commit f4bf733

File tree

9 files changed

+487
-31
lines changed

9 files changed

+487
-31
lines changed

core/storage.go

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type SQLiteStorage struct {
1515
db *sql.DB
1616
saveStmt *sql.Stmt
1717
getRecentStmt *sql.Stmt
18+
getBetweenStmt *sql.Stmt
1819
countStmt *sql.Stmt
1920
}
2021

@@ -101,6 +102,17 @@ func (s *SQLiteStorage) prepareStatements() error {
101102
return fmt.Errorf("failed to prepare get recent statement: %w", err)
102103
}
103104

105+
s.getBetweenStmt, err = s.db.Prepare(`
106+
SELECT id, timestamp, lines, language, project, editor, file,
107+
branch, is_write
108+
FROM activities
109+
WHERE timestamp >= ? AND timestamp < ?
110+
ORDER BY timestamp ASC
111+
`)
112+
if err != nil {
113+
return fmt.Errorf("failed to prepare get between statement: %w", err)
114+
}
115+
104116
s.countStmt, err = s.db.Prepare("SELECT COUNT(*) FROM activities")
105117
if err != nil {
106118
return fmt.Errorf("failed to prepare count statement: %w", err)
@@ -199,8 +211,8 @@ func (s *SQLiteStorage) SaveActivity(activity Activity) error {
199211
total_time = total_time + excluded.total_time,
200212
total_lines = total_lines + excluded.total_lines,
201213
main_language = CASE
202-
WHEN (SELECT total_time FROM daily_language_summary WHERE date = excluded.date AND language = excluded.main_language) >
203-
(SELECT COALESCE(MAX(total_time), 0) FROM daily_language_summary WHERE date = excluded.date AND language = excluded.main_language)
214+
WHEN (SELECT total_time FROM daily_language_summary WHERE date = excluded.date AND language = excluded.main_language) >=
215+
(SELECT COALESCE(MAX(total_time), 0) FROM daily_language_summary WHERE date = excluded.date AND language = daily_project_summary.main_language)
204216
THEN excluded.main_language
205217
ELSE daily_project_summary.main_language
206218
END,
@@ -238,6 +250,16 @@ func (s *SQLiteStorage) GetActivitiesSince(since time.Time) ([]Activity, error)
238250
return scanActivities(rows, 1000)
239251
}
240252

253+
func (s *SQLiteStorage) GetActivitiesBetween(from, to time.Time) ([]Activity, error) {
254+
rows, err := s.getBetweenStmt.Query(from.Unix(), to.Unix())
255+
if err != nil {
256+
return nil, fmt.Errorf("failed to query activities: %w", err)
257+
}
258+
defer rows.Close()
259+
260+
return scanActivities(rows, 100)
261+
}
262+
241263
func (s *SQLiteStorage) GetActivityCount() (int, error) {
242264
var count int
243265
err := s.countStmt.QueryRow().Scan(&count)
@@ -407,6 +429,14 @@ func (s *SQLiteStorage) RebuildSummaries() error {
407429
projSummary := make(map[string]projAgg)
408430
editorSummary := make(map[string]editorAgg)
409431

432+
// To track most used language per project per day during rebuild
433+
type projLangKey struct {
434+
date string
435+
project string
436+
lang string
437+
}
438+
projLangTime := make(map[projLangKey]float64)
439+
410440
var prevTS int64
411441
for i, a := range activitiesList {
412442
ts := a.timestamp
@@ -443,10 +473,18 @@ func (s *SQLiteStorage) RebuildSummaries() error {
443473
ps := projSummary[projKey]
444474
ps.totalTime += gap
445475
ps.totalLines += a.lines
446-
if a.language != "" {
476+
ps.fileCount++
477+
478+
// Track time per language for this project/day
479+
plk := projLangKey{date, a.project, a.language}
480+
projLangTime[plk] += gap
481+
482+
// Update main language if this one now has more time
483+
currentMainTime := projLangTime[projLangKey{date, a.project, ps.mainLanguage}]
484+
if a.language != "" && (ps.mainLanguage == "" || projLangTime[plk] > currentMainTime) {
447485
ps.mainLanguage = a.language
448486
}
449-
ps.fileCount++
487+
450488
projSummary[projKey] = ps
451489

452490
editorKey := date + "|" + a.editor
@@ -534,6 +572,9 @@ func (s *SQLiteStorage) Close() error {
534572
if s.getRecentStmt != nil {
535573
s.getRecentStmt.Close()
536574
}
575+
if s.getBetweenStmt != nil {
576+
s.getBetweenStmt.Close()
577+
}
537578
if s.countStmt != nil {
538579
s.countStmt.Close()
539580
}

core/tracker_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ func (m *mockStorage) GetActivitiesSince(since time.Time) ([]Activity, error) {
2727
return result, nil
2828
}
2929

30+
func (m *mockStorage) GetActivitiesBetween(from, to time.Time) ([]Activity, error) {
31+
var result []Activity
32+
for _, a := range m.activities {
33+
if !a.Timestamp.Before(from) && a.Timestamp.Before(to) {
34+
result = append(result, a)
35+
}
36+
}
37+
return result, nil
38+
}
39+
3040
func (m *mockStorage) GetActivityCount() (int, error) {
3141
return len(m.activities), nil
3242
}

core/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ type EditorRow struct {
6161
type Storage interface {
6262
SaveActivity(Activity) error
6363
GetActivitiesSince(time.Time) ([]Activity, error)
64+
GetActivitiesBetween(from, to time.Time) ([]Activity, error)
6465
GetActivityCount() (int, error)
6566
GetPeriodSummary(from, to time.Time) (PeriodSummary, error)
6667
GetLanguageSummary(from, to time.Time) ([]LanguageRow, error)

main.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ func main() {
3737
handleToday()
3838
case "projects":
3939
handleProjects()
40+
case "day":
41+
handleDay(os.Args[2:])
4042
case "api":
4143
handleAPI(os.Args[2:])
4244
case "optimize":
@@ -72,6 +74,7 @@ func printHelp() {
7274
fmt.Println(" track Track a file activity")
7375
fmt.Println(" stats Show statistics (pretty printed)")
7476
fmt.Println(" today Show today's activity")
77+
fmt.Println(" day Show stats for a specific day")
7578
fmt.Println(" projects Show project breakdown")
7679
fmt.Println(" api Output JSON for external tools (Neovim, etc)")
7780
fmt.Println(" optimize Optimize database (run monthly)")
@@ -220,6 +223,54 @@ func handleProjects() {
220223
printProjectStats(apiStats)
221224
}
222225

226+
func handleDay(args []string) {
227+
fs := flag.NewFlagSet("day", flag.ExitOnError)
228+
dateStr := fs.String("date", time.Now().Format("2006-01-02"), "Date in YYYY-MM-DD format")
229+
compact := fs.Bool("compact", false, "Output compact JSON")
230+
tzName := fs.String("tz", "", "Timezone name (e.g. America/New_York)")
231+
fs.Parse(args)
232+
233+
dbPath, err := core.GetDefaultDBPath()
234+
if err != nil {
235+
fmt.Fprintf(os.Stderr, "Error resolving DB path: %v\n", err)
236+
os.Exit(1)
237+
}
238+
239+
storage, err := core.OpenReadOnlyStorage(dbPath)
240+
if err != nil {
241+
fmt.Fprintf(os.Stderr, "Error opening database: %v\n", err)
242+
os.Exit(1)
243+
}
244+
defer storage.Close()
245+
246+
tz := time.Local
247+
if *tzName != "" {
248+
location, err := time.LoadLocation(*tzName)
249+
if err != nil {
250+
fmt.Fprintf(os.Stderr, "Error loading timezone: %v\n", err)
251+
os.Exit(1)
252+
}
253+
tz = location
254+
}
255+
256+
calc := stats.NewCalculator(tz)
257+
dayStats, err := calc.CalculateDay(storage, *dateStr)
258+
if err != nil {
259+
fmt.Fprintf(os.Stderr, "Error calculating day stats: %v\n", err)
260+
os.Exit(1)
261+
}
262+
263+
encoder := json.NewEncoder(os.Stdout)
264+
if !*compact {
265+
encoder.SetIndent("", " ")
266+
}
267+
268+
if err := encoder.Encode(dayStats); err != nil {
269+
fmt.Fprintf(os.Stderr, "Error encoding JSON: %v\n", err)
270+
os.Exit(1)
271+
}
272+
}
273+
223274
func handleAPI(args []string) {
224275
fs := flag.NewFlagSet("api", flag.ExitOnError)
225276
compact := fs.Bool("compact", false, "Output compact JSON (no indentation)")

stats/api.go

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,6 @@ func (c *Calculator) CalculateAPI(storage core.Storage, opts APIOptions) (*APISt
8383
lifetimeHours[lr.Language] = lr.TotalTime / 3600
8484
}
8585

86-
projectLangs := make(map[string]map[string]float64)
87-
for _, pr := range allTimeProjs {
88-
pl := make(map[string]float64)
89-
for _, lr := range allTimeLangs {
90-
pl[lr.Language] = lr.TotalTime / 3600
91-
}
92-
projectLangs[pr.Project] = pl
93-
}
94-
9586
cutoff := time.Now().AddDate(0, 0, -opts.LoadRecentDays)
9687
activities, err := storage.GetActivitiesSince(cutoff)
9788
if err != nil {
@@ -104,6 +95,15 @@ func (c *Calculator) CalculateAPI(storage core.Storage, opts APIOptions) (*APISt
10495
activities, sessions := sessionMgr.GroupAndCalculate(activities)
10596
sessionsByDay := c.indexSessionsByDay(sessions)
10697

98+
// Build projectLangs map using durations from grouped activities
99+
projectLangs := make(map[string]map[string]float64)
100+
for _, a := range activities {
101+
if projectLangs[a.Project] == nil {
102+
projectLangs[a.Project] = make(map[string]float64)
103+
}
104+
projectLangs[a.Project][a.Language] += a.Duration
105+
}
106+
107107
today := c.buildPeriodFromSummary("today", todaySummary, todayLangs, todayProjs, todayEditors, sessions, sessionsByDay, lifetimeHours, projectLangs, todayStart, now, activities)
108108
yesterday := c.buildPeriodFromSummary("yesterday", yesterdaySummary, yesterdayLangs, yesterdayProjs, yesterdayEditors, sessions, sessionsByDay, lifetimeHours, projectLangs, yesterdayStart, todayStart, activities)
109109
thisWeek := c.buildPeriodFromSummary("this_week", thisWeekSummary, thisWeekLangs, thisWeekProjs, thisWeekEditors, sessions, sessionsByDay, lifetimeHours, projectLangs, thisWeekStart, now, activities)
@@ -310,22 +310,62 @@ func (c *Calculator) convertLanguageRows(rows []core.LanguageRow, lifetimeHours
310310
func (c *Calculator) convertProjectRows(rows []core.ProjectRow, projectLangs map[string]map[string]float64, total float64) []APIProjectStats {
311311
result := make([]APIProjectStats, 0, len(rows))
312312
for _, r := range rows {
313-
mainLang := r.MainLanguage
313+
mainLang := ""
314+
315+
if langs, ok := projectLangs[r.Project]; ok {
316+
// Priority 1: Most used actual programming language (CODE)
317+
maxDur := -1.0
318+
for lang, dur := range langs {
319+
if IsCodeLanguage(lang) && dur > maxDur {
320+
maxDur = dur
321+
mainLang = lang
322+
}
323+
}
324+
325+
// Priority 2: Most used Documentation or Markup (Useful for notes/web)
326+
if mainLang == "" {
327+
maxDur = -1.0
328+
for lang, dur := range langs {
329+
class := GetLanguageClass(lang)
330+
if (class == "doc" || class == "markup") && IsValidLanguage(lang) && dur > maxDur {
331+
maxDur = dur
332+
mainLang = lang
333+
}
334+
}
335+
}
314336

315-
// If mainLang is not a code language, find the first code language from projectLangs
316-
if mainLang == "" || !IsCodeLanguage(mainLang) {
317-
if langs, ok := projectLangs[r.Project]; ok {
318-
for lang := range langs {
319-
if IsCodeLanguage(lang) {
337+
// Priority 3: Most used Data or Config
338+
if mainLang == "" {
339+
maxDur = -1.0
340+
for lang, dur := range langs {
341+
class := GetLanguageClass(lang)
342+
if (class == "data" || class == "config") && IsValidLanguage(lang) && dur > maxDur {
343+
maxDur = dur
320344
mainLang = lang
321-
break
322345
}
323346
}
324347
}
348+
349+
// Priority 4: Any other non-meta valid language
350+
if mainLang == "" {
351+
maxDur = -1.0
352+
for lang, dur := range langs {
353+
if IsValidLanguage(lang) && dur > maxDur {
354+
maxDur = dur
355+
mainLang = lang
356+
}
357+
}
358+
}
359+
}
360+
361+
// Fallback to the row's main language only if it's actually valid
362+
if mainLang == "" && IsValidLanguage(r.MainLanguage) {
363+
mainLang = r.MainLanguage
325364
}
326365

327-
if mainLang == "" || !IsCodeLanguage(mainLang) {
328-
mainLang = "Mixed"
366+
// Final safety fallback
367+
if mainLang == "" {
368+
mainLang = "Project"
329369
}
330370

331371
pct := 0.0

0 commit comments

Comments
 (0)