diff --git a/DOCS.md b/DOCS.md index 5c4bb3be1..7de868637 100644 --- a/DOCS.md +++ b/DOCS.md @@ -4,7 +4,8 @@ 2. [Settings](#settings) 1. [Global settings](#global-settings) 2. [Agenda settings](#agenda-settings) - 3. [Tags settings](#tags-settings) + 3. [Calendar settings](#calendar-settings) + 4. [Tags settings](#tags-settings) 3. [Mappings](#mappings) 1. [Global mappings](#global-mappings) 2. [Agenda mappings](#agenda-mappings) @@ -677,6 +678,28 @@ Additional files to search from agenda search prompt.
Currently it accepts only a single value: `agenda-archives`.
Example value: `{'agenda-archives'}` +### Calendar settings + +Adjust behavior of the calendar modal (ex: [changing the date under cursor](#org_change_date)). + +#### **calendar.round_min_with_hours** + +_type_: `boolean`
+_default_: `true`
+Should minutes be rounded, when the hour is changed. It behaves more fluently when changing the hours, especially when scheduling from the current time (which can be something odd). If set to false, the minutes are unchanged while changing the hours. + +#### **calendar.min_big_step** + +_type_: `number`
+_default_: `15`
+The step size for changing the minutes while the cursor is on the first digit. + +#### **calendar.min_small_step** + +_type_: `number`
+_default_: same as [](#org_time_stamp_rounding_minutes)
+The step size for changing the minutes while the cursor is on the second digit. + ### Tags settings #### **org_tags_column** @@ -1712,7 +1735,7 @@ This option is most optimized because it doesn't load plugins and your init.vim For **MacOS**, things should be very similar, but I wasn't able to test it. Any help on this is appreciated. ## Clocking -There is partial suport for [Clocking work time](https://orgmode.org/manual/Clocking-Work-Time.html).
+There is partial support for [Clocking work time](https://orgmode.org/manual/Clocking-Work-Time.html).
Supported actions: ##### Clock in Org file mapping: `oxi`
diff --git a/lua/orgmode/config/defaults.lua b/lua/orgmode/config/defaults.lua index 6577a86ba..583d12284 100644 --- a/lua/orgmode/config/defaults.lua +++ b/lua/orgmode/config/defaults.lua @@ -1,6 +1,7 @@ ---@class OrgDefaultConfig ---@field org_id_method 'uuid' | 'ts' | 'org' ---@field org_agenda_span 'day' | 'week' | 'month' | 'year' | number +---@field calendar { round_min_with_hours: boolean, min_big_step: number, min_small_step: number? } local DefaultConfig = { org_agenda_files = '', org_default_notes_file = '', @@ -13,6 +14,10 @@ local DefaultConfig = { org_agenda_start_on_weekday = 1, org_agenda_start_day = nil, -- start from today + this modifier calendar_week_start_day = 1, + calendar = { + round_min_with_hours = true, + min_big_step = 15, + }, org_capture_templates = { t = { description = 'Task', diff --git a/lua/orgmode/objects/calendar.lua b/lua/orgmode/objects/calendar.lua index 963202f82..25cb91774 100644 --- a/lua/orgmode/objects/calendar.lua +++ b/lua/orgmode/objects/calendar.lua @@ -7,25 +7,35 @@ local namespace = vim.api.nvim_create_namespace('org_calendar') ---@alias OrgCalendarOnRenderDayOpts { line: number, from: number, to: number, buf: number, namespace: number } ---@alias OrgCalendarOnRenderDay fun(day: OrgDate, opts: OrgCalendarOnRenderDayOpts) +local SelState = { DAY = 0, HOUR = 1, MIN_BIG = 2, MIN_SMALL = 3 } +local big_minute_step = config.calendar.min_big_step +local small_minute_step = config.calendar.min_small_step or config.org_time_stamp_rounding_minutes + ---@class OrgCalendar ----@field win number ----@field buf number +---@field win number? +---@field buf number? ---@field callback fun(date: OrgDate | nil, cleared?: boolean) ---@field namespace function ----@field date OrgDate +---@field date OrgDate? ---@field month OrgDate ---@field title? string ---@field on_day? OrgCalendarOnRenderDay - +---@field selected OrgDate? +---@field select_state integer +---@field clearable boolean local Calendar = { win = nil, buf = nil, date = nil, + month = Date.today():start_of('month'), + selected = nil, + select_state = SelState.DAY, clearable = false, } Calendar.__index = Calendar vim.cmd([[hi default OrgCalendarToday gui=reverse cterm=reverse]]) +vim.cmd([[hi default OrgCalendarSelected gui=underline cterm=underline]]) ---@param data { date?: OrgDate, clearable?: boolean, title?: string, on_day?: OrgCalendarOnRenderDay } function Calendar.new(data) @@ -44,7 +54,7 @@ function Calendar.new(data) end local width = 36 -local height = 10 +local height = 14 local x_offset = 1 -- one border cell local y_offset = 2 -- one border cell and one padding cell @@ -53,7 +63,7 @@ function Calendar:open() local opts = { relative = 'editor', width = width, - height = self.clearable and height + 1 or height, + height = height, style = 'minimal', border = config.win_border, row = vim.o.lines / 2 - (y_offset + height) / 2, @@ -121,12 +131,16 @@ function Calendar:open() return self:clear_date() end, map_opts) end - local search_day = Date.today():format('%d') - if self.date then - search_day = self.date:format('%d') + vim.keymap.set('n', 't', function() + self:set_time() + end, map_opts) + if self:has_time() then + print('apply clear_time') + vim.keymap.set('n', 'T', function() + self:clear_time() + end, map_opts) end - vim.fn.cursor(2, 1) - vim.fn.search(search_day, 'W') + self:jump_day() return Promise.new(function(resolve) self.callback = resolve end) @@ -175,6 +189,10 @@ function Calendar:render() -- put it all together table.insert(content, 1, ' ' .. table.concat(weekday_row, ' ')) table.insert(content, 1, title) + + table.insert(content, self:render_time()) + table.insert(content, '') + -- TODO: redundant, since it's static data table.insert(content, ' [<] - prev month [>] - next month') table.insert(content, ' [.] - today [Enter] - select day') @@ -184,11 +202,28 @@ function Calendar:render() table.insert(content, ' [i] - enter date') end + if self:has_time() or self.select_state ~= SelState.DAY then + if self.select_state == SelState.DAY then + table.insert(content, ' [t] - enter time [T] - clear time') + else + table.insert(content, ' [d] - enter time [T] - clear time') + end + else + table.insert(content, ' [t] - enter time') + end + vim.api.nvim_buf_set_lines(self.buf, 0, -1, true, content) vim.api.nvim_buf_clear_namespace(self.buf, namespace, 0, -1) if self.clearable then vim.api.nvim_buf_add_highlight(self.buf, namespace, 'Comment', #content - 3, 0, -1) end + + if not self:has_time() then + vim.api.nvim_buf_add_highlight(self.buf, namespace, 'Comment', 8, 0, -1) + end + + vim.api.nvim_buf_add_highlight(self.buf, namespace, 'Comment', #content - 4, 0, -1) + vim.api.nvim_buf_add_highlight(self.buf, namespace, 'Comment', #content - 3, 0, -1) vim.api.nvim_buf_add_highlight(self.buf, namespace, 'Comment', #content - 2, 0, -1) vim.api.nvim_buf_add_highlight(self.buf, namespace, 'Comment', #content - 1, 0, -1) @@ -218,10 +253,12 @@ end ---@param day OrgDate ---@param opts { from: number, to: number, line: number} function Calendar:on_render_day(day, opts) - local is_today = day:is_today() - if is_today then + if day:is_today() then vim.api.nvim_buf_add_highlight(self.buf, namespace, 'OrgCalendarToday', opts.line - 1, opts.from - 1, opts.to) end + if day:is_same_day(self.date) then + vim.api.nvim_buf_add_highlight(self.buf, namespace, 'OrgCalendarSelected', opts.line - 1, opts.from - 1, opts.to) + end if self.on_day then self.on_day( day, @@ -231,6 +268,50 @@ function Calendar:on_render_day(day, opts) }) ) end + + vim.api.nvim_set_option_value('modifiable', false, { buf = self.buf }) +end + +function Calendar.left_pad(time_part) + return time_part < 10 and '0' .. time_part or time_part +end + +function Calendar:render_time() + local l_pad = ' ' + local r_pad = ' ' + local hour_str = self:has_time() and Calendar.left_pad(self.date.hour) or '--' + local min_str = self:has_time() and Calendar.left_pad(self.date.min) or '--' + return l_pad .. hour_str .. ':' .. min_str .. r_pad +end + +function Calendar:rerender_time() + vim.api.nvim_set_option_value('modifiable', true, { buf = self.buf }) + vim.api.nvim_buf_set_lines(self.buf, 8, 9, true, { self:render_time() }) + if self:has_time() then + local map_opts = { buffer = self.buf, silent = true, nowait = true } + vim.keymap.set('n', 'T', function() + self:clear_time() + end, map_opts) + vim.keymap.set('n', 'd', function() + self:set_day() + end, map_opts) + if self.select_state == SelState.DAY then + vim.api.nvim_buf_set_lines(self.buf, 13, 14, true, { ' [t] - select day [T] - clear time' }) + else + vim.api.nvim_buf_set_lines(self.buf, 13, 14, true, { ' [d] - select day [T] - clear time' }) + end + vim.api.nvim_buf_add_highlight(self.buf, namespace, 'Normal', 8, 0, -1) + vim.api.nvim_buf_add_highlight(self.buf, namespace, 'Comment', 13, 0, -1) + else + vim.api.nvim_buf_set_lines(self.buf, 13, 14, true, { ' [t] - enter time' }) + vim.api.nvim_buf_add_highlight(self.buf, namespace, 'Comment', 8, 0, -1) + vim.api.nvim_buf_add_highlight(self.buf, namespace, 'Comment', 13, 0, -1) + end + vim.api.nvim_set_option_value('modifiable', false, { buf = self.buf }) +end + +function Calendar:has_time() + return not self.date.date_only end function Calendar:forward() @@ -238,6 +319,7 @@ function Calendar:forward() self:render() vim.fn.cursor(2, 1) vim.fn.search('01') + self:render() end function Calendar:backward() @@ -245,10 +327,21 @@ function Calendar:backward() self:render() vim.fn.cursor(vim.fn.line('$'), 0) vim.fn.search([[\d\d]], 'b') + self:render() end function Calendar:cursor_right() - for i = 1, vim.v.count1 do + if self.select_state ~= SelState.DAY then + if self.select_state == SelState.HOUR then + self:set_min_big() + elseif self.select_state == SelState.MIN_BIG then + self:set_min_small() + elseif self.select_state == SelState.MIN_SMALL then + self:set_sel_hour() + end + return + end + for _ = 1, vim.v.count1 do local line, col = vim.fn.line('.'), vim.fn.col('.') local curr_line = vim.fn.getline('.') local offset = curr_line:sub(col + 1, #curr_line):find('%d%d') @@ -256,10 +349,22 @@ function Calendar:cursor_right() vim.fn.cursor(line, col + offset) end end + self.date = self:get_selected_date() + self:render() end function Calendar:cursor_left() - for i = 1, vim.v.count1 do + if self.select_state ~= SelState.DAY then + if self.select_state == SelState.HOUR then + self:set_min_small() + elseif self.select_state == SelState.MIN_BIG then + self:set_sel_hour() + elseif self.select_state == SelState.MIN_SMALL then + self:set_min_big() + end + return + end + for _ = 1, vim.v.count1 do local line, col = vim.fn.line('.'), vim.fn.col('.') local curr_line = vim.fn.getline('.') local _, offset = curr_line:sub(1, col - 1):find('.*%d%d') @@ -267,10 +372,59 @@ function Calendar:cursor_left() vim.fn.cursor(line, offset) end end + self.date = self:get_selected_date() + self:render() +end + +---@param direction string +---@param step_size number +---@param current number +---@param count number +local function step_minute(direction, step_size, current, count) + local sign = direction == 'up' and -1 or 1 + local residual = current % step_size + local factor = (residual == 0 or direction == 'up') and count or count - 1 + return factor * step_size + sign * residual +end + +--- Controls, how the hours are adjusted. The rounding the minutes can be disabled +--- by the user, so adjusting the hours would just move the time 1 hour back or forth +---@param direction string +---@param current OrgDate +---@param count number +---@return table +local function step_hour(direction, current, count) + if not config.calendar.round_min_with_hours or current.min % big_minute_step == 0 then + return { hour = count, min = 0 } + end + + -- if adjusting the mins would land on a full hour, we don't step a full hour, + -- otherwise we do and round the minutes + local sign = direction == 'up' and 1 or -1 + local min = step_minute(direction, big_minute_step, current.min, 1) + local min_new = current.min + sign * min + local hour = min_new % 60 ~= 0 and count or count - 1 + return { hour = hour, min = min } end function Calendar:cursor_up() - for i = 1, vim.v.count1 do + if self.select_state ~= SelState.DAY then + -- to avoid unexpectedly changing the day we cache it ... + local day = self.date.day + if self.select_state == SelState.HOUR then + self.date = self.date:add(step_hour('up', self.date, vim.v.count1)) + elseif self.select_state == SelState.MIN_BIG then + self.date = self.date:add({ min = step_minute('up', big_minute_step, self.date.min, vim.v.count1) }) + elseif self.select_state == SelState.MIN_SMALL then + self.date = self.date:add({ min = step_minute('up', small_minute_step, self.date.min, vim.v.count1) }) + end + -- and restore the cached day after adjusting the time + self.date = self.date:set({ day = day }) + self:rerender_time() + return + end + + for _ = 1, vim.v.count1 do local line, col = vim.fn.line('.'), vim.fn.col('.') if line > 9 then vim.fn.cursor(line - 1, col) @@ -291,10 +445,26 @@ function Calendar:cursor_up() end vim.fn.cursor(line - 1, move_to) end + self.date = self:get_selected_date() + self:render() end function Calendar:cursor_down() - for i = 1, vim.v.count1 do + if self.select_state ~= SelState.DAY then + local day = self.date.day + if self.select_state == SelState.HOUR then + self.date = self.date:subtract(step_hour('down', self.date, vim.v.count1)) + elseif self.select_state == SelState.MIN_BIG then + self.date = self.date:subtract({ min = step_minute('down', big_minute_step, self.date.min, vim.v.count1) }) + elseif self.select_state == SelState.MIN_SMALL then + self.date = self.date:subtract({ min = step_minute('down', small_minute_step, self.date.min, vim.v.count1) }) + end + -- and restore the cached day after adjusting the time + self.date = self.date:set({ day = day }) + self:rerender_time() + return + end + for _ = 1, vim.v.count1 do local line, col = vim.fn.line('.'), vim.fn.col('.') if line <= 1 then vim.fn.cursor(line + 1, col) @@ -315,6 +485,8 @@ function Calendar:cursor_down() end vim.fn.cursor(line + 1, move_to) end + self.date = self:get_selected_date() + self:render() end function Calendar:reset() @@ -326,21 +498,40 @@ function Calendar:reset() end function Calendar:get_selected_date() + if self.select_state ~= SelState.DAY then + return self.date + end local col = vim.fn.col('.') local char = vim.fn.getline('.'):sub(col, col) - local day = vim.trim(vim.fn.expand('')) + local day = tonumber(vim.trim(vim.fn.expand(''))) local line = vim.fn.line('.') vim.cmd([[redraw!]]) if line < 3 or not char:match('%d') then return utils.echo_warning('Please select valid day number.', nil, false) end - return self.month:set({ day = tonumber(day) }) + return self.date:set({ + month = self.month.month, + day = day, + date_only = self.date.date_only, + }) end function Calendar:select() - local selected_date = self:get_selected_date() + local selected_date + if self.select_state == SelState.DAY then + selected_date = self:get_selected_date() + else + selected_date = self.date:set({ + day = self.date.day, + hour = self.date.hour, + min = self.date.min, + date_only = false, + }) + self.select_state = SelState.DAY + end local cb = self.callback self.callback = nil + vim.cmd([[echon]]) vim.api.nvim_win_close(0, true) return cb(selected_date) @@ -365,7 +556,8 @@ function Calendar:clear_date() end function Calendar:read_date() - vim.ui.input({ prompt = 'Enter date: ' }, function(result) + local default = self:get_selected_date():to_string() + vim.ui.input({ prompt = 'Enter date: ', default = default }, function(result) if result then local date = Date.from_string(result) if not date then @@ -381,4 +573,49 @@ function Calendar:read_date() end) end +function Calendar:set_time() + self.date = self:get_selected_date() + self.date = self.date:set({ date_only = false }) + --self:rerender_time() + self:set_sel_hour() + self:render() -- because we want to highlight the currently selected date, we have to render everything +end + +function Calendar:set_day() + self:set_sel_day() + self:rerender_time() +end + +function Calendar:clear_time() + self.date = self.date:set({ hour = 0, min = 0, date_only = true }) + self:set_sel_day() + self:rerender_time() +end + +function Calendar:set_sel_hour() + self.select_state = SelState.HOUR + vim.fn.cursor({ 9, 16 }) +end + +function Calendar:set_sel_day() + self.select_state = SelState.DAY + self:jump_day() +end + +function Calendar:set_min_big() + self.select_state = SelState.MIN_BIG + vim.fn.cursor({ 9, 19 }) +end + +function Calendar:set_min_small() + self.select_state = SelState.MIN_SMALL + vim.fn.cursor({ 9, 20 }) +end + +function Calendar:jump_day() + local search_day = (self.date or Date.today()):format('%d') + vim.fn.cursor(2, 1) + vim.fn.search(search_day, 'W') +end + return Calendar diff --git a/lua/orgmode/objects/date.lua b/lua/orgmode/objects/date.lua index 6e9e147bd..6784f8444 100644 --- a/lua/orgmode/objects/date.lua +++ b/lua/orgmode/objects/date.lua @@ -29,6 +29,7 @@ local time_format = '%H:%M' ---@field related_date_range OrgDate ---@field dayname string ---@field adjustments string[] +---@private is_today_date boolean? local Date = { ---@type fun(this: OrgDate, other: OrgDate): boolean __eq = function(this, other) @@ -605,11 +606,16 @@ end function Date:is_today() if self.is_today_date == nil then local date = now() - self.is_today_date = date.year == self.year and date.month == self.month and date.day == self.day + self.is_today_date = self:is_same_day(date) end return self.is_today_date end +---@return boolean +function Date:is_same_day(date) + return date and date.year == self.year and date.month == self.month and date.day == self.day +end + ---@return boolean function Date:is_obsolete_range_end() return self.is_date_range_end and self.related_date_range:is_same(self, 'day')