charmbracelet / bubbles

TUI components for Bubble Tea 🫧
MIT License
5.49k stars 265 forks source link

Filepicker is not visible #432

Closed llogen closed 10 months ago

llogen commented 11 months ago

Describe the bug I am working on a tui with multiple nested models and I wanted to use a filepicker to select files from the fs. I was very happy that bubbles already got such object, but the problem is, that is does not work as expected. When I call into my nested model that contains the filepicker, the filepicker does not show up, although I can navigate within it (I know this due to tracking the current path, which changes while using the arrow keys). The thing, what makes me suspicious and let me think that this behavior is a bug is, that when I call into that nested model from the beginning of the tea program (so the nested Model is the initial Model), than the filepicker works perfectly fine and everything is displayed properly.

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

To Reproduce Steps to reproduce the behavior:

  1. Start tui and navigate into the nested model and than focus the filepicker. It won't work.
  2. Change code, so that the nested model is the initial model, focus the filepicker. I will work.

Source Code This is the code for my program start:


type TUICmd struct{}

func (cmd *TUICmd) Run() error { if _, err := tea.NewProgram( tui.MasterModel{ CurrentModel: tui.NewStartModel(constants.StartListLogin), }, tea.WithAltScreen(), tea.WithMouseCellMotion()).Run(); err != nil { return err } return nil }


This is the code, where I manage the models:


type MasterModel struct { CurrentModel tea.Model }

func (m MasterModel) Init() tea.Cmd { return m.CurrentModel.Init() }

func (m MasterModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd

switch msg := msg.(type) {
case messages.StartListMsg:
    m.CurrentModel = NewStartModel(msg.Index)

case messages.TestAddMsg:
    m.CurrentModel = test.NewAddTestModel(msg.Index)

    cmd = m.Init()

default:
    m.CurrentModel, cmd = m.CurrentModel.Update(msg)
}

return m, cmd

}

func (m MasterModel) View() string { return m.CurrentModel.View() }


And this is the code that uses the filepicker:


type AddModel struct { inputs []textinput.Model filePicker filepicker.Model selectedFile string cursor int originalIndex int err error } func NewAddTestModel(originalIndex int) *AddModel { inputs := make([]textinput.Model, addItems)

inputs[name] = textinput.New()
inputs[name].Placeholder = "Name"
inputs[name].Width = 32
inputs[name].CharLimit = 32
inputs[name].Focus()

inputs[description] = textinput.New()
inputs[description].Placeholder = "Description"
inputs[description].Width = 64
inputs[description].CharLimit = 64

filePicker := filepicker.New()
filePicker.AllowedTypes = []string{".mod", ".sum", ".go", ".txt", ".md"}
filePicker.CurrentDirectory, _ = os.UserHomeDir()

return &AddModel{
    originalIndex:   originalIndex,
    inputs:          inputs,
    filePicker:      filePicker,
    cursor:          0,
    err:             nil,
}

}

func (am AddModel) Init() tea.Cmd { return am.filePicker.Init() }

func (am AddModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd

cmds := make([]tea.Cmd, len(am.inputs))

switch msg := msg.(type) {
case tea.WindowSizeMsg:
    x, y := styles.List.GetFrameSize()

    columnWidth := (msg.Width - x)
    columnHeight := (msg.Height - y)
    styles.List.Width(columnWidth).Height(columnHeight)
    styles.Help.Width(msg.Width)

    am.filePicker, cmd = am.filePicker.Update(msg)

case tea.KeyMsg:
    if am.cursor == len(am.inputs) {
        switch msg.Type {
        case tea.KeyCtrlC:
            return am, tea.Quit

        case tea.KeyCtrlQ:
            return NewListModel(am.originalIndex), nil

        case tea.KeyTab:
            am.nextInput()

        case tea.KeyShiftTab:
            am.prevInput()
        }

        am.filePicker, cmd = am.filePicker.Update(msg)
        cmds = append(cmds, cmd)

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

        // Did the user select a disabled file?
        // This is only necessary to display an error to the user.
        if didSelect, path := am.filePicker.DidSelectDisabledFile(msg); didSelect {
            // Let's clear the selectedFile and display an error.
            am.err = errors.New(path + " is not valid.")
            am.selectedFile = ""

            cmd := clearErrorAfter(2 * time.Second)

            cmds = append(cmds, cmd)

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

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

    switch msg.Type {
    case tea.KeyEnter:
        am.nextInput()

    case tea.KeyCtrlC:
        return am, tea.Quit

    case tea.KeyTab:
        am.nextInput()

    case tea.KeyShiftTab:
        am.prevInput()

    case tea.KeyCtrlS:
        hardwareTypeIDs := strings.Split(am.inputs[hardwaretypes].Value(), ",")
        toolIDs := strings.Split(am.inputs[tools].Value(), ",")

        cmd = addTest(testAddMsg{
            Name:            am.inputs[name].Value(),
            Description:     am.inputs[description].Value(),
            HardwareTypeIDs: hardwareTypeIDs,
            ToolIDs:         toolIDs,
            //TestFile:        am.inputs[testfile].Value(),
        })
        cmds = []tea.Cmd{cmd}

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

    case tea.KeyCtrlQ:
        return NewListModel(am.originalIndex), nil
    }

// We handle errors just like any other message
case clearErrorMsg:
    am.err = nil

case msgError:
    am.err = msg.err

    return am, nil

case debug:
    am.err = msg.msg
    cmd := clearErrorAfter(2 * time.Second)

    return am, cmd

case msgSuccess:
    return NewListModel(am.originalIndex), nil

default:
    am.filePicker, cmd = am.filePicker.Update(msg)
}

for i := range am.inputs {
    am.inputs[i], cmds[i] = am.inputs[i].Update(msg)
}

cmds = append(cmds, cmd)

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

}

func (am AddModel) View() string { var ( output string errorMsg string )

if am.err != nil {
    errorMsg = styles.Error.Render("Error: ", am.err.Error(), "\n")
}

var filePickerString strings.Builder

if am.err != nil {
    filePickerString.WriteString(am.filePicker.Styles.DisabledFile.Render(am.err.Error()))
} else if am.selectedFile == "" {
    filePickerString.WriteString("Pick a file:")
} else {
    filePickerString.WriteString("Selected file: " + am.filePicker.Styles.Selected.Render(am.selectedFile))
}

filePickerString.WriteString("\n\n" + am.filePicker.View() + "\n")

switch {
case am.cursor == len(am.inputs):
    output += styles.Login.Render(
        lipgloss.JoinVertical(
            lipgloss.Left,
            am.headerView(),
            styles.Login.Render(
                styles.Input.Render("Name:"),
                am.inputs[name].View(),
            ),
            styles.Login.Render(
                styles.Input.Render("Description:"),
                am.inputs[description].View(),
            ),
            styles.Login.Render(
                styles.Input.Render("Filepicker:"),
                filePickerString.String(),
            ),
        ),
    )
default:
    output += styles.Login.Render(
        lipgloss.JoinVertical(
            lipgloss.Left,
            am.headerView(),
            styles.Login.Render(
                styles.Input.Render("Name:"),
                am.inputs[name].View(),
            ),
            styles.Login.Render(
                styles.Input.Render("Description:"),
                am.inputs[description].View(),
            ),
            styles.Login.Render(
                styles.Input.Render("Filepicker:"),
            ),
        ),
    )
}

output += lipgloss.JoinVertical(
    lipgloss.Left,
    styles.Error.Render(errorMsg),
    styles.Help.Render(
        "ctrl + s: add\nctrl + r: reset\nctrl + q: abort\nctrl + c: quit",
    ),
)

return output

}


Expected behavior The expected behavior is that the filepicker should also work if it is used in a nested model.

llogen commented 10 months ago

If the Filepicker is called from another model and is not used as the initial model that the heigth of the filepicker is 0. Setting the heigth fixed it.