From ec8d38a048fde54d61eae3f750d53085fd42273e Mon Sep 17 00:00:00 2001
From: Zeu Capua <coding@zeu.dev>
Date: Wed, 4 Sep 2024 14:28:06 -0700
Subject: [PATCH 01/18] init  command

---
 cmd/generate/config/config.go | 51 +++++++++++++++++++++++++++++++++++
 cmd/generate/generate.go      |  3 +++
 2 files changed, 54 insertions(+)
 create mode 100644 cmd/generate/config/config.go

diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go
new file mode 100644
index 0000000..cd70f54
--- /dev/null
+++ b/cmd/generate/config/config.go
@@ -0,0 +1,51 @@
+package config
+
+import (
+	"errors"
+	"github.com/spf13/cobra"
+
+	"github.com/open-sauced/pizza-cli/pkg/config"
+)
+
+// Options for the codeowners generation command
+type Options struct {
+	// the path to the git repository on disk to generate a codeowners file for
+	path string
+
+	tty      bool
+	loglevel int
+
+	config *config.Spec
+}
+
+const codeownersLongDesc string = `WARNING: Proof of concept feature.
+
+Generates a ~/.sauced.yaml configuration file. The attribution of emails to given entities
+is based on the repository this command is ran in.`
+
+func NewConfigCommand() *cobra.Command {
+	opts := &Options{}
+	print(opts.path);
+
+	cmd := &cobra.Command{
+		Use:   "config path/to/repo [flags]",
+		Short: "Generates a \"~/.sauced.yaml\" config based on the current repository",
+		Long:  codeownersLongDesc,
+		Args: func(_ *cobra.Command, args []string) error {
+			if len(args) != 1 {
+				return errors.New("you must provide exactly one argument: the path to the repository")
+			}
+
+			path := args[0]
+			print(path)
+
+			return nil 
+		},
+
+		RunE: func(cmd *cobra.Command, _ []string) error {
+			return nil
+		},
+	}
+
+	return cmd
+}
diff --git a/cmd/generate/generate.go b/cmd/generate/generate.go
index b3ff321..6d1a29f 100644
--- a/cmd/generate/generate.go
+++ b/cmd/generate/generate.go
@@ -6,6 +6,8 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/open-sauced/pizza-cli/cmd/generate/codeowners"
+
+	"github.com/open-sauced/pizza-cli/cmd/generate/config"
 )
 
 const generateLongDesc string = `WARNING: Proof of concept feature.
@@ -28,6 +30,7 @@ func NewGenerateCommand() *cobra.Command {
 	}
 
 	cmd.AddCommand(codeowners.NewCodeownersCommand())
+	cmd.AddCommand(config.NewConfigCommand())
 
 	return cmd
 }

From 104bf47830a7374e7d672c750bbd05ceafcbdcd0 Mon Sep 17 00:00:00 2001
From: Zeu Capua <coding@zeu.dev>
Date: Wed, 4 Sep 2024 16:45:19 -0700
Subject: [PATCH 02/18] print every commit and its author email and name

---
 cmd/generate/config/config.go | 50 ++++++++++++++++++++++++++++++-----
 1 file changed, 44 insertions(+), 6 deletions(-)

diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go
index cd70f54..a74f0c9 100644
--- a/cmd/generate/config/config.go
+++ b/cmd/generate/config/config.go
@@ -2,6 +2,12 @@ package config
 
 import (
 	"errors"
+	"fmt"
+	"os"
+	"path/filepath"
+
+	"github.com/go-git/go-git/v5"
+	"github.com/go-git/go-git/v5/plumbing/object"
 	"github.com/spf13/cobra"
 
 	"github.com/open-sauced/pizza-cli/pkg/config"
@@ -12,8 +18,9 @@ type Options struct {
 	// the path to the git repository on disk to generate a codeowners file for
 	path string
 
-	tty      bool
-	loglevel int
+	previousDays int
+	tty          bool
+	loglevel     int
 
 	config *config.Spec
 }
@@ -25,7 +32,6 @@ is based on the repository this command is ran in.`
 
 func NewConfigCommand() *cobra.Command {
 	opts := &Options{}
-	print(opts.path);
 
 	cmd := &cobra.Command{
 		Use:   "config path/to/repo [flags]",
@@ -37,15 +43,47 @@ func NewConfigCommand() *cobra.Command {
 			}
 
 			path := args[0]
-			print(path)
 
-			return nil 
+			// Validate that the path is a real path on disk and accessible by the user
+			absPath, err := filepath.Abs(path)
+			if err != nil {
+				return err
+			}
+
+			if _, err := os.Stat(absPath); os.IsNotExist(err) {
+				return fmt.Errorf("the provided path does not exist: %w", err)
+			}
+
+			opts.path = absPath
+			return nil
 		},
 
 		RunE: func(cmd *cobra.Command, _ []string) error {
-			return nil
+			// TODO: error checking based on given command
+
+			return run(opts, cmd)
 		},
 	}
 
 	return cmd
 }
+
+func run(opts *Options, cmd *cobra.Command) error {
+	configuration := &config.Spec{}
+	fmt.Println("CONFIG", configuration)
+
+	// Open repo
+	repo, err := git.PlainOpen(opts.path)
+	if err != nil {
+		return fmt.Errorf("error opening repo: %w", err)
+	}
+
+	commitIter, err := repo.CommitObjects()
+
+	commitIter.ForEach(func(c *object.Commit) error {
+		fmt.Println("COMMIT", c.Author.Email, c.Author.Name)
+		return nil
+	})
+
+	return nil
+}

From 1d5fdebd98d98bed3c6e71bbb4e747a68477e2c6 Mon Sep 17 00:00:00 2001
From: Zeu Capua <coding@zeu.dev>
Date: Wed, 4 Sep 2024 17:06:48 -0700
Subject: [PATCH 03/18] remove passed cmd to run, create attributionMap based
 on commits

---
 cmd/generate/config/config.go | 23 ++++++++++++++++++-----
 1 file changed, 18 insertions(+), 5 deletions(-)

diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go
index a74f0c9..fd71278 100644
--- a/cmd/generate/config/config.go
+++ b/cmd/generate/config/config.go
@@ -1,10 +1,12 @@
 package config
 
 import (
+	"encoding/json"
 	"errors"
 	"fmt"
 	"os"
 	"path/filepath"
+	"slices"
 
 	"github.com/go-git/go-git/v5"
 	"github.com/go-git/go-git/v5/plumbing/object"
@@ -61,16 +63,16 @@ func NewConfigCommand() *cobra.Command {
 		RunE: func(cmd *cobra.Command, _ []string) error {
 			// TODO: error checking based on given command
 
-			return run(opts, cmd)
+			return run(opts)
 		},
 	}
 
 	return cmd
 }
 
-func run(opts *Options, cmd *cobra.Command) error {
-	configuration := &config.Spec{}
-	fmt.Println("CONFIG", configuration)
+func run(opts *Options) error {
+	attributionMap := make(map[string][]string)
+	fmt.Println("CONFIG", attributionMap)
 
 	// Open repo
 	repo, err := git.PlainOpen(opts.path)
@@ -81,9 +83,20 @@ func run(opts *Options, cmd *cobra.Command) error {
 	commitIter, err := repo.CommitObjects()
 
 	commitIter.ForEach(func(c *object.Commit) error {
-		fmt.Println("COMMIT", c.Author.Email, c.Author.Name)
+		name := c.Author.Name
+		email := c.Author.Email
+
+		doesEmailExist := slices.Contains(attributionMap[name], email)
+		if !doesEmailExist {
+			attributionMap[name] = append(attributionMap[name], email)
+		}
+
 		return nil
 	})
 
+	// for pretty print test
+	test, err := json.MarshalIndent(attributionMap, "", " ")
+	fmt.Println(string(test))
+
 	return nil
 }

From 62c2793a284696538f584acec451ec83eed1bd3d Mon Sep 17 00:00:00 2001
From: Zeu Capua <coding@zeu.dev>
Date: Thu, 5 Sep 2024 10:51:44 -0700
Subject: [PATCH 04/18] remove ununsed Options props, create .sauced.yaml file

---
 cmd/generate/config/config.go | 21 ++++++++++-----------
 cmd/generate/config/output.go | 34 ++++++++++++++++++++++++++++++++++
 2 files changed, 44 insertions(+), 11 deletions(-)
 create mode 100644 cmd/generate/config/output.go

diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go
index fd71278..56b6d5e 100644
--- a/cmd/generate/config/config.go
+++ b/cmd/generate/config/config.go
@@ -1,7 +1,6 @@
 package config
 
 import (
-	"encoding/json"
 	"errors"
 	"fmt"
 	"os"
@@ -18,12 +17,7 @@ import (
 // Options for the codeowners generation command
 type Options struct {
 	// the path to the git repository on disk to generate a codeowners file for
-	path string
-
-	previousDays int
-	tty          bool
-	loglevel     int
-
+	path   string
 	config *config.Spec
 }
 
@@ -72,7 +66,6 @@ func NewConfigCommand() *cobra.Command {
 
 func run(opts *Options) error {
 	attributionMap := make(map[string][]string)
-	fmt.Println("CONFIG", attributionMap)
 
 	// Open repo
 	repo, err := git.PlainOpen(opts.path)
@@ -86,17 +79,23 @@ func run(opts *Options) error {
 		name := c.Author.Name
 		email := c.Author.Email
 
+		// TODO: edge case- same email multiple names
+		// eg: 'coding@zeu.dev' = 'zeudev' & 'Zeu Capua'
+
+		// AUTOMATIC: set every name and associated emails
 		doesEmailExist := slices.Contains(attributionMap[name], email)
 		if !doesEmailExist {
 			attributionMap[name] = append(attributionMap[name], email)
 		}
 
+		// TODO: INTERACTIVE: per unique email, set a name (existing or new)
+
 		return nil
 	})
 
-	// for pretty print test
-	test, err := json.MarshalIndent(attributionMap, "", " ")
-	fmt.Println(string(test))
+	// generate an output file
+	// default: `~/.sauced.yaml`
+	generateOutputFile(".sauced.yaml", attributionMap)
 
 	return nil
 }
diff --git a/cmd/generate/config/output.go b/cmd/generate/config/output.go
new file mode 100644
index 0000000..77e2ca5
--- /dev/null
+++ b/cmd/generate/config/output.go
@@ -0,0 +1,34 @@
+package config
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+
+	"github.com/open-sauced/pizza-cli/pkg/config"
+	"github.com/open-sauced/pizza-cli/pkg/utils"
+)
+
+func generateOutputFile(outputPath string, attributionMap map[string][]string) error {
+	// Open the file for writing
+	homeDir, err := os.UserHomeDir()
+	file, err := os.Create(filepath.Join(homeDir, outputPath))
+	if err != nil {
+		return fmt.Errorf("error creating %s file: %w", outputPath, err)
+	}
+	defer file.Close()
+
+	var config config.Spec
+	config.Attributions = attributionMap
+
+	// for pretty print test
+	yaml, err := utils.OutputYAML(config)
+
+	if err != nil {
+		return fmt.Errorf("Failed to turn into YAML")
+	}
+
+	file.WriteString(yaml)
+
+	return nil
+}

From 06e9bf1398ddc940dc16349b89f7492ba9b69c38 Mon Sep 17 00:00:00 2001
From: Zeu Capua <coding@zeu.dev>
Date: Thu, 5 Sep 2024 11:15:38 -0700
Subject: [PATCH 05/18] implement --output-path flag

---
 cmd/generate/config/config.go | 25 +++++++++++++++++--------
 cmd/generate/config/output.go |  5 +----
 2 files changed, 18 insertions(+), 12 deletions(-)

diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go
index 56b6d5e..dd549ca 100644
--- a/cmd/generate/config/config.go
+++ b/cmd/generate/config/config.go
@@ -10,18 +10,18 @@ import (
 	"github.com/go-git/go-git/v5"
 	"github.com/go-git/go-git/v5/plumbing/object"
 	"github.com/spf13/cobra"
-
-	"github.com/open-sauced/pizza-cli/pkg/config"
 )
 
-// Options for the codeowners generation command
+// Options for the config generation command
 type Options struct {
 	// the path to the git repository on disk to generate a codeowners file for
 	path   string
-	config *config.Spec
+
+	// where the '.sauced.yaml' file will go
+	outputPath string
 }
 
-const codeownersLongDesc string = `WARNING: Proof of concept feature.
+const configLongDesc string = `WARNING: Proof of concept feature.
 
 Generates a ~/.sauced.yaml configuration file. The attribution of emails to given entities
 is based on the repository this command is ran in.`
@@ -32,7 +32,7 @@ func NewConfigCommand() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "config path/to/repo [flags]",
 		Short: "Generates a \"~/.sauced.yaml\" config based on the current repository",
-		Long:  codeownersLongDesc,
+		Long:  configLongDesc,
 		Args: func(_ *cobra.Command, args []string) error {
 			if len(args) != 1 {
 				return errors.New("you must provide exactly one argument: the path to the repository")
@@ -57,10 +57,13 @@ func NewConfigCommand() *cobra.Command {
 		RunE: func(cmd *cobra.Command, _ []string) error {
 			// TODO: error checking based on given command
 
+			opts.outputPath, _ = cmd.Flags().GetString("output-path");
+
 			return run(opts)
 		},
 	}
 
+	cmd.PersistentFlags().StringP("output-path", "o", "~/", "Directory to create the `.sauced.yaml` file.")
 	return cmd
 }
 
@@ -94,8 +97,14 @@ func run(opts *Options) error {
 	})
 
 	// generate an output file
-	// default: `~/.sauced.yaml`
-	generateOutputFile(".sauced.yaml", attributionMap)
+	// default: `~/.sauced.yaml`	
+	if opts.outputPath == "~/" {
+		homeDir, _ := os.UserHomeDir()
+		generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap)
+	} else {
+		generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap)
+	}
+
 
 	return nil
 }
diff --git a/cmd/generate/config/output.go b/cmd/generate/config/output.go
index 77e2ca5..8cc4723 100644
--- a/cmd/generate/config/output.go
+++ b/cmd/generate/config/output.go
@@ -3,16 +3,13 @@ package config
 import (
 	"fmt"
 	"os"
-	"path/filepath"
 
 	"github.com/open-sauced/pizza-cli/pkg/config"
 	"github.com/open-sauced/pizza-cli/pkg/utils"
 )
 
 func generateOutputFile(outputPath string, attributionMap map[string][]string) error {
-	// Open the file for writing
-	homeDir, err := os.UserHomeDir()
-	file, err := os.Create(filepath.Join(homeDir, outputPath))
+	file, err := os.Create(outputPath)
 	if err != nil {
 		return fmt.Errorf("error creating %s file: %w", outputPath, err)
 	}

From 702ccefbbcc933bca9cca9ffd0e77faf3089cfa6 Mon Sep 17 00:00:00 2001
From: Zeu Capua <coding@zeu.dev>
Date: Fri, 6 Sep 2024 13:28:56 -0700
Subject: [PATCH 06/18] wip interactive mode

---
 cmd/generate/config/config.go | 132 ++++++++++++++++++++++++++++++----
 go.mod                        |   5 +-
 go.sum                        |   2 +
 3 files changed, 126 insertions(+), 13 deletions(-)

diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go
index dd549ca..6bc9eb6 100644
--- a/cmd/generate/config/config.go
+++ b/cmd/generate/config/config.go
@@ -9,16 +9,26 @@ import (
 
 	"github.com/go-git/go-git/v5"
 	"github.com/go-git/go-git/v5/plumbing/object"
+	"github.com/jpmcb/gopherlogs"
+	"github.com/open-sauced/pizza-cli/pkg/logging"
 	"github.com/spf13/cobra"
+
+	"github.com/charmbracelet/bubbles/help"
+	"github.com/charmbracelet/bubbles/key"
+	"github.com/charmbracelet/bubbles/textinput"
+	tea "github.com/charmbracelet/bubbletea"
 )
 
 // Options for the config generation command
 type Options struct {
 	// the path to the git repository on disk to generate a codeowners file for
-	path   string
+	path string
 
 	// where the '.sauced.yaml' file will go
 	outputPath string
+
+	// whether to use interactive mode
+	isInteractive bool
 }
 
 const configLongDesc string = `WARNING: Proof of concept feature.
@@ -57,17 +67,21 @@ func NewConfigCommand() *cobra.Command {
 		RunE: func(cmd *cobra.Command, _ []string) error {
 			// TODO: error checking based on given command
 
-			opts.outputPath, _ = cmd.Flags().GetString("output-path");
+			opts.outputPath, _ = cmd.Flags().GetString("output-path")
+			opts.isInteractive, _ = cmd.Flags().GetBool("interactive")
 
 			return run(opts)
 		},
 	}
 
 	cmd.PersistentFlags().StringP("output-path", "o", "~/", "Directory to create the `.sauced.yaml` file.")
+	cmd.PersistentFlags().BoolP("interactive", "i", true, "Whether to be interactive")
 	return cmd
 }
 
 func run(opts *Options) error {
+	logger, err := gopherlogs.NewLogger()
+
 	attributionMap := make(map[string][]string)
 
 	// Open repo
@@ -84,20 +98,30 @@ func run(opts *Options) error {
 
 		// TODO: edge case- same email multiple names
 		// eg: 'coding@zeu.dev' = 'zeudev' & 'Zeu Capua'
-
-		// AUTOMATIC: set every name and associated emails
-		doesEmailExist := slices.Contains(attributionMap[name], email)
-		if !doesEmailExist {
-			attributionMap[name] = append(attributionMap[name], email)
+		
+		if !opts.isInteractive {
+			doesEmailExist := slices.Contains(attributionMap[name], email)
+			if !doesEmailExist {
+				// AUTOMATIC: set every name and associated emails
+				attributionMap[name] = append(attributionMap[name], email)
+			}
+		} else {
+			// TODO: INTERACTIVE: per unique email, set a name (existing or new or ignore)
+			var uniqueEmails []string
+			if slices.Contains(uniqueEmails, email) {
+				uniqueEmails = append(uniqueEmails, email)
+			}
+			program := tea.NewProgram(initialModel(uniqueEmails))
+			if _, err := program.Run(); err != nil {
+				logger.V(logging.LogError).Info(err.Error())
+			}
 		}
-
-		// TODO: INTERACTIVE: per unique email, set a name (existing or new)
-
 		return nil
 	})
 
+
 	// generate an output file
-	// default: `~/.sauced.yaml`	
+	// default: `~/.sauced.yaml`
 	if opts.outputPath == "~/" {
 		homeDir, _ := os.UserHomeDir()
 		generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap)
@@ -105,6 +129,90 @@ func run(opts *Options) error {
 		generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap)
 	}
 
-
 	return nil
 }
+
+// Bubbletea for Interactive Mode
+
+type model struct {
+	textInput textinput.Model
+	help help.Model
+	keymap keymap
+
+	attributionMap map[string][]string
+	uniqueEmails []string
+	currentIndex int
+}
+
+type keymap struct{}
+
+func (k keymap) ShortHelp() []key.Binding {
+	return []key.Binding{
+		key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "complete")),
+		key.NewBinding(key.WithKeys("ctrl+n"), key.WithHelp("ctrl+n", "next")),
+		key.NewBinding(key.WithKeys("ctrl+p"), key.WithHelp("ctrl+p", "prev")),
+		key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "quit")),
+	}
+}
+
+func (k keymap) FullHelp() [][]key.Binding {
+	return [][]key.Binding{k.ShortHelp()}
+}
+
+func initialModel(uniqueEmails []string) model {
+	ti := textinput.New()
+	ti.Placeholder = "name"
+	ti.Focus()
+	ti.ShowSuggestions = true
+
+	return model{
+		textInput: ti,
+		help: help.New(),
+		keymap: keymap{},
+
+		attributionMap: make(map[string][]string),
+		uniqueEmails: uniqueEmails,
+		currentIndex: 0,
+	}
+}
+
+func (m model) Init() tea.Cmd {
+	return textinput.Blink
+}
+
+func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+	var cmd tea.Cmd
+	currentEmail := m.uniqueEmails[m.currentIndex]
+
+
+	existingUsers := make([]string, len(m.attributionMap))
+	for k := range m.attributionMap {
+		existingUsers = append(existingUsers, k)	
+	}
+
+	m.textInput.SetSuggestions(existingUsers)
+
+	switch msg := msg.(type) {
+		case tea.KeyMsg:
+			switch msg.Type {
+				case tea.KeyCtrlC, tea.KeyEsc:
+					return m, tea.Quit
+
+				case tea.KeyEnter: 
+					m.attributionMap[currentEmail] = append(m.attributionMap[currentEmail], m.textInput.Value())
+			}
+	}
+
+	m.textInput, cmd = m.textInput.Update(msg)
+
+	return m, cmd
+}
+
+func (m model) View() string {
+	return fmt.Sprintf(
+		"Found email %s - who to attribute to?: %s\n\n%s\n",
+		m.uniqueEmails[m.currentIndex],
+		m.textInput.View(), 
+		m.help.View(m.keymap),
+	)
+}
diff --git a/go.mod b/go.mod
index a28aa98..dd05559 100644
--- a/go.mod
+++ b/go.mod
@@ -16,7 +16,10 @@ require (
 	gopkg.in/yaml.v3 v3.0.1
 )
 
-require github.com/charmbracelet/bubbletea v0.27.1 // indirect
+require (
+	github.com/atotto/clipboard v0.1.4 // indirect
+	github.com/charmbracelet/bubbletea v0.27.1 // indirect
+)
 
 require (
 	dario.cat/mergo v1.0.0 // indirect
diff --git a/go.sum b/go.sum
index 3adc853..51da393 100644
--- a/go.sum
+++ b/go.sum
@@ -9,6 +9,8 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
+github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
 github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=

From 852cd6196abdf3503028b0f3fb69177f7e272fa1 Mon Sep 17 00:00:00 2001
From: Zeu Capua <coding@zeu.dev>
Date: Fri, 6 Sep 2024 14:23:40 -0700
Subject: [PATCH 07/18] go through each email with autocomplete

---
 cmd/generate/config/config.go | 68 +++++++++++++++++++----------------
 1 file changed, 37 insertions(+), 31 deletions(-)

diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go
index 6bc9eb6..fc9f713 100644
--- a/cmd/generate/config/config.go
+++ b/cmd/generate/config/config.go
@@ -9,8 +9,6 @@ import (
 
 	"github.com/go-git/go-git/v5"
 	"github.com/go-git/go-git/v5/plumbing/object"
-	"github.com/jpmcb/gopherlogs"
-	"github.com/open-sauced/pizza-cli/pkg/logging"
 	"github.com/spf13/cobra"
 
 	"github.com/charmbracelet/bubbles/help"
@@ -80,8 +78,6 @@ func NewConfigCommand() *cobra.Command {
 }
 
 func run(opts *Options) error {
-	logger, err := gopherlogs.NewLogger()
-
 	attributionMap := make(map[string][]string)
 
 	// Open repo
@@ -92,13 +88,14 @@ func run(opts *Options) error {
 
 	commitIter, err := repo.CommitObjects()
 
+	var uniqueEmails []string
 	commitIter.ForEach(func(c *object.Commit) error {
 		name := c.Author.Name
 		email := c.Author.Email
 
 		// TODO: edge case- same email multiple names
 		// eg: 'coding@zeu.dev' = 'zeudev' & 'Zeu Capua'
-		
+
 		if !opts.isInteractive {
 			doesEmailExist := slices.Contains(attributionMap[name], email)
 			if !doesEmailExist {
@@ -106,19 +103,18 @@ func run(opts *Options) error {
 				attributionMap[name] = append(attributionMap[name], email)
 			}
 		} else {
-			// TODO: INTERACTIVE: per unique email, set a name (existing or new or ignore)
-			var uniqueEmails []string
-			if slices.Contains(uniqueEmails, email) {
+			if !slices.Contains(uniqueEmails, email) {
 				uniqueEmails = append(uniqueEmails, email)
 			}
-			program := tea.NewProgram(initialModel(uniqueEmails))
-			if _, err := program.Run(); err != nil {
-				logger.V(logging.LogError).Info(err.Error())
-			}
 		}
 		return nil
 	})
 
+	// TODO: INTERACTIVE: per unique email, set a name (existing or new or ignore)
+	program := tea.NewProgram(initialModel(uniqueEmails))
+	if _, err := program.Run(); err != nil {
+		return fmt.Errorf(err.Error())
+	}
 
 	// generate an output file
 	// default: `~/.sauced.yaml`
@@ -136,12 +132,12 @@ func run(opts *Options) error {
 
 type model struct {
 	textInput textinput.Model
-	help help.Model
-	keymap keymap
+	help      help.Model
+	keymap    keymap
 
 	attributionMap map[string][]string
-	uniqueEmails []string
-	currentIndex int
+	uniqueEmails   []string
+	currentIndex   int
 }
 
 type keymap struct{}
@@ -151,7 +147,9 @@ func (k keymap) ShortHelp() []key.Binding {
 		key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "complete")),
 		key.NewBinding(key.WithKeys("ctrl+n"), key.WithHelp("ctrl+n", "next")),
 		key.NewBinding(key.WithKeys("ctrl+p"), key.WithHelp("ctrl+p", "prev")),
+		key.NewBinding(key.WithKeys("ctrl+i"), key.WithHelp("ctrl+i", "ignore email")),
 		key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "quit")),
+		key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit")),
 	}
 }
 
@@ -167,12 +165,12 @@ func initialModel(uniqueEmails []string) model {
 
 	return model{
 		textInput: ti,
-		help: help.New(),
-		keymap: keymap{},
+		help:      help.New(),
+		keymap:    keymap{},
 
 		attributionMap: make(map[string][]string),
-		uniqueEmails: uniqueEmails,
-		currentIndex: 0,
+		uniqueEmails:   uniqueEmails,
+		currentIndex:   0,
 	}
 }
 
@@ -184,23 +182,31 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	var cmd tea.Cmd
 	currentEmail := m.uniqueEmails[m.currentIndex]
 
-
-	existingUsers := make([]string, len(m.attributionMap))
+	existingUsers := make([]string, 0, len(m.attributionMap))
 	for k := range m.attributionMap {
-		existingUsers = append(existingUsers, k)	
+		existingUsers = append(existingUsers, k)
 	}
 
 	m.textInput.SetSuggestions(existingUsers)
 
 	switch msg := msg.(type) {
-		case tea.KeyMsg:
-			switch msg.Type {
-				case tea.KeyCtrlC, tea.KeyEsc:
-					return m, tea.Quit
-
-				case tea.KeyEnter: 
-					m.attributionMap[currentEmail] = append(m.attributionMap[currentEmail], m.textInput.Value())
+	case tea.KeyMsg:
+		switch msg.Type {
+		case tea.KeyCtrlC, tea.KeyEsc:
+			return m, tea.Quit
+
+		case tea.KeyCtrlI:
+			m.currentIndex++
+			return m, nil
+
+		case tea.KeyEnter:
+			m.attributionMap[m.textInput.Value()] = append(m.attributionMap[currentEmail], currentEmail)
+			m.currentIndex++
+			if m.currentIndex > len(m.attributionMap) {
+				return m, tea.Quit
 			}
+			return m, nil
+		}
 	}
 
 	m.textInput, cmd = m.textInput.Update(msg)
@@ -212,7 +218,7 @@ func (m model) View() string {
 	return fmt.Sprintf(
 		"Found email %s - who to attribute to?: %s\n\n%s\n",
 		m.uniqueEmails[m.currentIndex],
-		m.textInput.View(), 
+		m.textInput.View(),
 		m.help.View(m.keymap),
 	)
 }

From 6908e1626ab7adac8102ce4483f8f4f6d1a5511c Mon Sep 17 00:00:00 2001
From: Zeu Capua <coding@zeu.dev>
Date: Mon, 9 Sep 2024 03:07:29 -0700
Subject: [PATCH 08/18] tidy go.mod

---
 go.mod | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/go.mod b/go.mod
index 603715d..757f5ea 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.22
 
 require (
 	github.com/charmbracelet/bubbles v0.19.0
+	github.com/charmbracelet/bubbletea v0.27.1
 	github.com/charmbracelet/lipgloss v0.13.0
 	github.com/cli/browser v1.3.0
 	github.com/go-git/go-git/v5 v5.12.0
@@ -17,8 +18,7 @@ require (
 )
 
 require (
-	github.com/charmbracelet/bubbletea v0.27.1 // indirect
-	github.com/charmbracelet/bubbletea v0.27.1 // indirect
+	github.com/atotto/clipboard v0.1.4 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 

From 545974ee2e4fbc8b9c2c3effdf3299d940c6ea2e Mon Sep 17 00:00:00 2001
From: Zeu Capua <coding@zeu.dev>
Date: Mon, 9 Sep 2024 03:48:49 -0700
Subject: [PATCH 09/18] make interactive mode non default, working interactive
 mode

---
 cmd/generate/config/config.go | 71 ++++++++++++++++++++++++-----------
 1 file changed, 50 insertions(+), 21 deletions(-)

diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go
index fc9f713..5b8b5e5 100644
--- a/cmd/generate/config/config.go
+++ b/cmd/generate/config/config.go
@@ -6,6 +6,7 @@ import (
 	"os"
 	"path/filepath"
 	"slices"
+	"strings"
 
 	"github.com/go-git/go-git/v5"
 	"github.com/go-git/go-git/v5/plumbing/object"
@@ -72,8 +73,8 @@ func NewConfigCommand() *cobra.Command {
 		},
 	}
 
-	cmd.PersistentFlags().StringP("output-path", "o", "~/", "Directory to create the `.sauced.yaml` file.")
-	cmd.PersistentFlags().BoolP("interactive", "i", true, "Whether to be interactive")
+	cmd.PersistentFlags().StringP("output-path", "o", "./", "Directory to create the `.sauced.yaml` file.")
+	cmd.PersistentFlags().BoolP("interactive", "i", false, "Whether to be interactive")
 	return cmd
 }
 
@@ -111,18 +112,18 @@ func run(opts *Options) error {
 	})
 
 	// TODO: INTERACTIVE: per unique email, set a name (existing or new or ignore)
-	program := tea.NewProgram(initialModel(uniqueEmails))
-	if _, err := program.Run(); err != nil {
-		return fmt.Errorf(err.Error())
-	}
-
-	// generate an output file
-	// default: `~/.sauced.yaml`
-	if opts.outputPath == "~/" {
-		homeDir, _ := os.UserHomeDir()
-		generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap)
+	if opts.isInteractive {
+		program := tea.NewProgram(initialModel(opts, uniqueEmails))
+		if _, err := program.Run(); err != nil {
+			return fmt.Errorf(err.Error())
+		}
 	} else {
-		generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap)
+		if opts.outputPath == "~/" {
+			homeDir, _ := os.UserHomeDir()
+			generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap)
+		} else {
+			generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap)
+		}
 	}
 
 	return nil
@@ -135,6 +136,7 @@ type model struct {
 	help      help.Model
 	keymap    keymap
 
+	opts           *Options
 	attributionMap map[string][]string
 	uniqueEmails   []string
 	currentIndex   int
@@ -144,7 +146,6 @@ type keymap struct{}
 
 func (k keymap) ShortHelp() []key.Binding {
 	return []key.Binding{
-		key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "complete")),
 		key.NewBinding(key.WithKeys("ctrl+n"), key.WithHelp("ctrl+n", "next")),
 		key.NewBinding(key.WithKeys("ctrl+p"), key.WithHelp("ctrl+p", "prev")),
 		key.NewBinding(key.WithKeys("ctrl+i"), key.WithHelp("ctrl+i", "ignore email")),
@@ -157,7 +158,7 @@ func (k keymap) FullHelp() [][]key.Binding {
 	return [][]key.Binding{k.ShortHelp()}
 }
 
-func initialModel(uniqueEmails []string) model {
+func initialModel(opts *Options, uniqueEmails []string) model {
 	ti := textinput.New()
 	ti.Placeholder = "name"
 	ti.Focus()
@@ -168,6 +169,7 @@ func initialModel(uniqueEmails []string) model {
 		help:      help.New(),
 		keymap:    keymap{},
 
+		opts:           opts,
 		attributionMap: make(map[string][]string),
 		uniqueEmails:   uniqueEmails,
 		currentIndex:   0,
@@ -200,11 +202,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			return m, nil
 
 		case tea.KeyEnter:
-			m.attributionMap[m.textInput.Value()] = append(m.attributionMap[currentEmail], currentEmail)
-			m.currentIndex++
-			if m.currentIndex > len(m.attributionMap) {
-				return m, tea.Quit
+			if len(strings.Trim(m.textInput.Value(), " ")) == 0 {
+				return m, nil
+			}
+			m.attributionMap[m.textInput.Value()] = append(m.attributionMap[m.textInput.Value()], currentEmail)
+			m.textInput.Reset()
+			if m.currentIndex+1 >= len(m.uniqueEmails) {
+				return m, runOutputGeneration(m.opts, m.attributionMap)
 			}
+
+			m.currentIndex++
 			return m, nil
 		}
 	}
@@ -215,10 +222,32 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 }
 
 func (m model) View() string {
+	currentEmail := ""
+	if m.currentIndex < len(m.uniqueEmails) {
+		currentEmail = m.uniqueEmails[m.currentIndex]
+	}
+
 	return fmt.Sprintf(
-		"Found email %s - who to attribute to?: %s\n\n%s\n",
-		m.uniqueEmails[m.currentIndex],
+		"Found email %s - who to attribute to?: \n%s\n\n%s\n",
+		currentEmail,
 		m.textInput.View(),
 		m.help.View(m.keymap),
 	)
 }
+
+func runOutputGeneration(opts *Options, attributionMap map[string][]string) tea.Cmd {
+	// generate an output file
+	// default: `./.sauced.yaml`
+	// fallback for home directories
+	return func() tea.Msg {
+		if opts.outputPath == "~/" {
+			homeDir, _ := os.UserHomeDir()
+			generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap)
+		} else {
+			generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap)
+		}
+
+		return tea.Quit()
+	}
+
+}

From 583c49a1d2db5ca1c2631d9648280be7b64de455 Mon Sep 17 00:00:00 2001
From: Zeu Capua <coding@zeu.dev>
Date: Mon, 9 Sep 2024 10:31:17 -0700
Subject: [PATCH 10/18] add more error handling

---
 cmd/generate/config/config.go | 42 ++++++++++++++++++++++++++---------
 cmd/generate/config/output.go | 10 ++++++---
 2 files changed, 38 insertions(+), 14 deletions(-)

diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go
index 5b8b5e5..8fbab34 100644
--- a/cmd/generate/config/config.go
+++ b/cmd/generate/config/config.go
@@ -84,19 +84,20 @@ func run(opts *Options) error {
 	// Open repo
 	repo, err := git.PlainOpen(opts.path)
 	if err != nil {
-		return fmt.Errorf("error opening repo: %w", err)
+		return fmt.Errorf("Error opening repo: %w", err)
 	}
 
 	commitIter, err := repo.CommitObjects()
 
+	if err != nil {
+		return fmt.Errorf("Error opening repo commits: %w", err)
+	}
+
 	var uniqueEmails []string
-	commitIter.ForEach(func(c *object.Commit) error {
+	err = commitIter.ForEach(func(c *object.Commit) error {
 		name := c.Author.Name
 		email := c.Author.Email
 
-		// TODO: edge case- same email multiple names
-		// eg: 'coding@zeu.dev' = 'zeudev' & 'Zeu Capua'
-
 		if !opts.isInteractive {
 			doesEmailExist := slices.Contains(attributionMap[name], email)
 			if !doesEmailExist {
@@ -111,18 +112,31 @@ func run(opts *Options) error {
 		return nil
 	})
 
-	// TODO: INTERACTIVE: per unique email, set a name (existing or new or ignore)
+	if err != nil {
+		return fmt.Errorf("Error iterating over repo commits: %w", err)
+	}
+
+	// INTERACTIVE: per unique email, set a name (existing or new or ignore)
 	if opts.isInteractive {
 		program := tea.NewProgram(initialModel(opts, uniqueEmails))
 		if _, err := program.Run(); err != nil {
-			return fmt.Errorf(err.Error())
+			return fmt.Errorf("Error running interactive mode: %w", err)
 		}
 	} else {
+		// generate an output file
+		// default: `./.sauced.yaml`
+		// fallback for home directories
 		if opts.outputPath == "~/" {
 			homeDir, _ := os.UserHomeDir()
-			generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap)
+			err := generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap)
+			if err != nil {
+				return fmt.Errorf("Error generating output file: %w", err)
+			}
 		} else {
-			generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap)
+			err := generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap)
+			if err != nil {
+				return fmt.Errorf("Error generating output file: %w", err)
+			}
 		}
 	}
 
@@ -242,9 +256,15 @@ func runOutputGeneration(opts *Options, attributionMap map[string][]string) tea.
 	return func() tea.Msg {
 		if opts.outputPath == "~/" {
 			homeDir, _ := os.UserHomeDir()
-			generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap)
+			err := generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap)
+			if err != nil {
+				return fmt.Errorf("Error generating output file: %w", err)
+			}
 		} else {
-			generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap)
+			err := generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap)
+			if err != nil {
+				return fmt.Errorf("Error generating output file: %w", err)
+			}
 		}
 
 		return tea.Quit()
diff --git a/cmd/generate/config/output.go b/cmd/generate/config/output.go
index 8cc4723..f5a06aa 100644
--- a/cmd/generate/config/output.go
+++ b/cmd/generate/config/output.go
@@ -11,7 +11,7 @@ import (
 func generateOutputFile(outputPath string, attributionMap map[string][]string) error {
 	file, err := os.Create(outputPath)
 	if err != nil {
-		return fmt.Errorf("error creating %s file: %w", outputPath, err)
+		return fmt.Errorf("Error creating %s file: %w", outputPath, err)
 	}
 	defer file.Close()
 
@@ -22,10 +22,14 @@ func generateOutputFile(outputPath string, attributionMap map[string][]string) e
 	yaml, err := utils.OutputYAML(config)
 
 	if err != nil {
-		return fmt.Errorf("Failed to turn into YAML")
+		return fmt.Errorf("Failed to turn into YAML: %w", err)
 	}
 
-	file.WriteString(yaml)
+	_, err = file.WriteString(yaml)
+
+	if err != nil {
+		return fmt.Errorf("Failed to turn into YAML: %w", err)
+	}
 
 	return nil
 }

From 613ced5710d145b91dae6bc7b2496cc6d2248aea Mon Sep 17 00:00:00 2001
From: Zeu Capua <coding@zeu.dev>
Date: Mon, 9 Sep 2024 11:10:38 -0700
Subject: [PATCH 11/18] change else to else if, replace one case switch to if
 statement

---
 cmd/generate/config/config.go | 13 ++++++-------
 1 file changed, 6 insertions(+), 7 deletions(-)

diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go
index 8fbab34..17d2431 100644
--- a/cmd/generate/config/config.go
+++ b/cmd/generate/config/config.go
@@ -104,10 +104,8 @@ func run(opts *Options) error {
 				// AUTOMATIC: set every name and associated emails
 				attributionMap[name] = append(attributionMap[name], email)
 			}
-		} else {
-			if !slices.Contains(uniqueEmails, email) {
-				uniqueEmails = append(uniqueEmails, email)
-			}
+		} else if !slices.Contains(uniqueEmails, email) {
+			uniqueEmails = append(uniqueEmails, email)
 		}
 		return nil
 	})
@@ -205,9 +203,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 	m.textInput.SetSuggestions(existingUsers)
 
-	switch msg := msg.(type) {
-	case tea.KeyMsg:
-		switch msg.Type {
+	keyMsg, ok := msg.(tea.KeyMsg)
+
+	if ok {
+		switch keyMsg.Type {
 		case tea.KeyCtrlC, tea.KeyEsc:
 			return m, tea.Quit
 

From 33c205787394de76db7992345b66194040e72057 Mon Sep 17 00:00:00 2001
From: Zeu Capua <coding@zeu.dev>
Date: Mon, 9 Sep 2024 11:41:34 -0700
Subject: [PATCH 12/18] lint via golangci-liint --fix

---
 cmd/generate/config/config.go | 7 +++----
 cmd/generate/generate.go      | 1 -
 2 files changed, 3 insertions(+), 5 deletions(-)

diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go
index 17d2431..4742544 100644
--- a/cmd/generate/config/config.go
+++ b/cmd/generate/config/config.go
@@ -8,14 +8,13 @@ import (
 	"slices"
 	"strings"
 
-	"github.com/go-git/go-git/v5"
-	"github.com/go-git/go-git/v5/plumbing/object"
-	"github.com/spf13/cobra"
-
 	"github.com/charmbracelet/bubbles/help"
 	"github.com/charmbracelet/bubbles/key"
 	"github.com/charmbracelet/bubbles/textinput"
 	tea "github.com/charmbracelet/bubbletea"
+	"github.com/go-git/go-git/v5"
+	"github.com/go-git/go-git/v5/plumbing/object"
+	"github.com/spf13/cobra"
 )
 
 // Options for the config generation command
diff --git a/cmd/generate/generate.go b/cmd/generate/generate.go
index 6d1a29f..fee0146 100644
--- a/cmd/generate/generate.go
+++ b/cmd/generate/generate.go
@@ -6,7 +6,6 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/open-sauced/pizza-cli/cmd/generate/codeowners"
-
 	"github.com/open-sauced/pizza-cli/cmd/generate/config"
 )
 

From 5e7e4bcba1f1c933556240035df91f213944e3b4 Mon Sep 17 00:00:00 2001
From: Zeu Capua <coding@zeu.dev>
Date: Mon, 9 Sep 2024 17:31:09 -0700
Subject: [PATCH 13/18] lowercase error messages

---
 cmd/generate/config/config.go | 16 ++++++++--------
 cmd/generate/config/output.go |  6 +++---
 2 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go
index 4742544..c543b2a 100644
--- a/cmd/generate/config/config.go
+++ b/cmd/generate/config/config.go
@@ -83,13 +83,13 @@ func run(opts *Options) error {
 	// Open repo
 	repo, err := git.PlainOpen(opts.path)
 	if err != nil {
-		return fmt.Errorf("Error opening repo: %w", err)
+		return fmt.Errorf("error opening repo: %w", err)
 	}
 
 	commitIter, err := repo.CommitObjects()
 
 	if err != nil {
-		return fmt.Errorf("Error opening repo commits: %w", err)
+		return fmt.Errorf("error opening repo commits: %w", err)
 	}
 
 	var uniqueEmails []string
@@ -110,14 +110,14 @@ func run(opts *Options) error {
 	})
 
 	if err != nil {
-		return fmt.Errorf("Error iterating over repo commits: %w", err)
+		return fmt.Errorf("error iterating over repo commits: %w", err)
 	}
 
 	// INTERACTIVE: per unique email, set a name (existing or new or ignore)
 	if opts.isInteractive {
 		program := tea.NewProgram(initialModel(opts, uniqueEmails))
 		if _, err := program.Run(); err != nil {
-			return fmt.Errorf("Error running interactive mode: %w", err)
+			return fmt.Errorf("error running interactive mode: %w", err)
 		}
 	} else {
 		// generate an output file
@@ -127,12 +127,12 @@ func run(opts *Options) error {
 			homeDir, _ := os.UserHomeDir()
 			err := generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap)
 			if err != nil {
-				return fmt.Errorf("Error generating output file: %w", err)
+				return fmt.Errorf("error generating output file: %w", err)
 			}
 		} else {
 			err := generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap)
 			if err != nil {
-				return fmt.Errorf("Error generating output file: %w", err)
+				return fmt.Errorf("error generating output file: %w", err)
 			}
 		}
 	}
@@ -256,12 +256,12 @@ func runOutputGeneration(opts *Options, attributionMap map[string][]string) tea.
 			homeDir, _ := os.UserHomeDir()
 			err := generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap)
 			if err != nil {
-				return fmt.Errorf("Error generating output file: %w", err)
+				return fmt.Errorf("error generating output file: %w", err)
 			}
 		} else {
 			err := generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap)
 			if err != nil {
-				return fmt.Errorf("Error generating output file: %w", err)
+				return fmt.Errorf("error generating output file: %w", err)
 			}
 		}
 
diff --git a/cmd/generate/config/output.go b/cmd/generate/config/output.go
index f5a06aa..f8db065 100644
--- a/cmd/generate/config/output.go
+++ b/cmd/generate/config/output.go
@@ -11,7 +11,7 @@ import (
 func generateOutputFile(outputPath string, attributionMap map[string][]string) error {
 	file, err := os.Create(outputPath)
 	if err != nil {
-		return fmt.Errorf("Error creating %s file: %w", outputPath, err)
+		return fmt.Errorf("error creating %s file: %w", outputPath, err)
 	}
 	defer file.Close()
 
@@ -22,13 +22,13 @@ func generateOutputFile(outputPath string, attributionMap map[string][]string) e
 	yaml, err := utils.OutputYAML(config)
 
 	if err != nil {
-		return fmt.Errorf("Failed to turn into YAML: %w", err)
+		return fmt.Errorf("failed to turn into YAML: %w", err)
 	}
 
 	_, err = file.WriteString(yaml)
 
 	if err != nil {
-		return fmt.Errorf("Failed to turn into YAML: %w", err)
+		return fmt.Errorf("failed to turn into YAML: %w", err)
 	}
 
 	return nil

From 38dda02d412ad9114985c416fdff08cd78390863 Mon Sep 17 00:00:00 2001
From: Zeu Capua <coding@zeu.dev>
Date: Mon, 9 Sep 2024 17:35:46 -0700
Subject: [PATCH 14/18] remove todo and empty lines

---
 cmd/generate/config/config.go | 3 ---
 go.mod                        | 1 -
 2 files changed, 4 deletions(-)

diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go
index c543b2a..22d0156 100644
--- a/cmd/generate/config/config.go
+++ b/cmd/generate/config/config.go
@@ -63,8 +63,6 @@ func NewConfigCommand() *cobra.Command {
 		},
 
 		RunE: func(cmd *cobra.Command, _ []string) error {
-			// TODO: error checking based on given command
-
 			opts.outputPath, _ = cmd.Flags().GetString("output-path")
 			opts.isInteractive, _ = cmd.Flags().GetBool("interactive")
 
@@ -267,5 +265,4 @@ func runOutputGeneration(opts *Options, attributionMap map[string][]string) tea.
 
 		return tea.Quit()
 	}
-
 }
diff --git a/go.mod b/go.mod
index 757f5ea..cbf822d 100644
--- a/go.mod
+++ b/go.mod
@@ -21,7 +21,6 @@ require (
 	github.com/atotto/clipboard v0.1.4 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
-
 )
 
 require (

From 75eccb8d10e28d155bd2b2fd4f6e3f807eaa2a57 Mon Sep 17 00:00:00 2001
From: Zeu Capua <coding@zeu.dev>
Date: Mon, 9 Sep 2024 17:46:21 -0700
Subject: [PATCH 15/18] move bubbletea program to spec.go

---
 cmd/generate/config/config.go | 140 +--------------------------------
 cmd/generate/config/spec.go   | 142 ++++++++++++++++++++++++++++++++++
 2 files changed, 146 insertions(+), 136 deletions(-)
 create mode 100644 cmd/generate/config/spec.go

diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go
index 22d0156..5c571a4 100644
--- a/cmd/generate/config/config.go
+++ b/cmd/generate/config/config.go
@@ -6,11 +6,7 @@ import (
 	"os"
 	"path/filepath"
 	"slices"
-	"strings"
 
-	"github.com/charmbracelet/bubbles/help"
-	"github.com/charmbracelet/bubbles/key"
-	"github.com/charmbracelet/bubbles/textinput"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/go-git/go-git/v5"
 	"github.com/go-git/go-git/v5/plumbing/object"
@@ -27,11 +23,11 @@ type Options struct {
 
 	// whether to use interactive mode
 	isInteractive bool
-}
 
-const configLongDesc string = `WARNING: Proof of concept feature.
+	ttyDisabled bool
+}
 
-Generates a ~/.sauced.yaml configuration file. The attribution of emails to given entities
+const configLongDesc string = `Generates a ~/.sauced.yaml configuration file. The attribution of emails to given entities
 is based on the repository this command is ran in.`
 
 func NewConfigCommand() *cobra.Command {
@@ -65,6 +61,7 @@ func NewConfigCommand() *cobra.Command {
 		RunE: func(cmd *cobra.Command, _ []string) error {
 			opts.outputPath, _ = cmd.Flags().GetString("output-path")
 			opts.isInteractive, _ = cmd.Flags().GetBool("interactive")
+			opts.ttyDisabled, _ = cmd.Flags().GetBool("tty-disable")
 
 			return run(opts)
 		},
@@ -137,132 +134,3 @@ func run(opts *Options) error {
 
 	return nil
 }
-
-// Bubbletea for Interactive Mode
-
-type model struct {
-	textInput textinput.Model
-	help      help.Model
-	keymap    keymap
-
-	opts           *Options
-	attributionMap map[string][]string
-	uniqueEmails   []string
-	currentIndex   int
-}
-
-type keymap struct{}
-
-func (k keymap) ShortHelp() []key.Binding {
-	return []key.Binding{
-		key.NewBinding(key.WithKeys("ctrl+n"), key.WithHelp("ctrl+n", "next")),
-		key.NewBinding(key.WithKeys("ctrl+p"), key.WithHelp("ctrl+p", "prev")),
-		key.NewBinding(key.WithKeys("ctrl+i"), key.WithHelp("ctrl+i", "ignore email")),
-		key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "quit")),
-		key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit")),
-	}
-}
-
-func (k keymap) FullHelp() [][]key.Binding {
-	return [][]key.Binding{k.ShortHelp()}
-}
-
-func initialModel(opts *Options, uniqueEmails []string) model {
-	ti := textinput.New()
-	ti.Placeholder = "name"
-	ti.Focus()
-	ti.ShowSuggestions = true
-
-	return model{
-		textInput: ti,
-		help:      help.New(),
-		keymap:    keymap{},
-
-		opts:           opts,
-		attributionMap: make(map[string][]string),
-		uniqueEmails:   uniqueEmails,
-		currentIndex:   0,
-	}
-}
-
-func (m model) Init() tea.Cmd {
-	return textinput.Blink
-}
-
-func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
-	var cmd tea.Cmd
-	currentEmail := m.uniqueEmails[m.currentIndex]
-
-	existingUsers := make([]string, 0, len(m.attributionMap))
-	for k := range m.attributionMap {
-		existingUsers = append(existingUsers, k)
-	}
-
-	m.textInput.SetSuggestions(existingUsers)
-
-	keyMsg, ok := msg.(tea.KeyMsg)
-
-	if ok {
-		switch keyMsg.Type {
-		case tea.KeyCtrlC, tea.KeyEsc:
-			return m, tea.Quit
-
-		case tea.KeyCtrlI:
-			m.currentIndex++
-			return m, nil
-
-		case tea.KeyEnter:
-			if len(strings.Trim(m.textInput.Value(), " ")) == 0 {
-				return m, nil
-			}
-			m.attributionMap[m.textInput.Value()] = append(m.attributionMap[m.textInput.Value()], currentEmail)
-			m.textInput.Reset()
-			if m.currentIndex+1 >= len(m.uniqueEmails) {
-				return m, runOutputGeneration(m.opts, m.attributionMap)
-			}
-
-			m.currentIndex++
-			return m, nil
-		}
-	}
-
-	m.textInput, cmd = m.textInput.Update(msg)
-
-	return m, cmd
-}
-
-func (m model) View() string {
-	currentEmail := ""
-	if m.currentIndex < len(m.uniqueEmails) {
-		currentEmail = m.uniqueEmails[m.currentIndex]
-	}
-
-	return fmt.Sprintf(
-		"Found email %s - who to attribute to?: \n%s\n\n%s\n",
-		currentEmail,
-		m.textInput.View(),
-		m.help.View(m.keymap),
-	)
-}
-
-func runOutputGeneration(opts *Options, attributionMap map[string][]string) tea.Cmd {
-	// generate an output file
-	// default: `./.sauced.yaml`
-	// fallback for home directories
-	return func() tea.Msg {
-		if opts.outputPath == "~/" {
-			homeDir, _ := os.UserHomeDir()
-			err := generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap)
-			if err != nil {
-				return fmt.Errorf("error generating output file: %w", err)
-			}
-		} else {
-			err := generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap)
-			if err != nil {
-				return fmt.Errorf("error generating output file: %w", err)
-			}
-		}
-
-		return tea.Quit()
-	}
-}
diff --git a/cmd/generate/config/spec.go b/cmd/generate/config/spec.go
new file mode 100644
index 0000000..b8732ba
--- /dev/null
+++ b/cmd/generate/config/spec.go
@@ -0,0 +1,142 @@
+package config
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/charmbracelet/bubbles/help"
+	"github.com/charmbracelet/bubbles/key"
+	"github.com/charmbracelet/bubbles/textinput"
+	tea "github.com/charmbracelet/bubbletea"
+)
+
+// Bubbletea for Interactive Mode
+
+type model struct {
+	textInput textinput.Model
+	help      help.Model
+	keymap    keymap
+
+	opts           *Options
+	attributionMap map[string][]string
+	uniqueEmails   []string
+	currentIndex   int
+}
+
+type keymap struct{}
+
+func (k keymap) ShortHelp() []key.Binding {
+	return []key.Binding{
+		key.NewBinding(key.WithKeys("ctrl+n"), key.WithHelp("ctrl+n", "next")),
+		key.NewBinding(key.WithKeys("ctrl+p"), key.WithHelp("ctrl+p", "prev")),
+		key.NewBinding(key.WithKeys("ctrl+i"), key.WithHelp("ctrl+i", "ignore email")),
+		key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "quit")),
+		key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit")),
+	}
+}
+
+func (k keymap) FullHelp() [][]key.Binding {
+	return [][]key.Binding{k.ShortHelp()}
+}
+
+func initialModel(opts *Options, uniqueEmails []string) model {
+	ti := textinput.New()
+	ti.Placeholder = "name"
+	ti.Focus()
+	ti.ShowSuggestions = true
+
+	return model{
+		textInput: ti,
+		help:      help.New(),
+		keymap:    keymap{},
+
+		opts:           opts,
+		attributionMap: make(map[string][]string),
+		uniqueEmails:   uniqueEmails,
+		currentIndex:   0,
+	}
+}
+
+func (m model) Init() tea.Cmd {
+	return textinput.Blink
+}
+
+func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+	var cmd tea.Cmd
+	currentEmail := m.uniqueEmails[m.currentIndex]
+
+	existingUsers := make([]string, 0, len(m.attributionMap))
+	for k := range m.attributionMap {
+		existingUsers = append(existingUsers, k)
+	}
+
+	m.textInput.SetSuggestions(existingUsers)
+
+	keyMsg, ok := msg.(tea.KeyMsg)
+
+	if ok {
+		switch keyMsg.Type {
+		case tea.KeyCtrlC, tea.KeyEsc:
+			return m, tea.Quit
+
+		case tea.KeyCtrlI:
+			m.currentIndex++
+			return m, nil
+
+		case tea.KeyEnter:
+			if len(strings.Trim(m.textInput.Value(), " ")) == 0 {
+				return m, nil
+			}
+			m.attributionMap[m.textInput.Value()] = append(m.attributionMap[m.textInput.Value()], currentEmail)
+			m.textInput.Reset()
+			if m.currentIndex+1 >= len(m.uniqueEmails) {
+				return m, runOutputGeneration(m.opts, m.attributionMap)
+			}
+
+			m.currentIndex++
+			return m, nil
+		}
+	}
+
+	m.textInput, cmd = m.textInput.Update(msg)
+
+	return m, cmd
+}
+
+func (m model) View() string {
+	currentEmail := ""
+	if m.currentIndex < len(m.uniqueEmails) {
+		currentEmail = m.uniqueEmails[m.currentIndex]
+	}
+
+	return fmt.Sprintf(
+		"Found email %s - who to attribute to?: \n%s\n\n%s\n",
+		currentEmail,
+		m.textInput.View(),
+		m.help.View(m.keymap),
+	)
+}
+
+func runOutputGeneration(opts *Options, attributionMap map[string][]string) tea.Cmd {
+	// generate an output file
+	// default: `./.sauced.yaml`
+	// fallback for home directories
+	return func() tea.Msg {
+		if opts.outputPath == "~/" {
+			homeDir, _ := os.UserHomeDir()
+			err := generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap)
+			if err != nil {
+				return fmt.Errorf("error generating output file: %w", err)
+			}
+		} else {
+			err := generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap)
+			if err != nil {
+				return fmt.Errorf("error generating output file: %w", err)
+			}
+		}
+
+		return tea.Quit()
+	}
+}

From be30445a9546f9884fbce635358260876336fa69 Mon Sep 17 00:00:00 2001
From: Zeu Capua <coding@zeu.dev>
Date: Mon, 9 Sep 2024 17:49:11 -0700
Subject: [PATCH 16/18] check for tty-disable flag

---
 cmd/generate/config/config.go | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go
index 5c571a4..99e9825 100644
--- a/cmd/generate/config/config.go
+++ b/cmd/generate/config/config.go
@@ -24,6 +24,7 @@ type Options struct {
 	// whether to use interactive mode
 	isInteractive bool
 
+	// from global config
 	ttyDisabled bool
 }
 
@@ -92,7 +93,7 @@ func run(opts *Options) error {
 		name := c.Author.Name
 		email := c.Author.Email
 
-		if !opts.isInteractive {
+		if opts.ttyDisabled || !opts.isInteractive {
 			doesEmailExist := slices.Contains(attributionMap[name], email)
 			if !doesEmailExist {
 				// AUTOMATIC: set every name and associated emails
@@ -109,7 +110,7 @@ func run(opts *Options) error {
 	}
 
 	// INTERACTIVE: per unique email, set a name (existing or new or ignore)
-	if opts.isInteractive {
+	if opts.isInteractive && !opts.ttyDisabled {
 		program := tea.NewProgram(initialModel(opts, uniqueEmails))
 		if _, err := program.Run(); err != nil {
 			return fmt.Errorf("error running interactive mode: %w", err)

From bfdd66918c433934b629de1582624d97665d013e Mon Sep 17 00:00:00 2001
From: Zeu Capua <coding@zeu.dev>
Date: Tue, 10 Sep 2024 13:07:30 -0700
Subject: [PATCH 17/18] change placeholder/help copy

---
 cmd/generate/config/config.go | 4 ++--
 cmd/generate/config/spec.go   | 6 +++---
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go
index 99e9825..e5e3e19 100644
--- a/cmd/generate/config/config.go
+++ b/cmd/generate/config/config.go
@@ -28,7 +28,7 @@ type Options struct {
 	ttyDisabled bool
 }
 
-const configLongDesc string = `Generates a ~/.sauced.yaml configuration file. The attribution of emails to given entities
+const configLongDesc string = `Generates a ".sauced.yaml" configuration file. The attribution of emails to given entities
 is based on the repository this command is ran in.`
 
 func NewConfigCommand() *cobra.Command {
@@ -36,7 +36,7 @@ func NewConfigCommand() *cobra.Command {
 
 	cmd := &cobra.Command{
 		Use:   "config path/to/repo [flags]",
-		Short: "Generates a \"~/.sauced.yaml\" config based on the current repository",
+		Short: "Generates a \".sauced.yaml\" config based on the current repository",
 		Long:  configLongDesc,
 		Args: func(_ *cobra.Command, args []string) error {
 			if len(args) != 1 {
diff --git a/cmd/generate/config/spec.go b/cmd/generate/config/spec.go
index b8732ba..4b24c8d 100644
--- a/cmd/generate/config/spec.go
+++ b/cmd/generate/config/spec.go
@@ -29,8 +29,8 @@ type keymap struct{}
 
 func (k keymap) ShortHelp() []key.Binding {
 	return []key.Binding{
-		key.NewBinding(key.WithKeys("ctrl+n"), key.WithHelp("ctrl+n", "next")),
-		key.NewBinding(key.WithKeys("ctrl+p"), key.WithHelp("ctrl+p", "prev")),
+		key.NewBinding(key.WithKeys("ctrl+n"), key.WithHelp("ctrl+n", "next suggestion")),
+		key.NewBinding(key.WithKeys("ctrl+p"), key.WithHelp("ctrl+p", "prev suggestion")),
 		key.NewBinding(key.WithKeys("ctrl+i"), key.WithHelp("ctrl+i", "ignore email")),
 		key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "quit")),
 		key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit")),
@@ -43,7 +43,7 @@ func (k keymap) FullHelp() [][]key.Binding {
 
 func initialModel(opts *Options, uniqueEmails []string) model {
 	ti := textinput.New()
-	ti.Placeholder = "name"
+	ti.Placeholder = "username"
 	ti.Focus()
 	ti.ShowSuggestions = true
 

From 315a97b9c4cdfbda43c6a1b24fe5b23aa46ca51a Mon Sep 17 00:00:00 2001
From: Zeu Capua <coding@zeu.dev>
Date: Tue, 10 Sep 2024 13:42:09 -0700
Subject: [PATCH 18/18] check if last attribution is ignored

---
 cmd/generate/config/spec.go | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/cmd/generate/config/spec.go b/cmd/generate/config/spec.go
index 4b24c8d..5284cc9 100644
--- a/cmd/generate/config/spec.go
+++ b/cmd/generate/config/spec.go
@@ -83,6 +83,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 		case tea.KeyCtrlI:
 			m.currentIndex++
+			if m.currentIndex+1 >= len(m.uniqueEmails) {
+				return m, runOutputGeneration(m.opts, m.attributionMap)
+			}
 			return m, nil
 
 		case tea.KeyEnter: