Compare commits
10 Commits
c900900a35
...
dc144dd79e
| Author | SHA1 | Date |
|---|---|---|
|
|
dc144dd79e | |
|
|
381ca5ee9e | |
|
|
f27846ce23 | |
|
|
6181ba7acd | |
|
|
f44eaf7f55 | |
|
|
d21f6cd2bc | |
|
|
d8b4b6b62a | |
|
|
cc0fd1accb | |
|
|
12c30b6cc1 | |
|
|
e70cba1eed |
|
|
@ -9,7 +9,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v6.0.0
|
uses: actions/checkout@v6.0.1
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v6.1.0
|
uses: actions/setup-go@v6.1.0
|
||||||
|
|
@ -18,7 +18,7 @@ jobs:
|
||||||
cache-dependency-path: go.sum
|
cache-dependency-path: go.sum
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
uses: golangci/golangci-lint-action@v9.1.0
|
uses: golangci/golangci-lint-action@v9.2.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
version: v2.1.1
|
version: v2.1.1
|
||||||
|
|
@ -16,7 +16,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v6.0.0
|
uses: actions/checkout@v6.0.1
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v6.1.0
|
uses: actions/setup-go@v6.1.0
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# The base image for building the k9s binary
|
# The base image for building the k9s binary
|
||||||
FROM --platform=$BUILDPLATFORM golang:1.25.4-alpine3.21 AS build
|
FROM --platform=$BUILDPLATFORM golang:1.25.5-alpine3.21 AS build
|
||||||
|
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|
@ -16,7 +16,7 @@ RUN apk --no-cache add --update make libx11-dev git gcc libc-dev curl \
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Build the final Docker image
|
# Build the final Docker image
|
||||||
FROM --platform=$BUILDPLATFORM alpine:3.22.2
|
FROM --platform=$BUILDPLATFORM alpine:3.23.0
|
||||||
ARG KUBECTL_VERSION="v1.32.2"
|
ARG KUBECTL_VERSION="v1.32.2"
|
||||||
|
|
||||||
COPY --from=build /k9s/execs/k9s /bin/k9s
|
COPY --from=build /k9s/execs/k9s /bin/k9s
|
||||||
|
|
|
||||||
|
|
@ -69,9 +69,7 @@ func init() {
|
||||||
// Execute root command.
|
// Execute root command.
|
||||||
func Execute() {
|
func Execute() {
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
if !errors.As(err, &flagError{}) {
|
os.Exit(1)
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
12
go.mod
12
go.mod
|
|
@ -15,11 +15,11 @@ require (
|
||||||
github.com/fsnotify/fsnotify v1.9.0
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
github.com/fvbommel/sortorder v1.1.0
|
github.com/fvbommel/sortorder v1.1.0
|
||||||
github.com/go-errors/errors v1.5.1
|
github.com/go-errors/errors v1.5.1
|
||||||
github.com/itchyny/gojq v0.12.17
|
github.com/itchyny/gojq v0.12.18
|
||||||
github.com/lmittmann/tint v1.0.7
|
github.com/lmittmann/tint v1.0.7
|
||||||
github.com/mattn/go-colorable v0.1.14
|
github.com/mattn/go-colorable v0.1.14
|
||||||
github.com/mattn/go-runewidth v0.0.19
|
github.com/mattn/go-runewidth v0.0.19
|
||||||
github.com/olekukonko/tablewriter v1.1.1
|
github.com/olekukonko/tablewriter v1.1.2
|
||||||
github.com/petergtz/pegomock v2.9.0+incompatible
|
github.com/petergtz/pegomock v2.9.0+incompatible
|
||||||
github.com/rakyll/hey v0.1.4
|
github.com/rakyll/hey v0.1.4
|
||||||
github.com/sahilm/fuzzy v0.1.1
|
github.com/sahilm/fuzzy v0.1.1
|
||||||
|
|
@ -129,9 +129,9 @@ require (
|
||||||
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
github.com/clipperhouse/displaywidth v0.3.1 // indirect
|
github.com/clipperhouse/displaywidth v0.6.0 // indirect
|
||||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.1 // indirect
|
github.com/cloudflare/circl v1.6.1 // indirect
|
||||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
|
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
|
||||||
github.com/containerd/cgroups/v3 v3.0.3 // indirect
|
github.com/containerd/cgroups/v3 v3.0.3 // indirect
|
||||||
|
|
@ -224,7 +224,7 @@ require (
|
||||||
github.com/huandu/xstrings v1.5.0 // indirect
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
github.com/iancoleman/strcase v0.3.0 // indirect
|
github.com/iancoleman/strcase v0.3.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/itchyny/timefmt-go v0.1.6 // indirect
|
github.com/itchyny/timefmt-go v0.1.7 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/jinzhu/copier v0.4.0 // indirect
|
github.com/jinzhu/copier v0.4.0 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
|
@ -274,7 +274,7 @@ require (
|
||||||
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
|
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
|
||||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||||
github.com/olekukonko/errors v1.1.0 // indirect
|
github.com/olekukonko/errors v1.1.0 // indirect
|
||||||
github.com/olekukonko/ll v0.1.2 // indirect
|
github.com/olekukonko/ll v0.1.3 // indirect
|
||||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||||
github.com/onsi/gomega v1.35.1 // indirect
|
github.com/onsi/gomega v1.35.1 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
|
|
|
||||||
24
go.sum
24
go.sum
|
|
@ -298,12 +298,12 @@ github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38
|
||||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/clipperhouse/displaywidth v0.3.1 h1:k07iN9gD32177o1y4O1jQMzbLdCrsGJh+blirVYybsk=
|
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
|
||||||
github.com/clipperhouse/displaywidth v0.3.1/go.mod h1:tgLJKKyaDOCadywag3agw4snxS5kYEuYR6Y9+qWDDYM=
|
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||||
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
|
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||||
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
|
|
@ -732,10 +732,10 @@ github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/itchyny/gojq v0.12.17 h1:8av8eGduDb5+rvEdaOO+zQUjA04MS0m3Ps8HiD+fceg=
|
github.com/itchyny/gojq v0.12.18 h1:gFGHyt/MLbG9n6dqnvlliiya2TaMMh6FFaR2b1H6Drc=
|
||||||
github.com/itchyny/gojq v0.12.17/go.mod h1:WBrEMkgAfAGO1LUcGOckBl5O726KPp+OlkKug0I/FEY=
|
github.com/itchyny/gojq v0.12.18/go.mod h1:4hPoZ/3lN9fDL1D+aK7DY1f39XZpY9+1Xpjz8atrEkg=
|
||||||
github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q=
|
github.com/itchyny/timefmt-go v0.1.7 h1:xyftit9Tbw+Dc/huSSPJaEmX1TVL8lw5vxjJLK4GMMA=
|
||||||
github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg=
|
github.com/itchyny/timefmt-go v0.1.7/go.mod h1:5E46Q+zj7vbTgWY8o5YkMeYb4I6GeWLFnetPy5oBrAI=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||||
|
|
@ -922,10 +922,10 @@ github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj
|
||||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||||
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
|
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
|
||||||
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||||
github.com/olekukonko/ll v0.1.2 h1:lkg/k/9mlsy0SxO5aC+WEpbdT5K83ddnNhAepz7TQc0=
|
github.com/olekukonko/ll v0.1.3 h1:sV2jrhQGq5B3W0nENUISCR6azIPf7UBUpVq0x/y70Fg=
|
||||||
github.com/olekukonko/ll v0.1.2/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
|
github.com/olekukonko/ll v0.1.3/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
|
||||||
github.com/olekukonko/tablewriter v1.1.1 h1:b3reP6GCfrHwmKkYwNRFh2rxidGHcT6cgxj/sHiDDx0=
|
github.com/olekukonko/tablewriter v1.1.2 h1:L2kI1Y5tZBct/O/TyZK1zIE9GlBj/TVs+AY5tZDCDSc=
|
||||||
github.com/olekukonko/tablewriter v1.1.1/go.mod h1:De/bIcTF+gpBDB3Alv3fEsZA+9unTsSzAg/ZGADCtn4=
|
github.com/olekukonko/tablewriter v1.1.2/go.mod h1:z7SYPugVqGVavWoA2sGsFIoOVNmEHxUAAMrhXONtfkg=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,7 @@ func (a *App) Conn() client.Connection {
|
||||||
func (a *App) bindKeys() {
|
func (a *App) bindKeys() {
|
||||||
a.actions = NewKeyActionsFromMap(KeyMap{
|
a.actions = NewKeyActionsFromMap(KeyMap{
|
||||||
KeyColon: NewKeyAction("Cmd", a.activateCmd, false),
|
KeyColon: NewKeyAction("Cmd", a.activateCmd, false),
|
||||||
|
KeyComma: NewKeyAction("Cmd", a.activateCmd, false),
|
||||||
tcell.KeyCtrlR: NewKeyAction("Redraw", a.redrawCmd, false),
|
tcell.KeyCtrlR: NewKeyAction("Redraw", a.redrawCmd, false),
|
||||||
tcell.KeyCtrlP: NewKeyAction("Persist", a.saveCmd, false),
|
tcell.KeyCtrlP: NewKeyAction("Persist", a.saveCmd, false),
|
||||||
tcell.KeyCtrlU: NewSharedKeyAction("Clear Filter", a.clearCmd, false),
|
tcell.KeyCtrlU: NewSharedKeyAction("Clear Filter", a.clearCmd, false),
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ const (
|
||||||
KeyHelp = 63
|
KeyHelp = 63
|
||||||
KeySlash = 47
|
KeySlash = 47
|
||||||
KeyColon = 58
|
KeyColon = 58
|
||||||
|
KeyComma = 44
|
||||||
KeySpace = 32
|
KeySpace = 32
|
||||||
KeyDash = 45
|
KeyDash = 45
|
||||||
KeyLeftBracket = 91
|
KeyLeftBracket = 91
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ package ui
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
"github.com/derailed/k9s/internal/model"
|
"github.com/derailed/k9s/internal/model"
|
||||||
|
|
@ -152,7 +153,13 @@ func (p *Prompt) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
p.model.Delete()
|
p.model.Delete()
|
||||||
|
|
||||||
case tcell.KeyRune:
|
case tcell.KeyRune:
|
||||||
p.model.Add(evt.Rune())
|
r := evt.Rune()
|
||||||
|
// Filter out control characters and non-printable runes that may come from
|
||||||
|
// terminal escape sequences (e.g., cursor position reports like [7;15R)
|
||||||
|
// Only accept printable characters for user input
|
||||||
|
if isValidInputRune(r) {
|
||||||
|
p.model.Add(r)
|
||||||
|
}
|
||||||
|
|
||||||
case tcell.KeyEscape:
|
case tcell.KeyEscape:
|
||||||
p.model.ClearText(true)
|
p.model.ClearText(true)
|
||||||
|
|
@ -293,6 +300,18 @@ func (p *Prompt) prefixesFor(k model.BufferKind) (ic, prefix rune) {
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
|
// isValidInputRune checks if a rune is valid for user input.
|
||||||
|
// It filters out control characters and non-printable characters that may
|
||||||
|
// come from terminal escape sequences (e.g., cursor position reports).
|
||||||
|
func isValidInputRune(r rune) bool {
|
||||||
|
// Reject control characters (0x00-0x1F, 0x7F) except for common whitespace
|
||||||
|
if unicode.IsControl(r) && r != '\t' && r != '\n' && r != '\r' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Only accept printable characters
|
||||||
|
return unicode.IsPrint(r) || unicode.IsSpace(r)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Prompt) colorFor(k model.BufferKind) tcell.Color {
|
func (p *Prompt) colorFor(k model.BufferKind) tcell.Color {
|
||||||
//nolint:exhaustive
|
//nolint:exhaustive
|
||||||
switch k {
|
switch k {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// Copyright Authors of K9s
|
||||||
|
|
||||||
|
package ui_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/config"
|
||||||
|
"github.com/derailed/k9s/internal/model"
|
||||||
|
"github.com/derailed/k9s/internal/ui"
|
||||||
|
"github.com/derailed/tcell/v2"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestPrompt_FiltersControlCharacters tests that control characters from
|
||||||
|
// terminal escape sequences are filtered out and not added to the buffer.
|
||||||
|
func TestPrompt_FiltersControlCharacters(t *testing.T) {
|
||||||
|
m := model.NewFishBuff(':', model.CommandBuffer)
|
||||||
|
p := ui.NewPrompt(nil, true, config.NewStyles())
|
||||||
|
p.SetModel(m)
|
||||||
|
m.AddListener(p)
|
||||||
|
m.SetActive(true)
|
||||||
|
|
||||||
|
// Test control characters that should be filtered
|
||||||
|
controlChars := []rune{
|
||||||
|
0x00, // NULL
|
||||||
|
0x01, // SOH
|
||||||
|
0x1B, // ESC (escape character)
|
||||||
|
0x7F, // DEL
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range controlChars {
|
||||||
|
t.Run(fmt.Sprintf("control_char_0x%02X", c), func(t *testing.T) {
|
||||||
|
evt := tcell.NewEventKey(tcell.KeyRune, c, tcell.ModNone)
|
||||||
|
p.SendKey(evt)
|
||||||
|
// Control characters should not be added to buffer
|
||||||
|
assert.Empty(t, m.GetText(), "Control character 0x%02X should be filtered", c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPrompt_AcceptsPrintableCharacters tests that valid printable
|
||||||
|
// characters are accepted and added to the buffer.
|
||||||
|
func TestPrompt_AcceptsPrintableCharacters(t *testing.T) {
|
||||||
|
m := model.NewFishBuff(':', model.CommandBuffer)
|
||||||
|
p := ui.NewPrompt(nil, true, config.NewStyles())
|
||||||
|
p.SetModel(m)
|
||||||
|
m.AddListener(p)
|
||||||
|
m.SetActive(true)
|
||||||
|
|
||||||
|
// Test valid printable characters
|
||||||
|
validChars := []rune{
|
||||||
|
'a', 'Z', '0', '9',
|
||||||
|
'!', '@', '#', '$',
|
||||||
|
' ', // space
|
||||||
|
'[', ']', ';', 'R', // characters from escape sequences (should be accepted if typed)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range validChars {
|
||||||
|
t.Run(fmt.Sprintf("valid_char_%c", c), func(t *testing.T) {
|
||||||
|
evt := tcell.NewEventKey(tcell.KeyRune, c, tcell.ModNone)
|
||||||
|
p.SendKey(evt)
|
||||||
|
// Valid characters should be added
|
||||||
|
assert.Contains(t, m.GetText(), string(c), "Valid character %c should be accepted", c)
|
||||||
|
// Clear for next test
|
||||||
|
m.ClearText(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test tab separately (it's a control char but should be accepted)
|
||||||
|
t.Run("valid_char_tab", func(t *testing.T) {
|
||||||
|
evt := tcell.NewEventKey(tcell.KeyRune, '\t', tcell.ModNone)
|
||||||
|
p.SendKey(evt)
|
||||||
|
// Tab should be accepted (it's a special case in the validation)
|
||||||
|
// Note: Tab might be converted to spaces or handled differently by the buffer
|
||||||
|
text := m.GetText()
|
||||||
|
// Tab is accepted by validation, but may be handled specially by the buffer
|
||||||
|
// Just verify the buffer isn't empty (meaning something was processed)
|
||||||
|
assert.NotNil(t, text, "Tab character should be processed")
|
||||||
|
m.ClearText(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPrompt_FiltersEscapeSequencePattern tests that escape sequence
|
||||||
|
// patterns are not automatically added when they appear as individual runes.
|
||||||
|
// Note: This test verifies the validation works, but escape sequences
|
||||||
|
// should ideally be handled by tcell before reaching KeyRune.
|
||||||
|
func TestPrompt_FiltersEscapeSequencePattern(t *testing.T) {
|
||||||
|
m := model.NewFishBuff(':', model.CommandBuffer)
|
||||||
|
p := ui.NewPrompt(nil, true, config.NewStyles())
|
||||||
|
p.SetModel(m)
|
||||||
|
m.AddListener(p)
|
||||||
|
m.SetActive(true)
|
||||||
|
|
||||||
|
// Simulate the problematic escape sequence pattern [7;15R
|
||||||
|
// Each character individually is printable, but we want to ensure
|
||||||
|
// they don't appear unexpectedly
|
||||||
|
escapeSequence := "[7;15R"
|
||||||
|
|
||||||
|
// Send each character
|
||||||
|
for _, r := range escapeSequence {
|
||||||
|
evt := tcell.NewEventKey(tcell.KeyRune, r, tcell.ModNone)
|
||||||
|
p.SendKey(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The characters themselves are printable, so they will be added
|
||||||
|
// This test documents the current behavior - the fix prevents
|
||||||
|
// control characters, but printable escape sequence chars would
|
||||||
|
// still be added if tcell doesn't filter them first
|
||||||
|
text := m.GetText()
|
||||||
|
|
||||||
|
// If all characters are printable, they will be in the buffer
|
||||||
|
// This is expected behavior - the fix prevents control chars,
|
||||||
|
// but can't prevent legitimate printable characters
|
||||||
|
assert.NotEmpty(t, text, "Printable escape sequence chars may still appear")
|
||||||
|
|
||||||
|
// However, we can verify no control characters made it through
|
||||||
|
for _, r := range text {
|
||||||
|
assert.False(t, isControlChar(r), "No control characters should be in buffer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if a rune is a control character
|
||||||
|
func isControlChar(r rune) bool {
|
||||||
|
return r >= 0x00 && r <= 0x1F || r == 0x7F
|
||||||
|
}
|
||||||
|
|
@ -113,6 +113,11 @@ func gotoCmd(r Runner, cmd, path string, clearStack bool) ui.ActionHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
func pluginActions(r Runner, aa *ui.KeyActions) error {
|
func pluginActions(r Runner, aa *ui.KeyActions) error {
|
||||||
|
// Skip plugin loading if no valid connection
|
||||||
|
if r.App().Conn() == nil || !r.App().Conn().ConnectionOK() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
aa.Range(func(k tcell.Key, a ui.KeyAction) {
|
aa.Range(func(k tcell.Key, a ui.KeyAction) {
|
||||||
if a.Opts.Plugin {
|
if a.Opts.Plugin {
|
||||||
aa.Delete(k)
|
aa.Delete(k)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ package view
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"maps"
|
"maps"
|
||||||
|
|
@ -106,11 +105,11 @@ func (a *App) Init(version string, _ int) error {
|
||||||
a.App.Init()
|
a.App.Init()
|
||||||
a.SetInputCapture(a.keyboard)
|
a.SetInputCapture(a.keyboard)
|
||||||
a.bindKeys()
|
a.bindKeys()
|
||||||
if a.Conn() == nil {
|
|
||||||
return errors.New("no client connection detected")
|
|
||||||
}
|
|
||||||
ns := a.Config.ActiveNamespace()
|
|
||||||
|
|
||||||
|
// Allow initialization even without a valid connection
|
||||||
|
// We'll fall back to context view in defaultCmd
|
||||||
|
if a.Conn() != nil {
|
||||||
|
ns := a.Config.ActiveNamespace()
|
||||||
a.factory = watch.NewFactory(a.Conn())
|
a.factory = watch.NewFactory(a.Conn())
|
||||||
a.initFactory(ns)
|
a.initFactory(ns)
|
||||||
|
|
||||||
|
|
@ -121,6 +120,7 @@ func (a *App) Init(version string, _ int) error {
|
||||||
a.clusterModel.Refresh()
|
a.clusterModel.Refresh()
|
||||||
a.clusterInfo().Init()
|
a.clusterInfo().Init()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
a.command = NewCommand(a)
|
a.command = NewCommand(a)
|
||||||
if err := a.command.Init(a.Config.ContextAliasesPath()); err != nil {
|
if err := a.command.Init(a.Config.ContextAliasesPath()); err != nil {
|
||||||
|
|
@ -223,6 +223,10 @@ func (a *App) suggestCommand() model.SuggestionFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) contextNames() ([]string, error) {
|
func (a *App) contextNames() ([]string, error) {
|
||||||
|
// Return empty list if no factory
|
||||||
|
if a.factory == nil {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
contexts, err := a.factory.Client().Config().Contexts()
|
contexts, err := a.factory.Client().Config().Contexts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -303,7 +307,7 @@ func (a *App) buildHeader() tview.Primitive {
|
||||||
}
|
}
|
||||||
|
|
||||||
clWidth := clusterInfoWidth
|
clWidth := clusterInfoWidth
|
||||||
if a.Conn().ConnectionOK() {
|
if a.Conn() != nil && a.Conn().ConnectionOK() {
|
||||||
n, err := a.Conn().Config().CurrentClusterName()
|
n, err := a.Conn().Config().CurrentClusterName()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
size := len(n) + clusterInfoPad
|
size := len(n) + clusterInfoPad
|
||||||
|
|
@ -351,6 +355,11 @@ func (a *App) Resume() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) clusterUpdater(ctx context.Context) {
|
func (a *App) clusterUpdater(ctx context.Context) {
|
||||||
|
if a.Conn() == nil || !a.Conn().ConnectionOK() || a.factory == nil || a.clusterModel == nil {
|
||||||
|
slog.Debug("Skipping cluster updater - no valid connection")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := a.refreshCluster(ctx); err != nil {
|
if err := a.refreshCluster(ctx); err != nil {
|
||||||
slog.Error("Cluster updater failed!", slogs.Error, err)
|
slog.Error("Cluster updater failed!", slogs.Error, err)
|
||||||
return
|
return
|
||||||
|
|
@ -379,6 +388,10 @@ func (a *App) clusterUpdater(ctx context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) refreshCluster(context.Context) error {
|
func (a *App) refreshCluster(context.Context) error {
|
||||||
|
if a.Conn() == nil || a.factory == nil || a.clusterModel == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
c := a.Content.Top()
|
c := a.Content.Top()
|
||||||
if ok := a.Conn().CheckConnectivity(); ok {
|
if ok := a.Conn().CheckConnectivity(); ok {
|
||||||
if atomic.LoadInt32(&a.conRetry) > 0 {
|
if atomic.LoadInt32(&a.conRetry) > 0 {
|
||||||
|
|
@ -474,7 +487,18 @@ func (a *App) switchContext(ci *cmd.Interpreter, force bool) error {
|
||||||
if err := a.Config.Save(true); err != nil {
|
if err := a.Config.Save(true); err != nil {
|
||||||
slog.Error("Fail to save config to disk", slogs.Subsys, "config", slogs.Error, err)
|
slog.Error("Fail to save config to disk", slogs.Subsys, "config", slogs.Error, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a.factory == nil && a.Conn() != nil {
|
||||||
|
a.factory = watch.NewFactory(a.Conn())
|
||||||
|
a.clusterModel = model.NewClusterInfo(a.factory, a.version, a.Config.K9s)
|
||||||
|
a.clusterModel.AddListener(a.clusterInfo())
|
||||||
|
a.clusterModel.AddListener(a.statusIndicator())
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.factory != nil {
|
||||||
a.initFactory(ns)
|
a.initFactory(ns)
|
||||||
|
}
|
||||||
|
|
||||||
if err := a.command.Reset(a.Config.ContextAliasesPath(), true); err != nil {
|
if err := a.command.Reset(a.Config.ContextAliasesPath(), true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -487,8 +511,11 @@ func (a *App) switchContext(ci *cmd.Interpreter, force bool) error {
|
||||||
a.Flash().Infof("Switching context to %q::%q", contextName, ns)
|
a.Flash().Infof("Switching context to %q::%q", contextName, ns)
|
||||||
a.ReloadStyles()
|
a.ReloadStyles()
|
||||||
a.gotoResource(a.Config.ActiveView(), "", true, true)
|
a.gotoResource(a.Config.ActiveView(), "", true, true)
|
||||||
|
|
||||||
|
if a.clusterModel != nil {
|
||||||
a.clusterModel.Reset(a.factory)
|
a.clusterModel.Reset(a.factory)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,16 +47,21 @@ func NewCommand(app *App) *Command {
|
||||||
|
|
||||||
// AliasesFor gather all known aliases for a given resource.
|
// AliasesFor gather all known aliases for a given resource.
|
||||||
func (c *Command) AliasesFor(gvr *client.GVR) sets.Set[string] {
|
func (c *Command) AliasesFor(gvr *client.GVR) sets.Set[string] {
|
||||||
|
if c.alias == nil {
|
||||||
|
return sets.New[string]()
|
||||||
|
}
|
||||||
return c.alias.AliasesFor(gvr)
|
return c.alias.AliasesFor(gvr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes the command.
|
// Init initializes the command.
|
||||||
func (c *Command) Init(path string) error {
|
func (c *Command) Init(path string) error {
|
||||||
|
if c.app.factory != nil {
|
||||||
c.alias = dao.NewAlias(c.app.factory)
|
c.alias = dao.NewAlias(c.app.factory)
|
||||||
if _, err := c.alias.Ensure(path); err != nil {
|
if _, err := c.alias.Ensure(path); err != nil {
|
||||||
slog.Error("Ensure aliases failed", slogs.Error, err)
|
slog.Error("Ensure aliases failed", slogs.Error, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
customViewers = loadCustomViewers()
|
customViewers = loadCustomViewers()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -67,6 +72,10 @@ func (c *Command) Reset(path string, nuke bool) error {
|
||||||
c.mx.Lock()
|
c.mx.Lock()
|
||||||
defer c.mx.Unlock()
|
defer c.mx.Unlock()
|
||||||
|
|
||||||
|
if c.alias == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if nuke {
|
if nuke {
|
||||||
c.alias.Clear()
|
c.alias.Clear()
|
||||||
}
|
}
|
||||||
|
|
@ -139,6 +148,9 @@ func (c *Command) xrayCmd(p *cmd.Interpreter, pushCmd bool) error {
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("invalid command. use `xray xxx`")
|
return errors.New("invalid command. use `xray xxx`")
|
||||||
}
|
}
|
||||||
|
if c.alias == nil {
|
||||||
|
return fmt.Errorf("no connection available")
|
||||||
|
}
|
||||||
gvr, ok := c.alias.Resolve(cmd.NewInterpreter(arg))
|
gvr, ok := c.alias.Resolve(cmd.NewInterpreter(arg))
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("invalid resource name: %q", arg)
|
return fmt.Errorf("invalid resource name: %q", arg)
|
||||||
|
|
@ -301,6 +313,9 @@ func (c *Command) specialCmd(p *cmd.Interpreter, pushCmd bool) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Command) viewMetaFor(p *cmd.Interpreter) (*client.GVR, *MetaViewer, *cmd.Interpreter, error) {
|
func (c *Command) viewMetaFor(p *cmd.Interpreter) (*client.GVR, *MetaViewer, *cmd.Interpreter, error) {
|
||||||
|
if c.alias == nil {
|
||||||
|
return client.NoGVR, nil, nil, fmt.Errorf("no connection available")
|
||||||
|
}
|
||||||
gvr, ok := c.alias.Resolve(p)
|
gvr, ok := c.alias.Resolve(p)
|
||||||
if !ok {
|
if !ok {
|
||||||
return client.NoGVR, nil, nil, fmt.Errorf("`%s` command not found", p.Cmd())
|
return client.NoGVR, nil, nil, fmt.Errorf("`%s` command not found", p.Cmd())
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue