cherry/launcher/main.go

315 lines
7.3 KiB
Go

// warning this code is messy and terrible
package launcher
import (
"fmt"
"os"
"os/exec"
"github.com/spf13/cobra"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// ===============
// lipgloss styles
var appStyle = lipgloss.NewStyle().Margin(1, 2)
var titleStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("0")).
Background(lipgloss.Color("5")).
Padding(0, 1)
var categoryTitleStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("0")).
Background(lipgloss.Color("4")).
Padding(0, 1)
// ============
// data structs
type Category struct {
title string
desc string
programs []list.Item
}
func (c Category) Title() string { return c.title }
func (c Category) Description() string { return c.desc }
func (c Category) FilterValue() string { return c.title }
type Program struct {
title string
desc string
exec string
}
func (p Program) Title() string { return p.title }
func (p Program) Description() string { return p.desc }
func (p Program) FilterValue() string { return p.title }
// ======================
// bubbletea model struct
type model struct {
categoriesList list.Model
categoriesKeys *categoriesKeyMap
programsList list.Model
programsKeys *programsKeyMap
selectedCategory bool
shouldExec string
}
// =============
// list keybinds
type categoriesKeyMap struct {
selectKey key.Binding
}
func newCategoriesKeyMap() *categoriesKeyMap {
return &categoriesKeyMap{
selectKey: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "select"),
),
}
}
type programsKeyMap struct {
exec key.Binding
back key.Binding
}
func newProgramsKeyMap() *programsKeyMap {
return &programsKeyMap{
exec: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "exec"),
),
back: key.NewBinding(
key.WithKeys("b"),
key.WithHelp("b", "back"),
),
}
}
// =======================
// bubbletea model methods
func (m model) Init() tea.Cmd {
return nil
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.String() == "ctrl+c" {
return m, tea.Quit
}
if !m.selectedCategory {
switch {
case key.Matches(msg, m.categoriesKeys.selectKey):
var cat = m.categoriesList.SelectedItem().(Category)
m.programsList.SetItems(cat.programs)
m.programsList.Title = cat.title
m.selectedCategory = true
}
} else {
switch {
case key.Matches(msg, m.programsKeys.back):
m.selectedCategory = false
case key.Matches(msg, m.programsKeys.exec):
var prog = m.programsList.SelectedItem().(Program)
m.shouldExec = prog.exec
return m, tea.Quit
}
}
case tea.WindowSizeMsg:
h, v := appStyle.GetFrameSize()
m.categoriesList.SetSize(msg.Width-h, msg.Height-v)
m.programsList.SetSize(msg.Width-h, msg.Height-v)
}
var cmd tea.Cmd
if m.selectedCategory {
m.programsList, cmd = m.programsList.Update(msg)
} else {
m.categoriesList, cmd = m.categoriesList.Update(msg)
}
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
func (m model) View() string {
if m.selectedCategory {
return appStyle.Render(m.programsList.View())
} else {
return appStyle.Render(m.categoriesList.View())
}
}
// ============
// run launcher
func Run(cmd *cobra.Command, args []string) error {
// =========
// init data
// TODO read this from a file instead
categories := []list.Item{
Category{
title: "editors",
desc: "for all your programming needs",
programs: []list.Item{
Program{
title: "helix (hx)",
desc: "next-generation modal editor",
exec: "hx",
},
Program{
title: "micro",
desc: "modern batteries-included editor",
exec: "micro",
},
Program{
title: "nano",
desc: "the easy option",
exec: "nano",
},
Program{
title: "neovim (nvim)",
desc: "like vim, but neo",
exec: "nvim",
},
Program{
title: "vim",
desc: "the one and only",
exec: "vim",
},
},
},
Category{
title: "games",
desc: "terminal fun",
programs: []list.Item{
Program{
title: "botany",
desc: "grow a plant in your terminal",
exec: "botany",
},
Program{
title: "nsnake",
desc: "ncurses snake game",
exec: "nsnake",
},
},
},
Category{
title: "social",
desc: "email, irc, etc.",
programs: []list.Item{
Program{
title: "irssi",
desc: "minimal irc client",
exec: "irssi",
},
Program{
title: "mutt",
desc: "tui email client",
exec: "mutt",
},
Program{
title: "weechat",
desc: "extendable, easy-to-use irc client",
exec: "weechat",
},
},
},
}
// =============================
// create delegates and keybinds
categoryDelegate := list.NewDefaultDelegate()
categoryKeys := newCategoriesKeyMap()
programDelegate := list.NewDefaultDelegate()
programKeys := newProgramsKeyMap()
// ===================
// set delegate styles
categoryDelegate.Styles.SelectedTitle = categoryDelegate.Styles.SelectedTitle.
Foreground(lipgloss.Color("4")).BorderLeftForeground(lipgloss.Color("4"))
categoryDelegate.Styles.SelectedDesc = categoryDelegate.Styles.SelectedTitle
programDelegate.Styles.SelectedTitle = programDelegate.Styles.SelectedTitle.
Foreground(lipgloss.Color("1")).BorderLeftForeground(lipgloss.Color("1"))
programDelegate.Styles.SelectedDesc = programDelegate.Styles.SelectedTitle
// ====================
// define initial model
initialModel := model{
categoriesList: list.New(categories, categoryDelegate, 0, 0),
categoriesKeys: categoryKeys,
programsList: list.New([]list.Item{}, programDelegate, 0, 0),
programsKeys: programKeys,
selectedCategory: false,
}
// =================================
// set initial model styles and keys
initialModel.categoriesList.Title = "the cherry.town program directory"
initialModel.categoriesList.Styles.Title = titleStyle
initialModel.categoriesList.AdditionalShortHelpKeys = func() []key.Binding {
return []key.Binding{
categoryKeys.selectKey,
}
}
initialModel.categoriesList.AdditionalFullHelpKeys = func() []key.Binding {
return []key.Binding{
categoryKeys.selectKey,
}
}
initialModel.programsList.Styles.Title = categoryTitleStyle
initialModel.programsList.AdditionalShortHelpKeys = func() []key.Binding {
return []key.Binding{
programKeys.exec,
programKeys.back,
}
}
initialModel.programsList.AdditionalFullHelpKeys = func() []key.Binding {
return []key.Binding{
programKeys.exec,
programKeys.back,
}
}
// =====================
// run bubbletea program
p := tea.NewProgram(initialModel)
finalModel, err := p.Run()
if err != nil {
fmt.Println("error: ", err)
os.Exit(1)
}
// ============
// clear screen
// TODO this feels hacky and should probably be replaced
execCmd := exec.Command("clear")
execCmd.Stdout = os.Stdout
execCmd.Run()
// ====================================
// execute chosen command if one is set
if finalModel, ok := finalModel.(model); ok && finalModel.shouldExec != "" {
execCmd := exec.Command(finalModel.shouldExec)
execCmd.Stdin = os.Stdin
execCmd.Stdout = os.Stdout
execCmd.Stderr = os.Stderr
return execCmd.Run()
}
return nil
}