@abelfubu/dv 0.1.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.
Files changed (118) hide show
  1. package/dist/ansi-html.d.ts +42 -0
  2. package/dist/ansi-html.d.ts.map +1 -0
  3. package/dist/ansi-html.js +327 -0
  4. package/dist/ansi-output.d.ts +22 -0
  5. package/dist/ansi-output.d.ts.map +1 -0
  6. package/dist/ansi-output.js +154 -0
  7. package/dist/balance-delimiters.d.ts +25 -0
  8. package/dist/balance-delimiters.d.ts.map +1 -0
  9. package/dist/balance-delimiters.js +539 -0
  10. package/dist/balance-delimiters.test.d.ts +2 -0
  11. package/dist/balance-delimiters.test.d.ts.map +1 -0
  12. package/dist/balance-delimiters.test.js +1029 -0
  13. package/dist/cli-copy-notification.test.d.ts +2 -0
  14. package/dist/cli-copy-notification.test.d.ts.map +1 -0
  15. package/dist/cli-copy-notification.test.js +80 -0
  16. package/dist/cli-scroll.test.d.ts +2 -0
  17. package/dist/cli-scroll.test.d.ts.map +1 -0
  18. package/dist/cli-scroll.test.js +283 -0
  19. package/dist/cli.d.ts +9 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +976 -0
  22. package/dist/clipboard.d.ts +16 -0
  23. package/dist/clipboard.d.ts.map +1 -0
  24. package/dist/clipboard.js +128 -0
  25. package/dist/components/diff-view.d.ts +32 -0
  26. package/dist/components/diff-view.d.ts.map +1 -0
  27. package/dist/components/diff-view.js +123 -0
  28. package/dist/components/diff-view.test.d.ts +5 -0
  29. package/dist/components/diff-view.test.d.ts.map +1 -0
  30. package/dist/components/diff-view.test.js +312 -0
  31. package/dist/components/directory-tree-view.d.ts +33 -0
  32. package/dist/components/directory-tree-view.d.ts.map +1 -0
  33. package/dist/components/directory-tree-view.js +262 -0
  34. package/dist/components/index.d.ts +4 -0
  35. package/dist/components/index.d.ts.map +1 -0
  36. package/dist/components/index.js +5 -0
  37. package/dist/components/toast.d.ts +21 -0
  38. package/dist/components/toast.d.ts.map +1 -0
  39. package/dist/components/toast.js +47 -0
  40. package/dist/diff-cursor-utils.d.ts +20 -0
  41. package/dist/diff-cursor-utils.d.ts.map +1 -0
  42. package/dist/diff-cursor-utils.js +105 -0
  43. package/dist/diff-cursor-utils.test.d.ts +2 -0
  44. package/dist/diff-cursor-utils.test.d.ts.map +1 -0
  45. package/dist/diff-cursor-utils.test.js +40 -0
  46. package/dist/diff-surface-copy.d.ts +23 -0
  47. package/dist/diff-surface-copy.d.ts.map +1 -0
  48. package/dist/diff-surface-copy.js +64 -0
  49. package/dist/diff-surface-copy.test.d.ts +5 -0
  50. package/dist/diff-surface-copy.test.d.ts.map +1 -0
  51. package/dist/diff-surface-copy.test.js +142 -0
  52. package/dist/diff-utils.d.ts +196 -0
  53. package/dist/diff-utils.d.ts.map +1 -0
  54. package/dist/diff-utils.js +682 -0
  55. package/dist/diff-utils.test.d.ts +2 -0
  56. package/dist/diff-utils.test.d.ts.map +1 -0
  57. package/dist/diff-utils.test.js +727 -0
  58. package/dist/directory-tree.d.ts +72 -0
  59. package/dist/directory-tree.d.ts.map +1 -0
  60. package/dist/directory-tree.js +161 -0
  61. package/dist/directory-tree.test.d.ts +2 -0
  62. package/dist/directory-tree.test.d.ts.map +1 -0
  63. package/dist/directory-tree.test.js +383 -0
  64. package/dist/dropdown.d.ts +26 -0
  65. package/dist/dropdown.d.ts.map +1 -0
  66. package/dist/dropdown.js +172 -0
  67. package/dist/dropdown.test.d.ts +2 -0
  68. package/dist/dropdown.test.d.ts.map +1 -0
  69. package/dist/dropdown.test.js +106 -0
  70. package/dist/filter-submodule.e2e.test.d.ts +2 -0
  71. package/dist/filter-submodule.e2e.test.d.ts.map +1 -0
  72. package/dist/filter-submodule.e2e.test.js +109 -0
  73. package/dist/hooks/use-copy-selection.d.ts +29 -0
  74. package/dist/hooks/use-copy-selection.d.ts.map +1 -0
  75. package/dist/hooks/use-copy-selection.js +46 -0
  76. package/dist/kv-codec.d.ts +16 -0
  77. package/dist/kv-codec.d.ts.map +1 -0
  78. package/dist/kv-codec.js +36 -0
  79. package/dist/license.d.ts +14 -0
  80. package/dist/license.d.ts.map +1 -0
  81. package/dist/license.js +63 -0
  82. package/dist/logger.d.ts +9 -0
  83. package/dist/logger.d.ts.map +1 -0
  84. package/dist/logger.js +78 -0
  85. package/dist/monochrome.d.ts +34 -0
  86. package/dist/monochrome.d.ts.map +1 -0
  87. package/dist/monochrome.js +613 -0
  88. package/dist/monotone.d.ts +22 -0
  89. package/dist/monotone.d.ts.map +1 -0
  90. package/dist/monotone.js +185 -0
  91. package/dist/parsers-config.d.ts +19 -0
  92. package/dist/parsers-config.d.ts.map +1 -0
  93. package/dist/parsers-config.js +271 -0
  94. package/dist/patch-terminal-dimensions.d.ts +2 -0
  95. package/dist/patch-terminal-dimensions.d.ts.map +1 -0
  96. package/dist/patch-terminal-dimensions.js +45 -0
  97. package/dist/stdin-pager.test.d.ts +2 -0
  98. package/dist/stdin-pager.test.d.ts.map +1 -0
  99. package/dist/stdin-pager.test.js +497 -0
  100. package/dist/store.d.ts +16 -0
  101. package/dist/store.d.ts.map +1 -0
  102. package/dist/store.js +48 -0
  103. package/dist/themes/github.json +247 -0
  104. package/dist/themes.d.ts +59 -0
  105. package/dist/themes.d.ts.map +1 -0
  106. package/dist/themes.js +248 -0
  107. package/dist/tree-icons.d.ts +4 -0
  108. package/dist/tree-icons.d.ts.map +1 -0
  109. package/dist/tree-icons.js +18 -0
  110. package/dist/utils.d.ts +2 -0
  111. package/dist/utils.d.ts.map +1 -0
  112. package/dist/utils.js +13 -0
  113. package/dist/web-utils.d.ts +56 -0
  114. package/dist/web-utils.d.ts.map +1 -0
  115. package/dist/web-utils.js +363 -0
  116. package/package.json +37 -0
  117. package/public/jetbrains-mono-nerd.ttf +0 -0
  118. package/public/jetbrains-mono-nerd.woff2 +0 -0
@@ -0,0 +1,262 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@opentuah/react/jsx-runtime";
2
+ // DirectoryTreeView - Renders a directory tree with file status colors and change counts.
3
+ // Shows added files in green, modified in default text, deleted in red, renamed in yellow.
4
+ // Change counts (+n,-n) use green/red for the numbers, brackets are muted.
5
+ // Supports fixed-width sidebar rendering with filename-preserving truncation.
6
+ import * as React from "react";
7
+ import { RGBA } from "@opentuah/core";
8
+ import { buildHierarchicalTree } from "../directory-tree.js";
9
+ import { getResolvedTheme, rgbaToHex } from "../themes.js";
10
+ import { FOLDER_ICON_CLOSED, FOLDER_ICON_OPEN, getFileIcon } from "../tree-icons.js";
11
+ const ICON_WIDTH = 3; // 2-char nerd font icon + 1 space
12
+ export const DEFAULT_SIDEBAR_WIDTH = 60;
13
+ /**
14
+ * Get the color for a file based on its status
15
+ * Uses diff colors from theme: green (added), red (deleted), yellow (renamed), default text (modified)
16
+ */
17
+ function getStatusColor(status, theme) {
18
+ switch (status) {
19
+ case "added":
20
+ return rgbaToHex(theme.diffAdded); // green
21
+ case "deleted":
22
+ return rgbaToHex(theme.diffRemoved); // red
23
+ case "renamed":
24
+ return rgbaToHex(theme.warning); // yellow - renamed/moved file
25
+ case "modified":
26
+ return rgbaToHex(theme.text); // default text color, same as folders
27
+ }
28
+ }
29
+ function truncateKeepingEnd(text, maxWidth) {
30
+ if (maxWidth <= 0) {
31
+ return "";
32
+ }
33
+ if (text.length <= maxWidth) {
34
+ return text;
35
+ }
36
+ if (maxWidth === 1) {
37
+ return "…";
38
+ }
39
+ return `…${text.slice(-(maxWidth - 1))}`;
40
+ }
41
+ function getStatsParts(node) {
42
+ const additions = node.additions ?? 0;
43
+ const deletions = node.deletions ?? 0;
44
+ const hasAdditions = additions > 0;
45
+ const hasDeletions = deletions > 0;
46
+ let text = " (";
47
+ if (hasAdditions) {
48
+ text += `+${additions}`;
49
+ }
50
+ if (hasAdditions && hasDeletions) {
51
+ text += ",";
52
+ }
53
+ if (hasDeletions) {
54
+ text += `-${deletions}`;
55
+ }
56
+ text += ")";
57
+ return {
58
+ hasAdditions,
59
+ hasDeletions,
60
+ text,
61
+ };
62
+ }
63
+ /**
64
+ * Render a single tree node line with proper colors
65
+ */
66
+ function TreeNodeLine({ node, theme, mutedColor, textColor, onSelect, width, isActive, isFocused, isCollapsed, transparentBackground, }) {
67
+ const [isHovered, setIsHovered] = React.useState(false);
68
+ const treePrefix = `${node.prefix}${node.connector}`;
69
+ const icon = node.isFile
70
+ ? getFileIcon(node.displayPath)
71
+ : isCollapsed
72
+ ? FOLDER_ICON_CLOSED
73
+ : FOLDER_ICON_OPEN;
74
+ const iconFg = isFocused ? rgbaToHex(theme.primary) : mutedColor;
75
+ if (node.isFile) {
76
+ // File node - colorize based on status
77
+ const pathColor = node.status ? getStatusColor(node.status, theme) : textColor;
78
+ const addColor = rgbaToHex(theme.diffAdded); // green
79
+ const delColor = rgbaToHex(theme.diffRemoved); // red
80
+ const stats = getStatsParts(node);
81
+ const availablePathWidth = Math.max(1, width - ICON_WIDTH - treePrefix.length - stats.text.length);
82
+ const truncatedPath = truncateKeepingEnd(node.displayPath, availablePathWidth);
83
+ const activeBg = isActive ? rgbaToHex(theme.primary.brighten(0.3)) : undefined;
84
+ const focusBg = isFocused ? rgbaToHex(theme.primary.brighten(0.15)) : undefined;
85
+ const hoverBg = isHovered && !transparentBackground ? rgbaToHex(theme.backgroundPanel) : RGBA.fromInts(0, 0, 0, 0);
86
+ const bgColor = activeBg ?? focusBg ?? hoverBg;
87
+ return (_jsxs("box", { style: {
88
+ flexDirection: "row",
89
+ width,
90
+ backgroundColor: bgColor,
91
+ }, onMouseMove: () => setIsHovered(true), onMouseOut: () => setIsHovered(false), children: [_jsx("text", { fg: mutedColor, children: treePrefix }), _jsxs("text", { fg: iconFg, children: [icon, " "] }), _jsx("text", { fg: pathColor, children: truncatedPath }), _jsx("text", { fg: mutedColor, children: " (" }), stats.hasAdditions && _jsxs("text", { fg: addColor, children: ["+", node.additions] }), stats.hasAdditions && stats.hasDeletions && _jsx("text", { fg: mutedColor, children: "," }), stats.hasDeletions && _jsxs("text", { fg: delColor, children: ["-", node.deletions] }), _jsx("text", { fg: mutedColor, children: ")" })] }));
92
+ }
93
+ const availablePathWidth = Math.max(1, width - ICON_WIDTH - treePrefix.length);
94
+ const truncatedPath = truncateKeepingEnd(node.displayPath, availablePathWidth);
95
+ // Directory node - use muted color for everything
96
+ const dirActiveBg = isActive ? rgbaToHex(theme.primary.brighten(0.3)) : undefined;
97
+ const dirFocusBg = isFocused ? rgbaToHex(theme.primary.brighten(0.15)) : undefined;
98
+ return (_jsxs("box", { style: { flexDirection: "row", width, backgroundColor: dirActiveBg ?? dirFocusBg }, children: [_jsx("text", { fg: mutedColor, children: treePrefix }), _jsxs("text", { fg: iconFg, children: [icon, " "] }), _jsx("text", { fg: textColor, children: truncatedPath })] }));
99
+ }
100
+ /**
101
+ * DirectoryTreeView component
102
+ * Renders a directory tree with file status colors and fixed-width truncation for sidebar layout.
103
+ */
104
+ function flattenVisible(nodes, collapsedPaths, prefix = "") {
105
+ const result = [];
106
+ nodes.forEach((node) => {
107
+ result.push({
108
+ displayPath: node.displayPath,
109
+ fullPath: node.fullPath,
110
+ isFile: node.isFile,
111
+ fileIndex: node.fileIndex,
112
+ status: node.status,
113
+ additions: node.additions,
114
+ deletions: node.deletions,
115
+ prefix,
116
+ connector: " ",
117
+ });
118
+ if (!node.isFile && !collapsedPaths.has(node.displayPath)) {
119
+ result.push(...flattenVisible(node.children, collapsedPaths, prefix + " "));
120
+ }
121
+ });
122
+ return result;
123
+ }
124
+ export const DirectoryTreeView = React.forwardRef(function DirectoryTreeView({ files, onFileSelect, onFolderSelect, onFocusRowChange, themeName, width = DEFAULT_SIDEBAR_WIDTH, activeFileIndex, activeFolderPath, initialCollapsedPaths = [], transparentBackground, }, ref) {
125
+ const [collapsedPaths, setCollapsedPaths] = React.useState(() => new Set(initialCollapsedPaths));
126
+ const [focusedRowIndex, setFocusedRowIndex] = React.useState(0);
127
+ const onFileSelectRef = React.useRef(onFileSelect);
128
+ onFileSelectRef.current = onFileSelect;
129
+ const onFolderSelectRef = React.useRef(onFolderSelect);
130
+ onFolderSelectRef.current = onFolderSelect;
131
+ const hierarchicalNodes = React.useMemo(() => buildHierarchicalTree(files), [files]);
132
+ const visibleNodes = React.useMemo(() => flattenVisible(hierarchicalNodes, collapsedPaths), [hierarchicalNodes, collapsedPaths]);
133
+ React.useEffect(() => {
134
+ setFocusedRowIndex((prev) => Math.min(prev, Math.max(0, visibleNodes.length - 1)));
135
+ }, [visibleNodes.length]);
136
+ React.useEffect(() => {
137
+ onFocusRowChange?.(focusedRowIndex);
138
+ }, [focusedRowIndex, onFocusRowChange]);
139
+ const activeFileFolders = React.useMemo(() => {
140
+ const result = new Set();
141
+ if (activeFileIndex === undefined)
142
+ return result;
143
+ function walk(nodes) {
144
+ for (const node of nodes) {
145
+ if (node.isFile && node.fileIndex === activeFileIndex) {
146
+ return true;
147
+ }
148
+ if (!node.isFile) {
149
+ const hasActiveFile = walk(node.children);
150
+ if (hasActiveFile) {
151
+ result.add(node.displayPath);
152
+ }
153
+ }
154
+ }
155
+ return false;
156
+ }
157
+ walk(hierarchicalNodes);
158
+ return result;
159
+ }, [hierarchicalNodes, activeFileIndex]);
160
+ React.useEffect(() => {
161
+ if (activeFolderPath) {
162
+ const idx = visibleNodes.findIndex((n) => !n.isFile && n.fullPath === activeFolderPath);
163
+ if (idx >= 0) {
164
+ setFocusedRowIndex(idx);
165
+ return;
166
+ }
167
+ }
168
+ if (activeFileIndex === undefined)
169
+ return;
170
+ const idx = visibleNodes.findIndex((n) => n.isFile && n.fileIndex === activeFileIndex);
171
+ if (idx >= 0) {
172
+ setFocusedRowIndex(idx);
173
+ return;
174
+ }
175
+ // Active file is hidden in a collapsed folder — focus the containing folder
176
+ for (let i = 0; i < visibleNodes.length; i++) {
177
+ const node = visibleNodes[i];
178
+ if (node && !node.isFile && collapsedPaths.has(node.displayPath) && activeFileFolders.has(node.displayPath)) {
179
+ setFocusedRowIndex(i);
180
+ return;
181
+ }
182
+ }
183
+ }, [activeFileIndex, activeFolderPath, visibleNodes, activeFileFolders, collapsedPaths]);
184
+ React.useImperativeHandle(ref, () => ({
185
+ focusNext() {
186
+ setFocusedRowIndex((prev) => {
187
+ const next = Math.min(visibleNodes.length - 1, prev + 1);
188
+ const node = visibleNodes[next];
189
+ if (node?.isFile && node.fileIndex !== undefined) {
190
+ onFileSelectRef.current?.(node.fileIndex);
191
+ }
192
+ else if (node && !node.isFile) {
193
+ onFolderSelectRef.current?.(node.fullPath);
194
+ }
195
+ return next;
196
+ });
197
+ },
198
+ focusPrev() {
199
+ setFocusedRowIndex((prev) => {
200
+ const next = Math.max(0, prev - 1);
201
+ const node = visibleNodes[next];
202
+ if (node?.isFile && node.fileIndex !== undefined) {
203
+ onFileSelectRef.current?.(node.fileIndex);
204
+ }
205
+ else if (node && !node.isFile) {
206
+ onFolderSelectRef.current?.(node.fullPath);
207
+ }
208
+ return next;
209
+ });
210
+ },
211
+ toggleCollapse() {
212
+ setFocusedRowIndex((prev) => {
213
+ const node = visibleNodes[prev];
214
+ if (!node || node.isFile)
215
+ return prev;
216
+ const path = node.displayPath;
217
+ setCollapsedPaths((current) => {
218
+ const next = new Set(current);
219
+ if (next.has(path))
220
+ next.delete(path);
221
+ else
222
+ next.add(path);
223
+ return next;
224
+ });
225
+ return prev;
226
+ });
227
+ },
228
+ getActiveRowIndex() {
229
+ if (activeFolderPath) {
230
+ const idx = visibleNodes.findIndex((n) => !n.isFile && n.fullPath === activeFolderPath);
231
+ if (idx >= 0)
232
+ return idx;
233
+ }
234
+ const idx = visibleNodes.findIndex((n) => n.isFile && n.fileIndex === activeFileIndex);
235
+ if (idx >= 0)
236
+ return idx;
237
+ for (let i = 0; i < visibleNodes.length; i++) {
238
+ const node = visibleNodes[i];
239
+ if (node && !node.isFile && collapsedPaths.has(node.displayPath) && activeFileFolders.has(node.displayPath)) {
240
+ return i;
241
+ }
242
+ }
243
+ return 0;
244
+ },
245
+ }), [visibleNodes, activeFileFolders, activeFileIndex, activeFolderPath, collapsedPaths]);
246
+ const resolvedTheme = getResolvedTheme(themeName);
247
+ const mutedColor = rgbaToHex(resolvedTheme.textMuted);
248
+ const textColor = rgbaToHex(resolvedTheme.text);
249
+ if (visibleNodes.length === 0) {
250
+ return null;
251
+ }
252
+ return (_jsx("box", { style: {
253
+ flexDirection: "column",
254
+ width,
255
+ }, children: visibleNodes.map((node, idx) => (_jsx(TreeNodeLine, { node: node, theme: resolvedTheme, mutedColor: mutedColor, textColor: textColor, width: width, isActive: (node.isFile && node.fileIndex === activeFileIndex) ||
256
+ (!node.isFile && (node.fullPath === activeFolderPath ||
257
+ (collapsedPaths.has(node.displayPath) && activeFileFolders.has(node.displayPath)))), isFocused: idx === focusedRowIndex, isCollapsed: !node.isFile && collapsedPaths.has(node.displayPath), transparentBackground: transparentBackground, onSelect: node.isFile && node.fileIndex !== undefined && onFileSelect
258
+ ? () => onFileSelect(node.fileIndex)
259
+ : !node.isFile && onFolderSelect
260
+ ? () => onFolderSelect(node.fullPath)
261
+ : undefined }, idx))) }));
262
+ });
@@ -0,0 +1,4 @@
1
+ export { DiffView, type DiffViewProps, type DiffViewRef } from "./diff-view.js";
2
+ export { DEFAULT_SIDEBAR_WIDTH, DirectoryTreeView, type DirectoryTreeViewProps, type DirectoryTreeViewRef, } from "./directory-tree-view.js";
3
+ export { Toast, type ToastProps } from "./toast.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC/E,OAAO,EACN,qBAAqB,EAAE,iBAAiB,EAAE,KAAK,sBAAsB,EAAE,KAAK,oBAAoB,GAChG,MAAM,0BAA0B,CAAA;AACjC,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAA"}
@@ -0,0 +1,5 @@
1
+ // Shared React components for the TUI interface.
2
+ // Exports reusable components used across main diff view and review mode.
3
+ export { DiffView } from "./diff-view.js";
4
+ export { DEFAULT_SIDEBAR_WIDTH, DirectoryTreeView, } from "./directory-tree-view.js";
5
+ export { Toast } from "./toast.js";
@@ -0,0 +1,21 @@
1
+ import * as React from "react";
2
+ import { type ResolvedTheme } from "../themes.js";
3
+ export interface ToastProps {
4
+ /** Toast message body. */
5
+ message: string;
6
+ /** Visual style — success (green) or error (red). */
7
+ type: "success" | "error";
8
+ /** Optional title shown above the message. */
9
+ title?: string;
10
+ /** Current resolved theme for colors. */
11
+ theme: ResolvedTheme;
12
+ /** Whether the app is using a transparent background. */
13
+ transparentBackground?: boolean;
14
+ }
15
+ /**
16
+ * Render a floating toast notification directly onto the renderer's root
17
+ * using a post-render callback. This lets the toast overlay all other content
18
+ * without taking part in layout.
19
+ */
20
+ export declare function Toast({ message, type, title, theme, transparentBackground }: ToastProps): React.ReactNode;
21
+ //# sourceMappingURL=toast.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toast.d.ts","sourceRoot":"","sources":["../../src/components/toast.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAEjD,MAAM,WAAW,UAAU;IACzB,0BAA0B;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,qDAAqD;IACrD,IAAI,EAAE,SAAS,GAAG,OAAO,CAAA;IACzB,8CAA8C;IAC9C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,yCAAyC;IACzC,KAAK,EAAE,aAAa,CAAA;IACpB,yDAAyD;IACzD,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC;AAGD;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,qBAAqB,EAAE,EAAE,UAAU,GAAG,KAAK,CAAC,SAAS,CAwCzG"}
@@ -0,0 +1,47 @@
1
+ // Floating toast notification used for transient copy feedback.
2
+ import { RGBA, TextAttributes } from "@opentuah/core";
3
+ import * as React from "react";
4
+ import { useRenderer } from "@opentuah/react";
5
+ import {} from "../themes.js";
6
+ /**
7
+ * Render a floating toast notification directly onto the renderer's root
8
+ * using a post-render callback. This lets the toast overlay all other content
9
+ * without taking part in layout.
10
+ */
11
+ export function Toast({ message, type, title, theme, transparentBackground }) {
12
+ const renderer = useRenderer();
13
+ React.useEffect(() => {
14
+ const borderColor = RGBA.fromHex(type === "success" ? "#2d8a47" : "#c53b53");
15
+ const bgColor = transparentBackground ? RGBA.fromInts(0, 0, 0, 0) : theme.background;
16
+ const fgColor = theme.text;
17
+ const icon = type === "success" ? "✓" : "✗";
18
+ const fullText = title ? `${icon} ${title} ${message}` : `${icon} ${message}`;
19
+ const toastWidth = Math.min(60, Math.max(20, fullText.length + 4));
20
+ const toastHeight = 3;
21
+ const x = Math.max(0, Math.floor((renderer.width - toastWidth) / 2));
22
+ const y = Math.max(0, Math.min(2, renderer.height - toastHeight));
23
+ const postProcess = (buffer, _deltaTime) => {
24
+ buffer.drawBox({
25
+ x,
26
+ y,
27
+ width: toastWidth,
28
+ height: toastHeight,
29
+ border: true,
30
+ borderStyle: "rounded",
31
+ borderColor,
32
+ backgroundColor: bgColor,
33
+ shouldFill: true,
34
+ title: undefined,
35
+ titleAlignment: "left",
36
+ });
37
+ buffer.drawText(fullText, x + 2, y + 1, fgColor, bgColor, TextAttributes.BOLD);
38
+ };
39
+ renderer.addPostProcessFn(postProcess);
40
+ renderer.requestRender();
41
+ return () => {
42
+ renderer.removePostProcessFn(postProcess);
43
+ renderer.requestRender();
44
+ };
45
+ }, [message, type, title, theme, transparentBackground, renderer]);
46
+ return null;
47
+ }
@@ -0,0 +1,20 @@
1
+ export interface DiffLogicalLine {
2
+ type: "hunk-header" | "context" | "add" | "remove" | "empty";
3
+ content: string;
4
+ oldLineNum?: number;
5
+ newLineNum?: number;
6
+ }
7
+ /**
8
+ * Rebuild the logical line order that DiffRenderable uses for its unified view.
9
+ * Each returned item corresponds to one row in the internal CodeRenderable content,
10
+ * so line indices match what DiffRenderable.highlightLines() expects.
11
+ *
12
+ * This mirrors opentuah's DiffRenderable.buildUnifiedView: consecutive removed
13
+ * and added lines are grouped into one change block, with all removed lines
14
+ * emitted before all added lines.
15
+ *
16
+ * Hunk headers are included as rows so cursor alignment stays correct, but they
17
+ * are skipped when extracting code.
18
+ */
19
+ export declare function buildUnifiedLogicalLines(rawDiff: string): DiffLogicalLine[];
20
+ //# sourceMappingURL=diff-cursor-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff-cursor-utils.d.ts","sourceRoot":"","sources":["../src/diff-cursor-utils.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,aAAa,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAA;IAC5D,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAoBD;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,EAAE,CA2E3E"}
@@ -0,0 +1,105 @@
1
+ // Utilities for keyboard-driven cursor/selection in the diff view.
2
+ // Maps between logical diff lines (as rendered by DiffRenderable in unified view)
3
+ // and user selections, so we can copy selected new content in markdown format.
4
+ const HUNK_HEADER_RE = /^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@(.*)/;
5
+ function isFileMetaLine(line) {
6
+ return (line.startsWith("diff --git") ||
7
+ line.startsWith("index ") ||
8
+ line.startsWith("--- ") ||
9
+ line.startsWith("+++ ") ||
10
+ line.startsWith("similarity index") ||
11
+ line.startsWith("rename from") ||
12
+ line.startsWith("rename to") ||
13
+ line.startsWith("copy from") ||
14
+ line.startsWith("copy to") ||
15
+ line.startsWith("new file mode") ||
16
+ line.startsWith("deleted file mode"));
17
+ }
18
+ /**
19
+ * Rebuild the logical line order that DiffRenderable uses for its unified view.
20
+ * Each returned item corresponds to one row in the internal CodeRenderable content,
21
+ * so line indices match what DiffRenderable.highlightLines() expects.
22
+ *
23
+ * This mirrors opentuah's DiffRenderable.buildUnifiedView: consecutive removed
24
+ * and added lines are grouped into one change block, with all removed lines
25
+ * emitted before all added lines.
26
+ *
27
+ * Hunk headers are included as rows so cursor alignment stays correct, but they
28
+ * are skipped when extracting code.
29
+ */
30
+ export function buildUnifiedLogicalLines(rawDiff) {
31
+ const lines = [];
32
+ let oldLineNum = 0;
33
+ let newLineNum = 0;
34
+ const rawLines = rawDiff.split("\n");
35
+ let i = 0;
36
+ while (i < rawLines.length) {
37
+ const line = rawLines[i];
38
+ if (line === undefined || line.length === 0) {
39
+ i++;
40
+ continue;
41
+ }
42
+ if (line.startsWith("@@")) {
43
+ const match = HUNK_HEADER_RE.exec(line);
44
+ if (match) {
45
+ oldLineNum = parseInt(match[1], 10);
46
+ newLineNum = parseInt(match[2], 10);
47
+ const context = match[3].trimStart();
48
+ lines.push({ type: "hunk-header", content: context });
49
+ }
50
+ i++;
51
+ continue;
52
+ }
53
+ if (isFileMetaLine(line)) {
54
+ i++;
55
+ continue;
56
+ }
57
+ const firstChar = line[0];
58
+ const content = line.slice(1);
59
+ if (firstChar === " ") {
60
+ lines.push({ type: "context", content, oldLineNum, newLineNum });
61
+ oldLineNum++;
62
+ newLineNum++;
63
+ i++;
64
+ }
65
+ else if (firstChar === "\\") {
66
+ // "" marker — skip
67
+ i++;
68
+ }
69
+ else if (firstChar === "-" || firstChar === "+") {
70
+ // Collect a contiguous change block of removes and adds, then emit all
71
+ // removes followed by all adds (matching DiffRenderable grouping).
72
+ const removes = [];
73
+ const adds = [];
74
+ while (i < rawLines.length) {
75
+ const currentLine = rawLines[i];
76
+ if (currentLine === undefined || currentLine.length === 0)
77
+ break;
78
+ const currentChar = currentLine[0];
79
+ if (currentChar === " " || currentChar === "\\")
80
+ break;
81
+ const currentContent = currentLine.slice(1);
82
+ if (currentChar === "-") {
83
+ removes.push({ content: currentContent, oldLineNum });
84
+ oldLineNum++;
85
+ }
86
+ else if (currentChar === "+") {
87
+ adds.push({ content: currentContent, newLineNum });
88
+ newLineNum++;
89
+ }
90
+ i++;
91
+ }
92
+ for (const remove of removes) {
93
+ lines.push({ type: "remove", content: remove.content, oldLineNum: remove.oldLineNum });
94
+ }
95
+ for (const add of adds) {
96
+ lines.push({ type: "add", content: add.content, newLineNum: add.newLineNum });
97
+ }
98
+ }
99
+ else {
100
+ // Unknown line prefix — skip
101
+ i++;
102
+ }
103
+ }
104
+ return lines;
105
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=diff-cursor-utils.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff-cursor-utils.test.d.ts","sourceRoot":"","sources":["../src/diff-cursor-utils.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { buildUnifiedLogicalLines } from "./diff-cursor-utils.js";
3
+ const sampleDiff = `diff --git a/src/utils.ts b/src/utils.ts
4
+ index abc123..def456 100644
5
+ --- a/src/utils.ts
6
+ +++ b/src/utils.ts
7
+ @@ -1,4 +1,5 @@
8
+ function helper() {
9
+ const x = 1
10
+ - return x
11
+ + // validate
12
+ + return x + 1
13
+ }
14
+ `;
15
+ describe("buildUnifiedLogicalLines", () => {
16
+ it("builds logical lines in unified view order", () => {
17
+ const lines = buildUnifiedLogicalLines(sampleDiff);
18
+ expect(lines.map((l) => ({ type: l.type, content: l.content }))).toEqual([
19
+ { type: "hunk-header", content: "" },
20
+ { type: "context", content: "function helper() {" },
21
+ { type: "context", content: " const x = 1" },
22
+ { type: "remove", content: " return x" },
23
+ { type: "add", content: " // validate" },
24
+ { type: "add", content: " return x + 1" },
25
+ { type: "context", content: "}" },
26
+ ]);
27
+ });
28
+ it("tracks new and old line numbers", () => {
29
+ const lines = buildUnifiedLogicalLines(sampleDiff);
30
+ expect(lines[2].newLineNum).toBe(2);
31
+ expect(lines[3].oldLineNum).toBe(3);
32
+ expect(lines[4].newLineNum).toBe(3);
33
+ expect(lines[5].newLineNum).toBe(4);
34
+ });
35
+ it("ignores file metadata lines", () => {
36
+ const lines = buildUnifiedLogicalLines(sampleDiff);
37
+ expect(lines.some((l) => l.content.startsWith("diff --git"))).toBe(false);
38
+ expect(lines.some((l) => l.content.startsWith("index "))).toBe(false);
39
+ });
40
+ });
@@ -0,0 +1,23 @@
1
+ import type { DiffRenderable } from "@opentuah/core";
2
+ export interface CaptureSelectedDiffTextResult {
3
+ /** The captured text, or null if the renderable could not be read. */
4
+ text: string | null;
5
+ /** The first new line number in the selection, if available. */
6
+ startLineNum?: number;
7
+ }
8
+ /**
9
+ * Read the text currently shown on the selected logical rows of a DiffRenderable.
10
+ *
11
+ * - Unified view: returns the rendered unified diff rows.
12
+ * - Split view: returns the new (right-hand) side rows.
13
+ *
14
+ * The helper reaches into DiffRenderable private internals; callers should treat
15
+ * it as tightly coupled to the current opentuah implementation.
16
+ */
17
+ export declare function captureSelectedDiffText(diffRenderable: DiffRenderable | null | undefined, selection: {
18
+ start: number;
19
+ end: number;
20
+ }): CaptureSelectedDiffTextResult;
21
+ /** Return the number of logical rows rendered by a DiffRenderable. */
22
+ export declare function getDiffRenderableLineCount(diffRenderable: DiffRenderable | null | undefined): number;
23
+ //# sourceMappingURL=diff-surface-copy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff-surface-copy.d.ts","sourceRoot":"","sources":["../src/diff-surface-copy.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAEpD,MAAM,WAAW,6BAA6B;IAC5C,sEAAsE;IACtE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAgCD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,cAAc,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,EACjD,SAAS,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACxC,6BAA6B,CAiC/B;AAED,sEAAsE;AACtE,wBAAgB,0BAA0B,CAAC,cAAc,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAOpG"}
@@ -0,0 +1,64 @@
1
+ // Capture selected diff text directly from the rendered DiffRenderable surface.
2
+ // This avoids reconstructing the patch from file.rawDiff and is immune to
3
+ // balanceDelimiters drift because it copies exactly what is on screen.
4
+ function getCodeRenderable(diff, view) {
5
+ const d = diff;
6
+ if (view === "split") {
7
+ return d.rightCodeRenderable;
8
+ }
9
+ return d.leftCodeRenderable;
10
+ }
11
+ function getLineNumbers(diff, view) {
12
+ const d = diff;
13
+ const side = view === "split" ? d.rightSide : d.leftSide;
14
+ return side?._lineNumbers;
15
+ }
16
+ /**
17
+ * Read the text currently shown on the selected logical rows of a DiffRenderable.
18
+ *
19
+ * - Unified view: returns the rendered unified diff rows.
20
+ * - Split view: returns the new (right-hand) side rows.
21
+ *
22
+ * The helper reaches into DiffRenderable private internals; callers should treat
23
+ * it as tightly coupled to the current opentuah implementation.
24
+ */
25
+ export function captureSelectedDiffText(diffRenderable, selection) {
26
+ if (!diffRenderable) {
27
+ return { text: null };
28
+ }
29
+ const view = diffRenderable.view ?? "unified";
30
+ const codeRenderable = getCodeRenderable(diffRenderable, view);
31
+ if (!codeRenderable) {
32
+ return { text: null };
33
+ }
34
+ const allLines = codeRenderable.plainText.split("\n");
35
+ const startLogical = Math.min(selection.start, selection.end);
36
+ const endLogical = Math.max(selection.start, selection.end);
37
+ const selectedLines = [];
38
+ for (let i = startLogical; i <= endLogical; i++) {
39
+ if (i >= 0 && i < allLines.length) {
40
+ selectedLines.push(allLines[i]);
41
+ }
42
+ }
43
+ const lineNumbers = getLineNumbers(diffRenderable, view);
44
+ let startLineNum;
45
+ for (let i = startLogical; i <= endLogical; i++) {
46
+ const num = lineNumbers?.get(i);
47
+ if (num !== undefined) {
48
+ startLineNum = num;
49
+ break;
50
+ }
51
+ }
52
+ return { text: selectedLines.join("\n"), startLineNum };
53
+ }
54
+ /** Return the number of logical rows rendered by a DiffRenderable. */
55
+ export function getDiffRenderableLineCount(diffRenderable) {
56
+ if (!diffRenderable)
57
+ return 0;
58
+ const view = diffRenderable.view ?? "unified";
59
+ const codeRenderable = getCodeRenderable(diffRenderable, view);
60
+ if (!codeRenderable)
61
+ return 0;
62
+ const text = codeRenderable.plainText;
63
+ return text.length === 0 ? 0 : text.split("\n").length;
64
+ }
@@ -0,0 +1,5 @@
1
+ declare global {
2
+ var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;
3
+ }
4
+ export {};
5
+ //# sourceMappingURL=diff-surface-copy.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff-surface-copy.test.d.ts","sourceRoot":"","sources":["../src/diff-surface-copy.test.tsx"],"names":[],"mappings":"AASA,OAAO,CAAC,MAAM,CAAC;IACb,IAAI,wBAAwB,EAAE,OAAO,GAAG,SAAS,CAAA;CAClD"}