Bloocky logoBloocky
Bloocky logo

A timeblocking calendar, inside Neovim.

Bloocky lets you plan your day by placing time blocks on a calendar with day, week and month views — navigated entirely with hjkl. Free, open source, zero dependencies, and everything stays on your machine.

View on GitHubNeovim ≥ 0.10.0 · Lua only
Bloocky — week view with the day view alongside

Features

  • Three views full month calendar, week grid with hour rows, and a detailed day view.
  • hjkl navigation move across days and hours; H/L jump a whole month or week.
  • Float or sidebar a centered floating window by default, or a persistent split next to your code.
  • Time blocks give an action a start time and a duration, snapped to a configurable granularity (30 min by default).
  • Recurring blocks daily, weekly, weekdays or a custom set of days, with an optional end date.
  • Two-way calendar sync CalDAV and Google Calendar, both directions, conflicts surfaced — never silently resolved.
  • All-day events imported from your calendar and shown above the hour grid, spanning every day they cover.
  • Companion app opt-in LAN bus for your local blocks, paired by QR, three-way merged.
  • Dooing integration read-only: your todos show up on their due date without touching Dooing's data.
  • Configurable working hours decide which hour your day starts and ends, and whether the week starts on Sunday or Monday.
  • Automatic persistence blocks are saved to a plain JSON file on every change.
  • :checkhealth bloocky verifies the things that fail quietly, whether or not you use sync.

Installation

Requires Neovim ≥ 0.10.0. A Nerd Font is optional (icons are configurable); curl only if you turn on calendar sync.

lazy.nvim

{
    "atiladefreitas/bloocky",
    config = function()
        require("bloocky").setup({
            -- your custom config here (optional)
        })
    end,
}

vim.pack (built-in, Neovim ≥ 0.12)

vim.pack.add({ "https://github.com/atiladefreitas/bloocky" })

require("bloocky").setup()

packer.nvim

use({
    "atiladefreitas/bloocky",
    config = function()
        require("bloocky").setup()
    end,
})

vim-plug

Plug 'atiladefreitas/bloocky'

" after plug#end():
lua require("bloocky").setup()

Usage

  1. Open the calendar with <leader>tb (or :Bloocky)
  2. Move around with hjkl; the highlighted slot is your cursor
  3. Press a (or <CR> on an empty slot) to open the block dialog
  4. Fill in the fields — Duration accepts 1h30m, 45m, 2h, 90; set Repeat to daily, weekly, weekdays or custom, with an optional end date
  5. Save with <CR>, and watch the block spread over its hours on the grid
  6. <CR> on an existing block edits it, x deletes it (recurring blocks delete the whole series)

Keybindings

Global

<leader>tbToggle the calendar
<leader>tBToggle the calendar as a sidebar in day view

Inside the calendar

h / lPrevious / next day
j / kNext / previous hour (week/day) or week (month)
H / LPrevious / next month (month view) or week
gd gw gmSwitch to day / week / month view
<Tab>Cycle through the views
tJump to today
aCreate a block at the cursor slot
<CR>Edit the block under the cursor (or create one)
xDelete the block under the cursor
sSync with your calendar now (only when sync is on)
q / <Esc>Close the calendar (<Esc> in floating mode only)

The block dialog has its own set — <Tab> between fields, <CR> or <C-s> to save, dd to clear a field. Invalid fields are marked inline with the reason. Every keymap is remappable via setup().

Commands

:Bloocky [day|week|month]Open the calendar, optionally in a specific view
:BloockyToggleToggle the calendar
:BloockySidebar [view]Open the calendar as a sidebar
:BloockyAddOpen the calendar and jump straight into the creation dialog
:BloockySync [account]Sync now (when sync is enabled)
:BloockySyncStatusLast sync, pending changes and problems per account
:BloockySyncReportConflicts resolved in the calendar's favour
:BloockySyncRestore <n>Restore a losing local version as a new block
:BloockyShareOpen the companion-app pairing QR in your browser
:BloockyServe / :BloockyServeStopStart / stop the app server by hand
:checkhealth bloockyVerify your setup end to end

Configuration

Everything is configured through a single setup(opts) call, merged over sensible defaults. Sync and the companion app are off until you opt in. The most useful options:

require("bloocky").setup({
    -- Where time blocks are persisted
    save_path = vim.fn.stdpath("data") .. "/bloocky_blocks.json",

    -- View shown when the calendar opens: "day" | "week" | "month"
    default_view = "week",

    -- First day of the week: "sunday" | "monday"
    week_start = "sunday",

    -- Visible hour range in the day and week views
    hours = { start = 5, ["end"] = 22 },

    -- Block start/duration are snapped to this many minutes
    granularity = 30,

    window = {
        mode = "float",         -- "float" | "sidebar"
        width = { month = 0.8, week = 0.6, day = 46 },
        height = "auto",        -- "auto" | "full" | number
        border = "rounded",
        sidebar = { position = "right", width = 46, view = "day" },
    },

    -- Two-way sync with a real calendar. Off by default.
    sync = {
        enabled = false,
        accounts = {},          -- CalDAV / Google — see CALENDARS.md
        sync_on_open = true,
        sync_on_edit = true,
        interval_min = 15,
    },

    -- The companion-app bus: bloocky's LAN server for local blocks
    server = {
        enabled = "auto",       -- start on setup only if a device is paired
        port = 7284,
    },

    -- Show Dooing todos on their due date (read-only)
    integrations = {
        dooing = { enabled = false, show_done = false },
    },

    keymaps = {
        toggle = "<leader>tb",
        toggle_sidebar = "<leader>tB",
        -- full in-calendar map in the README
    },
})

Full screen

Set both sizes to "full" and the grid stretches to cover the editor exactly — hour slots grow taller, month cells take the spare rows. Both options also accept a value per view.

require("bloocky").setup({
    window = {
        width = "full",
        height = "full",
    },
})

Highlight groups

Every group (BloockyBlock16, BloockyToday, BloockyCursor, the dialog, the Dooing entries…) is defined with default = true, so your colorscheme can override them:

vim.api.nvim_set_hl(0, "BloockyBlock1", { fg = "#ffffff", bg = "#005f87" })
vim.api.nvim_set_hl(0, "BloockyToday", { fg = "#ff9e64", bold = true })

Calendar sync

Bloocky keeps your blocks in step with a real calendar, in both directions — CalDAV (Fastmail, Nextcloud, iCloud, Radicale…) and Google Calendar with your own OAuth client. Press s in the calendar to sync, or let it happen on open and after each edit.

require("bloocky").setup({
    sync = {
        enabled = true,
        accounts = {
            {
                id = "work",
                provider = "caldav",
                url = "https://caldav.fastmail.com/dav/",
                username = "you@fastmail.com",
                password_cmd = { "secret-tool", "lookup", "service", "bloocky", "key", "caldav" },
            },
        },
    },
})

Conflicts are surfaced and recoverable rather than silently resolved: :BloockySyncReport lists what the calendar overwrote, and :BloockySyncRestore brings a losing version back. Setup, secrets, limits and troubleshooting: CALENDARS.md.

Companion app

Bloocky runs its own small server on your LAN so a companion app can reach your time blocks. Nothing is exposed until you pair a device: :BloockyShare opens a QR code, you scan it from the app, and from then on every route needs that device’s token.

  • One road per block — calendar-backed blocks converge through the calendar; only your local blocks travel this bus.
  • Three-way merged — a title changed on your phone and a time changed in Neovim both survive; when edits genuinely clash, the losing version is kept.
  • Pairing tokens — the QR token lives 10 minutes, single use; device tokens are stored hashed.
  • Your network only bind = "127.0.0.1" keeps it off the LAN entirely if you prefer a tunnel.

Writing a client? The protocol is normative: APP-SYNC.md.

Dooing integration

If you use Dooing, todos with a due date appear on their due day — entries in the month view, a duestrip above the week grid, and a section with time estimates and priorities in the day view. Overdue todos are highlighted in red. The integration is read-only: Bloocky never modifies Dooing’s data.

require("bloocky").setup({
    integrations = {
        dooing = {
            enabled = true,
        },
    },
})