GoSetup-bubble/setup/fields/dirSelect.go

245 lines
7.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
}