mirror of
https://github.com/fleetdm/fleet
synced 2026-04-29 09:27:18 +00:00
312 lines
7.9 KiB
Go
312 lines
7.9 KiB
Go
package tui
|
|
|
|
import (
|
|
"fleetdm/gm/pkg/ghapi"
|
|
"fleetdm/gm/pkg/logger"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
func (m *model) HandleHotkeys(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case tea.KeyMsg:
|
|
switch m.workflowState {
|
|
case Loading:
|
|
switch msg.String() {
|
|
case "q", "ctrl+c":
|
|
return m, tea.Quit
|
|
}
|
|
return m, nil
|
|
case WorkflowRunning:
|
|
// Only allow quitting during workflow execution
|
|
switch msg.String() {
|
|
case "q", "ctrl+c":
|
|
return m, tea.Quit
|
|
}
|
|
return m, nil
|
|
case WorkflowComplete:
|
|
// Exit when workflow is complete
|
|
switch msg.String() {
|
|
case "q", "ctrl+c", "enter", "esc", " ":
|
|
return m, tea.Quit
|
|
}
|
|
return m, nil
|
|
case NormalMode:
|
|
switch msg.String() {
|
|
case "/":
|
|
m.workflowState = FilterInput
|
|
m.filterInput = ""
|
|
m.applyFilter()
|
|
case "w":
|
|
if len(m.selected) > 0 {
|
|
m.workflowState = WorkflowSelection
|
|
m.workflowCursor = 0
|
|
m.errorMessage = ""
|
|
}
|
|
case "o":
|
|
currentChoices := m.getCurrentChoices()
|
|
if len(currentChoices) > 0 && m.cursor < len(currentChoices) {
|
|
// Get the original issue (not filtered)
|
|
originalIndex := m.getOriginalIndex(m.cursor)
|
|
if originalIndex < len(m.choices) {
|
|
issue := m.choices[originalIndex]
|
|
content := m.generateIssueContent(issue)
|
|
|
|
if m.glamourRenderer != nil {
|
|
rendered, err := m.glamourRenderer.Render(content)
|
|
if err != nil {
|
|
// Fallback to plain content if rendering fails
|
|
m.issueContent = content
|
|
} else {
|
|
m.issueContent = rendered
|
|
}
|
|
} else {
|
|
m.issueContent = content
|
|
}
|
|
|
|
m.detailViewport.SetContent(m.issueContent)
|
|
m.workflowState = IssueDetail
|
|
}
|
|
}
|
|
case "j", "down":
|
|
currentChoices := m.getCurrentChoices()
|
|
if m.cursor < len(currentChoices)-1 {
|
|
m.cursor++
|
|
m.adjustViewForCursor()
|
|
}
|
|
case "k", "up":
|
|
if m.cursor > 0 {
|
|
m.cursor--
|
|
m.adjustViewForCursor()
|
|
}
|
|
case "pgdown", "ctrl+f":
|
|
// Page down - move cursor by view height
|
|
currentChoices := m.getCurrentChoices()
|
|
newCursor := m.cursor + m.viewHeight
|
|
if newCursor >= len(currentChoices) {
|
|
newCursor = len(currentChoices) - 1
|
|
}
|
|
m.cursor = newCursor
|
|
m.adjustViewForCursor()
|
|
case "pgup", "ctrl+b":
|
|
// Page up - move cursor by view height
|
|
newCursor := m.cursor - m.viewHeight
|
|
if newCursor < 0 {
|
|
newCursor = 0
|
|
}
|
|
m.cursor = newCursor
|
|
m.adjustViewForCursor()
|
|
case "home", "ctrl+a":
|
|
// Go to first issue
|
|
m.cursor = 0
|
|
m.adjustViewForCursor()
|
|
case "end", "ctrl+e":
|
|
// Go to last issue
|
|
currentChoices := m.getCurrentChoices()
|
|
m.cursor = len(currentChoices) - 1
|
|
m.adjustViewForCursor()
|
|
case "enter", "x", " ":
|
|
currentChoices := m.getCurrentChoices()
|
|
if m.cursor < len(currentChoices) {
|
|
originalIndex := m.getOriginalIndex(m.cursor)
|
|
if _, exists := m.selected[originalIndex]; exists {
|
|
delete(m.selected, originalIndex)
|
|
} else {
|
|
m.selected[originalIndex] = struct{}{}
|
|
}
|
|
m.selectedCount = len(m.selected)
|
|
}
|
|
case "s":
|
|
// Toggle selection of related issues (parent/sub-tasks)
|
|
currentChoices := m.getCurrentChoices()
|
|
if m.cursor < len(currentChoices) {
|
|
issue := currentChoices[m.cursor]
|
|
// Load from cache or fetch
|
|
related, ok := m.relatedCache[issue.Number]
|
|
if !ok {
|
|
if rel, err := ghapi.GetRelatedIssueNumbers(issue.Number); err == nil {
|
|
related = rel
|
|
m.relatedCache[issue.Number] = related
|
|
}
|
|
}
|
|
related = append(related, issue.Number)
|
|
logger.Debugf("Related issues for #%d: %v", issue.Number, related)
|
|
if len(related) > 0 {
|
|
// Determine if all related currently selected
|
|
allSelected := true
|
|
for _, rn := range related {
|
|
// find index for issue number in m.choices
|
|
for idx, iss := range m.choices {
|
|
if iss.Number == rn {
|
|
if _, ok := m.selected[idx]; !ok {
|
|
allSelected = false
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
// Toggle accordingly
|
|
for _, rn := range related {
|
|
for idx, iss := range m.choices {
|
|
if iss.Number == rn {
|
|
if allSelected {
|
|
delete(m.selected, idx)
|
|
} else {
|
|
m.selected[idx] = struct{}{}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
m.selectedCount = len(m.selected)
|
|
}
|
|
}
|
|
case "l":
|
|
// Select all visible (filtered) issues
|
|
m.selected = make(map[int]struct{})
|
|
currentChoices := m.getCurrentChoices()
|
|
for i := range currentChoices {
|
|
orig := m.getOriginalIndex(i)
|
|
m.selected[orig] = struct{}{}
|
|
}
|
|
m.selectedCount = len(m.selected)
|
|
case "h":
|
|
// Clear all selections
|
|
m.selected = make(map[int]struct{})
|
|
m.selectedCount = 0
|
|
case "q", "ctrl+c":
|
|
return m, tea.Quit
|
|
}
|
|
case IssueDetail:
|
|
switch msg.String() {
|
|
case "q", "ctrl+c":
|
|
return m, tea.Quit
|
|
case "esc":
|
|
m.workflowState = NormalMode
|
|
case "j", "down":
|
|
m.detailViewport.LineDown(1)
|
|
case "k", "up":
|
|
m.detailViewport.LineUp(1)
|
|
case "pgdown", "ctrl+f":
|
|
m.detailViewport.HalfViewDown()
|
|
case "pgup", "ctrl+b":
|
|
m.detailViewport.HalfViewUp()
|
|
case "home", "ctrl+a":
|
|
m.detailViewport.GotoTop()
|
|
case "end", "ctrl+e":
|
|
m.detailViewport.GotoBottom()
|
|
}
|
|
case FilterInput:
|
|
switch msg.String() {
|
|
case "esc":
|
|
m.workflowState = NormalMode
|
|
m.filterInput = ""
|
|
m.applyFilter()
|
|
m.cursor = 0
|
|
m.adjustViewForCursor()
|
|
case "enter":
|
|
m.workflowState = NormalMode
|
|
m.adjustViewForCursor()
|
|
case "backspace":
|
|
if len(m.filterInput) > 0 {
|
|
m.filterInput = m.filterInput[:len(m.filterInput)-1]
|
|
m.applyFilter()
|
|
m.cursor = 0
|
|
m.adjustViewForCursor()
|
|
}
|
|
case "ctrl+c":
|
|
return m, tea.Quit
|
|
default:
|
|
// Add character to filter
|
|
if len(msg.String()) == 1 {
|
|
m.filterInput += msg.String()
|
|
m.applyFilter()
|
|
m.cursor = 0
|
|
m.adjustViewForCursor()
|
|
}
|
|
}
|
|
case WorkflowSelection:
|
|
switch msg.String() {
|
|
case "q", "ctrl+c":
|
|
return m, tea.Quit
|
|
case "j", "down":
|
|
if m.workflowCursor < len(WorkflowTypeValues)-1 {
|
|
m.workflowCursor++
|
|
}
|
|
case "k", "up":
|
|
if m.workflowCursor > 0 {
|
|
m.workflowCursor--
|
|
}
|
|
case "enter":
|
|
m.workflowType = WorkflowType(m.workflowCursor)
|
|
switch m.workflowType {
|
|
// newworkflow Add to switch to support
|
|
case BulkAddLabel, BulkRemoveLabel:
|
|
m.workflowState = LabelInput
|
|
m.labelInput = ""
|
|
case BulkDemoSummary:
|
|
return m, m.executeWorkflow()
|
|
case BulkSprintKickoff, BulkKickOutOfSprint, BulkMoveToCurrentSprint:
|
|
if m.projectID != 0 {
|
|
// Use the provided project ID
|
|
return m, m.executeWorkflow()
|
|
} else {
|
|
m.workflowState = ProjectInput
|
|
m.projectInput = ""
|
|
}
|
|
case BulkMilestoneClose:
|
|
return m, m.executeWorkflow()
|
|
}
|
|
case "esc":
|
|
m.workflowState = NormalMode
|
|
m.errorMessage = ""
|
|
}
|
|
case LabelInput:
|
|
switch msg.String() {
|
|
case "ctrl+c":
|
|
return m, tea.Quit
|
|
case "enter":
|
|
if m.labelInput != "" {
|
|
return m, m.executeWorkflow()
|
|
}
|
|
case "esc":
|
|
m.workflowState = NormalMode
|
|
m.errorMessage = ""
|
|
case "backspace":
|
|
if len(m.labelInput) > 0 {
|
|
m.labelInput = m.labelInput[:len(m.labelInput)-1]
|
|
}
|
|
default:
|
|
// Add character to input
|
|
if len(msg.String()) == 1 {
|
|
m.labelInput += msg.String()
|
|
}
|
|
}
|
|
case ProjectInput:
|
|
switch msg.String() {
|
|
case "ctrl+c":
|
|
return m, tea.Quit
|
|
case "enter":
|
|
if m.projectInput != "" {
|
|
return m, m.executeWorkflow()
|
|
}
|
|
case "esc":
|
|
m.workflowState = NormalMode
|
|
m.errorMessage = ""
|
|
case "backspace":
|
|
if len(m.projectInput) > 0 {
|
|
m.projectInput = m.projectInput[:len(m.projectInput)-1]
|
|
}
|
|
default:
|
|
// Add character to input
|
|
if len(msg.String()) == 1 {
|
|
m.projectInput += msg.String()
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
// Unknown state, just return
|
|
return m, nil
|
|
}
|
|
return m, nil
|
|
}
|