settings_crypto.go

  1package tui
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	tea "charm.land/bubbletea/v2"
  8	"github.com/floatpane/matcha/config"
  9)
 10
 11const cryptoConfigMaxFocus = 9
 12
 13func (m *Settings) updateSMIMEConfig(msg tea.KeyPressMsg) (*Settings, tea.Cmd) {
 14	key := msg.Key()
 15	isEnter := key.Code == tea.KeyEnter || key.Code == tea.KeyReturn || key.Code == tea.KeyKpEnter
 16	isSpace := key.Code == tea.KeySpace
 17
 18	switch msg.String() {
 19	case "esc":
 20		m.isCryptoConfig = false
 21		return m, nil
 22	case "tab", keyShiftTab, "up", keyDown:
 23		return m, m.cryptoNavigate(msg.String())
 24	}
 25
 26	if isEnter {
 27		return m, m.cryptoHandleEnter()
 28	}
 29	if isSpace && m.cryptoToggle() {
 30		return m, nil
 31	}
 32
 33	// Forward any remaining key press (typing, backspace, paste, cursor) to the
 34	// focused text input.
 35	return m, m.cryptoForwardInput(msg)
 36}
 37
 38// cryptoSetFocus moves focus to index next, blurring every input and focusing
 39// the one (if any) bound to that row.
 40func (m *Settings) cryptoSetFocus(next int) tea.Cmd {
 41	m.cryptoFocusIndex = next
 42	m.smimeCertInput.Blur()
 43	m.smimeKeyInput.Blur()
 44	m.pgpPublicKeyInput.Blur()
 45	m.pgpPrivateKeyInput.Blur()
 46	m.pgpPINInput.Blur()
 47
 48	switch m.cryptoFocusIndex {
 49	case 0:
 50		return m.smimeCertInput.Focus()
 51	case 1:
 52		return m.smimeKeyInput.Focus()
 53	case 3:
 54		return m.pgpPublicKeyInput.Focus()
 55	case 4:
 56		return m.pgpPrivateKeyInput.Focus()
 57	case 6:
 58		return m.pgpPINInput.Focus()
 59	}
 60	return nil
 61}
 62
 63// cryptoNavigate handles tab/shift-tab/up/down, skipping the YubiKey PIN row
 64// (index 6) when the key source is not a YubiKey.
 65func (m *Settings) cryptoNavigate(s string) tea.Cmd {
 66	back := s == keyShiftTab || s == "up"
 67	if back {
 68		m.cryptoFocusIndex--
 69		if m.cryptoFocusIndex < 0 {
 70			m.cryptoFocusIndex = cryptoConfigMaxFocus
 71		}
 72	} else {
 73		m.cryptoFocusIndex++
 74		if m.cryptoFocusIndex > cryptoConfigMaxFocus {
 75			m.cryptoFocusIndex = 0
 76		}
 77	}
 78	if m.cryptoFocusIndex == 6 && m.pgpKeySource != keyYubikey {
 79		if back {
 80			m.cryptoFocusIndex = 5
 81		} else {
 82			m.cryptoFocusIndex = 7
 83		}
 84	}
 85	return m.cryptoSetFocus(m.cryptoFocusIndex)
 86}
 87
 88// cryptoHandleEnter saves (row 8), cancels (row 9), or advances focus.
 89func (m *Settings) cryptoHandleEnter() tea.Cmd {
 90	switch m.cryptoFocusIndex {
 91	case 8: // Save
 92		acct := &m.cfg.Accounts[m.editingAccountIdx]
 93		acct.SMIMECert = m.smimeCertInput.Value()
 94		acct.SMIMEKey = m.smimeKeyInput.Value()
 95		acct.PGPPublicKey = m.pgpPublicKeyInput.Value()
 96		acct.PGPPrivateKey = m.pgpPrivateKeyInput.Value()
 97		acct.PGPKeySource = m.pgpKeySource
 98		acct.PGPPIN = m.pgpPINInput.Value()
 99		_ = config.SaveConfig(m.cfg)
100		m.isCryptoConfig = false
101		return nil
102	case 9: // Cancel
103		m.isCryptoConfig = false
104		return nil
105	default:
106		next := m.cryptoFocusIndex + 1
107		if next == 6 && m.pgpKeySource != keyYubikey {
108			next = 7
109		}
110		return m.cryptoSetFocus(next)
111	}
112}
113
114// cryptoToggle flips the boolean/choice rows (Sign By Default, Key Source).
115// It reports whether the current row was a toggle.
116func (m *Settings) cryptoToggle() bool {
117	acct := &m.cfg.Accounts[m.editingAccountIdx]
118	switch m.cryptoFocusIndex {
119	case 2:
120		acct.SMIMESignByDefault = !acct.SMIMESignByDefault
121	case 5:
122		if m.pgpKeySource == "file" {
123			m.pgpKeySource = keyYubikey
124		} else {
125			m.pgpKeySource = "file"
126		}
127	case 7:
128		acct.PGPSignByDefault = !acct.PGPSignByDefault
129	default:
130		return false
131	}
132	return true
133}
134
135// cryptoForwardInput routes a key press to the focused text input. The
136// toggle/button rows (2, 5, 7, 8, 9) have no input and are no-ops.
137func (m *Settings) cryptoForwardInput(msg tea.KeyPressMsg) tea.Cmd {
138	var cmd tea.Cmd
139	switch m.cryptoFocusIndex {
140	case 0:
141		m.smimeCertInput, cmd = m.smimeCertInput.Update(msg)
142	case 1:
143		m.smimeKeyInput, cmd = m.smimeKeyInput.Update(msg)
144	case 3:
145		m.pgpPublicKeyInput, cmd = m.pgpPublicKeyInput.Update(msg)
146	case 4:
147		m.pgpPrivateKeyInput, cmd = m.pgpPrivateKeyInput.Update(msg)
148	case 6:
149		m.pgpPINInput, cmd = m.pgpPINInput.Update(msg)
150	}
151	return cmd
152}
153
154func (m *Settings) viewSMIMEConfig() string {
155	var b strings.Builder
156	account := m.cfg.Accounts[m.editingAccountIdx]
157	b.WriteString(titleStyle.Render(fmt.Sprintf("Crypto Config: %s", account.FetchEmail)) + "\n\n")
158
159	renderField := func(index int, label, content string) {
160		if m.cryptoFocusIndex == index {
161			b.WriteString(m.contentFocusStyle().Render(label) + "\n")
162		} else {
163			b.WriteString(settingsBlurredStyle.Render(label) + "\n")
164		}
165		b.WriteString(content + "\n\n")
166	}
167
168	// S/MIME
169	b.WriteString(settingsFocusedStyle.Render("S/MIME") + "\n")
170	renderField(0, "Certificate (PEM) Path:", m.smimeCertInput.View())
171	renderField(1, "Private Key (PEM) Path:", m.smimeKeyInput.View())
172	smimeSign := "OFF"
173	if account.SMIMESignByDefault {
174		smimeSign = "ON"
175	}
176	renderField(2, "Sign By Default:", smimeSign)
177
178	// PGP
179	b.WriteString(settingsFocusedStyle.Render("PGP") + "\n")
180	renderField(3, "Public Key Path:", m.pgpPublicKeyInput.View())
181	renderField(4, "Private Key Path:", m.pgpPrivateKeyInput.View())
182
183	keySource := "File"
184	if m.pgpKeySource == keyYubikey {
185		keySource = "YubiKey"
186	}
187	renderField(5, "Key Source:", keySource)
188
189	if m.pgpKeySource == keyYubikey {
190		renderField(6, "YubiKey PIN:", m.pgpPINInput.View())
191	}
192
193	pgpSign := "OFF"
194	if account.PGPSignByDefault {
195		pgpSign = "ON"
196	}
197	renderField(7, "Sign By Default:", pgpSign)
198
199	saveBtn := "[ Save ]"
200	cancelBtn := "[ Cancel ]"
201	if m.cryptoFocusIndex == 8 {
202		saveBtn = m.contentFocusStyle().Render(saveBtn)
203	} else {
204		saveBtn = settingsBlurredStyle.Render(saveBtn)
205	}
206	if m.cryptoFocusIndex == 9 {
207		cancelBtn = m.contentFocusStyle().Render(cancelBtn)
208	} else {
209		cancelBtn = settingsBlurredStyle.Render(cancelBtn)
210	}
211
212	b.WriteString(saveBtn + "  " + cancelBtn + "\n\n")
213	b.WriteString(helpStyle.Render("tab: next • enter: next/save • space: toggle • esc: cancel"))
214
215	return b.String()
216}