Allow opening the editor while creating a ticket with Ctrl+e.

Draft headline from the create input is passed into $EDITOR so title and description can be written before the ticket is saved.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Skruecken
2026-10-19 10:35:34 +02:00
co-authored by Cursor
parent 28cd2628e4
commit 6527d91b4f
3 changed files with 66 additions and 16 deletions
+47 -6
View File
@@ -21,9 +21,10 @@ import (
// editorFinishedMsg is sent after the external $EDITOR exits. // editorFinishedMsg is sent after the external $EDITOR exits.
type editorFinishedMsg struct { type editorFinishedMsg struct {
ticketID string ticketID string // empty when create==true
path string path string
err error err error
create bool // true: create ticket from edited file
} }
// Mode is the active UI mode. // Mode is the active UI mode.
@@ -374,6 +375,20 @@ func (m Model) updateInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
} }
return m, nil return m, nil
case key.Matches(msg, m.keys.EditInEditor):
if m.mode != ModeCreate || m.creatingProject {
break
}
m.clearStatus()
cmd, err := m.openEditorForNewTicket()
if err != nil {
m.setError(err)
return m, nil
}
m.input.Blur()
m.mode = ModeList
return m, cmd
case key.Matches(msg, m.keys.Enter): case key.Matches(msg, m.keys.Enter):
value := strings.TrimSpace(m.input.Value()) value := strings.TrimSpace(m.input.Value())
if m.mode == ModeCreate { if m.mode == ModeCreate {
@@ -397,7 +412,7 @@ func (m Model) updateInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.setStatus(fmt.Sprintf("created project %s", id)) m.setStatus(fmt.Sprintf("created project %s", id))
return m, nil return m, nil
} }
if err := m.createTicket(value); err != nil { if err := m.createTicket(value, ""); err != nil {
m.setError(err) m.setError(err)
return m, nil return m, nil
} }
@@ -474,6 +489,21 @@ func (m Model) openEditorForSelected() (tea.Cmd, error) {
}), nil }), nil
} }
func (m Model) openEditorForNewTicket() (tea.Cmd, error) {
title := strings.TrimSpace(m.input.Value())
if title == "" {
title = "New ticket"
}
path, err := ticket.WriteEditFile(ticket.Ticket{Title: title})
if err != nil {
return nil, err
}
c := ticket.EditorCmd(path)
return tea.ExecProcess(c, func(err error) tea.Msg {
return editorFinishedMsg{path: path, err: err, create: true}
}), nil
}
func (m Model) handleEditorFinished(msg editorFinishedMsg) (tea.Model, tea.Cmd) { func (m Model) handleEditorFinished(msg editorFinishedMsg) (tea.Model, tea.Cmd) {
defer func() { _ = os.Remove(msg.path) }() defer func() { _ = os.Remove(msg.path) }()
@@ -493,6 +523,16 @@ func (m Model) handleEditorFinished(msg editorFinishedMsg) (tea.Model, tea.Cmd)
return m, nil return m, nil
} }
if msg.create {
if err := m.createTicket(title, description); err != nil {
m.setError(err)
return m, nil
}
m.mode = ModeList
m.setStatus("created")
return m, nil
}
before, err := m.store.Get(msg.ticketID) before, err := m.store.Get(msg.ticketID)
if err != nil { if err != nil {
m.setError(err) m.setError(err)
@@ -521,13 +561,14 @@ func (m Model) handleEditorFinished(msg editorFinishedMsg) (tea.Model, tea.Cmd)
return m, nil return m, nil
} }
func (m *Model) createTicket(title string) error { func (m *Model) createTicket(title, description string) error {
cmd := &command.CreateTicket{ cmd := &command.CreateTicket{
Store: m.store, Store: m.store,
Ticket: ticket.Ticket{ Ticket: ticket.Ticket{
Title: title, Title: title,
ProjectID: m.cfg.ActiveProject, Description: description,
Status: m.boardList.DefaultStatus(), ProjectID: m.cfg.ActiveProject,
Status: m.boardList.DefaultStatus(),
}, },
} }
if err := m.exec.Do(cmd); err != nil { if err := m.exec.Do(cmd); err != nil {
+5 -1
View File
@@ -14,10 +14,14 @@ type helpEntry struct {
func (m Model) helpEntries() []helpEntry { func (m Model) helpEntries() []helpEntry {
switch m.mode { switch m.mode {
case ModeCreate: case ModeCreate:
return []helpEntry{ entries := []helpEntry{
{"enter", "save"}, {"enter", "save"},
{"esc", "cancel"}, {"esc", "cancel"},
} }
if !m.creatingProject {
entries = append([]helpEntry{{"ctrl+e", "editor"}}, entries...)
}
return entries
case ModeSearch: case ModeSearch:
return []helpEntry{ return []helpEntry{
{"enter", "apply"}, {"enter", "apply"},
+14 -9
View File
@@ -11,15 +11,16 @@ type KeyMap struct {
Project key.Binding Project key.Binding
Board key.Binding Board key.Binding
Status key.Binding Status key.Binding
Edit key.Binding Edit key.Binding
Delete key.Binding // x — confirm first EditInEditor key.Binding // ctrl+e — open $EDITOR while creating
DeleteForce key.Binding // X — immediate Delete key.Binding // x — confirm first
ConfirmYes key.Binding DeleteForce key.Binding // X — immediate
Undo key.Binding ConfirmYes key.Binding
Redo key.Binding Undo key.Binding
Quit key.Binding Redo key.Binding
Enter key.Binding Quit key.Binding
Escape key.Binding Enter key.Binding
Escape key.Binding
} }
// DefaultKeyMap returns the default key bindings. // DefaultKeyMap returns the default key bindings.
@@ -57,6 +58,10 @@ func DefaultKeyMap() KeyMap {
key.WithKeys("e"), key.WithKeys("e"),
key.WithHelp("e", "edit"), key.WithHelp("e", "edit"),
), ),
EditInEditor: key.NewBinding(
key.WithKeys("ctrl+e"),
key.WithHelp("ctrl+e", "editor"),
),
Delete: key.NewBinding( Delete: key.NewBinding(
key.WithKeys("x"), key.WithKeys("x"),
key.WithHelp("x", "delete"), key.WithHelp("x", "delete"),