From 8e02b15578affe919087900b9fdeb04614d089a9 Mon Sep 17 00:00:00 2001 From: Ronnie Smith Date: Sun, 14 Apr 2024 00:55:56 -0700 Subject: [PATCH 1/6] support modifying default keybindings prototype --- config/parser.go | 36 ++++++++++++++++++++++++++------ ui/keys/keys.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++- ui/keys/prKeys.go | 43 +++++++++++++++++++++++++++++++++++++++ ui/ui.go | 23 +++++++++++++++------ 4 files changed, 141 insertions(+), 13 deletions(-) diff --git a/config/parser.go b/config/parser.go index a9978bfc..5baebaa6 100644 --- a/config/parser.go +++ b/config/parser.go @@ -8,6 +8,7 @@ import ( "reflect" "strings" + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/lipgloss" "github.com/go-playground/validator/v10" "gopkg.in/yaml.v2" @@ -102,13 +103,35 @@ type Defaults struct { } type Keybinding struct { - Key string `yaml:"key"` - Command string `yaml:"command"` + Key string `yaml:"key"` + Keys []string `yaml:"keys"` + Command string `yaml:"command"` + Builtin string `yaml:"builtin"` +} + +func (kb Keybinding) NewBinding(previous *key.Binding) key.Binding { + helpDesc := "" + if previous != nil { + helpDesc = previous.Help().Desc + } + + if len(kb.Keys) > 0 { + return key.NewBinding( + key.WithKeys(kb.Keys...), + key.WithHelp(strings.Join(kb.Keys, "/"), helpDesc), + ) + } + + return key.NewBinding( + key.WithKeys(kb.Key), + key.WithHelp(kb.Key, helpDesc), + ) } type Keybindings struct { - Issues []Keybinding `yaml:"issues"` - Prs []Keybinding `yaml:"prs"` + Universal []Keybinding `yaml:"universal"` + Issues []Keybinding `yaml:"issues"` + Prs []Keybinding `yaml:"prs"` } type Pager struct { @@ -257,8 +280,9 @@ func (parser ConfigParser) getDefaultConfig() Config { }, }, Keybindings: Keybindings{ - Issues: []Keybinding{}, - Prs: []Keybinding{}, + Universal: []Keybinding{}, + Issues: []Keybinding{}, + Prs: []Keybinding{}, }, RepoPaths: map[string]string{}, Theme: &ThemeConfig{ diff --git a/ui/keys/keys.go b/ui/keys/keys.go index b2147944..c886bd7e 100644 --- a/ui/keys/keys.go +++ b/ui/keys/keys.go @@ -3,6 +3,7 @@ package keys import ( "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" + log "github.com/charmbracelet/log" "github.com/dlvhdr/gh-dash/config" ) @@ -84,7 +85,7 @@ func (k KeyMap) QuitAndHelpKeys() []key.Binding { return []key.Binding{k.Help, k.Quit} } -var Keys = KeyMap{ +var Keys = &KeyMap{ Up: key.NewBinding( key.WithKeys("up", "k"), key.WithHelp("↑/k", "move up"), @@ -158,3 +159,52 @@ var Keys = KeyMap{ key.WithHelp("q", "quit"), ), } + +// Rebind will update our saved keybindings from configuration values. +func Rebind(universal, issueKeys, prKeys []config.Keybinding) error { + err := rebindUniversal(universal) + if err != nil { + return err + } + + err = rebindPRKeys(prKeys) + if err != nil { + return err + } + + return nil +} + +func rebindUniversal(universal []config.Keybinding) error { + for _, kb := range universal { + log.Debug("Rebinding key", "builtin", kb.Builtin, "key", kb.Key) + switch kb.Builtin { + case "up": + Keys.Up = kb.NewBinding(&Keys.Up) + case "down": + Keys.Down = kb.NewBinding(&Keys.Down) + case "firstline": // should this be first-line or firstLine? + case "lastline": + case "togglepreview": + case "opengithub": + case "refresh": + case "refreshall": + case "pagedown": + case "pageup": + case "nextsection": + case "prevsection": + case "switchview": + Keys.SwitchView = kb.NewBinding(&Keys.SwitchView) + case "search": + case "copyurl": + case "copynumber": + case "help": + case "quit": + default: + // TODO: return an error here + return nil + } + } + + return nil +} diff --git a/ui/keys/prKeys.go b/ui/keys/prKeys.go index 55650647..62efe00f 100644 --- a/ui/keys/prKeys.go +++ b/ui/keys/prKeys.go @@ -2,6 +2,7 @@ package keys import ( "github.com/charmbracelet/bubbles/key" + "github.com/dlvhdr/gh-dash/config" ) type PRKeyMap struct { @@ -74,3 +75,45 @@ func PRFullHelp() []key.Binding { PRKeys.WatchChecks, } } + +func rebindPRKeys(keys []config.Keybinding) error { + for _, prKey := range keys { + var k key.Binding + + switch prKey.Builtin { + case "assign": + k = PRKeys.Assign + case "unassign": + k = PRKeys.Unassign + case "comment": + k = PRKeys.Comment + case "diff": + k = PRKeys.Diff + case "checkout": + k = PRKeys.Checkout + case "close": + k = PRKeys.Close + case "ready": + k = PRKeys.Ready + case "reopen": + k = PRKeys.Reopen + case "merge": + k = PRKeys.Merge + case "watchchecks": + k = PRKeys.WatchChecks + default: + // TODO: return an error here + return nil + } + + // Not really sure if this is the best idea but I am not + // sure how else we are meant to define alt keys. + if len(prKey.Keys) > 0 { + k.SetKeys(prKey.Keys...) + } else { + k.SetKeys(prKey.Key) + } + } + + return nil +} diff --git a/ui/ui.go b/ui/ui.go index c2c4973a..eb69eb46 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -33,7 +33,7 @@ import ( ) type Model struct { - keys keys.KeyMap + keys *keys.KeyMap sidebar sidebar.Model prSidebar prsidebar.Model issueSidebar issuesidebar.Model @@ -82,10 +82,7 @@ func NewModel(configPath string) Model { } func (m *Model) initScreen() tea.Msg { - var err error - - config, err := config.ParseConfig(m.ctx.ConfigPath) - if err != nil { + showError := func(err error) { styles := log.DefaultStyles() styles.Key = lipgloss.NewStyle(). Foreground(lipgloss.Color("1")). @@ -107,10 +104,24 @@ func (m *Model) initScreen() tea.Msg { "err", err, ) + } + + cfg, err := config.ParseConfig(m.ctx.ConfigPath) + if err != nil { + showError(err) + return initMsg{Config: cfg} + } + err = keys.Rebind( + cfg.Keybindings.Universal, + cfg.Keybindings.Issues, + cfg.Keybindings.Prs, + ) + if err != nil { + showError(err) } - return initMsg{Config: config} + return initMsg{Config: cfg} } func (m Model) Init() tea.Cmd { From 2641b13d349dbfb52f1f3d7af6bd39ed733dcdfe Mon Sep 17 00:00:00 2001 From: Ronnie Smith Date: Thu, 9 May 2024 23:24:35 -0700 Subject: [PATCH 2/6] Further work on custom keybindings * Remove keys and just use key * Handle issue and pr keys * Handle all keys in universal --- config/parser.go | 14 +++------- ui/keys/issueKeys.go | 40 +++++++++++++++++++++++++++- ui/keys/keys.go | 63 +++++++++++++++++++++++++++++++------------- ui/keys/prKeys.go | 46 +++++++++++++++++--------------- ui/modelUtils.go | 14 +++++----- ui/ui.go | 3 ++- 6 files changed, 122 insertions(+), 58 deletions(-) diff --git a/config/parser.go b/config/parser.go index 5baebaa6..a04dced5 100644 --- a/config/parser.go +++ b/config/parser.go @@ -103,10 +103,9 @@ type Defaults struct { } type Keybinding struct { - Key string `yaml:"key"` - Keys []string `yaml:"keys"` - Command string `yaml:"command"` - Builtin string `yaml:"builtin"` + Key string `yaml:"key"` + Command string `yaml:"command"` + Builtin string `yaml:"builtin"` } func (kb Keybinding) NewBinding(previous *key.Binding) key.Binding { @@ -115,13 +114,6 @@ func (kb Keybinding) NewBinding(previous *key.Binding) key.Binding { helpDesc = previous.Help().Desc } - if len(kb.Keys) > 0 { - return key.NewBinding( - key.WithKeys(kb.Keys...), - key.WithHelp(strings.Join(kb.Keys, "/"), helpDesc), - ) - } - return key.NewBinding( key.WithKeys(kb.Key), key.WithHelp(kb.Key, helpDesc), diff --git a/ui/keys/issueKeys.go b/ui/keys/issueKeys.go index 845f3658..c01fcdc2 100644 --- a/ui/keys/issueKeys.go +++ b/ui/keys/issueKeys.go @@ -1,6 +1,12 @@ package keys -import "github.com/charmbracelet/bubbles/key" +import ( + "fmt" + + "github.com/charmbracelet/bubbles/key" + log "github.com/charmbracelet/log" + "github.com/dlvhdr/gh-dash/config" +) type IssueKeyMap struct { Assign key.Binding @@ -42,3 +48,35 @@ func IssueFullHelp() []key.Binding { IssueKeys.Reopen, } } + +func rebindIssueKeys(keys []config.Keybinding) error { + for _, issueKey := range keys { + if issueKey.Builtin == "" { + continue + } + + log.Debug("Rebinding issue key", "builtin", issueKey.Builtin, "key", issueKey.Key) + + var key *key.Binding + + switch issueKey.Builtin { + case "assign": + key = &IssueKeys.Assign + case "unassign": + key = &IssueKeys.Unassign + case "comment": + key = &IssueKeys.Comment + case "close": + key = &IssueKeys.Close + case "reopen": + key = &IssueKeys.Reopen + default: + return fmt.Errorf("unknown built-in issue key: '%s'", issueKey.Builtin) + } + + key.SetKeys(issueKey.Key) + key.SetHelp(issueKey.Key, key.Help().Desc) + } + + return nil +} diff --git a/ui/keys/keys.go b/ui/keys/keys.go index c886bd7e..385a3d09 100644 --- a/ui/keys/keys.go +++ b/ui/keys/keys.go @@ -1,6 +1,8 @@ package keys import ( + "fmt" + "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" log "github.com/charmbracelet/log" @@ -172,38 +174,63 @@ func Rebind(universal, issueKeys, prKeys []config.Keybinding) error { return err } - return nil + return rebindIssueKeys(issueKeys) } func rebindUniversal(universal []config.Keybinding) error { + log.Debug("Rebinding universal keys", "keys", universal) for _, kb := range universal { - log.Debug("Rebinding key", "builtin", kb.Builtin, "key", kb.Key) + if kb.Builtin == "" { + continue + } + + log.Debug("Rebinding universal key", "builtin", kb.Builtin, "key", kb.Key) + + var key *key.Binding + switch kb.Builtin { case "up": - Keys.Up = kb.NewBinding(&Keys.Up) + key = &Keys.Up case "down": - Keys.Down = kb.NewBinding(&Keys.Down) - case "firstline": // should this be first-line or firstLine? - case "lastline": - case "togglepreview": - case "opengithub": + key = &Keys.Down + case "firstLine": + key = &Keys.FirstLine + case "lastLine": + key = &Keys.LastLine + case "togglePreview": + key = &Keys.TogglePreview + case "openGithub": + key = &Keys.OpenGithub case "refresh": - case "refreshall": - case "pagedown": - case "pageup": - case "nextsection": - case "prevsection": - case "switchview": - Keys.SwitchView = kb.NewBinding(&Keys.SwitchView) + key = &Keys.Refresh + case "refreshAll": + key = &Keys.RefreshAll + case "pageDown": + key = &Keys.PageDown + case "pageUp": + key = &Keys.PageUp + case "nextSection": + key = &Keys.NextSection + case "prevSection": + key = &Keys.PrevSection + case "switchView": + key = &Keys.SwitchView case "search": + key = &Keys.Search case "copyurl": - case "copynumber": + key = &Keys.CopyUrl + case "copyNumber": + key = &Keys.CopyNumber case "help": + key = &Keys.Help case "quit": + key = &Keys.Quit default: - // TODO: return an error here - return nil + return fmt.Errorf("unknown built-in universal key: '%s'", kb.Builtin) } + + key.SetKeys(kb.Key) + key.SetHelp(kb.Key, key.Help().Desc) } return nil diff --git a/ui/keys/prKeys.go b/ui/keys/prKeys.go index 62efe00f..88187b64 100644 --- a/ui/keys/prKeys.go +++ b/ui/keys/prKeys.go @@ -1,7 +1,10 @@ package keys import ( + "fmt" + "github.com/charmbracelet/bubbles/key" + log "github.com/charmbracelet/log" "github.com/dlvhdr/gh-dash/config" ) @@ -78,41 +81,42 @@ func PRFullHelp() []key.Binding { func rebindPRKeys(keys []config.Keybinding) error { for _, prKey := range keys { - var k key.Binding + if prKey.Builtin == "" { + continue + } + + log.Debug("Rebinding PR key", "builtin", prKey.Builtin, "key", prKey.Key) + + var key *key.Binding switch prKey.Builtin { case "assign": - k = PRKeys.Assign + key = &PRKeys.Assign case "unassign": - k = PRKeys.Unassign + key = &PRKeys.Unassign case "comment": - k = PRKeys.Comment + PRKeys.Comment = prKey.NewBinding(&PRKeys.Comment) + continue case "diff": - k = PRKeys.Diff + key = &PRKeys.Diff case "checkout": - k = PRKeys.Checkout + key = &PRKeys.Checkout case "close": - k = PRKeys.Close + key = &PRKeys.Close case "ready": - k = PRKeys.Ready + key = &PRKeys.Ready case "reopen": - k = PRKeys.Reopen + key = &PRKeys.Reopen case "merge": - k = PRKeys.Merge - case "watchchecks": - k = PRKeys.WatchChecks + key = &PRKeys.Merge + case "watchChecks": + key = &PRKeys.WatchChecks default: - // TODO: return an error here - return nil + return fmt.Errorf("unknown built-in pr key: '%s'", prKey.Builtin) } - // Not really sure if this is the best idea but I am not - // sure how else we are meant to define alt keys. - if len(prKey.Keys) > 0 { - k.SetKeys(prKey.Keys...) - } else { - k.SetKeys(prKey.Key) - } + key.SetKeys(prKey.Key) + key.SetHelp(prKey.Key, key.Help().Desc) } return nil diff --git a/ui/modelUtils.go b/ui/modelUtils.go index cc87cf26..8f5309ab 100644 --- a/ui/modelUtils.go +++ b/ui/modelUtils.go @@ -3,13 +3,13 @@ package ui import ( "bytes" "fmt" - "log" "os" "os/exec" "text/template" "time" tea "github.com/charmbracelet/bubbletea" + log "github.com/charmbracelet/log" "github.com/charmbracelet/lipgloss" "github.com/dlvhdr/gh-dash/config" @@ -91,10 +91,12 @@ func (m *Model) executeKeybinding(key string) tea.Cmd { } case config.PRsView: for _, keybinding := range m.ctx.Config.Keybindings.Prs { - if keybinding.Key != key { + if keybinding.Key != key || keybinding.Command == "" { continue } + log.Debug("executing keybind", "key", keybinding.Key, "command", keybinding.Command) + switch data := currRowData.(type) { case *data.PullRequestData: return m.runCustomPRCommand(keybinding.Command, data) @@ -127,13 +129,13 @@ func (m *Model) runCustomPRCommand(commandTemplate string, prData *data.PullRequ cmd, err := template.New("keybinding_command").Parse(commandTemplate) if err != nil { - log.Fatal("Failed parse keybinding template", err) + log.Fatal("Failed parse keybinding template", "error", err) } var buff bytes.Buffer err = cmd.Execute(&buff, input) if err != nil { - log.Fatal("Failed executing keybinding command", err) + log.Fatal("Failed executing keybinding command", "error", err) } return m.executeCustomCommand(buff.String()) } @@ -156,13 +158,13 @@ func (m *Model) runCustomIssueCommand(commandTemplate string, issueData *data.Is cmd, err := template.New("keybinding_command").Parse(commandTemplate) if err != nil { - log.Fatal("Failed parse keybinding template", err) + log.Fatal("Failed parse keybinding template", "error", err) } var buff bytes.Buffer err = cmd.Execute(&buff, input) if err != nil { - log.Fatal("Failed executing keybinding command", err) + log.Fatal("Failed executing keybinding command", "error", err) } return m.executeCustomCommand(buff.String()) } diff --git a/ui/ui.go b/ui/ui.go index eb69eb46..af589fff 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -247,7 +247,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } - case key.Matches(msg, keys.PRKeys.Comment), key.Matches(msg, keys.IssueKeys.Comment): + case key.Matches(msg, keys.PRKeys.Comment, keys.IssueKeys.Comment): + log.Debug("pr comment key", "key", msg.String(), "comment keybind", keys.PRKeys.Comment.Keys()) m.sidebar.IsOpen = true if m.ctx.View == config.PRsView { cmd = m.prSidebar.SetIsCommenting(true) From 600c226e038078f4213183b7b72706ec714fdf83 Mon Sep 17 00:00:00 2001 From: Ronnie Smith Date: Fri, 7 Jun 2024 22:45:30 -0700 Subject: [PATCH 3/6] Add comment about key check handle the pr and issue keys in different cases * remove universal switch view for per view binds v4 --- ui/keys/issueKeys.go | 4 +- ui/keys/keys.go | 2 - ui/keys/prKeys.go | 15 ++-- ui/ui.go | 206 ++++++++++++++++++++++++++----------------- 4 files changed, 134 insertions(+), 93 deletions(-) diff --git a/ui/keys/issueKeys.go b/ui/keys/issueKeys.go index a44f49d3..05633176 100644 --- a/ui/keys/issueKeys.go +++ b/ui/keys/issueKeys.go @@ -5,7 +5,7 @@ import ( "github.com/charmbracelet/bubbles/key" log "github.com/charmbracelet/log" - "github.com/dlvhdr/gh-dash/config" + "github.com/dlvhdr/gh-dash/v4/config" ) type IssueKeyMap struct { @@ -76,6 +76,8 @@ func rebindIssueKeys(keys []config.Keybinding) error { key = &IssueKeys.Close case "reopen": key = &IssueKeys.Reopen + case "viewPrs": + key = &IssueKeys.ViewPRs default: return fmt.Errorf("unknown built-in issue key: '%s'", issueKey.Builtin) } diff --git a/ui/keys/keys.go b/ui/keys/keys.go index a5208637..9ed32132 100644 --- a/ui/keys/keys.go +++ b/ui/keys/keys.go @@ -207,8 +207,6 @@ func rebindUniversal(universal []config.Keybinding) error { key = &Keys.NextSection case "prevSection": key = &Keys.PrevSection - case "switchView": - key = &Keys.SwitchView case "search": key = &Keys.Search case "copyurl": diff --git a/ui/keys/prKeys.go b/ui/keys/prKeys.go index d3c6ae24..f202659b 100644 --- a/ui/keys/prKeys.go +++ b/ui/keys/prKeys.go @@ -5,7 +5,7 @@ import ( "github.com/charmbracelet/bubbles/key" log "github.com/charmbracelet/log" - "github.com/dlvhdr/gh-dash/config" + "github.com/dlvhdr/gh-dash/v4/config" ) type PRKeyMap struct { @@ -93,7 +93,7 @@ func rebindPRKeys(keys []config.Keybinding) error { log.Debug("Rebinding PR key", "builtin", prKey.Builtin, "key", prKey.Key) - var key *key.Binding + var key *key.Binding switch prKey.Builtin { case "assign": @@ -101,8 +101,7 @@ func rebindPRKeys(keys []config.Keybinding) error { case "unassign": key = &PRKeys.Unassign case "comment": - PRKeys.Comment = prKey.NewBinding(&PRKeys.Comment) - continue + key = &PRKeys.Comment case "diff": key = &PRKeys.Diff case "checkout": @@ -117,12 +116,14 @@ func rebindPRKeys(keys []config.Keybinding) error { key = &PRKeys.Merge case "watchChecks": key = &PRKeys.WatchChecks + case "viewIssues": + key = &PRKeys.ViewIssues default: - return fmt.Errorf("unknown built-in pr key: '%s'", prKey.Builtin) + return fmt.Errorf("unknown built-in pr key: '%s'", prKey.Builtin) } - key.SetKeys(prKey.Key) - key.SetHelp(prKey.Key, key.Help().Desc) + key.SetKeys(prKey.Key) + key.SetHelp(prKey.Key, key.Help().Desc) } return nil diff --git a/ui/ui.go b/ui/ui.go index 738c9f5d..e83ab0d9 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -228,93 +228,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.setCurrentViewSections(newSections) cmds = append(cmds, fetchSectionsCmds) - case key.Matches(msg, keys.PRKeys.ViewIssues, keys.IssueKeys.ViewPRs): - m.ctx.View = m.switchSelectedView() - m.syncMainContentWidth() - m.setCurrSectionId(1) - - currSections := m.getCurrentViewSections() - if len(currSections) == 0 { - newSections, fetchSectionsCmds := m.fetchAllViewSections() - m.setCurrentViewSections(newSections) - cmd = fetchSectionsCmds - } - m.onViewedRowChanged() - case key.Matches(msg, m.keys.Search): if currSection != nil { cmd = currSection.SetIsSearching(true) return m, cmd } - case key.Matches(msg, keys.PRKeys.Comment, keys.IssueKeys.Comment): - log.Debug("pr comment key", "key", msg.String(), "comment keybind", keys.PRKeys.Comment.Keys()) - m.sidebar.IsOpen = true - if m.ctx.View == config.PRsView { - cmd = m.prSidebar.SetIsCommenting(true) - } else { - cmd = m.issueSidebar.SetIsCommenting(true) - } - m.syncMainContentWidth() - m.syncSidebar() - m.sidebar.ScrollToBottom() - return m, cmd - - case key.Matches( - msg, - keys.PRKeys.Close, - keys.PRKeys.Reopen, - keys.PRKeys.Ready, - keys.PRKeys.Merge, - keys.IssueKeys.Close, - keys.IssueKeys.Reopen, - ): - - var action string - switch { - case key.Matches(msg, keys.PRKeys.Close, keys.IssueKeys.Close): - action = "close" - - case key.Matches(msg, keys.PRKeys.Reopen, keys.IssueKeys.Reopen): - action = "reopen" - - case key.Matches(msg, keys.PRKeys.Ready): - action = "ready" - - case key.Matches(msg, keys.PRKeys.Merge): - action = "merge" - } - - if currSection != nil { - currSection.SetPromptConfirmationAction(action) - cmd = currSection.SetIsPromptConfirmationShown(true) - } - return m, cmd - - case key.Matches(msg, keys.IssueKeys.Assign), key.Matches(msg, keys.PRKeys.Assign): - m.sidebar.IsOpen = true - if m.ctx.View == config.PRsView { - cmd = m.prSidebar.SetIsAssigning(true) - } else { - cmd = m.issueSidebar.SetIsAssigning(true) - } - m.syncMainContentWidth() - m.syncSidebar() - m.sidebar.ScrollToBottom() - return m, cmd - - case key.Matches(msg, keys.IssueKeys.Unassign), key.Matches(msg, keys.PRKeys.Unassign): - m.sidebar.IsOpen = true - if m.ctx.View == config.PRsView { - cmd = m.prSidebar.SetIsUnassigning(true) - } else { - cmd = m.issueSidebar.SetIsUnassigning(true) - } - m.syncMainContentWidth() - m.syncSidebar() - m.sidebar.ScrollToBottom() - return m, cmd - case key.Matches(msg, m.keys.Help): if !m.footer.ShowAll { m.ctx.MainContentHeight = m.ctx.MainContentHeight + common.FooterHeight - common.ExpandedHelpHeight @@ -350,6 +269,127 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } cmd = tea.Quit + + case m.ctx.View == config.PRsView: + switch { + case key.Matches(msg, keys.PRKeys.Assign): + m.sidebar.IsOpen = true + cmd = m.prSidebar.SetIsAssigning(true) + m.syncMainContentWidth() + m.syncSidebar() + m.sidebar.ScrollToBottom() + return m, cmd + + case key.Matches(msg, keys.PRKeys.Unassign): + m.sidebar.IsOpen = true + cmd = m.prSidebar.SetIsUnassigning(true) + m.syncMainContentWidth() + m.syncSidebar() + m.sidebar.ScrollToBottom() + return m, cmd + + case key.Matches(msg, keys.PRKeys.Comment): + m.sidebar.IsOpen = true + cmd = m.prSidebar.SetIsCommenting(true) + m.syncMainContentWidth() + m.syncSidebar() + m.sidebar.ScrollToBottom() + return m, cmd + + case key.Matches(msg, keys.PRKeys.Close): + if currSection != nil { + currSection.SetPromptConfirmationAction("close") + cmd = currSection.SetIsPromptConfirmationShown(true) + } + return m, cmd + + case key.Matches(msg, keys.PRKeys.Ready): + if currSection != nil { + currSection.SetPromptConfirmationAction("ready") + cmd = currSection.SetIsPromptConfirmationShown(true) + } + return m, cmd + + case key.Matches(msg, keys.PRKeys.Reopen): + if currSection != nil { + currSection.SetPromptConfirmationAction("reopen") + cmd = currSection.SetIsPromptConfirmationShown(true) + } + return m, cmd + + case key.Matches(msg, keys.PRKeys.Merge): + if currSection != nil { + currSection.SetPromptConfirmationAction("merge") + cmd = currSection.SetIsPromptConfirmationShown(true) + } + return m, cmd + + case key.Matches(msg, keys.PRKeys.ViewIssues): + m.ctx.View = m.switchSelectedView() + m.syncMainContentWidth() + m.setCurrSectionId(1) + + currSections := m.getCurrentViewSections() + if len(currSections) == 0 { + newSections, fetchSectionsCmds := m.fetchAllViewSections() + m.setCurrentViewSections(newSections) + cmd = fetchSectionsCmds + } + m.onViewedRowChanged() + } + case m.ctx.View == config.IssuesView: + switch { + case key.Matches(msg, keys.IssueKeys.Assign): + m.sidebar.IsOpen = true + cmd = m.issueSidebar.SetIsAssigning(true) + m.syncMainContentWidth() + m.syncSidebar() + m.sidebar.ScrollToBottom() + return m, cmd + + case key.Matches(msg, keys.IssueKeys.Unassign): + m.sidebar.IsOpen = true + cmd = m.issueSidebar.SetIsUnassigning(true) + m.syncMainContentWidth() + m.syncSidebar() + m.sidebar.ScrollToBottom() + return m, cmd + + case key.Matches(msg, keys.IssueKeys.Comment): + m.sidebar.IsOpen = true + cmd = m.issueSidebar.SetIsCommenting(true) + m.syncMainContentWidth() + m.syncSidebar() + m.sidebar.ScrollToBottom() + return m, cmd + + case key.Matches(msg, keys.IssueKeys.Close): + if currSection != nil { + currSection.SetPromptConfirmationAction("close") + cmd = currSection.SetIsPromptConfirmationShown(true) + } + return m, cmd + + case key.Matches(msg, keys.IssueKeys.Reopen): + if currSection != nil { + currSection.SetPromptConfirmationAction("reopen") + cmd = currSection.SetIsPromptConfirmationShown(true) + } + return m, cmd + + case key.Matches(msg, keys.IssueKeys.ViewPRs): + m.ctx.View = m.switchSelectedView() + m.syncMainContentWidth() + m.setCurrSectionId(1) + + currSections := m.getCurrentViewSections() + if len(currSections) == 0 { + newSections, fetchSectionsCmds := m.fetchAllViewSections() + m.setCurrentViewSections(newSections) + cmd = fetchSectionsCmds + } + m.onViewedRowChanged() + } } case initMsg: @@ -647,7 +687,7 @@ func (m *Model) switchSelectedView() config.ViewType { func (m *Model) isUserDefinedKeybinding(msg tea.KeyMsg) bool { if m.ctx.View == config.IssuesView { for _, keybinding := range m.ctx.Config.Keybindings.Issues { - if keybinding.Key == msg.String() { + if keybinding.Builtin == "" && keybinding.Key == msg.String() { return true } } @@ -655,7 +695,7 @@ func (m *Model) isUserDefinedKeybinding(msg tea.KeyMsg) bool { if m.ctx.View == config.PRsView { for _, keybinding := range m.ctx.Config.Keybindings.Prs { - if keybinding.Key == msg.String() { + if keybinding.Builtin == "" && keybinding.Key == msg.String() { return true } } From ff3797568a828a80c716c02deffa07e999caeb14 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Fri, 21 Jun 2024 00:34:21 +0300 Subject: [PATCH 4/6] fix merge conflicts + format --- ui/keys/issueKeys.go | 1 + ui/keys/keys.go | 50 ++++++++++++++++++++++---------------------- ui/modelUtils.go | 34 +++++++----------------------- 3 files changed, 33 insertions(+), 52 deletions(-) diff --git a/ui/keys/issueKeys.go b/ui/keys/issueKeys.go index 05633176..c0eb407f 100644 --- a/ui/keys/issueKeys.go +++ b/ui/keys/issueKeys.go @@ -5,6 +5,7 @@ import ( "github.com/charmbracelet/bubbles/key" log "github.com/charmbracelet/log" + "github.com/dlvhdr/gh-dash/v4/config" ) diff --git a/ui/keys/keys.go b/ui/keys/keys.go index 9ed32132..541f65b3 100644 --- a/ui/keys/keys.go +++ b/ui/keys/keys.go @@ -172,57 +172,57 @@ func Rebind(universal, issueKeys, prKeys []config.Keybinding) error { } func rebindUniversal(universal []config.Keybinding) error { - log.Debug("Rebinding universal keys", "keys", universal) + log.Debug("Rebinding universal keys", "keys", universal) for _, kb := range universal { - if kb.Builtin == "" { - continue - } + if kb.Builtin == "" { + continue + } log.Debug("Rebinding universal key", "builtin", kb.Builtin, "key", kb.Key) - var key *key.Binding + var key *key.Binding switch kb.Builtin { case "up": - key = &Keys.Up + key = &Keys.Up case "down": - key = &Keys.Down + key = &Keys.Down case "firstLine": - key = &Keys.FirstLine + key = &Keys.FirstLine case "lastLine": - key = &Keys.LastLine + key = &Keys.LastLine case "togglePreview": - key = &Keys.TogglePreview + key = &Keys.TogglePreview case "openGithub": - key = &Keys.OpenGithub + key = &Keys.OpenGithub case "refresh": - key = &Keys.Refresh + key = &Keys.Refresh case "refreshAll": - key = &Keys.RefreshAll + key = &Keys.RefreshAll case "pageDown": - key = &Keys.PageDown + key = &Keys.PageDown case "pageUp": - key = &Keys.PageUp + key = &Keys.PageUp case "nextSection": - key = &Keys.NextSection + key = &Keys.NextSection case "prevSection": - key = &Keys.PrevSection + key = &Keys.PrevSection case "search": - key = &Keys.Search + key = &Keys.Search case "copyurl": - key = &Keys.CopyUrl + key = &Keys.CopyUrl case "copyNumber": - key = &Keys.CopyNumber + key = &Keys.CopyNumber case "help": - key = &Keys.Help + key = &Keys.Help case "quit": - key = &Keys.Quit + key = &Keys.Quit default: - return fmt.Errorf("unknown built-in universal key: '%s'", kb.Builtin) + return fmt.Errorf("unknown built-in universal key: '%s'", kb.Builtin) } - key.SetKeys(kb.Key) - key.SetHelp(kb.Key, key.Help().Desc) + key.SetKeys(kb.Key) + key.SetHelp(kb.Key, key.Help().Desc) } return nil diff --git a/ui/modelUtils.go b/ui/modelUtils.go index 486ab988..6a258ca1 100644 --- a/ui/modelUtils.go +++ b/ui/modelUtils.go @@ -131,7 +131,7 @@ func (m *Model) runCustomCommand(commandTemplate string, contextData *map[string err = cmd.Execute(&buff, input) if err != nil { return func() tea.Msg { - return constants.ErrMsg{Err: fmt.Errorf("Failed to parsetemplate %s", commandTemplate)} + return constants.ErrMsg{Err: fmt.Errorf("failed to parsetemplate %s", commandTemplate)} } } return m.executeCustomCommand(buff.String()) @@ -148,32 +148,12 @@ func (m *Model) runCustomPRCommand(commandTemplate string, prData *data.PullRequ } func (m *Model) runCustomIssueCommand(commandTemplate string, issueData *data.IssueData) tea.Cmd { - repoName := issueData.GetRepoNameWithOwner() - repoPath, ok := common.GetRepoLocalPath(repoName, m.ctx.Config.RepoPaths) - - if !ok { - return func() tea.Msg { - return constants.ErrMsg{Err: fmt.Errorf("Failed to find local path for repo %s", repoName)} - } - } - - input := IssueCommandTemplateInput{ - RepoName: repoName, - RepoPath: repoPath, - IssueNumber: issueData.Number, - } - - cmd, err := template.New("keybinding_command").Parse(commandTemplate) - if err != nil { - log.Fatal("Failed parse keybinding template", "error", err) - } - - var buff bytes.Buffer - err = cmd.Execute(&buff, input) - if err != nil { - log.Fatal("Failed executing keybinding command", "error", err) - } - return m.executeCustomCommand(buff.String()) + return m.runCustomCommand(commandTemplate, + &map[string]any{ + "RepoName": issueData.GetRepoNameWithOwner(), + "IssueNumber": issueData.Number, + }, + ) } func (m *Model) executeCustomCommand(cmd string) tea.Cmd { From 01755fc18d3a1180e7b1c7368e86b37743e9d316 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Fri, 21 Jun 2024 00:49:30 +0300 Subject: [PATCH 5/6] readme --- README.md | 64 ++++++++++++++++++++++++++++++++++------------- ui/keys/prKeys.go | 7 +++--- 2 files changed, 50 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ed51fbef..355067e6 100644 --- a/README.md +++ b/README.md @@ -7,15 +7,16 @@ ## ✨ Features -* 🌅 fully configurable - define sections using github filters -* 🔍 search for both prs and issues -* 📝 customize columns with `hidden`, `width` and `grow` props -* ⚡️ act on prs and issues with checkout, comment, open, merge, diff, etc... -* ⌨️ set custom actions with new keybindings -* 💅 use custom themes -* 🔭 view details about a pr/issue with a detailed sidebar -* 🪟 write multiple configuration files to easily switch between completely different dashboards -* ♻️ set an interval for auto refreshing the dashboard + +- 🌅 fully configurable - define sections using github filters +- 🔍 search for both prs and issues +- 📝 customize columns with `hidden`, `width` and `grow` props +- ⚡️ act on prs and issues with checkout, comment, open, merge, diff, etc... +- ⌨️ set custom actions with new keybindings +- 💅 use custom themes +- 🔭 view details about a pr/issue with a detailed sidebar +- 🪟 write multiple configuration files to easily switch between completely different dashboards +- ♻️ set an interval for auto refreshing the dashboard ## 📦 Installation @@ -70,19 +71,23 @@
How do I get these exact colors and font? - > I'm using [Alacritty](https://github.com/alacritty/alacritty) with the [tokyonight theme](https://github.com/folke/tokyonight.nvim) and the [Fira Code](https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts/FiraCode) Nerd Font. - > For my full setup check out [my dotfiles](https://github.com/dlvhdr/dotfiles/blob/main/.config/alacritty/alacritty.yml). +> I'm using [Alacritty](https://github.com/alacritty/alacritty) with the [tokyonight theme](https://github.com/folke/tokyonight.nvim) and the [Fira Code](https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts/FiraCode) Nerd Font. +> For my full setup check out [my dotfiles](https://github.com/dlvhdr/dotfiles/blob/main/.config/alacritty/alacritty.yml). +
## ⚡️ Usage Run + ```sh gh dash ``` + Then press ? for help. Run `gh dash --help` for more info: + ``` Usage: gh dash [flags] @@ -96,6 +101,7 @@ Flags: ## ⚙️ Configuring A section is defined by a: + - title - shown in the TUI - filters - how the repo's PRs should be filtered - these are plain [github filters](https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests) @@ -158,9 +164,31 @@ It can be useful if you want to have a 🧳 work and 👩‍💻 personal dashbo ### ⌨️ Keybindings -Define your own custom keybindings to run bash commands using [Go Templates](https://pkg.go.dev/text/template). -This is available for both PRs and Issues. +You can: + +1. Override the builtin commands keybindings +2. Define your own custom keybindings to run bash commands using [Go Templates](https://pkg.go.dev/text/template). + +#### Overriding builtin commands keybindings + +To override the "checkout" keybinding you can include this in your `config.yml` file: + +```yaml +keybindings: + prs: + - key: O + builtin: checkout +``` + +The list of available builtin commands are: + +1. Universal: "up", "down", "firstLine", "lastLine", "togglePreview", "openGithub", "refresh", "refreshAll", "pageDown", "pageUp", "nextSection", "prevSection", "search", "copyurl", "copyNumber", "help", "quit" +2. PRs: "assign", "unassign", "comment", "diff", "checkout", "close", "ready", "reopen", "merge", "watchChecks", "viewIssues" +3. Issues: "assign", "unassign", "comment", "close", "reopen", "viewPrs" +#### Defining custom keybindings + +This is available for both PRs and Issues. For PRs, the available arguments are: | Argument | Description | @@ -179,9 +207,9 @@ For Issues, the available arguments are: | `RepoPath` | The path to the Repo, using the `config.yml` `repoPaths` key to get the mapping | | `IssueNumber` | The Issue number | -#### Examples +**Examples** -To review a PR with either Neovim or VSCode include the following in your `config.yml` file: +1. To review a PR with either Neovim or VSCode include the following in your `config.yml` file: ```yaml repoPaths: @@ -202,7 +230,7 @@ keybindings: gh pr checkout {{.PrNumber}} ``` -To pin an issue include the following in your `config.yml` file: +2. To pin an issue include the following in your `config.yml` file: ```yaml keybindings: @@ -222,7 +250,7 @@ An `:owner/:repo` template can be specified as a generic fallback. ```yaml repoPaths: :owner/:repo: ~/src/github.com/:owner/:repo # template if you always clone github repos in a consistent location - dlvhdr/*: ~/code/repos/dlvhdr/* # will match dlvhdr/repo-name to ~/code/repos/dlvhdr/repo-name + dlvhdr/*: ~/code/repos/dlvhdr/* # will match dlvhdr/repo-name to ~/code/repos/dlvhdr/repo-name dlvhdr/gh-dash: ~/code/gh-dash # will not match wildcard and map to specified path ``` @@ -259,6 +287,7 @@ theme: You can customize each section's layout as well as the global layout. For example, to hide the `author` column for **all** PR sections, include the following in your `config.yml`. + ``` defaults: layout: @@ -271,7 +300,6 @@ defaults: - For `issues` the column names are: `updatedAt, state, repo, title, creator, assignees, comments, reactions`. - The available properties to control are: `grow` (false, true), `width` (number of cells), and `hidden` (false, true). - ## Author Dolev Hadar dolevc2@gmail.com diff --git a/ui/keys/prKeys.go b/ui/keys/prKeys.go index f202659b..c79ad556 100644 --- a/ui/keys/prKeys.go +++ b/ui/keys/prKeys.go @@ -5,6 +5,7 @@ import ( "github.com/charmbracelet/bubbles/key" log "github.com/charmbracelet/log" + "github.com/dlvhdr/gh-dash/v4/config" ) @@ -101,7 +102,7 @@ func rebindPRKeys(keys []config.Keybinding) error { case "unassign": key = &PRKeys.Unassign case "comment": - key = &PRKeys.Comment + key = &PRKeys.Comment case "diff": key = &PRKeys.Diff case "checkout": @@ -116,8 +117,8 @@ func rebindPRKeys(keys []config.Keybinding) error { key = &PRKeys.Merge case "watchChecks": key = &PRKeys.WatchChecks - case "viewIssues": - key = &PRKeys.ViewIssues + case "viewIssues": + key = &PRKeys.ViewIssues default: return fmt.Errorf("unknown built-in pr key: '%s'", prKey.Builtin) } From 994578d41d35e0f08b35d4647e52249727cbcaa9 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Fri, 21 Jun 2024 00:51:42 +0300 Subject: [PATCH 6/6] readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 355067e6..1537e14e 100644 --- a/README.md +++ b/README.md @@ -182,9 +182,9 @@ keybindings: The list of available builtin commands are: -1. Universal: "up", "down", "firstLine", "lastLine", "togglePreview", "openGithub", "refresh", "refreshAll", "pageDown", "pageUp", "nextSection", "prevSection", "search", "copyurl", "copyNumber", "help", "quit" -2. PRs: "assign", "unassign", "comment", "diff", "checkout", "close", "ready", "reopen", "merge", "watchChecks", "viewIssues" -3. Issues: "assign", "unassign", "comment", "close", "reopen", "viewPrs" +1. `universal`: up, down, firstLine, lastLine, togglePreview, openGithub, refresh, refreshAll, pageDown, pageUp, nextSection, prevSection, search, copyurl, copyNumber, help, quit +2. `prs`: assign, unassign, comment, diff, checkout, close, ready, reopen, merge, watchChecks, viewIssues +3. `Issues`: assign, unassign, comment, close, reopen, viewPrs #### Defining custom keybindings