Compare commits
3 commits
main
...
feature/rs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8ec434b5d | ||
|
|
a1bda94e74 | ||
|
|
4e41ae9cd2 |
|
|
@ -4,10 +4,12 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- main
|
||||
- feature/*
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
Build:
|
||||
build:
|
||||
runs-on: docker
|
||||
services:
|
||||
localstack:
|
||||
|
|
@ -22,7 +24,7 @@ jobs:
|
|||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.25
|
||||
go-version: 1.24
|
||||
- name: Configure
|
||||
run: |
|
||||
git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika"
|
||||
|
|
@ -33,4 +35,4 @@ jobs:
|
|||
go test -p 1 ./...
|
||||
env:
|
||||
TEST_DYNAMO_URL: "http://localstack:4566"
|
||||
GOPRIVATE: "github:com/lmika/*"
|
||||
GOPRIVATE: "github:com/lmika/*"
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
name: Release
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
Build:
|
||||
build:
|
||||
runs-on: docker
|
||||
services:
|
||||
localstack:
|
||||
|
|
@ -21,7 +20,7 @@ jobs:
|
|||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.25
|
||||
go-version: 1.24
|
||||
- name: Configure
|
||||
run: |
|
||||
git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika"
|
||||
|
|
@ -31,71 +30,53 @@ jobs:
|
|||
go get ./...
|
||||
go test -p 1 ./...
|
||||
env:
|
||||
TEST_DYNAMO_URL: "http://localstack:4566"
|
||||
GOPRIVATE: "github:com/lmika/*"
|
||||
TEST_DYNAMO_URL: "http://localstack:4566"
|
||||
|
||||
Site:
|
||||
needs: Build
|
||||
runs-on: docker
|
||||
env:
|
||||
NETLIFY_SITE_ID: 987651c8-4ffd-48d8-af67-4dbd49c48887
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
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.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':
|
||||
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: Setup Dependencies
|
||||
run: |
|
||||
brew install gpg
|
||||
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: |
|
||||
go install github.com/goreleaser/goreleaser/v2@v2.12.7
|
||||
brew install goreleaser/tap/goreleaser
|
||||
brew install goreleaser
|
||||
- name: Release
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
goreleaser release -f goreleaser.yml --skip=validate --clean
|
||||
goreleaser release -f macos.goreleaser.yml --skip=validate --clean
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HOMEBREW_TAP_PRIVATE_KEY: ${{ secrets.HOMEBREW_TAP_PRIVATE_KEY }}
|
||||
MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }}
|
||||
MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }}
|
||||
MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }}
|
||||
MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}
|
||||
MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }}
|
||||
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
|
||||
|
|
|
|||
6
_certs/.gitignore
vendored
|
|
@ -1,6 +0,0 @@
|
|||
*.key
|
||||
*.p8
|
||||
*.certSigningRequest
|
||||
*.cer
|
||||
*.p12
|
||||
*.txt
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
# Certs
|
||||
|
||||
These hold the certificates for MacOS notarisation. As such they are not checked into the repository.
|
||||
|
||||
List of files is as follows:
|
||||
|
||||
- ALDsigning.key : private key
|
||||
- csr3072ALDSigning.certSigningRequest : certificate signing request
|
||||
- developerID_application.p12 : signed certificate
|
||||
- keyStore.p12 : pkcs12 keystore holding both the certificate and private key
|
||||
- AthKey_UD4...p8 : private key granting API access to AppStore connect
|
||||
|
||||
## Producing These Files
|
||||
|
||||
To produce the keys, run the following command:
|
||||
|
||||
```bash
|
||||
# create the private key. It must be RSA 2048
|
||||
$ openssl genrsa -out ALDsigning.key 2048
|
||||
|
||||
# create the CSR
|
||||
$ openssl req -new -key ALDsigning.key -out csr3072ALDSigning.certSigningRequest -subj "/emailAddress=lmika@lmika.org, CN=dev.lmika.dynamo-browse, C=IE"
|
||||
```
|
||||
|
||||
These are based on [these instructions](https://developer.apple.com/help/account/certificates/create-a-certificate-signing-request).
|
||||
The instructions are incorrect though. They claim that the key lenght should be 3096, but AppStore connect only supports 2048.
|
||||
|
||||
Then, upload the CSR to AppStore Connect, choosing the "Developer ID Application" certificate type. If successful,
|
||||
you will be given a signed certificate, which will have the filename `developerID_application.signing.cer`.
|
||||
|
||||
Then, produce a PKCS12 (.p12) file by running the following command ([source](https://stackoverflow.com/questions/21141215/creating-a-p12-file)):
|
||||
|
||||
```bash
|
||||
openssl pkcs12 -export -out keyStore.p12 -inkey ALDsigning.key -in developerID_application.signing.cer
|
||||
```
|
||||
|
||||
## Getting the .p8 file
|
||||
|
||||
To download the .p8 file, go to the [Apple Developer Portal](https://appstoreconnect.apple.com/access/integrations/api/new),
|
||||
and download a new API key for AppStore Connect. The role of the new key should be "Developer".
|
||||
|
||||
## Configuring the CI/CD secrets
|
||||
|
||||
The following secrets correspond to the given secrets:
|
||||
|
||||
- `MACOS_SIGN_P12`: base64 of keyStore.p12
|
||||
- `MACOS_SIGN_PASSWORD` the p12 password
|
||||
- `MACOS_NOTARY_ISSUER_ID`: see the UUID on this page: https://appstoreconnect.apple.com/access/integrations/api
|
||||
- `MACOS_NOTARY_KEY_ID`: the ID of the .p8 file - `U4....`
|
||||
- `MACOS_NOTARY_KEY`: base64 of the .p8 file
|
||||
4
_site/.gitignore
vendored
|
|
@ -1,4 +0,0 @@
|
|||
docs/
|
||||
node_modules/
|
||||
resources/
|
||||
themes/
|
||||
|
|
@ -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,46 +0,0 @@
|
|||
---
|
||||
weight: 10
|
||||
---
|
||||
|
||||
<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
|
||||
|
||||
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).
|
||||
|
||||
## Download
|
||||
|
||||
Binary packages for MacOS and Linux can be [found at Forgejo](https://lmika.dev/cmd/dynamo-browse/releases).
|
||||
|
||||
### MacOS Using Homebrew
|
||||
|
||||
If you have Homebrew, you can install Dynamo-Browse using the following command:
|
||||
|
||||
```
|
||||
brew tap lmika/dynamo-browse https://lmika.dev/casks/dynamo-browse
|
||||
brew install dynamo-browse
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
If you have Go installed, you can install Dynamo-Browse using the following command:
|
||||
|
||||
```
|
||||
go install lmika.dev/cmd/dynamo-browse/cmd/dynamo-browse@latest
|
||||
```
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
bookFlatSection: true
|
||||
weight: 40
|
||||
---
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
---
|
||||
title: Commands
|
||||
weight: 20
|
||||
---
|
||||
# Commands
|
||||
|
||||
These commands are meant for general, interactive use. For additional commands meant for defining
|
||||
extensions, see [Extensions](/docs/reference/extensions/).
|
||||
|
||||
## 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 <filename> [-all]
|
||||
```
|
||||
|
||||
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-opt
|
||||
|
||||
```
|
||||
: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 <attributeName> [type]
|
||||
```
|
||||
|
||||
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,7 +0,0 @@
|
|||
---
|
||||
title: "Extensions"
|
||||
type: script-api
|
||||
weight: 60
|
||||
draft: true
|
||||
---
|
||||
# Extensions
|
||||
|
|
@ -1,193 +0,0 @@
|
|||
---
|
||||
title: Key Bindings
|
||||
weight: 10
|
||||
---
|
||||
# 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,53 +0,0 @@
|
|||
---
|
||||
title: Launch Flags
|
||||
weight: 40
|
||||
---
|
||||
# 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,255 +0,0 @@
|
|||
---
|
||||
title: Query Expressions
|
||||
weight: 30
|
||||
---
|
||||
# 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,25 +0,0 @@
|
|||
---
|
||||
title: Settings
|
||||
weight: 50
|
||||
---
|
||||
# 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,4 +0,0 @@
|
|||
---
|
||||
bookFlatSection: true
|
||||
weight: 30
|
||||
---
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
---
|
||||
title: Customising Dynamo-Browse
|
||||
weight: 50
|
||||
---
|
||||
# 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. To keep customisations across relaunches, these commands
|
||||
can be added to an RC file.
|
||||
|
||||
## RC Files
|
||||
|
||||
RC files are text files containing commands that will be executed by Dynamo-Browse upon launch.
|
||||
By default, RC files are located in `$HOME/.config/dynamo-browse/` and have the suffix `.ucl`. For example:
|
||||
|
||||
```
|
||||
$HOME/.config/dynamo-browse/init.ucl
|
||||
```
|
||||
|
||||
Any number of RC files can be present in this directory, and they are executed in lexicographical order.
|
||||
|
||||
## 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.
|
||||
|
||||
## Extensions
|
||||
|
||||
The RC files are primarily intended for commands that customise Dynamo-Browse in a particular way, but any
|
||||
command can be entered here, including those for defining new commands or running predefined queries. These
|
||||
are generally known as extensions, although act as any other RC file located within that directory. More
|
||||
information on extensions can be found in the [reference](/docs/reference/script-api).
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
---
|
||||
title: Editing Items
|
||||
weight: 40
|
||||
---
|
||||
# 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,60 +0,0 @@
|
|||
---
|
||||
title: Querying and Filtering Results
|
||||
weight: 30
|
||||
---
|
||||
# Querying and Filtering 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.
|
||||
|
||||
Query expressions are entered as expressions, with most being of the form `attribute operator value`.
|
||||
For example, the expression for selecting records where `color` equals `red` is:
|
||||
|
||||
```
|
||||
color = "red"
|
||||
```
|
||||
|
||||
The `color` attribute can either be a partition key, sort key, or a regular attribute. Dynamo Browse
|
||||
will do it's best to run the query as a DynamoDB query if possible. Usually query expressions of the
|
||||
form `pk = <something>` or `pk = <something> and sk <comparison> <something>` where `pk` is a partition key,
|
||||
`sk` is the sort key, and `<comparison>` is one of the [equality](/docs/reference/query-expressions/#equality),
|
||||
[numerical comparison](/docs/reference/query-expressions/#numerical-comparison), or
|
||||
[prefix operator](/docs/reference/query-expressions/#prefix-operator), will be executed as queries.
|
||||
Other expressions will be executed as scans.
|
||||
|
||||
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,157 +0,0 @@
|
|||
---
|
||||
title: Getting Around
|
||||
weight: 20
|
||||
---
|
||||
# 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,66 +0,0 @@
|
|||
---
|
||||
title: Launching and Quitting
|
||||
weight: 10
|
||||
---
|
||||
# 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,30 +0,0 @@
|
|||
---
|
||||
weight: 20
|
||||
---
|
||||
|
||||
|
||||
|
||||
## 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,40 +0,0 @@
|
|||
module: async
|
||||
docs: |
|
||||
Provides commands for executing blocks asynchronously.
|
||||
|
||||
Asynchronous blocks are executed in the same thread as regular commands, so it's recommended
|
||||
to avoid long running operations in these blocks. Exception to these are commands which require fetching
|
||||
data from the database.
|
||||
symbols:
|
||||
- name: do
|
||||
syntax: async:do BLOCK
|
||||
docs: |
|
||||
Schedules a block to be executed at the conclusion of all other running commands.
|
||||
Blocks are place in a queue in the order the call to `async:do` is made, with a maximum
|
||||
queue size of 100 blocks. If the queue is full, the command will return an error.
|
||||
example: |
|
||||
async:do {
|
||||
echo "World"
|
||||
}
|
||||
echo "Hello"
|
||||
- name: in
|
||||
syntax: async:in SECONDS BLOCK
|
||||
docs: |
|
||||
Schedules a block to be executed in SECONDS seconds. Once the timout has ellapsed, the block will
|
||||
be placed on the queue and will be executed once all other pending blocks have been consumed.
|
||||
example: |
|
||||
async:in 5 {
|
||||
echo "5 seconds have ellapsed"
|
||||
}
|
||||
- name: query
|
||||
syntax: async:query EXPRESSION [QUERY_ARGS] BLOCK [OPTIONS]
|
||||
docs: |
|
||||
Executes the query in the background and schedules BLOCK once the resultset is available. The available
|
||||
options match that of rs:query.
|
||||
|
||||
Note: the RC files are invoked before the table picker is shown, so any async:query calls invoked during
|
||||
startup should have the `-table` option set.
|
||||
example: |
|
||||
async:query 'pk = $arg' [arg:"abc123"] { |rs|
|
||||
echo $rs.First.pk
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
module: av
|
||||
docs: |
|
||||
Provides commands for translating between UCL and DynamoDB types.
|
||||
symbols:
|
||||
- name: 'true'
|
||||
syntax: av:true
|
||||
docs: |
|
||||
Returns a true BOOL attribute value.
|
||||
- name: 'false'
|
||||
syntax: av:false
|
||||
docs: |
|
||||
Returns a false BOOL attribute value.
|
||||
- name: 'null'
|
||||
syntax: av:null
|
||||
docs: |
|
||||
Returns a NULL attribute value.
|
||||
- name: string-set
|
||||
syntax: av:string-set LIST
|
||||
docs: |
|
||||
Converts a list into a string set (SS) attribute value.
|
||||
example: |
|
||||
av:string-set [apple banana cherry]
|
||||
|
|
@ -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,9 +0,0 @@
|
|||
module: opt
|
||||
docs: |
|
||||
Provides commands for modifying options.
|
||||
symbols:
|
||||
- name: 'set'
|
||||
syntax: opt:set NAME [VALUE]
|
||||
docs: |
|
||||
Returns the current value of the setting option NAME. If VALUE is present, modifies the setting option first before
|
||||
returning the value.
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
module: pb
|
||||
docs: |
|
||||
Provides access to the pasteboard.
|
||||
symbols:
|
||||
- name: 'paste'
|
||||
syntax: pb:paste
|
||||
docs: |
|
||||
Returns the current text value of the paste board. If the paste board contains no text value,
|
||||
or is unavailable, returns the empty string.
|
||||
- name: 'copy'
|
||||
syntax: pb:copy VALUE
|
||||
docs: |
|
||||
Sets the current text value of the paste board to VALUE. If the paste board is unavailable, this
|
||||
command nops.
|
||||
|
|
@ -1,27 +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.
|
||||
|
||||
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: Items
|
||||
syntax: '$resultset.Items'
|
||||
docs: |
|
||||
Returns the items within the result set.
|
||||
- name: HasNextPage
|
||||
syntax: '$resultset.HasNextPage'
|
||||
docs: |
|
||||
Returns true if this result-set has another page. The next page can be retrieved by calling `rs:next-page $resultset`.
|
||||
- name: First
|
||||
syntax: '$resultset.First'
|
||||
docs: |
|
||||
Returns the first item of the result-set, or nil if the result set is empty. Shorthand for `$result.Items.(0)`.
|
||||
- name: Table
|
||||
syntax: '$resultset.Table'
|
||||
docs: |
|
||||
Returns information about the table this result set belongs to.
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
module: rs
|
||||
docs: |
|
||||
Provides operations over result-sets, or commands which return result-sets.
|
||||
symbols:
|
||||
- name: new
|
||||
syntax: rs:new [-table TABLE]
|
||||
docs: |
|
||||
Creates a new, empty result-set. If -table is specific, will configure the result-set table to
|
||||
that of TABLE. Otherwise, the result-set will inherit the current table.
|
||||
- name: query
|
||||
syntax: rs:query EXPRESSION [ARGUMENTS] [-table TABLE]
|
||||
docs: |
|
||||
Invokes a query expression and returns the result as a result-set. The query can be invoked with
|
||||
ARGUMENTS, which must be a dictionary with key/value pairs that are made available to the query
|
||||
as either attribute placeholders (`:attr`), or value placeholders (`$value`).
|
||||
|
||||
If -table is specific, will run the query over TABLE. Otherwise, the query will be invoked
|
||||
over the current table.
|
||||
|
||||
Note that this command blocks the command thread until either the results are available, or if
|
||||
an error occurs. To avoid this, use 'async:query', which will run the query in the background.
|
||||
example: |
|
||||
results = rs:query 'pk = $myID' [myID:"abc132"] -table users
|
||||
- name: scan
|
||||
syntax: rs:scan [-table TABLE]
|
||||
docs: |
|
||||
Runs a table scan, returning the first page of results as a result-set. If -table is specific, will
|
||||
run the scan over TABLE. Otherwise, the scan will be over the current table.
|
||||
- name: next-page
|
||||
syntax: rs:next-page RESULTSET
|
||||
docs: |
|
||||
Runs the next page of results from the passed in result-set. This will depend on how RESULTSET was
|
||||
created. For example, if RESULTSET was from a query, this will return the next page of results from that
|
||||
query. Likewise, for scans.
|
||||
|
||||
If the next page is available, the results will be returned as a new result-set, leaving the original result-set
|
||||
unmodified. If no next page is available, then nil will be returned.
|
||||
|
|
@ -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,17 +4,15 @@ import (
|
|||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"lmika.dev/cmd/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"
|
||||
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"
|
||||
|
|
@ -35,6 +33,8 @@ import (
|
|||
"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"
|
||||
bus "github.com/lmika/events"
|
||||
"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
|
||||
}
|
||||
|
|
@ -177,13 +167,11 @@ func main() {
|
|||
keyBindingController,
|
||||
pasteboardProvider,
|
||||
settingsController,
|
||||
itemRendererService,
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
|
|
|
|||
15
go.mod
|
|
@ -1,8 +1,8 @@
|
|||
module lmika.dev/cmd/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.11.1
|
||||
github.com/stretchr/testify v1.10.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.1.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
@ -50,12 +51,9 @@ require (
|
|||
github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-co-op/gocron/v2 v2.17.0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
|
|
@ -66,7 +64,6 @@ require (
|
|||
github.com/muesli/termenv v0.13.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.2 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
github.com/sahilm/fuzzy v0.1.0 // indirect
|
||||
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
|
||||
|
|
@ -80,5 +77,5 @@ 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
|
||||
lmika.dev/pkg/modash v0.0.0-20250619112300-0be0b6b35b1b // indirect
|
||||
)
|
||||
|
|
|
|||
38
go.sum
|
|
@ -75,8 +75,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-co-op/gocron/v2 v2.17.0 h1:e/oj6fcAM8vOOKZxv2Cgfmjo+s8AXC46po5ZPtaSea4=
|
||||
github.com/go-co-op/gocron/v2 v2.17.0/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
|
|
@ -86,16 +84,12 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
|
|||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc h1:ZQrgZFsLzkw7o3CoDzsfBhx0bf/1rVBXrLy8dXKRe8o=
|
||||
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc/go.mod h1:PyXUpnI3olx3bsPcHt98FGPX/KCFZ1Fi+hw1XLI6384=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
|
|
@ -111,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,18 +148,16 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
|
|||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
|
||||
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
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=
|
||||
|
|
@ -255,9 +249,21 @@ 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=
|
||||
lmika.dev/pkg/modash v0.0.0-20250216001243-c73e50a0913d h1:x5aMBOkCr4cjJyFmq+qJVUsByfffD9k56HYDx1yZSR4=
|
||||
lmika.dev/pkg/modash v0.0.0-20250216001243-c73e50a0913d/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI=
|
||||
lmika.dev/pkg/modash v0.0.0-20250619112300-0be0b6b35b1b h1:Oymcj66pgyJ2CtGk9lPh06P4FOekllE1iPehDwaL0vw=
|
||||
lmika.dev/pkg/modash v0.0.0-20250619112300-0be0b6b35b1b/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI=
|
||||
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=
|
||||
ucl.lmika.dev v0.0.0-20250527110948-e869e6c9bd4d h1:SlmmY92u7nvPW6xa66n2ZPfCOx90uNp1KkJZ1IDF6K0=
|
||||
ucl.lmika.dev v0.0.0-20250527110948-e869e6c9bd4d/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY=
|
||||
ucl.lmika.dev v0.0.0-20250527112110-03e6878524a1 h1:e++1/TfwVKdWi1TmO+kfCdO2+lCTKCrh1m4ps0p7UUM=
|
||||
ucl.lmika.dev v0.0.0-20250527112110-03e6878524a1/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY=
|
||||
ucl.lmika.dev v0.0.0-20250527114213-41b4fdb00382 h1:rDJtNrcKVmEqLep1l2YrodPjCfq+/yl7p8EZUrKW7Aw=
|
||||
ucl.lmika.dev v0.0.0-20250527114213-41b4fdb00382/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY=
|
||||
ucl.lmika.dev v0.0.0-20250528113931-3a88c0c777d8 h1:kC312X0SvM9YHtuS1r6Js+CgmSS+kSAMLj8cYFuI0+4=
|
||||
ucl.lmika.dev v0.0.0-20250528113931-3a88c0c777d8/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY=
|
||||
ucl.lmika.dev v0.0.0-20250718121358-7c76e61b08e4 h1:4HF6Av2/cOXBmRfHBthHn2iHJhk9GvHAFg6Tu6LVUTA=
|
||||
ucl.lmika.dev v0.0.0-20250718121358-7c76e61b08e4/go.mod h1:+HB5VAi0cI28mr3LbclJvv5lb/HclJ3R60x6cbjgt4c=
|
||||
ucl.lmika.dev v0.1.0 h1:gIZvLjruY1buIH25cm1hcIOvZ/+BvsZ+f84xrhcS6pY=
|
||||
ucl.lmika.dev v0.1.0/go.mod h1:+HB5VAi0cI28mr3LbclJvv5lb/HclJ3R60x6cbjgt4c=
|
||||
|
|
|
|||
102
goreleaser.yml
|
|
@ -1,102 +0,0 @@
|
|||
version: 2
|
||||
|
||||
builds:
|
||||
- id: dynamo-browse_macos
|
||||
targets:
|
||||
- darwin_amd64
|
||||
- darwin_arm64
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
main: ./cmd/dynamo-browse/.
|
||||
binary: dynamo-browse
|
||||
- id: dynamo-browse_linux
|
||||
targets:
|
||||
- linux_amd64
|
||||
- linux_arm64
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
main: ./cmd/dynamo-browse/.
|
||||
binary: dynamo-browse
|
||||
|
||||
|
||||
|
||||
# MacOS Release: notarise and publish via Homebrew Tap
|
||||
notarize:
|
||||
macos:
|
||||
- enabled: true
|
||||
ids:
|
||||
- dynamo-browse_macos
|
||||
sign:
|
||||
certificate: "{{.Env.MACOS_SIGN_P12}}"
|
||||
password: "{{.Env.MACOS_SIGN_PASSWORD}}"
|
||||
notarize:
|
||||
issuer_id: "{{.Env.MACOS_NOTARY_ISSUER_ID}}"
|
||||
key_id: "{{.Env.MACOS_NOTARY_KEY_ID}}"
|
||||
key: "{{.Env.MACOS_NOTARY_KEY}}"
|
||||
wait: true
|
||||
timeout: 20m
|
||||
|
||||
archives:
|
||||
- id: macos_tgz
|
||||
ids:
|
||||
- dynamo-browse_macos
|
||||
wrap_in_directory: false
|
||||
formats:
|
||||
- tar.gz
|
||||
- id: linux_tgz
|
||||
ids:
|
||||
- dynamo-browse_linux
|
||||
wrap_in_directory: false
|
||||
formats:
|
||||
- tar.gz
|
||||
|
||||
homebrew_casks:
|
||||
- name: dynamo-browse
|
||||
ids:
|
||||
- macos_tgz
|
||||
repository:
|
||||
owner: casks
|
||||
name: dynamo-browse
|
||||
git:
|
||||
url: 'forgejo@lmika.dev:casks/dynamo-browse.git'
|
||||
private_key: "{{ .Env.HOMEBREW_TAP_PRIVATE_KEY }}"
|
||||
directory: Casks
|
||||
homepage: https://dynamo-browse.lmika.dev/
|
||||
description: TUI tools for working with DynamoDB
|
||||
license: MIT
|
||||
|
||||
# Linux releases: publish as deb and rpm packages
|
||||
nfpms:
|
||||
- id: package_nfpms
|
||||
package_name: audax
|
||||
ids:
|
||||
- dynamo-browse_linux
|
||||
vendor: lmika
|
||||
homepage: https://dynamo-browse.lmika.dev/
|
||||
maintainer: Leon Mika <lmika@lmika.org>
|
||||
description: TUI tools for working with DynamoDB
|
||||
license: MIT
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
bindir: /usr/local/bin
|
||||
|
||||
release:
|
||||
gitea:
|
||||
owner: cmd
|
||||
name: dynamo-browse
|
||||
ids:
|
||||
- macos_tgz
|
||||
- linux_tgz
|
||||
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
|
||||
snapshot:
|
||||
version_template: "{{ .Tag }}-next"
|
||||
|
||||
gitea_urls:
|
||||
api: https://lmika.dev/api/v1
|
||||
download: https://lmika.dev
|
||||
# set to true if you use a self-signed certificate
|
||||
skip_tls_verify: false
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
package cmdpacks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
"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/services/tables"
|
||||
"ucl.lmika.dev/ucl"
|
||||
)
|
||||
|
||||
type asyncModule struct {
|
||||
tableService *tables.Service
|
||||
state *controllers.State
|
||||
}
|
||||
|
||||
func (m asyncModule) asyncDo(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||
var block ucl.Invokable
|
||||
if err := args.Bind(&block); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, commandctrl.ScheduleTask(ctx, func(ctx context.Context) error {
|
||||
_, err := block.Invoke(ctx)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (m asyncModule) asyncIn(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||
var (
|
||||
duration int
|
||||
block ucl.Invokable
|
||||
)
|
||||
if err := args.Bind(&duration, &block); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err := commandctrl.CronScheduler(ctx).NewJob(
|
||||
gocron.OneTimeJob(
|
||||
gocron.OneTimeJobStartDateTime(time.Now().Add(time.Duration(duration)*time.Second)),
|
||||
),
|
||||
gocron.NewTask(func(ctx context.Context) {
|
||||
commandctrl.ScheduleTask(ctx, func(ctx context.Context) error {
|
||||
_, err := block.Invoke(ctx)
|
||||
return err
|
||||
})
|
||||
}),
|
||||
gocron.WithContext(ctx),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (m asyncModule) asyncQuery(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||
var (
|
||||
block ucl.Invokable
|
||||
)
|
||||
|
||||
args, q, tableInfo, err := parseQuery(ctx, args, m.state.ResultSet(), m.tableService, 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := args.Bind(&block); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, commandctrl.ScheduleAuxTask(ctx, "query: "+q.String(), func(ctx context.Context) error {
|
||||
newResultSet, err := m.tableService.ScanOrQuery(context.Background(), tableInfo, q, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return commandctrl.ScheduleTask(ctx, func(ctx context.Context) error {
|
||||
_, err := block.Invoke(ctx, newResultSetProxy(newResultSet))
|
||||
return err
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func moduleAsync(tableService *tables.Service, state *controllers.State) ucl.Module {
|
||||
m := asyncModule{
|
||||
state: state,
|
||||
tableService: tableService,
|
||||
}
|
||||
|
||||
return ucl.Module{
|
||||
Name: "async",
|
||||
Builtins: map[string]ucl.BuiltinHandler{
|
||||
"do": m.asyncDo,
|
||||
"in": m.asyncIn,
|
||||
"query": m.asyncQuery,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ package cmdpacks
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"ucl.lmika.dev/ucl"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package cmdpacks
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/controllers"
|
||||
"ucl.lmika.dev/ucl"
|
||||
|
|
@ -12,7 +11,7 @@ type optModule struct {
|
|||
settingsController *controllers.SettingsController
|
||||
}
|
||||
|
||||
func (m optModule) optSet(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||
func (m optModule) pbSet(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||
var (
|
||||
name string
|
||||
newVale string
|
||||
|
|
@ -42,7 +41,7 @@ func moduleOpt(
|
|||
return ucl.Module{
|
||||
Name: "opt",
|
||||
Builtins: map[string]ucl.BuiltinHandler{
|
||||
"set": m.optSet,
|
||||
"set": m.pbSet,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ package cmdpacks
|
|||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/pkg/errors"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl"
|
||||
|
|
@ -11,6 +9,7 @@ import (
|
|||
"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"
|
||||
)
|
||||
|
|
@ -72,22 +71,21 @@ func parseQuery(
|
|||
args ucl.CallArgs,
|
||||
currentRS *models.ResultSet,
|
||||
tablesService *tables.Service,
|
||||
extraArgs int,
|
||||
) (ucl.CallArgs, *queryexpr.QueryExpr, *models.TableInfo, error) {
|
||||
) (*queryexpr.QueryExpr, *models.TableInfo, error) {
|
||||
var expr string
|
||||
if err := args.Bind(&expr); err != nil {
|
||||
return args, nil, nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
q, err := queryexpr.Parse(expr)
|
||||
if err != nil {
|
||||
return args, nil, nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if args.NArgs() > extraArgs {
|
||||
if args.NArgs() > 0 {
|
||||
var queryArgs ucl.Hashable
|
||||
if err := args.Bind(&queryArgs); err != nil {
|
||||
return args, nil, nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
queryNames := map[string]string{}
|
||||
|
|
@ -99,15 +97,12 @@ func parseQuery(
|
|||
|
||||
queryNames[k] = v.String()
|
||||
|
||||
switch t := v.(type) {
|
||||
switch v.(type) {
|
||||
case ucl.StringObject:
|
||||
queryValues[k] = &types.AttributeValueMemberS{Value: v.String()}
|
||||
case ucl.IntObject:
|
||||
queryValues[k] = &types.AttributeValueMemberN{Value: v.String()}
|
||||
case ucl.BoolObject:
|
||||
queryValues[k] = &types.AttributeValueMemberBOOL{Value: t.Truthy()}
|
||||
case attributeValueProxy:
|
||||
queryValues[k] = t.value
|
||||
// TODO: other types
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
|
@ -119,24 +114,24 @@ func parseQuery(
|
|||
if args.HasSwitch("table") {
|
||||
var tblName string
|
||||
if err := args.BindSwitch("table", &tblName); err != nil {
|
||||
return args, nil, nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
tableInfo, err = tablesService.Describe(ctx, tblName)
|
||||
if err != nil {
|
||||
return args, nil, nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
} else if currentRS != nil && currentRS.TableInfo != nil {
|
||||
tableInfo = currentRS.TableInfo
|
||||
} else {
|
||||
return args, nil, nil, errors.New("no table specified")
|
||||
return nil, nil, errors.New("no table specified")
|
||||
}
|
||||
|
||||
return args, q, tableInfo, nil
|
||||
return q, tableInfo, nil
|
||||
}
|
||||
|
||||
func (rs *rsModule) rsQuery(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||
_, q, tableInfo, err := parseQuery(ctx, args, rs.state.ResultSet(), rs.tableService, 0)
|
||||
q, tableInfo, err := parseQuery(ctx, args, rs.state.ResultSet(), rs.tableService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -187,7 +182,7 @@ func (rs *rsModule) rsScan(ctx context.Context, args ucl.CallArgs) (_ any, err e
|
|||
return newResultSetProxy(newResultSet), nil
|
||||
}
|
||||
|
||||
func (rs *rsModule) rsHide(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||
func (rs *rsModule) rsFilter(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||
var (
|
||||
rsProxy SimpleProxy[*models.ResultSet]
|
||||
filter string
|
||||
|
|
@ -307,7 +302,7 @@ func moduleRS(tableService *tables.Service, state *controllers.State) ucl.Module
|
|||
"new": m.rsNew,
|
||||
"query": m.rsQuery,
|
||||
"scan": m.rsScan,
|
||||
"hide": m.rsHide,
|
||||
"filter": m.rsFilter,
|
||||
"next-page": m.rsNextPage,
|
||||
"union": m.rsUnion,
|
||||
"set": m.rsSet,
|
||||
|
|
|
|||
|
|
@ -154,40 +154,6 @@ func TestModRS_Query(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestModRS_Hide(t *testing.T) {
|
||||
tests := []struct {
|
||||
descr string
|
||||
cmd string
|
||||
}{
|
||||
{
|
||||
descr: "returns hidden items 1",
|
||||
cmd: `
|
||||
rs = rs:scan -table service-test-data
|
||||
rs = rs:hide $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
|
||||
|
|
@ -214,14 +180,6 @@ func TestModRS_First(t *testing.T) {
|
|||
rs = rs:query 'pk="zzz"' -table service-test-data
|
||||
assert (eq $rs.First ()) "expected First to be nil"
|
||||
`,
|
||||
}, {
|
||||
descr: "returns the first item using placeholders",
|
||||
cmd: `
|
||||
rs = rs:query 'pk=$v and sk=$u' [v:"abc" u:"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"
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
|
|
|||
|
|
@ -2,14 +2,10 @@ package cmdpacks
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
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/models"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/itemrenderer"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables"
|
||||
"ucl.lmika.dev/ucl"
|
||||
)
|
||||
|
|
@ -19,7 +15,6 @@ type uiModule struct {
|
|||
state *controllers.State
|
||||
ckb *customKeyBinding
|
||||
readController *controllers.TableReadController
|
||||
itemRenderer *itemrenderer.Service
|
||||
}
|
||||
|
||||
func (m *uiModule) uiCommand(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||
|
|
@ -43,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()
|
||||
}
|
||||
|
|
@ -90,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 {
|
||||
|
|
@ -175,7 +128,7 @@ func (m *uiModule) uiBind(ctx context.Context, args ucl.CallArgs) (any, error) {
|
|||
}
|
||||
|
||||
func (m *uiModule) uiQuery(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||
_, q, tableInfo, err := parseQuery(ctx, args, m.state.ResultSet(), m.tableService, 0)
|
||||
q, tableInfo, err := parseQuery(ctx, args, m.state.ResultSet(), m.tableService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -195,36 +148,15 @@ func (m *uiModule) uiFilter(ctx context.Context, args ucl.CallArgs) (any, error)
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *uiModule) uiSetItemAnnotator(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||
var inv ucl.Invokable
|
||||
if err := args.Bind(&inv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.itemRenderer.SetAnnotation(itemrenderer.AnnotationFunc(func(rs *models.ResultSet, item models.Item, path models.AttrPathNode) string {
|
||||
v, err := inv.Invoke(ctx, newResultSetProxy(rs), itemProxy{rs, 0, item}, attrPathProxy{attrPath: &path})
|
||||
if err != nil {
|
||||
return ""
|
||||
} else if v == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprint(v)
|
||||
}))
|
||||
commandctrl.QueueRefresh(ctx)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func moduleUI(
|
||||
tableService *tables.Service,
|
||||
state *controllers.State,
|
||||
readController *controllers.TableReadController,
|
||||
itemRenderer *itemrenderer.Service,
|
||||
) (ucl.Module, controllers.CustomKeyBindingSource) {
|
||||
m := &uiModule{
|
||||
tableService: tableService,
|
||||
state: state,
|
||||
readController: readController,
|
||||
itemRenderer: itemRenderer,
|
||||
ckb: &customKeyBinding{
|
||||
bindings: map[string]tea.Cmd{},
|
||||
keyBindings: map[string]string{},
|
||||
|
|
@ -234,15 +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,
|
||||
"set-item-annotator": m.uiSetItemAnnotator,
|
||||
"command": m.uiCommand,
|
||||
"prompt": m.uiPrompt,
|
||||
"prompt-table": m.uiPromptTable,
|
||||
"confirm": m.uiConfirm,
|
||||
"query": m.uiQuery,
|
||||
"filter": m.uiFilter,
|
||||
"bind": m.uiBind,
|
||||
},
|
||||
}, m.ckb
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,31 +174,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,49 +209,6 @@ func (tp itemProxy) Each(fn func(k string, v ucl.Object) error) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
type attrPathProxy struct {
|
||||
attrPath *models.AttrPathNode
|
||||
}
|
||||
|
||||
func (ap attrPathProxy) String() string {
|
||||
return "RSItem()"
|
||||
}
|
||||
|
||||
func (ap attrPathProxy) Truthy() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ap attrPathProxy) Len() (l int) {
|
||||
for p := ap.attrPath; p != nil; p = p.Parent {
|
||||
l++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ap attrPathProxy) Index(k int) ucl.Object {
|
||||
if k == -1 {
|
||||
return ucl.StringObject(ap.attrPath.Key)
|
||||
}
|
||||
|
||||
if k >= 0 {
|
||||
k = ap.Len() - k - 1
|
||||
} else {
|
||||
k = -k - 1
|
||||
}
|
||||
|
||||
if k < 0 {
|
||||
return nil
|
||||
}
|
||||
for p := ap.attrPath; p != nil; p = p.Parent {
|
||||
if k <= 0 {
|
||||
return ucl.StringObject(p.Key)
|
||||
}
|
||||
k -= 1
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type attributeValueProxy struct {
|
||||
value types.AttributeValue
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,101 +0,0 @@
|
|||
package cmdpacks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models"
|
||||
)
|
||||
|
||||
func TestAttrPathProxy_Index(t *testing.T) {
|
||||
tests := []struct {
|
||||
descr string
|
||||
attrPath models.AttrPathNode
|
||||
index int
|
||||
want string
|
||||
wantNil bool
|
||||
}{
|
||||
{
|
||||
descr: "return leaf 1",
|
||||
attrPath: models.AttrPathNode{Key: "leaf"},
|
||||
index: -1,
|
||||
want: "leaf",
|
||||
},
|
||||
{
|
||||
descr: "return leaf 2",
|
||||
attrPath: models.AttrPathNode{Key: "leaf", Parent: &models.AttrPathNode{Key: "parent"}},
|
||||
index: -1,
|
||||
want: "leaf",
|
||||
},
|
||||
{
|
||||
descr: "return parent 1",
|
||||
attrPath: models.AttrPathNode{Key: "leaf", Parent: &models.AttrPathNode{Key: "parent"}},
|
||||
index: -2,
|
||||
want: "parent",
|
||||
},
|
||||
{
|
||||
descr: "return parent 1",
|
||||
attrPath: models.AttrPathNode{Key: "leaf", Parent: &models.AttrPathNode{Key: "parent", Parent: &models.AttrPathNode{Key: "grandparent"}}},
|
||||
index: -2,
|
||||
want: "parent",
|
||||
},
|
||||
{
|
||||
descr: "return parent 3",
|
||||
attrPath: models.AttrPathNode{Key: "leaf", Parent: &models.AttrPathNode{Key: "parent", Parent: &models.AttrPathNode{Key: "grandparent"}}},
|
||||
index: -3,
|
||||
want: "grandparent",
|
||||
},
|
||||
{
|
||||
descr: "return root 1",
|
||||
attrPath: models.AttrPathNode{Key: "leaf", Parent: &models.AttrPathNode{Key: "parent", Parent: &models.AttrPathNode{Key: "grandparent"}}},
|
||||
index: 0,
|
||||
want: "grandparent",
|
||||
},
|
||||
{
|
||||
descr: "return root 2",
|
||||
attrPath: models.AttrPathNode{Key: "leaf", Parent: &models.AttrPathNode{Key: "parent"}},
|
||||
index: 0,
|
||||
want: "parent",
|
||||
},
|
||||
{
|
||||
descr: "return root 3",
|
||||
attrPath: models.AttrPathNode{Key: "leaf"},
|
||||
index: 0,
|
||||
want: "leaf",
|
||||
},
|
||||
{
|
||||
descr: "return first child 1",
|
||||
attrPath: models.AttrPathNode{Key: "leaf", Parent: &models.AttrPathNode{Key: "parent", Parent: &models.AttrPathNode{Key: "grandparent"}}},
|
||||
index: 1,
|
||||
want: "parent",
|
||||
},
|
||||
{
|
||||
descr: "return first child 2",
|
||||
attrPath: models.AttrPathNode{Key: "leaf", Parent: &models.AttrPathNode{Key: "parent"}},
|
||||
index: 1,
|
||||
want: "leaf",
|
||||
},
|
||||
{
|
||||
descr: "return nil 1",
|
||||
attrPath: models.AttrPathNode{Key: "leaf", Parent: &models.AttrPathNode{Key: "parent", Parent: &models.AttrPathNode{Key: "grandparent"}}},
|
||||
index: -5,
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
descr: "return nil 2",
|
||||
attrPath: models.AttrPathNode{Key: "leaf", Parent: &models.AttrPathNode{Key: "parent"}},
|
||||
index: 56,
|
||||
wantNil: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.descr, func(t *testing.T) {
|
||||
proxy := attrPathProxy{&tt.attrPath}
|
||||
if tt.wantNil {
|
||||
assert.Nil(t, proxy.Index(tt.index))
|
||||
} else {
|
||||
assert.Equal(t, tt.want, proxy.Index(tt.index).String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -2,53 +2,20 @@ package cmdpacks
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
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,12 @@ package cmdpacks
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"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/itemrenderer"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables"
|
||||
"ucl.lmika.dev/repl"
|
||||
"ucl.lmika.dev/ucl"
|
||||
|
|
@ -24,7 +22,6 @@ type StandardCommands struct {
|
|||
KeyBindingController *controllers.KeyBindingController
|
||||
PBProvider services.PasteboardProvider
|
||||
SettingsController *controllers.SettingsController
|
||||
ItemRenderer *itemrenderer.Service
|
||||
|
||||
modUI ucl.Module
|
||||
}
|
||||
|
|
@ -38,9 +35,8 @@ func NewStandardCommands(
|
|||
keyBindingController *controllers.KeyBindingController,
|
||||
pbProvider services.PasteboardProvider,
|
||||
settingsController *controllers.SettingsController,
|
||||
itemRenderer *itemrenderer.Service,
|
||||
) StandardCommands {
|
||||
modUI, ckbs := moduleUI(tableService, state, readController, itemRenderer)
|
||||
modUI, ckbs := moduleUI(tableService, state, readController)
|
||||
keyBindingController.SetCustomKeyBindingSource(ckbs)
|
||||
|
||||
return StandardCommands{
|
||||
|
|
@ -52,7 +48,6 @@ func NewStandardCommands(
|
|||
KeyBindingController: keyBindingController,
|
||||
PBProvider: pbProvider,
|
||||
SettingsController: settingsController,
|
||||
ItemRenderer: itemRenderer,
|
||||
modUI: modUI,
|
||||
}
|
||||
}
|
||||
|
|
@ -404,7 +399,6 @@ func (sc StandardCommands) InstOptions() []ucl.InstOption {
|
|||
ucl.WithModule(modulePB(sc.PBProvider)),
|
||||
ucl.WithModule(moduleOpt(sc.SettingsController)),
|
||||
ucl.WithModule(moduleAttrValue()),
|
||||
ucl.WithModule(moduleAsync(sc.TableService, sc.State)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -432,9 +426,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 {
|
||||
|
|
@ -445,13 +438,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
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ package cmdpacks_test
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
bus "github.com/lmika/events"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -23,6 +21,7 @@ import (
|
|||
"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"
|
||||
)
|
||||
|
||||
func TestStdCmds_Mark(t *testing.T) {
|
||||
|
|
@ -163,7 +162,6 @@ func newService(t *testing.T, opts ...serviceOpt) *services {
|
|||
keyBindingController,
|
||||
testPB,
|
||||
settingsController,
|
||||
itemRendererService,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/pkg/errors"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
"github.com/pkg/errors"
|
||||
"ucl.lmika.dev/ucl"
|
||||
"ucl.lmika.dev/ucl/builtins"
|
||||
|
||||
|
|
@ -19,22 +17,12 @@ import (
|
|||
"lmika.dev/cmd/dynamo-browse/internal/common/ui/events"
|
||||
)
|
||||
|
||||
const (
|
||||
commandsCategory = "commands"
|
||||
pendingTaskBuffer = 100
|
||||
pendingAuxTaskBuffer = 100
|
||||
auxWorkers = 4
|
||||
)
|
||||
const commandsCategory = "commands"
|
||||
|
||||
type cmdMessage struct {
|
||||
cmd string
|
||||
}
|
||||
|
||||
type pendingTask struct {
|
||||
descr string
|
||||
task func(ctx context.Context) error
|
||||
}
|
||||
|
||||
type CommandController struct {
|
||||
uclInst *ucl.Inst
|
||||
historyProvider IterProvider
|
||||
|
|
@ -42,29 +30,19 @@ type CommandController struct {
|
|||
lookupExtensions []CommandLookupExtension
|
||||
completionProvider CommandCompletionProvider
|
||||
uiStateProvider UIStateProvider
|
||||
cronScheduler gocron.Scheduler
|
||||
cmdChan chan cmdMessage
|
||||
pendingTaskChan chan pendingTask
|
||||
pendingAuxTaskChan chan pendingTask
|
||||
msgChan chan tea.Msg
|
||||
interactive bool
|
||||
}
|
||||
|
||||
func NewCommandController(historyProvider IterProvider, pkgs ...CommandPack) (*CommandController, error) {
|
||||
sched, err := gocron.NewScheduler()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cc := &CommandController{
|
||||
historyProvider: historyProvider,
|
||||
commandList: nil,
|
||||
lookupExtensions: nil,
|
||||
cronScheduler: sched,
|
||||
cmdChan: make(chan cmdMessage),
|
||||
pendingTaskChan: make(chan pendingTask, pendingTaskBuffer),
|
||||
pendingAuxTaskChan: make(chan pendingTask, pendingAuxTaskBuffer),
|
||||
msgChan: make(chan tea.Msg),
|
||||
interactive: true,
|
||||
historyProvider: historyProvider,
|
||||
commandList: nil,
|
||||
lookupExtensions: nil,
|
||||
cmdChan: make(chan cmdMessage),
|
||||
msgChan: make(chan tea.Msg),
|
||||
interactive: true,
|
||||
}
|
||||
|
||||
options := []ucl.InstOption{
|
||||
|
|
@ -97,8 +75,6 @@ func NewCommandController(historyProvider IterProvider, pkgs ...CommandPack) (*C
|
|||
}
|
||||
|
||||
go cc.cmdLooper()
|
||||
go cc.auxCmdLooper()
|
||||
sched.Start()
|
||||
|
||||
return cc, nil
|
||||
}
|
||||
|
|
@ -196,13 +172,12 @@ func (c *CommandController) Invoke(invokable ucl.Invokable, args []any) (msg tea
|
|||
}
|
||||
|
||||
func (c *CommandController) cmdLooper() {
|
||||
ctx := context.Background()
|
||||
execCtx := execContext{ctrl: c}
|
||||
ctx := context.WithValue(context.Background(), commandCtlKey, &execCtx)
|
||||
|
||||
for {
|
||||
select {
|
||||
case cmdChan := <-c.cmdChan:
|
||||
execCtx := execContext{ctrl: c}
|
||||
ctx := context.WithValue(ctx, commandCtlKey, &execCtx)
|
||||
|
||||
res, err := c.ExecuteAndWait(ctx, cmdChan.cmd)
|
||||
if err != nil {
|
||||
c.postMessage(events.Error(err))
|
||||
|
|
@ -212,16 +187,6 @@ func (c *CommandController) cmdLooper() {
|
|||
if execCtx.requestRefresh {
|
||||
c.postMessage(events.ResultSetUpdated{})
|
||||
}
|
||||
case task := <-c.pendingTaskChan:
|
||||
execCtx := execContext{ctrl: c}
|
||||
ctx := context.WithValue(ctx, commandCtlKey, &execCtx)
|
||||
|
||||
if err := task.task(ctx); err != nil {
|
||||
c.postMessage(events.Error(err))
|
||||
}
|
||||
if execCtx.requestRefresh {
|
||||
c.postMessage(events.ResultSetUpdated{})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -340,13 +305,15 @@ func (c *CommandController) cmdInvoker(ctx context.Context, name string, args uc
|
|||
}
|
||||
|
||||
func (c *CommandController) printLine(s string) {
|
||||
log.Println(s)
|
||||
if c.msgChan == nil || !c.interactive {
|
||||
log.Println(s)
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case c.msgChan <- events.StatusMsg(s):
|
||||
default:
|
||||
log.Println(s)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -358,21 +325,6 @@ func (c *CommandController) postMessage(msg tea.Msg) {
|
|||
c.msgChan <- msg
|
||||
}
|
||||
|
||||
func (c *CommandController) auxCmdLooper() {
|
||||
ctx := context.WithValue(context.Background(), commandCtlKey, &execContext{ctrl: c})
|
||||
|
||||
for i := 0; i < auxWorkers; i++ {
|
||||
go func() {
|
||||
for auxTask := range c.pendingAuxTaskChan {
|
||||
log.Printf("running aux task: %v", auxTask.descr)
|
||||
if err := auxTask.task(ctx); err != nil {
|
||||
log.Printf("aux task error: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
type teaMsgWrapper struct {
|
||||
msg tea.Msg
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,7 @@ package commandctrl
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
"github.com/pkg/errors"
|
||||
"ucl.lmika.dev/ucl"
|
||||
)
|
||||
|
||||
|
|
@ -60,40 +57,6 @@ func QueueRefresh(ctx context.Context) {
|
|||
cmdCtl.requestRefresh = true
|
||||
}
|
||||
|
||||
func CronScheduler(ctx context.Context) gocron.Scheduler {
|
||||
cmdCtl, ok := ctx.Value(commandCtlKey).(*execContext)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return cmdCtl.ctrl.cronScheduler
|
||||
}
|
||||
|
||||
func ScheduleTask(ctx context.Context, task func(ctx context.Context) error) error {
|
||||
cmdCtl, ok := ctx.Value(commandCtlKey).(*execContext)
|
||||
if !ok {
|
||||
return errors.New("no command controller")
|
||||
}
|
||||
select {
|
||||
case cmdCtl.ctrl.pendingTaskChan <- pendingTask{task: task}:
|
||||
return nil
|
||||
default:
|
||||
return errors.New("task queue is full")
|
||||
}
|
||||
}
|
||||
|
||||
func ScheduleAuxTask(ctx context.Context, descr string, task func(ctx context.Context) error) error {
|
||||
cmdCtl, ok := ctx.Value(commandCtlKey).(*execContext)
|
||||
if !ok {
|
||||
return errors.New("no command controller")
|
||||
}
|
||||
select {
|
||||
case cmdCtl.ctrl.pendingAuxTaskChan <- pendingTask{descr: descr, task: task}:
|
||||
return nil
|
||||
default:
|
||||
return errors.New("aux task queue is full")
|
||||
}
|
||||
}
|
||||
|
||||
type Invoker interface {
|
||||
Invoke(invokable ucl.Invokable, args []any) tea.Msg
|
||||
Inst() *ucl.Inst
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
package events
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"lmika.dev/cmd/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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ import (
|
|||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
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"
|
||||
|
|
@ -22,6 +20,8 @@ import (
|
|||
"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"
|
||||
bus "github.com/lmika/events"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type resultSetUpdateOp int
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
package models
|
||||
|
||||
type AttrPathNode struct {
|
||||
Key string
|
||||
Parent *AttrPathNode
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
@ -10,8 +10,8 @@ import (
|
|||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/queryexpr"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestModExpr_Query(t *testing.T) {
|
||||
|
|
@ -502,9 +502,6 @@ func TestQueryExpr_EvalItem(t *testing.T) {
|
|||
{expr: `alpha^="al"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
|
||||
{expr: `alpha="foobar"`, expected: &types.AttributeValueMemberBOOL{Value: false}},
|
||||
{expr: `alpha^="need-something"`, expected: &types.AttributeValueMemberBOOL{Value: false}},
|
||||
{expr: `""=""`, expected: &types.AttributeValueMemberBOOL{Value: true}},
|
||||
{expr: `"abc"="abc"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
|
||||
{expr: `""="abc"`, expected: &types.AttributeValueMemberBOOL{Value: false}},
|
||||
|
||||
// Comparison
|
||||
{expr: "three > 4", expected: &types.AttributeValueMemberBOOL{Value: false}},
|
||||
|
|
|
|||
|
|
@ -2,30 +2,18 @@ package itemrenderer
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"text/tabwriter"
|
||||
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/itemrender"
|
||||
"io"
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
annotation Annotation
|
||||
styles styleRenderer
|
||||
styles styleRenderer
|
||||
}
|
||||
|
||||
func NewService(
|
||||
fileTypeStyle StyleRenderer,
|
||||
metaInfoStyle StyleRenderer,
|
||||
) *Service {
|
||||
if fileTypeStyle == nil {
|
||||
fileTypeStyle = plainTextStyleRenderer{}
|
||||
}
|
||||
if metaInfoStyle == nil {
|
||||
metaInfoStyle = plainTextStyleRenderer{}
|
||||
}
|
||||
func NewService(fileTypeStyle StyleRenderer, metaInfoStyle StyleRenderer) *Service {
|
||||
return &Service{
|
||||
annotation: nil,
|
||||
styles: styleRenderer{
|
||||
fileTypeRenderer: fileTypeStyle,
|
||||
metaInfoRenderer: metaInfoStyle,
|
||||
|
|
@ -33,10 +21,6 @@ func NewService(
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Service) SetAnnotation(a Annotation) {
|
||||
s.annotation = a
|
||||
}
|
||||
|
||||
func (s *Service) RenderItem(w io.Writer, item models.Item, resultSet *models.ResultSet, plainText bool) {
|
||||
styles := s.styles
|
||||
if plainText {
|
||||
|
|
@ -49,47 +33,25 @@ func (s *Service) RenderItem(w io.Writer, item models.Item, resultSet *models.Re
|
|||
for _, colName := range resultSet.Columns() {
|
||||
seenColumns[colName] = struct{}{}
|
||||
if r := itemrender.ToRenderer(item[colName]); r != nil {
|
||||
p := models.AttrPathNode{Key: colName}
|
||||
s.renderItem(tabWriter, resultSet, item, p, "", r, styles)
|
||||
s.renderItem(tabWriter, "", colName, r, styles)
|
||||
}
|
||||
}
|
||||
for k, _ := range item {
|
||||
if _, seen := seenColumns[k]; !seen {
|
||||
if r := itemrender.ToRenderer(item[k]); r != nil {
|
||||
p := models.AttrPathNode{Key: k}
|
||||
s.renderItem(tabWriter, resultSet, item, p, "", r, styles)
|
||||
s.renderItem(tabWriter, "", k, r, styles)
|
||||
}
|
||||
}
|
||||
}
|
||||
tabWriter.Flush()
|
||||
}
|
||||
|
||||
func (m *Service) renderItem(
|
||||
w io.Writer,
|
||||
resultSet *models.ResultSet,
|
||||
item models.Item,
|
||||
path models.AttrPathNode,
|
||||
prefix string,
|
||||
r itemrender.Renderer,
|
||||
sr styleRenderer,
|
||||
) {
|
||||
fmt.Fprint(w, prefix)
|
||||
fmt.Fprint(w, path.Key)
|
||||
fmt.Fprint(w, "\t")
|
||||
fmt.Fprint(w, sr.fileTypeRenderer.Render(r.TypeName()))
|
||||
fmt.Fprint(w, "\t")
|
||||
fmt.Fprint(w, r.StringValue())
|
||||
fmt.Fprint(w, sr.metaInfoRenderer.Render(r.MetaInfo()))
|
||||
if m.annotation != nil {
|
||||
fmt.Fprint(w, " ")
|
||||
fmt.Fprint(w, sr.metaInfoRenderer.Render(m.annotation.AnnotateAttribute(resultSet, item, path)))
|
||||
}
|
||||
fmt.Fprint(w, "\n")
|
||||
|
||||
func (m *Service) renderItem(w io.Writer, prefix string, name string, r itemrender.Renderer, sr styleRenderer) {
|
||||
fmt.Fprintf(w, "%s%v\t%s\t%s%s\n",
|
||||
prefix, name, sr.fileTypeRenderer.Render(r.TypeName()), r.StringValue(), sr.metaInfoRenderer.Render(r.MetaInfo()))
|
||||
if subitems := r.SubItems(); len(subitems) > 0 {
|
||||
for _, si := range subitems {
|
||||
p := models.AttrPathNode{Key: si.Key, Parent: &path}
|
||||
m.renderItem(w, resultSet, item, p, prefix+" ", si.Value, sr)
|
||||
m.renderItem(w, prefix+" ", si.Key, si.Value, sr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -98,13 +60,3 @@ type styleRenderer struct {
|
|||
fileTypeRenderer StyleRenderer
|
||||
metaInfoRenderer StyleRenderer
|
||||
}
|
||||
|
||||
type Annotation interface {
|
||||
AnnotateAttribute(rs *models.ResultSet, item models.Item, path models.AttrPathNode) string
|
||||
}
|
||||
|
||||
type AnnotationFunc func(rs *models.ResultSet, item models.Item, path models.AttrPathNode) string
|
||||
|
||||
func (af AnnotationFunc) AnnotateAttribute(rs *models.ResultSet, item models.Item, path models.AttrPathNode) string {
|
||||
return af(rs, item, path)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ func Default() *KeyBindings {
|
|||
},
|
||||
View: &ViewKeyBindings{
|
||||
Mark: key.NewBinding(key.WithKeys("m"), key.WithHelp("m", "mark")),
|
||||
ToggleMarkedItems: key.NewBinding(key.WithKeys("M"), key.WithHelp("M", "toggle marged items")),
|
||||
CopyItemToClipboard: key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "copy item to clipboard")),
|
||||
CopyTableToClipboard: key.NewBinding(key.WithKeys("C"), key.WithHelp("C", "copy table to clipboard")),
|
||||
Rescan: key.NewBinding(key.WithKeys("R"), key.WithHelp("R", "rescan")),
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ type TableKeyBinding struct {
|
|||
|
||||
type ViewKeyBindings struct {
|
||||
Mark key.Binding `keymap:"mark"`
|
||||
ToggleMarkedItems key.Binding `keymap:"toggle-marked-items"`
|
||||
CopyItemToClipboard key.Binding `keymap:"copy-item-to-clipboard"`
|
||||
CopyTableToClipboard key.Binding `keymap:"copy-table-to-clipboard"`
|
||||
Rescan key.Binding `keymap:"rescan"`
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
bus "github.com/lmika/events"
|
||||
"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"
|
||||
|
|
@ -23,6 +20,8 @@ import (
|
|||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/tableselect"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils"
|
||||
bus "github.com/lmika/events"
|
||||
"log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -126,6 +125,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
if idx := m.tableView.SelectedItemIndex(); idx >= 0 {
|
||||
return m, events.SetTeaMessage(m.tableWriteController.ToggleMark(idx))
|
||||
}
|
||||
case key.Matches(msg, m.keyMap.ToggleMarkedItems):
|
||||
return m, events.SetTeaMessage(m.tableReadController.Mark(controllers.MarkOpToggle, ""))
|
||||
case key.Matches(msg, m.keyMap.CopyItemToClipboard):
|
||||
if idx := m.tableView.SelectedItemIndex(); idx >= 0 {
|
||||
return m, events.SetTeaMessage(m.tableReadController.CopyItemToClipboard(idx))
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package dynamoitemview
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
|
@ -11,6 +9,7 @@ import (
|
|||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/frame"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package statusandprompt
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
|
@ -11,6 +9,7 @@ import (
|
|||
"lmika.dev/cmd/dynamo-browse/internal/common/ui/events"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StatusAndPrompt is a resizing model which displays a submodel and a status bar. When the start prompt
|
||||
|
|
@ -25,7 +24,6 @@ type StatusAndPrompt struct {
|
|||
spinner spinner.Model
|
||||
spinnerVisible bool
|
||||
pendingInput *pendingInputState
|
||||
pendingKeyState *pendingKeyState
|
||||
textInput textinput.Model
|
||||
width, height int
|
||||
lastModeLineHeight int
|
||||
|
|
@ -87,7 +85,7 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
}
|
||||
s.statusMessage = msg.StatusMessage()
|
||||
case events.PromptForInputMsg:
|
||||
if s.pendingInput != nil || s.pendingKeyState != nil {
|
||||
if s.pendingInput != nil {
|
||||
// ignore, already in an input
|
||||
return s, nil
|
||||
}
|
||||
|
|
@ -96,40 +94,7 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
s.textInput.Focus()
|
||||
s.textInput.SetValue("")
|
||||
s.pendingInput = newPendingInputState(msg)
|
||||
case events.PromptForKeyMsg:
|
||||
if s.pendingInput != nil || s.pendingKeyState != nil {
|
||||
// ignore, already in an input
|
||||
return s, nil
|
||||
}
|
||||
|
||||
s.statusMessage = msg.Prompt
|
||||
s.pendingKeyState = &pendingKeyState{msg}
|
||||
case tea.KeyMsg:
|
||||
if s.pendingKeyState != nil {
|
||||
switch msg.Type {
|
||||
case tea.KeyCtrlC, tea.KeyEsc:
|
||||
if s.pendingKeyState.originalMsg.OnCancel != nil {
|
||||
pendingKeyState := s.pendingKeyState
|
||||
cc.Add(func() tea.Msg {
|
||||
m := pendingKeyState.originalMsg.OnCancel()
|
||||
return m
|
||||
})
|
||||
}
|
||||
s.pendingKeyState = nil
|
||||
default:
|
||||
if s.pendingKeyState.originalMsg.OnDone != nil {
|
||||
pendingKeyState := s.pendingKeyState
|
||||
cc.Add(func() tea.Msg {
|
||||
m := pendingKeyState.originalMsg.OnDone(msg.String())
|
||||
return m
|
||||
})
|
||||
}
|
||||
s.pendingKeyState = nil
|
||||
}
|
||||
s.statusMessage = ""
|
||||
return s, cc.Cmd()
|
||||
}
|
||||
|
||||
if s.pendingInput != nil {
|
||||
switch msg.Type {
|
||||
case tea.KeyCtrlC, tea.KeyEsc:
|
||||
|
|
@ -222,7 +187,7 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
}
|
||||
|
||||
func (s *StatusAndPrompt) InPrompt() bool {
|
||||
return s.pendingInput != nil || s.pendingKeyState != nil
|
||||
return s.pendingInput != nil
|
||||
}
|
||||
|
||||
func (s *StatusAndPrompt) View() string {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,3 @@ func newPendingInputState(msg events.PromptForInputMsg) *pendingInputState {
|
|||
type PasteboardProvider interface {
|
||||
ReadText() (string, bool)
|
||||
}
|
||||
|
||||
type pendingKeyState struct {
|
||||
originalMsg events.PromptForKeyMsg
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ type ItemViewStyle struct {
|
|||
var DefaultStyles = Styles{
|
||||
ItemView: ItemViewStyle{
|
||||
FieldType: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#2B800C", Dark: "#73C653"}),
|
||||
MetaInfo: lipgloss.NewStyle().Foreground(lipgloss.Color("#707070")),
|
||||
MetaInfo: lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")),
|
||||
},
|
||||
Frames: frame.Style{
|
||||
ActiveTitle: lipgloss.NewStyle().
|
||||
|
|
|
|||
37
linux.goreleaser.yml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
builds:
|
||||
- id: dynamo-browse
|
||||
targets:
|
||||
- windows_amd64
|
||||
- linux_amd64
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
main: ./cmd/dynamo-browse/.
|
||||
binary: dynamo-browse
|
||||
archives:
|
||||
- id: zip
|
||||
builds:
|
||||
- dynamo-browse
|
||||
wrap_in_directory: true
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
- goos: linux
|
||||
format: tar.gz
|
||||
nfpms:
|
||||
- id: package_nfpms
|
||||
package_name: audax
|
||||
builds:
|
||||
- dynamo-browse
|
||||
vendor: lmika
|
||||
homepage: https://audax.tools/
|
||||
maintainer: Leon Mika <lmika@lmika.org>
|
||||
description: TUI tools for AWS administration
|
||||
license: MIT
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
bindir: /usr/local/bin
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
31
macos.goreleaser.yml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
builds:
|
||||
- id: dynamo-browse
|
||||
targets:
|
||||
- darwin_amd64
|
||||
- darwin_arm64
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
main: ./cmd/dynamo-browse/.
|
||||
binary: dynamo-browse
|
||||
archives:
|
||||
- id: zip
|
||||
builds:
|
||||
- dynamo-browse
|
||||
wrap_in_directory: true
|
||||
format_overrides:
|
||||
- goos: macos
|
||||
format: tar.gz
|
||||
brews:
|
||||
- name: audax
|
||||
repository:
|
||||
owner: lmika
|
||||
name: homebrew-audax
|
||||
token: "{{ .Env.HOMEBREW_GITHUB_TOKEN }}"
|
||||
folder: Formula
|
||||
homepage: https://dynamobrowse.app/
|
||||
description: TUI tools for AWS administration
|
||||
license: MIT
|
||||
checksum:
|
||||
name_template: 'checksums-macos.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
|
|
@ -4,17 +4,17 @@ import (
|
|||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"github.com/pkg/errors"
|
||||
"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/services/tables"
|
||||
"github.com/lmika/gopkgs/cli"
|
||||
"github.com/pkg/errors"
|
||||
"log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -28,7 +28,7 @@ func main() {
|
|||
|
||||
cfg, err := config.LoadDefaultConfig(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("cannot load AWS config: %v", err)
|
||||
cli.Fatalf("cannot load AWS config: %v", err)
|
||||
}
|
||||
|
||||
dynamoClient := dynamodb.NewFromConfig(cfg,
|
||||
|
|
|
|||