package steps import ( "bufio" "fmt" "miasma-installer/config" "miasma-installer/tui/styles" "os" "os/exec" "path/filepath" "strings" "time" tea "github.com/charmbracelet/bubbletea" ) type InstallModel struct { config *config.InstallConfig Finished bool status string progress []string err error currentStep int totalSteps int } func NewInstallModel(disk string, enableLUKS bool, hostname string, username string, userPassword string, rootPassword string) InstallModel { return InstallModel{ config: &config.InstallConfig{ Disk: disk, EnableLUKS: enableLUKS, Hostname: hostname, Username: username, UserPassword: userPassword, RootPassword: rootPassword, Timezone: "UTC", Locale: "en_US.UTF-8", }, status: "Preparing installation...", progress: []string{}, currentStep: 0, totalSteps: 8, } } func (m InstallModel) Init() tea.Cmd { return m.startInstall() } type installCompleteMsg struct { err error } type installProgressMsg struct { step int message string } func (m InstallModel) startInstall() tea.Cmd { return func() tea.Msg { if err := m.performInstall(); err != nil { return installCompleteMsg{err: err} } return installCompleteMsg{err: nil} } } func (m InstallModel) performInstall() error { m.updateProgress(1, "Partitioning disk...") if err := m.partitionDisk(); err != nil { return fmt.Errorf("partitioning failed: %w", err) } m.updateProgress(2, "Formatting partitions...") if err := m.formatPartitions(); err != nil { return fmt.Errorf("formatting failed: %w", err) } m.updateProgress(3, "Mounting filesystems...") if err := m.mountFilesystems(); err != nil { return fmt.Errorf("mounting failed: %w", err) } m.updateProgress(4, "Installing base system...") if err := m.installBase(); err != nil { return fmt.Errorf("base install failed: %w", err) } m.updateProgress(5, "Configuring system...") if err := m.configureSystem(); err != nil { return fmt.Errorf("configuration failed: %w", err) } m.updateProgress(6, "Installing bootloader...") if err := m.installBootloader(); err != nil { return fmt.Errorf("bootloader install failed: %w", err) } m.updateProgress(7, "Installing desktop and packages...") if err := m.installDesktop(); err != nil { return fmt.Errorf("desktop install failed: %w", err) } m.updateProgress(8, "Running post-install scripts...") if err := m.runPostInstallScripts(); err != nil { return fmt.Errorf("post-install scripts failed: %w", err) } return nil } func (m InstallModel) updateProgress(step int, message string) { m.currentStep = step m.status = message time.Sleep(100 * time.Millisecond) } func (m InstallModel) partitionDisk() error { disk := m.config.Disk wipeDisk := exec.Command("wipefs", "-af", disk) if err := wipeDisk.Run(); err != nil { return err } sgdisk := exec.Command("sgdisk", "-Z", disk) if err := sgdisk.Run(); err != nil { return err } createEFI := exec.Command("sgdisk", "-n", "1:0:+512M", "-t", "1:ef00", disk) if err := createEFI.Run(); err != nil { return err } createRoot := exec.Command("sgdisk", "-n", "2:0:0", "-t", "2:8300", disk) if err := createRoot.Run(); err != nil { return err } time.Sleep(1 * time.Second) exec.Command("partprobe", disk).Run() return nil } func (m InstallModel) formatPartitions() error { disk := m.config.Disk efiPart := disk + "1" rootPart := disk + "2" if strings.Contains(disk, "nvme") || strings.Contains(disk, "mmcblk") { efiPart = disk + "p1" rootPart = disk + "p2" } mkfsEFI := exec.Command("mkfs.fat", "-F32", efiPart) if err := mkfsEFI.Run(); err != nil { return err } if m.config.EnableLUKS { cryptsetup := exec.Command("cryptsetup", "luksFormat", "--type", "luks2", rootPart) cryptsetup.Stdin = strings.NewReader(m.config.RootPassword + "\n") if err := cryptsetup.Run(); err != nil { return err } cryptOpen := exec.Command("cryptsetup", "open", rootPart, "cryptroot") cryptOpen.Stdin = strings.NewReader(m.config.RootPassword + "\n") if err := cryptOpen.Run(); err != nil { return err } rootPart = "/dev/mapper/cryptroot" } mkfsBtrfs := exec.Command("mkfs.btrfs", "-f", rootPart) if err := mkfsBtrfs.Run(); err != nil { return err } return nil } func (m InstallModel) mountFilesystems() error { disk := m.config.Disk rootPart := disk + "2" efiPart := disk + "1" if strings.Contains(disk, "nvme") || strings.Contains(disk, "mmcblk") { efiPart = disk + "p1" rootPart = disk + "p2" } if m.config.EnableLUKS { rootPart = "/dev/mapper/cryptroot" } os.MkdirAll("/mnt", 0755) mount := exec.Command("mount", rootPart, "/mnt") if err := mount.Run(); err != nil { return err } btrfsOpts := "subvol=@,compress=zstd,noatime" for _, subvol := range []string{"@", "@home", "@var", "@snapshots"} { create := exec.Command("btrfs", "subvolume", "create", "/mnt/"+subvol) create.Run() } exec.Command("umount", "/mnt").Run() mount = exec.Command("mount", "-o", btrfsOpts, rootPart, "/mnt") if err := mount.Run(); err != nil { return err } os.MkdirAll("/mnt/home", 0755) os.MkdirAll("/mnt/var", 0755) os.MkdirAll("/mnt/.snapshots", 0755) os.MkdirAll("/mnt/boot", 0755) mountHome := exec.Command("mount", "-o", "subvol=@home,compress=zstd,noatime", rootPart, "/mnt/home") mountHome.Run() mountVar := exec.Command("mount", "-o", "subvol=@var,compress=zstd,noatime", rootPart, "/mnt/var") mountVar.Run() mountSnap := exec.Command("mount", "-o", "subvol=@snapshots,compress=zstd,noatime", rootPart, "/mnt/.snapshots") mountSnap.Run() mountEFI := exec.Command("mount", efiPart, "/mnt/boot") if err := mountEFI.Run(); err != nil { return err } return nil } func (m InstallModel) installBase() error { packages := []string{ "base", "base-devel", "linux-hardened", "linux-firmware", "btrfs-progs", "neovim", "tmux", "git", "networkmanager", "sudo", } pacstrap := exec.Command("pacstrap", append([]string{"/mnt"}, packages...)...) pacstrap.Stdout = os.Stdout pacstrap.Stderr = os.Stderr if err := pacstrap.Run(); err != nil { return err } return nil } func (m InstallModel) configureSystem() error { genfstab := exec.Command("genfstab", "-U", "/mnt") fstab, err := genfstab.Output() if err != nil { return err } os.WriteFile("/mnt/etc/fstab", fstab, 0644) os.WriteFile("/mnt/etc/hostname", []byte(m.config.Hostname+"\n"), 0644) hosts := fmt.Sprintf("127.0.0.1\tlocalhost\n::1\t\tlocalhost\n127.0.1.1\t%s.localdomain\t%s\n", m.config.Hostname, m.config.Hostname) os.WriteFile("/mnt/etc/hosts", []byte(hosts), 0644) os.WriteFile("/mnt/etc/locale.gen", []byte("en_US.UTF-8 UTF-8\n"), 0644) chroot([]string{"locale-gen"}) os.WriteFile("/mnt/etc/locale.conf", []byte("LANG=en_US.UTF-8\n"), 0644) chroot([]string{"ln", "-sf", "/usr/share/zoneinfo/UTC", "/etc/localtime"}) chroot([]string{"hwclock", "--systohc"}) useraddCmd := fmt.Sprintf("useradd -m -G wheel -s /bin/bash %s", m.config.Username) chroot([]string{"sh", "-c", useraddCmd}) passwdCmd := fmt.Sprintf("echo '%s:%s' | chpasswd", m.config.Username, m.config.UserPassword) chroot([]string{"sh", "-c", passwdCmd}) rootPasswdCmd := fmt.Sprintf("echo 'root:%s' | chpasswd", m.config.RootPassword) chroot([]string{"sh", "-c", rootPasswdCmd}) sudoers := "%wheel ALL=(ALL:ALL) ALL\n" os.WriteFile("/mnt/etc/sudoers.d/wheel", []byte(sudoers), 0440) chroot([]string{"systemctl", "enable", "NetworkManager"}) return nil } func (m InstallModel) installBootloader() error { chroot([]string{"bootctl", "install"}) loaderConf := `default arch.conf timeout 3 console-mode max editor no ` os.WriteFile("/mnt/boot/loader/loader.conf", []byte(loaderConf), 0644) disk := m.config.Disk rootPart := disk + "2" if strings.Contains(disk, "nvme") || strings.Contains(disk, "mmcblk") { rootPart = disk + "p2" } rootUUID := "" if m.config.EnableLUKS { blkid := exec.Command("blkid", "-s", "UUID", "-o", "value", rootPart) output, _ := blkid.Output() rootUUID = strings.TrimSpace(string(output)) } else { rootPart = "/dev/mapper/cryptroot" blkid := exec.Command("blkid", "-s", "UUID", "-o", "value", rootPart) output, _ := blkid.Output() rootUUID = strings.TrimSpace(string(output)) } options := fmt.Sprintf("root=UUID=%s rootflags=subvol=@ rw", rootUUID) if m.config.EnableLUKS { cryptUUID := "" blkid := exec.Command("blkid", "-s", "UUID", "-o", "value", disk+"2") output, _ := blkid.Output() cryptUUID = strings.TrimSpace(string(output)) options = fmt.Sprintf("cryptdevice=UUID=%s:cryptroot root=/dev/mapper/cryptroot rootflags=subvol=@ rw", cryptUUID) } entryConf := fmt.Sprintf(`title Miasma OS linux /vmlinuz-linux-hardened initrd /initramfs-linux-hardened.img options %s `, options) os.WriteFile("/mnt/boot/loader/entries/arch.conf", []byte(entryConf), 0644) if m.config.EnableLUKS { mkinitcpio, _ := os.ReadFile("/mnt/etc/mkinitcpio.conf") newConf := strings.Replace(string(mkinitcpio), "HOOKS=(base udev autodetect modconf block filesystems keyboard fsck)", "HOOKS=(base udev autodetect keyboard keymap consolefont modconf block encrypt filesystems fsck)", 1) os.WriteFile("/mnt/etc/mkinitcpio.conf", []byte(newConf), 0644) chroot([]string{"mkinitcpio", "-P"}) } return nil } func (m InstallModel) installDesktop() error { chroot([]string{"pacman", "-Sy", "--noconfirm", "cosmic-session", "cosmic-greeter"}) chroot([]string{"systemctl", "enable", "cosmic-greeter"}) return nil } func (m InstallModel) runPostInstallScripts() error { scriptsDir := "./scripts" entries, err := os.ReadDir(scriptsDir) if err != nil { if os.IsNotExist(err) { return nil } return err } for _, entry := range entries { if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".sh") { continue } scriptPath := filepath.Join(scriptsDir, entry.Name()) scriptContent, err := os.ReadFile(scriptPath) if err != nil { return err } chrootScript := "/tmp/" + entry.Name() os.WriteFile("/mnt"+chrootScript, scriptContent, 0755) if output, err := chroot([]string{chrootScript}); err != nil { return fmt.Errorf("script %s failed: %w\nOutput: %s", entry.Name(), err, output) } os.Remove("/mnt" + chrootScript) } return nil } func chroot(args []string) (string, error) { cmd := exec.Command("arch-chroot", append([]string{"/mnt"}, args...)...) output, err := cmd.CombinedOutput() return string(output), err } func (m InstallModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case installCompleteMsg: if msg.err != nil { m.err = msg.err m.status = "Installation failed" } else { m.status = "Installation complete!" } m.Finished = true return m, nil } return m, nil } func (m InstallModel) View() string { var content strings.Builder content.WriteString(styles.TitleStyle.Render("⚙️ Installing Miasma OS")) content.WriteString("\n\n") progress := fmt.Sprintf("Step %d/%d", m.currentStep, m.totalSteps) content.WriteString(styles.ProgressTextStyle.Render(progress)) content.WriteString("\n") bar := strings.Repeat("█", m.currentStep) + strings.Repeat("░", m.totalSteps-m.currentStep) content.WriteString(styles.ProgressBarStyle.Render(bar)) content.WriteString("\n\n") content.WriteString(styles.SpinnerStyle.Render("● ") + m.status) content.WriteString("\n\n") if m.err != nil { errMsg := fmt.Sprintf("✗ Error: %v", m.err) content.WriteString(styles.ErrorBoxStyle.Render(errMsg)) content.WriteString("\n\n") content.WriteString(styles.HelpStyle.Render("Press Ctrl+C to exit")) } if m.Finished && m.err == nil { success := "✓ Installation successful!\n\nYou can now reboot your system." content.WriteString(styles.BoxStyle.Render(success)) content.WriteString("\n\n") content.WriteString(styles.RenderHelp("Ctrl+C", "Exit")) } return styles.AppStyle.Render(content.String()) } func readLines(path string) ([]string, error) { file, err := os.Open(path) if err != nil { return nil, err } defer file.Close() var lines []string scanner := bufio.NewScanner(file) for scanner.Scan() { lines = append(lines, scanner.Text()) } return lines, scanner.Err() }