Gitfed
bastien-mrq/sssh / internal / tui / list.go
// Package tui fournit la liste interactive et les formulaires de sssh.
package tui

import (
	"github.com/charmbracelet/bubbles/key"
	"github.com/charmbracelet/bubbles/list"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"git.neuromancer.ovh/bastien-mrq/sssh/internal/host"
)

// Action est ce que l'utilisateur a demandé en quittant la liste.
type Action int

const (
	ActionQuit Action = iota
	ActionConnect
	ActionAdd
	ActionEdit
	ActionDelete
	ActionImportSSH
)

// Result porte l'action choisie et le host concerné le cas échéant.
type Result struct {
	Action Action
	Host   host.Host
}

type item struct{ h host.Host }

func (i item) Title() string       { return i.h.Name }
func (i item) Description() string { return i.h.Describe() }
func (i item) FilterValue() string {
	v := i.h.Name + " " + i.h.Host
	for _, t := range i.h.Tags {
		v += " " + t
	}
	return v
}

var docStyle = lipgloss.NewStyle().Margin(1, 2)

type model struct {
	list         list.Model
	canImportSSH bool
	result       Result
}

func (m model) Init() tea.Cmd { return nil }

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		fw, fh := docStyle.GetFrameSize()
		m.list.SetSize(msg.Width-fw, msg.Height-fh)

	case tea.KeyMsg:
		if m.list.FilterState() == list.Filtering {
			break
		}
		switch msg.String() {
		case "enter":
			if it, ok := m.list.SelectedItem().(item); ok {
				m.result = Result{Action: ActionConnect, Host: it.h}
				return m, tea.Quit
			}
		case "a":
			m.result = Result{Action: ActionAdd}
			return m, tea.Quit
		case "e", "d":
			it, ok := m.list.SelectedItem().(item)
			if !ok {
				break
			}
			action := ActionEdit
			if msg.String() == "d" {
				action = ActionDelete
			}
			m.result = Result{Action: action, Host: it.h}
			return m, tea.Quit
		case "i":
			if !m.canImportSSH {
				return m, m.list.NewStatusMessage("rien à importer depuis ~/.ssh/config")
			}
			m.result = Result{Action: ActionImportSSH}
			return m, tea.Quit
		case "q", "ctrl+c":
			m.result = Result{Action: ActionQuit}
			return m, tea.Quit
		}
	}

	var cmd tea.Cmd
	m.list, cmd = m.list.Update(msg)
	return m, cmd
}

func (m model) View() string {
	return docStyle.Render(m.list.View())
}

// Run affiche la liste des hosts et rend l'action choisie.
// canImportSSH indique si ~/.ssh/config contient des hosts importables.
func Run(hosts []host.Host, canImportSSH bool) (Result, error) {
	items := make([]list.Item, len(hosts))
	for i, h := range hosts {
		items[i] = item{h}
	}

	l := list.New(items, list.NewDefaultDelegate(), 0, 0)
	l.Title = "sssh"
	l.SetStatusBarItemName("host", "hosts")

	// Libellés compacts (et en français) pour que toute l'aide, touche i
	// comprise, tienne sur une ligne en 80 colonnes.
	l.KeyMap.CursorUp.SetHelp("↑", "haut")
	l.KeyMap.CursorDown.SetHelp("↓", "bas")
	l.KeyMap.Filter.SetHelp("/", "filtre")
	l.KeyMap.Quit.SetHelp("q", "quitter")
	l.KeyMap.ShowFullHelp.SetHelp("?", "aide")
	l.KeyMap.CloseFullHelp.SetHelp("?", "fermer l'aide")
	extraKeys := func() []key.Binding {
		return []key.Binding{
			key.NewBinding(key.WithKeys("enter"), key.WithHelp("↵", "ssh")),
			key.NewBinding(key.WithKeys("i"), key.WithHelp("i", "import")),
			key.NewBinding(key.WithKeys("a"), key.WithHelp("a", "ajout")),
			key.NewBinding(key.WithKeys("e"), key.WithHelp("e", "édite")),
			key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "suppr")),
		}
	}
	l.AdditionalShortHelpKeys = extraKeys
	l.AdditionalFullHelpKeys = extraKeys

	p := tea.NewProgram(model{list: l, canImportSSH: canImportSSH}, tea.WithAltScreen())
	out, err := p.Run()
	if err != nil {
		return Result{}, err
	}
	return out.(model).result, nil
}