@abelfubu/dv 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -0
- package/dist/cli-copy-notification.test.js +7 -3
- package/dist/cli.d.ts +3 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/components/toast.js +1 -1
- package/package.json +9 -2
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# `dv` — Diffview
|
|
2
|
+
|
|
3
|
+
A fast, keyboard-driven diff viewer for the terminal. Pipe any `git diff` into it and navigate changes with a TUI, copy selections, switch themes, and more.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun install -g @abelfubu/dv
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires [Bun](https://bun.sh/).
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Pipe git diff output
|
|
19
|
+
git diff | dv
|
|
20
|
+
|
|
21
|
+
# Diff specific refs
|
|
22
|
+
git diff main...feature | dv
|
|
23
|
+
|
|
24
|
+
# Show working tree changes (default when stdin is empty)
|
|
25
|
+
dv
|
|
26
|
+
|
|
27
|
+
# Scrollback mode for non-TTY output
|
|
28
|
+
git diff | dv --scrollback
|
|
29
|
+
|
|
30
|
+
# Transparent background
|
|
31
|
+
dv --transparent
|
|
32
|
+
|
|
33
|
+
# Watch for changes
|
|
34
|
+
dv --watch
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- **Interactive TUI** built with [`@opentuah/react`](https://github.com/opentuah/opentuah)
|
|
40
|
+
- **Unified and split view** modes (auto-switches based on terminal width)
|
|
41
|
+
- **Directory tree** sidebar for jumping between files
|
|
42
|
+
- **Keyboard selection** and **clipboard copy** of diff text
|
|
43
|
+
- **Syntax highlighting** via Tree-sitter parsers
|
|
44
|
+
- **Themes** — switch at runtime
|
|
45
|
+
- **Transparent background** mode
|
|
46
|
+
- **Scrollback output** when stdout isn't a TTY
|
|
47
|
+
- **Watch mode** for live diff updates
|
|
48
|
+
- **Submodule support** — dirty submodules shown inline
|
|
49
|
+
- **Untracked files** displayed via synthetic diffs
|
|
50
|
+
|
|
51
|
+
## Keybindings
|
|
52
|
+
|
|
53
|
+
| Key | Action |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| `↑` / `↓` or `k` / `j` | Move cursor |
|
|
56
|
+
| `←` / `→` or `h` / `l` | Switch focus (tree / diff) |
|
|
57
|
+
| `Enter` | Jump to file under cursor |
|
|
58
|
+
| `Tab` | Toggle focus between tree and diff |
|
|
59
|
+
| `u` | Toggle unified / split view |
|
|
60
|
+
| `v` | Set selection anchor |
|
|
61
|
+
| `y` | Copy selected diff content |
|
|
62
|
+
| `t` | Cycle themes |
|
|
63
|
+
| `T` | Toggle transparent background |
|
|
64
|
+
| `c` | Change context lines |
|
|
65
|
+
| `r` | Refresh diff |
|
|
66
|
+
| `q` / `Esc` | Quit |
|
|
67
|
+
|
|
68
|
+
## Development
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Install dependencies
|
|
72
|
+
bun install
|
|
73
|
+
|
|
74
|
+
# Run in development
|
|
75
|
+
bun run cli
|
|
76
|
+
|
|
77
|
+
# Watch mode
|
|
78
|
+
bun run cli:watch
|
|
79
|
+
|
|
80
|
+
# Build
|
|
81
|
+
cd /Users/abelfubu/dev/diffview && bun run build
|
|
82
|
+
|
|
83
|
+
# Run tests
|
|
84
|
+
bun test
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Project Structure
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
.
|
|
91
|
+
├── src/
|
|
92
|
+
│ ├── cli.tsx # CLI entrypoint
|
|
93
|
+
│ ├── components/ # React/TUI components
|
|
94
|
+
│ ├── diff-*.ts # Diff parsing, cursor, copy utilities
|
|
95
|
+
│ ├── themes.ts # Theme definitions
|
|
96
|
+
│ ├── store.ts # Persistent state
|
|
97
|
+
│ └── parsers/ # Tree-sitter syntax parsers
|
|
98
|
+
├── docs/ # ADRs and feature docs
|
|
99
|
+
├── dist/ # Compiled output
|
|
100
|
+
└── package.json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Docs
|
|
104
|
+
|
|
105
|
+
- [`docs/adr-cursor-selection.md`](docs/adr-cursor-selection.md)
|
|
106
|
+
- [`docs/transparent-background.md`](docs/transparent-background.md)
|
|
107
|
+
- [`CONTEXT.md`](CONTEXT.md) — domain glossary
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
|
@@ -43,7 +43,7 @@ describe("App copy notification", () => {
|
|
|
43
43
|
});
|
|
44
44
|
it("shows a transient success notification after copying selected lines", async () => {
|
|
45
45
|
const parsedFiles = [createParsedFile("a.ts")];
|
|
46
|
-
testSetup = await testRender(_jsx(App, { parsedFiles: parsedFiles }), {
|
|
46
|
+
testSetup = await testRender(_jsx(App, { parsedFiles: parsedFiles, copyNotificationDuration: 100 }), {
|
|
47
47
|
width: 120,
|
|
48
48
|
height: 16,
|
|
49
49
|
});
|
|
@@ -61,15 +61,19 @@ describe("App copy notification", () => {
|
|
|
61
61
|
testSetup.mockInput.pressKey("j");
|
|
62
62
|
await testSetup.renderOnce();
|
|
63
63
|
});
|
|
64
|
+
testSetup.mockInput.pressKey("y");
|
|
65
|
+
// The toast registers a post-render callback from an effect, and on the
|
|
66
|
+
// single-threaded Linux test renderer the state update isn't flushed in
|
|
67
|
+
// time for the next renderOnce(). Yield briefly before rendering.
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
64
69
|
await act(async () => {
|
|
65
|
-
testSetup.mockInput.pressKey("y");
|
|
66
70
|
await testSetup.renderOnce();
|
|
67
71
|
});
|
|
68
72
|
const frame = testSetup.captureCharFrame();
|
|
69
73
|
expect(frame).toContain("Copied 2 lines");
|
|
70
74
|
expect(frame).toContain("✓");
|
|
71
75
|
// Wait for the auto-hide timeout to clear the notification.
|
|
72
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
76
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
73
77
|
await act(async () => {
|
|
74
78
|
await testSetup.renderOnce();
|
|
75
79
|
});
|
package/dist/cli.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import * as React from "react";
|
|
|
4
4
|
import { type ParsedFile } from "./diff-utils.js";
|
|
5
5
|
export interface AppProps {
|
|
6
6
|
parsedFiles: ParsedFile[];
|
|
7
|
+
/** Duration (ms) the copy notification toast stays visible. Defaults to 2000. */
|
|
8
|
+
copyNotificationDuration?: number;
|
|
7
9
|
}
|
|
8
|
-
export declare function App({ parsedFiles }: AppProps): React.ReactNode;
|
|
10
|
+
export declare function App({ parsedFiles, copyNotificationDuration }: AppProps): React.ReactNode;
|
|
9
11
|
//# sourceMappingURL=cli.d.ts.map
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.tsx"],"names":[],"mappings":";AAOA,OAAO,gCAAgC,CAAC;AAoBxC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAgB/B,OAAO,EAoBN,KAAK,UAAU,EACf,MAAM,iBAAiB,CAAC;AAuIzB,MAAM,WAAW,QAAQ;IACxB,WAAW,EAAE,UAAU,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.tsx"],"names":[],"mappings":";AAOA,OAAO,gCAAgC,CAAC;AAoBxC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAgB/B,OAAO,EAoBN,KAAK,UAAU,EACf,MAAM,iBAAiB,CAAC;AAuIzB,MAAM,WAAW,QAAQ;IACxB,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,iFAAiF;IACjF,wBAAwB,CAAC,EAAE,MAAM,CAAC;CAClC;AAKD,wBAAgB,GAAG,CAAC,EAAE,WAAW,EAAE,wBAA+B,EAAE,EAAE,QAAQ,GAAG,KAAK,CAAC,SAAS,CAuzB/F"}
|
package/dist/cli.js
CHANGED
|
@@ -110,7 +110,7 @@ class ScrollAcceleration {
|
|
|
110
110
|
}
|
|
111
111
|
const SIDEBAR_GAP = 2;
|
|
112
112
|
const APP_HORIZONTAL_PADDING = 2;
|
|
113
|
-
export function App({ parsedFiles }) {
|
|
113
|
+
export function App({ parsedFiles, copyNotificationDuration = 2000 }) {
|
|
114
114
|
const { width: initialWidth, height: initialHeight } = useTerminalDimensions();
|
|
115
115
|
const [width, setWidth] = React.useState(initialWidth);
|
|
116
116
|
const [_terminalHeight, setTerminalHeight] = React.useState(initialHeight);
|
|
@@ -231,7 +231,7 @@ export function App({ parsedFiles }) {
|
|
|
231
231
|
copyNotificationTimeoutRef.current = setTimeout(() => {
|
|
232
232
|
setCopyNotification(null);
|
|
233
233
|
copyNotificationTimeoutRef.current = null;
|
|
234
|
-
},
|
|
234
|
+
}, copyNotificationDuration);
|
|
235
235
|
return () => {
|
|
236
236
|
if (copyNotificationTimeoutRef.current) {
|
|
237
237
|
clearTimeout(copyNotificationTimeoutRef.current);
|
package/dist/components/toast.js
CHANGED
|
@@ -10,7 +10,7 @@ import {} from "../themes.js";
|
|
|
10
10
|
*/
|
|
11
11
|
export function Toast({ message, type, title, theme, transparentBackground }) {
|
|
12
12
|
const renderer = useRenderer();
|
|
13
|
-
React.
|
|
13
|
+
React.useLayoutEffect(() => {
|
|
14
14
|
const borderColor = RGBA.fromHex(type === "success" ? "#2d8a47" : "#c53b53");
|
|
15
15
|
const bgColor = transparentBackground ? RGBA.fromInts(0, 0, 0, 0) : theme.background;
|
|
16
16
|
const fgColor = theme.text;
|
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abelfubu/dv",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "1.0.0",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"bin":
|
|
6
|
+
"bin": {
|
|
7
|
+
"dv": "./dist/cli.js"
|
|
8
|
+
},
|
|
7
9
|
"publishConfig": {
|
|
8
10
|
"access": "public"
|
|
9
11
|
},
|
|
@@ -17,8 +19,13 @@
|
|
|
17
19
|
"build": "rm -rf dist && tsc",
|
|
18
20
|
"prepublishOnly": "bun run build"
|
|
19
21
|
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/abelfubu/diffview.git"
|
|
25
|
+
},
|
|
20
26
|
"devDependencies": {
|
|
21
27
|
"@types/bun": "1.3.5",
|
|
28
|
+
"semantic-release": "^24.2.3",
|
|
22
29
|
"tuistory": "^0.0.13",
|
|
23
30
|
"typescript": "^5.9.3"
|
|
24
31
|
},
|