This commit is contained in:
@@ -1,4 +1,26 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
MOUNT_POINT="/mnt"
|
||||||
|
|
||||||
echo "Setting up Cosmic Desktop..."
|
echo "Setting up Cosmic Desktop..."
|
||||||
|
|
||||||
|
echo "Configuring display manager..."
|
||||||
|
systemctl enable cosmic-greeter
|
||||||
|
|
||||||
|
echo "Installing additional Cosmic components..."
|
||||||
|
pacman -S --noconfirm --needed \
|
||||||
|
cosmic-files \
|
||||||
|
cosmic-edit \
|
||||||
|
cosmic-term \
|
||||||
|
cosmic-store \
|
||||||
|
cosmic-settings
|
||||||
|
|
||||||
|
echo "Setting up user environment..."
|
||||||
|
if [ -n "$SUDO_USER" ]; then
|
||||||
|
USER_HOME="/home/$SUDO_USER"
|
||||||
|
mkdir -p "$USER_HOME/.config"
|
||||||
|
chown -R "$SUDO_USER:$SUDO_USER" "$USER_HOME/.config"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Cosmic Desktop setup complete!"
|
||||||
|
|||||||
@@ -2,3 +2,36 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
echo "Applying system hardening..."
|
echo "Applying system hardening..."
|
||||||
|
|
||||||
|
echo "Configuring kernel parameters..."
|
||||||
|
cat > /etc/sysctl.d/99-hardening.conf << 'EOF'
|
||||||
|
kernel.dmesg_restrict = 1
|
||||||
|
kernel.kptr_restrict = 2
|
||||||
|
kernel.unprivileged_bpf_disabled = 1
|
||||||
|
net.core.bpf_jit_harden = 2
|
||||||
|
kernel.yama.ptrace_scope = 2
|
||||||
|
net.ipv4.conf.all.rp_filter = 1
|
||||||
|
net.ipv4.conf.default.rp_filter = 1
|
||||||
|
net.ipv4.conf.all.accept_redirects = 0
|
||||||
|
net.ipv4.conf.default.accept_redirects = 0
|
||||||
|
net.ipv6.conf.all.accept_redirects = 0
|
||||||
|
net.ipv6.conf.default.accept_redirects = 0
|
||||||
|
net.ipv4.conf.all.send_redirects = 0
|
||||||
|
net.ipv4.conf.default.send_redirects = 0
|
||||||
|
net.ipv4.icmp_echo_ignore_all = 1
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Configuring firewall..."
|
||||||
|
pacman -S --noconfirm --needed ufw
|
||||||
|
systemctl enable ufw
|
||||||
|
ufw default deny incoming
|
||||||
|
ufw default allow outgoing
|
||||||
|
ufw enable
|
||||||
|
|
||||||
|
echo "Setting secure umask..."
|
||||||
|
echo "umask 077" >> /etc/profile
|
||||||
|
|
||||||
|
echo "Disabling core dumps..."
|
||||||
|
echo "* hard core 0" >> /etc/security/limits.conf
|
||||||
|
|
||||||
|
echo "System hardening complete!"
|
||||||
|
|||||||
@@ -1,21 +1,28 @@
|
|||||||
package steps
|
package steps
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"miasma-installer/tui/styles"
|
"miasma-installer/tui/styles"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/list"
|
"github.com/charmbracelet/bubbles/list"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Disk struct {
|
type Disk struct {
|
||||||
name, description string
|
name string
|
||||||
|
description string
|
||||||
|
size string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i Disk) FilterValue() string { return i.name }
|
func (i Disk) FilterValue() string { return i.name }
|
||||||
func (i Disk) Title() 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{}
|
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())
|
s := fmt.Sprintf("%d. %s - %s", index+1, disk.Title(), disk.Description())
|
||||||
|
|
||||||
// focus styles
|
|
||||||
if index == m.Index() {
|
if index == m.Index() {
|
||||||
s = styles.ActiveItemStyle.Render("> " + s)
|
s = styles.ActiveItemStyle.Render("> " + s)
|
||||||
} else {
|
} else {
|
||||||
@@ -43,27 +49,85 @@ type DiskSelModel struct {
|
|||||||
list list.Model
|
list list.Model
|
||||||
Finished bool
|
Finished bool
|
||||||
SelectedDisk string
|
SelectedDisk string
|
||||||
|
loading bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type disksLoadedMsg struct {
|
||||||
|
disks []list.Item
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDiskSelModel(width, height int) DiskSelModel {
|
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
|
l := list.New([]list.Item{}, ItemDelegate{}, width, height-5)
|
||||||
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.Title = "Select Installation Disk"
|
l.Title = "Select Installation Disk"
|
||||||
l.SetShowStatusBar(false)
|
l.SetShowStatusBar(false)
|
||||||
l.SetFilteringEnabled(false)
|
l.SetFilteringEnabled(false)
|
||||||
l.Styles.Title = styles.TitleStyle
|
l.Styles.Title = styles.TitleStyle
|
||||||
|
|
||||||
return DiskSelModel{list: l}
|
return DiskSelModel{
|
||||||
|
list: l,
|
||||||
|
loading: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m DiskSelModel) Init() tea.Cmd {
|
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) {
|
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) {
|
switch msg := msg.(type) {
|
||||||
|
case disksLoadedMsg:
|
||||||
|
m.loading = false
|
||||||
|
m.list.SetItems(msg.disks)
|
||||||
|
return m, nil
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
if msg.String() == "enter" {
|
if !m.loading && msg.String() == "enter" {
|
||||||
if item, ok := m.list.SelectedItem().(Disk); ok {
|
if item, ok := m.list.SelectedItem().(Disk); ok {
|
||||||
m.SelectedDisk = item.name
|
m.SelectedDisk = item.name
|
||||||
m.Finished = true
|
m.Finished = true
|
||||||
@@ -81,13 +150,46 @@ func (m DiskSelModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
m.list, cmd = m.list.Update(msg)
|
m.list, cmd = m.list.Update(msg)
|
||||||
return m, cmd
|
return m, cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m DiskSelModel) View() string {
|
func (m DiskSelModel) View() string {
|
||||||
|
if m.loading {
|
||||||
|
return styles.MainStyle.Render(styles.TitleStyle.Render("Scanning disks..."))
|
||||||
|
}
|
||||||
|
|
||||||
s := m.list.View()
|
s := m.list.View()
|
||||||
s += "\n" + styles.HelpStyle.Render("Use ↑/↓ to navigate, [Enter] to select disk.")
|
s += "\n" + styles.HelpStyle.Render("Use ↑/↓ to navigate, [Enter] to select disk.")
|
||||||
return s
|
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 (
|
import (
|
||||||
"miasma-installer/tui/styles"
|
"miasma-installer/tui/styles"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EncryptionModel struct {
|
type EncryptionModel struct {
|
||||||
@@ -22,11 +24,13 @@ func NewEncryptionModel() EncryptionModel {
|
|||||||
pi := textinput.New()
|
pi := textinput.New()
|
||||||
pi.Placeholder = "Enter encryption password"
|
pi.Placeholder = "Enter encryption password"
|
||||||
pi.EchoMode = textinput.EchoPassword
|
pi.EchoMode = textinput.EchoPassword
|
||||||
|
pi.Width = 40
|
||||||
pi.Focus()
|
pi.Focus()
|
||||||
|
|
||||||
ci := textinput.New()
|
ci := textinput.New()
|
||||||
ci.Placeholder = "Confirm password"
|
ci.Placeholder = "Confirm password"
|
||||||
ci.EchoMode = textinput.EchoPassword
|
ci.EchoMode = textinput.EchoPassword
|
||||||
|
ci.Width = 40
|
||||||
|
|
||||||
return EncryptionModel{
|
return EncryptionModel{
|
||||||
EnableEncryption: true,
|
EnableEncryption: true,
|
||||||
@@ -108,25 +112,52 @@ func (m EncryptionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m EncryptionModel) View() string {
|
func (m EncryptionModel) View() string {
|
||||||
s := styles.TitleStyle.Render("Disk Encryption")
|
var content strings.Builder
|
||||||
s += "\n\n"
|
|
||||||
|
content.WriteString(styles.TitleStyle.Render("🔒 Disk Encryption"))
|
||||||
|
content.WriteString("\n\n")
|
||||||
|
|
||||||
if !m.showInputs {
|
if !m.showInputs {
|
||||||
s += "Enable LUKS2 disk encryption? (recommended)\n\n"
|
info := "LUKS2 encryption protects your data if your device is lost or stolen.\n"
|
||||||
s += styles.HelpStyle.Render("[y] Yes [n] No")
|
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 {
|
} else {
|
||||||
s += "Enter encryption password:\n\n"
|
content.WriteString(styles.LabelStyle.Render("Password:"))
|
||||||
s += m.passwordInput.View() + "\n\n"
|
content.WriteString("\n")
|
||||||
s += m.confirmInput.View() + "\n\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() != "" {
|
||||||
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
|
package steps
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"miasma-installer/config"
|
"miasma-installer/config"
|
||||||
"miasma-installer/tui/styles"
|
"miasma-installer/tui/styles"
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
@@ -19,6 +20,8 @@ type InstallModel struct {
|
|||||||
status string
|
status string
|
||||||
progress []string
|
progress []string
|
||||||
err error
|
err error
|
||||||
|
currentStep int
|
||||||
|
totalSteps int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInstallModel(disk string, enableLUKS bool, hostname string, username string, userPassword string, rootPassword string) InstallModel {
|
func NewInstallModel(disk string, enableLUKS bool, hostname string, username string, userPassword string, rootPassword string) InstallModel {
|
||||||
@@ -35,6 +38,8 @@ func NewInstallModel(disk string, enableLUKS bool, hostname string, username str
|
|||||||
},
|
},
|
||||||
status: "Preparing installation...",
|
status: "Preparing installation...",
|
||||||
progress: []string{},
|
progress: []string{},
|
||||||
|
currentStep: 0,
|
||||||
|
totalSteps: 8,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,34 +51,319 @@ type installCompleteMsg struct {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type installProgressMsg struct {
|
||||||
|
step int
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
func (m InstallModel) startInstall() tea.Cmd {
|
func (m InstallModel) startInstall() tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
archConfig := m.config.ToArchInstall()
|
if err := m.performInstall(); err != nil {
|
||||||
|
return installCompleteMsg{err: err}
|
||||||
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 := 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}
|
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 {
|
func (m InstallModel) runPostInstallScripts() error {
|
||||||
scriptsDir := "./scripts"
|
scriptsDir := "./scripts"
|
||||||
entries, err := os.ReadDir(scriptsDir)
|
entries, err := os.ReadDir(scriptsDir)
|
||||||
@@ -90,15 +380,30 @@ func (m InstallModel) runPostInstallScripts() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scriptPath := filepath.Join(scriptsDir, entry.Name())
|
scriptPath := filepath.Join(scriptsDir, entry.Name())
|
||||||
cmd := exec.Command("/bin/bash", scriptPath)
|
scriptContent, err := os.ReadFile(scriptPath)
|
||||||
if output, err := cmd.CombinedOutput(); err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("script %s failed: %w\nOutput: %s", entry.Name(), err, string(output))
|
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
|
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) {
|
func (m InstallModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case installCompleteMsg:
|
case installCompleteMsg:
|
||||||
@@ -118,15 +423,30 @@ func (m InstallModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
func (m InstallModel) View() string {
|
func (m InstallModel) View() string {
|
||||||
s := styles.TitleStyle.Render("Installing Miasma OS")
|
s := styles.TitleStyle.Render("Installing Miasma OS")
|
||||||
s += "\n\n"
|
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 {
|
if m.err != nil {
|
||||||
s += styles.HelpStyle.Render(fmt.Sprintf("Error: %v\n", m.err))
|
s += styles.HelpStyle.Render(fmt.Sprintf("Error: %v\n", m.err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.Finished && m.err == nil {
|
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)
|
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 (
|
import (
|
||||||
"miasma-installer/tui/styles"
|
"miasma-installer/tui/styles"
|
||||||
|
"strings"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WelcomeModel struct {
|
type WelcomeModel struct {
|
||||||
@@ -34,13 +36,42 @@ func (m WelcomeModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m WelcomeModel) View() string {
|
func (m WelcomeModel) View() string {
|
||||||
s := styles.TitleStyle.Render("Welcome to the Miasma OS Installer")
|
logo := `
|
||||||
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")
|
╚═╝ ╚═╝╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
|
||||||
|
`
|
||||||
|
|
||||||
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"
|
import "github.com/charmbracelet/lipgloss"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
MainStyle = lipgloss.NewStyle().
|
Primary = lipgloss.Color("#7aa2f7")
|
||||||
Border(lipgloss.RoundedBorder()).
|
Secondary = lipgloss.Color("#bb9af7")
|
||||||
BorderForeground(lipgloss.Color("#7fb4ca")).
|
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)
|
Padding(1, 2)
|
||||||
|
|
||||||
HelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
|
|
||||||
|
|
||||||
TitleStyle = lipgloss.NewStyle().
|
TitleStyle = lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#7fb4ca")).
|
Foreground(Primary).
|
||||||
Align(lipgloss.Center).
|
|
||||||
Bold(true).
|
Bold(true).
|
||||||
PaddingBottom(1)
|
MarginBottom(1).
|
||||||
|
MarginTop(1)
|
||||||
|
|
||||||
ActiveItemStyle = lipgloss.NewStyle().
|
LogoStyle = lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#87a987")).
|
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)
|
Bold(true)
|
||||||
|
|
||||||
|
ValueStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(Text)
|
||||||
|
|
||||||
|
ActiveItemStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(Primary).
|
||||||
|
Bold(true).
|
||||||
|
PaddingLeft(2)
|
||||||
|
|
||||||
InactiveItemStyle = lipgloss.NewStyle().
|
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