|
|
@@ -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"
|