Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions pkg/filenode/file_node.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package filenode

import (
"fmt"
"image/color"
"path/filepath"

Expand Down Expand Up @@ -153,11 +154,11 @@ func (f *FileNode) getStatusIcon() string {
// StatusColor returns the color for this file based on its git status.
func (f *FileNode) StatusColor() color.Color {
if f.File.IsNew {
return lipgloss.Color("2") // green
return lipgloss.Green
} else if f.File.IsDelete {
return lipgloss.Color("1") // red
return lipgloss.Red
}
return lipgloss.Color("3") // yellow/orange
return lipgloss.Yellow
}

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

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

func LinesCounts(file *gitdiff.File) (int64, int64) {
var added int64 = 0
var deleted int64 = 0
frags := file.TextFragments
for _, frag := range frags {
added += frag.LinesAdded
deleted += frag.LinesDeleted
}
return added, deleted
}

func ViewLinesCounts(added, deleted int64, base lipgloss.Style) string {
return lipgloss.JoinHorizontal(
lipgloss.Top,
base.Foreground(lipgloss.Green).Render(fmt.Sprintf("+%d ", added)),
base.Foreground(lipgloss.Red).Render(fmt.Sprintf("-%d", deleted)),
)
}

func ViewFileLinesCounts(file *gitdiff.File, base lipgloss.Style) string {
added, deleted := LinesCounts(file)

return ViewLinesCounts(added, deleted, base)
}

func GetFileName(file *gitdiff.File) string {
if file.NewName != "" {
return file.NewName
Expand Down
32 changes: 32 additions & 0 deletions pkg/ui/common/styles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package common

import (
"fmt"
"image/color"

"charm.land/lipgloss/v2"
)

type Key int

// Available colors.
const (
Selected Key = iota
DarkerSelected
)

var Colors = map[Key]color.RGBA{
Selected: {R: 0x2d, G: 0x2c, B: 0x35, A: 0xFF}, // "#2d2c35"
DarkerSelected: {R: 0x20, G: 0x1F, B: 0x26, A: 0xFF}, // "#201F26"
}

var BgStyles = map[Key]lipgloss.Style{
Selected: lipgloss.NewStyle().Background(Colors[Selected]),
DarkerSelected: lipgloss.NewStyle().Background(Colors[DarkerSelected]),
}

// lipglossColorToHex converts a color.Color to hex string
func LipglossColorToHex(c color.Color) string {
r, g, b, _ := c.RGBA()
return fmt.Sprintf("#%02x%02x%02x", r>>8, g>>8, b>>8)
}
15 changes: 15 additions & 0 deletions pkg/ui/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package ui
import "charm.land/bubbles/v2/key"

type KeyMap struct {
ExpandNode key.Binding
CollapseNode key.Binding
ToggleNode key.Binding
Up key.Binding
Down key.Binding
CtrlD key.Binding
Expand All @@ -18,6 +21,18 @@ type KeyMap struct {
}

var keys = &KeyMap{
ExpandNode: key.NewBinding(
key.WithKeys("l"),
key.WithHelp("l", "expand"),
),
CollapseNode: key.NewBinding(
key.WithKeys("h"),
key.WithHelp("h", "collapse"),
),
ToggleNode: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "toggle"),
),
Up: key.NewBinding(
key.WithKeys("up", "k"),
key.WithHelp("↑/k", "prev file"),
Expand Down
126 changes: 98 additions & 28 deletions pkg/ui/panes/diffviewer/diffviewer.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package diffviewer

import (
"bytes"
"fmt"
"os"
"os/exec"
Expand All @@ -13,6 +12,8 @@ import (
"github.com/bluekeyes/go-gitdiff/gitdiff"
"github.com/charmbracelet/x/ansi"

"github.com/dlvhdr/diffnav/pkg/filenode"
"github.com/dlvhdr/diffnav/pkg/icons"
"github.com/dlvhdr/diffnav/pkg/ui/common"
"github.com/dlvhdr/diffnav/pkg/utils"
)
Expand All @@ -22,8 +23,9 @@ const dirHeaderHeight = 3
type Model struct {
common.Common
vp viewport.Model
buffer *bytes.Buffer
file *gitdiff.File
dir string
dirFiles []*gitdiff.File
sideBySide bool
}

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

func (m Model) View() string {
if m.buffer == nil {
return "Loading..."
}
return lipgloss.JoinVertical(lipgloss.Left, m.headerView(), m.vp.View())
}

Expand All @@ -79,37 +78,60 @@ func (m *Model) SetSize(width, height int) tea.Cmd {
m.Height = height
m.vp.SetWidth(m.Width)
m.vp.SetHeight(m.Height - dirHeaderHeight)
return diff(m.file, m.Width, m.sideBySide)
return m.diff()
}

func (m *Model) diff() tea.Cmd {
if m.file != nil {
return diffFile(m.file, m.Width, m.sideBySide)
} else if m.dir != "" {
return diffDir(m.dir, m.dirFiles, m.Width, m.sideBySide)
}

return nil
}

func (m Model) headerView() string {
if m.dir != "" {
return m.dirHeaderView()
}

if m.file == nil {
return ""
}
name := m.file.NewName
if name == "" {
name = m.file.OldName
}

name := filenode.GetFileName(m.file)
base := lipgloss.NewStyle()
prefix := base.Render("") + base.Render(" ")

fileIcon := icons.GetIcon(name, false)
prefix := base.Render(fileIcon) + base.Render(" ")
name = utils.TruncateString(name, m.Width-lipgloss.Width(prefix))
top := prefix + base.Bold(true).Render(name)

var added int64 = 0
var deleted int64 = 0
frags := m.file.TextFragments
for _, frag := range frags {
added += frag.LinesAdded
deleted += frag.LinesDeleted
}
bottom := filenode.ViewFileLinesCounts(m.file, base)

bottom := lipgloss.JoinHorizontal(
lipgloss.Top,
base.Foreground(lipgloss.Color("2")).Render(fmt.Sprintf(" +%d ", added)),
base.Foreground(lipgloss.Color("1")).Render(fmt.Sprintf("-%d", deleted)),
)
return base.
Width(m.Width).
Height(dirHeaderHeight - 1).
BorderStyle(lipgloss.NormalBorder()).
BorderBottom(true).
BorderForeground(lipgloss.Color("8")).
Render(lipgloss.JoinVertical(lipgloss.Left, top, bottom))
}

func (m Model) dirHeaderView() string {
base := lipgloss.NewStyle().Foreground(lipgloss.Blue)
prefix := base.Render(" ")
name := utils.TruncateString(m.dir, m.Width-lipgloss.Width(prefix))

var additions, deletions int64
for _, file := range m.dirFiles {
a, d := filenode.LinesCounts(file)
additions += a
deletions += d
}

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

func (m Model) SetFilePatch(file *gitdiff.File) (Model, tea.Cmd) {
m.buffer = new(bytes.Buffer)
m.file = file
return m, diff(m.file, m.Width, m.sideBySide)
m.dir = ""
return m, diffFile(m.file, m.Width, m.sideBySide)
}

func (m Model) SetDirPatch(dirPath string, files []*gitdiff.File) (Model, tea.Cmd) {
m.file = nil
m.dir = dirPath
m.dirFiles = files
return m, diffDir(dirPath, files, m.Width, m.sideBySide)
}

func (m *Model) GoToTop() {
Expand All @@ -132,7 +161,7 @@ func (m *Model) GoToTop() {
// SetSideBySide updates the diff view mode and re-renders.
func (m *Model) SetSideBySide(sideBySide bool) tea.Cmd {
m.sideBySide = sideBySide
return diff(m.file, m.Width, m.sideBySide)
return diffFile(m.file, m.Width, m.sideBySide)
}

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

func diff(file *gitdiff.File, width int, sideBySidePreference bool) tea.Cmd {
func diffFile(file *gitdiff.File, width int, sideBySidePreference bool) tea.Cmd {
if width == 0 || file == nil {
return nil
}
Expand All @@ -172,6 +201,47 @@ func diff(file *gitdiff.File, width int, sideBySidePreference bool) tea.Cmd {
}
}

func diffDir(dirPath string, files []*gitdiff.File, width int, sideBySidePreference bool) tea.Cmd {
if width == 0 || dirPath == "" {
return nil
}
return func() tea.Msg {
// Only use side-by-side if preference is true AND file is not new/deleted
s := common.BgStyles[common.Selected]
c := common.LipglossColorToHex(common.Colors[common.Selected])
useSideBySide := sideBySidePreference
args := []string{
"--paging=never",
fmt.Sprintf("--file-modified-label=%s",
utils.RemoveReset(s.Foreground(lipgloss.Yellow).Render(" "))),
fmt.Sprintf("--file-removed-label=%s",
utils.RemoveReset(s.Foreground(lipgloss.Red).Render(" "))),
fmt.Sprintf("--file-added-label=%s",
utils.RemoveReset(s.Foreground(lipgloss.Green).Render(" "))),
fmt.Sprintf("--file-style='%s bold %s'", c, c),
fmt.Sprintf("--file-decoration-style='%s box %s'", c, c),
fmt.Sprintf("-w=%d", width),
fmt.Sprintf("--max-line-length=%d", width),
}
if useSideBySide {
args = append(args, "--side-by-side")
}
deltac := exec.Command("delta", args...)
deltac.Env = os.Environ()
strs := strings.Builder{}
for _, file := range files {
strs.WriteString(file.String())
}
deltac.Stdin = strings.NewReader(strs.String() + "\n")
out, err := deltac.Output()
if err != nil {
return common.ErrMsg{Err: err}
}

return diffContentMsg{text: string(out)}
}
}

type diffContentMsg struct {
text string
}
Loading