feat(ui): add flat tree mode via f or -flat
This commit is contained in:
parent
ba956170e9
commit
a8303b51fa
|
|
@ -16,6 +16,7 @@ var version = "dev"
|
|||
func main() {
|
||||
showVersion := flag.Bool("version", false, "Show version")
|
||||
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() {
|
||||
w := os.Stderr
|
||||
|
|
@ -52,6 +53,7 @@ func main() {
|
|||
}
|
||||
|
||||
cfg := config.Load()
|
||||
cfg.Flat = *flat
|
||||
|
||||
p := tea.NewProgram(ui.NewModel(cfg, target), tea.WithAltScreen())
|
||||
if _, err := p.Run(); err != nil {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package config
|
|||
|
||||
type Config struct {
|
||||
UI UIConfig
|
||||
Flat bool
|
||||
}
|
||||
|
||||
type UIConfig struct {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// FileTree holds the state of the entire file graph.
|
||||
|
|
@ -32,6 +33,7 @@ type TreeItem struct {
|
|||
Depth int
|
||||
Expanded bool
|
||||
Icon string
|
||||
Flat bool
|
||||
}
|
||||
|
||||
// Implement list.Item interface
|
||||
|
|
@ -104,6 +106,40 @@ func (t *FileTree) Items() []list.Item {
|
|||
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.
|
||||
func flatten(node *Node, items *[]list.Item) {
|
||||
// Collect children to sort
|
||||
|
|
@ -189,3 +225,22 @@ func getIcon(name string, isDir bool) string {
|
|||
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"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"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
|
||||
}
|
||||
|
||||
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() {
|
||||
style := lipgloss.NewStyle().
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ type Model struct {
|
|||
|
||||
focus Focus
|
||||
showHelp bool
|
||||
flatMode bool
|
||||
|
||||
width, height int
|
||||
}
|
||||
|
|
@ -68,7 +69,12 @@ func NewModel(cfg config.Config, targetBranch string) Model {
|
|||
files, _ := git.ListChangedFiles(targetBranch)
|
||||
|
||||
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}
|
||||
l := list.New(items, delegate, 0, 0)
|
||||
|
|
@ -90,6 +96,7 @@ func NewModel(cfg config.Config, targetBranch string) Model {
|
|||
targetBranch: targetBranch,
|
||||
repoName: git.GetRepoName(),
|
||||
showHelp: false,
|
||||
flatMode: cfg.Flat,
|
||||
inputBuffer: "",
|
||||
pendingZ: false,
|
||||
}
|
||||
|
|
@ -222,7 +229,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
m.inputBuffer = ""
|
||||
|
||||
case "enter":
|
||||
if m.focus == FocusTree {
|
||||
if m.focus == FocusTree && !m.flatMode {
|
||||
if i, ok := m.fileList.SelectedItem().(tree.TreeItem); ok && i.IsDir {
|
||||
m.treeState.ToggleExpand(i.FullPath)
|
||||
m.fileList.SetItems(m.treeState.Items())
|
||||
|
|
@ -255,6 +262,37 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
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 "z":
|
||||
if m.focus == FocusDiff {
|
||||
m.pendingZ = true
|
||||
|
|
@ -432,7 +470,7 @@ func (m *Model) updateSizes() {
|
|||
// 1 line Top Bar + 1 line Bottom Bar = 2 reserved
|
||||
reservedHeight := 2
|
||||
if m.showHelp {
|
||||
reservedHeight += 6
|
||||
reservedHeight += 7
|
||||
}
|
||||
|
||||
contentHeight := m.height - reservedHeight
|
||||
|
|
@ -471,7 +509,7 @@ func (m Model) View() string {
|
|||
var mainContent string
|
||||
contentHeight := m.height - 2
|
||||
if m.showHelp {
|
||||
contentHeight -= 6
|
||||
contentHeight -= 7
|
||||
}
|
||||
if contentHeight < 0 {
|
||||
contentHeight = 0
|
||||
|
|
@ -658,7 +696,7 @@ func (m Model) renderTopBar() string {
|
|||
}
|
||||
|
||||
func (m Model) viewStatusBar() string {
|
||||
shortcuts := StatusKeyStyle.Render("? Help q Quit Tab Switch")
|
||||
shortcuts := StatusKeyStyle.Render("? Help q Quit Tab Switch f Flat")
|
||||
return StatusBarStyle.Width(m.width).Render(shortcuts)
|
||||
}
|
||||
|
||||
|
|
@ -678,6 +716,7 @@ func (m Model) renderHelpDrawer() string {
|
|||
col4 := lipgloss.JoinVertical(lipgloss.Left,
|
||||
HelpTextStyle.Render("H/M/L Move Cursor"),
|
||||
HelpTextStyle.Render("e Edit File"),
|
||||
HelpTextStyle.Render("f Flat Tree"),
|
||||
)
|
||||
|
||||
return HelpDrawerStyle.Copy().
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user