diffstalker 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 (51) hide show
  1. package/.github/workflows/release.yml +40 -0
  2. package/CHANGELOG.md +19 -0
  3. package/LICENSE +21 -0
  4. package/README.md +154 -0
  5. package/assets/diff.png +0 -0
  6. package/assets/history.png +0 -0
  7. package/bin/diffstalker +2 -0
  8. package/dist/App.js +1 -0
  9. package/dist/components/BaseBranchPicker.js +1 -0
  10. package/dist/components/BottomPane.js +1 -0
  11. package/dist/components/CommitPanel.js +1 -0
  12. package/dist/components/CompareListView.js +1 -0
  13. package/dist/components/CompareView.js +1 -0
  14. package/dist/components/DiffView.js +1 -0
  15. package/dist/components/FileList.js +1 -0
  16. package/dist/components/Footer.js +1 -0
  17. package/dist/components/Header.js +1 -0
  18. package/dist/components/HistoryDiffView.js +1 -0
  19. package/dist/components/HistoryView.js +1 -0
  20. package/dist/components/HotkeysModal.js +1 -0
  21. package/dist/components/Modal.js +1 -0
  22. package/dist/components/ScrollableList.js +1 -0
  23. package/dist/components/ThemePicker.js +1 -0
  24. package/dist/components/TopPane.js +1 -0
  25. package/dist/config.js +2 -0
  26. package/dist/core/GitOperationQueue.js +1 -0
  27. package/dist/core/GitStateManager.js +1 -0
  28. package/dist/git/diff.js +10 -0
  29. package/dist/git/status.js +5 -0
  30. package/dist/hooks/useCommitFlow.js +1 -0
  31. package/dist/hooks/useCompareState.js +1 -0
  32. package/dist/hooks/useGit.js +1 -0
  33. package/dist/hooks/useHistoryState.js +1 -0
  34. package/dist/hooks/useKeymap.js +1 -0
  35. package/dist/hooks/useLayout.js +1 -0
  36. package/dist/hooks/useMouse.js +1 -0
  37. package/dist/hooks/useTerminalSize.js +1 -0
  38. package/dist/hooks/useWatcher.js +11 -0
  39. package/dist/index.js +43 -0
  40. package/dist/services/commitService.js +1 -0
  41. package/dist/themes.js +1 -0
  42. package/dist/utils/baseBranchCache.js +2 -0
  43. package/dist/utils/commitFormat.js +1 -0
  44. package/dist/utils/diffFilters.js +1 -0
  45. package/dist/utils/fileCategories.js +1 -0
  46. package/dist/utils/formatDate.js +1 -0
  47. package/dist/utils/formatPath.js +1 -0
  48. package/dist/utils/layoutCalculations.js +1 -0
  49. package/dist/utils/mouseCoordinates.js +1 -0
  50. package/dist/utils/rowCalculations.js +3 -0
  51. package/package.json +70 -0
@@ -0,0 +1,40 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ jobs:
9
+ release:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: write
13
+ id-token: write
14
+
15
+ steps:
16
+ - name: Checkout
17
+ uses: actions/checkout@v4
18
+
19
+ - name: Setup Node.js
20
+ uses: actions/setup-node@v4
21
+ with:
22
+ node-version: '20'
23
+ registry-url: 'https://registry.npmjs.org'
24
+
25
+ - name: Install dependencies
26
+ run: npm ci
27
+
28
+ - name: Run tests
29
+ run: npm test
30
+
31
+ - name: Build
32
+ run: npm run build:prod
33
+
34
+ - name: Publish to npm
35
+ run: npm publish --provenance --access public
36
+
37
+ - name: Create GitHub Release
38
+ uses: softprops/action-gh-release@v2
39
+ with:
40
+ generate_release_notes: true
package/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-01-21
9
+
10
+ ### Added
11
+
12
+ - Initial release
13
+ - Four views: Diff, Commit, History, and PR comparison
14
+ - Two-pane layout with resizable split
15
+ - Mouse support: click to select, stage/unstage, scroll, switch tabs
16
+ - Word-level diff highlighting
17
+ - 6 color themes including colorblind-friendly and ANSI-only variants
18
+ - Follow mode for shell integration
19
+ - Keyboard navigation with vim-style bindings
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 diffstalker contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # diffstalker
2
+
3
+ Keep your changes visible. A terminal-based git UI designed to run on a secondary monitor, automatically tracking whichever repository you're working in.
4
+
5
+ ![diffstalker diff view](assets/diff.png)
6
+ *Stage files and review changes with word-level diff highlighting.*
7
+
8
+ ![diffstalker history view](assets/history.png)
9
+ *Browse commit history and inspect past changes.*
10
+
11
+ ## Why diffstalker?
12
+
13
+ **Keep up with AI.** When AI assistants edit your code, changes happen fast. diffstalker gives you a live view of what's being modified, so you can review changes as they happen rather than piecing things together afterward.
14
+
15
+ **Always-on visibility.** Put diffstalker on your second monitor and forget about it. As you switch between projects, it follows along - showing your current changes, staged files, and diffs without you ever needing to alt-tab or type `git status`.
16
+
17
+ **Dead-simple integration.** Follow mode watches a plain text file for paths. Any script, hook, or tool can write to it. Add two lines to your shell config and every `cd` into a git repo updates the display automatically.
18
+
19
+ **Everything at a glance.** Auto-tab mode ensures there's always something useful on screen - uncommitted changes when you have them, recent commits when you don't. Word-level diff highlighting shows exactly what changed, not just which lines.
20
+
21
+ ## Features
22
+
23
+ - **Follow mode** - Automatically tracks repos via a simple file-based hook
24
+ - **Auto-tab** - Always shows relevant content (changes → history → PR diff)
25
+ - **Word-level diffs** - See precise changes within each line
26
+ - **Four views** - Diff, Commit, History, and PR comparison
27
+ - **Mouse & keyboard** - Click, scroll, or use vim-style navigation
28
+ - **6 themes** - Including colorblind-friendly and ANSI-only variants
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ npm install -g diffstalker
34
+ ```
35
+
36
+ Or from source:
37
+ ```bash
38
+ git clone https://github.com/yogh-io/diffstalker.git
39
+ cd diffstalker
40
+ npm install && npm run build:prod
41
+ npm link
42
+ ```
43
+
44
+ ## Quick Start
45
+
46
+ **Basic usage:**
47
+ ```bash
48
+ diffstalker # current directory
49
+ diffstalker /path/to/repo
50
+ ```
51
+
52
+ **Follow mode** (recommended for secondary monitor):
53
+ ```bash
54
+ diffstalker --follow
55
+ ```
56
+
57
+ Follow mode watches `~/.cache/diffstalker/target` for repository paths. Write or append to this file - diffstalker reads the last non-empty line, so both styles work:
58
+
59
+ ### Integration Examples
60
+
61
+ **Shell hook** - update on every `cd`:
62
+ ```bash
63
+ # Add to .bashrc or .zshrc
64
+ diffstalker_notify() {
65
+ [[ -d .git ]] && echo "$PWD" > ~/.cache/diffstalker/target
66
+ }
67
+ cd() { builtin cd "$@" && diffstalker_notify; }
68
+ ```
69
+
70
+ **Tmux** - update on pane/window switch:
71
+ ```bash
72
+ # In .tmux.conf
73
+ set-hook -g pane-focus-in 'run-shell "tmux display -p \"#{pane_current_path}\" > ~/.cache/diffstalker/target"'
74
+ ```
75
+
76
+ **Neovim** - update when changing buffers:
77
+ ```lua
78
+ -- In init.lua
79
+ vim.api.nvim_create_autocmd({"BufEnter"}, {
80
+ callback = function()
81
+ local root = vim.fn.finddir('.git/..', vim.fn.expand('%:p:h') .. ';')
82
+ if root ~= '' then
83
+ local f = io.open(os.getenv('HOME') .. '/.cache/diffstalker/target', 'w')
84
+ if f then f:write(vim.fn.fnamemodify(root, ':p:h')); f:close() end
85
+ end
86
+ end
87
+ })
88
+ ```
89
+
90
+ **Any script** - write or append:
91
+ ```bash
92
+ echo "/path/to/repo" > ~/.cache/diffstalker/target # overwrite
93
+ echo "/path/to/repo" >> ~/.cache/diffstalker/target # append (also works)
94
+ ```
95
+
96
+ The file-based approach is intentionally simple. IDE plugins, window manager hooks, project switchers, git hooks - if it can write to a file, it can drive diffstalker. Get creative.
97
+
98
+ ## Views
99
+
100
+ | View | Key | Purpose |
101
+ |------|-----|---------|
102
+ | **Diff** | `1` | Stage/unstage files, review changes |
103
+ | **Commit** | `2` | Write commit messages |
104
+ | **History** | `3` | Browse recent commits and their diffs |
105
+ | **PR** | `4` | Compare branch against base (main/master) |
106
+
107
+ ## Keybindings
108
+
109
+ **Navigation:** `↑↓` or `jk` to move, `Tab` to switch panes, `1-4` for views
110
+
111
+ **Staging:** `Space` toggle, `Ctrl+A` stage all, `Ctrl+Z` unstage all
112
+
113
+ **Other:** `t` themes, `?` help, `q` quit
114
+
115
+ Full keybinding reference available with `?` in the app.
116
+
117
+ ## Themes
118
+
119
+ Six built-in themes (`t` to switch):
120
+
121
+ | Theme | Description |
122
+ |-------|-------------|
123
+ | Dark | Default |
124
+ | Light | Light background |
125
+ | Dark/Light (colorblind) | Blue/red palette |
126
+ | Dark/Light (ANSI) | Terminal's 16 colors |
127
+
128
+ ## Configuration
129
+
130
+ Config: `~/.config/diffstalker/config.json`
131
+
132
+ ```json
133
+ {
134
+ "theme": "dark",
135
+ "splitRatio": 0.4,
136
+ "targetFile": "~/.cache/diffstalker/target"
137
+ }
138
+ ```
139
+
140
+ ## CLI Options
141
+
142
+ ```
143
+ diffstalker [options] [path]
144
+
145
+ Options:
146
+ -f, --follow [FILE] Watch file for repo paths
147
+ --once Show status once and exit
148
+ -d, --debug Log path changes to stderr
149
+ -h, --help Show help
150
+ ```
151
+
152
+ ## License
153
+
154
+ MIT
Binary file
Binary file
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/index.js');
package/dist/App.js ADDED
@@ -0,0 +1 @@
1
+ import{jsx as i,jsxs as W}from"react/jsx-runtime";import{useState as u,useCallback as c,useMemo as vt,useEffect as M,useRef as _}from"react";import{Box as B,Text as A,useApp as Dt,useInput as Ht}from"ink";import{Header as Ft,getHeaderHeight as Lt}from"./components/Header.js";import{getFileAtIndex as Fe,getTotalFileCount as Mt}from"./components/FileList.js";import{getCommitIndexFromRow as At}from"./components/HistoryView.js";import{Footer as Et}from"./components/Footer.js";import{TopPane as Ot}from"./components/TopPane.js";import{BottomPane as Ut}from"./components/BottomPane.js";import{useWatcher as jt}from"./hooks/useWatcher.js";import{useGit as Nt}from"./hooks/useGit.js";import{useKeymap as Wt}from"./hooks/useKeymap.js";import{useMouse as _t}from"./hooks/useMouse.js";import{useTerminalSize as zt}from"./hooks/useTerminalSize.js";import{useLayout as Gt,SPLIT_RATIO_STEP as Le}from"./hooks/useLayout.js";import{useHistoryState as Kt}from"./hooks/useHistoryState.js";import{useCompareState as Qt}from"./hooks/useCompareState.js";import{getClickedFileIndex as Yt,getClickedTab as qt,getFooterLeftClick as Jt,isButtonAreaClick as Vt,isInPane as oe}from"./utils/mouseCoordinates.js";import{saveConfig as Me}from"./config.js";import{ThemePicker as Xt}from"./components/ThemePicker.js";import{HotkeysModal as Zt}from"./components/HotkeysModal.js";import{BaseBranchPicker as $t}from"./components/BaseBranchPicker.js";export function App({config:p,initialPath:Ae}){const{exit:Ee}=Dt(),{rows:k,columns:r}=zt(),{state:z,setEnabled:Oe}=jt(p.watcherEnabled,p.targetFile,p.debug),R=Ae??z.path??process.cwd(),{status:v,diff:ie,selectedFile:Ue,isLoading:ne,error:re,selectFile:se,stage:g,unstage:y,discard:je,stageAll:Ne,unstageAll:We,commit:_e,refresh:ze,getHeadCommitMessage:Ge,compareDiff:w,compareLoading:Ke,compareError:Qe,refreshCompareDiff:Ye,getCandidateBaseBranches:qe,setCompareBaseBranch:Je,historySelectedCommit:ae,historyCommitDiff:le,selectHistoryCommit:Ve,compareSelectionDiff:Xe,selectCompareCommit:Ze}=Nt(R),m=v?.files??[],s=Mt(m),ce=m.filter(e=>e.staged).length,[l,f]=u("files"),[t,me]=u("diff"),[C,E]=u(0),[D,G]=u(null),[fe,$e]=u(!1),[de,et]=u(p.theme),[H,S]=u(null),[K,he]=u(!1),ue=Lt(R,v?.branch??null,z,r,re,ne),tt=ue-1,{topPaneHeight:Q,bottomPaneHeight:ot,paneBoundaries:pe,splitRatio:Y,adjustSplitRatio:ge,fileListScrollOffset:q,diffScrollOffset:it,historyScrollOffset:O,compareScrollOffset:U,setDiffScrollOffset:x,setHistoryScrollOffset:J,setCompareScrollOffset:nt,scrollDiff:P,scrollFileList:ye,scrollHistory:Ce,scrollCompare:Se}=Gt(k,r,m,C,ie,t,void 0,p.splitRatio,tt),{commits:j,historySelectedIndex:rt,setHistorySelectedIndex:F,historyDiffTotalRows:Te,navigateHistoryUp:be,navigateHistoryDown:we,historyTotalRows:xe}=Kt({repoPath:R,isActive:t==="history",selectHistoryCommit:Ve,historyCommitDiff:le,historySelectedCommit:ae,terminalWidth:r,topPaneHeight:Q,historyScrollOffset:O,setHistoryScrollOffset:J,setDiffScrollOffset:x,status:v}),{includeUncommitted:st,compareListSelection:d,baseBranchCandidates:at,showBaseBranchPicker:V,compareTotalItems:X,compareDiffTotalRows:I,setCompareSelectedIndex:Pe,toggleIncludeUncommitted:lt,openBaseBranchPicker:ct,closeBaseBranchPicker:mt,selectBaseBranch:ft,navigateCompareUp:Ie,navigateCompareDown:Be,markSelectionInitialized:ke,getItemIndexFromRow:Re}=Qt({repoPath:R,isActive:t==="compare",compareDiff:w,refreshCompareDiff:Ye,getCandidateBaseBranches:qe,setCompareBaseBranch:Je,selectCompareCommit:Ze,topPaneHeight:Q,compareScrollOffset:U,setCompareScrollOffset:nt,setDiffScrollOffset:x,status:v}),ve=_(pe);ve.current=pe;const dt=_(p.splitRatio);M(()=>{if(Y!==dt.current){const e=setTimeout(()=>Me({splitRatio:Y}),500);return()=>clearTimeout(e)}},[Y]);const a=vt(()=>Fe(m,C),[m,C]);M(()=>{s>0&&C>=s&&E(Math.max(0,s-1))},[s,C]),M(()=>{se(a)},[a,se]),M(()=>{(t==="diff"||t==="commit")&&x(0)},[C,t,x]);const T=c(e=>{me(e),f({diff:"files",commit:"commit",history:"history",compare:"compare"}[e])},[]),De=_(()=>{}),ht=c(e=>{const{x:h,y:b,type:N,button:ee}=e,{stagingPaneStart:L,fileListEnd:te,diffPaneStart:It,diffPaneEnd:Bt,footerRow:kt}=ve.current;if(N==="click"){if(H!==null){S(null);return}if(b===kt&&ee==="left"){const n=qt(h,r);if(n){T(n);return}const o=Jt(h);if(o==="hotkeys"){S("hotkeys");return}else if(o==="mouse-mode"){De.current();return}else if(o==="auto-tab"){he(Rt=>!Rt);return}}if(oe(b,L+1,te)){if(t==="diff"||t==="commit"){const n=Yt(b,q,m,L,te);if(n>=0&&n<s){E(n),f("files");const o=Fe(m,n);o&&(ee==="right"&&!o.staged&&o.status!=="untracked"?G(o):ee==="left"&&Vt(h)&&(o.staged?y(o):g(o)));return}}else if(t==="history"){const n=b-L-1,o=At(n,j,r,O);if(o>=0&&o<j.length){F(o),f("history"),x(0);return}}else if(t==="compare"&&w){const n=b-L-1+U,o=Re(n);if(o>=0&&o<X){ke(),Pe(o),f("compare");return}}}oe(b,It,Bt)&&f(t)}else if(N==="scroll-up"||N==="scroll-down"){const n=N==="scroll-up"?"up":"down";if(oe(b,L,te))t==="diff"||t==="commit"?ye(n):t==="history"?Ce(n,xe):t==="compare"&&Se(n,X);else{let o;t==="compare"&&d?.type!=="commit"?o=I:t==="history"&&(o=Te),P(n,3,o)}}},[r,q,m,s,t,j,w,X,g,y,P,ye,Ce,Se,O,U,x,F,Pe,ke,Re,d?.type,I,Te,xe,H]),ut=fe||V,{mouseEnabled:pt,toggleMouse:He}=_t(ht,ut);De.current=He;const Z=_(s);M(()=>{if(!K){Z.current=s;return}const e=Z.current;e===0&&s>0?T("diff"):e>0&&s===0&&(F(0),J(0),T("history")),Z.current=s},[s,K,T,F,J]);const gt=c(()=>{if(l==="files")E(e=>Math.max(0,e-1));else if(l==="diff"){const e=t==="compare"&&d?.type!=="commit"?I:void 0;P("up",3,e)}else l==="history"?be():l==="compare"&&Ie()},[l,t,d?.type,I,P,be,Ie]),yt=c(()=>{if(l==="files")E(e=>Math.min(s-1,e+1));else if(l==="diff"){const e=t==="compare"&&d?.type!=="commit"?I:void 0;P("down",3,e)}else l==="history"?we():l==="compare"&&Be()},[l,t,d?.type,I,s,P,we,Be]),Ct=c(()=>{t==="diff"||t==="commit"?f(e=>e==="files"?"diff":"files"):t==="history"?f(e=>e==="history"?"diff":"history"):t==="compare"&&f(e=>e==="compare"?"diff":"compare")},[t]),St=c(async()=>{a&&!a.staged&&await g(a)},[a,g]),Tt=c(async()=>{a?.staged&&await y(a)},[a,y]),bt=c(async()=>{a&&(a.staged?await y(a):await g(a))},[a,g,y]),wt=c(()=>T("commit"),[T]),xt=c(()=>{me("diff"),f("files")},[]),Pt=c(e=>{et(e),S(null),Me({theme:e})},[]);Wt({onStage:St,onUnstage:Tt,onStageAll:Ne,onUnstageAll:We,onCommit:wt,onQuit:Ee,onRefresh:ze,onNavigateUp:gt,onNavigateDown:yt,onTogglePane:Ct,onSwitchTab:T,onSelect:bt,onToggleIncludeUncommitted:lt,onCycleBaseBranch:ct,onOpenThemePicker:()=>S("theme"),onShrinkTopPane:()=>ge(-Le),onGrowTopPane:()=>ge(Le),onOpenHotkeysModal:()=>S("hotkeys"),onToggleMouse:He,onToggleFollow:()=>Oe(e=>!e),onToggleAutoTab:()=>he(e=>!e)},l,fe||H!==null||V),Ht((e,h)=>{D&&(e==="y"||e==="Y"?(je(D),G(null)):(e==="n"||e==="N"||h.escape)&&G(null))},{isActive:!!D});const $=()=>i(A,{dimColor:!0,children:"\u2500".repeat(r)});return W(B,{flexDirection:"column",height:k,width:r,children:[i(B,{height:ue,width:r,children:i(Ft,{repoPath:R,branch:v?.branch??null,isLoading:ne,error:re,debug:p.debug,watcherState:z,width:r})}),i($,{}),i(Ot,{bottomTab:t,currentPane:l,terminalWidth:r,topPaneHeight:Q,files:m,selectedIndex:C,fileListScrollOffset:q,stagedCount:ce,onStage:g,onUnstage:y,commits:j,historySelectedIndex:rt,historyScrollOffset:O,onSelectHistoryCommit:(e,h)=>F(h),compareDiff:w,compareListSelection:d,compareScrollOffset:U,includeUncommitted:st}),i($,{}),i(Ut,{bottomTab:t,currentPane:l,terminalWidth:r,bottomPaneHeight:ot,diffScrollOffset:it,currentTheme:de,diff:ie,selectedFile:Ue,stagedCount:ce,onCommit:_e,onCommitCancel:xt,getHeadCommitMessage:Ge,onCommitInputFocusChange:$e,historySelectedCommit:ae,historyCommitDiff:le,compareDiff:w,compareLoading:Ke,compareError:Qe,compareListSelection:d,compareSelectionDiff:Xe}),i($,{}),D?W(B,{children:[W(A,{color:"yellow",bold:!0,children:["Discard changes to"," "]}),i(A,{color:"cyan",children:D.path}),W(A,{color:"yellow",bold:!0,children:["?"," "]}),i(A,{dimColor:!0,children:"(y/n)"})]}):i(Et,{activeTab:t,mouseEnabled:pt,autoTabEnabled:K}),H==="theme"&&i(B,{position:"absolute",marginTop:0,marginLeft:0,children:i(Xt,{currentTheme:de,onSelect:Pt,onCancel:()=>S(null),width:r,height:k})}),H==="hotkeys"&&i(B,{position:"absolute",marginTop:0,marginLeft:0,children:i(Zt,{onClose:()=>S(null),width:r,height:k})}),V&&i(B,{position:"absolute",marginTop:0,marginLeft:0,children:i($t,{candidates:at,currentBranch:w?.baseBranch??null,onSelect:ft,onCancel:mt,width:r,height:k})})]})}
@@ -0,0 +1 @@
1
+ import{jsxs as d,jsx as e}from"react/jsx-runtime";import{useState as I,useMemo as v}from"react";import{Box as i,Text as n,useInput as A}from"ink";import{Modal as D,centerModal as H}from"./Modal.js";export function BaseBranchPicker({candidates:m,currentBranch:b,onSelect:g,onCancel:j,width:p,height:f}){const[r,M]=I(""),[y,h]=I(0),o=v(()=>{if(!r)return m;const l=r.toLowerCase();return m.filter(t=>t.toLowerCase().includes(l))},[m,r]),x=Math.min(y,Math.max(0,o.length-1));A((l,t)=>{t.escape?j():t.return?o.length===0&&r?g(r):o.length>0&&g(o[x]):t.upArrow?h(c=>Math.max(0,c-1)):t.downArrow?h(c=>Math.min(o.length-1,c+1)):t.backspace||t.delete?(M(c=>c.slice(0,-1)),h(0)):l&&!t.ctrl&&!t.meta&&(M(c=>c+l),h(0))});const C=Math.min(60,p-4),s=Math.min(10,f-10),w=Math.min(s+9,f-4),{x:S,y:E}=H(C,w,p,f),a=Math.max(0,x-s+1),B=o.slice(a,a+s);return e(D,{x:S,y:E,width:C,height:w,children:d(i,{borderStyle:"round",borderColor:"cyan",flexDirection:"column",width:C,children:[e(i,{justifyContent:"center",marginBottom:1,children:d(n,{bold:!0,color:"cyan",children:[" ","Select Base Branch"," "]})}),d(i,{marginBottom:1,children:[e(n,{dimColor:!0,children:"Filter: "}),e(n,{color:"cyan",children:r}),e(n,{color:"cyan",children:"\u258C"})]}),e(i,{flexDirection:"column",height:s,children:B.length>0?B.map((l,t)=>{const u=a+t===x,L=l===b;return d(i,{children:[e(n,{color:u?"cyan":void 0,children:u?"\u25B8 ":" "}),e(n,{bold:u,color:u?"cyan":void 0,children:l}),L&&e(n,{dimColor:!0,children:" (current)"})]},l)}):r?d(i,{children:[e(n,{dimColor:!0,children:" No matches. Press Enter to use: "}),e(n,{color:"yellow",children:r})]}):e(n,{dimColor:!0,children:" No candidates found"})}),o.length>s&&e(i,{children:d(n,{dimColor:!0,children:[a>0?"\u2191 ":" ",a+s<o.length?"\u2193 more":""]})}),e(i,{marginTop:1,justifyContent:"center",children:e(n,{dimColor:!0,children:"\u2191\u2193 navigate \u2022 Enter select \u2022 Esc cancel"})})]})})}
@@ -0,0 +1 @@
1
+ import{jsx as r,jsxs as u}from"react/jsx-runtime";import{Box as x,Text as i}from"ink";import{DiffView as C}from"./DiffView.js";import{CommitPanel as O}from"./CommitPanel.js";import{HistoryDiffView as _}from"./HistoryDiffView.js";import{CompareView as A}from"./CompareView.js";import{shortenPath as g}from"../utils/formatPath.js";export function BottomPane({bottomTab:e,currentPane:f,terminalWidth:s,bottomPaneHeight:o,diffScrollOffset:d,currentTheme:t,diff:w,selectedFile:c,stagedCount:j,onCommit:y,onCommitCancel:H,getHeadCommitMessage:v,onCommitInputFocusChange:I,historySelectedCommit:h,historyCommitDiff:M,compareDiff:l,compareLoading:V,compareError:a,compareListSelection:n,compareSelectionDiff:p}){const B=f!=="files"&&f!=="history"&&f!=="compare",D=()=>{if(c&&e==="diff")return r(i,{dimColor:!0,children:g(c.path,s-10)});if(e==="history"&&h)return u(i,{dimColor:!0,children:[h.shortHash," - ",h.message.slice(0,50)]});if(e==="compare"&&n)if(n.type==="commit"){const m=l?.commits[n.index];return u(i,{dimColor:!0,children:[m?.shortHash??""," - ",m?.message.slice(0,40)??""]})}else{const m=l?.files[n.index]?.path??"";return r(i,{dimColor:!0,children:g(m,s-10)})}return null},F=()=>e==="diff"?r(C,{diff:w,filePath:c?.path,maxHeight:o-1,scrollOffset:d,theme:t}):e==="commit"?r(O,{isActive:f==="commit",stagedCount:j,onCommit:y,onCancel:H,getHeadMessage:v,onInputFocusChange:I}):e==="history"?r(_,{commit:h,diff:M,maxHeight:o-1,scrollOffset:d,theme:t}):V?r(i,{dimColor:!0,children:"Loading compare diff..."}):a?r(i,{color:"red",children:a}):n?.type==="commit"&&p?r(C,{diff:p,maxHeight:o-1,scrollOffset:d,theme:t}):l?r(A,{compareDiff:l,isLoading:!1,error:null,scrollOffset:d,maxHeight:o-1,theme:t}):r(i,{dimColor:!0,children:"No compare diff available"});return u(x,{flexDirection:"column",height:o,width:s,overflowY:"hidden",children:[u(x,{width:s,children:[r(i,{bold:!0,color:B?"cyan":void 0,children:e==="commit"?"COMMIT":"DIFF"}),r(x,{flexGrow:1,justifyContent:"flex-end",children:D()})]}),F()]})}
@@ -0,0 +1 @@
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"})})}
@@ -0,0 +1 @@
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}
@@ -0,0 +1 @@
1
+ import{jsx as r,jsxs as s}from"react/jsx-runtime";import{useMemo as a}from"react";import{Box as e,Text as o}from"ink";import{DiffView as c}from"./DiffView.js";import{buildCombinedCompareDiff as h}from"../utils/rowCalculations.js";export{buildCombinedCompareDiff,getCompareDiffTotalRows,getFileScrollOffset}from"../utils/rowCalculations.js";export function CompareView({compareDiff:i,isLoading:d,error:n,scrollOffset:t,maxHeight:f,theme:m="dark"}){const l=a(()=>h(i),[i]);return d?r(e,{paddingX:1,children:r(o,{dimColor:!0,children:"Loading diff..."})}):n?r(e,{paddingX:1,children:r(o,{color:"red",children:n})}):i?i.files.length===0?r(e,{paddingX:1,children:s(o,{dimColor:!0,children:["No changes compared to ",i.baseBranch]})}):r(c,{diff:l,maxHeight:f,scrollOffset:t,theme:m}):r(e,{paddingX:1,children:r(o,{dimColor:!0,children:"No base branch found (no origin/main or origin/master)"})})}
@@ -0,0 +1 @@
1
+ import{jsx as c,jsxs as h}from"react/jsx-runtime";import{useMemo as k}from"react";import{Box as m,Text as a}from"ink";import{createEmphasize as E,common as B}from"emphasize";import p from"fast-diff";import{getTheme as D}from"../themes.js";import{ScrollableList as M}from"./ScrollableList.js";import{isDisplayableDiffLine as $}from"../utils/diffFilters.js";const A=E(B),H={ts:"typescript",tsx:"typescript",js:"javascript",jsx:"javascript",mjs:"javascript",cjs:"javascript",py:"python",rb:"ruby",rs:"rust",go:"go",java:"java",c:"c",cpp:"cpp",h:"c",hpp:"cpp",cs:"csharp",php:"php",sh:"bash",bash:"bash",zsh:"bash",json:"json",yaml:"yaml",yml:"yaml",md:"markdown",html:"html",htm:"html",css:"css",scss:"scss",sass:"scss",less:"less",sql:"sql",xml:"xml",toml:"ini",ini:"ini",dockerfile:"dockerfile",makefile:"makefile",lua:"lua",vim:"vim",swift:"swift",kt:"kotlin",kts:"kotlin",scala:"scala",r:"r",pl:"perl",ex:"elixir",exs:"elixir",erl:"erlang",hs:"haskell",clj:"clojure",ml:"ocaml",fs:"fsharp",vue:"xml",svelte:"xml"};function T(t){if(!t)return;const e=t.split("/").pop()?.toLowerCase()??"";if(e==="dockerfile")return"dockerfile";if(e==="makefile"||e==="gnumakefile")return"makefile";const n=e.split(".").pop()?.toLowerCase();return n?H[n]||n:void 0}function W(t,e){if(!e||!t.trim())return t;try{return A.highlight(e,t).value}catch{return t}}function _(t){let e=0;for(const n of t)n.oldLineNum&&n.oldLineNum>e&&(e=n.oldLineNum),n.newLineNum&&n.newLineNum>e&&(e=n.newLineNum);return Math.max(3,String(e).length)}function x(t){return t.type==="addition"||t.type==="deletion"||t.type==="context"&&t.content.startsWith(" ")?t.content.slice(1):t.content}const R=3;function I(t){if(t.length===0)return t;const e=[];for(let n=0;n<t.length;n++){const o=t[n];if(!o.isChange&&o.text.length<R){const i=n>0&&e[e.length-1]?.isChange,r=n<t.length-1&&t[n+1]?.isChange;if(i||r){i&&e.length>0?e[e.length-1].text+=o.text:e.push({text:o.text,isChange:!0});continue}}e.length>0&&e[e.length-1].isChange===o.isChange?e[e.length-1].text+=o.text:e.push({...o})}return e}function z(t,e){const n=p(t,e);let o=[],i=[];for(const[r,s]of n)r===p.EQUAL?(o.push({text:s,isChange:!1}),i.push({text:s,isChange:!1})):r===p.DELETE?o.push({text:s,isChange:!0}):r===p.INSERT&&i.push({text:s,isChange:!0});return o=I(o),i=I(i),{oldSegments:o,newSegments:i}}function G(t,e){if(t.length===0&&e.length===0)return 1;if(t.length===0||e.length===0)return 0;const n=p(t,e);let o=0,i=0;for(const[r,s]of n)r===p.EQUAL&&(o+=s.length),i+=s.length;return i>0?o/i:0}const U=.35;function q(t){const e=new Map;for(let n=0;n<t.length-1;n++){const o=t[n],i=t[n+1];if(o.type==="deletion"&&i.type==="addition"){const r=x(o),s=x(i);if(G(r,s)>=U){const u={deletion:o,addition:i,deletionIndex:n,additionIndex:n+1};e.set(n,u),e.set(n+1,u)}}}return e}function j({segments:t,isAddition:e,theme:n}){const{colors:o}=n,i=e?o.addBg:o.delBg,r=e?o.addHighlight:o.delHighlight;return c(a,{backgroundColor:i,children:t.map((s,u)=>c(a,{color:o.text,backgroundColor:s.isChange?r:i,children:s.text||(u===t.length-1?" ":"")},u))})}function O({line:t,lineNumWidth:e,language:n,wordDiffSegments:o,theme:i}){const{colors:r}=i;if(t.type==="header"){const l=t.content;if(l.startsWith("diff --git")){const d=l.match(/diff --git a\/.+ b\/(.+)$/);if(d)return c(m,{children:h(a,{color:"cyan",bold:!0,children:["\u2500\u2500 ",d[1]," \u2500\u2500"]})})}return c(m,{children:c(a,{dimColor:!0,children:l})})}if(t.type==="hunk"){const l=t.content.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/);if(l){const d=parseInt(l[1],10),C=l[2]?parseInt(l[2],10):1,f=parseInt(l[3],10),y=l[4]?parseInt(l[4],10):1,L=l[5].trim(),N=d+C-1,w=f+y-1,S=C===1?`${d}`:`${d}-${N}`,v=y===1?`${f}`:`${f}-${w}`;return h(m,{children:[h(a,{color:"cyan",dimColor:!0,children:["Lines ",S," \u2192 ",v]}),L&&h(a,{color:"gray",children:[" ",L]})]})}return c(a,{color:"cyan",dimColor:!0,children:t.content})}const s=t.type==="addition"?t.newLineNum:t.type==="deletion"?t.oldLineNum:t.oldLineNum??t.newLineNum,u=s!==void 0?String(s).padStart(e," "):" ".repeat(e),g=x(t);if(t.type==="addition")return h(m,{children:[h(a,{backgroundColor:r.addBg,color:r.addLineNum,children:[u," "]}),h(a,{backgroundColor:r.addBg,color:r.addSymbol,bold:!0,children:["+"," "]}),o?c(j,{segments:o,isAddition:!0,theme:i}):c(a,{backgroundColor:r.addBg,color:r.text,children:g||" "})]});if(t.type==="deletion")return h(m,{children:[h(a,{backgroundColor:r.delBg,color:r.delLineNum,children:[u," "]}),h(a,{backgroundColor:r.delBg,color:r.delSymbol,bold:!0,children:["-"," "]}),o?c(j,{segments:o,isAddition:!1,theme:i}):c(a,{backgroundColor:r.delBg,color:r.text,children:g||" "})]});const b=W(g,n);return h(m,{children:[h(a,{color:r.contextLineNum,children:[u," "]}),c(a,{children:b})]})}export function DiffView({diff:t,filePath:e,maxHeight:n=20,scrollOffset:o=0,theme:i="dark"}){const r=k(()=>D(i),[i]),s=k(()=>T(e),[e]),u=k(()=>{if(!t)return new Map;const l=q(t.lines),d=new Map;for(const[C,f]of l)if(C===f.deletionIndex){const y=x(f.deletion),L=x(f.addition),{oldSegments:N,newSegments:w}=z(y,L);d.set(f.deletionIndex,N),d.set(f.additionIndex,w)}return d},[t]),g=k(()=>t?.lines.map((l,d)=>({line:l,originalIndex:d})).filter(({line:l})=>$(l))??[],[t]);if(!t||g.length===0)return c(m,{paddingX:1,children:c(a,{dimColor:!0,children:"No diff to display"})});const b=_(g.map(l=>l.line));return c(m,{flexDirection:"column",paddingX:1,children:c(M,{items:g,maxHeight:n,scrollOffset:o,getKey:l=>`${l.originalIndex}`,renderItem:l=>{const d=u.get(l.originalIndex);return c(O,{line:l.line,lineNumWidth:b,language:s,wordDiffSegments:d,theme:r})}})})}
@@ -0,0 +1 @@
1
+ import{jsxs as s,jsx as c}from"react/jsx-runtime";import{Box as y,Text as t}from"ink";import{shortenPath as x}from"../utils/formatPath.js";import{categorizeFiles as C}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,i){if(e===void 0&&i===void 0)return null;const o=e??0,d=i??0;if(o===0&&d===0)return null;const a=[];return o>0&&a.push(`+${o}`),d>0&&a.push(`-${d}`),a.join(" ")}function j({file:e,isSelected:i,isFocused:o,maxPathLength:d}){const a=P(e.status),f=b(e.status),g=e.staged?"[-]":"[+]",l=e.staged?"red":"green",u=k(e.insertions,e.deletions),p=u?u.length+1:0,r=d-p,h=x(e.path,r);return s(y,{children:[i&&o?s(t,{color:"cyan",bold:!0,children:["\u25B8"," "]}):c(t,{children:" "}),s(t,{color:l,children:[g," "]}),s(t,{color:f,children:[a," "]}),s(t,{color:i&&o?"cyan":void 0,children:[h,e.originalPath&&s(t,{dimColor:!0,children:[" \u2190 ",x(e.originalPath,30)]})]}),u&&s(t,{children:[c(t,{dimColor:!0,children:" "}),e.insertions!==void 0&&e.insertions>0&&s(t,{color:"green",children:["+",e.insertions]}),e.insertions!==void 0&&e.insertions>0&&e.deletions!==void 0&&e.deletions>0&&c(t,{dimColor:!0,children:" "}),e.deletions!==void 0&&e.deletions>0&&s(t,{color:"red",children:["-",e.deletions]})]})]})}export function FileList({files:e,selectedIndex:i,isFocused:o,scrollOffset:d=0,maxHeight:a,width:f=80}){const g=f-10,{modified:l,untracked:u,staged:p}=C(e);if(e.length===0)return c(y,{flexDirection:"column",children:c(t,{dimColor:!0,children:" No changes"})});const r=[];let h=0;l.length>0&&(r.push({type:"header",content:"Modified:",headerColor:"yellow"}),l.forEach(n=>{r.push({type:"file",file:n,fileIndex:h++})})),u.length>0&&(l.length>0&&r.push({type:"spacer"}),r.push({type:"header",content:"Untracked:",headerColor:"gray"}),u.forEach(n=>{r.push({type:"file",file:n,fileIndex:h++})})),p.length>0&&((l.length>0||u.length>0)&&r.push({type:"spacer"}),r.push({type:"header",content:"Staged:",headerColor:"green"}),p.forEach(n=>{r.push({type:"file",file:n,fileIndex:h++})}));const F=a?r.slice(d,d+a):r.slice(d);return c(y,{flexDirection:"column",children:F.map((n,I)=>{const m=`row-${d+I}`;return n.type==="header"?c(t,{bold:!0,color:n.headerColor,children:n.content},m):n.type==="spacer"?c(t,{children:" "},m):n.type==="file"&&n.file!==void 0&&n.fileIndex!==void 0?c(j,{file:n.file,isSelected:n.fileIndex===i,isFocused:o,maxPathLength:g},m):null})})}export function getFileAtIndex(e,i){const{ordered:o}=C(e);return o[i]??null}export function getTotalFileCount(e){return e.length}
@@ -0,0 +1 @@
1
+ import{jsx as e,jsxs as l}from"react/jsx-runtime";import{Box as i,Text as o}from"ink";export function Footer({activeTab:r,mouseEnabled:d=!0,autoTabEnabled:n=!1}){return l(i,{justifyContent:"space-between",children:[l(o,{children:[e(o,{dimColor:!0,children:"?"})," hotkeys",e(o,{dimColor:!0,children:" | "}),l(o,{color:"yellow",children:["[",d?"scroll":"select","]"]}),e(o,{dimColor:!0,children:" | "}),l(o,{color:n?"blue":void 0,dimColor:!n,children:["[auto-tab:",n?"on":"off","]"]})]}),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"})]})]})}
@@ -0,0 +1 @@
1
+ import{jsx as n,Fragment as W,jsxs as i}from"react/jsx-runtime";import{Box as d,Text as e}from"ink";import{abbreviateHomePath as p}from"../config.js";export function getHeaderHeight(t,l,s,c,g=null,o=!1){if(!t)return 1;const y=p(t),a=g==="Not a git repository";let r=0;l&&(r=l.current.length,l.tracking&&(r+=3+l.tracking.length),l.ahead>0&&(r+=3+String(l.ahead).length),l.behind>0&&(r+=3+String(l.behind).length));let h=y.length;if(o&&(h+=2),a&&(h+=24),g&&!a&&(h+=g.length+3),s?.enabled&&s.sourceFile){const u=` (follow: ${p(s.sourceFile)})`,f=c-h-r-4;if(u.length>f){const C=c-h-2;if(u.length<=C)return 2}}return 1}function j({branch:t}){return i(d,{children:[n(e,{color:"green",bold:!0,children:t.current}),t.tracking&&i(W,{children:[n(e,{dimColor:!0,children:" \u2192 "}),n(e,{color:"blue",children:t.tracking})]}),(t.ahead>0||t.behind>0)&&i(e,{children:[t.ahead>0&&i(e,{color:"green",children:[" \u2191",t.ahead]}),t.behind>0&&i(e,{color:"red",children:[" \u2193",t.behind]})]})]})}export function Header({repoPath:t,branch:l,isLoading:s,error:c,debug:g,watcherState:o,width:y=80}){if(!t)return i(d,{flexDirection:"column",children:[i(d,{children:[n(e,{dimColor:!0,children:"Waiting for target path..."}),n(e,{dimColor:!0,children:" (write path to ~/.cache/diffstalker/target)"})]}),g&&o&&o.enabled&&o.sourceFile&&i(d,{children:[i(e,{dimColor:!0,children:["[debug] source: ",p(o.sourceFile)]}),o.rawContent&&i(e,{dimColor:!0,children:[' | raw: "',o.rawContent,'"']})]})]});const a=p(t),r=c==="Not a git repository",h=x=>x?x.toLocaleTimeString():"";let m=0;l&&(m=l.current.length,l.tracking&&(m+=3+l.tracking.length),l.ahead>0&&(m+=3+String(l.ahead).length),l.behind>0&&(m+=3+String(l.behind).length));let u=a.length;s&&(u+=2),r&&(u+=24),c&&!r&&(u+=c.length+3);let f=null,C=!1;if(o?.enabled&&o.sourceFile){const F=` (follow: ${p(o.sourceFile)})`,k=y-u-m-4;if(F.length<=k)f=F;else{const v=y-u-2;F.length<=v&&(f=F,C=!0)}}return i(d,{flexDirection:"column",width:y,children:[C?i(W,{children:[n(d,{justifyContent:"space-between",children:i(d,{children:[n(e,{bold:!0,color:"cyan",children:a}),s&&n(e,{color:"yellow",children:" \u27F3"}),r&&n(e,{color:"yellow",children:" (not a git repository)"}),c&&!r&&i(e,{color:"red",children:[" (",c,")"]}),f&&n(e,{dimColor:!0,children:f})]})}),n(d,{justifyContent:"flex-end",children:l&&n(j,{branch:l})})]}):i(d,{justifyContent:"space-between",children:[i(d,{children:[n(e,{bold:!0,color:"cyan",children:a}),s&&n(e,{color:"yellow",children:" \u27F3"}),r&&n(e,{color:"yellow",children:" (not a git repository)"}),c&&!r&&i(e,{color:"red",children:[" (",c,")"]}),f&&n(e,{dimColor:!0,children:f})]}),l&&n(j,{branch:l})]}),g&&o&&o.enabled&&o.sourceFile&&i(d,{children:[i(e,{dimColor:!0,children:["[debug] source: ",p(o.sourceFile)]}),i(e,{dimColor:!0,children:[' | raw: "',o.rawContent,'"']}),o.lastUpdate&&i(e,{dimColor:!0,children:[" | updated: ",h(o.lastUpdate)]})]})]})}
@@ -0,0 +1 @@
1
+ import{jsx as i,jsxs as y}from"react/jsx-runtime";import{useMemo as w}from"react";import{Box as r,Text as n}from"ink";import{DiffView as x}from"./DiffView.js";import{buildHistoryDiffRows as D}from"../utils/rowCalculations.js";import{isDisplayableDiffLine as R}from"../utils/diffFilters.js";export{buildHistoryDiffRows,getHistoryDiffTotalRows}from"../utils/rowCalculations.js";export function HistoryDiffView({commit:s,diff:t,scrollOffset:l,maxHeight:f,theme:h="dark"}){const c=w(()=>D(s,t),[s,t]),a=w(()=>{for(let e=0;e<c.length;e++)if(c[e].type==="diff-line")return e;return c.length},[c]),d=c.slice(l,l+f);if(!s)return i(r,{paddingX:1,children:i(n,{dimColor:!0,children:"Select a commit to view its diff"})});if(!t||t.lines.length===0)return y(r,{flexDirection:"column",children:[d.map((e,m)=>{const o=`row-${l+m}`;return e.type==="commit-header"?i(r,{children:i(n,{color:"yellow",children:e.content})},o):e.type==="commit-message"?i(r,{children:i(n,{children:e.content})},o):e.type==="spacer"?i(r,{children:i(n,{children:" "})},o):null}),i(r,{children:i(n,{dimColor:!0,children:"No changes in this commit"})})]});const u=t.lines.filter(R);if(l>=a)return i(x,{diff:{raw:t.raw,lines:u},maxHeight:f,scrollOffset:l-a,theme:h});const g=d.filter(e=>e.type!=="diff-line"),p=f-g.length;return y(r,{flexDirection:"column",children:[d.map((e,m)=>{const o=`row-${l+m}`;return e.type==="commit-header"?i(r,{children:i(n,{color:"yellow",children:e.content})},o):e.type==="commit-message"?i(r,{children:i(n,{children:e.content})},o):e.type==="spacer"?i(r,{children:i(n,{children:" "})},o):null}),p>0&&i(x,{diff:{raw:t.raw,lines:u},maxHeight:p,scrollOffset:0,theme:h})]})}
@@ -0,0 +1 @@
1
+ import{jsx as e,jsxs as i,Fragment as d}from"react/jsx-runtime";import{Box as u,Text as r}from"ink";import{ScrollableList as C}from"./ScrollableList.js";import{formatDate as w}from"../utils/formatDate.js";import{formatCommitDisplay as H}from"../utils/commitFormat.js";export{getCommitIndexFromRow,getHistoryTotalRows,getHistoryRowOffset}from"../utils/rowCalculations.js";export function HistoryView({commits:s,selectedIndex:m,scrollOffset:a,maxHeight:c,isActive:h,width:f,onSelectCommit:S}){return s.length===0?e(u,{children:e(r,{dimColor:!0,children:"No commits yet"})}):e(C,{items:s,maxHeight:c,scrollOffset:a,getKey:t=>t.hash,renderItem:(t,g)=>{const o=g===m&&h,n=w(t.date),p=11+n.length+2,x=f-p,{displayMessage:y,displayRefs:l}=H(t.message,t.refs,x);return i(d,{children:[e(r,{color:"yellow",children:t.shortHash}),e(r,{children:" "}),e(r,{color:o?"cyan":void 0,bold:o,inverse:o,children:y}),e(r,{children:" "}),i(r,{dimColor:!0,children:["(",n,")"]}),l&&i(d,{children:[e(r,{children:" "}),e(r,{color:"green",children:l})]})]})}})}
@@ -0,0 +1 @@
1
+ import{jsx as t,jsxs as c}from"react/jsx-runtime";import{Box as i,Text as s,useInput as C}from"ink";import{Modal as b,centerModal as M}from"./Modal.js";const r=[{title:"Navigation",entries:[{key:"\u2191/k",description:"Move up"},{key:"\u2193/j",description:"Move down"},{key:"Tab",description:"Toggle pane focus"}]},{title:"Staging",entries:[{key:"^S",description:"Stage file"},{key:"^U",description:"Unstage file"},{key:"^A",description:"Stage all"},{key:"^Z",description:"Unstage all"},{key:"Space/Enter",description:"Toggle stage"}]},{title:"Actions",entries:[{key:"c",description:"Open commit panel"},{key:"r",description:"Refresh"},{key:"q",description:"Quit"}]},{title:"Pane Resize",entries:[{key:"[",description:"Shrink top pane"},{key:"]",description:"Grow top pane"}]},{title:"Tabs",entries:[{key:"1",description:"Diff view"},{key:"2",description:"Commit panel"},{key:"3",description:"History view"},{key:"4",description:"Compare view"},{key:"a",description:"Toggle auto-tab mode"}]},{title:"Other",entries:[{key:"m",description:"Toggle scroll/select mode"},{key:"f",description:"Toggle follow mode"},{key:"t",description:"Theme picker"},{key:"b",description:"Base branch picker"},{key:"u",description:"Toggle uncommitted"},{key:"?",description:"This help"}]}];export function HotkeysModal({onClose:T,width:h,height:y}){C((e,o)=>{(o.escape||o.return||e==="?")&&T()});const m=h>=90,l=m?38:30,d=Math.min(m?82:40,h-4);let a;if(m){const e=Math.ceil(r.length/2),o=r.slice(0,e),n=r.slice(e),p=o.reduce((g,k)=>g+k.entries.length+2,0),w=n.reduce((g,k)=>g+k.entries.length+2,0);a=Math.min(Math.max(p,w)+5,y-4)}else{const e=r.reduce((o,n)=>o+n.entries.length+2,0)+4;a=Math.min(e,y-4)}const{x:f,y:x}=M(d,a,h,y),u=(e,o)=>c(i,{flexDirection:"column",marginBottom:1,children:[t(s,{bold:!0,dimColor:!0,children:e.title}),e.entries.map(n=>c(i,{children:[t(i,{width:13,children:t(s,{color:"cyan",children:n.key})}),t(i,{width:o-13,children:t(s,{children:n.description})})]},n.key))]},e.title);if(m){const e=Math.ceil(r.length/2),o=r.slice(0,e),n=r.slice(e);return t(b,{x:f,y:x,width:d,height:a,children:c(i,{borderStyle:"round",borderColor:"cyan",flexDirection:"column",width:d,children:[t(i,{justifyContent:"center",marginBottom:1,children:c(s,{bold:!0,color:"cyan",children:[" ","Keyboard Shortcuts"," "]})}),c(i,{children:[t(i,{flexDirection:"column",width:l,marginRight:2,children:o.map(p=>u(p,l))}),t(i,{flexDirection:"column",width:l,children:n.map(p=>u(p,l))})]}),t(i,{marginTop:1,justifyContent:"center",children:t(s,{dimColor:!0,children:"Press Esc, Enter, or ? to close"})})]})})}return t(b,{x:f,y:x,width:d,height:a,children:c(i,{borderStyle:"round",borderColor:"cyan",flexDirection:"column",width:d,children:[t(i,{justifyContent:"center",marginBottom:1,children:c(s,{bold:!0,color:"cyan",children:[" ","Keyboard Shortcuts"," "]})}),r.map(e=>u(e,l)),t(i,{marginTop:1,justifyContent:"center",children:t(s,{dimColor:!0,children:"Press Esc, Enter, or ? to close"})})]})})}
@@ -0,0 +1 @@
1
+ import{jsx as e,jsxs as c}from"react/jsx-runtime";import{Box as i,Text as x}from"ink";export function Modal({x:o,y:r,width:n,height:t,children:l}){const a=" ".repeat(n);return c(i,{position:"absolute",marginLeft:o,marginTop:r,flexDirection:"column",children:[Array.from({length:t}).map((f,s)=>e(x,{children:a},`blank-${s}`)),e(i,{position:"absolute",flexDirection:"column",children:l})]})}export function centerModal(o,r,n,t){return{x:Math.floor((n-o)/2),y:Math.floor((t-r)/2)}}
@@ -0,0 +1 @@
1
+ import{jsxs as h,jsx as j}from"react/jsx-runtime";import{Box as M,Text as g}from"ink";export function ScrollableList({items:a,renderItem:u,maxHeight:o,scrollOffset:e,getKey:i,header:t,showIndicators:r=!0}){let n=o;t&&n--;const b=e>0,x=a.length,c=x>e+n;r&&(b&&n--,c&&n--),n=Math.max(1,n);const l=a.slice(e,e+n),p=x>e+n,C=e,d=x-e-l.length;return h(M,{flexDirection:"column",children:[t,r&&b&&h(g,{dimColor:!0,children:["\u2191 ",C," more above"]}),l.map((v,m)=>j(M,{children:u(v,e+m)},`${e}-${m}-${i(v,e+m)}`)),r&&p&&h(g,{dimColor:!0,children:["\u2193 ",d," more below"]})]})}export function getMaxScrollOffset(a,u,o=!1,e=!0){let i=u;return o&&i--,e&&a>i&&(i-=2),i=Math.max(1,i),Math.max(0,a-i)}export function getVisibleItemCount(a,u,o,e=!1,i=!0){let t=u;return e&&t--,i&&(o>0&&t--,a>o+t&&t--),t=Math.max(1,t),Math.min(t,a-o)}
@@ -0,0 +1 @@
1
+ import{jsx as o,jsxs as c}from"react/jsx-runtime";import{useState as y}from"react";import{Box as l,Text as r,useInput as w}from"ink";import{themes as j,themeOrder as t,getTheme as S}from"../themes.js";import{Modal as T,centerModal as v}from"./Modal.js";function M({theme:h}){const{colors:e}=h;return c(l,{flexDirection:"column",marginLeft:2,children:[c(l,{children:[o(r,{backgroundColor:e.delBg,color:e.delLineNum,children:" 5 "}),o(r,{backgroundColor:e.delBg,color:e.delSymbol,bold:!0,children:"- "}),o(r,{backgroundColor:e.delBg,color:e.text,children:"const "}),o(r,{backgroundColor:e.delHighlight,color:e.text,children:"old"}),o(r,{backgroundColor:e.delBg,color:e.text,children:" = value;"})]}),c(l,{children:[o(r,{backgroundColor:e.addBg,color:e.addLineNum,children:" 5 "}),o(r,{backgroundColor:e.addBg,color:e.addSymbol,bold:!0,children:"+ "}),o(r,{backgroundColor:e.addBg,color:e.text,children:"const "}),o(r,{backgroundColor:e.addHighlight,color:e.text,children:"new"}),o(r,{backgroundColor:e.addBg,color:e.text,children:" = value;"})]})]})}export function ThemePicker({currentTheme:h,onSelect:e,onCancel:f,width:m,height:g}){const[a,x]=y(()=>{const n=t.indexOf(h);return n>=0?n:0}),C=S(t[a]);w((n,d)=>{d.escape?f():d.return?e(t[a]):d.upArrow||n==="k"?x(i=>Math.max(0,i-1)):(d.downArrow||n==="j")&&x(i=>Math.min(t.length-1,i+1))});const s=Math.min(50,m-4),b=Math.min(t.length+10,g-4),{x:p,y:B}=v(s,b,m,g);return o(T,{x:p,y:B,width:s,height:b,children:c(l,{borderStyle:"round",borderColor:"cyan",flexDirection:"column",width:s,children:[o(l,{justifyContent:"center",marginBottom:1,children:c(r,{bold:!0,color:"cyan",children:[" ","Select Theme"," "]})}),t.map((n,d)=>{const i=j[n],u=d===a,k=n===h;return c(l,{children:[o(r,{color:u?"cyan":void 0,children:u?"\u25B8 ":" "}),o(r,{bold:u,color:u?"cyan":void 0,children:i.displayName}),k&&o(r,{dimColor:!0,children:" (current)"})]},n)}),c(l,{marginTop:1,flexDirection:"column",children:[o(r,{dimColor:!0,children:"Preview:"}),o(M,{theme:C})]}),o(l,{marginTop:1,justifyContent:"center",children:o(r,{dimColor:!0,children:"\u2191\u2193 navigate \u2022 Enter select \u2022 Esc cancel"})})]})})}
@@ -0,0 +1 @@
1
+ import{jsx as o,jsxs as e,Fragment as s}from"react/jsx-runtime";import{Box as n,Text as i}from"ink";import{FileList as k}from"./FileList.js";import{HistoryView as p}from"./HistoryView.js";import{CompareListView as M}from"./CompareListView.js";import{categorizeFiles as _}from"../utils/fileCategories.js";export function TopPane({bottomTab:d,currentPane:r,terminalWidth:t,topPaneHeight:c,files:m,selectedIndex:f,fileListScrollOffset:x,stagedCount:g,onStage:C,onUnstage:y,commits:h,historySelectedIndex:a,historyScrollOffset:w,onSelectHistoryCommit:A,compareDiff:l,compareListSelection:F,compareScrollOffset:I,includeUncommitted:u}){const{modified:O,untracked:j}=_(m),v=O.length,S=j.length;return e(n,{flexDirection:"column",height:c,width:t,overflowY:"hidden",children:[(d==="diff"||d==="commit")&&e(s,{children:[e(n,{children:[o(i,{bold:!0,color:r==="files"?"cyan":void 0,children:"STAGING AREA"}),e(i,{dimColor:!0,children:[" ","(",v," modified, ",S," untracked, ",g," staged)"]})]}),o(k,{files:m,selectedIndex:f,isFocused:r==="files",scrollOffset:x,maxHeight:c-1,width:t,onStage:C,onUnstage:y})]}),d==="history"&&e(s,{children:[e(n,{children:[o(i,{bold:!0,color:r==="history"?"cyan":void 0,children:"COMMITS"}),e(i,{dimColor:!0,children:[" (",h.length," commits)"]})]}),o(p,{commits:h,selectedIndex:a,scrollOffset:w,maxHeight:c-1,isActive:r==="history",width:t,onSelectCommit:A})]}),d==="compare"&&e(s,{children:[e(n,{children:[o(i,{bold:!0,color:r==="compare"?"cyan":void 0,children:"COMPARE"}),o(i,{dimColor:!0,children:" (vs "}),o(i,{color:"cyan",children:l?.baseBranch??"..."}),e(i,{dimColor:!0,children:[": ",l?.commits.length??0," commits, ",l?.files.length??0," files) (b)"]}),l&&l.uncommittedCount>0&&e(s,{children:[o(i,{dimColor:!0,children:" | "}),e(i,{color:u?"magenta":"yellow",children:["[",u?"x":" ","] uncommitted"]}),o(i,{dimColor:!0,children:" (u)"})]})]}),o(M,{commits:l?.commits??[],files:l?.files??[],selectedItem:F,scrollOffset:I,maxHeight:c-1,isActive:r==="compare",width:t})]})]})}
package/dist/config.js ADDED
@@ -0,0 +1,2 @@
1
+ import*as t from"node:fs";import*as n from"node:path";import*as s from"node:os";const a={targetFile:n.join(s.homedir(),".cache","diffstalker","target"),watcherEnabled:!1,debug:!1,theme:"dark"},r=n.join(s.homedir(),".config","diffstalker","config.json");export const VALID_THEMES=["dark","light","dark-colorblind","light-colorblind","dark-ansi","light-ansi"];export function isValidTheme(i){return typeof i=="string"&&VALID_THEMES.includes(i)}export function loadConfig(){const i={...a};if(process.env.DIFFSTALKER_PAGER&&(i.pager=process.env.DIFFSTALKER_PAGER),t.existsSync(r))try{const e=JSON.parse(t.readFileSync(r,"utf-8"));e.pager&&(i.pager=e.pager),e.targetFile&&(i.targetFile=e.targetFile),isValidTheme(e.theme)&&(i.theme=e.theme),typeof e.splitRatio=="number"&&e.splitRatio>=.15&&e.splitRatio<=.85&&(i.splitRatio=e.splitRatio)}catch{}return i}export function saveConfig(i){const e=n.dirname(r);t.existsSync(e)||t.mkdirSync(e,{recursive:!0});let o={};if(t.existsSync(r))try{o=JSON.parse(t.readFileSync(r,"utf-8"))}catch{}Object.assign(o,i),t.writeFileSync(r,JSON.stringify(o,null,2)+`
2
+ `)}export function ensureTargetDir(i){const e=n.dirname(i);t.existsSync(e)||t.mkdirSync(e,{recursive:!0})}export function abbreviateHomePath(i){const e=s.homedir();return i.startsWith(e)?"~"+i.slice(e.length):i}
@@ -0,0 +1 @@
1
+ export class GitOperationQueue{queue=[];isProcessing=!1;pendingMutations=0;refreshScheduled=!1;enqueue(e){return new Promise((s,r)=>{this.queue.push({execute:e,resolve:s,reject:r}),this.processNext()})}enqueueMutation(e){return this.pendingMutations++,this.enqueue(e).finally(()=>{this.pendingMutations--})}hasPendingMutations(){return this.pendingMutations>0}scheduleRefresh(e){this.pendingMutations>0||this.refreshScheduled||(this.refreshScheduled=!0,this.enqueue(async()=>{this.refreshScheduled=!1,await e()}).catch(()=>{this.refreshScheduled=!1}))}isBusy(){return this.isProcessing||this.queue.length>0}async processNext(){if(this.isProcessing||this.queue.length===0)return;this.isProcessing=!0;const e=this.queue.shift();try{const s=await e.execute();e.resolve(s)}catch(s){e.reject(s instanceof Error?s:new Error(String(s)))}finally{this.isProcessing=!1,this.processNext()}}}const i=new Map;export function getQueueForRepo(t){let e=i.get(t);return e||(e=new GitOperationQueue,i.set(t,e)),e}export function removeQueueForRepo(t){i.delete(t)}
@@ -0,0 +1 @@
1
+ import*as h from"node:path";import*as m from"node:fs";import{watch as l}from"chokidar";import{EventEmitter as g}from"node:events";import{getQueueForRepo as S,removeQueueForRepo as y}from"./GitOperationQueue.js";import{getStatus as w,stageFile as D,unstageFile as C,stageAll as _,unstageAll as F,discardChanges as q,commit as P,getHeadMessage as B}from"../git/status.js";import{getDiff as c,getDiffForUntracked as p,getStagedDiff as f,getDefaultBaseBranch as E,getCandidateBaseBranches as R,getDiffBetweenRefs as W,getCompareDiffWithUncommitted as k,getCommitDiff as d}from"../git/diff.js";import{getCachedBaseBranch as $,setCachedBaseBranch as M}from"../utils/baseBranchCache.js";export class GitStateManager extends g{repoPath;queue;gitWatcher=null;workingDirWatcher=null;_state={status:null,diff:null,stagedDiff:"",selectedFile:null,isLoading:!1,error:null};_compareState={compareDiff:null,compareBaseBranch:null,compareLoading:!1,compareError:null};_historyState={selectedCommit:null,commitDiff:null};_compareSelectionState={type:null,index:0,diff:null};constructor(t){super(),this.repoPath=t,this.queue=S(t)}get state(){return this._state}get compareState(){return this._compareState}get historyState(){return this._historyState}get compareSelectionState(){return this._compareSelectionState}updateState(t){this._state={...this._state,...t},this.emit("state-change",this._state)}updateCompareState(t){this._compareState={...this._compareState,...t},this.emit("compare-state-change",this._compareState)}updateHistoryState(t){this._historyState={...this._historyState,...t},this.emit("history-state-change",this._historyState)}updateCompareSelectionState(t){this._compareSelectionState={...this._compareSelectionState,...t},this.emit("compare-selection-change",this._compareSelectionState)}startWatching(){const t=h.join(this.repoPath,".git");if(!m.existsSync(t))return;const e=h.join(t,"index"),a=h.join(t,"HEAD"),s=h.join(t,"refs");this.gitWatcher=l([e,a,s],{persistent:!0,ignoreInitial:!0,awaitWriteFinish:{stabilityThreshold:100,pollInterval:50}}),this.workingDirWatcher=l(this.repoPath,{persistent:!0,ignoreInitial:!0,ignored:["**/node_modules/**","**/.git/**","**/dist/**","**/build/**","**/*.log","**/.DS_Store"],awaitWriteFinish:{stabilityThreshold:100,pollInterval:50},depth:10});const i=()=>this.scheduleRefresh();this.gitWatcher.on("change",i),this.gitWatcher.on("add",i),this.gitWatcher.on("unlink",i),this.workingDirWatcher.on("change",i),this.workingDirWatcher.on("add",i),this.workingDirWatcher.on("unlink",i)}dispose(){this.gitWatcher?.close(),this.workingDirWatcher?.close(),y(this.repoPath)}scheduleRefresh(){this.queue.scheduleRefresh(()=>this.doRefresh())}async refresh(){await this.queue.enqueue(()=>this.doRefresh())}async doRefresh(){this.updateState({isLoading:!0,error:null});try{const t=await w(this.repoPath);if(!t.isRepo){this.updateState({status:t,diff:null,stagedDiff:"",isLoading:!1,error:"Not a git repository"});return}const[e,a]=await Promise.all([f(this.repoPath),c(this.repoPath,void 0,!1)]);let s;const i=this._state.selectedFile;if(i){const o=t.files.find(u=>u.path===i.path&&u.staged===i.staged);o?o.status==="untracked"?s=await p(this.repoPath,o.path):s=await c(this.repoPath,o.path,o.staged):(s=a.raw?a:e,this.updateState({selectedFile:null}))}else a.raw?s=a:e.raw?s=e:s={raw:"",lines:[]};this.updateState({status:t,diff:s,stagedDiff:e.raw,isLoading:!1})}catch(t){this.updateState({isLoading:!1,error:t instanceof Error?t.message:"Unknown error"})}}async selectFile(t){this.updateState({selectedFile:t}),this._state.status?.isRepo&&await this.queue.enqueue(async()=>{if(t){let e;t.status==="untracked"?e=await p(this.repoPath,t.path):e=await c(this.repoPath,t.path,t.staged),this.updateState({diff:e})}else{const e=await f(this.repoPath);this.updateState({diff:e})}})}async stage(t){const e=this._state.status;e&&this.updateState({status:{...e,files:e.files.map(a=>a.path===t.path&&!a.staged?{...a,staged:!0}:a)}});try{await this.queue.enqueueMutation(()=>D(this.repoPath,t.path)),this.scheduleRefresh()}catch(a){await this.refresh(),this.updateState({error:`Failed to stage ${t.path}: ${a instanceof Error?a.message:String(a)}`})}}async unstage(t){const e=this._state.status;e&&this.updateState({status:{...e,files:e.files.map(a=>a.path===t.path&&a.staged?{...a,staged:!1}:a)}});try{await this.queue.enqueueMutation(()=>C(this.repoPath,t.path)),this.scheduleRefresh()}catch(a){await this.refresh(),this.updateState({error:`Failed to unstage ${t.path}: ${a instanceof Error?a.message:String(a)}`})}}async discard(t){if(!(t.staged||t.status==="untracked"))try{await this.queue.enqueueMutation(()=>q(this.repoPath,t.path)),await this.refresh()}catch(e){this.updateState({error:`Failed to discard ${t.path}: ${e instanceof Error?e.message:String(e)}`})}}async stageAll(){try{await this.queue.enqueueMutation(()=>_(this.repoPath)),await this.refresh()}catch(t){this.updateState({error:`Failed to stage all: ${t instanceof Error?t.message:String(t)}`})}}async unstageAll(){try{await this.queue.enqueueMutation(()=>F(this.repoPath)),await this.refresh()}catch(t){this.updateState({error:`Failed to unstage all: ${t instanceof Error?t.message:String(t)}`})}}async commit(t,e=!1){try{await this.queue.enqueue(()=>P(this.repoPath,t,e)),await this.refresh()}catch(a){this.updateState({error:`Failed to commit: ${a instanceof Error?a.message:String(a)}`})}}async getHeadCommitMessage(){return this.queue.enqueue(()=>B(this.repoPath))}async refreshCompareDiff(t=!1){this.updateCompareState({compareLoading:!0,compareError:null});try{await this.queue.enqueue(async()=>{let e=this._compareState.compareBaseBranch;if(e||(e=$(this.repoPath)??await E(this.repoPath),this.updateCompareState({compareBaseBranch:e})),e){const a=t?await k(this.repoPath,e):await W(this.repoPath,e);this.updateCompareState({compareDiff:a,compareLoading:!1})}else this.updateCompareState({compareDiff:null,compareLoading:!1,compareError:"No base branch found"})})}catch(e){this.updateCompareState({compareLoading:!1,compareError:`Failed to load compare diff: ${e instanceof Error?e.message:String(e)}`})}}async getCandidateBaseBranches(){return R(this.repoPath)}async setCompareBaseBranch(t,e=!1){this.updateCompareState({compareBaseBranch:t}),M(this.repoPath,t),await this.refreshCompareDiff(e)}async selectHistoryCommit(t){if(this.updateHistoryState({selectedCommit:t,commitDiff:null}),!!t)try{await this.queue.enqueue(async()=>{const e=await d(this.repoPath,t.hash);this.updateHistoryState({commitDiff:e})})}catch(e){this.updateState({error:`Failed to load commit diff: ${e instanceof Error?e.message:String(e)}`})}}async selectCompareCommit(t){const e=this._compareState.compareDiff;if(!e||t<0||t>=e.commits.length){this.updateCompareSelectionState({type:null,index:0,diff:null});return}const a=e.commits[t];this.updateCompareSelectionState({type:"commit",index:t,diff:null});try{await this.queue.enqueue(async()=>{const s=await d(this.repoPath,a.hash);this.updateCompareSelectionState({diff:s})})}catch(s){this.updateState({error:`Failed to load commit diff: ${s instanceof Error?s.message:String(s)}`})}}selectCompareFile(t){const e=this._compareState.compareDiff;if(!e||t<0||t>=e.files.length){this.updateCompareSelectionState({type:null,index:0,diff:null});return}const a=e.files[t];this.updateCompareSelectionState({type:"file",index:t,diff:a.diff})}}const n=new Map;export function getManagerForRepo(r){let t=n.get(r);return t||(t=new GitStateManager(r),n.set(r,t)),t}export function removeManagerForRepo(r){const t=n.get(r);t&&(t.dispose(),n.delete(r))}
@@ -0,0 +1,10 @@
1
+ import{execSync as B}from"node:child_process";import{simpleGit as b}from"simple-git";export function parseDiffLine(s){return s.startsWith("diff --git")||s.startsWith("index ")||s.startsWith("---")||s.startsWith("+++")||s.startsWith("new file")||s.startsWith("deleted file")?{type:"header",content:s}:s.startsWith("@@")?{type:"hunk",content:s}:s.startsWith("+")?{type:"addition",content:s}:s.startsWith("-")?{type:"deletion",content:s}:{type:"context",content:s}}export function parseHunkHeader(s){const o=s.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);return o?{oldStart:parseInt(o[1],10),newStart:parseInt(o[2],10)}:null}export function parseDiffWithLineNumbers(s){const o=s.split(`
2
+ `),i=[];let a=0,c=0;for(const n of o)if(n.startsWith("diff --git")||n.startsWith("index ")||n.startsWith("---")||n.startsWith("+++")||n.startsWith("new file")||n.startsWith("deleted file")||n.startsWith("Binary files")||n.startsWith("similarity index")||n.startsWith("rename from")||n.startsWith("rename to"))i.push({type:"header",content:n});else if(n.startsWith("@@")){const l=parseHunkHeader(n);l&&(a=l.oldStart,c=l.newStart),i.push({type:"hunk",content:n})}else n.startsWith("+")?i.push({type:"addition",content:n,newLineNum:c++}):n.startsWith("-")?i.push({type:"deletion",content:n,oldLineNum:a++}):i.push({type:"context",content:n,oldLineNum:a++,newLineNum:c++});return i}export async function getDiff(s,o,i=!1){const a=b(s);try{const c=[];i&&c.push("--cached"),o&&c.push("--",o);const n=await a.diff(c),l=parseDiffWithLineNumbers(n);return{raw:n,lines:l}}catch{return{raw:"",lines:[]}}}export async function getDiffForUntracked(s,o){try{const i=B(`cat "${o}"`,{cwd:s,encoding:"utf-8"}),a=[{type:"header",content:`diff --git a/${o} b/${o}`},{type:"header",content:"new file mode 100644"},{type:"header",content:"--- /dev/null"},{type:"header",content:`+++ b/${o}`}],c=i.split(`
3
+ `);a.push({type:"hunk",content:`@@ -0,0 +1,${c.length} @@`});let n=1;for(const h of c)a.push({type:"addition",content:"+"+h,newLineNum:n++});return{raw:a.map(h=>h.content).join(`
4
+ `),lines:a}}catch{return{raw:"",lines:[]}}}export async function getStagedDiff(s){return getDiff(s,void 0,!0)}export async function getCandidateBaseBranches(s){const o=b(s),i=new Set,a=[];try{const c=await o.raw(["log","--oneline","--decorate=short","--all","-n","200"]),n=/\(([^)]+)\)/g;for(const l of c.split(`
5
+ `)){const h=n.exec(l);if(h){const m=h[1].split(",").map(u=>u.trim());for(const u of m){if(u.startsWith("HEAD")||u.startsWith("tag:")||!u.includes("/"))continue;const d=u.replace(/^.*-> /,"");d.includes("/")&&!i.has(d)&&(i.add(d),a.push(d))}}n.lastIndex=0}a.length>0&&a.sort((l,h)=>{const m=l.split("/").slice(1).join("/"),u=h.split("/").slice(1).join("/"),d=m==="main"||m==="master",g=u==="main"||u==="master";if(d&&!g)return-1;if(!d&&g)return 1;if(d&&g){const w=l.startsWith("origin/"),y=h.startsWith("origin/");if(w&&!y)return 1;if(!w&&y)return-1}return 0})}catch{}return[...new Set(a)]}export async function getDefaultBaseBranch(s){return(await getCandidateBaseBranches(s))[0]??null}export async function getDiffBetweenRefs(s,o){const i=b(s),c=(await i.raw(["merge-base",o,"HEAD"])).trim(),n=await i.raw(["diff","--numstat",`${c}...HEAD`]),l=await i.raw(["diff","--name-status",`${c}...HEAD`]),h=await i.raw(["diff",`${c}...HEAD`]),m=n.trim().split(`
6
+ `).filter(t=>t),u=new Map;for(const t of m){const e=t.split(" ");if(e.length>=3){const r=e[0]==="-"?0:parseInt(e[0],10),p=e[1]==="-"?0:parseInt(e[1],10),f=e.slice(2).join(" ");u.set(f,{additions:r,deletions:p})}}const d=l.trim().split(`
7
+ `).filter(t=>t),g=new Map;for(const t of d){const e=t.split(" ");if(e.length>=2){const r=e[0][0],p=e[e.length-1];let f;switch(r){case"A":f="added";break;case"D":f="deleted";break;case"R":f="renamed";break;default:f="modified"}g.set(p,f)}}const w=[],y=h.split(/(?=^diff --git )/m).filter(t=>t.trim());for(const t of y){const e=t.match(/^diff --git a\/.+ b\/(.+)$/m);if(!e)continue;const r=e[1],p=parseDiffWithLineNumbers(t),f=u.get(r)||{additions:0,deletions:0},D=g.get(r)||"modified";w.push({path:r,status:D,additions:f.additions,deletions:f.deletions,diff:{raw:t,lines:p}})}let x=0,k=0;for(const t of w)x+=t.additions,k+=t.deletions;const I=(await i.status()).files.length,S=(await i.log({from:c,to:"HEAD"})).all.map(t=>({hash:t.hash,shortHash:t.hash.slice(0,7),message:t.message.split(`
8
+ `)[0],author:t.author_name,date:new Date(t.date),refs:t.refs||""}));return{baseBranch:o,stats:{filesChanged:w.length,additions:x,deletions:k},files:w,commits:S,uncommittedCount:I}}export async function getCommitDiff(s,o){const i=b(s);try{const a=await i.raw(["show",o,"--format="]),c=parseDiffWithLineNumbers(a);return{raw:a,lines:c}}catch{return{raw:"",lines:[]}}}export async function getCompareDiffWithUncommitted(s,o){const i=b(s),a=await getDiffBetweenRefs(s,o),c=await i.diff(["--cached","--numstat"]),n=await i.diff(["--numstat"]),l=await i.diff(["--cached"]),h=await i.diff([]),m=new Map;for(const t of c.trim().split(`
9
+ `).filter(e=>e)){const e=t.split(" ");if(e.length>=3){const r=e[0]==="-"?0:parseInt(e[0],10),p=e[1]==="-"?0:parseInt(e[1],10),f=e.slice(2).join(" ");m.set(f,{additions:r,deletions:p,staged:!0,unstaged:!1})}}for(const t of n.trim().split(`
10
+ `).filter(e=>e)){const e=t.split(" ");if(e.length>=3){const r=e[0]==="-"?0:parseInt(e[0],10),p=e[1]==="-"?0:parseInt(e[1],10),f=e.slice(2).join(" "),D=m.get(f);D?(D.additions+=r,D.deletions+=p,D.unstaged=!0):m.set(f,{additions:r,deletions:p,staged:!1,unstaged:!0})}}const u=await i.status(),d=new Map;for(const t of u.files)t.index==="A"||t.working_dir==="?"?d.set(t.path,"added"):t.index==="D"||t.working_dir==="D"?d.set(t.path,"deleted"):t.index==="R"?d.set(t.path,"renamed"):d.set(t.path,"modified");const g=[],y=(l+h).split(/(?=^diff --git )/m).filter(t=>t.trim()),x=new Set;for(const t of y){const e=t.match(/^diff --git a\/.+ b\/(.+)$/m);if(!e)continue;const r=e[1];if(x.has(r))continue;x.add(r);const p=parseDiffWithLineNumbers(t),f=m.get(r)||{additions:0,deletions:0},D=d.get(r)||"modified";g.push({path:r,status:D,additions:f.additions,deletions:f.deletions,diff:{raw:t,lines:p},isUncommitted:!0})}const k=new Set(a.files.map(t=>t.path)),W=[];for(const t of a.files){const e=g.find(r=>r.path===t.path);e?(W.push(t),W.push(e)):W.push(t)}for(const t of g)k.has(t.path)||W.push(t);let I=0,L=0;const S=new Set;for(const t of W)S.has(t.path)||S.add(t.path),I+=t.additions,L+=t.deletions;return{baseBranch:a.baseBranch,stats:{filesChanged:S.size,additions:I,deletions:L},files:W,commits:a.commits,uncommittedCount:a.uncommittedCount}}
@@ -0,0 +1,5 @@
1
+ import{simpleGit as c}from"simple-git";import*as x from"node:fs";import*as k from"node:path";export function parseNumstat(n){const e=new Map;for(const i of n.trim().split(`
2
+ `)){if(!i)continue;const s=i.split(" ");if(s.length>=3){const a=s[0]==="-"?0:parseInt(s[0],10),r=s[1]==="-"?0:parseInt(s[1],10),d=s.slice(2).join(" ");e.set(d,{insertions:a,deletions:r})}}return e}async function y(n,e){try{const i=k.join(n,e);return(await x.promises.readFile(i,"utf-8")).split(`
3
+ `).filter(a=>a.length>0).length}catch{return 0}}async function S(n,e){if(e.length===0)return new Set;try{const i=new Set,s=100;for(let a=0;a<e.length;a+=s){const r=e.slice(a,a+s);try{const l=(await n.raw(["check-ignore",...r])).trim().split(`
4
+ `).filter(u=>u.length>0);for(const u of l)i.add(u)}catch{}}return i}catch{return new Set}}export function parseStatusCode(n){switch(n){case"M":return"modified";case"A":return"added";case"D":return"deleted";case"?":return"untracked";case"R":return"renamed";case"C":return"copied";default:return"modified"}}export async function getStatus(n){const e=c(n);try{if(!await e.checkIsRepo())return{files:[],branch:{current:"",ahead:0,behind:0},isRepo:!1};const s=await e.status(),a=[];for(const t of s.staged)a.push({path:t,status:"added",staged:!0});for(const t of s.modified)a.find(g=>g.path===t&&g.staged)||a.push({path:t,status:"modified",staged:!1});for(const t of s.deleted)a.push({path:t,status:"deleted",staged:!1});for(const t of s.not_added)a.push({path:t,status:"untracked",staged:!1});for(const t of s.renamed)a.push({path:t.to,originalPath:t.from,status:"renamed",staged:!0});const r=[],d=new Set,l=s.files.filter(t=>t.working_dir==="?").map(t=>t.path),u=await S(e,l);for(const t of s.files){if(t.index==="!"||t.working_dir==="!"||u.has(t.path))continue;const o=`${t.path}-${t.index!==" "&&t.index!=="?"}`;d.has(o)||(d.add(o),t.index&&t.index!==" "&&t.index!=="?"&&r.push({path:t.path,status:parseStatusCode(t.index),staged:!0}),t.working_dir&&t.working_dir!==" "&&r.push({path:t.path,status:t.working_dir==="?"?"untracked":parseStatusCode(t.working_dir),staged:!1}))}const[h,p]=await Promise.all([e.diff(["--cached","--numstat"]).catch(()=>""),e.diff(["--numstat"]).catch(()=>"")]),m=parseNumstat(h),w=parseNumstat(p);for(const t of r){const o=t.staged?m.get(t.path):w.get(t.path);o&&(t.insertions=o.insertions,t.deletions=o.deletions)}const f=r.filter(t=>t.status==="untracked");if(f.length>0){const t=await Promise.all(f.map(o=>y(n,o.path)));for(let o=0;o<f.length;o++)f[o].insertions=t[o],f[o].deletions=0}return{files:r,branch:{current:s.current||"HEAD",tracking:s.tracking||void 0,ahead:s.ahead,behind:s.behind},isRepo:!0}}catch{return{files:[],branch:{current:"",ahead:0,behind:0},isRepo:!1}}}export async function stageFile(n,e){await c(n).add(e)}export async function unstageFile(n,e){await c(n).reset(["HEAD","--",e])}export async function stageAll(n){await c(n).add("-A")}export async function unstageAll(n){await c(n).reset(["HEAD"])}export async function discardChanges(n,e){await c(n).checkout(["--",e])}export async function commit(n,e,i=!1){await c(n).commit(e,void 0,i?{"--amend":null}:void 0)}export async function getHeadMessage(n){const e=c(n);try{return(await e.log({n:1})).latest?.message||""}catch{return""}}export async function getCommitHistory(n,e=50){const i=c(n);try{return(await i.log({n:e})).all.map(a=>({hash:a.hash,shortHash:a.hash.slice(0,7),message:a.message.split(`
5
+ `)[0],author:a.author_name,date:new Date(a.date),refs:a.refs||""}))}catch{return[]}}
@@ -0,0 +1 @@
1
+ import{useState as o,useCallback as i,useEffect as M}from"react";import{validateCommit as S,formatCommitMessage as b}from"../services/commitService.js";export function useCommitFlow(C){const{stagedCount:l,onCommit:m,onSuccess:c,getHeadMessage:f}=C,[s,n]=o(""),[t,r]=o(!1),[p,u]=o(!1),[h,a]=o(null),[v,d]=o(!1);M(()=>{t&&f().then(e=>{e&&!s&&n(e)})},[t,f]);const y=i(()=>{r(e=>!e)},[]),E=i(async()=>{const e=S(s,l,t);if(!e.valid){a(e.error);return}u(!0),a(null);try{await m(b(s),t),n(""),r(!1),c()}catch(g){a(g instanceof Error?g.message:"Commit failed")}finally{u(!1)}},[s,l,t,m,c]),F=i(()=>{n(""),r(!1),a(null),d(!1)},[]);return{message:s,amend:t,isCommitting:p,error:h,inputFocused:v,setMessage:n,toggleAmend:y,setInputFocused:d,handleSubmit:E,reset:F}}
@@ -0,0 +1 @@
1
+ import{useState as r,useEffect as g,useCallback as o,useMemo as b,useRef as X}from"react";import{getCompareItemIndexFromRow as Y,getFileScrollOffset as Z,getCompareDiffTotalRows as _}from"../utils/rowCalculations.js";export function useCompareState({repoPath:a,isActive:e,compareDiff:n,refreshCompareDiff:k,getCandidateBaseBranches:R,setCompareBaseBranch:C,selectCompareCommit:T,topPaneHeight:U,compareScrollOffset:c,setCompareScrollOffset:m,setDiffScrollOffset:l,status:F}){const[d,M]=r(!0),[y,h]=r(null),[u,x]=r(0),i=X(!1),[z,E]=r([]),[L,w]=r(!1);g(()=>{a&&e&&k(d)},[a,e,F,k,d]),g(()=>{a&&e&&R().then(E)},[a,e,R]),g(()=>{e&&(i.current=!1,h(null),l(0))},[e,l]),g(()=>{if(e&&n&&i.current){const t=n.commits.length,s=n.files.length;if(u<t)h({type:"commit",index:u}),T(u),l(0);else if(u<t+s){const I=u-t;h({type:"file",index:I});const W=Z(n,I);l(W)}}},[e,n,u,T,l]);const B=b(()=>n?n.commits.length+n.files.length:0,[n]),j=b(()=>_(n),[n]),q=o(()=>{M(t=>!t)},[]),G=o(()=>{w(!0)},[]),J=o(()=>{w(!1)},[]),K=o(t=>{w(!1),C(t,d)},[C,d]),N=o(()=>{i.current=!0},[]),P=o(()=>{i.current=!0,x(t=>{const s=Math.max(0,t-1);return s<c&&m(s),s})},[c,m]),Q=o(()=>{i.current=!0,x(t=>{const s=Math.min(B-1,t+1),I=c+U-2;return s>=I&&m(c+1),s})},[B,c,U,m]),V=o(t=>n?Y(t,n.commits.length,n.files.length):-1,[n]);return{includeUncommitted:d,compareListSelection:y,compareSelectedIndex:u,baseBranchCandidates:z,showBaseBranchPicker:L,compareTotalItems:B,compareDiffTotalRows:j,setCompareSelectedIndex:x,toggleIncludeUncommitted:q,openBaseBranchPicker:G,closeBaseBranchPicker:J,selectBaseBranch:K,navigateCompareUp:P,navigateCompareDown:Q,markSelectionInitialized:N,getItemIndexFromRow:V}}
@@ -0,0 +1 @@
1
+ import{useState as i,useEffect as k,useCallback as a,useRef as v}from"react";import{getManagerForRepo as I,removeManagerForRepo as T}from"../core/GitStateManager.js";export function useGit(c){const[n,m]=i({status:null,diff:null,stagedDiff:"",selectedFile:null,isLoading:!1,error:null}),[o,p]=i({compareDiff:null,compareBaseBranch:null,compareLoading:!1,compareError:null}),[f,h]=i({selectedCommit:null,commitDiff:null}),[l,C]=i({type:null,index:0,diff:null}),t=v(null);k(()=>{if(!c){m({status:null,diff:null,stagedDiff:"",selectedFile:null,isLoading:!1,error:null});return}const e=I(c);t.current=e;const s=r=>{m(r)},u=r=>{p(r)},g=r=>{h(r)},d=r=>{C(r)};return e.on("state-change",s),e.on("compare-state-change",u),e.on("history-state-change",g),e.on("compare-selection-change",d),e.startWatching(),e.refresh(),()=>{e.off("state-change",s),e.off("compare-state-change",u),e.off("history-state-change",g),e.off("compare-selection-change",d),T(c),t.current=null}},[c]);const y=a(e=>{t.current?.selectFile(e)},[]),S=a(async e=>{await t.current?.stage(e)},[]),B=a(async e=>{await t.current?.unstage(e)},[]),D=a(async e=>{await t.current?.discard(e)},[]),w=a(async()=>{await t.current?.stageAll()},[]),F=a(async()=>{await t.current?.unstageAll()},[]),L=a(async(e,s=!1)=>{await t.current?.commit(e,s)},[]),H=a(async()=>{await t.current?.refresh()},[]),x=a(async()=>t.current?.getHeadCommitMessage()??"",[]),A=a(async(e=!1)=>{await t.current?.refreshCompareDiff(e)},[]),E=a(async()=>t.current?.getCandidateBaseBranches()??[],[]),M=a(async(e,s=!1)=>{await t.current?.setCompareBaseBranch(e,s)},[]),R=a(async e=>{await t.current?.selectHistoryCommit(e)},[]),G=a(async e=>{await t.current?.selectCompareCommit(e)},[]),b=a(e=>{t.current?.selectCompareFile(e)},[]);return{status:n.status,diff:n.diff,stagedDiff:n.stagedDiff,selectedFile:n.selectedFile,isLoading:n.isLoading,error:n.error,selectFile:y,stage:S,unstage:B,discard:D,stageAll:w,unstageAll:F,commit:L,refresh:H,getHeadCommitMessage:x,compareDiff:o.compareDiff,compareBaseBranch:o.compareBaseBranch,compareLoading:o.compareLoading,compareError:o.compareError,refreshCompareDiff:A,getCandidateBaseBranches:E,setCompareBaseBranch:M,historySelectedCommit:f.selectedCommit,historyCommitDiff:f.commitDiff,selectHistoryCommit:R,compareSelectionType:l.type,compareSelectionIndex:l.index,compareSelectionDiff:l.diff,selectCompareCommit:G,selectCompareFile:b}}
@@ -0,0 +1 @@
1
+ import{useState as R,useEffect as T,useCallback as M,useMemo as b}from"react";import{getCommitHistory as j}from"../git/status.js";import{getHistoryDiffTotalRows as q,getHistoryTotalRows as v}from"../utils/rowCalculations.js";export function useHistoryState({repoPath:u,isActive:a,selectHistoryCommit:w,historyCommitDiff:i,historySelectedCommit:x,terminalWidth:d,topPaneHeight:c,historyScrollOffset:e,setHistoryScrollOffset:m,setDiffScrollOffset:I,status:p}){const[n,r]=R([]),[s,g]=R(0);T(()=>{u&&a&&j(u,100).then(r)},[u,a,p]),T(()=>{if(a&&n.length>0){const t=n[s];t&&(w(t),I(0))}},[a,n,s,w,I]);const E=b(()=>q(x,i),[x,i]),H=b(()=>v(n,d),[n,d]),k=M(()=>{g(t=>{const o=Math.max(0,t-1);return o<e&&m(o),o})},[e,m]),D=M(()=>{g(t=>{const o=Math.min(n.length-1,t+1),U=e+c-2;return o>=U&&m(e+1),o})},[n.length,e,c,m]);return{commits:n,historySelectedIndex:s,setHistorySelectedIndex:g,historyDiffTotalRows:E,navigateHistoryUp:k,navigateHistoryDown:D,historyTotalRows:H}}
@@ -0,0 +1 @@
1
+ import{useInput as n}from"ink";export function useKeymap(r,l,f){n((e,o)=>{if(!f){if(o.ctrl&&e==="c"){r.onQuit();return}if(e==="q"){r.onQuit();return}if(e==="j"||o.downArrow){r.onNavigateDown();return}if(e==="k"||o.upArrow){r.onNavigateUp();return}if(o.tab){r.onTogglePane();return}if(e==="1"){r.onSwitchTab("diff");return}if(e==="2"){r.onSwitchTab("commit");return}if(e==="3"){r.onSwitchTab("history");return}if(e==="4"){r.onSwitchTab("compare");return}if(e==="u"&&r.onToggleIncludeUncommitted){r.onToggleIncludeUncommitted();return}if(e==="b"&&r.onCycleBaseBranch){r.onCycleBaseBranch();return}if(e==="t"&&r.onOpenThemePicker){r.onOpenThemePicker();return}if(e==="?"&&r.onOpenHotkeysModal){r.onOpenHotkeysModal();return}if(e==="["&&r.onShrinkTopPane){r.onShrinkTopPane();return}if(e==="]"&&r.onGrowTopPane){r.onGrowTopPane();return}if(e==="m"&&r.onToggleMouse){r.onToggleMouse();return}if(e==="f"&&r.onToggleFollow){r.onToggleFollow();return}if(e==="a"&&r.onToggleAutoTab){r.onToggleAutoTab();return}if(o.ctrl&&e==="s"){r.onStage();return}if(o.ctrl&&e==="u"){r.onUnstage();return}if(o.ctrl&&e==="a"){r.onStageAll();return}if(o.ctrl&&e==="z"){r.onUnstageAll();return}if(e==="c"){r.onCommit();return}if(o.ctrl&&e==="r"){r.onRefresh();return}if(e==="r"){r.onRefresh();return}if(o.return||e===" "){r.onSelect();return}}})}
@@ -0,0 +1 @@
1
+ import{useState as m,useEffect as R,useMemo as P,useCallback as a}from"react";import{getRowForFileIndex as W,calculateScrollOffset as Y,getFileListSectionCounts as q,getFileListTotalRows as z}from"../utils/layoutCalculations.js";import{calculatePaneBoundaries as G}from"../utils/mouseCoordinates.js";export const LAYOUT_OVERHEAD=5;const T={diff:.4,commit:.4,history:.5,compare:.5};export const SPLIT_RATIO_STEP=.05;export function useLayout(u,J,l,O,S,p="diff",K,F,H=0){const r=u-LAYOUT_OVERHEAD-H,[g,d]=m(F??null),M=g??T[p],{topPaneHeight:i,bottomPaneHeight:f}=P(()=>{const t=r-5,o=Math.floor(r*M),n=Math.max(5,Math.min(o,t)),s=r-n;return{topPaneHeight:n,bottomPaneHeight:s}},[r,M]),b=a(e=>{const t=Math.max(.15,Math.min(.85,e));d(t)},[]),y=a(e=>{const t=g??T[p],o=Math.max(.15,Math.min(.85,t+e));d(o)},[g,p]),A=M,L=H+1,E=P(()=>G(i,f,u,L),[i,f,u,L]),[I,h]=m(0),[_,x]=m(0),[D,w]=m(0),[k,C]=m(0);R(()=>{h(0)},[l.length]),R(()=>{x(0)},[S]),R(()=>{const{modifiedCount:e,untrackedCount:t,stagedCount:o}=q(l),n=W(O,e,t,o),s=i-1;h(c=>Y(n,c,s))},[O,l,i]);const B=a((e,t=3,o)=>{const n=o??S?.lines.length??0,s=Math.max(0,n-(f-4));x(c=>e==="up"?Math.max(0,c-t):Math.min(s,c+t))},[S?.lines.length,f]),U=a((e,t=3)=>{const o=z(l),n=i-1,s=Math.max(0,o-n);h(c=>e==="up"?Math.max(0,c-t):Math.min(s,c+t))},[l,i]),j=a((e,t=0,o=3)=>{const n=Math.max(0,t-(i-1));w(s=>e==="up"?Math.max(0,s-o):Math.min(n,s+o))},[i]),V=a((e,t,o=3)=>{const n=Math.max(0,t-(i-1));C(s=>e==="up"?Math.max(0,s-o):Math.min(n,s+o))},[i]);return{topPaneHeight:i,bottomPaneHeight:f,contentHeight:r,paneBoundaries:E,splitRatio:A,setSplitRatio:b,adjustSplitRatio:y,fileListScrollOffset:I,diffScrollOffset:_,historyScrollOffset:D,compareScrollOffset:k,setFileListScrollOffset:h,setDiffScrollOffset:x,setHistoryScrollOffset:w,setCompareScrollOffset:C,scrollDiff:B,scrollFileList:U,scrollHistory:j,scrollCompare:V}}
@@ -0,0 +1 @@
1
+ import{useEffect as d,useState as y,useCallback as C,useRef as x}from"react";import{useStdin as R}from"ink";export function useMouse(p,b=!1){const{stdin:c,setRawMode:h}=R(),[u,w]=y(!0),e=x(p);d(()=>{e.current=p});const M=C(()=>{w(l=>!l)},[]),a=x(u);return d(()=>{a.current=u},[u]),d(()=>{b?(process.stdout.write("\x1B[?1006l"),process.stdout.write("\x1B[?1000l")):(process.stdout.write("\x1B[?1000h"),process.stdout.write("\x1B[?1006h"))},[b]),d(()=>{if(!c||!h)return;const l=g=>{const m=g.toString(),o=m.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/);if(o){const t=parseInt(o[1],10),s=parseInt(o[2],10),n=parseInt(o[3],10),r=o[4]==="m";if(t>=64&&t<96){if(a.current){const f=t===64?"scroll-up":"scroll-down";e.current({x:s,y:n,type:f,button:"none"})}}else if(r&&t>=0&&t<3){const f=t===0?"left":t===1?"middle":"right";e.current({x:s,y:n,type:"click",button:f})}return}const i=m.match(/\x1b\[M(.)(.)(.)/);if(i){const t=i[1].charCodeAt(0)-32,s=i[2].charCodeAt(0)-32,n=i[3].charCodeAt(0)-32;if(t>=64){if(a.current){const r=t===64?"scroll-up":"scroll-down";e.current({x:s,y:n,type:r,button:"none"})}}else if(t>=0&&t<3){const r=t===0?"left":t===1?"middle":"right";e.current({x:s,y:n,type:"click",button:r})}}};return c.on("data",l),()=>{c.off("data",l),process.stdout.write("\x1B[?1006l"),process.stdout.write("\x1B[?1000l")}},[c,h]),{mouseEnabled:u,toggleMouse:M}}
@@ -0,0 +1 @@
1
+ import{useState as t,useEffect as r}from"react";export function useTerminalSize(){const[o,e]=t({rows:process.stdout.rows??24,columns:process.stdout.columns??80});return r(()=>{const s=()=>{e({rows:process.stdout.rows??24,columns:process.stdout.columns??80})};return process.stdout.on("resize",s),()=>{process.stdout.off("resize",s)}},[]),o}
@@ -0,0 +1,11 @@
1
+ import{useState as h,useEffect as S,useRef as $}from"react";import*as f from"node:fs";import*as a from"node:path";import*as v from"node:os";import{watch as b}from"chokidar";import{ensureTargetDir as I}from"../config.js";function y(t){return t.startsWith("~/")?a.join(v.homedir(),t.slice(2)):t==="~"?v.homedir():t}function R(t){const e=t.split(`
2
+ `);for(let s=e.length-1;s>=0;s--){const n=e[s].trim();if(n)return n}return""}export function useWatcher(t,e,s=!1){const[n,T]=h(t),[x,p]=h({path:null,lastUpdate:null,rawContent:null,sourceFile:t?e:null,enabled:t}),i=$(null),l=$(null);return S(()=>{p(d=>({...d,enabled:n,sourceFile:n?e:null}))},[n,e]),S(()=>{if(!n)return;I(e),f.existsSync(e)||f.writeFileSync(e,"");const d=()=>{i.current&&clearTimeout(i.current),i.current=setTimeout(()=>{try{const m=f.readFileSync(e,"utf-8"),r=R(m);if(r&&r!==l.current){const o=y(r),c=a.isAbsolute(o)?o:a.resolve(o),u=new Date;s&&(process.stderr.write(`[diffstalker ${u.toISOString()}] Path change detected
3
+ `),process.stderr.write(` Source file: ${e}
4
+ `),process.stderr.write(` Raw content: "${r}"
5
+ `),process.stderr.write(` Previous: "${l.current??"(none)"}"
6
+ `),process.stderr.write(` Resolved: "${c}"
7
+ `)),l.current=c,p({path:c,lastUpdate:u,rawContent:r,sourceFile:e,enabled:!0})}}catch{}},100)};try{const m=f.readFileSync(e,"utf-8"),r=R(m);if(r){const o=y(r),c=a.isAbsolute(o)?o:a.resolve(o),u=new Date;s&&(process.stderr.write(`[diffstalker ${u.toISOString()}] Initial path read
8
+ `),process.stderr.write(` Source file: ${e}
9
+ `),process.stderr.write(` Raw content: "${r}"
10
+ `),process.stderr.write(` Resolved: "${c}"
11
+ `)),l.current=c,p({path:c,lastUpdate:u,rawContent:r,sourceFile:e,enabled:!0})}}catch{}const w=b(e,{persistent:!0,ignoreInitial:!0});return w.on("change",d),w.on("add",d),()=>{i.current&&clearTimeout(i.current),w.close()}},[n,e,s]),{state:x,setEnabled:T}}
package/dist/index.js ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ import{jsx as n}from"react/jsx-runtime";import{render as f}from"ink";import{App as a}from"./App.js";import{loadConfig as c}from"./config.js";function l(){process.stdout.write("\x1B[?1006l"),process.stdout.write("\x1B[?1002l"),process.stdout.write("\x1B[?1000l"),process.stdout.write("\x1B[?25h")}process.on("exit",l),process.on("SIGINT",()=>{l(),process.exit(0)}),process.on("SIGTERM",()=>{l(),process.exit(0)}),process.on("uncaughtException",e=>{l(),console.error("Uncaught exception:",e),process.exit(1)}),process.on("unhandledRejection",e=>{l(),console.error("Unhandled rejection:",e),process.exit(1)});function p(e){const o={};for(let i=0;i<e.length;i++){const t=e[i];t==="--follow"||t==="-f"?(o.follow=!0,e[i+1]&&!e[i+1].startsWith("-")&&(o.followFile=e[++i])):t==="--once"?o.once=!0:t==="--debug"||t==="-d"?o.debug=!0:t==="--help"||t==="-h"?(console.log(`
3
+ diffstalker - Terminal git diff/status viewer
4
+
5
+ Usage: diffstalker [options] [path]
6
+
7
+ Options:
8
+ -f, --follow [FILE] Follow hook file for dynamic repo switching
9
+ (default: ~/.cache/diffstalker/target)
10
+ --once Show status once and exit
11
+ -d, --debug Log path changes to stderr for debugging
12
+ -h, --help Show this help message
13
+
14
+ Arguments:
15
+ [path] Path to a git repository (fixed, no watching)
16
+
17
+ Modes:
18
+ diffstalker Fixed on current directory
19
+ diffstalker /path/to/repo Fixed on specified repo
20
+ diffstalker --follow Follow default hook file
21
+ diffstalker --follow /tmp/hook Follow custom hook file
22
+
23
+ Environment:
24
+ DIFFSTALKER_PAGER External pager for diff display
25
+
26
+ Keyboard:
27
+ j/k, \u2191/\u2193 Navigate files / scroll diff
28
+ Ctrl+S Stage selected file
29
+ Ctrl+U Unstage selected file
30
+ Ctrl+A Stage all files
31
+ Ctrl+Z Unstage all files
32
+ Enter/Space Toggle stage/unstage
33
+ Tab Switch between panes
34
+ 1/2 Switch bottom tab (Diff/Commit)
35
+ c Open commit panel
36
+ r Refresh
37
+ q / Ctrl+C Quit
38
+
39
+ Mouse:
40
+ Click Select file / focus pane
41
+ Click [+/-] Stage/unstage file
42
+ Scroll Navigate files / scroll diff
43
+ `),process.exit(0)):t.startsWith("-")||(o.initialPath=t)}return o}const s=p(process.argv.slice(2)),r=c();s.follow&&(r.watcherEnabled=!0,s.followFile&&(r.targetFile=s.followFile)),s.debug&&(r.debug=!0);const{waitUntilExit:d}=f(n(a,{config:r,initialPath:s.initialPath}));d().then(()=>{process.exit(0)});
@@ -0,0 +1 @@
1
+ export function validateCommit(r,e,t){return r.trim()?e===0&&!t?{valid:!1,error:"No changes staged for commit"}:{valid:!0,error:null}:{valid:!1,error:"Commit message cannot be empty"}}export function formatCommitMessage(r){return r.trim()}
package/dist/themes.js ADDED
@@ -0,0 +1 @@
1
+ const i={name:"dark",displayName:"Dark",colors:{addBg:"#022800",delBg:"#3D0100",addHighlight:"#044700",delHighlight:"#5C0200",text:"white",addLineNum:"#368F35",delLineNum:"#A14040",contextLineNum:"gray",addSymbol:"greenBright",delSymbol:"redBright"}},l={name:"light",displayName:"Light",colors:{addBg:"#69db7c",delBg:"#ffa8b4",addHighlight:"#2f9d44",delHighlight:"#d1454b",text:"black",addLineNum:"#2f9d44",delLineNum:"#d1454b",contextLineNum:"#6c757d",addSymbol:"green",delSymbol:"red"}},t={name:"dark-colorblind",displayName:"Dark (colorblind)",colors:{addBg:"#004466",delBg:"#660000",addHighlight:"#0077b3",delHighlight:"#b30000",text:"white",addLineNum:"#0077b3",delLineNum:"#b30000",contextLineNum:"gray",addSymbol:"cyanBright",delSymbol:"redBright"}},r={name:"light-colorblind",displayName:"Light (colorblind)",colors:{addBg:"#99ccff",delBg:"#ffcccc",addHighlight:"#3366cc",delHighlight:"#993333",text:"black",addLineNum:"#3366cc",delLineNum:"#993333",contextLineNum:"#6c757d",addSymbol:"blue",delSymbol:"red"}},n={name:"dark-ansi",displayName:"Dark (ANSI)",colors:{addBg:"green",delBg:"red",addHighlight:"greenBright",delHighlight:"redBright",text:"white",addLineNum:"greenBright",delLineNum:"redBright",contextLineNum:"gray",addSymbol:"greenBright",delSymbol:"redBright"}},g={name:"light-ansi",displayName:"Light (ANSI)",colors:{addBg:"green",delBg:"red",addHighlight:"greenBright",delHighlight:"redBright",text:"black",addLineNum:"green",delLineNum:"red",contextLineNum:"gray",addSymbol:"green",delSymbol:"red"}};export const themes={dark:i,light:l,"dark-colorblind":t,"light-colorblind":r,"dark-ansi":n,"light-ansi":g},themeOrder=["dark","light","dark-colorblind","light-colorblind","dark-ansi","light-ansi"];export function getTheme(e){return themes[e]??themes.dark}export function getNextTheme(e){const d=(themeOrder.indexOf(e)+1)%themeOrder.length;return themeOrder[d]}
@@ -0,0 +1,2 @@
1
+ import*as n from"node:fs";import*as c from"node:path";import*as i from"node:os";const t=c.join(i.homedir(),".cache","diffstalker","base-branches.json");function h(){const e=c.dirname(t);n.existsSync(e)||n.mkdirSync(e,{recursive:!0})}function s(){try{if(n.existsSync(t))return JSON.parse(n.readFileSync(t,"utf-8"))}catch{}return{}}function f(e){h(),n.writeFileSync(t,JSON.stringify(e,null,2)+`
2
+ `)}export function getCachedBaseBranch(e){const a=s(),r=c.resolve(e);return a[r]}export function setCachedBaseBranch(e,a){const r=s(),o=c.resolve(e);r[o]=a,f(r)}
@@ -0,0 +1 @@
1
+ export function truncateWithEllipsis(t,s){return t.length<=s?t:s<=3?t.slice(0,s):t.slice(0,s-3)+"..."}export function formatCommitDisplay(t,s,n,l=20){const r=s||"",i=Math.max(0,n-l-1);let e=r;e.length>i&&i>3?e=e.slice(0,i-3)+"...":e.length>i&&(e="");const c=e?e.length+1:0,f=Math.max(l,n-c);return{displayMessage:truncateWithEllipsis(t,f),displayRefs:e}}
@@ -0,0 +1 @@
1
+ export function isDisplayableDiffHeader(i){return!(i.startsWith("index ")||i.startsWith("--- ")||i.startsWith("+++ ")||i.startsWith("similarity index"))}export function isDisplayableDiffLine(i){return i.type!=="header"?!0:isDisplayableDiffHeader(i.content)}
@@ -0,0 +1 @@
1
+ export function categorizeFiles(e){const n=e.filter(t=>!t.staged&&t.status!=="untracked"),d=e.filter(t=>!t.staged&&t.status==="untracked"),o=e.filter(t=>t.staged);return{modified:n,untracked:d,staged:o,ordered:[...n,...d,...o]}}export function getFileListSectionCounts(e){const{modified:n,untracked:d,staged:o}=categorizeFiles(e);return{modifiedCount:n.length,untrackedCount:d.length,stagedCount:o.length}}
@@ -0,0 +1 @@
1
+ export function formatDate(t){const o=new Date().getTime()-t.getTime(),e=Math.floor(o/(1e3*60*60)),n=Math.floor(o/(1e3*60*60*24));return e<1?`${Math.floor(o/6e4)}m ago`:e<48?`${e}h ago`:n<=14?`${n}d ago`:t.toLocaleDateString("en-US",{month:"short",day:"numeric"})}export function formatDateAbsolute(t){return t.toLocaleString("en-US",{year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}
@@ -0,0 +1 @@
1
+ export function shortenPath(e,f){if(e.length<=f)return e;const i=Math.max(f,20);if(e.length<=i)return e;const t=e.split("/");if(t.length===1){const n=Math.floor((i-1)/2);return e.slice(0,n)+"\u2026"+e.slice(-(i-n-1))}const l=t[t.length-1],h=t[0],o="/\u2026/";if(h.length+o.length+l.length>i){const n=i-2;if(l.length>n){const r=Math.floor((n-1)/2);return"\u2026/"+l.slice(0,r)+"\u2026"+l.slice(-(n-r-1))}return"\u2026/"+l}let c=h,s=1;for(;s<t.length-1;){const n=t[s],r=c+"/"+n;if(r.length+o.length+l.length<=i)c=r,s++;else break}return s===t.length-1?e:c+o+l}
@@ -0,0 +1 @@
1
+ import{getFileListSectionCounts as h}from"./fileCategories.js";export{getFileListSectionCounts}from"./fileCategories.js";export function getFileListTotalRows(o){const{modifiedCount:t,untrackedCount:i,stagedCount:n}=h(o);let r=0;return t>0&&(r+=1+t),i>0&&(t>0&&(r+=1),r+=1+i),n>0&&((t>0||i>0)&&(r+=1),r+=1+n),r}export function calculatePaneHeights(o,t,i=.4){const n=getFileListTotalRows(o),r=3,a=Math.floor(t*i),s=Math.max(r,Math.min(n,a)),f=t-s;return{topPaneHeight:s,bottomPaneHeight:f}}export function getRowForFileIndex(o,t,i,n){let r=0;if(o<t)return 1+o;t>0&&(r+=1+t);const a=t;if(o<a+i){const g=o-a;return t>0&&(r+=1),r+1+g}i>0&&(t>0&&(r+=1),r+=1+i);const s=t+i,f=o-s;return(t>0||i>0)&&(r+=1),r+1+f}export function calculateScrollOffset(o,t,i){return o<t?Math.max(0,o-1):o>=t+i?o-i+1:t}
@@ -0,0 +1 @@
1
+ import{categorizeFiles as d}from"./fileCategories.js";export function calculatePaneBoundaries(t,e,r,u=1){const l=u+2,i=u+1+t,f=i+1,a=i+2,s=a+e-1;return{stagingPaneStart:l,fileListEnd:i,separatorRow:f,diffPaneStart:a,diffPaneEnd:s,footerRow:r}}export function getClickedFileIndex(t,e,r,u,l){if(t<u+1||t>l)return-1;const i=t-4+e,{modified:f,untracked:a,staged:s}=d(r);let n=0,c=0;if(f.length>0){n++;for(let o=0;o<f.length;o++){if(i===n)return c;n++,c++}}if(a.length>0){f.length>0&&n++,n++;for(let o=0;o<a.length;o++){if(i===n)return c;n++,c++}}if(s.length>0){(f.length>0||a.length>0)&&n++,n++;for(let o=0;o<s.length;o++){if(i===n)return c;n++,c++}}return-1}export function getTabBoundaries(t){const e=t-39;return{diffStart:e,diffEnd:e+6,commitStart:e+8,commitEnd:e+16,historyStart:e+18,historyEnd:e+27,compareStart:e+29,compareEnd:e+38}}export function getClickedTab(t,e){const r=getTabBoundaries(e);return t>=r.diffStart&&t<=r.diffEnd?"diff":t>=r.commitStart&&t<=r.commitEnd?"commit":t>=r.historyStart&&t<=r.historyEnd?"history":t>=r.compareStart&&t<=r.compareEnd?"compare":null}export function isButtonAreaClick(t){return t<=6}export function isInPane(t,e,r){return t>=e&&t<=r}export function getFooterLeftClick(t){return t>=1&&t<=9?"hotkeys":t>=13&&t<=20?"mouse-mode":t>=24&&t<=38?"auto-tab":null}
@@ -0,0 +1,3 @@
1
+ import{isDisplayableDiffLine as s}from"./diffFilters.js";import{formatDateAbsolute as f}from"./formatDate.js";export function getCommitIndexFromRow(e,t,n,i=0){const o=e+i;return o<0||o>=t.length?-1:o}export function getHistoryTotalRows(e,t){return e.length}export function getHistoryRowOffset(e,t,n){return t}export function buildHistoryDiffRows(e,t){const n=[];if(e){n.push({type:"commit-header",content:`commit ${e.hash}`}),n.push({type:"commit-header",content:`Author: ${e.author}`}),n.push({type:"commit-header",content:`Date: ${f(e.date)}`}),n.push({type:"spacer"});const i=e.message.split(`
2
+ `);for(const o of i)n.push({type:"commit-message",content:` ${o}`});n.push({type:"spacer"})}if(t)for(const i of t.lines)s(i)&&n.push({type:"diff-line",diffLine:i});return n}export function getHistoryDiffTotalRows(e,t){return buildHistoryDiffRows(e,t).length}export function buildCombinedCompareDiff(e){if(!e||e.files.length===0)return{raw:"",lines:[]};const t=[],n=[];for(const i of e.files){for(const o of i.diff.lines)t.push(o);n.push(i.diff.raw)}return{raw:n.join(`
3
+ `),lines:t}}export function getCompareDiffTotalRows(e){return buildCombinedCompareDiff(e).lines.filter(s).length}export function getFileScrollOffset(e,t){if(!e||t<0||t>=e.files.length)return 0;const n=buildCombinedCompareDiff(e);let i=0,o=0;for(const r of n.lines){if(r.type==="header"&&r.content.startsWith("diff --git")){if(o===t)return i;o++}s(r)&&i++}return 0}export function getCompareItemIndexFromRow(e,t,n,i=!0,o=!0){let r=0;if(t>0){if(e===r)return-1;if(r++,i){if(e<r+t)return e-r;r+=t}}if(n>0){if(t>0){if(e===r)return-1;r++}if(e===r)return-1;if(r++,o&&e<r+n)return t+(e-r)}return-1}
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "diffstalker",
3
+ "version": "0.1.0",
4
+ "description": "Terminal application that displays git diff/status for directories",
5
+ "author": "yogh-io",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/yogh-io/diffstalker.git"
10
+ },
11
+ "homepage": "https://github.com/yogh-io/diffstalker#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/yogh-io/diffstalker/issues"
14
+ },
15
+ "type": "module",
16
+ "main": "dist/index.js",
17
+ "bin": {
18
+ "diffstalker": "bin/diffstalker"
19
+ },
20
+ "scripts": {
21
+ "dev": "tsx src/index.tsx",
22
+ "build": "tsc",
23
+ "build:prod": "tsc && node scripts/minify.js",
24
+ "bundle": "npm run build:prod && ncc build dist/index.js -o dist/bundle -m",
25
+ "start": "node dist/index.js",
26
+ "start:bundle": "node dist/bundle/index.js",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "lint": "eslint src/",
30
+ "lint:fix": "eslint src/ --fix",
31
+ "format": "prettier --write src/",
32
+ "format:check": "prettier --check src/",
33
+ "prepublishOnly": "npm run build:prod"
34
+ },
35
+ "keywords": [
36
+ "git",
37
+ "diff",
38
+ "terminal",
39
+ "tui",
40
+ "cli"
41
+ ],
42
+ "engines": {
43
+ "node": ">=18.0.0"
44
+ },
45
+ "dependencies": {
46
+ "chokidar": "^4.0.3",
47
+ "emphasize": "^7.0.0",
48
+ "fast-diff": "^1.3.0",
49
+ "ink": "^6.6.0",
50
+ "ink-text-input": "^6.0.0",
51
+ "react": "^19.2.0",
52
+ "simple-git": "^3.27.0",
53
+ "string-width": "^8.1.0"
54
+ },
55
+ "devDependencies": {
56
+ "@eslint/js": "^9.39.2",
57
+ "@types/node": "^22.10.7",
58
+ "@types/react": "^19.2.0",
59
+ "@vercel/ncc": "^0.38.4",
60
+ "esbuild": "^0.27.2",
61
+ "eslint": "^9.39.2",
62
+ "eslint-config-prettier": "^10.1.8",
63
+ "eslint-plugin-react-hooks": "^7.0.1",
64
+ "prettier": "^3.8.0",
65
+ "tsx": "^4.19.2",
66
+ "typescript": "^5.7.3",
67
+ "typescript-eslint": "^8.53.1",
68
+ "vitest": "^2.1.0"
69
+ }
70
+ }