package main import ( "encoding/csv" "fmt" "os" "os/exec" "strings" "time" "charm.land/bubbles/v2/viewport" tea "charm.land/bubbletea/v2" ) type model struct { choices [][]string cursor int selected map[int]struct{} ready bool viewport viewport.Model } func listModel(arg string) model { out, err := exec.Command("hugo", "list", arg).Output() if err != nil { println("Error fetching Hugo drafts: Is this a Hugo site? Is Hugo installed?") os.Exit(1) } csvReader := csv.NewReader(strings.NewReader(string(out))) records, err := csvReader.ReadAll() if err != nil { println("Error parsing draft list.") os.Exit(1) } if len(records) <= 1 { println("No drafts found.") os.Exit(1) } // remove the table head row records = records[1:] return model{ choices: records, selected: make(map[int]struct{}), } } func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var ( cmd tea.Cmd cmds []tea.Cmd ) switch msg := msg.(type) { case tea.KeyPressMsg: switch msg.String() { case "ctrl+c", "q", "esc": return m, tea.Quit case "k": if m.cursor > 0 { m.cursor-- m.viewport.SetContent(m.buildContent()) } return m, nil case "j": if m.cursor < len(m.choices)-1 { m.cursor++ m.viewport.SetContent(m.buildContent()) } return m, nil case "enter": choice := m.choices[m.cursor] path := choice[0] return m, openEditor(path) case "space": _, ok := m.selected[m.cursor] if ok { delete(m.selected, m.cursor) } else { m.selected[m.cursor] = struct{}{} } m.viewport.SetContent(m.buildContent()) return m, nil } case tea.WindowSizeMsg: headerHeight := 1 footerHeight := 1 verticalMarginHeight := headerHeight + footerHeight if !m.ready { m.viewport = viewport.New(viewport.WithWidth(msg.Width), viewport.WithHeight(msg.Height-verticalMarginHeight)) m.viewport.YPosition = headerHeight m.viewport.SetContent(m.buildContent()) m.ready = true } else { m.viewport.SetWidth(msg.Width) m.viewport.SetHeight(msg.Height - verticalMarginHeight) } case editorFinishedMsg: return m, tea.Quit } m.viewport, cmd = m.viewport.Update(msg) cmds = append(cmds, cmd) return m, tea.Batch(cmds...) } func max(a, b int) int { if a > b { return a } return b } func (m model) buildContent() string { var s strings.Builder for i, choice := range m.choices { cursor := " " // no cursor if m.cursor == i { cursor = ">" } checked := " " // not checked if _, ok := m.selected[i]; ok { checked = "x" } title := choice[2] t, err := time.Parse(time.RFC3339, choice[3]) if err != nil { continue } datetime := t.Format("2006-01-02 15:04") s.WriteString(fmt.Sprintf("%s [%s] %s \t %s\n", cursor, checked, datetime, title)) } return s.String() } func (m model) headerView() string { title := fmt.Sprintf("Hugo Content Manager (%d items)", len(m.choices)) line := strings.Repeat("─", max(0, m.viewport.Width()-len(title))) return title + line } func (m model) footerView() string { help := "[q] quit | [Enter] edit | [SPACE] select | [j/k] cursor | [↑/↓] scroll" line := strings.Repeat("─", max(0, m.viewport.Width()-len(help))) return line + help } func (m model) View() tea.View { if !m.ready { return tea.NewView("\n Initializing...") } return tea.NewView(fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView())) }