package main import ( "fmt" "io/fs" "log" "os" "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 commandString string } 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"), commandString: ""}) } } // 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: "neomutt", commandString: "neomutt", }, { displayString: "bottom", commandString: "btm --group --battery --color gruvbox-light", }}, }, // 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() tea.Msg { return statusMsg("This is a test launch message.") } 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 } } 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 += "\nPress 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) } }