Almost version 1.0 - doing finishing touches on README

This commit is contained in:
gearheaded 2026-06-10 23:36:08 -04:00
parent 86b8689cac
commit b15b564845
27 changed files with 79 additions and 35 deletions

View file

@ -1,23 +1,33 @@
# GitHub Contributions for Obsidian
View your GitHub and local Git contribution heatmap directly in the Obsidian sidebar — with streak tracking, per-repo tooltips, colour palettes, and one-click daily note creation.
View your GitHub and local Git contribution heatmap directly in the Obsidian sidebar - with streak tracking, per-repo tooltips, color palettes, and one-click daily note creation.
![GitHub contribution graph in Obsidian sidebar]
![Palette showcase](media/ss-palettes_labelled.png)
## Features
- **Contribution heatmap** - full year or month view, dark/light theme aware
- **Local Git support** - scans your local repos and merges commits into the same graph
- **OAuth authentication** - connect GitHub with one click, no token copying required
- **Year and month navigation** - flip back through any year since 2008, or browse month by month
- **Stats bar** - total contributions, current streak, best streak, days since last commit, most recent repo
- **Hover tooltips** - date, total count, and per-repo breakdown on every cell
![Tooltip showing per-repo breakdown](media/ss-tooltip.png)
- **Click to open daily note** - clicking any day opens (or creates) the matching daily note
- **Five colour palettes** - Default (GitHub greens), High Contrast, Cobalt (blue/cyan), Neon (purple/yellow), Ember (amber/gold)
- **Configurable size** - Ultra compact, Compact, Medium, Large, or Fit to sidebar
- **Five colour palettes** - Classic (GitHub greens), High Contrast, Cobalt (blue/cyan), Neon (purple/yellow), Ember (amber/gold)
- **Three stats styles** - Default list, Compact chips, or Grid
![Stats display styles](media/ss-stats-styles.png)
- **Year and month views** - navigate the full year or zoom into a single month
![Month views](media/ss-month-views.png)
- **Configurable size** - Ultra compact, Compact, Medium, Large, or Fit to sidebar
- **Demo mode** - generates realistic fake data for screenshots or testing
- **OAuth connect** - connect GitHub with one click, no token copying required
- **Mobile safe** - GitHub contributions work on mobile; local Git gracefully disabled
- **Mobile friendly** - GitHub contributions work on mobile; local Git gracefully disabled
## Installation
@ -35,7 +45,7 @@ View your GitHub and local Git contribution heatmap directly in the Obsidian sid
```
<YourVault>/.obsidian/plugins/github-contributions/
```
3. In Obsidian **Settings → Community Plugins**, enable **GitHub Contributions**
3. In Obsidian -> **Settings -> Community Plugins**, enable **GitHub Contributions**
## Connecting GitHub
@ -53,7 +63,7 @@ If you prefer a PAT, switch Authentication to **Personal Access Token** and ente
- Your GitHub username
- A PAT from [github.com/settings/tokens](https://github.com/settings/tokens) with only the `read:user` scope
The token is stored locally in your vault and never leaves your machine except to call the GitHub GraphQL API.
The token is stored locally in your vault and never leaves your machine except to call the GitHub GraphQL API. See the [Privacy](#privacy) section for more information.
## Local Git Support
@ -61,7 +71,7 @@ To include commits from local repositories:
1. Set **Source** to **Local git only** or **Both**
2. Set **Local repo root** to a folder containing your git repos (e.g. `C:\Users\You\Projects`)
3. The plugin scans that folder up to the configured depth and finds all git repos automatically
3. Click **Scan** - the plugin finds all git repos automatically up to the configured depth
Local Git mode only runs `git log` locally - no data is sent anywhere.
@ -69,9 +79,12 @@ Local Git mode only runs `git log` locally - no data is sent anywhere.
- Click the **GitHub icon** in the ribbon
- Or run the command palette: `GitHub Contributions: Open GitHub Contributions panel`
- On mobile: use the command palette (swipe down or tap the search icon)
## Settings reference
![Settings page](media/ss-settings.png)
### Data Sources
| Setting | Description |
|---|---|
@ -85,11 +98,12 @@ Local Git mode only runs `git log` locally - no data is sent anywhere.
|---|---|
| Sidebar side | Left or right panel |
| Stats display | Default list, Compact chips, or Grid |
| Colour palette | Default, High Contrast, Colorblind Friendly, Neon, or Ember |
| Colour palette | Classic, High Contrast, Cobalt, Neon, or Ember |
| Size | Ultra compact, Compact, Medium, Large, or Fit to sidebar |
| Default view | Year or Month |
| Default year | Year shown on first open |
| Demo mode | Show fake data for screenshots or testing |
| Show legend | Show or hide the Less / More colour legend |
### Daily Notes
| Setting | Description |
@ -122,4 +136,4 @@ npm run dev
## License
MIT
MIT

View file

@ -49,7 +49,7 @@ var DEFAULT_SETTINGS = {
selectedYear: new Date().getFullYear(),
sizePreset: "medium",
defaultView: "year",
palette: "default",
palette: "classic",
statsStyle: "default",
dailyNoteFolder: "",
dailyNoteDateFormat: "YYYY-MM-DD"
@ -63,7 +63,7 @@ var PRESET_SIZES = {
// calculated at render time
};
var PALETTES = {
"default": {
"classic": {
dark: ["var(--background-modifier-border)", "#0e4429", "#006d32", "#26a641", "#39d353"],
light: ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"]
},
@ -975,7 +975,7 @@ var GitHubContributionsSettingTab = class extends import_obsidian.PluginSettingT
})
);
new import_obsidian.Setting(containerEl).setName("Colour palette").setDesc("Colour scheme for contribution cells").addDropdown(
(d) => d.addOption("default", "Default (GitHub greens)").addOption("high-contrast", "High contrast (vivid greens)").addOption("cobalt", "Cobalt (blue to cyan)").addOption("neon", "Neon (purple to yellow)").addOption("ember", "Ember (amber to gold)").setValue(this.plugin.settings.palette).onChange(async (v) => {
(d) => d.addOption("classic", "Classic (GitHub greens)").addOption("high-contrast", "High contrast (vivid greens)").addOption("cobalt", "Cobalt (blue to cyan)").addOption("neon", "Neon (purple to yellow)").addOption("ember", "Ember (amber to gold)").setValue(this.plugin.settings.palette).onChange(async (v) => {
this.plugin.settings.palette = v;
await this.plugin.saveSettings();
})
@ -1071,7 +1071,7 @@ var GitHubContributionsPlugin = class extends import_obsidian.Plugin {
const existing = document.getElementById("gh-palette-styles");
if (existing)
existing.remove();
const p = (_a = PALETTES[this.settings.palette]) != null ? _a : PALETTES["default"];
const p = (_a = PALETTES[this.settings.palette]) != null ? _a : PALETTES["classic"];
const style = document.createElement("style");
style.id = "gh-palette-styles";
style.textContent = `

View file

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View file

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View file

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

View file

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View file

Before

Width:  |  Height:  |  Size: 9 KiB

After

Width:  |  Height:  |  Size: 9 KiB

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

BIN
media/ss-month-views.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

BIN
media/ss-palettes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

BIN
media/ss-settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

BIN
media/ss-stats-styles.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

BIN
media/ss-tooltip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View file

@ -11,7 +11,9 @@ import {
requestUrl,
} from "obsidian";
// ── Constants ────────────────────────────────────────────────────────────────
// ########################################################################
// Constants
// ########################################################################
const VIEW_TYPE = "github-contributions";
const GITHUB_GRAPHQL = "https://api.github.com/graphql";
const GITHUB_CLIENT_ID = "Ov23litfj5GbQ8mw81VV";
@ -21,7 +23,9 @@ const GITHUB_USER_URL = "https://api.github.com/user";
const MONTHS = ["January","February","March","April","May","June","July","August","September","October","November","December"];
const MONTHS_SHORT = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
// ── Types ────────────────────────────────────────────────────────────────────
// ########################################################################
// Types
// ########################################################################
type SizePreset = "ultra-compact" | "compact" | "medium" | "large" | "fit";
type ViewMode = "year" | "month";
type DataSource = "github" | "local" | "both";
@ -81,7 +85,7 @@ const DEFAULT_SETTINGS: GitHubContributionsSettings = {
selectedYear: new Date().getFullYear(),
sizePreset: "medium",
defaultView: "year",
palette: "default",
palette: "classic",
statsStyle: "default",
dailyNoteFolder: "",
dailyNoteDateFormat: "YYYY-MM-DD",
@ -96,8 +100,10 @@ const PRESET_SIZES: Record<SizePreset, { cell: number; gap: number }> = {
"fit": { cell: 0, gap: 2 }, // calculated at render time
};
// ── Palettes ─────────────────────────────────────────────────────────────────
type Palette = "default" | "high-contrast" | "cobalt" | "neon" | "ember";
// ########################################################################
// Palettes
// ########################################################################
type Palette = "classic" | "high-contrast" | "cobalt" | "neon" | "ember";
type StatsStyle = "compact" | "default" | "grid";
interface PaletteColors {
@ -106,7 +112,7 @@ interface PaletteColors {
}
const PALETTES: Record<Palette, PaletteColors> = {
"default": {
"classic": {
dark: ["var(--background-modifier-border)", "#0e4429", "#006d32", "#26a641", "#39d353"],
light: ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"],
},
@ -128,7 +134,9 @@ const PALETTES: Record<Palette, PaletteColors> = {
},
};
// ── Helpers ──────────────────────────────────────────────────────────────────
// ########################################################################
// Helpers
// ########################################################################
function debounce<T extends (...args: unknown[]) => void>(fn: T, ms: number): T {
let timer: ReturnType<typeof setTimeout>;
return ((...args: unknown[]) => {
@ -137,7 +145,9 @@ function debounce<T extends (...args: unknown[]) => void>(fn: T, ms: number): T
}) as T;
}
// ── OAuth Device Flow ────────────────────────────────────────────────────────
// ########################################################################
// OAuth Device Flow
// ########################################################################
interface DeviceCodeResponse {
device_code: string;
@ -231,7 +241,9 @@ async function fetchGitHubUsername(token: string): Promise<string> {
return res.json.login;
}
// ── GitHub API ───────────────────────────────────────────────────────────────
// ########################################################################
// GitHub API
// ########################################################################
async function fetchGitHubContributions(
username: string,
token: string,
@ -259,7 +271,9 @@ async function fetchGitHubContributions(
return map;
}
// ── Local Git ────────────────────────────────────────────────────────────────
// ########################################################################
// Local Git
// ########################################################################
// Use Obsidian's adapter to run shell commands (desktop only via child_process via require)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -339,7 +353,9 @@ function fetchLocalCommits(repo: LocalRepo, year: number): Map<string, number> {
return map;
}
// ── Build unified day map ────────────────────────────────────────────────────
// ########################################################################
// Build unified day map
// ########################################################################
async function buildDayMap(
settings: GitHubContributionsSettings,
year: number,
@ -384,7 +400,9 @@ async function buildDayMap(
return { days, totalGH, totalLocal, repoList: repos };
}
// ── Generate full year week structure ────────────────────────────────────────
// ########################################################################
// Generate full year week structure
// ########################################################################
function buildYearWeeks(year: number, days: Map<string, DayData>): DayData[][] {
// Build array of all dates in the year, padded to start on Sunday
const jan1 = new Date(year, 0, 1);
@ -432,7 +450,9 @@ function buildMonthDays(year: number, month: number, days: Map<string, DayData>)
return weeks;
}
// ── Streaks ──────────────────────────────────────────────────────────────────
// ########################################################################
// Streaks
// ########################################################################
function calculateStreaks(days: Map<string, DayData>, year: number): StreakInfo {
const isLeap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
const daysInYear = isLeap ? 366 : 365;
@ -473,7 +493,9 @@ function daysSinceLastCommit(days: Map<string, DayData>): number | null {
return null;
}
// ── Demo mode ────────────────────────────────────────────────────────────────
// ########################################################################
// Demo mode
// ########################################################################
function buildDemoData(year: number): Map<string, DayData> {
const days = new Map<string, DayData>();
const isLeap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
@ -510,7 +532,9 @@ function mostRecentRepo(repos: LocalRepo[]): string | null {
return sorted[0]?.name ?? null;
}
// ── View ─────────────────────────────────────────────────────────────────────
// ########################################################################
// View
// ########################################################################
export class ContributionsView extends ItemView {
plugin: GitHubContributionsPlugin;
displayYear: number;
@ -940,7 +964,9 @@ export class ContributionsView extends ItemView {
}
}
// ── Helpers ──────────────────────────────────────────────────────────────────
// ########################################################################
// Helpers
// ########################################################################
function countToLevel(n: number): number {
if (n === 0) return 0;
if (n <= 2) return 1;
@ -949,7 +975,9 @@ function countToLevel(n: number): number {
return 4;
}
// ── Settings Tab ─────────────────────────────────────────────────────────────
// ########################################################################
// Settings Tab
// ########################################################################
class GitHubContributionsSettingTab extends PluginSettingTab {
plugin: GitHubContributionsPlugin;
constructor(app: App, plugin: GitHubContributionsPlugin) {
@ -1151,7 +1179,7 @@ class GitHubContributionsSettingTab extends PluginSettingTab {
.setName("Colour palette")
.setDesc("Colour scheme for contribution cells")
.addDropdown(d => d
.addOption("default", "Default (GitHub greens)")
.addOption("classic", "Classic (GitHub greens)")
.addOption("high-contrast", "High contrast (vivid greens)")
.addOption("cobalt", "Cobalt (blue to cyan)")
.addOption("neon", "Neon (purple to yellow)")
@ -1222,7 +1250,9 @@ class GitHubContributionsSettingTab extends PluginSettingTab {
}
}
// ── Plugin ───────────────────────────────────────────────────────────────────
// ########################################################################
// Plugin
// ########################################################################
export default class GitHubContributionsPlugin extends Plugin {
settings: GitHubContributionsSettings = { ...DEFAULT_SETTINGS };
@ -1268,7 +1298,7 @@ export default class GitHubContributionsPlugin extends Plugin {
injectPaletteStyles() {
const existing = document.getElementById("gh-palette-styles");
if (existing) existing.remove();
const p = PALETTES[this.settings.palette] ?? PALETTES["default"];
const p = PALETTES[this.settings.palette] ?? PALETTES["classic"];
const style = document.createElement("style");
style.id = "gh-palette-styles";
// Use body instead of :root for reliability in Obsidian's DOM