1- local M = {}
1+ -- CodeMe
2+ -- - Plugin tracks file activity
3+ -- - Git diff calculated on save
4+ -- - Backend stores and aggregates data
25
3- -- File state cache for tracking line changes
4- local file_state = {}
6+ local M = {}
57
6- local config = {
7- codeme_bin = " codeme" , -- Binary name in PATH
8- auto_track = true , -- Auto track on file save and open
9- track_on_idle = false , -- Track on cursor idle (not implemented yet)
10- verbose = false , -- Show tracking notifications
11- auto_install = true , -- Auto-install binary if not found
12- -- Goals configuration
13- goals = {
14- daily_hours = 5 , -- Daily goal in hours (set to 0 to disable)
15- daily_lines = 1000 , -- Daily goal in lines (set to 0 to disable)
16- },
8+ M .config = {
9+ codeme_bin = os.getenv (" CODEME_BIN" ) or " codeme" ,
10+ verbose = false ,
11+ auto_track = true ,
1712}
1813
19- -- Expose config for other modules
20- function M .get_config ()
21- return config
22- end
23-
2414function M .setup (opts )
25- config = vim .tbl_deep_extend (" force" , config , opts or {})
15+ opts = opts or {}
16+ M .config = vim .tbl_deep_extend (" force" , M .config , opts )
2617
27- -- Setup highlights
18+ -- Setup highlights (must be before commands/tracking)
2819 local highlights = require (" codeme.highlights" )
2920 highlights .setup ()
3021 highlights .setup_autocmd () -- Auto-reload on colorscheme change
3122
32- -- Check if binary is installed
33- local installer = require (" codeme.installer" )
34- if config .auto_install and not installer .is_installed () then
35- installer .ensure_installed (function (success )
36- if success then
37- local bin = installer .get_binary ()
38- if bin then
39- config .codeme_bin = bin
40- end
41- M .setup_tracking ()
42- end
43- end )
44- else
45- -- Update config with actual binary path
46- local bin = installer .get_binary ()
47- if bin then
48- config .codeme_bin = bin
49- end
23+ M .setup_commands ()
24+
25+ if M .config .auto_track then
5026 M .setup_tracking ()
5127 end
52-
53- M .setup_commands ()
5428end
5529
5630function M .setup_commands ()
@@ -71,239 +45,77 @@ function M.setup_commands()
7145 end , { desc = " Show project breakdown" })
7246
7347 vim .api .nvim_create_user_command (" CodeMeTrack" , function ()
74- M .track_current_file ( )
75- if config .verbose then
76- vim .notify (" CodeMe: Tracked current file " , vim .log .levels .INFO )
48+ M .track ( true )
49+ if M . config .verbose then
50+ vim .notify (" CodeMe: Heartbeat sent " , vim .log .levels .INFO )
7751 end
78- end , { desc = " Manually track current file" })
79-
80- vim .api .nvim_create_user_command (" CodeMeInstall" , function ()
81- local installer = require (" codeme.installer" )
82- installer .install_latest (function (success , err )
83- if success then
84- vim .notify (" ✓ codeme installed successfully" , vim .log .levels .INFO )
85- -- Update binary path
86- config .codeme_bin = installer .get_binary () or " codeme"
87- M .setup_tracking ()
88- else
89- vim .notify (" Failed to install codeme: " .. (err or " unknown error" ), vim .log .levels .ERROR )
90- end
91- end )
92- end , { desc = " Install/update codeme binary" })
93-
94- vim .api .nvim_create_user_command (" CodeMeVersion" , function ()
95- local installer = require (" codeme.installer" )
96- installer .get_version (function (version )
97- if version then
98- vim .notify (" codeme " .. version , vim .log .levels .INFO )
99- else
100- vim .notify (" codeme not installed" , vim .log .levels .WARN )
101- end
102- end )
103- end , { desc = " Show codeme version" })
52+ end , { desc = " Manually send heartbeat" })
10453end
10554
10655function M .setup_tracking ()
107- if not config .auto_track then
108- return
109- end
110-
11156 local augroup = vim .api .nvim_create_augroup (" CodeMeTrack" , { clear = true })
11257
113- -- Track on file save
58+ -- Track on save - this is where we calculate git diff
11459 vim .api .nvim_create_autocmd (" BufWritePost" , {
11560 group = augroup ,
11661 callback = function ()
117- M .track_current_file ( )
62+ M .track ( true )
11863 end ,
64+ desc = " CodeMe: Track on save" ,
11965 })
12066
121- -- Track when opening a file (but throttle to avoid spam)
67+ -- Track on open - just a heartbeat, no line counting
12268 vim .api .nvim_create_autocmd (" BufReadPost" , {
12369 group = augroup ,
12470 callback = function ()
125- -- Delay to let buffer fully load
12671 vim .defer_fn (function ()
127- M .track_current_file ( )
72+ M .track ( false )
12873 end , 100 )
12974 end ,
75+ desc = " CodeMe: Track on open" ,
13076 })
131-
132- -- Track on focus gained (when switching back to Neovim)
133- vim .api .nvim_create_autocmd (" FocusGained" , {
134- group = augroup ,
135- callback = function ()
136- M .track_current_file ()
137- end ,
138- })
139- end
140-
141- -- Helper: Get git diff stats for a file
142- local function get_git_diff (filepath )
143- -- Check if we're in a git repo
144- if vim .v .shell_error ~= 0 then
145- return nil -- Not a git repo
146- end
147-
148- -- Get git diff stats (staged + unstaged changes)
149- local cmd = string.format (" git diff HEAD --numstat -- %s 2>/dev/null" , vim .fn .shellescape (filepath ))
150- local output = vim .fn .system (cmd )
151- if vim .v .shell_error ~= 0 or output == " " then
152- return nil
153- end
154-
155- -- Parse: "5 2 file.lua" = 5 added, 2 deleted = 7 total changes
156- local added , deleted = output :match (" ^(%d+)%s+(%d+)" )
157- if added and deleted then
158- return tonumber (added ) + tonumber (deleted )
159- end
160- return nil
16177end
16278
163- -- Track current file
164- function M .track_current_file ()
165- local file = vim .fn .expand (" %:p" )
166- local lang = vim .bo .filetype
167- local bufnr = vim .api .nvim_get_current_buf ()
168- local current_lines = vim .api .nvim_buf_line_count (bufnr )
169-
170- -- Skip if no file or empty buffer
171- if file == " " or not vim .api .nvim_buf_is_valid (bufnr ) then
172- return
173- end
174-
175- -- Skip certain filetypes
176- local skip_fts = { " " , " NvimTree" , " neo-tree" , " dashboard" , " alpha" , " help" , " qf" , " fugitive" , " TelescopePrompt" }
177- if vim .tbl_contains (skip_fts , lang ) then
178- return
179- end
180-
181- -- Skip non-file buffers
182- if vim .bo .buftype ~= " " then
183- return
184- end
185-
186- -- Try git diff first (more accurate for git repos)
187- local lines_changed = get_git_diff (file )
188-
189- -- Fallback to delta calculation if not in git or no changes detected
190- if not lines_changed or lines_changed == 0 then
191- local previous_lines = file_state [file ] or current_lines
192- lines_changed = math.abs (current_lines - previous_lines )
193- end
194-
195- -- Update cache
196- file_state [file ] = current_lines
197-
198- -- Call codeme binary to track (with both changed and total)
199- local cmd = string.format (
200- " %s track --file %s --lang %s --lines %d --total %d" ,
201- config .codeme_bin ,
202- vim .fn .shellescape (file ),
203- lang ~= " " and lang or " unknown" ,
204- lines_changed , -- Changed lines
205- current_lines -- Total lines for reference
206- )
207-
208- vim .fn .jobstart (cmd , {
209- detach = true ,
210- on_exit = function (_ , code )
211- if code ~= 0 then
212- if config .verbose then
213- vim .notify (" CodeMe: Failed to track file" , vim .log .levels .WARN )
214- end
215- elseif config .verbose then
216- vim .notify (
217- string.format (" CodeMe: Tracked %s (+%d lines)" , vim .fn .fnamemodify (file , " :t" ), lines_changed ),
218- vim .log .levels .INFO
219- )
220- end
221- end ,
79+ --- Send tracking heartbeat
80+ --- @param is_save boolean true if this is a save event
81+ function M .track (is_save )
82+ local tracker = require (" codeme.tracker" )
83+ tracker .send_heartbeat ({
84+ is_save = is_save ,
85+ codeme_bin = M .config .codeme_bin ,
86+ verbose = M .config .verbose ,
22287 })
22388end
22489
225- -- Get stats from codeme binary
226- -- @param callback function to call with stats
227- -- @param today_only boolean if true, only get today's stats
90+ --- Get stats from backend
22891function M .get_stats (callback , today_only )
229- local cmd = config .codeme_bin .. " stats --json"
92+ local cmd = M . config .codeme_bin .. " stats --json"
23093 if today_only then
23194 cmd = cmd .. " --today"
23295 end
23396
234- -- DEBUG: Log the command being executed
235- if config .verbose then
236- vim .notify (" CodeMe: Running command: " .. cmd , vim .log .levels .DEBUG )
237- end
238-
23997 vim .fn .jobstart (cmd , {
24098 stdout_buffered = true ,
24199 on_stdout = function (_ , data )
242- -- DEBUG: Log raw data received
243- if config .verbose then
244- vim .notify (string.format (" CodeMe: Received %d data items" , # data ), vim .log .levels .DEBUG )
245- end
246-
247100 if data and # data > 0 then
248- -- Filter out empty strings AND lines that don't look like JSON
249- -- (to handle shell initialization output like "Using Node v22.11.0")
250101 local filtered = vim .tbl_filter (function (line )
251- if line == " " then
252- return false
253- end
254- -- Only accept lines that start with { or are part of JSON
255- -- First real line should start with {
256- return line :match (" ^%s*{" ) or line :match (" [,}%]]%s*$" )
102+ return line ~= " " and (line :match (" ^%s*{" ) or line :match (" [,}%]]%s*$" ))
257103 end , data )
258104
259- -- DEBUG: Log filtered count
260- if config .verbose then
261- vim .notify (string.format (" CodeMe: After filtering: %d items" , # filtered ), vim .log .levels .DEBUG )
262- end
263-
264105 if # filtered > 0 then
265106 local json_str = table.concat (filtered , " " )
266-
267- -- DEBUG: Log JSON string info
268- if config .verbose then
269- vim .notify (string.format (" CodeMe: JSON string length: %d" , # json_str ), vim .log .levels .DEBUG )
270- end
271-
272107 local ok , stats = pcall (vim .json .decode , json_str )
273108 if ok and stats then
274109 callback (stats )
275- else
276- -- Enhanced error message with details
277- vim .notify (
278- string.format (
279- " CodeMe: Failed to parse stats - invalid JSON\n Error: %s\n First 200 chars: %s" ,
280- tostring (stats ),
281- json_str :sub (1 , 200 )
282- ),
283- vim .log .levels .ERROR
284- )
285110 end
286- else
287- vim .notify (" CodeMe: No data after filtering empty lines" , vim .log .levels .WARN )
288- end
289- else
290- vim .notify (" CodeMe: No data received from stdout" , vim .log .levels .WARN )
291- end
292- end ,
293- on_stderr = function (_ , data )
294- if data and # data > 0 then
295- local errors = table.concat (data , " \n " )
296- if errors ~= " " then
297- vim .notify (" CodeMe error: " .. errors , vim .log .levels .ERROR )
298111 end
299112 end
300113 end ,
301- on_exit = function (_ , code )
302- if code ~= 0 then
303- vim .notify (string.format (" CodeMe: Command exited with code %d" , code ), vim .log .levels .ERROR )
304- end
305- end ,
306114 })
307115end
308116
117+ function M .get_config ()
118+ return M .config
119+ end
120+
309121return M
0 commit comments