package main import ( "fmt" "io/fs" "log" "os" "os/exec" "strings" tea "github.com/charmbracelet/bubbletea" ) // Enum for the different launch types (browsers and CLI's) const ( BrowserSessions int = iota Commands ) type sessionOrCommand struct { displayString string command *exec.Cmd } type Choices [][]sessionOrCommand type SelectionSet map[int]struct{} type model struct { activeSection int cursor []int choices Choices selected []SelectionSet status string } type statusMsg string var browserSelection SelectionSet var commandSelection SelectionSet func getBrowserSessions() []sessionOrCommand { // 1. List files in $XDG_DATA_HOME/qutebrowser/sessions/ (N.B.: // UserConfigDir() in os) userConfigDir, err := os.UserConfigDir() if err != nil { log.Printf("Error finding user configuration directory: %v", err) return []sessionOrCommand{} } log.Printf("INFO userConfigDir: %+v", userConfigDir) fileSystem := os.DirFS(userConfigDir) log.Printf("INFO fileSystem: %+v", fileSystem) fileList, err := fs.ReadDir(fileSystem, "local/share/qutebrowser/sessions") if err != nil { log.Printf("Error reading browser sessions directory: %v", err) return []sessionOrCommand{} } // 2. Exclude non-YAML files // 3. Wrangle them into this struct array result := make([]sessionOrCommand, 0) for _, entry := range fileList { if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".yml") { log.Printf("INFO %s", entry.Name()) result = append(result, sessionOrCommand{ displayString: strings.TrimSuffix(entry.Name(), ".yml"), command: exec.Command("qutebrowser", "--restore", strings.TrimSuffix(entry.Name(), ".yml")), }) } } // log.Printf("result: %v", result) return result } func initialModel() model { browserSelection = make(SelectionSet) commandSelection = make(SelectionSet) return model{ activeSection: BrowserSessions, cursor: []int{0, 0}, choices: Choices{ getBrowserSessions(), []sessionOrCommand{{ displayString: "bottom", command: exec.Command("xterm", "-maximized", "-e", "btm", "--group", "--battery", "--color", "gruvbox-light"), }, { displayString: "broot", command: exec.Command("xterm", "-maximized", "-e", "broot"), }, { displayString: "joplin", command: exec.Command("xterm", "-maximized", "-e", "joplin"), }, { displayString: "neomutt", command: exec.Command("xterm", "-maximized", "-e", "neomutt"), }, { displayString: "newsboat", command: exec.Command("xterm", "-maximized", "-e", "newsboat"), }}, }, // An array of maps which indicates which choices are selected. // We're using the map like a mathematical set. The keys refer to // the indexes of the `choices` slice, above. selected: []SelectionSet{browserSelection, commandSelection}, } } func launch(m model) tea.Cmd { return func() tea.Msg { var result statusMsg for p := 0; p < 2; p++ { for q, item := range m.choices[p] { if _, ok := m.selected[p][q]; ok { log.Printf("INFO launching: %v\n", item.displayString) result += statusMsg(fmt.Sprintf("Launching command: %v\n", item.displayString)) err := item.command.Start() if err != nil { result += statusMsg(fmt.Sprintf("%v\n", err)) log.Fatalf("Error launching: %v\n", err) } } } } return result } } func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case statusMsg: m.status = string(msg) return m, tea.Quit case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q": return m, tea.Quit case "tab": if m.activeSection == BrowserSessions { m.activeSection = Commands } else { m.activeSection = BrowserSessions } case "up", "k": if m.cursor[m.activeSection] > 0 { m.cursor[m.activeSection]-- } case "down", "j": if m.cursor[m.activeSection] < len(m.choices[m.activeSection])-1 { m.cursor[m.activeSection]++ } case " ": _, ok := m.selected[m.activeSection][m.cursor[m.activeSection]] if ok { delete(m.selected[m.activeSection], m.cursor[m.activeSection]) } else { m.selected[m.activeSection][m.cursor[m.activeSection]] = struct{}{} } case "enter": return m, launch(m) } } return m, nil } func (m model) View() string { s := "Let's get started!\n\n" for j := 0; j < 2; j++ { for i, choice := range m.choices[j] { cursor := " " if m.cursor[j] == i { cursor = ">" } checked := " " if _, ok := m.selected[j][i]; ok { checked = "x" } s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice.displayString) } s += "\n\n" } // s += fmt.Sprintf("\n%+v", m.selected) // debug s += fmt.Sprintf("%s\n", m.status) s += "Press enter to launch.\n" s += "Press q to quit.\n" return s } func main() { p := tea.NewProgram(initialModel()) if err := p.Start(); err != nil { fmt.Printf("Alas, there's been an error: %v", err) os.Exit(1) } }