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

Initial loading of Filepicker isn't working #564

Open
justin-9rd opened this issue Jul 21, 2024 · 3 comments
Open

Initial loading of Filepicker isn't working #564

justin-9rd opened this issue Jul 21, 2024 · 3 comments

Comments

@justin-9rd
Copy link

Describe the bug
Working on a tui http client Muninn and I'm using the filepicker bubble to find the .http files on my system. Well when the application initialize I get a "no files found" this isn't the same behaivor I get when working on the example you guys have built. I've rewritten the example using all the same steps I use in my code and it produces list of directory as expected but if I copy that code into my code base it still shows "no files found".

Setup
Please complete the following information along with version numbers, if applicable.

  • OS: Pop-os
  • Shell: Fish
  • Terminal Emulator: Alacritty
  • Terminal Multiplexer: Zellij
  • Locale: en_US.UTF-8

To Reproduce
Steps to reproduce the behavior:

  1. Clone Muninn
  2. Start muninn go run main.go
  3. Notice that it shows the filepicker is empty
  4. Hit h to go back a directory and see that the filepicker is working fine from there

Source Code
Please include source code if needed to reproduce the behavior.
muninn filepicker: /internal/tui/filepicker_view.go

package tui

import (
	"errors"
	"strings"
	"time"

	"github.com/charmbracelet/bubbles/filepicker"
	tea "github.com/charmbracelet/bubbletea"
)

type clearErrorMsg struct{}

func clearErrorAfter(t time.Duration) tea.Cmd {
	return tea.Tick(t, func(_ time.Time) tea.Msg {
		return clearErrorMsg{}
	})
}

type filepickerModel struct {
	picker   filepicker.Model
	selected string
	quitting bool
	err      error
}

func initFilepicker(path string) filepickerModel {
	fp := filepicker.New()
	fp.AllowedTypes = []string{".http"}
	fp.CurrentDirectory = path
	fp.ShowPermissions = false
	fp.ShowSize = false
	fp.AutoHeight = false

	return filepickerModel{
		picker: fp,
	}
}

func (m filepickerModel) Init() tea.Cmd {
	return m.picker.Init()
}

func (m filepickerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		m.picker.Height = msg.Height / 4
	case tea.KeyMsg:
		switch msg.String() {
		case "ctrl+c", "q":
			m.quitting = true
			return m, tea.Quit
		}
	case clearErrorMsg:
		m.err = nil
	}

	var cmd tea.Cmd
	m.picker, cmd = m.picker.Update(msg)

	// Did the user select a file?
	if didSelect, path := m.picker.DidSelectFile(msg); didSelect {
		// Get the path of the selected file.
		m.selected = path
	}

	// Did the user select a disabled file?
	// This is only necessary to display an error to the user.
	if didSelect, path := m.picker.DidSelectDisabledFile(msg); didSelect {
		// Let's clear the selectedFile and display an error.
		m.err = errors.New(path + " is not valid.")
		m.selected = ""
		return m, tea.Batch(cmd, clearErrorAfter(2*time.Second))
	}

	return m, cmd
}

func (m filepickerModel) View() string {
	if m.quitting {
		return ""
	}
	var s strings.Builder
	s.WriteString("\n  ")
	if m.err != nil {
		s.WriteString(m.picker.Styles.DisabledFile.Render(m.err.Error()))
	} else if m.selected == "" {
		s.WriteString("Pick a file:")
	} else {
		s.WriteString("Selected file: " + m.picker.Styles.Selected.Render(m.selected))
	}
	s.WriteString("\n\n" + m.picker.View() + "\n")
	return s.String()
}

Minimal standalone file

package main

import (
	"errors"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/charmbracelet/bubbles/filepicker"
	tea "github.com/charmbracelet/bubbletea"
)

type model struct {
	filepicker   filepicker.Model
	selectedFile string
	quitting     bool
	err          error
}

type clearErrorMsg struct{}

func clearErrorAfter(t time.Duration) tea.Cmd {
	return tea.Tick(t, func(_ time.Time) tea.Msg {
		return clearErrorMsg{}
	})
}

func (m model) Init() tea.Cmd {
	return m.filepicker.Init()
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		m.filepicker.Height = msg.Height / 4
	case tea.KeyMsg:
		switch msg.String() {
		case "ctrl+c", "q":
			m.quitting = true
			return m, tea.Quit
		}
	case clearErrorMsg:
		m.err = nil
	}

	var cmd tea.Cmd
	m.filepicker, cmd = m.filepicker.Update(msg)

	// Did the user select a file?
	if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect {
		// Get the path of the selected file.
		m.selectedFile = path
	}

	// Did the user select a disabled file?
	// This is only necessary to display an error to the user.
	if didSelect, path := m.filepicker.DidSelectDisabledFile(msg); didSelect {
		// Let's clear the selectedFile and display an error.
		m.err = errors.New(path + " is not valid.")
		m.selectedFile = ""
		return m, tea.Batch(cmd, clearErrorAfter(2*time.Second))
	}

	return m, cmd
}

func (m model) View() string {
	if m.quitting {
		return ""
	}
	var s strings.Builder
	s.WriteString("\n  ")
	if m.err != nil {
		s.WriteString(m.filepicker.Styles.DisabledFile.Render(m.err.Error()))
	} else if m.selectedFile == "" {
		s.WriteString("Pick a file:")
	} else {
		s.WriteString("Selected file: " + m.filepicker.Styles.Selected.Render(m.selectedFile))
	}
	s.WriteString("\n\n" + m.filepicker.View() + "\n")
	return s.String()
}

func main() {
	fp := filepicker.New()
	path, _ := os.UserHomeDir()
	fp.AllowedTypes = []string{".http"}
	fp.CurrentDirectory = path
	fp.ShowPermissions = false
	fp.ShowSize = false
	fp.AutoHeight = false

	m := model{
		filepicker: fp,
	}
	tm, _ := tea.NewProgram(&m).Run()
	mm := tm.(model)
	fmt.Println("\n  You selected: " + m.filepicker.Styles.Selected.Render(mm.selectedFile) + "\n")
}

Expected behavior
When my application starts I expect the files and directories to appear just like the minimal example

Screenshots
What my application renders
Screenshot from 2024-07-21 11-27-56

What I expect it to render in the first box
Screenshot from 2024-07-21 11-29-03

@justin-9rd
Copy link
Author

After some debugging and having to pull the filepicker into my own project I found that I can resolve this issue in my code base by up dated the WindowSize msg check

  case tea.WindowSizeMsg:
    if m.AutoHeight {
	    m.Height = msg.Height - marginBottom
    }
    m.max = m.Height - 1
    if len(m.files) == 0 {
	    return m, m.readDir(m.CurrentDirectory, m.ShowHidden)
    }

This does not feel like the right solution.

Overall seems that the readDirMsg isn't getting picked up in time as my WindowSizeMsg is begin the first thing I pick up in my logs even though I can see the Init function firing in my logs. Not sure what would be the best fix without understanding how the underlining system is processing the messages

@xoxloviwan
Copy link

xoxloviwan commented Jan 7, 2025

I have similar behavior, but render depends on number of page in my app. If I set picker page as first view I see:

Select file:
  c:\projects\test

> drwxrwxrwx     0B .git
  drwxrwxrwx     0B certs
  drwxrwxrwx     0B cmd
  drwxrwxrwx     0B db
  drwxrwxrwx     0B internal
  -rw-rw-rw-    29B .gitignore
  -rw-rw-rw-  1.6MB 359432a5c48211efb6a46aa10c14c35a_1.jpg
  -rw-rw-rw-  1.9kB Taskfile.yml
  -rw-rw-rw-  1.4kB go.mod
  -rw-rw-rw-  5.6kB go.sum
  -rw-rw-rw-  9.7MB goph.exe
  -rw-rw-rw-   14MB server.exe

renders: 1 updates: 5 []updates:[{} {188 50} {} {188 50} {1 [{0xc000322000} {0xc000322150} {0xc0003221c0} {0xc000322230} {0xc0003223f0} {0xc000322070} {0xc0003220e0} {0xc0003224d0} {0xc000

but if I go to picker page from business logic flow (from main app menu page) I got this:

Select file:
  c:\projects\test

  Bummer. No Files Found.


renders: 1 updates: 0 []updates:[]

When I start any actions Update() function is called and picker outputs file list...

I had to make a hack to render any file list in my app like this:

type modelPicker struct {
	filepicker     filepicker.Model
	selectedFile   string
	quitting       bool
	err            error
	nextPage       func()
	nonFirstRender bool
}

func (m modelPicker) Init() tea.Cmd {
	return nil
}

func (m modelPicker) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	if !m.nonFirstRender {
		m.nonFirstRender = true
		cmd := m.filepicker.Init()
		return m, cmd
	}
...
}

Maybe in your situation, as in mine, initial updates are not enough while pages changing...

@xoxloviwan
Copy link

xoxloviwan commented Jan 12, 2025

Resolve this problem with using interface:

type Sender interface {
	Send(tea.Msg)
}

type App struct {
	pages []Page
	Sender
}

And when changing page (view) call goroutine go app.Sender.Send(new(tea.Msg)).
That will call Update() method forcely.

func main() {
	m, err := views.NewApp()
	fatal(err)
	p := tea.NewProgram(m, func(pp *tea.Program) {
		m.Sender = pp
	})
	_, err = p.Run()
	fatal(err)
}

Model looks like this:

type modelPicker struct {
	filepicker   filepicker.Model
	selectedFile string
	quitting     bool
	err          error
	nextPage     func()
	is1stUpdate  bool
}

func newModelPicker(nextPage func()) modelPicker {
	fp := filepicker.New()
	return modelPicker{
		filepicker:  fp,
		nextPage:    nextPage,
		is1stUpdate: true,
	}
}

func NewPage(nextPage func()) *PickerPage {
	return &PickerPage{newModelPicker(nextPage), 0, 0}
}

func (m modelPicker) Init() tea.Cmd {
	return m.filepicker.Init()
}

func (m modelPicker) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	if m.is1stUpdate {
		m.is1stUpdate = false
		return m, m.Init()
	}
...
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants