Polisehd ui
This commit is contained in:
parent
d4ddcc6e98
commit
c9589fabf1
|
|
@ -9,21 +9,28 @@ import (
|
|||
|
||||
type Config struct {
|
||||
Colors struct {
|
||||
Border string `yaml:"border"`
|
||||
Focus string `yaml:"focus"`
|
||||
LineNumber string `yaml:"line_number"`
|
||||
Border string `yaml:"border"`
|
||||
Focus string `yaml:"focus"`
|
||||
LineNumber string `yaml:"line_number"`
|
||||
DiffSelectionBg string `yaml:"diff_selection_bg"` // New config
|
||||
} `yaml:"colors"`
|
||||
UI struct {
|
||||
LineNumbers string `yaml:"line_numbers"` // "absolute", "relative", "hybrid", "hidden"
|
||||
ShowGuide bool `yaml:"show_guide"` // The vertical separation line
|
||||
LineNumbers string `yaml:"line_numbers"`
|
||||
ShowGuide bool `yaml:"show_guide"`
|
||||
} `yaml:"ui"`
|
||||
}
|
||||
|
||||
func DefaultConfig() Config {
|
||||
var c Config
|
||||
c.Colors.Border = "#D9DCCF"
|
||||
c.Colors.Focus = "#000000" // Default neutral focus
|
||||
c.Colors.Focus = "#6e7781"
|
||||
c.Colors.LineNumber = "#808080"
|
||||
|
||||
// Default: "Neutral Light Transparent Blue"
|
||||
// Dark Mode: Deep subtle blue-grey | Light Mode: Very faint blue
|
||||
// We only set one default here, but AdaptiveColor handles the split in styles.go
|
||||
c.Colors.DiffSelectionBg = "" // Empty means use internal defaults
|
||||
|
||||
c.UI.LineNumbers = "hybrid"
|
||||
c.UI.ShowGuide = true
|
||||
return c
|
||||
|
|
@ -31,7 +38,6 @@ func DefaultConfig() Config {
|
|||
|
||||
func Load() Config {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return cfg
|
||||
|
|
@ -43,7 +49,6 @@ func Load() Config {
|
|||
return cfg
|
||||
}
|
||||
|
||||
// Parse YAML
|
||||
_ = yaml.Unmarshal(data, &cfg)
|
||||
return cfg
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ func (i TreeItem) Title() string {
|
|||
return fmt.Sprintf("%s%s %s", indent, icon, i.Path)
|
||||
}
|
||||
|
||||
// Build converts a list of file paths into a compacted, sorted tree list.
|
||||
// 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 {
|
||||
// FIX 1: Initialize root as a directory so logic works,
|
||||
// but we won't compact the root itself.
|
||||
// Initialize root
|
||||
root := &node{
|
||||
children: make(map[string]*node),
|
||||
isDir: true,
|
||||
|
|
@ -53,13 +53,7 @@ func Build(paths []string) []list.Item {
|
|||
}
|
||||
}
|
||||
|
||||
// FIX 2: Do NOT compact the root node itself (which would hide top-level folders).
|
||||
// Instead, compact each top-level child individually.
|
||||
for _, child := range root.children {
|
||||
compact(child)
|
||||
}
|
||||
|
||||
// 3. Flatten to list items
|
||||
// 2. Flatten to list items (Sorting happens here)
|
||||
var items []list.Item
|
||||
flatten(root, 0, &items)
|
||||
return items
|
||||
|
|
@ -74,41 +68,14 @@ type node struct {
|
|||
isDir bool
|
||||
}
|
||||
|
||||
// compact recursively merges directories that contain only a single directory child.
|
||||
func compact(n *node) {
|
||||
if !n.isDir {
|
||||
return
|
||||
}
|
||||
|
||||
// Compact children first (bottom-up traversal)
|
||||
for _, child := range n.children {
|
||||
compact(child)
|
||||
}
|
||||
|
||||
// Logic: If I am a directory, and I have exactly 1 child, and that child is also a directory...
|
||||
if len(n.children) == 1 {
|
||||
var child *node
|
||||
for _, c := range n.children {
|
||||
child = c
|
||||
break
|
||||
}
|
||||
|
||||
if child.isDir {
|
||||
// Merge child into parent
|
||||
// e.g. "internal" + "ui" becomes "internal/ui"
|
||||
n.name = filepath.Join(n.name, child.name)
|
||||
n.fullPath = child.fullPath
|
||||
n.children = child.children // Inherit grandchildren
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// flatten recursively converts the tree into a linear list, sorting children by type and name.
|
||||
func flatten(n *node, depth int, items *[]list.Item) {
|
||||
keys := make([]string, 0, len(n.children))
|
||||
for k := range n.children {
|
||||
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
|
||||
|
|
@ -123,6 +90,7 @@ 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,
|
||||
|
|
@ -130,6 +98,7 @@ func flatten(n *node, depth int, items *[]list.Item) {
|
|||
Depth: depth,
|
||||
})
|
||||
|
||||
// Recurse if directory
|
||||
if child.isDir {
|
||||
flatten(child, depth+1, items)
|
||||
}
|
||||
|
|
|
|||
113
internal/ui/delegate.go
Normal file
113
internal/ui/delegate.go
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/oug-t/difi/internal/tree"
|
||||
)
|
||||
|
||||
type TreeDelegate struct {
|
||||
Focused bool
|
||||
}
|
||||
|
||||
func (d TreeDelegate) Height() int { return 1 }
|
||||
func (d TreeDelegate) Spacing() int { return 0 }
|
||||
func (d TreeDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
|
||||
|
||||
func (d TreeDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) {
|
||||
i, ok := item.(tree.TreeItem)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 1. Setup Indentation
|
||||
indentSize := i.Depth * 2
|
||||
indent := strings.Repeat(" ", indentSize)
|
||||
|
||||
// 2. Get Icon and Raw Name
|
||||
iconStr, iconStyle := getIconInfo(i.Path, i.IsDir)
|
||||
|
||||
// 3. Truncation (Safety)
|
||||
availableWidth := m.Width() - indentSize - 4
|
||||
displayName := i.Path
|
||||
if availableWidth > 0 && len(displayName) > availableWidth {
|
||||
displayName = displayName[:max(0, availableWidth-1)] + "…"
|
||||
}
|
||||
|
||||
// 4. Render Logic ("Oil" Block Cursor)
|
||||
var row string
|
||||
isSelected := index == m.Index()
|
||||
|
||||
if isSelected && d.Focused {
|
||||
// -- SELECTED STATE (Oil Style) --
|
||||
// We do NOT use iconStyle here. We want the icon to inherit the
|
||||
// selection text color so the background block is unbroken.
|
||||
// Content: Icon + Space + Name
|
||||
content := fmt.Sprintf("%s %s", iconStr, displayName)
|
||||
|
||||
// Apply the solid block style to the whole content
|
||||
renderedContent := SelectedBlockStyle.Render(content)
|
||||
|
||||
// Combine: Indent (unhighlighted) + Block (highlighted)
|
||||
row = fmt.Sprintf("%s%s", indent, renderedContent)
|
||||
|
||||
} else {
|
||||
// -- NORMAL / INACTIVE STATE --
|
||||
// Render icon with its specific color
|
||||
renderedIcon := iconStyle.Render(iconStr)
|
||||
|
||||
// Combine
|
||||
row = fmt.Sprintf("%s%s %s", indent, renderedIcon, displayName)
|
||||
|
||||
// Apply generic padding/style
|
||||
row = ItemStyle.Render(row)
|
||||
}
|
||||
|
||||
fmt.Fprint(w, row)
|
||||
}
|
||||
|
||||
// Helper: Returns raw icon string and its preferred style
|
||||
func getIconInfo(name string, isDir bool) (string, lipgloss.Style) {
|
||||
if isDir {
|
||||
return "", FolderIconStyle
|
||||
}
|
||||
|
||||
ext := filepath.Ext(name)
|
||||
icon := ""
|
||||
|
||||
switch strings.ToLower(ext) {
|
||||
case ".go":
|
||||
icon = ""
|
||||
case ".js", ".ts", ".tsx", ".jsx":
|
||||
icon = ""
|
||||
case ".md":
|
||||
icon = ""
|
||||
case ".json", ".yml", ".yaml", ".toml":
|
||||
icon = ""
|
||||
case ".css", ".scss":
|
||||
icon = ""
|
||||
case ".html":
|
||||
icon = ""
|
||||
case ".git":
|
||||
icon = ""
|
||||
case ".dockerfile":
|
||||
icon = ""
|
||||
case ".svelte":
|
||||
icon = ""
|
||||
}
|
||||
|
||||
return icon, FileIconStyle
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ package ui
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -28,6 +27,7 @@ const (
|
|||
|
||||
type Model struct {
|
||||
fileTree list.Model
|
||||
treeDelegate TreeDelegate
|
||||
diffViewport viewport.Model
|
||||
|
||||
selectedPath string
|
||||
|
|
@ -47,21 +47,24 @@ type Model struct {
|
|||
}
|
||||
|
||||
func NewModel(cfg config.Config) Model {
|
||||
// Initialize styles with the loaded config
|
||||
InitStyles(cfg)
|
||||
|
||||
files, _ := git.ListChangedFiles(TargetBranch)
|
||||
items := tree.Build(files)
|
||||
|
||||
l := list.New(items, listDelegate{}, 0, 0)
|
||||
delegate := TreeDelegate{Focused: true}
|
||||
l := list.New(items, delegate, 0, 0)
|
||||
|
||||
l.SetShowTitle(false)
|
||||
l.SetShowHelp(false)
|
||||
l.SetShowStatusBar(false)
|
||||
l.SetFilteringEnabled(false)
|
||||
l.SetShowPagination(false)
|
||||
l.DisableQuitKeybindings()
|
||||
|
||||
m := Model{
|
||||
fileTree: l,
|
||||
treeDelegate: delegate,
|
||||
diffViewport: viewport.New(0, 0),
|
||||
focus: FocusTree,
|
||||
currentBranch: git.GetCurrentBranch(),
|
||||
|
|
@ -132,14 +135,17 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
} else {
|
||||
m.focus = FocusTree
|
||||
}
|
||||
m.updateTreeFocus()
|
||||
m.inputBuffer = ""
|
||||
|
||||
case "l", "]", "ctrl+l", "right":
|
||||
m.focus = FocusDiff
|
||||
m.updateTreeFocus()
|
||||
m.inputBuffer = ""
|
||||
|
||||
case "h", "[", "ctrl+h", "left":
|
||||
m.focus = FocusTree
|
||||
m.updateTreeFocus()
|
||||
m.inputBuffer = ""
|
||||
|
||||
case "e", "enter":
|
||||
|
|
@ -243,6 +249,11 @@ func (m *Model) updateSizes() {
|
|||
m.diffViewport.Height = contentHeight
|
||||
}
|
||||
|
||||
func (m *Model) updateTreeFocus() {
|
||||
m.treeDelegate.Focused = (m.focus == FocusTree)
|
||||
m.fileTree.SetDelegate(m.treeDelegate)
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
if m.width == 0 {
|
||||
return "Loading..."
|
||||
|
|
@ -272,29 +283,22 @@ func (m Model) View() string {
|
|||
for i := start; i < end; i++ {
|
||||
line := m.diffLines[i]
|
||||
|
||||
// --- LINE NUMBER LOGIC ---
|
||||
// --- LINE NUMBERS ---
|
||||
var numStr string
|
||||
mode := CurrentConfig.UI.LineNumbers
|
||||
|
||||
if mode == "hidden" {
|
||||
numStr = ""
|
||||
} else {
|
||||
// Is this the cursor line?
|
||||
isCursor := (i == m.diffCursor)
|
||||
|
||||
if isCursor && mode == "hybrid" {
|
||||
// HYBRID: Show Real File Line Number
|
||||
realLine := git.CalculateFileLine(m.diffContent, m.diffCursor)
|
||||
numStr = fmt.Sprintf("%d", realLine)
|
||||
} else if isCursor && mode == "relative" {
|
||||
numStr = "0"
|
||||
} else if mode == "absolute" {
|
||||
// Note: Calculating absolute for every line is expensive,
|
||||
// usually absolute view shows Diff Line Index or File Line.
|
||||
// For simple 'absolute' view, we often show viewport index + 1
|
||||
numStr = fmt.Sprintf("%d", i+1)
|
||||
} else {
|
||||
// Default / Hybrid-non-cursor: Show Relative Distance
|
||||
dist := int(math.Abs(float64(i - m.diffCursor)))
|
||||
numStr = fmt.Sprintf("%d", dist)
|
||||
}
|
||||
|
|
@ -304,10 +308,12 @@ func (m Model) View() string {
|
|||
if numStr != "" {
|
||||
lineNumRendered = LineNumberStyle.Render(numStr)
|
||||
}
|
||||
// -------------------------
|
||||
|
||||
// --- DIFF VIEW HIGHLIGHT ---
|
||||
if m.focus == FocusDiff && i == m.diffCursor {
|
||||
line = SelectedItemStyle.Render(line)
|
||||
// FIXED: Uses DiffSelectionStyle (Background only)
|
||||
// This keeps green/red text colors visible while adding the blue selection bar
|
||||
line = DiffSelectionStyle.Render(line)
|
||||
} else {
|
||||
line = " " + line
|
||||
}
|
||||
|
|
@ -380,22 +386,3 @@ func (m Model) View() string {
|
|||
|
||||
return finalView
|
||||
}
|
||||
|
||||
// -- Delegates (unchanged) --
|
||||
type listDelegate struct{}
|
||||
|
||||
func (d listDelegate) Height() int { return 1 }
|
||||
func (d listDelegate) Spacing() int { return 0 }
|
||||
func (d listDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
|
||||
func (d listDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) {
|
||||
i, ok := item.(tree.TreeItem)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
str := i.Title()
|
||||
if index == m.Index() {
|
||||
fmt.Fprint(w, SelectedItemStyle.Render(str))
|
||||
} else {
|
||||
fmt.Fprint(w, ItemStyle.Render(str))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,24 +5,38 @@ import (
|
|||
"github.com/oug-t/difi/internal/config"
|
||||
)
|
||||
|
||||
// Global UI colors and styles.
|
||||
// Initialized once at startup via InitStyles.
|
||||
var (
|
||||
ColorBorder lipgloss.AdaptiveColor
|
||||
ColorFocus lipgloss.AdaptiveColor
|
||||
ColorText = lipgloss.AdaptiveColor{Light: "#1F1F1F", Dark: "#F8F8F2"}
|
||||
ColorSubtle = lipgloss.AdaptiveColor{Light: "#A8A8A8", Dark: "#626262"}
|
||||
ColorCursorBg = lipgloss.AdaptiveColor{Light: "#E5E5E5", Dark: "#3E3E3E"}
|
||||
// -- Colors --
|
||||
ColorText = lipgloss.AdaptiveColor{Light: "#24292f", Dark: "#c9d1d9"}
|
||||
ColorSubtle = lipgloss.AdaptiveColor{Light: "#6e7781", Dark: "#8b949e"}
|
||||
|
||||
// UNIFIED SELECTION COLOR (The "Neutral Light Transparent Blue")
|
||||
// This is used for BOTH the file tree and the diff panel background.
|
||||
// Dark: Deep subtle slate blue | Light: Pale selection blue
|
||||
ColorVisualBg = lipgloss.AdaptiveColor{Light: "#daeaff", Dark: "#3a4b5c"}
|
||||
|
||||
// Tree Text Color (High Contrast for the block cursor)
|
||||
ColorVisualFg = lipgloss.AdaptiveColor{Light: "#000000", Dark: "#ffffff"}
|
||||
|
||||
ColorFolder = lipgloss.AdaptiveColor{Light: "#0969da", Dark: "#83a598"}
|
||||
ColorFile = lipgloss.AdaptiveColor{Light: "#24292f", Dark: "#ebdbb2"}
|
||||
|
||||
ColorBarBg = lipgloss.AdaptiveColor{Light: "#F2F2F2", Dark: "#1F1F1F"}
|
||||
ColorBarFg = lipgloss.AdaptiveColor{Light: "#6E6E6E", Dark: "#9E9E9E"}
|
||||
|
||||
PaneStyle lipgloss.Style
|
||||
FocusedPaneStyle lipgloss.Style
|
||||
DiffStyle lipgloss.Style
|
||||
// -- Styles --
|
||||
PaneStyle lipgloss.Style
|
||||
FocusedPaneStyle lipgloss.Style
|
||||
DiffStyle lipgloss.Style
|
||||
|
||||
ItemStyle lipgloss.Style
|
||||
SelectedItemStyle lipgloss.Style
|
||||
LineNumberStyle lipgloss.Style
|
||||
SelectedBlockStyle lipgloss.Style // Tree (Opaque)
|
||||
DiffSelectionStyle lipgloss.Style // Diff (Transparent/BG only)
|
||||
|
||||
FolderIconStyle lipgloss.Style
|
||||
FileIconStyle lipgloss.Style
|
||||
LineNumberStyle lipgloss.Style
|
||||
|
||||
StatusBarStyle lipgloss.Style
|
||||
StatusKeyStyle lipgloss.Style
|
||||
StatusDividerStyle lipgloss.Style
|
||||
|
|
@ -32,16 +46,20 @@ var (
|
|||
CurrentConfig config.Config
|
||||
)
|
||||
|
||||
// InitStyles initializes global styles based on the provided config.
|
||||
// This should be called once during application startup.
|
||||
func InitStyles(cfg config.Config) {
|
||||
CurrentConfig = cfg
|
||||
|
||||
// Colors derived from user config
|
||||
ColorBorder = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: cfg.Colors.Border}
|
||||
ColorFocus = lipgloss.AdaptiveColor{Light: "#000000", Dark: cfg.Colors.Focus}
|
||||
ColorBorder := lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: cfg.Colors.Border}
|
||||
ColorFocus := lipgloss.AdaptiveColor{Light: "#6e7781", Dark: cfg.Colors.Focus}
|
||||
|
||||
// Allow user override for the selection background
|
||||
var selectionBg lipgloss.TerminalColor
|
||||
if cfg.Colors.DiffSelectionBg != "" {
|
||||
selectionBg = lipgloss.Color(cfg.Colors.DiffSelectionBg)
|
||||
} else {
|
||||
selectionBg = ColorVisualBg
|
||||
}
|
||||
|
||||
// Pane styles
|
||||
PaneStyle = lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder(), false, cfg.UI.ShowGuide, false, false).
|
||||
BorderForeground(ColorBorder)
|
||||
|
|
@ -49,44 +67,42 @@ func InitStyles(cfg config.Config) {
|
|||
FocusedPaneStyle = PaneStyle.Copy().
|
||||
BorderForeground(ColorFocus)
|
||||
|
||||
// Diff and list item styles
|
||||
DiffStyle = lipgloss.NewStyle().Padding(0, 0)
|
||||
ItemStyle = lipgloss.NewStyle().PaddingLeft(2)
|
||||
|
||||
SelectedItemStyle = lipgloss.NewStyle().
|
||||
// Base Row
|
||||
ItemStyle = lipgloss.NewStyle().
|
||||
PaddingLeft(1).
|
||||
Background(ColorCursorBg).
|
||||
Foreground(ColorText).
|
||||
Bold(true).
|
||||
Width(1000)
|
||||
PaddingRight(1).
|
||||
Foreground(ColorText)
|
||||
|
||||
// 1. LEFT PANE STYLE (Tree)
|
||||
// Uses the shared background + forces a foreground color for readability
|
||||
SelectedBlockStyle = lipgloss.NewStyle().
|
||||
Background(selectionBg).
|
||||
Foreground(ColorVisualFg).
|
||||
PaddingLeft(1).
|
||||
PaddingRight(1).
|
||||
Bold(true)
|
||||
|
||||
// 2. RIGHT PANE STYLE (Diff)
|
||||
// Uses the SAME shared background, but NO foreground.
|
||||
// This makes it "transparent" so Green(+)/Red(-) text colors show through.
|
||||
DiffSelectionStyle = lipgloss.NewStyle().
|
||||
Background(selectionBg)
|
||||
|
||||
FolderIconStyle = lipgloss.NewStyle().Foreground(ColorFolder)
|
||||
FileIconStyle = lipgloss.NewStyle().Foreground(ColorFile)
|
||||
|
||||
// Line numbers
|
||||
LineNumberStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(cfg.Colors.LineNumber)).
|
||||
PaddingRight(1).
|
||||
Width(4)
|
||||
|
||||
// Status bar
|
||||
StatusBarStyle = lipgloss.NewStyle().
|
||||
Foreground(ColorBarFg).
|
||||
Background(ColorBarBg).
|
||||
Padding(0, 1)
|
||||
|
||||
StatusKeyStyle = lipgloss.NewStyle().
|
||||
Foreground(ColorText).
|
||||
Background(ColorBarBg).
|
||||
Bold(true).
|
||||
Padding(0, 1)
|
||||
|
||||
StatusDividerStyle = lipgloss.NewStyle().
|
||||
Foreground(ColorSubtle).
|
||||
Background(ColorBarBg)
|
||||
|
||||
// Help drawer
|
||||
HelpTextStyle = lipgloss.NewStyle().
|
||||
Foreground(ColorSubtle).
|
||||
Padding(0, 1)
|
||||
StatusBarStyle = lipgloss.NewStyle().Foreground(ColorBarFg).Background(ColorBarBg).Padding(0, 1)
|
||||
StatusKeyStyle = lipgloss.NewStyle().Foreground(ColorText).Background(ColorBarBg).Bold(true).Padding(0, 1)
|
||||
StatusDividerStyle = lipgloss.NewStyle().Foreground(ColorSubtle).Background(ColorBarBg).Padding(0, 0)
|
||||
|
||||
HelpTextStyle = lipgloss.NewStyle().Foreground(ColorSubtle).Padding(0, 1)
|
||||
HelpDrawerStyle = lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder(), true, false, false, false).
|
||||
BorderForeground(ColorBorder).
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user