Skip to content

Commit 210a235

Browse files
committed
feat: add hover on achievements
1 parent 382cd0a commit 210a235

File tree

3 files changed

+501
-234
lines changed

3 files changed

+501
-234
lines changed

lua/codeme/ui/dashboard.lua

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,11 @@ function M.show_window(stat_data)
201201
end
202202

203203
local function close()
204+
local ok, records_tab = pcall(require, "codeme.ui.tabs.records")
205+
if ok and records_tab.teardown_hover then
206+
records_tab.teardown_hover()
207+
end
208+
204209
local win_handle = stats.get_win()
205210
if win_handle and vim.api.nvim_win_is_valid(win_handle) then
206211
vim.api.nvim_win_close(win_handle, true)

lua/codeme/ui/tabs/activity.lua

Lines changed: 195 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,35 @@ local renderer = require("codeme.ui.renderer")
33

44
local M = {}
55

6-
function M.render(stats, width)
7-
stats = require("codeme.util").apply_privacy_mask(stats)
6+
function M.render(stats, width, height)
87
local lines = {}
98
local today = stats.today or {}
109
local today_sessions = today.sessions or {}
1110
local total_time = today.total_time or 0
1211
local focus_score = today.focus_score or 0
1312

14-
-- Header
15-
local today_date = os.date("%A, %B %d")
16-
13+
-- ── Header ─────────────────────────────────────────────────────────
1714
table.insert(lines, {})
1815
table.insert(lines, {
19-
{ " ☀️ Activity Timeline", "exgreen" },
20-
{ string.rep(" ", 30), "commentfg" },
16+
{ " ☀️ Today's Activity", "exgreen" },
17+
{ "", "commentfg" },
18+
{ os.date("%A, %B %d"), "exyellow" },
19+
{ "", "commentfg" },
20+
{ "Total: ", "commentfg" },
2121
{ util.format_duration(total_time), "exgreen" },
22-
{ "", "commentfg" },
23-
{ today_date, "exyellow" },
2422
})
2523
table.insert(lines, {})
2624

2725
if total_time == 0 then
28-
table.insert(lines, { { " No coding activity yet today", "commentfg" } })
29-
table.insert(lines, { { " Start working to track your productivity!", "commentfg" } })
26+
table.insert(
27+
lines,
28+
{ { " No coding activity yet today. Start working to track your productivity!", "commentfg" } }
29+
)
3030
table.insert(lines, {})
3131
return lines
3232
end
3333

34-
-- Hourly Activity Sparkline (Condensed)
34+
-- ── Hourly sparkline ───────────────────────────────────────────────
3535
local hourly_raw = {}
3636
for i = 1, 24 do
3737
hourly_raw[i] = 0
@@ -41,151 +41,227 @@ function M.render(stats, width)
4141
hourly_raw[item.hour + 1] = item.duration or 0
4242
end
4343
end
44-
if #today_sessions > 0 then
45-
local hist_line = { { " Activity Map: ", "commentfg" } }
46-
local hist_segs = renderer.histogram(hourly_raw, 0, 1, "exblue")
47-
for _, s in ipairs(hist_segs) do
48-
table.insert(hist_line, s)
49-
end
50-
table.insert(hist_line, { " Focus: " .. focus_score .. "%", focus_score >= 70 and "exgreen" or "exyellow" })
51-
table.insert(lines, hist_line)
52-
table.insert(lines, { { " 00 04 08 12 16 20 23", "commentfg" } })
5344

54-
-- Peak Block Insight (Restored)
45+
local hist_line = { { " ", "commentfg" } }
46+
for _, s in ipairs(renderer.histogram(hourly_raw, 0, 1, "exblue")) do
47+
table.insert(hist_line, s)
48+
end
49+
table.insert(lines, hist_line)
50+
table.insert(lines, { { " 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23", "commentfg" } })
51+
52+
-- Focus score inline
53+
if focus_score > 0 then
54+
local focus_hl = focus_score >= 70 and "exgreen" or focus_score >= 40 and "exyellow" or "exred"
55+
table.insert(lines, {
56+
{ " Focus score: ", "commentfg" },
57+
{ tostring(focus_score) .. "%", focus_hl },
58+
{ " Peak block: ", "commentfg" },
59+
})
60+
-- find peak 4-hour block
61+
local block_labels = {
62+
{ label = "early morning", start_h = 4, end_h = 7 },
63+
{ label = "morning", start_h = 8, end_h = 11 },
64+
{ label = "afternoon", start_h = 12, end_h = 15 },
65+
{ label = "evening", start_h = 16, end_h = 19 },
66+
{ label = "night", start_h = 20, end_h = 23 },
67+
{ label = "late night", start_h = 0, end_h = 3 },
68+
}
5569
local total_hourly = 0
56-
for _, dur in pairs(hourly_raw) do
57-
total_hourly = total_hourly + dur
70+
for _, v in ipairs(hourly_raw) do
71+
total_hourly = total_hourly + v
5872
end
5973
if total_hourly > 0 then
60-
local blocks = {
61-
{ label = "early morning 🌅", start_h = 4, end_h = 7 },
62-
{ label = "morning ☕", start_h = 8, end_h = 11 },
63-
{ label = "afternoon ☀️", start_h = 12, end_h = 15 },
64-
{ label = "evening 🌆", start_h = 16, end_h = 19 },
65-
{ label = "night 🌃", start_h = 20, end_h = 23 },
66-
{ label = "late night 🌙", start_h = 0, end_h = 3 },
67-
}
68-
local peak_block, max_pct = nil, 0
69-
for _, block in ipairs(blocks) do
70-
local block_time = 0
71-
for h = block.start_h, block.end_h do
72-
block_time = block_time + (hourly_raw[h + 1] or 0)
74+
local peak_block, max_pct = "", 0
75+
for _, b in ipairs(block_labels) do
76+
local bt = 0
77+
for h = b.start_h, b.end_h do
78+
bt = bt + (hourly_raw[h + 1] or 0)
7379
end
74-
local pct = math.floor((block_time / total_hourly) * 100)
80+
local pct = math.floor((bt / total_hourly) * 100)
7581
if pct > max_pct then
7682
max_pct = pct
77-
peak_block = block.label
83+
peak_block = b.label
7884
end
7985
end
80-
if peak_block then
81-
table.insert(lines, {
82-
{ " 💡 You're most productive in the ", "commentfg" },
83-
{ peak_block, "exgreen" },
84-
{ " today.", "commentfg" },
85-
})
86-
end
86+
-- patch the last inserted line to complete it
87+
local last = lines[#lines]
88+
table.insert(last, { peak_block, "normal" })
8789
end
90+
end
91+
table.insert(lines, {})
92+
93+
-- ── Sessions ───────────────────────────────────────────────────────
94+
if #today_sessions == 0 then
95+
table.insert(lines, { { " No sessions recorded yet.", "commentfg" } })
8896
table.insert(lines, {})
97+
return lines
98+
end
8999

90-
-- Narrative Story (Restored)
91-
local first_session = today_sessions[1]
92-
local last_session = today_sessions[#today_sessions]
93-
if first_session.start_time and last_session.end_time then
94-
local start_hour = tonumber(first_session.start_time:sub(12, 13))
95-
local end_hour = tonumber(last_session.end_time:sub(12, 13))
96-
local function get_period(hour)
97-
if hour >= 5 and hour < 12 then
98-
return "morning", "🌅"
99-
elseif hour >= 12 and hour < 17 then
100-
return "afternoon", "☀️"
101-
elseif hour >= 17 and hour < 21 then
102-
return "evening", "🌆"
103-
else
104-
return "late night", "🦉"
100+
-- Compute per-session duration defensively.
101+
-- Backend may send duration directly (seconds) OR start+end timestamps.
102+
-- We never accumulate across sessions — each session is independent.
103+
local function session_duration(s)
104+
-- Prefer explicit duration field if it looks sane (< 24h = 86400s)
105+
if s.duration and s.duration > 0 and s.duration < 86400 then
106+
return s.duration
107+
end
108+
-- Fall back: parse ISO start/end strings
109+
if s.start_time and s.end_time then
110+
local function parse_t(iso)
111+
local h, m, sec = iso:match("T?(%d%d):(%d%d):(%d%d)")
112+
if h then
113+
return tonumber(h) * 3600 + tonumber(m) * 60 + tonumber(sec)
114+
end
115+
-- short "HH:MM" form
116+
h, m = iso:match("(%d%d):(%d%d)$")
117+
if h then
118+
return tonumber(h) * 3600 + tonumber(m) * 60
105119
end
120+
return nil
106121
end
107-
local start_period, start_icon = get_period(start_hour)
108-
local end_period, end_icon = get_period(end_hour)
109-
local story = {
110-
{ " 📖 ", "exgreen" },
111-
{ "You started in the ", "commentfg" },
112-
{ start_period .. " " .. start_icon, "normal" },
113-
}
114-
if start_period ~= end_period then
115-
table.insert(story, { " and continued into the ", "commentfg" })
116-
table.insert(story, { end_period .. " " .. end_icon, "normal" })
122+
local st = parse_t(s.start_time)
123+
local et = parse_t(s.end_time)
124+
if st and et then
125+
local diff = et - st
126+
-- Handle midnight crossover
127+
if diff < 0 then
128+
diff = diff + 86400
129+
end
130+
if diff > 0 and diff < 86400 then
131+
return diff
132+
end
117133
end
118-
table.insert(story, { ".", "commentfg" })
119-
table.insert(lines, story)
120-
table.insert(lines, {})
121134
end
135+
return 0
122136
end
123137

124-
-- Visual Timeline
125-
table.insert(lines, { { " ⏰ Sessions", "exgreen" } })
138+
-- Clamp sum: sessions should never exceed total_time reported by backend.
139+
-- If they do, it means durations overlap or are cumulative — we scale them.
140+
local raw_sum = 0
141+
local durations = {}
142+
for i, s in ipairs(today_sessions) do
143+
durations[i] = session_duration(s)
144+
raw_sum = raw_sum + durations[i]
145+
end
146+
local scale = (raw_sum > 0 and raw_sum > total_time * 1.05) and (total_time / raw_sum) or 1.0
147+
148+
-- Find longest session index for star marker
149+
local max_dur, max_idx = 0, 1
150+
for i, d in ipairs(durations) do
151+
local scaled = math.floor(d * scale)
152+
if scaled > max_dur then
153+
max_dur = scaled
154+
max_idx = i
155+
end
156+
end
157+
158+
-- Section header
159+
table.insert(lines, {
160+
{ " ⏰ Sessions", "exgreen" },
161+
{ " (" .. #today_sessions .. " total)", "commentfg" },
162+
})
126163
table.insert(lines, {})
127164

128-
if #today_sessions == 0 then
129-
table.insert(lines, { { " No sessions recorded yet", "commentfg" } })
130-
table.insert(lines, {})
131-
else
132-
local max_dur = 0
133-
for _, s in ipairs(today_sessions) do
134-
if (s.duration or 0) > max_dur then
135-
max_dur = s.duration
136-
end
165+
-- Bar width for mini per-session bar (adapts to window)
166+
local bar_w = math.max(8, math.min(20, math.floor((width - 60) / 2)))
167+
168+
for i, session in ipairs(today_sessions) do
169+
local dur = math.floor(durations[i] * scale)
170+
local is_last = (i == #today_sessions)
171+
local is_peak = (i == max_idx) and (#today_sessions > 1)
172+
173+
-- Time range string
174+
local t_start = ""
175+
local t_end = ""
176+
if session.start_time then
177+
t_start = session.start_time:match("T?(%d%d:%d%d)") or session.start_time:sub(1, 5)
178+
end
179+
if session.end_time then
180+
t_end = session.end_time:match("T?(%d%d:%d%d)") or session.end_time:sub(1, 5)
181+
end
182+
local time_range = (t_start ~= "" and t_end ~= "") and (t_start .. "" .. t_end)
183+
or (t_start ~= "" and t_start or "??:??")
184+
185+
-- Mini progress bar proportional to total_time
186+
local pct = total_time > 0 and math.floor((dur / total_time) * 100) or 0
187+
local bar_hl = is_peak and "exyellow" or "exblue"
188+
189+
-- Tree connector
190+
local connector = is_last and "╰─" or "├─"
191+
192+
-- Projects / languages (compact)
193+
local proj_str = util.top_items(session.projects or {}, 2)
194+
local lang_str = util.top_items(session.languages or {}, 2)
195+
local meta = ""
196+
if proj_str ~= "" and lang_str ~= "" then
197+
meta = proj_str .. " [" .. lang_str .. "]"
198+
elseif proj_str ~= "" then
199+
meta = proj_str
200+
elseif lang_str ~= "" then
201+
meta = "[" .. lang_str .. "]"
137202
end
138203

139-
for i, session in ipairs(today_sessions) do
140-
local time_str = session.start_time and session.start_time:sub(12, 16) or "??:??"
141-
local dur_str = util.format_duration(session.duration or 0)
142-
local is_peak = (session.duration or 0) == max_dur and #today_sessions > 1
143-
144-
-- Session entry
145-
local line = {
146-
{ " " .. time_str .. " ", "commentfg" },
147-
{ i == #today_sessions and "╰─ " or "├─ ", "exblue" },
148-
{ dur_str, is_peak and "exyellow" or "normal" },
149-
{ is_peak and "" or " ", "exyellow" },
150-
{ util.top_items(session.projects or {}, 2), "exgreen" },
151-
{ " (", "commentfg" },
152-
{ util.top_items(session.languages or {}, 3), "excyan" },
153-
{ ")", "commentfg" },
154-
}
155-
table.insert(lines, line)
156-
157-
-- Break indicator
158-
if session.break_after and session.break_after > 300 then
159-
local icon = session.break_after < 1800 and "" or "🍽️"
204+
-- Session line
205+
local sess_line = {
206+
{ " " .. connector .. " ", "commentfg" },
207+
{ time_range, "commentfg" },
208+
{ " ", "normal" },
209+
{ util.format_duration(dur), is_peak and "exyellow" or "normal" },
210+
{ is_peak and "" or " ", "exyellow" },
211+
{ " ", "normal" },
212+
}
213+
-- Mini bar
214+
local filled = math.floor(pct / 100 * bar_w)
215+
table.insert(sess_line, { string.rep("", filled), bar_hl })
216+
table.insert(sess_line, { string.rep("·", bar_w - filled), "commentfg" })
217+
table.insert(sess_line, { string.format(" %3d%% ", pct), "commentfg" })
218+
if meta ~= "" then
219+
table.insert(sess_line, { meta, "exgreen" })
220+
end
221+
table.insert(lines, sess_line)
222+
223+
-- Break gap between sessions (only show if meaningful > 5 min)
224+
if not is_last then
225+
local break_sec = session.break_after or 0
226+
if break_sec >= 300 then
227+
local break_icon = break_sec >= 3600 and "🍽️ " or ""
160228
table.insert(lines, {
161-
{ " ", "commentfg" },
162-
{ "", "exblue" },
163-
{ icon .. " " .. util.format_duration(session.break_after) .. " break", "commentfg" },
229+
{ " ", "commentfg" },
230+
{ break_icon, "normal" },
231+
{ util.format_duration(break_sec) .. " break", "commentfg" },
164232
})
165-
elseif i < #today_sessions then
166-
table.insert(lines, { { " ", "commentfg" }, { "", "exblue" } })
233+
else
234+
table.insert(lines, { { "", "commentfg" } })
167235
end
168236
end
169-
table.insert(lines, {})
170237
end
238+
table.insert(lines, {})
171239

172-
-- FileType Activity Table (Adaptive)
240+
-- ── Summary bar (total confirmed) ─────────────────────────────────
241+
table.insert(lines, {
242+
{ " Total coded today: ", "commentfg" },
243+
{ util.format_duration(total_time), "exgreen" },
244+
{ " across ", "commentfg" },
245+
{ tostring(#today_sessions), "normal" },
246+
{ " session" .. (#today_sessions == 1 and "" or "s"), "commentfg" },
247+
})
248+
table.insert(lines, {})
249+
250+
-- ── Language breakdown table ───────────────────────────────────────
173251
local languages = today.languages or {}
174252
if #languages > 0 then
175-
table.insert(lines, { { " FileType breakdown", "exgreen" } })
253+
table.insert(lines, { { " 📝 FileType Breakdown", "exgreen" } })
176254
table.insert(lines, {})
177-
178-
local tblLang = { { "FileType", "Time", "Lines", "Share" } }
255+
local tbl = { { "FileType", "Time", "Lines", "Share" } }
179256
for _, lang in ipairs(languages) do
180-
table.insert(tblLang, {
257+
table.insert(tbl, {
181258
lang.name,
182259
util.format_duration(lang.time),
183260
util.format_number(lang.lines),
184261
string.format("%.1f%%", lang.percent_total or 0),
185262
})
186263
end
187-
188-
for _, l in ipairs(renderer.table(tblLang, width - 10)) do
264+
for _, l in ipairs(renderer.table(tbl, width - 10)) do
189265
table.insert(lines, l)
190266
end
191267
table.insert(lines, {})

0 commit comments

Comments
 (0)