added search/filter feature + support init container

mine
derailed 2019-02-22 17:36:09 -07:00
parent 2cf5ec8877
commit efb95866b1
39 changed files with 993 additions and 921 deletions

81
go.mod
View File

@ -1,71 +1,48 @@
module github.com/derailed/k9s module github.com/derailed/k9s
// replace github.com/k8sland/tview => /Users/fernand/go_wk/k8sland/src/github.com/k8sland/tview // replace github.com/derailed/tview => /Users/fernand/go_wk/derailed/src/github.com/derailed/tview
require ( require (
cloud.google.com/go v0.34.0 contrib.go.opencensus.io/exporter/ocagent v0.4.3 // indirect
contrib.go.opencensus.io/exporter/ocagent v0.4.3 github.com/Azure/go-autorest v11.4.0+incompatible // indirect
github.com/Azure/go-autorest v11.4.0+incompatible github.com/derailed/tview v0.1.1
github.com/census-instrumentation/opencensus-proto v0.1.0-0.20181214143942-ba49f56771b8 github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/davecgh/go-spew v1.1.1 github.com/evanphx/json-patch v4.1.0+incompatible // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/evanphx/json-patch v4.1.0+incompatible
github.com/fatih/camelcase v1.0.0 // indirect github.com/fatih/camelcase v1.0.0 // indirect
github.com/gdamore/encoding v1.0.0 github.com/gdamore/tcell v1.1.1
github.com/gdamore/tcell v1.1.0 github.com/gogo/protobuf v1.1.1 // indirect
github.com/gogo/protobuf v1.1.1 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect
github.com/golang/protobuf v1.2.0 github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c github.com/googleapis/gnostic v0.2.0 // indirect
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf github.com/gophercloud/gophercloud v0.0.0-20190206201033-83b528acebb4 // indirect
github.com/googleapis/gnostic v0.2.0 github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
github.com/gophercloud/gophercloud v0.0.0-20190206201033-83b528acebb4 github.com/imdario/mergo v0.3.6 // indirect
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 github.com/json-iterator/go v1.1.5 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.6.2
github.com/imdario/mergo v0.3.6
github.com/inconshreveable/mousetrap v1.0.0
github.com/json-iterator/go v1.1.5
github.com/jtolds/gls v4.2.1+incompatible // indirect
github.com/k8sland/tview v0.1.1
github.com/kr/pretty v0.1.0 // indirect github.com/kr/pretty v0.1.0 // indirect
github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/mattn/go-runewidth v0.0.4 github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd github.com/onsi/gomega v1.4.3 // indirect
github.com/modern-go/reflect2 v1.0.1 github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/onsi/gomega v1.4.3
github.com/peterbourgon/diskv v2.0.1+incompatible
github.com/petergtz/pegomock v0.0.0-20181206220228-b113d17a7e81 github.com/petergtz/pegomock v0.0.0-20181206220228-b113d17a7e81
github.com/pmezard/go-difflib v1.0.0 github.com/sirupsen/logrus v1.3.0
github.com/sirupsen/logrus v1.2.0
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
github.com/spf13/cobra v0.0.3 github.com/spf13/cobra v0.0.3
github.com/spf13/pflag v1.0.3 github.com/spf13/pflag v1.0.3 // indirect
github.com/stretchr/testify v1.2.2 github.com/stretchr/testify v1.2.2
go.opencensus.io v0.19.0 go.opencensus.io v0.19.0 // indirect
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd // indirect
golang.org/x/net v0.0.0-20181217023233-e147a9138326 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 // indirect
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 gopkg.in/inf.v0 v0.9.1 // indirect
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f
golang.org/x/sys v0.0.0-20181218192612-074acd46bca6
golang.org/x/text v0.3.0
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2
google.golang.org/appengine v1.3.0
google.golang.org/genproto v0.0.0-20181219182458-5a97ab628bfb
google.golang.org/grpc v1.17.0
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 // indirect
gopkg.in/inf.v0 v0.9.1
gopkg.in/yaml.v2 v2.2.2 gopkg.in/yaml.v2 v2.2.2
k8s.io/api v0.0.0-20190202010724-74b699b93c15 k8s.io/api v0.0.0-20190202010724-74b699b93c15
k8s.io/apiextensions-apiserver v0.0.0-20190219174952-73ef971883ab
k8s.io/apimachinery v0.0.0-20190207091153-095b9d203467 k8s.io/apimachinery v0.0.0-20190207091153-095b9d203467
k8s.io/cli-runtime v0.0.0-20190207094101-a32b78e5dd0a k8s.io/cli-runtime v0.0.0-20190207094101-a32b78e5dd0a
k8s.io/client-go v10.0.0+incompatible k8s.io/client-go v10.0.0+incompatible
k8s.io/klog v0.1.0 k8s.io/klog v0.1.0 // indirect
k8s.io/kube-openapi v0.0.0-20190215190454-ea82251f3668 // indirect k8s.io/kube-openapi v0.0.0-20190215190454-ea82251f3668 // indirect
k8s.io/kubernetes v1.13.3 k8s.io/kubernetes v1.13.3
k8s.io/metrics v0.0.0-20181121073115-d8618695b08f k8s.io/metrics v0.0.0-20181121073115-d8618695b08f
k8s.io/utils v0.0.0-20190212002617-cdba02414f76 // indirect k8s.io/utils v0.0.0-20190212002617-cdba02414f76 // indirect
sigs.k8s.io/structured-merge-diff v0.0.0-20190130003954-e5e029740eb8 sigs.k8s.io/structured-merge-diff v0.0.0-20190130003954-e5e029740eb8 // indirect
sigs.k8s.io/yaml v1.1.0 sigs.k8s.io/yaml v1.1.0 // indirect
) )

36
go.sum
View File

@ -13,20 +13,24 @@ github.com/census-instrumentation/opencensus-proto v0.1.0-0.20181214143942-ba49f
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/derailed/tview v0.1.1 h1:s9rGoufUkZYsIFCfV3M23yHxTKa7Rt6n+QFaOgnirpk=
github.com/derailed/tview v0.1.1/go.mod h1:WRYVfgb2PBMLZ/muaSpOc/4H4fYsOPnHOaGnBoJ+hGE=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/evanphx/json-patch v4.1.0+incompatible h1:K1MDoo4AZ4wU0GIU/fPmtZg7VpzLjCxu+UwBD1FvwOc= github.com/evanphx/json-patch v4.1.0+incompatible h1:K1MDoo4AZ4wU0GIU/fPmtZg7VpzLjCxu+UwBD1FvwOc=
github.com/evanphx/json-patch v4.1.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.1.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.1.0 h1:RbQgl7jukmdqROeNcKps7R2YfDCQbWkOd1BwdXrxfr4= github.com/gdamore/tcell v1.1.1 h1:U73YL+jMem2XfhvaIUfPO6MpJawaG92B2funXVb9qLs=
github.com/gdamore/tcell v1.1.0/go.mod h1:tqyG50u7+Ctv1w5VX67kLzKcj9YXR/JSBZQq/+mLl1A= github.com/gdamore/tcell v1.1.1/go.mod h1:K1udHkiR3cOtlpKG5tZPD5XxrF7v2y7lDq7Whcj+xkQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@ -35,6 +39,7 @@ github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
@ -42,29 +47,29 @@ github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhp
github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/gophercloud/gophercloud v0.0.0-20190206201033-83b528acebb4 h1:wAcVWwS69gs5c6cFkCa/ns/eaL2gC761nF8Ugvd1dGw= github.com/gophercloud/gophercloud v0.0.0-20190206201033-83b528acebb4 h1:wAcVWwS69gs5c6cFkCa/ns/eaL2gC761nF8Ugvd1dGw=
github.com/gophercloud/gophercloud v0.0.0-20190206201033-83b528acebb4/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= github.com/gophercloud/gophercloud v0.0.0-20190206201033-83b528acebb4/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/grpc-ecosystem/grpc-gateway v1.6.2 h1:8KyC64BiO8ndiGHY5DlFWWdangUPC9QHPakFRre/Ud0= github.com/grpc-ecosystem/grpc-gateway v1.6.2 h1:8KyC64BiO8ndiGHY5DlFWWdangUPC9QHPakFRre/Ud0=
github.com/grpc-ecosystem/grpc-gateway v1.6.2/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/grpc-ecosystem/grpc-gateway v1.6.2/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/k8sland/tview v0.1.1 h1:732F8kcz5EjUAsFTZJ5BkJx3n34+EwiQRuDaegeS2yU=
github.com/k8sland/tview v0.1.1/go.mod h1:PwEtOCvGYNgUA2FQuciQBKB6igksu4GHtq3GY6vOkQo=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08 h1:5MnxBC15uMxFv5FY/J/8vzyaBiArCOkMdFT9Jsw78iY= github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08 h1:5MnxBC15uMxFv5FY/J/8vzyaBiArCOkMdFT9Jsw78iY=
github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4= github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@ -72,6 +77,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
@ -91,11 +97,9 @@ github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7q
github.com/prometheus/common v0.0.0-20181218105931-67670fe90761/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20181218105931-67670fe90761/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/rivo/tview v0.0.0-20190124120153-84fdb36408f3/go.mod h1:J4W+hErFfITUbyFAEXizpmkuxX7ZN56dopxHB4XQhMw= github.com/rivo/tview v0.0.0-20190213202703-b373355e9db4/go.mod h1:J4W+hErFfITUbyFAEXizpmkuxX7ZN56dopxHB4XQhMw=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
@ -140,6 +144,7 @@ google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+
google.golang.org/api v0.0.0-20181220000619-583d854617af h1:iQMS7JKv/0w/iiWf1M49Cg3dmOkBoBZT5KheqPDpaac= google.golang.org/api v0.0.0-20181220000619-583d854617af h1:iQMS7JKv/0w/iiWf1M49Cg3dmOkBoBZT5KheqPDpaac=
google.golang.org/api v0.0.0-20181220000619-583d854617af/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181220000619-583d854617af/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@ -150,12 +155,16 @@ google.golang.org/grpc v1.15.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9M
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0 h1:TRJYBgMclJvGYn2rIMjj+h9KtMt5r1Ij7ODVRIZkwhk= google.golang.org/grpc v1.17.0 h1:TRJYBgMclJvGYn2rIMjj+h9KtMt5r1Ij7ODVRIZkwhk=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 h1:FVCohIoYO7IJoDDVpV2pdq7SgrMH6wHnuTyrdrxJNoY=
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw= gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
@ -164,7 +173,6 @@ honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.0.0-20190202010724-74b699b93c15 h1:AoUGjnJ3PJMFz+Rkp4lx3X+6mPUnY1MESJhbUSGX+pc= k8s.io/api v0.0.0-20190202010724-74b699b93c15 h1:AoUGjnJ3PJMFz+Rkp4lx3X+6mPUnY1MESJhbUSGX+pc=
k8s.io/api v0.0.0-20190202010724-74b699b93c15/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA= k8s.io/api v0.0.0-20190202010724-74b699b93c15/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
k8s.io/apiextensions-apiserver v0.0.0-20190219174952-73ef971883ab/go.mod h1:IxkesAMoaCRoLrPJdZNZUQp9NfZnzqaVzLhb2VEQzXE=
k8s.io/apimachinery v0.0.0-20190207091153-095b9d203467 h1:zmz9UYvvXrK/B8EDqFuqreJEaXbIWdzEkNgWrN/Cd3o= k8s.io/apimachinery v0.0.0-20190207091153-095b9d203467 h1:zmz9UYvvXrK/B8EDqFuqreJEaXbIWdzEkNgWrN/Cd3o=
k8s.io/apimachinery v0.0.0-20190207091153-095b9d203467/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= k8s.io/apimachinery v0.0.0-20190207091153-095b9d203467/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
k8s.io/cli-runtime v0.0.0-20190207094101-a32b78e5dd0a h1:MrGQxLLZ09Bl5hYYU9VlKnhY60bpPlYd9yXOPnxkdc0= k8s.io/cli-runtime v0.0.0-20190207094101-a32b78e5dd0a h1:MrGQxLLZ09Bl5hYYU9VlKnhY60bpPlYd9yXOPnxkdc0=
@ -173,11 +181,13 @@ k8s.io/client-go v10.0.0+incompatible h1:F1IqCqw7oMBzDkqlcBymRq1450wD0eNqLE9jzUr
k8s.io/client-go v10.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= k8s.io/client-go v10.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
k8s.io/klog v0.1.0 h1:I5HMfc/DtuVaGR1KPwUrTc476K8NCqNBldC7H4dYEzk= k8s.io/klog v0.1.0 h1:I5HMfc/DtuVaGR1KPwUrTc476K8NCqNBldC7H4dYEzk=
k8s.io/klog v0.1.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.1.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/kube-openapi v0.0.0-20190215190454-ea82251f3668 h1:M80qeWaBNOX2Uc4plRHcb6k+3YE5VWMaJXKZo+tX9aU=
k8s.io/kube-openapi v0.0.0-20190215190454-ea82251f3668/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= k8s.io/kube-openapi v0.0.0-20190215190454-ea82251f3668/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
k8s.io/kubernetes v1.13.3 h1:46t44D87wKtdKFgr/lXM60K8xPrW0wO67Woof3Vsv6E= k8s.io/kubernetes v1.13.3 h1:46t44D87wKtdKFgr/lXM60K8xPrW0wO67Woof3Vsv6E=
k8s.io/kubernetes v1.13.3/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= k8s.io/kubernetes v1.13.3/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
k8s.io/metrics v0.0.0-20181121073115-d8618695b08f h1:HyUoIBzks9xTaSnMJ6kv/SSmwaQQccokuiriu2cV0aA= k8s.io/metrics v0.0.0-20181121073115-d8618695b08f h1:HyUoIBzks9xTaSnMJ6kv/SSmwaQQccokuiriu2cV0aA=
k8s.io/metrics v0.0.0-20181121073115-d8618695b08f/go.mod h1:a25VAbm3QT3xiVl1jtoF1ueAKQM149UdZ+L93ePfV3M= k8s.io/metrics v0.0.0-20181121073115-d8618695b08f/go.mod h1:a25VAbm3QT3xiVl1jtoF1ueAKQM149UdZ+L93ePfV3M=
k8s.io/utils v0.0.0-20190212002617-cdba02414f76 h1:xj+3UbmNFpFTZmF/snXlV1oYutGHU1VmjfkfH1ugr6A=
k8s.io/utils v0.0.0-20190212002617-cdba02414f76/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0= k8s.io/utils v0.0.0-20190212002617-cdba02414f76/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0=
sigs.k8s.io/structured-merge-diff v0.0.0-20190130003954-e5e029740eb8 h1:UBkrbecoQliUCGP3Izc0NRKu877BV6VLT3lUykRJURM= sigs.k8s.io/structured-merge-diff v0.0.0-20190130003954-e5e029740eb8 h1:UBkrbecoQliUCGP3Izc0NRKu877BV6VLT3lUykRJURM=
sigs.k8s.io/structured-merge-diff v0.0.0-20190130003954-e5e029740eb8/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/structured-merge-diff v0.0.0-20190130003954-e5e029740eb8/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=

View File

@ -6,8 +6,6 @@ import (
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/views" "github.com/derailed/k9s/internal/views"
"github.com/gdamore/tcell"
"github.com/k8sland/tview"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
@ -82,7 +80,7 @@ func initK9sConfig() {
} }
ctx := cfg.CurrentContext ctx := cfg.CurrentContext
switch{ switch {
case isSet(k8sFlags.Context): case isSet(k8sFlags.Context):
ctx = *k8sFlags.Context ctx = *k8sFlags.Context
config.Root.K9s.CurrentContext = ctx config.Root.K9s.CurrentContext = ctx
@ -131,9 +129,6 @@ func run(cmd *cobra.Command, args []string) {
log.SetFormatter(&log.TextFormatter{FullTimestamp: true, ForceColors: true}) log.SetFormatter(&log.TextFormatter{FullTimestamp: true, ForceColors: true})
initK9s() initK9s()
initStyles()
initKeys()
app := views.NewApp() app := views.NewApp()
{ {
app.Init(version, refreshRate, k8sFlags) app.Init(version, refreshRate, k8sFlags)
@ -141,52 +136,6 @@ func run(cmd *cobra.Command, args []string) {
} }
} }
func initKeys() {
tcell.KeyNames[tcell.Key(views.Key0)] = "0"
tcell.KeyNames[tcell.Key(views.Key1)] = "1"
tcell.KeyNames[tcell.Key(views.Key2)] = "2"
tcell.KeyNames[tcell.Key(views.Key3)] = "3"
tcell.KeyNames[tcell.Key(views.Key4)] = "4"
tcell.KeyNames[tcell.Key(views.Key5)] = "5"
tcell.KeyNames[tcell.Key(views.Key6)] = "6"
tcell.KeyNames[tcell.Key(views.Key7)] = "7"
tcell.KeyNames[tcell.Key(views.Key8)] = "8"
tcell.KeyNames[tcell.Key(views.Key9)] = "9"
tcell.KeyNames[tcell.Key(views.KeyA)] = "a"
tcell.KeyNames[tcell.Key(views.KeyB)] = "b"
tcell.KeyNames[tcell.Key(views.KeyC)] = "c"
tcell.KeyNames[tcell.Key(views.KeyD)] = "d"
tcell.KeyNames[tcell.Key(views.KeyE)] = "e"
tcell.KeyNames[tcell.Key(views.KeyF)] = "f"
tcell.KeyNames[tcell.Key(views.KeyG)] = "g"
tcell.KeyNames[tcell.Key(views.KeyH)] = "h"
tcell.KeyNames[tcell.Key(views.KeyI)] = "i"
tcell.KeyNames[tcell.Key(views.KeyJ)] = "j"
tcell.KeyNames[tcell.Key(views.KeyK)] = "k"
tcell.KeyNames[tcell.Key(views.KeyL)] = "l"
tcell.KeyNames[tcell.Key(views.KeyM)] = "m"
tcell.KeyNames[tcell.Key(views.KeyN)] = "n"
tcell.KeyNames[tcell.Key(views.KeyO)] = "o"
tcell.KeyNames[tcell.Key(views.KeyP)] = "p"
tcell.KeyNames[tcell.Key(views.KeyQ)] = "q"
tcell.KeyNames[tcell.Key(views.KeyR)] = "r"
tcell.KeyNames[tcell.Key(views.KeyS)] = "s"
tcell.KeyNames[tcell.Key(views.KeyT)] = "t"
tcell.KeyNames[tcell.Key(views.KeyU)] = "u"
tcell.KeyNames[tcell.Key(views.KeyV)] = "v"
tcell.KeyNames[tcell.Key(views.KeyX)] = "x"
tcell.KeyNames[tcell.Key(views.KeyY)] = "y"
tcell.KeyNames[tcell.Key(views.KeyZ)] = "z"
tcell.KeyNames[tcell.Key(views.KeyHelp)] = "?"
}
func initStyles() {
tview.Styles.PrimitiveBackgroundColor = tcell.ColorBlack
tview.Styles.ContrastBackgroundColor = tcell.ColorBlack
tview.Styles.FocusColor = tcell.ColorLightSkyBlue
tview.Styles.BorderColor = tcell.ColorDodgerBlue
}
func initK8sFlags() { func initK8sFlags() {
k8sFlags = genericclioptions.NewConfigFlags(false) k8sFlags = genericclioptions.NewConfigFlags(false)
rootCmd.Flags().StringVar( rootCmd.Flags().StringVar(

View File

@ -78,6 +78,8 @@ func (c *Config) FavNamespaces() []string {
func (c *Config) SetActiveNamespace(ns string) { func (c *Config) SetActiveNamespace(ns string) {
if c.K9s.ActiveCluster() != nil { if c.K9s.ActiveCluster() != nil {
c.K9s.ActiveCluster().Namespace.SetActive(ns) c.K9s.ActiveCluster().Namespace.SetActive(ns)
} else {
log.Debug("Doh! no active cluster. unable to set active namespace")
} }
} }

View File

@ -81,7 +81,7 @@ func (k *K9s) Validate(ks KubeSettings) {
k.Clusters[k.CurrentCluster] = NewCluster() k.Clusters[k.CurrentCluster] = NewCluster()
} }
if ns, err := ks.CurrentNamespaceName(); err == nil { if ns, err := ks.CurrentNamespaceName(); err == nil && len(ns) != 0 {
k.Clusters[k.CurrentCluster].Namespace.Active = ns k.Clusters[k.CurrentCluster].Namespace.Active = ns
} }
} }

View File

@ -203,12 +203,12 @@ func (c *Config) CurrentNamespaceName() (string, error) {
return "", err return "", err
} }
if ctx, ok := cfg.Contexts[ctx]; ok { if ctx, ok := cfg.Contexts[ctx]; ok {
if isSet(&ctx.Namespace) { if isSet(&ctx.Namespace) {
return ctx.Namespace, nil return ctx.Namespace, nil
} }
} }
return defaultNamespace, nil return "", nil
} }
// NamespaceNames fetch all available namespaces on current cluster. // NamespaceNames fetch all available namespaces on current cluster.

View File

@ -48,13 +48,13 @@ func (*Job) Delete(ns, n string) error {
} }
// Containers returns all container names on pod // Containers returns all container names on pod
func (j *Job) Containers(ns, n string) ([]string, error) { func (j *Job) Containers(ns, n string, includeInit bool) ([]string, error) {
pod, err := j.assocPod(ns, n) pod, err := j.assocPod(ns, n)
if err != nil { if err != nil {
return nil, err return nil, err
} }
log.Debug("Containers found assoc pod", pod) log.Debug("Containers found assoc pod", pod)
return NewPod().(Loggable).Containers(ns, pod) return NewPod().(Loggable).Containers(ns, pod, includeInit)
} }
// Logs fetch container logs for a given pod and container. // Logs fetch container logs for a given pod and container.

View File

@ -12,7 +12,7 @@ type (
// Loggable represents a K8s resource that has containers and can be logged. // Loggable represents a K8s resource that has containers and can be logged.
Loggable interface { Loggable interface {
Res Res
Containers(ns, n string) ([]string, error) Containers(ns, n string, includeInit bool) ([]string, error)
Logs(ns, n, co string, lines int64, previous bool) *restclient.Request Logs(ns, n, co string, lines int64, previous bool) *restclient.Request
} }
@ -58,7 +58,7 @@ func (*Pod) Delete(ns, n string) error {
} }
// Containers returns all container names on pod // Containers returns all container names on pod
func (*Pod) Containers(ns, n string) ([]string, error) { func (*Pod) Containers(ns, n string, includeInit bool) ([]string, error) {
opts := metav1.GetOptions{} opts := metav1.GetOptions{}
cc := []string{} cc := []string{}
po, err := conn.dialOrDie().CoreV1().Pods(ns).Get(n, opts) po, err := conn.dialOrDie().CoreV1().Pods(ns).Get(n, opts)
@ -66,6 +66,11 @@ func (*Pod) Containers(ns, n string) ([]string, error) {
return cc, err return cc, err
} }
if includeInit {
for _, c := range po.Spec.InitContainers {
cc = append(cc, c.Name)
}
}
for _, c := range po.Spec.Containers { for _, c := range po.Spec.Containers {
cc = append(cc, c.Name) cc = append(cc, c.Name)
} }

View File

@ -8,7 +8,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/k8sland/tview" "github.com/derailed/tview"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/duration" "k8s.io/apimachinery/pkg/util/duration"
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"

View File

@ -74,11 +74,13 @@ func (r *Job) Marshal(path string) (string, error) {
return r.marshalObject(jo) return r.marshalObject(jo)
} }
func (r *Job) Containers(path string) ([]string, error) { // Containers fetch all the containers on this job, may include init containers.
func (r *Job) Containers(path string, includeInit bool) ([]string, error) {
ns, n := namespaced(path) ns, n := namespaced(path)
return r.caller.(k8s.Loggable).Containers(ns, n) return r.caller.(k8s.Loggable).Containers(ns, n, includeInit)
} }
// Logs retrieves logs for a given container.
func (r *Job) Logs(c chan<- string, ns, n, co string, lines int64, prev bool) (context.CancelFunc, error) { func (r *Job) Logs(c chan<- string, ns, n, co string, lines int64, prev bool) (context.CancelFunc, error) {
req := r.caller.(k8s.Loggable).Logs(ns, n, co, lines, prev) req := r.caller.(k8s.Loggable).Logs(ns, n, co, lines, prev)
ctx, cancel := context.WithCancel(context.TODO()) ctx, cancel := context.WithCancel(context.TODO())

View File

@ -5,7 +5,6 @@ import (
"sort" "sort"
"github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/k8s"
log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
) )
@ -219,7 +218,6 @@ func (l *list) Reconcile() error {
err error err error
) )
log.Debugf("Fetching list for resource `%s` in ns `%s`", l.name, l.namespace)
if items, err = l.api.List(l.namespace); err != nil { if items, err = l.api.List(l.namespace); err != nil {
return err return err
} }

View File

@ -17,7 +17,7 @@ const defaultTimeout = 1 * time.Second
type ( type (
// Container represents a resource that encompass multiple containers. // Container represents a resource that encompass multiple containers.
Container interface { Container interface {
Containers(path string) ([]string, error) Containers(path string, includeInit bool) ([]string, error)
} }
// Tailable represents a resource with tailable logs. // Tailable represents a resource with tailable logs.
@ -114,9 +114,9 @@ func (r *Pod) Marshal(path string) (string, error) {
} }
// Containers lists out all the docker contrainers name contained in a pod. // Containers lists out all the docker contrainers name contained in a pod.
func (r *Pod) Containers(path string) ([]string, error) { func (r *Pod) Containers(path string, includeInit bool) ([]string, error) {
ns, po := namespaced(path) ns, po := namespaced(path)
return r.caller.(k8s.Loggable).Containers(ns, po) return r.caller.(k8s.Loggable).Containers(ns, po, includeInit)
} }
// Logs tails a given container logs // Logs tails a given container logs

96
internal/views/alias.go Normal file
View File

@ -0,0 +1,96 @@
package views
import (
"context"
"fmt"
"strings"
"github.com/derailed/k9s/internal/resource"
"github.com/gdamore/tcell"
)
const (
aliasTitle = "Aliases"
aliasTitleFmt = " [aqua::b]%s[[aqua::b]%d[aqua::-]][aqua::-] "
)
type aliasView struct {
*tableView
current igniter
}
func newAliasView(app *appView) *aliasView {
v := aliasView{tableView: newTableView(app, aliasTitle, nil)}
{
v.SetSelectedStyle(tcell.ColorWhite, tcell.ColorFuchsia, tcell.AttrNone)
v.colorerFn = aliasColorer
v.current = app.content.GetPrimitive("main").(igniter)
}
v.actions[tcell.KeyEnter] = newKeyAction("Search", v.aliasCmd)
v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd)
return &v
}
// Init the view.
func (v *aliasView) init(context.Context, string) {
v.update(v.hydrate())
v.app.SetFocus(v)
v.resetTitle()
}
func (v *aliasView) getTitle() string {
return aliasTitle
}
func (v *aliasView) aliasCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.cmdBuff.isActive() {
return v.filterCmd(evt)
}
return v.runCmd(evt)
}
func (v *aliasView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
v.app.inject(v.current)
return nil
}
func (v *aliasView) runCmd(evt *tcell.EventKey) *tcell.EventKey {
r, _ := v.GetSelection()
if r > 0 {
v.app.command.run(strings.TrimSpace(v.GetCell(r, 0).Text))
}
return nil
}
func (v *aliasView) hints() hints {
return v.actions.toHints()
}
func (v *aliasView) hydrate() resource.TableData {
cmds := helpCmds()
data := resource.TableData{
Header: resource.Row{"ALIAS", "RESOURCE", "APIGROUP"},
Rows: make(resource.RowEvents, len(cmds)),
Namespace: "",
}
for k := range cmds {
fields := resource.Row{
resource.Pad(k, 30),
resource.Pad(cmds[k].title, 30),
resource.Pad(cmds[k].api, 30),
}
data.Rows[k] = &resource.RowEvent{
Action: resource.New,
Fields: fields,
Deltas: fields,
}
}
return data
}
func (v *aliasView) resetTitle() {
v.SetTitle(fmt.Sprintf(aliasTitleFmt, aliasTitle, v.GetRowCount()-1))
}

View File

@ -3,12 +3,11 @@ package views
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"time" "time"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/tview"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/k8sland/tview"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
) )
@ -24,6 +23,10 @@ type (
init(ctx context.Context, ns string) init(ctx context.Context, ns string)
} }
keyHandler interface {
keyboard(evt *tcell.EventKey) *tcell.EventKey
}
resourceViewer interface { resourceViewer interface {
igniter igniter
} }
@ -44,35 +47,47 @@ type (
cancel context.CancelFunc cancel context.CancelFunc
cmdBuff *cmdBuff cmdBuff *cmdBuff
cmdView *cmdView cmdView *cmdView
actions keyActions
} }
) )
func init() {
initKeys()
initStyles()
}
// NewApp returns a K9s app instance. // NewApp returns a K9s app instance.
func NewApp() *appView { func NewApp() *appView {
var app appView v := appView{Application: tview.NewApplication()}
{ {
app = appView{ v.pages = tview.NewPages()
Application: tview.NewApplication(), v.actions = make(keyActions)
pages: tview.NewPages(), v.menuView = newMenuView()
menuView: newMenuView(), v.content = tview.NewPages()
content: tview.NewPages(), v.cmdBuff = newCmdBuff(':')
cmdBuff: newCmdBuff(':'), v.cmdView = newCmdView('🐶')
cmdView: newCmdView('🐶'), v.command = newCommand(&v)
} v.flashView = newFlashView(v.Application, "Initializing...")
app.command = newCommand(&app) v.infoView = newInfoView(&v)
app.focusChanged = app.changedFocus v.focusChanged = v.changedFocus
app.SetInputCapture(app.keyboard) v.SetInputCapture(v.keyboard)
} }
return &app
v.actions[KeyColon] = newKeyAction("Cmd", v.activateCmd)
v.actions[tcell.KeyCtrlR] = newKeyAction("Redraw", v.redrawCmd)
v.actions[KeyQ] = newKeyAction("Quit", v.quitCmd)
v.actions[KeyHelp] = newKeyAction("Help", v.helpCmd)
v.actions[tcell.KeyEscape] = newKeyAction("Exit Cmd", v.deactivateCmd)
v.actions[tcell.KeyEnter] = newKeyAction("Goto", v.gotoCmd)
v.actions[tcell.KeyBackspace2] = newKeyAction("Goto", v.eraseCmd)
v.actions[tcell.KeyTab] = newKeyAction("Focus", v.focusCmd)
return &v
} }
func (a *appView) Init(v string, rate int, flags *genericclioptions.ConfigFlags) { func (a *appView) Init(v string, rate int, flags *genericclioptions.ConfigFlags) {
a.version = v a.version = v
a.infoView = newInfoView(a)
a.infoView.init() a.infoView.init()
a.flashView = newFlashView(a.Application, "Initializing...")
a.cmdBuff.addListener(a.cmdView) a.cmdBuff.addListener(a.cmdView)
header := tview.NewFlex() header := tview.NewFlex()
@ -114,51 +129,81 @@ func (a *appView) Run() {
func (a *appView) keyboard(evt *tcell.EventKey) *tcell.EventKey { func (a *appView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
key := evt.Key() key := evt.Key()
if key == tcell.KeyRune { if key == tcell.KeyRune {
switch evt.Rune() {
case 'q':
if !a.cmdMode() {
a.quit(evt)
}
case '?':
if !a.cmdView.inCmdMode() {
a.help(evt)
return evt
}
case a.cmdBuff.hotKey:
a.flash(flashInfo, "Entering command mode...")
log.Debug("K9s entering command mode...")
a.cmdBuff.setActive(true)
a.cmdBuff.clear()
return evt
}
if a.cmdBuff.isActive() { if a.cmdBuff.isActive() {
a.cmdBuff.add(evt.Rune()) a.cmdBuff.add(evt.Rune())
} }
return evt key = tcell.Key(evt.Rune())
} }
if a, ok := a.actions[key]; ok {
switch evt.Key() { log.Debug(">> AppView handled key: ", tcell.KeyNames[key])
case tcell.KeyCtrlR: return a.action(evt)
log.Debug("Refreshing screen...")
a.Draw()
case tcell.KeyEsc:
a.cmdBuff.reset()
case tcell.KeyEnter:
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()
}
case tcell.KeyTab:
a.nextFocus()
} }
return evt return evt
} }
func (a *appView) redrawCmd(evt *tcell.EventKey) *tcell.EventKey {
a.Draw()
return evt
}
func (a *appView) focusCmd(evt *tcell.EventKey) *tcell.EventKey {
a.nextFocus()
return evt
}
func (a *appView) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
if a.cmdBuff.isActive() {
a.cmdBuff.del()
return nil
}
return evt
}
func (a *appView) deactivateCmd(evt *tcell.EventKey) *tcell.EventKey {
if a.cmdBuff.isActive() {
a.cmdBuff.reset()
}
return evt
}
func (a *appView) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
if a.cmdBuff.isActive() && !a.cmdBuff.empty() {
a.command.run(a.cmdBuff.String())
a.cmdBuff.reset()
return nil
}
a.cmdBuff.setActive(false)
return evt
}
func (a *appView) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
a.flash(flashInfo, "Entering command mode...")
log.Debug("Entering app command mode...")
a.cmdBuff.setActive(true)
a.cmdBuff.clear()
return nil
}
func (a *appView) quitCmd(evt *tcell.EventKey) *tcell.EventKey {
if a.cmdMode() {
return evt
}
a.Stop()
return nil
}
func (a *appView) helpCmd(evt *tcell.EventKey) *tcell.EventKey {
if a.cmdView.inCmdMode() {
return evt
}
a.inject(newAliasView(a))
return nil
}
func (a *appView) noopCmd(*tcell.EventKey) *tcell.EventKey {
return nil
}
func (a *appView) showPage(p string) { func (a *appView) showPage(p string) {
a.pages.SwitchToPage(p) a.pages.SwitchToPage(p)
} }
@ -195,18 +240,6 @@ func (a *appView) refresh() {
a.infoView.refresh() a.infoView.refresh()
} }
func (a *appView) help(*tcell.EventKey) {
a.inject(newHelpView(a))
}
func (a *appView) noop(*tcell.EventKey) {
}
func (a *appView) quit(*tcell.EventKey) {
a.Stop()
os.Exit(0)
}
func (a *appView) flash(level flashLevel, m ...string) { func (a *appView) flash(level flashLevel, m ...string) {
a.flashView.setMessage(level, m...) a.flashView.setMessage(level, m...)
} }
@ -260,3 +293,10 @@ func (a *appView) nextFocus() {
} }
return return
} }
func initStyles() {
tview.Styles.PrimitiveBackgroundColor = tcell.ColorBlack
tview.Styles.ContrastBackgroundColor = tcell.ColorBlack
tview.Styles.FocusColor = tcell.ColorLightSkyBlue
tview.Styles.BorderColor = tcell.ColorDodgerBlue
}

View File

@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/k8sland/tview" "github.com/derailed/tview"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )

View File

@ -49,6 +49,10 @@ func (c *cmdBuff) del() {
c.fireChanged() c.fireChanged()
} }
func (c *cmdBuff) wipe() {
c.buff = make([]rune, 0, maxBuff)
}
func (c *cmdBuff) clear() { func (c *cmdBuff) clear() {
c.buff = make([]rune, 0, maxBuff) c.buff = make([]rune, 0, maxBuff)
c.fireChanged() c.fireChanged()

View File

@ -29,6 +29,10 @@ func defaultColorer(ns string, r *resource.RowEvent) tcell.Color {
return c return c
} }
func aliasColorer(string, *resource.RowEvent) tcell.Color {
return tcell.ColorFuchsia
}
func podColorer(ns string, r *resource.RowEvent) tcell.Color { func podColorer(ns string, r *resource.RowEvent) tcell.Color {
c := defaultColorer(ns, r) c := defaultColorer(ns, r)
@ -37,7 +41,7 @@ func podColorer(ns string, r *resource.RowEvent) tcell.Color {
statusCol = 2 statusCol = 2
} }
switch strings.TrimSpace(r.Fields[statusCol]) { switch strings.TrimSpace(r.Fields[statusCol]) {
case "ContainerCreating": case "ContainerCreating", "PodInitializing":
return addColor return addColor
case "Terminating", "Initialized": case "Terminating", "Initialized":
return highlightColor return highlightColor

View File

@ -27,9 +27,11 @@ func (c *command) run(cmd string) {
var v igniter var v igniter
switch cmd { switch cmd {
case "q", "quit": case "q", "quit":
c.app.quit(nil) c.app.Stop()
return
case "?", "help", "alias": case "?", "help", "alias":
c.app.help(nil) c.app.inject(newAliasView(c.app))
return
default: default:
if res, ok := cmdMap[cmd]; ok { if res, ok := cmdMap[cmd]; ok {
v = res.viewFn(res.title, c.app, res.listFn(resource.DefaultNamespace), res.colorerFn) v = res.viewFn(res.title, c.app, res.listFn(resource.DefaultNamespace), res.colorerFn)

View File

@ -21,9 +21,9 @@ func newContextView(t string, app *appView, list resource.List, c colorerFn) res
return &v return &v
} }
func (v *contextView) useContext(*tcell.EventKey) { func (v *contextView) useContext(evt *tcell.EventKey) *tcell.EventKey {
if !v.rowSelected() { if !v.rowSelected() {
return return evt
} }
ctx := strings.TrimSpace(v.selectedItem) ctx := strings.TrimSpace(v.selectedItem)
@ -37,7 +37,7 @@ func (v *contextView) useContext(*tcell.EventKey) {
err := v.list.Resource().(*resource.Context).Switch(ctx) err := v.list.Resource().(*resource.Context).Switch(ctx)
if err != nil { if err != nil {
v.app.flash(flashWarn, err.Error()) v.app.flash(flashWarn, err.Error())
return return evt
} }
config.Root.Reset() config.Root.Reset()
@ -46,10 +46,11 @@ func (v *contextView) useContext(*tcell.EventKey) {
v.app.flash(flashInfo, "Switching context to", ctx) v.app.flash(flashInfo, "Switching context to", ctx)
v.refresh() v.refresh()
if tv, ok := v.GetPrimitive("ctx").(*tableView); ok { if tv, ok := v.GetPrimitive("ctx").(*tableView); ok {
tv.table.Select(0, 0) tv.Select(0, 0)
} }
return nil
} }
func (v *contextView) extraActions(aa keyActions) { func (v *contextView) extraActions(aa keyActions) {
aa[KeyU] = keyAction{description: "Use", action: v.useContext} aa[KeyU] = newKeyAction("Use", v.useContext)
} }

View File

@ -20,17 +20,19 @@ func newCronJobView(t string, app *appView, list resource.List, c colorerFn) res
return &v return &v
} }
func (v *cronJobView) trigger(*tcell.EventKey) { func (v *cronJobView) trigger(evt *tcell.EventKey) *tcell.EventKey {
if !v.rowSelected() { if !v.rowSelected() {
return return evt
} }
v.app.flash(flashInfo, fmt.Sprintf("Triggering %s %s", v.list.GetName(), v.selectedItem)) v.app.flash(flashInfo, fmt.Sprintf("Triggering %s %s", v.list.GetName(), v.selectedItem))
if err := v.list.Resource().(resource.Runner).Run(v.selectedItem); err != nil { if err := v.list.Resource().(resource.Runner).Run(v.selectedItem); err != nil {
v.app.flash(flashErr, "Boom!", err.Error()) v.app.flash(flashErr, "Boom!", err.Error())
return evt
} }
return nil
} }
func (v *cronJobView) extraActions(aa keyActions) { func (v *cronJobView) extraActions(aa keyActions) {
aa[tcell.KeyCtrlT] = keyAction{description: "Trigger", action: v.trigger} aa[tcell.KeyCtrlT] = newKeyAction("Trigger", v.trigger)
} }

View File

@ -2,27 +2,59 @@ package views
import ( import (
"fmt" "fmt"
"regexp"
"strconv"
"strings"
"github.com/derailed/tview"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/k8sland/tview" log "github.com/sirupsen/logrus"
) )
const detailFmt = " [aqua::-]%s [fuchsia::b]%s " const detailsTitleFmt = " [aqua::b]%s([fuchsia::b]%s[aqua::-])[aqua::-] "
// detailsView display yaml output // detailsView displays text output.
type detailsView struct { type detailsView struct {
*tview.TextView *tview.TextView
actions keyActions app *appView
category string actions keyActions
title string
category string
cmdBuff *cmdBuff
backFn actionHandler
numSelections int
} }
func newDetailsView() *detailsView { func newDetailsView(app *appView, backFn actionHandler) *detailsView {
v := detailsView{TextView: tview.NewTextView()} v := detailsView{TextView: tview.NewTextView(), app: app, actions: make(keyActions)}
v.TextView.SetDynamicColors(true) {
v.TextView.SetBorder(true) v.backFn = backFn
v.SetTitleColor(tcell.ColorAqua) v.SetScrollable(true)
v.SetInputCapture(v.keyboard) v.SetWrap(false)
v.SetDynamicColors(true)
v.SetRegions(true)
v.SetBorder(true)
v.SetHighlightColor(tcell.ColorOrange)
v.SetTitleColor(tcell.ColorAqua)
v.SetInputCapture(v.keyboard)
v.cmdBuff = newCmdBuff('/')
{
v.cmdBuff.addListener(app.cmdView)
v.cmdBuff.reset()
}
v.SetChangedFunc(func() {
app.Draw()
})
}
v.actions[KeySlash] = newKeyAction("Search", v.activateCmd)
v.actions[tcell.KeyEnter] = newKeyAction("Search", v.searchCmd)
v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd)
v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.backCmd)
v.actions[tcell.KeyTab] = newKeyAction("Next", v.nextCmd)
v.actions[tcell.KeyBacktab] = newKeyAction("Previous", v.prevCmd)
return &v return &v
} }
@ -31,23 +63,120 @@ func (v *detailsView) setCategory(n string) {
} }
func (v *detailsView) keyboard(evt *tcell.EventKey) *tcell.EventKey { func (v *detailsView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
if evt.Key() == tcell.KeyRune { key := evt.Key()
if a, ok := v.actions[evt.Key()]; ok { if key == tcell.KeyRune {
a.action(evt) if v.cmdBuff.isActive() {
evt = nil v.cmdBuff.add(evt.Rune())
} v.refreshTitle()
} else { return nil
if a, ok := v.actions[evt.Key()]; ok {
a.action(evt)
evt = nil
} }
key = tcell.Key(evt.Rune())
}
if a, ok := v.actions[key]; ok {
log.Debug(">> DetailsView handled ", tcell.KeyNames[key])
return a.action(evt)
}
log.Debug("Doh! DetailsView got no registered action for key ", tcell.KeyNames[key])
return evt
}
func (v *detailsView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.cmdBuff.empty() {
v.cmdBuff.reset()
v.search(evt)
return nil
}
v.cmdBuff.reset()
if v.backFn != nil {
return v.backFn(evt)
} }
return evt return evt
} }
func (v *detailsView) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.cmdBuff.isActive() {
return evt
}
v.cmdBuff.del()
return nil
}
func (v *detailsView) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.app.cmdView.inCmdMode() {
v.cmdBuff.setActive(true)
v.cmdBuff.clear()
return nil
}
return evt
}
func (v *detailsView) searchCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.cmdBuff.isActive() && !v.cmdBuff.empty() {
v.app.flash(flashInfo, fmt.Sprintf("Searching for %s...", v.cmdBuff))
v.search(evt)
highlights := v.GetHighlights()
if len(highlights) > 0 {
v.Highlight()
} else {
v.Highlight("0").ScrollToHighlight()
}
}
v.cmdBuff.setActive(false)
return evt
}
func (v *detailsView) search(evt *tcell.EventKey) {
v.numSelections = 0
v.Highlight()
log.Debug("Searching...", v.cmdBuff, v.numSelections)
v.SetText(v.decorateLines(v.GetText(true), v.cmdBuff.String()))
if v.cmdBuff.empty() {
v.app.flash(flashWarn, "Clearing out search query...")
v.refreshTitle()
return
}
if v.numSelections == 0 {
v.app.flash(flashWarn, "No matches found!")
return
}
v.app.flash(flashWarn, fmt.Sprintf("Found <%d> matches! <tab>/<TAB> for next/previous", v.numSelections))
}
func (v *detailsView) nextCmd(evt *tcell.EventKey) *tcell.EventKey {
highlights := v.GetHighlights()
if len(highlights) == 0 || v.numSelections == 0 {
return evt
}
index, _ := strconv.Atoi(highlights[0])
index = (index + 1) % v.numSelections
if index+1 == v.numSelections {
v.app.flash(flashInfo, "Search hit BOTTOM, continuing at TOP")
}
v.Highlight(strconv.Itoa(index)).ScrollToHighlight()
return nil
}
func (v *detailsView) prevCmd(evt *tcell.EventKey) *tcell.EventKey {
highlights := v.GetHighlights()
if len(highlights) == 0 || v.numSelections == 0 {
return evt
}
index, _ := strconv.Atoi(highlights[0])
index = (index - 1 + v.numSelections) % v.numSelections
if index == 0 {
v.app.flash(flashInfo, "Search hit TOP, continuing at BOTTOM")
}
v.Highlight(strconv.Itoa(index)).ScrollToHighlight()
return nil
}
// SetActions to handle keyboard inputs // SetActions to handle keyboard inputs
func (v *detailsView) setActions(aa keyActions) { func (v *detailsView) setActions(aa keyActions) {
v.actions = aa for k, a := range aa {
v.actions[k] = a
}
} }
// Hints fetch mmemonic and hints // Hints fetch mmemonic and hints
@ -58,6 +187,27 @@ func (v *detailsView) hints() hints {
return nil return nil
} }
func (v *detailsView) setTitle(t string) { func (v *detailsView) refreshTitle() {
v.SetTitle(fmt.Sprintf(detailFmt, t, v.category)) v.setTitle(v.title)
}
func (v *detailsView) setTitle(t string) {
v.title = t
title := fmt.Sprintf(detailsTitleFmt, v.category, t)
if !v.cmdBuff.empty() {
title += fmt.Sprintf(searchFmt, v.cmdBuff.String())
}
v.SetTitle(title)
}
func (v *detailsView) decorateLines(buff, q string) string {
rx := regexp.MustCompile(`(?i)` + q)
lines := strings.Split(buff, "\n")
for i, l := range lines {
if m := rx.FindString(l); len(m) > 0 {
lines[i] = rx.ReplaceAllString(l, fmt.Sprintf(`["%d"]%s[""]`, v.numSelections, m))
v.numSelections++
}
}
return strings.Join(lines, "\n")
} }

View File

@ -0,0 +1,20 @@
package views
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestDetailsDecorateLines(t *testing.T) {
buff := `
I love blee
blee is much [blue::]cooler [green::]than foo!
`
exp := `
I love ["0"]blee[""]
["1"]blee[""] is much [blue::]cooler [green::]than foo!
`
v := detailsView{}
assert.Equal(t, exp, v.decorateLines(buff, "blee"))
}

View File

@ -7,12 +7,13 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
func run(app *appView, args ...string) { func run(app *appView, args ...string) bool {
app.Suspend(func() { return app.Suspend(func() {
if err := execute(args...); err != nil { if err := execute(args...); err != nil {
log.Error("Command failed:", err, args) log.Error("Command failed:", err, args)
app.flash(flashErr, "Doh! command failed", err.Error()) app.flash(flashErr, "Doh! command failed", err.Error())
} }
log.Debug("Command Done Running..")
}) })
} }

View File

@ -6,7 +6,7 @@ import (
"time" "time"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/k8sland/tview" "github.com/derailed/tview"
) )
const ( const (

View File

@ -1,110 +0,0 @@
package views
import (
"context"
"sort"
"strings"
"github.com/derailed/k9s/internal/resource"
"github.com/gdamore/tcell"
"github.com/k8sland/tview"
)
type helpView struct {
*tview.Pages
app *appView
title string
current igniter
keys keyActions
}
func newHelpView(app *appView) *helpView {
return &helpView{
app: app,
title: "Help",
current: app.content.GetPrimitive("main").(igniter),
Pages: tview.NewPages(),
}
}
// Init the view.
func (v *helpView) init(context.Context, string) {
v.keys = keyActions{
tcell.KeyEscape: keyAction{description: "Back", action: v.back},
}
t := tview.NewTable()
{
t.SetBorder(true)
t.SetTitle(" [::b]Commands Aliases ")
t.SetTitleColor(tcell.ColorAqua)
t.SetBorderColor(tcell.ColorDodgerBlue)
t.SetSelectable(true, false)
t.SetSelectedStyle(tcell.ColorWhite, tcell.ColorFuchsia, tcell.AttrNone)
t.SetInputCapture(v.keyboard)
}
var row int
for c, h := range []string{"ALIAS", "RESOURCE", "APIGROUP"} {
th := tview.NewTableCell(h)
th.SetExpansion(3)
t.SetCell(row, c, th)
}
row++
cmds := helpCmds()
kk := make([]string, 0, len(cmds))
for k := range cmds {
kk = append(kk, k)
}
sort.Strings(kk)
var col int
for _, k := range kk {
tc := tview.NewTableCell(resource.Pad(k, 30))
tc.SetExpansion(2)
t.SetCell(row, col, tc)
col++
tc = tview.NewTableCell(resource.Pad(cmds[k].title, 30))
tc.SetExpansion(2)
t.SetCell(row, col, tc)
col++
tc = tview.NewTableCell(resource.Pad(cmds[k].api, 30))
tc.SetExpansion(2)
t.SetCell(row, col, tc)
col = 0
row++
}
v.AddPage("main", t, true, true)
v.SwitchToPage("main")
v.app.SetFocus(v.CurrentPage().Item)
}
func (v *helpView) getTitle() string {
return v.title
}
func (v *helpView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
switch evt.Key() {
case tcell.KeyEscape:
v.back(evt)
return nil
case tcell.KeyEnter:
t := v.GetPrimitive("main").(*tview.Table)
r, _ := t.GetSelection()
if r > 0 {
v.app.command.run(strings.TrimSpace(t.GetCell(r, 0).Text))
return nil
}
}
return evt
}
func (v *helpView) back(evt *tcell.EventKey) {
v.app.inject(v.current)
}
func (v *helpView) hints() hints {
return v.keys.toHints()
}

View File

@ -3,7 +3,7 @@ package views
import ( import (
"github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/resource"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/k8sland/tview" "github.com/derailed/tview"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )

View File

@ -12,7 +12,9 @@ type jobView struct {
func newJobView(t string, app *appView, list resource.List, c colorerFn) resourceViewer { func newJobView(t string, app *appView, list resource.List, c colorerFn) resourceViewer {
v := jobView{newResourceView(t, app, list, c).(*resourceView)} v := jobView{newResourceView(t, app, list, c).(*resourceView)}
v.extraActionsFn = v.extraActions {
v.extraActionsFn = v.extraActions
}
v.AddPage("logs", newLogsView(&v), true, false) v.AddPage("logs", newLogsView(&v), true, false)
v.switchPage("job") v.switchPage("job")
return &v return &v
@ -23,23 +25,31 @@ func newJobView(t string, app *appView, list resource.List, c colorerFn) resourc
func (v *jobView) appView() *appView { func (v *jobView) appView() *appView {
return v.app return v.app
} }
func (v *jobView) backFn() actionHandler {
return v.backCmd
}
func (v *jobView) getList() resource.List { func (v *jobView) getList() resource.List {
return v.list return v.list
} }
func (v *jobView) getSelection() string { func (v *jobView) getSelection() string {
return v.selectedItem return v.selectedItem
} }
// Handlers... // Handlers...
func (v *jobView) logs(*tcell.EventKey) { func (v *jobView) logs(evt *tcell.EventKey) *tcell.EventKey {
if !v.rowSelected() { if !v.rowSelected() {
return return evt
} }
cc, err := fetchContainers(v.list, v.selectedItem) cc, err := fetchContainers(v.list, v.selectedItem, true)
if err != nil { if err != nil {
v.app.flash(flashErr, err.Error())
log.Error(err) log.Error(err)
return evt
} }
l := v.GetPrimitive("logs").(*logsView) l := v.GetPrimitive("logs").(*logsView)
@ -50,8 +60,9 @@ func (v *jobView) logs(*tcell.EventKey) {
v.switchPage("logs") v.switchPage("logs")
l.init() l.init()
return nil
} }
func (v *jobView) extraActions(aa keyActions) { func (v *jobView) extraActions(aa keyActions) {
aa[KeyL] = newKeyHandler("Logs", v.logs) aa[KeyL] = newKeyAction("Logs", v.logs)
} }

View File

@ -3,29 +3,20 @@ package views
import ( import (
"fmt" "fmt"
"github.com/k8sland/tview" log "github.com/sirupsen/logrus"
)
const (
logTitleFmt = " [aqua::b]Logs %s ([fuchsia::-]container=[fuchsia::b]%s[aqua::b]) "
) )
type logView struct { type logView struct {
*tview.TextView *detailsView
} }
func newLogView(title string, parent loggable) *logView { func newLogView(title string, parent loggable) *logView {
v := logView{TextView: tview.NewTextView()} log.Debug("LogsView init...")
v := logView{detailsView: newDetailsView(parent.appView(), parent.backFn())}
{ {
v.SetScrollable(true)
v.SetDynamicColors(true)
v.SetBorder(true)
v.SetBorderPadding(0, 0, 1, 1) v.SetBorderPadding(0, 0, 1, 1)
v.SetTitle(fmt.Sprintf(logTitleFmt, parent.getSelection(), title)) v.setCategory("Logs")
v.SetWrap(false) v.setTitle(parent.getSelection())
v.SetChangedFunc(func() {
parent.appView().Draw()
})
} }
return &v return &v
} }

View File

@ -8,8 +8,8 @@ import (
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/resource"
"github.com/derailed/tview"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/k8sland/tview"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -42,8 +42,8 @@ func newLogsView(parent loggable) *logsView {
KeyC: {description: "Clear", action: v.clearLogs}, KeyC: {description: "Clear", action: v.clearLogs},
KeyU: {description: "Top", action: v.top}, KeyU: {description: "Top", action: v.top},
KeyD: {description: "Bottom", action: v.bottom}, KeyD: {description: "Bottom", action: v.bottom},
KeyF: {description: "PageUp", action: v.pageUp}, KeyF: {description: "Up", action: v.pageUp},
KeyB: {description: "PageDown", action: v.pageDown}, KeyB: {description: "Down", action: v.pageDown},
}) })
v.SetInputCapture(v.keyboard) v.SetInputCapture(v.keyboard)
@ -57,26 +57,26 @@ func (v *logsView) init() {
} }
func (v *logsView) keyboard(evt *tcell.EventKey) *tcell.EventKey { func (v *logsView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
if m, ok := v.actions[evt.Key()]; ok { if kv, ok := v.CurrentPage().Item.(keyHandler); ok {
m.action(evt) if kv.keyboard(evt) == nil {
return nil
}
if evt.Key() != tcell.KeyRune {
return evt
}
if m, ok := v.actions[tcell.Key(evt.Rune())]; ok {
if m.action != nil {
m.action(evt)
return nil return nil
} }
} }
if i, err := strconv.Atoi(string(evt.Rune())); err == nil { key := evt.Key()
if _, ok := numKeys[i]; ok { if key == tcell.KeyRune {
v.load(i - 1) if i, err := strconv.Atoi(string(evt.Rune())); err == nil {
if _, ok := numKeys[i]; ok {
v.load(i - 1)
return nil
}
} }
key = tcell.Key(evt.Rune())
}
if m, ok := v.actions[key]; ok {
log.Debug(">> LogsView handled ", tcell.KeyNames[key])
return m.action(evt)
} }
return evt return evt
} }
@ -90,7 +90,7 @@ func (v *logsView) setActions(aa keyActions) {
func (v *logsView) hints() hints { func (v *logsView) hints() hints {
if len(v.containers) > 1 { if len(v.containers) > 1 {
for i, c := range v.containers { for i, c := range v.containers {
v.actions[tcell.Key(numKeys[i+1])] = keyAction{description: c} v.actions[tcell.Key(numKeys[i+1])] = newKeyAction(c, nil)
} }
} }
return v.actions.toHints() return v.actions.toHints()
@ -99,7 +99,9 @@ func (v *logsView) hints() hints {
func (v *logsView) addContainer(n string) { func (v *logsView) addContainer(n string) {
v.containers = append(v.containers, n) v.containers = append(v.containers, n)
l := newLogView(n, v.parent) l := newLogView(n, v.parent)
l.SetInputCapture(v.keyboard) {
l.SetInputCapture(v.keyboard)
}
v.AddPage(n, l, true, false) v.AddPage(n, l, true, false)
} }
@ -145,6 +147,7 @@ func (v *logsView) doLoad(path, co string) error {
c := make(chan string) c := make(chan string)
go func() { go func() {
l, count, first := v.CurrentPage().Item.(*logView), 0, true l, count, first := v.CurrentPage().Item.(*logView), 0, true
l.setTitle(path + ":" + co)
for { for {
select { select {
case line, ok := <-c: case line, ok := <-c:
@ -189,50 +192,55 @@ func (v *logsView) doLoad(path, co string) error {
return err return err
} }
v.cancelFunc = cancelFn v.cancelFunc = cancelFn
return nil return nil
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Actions... // Actions...
func (v *logsView) back(*tcell.EventKey) { func (v *logsView) back(evt *tcell.EventKey) *tcell.EventKey {
v.stop() v.stop()
v.parent.switchPage(v.parent.getList().GetName()) v.parent.switchPage(v.parent.getList().GetName())
return nil
} }
func (v *logsView) top(*tcell.EventKey) { func (v *logsView) top(evt *tcell.EventKey) *tcell.EventKey {
if p := v.CurrentPage(); p != nil { if p := v.CurrentPage(); p != nil {
v.parent.appView().flash(flashInfo, "Top logs...") v.parent.appView().flash(flashInfo, "Top logs...")
p.Item.(*logView).ScrollToBeginning() p.Item.(*logView).ScrollToBeginning()
} }
return nil
} }
func (v *logsView) bottom(*tcell.EventKey) { func (v *logsView) bottom(*tcell.EventKey) *tcell.EventKey {
if p := v.CurrentPage(); p != nil { if p := v.CurrentPage(); p != nil {
v.parent.appView().flash(flashInfo, "Bottom logs...") v.parent.appView().flash(flashInfo, "Bottom logs...")
p.Item.(*logView).ScrollToEnd() p.Item.(*logView).ScrollToEnd()
} }
return nil
} }
func (v *logsView) pageUp(*tcell.EventKey) { func (v *logsView) pageUp(*tcell.EventKey) *tcell.EventKey {
if p := v.CurrentPage(); p != nil { if p := v.CurrentPage(); p != nil {
v.parent.appView().flash(flashInfo, "Page Up logs...") v.parent.appView().flash(flashInfo, "Page Up logs...")
p.Item.(*logView).PageUp() p.Item.(*logView).PageUp()
} }
return nil
} }
func (v *logsView) pageDown(*tcell.EventKey) { func (v *logsView) pageDown(*tcell.EventKey) *tcell.EventKey {
if p := v.CurrentPage(); p != nil { if p := v.CurrentPage(); p != nil {
v.parent.appView().flash(flashInfo, "Page Down logs...") v.parent.appView().flash(flashInfo, "Page Down logs...")
p.Item.(*logView).PageDown() p.Item.(*logView).PageDown()
} }
return nil
} }
func (v *logsView) clearLogs(*tcell.EventKey) { func (v *logsView) clearLogs(*tcell.EventKey) *tcell.EventKey {
if p := v.CurrentPage(); p != nil { if p := v.CurrentPage(); p != nil {
v.parent.appView().flash(flashInfo, "Clearing logs...") v.parent.appView().flash(flashInfo, "Clearing logs...")
v.buffer.clear() v.buffer.clear()
p.Item.(*logView).Clear() p.Item.(*logView).Clear()
} }
return nil
} }

View File

@ -2,25 +2,27 @@ package views
import ( import (
"fmt" "fmt"
"regexp"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/resource"
"github.com/derailed/tview"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/k8sland/tview" log "github.com/sirupsen/logrus"
) )
const ( const (
menuSepFmt = " [dodgerblue::b]%-8s [white::d]%s " menuSepFmt = " [dodgerblue::b]%-9s [white::d]%s "
menuIndexFmt = " [fuchsia::b]<%d> [white::d]%s " menuIndexFmt = " [fuchsia::b]<%d> [white::d]%s "
maxRows = 6 maxRows = 6
colLen = 20 colLen = 20
) )
type ( var menuRX = regexp.MustCompile(`\d`)
keyboardHandler func(*tcell.EventKey)
type (
hint struct { hint struct {
mnemonic, display string mnemonic, display string
} }
@ -29,37 +31,43 @@ type (
hinter interface { hinter interface {
hints() hints hints() hints
} }
keyAction struct {
description string
action keyboardHandler
}
keyActions map[tcell.Key]keyAction
menuView struct {
*tview.Table
}
) )
func (h hints) Len() int { func (h hints) Len() int {
return len(h) return len(h)
} }
func (h hints) Swap(i, j int) { func (h hints) Swap(i, j int) {
h[i], h[j] = h[j], h[i] h[i], h[j] = h[j], h[i]
} }
func (h hints) Less(i, j int) bool { func (h hints) Less(i, j int) bool {
n, err1 := strconv.Atoi(h[i].mnemonic) n, err1 := strconv.Atoi(h[i].mnemonic)
m, err2 := strconv.Atoi(h[j].mnemonic) m, err2 := strconv.Atoi(h[j].mnemonic)
if err1 == nil && err2 == nil { if err1 == nil && err2 == nil {
return n < m return n < m
} }
if err1 == nil && err2 != nil {
d := strings.Compare(h[i].mnemonic, h[j].mnemonic) return true
return d < 0 }
if err1 != nil && err2 == nil {
return false
}
return strings.Compare(h[i].mnemonic, h[j].mnemonic) < 0
} }
func newKeyHandler(d string, a keyboardHandler) keyAction { // -----------------------------------------------------------------------------
type (
actionHandler func(*tcell.EventKey) *tcell.EventKey
keyAction struct {
description string
action actionHandler
}
keyActions map[tcell.Key]keyAction
)
func newKeyAction(d string, a actionHandler) keyAction {
return keyAction{description: d, action: a} return keyAction{description: d, action: a}
} }
@ -68,6 +76,11 @@ func newMenuView() *menuView {
return &v return &v
} }
// -----------------------------------------------------------------------------
type menuView struct {
*tview.Table
}
func (v *menuView) setMenu(hh hints) { func (v *menuView) setMenu(hh hints) {
v.Clear() v.Clear()
sort.Sort(hh) sort.Sort(hh)
@ -75,17 +88,12 @@ func (v *menuView) setMenu(hh hints) {
var row, col int var row, col int
firstNS, firstCmd := true, true firstNS, firstCmd := true, true
for _, h := range hh { for _, h := range hh {
_, err := strconv.Atoi(h.mnemonic) isDigit := menuRX.MatchString(h.mnemonic)
if err == nil && firstNS { if isDigit && firstNS {
row = 0 row, col, firstNS = 0, 2, false
col = 2
firstNS = false
} }
if !isDigit && firstCmd {
if err != nil && firstCmd { row, col, firstCmd = 0, 0, false
row = 0
col = 0
firstCmd = false
} }
c := tview.NewTableCell(v.item(h)) c := tview.NewTableCell(v.item(h))
v.SetCell(row, col, c) v.SetCell(row, col, c)
@ -106,14 +114,25 @@ func (v *menuView) item(h hint) string {
if err == nil { if err == nil {
return fmt.Sprintf(menuIndexFmt, i, resource.Truncate(h.display, 14)) return fmt.Sprintf(menuIndexFmt, i, resource.Truncate(h.display, 14))
} }
return fmt.Sprintf(menuSepFmt, v.toMnemonic(h.mnemonic), h.display) return fmt.Sprintf(menuSepFmt, v.toMnemonic(h.mnemonic), h.display)
} }
func (a keyActions) skipKey(k tcell.Key) bool {
switch k {
case tcell.KeyBackspace2:
fallthrough
case tcell.KeyEnter:
return true
}
return false
}
func (a keyActions) toHints() hints { func (a keyActions) toHints() hints {
kk := make([]int, 0, len(a)) kk := make([]int, 0, len(a))
for k := range a { for k := range a {
kk = append(kk, int(k)) if !a.skipKey(k) {
kk = append(kk, int(k))
}
} }
sort.Ints(kk) sort.Ints(kk)
@ -123,6 +142,8 @@ func (a keyActions) toHints() hints {
hh = append(hh, hint{ hh = append(hh, hint{
mnemonic: name, mnemonic: name,
display: a[tcell.Key(k)].description}) display: a[tcell.Key(k)].description})
} else {
log.Errorf("Unable to local KeyName for %#v", k)
} }
} }
return hh return hh
@ -173,7 +194,39 @@ const (
KeyX KeyX
KeyY KeyY
KeyZ KeyZ
KeyHelp = 63 KeyHelp = 63
KeySlash = 47
KeyColon = 58
)
// Define Shift Keys
const (
KeyShiftA tcell.Key = iota + 65
KeyShiftB
KeyShiftC
KeyShiftD
KeyShiftE
KeyShiftF
KeyShiftG
KeyShiftH
KeyShiftI
KeyShiftJ
KeyShiftK
KeyShiftL
KeyShiftM
KeyShiftN
KeyShiftO
KeyShiftP
KeyShiftQ
KeyShiftR
KeyShiftS
KeyShiftT
KeyShiftU
KeyShiftV
KeyShiftW
KeyShiftX
KeyShiftY
KeyShiftZ
) )
var numKeys = map[int]int32{ var numKeys = map[int]int32{
@ -188,3 +241,71 @@ var numKeys = map[int]int32{
8: Key8, 8: Key8,
9: Key9, 9: Key9,
} }
func initKeys() {
tcell.KeyNames[tcell.Key(Key0)] = "0"
tcell.KeyNames[tcell.Key(Key1)] = "1"
tcell.KeyNames[tcell.Key(Key2)] = "2"
tcell.KeyNames[tcell.Key(Key3)] = "3"
tcell.KeyNames[tcell.Key(Key4)] = "4"
tcell.KeyNames[tcell.Key(Key5)] = "5"
tcell.KeyNames[tcell.Key(Key6)] = "6"
tcell.KeyNames[tcell.Key(Key7)] = "7"
tcell.KeyNames[tcell.Key(Key8)] = "8"
tcell.KeyNames[tcell.Key(Key9)] = "9"
tcell.KeyNames[tcell.Key(KeyA)] = "a"
tcell.KeyNames[tcell.Key(KeyB)] = "b"
tcell.KeyNames[tcell.Key(KeyC)] = "c"
tcell.KeyNames[tcell.Key(KeyD)] = "d"
tcell.KeyNames[tcell.Key(KeyE)] = "e"
tcell.KeyNames[tcell.Key(KeyF)] = "f"
tcell.KeyNames[tcell.Key(KeyG)] = "g"
tcell.KeyNames[tcell.Key(KeyH)] = "h"
tcell.KeyNames[tcell.Key(KeyI)] = "i"
tcell.KeyNames[tcell.Key(KeyJ)] = "j"
tcell.KeyNames[tcell.Key(KeyK)] = "k"
tcell.KeyNames[tcell.Key(KeyL)] = "l"
tcell.KeyNames[tcell.Key(KeyM)] = "m"
tcell.KeyNames[tcell.Key(KeyN)] = "n"
tcell.KeyNames[tcell.Key(KeyO)] = "o"
tcell.KeyNames[tcell.Key(KeyP)] = "p"
tcell.KeyNames[tcell.Key(KeyQ)] = "q"
tcell.KeyNames[tcell.Key(KeyR)] = "r"
tcell.KeyNames[tcell.Key(KeyS)] = "s"
tcell.KeyNames[tcell.Key(KeyT)] = "t"
tcell.KeyNames[tcell.Key(KeyU)] = "u"
tcell.KeyNames[tcell.Key(KeyV)] = "v"
tcell.KeyNames[tcell.Key(KeyX)] = "x"
tcell.KeyNames[tcell.Key(KeyY)] = "y"
tcell.KeyNames[tcell.Key(KeyZ)] = "z"
tcell.KeyNames[tcell.Key(KeyShiftA)] = "SHIFT-A"
tcell.KeyNames[tcell.Key(KeyShiftB)] = "SHIFT-B"
tcell.KeyNames[tcell.Key(KeyShiftC)] = "SHIFT-C"
tcell.KeyNames[tcell.Key(KeyShiftD)] = "SHIFT-D"
tcell.KeyNames[tcell.Key(KeyShiftE)] = "SHIFT-E"
tcell.KeyNames[tcell.Key(KeyShiftF)] = "SHIFT-F"
tcell.KeyNames[tcell.Key(KeyShiftG)] = "SHIFT-G"
tcell.KeyNames[tcell.Key(KeyShiftH)] = "SHIFT-H"
tcell.KeyNames[tcell.Key(KeyShiftI)] = "SHIFT-I"
tcell.KeyNames[tcell.Key(KeyShiftJ)] = "SHIFT-J"
tcell.KeyNames[tcell.Key(KeyShiftK)] = "SHIFT-K"
tcell.KeyNames[tcell.Key(KeyShiftL)] = "SHIFT-L"
tcell.KeyNames[tcell.Key(KeyShiftM)] = "SHIFT-M"
tcell.KeyNames[tcell.Key(KeyShiftN)] = "SHIFT-N"
tcell.KeyNames[tcell.Key(KeyShiftO)] = "SHIFT-O"
tcell.KeyNames[tcell.Key(KeyShiftP)] = "SHIFT-P"
tcell.KeyNames[tcell.Key(KeyShiftQ)] = "SHIFT-Q"
tcell.KeyNames[tcell.Key(KeyShiftR)] = "SHIFT-R"
tcell.KeyNames[tcell.Key(KeyShiftS)] = "SHIFT-S"
tcell.KeyNames[tcell.Key(KeyShiftT)] = "SHIFT-T"
tcell.KeyNames[tcell.Key(KeyShiftU)] = "SHIFT-U"
tcell.KeyNames[tcell.Key(KeyShiftV)] = "SHIFT-V"
tcell.KeyNames[tcell.Key(KeyShiftX)] = "SHIFT-X"
tcell.KeyNames[tcell.Key(KeyShiftY)] = "SHIFT-Y"
tcell.KeyNames[tcell.Key(KeyShiftZ)] = "SHIFT-Z"
tcell.KeyNames[tcell.Key(KeyHelp)] = "?"
tcell.KeyNames[tcell.Key(KeySlash)] = "/"
}

View File

@ -2,7 +2,7 @@ package views
import ( import (
"fmt" "fmt"
"strings" "regexp"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/resource"
@ -15,40 +15,43 @@ const (
deltaNSIndicator = "(𝜟)" deltaNSIndicator = "(𝜟)"
) )
var nsCleanser = regexp.MustCompile(`(\w+)[+|(*)|(𝜟)]*`)
type namespaceView struct { type namespaceView struct {
*resourceView *resourceView
} }
func newNamespaceView(t string, app *appView, list resource.List, c colorerFn) resourceViewer { func newNamespaceView(t string, app *appView, list resource.List, c colorerFn) resourceViewer {
v := namespaceView{ v := namespaceView{newResourceView(t, app, list, c).(*resourceView)}
resourceView: newResourceView(t, app, list, c).(*resourceView),
}
v.extraActionsFn = v.extraActions v.extraActionsFn = v.extraActions
v.selectedFn = v.getSelectedItem
v.decorateDataFn = v.decorate v.decorateDataFn = v.decorate
v.switchPage("ns") v.switchPage("ns")
return &v return &v
} }
func (v *namespaceView) useNamespace(*tcell.EventKey) { func (v *namespaceView) extraActions(aa keyActions) {
if !v.rowSelected() { aa[KeyU] = newKeyAction("Use", v.useNamespace)
return
}
ns := v.selectedItem
for _, i := range []string{deltaNSIndicator, favNSIndicator, defaultNSIndicator} {
if strings.HasSuffix(ns, i) {
ns = strings.TrimRight(ns, i)
}
}
v.refresh()
config.Root.SetActiveNamespace(ns)
config.Root.Save()
v.app.flash(flashInfo, fmt.Sprintf("Setting namespace `%s as your default namespace", ns))
} }
func (v *namespaceView) extraActions(aa keyActions) { func (v *namespaceView) useNamespace(evt *tcell.EventKey) *tcell.EventKey {
aa[KeyU] = keyAction{description: "Use", action: v.useNamespace} if !v.rowSelected() {
return evt
}
ns := v.getSelectedItem()
config.Root.SetActiveNamespace(ns)
config.Root.Save()
v.refresh()
v.app.flash(flashInfo, fmt.Sprintf("Setting namespace `%s as your default namespace", ns))
return nil
}
func (v *namespaceView) getSelectedItem() string {
return v.cleanser(v.selectedItem)
}
func (*namespaceView) cleanser(s string) string {
return nsCleanser.ReplaceAllString(s, `$1`)
} }
func (v *namespaceView) decorate(data resource.TableData) resource.TableData { func (v *namespaceView) decorate(data resource.TableData) resource.TableData {
@ -59,18 +62,15 @@ func (v *namespaceView) decorate(data resource.TableData) resource.TableData {
Deltas: resource.Row{"", "", ""}, Deltas: resource.Row{"", "", ""},
} }
} }
for k, v := range data.Rows { for k, v := range data.Rows {
if config.InList(config.Root.FavNamespaces(), k) { if config.InList(config.Root.FavNamespaces(), k) {
v.Fields[0] += "+" v.Fields[0] += "+"
v.Action = resource.Unchanged v.Action = resource.Unchanged
} }
if config.Root.ActiveNamespace() == k { if config.Root.ActiveNamespace() == k {
v.Fields[0] += "(*)" v.Fields[0] += "(*)"
v.Action = resource.Unchanged v.Action = resource.Unchanged
} }
} }
return data return data
} }

View File

@ -0,0 +1,27 @@
package views
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNSCleanser(t *testing.T) {
var v namespaceView
uu := []struct {
s, e string
}{
{"fred", "fred"},
{"fred+", "fred"},
{"fred(*)", "fred"},
{"fred+(*)", "fred"},
{"fred-blee+(*)", "fred-blee"},
{"fred1-blee2+(*)", "fred1-blee2"},
{"fred(𝜟)", "fred"},
}
for _, u := range uu {
assert.Equal(t, u.e, v.cleanser(u.s))
}
}

View File

@ -12,6 +12,7 @@ type podView struct {
type loggable interface { type loggable interface {
appView() *appView appView() *appView
backFn() actionHandler
getSelection() string getSelection() string
getList() resource.List getList() resource.List
switchPage(n string) switchPage(n string)
@ -19,7 +20,9 @@ type loggable interface {
func newPodView(t string, app *appView, list resource.List, c colorerFn) resourceViewer { func newPodView(t string, app *appView, list resource.List, c colorerFn) resourceViewer {
v := podView{newResourceView(t, app, list, c).(*resourceView)} v := podView{newResourceView(t, app, list, c).(*resourceView)}
v.extraActionsFn = v.extraActions {
v.extraActionsFn = v.extraActions
}
v.AddPage("logs", newLogsView(&v), true, false) v.AddPage("logs", newLogsView(&v), true, false)
@ -29,7 +32,7 @@ func newPodView(t string, app *appView, list resource.List, c colorerFn) resourc
v.sshInto(v.selectedItem, t) v.sshInto(v.selectedItem, t)
}) })
picker.setActions(keyActions{ picker.setActions(keyActions{
tcell.KeyEscape: {description: "Back", action: v.back}, tcell.KeyEscape: {description: "Back", action: v.backCmd},
}) })
v.AddPage("choose", picker, true, false) v.AddPage("choose", picker, true, false)
} }
@ -40,56 +43,60 @@ func newPodView(t string, app *appView, list resource.List, c colorerFn) resourc
// Protocol... // Protocol...
func (v *podView) backFn() actionHandler {
return v.backCmd
}
func (v *podView) appView() *appView { func (v *podView) appView() *appView {
return v.app return v.app
} }
func (v *podView) getList() resource.List { func (v *podView) getList() resource.List {
return v.list return v.list
} }
func (v *podView) getSelection() string { func (v *podView) getSelection() string {
return v.selectedItem return v.selectedItem
} }
// Handlers... // Handlers...
func (v *podView) logs(*tcell.EventKey) { func (v *podView) logsCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.rowSelected() { if !v.rowSelected() {
return return evt
} }
cc, err := fetchContainers(v.list, v.selectedItem, true)
cc, err := fetchContainers(v.list, v.selectedItem)
if err != nil { if err != nil {
v.app.flash(flashErr, err.Error())
log.Error(err) log.Error(err)
return evt
} }
l := v.GetPrimitive("logs").(*logsView) l := v.GetPrimitive("logs").(*logsView)
l.deleteAllPages() l.deleteAllPages()
for _, c := range cc { for _, c := range cc {
l.addContainer(c) l.addContainer(c)
} }
v.switchPage("logs") v.switchPage("logs")
l.init() l.init()
return nil
} }
func (v *podView) shell(*tcell.EventKey) { func (v *podView) shellCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.rowSelected() { if !v.rowSelected() {
return return evt
} }
cc, err := fetchContainers(v.list, v.selectedItem, false)
cc, err := fetchContainers(v.list, v.selectedItem)
if err != nil { if err != nil {
v.app.flash(flashErr, err.Error())
log.Error("Error fetching containers", err) log.Error("Error fetching containers", err)
return return evt
} }
if len(cc) == 1 { if len(cc) == 1 {
v.sshInto(v.selectedItem, "") v.sshInto(v.selectedItem, "")
return return nil
} }
v.showPicker(cc) v.showPicker(cc)
return return nil
} }
func (v *podView) showPicker(cc []string) { func (v *podView) showPicker(cc []string) {
@ -109,13 +116,13 @@ func (v *podView) sshInto(path, co string) {
} }
func (v *podView) extraActions(aa keyActions) { func (v *podView) extraActions(aa keyActions) {
aa[KeyL] = newKeyHandler("Logs", v.logs) aa[KeyL] = newKeyAction("Logs", v.logsCmd)
aa[KeyS] = newKeyHandler("Shell", v.shell) aa[KeyS] = newKeyAction("Shell", v.shellCmd)
} }
func fetchContainers(l resource.List, po string) ([]string, error) { func fetchContainers(l resource.List, po string, includeInit bool) ([]string, error) {
if len(po) == 0 { if len(po) == 0 {
return []string{}, nil return []string{}, nil
} }
return l.Resource().(resource.Container).Containers(po) return l.Resource().(resource.Container).Containers(po, includeInit)
} }

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"path" "path"
"regexp"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -13,8 +12,8 @@ import (
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/resource"
"github.com/derailed/tview"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/k8sland/tview"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -40,6 +39,7 @@ type (
update sync.Mutex update sync.Mutex
list resource.List list resource.List
extraActionsFn func(keyActions) extraActionsFn func(keyActions)
selectedFn func() string
decorateDataFn func(resource.TableData) resource.TableData decorateDataFn func(resource.TableData) resource.TableData
} }
) )
@ -56,36 +56,17 @@ func newResourceView(title string, app *appView, list resource.List, c colorerFn
tv := newTableView(app, v.title, list.SortFn()) tv := newTableView(app, v.title, list.SortFn())
{ {
tv.SetColorer(c) tv.SetColorer(c)
tv.table.SetSelectionChangedFunc(v.selChanged) tv.SetSelectionChangedFunc(v.selChanged)
} }
v.AddPage(v.list.GetName(), tv, true, true) v.AddPage(v.list.GetName(), tv, true, true)
var xray details details := newDetailsView(app, v.backCmd)
if list.HasXRay() {
xray = newXrayView(app)
} else {
xray = newYamlView(app)
}
xray.setActions(keyActions{
tcell.KeyEscape: {description: "Back", action: v.back},
})
details := newDetailsView()
details.setActions(keyActions{
tcell.KeyEscape: {description: "Back", action: v.back},
})
v.AddPage("details", details, true, false) v.AddPage("details", details, true, false)
v.AddPage("xray", xray, true, false)
return &v return &v
} }
// Init watches all running pods in given namespace // Init watches all running pods in given namespace
func (v *resourceView) init(ctx context.Context, ns string) { func (v *resourceView) init(ctx context.Context, ns string) {
details := v.GetPrimitive("xray").(details)
details.clear()
v.selectedItem, v.selectedNS = noSelection, ns v.selectedItem, v.selectedNS = noSelection, ns
go func(ctx context.Context) { go func(ctx context.Context) {
@ -103,7 +84,7 @@ func (v *resourceView) init(ctx context.Context, ns string) {
}(ctx) }(ctx)
v.refreshActions() v.refreshActions()
if tv, ok := v.CurrentPage().Item.(*tableView); ok { if tv, ok := v.CurrentPage().Item.(*tableView); ok {
tv.table.Select(0, 0) tv.Select(0, 0)
} }
} }
@ -119,6 +100,13 @@ func (v *resourceView) colorFn(f colorerFn) {
v.getTV().SetColorer(f) v.getTV().SetColorer(f)
} }
func (v *resourceView) getSelectedItem() string {
if v.selectedFn != nil {
return v.selectedFn()
}
return v.selectedItem
}
// Protocol... // Protocol...
// Hints fetch menu hints // Hints fetch menu hints
@ -126,89 +114,89 @@ func (v *resourceView) hints() hints {
return v.CurrentPage().Item.(hinter).hints() return v.CurrentPage().Item.(hinter).hints()
} }
// ----------------------------------------------------------------------------
// Actions... // Actions...
func (v *resourceView) back(*tcell.EventKey) { func (v *resourceView) backCmd(*tcell.EventKey) *tcell.EventKey {
v.switchPage(v.list.GetName()) v.switchPage(v.list.GetName())
return nil
} }
func (v *resourceView) delete(*tcell.EventKey) { func (v *resourceView) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.rowSelected() { if !v.rowSelected() {
return return evt
} }
v.getTV().setDeleted() v.getTV().setDeleted()
v.app.flash(flashInfo, fmt.Sprintf("Deleting %s %s", v.list.GetName(), v.selectedItem)) sel := v.getSelectedItem()
if err := v.list.Resource().Delete(v.selectedItem); err != nil { v.app.flash(flashInfo, fmt.Sprintf("Deleting %s %s", v.list.GetName(), sel))
if err := v.list.Resource().Delete(sel); err != nil {
v.app.flash(flashErr, "Boom!", err.Error()) v.app.flash(flashErr, "Boom!", err.Error())
} }
v.selectedItem = noSelection v.selectedItem = noSelection
return nil
} }
func (v *resourceView) describe(*tcell.EventKey) { func (v *resourceView) describeCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.rowSelected() { if !v.rowSelected() {
return return evt
} }
sel := v.getSelectedItem()
selected := v.selectedItem raw, err := v.list.Resource().Describe(v.title, sel)
selected = strings.Replace(selected, "+", "", -1)
selected = strings.Replace(selected, "(*)", "", -1)
raw, err := v.list.Resource().Describe(v.title, selected)
if err != nil { if err != nil {
v.app.flash(flashErr, "Unable to describe this resource", err.Error()) v.app.flash(flashErr, "Unable to describeCmd this resource", err.Error())
log.Error(err) log.Error(err)
return return evt
} }
var re = regexp.MustCompile(`(?m:(^(.+)$))`)
str := re.ReplaceAllString(string(raw), `[aqua]$1`)
details := v.GetPrimitive("details").(*detailsView) details := v.GetPrimitive("details").(*detailsView)
details.ScrollToBeginning() {
details.setCategory("DESC") details.setCategory("Describe")
details.SetText(str) details.setTitle(sel)
details.setTitle(selected) details.SetTextColor(tcell.ColorAqua)
details.SetText(string(raw))
details.ScrollToBeginning()
}
v.switchPage("details") v.switchPage("details")
return nil
} }
func (v *resourceView) view(*tcell.EventKey) { func (v *resourceView) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.rowSelected() { if !v.rowSelected() {
return return evt
} }
sel := v.getSelectedItem()
raw, err := v.list.Resource().Marshal(v.selectedItem) raw, err := v.list.Resource().Marshal(sel)
if err != nil { if err != nil {
v.app.flash(flashErr, "Unable to marshal resource", err.Error()) v.app.flash(flashErr, "Unable to marshal resource", err.Error())
log.Error(err) log.Error(err)
return return evt
} }
var re = regexp.MustCompile(`(?m:([\w|\.|"|\-|\/|\@]+):(.*)$)`)
str := re.ReplaceAllString(string(raw), `[aqua]$1: [white]$2`)
details := v.GetPrimitive("details").(*detailsView) details := v.GetPrimitive("details").(*detailsView)
details.ScrollToBeginning() {
details.setCategory("YAML") details.setCategory("View")
details.SetText(str) details.setTitle(sel)
details.setTitle(v.selectedItem) details.SetTextColor(tcell.ColorMediumAquamarine)
details.SetText(string(raw))
details.ScrollToBeginning()
}
v.switchPage("details") v.switchPage("details")
return nil
} }
func (v *resourceView) edit(*tcell.EventKey) { func (v *resourceView) editCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.rowSelected() { if !v.rowSelected() {
return return evt
} }
v.app.flash(flashInfo, fmt.Sprintf("Editing %s %s", v.title, v.selectedItem)) v.app.flash(flashInfo, fmt.Sprintf("Editing %s %s", v.title, v.selectedItem))
ns, s := namespaced(v.selectedItem) ns, s := namespaced(v.selectedItem)
run(v.app, "edit", v.list.GetName(), "-n", ns, s) run(v.app, "edit", v.list.GetName(), "-n", ns, s)
return return evt
} }
func (v *resourceView) switchNamespace(evt *tcell.EventKey) { func (v *resourceView) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
i, _ := strconv.Atoi(string(evt.Rune())) i, _ := strconv.Atoi(string(evt.Rune()))
ns := v.namespaces[i] ns := v.namespaces[i]
v.doSwitchNamespace(ns) v.doSwitchNamespace(ns)
return nil
} }
func (v *resourceView) doSwitchNamespace(ns string) { func (v *resourceView) doSwitchNamespace(ns string) {
@ -225,7 +213,7 @@ func (v *resourceView) doSwitchNamespace(ns string) {
v.refresh() v.refresh()
v.selectItem(0, 0) v.selectItem(0, 0)
v.getTV().resetTitle() v.getTV().resetTitle()
v.getTV().table.Select(0, 0) v.getTV().Select(0, 0)
v.app.cmdBuff.reset() v.app.cmdBuff.reset()
config.Root.SetActiveNamespace(v.selectedNS) config.Root.SetActiveNamespace(v.selectedNS)
config.Root.Save() config.Root.Save()
@ -275,16 +263,16 @@ func (v *resourceView) selectItem(r, c int) {
t := v.getTV() t := v.getTV()
switch v.list.GetNamespace() { switch v.list.GetNamespace() {
case resource.NotNamespaced: case resource.NotNamespaced:
v.selectedItem = strings.TrimSpace(t.table.GetCell(r, 0).Text) v.selectedItem = strings.TrimSpace(t.GetCell(r, 0).Text)
case resource.AllNamespaces: case resource.AllNamespaces:
v.selectedItem = path.Join( v.selectedItem = path.Join(
strings.TrimSpace(t.table.GetCell(r, 0).Text), strings.TrimSpace(t.GetCell(r, 0).Text),
strings.TrimSpace(t.table.GetCell(r, 1).Text), strings.TrimSpace(t.GetCell(r, 1).Text),
) )
default: default:
v.selectedItem = path.Join( v.selectedItem = path.Join(
v.selectedNS, v.selectedNS,
strings.TrimSpace(t.table.GetCell(r, 0).Text), strings.TrimSpace(t.GetCell(r, 0).Text),
) )
} }
} }
@ -293,8 +281,8 @@ func (v *resourceView) switchPage(p string) {
v.update.Lock() v.update.Lock()
{ {
v.SwitchToPage(p) v.SwitchToPage(p)
h := v.GetPrimitive(p).(hinter)
v.selectedNS = v.list.GetNamespace() v.selectedNS = v.list.GetNamespace()
h := v.GetPrimitive(p).(hinter)
v.app.setHints(h.hints()) v.app.setHints(h.hints())
v.app.SetFocus(v.CurrentPage().Item) v.app.SetFocus(v.CurrentPage().Item)
} }
@ -328,30 +316,30 @@ func (v *resourceView) refreshActions() {
} }
} }
aa := keyActions{} aa := make(keyActions)
if v.list.Access(resource.NamespaceAccess) { if v.list.Access(resource.NamespaceAccess) {
v.namespaces = make(map[int]string, config.MaxFavoritesNS) v.namespaces = make(map[int]string, config.MaxFavoritesNS)
for i, n := range config.Root.FavNamespaces() { for i, n := range config.Root.FavNamespaces() {
aa[tcell.Key(numKeys[i])] = newKeyHandler(n, v.switchNamespace) aa[tcell.Key(numKeys[i])] = newKeyAction(n, v.switchNamespaceCmd)
v.namespaces[i] = n v.namespaces[i] = n
} }
} }
if v.list.Access(resource.EditAccess) { if v.list.Access(resource.EditAccess) {
aa[KeyE] = newKeyHandler("Edit", v.edit) aa[KeyE] = newKeyAction("Edit", v.editCmd)
} }
if v.list.Access(resource.DeleteAccess) { if v.list.Access(resource.DeleteAccess) {
aa[tcell.KeyCtrlD] = newKeyHandler("Delete", v.delete) aa[tcell.KeyCtrlD] = newKeyAction("Delete", v.deleteCmd)
} }
if v.list.Access(resource.ViewAccess) { if v.list.Access(resource.ViewAccess) {
aa[KeyV] = newKeyHandler("View", v.view) aa[KeyV] = newKeyAction("View", v.viewCmd)
} }
if v.list.Access(resource.DescribeAccess) { if v.list.Access(resource.DescribeAccess) {
aa[KeyD] = newKeyHandler("Describe", v.describe) aa[KeyD] = newKeyAction("Describe", v.describeCmd)
} }
aa[KeyHelp] = newKeyHandler("Help", v.app.noop) aa[KeyHelp] = newKeyAction("Help", v.app.noopCmd)
if v.extraActionsFn != nil { if v.extraActionsFn != nil {
v.extraActionsFn(aa) v.extraActionsFn(aa)

View File

@ -3,8 +3,8 @@ package views
import ( import (
"strconv" "strconv"
"github.com/derailed/tview"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/k8sland/tview"
) )
type selectList struct { type selectList struct {
@ -15,15 +15,17 @@ type selectList struct {
func newSelectList() *selectList { func newSelectList() *selectList {
v := selectList{List: tview.NewList()} v := selectList{List: tview.NewList()}
v.SetBorder(true) {
v.SetTitle(" Please select a Container ") v.SetBorder(true)
v.SetInputCapture(func(evt *tcell.EventKey) *tcell.EventKey { v.SetTitle(" [aqua::b]Container Selector ")
if a, ok := v.actions[evt.Key()]; ok { v.SetInputCapture(func(evt *tcell.EventKey) *tcell.EventKey {
a.action(evt) if a, ok := v.actions[evt.Key()]; ok {
evt = nil a.action(evt)
} evt = nil
return evt }
}) return evt
})
}
return &v return &v
} }

View File

@ -5,7 +5,7 @@ import (
"strings" "strings"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/k8sland/tview" "github.com/derailed/tview"
) )
const ( const (

View File

@ -7,149 +7,125 @@ import (
"sync" "sync"
"github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/resource"
"github.com/derailed/tview"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/k8sland/tview"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
const ( const (
titleFmt = " [aqua::b]%s[aqua::-]([fuchsia::b]%d[aqua::-]) " titleFmt = " [aqua::b]%s[aqua::-]([fuchsia::b]%d[aqua::-]) "
searchFmt = "<[green::b]/%s[aqua::]> "
nsTitleFmt = " [aqua::b]%s([fuchsia::b]%s[aqua::-])[aqua::-][[aqua::b]%d[aqua::-]][aqua::-] " nsTitleFmt = " [aqua::b]%s([fuchsia::b]%s[aqua::-])[aqua::-][[aqua::b]%d[aqua::-]][aqua::-] "
) )
type ( type (
tableView struct { tableView struct {
*tview.Flex *tview.Table
app *appView app *appView
baseTitle string baseTitle string
currentNS string currentNS string
refresh sync.Mutex refreshMX sync.Mutex
actions keyActions actions keyActions
colorer colorerFn colorerFn colorerFn
sortFn resource.SortFn sortFn resource.SortFn
table *tview.Table
data resource.TableData data resource.TableData
cmdBuff *cmdBuff cmdBuff *cmdBuff
} }
) )
func newTableView(app *appView, title string, sortFn resource.SortFn) *tableView { func newTableView(app *appView, title string, sortFn resource.SortFn) *tableView {
v := tableView{app: app, Flex: tview.NewFlex().SetDirection(tview.FlexRow)} v := tableView{app: app, Table: tview.NewTable()}
{ {
v.baseTitle = title v.baseTitle = title
v.sortFn = sortFn v.sortFn = sortFn
v.actions = make(keyActions)
v.SetBorder(true) v.SetBorder(true)
v.SetBorderColor(tcell.ColorDodgerBlue) v.SetBorderColor(tcell.ColorDodgerBlue)
v.SetBorderAttributes(tcell.AttrBold) v.SetBorderAttributes(tcell.AttrBold)
v.SetBorderPadding(0, 0, 1, 1) v.SetBorderPadding(0, 0, 1, 1)
v.cmdBuff = newCmdBuff('/') v.cmdBuff = newCmdBuff('/')
v.cmdBuff.addListener(app.cmdView)
v.cmdBuff.reset()
v.SetSelectable(true, false)
v.SetSelectedStyle(tcell.ColorBlack, tcell.ColorAqua, tcell.AttrBold)
v.SetInputCapture(v.keyboard)
} }
v.cmdBuff.addListener(app.cmdView) v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd)
v.cmdBuff.reset() v.actions[tcell.KeyEnter] = newKeyAction("Search", v.filterCmd)
v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.resetCmd)
v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd)
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 return &v
} }
func (v *tableView) setDeleted() {
r, _ := v.table.GetSelection()
cols := v.table.GetColumnCount()
for x := 0; x < cols; x++ {
v.table.GetCell(r, x).SetAttributes(tcell.AttrDim)
}
}
func (v *tableView) keyboard(evt *tcell.EventKey) *tcell.EventKey { func (v *tableView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
key := evt.Key() key := evt.Key()
if evt.Key() == tcell.KeyRune { if key == tcell.KeyRune {
if v.cmdBuff.isActive() { if v.cmdBuff.isActive() {
v.cmdBuff.add(evt.Rune()) v.cmdBuff.add(evt.Rune())
} return nil
switch evt.Rune() {
case v.cmdBuff.hotKey:
if !v.app.cmdView.inCmdMode() {
v.app.flash(flashInfo, "Entering filtering mode...")
log.Info("K9s entering filtering mode...")
v.cmdBuff.setActive(true)
}
return evt
} }
key = tcell.Key(evt.Rune()) key = tcell.Key(evt.Rune())
} }
if a, ok := v.actions[key]; ok { if a, ok := v.actions[key]; ok {
if !v.app.cmdView.inCmdMode() { log.Debug(">> TableView handled ", tcell.KeyNames[key])
a.action(evt) return a.action(evt)
}
}
switch evt.Key() {
case tcell.KeyEnter:
if v.cmdBuff.isActive() && !v.cmdBuff.empty() {
v.filter()
}
v.cmdBuff.setActive(false)
case tcell.KeyEsc:
v.cmdBuff.reset()
v.filter()
case tcell.KeyBackspace2:
if v.cmdBuff.isActive() {
v.cmdBuff.del()
}
} }
return evt return evt
} }
func (v *tableView) filter() { func (v *tableView) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
v.filterData(v.cmdBuff) v.cmdBuff.setActive(false)
v.refresh()
return nil
} }
func (v *tableView) filterData(filter fmt.Stringer) { func (v *tableView) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
filtered := resource.TableData{ if v.cmdBuff.isActive() {
Header: v.data.Header, v.cmdBuff.del()
Rows: resource.RowEvents{}, }
Namespace: v.data.Namespace, return nil
}
func (v *tableView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
v.cmdBuff.reset()
v.refresh()
return nil
}
func (v *tableView) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.app.cmdView.inCmdMode() {
return evt
} }
rx, err := regexp.Compile(filter.String()) v.app.flash(flashInfo, "Entering filtering mode...")
if err != nil { log.Info("Entering filtering mode...")
v.app.flash(flashErr, "Invalid search expression") v.cmdBuff.reset()
v.cmdBuff.clear() v.cmdBuff.setActive(true)
return return nil
}
func (v *tableView) setDeleted() {
r, _ := v.GetSelection()
cols := v.GetColumnCount()
for x := 0; x < cols; x++ {
v.GetCell(r, x).SetAttributes(tcell.AttrDim)
} }
for k, row := range v.data.Rows {
f := strings.Join(row.Fields, " ")
if rx.MatchString(f) {
filtered.Rows[k] = row
}
}
v.doUpdate(filtered)
} }
// SetColorer sets up table row color management. // SetColorer sets up table row color management.
func (v *tableView) SetColorer(f colorerFn) { func (v *tableView) SetColorer(f colorerFn) {
v.colorer = f v.colorerFn = f
}
// AddActions sets up keyboard action listener.
func (v *tableView) addActions(kk keyActions) {
for k, a := range kk {
v.actions[k] = a
}
} }
// SetActions sets up keyboard action listener. // SetActions sets up keyboard action listener.
func (v *tableView) setActions(aa keyActions) { func (v *tableView) setActions(aa keyActions) {
v.actions = aa for k, a := range aa {
v.actions[k] = a
}
} }
// Hints options // Hints options
@ -160,43 +136,53 @@ func (v *tableView) hints() hints {
return nil return nil
} }
func (v *tableView) resetTitle() { func (v *tableView) refresh() {
var title string v.update(v.data)
switch v.currentNS {
case resource.NotNamespaced:
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.table.GetRowCount()-1)
}
if !v.cmdBuff.empty() {
title += fmt.Sprintf("<[green::b]/%s[aqua::]> ", v.cmdBuff)
}
v.SetTitle(title)
} }
// Update table content // Update table content
func (v *tableView) update(data resource.TableData) { func (v *tableView) update(data resource.TableData) {
v.refresh.Lock() v.refreshMX.Lock()
{ {
v.data = data v.data = data
if !v.cmdBuff.empty() { if !v.cmdBuff.isActive() && !v.cmdBuff.empty() {
v.filter() v.doUpdate(v.filtered())
} else { } else {
v.doUpdate(data) v.doUpdate(data)
} }
v.resetTitle()
} }
v.refresh.Unlock() v.refreshMX.Unlock()
v.resetTitle()
}
func (v *tableView) filtered() resource.TableData {
if v.cmdBuff.empty() {
return v.data
}
rx, err := regexp.Compile(`(?i)` + v.cmdBuff.String())
if err != nil {
v.app.flash(flashErr, "Invalid filter expression")
v.cmdBuff.clear()
return v.data
}
filtered := resource.TableData{
Header: v.data.Header,
Rows: resource.RowEvents{},
Namespace: v.data.Namespace,
}
for k, row := range v.data.Rows {
f := strings.Join(row.Fields, " ")
if rx.MatchString(f) {
filtered.Rows[k] = row
}
}
return filtered
} }
func (v *tableView) doUpdate(data resource.TableData) { func (v *tableView) doUpdate(data resource.TableData) {
v.table.Clear() v.Clear()
v.currentNS = data.Namespace v.currentNS = data.Namespace
var row int var row int
@ -209,7 +195,7 @@ func (v *tableView) doUpdate(data resource.TableData) {
} }
c.SetTextColor(tcell.ColorWhite) c.SetTextColor(tcell.ColorWhite)
} }
v.table.SetCell(row, col, c) v.SetCell(row, col, c)
} }
row++ row++
@ -217,11 +203,13 @@ func (v *tableView) doUpdate(data resource.TableData) {
for k := range data.Rows { for k := range data.Rows {
keys = append(keys, k) keys = append(keys, k)
} }
v.sortFn(keys) if v.sortFn != nil {
v.sortFn(keys)
}
for _, k := range keys { for _, k := range keys {
fgColor := tcell.ColorGray fgColor := tcell.ColorGray
if v.colorer != nil { if v.colorerFn != nil {
fgColor = v.colorer(data.Namespace, data.Rows[k]) fgColor = v.colorerFn(data.Namespace, data.Rows[k])
} }
for col, f := range data.Rows[k].Fields { for col, f := range data.Rows[k].Fields {
c := tview.NewTableCell(deltas(data.Rows[k].Deltas[col], f)) c := tview.NewTableCell(deltas(data.Rows[k].Deltas[col], f))
@ -232,12 +220,36 @@ func (v *tableView) doUpdate(data resource.TableData) {
} }
c.SetTextColor(fgColor) c.SetTextColor(fgColor)
} }
v.table.SetCell(row, col, c) v.SetCell(row, col, c)
} }
row++ row++
} }
} }
func (v *tableView) resetTitle() {
var title string
rc := v.GetRowCount()
if rc > 0 {
rc--
}
switch v.currentNS {
case resource.NotNamespaced:
title = fmt.Sprintf(titleFmt, v.baseTitle, rc)
default:
ns := v.currentNS
if v.currentNS == resource.AllNamespaces {
ns = resource.AllNamespace
}
title = fmt.Sprintf(nsTitleFmt, v.baseTitle, ns, rc)
}
if !v.cmdBuff.isActive() && !v.cmdBuff.empty() {
title += fmt.Sprintf(searchFmt, v.cmdBuff)
}
v.SetTitle(title)
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Event listeners... // Event listeners...

View File

@ -1,91 +0,0 @@
package views
import (
"context"
"github.com/derailed/k9s/internal/resource"
"github.com/gdamore/tcell"
"github.com/k8sland/tview"
)
type xrayView struct {
*tview.Table
app *appView
actions keyActions
}
func newXrayView(app *appView) *xrayView {
v := xrayView{app: app, Table: tview.NewTable()}
v.SetBorder(true)
v.SetTitle(" Details ")
v.SetTitleColor(tcell.ColorAqua)
v.SetSelectable(true, false)
v.SetSelectedStyle(tcell.ColorBlack, tcell.ColorAqua, tcell.AttrNone)
v.SetInputCapture(v.keyboard)
return &v
}
func (v *xrayView) setTitle(t string) {
v.Table.SetTitle(t)
}
func (v *xrayView) clear() {
v.Table.Clear()
}
func (v *xrayView) blur() {
v.Table.Blur()
}
func (v *xrayView) init(_ context.Context) {
}
// SetActions to handle keyboard inputs
func (v *xrayView) setActions(aa keyActions) {
v.actions = aa
}
// Hints fetch mmemonic and hints
func (v *xrayView) hints() hints {
if v.actions != nil {
return v.actions.toHints()
}
return nil
}
func (v *xrayView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
if evt.Key() == tcell.KeyRune {
if a, ok := v.actions[evt.Key()]; ok {
a.action(evt)
evt = nil
}
} else {
if a, ok := v.actions[evt.Key()]; ok {
a.action(evt)
evt = nil
}
}
return evt
}
func (v *xrayView) update(pp resource.Properties) {
v.Clear()
var row int
for col, h := range pp["Headers"].(resource.Row) {
tc := tview.NewTableCell(h)
tc.SetExpansion(2)
v.SetCell(0, col, tc)
}
row++
for _, r := range pp["Rows"].([]resource.Row) {
for col, c := range r {
tc := tview.NewTableCell(c)
tc.SetExpansion(2)
v.SetCell(row, col, tc)
}
row++
}
}

View File

@ -1,157 +0,0 @@
package views
import (
"context"
"fmt"
"sort"
"strings"
"github.com/derailed/k9s/internal/resource"
"github.com/gdamore/tcell"
"github.com/k8sland/tview"
)
const (
keyColor = "#00FF00"
valColor = "#ADFF2F"
yamlTitleFmt = " [aqua::-] YAML [orange::-]%s"
)
type yamlView struct {
*tview.TextView
app *appView
actions keyActions
}
func newYamlView(app *appView) *yamlView {
v := yamlView{app: app, TextView: tview.NewTextView()}
{
v.SetBorder(true)
v.SetDynamicColors(true)
v.SetWrap(false)
v.SetTitleColor(tcell.ColorAqua)
v.SetInputCapture(v.keyboard)
}
return &v
}
func (v *yamlView) setTitle(t string) {
v.TextView.SetTitle(fmt.Sprintf(yamlTitleFmt, t))
}
func (v *yamlView) clear() {
v.TextView.Clear()
}
func (v *yamlView) blur() {
}
func (v *yamlView) init(_ context.Context) {
}
// SetActions to handle keyboard inputs
func (v *yamlView) setActions(aa keyActions) {
v.actions = aa
}
func (v *yamlView) hints() hints {
if v.actions != nil {
return v.actions.toHints()
}
return nil
}
func (v *yamlView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
if evt.Key() == tcell.KeyRune {
if a, ok := v.actions[evt.Key()]; ok {
a.action(evt)
evt = nil
}
} else {
if a, ok := v.actions[evt.Key()]; ok {
a.action(evt)
evt = nil
}
}
return evt
}
func (v *yamlView) isEmpty(c interface{}) bool {
switch c.(type) {
case []string:
return len(c.([]string)) == 0
case map[string]string:
return len(c.(map[string]string)) == 0
case map[string]interface{}:
return len(c.(map[string]interface{})) == 0
}
return false
}
func (v *yamlView) update(pp resource.Properties) {
v.Clear()
kk := make([]string, 0, len(pp))
for k := range pp {
kk = append(kk, k)
}
sort.Strings(kk)
var level int
for _, k := range kk {
if v.isEmpty(pp[k]) {
continue
}
fmt.Fprintf(v, "[%s::b]%-20s", keyColor, k+":")
v.textFor(level, pp[k])
fmt.Fprintf(v, "\n")
}
v.ScrollToBeginning()
v.app.Draw()
}
func (v *yamlView) textFor(level int, p interface{}) string {
var indent string
for i := 0; i < level; i++ {
if level < 1 {
indent += strings.Repeat(" ", 20)
} else {
indent += strings.Repeat(" ", 2)
}
}
switch p.(type) {
case string:
fmt.Fprintf(v, "[%s::-]%s", valColor, p.(string))
case []string:
aa := p.([]string)
for i, s := range aa {
if i == 0 {
fmt.Fprintf(v, "[%s::-]%s", valColor, s)
} else {
if level < 1 {
fmt.Fprintf(v, "%s[%s::-]%-20s%s", indent, valColor, "", s)
} else {
fmt.Fprintf(v, "%s[%s::-]%-13s%s", indent, valColor, "", s)
}
}
if i+1 < len(aa) {
fmt.Fprintf(v, "\n")
}
}
case map[string]interface{}:
m := p.(map[string]interface{})
indent += strings.Repeat(" ", 2)
fmt.Fprintf(v, "\n")
var i int
for key, val := range m {
fmt.Fprintf(v, "%s[%s::b]%-12s ", indent, keyColor, key+":")
v.textFor(level+1, val)
i++
if i < len(m) {
fmt.Fprintf(v, "\n")
}
}
}
return indent
}