/* This code is part of RF Switch by @Penthertz * Author(s): Sébastien Dudek (@FlUxIuS) */ package cli import ( "fmt" "os" "runtime" "path/filepath" "os/exec" "strings" "github.com/spf13/cobra" common "penthertz/rfswift/common" rfdock "penthertz/rfswift/dock" rfutils "penthertz/rfswift/rfutils" ) var DImage string var ContID string var ExecCmd string var FilterLast string var ExtraBind string var XDisplay string var SInstall string var ImageRef string var ImageTag string var ExtraHost string var UsbDevice string var PulseServer string var DockerName string var DockerNewName string var Bsource string var Btarget string var NetMode string var NetExporsedPorts string var NetBindedPorts string var Devices string var Privileged int var Caps string var Cgroups string var isADevice bool var Seccomp string var rootCmd = &cobra.Command{ Use: "rfswift", Short: "rfswift - you RF & HW swiss army", Long: `rfswift is THE toolbox for any HAM & radiocommunications and hardware professionals`, Run: func(cmd *cobra.Command, args []string) { fmt.Println("Use '-h' for help") }, } var runCmd = &cobra.Command{ Use: "run", Short: "Create and run a program", Long: `Create a container and run a program inside the docker container`, Run: func(cmd *cobra.Command, args []string) { os := runtime.GOOS if os == "windows" { rfdock.DockerSetx11("/run/desktop/mnt/host/wslg/.X11-unix:/tmp/.X11-unix,/run/desktop/mnt/host/wslg:/mnt/wslg") } else { rfutils.XHostEnable() // force xhost to add local connections ALCs, TODO: to optimize later } rfdock.DockerSetXDisplay(XDisplay) rfdock.DockerSetShell(ExecCmd) rfdock.DockerAddBinding(ExtraBind) rfdock.DockerSetImage(DImage) rfdock.DockerSetExtraHosts(ExtraHost) rfdock.DockerSetPulse(PulseServer) rfdock.DockerSetNetworkMode(NetMode) rfdock.DockerSetExposedPorts(NetExporsedPorts) rfdock.DockerSetBindexPorts(NetBindedPorts) rfdock.DockerAddDevices(Devices) rfdock.DockerAddCaps(Caps) rfdock.DockerAddCgroups(Cgroups) rfdock.DockerSetPrivileges(Privileged) rfdock.DockerSetSeccomp(Seccomp) if os == "linux" { // use pactl to configure ACLs rfutils.SetPulseCTL(PulseServer) } rfdock.DockerRun(DockerName) }, } var execCmd = &cobra.Command{ Use: "exec", Short: "Exec a command", Long: `Exec a program on a created docker container, even not started`, Run: func(cmd *cobra.Command, args []string) { os := runtime.GOOS if os == "windows" { rfdock.DockerSetx11("/run/desktop/mnt/host/wslg/.X11-unix:/tmp/.X11-unix,/run/desktop/mnt/host/wslg:/mnt/wslg") } else { rfutils.XHostEnable() // force xhost to add local connections ALCs, TODO: to optimize later } rfdock.DockerSetShell(ExecCmd) rfdock.DockerExec(ContID, "/root") }, } var lastCmd = &cobra.Command{ Use: "last", Short: "Last container run", Long: `Display the latest container that was run`, Run: func(cmd *cobra.Command, args []string) { labelKey := "org.container.project" labelValue := "rfswift" rfdock.DockerLast(FilterLast, labelKey, labelValue) }, } var installCmd = &cobra.Command{ Use: "install", Short: "Install function script", Long: `Install function script inside the container`, Run: func(cmd *cobra.Command, args []string) { rfdock.DockerSetShell(ExecCmd) rfdock.DockerInstallFromScript(ContID) }, } var commitCmd = &cobra.Command{ Use: "commit", Short: "Commit a container", Long: `Commit a container with change we have made`, Run: func(cmd *cobra.Command, args []string) { rfdock.DockerSetImage(DImage) rfdock.DockerCommit(ContID) }, } var stopCmd = &cobra.Command{ Use: "stop", Short: "Stop a container", Long: `Stop last or a particular container running`, Run: func(cmd *cobra.Command, args []string) { rfdock.DockerStop(ContID) }, } var pullCmd = &cobra.Command{ Use: "pull", Short: "Pull a container", Long: `Pull a container from internet`, Run: func(cmd *cobra.Command, args []string) { rfdock.DockerPull(ImageRef, ImageTag) }, } 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.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) }, } var removeCmd = &cobra.Command{ Use: "remove", Short: "Remove a container", Long: `Remore an existing container`, Run: func(cmd *cobra.Command, args []string) { rfdock.DockerRemove(ContID) }, } var winusbCmd = &cobra.Command{ Use: "winusb", Short: "Manage WinUSB devices", } var winusblistCmd = &cobra.Command{ Use: "list", Short: "List bus IDs", Long: `Lists all USB device connecter to the Windows host`, Run: func(cmd *cobra.Command, args []string) { devices, err := rfutils.ListUSBDevices() if err != nil { fmt.Println("Error:", err) return } fmt.Println("USB Devices:") for _, device := range devices { fmt.Printf("BusID: %s, DeviceID: %s, VendorID: %s, ProductID: %s, Description: %s\n", device.BusID, device.DeviceID, device.VendorID, device.ProductID, device.Description) } }, } var winusbattachCmd = &cobra.Command{ Use: "attach", Short: "Attach a bus ID", Long: `Attach a bus ID from the host to containers`, Run: func(cmd *cobra.Command, args []string) { rfutils.BindAndAttachDevice(UsbDevice) }, } var winusbdetachCmd = &cobra.Command{ Use: "detach", Short: "Detach a bus ID", Long: `Detach a bus ID from the host to containers`, Run: func(cmd *cobra.Command, args []string) { rfutils.BindAndAttachDevice(UsbDevice) }, } var ImagesCmd = &cobra.Command{ Use: "images", Short: "RF Swift images management remote/local", Long: `List local and remote images`, } var ImagesLocalCmd = &cobra.Command{ Use: "local", Short: "List local images", Long: `List pulled and built images`, Run: func(cmd *cobra.Command, args []string) { labelKey := "org.container.project" labelValue := "rfswift" rfdock.PrintImagesTable(labelKey, labelValue) }, } var ImagesRemoteCmd = &cobra.Command{ Use: "remote", Short: "List remote images", Long: `Lists RF Swift images from official repository`, Run: func(cmd *cobra.Command, args []string) { rfdock.ListDockerImagesRepo() }, } var ImagesPullCmd = &cobra.Command{ Use: "pull", Short: "Pull a container", Long: `Pull a container from internet`, Run: func(cmd *cobra.Command, args []string) { rfdock.DockerPull(ImageRef, ImageTag) }, } var DeleteCmd = &cobra.Command{ Use: "delete", Short: "Delete an rfswift images", Long: `Delete an RF Swift image from image name or tag`, Run: func(cmd *cobra.Command, args []string) { rfdock.DeleteImage(DImage) }, } var HostCmd = &cobra.Command{ Use: "host", Short: "Host configuration", Long: `Configures the host for container operations`, } var HostPulseAudioCmd = &cobra.Command{ Use: "audio", Short: "Pulseaudio server", Long: `Manage pulseaudio server`, } var HostPulseAudioEnableCmd = &cobra.Command{ Use: "enable", Short: "Enable connection", Long: `Allow connections to a specific port and interface. Warning: command to be executed as user!`, Run: func(cmd *cobra.Command, args []string) { rfutils.SetPulseCTL(PulseServer) }, } var HostPulseAudioUnloadCmd = &cobra.Command{ Use: "unload", Short: "Unload TCP module from Pulseaudio server", Run: func(cmd *cobra.Command, args []string) { rfutils.UnloadPulseCTL() }, } var UpdateCmd = &cobra.Command{ Use: "update", Short: "Update RF Swift", Long: `Update RF Swift binary from official Penthertz' repository`, Run: func(cmd *cobra.Command, args []string) { rfutils.GetLatestRFSwift() }, } var BindingsCmd = &cobra.Command{ Use: "bindings", Short: "Manage devices and volumes bindings", Long: `Add, or remove, a binding for a container`, } var BindingsAddCmd = &cobra.Command{ Use: "add", Short: "Add a binding", Long: `Adding a new binding for a container ID`, Run: func(cmd *cobra.Command, args []string) { if isADevice == true { rfdock.UpdateDeviceBinding(ContID, Bsource, Btarget, true) } else { rfdock.UpdateMountBinding(ContID, Bsource, Btarget, true) } }, } var BindingsRmCmd = &cobra.Command{ Use: "rm", Short: "Remove a binding", Long: `Remove a new binding for a container ID`, Run: func(cmd *cobra.Command, args []string) { if isADevice == true { rfdock.UpdateDeviceBinding(ContID, Bsource, Btarget, false) } else { rfdock.UpdateMountBinding(ContID, Bsource, Btarget, false) } }, } var completionCmd = &cobra.Command{ Use: "completion [bash|zsh|fish|powershell]", Short: "Generate and install completion script", Long: `Generate and install completion script for rfswift. To load completions: Bash: $ rfswift completion bash > /etc/bash_completion.d/rfswift # or $ rfswift completion bash > ~/.bash_completion Zsh: $ rfswift completion zsh > "${fpath[1]}/_rfswift" # or $ rfswift completion zsh > ~/.zsh/completion/_rfswift Fish: $ rfswift completion fish > ~/.config/fish/completions/rfswift.fish PowerShell: PS> rfswift completion powershell > rfswift.ps1 `, ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { var shell string if len(args) > 0 { shell = args[0] } else { shell = detectShell() common.PrintInfoMessage(fmt.Sprintf("Detected shell: %s", shell)) } installCompletion(shell) }, } func detectShell() string { shell := os.Getenv("SHELL") if shell == "" { if runtime.GOOS == "windows" { // Default to PowerShell on Windows return "powershell" } // Default to bash return "bash" } // Extract the shell name from the path shell = filepath.Base(shell) switch shell { case "bash", "zsh", "fish": return shell default: return "bash" // Default to bash } } func installCompletion(shell string) { var err error var dir string var filename string fmt.Println("🔍 Finding appropriate completion directory for " + shell + "...") switch shell { case "bash": // Try common bash completion directories if runtime.GOOS == "darwin" { // macOS often uses homebrew's bash completion if _, err := os.Stat("/usr/local/etc/bash_completion.d"); err == nil { dir = "/usr/local/etc/bash_completion.d" } else { // Fallback to user's home directory dir = filepath.Join(os.Getenv("HOME"), ".bash_completion.d") os.MkdirAll(dir, 0755) } } else { // Linux if _, err := os.Stat("/etc/bash_completion.d"); err == nil { dir = "/etc/bash_completion.d" } else { // Fallback to user's home directory dir = filepath.Join(os.Getenv("HOME"), ".bash_completion.d") os.MkdirAll(dir, 0755) } } filename = "rfswift" case "zsh": // Try common zsh completion directories var zshCompletionDirs []string homeDir := os.Getenv("HOME") // Check fpath directories fpathCmd := exec.Command("zsh", "-c", "echo ${fpath[1]}") fpathOutput, err := fpathCmd.Output() if err == nil && len(fpathOutput) > 0 { zshCompletionDirs = append(zshCompletionDirs, strings.TrimSpace(string(fpathOutput))) } // Common locations zshCompletionDirs = append(zshCompletionDirs, filepath.Join(homeDir, ".zsh/completion"), filepath.Join(homeDir, ".oh-my-zsh/completions"), "/usr/local/share/zsh/site-functions", "/usr/share/zsh/vendor-completions", ) // Find first existing directory for _, d := range zshCompletionDirs { if _, err := os.Stat(d); err == nil { dir = d common.PrintInfoMessage(fmt.Sprintf("Found existing completion directory: %s", dir)) break } } // If no directory exists, create one if dir == "" { dir = filepath.Join(homeDir, ".zsh/completion") common.PrintInfoMessage(fmt.Sprintf("Creating completion directory: %s", dir)) os.MkdirAll(dir, 0755) } filename = "_rfswift" case "fish": // Fish completion directory dir = filepath.Join(os.Getenv("HOME"), ".config/fish/completions") os.MkdirAll(dir, 0755) filename = "rfswift.fish" case "powershell": // PowerShell profile directory output, err := exec.Command("powershell", "-Command", "echo $PROFILE").Output() if err == nil { profileDir := filepath.Dir(strings.TrimSpace(string(output))) dir = filepath.Join(profileDir, "CompletionScripts") } else { dir = filepath.Join(os.Getenv("USERPROFILE"), "Documents", "WindowsPowerShell", "CompletionScripts") } os.MkdirAll(dir, 0755) filename = "rfswift.ps1" default: common.PrintErrorMessage(fmt.Errorf("Unsupported shell: %s", shell)) os.Exit(1) } filepath := filepath.Join(dir, filename) fmt.Println("📝 Installing completion script to " + filepath) file, err := os.Create(filepath) if err != nil { if os.IsPermission(err) { common.PrintErrorMessage(fmt.Errorf("Permission denied when writing to %s", filepath)) common.PrintWarningMessage("Try running with sudo or choose a different directory.") } else { common.PrintErrorMessage(fmt.Errorf("Error creating file: %v", err)) } os.Exit(1) } defer file.Close() // Generate completion script common.PrintInfoMessage(fmt.Sprintf("Generating completion script for %s...", shell)) switch shell { case "bash": rootCmd.GenBashCompletion(file) case "zsh": rootCmd.GenZshCompletion(file) // Add compdef line at the beginning content, err := os.ReadFile(filepath) if err == nil { newContent := []byte("#compdef rfswift\n" + string(content)) os.WriteFile(filepath, newContent, 0644) } case "fish": rootCmd.GenFishCompletion(file, true) case "powershell": rootCmd.GenPowerShellCompletion(file) } os.Chmod(filepath, 0644) common.PrintSuccessMessage(fmt.Sprintf("Completion script installed successfully to %s", filepath)) // Instructions for shell configuration fmt.Println("\n📋 Configuration Instructions:") switch shell { case "zsh": common.PrintInfoMessage("To enable completions, add the following to your ~/.zshrc:") fmt.Println("fpath=(" + dir + " $fpath)") fmt.Println("autoload -Uz compinit") fmt.Println("compinit") common.PrintInfoMessage("Then restart your shell or run: source ~/.zshrc") case "bash": common.PrintInfoMessage("To enable completions, add the following to your ~/.bashrc:") fmt.Printf("[[ -f %s ]] && source %s\n", filepath, filepath) common.PrintInfoMessage("Then restart your shell or run: source ~/.bashrc") case "fish": common.PrintSuccessMessage("Completions should be automatically loaded by fish.") case "powershell": common.PrintInfoMessage("To enable completions, add the following to your PowerShell profile:") fmt.Printf(". '%s'\n", filepath) } fmt.Println("\n🚀 Happy tab-completing with rfswift!") } func init() { isCompletion := false rootCmd.AddCommand(completionCmd) rootCmd.AddCommand(runCmd) rootCmd.AddCommand(lastCmd) rootCmd.AddCommand(execCmd) rootCmd.AddCommand(commitCmd) rootCmd.AddCommand(renameCmd) rootCmd.AddCommand(retagCmd) rootCmd.AddCommand(installCmd) rootCmd.AddCommand(removeCmd) rootCmd.AddCommand(ImagesCmd) rootCmd.AddCommand(DeleteCmd) rootCmd.AddCommand(HostCmd) rootCmd.AddCommand(UpdateCmd) rootCmd.AddCommand(BindingsCmd) rootCmd.AddCommand(stopCmd) rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { if len(os.Args) > 1 { if (os.Args[1] == "completion") || (os.Args[1] == "__complete") { isCompletion = true } } if isCompletion == false { rfutils.DisplayVersion() } } rootCmd.PersistentFlags().BoolVarP(&common.Disconnected, "disconnect", "q", false, "Don't query updates (disconnected mode)") // Adding special commands for Windows os := runtime.GOOS if os == "windows" { rootCmd.AddCommand(winusbCmd) winusbCmd.AddCommand(winusblistCmd) winusbCmd.AddCommand(winusbattachCmd) winusbCmd.AddCommand(winusbdetachCmd) winusbattachCmd.Flags().StringVarP(&UsbDevice, "busid", "i", "", "busid") winusbdetachCmd.Flags().StringVarP(&UsbDevice, "busid", "i", "", "busid") } ImagesCmd.AddCommand(pullCmd) ImagesCmd.AddCommand(ImagesRemoteCmd) ImagesCmd.AddCommand(ImagesLocalCmd) pullCmd.Flags().StringVarP(&ImageRef, "image", "i", "", "image reference") pullCmd.Flags().StringVarP(&ImageTag, "tag", "t", "", "rename to target tag") pullCmd.MarkFlagRequired("image") HostCmd.AddCommand(HostPulseAudioCmd) HostPulseAudioCmd.AddCommand(HostPulseAudioEnableCmd) HostPulseAudioCmd.AddCommand(HostPulseAudioUnloadCmd) HostPulseAudioEnableCmd.Flags().StringVarP(&PulseServer, "pulseserver", "s", "tcp:127.0.0.1:34567", "pulse server address (by default: 'tcp:127.0.0.1:34567')") DeleteCmd.Flags().StringVarP(&DImage, "image", "i", "", "image ID or tag") removeCmd.Flags().StringVarP(&ContID, "container", "c", "", "container to remove") installCmd.Flags().StringVarP(&ExecCmd, "install", "i", "", "function for installation") installCmd.Flags().StringVarP(&ContID, "container", "c", "", "container to run") //pullCmd.MarkFlagRequired("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") commitCmd.MarkFlagRequired("image") execCmd.Flags().StringVarP(&ContID, "container", "c", "", "container to run") execCmd.Flags().StringVarP(&ExecCmd, "command", "e", "/bin/bash", "command to exec (by default: /bin/bash)") execCmd.Flags().StringVarP(&SInstall, "install", "i", "", "install from function script (e.g: 'sdrpp_soft_install')") //execCmd.MarkFlagRequired("command") runCmd.Flags().StringVarP(&ExtraHost, "extrahosts", "x", "", "set extra hosts (default: 'pluto.local:192.168.1.2', and separate them with commas)") runCmd.Flags().StringVarP(&XDisplay, "display", "d", rfutils.GetDisplayEnv(), "set X Display (duplicates hosts's env by default)") runCmd.Flags().StringVarP(&ExecCmd, "command", "e", "", "command to exec (by default: '/bin/bash')") 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.Flags().StringVarP(&NetMode, "network", "t", "", "Network mode (default: 'host')") runCmd.Flags().StringVarP(&Devices, "devices", "s", "", "extra devices mapping (separate them with commas)") runCmd.Flags().IntVarP(&Privileged, "privileged", "u", 0, "Set privilege level (1: privileged, 0: unprivileged)") runCmd.Flags().StringVarP(&Caps, "capabilities", "a", "", "extra capabilities (separate them with commas)") runCmd.Flags().StringVarP(&Cgroups, "cgroups", "g", "", "extra cgroup rules (separate them with commas)") runCmd.Flags().StringVarP(&Seccomp, "seccomp", "m", "", "Set Seccomp profile ('default' one used by default)") runCmd.MarkFlagRequired("name") runCmd.Flags().StringVarP(&NetExporsedPorts, "exposedports", "z", "", "Exposed ports") runCmd.Flags().StringVarP(&NetBindedPorts, "bindedports", "w", "", "Exposed ports") lastCmd.Flags().StringVarP(&FilterLast, "filter", "f", "", "filter by image name") stopCmd.Flags().StringVarP(&ContID, "container", "c", "", "container to stop") BindingsCmd.AddCommand(BindingsAddCmd) BindingsCmd.PersistentFlags().BoolVarP(&isADevice, "devices", "d", false, "Manage a device rather than a volume") BindingsCmd.AddCommand(BindingsRmCmd) BindingsAddCmd.Flags().StringVarP(&ContID, "container", "c", "", "container to run") BindingsAddCmd.Flags().StringVarP(&Bsource, "source", "s", "", "source binding (by default: source=target)") BindingsAddCmd.Flags().StringVarP(&Btarget, "target", "t", "", "target binding") BindingsAddCmd.MarkFlagRequired("container") BindingsAddCmd.MarkFlagRequired("target") BindingsRmCmd.Flags().StringVarP(&ContID, "container", "c", "", "container to run") BindingsRmCmd.Flags().StringVarP(&Bsource, "source", "s", "", "source binding (by default: source=target)") BindingsRmCmd.Flags().StringVarP(&Btarget, "target", "t", "", "target binding") BindingsRmCmd.MarkFlagRequired("container") BindingsRmCmd.MarkFlagRequired("target") } func Execute() { if err := rootCmd.Execute(); err != nil { fmt.Fprintf(os.Stderr, "Whoops. There was an error while executing your CLI '%s'", err) os.Exit(1) } }