GoSetup-bubble/setup/fields/dirSelect.go

243 lines
7.6 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"
"fmt"
"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 msg.String() {
case "esc":
//Закрываем список
m.open = false
case "up":
//Уменьшаем фокус, поднимаем его
m.focus--
if m.focus < 0 {
m.focus = len(m.options) - 1
}
case "down":
//Увеличиваем фокус, опуская его
m.focus++
if m.focus == len(m.options) {
m.focus = 0
}
case "enter":
//Выбираем директорию из списка. Если список пуст ничего не делаем.
//Получаем имя файла.
//Если имя файла ".." возвращаемся в родительскую папку.
//Если файл является директорией, то записываем его и закрываем список.
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 {
//Если список не открыт и был нажат enter, открываем его и рисуем опции списка.
//Иначе просто передаем сообщения нажатия клавиш в input.
switch msg.String() {
case "enter":
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.Batch(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
}
}