chore: refine file-tree
This commit is contained in:
parent
c654424094
commit
b14181e88a
|
|
@ -74,8 +74,6 @@ func OpenEditorCmd(path string, lineNumber int, targetBranch string) tea.Cmd {
|
|||
c := exec.Command(editor, args...)
|
||||
c.Stdin, c.Stdout, c.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
|
||||
// Pass the diff target branch to the editor via environment variable
|
||||
// This enables plugins like difi.nvim to auto-configure the view
|
||||
c.Env = append(os.Environ(), fmt.Sprintf("DIFI_TARGET=%s", targetBranch))
|
||||
|
||||
return tea.ExecProcess(c, func(err error) tea.Msg {
|
||||
|
|
@ -83,6 +81,38 @@ func OpenEditorCmd(path string, lineNumber int, targetBranch string) tea.Cmd {
|
|||
})
|
||||
}
|
||||
|
||||
func DiffStats(targetBranch string) (added int, deleted int, err error) {
|
||||
cmd := exec.Command("git", "diff", "--numstat", targetBranch)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("git diff stats error: %w", err)
|
||||
}
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||
for _, line := range lines {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
if parts[0] != "-" {
|
||||
if n, err := strconv.Atoi(parts[0]); err == nil {
|
||||
added += n
|
||||
}
|
||||
}
|
||||
|
||||
if parts[1] != "-" {
|
||||
if n, err := strconv.Atoi(parts[1]); err == nil {
|
||||
deleted += n
|
||||
}
|
||||
}
|
||||
}
|
||||
return added, deleted, nil
|
||||
}
|
||||
|
||||
func CalculateFileLine(diffContent string, visualLineIndex int) int {
|
||||
lines := strings.Split(diffContent, "\n")
|
||||
if visualLineIndex >= len(lines) {
|
||||
|
|
|
|||
|
|
@ -1,126 +0,0 @@
|
|||
package tree
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
)
|
||||
|
||||
// TreeItem represents a file or folder
|
||||
type TreeItem struct {
|
||||
Path string
|
||||
FullPath string
|
||||
IsDir bool
|
||||
Depth int
|
||||
}
|
||||
|
||||
func (i TreeItem) FilterValue() string { return i.FullPath }
|
||||
func (i TreeItem) Description() string { return "" }
|
||||
func (i TreeItem) Title() string {
|
||||
indent := strings.Repeat(" ", i.Depth)
|
||||
icon := getIcon(i.Path, i.IsDir)
|
||||
return fmt.Sprintf("%s%s %s", indent, icon, i.Path)
|
||||
}
|
||||
|
||||
func Build(paths []string) []list.Item {
|
||||
root := &node{
|
||||
children: make(map[string]*node),
|
||||
isDir: true,
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
parts := strings.Split(path, "/")
|
||||
current := root
|
||||
for i, part := range parts {
|
||||
if _, exists := current.children[part]; !exists {
|
||||
isDir := i < len(parts)-1
|
||||
fullPath := strings.Join(parts[:i+1], "/")
|
||||
current.children[part] = &node{
|
||||
name: part,
|
||||
fullPath: fullPath,
|
||||
children: make(map[string]*node),
|
||||
isDir: isDir,
|
||||
}
|
||||
}
|
||||
current = current.children[part]
|
||||
}
|
||||
}
|
||||
|
||||
var items []list.Item
|
||||
flatten(root, 0, &items)
|
||||
return items
|
||||
}
|
||||
|
||||
type node struct {
|
||||
name string
|
||||
fullPath string
|
||||
children map[string]*node
|
||||
isDir bool
|
||||
}
|
||||
|
||||
// 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.Slice(keys, func(i, j int) bool {
|
||||
a, b := n.children[keys[i]], n.children[keys[j]]
|
||||
if a.isDir && !b.isDir {
|
||||
return true
|
||||
}
|
||||
if !a.isDir && b.isDir {
|
||||
return false
|
||||
}
|
||||
return a.name < b.name
|
||||
})
|
||||
|
||||
for _, k := range keys {
|
||||
child := n.children[k]
|
||||
*items = append(*items, TreeItem{
|
||||
Path: child.name,
|
||||
FullPath: child.fullPath,
|
||||
IsDir: child.isDir,
|
||||
Depth: depth,
|
||||
})
|
||||
|
||||
if child.isDir {
|
||||
flatten(child, depth+1, items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getIcon(name string, isDir bool) string {
|
||||
if isDir {
|
||||
return " "
|
||||
}
|
||||
ext := filepath.Ext(name)
|
||||
switch strings.ToLower(ext) {
|
||||
case ".go":
|
||||
return " "
|
||||
case ".js", ".ts", ".tsx":
|
||||
return " "
|
||||
case ".svelte":
|
||||
return " "
|
||||
case ".md":
|
||||
return " "
|
||||
case ".json":
|
||||
return " "
|
||||
case ".yml", ".yaml":
|
||||
return " "
|
||||
case ".html":
|
||||
return " "
|
||||
case ".css":
|
||||
return " "
|
||||
case ".git":
|
||||
return " "
|
||||
case ".dockerfile":
|
||||
return " "
|
||||
default:
|
||||
return " "
|
||||
}
|
||||
}
|
||||
191
internal/tree/tree.go
Normal file
191
internal/tree/tree.go
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
package tree
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
)
|
||||
|
||||
// FileTree holds the state of the entire file graph.
|
||||
type FileTree struct {
|
||||
Root *Node
|
||||
}
|
||||
|
||||
// Node represents a file or directory in the tree.
|
||||
type Node struct {
|
||||
Name string
|
||||
FullPath string
|
||||
IsDir bool
|
||||
Children map[string]*Node
|
||||
Expanded bool
|
||||
Depth int
|
||||
}
|
||||
|
||||
// TreeItem represents a file or folder for the Bubble Tea list.
|
||||
type TreeItem struct {
|
||||
Name string
|
||||
FullPath string
|
||||
IsDir bool
|
||||
Depth int
|
||||
Expanded bool
|
||||
Icon string
|
||||
}
|
||||
|
||||
// Implement list.Item interface
|
||||
func (i TreeItem) FilterValue() string { return i.Name }
|
||||
func (i TreeItem) Description() string { return "" }
|
||||
func (i TreeItem) Title() string {
|
||||
indent := strings.Repeat(" ", i.Depth)
|
||||
disclosure := " "
|
||||
if i.IsDir {
|
||||
if i.Expanded {
|
||||
disclosure = "▾"
|
||||
} else {
|
||||
disclosure = "▸"
|
||||
}
|
||||
}
|
||||
// Icon spacing handled in formatting
|
||||
return fmt.Sprintf("%s%s %s %s", indent, disclosure, i.Icon, i.Name)
|
||||
}
|
||||
|
||||
// New creates a new FileTree from a list of changed file paths.
|
||||
func New(paths []string) *FileTree {
|
||||
root := &Node{
|
||||
Name: "root",
|
||||
IsDir: true,
|
||||
Children: make(map[string]*Node),
|
||||
Expanded: true, // Root always expanded
|
||||
Depth: -1, // Root is hidden
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
addPath(root, path)
|
||||
}
|
||||
|
||||
return &FileTree{Root: root}
|
||||
}
|
||||
|
||||
// addPath inserts a path into the tree, creating directory nodes as needed.
|
||||
func addPath(root *Node, path string) {
|
||||
cleanPath := filepath.ToSlash(filepath.Clean(path))
|
||||
parts := strings.Split(cleanPath, "/")
|
||||
|
||||
current := root
|
||||
for i, name := range parts {
|
||||
if _, exists := current.Children[name]; !exists {
|
||||
isFile := i == len(parts)-1
|
||||
nodePath := name
|
||||
if current.FullPath != "" {
|
||||
nodePath = current.FullPath + "/" + name
|
||||
}
|
||||
|
||||
// Directories default to expanded for visibility, or collapsed if preferred
|
||||
// GitHub usually auto-expands to show changed files. Here we auto-expand.
|
||||
current.Children[name] = &Node{
|
||||
Name: name,
|
||||
FullPath: nodePath,
|
||||
IsDir: !isFile,
|
||||
Children: make(map[string]*Node),
|
||||
Expanded: true,
|
||||
Depth: current.Depth + 1,
|
||||
}
|
||||
}
|
||||
current = current.Children[name]
|
||||
}
|
||||
}
|
||||
|
||||
// Items returns the flattened, visible list items based on expansion state.
|
||||
func (t *FileTree) Items() []list.Item {
|
||||
var items []list.Item
|
||||
flatten(t.Root, &items)
|
||||
return items
|
||||
}
|
||||
|
||||
// flatten recursively builds the list, respecting expansion state.
|
||||
func flatten(node *Node, items *[]list.Item) {
|
||||
// Collect children to sort
|
||||
children := make([]*Node, 0, len(node.Children))
|
||||
for _, child := range node.Children {
|
||||
children = append(children, child)
|
||||
}
|
||||
|
||||
// Sort: Directories first, then alphabetical
|
||||
sort.Slice(children, func(i, j int) bool {
|
||||
if children[i].IsDir != children[j].IsDir {
|
||||
return children[i].IsDir
|
||||
}
|
||||
return strings.ToLower(children[i].Name) < strings.ToLower(children[j].Name)
|
||||
})
|
||||
|
||||
for _, child := range children {
|
||||
*items = append(*items, TreeItem{
|
||||
Name: child.Name,
|
||||
FullPath: child.FullPath,
|
||||
IsDir: child.IsDir,
|
||||
Depth: child.Depth,
|
||||
Expanded: child.Expanded,
|
||||
Icon: getIcon(child.Name, child.IsDir),
|
||||
})
|
||||
|
||||
// Only traverse children if expanded
|
||||
if child.IsDir && child.Expanded {
|
||||
flatten(child, items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ToggleExpand toggles the expansion state of a specific node.
|
||||
func (t *FileTree) ToggleExpand(fullPath string) {
|
||||
node := findNode(t.Root, fullPath)
|
||||
if node != nil && node.IsDir {
|
||||
node.Expanded = !node.Expanded
|
||||
}
|
||||
}
|
||||
|
||||
func findNode(node *Node, fullPath string) *Node {
|
||||
if node.FullPath == fullPath {
|
||||
return node
|
||||
}
|
||||
// Simple traversal. For very large trees, a map cache in FileTree might be faster.
|
||||
for _, child := range node.Children {
|
||||
if strings.HasPrefix(fullPath, child.FullPath) {
|
||||
if child.FullPath == fullPath {
|
||||
return child
|
||||
}
|
||||
if found := findNode(child, fullPath); found != nil {
|
||||
return found
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getIcon(name string, isDir bool) string {
|
||||
if isDir {
|
||||
return ""
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(name))
|
||||
switch ext {
|
||||
case ".go":
|
||||
return ""
|
||||
case ".js", ".ts", ".tsx":
|
||||
return ""
|
||||
case ".css", ".scss":
|
||||
return ""
|
||||
case ".html":
|
||||
return ""
|
||||
case ".json", ".yaml", ".yml", ".toml":
|
||||
return ""
|
||||
case ".md":
|
||||
return ""
|
||||
case ".png", ".jpg", ".jpeg", ".svg":
|
||||
return ""
|
||||
case ".gitignore", ".gitmodules":
|
||||
return ""
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/oug-t/difi/internal/tree"
|
||||
)
|
||||
|
||||
|
|
@ -16,6 +17,7 @@ type TreeDelegate struct {
|
|||
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 {
|
||||
|
|
@ -24,17 +26,19 @@ func (d TreeDelegate) Render(w io.Writer, m list.Model, index int, item list.Ite
|
|||
|
||||
title := i.Title()
|
||||
|
||||
// If this item is selected
|
||||
if index == m.Index() {
|
||||
if d.Focused {
|
||||
// Render the whole line (including indent) with the selection background
|
||||
fmt.Fprint(w, SelectedBlockStyle.Render(title))
|
||||
} else {
|
||||
// Dimmed selection if focus is on the other panel
|
||||
fmt.Fprint(w, SelectedBlockStyle.Copy().Foreground(ColorSubtle).Render(title))
|
||||
style := lipgloss.NewStyle().
|
||||
Background(lipgloss.Color("237")). // Dark gray background
|
||||
Foreground(lipgloss.Color("255")). // White text
|
||||
Bold(true)
|
||||
|
||||
if !d.Focused {
|
||||
style = style.Foreground(lipgloss.Color("245"))
|
||||
}
|
||||
|
||||
fmt.Fprint(w, style.Render(title))
|
||||
} else {
|
||||
// Normal Item (No icons added, just the text)
|
||||
fmt.Fprint(w, ItemStyle.Render(title))
|
||||
style := lipgloss.NewStyle().Foreground(lipgloss.Color("252"))
|
||||
fmt.Fprint(w, style.Render(title))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,14 @@ const (
|
|||
FocusDiff
|
||||
)
|
||||
|
||||
type StatsMsg struct {
|
||||
Added int
|
||||
Deleted int
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
fileTree list.Model
|
||||
fileList list.Model
|
||||
treeState *tree.FileTree
|
||||
treeDelegate TreeDelegate
|
||||
diffViewport viewport.Model
|
||||
|
||||
|
|
@ -34,6 +40,9 @@ type Model struct {
|
|||
targetBranch string
|
||||
repoName string
|
||||
|
||||
statsAdded int
|
||||
statsDeleted int
|
||||
|
||||
diffContent string
|
||||
diffLines []string
|
||||
diffCursor int
|
||||
|
|
@ -51,7 +60,9 @@ func NewModel(cfg config.Config, targetBranch string) Model {
|
|||
InitStyles(cfg)
|
||||
|
||||
files, _ := git.ListChangedFiles(targetBranch)
|
||||
items := tree.Build(files)
|
||||
|
||||
t := tree.New(files)
|
||||
items := t.Items()
|
||||
|
||||
delegate := TreeDelegate{Focused: true}
|
||||
l := list.New(items, delegate, 0, 0)
|
||||
|
|
@ -64,7 +75,8 @@ func NewModel(cfg config.Config, targetBranch string) Model {
|
|||
l.DisableQuitKeybindings()
|
||||
|
||||
m := Model{
|
||||
fileTree: l,
|
||||
fileList: l,
|
||||
treeState: t,
|
||||
treeDelegate: delegate,
|
||||
diffViewport: viewport.New(0, 0),
|
||||
focus: FocusTree,
|
||||
|
|
@ -78,17 +90,34 @@ func NewModel(cfg config.Config, targetBranch string) Model {
|
|||
|
||||
if len(items) > 0 {
|
||||
if first, ok := items[0].(tree.TreeItem); ok {
|
||||
m.selectedPath = first.FullPath
|
||||
if !first.IsDir {
|
||||
m.selectedPath = first.FullPath
|
||||
}
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
if m.selectedPath != "" {
|
||||
return git.DiffCmd(m.targetBranch, m.selectedPath)
|
||||
cmds = append(cmds, git.DiffCmd(m.targetBranch, m.selectedPath))
|
||||
}
|
||||
|
||||
cmds = append(cmds, fetchStatsCmd(m.targetBranch))
|
||||
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func fetchStatsCmd(target string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
added, deleted, err := git.DiffStats(target)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return StatsMsg{Added: added, Deleted: deleted}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Model) getRepeatCount() int {
|
||||
|
|
@ -115,16 +144,19 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
m.height = msg.Height
|
||||
m.updateSizes()
|
||||
|
||||
case StatsMsg:
|
||||
m.statsAdded = msg.Added
|
||||
m.statsDeleted = msg.Deleted
|
||||
|
||||
case tea.KeyMsg:
|
||||
if msg.String() == "q" || msg.String() == "ctrl+c" {
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
if len(m.fileTree.Items()) == 0 {
|
||||
if len(m.fileList.Items()) == 0 {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Handle z-prefix commands (zz, zt, zb)
|
||||
if m.pendingZ {
|
||||
m.pendingZ = false
|
||||
if m.focus == FocusDiff {
|
||||
|
|
@ -158,6 +190,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
switch msg.String() {
|
||||
case "tab":
|
||||
if m.focus == FocusTree {
|
||||
if item, ok := m.fileList.SelectedItem().(tree.TreeItem); ok && item.IsDir {
|
||||
return m, nil
|
||||
}
|
||||
m.focus = FocusDiff
|
||||
} else {
|
||||
m.focus = FocusTree
|
||||
|
|
@ -166,6 +201,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
m.inputBuffer = ""
|
||||
|
||||
case "l", "]", "ctrl+l", "right":
|
||||
if m.focus == FocusTree {
|
||||
if item, ok := m.fileList.SelectedItem().(tree.TreeItem); ok && item.IsDir {
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
m.focus = FocusDiff
|
||||
m.updateTreeFocus()
|
||||
m.inputBuffer = ""
|
||||
|
|
@ -175,8 +215,29 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
m.updateTreeFocus()
|
||||
m.inputBuffer = ""
|
||||
|
||||
case "e", "enter":
|
||||
case "enter":
|
||||
if m.focus == FocusTree {
|
||||
if i, ok := m.fileList.SelectedItem().(tree.TreeItem); ok && i.IsDir {
|
||||
m.treeState.ToggleExpand(i.FullPath)
|
||||
m.fileList.SetItems(m.treeState.Items())
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
if m.selectedPath != "" {
|
||||
if i, ok := m.fileList.SelectedItem().(tree.TreeItem); ok && !i.IsDir {
|
||||
// proceed
|
||||
} else {
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
fallthrough
|
||||
|
||||
case "e":
|
||||
if m.selectedPath != "" {
|
||||
if i, ok := m.fileList.SelectedItem().(tree.TreeItem); ok && i.IsDir {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
line := 0
|
||||
if m.focus == FocusDiff {
|
||||
line = git.CalculateFileLine(m.diffContent, m.diffCursor)
|
||||
|
|
@ -184,18 +245,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
line = git.CalculateFileLine(m.diffContent, 0)
|
||||
}
|
||||
m.inputBuffer = ""
|
||||
// Integration Point: Pass targetBranch to the editor command
|
||||
return m, git.OpenEditorCmd(m.selectedPath, line, m.targetBranch)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
@ -221,7 +279,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
}
|
||||
}
|
||||
|
||||
// Page navigation
|
||||
case "ctrl+d":
|
||||
if m.focus == FocusDiff {
|
||||
halfScreen := m.diffViewport.Height / 2
|
||||
|
|
@ -251,13 +308,12 @@ 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)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
m.fileTree.CursorDown()
|
||||
m.fileList.CursorDown()
|
||||
}
|
||||
}
|
||||
m.inputBuffer = ""
|
||||
|
|
@ -269,13 +325,12 @@ 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)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
m.fileTree.CursorUp()
|
||||
m.fileList.CursorUp()
|
||||
}
|
||||
}
|
||||
m.inputBuffer = ""
|
||||
|
|
@ -285,14 +340,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
}
|
||||
}
|
||||
|
||||
if len(m.fileTree.Items()) > 0 && m.focus == FocusTree {
|
||||
if len(m.fileList.Items()) > 0 && m.focus == FocusTree {
|
||||
if !keyHandled {
|
||||
m.fileTree, cmd = m.fileTree.Update(msg)
|
||||
m.fileList, cmd = m.fileList.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
if item, ok := m.fileTree.SelectedItem().(tree.TreeItem); ok && !item.IsDir {
|
||||
if item.FullPath != m.selectedPath {
|
||||
if item, ok := m.fileList.SelectedItem().(tree.TreeItem); ok {
|
||||
if !item.IsDir && item.FullPath != m.selectedPath {
|
||||
m.selectedPath = item.FullPath
|
||||
m.diffCursor = 0
|
||||
m.diffViewport.GotoTop()
|
||||
|
|
@ -324,29 +379,41 @@ func (m *Model) centerDiffCursor() {
|
|||
}
|
||||
|
||||
func (m *Model) updateSizes() {
|
||||
reservedHeight := 1
|
||||
// 1 line Top Bar + 1 line Bottom Bar = 2 reserved
|
||||
reservedHeight := 2
|
||||
if m.showHelp {
|
||||
reservedHeight += 6
|
||||
}
|
||||
|
||||
// Calculate main area height
|
||||
contentHeight := m.height - reservedHeight
|
||||
if contentHeight < 1 {
|
||||
contentHeight = 1
|
||||
}
|
||||
|
||||
// Calculate widths
|
||||
treeWidth := int(float64(m.width) * 0.20)
|
||||
if treeWidth < 20 {
|
||||
treeWidth = 20
|
||||
}
|
||||
|
||||
m.fileTree.SetSize(treeWidth, contentHeight)
|
||||
// The Tree PaneStyle has a border (1 top, 1 bottom = 2 lines).
|
||||
// We must subtract this from the content height for the inner list.
|
||||
listHeight := contentHeight - 2
|
||||
if listHeight < 1 {
|
||||
listHeight = 1
|
||||
}
|
||||
m.fileList.SetSize(treeWidth, listHeight)
|
||||
|
||||
// We align the Diff Viewport height with the List height to ensure
|
||||
// the bottom edges match visually and to prevent overflow.
|
||||
m.diffViewport.Width = m.width - treeWidth - 2
|
||||
m.diffViewport.Height = contentHeight
|
||||
m.diffViewport.Height = listHeight
|
||||
}
|
||||
|
||||
func (m *Model) updateTreeFocus() {
|
||||
m.treeDelegate.Focused = (m.focus == FocusTree)
|
||||
m.fileTree.SetDelegate(m.treeDelegate)
|
||||
m.fileList.SetDelegate(m.treeDelegate)
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
|
|
@ -354,183 +421,238 @@ func (m Model) View() string {
|
|||
return "Loading..."
|
||||
}
|
||||
|
||||
if len(m.fileTree.Items()) == 0 {
|
||||
return m.viewEmptyState()
|
||||
}
|
||||
topBar := m.renderTopBar()
|
||||
|
||||
// Panes
|
||||
treeStyle := PaneStyle
|
||||
if m.focus == FocusTree {
|
||||
treeStyle = FocusedPaneStyle
|
||||
} else {
|
||||
treeStyle = PaneStyle
|
||||
}
|
||||
|
||||
treeView := treeStyle.Copy().
|
||||
Width(m.fileTree.Width()).
|
||||
Height(m.fileTree.Height()).
|
||||
Render(m.fileTree.View())
|
||||
|
||||
var renderedDiff strings.Builder
|
||||
start := m.diffViewport.YOffset
|
||||
end := start + m.diffViewport.Height
|
||||
if end > len(m.diffLines) {
|
||||
end = len(m.diffLines)
|
||||
}
|
||||
|
||||
for i := start; i < end; i++ {
|
||||
line := m.diffLines[i]
|
||||
|
||||
var numStr string
|
||||
mode := CurrentConfig.UI.LineNumbers
|
||||
|
||||
if mode == "hidden" {
|
||||
numStr = ""
|
||||
} else {
|
||||
isCursor := (i == m.diffCursor)
|
||||
if isCursor && mode == "hybrid" {
|
||||
realLine := git.CalculateFileLine(m.diffContent, m.diffCursor)
|
||||
numStr = fmt.Sprintf("%d", realLine)
|
||||
} else if isCursor && mode == "relative" {
|
||||
numStr = "0"
|
||||
} else if mode == "absolute" {
|
||||
numStr = fmt.Sprintf("%d", i+1)
|
||||
} else {
|
||||
dist := int(math.Abs(float64(i - m.diffCursor)))
|
||||
numStr = fmt.Sprintf("%d", dist)
|
||||
}
|
||||
}
|
||||
|
||||
lineNumRendered := ""
|
||||
if numStr != "" {
|
||||
lineNumRendered = LineNumberStyle.Render(numStr)
|
||||
}
|
||||
|
||||
if m.focus == FocusDiff && i == m.diffCursor {
|
||||
cleanLine := stripAnsi(line)
|
||||
line = DiffSelectionStyle.Render(" " + cleanLine)
|
||||
} else {
|
||||
line = " " + line
|
||||
}
|
||||
|
||||
renderedDiff.WriteString(lineNumRendered + line + "\n")
|
||||
}
|
||||
|
||||
diffView := DiffStyle.Copy().
|
||||
Width(m.diffViewport.Width).
|
||||
Height(m.diffViewport.Height).
|
||||
Render(renderedDiff.String())
|
||||
|
||||
mainPanes := lipgloss.JoinHorizontal(lipgloss.Top, treeView, diffView)
|
||||
|
||||
// Bottom area
|
||||
repoSection := StatusKeyStyle.Render(" " + m.repoName)
|
||||
divider := StatusDividerStyle.Render("│")
|
||||
|
||||
statusText := fmt.Sprintf(" %s ↔ %s", m.currentBranch, m.targetBranch)
|
||||
if m.inputBuffer != "" {
|
||||
statusText += fmt.Sprintf(" [Cmd: %s]", m.inputBuffer)
|
||||
}
|
||||
branchSection := StatusBarStyle.Render(statusText)
|
||||
|
||||
leftStatus := lipgloss.JoinHorizontal(lipgloss.Center, repoSection, divider, branchSection)
|
||||
rightStatus := StatusBarStyle.Render("? Help")
|
||||
|
||||
statusBar := StatusBarStyle.Copy().
|
||||
Width(m.width).
|
||||
Render(lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
leftStatus,
|
||||
lipgloss.PlaceHorizontal(m.width-lipgloss.Width(leftStatus)-lipgloss.Width(rightStatus), lipgloss.Right, rightStatus),
|
||||
))
|
||||
|
||||
var finalView string
|
||||
var mainContent string
|
||||
contentHeight := m.height - 2
|
||||
if m.showHelp {
|
||||
col1 := lipgloss.JoinVertical(lipgloss.Left,
|
||||
HelpTextStyle.Render("↑/k Move Up"),
|
||||
HelpTextStyle.Render("↓/j Move Down"),
|
||||
)
|
||||
col2 := lipgloss.JoinVertical(lipgloss.Left,
|
||||
HelpTextStyle.Render("←/h Left Panel"),
|
||||
HelpTextStyle.Render("→/l Right Panel"),
|
||||
)
|
||||
col3 := lipgloss.JoinVertical(lipgloss.Left,
|
||||
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"),
|
||||
)
|
||||
|
||||
helpDrawer := HelpDrawerStyle.Copy().
|
||||
Width(m.width).
|
||||
Render(lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
col1,
|
||||
lipgloss.NewStyle().Width(4).Render(""),
|
||||
col2,
|
||||
lipgloss.NewStyle().Width(4).Render(""),
|
||||
col3,
|
||||
lipgloss.NewStyle().Width(4).Render(""),
|
||||
col4,
|
||||
))
|
||||
|
||||
finalView = lipgloss.JoinVertical(lipgloss.Top, mainPanes, helpDrawer, statusBar)
|
||||
} else {
|
||||
finalView = lipgloss.JoinVertical(lipgloss.Top, mainPanes, statusBar)
|
||||
contentHeight -= 6
|
||||
}
|
||||
if contentHeight < 0 {
|
||||
contentHeight = 0
|
||||
}
|
||||
|
||||
return finalView
|
||||
if len(m.fileList.Items()) == 0 {
|
||||
mainContent = m.renderEmptyState(m.width, contentHeight, "No changes found against "+m.targetBranch)
|
||||
} else {
|
||||
treeStyle := PaneStyle
|
||||
if m.focus == FocusTree {
|
||||
treeStyle = FocusedPaneStyle
|
||||
} else {
|
||||
treeStyle = PaneStyle
|
||||
}
|
||||
|
||||
treeView := treeStyle.Copy().
|
||||
Width(m.fileList.Width()).
|
||||
Height(m.fileList.Height()).
|
||||
Render(m.fileList.View())
|
||||
|
||||
var rightPaneView string
|
||||
|
||||
selectedItem, ok := m.fileList.SelectedItem().(tree.TreeItem)
|
||||
if ok && selectedItem.IsDir {
|
||||
rightPaneView = m.renderEmptyState(m.diffViewport.Width, m.diffViewport.Height, "Directory: "+selectedItem.Name)
|
||||
} else {
|
||||
var renderedDiff strings.Builder
|
||||
start := m.diffViewport.YOffset
|
||||
end := start + m.diffViewport.Height
|
||||
if end > len(m.diffLines) {
|
||||
end = len(m.diffLines)
|
||||
}
|
||||
|
||||
for i := start; i < end; i++ {
|
||||
line := m.diffLines[i]
|
||||
|
||||
var numStr string
|
||||
mode := "relative"
|
||||
|
||||
if mode == "hidden" {
|
||||
numStr = ""
|
||||
} else {
|
||||
isCursor := (i == m.diffCursor)
|
||||
if isCursor && mode == "hybrid" {
|
||||
realLine := git.CalculateFileLine(m.diffContent, m.diffCursor)
|
||||
numStr = fmt.Sprintf("%d", realLine)
|
||||
} else if isCursor && mode == "relative" {
|
||||
numStr = "0"
|
||||
} else if mode == "absolute" {
|
||||
numStr = fmt.Sprintf("%d", i+1)
|
||||
} else {
|
||||
dist := int(math.Abs(float64(i - m.diffCursor)))
|
||||
numStr = fmt.Sprintf("%d", dist)
|
||||
}
|
||||
}
|
||||
|
||||
lineNumRendered := ""
|
||||
if numStr != "" {
|
||||
lineNumRendered = LineNumberStyle.Render(numStr)
|
||||
}
|
||||
|
||||
if m.focus == FocusDiff && i == m.diffCursor {
|
||||
cleanLine := stripAnsi(line)
|
||||
line = DiffSelectionStyle.Render(" " + cleanLine)
|
||||
} else {
|
||||
line = " " + line
|
||||
}
|
||||
|
||||
renderedDiff.WriteString(lineNumRendered + line + "\n")
|
||||
}
|
||||
|
||||
// Trim the trailing newline to prevent an extra empty line
|
||||
// which pushes the layout height +1 and causes the top bar to scroll off.
|
||||
diffContentStr := strings.TrimRight(renderedDiff.String(), "\n")
|
||||
|
||||
rightPaneView = DiffStyle.Copy().
|
||||
Width(m.diffViewport.Width).
|
||||
Height(m.diffViewport.Height).
|
||||
Render(diffContentStr)
|
||||
}
|
||||
|
||||
mainContent = lipgloss.JoinHorizontal(lipgloss.Top, treeView, rightPaneView)
|
||||
}
|
||||
|
||||
var bottomBar string
|
||||
if m.showHelp {
|
||||
bottomBar = m.renderHelpDrawer()
|
||||
} else {
|
||||
bottomBar = m.viewStatusBar()
|
||||
}
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Top, topBar, mainContent, bottomBar)
|
||||
}
|
||||
|
||||
func (m Model) viewEmptyState() string {
|
||||
func (m Model) renderTopBar() string {
|
||||
repo := fmt.Sprintf(" %s", m.repoName)
|
||||
branches := fmt.Sprintf(" %s ➜ %s", m.currentBranch, m.targetBranch)
|
||||
info := fmt.Sprintf("%s %s", repo, branches)
|
||||
leftSide := TopInfoStyle.Render(info)
|
||||
|
||||
middle := ""
|
||||
if m.selectedPath != "" {
|
||||
middle = lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render(m.selectedPath)
|
||||
}
|
||||
|
||||
rightSide := ""
|
||||
if m.statsAdded > 0 || m.statsDeleted > 0 {
|
||||
added := TopStatsAddedStyle.Render(fmt.Sprintf("+%d", m.statsAdded))
|
||||
deleted := TopStatsDeletedStyle.Render(fmt.Sprintf("-%d", m.statsDeleted))
|
||||
rightSide = lipgloss.JoinHorizontal(lipgloss.Center, added, deleted)
|
||||
}
|
||||
|
||||
availWidth := m.width - lipgloss.Width(leftSide) - lipgloss.Width(rightSide)
|
||||
if availWidth < 0 {
|
||||
availWidth = 0
|
||||
}
|
||||
|
||||
midWidth := lipgloss.Width(middle)
|
||||
var centerBlock string
|
||||
if midWidth > availWidth {
|
||||
centerBlock = strings.Repeat(" ", availWidth)
|
||||
} else {
|
||||
padL := (availWidth - midWidth) / 2
|
||||
padR := availWidth - midWidth - padL
|
||||
centerBlock = strings.Repeat(" ", padL) + middle + strings.Repeat(" ", padR)
|
||||
}
|
||||
|
||||
finalBar := lipgloss.JoinHorizontal(lipgloss.Top, leftSide, centerBlock, rightSide)
|
||||
return TopBarStyle.Width(m.width).Render(finalBar)
|
||||
}
|
||||
|
||||
func (m Model) viewStatusBar() string {
|
||||
shortcuts := StatusKeyStyle.Render("? Help q Quit Tab Switch")
|
||||
return StatusBarStyle.Width(m.width).Render(shortcuts)
|
||||
}
|
||||
|
||||
func (m Model) renderHelpDrawer() string {
|
||||
col1 := lipgloss.JoinVertical(lipgloss.Left,
|
||||
HelpTextStyle.Render("↑/k Move Up"),
|
||||
HelpTextStyle.Render("↓/j Move Down"),
|
||||
)
|
||||
col2 := lipgloss.JoinVertical(lipgloss.Left,
|
||||
HelpTextStyle.Render("←/h Left Panel"),
|
||||
HelpTextStyle.Render("→/l Right Panel"),
|
||||
)
|
||||
col3 := lipgloss.JoinVertical(lipgloss.Left,
|
||||
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"),
|
||||
)
|
||||
|
||||
return HelpDrawerStyle.Copy().
|
||||
Width(m.width).
|
||||
Render(lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
col1,
|
||||
lipgloss.NewStyle().Width(4).Render(""),
|
||||
col2,
|
||||
lipgloss.NewStyle().Width(4).Render(""),
|
||||
col3,
|
||||
lipgloss.NewStyle().Width(4).Render(""),
|
||||
col4,
|
||||
))
|
||||
}
|
||||
|
||||
func (m Model) renderEmptyState(w, h int, statusMsg string) string {
|
||||
logo := EmptyLogoStyle.Render("difi")
|
||||
desc := EmptyDescStyle.Render("A calm, focused way to review Git diffs.")
|
||||
|
||||
statusMsg := fmt.Sprintf("✓ No changes found against '%s'", m.targetBranch)
|
||||
status := EmptyStatusStyle.Render(statusMsg)
|
||||
|
||||
usageHeader := EmptyHeaderStyle.Render("Usage Patterns")
|
||||
|
||||
cmd1 := lipgloss.NewStyle().Foreground(ColorText).Render("difi")
|
||||
desc1 := EmptyCodeStyle.Render("Diff against main")
|
||||
|
||||
cmd2 := lipgloss.NewStyle().Foreground(ColorText).Render("difi develop")
|
||||
desc2 := EmptyCodeStyle.Render("Diff against target branch")
|
||||
|
||||
cmd3 := lipgloss.NewStyle().Foreground(ColorText).Render("difi HEAD~1")
|
||||
desc3 := EmptyCodeStyle.Render("Diff against previous commit")
|
||||
cmd2 := lipgloss.NewStyle().Foreground(ColorText).Render("difi dev")
|
||||
desc2 := EmptyCodeStyle.Render("Diff against branch")
|
||||
|
||||
usageBlock := lipgloss.JoinVertical(lipgloss.Left,
|
||||
usageHeader,
|
||||
lipgloss.JoinHorizontal(lipgloss.Left, cmd1, desc1),
|
||||
lipgloss.JoinHorizontal(lipgloss.Left, cmd2, desc2),
|
||||
lipgloss.JoinHorizontal(lipgloss.Left, cmd3, desc3),
|
||||
lipgloss.JoinHorizontal(lipgloss.Left, cmd1, " ", desc1),
|
||||
lipgloss.JoinHorizontal(lipgloss.Left, cmd2, " ", desc2),
|
||||
)
|
||||
|
||||
navHeader := EmptyHeaderStyle.Render("Navigation")
|
||||
|
||||
key1 := lipgloss.NewStyle().Foreground(ColorText).Render("Tab")
|
||||
key2 := lipgloss.NewStyle().Foreground(ColorText).Render("j/k")
|
||||
keyDesc1 := EmptyCodeStyle.Render("Switch panels")
|
||||
|
||||
key2 := lipgloss.NewStyle().Foreground(ColorText).Render("j / k")
|
||||
keyDesc2 := EmptyCodeStyle.Render("Move cursor")
|
||||
|
||||
key3 := lipgloss.NewStyle().Foreground(ColorText).Render("zz/zt")
|
||||
keyDesc3 := EmptyCodeStyle.Render("Center/Top")
|
||||
|
||||
navBlock := lipgloss.JoinVertical(lipgloss.Left,
|
||||
navHeader,
|
||||
lipgloss.JoinHorizontal(lipgloss.Left, key1, keyDesc1),
|
||||
lipgloss.JoinHorizontal(lipgloss.Left, key2, keyDesc2),
|
||||
lipgloss.JoinHorizontal(lipgloss.Left, key3, keyDesc3),
|
||||
lipgloss.JoinHorizontal(lipgloss.Left, key1, " ", keyDesc1),
|
||||
lipgloss.JoinHorizontal(lipgloss.Left, key2, " ", keyDesc2),
|
||||
)
|
||||
|
||||
guides := lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
usageBlock,
|
||||
lipgloss.NewStyle().Width(8).Render(""),
|
||||
navBlock,
|
||||
nvimHeader := EmptyHeaderStyle.Render("Neovim Integration")
|
||||
nvim1 := lipgloss.NewStyle().Foreground(ColorText).Render("oug-t/difi.nvim")
|
||||
nvimDesc1 := EmptyCodeStyle.Render("Install plugin")
|
||||
nvim2 := lipgloss.NewStyle().Foreground(ColorText).Render("Press 'e'")
|
||||
nvimDesc2 := EmptyCodeStyle.Render("Edit with context")
|
||||
|
||||
nvimBlock := lipgloss.JoinVertical(lipgloss.Left,
|
||||
nvimHeader,
|
||||
lipgloss.JoinHorizontal(lipgloss.Left, nvim1, " ", nvimDesc1),
|
||||
lipgloss.JoinHorizontal(lipgloss.Left, nvim2, " ", nvimDesc2),
|
||||
)
|
||||
|
||||
var guides string
|
||||
if w > 80 {
|
||||
guides = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
usageBlock,
|
||||
lipgloss.NewStyle().Width(6).Render(""),
|
||||
navBlock,
|
||||
lipgloss.NewStyle().Width(6).Render(""),
|
||||
nvimBlock,
|
||||
)
|
||||
} else {
|
||||
topRow := lipgloss.JoinHorizontal(lipgloss.Top, usageBlock, lipgloss.NewStyle().Width(4).Render(""), navBlock)
|
||||
guides = lipgloss.JoinVertical(lipgloss.Left,
|
||||
topRow,
|
||||
lipgloss.NewStyle().Height(1).Render(""),
|
||||
nvimBlock,
|
||||
)
|
||||
}
|
||||
|
||||
content := lipgloss.JoinVertical(lipgloss.Center,
|
||||
logo,
|
||||
desc,
|
||||
|
|
@ -540,14 +662,14 @@ func (m Model) viewEmptyState() string {
|
|||
)
|
||||
|
||||
var verticalPad string
|
||||
if m.height > lipgloss.Height(content) {
|
||||
lines := (m.height - lipgloss.Height(content)) / 2
|
||||
if h > lipgloss.Height(content) {
|
||||
lines := (h - lipgloss.Height(content)) / 2
|
||||
verticalPad = strings.Repeat("\n", lines)
|
||||
}
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Top,
|
||||
verticalPad,
|
||||
lipgloss.PlaceHorizontal(m.width, lipgloss.Center, content),
|
||||
lipgloss.PlaceHorizontal(w, lipgloss.Center, content),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,118 +1,74 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/oug-t/difi/internal/config"
|
||||
)
|
||||
import "github.com/charmbracelet/lipgloss"
|
||||
|
||||
var (
|
||||
// Config
|
||||
CurrentConfig config.Config
|
||||
// -- NORD PALETTE --
|
||||
nord0 = lipgloss.Color("#2E3440") // Dark background
|
||||
nord3 = lipgloss.Color("#4C566A") // Separators / Dimmed
|
||||
nord4 = lipgloss.Color("#D8DEE9") // Main Text
|
||||
nord11 = lipgloss.Color("#BF616A") // Red (Deleted)
|
||||
nord14 = lipgloss.Color("#A3BE8C") // Green (Added)
|
||||
nord9 = lipgloss.Color("#81A1C1") // Blue (Focus)
|
||||
|
||||
// Theme colors
|
||||
ColorBorder = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"}
|
||||
ColorFocus = lipgloss.AdaptiveColor{Light: "#000000", Dark: "#E5E5E5"}
|
||||
ColorText = lipgloss.AdaptiveColor{Light: "#1F1F1F", Dark: "#FFFFFF"}
|
||||
ColorSubtle = lipgloss.AdaptiveColor{Light: "#A8A8A8", Dark: "#D0D0D0"}
|
||||
ColorCursorBg = lipgloss.AdaptiveColor{Light: "#E5E5E5", Dark: "#3E3E3E"}
|
||||
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)
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(nord3). // Goal 1: Nord3 Separator
|
||||
Padding(0, 1)
|
||||
|
||||
FocusedPaneStyle = PaneStyle.Copy().
|
||||
BorderForeground(ColorFocus)
|
||||
BorderForeground(nord9)
|
||||
|
||||
DiffStyle = lipgloss.NewStyle().Padding(0, 0)
|
||||
ItemStyle = lipgloss.NewStyle().PaddingLeft(2)
|
||||
// -- TOP BAR STYLES (Goal 2) --
|
||||
TopBarStyle = lipgloss.NewStyle().
|
||||
Background(nord0).
|
||||
Foreground(nord4).
|
||||
Height(1)
|
||||
|
||||
// List styles
|
||||
SelectedItemStyle = lipgloss.NewStyle().
|
||||
PaddingLeft(1).
|
||||
Background(ColorCursorBg).
|
||||
Foreground(ColorText).
|
||||
Bold(true).
|
||||
Width(1000)
|
||||
TopInfoStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Padding(0, 1)
|
||||
|
||||
SelectedBlockStyle = lipgloss.NewStyle().
|
||||
Background(ColorCursorBg).
|
||||
Foreground(ColorText).
|
||||
Bold(true).
|
||||
TopStatsAddedStyle = lipgloss.NewStyle().
|
||||
Foreground(nord14).
|
||||
PaddingLeft(1)
|
||||
|
||||
// Icon styles
|
||||
FolderIconStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#F7B96E", Dark: "#E5C07B"})
|
||||
FileIconStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#969696", Dark: "#ABB2BF"})
|
||||
TopStatsDeletedStyle = lipgloss.NewStyle().
|
||||
Foreground(nord11).
|
||||
PaddingLeft(1).
|
||||
PaddingRight(1)
|
||||
|
||||
// Diff view styles
|
||||
LineNumberStyle = lipgloss.NewStyle().
|
||||
Foreground(ColorSubtle).
|
||||
PaddingRight(1).
|
||||
Width(4)
|
||||
// -- TREE STYLES --
|
||||
DirectoryStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("99"))
|
||||
FileStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("252"))
|
||||
|
||||
DiffSelectionStyle = lipgloss.NewStyle().
|
||||
Background(ColorCursorBg).
|
||||
Width(1000)
|
||||
// -- DIFF VIEW STYLES --
|
||||
DiffStyle = lipgloss.NewStyle().Padding(0, 0)
|
||||
DiffSelectionStyle = lipgloss.NewStyle().Background(lipgloss.Color("237")).Foreground(lipgloss.Color("255"))
|
||||
LineNumberStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Width(4).Align(lipgloss.Right).MarginRight(1)
|
||||
|
||||
// Status bar colors
|
||||
ColorBarBg = lipgloss.AdaptiveColor{Light: "#F2F2F2", Dark: "#1F1F1F"}
|
||||
ColorBarFg = lipgloss.AdaptiveColor{Light: "#6E6E6E", Dark: "#9E9E9E"}
|
||||
// -- EMPTY STATE STYLES --
|
||||
EmptyLogoStyle = lipgloss.NewStyle().Foreground(nord9).Bold(true).MarginBottom(1)
|
||||
EmptyDescStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240")).MarginBottom(1)
|
||||
EmptyStatusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("245")).MarginBottom(2)
|
||||
EmptyHeaderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Bold(true).MarginBottom(1)
|
||||
EmptyCodeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
|
||||
|
||||
// Status bar styles
|
||||
StatusBarStyle = lipgloss.NewStyle().
|
||||
Foreground(ColorBarFg).
|
||||
Background(ColorBarBg).
|
||||
Padding(0, 1)
|
||||
// -- HELPER STYLES --
|
||||
HelpDrawerStyle = lipgloss.NewStyle().Border(lipgloss.NormalBorder(), true, false, false, false).BorderForeground(nord3).Padding(1, 2)
|
||||
HelpTextStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).MarginRight(2)
|
||||
|
||||
StatusKeyStyle = lipgloss.NewStyle().
|
||||
Foreground(ColorText).
|
||||
Background(ColorBarBg).
|
||||
Bold(true).
|
||||
Padding(0, 1)
|
||||
// -- BOTTOM STATUS BAR STYLES --
|
||||
StatusBarStyle = lipgloss.NewStyle().Background(nord0).Foreground(nord4).Height(1)
|
||||
StatusKeyStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Padding(0, 1)
|
||||
StatusRepoStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#7aa2f7")).Padding(0, 1)
|
||||
StatusBranchStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#bb9af7")).Padding(0, 1)
|
||||
StatusAddedStyle = lipgloss.NewStyle().Foreground(nord14).Padding(0, 1)
|
||||
StatusDeletedStyle = lipgloss.NewStyle().Foreground(nord11).Padding(0, 1)
|
||||
StatusDividerStyle = lipgloss.NewStyle().Foreground(nord3).Padding(0, 1)
|
||||
|
||||
StatusDividerStyle = lipgloss.NewStyle().
|
||||
Foreground(ColorSubtle).
|
||||
Background(ColorBarBg).
|
||||
Padding(0, 0)
|
||||
|
||||
// Help styles
|
||||
HelpTextStyle = lipgloss.NewStyle().
|
||||
Foreground(ColorSubtle).
|
||||
Padding(0, 1)
|
||||
|
||||
HelpDrawerStyle = lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder(), true, false, false, false).
|
||||
BorderForeground(ColorBorder).
|
||||
Padding(1, 2)
|
||||
|
||||
// Empty/landing styles
|
||||
EmptyLogoStyle = lipgloss.NewStyle().
|
||||
Foreground(ColorAccent).
|
||||
Bold(true).
|
||||
PaddingBottom(1)
|
||||
|
||||
EmptyDescStyle = lipgloss.NewStyle().
|
||||
Foreground(ColorSubtle).
|
||||
PaddingBottom(2)
|
||||
|
||||
EmptyStatusStyle = lipgloss.NewStyle().
|
||||
Foreground(ColorText).
|
||||
Background(ColorCursorBg).
|
||||
Padding(0, 2).
|
||||
MarginBottom(2)
|
||||
|
||||
EmptyCodeStyle = lipgloss.NewStyle().
|
||||
Foreground(ColorSubtle).
|
||||
MarginLeft(2)
|
||||
|
||||
EmptyHeaderStyle = lipgloss.NewStyle().
|
||||
Foreground(ColorText).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
ColorText = lipgloss.Color("252")
|
||||
)
|
||||
|
||||
func InitStyles(cfg config.Config) {
|
||||
CurrentConfig = cfg
|
||||
}
|
||||
func InitStyles(cfg interface{}) {}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user