initial commit

This commit is contained in:
bunny winter 2025-02-11 18:27:13 -06:00
commit 5a2ca8f361
Signed by: nya
SSH key fingerprint: SHA256:aFOeJZAb3Y7XJxd+BReehNPp/dmurGW9M13mIayeOm0
7 changed files with 418 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
bin/

21
Makefile Normal file
View file

@ -0,0 +1,21 @@
BIN = bin
BINS += $(BIN)/cherry
SRC = $(shell find . -iname "*.go")
.PHONY: all
all: $(BINS)
.PHONY: clean
clean:
$(RM) $(BINS)
.PHONY: install
install: all
cp bin/cherry /usr/local/bin/cherry
$(BIN):
mkdir -p $(BIN)
$(BIN)/cherry: $(BIN) $(SRC)
go build -o $@ .

25
cmd/root.go Normal file
View file

@ -0,0 +1,25 @@
package cmd
import (
"fmt"
"os"
"git.cherry.town/town/cherry/launcher"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "cherry",
Short: "command-line utilities for cherry.town",
Long: "command-line utilities for cherry.town. if no sub-command\n" +
"is specified, the cherry.town program directory will be run.",
RunE: launcher.Run,
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

32
go.mod Normal file
View file

@ -0,0 +1,32 @@
module git.cherry.town/town/cherry
go 1.22.2
require (
github.com/charmbracelet/bubbles v0.20.0
github.com/charmbracelet/bubbletea v1.3.3
github.com/charmbracelet/lipgloss v1.0.0
github.com/spf13/cobra v1.8.1
)
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.3.8 // indirect
)

55
go.sum Normal file
View file

@ -0,0 +1,55 @@
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
github.com/charmbracelet/bubbletea v1.3.3 h1:WpU6fCY0J2vDWM3zfS3vIDi/ULq3SYphZhkAGGvmEUY=
github.com/charmbracelet/bubbletea v1.3.3/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

275
launcher/main.go Normal file
View file

@ -0,0 +1,275 @@
// warning this code is messy and terrible
// TODO read list data from file
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"
)
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)
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 }
type model struct {
categoriesList list.Model
categoriesKeys *categoriesKeyMap
programsList list.Model
programsKeys *programsKeyMap
selectedCategory bool
shouldExec string
}
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"),
),
}
}
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())
}
}
func Run(cmd *cobra.Command, args []string) error {
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: "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",
},
},
},
}
categoryDelegate := list.NewDefaultDelegate()
categoryKeys := newCategoriesKeyMap()
programDelegate := list.NewDefaultDelegate()
programKeys := newProgramsKeyMap()
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
m := model{
categoriesList: list.New(categories, categoryDelegate, 0, 0),
categoriesKeys: categoryKeys,
programsList: list.New([]list.Item{}, programDelegate, 0, 0),
programsKeys: programKeys,
selectedCategory: false,
}
m.categoriesList.Title = "the cherry.town program directory"
m.categoriesList.Styles.Title = titleStyle
m.categoriesList.AdditionalShortHelpKeys = func() []key.Binding {
return []key.Binding{
categoryKeys.selectKey,
}
}
m.categoriesList.AdditionalFullHelpKeys = func() []key.Binding {
return []key.Binding{
categoryKeys.selectKey,
}
}
m.programsList.Styles.Title = categoryTitleStyle
m.programsList.AdditionalShortHelpKeys = func() []key.Binding {
return []key.Binding{
programKeys.exec,
programKeys.back,
}
}
m.programsList.AdditionalFullHelpKeys = func() []key.Binding {
return []key.Binding{
programKeys.exec,
programKeys.back,
}
}
p := tea.NewProgram(m)
mf, err := p.Run()
if err != nil {
fmt.Println("error: ", err)
os.Exit(1)
}
if mf, ok := mf.(model); ok && mf.shouldExec != "" {
execCmd := exec.Command(mf.shouldExec)
execCmd.Stdin = os.Stdin
execCmd.Stdout = os.Stdout
execCmd.Stderr = os.Stderr
return execCmd.Run()
}
return nil
}

9
main.go Normal file
View file

@ -0,0 +1,9 @@
package main
import (
"git.cherry.town/town/cherry/cmd"
)
func main() {
cmd.Execute()
}