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.
- package/.github/workflows/release.yml +40 -0
- package/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +154 -0
- package/assets/diff.png +0 -0
- package/assets/history.png +0 -0
- package/bin/diffstalker +2 -0
- package/dist/App.js +1 -0
- package/dist/components/BaseBranchPicker.js +1 -0
- package/dist/components/BottomPane.js +1 -0
- package/dist/components/CommitPanel.js +1 -0
- package/dist/components/CompareListView.js +1 -0
- package/dist/components/CompareView.js +1 -0
- package/dist/components/DiffView.js +1 -0
- package/dist/components/FileList.js +1 -0
- package/dist/components/Footer.js +1 -0
- package/dist/components/Header.js +1 -0
- package/dist/components/HistoryDiffView.js +1 -0
- package/dist/components/HistoryView.js +1 -0
- package/dist/components/HotkeysModal.js +1 -0
- package/dist/components/Modal.js +1 -0
- package/dist/components/ScrollableList.js +1 -0
- package/dist/components/ThemePicker.js +1 -0
- package/dist/components/TopPane.js +1 -0
- package/dist/config.js +2 -0
- package/dist/core/GitOperationQueue.js +1 -0
- package/dist/core/GitStateManager.js +1 -0
- package/dist/git/diff.js +10 -0
- package/dist/git/status.js +5 -0
- package/dist/hooks/useCommitFlow.js +1 -0
- package/dist/hooks/useCompareState.js +1 -0
- package/dist/hooks/useGit.js +1 -0
- package/dist/hooks/useHistoryState.js +1 -0
- package/dist/hooks/useKeymap.js +1 -0
- package/dist/hooks/useLayout.js +1 -0
- package/dist/hooks/useMouse.js +1 -0
- package/dist/hooks/useTerminalSize.js +1 -0
- package/dist/hooks/useWatcher.js +11 -0
- package/dist/index.js +43 -0
- package/dist/services/commitService.js +1 -0
- package/dist/themes.js +1 -0
- package/dist/utils/baseBranchCache.js +2 -0
- package/dist/utils/commitFormat.js +1 -0
- package/dist/utils/diffFilters.js +1 -0
- package/dist/utils/fileCategories.js +1 -0
- package/dist/utils/formatDate.js +1 -0
- package/dist/utils/formatPath.js +1 -0
- package/dist/utils/layoutCalculations.js +1 -0
- package/dist/utils/mouseCoordinates.js +1 -0
- package/dist/utils/rowCalculations.js +3 -0
- 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
|
+

|
|
6
|
+
*Stage files and review changes with word-level diff highlighting.*
|
|
7
|
+
|
|
8
|
+

|
|
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
|
package/assets/diff.png
ADDED
|
Binary file
|
|
Binary file
|
package/bin/diffstalker
ADDED
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))}
|
package/dist/git/diff.js
ADDED
|
@@ -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
|
+
}
|