Ver Fonte

Implements MVP for create widget

arianagiroux há 2 semanas atrás
pai
commit
d82b29794e
2 ficheiros alterados com 167 adições e 71 exclusões
  1. 162 66
      tui.go
  2. 5 5
      tui_test.go

+ 162 - 66
tui.go

@@ -54,7 +54,7 @@ var (
 
 // interface for renderable structs
 type widget interface {
-	view() tea.Msg
+	render() tea.Msg
 }
 
 // The main bubbletea Model
@@ -74,14 +74,8 @@ func (m Model) Init() tea.Cmd { return m.load }
 // Handles quit logic and viewport scroll and size updates
 func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	// widget specifc keyhandling
+	var cmds []tea.Cmd
 	switch m.widget.(type) {
-	case issueCreate:
-		if msg, ok := msg.(tea.KeyMsg); ok {
-			switch msg.String() {
-			case "enter": // TODO create, write, and render an issue on press enter
-				return m, tea.Quit
-			}
-		}
 	case IssueCollection:
 		if msg, ok := msg.(tea.KeyMsg); ok {
 			switch msg.String() {
@@ -93,9 +87,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 						collection.selection = 0
 					}
 					m.widget = collection
-					return m, collection.view
+					return m, collection.render
 				}
-				return m, nil
 			case "k":
 				// do something only if widget is collection
 				if collection, ok := m.widget.(IssueCollection); ok {
@@ -105,50 +98,47 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 						collection.selection = len(collection.Collection) - 1
 					}
 					m.widget = collection
-					return m, collection.view
+					return m, collection.render
 				}
-				return m, nil
 			case "enter":
 				if _, ok := m.widget.(IssueCollection); ok {
 					m.Path = m.widget.(IssueCollection).Collection[m.widget.(IssueCollection).selection].Path
 					return m, m.load
 				}
-				return m, nil
 			case "q":
-				return m, tea.Quit
-			}
-		}
-	default:
-		if msg, ok := msg.(tea.KeyMsg); ok {
-			switch msg.String() {
-			case "q":
-				return m, tea.Quit
+				cmds = append(cmds, tea.Quit)
 			}
 		}
 	}
 
+	// general message handling
 	switch msg := msg.(type) {
 	case tea.KeyMsg: // keymsg capture that is always present
 		switch msg.String() {
 		case "ctrl+c":
-			return m, tea.Quit
+			cmds = append(cmds, tea.Quit)
 		}
-	case widget:
+	case widget: // widget is initialized from m.load()
 		switch T := msg.(type) {
-		default:
+		case createIssue:
 			m.widget = T
-			return m, T.view
-		case issueCreate:
-			T = T.init(Issue{Path: m.Path})
+			cmds = append(cmds, T.render, T.init())
+		default:
 			m.widget = T
-			return m, T.view
+			cmds = append(cmds, T.render)
 		}
 	case string:
 		m.content = msg
-		return m, nil
 	}
 
-	return m, nil
+	// finally, handle input updates if any
+	if w, ok := m.widget.(createIssue); ok {
+		var cmd tea.Cmd
+		m.widget, cmd = w.update(msg)
+		cmds = append(cmds, cmd, w.render)
+	}
+
+	return m, tea.Batch(cmds...)
 }
 
 // Handles top level view functionality
@@ -163,53 +153,159 @@ func (m Model) View() string {
 	return output
 }
 
-// Create widget definitions --------------------------------------------------
+// WIDGET DEFINITIONS ---------------------------------------------------------
 // ----------------------------------------------------------------------------
+// -------- creatIssue definitions --------------------------------------------
+// ----------------------------------------------------------------------------
+
+// data struct for createIssue
+type inputField struct {
+	input textinput.Model
+	title string
+}
+
+// widget for creating an issue
+type createIssue struct {
+	inputFields []inputField
+	Path        string
+	selected    int
+	Err         error // behaviour undefined
+}
+
+func initialInputModel(path string, placeholder string) createIssue {
+	spawnInput := func(f bool) textinput.Model {
+		ti := textinput.New()
+		ti.Placeholder = placeholder
+		if f {
+			ti.Focus()
+		}
+		ti.CharLimit = 80
+		ti.Width = 30
+		return ti
+	}
+
+	var inputs []inputField
+	for i, t := range [4]string{"title", "status", "tags", "blockers"} {
+		if i == 0 {
+			inputs = append(inputs, inputField{title: t, input: spawnInput(true)})
+		} else {
+			inputs = append(inputs, inputField{title: t, input: spawnInput(false)})
+		}
+	}
 
-type issueCreate struct {
-	focusedField int
-	inputs       []textinput.Model
-	issue        Issue
+	return createIssue{
+		inputFields: inputs,
+		Path:        path,
+		selected:    0,
+		Err:         nil,
+	}
 }
 
-func (c issueCreate) init(i Issue) issueCreate {
-	c.issue = i
-	title := textinput.Model{Placeholder: "title", CharLimit: 64, Width: 45}
-	status := textinput.Model{Placeholder: "status", CharLimit: 64, Width: 45}
-	tags := textinput.Model{Placeholder: "tags", CharLimit: 64, Width: 45}
-	blockers := textinput.Model{Placeholder: "blockers", CharLimit: 64, Width: 45}
-	c.inputs = append(c.inputs, title, status, tags, blockers)
-	return c
+func (ci createIssue) init() tea.Cmd {
+	return textinput.Blink
 }
 
-func (c issueCreate) update(msg tea.Msg) (issueCreate, tea.Cmd) {
-	switch T := msg.(type) {
-	case issueCreate:
-		c.issue = T.issue
-		c.focusedField = T.focusedField
-		c.inputs = T.inputs
+func (ci createIssue) update(msg tea.Msg) (createIssue, tea.Cmd) {
+	var cmds []tea.Cmd
+	var cmd tea.Cmd
+
+	// simple anon funcs to increment the selected index
+	incrementSelected := func() {
+		if ci.selected < len(ci.inputFields) {
+			ci.selected++
+			for i := 0; i < len(ci.inputFields); i++ {
+				if i == ci.selected {
+					ci.inputFields[i].input.Focus()
+				} else {
+					ci.inputFields[i].input.Blur()
+				}
+			}
+		} else {
+			ci.selected = 0
+			ci.inputFields[ci.selected].input.Focus()
+		}
+	}
+
+	decrementSelected := func() {
+		if ci.selected != 0 {
+			ci.selected--
+			for i := 0; i < len(ci.inputFields); i++ {
+				if i == ci.selected {
+					ci.inputFields[i].input.Focus()
+				} else {
+					ci.inputFields[i].input.Blur()
+				}
+			}
+		} else {
+			for i := 0; i < len(ci.inputFields); i++ {
+				ci.inputFields[i].input.Blur()
+			}
+			ci.selected = len(ci.inputFields)
+		}
+	}
 
-		return c, nil
+	switch msg := msg.(type) { // keybinding handler
 	case tea.KeyMsg:
-		switch T.String() {
+		switch msg.String() {
 		case "tab":
-			if c.focusedField < len(c.inputs)-1 {
-				c.focusedField++
+			incrementSelected()
+		case "shift+tab":
+			decrementSelected()
+		case "enter":
+			if ci.selected == len(ci.inputFields) { // confirm create
+				ci.selected++
+			} else if ci.selected == len(ci.inputFields)+1 { // confirmed
+				cmds = append(cmds, tea.Quit)
 			} else {
-				c.focusedField = 0
+				incrementSelected()
 			}
-			return c, nil
-		default:
-			inputModel, inputCmd := c.inputs[c.focusedField].Update(msg)
-			c.inputs[c.focusedField] = inputModel
-			return c, inputCmd
+		case "esc": // cancel
+			cmds = append(cmds, tea.Quit)
 		}
 	}
 
-	return c, nil
+	for i, ti := range ci.inputFields {
+		ci.inputFields[i].input, cmd = ti.input.Update(msg)
+		cmds = append(cmds, cmd)
+	}
+
+	cmds = append(cmds, cmd)
+	return ci, tea.Batch(cmds...)
 }
 
-func (c issueCreate) view() tea.Msg { return "testing" }
+func (ci createIssue) render() tea.Msg {
+	borderStyle := lipgloss.NewStyle().
+		BorderStyle(lipgloss.NormalBorder()).
+		Margin(1).
+		Padding(0, 1)
+
+	ulStyle := lipgloss.NewStyle().Underline(true)
+
+	var output string
+	for _, field := range ci.inputFields {
+		output = output + fmt.Sprintf(
+			"\n%s:%s",
+			field.title,
+			borderStyle.Render(field.input.View()),
+		)
+	}
+
+	output = strings.TrimLeft(output, "\n")
+
+	if ci.selected < len(ci.inputFields) {
+		output = output + borderStyle.Render("press enter to submit...")
+	} else if ci.selected == len(ci.inputFields) {
+		output = output + borderStyle.Render(ulStyle.Render("press enter to submit..."))
+	} else if ci.selected == len(ci.inputFields)+1 {
+		confirmPrompt := fmt.Sprintf(
+			"creating issue titled \"%s\"...\n\n%s",
+			ulStyle.Render(ci.inputFields[0].input.Value()),
+			ulStyle.Render("press enter to confirm..."),
+		)
+		output = output + borderStyle.Render(confirmPrompt)
+	}
+	return output
+}
 
 // tea.Cmd definitions --------------------------------------------------------
 // ----------------------------------------------------------------------------
@@ -236,11 +332,11 @@ func (m Model) load() tea.Msg {
 		return collection
 	}
 
-	return issueCreate{issue: Issue{Path: m.Path}}
+	return initialInputModel(m.Path, "lorem ipsum")
 }
 
-// Handles all view logic for Issue structs
-func (i Issue) view() tea.Msg {
+// Handles all render logic for Issue structs
+func (i Issue) render() tea.Msg {
 	var output string
 	// title
 	output = output + titleStyle.Render(i.Title)
@@ -278,8 +374,8 @@ func (i Issue) view() tea.Msg {
 	return borderStyle.Render(output)
 }
 
-// Handles all view logic for IssueCollection structs.
-func (ic IssueCollection) view() tea.Msg {
+// Handles all render logic for IssueCollection structs.
+func (ic IssueCollection) render() tea.Msg {
 	var output string
 	var left string
 	output = output + "Issues in " + ic.Path + "...\n\n"

+ 5 - 5
tui_test.go

@@ -131,7 +131,7 @@ func Test_Model_Update_renderIssue(t *testing.T) {
 	}
 
 	assert.Equal(t, testIssue.Title, widget.Title)
-	assert.Equal(t, testIssue.view(), cmd())
+	assert.Equal(t, testIssue.render(), cmd())
 }
 
 func Test_Model_Update_renderIssueCollection(t *testing.T) {
@@ -145,7 +145,7 @@ func Test_Model_Update_renderIssueCollection(t *testing.T) {
 	}
 
 	assert.Equal(t, len(testCollection.Collection), len(widget.Collection))
-	assert.Equal(t, testCollection.view(), cmd())
+	assert.Equal(t, testCollection.render(), cmd())
 }
 
 func Test_Model_Update_updates_content(t *testing.T) {
@@ -153,7 +153,7 @@ func Test_Model_Update_updates_content(t *testing.T) {
 	testModel := Model{widget: testIssue}
 	testWidget := testModel.widget.(Issue)
 
-	testModel.content = testWidget.view().(string)
+	testModel.content = testWidget.render().(string)
 
 	model, cmd := testModel.Update(tea.Msg(testModel.content))
 	if cmd != nil {
@@ -176,7 +176,7 @@ func Test_Model_Update_do_nothing(t *testing.T) {
 
 func Test_Model_renderIssue(t *testing.T) {
 	testIssue, _ := Issue.NewFromPath(Issue{}, "tests/bugs/test-1")
-	testRender := testIssue.view()
+	testRender := testIssue.render()
 
 	renderContent, _ := testRender.(string)
 	assert.True(t, strings.Contains(renderContent, "test description"))
@@ -190,7 +190,7 @@ func Test_Model_View(t *testing.T) {
 		assert.Fail(t, "should not return cmd")
 	}
 
-	testView := testIssue.view().(string) + "\nj/k: down/up\tenter: select\tq: quit"
+	testView := testIssue.render().(string) + "\nj/k: down/up\tenter: select\tq: quit"
 	assert.Equal(t, testView, model.(Model).View())
 
 	render2 := Model{}.View()