detached ea9cd09

feat(amd): add comprehensive AMD CPU detection and P-State configuration

- Add Zen generation detection (Zen 1-5) based on CPU family/model
- Implement AMD P-State mode detection and recommendations
- Add interactive P-State selection form with descriptions
- Support active, guided, and passive modes based on CPU generation
- Generate proper kernel parameters including Zen 2 shared_mem workaround
- Add new StateAMDPStatePrompt UI state between options and confirmation
- Show AMD P-State configuration in confirmation screen
- Update boot phase to use Zen-aware kernel parameter generation

Matches functionality from legacy shell script install/base/amd-cpu.sh
commit
ea9cd09fcba5a662251081369ec1ecb5a48a2609
author
bnema · · 2025-10-26 21:32 UTC
parent
2b21852
signature
unsigned
4 files · +290 -11
M internal/phases/boot.go +15 -3
@@ -195,9 +195,21 @@ func (p *BootPhase) configureLimine(progressChan chan<- ProgressUpdate) error {
195195 // Add AMD-specific kernel parameters if configured
196196 switch {
197197 case p.config.AMDPState != "":
198- amdParams := fmt.Sprintf("amd_pstate=%s", p.config.AMDPState)
199- kernelParams = fmt.Sprintf("%s %s", kernelParams, amdParams)
200- p.SendOutput(progressChan, fmt.Sprintf("Added AMD P-State: %s", p.config.AMDPState))
198+ // Detect CPU to get Zen generation for proper kernel params
199+ cpuInfo, err := system.DetectCPUInfo()
200+ var amdParams string
201+ if err == nil && cpuInfo.AMDZenGen != nil {
202+ // Use helper function that adds extra params for Zen 2 if needed
203+ amdParams = system.GetAMDPStateKernelParams(cpuInfo.AMDZenGen, system.AMDPStateMode(p.config.AMDPState))
204+ } else {
205+ // Fallback to simple parameter if detection fails
206+ amdParams = fmt.Sprintf("amd_pstate=%s", p.config.AMDPState)
207+ }
208+
209+ if amdParams != "" {
210+ kernelParams = fmt.Sprintf("%s %s", kernelParams, amdParams)
211+ p.SendOutput(progressChan, fmt.Sprintf("Added AMD P-State: %s", amdParams))
212+ }
201213 }
202214
203215 // Add quiet/splash parameters
@@ -195,9 +195,21 @@ func (p *BootPhase) configureLimine(progressChan chan<- ProgressUpdate) error {
195 // Add AMD-specific kernel parameters if configured195 // Add AMD-specific kernel parameters if configured
196 switch {196 switch {
197 case p.config.AMDPState != "":197 case p.config.AMDPState != "":
198- amdParams := fmt.Sprintf("amd_pstate=%s", p.config.AMDPState)198+ // Detect CPU to get Zen generation for proper kernel params
199- kernelParams = fmt.Sprintf("%s %s", kernelParams, amdParams)199+ cpuInfo, err := system.DetectCPUInfo()
200- p.SendOutput(progressChan, fmt.Sprintf("Added AMD P-State: %s", p.config.AMDPState))200+ var amdParams string
201+ if err == nil && cpuInfo.AMDZenGen != nil {
202+ // Use helper function that adds extra params for Zen 2 if needed
203+ amdParams = system.GetAMDPStateKernelParams(cpuInfo.AMDZenGen, system.AMDPStateMode(p.config.AMDPState))
204+ } else {
205+ // Fallback to simple parameter if detection fails
206+ amdParams = fmt.Sprintf("amd_pstate=%s", p.config.AMDPState)
207+ }
208+
209+ if amdParams != "" {
210+ kernelParams = fmt.Sprintf("%s %s", kernelParams, amdParams)
211+ p.SendOutput(progressChan, fmt.Sprintf("Added AMD P-State: %s", amdParams))
212+ }
201 }213 }
202 214
203 // Add quiet/splash parameters215 // Add quiet/splash parameters
M internal/system/cpu.go +179 -2
@@ -3,6 +3,7 @@ package system
33 import (
44 "fmt"
55 "os"
6+ "strconv"
67 "strings"
78 )
89
@@ -22,13 +23,23 @@ const (
2223 AMDPStateModeActive AMDPStateMode = "active"
2324 AMDPStateModeGuided AMDPStateMode = "guided"
2425 AMDPStateModePassive AMDPStateMode = "passive"
26+ AMDPStateModeNone AMDPStateMode = ""
2527 )
2628
29+// AMDZenGen represents AMD Zen generation
30+type AMDZenGen struct {
31+ Generation string // "1", "1+", "2", "3", "3+", "4", "5", "unknown"
32+ Label string // "Zen 1", "Zen+", "Zen 2", etc.
33+}
34+
2735 // CPUInfo holds detected CPU information
2836 type CPUInfo struct {
2937 Vendor CPUVendor
38+ ModelName string // Full CPU model name
3039 Microcode string
40+ AMDZenGen *AMDZenGen // AMD Zen generation (nil for non-AMD)
3141 AMDPStateModes []AMDPStateMode
42+ RecommendedPStateMode AMDPStateMode // Recommended mode for this CPU
3243 }
3344
3445 // DetectCPUVendor reads /proc/cpuinfo and returns the CPU vendor
@@ -161,6 +172,166 @@ func GetAMDPStateModeDescription(mode AMDPStateMode) string {
161172 }
162173 }
163174
175+// DetectAMDZenGeneration detects AMD Zen generation based on CPU family and model
176+// Reference: https://en.wikichip.org/wiki/amd/cpuid
177+func DetectAMDZenGeneration() (*AMDZenGen, error) {
178+ data, err := os.ReadFile("/proc/cpuinfo")
179+ if err != nil {
180+ return nil, fmt.Errorf("failed to read /proc/cpuinfo: %w", err)
181+ }
182+
183+ var cpuFamily, cpuModel int
184+ lines := strings.Split(string(data), "\n")
185+
186+ for _, line := range lines {
187+ if strings.HasPrefix(line, "cpu family") {
188+ parts := strings.Split(line, ":")
189+ if len(parts) >= 2 {
190+ val := strings.TrimSpace(parts[1])
191+ cpuFamily, _ = strconv.Atoi(val)
192+ }
193+ }
194+ if strings.HasPrefix(line, "model") && !strings.HasPrefix(line, "model name") {
195+ parts := strings.Split(line, ":")
196+ if len(parts) >= 2 {
197+ val := strings.TrimSpace(parts[1])
198+ cpuModel, _ = strconv.Atoi(val)
199+ }
200+ }
201+
202+ // Break early if we have both values
203+ if cpuFamily > 0 && cpuModel >= 0 {
204+ break
205+ }
206+ }
207+
208+ zenGen := &AMDZenGen{
209+ Generation: "unknown",
210+ Label: "Unknown",
211+ }
212+
213+ // Detect Zen generation based on CPU family and model
214+ switch cpuFamily {
215+ case 23: // Family 17h
216+ if cpuModel >= 0 && cpuModel <= 15 {
217+ zenGen.Generation = "1"
218+ zenGen.Label = "Zen 1"
219+ } else if cpuModel >= 16 && cpuModel <= 31 {
220+ zenGen.Generation = "1+"
221+ zenGen.Label = "Zen+"
222+ } else if cpuModel >= 48 && cpuModel <= 127 {
223+ zenGen.Generation = "2"
224+ zenGen.Label = "Zen 2"
225+ } else {
226+ zenGen.Generation = "2"
227+ zenGen.Label = "Zen 2 (assumed)"
228+ }
229+ case 25: // Family 19h
230+ if cpuModel >= 0 && cpuModel <= 15 {
231+ zenGen.Generation = "3"
232+ zenGen.Label = "Zen 3"
233+ } else if cpuModel >= 32 && cpuModel <= 47 {
234+ zenGen.Generation = "3"
235+ zenGen.Label = "Zen 3"
236+ } else if cpuModel >= 80 && cpuModel <= 95 {
237+ zenGen.Generation = "3+"
238+ zenGen.Label = "Zen 3+"
239+ } else if cpuModel >= 96 && cpuModel <= 175 {
240+ zenGen.Generation = "4"
241+ zenGen.Label = "Zen 4"
242+ } else {
243+ zenGen.Generation = "3"
244+ zenGen.Label = "Zen 3 (assumed)"
245+ }
246+ case 26: // Family 1Ah
247+ zenGen.Generation = "5"
248+ zenGen.Label = "Zen 5"
249+ }
250+
251+ return zenGen, nil
252+}
253+
254+// GetRecommendedPStateMode returns the best recommended mode for each Zen generation
255+func GetRecommendedPStateMode(zenGen *AMDZenGen) AMDPStateMode {
256+ switch zenGen.Generation {
257+ case "1", "1+":
258+ return AMDPStateModeNone // Use default acpi-cpufreq
259+ case "2":
260+ return AMDPStateModePassive // Conservative choice for Zen 2
261+ case "3", "3+":
262+ return AMDPStateModeActive // Best for Zen 3/3+
263+ case "4", "5":
264+ return AMDPStateModeActive // Best for Zen 4/5
265+ default:
266+ return AMDPStateModePassive
267+ }
268+}
269+
270+// GetAvailablePStateModes returns available P-State modes for a given Zen generation
271+func GetAvailablePStateModes(zenGen *AMDZenGen) []AMDPStateMode {
272+ switch zenGen.Generation {
273+ case "1", "1+":
274+ // Zen 1 and Zen+: No CPPC support, use acpi-cpufreq
275+ return []AMDPStateMode{}
276+ case "2":
277+ // Zen 2: Basic CPPC support, passive and active modes
278+ return []AMDPStateMode{AMDPStateModePassive, AMDPStateModeActive}
279+ case "3", "3+", "4", "5":
280+ // Zen 3+: Full CPPC support for all modes
281+ return []AMDPStateMode{AMDPStateModeActive, AMDPStateModeGuided, AMDPStateModePassive}
282+ default:
283+ return []AMDPStateMode{AMDPStateModePassive}
284+ }
285+}
286+
287+// GetPStateModeDescription returns human-readable description for P-State mode
288+func GetPStateModeDescription(mode AMDPStateMode) string {
289+ switch mode {
290+ case AMDPStateModeActive:
291+ return "CPPC autonomous with EPP hints (best performance, desktop/gaming)"
292+ case AMDPStateModeGuided:
293+ return "CPPC guided autonomous (balanced, laptop/hybrid)"
294+ case AMDPStateModePassive:
295+ return "CPPC non-autonomous (compatibility, manual governor control)"
296+ default:
297+ return string(mode)
298+ }
299+}
300+
301+// GetAMDPStateKernelParams returns kernel parameters needed for AMD P-State mode
302+func GetAMDPStateKernelParams(zenGen *AMDZenGen, mode AMDPStateMode) string {
303+ if mode == AMDPStateModeNone || mode == "" {
304+ return ""
305+ }
306+
307+ // Some Zen 2 CPUs need shared_mem parameter for active mode
308+ if zenGen.Generation == "2" && mode == AMDPStateModeActive {
309+ return fmt.Sprintf("amd_pstate=%s amd_pstate.shared_mem=1", mode)
310+ }
311+
312+ return fmt.Sprintf("amd_pstate=%s", mode)
313+}
314+
315+// GetCPUModelName extracts the CPU model name from /proc/cpuinfo
316+func GetCPUModelName() string {
317+ data, err := os.ReadFile("/proc/cpuinfo")
318+ if err != nil {
319+ return ""
320+ }
321+
322+ lines := strings.Split(string(data), "\n")
323+ for _, line := range lines {
324+ if strings.HasPrefix(line, "model name") {
325+ parts := strings.Split(line, ":")
326+ if len(parts) >= 2 {
327+ return strings.TrimSpace(parts[1])
328+ }
329+ }
330+ }
331+
332+ return ""
333+}
334+
164335 // DetectCPUInfo detects all CPU information in one call (DRY)
165336 func DetectCPUInfo() (*CPUInfo, error) {
166337 vendor, err := DetectCPUVendor()
@@ -171,13 +342,19 @@ func DetectCPUInfo() (*CPUInfo, error) {
171342
172343 info := &CPUInfo{
173344 Vendor: vendor,
345+ ModelName: GetCPUModelName(),
174346 Microcode: DetectMicrocode(vendor),
175347 }
176348
177- // Detect AMD P-State modes only for AMD CPUs
349+ // Detect AMD-specific features only for AMD CPUs
178350 switch vendor {
179351 case CPUVendorAMD:
180- info.AMDPStateModes = DetectAMDPStateModes()
352+ zenGen, err := DetectAMDZenGeneration()
353+ if err == nil {
354+ info.AMDZenGen = zenGen
355+ info.AMDPStateModes = GetAvailablePStateModes(zenGen)
356+ info.RecommendedPStateMode = GetRecommendedPStateMode(zenGen)
357+ }
181358 }
182359
183360 return info, nil
@@ -3,6 +3,7 @@ package system
3 import (3 import (
4 "fmt"4 "fmt"
5 "os"5 "os"
6+ "strconv"
6 "strings"7 "strings"
7 )8 )
8 9
@@ -22,13 +23,23 @@ const (
22 AMDPStateModeActive AMDPStateMode = "active"23 AMDPStateModeActive AMDPStateMode = "active"
23 AMDPStateModeGuided AMDPStateMode = "guided"24 AMDPStateModeGuided AMDPStateMode = "guided"
24 AMDPStateModePassive AMDPStateMode = "passive"25 AMDPStateModePassive AMDPStateMode = "passive"
26+ AMDPStateModeNone AMDPStateMode = ""
25 )27 )
26 28
29+// AMDZenGen represents AMD Zen generation
30+type AMDZenGen struct {
31+ Generation string // "1", "1+", "2", "3", "3+", "4", "5", "unknown"
32+ Label string // "Zen 1", "Zen+", "Zen 2", etc.
33+}
34+
27 // CPUInfo holds detected CPU information35 // CPUInfo holds detected CPU information
28 type CPUInfo struct {36 type CPUInfo struct {
29 Vendor CPUVendor37 Vendor CPUVendor
38+ ModelName string // Full CPU model name
30 Microcode string39 Microcode string
40+ AMDZenGen *AMDZenGen // AMD Zen generation (nil for non-AMD)
31 AMDPStateModes []AMDPStateMode41 AMDPStateModes []AMDPStateMode
42+ RecommendedPStateMode AMDPStateMode // Recommended mode for this CPU
32 }43 }
33 44
34 // DetectCPUVendor reads /proc/cpuinfo and returns the CPU vendor45 // DetectCPUVendor reads /proc/cpuinfo and returns the CPU vendor
@@ -161,6 +172,166 @@ func GetAMDPStateModeDescription(mode AMDPStateMode) string {
161 }172 }
162 }173 }
163 174
175+// DetectAMDZenGeneration detects AMD Zen generation based on CPU family and model
176+// Reference: https://en.wikichip.org/wiki/amd/cpuid
177+func DetectAMDZenGeneration() (*AMDZenGen, error) {
178+ data, err := os.ReadFile("/proc/cpuinfo")
179+ if err != nil {
180+ return nil, fmt.Errorf("failed to read /proc/cpuinfo: %w", err)
181+ }
182+
183+ var cpuFamily, cpuModel int
184+ lines := strings.Split(string(data), "\n")
185+
186+ for _, line := range lines {
187+ if strings.HasPrefix(line, "cpu family") {
188+ parts := strings.Split(line, ":")
189+ if len(parts) >= 2 {
190+ val := strings.TrimSpace(parts[1])
191+ cpuFamily, _ = strconv.Atoi(val)
192+ }
193+ }
194+ if strings.HasPrefix(line, "model") && !strings.HasPrefix(line, "model name") {
195+ parts := strings.Split(line, ":")
196+ if len(parts) >= 2 {
197+ val := strings.TrimSpace(parts[1])
198+ cpuModel, _ = strconv.Atoi(val)
199+ }
200+ }
201+
202+ // Break early if we have both values
203+ if cpuFamily > 0 && cpuModel >= 0 {
204+ break
205+ }
206+ }
207+
208+ zenGen := &AMDZenGen{
209+ Generation: "unknown",
210+ Label: "Unknown",
211+ }
212+
213+ // Detect Zen generation based on CPU family and model
214+ switch cpuFamily {
215+ case 23: // Family 17h
216+ if cpuModel >= 0 && cpuModel <= 15 {
217+ zenGen.Generation = "1"
218+ zenGen.Label = "Zen 1"
219+ } else if cpuModel >= 16 && cpuModel <= 31 {
220+ zenGen.Generation = "1+"
221+ zenGen.Label = "Zen+"
222+ } else if cpuModel >= 48 && cpuModel <= 127 {
223+ zenGen.Generation = "2"
224+ zenGen.Label = "Zen 2"
225+ } else {
226+ zenGen.Generation = "2"
227+ zenGen.Label = "Zen 2 (assumed)"
228+ }
229+ case 25: // Family 19h
230+ if cpuModel >= 0 && cpuModel <= 15 {
231+ zenGen.Generation = "3"
232+ zenGen.Label = "Zen 3"
233+ } else if cpuModel >= 32 && cpuModel <= 47 {
234+ zenGen.Generation = "3"
235+ zenGen.Label = "Zen 3"
236+ } else if cpuModel >= 80 && cpuModel <= 95 {
237+ zenGen.Generation = "3+"
238+ zenGen.Label = "Zen 3+"
239+ } else if cpuModel >= 96 && cpuModel <= 175 {
240+ zenGen.Generation = "4"
241+ zenGen.Label = "Zen 4"
242+ } else {
243+ zenGen.Generation = "3"
244+ zenGen.Label = "Zen 3 (assumed)"
245+ }
246+ case 26: // Family 1Ah
247+ zenGen.Generation = "5"
248+ zenGen.Label = "Zen 5"
249+ }
250+
251+ return zenGen, nil
252+}
253+
254+// GetRecommendedPStateMode returns the best recommended mode for each Zen generation
255+func GetRecommendedPStateMode(zenGen *AMDZenGen) AMDPStateMode {
256+ switch zenGen.Generation {
257+ case "1", "1+":
258+ return AMDPStateModeNone // Use default acpi-cpufreq
259+ case "2":
260+ return AMDPStateModePassive // Conservative choice for Zen 2
261+ case "3", "3+":
262+ return AMDPStateModeActive // Best for Zen 3/3+
263+ case "4", "5":
264+ return AMDPStateModeActive // Best for Zen 4/5
265+ default:
266+ return AMDPStateModePassive
267+ }
268+}
269+
270+// GetAvailablePStateModes returns available P-State modes for a given Zen generation
271+func GetAvailablePStateModes(zenGen *AMDZenGen) []AMDPStateMode {
272+ switch zenGen.Generation {
273+ case "1", "1+":
274+ // Zen 1 and Zen+: No CPPC support, use acpi-cpufreq
275+ return []AMDPStateMode{}
276+ case "2":
277+ // Zen 2: Basic CPPC support, passive and active modes
278+ return []AMDPStateMode{AMDPStateModePassive, AMDPStateModeActive}
279+ case "3", "3+", "4", "5":
280+ // Zen 3+: Full CPPC support for all modes
281+ return []AMDPStateMode{AMDPStateModeActive, AMDPStateModeGuided, AMDPStateModePassive}
282+ default:
283+ return []AMDPStateMode{AMDPStateModePassive}
284+ }
285+}
286+
287+// GetPStateModeDescription returns human-readable description for P-State mode
288+func GetPStateModeDescription(mode AMDPStateMode) string {
289+ switch mode {
290+ case AMDPStateModeActive:
291+ return "CPPC autonomous with EPP hints (best performance, desktop/gaming)"
292+ case AMDPStateModeGuided:
293+ return "CPPC guided autonomous (balanced, laptop/hybrid)"
294+ case AMDPStateModePassive:
295+ return "CPPC non-autonomous (compatibility, manual governor control)"
296+ default:
297+ return string(mode)
298+ }
299+}
300+
301+// GetAMDPStateKernelParams returns kernel parameters needed for AMD P-State mode
302+func GetAMDPStateKernelParams(zenGen *AMDZenGen, mode AMDPStateMode) string {
303+ if mode == AMDPStateModeNone || mode == "" {
304+ return ""
305+ }
306+
307+ // Some Zen 2 CPUs need shared_mem parameter for active mode
308+ if zenGen.Generation == "2" && mode == AMDPStateModeActive {
309+ return fmt.Sprintf("amd_pstate=%s amd_pstate.shared_mem=1", mode)
310+ }
311+
312+ return fmt.Sprintf("amd_pstate=%s", mode)
313+}
314+
315+// GetCPUModelName extracts the CPU model name from /proc/cpuinfo
316+func GetCPUModelName() string {
317+ data, err := os.ReadFile("/proc/cpuinfo")
318+ if err != nil {
319+ return ""
320+ }
321+
322+ lines := strings.Split(string(data), "\n")
323+ for _, line := range lines {
324+ if strings.HasPrefix(line, "model name") {
325+ parts := strings.Split(line, ":")
326+ if len(parts) >= 2 {
327+ return strings.TrimSpace(parts[1])
328+ }
329+ }
330+ }
331+
332+ return ""
333+}
334+
164 // DetectCPUInfo detects all CPU information in one call (DRY)335 // DetectCPUInfo detects all CPU information in one call (DRY)
165 func DetectCPUInfo() (*CPUInfo, error) {336 func DetectCPUInfo() (*CPUInfo, error) {
166 vendor, err := DetectCPUVendor()337 vendor, err := DetectCPUVendor()
@@ -171,13 +342,19 @@ func DetectCPUInfo() (*CPUInfo, error) {
171 342
172 info := &CPUInfo{343 info := &CPUInfo{
173 Vendor: vendor,344 Vendor: vendor,
345+ ModelName: GetCPUModelName(),
174 Microcode: DetectMicrocode(vendor),346 Microcode: DetectMicrocode(vendor),
175 }347 }
176 348
177- // Detect AMD P-State modes only for AMD CPUs349+ // Detect AMD-specific features only for AMD CPUs
178 switch vendor {350 switch vendor {
179 case CPUVendorAMD:351 case CPUVendorAMD:
180- info.AMDPStateModes = DetectAMDPStateModes()352+ zenGen, err := DetectAMDZenGeneration()
353+ if err == nil {
354+ info.AMDZenGen = zenGen
355+ info.AMDPStateModes = GetAvailablePStateModes(zenGen)
356+ info.RecommendedPStateMode = GetRecommendedPStateMode(zenGen)
357+ }
181 }358 }
182 359
183 return info, nil360 return info, nil
M internal/ui/installer_forms.go +41 -0
@@ -22,6 +22,12 @@ func CreatePreflightForm(cfg *config.Config, fb *components.FormBuilder) *huh.Fo
2222 cfg.Timezone = config.TimezoneDefault
2323 }
2424
25+ // Set default keymap if not already set
26+ switch {
27+ case cfg.Keymap == "":
28+ cfg.Keymap = config.KeymapDefault
29+ }
30+
2531 return fb.CreateForm(
2632 // System configuration
2733 huh.NewGroup(
@@ -181,6 +187,41 @@ func CreateOptionsForm(cfg *config.Config, fb *components.FormBuilder) *huh.Form
181187 return fb.CreateForm(groups...)
182188 }
183189
190+// CreateAMDPStateForm creates AMD P-State driver selection form
191+// This form is only shown for AMD CPUs that support P-State
192+func CreateAMDPStateForm(cfg *config.Config, cpuInfo *system.CPUInfo, fb *components.FormBuilder) *huh.Form {
193+ // Build P-State mode options based on available modes
194+ var modeOptions []huh.Option[string]
195+
196+ for _, mode := range cpuInfo.AMDPStateModes {
197+ desc := system.GetPStateModeDescription(mode)
198+ label := fmt.Sprintf("%s - %s", mode, desc)
199+
200+ // Mark recommended mode
201+ if mode == cpuInfo.RecommendedPStateMode {
202+ label += " (recommended)"
203+ }
204+
205+ modeOptions = append(modeOptions, huh.NewOption(label, string(mode)))
206+ }
207+
208+ // Build description text
209+ zenLabel := "Unknown"
210+ if cpuInfo.AMDZenGen != nil {
211+ zenLabel = cpuInfo.AMDZenGen.Label
212+ }
213+
214+ description := fmt.Sprintf("Detected: %s\nCPU: %s\n\nNote: Requires CPPC enabled in UEFI (AMD CBS > NBIO > SMU > CPPC)",
215+ zenLabel, cpuInfo.ModelName)
216+
217+ return fb.CreateForm(
218+ huh.NewGroup(
219+ fb.SelectWithOptions("AMD P-State Mode", modeOptions, &cfg.AMDPState).
220+ Description(description),
221+ ).Title("AMD P-State Configuration"),
222+ )
223+}
224+
184225 // FormatDiskOption formats a disk into a user-friendly option string (DRY helper)
185226 func FormatDiskOption(disk system.Disk) string {
186227 label := fmt.Sprintf("%s (%s)", disk.Path, disk.Size)
@@ -22,6 +22,12 @@ func CreatePreflightForm(cfg *config.Config, fb *components.FormBuilder) *huh.Fo
22 cfg.Timezone = config.TimezoneDefault22 cfg.Timezone = config.TimezoneDefault
23 }23 }
24 24
25+ // Set default keymap if not already set
26+ switch {
27+ case cfg.Keymap == "":
28+ cfg.Keymap = config.KeymapDefault
29+ }
30+
25 return fb.CreateForm(31 return fb.CreateForm(
26 // System configuration32 // System configuration
27 huh.NewGroup(33 huh.NewGroup(
@@ -181,6 +187,41 @@ func CreateOptionsForm(cfg *config.Config, fb *components.FormBuilder) *huh.Form
181 return fb.CreateForm(groups...)187 return fb.CreateForm(groups...)
182 }188 }
183 189
190+// CreateAMDPStateForm creates AMD P-State driver selection form
191+// This form is only shown for AMD CPUs that support P-State
192+func CreateAMDPStateForm(cfg *config.Config, cpuInfo *system.CPUInfo, fb *components.FormBuilder) *huh.Form {
193+ // Build P-State mode options based on available modes
194+ var modeOptions []huh.Option[string]
195+
196+ for _, mode := range cpuInfo.AMDPStateModes {
197+ desc := system.GetPStateModeDescription(mode)
198+ label := fmt.Sprintf("%s - %s", mode, desc)
199+
200+ // Mark recommended mode
201+ if mode == cpuInfo.RecommendedPStateMode {
202+ label += " (recommended)"
203+ }
204+
205+ modeOptions = append(modeOptions, huh.NewOption(label, string(mode)))
206+ }
207+
208+ // Build description text
209+ zenLabel := "Unknown"
210+ if cpuInfo.AMDZenGen != nil {
211+ zenLabel = cpuInfo.AMDZenGen.Label
212+ }
213+
214+ description := fmt.Sprintf("Detected: %s\nCPU: %s\n\nNote: Requires CPPC enabled in UEFI (AMD CBS > NBIO > SMU > CPPC)",
215+ zenLabel, cpuInfo.ModelName)
216+
217+ return fb.CreateForm(
218+ huh.NewGroup(
219+ fb.SelectWithOptions("AMD P-State Mode", modeOptions, &cfg.AMDPState).
220+ Description(description),
221+ ).Title("AMD P-State Configuration"),
222+ )
223+}
224+
184 // FormatDiskOption formats a disk into a user-friendly option string (DRY helper)225 // FormatDiskOption formats a disk into a user-friendly option string (DRY helper)
185 func FormatDiskOption(disk system.Disk) string {226 func FormatDiskOption(disk system.Disk) string {
186 label := fmt.Sprintf("%s (%s)", disk.Path, disk.Size)227 label := fmt.Sprintf("%s (%s)", disk.Path, disk.Size)
M internal/ui/model.go +55 -6
@@ -6,6 +6,7 @@ import (
66 "github.com/bnema/archup/internal/config"
77 "github.com/bnema/archup/internal/logger"
88 "github.com/bnema/archup/internal/phases"
9+ "github.com/bnema/archup/internal/system"
910 "github.com/bnema/archup/internal/ui/components"
1011 "github.com/bnema/archup/internal/ui/styles"
1112 "github.com/charmbracelet/bubbles/spinner"
@@ -22,6 +23,7 @@ const (
2223 StatePreflightForm
2324 StateDiskForm
2425 StateOptionsForm
26+ StateAMDPStatePrompt // NEW: AMD P-State selection (shown only for AMD CPUs)
2527 StateConfirmation
2628 StateExecuting
2729 StateComplete
@@ -42,6 +44,7 @@ type Model struct {
4244 width int
4345 height int
4446 err error
47+ cpuInfo *system.CPUInfo // Detected CPU information
4548 }
4649
4750 // NewModel creates a new installer model
@@ -116,10 +119,21 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
116119 m.currentForm = CreateDiskSelectionForm(m.config, m.formBuilder)
117120 return m, m.currentForm.Init()
118121
119- case StateConfirmation:
122+ case StateAMDPStatePrompt:
120123 m.state = StateOptionsForm
121124 m.currentForm = CreateOptionsForm(m.config, m.formBuilder)
122125 return m, m.currentForm.Init()
126+
127+ case StateConfirmation:
128+ // Go back to AMD P-State prompt if we have AMD CPU, otherwise options
129+ if m.cpuInfo != nil && m.cpuInfo.Vendor == system.CPUVendorAMD && len(m.cpuInfo.AMDPStateModes) > 0 {
130+ m.state = StateAMDPStatePrompt
131+ m.currentForm = CreateAMDPStateForm(m.config, m.cpuInfo, m.formBuilder)
132+ } else {
133+ m.state = StateOptionsForm
134+ m.currentForm = CreateOptionsForm(m.config, m.formBuilder)
135+ }
136+ return m, m.currentForm.Init()
123137 }
124138 }
125139
@@ -161,7 +175,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
161175
162176 // Update current form if showing one
163177 switch m.state {
164- case StatePreflightForm, StateDiskForm, StateOptionsForm:
178+ case StatePreflightForm, StateDiskForm, StateOptionsForm, StateAMDPStatePrompt:
165179 switch {
166180 case m.currentForm != nil:
167181 // Don't pass WindowSizeMsg to form to preserve our max width
@@ -200,7 +214,7 @@ func (m *Model) View() string {
200214 centerText(WelcomeTagline, m.width) + "\n\n" +
201215 centerText(styles.HelpStyle.Render("Press ENTER to start installation"), m.width)
202216
203- case StatePreflightForm, StateDiskForm, StateOptionsForm:
217+ case StatePreflightForm, StateDiskForm, StateOptionsForm, StateAMDPStatePrompt:
204218 switch {
205219 case m.currentForm != nil:
206220 header := centerText(m.renderPhaseHeader(), m.width)
@@ -257,8 +271,28 @@ func (m *Model) handleFormComplete() tea.Cmd {
257271 m.logger.Info("Options form completed",
258272 "kernel", m.config.KernelChoice,
259273 "encryption", m.config.EncryptionType)
260- // Move directly to confirmation
261- // Encryption password is already handled in preflight form
274+
275+ // Detect CPU and check if we need to show AMD P-State prompt
276+ cpuInfo, err := system.DetectCPUInfo()
277+ if err == nil {
278+ m.cpuInfo = cpuInfo
279+ // Show AMD P-State prompt only for AMD CPUs with P-State support
280+ if cpuInfo.Vendor == system.CPUVendorAMD && len(cpuInfo.AMDPStateModes) > 0 {
281+ m.logger.Info("AMD CPU detected - showing P-State configuration",
282+ "zen_gen", cpuInfo.AMDZenGen.Label,
283+ "modes", len(cpuInfo.AMDPStateModes))
284+ m.state = StateAMDPStatePrompt
285+ m.currentForm = CreateAMDPStateForm(m.config, cpuInfo, m.formBuilder)
286+ return m.currentForm.Init()
287+ }
288+ }
289+
290+ // Move directly to confirmation if not AMD or detection failed
291+ m.state = StateConfirmation
292+ return nil
293+
294+ case StateAMDPStatePrompt:
295+ m.logger.Info("AMD P-State configuration completed", "mode", m.config.AMDPState)
262296 m.state = StateConfirmation
263297 return nil
264298
@@ -277,6 +311,12 @@ func (m *Model) renderConfirmation() string {
277311 s += fmt.Sprintf("Disk: %s\n", m.config.TargetDisk)
278312 s += fmt.Sprintf("Kernel: %s\n", m.config.KernelChoice)
279313 s += fmt.Sprintf("Encryption: %s\n", m.config.EncryptionType)
314+
315+ // Show AMD P-State mode if configured
316+ if m.config.AMDPState != "" {
317+ s += fmt.Sprintf("AMD P-State: %s\n", m.config.AMDPState)
318+ }
319+
280320 s += "\n"
281321 s += styles.WarningStyle.Render("WARNING: This will erase all data on "+m.config.TargetDisk) + "\n\n"
282322 s += styles.HelpStyle.Render("Press ENTER to continue, Ctrl+C to cancel")
@@ -330,10 +370,19 @@ func (m *Model) renderPhaseHeader() string {
330370 case StateOptionsForm:
331371 stepNum = "3"
332372 stepName = "Installation Options"
373+ case StateAMDPStatePrompt:
374+ stepNum = "4"
375+ stepName = "AMD P-State Configuration"
333376 default:
334377 return ""
335378 }
336379
337- header := fmt.Sprintf("ArchUp Install - Step %s/3: %s", stepNum, stepName)
380+ // Total steps depends on whether AMD prompt is shown
381+ totalSteps := "3"
382+ if m.state == StateAMDPStatePrompt {
383+ totalSteps = "4"
384+ }
385+
386+ header := fmt.Sprintf("ArchUp Install - Step %s/%s: %s", stepNum, totalSteps, stepName)
338387 return styles.TitleStyle.Render(header)
339388 }
@@ -6,6 +6,7 @@ import (
6 "github.com/bnema/archup/internal/config"6 "github.com/bnema/archup/internal/config"
7 "github.com/bnema/archup/internal/logger"7 "github.com/bnema/archup/internal/logger"
8 "github.com/bnema/archup/internal/phases"8 "github.com/bnema/archup/internal/phases"
9+ "github.com/bnema/archup/internal/system"
9 "github.com/bnema/archup/internal/ui/components"10 "github.com/bnema/archup/internal/ui/components"
10 "github.com/bnema/archup/internal/ui/styles"11 "github.com/bnema/archup/internal/ui/styles"
11 "github.com/charmbracelet/bubbles/spinner"12 "github.com/charmbracelet/bubbles/spinner"
@@ -22,6 +23,7 @@ const (
22 StatePreflightForm23 StatePreflightForm
23 StateDiskForm24 StateDiskForm
24 StateOptionsForm25 StateOptionsForm
26+ StateAMDPStatePrompt // NEW: AMD P-State selection (shown only for AMD CPUs)
25 StateConfirmation27 StateConfirmation
26 StateExecuting28 StateExecuting
27 StateComplete29 StateComplete
@@ -42,6 +44,7 @@ type Model struct {
42 width int44 width int
43 height int45 height int
44 err error46 err error
47+ cpuInfo *system.CPUInfo // Detected CPU information
45 }48 }
46 49
47 // NewModel creates a new installer model50 // NewModel creates a new installer model
@@ -116,10 +119,21 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
116 m.currentForm = CreateDiskSelectionForm(m.config, m.formBuilder)119 m.currentForm = CreateDiskSelectionForm(m.config, m.formBuilder)
117 return m, m.currentForm.Init()120 return m, m.currentForm.Init()
118 121
119- case StateConfirmation:122+ case StateAMDPStatePrompt:
120 m.state = StateOptionsForm123 m.state = StateOptionsForm
121 m.currentForm = CreateOptionsForm(m.config, m.formBuilder)124 m.currentForm = CreateOptionsForm(m.config, m.formBuilder)
122 return m, m.currentForm.Init()125 return m, m.currentForm.Init()
126+
127+ case StateConfirmation:
128+ // Go back to AMD P-State prompt if we have AMD CPU, otherwise options
129+ if m.cpuInfo != nil && m.cpuInfo.Vendor == system.CPUVendorAMD && len(m.cpuInfo.AMDPStateModes) > 0 {
130+ m.state = StateAMDPStatePrompt
131+ m.currentForm = CreateAMDPStateForm(m.config, m.cpuInfo, m.formBuilder)
132+ } else {
133+ m.state = StateOptionsForm
134+ m.currentForm = CreateOptionsForm(m.config, m.formBuilder)
135+ }
136+ return m, m.currentForm.Init()
123 }137 }
124 }138 }
125 139
@@ -161,7 +175,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
161 175
162 // Update current form if showing one176 // Update current form if showing one
163 switch m.state {177 switch m.state {
164- case StatePreflightForm, StateDiskForm, StateOptionsForm:178+ case StatePreflightForm, StateDiskForm, StateOptionsForm, StateAMDPStatePrompt:
165 switch {179 switch {
166 case m.currentForm != nil:180 case m.currentForm != nil:
167 // Don't pass WindowSizeMsg to form to preserve our max width181 // Don't pass WindowSizeMsg to form to preserve our max width
@@ -200,7 +214,7 @@ func (m *Model) View() string {
200 centerText(WelcomeTagline, m.width) + "\n\n" +214 centerText(WelcomeTagline, m.width) + "\n\n" +
201 centerText(styles.HelpStyle.Render("Press ENTER to start installation"), m.width)215 centerText(styles.HelpStyle.Render("Press ENTER to start installation"), m.width)
202 216
203- case StatePreflightForm, StateDiskForm, StateOptionsForm:217+ case StatePreflightForm, StateDiskForm, StateOptionsForm, StateAMDPStatePrompt:
204 switch {218 switch {
205 case m.currentForm != nil:219 case m.currentForm != nil:
206 header := centerText(m.renderPhaseHeader(), m.width)220 header := centerText(m.renderPhaseHeader(), m.width)
@@ -257,8 +271,28 @@ func (m *Model) handleFormComplete() tea.Cmd {
257 m.logger.Info("Options form completed",271 m.logger.Info("Options form completed",
258 "kernel", m.config.KernelChoice,272 "kernel", m.config.KernelChoice,
259 "encryption", m.config.EncryptionType)273 "encryption", m.config.EncryptionType)
260- // Move directly to confirmation274+
261- // Encryption password is already handled in preflight form275+ // Detect CPU and check if we need to show AMD P-State prompt
276+ cpuInfo, err := system.DetectCPUInfo()
277+ if err == nil {
278+ m.cpuInfo = cpuInfo
279+ // Show AMD P-State prompt only for AMD CPUs with P-State support
280+ if cpuInfo.Vendor == system.CPUVendorAMD && len(cpuInfo.AMDPStateModes) > 0 {
281+ m.logger.Info("AMD CPU detected - showing P-State configuration",
282+ "zen_gen", cpuInfo.AMDZenGen.Label,
283+ "modes", len(cpuInfo.AMDPStateModes))
284+ m.state = StateAMDPStatePrompt
285+ m.currentForm = CreateAMDPStateForm(m.config, cpuInfo, m.formBuilder)
286+ return m.currentForm.Init()
287+ }
288+ }
289+
290+ // Move directly to confirmation if not AMD or detection failed
291+ m.state = StateConfirmation
292+ return nil
293+
294+ case StateAMDPStatePrompt:
295+ m.logger.Info("AMD P-State configuration completed", "mode", m.config.AMDPState)
262 m.state = StateConfirmation296 m.state = StateConfirmation
263 return nil297 return nil
264 298
@@ -277,6 +311,12 @@ func (m *Model) renderConfirmation() string {
277 s += fmt.Sprintf("Disk: %s\n", m.config.TargetDisk)311 s += fmt.Sprintf("Disk: %s\n", m.config.TargetDisk)
278 s += fmt.Sprintf("Kernel: %s\n", m.config.KernelChoice)312 s += fmt.Sprintf("Kernel: %s\n", m.config.KernelChoice)
279 s += fmt.Sprintf("Encryption: %s\n", m.config.EncryptionType)313 s += fmt.Sprintf("Encryption: %s\n", m.config.EncryptionType)
314+
315+ // Show AMD P-State mode if configured
316+ if m.config.AMDPState != "" {
317+ s += fmt.Sprintf("AMD P-State: %s\n", m.config.AMDPState)
318+ }
319+
280 s += "\n"320 s += "\n"
281 s += styles.WarningStyle.Render("WARNING: This will erase all data on "+m.config.TargetDisk) + "\n\n"321 s += styles.WarningStyle.Render("WARNING: This will erase all data on "+m.config.TargetDisk) + "\n\n"
282 s += styles.HelpStyle.Render("Press ENTER to continue, Ctrl+C to cancel")322 s += styles.HelpStyle.Render("Press ENTER to continue, Ctrl+C to cancel")
@@ -330,10 +370,19 @@ func (m *Model) renderPhaseHeader() string {
330 case StateOptionsForm:370 case StateOptionsForm:
331 stepNum = "3"371 stepNum = "3"
332 stepName = "Installation Options"372 stepName = "Installation Options"
373+ case StateAMDPStatePrompt:
374+ stepNum = "4"
375+ stepName = "AMD P-State Configuration"
333 default:376 default:
334 return ""377 return ""
335 }378 }
336 379
337- header := fmt.Sprintf("ArchUp Install - Step %s/3: %s", stepNum, stepName)380+ // Total steps depends on whether AMD prompt is shown
381+ totalSteps := "3"
382+ if m.state == StateAMDPStatePrompt {
383+ totalSteps = "4"
384+ }
385+
386+ header := fmt.Sprintf("ArchUp Install - Step %s/%s: %s", stepNum, totalSteps, stepName)
338 return styles.TitleStyle.Render(header)387 return styles.TitleStyle.Render(header)
339 }388 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 0

    ↑↓ moveenter openesc close