├── LICENSE ├── README.md ├── assets ├── demo.gif ├── logo.png └── menu.png └── lua └── nomodoro ├── init.lua └── menu.lua /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Diego Binagi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![logo](assets/logo.png) 2 | 3 | Finally, Nvim + Pomodoro! 4 | 5 | Yes, its another Pomodoro plugin for Nvim. Originally made to my own use and to learn the basics of plugin creation with Neovim. 6 | 7 | I decided to leave this available for everyone that can find it useful also and please feel free to use, copy, comment or change anything you want. 8 | 9 | ![demo](assets/demo.gif) 10 | 11 | # Features 12 | 13 | * Setup your pomodoro sessions time 14 | * Start and Stop sessions 15 | * Show status realtime from other plugins 16 | * Not just Pomodoro focused! Custom timer for any use 17 | 18 | # Quickstart 19 | 20 | ## Instalation 21 | 22 | Using [vim-plug](https://github.com/junegunn/vim-plug) 23 | ```vim 24 | Plug 'dbinagi/nomodoro' 25 | ``` 26 | ## Setup 27 | 28 | To load plugin with default configuration: 29 | ```lua 30 | require('nomodoro').setup({}) 31 | ``` 32 | 33 | # Setup 34 | 35 | ## Default Configuration 36 | 37 | ```lua 38 | require('nomodoro').setup({ 39 | work_time = 25, 40 | short_break_time = 5, 41 | long_break_time = 15, 42 | break_cycle = 4, 43 | menu_available = true, 44 | texts = { 45 | on_break_complete = "TIME IS UP!", 46 | on_work_complete = "TIME IS UP!", 47 | status_icon = "🍅 ", 48 | timer_format = '!%0M:%0S' -- To include hours: '!%0H:%0M:%0S' 49 | }, 50 | on_work_complete = function() end, 51 | on_break_complete = function() end 52 | }) 53 | 54 | ``` 55 | 56 | # Commands 57 | 58 | | Command | Description | 59 | | ----------- | ----------- | 60 | | NomoWork | Start work timer | 61 | | NomoBreak | Start break timer | 62 | | NomoStop | Stop all timers | 63 | | NomoStatus | Print time left manually | 64 | | NomoTimer N | Runs a timer for N minutes | 65 | | NomoPause | Pauses current timer | 66 | | NomoContinue | Resumes current timer | 67 | 68 | # Configure keys 69 | 70 | By default, no shortcuts are provided, you could configure the following. 71 | 72 | ```lua 73 | local map = vim.api.nvim_set_keymap 74 | local opts = { noremap = true, silent = true } 75 | 76 | map('n', 'nw', 'NomoWork', opts) 77 | map('n', 'nb', 'NomoBreak', opts) 78 | map('n', 'ns', 'NomoStop', opts) 79 | ``` 80 | 81 | # Optional UI 82 | 83 | ## Integration with lualine 84 | 85 | As an example, to integrate the status realtime with lualine use the following: 86 | 87 | ```lua 88 | 89 | local lualine = require'lualine' 90 | lualine.setup({ 91 | sections = { 92 | lualine_x = { 93 | require('nomodoro').status, 94 | } 95 | } 96 | }) 97 | ``` 98 | 99 | ## Integration with [nui.menu](https://github.com/MunifTanjim/nui.nvim) 100 | 101 | If you like menus, you can install the dependency nui.menu and you will have enable a command `NomoMenu` to display options in a popup 102 | 103 | ![menu](assets/menu.png) 104 | 105 | # Contributions 106 | 107 | Contributions are more than welcome! Thanks to: 108 | 109 | @gaardhus 110 | @nfwyst 111 | 112 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbinagi/nomodoro/35076f96ea21fecc6202162304891338c8eebe3c/assets/demo.gif -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbinagi/nomodoro/35076f96ea21fecc6202162304891338c8eebe3c/assets/logo.png -------------------------------------------------------------------------------- /assets/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbinagi/nomodoro/35076f96ea21fecc6202162304891338c8eebe3c/assets/menu.png -------------------------------------------------------------------------------- /lua/nomodoro/init.lua: -------------------------------------------------------------------------------- 1 | -- Check if already loaded 2 | if vim.g.loaded_nomodoro then 3 | return 4 | end 5 | vim.g.loaded_nomodoro = true 6 | 7 | local menu = require('nomodoro.menu') 8 | 9 | local command = vim.api.nvim_create_user_command 10 | 11 | local start_time = 0 12 | local total_minutes = 0 13 | 14 | local DONE = 0 15 | local RUNNING = 1 16 | local PAUSE = 2 17 | local state = DONE 18 | 19 | local already_notified_end = false 20 | 21 | vim.g.break_count = 0 22 | 23 | --- The default options 24 | local DEFAULT_OPTIONS = { 25 | work_time = 25, 26 | short_break_time = 5, 27 | long_break_time = 15, 28 | break_cycle = 4, 29 | menu_available = true, 30 | texts = { 31 | on_break_complete = "TIME IS UP!", 32 | on_work_complete = "TIME IS UP!", 33 | status_icon = "🍅 ", 34 | timer_format = '!%0M:%0S' -- To include hours: '!%0H:%0M:%0S' 35 | }, 36 | on_work_complete = function() end, 37 | on_break_complete = function() end 38 | } 39 | 40 | -- Local functions 41 | 42 | local previous_time_remaining = nil 43 | local new_start = nil 44 | local function time_remaining_seconds(duration, start) 45 | if state == PAUSE then 46 | if not previous_time_remaining then 47 | previous_time_remaining = duration * 60 - os.difftime(os.time(), new_start or start) 48 | new_start = nil 49 | end 50 | return previous_time_remaining 51 | end 52 | if previous_time_remaining and not new_start then 53 | new_start = os.difftime(os.time(), duration * 60 - previous_time_remaining) 54 | previous_time_remaining = nil 55 | end 56 | return duration * 60 - os.difftime(os.time(), new_start or start) 57 | end 58 | 59 | local function time_remaining(duration, start) 60 | return os.date(vim.g.nomodoro.texts.timer_format, time_remaining_seconds(duration, start)) 61 | end 62 | 63 | local function is_work_time(duration) 64 | return duration == vim.g.nomodoro.work_time 65 | end 66 | 67 | -- Plugin functions 68 | 69 | local nomodoro = { 70 | } 71 | 72 | function nomodoro.start(minutes) 73 | start_time = os.time() 74 | total_minutes = minutes 75 | already_notified_end = false 76 | state = RUNNING 77 | end 78 | 79 | function nomodoro.pause() 80 | state = PAUSE 81 | end 82 | 83 | function nomodoro.continue() 84 | state = RUNNING 85 | end 86 | 87 | function nomodoro.is_pause() 88 | return state == PAUSE 89 | end 90 | 91 | function nomodoro.is_running() 92 | return state == RUNNING 93 | end 94 | 95 | function nomodoro.start_break() 96 | if nomodoro.is_short_break() then 97 | nomodoro.start(vim.g.nomodoro.short_break_time) 98 | else 99 | nomodoro.start(vim.g.nomodoro.long_break_time) 100 | end 101 | end 102 | 103 | function nomodoro.is_short_break() 104 | return vim.g.break_count % vim.g.nomodoro.break_cycle ~= 0 or vim.g.break_count == 0 105 | end 106 | 107 | function nomodoro.setup(options) 108 | local new_config = vim.tbl_deep_extend('force', DEFAULT_OPTIONS, options) 109 | vim.g.nomodoro = new_config 110 | menu.has_dependencies = new_config.menu_available 111 | end 112 | 113 | local previous_status = nil 114 | function nomodoro.status() 115 | local status_string = "" 116 | 117 | if previous_status then 118 | if nomodoro.is_pause() then 119 | return previous_status 120 | else 121 | previous_status = nil 122 | end 123 | end 124 | 125 | if nomodoro.is_running() or nomodoro.is_pause() then 126 | if time_remaining_seconds(total_minutes, start_time) <= 0 then 127 | state = DONE 128 | if is_work_time(total_minutes) then 129 | status_string = vim.g.nomodoro.texts.on_work_complete 130 | if not already_notified_end then 131 | vim.g.nomodoro.on_work_complete() 132 | already_notified_end = true 133 | nomodoro.show_menu(2 + (nomodoro.is_short_break() and 0 or 1)) 134 | end 135 | else 136 | status_string = vim.g.nomodoro.texts.on_break_complete 137 | if not already_notified_end then 138 | vim.g.nomodoro.on_break_complete() 139 | already_notified_end = true 140 | vim.g.break_count = vim.g.break_count + 1 141 | nomodoro.show_menu() 142 | end 143 | 144 | end 145 | else 146 | status_string = vim.g.nomodoro.texts.status_icon .. time_remaining(total_minutes, start_time) 147 | end 148 | end 149 | 150 | if nomodoro.is_pause() then 151 | previous_status = status_string 152 | end 153 | 154 | return status_string 155 | end 156 | 157 | function nomodoro.stop() 158 | state = DONE 159 | end 160 | 161 | function nomodoro.show_menu(focus_line) 162 | menu.show(nomodoro, focus_line) 163 | end 164 | 165 | -- Expose commands 166 | 167 | command("NomoWork", function () 168 | nomodoro.start(vim.g.nomodoro.work_time) 169 | end, {}) 170 | 171 | command("NomoPause", function () 172 | if nomodoro.is_running() then 173 | nomodoro.pause() 174 | end 175 | end, {}) 176 | 177 | command("NomoContinue", function () 178 | if nomodoro.is_pause() then 179 | nomodoro.continue() 180 | end 181 | end, {}) 182 | 183 | command("NomoBreak", function () 184 | nomodoro.start_break() 185 | end, {}) 186 | 187 | command("NomoStop", function () 188 | nomodoro.stop() 189 | end, {}) 190 | 191 | command("NomoStatus", function () 192 | print(nomodoro.status()) 193 | end, {}) 194 | 195 | command("NomoTimer", function (opts) 196 | nomodoro.start(opts.args) 197 | end, {nargs = 1}) 198 | 199 | if menu.has_dependencies then 200 | command("NomoMenu", function() 201 | nomodoro.show_menu() 202 | end, {}) 203 | end 204 | 205 | return nomodoro 206 | -------------------------------------------------------------------------------- /lua/nomodoro/menu.lua: -------------------------------------------------------------------------------- 1 | 2 | local Menu 3 | local event 4 | local NuiText 5 | 6 | local function on_close() 7 | end 8 | 9 | 10 | local function check_dependencies() 11 | local ok, t = pcall(require, "nui.menu") 12 | if ok then 13 | Menu = t 14 | NuiText = require("nui.text") 15 | else 16 | return false 17 | end 18 | 19 | ok, t = pcall(require, "nui.utils.autocmd") 20 | if ok then 21 | event = t.event 22 | else 23 | return false 24 | end 25 | 26 | return true 27 | end 28 | 29 | local has_dependencies = check_dependencies() 30 | 31 | local M = {} 32 | 33 | local function show(nomodoro, focus_line) 34 | if not has_dependencies then return end 35 | if not focus_line then focus_line = 1 end 36 | 37 | local popup_options = { 38 | border = { 39 | style = 'rounded', 40 | padding = { 1, 3 }, 41 | }, 42 | position = '50%', 43 | size = { 44 | width = '25%', 45 | }, 46 | opacity = 1, 47 | enter=true, 48 | } 49 | 50 | local menu_options = { 51 | keymap = { 52 | focus_next = { 'j', '', '' }, 53 | focus_prev = { 'k', '', '' }, 54 | close = { '', '' }, 55 | submit = { '', '' }, 56 | }, 57 | lines = { 58 | Menu.item('Work'), 59 | Menu.item('Short Break'), 60 | Menu.item('Long Break'), 61 | Menu.item('Stop'), 62 | Menu.separator(tostring(vim.g.break_count) .. (vim.g.break_count == 1 and ' break taken' or ' breaks taken'), { text_align = "center", char = "" }), 63 | }, 64 | on_close = on_close, 65 | on_submit = function(item) 66 | if item.text == 'Work' then 67 | nomodoro.start(vim.g.nomodoro.work_time) 68 | elseif item.text == 'Short Break' then 69 | nomodoro.start(vim.g.nomodoro.short_break_time) 70 | elseif item.text == 'Long Break' then 71 | nomodoro.start(vim.g.nomodoro.long_break_time) 72 | elseif item.text == 'Stop' then 73 | nomodoro.stop() 74 | elseif item.text == "Continue" then 75 | nomodoro.continue() 76 | elseif item.text == "Pause" then 77 | nomodoro.pause() 78 | end 79 | end 80 | } 81 | 82 | if nomodoro.is_pause() then 83 | table.insert(menu_options.lines, 1, Menu.item('Continue')) 84 | elseif nomodoro.is_running() then 85 | table.insert(menu_options.lines, 1, Menu.item('Pause')) 86 | end 87 | 88 | local menu = Menu(popup_options, menu_options) 89 | 90 | menu:mount() 91 | 92 | menu:on(event.BufLeave, function() 93 | menu:unmount() 94 | end, { once = true }) 95 | menu:map('n', 'q', function() 96 | menu:unmount() 97 | end, { noremap = true }) 98 | 99 | vim.api.nvim_win_set_cursor(menu.winid, { focus_line, 0 }) 100 | 101 | end 102 | 103 | M.show = show 104 | M.has_dependencies = has_dependencies 105 | 106 | return M 107 | --------------------------------------------------------------------------------