package model import ( "fmt" "github.com/StevanFreeborn/term-tier/internal/handlers" "github.com/StevanFreeborn/term-tier/internal/state" "github.com/StevanFreeborn/term-tier/internal/styles" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) type Mode int const ( ModeNav Mode = iota ModeInput ) type Model struct { State state.State KeyMsgHandlers []handlers.KeyMsgHandler WinSizeMsgHandlers []handlers.WinSizeMsgHandler } type option func(m *Model) func New(state state.State, opts ...option) Model { ti := textinput.New() ti.CharLimit = 50 ti.Width = 30 m := Model{ State: state, } for _, opt := range opts { opt(&m) } return m } func WithKeyMsgHandlers(handlers []handlers.KeyMsgHandler) option { return func(m *Model) { m.KeyMsgHandlers = handlers } } func WithWinSizeMsgHandlers(handlers []handlers.WinSizeMsgHandler) option { return func(m *Model) { m.WinSizeMsgHandlers = handlers } } func (m Model) Init() tea.Cmd { return textinput.Blink } func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: for _, handler := range m.WinSizeMsgHandlers { if handler.Match(m.State, msg) { return m, handler.Handle(m.State, msg) } } case tea.KeyMsg: for _, handler := range m.KeyMsgHandlers { if handler.Match(m.State, msg) { return m, handler.Handle(m.State, msg) } } } return m, nil } func (m Model) View() string { if m.State.Mode() == state.InputMode { return fmt.Sprintf( "\n %s\n\n%s\n\n (Enter to Confirm, Esc to Cancel)", m.State.InputTitle(), styles.InputBoxStyle.Render(m.State.Input().View()), ) } if m.State.Mode() == state.HelpMode { helpText := " HELP MENU\n\n" for _, handler := range m.KeyMsgHandlers { if desc := handler.Description(); desc != "" { keys := handler.KeysHandled() helpText += fmt.Sprintf(" %s: %s\n", keys, desc) } } helpText += "\n (Press ? to close this menu)" box := styles.HelpBoxStyle.Render(helpText) return lipgloss.Place( m.State.WindowWidth(), m.State.WindowHeight(), lipgloss.Center, lipgloss.Center, box, ) } s := "[?] Help ️ [q] Quit\n\n" availableWidth := max(m.State.WindowWidth()-15, 20) for tierIndex, tier := range m.State.Tiers() { var rows []string var currentRowItems []string currentWidth := 0 for itemIndex, item := range tier.Items { renderStyle := styles.ItemStyle isFocused := tierIndex == m.State.CursorRow() && itemIndex == m.State.CursorCol() if isFocused { if m.State.Dragging() { renderStyle = styles.DraggingItemStyle } else { renderStyle = styles.SelectedItemStyle } } renderedItem := renderStyle.Render(string(item)) itemWidth := lipgloss.Width(renderedItem) if currentWidth+itemWidth > availableWidth && len(currentRowItems) > 0 { rows = append(rows, lipgloss.JoinHorizontal(lipgloss.Top, currentRowItems...)) currentRowItems = []string{renderedItem} currentWidth = itemWidth } else { currentRowItems = append(currentRowItems, renderedItem) currentWidth += itemWidth } } if len(tier.Items) == 0 && tierIndex == m.State.CursorRow() { ghost := lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render(" [ ]") currentRowItems = append(currentRowItems, ghost) } if len(currentRowItems) > 0 { rows = append(rows, lipgloss.JoinHorizontal(lipgloss.Top, currentRowItems...)) } itemBlock := lipgloss.JoinVertical(lipgloss.Left, rows...) blockHeight := lipgloss.Height(itemBlock) label := styles.TierLabelStyle. Background(tier.Color). Height(blockHeight). AlignVertical(lipgloss.Center). Render(tier.Name) row := lipgloss.JoinHorizontal(lipgloss.Center, label, itemBlock) s += row + "\n\n" } if m.State.StatusMsg() != "" { s += styles.StatusStyle.Render(" STATUS: " + m.State.StatusMsg()) } return s }