From 1537daa077d6945b63432ea42315809345cda449 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Fri, 27 Sep 2024 23:16:06 +0300 Subject: [PATCH 01/18] docs: update picture --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 85297b3..ce16d29 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ A git diff pager based on [delta](https://github.com/dandavison/delta) but with a file tree, ร  la GitHub.

- +

> [!CAUTION] From d5b7641eb7c94c7c7da5186acdad6dde696f1636 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Sat, 28 Sep 2024 00:36:14 +0300 Subject: [PATCH 02/18] fix: tree height --- filetree.go | 2 +- main.go | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/filetree.go b/filetree.go index d7594f3..c4e79b2 100644 --- a/filetree.go +++ b/filetree.go @@ -263,7 +263,7 @@ func truncateTree(t *tree.Tree, depth int, numNodes int, numChildren int) (*tree } func applyStyles(t *tree.Tree, selectedFile *string) { - enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")).PaddingRight(1) + enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).PaddingRight(1) rootStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("4")) t.Enumerator(enumerator).Indenter(indenter). EnumeratorStyle(enumeratorStyle). diff --git a/main.go b/main.go index 9f7cf6b..0bd367c 100644 --- a/main.go +++ b/main.go @@ -235,7 +235,8 @@ func (m mainModel) View() string { Border(lipgloss.NormalBorder(), false, false, true, false). BorderForeground(lipgloss.Color("8")). Foreground(lipgloss.Color("6")). - Render("๓ฐŠข ๐Ÿ…ณ ๐Ÿ…ธ ๐Ÿ…ต ๐Ÿ…ต ๐Ÿ…ฝ ๐Ÿ…ฐ ๐Ÿ†… ") + Bold(true). + Render("DIFFNAV") footer := m.footerView() sidebar := "" @@ -322,7 +323,7 @@ func sortFiles(files []*gitdiff.File) { const ( footerHeight = 2 headerHeight = 2 - searchHeight = 10 + searchHeight = 3 ) func (m mainModel) footerView() string { From f9d90a36f9cd0b7f57dd9cc9dd8da82e858e1a5e Mon Sep 17 00:00:00 2001 From: Dolev Hadar <6196971+dlvhdr@users.noreply.github.com> Date: Tue, 1 Oct 2024 23:59:48 +0300 Subject: [PATCH 03/18] fix: don't enter altscreen on empty input (#27) * fix: don't enter altscreen on empty input * fix: exit(1) --- main.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/main.go b/main.go index 0bd367c..cf2c368 100644 --- a/main.go +++ b/main.go @@ -380,6 +380,10 @@ func main() { } input := ansi.Strip(b.String()) + if strings.TrimSpace(input) == "" { + fmt.Println("No input provided, exiting") + os.Exit(1) + } p := tea.NewProgram(newModel(input), tea.WithMouseAllMotion()) if _, err := p.Run(); err != nil { From ebbfbbba9fd47b9b639a74e0a8474572d1a40c89 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Thu, 3 Oct 2024 20:14:06 +0300 Subject: [PATCH 04/18] fix: diff width after exiting search mode --- diffviewer.go | 6 +- main.go | 212 +++++++++++++++++++++++++------------------------- 2 files changed, 108 insertions(+), 110 deletions(-) diff --git a/diffviewer.go b/diffviewer.go index fe66e82..3723a6f 100644 --- a/diffviewer.go +++ b/diffviewer.go @@ -13,7 +13,7 @@ import ( "github.com/charmbracelet/lipgloss" ) -const dirrHeaderHeight = 3 +const dirHeaderHeight = 3 type diffModel struct { vp viewport.Model @@ -54,7 +54,7 @@ func (m diffModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.width = msg.Width m.height = msg.Height m.vp.Width = m.width - m.vp.Height = m.height - dirrHeaderHeight + m.vp.Height = m.height - dirHeaderHeight cmds = append(cmds, diff(m.file, m.width)) } @@ -96,7 +96,7 @@ func (m diffModel) headerView() string { return base. Width(m.width). PaddingLeft(1). - Height(dirrHeaderHeight - 1). + Height(dirHeaderHeight - 1). BorderStyle(lipgloss.NormalBorder()). BorderBottom(true). BorderForeground(lipgloss.Color("8")). diff --git a/main.go b/main.go index cf2c368..82f9374 100644 --- a/main.go +++ b/main.go @@ -63,7 +63,7 @@ func newModel(input string) mainModel { m.search.KeyMap.AcceptSuggestion = key.NewBinding(key.WithKeys("tab")) m.search.Prompt = "๏ข " m.search.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) - m.search.Placeholder = "Filter files ๐Ÿ…ƒ" + m.search.Placeholder = "Filter files ๓ฐฌ› " m.search.PlaceholderStyle = lipgloss.NewStyle().MaxWidth(lipgloss.Width(m.search.Placeholder)).Foreground(lipgloss.Color("8")) m.search.Width = constants.OpenFileTreeWidth - 5 @@ -80,28 +80,108 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd var cmds []tea.Cmd + if !m.searching { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + return m, tea.Quit + case "t": + m.searching = true + m.search.Width = m.sidebarWidth() - 5 + m.search.SetValue("") + m.resultsCursor = 0 + m.filtered = make([]string, 0) + + m.resultsVp.Width = constants.SearchingFileTreeWidth + m.resultsVp.Height = m.height - footerHeight - headerHeight - searchHeight + m.resultsVp.SetContent(m.resultsView()) + + df, dfCmd := m.setDiffViewerDimensions() + cmds = append(cmds, dfCmd) + m.diffViewer = df + cmds = append(cmds, m.search.Focus()) + case "e": + m.isShowingFileTree = !m.isShowingFileTree + df, dfCmd := m.setDiffViewerDimensions() + cmds = append(cmds, dfCmd) + m.diffViewer = df + case "up", "k", "ctrl+p": + if m.cursor > 0 { + m.cursor-- + m.diffViewer, cmd = m.diffViewer.(diffModel).SetFilePatch(m.files[m.cursor]) + cmds = append(cmds, cmd) + } + case "down", "j", "ctrl+n": + if m.cursor < len(m.files)-1 { + m.cursor++ + m.diffViewer, cmd = m.diffViewer.(diffModel).SetFilePatch(m.files[m.cursor]) + cmds = append(cmds, cmd) + } + } + + case tea.WindowSizeMsg: + m.help.Width = msg.Width + m.width = msg.Width + m.height = msg.Height + df, dfCmd := m.setDiffViewerDimensions() + m.diffViewer = df + cmds = append(cmds, dfCmd) + ft, ftCmd := m.fileTree.(ftModel).Update(dimensionsMsg{Width: m.sidebarWidth(), Height: m.height - footerHeight - headerHeight - searchHeight}) + m.fileTree = ft + cmds = append(cmds, ftCmd) + + case fileTreeMsg: + m.files = msg.files + if len(m.files) == 0 { + return m, tea.Quit + } + m.fileTree = m.fileTree.(ftModel).SetFiles(m.files) + m.diffViewer, cmd = m.diffViewer.(diffModel).SetFilePatch(m.files[0]) + cmds = append(cmds, cmd) + + case errMsg: + fmt.Printf("Error: %v\n", msg.err) + log.Fatal(msg.err) + } + } else { + var sCmds []tea.Cmd + m, sCmds = m.searchUpdate(msg) + cmds = append(cmds, sCmds...) + } + + m.fileTree = m.fileTree.(ftModel).SetCursor(m.cursor) + + m.diffViewer, cmd = m.diffViewer.Update(msg) + cmds = append(cmds, cmd) + + m.fileTree, cmd = m.fileTree.Update(msg) + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +func (m mainModel) searchUpdate(msg tea.Msg) (mainModel, []tea.Cmd) { + var cmd tea.Cmd + var cmds []tea.Cmd if m.search.Focused() { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "esc": + m.stopSearch() df, dfCmd := m.setDiffViewerDimensions() - cmds = append(cmds, dfCmd) m.diffViewer = df - m.search.SetValue("") - m.search.Blur() - m.searching = false - break + cmds = append(cmds, dfCmd) case "ctrl+c": - return m, tea.Quit + return m, []tea.Cmd{tea.Quit} case "enter": - m.searching = false - m.search.SetValue("") - m.search.Blur() - selected := m.filtered[m.resultsCursor] + m.stopSearch() df, dfCmd := m.setDiffViewerDimensions() cmds = append(cmds, dfCmd) m.diffViewer = df + + selected := m.filtered[m.resultsCursor] for i, f := range m.files { if filetree.GetFileName(f) == selected { m.cursor = i @@ -111,7 +191,6 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } - return m, tea.Batch(cmds...) case "ctrl+n", "down": m.resultsCursor = min(len(m.files)-1, m.resultsCursor+1) m.resultsVp.LineDown(1) @@ -133,101 +212,9 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } m.filtered = filtered m.resultsVp.SetContent(m.resultsView()) - - return m, tea.Batch(cmds...) - } - - switch msg := msg.(type) { - case tea.KeyMsg: - if m.searching { - switch msg.String() { - case "ctrl+n": - if m.searching { - m.resultsCursor = min(len(m.files)-1, m.resultsCursor+1) - m.resultsVp.LineDown(1) - } - case "ctrl+p": - if m.searching { - m.resultsCursor = max(0, m.resultsCursor-1) - m.resultsVp.LineUp(1) - } - } - } - switch msg.String() { - case "ctrl+c", "q": - return m, tea.Quit - case "t": - m.searching = true - m.search.Width = m.sidebarWidth() - 5 - m.search.SetValue("") - m.resultsCursor = 0 - m.filtered = make([]string, 0) - m.resultsVp.SetContent(m.resultsView()) - m.resultsVp.Height = m.height - footerHeight - headerHeight - searchHeight - m.resultsVp.Width = constants.SearchingFileTreeWidth - df, dfCmd := m.setDiffViewerDimensions() - cmds = append(cmds, dfCmd) - m.diffViewer = df - cmds = append(cmds, m.search.Focus()) - return m, tea.Batch(cmds...) - case "e": - m.isShowingFileTree = !m.isShowingFileTree - df, dfCmd := m.setDiffViewerDimensions() - m.diffViewer = df - return m, dfCmd - case "up", "k", "ctrl+p": - if m.cursor > 0 { - m.cursor-- - m.diffViewer, cmd = m.diffViewer.(diffModel).SetFilePatch(m.files[m.cursor]) - cmds = append(cmds, cmd) - } - case "down", "j", "ctrl+n": - if m.cursor < len(m.files)-1 { - m.cursor++ - m.diffViewer, cmd = m.diffViewer.(diffModel).SetFilePatch(m.files[m.cursor]) - cmds = append(cmds, cmd) - } - } - - case tea.WindowSizeMsg: - m.help.Width = msg.Width - m.width = msg.Width - m.height = msg.Height - df, dfCmd := m.diffViewer.(diffModel).Update(dimensionsMsg{Width: m.width - m.sidebarWidth(), Height: m.height - footerHeight - headerHeight}) - m.diffViewer = df - cmds = append(cmds, dfCmd) - ft, ftCmd := m.fileTree.(ftModel).Update(dimensionsMsg{Width: m.sidebarWidth(), Height: m.height - footerHeight - headerHeight - searchHeight}) - m.fileTree = ft - cmds = append(cmds, ftCmd) - - case fileTreeMsg: - m.files = msg.files - if len(m.files) == 0 { - return m, tea.Quit - } - m.fileTree = m.fileTree.(ftModel).SetFiles(m.files) - m.diffViewer, cmd = m.diffViewer.(diffModel).SetFilePatch(m.files[0]) - cmds = append(cmds, cmd) - - case errMsg: - fmt.Printf("Error: %v\n", msg.err) - log.Fatal(msg.err) } - s, sCmd := m.search.Update(msg) - cmds = append(cmds, sCmd) - m.search = s - m.search.Width = m.sidebarWidth() - 5 - - m.fileTree = m.fileTree.(ftModel).SetCursor(m.cursor) - - m.diffViewer, cmd = m.diffViewer.Update(msg) - cmds = append(cmds, cmd) - - m.fileTree, cmd = m.fileTree.Update(msg) - cmds = append(cmds, cmd) - - return m, tea.Batch(cmds...) + return m, cmds } func (m mainModel) View() string { @@ -261,12 +248,16 @@ func (m mainModel) View() string { Height(m.height - footerHeight - headerHeight).Render(lipgloss.JoinVertical(lipgloss.Left, search, content)) sidebar = lipgloss.NewStyle(). + Width(width). Border(lipgloss.NormalBorder(), false, true, false, false). BorderForeground(lipgloss.Color("8")).Render(content) } dv := lipgloss.NewStyle().MaxHeight(m.height - footerHeight - headerHeight).Width(m.width - m.sidebarWidth()).Render(m.diffViewer.View()) - content := lipgloss.JoinHorizontal(lipgloss.Top, sidebar, dv) - return lipgloss.JoinVertical(lipgloss.Left, header, content, footer) + return lipgloss.JoinVertical(lipgloss.Left, + header, + lipgloss.JoinHorizontal(lipgloss.Top, sidebar, dv), + footer, + ) } type dimensionsMsg struct { @@ -418,3 +409,10 @@ func (m mainModel) setDiffViewerDimensions() (tea.Model, tea.Cmd) { df, dfCmd := m.diffViewer.(diffModel).Update(dimensionsMsg{Width: m.width - m.sidebarWidth(), Height: m.height - footerHeight - headerHeight}) return df, dfCmd } + +func (m *mainModel) stopSearch() { + m.searching = false + m.search.SetValue("") + m.search.Blur() + m.search.Width = m.sidebarWidth() - 5 +} From 3f9a3cd8008d9a0167feacb8badff692d519ad24 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Thu, 3 Oct 2024 23:58:12 +0300 Subject: [PATCH 05/18] refactor: move to subpackages and introduce component with a setSize --- main.go | 351 +----------------- pkg/{file_tree => filenode}/file_node.go | 2 +- pkg/ui/common/component.go | 14 + pkg/ui/common/msgs.go | 5 + keys.go => pkg/ui/keys.go | 2 +- pkg/ui/mainModel.go | 349 +++++++++++++++++ .../ui/panes/diffviewer/diffviewer.go | 43 ++- .../ui/panes/filetree/filetree.go | 56 ++- 8 files changed, 422 insertions(+), 400 deletions(-) rename pkg/{file_tree => filenode}/file_node.go (98%) create mode 100644 pkg/ui/common/component.go create mode 100644 pkg/ui/common/msgs.go rename keys.go => pkg/ui/keys.go (98%) create mode 100644 pkg/ui/mainModel.go rename diffviewer.go => pkg/ui/panes/diffviewer/diffviewer.go (78%) rename filetree.go => pkg/ui/panes/filetree/filetree.go (86%) diff --git a/main.go b/main.go index 82f9374..0b065ff 100644 --- a/main.go +++ b/main.go @@ -5,328 +5,16 @@ import ( "fmt" "io" "os" - "path/filepath" - "slices" "strings" "time" - "github.com/bluekeyes/go-gitdiff/gitdiff" - "github.com/charmbracelet/bubbles/help" - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/textinput" - "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/log" "github.com/charmbracelet/x/ansi" - "github.com/dlvhdr/diffnav/pkg/constants" - filetree "github.com/dlvhdr/diffnav/pkg/file_tree" - "github.com/dlvhdr/diffnav/pkg/utils" + "github.com/dlvhdr/diffnav/pkg/ui" ) -type mainModel struct { - input string - files []*gitdiff.File - cursor int - fileTree tea.Model - diffViewer tea.Model - width int - height int - isShowingFileTree bool - search textinput.Model - help help.Model - resultsVp viewport.Model - resultsCursor int - searching bool - filtered []string -} - -func newModel(input string) mainModel { - m := mainModel{input: input, isShowingFileTree: true} - m.fileTree = initialFileTreeModel() - m.diffViewer = initialDiffModel() - - m.help = help.New() - helpSt := lipgloss.NewStyle() - m.help.ShortSeparator = " ยท " - m.help.Styles.ShortKey = helpSt - m.help.Styles.ShortDesc = helpSt - m.help.Styles.ShortSeparator = helpSt - m.help.Styles.ShortKey = helpSt.Foreground(lipgloss.Color("254")) - m.help.Styles.ShortDesc = helpSt - m.help.Styles.ShortSeparator = helpSt - m.help.Styles.Ellipsis = helpSt - - m.search = textinput.New() - m.search.ShowSuggestions = true - m.search.KeyMap.AcceptSuggestion = key.NewBinding(key.WithKeys("tab")) - m.search.Prompt = "๏ข " - m.search.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) - m.search.Placeholder = "Filter files ๓ฐฌ› " - m.search.PlaceholderStyle = lipgloss.NewStyle().MaxWidth(lipgloss.Width(m.search.Placeholder)).Foreground(lipgloss.Color("8")) - m.search.Width = constants.OpenFileTreeWidth - 5 - - m.resultsVp = viewport.Model{} - - return m -} - -func (m mainModel) Init() tea.Cmd { - return tea.Batch(tea.EnterAltScreen, m.fetchFileTree, m.diffViewer.Init()) -} - -func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - var cmds []tea.Cmd - - if !m.searching { - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "ctrl+c", "q": - return m, tea.Quit - case "t": - m.searching = true - m.search.Width = m.sidebarWidth() - 5 - m.search.SetValue("") - m.resultsCursor = 0 - m.filtered = make([]string, 0) - - m.resultsVp.Width = constants.SearchingFileTreeWidth - m.resultsVp.Height = m.height - footerHeight - headerHeight - searchHeight - m.resultsVp.SetContent(m.resultsView()) - - df, dfCmd := m.setDiffViewerDimensions() - cmds = append(cmds, dfCmd) - m.diffViewer = df - cmds = append(cmds, m.search.Focus()) - case "e": - m.isShowingFileTree = !m.isShowingFileTree - df, dfCmd := m.setDiffViewerDimensions() - cmds = append(cmds, dfCmd) - m.diffViewer = df - case "up", "k", "ctrl+p": - if m.cursor > 0 { - m.cursor-- - m.diffViewer, cmd = m.diffViewer.(diffModel).SetFilePatch(m.files[m.cursor]) - cmds = append(cmds, cmd) - } - case "down", "j", "ctrl+n": - if m.cursor < len(m.files)-1 { - m.cursor++ - m.diffViewer, cmd = m.diffViewer.(diffModel).SetFilePatch(m.files[m.cursor]) - cmds = append(cmds, cmd) - } - } - - case tea.WindowSizeMsg: - m.help.Width = msg.Width - m.width = msg.Width - m.height = msg.Height - df, dfCmd := m.setDiffViewerDimensions() - m.diffViewer = df - cmds = append(cmds, dfCmd) - ft, ftCmd := m.fileTree.(ftModel).Update(dimensionsMsg{Width: m.sidebarWidth(), Height: m.height - footerHeight - headerHeight - searchHeight}) - m.fileTree = ft - cmds = append(cmds, ftCmd) - - case fileTreeMsg: - m.files = msg.files - if len(m.files) == 0 { - return m, tea.Quit - } - m.fileTree = m.fileTree.(ftModel).SetFiles(m.files) - m.diffViewer, cmd = m.diffViewer.(diffModel).SetFilePatch(m.files[0]) - cmds = append(cmds, cmd) - - case errMsg: - fmt.Printf("Error: %v\n", msg.err) - log.Fatal(msg.err) - } - } else { - var sCmds []tea.Cmd - m, sCmds = m.searchUpdate(msg) - cmds = append(cmds, sCmds...) - } - - m.fileTree = m.fileTree.(ftModel).SetCursor(m.cursor) - - m.diffViewer, cmd = m.diffViewer.Update(msg) - cmds = append(cmds, cmd) - - m.fileTree, cmd = m.fileTree.Update(msg) - cmds = append(cmds, cmd) - - return m, tea.Batch(cmds...) -} - -func (m mainModel) searchUpdate(msg tea.Msg) (mainModel, []tea.Cmd) { - var cmd tea.Cmd - var cmds []tea.Cmd - if m.search.Focused() { - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "esc": - m.stopSearch() - df, dfCmd := m.setDiffViewerDimensions() - m.diffViewer = df - cmds = append(cmds, dfCmd) - case "ctrl+c": - return m, []tea.Cmd{tea.Quit} - case "enter": - m.stopSearch() - df, dfCmd := m.setDiffViewerDimensions() - cmds = append(cmds, dfCmd) - m.diffViewer = df - - selected := m.filtered[m.resultsCursor] - for i, f := range m.files { - if filetree.GetFileName(f) == selected { - m.cursor = i - m.diffViewer, cmd = m.diffViewer.(diffModel).SetFilePatch(f) - cmds = append(cmds, cmd) - break - } - } - - case "ctrl+n", "down": - m.resultsCursor = min(len(m.files)-1, m.resultsCursor+1) - m.resultsVp.LineDown(1) - case "ctrl+p", "up": - m.resultsCursor = max(0, m.resultsCursor-1) - m.resultsVp.LineUp(1) - default: - m.resultsCursor = 0 - } - } - s, sc := m.search.Update(msg) - cmds = append(cmds, sc) - m.search = s - filtered := make([]string, 0) - for _, f := range m.files { - if strings.Contains(strings.ToLower(filetree.GetFileName(f)), strings.ToLower(m.search.Value())) { - filtered = append(filtered, filetree.GetFileName(f)) - } - } - m.filtered = filtered - m.resultsVp.SetContent(m.resultsView()) - } - - return m, cmds -} - -func (m mainModel) View() string { - header := lipgloss.NewStyle().Width(m.width). - Border(lipgloss.NormalBorder(), false, false, true, false). - BorderForeground(lipgloss.Color("8")). - Foreground(lipgloss.Color("6")). - Bold(true). - Render("DIFFNAV") - footer := m.footerView() - - sidebar := "" - if m.isShowingFileTree { - search := lipgloss.NewStyle(). - Border(lipgloss.RoundedBorder()). - BorderForeground(lipgloss.Color("8")). - MaxHeight(3). - Width(m.sidebarWidth() - 2). - Render(m.search.View()) - - content := "" - width := m.sidebarWidth() - if m.searching { - content = m.resultsVp.View() - } else { - content = m.fileTree.View() - } - - content = lipgloss.NewStyle(). - Width(width). - Height(m.height - footerHeight - headerHeight).Render(lipgloss.JoinVertical(lipgloss.Left, search, content)) - - sidebar = lipgloss.NewStyle(). - Width(width). - Border(lipgloss.NormalBorder(), false, true, false, false). - BorderForeground(lipgloss.Color("8")).Render(content) - } - dv := lipgloss.NewStyle().MaxHeight(m.height - footerHeight - headerHeight).Width(m.width - m.sidebarWidth()).Render(m.diffViewer.View()) - return lipgloss.JoinVertical(lipgloss.Left, - header, - lipgloss.JoinHorizontal(lipgloss.Top, sidebar, dv), - footer, - ) -} - -type dimensionsMsg struct { - Width int - Height int -} - -func (m mainModel) fetchFileTree() tea.Msg { - // TODO: handle error - files, _, err := gitdiff.Parse(strings.NewReader(m.input + "\n")) - if err != nil { - return errMsg{err} - } - sortFiles(files) - - return fileTreeMsg{files: files} -} - -type fileTreeMsg struct { - files []*gitdiff.File -} - -func sortFiles(files []*gitdiff.File) { - slices.SortFunc(files, func(a *gitdiff.File, b *gitdiff.File) int { - nameA := filetree.GetFileName(a) - nameB := filetree.GetFileName(b) - dira := filepath.Dir(nameA) - dirb := filepath.Dir(nameB) - if dira != "." && dirb != "." && dira == dirb { - return strings.Compare(strings.ToLower(nameA), strings.ToLower(nameB)) - } - - if dira != "." && dirb == "." { - return -1 - } - if dirb != "." && dira == "." { - return 1 - } - - if dira != "." && dirb != "." { - if strings.HasPrefix(dira, dirb) { - return -1 - } - - if strings.HasPrefix(dirb, dira) { - return 1 - } - } - - return strings.Compare(strings.ToLower(nameA), strings.ToLower(nameB)) - }) -} - -const ( - footerHeight = 2 - headerHeight = 2 - searchHeight = 3 -) - -func (m mainModel) footerView() string { - return lipgloss.NewStyle(). - Width(m.width). - Border(lipgloss.NormalBorder(), true, false, false, false). - BorderForeground(lipgloss.Color("8")). - Height(1). - Render(m.help.ShortHelpView(getKeys())) - -} - func main() { stat, err := os.Stdin.Stat() if err != nil { @@ -375,44 +63,9 @@ func main() { fmt.Println("No input provided, exiting") os.Exit(1) } - p := tea.NewProgram(newModel(input), tea.WithMouseAllMotion()) + p := tea.NewProgram(ui.New(input), tea.WithMouseAllMotion()) if _, err := p.Run(); err != nil { log.Fatal(err) } } - -func (m mainModel) resultsView() string { - sb := strings.Builder{} - for i, f := range m.filtered { - fName := utils.TruncateString("๏’ฅ "+f, constants.SearchingFileTreeWidth-2) - if i == m.resultsCursor { - sb.WriteString(lipgloss.NewStyle().Background(lipgloss.Color("#1b1b33")).Bold(true).Render(fName) + "\n") - } else { - sb.WriteString(fName + "\n") - } - } - return sb.String() -} - -func (m mainModel) sidebarWidth() int { - if m.searching { - return constants.SearchingFileTreeWidth - } else if m.isShowingFileTree { - return constants.OpenFileTreeWidth - } else { - return 0 - } -} - -func (m mainModel) setDiffViewerDimensions() (tea.Model, tea.Cmd) { - df, dfCmd := m.diffViewer.(diffModel).Update(dimensionsMsg{Width: m.width - m.sidebarWidth(), Height: m.height - footerHeight - headerHeight}) - return df, dfCmd -} - -func (m *mainModel) stopSearch() { - m.searching = false - m.search.SetValue("") - m.search.Blur() - m.search.Width = m.sidebarWidth() - 5 -} diff --git a/pkg/file_tree/file_node.go b/pkg/filenode/file_node.go similarity index 98% rename from pkg/file_tree/file_node.go rename to pkg/filenode/file_node.go index 144e975..9409e20 100644 --- a/pkg/file_tree/file_node.go +++ b/pkg/filenode/file_node.go @@ -1,4 +1,4 @@ -package filetree +package filenode import ( "path/filepath" diff --git a/pkg/ui/common/component.go b/pkg/ui/common/component.go new file mode 100644 index 0000000..0cc206b --- /dev/null +++ b/pkg/ui/common/component.go @@ -0,0 +1,14 @@ +package common + +import ( + tea "github.com/charmbracelet/bubbletea" +) + +// Common is a struct that contains the width and height of a component. +type Common struct { + Width, Height int +} + +type Component interface { + SetSize(width, height int) tea.Cmd +} diff --git a/pkg/ui/common/msgs.go b/pkg/ui/common/msgs.go new file mode 100644 index 0000000..cd8e1fa --- /dev/null +++ b/pkg/ui/common/msgs.go @@ -0,0 +1,5 @@ +package common + +type ErrMsg struct { + Err error +} diff --git a/keys.go b/pkg/ui/keys.go similarity index 98% rename from keys.go rename to pkg/ui/keys.go index 609fee8..7d0e706 100644 --- a/keys.go +++ b/pkg/ui/keys.go @@ -1,4 +1,4 @@ -package main +package ui import "github.com/charmbracelet/bubbles/key" diff --git a/pkg/ui/mainModel.go b/pkg/ui/mainModel.go new file mode 100644 index 0000000..e2f9d44 --- /dev/null +++ b/pkg/ui/mainModel.go @@ -0,0 +1,349 @@ +package ui + +import ( + "fmt" + "path/filepath" + "slices" + "strings" + + "github.com/bluekeyes/go-gitdiff/gitdiff" + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textinput" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/log" + + "github.com/dlvhdr/diffnav/pkg/constants" + "github.com/dlvhdr/diffnav/pkg/filenode" + "github.com/dlvhdr/diffnav/pkg/ui/common" + "github.com/dlvhdr/diffnav/pkg/ui/panes/diffviewer" + "github.com/dlvhdr/diffnav/pkg/ui/panes/filetree" + "github.com/dlvhdr/diffnav/pkg/utils" +) + +const ( + footerHeight = 2 + headerHeight = 2 + searchHeight = 3 +) + +type mainModel struct { + input string + files []*gitdiff.File + cursor int + fileTree filetree.Model + diffViewer diffviewer.Model + width int + height int + isShowingFileTree bool + search textinput.Model + help help.Model + resultsVp viewport.Model + resultsCursor int + searching bool + filtered []string +} + +func New(input string) mainModel { + m := mainModel{input: input, isShowingFileTree: true} + m.fileTree = filetree.New() + m.diffViewer = diffviewer.New() + + m.help = help.New() + helpSt := lipgloss.NewStyle() + m.help.ShortSeparator = " ยท " + m.help.Styles.ShortKey = helpSt + m.help.Styles.ShortDesc = helpSt + m.help.Styles.ShortSeparator = helpSt + m.help.Styles.ShortKey = helpSt.Foreground(lipgloss.Color("254")) + m.help.Styles.ShortDesc = helpSt + m.help.Styles.ShortSeparator = helpSt + m.help.Styles.Ellipsis = helpSt + + m.search = textinput.New() + m.search.ShowSuggestions = true + m.search.KeyMap.AcceptSuggestion = key.NewBinding(key.WithKeys("tab")) + m.search.Prompt = "๏ข " + m.search.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) + m.search.Placeholder = "Filter files ๓ฐฌ› " + m.search.PlaceholderStyle = lipgloss.NewStyle().MaxWidth(lipgloss.Width(m.search.Placeholder)).Foreground(lipgloss.Color("8")) + m.search.Width = constants.OpenFileTreeWidth - 5 + + m.resultsVp = viewport.Model{} + + return m +} + +func (m mainModel) Init() tea.Cmd { + return tea.Batch(tea.EnterAltScreen, m.fetchFileTree, m.diffViewer.Init()) +} + +func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + var cmds []tea.Cmd + + if !m.searching { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + return m, tea.Quit + case "t": + m.searching = true + m.search.Width = m.sidebarWidth() - 5 + m.search.SetValue("") + m.resultsCursor = 0 + m.filtered = make([]string, 0) + + m.resultsVp.Width = constants.SearchingFileTreeWidth + m.resultsVp.Height = m.height - footerHeight - headerHeight - searchHeight + m.resultsVp.SetContent(m.resultsView()) + + dfCmd := m.diffViewer.SetSize(m.width-m.sidebarWidth(), m.height-footerHeight-headerHeight) + cmds = append(cmds, dfCmd, m.search.Focus()) + case "e": + m.isShowingFileTree = !m.isShowingFileTree + dfCmd := m.diffViewer.SetSize(m.width-m.sidebarWidth(), m.height-footerHeight-headerHeight) + cmds = append(cmds, dfCmd) + case "up", "k", "ctrl+p": + if m.cursor > 0 { + m.cursor-- + m.diffViewer, cmd = m.diffViewer.SetFilePatch(m.files[m.cursor]) + cmds = append(cmds, cmd) + } + case "down", "j", "ctrl+n": + if m.cursor < len(m.files)-1 { + m.cursor++ + m.diffViewer, cmd = m.diffViewer.SetFilePatch(m.files[m.cursor]) + cmds = append(cmds, cmd) + } + } + + case tea.WindowSizeMsg: + m.help.Width = msg.Width + m.width = msg.Width + m.height = msg.Height + dfCmd := m.diffViewer.SetSize(m.width-m.sidebarWidth(), m.height-footerHeight-headerHeight) + cmds = append(cmds, dfCmd) + ftCmd := m.fileTree.SetSize(m.sidebarWidth(), m.height-footerHeight-headerHeight-searchHeight) + cmds = append(cmds, ftCmd) + + case fileTreeMsg: + m.files = msg.files + if len(m.files) == 0 { + return m, tea.Quit + } + m.fileTree = m.fileTree.SetFiles(m.files) + m.diffViewer, cmd = m.diffViewer.SetFilePatch(m.files[0]) + cmds = append(cmds, cmd) + + case common.ErrMsg: + fmt.Printf("Error: %v\n", msg.Err) + log.Fatal(msg.Err) + } + } else { + var sCmds []tea.Cmd + m, sCmds = m.searchUpdate(msg) + cmds = append(cmds, sCmds...) + } + + m.fileTree = m.fileTree.SetCursor(m.cursor) + + m.diffViewer, cmd = m.diffViewer.Update(msg) + cmds = append(cmds, cmd) + + m.fileTree, cmd = m.fileTree.Update(msg) + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +func (m mainModel) searchUpdate(msg tea.Msg) (mainModel, []tea.Cmd) { + var cmd tea.Cmd + var cmds []tea.Cmd + if m.search.Focused() { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "esc": + m.stopSearch() + dfCmd := m.diffViewer.SetSize(m.width-m.sidebarWidth(), m.height-footerHeight-headerHeight) + cmds = append(cmds, dfCmd) + case "ctrl+c": + return m, []tea.Cmd{tea.Quit} + case "enter": + m.stopSearch() + dfCmd := m.diffViewer.SetSize(m.width-m.sidebarWidth(), m.height-footerHeight-headerHeight) + cmds = append(cmds, dfCmd) + + selected := m.filtered[m.resultsCursor] + for i, f := range m.files { + if filenode.GetFileName(f) == selected { + m.cursor = i + m.diffViewer, cmd = m.diffViewer.SetFilePatch(f) + cmds = append(cmds, cmd) + break + } + } + + case "ctrl+n", "down": + m.resultsCursor = min(len(m.files)-1, m.resultsCursor+1) + m.resultsVp.LineDown(1) + case "ctrl+p", "up": + m.resultsCursor = max(0, m.resultsCursor-1) + m.resultsVp.LineUp(1) + default: + m.resultsCursor = 0 + } + } + s, sc := m.search.Update(msg) + cmds = append(cmds, sc) + m.search = s + filtered := make([]string, 0) + for _, f := range m.files { + if strings.Contains(strings.ToLower(filenode.GetFileName(f)), strings.ToLower(m.search.Value())) { + filtered = append(filtered, filenode.GetFileName(f)) + } + } + m.filtered = filtered + m.resultsVp.SetContent(m.resultsView()) + } + + return m, cmds +} + +func (m mainModel) View() string { + header := lipgloss.NewStyle().Width(m.width). + Border(lipgloss.NormalBorder(), false, false, true, false). + BorderForeground(lipgloss.Color("8")). + Foreground(lipgloss.Color("6")). + Bold(true). + Render("DIFFNAV") + footer := m.footerView() + + sidebar := "" + if m.isShowingFileTree { + search := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("8")). + MaxHeight(3). + Width(m.sidebarWidth() - 2). + Render(m.search.View()) + + content := "" + width := m.sidebarWidth() + if m.searching { + content = m.resultsVp.View() + } else { + content = m.fileTree.View() + } + + content = lipgloss.NewStyle(). + Width(width). + Height(m.height - footerHeight - headerHeight).Render(lipgloss.JoinVertical(lipgloss.Left, search, content)) + + sidebar = lipgloss.NewStyle(). + Width(width). + Border(lipgloss.NormalBorder(), false, true, false, false). + BorderForeground(lipgloss.Color("8")).Render(content) + } + dv := lipgloss.NewStyle().MaxHeight(m.height - footerHeight - headerHeight).Width(m.width - m.sidebarWidth()).Render(m.diffViewer.View()) + return lipgloss.JoinVertical(lipgloss.Left, + header, + lipgloss.JoinHorizontal(lipgloss.Top, sidebar, dv), + footer, + ) +} + +func (m mainModel) fetchFileTree() tea.Msg { + // TODO: handle error + files, _, err := gitdiff.Parse(strings.NewReader(m.input + "\n")) + if err != nil { + return common.ErrMsg{Err: err} + } + sortFiles(files) + + return fileTreeMsg{files: files} +} + +type fileTreeMsg struct { + files []*gitdiff.File +} + +func sortFiles(files []*gitdiff.File) { + slices.SortFunc(files, func(a *gitdiff.File, b *gitdiff.File) int { + nameA := filenode.GetFileName(a) + nameB := filenode.GetFileName(b) + dira := filepath.Dir(nameA) + dirb := filepath.Dir(nameB) + if dira != "." && dirb != "." && dira == dirb { + return strings.Compare(strings.ToLower(nameA), strings.ToLower(nameB)) + } + + if dira != "." && dirb == "." { + return -1 + } + if dirb != "." && dira == "." { + return 1 + } + + if dira != "." && dirb != "." { + if strings.HasPrefix(dira, dirb) { + return -1 + } + + if strings.HasPrefix(dirb, dira) { + return 1 + } + } + + return strings.Compare(strings.ToLower(nameA), strings.ToLower(nameB)) + }) +} + +func (m mainModel) footerView() string { + return lipgloss.NewStyle(). + Width(m.width). + Border(lipgloss.NormalBorder(), true, false, false, false). + BorderForeground(lipgloss.Color("8")). + Height(1). + Render(m.help.ShortHelpView(getKeys())) + +} + +func (m mainModel) resultsView() string { + sb := strings.Builder{} + for i, f := range m.filtered { + fName := utils.TruncateString("๏’ฅ "+f, constants.SearchingFileTreeWidth-2) + if i == m.resultsCursor { + sb.WriteString(lipgloss.NewStyle().Background(lipgloss.Color("#1b1b33")).Bold(true).Render(fName) + "\n") + } else { + sb.WriteString(fName + "\n") + } + } + return sb.String() +} + +func (m mainModel) sidebarWidth() int { + if m.searching { + return constants.SearchingFileTreeWidth + } else if m.isShowingFileTree { + return constants.OpenFileTreeWidth + } else { + return 0 + } +} + +func (m mainModel) setDiffViewerDimensions() tea.Cmd { + dfCmd := m.diffViewer.SetSize(m.width-m.sidebarWidth(), m.height-footerHeight-headerHeight) + return dfCmd +} + +func (m *mainModel) stopSearch() { + m.searching = false + m.search.SetValue("") + m.search.Blur() + m.search.Width = m.sidebarWidth() - 5 +} diff --git a/diffviewer.go b/pkg/ui/panes/diffviewer/diffviewer.go similarity index 78% rename from diffviewer.go rename to pkg/ui/panes/diffviewer/diffviewer.go index 3723a6f..099e3e0 100644 --- a/diffviewer.go +++ b/pkg/ui/panes/diffviewer/diffviewer.go @@ -1,4 +1,4 @@ -package main +package diffviewer import ( "bytes" @@ -11,29 +11,30 @@ import ( "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + + "github.com/dlvhdr/diffnav/pkg/ui/common" ) const dirHeaderHeight = 3 -type diffModel struct { +type Model struct { + common.Common vp viewport.Model buffer *bytes.Buffer - width int - height int file *gitdiff.File } -func initialDiffModel() diffModel { - return diffModel{ +func New() Model { + return Model{ vp: viewport.Model{}, } } -func (m diffModel) Init() tea.Cmd { +func (m Model) Init() tea.Cmd { return nil } -func (m diffModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { cmds := make([]tea.Cmd, 0) switch msg := msg.(type) { case tea.KeyMsg: @@ -50,25 +51,27 @@ func (m diffModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case diffContentMsg: m.vp.SetContent(msg.text) - case dimensionsMsg: - m.width = msg.Width - m.height = msg.Height - m.vp.Width = m.width - m.vp.Height = m.height - dirHeaderHeight - cmds = append(cmds, diff(m.file, m.width)) } return m, tea.Batch(cmds...) } -func (m diffModel) View() string { +func (m Model) View() string { if m.buffer == nil { return "Loading..." } return lipgloss.JoinVertical(lipgloss.Left, m.headerView(), m.vp.View()) } -func (m diffModel) headerView() string { +func (m *Model) SetSize(width, height int) tea.Cmd { + m.Width = width + m.Height = height + m.vp.Width = m.Width + m.vp.Height = m.Height - dirHeaderHeight + return diff(m.file, m.Width) +} + +func (m Model) headerView() string { if m.file == nil { return "" } @@ -94,7 +97,7 @@ func (m diffModel) headerView() string { ) return base. - Width(m.width). + Width(m.Width). PaddingLeft(1). Height(dirHeaderHeight - 1). BorderStyle(lipgloss.NormalBorder()). @@ -103,10 +106,10 @@ func (m diffModel) headerView() string { Render(lipgloss.JoinVertical(lipgloss.Left, top, bottom)) } -func (m diffModel) SetFilePatch(file *gitdiff.File) (diffModel, tea.Cmd) { +func (m Model) SetFilePatch(file *gitdiff.File) (Model, tea.Cmd) { m.buffer = new(bytes.Buffer) m.file = file - return m, diff(m.file, m.width) + return m, diff(m.file, m.Width) } func diff(file *gitdiff.File, width int) tea.Cmd { @@ -125,7 +128,7 @@ func diff(file *gitdiff.File, width int) tea.Cmd { out, err := deltac.Output() if err != nil { - return errMsg{err} + return common.ErrMsg{Err: err} } return diffContentMsg{text: string(out)} diff --git a/filetree.go b/pkg/ui/panes/filetree/filetree.go similarity index 86% rename from filetree.go rename to pkg/ui/panes/filetree/filetree.go index c4e79b2..b842f95 100644 --- a/filetree.go +++ b/pkg/ui/panes/filetree/filetree.go @@ -1,4 +1,4 @@ -package main +package filetree import ( "os" @@ -12,18 +12,18 @@ import ( "github.com/charmbracelet/lipgloss/tree" "github.com/dlvhdr/diffnav/pkg/constants" - filetree "github.com/dlvhdr/diffnav/pkg/file_tree" + "github.com/dlvhdr/diffnav/pkg/filenode" "github.com/dlvhdr/diffnav/pkg/utils" ) -type ftModel struct { +type Model struct { files []*gitdiff.File tree *tree.Tree vp viewport.Model selectedFile *string } -func (m ftModel) SetFiles(files []*gitdiff.File) ftModel { +func (m Model) SetFiles(files []*gitdiff.File) Model { m.files = files t := buildFullFileTree(files) collapsed := collapseTree(t) @@ -32,11 +32,11 @@ func (m ftModel) SetFiles(files []*gitdiff.File) ftModel { return m } -func (m ftModel) SetCursor(cursor int) ftModel { +func (m Model) SetCursor(cursor int) Model { if len(m.files) == 0 { return m } - name := filetree.GetFileName(m.files[cursor]) + name := filenode.GetFileName(m.files[cursor]) m.selectedFile = &name applyStyles(m.tree, m.selectedFile) m.scrollSelectedFileIntoView(m.tree) @@ -46,14 +46,14 @@ func (m ftModel) SetCursor(cursor int) ftModel { const contextLines = 15 -func (m *ftModel) scrollSelectedFileIntoView(t *tree.Tree) { +func (m *Model) scrollSelectedFileIntoView(t *tree.Tree) { children := t.Children() for i := 0; i < children.Length(); i++ { child := children.At(i) switch child := child.(type) { case *tree.Tree: m.scrollSelectedFileIntoView(child) - case filetree.FileNode: + case filenode.FileNode: if child.Path() == *m.selectedFile { // offset is 1-based, so we need to subtract 1 offset := child.YOffset - 1 - contextLines @@ -67,24 +67,19 @@ func (m *ftModel) scrollSelectedFileIntoView(t *tree.Tree) { } } -func initialFileTreeModel() ftModel { - return ftModel{ +func New() Model { + return Model{ files: []*gitdiff.File{}, vp: viewport.Model{}, } } -func (m ftModel) Init() tea.Cmd { +func (m Model) Init() tea.Cmd { return nil } -func (m ftModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case dimensionsMsg: - m.vp.Width = msg.Width - m.vp.Height = msg.Height - m.vp, _ = m.vp.Update(msg) - } +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + m.vp, _ = m.vp.Update(msg) return m, nil } @@ -102,15 +97,18 @@ var enumerator = func(children tree.Children, index int) string { return "โ”œ" } -func (m ftModel) View() string { +func (m Model) View() string { return m.vp.View() } -type errMsg struct { - err error +// SetSize implements the Component interface. +func (m *Model) SetSize(width, height int) tea.Cmd { + m.vp.Width = width + m.vp.Height = height + return nil } -func (m ftModel) printWithoutRoot() string { +func (m Model) printWithoutRoot() string { if m.tree.Value() != dirIcon+"." { return m.tree.String() } @@ -125,7 +123,7 @@ func (m ftModel) printWithoutRoot() string { applyStyles(normalized, m.selectedFile) s += normalized.String() - case filetree.FileNode: + case filenode.FileNode: child.Depth = 0 s += applyStyleToNode(child, m.selectedFile).Render(child.Value()) } @@ -145,7 +143,7 @@ func normalizeDepth(node *tree.Tree, depth int) *tree.Tree { case *tree.Tree: sub := normalizeDepth(child, depth+1) t.Child(sub) - case filetree.FileNode: + case filenode.FileNode: child.Depth = depth + 1 t.Child(child) } @@ -158,7 +156,7 @@ func buildFullFileTree(files []*gitdiff.File) *tree.Tree { for _, file := range files { subTree := t - name := filetree.GetFileName(file) + name := filenode.GetFileName(file) dir := filepath.Dir(name) parts := strings.Split(dir, string(os.PathSeparator)) path := "" @@ -189,7 +187,7 @@ func buildFullFileTree(files []*gitdiff.File) *tree.Tree { for i, part := range parts { var c *tree.Tree if i == len(parts)-1 { - subTree.Child(filetree.FileNode{File: file}) + subTree.Child(filenode.FileNode{File: file}) } else { c = tree.Root(part) subTree.Child(c) @@ -252,9 +250,9 @@ func truncateTree(t *tree.Tree, depth int, numNodes int, numChildren int) (*tree numChildren += subNum numNodes += subNum + 1 newT.Child(sub) - case filetree.FileNode: + case filenode.FileNode: numNodes++ - newT.Child(filetree.FileNode{File: child.File, Depth: depth + 1, YOffset: numNodes}) + newT.Child(filenode.FileNode{File: child.File, Depth: depth + 1, YOffset: numNodes}) default: newT.Child(child) } @@ -288,7 +286,7 @@ func applyStyleAux(children tree.Children, i int, selectedFile *string) lipgloss func applyStyleToNode(node tree.Node, selectedFile *string) lipgloss.Style { st := lipgloss.NewStyle().MaxHeight(1) switch n := node.(type) { - case filetree.FileNode: + case filenode.FileNode: if selectedFile != nil && n.Path() == *selectedFile { return st.Background(lipgloss.Color("#1b1b33")).Bold(true) } From edefcbaf9d7589402d6c2dec06d76c06dd371fef Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Fri, 4 Oct 2024 00:02:01 +0300 Subject: [PATCH 06/18] refactor: move sortFiles to utils --- pkg/ui/mainModel.go | 46 ++++----------------------------------------- pkg/ui/utils.go | 42 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 42 deletions(-) create mode 100644 pkg/ui/utils.go diff --git a/pkg/ui/mainModel.go b/pkg/ui/mainModel.go index e2f9d44..141cd50 100644 --- a/pkg/ui/mainModel.go +++ b/pkg/ui/mainModel.go @@ -2,8 +2,6 @@ package ui import ( "fmt" - "path/filepath" - "slices" "strings" "github.com/bluekeyes/go-gitdiff/gitdiff" @@ -257,6 +255,10 @@ func (m mainModel) View() string { ) } +type fileTreeMsg struct { + files []*gitdiff.File +} + func (m mainModel) fetchFileTree() tea.Msg { // TODO: handle error files, _, err := gitdiff.Parse(strings.NewReader(m.input + "\n")) @@ -268,41 +270,6 @@ func (m mainModel) fetchFileTree() tea.Msg { return fileTreeMsg{files: files} } -type fileTreeMsg struct { - files []*gitdiff.File -} - -func sortFiles(files []*gitdiff.File) { - slices.SortFunc(files, func(a *gitdiff.File, b *gitdiff.File) int { - nameA := filenode.GetFileName(a) - nameB := filenode.GetFileName(b) - dira := filepath.Dir(nameA) - dirb := filepath.Dir(nameB) - if dira != "." && dirb != "." && dira == dirb { - return strings.Compare(strings.ToLower(nameA), strings.ToLower(nameB)) - } - - if dira != "." && dirb == "." { - return -1 - } - if dirb != "." && dira == "." { - return 1 - } - - if dira != "." && dirb != "." { - if strings.HasPrefix(dira, dirb) { - return -1 - } - - if strings.HasPrefix(dirb, dira) { - return 1 - } - } - - return strings.Compare(strings.ToLower(nameA), strings.ToLower(nameB)) - }) -} - func (m mainModel) footerView() string { return lipgloss.NewStyle(). Width(m.width). @@ -336,11 +303,6 @@ func (m mainModel) sidebarWidth() int { } } -func (m mainModel) setDiffViewerDimensions() tea.Cmd { - dfCmd := m.diffViewer.SetSize(m.width-m.sidebarWidth(), m.height-footerHeight-headerHeight) - return dfCmd -} - func (m *mainModel) stopSearch() { m.searching = false m.search.SetValue("") diff --git a/pkg/ui/utils.go b/pkg/ui/utils.go new file mode 100644 index 0000000..54e9113 --- /dev/null +++ b/pkg/ui/utils.go @@ -0,0 +1,42 @@ +package ui + +import ( + "path/filepath" + "slices" + "strings" + + "github.com/bluekeyes/go-gitdiff/gitdiff" + + "github.com/dlvhdr/diffnav/pkg/filenode" +) + +func sortFiles(files []*gitdiff.File) { + slices.SortFunc(files, func(a *gitdiff.File, b *gitdiff.File) int { + nameA := filenode.GetFileName(a) + nameB := filenode.GetFileName(b) + dira := filepath.Dir(nameA) + dirb := filepath.Dir(nameB) + if dira != "." && dirb != "." && dira == dirb { + return strings.Compare(strings.ToLower(nameA), strings.ToLower(nameB)) + } + + if dira != "." && dirb == "." { + return -1 + } + if dirb != "." && dira == "." { + return 1 + } + + if dira != "." && dirb != "." { + if strings.HasPrefix(dira, dirb) { + return -1 + } + + if strings.HasPrefix(dirb, dira) { + return 1 + } + } + + return strings.Compare(strings.ToLower(nameA), strings.ToLower(nameB)) + }) +} From 4bfc333a061e0ee9e380c10e1f4824392f405e5a Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Fri, 4 Oct 2024 00:03:06 +0300 Subject: [PATCH 07/18] chore: remove broken test --- filetree_test.go | 335 ----------------------------------------------- 1 file changed, 335 deletions(-) delete mode 100644 filetree_test.go diff --git a/filetree_test.go b/filetree_test.go deleted file mode 100644 index 9d3f1e4..0000000 --- a/filetree_test.go +++ /dev/null @@ -1,335 +0,0 @@ -package main - -import ( - "testing" - - "github.com/charmbracelet/lipgloss/tree" -) - -func TestEmptyTree(t *testing.T) { - files := []string{} - - want := `.` - got := buildFullFileTree(files).String() - - if got != want { - t.Errorf("files:\n%v\n\n------- want:\n%v\n\n-------got:\n%v\n", files, want, got) - } -} - -func TestBuildTreeSingleFile(t *testing.T) { - files := []string{ - "main.go", - } - - want := `. -โ””โ”€โ”€ main.go` - got := buildFullFileTree(files).String() - - if got != want { - t.Errorf("files:\n%v\n\n------- want:\n%v\n\n-------got:\n%v\n", files, want, got) - } -} - -func TestFileWithinDir(t *testing.T) { - files := []string{"ui/main.go"} - - want := tree.Root("ui"). - Child("main.go") - got := buildFullFileTree(files) - got = collapseTree(got) - compareTree(t, want, got) -} - -func TestFileWithinNestedDirAndRootFile(t *testing.T) { - files := []string{"ui/components/main.go", "cmd.go"} - - want := tree.Root("."). - Child("cmd.go"). - Child( - tree.Root("ui/components"). - Child("main.go")) - - got := buildFullFileTree(files) - got = collapseTree(got) - compareTree(t, want, got) -} - -func TestFileWithMultipleFiles(t *testing.T) { - files := []string{"main.go", "cmd.go"} - - want := tree.Root("."). - Child("cmd.go"). - Child("main.go") - - got := buildFullFileTree(files) - got = collapseTree(got) - compareTree(t, want, got) -} - -func TestFileWithNestedDirWithFile(t *testing.T) { - files := []string{"components/main.go", "components/subdir/comp.go"} - - want := tree.Root("."). - Child(tree.Root("components"). - Child("main.go"). - Child(tree.Root("subdir"). - Child("comp.go"))) - got := buildFullFileTree(files).String() - - if got != want.String() { - t.Errorf("files:\n%v\n\n------- want:\n%v\n\n-------got:\n%v\n", files, want, got) - } -} - -func TestDeeplyNestedFile(t *testing.T) { - files := []string{"components/main.go", "components/subdir/comp.go", "components/subdir/subsubdir/deepcomp.go"} - - want := tree.Root("."). - Child(tree.Root("components"). - Child("main.go"). - Child(tree.Root("subdir"). - Child("comp.go"). - Child(tree.Root("subsubdir"). - Child("deepcomp.go")))) - got := buildFullFileTree(files).String() - - if got != want.String() { - t.Errorf("files:\n%v\n\n------- want:\n%v\n\n-------got:\n%v\n", files, want, got) - } -} - -func TestComplex(t *testing.T) { - files := []string{ - "ui/components/a.go", - "ui/components/b.go", - "ui/components/sub/c.go", - "ui/main.go", - "utils/misc/pointers.go", - "utils/misc/sorters.go", - "pkg/internal/ws.go", - } - - want := tree.Root("."). - Child(tree.Root("pkg/internal"). - Child("ws.go")). - Child(tree.Root("ui"). - Child("main.go"). - Child(tree.Root("components"). - Child("a.go"). - Child("b.go"). - Child(tree.Root("sub"). - Child("c.go")))). - Child(tree.Root("utils/misc"). - Child("pointers.go"). - Child("sorters.go")) - got := buildFullFileTree(files) - got = collapseTree(got) - compareTree(t, want, got) -} - -func TestDirectChild(t *testing.T) { - files := []string{"main.go"} - want := tree.Root(".").Child("main.go").String() - got := buildFullFileTree(files).String() - - if got != want { - t.Errorf("want:\n%v\n\n-------got:\n%v\n", want, got) - } -} - -func TestChildWithMultiDirectory(t *testing.T) { - files := []string{"ui/components/subdir/comp.go", "ui/main.go"} - want := tree.Root("ui"). - Child("main.go"). - Child(tree.Root("components/subdir"). - Child("comp.go")) - got := buildFullFileTree(files) - got = collapseTree(got) - compareTree(t, want, got) -} - -func TestCommonAncestor(t *testing.T) { - files := []string{ - "ui/components/subdir/section.go", - "ui/components/subdir/pr.go", - "ui/components/tasks/task/task.go", - } - want := tree.Root("ui/components"). - Child(tree.Root("subdir"). - Child("pr.go"). - Child("section.go")). - Child(tree.Root("tasks/task"). - Child("task.go")) - - got := buildFullFileTree(files) - got = collapseTree(got) - compareTree(t, want, got) -} - -func TestCommonAncestorSorting(t *testing.T) { - files := []string{ - "ui/comp/subdir/pr.go", - "ui/z/section.go", - } - want := tree.Root("."). - Child(tree.Root("ui"). - Child(tree.Root("comp"). - Child(tree.Root("subdir").Child("pr.go"))). - Child(tree.Root("z"). - Child("section.go")), - ). - String() - - got := buildFullFileTree(files).String() - if got != want { - t.Errorf("want:\n%v\n\n-------got:\n%v\n", want, got) - } -} - -func TestGhData(t *testing.T) { - files := []string{ - "ui/components/reposection/commands.go", - "ui/components/reposection/reposection.go", - "ui/components/section/section.go", - "ui/components/tasks/pr.go", - "ui/keys/branchKeys.go", - } - want := tree.Root("."). - Child(tree.Root("ui"). - Child(tree.Root("components"). - Child(tree.Root("reposection"). - Child("commands.go"). - Child("reposection.go")). - Child(tree.Root("section"). - Child("section.go")). - Child(tree.Root("tasks"). - Child("pr.go"))). - Child(tree.Root("keys"). - Child("branchKeys.go"))).String() - - got := buildFullFileTree(files).String() - if got != want { - t.Errorf("want:\n%v\n\n-------got:\n%v\n", want, got) - } -} - -func TestGetDirStructureOneFile(t *testing.T) { - files := []string{ - "ui/main.go", - } - want := tree.Root("."). - Child(tree.Root("ui"). - Child("main.go")).String() - - got := buildFullFileTree(files).String() - if got != want { - t.Errorf("want:\n%v\n\n-------got:\n%v\n", want, got) - } -} - -func TestGetDirStructureTwoUnrelatedFiles(t *testing.T) { - files := []string{ - "ui/main.go", - "pkg/cmd.go", - } - want := tree.Root("."). - Child(tree.Root("pkg"). - Child("cmd.go")). - Child(tree.Root("ui"). - Child("main.go")).String() - - got := buildFullFileTree(files).String() - if got != want { - t.Errorf("want:\n%v\n\n-------got:\n%v\n", want, got) - } -} - -func TestBuildFullComplexTree(t *testing.T) { - files := []string{ - "ui/components/reposection/commands.go", - "ui/components/reposection/reposection.go", - "ui/components/section/section.go", - "ui/components/tasks/pr.go", - "ui/keys/branchKeys.go", - } - want := tree.Root("."). - Child(tree.Root("ui"). - Child(tree.Root("components"). - Child(tree.Root("reposection"). - Child("commands.go"). - Child("reposection.go")). - Child(tree.Root("section"). - Child("section.go")). - Child(tree.Root("tasks"). - Child("pr.go"))). - Child(tree.Root("keys"). - Child("branchKeys.go"))).String() - - got := buildFullFileTree(files).String() - - if got != want { - t.Errorf("want:\n%v\n\n-------got:\n%v\n", want, got) - } -} - -func TestCollapseUncollapsibleTree(t *testing.T) { - input := tree.Root("."). - Child(tree.Root("pkg"). - Child("cmd.go")). - Child(tree.Root("ui"). - Child("main.go")) - want := tree.Root("."). - Child(tree.Root("pkg"). - Child("cmd.go")). - Child(tree.Root("ui"). - Child("main.go")).String() - - collapseTree(input) - if input.String() != want { - t.Errorf("want:\n%v\n\n-------got:\n%v\n", want, input) - } -} - -func TestCollapsibleComplexTree(t *testing.T) { - input := tree.Root("."). - Child(tree.Root("ui"). - Child(tree.Root("components"). - Child(tree.Root("reposection"). - Child("commands.go")). - Child(tree.Root("tasks"). - Child("pr.go")))) - - want := tree.Root("ui/components"). - Child(tree.Root("reposection"). - Child("commands.go")). - Child(tree.Root("tasks"). - Child("pr.go")) - - got := collapseTree(input) - if got.String() != want.String() { - t.Errorf("want:\n%v\n\n-------got:\n%v\n", want, got) - } -} - -func TestCollapsibleTree(t *testing.T) { - input := tree.Root("."). - Child(tree.Root("ui"). - Child(tree.Root("subdir"). - Child("pr.go"). - Child("section.go"))) - want := tree.Root("ui/subdir"). - Child("pr.go"). - Child("section.go") - - got := collapseTree(input) - if got.String() != want.String() { - t.Errorf("want:\n%v\n\n-------got:\n%v\n", want, got) - } -} - -func compareTree(t *testing.T, want, got tree.Node) { - if got.String() != want.String() { - t.Errorf("want:\n%v\n\n-------got:\n%v\n", want, got) - } -} From e12383c64650aeff6095bc32c187b02f16302746 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Fri, 4 Oct 2024 14:57:18 +0300 Subject: [PATCH 08/18] fix: filetree viewport double offset --- pkg/ui/mainModel.go | 18 +++++++++++------- pkg/ui/panes/filetree/filetree.go | 6 ++++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/pkg/ui/mainModel.go b/pkg/ui/mainModel.go index 141cd50..a371c32 100644 --- a/pkg/ui/mainModel.go +++ b/pkg/ui/mainModel.go @@ -107,14 +107,12 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, dfCmd) case "up", "k", "ctrl+p": if m.cursor > 0 { - m.cursor-- - m.diffViewer, cmd = m.diffViewer.SetFilePatch(m.files[m.cursor]) + cmd = m.setCursor(m.cursor - 1) cmds = append(cmds, cmd) } case "down", "j", "ctrl+n": if m.cursor < len(m.files)-1 { - m.cursor++ - m.diffViewer, cmd = m.diffViewer.SetFilePatch(m.files[m.cursor]) + cmd = m.setCursor(m.cursor + 1) cmds = append(cmds, cmd) } } @@ -134,7 +132,7 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit } m.fileTree = m.fileTree.SetFiles(m.files) - m.diffViewer, cmd = m.diffViewer.SetFilePatch(m.files[0]) + cmd = m.setCursor(0) cmds = append(cmds, cmd) case common.ErrMsg: @@ -147,8 +145,6 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, sCmds...) } - m.fileTree = m.fileTree.SetCursor(m.cursor) - m.diffViewer, cmd = m.diffViewer.Update(msg) cmds = append(cmds, cmd) @@ -309,3 +305,11 @@ func (m *mainModel) stopSearch() { m.search.Blur() m.search.Width = m.sidebarWidth() - 5 } + +func (m *mainModel) setCursor(cursor int) tea.Cmd { + var cmd tea.Cmd + m.cursor = cursor + m.diffViewer, cmd = m.diffViewer.SetFilePatch(m.files[m.cursor]) + m.fileTree = m.fileTree.SetCursor(m.cursor) + return cmd +} diff --git a/pkg/ui/panes/filetree/filetree.go b/pkg/ui/panes/filetree/filetree.go index b842f95..45a64c5 100644 --- a/pkg/ui/panes/filetree/filetree.go +++ b/pkg/ui/panes/filetree/filetree.go @@ -48,6 +48,7 @@ const contextLines = 15 func (m *Model) scrollSelectedFileIntoView(t *tree.Tree) { children := t.Children() + found := false for i := 0; i < children.Length(); i++ { child := children.At(i) switch child := child.(type) { @@ -62,8 +63,13 @@ func (m *Model) scrollSelectedFileIntoView(t *tree.Tree) { offset = offset - 1 } m.vp.SetYOffset(offset) + found = true + break } } + if found { + break + } } } From 2ca04b3d07ff73c8cd3aa89b4d99b68646640e5f Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Sat, 5 Oct 2024 14:03:09 +0300 Subject: [PATCH 09/18] fix: only create debug.log if env var is set --- main.go | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/main.go b/main.go index 0b065ff..c9fd82e 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/log" "github.com/charmbracelet/x/ansi" + "github.com/muesli/termenv" "github.com/dlvhdr/diffnav/pkg/ui" ) @@ -22,20 +23,34 @@ func main() { } if stat.Mode()&os.ModeNamedPipe == 0 && stat.Size() == 0 { - fmt.Println("Try piping in some text.") - os.Exit(1) + fmt.Println("No diff, exiting") + os.Exit(0) } - var fileErr error - logFile, fileErr := os.OpenFile("debug.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) - if fileErr == nil { - log.SetOutput(logFile) - log.SetTimeFormat(time.Kitchen) - log.SetReportCaller(true) - log.SetLevel(log.DebugLevel) + if os.Getenv("DEBUG") == "true" { + var fileErr error + logFile, fileErr := os.OpenFile("debug.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if fileErr != nil { + fmt.Println("Error opening debug.log:", fileErr) + os.Exit(1) + } defer logFile.Close() - log.SetOutput(logFile) - log.Debug("Starting diffnav, logging to debug.log") + + if fileErr == nil { + log.SetOutput(logFile) + log.SetTimeFormat(time.Kitchen) + log.SetReportCaller(true) + log.SetLevel(log.DebugLevel) + + log.SetOutput(logFile) + log.SetColorProfile(termenv.TrueColor) + wd, err := os.Getwd() + if err != nil { + fmt.Println("Error getting current working dir", err) + os.Exit(1) + } + log.Debug("๐Ÿš€ Starting diffnav", "logFile", wd+string(os.PathSeparator)+logFile.Name()) + } } reader := bufio.NewReader(os.Stdin) @@ -53,15 +68,10 @@ func main() { } } - if os.Getenv("DEBUG") == "true" { - logger, _ := tea.LogToFile("debug.log", "debug") - defer logger.Close() - } - input := ansi.Strip(b.String()) if strings.TrimSpace(input) == "" { fmt.Println("No input provided, exiting") - os.Exit(1) + os.Exit(0) } p := tea.NewProgram(ui.New(input), tea.WithMouseAllMotion()) From eb2dfa12d56fb5a0bdde70be912a22acdf1ccca4 Mon Sep 17 00:00:00 2001 From: Nikola Milenkovic Date: Sun, 5 Jan 2025 21:28:18 +0000 Subject: [PATCH 10/18] feat: add copy selected file path --- pkg/ui/keys.go | 16 +++++++++++++++- pkg/ui/mainModel.go | 10 ++++++++++ pkg/ui/panes/filetree/filetree.go | 16 ++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/pkg/ui/keys.go b/pkg/ui/keys.go index 7d0e706..986030b 100644 --- a/pkg/ui/keys.go +++ b/pkg/ui/keys.go @@ -10,6 +10,7 @@ type KeyMap struct { ToggleFileTree key.Binding Search key.Binding Quit key.Binding + Copy key.Binding } var keys = &KeyMap{ @@ -41,8 +42,21 @@ var keys = &KeyMap{ key.WithKeys("q", "ctrl+c"), key.WithHelp("q", "quit"), ), + Copy: key.NewBinding( + key.WithKeys("c"), + key.WithHelp("c", "copy file path"), + ), } func getKeys() []key.Binding { - return []key.Binding{keys.Up, keys.Down, keys.CtrlD, keys.CtrlU, keys.ToggleFileTree, keys.Search, keys.Quit} + return []key.Binding{ + keys.Up, + keys.Down, + keys.CtrlD, + keys.CtrlU, + keys.ToggleFileTree, + keys.Search, + keys.Copy, + keys.Quit, + } } diff --git a/pkg/ui/mainModel.go b/pkg/ui/mainModel.go index a371c32..6954922 100644 --- a/pkg/ui/mainModel.go +++ b/pkg/ui/mainModel.go @@ -115,6 +115,11 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmd = m.setCursor(m.cursor + 1) cmds = append(cmds, cmd) } + case "c": + cmd = m.copySelectedFilePath() + if cmd != nil { + cmds = append(cmds, cmd) + } } case tea.WindowSizeMsg: @@ -313,3 +318,8 @@ func (m *mainModel) setCursor(cursor int) tea.Cmd { m.fileTree = m.fileTree.SetCursor(m.cursor) return cmd } + +func (m *mainModel) copySelectedFilePath() tea.Cmd { + cmd := m.fileTree.CopyFilePath(m.cursor) + return cmd +} diff --git a/pkg/ui/panes/filetree/filetree.go b/pkg/ui/panes/filetree/filetree.go index 45a64c5..08006f7 100644 --- a/pkg/ui/panes/filetree/filetree.go +++ b/pkg/ui/panes/filetree/filetree.go @@ -5,6 +5,7 @@ import ( "path/filepath" "strings" + "github.com/atotto/clipboard" "github.com/bluekeyes/go-gitdiff/gitdiff" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" @@ -13,6 +14,7 @@ import ( "github.com/dlvhdr/diffnav/pkg/constants" "github.com/dlvhdr/diffnav/pkg/filenode" + "github.com/dlvhdr/diffnav/pkg/ui/common" "github.com/dlvhdr/diffnav/pkg/utils" ) @@ -44,6 +46,20 @@ func (m Model) SetCursor(cursor int) Model { return m } +func (m Model) CopyFilePath(cursor int) tea.Cmd { + if len(m.files) == 0 { + return nil + } + name := filenode.GetFileName(m.files[cursor]) + err := clipboard.WriteAll(name) + if err != nil { + return func() tea.Msg { + return common.ErrMsg{Err: err} + } + } + return nil +} + const contextLines = 15 func (m *Model) scrollSelectedFileIntoView(t *tree.Tree) { From deafe64d3154386de2c674fb8100894aa78b785e Mon Sep 17 00:00:00 2001 From: Nikola Milenkovic Date: Sun, 5 Jan 2025 21:34:47 +0000 Subject: [PATCH 11/18] chore: remove copySelectedFilePath func --- pkg/ui/mainModel.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pkg/ui/mainModel.go b/pkg/ui/mainModel.go index 6954922..4aec442 100644 --- a/pkg/ui/mainModel.go +++ b/pkg/ui/mainModel.go @@ -116,7 +116,7 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, cmd) } case "c": - cmd = m.copySelectedFilePath() + cmd = m.fileTree.CopyFilePath(m.cursor) if cmd != nil { cmds = append(cmds, cmd) } @@ -318,8 +318,3 @@ func (m *mainModel) setCursor(cursor int) tea.Cmd { m.fileTree = m.fileTree.SetCursor(m.cursor) return cmd } - -func (m *mainModel) copySelectedFilePath() tea.Cmd { - cmd := m.fileTree.CopyFilePath(m.cursor) - return cmd -} From bff463394c8e6af41bc51a5b9a344a5167c3a2f4 Mon Sep 17 00:00:00 2001 From: Nikola Milenkovic Date: Sun, 5 Jan 2025 21:56:16 +0000 Subject: [PATCH 12/18] chore: update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ce16d29..6bfac53 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ git config --global pager.diff diffnav | Ctrl-u | Scroll the diff up | | e | Toggle the file tree | | t | Search/go-to file | +| c | Copy file path | | q | Quit | ## Under the hood From 3b0a3690b601b17e1595c625a04d3235232ca2d0 Mon Sep 17 00:00:00 2001 From: Nikola Milenkovic Date: Sun, 5 Jan 2025 23:11:33 +0000 Subject: [PATCH 13/18] fix: change keybinding from `c` to `y` --- README.md | 2 +- pkg/ui/keys.go | 4 ++-- pkg/ui/mainModel.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6bfac53..f5ddcf8 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ git config --global pager.diff diffnav | Ctrl-u | Scroll the diff up | | e | Toggle the file tree | | t | Search/go-to file | -| c | Copy file path | +| y | Copy file path | | q | Quit | ## Under the hood diff --git a/pkg/ui/keys.go b/pkg/ui/keys.go index 986030b..bdd3d4a 100644 --- a/pkg/ui/keys.go +++ b/pkg/ui/keys.go @@ -43,8 +43,8 @@ var keys = &KeyMap{ key.WithHelp("q", "quit"), ), Copy: key.NewBinding( - key.WithKeys("c"), - key.WithHelp("c", "copy file path"), + key.WithKeys("y"), + key.WithHelp("y", "copy file path"), ), } diff --git a/pkg/ui/mainModel.go b/pkg/ui/mainModel.go index 4aec442..4d24a3f 100644 --- a/pkg/ui/mainModel.go +++ b/pkg/ui/mainModel.go @@ -115,7 +115,7 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmd = m.setCursor(m.cursor + 1) cmds = append(cmds, cmd) } - case "c": + case "y": cmd = m.fileTree.CopyFilePath(m.cursor) if cmd != nil { cmds = append(cmds, cmd) From 62eefe88a3cfff27082c8c0a96141e72f9a0858a Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Sat, 5 Apr 2025 21:07:55 +0300 Subject: [PATCH 14/18] build: devbox --- .envrc | 9 ++++ devbox.json | 14 +++++ devbox.lock | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 .envrc create mode 100644 devbox.json create mode 100644 devbox.lock diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..2f05af9 --- /dev/null +++ b/.envrc @@ -0,0 +1,9 @@ +#!/bin/bash + +# Automatically sets up your devbox environment whenever you cd into this +# directory via our direnv integration: + +eval "$(devbox generate direnv --print-envrc)" + +# check out https://www.jetpack.io/devbox/docs/ide_configuration/direnv/ +# for more details diff --git a/devbox.json b/devbox.json new file mode 100644 index 0000000..978b180 --- /dev/null +++ b/devbox.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.14.0/.schema/devbox.schema.json", + "packages": { + "go": "1.22.6", + "gopls": "latest", + "golangci-lint": "latest" + }, + "shell": { + "scripts": { + "test": ["echo \"Error: no test specified\" && exit 1"] + } + } +} + diff --git a/devbox.lock b/devbox.lock new file mode 100644 index 0000000..6a18528 --- /dev/null +++ b/devbox.lock @@ -0,0 +1,152 @@ +{ + "lockfile_version": "1", + "packages": { + "github:NixOS/nixpkgs/nixpkgs-unstable": { + "resolved": "github:NixOS/nixpkgs/2bfc080955153be0be56724be6fa5477b4eefabb?lastModified=1743689281&narHash=sha256-y7Hg5lwWhEOgflEHRfzSH96BOt26LaYfrYWzZ%2BVoVdg%3D" + }, + "go@1.22.6": { + "last_modified": "2024-08-31T10:12:23Z", + "resolved": "github:NixOS/nixpkgs/5629520edecb69630a3f4d17d3d33fc96c13f6fe#go", + "source": "devbox-search", + "version": "1.22.6", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/bman2jjx2ykfclj3g0wb89cxyzqygh8y-go-1.22.6", + "default": true + } + ], + "store_path": "/nix/store/bman2jjx2ykfclj3g0wb89cxyzqygh8y-go-1.22.6" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/gnm672jywl1b778ql6pf57xka45452b6-go-1.22.6", + "default": true + } + ], + "store_path": "/nix/store/gnm672jywl1b778ql6pf57xka45452b6-go-1.22.6" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/qvr3slzx5av20xkw6i97yz7wla9sf4nc-go-1.22.6", + "default": true + } + ], + "store_path": "/nix/store/qvr3slzx5av20xkw6i97yz7wla9sf4nc-go-1.22.6" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/6rybf4g5b77kz27k07avr7qd44ssw3l2-go-1.22.6", + "default": true + } + ], + "store_path": "/nix/store/6rybf4g5b77kz27k07avr7qd44ssw3l2-go-1.22.6" + } + } + }, + "golangci-lint@latest": { + "last_modified": "2025-03-25T17:32:05Z", + "resolved": "github:NixOS/nixpkgs/25d1b84f5c90632a623c48d83a2faf156451e6b1#golangci-lint", + "source": "devbox-search", + "version": "2.0.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/idv9cyl4i6w9n4sgc29kvqhywm04n1rz-golangci-lint-2.0.0", + "default": true + } + ], + "store_path": "/nix/store/idv9cyl4i6w9n4sgc29kvqhywm04n1rz-golangci-lint-2.0.0" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/r0jxwvqvk2999dx04v3j9jgd46jscqc4-golangci-lint-2.0.0", + "default": true + } + ], + "store_path": "/nix/store/r0jxwvqvk2999dx04v3j9jgd46jscqc4-golangci-lint-2.0.0" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/lsyy8arab3zvkpi8lr9303mf88y5k1rc-golangci-lint-2.0.0", + "default": true + } + ], + "store_path": "/nix/store/lsyy8arab3zvkpi8lr9303mf88y5k1rc-golangci-lint-2.0.0" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/834gvbmhpwvy1d65r5x4xihkxm4g91ab-golangci-lint-2.0.0", + "default": true + } + ], + "store_path": "/nix/store/834gvbmhpwvy1d65r5x4xihkxm4g91ab-golangci-lint-2.0.0" + } + } + }, + "gopls@latest": { + "last_modified": "2025-03-25T17:32:05Z", + "resolved": "github:NixOS/nixpkgs/25d1b84f5c90632a623c48d83a2faf156451e6b1#gopls", + "source": "devbox-search", + "version": "0.18.1", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/1yhl06d3rp0v9nzj9w11d91mzdib9li0-gopls-0.18.1", + "default": true + } + ], + "store_path": "/nix/store/1yhl06d3rp0v9nzj9w11d91mzdib9li0-gopls-0.18.1" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/f276ys7dbl1c4h5i3yc1yavwn4vkznpm-gopls-0.18.1", + "default": true + } + ], + "store_path": "/nix/store/f276ys7dbl1c4h5i3yc1yavwn4vkznpm-gopls-0.18.1" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/idmdsky8xd2dcwkiwsxka50lwmp6f9s3-gopls-0.18.1", + "default": true + } + ], + "store_path": "/nix/store/idmdsky8xd2dcwkiwsxka50lwmp6f9s3-gopls-0.18.1" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/2w4s5abaqwm45wkkwwf15yyw5dcrqs7m-gopls-0.18.1", + "default": true + } + ], + "store_path": "/nix/store/2w4s5abaqwm45wkkwwf15yyw5dcrqs7m-gopls-0.18.1" + } + } + } + } +} From ad8e7ae38ff6916248e6c4519ec256fb2edcca73 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Sat, 10 May 2025 20:50:30 +0300 Subject: [PATCH 15/18] fix: go to top of diff when switching files --- pkg/ui/mainModel.go | 2 ++ pkg/ui/panes/diffviewer/diffviewer.go | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/pkg/ui/mainModel.go b/pkg/ui/mainModel.go index 4d24a3f..2ae11f7 100644 --- a/pkg/ui/mainModel.go +++ b/pkg/ui/mainModel.go @@ -107,11 +107,13 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, dfCmd) case "up", "k", "ctrl+p": if m.cursor > 0 { + m.diffViewer.GoToTop() cmd = m.setCursor(m.cursor - 1) cmds = append(cmds, cmd) } case "down", "j", "ctrl+n": if m.cursor < len(m.files)-1 { + m.diffViewer.GoToTop() cmd = m.setCursor(m.cursor + 1) cmds = append(cmds, cmd) } diff --git a/pkg/ui/panes/diffviewer/diffviewer.go b/pkg/ui/panes/diffviewer/diffviewer.go index 099e3e0..95648bd 100644 --- a/pkg/ui/panes/diffviewer/diffviewer.go +++ b/pkg/ui/panes/diffviewer/diffviewer.go @@ -112,6 +112,10 @@ func (m Model) SetFilePatch(file *gitdiff.File) (Model, tea.Cmd) { return m, diff(m.file, m.Width) } +func (m *Model) GoToTop() { + m.vp.GotoTop() +} + func diff(file *gitdiff.File, width int) tea.Cmd { if width == 0 || file == nil { return nil From 13232a25e95ebde0590fdfc602962b9cff63c282 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Sat, 10 May 2025 20:57:26 +0300 Subject: [PATCH 16/18] ci: add svu --- devbox.json | 8 ++++---- devbox.lock | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/devbox.json b/devbox.json index 978b180..8957906 100644 --- a/devbox.json +++ b/devbox.json @@ -1,9 +1,10 @@ { "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.14.0/.schema/devbox.schema.json", "packages": { - "go": "1.22.6", - "gopls": "latest", - "golangci-lint": "latest" + "go": "1.22.6", + "gopls": "latest", + "golangci-lint": "latest", + "svu": "latest" }, "shell": { "scripts": { @@ -11,4 +12,3 @@ } } } - diff --git a/devbox.lock b/devbox.lock index 6a18528..16d1923 100644 --- a/devbox.lock +++ b/devbox.lock @@ -147,6 +147,54 @@ "store_path": "/nix/store/2w4s5abaqwm45wkkwwf15yyw5dcrqs7m-gopls-0.18.1" } } + }, + "svu@latest": { + "last_modified": "2025-04-17T05:47:26Z", + "resolved": "github:NixOS/nixpkgs/ebe4301cbd8f81c4f8d3244b3632338bbeb6d49c#svu", + "source": "devbox-search", + "version": "3.2.3", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/djfi0c8vvpsfssadwls6y55zyhkif58c-svu-3.2.3", + "default": true + } + ], + "store_path": "/nix/store/djfi0c8vvpsfssadwls6y55zyhkif58c-svu-3.2.3" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/2jy02ldq76r8r3slbcc1cb1dbxahraxr-svu-3.2.3", + "default": true + } + ], + "store_path": "/nix/store/2jy02ldq76r8r3slbcc1cb1dbxahraxr-svu-3.2.3" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/jx3ilvqwkcjmfh9kccyx1rbm06lgfa00-svu-3.2.3", + "default": true + } + ], + "store_path": "/nix/store/jx3ilvqwkcjmfh9kccyx1rbm06lgfa00-svu-3.2.3" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/vmdd4wxfbgp9xiv6icyzsxi36f4i31j8-svu-3.2.3", + "default": true + } + ], + "store_path": "/nix/store/vmdd4wxfbgp9xiv6icyzsxi36f4i31j8-svu-3.2.3" + } + } } } } From 7e51ed93bd02dbfd0cdd2c1683d1d13a0e4a8c8d Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Sat, 10 May 2025 20:59:35 +0300 Subject: [PATCH 17/18] ci: add gh-dash.yml --- .gh-dash.yml | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 .gh-dash.yml diff --git a/.gh-dash.yml b/.gh-dash.yml new file mode 100644 index 0000000..8c62e1e --- /dev/null +++ b/.gh-dash.yml @@ -0,0 +1,86 @@ +# yaml-language-server: $schema=https://dlvhdr.github.io/diffnav/configuration/gh-dash/schema.json +prSections: + - title: Mine + filters: is:open author:@me repo:dlvhdr/diffnav updated:>={{ nowModify "-3w" }} sort:updated-desc + layout: + author: + hidden: true + - title: Review + filters: repo:dlvhdr/diffnav -author:@me is:open updated:>={{ nowModify "-2.5w" }} + - title: All + filters: repo:dlvhdr/diffnav +issuesSections: + - title: Open + filters: author:@me repo:dlvhdr/diffnav is:open -author:@me sort:reactions + - title: Creator + filters: author:@me repo:dlvhdr/diffnav is:open + - title: All + filters: repo:dlvhdr/diffnav sort:reactions + +pager: + diff: diffnav +defaults: + view: prs + refetchIntervalMinutes: 5 + layout: + prs: + repoName: + grow: true, + width: 10 + hidden: false + base: + hidden: true + + preview: + open: true + width: 84 + prsLimit: 20 + issuesLimit: 20 +repoPaths: + dlvhdr/*: ~/code/personal/* + +keybindings: + universal: + - key: g + name: lazygit + command: > + cd {{.RepoPath}} && lazygit + prs: + - key: O + builtin: checkout + - key: m + command: gh pr merge --admin --repo {{.RepoName}} {{.PrNumber}} + - key: C + name: code review + command: > + tmux new-window -c {{.RepoPath}} ' + nvim -c ":silent Octo pr edit {{.PrNumber}}" + ' + - key: a + name: lazygit add + command: > + cd {{.RepoPath}} && git add -A && lazygit + - key: v + name: approve + command: > + gh pr review --repo {{.RepoName}} --approve --body "$(gum input --prompt='Approval Comment: ')" {{.PrNumber}} + +theme: + ui: + sectionsShowCount: true + table: + compact: false + colors: + text: + primary: "#E2E1ED" + secondary: "#666CA6" + inverted: "#242347" + faint: "#B0B3BF" + warning: "#E0AF68" + success: "#3DF294" + background: + selected: "#1B1B33" + border: + primary: "#383B5B" + secondary: "#39386B" + faint: "#2B2B40" From 3a431deb6cd9d9085b39a60e3c7706c651cc4b60 Mon Sep 17 00:00:00 2001 From: Dolev Hadar <6196971+dlvhdr@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:14:01 +0300 Subject: [PATCH 18/18] chore: add funding configuration for GitHub --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..6e41a4c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [dlvhdr]