detached 4a74671

Feat: New progressbar using bubble tea

commit
4a74671515c2eeeabb1410e79b2c8b35e713be22
author
bnema · · 2023-11-08 18:17 UTC
parent
46bb1f6
signature
unsigned
5 files · +191 -141
M go.mod +1 -0
@@ -25,6 +25,7 @@ require (
2525
2626 require (
2727 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
28+ github.com/charmbracelet/harmonica v0.2.0 // indirect
2829 github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
2930 github.com/creack/pty v1.1.18 // indirect
3031 github.com/dustin/go-humanize v1.0.1 // indirect
@@ -25,6 +25,7 @@ require (
25 25
26 require (26 require (
27 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect27 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
28+ github.com/charmbracelet/harmonica v0.2.0 // indirect
28 github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect29 github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
29 github.com/creack/pty v1.1.18 // indirect30 github.com/creack/pty v1.1.18 // indirect
30 github.com/dustin/go-humanize v1.0.1 // indirect31 github.com/dustin/go-humanize v1.0.1 // indirect
M go.sum +2 -0 generated file: collapsed
M internal/cli/cmd/deploy.go +59 -43
@@ -14,7 +14,6 @@ import (
1414 "github.com/bnema/gordon/internal/cli/mvu"
1515 "github.com/bnema/gordon/internal/common"
1616 "github.com/bnema/gordon/pkg/docker"
17- "github.com/cheggaaa/pb"
1817 "github.com/fatih/color"
1918 "github.com/spf13/cobra"
2019 )
@@ -35,7 +34,7 @@ func NewDeployCommand(a *cli.App) *cobra.Command {
3534 },
3635 Run: func(cmd *cobra.Command, args []string) {
3736 imageName := args[0]
38- color.Blue("Deploying image: %s", imageName)
37+ color.White("Pushing image: %s", imageName)
3938
4039 // Validate the image name
4140 if err := handler.ValidateImageName(imageName); err != nil {
@@ -65,11 +64,14 @@ func NewDeployCommand(a *cli.App) *cobra.Command {
6564 return
6665 }
6766
68- // Initialize the progress bar
69- bar := pb.New64(actualSize)
67+ progressCh := make(chan mvu.ProgressMsg)
7068
71- // Wrap the ReadCounter with the bar proxy
72- progressReader := bar.NewProxyReader(reader)
69+ // Create a progress function to update the progress bar
70+ progressReader := &mvu.ProgressReader{
71+ Reader: reader, // This is the actual reader from exportDockerImage
72+ Total: actualSize, // This is the total size of the data to be read
73+ ProgressCh: progressCh, // This is the channel used to send progress updates
74+ }
7375
7476 // Create a RequestPayload and populate it
7577 reqPayload := common.RequestPayload{
@@ -82,56 +84,70 @@ func NewDeployCommand(a *cli.App) *cobra.Command {
8284 },
8385 }
8486
85- // Start the progress bar
86- bar.Start()
87- // Send the request to the backend
88- resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/push")
89- if err != nil {
90- fmt.Println("Error sending HTTP request:", err)
91- return
92- }
93- // Before finishing, ensure progress does not exceed 100%
94- bar.Set64(actualSize)
95-
96- // Stop the progress bar
97- bar.Finish()
87+ go func() {
88+ resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/push")
89+ if err != nil {
90+ fmt.Println("Error sending HTTP request:", err)
91+ // Don't forget to close the channel if there's an error
92+ close(progressCh)
93+ return
94+ }
9895
99- // Check the response
100- targetDomain := string(resp.Body)
101- targetDomain = strings.TrimSpace(targetDomain) // Remove leading and trailing whitespace
102- targetDomain = strings.Trim(targetDomain, "\"") // Remove leading and trailing quotes
96+ // Check the response
97+ targetDomain := string(resp.Body)
98+ targetDomain = strings.TrimSpace(targetDomain) // Remove leading and trailing whitespace
99+ targetDomain = strings.Trim(targetDomain, "\"") // Remove leading and trailing quotes
100+ // Close the progress channel after the upload is complete
101+ // Determine if the target is HTTPS or HTTP
102+ isHTTPS := strings.HasPrefix(targetDomain, "https://")
103+
104+ // Initialize HTTP client
105+ client := &http.Client{
106+ Timeout: 5 * time.Second,
107+ }
103108
104- // Determine if the target is HTTPS or HTTP
105- isHTTPS := strings.HasPrefix(targetDomain, "https://")
109+ // Only set TLS config if target is HTTPS
110+ if isHTTPS {
111+ client.Transport = &http.Transport{
112+ TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, // set false to ensure certificate is validated
113+ }
114+ }
106115
107- // Initialize HTTP client
108- client := &http.Client{
109- Timeout: 5 * time.Second,
110- }
116+ // Run the TUI program
117+ finalModel, err := mvu.RunDeploymentTUI(client, imageName, targetDomain, port)
118+ if err != nil {
119+ fmt.Println("Error running deployment TUI:", err)
120+ return
121+ }
111122
112- // Only set TLS config if target is HTTPS
113- if isHTTPS {
114- client.Transport = &http.Transport{
115- TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, // set false to ensure certificate is validated
123+ // Check if the deployment was successful
124+ if !finalModel.DeploymentDone {
125+ fmt.Println("Deployment failed check your configuration and try again.")
126+ return
116127 }
117- }
118128
119- // Run the TUI program
120- finalModel, err := mvu.RunDeploymentTUI(client, imageName, targetDomain, port)
129+ // Print the final message
130+ color.Blue(" Deployment successful!")
131+ fmt.Println(" Your application is now available at:", targetDomain)
132+ close(progressCh)
133+ }()
134+
135+ // Run the progress bar TUI
136+ m, err := mvu.RunProgressBarTUI(progressCh)
121137 if err != nil {
122- fmt.Println("Error running deployment TUI:", err)
138+ fmt.Println("Error running progress bar TUI:", err)
123139 return
124140 }
125141
126- // Check if the deployment was successful
127- if !finalModel.DeploymentDone {
128- fmt.Println("Deployment failed check your configuration and try again.")
142+ // Close the reader
143+ err = progressReader.Close()
144+ if err != nil {
145+ fmt.Println("Error closing reader:", err)
129146 return
130147 }
131148
132- // Print the final message
133- color.Green(" Deployment successful!")
134- fmt.Println(" Your application is now available at:", targetDomain)
149+ m.Done = true
150+
135151 },
136152 }
137153
@@ -14,7 +14,6 @@ import (
14 "github.com/bnema/gordon/internal/cli/mvu"14 "github.com/bnema/gordon/internal/cli/mvu"
15 "github.com/bnema/gordon/internal/common"15 "github.com/bnema/gordon/internal/common"
16 "github.com/bnema/gordon/pkg/docker"16 "github.com/bnema/gordon/pkg/docker"
17- "github.com/cheggaaa/pb"
18 "github.com/fatih/color"17 "github.com/fatih/color"
19 "github.com/spf13/cobra"18 "github.com/spf13/cobra"
20 )19 )
@@ -35,7 +34,7 @@ func NewDeployCommand(a *cli.App) *cobra.Command {
35 },34 },
36 Run: func(cmd *cobra.Command, args []string) {35 Run: func(cmd *cobra.Command, args []string) {
37 imageName := args[0]36 imageName := args[0]
38- color.Blue("Deploying image: %s", imageName)37+ color.White("Pushing image: %s", imageName)
39 38
40 // Validate the image name39 // Validate the image name
41 if err := handler.ValidateImageName(imageName); err != nil {40 if err := handler.ValidateImageName(imageName); err != nil {
@@ -65,11 +64,14 @@ func NewDeployCommand(a *cli.App) *cobra.Command {
65 return64 return
66 }65 }
67 66
68- // Initialize the progress bar67+ progressCh := make(chan mvu.ProgressMsg)
69- bar := pb.New64(actualSize)
70 68
71- // Wrap the ReadCounter with the bar proxy69+ // Create a progress function to update the progress bar
72- progressReader := bar.NewProxyReader(reader)70+ progressReader := &mvu.ProgressReader{
71+ Reader: reader, // This is the actual reader from exportDockerImage
72+ Total: actualSize, // This is the total size of the data to be read
73+ ProgressCh: progressCh, // This is the channel used to send progress updates
74+ }
73 75
74 // Create a RequestPayload and populate it76 // Create a RequestPayload and populate it
75 reqPayload := common.RequestPayload{77 reqPayload := common.RequestPayload{
@@ -82,56 +84,70 @@ func NewDeployCommand(a *cli.App) *cobra.Command {
82 },84 },
83 }85 }
84 86
85- // Start the progress bar87+ go func() {
86- bar.Start()88+ resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/push")
87- // Send the request to the backend89+ if err != nil {
88- resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/push")90+ fmt.Println("Error sending HTTP request:", err)
89- if err != nil {91+ // Don't forget to close the channel if there's an error
90- fmt.Println("Error sending HTTP request:", err)92+ close(progressCh)
91- return93+ return
92- }94+ }
93- // Before finishing, ensure progress does not exceed 100%
94- bar.Set64(actualSize)
95-
96- // Stop the progress bar
97- bar.Finish()
98 95
99- // Check the response96+ // Check the response
100- targetDomain := string(resp.Body)97+ targetDomain := string(resp.Body)
101- targetDomain = strings.TrimSpace(targetDomain) // Remove leading and trailing whitespace98+ targetDomain = strings.TrimSpace(targetDomain) // Remove leading and trailing whitespace
102- targetDomain = strings.Trim(targetDomain, "\"") // Remove leading and trailing quotes99+ targetDomain = strings.Trim(targetDomain, "\"") // Remove leading and trailing quotes
100+ // Close the progress channel after the upload is complete
101+ // Determine if the target is HTTPS or HTTP
102+ isHTTPS := strings.HasPrefix(targetDomain, "https://")
103+
104+ // Initialize HTTP client
105+ client := &http.Client{
106+ Timeout: 5 * time.Second,
107+ }
103 108
104- // Determine if the target is HTTPS or HTTP109+ // Only set TLS config if target is HTTPS
105- isHTTPS := strings.HasPrefix(targetDomain, "https://")110+ if isHTTPS {
111+ client.Transport = &http.Transport{
112+ TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, // set false to ensure certificate is validated
113+ }
114+ }
106 115
107- // Initialize HTTP client116+ // Run the TUI program
108- client := &http.Client{117+ finalModel, err := mvu.RunDeploymentTUI(client, imageName, targetDomain, port)
109- Timeout: 5 * time.Second,118+ if err != nil {
110- }119+ fmt.Println("Error running deployment TUI:", err)
120+ return
121+ }
111 122
112- // Only set TLS config if target is HTTPS123+ // Check if the deployment was successful
113- if isHTTPS {124+ if !finalModel.DeploymentDone {
114- client.Transport = &http.Transport{125+ fmt.Println("Deployment failed check your configuration and try again.")
115- TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, // set false to ensure certificate is validated126+ return
116 }127 }
117- }
118 128
119- // Run the TUI program129+ // Print the final message
120- finalModel, err := mvu.RunDeploymentTUI(client, imageName, targetDomain, port)130+ color.Blue(" Deployment successful!")
131+ fmt.Println(" Your application is now available at:", targetDomain)
132+ close(progressCh)
133+ }()
134+
135+ // Run the progress bar TUI
136+ m, err := mvu.RunProgressBarTUI(progressCh)
121 if err != nil {137 if err != nil {
122- fmt.Println("Error running deployment TUI:", err)138+ fmt.Println("Error running progress bar TUI:", err)
123 return139 return
124 }140 }
125 141
126- // Check if the deployment was successful142+ // Close the reader
127- if !finalModel.DeploymentDone {143+ err = progressReader.Close()
128- fmt.Println("Deployment failed check your configuration and try again.")144+ if err != nil {
145+ fmt.Println("Error closing reader:", err)
129 return146 return
130 }147 }
131 148
132- // Print the final message149+ m.Done = true
133- color.Green(" Deployment successful!")150+
134- fmt.Println(" Your application is now available at:", targetDomain)
135 },151 },
136 }152 }
137 153
D internal/cli/mvu/_progressbar.go +0 -98
@@ -1,98 +0,0 @@
1-package mvu
2-
3-import (
4- "fmt"
5- "strings"
6-
7- "github.com/charmbracelet/bubbles/progress"
8- tea "github.com/charmbracelet/bubbletea"
9- "github.com/charmbracelet/lipgloss"
10-)
11-
12-const (
13- padding = 2
14- maxWidth = 80
15-)
16-
17-type progressMsg int64
18-
19-type Model struct {
20- progress progress.Model
21- actualSize int64
22-}
23-
24-var helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render
25-
26-// Update your model to listen to the progress channel.
27-func NewProgressBarModel(actualSize int64, progressCh <-chan int64) Model {
28- m := Model{
29- progress: progress.New(progress.WithDefaultGradient()),
30- actualSize: actualSize,
31- }
32-
33- // Start the program and listen to the channel in a separate goroutine
34- go func() {
35- p := tea.NewProgram(m)
36- for n := range progressCh {
37- percent := float64(n) / float64(m.actualSize)
38- p.Send(progressMsg(int64(percent * 100))) // Send update as percent
39- }
40- }()
41-
42- return m
43-}
44-
45-func (m Model) Init() tea.Cmd {
46- // Start the channel listener
47- m.progress.SetPercent(100)
48- return nil
49-}
50-
51-func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
52- switch msg := msg.(type) {
53- case tea.KeyMsg:
54- return m, tea.Quit
55-
56- case tea.WindowSizeMsg:
57- m.progress.Width = msg.Width - padding*2 - 4
58- if m.progress.Width > maxWidth {
59- m.progress.Width = maxWidth
60- }
61- return m, nil
62-
63- case progressMsg:
64- percent := float64(msg) / 100.0 // Convert percentage back to a float
65- m.progress.SetPercent(percent)
66- fmt.Printf("Progress message received. Percent: %.2f%%\n", percent*100)
67- if percent >= 1.0 {
68- return m, tea.Quit
69- }
70- return m, nil
71-
72- // FrameMsg is sent when the progress bar wants to animate itself
73- case progress.FrameMsg:
74- progressModel, cmd := m.progress.Update(msg)
75- m.progress = progressModel.(progress.Model)
76- return m, cmd
77-
78- default:
79- return m, nil
80- }
81-}
82-
83-func (m Model) View() string {
84- pad := strings.Repeat(" ", padding)
85- return "\n" +
86- pad + m.progress.View() + "\n\n" +
87- pad + helpStyle("Press any key to quit")
88-}
89-
90-// p.Send(mvu.ProgressMsg(int64(percent * 100))) // Send update as percent
91-
92-func (m Model) Send(msg progressMsg) {
93- m.progress.Update(msg)
94-}
95-
96-func ProgressMsg(percent int64) progressMsg {
97- return progressMsg(percent)
98-}
@@ -1,98 +0,0 @@
1-package mvu
2-
3-import (
4- "fmt"
5- "strings"
6-
7- "github.com/charmbracelet/bubbles/progress"
8- tea "github.com/charmbracelet/bubbletea"
9- "github.com/charmbracelet/lipgloss"
10-)
11-
12-const (
13- padding = 2
14- maxWidth = 80
15-)
16-
17-type progressMsg int64
18-
19-type Model struct {
20- progress progress.Model
21- actualSize int64
22-}
23-
24-var helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render
25-
26-// Update your model to listen to the progress channel.
27-func NewProgressBarModel(actualSize int64, progressCh <-chan int64) Model {
28- m := Model{
29- progress: progress.New(progress.WithDefaultGradient()),
30- actualSize: actualSize,
31- }
32-
33- // Start the program and listen to the channel in a separate goroutine
34- go func() {
35- p := tea.NewProgram(m)
36- for n := range progressCh {
37- percent := float64(n) / float64(m.actualSize)
38- p.Send(progressMsg(int64(percent * 100))) // Send update as percent
39- }
40- }()
41-
42- return m
43-}
44-
45-func (m Model) Init() tea.Cmd {
46- // Start the channel listener
47- m.progress.SetPercent(100)
48- return nil
49-}
50-
51-func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
52- switch msg := msg.(type) {
53- case tea.KeyMsg:
54- return m, tea.Quit
55-
56- case tea.WindowSizeMsg:
57- m.progress.Width = msg.Width - padding*2 - 4
58- if m.progress.Width > maxWidth {
59- m.progress.Width = maxWidth
60- }
61- return m, nil
62-
63- case progressMsg:
64- percent := float64(msg) / 100.0 // Convert percentage back to a float
65- m.progress.SetPercent(percent)
66- fmt.Printf("Progress message received. Percent: %.2f%%\n", percent*100)
67- if percent >= 1.0 {
68- return m, tea.Quit
69- }
70- return m, nil
71-
72- // FrameMsg is sent when the progress bar wants to animate itself
73- case progress.FrameMsg:
74- progressModel, cmd := m.progress.Update(msg)
75- m.progress = progressModel.(progress.Model)
76- return m, cmd
77-
78- default:
79- return m, nil
80- }
81-}
82-
83-func (m Model) View() string {
84- pad := strings.Repeat(" ", padding)
85- return "\n" +
86- pad + m.progress.View() + "\n\n" +
87- pad + helpStyle("Press any key to quit")
88-}
89-
90-// p.Send(mvu.ProgressMsg(int64(percent * 100))) // Send update as percent
91-
92-func (m Model) Send(msg progressMsg) {
93- m.progress.Update(msg)
94-}
95-
96-func ProgressMsg(percent int64) progressMsg {
97- return progressMsg(percent)
98-}
A internal/cli/mvu/progressbar.go +129 -0
@@ -0,0 +1,129 @@
1+package mvu
2+
3+import (
4+ "fmt"
5+ "io"
6+ "os"
7+ "strings"
8+
9+ "github.com/charmbracelet/bubbles/progress"
10+ tea "github.com/charmbracelet/bubbletea"
11+ "github.com/charmbracelet/lipgloss"
12+)
13+
14+const (
15+ padding = 2
16+ maxWidth = 80
17+)
18+
19+var helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render
20+
21+type ProgressMsg float64
22+
23+type Model struct {
24+ progress progress.Model
25+ Done bool
26+}
27+
28+type ProgressReader struct {
29+ Reader io.ReadCloser
30+ Total int64
31+ ReadBytes int64
32+ ProgressCh chan<- ProgressMsg
33+}
34+
35+func NewPBModel() Model {
36+ // Gradient from #011E5C to #007BC0
37+ m := progress.New(progress.WithGradient("#007BC0", "#011E5C"))
38+ m.Width = maxWidth
39+
40+ return Model{
41+ progress: m,
42+ }
43+}
44+
45+// Modify RunProgressBarTUI to return a pointer to Model.
46+func RunProgressBarTUI(ProgressCh <-chan ProgressMsg) (*Model, error) {
47+ m := NewPBModel()
48+ p := tea.NewProgram(&m)
49+ // Start a goroutine that updates the progress bar as percentages are received on the channel.
50+ go func() {
51+ for percent := range ProgressCh {
52+ p.Send(ProgressMsg(percent))
53+ }
54+ }()
55+
56+ if _, err := p.Run(); err != nil {
57+ fmt.Println("Error running progress bar TUI:", err)
58+ os.Exit(1)
59+ }
60+
61+ return &m, nil // Return a pointer to the model.
62+}
63+
64+func (pr *ProgressReader) Read(p []byte) (int, error) {
65+ n, err := pr.Reader.Read(p)
66+ if err != nil {
67+ return n, err
68+ }
69+
70+ pr.ReadBytes += int64(n)
71+ percentage := float64(pr.ReadBytes) / float64(pr.Total)
72+ pr.ProgressCh <- ProgressMsg(percentage) // Send the percentage on the channel.
73+ return n, err
74+}
75+
76+func (pr *ProgressReader) Close() error {
77+ if closer, ok := pr.Reader.(io.Closer); ok {
78+ return closer.Close()
79+ }
80+ return nil // If there's nothing to close, return nil.
81+}
82+
83+func (m Model) Init() tea.Cmd {
84+ m.progress.Init()
85+ m.progress.SetPercent(0)
86+ return nil
87+}
88+
89+func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
90+ switch msg := msg.(type) {
91+ case tea.KeyMsg:
92+ return m, tea.Quit
93+
94+ case tea.WindowSizeMsg:
95+ m.progress.Width = msg.Width - padding*2 - 4
96+ if m.progress.Width > maxWidth {
97+ m.progress.Width = maxWidth
98+ }
99+ return m, nil
100+
101+ case ProgressMsg:
102+ // Update the progress bar with the percentage received on the channel.
103+ cmd := m.progress.SetPercent(float64(msg))
104+
105+ // If the percentage is 100, set Done to true.
106+ if msg == 1 {
107+ m.Done = true
108+ // quit
109+ return m, tea.Quit
110+ }
111+
112+ return m, cmd
113+
114+ case progress.FrameMsg:
115+ progressModel, cmd := m.progress.Update(msg)
116+ m.progress = progressModel.(progress.Model)
117+ return m, cmd
118+
119+ default:
120+ return m, nil
121+ }
122+}
123+
124+func (m Model) View() string {
125+ pad := strings.Repeat(" ", padding)
126+ return "\n" +
127+ pad + m.progress.View() + "\n\n" +
128+ pad + helpStyle("Press any key to quit")
129+}
@@ -0,0 +1,129 @@
1+package mvu
2+
3+import (
4+ "fmt"
5+ "io"
6+ "os"
7+ "strings"
8+
9+ "github.com/charmbracelet/bubbles/progress"
10+ tea "github.com/charmbracelet/bubbletea"
11+ "github.com/charmbracelet/lipgloss"
12+)
13+
14+const (
15+ padding = 2
16+ maxWidth = 80
17+)
18+
19+var helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render
20+
21+type ProgressMsg float64
22+
23+type Model struct {
24+ progress progress.Model
25+ Done bool
26+}
27+
28+type ProgressReader struct {
29+ Reader io.ReadCloser
30+ Total int64
31+ ReadBytes int64
32+ ProgressCh chan<- ProgressMsg
33+}
34+
35+func NewPBModel() Model {
36+ // Gradient from #011E5C to #007BC0
37+ m := progress.New(progress.WithGradient("#007BC0", "#011E5C"))
38+ m.Width = maxWidth
39+
40+ return Model{
41+ progress: m,
42+ }
43+}
44+
45+// Modify RunProgressBarTUI to return a pointer to Model.
46+func RunProgressBarTUI(ProgressCh <-chan ProgressMsg) (*Model, error) {
47+ m := NewPBModel()
48+ p := tea.NewProgram(&m)
49+ // Start a goroutine that updates the progress bar as percentages are received on the channel.
50+ go func() {
51+ for percent := range ProgressCh {
52+ p.Send(ProgressMsg(percent))
53+ }
54+ }()
55+
56+ if _, err := p.Run(); err != nil {
57+ fmt.Println("Error running progress bar TUI:", err)
58+ os.Exit(1)
59+ }
60+
61+ return &m, nil // Return a pointer to the model.
62+}
63+
64+func (pr *ProgressReader) Read(p []byte) (int, error) {
65+ n, err := pr.Reader.Read(p)
66+ if err != nil {
67+ return n, err
68+ }
69+
70+ pr.ReadBytes += int64(n)
71+ percentage := float64(pr.ReadBytes) / float64(pr.Total)
72+ pr.ProgressCh <- ProgressMsg(percentage) // Send the percentage on the channel.
73+ return n, err
74+}
75+
76+func (pr *ProgressReader) Close() error {
77+ if closer, ok := pr.Reader.(io.Closer); ok {
78+ return closer.Close()
79+ }
80+ return nil // If there's nothing to close, return nil.
81+}
82+
83+func (m Model) Init() tea.Cmd {
84+ m.progress.Init()
85+ m.progress.SetPercent(0)
86+ return nil
87+}
88+
89+func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
90+ switch msg := msg.(type) {
91+ case tea.KeyMsg:
92+ return m, tea.Quit
93+
94+ case tea.WindowSizeMsg:
95+ m.progress.Width = msg.Width - padding*2 - 4
96+ if m.progress.Width > maxWidth {
97+ m.progress.Width = maxWidth
98+ }
99+ return m, nil
100+
101+ case ProgressMsg:
102+ // Update the progress bar with the percentage received on the channel.
103+ cmd := m.progress.SetPercent(float64(msg))
104+
105+ // If the percentage is 100, set Done to true.
106+ if msg == 1 {
107+ m.Done = true
108+ // quit
109+ return m, tea.Quit
110+ }
111+
112+ return m, cmd
113+
114+ case progress.FrameMsg:
115+ progressModel, cmd := m.progress.Update(msg)
116+ m.progress = progressModel.(progress.Model)
117+ return m, cmd
118+
119+ default:
120+ return m, nil
121+ }
122+}
123+
124+func (m Model) View() string {
125+ pad := strings.Repeat(" ", padding)
126+ return "\n" +
127+ pad + m.progress.View() + "\n\n" +
128+ pad + helpStyle("Press any key to quit")
129+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close