Merge pull request #73 from LazyBachelor/LPM-138

LPM-138 Refactor Tui with composable modals and use canvas

Refactors the TUI (dashboard + kanban) to use a shared, composable modal system with canvas-based overlay rendering, while consolidating styling into internal/style and expanding issue interactions (e.g., comments).

Changes:

Introduces a new pkg/tui/modal system (manager/stack + multiple modal types) and overlays modals using Lipgloss compositor layers.
Refactors dashboard/kanban views and input handling to use the modal manager + focus manager instead of per-modal boolean state.
Consolidates TUI styling by moving from pkg/tui/styles to internal/style and updates components to use the new style package; adds a shared footer renderer.
This commit is contained in:
Robin Olsen
2026-03-20 08:28:10 -07:00
committed by GitHub
parent 9a47acc49a
commit 7facfb6afe
27 changed files with 2469 additions and 1752 deletions

View File

@@ -7,7 +7,7 @@ import (
"github.com/LazyBachelor/LazyPM/pkg/tui/msgs"
)
type DashboardKeyMap struct {
type KeyMap struct {
components.CommonKeyMap
SwitchToKanbanBoard key.Binding
Quit key.Binding
@@ -26,7 +26,7 @@ type DashboardKeyMap struct {
DeleteIssue key.Binding
}
var defaultDashboardKeyMap = DashboardKeyMap{
var defaultDashboardKeyMap = KeyMap{
CommonKeyMap: components.DefaultCommonKeyMap(),
SwitchToKanbanBoard: key.NewBinding(
key.WithKeys("v"),
@@ -69,73 +69,85 @@ var defaultDashboardKeyMap = DashboardKeyMap{
),
}
func (d *Model) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
func (m *Model) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
var cmd tea.Cmd
switch {
case key.Matches(msg, d.keyMap.Help):
d.helpBar.ToggleHelp()
d.logAction("tui toggled help")
case key.Matches(msg, d.keyMap.Quit):
d.logAction("tui quit requested")
case m.notInModalMsgWithKey(msg, m.keyMap.Help):
m.helpBar.ToggleHelp()
m.logAction("tui toggled help")
case m.notInModalMsgWithKey(msg, m.keyMap.Quit):
m.logAction("tui quit requested")
return tea.Quit
case !d.IsInModal() && key.Matches(msg, d.keyMap.SwitchToKanbanBoard):
case m.notInModalMsgWithKey(msg, m.keyMap.SwitchToKanbanBoard):
return func() tea.Msg { return msgs.SwitchToKanbanBoardMsg{} }
case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.ScrollUp):
d.issueDetail.ScrollUp(1)
d.logAction("tui scrolled issue detail up")
case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.ScrollDown):
d.issueDetail.ScrollDown(1)
d.logAction("tui scrolled issue detail down")
case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.EditTitle):
if selected := d.issueList.SelectedItem(); selected.ID != "" {
d.startEditTitle(selected)
cmd = d.titleInput.Focus()
d.logAction("tui started editing issue title")
case m.notInModalMsgWithKey(msg, m.keyMap.ScrollUp):
m.issueDetail.ScrollUp(1)
m.logAction("tui scrolled issue detail up")
case m.notInModalMsgWithKey(msg, m.keyMap.ScrollDown):
m.issueDetail.ScrollDown(1)
m.logAction("tui scrolled issue detail down")
case m.notInModalMsgWithKey(msg, m.keyMap.EditTitle):
if selected := m.issueList.SelectedItem(); selected.ID != "" {
cmd = m.startEditTitle(selected)
m.logAction("tui started editing issue title")
}
case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.EditDescription):
if selected := d.issueList.SelectedItem(); selected.ID != "" {
d.startEditDescription(selected)
cmd = d.descriptionInput.Focus()
d.logAction("tui started editing issue description")
case m.notInModalMsgWithKey(msg, m.keyMap.EditDescription):
if selected := m.issueList.SelectedItem(); selected.ID != "" {
cmd = m.startEditDescription(selected)
m.logAction("tui started editing issue description")
}
case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.ChangeStatus):
if selected := d.issueList.SelectedItem(); selected.ID != "" {
d.startChooseStatus(selected)
d.logAction("tui opened status picker")
case m.notInModalMsgWithKey(msg, m.keyMap.ChangeStatus):
if selected := m.issueList.SelectedItem(); selected.ID != "" {
cmd = m.startChooseStatus(selected)
m.logAction("tui opened status picker")
}
case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.ChangePriority):
if selected := d.issueList.SelectedItem(); selected.ID != "" {
d.startChoosePriority(selected)
d.logAction("tui opened priority picker")
case m.notInModalMsgWithKey(msg, m.keyMap.ChangePriority):
if selected := m.issueList.SelectedItem(); selected.ID != "" {
cmd = m.startChoosePriority(selected)
m.logAction("tui opened priority picker")
}
case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.ChangeType):
if selected := d.issueList.SelectedItem(); selected.ID != "" {
d.startChooseType(selected)
d.logAction("tui opened type picker")
case m.notInModalMsgWithKey(msg, m.keyMap.ChangeType):
if selected := m.issueList.SelectedItem(); selected.ID != "" {
cmd = m.startChooseType(selected)
m.logAction("tui opened type picker")
}
case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.ChangeAssignee):
if selected := d.issueList.SelectedItem(); selected.ID != "" {
d.startEditAssignee(selected)
cmd = d.assigneeInput.Focus()
d.logAction("tui started editing assignee")
case m.notInModalMsgWithKey(msg, m.keyMap.ChangeAssignee):
if selected := m.issueList.SelectedItem(); selected.ID != "" {
cmd = m.startEditAssignee(selected)
m.logAction("tui started editing assignee")
}
case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.AddComment):
if selected := d.issueList.SelectedItem(); selected.ID != "" {
d.startAddComment(selected)
cmd = d.commentInput.Focus()
case m.notInModalMsgWithKey(msg, m.keyMap.AddComment):
if selected := m.issueList.SelectedItem(); selected.ID != "" {
cmd = m.startAddComment(selected)
}
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.editingAssignee && !d.addingComment && key.Matches(msg, d.keyMap.AddIssue):
d.startCreateIssue()
cmd = d.createTitleInput.Focus()
d.logAction("tui started creating issue")
case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.DeleteIssue):
fl := d.issueList
case m.notInModalMsgWithKey(msg, m.keyMap.AddIssue):
cmd = m.startCreateIssue()
m.logAction("tui started creating issue")
case m.notInModalMsgWithKey(msg, m.keyMap.DeleteIssue):
fl := m.issueList
if selected := fl.SelectedItem(); selected.ID != "" {
d.startConfirmDelete(selected.ID, fl.Index())
d.logAction("tui opened delete confirmation")
cmd = m.startConfirmDelete(selected.ID, fl.Index())
m.logAction("tui opened delete confirmation")
}
}
return cmd
}
func (m *Model) notInModalMsgWithKey(msg tea.KeyPressMsg, keyBinding key.Binding) bool {
return !m.IsInModal() && key.Matches(msg, keyBinding)
}

View File

@@ -3,12 +3,11 @@ package dashboard
import (
"context"
"charm.land/bubbles/v2/textarea"
"charm.land/bubbles/v2/textinput"
tea "charm.land/bubbletea/v2"
"github.com/LazyBachelor/LazyPM/internal/app"
"github.com/LazyBachelor/LazyPM/internal/models"
"github.com/LazyBachelor/LazyPM/pkg/tui/components"
"github.com/LazyBachelor/LazyPM/pkg/tui/modal"
)
// Use shared types from components for consistency.
@@ -20,102 +19,80 @@ type (
)
type Model struct {
header Header
issueList IssueList
issueDetail IssueDetail
closedIssueList IssueList
helpBar components.HelpBar
keyMap DashboardKeyMap
app *app.App
width int
height int
focusedWindow int // 0 = main (display issues), 1 = closed issues
focusedPaneMain int // 0 = list, 1 = detail
focusedPaneClosed int
editingTitle bool // true while we are editing a title
titleInput textinput.Model
editingIssueID string
header Header
issueList IssueList
issueDetail IssueDetail
closedIssueList IssueList
helpBar components.HelpBar
keyMap KeyMap
app *app.App
width int
height int
editingDescription bool // true while editing a description
descriptionInput textarea.Model
editingDescIssueID string
creatingIssue bool // true while creating a new issue
createTitleInput textinput.Model
// Modal and Focus management
modalManager *modal.Manager
focusManager *modal.FocusManager
confirmingDelete bool // true while confirming a delete
deleteConfirmID string
deleteConfirmIndex int
// Current issue being operated on
currentIssueID string
deleteIndex int
choosingStatus bool
statusIssueID string
choosingPriority bool
priorityIssueID string
choosingType bool
typeIssueID string
editingAssignee bool
assigneeInput textinput.Model
assigneeIssueID string
addingComment bool
commentInput textarea.Model
commentIssueID string
choosingCloseReason bool
closeReasonIssueID string
closingOtherReason bool
closeReasonInput textarea.Model
feedbackChan chan models.ValidationFeedback
quitChan chan bool
currentFeedback models.ValidationFeedback
showComplete bool
submitChan chan<- struct{}
feedbackChan chan models.ValidationFeedback
quitChan chan bool
currentFeedback models.ValidationFeedback
showComplete bool
submitChan chan<- struct{}
}
func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool, submitChan chan<- struct{}) *Model {
m := &Model{
header: components.NewHeader("Project Manager Dashboard"),
keyMap: defaultDashboardKeyMap,
app: app,
width: 80,
height: 24,
focusedWindow: 0,
focusedPaneMain: 0,
focusedPaneClosed: 0,
feedbackChan: feedbackChan,
quitChan: quitChan,
submitChan: submitChan,
header: components.NewHeader("Project Manager Dashboard"),
keyMap: defaultDashboardKeyMap,
app: app,
width: 80,
height: 24,
feedbackChan: feedbackChan,
quitChan: quitChan,
submitChan: submitChan,
modalManager: modal.NewManager(),
focusManager: modal.NewFocusManager(),
deleteIndex: -1,
}
// Setup lists
allIssues, _ := app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{})
m.issueList = components.NewIssueListFromIssues(app, components.SortedIssues(allIssues), 0, 0)
m.issueDetail = components.NewIssueDetail()
m.helpBar = components.NewHelpBar(components.ViewIssues)
inputs := components.NewIssueInputs()
m.titleInput = inputs.Title
m.createTitleInput = inputs.CreateTitle
m.descriptionInput = inputs.Description
m.assigneeInput = inputs.Assignee
// Setup focus
m.focusManager.EnableArea(modal.FocusList)
m.focusManager.EnableArea(modal.FocusDetail)
m.focusManager.SetCurrent(modal.FocusList)
closeReasonTa := textarea.New()
closeReasonTa.Placeholder = "Enter closing reason..."
closeReasonTa.SetWidth(56)
closeReasonTa.SetHeight(4)
m.closeReasonInput = closeReasonTa
commentTa := textarea.New()
commentTa.Placeholder = "Write your comment..."
commentTa.SetWidth(56)
commentTa.SetHeight(6)
m.commentInput = commentTa
// Register modals
m.registerModals()
if selected := m.issueList.SelectedItem(); selected.ID != "" {
m.setDetailIssueWithComments(selected.Issue)
} else if selected := m.closedIssueList.SelectedItem(); selected.ID != "" {
m.setDetailIssueWithComments(selected.Issue)
}
return m
}
func (m *Model) Init() tea.Cmd {
if m.submitChan != nil {
m.submitChan <- struct{}{}
m.logAction("tui submitted validation")
}
return components.ListenForValidation(m.feedbackChan)
}
// registerModals sets up all modals using the common registration helper
func (m *Model) registerModals() {
modal.RegisterCommonModals(m.modalManager)
}
// setDetailIssueWithComments sets the issue in the detail pane and loads its comments.
func (m *Model) setDetailIssueWithComments(issue models.Issue) {
m.issueDetail.SetIssue(issue)
@@ -127,13 +104,6 @@ func (m *Model) setDetailIssueWithComments(issue models.Issue) {
m.issueDetail.SetComments(comments)
}
func (m *Model) startAddComment(selected ListIssue) {
m.addingComment = true
m.commentIssueID = selected.ID
m.commentInput.SetValue("")
m.commentInput.Reset()
}
func (m *Model) logAction(action string) {
if m.app != nil {
m.app.LogAction(models.EncodeActionEvent(models.ActionEvent{
@@ -144,7 +114,6 @@ func (m *Model) logAction(action string) {
}
// submitValidation sends a validation request to the submit channel.
// Call this after every successful user action that modifies issues.
func (m *Model) submitValidation() {
if m.submitChan != nil {
select {
@@ -155,80 +124,15 @@ func (m *Model) submitValidation() {
}
}
func (m *Model) startEditTitle(selected ListIssue) {
m.editingTitle = true
m.editingIssueID = selected.ID
m.titleInput.SetValue(selected.Issue.Title)
m.titleInput.CursorEnd()
}
func (m *Model) startEditDescription(selected ListIssue) {
m.editingDescription = true
m.editingDescIssueID = selected.ID
m.descriptionInput.SetValue(selected.Issue.Description)
m.descriptionInput.CursorEnd()
}
func (m *Model) startCreateIssue() {
m.creatingIssue = true
m.createTitleInput.SetValue("")
m.createTitleInput.Reset()
}
func (m *Model) startConfirmDelete(issueID string, index int) {
m.confirmingDelete = true
m.deleteConfirmID = issueID
m.deleteConfirmIndex = index
}
func (m *Model) startChooseStatus(selected ListIssue) {
m.choosingStatus = true
m.statusIssueID = selected.ID
}
func (m *Model) startChoosePriority(selected ListIssue) {
m.choosingPriority = true
m.priorityIssueID = selected.ID
}
func (m *Model) startChooseType(selected ListIssue) {
m.choosingType = true
m.typeIssueID = selected.ID
}
func (m *Model) startEditAssignee(selected ListIssue) {
m.editingAssignee = true
m.assigneeIssueID = selected.ID
m.assigneeInput.SetValue(selected.Assignee)
m.assigneeInput.CursorEnd()
}
func (m *Model) Init() tea.Cmd {
if m.submitChan != nil {
m.submitChan <- struct{}{}
m.logAction("tui submitted validation")
}
return components.ListenForValidation(m.feedbackChan)
}
// IsInModal returns true when a modal (edit, create, delete confirm, choose status/priority/type) is active.
// IsInModal returns true when a modal is active
func (m *Model) IsInModal() bool {
return m.editingTitle || m.creatingIssue || m.editingDescription ||
m.choosingStatus || m.choosingPriority || m.confirmingDelete ||
m.choosingType || m.editingAssignee ||
m.choosingCloseReason || m.closingOtherReason
return m.modalManager.IsModalActive()
}
func (m *Model) IsFocusedOnList() bool {
if m.focusedWindow == 0 {
return m.focusedPaneMain == 0
}
return m.focusedPaneClosed == 0
return m.focusManager.IsListFocused()
}
func (m *Model) IsFocusedOnDetail() bool {
if m.focusedWindow == 0 {
return m.focusedPaneMain == 1
}
return m.focusedPaneClosed == 1
return m.focusManager.IsDetailFocused()
}

View File

@@ -2,33 +2,18 @@ package dashboard
import (
"context"
"os"
"os/user"
"strconv"
"charm.land/bubbles/v2/list"
tea "charm.land/bubbletea/v2"
"github.com/LazyBachelor/LazyPM/internal/models"
"github.com/LazyBachelor/LazyPM/internal/utils/user"
"github.com/LazyBachelor/LazyPM/pkg/tui/components"
"github.com/LazyBachelor/LazyPM/pkg/tui/modal"
"github.com/LazyBachelor/LazyPM/pkg/tui/msgs"
)
func defaultCommentAuthor() string {
if u, err := user.Current(); err == nil && u.Username != "" {
return u.Username
}
if s := os.Getenv("USER"); s != "" {
return s
}
if s := os.Getenv("USERNAME"); s != "" {
return s
}
return "user"
}
func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd {
/* update handler for issueTitleUpdatedMsg, issueDescriptionUpdatedMsg, and issueStatusUpdatedMsg to avoid using nearly identical code for refreshing the issue lists and updating the detail view
Fetch all issues, update both lists, set the detail view for the given issue, and return a command to select that issue. Returns nil if fetch fails.
*/
allIssues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{})
if err != nil {
return nil
@@ -40,24 +25,228 @@ func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd {
break
}
}
return tea.Sequence(setItemsCmd, func() tea.Msg { return msgs.SelectIssueMsg{IssueID: issueID} })
}
// refreshAndSubmit refreshes the issue lists and submits validation.
// This is a wrapper that should be used after any successful user action.
func (m *Model) refreshAndSubmit(issueID string) tea.Cmd {
refreshCmd := m.refreshIssueListsAndSelectIssue(issueID)
m.submitValidation()
return refreshCmd
}
func (m *Model) startEditTitle(selected ListIssue) tea.Cmd {
m.currentIssueID = selected.ID
titleModal := m.modalManager.GetTextInputModal(modal.ModalEditTitle)
if titleModal != nil {
titleModal.SetValue(selected.Issue.Title)
titleModal.CursorEnd()
return m.modalManager.ShowModal(modal.ModalEditTitle)
}
return nil
}
func (m *Model) startEditDescription(selected ListIssue) tea.Cmd {
m.currentIssueID = selected.ID
descModal := m.modalManager.GetTextAreaModal(modal.ModalEditDescription)
if descModal != nil {
descModal.SetValue(selected.Issue.Description)
return m.modalManager.ShowModal(modal.ModalEditDescription)
}
return nil
}
func (m *Model) startCreateIssue() tea.Cmd {
createModal := m.modalManager.GetTextInputModal(modal.ModalCreateIssue)
if createModal != nil {
createModal.Reset()
return m.modalManager.ShowModal(modal.ModalCreateIssue)
}
return nil
}
func (m *Model) startConfirmDelete(issueID string, index int) tea.Cmd {
m.currentIssueID = issueID
m.deleteIndex = index
deleteModal := m.modalManager.GetConfirmModal(modal.ModalConfirmDelete)
if deleteModal != nil {
return m.modalManager.ShowModal(modal.ModalConfirmDelete)
}
return nil
}
func (m *Model) startChooseStatus(selected ListIssue) tea.Cmd {
m.currentIssueID = selected.ID
return m.modalManager.ShowModal(modal.ModalSelectStatus)
}
func (m *Model) startChoosePriority(selected ListIssue) tea.Cmd {
m.currentIssueID = selected.ID
return m.modalManager.ShowModal(modal.ModalSelectPriority)
}
func (m *Model) startChooseType(selected ListIssue) tea.Cmd {
m.currentIssueID = selected.ID
return m.modalManager.ShowModal(modal.ModalSelectType)
}
func (m *Model) startEditAssignee(selected ListIssue) tea.Cmd {
m.currentIssueID = selected.ID
assigneeModal := m.modalManager.GetTextInputModal(modal.ModalEditAssignee)
if assigneeModal != nil {
assigneeModal.SetValue(selected.Assignee)
assigneeModal.CursorEnd()
return m.modalManager.ShowModal(modal.ModalEditAssignee)
}
return nil
}
func (m *Model) startAddComment(selected ListIssue) tea.Cmd {
m.currentIssueID = selected.ID
commentModal := m.modalManager.GetTextAreaModal(modal.ModalAddComment)
if commentModal != nil {
commentModal.Reset()
return m.modalManager.ShowModal(modal.ModalAddComment)
}
return nil
}
// handleModalCompleted handles all modal completion messages
func (m *Model) handleModalCompleted(msg modal.ModalCompletedMsg) tea.Cmd {
switch msg.ModalID {
case modal.ModalEditTitle:
if r, ok := msg.Value.(modal.TextInputResult); ok {
m.logAction("tui submitted issue title edit")
cmd := msgs.UpdateIssueTitleCmd(m.app, m.currentIssueID, r.Value)
return func() tea.Msg { return cmd() }
}
case modal.ModalCreateIssue:
if r, ok := msg.Value.(modal.TextInputResult); ok && r.Value != "" {
m.logAction("tui submitted new issue")
cmd := msgs.CreateIssueCmd(m.app, r.Value)
return func() tea.Msg { return cmd() }
}
case modal.ModalEditAssignee:
if r, ok := msg.Value.(modal.TextInputResult); ok {
m.logAction("tui submitted assignee edit")
cmd := msgs.UpdateIssueAssigneeCmd(m.app, m.currentIssueID, r.Value)
return func() tea.Msg { return cmd() }
}
case modal.ModalEditDescription:
if r, ok := msg.Value.(modal.TextAreaResult); ok {
m.logAction("tui submitted issue description edit")
cmd := msgs.UpdateIssueDescriptionCmd(m.app, m.currentIssueID, r.Value)
return func() tea.Msg { return cmd() }
}
case modal.ModalAddComment:
if r, ok := msg.Value.(modal.TextAreaResult); ok && r.Value != "" {
cmd := msgs.AddIssueCommentCmd(m.app, m.currentIssueID, user.GetOsUsername(), r.Value)
return func() tea.Msg { return cmd() }
}
case modal.ModalCloseReason:
if r, ok := msg.Value.(modal.TextAreaResult); ok && r.Value != "" {
m.logAction("tui submitted custom close reason")
cmd := msgs.CloseIssueCmd(m.app, m.currentIssueID, r.Value)
return func() tea.Msg { return cmd() }
}
case modal.ModalConfirmDelete:
if r, ok := msg.Value.(modal.ConfirmResult); ok && r.Confirmed {
m.logAction("tui confirmed issue deletion")
idx := m.deleteIndex
issueID := m.currentIssueID
m.deleteIndex = -1
m.currentIssueID = ""
cmd := msgs.DeleteIssueCmd(m.app, issueID, idx)
return func() tea.Msg { return cmd() }
}
case modal.ModalSelectStatus:
if r, ok := msg.Value.(modal.SelectResult); ok {
m.logAction("tui selected issue status")
if r.SelectedValue == "closing" {
return m.modalManager.ShowModal(modal.ModalSelectCloseReason)
}
cmd := msgs.UpdateIssueStatusCmd(m.app, m.currentIssueID, r.SelectedValue)
return func() tea.Msg { return cmd() }
}
case modal.ModalSelectCloseReason:
if r, ok := msg.Value.(modal.SelectResult); ok {
if r.SelectedValue == "other" {
return m.modalManager.ShowModal(modal.ModalCloseReason)
}
cmd := msgs.CloseIssueCmd(m.app, m.currentIssueID, r.SelectedValue)
return func() tea.Msg { return cmd() }
}
case modal.ModalSelectPriority:
if r, ok := msg.Value.(modal.SelectResult); ok {
m.logAction("tui selected issue priority")
priority, _ := strconv.Atoi(r.SelectedValue)
cmd := msgs.UpdateIssuePriorityCmd(m.app, m.currentIssueID, priority)
return func() tea.Msg { return cmd() }
}
case modal.ModalSelectType:
if r, ok := msg.Value.(modal.SelectResult); ok {
m.logAction("tui selected issue type")
issueType := models.IssueType(r.SelectedValue)
cmd := msgs.UpdateIssueTypeCmd(m.app, m.currentIssueID, issueType)
return func() tea.Msg { return cmd() }
}
}
return nil
}
// handleModalCancelled handles all modal cancellation messages
func (m *Model) handleModalCancelled(msg modal.ModalCancelledMsg) {
switch msg.ModalID {
case modal.ModalEditTitle:
m.currentIssueID = ""
m.logAction("tui canceled issue title edit")
case modal.ModalCreateIssue:
m.logAction("tui canceled issue creation")
case modal.ModalEditAssignee:
m.currentIssueID = ""
m.logAction("tui canceled assignee edit")
case modal.ModalEditDescription:
m.currentIssueID = ""
m.logAction("tui canceled issue description edit")
case modal.ModalAddComment:
m.currentIssueID = ""
case modal.ModalCloseReason:
m.currentIssueID = ""
m.logAction("tui canceled close reason")
case modal.ModalConfirmDelete:
m.deleteIndex = -1
m.currentIssueID = ""
m.logAction("tui canceled issue deletion")
case modal.ModalSelectStatus:
m.currentIssueID = ""
m.logAction("tui canceled status picker")
case modal.ModalSelectCloseReason:
m.currentIssueID = ""
m.logAction("tui canceled close reason")
case modal.ModalSelectPriority:
m.currentIssueID = ""
m.logAction("tui canceled priority picker")
case modal.ModalSelectType:
m.currentIssueID = ""
m.logAction("tui canceled type picker")
}
}
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if cmd, handled := m.modalManager.Update(msg); handled {
return m, cmd
}
switch msg := msg.(type) {
case modal.ModalCompletedMsg:
return m, m.handleModalCompleted(msg)
case modal.ModalCancelledMsg:
m.handleModalCancelled(msg)
return m, nil
case msgs.TitleUpdatedMsg:
m.editingTitle = false
m.editingIssueID = ""
m.titleInput.Blur()
m.modalManager.GetTextInputModal(modal.ModalEditTitle).Reset()
m.currentIssueID = ""
if msg.Err != nil {
m.logAction("tui failed to update issue title")
return m, nil
@@ -66,9 +255,8 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.refreshAndSubmit(msg.IssueID)
case msgs.DescriptionUpdatedMsg:
m.editingDescription = false
m.editingDescIssueID = ""
m.descriptionInput.Blur()
m.modalManager.GetTextAreaModal(modal.ModalEditDescription).Reset()
m.currentIssueID = ""
if msg.Err != nil {
m.logAction("tui failed to update issue description")
return m, nil
@@ -77,8 +265,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.refreshAndSubmit(msg.IssueID)
case msgs.StatusUpdatedMsg:
m.choosingStatus = false
m.statusIssueID = ""
m.currentIssueID = ""
if msg.Err != nil {
m.logAction("tui failed to update issue status")
return m, nil
@@ -87,8 +274,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.refreshAndSubmit(msg.IssueID)
case msgs.PriorityUpdatedMsg:
m.choosingPriority = false
m.priorityIssueID = ""
m.currentIssueID = ""
if msg.Err != nil {
m.logAction("tui failed to update issue priority")
return m, nil
@@ -97,8 +283,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.refreshAndSubmit(msg.IssueID)
case msgs.TypeUpdatedMsg:
m.choosingType = false
m.typeIssueID = ""
m.currentIssueID = ""
if msg.Err != nil {
m.logAction("tui failed to update issue type")
return m, nil
@@ -107,9 +292,8 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.refreshAndSubmit(msg.IssueID)
case msgs.AssigneeUpdatedMsg:
m.editingAssignee = false
m.assigneeIssueID = ""
m.assigneeInput.Blur()
m.modalManager.GetTextInputModal(modal.ModalEditAssignee).Reset()
m.currentIssueID = ""
if msg.Err != nil {
m.logAction("tui failed to update issue assignee")
return m, nil
@@ -123,9 +307,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
case msgs.CreatedMsg:
m.creatingIssue = false
m.createTitleInput.Blur()
m.createTitleInput.Reset()
m.modalManager.GetTextInputModal(modal.ModalCreateIssue).Reset()
if msg.Err != nil || msg.Issue == nil {
m.logAction("tui failed to create issue")
return m, nil
@@ -136,11 +318,9 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
setItemsCmd := m.issueList.SetIssues(components.SortedIssues(allIssues))
// Determine the created issue from the refreshed list to ensure all fields (like ID) are populated.
selectedIssue := msg.Issue
if selectedIssue.ID == "" {
for _, issue := range allIssues {
// Prefer an issue that matches the created issue's title when ID is not yet known.
if issue.Title == msg.Issue.Title {
selectedIssue = issue
break
@@ -152,19 +332,19 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.logAction("tui created issue")
m.submitValidation()
return m, tea.Sequence(setItemsCmd, func() tea.Msg { return msgs.SelectIssueMsg{IssueID: selectedIssue.ID} })
case msgs.IssueCommentAddedMsg:
m.addingComment = false
m.commentIssueID = ""
m.commentInput.Blur()
m.commentInput.Reset()
m.modalManager.GetTextAreaModal(modal.ModalAddComment).Reset()
m.currentIssueID = ""
if msg.Err != nil {
return m, nil
}
m.submitValidation()
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
case msgs.DeletedMsg:
m.confirmingDelete = false
m.deleteConfirmID = ""
m.deleteIndex = -1
m.currentIssueID = ""
if msg.Err != nil {
m.logAction("tui failed to delete issue")
return m, nil
@@ -174,297 +354,11 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
case tea.KeyPressMsg:
if m.confirmingDelete {
switch msg.String() {
case "y", "Y":
m.logAction("tui confirmed issue deletion")
issueID := m.deleteConfirmID
idx := m.deleteConfirmIndex
m.confirmingDelete = false
m.deleteConfirmID = ""
return m, msgs.DeleteIssueCmd(m.app, issueID, idx)
case "n", "N", "esc":
m.logAction("tui canceled issue deletion")
m.confirmingDelete = false
m.deleteConfirmID = ""
return m, nil
}
}
if m.choosingStatus {
switch msg.String() {
case "o":
m.logAction("tui selected issue status open")
issueID := m.statusIssueID
m.choosingStatus = false
m.statusIssueID = ""
return m, msgs.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusOpen))
case "i":
m.logAction("tui selected issue status in_progress")
issueID := m.statusIssueID
m.choosingStatus = false
m.statusIssueID = ""
return m, msgs.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusInProgress))
case "b":
m.logAction("tui selected issue status blocked")
issueID := m.statusIssueID
m.choosingStatus = false
m.statusIssueID = ""
return m, msgs.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusBlocked))
case "r":
m.logAction("tui selected issue status ready_to_sprint")
issueID := m.statusIssueID
m.choosingStatus = false
m.statusIssueID = ""
return m, msgs.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusReadyToSprint))
case "c":
m.logAction("tui selected issue status closing")
issueID := m.statusIssueID
m.choosingStatus = false
m.statusIssueID = ""
m.choosingCloseReason = true
m.closeReasonIssueID = issueID
return m, nil
case "esc":
m.logAction("tui canceled status picker")
m.choosingStatus = false
m.statusIssueID = ""
return m, nil
}
}
if m.choosingCloseReason {
var reason string
switch msg.String() {
case "d":
m.logAction("tui selected close reason done")
reason = "Done"
case "u":
m.logAction("tui selected close reason duplicate issue")
reason = "Duplicate issue"
case "w":
m.logAction("tui selected close reason won't fix")
reason = "Won't fix"
case "o":
m.logAction("tui selected close reason obsolete")
reason = "Obsolete"
case "h":
m.logAction("tui selected close reason other")
m.choosingCloseReason = false
m.closingOtherReason = true
m.closeReasonInput.SetValue("")
m.closeReasonInput.Focus()
return m, nil
case "esc":
m.logAction("tui canceled close reason picker")
m.choosingCloseReason = false
m.closeReasonIssueID = ""
return m, nil
}
if reason != "" {
issueID := m.closeReasonIssueID
m.choosingCloseReason = false
m.closeReasonIssueID = ""
return m, msgs.CloseIssueCmd(m.app, issueID, reason)
}
}
if m.closingOtherReason {
switch msg.String() {
case "enter", "ctrl+s":
reason := m.closeReasonInput.Value()
if reason != "" {
m.logAction("tui submitted custom close reason")
issueID := m.closeReasonIssueID
m.closingOtherReason = false
m.closeReasonIssueID = ""
m.closeReasonInput.Blur()
return m, msgs.CloseIssueCmd(m.app, issueID, reason)
}
case "esc":
m.logAction("tui canceled custom close reason")
m.closingOtherReason = false
m.closeReasonIssueID = ""
m.closeReasonInput.Blur()
return m, nil
}
var cmd tea.Cmd
m.closeReasonInput, cmd = m.closeReasonInput.Update(msg)
return m, cmd
}
if m.choosingPriority {
switch msg.String() {
case "0", "1", "2", "3", "4":
m.logAction("tui selected issue priority")
issueID := m.priorityIssueID
priority := int(msg.String()[0] - '0')
m.choosingPriority = false
m.priorityIssueID = ""
return m, msgs.UpdateIssuePriorityCmd(m.app, issueID, priority)
case "esc":
m.logAction("tui canceled priority picker")
m.choosingPriority = false
m.priorityIssueID = ""
return m, nil
default:
return m, nil
}
}
if m.choosingType {
switch msg.String() {
case "b":
m.logAction("tui selected issue type bug")
issueID := m.typeIssueID
m.choosingType = false
m.typeIssueID = ""
return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeBug)
case "f":
m.logAction("tui selected issue type feature")
issueID := m.typeIssueID
m.choosingType = false
m.typeIssueID = ""
return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeFeature)
case "t":
m.logAction("tui selected issue type task")
issueID := m.typeIssueID
m.choosingType = false
m.typeIssueID = ""
return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeTask)
case "e":
m.logAction("tui selected issue type epic")
issueID := m.typeIssueID
m.choosingType = false
m.typeIssueID = ""
return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeEpic)
case "c":
m.logAction("tui selected issue type chore")
issueID := m.typeIssueID
m.choosingType = false
m.typeIssueID = ""
return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeChore)
case "esc":
m.logAction("tui canceled type picker")
m.choosingType = false
m.typeIssueID = ""
return m, nil
default:
return m, nil
}
}
if m.creatingIssue {
if msg.String() == "enter" {
title := m.createTitleInput.Value()
if title != "" {
m.logAction("tui submitted new issue")
return m, msgs.CreateIssueCmd(m.app, title)
}
}
if msg.String() == "esc" {
m.logAction("tui canceled issue creation")
m.creatingIssue = false
m.createTitleInput.Blur()
m.createTitleInput.Reset()
return m, nil
}
var cmd tea.Cmd
m.createTitleInput, cmd = m.createTitleInput.Update(msg)
return m, cmd
}
if m.editingAssignee {
if msg.String() == "enter" {
assignee := m.assigneeInput.Value()
m.logAction("tui submitted assignee edit")
return m, msgs.UpdateIssueAssigneeCmd(m.app, m.assigneeIssueID, assignee)
}
if msg.String() == "esc" {
m.logAction("tui canceled assignee edit")
m.editingAssignee = false
m.assigneeIssueID = ""
m.assigneeInput.Blur()
return m, nil
}
var cmd tea.Cmd
m.assigneeInput, cmd = m.assigneeInput.Update(msg)
return m, cmd
}
if m.editingTitle {
if msg.String() == "enter" {
newTitle := m.titleInput.Value()
if newTitle != "" {
m.logAction("tui submitted issue title edit")
return m, msgs.UpdateIssueTitleCmd(m.app, m.editingIssueID, newTitle)
}
}
if msg.String() == "esc" {
m.logAction("tui canceled issue title edit")
m.editingTitle = false
m.editingIssueID = ""
m.titleInput.Blur()
return m, nil
}
var cmd tea.Cmd
m.titleInput, cmd = m.titleInput.Update(msg)
return m, cmd
}
if m.addingComment {
if msg.String() == "ctrl+s" || msg.String() == "enter" {
text := m.commentInput.Value()
if text != "" {
issueID := m.commentIssueID
m.addingComment = false
m.commentIssueID = ""
m.commentInput.Blur()
m.commentInput.Reset()
return m, msgs.AddIssueCommentCmd(m.app, issueID, defaultCommentAuthor(), text)
}
}
if msg.String() == "esc" {
m.addingComment = false
m.commentIssueID = ""
m.commentInput.Blur()
m.commentInput.Reset()
return m, nil
}
var cmd tea.Cmd
m.commentInput, cmd = m.commentInput.Update(msg)
return m, cmd
}
if m.editingDescription {
if msg.String() == "ctrl+s" {
m.logAction("tui submitted issue description edit")
issueID := m.editingDescIssueID
newDesc := m.descriptionInput.Value()
m.editingDescription = false
m.editingDescIssueID = ""
m.descriptionInput.Blur()
return m, msgs.UpdateIssueDescriptionCmd(m.app, issueID, newDesc)
}
if msg.String() == "esc" {
m.logAction("tui canceled issue description edit")
m.editingDescription = false
m.editingDescIssueID = ""
m.descriptionInput.Blur()
return m, nil
}
var cmd tea.Cmd
m.descriptionInput, cmd = m.descriptionInput.Update(msg)
return m, cmd
}
if m.issueList.FilterState() == list.Filtering {
cmd, _ := m.issueList.Update(msg)
return m, cmd
}
// On main dashboard, ESC does nothing; only q quits; like in lazybeads.
if msg.String() == "esc" {
return m, nil
}
@@ -473,6 +367,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if cmd != nil {
return m, cmd
}
case components.ValidationFeedbackMsg:
m.currentFeedback = msg.Feedback
if msg.Feedback.Success {
@@ -484,10 +379,11 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
m.modalManager.SetSize(msg.Width, msg.Height)
return m, nil
}
if m.focusedWindow == 0 {
if m.focusManager.IsFocused(modal.FocusList) {
cmd, changed := m.issueList.Update(msg)
if changed {
if selected := m.issueList.SelectedItem(); selected.ID != "" {

View File

@@ -4,16 +4,17 @@ import (
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/LazyBachelor/LazyPM/pkg/tui/components"
"github.com/LazyBachelor/LazyPM/pkg/tui/styles"
"github.com/LazyBachelor/LazyPM/pkg/tui/modal"
"github.com/LazyBachelor/LazyPM/internal/style"
)
func (m *Model) View() tea.View {
if m.width == 0 || m.height == 0 {
// if there is no space just print a loading message
return tea.NewView("Loading...")
}
m.helpBar.SetWidth(m.width)
m.modalManager.SetSize(m.width, m.height)
header := m.header.View(m.width)
headerHeight := m.header.Height()
@@ -21,202 +22,27 @@ func (m *Model) View() tea.View {
footer := components.RenderFooter(m.width, &m.helpBar, m.currentFeedback)
footerHeight := lipgloss.Height(footer)
// To avoid layer overflow or clipping, the label heights are calculated and subtracted from the available height before calculating the list heights to avoid layout overflow or clipping.
contentHeight := m.height - headerHeight - footerHeight
//mainLabel := styles.LabelStyle.Render("Display issues")
//closedLabel := styles.LabelStyle.Render("Closed issues")
//labelHeight := lipgloss.Height(mainLabel) + lipgloss.Height(closedLabel)
availableForLists := contentHeight
halfHeight := max(availableForLists / 2, 1)
halfHeight := max(availableForLists/2, 1)
totalContentWidth := m.width - 1
listWidth := totalContentWidth * styles.ListViewRatio / 100
listWidth := totalContentWidth * style.ListViewRatio / 100
detailWidth := totalContentWidth - listWidth
m.issueList.SetSize(listWidth, halfHeight)
//m.closedIssueList.SetSize(listWidth, halfHeight)
m.issueDetail.SetSize(detailWidth, contentHeight)
// Only highlight the focused list; unfocused list should not show selection highlight.
m.issueList.SetHighlightSelected(m.focusedWindow == 0 && m.focusedPaneMain == 0)
m.closedIssueList.SetHighlightSelected(m.focusedWindow == 1 && m.focusedPaneClosed == 0)
m.issueList.SetHighlightSelected(m.focusManager.IsFocused(modal.FocusList))
listView := m.issueList.View()
//closedListView := m.closedIssueList.View()
detailView := m.issueDetail.View()
//if m.focusedWindow == 0 {
// mainLabel = lipgloss.NewStyle().Foreground(styles.Primary).Bold(true).Render("Display issues ▶")
//} else {
// closedLabel = lipgloss.NewStyle().Foreground(styles.Primary).Bold(true).Render("Closed issues ▶")
//}
leftColumn := lipgloss.JoinVertical(lipgloss.Left,
//mainLabel,
listView,
//closedLabel, closedListView,
)
leftColumn := lipgloss.JoinVertical(lipgloss.Left, listView)
content := lipgloss.JoinHorizontal(lipgloss.Left, leftColumn, detailView)
mainView := lipgloss.JoinVertical(lipgloss.Left, header, content, footer)
if m.editingAssignee {
editBoxWidth := min(60, m.width-4)
m.assigneeInput.SetWidth(editBoxWidth - 2)
editContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Edit assignee (Enter to save, Esc to cancel):"),
m.assigneeInput.View(),
)
editBox := styles.ContainerStyle.
Width(editBoxWidth).
BorderForeground(styles.PrimaryBorder).
Render(editContent)
return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox))
}
if m.editingTitle {
editBoxWidth := min(60, m.width-4)
m.titleInput.SetWidth(editBoxWidth - 2)
editContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Edit title (Enter to save, Esc to cancel):"),
m.titleInput.View(),
)
editBox := styles.ContainerStyle.
Width(editBoxWidth).
BorderForeground(styles.PrimaryBorder).
Render(editContent)
return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox))
}
if m.addingComment {
editBoxWidth := min(60, m.width-4)
m.commentInput.SetWidth(editBoxWidth - 2)
m.commentInput.SetHeight(8)
editContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Add comment for "+m.commentIssueID+" (Ctrl+S or Enter to save, Esc to cancel):"),
m.commentInput.View(),
)
editBox := styles.ContainerStyle.
Width(editBoxWidth).
BorderForeground(styles.PrimaryBorder).
Render(editContent)
return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox))
}
if m.editingDescription {
editBoxWidth := min(60, m.width-4)
m.descriptionInput.SetWidth(editBoxWidth - 2)
m.descriptionInput.SetHeight(10)
editContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Edit description (Ctrl+S to save, Esc to cancel):"),
m.descriptionInput.View(),
)
editBox := styles.ContainerStyle.
Width(editBoxWidth).
BorderForeground(styles.PrimaryBorder).
Render(editContent)
return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox))
}
if m.creatingIssue {
createBoxWidth := min(60, m.width-4)
m.createTitleInput.SetWidth(createBoxWidth - 2)
createContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("New issue (Enter to create, Esc to cancel):"),
m.createTitleInput.View(),
)
createBox := styles.ContainerStyle.
Width(createBoxWidth).
BorderForeground(styles.PrimaryBorder).
Render(createContent)
return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, createBox))
}
if m.confirmingDelete {
confirmContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Delete issue "+m.deleteConfirmID+"?"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("Press y to delete, n or Esc to cancel"),
)
confirmBoxWidth := min(50, m.width-4)
confirmBox := styles.ContainerStyle.
Width(confirmBoxWidth).
BorderForeground(styles.PrimaryBorder).
Render(confirmContent)
return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, confirmBox))
}
if m.choosingStatus {
statusContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Change status for "+m.statusIssueID+":"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("o = open i = in_progress r = ready_to_sprint c = closing (choose reason)"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"),
)
statusBoxWidth := min(50, m.width-4)
statusBox := styles.ContainerStyle.
Width(statusBoxWidth).
BorderForeground(styles.PrimaryBorder).
Render(statusContent)
return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, statusBox))
}
if m.choosingPriority {
priorityContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Change priority for "+m.priorityIssueID+":"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("0 = irrelevant 1 = low 2 = normal 3 = high 4 = critical"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"),
)
priorityBoxWidth := min(60, m.width-4)
priorityBox := styles.ContainerStyle.
Width(priorityBoxWidth).
BorderForeground(styles.PrimaryBorder).
Render(priorityContent)
return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, priorityBox))
}
if m.choosingCloseReason {
reasonContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Choose closing reason for "+m.closeReasonIssueID+":"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("d = Done u = Duplicate issue w = Won't fix o = Obsolete h = Other"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"),
)
reasonBoxWidth := min(70, m.width-4)
reasonBox := styles.ContainerStyle.
Width(reasonBoxWidth).
BorderForeground(styles.PrimaryBorder).
Render(reasonContent)
return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, reasonBox))
}
if m.closingOtherReason {
editBoxWidth := min(60, m.width-4)
m.closeReasonInput.SetWidth(editBoxWidth - 2)
m.closeReasonInput.SetHeight(4)
editContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Enter closing reason for "+m.closeReasonIssueID+" (Enter or Ctrl+S to save, Esc to cancel):"),
m.closeReasonInput.View(),
)
editBox := styles.ContainerStyle.
Width(editBoxWidth).
BorderForeground(styles.PrimaryBorder).
Render(editContent)
return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox))
}
if m.choosingType {
typeContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Change type for "+m.typeIssueID+":"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("b = bug f = feature t = task e = epic c = chore"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"),
)
typeBoxWidth := min(65, m.width-4)
typeBox := styles.ContainerStyle.
Width(typeBoxWidth).
BorderForeground(styles.PrimaryBorder).
Render(typeContent)
return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, typeBox))
}
return tea.NewView(mainView)
return tea.NewView(m.modalManager.RenderWithMainView(mainView))
}