package main import ( "bytes" "fmt" "os" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" "periodic/prddeploy/commands" ) type appState int const ( stateList appState = iota stateForm stateOutput ) type cmdResultMsg struct { output string } type CommandItem struct { ItemTitle string `json:"title"` ItemDescription string `json:"description"` } func (i CommandItem) FilterValue() string { return i.ItemTitle } func (i CommandItem) Title() string { return i.ItemTitle } func (i CommandItem) Description() string { return i.ItemDescription } func (i CommandItem) String() string { return i.ItemTitle } type model struct { list list.Model form *huh.Form state appState choice int output string RemoteCommand string PrivateKeyFile string Username string Host string LocalFilepath string DestinationFilepath string CloudflareAPIToken string ZoneID string RecordName string RecordValue string } func (m model) DebugState() { fmt.Printf("REMOTE COMMAND: %s\n", m.RemoteCommand) fmt.Printf("PRIVATE KEY FILE: %s\n", m.PrivateKeyFile) fmt.Printf("USERNAME: %s\n", m.Username) fmt.Printf("HOST: %s\n", m.Host) fmt.Printf("LOCAL FILEPATH: %s\n", m.LocalFilepath) fmt.Printf("DESTINATION FILEPATH: %s\n", m.DestinationFilepath) } func (m *model) ReadForm() { m.RemoteCommand = m.form.GetString("remote_command") m.PrivateKeyFile = m.form.GetString("private_key_file") m.Username = m.form.GetString("username") m.Host = m.form.GetString("host") m.LocalFilepath = m.form.GetString("local_filepath") m.DestinationFilepath = m.form.GetString("destination_filepath") m.CloudflareAPIToken = m.form.GetString("cloudflare_api_token") m.ZoneID = m.form.GetString("zone_id") m.RecordName = m.form.GetString("record_name") m.RecordValue = m.form.GetString("record_value") } func newModel() model { items := []list.Item{ CommandItem{ItemTitle: "Run Command", ItemDescription: "Run a command on the remote host"}, CommandItem{ItemTitle: "Upload File", ItemDescription: "Upload a file to the remote host"}, CommandItem{ItemTitle: "SSL DNS Challenge", ItemDescription: "Set an _acme-challenge TXT record"}, } l := list.New(items, list.NewDefaultDelegate(), 80, 20) l.Title = "Commands" return model{list: l, state: stateList} } func (m model) Init() tea.Cmd { return nil } func execCommand(choice int, m model) tea.Cmd { return func() tea.Msg { var buf bytes.Buffer var err error if choice == 0 { err = commands.RunCommand(commands.RunCommandArgs{ RemoteCommand: m.RemoteCommand, PrivateKeyFile: m.PrivateKeyFile, Username: m.Username, Host: m.Host, }, &buf, &buf) } else if choice == 1 { err = commands.UploadFile(commands.UploadFileArgs{ LocalFilepath: m.LocalFilepath, DestinationFilepath: m.DestinationFilepath, PrivateKeyFile: m.PrivateKeyFile, Username: m.Username, Host: m.Host, }, &buf, &buf) } else { err = commands.SetAcmeRecord(commands.SetAcmeRecordArgs{ APIToken: m.CloudflareAPIToken, ZoneID: m.ZoneID, RecordName: m.RecordName, RecordValue: m.RecordValue, }, &buf, &buf) } if err != nil { fmt.Fprintf(&buf, "\nError: %v", err) } return cmdResultMsg{output: buf.String()} } } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch m.state { case stateList: switch msg := msg.(type) { case tea.WindowSizeMsg: m.list.SetSize(msg.Width, msg.Height) case tea.KeyMsg: if msg.String() == "enter" { m.choice = m.list.Index() m.state = stateForm m.SetForm() return m, m.form.Init() } } var cmd tea.Cmd m.list, cmd = m.list.Update(msg) return m, cmd case stateForm: form, cmd := m.form.Update(msg) if f, ok := form.(*huh.Form); ok { m.form = f } if m.form.State == huh.StateCompleted { m.ReadForm() choice := m.choice m.form = nil m.state = stateOutput return m, execCommand(choice, m) } if m.form.State == huh.StateAborted { m.state = stateList m.form = nil return m, nil } return m, cmd case stateOutput: if _, ok := msg.(tea.KeyMsg); ok { m.state = stateList return m, nil } } switch msg := msg.(type) { case cmdResultMsg: m.output = msg.output m.state = stateOutput } return m, nil } func (m model) View() string { switch m.state { case stateList: return m.list.View() case stateForm: return m.form.View() case stateOutput: return m.output + "\n\nPress any key to return..." } return "" } func main() { p := tea.NewProgram(newModel(), tea.WithAltScreen()) if _, err := p.Run(); err != nil { fmt.Fprintf(os.Stderr, "Error running program: %v\n", err) os.Exit(1) } }