diff --git a/go.mod b/go.mod index 1f39a550..1a02d10f 100644 --- a/go.mod +++ b/go.mod @@ -1,71 +1,48 @@ 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 ( - cloud.google.com/go v0.34.0 - contrib.go.opencensus.io/exporter/ocagent v0.4.3 - github.com/Azure/go-autorest v11.4.0+incompatible - github.com/census-instrumentation/opencensus-proto v0.1.0-0.20181214143942-ba49f56771b8 - github.com/davecgh/go-spew v1.1.1 - github.com/dgrijalva/jwt-go v3.2.0+incompatible - github.com/evanphx/json-patch v4.1.0+incompatible + contrib.go.opencensus.io/exporter/ocagent v0.4.3 // indirect + github.com/Azure/go-autorest v11.4.0+incompatible // indirect + github.com/derailed/tview v0.1.1 + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/evanphx/json-patch v4.1.0+incompatible // indirect github.com/fatih/camelcase v1.0.0 // indirect - github.com/gdamore/encoding v1.0.0 - github.com/gdamore/tcell v1.1.0 - github.com/gogo/protobuf v1.1.1 - github.com/golang/protobuf v1.2.0 - github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c - github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf - github.com/googleapis/gnostic v0.2.0 - github.com/gophercloud/gophercloud v0.0.0-20190206201033-83b528acebb4 - github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect - github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 - 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/gdamore/tcell v1.1.1 + github.com/gogo/protobuf v1.1.1 // indirect + github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect + github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect + github.com/googleapis/gnostic v0.2.0 // indirect + github.com/gophercloud/gophercloud v0.0.0-20190206201033-83b528acebb4 // indirect + github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect + github.com/imdario/mergo v0.3.6 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/json-iterator/go v1.1.5 // indirect github.com/kr/pretty v0.1.0 // indirect - github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08 - github.com/mattn/go-runewidth v0.0.4 - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd - github.com/modern-go/reflect2 v1.0.1 - github.com/onsi/gomega v1.4.3 - github.com/peterbourgon/diskv v2.0.1+incompatible + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/onsi/gomega v1.4.3 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/petergtz/pegomock v0.0.0-20181206220228-b113d17a7e81 - github.com/pmezard/go-difflib v1.0.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/sirupsen/logrus v1.3.0 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 - go.opencensus.io v0.19.0 - golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd - golang.org/x/net v0.0.0-20181217023233-e147a9138326 - golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 - 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 + go.opencensus.io v0.19.0 // indirect + golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd // indirect + golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.2.2 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/cli-runtime v0.0.0-20190207094101-a32b78e5dd0a 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/kubernetes v1.13.3 k8s.io/metrics v0.0.0-20181121073115-d8618695b08f k8s.io/utils v0.0.0-20190212002617-cdba02414f76 // indirect - sigs.k8s.io/structured-merge-diff v0.0.0-20190130003954-e5e029740eb8 - sigs.k8s.io/yaml v1.1.0 + sigs.k8s.io/structured-merge-diff v0.0.0-20190130003954-e5e029740eb8 // indirect + sigs.k8s.io/yaml v1.1.0 // indirect ) diff --git a/go.sum b/go.sum index 7cd8bbb6..5219b220 100644 --- a/go.sum +++ b/go.sum @@ -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/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/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/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/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 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/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/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= 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.0/go.mod h1:tqyG50u7+Ctv1w5VX67kLzKcj9YXR/JSBZQq/+mLl1A= +github.com/gdamore/tcell v1.1.1 h1:U73YL+jMem2XfhvaIUfPO6MpJawaG92B2funXVb9qLs= +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/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/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/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= @@ -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/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/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 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/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/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/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/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.6.2 h1:8KyC64BiO8ndiGHY5DlFWWdangUPC9QHPakFRre/Ud0= 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/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= 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/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= 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/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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 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/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 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/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/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 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/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 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/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= 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/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/rivo/tview v0.0.0-20190124120153-84fdb36408f3/go.mod h1:J4W+hErFfITUbyFAEXizpmkuxX7ZN56dopxHB4XQhMw= -github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= -github.com/sirupsen/logrus v1.2.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/rivo/tview v0.0.0-20190213202703-b373355e9db4/go.mod h1:J4W+hErFfITUbyFAEXizpmkuxX7ZN56dopxHB4XQhMw= +github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME= +github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 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/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 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/genproto v0.0.0-20180817151627-c66870c02cf8/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.17.0 h1:TRJYBgMclJvGYn2rIMjj+h9KtMt5r1Ij7ODVRIZkwhk= 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/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/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 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/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/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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= 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/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/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= 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/klog v0.1.0 h1:I5HMfc/DtuVaGR1KPwUrTc476K8NCqNBldC7H4dYEzk= 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/kubernetes v1.13.3 h1:46t44D87wKtdKFgr/lXM60K8xPrW0wO67Woof3Vsv6E= 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/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= 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= diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 70ee9b49..d75660db 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -6,8 +6,6 @@ import ( "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/views" - "github.com/gdamore/tcell" - "github.com/k8sland/tview" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" @@ -82,7 +80,7 @@ func initK9sConfig() { } ctx := cfg.CurrentContext - switch{ + switch { case isSet(k8sFlags.Context): ctx = *k8sFlags.Context config.Root.K9s.CurrentContext = ctx @@ -131,9 +129,6 @@ func run(cmd *cobra.Command, args []string) { log.SetFormatter(&log.TextFormatter{FullTimestamp: true, ForceColors: true}) initK9s() - initStyles() - initKeys() - app := views.NewApp() { 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() { k8sFlags = genericclioptions.NewConfigFlags(false) rootCmd.Flags().StringVar( @@ -266,4 +215,4 @@ func initK8sFlags() { "", "If present, the namespace scope for this CLI request", ) -} \ No newline at end of file +} diff --git a/internal/config/config.go b/internal/config/config.go index 6e00fab3..307a644b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -78,6 +78,8 @@ func (c *Config) FavNamespaces() []string { func (c *Config) SetActiveNamespace(ns string) { if c.K9s.ActiveCluster() != nil { c.K9s.ActiveCluster().Namespace.SetActive(ns) + } else { + log.Debug("Doh! no active cluster. unable to set active namespace") } } diff --git a/internal/config/k9s.go b/internal/config/k9s.go index 52ecb359..4fc21828 100644 --- a/internal/config/k9s.go +++ b/internal/config/k9s.go @@ -81,7 +81,7 @@ func (k *K9s) Validate(ks KubeSettings) { 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 } } diff --git a/internal/k8s/config.go b/internal/k8s/config.go index 114f1ea0..de24afad 100644 --- a/internal/k8s/config.go +++ b/internal/k8s/config.go @@ -203,12 +203,12 @@ func (c *Config) CurrentNamespaceName() (string, error) { return "", err } - if ctx, ok := cfg.Contexts[ctx]; ok { + if ctx, ok := cfg.Contexts[ctx]; ok { if isSet(&ctx.Namespace) { return ctx.Namespace, nil } } - return defaultNamespace, nil + return "", nil } // NamespaceNames fetch all available namespaces on current cluster. diff --git a/internal/k8s/job.go b/internal/k8s/job.go index 8b485f27..a82ff6bb 100644 --- a/internal/k8s/job.go +++ b/internal/k8s/job.go @@ -48,13 +48,13 @@ func (*Job) Delete(ns, n string) error { } // 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) if err != nil { return nil, err } 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. diff --git a/internal/k8s/pod.go b/internal/k8s/pod.go index 25a88555..bdda50b0 100644 --- a/internal/k8s/pod.go +++ b/internal/k8s/pod.go @@ -12,7 +12,7 @@ type ( // Loggable represents a K8s resource that has containers and can be logged. Loggable interface { 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 } @@ -58,7 +58,7 @@ func (*Pod) Delete(ns, n string) error { } // 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{} cc := []string{} 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 } + if includeInit { + for _, c := range po.Spec.InitContainers { + cc = append(cc, c.Name) + } + } for _, c := range po.Spec.Containers { cc = append(cc, c.Name) } diff --git a/internal/resource/helpers.go b/internal/resource/helpers.go index d6ba0fb2..3b6a3bd1 100644 --- a/internal/resource/helpers.go +++ b/internal/resource/helpers.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/k8sland/tview" + "github.com/derailed/tview" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/duration" "k8s.io/apimachinery/pkg/watch" diff --git a/internal/resource/job.go b/internal/resource/job.go index 1c5c65cc..35914733 100644 --- a/internal/resource/job.go +++ b/internal/resource/job.go @@ -74,11 +74,13 @@ func (r *Job) Marshal(path string) (string, error) { 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) - 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) { req := r.caller.(k8s.Loggable).Logs(ns, n, co, lines, prev) ctx, cancel := context.WithCancel(context.TODO()) diff --git a/internal/resource/list.go b/internal/resource/list.go index c5f922f4..6e53b8e4 100644 --- a/internal/resource/list.go +++ b/internal/resource/list.go @@ -5,7 +5,6 @@ import ( "sort" "github.com/derailed/k9s/internal/k8s" - log "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/watch" ) @@ -219,7 +218,6 @@ func (l *list) Reconcile() 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 { return err } diff --git a/internal/resource/pod.go b/internal/resource/pod.go index 1accb2de..2b2a4538 100644 --- a/internal/resource/pod.go +++ b/internal/resource/pod.go @@ -17,7 +17,7 @@ const defaultTimeout = 1 * time.Second type ( // Container represents a resource that encompass multiple containers. Container interface { - Containers(path string) ([]string, error) + Containers(path string, includeInit bool) ([]string, error) } // 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. -func (r *Pod) Containers(path string) ([]string, error) { +func (r *Pod) Containers(path string, includeInit bool) ([]string, error) { 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 diff --git a/internal/views/alias.go b/internal/views/alias.go new file mode 100644 index 00000000..700f129b --- /dev/null +++ b/internal/views/alias.go @@ -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)) +} diff --git a/internal/views/app.go b/internal/views/app.go index a5fb4ea3..5c6ffb39 100644 --- a/internal/views/app.go +++ b/internal/views/app.go @@ -3,12 +3,11 @@ package views import ( "context" "fmt" - "os" "time" "github.com/derailed/k9s/internal/config" + "github.com/derailed/tview" "github.com/gdamore/tcell" - "github.com/k8sland/tview" log "github.com/sirupsen/logrus" "k8s.io/cli-runtime/pkg/genericclioptions" ) @@ -24,6 +23,10 @@ type ( init(ctx context.Context, ns string) } + keyHandler interface { + keyboard(evt *tcell.EventKey) *tcell.EventKey + } + resourceViewer interface { igniter } @@ -44,35 +47,47 @@ type ( cancel context.CancelFunc cmdBuff *cmdBuff cmdView *cmdView + actions keyActions } ) +func init() { + initKeys() + initStyles() +} + // NewApp returns a K9s app instance. func NewApp() *appView { - var app appView + v := appView{Application: tview.NewApplication()} { - app = appView{ - Application: tview.NewApplication(), - pages: tview.NewPages(), - menuView: newMenuView(), - content: tview.NewPages(), - cmdBuff: newCmdBuff(':'), - cmdView: newCmdView('🐶'), - } - app.command = newCommand(&app) - app.focusChanged = app.changedFocus - app.SetInputCapture(app.keyboard) + v.pages = tview.NewPages() + v.actions = make(keyActions) + v.menuView = newMenuView() + v.content = tview.NewPages() + v.cmdBuff = newCmdBuff(':') + v.cmdView = newCmdView('🐶') + v.command = newCommand(&v) + v.flashView = newFlashView(v.Application, "Initializing...") + v.infoView = newInfoView(&v) + v.focusChanged = v.changedFocus + 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) { a.version = v - a.infoView = newInfoView(a) a.infoView.init() - - a.flashView = newFlashView(a.Application, "Initializing...") - a.cmdBuff.addListener(a.cmdView) header := tview.NewFlex() @@ -114,51 +129,81 @@ func (a *appView) Run() { func (a *appView) keyboard(evt *tcell.EventKey) *tcell.EventKey { key := evt.Key() 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() { a.cmdBuff.add(evt.Rune()) } - return evt + key = tcell.Key(evt.Rune()) } - - switch evt.Key() { - case tcell.KeyCtrlR: - 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() + if a, ok := a.actions[key]; ok { + log.Debug(">> AppView handled key: ", tcell.KeyNames[key]) + return a.action(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) { a.pages.SwitchToPage(p) } @@ -195,18 +240,6 @@ func (a *appView) 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) { a.flashView.setMessage(level, m...) } @@ -260,3 +293,10 @@ func (a *appView) nextFocus() { } return } + +func initStyles() { + tview.Styles.PrimitiveBackgroundColor = tcell.ColorBlack + tview.Styles.ContrastBackgroundColor = tcell.ColorBlack + tview.Styles.FocusColor = tcell.ColorLightSkyBlue + tview.Styles.BorderColor = tcell.ColorDodgerBlue +} diff --git a/internal/views/cmd.go b/internal/views/cmd.go index 5a92ee26..e6d6f1e8 100644 --- a/internal/views/cmd.go +++ b/internal/views/cmd.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/gdamore/tcell" - "github.com/k8sland/tview" + "github.com/derailed/tview" log "github.com/sirupsen/logrus" ) diff --git a/internal/views/cmd_buff.go b/internal/views/cmd_buff.go index 27262419..e36ed793 100644 --- a/internal/views/cmd_buff.go +++ b/internal/views/cmd_buff.go @@ -49,6 +49,10 @@ func (c *cmdBuff) del() { c.fireChanged() } +func (c *cmdBuff) wipe() { + c.buff = make([]rune, 0, maxBuff) +} + func (c *cmdBuff) clear() { c.buff = make([]rune, 0, maxBuff) c.fireChanged() diff --git a/internal/views/colorer.go b/internal/views/colorer.go index 262c6568..f07ea23a 100644 --- a/internal/views/colorer.go +++ b/internal/views/colorer.go @@ -29,6 +29,10 @@ func defaultColorer(ns string, r *resource.RowEvent) tcell.Color { return c } +func aliasColorer(string, *resource.RowEvent) tcell.Color { + return tcell.ColorFuchsia +} + func podColorer(ns string, r *resource.RowEvent) tcell.Color { c := defaultColorer(ns, r) @@ -37,7 +41,7 @@ func podColorer(ns string, r *resource.RowEvent) tcell.Color { statusCol = 2 } switch strings.TrimSpace(r.Fields[statusCol]) { - case "ContainerCreating": + case "ContainerCreating", "PodInitializing": return addColor case "Terminating", "Initialized": return highlightColor diff --git a/internal/views/command.go b/internal/views/command.go index bedde239..2917cdf5 100644 --- a/internal/views/command.go +++ b/internal/views/command.go @@ -27,9 +27,11 @@ func (c *command) run(cmd string) { var v igniter switch cmd { case "q", "quit": - c.app.quit(nil) + c.app.Stop() + return case "?", "help", "alias": - c.app.help(nil) + c.app.inject(newAliasView(c.app)) + return default: if res, ok := cmdMap[cmd]; ok { v = res.viewFn(res.title, c.app, res.listFn(resource.DefaultNamespace), res.colorerFn) diff --git a/internal/views/context.go b/internal/views/context.go index 8b6b2ea6..ece347df 100644 --- a/internal/views/context.go +++ b/internal/views/context.go @@ -21,9 +21,9 @@ func newContextView(t string, app *appView, list resource.List, c colorerFn) res return &v } -func (v *contextView) useContext(*tcell.EventKey) { +func (v *contextView) useContext(evt *tcell.EventKey) *tcell.EventKey { if !v.rowSelected() { - return + return evt } ctx := strings.TrimSpace(v.selectedItem) @@ -37,7 +37,7 @@ func (v *contextView) useContext(*tcell.EventKey) { err := v.list.Resource().(*resource.Context).Switch(ctx) if err != nil { v.app.flash(flashWarn, err.Error()) - return + return evt } config.Root.Reset() @@ -46,10 +46,11 @@ func (v *contextView) useContext(*tcell.EventKey) { v.app.flash(flashInfo, "Switching context to", ctx) v.refresh() 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) { - aa[KeyU] = keyAction{description: "Use", action: v.useContext} + aa[KeyU] = newKeyAction("Use", v.useContext) } diff --git a/internal/views/cronjob.go b/internal/views/cronjob.go index 99aa9650..691b2733 100644 --- a/internal/views/cronjob.go +++ b/internal/views/cronjob.go @@ -20,17 +20,19 @@ func newCronJobView(t string, app *appView, list resource.List, c colorerFn) res return &v } -func (v *cronJobView) trigger(*tcell.EventKey) { +func (v *cronJobView) trigger(evt *tcell.EventKey) *tcell.EventKey { if !v.rowSelected() { - return + return evt } 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 { v.app.flash(flashErr, "Boom!", err.Error()) + return evt } + return nil } func (v *cronJobView) extraActions(aa keyActions) { - aa[tcell.KeyCtrlT] = keyAction{description: "Trigger", action: v.trigger} + aa[tcell.KeyCtrlT] = newKeyAction("Trigger", v.trigger) } diff --git a/internal/views/details.go b/internal/views/details.go index 82c81d82..bc9e75f0 100644 --- a/internal/views/details.go +++ b/internal/views/details.go @@ -2,27 +2,59 @@ package views import ( "fmt" + "regexp" + "strconv" + "strings" + "github.com/derailed/tview" "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 { *tview.TextView - actions keyActions - category string + app *appView + actions keyActions + title string + category string + cmdBuff *cmdBuff + backFn actionHandler + numSelections int } -func newDetailsView() *detailsView { - v := detailsView{TextView: tview.NewTextView()} - v.TextView.SetDynamicColors(true) - v.TextView.SetBorder(true) - v.SetTitleColor(tcell.ColorAqua) - v.SetInputCapture(v.keyboard) +func newDetailsView(app *appView, backFn actionHandler) *detailsView { + v := detailsView{TextView: tview.NewTextView(), app: app, actions: make(keyActions)} + { + v.backFn = backFn + v.SetScrollable(true) + 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 } @@ -31,23 +63,120 @@ func (v *detailsView) setCategory(n string) { } func (v *detailsView) 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 + key := evt.Key() + if key == tcell.KeyRune { + if v.cmdBuff.isActive() { + v.cmdBuff.add(evt.Rune()) + v.refreshTitle() + return 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 } +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! / 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 func (v *detailsView) setActions(aa keyActions) { - v.actions = aa + for k, a := range aa { + v.actions[k] = a + } } // Hints fetch mmemonic and hints @@ -58,6 +187,27 @@ func (v *detailsView) hints() hints { return nil } -func (v *detailsView) setTitle(t string) { - v.SetTitle(fmt.Sprintf(detailFmt, t, v.category)) +func (v *detailsView) refreshTitle() { + 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") } diff --git a/internal/views/details_test.go b/internal/views/details_test.go new file mode 100644 index 00000000..a7024f5e --- /dev/null +++ b/internal/views/details_test.go @@ -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")) +} diff --git a/internal/views/exec.go b/internal/views/exec.go index 578d1a87..e6776c53 100644 --- a/internal/views/exec.go +++ b/internal/views/exec.go @@ -7,12 +7,13 @@ import ( log "github.com/sirupsen/logrus" ) -func run(app *appView, args ...string) { - app.Suspend(func() { +func run(app *appView, args ...string) bool { + return app.Suspend(func() { if err := execute(args...); err != nil { log.Error("Command failed:", err, args) app.flash(flashErr, "Doh! command failed", err.Error()) } + log.Debug("Command Done Running..") }) } diff --git a/internal/views/flash.go b/internal/views/flash.go index 9878d250..082007ff 100644 --- a/internal/views/flash.go +++ b/internal/views/flash.go @@ -6,7 +6,7 @@ import ( "time" "github.com/gdamore/tcell" - "github.com/k8sland/tview" + "github.com/derailed/tview" ) const ( diff --git a/internal/views/help.go b/internal/views/help.go deleted file mode 100644 index c8a26910..00000000 --- a/internal/views/help.go +++ /dev/null @@ -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() -} diff --git a/internal/views/info.go b/internal/views/info.go index 658b9992..0944361f 100644 --- a/internal/views/info.go +++ b/internal/views/info.go @@ -3,7 +3,7 @@ package views import ( "github.com/derailed/k9s/internal/resource" "github.com/gdamore/tcell" - "github.com/k8sland/tview" + "github.com/derailed/tview" log "github.com/sirupsen/logrus" ) diff --git a/internal/views/job.go b/internal/views/job.go index c7bec26a..a43c1f81 100644 --- a/internal/views/job.go +++ b/internal/views/job.go @@ -12,7 +12,9 @@ type jobView struct { func newJobView(t string, app *appView, list resource.List, c colorerFn) resourceViewer { v := jobView{newResourceView(t, app, list, c).(*resourceView)} - v.extraActionsFn = v.extraActions + { + v.extraActionsFn = v.extraActions + } v.AddPage("logs", newLogsView(&v), true, false) v.switchPage("job") return &v @@ -23,23 +25,31 @@ func newJobView(t string, app *appView, list resource.List, c colorerFn) resourc func (v *jobView) appView() *appView { return v.app } + +func (v *jobView) backFn() actionHandler { + return v.backCmd +} + func (v *jobView) getList() resource.List { return v.list } + func (v *jobView) getSelection() string { return v.selectedItem } // Handlers... -func (v *jobView) logs(*tcell.EventKey) { +func (v *jobView) logs(evt *tcell.EventKey) *tcell.EventKey { if !v.rowSelected() { - return + return evt } - cc, err := fetchContainers(v.list, v.selectedItem) + cc, err := fetchContainers(v.list, v.selectedItem, true) if err != nil { + v.app.flash(flashErr, err.Error()) log.Error(err) + return evt } l := v.GetPrimitive("logs").(*logsView) @@ -50,8 +60,9 @@ func (v *jobView) logs(*tcell.EventKey) { v.switchPage("logs") l.init() + return nil } func (v *jobView) extraActions(aa keyActions) { - aa[KeyL] = newKeyHandler("Logs", v.logs) + aa[KeyL] = newKeyAction("Logs", v.logs) } diff --git a/internal/views/log.go b/internal/views/log.go index 41f02b4e..1d257602 100644 --- a/internal/views/log.go +++ b/internal/views/log.go @@ -3,29 +3,20 @@ package views import ( "fmt" - "github.com/k8sland/tview" -) - -const ( - logTitleFmt = " [aqua::b]Logs %s ([fuchsia::-]container=[fuchsia::b]%s[aqua::b]) " + log "github.com/sirupsen/logrus" ) type logView struct { - *tview.TextView + *detailsView } 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.SetTitle(fmt.Sprintf(logTitleFmt, parent.getSelection(), title)) - v.SetWrap(false) - v.SetChangedFunc(func() { - parent.appView().Draw() - }) + v.setCategory("Logs") + v.setTitle(parent.getSelection()) } return &v } diff --git a/internal/views/logs.go b/internal/views/logs.go index 615e93b5..ccbb375e 100644 --- a/internal/views/logs.go +++ b/internal/views/logs.go @@ -8,8 +8,8 @@ import ( "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/resource" + "github.com/derailed/tview" "github.com/gdamore/tcell" - "github.com/k8sland/tview" log "github.com/sirupsen/logrus" ) @@ -42,8 +42,8 @@ func newLogsView(parent loggable) *logsView { KeyC: {description: "Clear", action: v.clearLogs}, KeyU: {description: "Top", action: v.top}, KeyD: {description: "Bottom", action: v.bottom}, - KeyF: {description: "PageUp", action: v.pageUp}, - KeyB: {description: "PageDown", action: v.pageDown}, + KeyF: {description: "Up", action: v.pageUp}, + KeyB: {description: "Down", action: v.pageDown}, }) v.SetInputCapture(v.keyboard) @@ -57,26 +57,26 @@ func (v *logsView) init() { } func (v *logsView) keyboard(evt *tcell.EventKey) *tcell.EventKey { - if m, ok := v.actions[evt.Key()]; ok { - m.action(evt) - 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) + if kv, ok := v.CurrentPage().Item.(keyHandler); ok { + if kv.keyboard(evt) == nil { return nil } } - if i, err := strconv.Atoi(string(evt.Rune())); err == nil { - if _, ok := numKeys[i]; ok { - v.load(i - 1) + key := evt.Key() + if key == tcell.KeyRune { + 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 } @@ -90,7 +90,7 @@ func (v *logsView) setActions(aa keyActions) { func (v *logsView) hints() hints { if len(v.containers) > 1 { 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() @@ -99,7 +99,9 @@ func (v *logsView) hints() hints { func (v *logsView) addContainer(n string) { v.containers = append(v.containers, n) l := newLogView(n, v.parent) - l.SetInputCapture(v.keyboard) + { + l.SetInputCapture(v.keyboard) + } v.AddPage(n, l, true, false) } @@ -145,6 +147,7 @@ func (v *logsView) doLoad(path, co string) error { c := make(chan string) go func() { l, count, first := v.CurrentPage().Item.(*logView), 0, true + l.setTitle(path + ":" + co) for { select { case line, ok := <-c: @@ -189,50 +192,55 @@ func (v *logsView) doLoad(path, co string) error { return err } v.cancelFunc = cancelFn - return nil } // ---------------------------------------------------------------------------- // Actions... -func (v *logsView) back(*tcell.EventKey) { +func (v *logsView) back(evt *tcell.EventKey) *tcell.EventKey { v.stop() 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 { v.parent.appView().flash(flashInfo, "Top logs...") 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 { v.parent.appView().flash(flashInfo, "Bottom logs...") 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 { v.parent.appView().flash(flashInfo, "Page Up logs...") 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 { v.parent.appView().flash(flashInfo, "Page Down logs...") 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 { v.parent.appView().flash(flashInfo, "Clearing logs...") v.buffer.clear() p.Item.(*logView).Clear() } + return nil } diff --git a/internal/views/menu.go b/internal/views/menu.go index 38e62233..80fbf898 100644 --- a/internal/views/menu.go +++ b/internal/views/menu.go @@ -2,25 +2,27 @@ package views import ( "fmt" + "regexp" "sort" "strconv" "strings" "github.com/derailed/k9s/internal/resource" + "github.com/derailed/tview" "github.com/gdamore/tcell" - "github.com/k8sland/tview" + log "github.com/sirupsen/logrus" ) const ( - menuSepFmt = " [dodgerblue::b]%-8s [white::d]%s " + menuSepFmt = " [dodgerblue::b]%-9s [white::d]%s " menuIndexFmt = " [fuchsia::b]<%d> [white::d]%s " maxRows = 6 colLen = 20 ) -type ( - keyboardHandler func(*tcell.EventKey) +var menuRX = regexp.MustCompile(`\d`) +type ( hint struct { mnemonic, display string } @@ -29,37 +31,43 @@ type ( hinter interface { hints() hints } - - keyAction struct { - description string - action keyboardHandler - } - keyActions map[tcell.Key]keyAction - - menuView struct { - *tview.Table - } ) func (h hints) Len() int { return len(h) } + func (h hints) Swap(i, j int) { h[i], h[j] = h[j], h[i] } + func (h hints) Less(i, j int) bool { n, err1 := strconv.Atoi(h[i].mnemonic) m, err2 := strconv.Atoi(h[j].mnemonic) - if err1 == nil && err2 == nil { return n < m } - - d := strings.Compare(h[i].mnemonic, h[j].mnemonic) - return d < 0 + if err1 == nil && err2 != nil { + return true + } + 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} } @@ -68,6 +76,11 @@ func newMenuView() *menuView { return &v } +// ----------------------------------------------------------------------------- +type menuView struct { + *tview.Table +} + func (v *menuView) setMenu(hh hints) { v.Clear() sort.Sort(hh) @@ -75,17 +88,12 @@ func (v *menuView) setMenu(hh hints) { var row, col int firstNS, firstCmd := true, true for _, h := range hh { - _, err := strconv.Atoi(h.mnemonic) - if err == nil && firstNS { - row = 0 - col = 2 - firstNS = false + isDigit := menuRX.MatchString(h.mnemonic) + if isDigit && firstNS { + row, col, firstNS = 0, 2, false } - - if err != nil && firstCmd { - row = 0 - col = 0 - firstCmd = false + if !isDigit && firstCmd { + row, col, firstCmd = 0, 0, false } c := tview.NewTableCell(v.item(h)) v.SetCell(row, col, c) @@ -106,14 +114,25 @@ func (v *menuView) item(h hint) string { if err == nil { return fmt.Sprintf(menuIndexFmt, i, resource.Truncate(h.display, 14)) } - 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 { kk := make([]int, 0, len(a)) for k := range a { - kk = append(kk, int(k)) + if !a.skipKey(k) { + kk = append(kk, int(k)) + } } sort.Ints(kk) @@ -123,6 +142,8 @@ func (a keyActions) toHints() hints { hh = append(hh, hint{ mnemonic: name, display: a[tcell.Key(k)].description}) + } else { + log.Errorf("Unable to local KeyName for %#v", k) } } return hh @@ -173,7 +194,39 @@ const ( KeyX KeyY 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{ @@ -188,3 +241,71 @@ var numKeys = map[int]int32{ 8: Key8, 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)] = "/" +} diff --git a/internal/views/namespace.go b/internal/views/namespace.go index 04aebac4..32bdcd1f 100644 --- a/internal/views/namespace.go +++ b/internal/views/namespace.go @@ -2,7 +2,7 @@ package views import ( "fmt" - "strings" + "regexp" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/resource" @@ -15,40 +15,43 @@ const ( deltaNSIndicator = "(𝜟)" ) +var nsCleanser = regexp.MustCompile(`(\w+)[+|(*)|(𝜟)]*`) + type namespaceView struct { *resourceView } func newNamespaceView(t string, app *appView, list resource.List, c colorerFn) resourceViewer { - v := namespaceView{ - resourceView: newResourceView(t, app, list, c).(*resourceView), - } + v := namespaceView{newResourceView(t, app, list, c).(*resourceView)} v.extraActionsFn = v.extraActions + v.selectedFn = v.getSelectedItem v.decorateDataFn = v.decorate v.switchPage("ns") return &v } -func (v *namespaceView) useNamespace(*tcell.EventKey) { - if !v.rowSelected() { - 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) { + aa[KeyU] = newKeyAction("Use", v.useNamespace) } -func (v *namespaceView) extraActions(aa keyActions) { - aa[KeyU] = keyAction{description: "Use", action: v.useNamespace} +func (v *namespaceView) useNamespace(evt *tcell.EventKey) *tcell.EventKey { + 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 { @@ -59,18 +62,15 @@ func (v *namespaceView) decorate(data resource.TableData) resource.TableData { Deltas: resource.Row{"", "", ""}, } } - for k, v := range data.Rows { if config.InList(config.Root.FavNamespaces(), k) { v.Fields[0] += "+" v.Action = resource.Unchanged } - if config.Root.ActiveNamespace() == k { v.Fields[0] += "(*)" v.Action = resource.Unchanged } } - return data } diff --git a/internal/views/namespace_test.go b/internal/views/namespace_test.go new file mode 100644 index 00000000..1051bec4 --- /dev/null +++ b/internal/views/namespace_test.go @@ -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)) + } +} diff --git a/internal/views/pod.go b/internal/views/pod.go index 6e76a49d..4bd317eb 100644 --- a/internal/views/pod.go +++ b/internal/views/pod.go @@ -12,6 +12,7 @@ type podView struct { type loggable interface { appView() *appView + backFn() actionHandler getSelection() string getList() resource.List switchPage(n string) @@ -19,7 +20,9 @@ type loggable interface { func newPodView(t string, app *appView, list resource.List, c colorerFn) resourceViewer { v := podView{newResourceView(t, app, list, c).(*resourceView)} - v.extraActionsFn = v.extraActions + { + v.extraActionsFn = v.extraActions + } 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) }) picker.setActions(keyActions{ - tcell.KeyEscape: {description: "Back", action: v.back}, + tcell.KeyEscape: {description: "Back", action: v.backCmd}, }) v.AddPage("choose", picker, true, false) } @@ -40,56 +43,60 @@ func newPodView(t string, app *appView, list resource.List, c colorerFn) resourc // Protocol... +func (v *podView) backFn() actionHandler { + return v.backCmd +} + func (v *podView) appView() *appView { return v.app } + func (v *podView) getList() resource.List { return v.list } + func (v *podView) getSelection() string { return v.selectedItem } // Handlers... -func (v *podView) logs(*tcell.EventKey) { +func (v *podView) logsCmd(evt *tcell.EventKey) *tcell.EventKey { if !v.rowSelected() { - return + return evt } - - cc, err := fetchContainers(v.list, v.selectedItem) + cc, err := fetchContainers(v.list, v.selectedItem, true) if err != nil { + v.app.flash(flashErr, err.Error()) log.Error(err) + return evt } - l := v.GetPrimitive("logs").(*logsView) l.deleteAllPages() for _, c := range cc { l.addContainer(c) } - v.switchPage("logs") l.init() + return nil } -func (v *podView) shell(*tcell.EventKey) { +func (v *podView) shellCmd(evt *tcell.EventKey) *tcell.EventKey { if !v.rowSelected() { - return + return evt } - - cc, err := fetchContainers(v.list, v.selectedItem) + cc, err := fetchContainers(v.list, v.selectedItem, false) if err != nil { + v.app.flash(flashErr, err.Error()) log.Error("Error fetching containers", err) - return + return evt } - if len(cc) == 1 { v.sshInto(v.selectedItem, "") - return + return nil } - v.showPicker(cc) - return + return nil } func (v *podView) showPicker(cc []string) { @@ -109,13 +116,13 @@ func (v *podView) sshInto(path, co string) { } func (v *podView) extraActions(aa keyActions) { - aa[KeyL] = newKeyHandler("Logs", v.logs) - aa[KeyS] = newKeyHandler("Shell", v.shell) + aa[KeyL] = newKeyAction("Logs", v.logsCmd) + 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 { return []string{}, nil } - return l.Resource().(resource.Container).Containers(po) + return l.Resource().(resource.Container).Containers(po, includeInit) } diff --git a/internal/views/resource.go b/internal/views/resource.go index 58f31257..ace0bc20 100644 --- a/internal/views/resource.go +++ b/internal/views/resource.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "path" - "regexp" "strconv" "strings" "sync" @@ -13,8 +12,8 @@ import ( "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/resource" + "github.com/derailed/tview" "github.com/gdamore/tcell" - "github.com/k8sland/tview" log "github.com/sirupsen/logrus" ) @@ -40,6 +39,7 @@ type ( update sync.Mutex list resource.List extraActionsFn func(keyActions) + selectedFn func() string 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.SetColorer(c) - tv.table.SetSelectionChangedFunc(v.selChanged) + tv.SetSelectionChangedFunc(v.selChanged) } v.AddPage(v.list.GetName(), tv, true, true) - var xray details - 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}, - }) - + details := newDetailsView(app, v.backCmd) v.AddPage("details", details, true, false) - v.AddPage("xray", xray, true, false) - return &v } // Init watches all running pods in given namespace func (v *resourceView) init(ctx context.Context, ns string) { - details := v.GetPrimitive("xray").(details) - details.clear() - v.selectedItem, v.selectedNS = noSelection, ns go func(ctx context.Context) { @@ -103,7 +84,7 @@ func (v *resourceView) init(ctx context.Context, ns string) { }(ctx) v.refreshActions() 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) } +func (v *resourceView) getSelectedItem() string { + if v.selectedFn != nil { + return v.selectedFn() + } + return v.selectedItem +} + // Protocol... // Hints fetch menu hints @@ -126,89 +114,89 @@ func (v *resourceView) hints() hints { return v.CurrentPage().Item.(hinter).hints() } +// ---------------------------------------------------------------------------- // Actions... -func (v *resourceView) back(*tcell.EventKey) { +func (v *resourceView) backCmd(*tcell.EventKey) *tcell.EventKey { 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() { - return + return evt } - v.getTV().setDeleted() - v.app.flash(flashInfo, fmt.Sprintf("Deleting %s %s", v.list.GetName(), v.selectedItem)) - if err := v.list.Resource().Delete(v.selectedItem); err != nil { + sel := v.getSelectedItem() + 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.selectedItem = noSelection + return nil } -func (v *resourceView) describe(*tcell.EventKey) { +func (v *resourceView) describeCmd(evt *tcell.EventKey) *tcell.EventKey { if !v.rowSelected() { - return + return evt } - - selected := v.selectedItem - selected = strings.Replace(selected, "+", "", -1) - selected = strings.Replace(selected, "(*)", "", -1) - - raw, err := v.list.Resource().Describe(v.title, selected) + sel := v.getSelectedItem() + raw, err := v.list.Resource().Describe(v.title, sel) 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) - return + return evt } - - var re = regexp.MustCompile(`(?m:(^(.+)$))`) - str := re.ReplaceAllString(string(raw), `[aqua]$1`) - details := v.GetPrimitive("details").(*detailsView) - details.ScrollToBeginning() - details.setCategory("DESC") - details.SetText(str) - details.setTitle(selected) + { + details.setCategory("Describe") + details.setTitle(sel) + details.SetTextColor(tcell.ColorAqua) + details.SetText(string(raw)) + details.ScrollToBeginning() + } v.switchPage("details") + return nil } -func (v *resourceView) view(*tcell.EventKey) { +func (v *resourceView) viewCmd(evt *tcell.EventKey) *tcell.EventKey { if !v.rowSelected() { - return + return evt } - - raw, err := v.list.Resource().Marshal(v.selectedItem) + sel := v.getSelectedItem() + raw, err := v.list.Resource().Marshal(sel) if err != nil { v.app.flash(flashErr, "Unable to marshal resource", err.Error()) 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.ScrollToBeginning() - details.setCategory("YAML") - details.SetText(str) - details.setTitle(v.selectedItem) + { + details.setCategory("View") + details.setTitle(sel) + details.SetTextColor(tcell.ColorMediumAquamarine) + details.SetText(string(raw)) + details.ScrollToBeginning() + } v.switchPage("details") + return nil } -func (v *resourceView) edit(*tcell.EventKey) { +func (v *resourceView) editCmd(evt *tcell.EventKey) *tcell.EventKey { if !v.rowSelected() { - return + return evt } v.app.flash(flashInfo, fmt.Sprintf("Editing %s %s", v.title, v.selectedItem)) ns, s := namespaced(v.selectedItem) 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())) ns := v.namespaces[i] v.doSwitchNamespace(ns) + return nil } func (v *resourceView) doSwitchNamespace(ns string) { @@ -225,7 +213,7 @@ func (v *resourceView) doSwitchNamespace(ns string) { v.refresh() v.selectItem(0, 0) v.getTV().resetTitle() - v.getTV().table.Select(0, 0) + v.getTV().Select(0, 0) v.app.cmdBuff.reset() config.Root.SetActiveNamespace(v.selectedNS) config.Root.Save() @@ -275,16 +263,16 @@ func (v *resourceView) selectItem(r, c int) { t := v.getTV() switch v.list.GetNamespace() { 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: v.selectedItem = path.Join( - strings.TrimSpace(t.table.GetCell(r, 0).Text), - strings.TrimSpace(t.table.GetCell(r, 1).Text), + strings.TrimSpace(t.GetCell(r, 0).Text), + strings.TrimSpace(t.GetCell(r, 1).Text), ) default: v.selectedItem = path.Join( 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.SwitchToPage(p) - h := v.GetPrimitive(p).(hinter) v.selectedNS = v.list.GetNamespace() + h := v.GetPrimitive(p).(hinter) v.app.setHints(h.hints()) 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) { v.namespaces = make(map[int]string, config.MaxFavoritesNS) 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 } } if v.list.Access(resource.EditAccess) { - aa[KeyE] = newKeyHandler("Edit", v.edit) + aa[KeyE] = newKeyAction("Edit", v.editCmd) } 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) { - aa[KeyV] = newKeyHandler("View", v.view) + aa[KeyV] = newKeyAction("View", v.viewCmd) } 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 { v.extraActionsFn(aa) diff --git a/internal/views/select_list.go b/internal/views/select_list.go index a2659fce..73c3d2b2 100644 --- a/internal/views/select_list.go +++ b/internal/views/select_list.go @@ -3,8 +3,8 @@ package views import ( "strconv" + "github.com/derailed/tview" "github.com/gdamore/tcell" - "github.com/k8sland/tview" ) type selectList struct { @@ -15,15 +15,17 @@ type selectList struct { func newSelectList() *selectList { v := selectList{List: tview.NewList()} - v.SetBorder(true) - v.SetTitle(" Please select a Container ") - v.SetInputCapture(func(evt *tcell.EventKey) *tcell.EventKey { - if a, ok := v.actions[evt.Key()]; ok { - a.action(evt) - evt = nil - } - return evt - }) + { + v.SetBorder(true) + v.SetTitle(" [aqua::b]Container Selector ") + v.SetInputCapture(func(evt *tcell.EventKey) *tcell.EventKey { + if a, ok := v.actions[evt.Key()]; ok { + a.action(evt) + evt = nil + } + return evt + }) + } return &v } diff --git a/internal/views/splash.go b/internal/views/splash.go index b838571f..3aa7d1fa 100644 --- a/internal/views/splash.go +++ b/internal/views/splash.go @@ -5,7 +5,7 @@ import ( "strings" "github.com/gdamore/tcell" - "github.com/k8sland/tview" + "github.com/derailed/tview" ) const ( diff --git a/internal/views/table.go b/internal/views/table.go index 53f2ee76..c3b3c9b0 100644 --- a/internal/views/table.go +++ b/internal/views/table.go @@ -7,149 +7,125 @@ import ( "sync" "github.com/derailed/k9s/internal/resource" + "github.com/derailed/tview" "github.com/gdamore/tcell" - "github.com/k8sland/tview" log "github.com/sirupsen/logrus" ) const ( 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::-] " ) type ( tableView struct { - *tview.Flex + *tview.Table app *appView baseTitle string currentNS string - refresh sync.Mutex + refreshMX sync.Mutex actions keyActions - colorer colorerFn + colorerFn colorerFn sortFn resource.SortFn - table *tview.Table data resource.TableData cmdBuff *cmdBuff } ) 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.sortFn = sortFn + v.actions = make(keyActions) v.SetBorder(true) v.SetBorderColor(tcell.ColorDodgerBlue) v.SetBorderAttributes(tcell.AttrBold) v.SetBorderPadding(0, 0, 1, 1) v.cmdBuff = newCmdBuff('/') + v.cmdBuff.addListener(app.cmdView) + v.cmdBuff.reset() + v.SetSelectable(true, false) + v.SetSelectedStyle(tcell.ColorBlack, tcell.ColorAqua, tcell.AttrBold) + v.SetInputCapture(v.keyboard) } - v.cmdBuff.addListener(app.cmdView) - v.cmdBuff.reset() + v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd) + 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 } -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 { key := evt.Key() - if evt.Key() == tcell.KeyRune { + if key == tcell.KeyRune { if v.cmdBuff.isActive() { v.cmdBuff.add(evt.Rune()) - } - 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 + return nil } key = tcell.Key(evt.Rune()) } if a, ok := v.actions[key]; ok { - if !v.app.cmdView.inCmdMode() { - 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() - } + log.Debug(">> TableView handled ", tcell.KeyNames[key]) + return a.action(evt) } return evt } -func (v *tableView) filter() { - v.filterData(v.cmdBuff) +func (v *tableView) filterCmd(evt *tcell.EventKey) *tcell.EventKey { + v.cmdBuff.setActive(false) + v.refresh() + return nil } -func (v *tableView) filterData(filter fmt.Stringer) { - filtered := resource.TableData{ - Header: v.data.Header, - Rows: resource.RowEvents{}, - Namespace: v.data.Namespace, +func (v *tableView) eraseCmd(evt *tcell.EventKey) *tcell.EventKey { + if v.cmdBuff.isActive() { + v.cmdBuff.del() + } + 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()) - if err != nil { - v.app.flash(flashErr, "Invalid search expression") - v.cmdBuff.clear() - return + v.app.flash(flashInfo, "Entering filtering mode...") + log.Info("Entering filtering mode...") + v.cmdBuff.reset() + v.cmdBuff.setActive(true) + 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. func (v *tableView) SetColorer(f colorerFn) { - v.colorer = f -} - -// AddActions sets up keyboard action listener. -func (v *tableView) addActions(kk keyActions) { - for k, a := range kk { - v.actions[k] = a - } + v.colorerFn = f } // SetActions sets up keyboard action listener. func (v *tableView) setActions(aa keyActions) { - v.actions = aa + for k, a := range aa { + v.actions[k] = a + } } // Hints options @@ -160,43 +136,53 @@ func (v *tableView) hints() hints { return nil } -func (v *tableView) resetTitle() { - var title string - - 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) +func (v *tableView) refresh() { + v.update(v.data) } // Update table content func (v *tableView) update(data resource.TableData) { - v.refresh.Lock() + v.refreshMX.Lock() { v.data = data - if !v.cmdBuff.empty() { - v.filter() + if !v.cmdBuff.isActive() && !v.cmdBuff.empty() { + v.doUpdate(v.filtered()) } else { 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) { - v.table.Clear() + v.Clear() v.currentNS = data.Namespace var row int @@ -209,7 +195,7 @@ func (v *tableView) doUpdate(data resource.TableData) { } c.SetTextColor(tcell.ColorWhite) } - v.table.SetCell(row, col, c) + v.SetCell(row, col, c) } row++ @@ -217,11 +203,13 @@ func (v *tableView) doUpdate(data resource.TableData) { for k := range data.Rows { keys = append(keys, k) } - v.sortFn(keys) + if v.sortFn != nil { + v.sortFn(keys) + } for _, k := range keys { fgColor := tcell.ColorGray - if v.colorer != nil { - fgColor = v.colorer(data.Namespace, data.Rows[k]) + if v.colorerFn != nil { + fgColor = v.colorerFn(data.Namespace, data.Rows[k]) } for col, f := range data.Rows[k].Fields { c := tview.NewTableCell(deltas(data.Rows[k].Deltas[col], f)) @@ -232,12 +220,36 @@ func (v *tableView) doUpdate(data resource.TableData) { } c.SetTextColor(fgColor) } - v.table.SetCell(row, col, c) + v.SetCell(row, col, c) } 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... diff --git a/internal/views/xray.go b/internal/views/xray.go deleted file mode 100644 index c2e44bdc..00000000 --- a/internal/views/xray.go +++ /dev/null @@ -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++ - } -} diff --git a/internal/views/yaml.go b/internal/views/yaml.go deleted file mode 100644 index a95b3032..00000000 --- a/internal/views/yaml.go +++ /dev/null @@ -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 -}