Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ type Config struct {
// keyed by plugin name then setting key. Values are JSON-native types
// (bool, float64, string) matching the plugin's declared schema.
PluginSettings map[string]map[string]interface{} `json:"plugin_settings,omitempty"`

HasSeenSetupGuide bool `json:"has_seen_setup_guide,omitempty"`
}

// GetSplitPaneOrientation returns the configured split pane orientation,
Expand Down Expand Up @@ -471,6 +473,7 @@ type secureDiskConfig struct {
DateFormat string `json:"date_format,omitempty"`
Language string `json:"language,omitempty"`
PluginSettings map[string]map[string]interface{} `json:"plugin_settings,omitempty"`
HasSeenSetupGuide bool `json:"has_seen_setup_guide,omitempty"`
}

// SaveConfig saves the given configuration to the config file and passwords to the keyring.
Expand Down Expand Up @@ -521,6 +524,7 @@ func SaveConfig(config *Config) error {
MailingLists: config.MailingLists,
DateFormat: config.DateFormat,
PluginSettings: config.PluginSettings,
HasSeenSetupGuide: config.HasSeenSetupGuide,
}
for _, acc := range config.Accounts {
sdc.Accounts = append(sdc.Accounts, secureDiskAccount{
Expand Down Expand Up @@ -632,6 +636,7 @@ func LoadConfig() (*Config, error) {
Language string `json:"language,omitempty"`
BodyCacheThresholdMB int `json:"body_cache_threshold_mb,omitempty"`
PluginSettings map[string]map[string]interface{} `json:"plugin_settings,omitempty"`
HasSeenSetupGuide bool `json:"has_seen_setup_guide,omitempty"`
}

var raw diskConfig
Expand Down Expand Up @@ -675,6 +680,7 @@ func LoadConfig() (*Config, error) {
config.Language = raw.Language
config.BodyCacheThresholdMB = raw.BodyCacheThresholdMB
config.PluginSettings = raw.PluginSettings
config.HasSeenSetupGuide = raw.HasSeenSetupGuide

for _, rawAcc := range raw.Accounts {
acc := Account{
Expand Down
33 changes: 33 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,39 @@ func TestSaveAndLoadConfig(t *testing.T) {
}
}

func TestHasSeenSetupGuidePersistence(t *testing.T) {
keyring.MockInit()
tempDir := t.TempDir()
t.Setenv("HOME", tempDir)

cfg := &Config{
Accounts: []Account{
{
ID: "test-id",
Name: "Test",
Email: "test@example.com",
Password: "secret",
ServiceProvider: "gmail",
SC: &SessionCache{},
},
},
HasSeenSetupGuide: true,
}

if err := SaveConfig(cfg); err != nil {
t.Fatalf("SaveConfig() failed: %v", err)
}

loaded, err := LoadConfig()
if err != nil {
t.Fatalf("LoadConfig() failed: %v", err)
}

if !loaded.HasSeenSetupGuide {
t.Error("HasSeenSetupGuide should be true after load, got false")
}
}

// TestAccountGetIMAPServer tests the logic that determines the IMAP server address.
func TestAccountGetIMAPServer(t *testing.T) {
testCases := []struct {
Expand Down
57 changes: 55 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ func newInitialModel(cfg *config.Config, mailtoURL *url.URL) *mainModel {
}
initialModel.current = tui.NewLogin(hideTips)
} else {
if mailtoURL != nil {
switch {
case mailtoURL != nil:
// mailto:addr@example.com?subject=test
to := mailtoURL.Opaque
if to == "" {
Expand All @@ -165,14 +166,55 @@ func newInitialModel(cfg *config.Config, mailtoURL *url.URL) *mainModel {
composer := tui.NewComposerWithAccounts(cfg.Accounts, cfg.Accounts[0].ID, to, subject, body, cfg.HideTips)
composer.SetSpellcheckOptions(cfg.DisableSpellcheck, cfg.DisableSpellSuggestions)
initialModel.current = composer
} else {
case !cfg.HasSeenSetupGuide:
initialModel.current = newSetupGuide()
default:
initialModel.current = tui.NewChoice()
}
initialModel.config = cfg
}
return initialModel
}

// newSetupGuide creates the first-run wizard with platform-appropriate callbacks.
func newSetupGuide() *tui.SetupGuide {
isMac := runtime.GOOS == goosDarwin
isLinux := runtime.GOOS == "linux"

var installHelper func() error
if isMac {
installHelper = func() error {
return silenced(func() error { return matchaCli.RunHelper([]string{"install"}) })
}
}

var setupMailto func() error
if isMac || isLinux {
setupMailto = func() error {
return silenced(matchaCli.SetupMailto)
}
}

return tui.NewSetupGuide(isMac, isLinux, installHelper, setupMailto)
}

// silenced runs fn with os.Stdout and os.Stderr redirected to /dev/null so
// that CLI functions which print progress don't corrupt the Bubble Tea TUI.
// Bubble Tea captures its output writer at program creation time, so swapping
// os.Stdout here only suppresses fmt.Printf / cmd.Stderr = os.Stderr calls.
func silenced(fn func() error) error {
devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
if err != nil {
return fn()
}
defer func() { _ = devNull.Close() }()
origOut, origErr := os.Stdout, os.Stderr
os.Stdout, os.Stderr = devNull, devNull
runErr := fn()
os.Stdout, os.Stderr = origOut, origErr
return runErr
}

// ensureProviders creates backend providers for all configured accounts.
// newSettings constructs a settings model and wires it to the plugin manager
// so the Plugins category can list and edit plugin-declared settings.
Expand Down Expand Up @@ -1410,6 +1452,17 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { //nolint:gocyclo
m.current, _ = m.current.Update(m.currentWindowSize())
return m, m.current.Init()

case tui.SetupGuideDoneMsg:
if m.config != nil && !m.config.HasSeenSetupGuide {
m.config.HasSeenSetupGuide = true
if err := config.SaveConfig(m.config); err != nil {
log.Printf("could not save setup-guide flag: %v", err)
}
}
m.current = tui.NewChoice()
m.current, _ = m.current.Update(m.currentWindowSize())
return m, m.current.Init()

case tui.DeleteAccountMsg:
if m.config != nil {
if m.config.RemoveAccount(msg.AccountID) {
Expand Down
1 change: 1 addition & 0 deletions tui/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package tui

const (
keyEnter = "enter"
keyCtrlC = "ctrl+c"
keyDown = "down"
keyLeft = "left"
keyRight = "right"
Expand Down
6 changes: 6 additions & 0 deletions tui/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -593,3 +593,9 @@ type RSVPResultMsg struct {
Response string // "ACCEPTED", "DECLINED", "TENTATIVE"
Organizer string // organizer email for Google Calendar note
}

// GoToSetupGuideMsg signals that the first-run setup guide should be shown.
type GoToSetupGuideMsg struct{}

// SetupGuideDoneMsg signals that the user completed (or dismissed) the setup guide.
type SetupGuideDoneMsg struct{}
2 changes: 1 addition & 1 deletion tui/password_prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func (m *PasswordPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
m.verifying = true
return m, verifyPasswordCmd(password)
case "ctrl+c":
case keyCtrlC:
return m, tea.Quit
}
// Clear error on new input
Expand Down
Loading
Loading