Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions internal/ui/mode_branch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package ui

import (
"strings"

"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
)

// Branch picker/create mode state transitions and actions.

func (m Model) activeBranches() []string {
if m.filteredBranches != nil {
return m.filteredBranches
}
return m.branches
}

func filterBranches(branches []string, query string) []string {
if query == "" {
return nil
}
q := strings.ToLower(query)
out := []string{}
for _, b := range branches {
if strings.Contains(strings.ToLower(b), q) {
out = append(out, b)
}
}
return out
}

func (m Model) enterBranchMode() (tea.Model, tea.Cmd) {
repo := m.repo
return m, func() tea.Msg {
branches, err := repo.ListBranches()
if err != nil {
return branchesLoadedMsg{err: err}
}
current := repo.BranchName()
return branchesLoadedMsg{branches: branches, current: current}
}
}

func (m Model) updateBranchMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
if m.branchCreating {
return m.updateBranchCreateMode(msg)
}
switch msg.String() {
case "ctrl+n":
m.branchCreating = true
m.branchInput.Reset()
m.branchInput.Focus()
m.branchFilter.Blur()
return m, textinput.Blink
case "esc":
if m.branchFilter.Value() != "" {
m.branchFilter.Reset()
m.filteredBranches = nil
m.branchCursor = 0
m.branchOffset = 0
return m, nil
}
m.mode = modeFileList
m.branchFilter.Blur()
return m, nil
case "ctrl+c":
return m, tea.Quit
case "up", "ctrl+k":
if m.branchCursor > 0 {
m.branchCursor--
}
m = m.clampBranchScroll()
return m, nil
case "down", "ctrl+j":
list := m.activeBranches()
if m.branchCursor < len(list)-1 {
m.branchCursor++
}
m = m.clampBranchScroll()
return m, nil
case "enter":
list := m.activeBranches()
if m.branchCursor >= len(list) || len(list) == 0 {
return m, nil
}
selected := list[m.branchCursor]
m.branchFilter.Blur()
if selected == m.currentBranch {
m.mode = modeFileList
return m, nil
}
repo := m.repo
return m, func() tea.Msg {
return branchSwitchedMsg{err: repo.CheckoutBranch(selected)}
}
}
prevVal := m.branchFilter.Value()
var cmd tea.Cmd
m.branchFilter, cmd = m.branchFilter.Update(msg)
if m.branchFilter.Value() != prevVal {
m.filteredBranches = filterBranches(m.branches, m.branchFilter.Value())
m.branchCursor = 0
m.branchOffset = 0
}
return m, cmd
}

func (m Model) updateBranchCreateMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "esc", "ctrl+c":
m.branchCreating = false
m.branchInput.Reset()
m.branchFilter.Focus()
return m, nil
case "enter":
name := strings.TrimSpace(m.branchInput.Value())
if name == "" {
m.statusMsg = "empty branch name"
return m, nil
}
return m, m.createBranchCmd(name)
}
var cmd tea.Cmd
m.branchInput, cmd = m.branchInput.Update(msg)
return m, cmd
}

func (m Model) clampBranchScroll() Model {
h := m.contentHeight() - 1
if h <= 0 {
return m
}
if m.branchCursor < m.branchOffset {
m.branchOffset = m.branchCursor
} else if m.branchCursor >= m.branchOffset+h {
m.branchOffset = m.branchCursor - h + 1
}
return m
}

func (m Model) createBranchCmd(name string) tea.Cmd {
repo := m.repo
return func() tea.Msg {
if err := repo.CreateBranch(name); err != nil {
return branchCreatedMsg{name: name, err: err}
}
err := repo.CheckoutBranch(name)
return branchCreatedMsg{name: name, err: err}
}
}
36 changes: 36 additions & 0 deletions internal/ui/mode_diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package ui

import tea "github.com/charmbracelet/bubbletea"

// Diff mode key handling and viewport delegation.

func (m Model) updateDiffMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "esc", "h", "left":
m.mode = modeFileList
return m, nil
case "n":
return m.nextFile()
case "p":
return m.prevFile()
case "e":
if m.cursor < len(m.files) {
m.SelectedFile = m.files[m.cursor].change.Path
}
return m, tea.Quit
case "b":
return m.enterBranchMode()
case "tab":
return m.toggleStage()
case "v":
m.splitDiff = !m.splitDiff
m.prevCurs = -1
m.lastDiffContent = ""
return m, tea.Batch(m.loadDiffCmd(true), m.saveSplitPrefCmd())
}
var cmd tea.Cmd
m.viewport, cmd = m.viewport.Update(msg)
return m, cmd
}
100 changes: 100 additions & 0 deletions internal/ui/mode_filelist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package ui

import tea "github.com/charmbracelet/bubbletea"

// File-list mode input handling and file navigation actions.

func (m Model) updateFileListMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.statusMsg = ""
if msg.String() == "P" {
if m.pushConfirm {
m.pushConfirm = false
m.statusMsg = "pushing..."
if m.upstream.Upstream == "" {
return m, m.pushSetUpstreamCmd()
}
return m, m.pushCmd()
}
if m.upstream.Upstream == "" {
branch := m.currentBranch
if branch == "" {
branch = m.repo.BranchName()
}
m.pushConfirm = true
m.statusMsg = "press P again to push --set-upstream origin " + branch
return m, nil
}
m.pushConfirm = true
m.statusMsg = "press P again to push to " + m.upstream.Upstream
return m, nil
}
m.pushConfirm = false

switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "j", "down":
if m.cursor < len(m.files)-1 {
m.cursor++
}
case "k", "up":
if m.cursor > 0 {
m.cursor--
}
case "g":
m.cursor = 0
case "G":
m.cursor = max(0, len(m.files)-1)
case "enter", "l", "right":
m.mode = modeDiff
return m, nil
case "e":
if m.cursor < len(m.files) {
m.SelectedFile = m.files[m.cursor].change.Path
}
return m, tea.Quit
case "tab":
return m.toggleStage()
case "a":
return m.stageAll()
case "c":
return m.enterCommitMode()
case "b":
return m.enterBranchMode()
case "v":
m.splitDiff = !m.splitDiff
m.prevCurs = -1
m.lastDiffContent = ""
return m, tea.Batch(m.loadDiffCmd(true), m.saveSplitPrefCmd())
case "F":
if m.upstream.Upstream == "" {
m.statusMsg = "no upstream configured"
return m, nil
}
m.statusMsg = "pulling..."
return m, m.pullCmd()
}
if m.cursor != m.prevCurs {
m.prevCurs = m.cursor
return m, m.loadDiffCmd(true)
}
return m, nil
}

func (m Model) nextFile() (tea.Model, tea.Cmd) {
if m.cursor < len(m.files)-1 {
m.cursor++
m.prevCurs = m.cursor
return m, m.loadDiffCmd(true)
}
return m, nil
}

func (m Model) prevFile() (tea.Model, tea.Cmd) {
if m.cursor > 0 {
m.cursor--
m.prevCurs = m.cursor
return m, m.loadDiffCmd(true)
}
return m, nil
}
Loading