From: christiangoeschel Date: Tue, 1 Oct 2024 03:55:30 +0000 (-0400) Subject: Initial commit X-Git-Url: https://git.christiangoeschel.com/?a=commitdiff_plain;h=a89d2e16ca305d99c9b83cf1956470e68f8e34d7;p=repoman-cli.git Initial commit --- a89d2e16ca305d99c9b83cf1956470e68f8e34d7 diff --git a/cli/main b/cli/main new file mode 100755 index 0000000..ccf0d0b Binary files /dev/null and b/cli/main differ diff --git a/cli/main.go b/cli/main.go new file mode 100644 index 0000000..8469ae3 --- /dev/null +++ b/cli/main.go @@ -0,0 +1,299 @@ +package main + +// An example demonstrating an application with multiple views. +// +// Note that this example was produced before the Bubbles progress component +// was available (github.com/charmbracelet/bubbles/progress) and thus, we're +// implementing a progress bar from scratch here. + +import ( + "fmt" + "net/http" + "io/ioutil" + // "time" + "encoding/json" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +const ( + dotChar = " • " + arrowRightChar = " → " + apiUrl = "https://repoman.christiangoeschel.com:8443" +) + +// General stuff for styling the view +var ( + spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("69")) + keywordStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("211")) + subtleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")) + checkboxStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#03A9F4")) + titleStyle = lipgloss.NewStyle().Background(lipgloss.Color("#448AFF")).Foreground(lipgloss.Color("#FFFFFF")) + statusOK = lipgloss.NewStyle().Foreground(lipgloss.Color("200")) + dotStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("236")).Render(dotChar) + mainStyle = lipgloss.NewStyle().Margin(10) + + + mainMenuOptions = []string{ + "List all repos", + "Create a new repository", + "Delete a repository", + "Manage API Keys", + "Check API Status", + "Settings", + "Quit", + } + + +) + +func main() { + initialModel := model{0, false, false, APIStatusMsg{} } + + p := tea.NewProgram(initialModel, tea.WithAltScreen()) + if _, err := p.Run(); err != nil { + fmt.Println("could not start program:", err) + } + +} + +type ( + frameMsg struct{} +) + + +type model struct { + Choice int + Chosen bool + Quitting bool + APIStatus APIStatusMsg +} + +func (m model) Init() tea.Cmd { + + return nil +} + +// Main update function. +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + + // Make sure these keys always quit + if msg, ok := msg.(tea.KeyMsg); ok { + key := msg.String() + + if key == "q" || key == "esc" || key == "ctrl+c" { + m.Quitting = true + return m, tea.Quit + } else if key == "backspace" { + m.Chosen = false + } + } + + // Hand off the message and model to the appropriate update function for the + // appropriate view based on the current state. + if !m.Chosen { + return updateMainMenu(msg, m) + } + + return updateChosen(msg, m) +} + +// The main view, which just calls the appropriate sub-view +func (m model) View() string { + var s string + if m.Quitting { + return "\n See you soon my friend! \n\n" + } + if !m.Chosen { + s = mainMenuView(m) + } else { + s = chosenView(m) + } + + return mainStyle.Render("\n" + s + "\n\n") +} + +// Sub-update functions + +// Update loop for the first view where you're choosing a task. +func updateMainMenu(msg tea.Msg, m model) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + var lastChoice int = len(mainMenuOptions) - 1 + + switch msg.String() { + + case "j", "down": + m.Choice++ + if m.Choice > lastChoice { + m.Choice = 0 + } + case "k", "up": + m.Choice-- + if m.Choice < 0 { + m.Choice = lastChoice + } + case "enter", " ": + m.Chosen = true + + if mainMenuOptions[m.Choice] == "Quit" { + return m, tea.Quit + } + + return m, nil + } + +} + + return m, nil +} + + +// Update loop for the second view after a choice has been made +func updateChosen(msg tea.Msg, m model) (tea.Model, tea.Cmd) { + + switch msg := msg.(type) { + + case tea.KeyMsg: + switch msg.String() { + case "enter": + if m.Choice == 4 { + m.APIStatus = APIStatusMsg{} + return m, handleAPIStatusRequest(apiUrl) + } + } + + case APIStatusMsg: + m.APIStatus = msg + + return m, nil + } + + return m, nil +} + +// Sub-views + +// The first view, where you're choosing a task +func mainMenuView(m model) string { + c := m.Choice + + tpl := fmt.Sprintf("%s\n\n", titleStyle.Render("[ +-+ Repoman CLI +-+ ]")) + tpl += "Main Menu\n\n" + tpl += "%s\n\n" + tpl += fmt.Sprintf("%s\n%s\n%s\n%s\n", subtleStyle.Render("j/k, up/down: select"), subtleStyle.Render("enter, spacebar: choose"), + subtleStyle.Render("q, esc: quit"), subtleStyle.Render("backspace: main menu")) + + var choices string + for i := 0;i < len(mainMenuOptions);i++{ + choices += fmt.Sprintf("%s\n", checkbox(mainMenuOptions[i], c == i)) + } + + return fmt.Sprintf(tpl, choices) +} + +// The second view, after a task has been chosen +func chosenView(m model) string { + var msg string + + switch m.Choice { + + case 0: + msg = fmt.Sprintf("%s\n\nCool, we'll need %s and %s...", + titleStyle.Render("[ +-+ " + mainMenuOptions[m.Choice] + " +-+ ]"), + keywordStyle.Render("libgarden"), + keywordStyle.Render("vegeutils")) + + case 1: + msg = fmt.Sprintf("%s\n\nOkay, then we should install %s and %s...", + titleStyle.Render("[ +-+ " + mainMenuOptions[m.Choice] + " +-+ ]"), + keywordStyle.Render("marketkit"), + keywordStyle.Render("libshopping")) + + case 2: + msg = fmt.Sprintf("%s\n\nOkay, cool, then we’ll need a library. Yes, an %s.", + titleStyle.Render("[ +-+ " + mainMenuOptions[m.Choice] + " +-+ ]"), + keywordStyle.Render("actual library")) + + case 3: + msg = fmt.Sprintf("%s\n\nOkay, cool, then we’ll need a library. Yes, an %s.", + titleStyle.Render("[ +-+ " + mainMenuOptions[m.Choice] + " +-+ ]"), + keywordStyle.Render("actual library")) + + case 4: + + statusResponse := fmt.Sprintf("Status:%s\nDescription:%s\nStart:%s\nExpected End:%s\n", + keywordStyle.Render(m.APIStatus.Status), + keywordStyle.Render(m.APIStatus.Description), + keywordStyle.Render(m.APIStatus.StartDateTime), + keywordStyle.Render(m.APIStatus.EstimatedEndTime)) + + msg = fmt.Sprintf("%s\n\nRequesting API Server Status from:\n%s\n\nResponse:\n\n%v\n\n%s", + titleStyle.Render("[ +-+ " + mainMenuOptions[m.Choice] + " +-+ ]"), + keywordStyle.Render(apiUrl), + statusResponse, + subtleStyle.Render("Press Enter to refresh")) + + case 5: + msg = fmt.Sprintf("%s\n\nOkay, cool, then we’ll need a library. Yes, an %s.", + titleStyle.Render("[ +-+ " + mainMenuOptions[m.Choice] + " +-+ ]"), + keywordStyle.Render("actual library")) + + default: + msg = fmt.Sprintf("Seems like you are lost.\n\nI'll help you find your way back ... simply press %s", + keywordStyle.Render("backspace")) + } + + + return msg + "\n\n" +} + +func checkbox(label string, checked bool) string { + if checked { + return checkboxStyle.Render("+ " + label) + } + return fmt.Sprintf("- %s", label) +} + +// Utils + +// API Status Handle +func handleAPIStatusRequest(url string) tea.Cmd { + + return func() tea.Msg { + url := fmt.Sprintf("%s/status", url) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + fmt.Print(err.Error()) + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + + } + defer res.Body.Close() + + body, readErr := ioutil.ReadAll(res.Body) + if readErr != nil { + fmt.Print(err.Error()) + } + + var apiStatus APIStatusMsg + if err := json.Unmarshal(body, &apiStatus); err != nil { // Parse []byte to the go struct pointer + fmt.Println("Can not unmarshal JSON") + } + + + return apiStatus + } +} + +type APIStatus struct { + Status string `json:"Status"` + Description string `json:"Description"` + StartDateTime string `json:"StartDateTime"` + EstimatedEndTime string `json:"EstimatedEndTime"` +} + +type APIStatusMsg APIStatus + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..afe36cd --- /dev/null +++ b/go.mod @@ -0,0 +1,27 @@ +module git.christiangoeschel.com/repoman-cli + +go 1.23.0 + +require ( + github.com/charmbracelet/bubbles v0.20.0 + github.com/charmbracelet/bubbletea v1.1.1 + github.com/charmbracelet/lipgloss v0.13.0 +) + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/x/ansi v0.2.3 // indirect + github.com/charmbracelet/x/term v0.2.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..daac317 --- /dev/null +++ b/go.sum @@ -0,0 +1,39 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.1.1 h1:KJ2/DnmpfqFtDNVTvYZ6zpPFL9iRCRr0qqKOCvppbPY= +github.com/charmbracelet/bubbletea v1.1.1/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= +github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=