/* * 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 ( "fmt" "os" "os/exec" "git.src.quest/~skye/tamma/styles" "git.src.quest/~skye/tamma/types" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "gopkg.in/yaml.v3" ) type state int const ( targetSelect state = iota actionSelect ) type model struct { width int height int err string state state targetList list.Model selectedTarget types.TargetItem actionList list.Model selectedAction types.ActionItem actionListKeys *types.ActionListKeyMap } type execFinishedMsg struct{ err error } 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) return m, tea.ExecProcess(c, func(err error) tea.Msg { return execFinishedMsg{err} }) } } } 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 "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 execFinishedMsg: 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 } 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() } } 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, } } actionList.AdditionalFullHelpKeys = func() []key.Binding { return []key.Binding{ actionListKeys.Back, actionListKeys.CopyCommand, } } actionList.Styles.Title = styles.ListTitle actionList.Styles.PaginationStyle = styles.ListPaginaton p := tea.NewProgram(model{ state: targetSelect, targetList: targetList, actionList: actionList, actionListKeys: actionListKeys, }, tea.WithAltScreen()) if _, err := p.Run(); err != nil { fmt.Printf("An error occured while running: %v\n", err) os.Exit(1) } os.Exit(0) }