package ui import ( "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" ) func newMenuList() list.Model { items := []list.Item{ simpleItem{id: "users", title: "Пользователи", desc: "Создать, заблокировать, удалить учётные записи"}, simpleItem{id: "groups", title: "Группы и хосты", desc: "Кому какие хосты разрешены для прыжка/форвардинга"}, simpleItem{id: "export", title: "Экспорт архивов", desc: "Сгенерировать ключи и .7z архивы для доставки пользователям"}, simpleItem{id: "audit", title: "Аудит / кто в сети", desc: "Активные сессии и журнал подключений"}, simpleItem{id: "quit", title: "Выход", desc: ""}, } l := list.New(items, list.NewDefaultDelegate(), 0, 0) l.Title = "ssh-access-tui" l.SetShowStatusBar(false) return l } func (m Model) updateMenu(msg tea.Msg) (tea.Model, tea.Cmd) { if key, ok := msg.(tea.KeyMsg); ok { switch key.String() { case "enter": sel, ok := m.menu.SelectedItem().(simpleItem) if !ok { return m, nil } m.err, m.status = "", "" switch sel.id { case "users": m.screen = scrUsers m.refreshUsersList() case "groups": m.screen = scrGroups m.refreshGroupsList() case "export": m.screen = scrExportSelect m.initExportSelect() case "audit": m.screen = scrAudit m.refreshAudit() case "quit": return m.attemptQuit() } return m, nil case "q": return m.attemptQuit() } } var cmd tea.Cmd m.menu, cmd = m.menu.Update(msg) return m, cmd } func (m Model) viewMenu() string { return borderStyle.Render(m.menu.View()) } // unexportedUsers returns active users who have never had a key issued // (empty PublicKey) — created but forgotten before "Экспорт архивов". func unexportedUsers(m *Model) []string { st := m.loadState() var out []string for _, u := range st.Users { if u.PublicKey == "" { out = append(out, u.Username) } } return out } // attemptQuit is the only path that actually quits from the menu ("q" or // selecting "Выход"): if there are users created but never exported, it // warns instead of quitting straight away, since it's easy to create a // batch of users, get interrupted, and forget the export step entirely. // ctrl+c bypasses this deliberately, as an always-available hard exit. func (m Model) attemptQuit() (tea.Model, tea.Cmd) { pending := unexportedUsers(&m) if len(pending) == 0 { return m, tea.Quit } m.quitPending = pending m.quitReturnTo = scrMenu m.screen = scrQuitConfirm return m, nil } func (m Model) updateQuitConfirm(msg tea.Msg) (tea.Model, tea.Cmd) { if key, ok := msg.(tea.KeyMsg); ok { switch key.String() { case "enter", "y": return m, tea.Quit case "esc", "n": m.screen = m.quitReturnTo return m, nil } } return m, nil } func (m Model) viewQuitConfirm() string { body := warnStyle.Render("Есть пользователи, для которых ещё не сгенерирован ключ (не были в «Экспорт архивов»):") + "\n\n" for _, u := range m.quitPending { body += " - " + u + "\n" } body += "\nБез экспорта у них нет ключа и архива для передачи — доступ не работает.\n" body += "\n[enter/y] выйти всё равно [esc/n] вернуться" return borderStyle.Render(body) }