changed to command buffer to diff command vs nav + bugz fixes
parent
cc9b789123
commit
b332912d1b
67
README.md
67
README.md
|
|
@ -20,26 +20,28 @@ for changes and offers subsequent commands to interact with observed resources.
|
|||
---
|
||||
## Installation
|
||||
|
||||
### Homebrew (OSX)
|
||||
1. Homebrew (OSX)
|
||||
|
||||
```shell
|
||||
brew tap derailed/k9s && brew install k9s
|
||||
```
|
||||
|
||||
### Binary Releases
|
||||
1. Binary Releases
|
||||
|
||||
- [Releases](https://github.com/derailed/k9s/releases)
|
||||
|
||||
|
||||
<br/>
|
||||
|
||||
---
|
||||
## Command Line
|
||||
|
||||
To show all available options from the cli use:
|
||||
|
||||
```shell
|
||||
# List all available CLI options
|
||||
k9s -h
|
||||
# To get info about K9s runtime (logs, configs, etc..)
|
||||
k9s config
|
||||
# To run K9s in a given namespace
|
||||
k9s -n mybitchns
|
||||
```
|
||||
|
||||
<br/>
|
||||
|
|
@ -55,8 +57,9 @@ k9s -h
|
|||
|
||||
* For clusters with many namespaces you can either edit ~/.k9s/config.yml or
|
||||
go to the namespace(ns) view to switch your default namespace to your namespace
|
||||
of choice using *Ctrl-S*witch. K9s will keep your top 5 favorite namespaces.
|
||||
Namespaces will get evicted based on your namespace switching frequency.
|
||||
of choice using *Ctrl-S*witch. K9s keeps your top 10 favorite namespaces.
|
||||
Namespaces will get evicted from the top 10 list, based on your namespace
|
||||
switching frequency.
|
||||
|
||||
|
||||
```yaml
|
||||
|
|
@ -65,7 +68,7 @@ k9s -h
|
|||
logBufferSize: 200 # Size of the logs buffer. Try to keep a sensible default!
|
||||
namespace:
|
||||
active: myCoolNS # Current active namespace name
|
||||
favorites: # List of your 5 most frequently used namespaces
|
||||
favorites: # List of your 10 most frequently used namespaces
|
||||
- myCoolNS1
|
||||
- myCoolNS2
|
||||
- all
|
||||
|
|
@ -84,14 +87,39 @@ k9s -h
|
|||
<br/>
|
||||
|
||||
---
|
||||
## Commands
|
||||
## Usage
|
||||
|
||||
+ K9s uses 2 or 3 letters alias to navigate most K8s resource
|
||||
+ At any time you can use `?` to look up the various commands
|
||||
+ Use `alias<Enter>` to activate a resource under that alias
|
||||
+ `Ctrl` sequences are used to view, edit, delete, ssh ...
|
||||
+ Use `ctx<Enter>` to switch between clusters
|
||||
+ Use `Q` or `Ctrl-C` to Quit.
|
||||
K9s uses 2 or 3 letters alias to navigate most K8s resource.
|
||||
|
||||
| Command | Result | Example |
|
||||
| ------------------ | -------------------------------------------------- | -------------------- |
|
||||
| `>`alias`<ENTER>` | List a Kubernetes resource in the active namespace | `>po<ENTER>` |
|
||||
| Ctrl-A | Show all command aliases | |
|
||||
| `/`filter`ENTER`> | Filter out a resource view given a filter | `/bumblebeetuna` |
|
||||
| `<Esc>` | Bails out of command mode | |
|
||||
| `Ctrl-Key` | Key mapping to view, edit, see logs, etc... | `Ctrl-L` (view logs) |
|
||||
| `>`ctx`<ENTER>` | To view and switch to another Kubernetes cluster | |
|
||||
| `Ctrl-q`, `Ctrl-c` | To bail out of K9s | |
|
||||
|
||||
|
||||
<br/>
|
||||
|
||||
---
|
||||
## Building From Source
|
||||
|
||||
K9s was built using go 1.11. In order to build K9 from source:
|
||||
|
||||
+ Clone the repo
|
||||
+ Add the following command in your go.mod file
|
||||
```text
|
||||
replace (
|
||||
github.com/derailed/k9s => MY_K9S_CLONED_REPO
|
||||
)
|
||||
```
|
||||
+ Build and run the executable
|
||||
```shell
|
||||
go run main.go
|
||||
```
|
||||
|
||||
<br/>
|
||||
|
||||
|
|
@ -119,9 +147,9 @@ k9s -h
|
|||
---
|
||||
## Known Issues...
|
||||
|
||||
This initial drop is brittle. k9s will most likely blow up if...
|
||||
This initial drop is brittle. K9s will most likely blow up if...
|
||||
|
||||
+ Your kube-config file does not live under $HOME/.kube or you use multiple configs
|
||||
+ K9s does not support multiple cluster config specified via KUBECONFIG env var
|
||||
+ You don't have enough RBAC fu to manage your cluster
|
||||
+ Your cluster does not run a metrics-server
|
||||
|
||||
|
|
@ -139,8 +167,9 @@ dig this effort, please let us know that too!
|
|||
---
|
||||
## ATTA Girls/Boys!
|
||||
|
||||
k9s sits on top of on a lot of opensource projects. So *big thanks* to all the
|
||||
contributors that made this project a reality!
|
||||
K9s sits on top of many of opensource projects and libraries. Our *sincere*
|
||||
appreciations to all the OSS contributors that work nights and weekends
|
||||
to make this project a reality!
|
||||
|
||||
|
||||
<br/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
# Release v0.1.2
|
||||
|
||||
<br/>
|
||||
|
||||
---
|
||||
## Notes
|
||||
|
||||
Thank you to all that contributed with flushing out issues with K9s! I'll try
|
||||
to mark some of these issues as fixed. But if you don't mind grab the latest
|
||||
rev and see if we're happier with some of the fixes!
|
||||
|
||||
<br/>
|
||||
|
||||
---
|
||||
## Change Logs
|
||||
|
||||
+ Navigation changed! Thanks to [Teppei Fukuda](https://github.com/knqyf263) for
|
||||
hinting about the different modes ie command vs navigation. Now in order to
|
||||
navigate to a specific kubernetes resource you need to issue this command
|
||||
to say see all pods (using key `>`):
|
||||
|
||||
```text
|
||||
>po<ENTER>
|
||||
```
|
||||
+ Similarly to filter on a given resource you can use `/' and type your filter.
|
||||
+ In both instances `<ESC>` will back you out of command mode and into navigation mode.
|
||||
|
||||
<br/>
|
||||
|
||||
---
|
||||
## Resolved Bugs
|
||||
|
||||
+ [Issue #23](https://github.com/derailed/k9s/issues/23)
|
||||
+ [Issue #19](https://github.com/derailed/k9s/issues/19)
|
||||
|
|
@ -104,9 +104,9 @@ func run(cmd *cobra.Command, args []string) {
|
|||
initStyles()
|
||||
initKeys()
|
||||
|
||||
app := views.NewApp(version, refreshRate, namespace)
|
||||
app := views.NewApp()
|
||||
{
|
||||
app.Init()
|
||||
app.Init(version, refreshRate, namespace)
|
||||
app.Run()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
3
go.mod
3
go.mod
|
|
@ -1,9 +1,10 @@
|
|||
module github.com/derailed/k9s
|
||||
|
||||
// replace github.com/k8sland/tview => /Users/fernand/go_wk/k8sland/src/github.com/k8sland/tview
|
||||
replace github.com/k8sland/tview => /Users/fernand/go_wk/k8sland/src/github.com/k8sland/tview
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.34.0 // indirect
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/gdamore/tcell v1.1.0
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/gogo/protobuf v1.1.1 // indirect
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"math"
|
||||
"path"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
|
|
@ -94,7 +93,6 @@ func (m *MetricsServer) PodMetrics() (map[string]Metric, error) {
|
|||
|
||||
// PerNodeMetrics retrieves all nodes metrics
|
||||
func (m *MetricsServer) PerNodeMetrics(nn []v1.Node) (map[string]Metric, error) {
|
||||
log.Println("Getting per node metrics...")
|
||||
mx := map[string]Metric{}
|
||||
|
||||
mm, err := m.getNodeMetrics()
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import (
|
|||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
const lbIPWidth = 16
|
||||
|
||||
// Service tracks a kubernetes resource.
|
||||
type Service struct {
|
||||
*Base
|
||||
|
|
@ -107,7 +109,7 @@ func (r *Service) Fields(ns string) Row {
|
|||
i.ObjectMeta.Name,
|
||||
string(i.Spec.Type),
|
||||
i.Spec.ClusterIP,
|
||||
r.toIPs(i.Spec.Type, i.Spec.ExternalIPs),
|
||||
r.toIPs(i.Spec.Type, getSvcExtIPS(i)),
|
||||
r.toPorts(i.Spec.Ports),
|
||||
toAge(i.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
|
@ -120,6 +122,44 @@ func (r *Service) ExtFields() Properties {
|
|||
|
||||
// Helpers...
|
||||
|
||||
func getSvcExtIPS(svc *v1.Service) []string {
|
||||
results := []string{}
|
||||
|
||||
switch svc.Spec.Type {
|
||||
case v1.ServiceTypeClusterIP:
|
||||
fallthrough
|
||||
case v1.ServiceTypeNodePort:
|
||||
return svc.Spec.ExternalIPs
|
||||
case v1.ServiceTypeLoadBalancer:
|
||||
lbIps := lbIngressIP(svc.Status.LoadBalancer)
|
||||
if len(svc.Spec.ExternalIPs) > 0 {
|
||||
if len(lbIps) > 0 {
|
||||
results = append(results, lbIps)
|
||||
}
|
||||
return append(results, svc.Spec.ExternalIPs...)
|
||||
}
|
||||
if len(lbIps) > 0 {
|
||||
results = append(results, lbIps)
|
||||
}
|
||||
case v1.ServiceTypeExternalName:
|
||||
results = append(results, svc.Spec.ExternalName)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func lbIngressIP(s v1.LoadBalancerStatus) string {
|
||||
ingress := s.Ingress
|
||||
result := []string{}
|
||||
for i := range ingress {
|
||||
if len(ingress[i].IP) > 0 {
|
||||
result = append(result, ingress[i].IP)
|
||||
} else if len(ingress[i].Hostname) > 0 {
|
||||
result = append(result, ingress[i].Hostname)
|
||||
}
|
||||
}
|
||||
return strings.Join(result, ",")
|
||||
}
|
||||
|
||||
func (*Service) toIPs(svcType v1.ServiceType, ips []string) string {
|
||||
if len(ips) == 0 {
|
||||
if svcType == v1.ServiceTypeLoadBalancer {
|
||||
|
|
|
|||
75
views/app.go
75
views/app.go
|
|
@ -33,8 +33,6 @@ type (
|
|||
*tview.Application
|
||||
|
||||
version string
|
||||
refreshRate int
|
||||
namespace string
|
||||
pages *tview.Pages
|
||||
content *tview.Pages
|
||||
flashView *flashView
|
||||
|
|
@ -45,22 +43,22 @@ type (
|
|||
focusCurrent int
|
||||
focusChanged focusHandler
|
||||
cancel context.CancelFunc
|
||||
cmdBuff []rune
|
||||
cmdBuff *cmdBuff
|
||||
cmdView *cmdView
|
||||
}
|
||||
)
|
||||
|
||||
// NewApp returns a K9s app instance.
|
||||
func NewApp(v string, rate int, ns string) *appView {
|
||||
func NewApp() *appView {
|
||||
var app appView
|
||||
{
|
||||
app = appView{
|
||||
Application: tview.NewApplication(),
|
||||
pages: tview.NewPages(),
|
||||
version: v,
|
||||
refreshRate: rate,
|
||||
namespace: ns,
|
||||
menuView: newMenuView(),
|
||||
content: tview.NewPages(),
|
||||
cmdBuff: newCmdBuff('>'),
|
||||
cmdView: newCmdView('🐶'),
|
||||
}
|
||||
app.command = newCommand(&app)
|
||||
app.focusChanged = app.changedFocus
|
||||
|
|
@ -69,21 +67,24 @@ func NewApp(v string, rate int, ns string) *appView {
|
|||
return &app
|
||||
}
|
||||
|
||||
func (a *appView) Init() {
|
||||
func (a *appView) Init(v string, rate int, ns string) {
|
||||
a.version = v
|
||||
|
||||
log.Info("🐶 K9s starting up...")
|
||||
mustK8s()
|
||||
|
||||
initConfig(a.refreshRate, a.namespace)
|
||||
initConfig(rate, ns)
|
||||
|
||||
a.infoView = newInfoView(a)
|
||||
a.infoView.init()
|
||||
|
||||
a.flashView = newFlashView(a.Application, "Initializing...")
|
||||
|
||||
a.cmdBuff.addListener(a.cmdView)
|
||||
|
||||
header := tview.NewFlex()
|
||||
{
|
||||
header.SetDirection(tview.FlexColumn)
|
||||
header.AddItem(a.infoView, 25, 1, false)
|
||||
header.AddItem(a.infoView, 30, 1, false)
|
||||
header.AddItem(a.menuView, 0, 1, false)
|
||||
header.AddItem(logoView(), 26, 1, false)
|
||||
}
|
||||
|
|
@ -91,7 +92,8 @@ func (a *appView) Init() {
|
|||
main := tview.NewFlex()
|
||||
{
|
||||
main.SetDirection(tview.FlexRow)
|
||||
main.AddItem(header, 7, 1, false)
|
||||
main.AddItem(header, 6, 1, false)
|
||||
main.AddItem(a.cmdView, 1, 1, false)
|
||||
main.AddItem(a.content, 0, 10, true)
|
||||
main.AddItem(a.flashView, 2, 1, false)
|
||||
}
|
||||
|
|
@ -134,42 +136,45 @@ func (a *appView) Run() {
|
|||
}
|
||||
|
||||
func (a *appView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||
switch evt.Rune() {
|
||||
case 'q':
|
||||
a.quit(evt)
|
||||
return nil
|
||||
case '?':
|
||||
a.helpCmd()
|
||||
key := evt.Key()
|
||||
if key == tcell.KeyRune {
|
||||
switch evt.Rune() {
|
||||
case a.cmdBuff.hotKey:
|
||||
a.cmdBuff.setActive(true)
|
||||
a.cmdBuff.clear()
|
||||
return evt
|
||||
}
|
||||
|
||||
if a.cmdBuff.isActive() {
|
||||
a.cmdBuff.add(evt.Rune())
|
||||
}
|
||||
return evt
|
||||
}
|
||||
|
||||
switch evt.Key() {
|
||||
case tcell.KeyCtrlQ:
|
||||
a.quit(evt)
|
||||
case tcell.KeyCtrlH:
|
||||
a.help(evt)
|
||||
case tcell.KeyCtrlR:
|
||||
a.Draw()
|
||||
case tcell.KeyEsc:
|
||||
a.resetCmd()
|
||||
a.cmdBuff.reset()
|
||||
case tcell.KeyEnter:
|
||||
if len(a.cmdBuff) != 0 {
|
||||
a.command.run(string(a.cmdBuff))
|
||||
if a.cmdBuff.isActive() && !a.cmdBuff.empty() {
|
||||
a.command.run(a.cmdBuff.String())
|
||||
}
|
||||
a.cmdBuff.setActive(false)
|
||||
case tcell.KeyBackspace2:
|
||||
if a.cmdBuff.isActive() {
|
||||
a.cmdBuff.del()
|
||||
}
|
||||
a.resetCmd()
|
||||
case tcell.KeyTab:
|
||||
a.nextFocus()
|
||||
case tcell.KeyRune:
|
||||
a.cmdBuff = append([]rune(a.cmdBuff), evt.Rune())
|
||||
}
|
||||
return evt
|
||||
}
|
||||
|
||||
func (a *appView) helpCmd() {
|
||||
log.Info("Got help")
|
||||
a.inject(newHelpView(a))
|
||||
}
|
||||
|
||||
func (a *appView) resetCmd() {
|
||||
a.cmdBuff = []rune{}
|
||||
}
|
||||
|
||||
func (a *appView) showPage(p string) {
|
||||
a.pages.SwitchToPage(p)
|
||||
}
|
||||
|
|
@ -202,6 +207,10 @@ func (a *appView) refresh() {
|
|||
a.infoView.refresh()
|
||||
}
|
||||
|
||||
func (a *appView) help(*tcell.EventKey) {
|
||||
a.inject(newHelpView(a))
|
||||
}
|
||||
|
||||
func (a *appView) quit(*tcell.EventKey) {
|
||||
a.Stop()
|
||||
os.Exit(0)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/k8sland/tview"
|
||||
)
|
||||
|
||||
const defaultPrompt = "%c> %s"
|
||||
|
||||
type cmdView struct {
|
||||
*tview.TextView
|
||||
|
||||
icon rune
|
||||
text string
|
||||
}
|
||||
|
||||
func newCmdView(ic rune) *cmdView {
|
||||
v := cmdView{icon: ic, TextView: tview.NewTextView()}
|
||||
{
|
||||
v.SetWordWrap(false)
|
||||
v.SetWrap(false)
|
||||
v.SetDynamicColors(true)
|
||||
v.SetBorderPadding(0, 0, 1, 1)
|
||||
v.SetTextColor(tcell.ColorAqua)
|
||||
}
|
||||
return &v
|
||||
}
|
||||
|
||||
func (v *cmdView) activate() {
|
||||
v.write(v.text)
|
||||
}
|
||||
|
||||
func (v *cmdView) update(s string) {
|
||||
v.text = s
|
||||
v.Clear()
|
||||
v.write(s)
|
||||
}
|
||||
|
||||
func (v *cmdView) append(r rune) {
|
||||
fmt.Fprintf(v, string(r))
|
||||
}
|
||||
|
||||
func (v *cmdView) write(s string) {
|
||||
fmt.Fprintf(v, defaultPrompt, v.icon, s)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Event Listener protocol...
|
||||
|
||||
func (v *cmdView) changed(s string) {
|
||||
v.update(s)
|
||||
}
|
||||
|
||||
func (v *cmdView) active(f bool) {
|
||||
if f {
|
||||
v.activate()
|
||||
return
|
||||
}
|
||||
v.Clear()
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package views
|
||||
|
||||
const maxBuff = 10
|
||||
|
||||
type buffWatcher interface {
|
||||
changed(s string)
|
||||
active(state bool)
|
||||
}
|
||||
|
||||
type cmdBuff struct {
|
||||
buff []rune
|
||||
hotKey rune
|
||||
active bool
|
||||
listeners []buffWatcher
|
||||
}
|
||||
|
||||
func newCmdBuff(key rune) *cmdBuff {
|
||||
return &cmdBuff{
|
||||
hotKey: key,
|
||||
buff: make([]rune, 0, maxBuff),
|
||||
listeners: []buffWatcher{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cmdBuff) isActive() bool {
|
||||
return c.active
|
||||
}
|
||||
|
||||
func (c *cmdBuff) setActive(b bool) {
|
||||
c.active = b
|
||||
c.fireActive(c.active)
|
||||
}
|
||||
|
||||
// String turns rune to string (Stringer protocol)
|
||||
func (c *cmdBuff) String() string {
|
||||
return string(c.buff)
|
||||
}
|
||||
|
||||
func (c *cmdBuff) add(r rune) {
|
||||
c.buff = append(c.buff, r)
|
||||
c.fireChanged()
|
||||
}
|
||||
|
||||
func (c *cmdBuff) del() {
|
||||
if c.empty() {
|
||||
return
|
||||
}
|
||||
c.buff = c.buff[:len(c.buff)-1]
|
||||
c.fireChanged()
|
||||
}
|
||||
|
||||
func (c *cmdBuff) clear() {
|
||||
c.buff = make([]rune, 0, maxBuff)
|
||||
c.fireChanged()
|
||||
}
|
||||
|
||||
func (c *cmdBuff) reset() {
|
||||
c.active = false
|
||||
c.clear()
|
||||
c.fireChanged()
|
||||
c.fireActive(c.active)
|
||||
}
|
||||
|
||||
func (c *cmdBuff) empty() bool {
|
||||
return len(c.buff) == 0
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Event Listeners...
|
||||
|
||||
func (c *cmdBuff) addListener(w ...buffWatcher) {
|
||||
c.listeners = append(c.listeners, w...)
|
||||
}
|
||||
|
||||
func (c *cmdBuff) fireChanged() {
|
||||
for _, l := range c.listeners {
|
||||
l.changed(c.String())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cmdBuff) fireActive(b bool) {
|
||||
for _, l := range c.listeners {
|
||||
l.active(b)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type testListener struct {
|
||||
text string
|
||||
act int
|
||||
inact int
|
||||
}
|
||||
|
||||
func (l *testListener) changed(s string) {
|
||||
l.text = s
|
||||
}
|
||||
|
||||
func (l *testListener) active(s bool) {
|
||||
if s {
|
||||
l.act++
|
||||
return
|
||||
}
|
||||
l.inact++
|
||||
}
|
||||
|
||||
func TestCmdBuffActivate(t *testing.T) {
|
||||
b, l := newCmdBuff('>'), testListener{}
|
||||
b.addListener(&l)
|
||||
|
||||
b.setActive(true)
|
||||
assert.Equal(t, 1, l.act)
|
||||
assert.Equal(t, 0, l.inact)
|
||||
assert.True(t, b.active)
|
||||
}
|
||||
|
||||
func TestCmdBuffDeactivate(t *testing.T) {
|
||||
b, l := newCmdBuff('>'), testListener{}
|
||||
b.addListener(&l)
|
||||
|
||||
b.setActive(false)
|
||||
assert.Equal(t, 0, l.act)
|
||||
assert.Equal(t, 1, l.inact)
|
||||
assert.False(t, b.active)
|
||||
}
|
||||
|
||||
func TestCmdBuffChanged(t *testing.T) {
|
||||
b, l := newCmdBuff('>'), testListener{}
|
||||
b.addListener(&l)
|
||||
|
||||
b.add('b')
|
||||
assert.Equal(t, 0, l.act)
|
||||
assert.Equal(t, 0, l.inact)
|
||||
assert.Equal(t, "b", l.text)
|
||||
assert.Equal(t, "b", b.String())
|
||||
|
||||
b.del()
|
||||
assert.Equal(t, 0, l.act)
|
||||
assert.Equal(t, 0, l.inact)
|
||||
assert.Equal(t, "", l.text)
|
||||
assert.Equal(t, "", b.String())
|
||||
|
||||
b.add('c')
|
||||
b.clear()
|
||||
assert.Equal(t, 0, l.act)
|
||||
assert.Equal(t, 0, l.inact)
|
||||
assert.Equal(t, "", l.text)
|
||||
assert.Equal(t, "", b.String())
|
||||
|
||||
b.add('c')
|
||||
b.reset()
|
||||
assert.Equal(t, 0, l.act)
|
||||
assert.Equal(t, 1, l.inact)
|
||||
assert.Equal(t, "", l.text)
|
||||
assert.Equal(t, "", b.String())
|
||||
assert.True(t, b.empty())
|
||||
}
|
||||
|
||||
func TestCmdBuffAdd(t *testing.T) {
|
||||
b := newCmdBuff('>')
|
||||
|
||||
uu := []struct {
|
||||
runes []rune
|
||||
cmd string
|
||||
}{
|
||||
{[]rune{}, ""},
|
||||
{[]rune{'a'}, "a"},
|
||||
{[]rune{'a', 'b', 'c'}, "abc"},
|
||||
}
|
||||
|
||||
for _, u := range uu {
|
||||
for _, r := range u.runes {
|
||||
b.add(r)
|
||||
}
|
||||
assert.Equal(t, u.cmd, b.String())
|
||||
b.reset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdBuffDel(t *testing.T) {
|
||||
b := newCmdBuff('>')
|
||||
|
||||
uu := []struct {
|
||||
runes []rune
|
||||
cmd string
|
||||
}{
|
||||
{[]rune{}, ""},
|
||||
{[]rune{'a'}, ""},
|
||||
{[]rune{'a', 'b', 'c'}, "ab"},
|
||||
}
|
||||
|
||||
for _, u := range uu {
|
||||
for _, r := range u.runes {
|
||||
b.add(r)
|
||||
}
|
||||
b.del()
|
||||
assert.Equal(t, u.cmd, b.String())
|
||||
b.reset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdBuffEmpty(t *testing.T) {
|
||||
b := newCmdBuff('>')
|
||||
|
||||
uu := []struct {
|
||||
runes []rune
|
||||
empty bool
|
||||
}{
|
||||
{[]rune{}, true},
|
||||
{[]rune{'a'}, false},
|
||||
{[]rune{'a', 'b', 'c'}, false},
|
||||
}
|
||||
|
||||
for _, u := range uu {
|
||||
for _, r := range u.runes {
|
||||
b.add(r)
|
||||
}
|
||||
assert.Equal(t, u.empty, b.empty())
|
||||
b.reset()
|
||||
}
|
||||
}
|
||||
|
|
@ -24,35 +24,36 @@ func (c *command) defaultCmd() {
|
|||
// Exec the command by showing associated display.
|
||||
func (c *command) run(cmd string) {
|
||||
var v igniter
|
||||
switch cmd {
|
||||
case "q":
|
||||
c.app.quit(nil)
|
||||
default:
|
||||
if res, ok := cmdMap[cmd]; ok {
|
||||
v = res.viewFn(res.title, c.app, res.listFn(defaultNS), res.colorerFn)
|
||||
c.app.flash(flashInfo, "Viewing all "+res.title+"...")
|
||||
} else {
|
||||
if res, ok := getCRDS()[cmd]; !ok {
|
||||
c.app.flash(flashWarn, fmt.Sprintf("Huh? `%s` command not found", cmd))
|
||||
} else {
|
||||
n := res.Plural
|
||||
if len(n) == 0 {
|
||||
n = res.Singular
|
||||
}
|
||||
v = newResourceView(
|
||||
res.Kind,
|
||||
c.app,
|
||||
resource.NewCustomList("", res.Group, res.Version, n),
|
||||
defaultColorer,
|
||||
)
|
||||
}
|
||||
}
|
||||
if res, ok := cmdMap[cmd]; ok {
|
||||
v = res.viewFn(res.title, c.app, res.listFn(defaultNS), res.colorerFn)
|
||||
c.app.flash(flashInfo, "Viewing all "+res.title+"...")
|
||||
c.exec(cmd, v)
|
||||
return
|
||||
}
|
||||
|
||||
res, ok := getCRDS()[cmd]
|
||||
if !ok {
|
||||
c.app.flash(flashWarn, fmt.Sprintf("Huh? `%s` command not found", cmd))
|
||||
return
|
||||
}
|
||||
|
||||
n := res.Plural
|
||||
if len(n) == 0 {
|
||||
n = res.Singular
|
||||
}
|
||||
v = newResourceView(
|
||||
res.Kind,
|
||||
c.app,
|
||||
resource.NewCustomList("", res.Group, res.Version, n),
|
||||
defaultColorer,
|
||||
)
|
||||
c.exec(cmd, v)
|
||||
}
|
||||
|
||||
func (c *command) exec(cmd string, v igniter) {
|
||||
if v != nil {
|
||||
k9sCfg.K9s.View.Active = cmd
|
||||
k9sCfg.validateAndSave()
|
||||
c.app.inject(v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const (
|
|||
defaultRefreshRate = 2
|
||||
defaultLogBufferSize = 200
|
||||
defaultView = "po"
|
||||
maxFavorites = 10
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -30,8 +31,6 @@ var (
|
|||
K9sLogs = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-%s.log", mustK9sUser()))
|
||||
)
|
||||
|
||||
const maxFavorites = 5
|
||||
|
||||
type (
|
||||
namespace struct {
|
||||
Active string `yaml:"active"`
|
||||
|
|
@ -152,26 +151,6 @@ func (c *config) rmFavNS(ns string) {
|
|||
c.K9s.Namespace.Favorites = fv
|
||||
}
|
||||
|
||||
func defaultK9sConfig() *config {
|
||||
return &config{
|
||||
K9s: k9s{
|
||||
RefreshRate: 5,
|
||||
LogBufferSize: 200,
|
||||
View: view{
|
||||
Active: "po",
|
||||
},
|
||||
Namespace: namespace{
|
||||
Active: resource.DefaultNamespace,
|
||||
Favorites: []string{
|
||||
resource.AllNamespace,
|
||||
resource.DefaultNamespace,
|
||||
"kube-system",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func inList(ll []string, n string) bool {
|
||||
for _, l := range ll {
|
||||
if l == n {
|
||||
|
|
@ -230,3 +209,26 @@ func ensurePath(path string, mod os.FileMode) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Seed...
|
||||
|
||||
func defaultK9sConfig() *config {
|
||||
return &config{
|
||||
K9s: k9s{
|
||||
RefreshRate: 5,
|
||||
LogBufferSize: 200,
|
||||
View: view{
|
||||
Active: "po",
|
||||
},
|
||||
Namespace: namespace{
|
||||
Active: resource.DefaultNamespace,
|
||||
Favorites: []string{
|
||||
resource.AllNamespace,
|
||||
resource.DefaultNamespace,
|
||||
"kube-system",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ func TestConfigAddActive(t *testing.T) {
|
|||
{"all", []string{"all", "default", "kube-system"}},
|
||||
{"ns1", []string{"ns1", "all", "default", "kube-system"}},
|
||||
{"ns2", []string{"ns2", "ns1", "all", "default", "kube-system"}},
|
||||
{"ns3", []string{"ns3", "ns2", "ns1", "all", "default"}},
|
||||
{"ns4", []string{"ns4", "ns3", "ns2", "ns1", "all"}},
|
||||
{"ns3", []string{"ns3", "ns2", "ns1", "all", "default", "kube-system"}},
|
||||
{"ns4", []string{"ns4", "ns3", "ns2", "ns1", "all", "default", "kube-system"}},
|
||||
}
|
||||
|
||||
for _, u := range uu {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/resource"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
|
@ -22,15 +24,24 @@ func (v *contextView) useContext(*tcell.EventKey) {
|
|||
if !v.rowSelected() {
|
||||
return
|
||||
}
|
||||
err := v.list.Resource().(*resource.Context).Switch(v.selectedItem)
|
||||
|
||||
ctx := v.selectedItem
|
||||
if strings.HasSuffix(ctx, "*") {
|
||||
ctx = strings.TrimRight(ctx, "*")
|
||||
}
|
||||
if strings.HasSuffix(ctx, "(𝜟)") {
|
||||
ctx = strings.TrimRight(ctx, "(𝜟)")
|
||||
}
|
||||
|
||||
err := v.list.Resource().(*resource.Context).Switch(ctx)
|
||||
if err != nil {
|
||||
v.app.flash(flashWarn, err.Error())
|
||||
return
|
||||
}
|
||||
v.app.flash(flashInfo, "Switching context to ", v.selectedItem)
|
||||
v.app.flash(flashInfo, "Switching context to ", ctx)
|
||||
v.refresh()
|
||||
table := v.GetPrimitive("ctx").(*tableView)
|
||||
table.Select(0, 0)
|
||||
tv := v.GetPrimitive("ctx").(*tableView)
|
||||
tv.table.Select(0, 0)
|
||||
}
|
||||
|
||||
func (v *contextView) extraActions(aa keyActions) {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ func (v *helpView) init(context.Context, string) {
|
|||
t := tview.NewTable()
|
||||
{
|
||||
t.SetBorder(true)
|
||||
t.SetTitle(" [::b]Commands Help ")
|
||||
t.SetTitle(" [::b]Commands Aliases ")
|
||||
t.SetTitleColor(tcell.ColorAqua)
|
||||
t.SetBorderColor(tcell.ColorDodgerBlue)
|
||||
t.SetSelectable(true, false)
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ func (v *logsView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
if _, ok := numKeys[i]; ok {
|
||||
v.load(i - 1)
|
||||
v.pv.app.resetCmd()
|
||||
v.pv.app.cmdBuff.reset()
|
||||
return nil
|
||||
}
|
||||
return evt
|
||||
|
|
|
|||
204
views/menu.go
204
views/menu.go
|
|
@ -19,6 +19,123 @@ const (
|
|||
colLen = 20
|
||||
)
|
||||
|
||||
type (
|
||||
keyboardHandler func(*tcell.EventKey)
|
||||
|
||||
hint struct {
|
||||
mnemonic, display string
|
||||
}
|
||||
hints []hint
|
||||
|
||||
hinter interface {
|
||||
hints() hints
|
||||
}
|
||||
|
||||
keyAction struct {
|
||||
description string
|
||||
action keyboardHandler
|
||||
}
|
||||
keyActions map[tcell.Key]keyAction
|
||||
|
||||
menuView struct {
|
||||
*tview.Table
|
||||
}
|
||||
)
|
||||
|
||||
func (h hints) Len() int {
|
||||
return len(h)
|
||||
}
|
||||
func (h hints) Swap(i, j int) {
|
||||
h[i], h[j] = h[j], h[i]
|
||||
}
|
||||
func (h hints) Less(i, j int) bool {
|
||||
n, err1 := strconv.Atoi(h[i].mnemonic)
|
||||
m, err2 := strconv.Atoi(h[j].mnemonic)
|
||||
|
||||
if err1 == nil && err2 == nil {
|
||||
return n < m
|
||||
}
|
||||
|
||||
d := strings.Compare(h[i].mnemonic, h[j].mnemonic)
|
||||
return d < 0
|
||||
}
|
||||
|
||||
func newKeyHandler(d string, a keyboardHandler) keyAction {
|
||||
return keyAction{description: d, action: a}
|
||||
}
|
||||
|
||||
func newMenuView() *menuView {
|
||||
v := menuView{tview.NewTable()}
|
||||
return &v
|
||||
}
|
||||
|
||||
func (v *menuView) setMenu(hh hints) {
|
||||
v.Clear()
|
||||
sort.Sort(hh)
|
||||
|
||||
var row, col int
|
||||
firstNS, firstCmd := true, true
|
||||
for _, h := range hh {
|
||||
if len(h.mnemonic) == 1 && firstNS {
|
||||
row = 0
|
||||
col = 2
|
||||
firstNS = false
|
||||
}
|
||||
|
||||
if len(h.mnemonic) > 1 && firstCmd {
|
||||
row = 0
|
||||
col++
|
||||
firstCmd = false
|
||||
}
|
||||
c := tview.NewTableCell(v.item(h))
|
||||
v.SetCell(row, col, c)
|
||||
row++
|
||||
if row > maxRows {
|
||||
col++
|
||||
row = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *menuView) item(h hint) string {
|
||||
i, err := strconv.Atoi(h.mnemonic)
|
||||
if err == nil {
|
||||
return fmt.Sprintf(menuIndexFmt, i, resource.Truncate(h.display, 14))
|
||||
}
|
||||
|
||||
var s string
|
||||
// if strings.ToLower(h.display)[0] == h.mnemonic[0] {
|
||||
// s = fmt.Sprintf(menuFmt, strings.ToUpper(h.mnemonic), h.display[1:])
|
||||
// } else {
|
||||
s = fmt.Sprintf(menuSepFmt, strings.ToUpper(h.mnemonic), h.display)
|
||||
// }
|
||||
return s
|
||||
}
|
||||
|
||||
func (a keyActions) toHints() hints {
|
||||
kk := make([]int, 0, len(a))
|
||||
for k := range a {
|
||||
kk = append(kk, int(k))
|
||||
}
|
||||
// sort.Ints(kk)
|
||||
|
||||
hh := make(hints, 0, len(a))
|
||||
for _, k := range kk {
|
||||
if name, ok := tcell.KeyNames[tcell.Key(k)]; ok {
|
||||
if name == "Backspace2" {
|
||||
name = "delete"
|
||||
}
|
||||
hh = append(hh, hint{
|
||||
mnemonic: name,
|
||||
display: a[tcell.Key(k)].description})
|
||||
}
|
||||
}
|
||||
return hh
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Key mapping Constants
|
||||
|
||||
// Defines numeric keys for container actions
|
||||
const (
|
||||
Key0 int32 = iota + 48
|
||||
|
|
@ -75,90 +192,3 @@ var numKeys = map[int]int32{
|
|||
8: Key8,
|
||||
9: Key9,
|
||||
}
|
||||
|
||||
type (
|
||||
keyboardHandler func(*tcell.EventKey)
|
||||
|
||||
hint struct {
|
||||
mnemonic, display string
|
||||
}
|
||||
hints []hint
|
||||
|
||||
hinter interface {
|
||||
hints() hints
|
||||
}
|
||||
|
||||
keyAction struct {
|
||||
description string
|
||||
action keyboardHandler
|
||||
}
|
||||
keyActions map[tcell.Key]keyAction
|
||||
|
||||
menuView struct {
|
||||
*tview.Grid
|
||||
}
|
||||
)
|
||||
|
||||
func newKeyHandler(d string, a keyboardHandler) keyAction {
|
||||
return keyAction{description: d, action: a}
|
||||
}
|
||||
|
||||
func newMenuView() *menuView {
|
||||
v := menuView{tview.NewGrid()}
|
||||
v.SetGap(0, 1)
|
||||
return &v
|
||||
}
|
||||
|
||||
func (v *menuView) setMenu(hh hints) {
|
||||
v.Clear()
|
||||
v.SetRows(1, 1, 1, 1)
|
||||
v.SetColumns(colLen, colLen)
|
||||
isNS := true
|
||||
var row, col int
|
||||
for _, h := range hh {
|
||||
// Reset cols for namespace menus...
|
||||
if len(h.mnemonic) == 1 && isNS {
|
||||
col++
|
||||
row = 0
|
||||
isNS = false
|
||||
}
|
||||
v.AddItem(v.item(h), row, col, 1, 1, 1, 1, false)
|
||||
row++
|
||||
if row > maxRows {
|
||||
col++
|
||||
row = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *menuView) item(h hint) tview.Primitive {
|
||||
c := tview.NewTextView()
|
||||
c.SetDynamicColors(true)
|
||||
var s string
|
||||
if i, err := strconv.Atoi(h.mnemonic); err != nil {
|
||||
if strings.ToLower(h.display)[0] == h.mnemonic[0] {
|
||||
s = fmt.Sprintf(menuFmt, strings.ToUpper(h.mnemonic), h.display[1:])
|
||||
} else {
|
||||
s = fmt.Sprintf(menuSepFmt, strings.ToUpper(h.mnemonic), h.display)
|
||||
}
|
||||
} else {
|
||||
s = fmt.Sprintf(menuIndexFmt, i, resource.Truncate(h.display, 14))
|
||||
}
|
||||
c.SetText(s)
|
||||
return c
|
||||
}
|
||||
|
||||
func (a keyActions) toHints() hints {
|
||||
kk := make([]int, 0, len(a))
|
||||
for k := range a {
|
||||
kk = append(kk, int(k))
|
||||
}
|
||||
sort.Ints(kk)
|
||||
hh := make(hints, 0, len(a))
|
||||
for _, k := range kk {
|
||||
hh = append(hh, hint{
|
||||
mnemonic: tcell.KeyNames[tcell.Key(k)],
|
||||
display: a[tcell.Key(k)].description})
|
||||
}
|
||||
return hh
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,18 @@ package views
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/resource"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
const (
|
||||
favNSIndicator = "+"
|
||||
defaultNSIndicator = "(*)"
|
||||
deltaNSIndicator = "(𝜟)"
|
||||
)
|
||||
|
||||
type namespaceView struct {
|
||||
*resourceView
|
||||
}
|
||||
|
|
@ -26,10 +33,18 @@ func (v *namespaceView) useNamespace(*tcell.EventKey) {
|
|||
return
|
||||
}
|
||||
|
||||
k9sCfg.K9s.Namespace.Active = v.selectedItem
|
||||
ns := v.selectedItem
|
||||
for _, i := range []string{deltaNSIndicator, favNSIndicator, defaultNSIndicator} {
|
||||
if strings.HasSuffix(ns, i) {
|
||||
ns = strings.TrimRight(ns, i)
|
||||
}
|
||||
}
|
||||
v.refresh()
|
||||
|
||||
k9sCfg.K9s.Namespace.Active = ns
|
||||
k9sCfg.addFavNS(v.selectedItem)
|
||||
k9sCfg.validateAndSave()
|
||||
v.app.flash(flashInfo, fmt.Sprintf("Setting namespace `%s as your default namespace", v.selectedItem))
|
||||
v.app.flash(flashInfo, fmt.Sprintf("Setting namespace `%s as your default namespace", ns))
|
||||
}
|
||||
|
||||
func (v *namespaceView) extraActions(aa keyActions) {
|
||||
|
|
@ -46,8 +61,13 @@ func (v *namespaceView) decorate(data resource.TableData) resource.TableData {
|
|||
}
|
||||
|
||||
for k, v := range data.Rows {
|
||||
if inList(k9sCfg.K9s.Namespace.Favorites, k) {
|
||||
v.Fields[0] += "+"
|
||||
v.Action = resource.Unchanged
|
||||
}
|
||||
|
||||
if k9sCfg.K9s.Namespace.Active == k {
|
||||
v.Fields[0] = v.Fields[0] + "*"
|
||||
v.Fields[0] += "(*)"
|
||||
v.Action = resource.Unchanged
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/resource"
|
||||
|
|
@ -16,10 +17,7 @@ import (
|
|||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
noSelection = ""
|
||||
maxNamespaces = 5
|
||||
)
|
||||
const noSelection = ""
|
||||
|
||||
type (
|
||||
details interface {
|
||||
|
|
@ -38,7 +36,7 @@ type (
|
|||
selectedItem string
|
||||
namespaces map[int]string
|
||||
selectedNS string
|
||||
suspendUpdate bool
|
||||
update sync.Mutex
|
||||
list resource.List
|
||||
extraActionsFn func(keyActions)
|
||||
decorateDataFn func(resource.TableData) resource.TableData
|
||||
|
|
@ -54,10 +52,12 @@ func newResourceView(title string, app *appView, list resource.List, c colorerFn
|
|||
Pages: tview.NewPages(),
|
||||
}
|
||||
|
||||
table := newTableView(v.title, list.SortFn())
|
||||
table.SetColorer(c)
|
||||
table.SetSelectionChangedFunc(v.selChanged)
|
||||
v.AddPage(v.list.GetName(), table, true, true)
|
||||
tv := newTableView(app, v.title, list.SortFn())
|
||||
{
|
||||
tv.SetColorer(c)
|
||||
tv.table.SetSelectionChangedFunc(v.selChanged)
|
||||
}
|
||||
v.AddPage(v.list.GetName(), tv, true, true)
|
||||
|
||||
var xray details
|
||||
if list.HasXRay() {
|
||||
|
|
@ -95,15 +95,13 @@ func (v *resourceView) init(ctx context.Context, ns string) {
|
|||
log.Debugf("%s watcher canceled!", v.title)
|
||||
return
|
||||
case <-time.After(time.Duration(initTick) * time.Second):
|
||||
if !v.isSuspended() {
|
||||
v.refresh()
|
||||
}
|
||||
v.refresh()
|
||||
initTick = float64(k9sCfg.K9s.RefreshRate)
|
||||
}
|
||||
}
|
||||
}(ctx)
|
||||
v.refreshActions()
|
||||
v.CurrentPage().Item.(*tableView).Select(0, 0)
|
||||
v.CurrentPage().Item.(*tableView).table.Select(0, 0)
|
||||
}
|
||||
|
||||
func (v *resourceView) selChanged(r, c int) {
|
||||
|
|
@ -196,7 +194,7 @@ func (v *resourceView) switchNamespace(evt *tcell.EventKey) {
|
|||
}
|
||||
|
||||
func (v *resourceView) doSwitchNamespace(ns string) {
|
||||
v.suspend()
|
||||
v.update.Lock()
|
||||
{
|
||||
if ns == noSelection {
|
||||
ns = resource.AllNamespace
|
||||
|
|
@ -204,50 +202,43 @@ func (v *resourceView) doSwitchNamespace(ns string) {
|
|||
v.selectedNS = ns
|
||||
v.app.flash(flashInfo, fmt.Sprintf("Viewing `%s namespace...", ns))
|
||||
v.list.SetNamespace(v.selectedNS)
|
||||
v.refresh()
|
||||
}
|
||||
v.resume()
|
||||
v.update.Unlock()
|
||||
v.refresh()
|
||||
v.selectItem(0, 0)
|
||||
v.getTV().resetTitle()
|
||||
v.getTV().Select(0, 0)
|
||||
v.app.resetCmd()
|
||||
v.getTV().table.Select(0, 0)
|
||||
v.app.cmdBuff.reset()
|
||||
k9sCfg.K9s.Namespace.Active = v.selectedNS
|
||||
k9sCfg.validateAndSave()
|
||||
}
|
||||
|
||||
// Utils...
|
||||
|
||||
func (v *resourceView) suspend() {
|
||||
v.suspendUpdate = true
|
||||
}
|
||||
|
||||
func (v *resourceView) resume() {
|
||||
v.suspendUpdate = false
|
||||
}
|
||||
|
||||
func (v *resourceView) isSuspended() bool {
|
||||
return v.suspendUpdate
|
||||
}
|
||||
|
||||
func (v *resourceView) refresh() {
|
||||
if _, ok := v.CurrentPage().Item.(*tableView); !ok {
|
||||
return
|
||||
}
|
||||
if v.list.Namespaced() {
|
||||
v.list.SetNamespace(v.selectedNS)
|
||||
}
|
||||
if err := v.list.Reconcile(); err != nil {
|
||||
v.app.flash(flashErr, err.Error())
|
||||
}
|
||||
|
||||
v.refreshActions()
|
||||
data := v.list.Data()
|
||||
if v.decorateDataFn != nil {
|
||||
data = v.decorateDataFn(data)
|
||||
v.update.Lock()
|
||||
{
|
||||
if v.list.Namespaced() {
|
||||
v.list.SetNamespace(v.selectedNS)
|
||||
}
|
||||
if err := v.list.Reconcile(); err != nil {
|
||||
v.app.flash(flashErr, err.Error())
|
||||
}
|
||||
data := v.list.Data()
|
||||
if v.decorateDataFn != nil {
|
||||
data = v.decorateDataFn(data)
|
||||
}
|
||||
v.getTV().update(data)
|
||||
|
||||
v.refreshActions()
|
||||
v.app.infoView.refresh()
|
||||
v.app.Draw()
|
||||
}
|
||||
v.getTV().update(data)
|
||||
v.app.infoView.refresh()
|
||||
v.app.Draw()
|
||||
v.update.Unlock()
|
||||
}
|
||||
|
||||
func (v *resourceView) getTV() *tableView {
|
||||
|
|
@ -267,22 +258,22 @@ func (v *resourceView) selectItem(r, c int) {
|
|||
t := v.getTV()
|
||||
switch v.list.GetNamespace() {
|
||||
case resource.NotNamespaced:
|
||||
v.selectedItem = strings.TrimSpace(t.GetCell(r, 0).Text)
|
||||
v.selectedItem = strings.TrimSpace(t.table.GetCell(r, 0).Text)
|
||||
case resource.AllNamespaces:
|
||||
v.selectedItem = path.Join(
|
||||
strings.TrimSpace(t.GetCell(r, 0).Text),
|
||||
strings.TrimSpace(t.GetCell(r, 1).Text),
|
||||
strings.TrimSpace(t.table.GetCell(r, 0).Text),
|
||||
strings.TrimSpace(t.table.GetCell(r, 1).Text),
|
||||
)
|
||||
default:
|
||||
v.selectedItem = path.Join(
|
||||
v.selectedNS,
|
||||
strings.TrimSpace(t.GetCell(r, 0).Text),
|
||||
strings.TrimSpace(t.table.GetCell(r, 0).Text),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *resourceView) switchPage(p string) {
|
||||
v.suspend()
|
||||
v.update.Lock()
|
||||
{
|
||||
v.SwitchToPage(p)
|
||||
h := v.GetPrimitive(p).(hinter)
|
||||
|
|
@ -290,7 +281,7 @@ func (v *resourceView) switchPage(p string) {
|
|||
v.app.setHints(h.hints())
|
||||
v.app.SetFocus(v.CurrentPage().Item)
|
||||
}
|
||||
v.resume()
|
||||
v.update.Unlock()
|
||||
}
|
||||
|
||||
func (v *resourceView) rowSelected() bool {
|
||||
|
|
@ -314,15 +305,21 @@ func (v *resourceView) refreshActions() {
|
|||
return
|
||||
}
|
||||
|
||||
if v.list.Namespaced() && !v.list.AllNamespaces() && !inNSList(nn, v.list.GetNamespace()) {
|
||||
v.list.SetNamespace(resource.DefaultNamespace)
|
||||
if v.list.Namespaced() && !v.list.AllNamespaces() {
|
||||
if !inNSList(nn, v.list.GetNamespace()) {
|
||||
v.list.SetNamespace(resource.DefaultNamespace)
|
||||
}
|
||||
}
|
||||
|
||||
aa := keyActions{}
|
||||
if v.list.Access(resource.NamespaceAccess) {
|
||||
v.namespaces = make(map[int]string, maxNamespaces)
|
||||
v.namespaces = make(map[int]string, maxFavorites)
|
||||
var i int
|
||||
for _, n := range k9sCfg.K9s.Namespace.Favorites {
|
||||
if i > maxFavorites {
|
||||
break
|
||||
}
|
||||
|
||||
if n == resource.AllNamespace {
|
||||
aa[tcell.Key(numKeys[i])] = newKeyHandler(resource.AllNamespace, v.switchNamespace)
|
||||
v.namespaces[i] = resource.AllNamespaces
|
||||
|
|
@ -338,17 +335,15 @@ func (v *resourceView) refreshActions() {
|
|||
k9sCfg.rmFavNS(n)
|
||||
k9sCfg.validateAndSave()
|
||||
}
|
||||
if i > maxNamespaces {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if v.list.Access(resource.EditAccess) {
|
||||
aa[tcell.KeyCtrlE] = newKeyHandler("Edit", v.edit)
|
||||
}
|
||||
|
||||
if v.list.Access(resource.DeleteAccess) {
|
||||
aa[tcell.KeyCtrlD] = newKeyHandler("Delete", v.delete)
|
||||
aa[tcell.KeyBackspace2] = newKeyHandler("Delete", v.delete)
|
||||
}
|
||||
if v.list.Access(resource.ViewAccess) {
|
||||
aa[tcell.KeyCtrlV] = newKeyHandler("View", v.view)
|
||||
|
|
@ -357,13 +352,14 @@ func (v *resourceView) refreshActions() {
|
|||
aa[tcell.KeyCtrlX] = newKeyHandler("Describe", v.describe)
|
||||
}
|
||||
|
||||
aa[tcell.KeyCtrlQ] = newKeyHandler("Quit", v.app.quit)
|
||||
aa[tcell.KeyCtrlA] = newKeyHandler("Aliases", v.app.help)
|
||||
|
||||
if v.extraActionsFn != nil {
|
||||
v.extraActionsFn(aa)
|
||||
}
|
||||
|
||||
t := v.getTV()
|
||||
{
|
||||
t.setActions(aa)
|
||||
v.app.setHints(t.hints())
|
||||
}
|
||||
t.setActions(aa)
|
||||
v.app.setHints(t.hints())
|
||||
}
|
||||
|
|
|
|||
148
views/table.go
148
views/table.go
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/derailed/k9s/resource"
|
||||
"github.com/gdamore/tcell"
|
||||
|
|
@ -17,87 +18,110 @@ const (
|
|||
|
||||
type (
|
||||
tableView struct {
|
||||
*tview.Table
|
||||
baseTitle string
|
||||
currentNS string
|
||||
actions keyActions
|
||||
colorer colorerFn
|
||||
sortFn resource.SortFn
|
||||
parent *resourceView
|
||||
cmdBuffer []rune
|
||||
data resource.TableData
|
||||
searchMode bool
|
||||
filtered bool
|
||||
*tview.Flex
|
||||
|
||||
app *appView
|
||||
baseTitle string
|
||||
currentNS string
|
||||
refresh sync.Mutex
|
||||
actions keyActions
|
||||
colorer colorerFn
|
||||
sortFn resource.SortFn
|
||||
table *tview.Table
|
||||
data resource.TableData
|
||||
cmdBuff *cmdBuff
|
||||
}
|
||||
)
|
||||
|
||||
func newTableView(title string, sortFn resource.SortFn) *tableView {
|
||||
v := tableView{Table: tview.NewTable(), baseTitle: title, sortFn: sortFn}
|
||||
v.SetBorder(true)
|
||||
v.SetBorderColor(tcell.ColorDodgerBlue)
|
||||
v.SetBorderAttributes(tcell.AttrBold)
|
||||
v.SetBorderPadding(0, 0, 1, 1)
|
||||
v.SetSelectable(true, false)
|
||||
v.SetSelectedStyle(tcell.ColorBlack, tcell.ColorAqua, tcell.AttrBold)
|
||||
v.SetInputCapture(v.keyboard)
|
||||
func newTableView(app *appView, title string, sortFn resource.SortFn) *tableView {
|
||||
v := tableView{app: app, Flex: tview.NewFlex().SetDirection(tview.FlexRow)}
|
||||
{
|
||||
v.baseTitle = title
|
||||
v.sortFn = sortFn
|
||||
v.SetBorder(true)
|
||||
v.SetBorderColor(tcell.ColorDodgerBlue)
|
||||
v.SetBorderAttributes(tcell.AttrBold)
|
||||
v.SetBorderPadding(0, 0, 1, 1)
|
||||
v.cmdBuff = newCmdBuff('/')
|
||||
}
|
||||
|
||||
v.cmdBuff.addListener(app.cmdView)
|
||||
v.cmdBuff.reset()
|
||||
|
||||
v.table = tview.NewTable()
|
||||
{
|
||||
v.table.SetSelectable(true, false)
|
||||
v.table.SetSelectedStyle(tcell.ColorBlack, tcell.ColorAqua, tcell.AttrBold)
|
||||
v.table.SetInputCapture(v.keyboard)
|
||||
}
|
||||
|
||||
v.AddItem(v.table, 0, 1, true)
|
||||
return &v
|
||||
}
|
||||
|
||||
func (v *tableView) setDeleted() {
|
||||
r, _ := v.GetSelection()
|
||||
cols := v.GetColumnCount()
|
||||
r, _ := v.table.GetSelection()
|
||||
cols := v.table.GetColumnCount()
|
||||
for x := 0; x < cols; x++ {
|
||||
v.GetCell(r, x).SetAttributes(tcell.AttrDim)
|
||||
v.table.GetCell(r, x).SetAttributes(tcell.AttrDim)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *tableView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||
key := evt.Key()
|
||||
if evt.Key() == tcell.KeyRune {
|
||||
if evt.Rune() == '/' {
|
||||
v.searchMode = true
|
||||
v.cmdBuffer = []rune{}
|
||||
} else {
|
||||
if v.searchMode {
|
||||
v.cmdBuffer = append([]rune(v.cmdBuffer), evt.Rune())
|
||||
if v.cmdBuff.isActive() {
|
||||
v.cmdBuff.add(evt.Rune())
|
||||
}
|
||||
|
||||
switch evt.Rune() {
|
||||
case v.cmdBuff.hotKey:
|
||||
if !v.cmdBuff.isActive() {
|
||||
v.cmdBuff.setActive(true)
|
||||
}
|
||||
return evt
|
||||
}
|
||||
key = tcell.Key(evt.Rune())
|
||||
}
|
||||
|
||||
if a, ok := v.actions[key]; ok {
|
||||
a.action(evt)
|
||||
return nil
|
||||
return evt
|
||||
}
|
||||
|
||||
switch evt.Key() {
|
||||
case tcell.KeyEnter:
|
||||
if len(v.cmdBuffer) > 0 {
|
||||
v.filtered = true
|
||||
if v.cmdBuff.isActive() && !v.cmdBuff.empty() {
|
||||
v.filter()
|
||||
v.searchMode = false
|
||||
}
|
||||
evt = nil
|
||||
v.cmdBuff.setActive(false)
|
||||
case tcell.KeyEsc:
|
||||
v.filtered, v.searchMode = false, false
|
||||
v.cmdBuffer = []rune{}
|
||||
evt = nil
|
||||
v.cmdBuff.reset()
|
||||
v.filter()
|
||||
case tcell.KeyBackspace2:
|
||||
if v.cmdBuff.isActive() {
|
||||
v.cmdBuff.del()
|
||||
}
|
||||
}
|
||||
return evt
|
||||
}
|
||||
|
||||
func (v *tableView) filter() {
|
||||
v.filterData(string(v.cmdBuffer))
|
||||
v.filterData(v.cmdBuff)
|
||||
}
|
||||
|
||||
func (v *tableView) filterData(filter string) {
|
||||
func (v *tableView) filterData(filter fmt.Stringer) {
|
||||
filtered := resource.TableData{
|
||||
Header: v.data.Header,
|
||||
Rows: resource.RowEvents{},
|
||||
Namespace: v.data.Namespace,
|
||||
}
|
||||
|
||||
rx := regexp.MustCompile(filter)
|
||||
rx, err := regexp.Compile(filter.String())
|
||||
if err != nil {
|
||||
v.app.flash(flashErr, "Invalid search expression")
|
||||
return
|
||||
}
|
||||
for k, row := range v.data.Rows {
|
||||
f := strings.Join(row.Fields, " ")
|
||||
if rx.MatchString(f) {
|
||||
|
|
@ -137,33 +161,38 @@ func (v *tableView) resetTitle() {
|
|||
|
||||
switch v.currentNS {
|
||||
case resource.NotNamespaced:
|
||||
title = fmt.Sprintf(titleFmt, v.baseTitle, v.GetRowCount()-1)
|
||||
title = fmt.Sprintf(titleFmt, v.baseTitle, v.table.GetRowCount()-1)
|
||||
default:
|
||||
ns := v.currentNS
|
||||
if v.currentNS == resource.AllNamespaces {
|
||||
ns = resource.AllNamespace
|
||||
}
|
||||
title = fmt.Sprintf(nsTitleFmt, v.baseTitle, ns, v.GetRowCount()-1)
|
||||
title = fmt.Sprintf(nsTitleFmt, v.baseTitle, ns, v.table.GetRowCount()-1)
|
||||
}
|
||||
|
||||
if v.filtered {
|
||||
title += fmt.Sprintf("<[green::b]/%s[aqua::]> ", string(v.cmdBuffer))
|
||||
if !v.cmdBuff.empty() {
|
||||
title += fmt.Sprintf("<[green::b]/%s[aqua::]> ", v.cmdBuff)
|
||||
}
|
||||
v.SetTitle(title)
|
||||
}
|
||||
|
||||
// Update table content
|
||||
func (v *tableView) update(data resource.TableData) {
|
||||
v.data = data
|
||||
if v.filtered {
|
||||
v.filter()
|
||||
} else {
|
||||
v.doUpdate(data)
|
||||
v.refresh.Lock()
|
||||
{
|
||||
v.data = data
|
||||
if !v.cmdBuff.empty() {
|
||||
v.filter()
|
||||
} else {
|
||||
v.doUpdate(data)
|
||||
}
|
||||
v.resetTitle()
|
||||
}
|
||||
v.refresh.Unlock()
|
||||
}
|
||||
|
||||
func (v *tableView) doUpdate(data resource.TableData) {
|
||||
v.Clear()
|
||||
v.table.Clear()
|
||||
v.currentNS = data.Namespace
|
||||
|
||||
var row int
|
||||
|
|
@ -176,7 +205,7 @@ func (v *tableView) doUpdate(data resource.TableData) {
|
|||
}
|
||||
c.SetTextColor(tcell.ColorWhite)
|
||||
}
|
||||
v.SetCell(row, col, c)
|
||||
v.table.SetCell(row, col, c)
|
||||
}
|
||||
row++
|
||||
|
||||
|
|
@ -199,9 +228,22 @@ func (v *tableView) doUpdate(data resource.TableData) {
|
|||
}
|
||||
c.SetTextColor(fgColor)
|
||||
}
|
||||
v.SetCell(row, col, c)
|
||||
v.table.SetCell(row, col, c)
|
||||
}
|
||||
row++
|
||||
}
|
||||
v.resetTitle()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Event listeners...
|
||||
|
||||
func (v *tableView) changed(s string) {
|
||||
}
|
||||
|
||||
func (v *tableView) active(b bool) {
|
||||
if b {
|
||||
v.SetBorderColor(tcell.ColorRed)
|
||||
return
|
||||
}
|
||||
v.SetBorderColor(tcell.ColorDodgerBlue)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue