diff --git a/main.go b/main.go index 9f7a66597382..c61788e02bda 100644 --- a/main.go +++ b/main.go @@ -269,8 +269,8 @@ func init() { // Support -h for help cli.HelpFlag.Short('h') - if len(os.Args) <= 1 && isatty.IsTerminal(os.Stdout.Fd()) { - args := tui.Run() + if isatty.IsTerminal(os.Stdout.Fd()) && (len(os.Args) <= 1 || os.Args[1] == analyzeCmd.FullCommand()) { + args := tui.Run(os.Args[1:]) if len(args) == 0 { os.Exit(0) } @@ -520,46 +520,56 @@ func run(state overseer.State) { return } - topLevelSubCommand, _, _ := strings.Cut(cmd, " ") - switch topLevelSubCommand { - case analyzeCmd.FullCommand(): - analyzer.Run(cmd) - default: - metrics, err := runSingleScan(ctx, cmd, engConf) - if err != nil { - logFatal(err, "error running scan") - } - - verificationCacheMetrics := struct { - Hits int32 - Misses int32 - HitsWasted int32 - AttemptsSaved int32 - VerificationTimeSpentMS int64 - }{ - Hits: verificationCacheMetrics.ResultCacheHits.Load(), - Misses: verificationCacheMetrics.ResultCacheMisses.Load(), - HitsWasted: verificationCacheMetrics.ResultCacheHitsWasted.Load(), - AttemptsSaved: verificationCacheMetrics.CredentialVerificationsSaved.Load(), - VerificationTimeSpentMS: verificationCacheMetrics.FromDataVerifyTimeSpentMS.Load(), - } - - // Print results. - logger.Info("finished scanning", - "chunks", metrics.ChunksScanned, - "bytes", metrics.BytesScanned, - "verified_secrets", metrics.VerifiedSecretsFound, - "unverified_secrets", metrics.UnverifiedSecretsFound, - "scan_duration", metrics.ScanDuration.String(), - "trufflehog_version", version.BuildVersion, - "verification_caching", verificationCacheMetrics, - ) - - if metrics.hasFoundResults && *fail { - logger.V(2).Info("exiting with code 183 because results were found") - os.Exit(183) - } + metrics, err := runSingleScan(ctx, cmd, engConf) + if err != nil { + logFatal(err, "error running scan") + } + + verificationCacheMetricsSnapshot := struct { + Hits int32 + Misses int32 + HitsWasted int32 + AttemptsSaved int32 + VerificationTimeSpentMS int64 + }{ + Hits: verificationCacheMetrics.ResultCacheHits.Load(), + Misses: verificationCacheMetrics.ResultCacheMisses.Load(), + HitsWasted: verificationCacheMetrics.ResultCacheHitsWasted.Load(), + AttemptsSaved: verificationCacheMetrics.CredentialVerificationsSaved.Load(), + VerificationTimeSpentMS: verificationCacheMetrics.FromDataVerifyTimeSpentMS.Load(), + } + + // Print results. + logger.Info("finished scanning", + "chunks", metrics.ChunksScanned, + "bytes", metrics.BytesScanned, + "verified_secrets", metrics.VerifiedSecretsFound, + "unverified_secrets", metrics.UnverifiedSecretsFound, + "scan_duration", metrics.ScanDuration.String(), + "trufflehog_version", version.BuildVersion, + "verification_caching", verificationCacheMetricsSnapshot, + ) + + if metrics.hasFoundResults && *fail { + logger.V(2).Info("exiting with code 183 because results were found") + os.Exit(183) } + + // Print results. + logger.Info("finished scanning", + "chunks", metrics.ChunksScanned, + "bytes", metrics.BytesScanned, + "verified_secrets", metrics.VerifiedSecretsFound, + "unverified_secrets", metrics.UnverifiedSecretsFound, + "scan_duration", metrics.ScanDuration.String(), + "trufflehog_version", version.BuildVersion, + ) + + if metrics.hasFoundResults && *fail { + logger.V(2).Info("exiting with code 183 because results were found") + os.Exit(183) + } + } func compareScans(ctx context.Context, cmd string, cfg engine.Config) error { diff --git a/pkg/analyzer/cli.go b/pkg/analyzer/cli.go index b3cf2c7bc083..f6c8b5977505 100644 --- a/pkg/analyzer/cli.go +++ b/pkg/analyzer/cli.go @@ -1,11 +1,9 @@ package analyzer import ( - "fmt" "strings" "github.com/alecthomas/kingpin/v2" - "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/airbrake" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/asana" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/bitbucket" @@ -28,37 +26,18 @@ import ( "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/stripe" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/twilio" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config" - "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/tui" ) -var ( - // TODO: Add list of supported key types. - analyzeKeyType *string -) +type SecretInfo struct { + Parts map[string]string + Cfg *config.Config +} func Command(app *kingpin.Application) *kingpin.CmdClause { - cli := app.Command("analyze", "Analyze API keys for fine-grained permissions information.") - - keyTypeHelp := fmt.Sprintf( - "Type of key to analyze. Omit to interactively choose. Available key types: %s", - strings.Join(analyzers.AvailableAnalyzers(), ", "), - ) - // Lowercase the available analyzers. - availableAnalyzers := make([]string, len(analyzers.AvailableAnalyzers())) - for i, a := range analyzers.AvailableAnalyzers() { - availableAnalyzers[i] = strings.ToLower(a) - } - analyzeKeyType = cli.Arg("key-type", keyTypeHelp).Enum(availableAnalyzers...) - - return cli + return app.Command("analyze", "Analyze API keys for fine-grained permissions information.") } -func Run(cmd string) { - keyType, secretInfo, err := tui.Run(*analyzeKeyType) - if err != nil { - // TODO: Log error. - return - } +func Run(keyType string, secretInfo SecretInfo) { if secretInfo.Cfg == nil { secretInfo.Cfg = &config.Config{} } diff --git a/pkg/analyzer/tui/tui.go b/pkg/analyzer/tui/tui.go deleted file mode 100644 index 66954124b14d..000000000000 --- a/pkg/analyzer/tui/tui.go +++ /dev/null @@ -1,122 +0,0 @@ -package tui - -import ( - "errors" - "fmt" - "strings" - - tea "github.com/charmbracelet/bubbletea" - "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers" - "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config" - "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" - "github.com/trufflesecurity/trufflehog/v3/pkg/tui/keymap" - "github.com/trufflesecurity/trufflehog/v3/pkg/tui/styles" -) - -// TUI is the main TUI model. -type TUI struct { - keyType string - secretInfo *SecretInfo - common *common.Common - model tea.Model -} - -type SecretInfo struct { - Parts map[string]string - Cfg *config.Config -} - -var AbortError error = errors.New("command aborted") - -func Run(keyType string) (string, *SecretInfo, error) { - // If a keyType is provided, make sure it's in the list of AvailableAnalyzers. - if keyType != "" { - var found bool - for _, a := range analyzers.AvailableAnalyzers() { - if strings.EqualFold(a, keyType) { - keyType = a - found = true - break - } - } - if !found { - return "", nil, fmt.Errorf("Unrecognized command %q", keyType) - } - } - - t := &TUI{ - keyType: keyType, - common: &common.Common{ - KeyMap: keymap.DefaultKeyMap(), - }, - } - if _, err := tea.NewProgram(t).Run(); err != nil { - return "", nil, err - } - if t.secretInfo == nil { - return "", nil, AbortError - } - return t.keyType, t.secretInfo, nil -} - -func (ui *TUI) Init() tea.Cmd { - return nil -} - -func (ui *TUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - if msg, ok := msg.(tea.WindowSizeMsg); ok { - ui.SetSize(msg) - } - // Always be able to force quit. - if msg, ok := msg.(tea.KeyMsg); ok && msg.Type.String() == "ctrl+c" { - return ui, tea.Quit - } - - switch m := msg.(type) { - case SetKeyTypeMsg: - ui.keyType = string(m) - case SecretInfo: - ui.secretInfo = &m - return ui, tea.Quit - } - - if ui.model == nil { - return ui, nil - } - - var cmd tea.Cmd - ui.model, cmd = ui.model.Update(msg) - return ui, cmd -} - -func (ui *TUI) View() string { - if ui.model == nil { - return "Loading..." - } - return ui.model.View() -} - -func (ui *TUI) SetSize(msg tea.WindowSizeMsg) { - h, v := styles.AppStyle.GetFrameSize() - h, v = msg.Width-h, msg.Height-v - ui.common.SetSize(h, v) - if ui.model != nil { - return - } - - // Set the model only after we have size information. - // TODO: Responsive pages. - if ui.keyType == "" { - ui.model = NewKeyTypePage(ui.common) - } else { - ui.model = NewFormPage(ui.common, ui.keyType) - } -} - -type SetKeyTypeMsg string - -func SetKeyTypeCmd(keyType string) tea.Cmd { - return func() tea.Msg { - return SetKeyTypeMsg(keyType) - } -} diff --git a/pkg/analyzer/tui/form.go b/pkg/tui/pages/analyze_form/analyze_form.go similarity index 66% rename from pkg/analyzer/tui/form.go rename to pkg/tui/pages/analyze_form/analyze_form.go index d335e5c19b8e..d5c0e0656b59 100644 --- a/pkg/analyzer/tui/form.go +++ b/pkg/tui/pages/analyze_form/analyze_form.go @@ -1,26 +1,38 @@ -package tui +package analyze_form import ( "fmt" - "slices" "strings" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" - "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers" + "github.com/charmbracelet/lipgloss" + "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config" "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/textinputs" "github.com/trufflesecurity/trufflehog/v3/pkg/tui/styles" ) -type FormPage struct { - Common *common.Common +var ( + titleStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FFFDF5")). + Background(lipgloss.Color(styles.Colors["bronze"])). + Padding(0, 1) +) + +type AnalyzeForm struct { + common.Common KeyType string form textinputs.Model } -func NewFormPage(c *common.Common, keyType string) FormPage { +type Submission struct { + AnalyzerType string + AnalyzerInfo analyzer.SecretInfo +} + +func New(c common.Common, keyType string) *AnalyzeForm { var inputs []textinputs.InputConfig switch strings.ToLower(keyType) { case "twilio": @@ -65,25 +77,31 @@ func NewFormPage(c *common.Common, keyType string) FormPage { SetHeader(titleStyle.Render(fmt.Sprintf("Configuring %s analyzer", keyType))). SetFooter("⚠️ Running TruffleHog Analyze will send a lot of requests ⚠️\n\n🚧 Please confirm you have permission to run TruffleHog Analyze against this secret 🚧"). SetSubmitMsg("Run TruffleHog Analyze") - return FormPage{ + return &AnalyzeForm{ Common: c, KeyType: keyType, form: form, } } -func (FormPage) Init() tea.Cmd { +func (AnalyzeForm) Init() tea.Cmd { return nil } -func (ui FormPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - // TODO: Check form focus. - if msg, ok := msg.(tea.KeyMsg); ok { +type SetAnalyzerMsg string + +func (ui *AnalyzeForm) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case SetAnalyzerMsg: + ui = New(ui.Common, string(msg)) + return ui, nil + case tea.KeyMsg: switch { case key.Matches(msg, ui.Common.KeyMap.Back): - return ui.PrevPage() + return nil, tea.Quit } } + if _, ok := msg.(textinputs.SelectNextMsg); ok { values := make(map[string]string) for k, v := range ui.form.GetInputs() { @@ -96,27 +114,29 @@ func (ui FormPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { LogFile: logFile, LoggingEnabled: logFile != "", } - return SecretInfo{Cfg: &cfg, Parts: values} + return Submission{ + AnalyzerType: ui.KeyType, + AnalyzerInfo: analyzer.SecretInfo{Cfg: &cfg, Parts: values}, + } } - return nil, secretInfoCmd + return ui, secretInfoCmd } + form, cmd := ui.form.Update(msg) ui.form = form.(textinputs.Model) return ui, cmd } -func (ui FormPage) View() string { +func (ui *AnalyzeForm) View() string { return styles.AppStyle.Render(ui.form.View()) } -func (ui FormPage) PrevPage() (tea.Model, tea.Cmd) { - page := NewKeyTypePage(ui.Common) - // Select what was previously selected. - index, ok := slices.BinarySearch(analyzers.AvailableAnalyzers(), ui.KeyType) - if !ok { - // Should be impossible. - index = 0 - } - page.list.Select(index) - return page, nil +func (m *AnalyzeForm) ShortHelp() []key.Binding { + // TODO: actually return something + return nil +} + +func (m *AnalyzeForm) FullHelp() [][]key.Binding { + // TODO: actually return something + return nil } diff --git a/pkg/analyzer/tui/keytype.go b/pkg/tui/pages/analyze_keys/analyze_keys.go similarity index 67% rename from pkg/analyzer/tui/keytype.go rename to pkg/tui/pages/analyze_keys/analyze_keys.go index 485df873496d..d85bc6e85602 100644 --- a/pkg/analyzer/tui/keytype.go +++ b/pkg/tui/pages/analyze_keys/analyze_keys.go @@ -1,4 +1,4 @@ -package tui +package analyze_keys import ( "github.com/charmbracelet/bubbles/key" @@ -7,6 +7,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers" "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/selector" "github.com/trufflesecurity/trufflehog/v3/pkg/tui/styles" ) @@ -23,17 +24,16 @@ var ( Padding(0, 1) ) -type KeyTypePage struct { - Common *common.Common - +type AnalyzeKeyPage struct { + common.Common list list.Model } -func (ui KeyTypePage) Init() tea.Cmd { +func (ui *AnalyzeKeyPage) Init() tea.Cmd { return nil } -func NewKeyTypePage(c *common.Common) KeyTypePage { +func New(c common.Common) *AnalyzeKeyPage { items := make([]list.Item, len(analyzers.AvailableAnalyzers())) for i, analyzerType := range analyzers.AvailableAnalyzers() { items[i] = KeyTypeItem(analyzerType) @@ -47,39 +47,43 @@ func NewKeyTypePage(c *common.Common) KeyTypePage { list.Title = "Select an analyzer type" list.SetShowStatusBar(false) list.Styles.Title = titleStyle - return KeyTypePage{ + return &AnalyzeKeyPage{ Common: c, list: list, } } -func (ui KeyTypePage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (ui *AnalyzeKeyPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if !ui.list.SettingFilter() { switch msg := msg.(type) { + + case tea.WindowSizeMsg: + h, v := styles.AppStyle.GetFrameSize() + ui.list.SetSize(msg.Width-h, msg.Height-v) case tea.KeyMsg: switch { case key.Matches(msg, ui.Common.KeyMap.Back): return nil, tea.Quit case key.Matches(msg, ui.Common.KeyMap.Select): - chosen := string(ui.list.SelectedItem().(KeyTypeItem)) - return NewFormPage(ui.Common, chosen), SetKeyTypeCmd(chosen) + chosenAnalyzer := ui.list.SelectedItem().(KeyTypeItem) + + return ui, func() tea.Msg { + return selector.SelectMsg{IdentifiableItem: chosenAnalyzer} + } } } - } - var cmd tea.Cmd - ui.list, cmd = ui.list.Update(msg) - return ui, cmd + var cmd tea.Cmd + ui.list, cmd = ui.list.Update(msg) + return ui, cmd + } + return ui, func() tea.Msg { return nil } } -func (ui KeyTypePage) View() string { +func (ui AnalyzeKeyPage) View() string { return styles.AppStyle.Render(ui.list.View()) } -func (ui KeyTypePage) NextPage(keyType string) (tea.Model, tea.Cmd) { - return NewFormPage(ui.Common, keyType), SetKeyTypeCmd(keyType) -} - type KeyTypeItem string func (i KeyTypeItem) ID() string { return string(i) } @@ -87,10 +91,12 @@ func (i KeyTypeItem) Title() string { return string(i) } func (i KeyTypeItem) Description() string { return "" } func (i KeyTypeItem) FilterValue() string { return string(i) } -func init() { - // Preload HasDarkBackground call. For some reason, if we don't do - // this, the TUI can take a noticeably long time to start. We should - // investigate further, but this is a good-enough bandaid for now. - // See: https://github.com/charmbracelet/lipgloss/issues/73 - _ = lipgloss.HasDarkBackground() +func (m AnalyzeKeyPage) ShortHelp() []key.Binding { + // TODO: actually return something + return nil +} + +func (m AnalyzeKeyPage) FullHelp() [][]key.Binding { + // TODO: actually return something + return nil } diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go index e7f29f8ee126..bba7f1bd4912 100644 --- a/pkg/tui/tui.go +++ b/pkg/tui/tui.go @@ -8,9 +8,13 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" zone "github.com/lrstanley/bubblezone" + "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer" + "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers" "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/selector" "github.com/trufflesecurity/trufflehog/v3/pkg/tui/keymap" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/pages/analyze_form" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/pages/analyze_keys" "github.com/trufflesecurity/trufflehog/v3/pkg/tui/pages/contact_enterprise" "github.com/trufflesecurity/trufflehog/v3/pkg/tui/pages/source_configure" "github.com/trufflesecurity/trufflehog/v3/pkg/tui/pages/source_select" @@ -27,6 +31,8 @@ const ( sourceConfigurePage viewOSSProjectPage contactEnterprisePage + analyzeKeysPage + analyzeFormPage ) type sessionState int @@ -39,20 +45,31 @@ const ( // TUI is the main TUI model. type TUI struct { - common common.Common - pages []common.Component - activePage page - state sessionState - args []string + common common.Common + pages []common.Component + pageHistory []page + state sessionState + args []string + + // Analyzer specific values that are only set if running an analysis. + analyzerType string + analyzerInfo analyzer.SecretInfo } // New returns a new TUI model. -func New(c common.Common) *TUI { +func New(c common.Common, args []string) *TUI { ui := &TUI{ - common: c, - pages: make([]common.Component, 5), - activePage: wizardIntroPage, - state: startState, + common: c, + pages: make([]common.Component, 7), + pageHistory: []page{wizardIntroPage}, + state: startState, + args: args, + } + switch { + case len(args) == 0: + return ui + case len(args) == 1 && args[0] == "analyze": + ui.pageHistory = []page{wizardIntroPage, analyzeKeysPage} } return ui } @@ -69,11 +86,28 @@ func (ui *TUI) SetSize(width, height int) { // Init implements tea.Model. func (ui *TUI) Init() tea.Cmd { + ui.pages[wizardIntroPage] = wizard_intro.New(ui.common) ui.pages[sourceSelectPage] = source_select.New(ui.common) ui.pages[sourceConfigurePage] = source_configure.New(ui.common) ui.pages[viewOSSProjectPage] = view_oss.New(ui.common) ui.pages[contactEnterprisePage] = contact_enterprise.New(ui.common) + ui.pages[analyzeKeysPage] = analyze_keys.New(ui.common) + + if len(ui.args) > 1 && ui.args[0] == "analyze" { + analyzerArg := strings.ToLower(ui.args[1]) + ui.pages[analyzeFormPage] = analyze_form.New(ui.common, analyzerArg) + ui.setActivePage(analyzeKeysPage) + + for _, analyzer := range analyzers.AvailableAnalyzers() { + if strings.ToLower(analyzer) == analyzerArg { + ui.setActivePage(analyzeFormPage) + } + } + } else { + ui.pages[analyzeFormPage] = analyze_form.New(ui.common, "this is a bug") + } + ui.SetSize(ui.common.Width, ui.common.Height) cmds := make([]tea.Cmd, 0) cmds = append(cmds, @@ -82,6 +116,8 @@ func (ui *TUI) Init() tea.Cmd { ui.pages[sourceConfigurePage].Init(), ui.pages[viewOSSProjectPage].Init(), ui.pages[contactEnterprisePage].Init(), + ui.pages[analyzeKeysPage].Init(), + ui.pages[analyzeFormPage].Init(), ) ui.state = loadedState ui.SetSize(ui.common.Width, ui.common.Height) @@ -106,12 +142,12 @@ func (ui *TUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: switch { case key.Matches(msg, ui.common.KeyMap.Help): - case key.Matches(msg, ui.common.KeyMap.CmdQuit) && ui.activePage != sourceConfigurePage: + case key.Matches(msg, ui.common.KeyMap.CmdQuit) && ui.activePage() != sourceConfigurePage: return ui, tea.Quit case key.Matches(msg, ui.common.KeyMap.Quit): return ui, tea.Quit - case ui.activePage > 0 && key.Matches(msg, ui.common.KeyMap.Back): - ui.activePage -= 1 + case ui.activePage() > 0 && key.Matches(msg, ui.common.KeyMap.Back): + _ = ui.popHistory() return ui, nil } } @@ -120,37 +156,45 @@ func (ui *TUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case selector.SelectMsg: switch item := msg.IdentifiableItem.(type) { case wizard_intro.Item: + switch item { case wizard_intro.Quit: cmds = append(cmds, tea.Quit) case wizard_intro.ViewOSSProject: - ui.activePage = viewOSSProjectPage + ui.setActivePage(viewOSSProjectPage) case wizard_intro.ViewHelpDocs: ui.args = []string{"--help"} - return ui, tea.Quit case wizard_intro.EnterpriseInquire: - ui.activePage = contactEnterprisePage + ui.setActivePage(contactEnterprisePage) case wizard_intro.ScanSourceWithWizard: - ui.activePage = sourceSelectPage + ui.setActivePage(sourceSelectPage) case wizard_intro.AnalyzeSecret: - ui.args = []string{"analyze"} - return ui, tea.Quit + ui.setActivePage(analyzeKeysPage) } case source_select.SourceItem: - ui.activePage = sourceConfigurePage + ui.setActivePage(sourceConfigurePage) cmds = append(cmds, func() tea.Msg { return source_configure.SetSourceMsg{Source: item.ID()} }) + case analyze_keys.KeyTypeItem: + ui.setActivePage(analyzeFormPage) + cmds = append(cmds, func() tea.Msg { + return analyze_form.SetAnalyzerMsg(item.ID()) + }) } case source_configure.SetArgsMsg: ui.args = strings.Split(string(msg), " ")[1:] return ui, tea.Quit + case analyze_form.Submission: + ui.analyzerType = msg.AnalyzerType + ui.analyzerInfo = msg.AnalyzerInfo + return ui, tea.Quit } if ui.state == loadedState { - m, cmd := ui.pages[ui.activePage].Update(msg) - ui.pages[ui.activePage] = m.(common.Component) + m, cmd := ui.pages[ui.activePage()].Update(msg) + ui.pages[ui.activePage()] = m.(common.Component) if cmd != nil { cmds = append(cmds, cmd) } @@ -168,7 +212,7 @@ func (ui *TUI) View() string { case startState: view = "Loading..." case loadedState: - view = ui.pages[ui.activePage].View() + view = ui.pages[ui.activePage()].View() default: view = "Unknown state :/ this is a bug!" } @@ -177,7 +221,7 @@ func (ui *TUI) View() string { ) } -func Run() []string { +func Run(args []string) []string { c := common.Common{ Copy: nil, Styles: styles.DefaultStyles(), @@ -186,12 +230,36 @@ func Run() []string { Height: 0, Zone: zone.New(), } - m := New(c) + m := New(c, args) p := tea.NewProgram(m) // TODO: Print normal help message. if _, err := p.Run(); err != nil { fmt.Printf("Alas, there's been an error: %v", err) os.Exit(1) } + if m.analyzerType != "" { + analyzer.Run(m.analyzerType, m.analyzerInfo) + os.Exit(0) + } return m.args } + +func (ui *TUI) activePage() page { + if len(ui.pageHistory) == 0 { + return wizardIntroPage + } + return ui.pageHistory[len(ui.pageHistory)-1] +} + +func (ui *TUI) popHistory() page { + if len(ui.pageHistory) == 0 { + return wizardIntroPage + } + p := ui.activePage() + ui.pageHistory = ui.pageHistory[:len(ui.pageHistory)-1] + return p +} + +func (ui *TUI) setActivePage(p page) { + ui.pageHistory = append(ui.pageHistory, p) +}