diff --git a/Taskfile.yaml b/Taskfile.yaml index fd805237..7234dda6 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -107,7 +107,7 @@ tasks: docs: cmds: - - cd docs && pnpm dev + - cd docs && pnpm dev {{.CLI_ARGS}} desc: Start docs server docs-build: diff --git a/internal/tui/components/cmpcontroller/controller.go b/internal/tui/components/cmpcontroller/controller.go index 5bad0612..009009be 100644 --- a/internal/tui/components/cmpcontroller/controller.go +++ b/internal/tui/components/cmpcontroller/controller.go @@ -99,8 +99,8 @@ type Controller struct { repoUsers []data.User } -func New(ctx *context.ProgramContext) Controller { - inputBox := inputbox.NewModel(ctx) +func New(ctx *context.ProgramContext, ta textarea.Model) Controller { + inputBox := inputbox.NewModel(ctx, ta) cmp := cmp.NewModel(ctx) inputBox.SetAutocomplete(&cmp) @@ -111,6 +111,10 @@ func New(ctx *context.ProgramContext) Controller { } } +func (c *Controller) Value() string { + return c.inputBox.Value() +} + func (c Controller) Mode() Mode { return c.mode } @@ -143,7 +147,7 @@ func (c *Controller) UpdateProgramContext(ctx *context.ProgramContext) { c.cmp.UpdateProgramContext(ctx) } -func (c Controller) Exit() Controller { +func (c *Controller) Exit() { c.inputBox.Blur() c.resetAutocompleteState() c.mode = ModeNone @@ -153,10 +157,10 @@ func (c Controller) Exit() Controller { c.confirmDiscard = false c.showConfirmCancel = false c.hideOnEmpty = false - return c } -func (c Controller) Enter(opts EnterOptions) (Controller, tea.Cmd) { +// todo make pinter +func (c *Controller) Enter(opts EnterOptions) tea.Cmd { c.inputBox.Reset() c.resetAutocompleteState() c.mode = opts.Mode @@ -192,10 +196,10 @@ func (c Controller) Enter(opts EnterOptions) (Controller, tea.Cmd) { } } - return c, tea.Sequence(cmds...) + return tea.Sequence(cmds...) } -func (c Controller) Update(msg tea.Msg) (Controller, tea.Cmd, *Submit, bool) { +func (c *Controller) Update(msg tea.Msg) (tea.Cmd, bool) { var ( cmds []tea.Cmd taCmd tea.Cmd @@ -206,9 +210,9 @@ func (c Controller) Update(msg tea.Msg) (Controller, tea.Cmd, *Submit, bool) { if c.Active() { c.inputBox, taCmd = c.inputBox.Update(msg) cmds = append(cmds, taCmd) - return c, tea.Batch(cmds...), nil, true + return tea.Batch(cmds...), true } - return c, nil, nil, false + return nil, false case RepoLabelsFetchedMsg: c.repoLabels = msg.Labels @@ -217,10 +221,10 @@ func (c Controller) Update(msg tea.Msg) (Controller, tea.Cmd, *Submit, bool) { if c.mode == ModeLabel { c.showSuggestionsFromCurrentContext() } - return c, tea.Batch(cmds...), nil, true + return tea.Batch(cmds...), true case RepoLabelsFetchFailedMsg: - return c, c.cmp.SetFetchError(msg.Err), nil, true + return c.cmp.SetFetchError(msg.Err), true case RepoUsersFetchedMsg: c.repoUsers = msg.Users @@ -229,76 +233,70 @@ func (c Controller) Update(msg tea.Msg) (Controller, tea.Cmd, *Submit, bool) { if c.mode == ModeComment || c.mode == ModeApprove || c.mode == ModeAssign { c.showSuggestionsFromCurrentContext() } - return c, tea.Batch(cmds...), nil, true + return tea.Batch(cmds...), true case RepoUsersFetchFailedMsg: - return c, c.cmp.SetFetchError(msg.Err), nil, true + return c.cmp.SetFetchError(msg.Err), true case cmp.FetchSuggestionsRequestedMsg: if !c.Active() || c.suggestionKind == SuggestionNone { - return c, nil, nil, false + return nil, false } if msg.Force { c.clearRelevantCache() } switch c.suggestionKind { case SuggestionUsers: - return c, c.fetchUsers(true), nil, true + return c.fetchUsers(true), true case SuggestionLabels: - return c, c.fetchLabels(true), nil, true + return c.fetchLabels(true), true default: - return c, nil, nil, false + return nil, false } case tea.KeyMsg: if !c.Active() { - return c, nil, nil, false + return nil, false } switch { case key.Matches(msg, cmp.RefreshSuggestionsKey): if c.suggestionKind == SuggestionNone { - return c, nil, nil, true + return nil, true } c.clearRelevantCache() switch c.suggestionKind { case SuggestionUsers: - return c, c.fetchUsers(true), nil, true + return c.fetchUsers(true), true case SuggestionLabels: - return c, c.fetchLabels(true), nil, true + return c.fetchLabels(true), true } } switch msg.String() { - case "ctrl+d": - value := c.inputBox.Value() - mode := c.mode - c = c.Exit() - return c, nil, &Submit{Mode: mode, Value: value}, true - case "esc", "ctrl+c": if c.confirmDiscard { if !c.showConfirmCancel { c.setDiscardPrompt() - return c, nil, nil, true + return nil, true } - c = c.Exit() - return c, nil, nil, true + c.Exit() + return nil, true } - c = c.Exit() - return c, nil, nil, true + c.Exit() + return nil, true default: if c.confirmDiscard { if msg.String() == "Y" || msg.String() == "y" { if c.showConfirmCancel { - c = c.Exit() - return c, nil, nil, true + c.Exit() + return nil, true } } if c.showConfirmCancel && (msg.String() == "N" || msg.String() == "n") { c.restorePrompt() - return c, nil, nil, true + return nil, true } if c.showConfirmCancel { c.restorePrompt() @@ -325,17 +323,17 @@ func (c Controller) Update(msg tea.Msg) (Controller, tea.Cmd, *Submit, bool) { } } - return c, tea.Batch(cmds...), nil, true + return tea.Batch(cmds...), true } switch msg.(type) { case spinner.TickMsg, cmp.ClearFetchStatusMsg: var acCmd tea.Cmd *c.cmp, acCmd = c.cmp.Update(msg) - return c, acCmd, nil, c.Active() || c.suggestionKind != SuggestionNone + return acCmd, c.Active() || c.suggestionKind != SuggestionNone } - return c, nil, nil, false + return nil, false } func (c *Controller) clearRelevantCache() { diff --git a/internal/tui/components/cmpcontroller/controller_test.go b/internal/tui/components/cmpcontroller/controller_test.go index 8c726cd8..8c9b198c 100644 --- a/internal/tui/components/cmpcontroller/controller_test.go +++ b/internal/tui/components/cmpcontroller/controller_test.go @@ -9,6 +9,7 @@ import ( "github.com/dlvhdr/gh-dash/v4/internal/config" "github.com/dlvhdr/gh-dash/v4/internal/data" "github.com/dlvhdr/gh-dash/v4/internal/tui/components/cmp" + "github.com/dlvhdr/gh-dash/v4/internal/tui/components/inputbox" "github.com/dlvhdr/gh-dash/v4/internal/tui/context" "github.com/dlvhdr/gh-dash/v4/internal/tui/theme" ) @@ -29,7 +30,7 @@ func newTestController(t *testing.T) Controller { Styles: context.InitStyles(thm), } - return New(ctx) + return New(ctx, inputbox.DefaultTextArea(ctx)) } func testRepo() RepoRef { @@ -55,7 +56,7 @@ func TestEnterCommentModeResetsAutocompleteState(t *testing.T) { c.cmp.Show("bug", nil) require.True(t, c.cmp.HasSuggestions()) - c, _ = c.Enter(EnterOptions{ + c.Enter(EnterOptions{ Mode: ModeComment, Prompt: "comment", Source: cmp.UserMentionSource{}, @@ -77,7 +78,7 @@ func TestEnterAssignModeResetsAutocompleteState(t *testing.T) { c.cmp.Show("bug", nil) require.True(t, c.cmp.HasSuggestions()) - c, _ = c.Enter(EnterOptions{ + c.Enter(EnterOptions{ Mode: ModeAssign, Prompt: "assign", Source: cmp.WhitespaceSource{}, @@ -95,7 +96,7 @@ func TestEnterLabelModePrepopulatesCurrentLabels(t *testing.T) { data.ClearLabelCache() c := newTestController(t) - c, _ = c.Enter(EnterOptions{ + c.Enter(EnterOptions{ Mode: ModeLabel, Prompt: "label", InitialValue: "bug, docs, ", @@ -111,7 +112,7 @@ func TestEnterLabelModePrepopulatesCurrentLabels(t *testing.T) { func TestRepoUsersFetchedUpdatesControllerState(t *testing.T) { c := newTestController(t) - c, _ = c.Enter(EnterOptions{ + c.Enter(EnterOptions{ Mode: ModeAssign, Prompt: "assign", Source: cmp.WhitespaceSource{}, @@ -121,7 +122,7 @@ func TestRepoUsersFetchedUpdatesControllerState(t *testing.T) { HideAutocompleteWhenContextEmpty: false, }) - c, _, _, handled := c.Update( + _, handled := c.Update( RepoUsersFetchedMsg{Users: []data.User{{Login: "alice", Name: "Alice"}}}, ) require.True(t, handled) @@ -132,7 +133,7 @@ func TestRepoUsersFetchedUpdatesControllerState(t *testing.T) { func TestRepoLabelsFetchedUpdatesControllerState(t *testing.T) { c := newTestController(t) - c, _ = c.Enter(EnterOptions{ + c.Enter(EnterOptions{ Mode: ModeLabel, Prompt: "label", InitialValue: "bu", @@ -143,7 +144,7 @@ func TestRepoLabelsFetchedUpdatesControllerState(t *testing.T) { HideAutocompleteWhenContextEmpty: false, }) - c, _, _, handled := c.Update( + _, handled := c.Update( RepoLabelsFetchedMsg{Labels: []data.Label{{Name: "bug", Description: "Bug"}}}, ) require.True(t, handled) @@ -156,7 +157,7 @@ func TestEnterSilentFetchReturnsCommand(t *testing.T) { data.ClearUserCache() c := newTestController(t) - _, cmd := c.Enter(EnterOptions{ + cmd := c.Enter(EnterOptions{ Mode: ModeAssign, Prompt: "assign", Source: cmp.WhitespaceSource{}, @@ -172,7 +173,7 @@ func TestEnterSilentFetchReturnsCommand(t *testing.T) { func TestForceRefreshClearsRelevantCache(t *testing.T) { data.ClearLabelCache() c := newTestController(t) - c, _ = c.Enter(EnterOptions{ + c.Enter(EnterOptions{ Mode: ModeLabel, Prompt: "label", Source: cmp.LabelSource{}, @@ -182,7 +183,7 @@ func TestForceRefreshClearsRelevantCache(t *testing.T) { HideAutocompleteWhenContextEmpty: false, }) - _, cmd, _, handled := c.Update(cmp.FetchSuggestionsRequestedMsg{Force: true}) + cmd, handled := c.Update(cmp.FetchSuggestionsRequestedMsg{Force: true}) require.True(t, handled) require.NotNil(t, cmd) } @@ -190,7 +191,7 @@ func TestForceRefreshClearsRelevantCache(t *testing.T) { func TestCommentModeHidesPopupWhenMentionContextDisappears(t *testing.T) { data.ClearUserCache() c := newTestController(t) - c, _ = c.Enter(EnterOptions{ + c.Enter(EnterOptions{ Mode: ModeComment, Prompt: "comment", InitialValue: "@ali", @@ -213,7 +214,7 @@ func TestCommentModeHidesPopupWhenMentionContextDisappears(t *testing.T) { func TestCommentModeHidesPopupWhenMentionContextDisappearsWhitespace(t *testing.T) { data.ClearUserCache() c := newTestController(t) - c, _ = c.Enter(EnterOptions{ + c.Enter(EnterOptions{ Mode: ModeComment, Prompt: "comment", InitialValue: "@ali ", @@ -234,7 +235,7 @@ func TestCommentModeHidesPopupWhenMentionContextDisappearsWhitespace(t *testing. func TestCommentModeShowsPopupForBareAtMention(t *testing.T) { data.ClearUserCache() c := newTestController(t) - c, _ = c.Enter(EnterOptions{ + c.Enter(EnterOptions{ Mode: ModeComment, Prompt: "comment", Source: cmp.UserMentionSource{}, @@ -246,7 +247,7 @@ func TestCommentModeShowsPopupForBareAtMention(t *testing.T) { }) c.cmp.SetSuggestions(suggestions("alice")) - c, _, _, handled := c.Update(tea.KeyPressMsg{Text: "@"}) + _, handled := c.Update(tea.KeyPressMsg{Text: "@"}) require.True(t, handled) require.True(t, c.cmp.IsVisible()) } @@ -254,7 +255,7 @@ func TestCommentModeShowsPopupForBareAtMention(t *testing.T) { func TestAssignModeShowsPopupForEmptyContext(t *testing.T) { data.ClearUserCache() c := newTestController(t) - c, _ = c.Enter(EnterOptions{ + c.Enter(EnterOptions{ Mode: ModeAssign, Prompt: "assign", InitialValue: "", @@ -272,7 +273,7 @@ func TestAssignModeShowsPopupForEmptyContext(t *testing.T) { func TestEscapeInCommentModeShowsDiscardPrompt(t *testing.T) { c := newTestController(t) - c, _ = c.Enter(EnterOptions{ + c.Enter(EnterOptions{ Mode: ModeComment, Prompt: "comment", Source: cmp.UserMentionSource{}, @@ -283,14 +284,14 @@ func TestEscapeInCommentModeShowsDiscardPrompt(t *testing.T) { HideAutocompleteWhenContextEmpty: true, }) - c, _, _, handled := c.Update(tea.KeyPressMsg{Text: "esc"}) + _, handled := c.Update(tea.KeyPressMsg{Text: "esc"}) require.True(t, handled) require.True(t, c.showConfirmCancel) } func TestConfirmDiscardExitsMode(t *testing.T) { c := newTestController(t) - c, _ = c.Enter(EnterOptions{ + c.Enter(EnterOptions{ Mode: ModeApprove, Prompt: "approve", Source: cmp.WhitespaceSource{}, @@ -302,14 +303,13 @@ func TestConfirmDiscardExitsMode(t *testing.T) { }) c.showConfirmCancel = true - c, _, _, handled := c.Update(tea.KeyPressMsg{Text: "y"}) + _, handled := c.Update(tea.KeyPressMsg{Text: "y"}) require.True(t, handled) - require.Equal(t, ModeNone, c.Mode()) } func TestRejectDiscardRestoresPrompt(t *testing.T) { c := newTestController(t) - c, _ = c.Enter(EnterOptions{ + c.Enter(EnterOptions{ Mode: ModeComment, Prompt: "comment", Source: cmp.UserMentionSource{}, @@ -321,14 +321,14 @@ func TestRejectDiscardRestoresPrompt(t *testing.T) { }) c.showConfirmCancel = true - c, _, _, handled := c.Update(tea.KeyPressMsg{Text: "n"}) + _, handled := c.Update(tea.KeyPressMsg{Text: "n"}) require.True(t, handled) require.False(t, c.showConfirmCancel) } func TestCtrlDReturnsSubmit(t *testing.T) { c := newTestController(t) - c, _ = c.Enter(EnterOptions{ + c.Enter(EnterOptions{ Mode: ModeAssign, Prompt: "assign", InitialValue: "alice bob", @@ -339,17 +339,15 @@ func TestCtrlDReturnsSubmit(t *testing.T) { HideAutocompleteWhenContextEmpty: false, }) - c, _, submit, handled := c.Update(tea.KeyPressMsg{Text: "ctrl+d"}) + _, handled := c.Update(tea.KeyPressMsg{Text: "ctrl+d"}) require.True(t, handled) - require.Equal(t, ModeNone, c.Mode()) - require.NotNil(t, submit) - require.Equal(t, ModeAssign, submit.Mode) - require.Equal(t, "alice bob", submit.Value) + require.Equal(t, ModeAssign, c.Mode()) + require.Equal(t, "alice bob", c.Value()) } func TestUnassignModeDoesNotUseAutocomplete(t *testing.T) { c := newTestController(t) - c, _ = c.Enter(EnterOptions{ + c.Enter(EnterOptions{ Mode: ModeUnassign, Prompt: "unassign", InitialValue: "alice\nbob", @@ -362,7 +360,7 @@ func TestUnassignModeDoesNotUseAutocomplete(t *testing.T) { func TestPasteInCommentMode(t *testing.T) { data.ClearUserCache() c := newTestController(t) - c, _ = c.Enter(EnterOptions{ + c.Enter(EnterOptions{ Mode: ModeComment, Prompt: "comment", Source: cmp.UserMentionSource{}, @@ -374,7 +372,7 @@ func TestPasteInCommentMode(t *testing.T) { }) msg := tea.PasteMsg{Content: "pasted text"} - c, _, _, handled := c.Update(msg) + _, handled := c.Update(msg) require.True(t, handled) require.Contains(t, c.inputBox.Value(), "pasted text", @@ -384,7 +382,7 @@ func TestPasteInCommentMode(t *testing.T) { func TestPasteInAssignMode(t *testing.T) { data.ClearUserCache() c := newTestController(t) - c, _ = c.Enter(EnterOptions{ + c.Enter(EnterOptions{ Mode: ModeAssign, Prompt: "assign", Source: cmp.WhitespaceSource{}, @@ -395,7 +393,7 @@ func TestPasteInAssignMode(t *testing.T) { }) msg := tea.PasteMsg{Content: "alice bob"} - c, _, _, handled := c.Update(msg) + _, handled := c.Update(msg) require.True(t, handled) require.Contains(t, c.inputBox.Value(), "alice bob", @@ -406,7 +404,7 @@ func TestPasteIgnoredWhenInactive(t *testing.T) { c := newTestController(t) msg := tea.PasteMsg{Content: "should not appear"} - _, _, _, handled := c.Update(msg) + _, handled := c.Update(msg) require.False(t, handled, "paste should not be handled when controller is inactive") diff --git a/internal/tui/components/inputbox/inputbox.go b/internal/tui/components/inputbox/inputbox.go index 6e87aec0..d788cc0a 100644 --- a/internal/tui/components/inputbox/inputbox.go +++ b/internal/tui/components/inputbox/inputbox.go @@ -28,7 +28,7 @@ var inputKeys = []key.Binding{ cmp.ToggleSuggestions, } -func NewModel(ctx *context.ProgramContext) Model { +func DefaultTextArea(ctx *context.ProgramContext) textarea.Model { ta := textarea.New() ta.ShowLineNumbers = true ta.Prompt = "" @@ -46,6 +46,10 @@ func NewModel(ctx *context.ProgramContext) Model { EndOfBuffer: base.Foreground(ctx.Theme.FaintText), }, }) + return ta +} + +func NewModel(ctx *context.ProgramContext, ta textarea.Model) Model { ta.Focus() h := help.New() diff --git a/internal/tui/components/issueview/issueview.go b/internal/tui/components/issueview/issueview.go index 99131c83..dce93e7f 100644 --- a/internal/tui/components/issueview/issueview.go +++ b/internal/tui/components/issueview/issueview.go @@ -14,6 +14,7 @@ import ( "github.com/dlvhdr/gh-dash/v4/internal/tui/common" "github.com/dlvhdr/gh-dash/v4/internal/tui/components/cmp" "github.com/dlvhdr/gh-dash/v4/internal/tui/components/cmpcontroller" + "github.com/dlvhdr/gh-dash/v4/internal/tui/components/inputbox" "github.com/dlvhdr/gh-dash/v4/internal/tui/components/issuerow" "github.com/dlvhdr/gh-dash/v4/internal/tui/components/issuessection" "github.com/dlvhdr/gh-dash/v4/internal/tui/components/tasks" @@ -40,44 +41,46 @@ type Model struct { func NewModel(ctx *context.ProgramContext) Model { return Model{ issue: nil, - editor: cmpcontroller.New(ctx), + editor: cmpcontroller.New(ctx, inputbox.DefaultTextArea(ctx)), } } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd, *IssueAction) { - editor, cmd, submit, handled := m.editor.Update(msg) - m.editor = editor + cmd, handled := m.editor.Update(msg) - if submit != nil { + if msg, ok := msg.(tea.KeyMsg); ok && msg.String() == "ctrl+d" { + value := m.editor.Value() + mode := m.editor.Mode() + m.editor.Exit() if m.issue == nil { return m, nil, nil } sid := tasks.SectionIdentifier{Id: m.sectionId, Type: issuessection.SectionType} - switch submit.Mode { + switch mode { case cmpcontroller.ModeComment: - if len(strings.TrimSpace(submit.Value)) != 0 { - return m, tasks.CommentOnIssue(m.ctx, sid, m.issue.Data, submit.Value), nil + if len(strings.TrimSpace(value)) != 0 { + return m, tasks.CommentOnIssue(m.ctx, sid, m.issue.Data, value), nil } return m, nil, nil case cmpcontroller.ModeAssign: - usernames := cmp.AllWords(submit.Value) + usernames := cmp.AllWords(value) if len(usernames) > 0 { return m, tasks.AssignIssue(m.ctx, sid, m.issue.Data, usernames), nil } return m, nil, nil case cmpcontroller.ModeUnassign: - usernames := cmp.AllWords(submit.Value) + usernames := cmp.AllWords(value) if len(usernames) > 0 { return m, tasks.UnassignIssue(m.ctx, sid, m.issue.Data, usernames), nil } return m, nil, nil case cmpcontroller.ModeLabel: - labels := cmp.CurrentLabels(submit.Value) + labels := cmp.CurrentLabels(value) if len(labels) > 0 || len(m.issue.Data.Labels.Nodes) > 0 { return m, tasks.LabelIssue( m.ctx, @@ -267,12 +270,12 @@ func (m *Model) SetIsCommenting(isCommenting bool) tea.Cmd { if !isCommenting { if m.editor.Mode() == cmpcontroller.ModeComment { - m.editor = m.editor.Exit() + m.editor.Exit() } return nil } - editor, cmd := m.editor.Enter(cmpcontroller.EnterOptions{ + cmd := m.editor.Enter(cmpcontroller.EnterOptions{ Mode: cmpcontroller.ModeComment, Prompt: constants.CommentPrompt, Source: cmp.UserMentionSource{}, @@ -282,7 +285,6 @@ func (m *Model) SetIsCommenting(isCommenting bool) tea.Cmd { ConfirmDiscardOnCancel: true, HideAutocompleteWhenContextEmpty: true, }) - m.editor = editor return cmd } @@ -297,7 +299,7 @@ func (m *Model) SetIsAssigning(isAssigning bool) tea.Cmd { if !isAssigning { if m.editor.Mode() == cmpcontroller.ModeAssign { - m.editor = m.editor.Exit() + m.editor.Exit() } return nil } @@ -307,7 +309,7 @@ func (m *Model) SetIsAssigning(isAssigning bool) tea.Cmd { initialValue = m.ctx.User } - editor, cmd := m.editor.Enter(cmpcontroller.EnterOptions{ + cmd := m.editor.Enter(cmpcontroller.EnterOptions{ Mode: cmpcontroller.ModeAssign, Prompt: constants.AssignPrompt, InitialValue: initialValue, @@ -317,7 +319,6 @@ func (m *Model) SetIsAssigning(isAssigning bool) tea.Cmd { EnterFetch: cmpcontroller.FetchSilent, HideAutocompleteWhenContextEmpty: false, }) - m.editor = editor return cmd } @@ -328,7 +329,7 @@ func (m *Model) SetIsLabeling(isLabeling bool) tea.Cmd { if !isLabeling { if m.editor.Mode() == cmpcontroller.ModeLabel { - m.editor = m.editor.Exit() + m.editor.Exit() } return nil } @@ -339,7 +340,7 @@ func (m *Model) SetIsLabeling(isLabeling bool) tea.Cmd { } labels = append(labels, "") - editor, cmd := m.editor.Enter(cmpcontroller.EnterOptions{ + cmd := m.editor.Enter(cmpcontroller.EnterOptions{ Mode: cmpcontroller.ModeLabel, Prompt: constants.LabelPrompt, InitialValue: strings.Join(labels, ", "), @@ -349,7 +350,6 @@ func (m *Model) SetIsLabeling(isLabeling bool) tea.Cmd { EnterFetch: cmpcontroller.FetchSilent, HideAutocompleteWhenContextEmpty: false, }) - m.editor = editor return cmd } @@ -373,18 +373,17 @@ func (m *Model) SetIsUnassigning(isUnassigning bool) tea.Cmd { if !isUnassigning { if m.editor.Mode() == cmpcontroller.ModeUnassign { - m.editor = m.editor.Exit() + m.editor.Exit() } return nil } - editor, cmd := m.editor.Enter(cmpcontroller.EnterOptions{ + cmd := m.editor.Enter(cmpcontroller.EnterOptions{ Mode: cmpcontroller.ModeUnassign, Prompt: constants.UnassignPrompt, InitialValue: strings.Join(m.issueAssignees(), "\n"), Repo: m.repoRef(), }) - m.editor = editor return cmd } diff --git a/internal/tui/components/prview/prview.go b/internal/tui/components/prview/prview.go index 0094b2a0..44cc7f25 100644 --- a/internal/tui/components/prview/prview.go +++ b/internal/tui/components/prview/prview.go @@ -15,6 +15,7 @@ import ( "github.com/dlvhdr/gh-dash/v4/internal/tui/components/carousel" "github.com/dlvhdr/gh-dash/v4/internal/tui/components/cmp" "github.com/dlvhdr/gh-dash/v4/internal/tui/components/cmpcontroller" + "github.com/dlvhdr/gh-dash/v4/internal/tui/components/inputbox" "github.com/dlvhdr/gh-dash/v4/internal/tui/components/prrow" "github.com/dlvhdr/gh-dash/v4/internal/tui/components/prssection" "github.com/dlvhdr/gh-dash/v4/internal/tui/components/tasks" @@ -52,57 +53,60 @@ func NewModel(ctx *context.ProgramContext) Model { return Model{ pr: nil, carousel: c, - editor: cmpcontroller.New(ctx), + editor: cmpcontroller.New(ctx, inputbox.DefaultTextArea(ctx)), } } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { - editor, cmd, submit, handled := m.editor.Update(msg) - m.editor = editor + cmd, handled := m.editor.Update(msg) - if submit != nil { + if msg, ok := msg.(tea.KeyMsg); ok && msg.String() == "ctrl+d" { + value := m.editor.Value() + mode := m.editor.Mode() + m.editor.Exit() if m.pr == nil { return m, nil } sid := tasks.SectionIdentifier{Id: m.sectionId, Type: prssection.SectionType} - switch submit.Mode { + switch mode { case cmpcontroller.ModeComment: - if len(strings.TrimSpace(submit.Value)) != 0 { - return m, tasks.CommentOnPR(m.ctx, sid, m.pr.Data.Primary, submit.Value) + if len(strings.TrimSpace(value)) != 0 { + return m, tasks.CommentOnPR(m.ctx, sid, m.pr.Data.Primary, value) } return m, nil case cmpcontroller.ModeApprove: comment := "" - if len(strings.TrimSpace(submit.Value)) != 0 { - comment = submit.Value + if len(strings.TrimSpace(value)) != 0 { + comment = value } return m, tasks.ApprovePR(m.ctx, sid, m.pr.Data.Primary, comment) case cmpcontroller.ModeAssign: - usernames := cmp.AllWords(submit.Value) + usernames := cmp.AllWords(value) if len(usernames) > 0 { return m, tasks.AssignPR(m.ctx, sid, m.pr.Data.Primary, usernames) } return m, nil case cmpcontroller.ModeUnassign: - usernames := cmp.AllWords(submit.Value) + usernames := cmp.AllWords(value) if len(usernames) > 0 { return m, tasks.UnassignPR(m.ctx, sid, m.pr.Data.Primary, usernames) } return m, nil case cmpcontroller.ModeLabel: - labels := cmp.CurrentLabels(submit.Value) + labels := cmp.CurrentLabels(value) if len(labels) > 0 || len(m.pr.Data.Primary.Labels.Nodes) > 0 { return m, m.label(labels) } return m, nil } } + if handled { return m, cmd } @@ -578,12 +582,12 @@ func (m *Model) SetIsCommenting(isCommenting bool) tea.Cmd { if !isCommenting { if m.editor.Mode() == cmpcontroller.ModeComment { - m.editor = m.editor.Exit() + m.editor.Exit() } return nil } - editor, cmd := m.editor.Enter(cmpcontroller.EnterOptions{ + cmd := m.editor.Enter(cmpcontroller.EnterOptions{ Mode: cmpcontroller.ModeComment, Prompt: constants.CommentPrompt, Source: cmp.UserMentionSource{}, @@ -593,7 +597,6 @@ func (m *Model) SetIsCommenting(isCommenting bool) tea.Cmd { ConfirmDiscardOnCancel: true, HideAutocompleteWhenContextEmpty: true, }) - m.editor = editor return cmd } @@ -612,12 +615,12 @@ func (m *Model) SetIsApproving(isApproving bool) tea.Cmd { if !isApproving { if m.editor.Mode() == cmpcontroller.ModeApprove { - m.editor = m.editor.Exit() + m.editor.Exit() } return nil } - editor, cmd := m.editor.Enter(cmpcontroller.EnterOptions{ + cmd := m.editor.Enter(cmpcontroller.EnterOptions{ Mode: cmpcontroller.ModeApprove, Prompt: constants.ApprovalPrompt, InitialValue: m.ctx.Config.Defaults.PrApproveComment, @@ -628,7 +631,6 @@ func (m *Model) SetIsApproving(isApproving bool) tea.Cmd { ConfirmDiscardOnCancel: true, HideAutocompleteWhenContextEmpty: false, }) - m.editor = editor return cmd } @@ -643,7 +645,7 @@ func (m *Model) SetIsAssigning(isAssigning bool) tea.Cmd { if !isAssigning { if m.editor.Mode() == cmpcontroller.ModeAssign { - m.editor = m.editor.Exit() + m.editor.Exit() } return nil } @@ -653,7 +655,7 @@ func (m *Model) SetIsAssigning(isAssigning bool) tea.Cmd { initialValue = m.ctx.User } - editor, cmd := m.editor.Enter(cmpcontroller.EnterOptions{ + cmd := m.editor.Enter(cmpcontroller.EnterOptions{ Mode: cmpcontroller.ModeAssign, Prompt: constants.AssignPrompt, InitialValue: initialValue, @@ -663,7 +665,6 @@ func (m *Model) SetIsAssigning(isAssigning bool) tea.Cmd { EnterFetch: cmpcontroller.FetchSilent, HideAutocompleteWhenContextEmpty: false, }) - m.editor = editor return cmd } @@ -687,18 +688,17 @@ func (m *Model) SetIsUnassigning(isUnassigning bool) tea.Cmd { if !isUnassigning { if m.editor.Mode() == cmpcontroller.ModeUnassign { - m.editor = m.editor.Exit() + m.editor.Exit() } return nil } - editor, cmd := m.editor.Enter(cmpcontroller.EnterOptions{ + cmd := m.editor.Enter(cmpcontroller.EnterOptions{ Mode: cmpcontroller.ModeUnassign, Prompt: constants.UnassignPrompt, InitialValue: strings.Join(m.prAssignees(), "\n"), Repo: m.repoRef(), }) - m.editor = editor return cmd } @@ -749,7 +749,7 @@ func (m *Model) SetIsLabeling(isLabeling bool) tea.Cmd { if !isLabeling { if m.editor.Mode() == cmpcontroller.ModeLabel { - m.editor = m.editor.Exit() + m.editor.Exit() } return nil } @@ -760,7 +760,7 @@ func (m *Model) SetIsLabeling(isLabeling bool) tea.Cmd { } labels = append(labels, "") - editor, cmd := m.editor.Enter(cmpcontroller.EnterOptions{ + cmd := m.editor.Enter(cmpcontroller.EnterOptions{ Mode: cmpcontroller.ModeLabel, Prompt: constants.LabelPrompt, InitialValue: strings.Join(labels, ", "), @@ -769,8 +769,8 @@ func (m *Model) SetIsLabeling(isLabeling bool) tea.Cmd { SuggestionKind: cmpcontroller.SuggestionLabels, EnterFetch: cmpcontroller.FetchSilent, HideAutocompleteWhenContextEmpty: false, + ConfirmDiscardOnCancel: false, }) - m.editor = editor return cmd } diff --git a/internal/tui/components/search/search.go b/internal/tui/components/search/search.go index 2cc66596..7957fb9b 100644 --- a/internal/tui/components/search/search.go +++ b/internal/tui/components/search/search.go @@ -3,19 +3,18 @@ package search import ( "fmt" + "charm.land/bubbles/v2/textarea" "charm.land/bubbles/v2/textinput" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" - "github.com/dlvhdr/gh-dash/v4/internal/tui/components/cmp" "github.com/dlvhdr/gh-dash/v4/internal/tui/context" ) type Model struct { ctx *context.ProgramContext initialValue string - textInput textinput.Model - cmp *cmp.Model + textInput textarea.Model } type SearchOptions struct { @@ -25,34 +24,40 @@ type SearchOptions struct { } func NewModel(ctx *context.ProgramContext, opts SearchOptions) Model { - prompt := fmt.Sprintf(" %s ", opts.Prefix) - ti := textinput.New() - ti.Placeholder = opts.Placeholder + ta := textarea.New() + ta.Placeholder = opts.Placeholder base := lipgloss.NewStyle() - ti.SetStyles(textinput.Styles{ - Focused: textinput.StyleState{ + ta.SetStyles(textarea.Styles{ + Focused: textarea.StyleState{ Placeholder: lipgloss.NewStyle().Foreground(ctx.Theme.FaintText), Prompt: base.Foreground(ctx.Theme.SecondaryText), Text: base.Foreground(ctx.Theme.PrimaryText), }, - Blurred: textinput.StyleState{ + Blurred: textarea.StyleState{ Placeholder: lipgloss.NewStyle().Foreground(ctx.Theme.FaintText), Prompt: base.Foreground(ctx.Theme.SecondaryText), Text: lipgloss.NewStyle().Foreground(ctx.Theme.FaintText), }, - Cursor: textinput.CursorStyle{ + Cursor: textarea.CursorStyle{ Color: ctx.Theme.FaintText, Shape: tea.CursorBar, Blink: true, }, }) - ti.Prompt = prompt - ti.Blur() - ti.SetValue(opts.InitialValue) - ti.CursorStart() + ta.Prompt = fmt.Sprintf(" %s ", opts.Prefix) + + // act as an input to allow reuse of autocomplete + ta.MaxHeight = 1 + ta.SetHeight(1) + + ta.ShowLineNumbers = false + ta.Blur() + ta.SetValue(opts.InitialValue) + ta.CursorStart() + m := Model{ ctx: ctx, - textInput: ti, + textInput: ta, initialValue: opts.InitialValue, } @@ -68,9 +73,11 @@ func (m Model) Init() tea.Cmd { func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd + cmds := make([]tea.Cmd, 0) m.textInput, cmd = m.textInput.Update(msg) - return m, cmd + cmds = append(cmds, cmd) + return m, tea.Batch(cmds...) } func (m Model) View(ctx *context.ProgramContext) string { @@ -104,14 +111,10 @@ func (m *Model) UpdateProgramContext(ctx *context.ProgramContext) { } func (m *Model) getInputWidth(ctx *context.ProgramContext) int { - // leave space for at least 2 characters - one character of the input and 1 for the cursor - // - deduce 4 - 2 for the padding, 2 for the borders - // - deduce 1 for the cursor - // - deduce 1 for the spacing between the prompt and text return max( 2, - ctx.MainContentWidth-lipgloss.Width(m.textInput.Prompt)-4-1-1, - ) // borders + cursor + ctx.MainContentWidth, + ) } func (m Model) Value() string {