/* * 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" "strings" "git.src.quest/~skye/tamma/components/confirm" "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 ( stateTargetSelect state = iota stateActionSelect stateActionConfirm stateActionExec stateViewOutput ) type model struct { width int height int err string out string exec string confirmed bool 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 confirmDialog confirm.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 stateTargetSelect: i, ok := m.targetList.SelectedItem().(types.TargetItem) if ok { m.selectedTarget = i m.state = stateActionSelect } case stateActionSelect: a, ok := m.actionList.SelectedItem().(types.ActionItem) if ok { exec, err := a.ExecString(m.selectedTarget) if err != nil { m.err = err.Error() } else { m.exec = exec if a.Confirm { m.state = stateActionConfirm return m, nil } m.state = stateActionExec } } } case "q": switch m.state { case stateTargetSelect: return m, tea.Quit case stateActionSelect: if m.actionList.FilterState() != list.Filtering { m.actionList.ResetSelected() m.state = stateTargetSelect } case stateViewOutput: m.state = stateActionSelect } case "c": switch m.state { case stateActionSelect: 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 stateActionSelect: m.state = stateViewOutput } } case execFinishedMsg: m.out = string(msg.out.Bytes()) m.outputViewport.SetContent(m.out) if msg.err != nil { m.err = msg.err.Error() } case confirm.Result: if msg.Choice { m.state = stateActionExec } else { m.state = stateActionSelect } } switch m.state { case stateTargetSelect: var cmd tea.Cmd m.targetList, cmd = m.targetList.Update(msg) return m, cmd case stateActionSelect: var cmd tea.Cmd m.actionList, cmd = m.actionList.Update(msg) return m, cmd case stateActionConfirm: var cmd tea.Cmd m.confirmDialog, cmd = m.confirmDialog.Update(msg) return m, cmd case stateActionExec: m.state = stateActionSelect c := exec.Command("sh", "-c", m.exec) var out bytes.Buffer c.Stdout = &out return m, tea.ExecProcess(c, func(err error) tea.Msg { return execFinishedMsg{err, out} }) case stateViewOutput: var cmd tea.Cmd m.outputViewport, cmd = m.outputViewport.Update(msg) return m, cmd } return m, nil } func (m model) View() string { s := strings.Builder{} switch m.state { case stateTargetSelect: s.WriteString(m.targetList.View()) case stateActionSelect: s.WriteString(m.actionList.View()) s.WriteString("\n") selectedAction, ok := m.actionList.SelectedItem().(types.ActionItem) if ok { execString, err := selectedAction.ExecString(m.selectedTarget) if err != nil { s.WriteString("error in ExecString(): ") s.WriteString(err.Error()) } else { s.WriteString(styles.ActionExecString.Render(execString)) } } case stateActionConfirm: s.WriteString("Confirm that you want to run the following:\n\n") s.WriteString(styles.ActionExecString.Render(m.exec)) s.WriteString("\n\n") s.WriteString(m.confirmDialog.View()) case stateViewOutput: m.outputViewport.Height = m.height - 20 m.outputViewport.Width = m.width - 2 s.WriteString(m.outputViewport.View()) s.WriteString("\n\npager-like keys apply, plus the following:\n") s.WriteString(m.outputHelp.View(m.outputViewportKeys)) } if m.err != "" { s.WriteString("\n") s.WriteString(styles.Error.Render(m.err)) } return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, styles.TopLevel.Render(s.String())) } 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 = "Select action?" 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() confirmDialog := confirm.New() p := tea.NewProgram(model{ state: stateTargetSelect, targetList: targetList, actionList: actionList, actionListKeys: actionListKeys, outputViewport: outputViewport, outputViewportKeys: outputViewportKeys, outputHelp: outputHelp, confirmDialog: confirmDialog, }, tea.WithAltScreen()) if _, err := p.Run(); err != nil { fmt.Printf("An error occured while running: %v\n", err) os.Exit(1) } os.Exit(0) }