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 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 fmt.Sprintf("%s - %s", i.size, i.description) } type ItemDelegate struct{} func (i ItemDelegate) Height() int { return 1 } func (i ItemDelegate) Spacing() int { return 0 } func (i ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil } func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) { disk, ok := item.(Disk) if !ok { return } s := fmt.Sprintf("%d. %s - %s", index+1, disk.Title(), disk.Description()) if index == m.Index() { s = styles.ActiveItemStyle.Render("> " + s) } else { s = styles.InactiveItemStyle.Render(" " + s) } fmt.Fprint(w, s) } type DiskSelModel struct { list list.Model Finished bool SelectedDisk string loading bool } type disksLoadedMsg struct { disks []list.Item } func NewDiskSelModel(width, height int) DiskSelModel { 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, loading: true, } } func (m DiskSelModel) Init() tea.Cmd { 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) { if m.Finished { return m, nil } switch msg := msg.(type) { case disksLoadedMsg: m.loading = false m.list.SetItems(msg.disks) return m, nil case tea.KeyMsg: if !m.loading && msg.String() == "enter" { if item, ok := m.list.SelectedItem().(Disk); ok { m.SelectedDisk = item.name m.Finished = true return m, nil } } } var cmd tea.Cmd m.list, cmd = m.list.Update(msg) return m, cmd } func (m DiskSelModel) View() string { var content strings.Builder if m.loading { content.WriteString(styles.TitleStyle.Render("💿 Scanning Disks...")) content.WriteString("\n\n") content.WriteString(styles.SpinnerStyle.Render("● Detecting available storage devices...")) return styles.AppStyle.Render(content.String()) } content.WriteString(styles.TitleStyle.Render("💿 Select Installation Disk")) content.WriteString("\n\n") warning := "⚠️ All data on the selected disk will be erased!" content.WriteString(styles.WarningBoxStyle.Render(warning)) content.WriteString("\n\n") content.WriteString(m.list.View()) content.WriteString("\n\n") content.WriteString(styles.RenderHelp("↑/↓", "Navigate", "Enter", "Select")) return styles.AppStyle.Render(content.String()) } 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 }