GoSetup-bubble/setup/fields/fileSelect.go
2025-12-26 17:45:37 +05:00

257 lines
7 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/types"
"fmt"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/sirupsen/logrus"
"os"
"path/filepath"
"slices"
"strings"
"time"
)
type FileSelectModel struct {
input textinput.Model
label string
focus int
open bool
file string
currentDirectory string
options []os.FileInfo
extensions []string
optionsCount int
}
func NewFileSelectModel(label, file string, extensions []string) *FileSelectModel {
inputModel := textinput.NewModel()
inputModel.SetValue(file)
return &FileSelectModel{
input: inputModel,
label: label,
extensions: extensions,
currentDirectory: filepath.Dir(file),
open: false,
file: file,
optionsCount: 3,
}
}
func (m *FileSelectModel) Init() tea.Cmd {
return textinput.Blink
}
func (m *FileSelectModel) 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.file = newValue
m.input.CursorEnd()
return m, nil
}
if file.IsDir() {
m.currentDirectory = filepath.Join(m.currentDirectory, fileName)
m.focus = 0
newValue := m.currentDirectory + string(filepath.Separator)
m.input.SetValue(newValue)
m.file = newValue
m.input.CursorEnd()
m.RenderOptions()
} else if slices.Contains(m.extensions, filepath.Ext(fileName)) {
fullPath := filepath.Join(m.currentDirectory, fileName)
m.file = fullPath
m.input.SetValue(fullPath)
m.open = false
selected := types.SelectedFileMsg{
Filename: fileName,
Index: m.focus,
}
m.input.CursorEnd()
m.focus = 0
return m, func() tea.Msg {
return selected
}
}
}
} else {
switch msg.String() {
case "enter":
m.open = true
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.file {
m.file = m.input.Value()
m.currentDirectory = filepath.Dir(m.file)
}
}
}
default:
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 *FileSelectModel) View() string {
res := fmt.Sprintf("%s: %s", m.label, m.input.View())
if m.open && len(m.options) > 0 {
visibleCount := m.optionsCount //количество отображаемых
if visibleCount > len(m.options) { //если количество отображаемых больше, чем есть опций на самом деле
visibleCount = len(m.options) //то меняем количество видимых на количество опций
}
half := visibleCount / 2 //количество объектов сверху и снизу
start := m.focus - half //стартовая позиция
if start < 0 { //если стартовая позиция меньше 0, то меняем на 0
start = 0
}
end := start + visibleCount //последний элемент
if end > len(m.options) { //если последний элемент вне опций
end = len(m.options) //меняем на количество опций
start = end - visibleCount //перерасчет стартовой позиции
if start < 0 {
start = 0
}
}
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 if !slices.Contains(m.extensions, filepath.Ext(name)) {
res += prefix + ui.SelectFileDirDeniedStyle.Render(name)
} else {
res += prefix + ui.SelectFileStyle.Render(name)
}
}
}
return res
}
func (m *FileSelectModel) Focus() {
m.input.Focus()
}
func (m *FileSelectModel) Blur() {
m.input.Blur()
}
func (m *FileSelectModel) CaptureKeys() bool {
return m.open
}
func (m *FileSelectModel) GetLabel() string {
return m.label
}
func (m *FileSelectModel) GetValue() interface{} {
return m.file
}
func (m *FileSelectModel) 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: ".."})
//m.options = append(m.options, virtualDirInfo{name: ".."})
}
for _, entry := range filesInDir {
info, _ := entry.Info()
allOptions = append(allOptions, info)
//m.options = append(m.options, 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)
}
}
//если у нас нет опций, то и фокус нельзя куда-либо поставить, то есть надо быть осторожным,
//но после этой функции у нас задействуется view, где есть условие, отрисовка только если опций больше 0,
//так что у нас стоит защита на использование m.options[m.focus] при фокусе равном 0 и отсутствии опций.
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
}
}
type virtualDirInfo struct {
name string
}
func (v virtualDirInfo) Name() string { return v.name }
func (v virtualDirInfo) Size() int64 {
return 0
}
func (v virtualDirInfo) Mode() os.FileMode { return os.ModeDir }
func (v virtualDirInfo) ModTime() time.Time { return time.Time{} }
func (v virtualDirInfo) IsDir() bool { return true }
func (v virtualDirInfo) Sys() interface{} { return nil }