Skip to content

Commit

Permalink
feat: adding char limits to TUI editing (#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
JonathanHope authored Jan 11, 2024
1 parent 1761fea commit 992c2e8
Show file tree
Hide file tree
Showing 16 changed files with 702 additions and 283 deletions.
279 changes: 92 additions & 187 deletions cmd/cli/tui/booksview/books.go

Large diffs are not rendered by default.

14 changes: 2 additions & 12 deletions cmd/cli/tui/errorview/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/jonathanhope/armaria/cmd/cli/tui/msgs"
"github.com/jonathanhope/armaria/cmd/cli/tui/utils"
)

func TestHandlesViewMessage(t *testing.T) {
Expand Down Expand Up @@ -47,16 +48,5 @@ func verifyUpdate(t *testing.T, gotModel tea.Model, wantModel tea.Model, gotCmd
t.Errorf("Expected and actual models different:\n%s", modelDiff)
}

if gotCmd == nil || wantCmd == nil {
if gotCmd != nil || wantCmd != nil {
t.Errorf("Expected and actual cmds different: one is nil and one is non-nil")
}

return
}

cmdDiff := cmp.Diff(gotCmd(), wantCmd())
if modelDiff != "" {
t.Errorf("Expected and actual cmds different:\n%s", cmdDiff)
}
utils.CompareCommands(t, gotCmd, wantCmd)
}
164 changes: 164 additions & 0 deletions cmd/cli/tui/footer/footer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package footer

import (
"strings"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/jonathanhope/armaria/cmd/cli/tui/msgs"
"github.com/jonathanhope/armaria/cmd/cli/tui/textinput"
"github.com/jonathanhope/armaria/cmd/cli/tui/utils"
)

const HelpInfoWidth = 7 // width of the help info in the footer
const TextInputName = "BooksInput" // name of the textinput

// FooterModel is the model for a header.
// The footer can collect input, and displays informationa about the apps state.
type FooterModel struct {
name string // name of the footer
width int // max width of the footer
inputMode bool // if true footer is accepting input
filters []string // currently applied filters
input textinput.TextInputModel // allows text input
}

// Text returns the text in the footers input.
func (m FooterModel) Text() string {
return m.input.Text()
}

// InputMode returns whether the footer is accepting input or not.
func (m FooterModel) InputMode() bool {
return m.inputMode
}

// InitialModel builds the model.
func InitialModel(name string) FooterModel {
return FooterModel{
name: name,
input: textinput.InitialModel(TextInputName, ""),
}
}

// Update handles a message.
func (m FooterModel) Update(msg tea.Msg) (FooterModel, tea.Cmd) {
var cmds []tea.Cmd

switch msg := msg.(type) {

case msgs.SizeMsg:
if msg.Name == m.name {
m.width = msg.Width
} else {
var inputCmd tea.Cmd
m.input, inputCmd = m.input.Update(msgs.SizeMsg{
Name: TextInputName,
Width: m.width - HelpInfoWidth,
})
cmds = append(cmds, inputCmd)
}

case msgs.InputModeMsg:
if msg.Name == m.name {
m.inputMode = msg.InputMode

if m.inputMode {
cmds = append(cmds, func() tea.Msg {
return msgs.PromptMsg{Name: TextInputName, Prompt: msg.Prompt}
}, func() tea.Msg {
return msgs.TextMsg{Name: TextInputName, Text: msg.Text}
}, func() tea.Msg {
return msgs.FocusMsg{Name: TextInputName, MaxChars: msg.MaxChars}
})
} else {
cmds = append(cmds, func() tea.Msg {
return msgs.BlurMsg{Name: TextInputName}
}, func() tea.Msg {
return msgs.PromptMsg{Name: TextInputName, Prompt: ""}
}, func() tea.Msg {
return msgs.TextMsg{Name: TextInputName, Text: ""}
})
}
}

case msgs.FiltersMsg:
if msg.Name == m.name {
m.filters = msg.Filters
}

case tea.KeyMsg:
if m.inputMode {
switch msg.String() {
case "ctrl+c":
if m.inputMode {
return m, tea.Quit
}

case "esc":
cmds = append(cmds, func() tea.Msg {
return msgs.InputCancelledMsg{}
})

case "enter":
cmds = append(cmds, func() tea.Msg {
return msgs.InputConfirmedMsg{}
})

default:
var inputCmd tea.Cmd
m.input, inputCmd = m.input.Update(msg)
cmds = append(cmds, inputCmd)
}
}

default:
var inputCmd tea.Cmd
m.input, inputCmd = m.input.Update(msg)
cmds = append(cmds, inputCmd)
}

return m, tea.Batch(cmds...)
}

// View renders the model.
func (m FooterModel) View() string {
help := lipgloss.
NewStyle().
Foreground(lipgloss.Color("3")).
SetString("Help: ?")

footerStyle := lipgloss.
NewStyle().
Border(lipgloss.ThickBorder()).
BorderTop(true).
BorderBottom(false).
BorderLeft(false).
BorderRight(false).
Width(m.width).
BorderForeground(lipgloss.Color("5"))

var filters string
if len(m.filters) > 0 {
filters = strings.Join(m.filters, ", ")
} else {
filters = "No filters applied"
}
if m.width > 0 {
filters = utils.Substr(filters, 0, m.width-4)
}

filtersStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("2")).
SetString(filters).
Width(m.width).
Align(lipgloss.Right).
Padding(0, 2)

return footerStyle.Render(m.input.View() + help.Render() + "\n" + filtersStyle.Render())
}

// Init initializes the model.
func (m FooterModel) Init() tea.Cmd {
return nil
}
144 changes: 144 additions & 0 deletions cmd/cli/tui/footer/footer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package footer

import (
"testing"

tea "github.com/charmbracelet/bubbletea"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/jonathanhope/armaria/cmd/cli/tui/msgs"
"github.com/jonathanhope/armaria/cmd/cli/tui/utils"
)

const Name = "footer"

func TestCanUpdateWidth(t *testing.T) {
gotModel := FooterModel{
name: Name,
}
gotModel, gotCmd := gotModel.Update(msgs.SizeMsg{Name: Name, Width: 1})

wantModel := FooterModel{
name: Name,
width: 1,
}

verifyUpdate(t, gotModel, wantModel, gotCmd, nil)
}

func TestCanStartInputMode(t *testing.T) {
gotModel := FooterModel{
name: Name,
}
gotModel, gotCmd := gotModel.Update(msgs.InputModeMsg{
Name: Name,
InputMode: true,
Prompt: "prompt",
Text: "text",
MaxChars: 5,
})

wantModel := FooterModel{
name: Name,
inputMode: true,
}

wantCmd := func() tea.Msg {
return tea.BatchMsg{
func() tea.Msg { return msgs.PromptMsg{Name: TextInputName, Prompt: "prompt"} },
func() tea.Msg { return msgs.TextMsg{Name: TextInputName, Text: "text"} },
func() tea.Msg { return msgs.FocusMsg{Name: TextInputName, MaxChars: 5} },
}
}

verifyUpdate(t, gotModel, wantModel, gotCmd, wantCmd)
}

func TestCanEndInputMode(t *testing.T) {
gotModel := FooterModel{
name: Name,
inputMode: true,
}
gotModel, gotCmd := gotModel.Update(msgs.InputModeMsg{Name: Name, InputMode: false})

wantModel := FooterModel{
name: Name,
inputMode: false,
}

wantCmd := func() tea.Msg {
return tea.BatchMsg{
func() tea.Msg { return msgs.BlurMsg{Name: TextInputName} },
func() tea.Msg { return msgs.PromptMsg{Name: TextInputName} },
func() tea.Msg { return msgs.TextMsg{Name: TextInputName} },
}
}

verifyUpdate(t, gotModel, wantModel, gotCmd, wantCmd)
}

func TestCanSetFilters(t *testing.T) {
gotModel := FooterModel{
name: Name,
}
gotModel, gotCmd := gotModel.Update(msgs.FiltersMsg{Name: Name, Filters: []string{"one"}})

wantModel := FooterModel{
name: Name,
filters: []string{"one"},
}

verifyUpdate(t, gotModel, wantModel, gotCmd, nil)
}

func TestCanCancelInput(t *testing.T) {
gotModel := FooterModel{
name: Name,
inputMode: true,
}
gotModel, gotCmd := gotModel.Update(tea.KeyMsg{Type: tea.KeyEsc})

wantModel := FooterModel{
name: Name,
inputMode: true,
}

wantCmd := func() tea.Msg {
return tea.BatchMsg{
func() tea.Msg { return msgs.InputCancelledMsg{} },
}
}

verifyUpdate(t, gotModel, wantModel, gotCmd, wantCmd)
}

func TestCanConfirmInput(t *testing.T) {
gotModel := FooterModel{
name: Name,
inputMode: true,
}
gotModel, gotCmd := gotModel.Update(tea.KeyMsg{Type: tea.KeyEnter})

wantModel := FooterModel{
name: Name,
inputMode: true,
}

wantCmd := func() tea.Msg {
return tea.BatchMsg{
func() tea.Msg { return msgs.InputConfirmedMsg{} },
}
}

verifyUpdate(t, gotModel, wantModel, gotCmd, wantCmd)
}

func verifyUpdate(t *testing.T, gotModel FooterModel, wantModel FooterModel, gotCmd tea.Cmd, wantCmd tea.Cmd) {
unexported := cmp.AllowUnexported(FooterModel{})
modelDiff := cmp.Diff(gotModel, wantModel, unexported, cmpopts.IgnoreFields(FooterModel{}, "input"))
if modelDiff != "" {
t.Errorf("Expected and actual models different:\n%s", modelDiff)
}

utils.CompareCommands(t, gotCmd, wantCmd)
}
Loading

0 comments on commit 992c2e8

Please sign in to comment.