From 5a2ca8f361abd09d97a154b457f8e34c3da0cdf3 Mon Sep 17 00:00:00 2001 From: bunny winter Date: Tue, 11 Feb 2025 18:27:13 -0600 Subject: [PATCH] initial commit --- .gitignore | 1 + Makefile | 21 ++++ cmd/root.go | 25 +++++ go.mod | 32 ++++++ go.sum | 55 ++++++++++ launcher/main.go | 275 +++++++++++++++++++++++++++++++++++++++++++++++ main.go | 9 ++ 7 files changed, 418 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 cmd/root.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 launcher/main.go create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e660fd9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +bin/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bd65d3c --- /dev/null +++ b/Makefile @@ -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 $@ . diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..9369108 --- /dev/null +++ b/cmd/root.go @@ -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) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3df5867 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b4221ff --- /dev/null +++ b/go.sum @@ -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= diff --git a/launcher/main.go b/launcher/main.go new file mode 100644 index 0000000..03267b7 --- /dev/null +++ b/launcher/main.go @@ -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 +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..fd765d7 --- /dev/null +++ b/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "git.cherry.town/town/cherry/cmd" +) + +func main() { + cmd.Execute() +}