// package desktop -- консольный клиент для вар-танка package desktop import ( "fmt" "os" "time" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" ) // Desktop -- консольный клиент для вар-танка type Desktop struct { TeaModel p *tea.Program // Объект приложения } func NewDesktop() *Desktop { sf := &Desktop{} sf.TeaModel = NewModel() go sf.tick() sf.p = tea.NewProgram(sf, tea.WithAltScreen(), tea.WithANSICompressor()) if _, err := sf.p.Run(); err != nil { fmt.Printf("Опа! здесь какая-то ошибочка: %v", err) os.Exit(1) } return sf } // AMode -- режим обработки клавиш type AMode int const ( ModeDefault AMode = iota ModeConfig // Режим конфигурирования ModeConfigServer // Режим конфигуррования сервера ModeInputHostServer // Режим ввода хоста сервера ModeInputLoginServer // Режим ввода логина на сервер ModeInputPassServer // Режим ввода пароля входа на сервер ModeConnectServer // Режим подключения к серверу ) type TeaModel struct { choices []string // выбранный пункт в списке cursor int // позиция курсора в списке selected map[int]struct{} // что выбрано из списка mode AMode // Режим показа на экране textInput textinput.Model host string // Имя хоста сервера (с портом) login string // Логин для сервера pass string // Пароль для сервера } func NewModel() TeaModel { ti := textinput.New() ti.Placeholder = "localhost" ti.Focus() ti.CharLimit = 156 ti.Width = 20 m := TeaModel{ // Список дел choices: []string{"Купить картошку", "Оплатить мобилку", "Заплатить за кредит"}, selected: make(map[int]struct{}), textInput: ti, } return m } func (sf *Desktop) tick() { for { time.Sleep(time.Second * 1) msg := tea.MouseEvent{} // m.Update(msg) sf.p.Send(msg) } } // Run -- запускает в работу консольный клиент func (sf *Desktop) Run() error { return nil } func (sf Desktop) Init() tea.Cmd { // Просто вернуть `nil`, т.е. никаких команд return textinput.Blink } func (sf Desktop) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { // Может клавиша нажата case tea.KeyMsg: switch msg.String() { // Да, нажато, а что именно? case "ctrl+q": // Надо выйти из программы return sf, tea.Quit } } switch sf.mode { case ModeDefault: switch msg := msg.(type) { // Может клавиша нажата case tea.KeyMsg: switch msg.String() { // Да, нажато, а что именно? case "up", "k": // Если "up" или "k" перемещение курсора вверх if sf.cursor > 0 { sf.cursor-- } case "down", "j": // Если "down" или "j" перемещение курсора вниз if sf.cursor < len(sf.choices)-1 { sf.cursor++ } case "enter", " ": // Нажатие "enter" или "пробел" переключает выбранный элемент списка _, ok := sf.selected[sf.cursor] if ok { delete(sf.selected, sf.cursor) } else { sf.selected[sf.cursor] = struct{}{} } case "w": // Настройки графического клиента sf.mode = ModeConfig } } case ModeConfig: switch msg := msg.(type) { // Может клавиша нажата case tea.KeyMsg: switch msg.String() { // Да, нажато, а что именно? case "q": // Вернуться на шаг назад sf.mode = ModeDefault case "1": // Выбор один sf.mode = ModeConfigServer case "2": // Вход на сервер sf.mode = ModeConnectServer } } case ModeConfigServer: switch msg := msg.(type) { // Может клавиша нажата case tea.KeyMsg: switch msg.String() { // Да, нажато, а что именно? case "q": // Вернуться на шаг назад sf.mode = ModeConfig case "1": // Выбор хоста сервера sf.mode = ModeInputHostServer sf.textInput.Reset() sf.textInput.Placeholder = "localhost:18050" case "2": // Выбор логина для входа sf.mode = ModeInputLoginServer sf.textInput.Reset() sf.textInput.Placeholder = "login" case "3": // Выбор пароля для входа sf.mode = ModeInputPassServer sf.textInput.Reset() sf.textInput.Placeholder = "pass" } } case ModeInputHostServer: switch msg := msg.(type) { // Может клавиша нажата case tea.KeyMsg: switch msg.String() { // Да, нажато, а что именно? case "q": // Вернуться на шаг назад sf.mode = ModeConfigServer case "enter": // Нажатие "enter" или "пробел" переключает выбранный элемент списка sf.host = sf.textInput.Value() sf.mode = ModeConfigServer default: sf.textInput, cmd = sf.textInput.Update(msg) } } case ModeInputLoginServer: switch msg := msg.(type) { // Может клавиша нажата case tea.KeyMsg: switch msg.String() { // Да, нажато, а что именно? case "q": // Вернуться на шаг назад sf.mode = ModeConfigServer case "enter": // Нажатие "enter" или "пробел" переключает выбранный элемент списка sf.login = sf.textInput.Value() sf.mode = ModeConfigServer default: sf.textInput, cmd = sf.textInput.Update(msg) } } case ModeInputPassServer: switch msg := msg.(type) { // Может клавиша нажата case tea.KeyMsg: switch msg.String() { // Да, нажато, а что именно? case "q": // Вернуться на шаг назад sf.mode = ModeConfigServer case "enter": // Нажатие "enter" или "пробел" переключает выбранный элемент списка sf.pass = sf.textInput.Value() sf.mode = ModeConfigServer default: sf.textInput, cmd = sf.textInput.Update(msg) } } case ModeConnectServer: switch msg := msg.(type) { // Может клавиша нажата case tea.KeyMsg: switch msg.String() { // Да, нажато, а что именно? case "q": // Вернуться на шаг назад sf.mode = ModeConfigServer case "esc": // Нажатие "esc" или "пробел" прекращает подключение к серверу sf.pass = sf.textInput.Value() sf.mode = ModeConfigServer default: sf.textInput, cmd = sf.textInput.Update(msg) } } } // Возвращает обновлённую модель дляe Bubble Tea runtime для обработки. //Здесь команду возвращать надо. return sf, cmd } func (sf Desktop) View() string { s := time.Now().Local().Format("[ WarTank ] = 2006-01-02 15:04:05.000") s += fmt.Sprintf("\tХост=%q\tЛогин=%q\tПароль=%q\n\n", sf.host, sf.login, sf.pass) switch sf.mode { case ModeDefault: // Исходное содержимое s += sf.winDefault() case ModeConfig: // Показать настройки клиента s += sf.winConfig() case ModeConfigServer: // Конфигурация сервера s += sf.winConfigServer() case ModeInputHostServer: // Ввод хоста для сервера s += sf.winHostServer() case ModeInputLoginServer: // Ввод логина для входа на сервер s += sf.winLoginServer() case ModeInputPassServer: // ввод пароля s += sf.winPassServer() } return s } func (sf Desktop) winPassServer() string { s := "=Ввод пароля для сервера вар-танк=\n\n" s += fmt.Sprintf(">%v\n", sf.textInput.View()) s += "\n\n[ctrl+q]-выход [q]-назад\n" return s } func (sf Desktop) winLoginServer() string { s := "=Ввод логина для сервера вар-танк=\n\n" s += fmt.Sprintf(">%v\n", sf.textInput.View()) s += "\n\n[ctrl+q]-выход [q]-назад\n" return s } func (sf Desktop) winHostServer() string { s := "=Ввод хоста для сервера вар-танк=\n\n" s += fmt.Sprintf(">%v\n", sf.textInput.View()) s += "\n\n[ctrl+q]-выход [q]-назад\n" return s } func (sf Desktop) winConfigServer() string { s := "Настройки сервера вар-танк\n\n" s += "[1]-Настройки хоста сервера\n" s += "[2]-Логин для входа\n" s += "[3]-Пароль для входа\n" s += "\n\n[ctrl+q]-выход [q]-назад\n" return s } func (m TeaModel) winConfig() string { s := "Настройки клиента\n\n" s += "[1]-Настройки сервера\n" s += "[2]-Вход на сервер\n" s += "\n\n[ctrl+q]-выход [q]-назад\n" return s } func (m TeaModel) winDefault() string { // Заголовок s := "Что нам нужно сделать?\n\n" // Перебор по списку выбора for i, choice := range m.choices { // Курсор указывает на этот выбор? cursor := " " // нет курсора if m.cursor == i { cursor = ">" // есть курсор } // Этот элемент выбран? checked := " " // не выбран if _, ok := m.selected[i]; ok { checked = "x" // выбран! } // отрендерить заново s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice) } // Подвал s += "\n\n[ctrl+q]-выход [w]-настройки\n" // Вернуть строку экрана для отрисовки return s }