소스 검색

Implemented system editor description creation in tui

arianagiroux 2 주 전
부모
커밋
e374ecd373
3개의 변경된 파일121개의 추가작업 그리고 91개의 파일을 삭제
  1. 4 4
      Readme.md
  2. 116 86
      tui.go
  3. 1 1
      tui_test.go

+ 4 - 4
Readme.md

@@ -16,13 +16,13 @@ go install cmd/issues.go
 
 - `cmd/issues.go:// TODO implement edit/delete funcs`
 - `cmd/issues.go:// TODO implement attempt to auto load issues folder if no arg specified`
+- `io.go:func readPath(path string) (output string, err error) { // TODO DEPRECATE`
 - `io.go:func DeleteIssue(issue Issue) (success bool, err error) { return false, nil } // TODO: implement`
-- `tui.go:// TODO enable collection recursing (i.e, embeded collections)`
+- `tui.go:// TODO enable collection recursing (i.e, embedded collections)`
 - `tui.go:// TODO enable scroll/viewport logic`
 - `tui.go:	case IssueCollection: // TODO handle updates to IssueCollection widgets in its own update func`
-- `tui.go:// TODO invoke editor for descriptions`
-- `tui.go:// TODO handle reset on esc`
-- `tui.go:// TODO implement description field in createIssue.create cmd`
+- `tui.go:// TODO(create widget) handle reset on esc`
+- `tui.go:// TODO(create widget) implement description field in create.create`
 
 ## See also
 

+ 116 - 86
tui.go

@@ -1,6 +1,6 @@
 // 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
 //
@@ -157,14 +157,12 @@ type widget interface {
 	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 {
 	input textinput.Model
 	title string
@@ -175,11 +173,11 @@ type create struct {
 	inputFields []inputField
 	Path        string
 	selected    int
-	Err         error // not implemented
+	err         error // not implemented
 }
 
 // constructor for create widget
-func initialCreateModel(path string, placeholder string) create {
+func initialCreateWidget(path string, placeholder string) create {
 	spawnInput := func(f bool) textinput.Model {
 		ti := textinput.New()
 		ti.Placeholder = placeholder
@@ -217,7 +215,7 @@ func initialCreateModel(path string, placeholder string) create {
 		inputFields: inputs,
 		Path:        path,
 		selected:    0,
-		Err:         nil,
+		err:         nil,
 	}
 }
 
@@ -226,12 +224,21 @@ func (c create) init() tea.Cmd {
 	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
 func (c create) update(msg tea.Msg) (create, tea.Cmd) {
 	var cmds []tea.Cmd
 	var cmd tea.Cmd
 
-	// simple anon funcs to increment the selected index
+	// simple anon functions to increment the selected index
 	incrementSelected := func() {
 		if c.selected < len(c.inputFields) {
 			c.selected++
@@ -285,7 +292,13 @@ func (c create) update(msg tea.Msg) (create, tea.Cmd) {
 			cmds = append(cmds, tea.Quit)
 		}
 	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:
 		switch value := msg.(type) {
 		case bool:
@@ -294,7 +307,7 @@ func (c create) update(msg tea.Msg) (create, tea.Cmd) {
 				cmds = append(cmds, tea.Quit)
 			}
 		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...)
 }
 
+// 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
 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.NormalBorder()).
 		Margin(1).
@@ -335,10 +435,11 @@ func (c create) render() tea.Msg {
 		confirmPrompt := fmt.Sprintf(
 			"create issue titled \"%s\"?\n\n%s",
 			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)
 	}
+
 	return output
 }
 
@@ -456,76 +557,5 @@ func (m Model) load() tea.Msg {
 		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")
 }

+ 1 - 1
tui_test.go

@@ -206,7 +206,7 @@ func Test_create_create_cmd(t *testing.T) {
 
 	// because the resulting test data never gets written from memory to disk,
 	// we can set just about any test path
-	testC := initialCreateModel("tests/bugs/test-create-in-memory", "lorem ipsum")
+	testC := initialCreateWidget("tests/bugs/test-create-in-memory", "lorem ipsum")
 	for i := range testC.inputFields {
 		testData[testC.inputFields[i].title] = fmt.Sprintf("test%d", i)
 		testC.inputFields[i].input.SetValue(fmt.Sprintf("test%d", i))