Compare commits

..

No commits in common. "main" and "v0.1.5" have entirely different histories.
main ... v0.1.5

6 changed files with 25 additions and 160 deletions

View File

@ -10,7 +10,7 @@
</p> </p>
<p align="center"> <p align="center">
<img src= "https://github.com/user-attachments/assets/3695cfd2-148c-463d-9630-547d152adde0" alt="difi_demo" /> <img src= "https://github.com/user-attachments/assets/2cecb580-fe35-47ae-886b-8315226d122b" alt="difi_demo" />
</p> </p>
## Why difi? ## Why difi?
@ -172,4 +172,3 @@ 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>

View File

@ -16,7 +16,6 @@ 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
@ -53,7 +52,6 @@ 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 {

View File

@ -2,7 +2,6 @@ package config
type Config struct { type Config struct {
UI UIConfig UI UIConfig
Flat bool
} }
type UIConfig struct { type UIConfig struct {

View File

@ -7,7 +7,6 @@ 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.
@ -33,7 +32,6 @@ 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
@ -106,40 +104,6 @@ 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
@ -225,22 +189,3 @@ 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)
}

View File

@ -7,7 +7,6 @@ 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"
) )
@ -25,15 +24,7 @@ func (d TreeDelegate) Render(w io.Writer, m list.Model, index int, item list.Ite
return return
} }
var title string title := i.Title()
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().

View File

@ -58,8 +58,6 @@ type Model struct {
focus Focus focus Focus
showHelp bool showHelp bool
flatMode bool
treeHidden bool
width, height int width, height int
} }
@ -70,12 +68,7 @@ func NewModel(cfg config.Config, targetBranch string) Model {
files, _ := git.ListChangedFiles(targetBranch) files, _ := git.ListChangedFiles(targetBranch)
t := tree.New(files) t := tree.New(files)
var items []list.Item items := t.Items()
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)
@ -97,7 +90,6 @@ 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,
} }
@ -203,9 +195,6 @@ 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
@ -228,14 +217,12 @@ 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":
if !m.treeHidden {
m.focus = FocusTree m.focus = FocusTree
m.updateTreeFocus() m.updateTreeFocus()
}
m.inputBuffer = "" m.inputBuffer = ""
case "enter": case "enter":
if m.focus == FocusTree && !m.flatMode { if m.focus == FocusTree {
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())
@ -268,49 +255,6 @@ 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
@ -364,7 +308,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
m.inputBuffer = "" m.inputBuffer = ""
case "j", "down", "ctrl+n": case "j", "down":
keyHandled = true keyHandled = true
count := m.getRepeatCount() count := m.getRepeatCount()
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
@ -381,7 +325,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
m.inputBuffer = "" m.inputBuffer = ""
case "k", "up", "ctrl+p": case "k", "up":
keyHandled = true keyHandled = true
count := m.getRepeatCount() count := m.getRepeatCount()
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
@ -488,7 +432,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 += 7 reservedHeight += 6
} }
contentHeight := m.height - reservedHeight contentHeight := m.height - reservedHeight
@ -496,26 +440,21 @@ 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
} }
if m.treeHidden {
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.fileList.SetSize(treeWidth, listHeight)
m.diffViewport.Width = m.width - treeWidth - 4 // border (2) + padding (2) from tree pane m.diffViewport.Width = m.width - treeWidth - 4 // border (2) + padding (2) from tree pane
m.diffViewport.Height = listHeight m.diffViewport.Height = listHeight
} }
}
func (m *Model) updateTreeFocus() { func (m *Model) updateTreeFocus() {
m.treeDelegate.Focused = (m.focus == FocusTree) m.treeDelegate.Focused = (m.focus == FocusTree)
@ -532,7 +471,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 -= 7 contentHeight -= 6
} }
if contentHeight < 0 { if contentHeight < 0 {
contentHeight = 0 contentHeight = 0
@ -559,8 +498,8 @@ func (m Model) View() string {
} else { } else {
var renderedDiff strings.Builder var renderedDiff strings.Builder
// Reserve 2 line for the file header // Reserve 1 line for the file header
viewportHeight := m.diffViewport.Height - 2 viewportHeight := m.diffViewport.Height - 1
start := m.diffViewport.YOffset start := m.diffViewport.YOffset
end := start + viewportHeight end := start + viewportHeight
if end > len(m.diffLines) { if end > len(m.diffLines) {
@ -668,12 +607,8 @@ func (m Model) View() string {
rightPaneView = lipgloss.JoinVertical(lipgloss.Top, header, diffView) rightPaneView = lipgloss.JoinVertical(lipgloss.Top, header, diffView)
} }
if m.treeHidden {
mainContent = rightPaneView
} else {
mainContent = lipgloss.JoinHorizontal(lipgloss.Top, treeView, rightPaneView) mainContent = lipgloss.JoinHorizontal(lipgloss.Top, treeView, rightPaneView)
} }
}
var bottomBar string var bottomBar string
if m.showHelp { if m.showHelp {
@ -723,7 +658,7 @@ func (m Model) renderTopBar() string {
} }
func (m Model) viewStatusBar() string { func (m Model) viewStatusBar() string {
shortcuts := StatusKeyStyle.Render("? Help q Quit Tab Switch f Flat t Tree") shortcuts := StatusKeyStyle.Render("? Help q Quit Tab Switch")
return StatusBarStyle.Width(m.width).Render(shortcuts) return StatusBarStyle.Width(m.width).Render(shortcuts)
} }
@ -743,8 +678,6 @@ 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().