From: christiangoeschel Date: Thu, 3 Oct 2024 01:38:14 +0000 (-0400) Subject: Major changes X-Git-Url: https://git.christiangoeschel.com/?a=commitdiff_plain;h=db6d8cb46a3073d2a1e197849f07ed5173719d7a;p=repoman-cli.git Major changes --- diff --git a/go.mod b/go.mod index 673c02a..4541cde 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( ) require ( + github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/bubbles v0.20.0 // indirect github.com/charmbracelet/x/ansi v0.2.3 // indirect @@ -20,18 +21,20 @@ require ( 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/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect github.com/repoman-cli/api v0.0.0-00010101000000-000000000000 // indirect - github.com/repoman-cli/themes v0.0.0-00010101000000-000000000000 // indirect + github.com/repoman-cli/ui v0.0.0-00010101000000-000000000000 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/sahilm/fuzzy v0.1.1 // 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 + golang.org/x/term v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect ) replace github.com/repoman-cli/lib => /home/cgoesche/repoman-cli/lib -replace github.com/repoman-cli/themes => /home/cgoesche/repoman-cli/themes +replace github.com/repoman-cli/ui => /home/cgoesche/repoman-cli/ui replace github.com/repoman-cli/screens => /home/cgoesche/repoman-cli/screens diff --git a/go.sum b/go.sum index daac317..7e6a916 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 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= @@ -24,16 +28,20 @@ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D 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/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= 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= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= 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= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= diff --git a/main b/main index 3bb7e68..4f16249 100755 Binary files a/main and b/main differ diff --git a/screens/apistatus.go b/screens/apistatus.go index daad6a2..156bea3 100644 --- a/screens/apistatus.go +++ b/screens/apistatus.go @@ -3,8 +3,10 @@ package screens import ( "fmt" + "os" tea "github.com/charmbracelet/bubbletea" - "github.com/repoman-cli/themes" + "github.com/charmbracelet/lipgloss" + "github.com/repoman-cli/ui" "github.com/charmbracelet/bubbles/spinner" "github.com/repoman-cli/api" ) @@ -12,32 +14,37 @@ import ( type APIStatusScreenModel struct { APIStatus api.APIStatusMsg - URL string Err error - StatusCode int Spinner spinner.Model ReqReceived bool Requested bool + Width int + Height int //PrevScreen tea.Model } -// --- AltScreenModel functions - // Creates a new mainMenuScreenModel func NewAPIStatusScreen() APIStatusScreenModel { + // Get terminal size + w, h, err := ui.GetWindowSize() + if err != nil { + fmt.Printf("Could not get terminal window size.") + os.Exit(1) + } + s := spinner.New() s.Spinner = spinner.Pulse - s.Style = themes.SpinnerStyle + s.Style = ui.SpinnerStyle return APIStatusScreenModel { APIStatus: api.APIStatusMsg{}, - URL: "https://repoman.christiangoeschel.com:8443/status", Err: nil, - StatusCode: 200, Spinner: s, ReqReceived: false, Requested: false, + Width: w, + Height: h, } } @@ -50,17 +57,21 @@ func (m *APIStatusScreenModel) Init() tea.Cmd { func (m *APIStatusScreenModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { + case tea.KeyMsg: switch msg.String() { - case "q", "backspace": + case "q", "Q", "backspace": mainMenu := MainMenu() return RootScreen().SwitchScreen(&mainMenu) + case "ctrl+c", "ctrl+C": + return m, tea.Quit + case "enter": - m.APIStatus = api.APIStatusMsg{} - m.ReqReceived = false - m.Requested = true - return m, api.HandleAPIStatusRequest(api.StatusUrl) + m.APIStatus = api.APIStatusMsg{} + m.ReqReceived = false + m.Requested = true + return m, api.HandleAPIStatusRequest(api.StatusUrl) } @@ -71,13 +82,16 @@ func (m *APIStatusScreenModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case spinner.TickMsg: - - //if m.ReqReceived == false && m.Requested == true { var cmd tea.Cmd m.Spinner, cmd = m.Spinner.Update(msg) return m, cmd - //} - + + case tea.WindowSizeMsg: + w, h, _ := ui.GetWindowSize() + m.Width = w + m.Height = h + return m, nil + } return m, nil @@ -85,34 +99,50 @@ func (m *APIStatusScreenModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // View func (m *APIStatusScreenModel) View() string { - - msg := themes.KeywordStyle.Render("API Status") - - statusResponse := fmt.Sprintf("Status: %s\nDescription: %s\nStart: %s\nExpected End: %s\n", - themes.KeywordStyle.Render(m.APIStatus.Status), - themes.KeywordStyle.Render(m.APIStatus.Description), - themes.KeywordStyle.Render(m.APIStatus.StartDateTime), - themes.KeywordStyle.Render(m.APIStatus.EstimatedEndTime)) - - msg = fmt.Sprintf("%s\n\nRequesting API Server Status from:\n\n", - themes.TitleStyle.Render("[ +-+ API Server Status +-+ ]")) - - msg += fmt.Sprintf("%s\n\n%s\n%s\n", - themes.KeywordStyle.Render(api.StatusUrl), - statusResponse, - themes.SubtleStyle.Render("Press Enter to refresh")) - - msg += "\n%s" - - - switch m.ReqReceived { - case false: - if m.Requested == true { - return themes.MainStyle.Render(fmt.Sprintf(msg, m.Spinner.View() + " Fetching ...")) - } + + // Top bar styling + roundLabelCorner := ui.RoundLabelCornerStyle.Render("") + labelArrow := ui.LabelArrowStyle.Render("") + titleBlock := ui.TitleStyle.Render(" Repoman CLI ") + rightTopBarBlock := ui.TopBarRightLabel.Render(" API Status Check ") + + if !m.ReqReceived && m.Requested { + rightTopBarBlock = ui.TopBarRightLabel.Render( " Fetching ... " + m.Spinner.View()) } + + topBarBlock := ui.TopBarStyle.Render(lipgloss.JoinHorizontal(lipgloss.Center, + ui.TopBarLeftStyle.Width((m.Width / 2 ) - 1).Render(roundLabelCorner + titleBlock + labelArrow), + ui.TopBarRightStyle.Width((m.Width / 2 ) - 1).Render(rightTopBarBlock))) + + _, topBarBlockHeight := lipgloss.Size(topBarBlock) + + // Footer + helpBlock := fmt.Sprintf("%s\n%s\n", + ui.SubtleStyle.Render("Q, Backspace - Return to Main menu"), + ui.SubtleStyle.Render("Ctrl+C - Quit")) + + footerBlock := ui.FooterStyle.Render(lipgloss.JoinHorizontal(lipgloss.Center, + ui.FooterLeftStyle.Width((m.Width / 2 ) - 1).Render("Placeholder"), + ui.FooterRightStyle.Width((m.Width / 2 ) - 1 ).Render(helpBlock))) - return themes.MainStyle.Render(fmt.Sprintf(msg, "")) + _, footerBlockHeight := lipgloss.Size(footerBlock) + + // Main Body + statusResponse := fmt.Sprintf("Status: %s\nDescription: %s\nStart: %s\nExpected End: %s", + ui.KeywordStyle.Render(m.APIStatus.Status), + ui.KeywordStyle.Render(m.APIStatus.Description), + ui.KeywordStyle.Render(m.APIStatus.StartDateTime), + ui.KeywordStyle.Render(m.APIStatus.EstimatedEndTime)) + + mainBodyBlock := fmt.Sprintf("Requesting API Server Status from:\n\n%s\n\n%s", + ui.URLStyle.Render(api.StatusUrl), + statusResponse) + + mainBodyBlockStyle := lipgloss.NewStyle().Height(m.Height - footerBlockHeight - topBarBlockHeight) + mainBodyBlock = mainBodyBlockStyle.Render(mainBodyBlock) + + +return lipgloss.PlaceHorizontal(m.Width, lipgloss.Center, lipgloss.JoinVertical(lipgloss.Left, topBarBlock, mainBodyBlock, footerBlock)) } diff --git a/screens/go.mod b/screens/go.mod index b6b930d..0604bed 100644 --- a/screens/go.mod +++ b/screens/go.mod @@ -1,32 +1,3 @@ module git.christiangoeschel.com/repoman-cli/screens go 1.23.0 - -require ( - github.com/charmbracelet/bubbles v0.20.0 - github.com/charmbracelet/bubbletea v1.1.1 - github.com/repoman-cli/themes v0.0.0-00010101000000-000000000000 -) - -require ( - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/lipgloss v0.13.0 // 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 -) - -replace github.com/repoman-cli/themes => /home/cgoesche/repoman-cli/themes - -replace github.com/repoman-cli/lib => /home/cgoesche/repoman-cli/lib diff --git a/screens/go.sum b/screens/go.sum deleted file mode 100644 index daac317..0000000 --- a/screens/go.sum +++ /dev/null @@ -1,39 +0,0 @@ -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= diff --git a/screens/repolist.go b/screens/repolist.go new file mode 100644 index 0000000..2ed9669 --- /dev/null +++ b/screens/repolist.go @@ -0,0 +1,208 @@ +package screens + +import ( + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textarea" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "golang.org/x/term" + "os" +) + +const ( + InitialInputs = 2 + MaxInputs = 16 + MinInputs = 1 + HelpHeight = 5 +) + +var ( + cursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("212")) + + cursorLineStyle = lipgloss.NewStyle(). + Background(lipgloss.Color("57")). + Foreground(lipgloss.Color("230")) + + placeholderStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("238")) + + endOfBufferStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("235")) + + focusedPlaceholderStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("99")) + + focusedBorderStyle = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("238")) + + blurredBorderStyle = lipgloss.NewStyle(). + Border(lipgloss.HiddenBorder()) +) + +type Keymap = struct { + next, prev, add, remove, quit key.Binding +} + +func newTextarea() textarea.Model { + t := textarea.New() + t.Prompt = "" + t.Placeholder = "Type something" + t.ShowLineNumbers = true + t.Cursor.Style = cursorStyle + t.FocusedStyle.Placeholder = focusedPlaceholderStyle + t.BlurredStyle.Placeholder = placeholderStyle + t.FocusedStyle.CursorLine = cursorLineStyle + t.FocusedStyle.Base = focusedBorderStyle + t.BlurredStyle.Base = blurredBorderStyle + t.FocusedStyle.EndOfBuffer = endOfBufferStyle + t.BlurredStyle.EndOfBuffer = endOfBufferStyle + t.KeyMap.DeleteWordBackward.SetEnabled(false) + t.KeyMap.LineNext = key.NewBinding(key.WithKeys("down")) + t.KeyMap.LinePrevious = key.NewBinding(key.WithKeys("up")) + t.Blur() + return t +} + +type ListReposScreenModel struct { + Width int + Height int + Keymap Keymap + Help help.Model + Inputs []textarea.Model + Focus int + Err error +} + +func NewListRepoScreenModel() ListReposScreenModel { + // Get terminal size + fd := int(os.Stdin.Fd()) // Terminal's file descriptor + // Size + w, h, _ := term.GetSize(fd) + + m := ListReposScreenModel { + Inputs: make([]textarea.Model, InitialInputs), + Help: help.New(), + Keymap: Keymap{ + next: key.NewBinding( + key.WithKeys("tab"), + key.WithHelp("tab", "next"), + ), + prev: key.NewBinding( + key.WithKeys("shift+tab"), + key.WithHelp("shift+tab", "prev"), + ), + add: key.NewBinding( + key.WithKeys("ctrl+n"), + key.WithHelp("ctrl+n", "add an editor"), + ), + remove: key.NewBinding( + key.WithKeys("ctrl+w"), + key.WithHelp("ctrl+w", "remove an editor"), + ), + quit: key.NewBinding( + key.WithKeys("esc", "ctrl+c"), + key.WithHelp("esc", "quit"), + ), + }, + Width: w, + Height: h, + } + for i := 0; i < InitialInputs; i++ { + m.Inputs[i] = newTextarea() + } + m.Inputs[m.Focus].Focus() + m.updateKeybindings() + return m +} + + +func (m *ListReposScreenModel ) Init() tea.Cmd { + return textarea.Blink +} + +func (m *ListReposScreenModel ) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, m.Keymap.quit): + for i := range m.Inputs { + m.Inputs[i].Blur() + } + return m, tea.Quit + case key.Matches(msg, m.Keymap.next): + m.Inputs[m.Focus].Blur() + m.Focus++ + if m.Focus > len(m.Inputs)-1 { + m.Focus = 0 + } + cmd := m.Inputs[m.Focus].Focus() + cmds = append(cmds, cmd) + case key.Matches(msg, m.Keymap.prev): + m.Inputs[m.Focus].Blur() + m.Focus-- + if m.Focus < 0 { + m.Focus = len(m.Inputs) - 1 + } + cmd := m.Inputs[m.Focus].Focus() + cmds = append(cmds, cmd) + case key.Matches(msg, m.Keymap.add): + m.Inputs = append(m.Inputs, newTextarea()) + case key.Matches(msg, m.Keymap.remove): + m.Inputs = m.Inputs[:len(m.Inputs)-1] + if m.Focus > len(m.Inputs)-1 { + m.Focus = len(m.Inputs) - 1 + } + } + case tea.WindowSizeMsg: + m.Height = msg.Height + m.Width = msg.Width + } + + m.updateKeybindings() + m.sizeInputs() + + // Update all textareas + for i := range m.Inputs { + newModel, cmd := m.Inputs[i].Update(msg) + m.Inputs[i] = newModel + cmds = append(cmds, cmd) + } + + return m, tea.Batch(cmds...) +} + +func (m *ListReposScreenModel ) sizeInputs() { + for i := range m.Inputs { + m.Inputs[i].SetWidth(m.Width / len(m.Inputs)) + m.Inputs[i].SetHeight(m.Height - HelpHeight) + } +} + +func (m *ListReposScreenModel ) updateKeybindings() { + m.Keymap.add.SetEnabled(len(m.Inputs) < MaxInputs) + m.Keymap.remove.SetEnabled(len(m.Inputs) > MinInputs) +} + +func (m *ListReposScreenModel ) View() string { + help := m.Help.ShortHelpView([]key.Binding{ + m.Keymap.next, + m.Keymap.prev, + m.Keymap.add, + m.Keymap.remove, + m.Keymap.quit, + }) + + var views []string + for i := range m.Inputs { + views = append(views, m.Inputs[i].View()) + } + + return lipgloss.JoinHorizontal(lipgloss.Top, views...) + "\n\n" + help +} + + diff --git a/screens/root.go b/screens/root.go index d108578..4cb8dd8 100644 --- a/screens/root.go +++ b/screens/root.go @@ -2,10 +2,11 @@ package screens import ( "fmt" - //"github.com/repoman-cli/lib" - "github.com/repoman-cli/themes" + "os" + "github.com/repoman-cli/ui" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" ) @@ -14,23 +15,30 @@ import ( model tea.Model // Contains the current screen model which represents the programs current state } +type menuOptions struct { + title string + desc string +} //Main Menu Screen type mainMenuScreenModel struct { Title string Message string - OptionChoice int - } - -var mainMenuOptions = []string{ - "List all repos", - "Create a new repository", - "Delete a repository", - "Manage API Keys", - "Check API Status", - "Settings", - "Quit", + OptionChoice int + Width int + Height int + MenuOptions []menuOptions } +var menuOpts = []menuOptions{ + menuOptions{title: "List all repos", desc: "View detailed repository information"}, + menuOptions{title: "Create new repo", desc: "Nothing better than creating apps"}, + menuOptions{title: "Delete a repo", desc: "Archive it first ;)"}, + menuOptions{title: "Manage API keys", desc: "Why don't you open source your work ?"}, + menuOptions{title: "Check API Status", desc: "Don't worry, everything is fine ( ' o _ o )"}, + menuOptions{title: "Settings", desc: "You don't like the default me ? :("}, + menuOptions{title: "Repoman Shell", desc: "Ohhh you will like this one for sure :D"}, + menuOptions{title: "Quit", desc: "You are seeing someone else, admit it !!"}, +} // Initialize model func (m rootScreenModel) Init() tea.Cmd { @@ -53,12 +61,11 @@ func (m rootScreenModel) View() string { func RootScreen() rootScreenModel { var rootModel tea.Model - // Set the rootModel (current state) to be the mainMenuScreen mainMenu := MainMenu() rootModel = &mainMenu - _ = themes.ChangeTheme(themes.DefaultTheme) + _ = ui.ChangeTheme(ui.DefaultTheme) return rootScreenModel { model: rootModel, @@ -77,9 +84,19 @@ func (m rootScreenModel) SwitchScreen(model tea.Model) (tea.Model, tea.Cmd) { // Creates a new mainMenuScreenModel func MainMenu() mainMenuScreenModel { + // Get terminal size + w, h, err := ui.GetWindowSize() + if err != nil { + fmt.Printf("Could not get terminal window size.") + os.Exit(1) + } + return mainMenuScreenModel { Title: "Repoman CLI", Message: "Hello my friend", + Width: w, + Height: h, + MenuOptions: menuOpts, } } @@ -93,17 +110,20 @@ func (m *mainMenuScreenModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: - var lastChoice int = len(mainMenuOptions) - 1 + var lastChoice int = len(m.MenuOptions) - 1 switch msg.String() { + + case "q", "Q", "esc": + return m, tea.Quit - case "j", "down": + case "down": m.OptionChoice++ if m.OptionChoice > lastChoice { m.OptionChoice = 0 } - case "k", "up": + case "up": m.OptionChoice-- if m.OptionChoice < 0 { m.OptionChoice = lastChoice @@ -111,17 +131,29 @@ func (m *mainMenuScreenModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "enter", " ": - if mainMenuOptions[m.OptionChoice] == "Check API Status" { - apiStatusScreen := NewAPIStatusScreen() - return RootScreen().SwitchScreen(&apiStatusScreen) - } else if mainMenuOptions[m.OptionChoice] == "Quit" { + switch m.OptionChoice { + case 0: + listReposScreen := NewListRepoScreenModel() + return RootScreen().SwitchScreen(&listReposScreen) + + case 4: + apiStatusScreen := NewAPIStatusScreen() + return RootScreen().SwitchScreen(&apiStatusScreen) + + case 6: return m, tea.Quit - } + + } return m, nil } + + case tea.WindowSizeMsg: + w, h, _ := ui.GetWindowSize() + m.Width = w + m.Height = h - return m, nil + return m, nil } return m, nil @@ -129,30 +161,53 @@ func (m *mainMenuScreenModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // View func (m *mainMenuScreenModel) View() string { - + c := m.OptionChoice - - tpl := fmt.Sprintf("%s\n\n", themes.TitleStyle.Render("[ +-+ Repoman CLI +-+ ]")) - tpl += "Main Menu\n\n" - tpl += "%s\n\n" - tpl += fmt.Sprintf("%s\n%s\n%s\n%s\n", - themes.SubtleStyle.Render("j/k, up/down: select"), - themes.SubtleStyle.Render("enter, spacebar: choose"), - themes.SubtleStyle.Render("q, esc: quit"), - themes.SubtleStyle.Render("backspace: main menu")) - var opts string - for i := 0;i < len(mainMenuOptions);i++{ - opts += fmt.Sprintf("%s\n", checkbox(mainMenuOptions[i], c == i)) - } + // Top bar styling + + roundLabelCorner := ui.RoundLabelCornerStyle.Render("") + labelArrow := ui.LabelArrowStyle.Render("") + titleBlock := ui.TitleStyle.Render(" Repoman CLI ") + rightTopBarBlock := ui.TopBarRightLabel.Render(" " + m.MenuOptions[m.OptionChoice].title + " ") + + topBarBlock := ui.TopBarStyle.Render(lipgloss.JoinHorizontal(lipgloss.Center, + ui.TopBarLeftStyle.Width((m.Width / 2 ) - 1).Render(roundLabelCorner + titleBlock + labelArrow), + ui.TopBarRightStyle.Width((m.Width / 2 ) - 1).Render(rightTopBarBlock))) + + _, topBarBlockHeight := lipgloss.Size(topBarBlock) - return themes.MainStyle.Render(fmt.Sprintf(tpl, opts)) + // Footer styling + copyrightBlock := ui.SubtleStyle.Render("2024 Copyright - Repoman") + + helpBlock := fmt.Sprintf("%s\n%s\n%s", + ui.SubtleStyle.Render(" - Select"), + ui.SubtleStyle.Render("Enter, Spacebar - Choose"), + ui.SubtleStyle.Render("Q, Esc - Quit")) + + footerBlock := ui.FooterStyle.Render(lipgloss.JoinHorizontal(lipgloss.Center, + ui.FooterLeftStyle.Width((m.Width / 2 ) - 1).Render(copyrightBlock), + ui.FooterRightStyle.Width((m.Width / 2 ) - 1).Render(helpBlock))) + _, footerBlockHeight := lipgloss.Size(footerBlock) + + // Menu options + var mainMenuBlock string + + for i := 0; i < len(menuOpts); i++ { + title, desc := checkbox(m.MenuOptions[i].title, m.MenuOptions[i].desc, c == i) + mainMenuBlock += fmt.Sprintf("%s\n%s\n\n", title, desc ) + } + + mainMenuBlockStyle := lipgloss.NewStyle().Height(m.Height - footerBlockHeight - topBarBlockHeight) + mainMenuBlock = mainMenuBlockStyle.Render(mainMenuBlock) + +return lipgloss.PlaceHorizontal(m.Width, lipgloss.Center, lipgloss.JoinVertical(lipgloss.Left, topBarBlock, mainMenuBlock, footerBlock)) } -func checkbox(label string, checked bool) string { +func checkbox(title string, desc string, checked bool) (string, string) { if checked { - return themes.CheckboxStyle.Render("+ " + label) + return ui.CheckboxStyle.Render(title) , ui.CheckboxStyle.Render(desc) } - return fmt.Sprintf("- %s", label) + return ui.MenuOptionTitleStyle.Render(title), ui.MenuOptionDescStyle.Render(desc) } diff --git a/themes/default-theme.go b/themes/default-theme.go deleted file mode 100644 index 6a31224..0000000 --- a/themes/default-theme.go +++ /dev/null @@ -1,42 +0,0 @@ -package themes - -import ( - - "github.com/charmbracelet/lipgloss" -) - -var ( - DefaultTheme = 0 - CurrentTheme = 0 - - 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")) - MainStyle = lipgloss.NewStyle().MarginLeft(10).MarginTop(20) - Altstyle = lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()).BorderForeground(lipgloss.Color("63")) -) - -func ChangeTheme(themeID int) (err error) { - - switch themeID { - case 0: - KeywordStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("200")) - return nil - - case 1: - KeywordStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("190")) - return nil - } - - return nil -} - -func SetTheme(themeID int) (err error) { - DefaultTheme = themeID - _ = ChangeTheme(DefaultTheme) - - return nil -} diff --git a/themes/go.mod b/themes/go.mod deleted file mode 100644 index 48db9e8..0000000 --- a/themes/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.christiangoeschel.com/repoman-cli/themes - -go 1.23.0 diff --git a/ui/go.mod b/ui/go.mod new file mode 100644 index 0000000..3cf6b31 --- /dev/null +++ b/ui/go.mod @@ -0,0 +1,3 @@ +module git.christiangoeschel.com/repoman-cli/ui + +go 1.23.0 diff --git a/ui/ui.go b/ui/ui.go new file mode 100644 index 0000000..ca90f29 --- /dev/null +++ b/ui/ui.go @@ -0,0 +1,77 @@ +package ui + +import ( + "os" + "golang.org/x/term" + "github.com/charmbracelet/lipgloss" +) + +var ( + DefaultTheme = 0 + CurrentTheme = 0 + TopBarBackgroundColor = "236" + + RoundLabelCornerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#448AFF")) + LabelArrowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#448AFF")).Background(lipgloss.Color(TopBarBackgroundColor)) + SpinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("69")) + KeywordStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("211")) + URLStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#03A9F4")) + SubtleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")) + TitleStyle = lipgloss.NewStyle().Background(lipgloss.Color("#448AFF")).Foreground(lipgloss.Color("#FFFFFF")) + StatusOK = lipgloss.NewStyle().Foreground(lipgloss.Color("200")) + CheckboxStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#03A9F4")).BorderLeft(true).BorderStyle(lipgloss.NormalBorder()).BorderForeground(lipgloss.Color("#448AFF")).PaddingLeft(2) + MenuOptionTitleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")).BorderLeft(true).BorderStyle(lipgloss.NormalBorder()).BorderForeground(lipgloss.Color("241")).PaddingLeft(1) + MenuOptionDescStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).BorderLeft(true).BorderStyle(lipgloss.NormalBorder()).BorderForeground(lipgloss.Color("241")).PaddingLeft(1) + + + MainStyle = lipgloss.NewStyle().BorderTop(true).BorderStyle(lipgloss.NormalBorder()).BorderForeground(lipgloss.Color("241")) + FooterStyle = lipgloss.NewStyle().BorderTop(true).BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("241")) + + FooterRightStyle = lipgloss.NewStyle().Align(lipgloss.Right) + FooterLeftStyle = lipgloss.NewStyle().Align(lipgloss.Left) + + + TopBarStyle = lipgloss.NewStyle().BorderBottom(true).BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("241")) + + TopBarRightStyle = lipgloss.NewStyle().Align(lipgloss.Right).Background(lipgloss.Color(TopBarBackgroundColor)) + TopBarRightLabel = lipgloss.NewStyle().Background(lipgloss.Color("#2ECC71")).Foreground(lipgloss.Color("#FFFFFF")) + TopBarLeftStyle = lipgloss.NewStyle().Align(lipgloss.Left).Background(lipgloss.Color(TopBarBackgroundColor)) + +) + +func ChangeTheme(themeID int) (err error) { + switch themeID { + case 0: + KeywordStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("200")) + return nil + + case 1: + KeywordStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("190")) + return nil + } + + return nil +} + +func GetWindowSize() (width int, height int, err error) { + // Get terminal file descriptor + fd := int(os.Stdin.Fd()) + // Get terminal size + width, height, err = term.GetSize(fd) + if err != nil { + return 0, 0, err + } + + return width, height, nil +} + +func SetTheme(themeID int) (err error) { + DefaultTheme = themeID + _ = ChangeTheme(DefaultTheme) + + return nil +} + +