Almost version 1.0 - doing finishing touches on README
36
README.md
|
|
@ -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]
|
||||

|
||||
|
||||
## 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
|
||||
|
||||

|
||||
|
||||
- **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
|
||||
|
||||

|
||||
|
||||
- **Year and month views** - navigate the full year or zoom into a single month
|
||||
|
||||

|
||||
|
||||
- **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
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
|
|
|||
8
main.js
|
|
@ -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 = `
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
BIN
media/raw/contributions-settings_1.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
media/raw/contributions-settings_2.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9 KiB After Width: | Height: | Size: 9 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
BIN
media/ss-month-views.png
Normal file
|
After Width: | Height: | Size: 231 KiB |
BIN
media/ss-palettes.png
Normal file
|
After Width: | Height: | Size: 354 KiB |
BIN
media/ss-palettes_labelled.png
Normal file
|
After Width: | Height: | Size: 358 KiB |
BIN
media/ss-settings.png
Normal file
|
After Width: | Height: | Size: 365 KiB |
BIN
media/ss-stats-styles.png
Normal file
|
After Width: | Height: | Size: 251 KiB |
BIN
media/ss-tooltip.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
70
src/main.ts
|
|
@ -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
|
||||
|
|
|
|||