// 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: "nethack", desc: "80s console roguelike", exec: "nethack", }, 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", }, }, }, Category{ title: "utilities", desc: "misc commands", programs: []list.Item{ Program{ title: "chsh", desc: "change your login shell", exec: "chsh", }, Program{ title: "passwd", desc: "change your password", exec: "passwd", }, }, }, } // ============================= // 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 }