From ce711e1b666eb1d74e171580cfbeb16752292ec3 Mon Sep 17 00:00:00 2001 From: FlUxIuS Date: Thu, 1 Aug 2024 00:36:42 +0200 Subject: [PATCH] Updating visual CLI for more user friendly experience --- Dockerfile | 9 +- Dockerfiles/SDR/corebuild.docker | 9 +- config/.zshrc | 13 + go/rfswift/cli/rfcli.go | 28 +- go/rfswift/common/common.go | 85 +++ go/rfswift/dock/dockerhub.go | 166 ++++-- go/rfswift/dock/rfdock.go | 703 ++++++++++++++++++++++-- go/rfswift/dock/rfdock.go.old | 805 ++++++++++++++++++++++++++++ go/rfswift/go.mod | 10 +- go/rfswift/go.sum | 16 +- go/rfswift/main.go | 98 ++-- go/rfswift/rfutils/configs.go | 181 +++++++ go/rfswift/rfutils/githutils.go | 139 ++--- go/rfswift/rfutils/hostcli.go | 75 +-- go/rfswift/rfutils/notifications.go | 111 ++++ scripts/terminal_harness.sh | 7 +- 16 files changed, 2169 insertions(+), 286 deletions(-) create mode 100644 config/.zshrc create mode 100644 go/rfswift/common/common.go create mode 100644 go/rfswift/dock/rfdock.go.old create mode 100644 go/rfswift/rfutils/configs.go create mode 100644 go/rfswift/rfutils/notifications.go diff --git a/Dockerfile b/Dockerfile index d933e21..469c41d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,9 +40,11 @@ RUN echo apt-fast apt-fast/aptmanager string apt-get | debconf-set-selections RUN apt-get -y install apt-fast python3-matplotlib # Installing desktop features for next virtual desktop sessions -#RUN echo apt-fast keyboard-configuration/layout string "English (US)" | debconf-set-selections -#RUN echo apt-fast keyboard-configuration/variant string "English (US)" | debconf-set-selections -#RUN apt-get -y install task-lxqt-desktop +RUN echo apt-fast keyboard-configuration/layout string "English (US)" | debconf-set-selections +RUN echo apt-fast keyboard-configuration/variant string "English (US)" | debconf-set-selections +RUN apt-fast -y install task-lxqt-desktop +RUN apt-fast -y install language-pack-en +RUN update-locale # Audio part RUN apt-fast install -y pulseaudio-utils pulseaudio libasound2-dev libavahi-client-dev --no-install-recommends @@ -57,6 +59,7 @@ RUN chmod +x entrypoint.sh # Installing Terminal harnesses RUN ./entrypoint.sh fzf_soft_install RUN ./entrypoint.sh zsh_tools_install +COPY config/.zshrc /root/.zshrc RUN ./entrypoint.sh arsenal_soft_install # Installing Devices diff --git a/Dockerfiles/SDR/corebuild.docker b/Dockerfiles/SDR/corebuild.docker index 696ab1e..16aa62f 100644 --- a/Dockerfiles/SDR/corebuild.docker +++ b/Dockerfiles/SDR/corebuild.docker @@ -40,9 +40,11 @@ RUN echo apt-fast apt-fast/aptmanager string apt-get | debconf-set-selections RUN apt-get -y install apt-fast python3-matplotlib # Installing desktop features for next virtual desktop sessions -#RUN echo apt-fast keyboard-configuration/layout string "English (US)" | debconf-set-selections -#RUN echo apt-fast keyboard-configuration/variant string "English (US)" | debconf-set-selections -#RUN apt-get -y install task-lxqt-desktop +RUN echo apt-fast keyboard-configuration/layout string "English (US)" | debconf-set-selections +RUN echo apt-fast keyboard-configuration/variant string "English (US)" | debconf-set-selections +RUN apt-fast -y install task-lxqt-desktop +RUN apt-fast -y install language-pack-en +RUN update-locale # Audio part RUN apt-fast install -y pulseaudio-utils pulseaudio libasound2-dev libavahi-client-dev --no-install-recommends @@ -57,6 +59,7 @@ RUN chmod +x entrypoint.sh # Installing Terminal harnesses RUN ./entrypoint.sh fzf_soft_install RUN ./entrypoint.sh zsh_tools_install +COPY config/.zshrc /root/.zshrc RUN ./entrypoint.sh arsenal_soft_install # Installing Devices diff --git a/config/.zshrc b/config/.zshrc new file mode 100644 index 0000000..648cb7f --- /dev/null +++ b/config/.zshrc @@ -0,0 +1,13 @@ +export ZSH="$HOME/.oh-my-zsh" + +ZSH_THEME="xiong-chiamiov" + +plugins=( + git + zsh-autosuggestions + zsh-syntax-highlighting +) + +source $ZSH/oh-my-zsh.sh + +alias a='/opt/arsenal/run' diff --git a/go/rfswift/cli/rfcli.go b/go/rfswift/cli/rfcli.go index 58b32c4..bc066bb 100644 --- a/go/rfswift/cli/rfcli.go +++ b/go/rfswift/cli/rfcli.go @@ -26,6 +26,8 @@ var ImageTag string var ExtraHost string var UsbDevice string var PulseServer string +var DockerName string +var DockerNewName string var rootCmd = &cobra.Command{ Use: "rfswift", @@ -58,7 +60,7 @@ var runCmd = &cobra.Command{ if os == "linux" { // use pactl to configure ACLs rfutils.SetPulseCTL(PulseServer) } - rfdock.DockerRun() + rfdock.DockerRun(DockerName) }, } @@ -118,12 +120,21 @@ var pullCmd = &cobra.Command{ }, } -var renameCmd = &cobra.Command{ - Use: "rename", +var retagCmd = &cobra.Command{ + Use: "retag", Short: "Rename an image", Long: `Rename an image with another tag`, Run: func(cmd *cobra.Command, args []string) { - rfdock.DockerRename(ImageRef, ImageTag) + rfdock.DockerTag(ImageRef, ImageTag) + }, +} + +var renameCmd = &cobra.Command{ + Use: "rename", + Short: "Rename a container", + Long: `Rename a container by another name`, + Run: func(cmd *cobra.Command, args []string) { + rfdock.DockerRename(DockerName, DockerNewName) }, } @@ -266,6 +277,7 @@ func init() { rootCmd.AddCommand(execCmd) rootCmd.AddCommand(commitCmd) rootCmd.AddCommand(renameCmd) + rootCmd.AddCommand(retagCmd) rootCmd.AddCommand(installCmd) rootCmd.AddCommand(removeCmd) rootCmd.AddCommand(ImagesCmd) @@ -302,8 +314,10 @@ func init() { installCmd.Flags().StringVarP(&ContID, "container", "c", "", "container to run") //pullCmd.MarkFlagRequired("tag") - renameCmd.Flags().StringVarP(&ImageRef, "image", "i", "", "image reference") - renameCmd.Flags().StringVarP(&ImageTag, "tag", "t", "", "rename to target tag") + retagCmd.Flags().StringVarP(&ImageRef, "image", "i", "", "image reference") + retagCmd.Flags().StringVarP(&ImageTag, "tag", "t", "", "rename to target tag") + renameCmd.Flags().StringVarP(&DockerName, "name", "n", "", "Docker current name") + renameCmd.Flags().StringVarP(&DockerNewName, "destination", "d", "", "Docker new name") commitCmd.Flags().StringVarP(&ContID, "container", "c", "", "container to run") commitCmd.Flags().StringVarP(&DImage, "image", "i", "", "image (default: 'myrfswift:latest')") commitCmd.MarkFlagRequired("container") @@ -318,6 +332,8 @@ func init() { runCmd.Flags().StringVarP(&ExtraBind, "bind", "b", "", "extra bindings (separate them with commas)") runCmd.Flags().StringVarP(&DImage, "image", "i", "", "image (default: 'myrfswift:latest')") runCmd.Flags().StringVarP(&PulseServer, "pulseserver", "p", "tcp:127.0.0.1:34567", "PULSE SERVER TCP address (by default: tcp:127.0.0.1:34567)") + runCmd.Flags().StringVarP(&DockerName, "name", "n", "", "A docker name") + runCmd.MarkFlagRequired("name") lastCmd.Flags().StringVarP(&FilterLast, "filter", "f", "", "filter by image name") } diff --git a/go/rfswift/common/common.go b/go/rfswift/common/common.go new file mode 100644 index 0000000..da80c7f --- /dev/null +++ b/go/rfswift/common/common.go @@ -0,0 +1,85 @@ +package common + +import ( + "fmt" + "strings" +) + +var Version = "0.4.7" +var Branch = "main" +var ascii_art = ` + + -%@%- :==-. + :%:*+=:+%. .%@%#%+-:-=%@* + %==#=: . .+# :@%:*==--: . =%. + :@=::+#:. .+-:%. %@:*+==..=**+=-:::=#= + +****=:+. +=::*#-%- .@+:*.:.:=***:.::...:*#* + ::%*#*:* :+- *:-.=%**. =@:+*+:.==.:::=:: .-:.:#+ + :*%#**%.-%%%+ :#:#+:.=#%%@@*.:==++.=:.-#@@@#:*==+=% + %#@+*: +%##+%@@@%=...==-+= ::%@*#@%#%%%@%=*=* + :*#%#*. .=: **@%%%%#==+=-=+: .+@: %*+%=...*@+@: + +=*+*@: ::. .@*@@@@#*+*##%*::=@* :%*-:-%*. *@: + ::.@=@=#= . =@@@@%=*#*%@%===+%@+=*%*:*%=%.--*#-: %* + ##@:==%%%@@@@#=#%#*-:-=*%@@@%+:::=%%=#-:-:-@:= .@ + .%@. :==@@@%#@@@*=*#===:: .:=+***=+**.::-:-%=+ :@ + *@: %%@@@%+=*@@@%%%*====-::. ::=-=:*%=-.@* + .*%%%*=. :@%:. .*%=::*#@@@@%*=::::::::..:*@#-:.%%. + ** =%.- . @@ :@%*=:::==+**%%@@@@@@@%*=:..*@* + %: =@: %@: .@+%@%#==--::::----:::.:*@*:=#%@= + -%*%. :@* :. :@. :=%@@%%%#**#%%@@#-:#*: %= + .@= .. #@-**. -@: .#*#%. :%=. :@* + :%. -- =++@@=+*. :%. +**@@%@+-:::* %@@. + %@- -* =%@@@@@@%=**: =% :#@@=%@@@@- .%.%@@* + @@@+.=*:-@@@@%%%@%@%%@#: :#@@=. *#.:-@# *@@@@% + %@%@%=**@@%%%%@@@@@@@*.=@#: .*%:+%*- *@%@@@@@:*= + :@@@@@#@@@@@@@@@@= :*: :*@@= ** *+====-@@@@@#. *= + :@@@@@@@@@@+ .=%@@- -# :#@@@@@@@@--%. + :@@@@@#:.:- .=@@%= .%%%@@@@@@@*: :%@@%. + #@*.:=-:=%@@*...:::::.. .=+***=#@% + -@=--*@@@* :=+#%%#+:: #@@=%: + .@*%@@*. .-+=:.:@@@* -#=. + *@%= .: + : + + + 888~-_ 888~~ ,d88~~\ ,e, 88~\ d8 + 888 \ 888___ 8888 Y88b e / " _888__ _d88__ + 888 | 888 'Y88b Y88b d8b / 888 888 888 + 888 / 888 'Y88b, Y888/Y88b/ 888 888 888 + 888_-~ 888 8888 Y8/ Y8/ 888 888 888 + 888 ~-_ 888 \__88P' Y Y 888 888 "88_/ + + RF toolbox for HAMs and professionals +` + +func PrintASCII() { + colors := []string{ + "\033[31m", // Red + "\033[33m", // Yellow + "\033[32m", // Green + "\033[36m", // Cyan + "\033[34m", // Blue + "\033[35m", // Magenta + } + reset := "\033[0m" + + lines := strings.Split(ascii_art, "\n") + for i, line := range lines { + color := colors[i%len(colors)] + fmt.Println(color + line + reset) + } +} + +func PrintErrorMessage(err error) { + red := "\033[31m" + white := "\033[37m" + reset := "\033[0m" + fmt.Printf("%s[!] %s%s%s\n", red, white, err.Error(), reset) +} + +func PrintSuccessMessage(message string) { + green := "\033[32m" + white := "\033[37m" + reset := "\033[0m" + fmt.Printf("%s[+] %s%s%s\n", green, white, message, reset) +} \ No newline at end of file diff --git a/go/rfswift/dock/dockerhub.go b/go/rfswift/dock/dockerhub.go index 2111a99..1df53de 100644 --- a/go/rfswift/dock/dockerhub.go +++ b/go/rfswift/dock/dockerhub.go @@ -9,8 +9,9 @@ import ( "runtime" "sort" "time" + "strings" - "github.com/olekukonko/tablewriter" + "golang.org/x/crypto/ssh/terminal" rfutils "penthertz/rfswift/rfutils" ) @@ -42,13 +43,41 @@ func getArchitecture() string { } } +func getRemoteImageCreationDate(repo, tag, architecture string) (time.Time, error) { + url := fmt.Sprintf("https://hub.docker.com/v2/repositories/%s/tags/?page_size=100", repo) + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Get(url) + if err != nil { + return time.Time{}, err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return time.Time{}, fmt.Errorf("tag not found") + } else if resp.StatusCode != http.StatusOK { + return time.Time{}, fmt.Errorf("failed to get tags: %s", resp.Status) + } + + var tagList TagList + if err := json.NewDecoder(resp.Body).Decode(&tagList); err != nil { + return time.Time{}, err + } + + for _, t := range tagList.Results { + if t.Name == tag { + for _, image := range t.Images { + if image.Architecture == architecture { + return t.TagLastPushed, nil + } + } + } + } + + return time.Time{}, fmt.Errorf("tag not found") +} + func getLatestDockerHubTags(repo string, architecture string) ([]Tag, error) { - /* - * Get latest Docker images details - * in(1): remote repository string - * in(2): architecture string - * out: tuple status - */ url := fmt.Sprintf("https://hub.docker.com/v2/repositories/%s/tags/?page_size=100", repo) client := &http.Client{Timeout: 10 * time.Second} @@ -105,40 +134,91 @@ func getLatestDockerHubTags(repo string, architecture string) ([]Tag, error) { } func ListDockerImagesRepo() { - /* - * Prints Latest tags for RF Swift - */ - repo := "penthertz/rfswift" // Change this to the repository you want to check - architecture := getArchitecture() - - if architecture == "" { - log.Fatalf("Unsupported architecture: %s", runtime.GOARCH) - } - - tags, err := getLatestDockerHubTags(repo, architecture) - if err != nil { - log.Fatalf("Error getting tags: %v", err) - } - - rfutils.ClearScreen() - - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"Tag", "Pushed Date", "Image", "Architecture", "Digest"}) - - for _, tag := range tags { - for _, image := range tag.Images { - if image.Architecture == architecture { - table.Append([]string{ - tag.Name, - tag.TagLastPushed.Format(time.RFC3339), - fmt.Sprintf("%s:%s", repo, tag.Name), - image.Architecture, - image.Digest, - }) - break - } - } - } - - table.Render() + repo := "penthertz/rfswift" + architecture := getArchitecture() + if architecture == "" { + log.Fatalf("Unsupported architecture: %s", runtime.GOARCH) + } + tags, err := getLatestDockerHubTags(repo, architecture) + if err != nil { + log.Fatalf("Error getting tags: %v", err) + } + + rfutils.ClearScreen() + + headers := []string{"Tag", "Pushed Date", "Image", "Architecture"} + tableData := [][]string{} + + for _, tag := range tags { + for _, image := range tag.Images { + if image.Architecture == architecture { + tableData = append(tableData, []string{ + tag.Name, + tag.TagLastPushed.Format(time.RFC3339), + fmt.Sprintf("%s:%s", repo, tag.Name), + image.Architecture, + }) + break + } + } + } + + width, _, err := terminal.GetSize(int(os.Stdout.Fd())) + if err != nil { + width = 80 // default width if terminal size cannot be determined + } + + columnWidths := make([]int, len(headers)) + for i, header := range headers { + columnWidths[i] = len(header) + } + for _, row := range tableData { + for i, col := range row { + if len(col) > columnWidths[i] { + columnWidths[i] = len(col) + } + } + } + + // Adjust column widths to fit the terminal width + totalWidth := len(headers) + 1 // Adding 1 for the left border + for _, w := range columnWidths { + totalWidth += w + 2 // Adding 2 for padding + } + + if totalWidth > width { + excess := totalWidth - width + for i := range columnWidths { + reduction := excess / len(columnWidths) + if columnWidths[i] > reduction { + columnWidths[i] -= reduction + excess -= reduction + } + } + totalWidth = width + } + + blue := "\033[34m" + white := "\033[37m" + reset := "\033[0m" + title := "๐Ÿ’ฟ Official Images" + + fmt.Printf("%s%s%s%s%s\n", blue, strings.Repeat(" ", 2), title, strings.Repeat(" ", totalWidth-2-len(title)), reset) + fmt.Print(white) + + printHorizontalBorder(columnWidths, "โ”Œ", "โ”ฌ", "โ”") + printRow(headers, columnWidths, "โ”‚") + printHorizontalBorder(columnWidths, "โ”œ", "โ”ผ", "โ”ค") + + for i, row := range tableData { + printRow(row, columnWidths, "โ”‚") + if i < len(tableData)-1 { + printHorizontalBorder(columnWidths, "โ”œ", "โ”ผ", "โ”ค") + } + } + + printHorizontalBorder(columnWidths, "โ””", "โ”ด", "โ”˜") + + fmt.Print(reset) + fmt.Println() } \ No newline at end of file diff --git a/go/rfswift/dock/rfdock.go b/go/rfswift/dock/rfdock.go index 29986c3..f681092 100644 --- a/go/rfswift/dock/rfdock.go +++ b/go/rfswift/dock/rfdock.go @@ -1,6 +1,3 @@ -/* This code is part of RF Switch by @Penthertz -* Author(s): Sรฉbastien Dudek (@FlUxIuS) - */ package dock import ( @@ -12,6 +9,7 @@ import ( "strings" "time" "log" + "regexp" "context" "github.com/docker/docker/api/types" @@ -23,8 +21,9 @@ import ( "github.com/docker/docker/pkg/jsonmessage" "github.com/moby/term" "golang.org/x/crypto/ssh/terminal" - "github.com/olekukonko/tablewriter" + rfutils "penthertz/rfswift/rfutils" + common "penthertz/rfswift/common" ) var inout chan []byte @@ -60,6 +59,40 @@ var dockerObj = DockerInst{net: "host", pulse_server: "tcp:localhost:34567", shell: "/bin/bash"} // Instance with default values +func init() { + updateDockerObjFromConfig() +} + +func updateDockerObjFromConfig() { + config, err := rfutils.ReadOrCreateConfig("config.ini") + if err != nil { + log.Printf("Error reading config: %v. Using default values.", err) + return + } + + // Update dockerObj with values from config + dockerObj.imagename = config.General.ImageName + dockerObj.shell = config.Container.Shell + dockerObj.network_mode = config.Container.Network + dockerObj.x11forward = config.Container.X11Forward + dockerObj.xdisplay = config.Container.XDisplay + dockerObj.extrahosts = config.Container.ExtraHost + dockerObj.extraenv = config.Container.ExtraEnv + dockerObj.pulse_server = config.Audio.PulseServer + + // Handle bindings + var bindings []string + for _, binding := range config.Container.Bindings { + if strings.Contains(binding, "/dev/bus/usb") { + dockerObj.usbforward = binding + } else if strings.Contains(binding, ".X11-unix") { + dockerObj.x11forward = binding + } else { + bindings = append(bindings, binding) + } + } + dockerObj.extrabinding = strings.Join(bindings, ",") +} func resizeTty(ctx context.Context, cli *client.Client, contid string, fd int) { /** @@ -83,10 +116,212 @@ func resizeTty(ctx context.Context, cli *client.Client, contid string, fd int) { } } +func checkIfImageIsUpToDate(repo, tag string) (bool, error) { + architecture := getArchitecture() + tags, err := getLatestDockerHubTags(repo, architecture) + if err != nil { + return false, err + } + + for _, latestTag := range tags { + if latestTag.Name == tag { + return true, nil + } + } + + return false, nil +} + +func parseImageName(imageName string) (string, string) { + parts := strings.Split(imageName, ":") + repo := parts[0] + tag := "latest" + if len(parts) > 1 { + tag = parts[1] + } + return repo, tag +} + + +func getLocalImageCreationDate(ctx context.Context, cli *client.Client, imageName string) (time.Time, error) { + localImage, _, err := cli.ImageInspectWithRaw(ctx, imageName) + if err != nil { + return time.Time{}, err + } + localImageTime, err := time.Parse(time.RFC3339, localImage.Created) + if err != nil { + return time.Time{}, err + } + return localImageTime, nil +} + +func checkImageStatus(ctx context.Context, cli *client.Client, repo, tag string) (bool, bool, error) { + architecture := getArchitecture() + + // Get the local image creation date + localImageTime, err := getLocalImageCreationDate(ctx, cli, fmt.Sprintf("%s:%s", repo, tag)) + if err != nil { + return false, true, err + } + + // Get the remote image creation date + remoteImageTime, err := getRemoteImageCreationDate(repo, tag, architecture) + if err != nil { + if err.Error() == "tag not found" { + return false, true, nil // Custom image if tag not found + } + return false, true, err + } + + // Adjust the remote image creation time by an offset of 2 hours + remoteImageTimeAdjusted := remoteImageTime.Add(-2 * time.Hour) + + // Compare local and adjusted remote image times + if localImageTime.Before(remoteImageTimeAdjusted) { + return false, false, nil // Obsolete + } + return true, false, nil // Up-to-date +} + +func printContainerProperties(ctx context.Context, cli *client.Client, containerName string, props map[string]string, size string) { + white := "\033[37m" + blue := "\033[34m" + green := "\033[32m" + red := "\033[31m" + yellow := "\033[33m" + reset := "\033[0m" + + // Determine if the image is up-to-date, obsolete, or custom + repo, tag := parseImageName(props["ImageName"]) + isUpToDate, isCustom, err := checkImageStatus(ctx, cli, repo, tag) + if err != nil { + if err.Error() != "tag not found" { + log.Printf("Error checking image status: %v", err) + } + } + + imageStatus := fmt.Sprintf("%s (Custom)", props["ImageName"]) + imageStatusColor := yellow + if !isCustom { + if isUpToDate { + imageStatus = fmt.Sprintf("%s (Up to date)", props["ImageName"]) + imageStatusColor = green + } else { + imageStatus = fmt.Sprintf("%s (Obsolete)", props["ImageName"]) + imageStatusColor = red + } + } + + properties := [][]string{ + {"Container Name", containerName}, + {"X Display", props["XDisplay"]}, + {"Shell", props["Shell"]}, + {"Privileged Mode", props["Privileged"]}, + {"Network Mode", props["NetworkMode"]}, + {"Image Name", imageStatus}, + {"Size on Disk", size}, + {"Bindings", props["Bindings"]}, + {"Extra Hosts", props["ExtraHosts"]}, + } + + width, _, err := terminal.GetSize(int(os.Stdout.Fd())) + if err != nil { + width = 80 // default width if terminal size cannot be determined + } + + // Adjust width for table borders and padding + maxContentWidth := width - 4 + if maxContentWidth < 20 { + maxContentWidth = 20 // Minimum content width + } + + maxKeyLen := 0 + for _, property := range properties { + if len(property[0]) > maxKeyLen { + maxKeyLen = len(property[0]) + } + } + + maxValueLen := maxContentWidth - maxKeyLen - 7 // 7 for borders and spaces + if maxValueLen < 10 { + maxValueLen = 10 // Minimum value length + } + + totalWidth := maxKeyLen + maxValueLen + 7 + + // Print the title in blue, aligned to the left with some padding + title := "๐ŸงŠ Container Summary" + leftPadding := 2 // You can adjust this value for more or less left padding + fmt.Printf("%s%s%s%s%s\n", blue, strings.Repeat(" ", leftPadding), title, strings.Repeat(" ", totalWidth-leftPadding-len(title)), reset) + + fmt.Printf("%s", white) // Switch to white color for the box + fmt.Printf("โ•ญ%sโ•ฎ\n", strings.Repeat("โ”€", totalWidth-2)) + + for i, property := range properties { + key := property[0] + value := property[1] + valueColor := white + + if key == "Image Name" { + valueColor = imageStatusColor + } + + // Wrap long values + wrappedValue := wrapText(value, maxValueLen) + valueLines := strings.Split(wrappedValue, "\n") + + for j, line := range valueLines { + if j == 0 { + fmt.Printf("โ”‚ %-*s โ”‚ %s%-*s%s โ”‚\n", maxKeyLen, key, valueColor, maxValueLen, line, reset) + } else { + fmt.Printf("โ”‚ %-*s โ”‚ %s%-*s%s โ”‚\n", maxKeyLen, "", valueColor, maxValueLen, line, reset) + } + + if j < len(valueLines)-1 { + fmt.Printf("โ”‚%sโ”‚%sโ”‚\n", strings.Repeat(" ", maxKeyLen+2), strings.Repeat(" ", maxValueLen+2)) + } + } + + if i < len(properties)-1 { + fmt.Printf("โ”œ%sโ”ผ%sโ”ค\n", strings.Repeat("โ”€", maxKeyLen+2), strings.Repeat("โ”€", maxValueLen+2)) + } + } + + fmt.Printf("โ•ฐ%sโ•ฏ\n", strings.Repeat("โ”€", totalWidth-2)) + fmt.Printf("%s", reset) + fmt.Println() // Ensure we end with a newline for clarity +} + +func wrapText(text string, maxWidth int) string { + var result strings.Builder + currentLineWidth := 0 + + words := strings.Fields(text) + for i, word := range words { + if currentLineWidth+len(word) > maxWidth { + if currentLineWidth > 0 { + result.WriteString("\n") + currentLineWidth = 0 + } + if len(word) > maxWidth { + for len(word) > maxWidth { + result.WriteString(word[:maxWidth] + "\n") + word = word[maxWidth:] + } + } + } + result.WriteString(word) + currentLineWidth += len(word) + if i < len(words)-1 && currentLineWidth+1+len(words[i+1]) <= maxWidth { + result.WriteString(" ") + currentLineWidth++ + } + } + + return result.String() +} + func DockerLast(ifilter string, labelKey string, labelValue string) { - /* Lists 10 last Docker containers - in(1): string optional filter for image name - */ ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { @@ -94,15 +329,14 @@ func DockerLast(ifilter string, labelKey string, labelValue string) { } defer cli.Close() - // Create filters containerFilters := filters.NewArgs() if ifilter != "" { containerFilters.Add("ancestor", ifilter) } + if labelKey != "" && labelValue != "" { + containerFilters.Add("label", fmt.Sprintf("%s=%s", labelKey, labelValue)) + } - containerFilters.Add("label", fmt.Sprintf("%s=%s", labelKey, labelValue)) // filter by label - - // List containers with the specified filter containers, err := cli.ContainerList(ctx, container.ListOptions{ All: true, Limit: 10, @@ -112,24 +346,129 @@ func DockerLast(ifilter string, labelKey string, labelValue string) { panic(err) } - rfutils.ClearScreen() - - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"Created", "Image", "Container ID", "Command"}) + //rfutils.ClearScreen() + tableData := [][]string{} for _, container := range containers { created := time.Unix(container.Created, 0).Format(time.RFC3339) - table.Append([]string{ + imageTag := container.Image + containerName := container.Names[0] + if containerName[0] == '/' { + containerName = containerName[1:] + } + containerID := container.ID[:12] + command := container.Command + tableData = append(tableData, []string{ created, - container.Image, - container.ID[:12], - container.Command, + imageTag, + containerName, + containerID, + command, }) } - table.Render() + headers := []string{"Created", "Image Tag", "Container Name", "Container ID", "Command"} + width, _, err := terminal.GetSize(int(os.Stdout.Fd())) + if err != nil { + width = 80 + } + + columnWidths := make([]int, len(headers)) + for i, header := range headers { + columnWidths[i] = len(header) + } + for _, row := range tableData { + for i, col := range row { + if len(col) > columnWidths[i] { + columnWidths[i] = len(col) + } + } + } + + totalWidth := len(headers) + 1 + for _, w := range columnWidths { + totalWidth += w + 2 + } + + if totalWidth > width { + excess := totalWidth - width + for i := range columnWidths { + reduction := excess / len(columnWidths) + if columnWidths[i] > reduction { + columnWidths[i] -= reduction + excess -= reduction + } + } + totalWidth = width + } + + pink := "\033[35m" + white := "\033[37m" + reset := "\033[0m" + title := "๐Ÿค– Last Run Containers" + + fmt.Printf("%s%s%s%s%s\n", pink, strings.Repeat(" ", 2), title, strings.Repeat(" ", totalWidth-2-len(title)), reset) + fmt.Print(white) + + printHorizontalBorder(columnWidths, "โ”Œ", "โ”ฌ", "โ”") + printRow(headers, columnWidths, "โ”‚") + printHorizontalBorder(columnWidths, "โ”œ", "โ”ผ", "โ”ค") + + for i, row := range tableData { + printRow(row, columnWidths, "โ”‚") + if i < len(tableData)-1 { + printHorizontalBorder(columnWidths, "โ”œ", "โ”ผ", "โ”ค") + } + } + + printHorizontalBorder(columnWidths, "โ””", "โ”ด", "โ”˜") + + fmt.Print(reset) + fmt.Println() +} + +func printHorizontalBorder(columnWidths []int, left, middle, right string) { + fmt.Print(left) + for i, width := range columnWidths { + fmt.Print(strings.Repeat("โ”€", width+2)) + if i < len(columnWidths)-1 { + fmt.Print(middle) + } + } + fmt.Println(right) } +func printRow(row []string, columnWidths []int, separator string) { + fmt.Print(separator) + for i, col := range row { + fmt.Printf(" %-*s ", columnWidths[i], col) + fmt.Print(separator) + } + fmt.Println() +} + +func distributeColumnWidths(availableWidth int, columnWidths []int) []int { + totalCurrentWidth := 0 + for _, width := range columnWidths { + totalCurrentWidth += width + } + for i := range columnWidths { + columnWidths[i] = int(float64(columnWidths[i]) / float64(totalCurrentWidth) * float64(availableWidth)) + if columnWidths[i] < 1 { + columnWidths[i] = 1 + } + } + return columnWidths +} + +func truncateString(s string, maxLength int) string { + if len(s) <= maxLength { + return s + } + return s[:maxLength-3] + "..." +} + + func latestDockerID(labelKey string, labelValue string) string { /* Get latest Docker container ID by image label in(1): string label key @@ -170,43 +509,108 @@ func latestDockerID(labelKey string, labelValue string) string { return latestContainer.ID } -func DockerExec(contid string, WorkingDir string) { +func getContainerProperties(ctx context.Context, cli *client.Client, containerID string) (map[string]string, error) { + containerJSON, err := cli.ContainerInspect(ctx, containerID) + if err != nil { + return nil, err + } + + // Extract the DISPLAY environment variable value + var xdisplay string + for _, env := range containerJSON.Config.Env { + if strings.HasPrefix(env, "DISPLAY=") { + xdisplay = strings.TrimPrefix(env, "DISPLAY=") + break + } + } + + // Get the image details to find the size + imageInfo, _, err := cli.ImageInspectWithRaw(ctx, containerJSON.Image) + if err != nil { + return nil, err + } + imageSize := fmt.Sprintf("%.2f MB", float64(imageInfo.Size)/1024/1024) + + props := map[string]string{ + "XDisplay": xdisplay, + "Shell": containerJSON.Path, + "Privileged": fmt.Sprintf("%v", containerJSON.HostConfig.Privileged), + "NetworkMode": string(containerJSON.HostConfig.NetworkMode), + "ImageName": containerJSON.Config.Image, + "ImageHash": imageInfo.ID, + "Bindings": strings.Join(containerJSON.HostConfig.Binds, ","), + "ExtraHosts": strings.Join(containerJSON.HostConfig.ExtraHosts, ","), + "Size": imageSize, + } + + return props, nil +} + +func DockerExec(containerIdentifier string, WorkingDir string) { /* - * Start last or specified container ID and execute a program inside - * in(1): string container ID + * Start last or specified container ID/name and execute a program inside + * in(1): string container ID or name */ + ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { - panic(err) + common.PrintErrorMessage(err) + return } defer cli.Close() - if contid == "" { + if containerIdentifier == "" { labelKey := "org.container.project" labelValue := "rfswift" - contid = latestDockerID(labelKey, labelValue) + containerIdentifier = latestDockerID(labelKey, labelValue) } - if err := cli.ContainerStart(ctx, contid, container.StartOptions{}); err != nil { - panic(err) + if err := cli.ContainerStart(ctx, containerIdentifier, container.StartOptions{}); err != nil { + common.PrintErrorMessage(err) + return + } + + common.PrintSuccessMessage(fmt.Sprintf("Container '%s' started successfully", containerIdentifier)) + + // Get container properties + props, err := getContainerProperties(ctx, cli, containerIdentifier) + if err != nil { + common.PrintErrorMessage(err) + return + } + + // Get the container name + containerJSON, err := cli.ContainerInspect(ctx, containerIdentifier) + if err != nil { + common.PrintErrorMessage(err) + return } + containerName := containerJSON.Name + if containerName[0] == '/' { + containerName = containerName[1:] + } + + // Placeholder size, as it would require additional API calls + size := props["Size"] + printContainerProperties(ctx, cli, containerName, props, size) - if dockerObj.shell == "/bin/bash" { - attachAndInteract(ctx, cli, contid) + if dockerObj.shell == dockerObj.shell { + attachAndInteract(ctx, cli, containerIdentifier) } else { - execCommandInContainer(ctx, cli, contid, WorkingDir) + execCommandInContainer(ctx, cli, containerIdentifier, WorkingDir) } } -func DockerRun() { +func DockerRun(containerName string) { /* - * Create a container and run it + * Create a container with a specific name and run it */ ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { - panic(err) + common.PrintErrorMessage(err) + return } defer cli.Close() @@ -232,9 +636,10 @@ func DockerRun() { Binds: bindings, Privileged: true, ExtraHosts: extrahosts, - }, &network.NetworkingConfig{}, nil, "") + }, &network.NetworkingConfig{}, nil, containerName) if err != nil { - panic(err) + common.PrintErrorMessage(err) + return } waiter, err := cli.ContainerAttach(ctx, resp.ID, container.AttachOptions{ @@ -244,21 +649,34 @@ func DockerRun() { Stream: true, }) if err != nil { - panic(err) + common.PrintErrorMessage(err) + return } defer waiter.Close() if err := cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil { - panic(err) + common.PrintErrorMessage(err) + return } + props, err := getContainerProperties(ctx, cli, resp.ID) + if err != nil { + common.PrintErrorMessage(err) + return + } + size := props["Size"] + printContainerProperties(ctx, cli, containerName, props, size) + + common.PrintSuccessMessage(fmt.Sprintf("Container '%s' started successfully", containerName)) + handleIOStreams(waiter) fd := int(os.Stdin.Fd()) if terminal.IsTerminal(fd) { oldState, err := terminal.MakeRaw(fd) if err != nil { - panic(err) + common.PrintErrorMessage(err) + return } defer terminal.Restore(fd, oldState) @@ -269,7 +687,6 @@ func DockerRun() { waitForContainer(ctx, cli, resp.ID) } - func execCommandInContainer(ctx context.Context, cli *client.Client, contid, WorkingDir string) { execShell := []string{} if dockerObj.shell != "" { @@ -343,7 +760,7 @@ func handleIOStreams(response types.HijackedResponse) { } func readAndWriteInput(response types.HijackedResponse) { - reader := bufio.NewReaderSize(os.Stdin, 1) + reader := bufio.NewReaderSize(os.Stdin, 4096) // Increased buffer size for larger inputs for { input, err := reader.ReadByte() if err != nil { @@ -453,7 +870,7 @@ func DockerPull(imageref string, imagetag string) { } } -func DockerRename(imageref string, imagetag string) { +func DockerTag(imageref string, imagetag string) { /* Rename an image to another name in(1): string Image reference in(2): string Image tag target @@ -473,9 +890,10 @@ func DockerRename(imageref string, imagetag string) { } } -func DockerRemove(contid string) { - /* Remove a container - in(1): string container ID +func DockerRename(currentIdentifier string, newName string) { + /* Rename a container by ID or name + in(1): string current container ID or name + in(2): string new container name */ ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) @@ -483,11 +901,72 @@ func DockerRemove(contid string) { panic(err) } defer cli.Close() - err = cli.ContainerRemove(ctx, contid, container.RemoveOptions{Force: true}) + + // Attempt to find the container by the identifier (name or ID) + containers, err := cli.ContainerList(ctx, container.ListOptions{All: true}) + if err != nil { + panic(err) + } + + var containerID string + for _, container := range containers { + if container.ID == currentIdentifier || container.Names[0] == "/"+currentIdentifier { + containerID = container.ID + break + } + } + + if containerID == "" { + log.Fatalf("Container with ID or name '%s' not found.", currentIdentifier) + } + + // Rename the container + err = cli.ContainerRename(ctx, containerID, newName) if err != nil { panic(err) } else { - fmt.Println("[+] Container removed!") + fmt.Printf("[+] Container '%s' renamed to '%s'!\n", currentIdentifier, newName) + } +} + +func DockerRemove(containerIdentifier string) { + /* Remove a container by ID or name + in(1): string container ID or name + */ + ctx := context.Background() + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + common.PrintErrorMessage(err) + return + } + defer cli.Close() + + // Attempt to find the container by the identifier (name or ID) + containers, err := cli.ContainerList(ctx, container.ListOptions{All: true}) + if err != nil { + common.PrintErrorMessage(err) + return + } + + var containerID string + for _, container := range containers { + if container.ID == containerIdentifier || container.Names[0] == "/"+containerIdentifier { + containerID = container.ID + break + } + } + + if containerID == "" { + common.PrintErrorMessage(fmt.Errorf("container with ID or name '%s' not found", containerIdentifier)) + return + } + + // Remove the container + err = cli.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: true}) + if err != nil { + common.PrintErrorMessage(err) + } else { + common.PrintSuccessMessage(fmt.Sprintf("Container '%s' removed successfully", containerIdentifier)) } } @@ -528,10 +1007,13 @@ func ListImages(labelKey string, labelValue string) ([]image.Summary, error) { } func PrintImagesTable(labelKey string, labelValue string) { - /* Print RF Swift Images in a table - in(1): string labelKey - in(2): string labelValue - */ + ctx := context.Background() + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + log.Fatalf("Error creating Docker client: %v", err) + } + defer cli.Close() + images, err := ListImages(labelKey, labelValue) if err != nil { log.Fatalf("Error listing images: %v", err) @@ -539,28 +1021,137 @@ func PrintImagesTable(labelKey string, labelValue string) { rfutils.ClearScreen() - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"Repository", "Tag", "Image ID", "Created", "Size"}) - + // Prepare table data + tableData := [][]string{} + maxStatusLength := 0 for _, image := range images { for _, repoTag := range image.RepoTags { repoTagParts := strings.Split(repoTag, ":") repository := repoTagParts[0] tag := repoTagParts[1] + + // Check image status + isUpToDate, isCustom, err := checkImageStatus(ctx, cli, repository, tag) + var status string + if err != nil { + status = "Error" + } else if isCustom { + status = "Custom" + } else if isUpToDate { + status = "Up to date" + } else { + status = "Obsolete" + } + + if len(status) > maxStatusLength { + maxStatusLength = len(status) + } + created := time.Unix(image.Created, 0).Format(time.RFC3339) size := fmt.Sprintf("%.2f MB", float64(image.Size)/1024/1024) - table.Append([]string{ + tableData = append(tableData, []string{ repository, tag, image.ID[:12], created, size, + status, }) } } - table.Render() + headers := []string{"Repository", "Tag", "Image ID", "Created", "Size", "Status"} + width, _, err := terminal.GetSize(int(os.Stdout.Fd())) + if err != nil { + width = 80 // default width if terminal size cannot be determined + } + + columnWidths := make([]int, len(headers)) + for i, header := range headers { + columnWidths[i] = len(header) + } + for _, row := range tableData { + for i, col := range row { + if len(col) > columnWidths[i] { + columnWidths[i] = len(col) + } + } + } + + // Ensure the "Status" column is wide enough + columnWidths[len(columnWidths)-1] = max(columnWidths[len(columnWidths)-1], maxStatusLength) + + // Adjust column widths to fit the terminal width + totalWidth := len(headers) + 1 // Adding 1 for the left border + for _, w := range columnWidths { + totalWidth += w + 2 // Adding 2 for padding + } + + if totalWidth > width { + excess := totalWidth - width + for i := range columnWidths[:len(columnWidths)-1] { // Don't reduce the last (Status) column + reduction := excess / (len(columnWidths) - 1) + if columnWidths[i] > reduction { + columnWidths[i] -= reduction + excess -= reduction + } + } + totalWidth = width + } + + yellow := "\033[33m" + white := "\033[37m" + reset := "\033[0m" + title := "๐Ÿ“ฆ RF Swift Images" + + fmt.Printf("%s%s%s%s%s\n", yellow, strings.Repeat(" ", 2), title, strings.Repeat(" ", totalWidth-2-len(title)), reset) + fmt.Print(white) + + printHorizontalBorder(columnWidths, "โ”Œ", "โ”ฌ", "โ”") + printRow(headers, columnWidths, "โ”‚") + printHorizontalBorder(columnWidths, "โ”œ", "โ”ผ", "โ”ค") + + for i, row := range tableData { + printRowWithColor(row, columnWidths, "โ”‚") + if i < len(tableData)-1 { + printHorizontalBorder(columnWidths, "โ”œ", "โ”ผ", "โ”ค") + } + } + + printHorizontalBorder(columnWidths, "โ””", "โ”ด", "โ”˜") + + fmt.Print(reset) + fmt.Println() +} + +func printRowWithColor(row []string, columnWidths []int, separator string) { + fmt.Print(separator) + for i, col := range row { + if i == len(row)-1 { // Status column + status := col + color := "" + switch status { + case "Custom": + color = "\033[33m" // Yellow + case "Up to date": + color = "\033[32m" // Green + case "Obsolete": + color = "\033[31m" // Red + case "Error": + color = "\033[31m" // Red + } + fmt.Printf(" %s%-*s\033[0m ", color, columnWidths[i], status) + } else { + fmt.Printf(" %-*s ", columnWidths[i], truncateString(col, columnWidths[i])) + } + fmt.Print(separator) + } + fmt.Println() +} +func stripAnsiCodes(s string) string { + ansi := regexp.MustCompile(`\x1b\[[0-9;]*m`) + return ansi.ReplaceAllString(s, "") } func DeleteImage(imageIDOrTag string) error { diff --git a/go/rfswift/dock/rfdock.go.old b/go/rfswift/dock/rfdock.go.old new file mode 100644 index 0000000..18e97f3 --- /dev/null +++ b/go/rfswift/dock/rfdock.go.old @@ -0,0 +1,805 @@ +/* This code is part of RF Switch by @Penthertz +* Author(s): Sรฉbastien Dudek (@FlUxIuS) + */ +package dock + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "os" + "strings" + "time" + "log" + + "context" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/jsonmessage" + "github.com/moby/term" + "golang.org/x/crypto/ssh/terminal" + "github.com/olekukonko/tablewriter" + rfutils "penthertz/rfswift/rfutils" + common "penthertz/rfswift/common" +) + +var inout chan []byte + +type DockerInst struct { + net string + privileged bool + xdisplay string + x11forward string + usbforward string + usbdevice string + shell string + imagename string + extrabinding string + entrypoint string + extrahosts string + extraenv string + pulse_server string + network_mode string +} + +var dockerObj = DockerInst{net: "host", + privileged: true, + xdisplay: "DISPLAY=:0", + entrypoint: "/bin/bash", + x11forward: "/tmp/.X11-unix:/tmp/.X11-unix", + usbforward: "/dev/bus/usb:/dev/bus/usb", + extrabinding: "/dev/ttyACM0:/dev/ttyACM0", // Some more if needed /run/dbus/system_bus_socket:/run/dbus/system_bus_socket,/dev/snd:/dev/snd,/dev/dri:/dev/dri + imagename: "myrfswift:latest", + extrahosts: "", + extraenv: "", + network_mode: "host", + pulse_server: "tcp:localhost:34567", + shell: "/bin/bash"} // Instance with default values + +func init() { + config, err := rfutils.ReadOrCreateConfig("config.ini") + if err != nil { + log.Printf("Error reading config: %v. Using default values.", err) + return + } + + // Update dockerObj with values from config + dockerObj.imagename = config.General.ImageName + dockerObj.shell = config.Container.Shell + dockerObj.network_mode = config.Container.Network + dockerObj.x11forward = config.Container.X11Forward + dockerObj.xdisplay = config.Container.XDisplay + dockerObj.extrahosts = config.Container.ExtraHost + dockerObj.extraenv = config.Container.ExtraEnv + dockerObj.pulse_server = config.Audio.PulseServer + + // Handle bindings + var bindings []string + for _, binding := range config.Container.Bindings { + if strings.Contains(binding, "/dev/bus/usb") { + dockerObj.usbforward = binding + } else if strings.Contains(binding, ".X11-unix") { + dockerObj.x11forward = binding + } else { + bindings = append(bindings, binding) + } + } + dockerObj.extrabinding = strings.Join(bindings, ",") +} + +func resizeTty(ctx context.Context, cli *client.Client, contid string, fd int) { + /** + * Resizes TTY to handle larger terminal window + */ + for { + width, height, err := terminal.GetSize(fd) + if err != nil { + panic(err) + } + + err = cli.ContainerResize(ctx, contid, container.ResizeOptions{ + Height: uint(height), + Width: uint(width), + }) + if err != nil { + panic(err) + } + + time.Sleep(1 * time.Second) + } +} + +func printContainerProperties(containerName string, props DockerInst, size string) { + white := "\033[37m" + blue := "\033[34m" + reset := "\033[0m" + properties := [][]string{ + {"Container Name", containerName}, + {"X Display", props.xdisplay}, + {"Shell", props.shell}, + {"Privileged Mode", fmt.Sprintf("%v", props.privileged)}, + {"Network Mode", props.network_mode}, + {"Image Name", props.imagename}, + {"Size on Disk", size}, + {"Bindings", props.extrabinding}, + {"Extra Hosts", props.extrahosts}, + } + + width, _, err := terminal.GetSize(int(os.Stdout.Fd())) + if err != nil { + width = 80 // default width if terminal size cannot be determined + } + + // Adjust width for table borders and padding + maxContentWidth := width - 4 + if maxContentWidth < 20 { + maxContentWidth = 20 // Minimum content width + } + + maxKeyLen := 0 + for _, property := range properties { + if len(property[0]) > maxKeyLen { + maxKeyLen = len(property[0]) + } + } + + maxValueLen := maxContentWidth - maxKeyLen - 7 // 7 for borders and spaces + if maxValueLen < 10 { + maxValueLen = 10 // Minimum value length + } + + totalWidth := maxKeyLen + maxValueLen + 7 + + // Print the title in blue, aligned to the left with some padding + title := "๐ŸงŠ Container Summary" + leftPadding := 2 // You can adjust this value for more or less left padding + fmt.Printf("%s%s%s%s%s\n", blue, strings.Repeat(" ", leftPadding), title, strings.Repeat(" ", totalWidth-leftPadding-len(title)), reset) + + fmt.Printf("%s", white) // Switch to white color for the box + fmt.Printf("โ•ญ%sโ•ฎ\n", strings.Repeat("โ”€", totalWidth-2)) + + for i, property := range properties { + key := property[0] + value := property[1] + + // Wrap long values + wrappedValue := wrapText(value, maxValueLen) + valueLines := strings.Split(wrappedValue, "\n") + + for j, line := range valueLines { + if j == 0 { + fmt.Printf("โ”‚ %-*s โ”‚ %-*s โ”‚\n", maxKeyLen, key, maxValueLen, line) + } else { + fmt.Printf("โ”‚ %-*s โ”‚ %-*s โ”‚\n", maxKeyLen, "", maxValueLen, line) + } + + if j < len(valueLines)-1 { + fmt.Printf("โ”‚%sโ”‚%sโ”‚\n", strings.Repeat(" ", maxKeyLen+2), strings.Repeat(" ", maxValueLen+2)) + } + } + + if i < len(properties)-1 { + fmt.Printf("โ”œ%sโ”ผ%sโ”ค\n", strings.Repeat("โ”€", maxKeyLen+2), strings.Repeat("โ”€", maxValueLen+2)) + } + } + + fmt.Printf("โ•ฐ%sโ•ฏ\n", strings.Repeat("โ”€", totalWidth-2)) + fmt.Printf("%s", reset) +} + +func wrapText(text string, maxWidth int) string { + var result strings.Builder + currentLineWidth := 0 + + words := strings.Fields(text) + for i, word := range words { + if currentLineWidth+len(word) > maxWidth { + if currentLineWidth > 0 { + result.WriteString("\n") + currentLineWidth = 0 + } + if len(word) > maxWidth { + for len(word) > maxWidth { + result.WriteString(word[:maxWidth] + "\n") + word = word[maxWidth:] + } + } + } + result.WriteString(word) + currentLineWidth += len(word) + if i < len(words)-1 && currentLineWidth+1+len(words[i+1]) <= maxWidth { + result.WriteString(" ") + currentLineWidth++ + } + } + + return result.String() +} + +func DockerLast(ifilter string, labelKey string, labelValue string) { + /* Lists 10 last Docker containers + in(1): string optional filter for image name + */ + ctx := context.Background() + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + panic(err) + } + defer cli.Close() + + // Create filters + containerFilters := filters.NewArgs() + if ifilter != "" { + containerFilters.Add("ancestor", ifilter) + } + + containerFilters.Add("label", fmt.Sprintf("%s=%s", labelKey, labelValue)) // filter by label + + // List containers with the specified filter + containers, err := cli.ContainerList(ctx, container.ListOptions{ + All: true, + Limit: 10, + Filters: containerFilters, + }) + if err != nil { + panic(err) + } + + rfutils.ClearScreen() + + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{"Created", "Image", "Container ID", "Command"}) + + for _, container := range containers { + created := time.Unix(container.Created, 0).Format(time.RFC3339) + table.Append([]string{ + created, + container.Image, + container.ID[:12], + container.Command, + }) + } + + table.Render() +} + +func latestDockerID(labelKey string, labelValue string) string { + /* Get latest Docker container ID by image label + in(1): string label key + in(2): string label value + out: string container ID + */ + ctx := context.Background() + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + panic(err) + } + defer cli.Close() + + // Filter containers by the specified image label + containerFilters := filters.NewArgs() + containerFilters.Add("label", fmt.Sprintf("%s=%s", labelKey, labelValue)) + + containers, err := cli.ContainerList(ctx, container.ListOptions{ + All: true, + Filters: containerFilters, + }) + if err != nil { + panic(err) + } + + var latestContainer types.Container + for _, container := range containers { + if latestContainer.ID == "" || container.Created > latestContainer.Created { + latestContainer = container + } + } + + if latestContainer.ID == "" { + fmt.Println("No container found with the specified image label.") + return "" + } + + return latestContainer.ID +} + +func DockerExec(containerIdentifier string, WorkingDir string) { + /* + * Start last or specified container ID/name and execute a program inside + * in(1): string container ID or name + */ + + ctx := context.Background() + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + common.PrintErrorMessage(err) + return + } + defer cli.Close() + + if containerIdentifier == "" { + labelKey := "org.container.project" + labelValue := "rfswift" + containerIdentifier = latestDockerID(labelKey, labelValue) + } + + if err := cli.ContainerStart(ctx, containerIdentifier, container.StartOptions{}); err != nil { + common.PrintErrorMessage(err) + return + } + + common.PrintSuccessMessage(fmt.Sprintf("Container '%s' started successfully", containerIdentifier)) + + // Placeholder size, as it would require additional API calls + size := "N/A" + printContainerProperties(containerIdentifier, dockerObj, size) + + if dockerObj.shell == dockerObj.shell { + attachAndInteract(ctx, cli, containerIdentifier) + } else { + execCommandInContainer(ctx, cli, containerIdentifier, WorkingDir) + } +} + +func DockerRun(containerName string) { + /* + * Create a container with a specific name and run it + */ + ctx := context.Background() + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + common.PrintErrorMessage(err) + return + } + defer cli.Close() + + bindings := combineBindings(dockerObj.x11forward, dockerObj.usbforward, dockerObj.extrabinding) + extrahosts := splitAndCombine(dockerObj.extrahosts) + dockerenv := combineEnv(dockerObj.xdisplay, dockerObj.pulse_server, dockerObj.extraenv) + + resp, err := cli.ContainerCreate(ctx, &container.Config{ + Image: dockerObj.imagename, + Cmd: []string{dockerObj.shell}, + Env: dockerenv, + OpenStdin: true, + StdinOnce: false, + AttachStdin: true, + AttachStdout: true, + AttachStderr: true, + Tty: true, + Labels: map[string]string{ + "org.container.project": "rfswift", + }, + }, &container.HostConfig{ + NetworkMode: container.NetworkMode(dockerObj.network_mode), + Binds: bindings, + Privileged: true, + ExtraHosts: extrahosts, + }, &network.NetworkingConfig{}, nil, containerName) + if err != nil { + common.PrintErrorMessage(err) + return + } + + waiter, err := cli.ContainerAttach(ctx, resp.ID, container.AttachOptions{ + Stderr: true, + Stdout: true, + Stdin: true, + Stream: true, + }) + if err != nil { + common.PrintErrorMessage(err) + return + } + defer waiter.Close() + + if err := cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil { + common.PrintErrorMessage(err) + return + } + + size := "N/A" // Placeholder for size, as it would require additional API calls + printContainerProperties(containerName, dockerObj, size) + + common.PrintSuccessMessage(fmt.Sprintf("Container '%s' started successfully", containerName)) + + handleIOStreams(waiter) + + fd := int(os.Stdin.Fd()) + if terminal.IsTerminal(fd) { + oldState, err := terminal.MakeRaw(fd) + if err != nil { + common.PrintErrorMessage(err) + return + } + defer terminal.Restore(fd, oldState) + + go resizeTty(ctx, cli, resp.ID, fd) + go readAndWriteInput(waiter) + } + + waitForContainer(ctx, cli, resp.ID) +} + +func execCommandInContainer(ctx context.Context, cli *client.Client, contid, WorkingDir string) { + execShell := []string{} + if dockerObj.shell != "" { + execShell = append(execShell, strings.Split(dockerObj.shell, " ")...) + } + + optionsCreate := types.ExecConfig{ + WorkingDir: WorkingDir, + AttachStdin: true, + AttachStdout: true, + AttachStderr: true, + Detach: false, + Privileged: true, + Tty: true, + Cmd: execShell, + } + + rst, err := cli.ContainerExecCreate(ctx, contid, optionsCreate) + if err != nil { + panic(err) + } + + optionsStartCheck := types.ExecStartCheck{ + Detach: false, + Tty: true, + } + + response, err := cli.ContainerExecAttach(ctx, rst.ID, optionsStartCheck) + if err != nil { + panic(err) + } + defer response.Close() + + handleIOStreams(response) + waitForContainer(ctx, cli, contid) +} + +func attachAndInteract(ctx context.Context, cli *client.Client, contid string) { + response, err := cli.ContainerAttach(ctx, contid, container.AttachOptions{ + Stderr: true, + Stdout: true, + Stdin: true, + Stream: true, + }) + if err != nil { + panic(err) + } + defer response.Close() + + handleIOStreams(response) + + fd := int(os.Stdin.Fd()) + if terminal.IsTerminal(fd) { + oldState, err := terminal.MakeRaw(fd) + if err != nil { + panic(err) + } + defer terminal.Restore(fd, oldState) + + go resizeTty(ctx, cli, contid, fd) + go readAndWriteInput(response) + } + + waitForContainer(ctx, cli, contid) +} + +func handleIOStreams(response types.HijackedResponse) { + go io.Copy(os.Stdout, response.Reader) + go io.Copy(os.Stderr, response.Reader) + go io.Copy(response.Conn, os.Stdin) +} + +func readAndWriteInput(response types.HijackedResponse) { + reader := bufio.NewReaderSize(os.Stdin, 4096) // Increased buffer size for larger inputs + for { + input, err := reader.ReadByte() + if err != nil { + return + } + response.Conn.Write([]byte{input}) + } +} + +func waitForContainer(ctx context.Context, cli *client.Client, contid string) { + statusCh, errCh := cli.ContainerWait(ctx, contid, container.WaitConditionNextExit) + select { + case err := <-errCh: + if err != nil { + panic(err) + } + case <-statusCh: + } +} + +func combineBindings(x11forward, usbforward, extrabinding string) []string { + bindings := append(strings.Split(x11forward, ","), strings.Split(usbforward, ",")...) + if extrabinding != "" { + bindings = append(bindings, strings.Split(extrabinding, ",")...) + } + return bindings +} + +func splitAndCombine(commaSeparated string) []string { + if commaSeparated == "" { + return []string{} + } + return strings.Split(commaSeparated, ",") +} + +func combineEnv(xdisplay, pulse_server, extraenv string) []string { + dockerenv := append(strings.Split(xdisplay, ","), "PULSE_SERVER="+pulse_server) + if extraenv != "" { + dockerenv = append(dockerenv, strings.Split(extraenv, ",")...) + } + return dockerenv +} + +func DockerCommit(contid string) { + ctx := context.Background() + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + panic(err) + } + defer cli.Close() + + if err := cli.ContainerStart(ctx, contid, container.StartOptions{}); err != nil { + panic(err) + } + + commitResp, err := cli.ContainerCommit(ctx, contid, container.CommitOptions{Reference: dockerObj.imagename}) + if err != nil { + panic(err) + } + fmt.Println(commitResp.ID) +} + +func DockerPull(imageref string, imagetag string) { + /* Pulls an image from a registry + in(1): string Image reference + in(2): string Image tag target + */ + + if imagetag == "" { // if tag is empty, keep same tag + imagetag = imageref + } + + ctx := context.Background() + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + panic(err) + } + defer cli.Close() + + out, err := cli.ImagePull(ctx, imageref, image.PullOptions{}) + if err != nil { + panic(err) + } + defer out.Close() + + fd, isTerminal := term.GetFdInfo(os.Stdout) + jsonDecoder := json.NewDecoder(out) + + for { + var msg jsonmessage.JSONMessage + if err := jsonDecoder.Decode(&msg); err == io.EOF { + break + } else if err != nil { + panic(err) + } + + if isTerminal { + _ = jsonmessage.DisplayJSONMessagesStream(out, os.Stdout, fd, isTerminal, nil) + } else { + fmt.Println(msg) + } + } + + err = cli.ImageTag(ctx, imageref, imagetag) + if err != nil { + panic(err) + } +} + +func DockerTag(imageref string, imagetag string) { + /* Rename an image to another name + in(1): string Image reference + in(2): string Image tag target + */ + ctx := context.Background() + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + panic(err) + } + defer cli.Close() + + err = cli.ImageTag(ctx, imageref, imagetag) + if err != nil { + panic(err) + } else { + fmt.Println("[+] Image renamed!") + } +} + +func DockerRename(currentIdentifier string, newName string) { + /* Rename a container by ID or name + in(1): string current container ID or name + in(2): string new container name + */ + ctx := context.Background() + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + panic(err) + } + defer cli.Close() + + // Attempt to find the container by the identifier (name or ID) + containers, err := cli.ContainerList(ctx, container.ListOptions{All: true}) + if err != nil { + panic(err) + } + + var containerID string + for _, container := range containers { + if container.ID == currentIdentifier || container.Names[0] == "/"+currentIdentifier { + containerID = container.ID + break + } + } + + if containerID == "" { + log.Fatalf("Container with ID or name '%s' not found.", currentIdentifier) + } + + // Rename the container + err = cli.ContainerRename(ctx, containerID, newName) + if err != nil { + panic(err) + } else { + fmt.Printf("[+] Container '%s' renamed to '%s'!\n", currentIdentifier, newName) + } +} + + +func DockerRemove(containerIdentifier string) { + /* Remove a container by ID or name + in(1): string container ID or name + */ + ctx := context.Background() + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + common.PrintErrorMessage(err) + return + } + defer cli.Close() + + // Attempt to find the container by the identifier (name or ID) + containers, err := cli.ContainerList(ctx, container.ListOptions{All: true}) + if err != nil { + common.PrintErrorMessage(err) + return + } + + var containerID string + for _, container := range containers { + if container.ID == containerIdentifier || container.Names[0] == "/"+containerIdentifier { + containerID = container.ID + break + } + } + + if containerID == "" { + common.PrintErrorMessage(fmt.Errorf("container with ID or name '%s' not found", containerIdentifier)) + return + } + + // Remove the container + err = cli.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: true}) + if err != nil { + common.PrintErrorMessage(err) + } else { + common.PrintSuccessMessage(fmt.Sprintf("Container '%s' removed successfully", containerIdentifier)) + } +} + + +func ListImages(labelKey string, labelValue string) ([]image.Summary, error) { + /* List RF Swift Images + in(1): string labelKey + in(2): string labelValue + out: Tuple ImageSummary, error + */ + ctx := context.Background() + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return nil, err + } + defer cli.Close() + + // Filter images by the specified image label + imagesFilters := filters.NewArgs() + imagesFilters.Add("label", fmt.Sprintf("%s=%s", labelKey, labelValue)) + + images, err := cli.ImageList(ctx, image.ListOptions{ + All: true, + Filters: imagesFilters, + }) + if err != nil { + return nil, err + } + + // Only display images with RepoTags + var filteredImages []image.Summary + for _, image := range images { + if len(image.RepoTags) > 0 { + filteredImages = append(filteredImages, image) + } + } + + return filteredImages, nil +} + +func PrintImagesTable(labelKey string, labelValue string) { + /* Print RF Swift Images in a table + in(1): string labelKey + in(2): string labelValue + */ + images, err := ListImages(labelKey, labelValue) + if err != nil { + log.Fatalf("Error listing images: %v", err) + } + + rfutils.ClearScreen() + + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{"Repository", "Tag", "Image ID", "Created", "Size"}) + + for _, image := range images { + for _, repoTag := range image.RepoTags { + repoTagParts := strings.Split(repoTag, ":") + repository := repoTagParts[0] + tag := repoTagParts[1] + created := time.Unix(image.Created, 0).Format(time.RFC3339) + size := fmt.Sprintf("%.2f MB", float64(image.Size)/1024/1024) + + table.Append([]string{ + repository, + tag, + image.ID[:12], + created, + size, + }) + } + } + + table.Render() +} + +func DeleteImage(imageIDOrTag string) error { + /* Delete an image + in(1): string image ID or tag + out: error + */ + ctx := context.Background() + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return err + } + defer cli.Close() + + _, err = cli.ImageRemove(ctx, imageIDOrTag, image.RemoveOptions{Force: true, PruneChildren: true}) + if err != nil { + return err + } + + fmt.Printf("Successfully deleted image: %s\n", imageIDOrTag) + return nil +} diff --git a/go/rfswift/go.mod b/go/rfswift/go.mod index cd6422d..54006cf 100644 --- a/go/rfswift/go.mod +++ b/go/rfswift/go.mod @@ -4,13 +4,16 @@ go 1.22.5 require ( github.com/cheggaaa/pb/v3 v3.1.5 - github.com/docker/docker v27.0.3+incompatible + github.com/docker/docker v27.1.1+incompatible + github.com/fatih/color v1.17.0 github.com/go-resty/resty/v2 v2.13.1 github.com/lawl/pulseaudio v0.0.0-20220626105240-976bed5e247c + github.com/mattn/go-runewidth v0.0.16 github.com/moby/term v0.5.0 github.com/olekukonko/tablewriter v0.0.5 github.com/spf13/cobra v1.8.1 golang.org/x/crypto v0.25.0 + golang.org/x/term v0.22.0 ) require ( @@ -21,15 +24,13 @@ require ( github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/fatih/color v1.15.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect @@ -45,6 +46,5 @@ require ( go.opentelemetry.io/otel/trace v1.28.0 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/sys v0.22.0 // indirect - golang.org/x/term v0.22.0 // indirect gotest.tools/v3 v3.5.1 // indirect ) diff --git a/go/rfswift/go.sum b/go/rfswift/go.sum index 2bf794d..d8ac8f5 100644 --- a/go/rfswift/go.sum +++ b/go/rfswift/go.sum @@ -17,14 +17,14 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE= -github.com/docker/docker v27.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= +github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= -github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -52,11 +52,11 @@ github.com/lawl/pulseaudio v0.0.0-20220626105240-976bed5e247c/go.mod h1:9h36x4KH github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= diff --git a/go/rfswift/main.go b/go/rfswift/main.go index 2502500..d8eae43 100644 --- a/go/rfswift/main.go +++ b/go/rfswift/main.go @@ -6,70 +6,46 @@ package main import ( "fmt" + "strings" cli "penthertz/rfswift/cli" + common "penthertz/rfswift/common" + rfutils "penthertz/rfswift/rfutils" ) -var version = "0.4.5" - -var ascii_art = ` - - - :===: - .*@*=:-*%@=. .:*@@@@@@@@@@%+: - :*@*+*=:::-:=@*. -%@%=**+==-:::::-%@%: - =@*==%=-: .. .=%- =@@=-*+===-::. ::::::+%- - :%.*::*@=-. .+::.@+ :@@=:*+===- .=**+==-::-*%+%. - .#@*+=...:*+*+...::*=:*%. +@%.+=+==:.:=****+-:. .:::.-#: - .#***%%+-:+= .=*.:.=#%-=%: .%@=.*+..:.:==***=..::::... .=*@=. - .+.%%*%%=-*: :+: .*=::.:%%=%+ .@@:=%*=-: -==..- :=-::. :-: *%= - :.=@@**#**:-%@@#%* -*-:*-.:+#@%#@%=*%#%= :==+*+:.-=:. :-==:.:=-=::*+#. - .:@@=%++*: :*++*: .#%==@@#=-:::-+##: :===++:.--.:#@@@@@@@@@==%==#=*@. - #@#%#=*= -+@%#*%*%@@@@#=: : :====++: :.:%@%*%@@%%%*=*@@@**%:@* - .%+*=%*=: .:- :+@=@@@@@@%%#+:=++=-++- ..#@* *@*#%*:::.:*@@-*%. - %-%-%-%@: . *%@*@@*=+****=::=++*#. =@@: +@*=:.*@= =@@%: - -*:%@=+@#+ .::: :%@%%@@@@@@@@@%%@@@+:::=%@*. :=#@+:+ :=%%::. :%@: - :: *@=%#=#*: . .*@@@@@@*-+#%*%@@%+====+%@%-:=*%%*.-@%=+* -=.*@:+ %% - . -@%@#-==#@+.=%@@@@%**@%%@*::===*@@@@@@@@@%*=:. .*@%=+@::=::=@%-* =@= - *%@% :===*%@@@@@@@@%*=*%*=+=::::::=*%@@@@%%%@@@%=-%%..=::=-%%:%. -@+ - =@% :===%@@@#+*@@@@@*:+@#=====: .:------=*@* :..=-:=%%=% =@= - -@%. :**@@@@%*==*@@@@@%%%%*======-::. :-=-:=.=@**- .%@. - .:=%%%*. :%%*:.:-=#@@=.:*%@@@@@@@%+::-============-::= :%@*-= .%@: - =@@#==%%== :==%@= :@%=-..:==*%@@@@@@@%+:. :+%@@#:+: -@@- - %%. .%@. : :%@+ .#@%*+=-:::-===+**#%%@@@@@@@@%%#*=: . =%@#. - :@+ :@%. .%@* .@@%@@@*===-:..:-============: ..-%@%=..=%%+ - %%.:%%: *@%. .%# .-*@@@%%*=--------:::::-+#%@@*:-*%*-:.+%: - .@*@* :@@- :=. :@#. :+#%%@@@@#==+#@@@%#+:.=%#= -@# - *@: :. *@@-**=. .@* :%@%@#. =@*: *@@. - :%*. .=. .-=%@%==#+: :@* :%=:@@##%#%@%#####. -%@@: - .%%. :*: :**%@@%@@#==%+. :@+ ..:%+@@@@@%. .@: :%@@@- - *@@%. .+*. *%@@@@@@@@@@#==@+: :@* .=#%@@@:**@@@@@+ .*:-%@@@#. - %@@@%: =*+: =@@@@@@@%%%@%@@**@@*: :=*@@%*=: :%%=***@%: =%@@@@%@= - %@@%@@+=+#=%@@@%%%%%%@@@@@@@@@@@=..*@@%- .=:.%#%: #@@@:-@@@@@@=+%. - :@@@@@@#=*%@@%%%%@@@@@@@@@@%=::*#*=: :=*%@*:=%+-%: :%%@@@@@@@= -%. - =@@@@@@@#@@@@@@@@@@@@@%=..-*- .-#@@%+. =%= .%*++=====%@@@@@@%: *%: - +@@@@@@@@@@@@@@@%-. :*%@@*:. -@= =%@@@@@@@@@@= :%= - *@@@@@@@@@%+: .. .=*@@@*- -%: .:=*%@@@@@@@%*@@@@%#%*. - :@@@@@@%: .:=:. :%@@@%- #@@@@@@@@@@%+. .*@@@@%- - .%@@+:.:=-:::*%@@@+::::::.. :-=+****==#@@# - =@=-=-:-=#@@@%* .:=+#%%@@@#*=::. .=.*@@@::. - .@*:==%@@@@+. ...:=*%@@#=. .@@@@+=@%: - *@#@@@@*: .:==:: =@@@@%. .=#+. - .@@@%- .= - **: - - 888~-_ 888~~ ,d88~~\ ,e, 88~\ d8 - 888 \ 888___ 8888 Y88b e / " _888__ _d88__ - 888 | 888 'Y88b Y88b d8b / 888 888 888 - 888 / 888 'Y88b, Y888/Y88b/ 888 888 888 - 888_-~ 888 8888 Y8/ Y8/ 888 888 888 - 888 ~-_ 888 \__88P' Y Y 888 888 "88_/ - - RF toolbox for HAMs and professionals -` +func DisplayVersion() { + owner := "PentHertz" + repo := "RF-Swift" + + release, err := rfutils.GetLatestRelease(owner, repo) + if err != nil { + fmt.Printf("Error getting latest release: %v\n", err) + return + } + + currentVersion := common.Version + latestVersion := release.TagName + + compareResult := rfutils.VersionCompare(currentVersion, latestVersion) + if compareResult >= 0 { + fmt.Printf("\033[35m[+]\033[0m \033[37mYou are running version: \033[33m%s\033[37m (Up to date)\033[0m\n", currentVersion) + } else { + fmt.Printf("\033[31m[!]\033[0m \033[37mYou are running version: \033[33m%s\033[37m (\033[31mObsolete\033[37m)\033[0m\n", currentVersion) + fmt.Printf("\033[35m[+]\033[0m Do you want to update to the latest version? (yes/no): ") + var updateResponse string + fmt.Scanln(&updateResponse) + + if strings.ToLower(updateResponse) != "yes" { + fmt.Println("Update aborted.") + return + } + + rfutils.GetLatestRFSwift() + } +} func main() { - fmt.Println(ascii_art) - fmt.Print("Version: ", version, "\n\n") - cli.Execute() -} + common.PrintASCII() + DisplayVersion() + cli.Execute() +} \ No newline at end of file diff --git a/go/rfswift/rfutils/configs.go b/go/rfswift/rfutils/configs.go new file mode 100644 index 0000000..900c557 --- /dev/null +++ b/go/rfswift/rfutils/configs.go @@ -0,0 +1,181 @@ +package rfutils + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +type Config struct { + General struct { + ImageName string + } + Container struct { + Shell string + Bindings []string + Network string + X11Forward string + XDisplay string + ExtraHost string + ExtraEnv string + } + Audio struct { + PulseServer string + } +} + +const ( + orangeColor = "\033[38;5;208m" + resetColor = "\033[0m" +) + +func printOrange(message string) { + fmt.Printf("%s%s%s\n", orangeColor, message, resetColor) +} + +func ReadOrCreateConfig(filename string) (*Config, error) { + config := &Config{} + + if _, err := os.Stat(filename); os.IsNotExist(err) { + printOrange("Config file not found. Would you like to create one with default values? (y/n)") + reader := bufio.NewReader(os.Stdin) + response, _ := reader.ReadString('\n') + if strings.ToLower(strings.TrimSpace(response)) == "y" { + if err := createDefaultConfig(filename); err != nil { + return nil, fmt.Errorf("error creating default config: %v", err) + } + printOrange("Default config file created.") + } else { + return nil, fmt.Errorf("config file not found and user chose not to create one") + } + } + + file, err := os.Open(filename) + if err != nil { + return nil, fmt.Errorf("error opening file: %v", err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + currentSection := "" + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { + currentSection = strings.ToLower(line[1 : len(line)-1]) + continue + } + + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid config line: %s", line) + } + + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + + switch currentSection { + case "general": + if key == "imagename" { + config.General.ImageName = value + } + case "container": + switch key { + case "shell": + config.Container.Shell = value + case "bindings": + config.Container.Bindings = strings.Split(value, ",") + case "network": + config.Container.Network = value + case "x11forward": + config.Container.X11Forward = value + case "xdisplay": + config.Container.XDisplay = value + case "extrahost": + config.Container.ExtraHost = value + case "extraenv": + config.Container.ExtraEnv = value + } + case "audio": + if key == "pulse_server" { + config.Audio.PulseServer = value + } + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error reading file: %v", err) + } + + // Check for missing values and prompt user + if config.General.ImageName == "" { + printOrange("Image name is missing in the config file.") + config.General.ImageName = promptForValue("Image name", "myrfswift:latest") + } + if config.Container.Shell == "" { + printOrange("Shell is missing in the config file.") + config.Container.Shell = promptForValue("Shell", "/bin/zsh") + } + if len(config.Container.Bindings) == 0 { + printOrange("Bindings are missing in the config file.") + bindings := promptForValue("Bindings (comma-separated)", "/dev/bus/usb:/dev/bus/usb,/run/dbus/system_bus_socket:/run/dbus/system_bus_socket,/dev/snd:/dev/snd,/dev/dri:/dev/dri") + config.Container.Bindings = strings.Split(bindings, ",") + } + if config.Container.Network == "" { + printOrange("Network is missing in the config file.") + config.Container.Network = promptForValue("Network", "host") + } + if config.Container.X11Forward == "" { + printOrange("X11 forwarding is missing in the config file.") + config.Container.X11Forward = promptForValue("X11 forwarding", "/tmp/.X11-unix:/tmp/.X11-unix") + } + if config.Container.XDisplay == "" { + printOrange("X Display is missing in the config file.") + config.Container.XDisplay = promptForValue("X Display", "DISPLAY=:0") + } + if config.Container.ExtraHost == "" { + printOrange("Extra host is missing in the config file.") + config.Container.ExtraHost = promptForValue("Extra host", "pluto.local:192.168.2.1") + } + if config.Audio.PulseServer == "" { + printOrange("PulseAudio server is missing in the config file.") + config.Audio.PulseServer = promptForValue("PulseAudio server", "tcp:localhost:34567") + } + + return config, nil +} + +func createDefaultConfig(filename string) error { + content := `[general] +imagename = myrfswift:latest + +[container] +shell = /bin/zsh +bindings = /dev/bus/usb:/dev/bus/usb,/run/dbus/system_bus_socket:/run/dbus/system_bus_socket,/dev/snd:/dev/snd,/dev/dri:/dev/dri +network = host +x11forward = /tmp/.X11-unix:/tmp/.X11-unix +xdisplay = "DISPLAY=:0" +extrahost = pluto.local:192.168.2.1 +extraenv = "" + +[audio] +pulse_server = tcp:localhost:34567 +` + return os.WriteFile(filename, []byte(content), 0644) +} + +func promptForValue(prompt, defaultValue string) string { + fmt.Printf("%s%s (default: %s):%s ", orangeColor, prompt, defaultValue, resetColor) + reader := bufio.NewReader(os.Stdin) + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(input) + if input == "" { + return defaultValue + } + return input +} \ No newline at end of file diff --git a/go/rfswift/rfutils/githutils.go b/go/rfswift/rfutils/githutils.go index de02cd4..67dba48 100644 --- a/go/rfswift/rfutils/githutils.go +++ b/go/rfswift/rfutils/githutils.go @@ -7,27 +7,23 @@ import ( "log" "net/http" "os" - "os/exec" "path/filepath" "runtime" "strconv" "strings" + "time" "github.com/cheggaaa/pb/v3" "github.com/go-resty/resty/v2" + + common "penthertz/rfswift/common" ) type Release struct { TagName string `json:"tag_name"` } -func getLatestRelease(owner string, repo string) (Release, error) { - /* - * Get Latest Release information - * in(1): owner string - * in(2): repository string - * out: status - */ +func GetLatestRelease(owner string, repo string) (Release, error) { client := resty.New() resp, err := client.R(). @@ -50,25 +46,11 @@ func getLatestRelease(owner string, repo string) (Release, error) { return release, nil } -func constructDownloadURL(owner, repo, tag, fileName string) string { - /* - * Construct download URL link for RF Swift release - * in(1): owner string - * in(2): repository string - * in(3): tag string - * in(4): filename string - * out: status - */ +func ConstructDownloadURL(owner, repo, tag, fileName string) string { return fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/%s", owner, repo, tag, fileName) } -func downloadFile(url, dest string) error { - /* - * Download RF swift binary realse - * in(1): string url - * in(2): destinarion string - * out: status - */ +func DownloadFile(url, dest string) error { resp, err := http.Get(url) if err != nil { return err @@ -91,6 +73,15 @@ func downloadFile(url, dest string) error { defer out.Close() bar := pb.Full.Start64(int64(size)) + bar.Set(pb.Bytes, true) + go func() { + colors := []string{"\033[31m", "\033[32m", "\033[33m", "\033[34m", "\033[35m", "\033[36m"} + for i := 0; bar.IsStarted(); i++ { + bar.SetTemplateString(fmt.Sprintf("%s{{counters . }} {{bar . }} {{percent . }}%%", colors[i%len(colors)])) + time.Sleep(100 * time.Millisecond) + } + }() + barReader := bar.NewProxyReader(resp.Body) _, err = io.Copy(out, barReader) bar.Finish() @@ -98,12 +89,7 @@ func downloadFile(url, dest string) error { return err } -func makeExecutable(path string) error { - /* - * Making downloaded RF Swift binary executable - * in(1): string path - * out: status - */ +func MakeExecutable(path string) error { err := os.Chmod(path, 0755) if err != nil { return err @@ -111,26 +97,12 @@ func makeExecutable(path string) error { return nil } -func replaceBinary(newBinaryPath, binaryName string) error { - /* - * Replace original RF Swift binary by the latest release - * in(1): latest binary string - * in(2): original binary string - * out: status - */ - // Ensure the new binary is executable - err := makeExecutable(newBinaryPath) - if err != nil { - return err - } - - // Determine the current binary path - currentBinaryPath, err := exec.LookPath(binaryName) +func ReplaceBinary(newBinaryPath, currentBinaryPath string) error { + err := MakeExecutable(newBinaryPath) if err != nil { return err } - // Replace the current binary with the new one err = os.Rename(newBinaryPath, currentBinaryPath) if err != nil { return err @@ -139,18 +111,58 @@ func replaceBinary(newBinaryPath, binaryName string) error { return nil } +func VersionCompare(v1, v2 string) int { + parts1 := strings.Split(v1, ".") + parts2 := strings.Split(v2, ".") + + for i := 0; i < len(parts1) && i < len(parts2); i++ { + p1, _ := strconv.Atoi(parts1[i]) + p2, _ := strconv.Atoi(parts2[i]) + + if p1 > p2 { + return 1 + } + if p1 < p2 { + return -1 + } + } + + if len(parts1) > len(parts2) { + return 1 + } + if len(parts1) < len(parts2) { + return -1 + } + + return 0 +} + func GetLatestRFSwift() { - /* - * Print latest RF Swift binary from official Penthertz' repository - */ owner := "PentHertz" repo := "RF-Swift" - release, err := getLatestRelease(owner, repo) + release, err := GetLatestRelease(owner, repo) if err != nil { log.Fatalf("Error getting latest release: %v", err) } + compareResult := VersionCompare(common.Version, release.TagName) + if compareResult >= 0 { + fmt.Printf("\033[35m[+]\033[0m \033[37mYou already have the latest version: \033[33m%s\033[0m\n", common.Version) + return + } else if compareResult < 0 { + fmt.Printf("\033[31m[!]\033[0m \033[37mYour current version (\033[33m%s\033[37m) is obsolete. Please update to version (\033[33m%s\033[37m).\n", common.Version, release.TagName) + } + + fmt.Printf("\033[35m[+]\033[0m Do you want to update to the latest version? (yes/no): ") + var updateResponse string + fmt.Scanln(&updateResponse) + + if strings.ToLower(updateResponse) != "yes" { + fmt.Println("Update aborted.") + return + } + arch := runtime.GOARCH goos := runtime.GOOS @@ -179,45 +191,42 @@ func GetLatestRFSwift() { log.Fatalf("Unsupported operating system: %s", goos) } - downloadURL := constructDownloadURL(owner, repo, release.TagName, fileName) + downloadURL := ConstructDownloadURL(owner, repo, release.TagName, fileName) fmt.Printf("Latest release download URL: %s\n", downloadURL) - fmt.Printf("Do you want to replace the existing 'rfswift' binary with this new release? (yes/no): ") + fmt.Printf("\033[35m[+]\033[0m Do you want to replace the existing binary with this new release? (yes/no): ") var response string fmt.Scanln(&response) + currentBinaryPath, err := os.Executable() + if err != nil { + log.Fatalf("Error determining the current executable path: %v", err) + } + if response == "yes" { tempDest := filepath.Join(os.TempDir(), fileName) - err = downloadFile(downloadURL, tempDest) + err = DownloadFile(downloadURL, tempDest) if err != nil { log.Fatalf("Error downloading file: %v", err) } - err = replaceBinary(tempDest, "./rfswift") + err = ReplaceBinary(tempDest, currentBinaryPath) if err != nil { log.Fatalf("Error replacing binary: %v", err) } fmt.Println("File downloaded and replaced successfully.") } else { - // Get the current binary directory - currentBinaryPath, err := exec.LookPath("./rfswift") - if err != nil { - log.Fatalf("Error locating current binary: %v", err) - } - var dest string ext := filepath.Ext(fileName) - name := strings.TrimSuffix(fileName, ext) - dest = filepath.Join(filepath.Dir(currentBinaryPath), fmt.Sprintf("%s_%s%s", name, release.TagName, ext)) + dest = filepath.Join(filepath.Dir(currentBinaryPath), fmt.Sprintf("%s_%s%s", strings.TrimSuffix(fileName, ext), release.TagName, ext)) - err = downloadFile(downloadURL, dest) + err = DownloadFile(downloadURL, dest) if err != nil { log.Fatalf("Error downloading file: %v", err) } - // Make the new binary executable - err = makeExecutable(dest) + err = MakeExecutable(dest) if err != nil { log.Fatalf("Error making binary executable: %v", err) } diff --git a/go/rfswift/rfutils/hostcli.go b/go/rfswift/rfutils/hostcli.go index a916f1a..6ada6a6 100644 --- a/go/rfswift/rfutils/hostcli.go +++ b/go/rfswift/rfutils/hostcli.go @@ -256,44 +256,57 @@ func checkPulseServer(address string, port string) { // Attempt to establish a connection conn, err := net.DialTimeout("tcp", endpoint, 5*time.Second) if err != nil { - fmt.Printf("\033[33mWarning: Unable to connect to Pulse server at %s\033[0m\n", endpoint) - printInstallationInstructions() + // Connection failed, prepare the error message + message := fmt.Sprintf("\033[33mWarning: Unable to connect to Pulse server at %s\033[0m\n", endpoint) + message += retInstallationInstructions() + + // Display the notification + DisplayNotification(" Warning", message, "warning") return } + // Close the connection if successful conn.Close() - fmt.Printf("Pulse server found at %s\n", endpoint) + + // Prepare success message + successMessage := fmt.Sprintf("Pulse server found at %s", endpoint) + + // Display success notification + DisplayNotification(" Audio", successMessage, "info") } -// printInstallationInstructions prints installation instructions based on the operating system -func printInstallationInstructions() { - os := runtime.GOOS - switch os { - case "windows": - fmt.Println("To install Pulse server on Windows, follow these steps:") - fmt.Println("1. Download the Pulse server installer from the official website.") - fmt.Println("2. Run the installer and follow the on-screen instructions.") - case "darwin": - fmt.Println("To install Pulse server on macOS, follow these steps:") - fmt.Println("1. Install Homebrew if you haven't already: /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"") - fmt.Println("2. Install Pulse server using Homebrew: brew install pulse-server") - case "linux": - if isArchLinux() { - fmt.Println("To install Pulse server on Arch Linux, follow these steps:") - fmt.Println("1. Update your package database: sudo pacman -Syu") - fmt.Println("2. Install Pulse server: sudo pacman -S pulse-server") - } else { - fmt.Println("To install Pulse server on Linux, follow these steps:") - fmt.Println("1. Update your package manager: sudo apt update (for Debian-based) or sudo yum update (for Red Hat-based).") - fmt.Println("2. Install Pulse server: sudo apt install pulse-server (for Debian-based) or sudo yum install pulse-server (for Red Hat-based).") - } - default: - fmt.Println("Unsupported operating system. Please refer to the official Pulse server documentation for installation instructions.") - } - // Print the final command to enable the module - fmt.Println("\nAfter installation, enable the module with the following command as unprivileged user:") - fmt.Println("\033[33m./rfswift host audio enable\033[0m") +func retInstallationInstructions() string { + var retstring strings.Builder + os := runtime.GOOS + + switch os { + case "windows": + retstring.WriteString("\nTo install Pulse server on Windows, follow these steps:\n") + retstring.WriteString("1. Download the Pulse server installer from the official website.\n") + retstring.WriteString("2. Run the installer and follow the on-screen instructions.\n") + case "darwin": + retstring.WriteString("To install Pulse server on macOS, follow these steps:\n") + retstring.WriteString("1. Install Homebrew if you haven't already: /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n") + retstring.WriteString("2. Install Pulse server using Homebrew: brew install pulse-server\n") + case "linux": + if isArchLinux() { + retstring.WriteString("\nTo install Pulse server on Arch Linux, follow these steps:\n") + retstring.WriteString("1. Update your package database: sudo pacman -Syu\n") + retstring.WriteString("2. Install Pulse server: sudo pacman -S pulse-server\n") + } else { + retstring.WriteString("To install Pulse server on Linux, follow these steps:\n") + retstring.WriteString("1. Update your package manager: sudo apt update (for Debian-based) or sudo yum update (for Red Hat-based).\n") + retstring.WriteString("2. Install Pulse server: sudo apt install pulse-server (for Debian-based) or sudo yum install pulse-server (for Red Hat-based).\n") + } + default: + retstring.WriteString("\nPlease refer to the official Pulse server documentation for installation instructions.\n") + } + + retstring.WriteString("\n\nAfter installation, enable the module with the following command as unprivileged user:\n") + retstring.WriteString("\033[33m./rfswift host audio enable\033[0m") + + return retstring.String() } // isArchLinux checks if the current Linux distribution is Arch Linux diff --git a/go/rfswift/rfutils/notifications.go b/go/rfswift/rfutils/notifications.go new file mode 100644 index 0000000..791a11c --- /dev/null +++ b/go/rfswift/rfutils/notifications.go @@ -0,0 +1,111 @@ +package rfutils + +import ( + "fmt" + "strings" + + "github.com/fatih/color" + "github.com/mattn/go-runewidth" + "golang.org/x/term" + "os" + "regexp" +) + +func DisplayNotification(title string, message string, notificationType string) { + // Get terminal width + width, _, err := term.GetSize(int(os.Stdout.Fd())) + if err != nil { + width = 80 // Default width if unable to determine + } + + // Adjust box width based on terminal width + boxWidth := width - 4 + if boxWidth < 20 { + boxWidth = 20 + } else if boxWidth > 100 { + boxWidth = 100 + } + + var titleColor *color.Color + var emoji string + + switch notificationType { + case "warning": + titleColor = color.New(color.FgYellow) + emoji = "โš ๏ธ" + case "error": + titleColor = color.New(color.FgRed) + emoji = "โŒ" + case "info": + titleColor = color.New(color.FgCyan) + emoji = "โ„น๏ธ" + default: + titleColor = color.New(color.FgWhite) + emoji = "๐Ÿ“" + } + + // Print top border + fmt.Printf("โ”Œ%sโ”\n", strings.Repeat("โ”€", boxWidth-2)) + + // Print title + titleLine := fmt.Sprintf(" %s %s", emoji, title) + paddedTitle := padRight(titleLine, boxWidth-2) + fmt.Print("โ”‚") + titleColor.Print(paddedTitle) + fmt.Println("โ”‚") + + // Print separator + fmt.Printf("โ”œ%sโ”ค\n", strings.Repeat("โ”€", boxWidth-2)) + + // Print message + lines := strings.Split(message, "\n") + for _, line := range lines { + wrappedLines := wrapText(line, boxWidth-4) + for _, wrappedLine := range wrappedLines { + paddedLine := padRight(wrappedLine, boxWidth-4) + fmt.Printf("โ”‚ %s โ”‚\n", paddedLine) + } + } + + // Print bottom border + fmt.Printf("โ””%sโ”˜\n", strings.Repeat("โ”€", boxWidth-2)) +} + +func padRight(s string, width int) string { + padWidth := width - runewidth.StringWidth(stripAnsi(s)) + if padWidth < 0 { + padWidth = 0 + } + return s + strings.Repeat(" ", padWidth) +} + +func wrapText(text string, width int) []string { + var lines []string + words := strings.Fields(strings.ReplaceAll(text, "\t", " ")) + currentLine := "" + + for _, word := range words { + if runewidth.StringWidth(stripAnsi(currentLine))+runewidth.StringWidth(stripAnsi(word))+1 <= width { + if currentLine != "" { + currentLine += " " + } + currentLine += word + } else { + if currentLine != "" { + lines = append(lines, currentLine) + } + currentLine = word + } + } + + if currentLine != "" { + lines = append(lines, currentLine) + } + + return lines +} + +func stripAnsi(str string) string { + re := regexp.MustCompile(`\x1b\[[0-9;]*m`) + return re.ReplaceAllString(str, "") +} \ No newline at end of file diff --git a/scripts/terminal_harness.sh b/scripts/terminal_harness.sh index fb980f7..e2acc4e 100644 --- a/scripts/terminal_harness.sh +++ b/scripts/terminal_harness.sh @@ -9,14 +9,11 @@ function zsh_tools_install() { goodecho "[+] Installing zsh" installfromnet "apt-fast -y install zsh" chsh -s /bin/zsh - zsh goodecho "[+] Installing oh-my-zsh" sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" goodecho "[+] Installing pluggins" - thedir="${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions" - mkdir -p thedir - cd thedir - installfromnet "git clone https://github.com/zsh-users/zsh-autosuggestions" + git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions + git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting } function arsenal_soft_install() {