diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 01a82ff21a..c8e96bc419 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -4,7 +4,6 @@ package cmd import ( - "fmt" "os" "github.com/daytonaio/daytona/internal/util" @@ -14,8 +13,9 @@ import ( . "github.com/daytonaio/daytona/pkg/cmd/ports" . "github.com/daytonaio/daytona/pkg/cmd/profile" . "github.com/daytonaio/daytona/pkg/cmd/server" + apikeyCmd "github.com/daytonaio/daytona/pkg/cmd/server/apikey" . "github.com/daytonaio/daytona/pkg/cmd/workspace" - view_util "github.com/daytonaio/daytona/pkg/views/util" + view "github.com/daytonaio/daytona/pkg/views/initial" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -26,11 +26,23 @@ var rootCmd = &cobra.Command{ Short: "Daytona is a Dev Environment Manager", Long: "Daytona is a Dev Environment Manager", Run: func(cmd *cobra.Command, args []string) { - fmt.Print(view_util.GetLongDescription()) - err := cmd.Help() + command, err := view.GetCommand() if err != nil { log.Fatal(err) } + + switch command { + case "list": + ListCmd.Run(cmd, args) + case "profile add": + ProfileAddCmd.Run(cmd, []string{}) + case "server api-key new": + apikeyCmd.GenerateCmd.Run(cmd, []string{}) + case "create": + CreateCmd.Run(cmd, []string{}) + case "help": + cmd.Help() + } }, } diff --git a/pkg/cmd/gitprovider/add.go b/pkg/cmd/gitprovider/add.go index 95027549d1..bd1ddb9917 100644 --- a/pkg/cmd/gitprovider/add.go +++ b/pkg/cmd/gitprovider/add.go @@ -8,8 +8,8 @@ import ( "github.com/daytonaio/daytona/internal/util/apiclient/server" "github.com/daytonaio/daytona/pkg/serverapiclient" + "github.com/daytonaio/daytona/pkg/views" gitprovider_view "github.com/daytonaio/daytona/pkg/views/gitprovider" - "github.com/daytonaio/daytona/pkg/views/util" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -43,6 +43,6 @@ var gitProviderAddCmd = &cobra.Command{ log.Fatal(err) } - util.RenderInfoMessage("Git provider has been registered") + views.RenderInfoMessage("Git provider has been registered") }, } diff --git a/pkg/cmd/gitprovider/delete.go b/pkg/cmd/gitprovider/delete.go index a11dd0a014..2e4d1f0a34 100644 --- a/pkg/cmd/gitprovider/delete.go +++ b/pkg/cmd/gitprovider/delete.go @@ -9,8 +9,8 @@ import ( "github.com/daytonaio/daytona/internal/util/apiclient" "github.com/daytonaio/daytona/internal/util/apiclient/server" "github.com/daytonaio/daytona/pkg/serverapiclient" + "github.com/daytonaio/daytona/pkg/views" gitprovider_view "github.com/daytonaio/daytona/pkg/views/gitprovider" - "github.com/daytonaio/daytona/pkg/views/util" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -39,7 +39,7 @@ var gitProviderDeleteCmd = &cobra.Command{ gitProviderData.BaseApiUrl = new(string) if len(gitProviders) == 0 { - util.RenderInfoMessage("No git providers registered") + views.RenderInfoMessage("No git providers registered") return } @@ -55,6 +55,6 @@ var gitProviderDeleteCmd = &cobra.Command{ log.Fatal(err) } - util.RenderInfoMessage("Git provider has been removed") + views.RenderInfoMessage("Git provider has been removed") }, } diff --git a/pkg/cmd/gitprovider/gitprovider.go b/pkg/cmd/gitprovider/gitprovider.go index 319ab6fc07..29a13f1724 100644 --- a/pkg/cmd/gitprovider/gitprovider.go +++ b/pkg/cmd/gitprovider/gitprovider.go @@ -11,8 +11,8 @@ import ( "github.com/daytonaio/daytona/internal/util/apiclient" "github.com/daytonaio/daytona/internal/util/apiclient/server" "github.com/daytonaio/daytona/pkg/cmd/output" + "github.com/daytonaio/daytona/pkg/views" gitprovider_view "github.com/daytonaio/daytona/pkg/views/gitprovider" - "github.com/daytonaio/daytona/pkg/views/util" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -33,11 +33,11 @@ var GitProviderCmd = &cobra.Command{ } if len(gitProviders) == 0 { - util.RenderInfoMessage("No git providers registered. Add a new git provider by preparing a Personal Access Token and running 'daytona git-providers add'") + views.RenderInfoMessage("No git providers registered. Add a new git provider by preparing a Personal Access Token and running 'daytona git-providers add'") return } - util.RenderMainTitle("Registered Git providers:") + views.RenderMainTitle("Registered Git providers:") supportedProviders := config.GetSupportedGitProviders() var gitProviderViewList []gitprovider_view.GitProviderView @@ -62,7 +62,7 @@ var GitProviderCmd = &cobra.Command{ } for _, gitProviderView := range gitProviderViewList { - util.RenderListLine(fmt.Sprintf("%s (%s)", gitProviderView.Name, gitProviderView.Username)) + views.RenderListLine(fmt.Sprintf("%s (%s)", gitProviderView.Name, gitProviderView.Username)) } }, } diff --git a/pkg/cmd/ide.go b/pkg/cmd/ide.go index 831d70f87d..ff338e68ed 100644 --- a/pkg/cmd/ide.go +++ b/pkg/cmd/ide.go @@ -4,11 +4,9 @@ package cmd import ( - "fmt" - "github.com/daytonaio/daytona/cmd/daytona/config" + "github.com/daytonaio/daytona/pkg/views" "github.com/daytonaio/daytona/pkg/views/ide" - "github.com/daytonaio/daytona/pkg/views/util" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -45,6 +43,6 @@ var ideCmd = &cobra.Command{ log.Fatal(err) } - util.RenderInfoMessage(fmt.Sprintf("Default IDE set to: %s", chosenIde.Name)) + views.RenderDefaultIdeUpdated(chosenIde.Name) }, } diff --git a/pkg/cmd/ports/forward.go b/pkg/cmd/ports/forward.go index 85dd014888..a49877e976 100644 --- a/pkg/cmd/ports/forward.go +++ b/pkg/cmd/ports/forward.go @@ -17,7 +17,7 @@ import ( "github.com/daytonaio/daytona/internal/util/apiclient/server" "github.com/daytonaio/daytona/pkg/frpc" "github.com/daytonaio/daytona/pkg/ports" - view_util "github.com/daytonaio/daytona/pkg/views/util" + "github.com/daytonaio/daytona/pkg/views" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -61,9 +61,9 @@ var PortForwardCmd = &cobra.Command{ } } else { if *hostPort != uint16(port) { - view_util.RenderInfoMessage(fmt.Sprintf("Port %d already in use.", port)) + views.RenderInfoMessage(fmt.Sprintf("Port %d already in use.", port)) } - view_util.RenderInfoMessage(fmt.Sprintf("Port available at http://localhost:%d\n", *hostPort)) + views.RenderInfoMessage(fmt.Sprintf("Port available at http://localhost:%d\n", *hostPort)) } if publicPreview { @@ -98,7 +98,7 @@ func init() { } func forwardPublicPort(workspaceId, projectName string, hostPort, targetPort uint16) error { - view_util.RenderInfoMessage("Forwarding port to a public URL...") + views.RenderInfoMessage("Forwarding port to a public URL...") apiClient, err := server.GetApiClient(nil) if err != nil { @@ -117,7 +117,7 @@ func forwardPublicPort(workspaceId, projectName string, hostPort, targetPort uin go func() { time.Sleep(1 * time.Second) - view_util.RenderInfoMessage(fmt.Sprintf("Port available at %s", fmt.Sprintf("%s://%s.%s", *serverConfig.Frps.Protocol, subDomain, *serverConfig.Frps.Domain))) + views.RenderInfoMessage(fmt.Sprintf("Port available at %s", fmt.Sprintf("%s://%s.%s", *serverConfig.Frps.Protocol, subDomain, *serverConfig.Frps.Domain))) }() return frpc.Connect(frpc.FrpcConnectParams{ diff --git a/pkg/cmd/profile/add.go b/pkg/cmd/profile/add.go index 78c9b279e9..21e63904eb 100644 --- a/pkg/cmd/profile/add.go +++ b/pkg/cmd/profile/add.go @@ -12,7 +12,7 @@ import ( "github.com/spf13/cobra" ) -var profileAddCmd = &cobra.Command{ +var ProfileAddCmd = &cobra.Command{ Use: "add", Short: "Add profile", Args: cobra.NoArgs, @@ -97,7 +97,7 @@ var apiUrlFlag string var apiKeyFlag string func init() { - profileAddCmd.Flags().StringVarP(&profileNameFlag, "name", "n", "", "Profile name") - profileAddCmd.Flags().StringVarP(&apiUrlFlag, "api-url", "a", "", "API URL") - profileAddCmd.Flags().StringVarP(&apiKeyFlag, "api-key", "k", "", "API Key") + ProfileAddCmd.Flags().StringVarP(&profileNameFlag, "name", "n", "", "Profile name") + ProfileAddCmd.Flags().StringVarP(&apiUrlFlag, "api-url", "a", "", "API URL") + ProfileAddCmd.Flags().StringVarP(&apiKeyFlag, "api-key", "k", "", "API Key") } diff --git a/pkg/cmd/profile/profile.go b/pkg/cmd/profile/profile.go index 986b70d177..64ff0b59be 100644 --- a/pkg/cmd/profile/profile.go +++ b/pkg/cmd/profile/profile.go @@ -7,8 +7,8 @@ import ( "fmt" "github.com/daytonaio/daytona/cmd/daytona/config" + "github.com/daytonaio/daytona/pkg/views" "github.com/daytonaio/daytona/pkg/views/profile" - "github.com/daytonaio/daytona/pkg/views/util" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -27,12 +27,12 @@ var ProfileCmd = &cobra.Command{ profilesList := c.Profiles if len(profilesList) == 0 { - util.RenderInfoMessage("Add a profile by running `daytona profile add") + views.RenderInfoMessage("Add a profile by running `daytona profile add`") return } if len(profilesList) == 1 { - util.RenderInfoMessage(fmt.Sprintf("You are using profile %s. Add a new profile by running `daytona profile add`", profilesList[0].Name)) + views.RenderInfoMessage(fmt.Sprintf("You are using profile %s. Add a new profile by running `daytona profile add`", profilesList[0].Name)) return } @@ -62,14 +62,14 @@ var ProfileCmd = &cobra.Command{ log.Fatal(err) } - util.RenderInfoMessage(fmt.Sprintf("Active profile set to: %s", chosenProfile.Name)) + views.RenderInfoMessage(fmt.Sprintf("Active profile set to: %s", chosenProfile.Name)) }, } func init() { ProfileCmd.AddCommand(profileListCmd) ProfileCmd.AddCommand(ProfileUseCmd) - ProfileCmd.AddCommand(profileAddCmd) + ProfileCmd.AddCommand(ProfileAddCmd) ProfileCmd.AddCommand(profileEditCmd) ProfileCmd.AddCommand(profileDeleteCmd) } diff --git a/pkg/cmd/profile/use.go b/pkg/cmd/profile/use.go index 39118f59c6..b52946f579 100644 --- a/pkg/cmd/profile/use.go +++ b/pkg/cmd/profile/use.go @@ -11,7 +11,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/daytonaio/daytona/pkg/cmd/output" - view_util "github.com/daytonaio/daytona/pkg/views/util" + "github.com/daytonaio/daytona/pkg/views" "github.com/spf13/cobra" ) @@ -56,6 +56,6 @@ var ProfileUseCmd = &cobra.Command{ return } - view_util.RenderInfoMessage(fmt.Sprintf("Active profile set to %s", chosenProfile.Name)) + views.RenderInfoMessage(fmt.Sprintf("Active profile set to %s", chosenProfile.Name)) }, } diff --git a/pkg/cmd/purge.go b/pkg/cmd/purge.go index fe5d1b990a..75f0822bc6 100644 --- a/pkg/cmd/purge.go +++ b/pkg/cmd/purge.go @@ -6,9 +6,9 @@ package cmd import ( "github.com/daytonaio/daytona/cmd/daytona/config" workspace "github.com/daytonaio/daytona/pkg/cmd/workspace" + "github.com/daytonaio/daytona/pkg/views" profile_view "github.com/daytonaio/daytona/pkg/views/profile" view "github.com/daytonaio/daytona/pkg/views/purge" - view_util "github.com/daytonaio/daytona/pkg/views/util" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -33,7 +33,7 @@ var purgeCmd = &cobra.Command{ if c.ActiveProfileId != "default" { profile_view.SwitchToDefaultPrompt(&switchProfileCheck) if !switchProfileCheck { - view_util.RenderInfoMessage("Operation cancelled.") + views.RenderInfoMessage("Operation cancelled.") return } c.ActiveProfileId = "default" @@ -46,24 +46,24 @@ var purgeCmd = &cobra.Command{ if !yesFlag { view.ConfirmPrompt(&confirmCheck) if !confirmCheck { - view_util.RenderInfoMessage("Operation cancelled.") + views.RenderInfoMessage("Operation cancelled.") return } } - view_util.RenderLine("\nDeleting all workspaces") + views.RenderLine("\nDeleting all workspaces") err = workspace.DeleteAllWorkspaces() if err != nil { log.Fatal(err) } - view_util.RenderLine("Deleting the SSH configuration file") + views.RenderLine("Deleting the SSH configuration file") err = config.UnlinkSshFiles() if err != nil { log.Fatal(err) } - view_util.RenderLine("Deleting autocompletion data") + views.RenderLine("Deleting autocompletion data") err = config.DeleteAutocompletionData() if err != nil { log.Fatal(err) @@ -71,17 +71,17 @@ var purgeCmd = &cobra.Command{ view.ServerStoppedPrompt(&serverStoppedCheck) if !serverStoppedCheck { - view_util.RenderInfoMessage("Operation cancelled.") + views.RenderInfoMessage("Operation cancelled.") return } - view_util.RenderLine("Deleting the Daytona config directory") + views.RenderLine("Deleting the Daytona config directory") err = config.DeleteConfigDir() if err != nil { log.Fatal(err) } - view_util.RenderInfoMessage("All Daytona data has been successfully cleared from the device.\nYou may now delete the binary.") + views.RenderInfoMessage("All Daytona data has been successfully cleared from the device.\nYou may now delete the binary.") }, } diff --git a/pkg/cmd/server/apikey/api_key.go b/pkg/cmd/server/apikey/api_key.go index b25ddcf051..19f74a448c 100644 --- a/pkg/cmd/server/apikey/api_key.go +++ b/pkg/cmd/server/apikey/api_key.go @@ -14,7 +14,7 @@ var ApiKeyCmd = &cobra.Command{ } func init() { - ApiKeyCmd.AddCommand(generateCmd) + ApiKeyCmd.AddCommand(GenerateCmd) ApiKeyCmd.AddCommand(revokeCmd) ApiKeyCmd.AddCommand(listCmd) } diff --git a/pkg/cmd/server/apikey/generate.go b/pkg/cmd/server/apikey/generate.go index afa80c3f64..a85d97a254 100644 --- a/pkg/cmd/server/apikey/generate.go +++ b/pkg/cmd/server/apikey/generate.go @@ -13,13 +13,13 @@ import ( "github.com/daytonaio/daytona/cmd/daytona/config" "github.com/daytonaio/daytona/internal/util/apiclient" "github.com/daytonaio/daytona/internal/util/apiclient/server" + "github.com/daytonaio/daytona/pkg/views" "github.com/daytonaio/daytona/pkg/views/server/apikey" - "github.com/daytonaio/daytona/pkg/views/util" ) var saveFlag bool -var generateCmd = &cobra.Command{ +var GenerateCmd = &cobra.Command{ Use: "generate [NAME]", Short: "Generate a new API key", Aliases: []string{"g", "new"}, @@ -60,11 +60,11 @@ var generateCmd = &cobra.Command{ if err != nil { log.Fatal(err) } - util.RenderBorderedMessage("API key saved to your default profile") + views.RenderBorderedMessage("API key saved to your default profile") return } - util.RenderBorderedMessage(fmt.Sprintf("Generated API key: %s\n\nYou can add it to a profile by running:\n\ndaytona profile edit -k %s\n\nMake sure to copy it as you will not be able to see it again.", key, key)) + views.RenderBorderedMessage(fmt.Sprintf("Generated API key: %s\n\nYou can add it to a profile by running:\n\ndaytona profile edit -k %s\n\nMake sure to copy it as you will not be able to see it again.", key, key)) }, } @@ -85,5 +85,5 @@ func saveKeyToDefaultProfile(key string) error { } func init() { - generateCmd.Flags().BoolVarP(&saveFlag, "save", "s", false, "Save the API key to your default profile on this machine") + GenerateCmd.Flags().BoolVarP(&saveFlag, "save", "s", false, "Save the API key to your default profile on this machine") } diff --git a/pkg/cmd/server/apikey/revoke.go b/pkg/cmd/server/apikey/revoke.go index a797c0461c..ffe5257518 100644 --- a/pkg/cmd/server/apikey/revoke.go +++ b/pkg/cmd/server/apikey/revoke.go @@ -18,7 +18,6 @@ import ( "github.com/daytonaio/daytona/pkg/serverapiclient" "github.com/daytonaio/daytona/pkg/views" "github.com/daytonaio/daytona/pkg/views/server/apikey" - "github.com/daytonaio/daytona/pkg/views/util" ) var yesFlag bool @@ -97,7 +96,7 @@ var revokeCmd = &cobra.Command{ log.Fatal(apiclient.HandleErrorResponse(nil, err)) } - util.RenderInfoMessage("API key revoked") + views.RenderInfoMessage("API key revoked") } else { fmt.Println("Operation canceled.") } diff --git a/pkg/cmd/server/config.go b/pkg/cmd/server/config.go index 37548ce2cd..fc36a833ed 100644 --- a/pkg/cmd/server/config.go +++ b/pkg/cmd/server/config.go @@ -6,12 +6,14 @@ package server import ( "fmt" + "github.com/charmbracelet/lipgloss" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/daytonaio/daytona/internal/util" "github.com/daytonaio/daytona/pkg/cmd/output" "github.com/daytonaio/daytona/pkg/server" + "github.com/daytonaio/daytona/pkg/views" ) var configCmd = &cobra.Command{ @@ -26,6 +28,13 @@ var configCmd = &cobra.Command{ apiUrl := util.GetFrpcApiUrl(config.Frps.Protocol, config.Id, config.Frps.Domain) output.Output = apiUrl - fmt.Println(apiUrl) + output := "" + output += "If you want to connect to the server remotely:\n\n" + + output += "1. Create an API key on this machine: " + output += lipgloss.NewStyle().Foreground(views.Green).Render("daytona server api-key new") + "\n" + output += "2. Add a profile on the client machine: \n\t" + output += lipgloss.NewStyle().Foreground(views.Green).Render(fmt.Sprintf("daytona profile add -a %s -k API_KEY", apiUrl)) + views.RenderInfoMessage(output) }, } diff --git a/pkg/cmd/server/configure.go b/pkg/cmd/server/configure.go index c9cc174509..f9e43e6e23 100644 --- a/pkg/cmd/server/configure.go +++ b/pkg/cmd/server/configure.go @@ -8,8 +8,8 @@ import ( "github.com/daytonaio/daytona/internal/util/apiclient" "github.com/daytonaio/daytona/internal/util/apiclient/server" + "github.com/daytonaio/daytona/pkg/views" server_view "github.com/daytonaio/daytona/pkg/views/server" - "github.com/daytonaio/daytona/pkg/views/util" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -35,6 +35,6 @@ var configureCmd = &cobra.Command{ log.Fatal(apiclient.HandleErrorResponse(res, err)) } - util.RenderInfoMessage("Server configuration updated. You might need to restart the server for the changes to take effect.") + views.RenderInfoMessage("Server configuration updated. You might need to restart the server for the changes to take effect.") }, } diff --git a/pkg/cmd/server/containerregistry/delete.go b/pkg/cmd/server/containerregistry/delete.go index 2f98863813..fc179e3214 100644 --- a/pkg/cmd/server/containerregistry/delete.go +++ b/pkg/cmd/server/containerregistry/delete.go @@ -11,8 +11,8 @@ import ( "github.com/daytonaio/daytona/internal/util/apiclient" "github.com/daytonaio/daytona/internal/util/apiclient/server" "github.com/daytonaio/daytona/pkg/serverapiclient" + "github.com/daytonaio/daytona/pkg/views" containerregistry_view "github.com/daytonaio/daytona/pkg/views/containerregistry" - "github.com/daytonaio/daytona/pkg/views/util" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -48,7 +48,7 @@ var containerRegistryDeleteCmd = &cobra.Command{ } if len(containerRegistries) == 0 { - util.RenderInfoMessage("No container registries found") + views.RenderInfoMessage("No container registries found") return } @@ -67,6 +67,6 @@ var containerRegistryDeleteCmd = &cobra.Command{ log.Fatal(apiclient.HandleErrorResponse(res, err)) } - util.RenderInfoMessage("Container registry deleted successfully") + views.RenderInfoMessage("Container registry deleted successfully") }, } diff --git a/pkg/cmd/server/containerregistry/list.go b/pkg/cmd/server/containerregistry/list.go index f65862b70d..a0270f5199 100644 --- a/pkg/cmd/server/containerregistry/list.go +++ b/pkg/cmd/server/containerregistry/list.go @@ -9,8 +9,8 @@ import ( "github.com/daytonaio/daytona/internal/util/apiclient" "github.com/daytonaio/daytona/internal/util/apiclient/server" "github.com/daytonaio/daytona/pkg/cmd/output" + "github.com/daytonaio/daytona/pkg/views" containerregistry_view "github.com/daytonaio/daytona/pkg/views/containerregistry/list" - "github.com/daytonaio/daytona/pkg/views/util" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -32,7 +32,7 @@ var containerRegistryListCmd = &cobra.Command{ } if len(containerRegistries) == 0 { - util.RenderInfoMessage("No container registries found. Set a new container registry by running 'daytona server container-registry set'") + views.RenderInfoMessage("No container registries found. Set a new container registry by running 'daytona server container-registry set'") return } diff --git a/pkg/cmd/server/containerregistry/set.go b/pkg/cmd/server/containerregistry/set.go index b7f10e3592..0dc82016a6 100644 --- a/pkg/cmd/server/containerregistry/set.go +++ b/pkg/cmd/server/containerregistry/set.go @@ -11,8 +11,8 @@ import ( "github.com/daytonaio/daytona/internal/util/apiclient" "github.com/daytonaio/daytona/internal/util/apiclient/server" "github.com/daytonaio/daytona/pkg/serverapiclient" + "github.com/daytonaio/daytona/pkg/views" containerregistry_view "github.com/daytonaio/daytona/pkg/views/containerregistry" - "github.com/daytonaio/daytona/pkg/views/util" "github.com/spf13/cobra" log "github.com/sirupsen/logrus" @@ -90,7 +90,7 @@ var containerRegistrySetCmd = &cobra.Command{ log.Fatal(apiclient.HandleErrorResponse(res, err)) } - util.RenderInfoMessage("Registry set successfully") + views.RenderInfoMessage("Registry set successfully") }, } diff --git a/pkg/cmd/server/provider/install.go b/pkg/cmd/server/provider/install.go index 506ec3b41e..7faf48e65d 100644 --- a/pkg/cmd/server/provider/install.go +++ b/pkg/cmd/server/provider/install.go @@ -11,8 +11,8 @@ import ( "github.com/daytonaio/daytona/pkg/os" "github.com/daytonaio/daytona/pkg/provider/manager" "github.com/daytonaio/daytona/pkg/serverapiclient" + "github.com/daytonaio/daytona/pkg/views" "github.com/daytonaio/daytona/pkg/views/provider" - view_util "github.com/daytonaio/daytona/pkg/views/util" "github.com/spf13/cobra" log "github.com/sirupsen/logrus" @@ -45,7 +45,7 @@ var providerInstallCmd = &cobra.Command{ providerList := convertToDTO(providersManifest) - providerToInstall := provider.GetProviderFromPrompt(providerList, "CHOOSE A PROVIDER TO INSTALL") + providerToInstall := provider.GetProviderFromPrompt(providerList, "Choose a provider to install") if providerToInstall == nil { return @@ -64,7 +64,7 @@ var providerInstallCmd = &cobra.Command{ log.Fatal(err) } - view_util.RenderInfoMessageBold(fmt.Sprintf("Provider %s has been successfully installed", *providerToInstall.Name)) + views.RenderInfoMessageBold(fmt.Sprintf("Provider %s has been successfully installed", *providerToInstall.Name)) }, } diff --git a/pkg/cmd/server/provider/uninstall.go b/pkg/cmd/server/provider/uninstall.go index 57b4b8f049..c242493f5b 100644 --- a/pkg/cmd/server/provider/uninstall.go +++ b/pkg/cmd/server/provider/uninstall.go @@ -9,8 +9,8 @@ import ( "github.com/daytonaio/daytona/internal/util/apiclient" "github.com/daytonaio/daytona/internal/util/apiclient/server" + "github.com/daytonaio/daytona/pkg/views" "github.com/daytonaio/daytona/pkg/views/provider" - view_util "github.com/daytonaio/daytona/pkg/views/util" "github.com/spf13/cobra" log "github.com/sirupsen/logrus" @@ -27,7 +27,7 @@ var providerUninstallCmd = &cobra.Command{ log.Fatal(err) } - providerToUninstall := provider.GetProviderFromPrompt(providerList, "CHOOSE A PROVIDER TO UNINSTALL") + providerToUninstall := provider.GetProviderFromPrompt(providerList, "Choose a provider to uninstall") if providerToUninstall == nil { return @@ -45,6 +45,6 @@ var providerUninstallCmd = &cobra.Command{ log.Fatal(apiclient.HandleErrorResponse(res, err)) } - view_util.RenderInfoMessageBold(fmt.Sprintf("Provider %s has been successfully uninstalled", *providerToUninstall.Name)) + views.RenderInfoMessageBold(fmt.Sprintf("Provider %s has been successfully uninstalled", *providerToUninstall.Name)) }, } diff --git a/pkg/cmd/server/provider/update.go b/pkg/cmd/server/provider/update.go index 65e21939e9..8f2bfc10f7 100644 --- a/pkg/cmd/server/provider/update.go +++ b/pkg/cmd/server/provider/update.go @@ -58,7 +58,7 @@ var providerUpdateCmd = &cobra.Command{ return } - providerToUpdate := provider.GetProviderFromPrompt(providerList, "CHOOSE A PROVIDER TO UPDATE") + providerToUpdate := provider.GetProviderFromPrompt(providerList, "Choose a provider to update") if providerToUpdate == nil { return } diff --git a/pkg/cmd/server/restart.go b/pkg/cmd/server/restart.go index df92bc8495..f3b9eba4ee 100644 --- a/pkg/cmd/server/restart.go +++ b/pkg/cmd/server/restart.go @@ -4,29 +4,28 @@ package server import ( - "fmt" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/daytonaio/daytona/pkg/cmd/server/daemon" + "github.com/daytonaio/daytona/pkg/views" ) var restartCmd = &cobra.Command{ Use: "restart", Short: "Restarts the Daytona Server daemon", Run: func(cmd *cobra.Command, args []string) { - fmt.Println("Stopping the Daytona Server daemon...") + views.RenderInfoMessage("Stopping the Daytona Server daemon...") err := daemon.Stop() if err != nil { log.Fatal(err) } - fmt.Println("Starting the Daytona Server daemon...") + views.RenderInfoMessage("Starting the Daytona Server daemon...") err = daemon.Start() if err != nil { log.Fatal(err) } - fmt.Println("Daytona Server daemon restarted successfully") + views.RenderInfoMessageBold("Daytona Server daemon restarted successfully") }, } diff --git a/pkg/cmd/server/server.go b/pkg/cmd/server/server.go index 2d679dff3f..49bd5e4841 100644 --- a/pkg/cmd/server/server.go +++ b/pkg/cmd/server/server.go @@ -4,7 +4,6 @@ package server import ( - "fmt" "io" "net/url" "os" @@ -31,7 +30,8 @@ import ( "github.com/daytonaio/daytona/pkg/server/headscale" "github.com/daytonaio/daytona/pkg/server/providertargets" "github.com/daytonaio/daytona/pkg/server/workspaces" - views_util "github.com/daytonaio/daytona/pkg/views/util" + "github.com/daytonaio/daytona/pkg/views" + started_view "github.com/daytonaio/daytona/pkg/views/server/started" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -50,7 +50,7 @@ var ServerCmd = &cobra.Command{ } if runAsDaemon { - fmt.Println("Starting the Daytona Server daemon...") + views.RenderInfoMessageBold("Starting the Daytona Server daemon...") err := daemon.Start() if err != nil { log.Fatal(err) @@ -59,7 +59,7 @@ var ServerCmd = &cobra.Command{ if err != nil { log.Fatal(err) } - printServerStartedMessage(c) + printServerStartedMessage(c, runAsDaemon) return } @@ -202,7 +202,7 @@ var ServerCmd = &cobra.Command{ continue } - printServerStartedMessage(c) + printServerStartedMessage(c, runAsDaemon) break } @@ -227,8 +227,9 @@ func getDaytonaScriptUrl(config *server.Config) string { return url } -func printServerStartedMessage(c *server.Config) { - views_util.RenderBorderedMessage(fmt.Sprintf("Daytona Server running on port: %d.\nYou can now begin developing locally.\n\nIf you want to connect to the server remotely:\n\n1. Create an API key on this machine:\ndaytona server api-key new\n\n2. On the client machine run:\ndaytona profile add -a %s -k API_KEY", c.ApiPort, util.GetFrpcApiUrl(c.Frps.Protocol, c.Id, c.Frps.Domain))) +func printServerStartedMessage(c *server.Config, runAsDaemon bool) { + started_view.Render(c.ApiPort, util.GetFrpcApiUrl(c.Frps.Protocol, c.Id, c.Frps.Domain), runAsDaemon) + // views_util.RenderBorderedMessage(fmt.Sprintf("Daytona Server running on port: %d.\nYou can now begin developing locally.\n\nIf you want to connect to the server remotely:\n\n1. Create an API key on this machine:\ndaytona server api-key new\n\n2. On the client machine run:\ndaytona profile add -a %s -k API_KEY", c.ApiPort, util.GetFrpcApiUrl(c.Frps.Protocol, c.Id, c.Frps.Domain))) } func getDbPath() (string, error) { diff --git a/pkg/cmd/server/stop.go b/pkg/cmd/server/stop.go index f5eb06d755..122af45694 100644 --- a/pkg/cmd/server/stop.go +++ b/pkg/cmd/server/stop.go @@ -4,19 +4,18 @@ package server import ( - "fmt" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/daytonaio/daytona/pkg/cmd/server/daemon" + "github.com/daytonaio/daytona/pkg/views" ) var stopCmd = &cobra.Command{ Use: "stop", Short: "Stops the Daytona Server daemon", Run: func(cmd *cobra.Command, args []string) { - fmt.Println("Stopping the Daytona Server daemon...") + views.RenderInfoMessageBold("Stopping the Daytona Server daemon...") err := daemon.Stop() if err != nil { log.Fatal(err) diff --git a/pkg/cmd/server/target/list.go b/pkg/cmd/server/target/list.go index a69b2f2314..8913bfabb9 100644 --- a/pkg/cmd/server/target/list.go +++ b/pkg/cmd/server/target/list.go @@ -8,8 +8,8 @@ import ( "github.com/daytonaio/daytona/internal/util/apiclient/server" "github.com/daytonaio/daytona/pkg/cmd/output" + "github.com/daytonaio/daytona/pkg/views" list_view "github.com/daytonaio/daytona/pkg/views/target/list" - view_util "github.com/daytonaio/daytona/pkg/views/util" "github.com/spf13/cobra" ) @@ -25,8 +25,8 @@ var targetListCmd = &cobra.Command{ } if len(targets) == 0 { - view_util.RenderInfoMessageBold("No targets found") - view_util.RenderInfoMessage("Use 'daytona server target set' to add a target") + views.RenderInfoMessageBold("No targets found") + views.RenderInfoMessage("Use 'daytona server target set' to add a target") return } diff --git a/pkg/cmd/server/target/remove.go b/pkg/cmd/server/target/remove.go index b01fb99aa3..d5b85ae9ad 100644 --- a/pkg/cmd/server/target/remove.go +++ b/pkg/cmd/server/target/remove.go @@ -9,8 +9,8 @@ import ( "github.com/daytonaio/daytona/cmd/daytona/config" "github.com/daytonaio/daytona/internal/util/apiclient" "github.com/daytonaio/daytona/internal/util/apiclient/server" + "github.com/daytonaio/daytona/pkg/views" "github.com/daytonaio/daytona/pkg/views/target" - "github.com/daytonaio/daytona/pkg/views/util" "github.com/spf13/cobra" log "github.com/sirupsen/logrus" @@ -52,6 +52,6 @@ var targetRemoveCmd = &cobra.Command{ log.Fatal(apiclient.HandleErrorResponse(res, err)) } - util.RenderInfoMessageBold("Target removed successfully") + views.RenderInfoMessageBold("Target removed successfully") }, } diff --git a/pkg/cmd/server/target/set.go b/pkg/cmd/server/target/set.go index 90e0f113c0..ea5c9d9de3 100644 --- a/pkg/cmd/server/target/set.go +++ b/pkg/cmd/server/target/set.go @@ -11,9 +11,9 @@ import ( "github.com/daytonaio/daytona/internal/util/apiclient" "github.com/daytonaio/daytona/internal/util/apiclient/server" "github.com/daytonaio/daytona/pkg/serverapiclient" + "github.com/daytonaio/daytona/pkg/views" "github.com/daytonaio/daytona/pkg/views/provider" "github.com/daytonaio/daytona/pkg/views/target" - "github.com/daytonaio/daytona/pkg/views/util" "github.com/spf13/cobra" log "github.com/sirupsen/logrus" @@ -40,7 +40,7 @@ var targetSetCmd = &cobra.Command{ log.Fatal(err) } - selectedProvider := provider.GetProviderFromPrompt(pluginList, "CHOOSE A PROVIDER") + selectedProvider := provider.GetProviderFromPrompt(pluginList, "Choose a provier") if selectedProvider == nil { return @@ -98,6 +98,6 @@ var targetSetCmd = &cobra.Command{ log.Fatal(apiclient.HandleErrorResponse(res, err)) } - util.RenderInfoMessage("Target set successfully") + views.RenderInfoMessage("Target set successfully") }, } diff --git a/pkg/cmd/whoami.go b/pkg/cmd/whoami.go index c5f403e022..45946abf95 100644 --- a/pkg/cmd/whoami.go +++ b/pkg/cmd/whoami.go @@ -9,7 +9,7 @@ import ( "github.com/daytonaio/daytona/cmd/daytona/config" "github.com/daytonaio/daytona/pkg/cmd/output" - view_util "github.com/daytonaio/daytona/pkg/views/util" + "github.com/daytonaio/daytona/pkg/views" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -38,14 +38,14 @@ var whoamiCmd = &cobra.Command{ } if profile.Id == "default" { - view_util.RenderInfoMessageBold("You are currently on the default profile") + views.RenderInfoMessageBold("You are currently on the default profile") } else { - view_util.RenderInfoMessageBold("You are currently on profile " + profile.Name) + views.RenderInfoMessageBold("You are currently on profile " + profile.Name) } - view_util.RenderListLine(fmt.Sprintf("\x1b[1m%-*s\x1b[0m%s", listLabelWidth, "Profile ID:", profile.Id)) + views.RenderListLine(fmt.Sprintf("\x1b[1m%-*s\x1b[0m%s", listLabelWidth, "Profile ID:", profile.Id)) if profile.Api.Url != "" { - view_util.RenderListLine(fmt.Sprintf("\x1b[1m%-*s\x1b[0m%s", listLabelWidth, "API URL:", profile.Api.Url)) + views.RenderListLine(fmt.Sprintf("\x1b[1m%-*s\x1b[0m%s", listLabelWidth, "API URL:", profile.Api.Url)) } }, } diff --git a/pkg/cmd/workspace/code.go b/pkg/cmd/workspace/code.go index 17e1dd00cc..6f99299d76 100644 --- a/pkg/cmd/workspace/code.go +++ b/pkg/cmd/workspace/code.go @@ -13,7 +13,7 @@ import ( "github.com/daytonaio/daytona/internal/util/apiclient" "github.com/daytonaio/daytona/internal/util/apiclient/server" "github.com/daytonaio/daytona/pkg/ide" - view_util "github.com/daytonaio/daytona/pkg/views/util" + "github.com/daytonaio/daytona/pkg/views" "github.com/daytonaio/daytona/pkg/views/workspace/selection" log "github.com/sirupsen/logrus" @@ -86,7 +86,7 @@ var CodeCmd = &cobra.Command{ ideId = ideFlag } - view_util.RenderInfoMessage(fmt.Sprintf("Opening the workspace project '%s' in your preferred IDE.", projectName)) + views.RenderInfoMessage(fmt.Sprintf("Opening the workspace project '%s' in your preferred IDE.", projectName)) err = openIDE(ideId, activeProfile, workspaceId, projectName) if err != nil { diff --git a/pkg/cmd/workspace/create.go b/pkg/cmd/workspace/create.go index 3b49f7fd18..06bbd680bd 100644 --- a/pkg/cmd/workspace/create.go +++ b/pkg/cmd/workspace/create.go @@ -9,7 +9,6 @@ import ( "fmt" "net/url" "os" - "strings" "time" tea "github.com/charmbracelet/bubbletea" @@ -19,8 +18,8 @@ import ( "github.com/daytonaio/daytona/internal/util/apiclient/server" workspace_util "github.com/daytonaio/daytona/pkg/cmd/workspace/util" "github.com/daytonaio/daytona/pkg/serverapiclient" + "github.com/daytonaio/daytona/pkg/views" "github.com/daytonaio/daytona/pkg/views/target" - view_util "github.com/daytonaio/daytona/pkg/views/util" "github.com/daytonaio/daytona/pkg/views/workspace/info" status "github.com/daytonaio/daytona/pkg/views/workspace/status" "github.com/daytonaio/daytona/pkg/workspace" @@ -65,8 +64,6 @@ var CreateCmd = &cobra.Command{ processCmdArguments(cmd, args, apiClient, &workspaceName, &projects, ctx) } - view_util.RenderMainTitle("WORKSPACE CREATION") - if workspaceName == "" || len(projects) == 0 { log.Fatal("workspace name and repository urls are required") return @@ -131,7 +128,7 @@ var CreateCmd = &cobra.Command{ dialStartTime := time.Now() dialTimeout := 3 * time.Minute - statusProgram.Send(status.ResultMsg{Line: "Establishing connection with the workspace"}) + // statusProgram.Send(status.Message{Line: "Establishing connection with the workspace"}) waitForDial(tsConn, *createdWorkspace.Id, *createdWorkspace.Projects[0].Name, dialStartTime, dialTimeout, statusProgram) @@ -145,22 +142,31 @@ var CreateCmd = &cobra.Command{ return } + chosenIdeId := c.DefaultIdeId + if ideFlag != "" { + chosenIdeId = ideFlag + } + + ideList := config.GetIdeList() + var chosenIde config.Ide + + for _, ide := range ideList { + if ide.Id == chosenIdeId { + chosenIde = ide + } + } + fmt.Println() - info.Render(wsInfo) + info.Render(wsInfo, chosenIde.Name) - codeFlag, _ := cmd.Flags().GetBool("code") if !codeFlag { + views.RenderCreationInfoMessage("Run 'daytona code' when you're ready to start developing") return } - ide := c.DefaultIdeId - if ideFlag != "" { - ide = ideFlag - } + views.RenderCreationInfoMessage("Opening the workspace in your preferred editor ...") - view_util.RenderInfoMessage(fmt.Sprintf("Opening the workspace project '%s' in your preferred IDE.", *wsInfo.Projects[0].Name)) - - err = openIDE(ide, activeProfile, *createdWorkspace.Id, *wsInfo.Projects[0].Name) + err = openIDE(chosenIdeId, activeProfile, *createdWorkspace.Id, *wsInfo.Projects[0].Name) if err != nil { log.Fatal(err) } @@ -169,15 +175,18 @@ var CreateCmd = &cobra.Command{ var providerFlag string var targetNameFlag string +var manualFlag bool +var multiProjectFlag bool +var codeFlag bool func init() { CreateCmd.Flags().StringArrayVarP(&argRepos, "repo", "r", nil, "Set the repository url") CreateCmd.Flags().StringVar(&providerFlag, "provider", "", "Specify the provider (e.g. 'docker-provider')") CreateCmd.Flags().StringVarP(&ideFlag, "ide", "i", "", "Specify the IDE ('vscode' or 'browser')") CreateCmd.Flags().StringVarP(&targetNameFlag, "target", "t", "", "Specify the target (e.g. 'local')") - CreateCmd.Flags().Bool("manual", false, "Manually enter the git repositories") - CreateCmd.Flags().Bool("multi-project", false, "Workspace with multiple projects/repos") - CreateCmd.Flags().BoolP("code", "c", false, "Open the workspace in the IDE after workspace creation") + CreateCmd.Flags().BoolVar(&manualFlag, "manual", false, "Manually enter the git repositories") + CreateCmd.Flags().BoolVar(&multiProjectFlag, "multi-project", false, "Workspace with multiple projects/repos") + CreateCmd.Flags().BoolVarP(&multiProjectFlag, "code", "c", false, "Open the workspace in the IDE after workspace creation") } func getTarget(activeProfileName string) (*serverapiclient.ProviderTarget, error) { @@ -203,15 +212,6 @@ func getTarget(activeProfileName string) (*serverapiclient.ProviderTarget, error } func processPrompting(cmd *cobra.Command, apiClient *serverapiclient.APIClient, workspaceName *string, projects *[]serverapiclient.CreateWorkspaceRequestProject, ctx context.Context) { - manualFlag, err := cmd.Flags().GetBool("manual") - if err != nil { - log.Fatal(err) - } - multiProjectFlag, err := cmd.Flags().GetBool("multi-project") - if err != nil { - log.Fatal(err) - } - gitProviders, res, err := apiClient.GitProviderAPI.ListGitProviders(ctx).Execute() if err != nil { log.Fatal(apiclient.HandleErrorResponse(res, err)) @@ -220,7 +220,7 @@ func processPrompting(cmd *cobra.Command, apiClient *serverapiclient.APIClient, var workspaceNames []string if argRepos != nil { - view_util.RenderInfoMessage("Error: workspace name argument is required for this command") + views.RenderInfoMessage("Error: workspace name argument is required for this command") err := cmd.Help() if err != nil { log.Fatal(err) @@ -260,7 +260,7 @@ func processCmdArguments(cmd *cobra.Command, args []string, apiClient *serverapi if argRepos != nil { repoUrls = argRepos } else { - view_util.RenderInfoMessage("Error: --repo flag is required for this command") + views.RenderInfoMessage("Error: --repo flag is required for this command") err := cmd.Help() if err != nil { log.Fatal(err) @@ -305,15 +305,44 @@ func scanWorkspaceLogs(activeProfile config.Profile, workspaceId string, statusP return } - messages := strings.Split(string(msg), "\r") - for _, msg := range messages { - statusProgram.Send(status.ResultMsg{Line: msg}) + delimiter := byte('\r') + messages := splitWithDelimiter(string(msg), delimiter) + + for _, message := range messages { + line := fmt.Sprintf("%s %s", views.CheckmarkSymbol, message) + fmt.Print(line) } if *started { - statusProgram.Send(status.ResultMsg{Line: "END_SIGNAL"}) + fmt.Print("\nWorkspace creation complete.") + statusProgram.Send(status.Message{Line: "END_SIGNAL"}) break } + if len(messages) != 0 { + fmt.Print("\n") + } + } +} + +func splitWithDelimiter(s string, delimiter byte) []string { + var parts []string + var buffer []byte + + for i := 0; i < len(s); i++ { + if s[i] == delimiter { + parts = append(parts, string(buffer)) + buffer = nil + parts = append(parts, string(delimiter)) + } else { + buffer = append(buffer, s[i]) + } } + + // Add the remaining characters in the buffer + if len(buffer) > 0 { + parts = append(parts, string(buffer)) + } + + return parts } func waitForDial(tsConn *tsnet.Server, workspaceId string, projectName string, dialStartTime time.Time, dialTimeout time.Duration, statusProgram *tea.Program) { diff --git a/pkg/cmd/workspace/delete.go b/pkg/cmd/workspace/delete.go index 934e1e3d0e..9b7dd8e2eb 100644 --- a/pkg/cmd/workspace/delete.go +++ b/pkg/cmd/workspace/delete.go @@ -13,8 +13,6 @@ import ( "github.com/daytonaio/daytona/internal/util/apiclient/server" "github.com/daytonaio/daytona/pkg/serverapiclient" "github.com/daytonaio/daytona/pkg/views" - "github.com/daytonaio/daytona/pkg/views/util" - view_util "github.com/daytonaio/daytona/pkg/views/util" "github.com/daytonaio/daytona/pkg/views/workspace/selection" log "github.com/sirupsen/logrus" @@ -144,7 +142,7 @@ func DeleteAllWorkspaces() error { log.Errorf("Failed to delete workspace %s: %v", *workspace.Id, apiclient.HandleErrorResponse(res, err)) continue } - view_util.RenderLine(fmt.Sprintf("- Workspace %s successfully deleted\n", *workspace.Name)) + views.RenderLine(fmt.Sprintf("- Workspace %s successfully deleted\n", *workspace.Name)) } return nil } @@ -170,6 +168,6 @@ func removeWorkspace(ctx context.Context, apiClient *serverapiclient.APIClient, log.Fatal(err) } - util.RenderInfoMessage(fmt.Sprintf("Workspace %s successfully deleted", *workspace.Name)) + views.RenderInfoMessage(fmt.Sprintf("Workspace %s successfully deleted", *workspace.Name)) return nil } diff --git a/pkg/cmd/workspace/info.go b/pkg/cmd/workspace/info.go index 5235b5dd39..28eadf2fd8 100644 --- a/pkg/cmd/workspace/info.go +++ b/pkg/cmd/workspace/info.go @@ -61,7 +61,7 @@ var InfoCmd = &cobra.Command{ return } - info.Render(workspace) + info.Render(workspace, "") }, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) > 0 { diff --git a/pkg/cmd/workspace/list.go b/pkg/cmd/workspace/list.go index 593b6eabd7..1644c3f3d8 100644 --- a/pkg/cmd/workspace/list.go +++ b/pkg/cmd/workspace/list.go @@ -9,7 +9,7 @@ import ( "github.com/daytonaio/daytona/internal/util/apiclient" "github.com/daytonaio/daytona/internal/util/apiclient/server" "github.com/daytonaio/daytona/pkg/cmd/output" - views_util "github.com/daytonaio/daytona/pkg/views/util" + "github.com/daytonaio/daytona/pkg/views" list_view "github.com/daytonaio/daytona/pkg/views/workspace/list" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -52,7 +52,7 @@ var ListCmd = &cobra.Command{ } if len(workspaceList) == 0 { - views_util.RenderInfoMessage("The workspace list is empty. Start off by running 'daytona create'.") + views.RenderInfoMessage("The workspace list is empty. Start off by running 'daytona create'.") return } diff --git a/pkg/cmd/workspace/start.go b/pkg/cmd/workspace/start.go index 4aed58310c..46f5b17f9f 100644 --- a/pkg/cmd/workspace/start.go +++ b/pkg/cmd/workspace/start.go @@ -9,7 +9,7 @@ import ( "github.com/daytonaio/daytona/internal/util/apiclient" "github.com/daytonaio/daytona/internal/util/apiclient/server" - "github.com/daytonaio/daytona/pkg/views/util" + "github.com/daytonaio/daytona/pkg/views" "github.com/daytonaio/daytona/pkg/views/workspace/selection" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -72,7 +72,7 @@ var StartCmd = &cobra.Command{ } } - util.RenderInfoMessage(fmt.Sprintf("Workspace %s successfully started", workspaceId)) + views.RenderInfoMessage(fmt.Sprintf("Workspace %s successfully started", workspaceId)) }, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) != 0 { diff --git a/pkg/cmd/workspace/stop.go b/pkg/cmd/workspace/stop.go index d395d20bb7..7bc91febec 100644 --- a/pkg/cmd/workspace/stop.go +++ b/pkg/cmd/workspace/stop.go @@ -11,7 +11,7 @@ import ( internal_util "github.com/daytonaio/daytona/internal/util" "github.com/daytonaio/daytona/internal/util/apiclient" "github.com/daytonaio/daytona/internal/util/apiclient/server" - "github.com/daytonaio/daytona/pkg/views/util" + "github.com/daytonaio/daytona/pkg/views" "github.com/daytonaio/daytona/pkg/views/workspace/selection" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -69,7 +69,7 @@ var StopCmd = &cobra.Command{ } } - util.RenderInfoMessage(fmt.Sprintf("Workspace %s successfully stopped", workspaceId)) + views.RenderInfoMessage(fmt.Sprintf("Workspace %s successfully stopped", workspaceId)) }, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) >= 1 { diff --git a/pkg/cmd/workspace/util/repository_wizard.go b/pkg/cmd/workspace/util/repository_wizard.go index 770ae469ae..a1a31a0659 100644 --- a/pkg/cmd/workspace/util/repository_wizard.go +++ b/pkg/cmd/workspace/util/repository_wizard.go @@ -12,7 +12,7 @@ import ( "github.com/daytonaio/daytona/internal/util/apiclient/server" "github.com/daytonaio/daytona/pkg/serverapiclient" gitprovider_view "github.com/daytonaio/daytona/pkg/views/gitprovider" - view_util "github.com/daytonaio/daytona/pkg/views/util" + views_util "github.com/daytonaio/daytona/pkg/views/util" "github.com/daytonaio/daytona/pkg/views/workspace/selection" ) @@ -58,7 +58,7 @@ func getRepositoryFromWizard(userGitProviders []serverapiclient.GitProvider, sec var namespaceList []serverapiclient.GitNamespace - err = view_util.With(func() error { + err = views_util.With(func() error { namespaceList, _, err = apiClient.GitProviderAPI.GetNamespaces(ctx, providerId).Execute() return err }) @@ -76,7 +76,7 @@ func getRepositoryFromWizard(userGitProviders []serverapiclient.GitProvider, sec } var providerRepos []serverapiclient.GitRepository - err = view_util.With(func() error { + err = views_util.With(func() error { providerRepos, _, err = apiClient.GitProviderAPI.GetRepositories(ctx, providerId, namespaceId).Execute() return err }) @@ -91,7 +91,7 @@ func getRepositoryFromWizard(userGitProviders []serverapiclient.GitProvider, sec } var branchList []serverapiclient.GitBranch - err = view_util.With(func() error { + err = views_util.With(func() error { branchList, _, err = apiClient.GitProviderAPI.GetRepoBranches(ctx, providerId, namespaceId, *chosenRepo.Id).Execute() return err }) @@ -116,7 +116,7 @@ func getRepositoryFromWizard(userGitProviders []serverapiclient.GitProvider, sec } var prList []serverapiclient.GitPullRequest - err = view_util.With(func() error { + err = views_util.With(func() error { prList, _, err = apiClient.GitProviderAPI.GetRepoPRs(ctx, providerId, namespaceId, *chosenRepo.Id).Execute() return err }) diff --git a/pkg/ide/browser.go b/pkg/ide/browser.go index 4b75a4affa..eca9e39489 100644 --- a/pkg/ide/browser.go +++ b/pkg/ide/browser.go @@ -13,7 +13,7 @@ import ( "github.com/daytonaio/daytona/cmd/daytona/config" "github.com/daytonaio/daytona/internal/util" "github.com/daytonaio/daytona/pkg/ports" - view_util "github.com/daytonaio/daytona/pkg/views/util" + "github.com/daytonaio/daytona/pkg/views" "github.com/pkg/browser" log "github.com/sirupsen/logrus" @@ -28,7 +28,7 @@ func OpenBrowserIDE(activeProfile config.Profile, workspaceId string, projectNam return err } - view_util.RenderInfoMessageBold("Downloading OpenVSCode Server...") + views.RenderInfoMessageBold("Downloading OpenVSCode Server...") projectHostname := config.GetProjectHostname(activeProfile.Id, workspaceId, projectName) installServerCommand := exec.Command("ssh", projectHostname, "curl -fsSL https://download.daytona.io/daytona/get-openvscode-server.sh | sh") @@ -40,7 +40,7 @@ func OpenBrowserIDE(activeProfile config.Profile, workspaceId string, projectNam return err } - view_util.RenderInfoMessageBold("Starting OpenVSCode Server...") + views.RenderInfoMessageBold("Starting OpenVSCode Server...") go func() { startServerCommand := exec.CommandContext(context.Background(), "ssh", projectHostname, startVSCodeServerCommand) @@ -70,7 +70,7 @@ func OpenBrowserIDE(activeProfile config.Profile, workspaceId string, projectNam time.Sleep(500 * time.Millisecond) } - view_util.RenderInfoMessageBold(fmt.Sprintf("Forwarded %s IDE port to %s.\nOpening browser...\n", projectName, ideURL)) + views.RenderInfoMessageBold(fmt.Sprintf("Forwarded %s IDE port to %s.\nOpening browser...\n", projectName, ideURL)) err = browser.OpenURL(ideURL) if err != nil { diff --git a/pkg/ide/jetbrains.go b/pkg/ide/jetbrains.go index 5420bfc106..d410d77bfb 100644 --- a/pkg/ide/jetbrains.go +++ b/pkg/ide/jetbrains.go @@ -15,7 +15,7 @@ import ( "github.com/daytonaio/daytona/internal/jetbrains" "github.com/daytonaio/daytona/internal/util" ospkg "github.com/daytonaio/daytona/pkg/os" - view_util "github.com/daytonaio/daytona/pkg/views/util" + "github.com/daytonaio/daytona/pkg/views" "github.com/pkg/browser" ) @@ -62,11 +62,11 @@ func OpenJetbrainsIDE(activeProfile config.Profile, ide, workspaceId, projectNam func downloadJetbrainsIDE(projectHostname, downloadUrl, downloadPath string) error { if isAlreadyDownloaded(projectHostname, downloadPath) { - view_util.RenderInfoMessage("JetBrains IDE already downloaded. Opening...") + views.RenderInfoMessage("JetBrains IDE already downloaded. Opening...") return nil } - view_util.RenderInfoMessage(fmt.Sprintf("Downloading the IDE into the project from %s...", downloadUrl)) + views.RenderInfoMessage(fmt.Sprintf("Downloading the IDE into the project from %s...", downloadUrl)) downloadIdeCmd := exec.Command("ssh", projectHostname, fmt.Sprintf("mkdir -p %s && wget -q --show-progress --progress=bar:force -pO- %s | tar -xzC %s --strip-components=1", downloadPath, downloadUrl, downloadPath)) downloadIdeCmd.Stdout = os.Stdout @@ -77,7 +77,7 @@ func downloadJetbrainsIDE(projectHostname, downloadUrl, downloadPath string) err return err } - view_util.RenderInfoMessage("IDE downloaded. Opening...") + views.RenderInfoMessage("IDE downloaded. Opening...") return nil } diff --git a/pkg/views/containerregistry/select.go b/pkg/views/containerregistry/select.go index b3a703a409..4c6aad11cf 100644 --- a/pkg/views/containerregistry/select.go +++ b/pkg/views/containerregistry/select.go @@ -13,7 +13,6 @@ import ( "github.com/daytonaio/daytona/internal/util" "github.com/daytonaio/daytona/pkg/serverapiclient" "github.com/daytonaio/daytona/pkg/views" - view_util "github.com/daytonaio/daytona/pkg/views/util" ) const NewRegistryServerIdentifier = "+ New Container Registry" @@ -39,8 +38,8 @@ func GetRegistryFromPrompt(registries []serverapiclient.ContainerRegistry, activ l := views.GetStyledSelectList(items) m := model{list: l} - m.list.Title = "CHOOSE A REGISTRY" - m.footer = view_util.GetListFooter(activeProfileName) + m.list.Title = "Choose a container registry" + m.footer = views.GetListFooter(activeProfileName) p, err := tea.NewProgram(m, tea.WithAltScreen()).Run() if err != nil { diff --git a/pkg/views/containerregistry/view.go b/pkg/views/containerregistry/view.go index 6ea037ac5d..ea39ec9b16 100644 --- a/pkg/views/containerregistry/view.go +++ b/pkg/views/containerregistry/view.go @@ -7,7 +7,7 @@ import ( "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/daytonaio/daytona/pkg/serverapiclient" - view_util "github.com/daytonaio/daytona/pkg/views/util" + "github.com/daytonaio/daytona/pkg/views" ) type item struct { @@ -48,7 +48,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit } case tea.WindowSizeMsg: - h, v := view_util.DocStyle.GetFrameSize() + h, v := views.DocStyle.GetFrameSize() m.list.SetSize(msg.Width-h, msg.Height-v) } @@ -58,5 +58,5 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m model) View() string { - return view_util.DocStyle.Render(m.list.View() + m.footer) + return views.DocStyle.Render(m.list.View() + m.footer) } diff --git a/pkg/views/gitprovider/select.go b/pkg/views/gitprovider/select.go index 5bc2eff932..053d57d07c 100644 --- a/pkg/views/gitprovider/select.go +++ b/pkg/views/gitprovider/select.go @@ -12,7 +12,6 @@ import ( "github.com/daytonaio/daytona/cmd/daytona/config" "github.com/daytonaio/daytona/pkg/serverapiclient" "github.com/daytonaio/daytona/pkg/views" - view_util "github.com/daytonaio/daytona/pkg/views/util" ) type GitProviderView struct { @@ -96,7 +95,7 @@ func GitProviderSelectionView(gitProviderAddView *serverapiclient.GitProvider, u ).WithHide(isDeleting), ).WithTheme(views.GetCustomTheme()) - view_util.RenderInfoMessage(fmt.Sprintf("More information on:\n%s", config.GetDocsLinkFromGitProvider(*gitProviderAddView.Id))) + views.RenderInfoMessage(fmt.Sprintf("More information on:\n%s", config.GetDocsLinkFromGitProvider(*gitProviderAddView.Id))) err = userDataForm.Run() if err != nil { diff --git a/pkg/views/globals.go b/pkg/views/globals.go new file mode 100644 index 0000000000..451c0dfcaf --- /dev/null +++ b/pkg/views/globals.go @@ -0,0 +1,113 @@ +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package views + +import ( + "fmt" + "os" + + "github.com/charmbracelet/lipgloss" + "golang.org/x/term" +) + +var DocStyle = lipgloss. + NewStyle(). + Margin(3, 2, 1, 2). + Padding(1, 2). + BorderForeground(LightGray). + Border(lipgloss.RoundedBorder()) + +var DefaultLayoutMarginTop = 1 + +var CheckmarkSymbol = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).SetString("✓") + +func RenderMainTitle(title string) { + fmt.Println(lipgloss.NewStyle().Foreground(Green).Bold(true).Padding(1, 0, 1, 0).Render(title)) +} + +func RenderLine(message string) { + fmt.Println(lipgloss.NewStyle().PaddingLeft(1).Render(message)) +} + +func RenderInfoMessage(message string) { + fmt.Println(lipgloss.NewStyle().Padding(1, 0, 1, 1).Render(message)) +} + +func RenderCreationInfoMessage(message string) { + fmt.Println(lipgloss.NewStyle().Foreground(Gray).Padding(1, 0, 1, 1).Render(message)) +} + +func RenderListLine(message string) { + fmt.Println(lipgloss.NewStyle().Padding(0, 0, 1, 1).Render(message)) +} + +func RenderInfoMessageBold(message string) { + fmt.Println(lipgloss.NewStyle().Bold(true).Padding(1, 0, 1, 1).Render(message)) +} + +func GetListFooter(profileName string) string { + return lipgloss.NewStyle().Bold(true).PaddingLeft(2).Render("\n\nActive profile: " + profileName) +} + +func RenderBorderedMessage(message string) { + fmt.Println(GetBorderedMessage(message)) +} + +func GetBorderedMessage(message string) string { + return lipgloss. + NewStyle(). + Margin(1, 0). + Padding(1, 2). + BorderForeground(LightGray). + Border(lipgloss.RoundedBorder()). + Render(message) +} + +func GetStyledMainTitle(content string) string { + return lipgloss.NewStyle().Foreground(Black).Background(lipgloss.Color("#fff")).Padding(1, 2).Render(content) + + sidePadding := 2 + topBorder := "" + bottomBorder := "" + + title := lipgloss.NewStyle().Foreground(Black).Bold(true). + Background(lipgloss.Color("#fff")).Padding(0, sidePadding).Render(content) + + for i := 0; i < sidePadding+len(content)+sidePadding; i++ { + topBorder = topBorder + "▔" + // topBorder = topBorder + "▀" + bottomBorder = bottomBorder + "▁" + // bottomBorder = bottomBorder + "▂" + // bottomBorder = bottomBorder + "▄" + } + + title = title + "\n" + lipgloss.NewStyle().Foreground(lipgloss.Color("#fff")).Render(topBorder) + title = lipgloss.NewStyle().Foreground(lipgloss.Color("#fff")).Render(bottomBorder) + "\n" + title + + return title +} + +func GetSeparatorString() string { + return lipgloss.NewStyle().Foreground(LightGray).Render("===") +} + +func RenderDefaultIdeUpdated(ide string) { + + content := fmt.Sprintf("%s %s", lipgloss.NewStyle().Foreground(LightGray).Render("Default IDE: "), ide) + + terminalWidth, _, err := term.GetSize(int(os.Stdout.Fd())) + if err != nil { + fmt.Println(content) + return + } + + fmt.Println(lipgloss. + NewStyle(). + Margin(1, 0). + Padding(1, 0, 1, 2). + BorderForeground(LightGray). + Border(lipgloss.RoundedBorder()). + Width(terminalWidth - 10). + Render(content)) +} diff --git a/pkg/views/ide/view.go b/pkg/views/ide/view.go index 2b8bc304aa..e0ca56ac17 100644 --- a/pkg/views/ide/view.go +++ b/pkg/views/ide/view.go @@ -12,6 +12,7 @@ import ( "github.com/daytonaio/daytona/cmd/daytona/config" "github.com/daytonaio/daytona/internal/util" "github.com/daytonaio/daytona/pkg/views" + "golang.org/x/term" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" @@ -25,7 +26,6 @@ var ( paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) helpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1) quitTextStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4) - docStyle = lipgloss.NewStyle().Margin(1, 2) ) type item struct { @@ -53,10 +53,10 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list if isSelected { selectedItemStyle := lipgloss.NewStyle(). Border(lipgloss.NormalBorder(), false, false, false, true). - BorderForeground(views.Blue). + BorderForeground(views.TempGreen). Bold(true). Padding(0, 0, 0, 1) - ideString = selectedItemStyle.Copy().Foreground(views.Blue).Render(i.name) + ideString = selectedItemStyle.Copy().Foreground(views.TempGreen).Render(i.name) } s.WriteString(ideString) s.WriteRune('\n') @@ -64,9 +64,10 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list } type model struct { - list list.Model - choice item - quitting bool + list list.Model + choice item + quitting bool + initialWidthSet bool } func (m model) Init() tea.Cmd { @@ -74,9 +75,16 @@ func (m model) Init() tea.Cmd { } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if !m.initialWidthSet { + _, _, err := term.GetSize(int(os.Stdout.Fd())) + if err != nil { + m.list.SetWidth(150) + } + } + switch msg := msg.(type) { case tea.WindowSizeMsg: - h, v := docStyle.GetFrameSize() + h, v := views.DocStyle.GetFrameSize() m.list.SetSize(msg.Width-h, msg.Height-v) case tea.KeyMsg: @@ -106,7 +114,13 @@ func (m model) View() string { if m.quitting { return quitTextStyle.Render("Canceled") } - return docStyle.Render(m.list.View()) + + terminalWidth, terminalHeight, err := term.GetSize(int(os.Stdout.Fd())) + if err != nil { + return "" + } + + return views.DocStyle.Width(terminalWidth - 4).Height(terminalHeight - 4).Render(m.list.View()) } func Render(ideList []config.Ide, choiceChan chan<- string) { @@ -115,7 +129,7 @@ func Render(ideList []config.Ide, choiceChan chan<- string) { }) l := list.New(items, itemDelegate{}, 0, 0) - l.Title = lipgloss.NewStyle().Foreground(views.Green).Bold(true).Render("Choose your default IDE") + l.Title = views.GetStyledMainTitle("Choose your default IDE") l.SetShowStatusBar(false) l.SetFilteringEnabled(false) l.Styles.Title = titleStyle diff --git a/pkg/views/initial/view.go b/pkg/views/initial/view.go new file mode 100644 index 0000000000..eab51a8e13 --- /dev/null +++ b/pkg/views/initial/view.go @@ -0,0 +1,178 @@ +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package daytona + +import ( + "errors" + "fmt" + "os" + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" + "github.com/daytonaio/daytona/internal" + "github.com/daytonaio/daytona/pkg/views" +) + +const maxWidth = 80 + +var sigilParts = []string{ + " -#####= ", + " -######- ", + " +###= -######: ", + " ####* -#####%-............. ", + " ####*######:=##############- ", + " ####* =%#- =##############- ", + " :*%= ####* ....:*#:...... ", + "=####%==+++- +####*. ", + " :*####%= =%####+. ", + " .*####%= =%####+. ", + " .*###*. .####-=%####*. ", + " :::::=%+::::: . .####: =%##%- ", + " ############# .*#%-.####: +- ", + " %%%%%%%%%%%%%.*####%=####: ", + " .*####%= .####: ", + " .*####%= .####: ", + " +%##%= ****:"} + +var gradientColors = []lipgloss.AdaptiveColor{ + {Light: "#000", Dark: "#fff"}, + {Light: "#000", Dark: "#fff"}, + {Light: "#000", Dark: "#fff"}, + {Light: "#000", Dark: "#fff"}, + {Light: "#000", Dark: "#fff"}, + {Light: "#000", Dark: "#fff"}, + {Light: "#000", Dark: "#fff"}, + {Light: "#000", Dark: "#fff"}, + {Light: "#000", Dark: "#fff"}, + {Light: "#B9B9B9", Dark: "#B9B9B9"}, + {Light: "#B2B2B2", Dark: "#B2B2B2"}, + {Light: "#A4A4A4", Dark: "#A4A4A4"}, + {Light: "#969696", Dark: "#969696"}, + {Light: "#585858", Dark: "#585858"}, + {Light: "#363636", Dark: "#363636"}, + {Light: "#343434", Dark: "#343434"}, + {Light: "#343434", Dark: "#343434"}, +} + +var gradientSigil string + +type CommandView struct { + Command string + Name string + Desc string +} + +var commandViews []CommandView = []CommandView{ + {Command: "list", Name: "daytona list", Desc: "(list all workspaces)"}, + {Command: "profile add", Name: "daytona profile add", Desc: "(run on client machine)"}, + {Command: "server api-key new", Name: "daytona server api-key new", Desc: "(create API key on this machine)"}, + {Command: "create", Name: "daytona create", Desc: "(create new workspace)\n"}, + {Command: "help", Name: "view all commands", Desc: ""}, +} + +type Model struct { + form *huh.Form + width int + leftContainer string + choice string +} + +func NewModel() Model { + m := Model{width: maxWidth} + + var options []huh.Option[string] + + for _, commandView := range commandViews { + options = append(options, huh.Option[string]{Key: fmt.Sprintf("%s %s", commandView.Name, lipgloss.NewStyle().Foreground(views.LightGray).Render(commandView.Desc)), Value: commandView.Command}) + } + + form := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Key("command"). + Options(options...). + Value(&m.choice), + ), + ).WithTheme(views.GetInitialCommandTheme()).WithShowHelp(false) + + m.form = form + + return m +} + +func (m Model) Init() tea.Cmd { + return m.form.Init() +} + +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = maxWidth + case tea.KeyMsg: + switch keypress := msg.String(); keypress { + case "enter": + // m.form.State = huh.StateCompleted + case "q", "ctrl+c": + return m, tea.Quit + } + } + var cmds []tea.Cmd + + form, cmd := m.form.Update(msg) + if f, ok := form.(*huh.Form); ok { + m.form = f + cmds = append(cmds, cmd) + } + + if m.form.State == huh.StateCompleted { + command := m.form.GetString("command") + m.choice = command + cmds = append(cmds, tea.Quit) + } + + return m, tea.Batch(cmds...) +} + +func (m Model) View() string { + switch m.form.State { + case huh.StateCompleted: + return "" + default: + v := strings.TrimSuffix(m.form.View(), "\n\n") + formHeader := "\n" + formHeader += lipgloss.NewStyle().Foreground(views.Green).Render("Daytona") + formHeader += "\n" + formHeader += "The future of dev environemnets\n\n" + formHeader += internal.Version + "\n\n" + formHeader += "-------------------------------------------------------------\n\n" + formHeader += "Get started\n" + form := v + + body := lipgloss.JoinHorizontal(lipgloss.Top, gradientSigil, formHeader+form) + + return lipgloss.NewStyle().Padding(2, 0, 2, 2).Render(body) + } +} + +func GetCommand() (string, error) { + for _, line := range sigilParts { + gradientSigil += lipgloss.NewStyle().Foreground(gradientColors[0]).Render(line) + "\n" + gradientColors = gradientColors[1:] + } + + m := NewModel() + p, err := tea.NewProgram(m).Run() + if err != nil { + fmt.Println("Oh no:", err) + os.Exit(1) + } + + if m, ok := p.(Model); ok && m.choice != "" { + return m.choice, nil + } + + return "", errors.New("no command selected") +} diff --git a/pkg/views/profile/list.go b/pkg/views/profile/list.go index a57b9a43ca..8b4cefeb1e 100644 --- a/pkg/views/profile/list.go +++ b/pkg/views/profile/list.go @@ -10,7 +10,6 @@ import ( "github.com/daytonaio/daytona/cmd/daytona/config" "github.com/daytonaio/daytona/pkg/views" - "github.com/daytonaio/daytona/pkg/views/util" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" @@ -109,7 +108,7 @@ func renderProfileList(profileList []config.Profile, activeProfileId string, sel } func GetProfileIdFromPrompt(profileList []config.Profile, activeProfileId, title string, withCreateOption bool) string { - util.RenderMainTitle(title) + views.RenderMainTitle(title) withNewProfile := profileList @@ -135,7 +134,7 @@ func GetProfileIdFromPrompt(profileList []config.Profile, activeProfileId, title } func ListProfiles(profileList []config.Profile, activeProfileId string) { - util.RenderMainTitle("PROFILES") + views.RenderMainTitle("PROFILES") modelInstance := renderProfileList(profileList, activeProfileId, false) diff --git a/pkg/views/provider/list.go b/pkg/views/provider/list.go index 9d9f797ab7..7024166bb0 100644 --- a/pkg/views/provider/list.go +++ b/pkg/views/provider/list.go @@ -9,12 +9,10 @@ import ( "github.com/daytonaio/daytona/pkg/serverapiclient" "github.com/daytonaio/daytona/pkg/views" - "github.com/daytonaio/daytona/pkg/views/util" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - view_util "github.com/daytonaio/daytona/pkg/views/util" "golang.org/x/term" ) @@ -104,10 +102,10 @@ func renderProvidersList(providers []serverapiclient.Provider, selectable bool) } func List(providers []serverapiclient.Provider) { - util.RenderMainTitle("PROVIDERS") + views.RenderMainTitle("PROVIDERS") if len(providers) == 0 { - view_util.RenderInfoMessage("No providers found") + views.RenderInfoMessage("No providers found") return } diff --git a/pkg/views/provider/select.go b/pkg/views/provider/select.go index 71c42972cd..6349d407ce 100644 --- a/pkg/views/provider/select.go +++ b/pkg/views/provider/select.go @@ -11,15 +11,14 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/daytonaio/daytona/pkg/serverapiclient" - "github.com/daytonaio/daytona/pkg/views/util" - view_util "github.com/daytonaio/daytona/pkg/views/util" + "github.com/daytonaio/daytona/pkg/views" ) func GetProviderFromPrompt(providers []serverapiclient.Provider, title string) *serverapiclient.Provider { - util.RenderMainTitle(title) + views.RenderMainTitle(title) if len(providers) == 0 { - view_util.RenderInfoMessage("No providers found") + views.RenderInfoMessage("No providers found") return nil } diff --git a/pkg/views/server/apikey/list.go b/pkg/views/server/apikey/list.go index 6ca7667f7e..490eab00e3 100644 --- a/pkg/views/server/apikey/list.go +++ b/pkg/views/server/apikey/list.go @@ -10,7 +10,6 @@ import ( "github.com/daytonaio/daytona/pkg/serverapiclient" "github.com/daytonaio/daytona/pkg/views" - "github.com/daytonaio/daytona/pkg/views/util" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" @@ -103,7 +102,7 @@ func renderApiKeyList(apiKeyList []serverapiclient.ApiKey, selectable bool) mode } func GetApiKeyFromPrompt(apiKeyList []serverapiclient.ApiKey, title string) *serverapiclient.ApiKey { - util.RenderMainTitle(title) + views.RenderMainTitle(title) modelInstance := renderApiKeyList(apiKeyList, true) @@ -120,7 +119,7 @@ func GetApiKeyFromPrompt(apiKeyList []serverapiclient.ApiKey, title string) *ser } func ListApiKeys(apiKeyList []serverapiclient.ApiKey) { - util.RenderMainTitle("API KEYS") + views.RenderMainTitle("API KEYS") modelInstance := renderApiKeyList(apiKeyList, false) diff --git a/pkg/views/server/configure.go b/pkg/views/server/configure.go index c5b0350d02..0c97c0e509 100644 --- a/pkg/views/server/configure.go +++ b/pkg/views/server/configure.go @@ -9,7 +9,8 @@ import ( "github.com/charmbracelet/huh" "github.com/daytonaio/daytona/pkg/serverapiclient" - view_util "github.com/daytonaio/daytona/pkg/views/util" + "github.com/daytonaio/daytona/pkg/views" + "github.com/daytonaio/daytona/pkg/views/util" ) var CommandsInputHelp = "Comma separated list of commands. To use ',' in commands, escape them like this '\\,'" @@ -20,7 +21,7 @@ type ServerUpdateKeyView struct { } func ConfigurationForm(config *serverapiclient.ServerConfig) *serverapiclient.ServerConfig { - projectStartCommands := view_util.GetJoinedCommands(config.DefaultProjectPostStartCommands) + projectStartCommands := util.GetJoinedCommands(config.DefaultProjectPostStartCommands) form := huh.NewForm( huh.NewGroup( @@ -52,14 +53,14 @@ func ConfigurationForm(config *serverapiclient.ServerConfig) *serverapiclient.Se Description(CommandsInputHelp). Value(&projectStartCommands), ), - ) + ).WithTheme(views.GetCustomTheme()) err := form.Run() if err != nil { log.Fatal(err) } - config.DefaultProjectPostStartCommands = view_util.GetSplitCommands(projectStartCommands) + config.DefaultProjectPostStartCommands = util.GetSplitCommands(projectStartCommands) return config } diff --git a/pkg/views/server/started/view.go b/pkg/views/server/started/view.go new file mode 100644 index 0000000000..658d331f48 --- /dev/null +++ b/pkg/views/server/started/view.go @@ -0,0 +1,60 @@ +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package started + +import ( + "fmt" + "os" + + "github.com/charmbracelet/lipgloss" + "github.com/daytonaio/daytona/pkg/views" + "golang.org/x/term" +) + +const minTUIWidth = 80 +const maxTUIWidth = 140 + +func Render(apiPort uint32, frpcUrl string, isDaemonMode bool) { + output := "\n" + output += views.GetStyledMainTitle("Daytona") + "\n\n" + output += fmt.Sprintf("## Daytona Server is running on port: %d\n\n", apiPort) + output += views.GetSeparatorString() + "\n\n" + output += "You may now begin developing locally" + output += "\n" + + // output += fmt.Sprintf("If you want to connect to the server remotely:\n\n") + + // output += fmt.Sprintf("1. Create an API key on this machine: ") + // output += lipgloss.NewStyle().Foreground(views.Green).Render("daytona server api-key new") + "\n" + // output += fmt.Sprintf("2. Add a profile on the client machine: \n\t") + // output += lipgloss.NewStyle().Foreground(views.Green).Render(fmt.Sprintf("daytona profile add -a %s -k API_KEY", m.frpcUrl)) + "\n\n" + + // output += views.GetSeparatorString() + "\n\n" + + // output += "Press Enter to create an API key and copy the complete client command to clipboard automatically" + + terminalWidth, _, err := term.GetSize(int(os.Stdout.Fd())) + if err != nil || terminalWidth < minTUIWidth { + fmt.Println(output) + return + } + width := terminalWidth - 20 + if width > maxTUIWidth { + width = maxTUIWidth + } + + output = lipgloss.NewStyle().PaddingLeft(3).Render(output) + + if !isDaemonMode { + output = "\n" + output + } + + output = lipgloss. + NewStyle(). + BorderForeground(views.LightGray). + Border(lipgloss.RoundedBorder()).Width(width). + Render(output) + "\n" + + fmt.Println(output) +} diff --git a/pkg/views/styles.go b/pkg/views/styles.go index 323beea22e..cf5af8e414 100644 --- a/pkg/views/styles.go +++ b/pkg/views/styles.go @@ -11,13 +11,16 @@ import ( ) var ( - Green = lipgloss.AdaptiveColor{Light: "#23cc71", Dark: "#23cc71"} - DimmedGreen = lipgloss.AdaptiveColor{Light: "#7be0a9", Dark: "#7be0a9"} - Blue = lipgloss.AdaptiveColor{Light: "#017ffe", Dark: "#017ffe"} - Red = lipgloss.AdaptiveColor{Light: "#ff4567", Dark: "#ff4567"} - DimmedBlue = lipgloss.AdaptiveColor{Light: "#3398fe", Dark: "#3398fe"} - White = lipgloss.AdaptiveColor{Light: "000", Dark: "fff"} - Gray = lipgloss.AdaptiveColor{Light: "243", Dark: "243"} + Green = lipgloss.AdaptiveColor{Light: "#23cc71", Dark: "#23cc71"} + TempGreen = lipgloss.AdaptiveColor{Light: "#23cc71", Dark: "#23cc71"} + DimmedGreen = lipgloss.AdaptiveColor{Light: "#50d98f", Dark: "#50d98f"} + TempDimmedGreen = lipgloss.AdaptiveColor{Light: "#7be0a9", Dark: "#7be0a9"} + Red = lipgloss.AdaptiveColor{Light: "#ff4567", Dark: "#ff4567"} + Orange = lipgloss.AdaptiveColor{Light: "#eb9834", Dark: "#eb9834"} + White = lipgloss.AdaptiveColor{Light: "fff", Dark: "fff"} + Black = lipgloss.AdaptiveColor{Light: "000", Dark: "000"} + Gray = lipgloss.AdaptiveColor{Light: "243", Dark: "243"} + LightGray = lipgloss.AdaptiveColor{Light: "#828282", Dark: "#828282"} ) func ColorGrid(xSteps, ySteps int) [][]string { @@ -54,31 +57,63 @@ func GetStyledSelectList(items []list.Item) list.Model { d.Styles.SelectedTitle = lipgloss.NewStyle(). Border(lipgloss.NormalBorder(), false, false, false, true). - BorderForeground(Blue). - Foreground(Blue). + BorderForeground(Green). + Foreground(Green). Bold(true). Padding(0, 0, 0, 1) - d.Styles.SelectedDesc = d.Styles.SelectedTitle.Copy().Foreground(DimmedBlue) + d.Styles.SelectedDesc = d.Styles.SelectedTitle.Copy().Foreground(DimmedGreen).Bold(false) l := list.New(items, d, 0, 0) l.Styles.FilterPrompt = lipgloss.NewStyle().Foreground(Green) - l.Styles.FilterCursor = lipgloss.NewStyle().Foreground(Green) - l.Styles.Title = lipgloss.NewStyle().Foreground(Green).Bold(true) + l.Styles.FilterCursor = lipgloss.NewStyle().Foreground(Green).Background(Green) + l.Styles.Title = lipgloss.NewStyle().Foreground(Black).Bold(true). + Background(lipgloss.Color("#fff")).Padding(0) l.FilterInput.PromptStyle = lipgloss.NewStyle().Foreground(Green) l.FilterInput.TextStyle = lipgloss.NewStyle().Foreground(Green) + l.SetStatusBarItemName("item\n\n"+lipgloss.NewStyle().Foreground(LightGray).Render("==="), "items\n\n"+lipgloss.NewStyle().Foreground(LightGray).Render("===")) + return l } func GetCustomTheme() *huh.Theme { + newTheme := huh.ThemeCharm() + + newTheme.Blurred.Title = newTheme.Focused.Title + + b := &newTheme.Blurred + b.FocusedButton.Background(Green) + b.FocusedButton.Bold(true) + b.TextInput.Prompt.Foreground(White) + b.TextInput.Cursor.Foreground(White) + b.SelectSelector.Foreground(Green) + + f := &newTheme.Focused + f.Base = f.Base.BorderForeground(lipgloss.Color("fff")) + f.Title.Foreground(Green).Bold(true) + f.FocusedButton.Bold(true) + f.FocusedButton.Background(Green) + f.TextInput.Prompt.Foreground(White) + f.TextInput.Cursor.Foreground(White) + f.SelectSelector.Foreground(Green) + f.SelectedOption.Foreground(Green) + + f.Base.MarginTop(DefaultLayoutMarginTop) + b.Base.MarginTop(DefaultLayoutMarginTop) + + return newTheme +} + +func GetInitialCommandTheme() *huh.Theme { newTheme := huh.ThemeCharm() newTheme.Blurred.Title = newTheme.Focused.Title + // newTheme.Focused.SelectSelector = lipgloss.NewStyle().Foreground(Green) b := &newTheme.Blurred b.FocusedButton.Background(Green) b.FocusedButton.Bold(true) @@ -88,12 +123,18 @@ func GetCustomTheme() *huh.Theme { f := &newTheme.Focused f.Base = f.Base.BorderForeground(lipgloss.Color("fff")) - f.Title.Foreground(Blue).Bold(true) + f.Title.Foreground(Green).Bold(true) f.FocusedButton.Bold(true) f.FocusedButton.Background(Green) f.TextInput.Prompt.Foreground(Green) f.TextInput.Cursor.Foreground(White) f.SelectSelector.Foreground(Green) + f.Base.UnsetMarginLeft() + f.Base.UnsetPaddingLeft() + f.Base.BorderLeft(false) + + f.SelectedOption = lipgloss.NewStyle().Foreground(Green) + return newTheme } diff --git a/pkg/views/target/list/view.go b/pkg/views/target/list/view.go index 88db94c0dc..47e304b9ea 100644 --- a/pkg/views/target/list/view.go +++ b/pkg/views/target/list/view.go @@ -10,7 +10,7 @@ import ( "strings" "github.com/daytonaio/daytona/pkg/serverapiclient" - "github.com/daytonaio/daytona/pkg/views/util" + "github.com/daytonaio/daytona/pkg/views" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" @@ -100,7 +100,7 @@ func getRowsAndCols(width int, initialRows []table.Row) ([]table.Row, []table.Co } func ListTargets(targets []serverapiclient.ProviderTarget) { - util.RenderMainTitle("TARGETS") + views.RenderMainTitle("TARGETS") m := renderTargetList(targets) diff --git a/pkg/views/target/select.go b/pkg/views/target/select.go index cf8783c339..d08809ed21 100644 --- a/pkg/views/target/select.go +++ b/pkg/views/target/select.go @@ -13,7 +13,6 @@ import ( "github.com/daytonaio/daytona/internal/util" "github.com/daytonaio/daytona/pkg/serverapiclient" "github.com/daytonaio/daytona/pkg/views" - view_util "github.com/daytonaio/daytona/pkg/views/util" ) const NewTargetName = "+ New Target" @@ -38,8 +37,8 @@ func GetTargetFromPrompt(targets []serverapiclient.ProviderTarget, activeProfile l := views.GetStyledSelectList(items) m := model{list: l} - m.list.Title = "CHOOSE A TARGET" - m.footer = view_util.GetListFooter(activeProfileName) + m.list.Title = views.GetStyledMainTitle("Choose a target") + m.footer = views.GetListFooter(activeProfileName) p, err := tea.NewProgram(m, tea.WithAltScreen()).Run() if err != nil { diff --git a/pkg/views/target/view.go b/pkg/views/target/view.go index dc0a6ef3c5..aac558ccaf 100644 --- a/pkg/views/target/view.go +++ b/pkg/views/target/view.go @@ -7,7 +7,7 @@ import ( "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/daytonaio/daytona/pkg/serverapiclient" - view_util "github.com/daytonaio/daytona/pkg/views/util" + "github.com/daytonaio/daytona/pkg/views" ) type item struct { @@ -48,7 +48,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit } case tea.WindowSizeMsg: - h, v := view_util.DocStyle.GetFrameSize() + h, v := views.DocStyle.GetFrameSize() m.list.SetSize(msg.Width-h, msg.Height-v) } @@ -58,5 +58,5 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m model) View() string { - return view_util.DocStyle.Render(m.list.View() + m.footer) + return views.DocStyle.Render(m.list.View() + m.footer) } diff --git a/pkg/views/util/globals.go b/pkg/views/util/globals.go deleted file mode 100644 index e3c0e31a9f..0000000000 --- a/pkg/views/util/globals.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2024 Daytona Platforms Inc. -// SPDX-License-Identifier: Apache-2.0 - -package util - -import ( - "fmt" - - "github.com/charmbracelet/lipgloss" - "github.com/daytonaio/daytona/pkg/views" -) - -var DocStyle = lipgloss.NewStyle().Margin(3, 2, 1, 2) - -func RenderMainTitle(title string) { - fmt.Println(lipgloss.NewStyle().Foreground(views.Green).Bold(true).Padding(1, 0, 1, 0).Render(title)) -} - -func RenderLine(message string) { - fmt.Println(lipgloss.NewStyle().PaddingLeft(1).Render(message)) -} - -func RenderInfoMessage(message string) { - fmt.Println(lipgloss.NewStyle().Padding(1, 0, 1, 1).Render(message)) -} - -func RenderListLine(message string) { - fmt.Println(lipgloss.NewStyle().Padding(0, 0, 1, 1).Render(message)) -} - -func RenderInfoMessageBold(message string) { - fmt.Println(lipgloss.NewStyle().Bold(true).Padding(1, 0, 1, 1).Render(message)) -} - -func GetListFooter(profileName string) string { - return lipgloss.NewStyle().Bold(true).PaddingLeft(2).Render("\n\nActive profile: " + profileName) -} - -func RenderBorderedMessage(message string) { - fmt.Println(GetBorderedMessage(message)) -} - -func GetBorderedMessage(message string) string { - return lipgloss. - NewStyle(). - Margin(1, 0). - Padding(1, 1, 1, 1). - BorderForeground(views.Green). - Border(lipgloss.RoundedBorder()). - Render(message) -} - -func GetStyledMainTitle(content string) string { - return lipgloss.NewStyle().Foreground(views.Green).Bold(true).Render(content) -} diff --git a/pkg/views/util/spinner.go b/pkg/views/util/spinner.go index ecc08bda9f..f48176a401 100644 --- a/pkg/views/util/spinner.go +++ b/pkg/views/util/spinner.go @@ -24,7 +24,7 @@ type Msg string func initialModel() model { s := spinner.New() s.Spinner = spinner.Dot - s.Style = lipgloss.NewStyle().Foreground(views.Blue) + s.Style = lipgloss.NewStyle().Foreground(views.TempGreen) return model{spinner: s} } @@ -76,7 +76,7 @@ func (m model) View() string { return "" } - str := fmt.Sprintf("\n\n %s Loading...\n\n", m.spinner.View()) + str := views.DocStyle.Render(fmt.Sprintf("\n\n %s Loading...\n\n", m.spinner.View())) return str } diff --git a/pkg/views/workspace/create/configuration.go b/pkg/views/workspace/create/configuration.go index 592abde4d7..b2a4f0afa0 100644 --- a/pkg/views/workspace/create/configuration.go +++ b/pkg/views/workspace/create/configuration.go @@ -11,7 +11,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/daytonaio/daytona/pkg/serverapiclient" "github.com/daytonaio/daytona/pkg/views" - view_util "github.com/daytonaio/daytona/pkg/views/util" + "github.com/daytonaio/daytona/pkg/views/util" "github.com/daytonaio/daytona/pkg/views/workspace/selection" ) @@ -36,7 +36,7 @@ func ConfigureProjects(projectList []serverapiclient.CreateWorkspaceRequestProje containerUser = *project.User } if project.PostStartCommands != nil { - postStartCommands = view_util.GetJoinedCommands(project.PostStartCommands) + postStartCommands = util.GetJoinedCommands(project.PostStartCommands) } GetProjectConfigurationGroup(&containerImage, &containerUser, &postStartCommands) @@ -54,7 +54,7 @@ func ConfigureProjects(projectList []serverapiclient.CreateWorkspaceRequestProje if projectList[i].Name == project.Name { projectList[i].Image = &containerImage projectList[i].User = &containerUser - projectList[i].PostStartCommands = view_util.GetSplitCommands(postStartCommands) + projectList[i].PostStartCommands = util.GetSplitCommands(postStartCommands) } } diff --git a/pkg/views/workspace/create/summary.go b/pkg/views/workspace/create/summary.go index 856a72329d..a954986b53 100644 --- a/pkg/views/workspace/create/summary.go +++ b/pkg/views/workspace/create/summary.go @@ -13,7 +13,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/daytonaio/daytona/pkg/serverapiclient" "github.com/daytonaio/daytona/pkg/views" - view_util "github.com/daytonaio/daytona/pkg/views/util" + "github.com/daytonaio/daytona/pkg/views/util" ) type SummaryModel struct { @@ -55,7 +55,7 @@ func DisplayMultiSubmitForm(workspaceName string, projectList *[]serverapiclient (*projectList)[i].PostStartCommands = apiServerConfig.DefaultProjectPostStartCommands } - defaultPostStartCommandString := view_util.GetJoinedCommands(apiServerConfig.DefaultProjectPostStartCommands) + defaultPostStartCommandString := util.GetJoinedCommands(apiServerConfig.DefaultProjectPostStartCommands) configuredProjects, err := ConfigureProjects(*projectList, *apiServerConfig.DefaultProjectImage, *apiServerConfig.DefaultProjectUser, defaultPostStartCommandString) if err != nil { @@ -69,7 +69,7 @@ func DisplayMultiSubmitForm(workspaceName string, projectList *[]serverapiclient func RenderSummary(workspaceName string, projectList []serverapiclient.CreateWorkspaceRequestProject) string { - output := view_util.GetStyledMainTitle(fmt.Sprintf("SUMMARY - Workspace %s", workspaceName)) + output := views.GetStyledMainTitle(fmt.Sprintf("SUMMARY - Workspace %s", workspaceName)) for _, project := range projectList { if project.Source == nil || project.Source.Repository == nil || project.Source.Repository.Url == nil { @@ -77,7 +77,7 @@ func RenderSummary(workspaceName string, projectList []serverapiclient.CreateWor } } - output += fmt.Sprintf("\n\n%s - %s\n", lipgloss.NewStyle().Foreground(views.Blue).Render("Primary Project"), *projectList[0].Source.Repository.Url) + output += fmt.Sprintf("\n\n%s - %s\n", lipgloss.NewStyle().Foreground(views.TempGreen).Render("Primary Project"), *projectList[0].Source.Repository.Url) // Remove the primary project from the list projectList = projectList[1:] @@ -87,7 +87,7 @@ func RenderSummary(workspaceName string, projectList []serverapiclient.CreateWor } for i := range projectList { - output += fmt.Sprintf("%s - %s", lipgloss.NewStyle().Foreground(views.Blue).Render(fmt.Sprintf("#%d %s", i+1, "Secondary Project")), (*projectList[i].Source.Repository.Url)) + output += fmt.Sprintf("%s - %s", lipgloss.NewStyle().Foreground(views.TempGreen).Render(fmt.Sprintf("#%d %s", i+1, "Secondary Project")), (*projectList[i].Source.Repository.Url)) if i < len(projectList)-1 { output += "\n" } @@ -158,5 +158,5 @@ func (m SummaryModel) View() string { return "" } - return view_util.GetBorderedMessage(RenderSummary(m.workspaceName, m.projectList)) + "\n" + m.form.View() + configurationHelpLine + return views.GetBorderedMessage(RenderSummary(m.workspaceName, m.projectList)) + "\n" + m.form.View() + configurationHelpLine } diff --git a/pkg/views/workspace/create/workspacedata.go b/pkg/views/workspace/create/workspacedata.go index 0fadf4c449..02a5dc3edb 100644 --- a/pkg/views/workspace/create/workspacedata.go +++ b/pkg/views/workspace/create/workspacedata.go @@ -15,7 +15,7 @@ import ( "github.com/daytonaio/daytona/pkg/serverapiclient" "github.com/daytonaio/daytona/pkg/views" configure "github.com/daytonaio/daytona/pkg/views/server" - view_util "github.com/daytonaio/daytona/pkg/views/util" + views_util "github.com/daytonaio/daytona/pkg/views/util" ) var doneCheck bool @@ -37,7 +37,7 @@ func GetWorkspaceDataFromPrompt(apiServerConfig *serverapiclient.ServerConfig, s var postStartCommands []string - postStartCommandString := view_util.GetJoinedCommands(apiServerConfig.DefaultProjectPostStartCommands) + postStartCommandString := views_util.GetJoinedCommands(apiServerConfig.DefaultProjectPostStartCommands) workspaceName, containerImage, containerUser := suggestedName, *apiServerConfig.DefaultProjectImage, *apiServerConfig.DefaultProjectUser @@ -53,7 +53,7 @@ func GetWorkspaceDataFromPrompt(apiServerConfig *serverapiclient.ServerConfig, s } if postStartCommandString != "" { - postStartCommands = view_util.GetSplitCommands(postStartCommandString) + postStartCommands = views_util.GetSplitCommands(postStartCommandString) } return workspaceName, containerImage, containerUser, postStartCommands, nil diff --git a/pkg/views/workspace/info/view.go b/pkg/views/workspace/info/view.go index 8b84b929df..cff5f64bbc 100644 --- a/pkg/views/workspace/info/view.go +++ b/pkg/views/workspace/info/view.go @@ -3,107 +3,150 @@ package info -// A simple program that counts down from 5 and then exits. - import ( "fmt" + "os" + "strings" + "github.com/charmbracelet/lipgloss" "github.com/daytonaio/daytona/pkg/serverapiclient" "github.com/daytonaio/daytona/pkg/views" - - "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/lipgloss/table" + "golang.org/x/term" ) -var colors = views.ColorGrid(5, 5) - -var workspaceNameStyle = lipgloss.NewStyle(). - Foreground(views.Green). - Bold(true). - MarginLeft(2) - -var repositoryURLStyle = lipgloss.NewStyle(). - Bold(true). - Foreground(lipgloss.Color("227")). - MarginLeft(2) - -var repositoryBranchStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("227")). - MarginLeft(2) - -var viewStyle = lipgloss.NewStyle(). - MarginTop(1). - MarginBottom(0). - PaddingLeft(2). - PaddingRight(2) - -var projectNameStyle = lipgloss.NewStyle(). - Bold(true). - Foreground(views.Blue). - PaddingLeft(2) - -var projectStatusStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color(colors[0][4])). - PaddingLeft(2) - -func projectRender(projectInfo *serverapiclient.ProjectInfo, project *serverapiclient.Project) string { - projectState := "" - extensions := [][]string{} - extensionsTable := "" - repoBranch := "" - - if !*projectInfo.IsRunning && *projectInfo.Created == "" { - projectState = projectStatusStyle.Foreground(lipgloss.Color(colors[0][4])).Render("Unavailable") - } else if !*projectInfo.IsRunning { - projectState = projectStatusStyle.Render("Stopped") - } else { - projectState = projectStatusStyle.Foreground(lipgloss.Color(colors[4][4])).Render("Running") +const propertyNameWidth = 16 +const minTUIWidth = 80 +const maxTUIWidth = 140 - extensionsTable = table.New(). - Border(lipgloss.HiddenBorder()). - Rows(extensions...).Render() +var propertyNameStyle = lipgloss.NewStyle(). + Foreground(views.LightGray) + +var propertyValueStyle = lipgloss.NewStyle(). + Foreground(views.White). + Bold(true) + +func Render(workspace *serverapiclient.WorkspaceDTO, ide string) { + var isCreationView bool + nameLabel := "Name" + + if ide != "" { + isCreationView = true } - if project.Repository.Branch == nil { - repoBranch = "" + output := "" + + if !isCreationView { + output += views.GetStyledMainTitle("Workspace info") + "\n" } else { - repoBranch = "Branch" + repositoryBranchStyle.Render(*project.Repository.Branch) + nameLabel = "Workspace" } - repoView := "Url" + (repositoryURLStyle.Render(*project.Repository.Url)) + "\n" + repoBranch - repoView = "Repository" + viewStyle.Render(repoView) + output += "\n" - projectView := "Project" + projectNameStyle.Render(*projectInfo.Name) + "\n" + "State " + projectState + extensionsTable + output += getInfoLine(nameLabel, *workspace.Info.Name) + "\n" - return viewStyle.Render(projectView) + viewStyle.Render(repoView) -} + if isCreationView { + output += getInfoLine("Editor", ide) + "\n" + } else { + output += getInfoLine("ID", *workspace.Id) + "\n" + } + + if len(workspace.Projects) == 1 { + output += renderSingleProject(&workspace.Projects[0], isCreationView) + } else { + output += renderProjects(workspace.Projects, workspace.Info.Projects, isCreationView) + } -func Render(workspace *serverapiclient.WorkspaceDTO) { - if workspace == nil { + var width int + terminalWidth, _, err := term.GetSize(int(os.Stdout.Fd())) + if err != nil || terminalWidth < minTUIWidth { + fmt.Println(output) return } + width = terminalWidth - 20 + if width > maxTUIWidth { + width = maxTUIWidth + } + renderTUIView(output, width, isCreationView) +} + +func renderTUIView(output string, terminalWidth int, isCreationView bool) { + output = lipgloss.NewStyle().PaddingLeft(3).Render(output) + + content := lipgloss. + NewStyle(). + BorderForeground(views.LightGray). + Border(lipgloss.RoundedBorder()).Width(terminalWidth). + Render(output) + + if !isCreationView { + content = lipgloss.NewStyle().Margin(1, 0).Render(content) + } + fmt.Println(content) +} + +func renderSingleProject(project *serverapiclient.Project, isCreationView bool) string { var output string - output = "\n" - output += "Workspace" + workspaceNameStyle.Render(*workspace.Info.Name) + "\n" - output += "ID" + workspaceNameStyle.Render(*workspace.Id) + "\n" - output += "Target" + workspaceNameStyle.Render(*workspace.Target) + "\n" - if len(workspace.Projects) > 1 { - output += "\n" + "Projects" + repositoryUrl := *project.Repository.Url + repositoryUrl = strings.TrimPrefix(repositoryUrl, "https://") + repositoryUrl = strings.TrimPrefix(repositoryUrl, "http://") + + output += getInfoLineState("State", project.State) + "\n" + if project.Target != nil && !isCreationView { + output += getInfoLine("Target", *project.Target) + "\n" } + output += getInfoLine("Repository", repositoryUrl) + "\n" - for _, project := range workspace.Projects { - for _, projectInfo := range workspace.Info.Projects { + if project.Name != nil && !isCreationView { + output += getInfoLine("Project", *project.Name) + } + + return output +} + +func renderProjects(projects []serverapiclient.Project, projectInfos []serverapiclient.ProjectInfo, isCreationView bool) string { + var output string + for i, project := range projects { + for _, projectInfo := range projectInfos { if *projectInfo.Name == *project.Name { - output += projectRender(&projectInfo, &project) + output += getInfoLine(fmt.Sprintf("Project #%d", i+1), *project.Name) + output += getInfoLineState("State", project.State) + if project.Target != nil && !isCreationView { + output += getInfoLine("Target", *project.Target) + } + if project.Repository != nil { + output += getInfoLine("Repository", *project.Repository.Url) + } break } } + if project.Name != projects[len(projects)-1].Name { + output += "\n" + } } + return output +} - output = lipgloss.NewStyle().PaddingLeft(3).Render(output) +func getInfoLine(key, value string) string { + return propertyNameStyle.Render(fmt.Sprintf("%-*s", propertyNameWidth, key)) + propertyValueStyle.Render(value) + "\n" +} + +func getInfoLineState(key string, state *serverapiclient.ProjectState) string { + var uptime int + var stateProperty string - println(output) - fmt.Println() + if state == nil || state.Uptime == nil { + uptime = 0 + } else { + uptime = int(*state.Uptime) + } + + if uptime == 0 { + stateProperty = propertyValueStyle.Foreground(views.Gray).Render("STOPPED") + } else { + stateProperty = propertyValueStyle.Foreground(views.Green).Render("RUNNING") + } + return propertyNameStyle.Render(fmt.Sprintf("%-*s", propertyNameWidth, key)) + stateProperty + propertyValueStyle.Foreground(views.White).Render("\n") } diff --git a/pkg/views/workspace/initialize/view.go b/pkg/views/workspace/initialize/view.go index 71282cf8f2..8a73448e9b 100644 --- a/pkg/views/workspace/initialize/view.go +++ b/pkg/views/workspace/initialize/view.go @@ -40,7 +40,7 @@ var projectViewStyle = lipgloss.NewStyle(). var projectNameStyle = lipgloss.NewStyle(). Bold(true). - Foreground(views.Blue). + Foreground(views.TempGreen). PaddingLeft(2) var projectStatusStyle = lipgloss.NewStyle(). diff --git a/pkg/views/workspace/list/view.go b/pkg/views/workspace/list/view.go index 6fbd42b0d8..f5cd25a7f0 100644 --- a/pkg/views/workspace/list/view.go +++ b/pkg/views/workspace/list/view.go @@ -14,7 +14,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/daytonaio/daytona/internal/util" "github.com/daytonaio/daytona/pkg/serverapiclient" - "github.com/daytonaio/daytona/pkg/workspace" + "github.com/daytonaio/daytona/pkg/views" "golang.org/x/term" ) @@ -22,12 +22,12 @@ var defaultColumnWidth = 12 var columnPadding = 3 type RowData struct { - WorkspaceName string - Repository string - Branch string - Target string - Created string - Uptime string + Name string + Repository string + Branch string + Target string + Created string + Uptime string } type model struct { @@ -37,12 +37,12 @@ type model struct { } var columns = []table.Column{ - {Title: "WORKSPACE", Width: defaultColumnWidth}, - {Title: "REPOSITORY", Width: defaultColumnWidth}, - {Title: "BRANCH", Width: defaultColumnWidth}, - {Title: "TARGET", Width: defaultColumnWidth}, - {Title: "CREATED", Width: defaultColumnWidth}, - {Title: "UPTIME", Width: defaultColumnWidth}, + {Title: "Workspace", Width: defaultColumnWidth}, + {Title: "Repository", Width: defaultColumnWidth}, + {Title: "Branch", Width: defaultColumnWidth}, + {Title: "Target", Width: defaultColumnWidth}, + {Title: "Created", Width: defaultColumnWidth}, + {Title: "Uptime", Width: defaultColumnWidth}, } func (m model) Init() tea.Cmd { @@ -63,10 +63,46 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } var baseStyle = lipgloss.NewStyle(). - BorderStyle(lipgloss.HiddenBorder()) + BorderForeground(views.LightGray). + Border(lipgloss.RoundedBorder()).Padding(1, 2).Margin(1, 0) + +var nameStyle = lipgloss.NewStyle().Foreground(views.White) +var runningStyle = lipgloss.NewStyle().Foreground(views.Green) +var stoppedStyle = lipgloss.NewStyle().Foreground(views.Red) +var defaultCellStyle = lipgloss.NewStyle().Foreground(views.Gray) func (m model) View() string { - return baseStyle.Render(m.table.View()) + return baseStyle.Render(m.table.View()) + "\n" +} + +func getRowFromRowData(rowData RowData, isMultiProjectAccordion bool) table.Row { + var state string + if rowData.Uptime == "" { + state = stoppedStyle.Render("STOPPED") + } else { + state = runningStyle.Render("RUNNING") + } + + if isMultiProjectAccordion { + return table.Row{ + rowData.Name, + } + } + + row := table.Row{ + nameStyle.Render(rowData.Name), + defaultCellStyle.Render(rowData.Repository), + defaultCellStyle.Render(rowData.Branch), + defaultCellStyle.Render(rowData.Target), + defaultCellStyle.Render(rowData.Created), + state, + } + + if rowData.Uptime != "" { + row[5] = fmt.Sprintf("%s %s", state, defaultCellStyle.Render(fmt.Sprintf("(%s)", rowData.Uptime))) + } + + return row } func renderWorkspaceList(workspaceList []serverapiclient.WorkspaceDTO, specifyGitProviders bool) model { @@ -79,19 +115,22 @@ func renderWorkspaceList(workspaceList []serverapiclient.WorkspaceDTO, specifyGi for _, workspace := range workspaceList { if len(workspace.Projects) == 1 { rowData = getWorkspaceTableRowData(workspace, specifyGitProviders) - adjustColumsFormatting(rowData) - row = table.Row{rowData.WorkspaceName, rowData.Repository, rowData.Branch, rowData.Target} + row = getRowFromRowData(rowData, false) if workspace.Info != nil && len(workspace.Info.Projects) > 0 { row = append(row, rowData.Created, rowData.Uptime) } rows = append(rows, row) + adjustRowColumsFormatting(row) } else { - row = table.Row{*workspace.Name, "", "", "", "", ""} + row = getRowFromRowData(RowData{Name: *workspace.Name}, true) rows = append(rows, row) for _, project := range workspace.Projects { rowData = getProjectTableRowData(workspace, project, specifyGitProviders) - adjustColumsFormatting(rowData) - row = table.Row{rowData.WorkspaceName, rowData.Repository, rowData.Branch, rowData.Target} + if rowData == (RowData{}) { + continue + } + row = getRowFromRowData(rowData, false) + adjustRowColumsFormatting(row) if workspace.Info != nil && len(workspace.Info.Projects) > 0 { row = append(row, rowData.Created, rowData.Uptime) } @@ -127,16 +166,16 @@ func sortWorkspaces(workspaceList *[]serverapiclient.WorkspaceDTO) { }) } -func adjustColumsFormatting(rowData RowData) { - adjustColumnWidth("WORKSPACE", rowData) - adjustColumnWidth("REPOSITORY", rowData) - adjustColumnWidth("BRANCH", rowData) - adjustColumnWidth("TARGET", rowData) - adjustColumnWidth("CREATED", rowData) - adjustColumnWidth("UPTIME", rowData) +func adjustRowColumsFormatting(row table.Row) { + adjustRowColumnWidth("Workspace", row) + adjustRowColumnWidth("Repository", row) + adjustRowColumnWidth("Branch", row) + adjustRowColumnWidth("Target", row) + adjustRowColumnWidth("Created", row) + adjustRowColumnWidth("Uptime", row) } -func adjustColumnWidth(title string, rowData RowData) { +func adjustRowColumnWidth(title string, row table.Row) { var column *table.Column for i, col := range columns { if col.Title == title { @@ -146,18 +185,18 @@ func adjustColumnWidth(title string, rowData RowData) { } currentField := "" switch title { - case "WORKSPACE": - currentField = rowData.WorkspaceName - case "REPOSITORY": - currentField = rowData.Repository - case "BRANCH": - currentField = rowData.Branch - case "TARGET": - currentField = rowData.Target - case "CREATED": - currentField = rowData.Created - case "UPTIME": - currentField = rowData.Uptime + case "Workspace": + currentField = row[0] + case "Repository": + currentField = row[1] + case "Branch": + currentField = row[2] + case "Target": + currentField = row[3] + case "Created": + currentField = row[4] + case "Uptime": + currentField = row[5] } if len(currentField) > column.Width { @@ -168,7 +207,7 @@ func adjustColumnWidth(title string, rowData RowData) { func getWorkspaceTableRowData(workspace serverapiclient.WorkspaceDTO, specifyGitProviders bool) RowData { rowData := RowData{} if workspace.Name != nil { - rowData.WorkspaceName = *workspace.Name + rowData.Name = *workspace.Name + " " } if workspace.Projects != nil && len(workspace.Projects) > 0 && workspace.Projects[0].Repository != nil { rowData.Repository = getRepositorySlugFromUrl(*workspace.Projects[0].Repository.Url, specifyGitProviders) @@ -189,32 +228,32 @@ func getWorkspaceTableRowData(workspace serverapiclient.WorkspaceDTO, specifyGit } func getProjectTableRowData(workspaceDTO serverapiclient.WorkspaceDTO, project serverapiclient.Project, specifyGitProviders bool) RowData { - var currentProjectInfo *workspace.ProjectInfo - - if workspaceDTO.Info == nil || workspaceDTO.Info.Projects == nil { - return RowData{} - } - - for _, projectInfo := range workspaceDTO.Info.Projects { - if *projectInfo.Name == *project.Name { - currentProjectInfo = &workspace.ProjectInfo{ - Name: *projectInfo.Name, - Created: *projectInfo.Created, - } - break - } - } - - if currentProjectInfo == nil { - currentProjectInfo = &workspace.ProjectInfo{ - Name: *project.Name, - Created: "/", - } - } + // var currentProjectInfo *workspace.ProjectInfo + + // if workspaceDTO.Info == nil || workspaceDTO.Info.Projects == nil { + // return RowData{} + // } + + // for _, projectInfo := range workspaceDTO.Info.Projects { + // if *projectInfo.Name == *project.Name { + // currentProjectInfo = &workspace.ProjectInfo{ + // Name: *projectInfo.Name, + // Created: *projectInfo.Created, + // } + // break + // } + // } + + // if currentProjectInfo == nil { + // currentProjectInfo = &workspace.ProjectInfo{ + // Name: *project.Name, + // Created: "/", + // } + // } rowData := RowData{} if project.Name != nil { - rowData.WorkspaceName = " └ " + *project.Name + rowData.Name = " └ " + *project.Name } if project.Repository != nil && project.Repository.Url != nil { rowData.Repository = getRepositorySlugFromUrl(*project.Repository.Url, specifyGitProviders) @@ -225,10 +264,11 @@ func getProjectTableRowData(workspaceDTO serverapiclient.WorkspaceDTO, project s if project.Target != nil { rowData.Target = *project.Target } - rowData.Created = util.FormatCreatedTime(currentProjectInfo.Created) - if project.State.Uptime != nil { + if project.State != nil && project.State.UpdatedAt != nil && project.State.Uptime != nil { + rowData.Created = util.FormatCreatedTime(*project.State.UpdatedAt) rowData.Uptime = util.FormatUptime(*project.State.Uptime) } + return rowData } @@ -258,7 +298,6 @@ func ListWorkspaces(workspaceList []serverapiclient.WorkspaceDTO, specifyGitProv fmt.Println("Error running program:", err) os.Exit(1) } - fmt.Println() } func getTable(rows []table.Row, cols []table.Column, activeRow int) table.Model { @@ -270,15 +309,22 @@ func getTable(rows []table.Row, cols []table.Column, activeRow int) table.Model style := table.DefaultStyles() style.Header = style.Header. - BorderStyle(lipgloss.HiddenBorder()). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(views.LightGray). + Foreground(views.LightGray). BorderBottom(true). - AlignHorizontal(lipgloss.Left) + PaddingBottom(1). + AlignHorizontal(lipgloss.Left).MarginBottom(2) style.Selected = style.Selected. Foreground(style.Cell.GetForeground()). Background(style.Cell.GetBackground()). Bold(false) + // Double the table height to make space for the padding + style.Cell.PaddingBottom(1) + t.SetHeight(2 * t.Height()) + t.SetStyles(style) t.SetCursor(activeRow) diff --git a/pkg/views/workspace/list/view_temp.go b/pkg/views/workspace/list/view_temp.go new file mode 100644 index 0000000000..450f79ae61 --- /dev/null +++ b/pkg/views/workspace/list/view_temp.go @@ -0,0 +1,292 @@ +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package list + +// import ( +// "fmt" +// "sort" +// "strings" + +// "github.com/charmbracelet/lipgloss" +// "github.com/charmbracelet/lipgloss/table" +// "github.com/daytonaio/daytona/internal/util" +// "github.com/daytonaio/daytona/pkg/serverapiclient" +// "github.com/daytonaio/daytona/pkg/views" +// "github.com/daytonaio/daytona/pkg/workspace" +// ) + +// var defaultColumnWidth = 12 +// var columnPadding = 3 + +// type RowData struct { +// Name string +// Repository string +// Branch string +// Target string +// Created string +// Uptime string +// } + +// var headers = []string{"Workspace", "Repository", "Branch", "Target", "Created", "Uptime"} + +// var baseStyle = lipgloss.NewStyle(). +// BorderForeground(views.LightGray). +// Border(lipgloss.RoundedBorder()).Padding(1, 2).Margin(1, 0) + +// var headerStyle = lipgloss.NewStyle(). +// BorderStyle(lipgloss.RoundedBorder()). +// BorderForeground(views.LightGray). +// Foreground(views.LightGray). +// BorderBottom(true). +// PaddingBottom(1). +// AlignHorizontal(lipgloss.Left).MarginBottom(1) + +// func getRowFromRowData(rowData RowData) []string { +// return []string{rowData.Name, rowData.Repository, rowData.Branch, rowData.Target, rowData.Created, rowData.Uptime} +// } + +// func ListWorkspaces(workspaceList []serverapiclient.WorkspaceDTO, specifyGitProviders bool) { +// var rows [][]string +// var row []string +// var rowData RowData + +// sortWorkspaces(&workspaceList) + +// for _, workspace := range workspaceList { +// if len(workspace.Projects) == 1 { +// rowData = getWorkspaceTableRowData(workspace, specifyGitProviders) +// // adjustColumsFormatting(rowData) +// row = getRowFromRowData(rowData) +// if workspace.Info != nil && len(workspace.Info.Projects) > 0 { +// row = append(row, rowData.Created, rowData.Uptime) +// } +// rows = append(rows, row) +// rows = append(rows, []string{"", "", "", ""}) +// } else { +// row = []string{*workspace.Name, "", "", "", "", ""} +// rows = append(rows, row) +// rows = append(rows, []string{"", "", "", ""}) +// for _, project := range workspace.Projects { +// rowData = getProjectTableRowData(workspace, project, specifyGitProviders) +// if rowData == (RowData{}) { +// continue +// } +// // adjustColumsFormatting(rowData) +// row = getRowFromRowData(rowData) +// if workspace.Info != nil && len(workspace.Info.Projects) > 0 { +// row = append(row, rowData.Created, rowData.Uptime) +// } +// rows = append(rows, row) +// rows = append(rows, []string{"", "", "", ""}) +// } +// } +// } + +// // width, _, _ := term.GetSize(int(os.Stdout.Fd())) +// // adjustedRows, adjustedCols := getRowsAndCols(width, rows) + +// fmt.Println(getTable(rows).Render()) +// } + +// func sortWorkspaces(workspaceList *[]serverapiclient.WorkspaceDTO) { +// sort.Slice(*workspaceList, func(i, j int) bool { +// ws1 := (*workspaceList)[i] +// ws2 := (*workspaceList)[j] +// if ws1.Info == nil || ws2.Info == nil || ws1.Info.Projects == nil || ws2.Info.Projects == nil { +// return false +// } +// if len(ws1.Info.Projects) == 0 { +// return false +// } +// if len(ws2.Info.Projects) == 0 { +// return true +// } +// return *ws1.Info.Projects[0].Created > *ws2.Info.Projects[0].Created +// }) +// } + +// // func adjustColumsFormatting(rowData RowData) { +// // adjustColumnWidth("Workspace", rowData) +// // adjustColumnWidth("Repository", rowData) +// // adjustColumnWidth("Branch", rowData) +// // adjustColumnWidth("Target", rowData) +// // adjustColumnWidth("Created", rowData) +// // adjustColumnWidth("Uptime", rowData) +// // } + +// // func adjustColumnWidth(title string, rowData RowData) { +// // var column *table.Column +// // for i, col := range columns { +// // if col.Title == title { +// // column = &columns[i] +// // break +// // } +// // } +// // currentField := "" +// // switch title { +// // case "Workspace": +// // currentField = rowData.Name +// // case "Repository": +// // currentField = rowData.Repository +// // case "Branch": +// // currentField = rowData.Branch +// // case "Target": +// // currentField = rowData.Target +// // case "Created": +// // currentField = rowData.Created +// // case "Uptime": +// // currentField = rowData.Uptime +// // } + +// // if len(currentField) > column.Width { +// // column.Width = len(currentField) + columnPadding +// // } +// // } + +// func getWorkspaceTableRowData(workspace serverapiclient.WorkspaceDTO, specifyGitProviders bool) RowData { +// rowData := RowData{} +// if workspace.Name != nil { +// rowData.Name = *workspace.Name + " " +// } +// if workspace.Projects != nil && len(workspace.Projects) > 0 && workspace.Projects[0].Repository != nil { +// rowData.Repository = getRepositorySlugFromUrl(*workspace.Projects[0].Repository.Url, specifyGitProviders) +// if workspace.Projects[0].Repository.Branch != nil { +// rowData.Branch = *workspace.Projects[0].Repository.Branch +// } +// } +// if workspace.Target != nil { +// rowData.Target = *workspace.Target +// } +// if workspace.Info != nil && workspace.Info.Projects != nil && len(workspace.Info.Projects) > 0 && workspace.Info.Projects[0].Created != nil { +// rowData.Created = util.FormatCreatedTime(*workspace.Info.Projects[0].Created) +// } +// if len(workspace.Projects) > 0 && workspace.Projects[0].State != nil && workspace.Projects[0].State.Uptime != nil { +// rowData.Uptime = util.FormatUptime(*workspace.Projects[0].State.Uptime) +// } +// return rowData +// } + +// func getProjectTableRowData(workspaceDTO serverapiclient.WorkspaceDTO, project serverapiclient.Project, specifyGitProviders bool) RowData { +// var currentProjectInfo *workspace.ProjectInfo + +// if workspaceDTO.Info == nil { +// return RowData{} +// } +// for _, projectInfo := range workspaceDTO.Info.Projects { +// if *projectInfo.Name == *project.Name { +// currentProjectInfo = &workspace.ProjectInfo{ +// Name: *projectInfo.Name, +// Created: *projectInfo.Created, +// } +// break +// } +// } + +// if currentProjectInfo == nil { +// currentProjectInfo = &workspace.ProjectInfo{ +// Name: *project.Name, +// Created: "/", +// } +// } + +// rowData := RowData{} +// if project.Name != nil { +// rowData.Name = " └ " + *project.Name +// } +// if project.Repository != nil && project.Repository.Url != nil { +// rowData.Repository = getRepositorySlugFromUrl(*project.Repository.Url, specifyGitProviders) +// if project.Repository.Branch != nil { +// rowData.Branch = *project.Repository.Branch +// } +// } +// if project.Target != nil { +// rowData.Target = *project.Target +// } +// rowData.Created = util.FormatCreatedTime(currentProjectInfo.Created) +// if project.State.Uptime != nil { +// rowData.Uptime = util.FormatUptime(*project.State.Uptime) +// } +// return rowData +// } + +// func getRepositorySlugFromUrl(url string, specifyGitProviders bool) string { +// if url == "" { +// return "/" +// } +// url = strings.TrimSuffix(url, "/") + +// parts := strings.Split(url, "/") +// if len(parts) < 2 { +// return "" +// } + +// if specifyGitProviders { +// return parts[len(parts)-3] + "/" + parts[len(parts)-2] + "/" + parts[len(parts)-1] +// } + +// return parts[len(parts)-2] + "/" + parts[len(parts)-1] +// } + +// func getTable(data [][]string) *table.Table { + +// t := table.New(). +// Border(lipgloss.NormalBorder()). +// BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("238"))).Width(80). +// Rows(data...). +// StyleFunc(func(row, col int) lipgloss.Style { +// if row == 0 { +// return headerStyle +// } + +// even := row%2 == 0 + +// // switch col { +// // case 2, 3: // Type 1 + 2 +// // c := typeColors +// // if even { +// // c = dimTypeColors +// // } + +// // color := c[fmt.Sprint(data[row-1][col])] +// // return baseStyle.Copy().Foreground(color) +// // } + +// if even { +// return baseStyle.Copy().Foreground(lipgloss.Color("245")) +// } +// return baseStyle.Copy().Foreground(lipgloss.Color("252")) +// }) + +// return t +// } + +// // func getRowsAndCols(width int, initialRows []table.Row) ([]table.Row, []table.Column) { +// // colWidth := 0 +// // cols := []table.Column{} + +// // for i, col := range columns { +// // // keep columns length in sync with initialRows +// // if i >= len(initialRows[0]) { +// // break +// // } + +// // if colWidth+col.Width > width { +// // break +// // } + +// // colWidth += col.Width +// // cols = append(cols, col) +// // } + +// // rows := make([]table.Row, len(initialRows)) + +// // for i, row := range initialRows { +// // if len(row) >= len(cols) { +// // rows[i] = row[:len(cols)] +// // } else { +// // rows[i] = row +// // } +// // } +// // return rows, cols +// // } diff --git a/pkg/views/workspace/selection/branch.go b/pkg/views/workspace/selection/branch.go index 940b2096a2..3b382c44f4 100644 --- a/pkg/views/workspace/selection/branch.go +++ b/pkg/views/workspace/selection/branch.go @@ -27,11 +27,14 @@ func selectBranchPrompt(branches []serverapiclient.GitBranch, secondaryProjectOr } l := views.GetStyledSelectList(items) - m := model[string]{list: l} - m.list.Title = "CHOOSE A BRANCH" + + title := "Choose a branch" if secondaryProjectOrder > 0 { - m.list.Title += fmt.Sprintf(" (Secondary Project #%d)", secondaryProjectOrder) + title += fmt.Sprintf(" (Secondary Project #%d)", secondaryProjectOrder) } + l.Title = views.GetStyledMainTitle(title) + l.Styles.Title = titleStyle + m := model[string]{list: l} p, err := tea.NewProgram(m, tea.WithAltScreen()).Run() if err != nil { diff --git a/pkg/views/workspace/selection/checkout.go b/pkg/views/workspace/selection/checkout.go index 3cc1984d29..fcc9a4f9e3 100644 --- a/pkg/views/workspace/selection/checkout.go +++ b/pkg/views/workspace/selection/checkout.go @@ -33,11 +33,14 @@ func selectCheckoutPrompt(checkoutOptions []CheckoutOption, secondaryProjectOrde } l := views.GetStyledSelectList(items) - m := model[string]{list: l} - m.list.Title = "CLONING OPTIONS" + + title := "Cloning options" if secondaryProjectOrder > 0 { - m.list.Title += fmt.Sprintf(" (Secondary Project #%d)", secondaryProjectOrder) + title += fmt.Sprintf(" (Secondary Project #%d)", secondaryProjectOrder) } + l.Title = views.GetStyledMainTitle(title) + l.Styles.Title = titleStyle + m := model[string]{list: l} p, err := tea.NewProgram(m, tea.WithAltScreen()).Run() if err != nil { diff --git a/pkg/views/workspace/selection/namespace.go b/pkg/views/workspace/selection/namespace.go index 644b8cf981..b5a4f1876b 100644 --- a/pkg/views/workspace/selection/namespace.go +++ b/pkg/views/workspace/selection/namespace.go @@ -29,11 +29,14 @@ func selectNamespacePrompt(namespaces []serverapiclient.GitNamespace, secondaryP } l := views.GetStyledSelectList(items) - m := model[string]{list: l} - m.list.Title = "CHOOSE A NAMESPACE" + + title := "Choose a namespace" if secondaryProjectOrder > 0 { - m.list.Title += fmt.Sprintf(" (Secondary Project #%d)", secondaryProjectOrder) + title += fmt.Sprintf(" (Secondary Project #%d)", secondaryProjectOrder) } + l.Title = views.GetStyledMainTitle(title) + l.Styles.Title = titleStyle + m := model[string]{list: l} p, err := tea.NewProgram(m, tea.WithAltScreen()).Run() if err != nil { diff --git a/pkg/views/workspace/selection/project.go b/pkg/views/workspace/selection/project.go index 05f51aaa06..830e180dd3 100644 --- a/pkg/views/workspace/selection/project.go +++ b/pkg/views/workspace/selection/project.go @@ -6,7 +6,6 @@ package selection import ( "fmt" "os" - "strings" "github.com/daytonaio/daytona/pkg/serverapiclient" "github.com/daytonaio/daytona/pkg/views" @@ -40,12 +39,12 @@ func selectProjectPrompt(projects []serverapiclient.Project, actionVerb string, d.Styles.SelectedTitle = lipgloss.NewStyle(). Border(lipgloss.NormalBorder(), false, false, false, true). - BorderForeground(views.Blue). - Foreground(views.Blue). + BorderForeground(views.TempGreen). + Foreground(views.TempGreen). Bold(true). Padding(0, 0, 0, 1) - d.Styles.SelectedDesc = d.Styles.SelectedTitle.Copy().Foreground(views.DimmedBlue) + d.Styles.SelectedDesc = d.Styles.SelectedTitle.Copy().Foreground(views.TempDimmedGreen) l := list.New(items, d, 0, 0) @@ -57,7 +56,7 @@ func selectProjectPrompt(projects []serverapiclient.Project, actionVerb string, m := model[serverapiclient.Project]{list: l} - m.list.Title = "SELECT A PROJECT TO " + strings.ToUpper(actionVerb) + m.list.Title = views.GetStyledMainTitle("Select a project to " + actionVerb) m.list.Styles.Title = lipgloss.NewStyle().Foreground(views.Green).Bold(true) p, err := tea.NewProgram(m, tea.WithAltScreen()).Run() diff --git a/pkg/views/workspace/selection/projectrequest.go b/pkg/views/workspace/selection/projectrequest.go index 3439a94a30..cfbd86606f 100644 --- a/pkg/views/workspace/selection/projectrequest.go +++ b/pkg/views/workspace/selection/projectrequest.go @@ -11,7 +11,6 @@ import ( "github.com/daytonaio/daytona/pkg/serverapiclient" "github.com/daytonaio/daytona/pkg/views" - view_util "github.com/daytonaio/daytona/pkg/views/util" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" @@ -84,7 +83,7 @@ func selectProjectRequestPrompt(projects []serverapiclient.CreateWorkspaceReques m := projectRequestModel{} m.list = l - m.list.Title = "CHOOSE A PROJECT TO CONFIGURE" + m.list.Title = "Choose a project to configure" m.list.AdditionalShortHelpKeys = func() []key.Binding { return []key.Binding{ @@ -134,7 +133,7 @@ func (m projectRequestModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit } case tea.WindowSizeMsg: - h, v := view_util.DocStyle.GetFrameSize() + h, v := views.DocStyle.GetFrameSize() m.list.SetSize(msg.Width-h, msg.Height-v) } @@ -158,8 +157,8 @@ func (d projectRequestItemDelegate) Render(w io.Writer, m list.Model, index int, // Adjust styles as the user moves through the menu if isSelected { - name = selectedStyles.Copy().Foreground(views.Blue).Render(i.Name()) - imageLine = selectedStyles.Copy().Foreground(views.DimmedBlue).Render(i.Image()) + name = selectedStyles.Copy().Foreground(views.TempGreen).Render(i.Name()) + imageLine = selectedStyles.Copy().Foreground(views.TempDimmedGreen).Render(i.Image()) userLine = selectedStyles.Copy().Foreground(views.Gray).Render(i.User()) postStartCommandsLine = selectedStyles.Copy().Foreground(views.Gray).Render(i.PostStartCommands()) } diff --git a/pkg/views/workspace/selection/provider.go b/pkg/views/workspace/selection/provider.go index 41332a9700..bbca307a45 100644 --- a/pkg/views/workspace/selection/provider.go +++ b/pkg/views/workspace/selection/provider.go @@ -11,9 +11,12 @@ import ( "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" gitprovider_view "github.com/daytonaio/daytona/pkg/views/gitprovider" ) +var titleStyle = lipgloss.NewStyle() + func selectProviderPrompt(gitProviders []gitprovider_view.GitProviderView, secondaryProjectOrder int, choiceChan chan<- string) { items := []list.Item{} @@ -27,11 +30,14 @@ func selectProviderPrompt(gitProviders []gitprovider_view.GitProviderView, secon items = append(items, newItem) l := views.GetStyledSelectList(items) - m := model[string]{list: l} - m.list.Title = "CHOOSE YOUR PROVIDER" + + title := "Choose a provider" if secondaryProjectOrder > 0 { - m.list.Title += fmt.Sprintf(" (Secondary Project #%d)", secondaryProjectOrder) + title += fmt.Sprintf(" (Secondary Project #%d)", secondaryProjectOrder) } + l.Title = views.GetStyledMainTitle(title) + l.Styles.Title = titleStyle + m := model[string]{list: l} p, err := tea.NewProgram(m, tea.WithAltScreen()).Run() if err != nil { diff --git a/pkg/views/workspace/selection/pullrequest.go b/pkg/views/workspace/selection/pullrequest.go index 2f1ce8a2e5..cbb1fd2a73 100644 --- a/pkg/views/workspace/selection/pullrequest.go +++ b/pkg/views/workspace/selection/pullrequest.go @@ -27,11 +27,14 @@ func selectPullRequestPrompt(pullRequests []serverapiclient.GitPullRequest, seco } l := views.GetStyledSelectList(items) - m := model[string]{list: l} - m.list.Title = "CHOOSE A PULL/MERGE REQUEST" + + title := "Choose a pull/merge request" if secondaryProjectOrder > 0 { - m.list.Title += fmt.Sprintf(" (Secondary Project #%d)", secondaryProjectOrder) + title += fmt.Sprintf(" (Secondary Project #%d)", secondaryProjectOrder) } + l.Title = views.GetStyledMainTitle(title) + l.Styles.Title = titleStyle + m := model[string]{list: l} p, err := tea.NewProgram(m, tea.WithAltScreen()).Run() if err != nil { diff --git a/pkg/views/workspace/selection/repository.go b/pkg/views/workspace/selection/repository.go index b5640b2cc9..ec62bdd278 100644 --- a/pkg/views/workspace/selection/repository.go +++ b/pkg/views/workspace/selection/repository.go @@ -24,11 +24,14 @@ func selectRepositoryPrompt(repositories []serverapiclient.GitRepository, second } l := views.GetStyledSelectList(items) - m := model[string]{list: l} - m.list.Title = "CHOOSE A REPOSITORY" + + title := "Choose a repository" if secondaryProjectOrder > 0 { - m.list.Title += fmt.Sprintf(" (Secondary Project #%d)", secondaryProjectOrder) + title += fmt.Sprintf(" (Secondary Project #%d)", secondaryProjectOrder) } + l.Title = views.GetStyledMainTitle(title) + l.Styles.Title = titleStyle + m := model[string]{list: l} p, err := tea.NewProgram(m, tea.WithAltScreen()).Run() if err != nil { diff --git a/pkg/views/workspace/selection/view.go b/pkg/views/workspace/selection/view.go index edfbc4baa9..cca8ff91dc 100644 --- a/pkg/views/workspace/selection/view.go +++ b/pkg/views/workspace/selection/view.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "log" + "os" "strings" "github.com/charmbracelet/bubbles/list" @@ -14,14 +15,14 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/daytonaio/daytona/cmd/daytona/config" "github.com/daytonaio/daytona/pkg/views" - view_util "github.com/daytonaio/daytona/pkg/views/util" + "golang.org/x/term" ) var CustomRepoIdentifier = "" var selectedStyles = lipgloss.NewStyle(). Border(lipgloss.NormalBorder(), false, false, false, true). - BorderForeground(views.Blue). + BorderForeground(views.TempGreen). Bold(true). Padding(0, 0, 0, 1) @@ -39,9 +40,10 @@ func (i item[T]) Uptime() string { return i.uptime } func (i item[T]) Target() string { return i.target } type model[T any] struct { - list list.Model - choice *T - footer string + list list.Model + choice *T + footer string + initialWidthSet bool } func (m model[T]) Init() tea.Cmd { @@ -49,6 +51,13 @@ func (m model[T]) Init() tea.Cmd { } func (m model[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if !m.initialWidthSet { + _, _, err := term.GetSize(int(os.Stdout.Fd())) + if err != nil { + m.list.SetWidth(150) + } + } + switch msg := msg.(type) { case tea.KeyMsg: switch keypress := msg.String(); keypress { @@ -63,7 +72,7 @@ func (m model[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit } case tea.WindowSizeMsg: - h, v := view_util.DocStyle.GetFrameSize() + h, v := views.DocStyle.GetFrameSize() m.list.SetSize(msg.Width-h, msg.Height-v) } @@ -84,9 +93,15 @@ func (m model[T]) View() string { log.Fatal(err) } - m.footer = view_util.GetListFooter(activeProfile.Name) + m.footer = views.GetListFooter(activeProfile.Name) } - return view_util.DocStyle.Render(m.list.View() + m.footer) + + terminalWidth, terminalHeight, err := term.GetSize(int(os.Stdout.Fd())) + if err != nil { + return "" + } + + return views.DocStyle.Width(terminalWidth - 4).Height(terminalHeight - 4).Render(m.list.View() + m.footer) } type ItemDelegate[T any] struct { @@ -101,6 +116,7 @@ func (d ItemDelegate[T]) Render(w io.Writer, m list.Model, index int, listItem l baseStyles := lipgloss.NewStyle().Padding(0, 0, 0, 2) title := baseStyles.Copy().Render(i.Title()) + // title := views.GetStyledMainTitle(i.Title()) idWithTargetString := fmt.Sprintf("%s (%s)", i.Id(), i.Target()) idWithTarget := baseStyles.Copy().Foreground(views.Gray).Render(idWithTargetString) description := baseStyles.Copy().Render(i.Description()) @@ -119,10 +135,10 @@ func (d ItemDelegate[T]) Render(w io.Writer, m list.Model, index int, listItem l // Adjust styles as the user moves through the menu if isSelected { - title = selectedStyles.Copy().Foreground(views.Blue).Render(i.Title()) + title = selectedStyles.Copy().Foreground(views.TempGreen).Render(i.Title()) idWithTarget = selectedStyles.Copy().Foreground(views.Gray).Render(idWithTargetString) - description = selectedStyles.Copy().Foreground(views.DimmedBlue).Render(i.Description()) - timeString = timeStyles.Copy().Foreground(views.DimmedBlue).Render(timeString) + description = selectedStyles.Copy().Foreground(views.TempDimmedGreen).Render(i.Description()) + timeString = timeStyles.Copy().Foreground(views.TempDimmedGreen).Render(timeString) } // Render to the terminal diff --git a/pkg/views/workspace/selection/workspace.go b/pkg/views/workspace/selection/workspace.go index 3730e062de..6230007b9d 100644 --- a/pkg/views/workspace/selection/workspace.go +++ b/pkg/views/workspace/selection/workspace.go @@ -63,7 +63,7 @@ func selectWorkspacePrompt(workspaces []serverapiclient.WorkspaceDTO, actionVerb m := model[serverapiclient.WorkspaceDTO]{list: l} - m.list.Title = "SELECT A WORKSPACE TO " + strings.ToUpper(actionVerb) + m.list.Title = views.GetStyledMainTitle("Select a workspace to " + actionVerb) m.list.Styles.Title = lipgloss.NewStyle().Foreground(views.Green).Bold(true) p, err := tea.NewProgram(m, tea.WithAltScreen()).Run() diff --git a/pkg/views/workspace/status/view.go b/pkg/views/workspace/status/view.go index 91ec5a2061..3cf4acd8b5 100644 --- a/pkg/views/workspace/status/view.go +++ b/pkg/views/workspace/status/view.go @@ -4,57 +4,43 @@ package status import ( - "strings" + "fmt" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/daytonaio/daytona/pkg/views" ) var ( spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63")) - helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Margin(1, 0) - dotStyle = helpStyle.Copy().UnsetMargins() - appStyle = lipgloss.NewStyle().Margin(0, 2, 1, 2).BorderStyle(lipgloss.NormalBorder()).BorderForeground(views.Green).Width(50).Padding(1, 1, 0, 2) + checkMark = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).SetString("✓") ) -type ResultMsg struct { +type Message struct { Line string - Dots bool } type ClearScreenMsg struct{} -func (r ResultMsg) String() string { - if r.Dots { - return dotStyle.Render(strings.Repeat(".", 30)) - } +func (r Message) String() string { return r.Line } type model struct { spinner spinner.Model - results []ResultMsg + messages []Message quitting bool width, height int } func NewModel() model { - const numLastResults = 6 s := spinner.New() s.Style = spinnerStyle - results := make([]ResultMsg, numLastResults) - - for i := range results { - results[i] = ResultMsg{Dots: true} - } - - results[len(results)-1] = ResultMsg{Line: "Workspace creation request submitted", Dots: false} + messages := make([]Message, 0) return model{ - spinner: s, - results: results, + spinner: s, + messages: messages, } } @@ -64,12 +50,12 @@ func (m model) Init() tea.Cmd { func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case ResultMsg: + case Message: if msg.Line == "END_SIGNAL" { m.quitting = true return m, tea.Quit } - m.results = append(m.results[1:], msg) + m.messages = append(m.messages, msg) return m, nil case spinner.TickMsg: var cmd tea.Cmd @@ -96,9 +82,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m model) View() string { var s string - for _, res := range m.results { - s += res.String() + "\n" + for _, message := range m.messages { + if message.Line != "" { + // fmt.Printf("\r%s", msg) + s += fmt.Sprintf("%s %s\n", checkMark, message.String()) + } } - return appStyle.Width(m.width - 20).Render(s) + return s + m.spinner.View() }