Skip to content
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
2 changes: 1 addition & 1 deletion internal/tui/bulk_reply.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func newBulkReplyForm(postingIDs []int64, draft *generated.BulkReplyDraft, s sty
}
form.preview = viewport.New(viewport.WithWidth(80), viewport.WithHeight(24))
form.preview.SetContent(form.previewContent(80))
form.body = textarea.New()
form.body = newTextArea()
form.body.Prompt = ""
form.body.ShowLineNumbers = false
form.body.Placeholder = "Write the reply that every selected thread will receive…"
Expand Down
4 changes: 2 additions & 2 deletions internal/tui/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,12 @@ func newComposeForm(mode composeMode, s styles) *composeForm {
labels = append(labels, "Subject")
}
for _, l := range labels {
in := textinput.New()
in := newTextInput()
in.Prompt = ""
in.Placeholder = placeholderFor(l)
f.inputs = append(f.inputs, in)
}
f.body = textarea.New()
f.body = newTextArea()
f.body.Prompt = ""
f.body.ShowLineNumbers = false
f.body.Placeholder = "Write your message… Markdown works here"
Expand Down
4 changes: 2 additions & 2 deletions internal/tui/contact_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func newContactForm(mode contactFormMode, contact Contact, styles styles) *conta
form := &contactForm{mode: mode, contactID: contact.ID, styles: styles}
placeholders := []string{"Jane Doe", "jane@example.com", "jane.doe@example.org, jane@example.net"}
for _, placeholder := range placeholders {
input := textinput.New()
input := newTextInput()
input.Prompt = ""
input.Placeholder = placeholder
form.inputs = append(form.inputs, input)
Expand Down Expand Up @@ -170,7 +170,7 @@ type contactNoteForm struct {
}

func newContactNoteForm(contactID int64, note string, styles styles) *contactNoteForm {
input := textarea.New()
input := newTextArea()
input.Prompt = ""
input.ShowLineNumbers = false
input.Placeholder = "Add a private note…"
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/datetime.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func newDateTimePicker(at time.Time, allDay bool) *dateTimePicker {
}

func dateTimeInput(placeholder string, width int) textinput.Model {
input := textinput.New()
input := newTextInput()
input.Prompt = ""
input.Placeholder = placeholder
input.SetWidth(width)
Expand Down
4 changes: 2 additions & 2 deletions internal/tui/event_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func newEventForm(mode eventFormMode, event Recording, on time.Time, calendars [
}

func eventInput(placeholder string, width int) textinput.Model {
input := textinput.New()
input := newTextInput()
input.Prompt = ""
input.Placeholder = placeholder
if width > 0 {
Expand All @@ -200,7 +200,7 @@ func eventInput(placeholder string, width int) textinput.Model {
// eventNotesInput is the one field somebody writes a paragraph into, so it takes several lines
// and enter puts a new one in rather than moving on. Tab is how the reader leaves it.
func eventNotesInput() textarea.Model {
input := textarea.New()
input := newTextArea()
input.Prompt = ""
input.ShowLineNumbers = false
input.Placeholder = "Agenda, what to bring, anything"
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/folders.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func newFolderPicker(posting mail.Posting, sources []mail.Source) *folderPicker
folders = append(folders, source)
}
}
input := textinput.New()
input := newTextInput()
input.Prompt = ""
input.Placeholder = "Label name…"
return &folderPicker{posting: posting, folders: folders, input: input}
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/habit_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ type habitForm struct {
}

func newHabitForm(mode habitFormMode, recording Recording, styles styles) *habitForm {
name := textinput.New()
name := newTextInput()
name.Prompt = ""
name.Placeholder = "Morning strength training"

Expand Down
79 changes: 79 additions & 0 deletions internal/tui/inputs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package tui

import (
"charm.land/bubbles/v2/textarea"
"charm.land/bubbles/v2/textinput"
"charm.land/lipgloss/v2"
)

// newTextArea and newTextInput are how every text field in the TUI is built.
//
// bubbles' New() hands a field a palette chosen for one background: the focused
// cursor line on ANSI slot 0, blurred text on slot 7, placeholders in the
// 256-color cube. On a stock light terminal that is a black band over grey
// text (hey-cli#331); Omarchy remaps slots 0 and 7 to the theme's own paper
// and ink, which is why the defect never shows there. Choosing the other
// palette for a light theme would fix the band and leave a field that has a
// mode at all — stale the moment the theme flips under it, since a widget keeps
// the Styles it was handed.
//
// So these styles name no color. Like the rest of styles.go they lean on what
// the terminal already decided: default foreground for text, the SGR faint
// attribute (styleMuted) for what is secondary, reverse video for a selection.
// A theme switch retints all of it over OSC 4 with nothing to re-apply, and
// flipping Theme.Dark changes no byte of the output — TestTextFieldsDoNotDependOnTheThemeMode
// holds that line the way TestCoversDoNotDependOnTheThemeMode does for the covers.
// Do not call textarea.New or textinput.New directly.
func newTextArea() textarea.Model {
field := textarea.New()
field.SetStyles(textAreaStyles())
return field
}

func newTextInput() textinput.Model {
field := textinput.New()
field.SetStyles(textInputStyles())
return field
}

func textAreaStyles() textarea.Styles {
plain := lipgloss.NewStyle()
focused := textarea.StyleState{
Base: plain,
Text: plain,
LineNumber: styleMuted,
CursorLineNumber: plain,
CursorLine: plain, // no band: the cursor itself says where the line is
EndOfBuffer: styleMuted,
Placeholder: styleMuted,
Prompt: plain,
Selection: lipgloss.NewStyle().Reverse(true),
}
blurred := focused
blurred.Text = styleMuted
blurred.CursorLine = styleMuted
blurred.CursorLineNumber = styleMuted
return textarea.Styles{
Focused: focused,
Blurred: blurred,
// No Color: the terminal's own cursor color, whatever the theme made it.
Cursor: textarea.CursorStyle{Shape: textarea.DefaultDarkStyles().Cursor.Shape, Blink: true},
}
}

func textInputStyles() textinput.Styles {
plain := lipgloss.NewStyle()
focused := textinput.StyleState{
Text: plain,
Placeholder: styleMuted,
Suggestion: styleMuted,
Prompt: plain,
}
blurred := focused
blurred.Text = styleMuted
return textinput.Styles{
Focused: focused,
Blurred: blurred,
Cursor: textinput.CursorStyle{Shape: textinput.DefaultDarkStyles().Cursor.Shape, Blink: true},
}
}
53 changes: 53 additions & 0 deletions internal/tui/inputs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package tui

import (
"strings"
"testing"

"charm.land/lipgloss/v2"
)

// The text fields name no color of their own, so the theme's mode cannot change a
// byte of what they draw — the same guard TestCoversDoNotDependOnTheThemeMode gives
// the cover art. bubbles' default did depend on it: its dark palette paints a light
// terminal with a black cursor line and grey text (hey-cli#331).
func TestTextFieldsDoNotDependOnTheThemeMode(t *testing.T) {
t.Cleanup(func() { applyTheme(defaultTheme()) })

render := func(dark bool) (area, input string) {
theme := defaultTheme()
theme.Dark = dark
applyTheme(theme)

a := newTextArea()
a.SetWidth(40)
a.SetHeight(3)
a.SetValue("Quarterly numbers for the board")
a.Focus()
i := newTextInput()
i.SetWidth(40)
i.SetValue("Jane Doe")
i.Focus()
return a.View(), i.View()
}

darkArea, darkInput := render(true)
lightArea, lightInput := render(false)
if darkArea != lightArea {
t.Errorf("textarea renders differently on a light theme:\n%q\n%q", darkArea, lightArea)
}
if darkInput != lightInput {
t.Errorf("textinput renders differently on a light theme:\n%q\n%q", darkInput, lightInput)
}

// The focused cursor line carries no background: that band is the defect.
if bg, unset := textAreaStyles().Focused.CursorLine.GetBackground(), lipgloss.NewStyle().GetBackground(); bg != unset {
t.Errorf("focused cursor line has a background %v, want none", bg)
}
if strings.Contains(darkArea, "\x1b[40m") || strings.Contains(darkArea, "48;5;0m") {
t.Errorf("textarea paints a black cursor line: %q", darkArea)
}
if !strings.Contains(darkArea, "Quarterly numbers") || !strings.Contains(darkInput, "Jane Doe") {
t.Errorf("fields dropped their text:\n%q\n%q", darkArea, darkInput)
}
}
2 changes: 1 addition & 1 deletion internal/tui/journal.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ type journalPrompt struct {
}

func newJournalPrompt(kind journalPromptKind, value string, styles styles) *journalPrompt {
input := textinput.New()
input := newTextInput()
input.Prompt = ""
input.SetValue(value)
if kind == journalPromptSearch {
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/journal_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type journalForm struct {
}

func newJournalForm(date, content string, styles styles) *journalForm {
input := textarea.New()
input := newTextArea()
input.Prompt = ""
input.ShowLineNumbers = false
input.Placeholder = "Write about your day…"
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type mailSearchForm struct {
}

func newMailSearchForm(query string, styles styles) *mailSearchForm {
input := textinput.New()
input := newTextInput()
input.Prompt = ""
input.Placeholder = "Search threads and messages…"
input.SetValue(query)
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/snippets.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ type snippetPicker struct {
}

func newSnippetPicker(returnFocus int) *snippetPicker {
input := textinput.New()
input := newTextInput()
input.Prompt = ""
input.Placeholder = "Filter snippets…"
return &snippetPicker{input: input, cursor: -1, loading: true, returnFocus: returnFocus}
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/time_track.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ type timeTrackCategoryManager struct {
}

func newTimeTrackCategoryManager() *timeTrackCategoryManager {
input := textinput.New()
input := newTextInput()
input.Prompt = ""
input.Placeholder = "Category title…"
return &timeTrackCategoryManager{input: input}
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/time_track_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func newTimeTrackForm(track trackedTime, categories []generated.TimeTrackCategor
}

func trackInput(placeholder string) textinput.Model {
input := textinput.New()
input := newTextInput()
input.Prompt = ""
input.Placeholder = placeholder
return input
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/todos.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type todoPicker struct {
}

func newTodoPicker(todos []Recording) *todoPicker {
input := textinput.New()
input := newTextInput()
input.Prompt = ""
input.Placeholder = "Renew passport"
return &todoPicker{todos: todos, input: input}
Expand Down
Loading