242 lines
5.3 KiB
Go
242 lines
5.3 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright Authors of K9s
|
|
|
|
package view
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/derailed/k9s/internal/config"
|
|
"github.com/derailed/k9s/internal/slogs"
|
|
"github.com/derailed/k9s/internal/ui"
|
|
"github.com/derailed/k9s/internal/ui/dialog"
|
|
"github.com/derailed/tcell/v2"
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
)
|
|
|
|
// AllScopes represents actions available for all views.
|
|
const AllScopes = "all"
|
|
|
|
// Runner represents a runnable action handler.
|
|
type Runner interface {
|
|
// App returns the current app.
|
|
App() *App
|
|
|
|
// GetSelectedItem returns the current selected item.
|
|
GetSelectedItem() string
|
|
|
|
// Aliases returns all aliases assoxciated with the view GVR.
|
|
Aliases() sets.Set[string]
|
|
|
|
// EnvFn returns the current environment function.
|
|
EnvFn() EnvFunc
|
|
}
|
|
|
|
func hasAll(scopes []string) bool {
|
|
return slices.Contains(scopes, AllScopes)
|
|
}
|
|
|
|
func includes(aliases []string, s string) bool {
|
|
return slices.Contains(aliases, s)
|
|
}
|
|
|
|
func inScope(scopes []string, aliases sets.Set[string]) bool {
|
|
if hasAll(scopes) {
|
|
return true
|
|
}
|
|
for _, s := range scopes {
|
|
if _, ok := aliases[s]; ok {
|
|
return ok
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func hotKeyActions(r Runner, aa *ui.KeyActions) error {
|
|
hh := config.NewHotKeys()
|
|
aa.Range(func(k tcell.Key, a ui.KeyAction) {
|
|
if a.Opts.HotKey {
|
|
aa.Delete(k)
|
|
}
|
|
})
|
|
|
|
var errs error
|
|
if err := hh.Load(r.App().Config.ContextHotkeysPath()); err != nil {
|
|
errs = errors.Join(errs, err)
|
|
}
|
|
for k, hk := range hh.HotKey {
|
|
key, err := asKey(hk.ShortCut)
|
|
if err != nil {
|
|
errs = errors.Join(errs, err)
|
|
continue
|
|
}
|
|
if _, ok := aa.Get(key); ok {
|
|
if !hk.Override {
|
|
errs = errors.Join(errs, fmt.Errorf("duplicate hotkey found for %q in %q", hk.ShortCut, k))
|
|
continue
|
|
}
|
|
slog.Debug("HotKey overrode action shortcut",
|
|
slogs.Shortcut, hk.ShortCut,
|
|
slogs.Key, k,
|
|
)
|
|
}
|
|
|
|
command, err := r.EnvFn()().Substitute(hk.Command)
|
|
if err != nil {
|
|
slog.Warn("Invalid shortcut command", slogs.Error, err)
|
|
continue
|
|
}
|
|
|
|
aa.Add(key, ui.NewKeyActionWithOpts(
|
|
hk.Description,
|
|
gotoCmd(r, command, "", !hk.KeepHistory),
|
|
ui.ActionOpts{
|
|
Shared: true,
|
|
HotKey: true,
|
|
},
|
|
))
|
|
}
|
|
|
|
return errs
|
|
}
|
|
|
|
func gotoCmd(r Runner, cmd, path string, clearStack bool) ui.ActionHandler {
|
|
return func(*tcell.EventKey) *tcell.EventKey {
|
|
r.App().gotoResource(cmd, path, clearStack, true)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func pluginActions(r Runner, aa *ui.KeyActions) error {
|
|
// Skip plugin loading if no valid connection
|
|
if r.App().Conn() == nil || !r.App().Conn().ConnectionOK() {
|
|
return nil
|
|
}
|
|
|
|
aa.Range(func(k tcell.Key, a ui.KeyAction) {
|
|
if a.Opts.Plugin {
|
|
aa.Delete(k)
|
|
}
|
|
})
|
|
|
|
path, err := r.App().Config.ContextPluginsPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pp := config.NewPlugins()
|
|
if err := pp.Load(path, true); err != nil {
|
|
return err
|
|
}
|
|
|
|
var (
|
|
errs error
|
|
aliases = r.Aliases()
|
|
ro = r.App().Config.IsReadOnly()
|
|
)
|
|
for k := range pp.Plugins {
|
|
if !inScope(pp.Plugins[k].Scopes, aliases) || (ro && pp.Plugins[k].Dangerous) {
|
|
continue
|
|
}
|
|
key, err := asKey(pp.Plugins[k].ShortCut)
|
|
if err != nil {
|
|
errs = errors.Join(errs, err)
|
|
continue
|
|
}
|
|
if _, ok := aa.Get(key); ok {
|
|
if !pp.Plugins[k].Override {
|
|
errs = errors.Join(errs, fmt.Errorf("duplicate plugin key found for %q in %q", pp.Plugins[k].ShortCut, k))
|
|
continue
|
|
}
|
|
slog.Debug("Plugin overrode action shortcut",
|
|
slogs.Plugin, k,
|
|
slogs.Key, pp.Plugins[k].ShortCut,
|
|
)
|
|
}
|
|
|
|
plugin := pp.Plugins[k]
|
|
aa.Add(key, ui.NewKeyActionWithOpts(
|
|
pp.Plugins[k].Description,
|
|
pluginAction(r, &plugin),
|
|
ui.ActionOpts{
|
|
Visible: true,
|
|
Plugin: true,
|
|
Dangerous: plugin.Dangerous,
|
|
},
|
|
))
|
|
}
|
|
|
|
return errs
|
|
}
|
|
|
|
func pluginAction(r Runner, p *config.Plugin) ui.ActionHandler {
|
|
return func(evt *tcell.EventKey) *tcell.EventKey {
|
|
path := r.GetSelectedItem()
|
|
if path == "" {
|
|
return evt
|
|
}
|
|
if r.EnvFn() == nil {
|
|
return nil
|
|
}
|
|
|
|
args := make([]string, len(p.Args))
|
|
for i, a := range p.Args {
|
|
arg, err := r.EnvFn()().Substitute(a)
|
|
if err != nil {
|
|
slog.Error("Plugin Args match failed", slogs.Error, err)
|
|
return nil
|
|
}
|
|
args[i] = arg
|
|
}
|
|
|
|
cb := func() {
|
|
opts := shellOpts{
|
|
binary: p.Command,
|
|
background: p.Background,
|
|
pipes: p.Pipes,
|
|
args: args,
|
|
}
|
|
suspend, errChan, statusChan := run(r.App(), &opts)
|
|
if !suspend {
|
|
r.App().Flash().Infof("Plugin command failed: %q", p.Description)
|
|
return
|
|
}
|
|
var errs error
|
|
for e := range errChan {
|
|
errs = errors.Join(errs, e)
|
|
}
|
|
if errs != nil {
|
|
if !strings.Contains(errs.Error(), "signal: interrupt") {
|
|
slog.Error("Plugin command failed", slogs.Error, errs)
|
|
r.App().cowCmd(errs.Error())
|
|
return
|
|
}
|
|
}
|
|
go func() {
|
|
for st := range statusChan {
|
|
if !p.OverwriteOutput {
|
|
r.App().Flash().Infof("Plugin command launched successfully: %q", st)
|
|
} else if strings.Contains(st, outputPrefix) {
|
|
infoMsg := strings.TrimPrefix(st, outputPrefix)
|
|
r.App().Flash().Info(strings.TrimSpace(infoMsg))
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
if p.Confirm {
|
|
msg := fmt.Sprintf("Run?\n%s %s", p.Command, strings.Join(args, " "))
|
|
d := r.App().Styles.Dialog()
|
|
dialog.ShowConfirm(&d, r.App().Content.Pages, "Confirm "+p.Description, msg, cb, func() {})
|
|
return nil
|
|
}
|
|
cb()
|
|
|
|
return nil
|
|
}
|
|
}
|