Architecture¶
This document describes the internal architecture of Tickets.nvim.
Module Overview¶
lua/tickets/
├── init.lua Plugin entry point
├── config.lua Configuration validation
├── commands.lua User command registration
├── notifications.lua Centralized messaging
├── cache.lua In-memory + persistent caching
├── persistence.lua Disk cache management (JSON)
├── github.lua GitHub API integration
├── create.lua Issue creation functionality
├── utils.lua Utility functions
├── ui.lua Public UI API
└── ui/
├── config.lua Window configuration
├── formatters.lua Display formatting
├── issue_detail.lua Detail preview logic
├── issue_list.lua Issue list logic
└── prefetch.lua Background prefetching
Module Responsibilities¶
Core Modules¶
init.lua (16 lines)¶
Purpose: Plugin entry point
- Validates user configuration
- Initializes plugin modules
- Exposes public API
config.lua (211 lines)¶
Purpose: Configuration management
- Defines default configuration
- Validates user options
- Merges user config with defaults
- Provides helpful error messages
commands.lua (91 lines)¶
Purpose: Command registration
- Registers all user commands
- Sets up buffer keymaps
- Handles command callbacks
notifications.lua (108 lines)¶
Purpose: User messaging
- Centralizes all vim.notify calls
- Provides consistent message formatting
- Categorizes by type (INFO/WARN/ERROR)
Data Modules¶
cache.lua (150+ lines)¶
Purpose: In-memory + persistent caching
- Caches issue lists per repository (in-memory and on disk)
- Caches detailed issue data
- Auto-loads from disk on first access
- Auto-saves to disk on updates
- Provides cache statistics
- Supports cache invalidation
Data Structure:
persistence.lua (180+ lines)¶
Purpose: Disk cache management
- Saves/loads cache to/from
~/.local/share/nvim/tickets/cache/ - One JSON file per repository (
owner_repo.json) - Includes metadata (last_sync timestamp)
- Safe file operations with error handling
- Statistics and listing functions
github.lua (252 lines)¶
Purpose: GitHub API integration
- Authenticates with gh CLI or GITHUB_TOKEN
- Fetches issue lists
- Fetches detailed issue data with comments
- Handles API errors gracefully
Features: - Automatic fallback from gh CLI to curl - Forces keyring auth when GITHUB_TOKEN is invalid - Caches all responses - Async callbacks with vim.schedule
create.lua (200+ lines)¶
Purpose: Issue creation functionality
- Opens markdown template buffer with frontmatter for metadata
- Parses title, body, labels, and assignees from structured template
- Submits via
gh issue createcommand - Auto-invalidates cache after creation
- Optionally opens created issue in browser
Template Format:
Workflow:
1. :TicketsCreate opens template buffer (scratch buffer)
2. User fills in frontmatter, title, and description
3. <leader>s or <CR><CR> submits the issue
4. Success notification with issue URL
5. Cache invalidated to show new issue
6. Issue optionally opens in browser
utils.lua (51 lines)¶
Purpose: Helper functions
- Path expansion (
~/path→ absolute path) - Repository detection from git remote
- Buffer management utilities
UI Modules¶
ui.lua (48 lines)¶
Purpose: Public UI API
Thin coordinator layer that exposes:
- open_floating_file(path) - Opens todo file
- open_issues_window(issues) - Opens issue list
ui/config.lua (102 lines)¶
Purpose: Window configuration
- Defines layout ratios and spacing
- Calculates window positions and sizes
- Creates window config objects
- Normalizes position values
Functions:
- get_base_dimensions() - Calculates centered window dimensions
- create_list_window_config() - Config for issue list
- create_detail_window_config(list_win) - Config for detail pane
- create_file_window_config() - Config for todo file
ui/formatters.lua (92 lines)¶
Purpose: Data formatting
- Formats timestamps for display
- Formats issue details as markdown
- Formats issue list entries
Output: Markdown-formatted text arrays for display
ui/issue_list.lua (137 lines)¶
Purpose: Issue list window
- Creates and manages issue list buffer
- Sets up keymaps (q, Enter, gx)
- Sets up autocmds (cursor movement, window close)
- Starts background prefetching
- Finds issue at cursor position
Keymaps:
- q - Close window and detail preview
- <CR> - Toggle detail preview
- gx - Open issue in browser
ui/issue_detail.lua (139 lines)¶
Purpose: Detail preview window
- Manages detail preview lifecycle
- Tracks currently viewed issue
- Handles async detail loading
- Updates preview on cursor move
Features: - Issue number tracking (prevents redundant fetches) - Synchronous cache check (instant display) - Stale request detection - Auto-cleanup on window close
ui/prefetch.lua (139 lines)¶
Purpose: Background prefetching
- Queue-based prefetching system
- Fetches uncached issues in background
- Configurable delay and concurrency
- Cancellable when window closes
- Progress tracking and notifications
Data Flow¶
Opening Issue List¶
User runs :TicketsGithubFetch
↓
commands.lua → github.fetch_issues()
↓
github.lua checks cache.get_issues()
↓
├─ Cache hit: Return cached issues immediately
└─ Cache miss: Fetch from API, cache, and return
↓
ui.open_issues_window(issues)
↓
issue_list.lua creates window and starts prefetch
↓
prefetch.lua queues uncached issues for background loading
Viewing Issue Details¶
User presses <CR> on issue
↓
issue_list.lua → issue_detail.show_issue_detail_preview()
↓
issue_detail.lua checks if same issue (tracking)
↓
├─ Same issue: Skip (no-op)
└─ Different issue: Continue
↓
issue_detail.lua checks cache.get_issue_details()
↓
├─ Cache hit: Display instantly (synchronous)
└─ Cache miss: Show "Loading...", fetch from API
↓
formatters.format_issue_details() → Display
Cursor Movement¶
CursorMoved event fires
↓
issue_list.lua autocmd callback
↓
Check if detail preview is open
↓
├─ No: Skip
└─ Yes: Continue
↓
Find issue at cursor position
↓
issue_detail.update_detail_preview(issue)
↓
Check if same issue number
↓
├─ Same: Skip (prevents redundant fetch)
└─ Different: Update preview (see "Viewing Issue Details" above)
Performance Optimizations¶
1. Issue Number Tracking¶
Problem: CursorMoved fires on every cursor movement Solution: Track last viewed issue number, skip if unchanged
local last_viewed_issue = {}
if last_viewed_issue[buf] == issue.number then
return -- Already viewing this issue
end
2. Synchronous Cache Check¶
Problem: Showing "Loading..." even for cached data Solution: Check cache synchronously before async fetch
local cached = cache.get_issue_details(repo, issue_num)
if cached then
display(cached) -- Instant!
return
end
-- Not cached, fetch async
fetch_from_api(...)
3. Background Prefetching¶
Problem: First view of each issue requires API call Solution: Prefetch all uncached issues in background
-- Queue-based: fetches one at a time with delays
prefetch.start_prefetch(buf, repo, issues, {
delay = 500, -- 500ms between fetches
max_concurrent = 1, -- Don't overwhelm API
})
4. Stale Request Detection¶
Problem: Fast navigation causes old responses to overwrite new ones Solution: Track expected issue, discard stale responses
-- After fetch completes
if last_viewed_issue[buf] ~= issue.number then
return -- User moved on, discard this response
end
Extension Points¶
Adding a New Command¶
-
Define command in
commands.lua: -
Add notification in
notifications.lua:
Adding UI Configuration¶
-
Add to defaults in
config.lua: -
Add validation in
config.validate(): -
Use in
ui/config.lua:
Adding a New Data Source¶
- Create module
lua/tickets/my_source.lua - Implement
fetch_issues(callback)function - Update
commands.luato support new source - Cache responses in
cache.lua
Testing Strategy¶
Unit Tests (Recommended)¶
Each module can be tested independently:
-- Test config validation
local config = require("tickets.config")
local validated, errors = config.validate({ target_file = 123 })
assert(#errors > 0, "Should reject non-string target_file")
Integration Tests¶
Test command → API → UI flow:
-- Mock GitHub API
local github = require("tickets.github")
github.fetch_issues = function(callback)
callback({ { number = 1, title = "Test" } })
end
-- Test command
vim.cmd("TicketsGithubFetch")
-- Assert window opened, buffer created, etc.
Debugging¶
Enable Verbose Notifications¶
Modify notifications.lua to log all calls:
local function notify_with_log(msg, level)
print("[TICKETS]", msg) -- Log to :messages
vim.notify(msg, level)
end