added installer shell script and additional steps
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
build/
|
||||||
|
miasma-installer
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
CRUSH.md
|
||||||
|
.crush
|
||||||
46
Makefile
Normal file
46
Makefile
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
.PHONY: build clean install release
|
||||||
|
|
||||||
|
BINARY_NAME=miasma-installer
|
||||||
|
BUILD_DIR=build
|
||||||
|
VERSION?=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||||
|
LDFLAGS=-ldflags "-X main.Version=$(VERSION)"
|
||||||
|
|
||||||
|
build:
|
||||||
|
@echo "Building $(BINARY_NAME)..."
|
||||||
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) .
|
||||||
|
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@echo "Cleaning..."
|
||||||
|
@rm -rf $(BUILD_DIR)
|
||||||
|
@go clean
|
||||||
|
|
||||||
|
install: build
|
||||||
|
@echo "Installing to /usr/local/bin..."
|
||||||
|
@sudo cp $(BUILD_DIR)/$(BINARY_NAME) /usr/local/bin/
|
||||||
|
@sudo chmod +x /usr/local/bin/$(BINARY_NAME)
|
||||||
|
@echo "Installed successfully"
|
||||||
|
|
||||||
|
release:
|
||||||
|
@echo "Building release binary..."
|
||||||
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -a -installsuffix cgo -o $(BUILD_DIR)/$(BINARY_NAME) .
|
||||||
|
@strip $(BUILD_DIR)/$(BINARY_NAME)
|
||||||
|
@echo "Release build complete: $(BUILD_DIR)/$(BINARY_NAME)"
|
||||||
|
@ls -lh $(BUILD_DIR)/$(BINARY_NAME)
|
||||||
|
|
||||||
|
run: build
|
||||||
|
@$(BUILD_DIR)/$(BINARY_NAME)
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
go fmt ./...
|
||||||
|
|
||||||
|
vet:
|
||||||
|
go vet ./...
|
||||||
|
|
||||||
|
tidy:
|
||||||
|
go mod tidy
|
||||||
92
README.md
Normal file
92
README.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Miasma OS Installer
|
||||||
|
|
||||||
|
Opinionated Arch Linux installer built with Go and Bubble Tea TUI framework.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Filesystem**: btrfs with optimized subvolume layout
|
||||||
|
- **Desktop**: Cosmic Desktop
|
||||||
|
- **Kernel**: linux-hardened
|
||||||
|
- **Encryption**: LUKS2 by default (optional)
|
||||||
|
- **Boot**: UEFI only (systemd-boot)
|
||||||
|
- **Future**: Secure Boot support planned
|
||||||
|
|
||||||
|
## Quick Install
|
||||||
|
|
||||||
|
Boot into Arch Linux installation media and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://install.miasma-os.com | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Or manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download and run installer
|
||||||
|
curl -fsSL https://github.com/miasma-os/miasma-installer/releases/latest/download/miasma-installer -o miasma-installer
|
||||||
|
chmod +x miasma-installer
|
||||||
|
sudo ./miasma-installer
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Go 1.25.2 or later
|
||||||
|
- Arch Linux (for testing)
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build binary
|
||||||
|
make build
|
||||||
|
|
||||||
|
# Build release binary (stripped, static)
|
||||||
|
make release
|
||||||
|
|
||||||
|
# Install to /usr/local/bin
|
||||||
|
make install
|
||||||
|
|
||||||
|
# Run directly
|
||||||
|
make run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Format code
|
||||||
|
make fmt
|
||||||
|
|
||||||
|
# Lint
|
||||||
|
make vet
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── config/ # Installation configuration and archinstall JSON generation
|
||||||
|
├── scripts/ # Post-install shell scripts
|
||||||
|
├── tui/
|
||||||
|
│ ├── model.go # Root state machine
|
||||||
|
│ ├── steps/ # Installation step models
|
||||||
|
│ └── styles/ # Shared UI styles
|
||||||
|
├── install.sh # Bootstrap script for curl | sh installation
|
||||||
|
└── main.go # Entry point
|
||||||
|
```
|
||||||
|
|
||||||
|
## Post-Install Scripts
|
||||||
|
|
||||||
|
Custom shell scripts in `scripts/` run after archinstall completes:
|
||||||
|
|
||||||
|
- `01-cosmic-setup.sh` - Cosmic Desktop configuration
|
||||||
|
- `02-hardening.sh` - System hardening tweaks
|
||||||
|
|
||||||
|
Scripts execute in alphabetical order.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
65
config/config.go
Normal file
65
config/config.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type InstallConfig struct {
|
||||||
|
Disk string
|
||||||
|
EnableLUKS bool
|
||||||
|
Hostname string
|
||||||
|
Username string
|
||||||
|
RootPassword string
|
||||||
|
UserPassword string
|
||||||
|
Timezone string
|
||||||
|
Locale string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArchInstallConfig struct {
|
||||||
|
Bootloader string `json:"bootloader"`
|
||||||
|
Kernels []string `json:"kernels"`
|
||||||
|
Filesystem string `json:"filesystem"`
|
||||||
|
Disk string `json:"disk"`
|
||||||
|
Encryption *Encryption `json:"encryption,omitempty"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
Timezone string `json:"timezone"`
|
||||||
|
Locale string `json:"locale"`
|
||||||
|
Desktop string `json:"desktop"`
|
||||||
|
Users []User `json:"users"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Encryption struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Sudo bool `json:"sudo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *InstallConfig) ToArchInstall() *ArchInstallConfig {
|
||||||
|
ac := &ArchInstallConfig{
|
||||||
|
Bootloader: "systemd-boot",
|
||||||
|
Kernels: []string{"linux-hardened"},
|
||||||
|
Filesystem: "btrfs",
|
||||||
|
Disk: c.Disk,
|
||||||
|
Hostname: c.Hostname,
|
||||||
|
Timezone: c.Timezone,
|
||||||
|
Locale: c.Locale,
|
||||||
|
Desktop: "cosmic",
|
||||||
|
Users: []User{
|
||||||
|
{
|
||||||
|
Username: c.Username,
|
||||||
|
Password: c.UserPassword,
|
||||||
|
Sudo: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.EnableLUKS {
|
||||||
|
ac.Encryption = &Encryption{
|
||||||
|
Type: "luks2",
|
||||||
|
Password: c.RootPassword,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ac
|
||||||
|
}
|
||||||
36
install.sh
Executable file
36
install.sh
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
INSTALL_URL="https://github.com/miasma-os/miasma-installer/releases/latest/download/miasma-installer"
|
||||||
|
INSTALL_PATH="/usr/local/bin/miasma-installer"
|
||||||
|
|
||||||
|
echo "Miasma OS Installer"
|
||||||
|
echo "==================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo "Error: This installer must be run as root"
|
||||||
|
echo "Please run: sudo curl -fsSL https://install.miasma-os.com | sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "/sys/firmware/efi" ]; then
|
||||||
|
echo "Error: UEFI boot mode not detected"
|
||||||
|
echo "Miasma OS requires UEFI boot mode"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v archinstall &> /dev/null; then
|
||||||
|
echo "Installing archinstall..."
|
||||||
|
pacman -Sy --noconfirm archinstall
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Downloading Miasma installer..."
|
||||||
|
curl -fsSL "$INSTALL_URL" -o "$INSTALL_PATH"
|
||||||
|
chmod +x "$INSTALL_PATH"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Starting Miasma OS installer..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
exec "$INSTALL_PATH"
|
||||||
4
scripts/01-cosmic-setup.sh
Executable file
4
scripts/01-cosmic-setup.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Setting up Cosmic Desktop..."
|
||||||
4
scripts/02-hardening.sh
Executable file
4
scripts/02-hardening.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Applying system hardening..."
|
||||||
71
tui/steps/confirm.go
Normal file
71
tui/steps/confirm.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package steps
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"miasma-installer/tui/styles"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfirmModel struct {
|
||||||
|
Disk string
|
||||||
|
EnableLUKS bool
|
||||||
|
Hostname string
|
||||||
|
Username string
|
||||||
|
Confirmed bool
|
||||||
|
Finished bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfirmModel(disk string, enableLUKS bool, hostname string, username string) ConfirmModel {
|
||||||
|
return ConfirmModel{
|
||||||
|
Disk: disk,
|
||||||
|
EnableLUKS: enableLUKS,
|
||||||
|
Hostname: hostname,
|
||||||
|
Username: username,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m ConfirmModel) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m ConfirmModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
if m.Finished {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "y":
|
||||||
|
m.Confirmed = true
|
||||||
|
m.Finished = true
|
||||||
|
return m, nil
|
||||||
|
case "n":
|
||||||
|
m.Confirmed = false
|
||||||
|
m.Finished = true
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m ConfirmModel) View() string {
|
||||||
|
s := styles.TitleStyle.Render("Confirm Installation")
|
||||||
|
s += "\n\n"
|
||||||
|
s += "Please review your installation settings:\n\n"
|
||||||
|
s += fmt.Sprintf(" Disk: %s\n", m.Disk)
|
||||||
|
s += fmt.Sprintf(" Encryption: %s\n", map[bool]string{true: "Enabled (LUKS2)", false: "Disabled"}[m.EnableLUKS])
|
||||||
|
s += fmt.Sprintf(" Hostname: %s\n", m.Hostname)
|
||||||
|
s += fmt.Sprintf(" Username: %s\n", m.Username)
|
||||||
|
s += fmt.Sprintf(" Filesystem: btrfs\n")
|
||||||
|
s += fmt.Sprintf(" Desktop: Cosmic Desktop\n")
|
||||||
|
s += fmt.Sprintf(" Kernel: linux-hardened\n")
|
||||||
|
s += fmt.Sprintf(" Bootloader: systemd-boot (UEFI)\n")
|
||||||
|
s += "\n"
|
||||||
|
s += styles.HelpStyle.Render("WARNING: This will ERASE ALL DATA on ") + m.Disk + "\n\n"
|
||||||
|
s += styles.HelpStyle.Render("[y] Proceed with installation [n] Cancel")
|
||||||
|
|
||||||
|
return styles.MainStyle.Render(s)
|
||||||
|
}
|
||||||
132
tui/steps/encryption.go
Normal file
132
tui/steps/encryption.go
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package steps
|
||||||
|
|
||||||
|
import (
|
||||||
|
"miasma-installer/tui/styles"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EncryptionModel struct {
|
||||||
|
EnableEncryption bool
|
||||||
|
Password string
|
||||||
|
ConfirmPassword string
|
||||||
|
passwordInput textinput.Model
|
||||||
|
confirmInput textinput.Model
|
||||||
|
focusIndex int
|
||||||
|
Finished bool
|
||||||
|
showInputs bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEncryptionModel() EncryptionModel {
|
||||||
|
pi := textinput.New()
|
||||||
|
pi.Placeholder = "Enter encryption password"
|
||||||
|
pi.EchoMode = textinput.EchoPassword
|
||||||
|
pi.Focus()
|
||||||
|
|
||||||
|
ci := textinput.New()
|
||||||
|
ci.Placeholder = "Confirm password"
|
||||||
|
ci.EchoMode = textinput.EchoPassword
|
||||||
|
|
||||||
|
return EncryptionModel{
|
||||||
|
EnableEncryption: true,
|
||||||
|
passwordInput: pi,
|
||||||
|
confirmInput: ci,
|
||||||
|
showInputs: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m EncryptionModel) Init() tea.Cmd {
|
||||||
|
return textinput.Blink
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m EncryptionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
if m.Finished {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "enter":
|
||||||
|
if !m.showInputs {
|
||||||
|
m.Finished = true
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.focusIndex == 0 {
|
||||||
|
m.focusIndex = 1
|
||||||
|
m.passwordInput.Blur()
|
||||||
|
return m, m.confirmInput.Focus()
|
||||||
|
} else if m.focusIndex == 1 {
|
||||||
|
if m.passwordInput.Value() == m.confirmInput.Value() && m.passwordInput.Value() != "" {
|
||||||
|
m.Password = m.passwordInput.Value()
|
||||||
|
m.Finished = true
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "y":
|
||||||
|
if !m.showInputs {
|
||||||
|
m.EnableEncryption = true
|
||||||
|
m.showInputs = true
|
||||||
|
return m, m.passwordInput.Focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
case "n":
|
||||||
|
if !m.showInputs {
|
||||||
|
m.EnableEncryption = false
|
||||||
|
m.Finished = true
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
case "tab", "shift+tab":
|
||||||
|
if m.showInputs {
|
||||||
|
if m.focusIndex == 0 {
|
||||||
|
m.focusIndex = 1
|
||||||
|
m.passwordInput.Blur()
|
||||||
|
return m, m.confirmInput.Focus()
|
||||||
|
} else {
|
||||||
|
m.focusIndex = 0
|
||||||
|
m.confirmInput.Blur()
|
||||||
|
return m, m.passwordInput.Focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd tea.Cmd
|
||||||
|
if m.showInputs {
|
||||||
|
if m.focusIndex == 0 {
|
||||||
|
m.passwordInput, cmd = m.passwordInput.Update(msg)
|
||||||
|
} else {
|
||||||
|
m.confirmInput, cmd = m.confirmInput.Update(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m EncryptionModel) View() string {
|
||||||
|
s := styles.TitleStyle.Render("Disk Encryption")
|
||||||
|
s += "\n\n"
|
||||||
|
|
||||||
|
if !m.showInputs {
|
||||||
|
s += "Enable LUKS2 disk encryption? (recommended)\n\n"
|
||||||
|
s += styles.HelpStyle.Render("[y] Yes [n] No")
|
||||||
|
} else {
|
||||||
|
s += "Enter encryption password:\n\n"
|
||||||
|
s += m.passwordInput.View() + "\n\n"
|
||||||
|
s += m.confirmInput.View() + "\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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s += styles.HelpStyle.Render("[Tab] Switch fields [Enter] Continue")
|
||||||
|
}
|
||||||
|
|
||||||
|
return styles.MainStyle.Render(s)
|
||||||
|
}
|
||||||
61
tui/steps/hostname.go
Normal file
61
tui/steps/hostname.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package steps
|
||||||
|
|
||||||
|
import (
|
||||||
|
"miasma-installer/tui/styles"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HostnameModel struct {
|
||||||
|
Hostname string
|
||||||
|
input textinput.Model
|
||||||
|
Finished bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHostnameModel() HostnameModel {
|
||||||
|
ti := textinput.New()
|
||||||
|
ti.Placeholder = "miasma"
|
||||||
|
ti.Focus()
|
||||||
|
ti.CharLimit = 63
|
||||||
|
|
||||||
|
return HostnameModel{
|
||||||
|
input: ti,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m HostnameModel) Init() tea.Cmd {
|
||||||
|
return textinput.Blink
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m HostnameModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
if m.Finished {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "enter":
|
||||||
|
if m.input.Value() != "" {
|
||||||
|
m.Hostname = m.input.Value()
|
||||||
|
m.Finished = true
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.input, cmd = m.input.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m HostnameModel) View() string {
|
||||||
|
s := styles.TitleStyle.Render("System Hostname")
|
||||||
|
s += "\n\n"
|
||||||
|
s += "Enter a hostname for your system:\n\n"
|
||||||
|
s += m.input.View() + "\n\n"
|
||||||
|
s += styles.HelpStyle.Render("[Enter] Continue")
|
||||||
|
|
||||||
|
return styles.MainStyle.Render(s)
|
||||||
|
}
|
||||||
125
tui/steps/user.go
Normal file
125
tui/steps/user.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package steps
|
||||||
|
|
||||||
|
import (
|
||||||
|
"miasma-installer/tui/styles"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserModel struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
usernameInput textinput.Model
|
||||||
|
passwordInput textinput.Model
|
||||||
|
confirmInput textinput.Model
|
||||||
|
focusIndex int
|
||||||
|
Finished bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserModel() UserModel {
|
||||||
|
ui := textinput.New()
|
||||||
|
ui.Placeholder = "username"
|
||||||
|
ui.Focus()
|
||||||
|
ui.CharLimit = 32
|
||||||
|
|
||||||
|
pi := textinput.New()
|
||||||
|
pi.Placeholder = "password"
|
||||||
|
pi.EchoMode = textinput.EchoPassword
|
||||||
|
|
||||||
|
ci := textinput.New()
|
||||||
|
ci.Placeholder = "confirm password"
|
||||||
|
ci.EchoMode = textinput.EchoPassword
|
||||||
|
|
||||||
|
return UserModel{
|
||||||
|
usernameInput: ui,
|
||||||
|
passwordInput: pi,
|
||||||
|
confirmInput: ci,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m UserModel) Init() tea.Cmd {
|
||||||
|
return textinput.Blink
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m UserModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
if m.Finished {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "enter":
|
||||||
|
if m.focusIndex == 0 && m.usernameInput.Value() != "" {
|
||||||
|
m.focusIndex = 1
|
||||||
|
m.usernameInput.Blur()
|
||||||
|
return m, m.passwordInput.Focus()
|
||||||
|
} else if m.focusIndex == 1 && m.passwordInput.Value() != "" {
|
||||||
|
m.focusIndex = 2
|
||||||
|
m.passwordInput.Blur()
|
||||||
|
return m, m.confirmInput.Focus()
|
||||||
|
} else if m.focusIndex == 2 {
|
||||||
|
if m.passwordInput.Value() == m.confirmInput.Value() && m.passwordInput.Value() != "" {
|
||||||
|
m.Username = m.usernameInput.Value()
|
||||||
|
m.Password = m.passwordInput.Value()
|
||||||
|
m.Finished = true
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "tab", "shift+tab":
|
||||||
|
m.focusIndex = (m.focusIndex + 1) % 3
|
||||||
|
cmds := make([]tea.Cmd, 3)
|
||||||
|
|
||||||
|
if m.focusIndex == 0 {
|
||||||
|
cmds[0] = m.usernameInput.Focus()
|
||||||
|
m.passwordInput.Blur()
|
||||||
|
m.confirmInput.Blur()
|
||||||
|
} else if m.focusIndex == 1 {
|
||||||
|
cmds[1] = m.passwordInput.Focus()
|
||||||
|
m.usernameInput.Blur()
|
||||||
|
m.confirmInput.Blur()
|
||||||
|
} else {
|
||||||
|
cmds[2] = m.confirmInput.Focus()
|
||||||
|
m.usernameInput.Blur()
|
||||||
|
m.passwordInput.Blur()
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd tea.Cmd
|
||||||
|
if m.focusIndex == 0 {
|
||||||
|
m.usernameInput, cmd = m.usernameInput.Update(msg)
|
||||||
|
} else if m.focusIndex == 1 {
|
||||||
|
m.passwordInput, cmd = m.passwordInput.Update(msg)
|
||||||
|
} else {
|
||||||
|
m.confirmInput, cmd = m.confirmInput.Update(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m UserModel) View() string {
|
||||||
|
s := styles.TitleStyle.Render("User Account")
|
||||||
|
s += "\n\n"
|
||||||
|
s += "Create your user account:\n\n"
|
||||||
|
s += "Username:\n"
|
||||||
|
s += m.usernameInput.View() + "\n\n"
|
||||||
|
s += "Password:\n"
|
||||||
|
s += m.passwordInput.View() + "\n\n"
|
||||||
|
s += "Confirm Password:\n"
|
||||||
|
s += m.confirmInput.View() + "\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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s += styles.HelpStyle.Render("[Tab] Switch fields [Enter] Continue")
|
||||||
|
|
||||||
|
return styles.MainStyle.Render(s)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user