package ui import ( "fmt" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "ssh-access-tui/internal/model" ) func (m *Model) refreshGroupsList() { st := m.loadState() items := make([]list.Item, 0, len(st.Groups)) for _, g := range st.Groups { desc := fmt.Sprintf("%d хостов", len(g.Hosts)) if g.Description != "" { desc += " — " + g.Description } items = append(items, simpleItem{id: g.Name, title: g.Name, desc: desc}) } m.groupsList = newList(items, "Группы [n]новая [e]изменить [enter]хосты группы [d]удалить [esc]назад", m.groupsList.Width(), m.groupsList.Height()) } func (m Model) updateGroups(msg tea.Msg) (tea.Model, tea.Cmd) { if key, ok := msg.(tea.KeyMsg); ok { switch key.String() { case "esc": m.screen = scrMenu m.err, m.status = "", "" return m, nil case "n": m.groupFormEditTarget = "" for i := range m.groupForm { m.groupForm[i] = textinput.New() } m.groupForm[0].Placeholder = "имя группы, например team-billing" m.groupForm[1].Placeholder = "описание (необязательно)" m.groupFormFocus = 0 m.groupForm[0].Focus() m.err = "" m.screen = scrGroupForm return m, nil case "e": sel, ok := m.groupsList.SelectedItem().(simpleItem) if !ok { return m, nil } st := m.loadState() g := st.FindGroup(sel.id) if g == nil { return m, nil } m.groupFormEditTarget = g.Name for i := range m.groupForm { m.groupForm[i] = textinput.New() } m.groupForm[0].SetValue(g.Name) m.groupForm[1].SetValue(g.Description) m.groupFormFocus = 0 m.groupForm[0].Focus() m.err = "" m.screen = scrGroupForm return m, nil case "enter": sel, ok := m.groupsList.SelectedItem().(simpleItem) if !ok { return m, nil } m.selectedGroup = sel.id st := m.loadState() g := st.FindGroup(sel.id) m.groupHostsPickSel = map[string]bool{} if g != nil { for _, hn := range g.Hosts { m.groupHostsPickSel[hn] = true } } m.groupHostsPickOrder = nil for _, h := range st.Hosts { m.groupHostsPickOrder = append(m.groupHostsPickOrder, h.Name) } m.groupHostsPickCursor = 0 m.screen = scrGroupHostsPick return m, nil case "d": sel, ok := m.groupsList.SelectedItem().(simpleItem) if !ok { return m, nil } st := m.loadState() affected := st.UsernamesInGroup(sel.id) msg := fmt.Sprintf( "Удалить группу %q?\n\n"+ "Это затронет пользователей: %d — группа будет снята с них, и они\n"+ "немедленно потеряют доступ ко всем сервисам, которые давала только эта\n"+ "группа.", sel.id, len(affected)) m = m.startConfirm(confirmDeleteGroup, sel.id, msg, scrGroups) return m, nil } } var cmd tea.Cmd m.groupsList, cmd = m.groupsList.Update(msg) return m, cmd } func (m Model) viewGroups() string { return borderStyle.Render(m.groupsList.View()) } func (m Model) updateGroupForm(msg tea.Msg) (tea.Model, tea.Cmd) { if key, ok := msg.(tea.KeyMsg); ok { switch key.String() { case "esc": m.screen = scrGroups return m, nil case "tab", "down": m.groupForm[m.groupFormFocus].Blur() m.groupFormFocus = (m.groupFormFocus + 1) % len(m.groupForm) m.groupForm[m.groupFormFocus].Focus() return m, nil case "enter": name := m.groupForm[0].Value() desc := m.groupForm[1].Value() if name == "" { m.err = "имя группы не может быть пустым" return m, nil } if !model.ValidName(name) { m.err = validNameErr("группа", name) return m, nil } target := m.groupFormEditTarget m.err = "" if target == "" { m.mutate("создана группа "+name, func(st *model.State) error { if st.FindGroup(name) != nil { return fmt.Errorf("группа %q уже существует", name) } st.Groups = append(st.Groups, model.Group{Name: name, Description: desc}) return nil }) } else { m.mutate("изменена группа "+target, func(st *model.State) error { if name != target && st.FindGroup(name) != nil { return fmt.Errorf("группа %q уже существует", name) } if name != target { st.RenameGroup(target, name) } g := st.FindGroup(name) if g == nil { return fmt.Errorf("group %s not found", name) } g.Description = desc return nil }) } m.screen = scrGroups m.refreshGroupsList() return m, nil } } var cmd tea.Cmd m.groupForm[m.groupFormFocus], cmd = m.groupForm[m.groupFormFocus].Update(msg) return m, cmd } func (m Model) viewGroupForm() string { title := "Новая группа" if m.groupFormEditTarget != "" { title = fmt.Sprintf("Изменить группу %q", m.groupFormEditTarget) } body := fmt.Sprintf("%s\n\nИмя: %s\nОписание: %s\n\n[tab] след. поле [enter] сохранить [esc] отмена", title, m.groupForm[0].View(), m.groupForm[1].View()) return borderStyle.Render(body) } // --- pick which catalog hosts belong to the selected group --- func (m Model) updateGroupHostsPick(msg tea.Msg) (tea.Model, tea.Cmd) { if key, ok := msg.(tea.KeyMsg); ok { switch key.String() { case "esc": m.screen = scrGroups m.refreshGroupsList() return m, nil case "up", "k": if m.groupHostsPickCursor > 0 { m.groupHostsPickCursor-- } return m, nil case "down", "j": if m.groupHostsPickCursor < len(m.groupHostsPickOrder)-1 { m.groupHostsPickCursor++ } return m, nil case " ": if len(m.groupHostsPickOrder) > 0 { h := m.groupHostsPickOrder[m.groupHostsPickCursor] m.groupHostsPickSel[h] = !m.groupHostsPickSel[h] } return m, nil case "enter": var hosts []string for _, h := range m.groupHostsPickOrder { if m.groupHostsPickSel[h] { hosts = append(hosts, h) } } groupName := m.selectedGroup m.mutate("изменён список хостов группы "+groupName, func(st *model.State) error { g := st.FindGroup(groupName) if g == nil { return fmt.Errorf("group %s not found", groupName) } g.Hosts = hosts return resyncGroupUsers(st, groupName) }) m.screen = scrGroups m.refreshGroupsList() return m, nil } } return m, nil } func (m Model) viewGroupHostsPick() string { body := fmt.Sprintf("Какие хосты каталога входят в группу %q\n\n", m.selectedGroup) if len(m.groupHostsPickOrder) == 0 { body += warnStyle.Render("В каталоге ещё нет хостов — сначала добавьте их в разделе «Хосты и сервисы».") + "\n" } for i, h := range m.groupHostsPickOrder { cursor := " " if i == m.groupHostsPickCursor { cursor = "> " } check := "[ ]" if m.groupHostsPickSel[h] { check = "[x]" } body += fmt.Sprintf("%s%s %s\n", cursor, check, h) } body += "\n[space] отметить [enter] сохранить [esc] назад" return borderStyle.Render(body) }