dataraces + skins

mine
derailed 2019-04-29 11:41:37 -06:00
parent 3e36512e19
commit 445b475c21
35 changed files with 1133 additions and 260 deletions

1
.gitignore vendored
View File

@ -7,7 +7,6 @@ k9s
/k8s /k8s
dist dist
notes notes
styles
vendor vendor
go.mod1 go.mod1
popeye1.go popeye1.go

View File

@ -67,7 +67,8 @@ snapcraft:
By leveraging a terminal UI, you can easily traverse Kubernetes resources By leveraging a terminal UI, you can easily traverse Kubernetes resources
and view the state of you clusters in a single powerful session. and view the state of you clusters in a single powerful session.
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
publish: false # publish: false
publish: true
replacements: replacements:
amd64: 64-bit amd64: 64-bit
386: 32-bit 386: 32-bit
@ -76,16 +77,15 @@ snapcraft:
bit: Arm bit: Arm
bitv6: Arm6 bitv6: Arm6
bitv7: Arm7 bitv7: Arm7
grade: devel # grade: devel
confinement: devmode # confinement: devmode
grade: stable
confinement: strict
apps: apps:
k9s: k9s:
plugs: ["home", "network"] # plugs: ["home", "network"]
# plugs: ["home", "network", "personal-files"] plugs: ["home", "network", "personal-files"]
# plugs: plugs:
# personal-files: personal-files:
# read: read:
# - $HOME/.k9s - $HOME/.kube
# - $HOME/.kube
# write:
# - $HOME/.k9s

221
README.md
View File

@ -249,6 +249,227 @@ roleRef:
--- ---
## Skins
You can style K9s based on your own sense of style and look. This is very much an experimental feature at this time, more will be added/modified if this feature has legs so thread accordingly!
Skins are YAML files, that enable a user to change K9s presentation layer. K9s skins are loaded from `$HOME/.k9s/skin.yml`. If a skin file is detected then the skin would be loaded if not the current stock skin remains in effect.
Below is a sample skin file, more skins would be available in the skins directory, just simply copy any of these in your user's home dir as `skin.yml`.
```yaml
# InTheNavy Skin...
k9s:
# General K9s styles
fgColor: dodgerblue
bgColor: white
logoColor: blue
# ClusterInfoView styles.
info:
fgColor: lightskyblue
sectionColor: steelblue
# Borders styles.
border:
fgColor: dodgerblue
focusColor: aliceblue
# MenuView attributes and styles.
menu:
fgColor: darkblue
keyColor: cornflowerblue
# Used for favorite namespaces
numKeyColor: cadetblue
# CrumbView attributes for history navigation.
crumb:
fgColor: white
bgColor: steelblue
# Active view settings
activeColor: skyblue
# TableView attributes.
table:
fgColor: blue
bgColor: darkblue
cursorColor: aqua
# Header row styles.
header:
fgColor: white
bgColor: darkblue
sorterColor: orange
# Resource status and update styles
status:
newColor: blue
modifyColor: powderblue
addColor: lightskyblue
errorColor: indianred
highlightcolor: royalblue
killColor: slategray
completedColor: gray
# Border title styles.
title:
fgColor: aqua
bgColor: white
highlightColor: skyblue
counterColor: slateblue
filterColor: slategray
# YAML info styles.
yaml:
keyColor: steelblue
colonColor: blue
valueColor: royalblue
```
Available color names are defined below:
| Color Names |
|----------------------|
| black |
| maroon |
| green |
| olive |
| navy |
| purple |
| teal |
| silver |
| gray |
| red |
| lime |
| yellow |
| blue |
| fuchsia |
| aqua |
| white |
| aliceblue |
| antiquewhite |
| aquamarine |
| azure |
| beige |
| bisque |
| blanchedalmond |
| blueviolet |
| brown |
| burlywood |
| cadetblue |
| chartreuse |
| chocolate |
| coral |
| cornflowerblue |
| cornsilk |
| crimson |
| darkblue |
| darkcyan |
| darkgoldenrod |
| darkgray |
| darkgreen |
| darkkhaki |
| darkmagenta |
| darkolivegreen |
| darkorange |
| darkorchid |
| darkred |
| darksalmon |
| darkseagreen |
| darkslateblue |
| darkslategray |
| darkturquoise |
| darkviolet |
| deeppink |
| deepskyblue |
| dimgray |
| dodgerblue |
| firebrick |
| floralwhite |
| forestgreen |
| gainsboro |
| ghostwhite |
| gold |
| goldenrod |
| greenyellow |
| honeydew |
| hotpink |
| indianred |
| indigo |
| ivory |
| khaki |
| lavender |
| lavenderblush |
| lawngreen |
| lemonchiffon |
| lightblue |
| lightcoral |
| lightcyan |
| lightgoldenrodyellow |
| lightgray |
| lightgreen |
| lightpink |
| lightsalmon |
| lightseagreen |
| lightskyblue |
| lightslategray |
| lightsteelblue |
| lightyellow |
| limegreen |
| linen |
| mediumaquamarine |
| mediumblue |
| mediumorchid |
| mediumpurple |
| mediumseagreen |
| mediumslateblue |
| mediumspringgreen |
| mediumturquoise |
| mediumvioletred |
| midnightblue |
| mintcream |
| mistyrose |
| moccasin |
| navajowhite |
| oldlace |
| olivedrab |
| orange |
| orangered |
| orchid |
| palegoldenrod |
| palegreen |
| paleturquoise |
| palevioletred |
| papayawhip |
| peachpuff |
| peru |
| pink |
| plum |
| powderblue |
| rebeccapurple |
| rosybrown |
| royalblue |
| saddlebrown |
| salmon |
| sandybrown |
| seagreen |
| seashell |
| sienna |
| skyblue |
| slateblue |
| slategray |
| snow |
| springgreen |
| steelblue |
| tan |
| thistle |
| tomato |
| turquoise |
| violet |
| wheat |
| whitesmoke |
| yellowgreen |
| grey |
| dimgrey |
| darkgrey |
| darkslategrey |
| lightgrey |
| lightslategrey |
| slategrey |
---
## Disclaimer ## Disclaimer
This is still work in progress! If there is enough interest in the Kubernetes This is still work in progress! If there is enough interest in the Kubernetes

View File

@ -0,0 +1,34 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# Release v0.6.0
## Notes
Thank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes!
If you've filed an issue please help me verify and close.
Thank you so much for your support and awesome suggestions to make K9s better!!
Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)
---
## Change Logs
### Skins
You can now skin K9s based on your own sense of style. Skinning, is currently limited to color variations and is still very much experimental. More details on how to achieve this is provided in the README and skins directory in this repo. This could be a prime opportunity for someone to contribute to this project and help us come up with some cooler LNF and share with all K9s folks. Any schemes contributed will be added and featured on this repo.
---
## Resolved Bugs
+ [Issue #169](https://github.com/derailed/k9s/issues/169)
+ [Issue #171](https://github.com/derailed/k9s/issues/171)
+ [Issue #172](https://github.com/derailed/k9s/issues/172)
+ [Issue #175](https://github.com/derailed/k9s/issues/175)
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

5
go.mod
View File

@ -12,10 +12,12 @@ replace (
require ( require (
github.com/Azure/go-autorest/autorest v0.1.0 // indirect github.com/Azure/go-autorest/autorest v0.1.0 // indirect
github.com/derailed/tview v0.1.4 github.com/derailed/tview v0.1.6
github.com/evanphx/json-patch v4.1.0+incompatible // indirect github.com/evanphx/json-patch v4.1.0+incompatible // indirect
github.com/fatih/camelcase v1.0.0 // indirect github.com/fatih/camelcase v1.0.0 // indirect
github.com/fsnotify/fsnotify v1.4.7
github.com/gdamore/tcell v1.1.1 github.com/gdamore/tcell v1.1.1
github.com/go-fsnotify/fsnotify v0.0.0-20180321022601-755488143dae // indirect
github.com/gogo/protobuf v1.2.1 // indirect github.com/gogo/protobuf v1.2.1 // indirect
github.com/google/btree v1.0.0 // indirect github.com/google/btree v1.0.0 // indirect
github.com/google/gofuzz v1.0.0 // indirect github.com/google/gofuzz v1.0.0 // indirect
@ -30,6 +32,7 @@ require (
github.com/onsi/gomega v1.5.0 // indirect github.com/onsi/gomega v1.5.0 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/petergtz/pegomock v0.0.0-20181206220228-b113d17a7e81 github.com/petergtz/pegomock v0.0.0-20181206220228-b113d17a7e81
github.com/pkg/profile v1.3.0
github.com/rs/zerolog v1.14.3 github.com/rs/zerolog v1.14.3
github.com/spf13/cobra v0.0.3 github.com/spf13/cobra v0.0.3
github.com/spf13/pflag v1.0.3 // indirect github.com/spf13/pflag v1.0.3 // indirect

10
go.sum
View File

@ -47,6 +47,10 @@ 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.4 h1:6ZtMtb5+2bbGNH7SldHGcVB8GnSTXKIQwKxWRNb6DxY= github.com/derailed/tview v0.1.4 h1:6ZtMtb5+2bbGNH7SldHGcVB8GnSTXKIQwKxWRNb6DxY=
github.com/derailed/tview v0.1.4/go.mod h1:oLBQyhVeXqeUYWDZk7/5NJVbbq/JFXm3W7oEoEtpmSc= github.com/derailed/tview v0.1.4/go.mod h1:oLBQyhVeXqeUYWDZk7/5NJVbbq/JFXm3W7oEoEtpmSc=
github.com/derailed/tview v0.1.5 h1:Gj6K73V9d+zsex208KX8YsAw68+nWVNr7oM9qfTWbXQ=
github.com/derailed/tview v0.1.5/go.mod h1:g+ZyIsV5osK+lQ6LajiGQeLW10BQLJ6aMvy8Ldt2oa0=
github.com/derailed/tview v0.1.6 h1:mmp6Yg78IgbdHapV9wFoVYrbtZbMXilCdAIHadm2uqc=
github.com/derailed/tview v0.1.6/go.mod h1:g+ZyIsV5osK+lQ6LajiGQeLW10BQLJ6aMvy8Ldt2oa0=
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/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
@ -71,6 +75,8 @@ github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4/go.mod h1:4dBDuWmgqj2H
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-fsnotify/fsnotify v0.0.0-20180321022601-755488143dae h1:PeVNzgTRtWGm6fVic5i21t+n5ptPGCZuMcSPVMyTWjs=
github.com/go-fsnotify/fsnotify v0.0.0-20180321022601-755488143dae/go.mod h1:BbhqyaehKPCLD83cqfRYdm177Ylm1cdGHu3txjbQSQI=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
@ -193,6 +199,8 @@ github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.3.0 h1:OQIvuDgm00gWVWGTf4m4mCt6W1/0YqU7Ntg0mySWgaI=
github.com/pkg/profile v1.3.0/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -211,6 +219,8 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
github.com/rivo/tview v0.0.0-20190213202703-b373355e9db4/go.mod h1:J4W+hErFfITUbyFAEXizpmkuxX7ZN56dopxHB4XQhMw= github.com/rivo/tview v0.0.0-20190213202703-b373355e9db4/go.mod h1:J4W+hErFfITUbyFAEXizpmkuxX7ZN56dopxHB4XQhMw=
github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340 h1:nOZbL5f2xmBAHWYrrHbHV1xatzZirN++oOQ3g83Ypgs=
github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340/go.mod h1:SOLvOL4ybwgLJ6TYoX/rtaJ8EGOulH4XU7E9/TLrTCE=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.14.3 h1:4EGfSkR2hJDB0s3oFfrlPqjU1e4WLncergLil3nEKW0= github.com/rs/zerolog v1.14.3 h1:4EGfSkR2hJDB0s3oFfrlPqjU1e4WLncergLil3nEKW0=

View File

@ -50,7 +50,9 @@ func (n *Namespace) Validate(c Connection, ks KubeSettings) {
func (n *Namespace) SetActive(ns string, ks KubeSettings) error { func (n *Namespace) SetActive(ns string, ks KubeSettings) error {
log.Debug().Msgf("Setting active ns %s", ns) log.Debug().Msgf("Setting active ns %s", ns)
n.Active = ns n.Active = ns
n.addFavNS(ns) if ns != "" {
n.addFavNS(ns)
}
return nil return nil
} }

302
internal/config/style.go Normal file
View File

@ -0,0 +1,302 @@
package config
import (
"io/ioutil"
"path/filepath"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
"gopkg.in/yaml.v2"
)
var (
// K9sStylesFile represents K9s config file location.
K9sStylesFile = filepath.Join(K9sHome, "skin.yml")
)
type (
// Styles tracks K9s styling options.
Styles struct {
Style *Style `yaml:"k9s"`
}
// Style tracks K9s styles.
Style struct {
FgColor string `yaml:"fgColor"`
BgColor string `yaml:"bgColor"`
LogoColor string `yaml:"logoColor"`
Info *Info `yaml:"info"`
Border *Border `yaml:"border"`
Menu *Menu `yaml:"menu"`
Crumb *Crumb `yaml:"crumb"`
Table *Table `yaml:"table"`
Status *Status `yaml:"status"`
Title *Title `yaml:"title"`
Yaml *Yaml `yaml:"yaml"`
}
// Status tracks resource status styles.
Status struct {
NewColor string `yaml:"newColor"`
ModifyColor string `yaml:"modifyColor"`
AddColor string `yaml:"addColor"`
ErrorColor string `yaml:"errorColor"`
HighlightColor string `yaml:"highlightColor"`
KillColor string `yaml:"killColor"`
CompletedColor string `yaml:"completedColor"`
}
// Yaml tracks yaml styles.
Yaml struct {
KeyColor string `yaml:"keyColor"`
ValueColor string `yaml:"valueColor"`
ColonColor string `yaml:"colonColor"`
}
// Title tracks title styles.
Title struct {
FgColor string `yaml:"fgColor"`
BgColor string `yaml:"bgColor"`
HighlightColor string `yaml:"highlightColor"`
CounterColor string `yaml:"counterColor"`
FilterColor string `yaml:"filterColor"`
}
// Info tracks info styles.
Info struct {
SectionColor string `yaml:"sectionColor"`
FgColor string `yaml:"fgColor"`
}
// Border tracks border styles.
Border struct {
FgColor string `yaml:"fgColor"`
FocusColor string `yaml:"focusColor"`
}
// Crumb tracks crumbs styles.
Crumb struct {
FgColor string `yaml:"fgColor"`
BgColor string `yaml:"bgColor"`
ActiveColor string `yaml:"activeColor"`
}
// Table tracks table styles.
Table struct {
FgColor string `yaml:"fgColor"`
BgColor string `yaml:"bgColor"`
CursorColor string `yaml:"cursorColor"`
Header *TableHeader `yaml:"header"`
}
// TableHeader tracks table header styles.
TableHeader struct {
FgColor string `yaml:"fgColor"`
BgColor string `yaml:"bgColor"`
SorterColor string `yaml:"sorterColor"`
}
// Menu tracks menu styles.
Menu struct {
FgColor string `yaml:"fgColor"`
KeyColor string `yaml:"keyColor"`
NumKeyColor string `yaml:"numKeyColor"`
}
)
func newStyle() *Style {
return &Style{
FgColor: "cadetblue",
BgColor: "black",
LogoColor: "orange",
Info: newInfo(),
Border: newBorder(),
Menu: newMenu(),
Crumb: newCrumb(),
Table: newTable(),
Status: newStatus(),
Title: newTitle(),
Yaml: newYaml(),
}
}
func newStatus() *Status {
return &Status{
NewColor: "lightskyblue",
ModifyColor: "greenyellow",
AddColor: "white",
ErrorColor: "orangered",
HighlightColor: "aqua",
KillColor: "mediumpurple",
CompletedColor: "gray",
}
}
// NewYaml returns a new yaml style.
func newYaml() *Yaml {
return &Yaml{
KeyColor: "steelblue",
ColonColor: "white",
ValueColor: "papayawhip",
}
}
// NewTitle returns a new title style.
func newTitle() *Title {
return &Title{
FgColor: "aqua",
BgColor: "black",
HighlightColor: "fuchsia",
CounterColor: "papayawhip",
FilterColor: "steelblue",
}
}
// NewInfo returns a new info style.
func newInfo() *Info {
return &Info{
SectionColor: "white",
FgColor: "orange",
}
}
// NewTable returns a new table style.
func newTable() *Table {
return &Table{
FgColor: "aqua",
BgColor: "black",
CursorColor: "aqua",
Header: newTableHeader(),
}
}
// NewTableHeader returns a new table header style.
func newTableHeader() *TableHeader {
return &TableHeader{
FgColor: "white",
BgColor: "black",
SorterColor: "orange",
}
}
// NewCrumb returns a new crumbs style.
func newCrumb() *Crumb {
return &Crumb{
FgColor: "black",
BgColor: "aqua",
ActiveColor: "orange",
}
}
// NewBorder returns a new border style.
func newBorder() *Border {
return &Border{
FgColor: "dodgerblue",
FocusColor: "lightskyblue",
}
}
// NewMenu returns a new menu style.
func newMenu() *Menu {
return &Menu{
FgColor: "white",
KeyColor: "dodgerblue",
NumKeyColor: "fuchsia",
}
}
// NewStyles creates a new default config.
func NewStyles() (*Styles, error) {
s := &Styles{Style: newStyle()}
err := s.load(K9sStylesFile)
return s, err
}
// Ensure default styles are applied in not in stylesheet.
func (s *Styles) ensure() {
if s.Style == nil {
s.Style = newStyle()
}
if s.Style.Info == nil {
s.Style.Info = newInfo()
}
if s.Style.Border == nil {
s.Style.Border = newBorder()
}
if s.Style.Table == nil {
s.Style.Table = newTable()
}
if s.Style.Menu == nil {
s.Style.Menu = newMenu()
}
if s.Style.Crumb == nil {
s.Style.Crumb = newCrumb()
}
if s.Style.Status == nil {
s.Style.Status = newStatus()
}
if s.Style.Title == nil {
s.Style.Title = newTitle()
}
if s.Style.Yaml == nil {
s.Style.Yaml = newYaml()
}
}
// FgColor returns the foreground color.
func (s *Styles) FgColor() tcell.Color {
return AsColor(s.Style.FgColor)
}
// BgColor returns the background color.
func (s *Styles) BgColor() tcell.Color {
return AsColor(s.Style.BgColor)
}
// Load K9s configuration from file
func (s *Styles) load(path string) error {
f, err := ioutil.ReadFile(path)
if err != nil {
return err
}
var cfg Styles
if err := yaml.Unmarshal(f, &cfg); err != nil {
return err
}
if cfg.Style != nil {
s.Style = cfg.Style
}
s.ensure()
return nil
}
// Update apply terminal colors based on styles.
func (s *Styles) Update() {
tview.Styles.PrimitiveBackgroundColor = AsColor(s.Style.BgColor)
tview.Styles.ContrastBackgroundColor = AsColor(s.Style.BgColor)
tview.Styles.PrimaryTextColor = AsColor(s.Style.FgColor)
tview.Styles.BorderColor = AsColor(s.Style.Border.FgColor)
tview.Styles.FocusColor = AsColor(s.Style.Border.FocusColor)
}
// AsColor checks color index, if match return color otherwise pink it is.
func AsColor(c string) tcell.Color {
if color, ok := tcell.ColorNames[c]; ok {
return color
}
return tcell.ColorPink
}

View File

@ -3,8 +3,8 @@ package resource
import ( import (
"bufio" "bufio"
"context" "context"
"fmt"
"strconv" "strconv"
"sync"
"time" "time"
"github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/k8s"
@ -22,6 +22,7 @@ type (
instance v1.Container instance v1.Container
MetricsServer MetricsServer MetricsServer MetricsServer
metrics k8s.PodMetrics metrics k8s.PodMetrics
mx sync.RWMutex
} }
) )
@ -82,30 +83,49 @@ func (r *Container) Logs(c chan<- string, ns, n, co string, lines int64, prev bo
go func() { go func() {
select { select {
case <-time.After(defaultTimeout): case <-time.After(defaultTimeout):
if blocked { var closes bool
r.mx.RLock()
{
closes = blocked
}
r.mx.RUnlock()
if closes {
log.Debug().Msg(">>Closing Channel<<")
close(c) close(c)
cancel() cancel()
} }
} }
}() }()
// This call will block if nothing is in the stream!! // This call will block if nothing is in the stream!!
stream, err := req.Stream() stream, err := req.Stream()
blocked = false
if err != nil { if err != nil {
log.Error().Msgf("Tail logs failed `%s/%s:%s -- %v", ns, n, co, err) log.Error().Msgf("Tail logs failed `%s/%s:%s -- %v", ns, n, co, err)
return cancel, fmt.Errorf("%v", err) return cancel, err
} }
r.mx.Lock()
{
blocked = false
}
r.mx.Unlock()
go func() { go func() {
defer func() { defer func() {
log.Debug().Msg("!!!Closing Stream!!!")
close(c)
stream.Close() stream.Close()
cancel() cancel()
close(c)
}() }()
scanner := bufio.NewScanner(stream) scanner := bufio.NewScanner(stream)
for scanner.Scan() { for scanner.Scan() {
c <- scanner.Text() c <- scanner.Text()
select {
case <-ctx.Done():
return
default:
}
} }
}() }()

View File

@ -5,6 +5,7 @@ import (
"context" "context"
"fmt" "fmt"
"strconv" "strconv"
"sync"
"time" "time"
"github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/k8s"
@ -40,6 +41,7 @@ type (
instance *v1.Pod instance *v1.Pod
MetricsServer MetricsServer MetricsServer MetricsServer
metrics k8s.PodMetrics metrics k8s.PodMetrics
mx sync.RWMutex
} }
) )
@ -55,7 +57,11 @@ func NewPodList(c Connection, mx MetricsServer, ns string) List {
// NewPod instantiates a new Pod. // NewPod instantiates a new Pod.
func NewPod(c Connection, mx MetricsServer) *Pod { func NewPod(c Connection, mx MetricsServer) *Pod {
p := &Pod{&Base{Connection: c, Resource: k8s.NewPod(c)}, nil, mx, k8s.PodMetrics{}} p := &Pod{
Base: &Base{Connection: c, Resource: k8s.NewPod(c)},
MetricsServer: mx,
metrics: k8s.PodMetrics{},
}
p.Factory = p p.Factory = p
return p return p
@ -123,30 +129,49 @@ func (r *Pod) Logs(c chan<- string, ns, n, co string, lines int64, prev bool) (c
go func() { go func() {
select { select {
case <-time.After(defaultTimeout): case <-time.After(defaultTimeout):
if blocked { var closes bool
r.mx.RLock()
{
closes = blocked
}
r.mx.RUnlock()
if closes {
log.Debug().Msg(">>Closing Channel<<")
close(c) close(c)
cancel() cancel()
} }
} }
}() }()
// This call will block if nothing is in the stream!! // This call will block if nothing is in the stream!!
stream, err := req.Stream() stream, err := req.Stream()
blocked = false
if err != nil { if err != nil {
log.Error().Msgf("Tail logs failed `%s/%s:%s -- %v", ns, n, co, err) log.Error().Msgf("Tail logs failed `%s/%s:%s -- %v", ns, n, co, err)
return cancel, fmt.Errorf("%v", err) return cancel, err
} }
r.mx.Lock()
{
blocked = false
}
r.mx.Unlock()
go func() { go func() {
defer func() { defer func() {
log.Debug().Msg("!!!Closing Stream!!!")
close(c)
stream.Close() stream.Close()
cancel() cancel()
close(c)
}() }()
scanner := bufio.NewScanner(stream) scanner := bufio.NewScanner(stream)
for scanner.Scan() { for scanner.Scan() {
c <- scanner.Text() c <- scanner.Text()
select {
case <-ctx.Done():
return
default:
}
} }
}() }()

View File

@ -3,18 +3,22 @@ package views
import ( import (
"context" "context"
"fmt" "fmt"
"sync" "runtime"
"time" "time"
"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/tview" "github.com/derailed/tview"
"github.com/fsnotify/fsnotify"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
) )
const splashTime = 1 const (
splashTime = 1
devMode = "dev"
)
type ( type (
focusHandler func(tview.Primitive) focusHandler func(tview.Primitive)
@ -45,6 +49,7 @@ type (
*tview.Application *tview.Application
config *config.Config config *config.Config
styles *config.Styles
version string version string
flags *genericclioptions.ConfigFlags flags *genericclioptions.ConfigFlags
pages *tview.Pages pages *tview.Pages
@ -61,7 +66,6 @@ type (
cmdBuff *cmdBuff cmdBuff *cmdBuff
cmdView *cmdView cmdView *cmdView
actions keyActions actions keyActions
mx sync.Mutex
} }
) )
@ -69,15 +73,16 @@ type (
func NewApp(cfg *config.Config) *appView { func NewApp(cfg *config.Config) *appView {
v := appView{Application: tview.NewApplication(), config: cfg} v := appView{Application: tview.NewApplication(), config: cfg}
{ {
v.refreshStyles()
v.pages = tview.NewPages() v.pages = tview.NewPages()
v.actions = make(keyActions) v.actions = make(keyActions)
v.menuView = newMenuView() v.menuView = newMenuView(&v)
v.content = tview.NewPages() v.content = tview.NewPages()
v.cmdBuff = newCmdBuff(':') v.cmdBuff = newCmdBuff(':')
v.cmdView = newCmdView('🐶') v.cmdView = newCmdView(&v, '🐶')
v.command = newCommand(&v) v.command = newCommand(&v)
v.flashView = newFlashView(v.Application, "Initializing...") v.flashView = newFlashView(&v, "Initializing...")
v.crumbsView = newCrumbsView(v.Application) v.crumbsView = newCrumbsView(&v)
v.clusterInfoView = newClusterInfoView(&v, k8s.NewMetricsServer(cfg.GetConnection())) v.clusterInfoView = newClusterInfoView(&v, k8s.NewMetricsServer(cfg.GetConnection()))
v.focusChanged = v.changedFocus v.focusChanged = v.changedFocus
v.SetInputCapture(v.keyboard) v.SetInputCapture(v.keyboard)
@ -108,21 +113,21 @@ func (a *appView) Init(v string, rate int, flags *genericclioptions.ConfigFlags)
header.SetDirection(tview.FlexColumn) header.SetDirection(tview.FlexColumn)
header.AddItem(a.clusterInfoView, 35, 1, false) header.AddItem(a.clusterInfoView, 35, 1, false)
header.AddItem(a.menuView, 0, 1, false) header.AddItem(a.menuView, 0, 1, false)
header.AddItem(logoView(), 26, 1, false) header.AddItem(a.logoView(), 26, 1, false)
} }
main := tview.NewFlex() main := tview.NewFlex()
{ {
main.SetDirection(tview.FlexRow) main.SetDirection(tview.FlexRow)
main.AddItem(header, 7, 1, false) main.AddItem(header, 7, 1, false)
main.AddItem(a.cmdView, 1, 1, false) main.AddItem(a.cmdView, 3, 1, false)
main.AddItem(a.content, 0, 10, true) main.AddItem(a.content, 0, 10, true)
main.AddItem(a.crumbsView, 2, 1, false) main.AddItem(a.crumbsView, 2, 1, false)
main.AddItem(a.flashView, 1, 1, false) main.AddItem(a.flashView, 1, 1, false)
} }
a.pages.AddPage("main", main, true, false) a.pages.AddPage("main", main, true, false)
a.pages.AddPage("splash", newSplash(a.version), true, true) a.pages.AddPage("splash", newSplash(a), true, true)
a.SetRoot(a.pages, true) a.SetRoot(a.pages, true)
} }
@ -130,12 +135,52 @@ func (a *appView) conn() k8s.Connection {
return a.config.GetConnection() return a.config.GetConnection()
} }
func (a *appView) stylesUpdater() (*fsnotify.Watcher, error) {
w, err := fsnotify.NewWatcher()
if err != nil {
log.Error().Err(err).Msg("File notifier failed")
return w, err
}
go func() {
for {
select {
case evt := <-w.Events:
log.Debug().Msgf("Evt %#v", evt)
a.QueueUpdateDraw(func() {
a.refreshStyles()
})
case err := <-w.Errors:
log.Error().Err(err).Msg("Watcher failed")
}
}
}()
if err := w.Add(config.K9sStylesFile); err != nil {
log.Error().Err(err).Msg("Styles file watch failed")
return w, err
}
return w, nil
}
// Run starts the application loop // Run starts the application loop
func (a *appView) Run() { func (a *appView) Run() {
// Only enable updater while in dev mode.
if a.version == devMode {
w, err := a.stylesUpdater()
defer func() {
if err != nil {
w.Close()
}
}()
}
go func() { go func() {
<-time.After(splashTime * time.Second) <-time.After(splashTime * time.Second)
a.showPage("main") a.QueueUpdateDraw(func() {
a.Draw() a.showPage("main")
})
}() }()
a.command.defaultCmd() a.command.defaultCmd()
@ -145,9 +190,6 @@ func (a *appView) Run() {
} }
func (a *appView) keyboard(evt *tcell.EventKey) *tcell.EventKey { func (a *appView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
a.mx.Lock()
defer a.mx.Unlock()
key := evt.Key() key := evt.Key()
if key == tcell.KeyRune { if key == tcell.KeyRune {
if a.cmdBuff.isActive() { if a.cmdBuff.isActive() {
@ -156,10 +198,12 @@ func (a *appView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
} }
key = tcell.Key(evt.Rune()) key = tcell.Key(evt.Rune())
} }
if a, ok := a.actions[key]; ok { if a, ok := a.actions[key]; ok {
log.Debug().Msgf(">> AppView handled key: %s", tcell.KeyNames[key]) log.Debug().Msgf(">> AppView handled key: %s -- %d", tcell.KeyNames[key], runtime.NumGoroutine())
return a.action(evt) return a.action(evt)
} }
return evt return evt
} }
@ -216,8 +260,8 @@ func (a *appView) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
if a.cmdView.inCmdMode() { if a.cmdView.inCmdMode() {
return evt return evt
} }
a.flash(flashInfo, "Entering command mode...") a.flash(flashInfo, "Command mode activated.")
log.Debug().Msg("Entering app command mode...") log.Debug().Msg("Entering command mode...")
a.cmdBuff.setActive(true) a.cmdBuff.setActive(true)
a.cmdBuff.clear() a.cmdBuff.clear()
return nil return nil
@ -290,10 +334,6 @@ func (a *appView) cmdMode() bool {
return a.cmdView.inCmdMode() return a.cmdView.inCmdMode()
} }
func (a *appView) refresh() {
a.clusterInfoView.refresh()
}
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...)
} }
@ -302,14 +342,14 @@ func (a *appView) setHints(h hints) {
a.menuView.populateMenu(h) a.menuView.populateMenu(h)
} }
func logoView() tview.Primitive { func (a *appView) logoView() tview.Primitive {
v := tview.NewTextView() v := tview.NewTextView()
{ {
v.SetWordWrap(false) v.SetWordWrap(false)
v.SetWrap(false) v.SetWrap(false)
v.SetDynamicColors(true) v.SetDynamicColors(true)
for i, s := range LogoSmall { for i, s := range LogoSmall {
fmt.Fprintf(v, "[orange::b]%s", s) fmt.Fprintf(v, "[%s::b]%s", a.styles.Style.LogoColor, s)
if i+1 < len(LogoSmall) { if i+1 < len(LogoSmall) {
fmt.Fprintf(v, "\n") fmt.Fprintf(v, "\n")
} }
@ -348,9 +388,17 @@ func (a *appView) nextFocus() {
return return
} }
func initStyles() { func (a *appView) refreshStyles() {
tview.Styles.PrimitiveBackgroundColor = tcell.ColorBlack var err error
tview.Styles.ContrastBackgroundColor = tcell.ColorBlack if a.styles, err = config.NewStyles(); err != nil {
tview.Styles.FocusColor = tcell.ColorLightSkyBlue log.Error().Err(err).Msg("No skin file found. Loading defaults.")
tview.Styles.BorderColor = tcell.ColorDodgerBlue }
a.styles.Update()
stdColor = config.AsColor(a.styles.Style.Status.NewColor)
addColor = config.AsColor(a.styles.Style.Status.AddColor)
modColor = config.AsColor(a.styles.Style.Status.ModifyColor)
errColor = config.AsColor(a.styles.Style.Status.ErrorColor)
highlightColor = config.AsColor(a.styles.Style.Status.HighlightColor)
completedColor = config.AsColor(a.styles.Style.Status.CompletedColor)
} }

View File

@ -3,6 +3,7 @@ package views
import ( import (
"strings" "strings"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/resource"
"github.com/derailed/tview" "github.com/derailed/tview"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
@ -62,20 +63,25 @@ func (v *clusterInfoView) init() {
v.SetCell(row, 1, v.infoCell("n/a")) v.SetCell(row, 1, v.infoCell("n/a"))
v.SetCell(row+1, 0, v.sectionCell("MEM")) v.SetCell(row+1, 0, v.sectionCell("MEM"))
v.SetCell(row+1, 1, v.infoCell("n/a")) v.SetCell(row+1, 1, v.infoCell("n/a"))
v.refresh() v.refresh()
} }
func (*clusterInfoView) sectionCell(t string) *tview.TableCell { func (v *clusterInfoView) sectionCell(t string) *tview.TableCell {
c := tview.NewTableCell(t + ":") c := tview.NewTableCell(t + ":")
c.SetAlign(tview.AlignLeft) c.SetAlign(tview.AlignLeft)
var s tcell.Style
c.SetStyle(s.Bold(true).Foreground(config.AsColor(v.app.styles.Style.Info.SectionColor)))
c.SetBackgroundColor(v.app.styles.BgColor())
return c return c
} }
func (*clusterInfoView) infoCell(t string) *tview.TableCell { func (v *clusterInfoView) infoCell(t string) *tview.TableCell {
c := tview.NewTableCell(t) c := tview.NewTableCell(t)
c.SetExpansion(2) c.SetExpansion(2)
c.SetTextColor(tcell.ColorOrange) c.SetTextColor(config.AsColor(v.app.styles.Style.Info.FgColor))
c.SetBackgroundColor(v.app.styles.BgColor())
return c return c
} }

View File

@ -3,9 +3,8 @@ package views
import ( import (
"fmt" "fmt"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/tview" "github.com/derailed/tview"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
) )
const defaultPrompt = "%c> %s" const defaultPrompt = "%c> %s"
@ -16,16 +15,20 @@ type cmdView struct {
activated bool activated bool
icon rune icon rune
text string text string
app *appView
} }
func newCmdView(ic rune) *cmdView { func newCmdView(app *appView, ic rune) *cmdView {
v := cmdView{icon: ic, TextView: tview.NewTextView()} v := cmdView{app: app, icon: ic, TextView: tview.NewTextView()}
{ {
v.SetWordWrap(true) v.SetWordWrap(true)
v.SetWrap(true) v.SetWrap(true)
v.SetDynamicColors(true) v.SetDynamicColors(true)
v.SetBorder(true)
v.SetBorderPadding(0, 0, 1, 1) v.SetBorderPadding(0, 0, 1, 1)
v.SetTextColor(tcell.ColorAqua) v.SetBackgroundColor(app.styles.BgColor())
v.SetBorderColor(config.AsColor(app.styles.Style.Border.FocusColor))
v.SetTextColor(app.styles.FgColor())
} }
return &v return &v
} }
@ -62,11 +65,12 @@ func (v *cmdView) changed(s string) {
func (v *cmdView) active(f bool) { func (v *cmdView) active(f bool) {
v.activated = f v.activated = f
if f { if f {
log.Debug().Msg("CmdView was activated...") v.SetBorder(true)
v.SetBackgroundColor(tcell.ColorDodgerBlue) v.SetTextColor(v.app.styles.FgColor())
v.activate() v.activate()
} else { } else {
v.SetBackgroundColor(tcell.ColorDefault) v.SetBorder(false)
v.SetBackgroundColor(v.app.styles.BgColor())
v.Clear() v.Clear()
} }
} }

View File

@ -1,5 +1,9 @@
package views package views
import (
"github.com/rs/zerolog/log"
)
const maxBuff = 10 const maxBuff = 10
type buffWatcher interface { type buffWatcher interface {
@ -37,6 +41,7 @@ func (c *cmdBuff) String() string {
} }
func (c *cmdBuff) add(r rune) { func (c *cmdBuff) add(r rune) {
log.Debug().Msgf("Add %s", string(r))
c.buff = append(c.buff, r) c.buff = append(c.buff, r)
c.fireChanged() c.fireChanged()
} }

View File

@ -8,14 +8,14 @@ import (
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
) )
const ( var (
modColor = tcell.ColorGreenYellow modColor tcell.Color
addColor = tcell.ColorLightSkyBlue addColor tcell.Color
errColor = tcell.ColorOrangeRed errColor tcell.Color
stdColor = tcell.ColorWhite stdColor tcell.Color
highlightColor = tcell.ColorAqua highlightColor tcell.Color
killColor = tcell.ColorMediumPurple killColor tcell.Color
completedColor = tcell.ColorDodgerBlue completedColor tcell.Color
) )
func defaultColorer(ns string, r *resource.RowEvent) tcell.Color { func defaultColorer(ns string, r *resource.RowEvent) tcell.Color {

View File

@ -22,6 +22,7 @@ func newContainerView(t string, app *appView, list resource.List, path string) r
} }
v.AddPage("logs", newLogsView(list.GetName(), &v), true, false) v.AddPage("logs", newLogsView(list.GetName(), &v), true, false)
v.switchPage("co") v.switchPage("co")
v.selChanged(1, 0)
return &v return &v
} }

View File

@ -4,33 +4,36 @@ import (
"fmt" "fmt"
"github.com/derailed/tview" "github.com/derailed/tview"
"github.com/gdamore/tcell"
) )
type crumbsView struct { type crumbsView struct {
*tview.TextView *tview.TextView
app *tview.Application app *appView
} }
func newCrumbsView(app *tview.Application) *crumbsView { func newCrumbsView(app *appView) *crumbsView {
v := crumbsView{app: app, TextView: tview.NewTextView()} v := crumbsView{app: app, TextView: tview.NewTextView()}
{ {
v.SetTextColor(tcell.ColorAqua) v.SetBackgroundColor(app.styles.BgColor())
v.SetTextAlign(tview.AlignLeft) v.SetTextAlign(tview.AlignLeft)
v.SetBorderPadding(0, 0, 1, 1) v.SetBorderPadding(0, 0, 1, 1)
v.SetDynamicColors(true) v.SetDynamicColors(true)
} }
return &v return &v
} }
func (v *crumbsView) update(crumbs []string) { func (v *crumbsView) update(crumbs []string) {
v.Clear() v.Clear()
last, bgColor := len(crumbs)-1, "aqua" last, bgColor := len(crumbs)-1, v.app.styles.Style.Crumb.BgColor
for i, c := range crumbs { for i, c := range crumbs {
if i == last { if i == last {
bgColor = "orange" bgColor = v.app.styles.Style.Crumb.ActiveColor
} }
fmt.Fprintf(v, "[black:%s:b] <%s> [-:-:-] ", bgColor, c) fmt.Fprintf(v, "[%s:%s:b] <%s> [-:%s:-] ",
v.app.styles.Style.Crumb.FgColor,
bgColor, c,
v.app.styles.Style.BgColor)
} }
} }

View File

@ -5,14 +5,14 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"sync"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/tview" "github.com/derailed/tview"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
const detailsTitleFmt = " [aqua::b]%s([fuchsia::b]%s[aqua::-])[aqua::-] " const detailsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] "
// detailsView displays text output. // detailsView displays text output.
type detailsView struct { type detailsView struct {
@ -25,7 +25,6 @@ type detailsView struct {
cmdBuff *cmdBuff cmdBuff *cmdBuff
backFn actionHandler backFn actionHandler
numSelections int numSelections int
mx sync.Mutex
} }
func newDetailsView(app *appView, backFn actionHandler) *detailsView { func newDetailsView(app *appView, backFn actionHandler) *detailsView {
@ -37,6 +36,7 @@ func newDetailsView(app *appView, backFn actionHandler) *detailsView {
v.SetDynamicColors(true) v.SetDynamicColors(true)
v.SetRegions(true) v.SetRegions(true)
v.SetBorder(true) v.SetBorder(true)
v.SetBorderFocusColor(config.AsColor(v.app.styles.Style.Border.FocusColor))
v.SetHighlightColor(tcell.ColorOrange) v.SetHighlightColor(tcell.ColorOrange)
v.SetTitleColor(tcell.ColorAqua) v.SetTitleColor(tcell.ColorAqua)
v.SetInputCapture(v.keyboard) v.SetInputCapture(v.keyboard)
@ -65,9 +65,6 @@ func (v *detailsView) setCategory(n string) {
} }
func (v *detailsView) keyboard(evt *tcell.EventKey) *tcell.EventKey { func (v *detailsView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
v.mx.Lock()
defer v.mx.Unlock()
key := evt.Key() key := evt.Key()
if key == tcell.KeyRune { if key == tcell.KeyRune {
if v.cmdBuff.isActive() { if v.cmdBuff.isActive() {
@ -198,9 +195,16 @@ func (v *detailsView) refreshTitle() {
func (v *detailsView) setTitle(t string) { func (v *detailsView) setTitle(t string) {
v.title = t v.title = t
title := fmt.Sprintf(detailsTitleFmt, v.category, t)
fmat := strings.Replace(detailsTitleFmt, "[fg", "["+v.app.styles.Style.Title.FgColor, -1)
fmat = strings.Replace(fmat, ":bg:", ":"+v.app.styles.Style.Title.BgColor+":", -1)
fmat = strings.Replace(fmat, "[hilite", "["+v.app.styles.Style.Title.CounterColor, 1)
title := fmt.Sprintf(fmat, v.category, t)
if !v.cmdBuff.empty() { if !v.cmdBuff.empty() {
title += fmt.Sprintf(searchFmt, v.cmdBuff.String()) fmat := strings.Replace(searchFmt, "[fg", "["+v.app.styles.Style.Title.FgColor, -1)
fmat = strings.Replace(fmat, ":bg:", ":"+v.app.styles.Style.Title.BgColor+":", -1)
fmat = strings.Replace(fmat, "[filter", "["+v.app.styles.Style.Title.FilterColor, 1)
title += fmt.Sprintf(fmat, v.cmdBuff.String())
} }
v.SetTitle(title) v.SetTitle(title)
} }

View File

@ -30,42 +30,44 @@ type (
*tview.TextView *tview.TextView
cancel context.CancelFunc cancel context.CancelFunc
app *tview.Application app *appView
} }
) )
func newFlashView(app *tview.Application, m string) *flashView { func newFlashView(app *appView, m string) *flashView {
f := flashView{app: app, TextView: tview.NewTextView()} f := flashView{app: app, TextView: tview.NewTextView()}
{ f.SetTextColor(tcell.ColorAqua)
f.SetTextColor(tcell.ColorAqua) f.SetTextAlign(tview.AlignLeft)
f.SetTextAlign(tview.AlignLeft) f.SetBorderPadding(0, 0, 1, 1)
f.SetBorderPadding(0, 0, 1, 1) f.SetText("")
f.SetText("")
}
return &f return &f
} }
func (f *flashView) setMessage(level flashLevel, msg ...string) { func (v *flashView) setMessage(level flashLevel, msg ...string) {
if f.cancel != nil { if v.cancel != nil {
f.cancel() v.cancel()
} }
_, _, width, _ := v.GetRect()
if width <= 15 {
width = 100
}
m := strings.Join(msg, " ")
v.SetTextColor(flashColor(level))
v.SetText(resource.Truncate(flashEmoji(level)+" "+m, width-3))
var ctx context.Context var ctx context.Context
{ {
ctx, f.cancel = context.WithTimeout(context.TODO(), flashDelay*time.Second) ctx, v.cancel = context.WithTimeout(context.TODO(), flashDelay*time.Second)
go func(ctx context.Context) { go func(ctx context.Context) {
_, _, width, _ := f.GetRect()
if width <= 15 {
width = 100
}
m := strings.Join(msg, " ")
f.SetTextColor(flashColor(level))
f.SetText(resource.Truncate(flashEmoji(level)+" "+m, width-3))
f.app.Draw()
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
f.Clear() v.app.QueueUpdateDraw(func() {
f.app.Draw() v.Clear()
// v.app.Draw()
})
return return
} }
} }

View File

@ -3,6 +3,7 @@ package views
import ( import (
"fmt" "fmt"
"io" "io"
"strings"
"github.com/derailed/tview" "github.com/derailed/tview"
) )
@ -28,14 +29,23 @@ func newLogView(title string, parent loggable) *logView {
return &v return &v
} }
func (l *logView) logLine(line string, scroll bool) { func (l *logView) logLine(line string) {
fmt.Fprintln(l.ansiWriter, tview.Escape(line)) fmt.Fprintln(l.ansiWriter, tview.Escape(line))
if scroll {
l.ScrollToEnd()
}
} }
func (l *logView) log(lines fmt.Stringer) { func (l *logView) log(lines fmt.Stringer) {
l.Clear() l.Clear()
fmt.Fprintln(l.ansiWriter, lines.String()) fmt.Fprintln(l.ansiWriter, lines.String())
} }
func (l *logView) flush(index int, buff []string, scroll bool) {
if index > 0 {
l.logLine(strings.Join(buff[:index], "\n"))
if scroll {
l.app.QueueUpdate(func() {
l.ScrollToEnd()
})
}
index = 0
}
}

View File

@ -3,8 +3,8 @@ package views
import ( import (
"context" "context"
"fmt" "fmt"
"runtime"
"strconv" "strconv"
"sync"
"time" "time"
"github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/resource"
@ -17,6 +17,7 @@ const (
maxBuff1 int64 = 200 maxBuff1 int64 = 200
refreshRate = 200 * time.Millisecond refreshRate = 200 * time.Millisecond
maxCleanse = 100 maxCleanse = 100
logBuffSize = 100
) )
type logsView struct { type logsView struct {
@ -29,7 +30,6 @@ type logsView struct {
cancelFunc context.CancelFunc cancelFunc context.CancelFunc
autoScroll bool autoScroll bool
showPrevious bool showPrevious bool
mx sync.Mutex
} }
func newLogsView(pview string, parent loggable) *logsView { func newLogsView(pview string, parent loggable) *logsView {
@ -130,8 +130,9 @@ func (v *logsView) stop() {
if v.cancelFunc == nil { if v.cancelFunc == nil {
return return
} }
log.Debug().Msg("Canceling logs...")
v.cancelFunc() v.cancelFunc()
log.Debug().Msgf("Canceling logs... %d", runtime.NumGoroutine())
v.cancelFunc = nil v.cancelFunc = nil
} }
@ -139,47 +140,57 @@ func (v *logsView) load(i int) {
if i < 0 || i > len(v.containers)-1 { if i < 0 || i > len(v.containers)-1 {
return return
} }
v.SwitchToPage(v.containers[i]) v.SwitchToPage(v.containers[i])
if err := v.doLoad(v.parent.getSelection(), v.containers[i]); err != nil { if err := v.doLoad(v.parent.getSelection(), v.containers[i]); err != nil {
v.parent.appView().flash(flashErr, err.Error()) v.parent.appView().flash(flashErr, err.Error())
l := v.CurrentPage().Item.(*logView) l := v.CurrentPage().Item.(*logView)
l.logLine("😂 Doh! No logs are available at this time. Check again later on...", false) l.logLine("😂 Doh! No logs are available at this time. Check again later on...")
return return
} }
v.parent.appView().SetFocus(v) v.parent.appView().SetFocus(v)
} }
func (v *logsView) doLoad(path, co string) error { func (v *logsView) doLoad(path, co string) error {
v.mx.Lock()
defer v.mx.Unlock()
v.stop() v.stop()
c := make(chan string) maxBuff := int64(v.parent.appView().config.K9s.LogRequestSize)
go func() { l := v.CurrentPage().Item.(*logView)
l := v.CurrentPage().Item.(*logView) l.Clear()
l.Clear() l.setTitle(path + ":" + co)
l.setTitle(path + ":" + co)
c := make(chan string, 10)
go func(l *logView) {
buff, index := make([]string, logBuffSize), 0
for { for {
select { select {
case line, ok := <-c: case line, ok := <-c:
if !ok { if !ok {
if v.autoScroll { l.flush(index, buff, v.autoScroll)
l.ScrollToEnd() index = 0
}
return return
} }
l.logLine(line, v.autoScroll) if index < logBuffSize {
buff[index] = line
index++
continue
}
l.flush(index, buff, v.autoScroll)
index = 0
buff[index] = line
case <-time.After(1 * time.Second):
l.flush(index, buff, v.autoScroll)
index = 0
} }
} }
}() }(l)
ns, po := namespaced(path) ns, po := namespaced(path)
res, ok := v.parent.getList().Resource().(resource.Tailable) res, ok := v.parent.getList().Resource().(resource.Tailable)
if !ok { if !ok {
return fmt.Errorf("Resource %T is not tailable", v.parent.getList().Resource) return fmt.Errorf("Resource %T is not tailable", v.parent.getList().Resource)
} }
maxBuff := int64(v.parent.appView().config.K9s.LogRequestSize)
cancelFn, err := res.Logs(c, ns, po, co, maxBuff, v.showPrevious) cancelFn, err := res.Logs(c, ns, po, co, maxBuff, v.showPrevious)
if err != nil { if err != nil {
cancelFn() cancelFn()
@ -196,9 +207,9 @@ func (v *logsView) doLoad(path, co string) error {
func (v *logsView) toggleScrollCmd(evt *tcell.EventKey) *tcell.EventKey { func (v *logsView) toggleScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
v.autoScroll = !v.autoScroll v.autoScroll = !v.autoScroll
if v.autoScroll { if v.autoScroll {
v.parent.appView().flash(flashInfo, "Autoscroll is on") v.parent.appView().flash(flashInfo, "Autoscroll is on.")
} else { } else {
v.parent.appView().flash(flashInfo, "Autoscroll is off") v.parent.appView().flash(flashInfo, "Autoscroll is off.")
} }
return nil return nil
@ -208,7 +219,7 @@ func (v *logsView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
v.stop() v.stop()
v.parent.switchPage(v.parentView) v.parent.switchPage(v.parentView)
return nil return evt
} }
func (v *logsView) topCmd(evt *tcell.EventKey) *tcell.EventKey { func (v *logsView) topCmd(evt *tcell.EventKey) *tcell.EventKey {

View File

@ -6,7 +6,6 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sync"
"github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/resource"
"github.com/derailed/tview" "github.com/derailed/tview"
@ -16,12 +15,10 @@ import (
func init() { func init() {
initKeys() initKeys()
initStyles()
} }
const ( const (
menuSepFmt = " [dodgerblue::b]%-%ds [white::d]%s " menuIndexFmt = " [key:bg:b]<%d> [fg:bg:d]%s "
menuIndexFmt = " [fuchsia::b]<%d> [white::d]%s "
maxRows = 7 maxRows = 7
) )
@ -77,11 +74,6 @@ func newKeyAction(d string, a actionHandler, display bool) keyAction {
return keyAction{description: d, action: a, visible: display} return keyAction{description: d, action: a, visible: display}
} }
func newMenuView() *menuView {
v := menuView{Table: tview.NewTable()}
return &v
}
func (a keyActions) toHints() hints { func (a keyActions) toHints() hints {
kk := make([]int, 0, len(a)) kk := make([]int, 0, len(a))
for k, v := range a { for k, v := range a {
@ -108,16 +100,19 @@ func (a keyActions) toHints() hints {
type menuView struct { type menuView struct {
*tview.Table *tview.Table
mx sync.Mutex app *appView
}
func newMenuView(app *appView) *menuView {
v := menuView{Table: tview.NewTable(), app: app}
v.SetBackgroundColor(app.styles.BgColor())
return &v
} }
func (v *menuView) populateMenu(hh hints) { func (v *menuView) populateMenu(hh hints) {
v.mx.Lock()
defer v.mx.Unlock()
v.Clear() v.Clear()
sort.Sort(hh) sort.Sort(hh)
t := v.buildMenuTable(hh) t := v.buildMenuTable(hh)
for row := 0; row < len(t); row++ { for row := 0; row < len(t); row++ {
for col := 0; col < len(t[row]); col++ { for col := 0; col < len(t[row]); col++ {
@ -125,6 +120,7 @@ func (v *menuView) populateMenu(hh hints) {
continue continue
} }
c := tview.NewTableCell(t[row][col]) c := tview.NewTableCell(t[row][col])
c.SetBackgroundColor(v.app.styles.BgColor())
v.SetCell(row, col, c) v.SetCell(row, col, c)
} }
} }
@ -143,9 +139,6 @@ func (v *menuView) buildMenuTable(hh hints) [][]string {
maxKeys := make([]int, colCount+1) maxKeys := make([]int, colCount+1)
for _, h := range hh { for _, h := range hh {
isDigit := menuRX.MatchString(h.mnemonic) isDigit := menuRX.MatchString(h.mnemonic)
// if isDigit && firstNS {
// row, col, firstNS = 0, 2, false
// }
if !isDigit && firstCmd { if !isDigit && firstCmd {
row, col, firstCmd = 0, col+1, false row, col, firstCmd = 0, col+1, false
} }
@ -184,11 +177,17 @@ func (*menuView) toMnemonic(s string) string {
func (v *menuView) formatMenu(h hint, size int) string { func (v *menuView) formatMenu(h hint, size int) string {
i, err := strconv.Atoi(h.mnemonic) i, err := strconv.Atoi(h.mnemonic)
if err == nil { if err == nil {
return fmt.Sprintf(menuIndexFmt, i, resource.Truncate(h.description, 14)) fmat := strings.Replace(menuIndexFmt, "[key", "["+v.app.styles.Style.Menu.NumKeyColor, 1)
fmat = strings.Replace(fmat, ":bg:", ":"+v.app.styles.Style.Title.BgColor+":", -1)
fmat = strings.Replace(fmat, "[fg", "["+v.app.styles.Style.Menu.FgColor, 1)
return fmt.Sprintf(fmat, i, resource.Truncate(h.description, 14))
} }
menuFmt := " [dodgerblue::b]%-" + strconv.Itoa(size+2) + "s [white::d]%s " menuFmt := " [key:bg:b]%-" + strconv.Itoa(size+2) + "s [fg:bg:d]%s "
return fmt.Sprintf(menuFmt, v.toMnemonic(h.mnemonic), h.description) fmat := strings.Replace(menuFmt, "[key", "["+v.app.styles.Style.Menu.KeyColor, 1)
fmat = strings.Replace(fmat, "[fg", "["+v.app.styles.Style.Menu.FgColor, 1)
fmat = strings.Replace(fmat, ":bg:", ":"+v.app.styles.Style.Title.BgColor+":", -1)
return fmt.Sprintf(fmat, v.toMnemonic(h.mnemonic), h.description)
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------

View File

@ -63,8 +63,11 @@ func showPods(app *appView, ns, res, selected, labelSel, fieldSel string, b acti
title := fmt.Sprintf("%s:%s Pods", res, selected) title := fmt.Sprintf("%s:%s Pods", res, selected)
pv := newPodView(title, app, list) pv := newPodView(title, app, list)
pv.setColorerFn(podColorer)
pv.setExtraActionsFn(func(aa keyActions) { pv.setExtraActionsFn(func(aa keyActions) {
aa[tcell.KeyEsc] = newKeyAction("Back", b, true) aa[tcell.KeyEsc] = newKeyAction("Back", b, true)
}) })
// Reset active namespace to all.
app.config.SetActiveNamespace("")
app.inject(pv) app.inject(pv)
} }

View File

@ -2,6 +2,7 @@ package views
import ( import (
"fmt" "fmt"
"strings"
"github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/resource"
@ -10,7 +11,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
const fmat = "[aqua::b]%s([fuchsia::b]%s[aqua::-])" const containerFmt = "[fg:bg:b]%s([hilite:bg:b]%s[fg:bg:-])"
type podView struct { type podView struct {
*resourceView *resourceView
@ -61,7 +62,17 @@ func (v *podView) listContainers(app *appView, _, res, sel string) {
mx := k8s.NewMetricsServer(app.conn()) mx := k8s.NewMetricsServer(app.conn())
list := resource.NewContainerList(app.conn(), mx, po) list := resource.NewContainerList(app.conn(), mx, po)
app.inject(newContainerView(fmt.Sprintf(fmat, "Containers", sel), app, list, namespacedName(po.Namespace, po.Name)))
fmat := strings.Replace(containerFmt, "[fg", "["+v.app.styles.Style.Title.FgColor, -1)
fmat = strings.Replace(containerFmt, ":bg:", ":"+v.app.styles.Style.Title.BgColor+":", -1)
fmat = strings.Replace(fmat, "[hilite", "["+v.app.styles.Style.Title.CounterColor, 1)
app.inject(newContainerView(
fmt.Sprintf(fmat, "Containers", sel),
app,
list,
namespacedName(po.Namespace, po.Name),
))
} }
// Protocol... // Protocol...
@ -88,6 +99,7 @@ func (v *podView) logsCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.viewLogs(false) { if v.viewLogs(false) {
return nil return nil
} }
return evt return evt
} }
@ -95,6 +107,7 @@ func (v *podView) prevLogsCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.viewLogs(true) { if v.viewLogs(true) {
return nil return nil
} }
return evt return evt
} }

View File

@ -19,7 +19,7 @@ const (
all = "*" all = "*"
rbacTitle = "RBAC" rbacTitle = "RBAC"
rbacTitleFmt = " [aqua::b]%s([fuchsia::b]%s[aqua::-])" rbacTitleFmt = " [fg:bg:b]%s([hilite:bg:b]%s[fg:bg:-])"
) )
type ( type (
@ -28,6 +28,7 @@ type (
rbacView struct { rbacView struct {
*tableView *tableView
app *appView
current igniter current igniter
cancel context.CancelFunc cancel context.CancelFunc
roleType roleKind roleType roleKind
@ -78,7 +79,7 @@ var (
) )
func newRBACView(app *appView, ns, name string, kind roleKind) *rbacView { func newRBACView(app *appView, ns, name string, kind roleKind) *rbacView {
v := rbacView{} v := rbacView{app: app}
{ {
v.roleName, v.roleType = name, kind v.roleName, v.roleType = name, kind
v.tableView = newTableView(app, v.getTitle()) v.tableView = newTableView(app, v.getTitle())
@ -125,7 +126,11 @@ func (v *rbacView) bindKeys() {
} }
func (v *rbacView) getTitle() string { func (v *rbacView) getTitle() string {
return fmt.Sprintf(rbacTitleFmt, rbacTitle, v.roleName) fmat := strings.Replace(rbacTitleFmt, "[fg", "["+v.app.styles.Style.Title.FgColor, -1)
fmat = strings.Replace(fmat, ":bg:", ":"+v.app.styles.Style.Title.BgColor+":", -1)
fmat = strings.Replace(fmat, "[hilite", "["+v.app.styles.Style.Title.HighlightColor, 1)
return fmt.Sprintf(fmat, rbacTitle, v.roleName)
} }
func (v *rbacView) hints() hints { func (v *rbacView) hints() hints {

View File

@ -6,7 +6,6 @@ import (
"path" "path"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
@ -17,10 +16,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
const ( const noSelection = ""
refreshDelay = 0.1
noSelection = ""
)
type ( type (
details interface { details interface {
@ -40,7 +36,6 @@ type (
selectedRow int selectedRow int
namespaces map[int]string namespaces map[int]string
selectedNS string selectedNS string
update sync.Mutex
list resource.List list resource.List
enterFn enterFn enterFn enterFn
extraActionsFn func(keyActions) extraActionsFn func(keyActions)
@ -90,13 +85,16 @@ func (v *resourceView) init(ctx context.Context, ns string) {
log.Debug().Msgf("%s watcher canceled!", v.title) log.Debug().Msgf("%s watcher canceled!", v.title)
return return
case <-time.After(time.Duration(v.app.config.K9s.RefreshRate) * time.Second): case <-time.After(time.Duration(v.app.config.K9s.RefreshRate) * time.Second):
v.refresh() v.app.QueueUpdate(func() {
v.refresh()
})
} }
} }
}(ctx) }(ctx)
v.refresh() v.refresh()
if tv, ok := v.CurrentPage().Item.(*tableView); ok { if tv, ok := v.CurrentPage().Item.(*tableView); ok {
tv.Select(0, 0) tv.Select(1, 0)
v.selChanged(1, 0)
} }
} }
@ -118,6 +116,7 @@ func (v *resourceView) getSelectedItem() string {
if v.selectedFn != nil { if v.selectedFn != nil {
return v.selectedFn() return v.selectedFn()
} }
return v.selectedItem return v.selectedItem
} }
@ -220,7 +219,7 @@ func (v *resourceView) defaultEnter(app *appView, ns, resource, selection string
details.setCategory("Describe") details.setCategory("Describe")
details.setTitle(sel) details.setTitle(sel)
details.SetTextColor(tcell.ColorAqua) details.SetTextColor(tcell.ColorAqua)
details.SetText(colorizeYAML(yaml)) details.SetText(colorizeYAML(v.app.styles.Style, yaml))
details.ScrollToBeginning() details.ScrollToBeginning()
} }
v.switchPage("details") v.switchPage("details")
@ -239,6 +238,7 @@ func (v *resourceView) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.rowSelected() { if !v.rowSelected() {
return evt return evt
} }
sel := v.getSelectedItem() sel := v.getSelectedItem()
raw, err := v.list.Resource().Marshal(sel) raw, err := v.list.Resource().Marshal(sel)
if err != nil { if err != nil {
@ -251,10 +251,11 @@ func (v *resourceView) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
details.setCategory("View") details.setCategory("View")
details.setTitle(sel) details.setTitle(sel)
details.SetTextColor(tcell.ColorMediumAquamarine) details.SetTextColor(tcell.ColorMediumAquamarine)
details.SetText(colorizeYAML(raw)) details.SetText(colorizeYAML(v.app.styles.Style, raw))
details.ScrollToBeginning() details.ScrollToBeginning()
} }
v.switchPage("details") v.switchPage("details")
return nil return nil
} }
@ -262,6 +263,7 @@ func (v *resourceView) editCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.rowSelected() { if !v.rowSelected() {
return evt return evt
} }
ns, po := namespaced(v.selectedItem) ns, po := namespaced(v.selectedItem)
args := make([]string, 0, 10) args := make([]string, 0, 10)
args = append(args, "edit") args = append(args, "edit")
@ -270,6 +272,7 @@ func (v *resourceView) editCmd(evt *tcell.EventKey) *tcell.EventKey {
args = append(args, "--context", v.app.config.K9s.CurrentContext) args = append(args, "--context", v.app.config.K9s.CurrentContext)
args = append(args, po) args = append(args, po)
runK(true, v.app, args...) runK(true, v.app, args...)
return evt return evt
} }
@ -282,21 +285,17 @@ func (v *resourceView) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
} }
func (v *resourceView) doSwitchNamespace(ns string) { func (v *resourceView) doSwitchNamespace(ns string) {
v.update.Lock() if ns == "" {
{ ns = resource.AllNamespace
if ns == noSelection {
ns = resource.AllNamespace
}
v.selectedNS = ns
v.app.flash(flashInfo, fmt.Sprintf("Viewing `%s namespace...", ns))
v.list.SetNamespace(v.selectedNS)
} }
v.update.Unlock() v.selectedNS = ns
v.app.flash(flashInfo, fmt.Sprintf("Viewing `%s namespace...", ns))
v.list.SetNamespace(v.selectedNS)
v.refresh() v.refresh()
v.selectItem(0, 0)
v.getTV().resetTitle() v.getTV().resetTitle()
v.getTV().Select(0, 0) v.getTV().Select(1, 0)
v.selectItem(1, 0)
v.app.cmdBuff.reset() v.app.cmdBuff.reset()
v.app.config.SetActiveNamespace(v.selectedNS) v.app.config.SetActiveNamespace(v.selectedNS)
v.app.config.Save() v.app.config.Save()
@ -307,32 +306,32 @@ func (v *resourceView) refresh() {
return return
} }
v.update.Lock() if v.list.Namespaced() {
{ v.list.SetNamespace(v.selectedNS)
if v.list.Namespaced() {
v.list.SetNamespace(v.selectedNS)
}
if err := v.list.Reconcile(); err != nil {
log.Error().Err(err).Msg("Reconciliation failed")
v.app.flash(flashErr, err.Error())
}
data := v.list.Data()
if v.decorateFn != nil {
data = v.decorateFn(data)
}
v.getTV().update(data)
v.selectItem(v.selectedRow, 0)
v.refreshActions()
v.app.clusterInfoView.refresh()
v.app.Draw()
} }
v.update.Unlock()
v.refreshActions()
v.app.clusterInfoView.refresh()
if err := v.list.Reconcile(); err != nil {
log.Error().Err(err).Msg("Reconciliation failed")
v.app.flash(flashErr, err.Error())
}
data := v.list.Data()
if v.decorateFn != nil {
data = v.decorateFn(data)
}
v.getTV().update(data)
v.selectItem(v.selectedRow, 0)
v.app.Draw()
} }
func (v *resourceView) getTV() *tableView { func (v *resourceView) getTV() *tableView {
if tv, ok := v.GetPrimitive(v.list.GetName()).(*tableView); ok { if tv, ok := v.GetPrimitive(v.list.GetName()).(*tableView); ok {
return tv return tv
} }
return nil return nil
} }
@ -360,18 +359,11 @@ func (v *resourceView) selectItem(r, c int) {
} }
func (v *resourceView) switchPage(p string) { func (v *resourceView) switchPage(p string) {
v.update.Lock() v.SwitchToPage(p)
{ v.selectedNS = v.list.GetNamespace()
v.SwitchToPage(p) if h, ok := v.GetPrimitive(p).(hinter); ok {
v.selectedNS = v.list.GetNamespace() v.app.setHints(h.hints())
if h, ok := v.GetPrimitive(p).(hinter); ok {
v.app.setHints(h.hints())
v.app.SetFocus(v.CurrentPage().Item)
} else {
log.Error().Msgf("Hinter not implemented on %s", p)
}
} }
v.update.Unlock()
} }
func (v *resourceView) rowSelected() bool { func (v *resourceView) rowSelected() bool {
@ -389,7 +381,7 @@ func (v *resourceView) refreshActions() {
} }
var nn []interface{} var nn []interface{}
if !v.list.HasSelectors() && k8s.CanIAccess(v.app.conn().Config(), log.Logger, "", "list", "namespaces", "namespace.v1") { if k8s.CanIAccess(v.app.conn().Config(), log.Logger, "", "list", "namespaces", "namespace.v1") {
var err error var err error
nn, err = k8s.NewNamespace(v.app.conn()).List(resource.AllNamespaces) nn, err = k8s.NewNamespace(v.app.conn()).List(resource.AllNamespaces)
if err != nil { if err != nil {
@ -413,7 +405,6 @@ func (v *resourceView) refreshActions() {
} }
v.actions[tcell.KeyEnter] = newKeyAction("Enter", v.enterCmd, false) v.actions[tcell.KeyEnter] = newKeyAction("Enter", v.enterCmd, false)
v.actions[tcell.KeyCtrlR] = newKeyAction("Refresh", v.refreshCmd, false) v.actions[tcell.KeyCtrlR] = newKeyAction("Refresh", v.refreshCmd, false)
v.actions[KeyHelp] = newKeyAction("Help", v.app.noopCmd, false) v.actions[KeyHelp] = newKeyAction("Help", v.app.noopCmd, false)
v.actions[KeyP] = newKeyAction("Previous", v.app.prevCmd, false) v.actions[KeyP] = newKeyAction("Previous", v.app.prevCmd, false)

View File

@ -56,7 +56,7 @@ func (v *secretView) decodeCmd(evt *tcell.EventKey) *tcell.EventKey {
details.setCategory("Decoder") details.setCategory("Decoder")
details.setTitle(sel) details.setTitle(sel)
details.SetTextColor(tcell.ColorMediumAquamarine) details.SetTextColor(tcell.ColorMediumAquamarine)
details.SetText(colorizeYAML(string(raw))) details.SetText(colorizeYAML(v.app.styles.Style, string(raw)))
details.ScrollToBeginning() details.ScrollToBeginning()
} }
v.switchPage("details") v.switchPage("details")

View File

@ -36,11 +36,13 @@ var Logo = []string{
// Splash screen definition // Splash screen definition
type splashView struct { type splashView struct {
*tview.Flex *tview.Flex
app *appView
} }
// NewSplash instantiates a new splash screen with product and company info. // NewSplash instantiates a new splash screen with product and company info.
func newSplash(rev string) *splashView { func newSplash(app *appView) *splashView {
v := splashView{tview.NewFlex()} v := splashView{Flex: tview.NewFlex(), app: app}
logo := tview.NewTextView() logo := tview.NewTextView()
{ {
@ -56,7 +58,7 @@ func newSplash(rev string) *splashView {
vers.SetBackgroundColor(tcell.ColorDefault) vers.SetBackgroundColor(tcell.ColorDefault)
vers.SetTextAlign(tview.AlignCenter) vers.SetTextAlign(tview.AlignCenter)
} }
v.layoutRev(vers, rev) v.layoutRev(vers, app.version)
v.SetDirection(tview.FlexRow) v.SetDirection(tview.FlexRow)
v.AddItem(logo, 10, 1, false) v.AddItem(logo, 10, 1, false)
@ -65,10 +67,13 @@ func newSplash(rev string) *splashView {
} }
func (v *splashView) layoutLogo(t *tview.TextView) { func (v *splashView) layoutLogo(t *tview.TextView) {
logo := strings.Join(Logo, "\n[orange::b]") logo := strings.Join(Logo, fmt.Sprintf("\n[%s::b]", v.app.styles.Style.LogoColor))
fmt.Fprintf(t, "%s[orange::b]%s\n", strings.Repeat("\n", 2), logo) fmt.Fprintf(t, "%s[%s::b]%s\n",
strings.Repeat("\n", 2),
v.app.styles.Style.LogoColor,
logo)
} }
func (v *splashView) layoutRev(t *tview.TextView, rev string) { func (v *splashView) layoutRev(t *tview.TextView, rev string) {
fmt.Fprintf(t, "[white::b]Revision [red::b]%s", rev) fmt.Fprintf(t, "[%s::b]Revision [red::b]%s", v.app.styles.Style.FgColor, rev)
} }

View File

@ -14,8 +14,6 @@ import (
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
) )
const subjectTitleFmt = " [aqua::b]%s([fuchsia::b]%s[aqua::-])"
var subjectHeader = resource.Row{"NAME", "KIND", "FIRST LOCATION"} var subjectHeader = resource.Row{"NAME", "KIND", "FIRST LOCATION"}
type ( type (
@ -177,13 +175,11 @@ func (v *subjectView) reconcile() (resource.TableData, error) {
if err != nil { if err != nil {
return table, err return table, err
} }
log.Debug().Msgf("Cluster evts %d", len(evts))
nevts, err := v.namespacedSubjects() nevts, err := v.namespacedSubjects()
if err != nil { if err != nil {
return table, err return table, err
} }
log.Debug().Msgf("NS evts %d", len(nevts))
for k, v := range nevts { for k, v := range nevts {
evts[k] = v evts[k] = v
} }

View File

@ -6,9 +6,9 @@ import (
"runtime" "runtime"
"sort" "sort"
"strings" "strings"
"sync"
"time" "time"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/resource"
"github.com/derailed/tview" "github.com/derailed/tview"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
@ -17,9 +17,9 @@ import (
) )
const ( const (
titleFmt = " [aqua::b]%s[aqua::-][[fuchsia::b]%d[aqua::-]] " titleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%d[fg:bg:-]] "
searchFmt = "<[green::b]/%s[aqua::]> " searchFmt = "<[filter:bg:b]/%s[fg:bg:]> "
nsTitleFmt = " [aqua::b]%s([fuchsia::b]%s[aqua::-])[aqua::-][[aqua::b]%d[aqua::-]][aqua::-] " nsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] "
) )
type ( type (
@ -38,7 +38,6 @@ type (
app *appView app *appView
baseTitle string baseTitle string
currentNS string currentNS string
refreshMX sync.Mutex
actions keyActions actions keyActions
colorerFn colorerFn colorerFn colorerFn
sortFn sortFn sortFn sortFn
@ -46,7 +45,6 @@ type (
data resource.TableData data resource.TableData
cmdBuff *cmdBuff cmdBuff *cmdBuff
sortBuff *cmdBuff sortBuff *cmdBuff
tableMX sync.Mutex
sortCol sortColumn sortCol sortColumn
} }
) )
@ -58,15 +56,20 @@ func newTableView(app *appView, title string) *tableView {
v.actions = make(keyActions) v.actions = make(keyActions)
v.SetFixed(1, 0) v.SetFixed(1, 0)
v.SetBorder(true) v.SetBorder(true)
v.SetFixed(1, 0) v.SetBackgroundColor(config.AsColor(app.styles.Style.Table.BgColor))
v.SetBorderColor(tcell.ColorDodgerBlue) v.SetBorderColor(config.AsColor(app.styles.Style.Table.FgColor))
v.SetBorderFocusColor(config.AsColor(app.styles.Style.Border.FocusColor))
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.addListener(app.cmdView)
v.cmdBuff.reset() v.cmdBuff.reset()
v.SetSelectable(true, false) v.SetSelectable(true, false)
v.SetSelectedStyle(tcell.ColorBlack, tcell.ColorAqua, tcell.AttrBold) v.SetSelectedStyle(
tcell.ColorBlack,
config.AsColor(app.styles.Style.Table.CursorColor),
tcell.AttrBold,
)
v.SetInputCapture(v.keyboard) v.SetInputCapture(v.keyboard)
v.bindKeys() v.bindKeys()
} }
@ -205,7 +208,7 @@ func (v *tableView) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
return evt return evt
} }
v.app.flash(flashInfo, "Filtering...") v.app.flash(flashInfo, "Filter mode activated.")
v.cmdBuff.reset() v.cmdBuff.reset()
v.cmdBuff.setActive(true) v.cmdBuff.setActive(true)
@ -227,13 +230,13 @@ func (v *tableView) setColorer(f colorerFn) {
// 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.tableMX.Lock() // v.mx.Lock()
{ // {
for k, a := range aa { for k, a := range aa {
v.actions[k] = a v.actions[k] = a
}
} }
v.tableMX.Unlock() // }
// v.mx.Unlock()
} }
// Hints options // Hints options
@ -251,16 +254,12 @@ func (v *tableView) refresh() {
// Update table content // Update table content
func (v *tableView) update(data resource.TableData) { func (v *tableView) update(data resource.TableData) {
v.refreshMX.Lock() v.data = data
{ if !v.cmdBuff.empty() {
v.data = data v.doUpdate(v.filtered())
if !v.cmdBuff.empty() { } else {
v.doUpdate(v.filtered()) v.doUpdate(v.data)
} else {
v.doUpdate(data)
}
} }
v.refreshMX.Unlock()
v.resetTitle() v.resetTitle()
} }
@ -300,7 +299,7 @@ func (v *tableView) sortIndicator(index int, name string) string {
if v.sortCol.asc { if v.sortCol.asc {
order = "↑" order = "↑"
} }
return fmt.Sprintf("%s [green::]%s[::]", name, order) return fmt.Sprintf("%s [%s::]%s[::]", name, v.app.styles.Style.Table.Header.SorterColor, order)
} }
func (v *tableView) doUpdate(data resource.TableData) { func (v *tableView) doUpdate(data resource.TableData) {
@ -326,7 +325,11 @@ func (v *tableView) doUpdate(data resource.TableData) {
} }
pads := make(maxyPad, len(data.Header)) pads := make(maxyPad, len(data.Header))
// v.mx.Lock()
// {
computeMaxColumns(pads, v.sortCol.index, data) computeMaxColumns(pads, v.sortCol.index, data)
// }
// v.mx.Unlock()
var row int var row int
for col, h := range data.Header { for col, h := range data.Header {
v.addHeaderCell(col, h, pads) v.addHeaderCell(col, h, pads)
@ -340,7 +343,7 @@ func (v *tableView) doUpdate(data resource.TableData) {
prim, sec := v.sortAllRows(data.Rows, sortFn) prim, sec := v.sortAllRows(data.Rows, sortFn)
for _, pk := range prim { for _, pk := range prim {
for _, sk := range sec[pk] { for _, sk := range sec[pk] {
fgColor := tcell.ColorGray fgColor := config.AsColor(v.app.styles.Style.Table.FgColor)
if v.colorerFn != nil { if v.colorerFn != nil {
fgColor = v.colorerFn(data.Namespace, data.Rows[sk]) fgColor = v.colorerFn(data.Namespace, data.Rows[sk])
} }
@ -381,7 +384,8 @@ func (v *tableView) addHeaderCell(col int, name string, pads maxyPad) {
c := tview.NewTableCell(v.sortIndicator(col, name)) c := tview.NewTableCell(v.sortIndicator(col, name))
{ {
c.SetExpansion(1) c.SetExpansion(1)
c.SetTextColor(tcell.ColorAntiqueWhite) c.SetTextColor(config.AsColor(v.app.styles.Style.Table.Header.FgColor))
c.SetBackgroundColor(config.AsColor(v.app.styles.Style.Table.Header.BgColor))
} }
v.SetCell(0, col, c) v.SetCell(0, col, c)
} }
@ -438,17 +442,28 @@ func (v *tableView) resetTitle() {
} }
switch v.currentNS { switch v.currentNS {
case resource.NotNamespaced, "*": case resource.NotNamespaced, "*":
title = fmt.Sprintf(titleFmt, v.baseTitle, rc) fmat := strings.Replace(titleFmt, "[fg", "["+v.app.styles.Style.Title.FgColor, -1)
fmat = strings.Replace(fmat, ":bg:", ":"+v.app.styles.Style.Title.BgColor+":", -1)
fmat = strings.Replace(fmat, "[count", "["+v.app.styles.Style.Title.CounterColor, 1)
title = fmt.Sprintf(fmat, v.baseTitle, rc)
default: default:
ns := v.currentNS ns := v.currentNS
if v.currentNS == resource.AllNamespaces { if v.currentNS == resource.AllNamespaces {
ns = resource.AllNamespace ns = resource.AllNamespace
} }
title = fmt.Sprintf(nsTitleFmt, v.baseTitle, ns, rc) fmat := strings.Replace(nsTitleFmt, "[fg", "["+v.app.styles.Style.Title.FgColor, -1)
fmat = strings.Replace(fmat, ":bg:", ":"+v.app.styles.Style.Title.BgColor+":", -1)
fmat = strings.Replace(fmat, "[hilite", "["+v.app.styles.Style.Title.HighlightColor, 1)
fmat = strings.Replace(fmat, "[count", "["+v.app.styles.Style.Title.CounterColor, 1)
title = fmt.Sprintf(fmat, v.baseTitle, ns, rc)
} }
if !v.cmdBuff.isActive() && !v.cmdBuff.empty() { if !v.cmdBuff.isActive() && !v.cmdBuff.empty() {
title += fmt.Sprintf(searchFmt, v.cmdBuff) fmat := strings.Replace(searchFmt, "[fg", "["+v.app.styles.Style.Title.FgColor, 1)
fmat = strings.Replace(fmat, ":bg:", ":"+v.app.styles.Style.Title.BgColor+":", -1)
fmat = strings.Replace(fmat, "[filter", "["+v.app.styles.Style.Title.FilterColor, 1)
title += fmt.Sprintf(fmat, v.cmdBuff)
} }
v.SetTitle(title) v.SetTitle(title)
} }

View File

@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
"github.com/derailed/k9s/internal/config"
) )
var ( var (
@ -11,24 +13,39 @@ var (
keyRX = regexp.MustCompile(`\A(\s*)([\w|\-|\.|\s]+):\s*\z`) keyRX = regexp.MustCompile(`\A(\s*)([\w|\-|\.|\s]+):\s*\z`)
) )
func colorizeYAML(raw string) string { const (
yamlFullFmt = "%s[key::b]%s[colon::-]: [val::]%s"
yamlKeyFmt = "%s[key::b]%s[colon::-]:"
yamlValueFmt = "[val::]%s"
)
func colorizeYAML(style *config.Style, raw string) string {
lines := strings.Split(raw, "\n") lines := strings.Split(raw, "\n")
fullFmt := strings.Replace(yamlFullFmt, "[key", "["+style.Yaml.KeyColor, 1)
fullFmt = strings.Replace(fullFmt, "[colon", "["+style.Yaml.ColonColor, 1)
fullFmt = strings.Replace(fullFmt, "[val", "["+style.Yaml.ValueColor, 1)
keyFmt := strings.Replace(yamlKeyFmt, "[key", "["+style.Yaml.KeyColor, 1)
keyFmt = strings.Replace(keyFmt, "[colon", "["+style.Yaml.ColonColor, 1)
valFmt := strings.Replace(yamlValueFmt, "[val", "["+style.Yaml.ValueColor, 1)
buff := make([]string, 0, len(lines)) buff := make([]string, 0, len(lines))
for _, l := range lines { for _, l := range lines {
res := keyValRX.FindStringSubmatch(l) res := keyValRX.FindStringSubmatch(l)
if len(res) == 4 { if len(res) == 4 {
buff = append(buff, fmt.Sprintf("%s[steelblue::b]%s[white::-]: [papayawhip::]%s", res[1], res[2], res[3])) buff = append(buff, fmt.Sprintf(fullFmt, res[1], res[2], res[3]))
continue continue
} }
res = keyRX.FindStringSubmatch(l) res = keyRX.FindStringSubmatch(l)
if len(res) == 3 { if len(res) == 3 {
buff = append(buff, fmt.Sprintf("%s[steelblue::b]%s[white::-]:", res[1], res[2])) buff = append(buff, fmt.Sprintf(keyFmt, res[1], res[2]))
continue continue
} }
buff = append(buff, fmt.Sprintf("[papayawhip::]%s", l)) buff = append(buff, fmt.Sprintf(valFmt, l))
} }
return strings.Join(buff, "\n") return strings.Join(buff, "\n")

View File

@ -3,6 +3,7 @@ package views
import ( import (
"testing" "testing"
"github.com/derailed/k9s/internal/config"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -40,7 +41,8 @@ func TestYaml(t *testing.T) {
}, },
} }
s, _ := config.NewStyles()
for _, u := range uu { for _, u := range uu {
assert.Equal(t, u.e, colorizeYAML(u.s)) assert.Equal(t, u.e, colorizeYAML(s.Style, u.s))
} }
} }

43
skins/black_and_wtf.yml Normal file
View File

@ -0,0 +1,43 @@
k9s:
fgColor: white
bgColor: black
logoColor: white
info:
fgColor: navajowhite
sectionColor: white
border:
fgColor: white
focusColor: white
menu:
fgColor: white
keyColor: white
numKeyColor: navajowhite
crumb:
fgColor: black
bgColor: navajowhite
activeColor: whitesmoke
table:
fgColor: white
bgColor: black
cursorColor: white
header:
fgColor: darkgray
bgColor: black
sorterColor: white
status:
newColor: ghostwhite
modifyColor: navajowhite
addColor: darkslategray
errorColor: whitesmoke
highlightcolor: dimgray
killColor: slategray
completedColor: gray
title:
fgColor: ghostwhite
highlightColor: navajowhite
counterColor: navajowhite
filterColor: slategray
yaml:
keyColor: ghostwhite
colorColor: slategray
valueColor: navajowhite

44
skins/in_the_navy.yml Normal file
View File

@ -0,0 +1,44 @@
k9s:
fgColor: dodgerblue
bgColor: white
logoColor: blue
info:
fgColor: lightskyblue
sectionColor: steelblue
border:
fgColor: dodgerblue
focusColor: aliceblue
menu:
fgColor: darkblue
keyColor: cornflowerblue
numKeyColor: cadetblue
crumb:
fgColor: white
bgColor: steelblue
activeColor: skyblue
table:
fgColor: blue
bgColor: darkblue
cursorColor: aqua
header:
fgColor: white
bgColor: darkblue
sorterColor: orange
status:
newColor: blue
modifyColor: powderblue
addColor: lightskyblue
errorColor: indianred
highlightcolor: royalblue
killColor: slategray
completedColor: gray
title:
fgColor: aqua
bgColor: white
highlightColor: skyblue
counterColor: slateblue
filterColor: slategray
yaml:
keyColor: steelblue
colorColor: blue
valueColor: royalblue

30
skins/stock.yml Normal file
View File

@ -0,0 +1,30 @@
k9s:
fgColor: dodgerblue
bgColor: black
logoColor: orange
info:
fgColor: white
sectionColor: dodgerblue
border:
fgColor: dodgerblue
focusColor: aqua
menu:
fgColor: white
keyColor: dodgerblue
numKeyColor: fuchsia
crumb:
fgColor: black
bgColor: steelblue
activeColor: orange
table:
fgColor: blue
bgColor: black
cursorColor: aqua
header:
fgColor: aqua
bgColor: black
sorterColor: orange
yaml:
keyColor: steelblue
colonColor: white
valueColor: papayawhip