Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d974a36c93 | |||
| dd4544b2d0 | |||
| a8303b51fa | |||
|
|
ba956170e9 | ||
|
|
2adb36d497 | ||
|
|
0e975c1ae9 | ||
|
|
5d68bc8f02 | ||
|
|
a6d7375cf8 |
|
|
@ -10,7 +10,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src= "https://github.com/user-attachments/assets/2cecb580-fe35-47ae-886b-8315226d122b" alt="difi_demo" />
|
<img src= "https://github.com/user-attachments/assets/3695cfd2-148c-463d-9630-547d152adde0" alt="difi_demo" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Why difi?
|
## Why difi?
|
||||||
|
|
@ -172,3 +172,4 @@ Contributions are especially welcome in:
|
||||||
|
|
||||||
<p align="center"> Made with ❤️ by <a href="https://github.com/oug-t">oug-t</a> </p>
|
<p align="center"> Made with ❤️ by <a href="https://github.com/oug-t">oug-t</a> </p>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ var version = "dev"
|
||||||
func main() {
|
func main() {
|
||||||
showVersion := flag.Bool("version", false, "Show version")
|
showVersion := flag.Bool("version", false, "Show version")
|
||||||
plain := flag.Bool("plain", false, "Print a plain, non-interactive summary and exit")
|
plain := flag.Bool("plain", false, "Print a plain, non-interactive summary and exit")
|
||||||
|
flat := flag.Bool("flat", false, "Start in flat file list mode")
|
||||||
|
|
||||||
flag.Usage = func() {
|
flag.Usage = func() {
|
||||||
w := os.Stderr
|
w := os.Stderr
|
||||||
|
|
@ -52,6 +53,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := config.Load()
|
cfg := config.Load()
|
||||||
|
cfg.Flat = *flat
|
||||||
|
|
||||||
p := tea.NewProgram(ui.NewModel(cfg, target), tea.WithAltScreen())
|
p := tea.NewProgram(ui.NewModel(cfg, target), tea.WithAltScreen())
|
||||||
if _, err := p.Run(); err != nil {
|
if _, err := p.Run(); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
UI UIConfig
|
UI UIConfig
|
||||||
|
Flat bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type UIConfig struct {
|
type UIConfig struct {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/list"
|
"github.com/charmbracelet/bubbles/list"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FileTree holds the state of the entire file graph.
|
// FileTree holds the state of the entire file graph.
|
||||||
|
|
@ -32,6 +33,7 @@ type TreeItem struct {
|
||||||
Depth int
|
Depth int
|
||||||
Expanded bool
|
Expanded bool
|
||||||
Icon string
|
Icon string
|
||||||
|
Flat bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement list.Item interface
|
// Implement list.Item interface
|
||||||
|
|
@ -104,6 +106,40 @@ func (t *FileTree) Items() []list.Item {
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FlatItems returns all leaf (file) nodes as a flat list with full paths.
|
||||||
|
func (t *FileTree) FlatItems() []list.Item {
|
||||||
|
var items []list.Item
|
||||||
|
collectLeaves(t.Root, &items)
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectLeaves recursively collects all file (non-directory) nodes.
|
||||||
|
func collectLeaves(node *Node, items *[]list.Item) {
|
||||||
|
children := make([]*Node, 0, len(node.Children))
|
||||||
|
for _, child := range node.Children {
|
||||||
|
children = append(children, child)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(children, func(i, j int) bool {
|
||||||
|
return strings.ToLower(children[i].FullPath) < strings.ToLower(children[j].FullPath)
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, child := range children {
|
||||||
|
if child.IsDir {
|
||||||
|
collectLeaves(child, items)
|
||||||
|
} else {
|
||||||
|
*items = append(*items, TreeItem{
|
||||||
|
Name: child.Name,
|
||||||
|
FullPath: child.FullPath,
|
||||||
|
IsDir: false,
|
||||||
|
Depth: 0,
|
||||||
|
Icon: getIcon(child.Name, false),
|
||||||
|
Flat: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// flatten recursively builds the list, respecting expansion state.
|
// flatten recursively builds the list, respecting expansion state.
|
||||||
func flatten(node *Node, items *[]list.Item) {
|
func flatten(node *Node, items *[]list.Item) {
|
||||||
// Collect children to sort
|
// Collect children to sort
|
||||||
|
|
@ -189,3 +225,22 @@ func getIcon(name string, isDir bool) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShortenPath truncates a path from the left to fit within maxWidth display columns.
|
||||||
|
// "internal/tree/tree.go" → "…nal/tree/tree.go"
|
||||||
|
func ShortenPath(path string, maxWidth int) string {
|
||||||
|
if lipgloss.Width(path) <= maxWidth {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
ellipsis := "…"
|
||||||
|
avail := maxWidth - lipgloss.Width(ellipsis)
|
||||||
|
if avail <= 0 {
|
||||||
|
return ellipsis
|
||||||
|
}
|
||||||
|
// Trim runes from the left until the remainder fits
|
||||||
|
runes := []rune(path)
|
||||||
|
for len(runes) > 0 && lipgloss.Width(string(runes)) > avail {
|
||||||
|
runes = runes[1:]
|
||||||
|
}
|
||||||
|
return ellipsis + string(runes)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"github.com/charmbracelet/bubbles/list"
|
"github.com/charmbracelet/bubbles/list"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/charmbracelet/x/ansi"
|
||||||
"github.com/oug-t/difi/internal/tree"
|
"github.com/oug-t/difi/internal/tree"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -24,7 +25,15 @@ func (d TreeDelegate) Render(w io.Writer, m list.Model, index int, item list.Ite
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
title := i.Title()
|
var title string
|
||||||
|
if i.Flat {
|
||||||
|
iconWidth := lipgloss.Width(i.Icon) + 1 // icon + space
|
||||||
|
path := tree.ShortenPath(i.FullPath, m.Width()-2-iconWidth)
|
||||||
|
title = fmt.Sprintf("%s %s", i.Icon, path)
|
||||||
|
} else {
|
||||||
|
title = i.Title()
|
||||||
|
}
|
||||||
|
title = ansi.Truncate(title, m.Width()-2, "…")
|
||||||
|
|
||||||
if index == m.Index() {
|
if index == m.Index() {
|
||||||
style := lipgloss.NewStyle().
|
style := lipgloss.NewStyle().
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,9 @@ type Model struct {
|
||||||
statsAdded int
|
statsAdded int
|
||||||
statsDeleted int
|
statsDeleted int
|
||||||
|
|
||||||
|
currentFileAdded int
|
||||||
|
currentFileDeleted int
|
||||||
|
|
||||||
diffContent string
|
diffContent string
|
||||||
diffLines []string
|
diffLines []string
|
||||||
diffCursor int
|
diffCursor int
|
||||||
|
|
@ -53,8 +56,10 @@ type Model struct {
|
||||||
inputBuffer string
|
inputBuffer string
|
||||||
pendingZ bool
|
pendingZ bool
|
||||||
|
|
||||||
focus Focus
|
focus Focus
|
||||||
showHelp bool
|
showHelp bool
|
||||||
|
flatMode bool
|
||||||
|
treeHidden bool
|
||||||
|
|
||||||
width, height int
|
width, height int
|
||||||
}
|
}
|
||||||
|
|
@ -65,7 +70,12 @@ func NewModel(cfg config.Config, targetBranch string) Model {
|
||||||
files, _ := git.ListChangedFiles(targetBranch)
|
files, _ := git.ListChangedFiles(targetBranch)
|
||||||
|
|
||||||
t := tree.New(files)
|
t := tree.New(files)
|
||||||
items := t.Items()
|
var items []list.Item
|
||||||
|
if cfg.Flat {
|
||||||
|
items = t.FlatItems()
|
||||||
|
} else {
|
||||||
|
items = t.Items()
|
||||||
|
}
|
||||||
|
|
||||||
delegate := TreeDelegate{Focused: true}
|
delegate := TreeDelegate{Focused: true}
|
||||||
l := list.New(items, delegate, 0, 0)
|
l := list.New(items, delegate, 0, 0)
|
||||||
|
|
@ -87,6 +97,7 @@ func NewModel(cfg config.Config, targetBranch string) Model {
|
||||||
targetBranch: targetBranch,
|
targetBranch: targetBranch,
|
||||||
repoName: git.GetRepoName(),
|
repoName: git.GetRepoName(),
|
||||||
showHelp: false,
|
showHelp: false,
|
||||||
|
flatMode: cfg.Flat,
|
||||||
inputBuffer: "",
|
inputBuffer: "",
|
||||||
pendingZ: false,
|
pendingZ: false,
|
||||||
}
|
}
|
||||||
|
|
@ -192,6 +203,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "tab":
|
case "tab":
|
||||||
|
if m.treeHidden {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
if m.focus == FocusTree {
|
if m.focus == FocusTree {
|
||||||
if item, ok := m.fileList.SelectedItem().(tree.TreeItem); ok && item.IsDir {
|
if item, ok := m.fileList.SelectedItem().(tree.TreeItem); ok && item.IsDir {
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
@ -214,12 +228,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.inputBuffer = ""
|
m.inputBuffer = ""
|
||||||
|
|
||||||
case "h", "[", "ctrl+h", "left":
|
case "h", "[", "ctrl+h", "left":
|
||||||
m.focus = FocusTree
|
if !m.treeHidden {
|
||||||
m.updateTreeFocus()
|
m.focus = FocusTree
|
||||||
|
m.updateTreeFocus()
|
||||||
|
}
|
||||||
m.inputBuffer = ""
|
m.inputBuffer = ""
|
||||||
|
|
||||||
case "enter":
|
case "enter":
|
||||||
if m.focus == FocusTree {
|
if m.focus == FocusTree && !m.flatMode {
|
||||||
if i, ok := m.fileList.SelectedItem().(tree.TreeItem); ok && i.IsDir {
|
if i, ok := m.fileList.SelectedItem().(tree.TreeItem); ok && i.IsDir {
|
||||||
m.treeState.ToggleExpand(i.FullPath)
|
m.treeState.ToggleExpand(i.FullPath)
|
||||||
m.fileList.SetItems(m.treeState.Items())
|
m.fileList.SetItems(m.treeState.Items())
|
||||||
|
|
@ -252,6 +268,49 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, openFugitive(m.selectedPath, line)
|
return m, openFugitive(m.selectedPath, line)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "f":
|
||||||
|
m.flatMode = !m.flatMode
|
||||||
|
prevPath := m.selectedPath
|
||||||
|
if m.flatMode {
|
||||||
|
m.fileList.SetItems(m.treeState.FlatItems())
|
||||||
|
} else {
|
||||||
|
m.fileList.SetItems(m.treeState.Items())
|
||||||
|
}
|
||||||
|
// Restore selection to the same file if possible
|
||||||
|
restored := false
|
||||||
|
for idx, item := range m.fileList.Items() {
|
||||||
|
if ti, ok := item.(tree.TreeItem); ok && ti.FullPath == prevPath {
|
||||||
|
m.fileList.Select(idx)
|
||||||
|
restored = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// In flat mode, auto-select the first file if nothing was restored
|
||||||
|
if m.flatMode && !restored {
|
||||||
|
if first, ok := m.fileList.Items()[0].(tree.TreeItem); ok {
|
||||||
|
m.fileList.Select(0)
|
||||||
|
m.selectedPath = first.FullPath
|
||||||
|
m.diffCursor = 0
|
||||||
|
m.diffViewport.GotoTop()
|
||||||
|
m.inputBuffer = ""
|
||||||
|
return m, git.DiffCmd(m.targetBranch, m.selectedPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.inputBuffer = ""
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case "t":
|
||||||
|
m.treeHidden = !m.treeHidden
|
||||||
|
if m.treeHidden {
|
||||||
|
m.focus = FocusDiff
|
||||||
|
} else {
|
||||||
|
m.focus = FocusTree
|
||||||
|
}
|
||||||
|
m.updateTreeFocus()
|
||||||
|
m.updateSizes()
|
||||||
|
m.inputBuffer = ""
|
||||||
|
return m, nil
|
||||||
|
|
||||||
case "z":
|
case "z":
|
||||||
if m.focus == FocusDiff {
|
if m.focus == FocusDiff {
|
||||||
m.pendingZ = true
|
m.pendingZ = true
|
||||||
|
|
@ -305,7 +364,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
m.inputBuffer = ""
|
m.inputBuffer = ""
|
||||||
|
|
||||||
case "j", "down":
|
case "j", "down", "ctrl+n":
|
||||||
keyHandled = true
|
keyHandled = true
|
||||||
count := m.getRepeatCount()
|
count := m.getRepeatCount()
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
|
|
@ -322,7 +381,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
m.inputBuffer = ""
|
m.inputBuffer = ""
|
||||||
|
|
||||||
case "k", "up":
|
case "k", "up", "ctrl+p":
|
||||||
keyHandled = true
|
keyHandled = true
|
||||||
count := m.getRepeatCount()
|
count := m.getRepeatCount()
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
|
|
@ -362,9 +421,41 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case git.DiffMsg:
|
case git.DiffMsg:
|
||||||
m.diffContent = msg.Content
|
fullLines := strings.Split(msg.Content, "\n")
|
||||||
m.diffLines = strings.Split(msg.Content, "\n")
|
|
||||||
m.diffViewport.SetContent(msg.Content)
|
var cleanLines []string
|
||||||
|
var added, deleted int
|
||||||
|
foundHunk := false
|
||||||
|
|
||||||
|
for _, line := range fullLines {
|
||||||
|
cleanLine := stripAnsi(line)
|
||||||
|
|
||||||
|
if strings.HasPrefix(cleanLine, "@@") {
|
||||||
|
foundHunk = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundHunk {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanLines = append(cleanLines, line)
|
||||||
|
|
||||||
|
// Check "+++" to avoid counting file header
|
||||||
|
if strings.HasPrefix(cleanLine, "+") && !strings.HasPrefix(cleanLine, "+++") {
|
||||||
|
added++
|
||||||
|
} else if strings.HasPrefix(cleanLine, "-") && !strings.HasPrefix(cleanLine, "---") {
|
||||||
|
deleted++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.diffLines = cleanLines
|
||||||
|
m.currentFileAdded = added
|
||||||
|
m.currentFileDeleted = deleted
|
||||||
|
|
||||||
|
newContent := strings.Join(cleanLines, "\n")
|
||||||
|
m.diffContent = newContent
|
||||||
|
m.diffViewport.SetContent(newContent)
|
||||||
|
m.diffViewport.GotoTop()
|
||||||
|
|
||||||
case git.EditorFinishedMsg:
|
case git.EditorFinishedMsg:
|
||||||
return m, git.DiffCmd(m.targetBranch, m.selectedPath)
|
return m, git.DiffCmd(m.targetBranch, m.selectedPath)
|
||||||
|
|
@ -397,7 +488,7 @@ func (m *Model) updateSizes() {
|
||||||
// 1 line Top Bar + 1 line Bottom Bar = 2 reserved
|
// 1 line Top Bar + 1 line Bottom Bar = 2 reserved
|
||||||
reservedHeight := 2
|
reservedHeight := 2
|
||||||
if m.showHelp {
|
if m.showHelp {
|
||||||
reservedHeight += 6
|
reservedHeight += 7
|
||||||
}
|
}
|
||||||
|
|
||||||
contentHeight := m.height - reservedHeight
|
contentHeight := m.height - reservedHeight
|
||||||
|
|
@ -405,20 +496,25 @@ func (m *Model) updateSizes() {
|
||||||
contentHeight = 1
|
contentHeight = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
treeWidth := int(float64(m.width) * 0.20)
|
|
||||||
if treeWidth < 20 {
|
|
||||||
treeWidth = 20
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subtract border height (2) from contentHeight
|
// Subtract border height (2) from contentHeight
|
||||||
listHeight := contentHeight - 2
|
listHeight := contentHeight - 2
|
||||||
if listHeight < 1 {
|
if listHeight < 1 {
|
||||||
listHeight = 1
|
listHeight = 1
|
||||||
}
|
}
|
||||||
m.fileList.SetSize(treeWidth, listHeight)
|
|
||||||
|
|
||||||
m.diffViewport.Width = m.width - treeWidth - 4 // border (2) + padding (2) from tree pane
|
if m.treeHidden {
|
||||||
m.diffViewport.Height = listHeight
|
m.fileList.SetSize(0, listHeight)
|
||||||
|
m.diffViewport.Width = m.width - 2 // border (2)
|
||||||
|
m.diffViewport.Height = listHeight
|
||||||
|
} else {
|
||||||
|
treeWidth := int(float64(m.width) * 0.20)
|
||||||
|
if treeWidth < 20 {
|
||||||
|
treeWidth = 20
|
||||||
|
}
|
||||||
|
m.fileList.SetSize(treeWidth, listHeight)
|
||||||
|
m.diffViewport.Width = m.width - treeWidth - 4 // border (2) + padding (2) from tree pane
|
||||||
|
m.diffViewport.Height = listHeight
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) updateTreeFocus() {
|
func (m *Model) updateTreeFocus() {
|
||||||
|
|
@ -436,7 +532,7 @@ func (m Model) View() string {
|
||||||
var mainContent string
|
var mainContent string
|
||||||
contentHeight := m.height - 2
|
contentHeight := m.height - 2
|
||||||
if m.showHelp {
|
if m.showHelp {
|
||||||
contentHeight -= 6
|
contentHeight -= 7
|
||||||
}
|
}
|
||||||
if contentHeight < 0 {
|
if contentHeight < 0 {
|
||||||
contentHeight = 0
|
contentHeight = 0
|
||||||
|
|
@ -448,8 +544,6 @@ func (m Model) View() string {
|
||||||
treeStyle := PaneStyle
|
treeStyle := PaneStyle
|
||||||
if m.focus == FocusTree {
|
if m.focus == FocusTree {
|
||||||
treeStyle = FocusedPaneStyle
|
treeStyle = FocusedPaneStyle
|
||||||
} else {
|
|
||||||
treeStyle = PaneStyle
|
|
||||||
}
|
}
|
||||||
|
|
||||||
treeView := treeStyle.Copy().
|
treeView := treeStyle.Copy().
|
||||||
|
|
@ -458,33 +552,84 @@ func (m Model) View() string {
|
||||||
Render(m.fileList.View())
|
Render(m.fileList.View())
|
||||||
|
|
||||||
var rightPaneView string
|
var rightPaneView string
|
||||||
|
|
||||||
selectedItem, ok := m.fileList.SelectedItem().(tree.TreeItem)
|
selectedItem, ok := m.fileList.SelectedItem().(tree.TreeItem)
|
||||||
|
|
||||||
if ok && selectedItem.IsDir {
|
if ok && selectedItem.IsDir {
|
||||||
rightPaneView = m.renderEmptyState(m.diffViewport.Width, m.diffViewport.Height, "Directory: "+selectedItem.Name)
|
rightPaneView = m.renderEmptyState(m.diffViewport.Width, m.diffViewport.Height, "Directory: "+selectedItem.Name)
|
||||||
} else {
|
} else {
|
||||||
var renderedDiff strings.Builder
|
var renderedDiff strings.Builder
|
||||||
|
|
||||||
|
// Reserve 2 line for the file header
|
||||||
|
viewportHeight := m.diffViewport.Height - 2
|
||||||
start := m.diffViewport.YOffset
|
start := m.diffViewport.YOffset
|
||||||
end := start + m.diffViewport.Height
|
end := start + viewportHeight
|
||||||
if end > len(m.diffLines) {
|
if end > len(m.diffLines) {
|
||||||
end = len(m.diffLines)
|
end = len(m.diffLines)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5 for line number (Width 4 + MarginRight 1), 2 for indent
|
// Calculate stats
|
||||||
|
added, deleted := 0, 0
|
||||||
|
for _, line := range m.diffLines {
|
||||||
|
clean := stripAnsi(line)
|
||||||
|
if strings.HasPrefix(clean, "+") && !strings.HasPrefix(clean, "+++") {
|
||||||
|
added++
|
||||||
|
} else if strings.HasPrefix(clean, "-") && !strings.HasPrefix(clean, "---") {
|
||||||
|
deleted++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
headerStyle := lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("240")).
|
||||||
|
Border(lipgloss.NormalBorder(), false, false, true, false).
|
||||||
|
BorderForeground(lipgloss.Color("236")).
|
||||||
|
Width(m.diffViewport.Width).
|
||||||
|
Padding(0, 1)
|
||||||
|
|
||||||
|
headerText := fmt.Sprintf("%s (+%d / -%d)", m.selectedPath, added, deleted)
|
||||||
|
header := headerStyle.Render(headerText)
|
||||||
|
|
||||||
maxLineWidth := m.diffViewport.Width - 7
|
maxLineWidth := m.diffViewport.Width - 7
|
||||||
if maxLineWidth < 1 {
|
if maxLineWidth < 1 {
|
||||||
maxLineWidth = 1
|
maxLineWidth = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := start; i < end; i++ {
|
for i := start; i < end; i++ {
|
||||||
line := ansi.Truncate(m.diffLines[i], maxLineWidth, "")
|
rawLine := m.diffLines[i]
|
||||||
|
cleanLine := stripAnsi(rawLine)
|
||||||
|
line := ansi.Truncate(rawLine, maxLineWidth, "")
|
||||||
|
|
||||||
|
// Skip Git metadata noise
|
||||||
|
if strings.HasPrefix(cleanLine, "diff --git") ||
|
||||||
|
strings.HasPrefix(cleanLine, "index ") ||
|
||||||
|
strings.HasPrefix(cleanLine, "new file mode") ||
|
||||||
|
strings.HasPrefix(cleanLine, "old mode") ||
|
||||||
|
strings.HasPrefix(cleanLine, "--- /dev/") ||
|
||||||
|
strings.HasPrefix(cleanLine, "+++ b/") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Hunk Headers
|
||||||
|
if strings.HasPrefix(cleanLine, "@@") {
|
||||||
|
content := cleanLine
|
||||||
|
if idx := strings.LastIndex(content, "@@"); idx != -1 {
|
||||||
|
content = content[idx+2:]
|
||||||
|
}
|
||||||
|
content = strings.TrimSpace(content)
|
||||||
|
|
||||||
|
if content == "" {
|
||||||
|
content = "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
divider := lipgloss.NewStyle().Foreground(lipgloss.Color("245")).Render(" › " + content)
|
||||||
|
|
||||||
|
renderedDiff.WriteString(" " + divider + "\n")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
var numStr string
|
var numStr string
|
||||||
mode := "relative"
|
mode := "relative"
|
||||||
|
|
||||||
if mode == "hidden" {
|
if mode != "hidden" {
|
||||||
numStr = ""
|
|
||||||
} else {
|
|
||||||
isCursor := (i == m.diffCursor)
|
isCursor := (i == m.diffCursor)
|
||||||
if isCursor && mode == "hybrid" {
|
if isCursor && mode == "hybrid" {
|
||||||
realLine := git.CalculateFileLine(m.diffContent, m.diffCursor)
|
realLine := git.CalculateFileLine(m.diffContent, m.diffCursor)
|
||||||
|
|
@ -505,7 +650,6 @@ func (m Model) View() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.focus == FocusDiff && i == m.diffCursor {
|
if m.focus == FocusDiff && i == m.diffCursor {
|
||||||
cleanLine := stripAnsi(line)
|
|
||||||
line = DiffSelectionStyle.Render(" " + cleanLine)
|
line = DiffSelectionStyle.Render(" " + cleanLine)
|
||||||
} else {
|
} else {
|
||||||
line = " " + line
|
line = " " + line
|
||||||
|
|
@ -516,13 +660,19 @@ func (m Model) View() string {
|
||||||
|
|
||||||
diffContentStr := strings.TrimRight(renderedDiff.String(), "\n")
|
diffContentStr := strings.TrimRight(renderedDiff.String(), "\n")
|
||||||
|
|
||||||
rightPaneView = DiffStyle.Copy().
|
diffView := DiffStyle.Copy().
|
||||||
Width(m.diffViewport.Width).
|
Width(m.diffViewport.Width).
|
||||||
Height(m.diffViewport.Height).
|
Height(viewportHeight).
|
||||||
Render(diffContentStr)
|
Render(diffContentStr)
|
||||||
|
|
||||||
|
rightPaneView = lipgloss.JoinVertical(lipgloss.Top, header, diffView)
|
||||||
}
|
}
|
||||||
|
|
||||||
mainContent = lipgloss.JoinHorizontal(lipgloss.Top, treeView, rightPaneView)
|
if m.treeHidden {
|
||||||
|
mainContent = rightPaneView
|
||||||
|
} else {
|
||||||
|
mainContent = lipgloss.JoinHorizontal(lipgloss.Top, treeView, rightPaneView)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var bottomBar string
|
var bottomBar string
|
||||||
|
|
@ -573,7 +723,7 @@ func (m Model) renderTopBar() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) viewStatusBar() string {
|
func (m Model) viewStatusBar() string {
|
||||||
shortcuts := StatusKeyStyle.Render("? Help q Quit Tab Switch")
|
shortcuts := StatusKeyStyle.Render("? Help q Quit Tab Switch f Flat t Tree")
|
||||||
return StatusBarStyle.Width(m.width).Render(shortcuts)
|
return StatusBarStyle.Width(m.width).Render(shortcuts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -592,7 +742,9 @@ func (m Model) renderHelpDrawer() string {
|
||||||
)
|
)
|
||||||
col4 := lipgloss.JoinVertical(lipgloss.Left,
|
col4 := lipgloss.JoinVertical(lipgloss.Left,
|
||||||
HelpTextStyle.Render("H/M/L Move Cursor"),
|
HelpTextStyle.Render("H/M/L Move Cursor"),
|
||||||
HelpTextStyle.Render("e Edit File"),
|
HelpTextStyle.Render("e Edit File"),
|
||||||
|
HelpTextStyle.Render("f Flat Tree"),
|
||||||
|
HelpTextStyle.Render("t Toggle Tree"),
|
||||||
)
|
)
|
||||||
|
|
||||||
return HelpDrawerStyle.Copy().
|
return HelpDrawerStyle.Copy().
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user