package main import ( "fmt" "io" "log" "net/http" "os" "os/exec" "runtime" "strings" "github.com/charmbracelet/bubbles/progress" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" "github.com/spf13/cobra" "github.com/spf13/viper" ) type OS struct { ID string Name string Version string } func parseOsRelease(osRelease string) *OS { var result OS result.ID = "Unknown" result.Name = "Unknown" result.Version = "Unknown" lines := strings.Split(osRelease, "\n") for _, line := range lines { splitLine := strings.SplitN(line, "=", 2) if len(splitLine) != 2 { continue } switch splitLine[0] { case "ID": result.ID = strings.ToLower(strings.Trim(splitLine[1], "\"")) case "NAME": result.Name = strings.Trim(splitLine[1], "\"") case "VERSION_ID": result.Version = strings.Trim(splitLine[1], "\"") } } return &result } func getLinuxDistribution() (*OS, error) { _, err := os.Stat("/etc/os-release") if os.IsNotExist(err) { return nil, fmt.Errorf("unable to read system information") } osRelease, _ := os.ReadFile("/etc/os-release") return parseOsRelease(string(osRelease)), nil } func getPackageManager(os *OS) (string, error) { switch os.ID { case "debian", "ubuntu": return "apt", nil case "arch": return "pacman", nil case "fedora": return "dnf", nil default: var pmcommands = []string{ "apt", "dnf", "pacman", } for _, pmname := range pmcommands { _, err := exec.LookPath(pmname) if err == nil { return pmname, nil } } return "", fmt.Errorf("no packagemanager found for os: %s", os) } } func getSudoPassword() (string, error) { var password string form := huh.NewForm( huh.NewGroup( huh.NewInput(). Title("Bitte geben Sie Ihr sudo-Passwort ein"). Password(true). Value(&password), ), ).WithTheme(huh.ThemeCatppuccin()) err := form.Run() if err != nil { return "", fmt.Errorf("Fehler bei der Passwortabfrage: %v", err) } return password, nil } func getInstallCommand(pm string) (string, error) { switch pm { case "apt": return "apt install -y", nil case "pacman": return "pacman -S --noconfirm --needed", nil case "dnf": return "dnf install -y --best", nil default: return "", fmt.Errorf("no install command found for package manager: %s", pm) } } type specialSoftwareModel struct { items []string index int spinner spinner.Model progress progress.Model done bool sudoPassword string } func newSpecialSoftwareModel(sudoPassword string) specialSoftwareModel { // Definiere die zu installierenden Items items := []string{"oh-my-posh", "golang", "rust"} p := progress.New( progress.WithDefaultGradient(), progress.WithWidth(40), progress.WithoutPercentage(), ) s := spinner.New() s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("63")) return specialSoftwareModel{ items: items, spinner: s, progress: p, sudoPassword: sudoPassword, } } func (m specialSoftwareModel) Init() tea.Cmd { return tea.Batch(m.installItemCmd(m.items[m.index]), m.spinner.Tick) } func (m specialSoftwareModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "esc", "q": return m, tea.Quit } case installedItemMsg: if m.index >= len(m.items)-1 { m.done = true return m, tea.Quit } m.index++ progressCmd := m.progress.SetPercent(float64(m.index) / float64(len(m.items))) return m, tea.Batch( progressCmd, tea.Printf("%s %s", checkMark, m.items[m.index-1]), m.installItemCmd(m.items[m.index]), ) case spinner.TickMsg: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd } return m, nil } func (m specialSoftwareModel) View() string { if m.done { return doneStyle.Render(fmt.Sprintf("Spezielle Software Installation abgeschlossen!\n")) } spin := m.spinner.View() + " " prog := m.progress.View() info := fmt.Sprintf("Installiere %s", m.items[m.index]) return spin + info + " " + prog } type installedItemMsg string func (m specialSoftwareModel) installItemCmd(item string) tea.Cmd { return func() tea.Msg { switch item { case "oh-my-posh": if _, err := exec.LookPath("oh-my-posh"); err == nil { return installedItemMsg(item) } poshCommand := "curl -s https://ohmyposh.dev/install.sh | bash -s" if err := installPackage(poshCommand, "", ""); err != nil { log.Printf("Fehler beim Installieren von oh-my-posh: %v", err) } case "golang": if _, err := exec.LookPath("go"); err == nil { return installedItemMsg(item) } golangVersion := "1.23.4" if err := downloadGolang(golangVersion); err != nil { log.Printf("Fehler beim Herunterladen von Go: %v", err) } golangCommand := fmt.Sprintf("sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go%s.linux-%s.tar.gz", golangVersion, runtime.GOARCH) if err := installPackage(golangCommand, "", m.sudoPassword); err != nil { log.Printf("Fehler beim Installieren von Go: %v", err) } case "rust": if _, err := exec.LookPath("rustc"); err == nil { return installedItemMsg(item) } rustupCommand := "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -q -y" if err := installPackage(rustupCommand, "", ""); err != nil { log.Printf("Fehler beim Installieren von Rust: %v", err) } } return installedItemMsg(item) } } var unavailablePackages []string func installPackage(cmd, pkg, sudoPassword string) error { fullCmd := fmt.Sprintf("%s %s", cmd, pkg) command := exec.Command("sudo", "-S", "sh", "-c", fullCmd) command.Stdin = strings.NewReader(sudoPassword + "\n") output, err := command.CombinedOutput() if err != nil { if strings.Contains(string(output), "not found") || strings.Contains(string(output), "no matching package") || strings.Contains(string(output), "Keine Übereinstimmung") { unavailablePackages = append(unavailablePackages, pkg) return nil } return fmt.Errorf("failed to install %s: %v\n%s", pkg, err, string(output)) } return nil } func downloadGolang(golangVersion string) error { var link string if runtime.GOARCH == "arm64" { link = "https://go.dev/dl/go" + golangVersion + ".linux-arm64.tar.gz" } else { link = "https://go.dev/dl/go" + golangVersion + ".linux-amd64.tar.gz" } resp, err := http.Get(link) if err != nil { return fmt.Errorf("Fehler beim Herunterladen von Go: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("Unerwarteter HTTP-Status: %s", resp.Status) } outFile, err := os.Create("go" + golangVersion + ".linux-" + runtime.GOARCH + ".tar.gz") if err != nil { return fmt.Errorf("Fehler beim Erstellen der Ausgabedatei: %v", err) } defer outFile.Close() _, err = io.Copy(outFile, resp.Body) if err != nil { return fmt.Errorf("Fehler beim Schreiben der Ausgabedatei: %v", err) } return nil } func (m model) installSpecialSoftware() error { if _, err := exec.LookPath("oh-my-posh"); err == nil { fmt.Println("Oh-my-posh ist bereits installiert") } else { poshCommand := "curl -s https://ohmyposh.dev/install.sh | bash -s" if err := installPackage(poshCommand, "", ""); err != nil { return err } } if _, err := exec.LookPath("go"); err == nil { fmt.Println("Go ist bereits installiert.") } else { golangVersion := "1.23.4" if err := downloadGolang(golangVersion); err != nil { return fmt.Errorf("Fehler beim Herunterladen von Go: %v", err) } golangCommand := fmt.Sprintf("sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go%s.linux-%s.tar.gz", golangVersion, runtime.GOARCH) if err := installPackage(golangCommand, "", m.sudoPassword); err != nil { return err } } if _, err := exec.LookPath("rustc"); err == nil { fmt.Println("Rust ist bereits installiert.") } else { rustupCommand := "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -q -y" if err := installPackage(rustupCommand, "", ""); err != nil { return err } } // if _, err := exec.LookPath("pipx"); err == nil { // fmt.Println("Pipx ist bereits installiert.") // } else { // pipXCommand := "python3 -m pip install --user pipx && python3 -m pipx ensurepath" // if err := installPackage(pipXCommand, "", ""); err != nil { // return err // } // } return nil } type model struct { packages []string index int width int height int spinner spinner.Model progress progress.Model done bool sudoPassword string } var ( currentPkgNameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("211")) doneStyle = lipgloss.NewStyle().Margin(1, 2) checkMark = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).SetString("✓") ) func newModel(packages []string, sudoPassword string) model { p := progress.New( progress.WithDefaultGradient(), progress.WithWidth(40), progress.WithoutPercentage(), ) s := spinner.New() s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("63")) return model{ packages: packages, spinner: s, progress: p, sudoPassword: sudoPassword, } } func (m model) Init() tea.Cmd { return tea.Batch(m.installPackageCmd(m.packages[m.index]), m.spinner.Tick) } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.width, m.height = msg.Width, msg.Height case tea.KeyMsg: switch msg.String() { case "ctrl+c", "esc", "q": return m, tea.Quit } case installedPkgMsg: pkg := m.packages[m.index] if m.index >= len(m.packages)-1 { m.done = true return m, tea.Sequence( tea.Printf("%s %s", checkMark, pkg), tea.Quit, ) } m.index++ progressCmd := m.progress.SetPercent(float64(m.index) / float64(len(m.packages))) return m, tea.Batch( progressCmd, tea.Printf("%s %s", checkMark, pkg), m.installPackageCmd(m.packages[m.index]), ) case spinner.TickMsg: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd case progress.FrameMsg: newModel, cmd := m.progress.Update(msg) if newModel, ok := newModel.(progress.Model); ok { m.progress = newModel } return m, cmd } return m, nil } func (m model) View() string { n := len(m.packages) w := lipgloss.Width(fmt.Sprintf("%d", n)) if m.done { return doneStyle.Render(fmt.Sprintf("Done! Installed %d packages.\n", n)) } pkgCount := fmt.Sprintf(" %*d/%*d", w, m.index, w, n) spin := m.spinner.View() + " " prog := m.progress.View() cellsAvail := max(0, m.width-lipgloss.Width(spin+prog+pkgCount)) pkgName := currentPkgNameStyle.Render(m.packages[m.index]) info := lipgloss.NewStyle().MaxWidth(cellsAvail).Render("Installing " + pkgName) cellsRemaining := max(0, m.width-lipgloss.Width(spin+info+prog+pkgCount)) gap := strings.Repeat(" ", cellsRemaining) return spin + info + gap + prog + pkgCount } type installedPkgMsg string func (m model) installPackageCmd(pkg string) tea.Cmd { return func() tea.Msg { if err := installPackage(installCommand, pkg, m.sudoPassword); err != nil { log.Printf("Fehler beim Installieren von %s: %v", pkg, err) } return installedPkgMsg(pkg) } } func max(a, b int) int { if a > b { return a } return b } var rootCmd = &cobra.Command{ Use: "package-installer", Short: "Installiert Pakete basierend auf TOML-Konfiguration", Run: run, } type Packages struct { Headless []string `mapstructure:"headless"` NonHeadless []string `mapstructure:"non_headless"` } type SpecialPackages struct { Go []string `mapstructure:"go"` Cargo []string `mapstructure:"cargo"` Pipx []string `mapstructure:"pipx"` } type Config struct { Headless bool `mapstructure:"headless"` Packages Packages `mapstructure:"packages"` SpecialPackages SpecialPackages `mapstructure:"special_packages"` } var installCommand string func init() { cobra.OnInitialize(initConfig) rootCmd.PersistentFlags().StringP("config", "c", "", "Pfad zur Konfigurationsdatei") viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config")) } func initConfig() { if cfgFile := viper.GetString("config"); cfgFile != "" { viper.SetConfigFile(cfgFile) } else { viper.SetConfigName("config") viper.AddConfigPath(".") } if err := viper.ReadInConfig(); err != nil { fmt.Println("Fehler beim Lesen der Konfigurationsdatei:", err) os.Exit(1) } } func installSpecialPackages(sp SpecialPackages) error { for _, pkg := range sp.Go { cmd := exec.Command("go", "install", pkg+"@latest") if err := cmd.Run(); err != nil { return fmt.Errorf("Fehler bei der Installation von %s: %v", pkg, err) } } for _, pkg := range sp.Cargo { cmd := exec.Command("cargo", "install", pkg) if err := cmd.Run(); err != nil { return fmt.Errorf("Fehler bei der Installation von %s: %v", pkg, err) } } for _, pkg := range sp.Pipx { cmd := exec.Command("pipx", "install", pkg) if err := cmd.Run(); err != nil { return fmt.Errorf("Fehler bei der Installation von %s: %v", pkg, err) } } return nil } func run(cmd *cobra.Command, args []string) { os, err := getLinuxDistribution() if err != nil { log.Fatal(err) } pm, err := getPackageManager(os) if err != nil { log.Fatal(err) } installCommand, err = getInstallCommand(pm) if err != nil { log.Fatal(err) } sudoPassword, err := getSudoPassword() if err != nil { log.Fatal(err) } var cfg Config if err := viper.Unmarshal(&cfg); err != nil { log.Fatalf("Fehler beim Lesen der Konfiguration: %v", err) } form := huh.NewForm( huh.NewGroup( huh.NewConfirm(). Title("Möchten Sie eine headless Installation durchführen?"). Value(&cfg.Headless), ), ).WithTheme(huh.ThemeCatppuccin()) if err := form.Run(); err != nil { log.Fatalf("Fehler bei der Benutzerabfrage: %v", err) } var packages []string packages = append(packages, cfg.Packages.Headless...) if !cfg.Headless { packages = append(packages, cfg.Packages.NonHeadless...) } unavailablePackages = []string{} m := newModel(packages, sudoPassword) p := tea.NewProgram(m) if _, err := p.Run(); err != nil { log.Fatal(err) } printUnavailablePackages() var installSpecial bool form = huh.NewForm( huh.NewGroup( huh.NewConfirm(). Title("Möchten Sie die speziellen Pakete (Go, Rust, Pipx) installieren?"). Value(&installSpecial), ), ).WithTheme(huh.ThemeCatppuccin()) err = form.Run() if err != nil { log.Fatal(err) } if installSpecial { sm := newSpecialSoftwareModel(sudoPassword) p = tea.NewProgram(sm) if _, err := p.Run(); err != nil { log.Fatal(err) } } if len(cfg.SpecialPackages.Go) > 0 || len(cfg.SpecialPackages.Cargo) > 0 || len(cfg.SpecialPackages.Pipx) > 0 { // Combine all special packages for the progress bar var allSpecialPkgs []struct { typ string name string command string } for _, pkg := range cfg.SpecialPackages.Go { allSpecialPkgs = append(allSpecialPkgs, struct { typ string name string command string }{"go", pkg, "go install " + pkg + "@latest"}) } for _, pkg := range cfg.SpecialPackages.Cargo { allSpecialPkgs = append(allSpecialPkgs, struct { typ string name string command string }{"cargo", pkg, "cargo install " + pkg}) } for _, pkg := range cfg.SpecialPackages.Pipx { allSpecialPkgs = append(allSpecialPkgs, struct { typ string name string command string }{"pipx", pkg, "pipx install " + pkg}) } if len(allSpecialPkgs) > 0 { fmt.Println("\nInstalliere spezielle Pakete...") p := progress.New( progress.WithDefaultGradient(), progress.WithWidth(40), ) for i, pkg := range allSpecialPkgs { p.SetPercent(float64(i) / float64(len(allSpecialPkgs))) fmt.Printf("Installiere %s Paket: %s\n", pkg.typ, pkg.name) cmd := exec.Command("sh", "-c", pkg.command) if err := cmd.Run(); err != nil { log.Printf("Fehler bei der Installation von %s: %v", pkg.name, err) continue } } } } } func printUnavailablePackages() { if len(unavailablePackages) > 0 { fmt.Println("\nFolgende Pakete waren nicht verfügbar:") for _, pkg := range unavailablePackages { fmt.Printf("- %s\n", pkg) } } } func main() { if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) } }