├── .gitignore
├── .github
└── workflows
│ └── release.yml
├── main.go
├── go.mod
├── README.md
├── tui
├── styles
│ └── styles.go
├── delegate.go
├── keys
│ └── keys.go
├── rename.go
├── rebase.go
├── merge.go
├── delete.go
├── create.go
└── tui.go
├── git
└── git.go
└── go.sum
/.gitignore:
--------------------------------------------------------------------------------
1 | /gh-b
2 | /gh-b.exe
3 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 | on:
3 | push:
4 | tags:
5 | - "v*"
6 | permissions:
7 | contents: write
8 |
9 | jobs:
10 | release:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - uses: cli/gh-extension-precompile@v1
15 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | tea "github.com/charmbracelet/bubbletea"
8 | "github.com/joaom00/gh-b/tui"
9 | )
10 |
11 | func main() {
12 | p := tea.NewProgram(tui.NewModel())
13 |
14 | err := p.Start()
15 | if err != nil {
16 | fmt.Println("Error running program:", err)
17 | os.Exit(1)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/joaom00/gh-b
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/atotto/clipboard v0.1.4 // indirect
7 | github.com/containerd/console v1.0.3 // indirect
8 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
9 | github.com/mattn/go-isatty v0.0.14 // indirect
10 | github.com/mattn/go-runewidth v0.0.13 // indirect
11 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
12 | github.com/muesli/reflow v0.3.0 // indirect
13 | github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect
14 | github.com/rivo/uniseg v0.2.0 // indirect
15 | github.com/sahilm/fuzzy v0.1.0 // indirect
16 | golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912 // indirect
17 | golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
18 | )
19 |
20 | require (
21 | github.com/charmbracelet/bubbles v0.10.3
22 | github.com/charmbracelet/bubbletea v0.20.0
23 | github.com/charmbracelet/lipgloss v0.5.0
24 | )
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # gh b
2 |
3 | [GitHub CLI](https://github.com/cli/cli) extension to manage your branches.
4 |
5 | [](https://asciinema.org/a/472292)
6 |
7 | ## Installation
8 |
9 | ```
10 | gh extension install joaom00/gh-b
11 | ```
12 |
13 | ## Usage
14 |
15 | ```
16 | gh b
17 | ```
18 |
19 | ## Mappings
20 |
21 | | Key | Action |
22 | | ------------------- | ------------------------------------------------------- |
23 | | j/ctrl+j | Move down |
24 | | k/ctrl+k | Move up |
25 | | / | Enable filter |
26 | | ? | Toggle help |
27 | | Enter | Checkout the currently selected branch |
28 | | Ctrl+a | Create a new branch, with confirmation |
29 | | Ctrl+d | Delete the currently selected branch, with confirmation |
30 | | Ctrl+t | Track the currently selected branch, with confirmation |
31 | | Ctrl+y | Merge the currently selected branch, with confirmation |
32 | | Ctrl+u | Rebase the currently selected branch, with confirmation |
33 | | Ctrl+r | Rename the currently selected branch |
34 |
--------------------------------------------------------------------------------
/tui/styles/styles.go:
--------------------------------------------------------------------------------
1 | package styles
2 |
3 | import (
4 | "github.com/charmbracelet/bubbles/list"
5 | "github.com/charmbracelet/lipgloss"
6 | )
7 |
8 | type Styles struct {
9 | Title lipgloss.Style
10 |
11 | NormalTitle lipgloss.Style
12 | NormalDesc lipgloss.Style
13 |
14 | SelectedTitle lipgloss.Style
15 | SelectedDesc lipgloss.Style
16 |
17 | Pagination lipgloss.Style
18 | Help lipgloss.Style
19 | QuitText lipgloss.Style
20 | }
21 |
22 | func DefaultStyles() (s Styles) {
23 | s.Title = lipgloss.NewStyle().
24 | Background(lipgloss.Color("62")).
25 | Foreground(lipgloss.Color("230")).
26 | Padding(0, 1)
27 |
28 | s.NormalTitle = lipgloss.NewStyle().
29 | PaddingLeft(4).
30 | Foreground(lipgloss.AdaptiveColor{Light: "#1A1A1A", Dark: "#DDDDDD"})
31 |
32 | s.NormalDesc = lipgloss.NewStyle().
33 | Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"})
34 |
35 | s.SelectedTitle = lipgloss.NewStyle().
36 | PaddingLeft(2).
37 | Foreground(lipgloss.AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"})
38 |
39 | s.SelectedDesc = lipgloss.NewStyle().
40 | Foreground(lipgloss.AdaptiveColor{Light: "#F793FF", Dark: "#AD58B4"})
41 |
42 | s.Pagination = list.DefaultStyles().
43 | PaginationStyle.
44 | PaddingLeft(4)
45 |
46 | s.Help = list.DefaultStyles().
47 | HelpStyle.
48 | PaddingLeft(4).
49 | PaddingBottom(1)
50 |
51 | s.QuitText = lipgloss.NewStyle().
52 | Margin(1, 0, 2, 4)
53 |
54 | return s
55 | }
56 |
--------------------------------------------------------------------------------
/tui/delegate.go:
--------------------------------------------------------------------------------
1 | package tui
2 |
3 | import (
4 | "fmt"
5 | "io"
6 |
7 | "github.com/charmbracelet/bubbles/key"
8 | "github.com/charmbracelet/bubbles/list"
9 | tea "github.com/charmbracelet/bubbletea"
10 | "github.com/joaom00/gh-b/tui/keys"
11 | "github.com/joaom00/gh-b/tui/styles"
12 | )
13 |
14 | type itemDelegate struct {
15 | keys *keys.KeyMap
16 | styles *styles.Styles
17 | }
18 |
19 | func newItemDelegate(keys *keys.KeyMap, styles *styles.Styles) *itemDelegate {
20 | return &itemDelegate{
21 | keys: keys,
22 | styles: styles,
23 | }
24 | }
25 |
26 | func (d itemDelegate) Height() int { return 1 }
27 | func (d itemDelegate) Spacing() int { return 0 }
28 | func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
29 |
30 | func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
31 | i, ok := listItem.(item)
32 | if !ok {
33 | return
34 | }
35 |
36 | title := d.styles.NormalTitle.Render
37 | desc := d.styles.NormalDesc.Render
38 |
39 | if index == m.Index() {
40 | title = func(s string) string {
41 | return d.styles.SelectedTitle.Render("> " + s)
42 | }
43 | desc = func(s string) string {
44 | return d.styles.SelectedDesc.Render(s)
45 | }
46 | }
47 |
48 | branch := title(i.Name)
49 | author := desc(i.AuthorName)
50 | committerDate := desc(fmt.Sprintf("(%s)", i.CommitterDate))
51 |
52 | itemListStyle := fmt.Sprintf("%s %s %s", branch, author, committerDate)
53 |
54 | fmt.Fprint(w, itemListStyle)
55 | }
56 |
57 | func (d itemDelegate) ShortHelp() []key.Binding {
58 | return []key.Binding{}
59 | }
60 |
61 | func (d itemDelegate) FullHelp() [][]key.Binding {
62 | return [][]key.Binding{
63 | {d.keys.Track, d.keys.Create, d.keys.Delete, d.keys.Merge, d.keys.Rebase, d.keys.Rename},
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/tui/keys/keys.go:
--------------------------------------------------------------------------------
1 | package keys
2 |
3 | import (
4 | "github.com/charmbracelet/bubbles/key"
5 | )
6 |
7 | type KeyMap struct {
8 | CursorUp key.Binding
9 | CursorDown key.Binding
10 | Enter key.Binding
11 | Create key.Binding
12 | Delete key.Binding
13 | Track key.Binding
14 | Merge key.Binding
15 | Rebase key.Binding
16 | Rename key.Binding
17 | Cancel key.Binding
18 | Quit key.Binding
19 | ForceQuit key.Binding
20 |
21 | State string
22 | }
23 |
24 | func (k KeyMap) ShortHelp() []key.Binding {
25 | var kb []key.Binding
26 |
27 | if k.State != "browsing" {
28 | kb = append(kb, k.Cancel, k.ForceQuit)
29 | }
30 |
31 | return kb
32 | }
33 |
34 | func (k KeyMap) FullHelp() [][]key.Binding {
35 | return [][]key.Binding{}
36 | }
37 |
38 | func NewKeyMap() *KeyMap {
39 | return &KeyMap{
40 | CursorUp: key.NewBinding(
41 | key.WithKeys("ctrl+k"),
42 | key.WithHelp("ctrl+k", "move up"),
43 | ),
44 | CursorDown: key.NewBinding(
45 | key.WithKeys("ctrl+j"),
46 | key.WithHelp("ctrl+j", "move down"),
47 | ),
48 | Enter: key.NewBinding(
49 | key.WithKeys("enter"),
50 | key.WithHelp("enter", "Check out the currently selected branch"),
51 | ),
52 | Create: key.NewBinding(
53 | key.WithKeys("ctrl+a"),
54 | key.WithHelp(
55 | "ctrl+a",
56 | "Create a new branch, with confirmation",
57 | ),
58 | ),
59 | Delete: key.NewBinding(
60 | key.WithKeys("ctrl+d"),
61 | key.WithHelp(
62 | "ctrl+d",
63 | "Delete the currently selected branch, with confirmation",
64 | ),
65 | ),
66 | Track: key.NewBinding(
67 | key.WithKeys("ctrl+t"),
68 | key.WithHelp("ctrl+t", "Track the currently selected branch"),
69 | ),
70 | Merge: key.NewBinding(
71 | key.WithKeys("ctrl+y"),
72 | key.WithHelp(
73 | "ctrl+y",
74 | "Merge the currently selected branch, with confirmation",
75 | ),
76 | ),
77 | Rebase: key.NewBinding(
78 | key.WithKeys("ctrl+u"),
79 | key.WithHelp(
80 | "ctrl+u",
81 | "Rebase the currently selected branch, with confirmation",
82 | ),
83 | ),
84 | Rename: key.NewBinding(
85 | key.WithKeys("ctrl+r"),
86 | key.WithHelp("ctrl+r", "Rename the currently selected branch"),
87 | ),
88 | Cancel: key.NewBinding(
89 | key.WithKeys("esc"),
90 | key.WithHelp("esc", "Cancel"),
91 | ),
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/tui/rename.go:
--------------------------------------------------------------------------------
1 | package tui
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/charmbracelet/bubbles/help"
7 | "github.com/charmbracelet/bubbles/key"
8 | "github.com/charmbracelet/bubbles/textinput"
9 | tea "github.com/charmbracelet/bubbletea"
10 | "github.com/charmbracelet/lipgloss"
11 | "github.com/joaom00/gh-b/git"
12 | )
13 |
14 | type renameModel struct {
15 | input textinput.Model
16 | confirmInput textinput.Model
17 | showConfirmInput bool
18 | help help.Model
19 | }
20 |
21 | func newRenameModel() *renameModel {
22 | ti := textinput.New()
23 | ci := textinput.New()
24 | ci.CharLimit = 1
25 |
26 | return &renameModel{
27 | input: ti,
28 | confirmInput: ci,
29 | showConfirmInput: false,
30 | help: help.New(),
31 | }
32 | }
33 |
34 | func renameUpdate(msg tea.Msg, m Model) (tea.Model, tea.Cmd) {
35 | switch msg := msg.(type) {
36 | case tea.KeyMsg:
37 | switch {
38 | case key.Matches(msg, m.keyMap.Enter):
39 | if i, ok := m.selectedItem(); ok {
40 | out := git.RenameBranch(i.Name, m.rename.input.Value())
41 |
42 | m.state = browsing
43 | m.updateListItem()
44 | m.list.NewStatusMessage(out)
45 |
46 | return m, nil
47 | }
48 |
49 | case key.Matches(msg, m.keyMap.Cancel):
50 | m.rename.input.Reset()
51 | m.state = browsing
52 | m.updateKeybindins()
53 | }
54 | }
55 |
56 | var cmd tea.Cmd
57 | var cmds []tea.Cmd
58 |
59 | m.rename.input, cmd = m.rename.input.Update(msg)
60 | cmds = append(cmds, cmd)
61 |
62 | m.rename.confirmInput, cmd = m.rename.confirmInput.Update(msg)
63 | cmds = append(cmds, cmd)
64 |
65 | return m, tea.Batch(cmds...)
66 | }
67 |
68 | func (m Model) renameView() string {
69 | m.rename.input.Placeholder = strings.TrimSuffix(m.list.SelectedItem().(item).Name, "*")
70 |
71 | title := m.styles.Title.MarginLeft(2).Render("Rename Branch")
72 | textInput := lipgloss.NewStyle().MarginLeft(4).Render(m.rename.input.View())
73 | help := lipgloss.NewStyle().MarginLeft(4).Render(m.create.help.View(m.keyMap))
74 |
75 | if m.rename.showConfirmInput {
76 | confirmInput := lipgloss.NewStyle().
77 | MarginLeft(4).
78 | Render(lipgloss.JoinHorizontal(lipgloss.Left, "You would like rename remote branch? [y/N]", m.rename.confirmInput.View()))
79 |
80 | return lipgloss.NewStyle().MarginTop(1).Render(lipgloss.JoinVertical(lipgloss.Left, title, "\n", textInput, "\n", confirmInput, "\n", help))
81 | }
82 |
83 | return lipgloss.NewStyle().
84 | MarginTop(1).
85 | Render(lipgloss.JoinVertical(lipgloss.Left, title, "\n", textInput, "\n", help))
86 | }
87 |
--------------------------------------------------------------------------------
/tui/rebase.go:
--------------------------------------------------------------------------------
1 | package tui
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/charmbracelet/bubbles/help"
8 | "github.com/charmbracelet/bubbles/key"
9 | "github.com/charmbracelet/bubbles/textinput"
10 | tea "github.com/charmbracelet/bubbletea"
11 | "github.com/charmbracelet/lipgloss"
12 | "github.com/joaom00/gh-b/git"
13 | )
14 |
15 | type rebaseModel struct {
16 | help help.Model
17 | confirmInput textinput.Model
18 | }
19 |
20 | func newRebaseModel() *rebaseModel {
21 | ci := textinput.New()
22 | ci.CharLimit = 1
23 |
24 | return &rebaseModel{
25 | help: help.New(),
26 | confirmInput: ci,
27 | }
28 | }
29 |
30 | func rebaseUpdate(msg tea.Msg, m Model) (tea.Model, tea.Cmd) {
31 | switch msg := msg.(type) {
32 | case tea.KeyMsg:
33 | switch {
34 | case key.Matches(msg, m.keyMap.Enter):
35 | switch m.rebase.confirmInput.Value() {
36 | case "y", "Y", "":
37 | if i, ok := m.list.SelectedItem().(item); ok {
38 | i.Name = strings.TrimSuffix(i.Name, "*")
39 | out := git.RebaseBranch(i.Name)
40 |
41 | fmt.Println(m.styles.NormalTitle.Copy().MarginTop(1).Render(out))
42 |
43 | return m, tea.Quit
44 | }
45 |
46 | case "n", "N":
47 | m.rebase.confirmInput.Reset()
48 | m.state = browsing
49 | m.keyMap.State = "browsing"
50 | m.updateKeybindins()
51 |
52 | default:
53 | m.rebase.confirmInput.SetValue("")
54 | }
55 | case key.Matches(msg, m.keyMap.Cancel):
56 | m.rebase.confirmInput.Reset()
57 | m.state = browsing
58 | m.keyMap.State = "browsing"
59 | m.updateKeybindins()
60 | }
61 | case tea.WindowSizeMsg:
62 | m.rebase.help.Width = msg.Width
63 | }
64 |
65 | var cmd tea.Cmd
66 | m.rebase.confirmInput, cmd = m.rebase.confirmInput.Update(msg)
67 |
68 | return m, cmd
69 | }
70 |
71 | func (m Model) rebaseView() string {
72 | title := m.styles.Title.MarginLeft(2).Render("Rebase Branch")
73 | help := lipgloss.NewStyle().MarginLeft(4).Render(m.rebase.help.View(m.keyMap))
74 | var branchName string
75 |
76 | i, ok := m.list.SelectedItem().(item)
77 | if ok {
78 | branchName = lipgloss.NewStyle().
79 | Foreground(lipgloss.AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"}).
80 | Render(i.Name)
81 | }
82 |
83 | label := fmt.Sprintf("Do you really wanna rebase branch \"%s\"? [Y/n]", branchName)
84 |
85 | confirmInput := lipgloss.NewStyle().
86 | MarginLeft(4).
87 | Render(lipgloss.JoinHorizontal(
88 | lipgloss.Left,
89 | label,
90 | m.rebase.confirmInput.View(),
91 | ))
92 |
93 | return lipgloss.NewStyle().
94 | MarginTop(1).
95 | Render(lipgloss.JoinVertical(lipgloss.Left, title, "\n", confirmInput, "\n", help))
96 | }
97 |
--------------------------------------------------------------------------------
/tui/merge.go:
--------------------------------------------------------------------------------
1 | package tui
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/charmbracelet/bubbles/help"
8 | "github.com/charmbracelet/bubbles/key"
9 | "github.com/charmbracelet/bubbles/textinput"
10 | tea "github.com/charmbracelet/bubbletea"
11 | "github.com/charmbracelet/lipgloss"
12 | "github.com/joaom00/gh-b/git"
13 | )
14 |
15 | type mergeModel struct {
16 | help help.Model
17 | confirmInput textinput.Model
18 | }
19 |
20 | func newMergeModel() *mergeModel {
21 | ci := textinput.New()
22 | ci.CharLimit = 1
23 |
24 | return &mergeModel{
25 | help: help.New(),
26 | confirmInput: ci,
27 | }
28 | }
29 |
30 | func mergeUpdate(msg tea.Msg, m Model) (tea.Model, tea.Cmd) {
31 | switch msg := msg.(type) {
32 | case tea.KeyMsg:
33 | switch {
34 | case key.Matches(msg, m.keyMap.Enter):
35 | switch m.delete.confirmInput.Value() {
36 | case "y", "Y", "":
37 | if i, ok := m.list.SelectedItem().(item); ok {
38 | i.Name = strings.TrimSuffix(i.Name, "*")
39 | out := git.MergeBranch(i.Name)
40 |
41 | fmt.Println(m.styles.NormalTitle.Copy().MarginTop(1).Render(out))
42 |
43 | return m, tea.Quit
44 | }
45 |
46 | case "n", "N":
47 | m.merge.confirmInput.Reset()
48 | m.state = browsing
49 | m.keyMap.State = "browsing"
50 | m.updateKeybindins()
51 |
52 | default:
53 | m.merge.confirmInput.SetValue("")
54 | }
55 |
56 | case key.Matches(msg, m.keyMap.Cancel):
57 | m.merge.confirmInput.Reset()
58 | m.state = browsing
59 | m.keyMap.State = "browsing"
60 | m.updateKeybindins()
61 | }
62 |
63 | case tea.WindowSizeMsg:
64 | m.merge.help.Width = msg.Width
65 | }
66 |
67 | var cmd tea.Cmd
68 | m.merge.confirmInput, cmd = m.merge.confirmInput.Update(msg)
69 |
70 | return m, cmd
71 | }
72 |
73 | func (m Model) mergeView() string {
74 | title := m.styles.Title.MarginLeft(2).Render("Merge Branch")
75 | help := lipgloss.NewStyle().MarginLeft(4).Render(m.merge.help.View(m.keyMap))
76 |
77 | var branchName string
78 |
79 | i, ok := m.list.SelectedItem().(item)
80 | if ok {
81 | branchName = lipgloss.NewStyle().
82 | Foreground(lipgloss.AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"}).
83 | Render(i.Name)
84 | }
85 |
86 | label := fmt.Sprintf("Do you really wanna merge branch \"%s\"? [Y/n]", branchName)
87 |
88 | confirmInput := lipgloss.NewStyle().
89 | MarginLeft(4).
90 | Render(lipgloss.JoinHorizontal(
91 | lipgloss.Left,
92 | label,
93 | m.merge.confirmInput.View(),
94 | ))
95 |
96 | return lipgloss.NewStyle().
97 | MarginTop(1).
98 | Render(lipgloss.JoinVertical(lipgloss.Left, title, "\n", confirmInput, "\n", help))
99 | }
100 |
--------------------------------------------------------------------------------
/tui/delete.go:
--------------------------------------------------------------------------------
1 | package tui
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/charmbracelet/bubbles/help"
8 | "github.com/charmbracelet/bubbles/key"
9 | "github.com/charmbracelet/bubbles/textinput"
10 | tea "github.com/charmbracelet/bubbletea"
11 | "github.com/charmbracelet/lipgloss"
12 | "github.com/joaom00/gh-b/git"
13 | )
14 |
15 | type deleteModel struct {
16 | help help.Model
17 | confirmInput textinput.Model
18 | }
19 |
20 | func newDeleteModel() *deleteModel {
21 | ci := textinput.New()
22 | ci.CharLimit = 1
23 |
24 | return &deleteModel{
25 | help: help.New(),
26 | confirmInput: ci,
27 | }
28 | }
29 |
30 | func deleteUpdate(msg tea.Msg, m Model) (tea.Model, tea.Cmd) {
31 | switch msg := msg.(type) {
32 | case tea.KeyMsg:
33 | switch {
34 | case key.Matches(msg, m.keyMap.Enter):
35 | switch m.delete.confirmInput.Value() {
36 | case "y", "Y", "":
37 | if i, ok := m.list.SelectedItem().(item); ok {
38 | i.Name = strings.TrimSuffix(i.Name, "*")
39 | out := git.DeleteBranch(i.Name)
40 |
41 | m.updateListItem()
42 | m.state = browsing
43 | m.keyMap.State = "browsing"
44 | m.updateKeybindins()
45 | m.list.NewStatusMessage(out)
46 | }
47 |
48 | case "n", "N":
49 | m.delete.confirmInput.Reset()
50 | m.state = browsing
51 | m.keyMap.State = "browsing"
52 | m.updateKeybindins()
53 |
54 | default:
55 | m.delete.confirmInput.SetValue("")
56 | }
57 |
58 | case key.Matches(msg, m.keyMap.Cancel):
59 | m.delete.confirmInput.Reset()
60 | m.state = browsing
61 | m.keyMap.State = "browsing"
62 | m.updateKeybindins()
63 | }
64 | case tea.WindowSizeMsg:
65 | m.delete.help.Width = msg.Width
66 | }
67 |
68 | var cmd tea.Cmd
69 | m.delete.confirmInput, cmd = m.delete.confirmInput.Update(msg)
70 |
71 | return m, cmd
72 | }
73 |
74 | func (m Model) deleteView() string {
75 | title := m.styles.Title.MarginLeft(2).Render("Delete Branch")
76 | help := lipgloss.NewStyle().MarginLeft(4).Render(m.delete.help.View(m.keyMap))
77 |
78 | var branchName string
79 |
80 | if i, ok := m.list.SelectedItem().(item); ok {
81 | branchName = lipgloss.NewStyle().
82 | Foreground(lipgloss.AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"}).
83 | Render(i.Name)
84 | }
85 |
86 | label := fmt.Sprintf("Do you really wanna delete branch \"%s\"? [Y/n]", branchName)
87 |
88 | confirmInput := lipgloss.NewStyle().
89 | MarginLeft(4).
90 | Render(lipgloss.JoinHorizontal(
91 | lipgloss.Left,
92 | label,
93 | m.delete.confirmInput.View(),
94 | ))
95 |
96 | return lipgloss.NewStyle().
97 | MarginTop(1).
98 | Render(lipgloss.JoinVertical(lipgloss.Left, title, "\n", confirmInput, "\n", help))
99 | }
100 |
--------------------------------------------------------------------------------
/tui/create.go:
--------------------------------------------------------------------------------
1 | package tui
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/charmbracelet/bubbles/help"
7 | "github.com/charmbracelet/bubbles/key"
8 | "github.com/charmbracelet/bubbles/textinput"
9 | tea "github.com/charmbracelet/bubbletea"
10 | "github.com/charmbracelet/lipgloss"
11 | "github.com/joaom00/gh-b/git"
12 | )
13 |
14 | type createModel struct {
15 | help help.Model
16 | inputs []textinput.Model
17 | focusIndex int
18 | showConfirmInput bool
19 | }
20 |
21 | func newCreateModel() *createModel {
22 | ti := textinput.New()
23 | ti.Placeholder = "Try feature..."
24 |
25 | ci := textinput.New()
26 | ci.CharLimit = 1
27 |
28 | return &createModel{
29 | help: help.New(),
30 | inputs: []textinput.Model{ti, ci},
31 | showConfirmInput: false,
32 | }
33 | }
34 |
35 | func (m *createModel) prevFocus() tea.Cmd {
36 | m.inputs[m.focusIndex].Blur()
37 | m.focusIndex--
38 |
39 | if m.focusIndex < 0 {
40 | m.focusIndex = len(m.inputs) - 1
41 | }
42 |
43 | return m.inputs[m.focusIndex].Focus()
44 | }
45 |
46 | func (m *createModel) nextFocus() tea.Cmd {
47 | m.inputs[m.focusIndex].Blur()
48 | m.focusIndex++
49 |
50 | if m.focusIndex > len(m.inputs)-1 {
51 | m.focusIndex = 0
52 | }
53 |
54 | return m.inputs[m.focusIndex].Focus()
55 | }
56 |
57 | func createUpdate(msg tea.Msg, m Model) (tea.Model, tea.Cmd) {
58 | switch msg := msg.(type) {
59 | case tea.KeyMsg:
60 | switch {
61 | case key.Matches(msg, key.NewBinding(key.WithKeys("shift+tab", "up"))):
62 | cmd := m.create.prevFocus()
63 |
64 | return m, cmd
65 |
66 | case key.Matches(msg, key.NewBinding(key.WithKeys("tab", "down"))):
67 | cmd := m.create.nextFocus()
68 |
69 | return m, cmd
70 |
71 | case key.Matches(msg, m.keyMap.Enter):
72 | if !m.create.showConfirmInput {
73 | m.create.inputs[0].Blur()
74 | m.create.showConfirmInput = true
75 | m.create.inputs[1].Focus()
76 |
77 | return m, nil
78 | }
79 |
80 | switch m.create.inputs[1].Value() {
81 | case "y", "Y", "":
82 | out := git.CreateBranch(m.create.inputs[0].Value())
83 |
84 | fmt.Println(m.styles.NormalTitle.Copy().MarginTop(1).Render(out))
85 |
86 | return m, tea.Quit
87 |
88 | case "n", "N":
89 | m.create.inputs[0].Reset()
90 | m.create.inputs[1].Reset()
91 | m.create.showConfirmInput = false
92 | m.state = browsing
93 | m.keyMap.State = "browsing"
94 | m.updateKeybindins()
95 |
96 | return m, nil
97 |
98 | default:
99 | m.create.inputs[1].SetValue("")
100 | }
101 |
102 | case key.Matches(msg, m.keyMap.Cancel):
103 | m.create.inputs[0].Reset()
104 | m.create.inputs[1].Reset()
105 | m.create.showConfirmInput = false
106 | m.state = browsing
107 | m.updateKeybindins()
108 | }
109 |
110 | case tea.WindowSizeMsg:
111 | m.create.help.Width = msg.Width
112 | }
113 |
114 | cmds := make([]tea.Cmd, len(m.create.inputs))
115 |
116 | for i := range m.create.inputs {
117 | m.create.inputs[i], cmds[i] = m.create.inputs[i].Update(msg)
118 | }
119 |
120 | return m, tea.Batch(cmds...)
121 | }
122 |
123 | func (m Model) createView() string {
124 | title := m.styles.Title.MarginLeft(2).Render("Type name of the new branch")
125 | textInput := lipgloss.NewStyle().MarginLeft(4).Render(m.create.inputs[0].View())
126 | help := lipgloss.NewStyle().MarginLeft(4).Render(m.create.help.View(m.keyMap))
127 |
128 | if m.create.showConfirmInput {
129 | branch := lipgloss.NewStyle().
130 | Foreground(lipgloss.AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"}).
131 | Render(m.create.inputs[0].Value())
132 |
133 | confirmInput := lipgloss.NewStyle().
134 | MarginLeft(4).
135 | Render(lipgloss.JoinHorizontal(
136 | lipgloss.Left,
137 | fmt.Sprintf("Create new branch \"%s\"? [Y/n]", branch),
138 | m.create.inputs[1].View(),
139 | ))
140 |
141 | return lipgloss.NewStyle().
142 | MarginTop(1).
143 | Render(lipgloss.JoinVertical(lipgloss.Left, title, "\n", textInput, "\n", confirmInput, "\n", help))
144 | }
145 |
146 | return lipgloss.NewStyle().
147 | MarginTop(1).
148 | Render(lipgloss.JoinVertical(lipgloss.Left, title, "\n", textInput, "\n", help))
149 | }
150 |
--------------------------------------------------------------------------------
/git/git.go:
--------------------------------------------------------------------------------
1 | package git
2 |
3 | import (
4 | "os/exec"
5 | "strings"
6 | )
7 |
8 | type Branch struct {
9 | Name string
10 | AuthorName string
11 | CommitterDate string
12 | IsRemote bool
13 | }
14 |
15 | const FORMAT = `branch:%(refname:short)%(if)%(HEAD)%(then)*%(end)
16 | authorname:%(authorname)
17 | committerdate:%(committerdate:relative)
18 | `
19 |
20 | func getLocalBranches(branches []Branch) ([]Branch, error) {
21 | cmd := exec.Command(
22 | "git",
23 | "for-each-ref",
24 | "refs/heads",
25 | "--sort",
26 | "-committerdate",
27 | "--sort",
28 | "-upstream",
29 | "--format",
30 | FORMAT,
31 | )
32 |
33 | out, err := cmd.CombinedOutput()
34 | if err != nil {
35 | return []Branch{}, err
36 | }
37 |
38 | s := strings.Split(strings.TrimSpace(string(out)), "\n\n")
39 |
40 | for _, branch := range s {
41 | fields := strings.Split(branch, "\n")
42 |
43 | branch := strings.TrimPrefix(fields[0], "branch:")
44 | authorname := strings.TrimPrefix(fields[1], "authorname:")
45 | committerdate := strings.TrimPrefix(fields[2], "committerdate:")
46 | branches = append(branches, Branch{
47 | Name: strings.TrimSpace(branch),
48 | AuthorName: strings.TrimSpace(authorname),
49 | CommitterDate: strings.TrimSpace(committerdate),
50 | IsRemote: false,
51 | })
52 | }
53 |
54 | return branches, err
55 | }
56 |
57 | func getRemoteBranches(branches []Branch) ([]Branch, error) {
58 | cmd := exec.Command(
59 | "git",
60 | "for-each-ref",
61 | "refs/remotes",
62 | "--sort",
63 | "-committerdate",
64 | "--sort",
65 | "-upstream",
66 | "--format",
67 | FORMAT,
68 | )
69 |
70 | out, err := cmd.CombinedOutput()
71 | if err != nil {
72 | return []Branch{}, err
73 | }
74 |
75 | s := strings.Split(strings.TrimSpace(string(out)), "\n\n")
76 |
77 | for _, branch := range s {
78 | fields := strings.Split(branch, "\n")
79 |
80 | branch := strings.TrimPrefix(fields[0], "branch:")
81 | authorname := strings.TrimPrefix(fields[1], "authorname:")
82 | committerdate := strings.TrimPrefix(fields[2], "committerdate:")
83 | branches = append(branches, Branch{
84 | Name: strings.TrimSpace(branch),
85 | AuthorName: strings.TrimSpace(authorname),
86 | CommitterDate: strings.TrimSpace(committerdate),
87 | IsRemote: true,
88 | })
89 | }
90 |
91 | return branches, err
92 | }
93 |
94 | func GetAllBranches() (branches []Branch, err error) {
95 | branches, err = getLocalBranches(branches)
96 | if err != nil {
97 | return
98 | }
99 |
100 | branches, err = getRemoteBranches(branches)
101 | if err != nil {
102 | return
103 | }
104 |
105 | return
106 | }
107 |
108 | func CheckoutBranch(branch string) string {
109 | cmd := exec.Command("git", "checkout", branch)
110 |
111 | out, _ := cmd.CombinedOutput()
112 |
113 | return string(out)
114 | }
115 |
116 | func CreateBranch(branch string) string {
117 | cmd := exec.Command("git", "checkout", "-b", branch)
118 |
119 | out, _ := cmd.CombinedOutput()
120 |
121 | return string(out)
122 | }
123 |
124 | func DeleteBranch(branch string) string {
125 | cmd := exec.Command("git", "branch", "-D", branch)
126 |
127 | out, _ := cmd.CombinedOutput()
128 |
129 | return string(out)
130 | }
131 |
132 | func TrackBranch(branch string) string {
133 | cmd := exec.Command("git", "checkout", "--track", branch)
134 |
135 | out, _ := cmd.CombinedOutput()
136 |
137 | return string(out)
138 | }
139 |
140 | func MergeBranch(branch string) string {
141 | cmd := exec.Command("git", "merge", branch)
142 |
143 | out, _ := cmd.CombinedOutput()
144 |
145 | return string(out)
146 | }
147 |
148 | func RebaseBranch(branch string) string {
149 | cmd := exec.Command("git", "rebase", branch)
150 |
151 | out, _ := cmd.CombinedOutput()
152 |
153 | return string(out)
154 | }
155 |
156 | func RenameBranch(oldName, newName string) string {
157 | cmd := exec.Command("git", "branch", "-m", oldName, newName)
158 |
159 | out, _ := cmd.CombinedOutput()
160 |
161 | return string(out)
162 | }
163 |
164 | func RenameRemoteBranch(oldName, newName string) string {
165 | exec.Command("git", "push", "origin", "--delete", oldName)
166 |
167 | cmd := exec.Command("git", "push", "origin", "-u", newName)
168 | out, _ := cmd.CombinedOutput()
169 |
170 | return string(out)
171 | }
172 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
2 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
3 | github.com/charmbracelet/bubbles v0.10.3 h1:fKarbRaObLn/DCsZO4Y3vKCwRUzynQD9L+gGev1E/ho=
4 | github.com/charmbracelet/bubbles v0.10.3/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA=
5 | github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA=
6 | github.com/charmbracelet/bubbletea v0.20.0 h1:/b8LEPgCbNr7WWZ2LuE/BV1/r4t5PyYJtDb+J3vpwxc=
7 | github.com/charmbracelet/bubbletea v0.20.0/go.mod h1:zpkze1Rioo4rJELjRyGlm9T2YNou1Fm4LIJQSa5QMEM=
8 | github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
9 | github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM=
10 | github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
11 | github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
12 | github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
13 | github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
14 | github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
15 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
16 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
17 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
18 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
19 | github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
20 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
21 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
22 | github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
23 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
24 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
25 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
26 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
27 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
28 | github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
29 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
30 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
31 | github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
32 | github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
33 | github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=
34 | github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
35 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
36 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
37 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
38 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
39 | github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
40 | github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
41 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
42 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
43 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
44 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
45 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
46 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
47 | golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912 h1:uCLL3g5wH2xjxVREVuAbP9JM5PPKjRbXKRa6IBjkzmU=
48 | golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
49 | golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
50 | golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
51 | golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
52 |
--------------------------------------------------------------------------------
/tui/tui.go:
--------------------------------------------------------------------------------
1 | package tui
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "strings"
7 |
8 | "github.com/charmbracelet/bubbles/key"
9 | "github.com/charmbracelet/bubbles/list"
10 | tea "github.com/charmbracelet/bubbletea"
11 | "github.com/joaom00/gh-b/git"
12 | "github.com/joaom00/gh-b/tui/keys"
13 | "github.com/joaom00/gh-b/tui/styles"
14 | )
15 |
16 | const (
17 | defaultWidth = 20
18 | listHeight = 15
19 | )
20 |
21 | type item git.Branch
22 |
23 | func (i item) FilterValue() string { return i.Name }
24 |
25 | type state int
26 |
27 | const (
28 | browsing state = iota
29 | creating
30 | deleting
31 | merge
32 | rebasing
33 | renaming
34 | )
35 |
36 | type Model struct {
37 | create *createModel
38 | delete *deleteModel
39 | merge *mergeModel
40 | rebase *rebaseModel
41 | rename *renameModel
42 | keyMap *keys.KeyMap
43 | list list.Model
44 | styles styles.Styles
45 | state state
46 | }
47 |
48 | func NewModel() Model {
49 | branches, err := git.GetAllBranches()
50 | if err != nil {
51 | log.Fatal("not a git repository", err)
52 | }
53 |
54 | items := []list.Item{}
55 | for _, b := range branches {
56 | items = append(items, item{
57 | Name: b.Name,
58 | AuthorName: b.AuthorName,
59 | CommitterDate: b.CommitterDate,
60 | IsRemote: b.IsRemote,
61 | })
62 | }
63 |
64 | styles := styles.DefaultStyles()
65 | keys := keys.NewKeyMap()
66 |
67 | l := list.New(items, newItemDelegate(keys, &styles), defaultWidth, listHeight)
68 | l.Title = "Your Branches"
69 | l.SetShowStatusBar(false)
70 | l.Styles.PaginationStyle = styles.Pagination
71 | l.Styles.HelpStyle = styles.Help
72 |
73 | return Model{
74 | create: newCreateModel(),
75 | delete: newDeleteModel(),
76 | merge: newMergeModel(),
77 | rebase: newRebaseModel(),
78 | rename: newRenameModel(),
79 | keyMap: keys,
80 | list: l,
81 | styles: styles,
82 | state: browsing,
83 | }
84 | }
85 |
86 | func (m Model) selectedItem() (item, bool) {
87 | i, ok := m.list.SelectedItem().(item)
88 |
89 | i.Name = strings.TrimSuffix(i.Name, "*")
90 |
91 | return i, ok
92 | }
93 |
94 | func (m *Model) updateListItem() {
95 | branches, err := git.GetAllBranches()
96 | if err != nil {
97 | log.Fatal(err)
98 | }
99 |
100 | items := []list.Item{}
101 | for _, b := range branches {
102 | items = append(items, item{
103 | Name: b.Name,
104 | AuthorName: b.AuthorName,
105 | CommitterDate: b.CommitterDate,
106 | IsRemote: b.IsRemote,
107 | })
108 | }
109 |
110 | m.list.SetItems(items)
111 | }
112 |
113 | func (m *Model) updateKeybindins() {
114 | if m.list.SettingFilter() {
115 | m.keyMap.Enter.SetEnabled(false)
116 | }
117 |
118 | switch m.state {
119 | case creating, deleting, merge, rebasing, renaming:
120 | m.keyMap.Enter.SetEnabled(true)
121 | m.keyMap.Cancel.SetEnabled(true)
122 |
123 | m.keyMap.Quit.SetEnabled(false)
124 | m.keyMap.Delete.SetEnabled(false)
125 | m.keyMap.Track.SetEnabled(false)
126 | m.keyMap.Merge.SetEnabled(false)
127 | m.keyMap.Rebase.SetEnabled(false)
128 |
129 | m.list.KeyMap.AcceptWhileFiltering.SetEnabled(false)
130 | m.list.KeyMap.CancelWhileFiltering.SetEnabled(false)
131 | case browsing:
132 | m.keyMap.Enter.SetEnabled(true)
133 | m.keyMap.Create.SetEnabled(true)
134 | m.keyMap.Delete.SetEnabled(true)
135 | m.keyMap.Merge.SetEnabled(true)
136 | m.keyMap.Rebase.SetEnabled(true)
137 | m.keyMap.Track.SetEnabled(true)
138 |
139 | m.keyMap.Cancel.SetEnabled(false)
140 |
141 | default:
142 | m.keyMap.Enter.SetEnabled(true)
143 | m.keyMap.Create.SetEnabled(true)
144 | m.keyMap.Delete.SetEnabled(true)
145 | m.keyMap.Merge.SetEnabled(true)
146 | m.keyMap.Rebase.SetEnabled(true)
147 | m.keyMap.Track.SetEnabled(true)
148 |
149 | m.keyMap.Cancel.SetEnabled(false)
150 | }
151 | }
152 |
153 | func listUpdate(msg tea.Msg, m Model) (tea.Model, tea.Cmd) {
154 | switch msg := msg.(type) {
155 | case tea.WindowSizeMsg:
156 | m.list.SetWidth(msg.Width)
157 | return m, nil
158 |
159 | case tea.KeyMsg:
160 | switch {
161 | case key.Matches(msg, m.list.KeyMap.AcceptWhileFiltering):
162 | m.state = browsing
163 | m.updateKeybindins()
164 |
165 | case key.Matches(msg, m.keyMap.CursorUp):
166 | m.list.CursorUp()
167 |
168 | case key.Matches(msg, m.keyMap.CursorDown):
169 | m.list.CursorDown()
170 |
171 | case key.Matches(msg, m.keyMap.Create):
172 | m.state = creating
173 | m.keyMap.State = "creating"
174 | m.create.inputs[0].Focus()
175 | m.updateKeybindins()
176 |
177 | case key.Matches(msg, m.keyMap.Delete):
178 | m.state = deleting
179 | m.keyMap.State = "deleting"
180 | m.delete.confirmInput.Focus()
181 | m.updateKeybindins()
182 |
183 | case key.Matches(msg, m.keyMap.Track):
184 | if i, ok := m.list.SelectedItem().(item); ok {
185 | i.Name = strings.TrimSuffix(i.Name, "*")
186 | out := git.TrackBranch(i.Name)
187 |
188 | fmt.Println(m.styles.NormalTitle.Render(out))
189 |
190 | return m, tea.Quit
191 | }
192 |
193 | case key.Matches(msg, m.keyMap.Merge):
194 | m.state = merge
195 | m.keyMap.State = "merge"
196 | m.merge.confirmInput.Focus()
197 | m.updateKeybindins()
198 |
199 | case key.Matches(msg, m.keyMap.Rebase):
200 | m.state = rebasing
201 | m.keyMap.State = "rebasing"
202 | m.rebase.confirmInput.Focus()
203 | m.updateKeybindins()
204 |
205 | case key.Matches(msg, m.keyMap.Rename):
206 | i, ok := m.selectedItem()
207 | if !ok {
208 | return m, nil
209 | }
210 |
211 | if i.IsRemote {
212 | m.list.NewStatusMessage("We don't support renaming remote branch")
213 | return m, nil
214 | }
215 |
216 | m.state = renaming
217 | m.keyMap.State = "renaming"
218 | m.rename.input.Focus()
219 | m.updateKeybindins()
220 |
221 | case key.Matches(msg, m.keyMap.Enter):
222 | if i, ok := m.list.SelectedItem().(item); ok {
223 | i.Name = strings.TrimSuffix(i.Name, "*")
224 | out := git.CheckoutBranch(i.Name)
225 |
226 | fmt.Println(m.styles.NormalTitle.Copy().MarginTop(1).Render(out))
227 |
228 | return m, tea.Quit
229 | }
230 |
231 | }
232 | }
233 |
234 | var (
235 | cmds []tea.Cmd
236 | cmd tea.Cmd
237 | )
238 | m.list, cmd = m.list.Update(msg)
239 | cmds = append(cmds, cmd)
240 |
241 | return m, tea.Batch(cmds...)
242 | }
243 |
244 | func (m Model) Init() tea.Cmd {
245 | return nil
246 | }
247 |
248 | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
249 | if m.list.SettingFilter() {
250 | m.keyMap.Enter.SetEnabled(false)
251 | }
252 |
253 | switch m.state {
254 | case browsing:
255 | return listUpdate(msg, m)
256 |
257 | case creating:
258 | return createUpdate(msg, m)
259 |
260 | case deleting:
261 | return deleteUpdate(msg, m)
262 |
263 | case merge:
264 | return mergeUpdate(msg, m)
265 |
266 | case rebasing:
267 | return rebaseUpdate(msg, m)
268 |
269 | case renaming:
270 | return renameUpdate(msg, m)
271 |
272 | default:
273 | return m, nil
274 | }
275 | }
276 |
277 | func (m Model) View() string {
278 | switch m.state {
279 | case browsing:
280 | return "\n" + m.list.View()
281 |
282 | case creating:
283 | return m.createView()
284 |
285 | case deleting:
286 | return m.deleteView()
287 |
288 | case merge:
289 | return m.mergeView()
290 |
291 | case rebasing:
292 | return m.rebaseView()
293 |
294 | case renaming:
295 | return m.renameView()
296 |
297 | default:
298 | return ""
299 | }
300 | }
301 |
--------------------------------------------------------------------------------