Skip to content

tya5/almanac.nvim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

almanac.nvim

A generic, data-source-agnostic calendar/agenda UI for Neovim: month, week, and day views in a reusable sidebar window, styled after snacks.nvim's public-API conventions (Snacks.win/Snacks.picker). almanac.nvim itself knows nothing about where events come from — you hand it an EventProvider and it renders whatever it's given.

Its first consumer is outlook.nvim (an Outlook COM bridge), but almanac.nvim has no dependency on it or on any specific data source — Google Calendar, org-mode, a local .ics file, anything that can produce a list of {id, title, start, ...} events works.

Status: v1 implemented (month/week/day views, sidebar window management, position/view cycling, edgy.nvim-aware). Not yet tagged/versioned for wider distribution. See docs/DESIGN.md for the full API/IF spec and design rationale.

Preview

Left sidebar, month view (<CR> on a day shows its agenda below the grid; marks a day with events):

[Month]
« July 2026 »
Mo  Tu  We  Th  Fr  Sa  Su
29  30   1   2   3   4   5
 6   7   8   9  10  11  12
13  14  15  16  17  18  19
20  21  22  23  24• 25• 26
27  28  29  30  31   1   2

Fri, Jul 24 2026
  08:16 Team sync
  09:16 1:1 with Sam

Week view (gw) — one row per day, events listed underneath:

[Week]
« Jul 20 - 26 »
Mon 20
Tue 21
Wed 22
Thu 23
Fri 24
  08:16 Team sync
  09:16 1:1 with Sam
Sat 25
  08:16 Design review
Sun 26

Day view (gd) — hourly time-axis:

[Day]
« Fri, Jul 24 2026 »
00:00 -------------
...
07:00 -------------
08:00 -- Team sync
      |  Location: Zoom
09:00 -- 1:1 with Sam
10:00 -------------
...
23:00 -------------

Features

  • Three views — month (traditional grid), week (7-day agenda), day (hourly time-axis) — sharing one Event data model and EventProvider contract. Switch with gm/gw/gd or cycle with <Tab>.
  • Sidebar window, nvim-tree/neo-tree style: left/right/top/bottom/float, reused across opens (not a floating popup, not a takeover of your current window). Cycle position with <C-w><C-w>.
  • Any data source: events can be a static Event[] table or an async function(range, cb) — mirrors snacks.picker's finder contract (called once per visible-range change).
  • edgy.nvim aware: if present (e.g. via LazyVim's :LazyExtrasui.edgy), almanac's own position-cycling steps aside and lets edgy coordinate the sidebar alongside your other edgebars (neo-tree, Trouble, etc.) — see below for the copy-paste stanza.
  • English-only UI text, for global use; only your own event data (titles, locations) passes through untranslated.
  • All highlight groups are linked (default = true), never hardcoded, so any colorscheme applies automatically.

Requirements

  • Neovim >= 0.10

Installation

-- lua/plugins/almanac.lua
return {
  "tya5/almanac.nvim",
}

almanac.nvim is a library, not a standalone command-driven plugin — you construct and drive a Calendar instance yourself (see Usage), typically from another plugin or a small wrapper in your own config.

Usage

local Almanac = require("almanac")

local cal = Almanac({
  events = {
    { id = "1", title = "Team sync", start = os.time() + 3600 },
  },
})

cal:show()

Async data source (called once per visible range; see docs/DESIGN.md 3.2):

local cal = Almanac({
  events = function(range, cb)
    my_backend.fetch(range.from, range.to, function(events)
      cb(events)
    end)
  end,
})

React to selection:

cal:on("event_selected", function(_, event)
  print("selected: " .. event.title)
end)
cal:on("day_selected", function(_, epoch)
  print(os.date("%Y-%m-%d", epoch))
end)

API

See docs/DESIGN.md section 3 for the full spec. Summary:

cal:show() / cal:close() / cal:toggle()
cal:next() cal:prev()               -- page by the *current view's* unit: month/week/day (default <C-f>/<C-b>)
cal:next_day()  cal:prev_day()      -- move focus by a day, regardless of view
cal:next_week() cal:prev_week()     -- move focus by a week
cal:next_month() cal:prev_month()   -- jumps to day 1 of the target month
cal:move_down() cal:move_up()       -- move focus to the next/previous *rendered* line (day cell, agenda day, or event); pages only past the rendered edge (default j/k)
cal:move_left() cal:move_right()    -- move focus within the current grid row (month view); no-op on single-item rows (default h/l)
cal:next_event() cal:prev_event()   -- jump directly between event lines in the current render, no paging, wraps at the edges (default ]e/[e)
cal:goto_date(epoch) cal:today() cal:refresh()
cal:set_view("month"|"week"|"day")  cal:cycle_view()
cal:set_position("left"|"right"|"top"|"bottom"|"float")  cal:cycle_position()
cal:selected_day()  cal:selected_events()  cal:focused_event()
cal:on(event, callback)   -- range_changed, view_changed, day_selected, event_selected, position_changed, close
cal:map(lhs, action)      -- same shapes as opts.keys

Configuration

require("almanac")({
  date = os.time(),        -- initial focused day; default: today
  view = "month",           -- "month" | "week" | "day"
  week_start = "monday",     -- "sunday" | "monday"
  position = "left",         -- "left" | "right" | "top" | "bottom" | "float"
  size = 30,                 -- columns (left/right) or rows (top/bottom); <=1 is a fraction
  manage_position = "auto",  -- "auto": defer to edgy.nvim if present | "always": always self-manage
  events = nil,              -- Event[] or function(range, cb) — see docs/DESIGN.md 3.2
  keys = { --[[ see docs/DESIGN.md 3.4 for the full default table ]] },
  wo = {}, bo = {},          -- window-/buffer-local options
  on_open = nil, on_close = nil,
})

require("almanac").setup(opts) optionally changes the global defaults picked up by every subsequent Almanac(opts) call — not required, each call can pass its own opts instead.

Keymaps (sidebar buffer, filetype = "almanac")

Key Action
h/l move focus within the current row's cells (month view's grid); no-op on rows with only one item (week/day view)
j/k move focus to the next/previous rendered line — a grid row, an agenda day label, or an event line, whichever is actually there
]e/[e jump directly to the next/previous event line in the current render only (no paging; wraps at the first/last event)
<C-f>/<C-b> page by the current view's unit (month in month view, week in week view, day in day view)
gt today
gm/gw/gd switch to month/week/day view
<Tab> cycle view (month → week → day)
<CR> select day/event under cursor (day_selected/event_selected)
<C-w><C-w> cycle sidebar position (no-op if edgy.nvim is managing it)
q close

"Focus" is self.date (plus self:focused_event() when focus has landed on a specific event) — the day/event the calendar considers currently selected. The real Neovim cursor is kept in sync with it after every render, so h/j/k/l feel like moving a cursor through the view rather than the text silently changing under a stationary cursor.

h/j/k/l are screen-driven: each keypress moves focus to whatever is actually rendered next to the cursor right now (via the renderer's line_map), never by computing a new date first and deriving a screen position from it. Date arithmetic (next_day/next_week/etc.) only runs as a fallback once navigation reaches the edge of what's currently drawn — e.g. j on the last rendered line pages to the next week/month/day and lands wherever that page's own render puts focus. This is also why month view's grid flows straight into its trailing per-day agenda with no special-casing, and why week/day view's j/k naturally stop on event lines instead of always moving exactly one day.

edgy.nvim integration

If you use edgy.nvim (bundled as a LazyVim extra: :LazyExtrasui.edgy), add a stanza like this so almanac's sidebar is coordinated alongside your other edgebars:

-- in your edgy.nvim opts
{
  "folke/edgy.nvim",
  opts = {
    left = {
      { ft = "almanac", title = "Calendar", size = { width = 30 }, pinned = true },
    },
  },
}

With edgy.nvim present, almanac's own cycle_position() becomes a no-op by default (opts.manage_position = "auto") so the two don't fight over the same window — position management is edgy's job. Set manage_position = "always" to keep using almanac's own cycling regardless.

Health check

:checkhealth almanac

Design documentation

  • docs/DESIGN.md — full API/IF spec, data model, rendering approach, ecosystem integration research (edgy.nvim, prior art), roadmap
  • tests/README.md — how to run the test suite

Contributing

Lua formatting uses StyLua (stylua.toml); run stylua . before committing. See tests/README.md for running tests.

License

MIT

About

Generic, data-source-agnostic month/week/day calendar UI for Neovim

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages