Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ The data is stored in a SQLite database, you need to have `sqlite3` in your `PAT
event = "VeryLazy",
opts = {
data_file = vim.fn.stdpath("data") .. "/time-tracker.db",
storage = "sqlite",
},
}
```
Expand All @@ -36,6 +37,7 @@ You need to call the setup function, optionally passing a configuration object (
```lua
require("time-tracker").setup({
data_file = vim.fn.stdpath("data") .. "/time-tracker.db",
storage = "sqlite",
tracking_events = { "BufEnter", "BufWinEnter", "CursorMoved", "CursorMovedI", "WinScrolled" },
tracking_timeout_seconds = 5 * 60, -- 5 minutes
})
Expand Down
22 changes: 19 additions & 3 deletions lua/time-tracker/init.lua
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
local TimeTracker = require("time-tracker/tracker").TimeTracker
local TimeTracker = require("time-tracker.tracker").TimeTracker
local ui = require("time-tracker/ui")
local utils = require("time-tracker/utils")

local supported_storage = { "sqlite", "json" }

--- @type Config
local default_config = {
data_file = vim.fn.stdpath("data") .. "/time-tracker.sqlite",
tracking_events = { "BufEnter", "BufWinEnter", "CursorMoved", "CursorMovedI", "WinScrolled" },
tracking_timeout_seconds = 5 * 60,
storage = "sqlite",
}

--- @param config Config
local get_session_impl = function(config)
if config.storage == "json" then return require("time-tracker.json_session").JsonSession:new(config) end
return require("time-tracker.sqlite_session").SqliteSession:new(config)
end

--- @param user_config Config
local setup = function(user_config)
-- vim.pretty_print(user_config)
local config = vim.tbl_deep_extend("force", default_config, user_config or {})

if not vim.fn.isdirectory(vim.fn.fnamemodify(config.data_file, ":h")) then
Expand All @@ -20,8 +31,13 @@ local setup = function(user_config)
error("Invalid tracking timeout value: " .. config.tracking_timeout_seconds)
end

local tracker = TimeTracker:new(config)
tracker:start_session()
if utils.in_array(config.storage, supported_storage) == false then
error("Invalid storage type: " .. config.storage)
end

local session = get_session_impl(config)

local tracker = TimeTracker:new(session)

for _, event in ipairs(config.tracking_events) do
vim.api.nvim_create_autocmd(event, {
Expand Down
21 changes: 21 additions & 0 deletions lua/time-tracker/interface.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
--- @type Interface
local Interface = {
new = function(self, interface)
local obj = {}
setmetatable(obj, self)
self.__index = self
self.interface = interface
return obj
end,

implements = function(self, interface)
for k, _ in pairs(self.interface) do
if type(interface[k]) ~= "function" then return false end
end
return true
end,
}

return {
Interface = Interface,
}
24 changes: 24 additions & 0 deletions lua/time-tracker/interface_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
local Interface = require("time-tracker/interface").Interface

local SessionInterface = Interface:new({
init = true,
start_session = true,
end_session = true,
})

local FakeImpl = {
init = function() end,
start_session = function() end,
end_session = function() end,
}
local InvalidImpl = {
start_session = function() end,
end_session = function() end,
}

describe("Interface", function()
it("implements interface", function()
expect(SessionInterface:implements(FakeImpl)).toBe(true)
expect(SessionInterface:implements(InvalidImpl)).toBe(false)
end)
end)
130 changes: 130 additions & 0 deletions lua/time-tracker/json_session.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
local utils = require("time-tracker/utils")

---@class JsonSession
local JsonSession = {
--- @type Config
config = nil,
current_buffer = nil,
current_session = nil,

new = function(self, config)
local tracker = {
config = config,
}
setmetatable(tracker, self)
self.__index = self
return tracker
end,

init = function(self) end,

load_data = function(self)
local ok, data = pcall(vim.fn.readfile, self.config.data_file)
if not ok then return { roots = {} } end
return vim.fn.json_decode(data)
end,

start_session = function(self, bufnr)
if not utils.is_trackable_buffer(bufnr) then return end

self.current_session = {
buffers = {},
}
end,

handle_activity = function(self)
local bufnr = vim.api.nvim_get_current_buf()
if not utils.is_trackable_buffer(bufnr) then return end

-- session doesn't exist, create it
if not self.current_session then self:start_session(bufnr) end

-- store previous buffer activity on buffer change
if self.current_buffer and self.current_buffer.bufnr ~= bufnr then
local buf_cwd = self.current_buffer.cwd
local buf_path = self.current_buffer.path

---@type BufferSession
local entry = {
start = self.current_buffer.start,
["end"] = vim.fn.localtime(),
}

if not self.current_session.buffers[buf_cwd] then self.current_session.buffers[buf_cwd] = {} end
if not self.current_session.buffers[buf_cwd][buf_path] then
self.current_session.buffers[buf_cwd][buf_path] = {}
end
table.insert(self.current_session.buffers[buf_cwd][buf_path], entry)
end

-- set new current buffer on buffer change or when there is no current buffer
if self.current_buffer == nil or self.current_buffer.bufnr ~= bufnr then
local buf_path = vim.api.nvim_buf_get_name(bufnr)
local buf_cwd = vim.fn.getcwd()

self.current_buffer = {
bufnr = bufnr,
cwd = buf_cwd,
path = buf_path,
start = vim.fn.localtime(),
}
end

-- create/reset timer
if self.timer ~= nil then
self.timer:stop()
self.timer:close()
end
self.timer = vim.loop.new_timer()
self.timer:start(self.config.tracking_timeout_seconds * 1000, 0, function()
vim.schedule(function()
self:end_session()
self.timer:stop()
self.timer:close()
self.timer = nil
end)
end)
end,

end_session = function(self)
if not self.current_session then return end

-- record current buffer
if self.current_buffer then
local buf_cwd = self.current_buffer.cwd
local buf_path = self.current_buffer.path

---@type BufferSession
local entry = {
start = self.current_buffer.start,
["end"] = vim.fn.localtime(),
}

if not self.current_session.buffers[buf_cwd] then self.current_session.buffers[buf_cwd] = {} end
if not self.current_session.buffers[buf_cwd][buf_path] then
self.current_session.buffers[buf_cwd][buf_path] = {}
end
table.insert(self.current_session.buffers[buf_cwd][buf_path], entry)

self.current_buffer = nil
end

-- save session
local data = self:load_data()
for cwd, buffers in pairs(self.current_session.buffers) do
if not data.roots[cwd] then data.roots[cwd] = {} end
for path, sessions in pairs(buffers) do
if not data.roots[cwd][path] then data.roots[cwd][path] = {} end
for _, session in ipairs(sessions) do
table.insert(data.roots[cwd][path], session)
end
end
end
self:save_data(data)
self.current_session = nil
end,
}

return {
JsonSession = JsonSession,
}
26 changes: 26 additions & 0 deletions lua/time-tracker/json_session_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
local JsonSession = require("time-tracker/json_session").JsonSession

describe("JsonSession", function()
local config = {
data_file = "/tmp/time-tracker.json",
tracking_events = { "BufEnter" },
tracking_timeout_seconds = 1,
}

before_each(function()
os.remove(config.data_file)
end)

it("creates a new instance", function()
local tracker = JsonSession:new(config)
expect(tracker.config).toBe(config)
end)

it("starts a session", function()
local tracker = JsonSession:new(config)
local buf = vim.api.nvim_create_buf(true, false)
vim.api.nvim_buf_set_name(buf, "/dev/null")
tracker:start_session(buf)
expect(tracker.current_session).n.toBe(nil)
end)
end)
Loading