diff --git a/Makefile b/Makefile index c10a47a..3d61581 100644 --- a/Makefile +++ b/Makefile @@ -60,3 +60,7 @@ install-infracost: curl -L https://github.com/infracost/infracost/releases/download/v0.10.38/infracost-linux-amd64.tar.gz | tar -zxf - mv infracost-linux-amd64 ~/.local/bin/infracost chmod +x ~/.local/bin/infracost + +.PHONY: demo +demo: + vhs demo/vhs.tape diff --git a/README.md b/README.md index 9e378e7..869c10b 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,6 @@ FLAGS --data-dir STRING Directory in which to store plan files. (default: /home/louis/.pug) -e, --env STRING Environment variable to pass to terraform process. Can set more than once. -a, --arg STRING CLI arg to pass to terraform process. Can set more than once. - -f, --first-page STRING The first page to open on startup. (default: modules) -d, --debug Log bubbletea messages to messages.log -v, --version Print version. -c, --config STRING Path to config file. (default: /home/louis/.pug.yaml) @@ -84,55 +83,40 @@ max-tasks: 100 Pug automatically loads variables from a .tfvars file. It looks for a file named `.tfvars` in the module directory, where `` is the name of the workspace. For example, if the workspace is named `dev` then it'll look for `dev.tfvars`. If the file exists then it'll pass the name to `terraform plan`, e.g. for a workspace named `dev`, it'll invoke `terraform plan -vars-file=dev.tfvars`. -## Pages +## Panes -### Modules +### Explorer -![Modules screenshot](./demo/modules.png) - -Press `m` to go to the modules page. - -*Note: what Pug calls a module is equivalent to a [root module](https://developer.hashicorp.com/terraform/language/modules#the-root-module), i.e. a directory containing terraform configuration, including a state backend. It is not to be confused with a [child module](https://developer.hashicorp.com/terraform/language/modules#child-modules).* - -#### Key bindings - -| Key | Description | Multi-select | -|--|--|--| -|`i`|Run `terraform init`|✓| -|`u`|Run `terraform init -upgrade`|✓| -|`f`|Run `terraform fmt`|✓| -|`v`|Run `terraform validate`|✓| -|`p`|Run `terraform plan`|✓| -|`P`|Run `terraform plan -destroy`|✓| -|`a`|Run `terraform apply`|✓| -|`d`|Run `terraform apply -destroy`|✓| -|`e`|Open module in editor|✗| -|`x`|Run any program|✓| -|`Ctrl+r`|Reload all modules|-| -|`Ctrl+w`|Reload module's workspaces|✓| - -### Workspaces +The explorer pane a tree of [modules](#module) and [workspaces](#workspace) discovered on your filesystem. From here, terraform commands can be carried out on both modules and workspaces. -![Workspaces screenshot](./demo/workspaces.png) +You can select multiple modules or workspaces; you cannot select a combination of the two. Any terraform commands are then carried out on the selection. -Press `w` to go to the workspaces page. - -*Note: A workspace is directly equivalent to a [terraform workspace](https://developer.hashicorp.com/terraform/language/state/workspaces).* +The number of resources in the state is shown alongside the workspace. +![Modules screenshot](./demo/modules.png) + #### Key bindings -| Key | Description | Multi-select | -|--|--|--| -|`i`|Run `terraform init`|✓| -|`u`|Run `terraform init -upgrade`|✓| -|`f`|Run `terraform fmt`|✓| -|`v`|Run `terraform validate`|✓| -|`p`|Run `terraform plan`|✓| -|`P`|Run `terraform plan -destroy`|✓| -|`a`|Run `terraform apply`|✓| -|`d`|Run `terraform apply -destroy`|✓| -|`C`|Run `terraform workspace select`|✗| -|`$`|Run `infracost breakdown`|✓| +| Key | Description | Multi-select | Module | Workspace | +|--|--|--|--|--| +|`i`|Run `terraform init`|✓|✓|✓\*\*| +|`u`|Run `terraform init -upgrade`|✓|✓\*\*| +|`f`|Run `terraform fmt`|✓|✓|✓\*\*| +|`v`|Run `terraform validate`|✓|✓|✓\*\*| +|`p`|Run `terraform plan`|✓|✓\*|✓| +|`P`|Run `terraform plan -destroy`|✓\*|✓|✓| +|`a`|Run `terraform apply`|✓|✓\*|✓| +|`d`|Run `terraform apply -destroy`|✓\*|✓|✓| +|`C`|Run `terraform workspace select`|✗|✗|✓| +|`$`|Run `infracost breakdown`|✓|✓\*|✓| +|`E`|Open module in editor|✗|✓|✓\*\*| +|`x`|Run any program|✓|✓|✓\*\*| +|`Ctrl+r`|Reload all modules|-|✓|✓| +|`Ctrl+w`|Reload module's workspaces|✓|✓|✓\*\*| + +\* Operate on module's current workspace. + +\*\* Operate on workspace's parent module. ### State @@ -149,14 +133,10 @@ Press `s` to go to the state page, listing a workspace's resources. |`a`|Run `terraform apply -target`|✓| |`d`|Run `terraform apply -destroy -target`|✓| |`D`|Run `terraform state rm`|✓| -|`M`|Run `terraform state mv`|✗| +|`m`|Run `terraform state mv`|✗| |`Ctrl+t`|Run `terraform taint`|✓| |`U`|Run `terraform untaint`|✓| |`Ctrl+r`|Run `terraform state pull`|-| -|`S`|Toggle split screen|-| -|`+`|Increase split screen top pane|-| -|`-`|Decrease split screen top pane|-| -|`tab`|Switch split screen pane focus|-| ### Tasks @@ -170,11 +150,6 @@ Press `t` to go to the tasks page. |--|--|--| |`c`|Cancel task|✓| |`r`|Retry task|✓| -|`Enter`|Full screen task output|✗| -|`S`|Toggle split screen|-| -|`+`|Increase split screen top pane|-| -|`-`|Decrease split screen top pane|-| -|`tab`|Switch split screen pane focus|-| |`I`|Toggle task info sidebar|-| ### Task Group @@ -189,11 +164,6 @@ Creating multiple tasks, via a selection, creates a task group, and takes you to |--|--|--| |`c`|Cancel task|✓| |`r`|Retry task|✓| -|`Enter`|Full screen task output|✗| -|`S`|Toggle split screen|-| -|`+`|Increase split screen top pane|-| -|`-`|Decrease split screen top pane|-| -|`tab`|Switch split screen pane focus|-| |`I`|Toggle task info sidebar|-| ### Task Groups Listing @@ -218,16 +188,25 @@ These keys are valid on any page. |--|--| |`?`|Open help pane| |`Ctrl+c`|Quit| -|`Esc`|Go to previous page| -|`m`|Go to modules page| -|`w`|Go to workspaces page| -|`s`|Go to state page\*| -|`t`|Go to tasks page| -|`T`|Go to task groups page| +|`Esc`|Go to previous page\*\*| +|`0`|Focus left pane (explorer)| +|`1`|Focus top right pane| +|`2`|Focus bottom right pane| +|`e`|Go to explorer| +|`s`|Go to state \*| +|`t`|Go to tasks| +|`T`|Go to task groups| |`l`|Go to logs| +|`X`|Close pane| +|`+`|Increase pane height|-| +|`-`|Decrease pane height|-| +|`<`|Increase pane width|-| +|`>`|Decrease pane width|-| +|`tab`|Switch split screen pane focus|-| |`Ctrl+s`|Toggle auto-scrolling of terraform output| \* Only where the workspace can be ascertained. +\*\* Only history for the top right pane is maintained. ### Selections @@ -254,7 +233,7 @@ Items can be filtered to those containing a sub-string. ### Navigation -Common vim key bindings are supported for navigation. +Common vim key bindings are supported for navigating task output. | Key | Description | |--|--| diff --git a/demo/asdf_install_terraform_task_group.png b/demo/asdf_install_terraform_task_group.png index 58b3686..a06b03e 100644 Binary files a/demo/asdf_install_terraform_task_group.png and b/demo/asdf_install_terraform_task_group.png differ diff --git a/demo/demo.gif b/demo/demo.gif index bcb4483..2cc6c65 100644 Binary files a/demo/demo.gif and b/demo/demo.gif differ diff --git a/demo/execute_asdf_install_terraform.png b/demo/execute_asdf_install_terraform.png index 36c2830..40a8017 100644 Binary files a/demo/execute_asdf_install_terraform.png and b/demo/execute_asdf_install_terraform.png differ diff --git a/demo/filter.png b/demo/filter.png index 794a85b..3809702 100644 Binary files a/demo/filter.png and b/demo/filter.png differ diff --git a/demo/infracost_output.png b/demo/infracost_output.png index a52758e..bf4fe56 100644 Binary files a/demo/infracost_output.png and b/demo/infracost_output.png differ diff --git a/demo/logs.png b/demo/logs.png index 5b53f59..a0d56eb 100644 Binary files a/demo/logs.png and b/demo/logs.png differ diff --git a/demo/modules.png b/demo/modules.png index 755c0aa..a5fd300 100644 Binary files a/demo/modules.png and b/demo/modules.png differ diff --git a/demo/state.png b/demo/state.png index 9b407d3..a07f6f4 100644 Binary files a/demo/state.png and b/demo/state.png differ diff --git a/demo/task_group.png b/demo/task_group.png index b63798e..be2fde5 100644 Binary files a/demo/task_group.png and b/demo/task_group.png differ diff --git a/demo/task_groups.png b/demo/task_groups.png index 6d5a5f7..e57047c 100644 Binary files a/demo/task_groups.png and b/demo/task_groups.png differ diff --git a/demo/tasks.png b/demo/tasks.png index a473f2e..1ea2a77 100644 Binary files a/demo/tasks.png and b/demo/tasks.png differ diff --git a/demo/vhs.tape b/demo/vhs.tape index d151907..b36bac1 100644 --- a/demo/vhs.tape +++ b/demo/vhs.tape @@ -1,32 +1,47 @@ Output demo/demo.gif +Require terraform +Require infracost +Require asdf + Set Shell "bash" Set FontSize 14 Set Width 1200 Set Height 800 Set Framerate 24 Set Padding 5 +Set FontFamily "JetBrainsMono Nerd Font" +# Clear any leftover artefacts from last demo Hide -Type `TF_CLI_CONFIG_FILE=$PWD/mirror/mirror.tfrc go run main.go -w demo/dont_cost_money` Enter +Type@1ms "asdf uninstall terraform" Enter +Type@1ms "find demo/ -name .terraform -exec rm -rf {} \; > /dev/null 2>&1 || true" Enter +Type@1ms "find demo/ -name terraform.tfstate -exec rm {} \; > /dev/null 2>&1 || true" Enter +Type@1ms "find demo/ -name terraform.tfstate.* -exec rm {} \; > /dev/null 2>&1 || true" Enter +Type@1ms "find demo/ -name .terraform.lock.hcl -exec rm {} \; > /dev/null 2>&1 || true" Enter +Type@1ms "find demo/ -name environment -exec rm {} \; > /dev/null 2>&1 || true" Enter +Show + +Hide +Type@1ms `TF_CLI_CONFIG_FILE=$PWD/mirror/mirror.tfrc go run main.go -w demo/dont_cost_money` Enter Sleep 1s Show -# show unintialized modules +# show explorer tree Sleep 1s # init all modules Ctrl+a Sleep 0.5s Type "i" -# we're taken to the init task group page +# show init task group Sleep 0.5s # preview output for several tasks Down Sleep 0.5s Down Sleep 0.5s Down Sleep 0.5s -# go back to modules -Type "m" +# go back to explorer +Type "0" # validate all modules Type "v" -# we're taken to the validate task group page +# show validate task group Sleep 0.5s # enable task info side panel Type "I" Sleep 0.5s @@ -37,13 +52,13 @@ Down Sleep 0.5s Down Sleep 0.5s Down Sleep 0.5s # disable task info side panel Type "I" Sleep 0.5s -# go back to modules -Type "m" Sleep 0.5s +# go back to explorer +Type "0" Sleep 0.5s # show help Type "?" Sleep 0.5s # run plan on all modules Type "p" -# we're taken to the plan task group page +# show plan task group page Sleep 1s # hide help Type "?" @@ -57,14 +72,16 @@ Sleep 0.5s Type "y" # preview output for several tasks Sleep 2s Down Sleep 1s Down Sleep 1s Down Sleep 1s -# go to workspaces -Type "w" Sleep 1s -# take screen shot of workspaces +# go back to explorer +Type "0" Sleep 1s +# take screen shot of explorer tree populated with workspaces Screenshot demo/workspaces.png # filter dev workspaces Type "/" Sleep 0.5s Type "dev" Sleep 0.5s Enter # take screen shot of filter mode Screenshot demo/filter.png Sleep 0.5s +# move cursor down one onto dev workspace belonging to module a +Down Sleep 0.5s # select all dev workspaces and auto-apply Ctrl+a Sleep 0.5s Type "a" # accept confirmation @@ -72,36 +89,31 @@ Sleep 0.5s Type "y" # preview output for several tasks Sleep 2s Down Sleep 1s Down Sleep 1s -# go back to modules -Type "m" Sleep 0.5s +# go back to explorer +Type "0" Sleep 0.5s # take screen shot of modules (sleep to ensure page doesn't switch too soon) Screenshot demo/modules.png Sleep 0.5s -# go to state for current module +# go to state for current dev workspace Type "s" Sleep 0.5s # see preview for several resources Sleep 0.5s Down Sleep 0.5s Down Sleep 0.5s Down Sleep 0.5s # move current resource -Type "M" Sleep 0.5s Backspace 6 Sleep 0.5s Type "giraffe[99]" Sleep 0.5s Enter -# look at task output for a bit -Sleep 1s +Type "m" Sleep 0.5s Backspace 6 Sleep 0.5s Type "giraffe[99]" Sleep 0.5s Enter +# wait a bit of move to occur and message to appear in footer +Sleep 1.5s -# go back to state -Escape Sleep 0.5s # take screen shot of state Screenshot demo/state.png # delete another resource Down Sleep 0.5s -Type "D" +Delete # confirm deletion Sleep 0.5s Type "y" Sleep 0.5s # look at task output for a bit -Sleep 1s - -# go back to state -Escape Sleep 0.5s +Sleep 1.5s # select all resources Ctrl+a Sleep 0.5s @@ -117,7 +129,7 @@ Escape Sleep 0.5s # select all resources Ctrl+a Sleep 0.5s -# taint all resources +# untaint all resources Type "U" # we're taken to the untaint task group page Sleep 0.5s @@ -167,7 +179,7 @@ Type `TF_CLI_CONFIG_FILE=$PWD/mirror/mirror.tfrc go run main.go -w demo/do_cost_ Sleep 1s Show -# show unintialized modules +# show explorer tree Sleep 1s # init all modules @@ -175,9 +187,7 @@ Ctrl+a Sleep 0.5s Type "i" # we're taken to the init task group page, wait for a few seconds for tasks to finish Sleep 2s -# go to workspaces -Type "w" Sleep 2s -# select all workspaces +# select all modules Ctrl+a Sleep 0.5s # calculate cost Type "$" @@ -186,27 +196,27 @@ Sleep 6s # take screen shot of infracost output (sleep to ensure page doesn't switch too soon) Screenshot demo/infracost_output.png Sleep 0.5s -# go back to workspaces -Type "w" Sleep 0.5s -# take screen shot of workspaces (sleep to ensure page doesn't switch too soon) +# go back to explorer +Type "0" Sleep 0.5s +# take screen shot of workspaces with costs (sleep to ensure page doesn't switch too soon) Screenshot demo/workspaces_with_cost.png Sleep 0.5s Sleep 3s # quit app and restart, this time with modules with .tool-versions files, to demonstrate support for executing arbitrary programs, in this case, `asdf` Hide Ctrl+c Type "y" -Type `TF_CLI_CONFIG_FILE=$PWD/mirror/mirror.tfrc go run main.go -w demo/multiple_versions` Enter +Type@1ms `TF_CLI_CONFIG_FILE=$PWD/mirror/mirror.tfrc go run main.go -w demo/multiple_versions` Enter Sleep 1s Show -# show unintialized modules +# show explorer Sleep 1s # select all modules Ctrl+a Sleep 0.5s # install terraform for each module Type "x" Sleep 1s -Type "asdf install terraform" Sleep 1s +Type@1ms "asdf install terraform" Sleep 1s # take screen shot (sleep to ensure page doesn't switch too soon) Screenshot demo/execute_asdf_install_terraform.png Sleep 0.5s # Execute asdf diff --git a/demo/workspaces.png b/demo/workspaces.png index 6c40867..ef698d7 100644 Binary files a/demo/workspaces.png and b/demo/workspaces.png differ diff --git a/demo/workspaces_with_cost.png b/demo/workspaces_with_cost.png index e6e0134..292478b 100644 Binary files a/demo/workspaces_with_cost.png and b/demo/workspaces_with_cost.png differ diff --git a/hacks/make_demo.sh b/hacks/make_demo.sh deleted file mode 100755 index dd05326..0000000 --- a/hacks/make_demo.sh +++ /dev/null @@ -1,5 +0,0 @@ -# Clear any leftover artefacts from last demo -./hacks/reset_demo.sh - -# Run demo -vhs demo/vhs.tape diff --git a/hacks/reset_demo.sh b/hacks/reset_demo.sh deleted file mode 100755 index 3ca1c7a..0000000 --- a/hacks/reset_demo.sh +++ /dev/null @@ -1,8 +0,0 @@ -# remove all asdf installs of terraform -asdf uninstall terraform - -find demo/ -name .terraform -exec rm -rf {} \; > /dev/null 2>&1 || true -find demo/ -name terraform.tfstate -exec rm {} \; > /dev/null 2>&1 || true -find demo/ -name terraform.tfstate.* -exec rm {} \; > /dev/null 2>&1 || true -find demo/ -name .terraform.lock.hcl -exec rm {} \; > /dev/null 2>&1 || true -find demo/ -name environment -exec rm {} \; > /dev/null 2>&1 || true diff --git a/internal/tui/actions.go b/internal/tui/actions.go index f013970..28aedab 100644 --- a/internal/tui/actions.go +++ b/internal/tui/actions.go @@ -131,6 +131,19 @@ func (m *ActionHandler) Update(msg tea.Msg) tea.Cmd { return nil } return NavigateTo(ResourceListKind, WithParent(ids[0])) + case key.Matches(msg, keys.Common.Edit): + ids, err := m.GetModuleIDs() + if err != nil { + return ReportError(err) + } + if len(ids) == 0 { + return nil + } + mod, err := m.Modules.Get(ids[0]) + if err != nil { + return ReportError(err) + } + return OpenEditor(m.Workdir.Join(mod.Path)) } } return nil diff --git a/internal/tui/color.go b/internal/tui/color.go index 3bd83d3..d83f12a 100644 --- a/internal/tui/color.go +++ b/internal/tui/color.go @@ -13,6 +13,7 @@ const ( Green = lipgloss.Color("34") Turquoise = lipgloss.Color("86") DarkGreen = lipgloss.Color("#325451") + DarkishGreen = lipgloss.Color("75") LightGreen = lipgloss.Color("47") GreenBlue = lipgloss.Color("#00A095") DeepBlue = lipgloss.Color("39") @@ -26,7 +27,6 @@ const ( DarkGrey = lipgloss.Color("#606362") White = lipgloss.Color("#ffffff") OffWhite = lipgloss.Color("#a8a7a5") - Pink = lipgloss.Color("30") HotPink = lipgloss.Color("200") ) @@ -64,11 +64,6 @@ var ( Light: "", } - modulePathColor = lipgloss.AdaptiveColor{ - Dark: string(Grey), - Light: string(Grey), - } - GroupReportBackgroundColor = EvenLighterGrey TaskSummaryBackgroundColor = EvenLighterGrey diff --git a/internal/tui/explorer/model.go b/internal/tui/explorer/model.go index 24a31c4..b450ed5 100644 --- a/internal/tui/explorer/model.go +++ b/internal/tui/explorer/model.go @@ -268,6 +268,7 @@ func (m model) BorderText() map[tui.BorderPosition]string { tui.WorkspaceStyle.Render(fmt.Sprintf("%d", m.tracker.totalWorkspaces)), ) return map[tui.BorderPosition]string{ + tui.TopMiddleBorder: "PUG 󰩃 ", tui.BottomMiddleBorder: fmt.Sprintf("%s %s", modules, workspaces), } } diff --git a/internal/tui/helpers.go b/internal/tui/helpers.go index 572f276..015a035 100644 --- a/internal/tui/helpers.go +++ b/internal/tui/helpers.go @@ -8,6 +8,7 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/leg100/pug/internal" "github.com/leg100/pug/internal/logging" "github.com/leg100/pug/internal/module" "github.com/leg100/pug/internal/plan" @@ -30,6 +31,7 @@ type Helpers struct { Tasks *task.Service States *state.Service Logger logging.Interface + Workdir internal.Workdir } func (h *Helpers) ModuleCurrentWorkspace(mod *module.Module) *workspace.Workspace { diff --git a/internal/tui/style.go b/internal/tui/style.go index 088b6ce..8e09e02 100644 --- a/internal/tui/style.go +++ b/internal/tui/style.go @@ -10,15 +10,6 @@ var ( Border = Regular.Border(lipgloss.NormalBorder()) ThickBorder = Regular.Border(lipgloss.ThickBorder()).BorderForeground(Violet) - Title = Padded.Foreground(White).Background(Purple) - TitleCommand = Padded.Foreground(White).Background(Blue) - TitlePath = Padded.Foreground(White).Background(modulePathColor) - TitleWorkspace = Padded.Foreground(White).Background(Green) - TitleID = Padded.Foreground(White).Background(Green) - TitleAddress = Padded.Foreground(White).Background(Blue) - TitleSerial = Padded.Foreground(Black).Background(Orange) - TitleTainted = Padded.Foreground(White).Background(Red) - - ModuleStyle = Regular.Foreground(Purple) - WorkspaceStyle = Regular.Foreground(Pink) + ModuleStyle = Regular.Foreground(DarkishGreen) + WorkspaceStyle = Regular.Foreground(Purple) ) diff --git a/internal/tui/top/model.go b/internal/tui/top/model.go index 50bbbba..7b2f3a6 100644 --- a/internal/tui/top/model.go +++ b/internal/tui/top/model.go @@ -72,6 +72,7 @@ func newModel(cfg app.Config, app *app.App) (model, error) { States: app.States, Tasks: app.Tasks, Logger: app.Logger, + Workdir: cfg.Workdir, } spinner := spinner.New(spinner.WithSpinner(spinner.Line)) makers := makeMakers(cfg, app, &spinner, helpers) @@ -353,7 +354,6 @@ func (m model) View() string { Render(m.info) } footer += versionWidget - footer += pugIconWidget // Add footer components = append(components, tui.Regular. Inline(true). @@ -366,13 +366,12 @@ func (m model) View() string { var ( helpWidget = tui.Padded.Background(tui.Grey).Foreground(tui.White).Render("? help") - pugIconWidget = tui.Padded.Background(tui.HotPink).Foreground(tui.EvenLighterGrey).Render("󰩃 ") versionWidget = tui.Padded.Background(tui.DarkGrey).Foreground(tui.White).Render(version.Version) ) func (m model) availableFooterMsgWidth() int { // -2 to accommodate padding - return max(0, m.width-lipgloss.Width(helpWidget)-lipgloss.Width(pugIconWidget)-lipgloss.Width(versionWidget)) + return max(0, m.width-lipgloss.Width(helpWidget)-lipgloss.Width(versionWidget)) } // type taskCompletionMsg struct {