* New rel-picker that can be opened using Shift+O and allows for quickly going to related tables.
282 lines
7.5 KiB
Go
282 lines
7.5 KiB
Go
package controllers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
|
|
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
|
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems"
|
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
|
|
bus "github.com/lmika/events"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
type ScriptController struct {
|
|
scriptManager *scriptmanager.Service
|
|
tableReadController *TableReadController
|
|
jobController *JobsController
|
|
settingsController *SettingsController
|
|
eventBus *bus.Bus
|
|
sendMsg func(msg tea.Msg)
|
|
}
|
|
|
|
func NewScriptController(
|
|
scriptManager *scriptmanager.Service,
|
|
tableReadController *TableReadController,
|
|
jobController *JobsController,
|
|
settingsController *SettingsController,
|
|
eventBus *bus.Bus,
|
|
) *ScriptController {
|
|
sc := &ScriptController{
|
|
scriptManager: scriptManager,
|
|
tableReadController: tableReadController,
|
|
jobController: jobController,
|
|
settingsController: settingsController,
|
|
eventBus: eventBus,
|
|
}
|
|
|
|
sessionImpl := &sessionImpl{sc: sc, lastSelectedItemIndex: -1}
|
|
scriptManager.SetIFaces(scriptmanager.Ifaces{
|
|
UI: &uiImpl{sc: sc},
|
|
Session: sessionImpl,
|
|
})
|
|
|
|
sessionImpl.subscribeToEvents(eventBus)
|
|
|
|
// Setup event handling when settings have changed
|
|
eventBus.On(BusEventSettingsUpdated, func(name, value string) {
|
|
if !strings.HasPrefix(name, "script.") {
|
|
return
|
|
}
|
|
sc.Init()
|
|
})
|
|
|
|
return sc
|
|
}
|
|
|
|
func (sc *ScriptController) Init() {
|
|
if lookupPaths, err := sc.settingsController.settings.ScriptLookupFS(); err == nil {
|
|
sc.scriptManager.SetLookupPaths(lookupPaths)
|
|
} else {
|
|
log.Printf("warn: script lookup paths are invalid: %v", err)
|
|
}
|
|
}
|
|
|
|
func (sc *ScriptController) SetMessageSender(sendMsg func(msg tea.Msg)) {
|
|
sc.sendMsg = sendMsg
|
|
}
|
|
|
|
func (sc *ScriptController) LoadScript(filename string) tea.Msg {
|
|
ctx := context.Background()
|
|
plugin, err := sc.scriptManager.LoadScript(ctx, filename)
|
|
if err != nil {
|
|
return events.Error(err)
|
|
}
|
|
|
|
return events.StatusMsg(fmt.Sprintf("Script '%v' loaded", plugin.Name()))
|
|
}
|
|
|
|
func (sc *ScriptController) RunScript(filename string) tea.Msg {
|
|
ctx := context.Background()
|
|
if err := sc.scriptManager.StartAdHocScript(ctx, filename, sc.waitAndPrintScriptError()); err != nil {
|
|
return events.Error(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (sc *ScriptController) waitAndPrintScriptError() chan error {
|
|
errChan := make(chan error)
|
|
go func() {
|
|
if err := <-errChan; err != nil {
|
|
sc.sendMsg(events.Error(err))
|
|
}
|
|
}()
|
|
return errChan
|
|
}
|
|
|
|
func (sc *ScriptController) LookupCommand(name string) commandctrl.Command {
|
|
cmd := sc.scriptManager.LookupCommand(name)
|
|
if cmd == nil {
|
|
return nil
|
|
}
|
|
|
|
return func(execCtx commandctrl.ExecContext, args []string) tea.Msg {
|
|
errChan := sc.waitAndPrintScriptError()
|
|
ctx := context.Background()
|
|
|
|
if err := cmd.Invoke(ctx, args, errChan); err != nil {
|
|
return events.Error(err)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
type uiImpl struct {
|
|
sc *ScriptController
|
|
}
|
|
|
|
func (u uiImpl) PrintMessage(ctx context.Context, msg string) {
|
|
u.sc.sendMsg(events.StatusMsg(msg))
|
|
}
|
|
|
|
func (u uiImpl) Prompt(ctx context.Context, msg string) chan string {
|
|
resultChan := make(chan string)
|
|
u.sc.sendMsg(events.PromptForInputMsg{
|
|
Prompt: msg,
|
|
OnDone: func(value string) tea.Msg {
|
|
resultChan <- value
|
|
return nil
|
|
},
|
|
OnCancel: func() tea.Msg {
|
|
close(resultChan)
|
|
return nil
|
|
},
|
|
})
|
|
return resultChan
|
|
}
|
|
|
|
type sessionImpl struct {
|
|
sc *ScriptController
|
|
lastSelectedItemIndex int
|
|
}
|
|
|
|
func (s *sessionImpl) subscribeToEvents(bus *bus.Bus) {
|
|
bus.On("ui.new-item-selected", func(rs *models.ResultSet, itemIndex int) {
|
|
s.lastSelectedItemIndex = itemIndex
|
|
})
|
|
}
|
|
|
|
func (s *sessionImpl) SelectedItemIndex(ctx context.Context) int {
|
|
return s.lastSelectedItemIndex
|
|
}
|
|
|
|
func (s *sessionImpl) ResultSet(ctx context.Context) *models.ResultSet {
|
|
return s.sc.tableReadController.state.ResultSet()
|
|
}
|
|
|
|
func (s *sessionImpl) SetResultSet(ctx context.Context, newResultSet *models.ResultSet) {
|
|
state := s.sc.tableReadController.state
|
|
msg := s.sc.tableReadController.setResultSetAndFilter(newResultSet, state.filter, true, resultSetUpdateScript)
|
|
s.sc.sendMsg(msg)
|
|
}
|
|
|
|
func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanager.QueryOptions) (*models.ResultSet, error) {
|
|
// Parse the query
|
|
expr, err := queryexpr.Parse(query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if opts.NamePlaceholders != nil {
|
|
expr = expr.WithNameParams(opts.NamePlaceholders)
|
|
}
|
|
if opts.ValuePlaceholders != nil {
|
|
expr = expr.WithValueParams(opts.ValuePlaceholders)
|
|
}
|
|
if opts.IndexName != "" {
|
|
expr = expr.WithIndex(opts.IndexName)
|
|
}
|
|
|
|
return s.sc.doQuery(ctx, expr, opts)
|
|
}
|
|
|
|
func (s *ScriptController) doQuery(ctx context.Context, expr *queryexpr.QueryExpr, opts scriptmanager.QueryOptions) (*models.ResultSet, error) {
|
|
// Get the table info
|
|
var (
|
|
tableInfo *models.TableInfo
|
|
err error
|
|
)
|
|
|
|
tableName := opts.TableName
|
|
currentResultSet := s.tableReadController.state.ResultSet()
|
|
|
|
if tableName != "" {
|
|
// Table specified. If it's the same as the current table, then use the existing table info
|
|
if currentResultSet != nil && currentResultSet.TableInfo.Name == tableName {
|
|
tableInfo = currentResultSet.TableInfo
|
|
}
|
|
|
|
// Otherwise, describe the table
|
|
tableInfo, err = s.tableReadController.tableService.Describe(ctx, tableName)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "cannot describe table '%v'", tableName)
|
|
}
|
|
} else {
|
|
// Table not specified. Use the existing table, if any
|
|
if currentResultSet == nil {
|
|
return nil, errors.New("no table currently selected")
|
|
}
|
|
tableInfo = currentResultSet.TableInfo
|
|
}
|
|
|
|
newResultSet, err := s.tableReadController.tableService.ScanOrQuery(ctx, tableInfo, expr, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return newResultSet, nil
|
|
}
|
|
|
|
func (sc *ScriptController) CustomKeyCommand(key string) tea.Cmd {
|
|
_, cmd := sc.scriptManager.LookupKeyBinding(key)
|
|
if cmd == nil {
|
|
return nil
|
|
}
|
|
|
|
return func() tea.Msg {
|
|
errChan := sc.waitAndPrintScriptError()
|
|
ctx := context.Background()
|
|
|
|
if err := cmd.Invoke(ctx, nil, errChan); err != nil {
|
|
return events.Error(err)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (sc *ScriptController) Rebind(bindingName string, newKey string) error {
|
|
return sc.scriptManager.RebindKeyBinding(bindingName, newKey)
|
|
}
|
|
|
|
func (sc *ScriptController) LookupBinding(theKey string) string {
|
|
bindingName, _ := sc.scriptManager.LookupKeyBinding(theKey)
|
|
return bindingName
|
|
}
|
|
|
|
func (sc *ScriptController) UnbindKey(key string) {
|
|
sc.scriptManager.UnbindKey(key)
|
|
}
|
|
|
|
func (c *ScriptController) LookupRelatedItems(idx int) (res tea.Msg) {
|
|
rs := c.tableReadController.state.ResultSet()
|
|
|
|
relItems, err := c.scriptManager.RelatedItemOfItem(context.Background(), rs, idx)
|
|
if err != nil {
|
|
return events.Error(err)
|
|
} else if len(relItems) == 0 {
|
|
return events.StatusMsg("No related items available")
|
|
}
|
|
|
|
return ShowRelatedItemsOverlay{
|
|
Items: relItems,
|
|
OnSelected: func(item relitems.RelatedItem) tea.Msg {
|
|
if item.OnSelect != nil {
|
|
return item.OnSelect()
|
|
}
|
|
|
|
return NewJob(c.jobController, "Running query…", func(ctx context.Context) (*models.ResultSet, error) {
|
|
return c.doQuery(ctx, item.Query, scriptmanager.QueryOptions{
|
|
TableName: item.Table,
|
|
})
|
|
}).OnDone(func(rs *models.ResultSet) tea.Msg {
|
|
return c.tableReadController.setResultSetAndFilter(rs, "", true, resultSetUpdateQuery)
|
|
}).Submit()
|
|
},
|
|
}
|
|
}
|