├── .circleci
└── config.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── go.mod
├── go.sum
├── logbook.go
├── main.go
├── pkg
├── k8s
│ ├── client.go
│ ├── logs.go
│ └── pods.go
├── types
│ └── pod_status.go
├── ui
│ ├── keys.go
│ ├── statusbar.go
│ └── ui.go
└── widgets
│ ├── event.go
│ ├── highlight_text.go
│ ├── input.go
│ ├── line.go
│ ├── listview.go
│ ├── pager.go
│ └── tabs.go
├── screenshot.gif
├── scripts
└── deploy
│ ├── go.mod
│ ├── go.sum
│ └── main.go
├── worker.go
└── worker_test.go
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 |
3 | executors:
4 | default:
5 | docker:
6 | - image: circleci/golang:1.12.7
7 |
8 | jobs:
9 | build:
10 | executor:
11 | name: default
12 | steps:
13 | - checkout
14 | - run:
15 | name: Setup
16 | command: |
17 | go get -u golang.org/x/lint/golint
18 | - run: make lint
19 | - run: make test
20 | - run: make build
21 |
22 | deploy:
23 | executor:
24 | name: default
25 | steps:
26 | - checkout
27 | - run:
28 | name: Install deploy tool
29 | command: |
30 | cd scripts/deploy
31 | go install .
32 | - run:
33 | name: Deploy binaries
34 | command: |
35 | make binaries
36 | deploy "$CIRCLE_TAG" logbook_darwin_amd64
37 | deploy "$CIRCLE_TAG" logbook_linux_amd64
38 | deploy "$CIRCLE_TAG" logbook_windows_amd64.exe
39 |
40 | workflows:
41 | version: 2
42 | build:
43 | jobs:
44 | - build
45 | deploy:
46 | jobs:
47 | - build:
48 | filters:
49 | tags:
50 | only: /.*/
51 | branches:
52 | ignore: /.*/
53 | - deploy:
54 | requires:
55 | - build
56 | filters:
57 | tags:
58 | only: /^v.*/
59 | branches:
60 | ignore: /.*/
61 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /logbook
2 | /logbook_darwin_amd64
3 | /logbook_linux_amd64
4 | /logbook_windows_amd64.exe
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Shin'ya Ueoka
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | GO_FILES=$(shell find -name '*.go' -not -name '*_test.go')
2 | TARGET=logbook
3 | TARGET_LINUX_AMD64=logbook_linux_amd64
4 | TARGET_DARWIN_AMD64=logbook_darwin_amd64
5 | TARGET_WINDOWS_AMD64=logbook_windows_amd64.exe
6 |
7 | build: $(TARGET)
8 | $(TARGET): $(GO_FILES)
9 | go build -o $(TARGET) .
10 |
11 | binaries: $(TARGET_LINUX_AMD64) $(TARGET_DARWIN_AMD64) $(TARGET_WINDOWS_AMD64)
12 | $(TARGET_LINUX_AMD64): $(GO_FILES)
13 | GOOS=linux GOARCH=amd64 go build -o $(TARGET_LINUX_AMD64) .
14 | $(TARGET_DARWIN_AMD64): $(GO_FILES)
15 | GOOS=darwin GOARCH=amd64 go build -o $(TARGET_DARWIN_AMD64) .
16 | $(TARGET_WINDOWS_AMD64): $(GO_FILES)
17 | GOOS=windows GOARCH=amd64 go build -o $(TARGET_WINDOWS_AMD64) .
18 |
19 | test:
20 | go test -v -race ./...
21 |
22 | lint:
23 | test -z "$$(gofmt -s -d . | tee /dev/stderr)"
24 | test -z "$$(golint ./...| tee /dev/stderr)"
25 |
26 | clean:
27 | rm -rf $(TARGET) $(TARGET_LINUX_AMD64) $(TARGET_DARWIN_AMD64) $(TARGET_WINDOWS_AMD64)
28 |
29 | .PHONY: build binaries test lint clean
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # :ledger: logbook
2 |
3 | Logbook is a real-time log viewer for Kubernetes.
4 |
5 | 
6 |
7 | ## Install
8 |
9 | Download a latest version of the binary from releases, or use `go get` as follows:
10 |
11 | ```console
12 | $ go get -u github.com/ueokande/logbook
13 | ```
14 |
15 | ## Usage
16 |
17 | ```console
18 | $ logbook [--kubeconfig KUBECONFIG] [--namespace NAMESPACE]
19 |
20 | Flags:
21 | --kubeconfig Path to kubeconfig file
22 | --namespace Kubernetes namespace
23 | ```
24 |
25 | - Ctrl+n: Select next pod
26 | - Ctrl+p: Scroll previous pod
27 | - j: Scroll down
28 | - k: Scroll up
29 | - h: Scroll left
30 | - l: Scroll right
31 | - f: Enable and disable follow mode
32 | - Ctrl+D: Scroll half-page down
33 | - Ctrl+U: Scroll half-page up
34 | - Ctrl+F: Scroll page down
35 | - Ctrl+B: Scroll page up
36 | - G: Scroll to bottom
37 | - g: Scroll to top
38 | - Tab: Switch containers
39 | - /: Search forward for matching line.
40 | - n: Repeat previous search.
41 | - N: Repeat previous search in reverse direction.
42 | - q: Quit
43 |
44 | ## License
45 |
46 | MIT
47 |
48 | [releases]: https://github.com/ueokande/logbook/releases
49 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/ueokande/logbook
2 |
3 | go 1.12
4 |
5 | require (
6 | github.com/gdamore/tcell v1.1.4
7 | github.com/imdario/mergo v0.3.7 // indirect
8 | github.com/mattn/go-runewidth v0.0.4
9 | github.com/pkg/errors v0.8.1
10 | github.com/spf13/cobra v0.0.5
11 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 // indirect
12 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
13 | k8s.io/api v0.0.0-20190627205229-acea843d18eb
14 | k8s.io/apimachinery v0.0.0-20190629125103-05b5762916b3
15 | k8s.io/client-go v0.0.0-20190612210332-e4cdb82809fc
16 | k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a // indirect
17 | )
18 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | github.com/Azure/go-autorest v11.1.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
4 | github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=
5 | github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
6 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
7 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
8 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
9 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
10 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
14 | github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
15 | github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
16 | github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
17 | github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
18 | github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
19 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
20 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
21 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
22 | github.com/gdamore/tcell v1.1.4 h1:6Bubmk3vZvnL9umQ9qTV2kwNQnjaZ4HLAbxR+xR3ATg=
23 | github.com/gdamore/tcell v1.1.4/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
24 | github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415 h1:WSBJMqJbLxsn+bTCPyPYZfqHdJmc8MK4wrBjMft6BAM=
25 | github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
26 | github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
27 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
28 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
29 | github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
30 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
31 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
32 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
33 | github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
34 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
35 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
36 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
37 | github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k=
38 | github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
39 | github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4=
40 | github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
41 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
42 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
43 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
44 | github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
45 | github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
46 | github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
47 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
48 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
49 | github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
50 | github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
51 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
52 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
53 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
54 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
55 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
56 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
57 | github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
58 | github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
59 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
60 | github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
61 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
62 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
63 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
64 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
65 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
66 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
67 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
68 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
69 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
70 | github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
71 | github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
72 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
73 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
74 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
75 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
76 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
77 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
78 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
79 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
80 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
81 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
82 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
83 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
84 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
85 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
86 | github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
87 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
88 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
89 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
90 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
91 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
92 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
93 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
94 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
95 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
96 | golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
97 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
98 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
99 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
100 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
101 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
102 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
103 | golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
104 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU=
105 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
106 | golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
107 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
108 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
109 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
110 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
111 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
112 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
113 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
114 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
115 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
116 | golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
117 | golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
118 | golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
119 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
120 | golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
121 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
122 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
123 | golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
124 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
125 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
126 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
127 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
128 | google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
129 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
130 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
131 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
132 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
133 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
134 | gopkg.in/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o=
135 | gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
136 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
137 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
138 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
139 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
140 | k8s.io/api v0.0.0-20190612210016-7525909cc6da/go.mod h1:TBhBqb1AWbBQbW3XRusr7n7E4v2+5ZY8r8sAMnyFC5A=
141 | k8s.io/api v0.0.0-20190627205229-acea843d18eb h1:GPqcjfATaKQE9eB8VRN0cpueOB7NQZ7ZuAGy9j2fZoo=
142 | k8s.io/api v0.0.0-20190627205229-acea843d18eb/go.mod h1:dNIey7Yoxc4u51YMhX4E5Cs6xiuGvXIGghzAZ9RzR88=
143 | k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA=
144 | k8s.io/apimachinery v0.0.0-20190627205106-bc5732d141a8/go.mod h1:T1Vra67tZppnhHB3gqfn8/pmTb9EMVZXLvaevIRo6cU=
145 | k8s.io/apimachinery v0.0.0-20190629125103-05b5762916b3 h1:MmzCH8A05OJI3Eq++MvQaFmtaI3QsX57qJ1puoHEekY=
146 | k8s.io/apimachinery v0.0.0-20190629125103-05b5762916b3/go.mod h1:T1Vra67tZppnhHB3gqfn8/pmTb9EMVZXLvaevIRo6cU=
147 | k8s.io/client-go v0.0.0-20190612210332-e4cdb82809fc h1:85rhpDiB8u0/nFty6RKEVwREHhABccRIDfj7OLDPv7c=
148 | k8s.io/client-go v0.0.0-20190612210332-e4cdb82809fc/go.mod h1:IqkN1UjOOV1yBgoXkg6npj4xIWtfITwFaa0tqbzFVAA=
149 | k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
150 | k8s.io/klog v0.3.1 h1:RVgyDHY/kFKtLqh67NvEWIgkMneNoIrdkN0CxDSQc68=
151 | k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
152 | k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
153 | k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0=
154 | k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a h1:2jUDc9gJja832Ftp+QbDV0tVhQHMISFn01els+2ZAcw=
155 | k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
156 | sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
157 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
158 |
--------------------------------------------------------------------------------
/logbook.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/gdamore/tcell/views"
7 | "github.com/ueokande/logbook/pkg/k8s"
8 | "github.com/ueokande/logbook/pkg/types"
9 | "github.com/ueokande/logbook/pkg/ui"
10 | corev1 "k8s.io/api/core/v1"
11 | )
12 |
13 | // AppConfig is a config for Logbook App
14 | type AppConfig struct {
15 | Cluster string
16 | Namespace string
17 | }
18 |
19 | // App is an application of logbook
20 | type App struct {
21 | client *k8s.Client
22 | ui *ui.UI
23 |
24 | namespace string
25 | pods []*corev1.Pod
26 | currentPod *corev1.Pod
27 | podworker *Worker
28 | logworker *Worker
29 |
30 | *views.Application
31 | }
32 |
33 | // NewApp returns new App instance
34 | func NewApp(client *k8s.Client, config *AppConfig) *App {
35 | w := ui.NewUI()
36 | w.SetContext(config.Cluster, config.Namespace)
37 | w.SetStatusMode(ui.ModeNormal)
38 |
39 | app := &App{
40 | client: client,
41 | ui: w,
42 |
43 | namespace: config.Namespace,
44 | logworker: NewWorker(context.TODO()),
45 | podworker: NewWorker(context.TODO()),
46 |
47 | Application: new(views.Application),
48 | }
49 |
50 | w.WatchUIEvents(app)
51 | app.SetRootWidget(w)
52 |
53 | return app
54 | }
55 |
56 | // OnContainerSelected handles events on container selected by UI
57 | func (app *App) OnContainerSelected(name string, index int) {
58 | pod := app.currentPod
59 | app.ui.ClearPager()
60 | app.StartTailLog(pod.Namespace, pod.Name, name)
61 | }
62 |
63 | // OnPodSelected handles events on pod selected by UI
64 | func (app *App) OnPodSelected(name string, index int) {
65 | app.currentPod = app.pods[index]
66 | pod := app.currentPod
67 | app.ui.ClearContainers()
68 | for _, c := range append(pod.Spec.InitContainers, pod.Spec.Containers...) {
69 | app.ui.AddContainer(c.Name)
70 | }
71 | app.ui.SelectContainerAt(0)
72 | }
73 |
74 | // OnQuit handles events on quit is required by UI
75 | func (app *App) OnQuit() {
76 | app.Quit()
77 | }
78 |
79 | // StartTailLog starts tailing logs for container of pod in namespace
80 | func (app *App) StartTailLog(namespace, pod, container string) {
81 | app.StopTailLog()
82 |
83 | app.logworker.Start(func(ctx context.Context) error {
84 | logs, err := app.client.WatchLogs(ctx, namespace, pod, container)
85 | if err != nil {
86 | return err
87 | }
88 |
89 | // make channel to guarantee line order of logs
90 | ch := make(chan string)
91 | defer close(ch)
92 | for log := range logs {
93 | app.PostFunc(func() {
94 | for line := range ch {
95 | app.ui.AddPagerText(line)
96 | break
97 | }
98 | })
99 | select {
100 | case ch <- log:
101 | case <-ctx.Done():
102 | return nil
103 | }
104 | }
105 | return nil
106 | })
107 | }
108 |
109 | // StopTailLog stops tailing logs
110 | func (app *App) StopTailLog() {
111 | app.logworker.Stop()
112 | // TODO handle err
113 | }
114 |
115 | // StartTailPods tarts tailing pods on Kubernetes
116 | func (app *App) StartTailPods() {
117 | app.StopTailLog()
118 | app.podworker.Start(func(ctx context.Context) error {
119 | events, err := app.client.WatchPods(ctx, app.namespace)
120 | if err != nil {
121 | return err
122 | }
123 | for ev := range events {
124 | ev := ev
125 | app.PostFunc(func() {
126 | pod := ev.Pod
127 | switch ev.Type {
128 | case k8s.PodAdded:
129 | app.pods = append(app.pods, pod)
130 | app.ui.AddPod(pod.Name, types.GetPodStatus(pod))
131 | if len(app.pods) == 1 {
132 | app.ui.SelectPodAt(0)
133 | }
134 | case k8s.PodModified:
135 | app.ui.SetPodStatus(pod.Name, types.GetPodStatus(pod))
136 | case k8s.PodDeleted:
137 | for i, p := range app.pods {
138 | if p.Name == pod.Name {
139 | app.pods = append(app.pods[:i], app.pods[i+1:]...)
140 | break
141 | }
142 | }
143 | app.ui.DeletePod(pod.Name)
144 | }
145 | })
146 |
147 | }
148 | return nil
149 | })
150 | }
151 |
152 | // StopTailPods stops tailing pods
153 | func (app *App) StopTailPods() {
154 | app.podworker.Stop()
155 | // TODO handle err
156 | }
157 |
158 | // Run starts logbook application
159 | func (app *App) Run(ctx context.Context) error {
160 | app.StartTailPods()
161 | return app.Application.Run()
162 | }
163 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 |
8 | "github.com/spf13/cobra"
9 | "github.com/ueokande/logbook/pkg/k8s"
10 | )
11 |
12 | var homedir string
13 |
14 | func init() {
15 | if h := os.Getenv("HOME"); h != "" {
16 | homedir = h
17 | } else {
18 | homedir = os.Getenv("USERPROFILE") // windows
19 | }
20 | }
21 |
22 | // params contains values of the command-line parameter
23 | type params struct {
24 | namespace string
25 | kubeconfig string
26 | }
27 |
28 | func main() {
29 | p := params{}
30 |
31 | cmd := &cobra.Command{}
32 | cmd.Short = "View logs on multiple pods and containers from Kubernetes"
33 |
34 | cmd.Flags().StringVarP(&p.namespace, "namespace", "n", p.namespace, "Kubernetes namespace to use. Default to namespace configured in Kubernetes context")
35 | cmd.Flags().StringVarP(&p.kubeconfig, "kubeconfig", "", p.kubeconfig, " Path to kubeconfig file to use")
36 |
37 | cmd.RunE = func(cmd *cobra.Command, args []string) error {
38 | ctx, cancel := context.WithCancel(context.Background())
39 | defer cancel()
40 |
41 | context, err := k8s.LoadCurrentContext(p.kubeconfig)
42 | if err != nil {
43 | return err
44 | }
45 |
46 | client, err := k8s.NewClient(p.kubeconfig)
47 | if err != nil {
48 | return err
49 | }
50 |
51 | config := &AppConfig{
52 | Cluster: context.Cluster,
53 | Namespace: "default",
54 | }
55 | if len(context.Namespace) > 0 {
56 | config.Namespace = context.Namespace
57 | }
58 | if len(p.namespace) > 0 {
59 | config.Namespace = p.namespace
60 | }
61 |
62 | app := NewApp(client, config)
63 | err = app.Run(ctx)
64 | if err != nil {
65 | return err
66 | }
67 |
68 | return nil
69 | }
70 |
71 | if err := cmd.Execute(); err != nil {
72 | fmt.Fprintln(os.Stderr, err)
73 | os.Exit(1)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/pkg/k8s/client.go:
--------------------------------------------------------------------------------
1 | package k8s
2 |
3 | import (
4 | "github.com/pkg/errors"
5 | "k8s.io/client-go/kubernetes"
6 | "k8s.io/client-go/tools/clientcmd"
7 | "k8s.io/client-go/tools/clientcmd/api"
8 | )
9 |
10 | // Client is a wrapper for a Kubernetes client
11 | type Client struct {
12 | clientset *kubernetes.Clientset
13 | }
14 |
15 | // NewClient loads Kubernetes configuration by the kubeconfig and returns new
16 | // Client
17 | func NewClient(kubeconfig string) (*Client, error) {
18 | kubeConfig := getKubeConfig(kubeconfig)
19 | clientConfig, err := kubeConfig.ClientConfig()
20 | if err != nil {
21 | return nil, err
22 | }
23 | clientset, err := kubernetes.NewForConfig(clientConfig)
24 | if err != nil {
25 | return nil, err
26 | }
27 |
28 | return &Client{
29 | clientset: clientset,
30 | }, nil
31 | }
32 |
33 | // LoadCurrentContext loads a context in KUBECONFIG and returns it
34 | func LoadCurrentContext(kubeconfig string) (*api.Context, error) {
35 | kubeConfig := getKubeConfig(kubeconfig)
36 | rawConfig, err := kubeConfig.RawConfig()
37 | if err != nil {
38 | return nil, errors.Wrap(err, "failed to get raw config")
39 | }
40 | return rawConfig.Contexts[rawConfig.CurrentContext], nil
41 | }
42 |
43 | func getKubeConfig(kubeconfig string) clientcmd.ClientConfig {
44 | rules := clientcmd.NewDefaultClientConfigLoadingRules()
45 | if len(kubeconfig) > 0 {
46 | rules.Precedence = []string{kubeconfig}
47 | }
48 | overrides := &clientcmd.ConfigOverrides{}
49 |
50 | return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides)
51 | }
52 |
--------------------------------------------------------------------------------
/pkg/k8s/logs.go:
--------------------------------------------------------------------------------
1 | package k8s
2 |
3 | import (
4 | "bufio"
5 | "context"
6 |
7 | corev1 "k8s.io/api/core/v1"
8 | )
9 |
10 | // WatchLogs watches container's logs of pod in namespace. It returns channels
11 | // to subscribe log lines.
12 | func (c *Client) WatchLogs(ctx context.Context, namespace, pod, container string) (<-chan string, error) {
13 | opts := &corev1.PodLogOptions{
14 | Container: container,
15 | Follow: true,
16 | }
17 | req := c.clientset.CoreV1().Pods(namespace).GetLogs(pod, opts)
18 | req.Context(ctx)
19 | r, err := req.Stream()
20 | if err != nil {
21 | return nil, err
22 | }
23 |
24 | // TODO handle s.Err()
25 | s := bufio.NewScanner(r)
26 |
27 | ch := make(chan string)
28 | go func() {
29 | for s.Scan() {
30 | ch <- s.Text()
31 | }
32 | close(ch)
33 | defer r.Close()
34 | }()
35 |
36 | return ch, nil
37 | }
38 |
--------------------------------------------------------------------------------
/pkg/k8s/pods.go:
--------------------------------------------------------------------------------
1 | package k8s
2 |
3 | import (
4 | "context"
5 |
6 | corev1 "k8s.io/api/core/v1"
7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8 | "k8s.io/apimachinery/pkg/watch"
9 | )
10 |
11 | // PodEventType represents an event type of the pod
12 | type PodEventType int
13 |
14 | // The event type of the pods
15 | const (
16 | PodAdded PodEventType = iota // The pod is added
17 | PodModified // The pod is updated
18 | PodDeleted // The pod is deleted
19 | )
20 |
21 | // PodEvent represents an event of the pods in Kubernetes API
22 | type PodEvent struct {
23 | Type PodEventType
24 | Pod *corev1.Pod
25 | }
26 |
27 | // WatchPods watches pods from Kubernetes API in namespace. It returns a
28 | // channel to subscribe pods.
29 | func (c *Client) WatchPods(ctx context.Context, namespace string) (<-chan *PodEvent, error) {
30 | r, err := c.clientset.CoreV1().Pods(namespace).Watch(metav1.ListOptions{})
31 | if err != nil {
32 | return nil, err
33 | }
34 | ch := make(chan *PodEvent)
35 | go func() {
36 | <-ctx.Done()
37 | r.Stop()
38 | }()
39 | go func() {
40 | for ev := range r.ResultChan() {
41 | pod, ok := ev.Object.(*corev1.Pod)
42 | if !ok {
43 | continue
44 | }
45 | var t PodEventType
46 | switch ev.Type {
47 | case watch.Added:
48 | t = PodAdded
49 | case watch.Modified:
50 | t = PodModified
51 | case watch.Deleted:
52 | t = PodDeleted
53 | default:
54 | continue
55 | }
56 |
57 | ch <- &PodEvent{
58 | Type: t,
59 | Pod: pod,
60 | }
61 | }
62 | close(ch)
63 | }()
64 | return ch, nil
65 | }
66 |
--------------------------------------------------------------------------------
/pkg/types/pod_status.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | corev1 "k8s.io/api/core/v1"
5 | )
6 |
7 | // PodStatus represents pod's status
8 | type PodStatus string
9 |
10 | // PodStatus is a pod status of the pod. They determined the pod's phase,
11 | // status and its containers.
12 | const (
13 | PodRunning PodStatus = "Running" // The pod is running successfully,
14 | PodSucceeded = "Succeeded" // The pod created by job is completed successfully,
15 | PodPending = "Pending" // The Pod has been accepted, but some of the container images has not been created.
16 | PodTerminating = "Terminating" // The pod is terminating
17 | PodInitializing = "Initializing" // The init containers are running, or some of the container is initializing.
18 | PodFailed = "Failed" // The pod fails on the init ocntaienrs, or one or more of the container exits without exit code 0.
19 | PodUnknown = "Unknown" // Unknown phase or status
20 | )
21 |
22 | // GetPodStatus returns the status of the pod as PodStatus
23 | func GetPodStatus(pod *corev1.Pod) PodStatus {
24 | switch pod.Status.Phase {
25 | case corev1.PodSucceeded:
26 | return PodSucceeded
27 | case corev1.PodPending:
28 | return PodPending
29 | case corev1.PodFailed:
30 | return PodFailed
31 | case corev1.PodUnknown:
32 | return PodUnknown
33 | }
34 |
35 | for _, container := range pod.Status.InitContainerStatuses {
36 | switch {
37 | case container.State.Terminated != nil && container.State.Terminated.ExitCode == 0:
38 | continue
39 | case container.State.Terminated != nil:
40 | return PodFailed
41 | default:
42 | return PodInitializing
43 | }
44 | break
45 | }
46 |
47 | hasCompleted := false
48 | hasRunning := false
49 | for i := len(pod.Status.ContainerStatuses) - 1; i >= 0; i-- {
50 | container := pod.Status.ContainerStatuses[i]
51 | if container.State.Waiting != nil && container.State.Waiting.Reason != "" {
52 | return PodInitializing
53 | } else if container.State.Terminated != nil && container.State.Terminated.Reason == "Completed" {
54 | hasCompleted = true
55 | } else if container.State.Terminated != nil {
56 | return PodFailed
57 | } else if container.Ready && container.State.Running != nil {
58 | hasRunning = true
59 | }
60 | }
61 | if hasCompleted && hasRunning {
62 | return PodRunning
63 | }
64 |
65 | if pod.DeletionTimestamp != nil && pod.Status.Reason == "NodeLost" {
66 | return PodUnknown
67 | } else if pod.DeletionTimestamp != nil {
68 | return PodTerminating
69 | }
70 | return PodRunning
71 | }
72 |
--------------------------------------------------------------------------------
/pkg/ui/keys.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "github.com/gdamore/tcell"
5 | )
6 |
7 | func (ui *UI) handleEventKey(ev *tcell.EventKey) bool {
8 | var handles []func(ev *tcell.EventKey) bool
9 | switch ui.mode {
10 | case ModeNormal:
11 | handles = []func(ev *tcell.EventKey) bool{
12 | ui.handleKeyInputFind,
13 | ui.handleKeySelectContainer,
14 | ui.handleKeyToggleFollowMode,
15 | ui.handleKeyScroll,
16 | ui.handleKeyFind,
17 | ui.handleKeyQuit,
18 | }
19 | case ModeFollow:
20 | handles = []func(ev *tcell.EventKey) bool{
21 | ui.handleKeySelectContainer,
22 | ui.handleKeyToggleFollowMode,
23 | ui.handleKeyQuit,
24 | }
25 | case ModeInputFind:
26 | handles = []func(ev *tcell.EventKey) bool{
27 | ui.handleEventKeyInput,
28 | ui.handleKeyQuit,
29 | }
30 | }
31 |
32 | for _, h := range handles {
33 | if h(ev) {
34 | return true
35 | }
36 | }
37 |
38 | return false
39 | }
40 |
41 | func (ui *UI) handleKeyQuit(ev *tcell.EventKey) bool {
42 | switch ev.Key() {
43 | case tcell.KeyCtrlC:
44 | ui.listener.OnQuit()
45 | return true
46 | case tcell.KeyRune:
47 | switch ev.Rune() {
48 | case 'q':
49 | ui.listener.OnQuit()
50 | return true
51 | }
52 | }
53 | return false
54 | }
55 |
56 | func (ui *UI) handleKeyInputFind(ev *tcell.EventKey) bool {
57 | switch ev.Key() {
58 | case tcell.KeyRune:
59 | switch ev.Rune() {
60 | case '/':
61 | ui.enterFindInputMode()
62 | return true
63 | }
64 | }
65 | return false
66 | }
67 |
68 | func (ui *UI) handleKeyToggleFollowMode(ev *tcell.EventKey) bool {
69 | switch ev.Key() {
70 | case tcell.KeyRune:
71 | switch ev.Rune() {
72 | case 'f':
73 | ui.toggleFollowMode()
74 | return true
75 | }
76 | }
77 | return false
78 | }
79 |
80 | func (ui *UI) handleKeySelectContainer(ev *tcell.EventKey) bool {
81 | switch ev.Key() {
82 | case tcell.KeyCtrlP:
83 | ui.pods.SelectPrev()
84 | ui.pager.SetKeyword("")
85 | return true
86 | case tcell.KeyCtrlN:
87 | ui.pods.SelectNext()
88 | ui.pager.SetKeyword("")
89 | return true
90 | case tcell.KeyTab:
91 | ui.containers.SelectNext()
92 | return true
93 | }
94 | return false
95 | }
96 |
97 | func (ui *UI) handleKeyScroll(ev *tcell.EventKey) bool {
98 | switch ev.Key() {
99 | case tcell.KeyCtrlD:
100 | ui.scrollHalfPageDown()
101 | return true
102 | case tcell.KeyCtrlU:
103 | ui.scrollHalfPageUp()
104 | return true
105 | case tcell.KeyCtrlB:
106 | ui.scrollPageDown()
107 | return true
108 | case tcell.KeyCtrlF:
109 | ui.scrollPageUp()
110 | return true
111 | case tcell.KeyUp:
112 | ui.scrollUp()
113 | case tcell.KeyDown:
114 | ui.scrollDown()
115 | case tcell.KeyRight:
116 | ui.scrollHaftPageRight()
117 | case tcell.KeyLeft:
118 | ui.scrollHalfPageLeft()
119 | case tcell.KeyRune:
120 | switch ev.Rune() {
121 | case 'k':
122 | ui.scrollUp()
123 | return true
124 | case 'j':
125 | ui.scrollDown()
126 | return true
127 | case 'g':
128 | ui.scrollToTop()
129 | return true
130 | case 'G':
131 | ui.scrollToBottom()
132 | return true
133 | case 'h':
134 | ui.scrollHalfPageLeft()
135 | return true
136 | case 'l':
137 | ui.scrollHaftPageRight()
138 | return true
139 | }
140 | }
141 | return false
142 | }
143 |
144 | func (ui *UI) handleKeyFind(ev *tcell.EventKey) bool {
145 | switch ev.Key() {
146 | case tcell.KeyRune:
147 | switch ev.Rune() {
148 | case 'n':
149 | ui.findNext()
150 | return true
151 | case 'N':
152 | ui.findPrev()
153 | return true
154 | }
155 | }
156 | return false
157 | }
158 |
159 | func (ui *UI) handleEventKeyInput(ev *tcell.EventKey) bool {
160 | switch ev.Key() {
161 | case tcell.KeyEnter:
162 | ui.startFind()
163 | return true
164 | case tcell.KeyEscape:
165 | ui.startFind()
166 | return true
167 | }
168 | return ui.input.HandleEvent(ev)
169 | }
170 |
--------------------------------------------------------------------------------
/pkg/ui/statusbar.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/gdamore/tcell"
7 | "github.com/gdamore/tcell/views"
8 | )
9 |
10 | var (
11 | styleStatusBarModeNormal = tcell.StyleDefault.Background(tcell.ColorYellowGreen).Foreground(tcell.ColorDarkGreen).Bold(true)
12 | styleStatusBarModeFollow = tcell.StyleDefault.Background(tcell.ColorRed).Foreground(tcell.ColorWhite).Bold(true)
13 | styleStatusBarContext = tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorSilver)
14 | styleStatusBarPods = tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)
15 | styleStatusBarScroll = tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)
16 | )
17 |
18 | // StatusBar is a status bar on the bottom of the UI
19 | type StatusBar struct {
20 | mode *views.Text
21 | pods *views.Text
22 | context *views.Text
23 | scroll *views.Text
24 | views.BoxLayout
25 | }
26 |
27 | // NewStatusBar returns a new status bar
28 | func NewStatusBar() *StatusBar {
29 | mode := &views.Text{}
30 | mode.SetStyle(styleStatusBarPods)
31 | pods := &views.Text{}
32 | pods.SetStyle(styleStatusBarPods)
33 | context := &views.Text{}
34 | context.SetAlignment(views.AlignMiddle)
35 | context.SetStyle(styleStatusBarContext)
36 | scroll := &views.Text{}
37 | scroll.SetStyle(styleStatusBarScroll)
38 |
39 | w := &StatusBar{
40 | mode: mode,
41 | pods: pods,
42 | context: context,
43 | scroll: scroll,
44 | }
45 | w.AddWidget(mode, 0)
46 | w.AddWidget(pods, 0)
47 | w.AddWidget(context, 1)
48 | w.AddWidget(scroll, 0)
49 | return w
50 | }
51 |
52 | // SetMode sets current mode on the status bar
53 | func (w *StatusBar) SetMode(mode Mode) {
54 | switch mode {
55 | case ModeNormal:
56 | w.mode.SetText(" NORMAL ")
57 | w.mode.SetStyle(styleStatusBarModeNormal)
58 | case ModeFollow:
59 | w.mode.SetText(" FOLLOW ")
60 | w.mode.SetStyle(styleStatusBarModeFollow)
61 | default:
62 | panic("unsupported mode")
63 | }
64 | }
65 |
66 | // SetContext sets current kubeconfig context (cluster name and namespace) on
67 | // the status bar
68 | func (w *StatusBar) SetContext(cluster, namespace string) {
69 | w.context.SetText(fmt.Sprintf("%s/%s", cluster, namespace))
70 | }
71 |
72 | // SetPodCount sets the count of the pods
73 | func (w *StatusBar) SetPodCount(count int) {
74 | w.pods.SetText(fmt.Sprintf(" %d Pods ", count))
75 | }
76 |
77 | // SetScroll sets the percent of the scroll
78 | func (w *StatusBar) SetScroll(percent int) {
79 | w.scroll.SetText(fmt.Sprintf(" %d%% ", percent))
80 | }
81 |
--------------------------------------------------------------------------------
/pkg/ui/ui.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "github.com/gdamore/tcell"
5 | "github.com/gdamore/tcell/views"
6 | "github.com/ueokande/logbook/pkg/types"
7 | "github.com/ueokande/logbook/pkg/widgets"
8 | )
9 |
10 | // Mode represents a mode on UI
11 | type Mode int
12 |
13 | // UI mode
14 | const (
15 | ModeNormal Mode = iota // Normal mode
16 | ModeFollow // Follow mode
17 | ModeInputFind // Input find mode
18 | )
19 |
20 | var (
21 | stylePodActive = tcell.StyleDefault.Foreground(tcell.ColorGreen)
22 | stylePodError = tcell.StyleDefault.Foreground(tcell.ColorRed)
23 | stylePodPending = tcell.StyleDefault.Foreground(tcell.ColorYellow)
24 | )
25 |
26 | // EventListener is a listener interface for UI events
27 | type EventListener interface {
28 | // OnQuit is invoked on the quit is required
29 | OnQuit()
30 |
31 | // OnPodSelected is invoked when the selected pod is changed
32 | OnPodSelected(name string, index int)
33 |
34 | // OnContainerSelected is invoked when the selected container is changed
35 | OnContainerSelected(name string, index int)
36 | }
37 |
38 | type nopListener struct{}
39 |
40 | func (l nopListener) OnContainerSelected(name string, index int) {}
41 |
42 | func (l nopListener) OnPodSelected(name string, index int) {}
43 |
44 | func (l nopListener) OnQuit() {}
45 |
46 | // UI is an user interface for the logbook
47 | type UI struct {
48 | input *widgets.InputLine
49 | pods *widgets.ListView
50 | containers *widgets.Tabs
51 | pager *widgets.Pager
52 | statusbar *StatusBar
53 |
54 | mode Mode
55 | keyword string
56 | listener EventListener
57 |
58 | views.BoxLayout
59 | }
60 |
61 | // NewUI returns new UI
62 | func NewUI() *UI {
63 | statusbar := NewStatusBar()
64 | input := widgets.NewInputLine()
65 | pods := widgets.NewListView()
66 | line := widgets.NewVerticalLine(tcell.RuneVLine, tcell.StyleDefault)
67 | pager := widgets.NewPager()
68 | containers := widgets.NewTabs()
69 |
70 | detailLayout := &views.BoxLayout{}
71 | detailLayout.SetOrientation(views.Vertical)
72 | detailLayout.AddWidget(containers, 0)
73 | detailLayout.AddWidget(pager, 1)
74 |
75 | mainLayout := &views.BoxLayout{}
76 | mainLayout.SetOrientation(views.Horizontal)
77 | mainLayout.AddWidget(pods, 0)
78 | mainLayout.AddWidget(line, 0)
79 | mainLayout.AddWidget(detailLayout, 1)
80 |
81 | ui := &UI{
82 | input: input,
83 | pods: pods,
84 | containers: containers,
85 | pager: pager,
86 | statusbar: statusbar,
87 | listener: &nopListener{},
88 | }
89 |
90 | ui.SetOrientation(views.Vertical)
91 | ui.AddWidget(mainLayout, 1)
92 | ui.AddWidget(statusbar, 0)
93 |
94 | pods.Watch(ui)
95 | containers.Watch(ui)
96 |
97 | return ui
98 | }
99 |
100 | // WatchUIEvents registers an EventListener for the UI
101 | func (ui *UI) WatchUIEvents(l EventListener) {
102 | ui.listener = l
103 | }
104 |
105 | // AddPod adds a pod by the name and its status to the list view.
106 | func (ui *UI) AddPod(name string, status types.PodStatus) {
107 | ui.pods.AddItem(name, podStatusStyle(status))
108 | ui.statusbar.SetPodCount(ui.pods.ItemCount())
109 | }
110 |
111 | // DeletePod deletes pod by the name on the list view.
112 | func (ui *UI) DeletePod(name string) {
113 | ui.pods.DeleteItem(name)
114 | ui.statusbar.SetPodCount(ui.pods.ItemCount())
115 | }
116 |
117 | // SetPodStatus updates the pod status by name to the status
118 | func (ui *UI) SetPodStatus(name string, status types.PodStatus) {
119 | ui.pods.SetStyle(name, podStatusStyle(status))
120 | }
121 |
122 | // AddContainer adds container by the name into the tabs
123 | func (ui *UI) AddContainer(name string) {
124 | ui.containers.AddTab(name)
125 | }
126 |
127 | // ClearContainers clears containers in the tabs
128 | func (ui *UI) ClearContainers() {
129 | ui.containers.Clear()
130 | }
131 |
132 | // SetContext sets kubenetes context (the cluster name and the namespace)
133 | func (ui *UI) SetContext(cluster, namespace string) {
134 | ui.statusbar.SetContext(cluster, namespace)
135 | }
136 |
137 | // HandleEvent handles events on tcell
138 | func (ui *UI) HandleEvent(ev tcell.Event) bool {
139 | switch ev := ev.(type) {
140 | case *widgets.EventItemSelected:
141 | switch ev.Widget() {
142 | case ui.containers:
143 | ui.listener.OnContainerSelected(ev.Name, ev.Index)
144 | return true
145 | case ui.pods:
146 | ui.listener.OnPodSelected(ev.Name, ev.Index)
147 | return true
148 | }
149 | case *tcell.EventKey:
150 | return ui.handleEventKey(ev)
151 | }
152 | return false
153 | }
154 |
155 | // AddPagerText adds text line into the pager
156 | func (ui *UI) AddPagerText(line string) {
157 | ui.pager.AppendLine(line)
158 | if ui.mode == ModeFollow {
159 | ui.pager.ScrollToBottom()
160 | }
161 | ui.updateScrollStatus()
162 | }
163 |
164 | // ClearPager clears the pager
165 | func (ui *UI) ClearPager() {
166 | ui.pager.ClearText()
167 | ui.updateScrollStatus()
168 | ui.DisableFollowMode()
169 | }
170 |
171 | // SetStatusMode sets the mode in the status bar
172 | func (ui *UI) SetStatusMode(mode Mode) {
173 | ui.statusbar.SetMode(mode)
174 | }
175 |
176 | func (ui *UI) scrollDown() {
177 | if ui.mode == ModeFollow {
178 | return
179 | }
180 | ui.pager.ScrollDown()
181 | ui.updateScrollStatus()
182 | }
183 |
184 | func (ui *UI) scrollUp() {
185 | if ui.mode == ModeFollow {
186 | return
187 | }
188 | ui.pager.ScrollUp()
189 | ui.updateScrollStatus()
190 | }
191 |
192 | func (ui *UI) scrollPageDown() {
193 | if ui.mode == ModeFollow {
194 | return
195 | }
196 | ui.pager.ScrollPageDown()
197 | ui.updateScrollStatus()
198 | }
199 |
200 | func (ui *UI) scrollPageUp() {
201 | if ui.mode == ModeFollow {
202 | return
203 | }
204 | ui.pager.ScrollPageUp()
205 | ui.updateScrollStatus()
206 | }
207 |
208 | func (ui *UI) scrollHalfPageDown() {
209 | if ui.mode == ModeFollow {
210 | return
211 | }
212 | ui.pager.ScrollHalfPageDown()
213 | ui.updateScrollStatus()
214 | }
215 |
216 | func (ui *UI) scrollToTop() {
217 | if ui.mode == ModeFollow {
218 | return
219 | }
220 | ui.pager.ScrollToTop()
221 | ui.updateScrollStatus()
222 | }
223 |
224 | func (ui *UI) scrollToBottom() {
225 | if ui.mode == ModeFollow {
226 | return
227 | }
228 | ui.pager.ScrollToBottom()
229 | ui.updateScrollStatus()
230 | }
231 |
232 | func (ui *UI) scrollHalfPageUp() {
233 | if ui.mode == ModeFollow {
234 | return
235 | }
236 | ui.pager.ScrollHalfPageUp()
237 | ui.updateScrollStatus()
238 | }
239 |
240 | func (ui *UI) scrollHalfPageLeft() {
241 | if ui.mode == ModeFollow {
242 | return
243 | }
244 | ui.pager.ScrollHalfPageLeft()
245 | ui.updateScrollStatus()
246 | }
247 |
248 | func (ui *UI) scrollHaftPageRight() {
249 | if ui.mode == ModeFollow {
250 | return
251 | }
252 | ui.pager.ScrollHalfPageRight()
253 | ui.updateScrollStatus()
254 | }
255 |
256 | func (ui *UI) toggleFollowMode() {
257 | if ui.mode == ModeFollow {
258 | ui.DisableFollowMode()
259 | } else {
260 | ui.EnableFollowMode()
261 | }
262 | }
263 |
264 | // EnableFollowMode enabled follow mode on the pager
265 | func (ui *UI) EnableFollowMode() {
266 | ui.mode = ModeFollow
267 | ui.statusbar.SetMode(ModeFollow)
268 | ui.pager.ScrollToBottom()
269 | ui.updateScrollStatus()
270 | }
271 |
272 | // DisableFollowMode disables follow mode on the pager
273 | func (ui *UI) DisableFollowMode() {
274 | ui.mode = ModeNormal
275 | ui.statusbar.SetMode(ModeNormal)
276 | }
277 |
278 | // SelectPodAt selects a pod by the index
279 | func (ui *UI) SelectPodAt(index int) {
280 | ui.pods.SelectAt(index)
281 | }
282 |
283 | // SelectContainerAt selects a container by the index
284 | func (ui *UI) SelectContainerAt(index int) {
285 | ui.containers.SelectAt(index)
286 | }
287 |
288 | func (ui *UI) updateScrollStatus() {
289 | y := ui.pager.GetScrollYPosition()
290 | ui.statusbar.SetScroll(int(y * 100))
291 | }
292 |
293 | func (ui *UI) enterFindInputMode() {
294 | ui.input.SetPrompt("/")
295 | ui.input.SetValue("")
296 | ui.mode = ModeInputFind
297 | ui.RemoveWidget(ui.statusbar)
298 | ui.AddWidget(ui.input, 0)
299 | }
300 |
301 | func (ui *UI) findNext() {
302 | k := ui.pager.Keyword()
303 | if len(k) == 0 {
304 | ui.pager.SetKeyword(ui.keyword)
305 | }
306 | ui.pager.FindNext()
307 | }
308 |
309 | func (ui *UI) findPrev() {
310 | k := ui.pager.Keyword()
311 | if len(k) == 0 {
312 | ui.pager.SetKeyword(ui.keyword)
313 | }
314 | ui.pager.FindPrev()
315 | }
316 |
317 | func (ui *UI) cancelInput() {
318 | ui.mode = ModeNormal
319 | ui.RemoveWidget(ui.statusbar)
320 | ui.AddWidget(ui.input, 0)
321 | }
322 |
323 | func (ui *UI) startFind() {
324 | keyword := ui.input.Value()
325 | if len(keyword) > 0 {
326 | // Use previous keyword if the input is empty
327 | ui.keyword = keyword
328 | }
329 | ui.mode = ModeNormal
330 | ui.AddWidget(ui.statusbar, 0)
331 | ui.RemoveWidget(ui.input)
332 | ui.pager.SetKeyword(ui.keyword)
333 | ui.pager.FindNext()
334 | }
335 |
336 | func podStatusStyle(status types.PodStatus) tcell.Style {
337 | switch status {
338 | case types.PodRunning, types.PodSucceeded:
339 | return stylePodActive
340 | case types.PodPending, types.PodInitializing, types.PodTerminating:
341 | return stylePodPending
342 | default:
343 | return stylePodError
344 | }
345 | }
346 |
--------------------------------------------------------------------------------
/pkg/widgets/event.go:
--------------------------------------------------------------------------------
1 | package widgets
2 |
3 | import (
4 | "github.com/gdamore/tcell"
5 | "github.com/gdamore/tcell/views"
6 | )
7 |
8 | // EventItemSelected represents an event on the item selected
9 | type EventItemSelected struct {
10 | // The name of the item
11 | Name string
12 |
13 | // The index of the item
14 | Index int
15 |
16 | widget views.Widget
17 | tcell.EventTime
18 | }
19 |
20 | // Widget returns a target widget of the event
21 | func (e *EventItemSelected) Widget() views.Widget {
22 | return e.widget
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/widgets/highlight_text.go:
--------------------------------------------------------------------------------
1 | package widgets
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/gdamore/tcell"
7 | "github.com/gdamore/tcell/views"
8 | "github.com/mattn/go-runewidth"
9 | )
10 |
11 | type point struct {
12 | x int
13 | y int
14 | }
15 |
16 | var styleHighlightCurrent = tcell.StyleDefault.Background(tcell.ColorYellow)
17 |
18 | // HighlightText is a text widget with highlighted keyword
19 | type HighlightText struct {
20 | highlights []int
21 | current int
22 | keyword []rune
23 |
24 | text views.Text
25 | views.WidgetWatchers
26 | }
27 |
28 | // Draw draws the HighlightText.
29 | func (t *HighlightText) Draw() {
30 | t.text.Draw()
31 | }
32 |
33 | // Size returns the width and height of the HighlightText
34 | func (t *HighlightText) Size() (int, int) {
35 | return t.text.Size()
36 | }
37 |
38 | // SetView sets the view for the HighlightText
39 | func (t *HighlightText) SetView(view views.View) {
40 | t.text.SetView(view)
41 | }
42 |
43 | // HandleEvent implements a tcell.EventHandler
44 | func (t *HighlightText) HandleEvent(ev tcell.Event) bool {
45 | return t.text.HandleEvent(ev)
46 | }
47 |
48 | // AppendLine appends the line into the content
49 | func (t *HighlightText) AppendLine(line string) {
50 | text := t.text.Text()
51 | if len(text) > 0 {
52 | text += "\n"
53 | }
54 | text += line
55 | t.text.SetText(text)
56 |
57 | t.resetHighlights()
58 |
59 | t.PostEventWidgetContent(t)
60 | }
61 |
62 | // ClearText clears current content and highlights
63 | func (t *HighlightText) ClearText() {
64 | t.text.SetText("")
65 | t.keyword = nil
66 | t.current = -1
67 | t.highlights = nil
68 | }
69 |
70 | // SetKeyword sets the keyword to be highlighted in the content
71 | func (t *HighlightText) SetKeyword(keyword string) {
72 | t.keyword = []rune(keyword)
73 | t.current = -1
74 | t.text.SetStyle(t.text.Style())
75 | if len(keyword) == 0 {
76 | return
77 | }
78 |
79 | t.resetHighlights()
80 | t.PostEventWidgetContent(t)
81 | }
82 |
83 | func (t *HighlightText) resetHighlights() {
84 | t.highlights = nil
85 |
86 | str := t.text.Text()
87 | keyword := string(t.keyword)
88 | var x int
89 | for len(keyword) > 0 {
90 | i := strings.Index(str, keyword)
91 | if i == -1 {
92 | break
93 | }
94 | start := len([]rune(str[:i])) + x
95 | t.highlights = append(t.highlights, start)
96 | str = str[i+len(keyword):]
97 | x += i + len(keyword)
98 | }
99 |
100 | for i, start := range t.highlights {
101 | style := t.text.Style().Reverse(true)
102 | if i == t.current {
103 | style = styleHighlightCurrent
104 | }
105 | for offset := range t.keyword {
106 | t.text.SetStyleAt(start+offset, style)
107 | }
108 | }
109 | }
110 |
111 | // Keyword returns the current keyword in the content
112 | func (t *HighlightText) Keyword() string {
113 | return string(t.keyword)
114 | }
115 |
116 | // Resize is called when the View changes sizes.
117 | func (t *HighlightText) Resize() {
118 | t.text.Resize()
119 | }
120 |
121 | // HighlightPos returns the position (x, y) of highlighted text in the content
122 | func (t *HighlightText) HighlightPos(index int) (int, int) {
123 | if index < 0 || index >= len(t.highlights) {
124 | panic("index out of range")
125 | }
126 |
127 | start := t.highlights[index]
128 | var x, y int
129 | for i, c := range []rune(t.text.Text()) {
130 | if i == start {
131 | break
132 | }
133 | if c == '\n' {
134 | x = 0
135 | y++
136 | continue
137 | }
138 | x += runewidth.RuneWidth(c)
139 | }
140 | return x, y
141 | }
142 |
143 | // HighlightCount returns the count of the highlighted keywords
144 | func (t *HighlightText) HighlightCount() int {
145 | return len(t.highlights)
146 | }
147 |
148 | // ActivateHighlight makes the highlighted keyword active (focused)
149 | func (t *HighlightText) ActivateHighlight(index int) {
150 | if index < 0 || index > len(t.highlights) {
151 | panic("index out of range")
152 | }
153 | t.current = index
154 | t.resetHighlights()
155 | t.PostEventWidgetContent(t)
156 | }
157 |
158 | // CurrentHighlight returns the index of the current highlight. It returns -1 if no highlights are active.
159 | func (t *HighlightText) CurrentHighlight() int {
160 | return t.current
161 | }
162 |
--------------------------------------------------------------------------------
/pkg/widgets/input.go:
--------------------------------------------------------------------------------
1 | package widgets
2 |
3 | import (
4 | "github.com/gdamore/tcell"
5 | "github.com/gdamore/tcell/views"
6 | "github.com/mattn/go-runewidth"
7 | )
8 |
9 | // InputLine is a single-line input widget
10 | type InputLine struct {
11 | view views.View
12 | style tcell.Style
13 | value []rune
14 | prompt []rune
15 | content string
16 | cursor int
17 |
18 | views.WidgetWatchers
19 | }
20 |
21 | // NewInputLine returns new InputLine
22 | func NewInputLine() *InputLine {
23 | return &InputLine{}
24 | }
25 |
26 | // SetPrompt sets the prompt of the input
27 | func (w *InputLine) SetPrompt(prompt string) {
28 | w.prompt = []rune(prompt)
29 | w.PostEventWidgetContent(w)
30 | }
31 |
32 | // SetValue sets the value of the input
33 | func (w *InputLine) SetValue(value string) {
34 | w.value = []rune(value)
35 | w.cursor = len(w.value)
36 | w.PostEventWidgetContent(w)
37 | }
38 |
39 | // Value gets current value of the input
40 | func (w *InputLine) Value() string {
41 | return string(w.value)
42 | }
43 |
44 | // SetStyle sets the style of the input
45 | func (w *InputLine) SetStyle(style tcell.Style) {
46 | w.style = style
47 | w.PostEventWidgetContent(w)
48 | }
49 |
50 | // SetCursorAt sets the pos of the cursor
51 | func (w *InputLine) SetCursorAt(pos int) {
52 | w.cursor = pos
53 | if w.cursor < 0 {
54 | w.cursor = 0
55 | } else if w.cursor > len(w.value) {
56 | w.cursor = len(w.value)
57 | }
58 |
59 | w.PostEventWidgetContent(w)
60 | }
61 |
62 | // Draw draws the input with the cursor
63 | func (w *InputLine) Draw() {
64 | if w.view == nil {
65 | return
66 | }
67 |
68 | var x int
69 | for _, c := range w.prompt {
70 | w.view.SetContent(x, 0, c, nil, w.style)
71 | x += runewidth.RuneWidth(c)
72 | }
73 | for i, c := range w.value {
74 | style := w.style
75 | if i == w.cursor {
76 | style = style.Reverse(true)
77 | }
78 | w.view.SetContent(x, 0, c, nil, style)
79 | x += runewidth.RuneWidth(c)
80 | }
81 | if w.cursor == len(w.value) {
82 | w.view.SetContent(x, 0, ' ', nil, w.style.Reverse(true))
83 | }
84 | }
85 |
86 | // SetView sets the view
87 | func (w *InputLine) SetView(view views.View) {
88 | w.view = view
89 | w.PostEventWidgetContent(w)
90 | }
91 |
92 | // Resize is called when our View changes sizes.
93 | func (w *InputLine) Resize() {
94 | w.PostEventWidgetResize(w)
95 | }
96 |
97 | // HandleEvent handles events on tcell
98 | func (w *InputLine) HandleEvent(ev tcell.Event) bool {
99 | switch ev := ev.(type) {
100 | case *tcell.EventKey:
101 | switch ev.Key() {
102 | case tcell.KeyLeft:
103 | w.SetCursorAt(w.cursor - 1)
104 | return true
105 | case tcell.KeyRight:
106 | w.SetCursorAt(w.cursor + 1)
107 | return true
108 | case tcell.KeyDelete:
109 | copy(w.value[w.cursor:], w.value[w.cursor+1:])
110 | w.value = w.value[:len(w.value)-1]
111 | return true
112 | case tcell.KeyBackspace, tcell.KeyBackspace2:
113 | if w.cursor == 0 {
114 | break
115 | }
116 | copy(w.value[w.cursor-1:], w.value[w.cursor:])
117 | w.value = w.value[:len(w.value)-1]
118 | w.cursor--
119 | return true
120 | case tcell.KeyRune:
121 | runes := make([]rune, len(w.value)+1)
122 | copy(runes, w.value[:w.cursor])
123 | copy(runes[w.cursor+1:], w.value[w.cursor:])
124 | runes[w.cursor] = ev.Rune()
125 | w.value = runes
126 | w.cursor++
127 | return true
128 | }
129 | }
130 | return false
131 | }
132 |
133 | // Size returns the width and height in vertical line.
134 | func (w *InputLine) Size() (int, int) {
135 | width1 := runewidth.StringWidth(string(w.prompt))
136 | width2 := runewidth.StringWidth(string(w.value))
137 | return width1 + width2 + 1, 1
138 | }
139 |
--------------------------------------------------------------------------------
/pkg/widgets/line.go:
--------------------------------------------------------------------------------
1 | package widgets
2 |
3 | import (
4 | "github.com/gdamore/tcell"
5 | "github.com/gdamore/tcell/views"
6 | )
7 |
8 | // VerticalLine is a Widget with containing a vertical line.
9 | type VerticalLine struct {
10 | view views.View
11 | views.WidgetWatchers
12 | char rune
13 | style tcell.Style
14 | }
15 |
16 | // NewVerticalLine creates a new VerticalLine
17 | func NewVerticalLine(char rune, style tcell.Style) *VerticalLine {
18 | return &VerticalLine{
19 | char: char,
20 | style: style,
21 | }
22 | }
23 |
24 | // Draw draws the VerticalLine
25 | func (w *VerticalLine) Draw() {
26 | if w.view == nil {
27 | return
28 | }
29 | w.view.Fill(w.char, w.style)
30 | }
31 |
32 | // Resize is called when our View changes sizes.
33 | func (w *VerticalLine) Resize() {
34 | w.PostEventWidgetResize(w)
35 | }
36 |
37 | // HandleEvent handles events on tcell
38 | func (w *VerticalLine) HandleEvent(ev tcell.Event) bool {
39 | return false
40 | }
41 |
42 | // SetView sets the View object used for the vertical line
43 | func (w *VerticalLine) SetView(view views.View) {
44 | w.view = view
45 | }
46 |
47 | // Size returns the width and height in vertical line, which always (1, 1).
48 | func (w *VerticalLine) Size() (int, int) {
49 | return 1, 1
50 | }
51 |
--------------------------------------------------------------------------------
/pkg/widgets/listview.go:
--------------------------------------------------------------------------------
1 | package widgets
2 |
3 | import (
4 | "github.com/gdamore/tcell"
5 | "github.com/gdamore/tcell/views"
6 | )
7 |
8 | type item struct {
9 | name string
10 | text *views.Text
11 | view *views.ViewPort
12 | }
13 |
14 | // ListView is a Widget with containing multiple items as a list
15 | type ListView struct {
16 | view views.View
17 | items []item
18 | selected int
19 | changed bool
20 | width int
21 | height int
22 |
23 | views.WidgetWatchers
24 | }
25 |
26 | // NewListView returns a new ListView
27 | func NewListView() *ListView {
28 | return &ListView{
29 | selected: -1,
30 | }
31 | }
32 |
33 | // AddItem adds a new item with the text and its style. The text must be
34 | // unique in the list view. It panics when the text is already exists in the
35 | // list
36 | func (w *ListView) AddItem(text string, style tcell.Style) {
37 | if w.getItemIndex(text) != -1 {
38 | panic("item " + text + " already exists")
39 | }
40 |
41 | v := &views.ViewPort{}
42 | v.SetView(w.view)
43 |
44 | t := &views.Text{}
45 | t.SetText(text)
46 | t.SetStyle(style)
47 | t.SetView(v)
48 | t.Watch(w)
49 |
50 | item := item{name: text, text: t, view: v}
51 | w.items = append(w.items, item)
52 |
53 | w.changed = true
54 | w.layout()
55 | w.PostEventWidgetContent(w)
56 | }
57 |
58 | // SetStyle updates the style of the text. It panics when the text does not
59 | // exist in the list.
60 | func (w *ListView) SetStyle(text string, style tcell.Style) {
61 | idx := w.getItemIndex(text)
62 | if idx == -1 {
63 | panic("item " + text + " not fount")
64 | }
65 |
66 | w.items[idx].text.SetStyle(style)
67 | w.changed = true
68 | w.layout()
69 | w.PostEventWidgetContent(w)
70 | }
71 |
72 | // DeleteItem deletes a item with the text. It panics when the text does not
73 | // exist in the list
74 | func (w *ListView) DeleteItem(text string) {
75 | idx := w.getItemIndex(text)
76 | if idx == -1 {
77 | panic("item " + text + " not fount")
78 | }
79 | item := w.items[idx]
80 | item.text.Unwatch(w)
81 | w.items = append(w.items[:idx], w.items[idx+1:]...)
82 | }
83 |
84 | // ItemCount returns the count of the items.
85 | func (w *ListView) ItemCount() int {
86 | return len(w.items)
87 | }
88 |
89 | func (w *ListView) getItemIndex(name string) int {
90 | for i, item := range w.items {
91 | if item.name == name {
92 | return i
93 | }
94 | }
95 | return -1
96 | }
97 |
98 | // SelectNext selects next item of the current
99 | func (w *ListView) SelectNext() {
100 | index := w.selected + 1
101 | if index >= len(w.items) {
102 | index = 0
103 | }
104 | w.SelectAt(index)
105 | }
106 |
107 | // SelectPrev selects previous item of the current
108 | func (w *ListView) SelectPrev() {
109 | index := w.selected - 1
110 | if index < 0 {
111 | index = len(w.items) - 1
112 | }
113 | w.SelectAt(index)
114 | }
115 |
116 | // SelectAt selects nth items by the index.
117 | func (w *ListView) SelectAt(index int) {
118 | if index == w.selected {
119 | return
120 | }
121 | if w.selected >= 0 {
122 | i := w.items[w.selected]
123 | i.text.SetStyle(i.text.Style().Reverse(false))
124 | }
125 | if index < 0 || index >= len(w.items) {
126 | return
127 | }
128 |
129 | w.selected = index
130 | i := w.items[index]
131 | i.text.SetStyle(i.text.Style().Reverse(true))
132 |
133 | w.PostEventWidgetContent(w)
134 |
135 | ev := &EventItemSelected{
136 | Name: i.name,
137 | Index: index,
138 | widget: w,
139 | }
140 | ev.SetEventNow()
141 |
142 | w.PostEvent(ev)
143 | }
144 |
145 | // Draw draws the ListView
146 | func (w *ListView) Draw() {
147 | if w.view == nil {
148 | return
149 | }
150 | if w.changed {
151 | w.layout()
152 | }
153 | for _, i := range w.items {
154 | i.text.Draw()
155 | }
156 | }
157 |
158 | // Resize is called when our View changes sizes.
159 | func (w *ListView) Resize() {
160 | w.layout()
161 | w.PostEventWidgetResize(w)
162 | }
163 |
164 | // HandleEvent handles events on tcell
165 | func (w *ListView) HandleEvent(ev tcell.Event) bool {
166 | switch ev.(type) {
167 | case *views.EventWidgetContent:
168 | w.changed = true
169 | w.PostEventWidgetContent(w)
170 | return true
171 | }
172 | for _, item := range w.items {
173 | if item.text.HandleEvent(ev) {
174 | return true
175 | }
176 | }
177 | return false
178 |
179 | }
180 |
181 | // SetView sets the View object used for the list view
182 | func (w *ListView) SetView(view views.View) {
183 | w.view = view
184 | for _, item := range w.items {
185 | item.view.SetView(view)
186 | }
187 | w.changed = true
188 | }
189 |
190 | // Size returns the width and height in vertical line.
191 | func (w *ListView) Size() (int, int) {
192 | return w.width, w.height
193 | }
194 |
195 | func (w *ListView) layout() {
196 | vieww, _ := w.view.Size()
197 | w.width, w.height = 0, 0
198 | for y, item := range w.items {
199 | textw, texth := item.text.Size()
200 | if textw > w.width {
201 | w.width = textw
202 | }
203 | if textw < vieww {
204 | textw = vieww
205 | }
206 | item.view.Resize(0, y, textw, texth)
207 | item.text.Resize()
208 | }
209 | w.height = len(w.items)
210 | w.changed = false
211 | }
212 |
--------------------------------------------------------------------------------
/pkg/widgets/pager.go:
--------------------------------------------------------------------------------
1 | package widgets
2 |
3 | import (
4 | "github.com/gdamore/tcell"
5 | "github.com/gdamore/tcell/views"
6 | )
7 |
8 | // Pager is a Widget with the text and its view port. It provides a scrollable
9 | // view if the content size is larger than the actual view.
10 | type Pager struct {
11 | view views.View
12 | viewport views.ViewPort
13 | text HighlightText
14 | highlight string
15 |
16 | views.WidgetWatchers
17 | }
18 |
19 | // NewPager returns a new Pager
20 | func NewPager() *Pager {
21 | w := &Pager{}
22 | w.text.SetView(&w.viewport)
23 | return w
24 | }
25 |
26 | // AppendLine adds the line into the pager
27 | func (w *Pager) AppendLine(line string) {
28 | w.text.AppendLine(line)
29 |
30 | width, height := w.text.Size()
31 | w.viewport.SetContentSize(width, height, true)
32 | w.viewport.ValidateView()
33 | }
34 |
35 | // ScrollDown scrolls down by one line on the pager.
36 | func (w *Pager) ScrollDown() {
37 | w.viewport.ScrollDown(1)
38 | }
39 |
40 | // ScrollUp scrolls up by one line on the pager
41 | func (w *Pager) ScrollUp() {
42 | w.viewport.ScrollUp(1)
43 | }
44 |
45 | // ScrollHalfPageDown scrolls down by half-height of the screen.
46 | func (w *Pager) ScrollHalfPageDown() {
47 | _, vh := w.view.Size()
48 | w.viewport.ScrollDown(vh / 2)
49 | }
50 |
51 | // ScrollHalfPageUp scrolls up by half-height of the screen.
52 | func (w *Pager) ScrollHalfPageUp() {
53 | _, vh := w.view.Size()
54 | w.viewport.ScrollUp(vh / 2)
55 | }
56 |
57 | // ScrollHalfPageLeft scrolls left by half-width of the screen
58 | func (w *Pager) ScrollHalfPageLeft() {
59 | vw, _ := w.view.Size()
60 | w.viewport.ScrollLeft(vw / 2)
61 | }
62 |
63 | // ScrollHalfPageRight scrolls right by half-width of the screen.
64 | func (w *Pager) ScrollHalfPageRight() {
65 | vw, _ := w.view.Size()
66 | w.viewport.ScrollRight(vw / 2)
67 | }
68 |
69 | // ScrollPageDown scrolls down by the height of the screen.
70 | func (w *Pager) ScrollPageDown() {
71 | _, vh := w.view.Size()
72 | w.viewport.ScrollDown(vh)
73 | }
74 |
75 | // ScrollPageUp scrolls up by the height of the screen.
76 | func (w *Pager) ScrollPageUp() {
77 | _, vh := w.view.Size()
78 | w.viewport.ScrollUp(vh)
79 | }
80 |
81 | // ScrollToTop scrolls to the top of the content
82 | func (w *Pager) ScrollToTop() {
83 | _, h := w.text.Size()
84 | w.viewport.ScrollUp(h)
85 | }
86 |
87 | // ScrollToBottom scrolls to the bottom of the content.
88 | func (w *Pager) ScrollToBottom() {
89 | _, h := w.text.Size()
90 | w.viewport.ScrollDown(h)
91 | }
92 |
93 | // GetScrollYPosition returns vertical position of the scroll on the pager.
94 | // Its range is 0.0 to 1.0.
95 | func (w *Pager) GetScrollYPosition() float64 {
96 | _, contenth := w.viewport.GetContentSize()
97 | _, viewh := w.viewport.Size()
98 | _, y, _, _ := w.viewport.GetVisible()
99 | return float64(y) / float64(contenth-viewh)
100 | }
101 |
102 | // ClearText clears current content on the pager.
103 | func (w *Pager) ClearText() {
104 | w.text.ClearText()
105 |
106 | width, height := w.text.Size()
107 | w.viewport.SetContentSize(width, height, true)
108 | w.viewport.ValidateView()
109 | }
110 |
111 | // SetKeyword sets the keyword to be highlighted in the pager
112 | func (w *Pager) SetKeyword(keyword string) {
113 | w.text.SetKeyword(keyword)
114 | w.PostEventWidgetContent(w)
115 | }
116 |
117 | // Keyword returns the current keyword in the content
118 | func (w *Pager) Keyword() string {
119 | return w.text.Keyword()
120 | }
121 |
122 | // FindNext finds next keyword in the content. It returns true if the keyword found.
123 | func (w *Pager) FindNext() bool {
124 | count := w.text.HighlightCount()
125 | if count == 0 {
126 | return false
127 | }
128 | next := w.text.CurrentHighlight() + 1
129 | if next >= count {
130 | next = 0
131 | }
132 | w.text.ActivateHighlight(next)
133 | x, y := w.text.HighlightPos(next)
134 | w.viewport.Center(x, y)
135 | w.PostEventWidgetContent(w)
136 | return true
137 | }
138 |
139 | // FindPrev finds previous keyword in the content. It returns true if the keyword found.
140 | func (w *Pager) FindPrev() bool {
141 | count := w.text.HighlightCount()
142 | if count == 0 {
143 | return false
144 | }
145 | next := w.text.CurrentHighlight() - 1
146 | if next < 0 {
147 | next = count - 1
148 | }
149 | w.text.ActivateHighlight(next)
150 | x, y := w.text.HighlightPos(next)
151 | w.viewport.Center(x, y)
152 | w.PostEventWidgetContent(w)
153 | return true
154 | }
155 |
156 | // Draw draws the Pager
157 | func (w *Pager) Draw() {
158 | if w.view == nil {
159 | return
160 | }
161 | w.text.Draw()
162 | }
163 |
164 | // Resize is called when our View changes sizes.
165 | func (w *Pager) Resize() {
166 | width, height := w.view.Size()
167 | w.viewport.Resize(0, 0, width, height)
168 | w.viewport.ValidateView()
169 | }
170 |
171 | // HandleEvent handles events on tcell.
172 | func (w *Pager) HandleEvent(ev tcell.Event) bool {
173 | return false
174 | }
175 |
176 | // SetView sets the View object used for the pager
177 | func (w *Pager) SetView(view views.View) {
178 | w.view = view
179 | w.viewport.SetView(view)
180 | if view == nil {
181 | return
182 | }
183 | w.Resize()
184 | }
185 |
186 | // Size returns the width and height in vertical line.
187 | func (w *Pager) Size() (int, int) {
188 | width, height := w.view.Size()
189 | if width > 2 {
190 | width = 2
191 | }
192 | if height > 2 {
193 | height = 2
194 | }
195 | return width, height
196 | }
197 |
--------------------------------------------------------------------------------
/pkg/widgets/tabs.go:
--------------------------------------------------------------------------------
1 | package widgets
2 |
3 | import (
4 | "github.com/gdamore/tcell"
5 | "github.com/gdamore/tcell/views"
6 | )
7 |
8 | var (
9 | styleTabActive = tcell.StyleDefault.Background(tcell.ColorSilver).Foreground(tcell.ColorWhite).Bold(true)
10 | styleTabInactive = tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)
11 | styleTabBackground = tcell.StyleDefault.Background(tcell.ColorWhite)
12 | )
13 |
14 | type tabsItem struct {
15 | name string
16 | text *views.Text
17 | }
18 |
19 | // Tabs is a View with multiple items in single line.
20 | type Tabs struct {
21 | items []tabsItem
22 | selected int
23 |
24 | views.BoxLayout
25 | views.WidgetWatchers
26 | }
27 |
28 | // NewTabs returns a new Tabs.
29 | func NewTabs() *Tabs {
30 | w := &Tabs{
31 | selected: -1,
32 | }
33 | w.SetStyle(styleTabBackground)
34 | w.SetOrientation(views.Horizontal)
35 | return w
36 | }
37 |
38 | // AddTab adds a new item with the name.
39 | func (w *Tabs) AddTab(name string) {
40 | text := &views.Text{}
41 | text.SetText(" " + name + " ")
42 | text.SetStyle(styleTabInactive)
43 |
44 | w.AddWidget(text, 0)
45 | w.items = append(w.items, tabsItem{
46 | name: name,
47 | text: text,
48 | })
49 | }
50 |
51 | // TabCount returns the count of the tabs.
52 | func (w *Tabs) TabCount() int {
53 | return len(w.items)
54 | }
55 |
56 | // SelectNext selects next tab of the current
57 | func (w *Tabs) SelectNext() {
58 | index := w.selected + 1
59 | if index >= len(w.items) {
60 | index = 0
61 | }
62 | w.SelectAt(index)
63 | }
64 |
65 | // SelectPrev selects previous tab of the current
66 | func (w *Tabs) SelectPrev() {
67 | index := w.selected + 1
68 | if index >= len(w.items) {
69 | index = 0
70 | }
71 | w.SelectAt(index)
72 | }
73 |
74 | // Clear clears current tabs.
75 | func (w *Tabs) Clear() {
76 | for _, item := range w.items {
77 | w.RemoveWidget(item.text)
78 | }
79 | w.items = nil
80 | w.selected = -1
81 | }
82 |
83 | // SelectAt selects the tab of the index in items.
84 | func (w *Tabs) SelectAt(index int) {
85 | if index == w.selected {
86 | return
87 | }
88 | if w.selected >= 0 {
89 | item := w.items[w.selected]
90 | item.text.SetStyle(styleTabInactive)
91 | }
92 | if index < 0 || index >= len(w.items) {
93 | return
94 | }
95 | w.selected = index
96 | item := w.items[w.selected]
97 | item.text.SetStyle(styleTabActive)
98 |
99 | w.PostEventWidgetContent(w)
100 |
101 | ev := &EventItemSelected{
102 | Name: item.name,
103 | Index: index,
104 | widget: w,
105 | }
106 | ev.SetEventNow()
107 |
108 | w.PostEvent(ev)
109 | }
110 |
--------------------------------------------------------------------------------
/screenshot.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ueokande/logbook/fc35525cda8c020eed93c984c269f65951e80033/screenshot.gif
--------------------------------------------------------------------------------
/scripts/deploy/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/ueokande/logbook/scropts/deploy
2 |
3 | go 1.12
4 |
5 | require (
6 | github.com/google/go-github/v27 v27.0.1
7 | github.com/pkg/errors v0.8.1
8 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
9 | )
10 |
--------------------------------------------------------------------------------
/scripts/deploy/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
3 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
4 | github.com/google/go-github/v27 v27.0.1 h1:sSMFSShNn4VnqCqs+qhab6TS3uQc+uVR6TD1bW6MavM=
5 | github.com/google/go-github/v27 v27.0.1/go.mod h1:/0Gr8pJ55COkmv+S/yPKCczSkUPIM/LnFyubufRNIS0=
6 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
7 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
8 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
9 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
10 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
11 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
12 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
13 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
14 | golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
15 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
16 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
17 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
18 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
19 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
20 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI=
21 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
22 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
23 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
24 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
25 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
26 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
27 |
--------------------------------------------------------------------------------
/scripts/deploy/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "fmt"
7 | "net/http"
8 | "os"
9 | "path/filepath"
10 |
11 | "github.com/google/go-github/v27/github"
12 | "github.com/pkg/errors"
13 | "golang.org/x/oauth2"
14 | )
15 |
16 | var (
17 | githubToken = os.Getenv("GITHUB_TOKEN")
18 | )
19 |
20 | func strptr(s string) *string { return &s }
21 |
22 | func isNotFoundError(err error) bool {
23 | if err == nil {
24 | return false
25 | }
26 | rerr, ok := err.(*github.ErrorResponse)
27 | if !ok {
28 | return false
29 | }
30 | return rerr.Response.StatusCode == http.StatusNotFound
31 | }
32 |
33 | func deploy(tag, path string) error {
34 | if len(githubToken) == 0 {
35 | return errors.New("GITHUB_TOKEN not set")
36 | }
37 | f, err := os.Open(path)
38 | if err != nil {
39 | errors.Wrap(err, "failed to open "+path)
40 | }
41 | defer f.Close()
42 |
43 | ctx := context.Background()
44 | ts := oauth2.StaticTokenSource(
45 | &oauth2.Token{AccessToken: githubToken},
46 | )
47 | tc := oauth2.NewClient(ctx, ts)
48 |
49 | client := github.NewClient(tc)
50 | release, _, err := client.Repositories.GetReleaseByTag(ctx, "ueokande", "logbook", tag)
51 | if isNotFoundError(err) {
52 | release, _, err = client.Repositories.CreateRelease(ctx, "ueokande", "logbook", &github.RepositoryRelease{
53 | TagName: strptr(tag),
54 | Name: strptr("Release " + tag),
55 | })
56 | if err != nil {
57 | return errors.Wrap(err, "failed to create a release for "+tag)
58 | }
59 | fmt.Fprintln(os.Stderr, "Created release on", release.GetHTMLURL())
60 | } else if err != nil {
61 | return err
62 | }
63 |
64 | opt := &github.UploadOptions{
65 | Name: filepath.Base(path),
66 | }
67 | asset, _, err := client.Repositories.UploadReleaseAsset(ctx, "ueokande", "logbook", release.GetID(), opt, f)
68 | if err != nil {
69 | return errors.Wrap(err, "failed to upload assets "+opt.Name)
70 | }
71 | fmt.Fprintln(os.Stderr, "Uploaded on", asset.GetBrowserDownloadURL())
72 | return nil
73 | }
74 |
75 | func main() {
76 | flag.Parse()
77 | flag.Usage = func() {
78 | fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s TAG ASSET_FILE\n", os.Args[0])
79 | }
80 | if flag.NArg() < 2 {
81 | flag.Usage()
82 | os.Exit(2)
83 | }
84 | err := deploy(flag.Arg(0), flag.Arg(1))
85 | if err != nil {
86 | fmt.Fprintf(os.Stderr, "%s: %+v\n", os.Args[0], err)
87 | os.Exit(1)
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/worker.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "sync"
6 | )
7 |
8 | // Worker is a worker for asynchronous jobs
9 | type Worker struct {
10 | ctx context.Context
11 | wg sync.WaitGroup
12 | cancel context.CancelFunc
13 | err error
14 | }
15 |
16 | // NewWorker creates a new Worker with the ctx
17 | func NewWorker(ctx context.Context) *Worker {
18 | return &Worker{
19 | ctx: ctx,
20 | }
21 | }
22 |
23 | // Start starts a background job presented by f
24 | func (w *Worker) Start(f func(ctx context.Context) error) {
25 | if w.cancel != nil {
26 | panic("worker is already started")
27 | }
28 |
29 | ctx, cancel := context.WithCancel(w.ctx)
30 | w.cancel = cancel
31 |
32 | w.wg.Add(1)
33 | go func() {
34 | w.err = f(ctx)
35 | w.wg.Done()
36 | }()
37 | }
38 |
39 | // Stop stops current asynchronous jobs and returns last error occurs in the job
40 | func (w *Worker) Stop() error {
41 | if w.cancel == nil {
42 | return nil
43 | }
44 | w.cancel()
45 | w.cancel = nil
46 |
47 | w.wg.Wait()
48 | return w.err
49 | }
50 |
--------------------------------------------------------------------------------
/worker_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "testing"
7 | )
8 |
9 | func TestWorker(t *testing.T) {
10 | myerr := errors.New("test error")
11 |
12 | w := NewWorker(context.Background())
13 | w.Start(func(ctx context.Context) error {
14 | <-ctx.Done()
15 | return myerr
16 | })
17 |
18 | err := w.Stop()
19 | if err != myerr {
20 | t.Errorf("%v != %v", err, myerr)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------