|
|
@@ -3,8 +3,6 @@
|
|
|
// While the package remains in v0.0.X releases, this TUI may be undocumented.
|
|
|
//
|
|
|
// TODO enable collection recursing (i.e, embedded collections)
|
|
|
-//
|
|
|
-// TODO enable scroll/viewport logic
|
|
|
package issues
|
|
|
|
|
|
import (
|
|
|
@@ -12,9 +10,10 @@ import (
|
|
|
"path/filepath"
|
|
|
"strings"
|
|
|
|
|
|
+ "charm.land/bubbles/v2/viewport"
|
|
|
+ "charm.land/lipgloss/v2"
|
|
|
"github.com/charmbracelet/bubbles/textinput"
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
|
- "github.com/charmbracelet/lipgloss"
|
|
|
)
|
|
|
|
|
|
// Type and Style definitions -------------------------------------------------
|
|
|
@@ -63,10 +62,12 @@ var (
|
|
|
|
|
|
// The main bubbletea Model
|
|
|
type Model struct {
|
|
|
- widget widget
|
|
|
- content string
|
|
|
- Path string
|
|
|
- // viewport viewport.Model
|
|
|
+ widget widget
|
|
|
+ content string
|
|
|
+ Path string
|
|
|
+ viewport viewport.Model
|
|
|
+ viewportReady bool
|
|
|
+ width int
|
|
|
}
|
|
|
|
|
|
// The bubbletea init function
|
|
|
@@ -78,11 +79,34 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
|
|
|
// general message handling
|
|
|
switch msg := msg.(type) {
|
|
|
+ case tea.WindowSizeMsg:
|
|
|
+ m.width = msg.Width
|
|
|
+ headerHeight := lipgloss.Height(m.header())
|
|
|
+ footerHeight := lipgloss.Height(m.footer())
|
|
|
+ verticalMarginHeight := headerHeight + footerHeight
|
|
|
+ if !m.viewportReady {
|
|
|
+ m.viewport = viewport.New(
|
|
|
+ viewport.WithWidth(msg.Width),
|
|
|
+ viewport.WithHeight(msg.Height-verticalMarginHeight),
|
|
|
+ )
|
|
|
+ m.viewport.YPosition = headerHeight
|
|
|
+ m.viewport.SetContent(m.content)
|
|
|
+ m.viewportReady = true
|
|
|
+ } else {
|
|
|
+ m.viewport.SetWidth(msg.Width)
|
|
|
+ m.viewport.SetHeight(msg.Height - verticalMarginHeight)
|
|
|
+ }
|
|
|
case tea.KeyMsg: // KeyMsg capture that is always present
|
|
|
switch msg.String() {
|
|
|
case "ctrl+c":
|
|
|
cmds = append(cmds, tea.Quit)
|
|
|
}
|
|
|
+ switch msg.Type {
|
|
|
+ case tea.KeyUp:
|
|
|
+ m.viewport.ScrollUp(1)
|
|
|
+ case tea.KeyDown:
|
|
|
+ m.viewport.ScrollDown(1)
|
|
|
+ }
|
|
|
case widget: // widget is initialized from m.load()
|
|
|
switch T := msg.(type) {
|
|
|
case edit:
|
|
|
@@ -106,6 +130,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
return m, func() tea.Msg { return wg }
|
|
|
case string:
|
|
|
m.content = msg
|
|
|
+ m.viewport.SetContent(m.content)
|
|
|
+ cmds = append(cmds, tea.EnterAltScreen)
|
|
|
}
|
|
|
|
|
|
// finally, pass msg to widget
|
|
|
@@ -133,10 +159,24 @@ func (m Model) View() string {
|
|
|
var output string
|
|
|
if len(m.content) == 0 {
|
|
|
return "loading..."
|
|
|
+ } else {
|
|
|
+ output = output + m.header()
|
|
|
+ output = output + m.viewport.View()
|
|
|
+
|
|
|
+ footerLeft := lipgloss.NewStyle().Faint(true).Margin(1).Render(m.widget.keyhelp())
|
|
|
+ footerRight := lipgloss.NewStyle().Faint(true).Margin(1).Render(m.footer())
|
|
|
+
|
|
|
+ line := strings.Repeat("─",
|
|
|
+ max(0, m.viewport.Width()-lipgloss.Width(footerLeft)-lipgloss.Width(footerRight)))
|
|
|
+ line = lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render(line)
|
|
|
+ output = output + lipgloss.JoinHorizontal(lipgloss.Center,
|
|
|
+ footerLeft,
|
|
|
+ line,
|
|
|
+ footerRight,
|
|
|
+ )
|
|
|
+ // output = output + lipgloss.NewStyle().Faint(true).Margin(0, 1, 1, 1).Render(m.footer())
|
|
|
+ return output
|
|
|
}
|
|
|
- output = output + m.content
|
|
|
- output = output + lipgloss.NewStyle().Faint(true).Margin(1).Render(m.widget.keyhelp())
|
|
|
- return output
|
|
|
}
|
|
|
|
|
|
// Handles load logic
|
|
|
@@ -164,6 +204,24 @@ func (m Model) load() tea.Msg {
|
|
|
return newEditWidget(m.Path)
|
|
|
}
|
|
|
|
|
|
+// renders a header for the program
|
|
|
+func (m Model) header() string {
|
|
|
+ var borderStyle = lipgloss.NewStyle().
|
|
|
+ BorderStyle(lipgloss.NormalBorder()).
|
|
|
+ BorderForeground(lipgloss.Color("8")).
|
|
|
+ Padding(0, 1)
|
|
|
+
|
|
|
+ left := "tissues v0.0"
|
|
|
+ left = borderStyle.Render(left)
|
|
|
+
|
|
|
+ line := strings.Repeat("─", max(0, m.viewport.Width()-lipgloss.Width(left)))// -lipgloss.Width(right)))
|
|
|
+ line = lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render(line)
|
|
|
+ return lipgloss.JoinHorizontal(lipgloss.Center, line, left)
|
|
|
+}
|
|
|
+
|
|
|
+// renders a footer for the program
|
|
|
+func (m Model) footer() string { return "j/k: scroll\t\tctrl+c: quit" }
|
|
|
+
|
|
|
// WIDGET DEFINITIONS ---------------------------------------------------------
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
|
|
@@ -176,7 +234,6 @@ type widget interface {
|
|
|
|
|
|
// -------- edit widget definitions -----------------------------------------
|
|
|
// ----------------------------------------------------------------------------
|
|
|
-// TODO(create widget) implement description field in create.create
|
|
|
|
|
|
type ( // Type definitions for use in tea.Msg life cycle for create widget.
|
|
|
createResult Issue // type wrapper for create.create() result
|
|
|
@@ -347,9 +404,9 @@ func (e edit) update(msg tea.Msg) (widget, tea.Cmd) {
|
|
|
cmds = append(cmds, e.render)
|
|
|
case createResult:
|
|
|
if !e.existing {
|
|
|
- cmds = append(cmds, e.editBlankDescription(Issue(msg)))
|
|
|
+ cmds = append(cmds, tea.ExitAltScreen, e.editBlankDescription(Issue(msg)))
|
|
|
} else {
|
|
|
- cmds = append(cmds, e.editExistingDescription(Issue(msg)))
|
|
|
+ cmds = append(cmds, tea.ExitAltScreen, e.editExistingDescription(Issue(msg)))
|
|
|
}
|
|
|
case editorResult:
|
|
|
if msg.err != nil {
|
|
|
@@ -523,7 +580,7 @@ func (e edit) render() tea.Msg {
|
|
|
// keyhelp cmd for create widget
|
|
|
func (e edit) keyhelp() string {
|
|
|
var output string
|
|
|
- output = output + "\ntab/shift+tab: down/up\t\tenter: input value\t\tesc: reset\t\tctrl+e: quit"
|
|
|
+ output = output + "tab/shift+tab: down/up\t\tenter: input value\t\tesc: reset\t\tctrl+e: quit"
|
|
|
return output
|
|
|
}
|
|
|
|
|
|
@@ -603,7 +660,7 @@ func (i Issue) keyhelp() string {
|
|
|
if IsIssueCollection(remainder) {
|
|
|
escStr = "\t\tesc: back"
|
|
|
}
|
|
|
- output = output + fmt.Sprintf("\ne: edit issue%s\t\tctrl+c: quit", escStr)
|
|
|
+ output = output + fmt.Sprintf("e: edit issue%s\t\tctrl+c: quit", escStr)
|
|
|
return output
|
|
|
}
|
|
|
|
|
|
@@ -620,14 +677,14 @@ func (ic IssueCollection) update(msg tea.Msg) (widget, tea.Cmd) {
|
|
|
switch msg := msg.(type) {
|
|
|
case tea.KeyMsg:
|
|
|
switch msg.String() {
|
|
|
- case "j":
|
|
|
+ case "tab":
|
|
|
if ic.selection+1 < len(ic.Collection) {
|
|
|
ic.selection = ic.selection + 1
|
|
|
} else {
|
|
|
ic.selection = 0
|
|
|
}
|
|
|
return ic, ic.render
|
|
|
- case "k":
|
|
|
+ case "shift+tab":
|
|
|
if ic.selection != 0 {
|
|
|
ic.selection = ic.selection - 1
|
|
|
} else {
|
|
|
@@ -637,8 +694,6 @@ func (ic IssueCollection) update(msg tea.Msg) (widget, tea.Cmd) {
|
|
|
case "enter":
|
|
|
ic.Path = ic.Collection[ic.selection].Path
|
|
|
return ic, ic.sendLoad
|
|
|
- case "q":
|
|
|
- return ic, tea.Quit
|
|
|
case "c":
|
|
|
return ic, ic.newIssueInCollection
|
|
|
case "e":
|
|
|
@@ -660,7 +715,7 @@ func (ic IssueCollection) edit() tea.Msg { return newEditWidget(ic.Path) }
|
|
|
func (ic IssueCollection) render() tea.Msg {
|
|
|
var output string
|
|
|
var left string
|
|
|
- output = output + "Issues in " + ic.Path + "...\n\n"
|
|
|
+ output = output + "\nIssues in " + ic.Path + "...\n\n"
|
|
|
for i, issue := range ic.Collection {
|
|
|
// pointer render
|
|
|
if i == ic.selection {
|
|
|
@@ -686,7 +741,7 @@ func (ic IssueCollection) render() tea.Msg {
|
|
|
// keyhelp cmd for IssueCollection widget
|
|
|
func (ic IssueCollection) keyhelp() string {
|
|
|
var output string
|
|
|
- output = output + "\nc: create new issue\t\tenter: view issue\t\tj/k: down/up\t\tq/ctrl+c: quit"
|
|
|
+ output = output + "tab/shift+tab: select\t\tenter: view issue\t\tc: create new issue\t\te: edit selected issue"
|
|
|
return output
|
|
|
}
|
|
|
|