From b8da81a61a1b587392359407b3d98c81158fae61 Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Sun, 17 May 2026 18:56:38 +0800 Subject: [PATCH 1/2] fix(filepicker): treat symlinked directories as navigable Fixes charmbracelet/gum#236. Resolve symlink targets when deciding if an entry is a directory so symlinks to folders can be opened instead of being selected as files. Co-authored-by: Cursor --- filepicker/filepicker.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/filepicker/filepicker.go b/filepicker/filepicker.go index 1d2a1cc2..45bc7fd9 100644 --- a/filepicker/filepicker.go +++ b/filepicker/filepicker.go @@ -385,11 +385,15 @@ func (m Model) View() string { size := strings.Replace(humanize.Bytes(uint64(info.Size())), " ", "", 1) //nolint:gosec name := f.Name() + isDir := f.IsDir() if isSymlink { symlinkPath, _ = filepath.EvalSymlinks(filepath.Join(m.CurrentDirectory, name)) + if target, err := os.Stat(symlinkPath); err == nil && target.IsDir() { + isDir = true + } } - disabled := !m.canSelect(name) && !f.IsDir() + disabled := !m.canSelect(name) && !isDir if m.selected == i { //nolint:nestif selected := "" @@ -495,7 +499,10 @@ func (m Model) didSelectFile(msg tea.Msg) (bool, string) { } } - if (!isDir && m.FileAllowed) || (isDir && m.DirAllowed) && m.Path != "" { + if isDir { + return false, "" + } + if m.FileAllowed && m.Path != "" { return true, m.Path } From 0ef61785e48c179cc2346f14358d0f5e7bc5de88 Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Sun, 17 May 2026 19:19:03 +0800 Subject: [PATCH 2/2] fix(textinput): use display width for cursor X offset Fixes #906. Compute bubbletea cursor X using uniseg string width so wide characters such as CJK glyphs position the cursor correctly. Co-authored-by: Cursor --- textinput/textinput.go | 3 +-- textinput/textinput_test.go | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/textinput/textinput.go b/textinput/textinput.go index 363089b2..df83e0d7 100644 --- a/textinput/textinput.go +++ b/textinput/textinput.go @@ -921,8 +921,7 @@ func (m Model) Cursor() *tea.Cursor { w := lipgloss.Width promptWidth := w(m.promptView()) - xOffset := m.Position() + - promptWidth + xOffset := promptWidth + uniseg.StringWidth(string(m.value[:m.pos])) if m.width > 0 { xOffset = min(xOffset, m.width+promptWidth) } diff --git a/textinput/textinput_test.go b/textinput/textinput_test.go index b5e344b9..3e223c2c 100644 --- a/textinput/textinput_test.go +++ b/textinput/textinput_test.go @@ -118,3 +118,22 @@ func sendString(m Model, str string) Model { return m } + +func TestCursorWideCharacterOffset(t *testing.T) { + t.Parallel() + + m := New() + m.Prompt = "" + m.Focus() + m.SetVirtualCursor(false) + m.SetValue("你好") + m.SetCursor(len([]rune("你好"))) + + c := m.Cursor() + if c == nil { + t.Fatal("expected cursor, got nil") + } + if c.Position.X != 4 { + t.Fatalf("expected cursor X offset 4 for two wide runes, got %d", c.Position.X) + } +}