Skip to content
Open
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
47 changes: 20 additions & 27 deletions internal/ui/bookmarks/bookmarks.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import (

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"

"github.com/kooler/MiddayCommander/internal/bookmark"
"github.com/kooler/MiddayCommander/internal/ui/overlay"
uitext "github.com/kooler/MiddayCommander/internal/ui/text"
"github.com/kooler/MiddayCommander/internal/ui/theme"
)

Expand All @@ -22,17 +24,17 @@ type DismissMsg struct{}

// Model is the bookmark list overlay.
type Model struct {
store *bookmark.Store
items []bookmark.Bookmark
cursor int
offset int
width int
height int
store *bookmark.Store
items []bookmark.Bookmark
cursor int
offset int
width int
height int
filter string // search/filter query
filtering bool // true when filter input is active
adding bool // true when prompting for bookmark name
addPath string // path being bookmarked
addName string // name being typed
addPath string // path being bookmarked
addName string // name being typed
}

// New creates a new bookmark list overlay.
Expand Down Expand Up @@ -70,8 +72,8 @@ func (m Model) Update(msg tea.KeyMsg) (Model, tea.Cmd) {
}
m.filtering = false
case "backspace":
if len(m.filter) > 0 {
m.filter = m.filter[:len(m.filter)-1]
if start := uitext.PreviousGraphemeBoundary(m.filter, len(m.filter)); start >= 0 {
m.filter = m.filter[:start]
m.refilter()
} else {
m.filtering = false
Expand All @@ -87,8 +89,7 @@ func (m Model) Update(msg tea.KeyMsg) (Model, tea.Cmd) {
m.clampOffset()
}
default:
s := msg.String()
if len(s) == 1 && s[0] >= 32 {
if s, ok := uitext.PrintableInput(msg); ok {
m.filter += s
m.refilter()
}
Expand Down Expand Up @@ -153,13 +154,12 @@ func (m Model) updateAdding(msg tea.KeyMsg) (Model, tea.Cmd) {
m.adding = false
return m, nil
case "backspace":
if len(m.addName) > 0 {
m.addName = m.addName[:len(m.addName)-1]
if start := uitext.PreviousGraphemeBoundary(m.addName, len(m.addName)); start >= 0 {
m.addName = m.addName[:start]
}
return m, nil
default:
s := msg.String()
if len(s) == 1 && s[0] >= 32 {
if s, ok := uitext.PrintableInput(msg); ok {
m.addName += s
}
return m, nil
Expand Down Expand Up @@ -288,15 +288,15 @@ func (m Model) View(th theme.Theme, screenWidth, screenHeight int) string {
if b.Name != "" {
display = b.Name + " → " + b.Path
}
if len(display) > innerW-4 {
display = "…" + display[len(display)-innerW+5:]
if ansi.StringWidth(display) > innerW-4 {
display = overlay.TruncateLeftEllipsis(display, innerW-4)
}

line := prefix + display
if isCursor {
contentLines = append(contentLines, cursorStyle.Render(padStr(line, innerW)))
contentLines = append(contentLines, cursorStyle.Render(overlay.PadOrTrunc(line, innerW)))
} else {
contentLines = append(contentLines, numStyle.Render(prefix)+bgStyle.Render(padStr(display, innerW-len(prefix))))
contentLines = append(contentLines, numStyle.Render(prefix)+bgStyle.Render(overlay.PadOrTrunc(display, innerW-len(prefix))))
}
}

Expand Down Expand Up @@ -332,10 +332,3 @@ func (m Model) View(th theme.Theme, screenWidth, screenHeight int) string {
return overlay.RenderBox("Bookmarks", contentLines, footer, boxW, boxH,
accent, bg, highlight)
}

func padStr(s string, width int) string {
if len(s) >= width {
return s[:width]
}
return s + strings.Repeat(" ", width-len(s))
}
44 changes: 44 additions & 0 deletions internal/ui/bookmarks/bookmarks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package bookmarks

import (
"testing"
"unicode/utf8"

tea "github.com/charmbracelet/bubbletea"
"github.com/kooler/MiddayCommander/internal/bookmark"
)

func bookmarkKey(m Model, key tea.KeyType, runes ...rune) Model {
var msg tea.KeyMsg
if runes != nil {
msg = tea.KeyMsg{Type: tea.KeyRunes, Runes: runes}
} else {
msg = tea.KeyMsg{Type: key}
}
updated, _ := m.Update(msg)
return updated
}

func TestFilterUnicodeEditing(t *testing.T) {
m := New(&bookmark.Store{}, ".", 80, 24)
m.filtering = true
m = bookmarkKey(m, tea.KeyRunes, '日', '本', '🎉')
m = bookmarkKey(m, tea.KeyBackspace)
if m.filter != "日本" || !utf8.ValidString(m.filter) {
t.Errorf("filter after backspace = %q, want 日本", m.filter)
}
m = bookmarkKey(m, tea.KeySpace)
if m.filter != "日本 " {
t.Errorf("filter after space = %q, want 日本 ", m.filter)
}
}

func TestAddNameUnicodeEditing(t *testing.T) {
m := New(&bookmark.Store{}, ".", 80, 24)
m.adding = true
m = bookmarkKey(m, tea.KeyRunes, '👨', '\u200d', '👩')
m = bookmarkKey(m, tea.KeyBackspace)
if m.addName != "" {
t.Errorf("name after backspace = %q, want empty", m.addName)
}
}
40 changes: 23 additions & 17 deletions internal/ui/cmdexec/cmdexec.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import (

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"

"github.com/kooler/MiddayCommander/internal/ui/completion"
"github.com/kooler/MiddayCommander/internal/ui/overlay"
uitext "github.com/kooler/MiddayCommander/internal/ui/text"
"github.com/kooler/MiddayCommander/internal/ui/theme"
)

Expand Down Expand Up @@ -114,32 +116,36 @@ func (m Model) handleKey(msg tea.KeyMsg) (Model, tea.Cmd) {
return m.updateSuggestions(), nil

case "backspace":
if m.inputPos > 0 {
m.input = m.input[:m.inputPos-1] + m.input[m.inputPos:]
m.inputPos--
start := uitext.PreviousGraphemeBoundary(m.input, m.inputPos)
if start >= 0 {
m.input = m.input[:start] + m.input[m.inputPos:]
m.inputPos = start
}
m.output = ""
m.outputLines = nil
m.outputOffset = 0
m = m.updateSuggestions()

case "delete":
if m.inputPos < len(m.input) {
m.input = m.input[:m.inputPos] + m.input[m.inputPos+1:]
end := uitext.NextGraphemeBoundary(m.input, m.inputPos)
if end > m.inputPos {
m.input = m.input[:m.inputPos] + m.input[end:]
}
m.output = ""
m.outputLines = nil
m.outputOffset = 0
m = m.updateSuggestions()

case "left":
if m.inputPos > 0 {
m.inputPos--
start := uitext.PreviousGraphemeBoundary(m.input, m.inputPos)
if start >= 0 {
m.inputPos = start
}

case "right":
if m.inputPos < len(m.input) {
m.inputPos++
end := uitext.NextGraphemeBoundary(m.input, m.inputPos)
if end > m.inputPos {
m.inputPos = end
}

case "home":
Expand Down Expand Up @@ -179,15 +185,14 @@ func (m Model) handleKey(msg tea.KeyMsg) (Model, tea.Cmd) {
}

default:
s := msg.String()
if len(s) == 1 && s[0] >= 32 {
if s, ok := uitext.PrintableInput(msg); ok {
if m.inputPos < 0 {
m.inputPos = 0
} else if m.inputPos > len(m.input) {
m.inputPos = len(m.input)
}
m.input = m.input[:m.inputPos] + s + m.input[m.inputPos:]
m.inputPos++
m.inputPos += len(s)
m.output = ""
m.outputLines = nil
m.outputOffset = 0
Expand Down Expand Up @@ -238,8 +243,8 @@ func (m Model) View(th theme.Theme, screenWidth, screenHeight int) string {

// Directory line
dir := m.dir
if len(dir) > innerW-2 {
dir = "..." + dir[len(dir)-innerW+5:]
if ansi.StringWidth(dir) > innerW-2 {
dir = overlay.TruncateLeftEllipsis(dir, innerW-2)
}
dirLine := dimStyle.Render(" " + dir)
dirWidth := lipgloss.Width(dirLine)
Expand All @@ -251,7 +256,8 @@ func (m Model) View(th theme.Theme, screenWidth, screenHeight int) string {
// Input line with cursor
var inputDisplay string
if m.inputPos < len(m.input) {
inputDisplay = m.input[:m.inputPos] + "█" + m.input[m.inputPos:]
cluster, _ := ansi.FirstGraphemeCluster(m.input[m.inputPos:], ansi.GraphemeWidth)
inputDisplay = m.input[:m.inputPos] + "█" + m.input[m.inputPos+len(cluster):]
} else {
inputDisplay = m.input + "█"
}
Expand Down Expand Up @@ -286,8 +292,8 @@ func (m Model) View(th theme.Theme, screenWidth, screenHeight int) string {
}
for i := m.outputOffset; i < end; i++ {
line := " " + m.outputLines[i]
if lipgloss.Width(line) > innerW {
line = line[:innerW]
if ansi.StringWidth(line) > innerW {
line = ansi.Truncate(line, innerW, "")
}
rendered := bgStyle.Render(line)
renderedWidth := lipgloss.Width(rendered)
Expand Down
46 changes: 46 additions & 0 deletions internal/ui/cmdexec/cmdexec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package cmdexec

import (
"testing"
"unicode/utf8"

tea "github.com/charmbracelet/bubbletea"
)

func press(m *Model, key tea.KeyType, runes ...rune) {
var msg tea.KeyMsg
if runes != nil {
msg = tea.KeyMsg{Type: tea.KeyRunes, Runes: runes}
} else {
msg = tea.KeyMsg{Type: key}
}
updated, _ := m.handleKey(msg)
*m = updated
}

func TestUnicodeInputEditing(t *testing.T) {
m := New(".", 80, 24)
press(&m, tea.KeyRunes, '日', '本')
press(&m, tea.KeyRunes, '🎉')
if m.input != "日本🎉" {
t.Fatalf("input = %q, want 日本🎉", m.input)
}

press(&m, tea.KeyBackspace)
if m.input != "日本" || m.inputPos != len(m.input) {
t.Errorf("after backspace: input = %q, position = %d", m.input, m.inputPos)
}
press(&m, tea.KeyLeft)
press(&m, tea.KeyDelete)
if m.input != "日" || !utf8.ValidString(m.input) {
t.Errorf("after delete: input = %q, want 日", m.input)
}
}

func TestSpaceInput(t *testing.T) {
m := New(".", 80, 24)
press(&m, tea.KeySpace)
if m.input != " " {
t.Errorf("input = %q, want a space", m.input)
}
}
10 changes: 0 additions & 10 deletions internal/ui/completion/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,6 @@ func CommonPrefix(strs []string) string {
return prefix
}

func PadOrTrim(s string, width int) string {
if lipgloss.Width(s) > width {
if width > 3 {
return s[:width-3] + "..."
}
return s[:width]
}
return s + strings.Repeat(" ", width-lipgloss.Width(s))
}

func FormatSuggestions(suggestions []string, width, maxLines int, basename bool) []string {
if len(suggestions) == 0 {
return nil
Expand Down
21 changes: 8 additions & 13 deletions internal/ui/copypath/copypath.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"

"github.com/kooler/MiddayCommander/internal/platform"
"github.com/kooler/MiddayCommander/internal/ui/overlay"
Expand Down Expand Up @@ -103,9 +104,10 @@ func dismiss() tea.Msg { return DismissMsg{} }

// BoxSize returns the desired box dimensions.
func (m Model) BoxSize(screenWidth, screenHeight int) (int, int) {
maxLen := len(helpText)
maxLen := ansi.StringWidth(helpText)
for _, p := range m.paths {
if l := len(p) + 2; l > maxLen { // +2 for the cursor/selection prefix
l := ansi.StringWidth(p) + 2 // +2 for the cursor/selection prefix
if l > maxLen {
maxLen = l
}
}
Expand Down Expand Up @@ -145,7 +147,7 @@ func (m Model) View(_ theme.Theme, screenWidth, screenHeight int) string {
dimStyle := lipgloss.NewStyle().Background(bg).Foreground(subtle)

var contentLines []string
contentLines = append(contentLines, dimStyle.Render(padStr(" "+helpText, innerW)))
contentLines = append(contentLines, dimStyle.Render(overlay.PadOrTrunc(" "+helpText, innerW)))
contentLines = append(contentLines, bgStyle.Render(strings.Repeat(" ", innerW)))

for i, p := range m.paths {
Expand All @@ -154,10 +156,10 @@ func (m Model) View(_ theme.Theme, screenWidth, screenHeight int) string {
prefix = "> "
}
display := p
if len(display) > innerW-len(prefix) {
display = "…" + display[len(display)-(innerW-len(prefix))+1:]
if ansi.StringWidth(display) > innerW-len(prefix) {
display = overlay.TruncateLeftEllipsis(display, innerW-len(prefix))
}
line := padStr(prefix+display, innerW)
line := overlay.PadOrTrunc(prefix+display, innerW)
if i == m.cursor {
contentLines = append(contentLines, cursorStyle.Render(line))
} else {
Expand All @@ -179,10 +181,3 @@ func (m Model) View(_ theme.Theme, screenWidth, screenHeight int) string {
return overlay.RenderBox("Copy Path", contentLines, footer, boxW, boxH,
accent, bg, highlight)
}

func padStr(s string, width int) string {
if lipgloss.Width(s) >= width {
return s
}
return s + strings.Repeat(" ", width-lipgloss.Width(s))
}
Loading