From ffe7fa0e255916b68bac8c11194ac2587591f5f9 Mon Sep 17 00:00:00 2001 From: koki-develop Date: Sun, 26 Mar 2023 20:15:46 +0900 Subject: [PATCH 1/7] add WithHotReload Option --- option.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/option.go b/option.go index 5786041..ce643b1 100644 --- a/option.go +++ b/option.go @@ -3,6 +3,7 @@ package fzf import ( "strconv" "strings" + "sync" "github.com/charmbracelet/bubbles/key" ) @@ -36,6 +37,8 @@ var defaultOption = option{ _, _ = v.WriteString(strings.Repeat("─", max(windowWidth-v.Len(), 0))) return v.String() }, + + hotReloadLocker: nil, } type option struct { @@ -53,6 +56,8 @@ type option struct { countViewEnabled bool countViewFunc func(itemsCount, matchesCount, windowWidth int) string + + hotReloadLocker sync.Locker } func (o *option) multiple() bool { @@ -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 + } +} From cde5b2d4a5bef430871e266f74f1e710490f2cb1 Mon Sep 17 00:00:00 2001 From: koki-develop Date: Sun, 26 Mar 2023 20:17:04 +0900 Subject: [PATCH 2/7] support hot reload --- fzf.go | 15 ++++++++------- model.go | 29 ++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/fzf.go b/fzf.go index add6c4d..b1a99c5 100644 --- a/fzf.go +++ b/fzf.go @@ -13,7 +13,6 @@ var defaultFindOption = findOption{ // Fuzzy Finder. type FZF struct { - option *option model *model program *tea.Program } @@ -28,7 +27,6 @@ func New(opts ...Option) *FZF { m := newModel(&o) return &FZF{ - option: &o, model: m, program: tea.NewProgram(m), } @@ -42,11 +40,14 @@ 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) diff --git a/model.go b/model.go index 43d50e7..6dbc036 100644 --- a/model.go +++ b/model.go @@ -2,6 +2,7 @@ package fzf import ( "strings" + "time" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/textinput" @@ -15,6 +16,7 @@ var ( type model struct { items *items + itemsLen int option *option findOption *findOption @@ -94,6 +96,7 @@ func (m *model) setItems(items *items) { } m.items = items + m.itemsLen = items.Len() m.matches = matches } @@ -102,9 +105,16 @@ func (m *model) setFindOption(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..., ) } @@ -198,6 +208,8 @@ func (m *model) itemsView() string { * update */ +type watchReloadMsg struct{} + func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: @@ -232,6 +244,8 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.input.Width = m.windowWidth - m.promptWidth m.fixYPosition() m.fixCursor() + case watchReloadMsg: + return m, m.watchReload() } var cmds []tea.Cmd @@ -336,3 +350,16 @@ func (m *model) fixYPosition() { return } } + +func (m *model) watchReload() tea.Cmd { + return tea.Tick(30*time.Millisecond, func(_ time.Time) tea.Msg { + m.option.hotReloadLocker.Lock() + defer m.option.hotReloadLocker.Unlock() + + if m.itemsLen != m.items.Len() { + m.setItems(m.items) + } + + return watchReloadMsg{} + }) +} From 44ddae04a5c2bdd6c02ed9ad0db43ac9632c6fe4 Mon Sep 17 00:00:00 2001 From: koki-develop Date: Sun, 26 Mar 2023 20:26:52 +0900 Subject: [PATCH 3/7] implement fzf.ForceReload --- fzf.go | 12 ++++++++++++ model.go | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/fzf.go b/fzf.go index b1a99c5..d2baa03 100644 --- a/fzf.go +++ b/fzf.go @@ -1,6 +1,7 @@ package fzf import ( + "errors" "fmt" "reflect" @@ -68,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() diff --git a/model.go b/model.go index 6dbc036..c1ef724 100644 --- a/model.go +++ b/model.go @@ -209,6 +209,7 @@ func (m *model) itemsView() string { */ type watchReloadMsg struct{} +type forceReloadMsg struct{} func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { @@ -245,7 +246,12 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 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 @@ -351,6 +357,12 @@ func (m *model) fixYPosition() { } } +func (m *model) forceReload() { + m.option.hotReloadLocker.Lock() + defer m.option.hotReloadLocker.Unlock() + m.setItems(m.items) +} + func (m *model) watchReload() tea.Cmd { return tea.Tick(30*time.Millisecond, func(_ time.Time) tea.Msg { m.option.hotReloadLocker.Lock() From cc1ed161264c8259d654ed803e26d2ca02c97046 Mon Sep 17 00:00:00 2001 From: koki-develop Date: Sun, 26 Mar 2023 20:27:18 +0900 Subject: [PATCH 4/7] setItems -> loadItems --- fzf.go | 2 +- model.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fzf.go b/fzf.go index d2baa03..03386db 100644 --- a/fzf.go +++ b/fzf.go @@ -55,7 +55,7 @@ func (fzf *FZF) Find(items interface{}, itemFunc func(i int) string, opts ...Fin 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 { diff --git a/model.go b/model.go index c1ef724..f3d4129 100644 --- a/model.go +++ b/model.go @@ -86,7 +86,7 @@ func newModel(opt *option) *model { } } -func (m *model) setItems(items *items) { +func (m *model) loadItems(items *items) { var matches matches for i := 0; i < items.Len(); i++ { matches = append(matches, match{ @@ -360,7 +360,7 @@ func (m *model) fixYPosition() { func (m *model) forceReload() { m.option.hotReloadLocker.Lock() defer m.option.hotReloadLocker.Unlock() - m.setItems(m.items) + m.loadItems(m.items) } func (m *model) watchReload() tea.Cmd { @@ -369,7 +369,7 @@ func (m *model) watchReload() tea.Cmd { defer m.option.hotReloadLocker.Unlock() if m.itemsLen != m.items.Len() { - m.setItems(m.items) + m.loadItems(m.items) } return watchReloadMsg{} From 8ef6e2682c7f65e66bb7161f0c9faec1580dd070 Mon Sep 17 00:00:00 2001 From: koki-develop Date: Sun, 26 Mar 2023 20:29:55 +0900 Subject: [PATCH 5/7] use filter --- model.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/model.go b/model.go index f3d4129..fd99ff8 100644 --- a/model.go +++ b/model.go @@ -87,17 +87,9 @@ func newModel(opt *option) *model { } func (m *model) loadItems(items *items) { - var matches matches - for i := 0; i < items.Len(); i++ { - matches = append(matches, match{ - Str: items.String(i), - Index: i, - }) - } - m.items = items m.itemsLen = items.Len() - m.matches = matches + m.filter() } func (m *model) setFindOption(findOption *findOption) { From d00ccf993c9f054697c077f2168c5a137dda0256 Mon Sep 17 00:00:00 2001 From: koki-develop Date: Sun, 26 Mar 2023 20:39:38 +0900 Subject: [PATCH 6/7] lock locker --- model.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/model.go b/model.go index fd99ff8..7a82aa8 100644 --- a/model.go +++ b/model.go @@ -115,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()) @@ -254,6 +259,10 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, cmd) } if beforeValue != m.input.Value() { + if m.option.hotReloadLocker != nil { + m.option.hotReloadLocker.Lock() + defer m.option.hotReloadLocker.Unlock() + } m.filter() m.fixYPosition() m.fixCursor() From 76ee2cf1cc1db08191b67e74dccc0f88dcb54abd Mon Sep 17 00:00:00 2001 From: koki-develop Date: Sun, 26 Mar 2023 21:30:08 +0900 Subject: [PATCH 7/7] fix bug --- model.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/model.go b/model.go index 7a82aa8..9ae1946 100644 --- a/model.go +++ b/model.go @@ -152,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 { @@ -192,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") @@ -209,6 +213,11 @@ 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 @@ -259,10 +268,6 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, cmd) } if beforeValue != m.input.Value() { - if m.option.hotReloadLocker != nil { - m.option.hotReloadLocker.Lock() - defer m.option.hotReloadLocker.Unlock() - } m.filter() m.fixYPosition() m.fixCursor() @@ -359,16 +364,11 @@ func (m *model) fixYPosition() { } func (m *model) forceReload() { - m.option.hotReloadLocker.Lock() - defer m.option.hotReloadLocker.Unlock() m.loadItems(m.items) } func (m *model) watchReload() tea.Cmd { return tea.Tick(30*time.Millisecond, func(_ time.Time) tea.Msg { - m.option.hotReloadLocker.Lock() - defer m.option.hotReloadLocker.Unlock() - if m.itemsLen != m.items.Len() { m.loadItems(m.items) }