cherry/launcher/main.go

327 lines
7.5 KiB
Go
Raw Normal View History

2025-02-11 18:27:13 -06:00
// 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"
)
2025-02-11 18:52:20 -06:00
// ===============
// lipgloss styles
2025-02-11 18:27:13 -06:00
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)
2025-02-11 18:52:20 -06:00
// ============
// data structs
2025-02-11 18:27:13 -06:00
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 }
2025-02-11 18:52:20 -06:00
// ======================
// bubbletea model struct
2025-02-11 18:27:13 -06:00
type model struct {
categoriesList list.Model
categoriesKeys *categoriesKeyMap
programsList list.Model
programsKeys *programsKeyMap
selectedCategory bool
shouldExec string
}
2025-02-11 18:52:20 -06:00
// =============
// list keybinds
2025-02-11 18:27:13 -06:00
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"),
),
}
}
2025-02-11 18:52:20 -06:00
// =======================
// bubbletea model methods
2025-02-11 18:27:13 -06:00
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())
}
}
2025-02-11 18:52:20 -06:00
// ============
// run launcher
2025-02-11 18:27:13 -06:00
func Run(cmd *cobra.Command, args []string) error {
2025-02-11 18:52:20 -06:00
// =========
// init data
// TODO read this from a file instead
2025-02-11 18:27:13 -06:00
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",
},
2025-02-11 18:41:14 -06:00
Program{
title: "micro",
desc: "modern batteries-included editor",
exec: "micro",
},
2025-02-11 18:27:13 -06:00
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",
},
},
},
2025-02-11 21:26:04 -06:00
Category{
title: "utilities",
desc: "misc commands",
programs: []list.Item{
Program{
title: "chsh",
desc: "change your login shell",
exec: "chsh",
},
},
},
2025-02-11 18:27:13 -06:00
}
2025-02-11 18:52:20 -06:00
// =============================
// create delegates and keybinds
2025-02-11 18:27:13 -06:00
categoryDelegate := list.NewDefaultDelegate()
categoryKeys := newCategoriesKeyMap()
programDelegate := list.NewDefaultDelegate()
programKeys := newProgramsKeyMap()
2025-02-11 18:52:20 -06:00
// ===================
// set delegate styles
2025-02-11 18:27:13 -06:00
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
2025-02-11 18:52:20 -06:00
// ====================
// define initial model
2025-02-11 18:33:52 -06:00
initialModel := model{
2025-02-11 18:27:13 -06:00
categoriesList: list.New(categories, categoryDelegate, 0, 0),
categoriesKeys: categoryKeys,
programsList: list.New([]list.Item{}, programDelegate, 0, 0),
programsKeys: programKeys,
selectedCategory: false,
}
2025-02-11 18:52:20 -06:00
// =================================
// set initial model styles and keys
2025-02-11 18:33:52 -06:00
initialModel.categoriesList.Title = "the cherry.town program directory"
initialModel.categoriesList.Styles.Title = titleStyle
initialModel.categoriesList.AdditionalShortHelpKeys = func() []key.Binding {
2025-02-11 18:27:13 -06:00
return []key.Binding{
categoryKeys.selectKey,
}
}
2025-02-11 18:33:52 -06:00
initialModel.categoriesList.AdditionalFullHelpKeys = func() []key.Binding {
2025-02-11 18:27:13 -06:00
return []key.Binding{
categoryKeys.selectKey,
}
}
2025-02-11 18:33:52 -06:00
initialModel.programsList.Styles.Title = categoryTitleStyle
initialModel.programsList.AdditionalShortHelpKeys = func() []key.Binding {
2025-02-11 18:27:13 -06:00
return []key.Binding{
programKeys.exec,
programKeys.back,
}
}
2025-02-11 18:33:52 -06:00
initialModel.programsList.AdditionalFullHelpKeys = func() []key.Binding {
2025-02-11 18:27:13 -06:00
return []key.Binding{
programKeys.exec,
programKeys.back,
}
}
2025-02-11 18:52:20 -06:00
// =====================
// run bubbletea program
2025-02-11 18:33:52 -06:00
p := tea.NewProgram(initialModel)
finalModel, err := p.Run()
2025-02-11 18:27:13 -06:00
if err != nil {
fmt.Println("error: ", err)
os.Exit(1)
}
2025-02-11 18:52:20 -06:00
// ============
// clear screen
2025-02-11 18:46:59 -06:00
// TODO this feels hacky and should probably be replaced
execCmd := exec.Command("clear")
execCmd.Stdout = os.Stdout
execCmd.Run()
2025-02-11 18:52:20 -06:00
// ====================================
// execute chosen command if one is set
2025-02-11 18:33:52 -06:00
if finalModel, ok := finalModel.(model); ok && finalModel.shouldExec != "" {
execCmd := exec.Command(finalModel.shouldExec)
2025-02-11 18:27:13 -06:00
execCmd.Stdin = os.Stdin
execCmd.Stdout = os.Stdout
execCmd.Stderr = os.Stderr
return execCmd.Run()
}
return nil
}