Files
waypoint/DOCUMENTATION.md
2026-06-10 20:14:30 -04:00

17 KiB
Raw Permalink Blame History

Waypoint Sidebar — Technical Documentation

Obsidian plugin (id: waypoint-sidebar) providing a unified sidebar with calendar, recent files, and custom bookmarks. Replaces separate Calendar and Bookmarks core plugins.


Architecture Overview

src/
├── main.ts                  Plugin entry — lifecycle, commands, events, data persistence
├── settings.ts              Interface definitions + DEFAULT_SETTINGS constant
├── settings-tab.ts          PluginSettingTab UI (calendar, periodic paths, recent files)
├── models/
│   └── bookmark.ts          BookmarkItem + WaypointData interfaces
├── utils/
│   └── date-utils.ts        Moment.js helpers: period formatting, month grid, navigation
└── views/
    └── waypoint-view.ts     ItemView subclass — full sidebar rendering + interaction

Build: esbuild bundles src/main.tsmain.js. TypeScript strict, ESNext target.

Data persistence: Single data.json via Obsidian Plugin.loadData()/saveData(). Top-level keys: settings (merged with DEFAULT_SETTINGS), waypointData (bookmark tree). Recent files are in-memory only (lost on vault close).


Plugin Lifecycle

onload()

  1. Load dataloadSettings() then loadWaypointData(), both merge saved partials over defaults via Object.assign({}, DEFAULT, partial).
  2. Register viewWAYPOINT_VIEW_TYPE = "waypoint-view" maps to WaypointView factory (leaf) => new WaypointView(leaf, this).
  3. Settings tabWaypointSettingTab receives the live settings object + () => this.redrawAll() callback for live preview.
  4. Register commands (see Commands section below).
  5. Register eventsfile-open, vault:create, vault:delete, vault:rename.
  6. Auto-open — On onLayoutReady, if no existing leaves of the view type, opens one in the left sidebar.
  7. Midnight refresh — 10-minute setInterval checks if new Date().toDateString() changed; if so, calls redrawAll() (calendar day indicators refresh).

onunload()

Detaches all leaves of WAYPOINT_VIEW_TYPE.


Data Models

WaypointSettings

interface WaypointSettings {
  calendar: CalendarSettings;         // firstDayOfWeek (0=Sun,1=Mon), showNoteIndicators
  daily: PeriodNoteSettings;
  weekly: PeriodNoteSettings;
  monthly: PeriodNoteSettings;
  quarterly: PeriodNoteSettings;
  yearly: PeriodNoteSettings;
  recentFiles: RecentFilesSettings;   // maxItems, updateOn, omittedPaths[], omittedTags[]
}

interface PeriodNoteSettings {
  folder: string;           // e.g. "periodic/daily"
  templateFile: string;     // path without .md, e.g. "Templates/Daily note"
  nameFormat: string;       // moment.js format string
  typeProperty: string;     // frontmatter type value, e.g. "daily-note"
}

interface CalendarSettings {
  firstDayOfWeek: number;      // 0 = Sunday, 1 = Monday
  showNoteIndicators: boolean; // dot on days with existing .md files
}

interface RecentFilesSettings {
  maxItems: number;                          // default 50
  updateOn: 'file-open' | 'file-edit';       // trigger mode
  omittedPaths: string[];                    // regex patterns (one per line)
  omittedTags: string[];                      // regex patterns for frontmatter tags
}

BookmarkItem (recursive tree)

interface BookmarkItem {
  id: string;              // "bm-{timestamp}-{random4}"
  type: 'file' | 'group' | 'separator' | 'spacer';
  label: string;           // display text (empty for separator/spacer)
  filePath: string;        // vault-relative path (empty for non-file types)
  icon: string;            // Lucide icon name
  children: BookmarkItem[]; // nested items (groups contain children)
  collapsed: boolean;      // group collapse state
  indent: number;          // depth level (0 = root)
}

interface WaypointData {
  bookmarks: BookmarkItem[];
}

The bookmark tree is stored flat in data.json under waypointData.bookmarks but rendered recursively. Groups contain children; files/separators/spacers are leaf nodes.


Commands

Period note navigation (with default hotkeys)

ID Name Hotkey
waypoint-go-to-daily Go to daily note Mod+Shift+Alt+D
waypoint-go-to-weekly Go to weekly note Mod+Shift+Alt+W
waypoint-go-to-monthly Go to monthly note Mod+Shift+Alt+M
waypoint-go-to-quarterly Go to quarterly note Mod+Shift+Alt+Q
waypoint-go-to-yearly Go to yearly note Mod+Shift+Alt+Y

All call openPeriodNote(period, moment()) — opens or creates the current period's note.

Next/Previous period (no default hotkeys)

10 commands generated in a loop: waypoint-go-to-{next|prev}-{daily|weekly|monthly|quarterly|yearly}.

These call navigatePeriodNote(direction) which:

  1. Gets the active file's basename.
  2. Calls detectPeriodType(basename) — regex matching:
    • ^\d{4}-\d{2}-\d{2}$ → daily
    • ^\d{4}-W\d{2}$ → weekly
    • ^\d{4}-\d{2}$ → monthly
    • ^\d{4}-Q[1-4]$ → quarterly
    • ^\d{4}$ → yearly
  3. Parses date via moment(basename, format).
  4. Adds ±1 period (quarters add ±3 months).
  5. Calls openPeriodNote for the new date.

Other commands

ID Name
waypoint-open-view Open Waypoint sidebar
waypoint-add-bookmark Add current file as Waypoint bookmark

Period Note Creation (openPeriodNote)

For a given period + moment date:

  1. Build filename: date.format(periodSettings.nameFormat) + ".md".
  2. Build full path: If periodSettings.folder is set, prepend it; otherwise root.
  3. Check existence: vault.getFileByPath(fullPath).
  4. If not found, create:
    • Try to read template at periodSettings.templateFile + ".md".
    • If template exists → vault.create(fullPath, templateContent).
    • If no template → vault.create(fullPath, minimalFrontmatter) where frontmatter is type: {typeProperty} + date: YYYY-MM-DD.
  5. Open in new leaf: workspace.getLeaf(false).openFile(file).

Event Handling

file-open

If settings.recentFiles.updateOn === 'file-open' → calls addToRecentFiles(file).

If updateOn === 'file-edit' → currently skipped (placeholder for future edit-detection).

vault:create / vault:delete

Both trigger broadcastRedraw() — tells all WaypointView instances to re-render (needed for calendar note indicators and stale recent file references).

vault:rename

  1. Updates recentFiles entry if path matches oldPath.
  2. Calls updateBookmarkPath(oldPath, newPath) — recursively scans waypointData.bookmarks and updates any filePath matching oldPath.

Recent Files (in-memory)

recentFiles: { path: string; basename: string }[]

Update flow:

  1. addToRecentFiles(file) — dedupes (removes existing entry), prepends to front, truncates to maxItems.
  2. Calls broadcastRedraw().

Omitted paths/tags: Settings store regex patterns but the current addToRecentFiles does not filter by them — these are only exposed in the settings UI. The filtering is not yet wired up in the add logic.

On file not found: When clicking a recent file that no longer exists, focusFile() shows a Notice and removes the stale entry from recentFiles.


Sidebar View (WaypointView)

Extends ItemView. View type: "waypoint-view", icon: compass.

Rendering (redraw)

Called on every change. Full DOM rebuild — contentEl.empty() then three sections in order:

  1. Favorites (renderFavorites) — bookmark tree
  2. Recent Files (renderRecentFiles) — flat list
  3. Calendar (renderCalendar) — month grid

The last section (waypoint-section:last-child) gets margin-top: auto, pushing it to the bottom of the sidebar.

Calendar Panel

State: currentDisplayMonth (0-indexed), currentDisplayYear — allows navigating months independently of today.

Layout:

┌────────────────────────────────────┐
│ Q2  June  2026        ◀ Today ▶   │  ← breadcrumb (clickable) + nav
├──┬───┬───┬───┬───┬───┬───┬───┤
│24│sun│mon│tue│wed│thu│fri│sat│  ← header row + week# col
├──┼───┼───┼───┼───┼───┼───┼───┤
│25│ 1 │ 2 │ 3 │...                 ← day cells (clickable)
└──┴───┴───┴───┴───┴───┴───┴───┘
  • Breadcrumb: Q-label, month name, year — each clickable to open that period note.
  • Nav buttons: ◀/▶ shift month ±1. "Today" resets to current month.
  • Week number column: Clicking a week number opens the weekly note for that week's Monday.
  • Day cells: Click opens daily note. .other-month dimmed. .today has accent border. .has-note gets a dot indicator (checked via vault.getFiles().some(f => f.basename === dateStr)).
  • Grid generation: getMonthGrid(year, month, firstDayOfWeek) in date-utils.ts produces up to 6 weeks, each with 7 CalendarDay objects containing moment, dayOfMonth, isToday, isCurrentMonth, isoWeekNumber.

Recent Files Panel

Renders plugin.recentFiles as Obsidian-native nav-file elements using the file explorer's CSS classes (tree-item, nav-file-title, nav-file-title-content).

Per-file features:

  • Active indicator: .is-active class if path matches workspace.getActiveFile().
  • Remove button: × icon, appears on hover (.waypoint-recent-remove), removes entry from array + saves settings + redraws.
  • Drag: Uses app.dragManager.dragFile() for native Obsidian drag.
  • Hover preview: Triggers hover-link event for Obsidian's page preview popup.
  • Context menu: "Open in new tab" + Obsidian's native file-menu event.
  • Click: Keymap.isModEvent(event) determines if opening in new leaf; otherwise same leaf.
  • Middle click: Opens in new tab.

Bookmarks/Favorites Panel

Empty state: Right-click on "Waypoint Bookmarks" header shows add menu:

  • Add current file → addBookmark(path, basename, 'file')
  • New group → addBookmark('', 'New Group', 'group', 'folder')
  • Add separator → addBookmark('', '', 'separator')
  • Add spacer → addBookmark('', '', 'spacer')

Rendering: renderBookmarkList(container, items, depth) recursively renders items with paddingLeft: 8 + depth * 16px for indentation.

Item types:

Type Render Behavior
file icon + label Click opens file. Tooltip = filePath.
group chevron + icon + label Click toggles collapsed. Children rendered in nested waypoint-bookmark-children div (hidden when collapsed).
separator horizontal line (::after pseudo-element) No click handler. Context menu for delete.
spacer 14px empty div No click handler. Context menu for delete.

Drag-and-drop: All bookmark items are draggable. Drop indicators show a 3px accent border above/below the target. On drop:

  1. Remove dragged item from its current position (recursive search).
  2. Insert at the target position (before or after based on cursor Y position relative to target midY).
  3. If target not found (edge case), push to root.
  4. Save + redraw.

Context menu (right-click):

  • File items: "Open in new tab"
  • File/Group items: "Rename" (opens RenameModal), "Change icon" (opens IconSuggestModal)
  • "Move to group" submenu: Lists all available groups (excluding self + descendants) + "(Root)" for ungrouping
  • Group items: "Expand/Collapse", "Add bookmark here", "New sub-group"
  • All items: "Insert separator above/below", "Insert spacer above/below", "Remove"

Move to group (moveBookmarkToGroup):

  1. Remove item from current position.
  2. If targetGroupId: find group, set item.indent = group.indent + 1, push to group's children.
  3. If null (root): set item.indent = 0, push to waypointData.bookmarks.

Rename modal (RenameModal): Simple Modal with text input + Cancel/Save buttons. Enter key submits.

Icon picker (IconSuggestModal): Modal with:

  • Live preview of selected icon.
  • Search input with 60ms debounce.
  • Grid of matching icons (max 80 shown), loaded from https://cdn.jsdelivr.net/npm/lucide-static@0.517.0/tags.json with fallback to https://lucide.dev/api/tags and a hardcoded FALLBACK_ICONS object (~300 icons).
  • Click to select, "No icon" link to clear, Save/Cancel buttons.

CSS Architecture (styles.css)

All classes prefixed with waypoint-. Uses Obsidian CSS variables throughout:

  • --font-ui-small, --font-ui-medium, --font-semibold, --font-medium, --font-light
  • --text-muted, --text-faint, --text-accent, --text-on-accent
  • --background-modifier-border, --background-modifier-active-hover, --background-primary, --background-secondary
  • --interactive-accent
  • --cursor (for custom cursor support)

Key layout:

  • .waypoint-view — flex column, overflow-y: auto, 8px padding.
  • .waypoint-sectionflex-shrink: 0, 16px bottom margin. Last section gets margin-top: auto (pins calendar to bottom).
  • .waypoint-section-header — uppercase, muted, with bottom border.
  • Calendar table — table-layout: fixed, border-collapse: collapse.
  • .waypoint-day.today — accent color text + 1px accent border.
  • .waypoint-day.has-note::after — 4px dot indicator.
  • .waypoint-bm-chevronrotate(-90deg) on collapsed groups.
  • .waypoint-bm-drop-line / .waypoint-bm-drop-below — 3px accent border for drag indicators.

Date Utilities (date-utils.ts)

All use Obsidian's bundled moment (not the npm package).

Function Signature Returns
getPeriodInfo(date) Moment → PeriodInfo {day, week, month, quarter, year} strings for the date
getQuarterString(date) Moment → string e.g. "2026-Q3"
getTodayPeriod() () → PeriodInfo Current date's period info
getMonthGrid(year, month, firstDayOfWeek) number, number, number → CalendarWeek[] Up to 6 weeks, each with 7 CalendarDay objects
formatDateLabel(date) Moment → string "June 1st, 2026"
formatMonthLabel(date) Moment → string "June 2026"
formatQuarterLabel(date) Moment → string "Q3 2026"
navigateDate(date, period, delta) Moment, period, number → Moment Clones + adds delta (quarters: delta×3 months)
formatPeriodName(date, format) Moment, string → string date.format(format) wrapper
getISOWeek(date) Moment → number date.isoWeek()

getMonthGrid algorithm:

  1. Find the first day to display by subtracting (firstOfMonth.day() - firstDayOfWeek + 7) % 7 days from the 1st.
  2. Iterate day-by-day, building 7-day weeks.
  3. Each CalendarDay tracks: date (Moment), dayOfMonth, isToday, isCurrentMonth, isoWeekNumber.
  4. Week's weekNumber = first day's ISO week number.
  5. Safety break after 6 weeks.

Settings Persistence

Settings and waypoint data share a single data.json via Obsidian's Plugin.loadData()/saveData():

// loadData() returns:
{
  settings: { calendar: {...}, daily: {...}, ... },
  waypointData: { bookmarks: [...] }
}

Both loadSettings() and loadWaypointData() read from the same file, merging partials over defaults. saveSettings() and saveWaypointData() each re-read the full data, update their key, and write back. This means they must not run concurrently (no locking — relies on Obsidian's synchronous save behavior).


Redraw Mechanism

broadcastRedraw() iterates all leaves of WAYPOINT_VIEW_TYPE and calls view.redraw() on each. redraw() is a full DOM rebuild (no virtual DOM, no diffing). This is triggered by:

  • File open/create/delete/rename events
  • Bookmark add/remove/update
  • Period navigation
  • Settings changes (via onSettingsChange callback)
  • Midnight detection (10-min interval)

The sidebar is re-rendered from scratch on every change. For a small sidebar this is acceptable; for larger bookmark lists it may cause flicker.


Known Limitations / Gaps

  1. Recent files omittedPaths/omittedTags — Settings UI exposes these regex filters, but addToRecentFiles() does not apply them. Files are never filtered out.
  2. updateOn: 'file-edit' — The file-open handler returns early for this mode, but no edit-detection event is registered. Recent files never update in edit mode.
  3. No virtual DOM — Full rebuild on every redraw. Bookmark drag/drop, rename, and any change tears down and rebuilds the entire sidebar.
  4. Concurrent savessaveSettings() and saveWaypointData() both re-read then write data.json. Rapid sequential calls could race.
  5. Recent files not persisted — Cleared on vault reload. Only bookmarks survive restarts.
  6. Note indicators scan all vault fileshasNoteForDate() iterates vault.getFiles() on every calendar render (called per day cell). For large vaults, this is O(days × files). Not cached.
  7. detectPeriodType uses hardcoded formats — The regex patterns in main.ts don't read from settings.*.nameFormat. If a user changes the name format to something non-standard, next/prev navigation breaks.
  8. IconSuggestModal fetches from CDN — Network dependency on first icon picker open. Falls back to hardcoded list on failure.