Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable user-visible changes to Hunk are documented in this file.

### Added

- Added Catppuccin Latte and Mocha as built-in themes.
- Added mouse-drag text selection in diff views that copies selected rows to the system clipboard via OSC 52. A `View > Copy decorations` toggle (or `copy_decorations` config) controls whether the clipboard includes diff rails, gutters, and file headers or only the changed code.
- Added inline expansion for collapsed unchanged file content. Click an unchanged-context row (`▾ N unchanged lines` when expandable, otherwise the static `··· N unchanged lines ···` form) or press `e` while a hunk is selected to reveal surrounding and trailing file lines without leaving the review. The affordance is shown only for input modes that have reachable source content (`hunk diff`, `show`, `stash show`, file-pair `diff` and `difftool`, untracked files); raw `hunk patch` input still renders as before. Failed and in-flight loads surface a one-line status ("Loading…", "Could not load N unchanged lines") on the gap row. Expanded context rows use the same syntax highlighting as the surrounding diff.

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ You can persist preferences to a config file:
Example:

```toml
theme = "graphite" # graphite, midnight, paper, ember, custom
theme = "graphite" # graphite, midnight, paper, ember, catppuccin-latte, catppuccin-mocha, custom
mode = "auto" # auto, split, stack
vcs = "git" # git, jj
watch = false
Expand All @@ -137,7 +137,7 @@ Custom themes can inherit from any built-in base theme and override only the col
theme = "custom"

[custom_theme]
base = "graphite" # graphite, midnight, paper, ember
base = "graphite" # graphite, midnight, paper, ember, catppuccin-latte, catppuccin-mocha
label = "My Theme"
accent = "#7fd1ff"
panel = "#10161d"
Expand Down
26 changes: 13 additions & 13 deletions docs/opentui-component.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,19 +190,19 @@ If you need direct access to Pierre's parser, `parsePatchFiles(...)` is still re

## Common props

| Prop | Type | Default | Notes |
| -------------------- | ------------------------------------------------ | ------------ | ----------------------------------------------------------------------------------- |
| `layout` | `"split" \| "stack"` | `"split"` | Chooses side-by-side or stacked rendering. |
| `width` | `number` | — | Required content width in terminal columns. |
| `theme` | `"graphite" \| "midnight" \| "paper" \| "ember"` | `"graphite"` | Matches Hunk's built-in themes. |
| `showLineNumbers` | `boolean` | `true` | Toggles line-number columns. |
| `showHunkHeaders` | `boolean` | `true` | Toggles `@@ ... @@` hunk header rows. |
| `showFileSeparators` | `boolean` | `true` | Toggles separator rows between files in `HunkReviewStream`. |
| `wrapLines` | `boolean` | `false` | Wraps long lines instead of clipping horizontally. |
| `horizontalOffset` | `number` | `0` | Scroll offset for non-wrapped code rows. |
| `highlight` | `boolean` | `true` | Enables syntax highlighting. |
| `selectedHunkIndex` | `number` | `0` | Highlights one hunk as the active target for single-file components. |
| `scrollable` | `boolean` | `true` | `HunkDiffView` only; primitives should be wrapped in OpenTUI scrollbox when needed. |
| Prop | Type | Default | Notes |
| -------------------- | -------------------------------------------------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------- |
| `layout` | `"split" \| "stack"` | `"split"` | Chooses side-by-side or stacked rendering. |
| `width` | `number` | — | Required content width in terminal columns. |
| `theme` | `"graphite" \| "midnight" \| "paper" \| "ember" \| "catppuccin-latte" \| "catppuccin-mocha"` | `"graphite"` | Matches Hunk's built-in themes. |
| `showLineNumbers` | `boolean` | `true` | Toggles line-number columns. |
| `showHunkHeaders` | `boolean` | `true` | Toggles `@@ ... @@` hunk header rows. |
| `showFileSeparators` | `boolean` | `true` | Toggles separator rows between files in `HunkReviewStream`. |
| `wrapLines` | `boolean` | `false` | Wraps long lines instead of clipping horizontally. |
| `horizontalOffset` | `number` | `0` | Scroll offset for non-wrapped code rows. |
| `highlight` | `boolean` | `true` | Enables syntax highlighting. |
| `selectedHunkIndex` | `number` | `0` | Highlights one hunk as the active target for single-file components. |
| `scrollable` | `boolean` | `true` | `HunkDiffView` only; primitives should be wrapped in OpenTUI scrollbox when needed. |

## Other exports

Expand Down
9 changes: 8 additions & 1 deletion src/opentui/HunkDiffView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@ describe("OpenTUI public components", () => {
});

test("exports the documented built-in theme names", () => {
expect(HUNK_DIFF_THEME_NAMES).toEqual(["graphite", "midnight", "paper", "ember"]);
expect(HUNK_DIFF_THEME_NAMES).toEqual([
"graphite",
"midnight",
"paper",
"ember",
"catppuccin-latte",
"catppuccin-mocha",
]);
});
});
9 changes: 8 additions & 1 deletion src/opentui/themes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
export const HUNK_DIFF_THEME_NAMES = ["graphite", "midnight", "paper", "ember"] as const;
export const HUNK_DIFF_THEME_NAMES = [
"graphite",
"midnight",
"paper",
"ember",
"catppuccin-latte",
"catppuccin-mocha",
] as const;

export type HunkDiffThemeName = (typeof HUNK_DIFF_THEME_NAMES)[number];
4 changes: 2 additions & 2 deletions src/ui/diff/pierre.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ describe("Pierre diff rows", () => {
test("remaps Pierre markdown reds and greens away from diff-semantic hues", async () => {
const file = createMarkdownDiffFile();

for (const themeId of ["midnight", "paper"] as const) {
for (const themeId of ["midnight", "paper", "catppuccin-latte", "catppuccin-mocha"] as const) {
const theme = resolveTheme(themeId, null);
const highlighted = await loadHighlightedDiff(file, theme.appearance);
const rows = buildStackRows(file, highlighted, theme).filter(
Expand Down Expand Up @@ -453,7 +453,7 @@ describe("Pierre diff rows", () => {
const file = createMarkdownDiffFile();
const highlighted = await loadHighlightedDiff(file, "dark");

for (const themeId of ["graphite", "midnight", "ember"] as const) {
for (const themeId of ["graphite", "midnight", "ember", "catppuccin-mocha"] as const) {
const theme = resolveTheme(themeId, null);
const rows = buildStackRows(file, highlighted, theme).filter(
(row): row is Extract<DiffRow, { type: "stack-line" }> =>
Expand Down
14 changes: 12 additions & 2 deletions src/ui/lib/ui-lib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ describe("ui helpers", () => {
menus.theme
.filter((entry): entry is Extract<MenuEntry, { kind: "item" }> => entry.kind === "item")
.map((entry) => entry.label),
).toEqual(["Graphite", "Midnight", "Paper", "Ember"]);
).toEqual(["Graphite", "Midnight", "Paper", "Ember", "Catppuccin Latte", "Catppuccin Mocha"]);
expect(
menus.theme.some(
(entry) => entry.kind === "item" && entry.label === "Graphite" && entry.checked,
Expand Down Expand Up @@ -238,7 +238,15 @@ describe("ui helpers", () => {
menus.theme
.filter((entry): entry is Extract<MenuEntry, { kind: "item" }> => entry.kind === "item")
.map((entry) => entry.label),
).toEqual(["Graphite", "Midnight", "Paper", "Ember", "My Theme"]);
).toEqual([
"Graphite",
"Midnight",
"Paper",
"Ember",
"Catppuccin Latte",
"Catppuccin Mocha",
"My Theme",
]);
expect(
menus.theme.some(
(entry) => entry.kind === "item" && entry.label === "My Theme" && entry.checked,
Expand Down Expand Up @@ -457,5 +465,7 @@ describe("ui helpers", () => {
expect(missingCustom.id).toBe("graphite");
expect(resolveTheme("ember", null).syntaxStyle).toBeDefined();
expect(custom.syntaxStyle).toBeDefined();
expect(resolveTheme("catppuccin-latte", null).syntaxStyle).toBeDefined();
expect(resolveTheme("catppuccin-mocha", null).syntaxStyle).toBeDefined();
});
});
93 changes: 93 additions & 0 deletions src/ui/themes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { describe, expect, test } from "bun:test";
import { blendHex, hexColorDistance } from "./lib/color";
import { CATPPUCCIN_PALETTES, resolveTheme } from "./themes";

describe("themes", () => {
test("resolves Catppuccin Latte and Mocha by theme id", () => {
const latte = resolveTheme("catppuccin-latte", null);
const mocha = resolveTheme("catppuccin-mocha", null);

expect(latte.id).toBe("catppuccin-latte");
expect(latte.label).toBe("Catppuccin Latte");
expect(latte.appearance).toBe("light");
expect(mocha.id).toBe("catppuccin-mocha");
expect(mocha.label).toBe("Catppuccin Mocha");
expect(mocha.appearance).toBe("dark");
});

test("keeps official Catppuccin sentinel colors in source", () => {
expect(CATPPUCCIN_PALETTES.latte.base).toBe("#eff1f5");
expect(CATPPUCCIN_PALETTES.latte.mauve).toBe("#8839ef");
expect(CATPPUCCIN_PALETTES.latte.green).toBe("#40a02b");
expect(CATPPUCCIN_PALETTES.latte.red).toBe("#d20f39");
expect(CATPPUCCIN_PALETTES.mocha.base).toBe("#1e1e2e");
expect(CATPPUCCIN_PALETTES.mocha.mauve).toBe("#cba6f7");
expect(CATPPUCCIN_PALETTES.mocha.green).toBe("#a6e3a1");
expect(CATPPUCCIN_PALETTES.mocha.red).toBe("#f38ba8");
});

test("derives Catppuccin diff backgrounds from official semantic tokens", () => {
const latte = resolveTheme("catppuccin-latte", null);
const mocha = resolveTheme("catppuccin-mocha", null);

expect(latte.addedBg).toBe(blendHex(CATPPUCCIN_PALETTES.latte.green, latte.contextBg, 0.15));
expect(latte.removedBg).toBe(blendHex(CATPPUCCIN_PALETTES.latte.red, latte.contextBg, 0.15));
expect(latte.addedContentBg).toBe(
blendHex(CATPPUCCIN_PALETTES.latte.green, latte.contextBg, 0.25),
);
expect(latte.removedContentBg).toBe(
blendHex(CATPPUCCIN_PALETTES.latte.red, latte.contextBg, 0.25),
);
expect(mocha.addedBg).toBe(blendHex(CATPPUCCIN_PALETTES.mocha.green, mocha.contextBg, 0.15));
expect(mocha.removedBg).toBe(blendHex(CATPPUCCIN_PALETTES.mocha.red, mocha.contextBg, 0.15));
expect(mocha.addedContentBg).toBe(
blendHex(CATPPUCCIN_PALETTES.mocha.green, mocha.contextBg, 0.25),
);
expect(mocha.removedContentBg).toBe(
blendHex(CATPPUCCIN_PALETTES.mocha.red, mocha.contextBg, 0.25),
);
});

test("keeps Catppuccin add and remove rows semantically distinct", () => {
for (const theme of [
resolveTheme("catppuccin-latte", null),
resolveTheme("catppuccin-mocha", null),
]) {
expect(theme.addedBg).not.toBe(theme.removedBg);
expect(hexColorDistance(theme.addedBg, theme.contextBg)).toBeGreaterThan(0);
expect(hexColorDistance(theme.removedBg, theme.contextBg)).toBeGreaterThan(0);
expect(hexColorDistance(theme.addedContentBg, theme.contextBg)).toBeGreaterThan(
hexColorDistance(theme.addedBg, theme.contextBg),
);
expect(hexColorDistance(theme.removedContentBg, theme.contextBg)).toBeGreaterThan(
hexColorDistance(theme.removedBg, theme.contextBg),
);
}
});

test("maps Catppuccin syntax roles to documented editor tokens", () => {
const latte = resolveTheme("catppuccin-latte", null);
const mocha = resolveTheme("catppuccin-mocha", null);

expect(latte.syntaxColors).toMatchObject({
keyword: CATPPUCCIN_PALETTES.latte.mauve,
string: CATPPUCCIN_PALETTES.latte.green,
comment: CATPPUCCIN_PALETTES.latte.overlay2,
number: CATPPUCCIN_PALETTES.latte.peach,
function: CATPPUCCIN_PALETTES.latte.blue,
property: CATPPUCCIN_PALETTES.latte.blue,
type: CATPPUCCIN_PALETTES.latte.yellow,
punctuation: CATPPUCCIN_PALETTES.latte.overlay2,
});
expect(mocha.syntaxColors).toMatchObject({
keyword: CATPPUCCIN_PALETTES.mocha.mauve,
string: CATPPUCCIN_PALETTES.mocha.green,
comment: CATPPUCCIN_PALETTES.mocha.overlay2,
number: CATPPUCCIN_PALETTES.mocha.peach,
function: CATPPUCCIN_PALETTES.mocha.blue,
property: CATPPUCCIN_PALETTES.mocha.blue,
type: CATPPUCCIN_PALETTES.mocha.yellow,
punctuation: CATPPUCCIN_PALETTES.mocha.overlay2,
});
});
});
Loading