|
@@ -1,6 +1,6 @@
|
|
|
// The package defines an extensible TUI via the bubbletea framework.
|
|
// The package defines an extensible TUI via the bubbletea framework.
|
|
|
//
|
|
//
|
|
|
-// TODO enable collection recursing (i.e, embeded collections)
|
|
|
|
|
|
|
+// TODO enable collection recursing (i.e, embedded collections)
|
|
|
//
|
|
//
|
|
|
// TODO enable scroll/viewport logic
|
|
// TODO enable scroll/viewport logic
|
|
|
//
|
|
//
|
|
@@ -157,14 +157,12 @@ type widget interface {
|
|
|
keyhelp() string // renders key usage
|
|
keyhelp() string // renders key usage
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// -------- creatIssue definitions --------------------------------------------
|
|
|
|
|
|
|
+// -------- create widget definitions -----------------------------------------
|
|
|
// ----------------------------------------------------------------------------
|
|
// ----------------------------------------------------------------------------
|
|
|
-// TODO invoke editor for descriptions
|
|
|
|
|
-// TODO handle reset on esc
|
|
|
|
|
-// TODO handle errors and display gracefully
|
|
|
|
|
-// TODO implement description field in create.create
|
|
|
|
|
|
|
+// TODO(create widget) handle reset on esc
|
|
|
|
|
+// TODO(create widget) implement description field in create.create
|
|
|
|
|
|
|
|
-// data struct for createIssue
|
|
|
|
|
|
|
+// data struct for create widget
|
|
|
type inputField struct {
|
|
type inputField struct {
|
|
|
input textinput.Model
|
|
input textinput.Model
|
|
|
title string
|
|
title string
|
|
@@ -175,11 +173,11 @@ type create struct {
|
|
|
inputFields []inputField
|
|
inputFields []inputField
|
|
|
Path string
|
|
Path string
|
|
|
selected int
|
|
selected int
|
|
|
- Err error // not implemented
|
|
|
|
|
|
|
+ err error // not implemented
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// constructor for create widget
|
|
// constructor for create widget
|
|
|
-func initialCreateModel(path string, placeholder string) create {
|
|
|
|
|
|
|
+func initialCreateWidget(path string, placeholder string) create {
|
|
|
spawnInput := func(f bool) textinput.Model {
|
|
spawnInput := func(f bool) textinput.Model {
|
|
|
ti := textinput.New()
|
|
ti := textinput.New()
|
|
|
ti.Placeholder = placeholder
|
|
ti.Placeholder = placeholder
|
|
@@ -217,7 +215,7 @@ func initialCreateModel(path string, placeholder string) create {
|
|
|
inputFields: inputs,
|
|
inputFields: inputs,
|
|
|
Path: path,
|
|
Path: path,
|
|
|
selected: 0,
|
|
selected: 0,
|
|
|
- Err: nil,
|
|
|
|
|
|
|
+ err: nil,
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -226,12 +224,21 @@ func (c create) init() tea.Cmd {
|
|
|
return textinput.Blink
|
|
return textinput.Blink
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+type ( // Type definitions for use in tea.Msg life cycle for create widget.
|
|
|
|
|
+ createResult Issue // type wrapper for create.create() result
|
|
|
|
|
+ writeResult any // type wrapper for create.write() result
|
|
|
|
|
+ editorResult struct { // type wrapper for create.editor() result
|
|
|
|
|
+ err error // any error returned by InvokeEditor
|
|
|
|
|
+ issue Issue // the data in the template file
|
|
|
|
|
+ }
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
// update cmd for create widget
|
|
// update cmd for create widget
|
|
|
func (c create) update(msg tea.Msg) (create, tea.Cmd) {
|
|
func (c create) update(msg tea.Msg) (create, tea.Cmd) {
|
|
|
var cmds []tea.Cmd
|
|
var cmds []tea.Cmd
|
|
|
var cmd tea.Cmd
|
|
var cmd tea.Cmd
|
|
|
|
|
|
|
|
- // simple anon funcs to increment the selected index
|
|
|
|
|
|
|
+ // simple anon functions to increment the selected index
|
|
|
incrementSelected := func() {
|
|
incrementSelected := func() {
|
|
|
if c.selected < len(c.inputFields) {
|
|
if c.selected < len(c.inputFields) {
|
|
|
c.selected++
|
|
c.selected++
|
|
@@ -285,7 +292,13 @@ func (c create) update(msg tea.Msg) (create, tea.Cmd) {
|
|
|
cmds = append(cmds, tea.Quit)
|
|
cmds = append(cmds, tea.Quit)
|
|
|
}
|
|
}
|
|
|
case createResult:
|
|
case createResult:
|
|
|
- cmds = append(cmds, c.write(Issue(msg)))
|
|
|
|
|
|
|
+ cmds = append(cmds, c.editDescription(Issue(msg)))
|
|
|
|
|
+ case editorResult:
|
|
|
|
|
+ if msg.err != nil {
|
|
|
|
|
+ c.err = msg.err
|
|
|
|
|
+ } else {
|
|
|
|
|
+ cmds = append(cmds, c.write(msg.issue))
|
|
|
|
|
+ }
|
|
|
case writeResult:
|
|
case writeResult:
|
|
|
switch value := msg.(type) {
|
|
switch value := msg.(type) {
|
|
|
case bool:
|
|
case bool:
|
|
@@ -294,7 +307,7 @@ func (c create) update(msg tea.Msg) (create, tea.Cmd) {
|
|
|
cmds = append(cmds, tea.Quit)
|
|
cmds = append(cmds, tea.Quit)
|
|
|
}
|
|
}
|
|
|
case error:
|
|
case error:
|
|
|
- panic(value)
|
|
|
|
|
|
|
+ c.err = value
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -307,8 +320,95 @@ func (c create) update(msg tea.Msg) (create, tea.Cmd) {
|
|
|
return c, tea.Batch(cmds...)
|
|
return c, tea.Batch(cmds...)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// A tea.Cmd to translate create.inputs to a new Issue object
|
|
|
|
|
+func (c create) create() tea.Msg {
|
|
|
|
|
+ data := make(map[string]string)
|
|
|
|
|
+ commaSplit := func(t string) []string {
|
|
|
|
|
+ s := strings.Split(t, ",")
|
|
|
|
|
+ for i, v := range s {
|
|
|
|
|
+ s[i] = strings.TrimLeft(v, " \t\n")
|
|
|
|
|
+ s[i] = strings.TrimRight(s[i], " \t\n")
|
|
|
|
|
+
|
|
|
|
|
+ s[i] = parseHumanToPath(s[i])
|
|
|
|
|
+ }
|
|
|
|
|
+ return s
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for _, field := range c.inputFields {
|
|
|
|
|
+ data[field.title] = field.input.Value()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var newIssue = Issue{
|
|
|
|
|
+ Path: c.Path,
|
|
|
|
|
+ Tags: VariadicField{Path: "/tags"},
|
|
|
|
|
+ Blockedby: VariadicField{Path: "/blockedby"},
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for key, value := range data {
|
|
|
|
|
+ switch key {
|
|
|
|
|
+ case "title":
|
|
|
|
|
+ newIssue.Title = value
|
|
|
|
|
+ if parsePathToHuman(newIssue.Path) != value {
|
|
|
|
|
+ dir, _ := filepath.Split(newIssue.Path)
|
|
|
|
|
+ newIssue.Path = filepath.Join(dir, value)
|
|
|
|
|
+ }
|
|
|
|
|
+ case "status":
|
|
|
|
|
+ newIssue.Status = Field{Path: "/status", Data: value}
|
|
|
|
|
+ case "description":
|
|
|
|
|
+ newIssue.Description = Field{Path: "/description", Data: value}
|
|
|
|
|
+ case "tags":
|
|
|
|
|
+ splitTags := commaSplit(value)
|
|
|
|
|
+ for _, tag := range splitTags {
|
|
|
|
|
+ newIssue.Tags.Fields = append(newIssue.Tags.Fields, Field{Path: tag})
|
|
|
|
|
+ }
|
|
|
|
|
+ case "blockers":
|
|
|
|
|
+ splitBlockedby := commaSplit(value)
|
|
|
|
|
+ for _, blocker := range splitBlockedby {
|
|
|
|
|
+ newIssue.Blockedby.Fields = append(
|
|
|
|
|
+ newIssue.Blockedby.Fields, Field{Path: blocker},
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return createResult(newIssue)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Wraps a tea.Cmd function, passes an initialized Issue to WriteIssue()
|
|
|
|
|
+func (c create) write(issue Issue) tea.Cmd {
|
|
|
|
|
+ return func() tea.Msg {
|
|
|
|
|
+ result, err := WriteIssue(issue, false)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return writeResult(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ return writeResult(result)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Calls InvokeEditor via EditTemplate using DescriptionTemplate, reports output
|
|
|
|
|
+// and errors
|
|
|
|
|
+//
|
|
|
|
|
+// WARNING! THIS METHOD HANGS UNTIL THE USER KILLS THE EDITOR!
|
|
|
|
|
+func (c create) editDescription(issue Issue) tea.Cmd {
|
|
|
|
|
+ data, err := EditTemplate(DescriptionTemplate, InvokeEditor)
|
|
|
|
|
+ var output string
|
|
|
|
|
+ for _, line := range data {
|
|
|
|
|
+ output = output + line
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ issue.Description = Field{Path: "/description", Data: output}
|
|
|
|
|
+
|
|
|
|
|
+ return func() tea.Msg {
|
|
|
|
|
+ return editorResult{issue: issue, err: err}
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// render cmd for create widget
|
|
// render cmd for create widget
|
|
|
func (c create) render() tea.Msg {
|
|
func (c create) render() tea.Msg {
|
|
|
|
|
+ if c.err != nil {
|
|
|
|
|
+ return fmt.Sprintf("failed to create issue... %s", c.err.Error())
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
borderStyle := lipgloss.NewStyle().
|
|
borderStyle := lipgloss.NewStyle().
|
|
|
BorderStyle(lipgloss.NormalBorder()).
|
|
BorderStyle(lipgloss.NormalBorder()).
|
|
|
Margin(1).
|
|
Margin(1).
|
|
@@ -335,10 +435,11 @@ func (c create) render() tea.Msg {
|
|
|
confirmPrompt := fmt.Sprintf(
|
|
confirmPrompt := fmt.Sprintf(
|
|
|
"create issue titled \"%s\"?\n\n%s",
|
|
"create issue titled \"%s\"?\n\n%s",
|
|
|
ulStyle.Render(c.inputFields[0].input.Value()),
|
|
ulStyle.Render(c.inputFields[0].input.Value()),
|
|
|
- ulStyle.Render("press enter to confirm..."),
|
|
|
|
|
|
|
+ ulStyle.Render("press enter to write description..."),
|
|
|
)
|
|
)
|
|
|
output = output + borderStyle.Render(confirmPrompt)
|
|
output = output + borderStyle.Render(confirmPrompt)
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
return output
|
|
return output
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -456,76 +557,5 @@ func (m Model) load() tea.Msg {
|
|
|
return collection
|
|
return collection
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return initialCreateModel(m.Path, "lorem ipsum")
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// type wrapper for create.create() result
|
|
|
|
|
-type createResult Issue
|
|
|
|
|
-
|
|
|
|
|
-// A tea.Cmd to translate create.inputs to a new Issue object
|
|
|
|
|
-func (c create) create() tea.Msg {
|
|
|
|
|
- data := make(map[string]string)
|
|
|
|
|
- commaSplit := func(t string) []string {
|
|
|
|
|
- s := strings.Split(t, ",")
|
|
|
|
|
- for i, v := range s {
|
|
|
|
|
- s[i] = strings.TrimLeft(v, " \t\n")
|
|
|
|
|
- s[i] = strings.TrimRight(s[i], " \t\n")
|
|
|
|
|
-
|
|
|
|
|
- s[i] = parseHumanToPath(s[i])
|
|
|
|
|
- }
|
|
|
|
|
- return s
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- for _, field := range c.inputFields {
|
|
|
|
|
- data[field.title] = field.input.Value()
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- var newIssue = Issue{
|
|
|
|
|
- Path: c.Path,
|
|
|
|
|
- Tags: VariadicField{Path: "/tags"},
|
|
|
|
|
- Blockedby: VariadicField{Path: "/blockedby"},
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- for key, value := range data {
|
|
|
|
|
- switch key {
|
|
|
|
|
- case "title":
|
|
|
|
|
- newIssue.Title = value
|
|
|
|
|
- if parsePathToHuman(newIssue.Path) != value {
|
|
|
|
|
- dir, _ := filepath.Split(newIssue.Path)
|
|
|
|
|
- newIssue.Path = filepath.Join(dir, value)
|
|
|
|
|
- }
|
|
|
|
|
- case "status":
|
|
|
|
|
- newIssue.Status = Field{Path: "/status", Data: value}
|
|
|
|
|
- case "description":
|
|
|
|
|
- newIssue.Description = Field{Path: "/description", Data: value}
|
|
|
|
|
- case "tags":
|
|
|
|
|
- splitTags := commaSplit(value)
|
|
|
|
|
- for _, tag := range splitTags {
|
|
|
|
|
- newIssue.Tags.Fields = append(newIssue.Tags.Fields, Field{Path: tag})
|
|
|
|
|
- }
|
|
|
|
|
- case "blockers":
|
|
|
|
|
- splitBlockedby := commaSplit(value)
|
|
|
|
|
- for _, blocker := range splitBlockedby {
|
|
|
|
|
- newIssue.Blockedby.Fields = append(
|
|
|
|
|
- newIssue.Blockedby.Fields, Field{Path: blocker},
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return createResult(newIssue)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// type wrapper for create.write() result
|
|
|
|
|
-type writeResult any
|
|
|
|
|
-
|
|
|
|
|
-// Wraps a tea.Cmd func, passes an initialized Issue to WriteIssue()
|
|
|
|
|
-func (c create) write(issue Issue) tea.Cmd {
|
|
|
|
|
- return func() tea.Msg {
|
|
|
|
|
- result, err := WriteIssue(issue, false)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- return writeResult(err)
|
|
|
|
|
- }
|
|
|
|
|
- return writeResult(result)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return initialCreateWidget(m.Path, "lorem ipsum")
|
|
|
}
|
|
}
|