From 47c2b6bed989f191f8886c1a50fe6132f63d4208 Mon Sep 17 00:00:00 2001 From: Hon <8292703+hxnyk@users.noreply.github.com> Date: Wed, 9 Aug 2023 13:13:55 -0700 Subject: [PATCH] Add terminal UI (#1593) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Init attempt at tui with bubble tea. Co-authored-by: mcastorina * Add starting and source selection options Co-authored-by: mcastorina * Rewrite models into a state machine * Update source descriptions * Make subpages implement tea.Model * Rename page0 and page1 to be more descriptive * Adjust styling and adding color consts Co-authored-by: mcastorina * Add helper generic function to call Update and type cast * Setup plumbing for source configuration page * Use CLI introspection for source configuration (WIP) * Experiment with table view * Replace table with form fields Co-authored-by: mcastorina * Change šŸ”’ to šŸ’ø * Copy components from soft-serve Co-authored-by: hxnyk <8292703+hxnyk@users.noreply.github.com> * Copy styles from soft-serve Co-authored-by: hxnyk <8292703+hxnyk@users.noreply.github.com> * Copy common from soft-serve Co-authored-by: hxnyk <8292703+hxnyk@users.noreply.github.com> * Refactor into pages This is still a WIP, but the main structure is there. Co-authored-by: hxnyk <8292703+hxnyk@users.noreply.github.com> * Trying out selector for wizard intro Co-authored-by: mcastorina * Use selector with custom View Co-authored-by: hxnyk <8292703+hxnyk@users.noreply.github.com> * Change Item to be an enum Co-authored-by: hxnyk <8292703+hxnyk@users.noreply.github.com> * Add link pages Co-authored-by: mcastorina * Update source select to use selector Co-authored-by: mcastorina * Delete source configure page and add blank tabs Co-authored-by: hxnyk <8292703+hxnyk@users.noreply.github.com> * Add tab placeholder pages for configurationi Co-authored-by: mcastorina * Added headers and style to each tab Co-authored-by: hxnyk <8292703+hxnyk@users.noreply.github.com> * Update with new sources * Remove kingpin attribute from SourceItem * Add basic form field and source structuring * Hookup git form fields with an underlying textinput component Co-authored-by: hxnyk <8292703+hxnyk@users.noreply.github.com> * Update forms for git and github Co-authored-by: mcastorina * Add labels per text input * Add sources and adjust styling * add basic trufflehog configuration page * Add skip button to textinputs component * Emit and handle textinputs skip/submit button commands * Don't quit when q is pressed on the sourceConfigurePage * Build trufflehog command based on source config vals Co-authored-by: mcastorina * Build flags based on truffle config inputs * Update summary section * Add generated truffle fields Co-authored-by: mcastorina * update summary to correctly print info * Go back a page when escape key is pressed * WIP run page list Co-authored-by: hxnyk <8292703+hxnyk@users.noreply.github.com> * Allow running trufflehog from the run page Co-authored-by: hxnyk <8292703+hxnyk@users.noreply.github.com> * Add option to view help docs Co-authored-by: mcastorina * comment out unused styles and remove unused types * Capitalize H in TruffleHog * remove unneeded fmt.Sprintf --------- Co-authored-by: mcastorina --- go.mod | 29 +- go.sum | 67 ++- main.go | 14 + pkg/tui/common/common.go | 24 + pkg/tui/common/component.go | 13 + pkg/tui/common/error.go | 13 + pkg/tui/common/style.go | 27 + pkg/tui/common/utils.go | 27 + pkg/tui/components/footer/footer.go | 96 ++++ pkg/tui/components/formfield/formfield.go | 38 ++ pkg/tui/components/header/header.go | 42 ++ pkg/tui/components/selector/selector.go | 237 +++++++++ pkg/tui/components/statusbar/statusbar.go | 88 ++++ pkg/tui/components/tabs/tabs.go | 122 +++++ pkg/tui/components/textinput/textinput.go | 51 ++ pkg/tui/components/textinputs/textinputs.go | 229 ++++++++ pkg/tui/components/viewport/viewport.go | 97 ++++ pkg/tui/keymap/keymap.go | 217 ++++++++ .../contact_enterprise/contact_enterprise.go | 60 +++ pkg/tui/pages/source_configure/item.go | 23 + .../pages/source_configure/run_component.go | 118 +++++ .../source_configure/source_component.go | 71 +++ .../source_configure/source_configure.go | 139 +++++ .../source_configure/trufflehog_component.go | 66 +++ .../source_configure/trufflehog_configure.go | 115 ++++ pkg/tui/pages/source_select/item.go | 32 ++ pkg/tui/pages/source_select/source_select.go | 222 ++++++++ pkg/tui/pages/view_oss/view_oss.go | 59 +++ pkg/tui/pages/wizard_intro/item.go | 137 +++++ pkg/tui/pages/wizard_intro/wizard_intro.go | 109 ++++ pkg/tui/sources/circleci/circleci.go | 44 ++ pkg/tui/sources/docker/docker.go | 49 ++ pkg/tui/sources/filesystem/filesystem.go | 45 ++ pkg/tui/sources/gcs/gcs.go | 44 ++ pkg/tui/sources/git/git.go | 44 ++ pkg/tui/sources/github/github.go | 61 +++ pkg/tui/sources/gitlab/gitlab.go | 45 ++ pkg/tui/sources/s3/s3.go | 48 ++ pkg/tui/sources/sources.go | 60 +++ pkg/tui/sources/syslog/syslog.go | 82 +++ pkg/tui/styles/styles.go | 493 ++++++++++++++++++ pkg/tui/tui.go | 198 +++++++ 42 files changed, 3792 insertions(+), 3 deletions(-) create mode 100644 pkg/tui/common/common.go create mode 100644 pkg/tui/common/component.go create mode 100644 pkg/tui/common/error.go create mode 100644 pkg/tui/common/style.go create mode 100644 pkg/tui/common/utils.go create mode 100644 pkg/tui/components/footer/footer.go create mode 100644 pkg/tui/components/formfield/formfield.go create mode 100644 pkg/tui/components/header/header.go create mode 100644 pkg/tui/components/selector/selector.go create mode 100644 pkg/tui/components/statusbar/statusbar.go create mode 100644 pkg/tui/components/tabs/tabs.go create mode 100644 pkg/tui/components/textinput/textinput.go create mode 100644 pkg/tui/components/textinputs/textinputs.go create mode 100644 pkg/tui/components/viewport/viewport.go create mode 100644 pkg/tui/keymap/keymap.go create mode 100644 pkg/tui/pages/contact_enterprise/contact_enterprise.go create mode 100644 pkg/tui/pages/source_configure/item.go create mode 100644 pkg/tui/pages/source_configure/run_component.go create mode 100644 pkg/tui/pages/source_configure/source_component.go create mode 100644 pkg/tui/pages/source_configure/source_configure.go create mode 100644 pkg/tui/pages/source_configure/trufflehog_component.go create mode 100644 pkg/tui/pages/source_configure/trufflehog_configure.go create mode 100644 pkg/tui/pages/source_select/item.go create mode 100644 pkg/tui/pages/source_select/source_select.go create mode 100644 pkg/tui/pages/view_oss/view_oss.go create mode 100644 pkg/tui/pages/wizard_intro/item.go create mode 100644 pkg/tui/pages/wizard_intro/wizard_intro.go create mode 100644 pkg/tui/sources/circleci/circleci.go create mode 100644 pkg/tui/sources/docker/docker.go create mode 100644 pkg/tui/sources/filesystem/filesystem.go create mode 100644 pkg/tui/sources/gcs/gcs.go create mode 100644 pkg/tui/sources/git/git.go create mode 100644 pkg/tui/sources/github/github.go create mode 100644 pkg/tui/sources/gitlab/gitlab.go create mode 100644 pkg/tui/sources/s3/s3.go create mode 100644 pkg/tui/sources/sources.go create mode 100644 pkg/tui/sources/syslog/syslog.go create mode 100644 pkg/tui/styles/styles.go create mode 100644 pkg/tui/tui.go diff --git a/go.mod b/go.mod index 9adc98b7ced4..c085e3b624bc 100644 --- a/go.mod +++ b/go.mod @@ -14,10 +14,15 @@ require ( github.com/BobuSumisu/aho-corasick v1.0.3 github.com/TheZeroSlave/zapsentry v1.17.0 github.com/aws/aws-sdk-go v1.44.83 + github.com/aymanbagabas/go-osc52 v1.2.1 github.com/bill-rich/disk-buffer-reader v0.1.7 github.com/bill-rich/go-syslog v0.0.0-20220413021637-49edb52a574c github.com/bitfinexcom/bitfinex-api-go v0.0.0-20210608095005-9e0b26f200fb github.com/bradleyfalzon/ghinstallation/v2 v2.6.0 + github.com/charmbracelet/bubbles v0.16.1 + github.com/charmbracelet/bubbletea v0.24.1 + github.com/charmbracelet/glamour v0.6.0 + github.com/charmbracelet/lipgloss v0.7.1 github.com/couchbase/gocb/v2 v2.6.3 github.com/crewjam/rfc5424 v0.1.0 github.com/denisenkom/go-mssqldb v0.12.3 @@ -45,8 +50,11 @@ require ( github.com/jpillora/overseer v1.1.6 github.com/kylelemons/godebug v1.1.0 github.com/lib/pq v1.10.9 + github.com/lrstanley/bubblezone v0.0.0-20221222153816-e95291e2243e + github.com/mattn/go-isatty v0.0.18 github.com/mattn/go-sqlite3 v1.14.17 github.com/mholt/archiver/v4 v4.0.0-alpha.8 + github.com/muesli/reflow v0.3.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/paulbellamy/ratecounter v0.2.0 github.com/pkg/errors v0.9.1 @@ -88,9 +96,13 @@ require ( github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect github.com/acomagu/bufpipe v1.0.4 // indirect + github.com/alecthomas/chroma v0.10.0 // indirect github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect github.com/andybalholm/brotli v1.0.5 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect github.com/benbjohnson/clock v1.1.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bodgit/plumbing v1.2.0 // indirect @@ -99,10 +111,12 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cloudflare/circl v1.3.3 // indirect github.com/connesc/cipherio v0.2.1 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect github.com/couchbase/gocbcore/v10 v10.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect + github.com/dlclark/regexp2 v1.4.0 // indirect github.com/docker/cli v23.0.5+incompatible // indirect github.com/docker/distribution v2.8.2+incompatible // indirect github.com/docker/docker v23.0.5+incompatible // indirect @@ -125,6 +139,7 @@ require ( github.com/google/s2a-go v0.1.4 // indirect github.com/google/uuid v1.3.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect + github.com/gorilla/css v1.0.0 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -135,12 +150,19 @@ require ( github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.16.5 // indirect github.com/klauspost/pgzip v1.2.5 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/microcosm-cc/bluemonday v1.0.23 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect + github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.15.1 // indirect github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/onsi/ginkgo v1.16.5 // indirect github.com/onsi/gomega v1.23.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect @@ -152,7 +174,9 @@ require ( github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.10.1 // indirect + github.com/rivo/uniseg v0.4.2 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/sahilm/fuzzy v0.1.0 // indirect github.com/sirupsen/logrus v1.9.0 // indirect github.com/skeema/knownhosts v1.2.0 // indirect github.com/therootcompany/xz v1.0.1 // indirect @@ -163,6 +187,8 @@ require ( github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + github.com/yuin/goldmark v1.5.2 // indirect + github.com/yuin/goldmark-emoji v1.0.1 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect go.opencensus.io v0.24.0 // indirect go.uber.org/atomic v1.7.0 // indirect @@ -171,6 +197,7 @@ require ( golang.org/x/mod v0.11.0 // indirect golang.org/x/net v0.12.0 // indirect golang.org/x/sys v0.10.0 // indirect + golang.org/x/term v0.10.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.10.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect diff --git a/go.sum b/go.sum index e9b86dae5dad..cfbdb13a7866 100644 --- a/go.sum +++ b/go.sum @@ -68,6 +68,8 @@ github.com/TheZeroSlave/zapsentry v1.17.0 h1:RIQCG89U7vWWZVmmCxeUz/g32WEcAYXUrXH github.com/TheZeroSlave/zapsentry v1.17.0/go.mod h1:D1YMfSuu6xnkhwFXxrronesmsiyDhIqo+86I3Ok+r64= github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= +github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= +github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= @@ -79,8 +81,17 @@ github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/aws-sdk-go v1.44.83 h1:7+Rtc2Eio6EKUNoZeMV/IVxzVrY5oBQcNPtCcgIHYJA= github.com/aws/aws-sdk-go v1.44.83/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= +github.com/aymanbagabas/go-osc52 v1.2.1 h1:q2sWUyDcozPLcLabEMd+a+7Ea2DitxZVN9hTxab9L4E= +github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= +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/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -105,6 +116,14 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= +github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= +github.com/charmbracelet/bubbletea v0.24.1 h1:LpdYfnu+Qc6XtvMz6d/6rRY71yttHTP5HtrjMgWvixc= +github.com/charmbracelet/bubbletea v0.24.1/go.mod h1:rK3g/2+T8vOSEkNHvtq40umJpeVYDn6bLaqbgzhL/hg= +github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc= +github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc= +github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= +github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -120,6 +139,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/connesc/cipherio v0.2.1 h1:FGtpTPMbKNNWByNrr9aEBtaJtXjqOzkIXNYJp6OEycw= github.com/connesc/cipherio v0.2.1/go.mod h1:ukY0MWJDFnJEbXMQtOcn2VmTpRfzcTz4OoVrWGGJZcA= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= github.com/couchbase/gocb/v2 v2.6.3 h1:5RsMo+RRfK0mVxHLAfpBz3/tHlgXZb1WBNItLk9Ab+c= @@ -140,6 +161,8 @@ github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+ github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/docker/cli v23.0.5+incompatible h1:ufWmAOuD3Vmr7JP2G5K3cyuNC4YZWiAsuDEvFVVDafE= github.com/docker/cli v23.0.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= @@ -288,6 +311,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= @@ -343,24 +368,46 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lrstanley/bubblezone v0.0.0-20221222153816-e95291e2243e h1:XoxHx8K6ZKoMtjzWOMDuM69LCdjDDsTOtTfWGrT/fns= +github.com/lrstanley/bubblezone v0.0.0-20221222153816-e95291e2243e/go.mod h1:v5lEwWaguF1o2MW/ucO0ZIA/IZymdBYJJ+2cMRLE7LU= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mholt/archiver/v4 v4.0.0-alpha.8 h1:tRGQuDVPh66WCOelqe6LIGh0gwmfwxUrSSDunscGsRM= github.com/mholt/archiver/v4 v4.0.0-alpha.8/go.mod h1:5f7FUYGXdJWUjESffJaYR4R60VhnHxb2X3T1teMyv5A= +github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= +github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY= +github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA= +github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= +github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= +github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -369,6 +416,8 @@ github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n5 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= @@ -410,12 +459,18 @@ github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+Pymzi github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/rabbitmq/amqp091-go v1.8.1 h1:RejT1SBUim5doqcL6s7iN6SBmsQqyTgXb1xMlH0h1hA= github.com/rabbitmq/amqp091-go v1.8.1/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= +github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= +github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= @@ -471,6 +526,10 @@ github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7Jul github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU= +github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os= +github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE= @@ -568,6 +627,7 @@ golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= @@ -628,8 +688,10 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -644,6 +706,7 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/main.go b/main.go index 614101f1373c..c680b7c08d9f 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( "github.com/felixge/fgprof" "github.com/go-logr/logr" "github.com/jpillora/overseer" + "github.com/mattn/go-isatty" "google.golang.org/protobuf/types/known/anypb" "gopkg.in/alecthomas/kingpin.v2" @@ -28,6 +29,7 @@ import ( "github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb" "github.com/trufflesecurity/trufflehog/v3/pkg/sources" "github.com/trufflesecurity/trufflehog/v3/pkg/sources/git" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui" "github.com/trufflesecurity/trufflehog/v3/pkg/updater" "github.com/trufflesecurity/trufflehog/v3/pkg/version" ) @@ -144,6 +146,18 @@ func init() { } cli.Version("trufflehog " + version.BuildVersion) + + if len(os.Args) <= 1 && isatty.IsTerminal(os.Stdout.Fd()) { + args := tui.Run() + if len(args) == 0 { + os.Exit(0) + } + + // Overwrite the Args slice so overseer works properly. + os.Args = os.Args[:1] + os.Args = append(os.Args, args...) + } + cmd = kingpin.MustParse(cli.Parse(os.Args[1:])) switch { diff --git a/pkg/tui/common/common.go b/pkg/tui/common/common.go new file mode 100644 index 000000000000..9b8cf4a996f9 --- /dev/null +++ b/pkg/tui/common/common.go @@ -0,0 +1,24 @@ +package common + +import ( + "github.com/aymanbagabas/go-osc52" + zone "github.com/lrstanley/bubblezone" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/keymap" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/styles" +) + +// Common is a struct all components should embed. +type Common struct { + Copy *osc52.Output + Styles *styles.Styles + KeyMap *keymap.KeyMap + Width int + Height int + Zone *zone.Manager +} + +// SetSize sets the width and height of the common struct. +func (c *Common) SetSize(width, height int) { + c.Width = width + c.Height = height +} diff --git a/pkg/tui/common/component.go b/pkg/tui/common/component.go new file mode 100644 index 000000000000..ed8b9bf05cd3 --- /dev/null +++ b/pkg/tui/common/component.go @@ -0,0 +1,13 @@ +package common + +import ( + "github.com/charmbracelet/bubbles/help" + tea "github.com/charmbracelet/bubbletea" +) + +// Component represents a Bubble Tea model that implements a SetSize function. +type Component interface { + tea.Model + help.KeyMap + SetSize(width, height int) +} diff --git a/pkg/tui/common/error.go b/pkg/tui/common/error.go new file mode 100644 index 000000000000..fe9729805622 --- /dev/null +++ b/pkg/tui/common/error.go @@ -0,0 +1,13 @@ +package common + +import tea "github.com/charmbracelet/bubbletea" + +// ErrorMsg is a Bubble Tea message that represents an error. +type ErrorMsg error + +// ErrorCmd returns an ErrorMsg from error. +func ErrorCmd(err error) tea.Cmd { + return func() tea.Msg { + return ErrorMsg(err) + } +} diff --git a/pkg/tui/common/style.go b/pkg/tui/common/style.go new file mode 100644 index 000000000000..8b91d9afa08d --- /dev/null +++ b/pkg/tui/common/style.go @@ -0,0 +1,27 @@ +package common + +import ( + "github.com/charmbracelet/glamour" + gansi "github.com/charmbracelet/glamour/ansi" +) + +func strptr(s string) *string { + return &s +} + +// StyleConfig returns the default Glamour style configuration. +func StyleConfig() gansi.StyleConfig { + noColor := strptr("") + s := glamour.DarkStyleConfig + s.H1.BackgroundColor = noColor + s.H1.Prefix = "# " + s.H1.Suffix = "" + s.H1.Color = strptr("39") + s.Document.StylePrimitive.Color = noColor + s.CodeBlock.Chroma.Text.Color = noColor + s.CodeBlock.Chroma.Name.Color = noColor + // This fixes an issue with the default style config. For example + // highlighting empty spaces with red in Dockerfile type. + s.CodeBlock.Chroma.Error.BackgroundColor = noColor + return s +} diff --git a/pkg/tui/common/utils.go b/pkg/tui/common/utils.go new file mode 100644 index 000000000000..a61c052ef283 --- /dev/null +++ b/pkg/tui/common/utils.go @@ -0,0 +1,27 @@ +package common + +import ( + "strings" + + "github.com/muesli/reflow/truncate" +) + +// TruncateString is a convenient wrapper around truncate.TruncateString. +func TruncateString(s string, max int) string { + if max < 0 { + max = 0 + } + return truncate.StringWithTail(s, uint(max), "ā€¦") +} + +func SummarizeSource(keys []string, inputs map[string]string, labels map[string]string) string { + summary := strings.Builder{} + for _, key := range keys { + if inputs[key] != "" { + summary.WriteString("\t" + labels[key] + ": " + inputs[key] + "\n") + } + } + + summary.WriteString("\n") + return summary.String() +} diff --git a/pkg/tui/components/footer/footer.go b/pkg/tui/components/footer/footer.go new file mode 100644 index 000000000000..68ca0842edee --- /dev/null +++ b/pkg/tui/components/footer/footer.go @@ -0,0 +1,96 @@ +package footer + +import ( + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" +) + +// ToggleFooterMsg is a message sent to show/hide the footer. +type ToggleFooterMsg struct{} + +// Footer is a Bubble Tea model that displays help and other info. +type Footer struct { + common common.Common + help help.Model + keymap help.KeyMap +} + +// New creates a new Footer. +func New(c common.Common, keymap help.KeyMap) *Footer { + h := help.New() + h.Styles.ShortKey = c.Styles.HelpKey + h.Styles.ShortDesc = c.Styles.HelpValue + h.Styles.FullKey = c.Styles.HelpKey + h.Styles.FullDesc = c.Styles.HelpValue + f := &Footer{ + common: c, + help: h, + keymap: keymap, + } + f.SetSize(c.Width, c.Height) + return f +} + +// SetSize implements common.Component. +func (f *Footer) SetSize(width, height int) { + f.common.SetSize(width, height) + f.help.Width = width - + f.common.Styles.Footer.GetHorizontalFrameSize() +} + +// Init implements tea.Model. +func (f *Footer) Init() tea.Cmd { + return nil +} + +// Update implements tea.Model. +func (f *Footer) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + return f, nil +} + +// View implements tea.Model. +func (f *Footer) View() string { + if f.keymap == nil { + return "" + } + s := f.common.Styles.Footer.Copy(). + Width(f.common.Width) + helpView := f.help.View(f.keymap) + return f.common.Zone.Mark( + "footer", + s.Render(helpView), + ) +} + +// ShortHelp returns the short help key bindings. +func (f *Footer) ShortHelp() []key.Binding { + return f.keymap.ShortHelp() +} + +// FullHelp returns the full help key bindings. +func (f *Footer) FullHelp() [][]key.Binding { + return f.keymap.FullHelp() +} + +// ShowAll returns whether the full help is shown. +func (f *Footer) ShowAll() bool { + return f.help.ShowAll +} + +// SetShowAll sets whether the full help is shown. +func (f *Footer) SetShowAll(show bool) { + f.help.ShowAll = show +} + +// Height returns the height of the footer. +func (f *Footer) Height() int { + return lipgloss.Height(f.View()) +} + +// ToggleFooterCmd sends a ToggleFooterMsg to show/hide the help footer. +func ToggleFooterCmd() tea.Msg { + return ToggleFooterMsg{} +} diff --git a/pkg/tui/components/formfield/formfield.go b/pkg/tui/components/formfield/formfield.go new file mode 100644 index 000000000000..ab00749fcd80 --- /dev/null +++ b/pkg/tui/components/formfield/formfield.go @@ -0,0 +1,38 @@ +package formfield + +import ( + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/styles" +) + +type FormField struct { + Label string + Required bool + Help string + Component tea.Model +} + +func NewFormField(common common.Common) *FormField { + return &FormField{} +} + +func (field *FormField) ViewLabel() string { + var label strings.Builder + if field.Required { + label.WriteString(styles.BoldTextStyle.Render(field.Label) + "*\n") + } else { + label.WriteString(styles.BoldTextStyle.Render(field.Label) + "\n") + } + + return label.String() +} + +func (field *FormField) ViewHelp() string { + var help strings.Builder + help.WriteString(styles.HintTextStyle.Render(field.Help) + "\n") + + return help.String() +} diff --git a/pkg/tui/components/header/header.go b/pkg/tui/components/header/header.go new file mode 100644 index 000000000000..f83c191ad3a7 --- /dev/null +++ b/pkg/tui/components/header/header.go @@ -0,0 +1,42 @@ +package header + +import ( + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" +) + +// Header represents a header component. +type Header struct { + common common.Common + text string +} + +// New creates a new header component. +func New(c common.Common, text string) *Header { + return &Header{ + common: c, + text: text, + } +} + +// SetSize implements common.Component. +func (h *Header) SetSize(width, height int) { + h.common.SetSize(width, height) +} + +// Init implements tea.Model. +func (h *Header) Init() tea.Cmd { + return nil +} + +// Update implements tea.Model. +func (h *Header) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + return h, nil +} + +// View implements tea.Model. +func (h *Header) View() string { + return h.common.Styles.ServerName.Render(strings.TrimSpace(h.text)) +} diff --git a/pkg/tui/components/selector/selector.go b/pkg/tui/components/selector/selector.go new file mode 100644 index 000000000000..e76ccd915498 --- /dev/null +++ b/pkg/tui/components/selector/selector.go @@ -0,0 +1,237 @@ +package selector + +import ( + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" +) + +// Selector is a list of items that can be selected. +type Selector struct { + list.Model + common common.Common + active int + filterState list.FilterState +} + +// IdentifiableItem is an item that can be identified by a string. Implements +// list.DefaultItem. +type IdentifiableItem interface { + list.DefaultItem + ID() string +} + +// ItemDelegate is a wrapper around list.ItemDelegate. +type ItemDelegate interface { + list.ItemDelegate +} + +// SelectMsg is a message that is sent when an item is selected. +type SelectMsg struct{ IdentifiableItem } + +// ActiveMsg is a message that is sent when an item is active but not selected. +type ActiveMsg struct{ IdentifiableItem } + +// New creates a new selector. +func New(common common.Common, items []IdentifiableItem, delegate ItemDelegate) *Selector { + itms := make([]list.Item, len(items)) + for i, item := range items { + itms[i] = item + } + l := list.New(itms, delegate, common.Width, common.Height) + s := &Selector{ + Model: l, + common: common, + } + s.SetSize(common.Width, common.Height) + return s +} + +// PerPage returns the number of items per page. +func (s *Selector) PerPage() int { + return s.Model.Paginator.PerPage +} + +// SetPage sets the current page. +func (s *Selector) SetPage(page int) { + s.Model.Paginator.Page = page +} + +// Page returns the current page. +func (s *Selector) Page() int { + return s.Model.Paginator.Page +} + +// TotalPages returns the total number of pages. +func (s *Selector) TotalPages() int { + return s.Model.Paginator.TotalPages +} + +// Select selects the item at the given index. +func (s *Selector) Select(index int) { + s.Model.Select(index) +} + +// SetShowTitle sets the show title flag. +func (s *Selector) SetShowTitle(show bool) { + s.Model.SetShowTitle(show) +} + +// SetShowHelp sets the show help flag. +func (s *Selector) SetShowHelp(show bool) { + s.Model.SetShowHelp(show) +} + +// SetShowStatusBar sets the show status bar flag. +func (s *Selector) SetShowStatusBar(show bool) { + s.Model.SetShowStatusBar(show) +} + +// DisableQuitKeybindings disables the quit keybindings. +func (s *Selector) DisableQuitKeybindings() { + s.Model.DisableQuitKeybindings() +} + +// SetShowFilter sets the show filter flag. +func (s *Selector) SetShowFilter(show bool) { + s.Model.SetShowFilter(show) +} + +// SetShowPagination sets the show pagination flag. +func (s *Selector) SetShowPagination(show bool) { + s.Model.SetShowPagination(show) +} + +// SetFilteringEnabled sets the filtering enabled flag. +func (s *Selector) SetFilteringEnabled(enabled bool) { + s.Model.SetFilteringEnabled(enabled) +} + +// SetSize implements common.Component. +func (s *Selector) SetSize(width, height int) { + s.common.SetSize(width, height) + s.Model.SetSize(width, height) +} + +// SetItems sets the items in the selector. +func (s *Selector) SetItems(items []IdentifiableItem) tea.Cmd { + its := make([]list.Item, len(items)) + for i, item := range items { + its[i] = item + } + return s.Model.SetItems(its) +} + +// Index returns the index of the selected item. +func (s *Selector) Index() int { + return s.Model.Index() +} + +// Init implements tea.Model. +func (s *Selector) Init() tea.Cmd { + return s.activeCmd +} + +// Update implements tea.Model. +func (s *Selector) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + cmds := make([]tea.Cmd, 0) + switch msg := msg.(type) { + case tea.MouseMsg: + switch msg.Type { + case tea.MouseWheelUp: + s.Model.CursorUp() + case tea.MouseWheelDown: + s.Model.CursorDown() + case tea.MouseLeft: + curIdx := s.Model.Index() + for i, item := range s.Model.Items() { + item, _ := item.(IdentifiableItem) + // Check each item to see if it's in bounds. + if item != nil && s.common.Zone.Get(item.ID()).InBounds(msg) { + if i == curIdx { + cmds = append(cmds, s.selectCmd) + } else { + s.Model.Select(i) + } + break + } + } + } + case tea.KeyMsg: + filterState := s.Model.FilterState() + switch { + case key.Matches(msg, s.common.KeyMap.Help): + if filterState == list.Filtering { + return s, tea.Batch(cmds...) + } + case key.Matches(msg, s.common.KeyMap.Select): + if filterState != list.Filtering { + cmds = append(cmds, s.selectCmd) + } + } + case list.FilterMatchesMsg: + cmds = append(cmds, s.activeFilterCmd) + } + m, cmd := s.Model.Update(msg) + s.Model = m + if cmd != nil { + cmds = append(cmds, cmd) + } + // Track filter state and update active item when filter state changes. + filterState := s.Model.FilterState() + if s.filterState != filterState { + cmds = append(cmds, s.activeFilterCmd) + } + s.filterState = filterState + // Send ActiveMsg when index change. + if s.active != s.Model.Index() { + cmds = append(cmds, s.activeCmd) + } + s.active = s.Model.Index() + return s, tea.Batch(cmds...) +} + +// View implements tea.Model. +func (s *Selector) View() string { + return s.Model.View() +} + +// SelectItem is a command that selects the currently active item. +func (s *Selector) SelectItem() tea.Msg { + return s.selectCmd() +} + +func (s *Selector) selectCmd() tea.Msg { + item := s.Model.SelectedItem() + i, ok := item.(IdentifiableItem) + if !ok { + return SelectMsg{} + } + return SelectMsg{i} +} + +func (s *Selector) activeCmd() tea.Msg { + item := s.Model.SelectedItem() + i, ok := item.(IdentifiableItem) + if !ok { + return ActiveMsg{} + } + return ActiveMsg{i} +} + +func (s *Selector) activeFilterCmd() tea.Msg { + // Here we use VisibleItems because when list.FilterMatchesMsg is sent, + // VisibleItems is the only way to get the list of filtered items. The list + // bubble should export something like list.FilterMatchesMsg.Items(). + items := s.Model.VisibleItems() + if len(items) == 0 { + return nil + } + item := items[0] + i, ok := item.(IdentifiableItem) + if !ok { + return nil + } + return ActiveMsg{i} +} diff --git a/pkg/tui/components/statusbar/statusbar.go b/pkg/tui/components/statusbar/statusbar.go new file mode 100644 index 000000000000..a486784bf98f --- /dev/null +++ b/pkg/tui/components/statusbar/statusbar.go @@ -0,0 +1,88 @@ +package statusbar + +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/muesli/reflow/truncate" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" +) + +// StatusBarMsg is a message sent to the status bar. +type StatusBarMsg struct { + Key string + Value string + Info string + Branch string +} + +// StatusBar is a status bar model. +type StatusBar struct { + common common.Common + msg StatusBarMsg +} + +// Model is an interface that supports setting the status bar information. +type Model interface { + StatusBarValue() string + StatusBarInfo() string +} + +// New creates a new status bar component. +func New(c common.Common) *StatusBar { + s := &StatusBar{ + common: c, + } + return s +} + +// SetSize implements common.Component. +func (s *StatusBar) SetSize(width, height int) { + s.common.Width = width + s.common.Height = height +} + +// Init implements tea.Model. +func (s *StatusBar) Init() tea.Cmd { + return nil +} + +// Update implements tea.Model. +func (s *StatusBar) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case StatusBarMsg: + s.msg = msg + } + return s, nil +} + +// View implements tea.Model. +func (s *StatusBar) View() string { + st := s.common.Styles + w := lipgloss.Width + help := s.common.Zone.Mark( + "repo-help", + st.StatusBarHelp.Render("? Help"), + ) + key := st.StatusBarKey.Render(s.msg.Key) + info := "" + if s.msg.Info != "" { + info = st.StatusBarInfo.Render(s.msg.Info) + } + branch := st.StatusBarBranch.Render(s.msg.Branch) + maxWidth := s.common.Width - w(key) - w(info) - w(branch) - w(help) + v := truncate.StringWithTail(s.msg.Value, uint(maxWidth-st.StatusBarValue.GetHorizontalFrameSize()), "ā€¦") + value := st.StatusBarValue. + Width(maxWidth). + Render(v) + + return lipgloss.NewStyle().MaxWidth(s.common.Width). + Render( + lipgloss.JoinHorizontal(lipgloss.Top, + key, + value, + info, + branch, + help, + ), + ) +} diff --git a/pkg/tui/components/tabs/tabs.go b/pkg/tui/components/tabs/tabs.go new file mode 100644 index 000000000000..b3ad0ae1ab8b --- /dev/null +++ b/pkg/tui/components/tabs/tabs.go @@ -0,0 +1,122 @@ +package tabs + +import ( + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" +) + +// SelectTabMsg is a message that contains the index of the tab to select. +type SelectTabMsg int + +// ActiveTabMsg is a message that contains the index of the current active tab. +type ActiveTabMsg int + +// Tabs is bubbletea component that displays a list of tabs. +type Tabs struct { + common common.Common + tabs []string + activeTab int + TabSeparator lipgloss.Style + TabInactive lipgloss.Style + TabActive lipgloss.Style + TabDot lipgloss.Style + UseDot bool +} + +// New creates a new Tabs component. +func New(c common.Common, tabs []string) *Tabs { + r := &Tabs{ + common: c, + tabs: tabs, + activeTab: 0, + TabSeparator: c.Styles.TabSeparator, + TabInactive: c.Styles.TabInactive, + TabActive: c.Styles.TabActive, + } + return r +} + +// SetSize implements common.Component. +func (t *Tabs) SetSize(width, height int) { + t.common.SetSize(width, height) +} + +// Init implements tea.Model. +func (t *Tabs) Init() tea.Cmd { + t.activeTab = 0 + return nil +} + +// Update implements tea.Model. +func (t *Tabs) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + cmds := make([]tea.Cmd, 0) + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "tab": + t.activeTab = (t.activeTab + 1) % len(t.tabs) + cmds = append(cmds, t.activeTabCmd) + case "shift+tab": + t.activeTab = (t.activeTab - 1 + len(t.tabs)) % len(t.tabs) + cmds = append(cmds, t.activeTabCmd) + } + case tea.MouseMsg: + if msg.Type == tea.MouseLeft { + for i, tab := range t.tabs { + if t.common.Zone.Get(tab).InBounds(msg) { + t.activeTab = i + cmds = append(cmds, t.activeTabCmd) + } + } + } + case SelectTabMsg: + tab := int(msg) + if tab >= 0 && tab < len(t.tabs) { + t.activeTab = int(msg) + } + } + return t, tea.Batch(cmds...) +} + +// View implements tea.Model. +func (t *Tabs) View() string { + s := strings.Builder{} + sep := t.TabSeparator + for i, tab := range t.tabs { + style := t.TabInactive.Copy() + prefix := " " + if i == t.activeTab { + style = t.TabActive.Copy() + prefix = t.TabDot.Render("ā€¢ ") + } + if t.UseDot { + s.WriteString(prefix) + } + s.WriteString( + t.common.Zone.Mark( + tab, + style.Render(tab), + ), + ) + if i != len(t.tabs)-1 { + s.WriteString(sep.String()) + } + } + return lipgloss.NewStyle(). + MaxWidth(t.common.Width). + Render(s.String()) +} + +func (t *Tabs) activeTabCmd() tea.Msg { + return ActiveTabMsg(t.activeTab) +} + +// SelectTabCmd is a bubbletea command that selects the tab at the given index. +func SelectTabCmd(tab int) tea.Cmd { + return func() tea.Msg { + return SelectTabMsg(tab) + } +} diff --git a/pkg/tui/components/textinput/textinput.go b/pkg/tui/components/textinput/textinput.go new file mode 100644 index 000000000000..14a1675cac08 --- /dev/null +++ b/pkg/tui/components/textinput/textinput.go @@ -0,0 +1,51 @@ +package textinput + +import ( + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" +) + +type ( + errMsg error +) + +type TextInput struct { + textInput textinput.Model + err error +} + +func New(placeholder string) TextInput { + ti := textinput.New() + ti.Placeholder = placeholder + ti.Focus() + ti.CharLimit = 156 + ti.Width = 60 + + return TextInput{ + textInput: ti, + err: nil, + } +} + +func (m TextInput) Init() tea.Cmd { + return textinput.Blink +} + +func (m TextInput) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + + switch msg := msg.(type) { + + // We handle errors just like any other message + case errMsg: + m.err = msg + return m, nil + } + + m.textInput, cmd = m.textInput.Update(msg) + return m, cmd +} + +func (m TextInput) View() string { + return m.textInput.View() +} diff --git a/pkg/tui/components/textinputs/textinputs.go b/pkg/tui/components/textinputs/textinputs.go new file mode 100644 index 000000000000..d0e68c6ba30f --- /dev/null +++ b/pkg/tui/components/textinputs/textinputs.go @@ -0,0 +1,229 @@ +package textinputs + +// from https://github.com/charmbracelet/bubbletea/blob/master/examples/textinputs/main.go + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +var ( + focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) + blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) + noStyle = lipgloss.NewStyle() + helpStyle = blurredStyle.Copy() + // cursorStyle = focusedStyle.Copy() + // cursorModeHelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244")) + + focusedButton = focusedStyle.Copy().Render("[ Next ]") + blurredButton = fmt.Sprintf("[ %s ]", blurredStyle.Render("Next")) + focusedSkipButton = lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Render("[ Run with defaults ]") + blurredSkipButton = fmt.Sprintf("[ %s ]", lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render("Run with defaults")) +) + +// SelectNextMsg used for emitting events when the 'Next' button is selected. +type SelectNextMsg int + +// SelectSkipMsg used for emitting events when the 'Skip' button is selected. +type SelectSkipMsg int + +type Model struct { + focusIndex int + inputs []textinput.Model + configs []InputConfig + // cursorMode cursor.Mode + skipButton bool +} + +type InputConfig struct { + Label string + Key string + Help string + Required bool + Placeholder string +} + +func (m Model) GetInputs() map[string]string { + inputs := make(map[string]string) + + for i, input := range m.inputs { + inputs[m.configs[i].Key] = input.Value() + } + + return inputs +} + +func (m Model) GetLabels() map[string]string { + labels := make(map[string]string) + + for _, config := range m.configs { + labels[config.Key] = config.Label + } + + return labels +} + +func New(config []InputConfig) Model { + m := Model{ + inputs: make([]textinput.Model, len(config)), + } + + for i, conf := range config { + input := textinput.New() + input.Placeholder = conf.Placeholder + + if i == 0 { + input.Focus() + input.TextStyle = focusedStyle + input.PromptStyle = focusedStyle + } + + m.inputs[i] = input + } + + m.configs = config + return m +} + +func (m Model) Init() tea.Cmd { + return textinput.Blink +} + +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + // Set focus to next input + case "enter", "up", "down": + s := msg.String() + + // Did the user press enter while the submit or skip button was focused? + // If so, emit the appropriate command. + if s == "enter" && m.focusIndex == len(m.inputs) { + return m, func() tea.Msg { return SelectNextMsg(0) } + } else if s == "enter" && m.focusIndex == -1 { + return m, func() tea.Msg { return SelectSkipMsg(0) } + } + + // Cycle indexes + if s == "up" { + m.focusIndex-- + } else { + m.focusIndex++ + } + + if m.focusIndex > len(m.inputs) { + m.focusIndex = 0 + } else if !m.skipButton && m.focusIndex < 0 { + m.focusIndex = len(m.inputs) + } else if m.skipButton && m.focusIndex < -1 { + m.focusIndex = len(m.inputs) + } + + cmds := make([]tea.Cmd, len(m.inputs)) + for i := 0; i < len(m.inputs); i++ { + if i == m.focusIndex { + // Set focused state + cmds[i] = m.focusInput(i) + continue + } + // Remove focused state + m.unfocusInput(i) + } + + return m, tea.Batch(cmds...) + } + } + + // Handle character input and blinking + cmd := m.updateInputs(msg) + + return m, cmd +} + +func (m *Model) updateInputs(msg tea.Msg) tea.Cmd { + cmds := make([]tea.Cmd, len(m.inputs)) + + // Only text inputs with Focus() set will respond, so it's safe to simply + // update all of them here without any further logic. + for i := range m.inputs { + m.inputs[i], cmds[i] = m.inputs[i].Update(msg) + } + + return tea.Batch(cmds...) +} + +func (m Model) View() string { + var b strings.Builder + + if m.skipButton { + button := &blurredSkipButton + if m.focusIndex == -1 { + button = &focusedSkipButton + } + fmt.Fprintf(&b, "%s\n\n\n", *button) + } + + for i := range m.inputs { + if m.configs[i].Label != "" { + b.WriteString(m.GetLabel(m.configs[i])) + } + + b.WriteString(m.inputs[i].View()) + b.WriteRune('\n') + if i < len(m.inputs)-1 { + b.WriteRune('\n') + } + } + + button := &blurredButton + if m.focusIndex == len(m.inputs) { + button = &focusedButton + } + fmt.Fprintf(&b, "\n\n%s\n\n", *button) + + return b.String() +} + +func (m Model) GetLabel(c InputConfig) string { + var label strings.Builder + + label.WriteString(c.Label) + if c.Required { + label.WriteString("*") + } + + if len(c.Help) > 0 { + label.WriteString("\n" + helpStyle.Render(c.Help)) + } + + label.WriteString("\n") + return label.String() +} + +func (m Model) SetSkip(skip bool) Model { + m.skipButton = skip + if m.skipButton { + if len(m.inputs) > 0 { + m.unfocusInput(0) + } + m.focusIndex = -1 + } + return m +} + +func (m *Model) unfocusInput(index int) { + m.inputs[index].Blur() + m.inputs[index].PromptStyle = noStyle + m.inputs[index].TextStyle = noStyle +} + +func (m *Model) focusInput(index int) tea.Cmd { + m.inputs[index].PromptStyle = focusedStyle + m.inputs[index].TextStyle = focusedStyle + return m.inputs[index].Focus() +} diff --git a/pkg/tui/components/viewport/viewport.go b/pkg/tui/components/viewport/viewport.go new file mode 100644 index 000000000000..77fa618285b7 --- /dev/null +++ b/pkg/tui/components/viewport/viewport.go @@ -0,0 +1,97 @@ +package viewport + +import ( + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" +) + +// Viewport represents a viewport component. +type Viewport struct { + common common.Common + *viewport.Model +} + +// New returns a new Viewport. +func New(c common.Common) *Viewport { + vp := viewport.New(c.Width, c.Height) + vp.MouseWheelEnabled = true + return &Viewport{ + common: c, + Model: &vp, + } +} + +// SetSize implements common.Component. +func (v *Viewport) SetSize(width, height int) { + v.common.SetSize(width, height) + v.Model.Width = width + v.Model.Height = height +} + +// Init implements tea.Model. +func (v *Viewport) Init() tea.Cmd { + return nil +} + +// Update implements tea.Model. +func (v *Viewport) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + vp, cmd := v.Model.Update(msg) + v.Model = &vp + return v, cmd +} + +// View implements tea.Model. +func (v *Viewport) View() string { + return v.Model.View() +} + +// SetContent sets the viewport's content. +func (v *Viewport) SetContent(content string) { + v.Model.SetContent(content) +} + +// GotoTop moves the viewport to the top of the log. +func (v *Viewport) GotoTop() { + v.Model.GotoTop() +} + +// GotoBottom moves the viewport to the bottom of the log. +func (v *Viewport) GotoBottom() { + v.Model.GotoBottom() +} + +// HalfViewDown moves the viewport down by half the viewport height. +func (v *Viewport) HalfViewDown() { + v.Model.HalfViewDown() +} + +// HalfViewUp moves the viewport up by half the viewport height. +func (v *Viewport) HalfViewUp() { + v.Model.HalfViewUp() +} + +// ViewUp moves the viewport up by a page. +func (v *Viewport) ViewUp() []string { + return v.Model.ViewUp() +} + +// ViewDown moves the viewport down by a page. +func (v *Viewport) ViewDown() []string { + return v.Model.ViewDown() +} + +// LineUp moves the viewport up by the given number of lines. +func (v *Viewport) LineUp(n int) []string { + return v.Model.LineUp(n) +} + +// LineDown moves the viewport down by the given number of lines. +func (v *Viewport) LineDown(n int) []string { + return v.Model.LineDown(n) +} + +// ScrollPercent returns the viewport's scroll percentage. +func (v *Viewport) ScrollPercent() float64 { + return v.Model.ScrollPercent() +} diff --git a/pkg/tui/keymap/keymap.go b/pkg/tui/keymap/keymap.go new file mode 100644 index 000000000000..bb3cb8c2a8d8 --- /dev/null +++ b/pkg/tui/keymap/keymap.go @@ -0,0 +1,217 @@ +package keymap + +import "github.com/charmbracelet/bubbles/key" + +// KeyMap is a map of key bindings for the UI. +type KeyMap struct { + Quit key.Binding + CmdQuit key.Binding + Up key.Binding + Down key.Binding + UpDown key.Binding + LeftRight key.Binding + Arrows key.Binding + Select key.Binding + Section key.Binding + Back key.Binding + PrevPage key.Binding + NextPage key.Binding + Help key.Binding + + SelectItem key.Binding + BackItem key.Binding + + Copy key.Binding +} + +// DefaultKeyMap returns the default key map. +func DefaultKeyMap() *KeyMap { + km := new(KeyMap) + + km.Quit = key.NewBinding( + key.WithKeys( + "ctrl+c", + ), + key.WithHelp( + "ctrl+c", + "quit", + ), + ) + + km.CmdQuit = key.NewBinding( + key.WithKeys( + "q", + "ctrl+c", + ), + key.WithHelp( + "q", + "quit", + ), + ) + + km.Up = key.NewBinding( + key.WithKeys( + "up", + "k", + ), + key.WithHelp( + "ā†‘/k", + "up", + ), + ) + + km.Down = key.NewBinding( + key.WithKeys( + "down", + "j", + ), + key.WithHelp( + "ā†“/j", + "down", + ), + ) + + km.UpDown = key.NewBinding( + key.WithKeys( + "up", + "down", + "k", + "j", + ), + key.WithHelp( + "ā†‘ā†“", + "navigate", + ), + ) + + km.LeftRight = key.NewBinding( + key.WithKeys( + "left", + "h", + "right", + "l", + ), + key.WithHelp( + "ā†ā†’", + "navigate", + ), + ) + + km.Arrows = key.NewBinding( + key.WithKeys( + "up", + "right", + "down", + "left", + "k", + "j", + "h", + "l", + ), + key.WithHelp( + "ā†‘ā†ā†“ā†’", + "navigate", + ), + ) + + km.Select = key.NewBinding( + key.WithKeys( + "enter", + ), + key.WithHelp( + "enter", + "select", + ), + ) + + km.Section = key.NewBinding( + key.WithKeys( + "tab", + "shift+tab", + ), + key.WithHelp( + "tab", + "section", + ), + ) + + km.Back = key.NewBinding( + key.WithKeys( + "esc", + ), + key.WithHelp( + "esc", + "back", + ), + ) + + km.PrevPage = key.NewBinding( + key.WithKeys( + "pgup", + "b", + "u", + ), + key.WithHelp( + "pgup", + "prev page", + ), + ) + + km.NextPage = key.NewBinding( + key.WithKeys( + "pgdown", + "f", + "d", + ), + key.WithHelp( + "pgdn", + "next page", + ), + ) + + km.Help = key.NewBinding( + key.WithKeys( + "?", + ), + key.WithHelp( + "?", + "toggle help", + ), + ) + + km.SelectItem = key.NewBinding( + key.WithKeys( + "l", + "right", + ), + key.WithHelp( + "ā†’", + "select", + ), + ) + + km.BackItem = key.NewBinding( + key.WithKeys( + "h", + "left", + "backspace", + ), + key.WithHelp( + "ā†", + "back", + ), + ) + + km.Copy = key.NewBinding( + key.WithKeys( + "c", + "ctrl+c", + ), + key.WithHelp( + "c", + "copy text", + ), + ) + + return km +} diff --git a/pkg/tui/pages/contact_enterprise/contact_enterprise.go b/pkg/tui/pages/contact_enterprise/contact_enterprise.go new file mode 100644 index 000000000000..4f345e0c840e --- /dev/null +++ b/pkg/tui/pages/contact_enterprise/contact_enterprise.go @@ -0,0 +1,60 @@ +package contact_enterprise + +import ( + "strings" + + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/styles" +) + +type ContactEnterprise struct { + common.Common + viewed bool +} + +var ( + linkStyle = lipgloss.NewStyle().Foreground( + lipgloss.Color("28")) // green +) + +func New(c common.Common) *ContactEnterprise { + return &ContactEnterprise{ + Common: c, + viewed: false, + } +} + +func (m *ContactEnterprise) Init() tea.Cmd { + return nil +} + +func (m *ContactEnterprise) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if m.viewed { + return m, tea.Quit + } + + return m, func() tea.Msg { return nil } +} + +func (m *ContactEnterprise) View() string { + + s := strings.Builder{} + s.WriteString("Interested in Trufflehog enterprise?\n") + s.WriteString(linkStyle.Render("šŸ”— https://trufflesecurity.com/contact")) + + m.viewed = true + return styles.AppStyle.Render(s.String()) +} + +func (m *ContactEnterprise) ShortHelp() []key.Binding { + // TODO: actually return something + return nil +} + +func (m *ContactEnterprise) FullHelp() [][]key.Binding { + // TODO: actually return something + return nil +} diff --git a/pkg/tui/pages/source_configure/item.go b/pkg/tui/pages/source_configure/item.go new file mode 100644 index 000000000000..70110b673b06 --- /dev/null +++ b/pkg/tui/pages/source_configure/item.go @@ -0,0 +1,23 @@ +package source_configure + +type Item struct { + title string + description string +} + +func (i Item) ID() string { return i.title } + +func (i Item) Title() string { + return i.title +} +func (i Item) Description() string { + return i.description +} + +func (i Item) SetDescription(d string) Item { + i.description = d + return i +} + +// We shouldn't be filtering for these list items. +func (i Item) FilterValue() string { return "" } diff --git a/pkg/tui/pages/source_configure/run_component.go b/pkg/tui/pages/source_configure/run_component.go new file mode 100644 index 000000000000..db700259fb31 --- /dev/null +++ b/pkg/tui/pages/source_configure/run_component.go @@ -0,0 +1,118 @@ +package source_configure + +import ( + "strings" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/styles" +) + +type SetArgsMsg string + +type RunComponent struct { + common.Common + parent *SourceConfigure + reviewList list.Model + reviewListItems []list.Item +} + +func NewRunComponent(common common.Common, parent *SourceConfigure) *RunComponent { + // Make list of SourceItems. + listItems := []list.Item{ + Item{title: "šŸ”Ž Source configuration"}, + Item{title: "šŸ½ TruffleHog configuration"}, + Item{title: "šŸ’ø Sales pitch", description: "\tContinuous monitoring, state tracking, remediations, and more\n\tšŸ”— https://trufflesecurity.com/trufflehog"}, + } + + // Setup list + delegate := list.NewDefaultDelegate() + delegate.Styles.SelectedTitle.Foreground(lipgloss.Color("white")) + delegate.Styles.SelectedDesc.Foreground(lipgloss.Color("white")) + delegate.SetHeight(3) + + reviewList := list.New(listItems, delegate, common.Width, common.Height) + + reviewList.SetShowTitle(false) + reviewList.SetShowStatusBar(false) + reviewList.SetFilteringEnabled(false) + + return &RunComponent{ + Common: common, + parent: parent, + reviewList: reviewList, + reviewListItems: listItems, + } +} + +func (m *RunComponent) Init() tea.Cmd { + return nil +} + +func (m *RunComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + h, v := styles.AppStyle.GetFrameSize() + m.reviewList.SetSize(msg.Width-h, msg.Height/2-v) + case tea.KeyMsg: + if msg.Type == tea.KeyEnter { + command := m.parent.sourceFields.Cmd() + if m.parent.truffleFields.Cmd() != "" { + command += " " + m.parent.truffleFields.Cmd() + } + cmd := func() tea.Msg { return SetArgsMsg(command) } + return m, cmd + } + } + if len(m.reviewListItems) > 0 && m.parent != nil && m.parent.sourceFields != nil { + m.reviewListItems[0] = m.reviewListItems[0].(Item).SetDescription(m.parent.sourceFields.Summary()) + m.reviewListItems[1] = m.reviewListItems[1].(Item).SetDescription(m.parent.truffleFields.Summary()) + } + var cmd tea.Cmd + m.reviewList, cmd = m.reviewList.Update(msg) + return m, tea.Batch(cmd) +} + +func (m *RunComponent) View() string { + var view strings.Builder + + view.WriteString("\nšŸ”Ž Source configuration\n") + view.WriteString(m.parent.sourceFields.Summary()) + + view.WriteString("\nšŸ½ TruffleHog configuration\n") + view.WriteString(m.parent.truffleFields.Summary()) + + view.WriteString("\nšŸ’ø Sales pitch\n") + view.WriteString("\tContinuous monitoring, state tracking, remediations, and more\n") + view.WriteString("\tšŸ”— https://trufflesecurity.com/trufflehog\n\n") + + view.WriteString(styles.BoldTextStyle.Render("\n\nšŸ· Run TruffleHog for "+m.parent.configTabSource) + " šŸ·\n\n") + + view.WriteString("Generated TruffleHog command\n") + view.WriteString(styles.HintTextStyle.Render("Save this if you want to run it again later!") + "\n") + + command := m.parent.sourceFields.Cmd() + if m.parent.truffleFields.Cmd() != "" { + command += " " + m.parent.truffleFields.Cmd() + } + view.WriteString(styles.CodeTextStyle.Render(command)) + + focusedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("205")) + view.WriteString("\n\n" + focusedStyle.Render("[ Run TruffleHog ]") + "\n\n") + + // view.WriteString(m.reviewList.View()) + return view.String() +} + +func (m *RunComponent) ShortHelp() []key.Binding { + // TODO: actually return something + return nil +} + +func (m *RunComponent) FullHelp() [][]key.Binding { + // TODO: actually return something + return nil +} diff --git a/pkg/tui/pages/source_configure/source_component.go b/pkg/tui/pages/source_configure/source_component.go new file mode 100644 index 000000000000..f5ad69e3b6c9 --- /dev/null +++ b/pkg/tui/pages/source_configure/source_component.go @@ -0,0 +1,71 @@ +package source_configure + +import ( + "strings" + + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/sources" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/styles" +) + +type SourceComponent struct { + common.Common + parent *SourceConfigure + form tea.Model +} + +func NewSourceComponent(common common.Common, parent *SourceConfigure) *SourceComponent { + return &SourceComponent{ + Common: common, + parent: parent, + } +} + +func (m *SourceComponent) SetForm(form tea.Model) { + m.form = form +} + +func (m *SourceComponent) Init() tea.Cmd { + return nil +} + +func (m *SourceComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + // TODO: Add a focus variable. + if m.form != nil { + model, cmd := m.form.Update(msg) + m.form = model + return m, cmd + } + return m, nil +} + +func (m *SourceComponent) View() string { + var view strings.Builder + + view.WriteString(styles.BoldTextStyle.Render("\nConfiguring "+styles.PrimaryTextStyle.Render(m.parent.configTabSource)) + "\n") + + view.WriteString(styles.HintTextStyle.Render("* required field") + "\n\n") + + sourceNote := sources.GetSourceNotes(m.parent.configTabSource) + if len(sourceNote) > 0 { + view.WriteString("ā­ " + sourceNote + " ā­\n\n") + } + + if m.form != nil { + view.WriteString(m.form.View()) + view.WriteString("\n") + } + return view.String() +} + +func (m *SourceComponent) ShortHelp() []key.Binding { + // TODO: actually return something + return nil +} + +func (m *SourceComponent) FullHelp() [][]key.Binding { + // TODO: actually return something + return nil +} diff --git a/pkg/tui/pages/source_configure/source_configure.go b/pkg/tui/pages/source_configure/source_configure.go new file mode 100644 index 000000000000..d543a3f01c1c --- /dev/null +++ b/pkg/tui/pages/source_configure/source_configure.go @@ -0,0 +1,139 @@ +package source_configure + +import ( + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/tabs" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/textinputs" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/sources" +) + +type SetSourceMsg struct { + Source string +} + +type tab int + +const ( + configTab tab = iota + truffleConfigTab + runTab +) + +func (t tab) String() string { + return []string{ + "1. Source Configuration", + "2. TruffleHog Configuration", + "3. Run", + }[t] +} + +type SourceConfigure struct { + common.Common + activeTab tab + tabs *tabs.Tabs + configTabSource string + tabComponents []common.Component + sourceFields sources.CmdModel + truffleFields sources.CmdModel +} + +func (m SourceConfigure) Init() tea.Cmd { + return m.tabs.Init() +} + +func New(c common.Common) *SourceConfigure { + conf := SourceConfigure{Common: c, truffleFields: GetTrufflehogConfiguration()} + conf.tabs = tabs.New(c, []string{configTab.String(), truffleConfigTab.String(), runTab.String()}) + + conf.tabComponents = []common.Component{ + configTab: NewSourceComponent(c, &conf), + truffleConfigTab: NewTrufflehogComponent(c, &conf), + runTab: NewRunComponent(c, &conf), + } + return &conf +} + +func (m *SourceConfigure) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + for i := range m.tabComponents { + model, cmd := m.tabComponents[i].Update(msg) + m.tabComponents[i] = model.(common.Component) + cmds = append(cmds, cmd) + } + + case tabs.ActiveTabMsg: + m.activeTab = tab(msg) + t, cmd := m.tabs.Update(msg) + m.tabs = t.(*tabs.Tabs) + + if cmd != nil { + cmds = append(cmds, cmd) + } + case tabs.SelectTabMsg: + m.activeTab = tab(msg) + t, cmd := m.tabs.Update(msg) + m.tabs = t.(*tabs.Tabs) + + if cmd != nil { + cmds = append(cmds, cmd) + } + case tea.KeyMsg: + t, cmd := m.tabs.Update(msg) + m.tabs = t.(*tabs.Tabs) + if cmd != nil { + cmds = append(cmds, cmd) + } + case SetSourceMsg: + m.configTabSource = msg.Source + // TODO: Use actual messages or something? + m.tabComponents[truffleConfigTab].(*TrufflehogComponent).SetForm(m.truffleFields) + fields := sources.GetSourceFields(m.configTabSource) + + if fields != nil { + m.sourceFields = fields + m.tabComponents[configTab].(*SourceComponent).SetForm(fields) + } + + case textinputs.SelectNextMsg, textinputs.SelectSkipMsg: + if m.activeTab < runTab { + m.activeTab++ + } + t, cmd := m.tabs.Update(tabs.SelectTabMsg(int(m.activeTab))) + m.tabs = t.(*tabs.Tabs) + + if cmd != nil { + cmds = append(cmds, cmd) + } + } + + tab, cmd := m.tabComponents[m.activeTab].Update(msg) + m.tabComponents[m.activeTab] = tab.(common.Component) + if cmd != nil { + cmds = append(cmds, cmd) + } + + return m, tea.Batch(cmds...) +} + +func (m *SourceConfigure) View() string { + return lipgloss.JoinVertical(lipgloss.Top, + m.tabs.View(), + m.tabComponents[m.activeTab].View(), + ) +} + +func (m *SourceConfigure) ShortHelp() []key.Binding { + // TODO: actually return something + return nil +} + +func (m *SourceConfigure) FullHelp() [][]key.Binding { + // TODO: actually return something + return nil +} diff --git a/pkg/tui/pages/source_configure/trufflehog_component.go b/pkg/tui/pages/source_configure/trufflehog_component.go new file mode 100644 index 000000000000..13c8a56b3ed6 --- /dev/null +++ b/pkg/tui/pages/source_configure/trufflehog_component.go @@ -0,0 +1,66 @@ +package source_configure + +import ( + "strings" + + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/styles" +) + +type TrufflehogComponent struct { + common.Common + parent *SourceConfigure + form tea.Model +} + +func NewTrufflehogComponent(common common.Common, parent *SourceConfigure) *TrufflehogComponent { + return &TrufflehogComponent{ + Common: common, + parent: parent, + } +} + +func (m *TrufflehogComponent) SetForm(form tea.Model) { + m.form = form +} + +func (m *TrufflehogComponent) Init() tea.Cmd { + return nil +} + +func (m *TrufflehogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + // TODO: Add a focus variable. + if m.form != nil { + model, cmd := m.form.Update(msg) + m.form = model + + return m, cmd + } + return m, nil +} + +func (m *TrufflehogComponent) View() string { + var view strings.Builder + + view.WriteString(styles.BoldTextStyle.Render("\nConfiguring "+styles.PrimaryTextStyle.Render("TruffleHog")) + "\n") + view.WriteString(styles.HintTextStyle.Render("You can skip this completely and run with defaults") + "\n\n") + + if m.form != nil { + view.WriteString(m.form.View()) + view.WriteString("\n") + } + + return view.String() +} + +func (m *TrufflehogComponent) ShortHelp() []key.Binding { + // TODO: actually return something + return nil +} + +func (m *TrufflehogComponent) FullHelp() [][]key.Binding { + // TODO: actually return something + return nil +} diff --git a/pkg/tui/pages/source_configure/trufflehog_configure.go b/pkg/tui/pages/source_configure/trufflehog_configure.go new file mode 100644 index 000000000000..4d17efd8613d --- /dev/null +++ b/pkg/tui/pages/source_configure/trufflehog_configure.go @@ -0,0 +1,115 @@ +package source_configure + +import ( + "strconv" + "strings" + + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/textinputs" +) + +type truffleCmdModel struct { + textinputs.Model +} + +func GetTrufflehogConfiguration() truffleCmdModel { + verification := textinputs.InputConfig{ + Label: "Skip Verification", + Key: "no-verification", + Required: false, + Help: "Check if a suspected secret is real or not", + Placeholder: "false", + } + + verifiedResults := textinputs.InputConfig{ + Label: "Verified results", + Key: "only-verified", + Required: false, + Help: "Return only verified results", + Placeholder: "false", + } + + jsonOutput := textinputs.InputConfig{ + Label: "JSON output", + Key: "json", + Required: false, + Help: "Output results to JSON", + Placeholder: "false", + } + + excludeDetectors := textinputs.InputConfig{ + Label: "Exclude detectors", + Key: "exclude_detectors", + Required: false, + Help: "Comma separated list of detector types to exclude. Protobuf name or IDs may be used, as well as ranges. IDs defined here take precedence over the include list.", + Placeholder: "", + } + + concurrency := textinputs.InputConfig{ + Label: "Concurrency", + Key: "concurrency", + Required: false, + Help: "Number of concurrent workers.", + Placeholder: "1", + } + + return truffleCmdModel{textinputs.New([]textinputs.InputConfig{jsonOutput, verification, verifiedResults, excludeDetectors, concurrency}).SetSkip(true)} +} + +func (m truffleCmdModel) Cmd() string { + var command []string + inputs := m.GetInputs() + + if isTrue(inputs["json"]) { + command = append(command, "--json") + } + + if isTrue(inputs["no-verification"]) { + command = append(command, "--no-verification") + } + + if isTrue(inputs["only-verified"]) { + command = append(command, "--only-verified") + } + + if inputs["exclude_detectors"] != "" { + cmd := "--exclude-detectors=" + strings.ReplaceAll(inputs["exclude_detectors"], " ", "") + command = append(command, cmd) + } + + if inputs["concurrency"] != "" { + command = append(command, "--concurrency="+inputs["concurrency"]) + } + + return strings.Join(command, " ") +} + +func (m truffleCmdModel) Summary() string { + summary := strings.Builder{} + keys := []string{"no-verification", "only-verified", "json", "exclude_detectors", "concurrency"} + + inputs := m.GetInputs() + labels := m.GetLabels() + for _, key := range keys { + if inputs[key] != "" { + summary.WriteString("\t" + labels[key] + ": " + inputs[key] + "\n") + } + } + + if summary.Len() == 0 { + summary.WriteString("\tRunning with defaults\n") + + } + + summary.WriteString("\n") + return summary.String() +} + +func isTrue(val string) bool { + value := strings.ToLower(val) + isTrue, _ := strconv.ParseBool(value) + + if isTrue || value == "yes" || value == "y" { + return true + } + return false +} diff --git a/pkg/tui/pages/source_select/item.go b/pkg/tui/pages/source_select/item.go new file mode 100644 index 000000000000..dd57a1244480 --- /dev/null +++ b/pkg/tui/pages/source_select/item.go @@ -0,0 +1,32 @@ +package source_select + +type SourceItem struct { + title string + description string + enterprise bool +} + +func OssItem(title, description string) SourceItem { + return SourceItem{title, description, false} +} + +func EnterpriseItem(title, description string) SourceItem { + return SourceItem{title, description, true} +} + +func (i SourceItem) ID() string { return i.title } + +func (i SourceItem) Title() string { + if i.enterprise { + return "šŸ’ø " + i.title + } + return i.title +} +func (i SourceItem) Description() string { + if i.enterprise { + return i.description + " (Enterprise only)" + } + return i.description +} + +func (i SourceItem) FilterValue() string { return i.title + i.description } diff --git a/pkg/tui/pages/source_select/source_select.go b/pkg/tui/pages/source_select/source_select.go new file mode 100644 index 000000000000..94bd8b285b77 --- /dev/null +++ b/pkg/tui/pages/source_select/source_select.go @@ -0,0 +1,222 @@ +package source_select + +import ( + "time" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "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" +) + +// TODO: Review light theme styling +var ( + titleStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FFFDF5")). + Background(lipgloss.Color(styles.Colors["bronze"])). + Padding(0, 1) + + // FIXME: Hon pls help + errorStatusMessageStyle = lipgloss.NewStyle(). + Foreground(lipgloss.AdaptiveColor{Dark: "#ff0000"}). + Render + + selectedSourceItemStyle = lipgloss.NewStyle(). + Border(lipgloss.NormalBorder(), false, false, false, true). + BorderForeground(lipgloss.AdaptiveColor{Dark: styles.Colors["sprout"], Light: styles.Colors["bronze"]}). + Foreground(lipgloss.AdaptiveColor{Dark: styles.Colors["sprout"], Light: styles.Colors["fern"]}). + Padding(0, 0, 0, 1) + + selectedDescription = selectedSourceItemStyle.Copy(). + Foreground(lipgloss.AdaptiveColor{Dark: styles.Colors["sprout"], Light: styles.Colors["sprout"]}) +) + +type listKeyMap struct { + toggleHelpMenu key.Binding +} + +type ( + SourceSelect struct { + common.Common + sourcesList list.Model + keys *listKeyMap + delegateKeys *delegateKeyMap + selector *selector.Selector + } +) + +func New(c common.Common) *SourceSelect { + var ( + delegateKeys = newDelegateKeyMap() + listKeys = &listKeyMap{ + toggleHelpMenu: key.NewBinding( + key.WithKeys("H"), + key.WithHelp("H", "toggle help"), + ), + } + ) + + // Make list of SourceItems. + SourceItems := []list.Item{ + // Open source sources. + OssItem("Git", "Scan git repositories."), + OssItem("GitHub", "Scan GitHub repositories and/or organizations."), + OssItem("GitLab", "Scan GitLab repositories."), + OssItem("Filesystem", "Scan your filesystem by selecting what directories to scan."), + OssItem("AWS S3", "Scan Amazon S3 buckets."), + OssItem("CircleCI", "Scan CircleCI, a CI/CD platform."), + OssItem("Syslog", "Scan syslog, event data logs."), + OssItem("Docker", "Scan a Docker instance, a containerized application."), + OssItem("GCS (Google Cloud Storage)", "Scan a Google Cloud Storage instance."), + // Enterprise sources. + EnterpriseItem("Artifactory", "Scan JFrog Artifactory packages."), + EnterpriseItem("BitBucket", "Scan Atlassian's Git-based source code repository hosting service."), + EnterpriseItem("Buildkite", "Scan Buildkite, a CI/CD platform."), + EnterpriseItem("Confluence", "Scan Atlassian's web-based wiki and knowledge base."), + EnterpriseItem("Gerrit", "Scan Gerrit, a code collaboration tool"), + EnterpriseItem("Jenkins ", "Scan Jenkins, a CI/CD platform."), + EnterpriseItem("Jira", "Scan Atlassian's issue & project tracking software."), + EnterpriseItem("Slack", "Scan Slack, a messaging and communication platform."), + EnterpriseItem("Microsoft Teams", "Scan Microsoft Teams, a messaging and communication platform."), + EnterpriseItem("Microsoft Sharepoint", "Scan Microsoft Sharepoint, a collaboration and document management platform."), + EnterpriseItem("Google Drive", "Scan Google Drive, a cloud-based storage and file sync service."), + } + + // Setup list + delegate := newSourceItemDelegate(delegateKeys) + delegate.Styles.SelectedTitle = selectedSourceItemStyle + delegate.Styles.SelectedDesc = selectedDescription + + sourcesList := list.New(SourceItems, delegate, 0, 0) + sourcesList.Title = "Sources" + sourcesList.Styles.Title = titleStyle + sourcesList.StatusMessageLifetime = 10 * time.Second + + sourcesList.AdditionalFullHelpKeys = func() []key.Binding { + return []key.Binding{ + listKeys.toggleHelpMenu, + } + } + + sourcesList.SetShowStatusBar(false) + sel := selector.New(c, []selector.IdentifiableItem{}, delegate) + + return &SourceSelect{ + Common: c, + sourcesList: sourcesList, + keys: listKeys, + delegateKeys: delegateKeys, + selector: sel, + } +} + +func (m *SourceSelect) Init() tea.Cmd { + return nil +} + +func (m *SourceSelect) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + h, v := styles.AppStyle.GetFrameSize() + m.sourcesList.SetSize(msg.Width-h, msg.Height-v) + + case tea.KeyMsg: + // Don't match any of the keys below if we're actively filtering. + if m.sourcesList.FilterState() == list.Filtering { + break + } + + switch { + case key.Matches(msg, m.keys.toggleHelpMenu): + m.sourcesList.SetShowHelp(!m.sourcesList.ShowHelp()) + return m, nil + } + } + + // This will also call our delegate's update function. + newListModel, cmd := m.sourcesList.Update(msg) + m.sourcesList = newListModel + cmds = append(cmds, cmd) + + if m.selector != nil { + sel, cmd := m.selector.Update(msg) + m.selector = sel.(*selector.Selector) + cmds = append(cmds, cmd) + } + + return m, tea.Batch(cmds...) +} + +func (m *SourceSelect) View() string { + return styles.AppStyle.Render(m.sourcesList.View()) +} + +func (m *SourceSelect) ShortHelp() []key.Binding { + // TODO: actually return something + return nil +} + +func (m *SourceSelect) FullHelp() [][]key.Binding { + // TODO: actually return something + return nil +} + +func newSourceItemDelegate(keys *delegateKeyMap) list.DefaultDelegate { + d := list.NewDefaultDelegate() + + d.UpdateFunc = func(msg tea.Msg, m *list.Model) tea.Cmd { + selectedSourceItem, ok := m.SelectedItem().(SourceItem) + if !ok { + return nil + } + + if msg, ok := msg.(tea.KeyMsg); ok && key.Matches(msg, keys.choose) { + if selectedSourceItem.enterprise { + return m.NewStatusMessage(errorStatusMessageStyle( + "That's an enterprise only source. Learn more at trufflesecurity.com", + )) + } + + return func() tea.Msg { + return selector.SelectMsg{IdentifiableItem: selectedSourceItem} + } + } + return nil + } + + help := []key.Binding{keys.choose} + d.ShortHelpFunc = func() []key.Binding { return help } + d.FullHelpFunc = func() [][]key.Binding { return [][]key.Binding{help} } + + return d +} + +type delegateKeyMap struct { + choose key.Binding +} + +// Additional short help entries. This satisfies the help.KeyMap interface and +// is entirely optional. +func (d delegateKeyMap) ShortHelp() []key.Binding { + return []key.Binding{d.choose} +} + +// Additional full help entries. This satisfies the help.KeyMap interface and +// is entirely optional. +func (d delegateKeyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{{d.choose}} +} + +func newDelegateKeyMap() *delegateKeyMap { + return &delegateKeyMap{ + choose: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "choose"), + ), + } +} diff --git a/pkg/tui/pages/view_oss/view_oss.go b/pkg/tui/pages/view_oss/view_oss.go new file mode 100644 index 000000000000..1382900334a6 --- /dev/null +++ b/pkg/tui/pages/view_oss/view_oss.go @@ -0,0 +1,59 @@ +package view_oss + +import ( + "strings" + + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/styles" +) + +type ViewOSS struct { + common.Common + viewed bool +} + +var ( + linkStyle = lipgloss.NewStyle().Foreground( + lipgloss.Color("28")) // green +) + +func New(c common.Common) *ViewOSS { + return &ViewOSS{ + Common: c, + viewed: false, + } +} + +func (m *ViewOSS) Init() tea.Cmd { + return nil +} + +func (m *ViewOSS) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if m.viewed { + return m, tea.Quit + } + + return m, func() tea.Msg { return nil } +} + +func (m *ViewOSS) View() string { + s := strings.Builder{} + s.WriteString("View our open-source project on GitHub\n") + s.WriteString(linkStyle.Render("šŸ”— https://github.com/trufflesecurity/trufflehog ")) + + m.viewed = true + return styles.AppStyle.Render(s.String()) +} + +func (m *ViewOSS) ShortHelp() []key.Binding { + // TODO: actually return something + return nil +} + +func (m *ViewOSS) FullHelp() [][]key.Binding { + // TODO: actually return something + return nil +} diff --git a/pkg/tui/pages/wizard_intro/item.go b/pkg/tui/pages/wizard_intro/item.go new file mode 100644 index 000000000000..f9eef55d9ab3 --- /dev/null +++ b/pkg/tui/pages/wizard_intro/item.go @@ -0,0 +1,137 @@ +package wizard_intro + +import ( + "fmt" + "io" + "strings" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" +) + +// Item represents a single item in the selector. +type Item int + +// ID implements selector.IdentifiableItem. +func (i Item) ID() string { + return i.String() +} + +// Title returns the item title. Implements list.DefaultItem. +func (i Item) Title() string { return i.String() } + +// Description returns the item description. Implements list.DefaultItem. +func (i Item) Description() string { return "" } + +// FilterValue implements list.Item. +func (i Item) FilterValue() string { return i.Title() } + +// Command returns the item Command view. +func (i Item) Command() string { + return i.Title() +} + +// ItemDelegate is the delegate for the item. +type ItemDelegate struct { + common *common.Common +} + +// Width returns the item width. +func (d ItemDelegate) Width() int { + width := d.common.Styles.MenuItem.GetHorizontalFrameSize() + d.common.Styles.MenuItem.GetWidth() + return width +} + +// Height returns the item height. Implements list.ItemDelegate. +func (d ItemDelegate) Height() int { + height := d.common.Styles.MenuItem.GetVerticalFrameSize() + d.common.Styles.MenuItem.GetHeight() + return height +} + +// Spacing returns the spacing between items. Implements list.ItemDelegate. +func (d ItemDelegate) Spacing() int { return 1 } + +// Update implements list.ItemDelegate. +func (d ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { + idx := m.Index() + item, ok := m.SelectedItem().(Item) + if !ok { + return nil + } + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, d.common.KeyMap.Copy): + d.common.Copy.Copy(item.Command()) + return m.SetItem(idx, item) + } + } + return nil +} + +// Render implements list.ItemDelegate. +func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { + i := listItem.(Item) + s := strings.Builder{} + var matchedRunes []int + + // Conditions + var ( + isSelected = index == m.Index() + isFiltered = m.FilterState() == list.Filtering || m.FilterState() == list.FilterApplied + ) + + styles := d.common.Styles.RepoSelector.Normal + if isSelected { + styles = d.common.Styles.RepoSelector.Active + } + + title := i.Title() + title = common.TruncateString(title, m.Width()-styles.Base.GetHorizontalFrameSize()) + // if i.repo.IsPrivate() { + // title += " šŸ”’" + // } + if isSelected { + title += " " + } + updatedStr := " Updated" + if m.Width()-styles.Base.GetHorizontalFrameSize()-lipgloss.Width(updatedStr)-lipgloss.Width(title) <= 0 { + updatedStr = "" + } + updatedStyle := styles.Updated.Copy(). + Align(lipgloss.Right). + Width(m.Width() - styles.Base.GetHorizontalFrameSize() - lipgloss.Width(title)) + updated := updatedStyle.Render(updatedStr) + + if isFiltered && index < len(m.VisibleItems()) { + // Get indices of matched characters + matchedRunes = m.MatchesForItem(index) + } + + if isFiltered { + unmatched := styles.Title.Copy().Inline(true) + matched := unmatched.Copy().Underline(true) + title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched) + } + title = styles.Title.Render(title) + desc := i.Description() + desc = common.TruncateString(desc, m.Width()-styles.Base.GetHorizontalFrameSize()) + desc = styles.Desc.Render(desc) + + s.WriteString(lipgloss.JoinHorizontal(lipgloss.Bottom, title, updated)) + s.WriteRune('\n') + s.WriteString(desc) + s.WriteRune('\n') + cmd := common.TruncateString(i.Command(), m.Width()-styles.Base.GetHorizontalFrameSize()) + cmd = styles.Command.Render(cmd) + + s.WriteString(cmd) + fmt.Fprint(w, + d.common.Zone.Mark(i.ID(), + styles.Base.Render(s.String()), + ), + ) +} diff --git a/pkg/tui/pages/wizard_intro/wizard_intro.go b/pkg/tui/pages/wizard_intro/wizard_intro.go new file mode 100644 index 000000000000..dcc412f328fc --- /dev/null +++ b/pkg/tui/pages/wizard_intro/wizard_intro.go @@ -0,0 +1,109 @@ +package wizard_intro + +import ( + "strings" + + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "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" +) + +const ( + ScanSourceWithWizard Item = iota + // ScanSourceWithConfig + ViewHelpDocs + ViewOSSProject + EnterpriseInquire + Quit +) + +func (w Item) String() string { + switch w { + case ScanSourceWithWizard: + return "Scan a source using wizard" + //case ScanSourceWithConfig: + // return "Scan a source with a config file" + case ViewHelpDocs: + return "View help docs" + case ViewOSSProject: + return "View open-source project" + case EnterpriseInquire: + return "Inquire about TruffleHog Enterprise" + case Quit: + return "Quit" + } + panic("unreachable") +} + +type WizardIntro struct { + common.Common + selector *selector.Selector +} + +func New(cmn common.Common) *WizardIntro { + sel := selector.New(cmn, + []selector.IdentifiableItem{ + ScanSourceWithWizard, + // ScanSourceWithConfig, + ViewHelpDocs, + ViewOSSProject, + EnterpriseInquire, + Quit, + }, + ItemDelegate{&cmn}) + + return &WizardIntro{Common: cmn, selector: sel} +} + +func (m *WizardIntro) Init() tea.Cmd { + m.selector.Select(0) + return nil +} + +func (m *WizardIntro) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + cmds := make([]tea.Cmd, 0) + + s, cmd := m.selector.Update(msg) + m.selector = s.(*selector.Selector) + if cmd != nil { + cmds = append(cmds, cmd) + } + + return m, tea.Batch(cmds...) +} + +func (m *WizardIntro) View() string { + s := strings.Builder{} + s.WriteString("What do you want to do?\n\n") + + for i, selectorItem := range m.selector.Items() { + // Cast the interface to the concrete Item struct. + item := selectorItem.(Item) + if m.selector.Index() == i { + selectedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(styles.Colors["sprout"])) + s.WriteString(selectedStyle.Render(" (ā€¢) " + item.Title())) + } else { + s.WriteString(" ( ) " + item.Title()) + } + s.WriteString("\n") + } + + return styles.AppStyle.Render(s.String()) +} + +func (m *WizardIntro) ShortHelp() []key.Binding { + kb := make([]key.Binding, 0) + kb = append(kb, + m.Common.KeyMap.UpDown, + m.Common.KeyMap.Section, + ) + return kb +} + +func (m *WizardIntro) FullHelp() [][]key.Binding { + // TODO: actually return something + return nil +} diff --git a/pkg/tui/sources/circleci/circleci.go b/pkg/tui/sources/circleci/circleci.go new file mode 100644 index 000000000000..cd57a21d2a59 --- /dev/null +++ b/pkg/tui/sources/circleci/circleci.go @@ -0,0 +1,44 @@ +package circleci + +import ( + "strings" + + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/textinputs" +) + +type circleCiCmdModel struct { + textinputs.Model +} + +func GetFields() circleCiCmdModel { + token := textinputs.InputConfig{ + Label: "API Token", + Key: "token", + Required: true, + Placeholder: "top secret token", + } + + return circleCiCmdModel{textinputs.New([]textinputs.InputConfig{token})} +} + +func (m circleCiCmdModel) Cmd() string { + var command []string + command = append(command, "trufflehog", "circleci") + + inputs := m.GetInputs() + + if inputs["token"] != "" { + command = append(command, "--token="+inputs["token"]) + } + + return strings.Join(command, " ") +} + +func (m circleCiCmdModel) Summary() string { + inputs := m.GetInputs() + labels := m.GetLabels() + keys := []string{"token"} + + return common.SummarizeSource(keys, inputs, labels) +} diff --git a/pkg/tui/sources/docker/docker.go b/pkg/tui/sources/docker/docker.go new file mode 100644 index 000000000000..942459aa82d8 --- /dev/null +++ b/pkg/tui/sources/docker/docker.go @@ -0,0 +1,49 @@ +package docker + +import ( + "strings" + + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/textinputs" +) + +type dockerCmdModel struct { + textinputs.Model +} + +func GetFields() dockerCmdModel { + images := textinputs.InputConfig{ + Label: "Docker image(s)", + Key: "images", + Required: true, + Help: "Separate by space if multiple.", + Placeholder: "trufflesecurity/secrets", + } + + return dockerCmdModel{textinputs.New([]textinputs.InputConfig{images})} +} + +func (m dockerCmdModel) Cmd() string { + + var command []string + command = append(command, "trufflehog", "docker") + + inputs := m.GetInputs() + vals := inputs["images"] + if vals != "" { + images := strings.Fields(vals) + for _, image := range images { + command = append(command, "--image="+image) + } + } + + return strings.Join(command, " ") +} + +func (m dockerCmdModel) Summary() string { + inputs := m.GetInputs() + labels := m.GetLabels() + keys := []string{"images"} + + return common.SummarizeSource(keys, inputs, labels) +} diff --git a/pkg/tui/sources/filesystem/filesystem.go b/pkg/tui/sources/filesystem/filesystem.go new file mode 100644 index 000000000000..68b98c8c51cb --- /dev/null +++ b/pkg/tui/sources/filesystem/filesystem.go @@ -0,0 +1,45 @@ +package filesystem + +import ( + "strings" + + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/textinputs" +) + +type fsModel struct { + textinputs.Model +} + +func GetFields() fsModel { + path := textinputs.InputConfig{ + Label: "Path", + Key: "path", + Required: true, + Help: "Files and directories to scan. Separate by space if multiple.", + Placeholder: "path/to/file.txt path/to/another/dir", + } + + return fsModel{textinputs.New([]textinputs.InputConfig{path})} +} + +func (m fsModel) Cmd() string { + var command []string + command = append(command, "trufflehog", "filesystem") + + inputs := m.GetInputs() + + if inputs["path"] != "" { + command = append(command, inputs["path"]) + } + + return strings.Join(command, " ") +} + +func (m fsModel) Summary() string { + inputs := m.GetInputs() + labels := m.GetLabels() + + keys := []string{"path"} + return common.SummarizeSource(keys, inputs, labels) +} diff --git a/pkg/tui/sources/gcs/gcs.go b/pkg/tui/sources/gcs/gcs.go new file mode 100644 index 000000000000..0342a120e8e7 --- /dev/null +++ b/pkg/tui/sources/gcs/gcs.go @@ -0,0 +1,44 @@ +package gcs + +import ( + "strings" + + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/textinputs" +) + +type gcsCmdModel struct { + textinputs.Model +} + +func GetFields() gcsCmdModel { + projectId := textinputs.InputConfig{ + Label: "Project ID", + Key: "project_id", + Required: true, + Placeholder: "my-project", + } + + return gcsCmdModel{textinputs.New([]textinputs.InputConfig{projectId})} +} + +func (m gcsCmdModel) Cmd() string { + var command []string + command = append(command, "trufflehog", "gcs") + + inputs := m.GetInputs() + if inputs["project_id"] != "" { + command = append(command, "--project_id="+inputs["project_id"]) + } + + command = append(command, "--cloud-environment") + return strings.Join(command, " ") +} + +func (m gcsCmdModel) Summary() string { + inputs := m.GetInputs() + labels := m.GetLabels() + + keys := []string{"project_id"} + return common.SummarizeSource(keys, inputs, labels) +} diff --git a/pkg/tui/sources/git/git.go b/pkg/tui/sources/git/git.go new file mode 100644 index 000000000000..882c7c1cb1b1 --- /dev/null +++ b/pkg/tui/sources/git/git.go @@ -0,0 +1,44 @@ +package git + +import ( + "strings" + + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/textinputs" +) + +type gitCmdModel struct { + textinputs.Model +} + +func GetFields() gitCmdModel { + uri := textinputs.InputConfig{ + Label: "Git URI", + Key: "uri", + Required: true, + Placeholder: "git@github.com:trufflesecurity/trufflehog.git.", + } + + return gitCmdModel{textinputs.New([]textinputs.InputConfig{uri})} +} + +func (m gitCmdModel) Cmd() string { + var command []string + command = append(command, "trufflehog", "git") + + inputs := m.GetInputs() + + if inputs["uri"] != "" { + command = append(command, inputs["uri"]) + } + + return strings.Join(command, " ") +} + +func (m gitCmdModel) Summary() string { + inputs := m.GetInputs() + labels := m.GetLabels() + + keys := []string{"uri"} + return common.SummarizeSource(keys, inputs, labels) +} diff --git a/pkg/tui/sources/github/github.go b/pkg/tui/sources/github/github.go new file mode 100644 index 000000000000..a1817bb0f0f0 --- /dev/null +++ b/pkg/tui/sources/github/github.go @@ -0,0 +1,61 @@ +package github + +import ( + "strings" + + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/textinputs" +) + +type githubCmdModel struct { + textinputs.Model +} + +func GetNote() string { + return "Please enter an organization OR repository." +} + +func GetFields() githubCmdModel { + org := textinputs.InputConfig{ + Label: "Organization", + Key: "org", + Required: false, + Help: "GitHub organization to scan.", + Placeholder: "https://github.com/trufflesecurity", + } + + repo := textinputs.InputConfig{ + Label: "Repository", + Key: "repo", + Required: false, + Help: "GitHub repo to scan.", + Placeholder: "https://github.com/trufflesecurity/test_keys", + } + + return githubCmdModel{textinputs.New([]textinputs.InputConfig{org, repo})} +} + +func (m githubCmdModel) Cmd() string { + var command []string + command = append(command, "trufflehog", "github") + + inputs := m.GetInputs() + + if inputs["org"] != "" { + command = append(command, "--org="+inputs["org"]) + } + + if inputs["repo"] != "" { + command = append(command, "--repo="+inputs["repo"]) + } + + return strings.Join(command, " ") +} + +func (m githubCmdModel) Summary() string { + inputs := m.GetInputs() + labels := m.GetLabels() + + keys := []string{"org", "repo"} + return common.SummarizeSource(keys, inputs, labels) +} diff --git a/pkg/tui/sources/gitlab/gitlab.go b/pkg/tui/sources/gitlab/gitlab.go new file mode 100644 index 000000000000..f272fc8df6d7 --- /dev/null +++ b/pkg/tui/sources/gitlab/gitlab.go @@ -0,0 +1,45 @@ +package gitlab + +import ( + "strings" + + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/textinputs" +) + +type gitlabCmdModel struct { + textinputs.Model +} + +func GetFields() gitlabCmdModel { + token := textinputs.InputConfig{ + Label: "GitLab token", + Key: "token", + Required: true, + Help: "Personal access token with read access", + Placeholder: "glpat-", + } + + return gitlabCmdModel{textinputs.New([]textinputs.InputConfig{token})} +} + +func (m gitlabCmdModel) Cmd() string { + var command []string + command = append(command, "trufflehog", "gitlab") + + inputs := m.GetInputs() + + if inputs["token"] != "" { + command = append(command, "--token="+inputs["token"]) + } + + return strings.Join(command, " ") +} + +func (m gitlabCmdModel) Summary() string { + inputs := m.GetInputs() + labels := m.GetLabels() + + keys := []string{"token"} + return common.SummarizeSource(keys, inputs, labels) +} diff --git a/pkg/tui/sources/s3/s3.go b/pkg/tui/sources/s3/s3.go new file mode 100644 index 000000000000..42433e331a30 --- /dev/null +++ b/pkg/tui/sources/s3/s3.go @@ -0,0 +1,48 @@ +package s3 + +import ( + "strings" + + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/textinputs" +) + +type s3CmdModel struct { + textinputs.Model +} + +func GetFields() s3CmdModel { + bucket := textinputs.InputConfig{ + Label: "S3 bucket name(s)", + Key: "buckets", + Required: true, + Placeholder: "my-bucket-name", + Help: "Buckets to scan. Separate by space if multiple.", + } + + return s3CmdModel{textinputs.New([]textinputs.InputConfig{bucket})} +} + +func (m s3CmdModel) Cmd() string { + var command []string + command = append(command, "trufflehog", "s3") + + inputs := m.GetInputs() + vals := inputs["buckets"] + if vals != "" { + buckets := strings.Fields(vals) + for _, bucket := range buckets { + command = append(command, "--bucket="+bucket) + } + } + + return strings.Join(command, " ") +} + +func (m s3CmdModel) Summary() string { + inputs := m.GetInputs() + labels := m.GetLabels() + + keys := []string{"buckets"} + return common.SummarizeSource(keys, inputs, labels) +} diff --git a/pkg/tui/sources/sources.go b/pkg/tui/sources/sources.go new file mode 100644 index 000000000000..8269fcaf047e --- /dev/null +++ b/pkg/tui/sources/sources.go @@ -0,0 +1,60 @@ +package sources + +import ( + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/sources/circleci" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/sources/docker" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/sources/filesystem" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/sources/gcs" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/sources/git" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/sources/github" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/sources/gitlab" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/sources/s3" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/sources/syslog" +) + +func GetSourceNotes(sourceName string) string { + source := strings.ToLower(sourceName) + switch source { + case "github": + return github.GetNote() + + default: + return "" + } +} + +type CmdModel interface { + tea.Model + Cmd() string + Summary() string +} + +func GetSourceFields(sourceName string) CmdModel { + source := strings.ToLower(sourceName) + + switch source { + case "git": + return git.GetFields() + case "github": + return github.GetFields() + case "gitlab": + return gitlab.GetFields() + case "filesystem": + return filesystem.GetFields() + case "aws s3": + return s3.GetFields() + case "gcs (google cloud storage)": + return gcs.GetFields() + case "syslog": + return syslog.GetFields() + case "circleci": + return circleci.GetFields() + case "docker": + return docker.GetFields() + } + + return nil +} diff --git a/pkg/tui/sources/syslog/syslog.go b/pkg/tui/sources/syslog/syslog.go new file mode 100644 index 000000000000..c220d398d09e --- /dev/null +++ b/pkg/tui/sources/syslog/syslog.go @@ -0,0 +1,82 @@ +package syslog + +import ( + "strings" + + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/textinputs" +) + +type syslogCmdModel struct { + textinputs.Model +} + +// TODO: review fields +func GetFields() syslogCmdModel { + protocol := textinputs.InputConfig{ + Label: "Protocol", + Key: "protocol", + Required: true, + Help: "udp or tcp", + Placeholder: "tcp", + } + + listenAddress := textinputs.InputConfig{ + Label: "Address", + Key: "address", + Help: "Address and port to listen on for syslog", + Required: true, + Placeholder: "127.0.0.1:514", + } + + tlsCert := textinputs.InputConfig{ + Label: "TLS Certificate", + Key: "cert", + Required: true, + Help: "Path to TLS certificate", + Placeholder: "/path/to/cert", + } + + tlsKey := textinputs.InputConfig{ + Label: "TLS Key", + Key: "key", + Required: true, + Help: "Path to TLS key", + Placeholder: "/path/to/key", + } + + format := textinputs.InputConfig{ + Label: "Log format", + Key: "format", + Required: true, + Help: "Can be rfc3164 or rfc5424", + Placeholder: "rfc3164", + } + + return syslogCmdModel{textinputs.New([]textinputs.InputConfig{listenAddress, protocol, tlsCert, tlsKey, format})} +} + +func (m syslogCmdModel) Cmd() string { + var command []string + command = append(command, "trufflehog", "syslog") + + inputs := m.GetInputs() + syslogKeys := [5]string{"address", "protocol", "cert", "key", "format"} + + for _, key := range syslogKeys { + if inputs[key] != "" { + flag := "--" + key + "=" + inputs[key] + command = append(command, flag) + } + } + + return strings.Join(command, " ") +} + +func (m syslogCmdModel) Summary() string { + inputs := m.GetInputs() + labels := m.GetLabels() + keys := []string{"address", "protocol", "cert", "key", "format"} + + return common.SummarizeSource(keys, inputs, labels) +} diff --git a/pkg/tui/styles/styles.go b/pkg/tui/styles/styles.go new file mode 100644 index 000000000000..09c9252119b2 --- /dev/null +++ b/pkg/tui/styles/styles.go @@ -0,0 +1,493 @@ +package styles + +import ( + "github.com/charmbracelet/lipgloss" +) + +// XXX: For now, this is in its own package so that it can be shared between +// different packages without incurring an illegal import cycle. + +// https://github.com/charmbracelet/lipgloss#colors +var Colors = map[string]string{ + "softblack": "#1e1e1e", + "charcoal": "#252525", + "stone": "#5a5a5a", + "smoke": "#999999", + "sand": "#e1deda", + "cloud": "#f4efe9", + "offwhite": "#faf8f7", + "fern": "#38645a", + "sprout": "#5bb381", + "gold": "#ae8c57", + "bronze": "#89553d", + "coral": "#c15750", + "violet": "#6b5b9a", +} + +var ( + BoldTextStyle = lipgloss.NewStyle().Bold(true) + + PrimaryTextStyle = lipgloss.NewStyle().Foreground( + lipgloss.Color("28")) // green + + HintTextStyle = lipgloss.NewStyle().Foreground( + lipgloss.Color("8")) // grey + + CodeTextStyle = lipgloss.NewStyle().Background(lipgloss.Color("130")).Foreground(lipgloss.Color("15")) +) + +var AppStyle = lipgloss.NewStyle().Padding(1, 2) + +// Styles defines styles for the UI. +type Styles struct { + ActiveBorderColor lipgloss.Color + InactiveBorderColor lipgloss.Color + + App lipgloss.Style + ServerName lipgloss.Style + TopLevelNormalTab lipgloss.Style + TopLevelActiveTab lipgloss.Style + TopLevelActiveTabDot lipgloss.Style + + MenuItem lipgloss.Style + MenuLastUpdate lipgloss.Style + + RepoSelector struct { + Normal struct { + Base lipgloss.Style + Title lipgloss.Style + Desc lipgloss.Style + Command lipgloss.Style + Updated lipgloss.Style + } + Active struct { + Base lipgloss.Style + Title lipgloss.Style + Desc lipgloss.Style + Command lipgloss.Style + Updated lipgloss.Style + } + } + + Repo struct { + Base lipgloss.Style + Title lipgloss.Style + Command lipgloss.Style + Body lipgloss.Style + Header lipgloss.Style + HeaderName lipgloss.Style + HeaderDesc lipgloss.Style + } + + Footer lipgloss.Style + Branch lipgloss.Style + HelpKey lipgloss.Style + HelpValue lipgloss.Style + HelpDivider lipgloss.Style + URLStyle lipgloss.Style + + Error lipgloss.Style + ErrorTitle lipgloss.Style + ErrorBody lipgloss.Style + + AboutNoReadme lipgloss.Style + + LogItem struct { + Normal struct { + Base lipgloss.Style + Hash lipgloss.Style + Title lipgloss.Style + Desc lipgloss.Style + Keyword lipgloss.Style + } + Active struct { + Base lipgloss.Style + Hash lipgloss.Style + Title lipgloss.Style + Desc lipgloss.Style + Keyword lipgloss.Style + } + } + + Log struct { + Commit lipgloss.Style + CommitHash lipgloss.Style + CommitAuthor lipgloss.Style + CommitDate lipgloss.Style + CommitBody lipgloss.Style + CommitStatsAdd lipgloss.Style + CommitStatsDel lipgloss.Style + Paginator lipgloss.Style + } + + Ref struct { + Normal struct { + Item lipgloss.Style + ItemTag lipgloss.Style + } + Active struct { + Item lipgloss.Style + ItemTag lipgloss.Style + } + ItemSelector lipgloss.Style + ItemBranch lipgloss.Style + Paginator lipgloss.Style + } + + Tree struct { + Normal struct { + FileName lipgloss.Style + FileDir lipgloss.Style + FileMode lipgloss.Style + FileSize lipgloss.Style + } + Active struct { + FileName lipgloss.Style + FileDir lipgloss.Style + FileMode lipgloss.Style + FileSize lipgloss.Style + } + Selector lipgloss.Style + FileContent lipgloss.Style + Paginator lipgloss.Style + NoItems lipgloss.Style + } + + Spinner lipgloss.Style + + CodeNoContent lipgloss.Style + + StatusBar lipgloss.Style + StatusBarKey lipgloss.Style + StatusBarValue lipgloss.Style + StatusBarInfo lipgloss.Style + StatusBarBranch lipgloss.Style + StatusBarHelp lipgloss.Style + + Tabs lipgloss.Style + TabInactive lipgloss.Style + TabActive lipgloss.Style + TabSeparator lipgloss.Style +} + +// DefaultStyles returns default styles for the UI. +func DefaultStyles() *Styles { + highlightColor := lipgloss.Color("210") + highlightColorDim := lipgloss.Color("174") + selectorColor := lipgloss.Color("167") + hashColor := lipgloss.Color("185") + + s := new(Styles) + + s.ActiveBorderColor = lipgloss.Color("62") + s.InactiveBorderColor = lipgloss.Color("241") + + s.App = lipgloss.NewStyle(). + Margin(1, 2) + + s.ServerName = lipgloss.NewStyle(). + Height(1). + MarginLeft(1). + MarginBottom(1). + Padding(0, 1). + Background(lipgloss.Color("57")). + Foreground(lipgloss.Color("229")). + Bold(true) + + s.TopLevelNormalTab = lipgloss.NewStyle(). + MarginRight(2) + + s.TopLevelActiveTab = s.TopLevelNormalTab.Copy(). + Foreground(lipgloss.Color("36")) + + s.TopLevelActiveTabDot = lipgloss.NewStyle(). + Foreground(lipgloss.Color("36")) + + s.RepoSelector.Normal.Base = lipgloss.NewStyle(). + PaddingLeft(1). + Border(lipgloss.Border{Left: " "}, false, false, false, true). + Height(3) + + s.RepoSelector.Normal.Title = lipgloss.NewStyle().Bold(true) + + s.RepoSelector.Normal.Desc = lipgloss.NewStyle(). + Foreground(lipgloss.Color("243")) + + s.RepoSelector.Normal.Command = lipgloss.NewStyle(). + Foreground(lipgloss.Color("132")) + + s.RepoSelector.Normal.Updated = lipgloss.NewStyle(). + Foreground(lipgloss.Color("243")) + + s.RepoSelector.Active.Base = s.RepoSelector.Normal.Base.Copy(). + BorderStyle(lipgloss.Border{Left: "ā”ƒ"}). + BorderForeground(lipgloss.Color("176")) + + s.RepoSelector.Active.Title = s.RepoSelector.Normal.Title.Copy(). + Foreground(lipgloss.Color("212")) + + s.RepoSelector.Active.Desc = s.RepoSelector.Normal.Desc.Copy(). + Foreground(lipgloss.Color("246")) + + s.RepoSelector.Active.Updated = s.RepoSelector.Normal.Updated.Copy(). + Foreground(lipgloss.Color("212")) + + s.RepoSelector.Active.Command = s.RepoSelector.Normal.Command.Copy(). + Foreground(lipgloss.Color("204")) + + s.MenuItem = lipgloss.NewStyle(). + PaddingLeft(1). + Border(lipgloss.Border{ + Left: " ", + }, false, false, false, true). + Height(3) + + s.MenuLastUpdate = lipgloss.NewStyle(). + Foreground(lipgloss.Color("241")). + Align(lipgloss.Right) + + s.Repo.Base = lipgloss.NewStyle() + + s.Repo.Title = lipgloss.NewStyle(). + Padding(0, 2) + + s.Repo.Command = lipgloss.NewStyle(). + Foreground(lipgloss.Color("168")) + + s.Repo.Body = lipgloss.NewStyle(). + Margin(1, 0) + + s.Repo.Header = lipgloss.NewStyle(). + Height(2). + Border(lipgloss.NormalBorder(), false, false, true, false). + BorderForeground(lipgloss.Color("236")) + + s.Repo.HeaderName = lipgloss.NewStyle(). + Foreground(lipgloss.Color("212")). + Bold(true) + + s.Repo.HeaderDesc = lipgloss.NewStyle(). + Foreground(lipgloss.Color("243")) + + s.Footer = lipgloss.NewStyle(). + MarginTop(1). + Padding(0, 1). + Height(1) + + s.Branch = lipgloss.NewStyle(). + Foreground(lipgloss.Color("203")). + Background(lipgloss.Color("236")). + Padding(0, 1) + + s.HelpKey = lipgloss.NewStyle(). + Foreground(lipgloss.Color("241")) + + s.HelpValue = lipgloss.NewStyle(). + Foreground(lipgloss.Color("239")) + + s.HelpDivider = lipgloss.NewStyle(). + Foreground(lipgloss.Color("237")). + SetString(" ā€¢ ") + + s.URLStyle = lipgloss.NewStyle(). + MarginLeft(1). + Foreground(lipgloss.Color("168")) + + s.Error = lipgloss.NewStyle(). + MarginTop(2) + + s.ErrorTitle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("230")). + Background(lipgloss.Color("204")). + Bold(true). + Padding(0, 1) + + s.ErrorBody = lipgloss.NewStyle(). + Foreground(lipgloss.Color("252")). + MarginLeft(2) + + s.AboutNoReadme = lipgloss.NewStyle(). + MarginTop(1). + MarginLeft(2). + Foreground(lipgloss.Color("242")) + + s.LogItem.Normal.Base = lipgloss.NewStyle(). + Border(lipgloss.Border{ + Left: " ", + }, false, false, false, true). + PaddingLeft(1) + + s.LogItem.Active.Base = s.LogItem.Normal.Base.Copy(). + Border(lipgloss.Border{ + Left: "ā”ƒ", + }, false, false, false, true). + BorderForeground(selectorColor) + + s.LogItem.Active.Hash = s.LogItem.Normal.Hash.Copy(). + Foreground(hashColor) + + s.LogItem.Active.Hash = lipgloss.NewStyle(). + Bold(true). + Foreground(highlightColor) + + s.LogItem.Normal.Title = lipgloss.NewStyle(). + Foreground(lipgloss.Color("105")) + + s.LogItem.Active.Title = lipgloss.NewStyle(). + Foreground(highlightColor). + Bold(true) + + s.LogItem.Normal.Desc = lipgloss.NewStyle(). + Foreground(lipgloss.Color("246")) + + s.LogItem.Active.Desc = lipgloss.NewStyle(). + Foreground(lipgloss.Color("95")) + + s.LogItem.Active.Keyword = s.LogItem.Active.Desc.Copy(). + Foreground(highlightColorDim) + + s.LogItem.Normal.Hash = lipgloss.NewStyle(). + Foreground(hashColor) + + s.LogItem.Active.Hash = lipgloss.NewStyle(). + Foreground(highlightColor) + + s.Log.Commit = lipgloss.NewStyle(). + Margin(0, 2) + + s.Log.CommitHash = lipgloss.NewStyle(). + Foreground(hashColor). + Bold(true) + + s.Log.CommitBody = lipgloss.NewStyle(). + MarginTop(1). + MarginLeft(2) + + s.Log.CommitStatsAdd = lipgloss.NewStyle(). + Foreground(lipgloss.Color("42")). + Bold(true) + + s.Log.CommitStatsDel = lipgloss.NewStyle(). + Foreground(lipgloss.Color("203")). + Bold(true) + + s.Log.Paginator = lipgloss.NewStyle(). + Margin(0). + Align(lipgloss.Center) + + s.Ref.Normal.Item = lipgloss.NewStyle() + + s.Ref.ItemSelector = lipgloss.NewStyle(). + Foreground(selectorColor). + SetString("> ") + + s.Ref.Active.Item = lipgloss.NewStyle(). + Foreground(highlightColorDim) + + s.Ref.ItemBranch = lipgloss.NewStyle() + + s.Ref.Normal.ItemTag = lipgloss.NewStyle(). + Foreground(lipgloss.Color("39")) + + s.Ref.Active.ItemTag = lipgloss.NewStyle(). + Bold(true). + Foreground(highlightColor) + + s.Ref.Active.Item = lipgloss.NewStyle(). + Bold(true). + Foreground(highlightColor) + + s.Ref.Paginator = s.Log.Paginator.Copy() + + s.Tree.Selector = s.Tree.Normal.FileName.Copy(). + Width(1). + Foreground(selectorColor) + + s.Tree.Normal.FileName = lipgloss.NewStyle(). + MarginLeft(1) + + s.Tree.Active.FileName = s.Tree.Normal.FileName.Copy(). + Bold(true). + Foreground(highlightColor) + + s.Tree.Normal.FileDir = lipgloss.NewStyle(). + Foreground(lipgloss.Color("39")) + + s.Tree.Active.FileDir = lipgloss.NewStyle(). + Foreground(highlightColor) + + s.Tree.Normal.FileMode = s.Tree.Active.FileName.Copy(). + Width(10). + Foreground(lipgloss.Color("243")) + + s.Tree.Active.FileMode = s.Tree.Normal.FileMode.Copy(). + Foreground(highlightColorDim) + + s.Tree.Normal.FileSize = s.Tree.Normal.FileName.Copy(). + Foreground(lipgloss.Color("243")) + + s.Tree.Active.FileSize = s.Tree.Normal.FileName.Copy(). + Foreground(highlightColorDim) + + s.Tree.FileContent = lipgloss.NewStyle() + + s.Tree.Paginator = s.Log.Paginator.Copy() + + s.Tree.NoItems = s.AboutNoReadme.Copy() + + s.Spinner = lipgloss.NewStyle(). + MarginTop(1). + MarginLeft(2). + Foreground(lipgloss.Color("205")) + + s.CodeNoContent = lipgloss.NewStyle(). + SetString("No Content."). + MarginTop(1). + MarginLeft(2). + Foreground(lipgloss.Color("242")) + + s.StatusBar = lipgloss.NewStyle(). + Height(1) + + s.StatusBarKey = lipgloss.NewStyle(). + Bold(true). + Padding(0, 1). + Background(lipgloss.Color("206")). + Foreground(lipgloss.Color("228")) + + s.StatusBarValue = lipgloss.NewStyle(). + Padding(0, 1). + Background(lipgloss.Color("235")). + Foreground(lipgloss.Color("243")) + + s.StatusBarInfo = lipgloss.NewStyle(). + Padding(0, 1). + Background(lipgloss.Color("212")). + Foreground(lipgloss.Color("230")) + + s.StatusBarBranch = lipgloss.NewStyle(). + Padding(0, 1). + Background(lipgloss.Color("62")). + Foreground(lipgloss.Color("230")) + + s.StatusBarHelp = lipgloss.NewStyle(). + Padding(0, 1). + Background(lipgloss.Color("237")). + Foreground(lipgloss.Color("243")) + + s.Tabs = lipgloss.NewStyle(). + Height(1) + + s.TabInactive = lipgloss.NewStyle() + + s.TabActive = lipgloss.NewStyle(). + Underline(true). + Foreground(lipgloss.Color("36")) + + s.TabSeparator = lipgloss.NewStyle(). + SetString("ā”‚"). + Padding(0, 1). + Foreground(lipgloss.Color("238")) + + return s +} diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go new file mode 100644 index 000000000000..023e9b6a0640 --- /dev/null +++ b/pkg/tui/tui.go @@ -0,0 +1,198 @@ +package tui + +import ( + "fmt" + "os" + "strings" + + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + zone "github.com/lrstanley/bubblezone" + "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/contact_enterprise" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/pages/source_configure" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/pages/source_select" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/pages/view_oss" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/pages/wizard_intro" + "github.com/trufflesecurity/trufflehog/v3/pkg/tui/styles" +) + +type page int + +const ( + wizardIntroPage page = iota + sourceSelectPage + sourceConfigurePage + viewOSSProjectPage + contactEnterprisePage +) + +type sessionState int + +const ( + startState sessionState = iota + errorState + loadedState +) + +// TUI is the main TUI model. +type TUI struct { + common common.Common + pages []common.Component + activePage page + state sessionState + args []string +} + +// New returns a new TUI model. +func New(c common.Common) *TUI { + ui := &TUI{ + common: c, + pages: make([]common.Component, 5), + activePage: wizardIntroPage, + state: startState, + } + return ui +} + +// SetSize implements common.Component. +func (ui *TUI) SetSize(width, height int) { + ui.common.SetSize(width, height) + for _, p := range ui.pages { + if p != nil { + p.SetSize(width, height) + } + } +} + +// 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.SetSize(ui.common.Width, ui.common.Height) + cmds := make([]tea.Cmd, 0) + cmds = append(cmds, + ui.pages[wizardIntroPage].Init(), + ui.pages[sourceSelectPage].Init(), + ui.pages[sourceConfigurePage].Init(), + ui.pages[viewOSSProjectPage].Init(), + ui.pages[contactEnterprisePage].Init(), + ) + ui.state = loadedState + ui.SetSize(ui.common.Width, ui.common.Height) + return tea.Batch(cmds...) +} + +// Update implements tea.Model. +func (ui *TUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + cmds := make([]tea.Cmd, 0) + switch msg := msg.(type) { + case tea.WindowSizeMsg: + ui.SetSize(msg.Width, msg.Height) + for i, p := range ui.pages { + m, cmd := p.Update(msg) + ui.pages[i] = m.(common.Component) + if cmd != nil { + cmds = append(cmds, cmd) + } + } + case tea.KeyMsg, tea.MouseMsg: + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, ui.common.KeyMap.Help): + 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 + return ui, nil + } + case tea.MouseMsg: + switch msg.Type { + case tea.MouseLeft: + } + } + case common.ErrorMsg: + return ui, nil + 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 + case wizard_intro.ViewHelpDocs: + ui.args = []string{"--help"} + + return ui, tea.Batch(nil, tea.Quit) + case wizard_intro.EnterpriseInquire: + ui.activePage = contactEnterprisePage + case wizard_intro.ScanSourceWithWizard: + ui.activePage = sourceSelectPage + } + case source_select.SourceItem: + ui.activePage = sourceConfigurePage + cmds = append(cmds, func() tea.Msg { + return source_configure.SetSourceMsg{Source: item.ID()} + }) + } + case source_configure.SetArgsMsg: + ui.args = strings.Split(string(msg), " ")[1:] + return ui, tea.Quit + } + + if ui.state == loadedState { + m, cmd := ui.pages[ui.activePage].Update(msg) + ui.pages[ui.activePage] = m.(common.Component) + if cmd != nil { + cmds = append(cmds, cmd) + } + } + + // This fixes determining the height margin of the footer. + // ui.SetSize(ui.common.Width, ui.common.Height) + return ui, tea.Batch(cmds...) +} + +// View implements tea.Model. +func (ui *TUI) View() string { + var view string + switch ui.state { + case startState: + view = "Loading..." + case loadedState: + view = ui.pages[ui.activePage].View() + default: + view = "Unknown state :/ this is a bug!" + } + return ui.common.Zone.Scan( + ui.common.Styles.App.Render(view), + ) +} + +func Run() []string { + c := common.Common{ + Copy: nil, + Styles: styles.DefaultStyles(), + KeyMap: keymap.DefaultKeyMap(), + Width: 0, + Height: 0, + Zone: zone.New(), + } + m := New(c) + 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) + } + return m.args +}