Compare commits
	
		
			No commits in common. "main" and "feature/ucl" have entirely different histories.
		
	
	
		
			main
			...
			feature/uc
		
	
		
|  | @ -1,116 +0,0 @@ | |||
| name: Release | ||||
| 
 | ||||
| on: | ||||
|   push: | ||||
|     tags: | ||||
|       - 'v*' | ||||
| 
 | ||||
| jobs: | ||||
|   Build: | ||||
|     runs-on: docker | ||||
|     services: | ||||
|       localstack: | ||||
|         image: localstack/localstack | ||||
|         ports: | ||||
|           - "4566:4566" | ||||
|         env: | ||||
|           SERVICES: ssm,dynamodb | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v2 | ||||
|       - name: Setup Go | ||||
|         uses: actions/setup-go@v3 | ||||
|         with: | ||||
|           go-version: 1.25 | ||||
|       - name: Configure | ||||
|         run: | | ||||
|           git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika" | ||||
|       - name: Test | ||||
|         run: | | ||||
|           set -xue | ||||
|           go get ./... | ||||
|           go test -p 1 ./... | ||||
|         env: | ||||
|           TEST_DYNAMO_URL: "http://localstack:4566" | ||||
|           GOPRIVATE: "github:com/lmika/*" | ||||
| 
 | ||||
|   Site: | ||||
|     needs: Build | ||||
|     runs-on: docker | ||||
|     env: | ||||
|       NETLIFY_SITE_ID: 987651c8-4ffd-48d8-af67-4dbd49c48887 | ||||
|       NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v2 | ||||
|       - name: Setup Go | ||||
|         uses: actions/setup-go@v3 | ||||
|         with: | ||||
|           go-version: 1.24 | ||||
|       - uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version: 21.1 | ||||
|       - name: Install Hugo | ||||
|         run: | | ||||
|           curl -LO https://github.com/gohugoio/hugo/releases/download/v0.146.0/hugo_extended_0.146.0_linux-amd64.deb | ||||
|           apt install -y ./hugo_extended_0.146.0_linux-amd64.deb | ||||
|       - name: Install Netlify CLI | ||||
|         run: | | ||||
|           npm install netlify-cli@15.0.1 -g | ||||
|       - name: Build Site | ||||
|         run: | | ||||
|           cd _site | ||||
|           mkdir -p themes | ||||
|           git clone https://github.com/alex-shpak/hugo-book.git themes/hugo-book | ||||
|           npm install | ||||
|           hugo --minify | ||||
|       - name: Publish Site | ||||
|         run: | | ||||
|           cd _site | ||||
|           netlify deploy --dir docs --prod  | ||||
| 
 | ||||
|   'Release MacOS': | ||||
| #    needs: Build | ||||
|     runs-on: macos | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v2 | ||||
|       - name: Setup Go | ||||
|         uses: actions/setup-go@v3 | ||||
|         with: | ||||
|           go-version: 1.25 | ||||
|       - name: Configure | ||||
|         run: | | ||||
|           git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika" | ||||
|       - name: Setup Goreleaser | ||||
|         run: | | ||||
|           go install github.com/goreleaser/goreleaser/v2@v2.12.7 | ||||
|       - name: Release | ||||
|         run: | | ||||
|           goreleaser release -f macos.goreleaser.yml --skip=validate --clean | ||||
|         env: | ||||
|           GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||
|           HOMEBREW_TAP_PRIVATE_KEY: ${{ secrets.HOMEBREW_TAP_PRIVATE_KEY }} | ||||
| 
 | ||||
| #  release-linux: | ||||
| #    needs: build | ||||
| #    runs-on: ubuntu-latest | ||||
| #    steps: | ||||
| #      - name: Checkout | ||||
| #        uses: actions/checkout@v2 | ||||
| #      - name: Setup Go | ||||
| #        uses: actions/setup-go@v3 | ||||
| #        with: | ||||
| #          go-version: 1.22 | ||||
| #      - name: Configure | ||||
| #        run: | | ||||
| #          git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika" | ||||
| #      - name: Release | ||||
| #        uses: goreleaser/goreleaser-action@v1 | ||||
| #        if: startsWith(github.ref, 'refs/tags/') | ||||
| #        with: | ||||
| #          version: latest | ||||
| #          args: release -f linux.goreleaser.yml --skip=validate --clean | ||||
| #        env: | ||||
| #          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||
| #          HOMEBREW_GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }} | ||||
|  | @ -4,11 +4,13 @@ on: | |||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|       - feature/* | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
| 
 | ||||
| jobs: | ||||
|   Build: | ||||
|     runs-on: docker | ||||
|   build: | ||||
|     runs-on: ubuntu-latest | ||||
|     services: | ||||
|       localstack: | ||||
|         image: localstack/localstack | ||||
|  | @ -22,7 +24,7 @@ jobs: | |||
|       - name: Setup Go | ||||
|         uses: actions/setup-go@v3 | ||||
|         with: | ||||
|           go-version: 1.25 | ||||
|           go-version: 1.22 | ||||
|       - name: Configure | ||||
|         run: | | ||||
|           git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika" | ||||
|  | @ -32,5 +34,4 @@ jobs: | |||
|           go get ./... | ||||
|           go test -p 1 ./... | ||||
|         env: | ||||
|           TEST_DYNAMO_URL: "http://localstack:4566" | ||||
|           GOPRIVATE: "github:com/lmika/*" | ||||
|           GOPRIVATE: "github:com/lmika/*" | ||||
							
								
								
									
										81
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,81 @@ | |||
| name: release | ||||
| 
 | ||||
| on: | ||||
|   push: | ||||
|     tags: | ||||
|       - 'v*' | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: ubuntu-latest | ||||
|     services: | ||||
|       localstack: | ||||
|         image: localstack/localstack | ||||
|         ports: | ||||
|           - "4566:4566" | ||||
|         env: | ||||
|           SERVICES: ssm,dynamodb | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v2 | ||||
|       - name: Setup Go | ||||
|         uses: actions/setup-go@v3 | ||||
|         with: | ||||
|           go-version: 1.22 | ||||
|       - name: Configure | ||||
|         run: | | ||||
|           git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika" | ||||
|       - name: Test | ||||
|         run: | | ||||
|           set -xue | ||||
|           go get ./... | ||||
|           go test -p 1 ./... | ||||
|         env: | ||||
|           GOPRIVATE: "github:com/lmika/*" | ||||
| 
 | ||||
|   release-macos: | ||||
|     needs: build | ||||
|     runs-on: macos-12 | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v2 | ||||
|       - name: Setup Go | ||||
|         uses: actions/setup-go@v3 | ||||
|         with: | ||||
|           go-version: 1.22 | ||||
|       - name: Configure | ||||
|         run: | | ||||
|           git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika" | ||||
|       - name: Setup Goreleaser | ||||
|         run: | | ||||
|           brew install goreleaser/tap/goreleaser | ||||
|           brew install goreleaser | ||||
|       - name: Release | ||||
|         if: startsWith(github.ref, 'refs/tags/') | ||||
|         run: | | ||||
|           goreleaser release -f macos.goreleaser.yml --skip=validate --clean | ||||
|         env: | ||||
|           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||
|           HOMEBREW_GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }} | ||||
| 
 | ||||
|   release-linux: | ||||
|     needs: build | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v2 | ||||
|       - name: Setup Go | ||||
|         uses: actions/setup-go@v3 | ||||
|         with: | ||||
|           go-version: 1.22 | ||||
|       - name: Configure | ||||
|         run: | | ||||
|           git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika" | ||||
|       - name: Release | ||||
|         uses: goreleaser/goreleaser-action@v1 | ||||
|         if: startsWith(github.ref, 'refs/tags/') | ||||
|         with: | ||||
|           version: latest | ||||
|           args: release -f linux.goreleaser.yml --skip=validate --clean | ||||
|         env: | ||||
|           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||
|           HOMEBREW_GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }} | ||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -1,6 +1,3 @@ | |||
| debug.log | ||||
| .DS_store | ||||
| .idea | ||||
| 
 | ||||
| # Local Netlify folder | ||||
| .netlify | ||||
|  |  | |||
|  | @ -1,6 +0,0 @@ | |||
| --- | ||||
| title: "{{ replace .Name "-" " " | title }}" | ||||
| date: {{ .Date }} | ||||
| draft: true | ||||
| --- | ||||
| 
 | ||||
|  | @ -1,55 +0,0 @@ | |||
| :root { | ||||
|   --accent: #6591ff; | ||||
| } | ||||
| 
 | ||||
| figure.screenshot { | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| div.site-header { | ||||
|   text-align: center; | ||||
|   margin-bottom: 48px; | ||||
|   display: flex; | ||||
|    | ||||
|   flex-direction: row; | ||||
|   align-content: center; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
|    | ||||
|   & img { | ||||
|     width: 64px; | ||||
|     height: 64px; | ||||
|   } | ||||
|    | ||||
|   & h1 { | ||||
|     font-size: 2.2em; | ||||
|     font-variant: small-caps; | ||||
|     vertical-align: middle; | ||||
|     margin: 0; | ||||
|     margin-left: 16px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| kbd { | ||||
|   background: var(--body-font-color); | ||||
|   color: var(--body-background); | ||||
|   border-radius: 4px; | ||||
|   font-weight: bold; | ||||
|   padding:  2px 3px; | ||||
|   font-size: 1.0em; | ||||
| } | ||||
| 
 | ||||
| input { | ||||
|   -webkit-appearance: auto; | ||||
|   appearance: auto; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Keybinding settings. | ||||
|  */ | ||||
| table.key-bindings.show-binding-names .kb-key-binding { | ||||
|   display: none; | ||||
| } | ||||
| table.key-bindings:not(.show-binding-names) .kb-binding-name { | ||||
|   display: none; | ||||
| } | ||||
|  | @ -1,17 +0,0 @@ | |||
| import { Controller } from "@hotwired/stimulus" | ||||
| 
 | ||||
| export class KeybindingsController extends Controller { | ||||
| 	static targets = [ | ||||
| 		"showBindingNames", | ||||
| 		"keyBindingTable" | ||||
| 	]; | ||||
| 
 | ||||
| 	bindingNamesChanged() { | ||||
| 		let showBindingNames = this.showBindingNamesTarget; | ||||
| 		if (showBindingNames.checked) { | ||||
| 			this.keyBindingTableTarget.classList.add("show-binding-names"); | ||||
| 		} else { | ||||
| 			this.keyBindingTableTarget.classList.remove("show-binding-names"); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -1,5 +0,0 @@ | |||
| import { Application } from "@hotwired/stimulus"; | ||||
| import { KeybindingsController } from "./controllers/keybindings_controller"; | ||||
| 
 | ||||
| const application = Application.start(); | ||||
| application.register("keybindings", KeybindingsController); | ||||
|  | @ -1,10 +0,0 @@ | |||
| { | ||||
|  "compilerOptions": { | ||||
|   "baseUrl": ".", | ||||
|   "paths": { | ||||
|    "*": [ | ||||
|     "*" | ||||
|    ] | ||||
|   } | ||||
|  } | ||||
| } | ||||
|  | @ -1,19 +0,0 @@ | |||
| #!/usr/bin/env python3 | ||||
| 
 | ||||
| import os.path | ||||
| import subprocess | ||||
| 
 | ||||
| images = [ | ||||
| 	'main-item-view.png', | ||||
| 	'table-selection.png', | ||||
| 	'filter-items.png', | ||||
| 	'query-items.png', | ||||
| 	'modified-items.png', | ||||
| 	'fields-popup.png' | ||||
| ] | ||||
| 
 | ||||
| for image in images: | ||||
| 	src_file = os.path.join('images', image) | ||||
| 	target_file = os.path.join('static/images/dynamo-browse', image) | ||||
| 	 | ||||
| 	subprocess.run(['magick', src_file, '-shave', '24x16', target_file]) | ||||
|  | @ -1,34 +0,0 @@ | |||
| baseURL = "https://dynamobrowse.app/" | ||||
| languageCode = "en-us" | ||||
| title = "Dynamo-Browse" | ||||
| 
 | ||||
| theme = "hugo-book" | ||||
| 
 | ||||
| publishDir = "docs" | ||||
| 
 | ||||
| [markup.goldmark.renderer] | ||||
|   unsafe = true | ||||
|   | ||||
| [params]	 | ||||
| 	BookTheme = "auto" | ||||
| 	BookMenuBundle = "/menu" | ||||
| 	 | ||||
| [params.Logo] | ||||
| 	logoText = "Dynamo-Browse" | ||||
| 
 | ||||
| [menu] | ||||
| [[menu.main]] | ||||
| 	identifier = "download" | ||||
| 	name = "Download" | ||||
| 	url = "/download" | ||||
| 	weight = 20 | ||||
| [[menu.main]] | ||||
| 	identifier = "docs" | ||||
| 	name = "Documentation" | ||||
| 	url = "/docs" | ||||
| 	weight = 40 | ||||
| [[menu.main]] | ||||
| 	identifier = "updates" | ||||
| 	name = "Updates" | ||||
| 	url = "/updates" | ||||
| 	weight = 50 | ||||
|  | @ -1,31 +0,0 @@ | |||
| +++ | ||||
| title = "Dynamo-Browse" | ||||
| bookToc = false | ||||
| +++ | ||||
| 
 | ||||
| <div class="site-header"> | ||||
| 	<img src="/images/dynamo-browse/dynamo-browse-logo.png"> | ||||
| 	<h1>Dynamo-Browse</h1> | ||||
| </div> | ||||
| 
 | ||||
| <figure class="screenshot"> | ||||
| 	<img src="/images/dynamo-browse/main-item-view.png" alt="dynamo-browse"> | ||||
| </figure> | ||||
| 
 | ||||
| Dynamo-Browse is a terminal-based UI (TUI) app for working with DynamoDB tables. | ||||
| With it, you can quickly connect to and browse the contents of a DynamoDB table | ||||
| in your AWS account or local machine.  There are some basic facilities for | ||||
| editing as well. | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| ## Getting Started | ||||
| 
 | ||||
| Instructions for installing Dynamo-Browse can be found on the [Downloads page](/download). | ||||
| 
 | ||||
| This video gives a brief introduction of how to use Dynamo-Browse to view the items of a DynamoDB table: | ||||
| 
 | ||||
| {{< youtube cQnTIg1_tfg >}} | ||||
| 
 | ||||
| More information about the tool can be found within the [Dynamo-Browse user manual](/docs).   | ||||
| 
 | ||||
|  | @ -1,35 +0,0 @@ | |||
| # User Guide | ||||
| 
 | ||||
| ## Table Of Contents | ||||
| 
 | ||||
| - [Launching and Quitting](/docs/launching) | ||||
| 	- [Selecting a Table](/docs/launching#selecting-a-table) | ||||
| 	- [Selecting a Workspace](/docs/launching#selecting-a-workspace) | ||||
| 	- [Quitting](/docs/launching#quitting) | ||||
| - [Getting Around](/docs/getting-around) | ||||
| 	- [The Back-stack](/docs/getting-around#the-back-stack) | ||||
| 	- [Adjusting The Layout](/docs/getting-around#adjusting-the-layout) | ||||
| 	- [Adjusting The Displayed Columns](/docs/getting-around#adjusting-the-displayed-columns) | ||||
| 	- [Entering Commands](/docs/getting-around#entering-commands) | ||||
| - [Filtering and Querying](/docs/filtering-querying) | ||||
| 	- [Filtering](/docs/filtering-querying#filtering) | ||||
| 	- [Querying](/docs/filtering-querying#querying) | ||||
| - [Editing Items](/docs/editing-items) | ||||
| 	- [Marking Items](/docs/editing-items#marking-items) | ||||
| 	- [Modifying Attributes](/docs/editing-items#modifying-attributes) | ||||
| 	- [Deleting Attributes](/docs/editing-items#deleting-attributes) | ||||
| 	- [Adding Items](/docs/editing-items#adding-items) | ||||
| 	- [Deleting Items](/docs/editing-items#deleting-items) | ||||
| 	- [Committing Changes](/docs/editing-items#committing-changes) | ||||
| 	- [Backing Out of Changes](/docs/editing-items#backing-out-of-changes) | ||||
| - [Customising Dynamo-Browse](/docs/customising) | ||||
| 	- [The RC File](/docs/customising#rc-file) | ||||
| 	- [Rebinding Keys](/docs/customising#rebinding-keys) | ||||
| 
 | ||||
| References | ||||
| 
 | ||||
| - [Key Bindings](/docs/reference/key-bindings) | ||||
| - [Commands](/docs/reference/commands) | ||||
| - [Query Expressions](/docs/reference/query-expressions) | ||||
| - [Launch Flags](/docs/reference/launch-flags) | ||||
| - [Settings](/docs/reference/settings) | ||||
|  | @ -1,41 +0,0 @@ | |||
| # Customising Dynamo-Browse | ||||
| 
 | ||||
| Some commands can be used to customise Dynamo-Browse, such as modify key bindings. | ||||
| The effect of these commands will only be applied for the duration of the session: they are currently not | ||||
| tracked within the workspace file.  So in order to keep customisations across relaunches, these commands | ||||
| can be added to an RC file. | ||||
| 
 | ||||
| ## The RC File | ||||
| 
 | ||||
| The RC file is a text file containing commands that will be executed by Dynamo-Browse upon launch. | ||||
| By default, the RC file is located at the following path: | ||||
| 
 | ||||
| ``` | ||||
| $HOME/.config/audax/dynamo-browse/init.rc | ||||
| ``` | ||||
| 
 | ||||
| This file is primarily intended for commands that customise Dynamo-Browse in a particular way, but any | ||||
| command can be entered here.  If this file is found, Dynamo-Browse will invoke each command before loading | ||||
| or prompting the table. | ||||
| 
 | ||||
| ## Rebinding Keys | ||||
| 
 | ||||
| The default key bindings of Dynamo-Browse can be changed using the [rebind](/docs/reference/commands#rebind) command.  This takes | ||||
| a binding name corresponding to the particular action to invoke, and the key | ||||
| to which it should be mapped to. | ||||
| 
 | ||||
| Putting these commands in the RC file will effectively change the default bindings of Dynamo-Browse. | ||||
| 
 | ||||
| ``` | ||||
| # Rebind T to prompt for a table | ||||
| rebind "view.prompt-for-table" "T" | ||||
| 
 | ||||
| # Rebind escape to prompt for a command | ||||
| rebind "view.prompt-for-command" "esc" | ||||
| ``` | ||||
| 
 | ||||
| At the moment each binding name can only be mapped to a single key.  It's also currently not possible | ||||
| to setup bindings for commands.  These may be supported in the future. | ||||
| 
 | ||||
| A list of available binding names can be found the the [reference](/docs/reference/key-bindings) | ||||
| (check the "Show binding names" checkbox).  Note that some bindings may not have default key bindings. | ||||
|  | @ -1,91 +0,0 @@ | |||
| # Editing Items | ||||
| 
 | ||||
| Dynamo-Browse offers some basic facilities for editing items — such as creating items, deleting items, | ||||
| and modifying their attribute values. | ||||
| 
 | ||||
| <figure class="screenshot"> | ||||
| 	<img src="/images/dynamo-browse/modified-items.png" alt="Item indicators"> | ||||
| </figure> | ||||
| 
 | ||||
| ## Marking Items | ||||
| 
 | ||||
| Most modifications are applied to items that are marked.  A marked item is indicated by a grey | ||||
| background and a bullet indicator (`•`) on the left side of the table. | ||||
| 
 | ||||
| To mark or unmark the selected item, press <kbd>m</kbd>. | ||||
| 
 | ||||
| The command `unmark` can be used to clear all marked items. | ||||
| 
 | ||||
| ## Modifying Attributes | ||||
| 
 | ||||
| Item attributes can be added or modified by using the command `set-attr` or the alias `sa`. | ||||
| This command can be used to modify the value and type of an attribute of the currently selected items, or | ||||
| from any marked items. | ||||
| 
 | ||||
| The format of the command is as follows: | ||||
| 
 | ||||
| ``` | ||||
| :set-attr [<type>] <attributeName> | ||||
| ``` | ||||
| 
 | ||||
| Where type is one of the following (case insensitive): | ||||
| 
 | ||||
| - `-S`: string | ||||
| - `-N`: number | ||||
| - `-BOOL`: boolean | ||||
| - `-NULL`: null | ||||
| 
 | ||||
| If the type is not specified, and the attribute exists, then the attribute type will not change. | ||||
| The type must be specified if this is a new attribute or multiple items have been marked. | ||||
| After executing the command, Dynamo-Browse will prompt for the value of the new attribute if one is | ||||
| required. | ||||
| 
 | ||||
| Modified attributes will only be tracked in memory: they will not be written | ||||
| to the actual table until it is "putted" (see [Committing Changes](#committing-changes)). | ||||
| An item that has been modified will be displayed in red and a modified indicator (`M`) will appear | ||||
| on the left-most column. | ||||
| 
 | ||||
| ## Deleting Attributes | ||||
| 
 | ||||
| An attribute can be deleted by using the command `del-attr` or the alias `da`.  The format of the command | ||||
| is as follows: | ||||
| 
 | ||||
| ``` | ||||
| :del-attr <attributeName> | ||||
| ``` | ||||
| 
 | ||||
| When executed, the attribute with the name _attributeName_ will be deleted from the selected item, or | ||||
| from any marked items. | ||||
| 
 | ||||
| Deleted attributes will only be tracked in memory: they will not be removed from | ||||
| the actual table until it is "putted" (see [Committing Changes](#committing-changes)). | ||||
| An item that has been modified will be displayed in red and a modified indicator (`M`) will appear | ||||
| on the left-most column. | ||||
| 
 | ||||
| ## Adding Items | ||||
| 
 | ||||
| A new item can be created by typing in the command `new-item`.   | ||||
| 
 | ||||
| When entered, Dynamo-Browse will prompt for the partition and sort key.  Once these are entered, | ||||
| the item will appear in the top pane in green with an asterisk indicator (`*`) on the left left-most column. | ||||
| Any additional attributes can be set by using `set-attr`. | ||||
| 
 | ||||
| A new item will only appear in memory: it will not be written | ||||
| to the actual table until it is "putted" (see [Committing Changes](#committing-changes)). | ||||
| 
 | ||||
| ## Deleting Items | ||||
| 
 | ||||
| Items can be deleted by marking them and then typing in the command `delete`. | ||||
| 
 | ||||
| Unlike most of the other modified commands, running `delete` WILL make changes to the table | ||||
| immediately. | ||||
| 
 | ||||
| ## Committing Changes | ||||
| 
 | ||||
| New or modified items (but not deleted items) will be kept in memory until they are committed | ||||
| or "putted" to the table.  To put the changes, use the `put` command or `w` alias. | ||||
| 
 | ||||
| ## Backing Out of Changes | ||||
| 
 | ||||
| Any modified items can be reverted back to what they are in the actual table by rerunning the | ||||
| current query.  This can be done by pressing <kbd>⇧R</kbd>.  | ||||
|  | @ -1,39 +0,0 @@ | |||
| # Querying And Viewing Results | ||||
| 
 | ||||
| ## Querying | ||||
| 
 | ||||
| <figure class="screenshot"> | ||||
| 	<img src="/images/dynamo-browse/query-items.png" alt="Items with query applied"> | ||||
| </figure> | ||||
| 
 | ||||
| A query or scan over the table can be performed by entering a _Query Expression_. | ||||
| Query expressions are a built-in expression language which translates to either a DynamoDB query | ||||
| or scan, depending on the expression.  Details about the Query Expression language can be found in the | ||||
| [Query Expressions references](/docs/reference/query-expressions/). | ||||
| 
 | ||||
| To run a query, press <kbd>?</kbd>, and enter the query expression. | ||||
| 
 | ||||
| To clear a query, press <kbd>?</kbd>, and press <kbd>Enter</kbd> without entering any value. | ||||
| 
 | ||||
| While the query is running, a spinner indicating activity will be shown in the status bar.  A running | ||||
| query can be cancelled while this spinner is visible by pressing <kbd>^C</kbd>.  You have the option | ||||
| to view any partial results that have been retrieved at the time. | ||||
| 
 | ||||
| ## Filtering | ||||
| 
 | ||||
| <figure class="screenshot"> | ||||
| 	<img src="/images/dynamo-browse/filter-items.png" alt="Items with filter applied"> | ||||
| </figure> | ||||
| 
 | ||||
| The displayed items of the current result-set can be filtered down to those that contain a specific substring. | ||||
| 
 | ||||
| To set the filter, press <kbd>/</kbd>, and enter the substring you wish to filter on.   | ||||
| 
 | ||||
| To clear the filter, press <kbd>/</kbd>, and press <kbd>Enter</kbd> without entering any value. | ||||
| 
 | ||||
| When a filter is set, any item that does not have a top-level attribute containing the substring will be hidden. | ||||
| 
 | ||||
| Filtering will only consist the items that are in the current result-set.  It will not result in a call to the actual | ||||
| table itself. | ||||
| 
 | ||||
| Note that filtering is case sensitive. | ||||
|  | @ -1,153 +0,0 @@ | |||
| # Getting Around | ||||
| 
 | ||||
| After selecting a table, Dynamo-Browse will perform a scan and present the results in the default view mode. | ||||
| 
 | ||||
| <figure class="screenshot"> | ||||
|   <img src="/images/dynamo-browse/main-item-view.png" alt="Main item view"> | ||||
| </figure> | ||||
| 
 | ||||
| This mode consists of three panes: | ||||
| 
 | ||||
| - The top pane displays the result-set of the last scan or query.  The table name is at the top-left. | ||||
| - The middle pane displays the attributes of the currently selected item, along with their type. | ||||
| - The bottom pane displays the current query or filter, plus any messages.  Prompts for input will | ||||
|   also appear at the bottom. | ||||
| 
 | ||||
| The result-set is sorted in ascending order based on the value and type of the partition and sort key. | ||||
| Up to 1,000 rows will be displayed for the current result-set. | ||||
| 
 | ||||
| Since DynamoDB does not require all items to have the same attribute (unless they are pre-defined), any | ||||
| attribute not set for a column is indicated with a grey tilde character `~`. | ||||
| 
 | ||||
| Use the following keys to change the currently selected row, which is highlighted in purple: | ||||
| 
 | ||||
| - <kbd>↑</kbd>/<kbd>i</kbd>: Move selection up | ||||
| - <kbd>↓</kbd>/<kbd>k</kbd>: Move selection down | ||||
| - <kbd>PgUp</kbd>/<kbd>⇧I</kbd>: Page up | ||||
| - <kbd>PgDn</kbd>/<kbd>⇧K</kbd>: Page down | ||||
| - <kbd>Home</kbd>/<kbd>0</kbd>: First row | ||||
| - <kbd>End</kbd>/<kbd>$</kbd>: Last row | ||||
| 
 | ||||
| The columns of the table  | ||||
| consist of the top-level attributes of the result-set.  The partition key, sort key, plus any explicitly defined | ||||
| attributes will always be displayed from the left margin onwards.  The other attributes are determined | ||||
| from the results of the last scan or query, and may change depending on the result. | ||||
| 
 | ||||
| The display columns of the table can be scrolled across by using the following keys: | ||||
| 
 | ||||
| - <kbd>←</kbd>/<kbd>j</kbd>: Scroll to the left | ||||
| - <kbd>→</kbd>/<kbd>l</kbd>: Scroll to the right | ||||
| 
 | ||||
| The attributes of the currently selected item will appear in the middle pane.  Both the type and the value of each | ||||
| attribute will be displayed.  Any nested attributes will be indented, and will below their parent item.  A value | ||||
| displayed in grey does not represent the actual value of the item, but indicates some meta-information about the item, | ||||
| such as the length. | ||||
| 
 | ||||
| ## The Back-stack | ||||
| 
 | ||||
| Changes to the view of Dynamo-Browse will be maintained in back-stack, similar to how a | ||||
| web-browse keeps track of the webpages you've visited.  This stack will record the | ||||
| currently viewed table, filter, or query, allowing you to "go back" to a previous view | ||||
| by pressing <kbd>Backspace</kbd>.  Pressing <kbd>\\</kbd> will allow you to go forward through the stack. | ||||
| 
 | ||||
| The back-stack is preserved in the workspace file, and can be restored by launching Dynamo-Browse with the `-w` | ||||
| switch.  Launching Dynamo-Browse with a workspace that has a non-empty stack will restore the last viewed | ||||
| table, filter, or query from the session that was previously using the workspace. | ||||
| 
 | ||||
| {{<hint info>}} | ||||
|   **Note:** the back-stack does not preserve the actual items in the workspace.  Going backwards or forwards | ||||
|   through the back-stack will execute any queries or filters against the actual table itself. | ||||
| {{</hint>}} | ||||
| 
 | ||||
| ## Adjusting The Layout | ||||
| 
 | ||||
| The horizontal size of the item table and currently selected item pane can be changed to one of the | ||||
| following layout configurations: | ||||
| 
 | ||||
| - Item view taking up 14 rows on the bottom with the table pane taking up the rest of the vertical space (the default) | ||||
| - Item view and table view taking up half of the available space | ||||
| - Table view taking up 7 rows on the top with the item view taking up the rest of the vertical space | ||||
| - Table view hidden | ||||
| - Item view hidden | ||||
| 
 | ||||
| Pressing <kbd>w</kbd> will cycle forward though these layouts.  For example, while in the | ||||
| default layout, pressing <kbd>w</kbd> will switch to the second layout, where both the table view take up half the | ||||
| screen.  Pressing <kbd>⇧W</kbd> will cycle through the layouts in the reverse order. | ||||
| 
 | ||||
| ## Adjusting The Displayed Columns | ||||
| 
 | ||||
| The columns of the result-set can be adjusted by opening up the _Fields Popup_.  This popup can be opened by pressing <kbd>f</kbd>. | ||||
| 
 | ||||
| <figure class="screenshot"> | ||||
|   <img src="/images/dynamo-browse/fields-popup.png" alt="dynamo-browse"> | ||||
| </figure> | ||||
| 
 | ||||
| While this popup is opened, the following changes can be applied to the displayed columns of the main table: | ||||
| 
 | ||||
| - Columns can be hidden | ||||
| - The order columns appear in the main table can be rearranged  | ||||
| - New columns can be added | ||||
| 
 | ||||
| The popup will display the list of columns of the main result-set table.  Pressing <kbd>↑</kbd>/<kbd>i</kbd> | ||||
| or <kbd>↓</kbd>/<kbd>k</kbd> will move the selection indicator to the column to apply the operation.  Pressing | ||||
| <kbd>←</kbd>/<kbd>j</kbd> or <kbd>→</kbd>/<kbd>l</kbd> will scroll the main table left or right so that any | ||||
| operations can be previewed. | ||||
| 
 | ||||
| To reset the columns to the top-level fields of the current result set, press <kbd>⇧R</kbd>. | ||||
| 
 | ||||
| To close the popup, press <kbd>Escape</kbd>. | ||||
| 
 | ||||
| ### Showing And Hiding Columns | ||||
| 
 | ||||
| In the Fields Popup, each row has a symbol indicating whether the row is currently visible (`.`) or hidden (`✕`).  Pressing | ||||
| <kbd>Space</kbd> will toggle whether the currently selected column is shown or hidden. | ||||
| 
 | ||||
| ### Re-arranging The Order Of Columns | ||||
| 
 | ||||
| The currently selected row can be moved up or down the table.  This will move the corresponding column in the main table either | ||||
| left or right. | ||||
| 
 | ||||
| Press <kbd>⇧I</kbd> to the selected row up, which will move the corresponding column left. | ||||
| 
 | ||||
| Press <kbd>⇧K</kbd> to the selected row down, which will move the corresponding column right. | ||||
| 
 | ||||
| ### Adding And Removing Columns | ||||
| 
 | ||||
| New columns can be added in the table.  The value of these columns will be determined by the result of a query expression, | ||||
| and can be used to expose fields that are not at the top level. | ||||
| 
 | ||||
| Any nested fields of maps or lists will not be included as a column by default.  Consider, for example, a table of books | ||||
| with authors structured as so: | ||||
| 
 | ||||
| ``` | ||||
| { | ||||
|   "book": {"S": "The Lord Of The Rings"}, | ||||
|   "author": {"M": { | ||||
|     "firstName": {"S": "John"}, | ||||
|     "middleName": {"S": "Ronald Reuel"}, | ||||
|     "lastName": {"S": "Tolkien"}, | ||||
|   }} | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| If you wanted to show the the author's first and last name in the main table, rather than just see the description `(3 items)`, you | ||||
| can add a new column with an expression selecting the fields of the author map.  The expressions that can be used here | ||||
| are as follows: | ||||
| 
 | ||||
| - First name: `author.firstName` | ||||
| - Last name: `author.lastName`  | ||||
| 
 | ||||
| This can be extended to expressions that perform comparisons or operations.  For example, the expression `author.firstName ^= "J"` can be | ||||
| use in a new column to display `True` for any first name that begins with a J. | ||||
| 
 | ||||
| To add a new column, press <kbd>a</kbd> while the Fields Popup is visible.  You'll be prompted to enter a query expression, | ||||
| which will be evaluated over each row within the result-set when displaying the table. | ||||
| 
 | ||||
| Any column, that was either retrieved from the result-set or added by the user, can be deleted by selecting the column | ||||
| within the Fields Popup and pressing <kbd>d</kbd>.  | ||||
| 
 | ||||
| ## Entering Commands | ||||
| 
 | ||||
| Commands can be entered by pressing <kbd>:</kbd> and entering the command, with any arguments, at the prompt. | ||||
| 
 | ||||
| The list of available commands can be found within the [reference section](/docs/reference/#commands). | ||||
|  | @ -1,62 +0,0 @@ | |||
| # Launching And Quitting | ||||
| 
 | ||||
| To launch Dynamo-Browse, run the following command at the terminal: | ||||
| 
 | ||||
| ``` | ||||
| dynamo-browse | ||||
| ``` | ||||
| 
 | ||||
| This will use your current AWS configuration and region, which can be changed by setting | ||||
| the relevant `AWS_` environment variables. | ||||
| 
 | ||||
| To connect to a local instance of DynamoDB, such as one | ||||
| running in a Docker container, use the `--local` flag.  This takes as the argument the hostname | ||||
| and endpoint of the local DynamoDB server.  The hostname can be omitted, and will default to `localhost`: | ||||
| 
 | ||||
| ``` | ||||
| dynamo-browse --local :8080 | ||||
| ``` | ||||
| 
 | ||||
| ## Selecting a Table | ||||
| 
 | ||||
| Upon launch, Dynamo-Browse will present a list of all the tables within the region: | ||||
| 
 | ||||
| <figure class="screenshot"> | ||||
| 	<img src="/images/dynamo-browse/table-selection.png" alt="Table selection"> | ||||
| </figure> | ||||
| 
 | ||||
| Select the table to view by pressing <kbd>Enter</kbd>.  Use the following keys to navigate | ||||
| the items within the list: | ||||
| 
 | ||||
| - <kbd>↑</kbd>/<kbd>i</kbd>: Move selection up | ||||
| - <kbd>↓</kbd>/<kbd>k</kbd>: Move selection down | ||||
| 
 | ||||
| Once the table is selected, the table will be scanned and Dynamo-Browse will be presented in | ||||
| [View Mode](#view-mode).  Another table can be selected from within view mode using the `:table` command. | ||||
| 
 | ||||
| Dynamo-Browse can also be launched directly in view mode by specifying a table using the `-t` flag: | ||||
| 
 | ||||
| ``` | ||||
| dynamo-browse -t user-accounts | ||||
| ``` | ||||
| 
 | ||||
| ## Selecting a Workspace | ||||
| 
 | ||||
| Dynamo-Browse tracks session state, such as the back-stack, in a workspace file.  By default the workspace | ||||
| file will be a new file created within the temporary directory, but a specific workspace filename can be | ||||
| specified by using the `-w` flag: | ||||
| 
 | ||||
| ``` | ||||
| dynamo-browse -w my-workspace.ws | ||||
| ``` | ||||
| 
 | ||||
| If the workspace filename references an existing file, Dynamo-Browse will restore the workspace and use it for the duration of | ||||
| the session.  If the workspace filename references a non-existing file, Dynamo-Browse will initialise a new workspace | ||||
| using the specified filename. | ||||
| 
 | ||||
| Only one running instance of Dynamo-Browse can use a single workspace file at any one time. | ||||
| 
 | ||||
| ## Quitting | ||||
| 
 | ||||
| To quit dynamodb-browse, enter the command `q` by pressing <kbd>:</kbd>, then typing <kbd>q</kbd> <kbd>Enter</kbd>. | ||||
| The keystroke <kbd>Ctrl+C</kbd> can also be used to quit. | ||||
|  | @ -1,150 +0,0 @@ | |||
| # Commands | ||||
| 
 | ||||
| ## clone | ||||
| 
 | ||||
| ``` | ||||
| :clone | ||||
| ``` | ||||
| 
 | ||||
| Copies the currently selected item to a new item, which will appear at the bottom of the table. | ||||
| 
 | ||||
| Cloning an item will prompt for a new partition key and sort key but will not check for duplicates. | ||||
| 
 | ||||
| ## del-attr | ||||
| 
 | ||||
| ``` | ||||
| :del-attr <attribute> | ||||
| ``` | ||||
| 
 | ||||
| Alias: `da` | ||||
| 
 | ||||
| Deletes _attribute_ from the currently selected item; or if there are any marked items, the marked items. | ||||
| 
 | ||||
| ## delete | ||||
| 
 | ||||
| ``` | ||||
| :delete | ||||
| ``` | ||||
| 
 | ||||
| Deletes the marked items.  Unlike the other commands that modify items, this command will be executed on | ||||
| the table straight away. | ||||
| 
 | ||||
| ## echo | ||||
| 
 | ||||
| ``` | ||||
| :echo [message ...] | ||||
| ``` | ||||
| 
 | ||||
| Displays _message_ in the status bar.  Mainly used for debugging. | ||||
| 
 | ||||
| ## export | ||||
| 
 | ||||
| ``` | ||||
| :export [-all] <filename> | ||||
| ``` | ||||
| 
 | ||||
| Writes the currently loaded items as a CSV file to _filename_. | ||||
| 
 | ||||
| Only string, numerical, and boolean values will be written to the export; all other value types will be | ||||
| black. Exporting will honour the columns currently visible in the table. Filtered items will also be included | ||||
| in the exported file. | ||||
| 
 | ||||
| When called with the `-all` flag, any subsequent pages will be included in the export. If invoked after running | ||||
| a query, all items returned from that query will be exported to file. | ||||
| 
 | ||||
| ## mark | ||||
| 
 | ||||
| ``` | ||||
| :mark [all | none | toggle] [-where <expr>] | ||||
| ``` | ||||
| 
 | ||||
| Mark the rows in the following way: | ||||
| 
 | ||||
| - `all`: will mark all rows.  This is the default when invoked without an argument. | ||||
| - `none`: will unmark all rows. | ||||
| - `toggle`: will toggle all marked and unmarked rows. | ||||
| 
 | ||||
| Adding the `-where` option would only select rows that match the given query expression. | ||||
| 
 | ||||
| 
 | ||||
| ## new-item | ||||
| 
 | ||||
| ``` | ||||
| :new-item | ||||
| ``` | ||||
| 
 | ||||
| Creates a new item.  When executed, the value for the partition key and sort key will be prompted. | ||||
| The new item will not be written to the table until it is committed with the `put` command. | ||||
| 
 | ||||
| ## put | ||||
| 
 | ||||
| ``` | ||||
| :put | ||||
| ``` | ||||
| 
 | ||||
| Alias: `w` | ||||
| 
 | ||||
| Commits all new and modified items to the table. | ||||
| 
 | ||||
| ## quit | ||||
| 
 | ||||
| ``` | ||||
| :quit | ||||
| ``` | ||||
| 
 | ||||
| Alias: `q` | ||||
| 
 | ||||
| Quits Dynamo-Browse. | ||||
| 
 | ||||
| ## rebind | ||||
| 
 | ||||
| ``` | ||||
| :rebind <bindingName> <key> | ||||
| ``` | ||||
| 
 | ||||
| Rebinds the action with _bindingName_ to _key_.  This will replace any existing binding for that action. | ||||
| See [Key Bindings](#key-bindings) with "Show binding names" checked to see available binding names. | ||||
| 
 | ||||
| ## set | ||||
| 
 | ||||
| ``` | ||||
| :set <name> [value] | ||||
| ``` | ||||
| 
 | ||||
| Set the value of a setting.  Flag setting types can be enabled without any value.  See [Settings](#settings) for possible setting values. | ||||
| 
 | ||||
| ## set-attr | ||||
| 
 | ||||
| ``` | ||||
| :set-attr [type] <attributeName> | ||||
| ``` | ||||
| 
 | ||||
| Alias: `sa` | ||||
| 
 | ||||
| Modifies the value of _attribute_ of the currently selected item; or if there are any marked items, the marked items. | ||||
| 
 | ||||
| The value of _type_ can be use to specify the type of the attribute.  It can be one of the following (case insensitive): | ||||
| 
 | ||||
| - `-S`: string value | ||||
| - `-N`: number value | ||||
| - `-BOOL`: boolean value | ||||
| - `-NULL`: null value | ||||
| - `-TO`: value of an expression | ||||
| 
 | ||||
| If unset, the attribute type will not be changed.  _type_ must be set if multiple items have been marked. | ||||
| 
 | ||||
| ## table | ||||
| 
 | ||||
| ``` | ||||
| :table | ||||
| ``` | ||||
| 
 | ||||
| Select the table to display. | ||||
| 
 | ||||
| ## unmark | ||||
| 
 | ||||
| ``` | ||||
| :unmark | ||||
| ``` | ||||
| 
 | ||||
| Unmark all marked items.  This is essentially an alias for `mark none`. | ||||
|  | @ -1,189 +0,0 @@ | |||
| # Key Bindings | ||||
| 
 | ||||
| <div data-controller="keybindings"> | ||||
| 
 | ||||
| <label> | ||||
| 	<input type="checkbox" id="show-kb-binding-names"  | ||||
|     data-keybindings-target="showBindingNames" data-action="keybindings#bindingNamesChanged"> Show binding names | ||||
| </label> | ||||
| 
 | ||||
| <table class="key-bindings" data-keybindings-target="keyBindingTable"> | ||||
| <thead> | ||||
| <tr> | ||||
|   <th class="kb-key-binding" style="text-align:left">Key</th> | ||||
|   <th class="kb-binding-name" style="text-align:left">Binding Name</th> | ||||
|   <th style="text-align:left">Action</th> | ||||
| </tr> | ||||
| </thead> | ||||
| <tbody> | ||||
| <tr> | ||||
|   <td colspan="2">Main View Mode</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>↑</kbd>/<kbd>i</kbd></td> | ||||
|   <td class="kb-binding-name">table.move-up</td> | ||||
|   <td>Move selection up</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>↓</kbd>/<kbd>k</kbd></td> | ||||
|   <td class="kb-binding-name">table.move-down</td> | ||||
|   <td>Move selection down</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>PgUp</kbd>/<kbd>⇧I</kbd></td> | ||||
|   <td class="kb-binding-name">table.page-up</td> | ||||
|   <td>Page up</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>PgDn</kbd>/<kbd>⇧K</kbd></td> | ||||
|   <td class="kb-binding-name">table.page-down</td> | ||||
|   <td>Page down</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>Home</kbd>/<kbd>0</kbd></td> | ||||
|   <td class="kb-binding-name">table.goto-top</td> | ||||
|   <td>Move selection to first item</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>End</kbd>/<kbd>$</kbd></td> | ||||
|   <td class="kb-binding-name">table.goto-bottom</td> | ||||
|   <td>Move selection to last item</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>←</kbd>/<kbd>j</kbd></td> | ||||
|   <td class="kb-binding-name">table.move-left</td> | ||||
|   <td>Scroll displayed columns left</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>→</kbd>/<kbd>l</kbd></td> | ||||
|   <td class="kb-binding-name">table.move-right</td> | ||||
|   <td>Scroll displayed columns right</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>Backspace</kbd></td> | ||||
|   <td class="kb-binding-name">view.view-back</td> | ||||
|   <td>Go back</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>\</kbd></td> | ||||
|   <td class="kb-binding-name">view.view-forward</td> | ||||
|   <td>Go forward</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>w</kbd></td> | ||||
|   <td class="kb-binding-name">view.cycle-layout-forward</td> | ||||
|   <td>Cycle forward through layout</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>⇧W</kbd></td> | ||||
|   <td class="kb-binding-name">view.cycle-layout-backwards</td> | ||||
|   <td>Cycle backwards through layout</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>m</kbd></td> | ||||
|   <td class="kb-binding-name">view.mark</td> | ||||
|   <td>Mark/unmark currently selected item</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>M</kbd></td> | ||||
|   <td class="kb-binding-name">view.toggle-marked-items</td> | ||||
|   <td>Toggle marked/unmarked items</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>c</kbd></td> | ||||
|   <td class="kb-binding-name">view.copy-item-to-clipboard</td> | ||||
|   <td>Copy displayed item to pasteboard</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>C</kbd></td> | ||||
|   <td class="kb-binding-name">view.copy-table-to-clipboard</td> | ||||
|   <td>Copy displayed table to pasteboard as a CSV</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>/</kbd></td> | ||||
|   <td class="kb-binding-name">view.prompt-for-filter</td> | ||||
|   <td>Filter</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>?</kbd></td> | ||||
|   <td class="kb-binding-name">view.prompt-for-query</td> | ||||
|   <td>Run scan/query</td> | ||||
| </tr> | ||||
| <tr class="kb-binding-name"> | ||||
|   <td class="kb-key-binding"></td> | ||||
|   <td class="kb-binding-name">view.prompt-for-table</td> | ||||
|   <td>Select table</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>R</kbd></td> | ||||
|   <td class="kb-binding-name">view.rescan</td> | ||||
|   <td>Rerun last scan/query</td> | ||||
| </tr> | ||||
| <tr class="kb-binding-name"> | ||||
|   <td class="kb-key-binding"><kbd>></kbd></td> | ||||
|   <td class="kb-binding-name">view.fetch-next-page</td> | ||||
|   <td>Fetch the next page of results</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>:</kbd></td> | ||||
|   <td class="kb-binding-name">view.prompt-for-command</td> | ||||
|   <td>Enter command</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>f</td> | ||||
|   <td class="kb-binding-name">view.show-fields-popup</td> | ||||
|   <td>Show fields popup</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>^C</kbd></td> | ||||
|   <td class="kb-binding-name">view.cancel-running-job</td> | ||||
|   <td>Cancel running operation</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>^C</kbd>/<kbd>Esc</kbd></td> | ||||
|   <td class="kb-binding-name">view.quit</td> | ||||
|   <td>Quit</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td colspan="2">Field Popup Mode</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>⇧I</kbd></td> | ||||
|   <td class="kb-binding-name">fields-popup.shift-column-left</td> | ||||
|   <td>Shift selected column left</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>⇧K</kbd></td> | ||||
|   <td class="kb-binding-name">fields-popup.shift-column-right</td> | ||||
|   <td>Shift selected column right</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>Space</kbd></td> | ||||
|   <td class="kb-binding-name">fields-popup.toggle-column-visible</td> | ||||
|   <td>Toggle selected column visible</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>a</kbd></td> | ||||
|   <td class="kb-binding-name">fields-popup.add-column</td> | ||||
|   <td>Add new column</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>d</kbd></td> | ||||
|   <td class="kb-binding-name">fields-popup.delete-column</td> | ||||
|   <td>Delete selected column</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>⇧R</kbd></td> | ||||
|   <td class="kb-binding-name">fields-popup.reset-columns</td> | ||||
|   <td>Reset columns to that of the result-set</td> | ||||
| </tr> | ||||
| <tr> | ||||
|   <td class="kb-key-binding"><kbd>^C</kbd>/<kbd>Esc</kbd></td> | ||||
|   <td class="kb-binding-name">fields-popup.close</td> | ||||
|   <td>Close field popup</td> | ||||
| </tr> | ||||
| </tbody> | ||||
| </table> | ||||
| 
 | ||||
| </div> | ||||
| 
 | ||||
|  | @ -1,49 +0,0 @@ | |||
| # Launch Flags | ||||
| 
 | ||||
| ## -debug | ||||
| 
 | ||||
| ``` | ||||
| -debug <filename> | ||||
| ``` | ||||
| 
 | ||||
| Enable debug logs, which will be written to _filename_. | ||||
| 
 | ||||
| ## -default-limit | ||||
| 
 | ||||
| ``` | ||||
| -default-limit <int> | ||||
| ``` | ||||
| 
 | ||||
| Sets the default limit of queries or scans.  The default is 1,000 items. | ||||
| 
 | ||||
| ## -local | ||||
| 
 | ||||
| ``` | ||||
| -local [host]:<port> | ||||
| ``` | ||||
| 
 | ||||
| Connect to a local DynamoDB service listening on _host_:_port_.  The default _host_ is `localhost`. | ||||
| 
 | ||||
| ## -ro | ||||
| 
 | ||||
| ``` | ||||
| -ro | ||||
| ``` | ||||
| 
 | ||||
| Enable read-only mode. | ||||
| 
 | ||||
| ## -t | ||||
| 
 | ||||
| ``` | ||||
| -t <tableName> | ||||
| ``` | ||||
| 
 | ||||
| Open the table _tableName_, instead of prompting for a table. | ||||
| 
 | ||||
| ## -w | ||||
| 
 | ||||
| ``` | ||||
| -w <workspaceFile> | ||||
| ``` | ||||
| 
 | ||||
| Use _workspaceFile_ as the workspace file.  If unset, a temporary file will be used for the workspace. | ||||
|  | @ -1,251 +0,0 @@ | |||
| # Query Expression | ||||
| 
 | ||||
| Query expressions are used to select rows of a table.  When executed as a query (i.e. by pressing <kbd>?</kbd>), | ||||
| they will be translated into query or table scans that will run over the DynamoDB table in AWS. | ||||
| They work similar to the "where" clause in PartiQL except that they only require Query and Scan permission | ||||
| on the AWS table and do not require "select" clauses. | ||||
| Such expressions can also be used in other areas of Dynamo-Browse, such as populating the value of new columns. | ||||
| 
 | ||||
| ## Names And Values | ||||
| 
 | ||||
| A query expressions support the following literals: | ||||
| 
 | ||||
| - Strings: `"Hello"` | ||||
| - Integers: `123` | ||||
| - Boolean: `true` or `false` | ||||
| 
 | ||||
| Field names are represented as regular identifiers, such as `pk` or `address`. | ||||
| 
 | ||||
| ## Equality | ||||
| 
 | ||||
| To select rows with a field that equals a given value, use the `=` operator: | ||||
| 
 | ||||
| ``` | ||||
| pk = "something" | ||||
| ``` | ||||
| 
 | ||||
| Either operand will can be an identifier, placeholder, or value that resolves to any type. | ||||
| The result will be true if both the LHS and RHS equal the same type and value. If the types differ or | ||||
| the values differ, the result will be false. The field types can be different, but will always produce false. | ||||
| 
 | ||||
| The compliment is the `!=` operator: | ||||
| 
 | ||||
| ``` | ||||
| pk != "not this" | ||||
| ``` | ||||
| 
 | ||||
| ## Numerical Comparison | ||||
| 
 | ||||
| The operands `<`, `<=`, `>`, `>=` can be used to compare numerical fields and values: | ||||
| 
 | ||||
| ``` | ||||
| three < 5     // true | ||||
| three <= 3    // true | ||||
| three > 12    // false | ||||
| three >= 1    // true | ||||
| ``` | ||||
| 
 | ||||
| To verify that a number exists within a range, use the `between` operand: | ||||
| 
 | ||||
| ``` | ||||
| three between 1 and 5   // true | ||||
| ``` | ||||
| 
 | ||||
| ## Prefix Operator | ||||
| 
 | ||||
| To select rows with a field that starts with a given substring, use the `^=` operator: | ||||
| 
 | ||||
| ``` | ||||
| pk ^= "some" | ||||
| ``` | ||||
| 
 | ||||
| This is equivalent to using the [begins_with](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html) function in AWS query expressions. | ||||
| 
 | ||||
| ## Logical Operators | ||||
| 
 | ||||
| The logical operators `and`, `or` and `not` can be used to express conjunctions, disjunctions and logical negation | ||||
| between multiple expressions: | ||||
| 
 | ||||
| ``` | ||||
| pk = "this" and sk = "that" | ||||
| pk != "that" and count > 123 | ||||
| not count = 21 | ||||
| ``` | ||||
| 
 | ||||
| The order of precedence of these operators, from lowest to highest, is `or`, `and`, then `not`. This differs | ||||
| from AWS, in which all operators have the same precedence. For example, the query: | ||||
| 
 | ||||
| ``` | ||||
| pk="this" or pk="that" and sk="foo" | ||||
| ``` | ||||
| 
 | ||||
| is equivalent to: | ||||
| 
 | ||||
| ``` | ||||
| pk="this" or (pk="that" and sk="foo") | ||||
| ``` | ||||
| 
 | ||||
| The order can be overridden using brackets: | ||||
| 
 | ||||
| ``` | ||||
| (pk="this" or pk="that") and sk="foo" | ||||
| ``` | ||||
| {{< hint info >}} | ||||
| If a query expression is of the form `pk = <val>` or `pk = <val> and sk <op> <val>`, | ||||
| where: | ||||
| 
 | ||||
| - _pk_ and _sk_ are the partition and sort keys of the base table or a GSI, | ||||
| - _val_ resolves to a constant value, and, | ||||
| - _op_ is either `=`, `^=`, `<`, `<=`, `>`, `>=`, or `between` | ||||
| 
 | ||||
| the expression will be executed as a Query call. Unlike expressions on the AWS Query API method itself, | ||||
| the order of the `pk` and `sk` subexpressions can be swapped. | ||||
| 
 | ||||
| Other expressions are supported but they will be executed as a table Scan. | ||||
| {{< /hint >}} | ||||
| 
 | ||||
| ## The `in` Operator | ||||
| 
 | ||||
| The `in` operator can be used to determine if a value exists in a collection: | ||||
| 
 | ||||
| ``` | ||||
| three in (1, 2, 3, 4, 5) | ||||
| 
 | ||||
| name in ("Tom", "Dick", "Harry") | ||||
| ``` | ||||
| 
 | ||||
| The result will be a boolean, which will be true if the value of the LHS equals any of the items within the RHS. | ||||
| 
 | ||||
| The collection can be one or more fixed set of values within parenthesis separated by commas. A single | ||||
| value present within parenthesis is equivalent to the equality test: | ||||
| 
 | ||||
| ``` | ||||
| three in (3)   // equivalent to: three = 3 | ||||
| ``` | ||||
| 
 | ||||
| The right hand side can also be a subexpression without parenthesis that will resolve to either a string, | ||||
| list or map. The operand will behave differently based on the RHS type: | ||||
| 
 | ||||
| - If the RHS is a string, the result will be true if the LHS is a substring of the RHS (equivalent to the `contains` AWS conditional expressions function) | ||||
| - If the RHS is a list, the result will be true if the LHS equals any of the items of the list | ||||
| - If the RHS is a map, the result will be true if the LHS appears as a key of the map | ||||
| 
 | ||||
| The compliment operand is `not in`: | ||||
| 
 | ||||
| ``` | ||||
| three not in (6, 7, 8, 9) | ||||
| ``` | ||||
| 
 | ||||
| ## The `is` Operator | ||||
| 
 | ||||
| The `is` operator can be used to assert the value type.  The RHS operand is a string which is to represent an AWS | ||||
| DynamoDB item attribute type, for example `S` for strings, `N` for numbers, etc. | ||||
| 
 | ||||
| ``` | ||||
| "hello" is "S"    // true | ||||
| 123 is "N"        // true | ||||
| "hello" is "N"    // false | ||||
| ``` | ||||
| 
 | ||||
| This is equivalent to the `attribute_type` AWS conditional expressions function. | ||||
| 
 | ||||
| The special value `any` can be used to check that a field is set, regardless of type: | ||||
| 
 | ||||
| ``` | ||||
| pk is "any"  // true | ||||
| ``` | ||||
| 
 | ||||
| This is equivalent to the `attribute_exists` AWS conditional expressions function. | ||||
| 
 | ||||
| The compliment operand is `not is`.  Using it with the "any" special value (`not is "any"`) is equivalent to the | ||||
| `attribute_not_exists` AWS conditional expressions function. | ||||
| 
 | ||||
| ## The `using` Options | ||||
| 
 | ||||
| A query that is to be executed on the actual table in AWS will go though a short planning phase to determine | ||||
| whether it's possible to invoke the expression as a `Query` call. If the attributes map to partition and sort keys | ||||
| of either the main table, or exactly one GSI associated with the table, the expression will be executed as a Query | ||||
| over the table or the GSI found with those attributes. | ||||
| 
 | ||||
| In cases where multiple GSI candidates exist on the base table, the query will fail with the following error: | ||||
| 
 | ||||
| ``` | ||||
| multiple plans with index found. Specify index or scan with 'using' clause | ||||
| ``` | ||||
| 
 | ||||
| In these cases, the index will need to be specified with the `using` keyword with the `index` option: | ||||
| 
 | ||||
| ``` | ||||
| address="something" using index("specific-gsi-name") | ||||
| ``` | ||||
| 
 | ||||
| The `using` keyword can also be used to force the expression to run as a table scan, | ||||
| even if the query can be invoked using a Query call over the base table or GSI: | ||||
| 
 | ||||
| ``` | ||||
| address="something" using scan | ||||
| ``` | ||||
| 
 | ||||
| ## Builtin Functions | ||||
| 
 | ||||
| Query expressions support a number of builtin functions. | ||||
| 
 | ||||
| ### The `marked` function | ||||
| 
 | ||||
| ``` | ||||
| marked(fieldname) | ||||
| ``` | ||||
| 
 | ||||
| The `marked` function will return a list of field values of all marked rows of the current result set. The | ||||
| items will appear in the list as they appear in the result set. The _fieldname_ currently only supports top-level fields. | ||||
| If no fields are marked, the empty list is returned. | ||||
| 
 | ||||
| ``` | ||||
| marked("city") | ||||
| ``` | ||||
| 
 | ||||
| ### The `range` function | ||||
| 
 | ||||
| ``` | ||||
| range(from, to) | ||||
| ``` | ||||
| 
 | ||||
| The `range` function will return a list of integers between _from_ and _to_ inclusive.  Non integers will be truncated | ||||
| to integers, and the step is always be 1. | ||||
| 
 | ||||
| ``` | ||||
| range(2, 5)            // [2, 3, 4, 5] | ||||
| three in range(2, 5)   // true | ||||
| ``` | ||||
| 
 | ||||
| ### The `size` function | ||||
| 
 | ||||
| ``` | ||||
| size(v) | ||||
| ``` | ||||
| 
 | ||||
| The `size` function will return the number of items of a list or map, or the length of a string. | ||||
| 
 | ||||
| {{< hint info >}} | ||||
| The `size` function is equivalent to the `size` AWS conditional expressions function, and as such is the | ||||
| only function that is included as is in the generated Query or Scan expression.  All other functions are evaluated | ||||
| prior to making the Query or Scan AWS call. | ||||
| {{</hint>}} | ||||
| 
 | ||||
| 
 | ||||
| ## Placeholders | ||||
| 
 | ||||
| In some circumstances, such as the [session.query](/docs/reference/script-api/#session-query) method, it's possible to use a placeholder as a field or value. To expand a placeholder to an identifier, use the `:` prefix. To expanded the placeholder as a value, use the `$` prefix. For example, the expression `:key = $value` in the following script: | ||||
| 
 | ||||
| ``` | ||||
| out := session.query(":key = $value", { | ||||
|   table: "some-table", | ||||
|   args: { | ||||
|     key: "pk", | ||||
|     value: "value" | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| Is equivalent to the query `pk = "hello"`, as the placeholder `:key` is expanded to an identifier and `$value` is expanded | ||||
| to a value, in this case a string. | ||||
|  | @ -1,5 +0,0 @@ | |||
| --- | ||||
| title: "Script API" | ||||
| type: script-api | ||||
| --- | ||||
| # Script API | ||||
|  | @ -1,21 +0,0 @@ | |||
| # Settings | ||||
| 
 | ||||
| ## default-limit | ||||
| 
 | ||||
| - Type: int | ||||
| - Default: `1000` | ||||
| 
 | ||||
| The maximum number of rows returned from a query or scan. | ||||
| 
 | ||||
| ## ro | ||||
| 
 | ||||
| - Type: flag | ||||
| 
 | ||||
| Enable read-only mode.  When enabled, all modification operations are disabled, and will fail with a `Read-only mode` error. | ||||
| The `rw` setting will disable read-only mode. | ||||
| 
 | ||||
| ## rw | ||||
| 
 | ||||
| - Type: flag | ||||
| 
 | ||||
| Disable read-only mode.  The `ro` setting will enable read-only mode. | ||||
|  | @ -1,103 +0,0 @@ | |||
| --- | ||||
| title: "Scripting" | ||||
| --- | ||||
| # Scripting | ||||
| 
 | ||||
| Scripts can be used to automate certain tasks with Dynamo-Browse. They can also be used to define | ||||
| new commands or key bindings. | ||||
| 
 | ||||
| ## Scripting Basics | ||||
| 
 | ||||
| Dynamo-Browse scripts are written using the [Tamarin](https://cloudcmds.github.io/tamarin/) scripting language, | ||||
| which looks a lot like [Go](https://go.dev).  All features of the language are available in Dynamo-Browse. | ||||
| 
 | ||||
| The typical "hello world" script for Dynamo-Browse is below: | ||||
| 
 | ||||
| ``` | ||||
| ui.print("Hello, world") | ||||
| ``` | ||||
| 
 | ||||
| This uses the [ui](/docs/reference/script-api/#module-ui) package, which is the package used to interact with | ||||
| the Dynamo-Browse user interface. | ||||
| A full list of supported packages can be found in the [Script API](/docs/reference/script-api/) reference, along | ||||
| with the builtins and packages supported by Tamarin itself. | ||||
| 
 | ||||
| {{<hint info>}} | ||||
|   **Note:** the [ext](/docs/reference/script-api/#module-ext) package is only available to Extension Scripts. | ||||
| {{</hint>}} | ||||
| 
 | ||||
| To execute this script, use the `run-script` command: | ||||
| 
 | ||||
| ``` | ||||
| run-script /path/to/script/hello.tm | ||||
| ``` | ||||
| 
 | ||||
| You'll see that the message "Hello, world" will appear in the status bar of Dynamo-Browse. | ||||
| 
 | ||||
| <!-- TODO: Screenshot --> | ||||
| 
 | ||||
| Any `print` or `printf` messages will be written to the debug log with the prefix `script <filename>`.  The | ||||
| debug log is turned off by default, but it can be enabled using the [-debug](/docs/reference/launch-flags/#-debug) flag on launch. | ||||
| 
 | ||||
| Scripts loaded using the `run-script` command are for ad-hoc automation tasks that are not necessarily designed for | ||||
| repeated use. These ad-hoc scripts are executed, then immediately unloaded, and are not generally allowed to extend | ||||
| Dynamo-Browse.  In order to do so, you will need to write an Extension Script. | ||||
| 
 | ||||
| ## Extension Scripts | ||||
| 
 | ||||
| Extension scripts are scripts designed to extend Dynamo-Browse in some way, such as with new commands or key bindings. | ||||
| They are traditionally loaded on startup and exist in the predefined "script" directory. They are usually designed for | ||||
| repeated operations, including those that can be bound to command name or keys. | ||||
| 
 | ||||
| The following is an example script which will define a "goto" command.  When invoked, the script will prompt the | ||||
| user for the value of the partition key.  It will then perform a query over the currently viewed table for any rows with | ||||
| that partition key.  If no error occurred, the results of the query will be shown to the user. | ||||
| 
 | ||||
| ``` | ||||
| // Define a new "goto" command, which can be invoked when the user presses ':' and types in 'goto' | ||||
| ext.command("goto", func() { | ||||
|     // Use the information of the current table to get the name of the partition key. | ||||
|     pkName := session.current_table().keys["partition"] | ||||
| 
 | ||||
|     // Prompt the user for the value to go to.  The user can press Esc, which will cancel | ||||
|     // the input and return 'nil'. | ||||
|     keyVal := ui.prompt(pkName + "? ") | ||||
|     if keyVal == nil { | ||||
|         return nil | ||||
|     } | ||||
| 
 | ||||
|     // Run a query over the DynamoDB table for any rows with the partition key.  Notice | ||||
|     // the use of the 'args' option, and the presence of both the name prefix (':key') | ||||
|     // and value prefix ('$val'). | ||||
|     res := session.query(":key = $val", { | ||||
|         args: { | ||||
|             key: pkName, | ||||
|             val: keyVal, | ||||
|         }, | ||||
|     }) | ||||
|      | ||||
|     // The query method will return either an error or a result.  If it's an error, print | ||||
|     // a notice and exist. | ||||
|     if res.is_err() { | ||||
|         ui.print("Can't goto: " + res.err_msg()) | ||||
|         return nil | ||||
|     } | ||||
| 
 | ||||
|     // If no error, unwrap the result object to get the result-set returned from the query. | ||||
|     // Then change the current result-set to this one.  This will change the result-set the | ||||
|     // user is currently seeing. | ||||
|     session.set_result_set(res.unwrap()) | ||||
| }) | ||||
| ``` | ||||
| 
 | ||||
| To load an extension script, use the `load-script` command: | ||||
| 
 | ||||
| ``` | ||||
| load-script script.tm | ||||
| ``` | ||||
| 
 | ||||
| The script must exist in the "script" directory, which by default is: | ||||
| 
 | ||||
| ``` | ||||
| $HOME/.config/audax/dynamo-browse/scripts | ||||
| ``` | ||||
|  | @ -1,41 +0,0 @@ | |||
| +++ | ||||
| layout = "single" | ||||
| +++ | ||||
| 
 | ||||
| # Download | ||||
| 
 | ||||
| Binary packages can be [download from GitHub](https://github.com/lmika/audax/releases/latest). | ||||
| 
 | ||||
| ## MacOS Using Homebrew | ||||
| 
 | ||||
| If you have Homebrew, you can install using the following command: | ||||
| 
 | ||||
| ``` | ||||
| brew tap lmika/audax | ||||
| brew install audax | ||||
| ``` | ||||
| 
 | ||||
| ## Linux | ||||
| 
 | ||||
| To install the Debian package, download the `.deb` file, and install the package by running: | ||||
| 
 | ||||
| ``` | ||||
| sudo apt install ./audax_0.4.0_linux_amd64.deb | ||||
| ``` | ||||
| 
 | ||||
| To install the RPM package, download the `.rpm` file, and install the package by running: | ||||
| 
 | ||||
| ``` | ||||
| sudo yum install ./audax_0.4.0_linux_amd64.rpm | ||||
| ``` | ||||
| 
 | ||||
| ## Install Using Go | ||||
| 
 | ||||
| If you have Go 1.22, you can install using the following command: | ||||
| 
 | ||||
| ``` | ||||
| go install github.com/lmika/audax/cmd/dynamo-browse@v0.4.0 | ||||
| ``` | ||||
| 
 | ||||
| 
 | ||||
| The source code can be [found on GitHub](https://github.com/lmika/audax). | ||||
|  | @ -1,25 +0,0 @@ | |||
| +++ | ||||
| headless = true | ||||
| +++ | ||||
| - [Download]({{< relref "/download" >}}) | ||||
| - [Releases](https://github.com/lmika/audax/releases) | ||||
| - [Github](https://github.com/lmika/audax) | ||||
| <br> | ||||
| 
 | ||||
| [**User Guide**]({{< relref "/docs" >}}) | ||||
| - [Launching]({{< relref "/docs/launching" >}}) | ||||
| - [Getting Around]({{< relref "/docs/getting-around" >}}) | ||||
| - [Filtering And Querying]({{< relref "/docs/filtering-querying" >}}) | ||||
| - [Editing Items]({{< relref "/docs/editing-items" >}}) | ||||
| - [Customising]({{< relref "/docs/customising" >}}) | ||||
| - [Scripting]({{< relref "/docs/scripting" >}}) | ||||
| <br> | ||||
| 
 | ||||
| **References** | ||||
| - [Key Bindings]({{< relref "/docs/reference/key-bindings" >}}) | ||||
| - [Commands]({{< relref "/docs/reference/commands" >}}) | ||||
| - [Query Expressions]({{< relref "/docs/reference/query-expressions" >}}) | ||||
| - [Launch Flags]({{< relref "/docs/reference/launch-flags" >}}) | ||||
| - [Settings]({{< relref "/docs/reference/settings" >}}) | ||||
| - [Script API]({{< relref "/docs/reference/script-api" >}}) | ||||
| <br> | ||||
|  | @ -1,75 +0,0 @@ | |||
| module: ext | ||||
| docs: | | ||||
|   Provides access to the extension points scripts can used to extend the functionality of Dynamo-Browse. | ||||
|    | ||||
|   This module is only available for scripts loaded using the [load-script]() command. | ||||
| symbols: | ||||
|   - name: command | ||||
|     syntax: ext.command(name, fn) | ||||
|     docs: | | ||||
|       Defines a new command, which can be invoked by entering _name_ within the main view mode. | ||||
|       The parameter _fn_ must be a function, which will be executed when the _name_ command is entered | ||||
|       while in view mode. | ||||
|        | ||||
|       The command can accept arguments, which will be passed in to the parameters of _fn_. The number | ||||
|       of command arguments must match the number of parameters, except for any function arguments with | ||||
|       a default value. | ||||
|     example: | | ||||
|       ext.command("add", func(x, y) { | ||||
|         sum := x + y | ||||
|         ui.print("x + y = ", sum) | ||||
|       }) | ||||
|   - name: related_items | ||||
|     syntax: ext.related_items(table, fn) | ||||
|     docs: | | ||||
|       Defines a "related item" for a table. These act as quick jumps between tables. | ||||
|       When the user presses Shift+O, all the related item functions that match the given | ||||
|       table will be evaluated. Each one is to return zero or more related queries, which are presented | ||||
|       to the user as a list. When the user selects one, the query will be evaluated and the result set will | ||||
|       be shown. | ||||
|        | ||||
|       The _table_ parameter is the name of the table of the related items managed by this function. | ||||
|       If the last character of the table is `*`, then _table_ will be treated as a name prefix. | ||||
|        | ||||
|       The _fn_ will produce a list of queries that are related to a given item. The function takes the currently | ||||
|       selected item as the argument, and is expected to produce a list of maps, with each map having the following | ||||
|       fields: | ||||
|          | ||||
|       - `label`: The label to use for the picker option | ||||
|       - `query`: The query expression that will run when the option is chosen | ||||
|       - `table`: The table to run the query over. If not set, the current table will be used | ||||
|       - `args`: A map of query placeholder values | ||||
|       - `on_select`: An optional function that will run in place of a predefined query. If set, the `query` field will | ||||
|         be ignored. | ||||
|     example: | | ||||
|       ext.related_items("user-account", func(item) { | ||||
|         return [ | ||||
|           { | ||||
|             "label": "Customer", | ||||
|             "table": "billing", | ||||
|             "query": "email=$email", | ||||
|             "args": {"email": item.attr("email")}, | ||||
|           }, | ||||
|         ] | ||||
|       })	     | ||||
|   - name: key_binding | ||||
|     syntax: ext.key_binding(name, options, fn) | ||||
|     docs: | | ||||
|       Defines a new key binding, which can be invoked while viewing the table. | ||||
|        | ||||
|       The _name_ parameter defines the binding name. The binding names will be prefixed with | ||||
|       `ext.<script_basename>`. This name can be used with the [rebind]() command. | ||||
|        | ||||
|       The _option_ parameter defines a map of options. The only valid option is | ||||
|       `default`, which is the default key to use for this binding. If unset, the binding will | ||||
|       have no key binding and can only be bound using the [rebind]() command. | ||||
|        | ||||
|       The _fn_ parameter is the function that will be invoked when the key is pressed. | ||||
|       It must accept no parameters. | ||||
|     example: | | ||||
|       // Script name: sayhello.tm | ||||
|       // | ||||
|       // This binding can be rebound with the command "rebind ext.sayhello.hello <key>" | ||||
|       ext.key_binding("hello", {"default": "H"}, func() { | ||||
|         ui.print("Hello") | ||||
|       }) | ||||
|  | @ -1,44 +0,0 @@ | |||
| module: item | ||||
| type: type | ||||
| docs: | | ||||
|   A single record from a DynamoDB table. | ||||
|    | ||||
|   Item values are converted to tamarin types using the following: | ||||
|      | ||||
|   | Attribute Type | Tamarin Type | | ||||
|   |:---------------|:-------------| | ||||
|   | S              | string       | | ||||
|   | N              | int, float \[1\] | | ||||
|   | BOOL           | bool         | | ||||
|   | NULL           | nil          | | ||||
|   | L              | list         | | ||||
|   | M              | map          | | ||||
|   | SS             | set, with string values | | ||||
|   | NS             | set, with number values | | ||||
|    | ||||
|   Notes: | ||||
|          | ||||
|   - \[1\]: int will be used if the value can be parsed as an integer, otherwise it will be returned as a float. | ||||
|   - Byte array (B or BS) values are currently not supported. | ||||
|      | ||||
| symbols: | ||||
|   - name: resultset | ||||
|     syntax: item.resultset | ||||
|     docs: | | ||||
|       Returns the result-set this item is a member of. | ||||
|   - name: index | ||||
|     syntax: item.index | ||||
|     docs: | | ||||
|       Returns the index of this item within the result set. | ||||
|   - name: attr | ||||
|     syntax: item.attr(expression) | ||||
|     docs: | | ||||
|       Returns the attribute value from the query expression.       | ||||
|   - name: set_attr | ||||
|     syntax: item.set_attr(expression, value) | ||||
|     docs: | | ||||
|       Sets the value of the attribute. | ||||
|   - name: delete_attr | ||||
|     syntax: item.delete_attr(expression) | ||||
|     docs: | | ||||
|       Delete the attribute. | ||||
|  | @ -1,20 +0,0 @@ | |||
| module: resultset | ||||
| type: type | ||||
| docs: | | ||||
|   Holds a collection of items returned from a query, or presented to a user. | ||||
|    | ||||
|   A specific item of a result-set can be retrived using the subscript option.  For example, `result[21]` will | ||||
|   return the 21st item of the result-set from the first item. A negative index can be used to retrieve an | ||||
|   item from the last item. | ||||
|    | ||||
|   There is no guarantee to the ordering of items within the result-set, although items are usually | ||||
|   ordered based on the partition and sort key. | ||||
| symbols: | ||||
|   - name: length | ||||
|     syntax: resultset.length | ||||
|     docs: | | ||||
|       Returns the number of items within the result set. | ||||
|   - name: table | ||||
|     syntax: resultset.table | ||||
|     docs: | | ||||
|       Returns information about the table this result set belongs to. | ||||
|  | @ -1,52 +0,0 @@ | |||
| module: session | ||||
| docs: | | ||||
|   Provides access to the currently viewed table and result-set. | ||||
| symbols: | ||||
|   - name: query | ||||
|     syntax: session.query(expression, [options]) | ||||
|     docs: | | ||||
|       Executes a query against a DynamoDB table.  This returns a resultset if the query was successful. | ||||
|       A query with no results will be an empty result-set. | ||||
|        | ||||
|       The _expression_ is the query expression to execute. This is similar to the type of expressions entered | ||||
|       after pression <kbd>?</kbd>. | ||||
|        | ||||
|       The _options_ map can contain the following key/value pairs: | ||||
|          | ||||
|       - `table`: the DynamoDB table to execute the query against. Default is the currently displayed table. | ||||
|       - `args`: A map containing names and values that can be used as placeholders in the query expression. | ||||
|     example: | | ||||
|       out := session.query("pk = $key", { | ||||
|         table: "some-table", | ||||
|         args: { | ||||
|           key: "my partition key" | ||||
|         } | ||||
|       } | ||||
|       session.set_result_set(out.unwrap()) | ||||
|   - name: current_table | ||||
|     syntax: session.current_table() | ||||
|     docs: | | ||||
|       Returns information about the currently displayed table. This will be returned as a `table` object.  If no | ||||
|       table is displayed, this function will return `nil`. | ||||
|   - name: resultset | ||||
|     syntax: session.resultset | ||||
|     docs: | | ||||
|       Returns the currently displayed result set. This is the set of items that are shown to the user in the items | ||||
|       table. This will be returned as a `resultset` object. | ||||
|        | ||||
|       Note that this only contains the items of the current result set that exists in memory.  As such, it will be | ||||
|       capped to the configured query limit.  | ||||
|   - name: selected_item | ||||
|     syntax: session.selected_item() | ||||
|     docs: | | ||||
|       Returns the item currently highlighted in the items table. This will be returned as an `item` object.  If no | ||||
|       item is highlighted, it will return `nil`. | ||||
|   - name: set_result_set | ||||
|     syntax: session.set_result_set(new_result_set) | ||||
|     docs: | | ||||
|       Replaces the currently displayed result-set with a new one. This can be used alongside the `query` function | ||||
|       to display the results of a query. | ||||
|        | ||||
|       Changing the displayed result-set will trigger a redraw of the viewport and will push a new history record to | ||||
|       the backstack. Therefore, it's not recommended to call this method too often during a script execution session. | ||||
|       At most once with the final result-set you'd like to show the user is considered best practice. | ||||
|  | @ -1,20 +0,0 @@ | |||
| module: table | ||||
| type: type | ||||
| docs: | | ||||
|   Provides information about a DynamoDB table. | ||||
| symbols: | ||||
|   - name: name | ||||
|     syntax: table.name | ||||
|     docs: | | ||||
|       Returns the name of the table. | ||||
|   - name: keys | ||||
|     syntax: table.keys | ||||
|     docs: | | ||||
|       Returns the keys of the table. This will be returned as a map with the following names: | ||||
|          | ||||
|         - `hash`: the attribute name of the partition (hash) key | ||||
|         - `range`: the attribute name of the sort (range) key, or `nil` if one is not defined. | ||||
|   - name: gsis | ||||
|     syntax: table.gsis | ||||
|     docs: | | ||||
|       Returns a list of the GSIs used by this table.  The elements of the list will have the type table_index | ||||
|  | @ -1,16 +0,0 @@ | |||
| module: table_index | ||||
| type: type | ||||
| docs: | | ||||
|   Provides information about an DynamoDB index. | ||||
| symbols: | ||||
|   - name: name | ||||
|     syntax: table_index.name | ||||
|     docs: | | ||||
|       Returns the name of the index. | ||||
|   - name: keys | ||||
|     syntax: table_index.keys | ||||
|     docs: | | ||||
|       Returns the keys of the index. This will be returned as a map with the following names: | ||||
|          | ||||
|         - `hash`: the attribute name of the partition (hash) key | ||||
|         - `range`: the attribute name of the sort (range) key, or `nil` if one is not defined. | ||||
|  | @ -1,18 +0,0 @@ | |||
| module: ui | ||||
| docs: | | ||||
|   Provides control over the user interface. | ||||
| symbols: | ||||
|   - name: print | ||||
|     syntax: ui.print(args...) | ||||
|     docs: | | ||||
|       Displays a message in the status bar. | ||||
|   - name: prompt | ||||
|     syntax: ui.prompt(message) | ||||
|     docs: | | ||||
|       Request a line of input from the user, using _message_ as the prompt. | ||||
|        | ||||
|       This function will return the user's input as a string, or `nil` if the user cancels | ||||
|       the prompt by pressing <kbd>Esc</kbd> | ||||
|     example: | | ||||
|       line := ui.prompt("What is your name? ") | ||||
|       ui.print("Hello, ", line) | ||||
| Before Width: | Height: | Size: 162 KiB | 
| Before Width: | Height: | Size: 188 KiB | 
| Before Width: | Height: | Size: 198 KiB | 
| Before Width: | Height: | Size: 193 KiB | 
| Before Width: | Height: | Size: 150 KiB | 
| Before Width: | Height: | Size: 89 KiB | 
|  | @ -1,3 +0,0 @@ | |||
| {{- $options := dict "targetPath" "js/bundle.js" -}} | ||||
| {{- $jsBundle := resources.Get "js/index.js" | js.Build $options | resources.Minify | fingerprint -}} | ||||
| <script src="{{ $jsBundle.RelPermalink | absURL }}" defer></script> | ||||
|  | @ -1 +0,0 @@ | |||
| <script src="https://tinylytics.app/embed/vgYK9BZh7G14oSKuW2wR.js" defer></script> | ||||
|  | @ -1,62 +0,0 @@ | |||
| {{ define "main" }} | ||||
| <article class="markdown"> | ||||
|   {{ partial "docs/post-meta" . }} | ||||
|   {{- .Content -}} | ||||
|    | ||||
|   <!-- API Data --> | ||||
|   {{ range sort $.Site.Data.scriptmods }} | ||||
|   <section> | ||||
| 	  {{ if eq .type "type" }} | ||||
| 	    <h2 id="type-{{.module}}"> | ||||
| 			Type: {{ .module }} | ||||
| 			<a class="anchor" href="#type-{{.module}}">#</a> | ||||
| 		</h2> | ||||
| 	  {{ else }} | ||||
| 	  	<h2 id="module-{{.module}}"> | ||||
| 		  	Module: {{ .module }} | ||||
| 		  	<a class="anchor" href="#module-{{.module}}">#</a> | ||||
| 	  	</h2> | ||||
| 	  {{ end }} | ||||
| 	   | ||||
| 	  {{ $.RenderString .docs }} | ||||
| 	   | ||||
| 	  {{ $moduleName := .module }} | ||||
| 	  {{ range sort .symbols "name" }}	  	 | ||||
| 	  	<h3 id="{{$moduleName}}-{{.name}}"> | ||||
| 			  {{$moduleName}}.{{ .name }} | ||||
| 			  <a class="anchor" href="#{{$moduleName}}-{{.name}}">#</a> | ||||
| 	  	</h3> | ||||
| 		<pre><code>{{ .syntax }}</code></pre> | ||||
| 		{{ $.RenderString .docs }} | ||||
| 		 | ||||
| 		{{if .example}} | ||||
| 			<h4>Example</h4> | ||||
| 			<pre><code>{{ .example }}</code></pre> | ||||
| 		{{end}} | ||||
| 	  {{ end }} | ||||
|   </section> | ||||
|   {{end}} | ||||
| </article> | ||||
| {{ end }} | ||||
| 
 | ||||
| {{ define "toc" }} | ||||
| <nav id="TableOfContents"> | ||||
| 	<ul> | ||||
| 	{{ range sort $.Site.Data.scriptmods }} | ||||
| 	  {{ if eq .type "type" }} | ||||
| 	  	<li><a href="#type-{{.module}}">{{ .module }}</a></li> | ||||
| 	  {{ else }} | ||||
| 	  	<li><a href="#module-{{.module}}">{{ .module }}</a></li> | ||||
| 	  {{ end }} | ||||
| 	  {{/* | ||||
| 	  	<ul> | ||||
| 	    	{{ $moduleName := .module }} | ||||
| 			{{ range sort .symbols "name" }}	  	 | ||||
| 				<li><a href="#{{$moduleName}}-{{.name}}">{{ .name }}</a></li> | ||||
| 			{{ end }}		   | ||||
| 	  	</ul> | ||||
| 	  */}} | ||||
| 	{{ end }} | ||||
| 	</ul> | ||||
| </nav> | ||||
| {{ end }} | ||||
							
								
								
									
										28
									
								
								_site/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						|  | @ -1,28 +0,0 @@ | |||
| { | ||||
|   "name": "awstools-web", | ||||
|   "version": "1.0.0", | ||||
|   "lockfileVersion": 2, | ||||
|   "requires": true, | ||||
|   "packages": { | ||||
|     "": { | ||||
|       "name": "awstools-web", | ||||
|       "version": "1.0.0", | ||||
|       "license": "ISC", | ||||
|       "dependencies": { | ||||
|         "@hotwired/stimulus": "^3.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@hotwired/stimulus": { | ||||
|       "version": "3.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.1.0.tgz", | ||||
|       "integrity": "sha512-iDMHUhiEJ1xFeicyHcZQQgBzhtk5mPR0QZO3L6wtqzMsJEk2TKECuCQTGKjm+KJTHVY0dKq1dOOAWvODjpd2Mg==" | ||||
|     } | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@hotwired/stimulus": { | ||||
|       "version": "3.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.1.0.tgz", | ||||
|       "integrity": "sha512-iDMHUhiEJ1xFeicyHcZQQgBzhtk5mPR0QZO3L6wtqzMsJEk2TKECuCQTGKjm+KJTHVY0dKq1dOOAWvODjpd2Mg==" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -1,25 +0,0 @@ | |||
| { | ||||
|   "name": "awstools-web", | ||||
|   "version": "1.0.0", | ||||
|   "description": "Website for audax", | ||||
|   "main": "index.js", | ||||
|   "directories": { | ||||
|     "doc": "docs" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "test": "echo \"Error: no test specified\" && exit 1" | ||||
|   }, | ||||
|   "repository": { | ||||
|     "type": "git", | ||||
|     "url": "git+https://github.com/lmika/audax-web.git" | ||||
|   }, | ||||
|   "author": "", | ||||
|   "license": "ISC", | ||||
|   "bugs": { | ||||
|     "url": "https://github.com/lmika/audax-web/issues" | ||||
|   }, | ||||
|   "homepage": "https://github.com/lmika/audax-web#readme", | ||||
|   "dependencies": { | ||||
|     "@hotwired/stimulus": "^3.1.0" | ||||
|   } | ||||
| } | ||||
| Before Width: | Height: | Size: 30 KiB | 
| Before Width: | Height: | Size: 30 KiB | 
| Before Width: | Height: | Size: 132 KiB | 
| Before Width: | Height: | Size: 151 KiB | 
| Before Width: | Height: | Size: 160 KiB | 
| Before Width: | Height: | Size: 157 KiB | 
| Before Width: | Height: | Size: 116 KiB | 
| Before Width: | Height: | Size: 58 KiB | 
|  | @ -4,37 +4,37 @@ import ( | |||
| 	"context" | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl/cmdpacks" | ||||
| 	"log" | ||||
| 	"net" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl/cmdpacks" | ||||
| 
 | ||||
| 	"github.com/aws/aws-sdk-go-v2/config" | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb" | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/logging" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/osstyle" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/workspaces" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/dynamo" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/inputhistorystore" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/settingstore" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/workspacestore" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs" | ||||
| 	keybindings_service "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/keybindings" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/viewsnapshot" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles" | ||||
| 	bus "github.com/lmika/events" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/logging" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/osstyle" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/workspaces" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/controllers" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/queryexpr" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/providers/dynamo" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/providers/inputhistorystore" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/providers/settingstore" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/providers/workspacestore" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/inputhistory" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/itemrenderer" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/jobs" | ||||
| 	keybindings_service "lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/keybindings" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/viewsnapshot" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/keybindings" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles" | ||||
| 	"github.com/lmika/gopkgs/cli" | ||||
| ) | ||||
| 
 | ||||
| func main() { | ||||
|  | @ -52,8 +52,7 @@ func main() { | |||
| 
 | ||||
| 	cfg, err := config.LoadDefaultConfig(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, "cannot load AWS config: %v", err) | ||||
| 		os.Exit(1) | ||||
| 		cli.Fatalf("cannot load AWS config: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	closeFn := logging.EnableLogging(*flagDebug) | ||||
|  | @ -62,8 +61,7 @@ func main() { | |||
| 	wsManager := workspaces.New(workspaces.MetaInfo{Command: "dynamo-browse"}) | ||||
| 	ws, err := wsManager.OpenOrCreate(*flagWorkspace) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, "cannot create workspace: %v", ws) | ||||
| 		os.Exit(1) | ||||
| 		cli.Fatalf("cannot create workspace: %v", ws) | ||||
| 	} | ||||
| 	defer ws.Close() | ||||
| 
 | ||||
|  | @ -71,8 +69,7 @@ func main() { | |||
| 	if *flagLocal != "" { | ||||
| 		host, port, err := net.SplitHostPort(*flagLocal) | ||||
| 		if err != nil { | ||||
| 			fmt.Fprintf(os.Stderr, "invalid address '%v': %v", *flagLocal, err) | ||||
| 			os.Exit(1) | ||||
| 			cli.Fatalf("invalid address '%v': %v", *flagLocal, err) | ||||
| 		} | ||||
| 		if host == "" { | ||||
| 			host = "localhost" | ||||
|  | @ -97,14 +94,12 @@ func main() { | |||
| 
 | ||||
| 	if *flagRO { | ||||
| 		if err := settingStore.SetReadOnly(*flagRO); err != nil { | ||||
| 			fmt.Fprintf(os.Stderr, "unable to set read-only mode: %v", err) | ||||
| 			os.Exit(1) | ||||
| 			cli.Fatalf("unable to set read-only mode: %v", err) | ||||
| 		} | ||||
| 	} | ||||
| 	if *flagDefaultLimit > 0 { | ||||
| 		if err := settingStore.SetDefaultLimit(*flagDefaultLimit); err != nil { | ||||
| 			fmt.Fprintf(os.Stderr, "unable to set default limit: %v", err) | ||||
| 			os.Exit(1) | ||||
| 			cli.Fatalf("unable to set default limit: %v", err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  | @ -135,32 +130,27 @@ func main() { | |||
| 
 | ||||
| 	if *flagQuery != "" { | ||||
| 		if *flagTable == "" { | ||||
| 			fmt.Fprintf(os.Stderr, "-t will need to be set for -q") | ||||
| 			os.Exit(1) | ||||
| 			cli.Fatalf("-t will need to be set for -q") | ||||
| 		} | ||||
| 
 | ||||
| 		ctx := context.Background() | ||||
| 
 | ||||
| 		query, err := queryexpr.Parse(*flagQuery) | ||||
| 		if err != nil { | ||||
| 			fmt.Fprintf(os.Stderr, "query: %v", err) | ||||
| 			os.Exit(1) | ||||
| 			cli.Fatalf("query: %v", err) | ||||
| 		} | ||||
| 
 | ||||
| 		ti, err := tableService.Describe(ctx, *flagTable) | ||||
| 		if err != nil { | ||||
| 			fmt.Fprintf(os.Stderr, "cannot describe table: %v", err) | ||||
| 			os.Exit(1) | ||||
| 			cli.Fatalf("cannot describe table: %v", err) | ||||
| 		} | ||||
| 
 | ||||
| 		rs, err := tableService.ScanOrQuery(ctx, ti, query, nil) | ||||
| 		if err != nil { | ||||
| 			fmt.Fprintf(os.Stderr, "cannot execute query: %v", err) | ||||
| 			os.Exit(1) | ||||
| 			cli.Fatalf("cannot execute query: %v", err) | ||||
| 		} | ||||
| 		if err := exportController.ExportToWriter(os.Stdout, rs); err != nil { | ||||
| 			fmt.Fprintf(os.Stderr, "cannot export results of query: %v", err) | ||||
| 			os.Exit(1) | ||||
| 			cli.Fatalf("cannot export results of query: %v", err) | ||||
| 		} | ||||
| 		return | ||||
| 	} | ||||
|  | @ -181,8 +171,7 @@ func main() { | |||
| 
 | ||||
| 	commandController, err := commandctrl.NewCommandController(inputHistoryService, stdCommands) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, "cannot setup command controller: %v", err) | ||||
| 		os.Exit(1) | ||||
| 		cli.Fatalf("cannot setup command controller: %v", err) | ||||
| 	} | ||||
| 	commandController.SetCommandCompletionProvider(columnsController) | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										12
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						|  | @ -1,8 +1,8 @@ | |||
| module lmika.dev/cmd/dynamo-browse | ||||
| module github.com/lmika/dynamo-browse | ||||
| 
 | ||||
| go 1.25 | ||||
| go 1.24 | ||||
| 
 | ||||
| toolchain go1.25.0 | ||||
| toolchain go1.24.0 | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/alecthomas/participle/v2 v2.1.1 | ||||
|  | @ -20,15 +20,16 @@ require ( | |||
| 	github.com/charmbracelet/lipgloss v0.6.0 | ||||
| 	github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e | ||||
| 	github.com/lmika/go-bubble-table v0.2.2-0.20220616114432-6bbb2995e538 | ||||
| 	github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f | ||||
| 	github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe | ||||
| 	github.com/mattn/go-runewidth v0.0.14 | ||||
| 	github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 | ||||
| 	github.com/muesli/reflow v0.3.0 | ||||
| 	github.com/pkg/errors v0.9.1 | ||||
| 	github.com/stretchr/testify v1.10.0 | ||||
| 	github.com/stretchr/testify v1.9.0 | ||||
| 	golang.design/x/clipboard v0.6.2 | ||||
| 	golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a | ||||
| 	ucl.lmika.dev v0.1.2 | ||||
| 	ucl.lmika.dev v0.0.0-20250525023717-3076897eb73e | ||||
| ) | ||||
| 
 | ||||
| require ( | ||||
|  | @ -76,5 +77,4 @@ require ( | |||
| 	golang.org/x/text v0.9.0 // indirect | ||||
| 	google.golang.org/appengine v1.6.7 // indirect | ||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||
| 	lmika.dev/pkg/modash v0.1.0 // indirect | ||||
| ) | ||||
|  |  | |||
							
								
								
									
										14
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						|  | @ -105,6 +105,8 @@ github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e h1:0QkUe2ejnT/i+xbgGy | |||
| github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e/go.mod h1:qtkBmNC9OfD0STtOR9sF55pQchjIfNlC3gzm4n8CrqM= | ||||
| github.com/lmika/go-bubble-table v0.2.2-0.20220616114432-6bbb2995e538 h1:dtMPRNoDqDnnP3HgOvYhswcJVSqdISkYlCtGOjTqg6Q= | ||||
| github.com/lmika/go-bubble-table v0.2.2-0.20220616114432-6bbb2995e538/go.mod h1:0RT1upgKZ6qZ6B1SqseE3wWsPjSQRv/G/HjpYK8jNsg= | ||||
| github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f h1:tz68Lhc1oR15HVz69IGbtdukdH0x70kBDEvvj5pTXyE= | ||||
| github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f/go.mod h1:zHQvhjGXRro/Xp2C9dbC+ZUpE0gL4GYW75x1lk7hwzI= | ||||
| github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe h1:1UXS/6OFkbi6JrihPykmYO1VtsABB02QQ+YmYYzTY18= | ||||
| github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe/go.mod h1:qpdOkLougV5Yry4Px9f1w1pNMavcr6Z67VW5Ro+vW5I= | ||||
| github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= | ||||
|  | @ -152,8 +154,8 @@ github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncj | |||
| github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= | ||||
| github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= | ||||
| github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||||
| github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= | ||||
| github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||||
| github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= | ||||
| github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= | ||||
| github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= | ||||
|  | @ -245,9 +247,5 @@ gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= | |||
| gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| lmika.dev/pkg/modash v0.1.0 h1:fltroSvP0nKj9K0E6G+S9LULvB9Qhj47+SZ2b9v/v/c= | ||||
| lmika.dev/pkg/modash v0.1.0/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI= | ||||
| ucl.lmika.dev v0.1.1 h1:P8nEqJPKS+wmXZiSjEmJkOUeWQF9YxWSymDkLXt9mvg= | ||||
| ucl.lmika.dev v0.1.1/go.mod h1:f5RzeCTyBO+4k6LYFuDkwGRujnj4/4ONM60AEtQj02k= | ||||
| ucl.lmika.dev v0.1.2 h1:dTqLKGw/pPqE7UrkrJd5qPu2i6BTDzJLaM0cRkJGn6A= | ||||
| ucl.lmika.dev v0.1.2/go.mod h1:f5RzeCTyBO+4k6LYFuDkwGRujnj4/4ONM60AEtQj02k= | ||||
| ucl.lmika.dev v0.0.0-20250525023717-3076897eb73e h1:N+HzQUunDUvdjAzbSDtHQZVZ1k+XHbVgbNwmc+EKmlQ= | ||||
| ucl.lmika.dev v0.0.0-20250525023717-3076897eb73e/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY= | ||||
|  |  | |||
|  | @ -1,52 +0,0 @@ | |||
| package cmdpacks | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" | ||||
| 	"ucl.lmika.dev/ucl" | ||||
| ) | ||||
| 
 | ||||
| type avModule struct { | ||||
| } | ||||
| 
 | ||||
| func (avModule) avTrue(ctx context.Context, args ucl.CallArgs) (_ any, err error) { | ||||
| 	return attributeValueProxy{value: &types.AttributeValueMemberBOOL{Value: true}}, nil | ||||
| } | ||||
| 
 | ||||
| func (avModule) avFalse(ctx context.Context, args ucl.CallArgs) (_ any, err error) { | ||||
| 	return attributeValueProxy{value: &types.AttributeValueMemberBOOL{Value: false}}, nil | ||||
| } | ||||
| 
 | ||||
| func (avModule) avNull(ctx context.Context, args ucl.CallArgs) (_ any, err error) { | ||||
| 	return attributeValueProxy{value: &types.AttributeValueMemberNULL{Value: true}}, nil | ||||
| } | ||||
| 
 | ||||
| func (avModule) avStringSet(ctx context.Context, args ucl.CallArgs) (_ any, err error) { | ||||
| 	var listable ucl.Listable | ||||
| 
 | ||||
| 	if err := args.Bind(&listable); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	ss := make([]string, listable.Len()) | ||||
| 	for i := 0; i < listable.Len(); i++ { | ||||
| 		item := listable.Index(i) | ||||
| 		ss[i] = item.String() | ||||
| 	} | ||||
| 
 | ||||
| 	return attributeValueProxy{value: &types.AttributeValueMemberSS{Value: ss}}, nil | ||||
| } | ||||
| 
 | ||||
| func moduleAttrValue() ucl.Module { | ||||
| 	m := avModule{} | ||||
| 
 | ||||
| 	return ucl.Module{ | ||||
| 		Name: "av", | ||||
| 		Builtins: map[string]ucl.BuiltinHandler{ | ||||
| 			"true":       m.avTrue, | ||||
| 			"false":      m.avFalse, | ||||
| 			"null":       m.avNull, | ||||
| 			"string-set": m.avStringSet, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | @ -2,8 +2,8 @@ package cmdpacks | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/controllers" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" | ||||
| 	"ucl.lmika.dev/ucl" | ||||
| ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ package cmdpacks | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services" | ||||
| 	"ucl.lmika.dev/ucl" | ||||
| ) | ||||
| 
 | ||||
|  | @ -39,8 +39,8 @@ func modulePB( | |||
| 	return ucl.Module{ | ||||
| 		Name: "pb", | ||||
| 		Builtins: map[string]ucl.BuiltinHandler{ | ||||
| 			"paste": m.pbGet, | ||||
| 			"copy":  m.pbPut, | ||||
| 			"get": m.pbGet, | ||||
| 			"put": m.pbPut, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -1,35 +0,0 @@ | |||
| package cmdpacks_test | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
| func TestModPB_Copy(t *testing.T) { | ||||
| 	t.Run("copy 1", func(t *testing.T) { | ||||
| 		svc := newService(t) | ||||
| 
 | ||||
| 		_, err := svc.CommandController.ExecuteAndWait(t.Context(), ` | ||||
| 			items = @resultset.Items | ||||
| 			skItems = $items | map { |i| $i.sk } | lists:uniq | ||||
| 			pb:copy ($skItems | strs:join "\n") | ||||
| 		`) | ||||
| 		assert.NoError(t, err) | ||||
| 
 | ||||
| 		assert.Equal(t, "111\n222\n131", svc.pasteboard.content) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("copy 2", func(t *testing.T) { | ||||
| 		svc := newService(t) | ||||
| 
 | ||||
| 		_, err := svc.CommandController.ExecuteAndWait(t.Context(), ` | ||||
| 			items = @resultset.Items | ||||
| 			skItems = $items | map { |i| $i.alpha } | filter !nil | lists:uniq | ||||
| 			pb:copy ($skItems | strs:join "\n") | ||||
| 		`) | ||||
| 		assert.NoError(t, err) | ||||
| 
 | ||||
| 		assert.Equal(t, "This is some value\nThis is another some value", svc.pasteboard.content) | ||||
| 	}) | ||||
| } | ||||
|  | @ -3,12 +3,12 @@ package cmdpacks | |||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" | ||||
| 	"github.com/pkg/errors" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/controllers" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/queryexpr" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables" | ||||
| 	"time" | ||||
| 	"ucl.lmika.dev/repl" | ||||
| 	"ucl.lmika.dev/ucl" | ||||
|  | @ -252,12 +252,8 @@ func (rs *rsModule) rsSet(ctx context.Context, args ucl.CallArgs) (_ any, err er | |||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	vs, err := mapUCLObjectToAttributeType(val) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	if err := q.SetEvalItem(item.item, vs); err != nil { | ||||
| 	// TEMP: attribute is always S
 | ||||
| 	if err := q.SetEvalItem(item.item, &types.AttributeValueMemberS{Value: val.String()}); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	item.resultSet.SetDirty(item.idx, true) | ||||
|  |  | |||
|  | @ -2,11 +2,10 @@ package cmdpacks_test | |||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl/cmdpacks" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl/cmdpacks" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestModRS_New(t *testing.T) { | ||||
|  | @ -71,7 +70,7 @@ func TestModRS_Union(t *testing.T) { | |||
| 	svc := newService(t, withDefaultLimit(2)) | ||||
| 
 | ||||
| 	rsProxy, err := svc.CommandController.ExecuteAndWait(t.Context(), ` | ||||
| 		mr = rs:union @resultset (rs:next-page @resultset) | ||||
| 		$mr = rs:union @resultset (rs:next-page @resultset) | ||||
| 		 | ||||
| 		assert (eq (len $mr.Items) 3) "expected len == 3" | ||||
| 		assert (eq $mr.Items.(0).pk "abc") "expected 0.pk" | ||||
|  | @ -153,75 +152,3 @@ func TestModRS_Query(t *testing.T) { | |||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestModRS_Filter(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		descr string | ||||
| 		cmd   string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			descr: "returns filtered items 1", | ||||
| 			cmd: ` | ||||
| 				rs = rs:scan -table service-test-data | ||||
| 				rs = rs:filter $rs 'pk="abc"' | ||||
| 				assert (len $rs) "expected len == 2" | ||||
| 				assert (eq $rs.First.pk "abc") "expected First.pk == abc" | ||||
| 			`, | ||||
| 		}, | ||||
| 		//{
 | ||||
| 		//	descr: "returns filtered items 2",
 | ||||
| 		//	cmd: `
 | ||||
| 		//		rs = rs:scan -table service-test-data
 | ||||
| 		//		rs = rs:filter $rs 'pk="bbb"'
 | ||||
| 		//		assert (len $rs) "expected len == 1"
 | ||||
| 		//		assert (eq $rs.First.pk "bbb") "expected First.pk == bbb"
 | ||||
| 		//	`,
 | ||||
| 		//},
 | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.descr, func(t *testing.T) { | ||||
| 			svc := newService(t) | ||||
| 
 | ||||
| 			_, err := svc.CommandController.ExecuteAndWait(t.Context(), tt.cmd) | ||||
| 			assert.NoError(t, err) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestModRS_First(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		descr string | ||||
| 		cmd   string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			descr: "returns the first item in sorted order", | ||||
| 			cmd: ` | ||||
| 				rs = rs:query 'pk="abc"' -table service-test-data | ||||
| 				assert (eq $rs.First.pk "abc") "expected First.pk == abc" | ||||
| 				assert (eq $rs.First.sk "111") "expected First.sk == 111" | ||||
| 			`, | ||||
| 		}, { | ||||
| 			descr: "returns the first item in single item", | ||||
| 			cmd: ` | ||||
| 				rs = rs:query 'pk="abc" and sk="222"' -table service-test-data | ||||
| 				assert (eq $rs.First.pk "abc") "expected First.pk == abc" | ||||
| 				assert (eq $rs.First.sk "222") "expected First.sk == 222" | ||||
| 				assert (eq $rs.First.beta 1231) "expected First.beta == 1231" | ||||
| 			`, | ||||
| 		}, { | ||||
| 			descr: "returns the first item in empty result", | ||||
| 			cmd: ` | ||||
| 				rs = rs:query 'pk="zzz"' -table service-test-data | ||||
| 				assert (eq $rs.First ()) "expected First to be nil" | ||||
| 			`, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.descr, func(t *testing.T) { | ||||
| 			svc := newService(t) | ||||
| 
 | ||||
| 			_, err := svc.CommandController.ExecuteAndWait(t.Context(), tt.cmd) | ||||
| 			assert.NoError(t, err) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -2,12 +2,11 @@ package cmdpacks | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 
 | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/events" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/controllers" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/events" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" | ||||
| 	"ucl.lmika.dev/ucl" | ||||
| ) | ||||
| 
 | ||||
|  | @ -39,26 +38,16 @@ func (m *uiModule) uiPrompt(ctx context.Context, args ucl.CallArgs) (any, error) | |||
| 	} | ||||
| 
 | ||||
| 	resChan := make(chan string) | ||||
| 	cancelChan := make(chan struct{}) | ||||
| 	go func() { | ||||
| 		commandctrl.PostMsg(ctx, events.PromptForInputMsg{ | ||||
| 			Prompt: prompt, | ||||
| 			OnDone: func(value string) tea.Msg { | ||||
| 				resChan <- value | ||||
| 				return nil | ||||
| 			}, | ||||
| 			OnCancel: func() tea.Msg { | ||||
| 				cancelChan <- struct{}{} | ||||
| 				return nil | ||||
| 			}, | ||||
| 		}) | ||||
| 		commandctrl.PostMsg(ctx, events.PromptForInput(prompt, nil, func(value string) tea.Msg { | ||||
| 			resChan <- value | ||||
| 			return nil | ||||
| 		})) | ||||
| 	}() | ||||
| 
 | ||||
| 	select { | ||||
| 	case value := <-resChan: | ||||
| 		return value, nil | ||||
| 	case <-cancelChan: | ||||
| 		return nil, nil | ||||
| 	case <-ctx.Done(): | ||||
| 		return nil, ctx.Err() | ||||
| 	} | ||||
|  | @ -86,38 +75,6 @@ func (m *uiModule) uiConfirm(ctx context.Context, args ucl.CallArgs) (any, error | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (m *uiModule) uiInKey(ctx context.Context, args ucl.CallArgs) (any, error) { | ||||
| 	var prompt string | ||||
| 	if err := args.Bind(&prompt); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	resChan := make(chan string) | ||||
| 	cancelChan := make(chan struct{}) | ||||
| 	go func() { | ||||
| 		commandctrl.PostMsg(ctx, events.PromptForKeyMsg{ | ||||
| 			Prompt: prompt, | ||||
| 			OnDone: func(value string) tea.Msg { | ||||
| 				resChan <- value | ||||
| 				return nil | ||||
| 			}, | ||||
| 			OnCancel: func() tea.Msg { | ||||
| 				cancelChan <- struct{}{} | ||||
| 				return nil | ||||
| 			}, | ||||
| 		}) | ||||
| 	}() | ||||
| 
 | ||||
| 	select { | ||||
| 	case value := <-resChan: | ||||
| 		return value, nil | ||||
| 	case <-cancelChan: | ||||
| 		return nil, nil | ||||
| 	case <-ctx.Done(): | ||||
| 		return nil, ctx.Err() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (m *uiModule) uiPromptTable(ctx context.Context, args ucl.CallArgs) (any, error) { | ||||
| 	tables, err := m.tableService.ListTables(context.Background()) | ||||
| 	if err != nil { | ||||
|  | @ -209,14 +166,13 @@ func moduleUI( | |||
| 	return ucl.Module{ | ||||
| 		Name: "ui", | ||||
| 		Builtins: map[string]ucl.BuiltinHandler{ | ||||
| 			"command":         m.uiCommand, | ||||
| 			"prompt":          m.uiPrompt, | ||||
| 			"prompt-table":    m.uiPromptTable, | ||||
| 			"prompt-keypress": m.uiInKey, | ||||
| 			"confirm":         m.uiConfirm, | ||||
| 			"query":           m.uiQuery, | ||||
| 			"filter":          m.uiFilter, | ||||
| 			"bind":            m.uiBind, | ||||
| 			"command":      m.uiCommand, | ||||
| 			"prompt":       m.uiPrompt, | ||||
| 			"prompt-table": m.uiPromptTable, | ||||
| 			"confirm":      m.uiConfirm, | ||||
| 			"query":        m.uiQuery, | ||||
| 			"filter":       m.uiFilter, | ||||
| 			"bind":         m.uiBind, | ||||
| 		}, | ||||
| 	}, m.ckb | ||||
| } | ||||
|  |  | |||
|  | @ -2,12 +2,10 @@ package cmdpacks | |||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"maps" | ||||
| 	"strconv" | ||||
| 
 | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" | ||||
| 	"github.com/pkg/errors" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"ucl.lmika.dev/ucl" | ||||
| ) | ||||
| 
 | ||||
|  | @ -100,13 +98,6 @@ var resultSetProxyFields = &proxyInfo[*models.ResultSet]{ | |||
| 		"Table":       func(t *models.ResultSet) ucl.Object { return newTableProxy(t.TableInfo) }, | ||||
| 		"Items":       func(t *models.ResultSet) ucl.Object { return resultSetItemsProxy{t} }, | ||||
| 		"HasNextPage": func(t *models.ResultSet) ucl.Object { return ucl.BoolObject(t.HasNextPage()) }, | ||||
| 		"First": func(t *models.ResultSet) ucl.Object { | ||||
| 			items := t.Items() | ||||
| 			if len(items) == 0 { | ||||
| 				return nil | ||||
| 			} | ||||
| 			return itemProxy{resultSet: t, idx: 0, item: items[0]} | ||||
| 		}, | ||||
| 	}, | ||||
| } | ||||
| 
 | ||||
|  | @ -174,31 +165,6 @@ func (tp resultSetItemsProxy) Index(k int) ucl.Object { | |||
| 	return itemProxy{resultSet: tp.resultSet, idx: k, item: tp.resultSet.Items()[k]} | ||||
| } | ||||
| 
 | ||||
| type resultSetMarkedItemsProxy struct { | ||||
| 	resultSet *models.ResultSet | ||||
| } | ||||
| 
 | ||||
| func (ip resultSetMarkedItemsProxy) String() string { | ||||
| 	return fmt.Sprintf("MarkedItems(%v)", len(ip.resultSet.MarkedItems())) | ||||
| } | ||||
| 
 | ||||
| func (ip resultSetMarkedItemsProxy) Truthy() bool { | ||||
| 	return len(ip.resultSet.MarkedItems()) > 0 | ||||
| } | ||||
| 
 | ||||
| func (tp resultSetMarkedItemsProxy) Len() int { | ||||
| 	return len(tp.resultSet.MarkedItems()) | ||||
| } | ||||
| 
 | ||||
| func (tp resultSetMarkedItemsProxy) Index(k int) ucl.Object { | ||||
| 	markedItems := tp.resultSet.MarkedItems() | ||||
| 	if k >= len(markedItems) { | ||||
| 		return nil | ||||
| 	} | ||||
| 	actualItem := tp.resultSet.Items()[markedItems[k].Index] | ||||
| 	return itemProxy{resultSet: tp.resultSet, idx: markedItems[k].Index, item: actualItem} | ||||
| } | ||||
| 
 | ||||
| type itemProxy struct { | ||||
| 	resultSet *models.ResultSet | ||||
| 	idx       int | ||||
|  | @ -234,18 +200,6 @@ func (tp itemProxy) Each(fn func(k string, v ucl.Object) error) error { | |||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| type attributeValueProxy struct { | ||||
| 	value types.AttributeValue | ||||
| } | ||||
| 
 | ||||
| func (ip attributeValueProxy) String() string { | ||||
| 	return "attributeValueProxy()" | ||||
| } | ||||
| 
 | ||||
| func (ip attributeValueProxy) Truthy() bool { | ||||
| 	return ip.value != nil | ||||
| } | ||||
| 
 | ||||
| func convertAttributeValueToUCLObject(attrValue types.AttributeValue) ucl.Object { | ||||
| 	switch t := attrValue.(type) { | ||||
| 	case *types.AttributeValueMemberS: | ||||
|  | @ -256,58 +210,7 @@ func convertAttributeValueToUCLObject(attrValue types.AttributeValue) ucl.Object | |||
| 			return nil | ||||
| 		} | ||||
| 		return ucl.IntObject(i) | ||||
| 	case *types.AttributeValueMemberBOOL: | ||||
| 		return ucl.BoolObject(t.Value) | ||||
| 	case *types.AttributeValueMemberL: | ||||
| 		vs := make(ucl.ListObject, len(t.Value)) | ||||
| 		for i, v := range t.Value { | ||||
| 			vs[i] = convertAttributeValueToUCLObject(v) | ||||
| 		} | ||||
| 		return &vs | ||||
| 	case *types.AttributeValueMemberM: | ||||
| 		hs := make(ucl.HashObject) | ||||
| 		for k, v := range t.Value { | ||||
| 			hs[k] = convertAttributeValueToUCLObject(v) | ||||
| 		} | ||||
| 		return hs | ||||
| 	} | ||||
| 
 | ||||
| 	return attributeValueProxy{value: attrValue} | ||||
| } | ||||
| 
 | ||||
| func mapUCLObjectToAttributeType(obj ucl.Object) (types.AttributeValue, error) { | ||||
| 	switch t := obj.(type) { | ||||
| 	case ucl.StringObject: | ||||
| 		return &types.AttributeValueMemberS{Value: t.String()}, nil | ||||
| 	case ucl.IntObject: | ||||
| 		return &types.AttributeValueMemberN{Value: t.String()}, nil | ||||
| 	case ucl.BoolObject: | ||||
| 		return &types.AttributeValueMemberBOOL{Value: t.Truthy()}, nil | ||||
| 	case ucl.Listable: | ||||
| 		vals := make([]types.AttributeValue, t.Len()) | ||||
| 		for i := 0; i < t.Len(); i++ { | ||||
| 			v, err := mapUCLObjectToAttributeType(t.Index(i)) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			vals[i] = v | ||||
| 		} | ||||
| 		return &types.AttributeValueMemberL{Value: vals}, nil | ||||
| 	case ucl.Hashable: | ||||
| 		vals := make(map[string]types.AttributeValue) | ||||
| 		if err := t.Each(func(k string, v ucl.Object) error { | ||||
| 			vv, err := mapUCLObjectToAttributeType(v) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			vals[k] = vv | ||||
| 			return nil | ||||
| 		}); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		return &types.AttributeValueMemberM{Value: vals}, nil | ||||
| 	case attributeValueProxy: | ||||
| 		return t.value, nil | ||||
| 	} | ||||
| 	return nil, errors.New("unsupported attribute type") | ||||
| 	// TODO: the rest
 | ||||
| 	return nil | ||||
| } | ||||
|  |  | |||
|  | @ -2,53 +2,20 @@ package cmdpacks | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 
 | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"github.com/pkg/errors" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/controllers" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables" | ||||
| ) | ||||
| 
 | ||||
| type tablePVar struct { | ||||
| 	state          *controllers.State | ||||
| 	tableService   *tables.Service | ||||
| 	readController *controllers.TableReadController | ||||
| 	state *controllers.State | ||||
| } | ||||
| 
 | ||||
| func (rs tablePVar) Get(ctx context.Context) (any, error) { | ||||
| 	return newTableProxy(rs.state.ResultSet().TableInfo), nil | ||||
| } | ||||
| 
 | ||||
| func (rs tablePVar) Set(ctx context.Context, value any) error { | ||||
| 	scanNewTable := func(name string) error { | ||||
| 		tableInfo, err := rs.tableService.Describe(ctx, name) | ||||
| 		if err != nil { | ||||
| 			return errors.Wrapf(err, "cannot describe %v", name) | ||||
| 		} | ||||
| 
 | ||||
| 		resultSet, err := rs.tableService.Scan(ctx, tableInfo) | ||||
| 		if resultSet != nil { | ||||
| 			resultSet = rs.tableService.Filter(resultSet, rs.state.Filter()) | ||||
| 		} | ||||
| 
 | ||||
| 		msg := rs.readController.SetResultSet(resultSet) | ||||
| 		commandctrl.PostMsg(ctx, msg) | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	tblVal, ok := value.(SimpleProxy[*models.TableInfo]) | ||||
| 	if ok { | ||||
| 		return scanNewTable(tblVal.value.Name) | ||||
| 	} | ||||
| 
 | ||||
| 	strVal, ok := value.(string) | ||||
| 	if ok { | ||||
| 		return scanNewTable(strVal) | ||||
| 	} | ||||
| 	return errors.New("new value to @table is not a table name") | ||||
| } | ||||
| 
 | ||||
| type resultSetPVar struct { | ||||
| 	state          *controllers.State | ||||
| 	readController *controllers.TableReadController | ||||
|  | @ -69,15 +36,6 @@ func (rs resultSetPVar) Set(ctx context.Context, value any) error { | |||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| type markedSetPVar struct { | ||||
| 	state          *controllers.State | ||||
| 	readController *controllers.TableReadController | ||||
| } | ||||
| 
 | ||||
| func (rs markedSetPVar) Get(ctx context.Context) (any, error) { | ||||
| 	return resultSetMarkedItemsProxy{rs.state.ResultSet()}, nil | ||||
| } | ||||
| 
 | ||||
| type itemPVar struct { | ||||
| 	state *controllers.State | ||||
| } | ||||
|  | @ -85,14 +43,9 @@ type itemPVar struct { | |||
| func (rs itemPVar) Get(ctx context.Context) (any, error) { | ||||
| 	selItem, ok := commandctrl.SelectedItemIndex(ctx) | ||||
| 	if !ok { | ||||
| 		return nil, nil | ||||
| 		return nil, errors.New("no item selected") | ||||
| 	} | ||||
| 	rset := rs.state.ResultSet() | ||||
| 	if selItem < 0 || selItem >= len(rs.state.ResultSet().Items()) { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 
 | ||||
| 	return itemProxy{rset, selItem, rset.Items()[selItem]}, nil | ||||
| 	return itemProxy{rs.state.ResultSet(), selItem, rs.state.ResultSet().Items()[selItem]}, nil | ||||
| } | ||||
| 
 | ||||
| func (rs itemPVar) Set(ctx context.Context, value any) error { | ||||
|  |  | |||
|  | @ -1,32 +0,0 @@ | |||
| package cmdpacks_test | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
| func TestPVars(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		descr string | ||||
| 		cmd   string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			descr: "returns item on empty result set", | ||||
| 			cmd: ` | ||||
| 				ui:query '"a"="1"' -table service-test-data | ||||
| 				@item | ||||
| 			`, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.descr, func(t *testing.T) { | ||||
| 			svc := newService(t) | ||||
| 
 | ||||
| 			ctx := t.Context() | ||||
| 
 | ||||
| 			_, err := svc.CommandController.ExecuteAndWait(ctx, tt.cmd) | ||||
| 			assert.NoError(t, err) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | @ -2,14 +2,13 @@ package cmdpacks | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 
 | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" | ||||
| 	"github.com/pkg/errors" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/controllers" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables" | ||||
| 	"ucl.lmika.dev/repl" | ||||
| 	"ucl.lmika.dev/ucl" | ||||
| ) | ||||
|  | @ -399,7 +398,6 @@ func (sc StandardCommands) InstOptions() []ucl.InstOption { | |||
| 		ucl.WithModule(sc.modUI), | ||||
| 		ucl.WithModule(modulePB(sc.PBProvider)), | ||||
| 		ucl.WithModule(moduleOpt(sc.SettingsController)), | ||||
| 		ucl.WithModule(moduleAttrValue()), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -427,9 +425,8 @@ func (sc StandardCommands) ConfigureUCL(ucl *ucl.Inst) { | |||
| 	ucl.SetBuiltin("q", sc.cmdQuit) | ||||
| 
 | ||||
| 	ucl.SetPseudoVar("resultset", resultSetPVar{sc.State, sc.ReadController}) | ||||
| 	ucl.SetPseudoVar("table", tablePVar{sc.State, sc.TableService, sc.ReadController}) | ||||
| 	ucl.SetPseudoVar("table", tablePVar{sc.State}) | ||||
| 	ucl.SetPseudoVar("item", itemPVar{sc.State}) | ||||
| 	ucl.SetPseudoVar("marked", markedSetPVar{sc.State, sc.ReadController}) | ||||
| } | ||||
| 
 | ||||
| func (sc StandardCommands) RunPrelude(ctx context.Context, ucl *ucl.Inst) error { | ||||
|  | @ -440,13 +437,4 @@ func (sc StandardCommands) RunPrelude(ctx context.Context, ucl *ucl.Inst) error | |||
| const uclPrelude = ` | ||||
| ui:command unmark { mark none } | ||||
| ui:command set-opt { |n k| opt:set $n $k } | ||||
| 
 | ||||
| ui:bind "view.toggle-marked-items" "M" { | ||||
|   markedCount = len @marked | ||||
|   if (eq $markedCount (len @resultset)) { | ||||
|     mark none | ||||
|   } else { | ||||
| 	mark all | ||||
|   } | ||||
| } | ||||
| ` | ||||
|  |  | |||
|  | @ -3,24 +3,25 @@ package cmdpacks_test | |||
| import ( | ||||
| 	"fmt" | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl/cmdpacks" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/dynamo" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/inputhistorystore" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/settingstore" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/workspacestore" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs" | ||||
| 	keybindings_service "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/keybindings" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/viewsnapshot" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings" | ||||
| 	"github.com/lmika/dynamo-browse/test/testdynamo" | ||||
| 	"github.com/lmika/dynamo-browse/test/testworkspace" | ||||
| 	bus "github.com/lmika/events" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl/cmdpacks" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/controllers" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/providers/dynamo" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/providers/inputhistorystore" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/providers/settingstore" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/providers/workspacestore" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/inputhistory" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/itemrenderer" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/jobs" | ||||
| 	keybindings_service "lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/keybindings" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/viewsnapshot" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/keybindings" | ||||
| 	"lmika.dev/cmd/dynamo-browse/test/testdynamo" | ||||
| 	"lmika.dev/cmd/dynamo-browse/test/testworkspace" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
|  | @ -52,20 +53,6 @@ func TestStdCmds_Mark(t *testing.T) { | |||
| } | ||||
| 
 | ||||
| type testDataGenerator func() []testdynamo.TestData | ||||
| 
 | ||||
| type testPasteBoard struct { | ||||
| 	content string | ||||
| } | ||||
| 
 | ||||
| func (t *testPasteBoard) ReadText() (string, bool) { | ||||
| 	return t.content, true | ||||
| } | ||||
| 
 | ||||
| func (t *testPasteBoard) WriteText(bts []byte) error { | ||||
| 	t.content = string(bts) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| type services struct { | ||||
| 	CommandController *commandctrl.CommandController | ||||
| 	SelItemIndex      int | ||||
|  | @ -74,7 +61,6 @@ type services struct { | |||
| 
 | ||||
| 	settingStore *settingstore.SettingStore | ||||
| 	table        string | ||||
| 	pasteboard   *testPasteBoard | ||||
| 
 | ||||
| 	testDataGenerator testDataGenerator | ||||
| 	testData          []testdynamo.TestData | ||||
|  | @ -111,12 +97,10 @@ func newService(t *testing.T, opts ...serviceOpt) *services { | |||
| 	itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer()) | ||||
| 	inputHistoryService := inputhistory.New(inputHistoryStore) | ||||
| 
 | ||||
| 	testPB := &testPasteBoard{} | ||||
| 	s := &services{ | ||||
| 		table:             "service-test-data", | ||||
| 		settingStore:      settingStore, | ||||
| 		testDataGenerator: normalTestData, | ||||
| 		pasteboard:        testPB, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, opt := range opts { | ||||
|  | @ -141,13 +125,13 @@ func newService(t *testing.T, opts ...serviceOpt) *services { | |||
| 		jobsController, | ||||
| 		inputHistoryService, | ||||
| 		eventBus, | ||||
| 		testPB, | ||||
| 		pasteboardprovider.NilProvider{}, | ||||
| 		s.table, | ||||
| 	) | ||||
| 	writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore) | ||||
| 	settingsController := controllers.NewSettingsController(settingStore, eventBus) | ||||
| 	columnsController := controllers.NewColumnsController(readController, eventBus) | ||||
| 	exportController := controllers.NewExportController(state, service, jobsController, columnsController, testPB) | ||||
| 	exportController := controllers.NewExportController(state, service, jobsController, columnsController, pasteboardprovider.NilProvider{}) | ||||
| 
 | ||||
| 	keyBindingService := keybindings_service.NewService(keybindings.Default()) | ||||
| 	keyBindingController := controllers.NewKeyBindingController(keyBindingService, nil) | ||||
|  | @ -160,7 +144,7 @@ func newService(t *testing.T, opts ...serviceOpt) *services { | |||
| 			writeController, | ||||
| 			exportController, | ||||
| 			keyBindingController, | ||||
| 			testPB, | ||||
| 			pasteboardprovider.NilProvider{}, | ||||
| 			settingsController, | ||||
| 		), | ||||
| 	) | ||||
|  |  | |||
|  | @ -13,8 +13,8 @@ import ( | |||
| 	"ucl.lmika.dev/ucl" | ||||
| 	"ucl.lmika.dev/ucl/builtins" | ||||
| 
 | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/events" | ||||
| 	"github.com/lmika/shellwords" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/events" | ||||
| ) | ||||
| 
 | ||||
| const commandsCategory = "commands" | ||||
|  | @ -47,14 +47,8 @@ func NewCommandController(historyProvider IterProvider, pkgs ...CommandPack) (*C | |||
| 
 | ||||
| 	options := []ucl.InstOption{ | ||||
| 		ucl.WithOut(ucl.LineHandler(cc.printLine)), | ||||
| 		ucl.WithModule(builtins.CSV(nil)), | ||||
| 		ucl.WithModule(builtins.FS(nil)), | ||||
| 		ucl.WithModule(builtins.Log(nil)), | ||||
| 		ucl.WithModule(builtins.Itrs()), | ||||
| 		ucl.WithModule(builtins.OS()), | ||||
| 		ucl.WithModule(builtins.Strs()), | ||||
| 		ucl.WithModule(builtins.Lists()), | ||||
| 		ucl.WithModule(builtins.Time()), | ||||
| 		ucl.WithModule(builtins.FS(nil)), | ||||
| 	} | ||||
| 	for _, pkg := range pkgs { | ||||
| 		options = append(options, pkg.InstOptions()...) | ||||
|  |  | |||
|  | @ -2,11 +2,11 @@ package commandctrl_test | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/events" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/events" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ package commandctrl | |||
| import ( | ||||
| 	"context" | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services" | ||||
| ) | ||||
| 
 | ||||
| type IterProvider interface { | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ package dispatcher | |||
| 
 | ||||
| import ( | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/uimodels" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/uimodels" | ||||
| ) | ||||
| 
 | ||||
| type DispatcherContext struct { | ||||
|  |  | |||
|  | @ -4,8 +4,8 @@ import ( | |||
| 	"context" | ||||
| 	"sync" | ||||
| 
 | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/events" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/uimodels" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/events" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/uimodels" | ||||
| 	"github.com/pkg/errors" | ||||
| ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,10 +1,9 @@ | |||
| package events | ||||
| 
 | ||||
| import ( | ||||
| 	"log" | ||||
| 
 | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services" | ||||
| 	"log" | ||||
| ) | ||||
| 
 | ||||
| func Error(err error) tea.Msg { | ||||
|  | @ -32,23 +31,10 @@ func PromptForInput(prompt string, history services.HistoryProvider, onDone func | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| func PromptForKey(prompt string, onDone func(key string) tea.Msg) tea.Msg { | ||||
| 	return PromptForKeyMsg{ | ||||
| 		Prompt: prompt, | ||||
| 		OnDone: onDone, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func Confirm(prompt string, onResult func(yes bool) tea.Msg) tea.Msg { | ||||
| 	return PromptForInputMsg{ | ||||
| 		Prompt: prompt, | ||||
| 		OnDone: func(value string) tea.Msg { | ||||
| 			return onResult(value == "y") | ||||
| 		}, | ||||
| 		OnCancel: func() tea.Msg { | ||||
| 			return onResult(false) | ||||
| 		}, | ||||
| 	} | ||||
| 	return PromptForInput(prompt, nil, func(value string) tea.Msg { | ||||
| 		return onResult(value == "y") | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func ConfirmYes(prompt string, onYes func() tea.Msg) tea.Msg { | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ package events | |||
| 
 | ||||
| import ( | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services" | ||||
| ) | ||||
| 
 | ||||
| // Error indicates that an error occurred
 | ||||
|  | @ -27,10 +27,3 @@ type PromptForInputMsg struct { | |||
| 	OnCancel      func() tea.Msg | ||||
| 	OnTabComplete func(value string) (string, bool) | ||||
| } | ||||
| 
 | ||||
| // PromptForKey indicates that the context is requesting a single key press
 | ||||
| type PromptForKeyMsg struct { | ||||
| 	Prompt   string | ||||
| 	OnDone   func(key string) tea.Msg | ||||
| 	OnCancel func() tea.Msg | ||||
| } | ||||
|  |  | |||
|  | @ -2,11 +2,11 @@ package controllers | |||
| 
 | ||||
| import ( | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/events" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/columns" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/evaluators" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/queryexpr" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/events" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/columns" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/evaluators" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" | ||||
| 	bus "github.com/lmika/events" | ||||
| 	"strings" | ||||
| ) | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ package controllers | |||
| 
 | ||||
| import ( | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/events" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/events" | ||||
| ) | ||||
| 
 | ||||
| type promptSequence struct { | ||||
|  |  | |||
|  | @ -6,8 +6,8 @@ import ( | |||
| 	"time" | ||||
| 
 | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/relitems" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems" | ||||
| ) | ||||
| 
 | ||||
| type SetTableItemView struct { | ||||
|  |  | |||
|  | @ -9,12 +9,12 @@ import ( | |||
| 	"os" | ||||
| 
 | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/events" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/attrutils" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/columns" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/jobs" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/events" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/columns" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs" | ||||
| 	"github.com/pkg/errors" | ||||
| ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,8 +2,8 @@ package controllers_test | |||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/sliceutils" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/controllers" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/sliceutils" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"os" | ||||
| 	"strings" | ||||
|  |  | |||
|  | @ -6,8 +6,8 @@ import ( | |||
| 
 | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/relitems" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems" | ||||
| ) | ||||
| 
 | ||||
| type TableReadService interface { | ||||
|  |  | |||
|  | @ -3,8 +3,8 @@ package controllers | |||
| import ( | ||||
| 	"context" | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/events" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/jobs" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/events" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs" | ||||
| ) | ||||
| 
 | ||||
| func NewJob[T any](jc *JobsController, description string, job func(ctx context.Context) (T, error)) JobBuilder[T] { | ||||
|  |  | |||
|  | @ -2,8 +2,8 @@ package controllers | |||
| 
 | ||||
| import ( | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/events" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/jobs" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/events" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs" | ||||
| 	bus "github.com/lmika/events" | ||||
| 	"log" | ||||
| ) | ||||
|  |  | |||
|  | @ -3,8 +3,8 @@ package controllers | |||
| import ( | ||||
| 	"fmt" | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/events" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/keybindings" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/events" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/keybindings" | ||||
| 	"github.com/pkg/errors" | ||||
| ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ package controllers | |||
| import ( | ||||
| 	"fmt" | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/events" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/events" | ||||
| 	bus "github.com/lmika/events" | ||||
| 	"github.com/pkg/errors" | ||||
| 	"log" | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| package controllers_test | ||||
| 
 | ||||
| import ( | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/events" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/controllers" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/events" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"testing" | ||||
| ) | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| package controllers | ||||
| 
 | ||||
| import ( | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"sync" | ||||
| ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -10,18 +10,18 @@ import ( | |||
| 
 | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/events" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrcodec" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/serialisable" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/viewsnapshot" | ||||
| 	bus "github.com/lmika/events" | ||||
| 	"github.com/pkg/errors" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/events" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/attrcodec" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/attrutils" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/queryexpr" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/serialisable" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/inputhistory" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/itemrenderer" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/viewsnapshot" | ||||
| ) | ||||
| 
 | ||||
| type resultSetUpdateOp int | ||||
|  |  | |||
|  | @ -4,9 +4,9 @@ import ( | |||
| 	"fmt" | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/events" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/controllers" | ||||
| 	"lmika.dev/cmd/dynamo-browse/test/testdynamo" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/events" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" | ||||
| 	"github.com/lmika/dynamo-browse/test/testdynamo" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"os" | ||||
| 	"strings" | ||||
|  |  | |||
|  | @ -5,11 +5,11 @@ import ( | |||
| 	"fmt" | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/sliceutils" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/events" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/queryexpr" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/sliceutils" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/events" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" | ||||
| 	"github.com/pkg/errors" | ||||
| 	"log" | ||||
| 	"strconv" | ||||
|  |  | |||
|  | @ -4,21 +4,21 @@ import ( | |||
| 	"fmt" | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/controllers" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/providers/dynamo" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/providers/inputhistorystore" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/providers/settingstore" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/providers/workspacestore" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/inputhistory" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/itemrenderer" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/jobs" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/viewsnapshot" | ||||
| 	"lmika.dev/cmd/dynamo-browse/test/testdynamo" | ||||
| 	"lmika.dev/cmd/dynamo-browse/test/testworkspace" | ||||
| 	"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/dynamo" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/inputhistorystore" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/settingstore" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/workspacestore" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/viewsnapshot" | ||||
| 	"github.com/lmika/dynamo-browse/test/testdynamo" | ||||
| 	"github.com/lmika/dynamo-browse/test/testworkspace" | ||||
| 	bus "github.com/lmika/events" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"io/fs" | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| package controllers | ||||
| 
 | ||||
| import "lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models" | ||||
| import "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" | ||||
| 
 | ||||
| func applyToMarkedItems(rs *models.ResultSet, selectedIndex int, applyFn func(idx int, item models.Item) error) error { | ||||
| 	if markedItems := rs.MarkedItems(); len(markedItems) > 0 { | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ package attrcodec_test | |||
| import ( | ||||
| 	"bytes" | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/attrcodec" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrcodec" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| package columns | ||||
| 
 | ||||
| import ( | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models" | ||||
| ) | ||||
| 
 | ||||
| type Columns struct { | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| package evaluators | ||||
| 
 | ||||
| import ( | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/queryexpr" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" | ||||
| ) | ||||
| 
 | ||||
| func Equals(x, y models.FieldValueEvaluator) bool { | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ package models | |||
| 
 | ||||
| import ( | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" | ||||
| 	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/attrutils" | ||||
| 	"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils" | ||||
| ) | ||||
| 
 | ||||
| type ItemIndex struct { | ||||
|  |  | |||
|  | @ -1,11 +1,9 @@ | |||
| package models | ||||
| 
 | ||||
| import ( | ||||
| 	"sort" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" | ||||
| 	"sort" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| type ResultSet struct { | ||||
|  | @ -22,10 +20,6 @@ type ResultSet struct { | |||
| 
 | ||||
| 	columns      []string | ||||
| 	sortCriteria SortCriteria | ||||
| 
 | ||||
| 	mutex                sync.Mutex | ||||
| 	cachedMarkedItems    []ItemIndex | ||||
| 	hasCachedMarkedItems bool | ||||
| } | ||||
| 
 | ||||
| type Queryable interface { | ||||
|  | @ -53,11 +47,6 @@ func (rs *ResultSet) Items() []Item { | |||
| func (rs *ResultSet) SetItems(items []Item) { | ||||
| 	rs.items = items | ||||
| 	rs.attributes = make([]ItemAttribute, len(items)) | ||||
| 
 | ||||
| 	rs.mutex.Lock() | ||||
| 	defer rs.mutex.Unlock() | ||||
| 	rs.hasCachedMarkedItems = false | ||||
| 	rs.cachedMarkedItems = nil | ||||
| } | ||||
| 
 | ||||
| func (rs *ResultSet) SortCriteria() SortCriteria { | ||||
|  | @ -67,24 +56,10 @@ func (rs *ResultSet) SortCriteria() SortCriteria { | |||
| func (rs *ResultSet) AddNewItem(item Item, attrs ItemAttribute) { | ||||
| 	rs.items = append(rs.items, item) | ||||
| 	rs.attributes = append(rs.attributes, attrs) | ||||
| 
 | ||||
| 	rs.mutex.Lock() | ||||
| 	defer rs.mutex.Unlock() | ||||
| 	rs.hasCachedMarkedItems = false | ||||
| 	rs.cachedMarkedItems = nil | ||||
| } | ||||
| 
 | ||||
| func (rs *ResultSet) SetMark(idx int, marked bool) { | ||||
| 	rs.attributes[idx].Marked = marked | ||||
| 
 | ||||
| 	if !rs.hasCachedMarkedItems { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	rs.mutex.Lock() | ||||
| 	defer rs.mutex.Unlock() | ||||
| 	rs.hasCachedMarkedItems = false | ||||
| 	rs.cachedMarkedItems = nil | ||||
| } | ||||
| 
 | ||||
| func (rs *ResultSet) SetHidden(idx int, hidden bool) { | ||||
|  | @ -116,20 +91,12 @@ func (rs *ResultSet) IsNew(idx int) bool { | |||
| } | ||||
| 
 | ||||
| func (rs *ResultSet) MarkedItems() []ItemIndex { | ||||
| 	rs.mutex.Lock() | ||||
| 	defer rs.mutex.Unlock() | ||||
| 	if rs.hasCachedMarkedItems { | ||||
| 		return rs.cachedMarkedItems | ||||
| 	} | ||||
| 
 | ||||
| 	items := make([]ItemIndex, 0) | ||||
| 	for i, itemAttr := range rs.attributes { | ||||
| 		if itemAttr.Marked && !itemAttr.Hidden { | ||||
| 			items = append(items, ItemIndex{Index: i, Item: rs.items[i]}) | ||||
| 		} | ||||
| 	} | ||||
| 	rs.cachedMarkedItems = items | ||||
| 	rs.hasCachedMarkedItems = true | ||||
| 	return items | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,183 +0,0 @@ | |||
| package models | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
| func TestMarkedItems(t *testing.T) { | ||||
| 	t.Run("SetMark properly reflected in MarkedItems", func(t *testing.T) { | ||||
| 		rs := &ResultSet{} | ||||
| 		rs.SetItems([]Item{ | ||||
| 			{"id": &types.AttributeValueMemberS{Value: "item1"}}, | ||||
| 			{"id": &types.AttributeValueMemberS{Value: "item2"}}, | ||||
| 			{"id": &types.AttributeValueMemberS{Value: "item3"}}, | ||||
| 		}) | ||||
| 
 | ||||
| 		// Initially, no items should be marked
 | ||||
| 		assert.Len(t, rs.MarkedItems(), 0) | ||||
| 
 | ||||
| 		// Mark the first item
 | ||||
| 		rs.SetMark(0, true) | ||||
| 		markedItems := rs.MarkedItems() | ||||
| 		assert.Len(t, markedItems, 1) | ||||
| 		assert.Equal(t, 0, markedItems[0].Index) | ||||
| 
 | ||||
| 		// Mark the third item
 | ||||
| 		rs.SetMark(2, true) | ||||
| 		markedItems = rs.MarkedItems() | ||||
| 		assert.Len(t, markedItems, 2) | ||||
| 		assert.Equal(t, 0, markedItems[0].Index) | ||||
| 		assert.Equal(t, 2, markedItems[1].Index) | ||||
| 
 | ||||
| 		// Verify the items themselves are correct
 | ||||
| 		item1, ok1 := markedItems[0].Item.AttributeValueAsString("id") | ||||
| 		item2, ok2 := markedItems[1].Item.AttributeValueAsString("id") | ||||
| 		assert.True(t, ok1) | ||||
| 		assert.True(t, ok2) | ||||
| 		assert.Equal(t, "item1", item1) | ||||
| 		assert.Equal(t, "item3", item2) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("item with Marked=true is in MarkedItems", func(t *testing.T) { | ||||
| 		rs := &ResultSet{} | ||||
| 		rs.SetItems([]Item{ | ||||
| 			{"id": &types.AttributeValueMemberS{Value: "item1"}}, | ||||
| 			{"id": &types.AttributeValueMemberS{Value: "item2"}}, | ||||
| 			{"id": &types.AttributeValueMemberS{Value: "item3"}}, | ||||
| 		}) | ||||
| 
 | ||||
| 		// Directly set the Marked attribute to true for item at index 1
 | ||||
| 		rs.SetMark(1, true) | ||||
| 
 | ||||
| 		markedItems := rs.MarkedItems() | ||||
| 		assert.Len(t, markedItems, 1) | ||||
| 		assert.Equal(t, 1, markedItems[0].Index) | ||||
| 
 | ||||
| 		item, ok := markedItems[0].Item.AttributeValueAsString("id") | ||||
| 		assert.True(t, ok) | ||||
| 		assert.Equal(t, "item2", item) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("adding marked items affects result of MarkedItems", func(t *testing.T) { | ||||
| 		rs := &ResultSet{} | ||||
| 		rs.SetItems([]Item{ | ||||
| 			{"id": &types.AttributeValueMemberS{Value: "item1"}}, | ||||
| 			{"id": &types.AttributeValueMemberS{Value: "item2"}}, | ||||
| 			{"id": &types.AttributeValueMemberS{Value: "item3"}}, | ||||
| 		}) | ||||
| 
 | ||||
| 		// Mark all items
 | ||||
| 		rs.SetMark(0, true) | ||||
| 		rs.SetMark(1, true) | ||||
| 		assert.Len(t, rs.MarkedItems(), 2) | ||||
| 
 | ||||
| 		markedItems := rs.MarkedItems() | ||||
| 		expectedIndices := []int{0, 1} | ||||
| 		for i, expected := range expectedIndices { | ||||
| 			assert.Equal(t, expected, markedItems[i].Index) | ||||
| 		} | ||||
| 
 | ||||
| 		// Add a new unmarked item
 | ||||
| 		rs.AddNewItem(Item{"id": &types.AttributeValueMemberS{Value: "item4"}}, ItemAttribute{}) | ||||
| 		assert.Len(t, rs.MarkedItems(), 2) | ||||
| 
 | ||||
| 		// Add a new marked item
 | ||||
| 		rs.AddNewItem(Item{"id": &types.AttributeValueMemberS{Value: "item5"}}, ItemAttribute{Marked: true}) | ||||
| 		assert.Len(t, rs.MarkedItems(), 3) | ||||
| 
 | ||||
| 		markedItems = rs.MarkedItems() | ||||
| 		expectedIndices = []int{0, 1, 4} | ||||
| 		for i, expected := range expectedIndices { | ||||
| 			assert.Equal(t, expected, markedItems[i].Index) | ||||
| 		} | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("changing SetMark updates length of MarkedItems", func(t *testing.T) { | ||||
| 		rs := &ResultSet{} | ||||
| 		rs.SetItems([]Item{ | ||||
| 			{"id": &types.AttributeValueMemberS{Value: "item1"}}, | ||||
| 			{"id": &types.AttributeValueMemberS{Value: "item2"}}, | ||||
| 			{"id": &types.AttributeValueMemberS{Value: "item3"}}, | ||||
| 			{"id": &types.AttributeValueMemberS{Value: "item4"}}, | ||||
| 		}) | ||||
| 
 | ||||
| 		// Mark all items
 | ||||
| 		rs.SetMark(0, true) | ||||
| 		rs.SetMark(1, true) | ||||
| 		rs.SetMark(2, true) | ||||
| 		rs.SetMark(3, true) | ||||
| 		assert.Len(t, rs.MarkedItems(), 4) | ||||
| 
 | ||||
| 		// Unmark one item
 | ||||
| 		rs.SetMark(1, false) | ||||
| 		assert.Len(t, rs.MarkedItems(), 3) | ||||
| 
 | ||||
| 		// Verify the correct items are marked
 | ||||
| 		markedItems := rs.MarkedItems() | ||||
| 		expectedIndices := []int{0, 2, 3} | ||||
| 		for i, expected := range expectedIndices { | ||||
| 			assert.Equal(t, expected, markedItems[i].Index) | ||||
| 		} | ||||
| 
 | ||||
| 		// Unmark all remaining items
 | ||||
| 		rs.SetMark(0, false) | ||||
| 		rs.SetMark(2, false) | ||||
| 		rs.SetMark(3, false) | ||||
| 		assert.Len(t, rs.MarkedItems(), 0) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("changing items clears all marked items", func(t *testing.T) { | ||||
| 		rs := &ResultSet{} | ||||
| 		rs.SetItems([]Item{ | ||||
| 			{"id": &types.AttributeValueMemberS{Value: "item1"}}, | ||||
| 			{"id": &types.AttributeValueMemberS{Value: "item2"}}, | ||||
| 			{"id": &types.AttributeValueMemberS{Value: "item3"}}, | ||||
| 		}) | ||||
| 
 | ||||
| 		// Mark all items
 | ||||
| 		rs.SetMark(0, true) | ||||
| 		rs.SetMark(1, true) | ||||
| 		rs.SetMark(2, true) | ||||
| 		assert.Len(t, rs.MarkedItems(), 3) | ||||
| 
 | ||||
| 		// Call SetItems with new items
 | ||||
| 		rs.SetItems([]Item{ | ||||
| 			{"id": &types.AttributeValueMemberS{Value: "newitem1"}}, | ||||
| 			{"id": &types.AttributeValueMemberS{Value: "newitem2"}}, | ||||
| 		}) | ||||
| 
 | ||||
| 		// All marks should be cleared
 | ||||
| 		assert.Len(t, rs.MarkedItems(), 0) | ||||
| 
 | ||||
| 		// Verify none of the new items are marked
 | ||||
| 		assert.False(t, rs.Marked(0)) | ||||
| 		assert.False(t, rs.Marked(1)) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("hidden items are excluded from MarkedItems", func(t *testing.T) { | ||||
| 		rs := &ResultSet{} | ||||
| 		rs.SetItems([]Item{ | ||||
| 			{"id": &types.AttributeValueMemberS{Value: "item1"}}, | ||||
| 			{"id": &types.AttributeValueMemberS{Value: "item2"}}, | ||||
| 			{"id": &types.AttributeValueMemberS{Value: "item3"}}, | ||||
| 		}) | ||||
| 
 | ||||
| 		// Mark all items
 | ||||
| 		rs.SetMark(0, true) | ||||
| 		rs.SetMark(1, true) | ||||
| 		rs.SetMark(2, true) | ||||
| 
 | ||||
| 		// Hide the second item
 | ||||
| 		rs.SetHidden(1, true) | ||||
| 
 | ||||
| 		markedItems := rs.MarkedItems() | ||||
| 		assert.Len(t, markedItems, 2) | ||||
| 
 | ||||
| 		// Verify only items 0 and 2 are in the marked items
 | ||||
| 		assert.Equal(t, 0, markedItems[0].Index) | ||||
| 		assert.Equal(t, 2, markedItems[1].Index) | ||||
| 	}) | ||||
| } | ||||
|  | @ -1,6 +1,6 @@ | |||
| package modexpr | ||||
| 
 | ||||
| import "lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models" | ||||
| import "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" | ||||
| 
 | ||||
| func (a *astExpr) calcPatchMods(item models.Item) ([]patchMod, error) { | ||||
| 	patchMods := make([]patchMod, 0) | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| package modexpr | ||||
| 
 | ||||
| import "lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models" | ||||
| import "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" | ||||
| 
 | ||||
| type ModExpr struct { | ||||
| 	ast *astExpr | ||||
|  |  | |||