Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding char limits to TUI editing #39

Merged
merged 1 commit into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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