Skip to content

Commit 6e5d9c3

Browse files
authored
feat: expand/collapse dirs (#70)
1 parent 2d86e83 commit 6e5d9c3

7 files changed

Lines changed: 468 additions & 261 deletions

File tree

pkg/filenode/file_node.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package filenode
22

33
import (
4+
"fmt"
45
"image/color"
56
"path/filepath"
67

@@ -153,11 +154,11 @@ func (f *FileNode) getStatusIcon() string {
153154
// StatusColor returns the color for this file based on its git status.
154155
func (f *FileNode) StatusColor() color.Color {
155156
if f.File.IsNew {
156-
return lipgloss.Color("2") // green
157+
return lipgloss.Green
157158
} else if f.File.IsDelete {
158-
return lipgloss.Color("1") // red
159+
return lipgloss.Red
159160
}
160-
return lipgloss.Color("3") // yellow/orange
161+
return lipgloss.Yellow
161162
}
162163

163164
func (f *FileNode) String() string {
@@ -176,6 +177,31 @@ func (f *FileNode) SetHidden(bool) {}
176177

177178
func (f *FileNode) SetValue(any) {}
178179

180+
func LinesCounts(file *gitdiff.File) (int64, int64) {
181+
var added int64 = 0
182+
var deleted int64 = 0
183+
frags := file.TextFragments
184+
for _, frag := range frags {
185+
added += frag.LinesAdded
186+
deleted += frag.LinesDeleted
187+
}
188+
return added, deleted
189+
}
190+
191+
func ViewLinesCounts(added, deleted int64, base lipgloss.Style) string {
192+
return lipgloss.JoinHorizontal(
193+
lipgloss.Top,
194+
base.Foreground(lipgloss.Green).Render(fmt.Sprintf("+%d ", added)),
195+
base.Foreground(lipgloss.Red).Render(fmt.Sprintf("-%d", deleted)),
196+
)
197+
}
198+
199+
func ViewFileLinesCounts(file *gitdiff.File, base lipgloss.Style) string {
200+
added, deleted := LinesCounts(file)
201+
202+
return ViewLinesCounts(added, deleted, base)
203+
}
204+
179205
func GetFileName(file *gitdiff.File) string {
180206
if file.NewName != "" {
181207
return file.NewName

pkg/ui/common/styles.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package common
2+
3+
import (
4+
"fmt"
5+
"image/color"
6+
7+
"charm.land/lipgloss/v2"
8+
)
9+
10+
type Key int
11+
12+
// Available colors.
13+
const (
14+
Selected Key = iota
15+
DarkerSelected
16+
)
17+
18+
var Colors = map[Key]color.RGBA{
19+
Selected: {R: 0x2d, G: 0x2c, B: 0x35, A: 0xFF}, // "#2d2c35"
20+
DarkerSelected: {R: 0x20, G: 0x1F, B: 0x26, A: 0xFF}, // "#201F26"
21+
}
22+
23+
var BgStyles = map[Key]lipgloss.Style{
24+
Selected: lipgloss.NewStyle().Background(Colors[Selected]),
25+
DarkerSelected: lipgloss.NewStyle().Background(Colors[DarkerSelected]),
26+
}
27+
28+
// lipglossColorToHex converts a color.Color to hex string
29+
func LipglossColorToHex(c color.Color) string {
30+
r, g, b, _ := c.RGBA()
31+
return fmt.Sprintf("#%02x%02x%02x", r>>8, g>>8, b>>8)
32+
}

pkg/ui/keys.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package ui
33
import "charm.land/bubbles/v2/key"
44

55
type KeyMap struct {
6+
ExpandNode key.Binding
7+
CollapseNode key.Binding
8+
ToggleNode key.Binding
69
Up key.Binding
710
Down key.Binding
811
CtrlD key.Binding
@@ -18,6 +21,18 @@ type KeyMap struct {
1821
}
1922

2023
var keys = &KeyMap{
24+
ExpandNode: key.NewBinding(
25+
key.WithKeys("l"),
26+
key.WithHelp("l", "expand"),
27+
),
28+
CollapseNode: key.NewBinding(
29+
key.WithKeys("h"),
30+
key.WithHelp("h", "collapse"),
31+
),
32+
ToggleNode: key.NewBinding(
33+
key.WithKeys("enter"),
34+
key.WithHelp("enter", "toggle"),
35+
),
2136
Up: key.NewBinding(
2237
key.WithKeys("up", "k"),
2338
key.WithHelp("↑/k", "prev file"),

pkg/ui/panes/diffviewer/diffviewer.go

Lines changed: 98 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package diffviewer
22

33
import (
4-
"bytes"
54
"fmt"
65
"os"
76
"os/exec"
@@ -13,6 +12,8 @@ import (
1312
"github.com/bluekeyes/go-gitdiff/gitdiff"
1413
"github.com/charmbracelet/x/ansi"
1514

15+
"github.com/dlvhdr/diffnav/pkg/filenode"
16+
"github.com/dlvhdr/diffnav/pkg/icons"
1617
"github.com/dlvhdr/diffnav/pkg/ui/common"
1718
"github.com/dlvhdr/diffnav/pkg/utils"
1819
)
@@ -22,8 +23,9 @@ const dirHeaderHeight = 3
2223
type Model struct {
2324
common.Common
2425
vp viewport.Model
25-
buffer *bytes.Buffer
2626
file *gitdiff.File
27+
dir string
28+
dirFiles []*gitdiff.File
2729
sideBySide bool
2830
}
2931

@@ -68,9 +70,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
6870
}
6971

7072
func (m Model) View() string {
71-
if m.buffer == nil {
72-
return "Loading..."
73-
}
7473
return lipgloss.JoinVertical(lipgloss.Left, m.headerView(), m.vp.View())
7574
}
7675

@@ -79,37 +78,60 @@ func (m *Model) SetSize(width, height int) tea.Cmd {
7978
m.Height = height
8079
m.vp.SetWidth(m.Width)
8180
m.vp.SetHeight(m.Height - dirHeaderHeight)
82-
return diff(m.file, m.Width, m.sideBySide)
81+
return m.diff()
82+
}
83+
84+
func (m *Model) diff() tea.Cmd {
85+
if m.file != nil {
86+
return diffFile(m.file, m.Width, m.sideBySide)
87+
} else if m.dir != "" {
88+
return diffDir(m.dir, m.dirFiles, m.Width, m.sideBySide)
89+
}
90+
91+
return nil
8392
}
8493

8594
func (m Model) headerView() string {
95+
if m.dir != "" {
96+
return m.dirHeaderView()
97+
}
98+
8699
if m.file == nil {
87100
return ""
88101
}
89-
name := m.file.NewName
90-
if name == "" {
91-
name = m.file.OldName
92-
}
93-
102+
name := filenode.GetFileName(m.file)
94103
base := lipgloss.NewStyle()
95-
prefix := base.Render("") + base.Render(" ")
104+
105+
fileIcon := icons.GetIcon(name, false)
106+
prefix := base.Render(fileIcon) + base.Render(" ")
96107
name = utils.TruncateString(name, m.Width-lipgloss.Width(prefix))
97108
top := prefix + base.Bold(true).Render(name)
98109

99-
var added int64 = 0
100-
var deleted int64 = 0
101-
frags := m.file.TextFragments
102-
for _, frag := range frags {
103-
added += frag.LinesAdded
104-
deleted += frag.LinesDeleted
105-
}
110+
bottom := filenode.ViewFileLinesCounts(m.file, base)
106111

107-
bottom := lipgloss.JoinHorizontal(
108-
lipgloss.Top,
109-
base.Foreground(lipgloss.Color("2")).Render(fmt.Sprintf(" +%d ", added)),
110-
base.Foreground(lipgloss.Color("1")).Render(fmt.Sprintf("-%d", deleted)),
111-
)
112+
return base.
113+
Width(m.Width).
114+
Height(dirHeaderHeight - 1).
115+
BorderStyle(lipgloss.NormalBorder()).
116+
BorderBottom(true).
117+
BorderForeground(lipgloss.Color("8")).
118+
Render(lipgloss.JoinVertical(lipgloss.Left, top, bottom))
119+
}
120+
121+
func (m Model) dirHeaderView() string {
122+
base := lipgloss.NewStyle().Foreground(lipgloss.Blue)
123+
prefix := base.Render(" ")
124+
name := utils.TruncateString(m.dir, m.Width-lipgloss.Width(prefix))
125+
126+
var additions, deletions int64
127+
for _, file := range m.dirFiles {
128+
a, d := filenode.LinesCounts(file)
129+
additions += a
130+
deletions += d
131+
}
112132

133+
top := prefix + base.Bold(true).Render(name)
134+
bottom := filenode.ViewLinesCounts(additions, deletions, base)
113135
return base.
114136
Width(m.Width).
115137
Height(dirHeaderHeight - 1).
@@ -120,9 +142,16 @@ func (m Model) headerView() string {
120142
}
121143

122144
func (m Model) SetFilePatch(file *gitdiff.File) (Model, tea.Cmd) {
123-
m.buffer = new(bytes.Buffer)
124145
m.file = file
125-
return m, diff(m.file, m.Width, m.sideBySide)
146+
m.dir = ""
147+
return m, diffFile(m.file, m.Width, m.sideBySide)
148+
}
149+
150+
func (m Model) SetDirPatch(dirPath string, files []*gitdiff.File) (Model, tea.Cmd) {
151+
m.file = nil
152+
m.dir = dirPath
153+
m.dirFiles = files
154+
return m, diffDir(dirPath, files, m.Width, m.sideBySide)
126155
}
127156

128157
func (m *Model) GoToTop() {
@@ -132,7 +161,7 @@ func (m *Model) GoToTop() {
132161
// SetSideBySide updates the diff view mode and re-renders.
133162
func (m *Model) SetSideBySide(sideBySide bool) tea.Cmd {
134163
m.sideBySide = sideBySide
135-
return diff(m.file, m.Width, m.sideBySide)
164+
return diffFile(m.file, m.Width, m.sideBySide)
136165
}
137166

138167
// ScrollUp scrolls the viewport up by the given number of lines.
@@ -145,7 +174,7 @@ func (m *Model) ScrollDown(lines int) {
145174
m.vp.ScrollDown(lines)
146175
}
147176

148-
func diff(file *gitdiff.File, width int, sideBySidePreference bool) tea.Cmd {
177+
func diffFile(file *gitdiff.File, width int, sideBySidePreference bool) tea.Cmd {
149178
if width == 0 || file == nil {
150179
return nil
151180
}
@@ -172,6 +201,47 @@ func diff(file *gitdiff.File, width int, sideBySidePreference bool) tea.Cmd {
172201
}
173202
}
174203

204+
func diffDir(dirPath string, files []*gitdiff.File, width int, sideBySidePreference bool) tea.Cmd {
205+
if width == 0 || dirPath == "" {
206+
return nil
207+
}
208+
return func() tea.Msg {
209+
// Only use side-by-side if preference is true AND file is not new/deleted
210+
s := common.BgStyles[common.Selected]
211+
c := common.LipglossColorToHex(common.Colors[common.Selected])
212+
useSideBySide := sideBySidePreference
213+
args := []string{
214+
"--paging=never",
215+
fmt.Sprintf("--file-modified-label=%s",
216+
utils.RemoveReset(s.Foreground(lipgloss.Yellow).Render(" "))),
217+
fmt.Sprintf("--file-removed-label=%s",
218+
utils.RemoveReset(s.Foreground(lipgloss.Red).Render(" "))),
219+
fmt.Sprintf("--file-added-label=%s",
220+
utils.RemoveReset(s.Foreground(lipgloss.Green).Render(" "))),
221+
fmt.Sprintf("--file-style='%s bold %s'", c, c),
222+
fmt.Sprintf("--file-decoration-style='%s box %s'", c, c),
223+
fmt.Sprintf("-w=%d", width),
224+
fmt.Sprintf("--max-line-length=%d", width),
225+
}
226+
if useSideBySide {
227+
args = append(args, "--side-by-side")
228+
}
229+
deltac := exec.Command("delta", args...)
230+
deltac.Env = os.Environ()
231+
strs := strings.Builder{}
232+
for _, file := range files {
233+
strs.WriteString(file.String())
234+
}
235+
deltac.Stdin = strings.NewReader(strs.String() + "\n")
236+
out, err := deltac.Output()
237+
if err != nil {
238+
return common.ErrMsg{Err: err}
239+
}
240+
241+
return diffContentMsg{text: string(out)}
242+
}
243+
}
244+
175245
type diffContentMsg struct {
176246
text string
177247
}

0 commit comments

Comments
 (0)