├── .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 | [![asciicast](https://asciinema.org/a/472292.svg)](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 | --------------------------------------------------------------------------------