diffstalker 0.1.6 → 0.1.7

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 (53) hide show
  1. package/.github/workflows/release.yml +5 -3
  2. package/bun.lock +618 -0
  3. package/dist/App.js +541 -1
  4. package/dist/components/BaseBranchPicker.js +60 -1
  5. package/dist/components/BottomPane.js +101 -1
  6. package/dist/components/CommitPanel.js +58 -1
  7. package/dist/components/CompareListView.js +110 -1
  8. package/dist/components/ExplorerContentView.js +80 -3
  9. package/dist/components/ExplorerView.js +37 -1
  10. package/dist/components/FileList.js +131 -1
  11. package/dist/components/Footer.js +6 -1
  12. package/dist/components/Header.js +107 -1
  13. package/dist/components/HistoryView.js +21 -1
  14. package/dist/components/HotkeysModal.js +108 -1
  15. package/dist/components/Modal.js +19 -1
  16. package/dist/components/ScrollableList.js +125 -1
  17. package/dist/components/ThemePicker.js +42 -1
  18. package/dist/components/TopPane.js +14 -1
  19. package/dist/components/UnifiedDiffView.js +115 -1
  20. package/dist/config.js +83 -2
  21. package/dist/core/GitOperationQueue.js +109 -1
  22. package/dist/core/GitStateManager.js +466 -1
  23. package/dist/git/diff.js +471 -10
  24. package/dist/git/status.js +269 -5
  25. package/dist/hooks/useCommitFlow.js +66 -1
  26. package/dist/hooks/useCompareState.js +123 -1
  27. package/dist/hooks/useExplorerState.js +248 -9
  28. package/dist/hooks/useGit.js +156 -1
  29. package/dist/hooks/useHistoryState.js +62 -1
  30. package/dist/hooks/useKeymap.js +167 -1
  31. package/dist/hooks/useLayout.js +154 -1
  32. package/dist/hooks/useMouse.js +87 -1
  33. package/dist/hooks/useTerminalSize.js +20 -1
  34. package/dist/hooks/useWatcher.js +137 -11
  35. package/dist/index.js +43 -3
  36. package/dist/services/commitService.js +22 -1
  37. package/dist/themes.js +127 -1
  38. package/dist/utils/ansiTruncate.js +108 -0
  39. package/dist/utils/baseBranchCache.js +44 -2
  40. package/dist/utils/commitFormat.js +38 -1
  41. package/dist/utils/diffFilters.js +21 -1
  42. package/dist/utils/diffRowCalculations.js +113 -1
  43. package/dist/utils/displayRows.js +172 -2
  44. package/dist/utils/explorerDisplayRows.js +169 -0
  45. package/dist/utils/fileCategories.js +26 -1
  46. package/dist/utils/formatDate.js +39 -1
  47. package/dist/utils/formatPath.js +58 -1
  48. package/dist/utils/languageDetection.js +180 -0
  49. package/dist/utils/layoutCalculations.js +98 -1
  50. package/dist/utils/lineBreaking.js +88 -5
  51. package/dist/utils/mouseCoordinates.js +165 -1
  52. package/dist/utils/rowCalculations.js +209 -4
  53. package/package.json +7 -10
@@ -1 +1,101 @@
1
- import{jsx as r,jsxs as s}from"react/jsx-runtime";import{useMemo as v}from"react";import{Box as l,Text as i}from"ink";import{UnifiedDiffView as U}from"./UnifiedDiffView.js";import{CommitPanel as X}from"./CommitPanel.js";import{ExplorerContentView as k}from"./ExplorerContentView.js";import{shortenPath as g}from"../utils/formatPath.js";import{buildDiffDisplayRows as H,buildHistoryDisplayRows as q,buildCompareDisplayRows as z,getDisplayRowsLineNumWidth as F,wrapDisplayRows as J}from"../utils/displayRows.js";export function BottomPane({bottomTab:n,currentPane:t,terminalWidth:o,bottomPaneHeight:w,diffScrollOffset:I,currentTheme:N,diff:m,selectedFile:C,stagedCount:B,onCommit:O,onCommitCancel:D,getHeadCommitMessage:G,onCommitInputFocusChange:M,historySelectedCommit:f,historyCommitDiff:y,compareDiff:e,compareLoading:V,compareError:R,compareListSelection:u,compareSelectionDiff:x,wrapMode:p,explorerSelectedFile:h=null,explorerFileScrollOffset:Y=0}){const a=t!=="files"&&t!=="history"&&t!=="compare"&&t!=="explorer",c=v(()=>n==="diff"?H(m):n==="history"?q(f,y):n==="compare"?u?.type==="commit"&&x?H(x):z(e):[],[n,m,f,y,u,x,e]),_=v(()=>{if(!p||c.length===0)return c;const d=F(c),E=o-d-5;return J(c,E,p)},[c,o,p]),j=()=>{if(C&&n==="diff")return r(i,{dimColor:!0,children:g(C.path,o-10)});if(n==="history"&&f)return s(i,{dimColor:!0,children:[f.shortHash," - ",f.message.slice(0,50)]});if(n==="compare"&&u)if(u.type==="commit"){const d=e?.commits[u.index];return s(i,{dimColor:!0,children:[d?.shortHash??""," - ",d?.message.slice(0,40)??""]})}else{const d=e?.files[u.index]?.path??"";return r(i,{dimColor:!0,children:g(d,o-10)})}return n==="explorer"&&h?r(i,{dimColor:!0,children:g(h.path,o-10)}):null},A=()=>{if(n==="commit")return r(X,{isActive:t==="commit",stagedCount:B,onCommit:O,onCancel:D,getHeadMessage:G,onInputFocusChange:M});if(n==="compare"){if(V)return r(i,{dimColor:!0,children:"Loading compare diff..."});if(R)return r(i,{color:"red",children:R});if(!e)return r(i,{dimColor:!0,children:"No base branch found (no origin/main or origin/master)"});if(e.files.length===0)return s(i,{dimColor:!0,children:["No changes compared to ",e.baseBranch]})}return r(U,{rows:_,maxHeight:w-1,scrollOffset:I,theme:N,width:o,wrapMode:p})};return n==="explorer"?s(l,{flexDirection:"column",height:w,width:o,overflowY:"hidden",children:[s(l,{width:o,children:[r(i,{bold:!0,color:a?"cyan":void 0,children:"FILE"}),r(l,{flexGrow:1,justifyContent:"flex-end",children:j()})]}),r(k,{filePath:h?.path??null,content:h?.content??null,maxHeight:w-1,scrollOffset:Y,truncated:h?.truncated})]}):s(l,{flexDirection:"column",height:w,width:o,overflowX:"hidden",overflowY:"hidden",children:[s(l,{width:o,children:[r(i,{bold:!0,color:a?"cyan":void 0,children:n==="commit"?"COMMIT":"DIFF"}),r(l,{flexGrow:1,justifyContent:"flex-end",children:j()})]}),A()]})}
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import { UnifiedDiffView } from './UnifiedDiffView.js';
5
+ import { CommitPanel } from './CommitPanel.js';
6
+ import { ExplorerContentView } from './ExplorerContentView.js';
7
+ import { shortenPath } from '../utils/formatPath.js';
8
+ import { buildDiffDisplayRows, buildHistoryDisplayRows, buildCompareDisplayRows, getDisplayRowsLineNumWidth, wrapDisplayRows, } from '../utils/displayRows.js';
9
+ export function BottomPane({ bottomTab, currentPane, terminalWidth, bottomPaneHeight, diffScrollOffset, currentTheme, diff, selectedFile, stagedCount, onCommit, onCommitCancel, getHeadCommitMessage, onCommitInputFocusChange, historySelectedCommit, historyCommitDiff, compareDiff, compareLoading, compareError, compareListSelection, compareSelectionDiff, wrapMode, explorerSelectedFile = null, explorerFileScrollOffset = 0, showMiddleDots = false, }) {
10
+ const isDiffFocused = currentPane !== 'files' &&
11
+ currentPane !== 'history' &&
12
+ currentPane !== 'compare' &&
13
+ currentPane !== 'explorer';
14
+ // Build display rows based on current tab
15
+ const displayRows = useMemo(() => {
16
+ if (bottomTab === 'diff') {
17
+ return buildDiffDisplayRows(diff);
18
+ }
19
+ if (bottomTab === 'history') {
20
+ return buildHistoryDisplayRows(historySelectedCommit, historyCommitDiff);
21
+ }
22
+ if (bottomTab === 'compare') {
23
+ // If a specific commit is selected, show that commit's diff
24
+ if (compareListSelection?.type === 'commit' && compareSelectionDiff) {
25
+ return buildDiffDisplayRows(compareSelectionDiff);
26
+ }
27
+ // Otherwise show combined compare diff
28
+ return buildCompareDisplayRows(compareDiff);
29
+ }
30
+ return [];
31
+ }, [
32
+ bottomTab,
33
+ diff,
34
+ historySelectedCommit,
35
+ historyCommitDiff,
36
+ compareListSelection,
37
+ compareSelectionDiff,
38
+ compareDiff,
39
+ ]);
40
+ // Wrap display rows if wrap mode is enabled
41
+ const wrappedRows = useMemo(() => {
42
+ if (!wrapMode || displayRows.length === 0)
43
+ return displayRows;
44
+ // Calculate content width: width - paddingX(1) - lineNum - space(1) - symbol(1) - space(1) - paddingX(1)
45
+ const lineNumWidth = getDisplayRowsLineNumWidth(displayRows);
46
+ const contentWidth = terminalWidth - lineNumWidth - 5;
47
+ return wrapDisplayRows(displayRows, contentWidth, wrapMode);
48
+ }, [displayRows, terminalWidth, wrapMode]);
49
+ // Build header right-side content
50
+ const renderHeaderRight = () => {
51
+ if (selectedFile && bottomTab === 'diff') {
52
+ return _jsx(Text, { dimColor: true, children: shortenPath(selectedFile.path, terminalWidth - 10) });
53
+ }
54
+ if (bottomTab === 'history' && historySelectedCommit) {
55
+ return (_jsxs(Text, { dimColor: true, children: [historySelectedCommit.shortHash, " - ", historySelectedCommit.message.slice(0, 50)] }));
56
+ }
57
+ if (bottomTab === 'compare' && compareListSelection) {
58
+ if (compareListSelection.type === 'commit') {
59
+ const commit = compareDiff?.commits[compareListSelection.index];
60
+ return (_jsxs(Text, { dimColor: true, children: [commit?.shortHash ?? '', " - ", commit?.message.slice(0, 40) ?? ''] }));
61
+ }
62
+ else {
63
+ const path = compareDiff?.files[compareListSelection.index]?.path ?? '';
64
+ return _jsx(Text, { dimColor: true, children: shortenPath(path, terminalWidth - 10) });
65
+ }
66
+ }
67
+ if (bottomTab === 'explorer' && explorerSelectedFile) {
68
+ return _jsx(Text, { dimColor: true, children: shortenPath(explorerSelectedFile.path, terminalWidth - 10) });
69
+ }
70
+ return null;
71
+ };
72
+ // Render content based on tab
73
+ const renderContent = () => {
74
+ // Commit tab is special - not a diff view
75
+ if (bottomTab === 'commit') {
76
+ return (_jsx(CommitPanel, { isActive: currentPane === 'commit', stagedCount: stagedCount, onCommit: onCommit, onCancel: onCommitCancel, getHeadMessage: getHeadCommitMessage, onInputFocusChange: onCommitInputFocusChange }));
77
+ }
78
+ // Compare tab loading/error states
79
+ if (bottomTab === 'compare') {
80
+ if (compareLoading) {
81
+ return _jsx(Text, { dimColor: true, children: "Loading compare diff..." });
82
+ }
83
+ if (compareError) {
84
+ return _jsx(Text, { color: "red", children: compareError });
85
+ }
86
+ if (!compareDiff) {
87
+ return _jsx(Text, { dimColor: true, children: "No base branch found (no origin/main or origin/master)" });
88
+ }
89
+ if (compareDiff.files.length === 0) {
90
+ return _jsxs(Text, { dimColor: true, children: ["No changes compared to ", compareDiff.baseBranch] });
91
+ }
92
+ }
93
+ // All diff views use UnifiedDiffView
94
+ return (_jsx(UnifiedDiffView, { rows: wrappedRows, maxHeight: bottomPaneHeight - 1, scrollOffset: diffScrollOffset, theme: currentTheme, width: terminalWidth, wrapMode: wrapMode }));
95
+ };
96
+ // Explorer tab content
97
+ if (bottomTab === 'explorer') {
98
+ return (_jsxs(Box, { flexDirection: "column", height: bottomPaneHeight, width: terminalWidth, overflowY: "hidden", children: [_jsxs(Box, { width: terminalWidth, children: [_jsx(Text, { bold: true, color: isDiffFocused ? 'cyan' : undefined, children: "FILE" }), _jsx(Box, { flexGrow: 1, justifyContent: "flex-end", children: renderHeaderRight() })] }), _jsx(ExplorerContentView, { filePath: explorerSelectedFile?.path ?? null, content: explorerSelectedFile?.content ?? null, maxHeight: bottomPaneHeight - 1, scrollOffset: explorerFileScrollOffset, truncated: explorerSelectedFile?.truncated, wrapMode: wrapMode, width: terminalWidth, showMiddleDots: showMiddleDots })] }));
99
+ }
100
+ return (_jsxs(Box, { flexDirection: "column", height: bottomPaneHeight, width: terminalWidth, overflowX: "hidden", overflowY: "hidden", children: [_jsxs(Box, { width: terminalWidth, children: [_jsx(Text, { bold: true, color: isDiffFocused ? 'cyan' : undefined, children: bottomTab === 'commit' ? 'COMMIT' : 'DIFF' }), _jsx(Box, { flexGrow: 1, justifyContent: "flex-end", children: renderHeaderRight() })] }), renderContent()] }));
101
+ }
@@ -1 +1,58 @@
1
- import{jsx as e,jsxs as n}from"react/jsx-runtime";import{useEffect as T}from"react";import{Box as o,Text as r,useInput as S}from"ink";import j from"ink-text-input";import{useCommitFlow as w}from"../hooks/useCommitFlow.js";export function CommitPanel({isActive:l,stagedCount:c,onCommit:p,onCancel:s,getHeadMessage:x,onInputFocusChange:a}){const{message:t,amend:m,isCommitting:C,error:u,inputFocused:i,setMessage:E,toggleAmend:f,setInputFocused:g,handleSubmit:b}=w({stagedCount:c,onCommit:p,onSuccess:s,getHeadMessage:x});return T(()=>{a?.(i)},[i,a]),S((d,h)=>{if(l){if(h.escape){i?g(!1):s();return}if(!i){if(d==="i"||h.return){g(!0);return}if(d==="a"){f();return}return}if(d==="a"&&!t){f();return}}},{isActive:l}),l?n(o,{flexDirection:"column",paddingX:1,children:[n(o,{marginBottom:1,children:[e(r,{bold:!0,children:"Commit Message"}),m&&e(r,{color:"yellow",children:" (amending)"})]}),e(o,{borderStyle:"round",borderColor:i?"cyan":void 0,paddingX:1,children:i?e(j,{value:t,onChange:E,onSubmit:b,placeholder:"Enter commit message..."}):e(r,{dimColor:!t,children:t||"Press i or Enter to edit..."})}),n(o,{marginTop:1,gap:2,children:[n(r,{color:m?"green":"gray",children:["[",m?"x":" ","] Amend"]}),e(r,{dimColor:!0,children:"(a)"})]}),u&&e(o,{marginTop:1,children:e(r,{color:"red",children:u})}),C&&e(o,{marginTop:1,children:e(r,{color:"yellow",children:"Committing..."})}),e(o,{marginTop:1,children:n(r,{dimColor:!0,children:["Staged: ",c," file(s) |"," ",i?"Enter: commit | Esc: unfocus":"i/Enter: edit | Esc: cancel | 1/3: switch tab"]})})]}):e(o,{paddingX:1,children:e(r,{dimColor:!0,children:"Press '2' or 'c' to open commit panel"})})}
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import TextInput from 'ink-text-input';
5
+ import { useCommitFlow } from '../hooks/useCommitFlow.js';
6
+ export function CommitPanel({ isActive, stagedCount, onCommit, onCancel, getHeadMessage, onInputFocusChange, }) {
7
+ const { message, amend, isCommitting, error, inputFocused, setMessage, toggleAmend, setInputFocused, handleSubmit, } = useCommitFlow({
8
+ stagedCount,
9
+ onCommit,
10
+ onSuccess: onCancel,
11
+ getHeadMessage,
12
+ });
13
+ // Notify parent of focus state changes
14
+ useEffect(() => {
15
+ onInputFocusChange?.(inputFocused);
16
+ }, [inputFocused, onInputFocusChange]);
17
+ // Keyboard handling
18
+ useInput((input, key) => {
19
+ if (!isActive)
20
+ return;
21
+ // When input is focused, Escape unfocuses it (but stays on commit tab)
22
+ // When input is unfocused, Escape cancels and goes back to diff
23
+ if (key.escape) {
24
+ if (inputFocused) {
25
+ setInputFocused(false);
26
+ }
27
+ else {
28
+ onCancel();
29
+ }
30
+ return;
31
+ }
32
+ // When input is unfocused, allow refocusing with 'i' or Enter
33
+ if (!inputFocused) {
34
+ if (input === 'i' || key.return) {
35
+ setInputFocused(true);
36
+ return;
37
+ }
38
+ // Toggle amend with 'a' when unfocused
39
+ if (input === 'a') {
40
+ toggleAmend();
41
+ return;
42
+ }
43
+ return; // Don't handle other keys - let them bubble up to useKeymap
44
+ }
45
+ // When input is focused, only handle special keys
46
+ // Toggle amend with 'a' when message is empty
47
+ if (input === 'a' && !message) {
48
+ toggleAmend();
49
+ return;
50
+ }
51
+ }, { isActive });
52
+ if (!isActive) {
53
+ return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "Press '2' or 'c' to open commit panel" }) }));
54
+ }
55
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Commit Message" }), amend && _jsx(Text, { color: "yellow", children: " (amending)" })] }), _jsx(Box, { borderStyle: "round", borderColor: inputFocused ? 'cyan' : undefined, paddingX: 1, children: inputFocused ? (_jsx(TextInput, { value: message, onChange: setMessage, onSubmit: handleSubmit, placeholder: "Enter commit message..." })) : (_jsx(Text, { dimColor: !message, children: message || 'Press i or Enter to edit...' })) }), _jsxs(Box, { marginTop: 1, gap: 2, children: [_jsxs(Text, { color: amend ? 'green' : 'gray', children: ["[", amend ? 'x' : ' ', "] Amend"] }), _jsx(Text, { dimColor: true, children: "(a)" })] }), error && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", children: error }) })), isCommitting && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: "Committing..." }) })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Staged: ", stagedCount, " file(s) |", ' ', inputFocused
56
+ ? 'Enter: commit | Esc: unfocus'
57
+ : 'i/Enter: edit | Esc: cancel | 1/3: switch tab'] }) })] }));
58
+ }
@@ -1 +1,110 @@
1
- import{jsx as o,jsxs as l,Fragment as E}from"react/jsx-runtime";import{useMemo as R}from"react";import{Box as f,Text as n}from"ink";import{shortenPath as F}from"../utils/formatPath.js";import{formatDate as D}from"../utils/formatDate.js";import{formatCommitDisplay as I}from"../utils/commitFormat.js";export{getCompareItemIndexFromRow}from"../utils/rowCalculations.js";function L({commit:e,isSelected:r,isActive:i,width:s}){const d=D(e.date),m=13+d.length+2,c=s-m,{displayMessage:g,displayRefs:u}=I(e.message,e.refs,c);return l(f,{children:[o(n,{children:" "}),o(n,{color:"yellow",children:e.shortHash}),o(n,{children:" "}),o(n,{color:r&&i?"cyan":void 0,bold:r&&i,inverse:r&&i,children:g}),o(n,{children:" "}),l(n,{dimColor:!0,children:["(",d,")"]}),u&&l(E,{children:[o(n,{children:" "}),o(n,{color:"green",children:u})]})]})}function T({file:e,isSelected:r,isActive:i,maxPathLength:s}){const d={added:"green",modified:"yellow",deleted:"red",renamed:"blue"},m={added:"A",modified:"M",deleted:"D",renamed:"R"},c=e.isUncommitted??!1,g=5+String(e.additions).length+String(e.deletions).length,u=c?14:0,x=s-g-u;return l(f,{children:[o(n,{children:" "}),c&&o(n,{color:"magenta",bold:!0,children:"*"}),o(n,{color:c?"magenta":d[e.status],bold:!0,children:m[e.status]}),l(n,{bold:r&&i,color:r&&i?"cyan":c?"magenta":void 0,inverse:r&&i,children:[" ",F(e.path,x)]}),o(n,{dimColor:!0,children:" ("}),l(n,{color:"green",children:["+",e.additions]}),o(n,{dimColor:!0,children:" "}),l(n,{color:"red",children:["-",e.deletions]}),o(n,{dimColor:!0,children:")"}),c&&l(n,{color:"magenta",dimColor:!0,children:[" ","[uncommitted]"]})]})}export function CompareListView({commits:e,files:r,selectedItem:i,scrollOffset:s,maxHeight:d,isActive:m,width:c}){const y=R(()=>{const t=[];return e.length>0&&(t.push({type:"section-header",sectionType:"commits"}),e.forEach((p,a)=>{t.push({type:"commit",commitIndex:a,commit:p})})),r.length>0&&(e.length>0&&t.push({type:"spacer"}),t.push({type:"section-header",sectionType:"files"}),r.forEach((p,a)=>{t.push({type:"file",fileIndex:a,file:p})})),t},[e,r,!0,!0]).slice(s,s+d);return e.length===0&&r.length===0?o(f,{flexDirection:"column",children:o(n,{dimColor:!0,children:"No changes compared to base branch"})}):o(f,{flexDirection:"column",children:y.map((t,p)=>{const a=`row-${s+p}`;if(t.type==="section-header"){const h=t.sectionType==="commits",C=!0,b=h?e.length:r.length;return l(f,{children:[l(n,{bold:!0,color:"cyan",children:[C?"\u25BC":"\u25B6"," ",h?"Commits":"Files"]}),l(n,{dimColor:!0,children:[" (",b,")"]})]},a)}if(t.type==="spacer")return o(n,{children:" "},a);if(t.type==="commit"&&t.commit!==void 0&&t.commitIndex!==void 0){const h=i?.type==="commit"&&i.index===t.commitIndex;return o(L,{commit:t.commit,isSelected:h,isActive:m,width:c},a)}if(t.type==="file"&&t.file!==void 0&&t.fileIndex!==void 0){const h=i?.type==="file"&&i.index===t.fileIndex;return o(T,{file:t.file,isSelected:h,isActive:m,maxPathLength:c-5},a)}return null})})}export function getCompareListTotalRows(e,r,i=!0,s=!0){let d=0;return e.length>0&&(d+=1,i&&(d+=e.length)),r.length>0&&(e.length>0&&(d+=1),d+=1,s&&(d+=r.length)),d}
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import { shortenPath } from '../utils/formatPath.js';
5
+ import { formatDate } from '../utils/formatDate.js';
6
+ import { formatCommitDisplay } from '../utils/commitFormat.js';
7
+ // Re-export from utils for backwards compatibility
8
+ export { getCompareItemIndexFromRow } from '../utils/rowCalculations.js';
9
+ function CommitRow({ commit, isSelected, isActive, width, }) {
10
+ const dateStr = formatDate(commit.date);
11
+ // Fixed parts: indent(2) + hash(7) + spaces(4) + date + parens(2)
12
+ const baseWidth = 2 + 7 + 4 + dateStr.length + 2;
13
+ const remainingWidth = width - baseWidth;
14
+ const { displayMessage, displayRefs } = formatCommitDisplay(commit.message, commit.refs, remainingWidth);
15
+ return (_jsxs(Box, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: "yellow", children: commit.shortHash }), _jsx(Text, { children: " " }), _jsx(Text, { color: isSelected && isActive ? 'cyan' : undefined, bold: isSelected && isActive, inverse: isSelected && isActive, children: displayMessage }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: ["(", dateStr, ")"] }), displayRefs && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: "green", children: displayRefs })] }))] }));
16
+ }
17
+ function FileRow({ file, isSelected, isActive, maxPathLength, }) {
18
+ const statusColors = {
19
+ added: 'green',
20
+ modified: 'yellow',
21
+ deleted: 'red',
22
+ renamed: 'blue',
23
+ };
24
+ const statusChars = {
25
+ added: 'A',
26
+ modified: 'M',
27
+ deleted: 'D',
28
+ renamed: 'R',
29
+ };
30
+ const isUncommitted = file.isUncommitted ?? false;
31
+ // Account for stats: " (+123 -456)" and possible "[uncommitted]"
32
+ const statsLength = 5 + String(file.additions).length + String(file.deletions).length;
33
+ const uncommittedLength = isUncommitted ? 14 : 0;
34
+ const availableForPath = maxPathLength - statsLength - uncommittedLength;
35
+ return (_jsxs(Box, { children: [_jsx(Text, { children: " " }), isUncommitted && (_jsx(Text, { color: "magenta", bold: true, children: "*" })), _jsx(Text, { color: isUncommitted ? 'magenta' : statusColors[file.status], bold: true, children: statusChars[file.status] }), _jsxs(Text, { bold: isSelected && isActive, color: isSelected && isActive ? 'cyan' : isUncommitted ? 'magenta' : undefined, inverse: isSelected && isActive, children: [' ', shortenPath(file.path, availableForPath)] }), _jsx(Text, { dimColor: true, children: " (" }), _jsxs(Text, { color: "green", children: ["+", file.additions] }), _jsx(Text, { dimColor: true, children: " " }), _jsxs(Text, { color: "red", children: ["-", file.deletions] }), _jsx(Text, { dimColor: true, children: ")" }), isUncommitted && (_jsxs(Text, { color: "magenta", dimColor: true, children: [' ', "[uncommitted]"] }))] }));
36
+ }
37
+ export function CompareListView({ commits, files, selectedItem, scrollOffset, maxHeight, isActive, width, }) {
38
+ // Note: expand/collapse functionality is prepared but not exposed yet
39
+ const commitsExpanded = true;
40
+ const filesExpanded = true;
41
+ // Build flat list of rows
42
+ const rows = useMemo(() => {
43
+ const result = [];
44
+ // Commits section
45
+ if (commits.length > 0) {
46
+ result.push({ type: 'section-header', sectionType: 'commits' });
47
+ if (commitsExpanded) {
48
+ commits.forEach((commit, i) => {
49
+ result.push({ type: 'commit', commitIndex: i, commit });
50
+ });
51
+ }
52
+ }
53
+ // Files section
54
+ if (files.length > 0) {
55
+ if (commits.length > 0) {
56
+ result.push({ type: 'spacer' });
57
+ }
58
+ result.push({ type: 'section-header', sectionType: 'files' });
59
+ if (filesExpanded) {
60
+ files.forEach((file, i) => {
61
+ result.push({ type: 'file', fileIndex: i, file });
62
+ });
63
+ }
64
+ }
65
+ return result;
66
+ }, [commits, files, commitsExpanded, filesExpanded]);
67
+ const visibleRows = rows.slice(scrollOffset, scrollOffset + maxHeight);
68
+ if (commits.length === 0 && files.length === 0) {
69
+ return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "No changes compared to base branch" }) }));
70
+ }
71
+ return (_jsx(Box, { flexDirection: "column", children: visibleRows.map((row, i) => {
72
+ const key = `row-${scrollOffset + i}`;
73
+ if (row.type === 'section-header') {
74
+ const isCommits = row.sectionType === 'commits';
75
+ const expanded = isCommits ? commitsExpanded : filesExpanded;
76
+ const count = isCommits ? commits.length : files.length;
77
+ const label = isCommits ? 'Commits' : 'Files';
78
+ return (_jsxs(Box, { children: [_jsxs(Text, { bold: true, color: "cyan", children: [expanded ? '▼' : '▶', " ", label] }), _jsxs(Text, { dimColor: true, children: [" (", count, ")"] })] }, key));
79
+ }
80
+ if (row.type === 'spacer') {
81
+ return _jsx(Text, { children: " " }, key);
82
+ }
83
+ if (row.type === 'commit' && row.commit !== undefined && row.commitIndex !== undefined) {
84
+ const isSelected = selectedItem?.type === 'commit' && selectedItem.index === row.commitIndex;
85
+ return (_jsx(CommitRow, { commit: row.commit, isSelected: isSelected, isActive: isActive, width: width }, key));
86
+ }
87
+ if (row.type === 'file' && row.file !== undefined && row.fileIndex !== undefined) {
88
+ const isSelected = selectedItem?.type === 'file' && selectedItem.index === row.fileIndex;
89
+ return (_jsx(FileRow, { file: row.file, isSelected: isSelected, isActive: isActive, maxPathLength: width - 5 }, key));
90
+ }
91
+ return null;
92
+ }) }));
93
+ }
94
+ // Helper to get total row count for scrolling
95
+ export function getCompareListTotalRows(commits, files, commitsExpanded = true, filesExpanded = true) {
96
+ let count = 0;
97
+ if (commits.length > 0) {
98
+ count += 1; // header
99
+ if (commitsExpanded)
100
+ count += commits.length;
101
+ }
102
+ if (files.length > 0) {
103
+ if (commits.length > 0)
104
+ count += 1; // spacer
105
+ count += 1; // header
106
+ if (filesExpanded)
107
+ count += files.length;
108
+ }
109
+ return count;
110
+ }
@@ -1,3 +1,80 @@
1
- import{jsx as e,jsxs as m}from"react/jsx-runtime";import{useMemo as u}from"react";import{Box as t,Text as n}from"ink";import{ScrollableList as f}from"./ScrollableList.js";export function ExplorerContentView({filePath:i,content:l,maxHeight:c,scrollOffset:s,truncated:h=!1}){const o=u(()=>l?l.split(`
2
- `).map((r,d)=>({lineNum:d+1,content:r})):[],[l]),a=u(()=>{const r=o.length;return Math.max(3,String(r).length)},[o.length]);return i?l?o.length===0?e(t,{paddingX:1,children:e(n,{dimColor:!0,children:"(empty file)"})}):m(t,{flexDirection:"column",paddingX:1,children:[e(f,{items:o,maxHeight:c,scrollOffset:s,getKey:r=>`${r.lineNum}`,renderItem:r=>{const d=String(r.lineNum).padStart(a," ");return m(t,{children:[m(n,{dimColor:!0,children:[d," "]}),e(n,{children:r.content||" "})]})}}),h&&e(t,{children:e(n,{color:"yellow",dimColor:!0,children:"(file truncated)"})})]}):e(t,{paddingX:1,children:e(n,{dimColor:!0,children:"Loading..."})}):e(t,{paddingX:1,children:e(n,{dimColor:!0,children:"Select a file to view its contents"})})}export function getExplorerContentTotalRows(i){return i?i.split(`
3
- `).length:0}
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import { ScrollableList } from './ScrollableList.js';
5
+ import { buildExplorerContentRows, wrapExplorerContentRows, getExplorerContentRowCount, getExplorerContentLineNumWidth, applyMiddleDots, } from '../utils/explorerDisplayRows.js';
6
+ import { truncateAnsi } from '../utils/ansiTruncate.js';
7
+ export function ExplorerContentView({ filePath, content, maxHeight, scrollOffset, truncated = false, wrapMode = false, width, showMiddleDots = false, }) {
8
+ // Build base rows with syntax highlighting
9
+ const baseRows = useMemo(() => buildExplorerContentRows(content, filePath, truncated), [content, filePath, truncated]);
10
+ // Calculate line number width
11
+ const lineNumWidth = useMemo(() => getExplorerContentLineNumWidth(baseRows), [baseRows]);
12
+ // Calculate content width for wrapping
13
+ // Layout: paddingX(1) + lineNum + space(1) + content + paddingX(1)
14
+ const contentWidth = width - lineNumWidth - 3;
15
+ // Apply wrapping if enabled
16
+ const displayRows = useMemo(() => wrapExplorerContentRows(baseRows, contentWidth, wrapMode), [baseRows, contentWidth, wrapMode]);
17
+ if (!filePath) {
18
+ return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "Select a file to view its contents" }) }));
19
+ }
20
+ if (!content) {
21
+ return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "Loading..." }) }));
22
+ }
23
+ if (displayRows.length === 0) {
24
+ return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "(empty file)" }) }));
25
+ }
26
+ return (_jsx(Box, { flexDirection: "column", paddingX: 1, children: _jsx(ScrollableList, { items: displayRows, maxHeight: maxHeight, scrollOffset: scrollOffset, getKey: (row, index) => `${index}`, renderItem: (row) => {
27
+ if (row.type === 'truncation') {
28
+ return (_jsx(Box, { children: _jsx(Text, { color: "yellow", dimColor: true, children: row.content }) }));
29
+ }
30
+ // Code row
31
+ const isContinuation = row.isContinuation ?? false;
32
+ // Line number display
33
+ let lineNumDisplay;
34
+ if (isContinuation) {
35
+ // Show continuation marker instead of line number
36
+ lineNumDisplay = '>>'.padStart(lineNumWidth, ' ');
37
+ }
38
+ else {
39
+ lineNumDisplay = String(row.lineNum).padStart(lineNumWidth, ' ');
40
+ }
41
+ // Determine what content to display
42
+ const rawContent = row.content;
43
+ const shouldTruncate = !wrapMode && rawContent.length > contentWidth;
44
+ // Use highlighted content if available and not a continuation or middle-dots mode
45
+ const canUseHighlighting = row.highlighted && !isContinuation && !showMiddleDots;
46
+ let displayContent;
47
+ if (canUseHighlighting && row.highlighted) {
48
+ // Use ANSI-aware truncation to preserve syntax highlighting
49
+ displayContent = shouldTruncate
50
+ ? truncateAnsi(row.highlighted, contentWidth)
51
+ : row.highlighted;
52
+ }
53
+ else {
54
+ // Plain content path
55
+ let content = rawContent;
56
+ // Apply middle-dots to raw content
57
+ if (showMiddleDots && !isContinuation) {
58
+ content = applyMiddleDots(content, true);
59
+ }
60
+ // Simple truncation for plain content
61
+ if (shouldTruncate) {
62
+ content = content.slice(0, Math.max(0, contentWidth - 1)) + '…';
63
+ }
64
+ displayContent = content;
65
+ }
66
+ return (_jsxs(Box, { children: [_jsxs(Text, { dimColor: !isContinuation, color: isContinuation ? 'cyan' : undefined, children: [lineNumDisplay, ' '] }), _jsx(Text, { dimColor: showMiddleDots && !canUseHighlighting, children: displayContent || ' ' })] }));
67
+ } }) }));
68
+ }
69
+ /**
70
+ * Get total rows for scroll calculations.
71
+ * Accounts for wrap mode when calculating.
72
+ */
73
+ export function getExplorerContentTotalRows(content, filePath, truncated, width, wrapMode) {
74
+ if (!content)
75
+ return 0;
76
+ const rows = buildExplorerContentRows(content, filePath, truncated);
77
+ const lineNumWidth = getExplorerContentLineNumWidth(rows);
78
+ const contentWidth = width - lineNumWidth - 3;
79
+ return getExplorerContentRowCount(rows, contentWidth, wrapMode);
80
+ }
@@ -1 +1,37 @@
1
- import{jsxs as y,jsx as e}from"react/jsx-runtime";import{Box as t,Text as n}from"ink";import{ScrollableList as g}from"./ScrollableList.js";export function ExplorerView({currentPath:o,items:i,selectedIndex:a,scrollOffset:s,maxHeight:u,isActive:m,width:f,isLoading:h=!1,error:c=null}){if(c)return e(t,{flexDirection:"column",children:y(n,{color:"red",children:["Error: ",c]})});if(h)return e(t,{flexDirection:"column",children:e(n,{dimColor:!0,children:"Loading..."})});if(i.length===0)return e(t,{flexDirection:"column",children:e(n,{dimColor:!0,children:"(empty directory)"})});const x=Math.min(Math.max(...i.map(r=>r.name.length+(r.isDirectory?1:0))),f-10);return e(g,{items:i,maxHeight:u,scrollOffset:s,getKey:r=>r.path||r.name,renderItem:(r,p)=>{const l=p===a&&m,d=(r.isDirectory?`${r.name}/`:r.name).padEnd(x+1);return e(t,{children:e(n,{color:l?"cyan":void 0,bold:l,inverse:l,children:r.isDirectory?e(n,{color:l?"cyan":"blue",children:d}):e(n,{color:l?"cyan":void 0,children:d})})})}})}export function buildBreadcrumbs(o){return o?o.split("/").filter(Boolean):[]}export function getExplorerTotalRows(o){return o.length}
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { ScrollableList } from './ScrollableList.js';
4
+ export function ExplorerView({ currentPath: _currentPath, items, selectedIndex, scrollOffset, maxHeight, isActive, width, isLoading = false, error = null, }) {
5
+ if (error) {
6
+ return (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) }));
7
+ }
8
+ if (isLoading) {
9
+ return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "Loading..." }) }));
10
+ }
11
+ if (items.length === 0) {
12
+ return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "(empty directory)" }) }));
13
+ }
14
+ // Calculate max name width for alignment
15
+ const maxNameWidth = Math.min(Math.max(...items.map((item) => item.name.length + (item.isDirectory ? 1 : 0))), width - 10);
16
+ return (_jsx(ScrollableList, { items: items, maxHeight: maxHeight, scrollOffset: scrollOffset, getKey: (item) => item.path || item.name, renderItem: (item, actualIndex) => {
17
+ const isSelected = actualIndex === selectedIndex && isActive;
18
+ const displayName = item.isDirectory ? `${item.name}/` : item.name;
19
+ const paddedName = displayName.padEnd(maxNameWidth + 1);
20
+ return (_jsx(Box, { children: _jsx(Text, { color: isSelected ? 'cyan' : undefined, bold: isSelected, inverse: isSelected, children: item.isDirectory ? (_jsx(Text, { color: isSelected ? 'cyan' : 'blue', children: paddedName })) : (_jsx(Text, { color: isSelected ? 'cyan' : undefined, children: paddedName })) }) }));
21
+ } }));
22
+ }
23
+ /**
24
+ * Build breadcrumb segments from a path.
25
+ * Returns segments like ["src", "components"] for "src/components"
26
+ */
27
+ export function buildBreadcrumbs(currentPath) {
28
+ if (!currentPath)
29
+ return [];
30
+ return currentPath.split('/').filter(Boolean);
31
+ }
32
+ /**
33
+ * Get total rows in explorer for scroll calculations.
34
+ */
35
+ export function getExplorerTotalRows(items) {
36
+ return items.length;
37
+ }
@@ -1 +1,131 @@
1
- import{jsx as d,jsxs as l}from"react/jsx-runtime";import{Box as x,Text as t}from"ink";import{shortenPath as C}from"../utils/formatPath.js";import{categorizeFiles as F}from"../utils/fileCategories.js";function P(e){switch(e){case"modified":return"M";case"added":return"A";case"deleted":return"D";case"untracked":return"?";case"renamed":return"R";case"copied":return"C";default:return" "}}function b(e){switch(e){case"modified":return"yellow";case"added":return"green";case"deleted":return"red";case"untracked":return"gray";case"renamed":return"blue";case"copied":return"cyan";default:return"white"}}function k(e,s){if(e===void 0&&s===void 0)return null;const i=e??0,o=s??0;if(i===0&&o===0)return null;const c=[];return i>0&&c.push(`+${i}`),o>0&&c.push(`-${o}`),c.join(" ")}function j({file:e,isSelected:s,isFocused:i,maxPathLength:o}){const c=P(e.status),f=b(e.status),g=e.staged?"[-]":"[+]",h=e.staged?"red":"green",u=k(e.insertions,e.deletions),a=s&&i,r=u?u.length+1:0,p=o-r,m=C(e.path,p);return l(x,{children:[d(t,{color:a?"cyan":void 0,bold:a,children:a?"\u25B8 ":" "}),l(t,{color:h,children:[g," "]}),l(t,{color:f,children:[c," "]}),d(t,{color:a?"cyan":void 0,inverse:a,children:m}),e.originalPath&&l(t,{dimColor:!0,children:[" \u2190 ",C(e.originalPath,30)]}),u&&l(t,{children:[d(t,{dimColor:!0,children:" "}),e.insertions!==void 0&&e.insertions>0&&l(t,{color:"green",children:["+",e.insertions]}),e.insertions!==void 0&&e.insertions>0&&e.deletions!==void 0&&e.deletions>0&&d(t,{dimColor:!0,children:" "}),e.deletions!==void 0&&e.deletions>0&&l(t,{color:"red",children:["-",e.deletions]})]})]})}export function FileList({files:e,selectedIndex:s,isFocused:i,scrollOffset:o=0,maxHeight:c,width:f=80}){const g=f-10,{modified:h,untracked:u,staged:a}=F(e);if(e.length===0)return d(x,{flexDirection:"column",children:d(t,{dimColor:!0,children:" No changes"})});const r=[];let p=0;h.length>0&&(r.push({type:"header",content:"Modified:",headerColor:"yellow"}),h.forEach(n=>{r.push({type:"file",file:n,fileIndex:p++})})),u.length>0&&(h.length>0&&r.push({type:"spacer"}),r.push({type:"header",content:"Untracked:",headerColor:"gray"}),u.forEach(n=>{r.push({type:"file",file:n,fileIndex:p++})})),a.length>0&&((h.length>0||u.length>0)&&r.push({type:"spacer"}),r.push({type:"header",content:"Staged:",headerColor:"green"}),a.forEach(n=>{r.push({type:"file",file:n,fileIndex:p++})}));const m=c?r.slice(o,o+c):r.slice(o);return d(x,{flexDirection:"column",children:m.map((n,I)=>{const y=`row-${o+I}`;return n.type==="header"?d(t,{bold:!0,color:n.headerColor,children:n.content},y):n.type==="spacer"?d(t,{children:" "},y):n.type==="file"&&n.file!==void 0&&n.fileIndex!==void 0?d(j,{file:n.file,isSelected:n.fileIndex===s,isFocused:i,maxPathLength:g},y):null})})}export function getFileAtIndex(e,s){const{ordered:i}=F(e);return i[s]??null}export function getTotalFileCount(e){return e.length}
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { shortenPath } from '../utils/formatPath.js';
4
+ import { categorizeFiles } from '../utils/fileCategories.js';
5
+ function getStatusChar(status) {
6
+ switch (status) {
7
+ case 'modified':
8
+ return 'M';
9
+ case 'added':
10
+ return 'A';
11
+ case 'deleted':
12
+ return 'D';
13
+ case 'untracked':
14
+ return '?';
15
+ case 'renamed':
16
+ return 'R';
17
+ case 'copied':
18
+ return 'C';
19
+ default:
20
+ return ' ';
21
+ }
22
+ }
23
+ function getStatusColor(status) {
24
+ switch (status) {
25
+ case 'modified':
26
+ return 'yellow';
27
+ case 'added':
28
+ return 'green';
29
+ case 'deleted':
30
+ return 'red';
31
+ case 'untracked':
32
+ return 'gray';
33
+ case 'renamed':
34
+ return 'blue';
35
+ case 'copied':
36
+ return 'cyan';
37
+ default:
38
+ return 'white';
39
+ }
40
+ }
41
+ function formatStats(insertions, deletions) {
42
+ if (insertions === undefined && deletions === undefined)
43
+ return null;
44
+ const add = insertions ?? 0;
45
+ const del = deletions ?? 0;
46
+ if (add === 0 && del === 0)
47
+ return null;
48
+ const parts = [];
49
+ if (add > 0)
50
+ parts.push(`+${add}`);
51
+ if (del > 0)
52
+ parts.push(`-${del}`);
53
+ return parts.join(' ');
54
+ }
55
+ function FileRow({ file, isSelected, isFocused, maxPathLength }) {
56
+ const statusChar = getStatusChar(file.status);
57
+ const statusColor = getStatusColor(file.status);
58
+ const actionButton = file.staged ? '[-]' : '[+]';
59
+ const buttonColor = file.staged ? 'red' : 'green';
60
+ const stats = formatStats(file.insertions, file.deletions);
61
+ const isHighlighted = isSelected && isFocused;
62
+ // Calculate available space for path (account for stats if present)
63
+ const statsLength = stats ? stats.length + 1 : 0;
64
+ const availableForPath = maxPathLength - statsLength;
65
+ const displayPath = shortenPath(file.path, availableForPath);
66
+ return (_jsxs(Box, { children: [_jsx(Text, { color: isHighlighted ? 'cyan' : undefined, bold: isHighlighted, children: isHighlighted ? '▸ ' : ' ' }), _jsxs(Text, { color: buttonColor, children: [actionButton, " "] }), _jsxs(Text, { color: statusColor, children: [statusChar, " "] }), _jsx(Text, { color: isHighlighted ? 'cyan' : undefined, inverse: isHighlighted, children: displayPath }), file.originalPath && _jsxs(Text, { dimColor: true, children: [" \u2190 ", shortenPath(file.originalPath, 30)] }), stats && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: " " }), file.insertions !== undefined && file.insertions > 0 && (_jsxs(Text, { color: "green", children: ["+", file.insertions] })), file.insertions !== undefined &&
67
+ file.insertions > 0 &&
68
+ file.deletions !== undefined &&
69
+ file.deletions > 0 && _jsx(Text, { dimColor: true, children: " " }), file.deletions !== undefined && file.deletions > 0 && (_jsxs(Text, { color: "red", children: ["-", file.deletions] }))] }))] }));
70
+ }
71
+ export function FileList({ files, selectedIndex, isFocused, scrollOffset = 0, maxHeight, width = 80, }) {
72
+ // Calculate max path length: width minus prefix chars (▸/space + [+]/[-] + status + spaces = ~10)
73
+ const maxPathLength = width - 10;
74
+ // Split files into 3 categories: Modified, Untracked, Staged
75
+ const { modified: modifiedFiles, untracked: untrackedFiles, staged: stagedFiles, } = categorizeFiles(files);
76
+ if (files.length === 0) {
77
+ return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { dimColor: true, children: " No changes" }) }));
78
+ }
79
+ // Build a flat list of all rows
80
+ // Order: Modified → Untracked → Staged
81
+ const rows = [];
82
+ let currentFileIndex = 0;
83
+ if (modifiedFiles.length > 0) {
84
+ rows.push({ type: 'header', content: 'Modified:', headerColor: 'yellow' });
85
+ modifiedFiles.forEach((file) => {
86
+ rows.push({ type: 'file', file, fileIndex: currentFileIndex++ });
87
+ });
88
+ }
89
+ if (untrackedFiles.length > 0) {
90
+ if (modifiedFiles.length > 0) {
91
+ rows.push({ type: 'spacer' });
92
+ }
93
+ rows.push({ type: 'header', content: 'Untracked:', headerColor: 'gray' });
94
+ untrackedFiles.forEach((file) => {
95
+ rows.push({ type: 'file', file, fileIndex: currentFileIndex++ });
96
+ });
97
+ }
98
+ if (stagedFiles.length > 0) {
99
+ if (modifiedFiles.length > 0 || untrackedFiles.length > 0) {
100
+ rows.push({ type: 'spacer' });
101
+ }
102
+ rows.push({ type: 'header', content: 'Staged:', headerColor: 'green' });
103
+ stagedFiles.forEach((file) => {
104
+ rows.push({ type: 'file', file, fileIndex: currentFileIndex++ });
105
+ });
106
+ }
107
+ // Apply scroll offset and max height
108
+ const visibleRows = maxHeight
109
+ ? rows.slice(scrollOffset, scrollOffset + maxHeight)
110
+ : rows.slice(scrollOffset);
111
+ return (_jsx(Box, { flexDirection: "column", children: visibleRows.map((row, i) => {
112
+ const key = `row-${scrollOffset + i}`;
113
+ if (row.type === 'header') {
114
+ return (_jsx(Text, { bold: true, color: row.headerColor, children: row.content }, key));
115
+ }
116
+ if (row.type === 'spacer') {
117
+ return _jsx(Text, { children: " " }, key);
118
+ }
119
+ if (row.type === 'file' && row.file !== undefined && row.fileIndex !== undefined) {
120
+ return (_jsx(FileRow, { file: row.file, isSelected: row.fileIndex === selectedIndex, isFocused: isFocused, maxPathLength: maxPathLength }, key));
121
+ }
122
+ return null;
123
+ }) }));
124
+ }
125
+ export function getFileAtIndex(files, index) {
126
+ const { ordered } = categorizeFiles(files);
127
+ return ordered[index] ?? null;
128
+ }
129
+ export function getTotalFileCount(files) {
130
+ return files.length;
131
+ }
@@ -1 +1,6 @@
1
- import{jsx as e,jsxs as l}from"react/jsx-runtime";import{Box as c,Text as o}from"ink";export function Footer({activeTab:r,mouseEnabled:i=!0,autoTabEnabled:n=!1,wrapMode:d=!1}){return l(c,{justifyContent:"space-between",children:[l(o,{children:[e(o,{dimColor:!0,children:"?"})," ",e(o,{color:"yellow",children:i?"[scroll]":"m:[select]"})," ",e(o,{color:n?"blue":void 0,dimColor:!n,children:"[auto]"})," ",e(o,{color:d?"blue":void 0,dimColor:!d,children:"[wrap]"})]}),l(o,{children:[e(o,{color:r==="diff"?"cyan":void 0,bold:r==="diff",children:"[1]Diff"})," ",e(o,{color:r==="commit"?"cyan":void 0,bold:r==="commit",children:"[2]Commit"})," ",e(o,{color:r==="history"?"cyan":void 0,bold:r==="history",children:"[3]History"})," ",e(o,{color:r==="compare"?"cyan":void 0,bold:r==="compare",children:"[4]Compare"})]})]})}
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ export function Footer({ activeTab, mouseEnabled = true, autoTabEnabled = false, wrapMode = false, showMiddleDots = false, }) {
4
+ // Layout: "? [scroll] [auto] [wrap] [dots]" with spaces between
5
+ return (_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "?" }), ' ', _jsx(Text, { color: "yellow", children: mouseEnabled ? '[scroll]' : 'm:[select]' }), ' ', _jsx(Text, { color: autoTabEnabled ? 'blue' : undefined, dimColor: !autoTabEnabled, children: "[auto]" }), ' ', _jsx(Text, { color: wrapMode ? 'blue' : undefined, dimColor: !wrapMode, children: "[wrap]" }), activeTab === 'explorer' && (_jsxs(_Fragment, { children: [' ', _jsx(Text, { color: showMiddleDots ? 'blue' : undefined, dimColor: !showMiddleDots, children: "[dots]" })] }))] }), _jsxs(Text, { children: [_jsx(Text, { color: activeTab === 'diff' ? 'cyan' : undefined, bold: activeTab === 'diff', children: "[1]Diff" }), ' ', _jsx(Text, { color: activeTab === 'commit' ? 'cyan' : undefined, bold: activeTab === 'commit', children: "[2]Commit" }), ' ', _jsx(Text, { color: activeTab === 'history' ? 'cyan' : undefined, bold: activeTab === 'history', children: "[3]History" }), ' ', _jsx(Text, { color: activeTab === 'compare' ? 'cyan' : undefined, bold: activeTab === 'compare', children: "[4]Compare" })] })] }));
6
+ }