diff --git a/README.md b/README.md index 97defac..6c75cd6 100644 --- a/README.md +++ b/README.md @@ -55,16 +55,13 @@ The application is structured into four main components that communicate asynchr ## Setup & Configuration -Configuration is managed securely through JSON files and an interactive TUI setup wizard. +Configuration is managed locally through a JSON file and an interactive TUI setup wizard. ### Configuration Files -`gitpoll` uses a hierarchical JSON configuration system. You do not need to create these files manually; the application provides an interactive setup wizard that will generate them for you if they are missing. +`gitpoll` uses a local JSON configuration system. You do not need to create this file manually; the application provides an interactive, paginated setup wizard that will generate it for you if it is missing. -- **Global Configuration**: `~/.config/gitpoll/config.json` -- **Local Configuration**: `./.gitpoll.json` (Overrides global configuration) - -When the application starts, it will merge the local configuration over the global configuration. +- **Local Configuration**: `./gitpoll.config.json` ### Running the Application @@ -75,8 +72,9 @@ When the application starts, it will merge the local configuration over the glob ./gitpoll ``` -3. If this is your first time running the program or if the configuration is incomplete, an interactive **Setup Wizard** will launch. -4. Follow the on-screen prompts in the terminal to configure the repository URL, branch, local directory, polling interval, and execution command. The wizard will save your preferences to the appropriate configuration file. +3. If this is your first time running the program or if the configuration is incomplete, an interactive **Setup Wizard** will launch, displaying the project's ASCII art header. +4. Follow the paginated on-screen prompts in the terminal to configure the repository URL, local directory, branch, execution command, and polling interval. You can leave fields blank to use the suggested defaults shown in brackets. +5. Review the summary of your settings and confirm to save them to `./gitpoll.config.json`. ## License diff --git a/internal/config/config.go b/internal/config/config.go index f344e3a..d55cdfe 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -41,18 +41,9 @@ func UnmarshalString(data string, v any) error { return Unmarshal([]byte(data), v) } -// GetGlobalConfigPath returns the path to the global configuration file -func GetGlobalConfigPath() (string, error) { - homeDir, err := os.UserHomeDir() - if err != nil { - return "", err - } - return filepath.Join(homeDir, ".config", "gitpoll", "config.json"), nil -} - // GetLocalConfigPath returns the path to the local configuration file func GetLocalConfigPath() string { - return "./.gitpoll.json" + return "./gitpoll.config.json" } // Save writes the configuration to the specified path @@ -92,63 +83,25 @@ func loadFromFile(path string) (*Config, error) { return &cfg, nil } -// Merge overrides the fields of base with the non-empty fields of override -func Merge(base, override *Config) *Config { - if base == nil { - base = &Config{} - } - - merged := *base // Copy base - - if override == nil { - return &merged - } - - if override.RepoURL != "" { - merged.RepoURL = override.RepoURL - } - if override.RepoDir != "" { - merged.RepoDir = override.RepoDir - } - if override.Branch != "" { - merged.Branch = override.Branch - } - if override.Command != "" { - merged.Command = override.Command - } - if override.Interval != 0 { - merged.Interval = override.Interval - } - - return &merged -} - -// LoadConfig reads global and local configs, merges them, and checks if the config is fully valid +// LoadConfig reads local config and checks if the config is fully valid func LoadConfig() (*Config, bool, error) { - globalPath, err := GetGlobalConfigPath() - if err != nil { - return nil, false, fmt.Errorf("failed to get global config path: %w", err) - } localPath := GetLocalConfigPath() - globalCfg, err := loadFromFile(globalPath) + cfg, err := loadFromFile(localPath) if err != nil { - return nil, false, fmt.Errorf("failed to load global config: %w", err) + return nil, false, fmt.Errorf("failed to load config: %w", err) } - localCfg, err := loadFromFile(localPath) - if err != nil { - return nil, false, fmt.Errorf("failed to load local config: %w", err) + if cfg == nil { + return nil, false, nil } - mergedCfg := Merge(globalCfg, localCfg) - // Determine if config is completely valid - isValid := mergedCfg.RepoURL != "" && - mergedCfg.RepoDir != "" && - mergedCfg.Branch != "" && - mergedCfg.Command != "" && - mergedCfg.Interval > 0 + isValid := cfg.RepoURL != "" && + cfg.RepoDir != "" && + cfg.Branch != "" && + cfg.Command != "" && + cfg.Interval > 0 - return mergedCfg, isValid, nil + return cfg, isValid, nil } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 5b77f37..52a65dc 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -53,35 +53,6 @@ func TestMarshalUnmarshalString(t *testing.T) { } } -func TestMerge(t *testing.T) { - global := &Config{ - RepoURL: "global_url", - Branch: "global_branch", - Interval: 60 * time.Second, - } - - local := &Config{ - Branch: "local_branch", - Command: "local_command", - Interval: 30 * time.Second, - } - - merged := Merge(global, local) - - if merged.RepoURL != "global_url" { - t.Errorf("Expected global_url, got %s", merged.RepoURL) - } - if merged.Branch != "local_branch" { - t.Errorf("Expected local_branch, got %s", merged.Branch) - } - if merged.Command != "local_command" { - t.Errorf("Expected local_command, got %s", merged.Command) - } - if merged.Interval != 30*time.Second { - t.Errorf("Expected 30s, got %v", merged.Interval) - } -} - func TestLoadConfig_MissingFile(t *testing.T) { // Creating temp dir to override paths tempDir := t.TempDir() diff --git a/internal/tui/tui_wizard.go b/internal/tui/tui_wizard.go index d285f44..a3143b1 100644 --- a/internal/tui/tui_wizard.go +++ b/internal/tui/tui_wizard.go @@ -2,14 +2,24 @@ package tui import ( "fmt" + "os" "strconv" "time" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" "repo-gitpoll/internal/config" ) +const asciiArt = ` ____ _ _ ____ _ _ + / ___(_) |_| _ \ ___ | | | +| | _| | __| |_) / _ \| | | +| |_| | | |_| __/ (_) | | | + \____|_|\__|_| \___/|_|_|` + +var asciiArtStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63")).Bold(true) + type ConfigReadyMsg struct { Config *config.Config } @@ -20,39 +30,27 @@ type WizardModel struct { } func NewWizardModel(initialConfig *config.Config) *WizardModel { - if initialConfig == nil { - initialConfig = &config.Config{ - Branch: "main", - Command: "make", - Interval: 30 * time.Second, - } - } else { - if initialConfig.Branch == "" { - initialConfig.Branch = "main" - } - if initialConfig.Command == "" { - initialConfig.Command = "make" - } - if initialConfig.Interval == 0 { - initialConfig.Interval = 30 * time.Second - } + m := &WizardModel{ + initialConfig: initialConfig, } + m.createForm() + return m +} +func (m *WizardModel) createForm() { var repoURL, repoDir, branch, command, intervalStr string - var saveLocation string + var confirm bool - repoURL = initialConfig.RepoURL - repoDir = initialConfig.RepoDir - branch = initialConfig.Branch - command = initialConfig.Command - intervalStr = fmt.Sprintf("%d", int(initialConfig.Interval.Seconds())) - saveLocation = "local" // default + cwd, err := os.Getwd() + if err != nil { + cwd = "." + } - form := huh.NewForm( + m.form = huh.NewForm( huh.NewGroup( huh.NewInput(). Key("repoURL"). - Title("Git Repository URL"). + Title("Git Repository URL\n[example: https://github.com/starpia-forge/gitpoll.git]"). Value(&repoURL). Validate(func(str string) error { if str == "" { @@ -60,72 +58,74 @@ func NewWizardModel(initialConfig *config.Config) *WizardModel { } return nil }), - + ), + huh.NewGroup( huh.NewInput(). Key("repoDir"). - Title("Local Repository Directory"). - Value(&repoDir). - Validate(func(str string) error { - if str == "" { - return fmt.Errorf("local directory is required") - } - return nil - }), - + Title(fmt.Sprintf("Local Repository Directory\n[default: %s]", cwd)). + Value(&repoDir), + ), + huh.NewGroup( huh.NewInput(). Key("branch"). - Title("Branch to Monitor"). - Value(&branch). - Validate(func(str string) error { - if str == "" { - return fmt.Errorf("branch is required") - } - return nil - }), - + Title("Branch to Monitor\n[default: main]"). + Value(&branch), + ), + huh.NewGroup( huh.NewInput(). Key("command"). - Title("Command to Execute on Update"). - Value(&command). - Validate(func(str string) error { - if str == "" { - return fmt.Errorf("command is required") - } - return nil - }), - + Title("Command to Execute on Update\n[default: make]"). + Value(&command), + ), + huh.NewGroup( huh.NewInput(). Key("interval"). - Title("Polling Interval (seconds)"). + Title("Polling Interval (seconds)\n[default: 30]"). Value(&intervalStr). Validate(func(str string) error { - if str == "" { - return fmt.Errorf("interval is required") - } - if _, err := strconv.Atoi(str); err != nil { - return fmt.Errorf("interval must be an integer") + if str != "" { + if _, err := strconv.Atoi(str); err != nil { + return fmt.Errorf("interval must be an integer") + } } return nil }), ), huh.NewGroup( - huh.NewSelect[string](). - Key("saveLocation"). - Title("Where would you like to save this configuration?"). - Options( - huh.NewOption("Local (.gitpoll.json)", "local"), - huh.NewOption("Global (~/.config/gitpoll/config.json)", "global"), - ). - Value(&saveLocation), + huh.NewConfirm(). + Key("confirm"). + TitleFunc(func() string { + url := m.form.GetString("repoURL") + dir := m.form.GetString("repoDir") + if dir == "" { + dir = cwd + } + b := m.form.GetString("branch") + if b == "" { + b = "main" + } + c := m.form.GetString("command") + if c == "" { + c = "make" + } + iStr := m.form.GetString("interval") + if iStr == "" { + iStr = "30" + } + + return fmt.Sprintf("Summary of settings:\n\n"+ + "Repository URL: %s\n"+ + "Local Directory: %s\n"+ + "Branch: %s\n"+ + "Command: %s\n"+ + "Interval: %s seconds\n\n"+ + "Proceed with these settings?", url, dir, b, c, iStr) + }, &repoURL). + Value(&confirm), ), ) - form.Init() - - return &WizardModel{ - form: form, - initialConfig: initialConfig, - } + m.form.Init() } func (m *WizardModel) Init() tea.Cmd { @@ -147,13 +147,35 @@ func (m *WizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } if m.form.State == huh.StateCompleted { + confirm := m.form.GetBool("confirm") + if !confirm { + m.createForm() + return m, m.form.Init() + } + // Extract values repoURL := m.form.GetString("repoURL") repoDir := m.form.GetString("repoDir") branch := m.form.GetString("branch") command := m.form.GetString("command") intervalStr := m.form.GetString("interval") - saveLocation := m.form.GetString("saveLocation") + + if repoDir == "" { + cwd, err := os.Getwd() + if err != nil { + cwd = "." + } + repoDir = cwd + } + if branch == "" { + branch = "main" + } + if command == "" { + command = "make" + } + if intervalStr == "" { + intervalStr = "30" + } intervalSeconds, _ := strconv.Atoi(intervalStr) @@ -165,17 +187,7 @@ func (m *WizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { Interval: time.Duration(intervalSeconds) * time.Second, } - var savePath string - var err error - if saveLocation == "global" { - savePath, err = config.GetGlobalConfigPath() - if err != nil { - // We should ideally show error in UI, but for now we fallback to local or panic - savePath = config.GetLocalConfigPath() - } - } else { - savePath = config.GetLocalConfigPath() - } + savePath := config.GetLocalConfigPath() if err := config.Save(newConfig, savePath); err != nil { // If save fails, we proceed with the new config in memory anyway, @@ -193,5 +205,5 @@ func (m *WizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m *WizardModel) View() string { - return m.form.View() + return asciiArtStyle.Render(asciiArt) + "\n\n" + m.form.View() }