Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support hot reload #16

Merged
merged 7 commits into from Mar 26, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 21 additions & 8 deletions fzf.go
@@ -1,6 +1,7 @@
package fzf

import (
"errors"
"fmt"
"reflect"

Expand All @@ -13,7 +14,6 @@ var defaultFindOption = findOption{

// Fuzzy Finder.
type FZF struct {
option *option
model *model
program *tea.Program
}
Expand All @@ -28,7 +28,6 @@ func New(opts ...Option) *FZF {
m := newModel(&o)

return &FZF{
option: &o,
model: m,
program: tea.NewProgram(m),
}
Expand All @@ -42,18 +41,21 @@ func (fzf *FZF) Find(items interface{}, itemFunc func(i int) string, opts ...Fin
}

rv := reflect.ValueOf(items)
switch {
case rv.Kind() == reflect.Slice:
case rv.Kind() == reflect.Ptr && reflect.Indirect(rv).Kind() == reflect.Slice:
default:
return nil, fmt.Errorf("items must be a slice, but got %T", items)
if fzf.model.option.hotReloadLocker == nil {
if rv.Kind() != reflect.Slice {
return nil, fmt.Errorf("items must be a slice, but got %T", items)
}
} else {
if !(rv.Kind() == reflect.Ptr && reflect.Indirect(rv).Kind() == reflect.Slice) {
return nil, fmt.Errorf("items must be a pointer to slice, but got %T", items)
}
}

is, err := newItems(rv, itemFunc)
if err != nil {
return nil, err
}
fzf.model.setItems(is)
fzf.model.loadItems(is)
fzf.model.setFindOption(&findOption)

if _, err := fzf.program.Run(); err != nil {
Expand All @@ -67,6 +69,17 @@ func (fzf *FZF) Find(items interface{}, itemFunc func(i int) string, opts ...Fin
return fzf.model.choices, nil
}

// ForceReload forces the reload of items.
// HotReload must be enabled.
func (fzf *FZF) ForceReload() error {
if fzf.model.option.hotReloadLocker == nil {
return errors.New("hot reload is not enabled")
}

fzf.program.Send(forceReloadMsg{})
return nil
}

// Quit quits the Fuzzy Finder.
func (fzf *FZF) Quit() {
fzf.program.Quit()
Expand Down
68 changes: 54 additions & 14 deletions model.go
Expand Up @@ -2,6 +2,7 @@ package fzf

import (
"strings"
"time"

"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
Expand All @@ -15,6 +16,7 @@ var (

type model struct {
items *items
itemsLen int
option *option
findOption *findOption

Expand Down Expand Up @@ -84,27 +86,27 @@ func newModel(opt *option) *model {
}
}

func (m *model) setItems(items *items) {
var matches matches
for i := 0; i < items.Len(); i++ {
matches = append(matches, match{
Str: items.String(i),
Index: i,
})
}

func (m *model) loadItems(items *items) {
m.items = items
m.matches = matches
m.itemsLen = items.Len()
m.filter()
}

func (m *model) setFindOption(findOption *findOption) {
m.findOption = findOption
}

func (m *model) Init() tea.Cmd {
return tea.Batch(
cmds := []tea.Cmd{
textinput.Blink,
tea.EnterAltScreen,
}
if m.option.hotReloadLocker != nil {
cmds = append(cmds, m.watchReload())
}

return tea.Batch(
cmds...,
)
}

Expand All @@ -113,6 +115,11 @@ func (m *model) Init() tea.Cmd {
*/

func (m *model) View() string {
if m.option.hotReloadLocker != nil {
m.option.hotReloadLocker.Lock()
defer m.option.hotReloadLocker.Unlock()
}

var v strings.Builder

_, _ = v.WriteString(m.headerView())
Expand Down Expand Up @@ -145,8 +152,12 @@ func (m *model) itemsView() string {

headerHeight := m.headerHeight()

for i, match := range m.matches[m.windowYPosition:] {
cursorLine := m.cursorPosition == i+m.windowYPosition
for i, match := range m.matches {
if i < m.windowYPosition {
continue
}

cursorLine := m.cursorPosition == i

// write cursor
if cursorLine {
Expand Down Expand Up @@ -185,7 +196,7 @@ func (m *model) itemsView() string {
}
}

if i+1 >= m.windowHeight-headerHeight {
if i+1-m.windowYPosition >= m.windowHeight-headerHeight {
break
}
v.WriteString("\n")
Expand All @@ -198,7 +209,15 @@ func (m *model) itemsView() string {
* update
*/

type watchReloadMsg struct{}
type forceReloadMsg struct{}

func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.option.hotReloadLocker != nil {
m.option.hotReloadLocker.Lock()
defer m.option.hotReloadLocker.Unlock()
}

switch msg := msg.(type) {
case tea.KeyMsg:
// key
Expand Down Expand Up @@ -232,6 +251,13 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.input.Width = m.windowWidth - m.promptWidth
m.fixYPosition()
m.fixCursor()
case watchReloadMsg:
// watch reload
return m, m.watchReload()
case forceReloadMsg:
// force reload
m.forceReload()
return m, nil
}

var cmds []tea.Cmd
Expand Down Expand Up @@ -336,3 +362,17 @@ func (m *model) fixYPosition() {
return
}
}

func (m *model) forceReload() {
m.loadItems(m.items)
}

func (m *model) watchReload() tea.Cmd {
return tea.Tick(30*time.Millisecond, func(_ time.Time) tea.Msg {
if m.itemsLen != m.items.Len() {
m.loadItems(m.items)
}

return watchReloadMsg{}
})
}
12 changes: 12 additions & 0 deletions option.go
Expand Up @@ -3,6 +3,7 @@ package fzf
import (
"strconv"
"strings"
"sync"

"github.com/charmbracelet/bubbles/key"
)
Expand Down Expand Up @@ -36,6 +37,8 @@ var defaultOption = option{
_, _ = v.WriteString(strings.Repeat("─", max(windowWidth-v.Len(), 0)))
return v.String()
},

hotReloadLocker: nil,
}

type option struct {
Expand All @@ -53,6 +56,8 @@ type option struct {

countViewEnabled bool
countViewFunc func(itemsCount, matchesCount, windowWidth int) string

hotReloadLocker sync.Locker
}

func (o *option) multiple() bool {
Expand Down Expand Up @@ -152,3 +157,10 @@ func WithCountView(f func(itemsCount, matchesCount, windowWidth int) string) Opt
o.countViewFunc = f
}
}

// WithHotReload sets the locker for read items.
func WithHotReload(locker sync.Locker) Option {
return func(o *option) {
o.hotReloadLocker = locker
}
}