diff --git a/go.mod b/go.mod index 00ccc17..dd3874a 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss v1.0.0 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 - github.com/termkit/skeleton v0.1.3 + github.com/termkit/skeleton v0.2.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 70954ca..a971264 100644 --- a/go.sum +++ b/go.sum @@ -82,6 +82,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/termkit/skeleton v0.1.3 h1:XiCqLDQXhtU4LhrgDKYHaPFRumngLPs9ssebL8WXHVI= github.com/termkit/skeleton v0.1.3/go.mod h1:KjHXehkpVm8i3pli9PTG+Lat2PrUBsw6QQe5kbgYXHs= +github.com/termkit/skeleton v0.2.0 h1:Nbs7i5+vsouK25Gl4+vuMB1/7jokZ77HN2wV1IYgpBQ= +github.com/termkit/skeleton v0.2.0/go.mod h1:KjHXehkpVm8i3pli9PTG+Lat2PrUBsw6QQe5kbgYXHs= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e h1:4qufH0hlUYs6AO6XmZC3GqfDPGSXHVXUFR6OND+iJX4= diff --git a/internal/github/usecase/ports.go b/internal/github/usecase/ports.go index a0f706f..b825753 100644 --- a/internal/github/usecase/ports.go +++ b/internal/github/usecase/ports.go @@ -7,6 +7,7 @@ import ( type UseCase interface { GetAuthUser(ctx context.Context) (*GetAuthUserOutput, error) ListRepositories(ctx context.Context, input ListRepositoriesInput) (*ListRepositoriesOutput, error) + GetRepositoryBranches(ctx context.Context, input GetRepositoryBranchesInput) (*GetRepositoryBranchesOutput, error) GetWorkflowHistory(ctx context.Context, input GetWorkflowHistoryInput) (*GetWorkflowHistoryOutput, error) GetTriggerableWorkflows(ctx context.Context, input GetTriggerableWorkflowsInput) (*GetTriggerableWorkflowsOutput, error) InspectWorkflow(ctx context.Context, input InspectWorkflowInput) (*InspectWorkflowOutput, error) diff --git a/internal/github/usecase/types.go b/internal/github/usecase/types.go index 84cc7f8..b398a8e 100644 --- a/internal/github/usecase/types.go +++ b/internal/github/usecase/types.go @@ -31,6 +31,23 @@ type ListRepositoriesOutput struct { Repositories []GithubRepository } +// ------------------------------------------------------------ + +type GetRepositoryBranchesInput struct { + Repository string +} + +type GetRepositoryBranchesOutput struct { + Branches []GithubBranch +} + +type GithubBranch struct { + Name string + IsDefault bool +} + +// ------------------------------------------------------------ + type GithubRepository struct { Name string Private bool diff --git a/internal/github/usecase/usecase.go b/internal/github/usecase/usecase.go index 2c78943..f278a7a 100644 --- a/internal/github/usecase/usecase.go +++ b/internal/github/usecase/usecase.go @@ -75,6 +75,42 @@ func (u useCase) ListRepositories(ctx context.Context, input ListRepositoriesInp }, errors.Join(resultErrs...) } +func (u useCase) GetRepositoryBranches(ctx context.Context, input GetRepositoryBranchesInput) (*GetRepositoryBranchesOutput, error) { + // Get Repository to get the default branch + repository, err := u.githubRepository.GetRepository(ctx, input.Repository) + if err != nil { + return nil, err + } + + var mainBranch = repository.DefaultBranch + + branches, err := u.githubRepository.ListBranches(ctx, input.Repository) + if err != nil { + return nil, err + } + + if len(branches) == 0 { + return &GetRepositoryBranchesOutput{}, nil + } + + var result = []GithubBranch{ + { + Name: mainBranch, + IsDefault: true, + }, + } + + for _, branch := range branches { + result = append(result, GithubBranch{ + Name: branch.Name, + }) + } + + return &GetRepositoryBranchesOutput{ + Branches: result, + }, nil +} + func (u useCase) workerListRepositories(ctx context.Context, repository gr.GithubRepository, results chan<- GithubRepository, errs chan<- error) { getWorkflows, err := u.githubRepository.GetWorkflows(ctx, repository.FullName) if err != nil { diff --git a/internal/terminal/handler/ghrepository.go b/internal/terminal/handler/ghrepository.go index 4b9de66..d4c63e4 100644 --- a/internal/terminal/handler/ghrepository.go +++ b/internal/terminal/handler/ghrepository.go @@ -274,6 +274,18 @@ func (m *ModelGithubRepository) handleTableInputs(_ context.Context) { if len(selectedRow) > 0 && selectedRow[0] != "" { m.selectedRepository.RepositoryName = selectedRow[0] m.selectedRepository.BranchName = selectedRow[1] + + workflowCount := selectedRow[3] + if workflowCount != "" { + count, _ := strconv.Atoi(workflowCount) + if count == 0 { + m.skeleton.LockTab("workflow") + m.skeleton.LockTab("trigger") + } else { + m.skeleton.UnlockTab("workflow") + m.skeleton.UnlockTab("trigger") + } + } } } diff --git a/internal/terminal/handler/ghworkflow.go b/internal/terminal/handler/ghworkflow.go index d7fbd94..299b3b9 100644 --- a/internal/terminal/handler/ghworkflow.go +++ b/internal/terminal/handler/ghworkflow.go @@ -4,14 +4,14 @@ import ( "context" "errors" "fmt" - "github.com/termkit/skeleton" - "sort" - "strings" - "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/table" + "github.com/charmbracelet/bubbles/textinput" "github.com/termkit/gama/internal/terminal/handler/status" hdltypes "github.com/termkit/gama/internal/terminal/handler/types" + "github.com/termkit/skeleton" + "sort" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -25,6 +25,7 @@ type ModelGithubWorkflow struct { cancelSyncTriggerableWorkflows context.CancelFunc tableReady bool lastRepository string + mainBranch string // shared properties selectedRepository *hdltypes.SelectedRepository @@ -39,6 +40,7 @@ type ModelGithubWorkflow struct { help help.Model tableTriggerableWorkflow table.Model status *status.ModelStatus + textInput textinput.Model } func SetupModelGithubWorkflow(sk *skeleton.Skeleton, githubUseCase gu.UseCase) *ModelGithubWorkflow { @@ -63,6 +65,21 @@ func SetupModelGithubWorkflow(sk *skeleton.Skeleton, githubUseCase gu.UseCase) * Bold(false) tableTriggerableWorkflow.SetStyles(s) + tableTriggerableWorkflow.KeyMap = table.KeyMap{ + LineUp: key.NewBinding( + key.WithKeys("up"), + ), + LineDown: key.NewBinding( + key.WithKeys("down"), + ), + } + + ti := textinput.New() + ti.Focus() + ti.CharLimit = 128 + ti.Placeholder = "Type to switch branch" + ti.ShowSuggestions = true + modelStatus := status.SetupModelStatus(sk) return &ModelGithubWorkflow{ @@ -75,6 +92,7 @@ func SetupModelGithubWorkflow(sk *skeleton.Skeleton, githubUseCase gu.UseCase) * selectedRepository: hdltypes.NewSelectedRepository(), syncTriggerableWorkflowsContext: context.Background(), cancelSyncTriggerableWorkflows: func() {}, + textInput: ti, } } @@ -92,10 +110,38 @@ func (m *ModelGithubWorkflow) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.syncTriggerableWorkflowsContext, m.cancelSyncTriggerableWorkflows = context.WithCancel(context.Background()) m.lastRepository = m.selectedRepository.RepositoryName + m.mainBranch = m.selectedRepository.BranchName go m.syncTriggerableWorkflows(m.syncTriggerableWorkflowsContext) + go m.syncBranches(m.syncTriggerableWorkflowsContext) + } + + var selectedBranch = m.textInput.Value() + if selectedBranch != "" { + var isBranchExist bool + for _, branch := range m.textInput.AvailableSuggestions() { + if branch == selectedBranch { + isBranchExist = true + m.selectedRepository.BranchName = selectedBranch + break + } + } + + if !isBranchExist { + m.status.SetErrorMessage(fmt.Sprintf("Branch %s is not exist", selectedBranch)) + m.skeleton.LockTabsToTheRight() + } else { + m.skeleton.UnlockTabs() + } + } + if selectedBranch == "" { + m.selectedRepository.BranchName = m.mainBranch + m.skeleton.UnlockTabs() } + m.textInput, cmd = m.textInput.Update(msg) + cmds = append(cmds, cmd) + m.tableTriggerableWorkflow, cmd = m.tableTriggerableWorkflow.Update(msg) cmds = append(cmds, cmd) @@ -127,11 +173,28 @@ func (m *ModelGithubWorkflow) View() string { m.tableTriggerableWorkflow.SetHeight(termHeight - 17) } - doc := strings.Builder{} - doc.WriteString(style.Render(m.tableTriggerableWorkflow.View())) - doc.WriteString("\n\n\n") + return lipgloss.JoinVertical(lipgloss.Top, + style.Render(m.tableTriggerableWorkflow.View()), + m.viewSearchBar(), + m.status.View(), + helpWindowStyle.Render(m.ViewHelp()), + ) +} + +func (m *ModelGithubWorkflow) viewSearchBar() string { + // Define window style + windowStyle := lipgloss.NewStyle(). + Border(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#3b698f")). + Padding(0, 1). + Width(m.skeleton.GetTerminalWidth() - 6).MarginLeft(1) + + if len(m.textInput.AvailableSuggestions()) > 0 && m.textInput.Value() == "" { + var mainBranch = m.mainBranch + m.textInput.Placeholder = "Type to switch branch (default: " + mainBranch + ")" + } - return lipgloss.JoinVertical(lipgloss.Top, doc.String(), m.status.View(), helpWindowStyle.Render(m.ViewHelp())) + return windowStyle.Render(m.textInput.View()) } func (m *ModelGithubWorkflow) syncTriggerableWorkflows(ctx context.Context) { @@ -175,10 +238,47 @@ func (m *ModelGithubWorkflow) syncTriggerableWorkflows(ctx context.Context) { m.tableTriggerableWorkflow.SetRows(tableRowsTriggerableWorkflow) + if len(tableRowsTriggerableWorkflow) > 0 { + m.tableTriggerableWorkflow.SetCursor(0) + } + m.tableReady = true m.status.SetSuccessMessage(fmt.Sprintf("[%s@%s] Triggerable workflows fetched.", m.selectedRepository.RepositoryName, m.selectedRepository.BranchName)) } +func (m *ModelGithubWorkflow) syncBranches(ctx context.Context) { + defer m.skeleton.TriggerUpdate() + + m.status.Reset() + m.status.SetProgressMessage(fmt.Sprintf("[%s] Fetching branches...", m.selectedRepository.RepositoryName)) + + branches, err := m.github.GetRepositoryBranches(ctx, gu.GetRepositoryBranchesInput{ + Repository: m.selectedRepository.RepositoryName, + }) + if errors.Is(err, context.Canceled) { + return + } else if err != nil { + m.status.SetError(err) + m.status.SetErrorMessage("Branches cannot be listed") + return + } + + if branches == nil || len(branches.Branches) == 0 { + m.selectedRepository.BranchName = "" + m.status.SetDefaultMessage(fmt.Sprintf("[%s] No branches found.", m.selectedRepository.RepositoryName)) + return + } + + var bs = make([]string, len(branches.Branches)) + for i, branch := range branches.Branches { + bs[i] = branch.Name + } + + m.textInput.SetSuggestions(bs) + + m.status.SetSuccessMessage(fmt.Sprintf("[%s] Branches fetched.", m.selectedRepository.RepositoryName)) +} + func (m *ModelGithubWorkflow) handleTableInputs(_ context.Context) { if !m.tableReady { return diff --git a/internal/terminal/handler/ghworkflowhistory.go b/internal/terminal/handler/ghworkflowhistory.go index 065ea63..38a930a 100644 --- a/internal/terminal/handler/ghworkflowhistory.go +++ b/internal/terminal/handler/ghworkflowhistory.go @@ -307,7 +307,7 @@ func (m *ModelGithubWorkflowHistory) syncWorkflowHistory(ctx context.Context) { if len(workflowHistory.Workflows) == 0 { m.modelTabOptions.SetStatus(taboptions.OptionNone) - m.status.SetDefaultMessage(fmt.Sprintf("[%s@%s] No workflows found.", m.selectedRepository.RepositoryName, m.selectedRepository.BranchName)) + m.status.SetDefaultMessage(fmt.Sprintf("[%s@%s] No workflow history found.", m.selectedRepository.RepositoryName, m.selectedRepository.BranchName)) return }