Feat: New progressbar using bubble tea
- commit
- d8a0f1de225c0deb6ab0aab1b597d1605eb34186
- author
- bnema · · 2023-11-08 18:17 UTC
- parent
- 7b9ebb2
- signature
- unsigned
5 files · +191 -141
M go.mod +1 -0
| @@ -25,6 +25,7 @@ require ( | |||
| 25 | 25 | ||
| 26 | 26 | require ( | |
| 27 | 27 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect | |
| 28 | + | github.com/charmbracelet/harmonica v0.2.0 // indirect | |
| 28 | 29 | github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect | |
| 29 | 30 | github.com/creack/pty v1.1.18 // indirect | |
| 30 | 31 | 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 // indirect | 27 | 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 // indirect | 29 | github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect | ||
| 29 | github.com/creack/pty v1.1.18 // indirect | 30 | github.com/creack/pty v1.1.18 // indirect | ||
| 30 | github.com/dustin/go-humanize v1.0.1 // indirect | 31 | 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 ( | |||
| 14 | 14 | "github.com/bnema/gordon/internal/cli/mvu" | |
| 15 | 15 | "github.com/bnema/gordon/internal/common" | |
| 16 | 16 | "github.com/bnema/gordon/pkg/docker" | |
| 17 | - | "github.com/cheggaaa/pb" | |
| 18 | 17 | "github.com/fatih/color" | |
| 19 | 18 | "github.com/spf13/cobra" | |
| 20 | 19 | ) | |
| @@ -35,7 +34,7 @@ func NewDeployCommand(a *cli.App) *cobra.Command { | |||
| 35 | 34 | }, | |
| 36 | 35 | Run: func(cmd *cobra.Command, args []string) { | |
| 37 | 36 | imageName := args[0] | |
| 38 | - | color.Blue("Deploying image: %s", imageName) | |
| 37 | + | color.White("Pushing image: %s", imageName) | |
| 39 | 38 | ||
| 40 | 39 | // Validate the image name | |
| 41 | 40 | if err := handler.ValidateImageName(imageName); err != nil { | |
| @@ -65,11 +64,14 @@ func NewDeployCommand(a *cli.App) *cobra.Command { | |||
| 65 | 64 | return | |
| 66 | 65 | } | |
| 67 | 66 | ||
| 68 | - | // Initialize the progress bar | |
| 69 | - | bar := pb.New64(actualSize) | |
| 67 | + | progressCh := make(chan mvu.ProgressMsg) | |
| 70 | 68 | ||
| 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 | + | } | |
| 73 | 75 | ||
| 74 | 76 | // Create a RequestPayload and populate it | |
| 75 | 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 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 | + | } | |
| 98 | 95 | ||
| 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 | + | } | |
| 103 | 108 | ||
| 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 | + | } | |
| 106 | 115 | ||
| 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 | + | } | |
| 111 | 122 | ||
| 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 | |
| 116 | 127 | } | |
| 117 | - | } | |
| 118 | 128 | ||
| 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) | |
| 121 | 137 | if err != nil { | |
| 122 | - | fmt.Println("Error running deployment TUI:", err) | |
| 138 | + | fmt.Println("Error running progress bar TUI:", err) | |
| 123 | 139 | return | |
| 124 | 140 | } | |
| 125 | 141 | ||
| 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) | |
| 129 | 146 | return | |
| 130 | 147 | } | |
| 131 | 148 | ||
| 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 | + | ||
| 135 | 151 | }, | |
| 136 | 152 | } | |
| 137 | 153 | ||
| @@ -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 name | 39 | // 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 | return | 64 | return | ||
| 66 | } | 65 | } | ||
| 67 | 66 | ||||
| 68 | - | // Initialize the progress bar | 67 | + | progressCh := make(chan mvu.ProgressMsg) |
| 69 | - | bar := pb.New64(actualSize) | |||
| 70 | 68 | ||||
| 71 | - | // Wrap the ReadCounter with the bar proxy | 69 | + | // 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 it | 76 | // 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 bar | 87 | + | go func() { |
| 86 | - | bar.Start() | 88 | + | resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/push") |
| 87 | - | // Send the request to the backend | 89 | + | 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 | - | return | 93 | + | 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 response | 96 | + | // Check the response |
| 100 | - | targetDomain := string(resp.Body) | 97 | + | targetDomain := string(resp.Body) |
| 101 | - | targetDomain = strings.TrimSpace(targetDomain) // Remove leading and trailing whitespace | 98 | + | targetDomain = strings.TrimSpace(targetDomain) // Remove leading and trailing whitespace |
| 102 | - | targetDomain = strings.Trim(targetDomain, "\"") // Remove leading and trailing quotes | 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 | + | } | |||
| 103 | 108 | ||||
| 104 | - | // Determine if the target is HTTPS or HTTP | 109 | + | // 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 client | 116 | + | // 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 HTTPS | 123 | + | // 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 validated | 126 | + | return |
| 116 | } | 127 | } | ||
| 117 | - | } | |||
| 118 | 128 | ||||
| 119 | - | // Run the TUI program | 129 | + | // 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 | return | 139 | return | ||
| 124 | } | 140 | } | ||
| 125 | 141 | ||||
| 126 | - | // Check if the deployment was successful | 142 | + | // 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 | return | 146 | return | ||
| 130 | } | 147 | } | ||
| 131 | 148 | ||||
| 132 | - | // Print the final message | 149 | + | 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 | + | } | |||