-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: adding char limits to TUI editing (#39)
- Loading branch information
1 parent
1761fea
commit 992c2e8
Showing
16 changed files
with
702 additions
and
283 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.