/* * Copyright (C) 2025 Jonni Liljamo * * This file is licensed under GPL-3.0-only, see NOTICE and LICENSE for * more information. */ // nolint package main import ( "bytes" "fmt" "os" "os/exec" "git.src.quest/~skye/tamma/styles" "git.src.quest/~skye/tamma/types" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "gopkg.in/yaml.v3" ) type state int const ( targetSelect state = iota actionSelect viewOutput ) type model struct { width int height int err string out string state state targetList list.Model selectedTarget types.TargetItem actionList list.Model selectedAction types.ActionItem actionListKeys *types.ActionListKeyMap outputViewport viewport.Model outputViewportKeys *types.OutputViewportKeyMap outputHelp help.Model } type execFinishedMsg struct { err error out bytes.Buffer } func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height return m, nil case tea.KeyMsg: switch msg.String() { case "ctrl+c": return m, tea.Quit case "enter": switch m.state { case targetSelect: i, ok := m.targetList.SelectedItem().(types.TargetItem) if ok { m.selectedTarget = i m.state = actionSelect } case actionSelect: a, ok := m.actionList.SelectedItem().(types.ActionItem) if ok { execString, err := a.ExecString(m.selectedTarget) if err != nil { m.err = err.Error() } else { c := exec.Command("sh", "-c", execString) var out bytes.Buffer c.Stdout = &out return m, tea.ExecProcess(c, func(err error) tea.Msg { return execFinishedMsg{err, out} }) } } } case "q": switch m.state { case targetSelect: return m, tea.Quit case actionSelect: if m.actionList.FilterState() != list.Filtering { m.actionList.ResetSelected() m.state = targetSelect } case viewOutput: m.state = actionSelect } case "c": switch m.state { case actionSelect: if m.actionList.FilterState() != list.Filtering { if os.Getenv("WAYLAND_DISPLAY") != "" { a, ok := m.actionList.SelectedItem().(types.ActionItem) if ok { execString, err := a.ExecString(m.selectedTarget) if err != nil { m.err = err.Error() } else { err := exec.Command("wl-copy", execString).Run() if err != nil { m.err = err.Error() } } } } } } case "o": switch m.state { case actionSelect: m.state = viewOutput } } case execFinishedMsg: m.out = string(msg.out.Bytes()) m.outputViewport.SetContent(m.out) if msg.err != nil { m.err = msg.err.Error() } } switch m.state { case targetSelect: var cmd tea.Cmd m.targetList, cmd = m.targetList.Update(msg) return m, cmd case actionSelect: var cmd tea.Cmd m.actionList, cmd = m.actionList.Update(msg) return m, cmd case viewOutput: var cmd tea.Cmd m.outputViewport, cmd = m.outputViewport.Update(msg) return m, cmd } return m, nil } func (m model) View() string { tlw := lipgloss.Width(styles.TopLevel.Render("")) - 2 msg := "\n" switch m.state { case targetSelect: msg += m.targetList.View() msg += "\n" case actionSelect: alb := m.actionList.View() //alw := lipgloss.Width(alb) msg += alb msg += "\n" // NOTE: type asserting here will return nil if we've filtered the list to // have no results, thus this check is needed. selectedAction, ok := m.actionList.SelectedItem().(types.ActionItem) if ok { execString, err := selectedAction.ExecString(m.selectedTarget) if err != nil { msg += fmt.Sprintf("error in ExecString(): %s\n", err.Error()) } msg += styles.ActionExecString.Width(tlw).SetString(execString).String() } case viewOutput: m.outputViewport.Height = m.height - 20 m.outputViewport.Width = m.width - 2 msg += m.outputViewport.View() msg += "\n\npager-like keys apply, plus the following:\n" msg += m.outputHelp.View(m.outputViewportKeys) msg += "\n" } if m.err != "" { msg += m.err } msg = styles.TopLevel.Render(msg) return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, msg) } type config struct { Targets []types.TargetItem `yaml:"targets"` Actions []types.ActionItem `yaml:"actions"` } func main() { config := config{} configContent, err := os.ReadFile("./tamma.yaml") if err != nil { fmt.Printf("failed to read config file: %s\n", err.Error()) os.Exit(1) } err = yaml.Unmarshal(configContent, &config) if err != nil { fmt.Printf("failed to unmarshal config file: %s\n", err.Error()) os.Exit(1) } targetListItems := []list.Item{} for _, i := range config.Targets { targetListItems = append(targetListItems, i) } targetListH := min(len(targetListItems), 10) targetListKeys := types.NewTargetListKeyMap() targetList := list.New(targetListItems, types.TargetItemDelegate{}, 48, targetListH+4) targetList.Title = "Select target:" targetList.SetShowHelp(true) targetList.SetFilteringEnabled(true) targetList.SetShowStatusBar(false) targetList.DisableQuitKeybindings() targetList.AdditionalShortHelpKeys = func() []key.Binding { return []key.Binding{ targetListKeys.Quit, } } targetList.AdditionalFullHelpKeys = func() []key.Binding { return []key.Binding{ targetListKeys.Quit, } } targetList.Styles.Title = styles.ListTitle targetList.Styles.PaginationStyle = styles.ListPaginaton actionListItems := []list.Item{} for _, i := range config.Actions { if i.A == nil { i.A = map[string]any{} } actionListItems = append(actionListItems, i) } actionListH := min(len(actionListItems), 10) actionListKeys := types.NewActionListKeyMap() actionList := list.New(actionListItems, types.ActionItemDelegate{}, 48, actionListH+4) actionList.Title = "What shall we do today?" actionList.SetShowHelp(true) actionList.SetFilteringEnabled(true) actionList.SetShowStatusBar(false) actionList.DisableQuitKeybindings() actionList.AdditionalShortHelpKeys = func() []key.Binding { return []key.Binding{ actionListKeys.Back, actionListKeys.CopyCommand, actionListKeys.ViewOutput, } } actionList.AdditionalFullHelpKeys = func() []key.Binding { return []key.Binding{ actionListKeys.Back, actionListKeys.CopyCommand, actionListKeys.ViewOutput, } } actionList.Styles.Title = styles.ListTitle actionList.Styles.PaginationStyle = styles.ListPaginaton outputViewport := viewport.New(20, 20) outputViewport.SetHorizontalStep(6) // 6 will be default in bubbles v2 outputViewport.Style = styles.ViewportStyle outputViewportKeys := types.NewOutputViewportKeyMap() outputHelp := help.New() p := tea.NewProgram(model{ state: targetSelect, targetList: targetList, actionList: actionList, actionListKeys: actionListKeys, outputViewport: outputViewport, outputViewportKeys: outputViewportKeys, outputHelp: outputHelp, }, tea.WithAltScreen()) if _, err := p.Run(); err != nil { fmt.Printf("An error occured while running: %v\n", err) os.Exit(1) } os.Exit(0) }