This commit is contained in:
@@ -1,21 +1,28 @@
|
||||
package steps
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"miasma-installer/tui/styles"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type Disk struct {
|
||||
name, description string
|
||||
name string
|
||||
description string
|
||||
size string
|
||||
}
|
||||
|
||||
func (i Disk) FilterValue() string { return i.name }
|
||||
func (i Disk) Title() string { return i.name }
|
||||
func (i Disk) Description() string { return i.description }
|
||||
func (i Disk) Description() string { return fmt.Sprintf("%s - %s", i.size, i.description) }
|
||||
|
||||
type ItemDelegate struct{}
|
||||
|
||||
@@ -30,7 +37,6 @@ func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, item list.Ite
|
||||
|
||||
s := fmt.Sprintf("%d. %s - %s", index+1, disk.Title(), disk.Description())
|
||||
|
||||
// focus styles
|
||||
if index == m.Index() {
|
||||
s = styles.ActiveItemStyle.Render("> " + s)
|
||||
} else {
|
||||
@@ -43,27 +49,85 @@ type DiskSelModel struct {
|
||||
list list.Model
|
||||
Finished bool
|
||||
SelectedDisk string
|
||||
loading bool
|
||||
}
|
||||
|
||||
type disksLoadedMsg struct {
|
||||
disks []list.Item
|
||||
}
|
||||
|
||||
func NewDiskSelModel(width, height int) DiskSelModel {
|
||||
// This is a placeholder - I need to update this to a tea.Cmd to scan the system for drives
|
||||
disks := []list.Item{
|
||||
Disk{name: "/dev/sda", description: "500GB Samsung NVME"},
|
||||
Disk{name: "/dev/sdb", description: "1TB Baracuda HDD"},
|
||||
Disk{name: "/dev/loop0", description: "2GB Fanxiang SSD"},
|
||||
}
|
||||
|
||||
l := list.New(disks, ItemDelegate{}, width, height-5)
|
||||
l := list.New([]list.Item{}, ItemDelegate{}, width, height-5)
|
||||
l.Title = "Select Installation Disk"
|
||||
l.SetShowStatusBar(false)
|
||||
l.SetFilteringEnabled(false)
|
||||
l.Styles.Title = styles.TitleStyle
|
||||
|
||||
return DiskSelModel{list: l}
|
||||
return DiskSelModel{
|
||||
list: l,
|
||||
loading: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (m DiskSelModel) Init() tea.Cmd {
|
||||
return nil
|
||||
return loadDisks
|
||||
}
|
||||
|
||||
func loadDisks() tea.Msg {
|
||||
disks, err := scanDisks()
|
||||
if err != nil {
|
||||
return disksLoadedMsg{disks: []list.Item{
|
||||
Disk{name: "error", description: err.Error(), size: ""},
|
||||
}}
|
||||
}
|
||||
return disksLoadedMsg{disks: disks}
|
||||
}
|
||||
|
||||
func scanDisks() ([]list.Item, error) {
|
||||
cmd := exec.Command("lsblk", "-ndo", "NAME,SIZE,TYPE,MODEL")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list disks: %w", err)
|
||||
}
|
||||
|
||||
var disks []list.Item
|
||||
scanner := bufio.NewScanner(strings.NewReader(string(output)))
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
name := fields[0]
|
||||
size := fields[1]
|
||||
diskType := fields[2]
|
||||
model := ""
|
||||
if len(fields) > 3 {
|
||||
model = strings.Join(fields[3:], " ")
|
||||
}
|
||||
|
||||
if diskType != "disk" {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(name, "loop") || strings.HasPrefix(name, "sr") {
|
||||
continue
|
||||
}
|
||||
|
||||
disks = append(disks, Disk{
|
||||
name: "/dev/" + name,
|
||||
size: size,
|
||||
description: model,
|
||||
})
|
||||
}
|
||||
|
||||
if len(disks) == 0 {
|
||||
return nil, fmt.Errorf("no suitable disks found")
|
||||
}
|
||||
|
||||
return disks, nil
|
||||
}
|
||||
|
||||
func (m DiskSelModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
@@ -72,8 +136,13 @@ func (m DiskSelModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case disksLoadedMsg:
|
||||
m.loading = false
|
||||
m.list.SetItems(msg.disks)
|
||||
return m, nil
|
||||
|
||||
case tea.KeyMsg:
|
||||
if msg.String() == "enter" {
|
||||
if !m.loading && msg.String() == "enter" {
|
||||
if item, ok := m.list.SelectedItem().(Disk); ok {
|
||||
m.SelectedDisk = item.name
|
||||
m.Finished = true
|
||||
@@ -81,13 +150,46 @@ func (m DiskSelModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.list, cmd = m.list.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m DiskSelModel) View() string {
|
||||
if m.loading {
|
||||
return styles.MainStyle.Render(styles.TitleStyle.Render("Scanning disks..."))
|
||||
}
|
||||
|
||||
s := m.list.View()
|
||||
s += "\n" + styles.HelpStyle.Render("Use ↑/↓ to navigate, [Enter] to select disk.")
|
||||
return s
|
||||
}
|
||||
|
||||
func formatBytes(bytes uint64) string {
|
||||
const unit = 1024
|
||||
if bytes < unit {
|
||||
return fmt.Sprintf("%d B", bytes)
|
||||
}
|
||||
div, exp := uint64(unit), 0
|
||||
for n := bytes / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
|
||||
func getDiskSize(device string) (string, error) {
|
||||
data, err := os.ReadFile(fmt.Sprintf("/sys/block/%s/size", strings.TrimPrefix(device, "/dev/")))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sectors, err := strconv.ParseUint(strings.TrimSpace(string(data)), 10, 64)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
bytes := sectors * 512
|
||||
return formatBytes(bytes), nil
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ package steps
|
||||
|
||||
import (
|
||||
"miasma-installer/tui/styles"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type EncryptionModel struct {
|
||||
@@ -22,11 +24,13 @@ func NewEncryptionModel() EncryptionModel {
|
||||
pi := textinput.New()
|
||||
pi.Placeholder = "Enter encryption password"
|
||||
pi.EchoMode = textinput.EchoPassword
|
||||
pi.Width = 40
|
||||
pi.Focus()
|
||||
|
||||
ci := textinput.New()
|
||||
ci.Placeholder = "Confirm password"
|
||||
ci.EchoMode = textinput.EchoPassword
|
||||
ci.Width = 40
|
||||
|
||||
return EncryptionModel{
|
||||
EnableEncryption: true,
|
||||
@@ -108,25 +112,52 @@ func (m EncryptionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (m EncryptionModel) View() string {
|
||||
s := styles.TitleStyle.Render("Disk Encryption")
|
||||
s += "\n\n"
|
||||
var content strings.Builder
|
||||
|
||||
content.WriteString(styles.TitleStyle.Render("🔒 Disk Encryption"))
|
||||
content.WriteString("\n\n")
|
||||
|
||||
if !m.showInputs {
|
||||
s += "Enable LUKS2 disk encryption? (recommended)\n\n"
|
||||
s += styles.HelpStyle.Render("[y] Yes [n] No")
|
||||
info := "LUKS2 encryption protects your data if your device is lost or stolen.\n"
|
||||
info += "You will need to enter the password every time you boot."
|
||||
content.WriteString(styles.InfoBoxStyle.Render(info))
|
||||
content.WriteString("\n\n")
|
||||
|
||||
content.WriteString("Enable disk encryption? (recommended)\n\n")
|
||||
content.WriteString(styles.RenderHelp("y", "Yes", "n", "No"))
|
||||
} else {
|
||||
s += "Enter encryption password:\n\n"
|
||||
s += m.passwordInput.View() + "\n\n"
|
||||
s += m.confirmInput.View() + "\n\n"
|
||||
content.WriteString(styles.LabelStyle.Render("Password:"))
|
||||
content.WriteString("\n")
|
||||
|
||||
inputStyle := styles.InputStyle
|
||||
if m.focusIndex == 0 {
|
||||
inputStyle = styles.FocusedInputStyle
|
||||
}
|
||||
content.WriteString(inputStyle.Render(m.passwordInput.View()))
|
||||
content.WriteString("\n\n")
|
||||
|
||||
content.WriteString(styles.LabelStyle.Render("Confirm Password:"))
|
||||
content.WriteString("\n")
|
||||
|
||||
confirmStyle := styles.InputStyle
|
||||
if m.focusIndex == 1 {
|
||||
confirmStyle = styles.FocusedInputStyle
|
||||
}
|
||||
content.WriteString(confirmStyle.Render(m.confirmInput.View()))
|
||||
content.WriteString("\n\n")
|
||||
|
||||
if m.passwordInput.Value() != "" && m.confirmInput.Value() != "" {
|
||||
if m.passwordInput.Value() != m.confirmInput.Value() {
|
||||
s += styles.HelpStyle.Render("Passwords do not match\n")
|
||||
content.WriteString(styles.ErrorStyle.Render("✗ Passwords do not match"))
|
||||
content.WriteString("\n\n")
|
||||
} else {
|
||||
content.WriteString(styles.SuccessStyle.Render("✓ Passwords match"))
|
||||
content.WriteString("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
s += styles.HelpStyle.Render("[Tab] Switch fields [Enter] Continue")
|
||||
content.WriteString(styles.RenderHelp("Tab", "Switch fields", "Enter", "Continue"))
|
||||
}
|
||||
|
||||
return styles.MainStyle.Render(s)
|
||||
return styles.AppStyle.Render(content.String())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package steps
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"bufio"
|
||||
"fmt"
|
||||
"miasma-installer/config"
|
||||
"miasma-installer/tui/styles"
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
@@ -19,6 +20,8 @@ type InstallModel struct {
|
||||
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 {
|
||||
@@ -33,8 +36,10 @@ func NewInstallModel(disk string, enableLUKS bool, hostname string, username str
|
||||
Timezone: "UTC",
|
||||
Locale: "en_US.UTF-8",
|
||||
},
|
||||
status: "Preparing installation...",
|
||||
progress: []string{},
|
||||
status: "Preparing installation...",
|
||||
progress: []string{},
|
||||
currentStep: 0,
|
||||
totalSteps: 8,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,34 +51,319 @@ type installCompleteMsg struct {
|
||||
err error
|
||||
}
|
||||
|
||||
type installProgressMsg struct {
|
||||
step int
|
||||
message string
|
||||
}
|
||||
|
||||
func (m InstallModel) startInstall() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
archConfig := m.config.ToArchInstall()
|
||||
|
||||
configPath := "/tmp/miasma-install-config.json"
|
||||
data, err := json.MarshalIndent(archConfig, "", " ")
|
||||
if err != nil {
|
||||
return installCompleteMsg{err: fmt.Errorf("failed to marshal config: %w", err)}
|
||||
if err := m.performInstall(); err != nil {
|
||||
return installCompleteMsg{err: err}
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, data, 0644); err != nil {
|
||||
return installCompleteMsg{err: fmt.Errorf("failed to write config: %w", err)}
|
||||
}
|
||||
|
||||
cmd := exec.Command("archinstall", "--config", configPath)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return installCompleteMsg{err: fmt.Errorf("archinstall failed: %w\nOutput: %s", err, string(output))}
|
||||
}
|
||||
|
||||
if err := m.runPostInstallScripts(); err != nil {
|
||||
return installCompleteMsg{err: fmt.Errorf("post-install scripts failed: %w", 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)
|
||||
@@ -90,15 +380,30 @@ func (m InstallModel) runPostInstallScripts() error {
|
||||
}
|
||||
|
||||
scriptPath := filepath.Join(scriptsDir, entry.Name())
|
||||
cmd := exec.Command("/bin/bash", scriptPath)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("script %s failed: %w\nOutput: %s", entry.Name(), err, string(output))
|
||||
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:
|
||||
@@ -118,15 +423,30 @@ func (m InstallModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
func (m InstallModel) View() string {
|
||||
s := styles.TitleStyle.Render("Installing Miasma OS")
|
||||
s += "\n\n"
|
||||
s += m.status + "\n\n"
|
||||
s += fmt.Sprintf("Step %d/%d: %s\n\n", m.currentStep, m.totalSteps, m.status)
|
||||
|
||||
if m.err != nil {
|
||||
s += styles.HelpStyle.Render(fmt.Sprintf("Error: %v\n", m.err))
|
||||
}
|
||||
|
||||
if m.Finished && m.err == nil {
|
||||
s += styles.HelpStyle.Render("Press Ctrl+C to exit")
|
||||
s += "\n" + styles.HelpStyle.Render("Installation successful! You can now reboot.\nPress Ctrl+C to exit")
|
||||
}
|
||||
|
||||
return styles.MainStyle.Render(s)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ package steps
|
||||
|
||||
import (
|
||||
"miasma-installer/tui/styles"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type WelcomeModel struct {
|
||||
@@ -34,13 +36,42 @@ func (m WelcomeModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (m WelcomeModel) View() string {
|
||||
s := styles.TitleStyle.Render("Welcome to the Miasma OS Installer")
|
||||
s += "\n\n"
|
||||
s += "This program will guide you through installing Miasma OS. \n"
|
||||
s += "Please ensure that you have backed up any important data before proceding"
|
||||
s += "\n\n"
|
||||
s += styles.HelpStyle.Render("Press [Enter] to continue")
|
||||
s += styles.HelpStyle.Render("Press [Ctrl+C] to exit")
|
||||
logo := `
|
||||
███╗ ███╗██╗ █████╗ ███████╗███╗ ███╗ █████╗
|
||||
████╗ ████║██║██╔══██╗██╔════╝████╗ ████║██╔══██╗
|
||||
██╔████╔██║██║███████║███████╗██╔████╔██║███████║
|
||||
██║╚██╔╝██║██║██╔══██║╚════██║██║╚██╔╝██║██╔══██║
|
||||
██║ ╚═╝ ██║██║██║ ██║███████║██║ ╚═╝ ██║██║ ██║
|
||||
╚═╝ ╚═╝╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
|
||||
`
|
||||
|
||||
return styles.MainStyle.Render(s)
|
||||
var content strings.Builder
|
||||
|
||||
content.WriteString(styles.LogoStyle.Render(logo))
|
||||
content.WriteString("\n\n")
|
||||
content.WriteString(styles.TitleStyle.Render("Welcome to Miasma OS Installer"))
|
||||
content.WriteString("\n")
|
||||
content.WriteString(styles.SubtitleStyle.Render("An opinionated Arch Linux installation"))
|
||||
content.WriteString("\n\n")
|
||||
|
||||
features := []string{
|
||||
"🔒 LUKS2 encryption by default",
|
||||
"🗂️ Btrfs filesystem with snapshots",
|
||||
"🛡️ Hardened Linux kernel",
|
||||
"🚀 Cosmic Desktop environment",
|
||||
"⚡ UEFI boot with systemd-boot",
|
||||
}
|
||||
|
||||
featureBox := styles.InfoBoxStyle.Render(strings.Join(features, "\n"))
|
||||
content.WriteString(featureBox)
|
||||
content.WriteString("\n\n")
|
||||
|
||||
warning := styles.WarningBoxStyle.Render("⚠️ WARNING: This will erase all data on the selected disk!")
|
||||
content.WriteString(warning)
|
||||
content.WriteString("\n\n")
|
||||
|
||||
help := styles.RenderHelp("Enter", "Continue", "Ctrl+C", "Exit")
|
||||
content.WriteString(help)
|
||||
|
||||
return styles.AppStyle.Render(content.String())
|
||||
}
|
||||
|
||||
@@ -3,23 +3,137 @@ package styles
|
||||
import "github.com/charmbracelet/lipgloss"
|
||||
|
||||
var (
|
||||
MainStyle = lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("#7fb4ca")).
|
||||
Primary = lipgloss.Color("#7aa2f7")
|
||||
Secondary = lipgloss.Color("#bb9af7")
|
||||
Success = lipgloss.Color("#9ece6a")
|
||||
Warning = lipgloss.Color("#e0af68")
|
||||
Error = lipgloss.Color("#f7768e")
|
||||
Muted = lipgloss.Color("#565f89")
|
||||
Text = lipgloss.Color("#c0caf5")
|
||||
Subtle = lipgloss.Color("#414868")
|
||||
Border = lipgloss.Color("#7aa2f7")
|
||||
)
|
||||
|
||||
var (
|
||||
AppStyle = lipgloss.NewStyle().
|
||||
Padding(1, 2)
|
||||
|
||||
HelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
|
||||
|
||||
TitleStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#7fb4ca")).
|
||||
Align(lipgloss.Center).
|
||||
Foreground(Primary).
|
||||
Bold(true).
|
||||
PaddingBottom(1)
|
||||
MarginBottom(1).
|
||||
MarginTop(1)
|
||||
|
||||
ActiveItemStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#87a987")).
|
||||
LogoStyle = lipgloss.NewStyle().
|
||||
Foreground(Secondary).
|
||||
Bold(true).
|
||||
Align(lipgloss.Center)
|
||||
|
||||
SubtitleStyle = lipgloss.NewStyle().
|
||||
Foreground(Muted).
|
||||
Italic(true)
|
||||
|
||||
BoxStyle = lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(Border).
|
||||
Padding(1, 2).
|
||||
MarginTop(1).
|
||||
MarginBottom(1)
|
||||
|
||||
ActiveBoxStyle = lipgloss.NewStyle().
|
||||
Border(lipgloss.ThickBorder()).
|
||||
BorderForeground(Primary).
|
||||
Padding(1, 2).
|
||||
MarginTop(1).
|
||||
MarginBottom(1)
|
||||
|
||||
InfoBoxStyle = lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(Secondary).
|
||||
Padding(0, 1).
|
||||
MarginTop(1)
|
||||
|
||||
WarningBoxStyle = lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(Warning).
|
||||
Padding(0, 1).
|
||||
MarginTop(1)
|
||||
|
||||
ErrorBoxStyle = lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(Error).
|
||||
Padding(0, 1).
|
||||
MarginTop(1)
|
||||
|
||||
HelpStyle = lipgloss.NewStyle().
|
||||
Foreground(Muted).
|
||||
MarginTop(1)
|
||||
|
||||
KeyStyle = lipgloss.NewStyle().
|
||||
Foreground(Primary).
|
||||
Bold(true)
|
||||
|
||||
ValueStyle = lipgloss.NewStyle().
|
||||
Foreground(Text)
|
||||
|
||||
ActiveItemStyle = lipgloss.NewStyle().
|
||||
Foreground(Primary).
|
||||
Bold(true).
|
||||
PaddingLeft(2)
|
||||
|
||||
InactiveItemStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("240"))
|
||||
Foreground(Muted).
|
||||
PaddingLeft(2)
|
||||
|
||||
SelectedItemStyle = lipgloss.NewStyle().
|
||||
Foreground(Success).
|
||||
Bold(true)
|
||||
|
||||
LabelStyle = lipgloss.NewStyle().
|
||||
Foreground(Secondary).
|
||||
Bold(true).
|
||||
MarginRight(1)
|
||||
|
||||
InputStyle = lipgloss.NewStyle().
|
||||
Foreground(Text).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(Subtle).
|
||||
Padding(0, 1)
|
||||
|
||||
FocusedInputStyle = lipgloss.NewStyle().
|
||||
Foreground(Text).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(Primary).
|
||||
Padding(0, 1)
|
||||
|
||||
ProgressBarStyle = lipgloss.NewStyle().
|
||||
Foreground(Primary)
|
||||
|
||||
ProgressTextStyle = lipgloss.NewStyle().
|
||||
Foreground(Muted)
|
||||
|
||||
SuccessStyle = lipgloss.NewStyle().
|
||||
Foreground(Success).
|
||||
Bold(true)
|
||||
|
||||
ErrorStyle = lipgloss.NewStyle().
|
||||
Foreground(Error).
|
||||
Bold(true)
|
||||
|
||||
SpinnerStyle = lipgloss.NewStyle().
|
||||
Foreground(Primary)
|
||||
)
|
||||
|
||||
func RenderKeyValue(key, value string) string {
|
||||
return KeyStyle.Render(key+":") + " " + ValueStyle.Render(value)
|
||||
}
|
||||
|
||||
func RenderHelp(keys ...string) string {
|
||||
var parts []string
|
||||
for i := 0; i < len(keys); i += 2 {
|
||||
if i+1 < len(keys) {
|
||||
parts = append(parts, KeyStyle.Render(keys[i])+" "+HelpStyle.Render(keys[i+1]))
|
||||
}
|
||||
}
|
||||
return HelpStyle.Render(lipgloss.JoinHorizontal(lipgloss.Left, parts...))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user