package ui import ( "strings" "github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "motd-assist/internal/motd" "motd-assist/internal/preset" ) // Update behandelt Fenstergröße, Ergebnisse der Hintergrundkommandos und // Tastendrücke. func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.width, m.height = msg.Width, msg.Height m.ready = true if w := m.width - 20; w > 10 { m.input.Width = w } if len(m.working) > 0 { return m, m.previewCmd() // Breite geändert → Preview neu rendern } return m, nil case spinner.TickMsg: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd case discoverMsg: if msg.err != nil { m.err = msg.err m.current, m.working = nil, nil return m, nil } m.err = nil m.current = msg.scripts m.host = msg.host m.working = rowsFromCurrent(msg.scripts) if m.cursor >= len(m.working) { m.cursor = max(0, len(m.working)-1) } m.refreshPresetMatches() m.previewOff = 0 return m, m.previewCmd() case presetsMsg: if msg.err == nil { m.presets = msg.presets if m.presetIdx >= len(m.presets) { m.presetIdx = max(0, len(m.presets)-1) } m.refreshPresetMatches() } return m, nil case previewMsg: if msg.version < m.previewVersion { return m, nil // veraltetes Ergebnis, neuere Preview läuft schon } m.previewBusy = false m.preview = msg.text m.previewOff = 0 return m, nil case applyDoneMsg: m.applying = false if msg.err != nil { m.status = "Anwenden fehlgeschlagen: " + msg.err.Error() } else { m.status = "Angewendet: " + m.lastSummary } return m, discoverCmd(m.root) case tea.KeyMsg: return m.handleKey(msg) } return m, nil } func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { switch m.mode { case modeHelp: m.mode = modeNormal return m, nil case modeSaveName: switch msg.String() { case "enter": name := strings.TrimSpace(m.input.Value()) m.mode = modeNormal m.input.Blur() if name == "" { return m, nil } if _, err := preset.SaveUser(m.presetFromWorking(name)); err != nil { m.status = "Speichern fehlgeschlagen: " + err.Error() } else { m.status = "Preset \"" + name + "\" gespeichert" } return m, presetsCmd() case "esc", "ctrl+c": m.mode = modeNormal m.input.Blur() return m, nil } var cmd tea.Cmd m.input, cmd = m.input.Update(msg) return m, cmd } key := msg.String() switch key { case "q", "ctrl+c": return m, tea.Quit case "tab": m.focus = (m.focus + 1) % 3 return m, nil case "shift+tab": m.focus = (m.focus + 2) % 3 case "?": m.mode = modeHelp return m, nil case "r": m.status = "" return m, discoverCmd(m.root) case "esc": if m.err != nil { return m, tea.Quit } m.working = rowsFromCurrent(m.current) m.previewOff = 0 m.status = "Änderungen verworfen" return m, m.previewCmd() case "s": if m.err != nil { return m, nil } m.mode = modeSaveName m.input.SetValue("") m.input.Focus() return m, textinput.Blink case "a": return m.applyWorking() case "D": if len(m.presets) == 0 { return m, nil } p := m.presets[m.presetIdx] if err := preset.DeleteUser(p.Name); err != nil { m.status = err.Error() } else { m.status = "Preset \"" + p.Name + "\" gelöscht" } return m, presetsCmd() } switch m.focus { case panePresets: switch key { case "j", "down": if m.presetIdx < len(m.presets)-1 { m.presetIdx++ } case "k", "up": if m.presetIdx > 0 { m.presetIdx-- } case "enter", " ": return m.loadPreset() } case paneScripts: switch key { case "j", "down": if m.cursor < len(m.working)-1 { m.cursor++ } case "k", "up": if m.cursor > 0 { m.cursor-- } case " ": if len(m.working) == 0 { return m, nil } r := &m.working[m.cursor] if r.Remove { return m, nil } r.Enabled = !r.Enabled return m, m.previewCmd() case "u": return m.moveRow(-1) case "d": return m.moveRow(1) } case panePreview: lines := strings.Count(m.preview, "\n") + 1 switch key { case "j", "down": m.previewOff = min(m.previewOff+1, max(0, lines-1)) case "k", "up": if m.previewOff > 0 { m.previewOff-- } } } return m, nil } func (m Model) loadPreset() (tea.Model, tea.Cmd) { if len(m.presets) == 0 { return m, nil } p := m.presets[m.presetIdx] entries := make([]motd.DesiredEntry, 0, len(p.Scripts)) for _, e := range p.Scripts { entries = append(entries, motd.DesiredEntry{Name: e.Name, Enabled: e.Enabled, Install: e.Install}) } m.working = rowsFromDesired(motd.DesiredFromPreset(m.current, entries), m.current) m.cursor = 0 m.previewOff = 0 m.status = "Preset \"" + p.Name + "\" geladen – mit 'a' anwenden" return m, m.previewCmd() } func (m Model) moveRow(delta int) (tea.Model, tea.Cmd) { i, j := m.cursor, m.cursor+delta if i < 0 || i >= len(m.working) || j < 0 || j >= len(m.working) { return m, nil } m.working[i], m.working[j] = m.working[j], m.working[i] m.cursor = j return m, m.previewCmd() } func (m Model) applyWorking() (tea.Model, tea.Cmd) { if m.applying || m.err != nil { return m, nil } plan := m.currentPlan() if plan.Empty() { m.status = "Keine Änderungen zum Anwenden" return m, nil } // Vorlagen-Inhalte jetzt auflösen (User-Templates liegen im eigenen // $HOME); der sudo-Prozess kann sie später nicht mehr lesen. if err := plan.ResolveContents(); err != nil { m.status = err.Error() return m, nil } m.applying = true m.lastSummary = plan.Summary() return m, applyExec(plan) }