commit
f318e7f880
|
|
@ -10,10 +10,9 @@ type UIConfig struct {
|
|||
}
|
||||
|
||||
func Load() Config {
|
||||
// Default configuration
|
||||
return Config{
|
||||
UI: UIConfig{
|
||||
LineNumbers: "relative", // Default to relative numbers (vim style)
|
||||
LineNumbers: "hybrid",
|
||||
Theme: "default",
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"github.com/charmbracelet/bubbles/list"
|
||||
)
|
||||
|
||||
// TreeItem represents a file or folder in the UI list.
|
||||
// TreeItem represents a file or folder
|
||||
type TreeItem struct {
|
||||
Path string
|
||||
FullPath string
|
||||
|
|
@ -25,16 +25,12 @@ func (i TreeItem) Title() string {
|
|||
return fmt.Sprintf("%s%s %s", indent, icon, i.Path)
|
||||
}
|
||||
|
||||
// Build converts a list of file paths into a sorted tree list.
|
||||
// Compaction is disabled to ensure tree stability.
|
||||
func Build(paths []string) []list.Item {
|
||||
// Initialize root
|
||||
root := &node{
|
||||
children: make(map[string]*node),
|
||||
isDir: true,
|
||||
}
|
||||
|
||||
// 1. Build the raw tree structure
|
||||
for _, path := range paths {
|
||||
parts := strings.Split(path, "/")
|
||||
current := root
|
||||
|
|
@ -53,14 +49,11 @@ func Build(paths []string) []list.Item {
|
|||
}
|
||||
}
|
||||
|
||||
// 2. Flatten to list items (Sorting happens here)
|
||||
var items []list.Item
|
||||
flatten(root, 0, &items)
|
||||
return items
|
||||
}
|
||||
|
||||
// -- Helpers --
|
||||
|
||||
type node struct {
|
||||
name string
|
||||
fullPath string
|
||||
|
|
@ -75,10 +68,8 @@ func flatten(n *node, depth int, items *[]list.Item) {
|
|||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
// Sort: Directories first, then alphabetical
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
a, b := n.children[keys[i]], n.children[keys[j]]
|
||||
// Folders first
|
||||
if a.isDir && !b.isDir {
|
||||
return true
|
||||
}
|
||||
|
|
@ -90,7 +81,6 @@ func flatten(n *node, depth int, items *[]list.Item) {
|
|||
|
||||
for _, k := range keys {
|
||||
child := n.children[k]
|
||||
// Add current node
|
||||
*items = append(*items, TreeItem{
|
||||
Path: child.name,
|
||||
FullPath: child.fullPath,
|
||||
|
|
@ -98,7 +88,6 @@ func flatten(n *node, depth int, items *[]list.Item) {
|
|||
Depth: depth,
|
||||
})
|
||||
|
||||
// Recurse if directory
|
||||
if child.isDir {
|
||||
flatten(child, depth+1, items)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ type Model struct {
|
|||
diffCursor int
|
||||
|
||||
inputBuffer string
|
||||
pendingZ bool
|
||||
|
||||
focus Focus
|
||||
showHelp bool
|
||||
|
|
@ -72,6 +73,7 @@ func NewModel(cfg config.Config, targetBranch string) Model {
|
|||
repoName: git.GetRepoName(),
|
||||
showHelp: false,
|
||||
inputBuffer: "",
|
||||
pendingZ: false,
|
||||
}
|
||||
|
||||
if len(items) > 0 {
|
||||
|
|
@ -118,11 +120,30 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
return m, tea.Quit
|
||||
}
|
||||
|
||||
// If list is empty, ignore other keys
|
||||
if len(m.fileTree.Items()) == 0 {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Handle z-prefix commands (zz, zt, zb)
|
||||
if m.pendingZ {
|
||||
m.pendingZ = false
|
||||
if m.focus == FocusDiff {
|
||||
switch msg.String() {
|
||||
case "z", ".":
|
||||
m.centerDiffCursor()
|
||||
case "t":
|
||||
m.diffViewport.SetYOffset(m.diffCursor)
|
||||
case "b":
|
||||
offset := m.diffCursor - m.diffViewport.Height + 1
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
m.diffViewport.SetYOffset(offset)
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if len(msg.String()) == 1 && strings.ContainsAny(msg.String(), "0123456789") {
|
||||
m.inputBuffer += msg.String()
|
||||
return m, nil
|
||||
|
|
@ -166,6 +187,62 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
return m, git.OpenEditorCmd(m.selectedPath, line)
|
||||
}
|
||||
|
||||
// Viewport trigger
|
||||
case "z":
|
||||
if m.focus == FocusDiff {
|
||||
m.pendingZ = true
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Cursor screen positioning (H, M, L)
|
||||
case "H":
|
||||
if m.focus == FocusDiff {
|
||||
m.diffCursor = m.diffViewport.YOffset
|
||||
if m.diffCursor >= len(m.diffLines) {
|
||||
m.diffCursor = len(m.diffLines) - 1
|
||||
}
|
||||
}
|
||||
|
||||
case "M":
|
||||
if m.focus == FocusDiff {
|
||||
half := m.diffViewport.Height / 2
|
||||
m.diffCursor = m.diffViewport.YOffset + half
|
||||
if m.diffCursor >= len(m.diffLines) {
|
||||
m.diffCursor = len(m.diffLines) - 1
|
||||
}
|
||||
}
|
||||
|
||||
case "L":
|
||||
if m.focus == FocusDiff {
|
||||
m.diffCursor = m.diffViewport.YOffset + m.diffViewport.Height - 1
|
||||
if m.diffCursor >= len(m.diffLines) {
|
||||
m.diffCursor = len(m.diffLines) - 1
|
||||
}
|
||||
}
|
||||
|
||||
// Page navigation
|
||||
case "ctrl+d":
|
||||
if m.focus == FocusDiff {
|
||||
halfScreen := m.diffViewport.Height / 2
|
||||
m.diffCursor += halfScreen
|
||||
if m.diffCursor >= len(m.diffLines) {
|
||||
m.diffCursor = len(m.diffLines) - 1
|
||||
}
|
||||
m.centerDiffCursor()
|
||||
}
|
||||
m.inputBuffer = ""
|
||||
|
||||
case "ctrl+u":
|
||||
if m.focus == FocusDiff {
|
||||
halfScreen := m.diffViewport.Height / 2
|
||||
m.diffCursor -= halfScreen
|
||||
if m.diffCursor < 0 {
|
||||
m.diffCursor = 0
|
||||
}
|
||||
m.centerDiffCursor()
|
||||
}
|
||||
m.inputBuffer = ""
|
||||
|
||||
case "j", "down":
|
||||
keyHandled = true
|
||||
count := m.getRepeatCount()
|
||||
|
|
@ -173,6 +250,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
if m.focus == FocusDiff {
|
||||
if m.diffCursor < len(m.diffLines)-1 {
|
||||
m.diffCursor++
|
||||
// Scroll if hitting bottom edge
|
||||
if m.diffCursor >= m.diffViewport.YOffset+m.diffViewport.Height {
|
||||
m.diffViewport.LineDown(1)
|
||||
}
|
||||
|
|
@ -190,6 +268,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
if m.focus == FocusDiff {
|
||||
if m.diffCursor > 0 {
|
||||
m.diffCursor--
|
||||
// Scroll if hitting top edge
|
||||
if m.diffCursor < m.diffViewport.YOffset {
|
||||
m.diffViewport.LineUp(1)
|
||||
}
|
||||
|
|
@ -234,6 +313,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *Model) centerDiffCursor() {
|
||||
halfScreen := m.diffViewport.Height / 2
|
||||
targetOffset := m.diffCursor - halfScreen
|
||||
if targetOffset < 0 {
|
||||
targetOffset = 0
|
||||
}
|
||||
m.diffViewport.SetYOffset(targetOffset)
|
||||
}
|
||||
|
||||
func (m *Model) updateSizes() {
|
||||
reservedHeight := 1
|
||||
if m.showHelp {
|
||||
|
|
@ -265,12 +353,11 @@ func (m Model) View() string {
|
|||
return "Loading..."
|
||||
}
|
||||
|
||||
// EMPTY STATE CHECK
|
||||
if len(m.fileTree.Items()) == 0 {
|
||||
return m.viewEmptyState()
|
||||
}
|
||||
|
||||
// 1. PANES
|
||||
// Panes
|
||||
treeStyle := PaneStyle
|
||||
if m.focus == FocusTree {
|
||||
treeStyle = FocusedPaneStyle
|
||||
|
|
@ -335,7 +422,7 @@ func (m Model) View() string {
|
|||
|
||||
mainPanes := lipgloss.JoinHorizontal(lipgloss.Top, treeView, diffView)
|
||||
|
||||
// 2. BOTTOM AREA
|
||||
// Bottom area
|
||||
repoSection := StatusKeyStyle.Render(" " + m.repoName)
|
||||
divider := StatusDividerStyle.Render("│")
|
||||
|
||||
|
|
@ -366,12 +453,12 @@ func (m Model) View() string {
|
|||
HelpTextStyle.Render("→/l Right Panel"),
|
||||
)
|
||||
col3 := lipgloss.JoinVertical(lipgloss.Left,
|
||||
HelpTextStyle.Render("Tab Switch Panel"),
|
||||
HelpTextStyle.Render("Num Motion Count"),
|
||||
HelpTextStyle.Render("C-d/u Page Dn/Up"),
|
||||
HelpTextStyle.Render("zz/zt Scroll View"),
|
||||
)
|
||||
col4 := lipgloss.JoinVertical(lipgloss.Left,
|
||||
HelpTextStyle.Render("H/M/L Move Cursor"),
|
||||
HelpTextStyle.Render("e Edit File"),
|
||||
HelpTextStyle.Render("? Close Help"),
|
||||
)
|
||||
|
||||
helpDrawer := HelpDrawerStyle.Copy().
|
||||
|
|
@ -394,17 +481,13 @@ func (m Model) View() string {
|
|||
return finalView
|
||||
}
|
||||
|
||||
// viewEmptyState renders a "Landing Page" when there are no changes
|
||||
func (m Model) viewEmptyState() string {
|
||||
// 1. Logo & Tagline
|
||||
logo := EmptyLogoStyle.Render("difi")
|
||||
desc := EmptyDescStyle.Render("A calm, focused way to review Git diffs.")
|
||||
|
||||
// 2. Status Message
|
||||
statusMsg := fmt.Sprintf("✓ No changes found against '%s'", m.targetBranch)
|
||||
status := EmptyStatusStyle.Render(statusMsg)
|
||||
|
||||
// 3. Usage Guide
|
||||
usageHeader := EmptyHeaderStyle.Render("Usage Patterns")
|
||||
|
||||
cmd1 := lipgloss.NewStyle().Foreground(ColorText).Render("difi")
|
||||
|
|
@ -423,7 +506,6 @@ func (m Model) viewEmptyState() string {
|
|||
lipgloss.JoinHorizontal(lipgloss.Left, cmd3, desc3),
|
||||
)
|
||||
|
||||
// 4. Navigation Guide
|
||||
navHeader := EmptyHeaderStyle.Render("Navigation")
|
||||
|
||||
key1 := lipgloss.NewStyle().Foreground(ColorText).Render("Tab")
|
||||
|
|
@ -432,8 +514,8 @@ func (m Model) viewEmptyState() string {
|
|||
key2 := lipgloss.NewStyle().Foreground(ColorText).Render("j / k")
|
||||
keyDesc2 := EmptyCodeStyle.Render("Move cursor")
|
||||
|
||||
key3 := lipgloss.NewStyle().Foreground(ColorText).Render("?")
|
||||
keyDesc3 := EmptyCodeStyle.Render("Toggle help")
|
||||
key3 := lipgloss.NewStyle().Foreground(ColorText).Render("zz/zt")
|
||||
keyDesc3 := EmptyCodeStyle.Render("Center/Top")
|
||||
|
||||
navBlock := lipgloss.JoinVertical(lipgloss.Left,
|
||||
navHeader,
|
||||
|
|
@ -442,10 +524,9 @@ func (m Model) viewEmptyState() string {
|
|||
lipgloss.JoinHorizontal(lipgloss.Left, key3, keyDesc3),
|
||||
)
|
||||
|
||||
// Combine blocks
|
||||
guides := lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
usageBlock,
|
||||
lipgloss.NewStyle().Width(8).Render(""), // Spacer
|
||||
lipgloss.NewStyle().Width(8).Render(""),
|
||||
navBlock,
|
||||
)
|
||||
|
||||
|
|
@ -457,7 +538,6 @@ func (m Model) viewEmptyState() string {
|
|||
guides,
|
||||
)
|
||||
|
||||
// Center vertically
|
||||
var verticalPad string
|
||||
if m.height > lipgloss.Height(content) {
|
||||
lines := (m.height - lipgloss.Height(content)) / 2
|
||||
|
|
|
|||
|
|
@ -6,18 +6,18 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
// Global Config
|
||||
// Config
|
||||
CurrentConfig config.Config
|
||||
|
||||
// -- THEME COLORS --
|
||||
// Theme colors
|
||||
ColorBorder = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"}
|
||||
ColorFocus = lipgloss.AdaptiveColor{Light: "#000000", Dark: "#E5E5E5"}
|
||||
ColorText = lipgloss.AdaptiveColor{Light: "#1F1F1F", Dark: "#F8F8F2"}
|
||||
ColorSubtle = lipgloss.AdaptiveColor{Light: "#A8A8A8", Dark: "#626262"}
|
||||
ColorCursorBg = lipgloss.AdaptiveColor{Light: "#E5E5E5", Dark: "#3E3E3E"}
|
||||
ColorAccent = lipgloss.AdaptiveColor{Light: "#00ADD8", Dark: "#00ADD8"} // Go Blue
|
||||
ColorAccent = lipgloss.AdaptiveColor{Light: "#00ADD8", Dark: "#00ADD8"} // Go blue
|
||||
|
||||
// -- PANE STYLES --
|
||||
// Pane styles
|
||||
PaneStyle = lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder(), false, true, false, false).
|
||||
BorderForeground(ColorBorder)
|
||||
|
|
@ -28,7 +28,7 @@ var (
|
|||
DiffStyle = lipgloss.NewStyle().Padding(0, 0)
|
||||
ItemStyle = lipgloss.NewStyle().PaddingLeft(2)
|
||||
|
||||
// -- LIST DELEGATE STYLES --
|
||||
// List styles
|
||||
SelectedItemStyle = lipgloss.NewStyle().
|
||||
PaddingLeft(1).
|
||||
Background(ColorCursorBg).
|
||||
|
|
@ -42,11 +42,11 @@ var (
|
|||
Bold(true).
|
||||
PaddingLeft(1)
|
||||
|
||||
// -- ICON STYLES --
|
||||
// Icon styles
|
||||
FolderIconStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#F7B96E", Dark: "#E5C07B"})
|
||||
FileIconStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#969696", Dark: "#ABB2BF"})
|
||||
|
||||
// -- DIFF VIEW STYLES --
|
||||
// Diff view styles
|
||||
LineNumberStyle = lipgloss.NewStyle().
|
||||
Foreground(ColorSubtle).
|
||||
PaddingRight(1).
|
||||
|
|
@ -56,10 +56,11 @@ var (
|
|||
Background(ColorCursorBg).
|
||||
Width(1000)
|
||||
|
||||
// -- STATUS BAR STYLES --
|
||||
// Status bar colors
|
||||
ColorBarBg = lipgloss.AdaptiveColor{Light: "#F2F2F2", Dark: "#1F1F1F"}
|
||||
ColorBarFg = lipgloss.AdaptiveColor{Light: "#6E6E6E", Dark: "#9E9E9E"}
|
||||
|
||||
// Status bar styles
|
||||
StatusBarStyle = lipgloss.NewStyle().
|
||||
Foreground(ColorBarFg).
|
||||
Background(ColorBarBg).
|
||||
|
|
@ -76,7 +77,7 @@ var (
|
|||
Background(ColorBarBg).
|
||||
Padding(0, 0)
|
||||
|
||||
// -- HELP STYLES --
|
||||
// Help styles
|
||||
HelpTextStyle = lipgloss.NewStyle().
|
||||
Foreground(ColorSubtle).
|
||||
Padding(0, 1)
|
||||
|
|
@ -86,7 +87,7 @@ var (
|
|||
BorderForeground(ColorBorder).
|
||||
Padding(1, 2)
|
||||
|
||||
// -- EMPTY STATE / LANDING PAGE STYLES --
|
||||
// Empty/landing styles
|
||||
EmptyLogoStyle = lipgloss.NewStyle().
|
||||
Foreground(ColorAccent).
|
||||
Bold(true).
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user