From 63ae005f1da83829ca15dbca48e76bc5705bc396 Mon Sep 17 00:00:00 2001 From: Kirn Kim Date: Tue, 12 Jul 2022 12:08:21 -0700 Subject: [PATCH 1/3] initial filepicker code initial column code added right file pane showing fixed trailing cursor and range error panes reversed, selected item changed from box to highlight left and right panes functional, still need to manage when directory is large clean up code, fixed crash on empty directory found code redundancy, new panel append error permission issue resolved, new path error on key-right append error fixed fixed lag on directory permission error list rendering functional items disappearing on long directories, highlight sticking on first file item right pane highlight issue removed, left pane bounds error still needs work pagination functional refactor dirEntry to struct for more detailed file info file type colourisation functional, need to add code to enter directory symlinks resolving symlink dir working filepath.Join not placing root / in front of resolved symlink keypress right into symlink dir in root functional help items accessible, but filter not functional refactor key right to separate methods, key-right functionality given to key-enter and key-space remove repetitive logic, refactor methods to funcs fixed range error when key-right in empty directory key-left/right not functional when filtering SelectedFileInfo code added width resizing functional refactor pane code into FillPanes func filtering functional file selection on key-enter fully functional m.Cursor reimplemented due to filtering m.Cursor re-removed, key-right on filtering functional code cleanup, removing commented code fix left pane dir info error, clean up code fixed missing selected file path when filter is active fix logging errors and clean up comments/commented code shortening long file names functional filter highlighting functional, need to remove fileSelection lipgloss on empty or perm-denied dirs code cleanup, remove commented test code added delegate file added delegate file extension filter partially working, need right pane update and include folders adding arguments to new model func initDir and fileExt refactored, but bubbletea filter not functional again bubbletea filtering functional added symlinkdir to fileExt filter, but not all symlinkdir populating all symlinks populating on fileExt filter windows OS testing pass clean up comments and refactor triple OR helpStyle at full screen width Refactoring scope on all functions and properties refactor New func arguments to new funcs and refactor code fix symlink color and os perm mode Export file styles so they can be customized --- filepicker/delegate.go | 118 ++++++++++ filepicker/filepicker.go | 494 +++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 8 + list/list.go | 2 +- 5 files changed, 622 insertions(+), 1 deletion(-) create mode 100644 filepicker/delegate.go create mode 100644 filepicker/filepicker.go diff --git a/filepicker/delegate.go b/filepicker/delegate.go new file mode 100644 index 00000000..d68339bb --- /dev/null +++ b/filepicker/delegate.go @@ -0,0 +1,118 @@ +package filepicker + +import ( + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +type FileNameStyles struct { + FileNotSelected lipgloss.Style + FileRegular lipgloss.Style + FileDirectory lipgloss.Style + FileSymLink lipgloss.Style + FileBlockDevice lipgloss.Style + FileSelected lipgloss.Style + FilterMatch lipgloss.Style +} + +func newFileNameStyles() (s FileNameStyles) { + s.FileNotSelected = lipgloss.NewStyle() + s.FileRegular = lipgloss.NewStyle() + s.FileDirectory = lipgloss.NewStyle().Bold(true). + Foreground(lipgloss.Color("32")) + s.FileSymLink = lipgloss.NewStyle().Bold(true). + Foreground(lipgloss.Color("36")) + s.FileBlockDevice = lipgloss.NewStyle().Bold(true). + Foreground(lipgloss.Color("33")). + Background(lipgloss.Color("40")) + s.FileSelected = lipgloss.NewStyle(). + Background(lipgloss.Color("#FFFF00")) + s.FilterMatch = lipgloss.NewStyle().Underline(true) + return s +} + +func newItemDelegate(keys *delegateKeyMap) list.DefaultDelegate { + d := list.NewDefaultDelegate() + + d.UpdateFunc = func(msg tea.Msg, m *list.Model) tea.Cmd { + var title string + + if i, ok := m.SelectedItem().(listItem); ok { + title = i.value + } else { + return nil + } + statusMessageStyle := lipgloss.NewStyle(). + Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#04B575"}).Render + + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, keys.choose): + return m.NewStatusMessage(statusMessageStyle("You chose " + title)) + + case key.Matches(msg, keys.remove): + index := m.Index() + m.RemoveItem(index) + if len(m.Items()) == 0 { + keys.remove.SetEnabled(false) + } + return m.NewStatusMessage(statusMessageStyle("Deleted " + title)) + } + } + + return nil + } + + help := []key.Binding{keys.choose, keys.remove} + + d.ShortHelpFunc = func() []key.Binding { + return help + } + + d.FullHelpFunc = func() [][]key.Binding { + return [][]key.Binding{help} + } + + return d +} + +type delegateKeyMap struct { + choose key.Binding + remove key.Binding +} + +// Additional short help entries. This satisfies the help.KeyMap interface and +// is entirely optional. +func (d delegateKeyMap) ShortHelp() []key.Binding { + return []key.Binding{ + d.choose, + d.remove, + } +} + +// Additional full help entries. This satisfies the help.KeyMap interface and +// is entirely optional. +func (d delegateKeyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{ + { + d.choose, + d.remove, + }, + } +} + +func newDelegateKeyMap() *delegateKeyMap { + return &delegateKeyMap{ + choose: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "choose"), + ), + remove: key.NewBinding( + key.WithKeys("x", "backspace"), + key.WithHelp("x", "delete"), + ), + } +} diff --git a/filepicker/filepicker.go b/filepicker/filepicker.go new file mode 100644 index 00000000..b8b229e7 --- /dev/null +++ b/filepicker/filepicker.go @@ -0,0 +1,494 @@ +package filepicker + +import ( + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/muesli/reflow/truncate" + "golang.org/x/crypto/ssh/terminal" +) + +var ( + listWidth, listHeight, _ = terminal.GetSize(0) + docStyle = lipgloss.NewStyle().Margin(1, 2) + itemStyle = lipgloss.NewStyle().PaddingLeft(4) + + helpStyle = lipgloss.NewStyle(). + Height(5). + Width(listWidth) + + singlePaneStyle = lipgloss.NewStyle(). + Border(lipgloss.NormalBorder(), false, false, false, false). + MarginRight(2). + Height(listHeight - 2). + Width(listWidth - 2) + + dualPaneStyle = lipgloss.NewStyle(). + Border(lipgloss.NormalBorder(), false, true, false, false). + MarginRight(2). + Height(listHeight - 2). + Width(listWidth/2 - 2) + + statusMessageStyle = lipgloss.NewStyle(). + Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#04B575"}). + Render +) + +type ( + // Model contains the state of the filepicker view. + Model struct { + lFileInfo []listItem // files in the parent directory + rFileInfo []listItem // files in the directory + directory string + dirErr error + lList list.Model + rList list.Model + SelectionCompleted bool + SelectedFileInfo listItem + fileExt string + dualPane bool + } + + listItem struct { + value string + Entry os.DirEntry + Dir string + } + + itemDelegate struct { + styles FileNameStyles + defDelegate list.DefaultDelegate + dualPane bool + } +) + +const ( + permDenied = "" + dirEmpty = "" + bullet = "•" + ellipsis = "…" +) + +func (l listItem) FilterValue() string { + return l.value +} + +func (d itemDelegate) Height() int { return 1 } + +func (d itemDelegate) Spacing() int { return 0 } + +func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil } + +func (d itemDelegate) Render(w io.Writer, m list.Model, index int, lstItem list.Item) { + d.styles = newFileNameStyles() + s := &d.styles + var matchedRunes []int + i, ok := lstItem.(listItem) + if !ok { + return + } + fileName := i.value + + if m.Width() <= 0 { + return + } + + // Prevent text from exceeding list width + var textwidth uint + if d.dualPane { + textwidth = uint(m.Width() - dualPaneStyle.GetPaddingLeft() - dualPaneStyle.GetPaddingRight()) + } else { + textwidth = uint(m.Width() - singlePaneStyle.GetPaddingLeft() - singlePaneStyle.GetPaddingRight()) + } + fileName = truncate.StringWithTail(fileName, textwidth, ellipsis) + + // Conditions + var ( + isSelected = index == m.Index() + emptyFilter = m.FilterState() == list.Filtering && + m.FilterValue() == "" + isFiltered = m.FilterState() == list.Filtering || + m.FilterState() == list.FilterApplied + ) + + if isFiltered && index < len(m.VisibleItems()) { + // Get indices of matched characters + matchedRunes = m.MatchesForItem(index) + } + if emptyFilter { + } else if isSelected && m.FilterState() != list.Filtering { + if isFiltered { + // Highlight matches + unmatched := s.FileSelected.Inline(true) + matched := unmatched.Copy().Inherit(s.FilterMatch) + fileName = lipgloss.StyleRunes(fileName, matchedRunes, matched, unmatched) + } + fileName = s.FileSelected.Render(fileName) + } else { + if isFiltered { + // Highlight matches + unmatched := s.FileRegular.Inline(true) + matched := unmatched.Copy().Inherit(s.FilterMatch) + fileName = lipgloss.StyleRunes(fileName, matchedRunes, matched, unmatched) + } + fileName = s.FileRegular.Render(fileName) + } + + var fn func(str string) string + if i.Entry == nil { + fn = s.FileRegular.Render + fmt.Fprint(w, fn(fileName)) + return + } + switch mode := i.Entry.Type(); { + case isDir(i.Entry, i.Dir): + fn = s.FileDirectory.Render + case mode&os.ModeSymlink != 0: + fn = s.FileSymLink.Render + case mode&os.ModeDevice != 0: + fn = s.FileBlockDevice.Render + default: + fn = s.FileRegular.Render + } + if index == m.Index() { + fileName = fn(fileName) + fn = s.FileSelected.Render + } + + fmt.Fprint(w, fn(fileName)) +} + +func (m Model) WithFileExt(fileExt string) Model { + m.lFileInfo = []listItem{} + m.rFileInfo = []listItem{} + m.fileExt = strings.TrimSpace(fileExt) + return m.getFileinfo() +} + +func (m Model) WithInitDir(initDir string) Model { + var err error + m.lFileInfo = []listItem{} + m.rFileInfo = []listItem{} + m.directory = strings.TrimSpace(initDir) + m.directory, err = filepath.Abs(m.directory) + if err != nil { + m.dirErr = err + } + m.directory = strings.TrimSpace(m.directory) + + return m.getFileinfo() +} + +func (m Model) WithDualPane(dualPane bool) Model { + m.dualPane = dualPane + return m +} + +// New creates a new filepicker view with some useful defaults. +func New() Model { + var ( + m Model + err error + ) + m.dualPane = true + if m.directory == "" { + m.directory, err = os.Getwd() + if err != nil { + m.dirErr = err + } + } + m.directory, err = filepath.Abs(m.directory) + if err != nil { + m.dirErr = err + } + m.directory = strings.TrimSpace(m.directory) + m.lList = list.New([]list.Item{}, itemDelegate{dualPane: m.dualPane}, 0, 0) + m.lList.SetShowHelp(false) + m.rList = list.New([]list.Item{}, itemDelegate{dualPane: m.dualPane}, 0, 0) + m.rList.SetShowHelp(false) + m.rList.KeyMap = list.DefaultKeyMap() + m.rList.KeyMap.NextPage = key.NewBinding(key.WithKeys("pgdown"), + key.WithHelp("pgdn", "next page")) + m.rList.KeyMap.PrevPage = key.NewBinding(key.WithKeys("pgup"), + key.WithHelp("pgup", "prev page")) + m.rList.SetShowTitle(false) + m.lList.SetShowTitle(false) + return m.getFileinfo() +} + +func (m Model) Init() tea.Cmd { + if m.dirErr != nil { + return tea.Quit + } + // Just return `nil`, which means "no I/O right now, please." + return nil +} + +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "right": + // Permission to be in current dir? + if m.dirErr == nil { + // Is current dir empty? + if len(m.rFileInfo) != 0 { + // assigning var in case filtering is active + vi := m.rList.VisibleItems()[m.rList.Index()].(listItem) + // Is selected item a dir or symlink to a dir? + if isDir(vi.Entry, m.directory) { + m.rList, _ = m.rList.Update(msg) + m = m.runKeyRight(msg) + } + } + } + case "left": + // Is filter being used or already applied? + if m.rList.SettingFilter() { + break + } + if m.rList.IsFiltered() { + m.rList.ResetFilter() + } + + rootDir, _ := filepath.Abs("/") + if m.directory == rootDir { + break + } + m.dirErr = nil + m.rFileInfo = m.lFileInfo + // place active selection at former parent directory + m.rList.Select(m.lList.Index()) + m.directory = filepath.Dir(m.directory) + // read file entries in current parent dir + files, err := os.ReadDir(filepath.Dir(m.directory)) + if err != nil { + return m, nil + } + m.lFileInfo = nil + for _, f := range files { + li := listItem{Entry: f, Dir: filepath.Dir(m.directory)} + if isDir(f, filepath.Dir(m.directory)) { + m.lFileInfo = append(m.lFileInfo, li) + } else if strings.HasSuffix(f.Name(), m.fileExt) { + m.lFileInfo = append(m.lFileInfo, li) + } + } + m = m.fillPanes(msg) + for i, f := range m.lList.Items() { + if f.FilterValue() == filepath.Base(m.directory) { + m.lList.Select(i) + } + } + + case "enter": + filterVal := m.rList.Items()[0].FilterValue() + // Cannot select file when in a dir without permissions + // or when dir is empty + if filterVal == permDenied || filterVal == dirEmpty { + break + } + + // if setting filter, enable filter + if m.rList.SettingFilter() { + m.rList.SetFilteringEnabled(true) + m.rList, cmd = m.rList.Update(msg) + return m, cmd + } + // setting selected file info + m.SelectionCompleted = true + if li, ok := m.rList.VisibleItems()[m.rList.Index()].(listItem); ok { + for _, f := range m.rFileInfo { + if f.Entry.Name() == li.Entry.Name() { + m.SelectedFileInfo = f + } + } + } + return m, tea.Quit + + case "ctrl+c": + return m, tea.Quit + + default: + m.rList, cmd = m.rList.Update(msg) + return m, cmd + } + + case tea.WindowSizeMsg: + singlePaneStyle = singlePaneStyle.Width(msg.Width - 2).Height(msg.Height - 16) + dualPaneStyle = dualPaneStyle.Width(msg.Width/2 - 2).Height(msg.Height - 16) + helpStyle = helpStyle.Width(msg.Width - 1) + m.lList.SetSize(msg.Width/2-2, msg.Height-16) + if m.dualPane { + m.rList.SetSize(msg.Width/2-2, msg.Height-16) + } else { + m.rList.SetSize(msg.Width-2, msg.Height-16) + } + return m, nil + } + + m.rList, _ = m.rList.Update(msg) + return m, cmd +} + +// View renders the help view's current state. +func (m Model) View() string { + if m.dirErr != nil { + if !os.IsPermission(m.dirErr) { + dirErrPane := lipgloss.JoinHorizontal(lipgloss.Top, + helpStyle.Render(fmt.Sprintf("%v", m.dirErr))) + return dirErrPane + } + } + if !m.dualPane { + filePane := singlePaneStyle.Render( + m.rList.View(), + ) + m.rList.Help.Width = listWidth + windowPane := lipgloss.JoinVertical(lipgloss.Top, filePane, + helpStyle.Render(m.rList.Help.View(m.rList)), + ) + return windowPane + } + filePane := lipgloss.JoinHorizontal(lipgloss.Top, + dualPaneStyle.Render( + m.lList.View(), + ), + dualPaneStyle.Render( + m.rList.View(), + ), + ) + m.rList.Help.Width = listWidth + windowPane := lipgloss.JoinVertical(lipgloss.Top, + filePane, + helpStyle.Render(m.rList.Help.View(m.rList)), + ) + + return windowPane +} + +func (m Model) getFileinfo() Model { + var msg tea.Msg + lFiles, lErr := os.ReadDir(filepath.Dir(m.directory)) + if lErr != nil { + m.dirErr = lErr + return m + } + for _, f := range lFiles { + li := listItem{Entry: f, Dir: m.directory} + // include directories in filter regardless of name + if isDir(f, filepath.Dir(m.directory)) { + m.lFileInfo = append(m.lFileInfo, li) + // include only files with matching suffix (extension) + } else if strings.HasSuffix(f.Name(), m.fileExt) { + m.lFileInfo = append(m.lFileInfo, li) + } + } + rFiles, rErr := os.ReadDir(m.directory) + if rErr != nil { + m.dirErr = rErr + return m + } + for _, f := range rFiles { + li := listItem{Entry: f, Dir: m.directory} + if isDir(f, m.directory) { + m.rFileInfo = append(m.rFileInfo, li) + } else if strings.HasSuffix(f.Name(), m.fileExt) { + m.rFileInfo = append(m.rFileInfo, li) + } + } + + m = m.fillPanes(msg) + m.rList.ResetSelected() + for i, f := range m.lList.Items() { + if f.FilterValue() == filepath.Base(m.directory) { + m.lList.Select(i) + } + } + return m +} + +func (m Model) fillPanes(msg tea.Msg) Model { + leftPaneItems := []list.Item{} + for _, choice := range m.lFileInfo { + li := listItem{value: choice.Entry.Name(), Entry: choice.Entry, Dir: filepath.Dir(m.directory)} + leftPaneItems = append(leftPaneItems, li) + } + rightPaneItems := []list.Item{} + for _, choice := range m.rFileInfo { + li := listItem{value: choice.Entry.Name(), Entry: choice.Entry, Dir: m.directory} + rightPaneItems = append(rightPaneItems, li) + } + if os.IsPermission(m.dirErr) { + rightPaneItems = []list.Item{listItem{value: permDenied}} + } + if len(rightPaneItems) == 0 { + rightPaneItems = []list.Item{listItem{value: dirEmpty}} + } + rootDir, _ := filepath.Abs("/") + if m.directory == rootDir { + leftPaneItems = []list.Item{listItem{value: rootDir}} + } + m.rList.SetItems(rightPaneItems) + m.lList.SetItems(leftPaneItems) + m.rList.Title = m.directory + return m +} + +func isSymLinkToDir(f os.DirEntry, parent string) bool { + base := f.Name() + linkDest, err := filepath.EvalSymlinks(filepath.Join(parent, base)) + if err != nil { + return false + } + linkDir, dirErr := os.Lstat(linkDest) + if dirErr != nil { + return false + } + return linkDir.IsDir() +} + +// isDir returns true if fileEntry is a dir or a symlink to a dir +func isDir(fileEntry fs.DirEntry, parent string) bool { + if fileEntry.Type()&os.ModeSymlink != 0 { + return isSymLinkToDir(fileEntry, parent) + } + return fileEntry.IsDir() +} + +func (m Model) runKeyRight(msg tea.Msg) Model { + m.lFileInfo = m.rFileInfo + vi := m.rList.VisibleItems()[m.rList.Index()].(listItem).Entry.Name() + m.directory = filepath.Join(m.directory, vi) + files, err := os.ReadDir(m.directory) + m.dirErr = err + m.rFileInfo = nil + for _, f := range files { + li := listItem{Entry: f, Dir: m.directory} + if isDir(f, m.directory) { + m.rFileInfo = append(m.rFileInfo, li) + } else if strings.HasSuffix(f.Name(), m.fileExt) { + m.rFileInfo = append(m.rFileInfo, li) + } + } + + // If filter is being used, reset filter + m.rList.ResetFilter() + m = m.fillPanes(msg) + m.rList.ResetSelected() + for i, f := range m.lList.Items() { + if f.FilterValue() == filepath.Base(m.directory) { + m.lList.Select(i) + } + } + return m +} diff --git a/go.mod b/go.mod index 210ba983..55d8b692 100644 --- a/go.mod +++ b/go.mod @@ -13,4 +13,5 @@ require ( github.com/muesli/reflow v0.3.0 github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 github.com/sahilm/fuzzy v0.1.0 + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d ) diff --git a/go.sum b/go.sum index cdee52b5..9a632e6e 100644 --- a/go.sum +++ b/go.sum @@ -36,11 +36,19 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/list/list.go b/list/list.go index bec5e6fa..ec42f6bf 100644 --- a/list/list.go +++ b/list/list.go @@ -87,7 +87,7 @@ type Rank struct { // DefaultFilter uses the sahilm/fuzzy to filter through the list. // This is set by default. func DefaultFilter(term string, targets []string) []Rank { - var ranks = fuzzy.Find(term, targets) + ranks := fuzzy.Find(term, targets) sort.Stable(ranks) result := make([]Rank, len(ranks)) for i, r := range ranks { From 8c234f3e609a7e3d1c063a586be9594b765edeac Mon Sep 17 00:00:00 2001 From: Tai Groot Date: Fri, 23 Sep 2022 19:29:33 -0700 Subject: [PATCH 2/3] Revert "Export file styles so they can be customized" This reverts commit 18d4fff825f53272ea5877dac7fe03a2d4c5a8cb. --- filepicker/delegate.go | 32 ++++++++++++++++---------------- filepicker/filepicker.go | 26 +++++++++++++------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/filepicker/delegate.go b/filepicker/delegate.go index d68339bb..ff7c1c0f 100644 --- a/filepicker/delegate.go +++ b/filepicker/delegate.go @@ -7,29 +7,29 @@ import ( "github.com/charmbracelet/lipgloss" ) -type FileNameStyles struct { - FileNotSelected lipgloss.Style - FileRegular lipgloss.Style - FileDirectory lipgloss.Style - FileSymLink lipgloss.Style - FileBlockDevice lipgloss.Style - FileSelected lipgloss.Style - FilterMatch lipgloss.Style +type fileNameStyles struct { + fileNotSelected lipgloss.Style + fileRegular lipgloss.Style + fileDirectory lipgloss.Style + fileSymLink lipgloss.Style + fileBlockDevice lipgloss.Style + fileSelected lipgloss.Style + filterMatch lipgloss.Style } -func newFileNameStyles() (s FileNameStyles) { - s.FileNotSelected = lipgloss.NewStyle() - s.FileRegular = lipgloss.NewStyle() - s.FileDirectory = lipgloss.NewStyle().Bold(true). +func newFileNameStyles() (s fileNameStyles) { + s.fileNotSelected = lipgloss.NewStyle() + s.fileRegular = lipgloss.NewStyle() + s.fileDirectory = lipgloss.NewStyle().Bold(true). Foreground(lipgloss.Color("32")) - s.FileSymLink = lipgloss.NewStyle().Bold(true). + s.fileSymLink = lipgloss.NewStyle().Bold(true). Foreground(lipgloss.Color("36")) - s.FileBlockDevice = lipgloss.NewStyle().Bold(true). + s.fileBlockDevice = lipgloss.NewStyle().Bold(true). Foreground(lipgloss.Color("33")). Background(lipgloss.Color("40")) - s.FileSelected = lipgloss.NewStyle(). + s.fileSelected = lipgloss.NewStyle(). Background(lipgloss.Color("#FFFF00")) - s.FilterMatch = lipgloss.NewStyle().Underline(true) + s.filterMatch = lipgloss.NewStyle().Underline(true) return s } diff --git a/filepicker/filepicker.go b/filepicker/filepicker.go index b8b229e7..e94bcf13 100644 --- a/filepicker/filepicker.go +++ b/filepicker/filepicker.go @@ -64,7 +64,7 @@ type ( } itemDelegate struct { - styles FileNameStyles + styles fileNameStyles defDelegate list.DefaultDelegate dualPane bool } @@ -127,40 +127,40 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, lstItem list. } else if isSelected && m.FilterState() != list.Filtering { if isFiltered { // Highlight matches - unmatched := s.FileSelected.Inline(true) - matched := unmatched.Copy().Inherit(s.FilterMatch) + unmatched := s.fileSelected.Inline(true) + matched := unmatched.Copy().Inherit(s.filterMatch) fileName = lipgloss.StyleRunes(fileName, matchedRunes, matched, unmatched) } - fileName = s.FileSelected.Render(fileName) + fileName = s.fileSelected.Render(fileName) } else { if isFiltered { // Highlight matches - unmatched := s.FileRegular.Inline(true) - matched := unmatched.Copy().Inherit(s.FilterMatch) + unmatched := s.fileRegular.Inline(true) + matched := unmatched.Copy().Inherit(s.filterMatch) fileName = lipgloss.StyleRunes(fileName, matchedRunes, matched, unmatched) } - fileName = s.FileRegular.Render(fileName) + fileName = s.fileRegular.Render(fileName) } var fn func(str string) string if i.Entry == nil { - fn = s.FileRegular.Render + fn = s.fileRegular.Render fmt.Fprint(w, fn(fileName)) return } switch mode := i.Entry.Type(); { case isDir(i.Entry, i.Dir): - fn = s.FileDirectory.Render + fn = s.fileDirectory.Render case mode&os.ModeSymlink != 0: - fn = s.FileSymLink.Render + fn = s.fileSymLink.Render case mode&os.ModeDevice != 0: - fn = s.FileBlockDevice.Render + fn = s.fileBlockDevice.Render default: - fn = s.FileRegular.Render + fn = s.fileRegular.Render } if index == m.Index() { fileName = fn(fileName) - fn = s.FileSelected.Render + fn = s.fileSelected.Render } fmt.Fprint(w, fn(fileName)) From 01c5095ae4ec74c5cb6ee3cbc745b911bd979f4a Mon Sep 17 00:00:00 2001 From: Tai Groot Date: Mon, 23 Jan 2023 13:23:05 -0800 Subject: [PATCH 3/3] make highlighted item easier to read --- filepicker/delegate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/filepicker/delegate.go b/filepicker/delegate.go index ff7c1c0f..b6b0cf33 100644 --- a/filepicker/delegate.go +++ b/filepicker/delegate.go @@ -28,7 +28,7 @@ func newFileNameStyles() (s fileNameStyles) { Foreground(lipgloss.Color("33")). Background(lipgloss.Color("40")) s.fileSelected = lipgloss.NewStyle(). - Background(lipgloss.Color("#FFFF00")) + Background(lipgloss.Color("#FFFF00")).Foreground(lipgloss.Color("#000000")) s.filterMatch = lipgloss.NewStyle().Underline(true) return s }