Skip to content

Commit 297d8d0

Browse files
committed
feat: diffstats
1 parent 9305af9 commit 297d8d0

File tree

8 files changed

+88
-67
lines changed

8 files changed

+88
-67
lines changed

main.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ func main() {
4646
fmt.Println("Error opening debug.log:", fileErr)
4747
os.Exit(1)
4848
}
49-
defer logFile.Close()
49+
defer func() {
50+
if err := logFile.Close(); err != nil {
51+
log.Fatal("failed closing log file", "err", err)
52+
}
53+
}()
5054

5155
if fileErr == nil {
5256
log.SetOutput(logFile)

pkg/config/config.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type UIConfig struct {
1616
SearchTreeWidth int `yaml:"searchTreeWidth"`
1717
Icons string `yaml:"icons"` // "nerd-fonts-status" (default), "nerd-fonts-simple", "nerd-fonts-filetype", "nerd-fonts-full", "unicode", "ascii"
1818
ColorFileNames bool `yaml:"colorFileNames"` // Color filenames by git status (default: true)
19+
ShowDiffStats bool `yaml:"showDiffStats"` // Show the amount of lines added / removed next to the file
1920
SideBySide bool `yaml:"sideBySide"` // Side-by-side diff view (default: true)
2021
}
2122

@@ -29,11 +30,12 @@ func DefaultConfig() Config {
2930
HideHeader: false,
3031
HideFooter: false,
3132
ShowFileTree: true,
32-
FileTreeWidth: 26,
33+
FileTreeWidth: 30,
3334
SearchTreeWidth: 50,
3435
Icons: "nerd-fonts-status",
3536
ColorFileNames: true,
3637
SideBySide: true,
38+
ShowDiffStats: true,
3739
},
3840
}
3941
}

pkg/filenode/file_node.go

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"charm.land/lipgloss/v2/tree"
1010
"github.com/bluekeyes/go-gitdiff/gitdiff"
1111

12+
"github.com/dlvhdr/diffnav/pkg/config"
1213
"github.com/dlvhdr/diffnav/pkg/icons"
1314
"github.com/dlvhdr/diffnav/pkg/utils"
1415
)
@@ -24,13 +25,12 @@ const (
2425
)
2526

2627
type FileNode struct {
27-
File *gitdiff.File
28-
Depth int
29-
YOffset int
30-
IconStyle string
31-
Selected bool
32-
ColorFileNames bool
33-
PanelWidth int
28+
File *gitdiff.File
29+
Depth int
30+
YOffset int
31+
Selected bool
32+
PanelWidth int
33+
Cfg config.Config
3434
}
3535

3636
func (f *FileNode) Path() string {
@@ -41,7 +41,7 @@ func (f *FileNode) Value() string {
4141
name := filepath.Base(f.Path())
4242

4343
// full has a special layout: [status icon] [filename] [file-type icon]
44-
if f.IconStyle == IconsNerdFull {
44+
if f.Cfg.UI.Icons == IconsNerdFull {
4545
return utils.RemoveReset(f.renderFullLayout(name))
4646
}
4747

@@ -54,7 +54,13 @@ func (f *FileNode) Value() string {
5454
func (f *FileNode) renderStandardLayout(name string) string {
5555
icon := f.getIcon() + " "
5656
iconWidth := lipgloss.Width(icon) + 1
57-
nameMaxWidth := f.PanelWidth - f.Depth - iconWidth
57+
58+
stats := ""
59+
if f.Cfg.UI.ShowDiffStats {
60+
stats = " " + ViewFileDiffStats(f.File, lipgloss.NewStyle())
61+
}
62+
63+
nameMaxWidth := f.PanelWidth - f.Depth - iconWidth - lipgloss.Width(stats)
5864
truncatedName := utils.TruncateString(name, nameMaxWidth)
5965
coloredIcon := lipgloss.NewStyle().Foreground(f.StatusColor()).Render(icon)
6066

@@ -68,15 +74,15 @@ func (f *FileNode) renderStandardLayout(name string) string {
6874
bgStyle = bgStyle.Width(availableWidth)
6975
}
7076
}
71-
return coloredIcon + bgStyle.Render(truncatedName)
77+
return coloredIcon + bgStyle.Render(truncatedName) + stats
7278
}
7379

74-
if f.ColorFileNames {
80+
if f.Cfg.UI.ColorFileNames {
7581
styledName := lipgloss.NewStyle().Foreground(f.StatusColor()).Render(truncatedName)
76-
return coloredIcon + styledName
82+
return coloredIcon + styledName + stats
7783
}
7884

79-
return coloredIcon + truncatedName
85+
return coloredIcon + truncatedName + stats
8086
}
8187

8288
// renderFullLayout renders: [status icon colored] [file-type icon colored] [filename]
@@ -86,10 +92,15 @@ func (f *FileNode) renderFullLayout(name string) string {
8692
fileIcon := icons.GetIcon(name, false)
8793
style := lipgloss.NewStyle().Foreground(f.StatusColor())
8894

95+
stats := ""
96+
if f.Cfg.UI.ShowDiffStats {
97+
stats = " " + ViewFileDiffStats(f.File, lipgloss.NewStyle())
98+
}
99+
89100
iconsPrefix := style.Render(statusIcon) + " " + style.Render(fileIcon) + " "
90101
iconsWidth := lipgloss.Width(statusIcon) + 1 + lipgloss.Width(fileIcon) + 1
91102

92-
nameMaxWidth := f.PanelWidth - f.Depth - iconsWidth
103+
nameMaxWidth := f.PanelWidth - f.Depth - iconsWidth - lipgloss.Width(stats)
93104
truncatedName := utils.TruncateString(name, nameMaxWidth)
94105

95106
if f.Selected {
@@ -99,19 +110,19 @@ func (f *FileNode) renderFullLayout(name string) string {
99110
bgStyle = bgStyle.Width(w)
100111
}
101112
}
102-
return iconsPrefix + bgStyle.Render(truncatedName)
113+
return iconsPrefix + bgStyle.Render(truncatedName) + stats
103114
}
104115

105-
if f.ColorFileNames {
106-
return iconsPrefix + style.Render(truncatedName)
116+
if f.Cfg.UI.ColorFileNames {
117+
return iconsPrefix + style.Render(truncatedName) + stats
107118
}
108-
return iconsPrefix + lipgloss.NewStyle().Foreground(lipgloss.Color("15")).Render(truncatedName)
119+
return iconsPrefix + lipgloss.NewStyle().Foreground(lipgloss.Color("15")).Render(truncatedName) + stats
109120
}
110121

111122
// getIcon returns the left icon based on the icon style.
112123
func (f *FileNode) getIcon() string {
113124
name := filepath.Base(f.Path())
114-
switch f.IconStyle {
125+
switch f.Cfg.UI.Icons {
115126
case IconsNerdStatus:
116127
if f.File.IsNew {
117128
return ""
@@ -177,7 +188,7 @@ func (f *FileNode) SetHidden(bool) {}
177188

178189
func (f *FileNode) SetValue(any) {}
179190

180-
func LinesCounts(file *gitdiff.File) (int64, int64) {
191+
func DiffStats(file *gitdiff.File) (int64, int64) {
181192
if file == nil {
182193
return 0, 0
183194
}
@@ -191,18 +202,29 @@ func LinesCounts(file *gitdiff.File) (int64, int64) {
191202
return added, deleted
192203
}
193204

194-
func ViewLinesCounts(added, deleted int64, base lipgloss.Style) string {
195-
return lipgloss.JoinHorizontal(
196-
lipgloss.Top,
197-
base.Foreground(lipgloss.Green).Render(fmt.Sprintf("+%d ", added)),
198-
base.Foreground(lipgloss.Red).Render(fmt.Sprintf("-%d", deleted)),
199-
)
205+
func ViewDiffStats(added, deleted int64, base lipgloss.Style) string {
206+
addedView := ""
207+
deletedView := ""
208+
209+
if added > 0 {
210+
addedView = base.Foreground(lipgloss.Green).Render(fmt.Sprintf("+%d", added))
211+
}
212+
213+
if added > 0 && deleted > 0 {
214+
addedView += " "
215+
}
216+
217+
if deleted > 0 {
218+
deletedView = base.Foreground(lipgloss.Red).Render(fmt.Sprintf("-%d", deleted))
219+
}
220+
221+
return lipgloss.JoinHorizontal(lipgloss.Top, addedView, deletedView)
200222
}
201223

202-
func ViewFileLinesCounts(file *gitdiff.File, base lipgloss.Style) string {
203-
added, deleted := LinesCounts(file)
224+
func ViewFileDiffStats(file *gitdiff.File, base lipgloss.Style) string {
225+
added, deleted := DiffStats(file)
204226

205-
return ViewLinesCounts(added, deleted, base)
227+
return ViewDiffStats(added, deleted, base)
206228
}
207229

208230
func GetFileName(file *gitdiff.File) string {

pkg/ui/panes/diffviewer/diffviewer.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ func (m Model) headerView() string {
128128
name = utils.TruncateString(name, m.Width-lipgloss.Width(prefix))
129129
top := prefix + base.Bold(true).Render(name)
130130

131-
bottom := filenode.ViewFileLinesCounts(m.file.files[0], base)
131+
bottom := filenode.ViewFileDiffStats(m.file.files[0], base)
132132

133133
return base.
134134
Width(m.Width).
@@ -145,7 +145,7 @@ func (m Model) dirHeaderView() string {
145145
name := utils.TruncateString(m.dir.path, m.Width-lipgloss.Width(prefix))
146146

147147
top := prefix + base.Bold(true).Render(name)
148-
bottom := filenode.ViewLinesCounts(m.dir.additions, m.dir.deletions, base)
148+
bottom := filenode.ViewDiffStats(m.dir.additions, m.dir.deletions, base)
149149
return base.
150150
Width(m.Width).
151151
Height(dirHeaderHeight - 1).
@@ -167,7 +167,7 @@ func (m Model) SetFilePatch(file *gitdiff.File) (Model, tea.Cmd) {
167167

168168
files := make([]*gitdiff.File, 1)
169169
files[0] = file
170-
additions, deletions := filenode.LinesCounts(file)
170+
additions, deletions := filenode.DiffStats(file)
171171
m.file = &cachedNode{
172172
path: fname,
173173
files: files,
@@ -190,7 +190,7 @@ func (m Model) SetDirPatch(dirPath string, files []*gitdiff.File) (Model, tea.Cm
190190

191191
var added, deleted int64
192192
for _, file := range files {
193-
na, nd := filenode.LinesCounts(file)
193+
na, nd := filenode.DiffStats(file)
194194
added += na
195195
deleted += nd
196196
}

pkg/ui/panes/filetree/filetree.go

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/bluekeyes/go-gitdiff/gitdiff"
1515
"github.com/charmbracelet/log"
1616

17+
"github.com/dlvhdr/diffnav/pkg/config"
1718
"github.com/dlvhdr/diffnav/pkg/constants"
1819
"github.com/dlvhdr/diffnav/pkg/dirnode"
1920
"github.com/dlvhdr/diffnav/pkg/filenode"
@@ -22,26 +23,24 @@ import (
2223
)
2324

2425
type Model struct {
25-
t tree.Model
26-
files []*gitdiff.File
27-
iconStyle string
28-
colorFileNames bool
26+
t tree.Model
27+
files []*gitdiff.File
28+
cfg config.Config
2929
}
3030

31-
func New(iconStyle string, colorFileNames bool) Model {
31+
func New(cfg config.Config) Model {
3232
t := tree.New(nil, constants.OpenFileTreeWidth, 0)
3333
t.SetCursorCharacter("")
3434
t.SetShowHelp(false)
3535
t.Enumerator(enumerator).Indenter(indenter)
3636
t.SetScrollOff(3)
3737

3838
m := Model{
39-
t: t,
40-
iconStyle: iconStyle,
41-
colorFileNames: colorFileNames,
39+
t: t,
40+
cfg: cfg,
4241
}
4342

44-
open, closed := getDirIcons(m.iconStyle)
43+
open, closed := getDirIcons(m.cfg.UI.Icons)
4544
t.SetOpenCharacter(open)
4645
t.SetClosedCharacter(closed)
4746
m.updateStyles()
@@ -105,7 +104,7 @@ func (m *Model) updateStyles() {
105104
IndenterStyle: base.Foreground(dimmed),
106105
})
107106

108-
open, closed := getDirIcons(m.iconStyle)
107+
open, closed := getDirIcons(m.cfg.UI.Icons)
109108
m.t.SetOpenCharacter(open)
110109
m.t.SetClosedCharacter(closed)
111110
}
@@ -157,23 +156,15 @@ func (m *Model) SetCursorByPath(path string) {
157156
}
158157

159158
func (m *Model) rebuildTree() {
160-
t := buildFullFileTree(m.files, options{
161-
iconStyle: m.iconStyle,
162-
colorFileNames: m.colorFileNames,
163-
})
159+
t := buildFullFileTree(m.files, m.cfg)
164160
t = collapseTree(t)
165-
t, _ = truncateTree(t, 0, 0, 0, m.iconStyle, m.colorFileNames, m.t.Width())
161+
t, _ = truncateTree(t, 0, 0, 0, m.cfg, m.t.Width())
166162
m.t.SetNodes(t)
167163
m.t.SetWidth(m.t.Width())
168164
m.updateStyles()
169165
}
170166

171-
type options struct {
172-
iconStyle string
173-
colorFileNames bool
174-
}
175-
176-
func buildFullFileTree(files []*gitdiff.File, opts options) *tree.Node {
167+
func buildFullFileTree(files []*gitdiff.File, cfg config.Config) *tree.Node {
177168
t := tree.Root(&dirnode.DirNode{FullPath: "/", Name: constants.RootName})
178169
for _, file := range files {
179170
// start from the root
@@ -209,9 +200,8 @@ func buildFullFileTree(files []*gitdiff.File, opts options) *tree.Node {
209200
var c *tree.Node
210201
if i == len(parts)-1 {
211202
node := &filenode.FileNode{
212-
File: file,
213-
IconStyle: opts.iconStyle,
214-
ColorFileNames: opts.colorFileNames,
203+
File: file,
204+
Cfg: cfg,
215205
}
216206
subTree.Child(node)
217207
} else {
@@ -298,8 +288,7 @@ func collapseTree(t *tree.Node) *tree.Node {
298288
return newT
299289
}
300290

301-
func truncateTree(t *tree.Node, depth int, numNodes int, numChildren int, iconStyle string,
302-
colorFileNames bool, width int,
291+
func truncateTree(t *tree.Node, depth int, numNodes int, numChildren int, cfg config.Config, width int,
303292
) (*tree.Node, int) {
304293
dir, ok := t.GivenValue().(*dirnode.DirNode)
305294
if !ok {
@@ -313,7 +302,7 @@ func truncateTree(t *tree.Node, depth int, numNodes int, numChildren int, iconSt
313302
numChildren++
314303
switch value := child.GivenValue().(type) {
315304
case *dirnode.DirNode:
316-
subTree, subNum := truncateTree(child, depth+1, numNodes, 0, iconStyle, colorFileNames, width)
305+
subTree, subNum := truncateTree(child, depth+1, numNodes, 0, cfg, width)
317306
numChildren += subNum
318307
numNodes += subNum + 1
319308
child.SetValue(value)
@@ -420,7 +409,7 @@ func (m *Model) ScrollDown(lines int) {
420409

421410
// SetIconStyle changes the icon style and regenerates the tree.
422411
func (m *Model) SetIconStyle(iconStyle string) {
423-
m.iconStyle = iconStyle
412+
m.cfg.UI.Icons = iconStyle
424413
if len(m.files) > 0 {
425414
m.rebuildTree()
426415
}

pkg/ui/panes/filetree/filetree_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"testing"
66

77
"github.com/bluekeyes/go-gitdiff/gitdiff"
8+
"github.com/dlvhdr/diffnav/pkg/config"
89
"github.com/dlvhdr/diffnav/pkg/constants"
910
"github.com/dlvhdr/diffnav/pkg/dirnode"
1011
"github.com/dlvhdr/diffnav/pkg/filenode"
@@ -25,7 +26,7 @@ func TestBuildFullFileTree(t *testing.T) {
2526
t.Fatal(err)
2627
}
2728

28-
tr := buildFullFileTree(files, options{})
29+
tr := buildFullFileTree(files, config.Config{})
2930
allNodes := tr.AllNodes()
3031
if len(allNodes) != 5 {
3132
t.Fatalf("expected 5 nodes, but got %d", len(allNodes))
@@ -92,7 +93,7 @@ func TestCollapseTree(t *testing.T) {
9293
t.Fatal(err)
9394
}
9495

95-
tr := buildFullFileTree(files, options{})
96+
tr := buildFullFileTree(files, config.Config{})
9697
tr = collapseTree(tr)
9798

9899
allNodes := tr.AllNodes()
@@ -156,7 +157,7 @@ func TestUncollapsableTree(t *testing.T) {
156157
t.Fatal(err)
157158
}
158159

159-
tr := buildFullFileTree(files, options{})
160+
tr := buildFullFileTree(files, config.Config{})
160161

161162
tr = collapseTree(tr)
162163
allNodes := tr.AllNodes()

pkg/ui/tui.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func New(input string, cfg config.Config) mainModel {
8282
input: input, isShowingFileTree: cfg.UI.ShowFileTree,
8383
activePanel: FileTreePanel, config: cfg, iconStyle: cfg.UI.Icons, sideBySide: cfg.UI.SideBySide,
8484
}
85-
m.fileTree = filetree.New(cfg.UI.Icons, cfg.UI.ColorFileNames)
85+
m.fileTree = filetree.New(cfg)
8686
m.fileTree.SetSize(cfg.UI.FileTreeWidth, 0)
8787
m.diffViewer = diffviewer.New(cfg.UI.SideBySide)
8888

0 commit comments

Comments
 (0)