package fields import ( "bubble/setup/ui" "bubble/setup/utils" "bubble/types" "fmt" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/sirupsen/logrus" "os" "path/filepath" "strings" ) // DirSelectModel структура описывающая модель поля выбора директории type DirSelectModel struct { input textinput.Model //Готовый компонент input для ввода данных label string //Имя поля description string //Описание поля focus int //Фокус для управления опциями open bool //Открыт ли список dir string //Имя выбранной директории currentDirectory string //Директория текущего файла options []os.FileInfo //Элементы выбора файлов optionsCount int //Количество элементов к показу } // NewDirSelectModel создает новую модель поля выбора директории func NewDirSelectModel(label, dir, description string, validator textinput.ValidateFunc) *DirSelectModel { inputModel := textinput.New() inputModel.Validate = validator inputModel.SetValue(dir) return &DirSelectModel{ input: inputModel, label: label, description: description, currentDirectory: filepath.Dir(dir), open: false, dir: dir, optionsCount: 10, } } func (m *DirSelectModel) Init() tea.Cmd { return textinput.Blink } // Update отслеживает действия с полями func (m *DirSelectModel) Update(msg tea.Msg) (FieldModel, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: //Отслеживает нажатия клавиш. //Если список открыт, то отслеживаем клавиши отдельно if m.open { switch { case key.Matches(msg, types.MainKeys.Close): //Обработчик клавиши закрытия списка опций m.open = false case key.Matches(msg, types.MainKeys.Up): //Обработчик клавиши перехода на прошлую опцию m.focus-- if m.focus < 0 { m.focus = len(m.options) - 1 } case key.Matches(msg, types.MainKeys.Down): //Обработчик клавиши перехода на следующую опцию m.focus++ if m.focus == len(m.options) { m.focus = 0 } case key.Matches(msg, types.MainKeys.Confirm): //Обработчик клавиши выбора опции. Если список пуст, ничего не делаем. //Получаем имя файла. //Если имя файла ".." возвращаемся в родительскую папку. //Если файл является директорией, то записываем его и закрываем список. if len(m.options) == 0 || m.focus >= len(m.options) { return m, nil } file := m.options[m.focus] fileName := file.Name() if fileName == ".." { m.currentDirectory = filepath.Dir(m.currentDirectory) m.focus = 0 m.RenderOptions() newValue := m.currentDirectory + string(filepath.Separator) m.input.SetValue(newValue) m.dir = newValue m.input.CursorEnd() return m, nil } if file.IsDir() { m.currentDirectory = filepath.Join(m.currentDirectory, fileName) newValue := m.currentDirectory + string(filepath.Separator) m.input.SetValue(newValue) m.dir = newValue m.input.CursorEnd() m.open = false m.focus = 0 return m, nil } } } else { //Если список не открыт и была нажата клавиша подтверждения, открываем его и рисуем опции списка. //Иначе просто передаем сообщение нажатия клавиш в input. switch { case key.Matches(msg, types.MainKeys.Confirm): m.open = true newDir := strings.TrimSpace(m.input.Value()) if len(newDir) > 1 && strings.HasSuffix(newDir, string(filepath.Separator)) { newDir = strings.TrimSuffix(newDir, string(filepath.Separator)) } if info, err := os.Stat(newDir); err == nil && info.IsDir() { m.currentDirectory = newDir m.dir = newDir + string(filepath.Separator) m.input.SetValue(m.dir) m.input.CursorEnd() } else { m.currentDirectory = filepath.Dir(newDir) } m.RenderOptions() return m, nil default: var cmd tea.Cmd m.input, cmd = m.input.Update(msg) cmds = append(cmds, cmd) if m.input.Value() != m.dir { m.dir = m.input.Value() } } } default: //Все прочие виды сообщений передаем в input если список закрыт if !m.open { var cmd tea.Cmd m.input, cmd = m.input.Update(msg) cmds = append(cmds, cmd) } } return m, tea.Sequence(cmds...) } func (m *DirSelectModel) View() string { res := fmt.Sprintf("%s: %s", m.label, m.input.View()) if m.input.Err != nil && !m.open { res += fmt.Sprintf("\nОшибка валидации: %s", m.input.Err) } else if m.description != "" { res += fmt.Sprintf("\n%s", m.description) } if m.open && len(m.options) > 0 { start, end := utils.CalculateSelectOptionsIndex(m.focus, m.optionsCount, len(m.options)) for i := start; i < end; i++ { file := m.options[i] name := file.Name() prefix := "\n\t" if m.focus == i { prefix += "-> " } else { prefix += " " } if name == ".." { res += prefix + ui.SelectFileDirParentStyle.Render(name) } else if file.IsDir() { res += prefix + ui.SelectDirStyle.Render(name) } else { res += prefix + ui.SelectFileDirDeniedStyle.Render(name) } } } return res } func (m *DirSelectModel) Focus() { m.input.Focus() } func (m *DirSelectModel) Blur() { m.input.Blur() } func (m *DirSelectModel) CaptureKeys() bool { return m.open } func (m *DirSelectModel) GetLabel() string { return m.label } func (m *DirSelectModel) GetValue() interface{} { return m.dir } // RenderOptions заполняем список опций на основе директорий func (m *DirSelectModel) RenderOptions() { //Читаем файлы к текущей директории filesInDir, err := os.ReadDir(m.currentDirectory) if err != nil { logrus.Errorf("Ошибка чтения директории: %v", err) return } m.options = nil //Если это не корень файловой системы, добавляем переход на родительскую директорию var allOptions []os.FileInfo if m.currentDirectory != "/" && filepath.Clean(m.currentDirectory) != "/" { allOptions = append(allOptions, virtualDirInfo{name: ".."}) } //Заполняем прочие опции for _, entry := range filesInDir { info, _ := entry.Info() allOptions = append(allOptions, info) } //Заполняем фильтр для опций на основе значения поля ввода filter := "" if m.open { inputValue := strings.TrimSpace(m.input.Value()) if inputValue != "" { lastPart := inputValue[strings.LastIndex(inputValue, "/")+1:] if lastPart != "" { filter = strings.ToLower(lastPart) } } } //Чистим опции и заполняем их правильными значениями m.options = m.options[:0] for _, opt := range allOptions { name := opt.Name() if filter == "" || strings.HasPrefix(strings.ToLower(name), filter) || name == ".." { m.options = append(m.options, opt) } } if len(m.options) == 0 { m.focus = 0 return } if m.focus >= len(m.options) { m.focus = len(m.options) - 1 } if m.focus < 0 { m.focus = 0 } }