Skip to content

Commit

Permalink
add repeat timers
Browse files Browse the repository at this point in the history
  • Loading branch information
epwalsh committed Nov 30, 2023
1 parent 16e573f commit f67b59d
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 64 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## Unreleased

### Added

- Added support for repeat timers via `:TimerRepeat`.

## [v0.2.0](https://github.com/epwalsh/pomo.nvim/releases/tag/v0.2.0) - 2023-11-30

### Added
Expand Down
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ A simple, customizable [pomodoro](https://en.wikipedia.org/wiki/Pomodoro_Techniq
- 🪶 Lightweight and asynchronous
- 💻 Written in Lua
- ⚙️ Easily customizable and extendable
- ⏱️ Run multiple concurrent timers
- ⏱️ Run multiple concurrent timers and repeat timers
- ➕ Integrate with [lualine](#lualinenvim)

### Commands
Expand All @@ -29,6 +29,8 @@ A simple, customizable [pomodoro](https://en.wikipedia.org/wiki/Pomodoro_Techniq

- `:TimerStop [TIMERID]` to stop a running timer, e.g. `:TimerStop 1`. If no ID is given, the latest timer is stopped.

- `:TimerRepeat TIMELIMIT REPETITIONS [NAME]` to start a repeat timer, e.g. `:TimerRepeat 10s 2` to repeat a 10 second timer twice.

## Setup

To setup pomo.nvim you just need to call `require("pomo").setup({ ... })` with the desired options. Here are some examples using different plugin managers. The full set of [configuration options](#configuration-options) are listed below.
Expand All @@ -40,7 +42,7 @@ return {
"epwalsh/pomo.nvim",
version = "*", -- Recommended, use latest release instead of latest commit
lazy = true,
cmd = { "TimerStart", "TimerStop" },
cmd = { "TimerStart", "TimerStop", "TimerRepeat" },
dependencies = {
-- Optional, but highly recommended if you want to use the "Default" timer
"rcarriga/nvim-notify",
Expand Down Expand Up @@ -96,7 +98,7 @@ This is a complete list of all of the options that can be passed to `require("po

-- You can also define custom notifiers by providing an "init" function instead of a name.
-- See "Defining custom notifiers" below for an example 👇
-- { init = function(timer_id, time_limit, name) ... end }
-- { init = function(timer) ... end }
},
}
```
Expand All @@ -110,32 +112,30 @@ To define your own notifier you need to create a Lua `Notifier` class along with
- `Notifier.done(self)` - Called when the timer finishes.
- `Notifier.stop(self)` - Called when the timer is stopped before finishing.

The factory `init` function takes 3 or 4 arguments, the `timer_id` (an integer), the `time_limit` seconds (an integer), the `name` assigned to the timer (a string or `nil`), and optionally a table of options from the `opts` field in the notifier's config.
The factory `init` function takes 1 or 2 arguments, the `timer` (a `pomo.Timer`) and optionally a table of options from the `opts` field in the notifier's config.

For example, here's a simple notifier that just uses `print`:

```lua
local PrintNotifier = {}

PrintNotifier.new = function(timer_id, time_limit, name, opts)
PrintNotifier.new = function(timer, opts)
local self = setmetatable({}, { __index = PrintNotifier })
self.timer_id = timer_id
self.time_limit = time_limit
self.name = name and name or "Countdown"
self.timer = timer
self.opts = opts -- not used
return self
end

PrintNotifier.start = function(self)
print(string.format("Starting timer #%d, %s, for %ds", self.timer_id, self.name, self.time_limit))
print(string.format("Starting timer #%d, %s, for %ds", self.timer.id, self.timer.name, self.timer.time_limit))
end

PrintNotifier.tick = function(self, time_left)
print(string.format("Timer #%d, %s, %ds remaining...", self.timer_id, self.name, time_left))
print(string.format("Timer #%d, %s, %ds remaining...", self.timer.id, self.timer.name, time_left))
end

PrintNotifier.done = function(self)
print(string.format("Timer #%d, %s, complete", self.timer_id, self.name))
print(string.format("Timer #%d, %s, complete", self.timer.id, self.timer.name))
end

PrintNotifier.stop = function(self) end
Expand Down
33 changes: 25 additions & 8 deletions lua/pomo/commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,16 @@ local M = {}
M.register_commands = function()
vim.api.nvim_create_user_command("TimerStart", function(data)
if data.fargs == nil or #data.fargs == 0 or #data.fargs > 2 then
return log.error "Invalid number arguments, expected 2.\nUsage: TimerStart TIMELIMIT [NAME]"
return log.error "invalid number arguments, expected 1 or 2.\nUsage: TimerStart TIMELIMIT [NAME]"
end

local time_arg = string.lower(data.fargs[1])
local name = data.fargs[2]

---@type number|?
local time_limit = util.parse_time(time_arg)

local time_limit = util.parse_time(data.fargs[1])
if time_limit == nil then
return log.error("invalid time limit '%s'", time_arg)
return log.error("invalid time limit '%s'", data.fargs[1])
end

local name = data.fargs[2]

pomo.start_timer(time_limit, name)
end, { nargs = "+" })

Expand All @@ -42,6 +39,26 @@ M.register_commands = function()
return log.error "failed to stop timer"
end
end, { nargs = "?" })

vim.api.nvim_create_user_command("TimerRepeat", function(data)
if data.fargs == nil or #data.fargs < 2 or #data.fargs > 3 then
return log.error "invalid number arguments, expected 2 or 3.\nUsage: TimerRepeat TIMELIMIT REPETITIONS [NAME]"
end

local time_limit = util.parse_time(data.fargs[1])
if time_limit == nil then
return log.error("invalid time limit '%s'", data.fargs[1])
end

local repititions = tonumber(data.fargs[2])
if repititions == nil then
return log.error("invalid number of repetitions, expected number, got '%s'", data.fargs[2])
end

local name = data.fargs[3]

pomo.start_timer(time_limit, name, repititions)
end, { nargs = "+" })
end

return M
7 changes: 4 additions & 3 deletions lua/pomo/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ M.setup = function(opts)
end

---Start a new timer.
---@param time_limit integer seconds
---@param time_limit integer The time limit, in seconds.
---@param name string|?
---@param repeat_n integer|? The number of the times to repeat the timer.
---@return integer time_id
M.start_timer = function(time_limit, name)
M.start_timer = function(time_limit, name, repeat_n)
local timer_id = timers:first_available_id()
local timer = Timer.new(timer_id, time_limit, name, M.get_config())
local timer = Timer.new(timer_id, time_limit, name, M.get_config(), repeat_n)

timers:store(timer)

Expand Down
70 changes: 37 additions & 33 deletions lua/pomo/notifier.lua
Original file line number Diff line number Diff line change
Expand Up @@ -40,26 +40,20 @@ end

---The default implementation of `pomo.Notifier`, uses `vim.notify` to display the timer.
---@class pomo.DefaultNotifier : pomo.Notifier
---@field timer_id integer
---@field time_limit integer
---@field name string|?
---@field timer pomo.Timer
---@field notification any
---@field opts table
---@field title_icon string
---@field text_icon string
local DefaultNotifier = {}
M.DefaultNotifier = DefaultNotifier

---@param timer_id integer
---@param time_limit integer
---@param name string|?
---@param timer pomo.Timer
---@param opts table|?
---@return pomo.DefaultNotifier
DefaultNotifier.new = function(timer_id, time_limit, name, opts)
DefaultNotifier.new = function(timer, opts)
local self = setmetatable({}, { __index = DefaultNotifier })
self.timer_id = timer_id
self.time_limit = time_limit
self.name = name
self.timer = timer
self.notification = nil
self.opts = opts and opts or {}
self.title_icon = self.opts.title_icon and self.opts.title_icon or "󱎫"
Expand All @@ -71,13 +65,25 @@ end
---@param level string|integer
---@param timeout boolean|integer
DefaultNotifier._update = function(self, text, level, timeout)
local repetitions_str = ""
if self.timer.max_repetitions ~= nil and self.timer.max_repetitions > 0 then
repetitions_str = string.format(" [%d/%d]", self.timer.repetitions + 1, self.timer.max_repetitions)
end

---@type string
local title
if self.name ~= nil then
title = string.format("Timer #%d, %s, %s", self.timer_id, self.name, util.format_time(self.time_limit))
if self.timer.name ~= nil then
title = string.format(
"Timer #%d, %s, %s%s",
self.timer.id,
self.timer.name,
util.format_time(self.timer.time_limit),
repetitions_str
)
else
title = string.format("Timer #%d, %s", self.timer_id, util.format_time(self.time_limit))
title = string.format("Timer #%d, %s%s", self.timer.id, util.format_time(self.timer.time_limit), repetitions_str)
end

self.notification = vim.notify(text, level, {
icon = self.title_icon,
title = title,
Expand Down Expand Up @@ -106,30 +112,24 @@ end

---A `pomo.Notifier` that sends a system notification when the timer is finished.
---@class pomo.SystemNotifier : pomo.Notifier
---@field timer_id integer
---@field time_limit integer
---@field name string|?
---@field timer pomo.Timer
---@field notification any
---@field opts table
local SystemNotifier = {}
M.SystemNotifier = SystemNotifier

SystemNotifier.supported_oss = { util.OS.Darwin }

---@param timer_id integer
---@param time_limit integer
---@param name string|?
---@param timer pomo.Timer
---@param opts table|?
---@return pomo.SystemNotifier
SystemNotifier.new = function(timer_id, time_limit, name, opts)
SystemNotifier.new = function(timer, opts)
if not vim.tbl_contains(SystemNotifier.supported_oss, util.get_os()) then
error(string.format("SystemNotifier is not implemented for your OS (%s)", util.get_os()))
end

local self = setmetatable({}, { __index = SystemNotifier })
self.timer_id = timer_id
self.time_limit = time_limit
self.name = name
self.timer = timer
self.notification = nil
self.opts = opts and opts or {}
return self
Expand All @@ -143,12 +143,18 @@ SystemNotifier.start = function(self) ---@diagnostic disable-line: unused-local
end

SystemNotifier.done = function(self) ---@diagnostic disable-line: unused-local
local repetitions_str = ""
if self.timer.max_repetitions ~= nil and self.timer.max_repetitions > 0 then
repetitions_str = string.format(" [%d/%d]", self.timer.repetitions + 1, self.timer.max_repetitions)
end

if util.get_os() == util.OS.Darwin then
os.execute(
string.format(
[[osascript -e 'display notification "Timer done!" with title "Timer #%d, %s" sound name "Ping"']],
self.timer_id,
util.format_time(self.time_limit)
[[osascript -e 'display notification "Timer done!" with title "Timer #%d, %s%s" sound name "Ping"']],
self.timer.id,
util.format_time(self.timer.time_limit),
repetitions_str
)
)
else
Expand All @@ -160,25 +166,23 @@ SystemNotifier.stop = function(self) ---@diagnostic disable-line: unused-local
end

---Construct a `pomo.Notifier` given a notifier name (`pomo.NotifierType`) or factory function.
---@param timer pomo.Timer
---@param opts pomo.NotifierConfig
---@param timer_id integer
---@param time_limit integer
---@param name string|?
---@return pomo.Notifier
M.build = function(opts, timer_id, time_limit, name)
M.build = function(timer, opts)
if (opts.name == nil) == (opts.init == nil) then
error "invalid notifier config, 'name' and 'init' are mutually exclusive"
end

if opts.init ~= nil then
assert(opts.init)
return opts.init(timer_id, time_limit, name, opts)
return opts.init(timer, opts)
else
assert(opts.name)
if opts.name == NotifierType.Default then
return DefaultNotifier.new(timer_id, time_limit, name, opts.opts)
return DefaultNotifier.new(timer, opts.opts)
elseif opts.name == NotifierType.System then
return SystemNotifier.new(timer_id, time_limit, name, opts.opts)
return SystemNotifier.new(timer, opts.opts)
else
error(string.format("invalid notifier name '%s'", opts.name))
end
Expand Down
38 changes: 29 additions & 9 deletions lua/pomo/timer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@ local util = require "pomo.util"
---@field start_time integer|?
---@field notifiers pomo.Notifier[]
---@field config pomo.Config
---@field max_repetitions integer|?
---@field repetitions integer
local Timer = {}

---Initialize a `pomo.Timer`.
---@param id integer
---@param time_limit integer
---@param name string|?
---@param config pomo.Config
---@param repeat_n integer|? The number of times to repeat the timer
---@return pomo.Timer
Timer.new = function(id, time_limit, name, config)
Timer.new = function(id, time_limit, name, config, repeat_n)
local self = setmetatable({}, {
__index = Timer,
---@param self pomo.Timer
Expand All @@ -32,23 +35,33 @@ Timer.new = function(id, time_limit, name, config)
time_str = util.format_time(self.time_limit)
end

local repetitions_str = ""
if self.max_repetitions ~= nil and self.max_repetitions > 0 then
repetitions_str = string.format(" [%d/%d]", self.repetitions + 1, self.max_repetitions)
end

if self.name ~= nil then
return string.format("#%d, %s: %s", self.id, self.name, time_str)
return string.format("#%d, %s: %s%s", self.id, self.name, time_str, repetitions_str)
else
return string.format("#%d: %s", self.id, time_str)
return string.format("#%d: %s%s", self.id, time_str, repetitions_str)
end
end,
})

self.id = id
self.time_limit = time_limit
self.name = name
self.config = config
self.max_repetitions = repeat_n
self.repetitions = 0
self.timer = vim.loop.new_timer() ---@diagnostic disable-line: undefined-field

self.notifiers = {}
for _, noti_opts in ipairs(self.config.notifiers) do
local noti = notifier.build(noti_opts, self.id, self.time_limit, self.name)
local noti = notifier.build(self, noti_opts)
self.notifiers[#self.notifiers + 1] = noti
end

return self
end

Expand All @@ -68,7 +81,7 @@ end
---@return pomo.Timer
Timer.start = function(self, timer_done)
self.start_time = vim.loop.hrtime() ---@diagnostic disable-line: undefined-field

self.repetitions = 0
for _, noti in ipairs(self.notifiers) do
noti:start()
end
Expand All @@ -84,14 +97,21 @@ Timer.start = function(self, timer_done)
noti:tick(time_left)
end
else
self.timer:close()

for _, noti in ipairs(self.notifiers) do
noti:done()
end

if timer_done ~= nil then
timer_done(self)
if self.max_repetitions ~= nil and self.max_repetitions > 0 and self.repetitions + 1 < self.max_repetitions then
self.repetitions = self.repetitions + 1
self.start_time = vim.loop.hrtime() ---@diagnostic disable-line: undefined-field
for _, noti in ipairs(self.notifiers) do
noti:start()
end
else
self.timer:close()
if timer_done ~= nil then
timer_done(self)
end
end
end
end)
Expand Down

0 comments on commit f67b59d

Please sign in to comment.