├── .all-contributorsrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── go.yml ├── .gitignore ├── .goreleaser.yml ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── gen.go ├── statik │ └── statik.go └── themes │ ├── base16-256.toml │ ├── default-16.toml │ ├── default-256.toml │ ├── default-8.toml │ ├── dracula-256.toml │ └── solarized-256.toml ├── cmd └── termshark │ └── termshark.go ├── configs ├── profiles │ └── profiles.go └── termshark-dd01307f2423.json.enc ├── docs ├── FAQ.md ├── Maintainer.md ├── Packages.md └── UserGuide.md ├── go.mod ├── go.sum ├── pkg ├── capinfo │ └── loader.go ├── cli │ ├── all.go │ ├── flags.go │ ├── flags_windows.go │ └── tristate.go ├── confwatcher │ └── confwatcher.go ├── convs │ ├── loader.go │ └── types.go ├── fields │ ├── fields.go │ └── fields_test.go ├── format │ ├── hexdump.go │ ├── hexdump_test.go │ └── printable.go ├── noroot │ └── noroot.go ├── pcap │ ├── cmds.go │ ├── cmds_unix.go │ ├── cmds_windows.go │ ├── handlers.go │ ├── loader.go │ ├── loader_tshark_test.go │ ├── pdml.go │ ├── source.go │ ├── testdata │ │ ├── 1.hexdump │ │ ├── 1.pcap │ │ ├── 1.pdml │ │ ├── 1.psml │ │ ├── 2.hexdump-body │ │ ├── 2.hexdump-footer │ │ ├── 2.hexdump-header │ │ ├── 2.pcap-body │ │ ├── 2.pcap-footer │ │ ├── 2.pcap-header │ │ ├── 2.pdml-body │ │ ├── 2.pdml-footer │ │ ├── 2.pdml-header │ │ ├── 2.psml-body │ │ ├── 2.psml-footer │ │ └── 2.psml-header │ └── utils.go ├── pdmltree │ ├── pdmltree.go │ └── pdmltree_test.go ├── psmlmodel │ └── model.go ├── shark │ ├── columnformat.go │ ├── columnformat_test.go │ └── wiresharkcfg │ │ ├── cfg.go │ │ ├── parser.go │ │ ├── parser.peg │ │ └── parser_test.go ├── streams │ ├── follow.go │ ├── follow.peg │ ├── follow_test.go │ ├── loader.go │ ├── loader_test.go │ └── parse.go ├── summary │ └── summary.go ├── system │ ├── dumpcapext.go │ ├── dumpcapext_arm64.go │ ├── dumpcapext_darwin.go │ ├── dumpcapext_windows.go │ ├── dup.go │ ├── dup_linux_arm64.go │ ├── dup_linux_riscv64.go │ ├── errors.go │ ├── extcmds.go │ ├── extcmds_android.go │ ├── extcmds_darwin.go │ ├── extcmds_windows.go │ ├── fd.go │ ├── fd_windows.go │ ├── fdinfo.go │ ├── have_fdinfo.go │ ├── have_fdinfo_linux.go │ ├── picker.go │ ├── picker_android.go │ ├── signals.go │ └── signals_windows.go ├── tailfile │ ├── tailfile.go │ └── tailfile_windows.go ├── theme │ ├── modeswap │ │ └── modeswap.go │ └── utils.go └── tty │ ├── tty.go │ └── tty_windows.go ├── scripts ├── do-release.sh ├── pcaps │ └── telnet-cooked.pcap └── simple-tests.sh ├── ui ├── capinfoui.go ├── convscallbacks.go ├── convsui.go ├── convsui_test.go ├── darkmode.go ├── dialog.go ├── filterconvs.go ├── lastline.go ├── logsui.go ├── logsui_windows.go ├── menu.go ├── menuutil │ └── menu.go ├── messages.go ├── newprofile.go ├── palette.go ├── prochandlers.go ├── psmlcols.go ├── psmlcolsmodel.go ├── searchalg.go ├── searchbyfilter.go ├── searchcommon.go ├── searchpktbytes.go ├── searchpktlist.go ├── searchpktstruct.go ├── streamui.go ├── switchterm.go ├── tableutil │ └── tableutil.go ├── ui.go └── wormhole.go ├── utils.go ├── utils_test.go ├── version.go └── widgets ├── appkeys └── appkeys.go ├── copymodetable └── copymodetable.go ├── copymodetree └── copymodetree.go ├── enableselected └── enableselected.go ├── expander └── expander.go ├── fileviewer └── fileviewer.go ├── filter └── filter.go ├── framefocus └── framefocus.go ├── hexdumper ├── hexdumper.go └── hexdumper_test.go ├── hexdumper2 └── hexdumper2.go ├── ifwidget └── ifwidget.go ├── keepselected └── keepselected.go ├── mapkeys └── mapkeys.go ├── minibuffer └── minibuffer.go ├── number ├── number.go └── number_test.go ├── regexstyle └── regexstyle.go ├── renderfocused └── renderfocused.go ├── resizable ├── resizable.go └── resizable_test.go ├── rossshark └── rossshark.go ├── scrollabletable └── scrollabletable.go ├── scrollabletext └── scrollabletext.go ├── search └── search.go ├── streamwidget └── streamwidget.go ├── trackfocus ├── trackfocus.go └── trackfocus_test.go ├── utils.go ├── withscrollbar ├── withscrollbar.go └── withscrollbar_test.go └── wormhole └── wormhole.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Prerequisites 11 | 12 | Please verify these before submitting an issue. 13 | 14 | - [ ] I am running the latest versions of Termshark and Wireshark. 15 | - [ ] I checked the [README](https://github.com/gcla/termshark) and [User Guide](https://github.com/gcla/termshark/blob/master/docs/UserGuide.md) and found no answer 16 | - [ ] I searched [issues](https://github.com/gcla/termshark/issues?q=is%3Aissue) and this has not yet been filed 17 | 18 | ## Problem 19 | 20 | ### Current Behavior 21 | 22 | Please describe the behavior you are seeing. 23 | 24 | ### Expected Behavior 25 | 26 | Please describe the behavior you are expecting. 27 | 28 | ### Screenshots as applicable 29 | 30 | * Provide screenshots of wireshark/tshark if that's what the expectation is based on. 31 | * Provide screenshots of the problem state. 32 | 33 | ### Steps to Reproduce 34 | 35 | 1. Go to '...' 36 | 2. Click on '....' 37 | 3. Scroll down to '....' 38 | 4. See error 39 | 40 | ## Context 41 | 42 | Please provide the complete output of these commands: 43 | 44 | * termshark -v (or termshark -vv if running from git/HEAD) 45 | * termshark -v | cat 46 | 47 | Please also provide any relevant information about your environment (OS, VM, pi,...) 48 | 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Is your feature request related to a problem? Please describe. 11 | 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | 14 | ### Describe the solution you'd like 15 | A clear and concise description of what you want to happen. 16 | 17 | ### Describe alternatives you've considered 18 | A clear and concise description of any alternative solutions or features you've considered. 19 | 20 | ### Additional context 21 | Add any other context or screenshots about the feature request here. 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | 5 | build: 6 | name: Build 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | - name: Set up Go 1.19 11 | uses: actions/setup-go@v3 12 | with: 13 | go-version: '>=1.19.2' 14 | id: go 15 | 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v3 18 | 19 | - name: Get dependencies 20 | run: | 21 | go get -v -t -d ./... 22 | 23 | - name: Build 24 | run: go build -v ./... 25 | 26 | - name: Install tshark as prerequisite for testing 27 | run: sudo sh -c 'export DEBIAN_FRONTEND=noninteractive ; apt -y update && apt -y install tshark' 28 | 29 | - name: Test 30 | run: go test -v ./... 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Makefile 2 | dist/ 3 | .vscode/ 4 | *~ 5 | 6 | /cmd/termshark/termshark 7 | /typescript 8 | /go.work 9 | /go.work.sum 10 | 11 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example goreleaser.yaml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | before: 4 | hooks: 5 | builds: 6 | - env: 7 | - CGO_ENABLED=0 8 | - GO111MODULE=on 9 | main: ./cmd/termshark/termshark.go 10 | goos: 11 | - freebsd 12 | - netbsd 13 | - openbsd 14 | - windows 15 | - linux 16 | - darwin 17 | goarch: 18 | - arm 19 | - arm64 20 | - amd64 21 | ignore: 22 | - goos: darwin 23 | goarch: arm 24 | - goos: freebsd 25 | goarch: arm 26 | - goos: netbsd 27 | goarch: arm 28 | - goos: openbsd 29 | goarch: arm 30 | - goos: windows 31 | goarch: arm 32 | - goos: freebsd 33 | goarch: arm64 34 | - goos: netbsd 35 | goarch: arm64 36 | - goos: openbsd 37 | goarch: arm64 38 | - goos: windows 39 | goarch: arm64 40 | archives: 41 | - replacements: 42 | darwin: macOS 43 | linux: linux 44 | windows: windows 45 | amd64: x64 46 | wrap_in_directory: true 47 | format_overrides: 48 | - goos: windows 49 | format: zip 50 | files: 51 | - none* 52 | signs: 53 | - artifacts: checksum 54 | checksum: 55 | name_template: 'checksums.txt' 56 | snapshot: 57 | name_template: "{{ .Env.TERMSHARK_GIT_DESCRIBE }}" 58 | changelog: 59 | sort: asc 60 | filters: 61 | exclude: 62 | - '^docs:' 63 | - '^test:' 64 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | env: 3 | global: 4 | - GO111MODULE=on 5 | - GOOGLE_APPLICATION_CREDENTIALS=/tmp/google.json 6 | - secure: I+3P2j2pxXjaKBAemdC/NN8SozyG4BEcMXJqUPY4jmiJO6aVXHkEQUMHzk1hCzNNo2QA2ACz0ptSUKx9AIXlQuGmLPsbNGkCaLklT8+lLgwISbM+BGs3w8Bz777czbD1c2zHjb3k/90fgo0j+96Y+qpJOo3GAEBsz8Q47GQEZUFM05DPzd2Opj6fXkZBY0qaG5cXHF8UisgpYL4E6iYnXnPS7O3jPnyL28QnzYV9zsZGfPGWTSo0vOaO2l5gmPNZ33w+fnuN9pLayh6L8lLDYlwzAyzMUx9HTgp1qLknSzPmhSIqn1OebKVX5UiTWlfI/5mJj2TI0E/S+sFNTlkNR3r1qoNs09m7zRijxkQVKNj/Pzx5fbT5yYB6g7kPuD6Ag1NQb3TPocxOk/m4OHoU7m1kSl7IKr9LmNIGwEAzwIOPwxoHBXsSiZo4Zu4uD0pl6Jse7NTXCc5sPZagEwac70TZ0aPwuOwQDnwPU8PUUfDQwoYtuXkY8uv/lBki0y1JV8vVTNZoel6ZwJuJPB6IXn4ctI7c+MtX1eIg0zppkjaqnpi3eKbtpFGlbOAtquChd4LEr1O+Q5IJyPg7DFVfscH41owWR9T3sYXSpjVU8XKsAfjazS4Xx/brFItlvLjRQHdpZzng23EWBeYP9nCM5X5UZ2xEnrDJXefhrsHqWLw= 7 | - secure: FLDaxfvg2nwSPo045nwSINI/84MgGwlku/ZgkvVbDQUNiIUc305UhaNCPuxq503Y2OJdw39wN3kkLQd08g4vYHXygBYTEr7k63D1Dq+0GltItTGPpMmhW2LCZFxMZ6QLVpLf/LGYY9DviT+zIXfDJekgvCcxT9cXTWxaKNAxENnvbUu/arnNg0liL9zyhoCIMiEJmJ/Pybq9MkL8mv+4i6DpzWTh6vGsO2OXfN52QQ/y4TqnJYsOfsRzKX30AzX+OvONUOhwt4j5AbQWn1VTFHWUz++GhY/LerJxOXYea2GqaY0NZ0+cvpUaMRpeAENJA778IQvRojMZnWgyRm7RAScHkJ0dT8CTuCkIXn/XN/r+bmTBdoB9C9DuDovo2Y86HgOeM1ZIRGjgNAja6oBJ7xi94m8TlsjoQuPl4eilB8Y5dX+tkUGTOZ90zWaP5xViAMIZpTiMS8ZWnHOGWB3M4EfmDDiaX6SOEcT7QFQMS/kj0RtBSZlXYcXEfvZnduW2eykfYeOVoDZoBZOiB93yVfhg5NrOeaKPe3XiiqlTygNUNRllO1SaYB317EQwSPAuFG1X2ocS0uHvVNT/DvoHDTmztEvJBB3aI+9jZbj1ZzVFtD525MaVGHCvXpCYjfKkhq3eMTsdTFOH2QlW2cf1UWhMPQyvMABr4C0Ro0ep/Kk= 8 | branches: 9 | only: 10 | - gh-pages 11 | - "/.*/" 12 | git: 13 | depth: false 14 | go: 15 | - 1.19.x 16 | - 1.18.x 17 | - 1.17.x 18 | - 1.16.x 19 | - 1.15.x 20 | - 1.14.x 21 | notifications: 22 | email: true 23 | before_install: 24 | - openssl aes-256-cbc -K $encrypted_1286cb654632_key -iv $encrypted_1286cb654632_iv 25 | -in configs/termshark-dd01307f2423.json.enc -out /tmp/google.json -d 26 | - sudo apt-get install -y tshark socat coreutils 27 | addons: 28 | apt: 29 | update: true 30 | script: 31 | - go test -v ./... 32 | - bash scripts/simple-tests.sh 33 | deploy: 34 | - provider: script 35 | skip_cleanup: true 36 | script: bash scripts/do-release.sh 37 | on: 38 | all_branches: true 39 | condition: "$TRAVIS_OS_NAME = linux && $TRAVIS_GO_VERSION = 1.19.x" 40 | - provider: gcs 41 | skip_cleanup: true 42 | access_key_id: GOOGKKO2OYB5BE3XDNBJMDRK 43 | secret_access_key: 44 | secure: fKoBQYUVQLh5gZB/LE3jmv9atfuOh1oM3YZ+9nIvv39771WbI9nfcxpO9/LZldoWludUeamR8fGz8rqzP19g6aZK+tTAvBG8XZnbEIi7lCtrLmBY6FB7t9VkcA5oPyAd+ygnUF4BRh92gqGbYx4vdJdjPUSruiIq8HTw1eSdLCIjl2h+Rk4KSpmnPmZk1YtWI9TDkBG8dRLnIq16Rwb0ep5oeK1Omlvqr5PiuwUL3gJFfE7KWznjs2hv52yAXQY6J8vWyiifBnQI7wq3JvbcV0PkJjQ7oyCsIx56R4dMl+UjFynmA7PpOiZqCj+Rp4EcBmGnh1ofS7U9hDfvLiy9jbHNwPUBiYQj2aD/fykMmXvAUwqzvjxCqx1Ky/QweBcVUbs2jOwiCofl4gSrPYdB2dkcZ8I0x9BhhbByOIEN42MNdYGucaqAia8yP8fDgfofi7H3XW3dZCySFbpb2n2poskpDFqEiJdubtm4b15YxV3gKn0NppJVojlMinEFk0jQh0BIVSY6/30rygamhbTps7JR2rNRo/82QIStZ00ME8CgFBQuEU0tOvyX3ifJAsTJPy3g4/0p3SGLrc0iV9CW3ieboA0vXJQ7DkR+yAyKhvz0QoAZs3QiAxfJLFvKxe2L/UGlKqisRghW7WRiZn8AzQNuD0jhi63SqimA2q/HFe8= 45 | bucket: termshark 46 | acl: public-read 47 | local-dir: dist 48 | upload-dir: "$TRAVIS_COMMIT" 49 | on: 50 | repo: gcla/termshark 51 | all_branches: true 52 | condition: "$TRAVIS_OS_NAME = linux && $TRAVIS_GO_VERSION = 1.19.x" 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Graham Clark 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 | 23 | -------------------------------------------------------------------------------- /assets/gen.go: -------------------------------------------------------------------------------- 1 | //go:generate statik -src=. -f 2 | 3 | package assets 4 | -------------------------------------------------------------------------------- /assets/themes/base16-256.toml: -------------------------------------------------------------------------------- 1 | 2 | unused = "#79e11a" 3 | 4 | [base16] 5 | black = "Color00" 6 | base01 = "Color12" 7 | base02 = "Color13" 8 | base03 = "Color08" 9 | base04 = "Color14" 10 | base05 = "Color07" 11 | base06 = "Color15" 12 | white = "Color0f" 13 | red = "Color09" 14 | orange = "Color10" 15 | yellow = "Color0b" 16 | green = "Color0a" 17 | cyan = "Color0e" 18 | blue = "Color04" 19 | purple = "Color05" 20 | brown = "Color11" 21 | 22 | [dark] 23 | button = ["base16.black","base16.base04"] 24 | button-focus = ["base16.white","base16.purple"] 25 | button-selected = ["base16.white","base16.base04"] 26 | cmdline = ["base16.black","base16.yellow"] 27 | cmdline-button = ["base16.yellow","base16.black"] 28 | cmdline-border = ["base16.black","base16.yellow"] 29 | copy-mode = ["base16.black","base16.yellow"] 30 | copy-mode-alt = ["base16.yellow","base16.black"] 31 | copy-mode-label = ["base16.white","base16.red"] 32 | current-capture = ["base16.white","themes.unused"] 33 | dialog = ["base16.black","base16.yellow"] 34 | dialog-button = ["base16.yellow","base16.black"] 35 | default = ["base16.white","base16.black"] 36 | filter-empty = ["base16.black","base16.cyan"] 37 | filter-intermediate = ["base16.black","base16.orange"] 38 | filter-invalid = ["base16.black","base16.red"] 39 | filter-menu = ["base16.white","base16.black"] 40 | filter-valid = ["base16.black","base16.green"] 41 | hex-byte-selected = ["base16.white","base16.purple"] 42 | hex-byte-unselected = ["base16.white","base16.black"] 43 | hex-field-selected = ["base16.black","base16.cyan"] 44 | hex-field-unselected = ["base16.black","base16.white"] 45 | hex-interval-selected = ["base16.white","base16.base03"] 46 | hex-interval-unselected = ["base16.white","base16.base02"] 47 | hex-layer-selected = ["base16.white","base16.base03"] 48 | hex-layer-unselected = ["base16.white","base16.base02"] 49 | packet-list-cell-focus = ["base16.white","base16.purple"] 50 | packet-list-cell-selected = ["base16.white","base16.base03"] 51 | packet-list-row-focus = ["base16.white","base16.cyan"] 52 | packet-list-row-selected = ["base16.white","base16.base02"] 53 | packet-struct-focus = ["base16.white","base16.cyan"] 54 | packet-struct-selected = ["base16.black","base16.base03"] 55 | progress-complete = ["base16.white","base16.purple"] 56 | progress-default = ["base16.white","base16.black"] 57 | progress-spinner = ["base16.yellow","base16.purple"] 58 | spinner = ["base16.yellow","base16.black"] 59 | stream-client = ["base16.black","base16.red"] 60 | stream-match = ["base16.black","base16.yellow"] 61 | stream-search = ["base16.black","base16.white"] 62 | stream-server = ["base16.black","base16.blue"] 63 | title = ["base16.red","unused"] 64 | 65 | [light] 66 | button = ["base16.black","base16.white"] 67 | button-focus = ["base16.black","base16.purple"] 68 | button-selected = ["base16.black","base16.base04"] 69 | cmdline = ["base16.black","base16.yellow"] 70 | cmdline-button = ["base16.yellow","base16.black"] 71 | cmdline-border = ["base16.black","base16.yellow"] 72 | copy-mode = ["base16.white","base16.yellow"] 73 | copy-mode-alt = ["base16.yellow","base16.white"] 74 | copy-mode-label = ["base16.black","base16.red"] 75 | current-capture = ["base16.black","unused"] 76 | dialog = ["base16.black","base16.yellow"] 77 | dialog-button = ["base16.yellow","base16.black"] 78 | default = ["base16.black","base16.white"] 79 | filter-empty = ["base16.black","base16.cyan"] 80 | filter-intermediate = ["base16.black","base16.orange"] 81 | filter-invalid = ["base16.black","base16.red"] 82 | filter-menu = ["base16.black","base16.white"] 83 | filter-valid = ["base16.black","base16.green"] 84 | hex-byte-selected = ["base16.white","base16.purple"] 85 | hex-byte-unselected = ["base16.black","base16.white"] 86 | hex-field-selected = ["base16.black","base16.cyan"] 87 | hex-field-unselected = ["base16.black","base16.base03"] 88 | hex-interval-selected = ["base16.white","base16.base03"] 89 | hex-interval-unselected = ["base16.black","base16.base05"] 90 | hex-layer-selected = ["base16.white","base16.base03"] 91 | hex-layer-unselected = ["base16.black","base16.base05"] 92 | packet-list-cell-focus = ["base16.white","base16.purple"] 93 | packet-list-cell-selected = ["base16.black","base16.base04"] 94 | packet-list-row-focus = ["base16.white","base16.cyan"] 95 | packet-list-row-selected = ["base16.black","base16.base05"] 96 | packet-struct-focus = ["base16.white","base16.cyan"] 97 | packet-struct-selected = ["base16.black","base16.base05"] 98 | progress-complete = ["base16.white","base16.purple"] 99 | progress-default = ["base16.white","base16.black"] 100 | progress-spinner = ["base16.yellow","base16.black"] 101 | spinner = ["base16.yellow","base16.white"] 102 | stream-client = ["base16.white","base16.red"] 103 | stream-match = ["base16.white","base16.yellow"] 104 | stream-search = ["base16.white","base16.black"] 105 | stream-server = ["base16.white","base16.blue"] 106 | title = ["base16.red","unused"] 107 | -------------------------------------------------------------------------------- /assets/themes/default-16.toml: -------------------------------------------------------------------------------- 1 | 2 | unused = "Color00" 3 | 4 | [default] 5 | black = "Color00" 6 | gray1 = "Color08" 7 | gray2 = "Color08" 8 | gray3 = "Color08" 9 | gray4 = "Color08" 10 | white = "Color0f" 11 | red = "Color09" 12 | yellow = "Color0b" 13 | green = "Color0a" 14 | cyan = "Color0e" 15 | blue = "Color04" 16 | purple = "Color05" 17 | 18 | [dark] 19 | button = ["default.black","default.gray1"] 20 | button-focus = ["default.white","default.purple"] 21 | button-selected = ["default.white","default.black"] 22 | cmdline = ["default.black","default.yellow"] 23 | cmdline-button = ["default.yellow","default.black"] 24 | cmdline-border = ["default.black","default.yellow"] 25 | copy-mode = ["default.black","default.yellow"] 26 | copy-mode-alt = ["default.yellow","default.black"] 27 | copy-mode-label = ["default.white","default.red"] 28 | current-capture = ["default.white","themes.unused"] 29 | dialog = ["default.black","default.yellow"] 30 | dialog-button = ["default.yellow","default.black"] 31 | default = ["default.white","default.black"] 32 | filter-empty = ["default.black","default.cyan"] 33 | filter-intermediate = ["default.black","default.yellow"] 34 | filter-invalid = ["default.black","default.red"] 35 | filter-menu = ["default.white","default.black"] 36 | filter-valid = ["default.black","default.green"] 37 | hex-byte-selected = ["default.black","default.purple"] 38 | hex-byte-unselected = ["default.white","default.black"] 39 | hex-field-selected = ["default.black","default.cyan"] 40 | hex-field-unselected = ["default.black","default.white"] 41 | hex-interval-selected = ["default.white","default.gray2"] 42 | hex-interval-unselected = ["default.white","default.gray1"] 43 | hex-layer-selected = ["default.white","default.gray2"] 44 | hex-layer-unselected = ["default.white","default.gray1"] 45 | packet-list-cell-focus = ["default.black","default.purple"] 46 | packet-list-cell-selected = ["default.white","default.gray2"] 47 | packet-list-row-focus = ["default.gray2","default.cyan"] 48 | packet-list-row-selected = ["default.white","default.gray1"] 49 | packet-struct-focus = ["default.black","default.cyan"] 50 | packet-struct-selected = ["default.black","default.gray2"] 51 | progress-complete = ["default.white","default.purple"] 52 | progress-default = ["default.white","default.black"] 53 | progress-spinner = ["default.yellow","default.purple"] 54 | spinner = ["default.yellow","default.black"] 55 | stream-client = ["default.black","default.red"] 56 | stream-match = ["default.black","default.yellow"] 57 | stream-search = ["default.black","default.white"] 58 | stream-server = ["default.cyan","default.blue"] 59 | title = ["default.red","unused"] 60 | 61 | [light] 62 | button = ["default.white","default.gray3"] 63 | button-focus = ["default.white","default.purple"] 64 | button-selected = ["default.black","default.gray3"] 65 | cmdline = ["default.black","default.yellow"] 66 | cmdline-button = ["default.yellow","default.black"] 67 | cmdline-border = ["default.black","default.yellow"] 68 | copy-mode = ["default.white","default.yellow"] 69 | copy-mode-alt = ["default.yellow","default.white"] 70 | copy-mode-label = ["default.black","default.red"] 71 | current-capture = ["default.black","unused"] 72 | dialog = ["default.black","default.yellow"] 73 | dialog-button = ["default.yellow","default.black"] 74 | default = ["default.black","default.white"] 75 | filter-empty = ["default.black","default.cyan"] 76 | filter-intermediate = ["default.black","default.yellow"] 77 | filter-invalid = ["default.black","default.red"] 78 | filter-menu = ["default.black","default.white"] 79 | filter-valid = ["default.black","default.green"] 80 | hex-byte-selected = ["default.black","default.purple"] 81 | hex-byte-unselected = ["default.black","default.white"] 82 | hex-field-selected = ["default.black","default.cyan"] 83 | hex-field-unselected = ["default.black","default.gray2"] 84 | hex-interval-selected = ["default.white","default.gray2"] 85 | hex-interval-unselected = ["default.black","default.gray4"] 86 | hex-layer-selected = ["default.white","default.gray2"] 87 | hex-layer-unselected = ["default.black","default.gray4"] 88 | packet-list-cell-focus = ["default.black","default.purple"] 89 | packet-list-cell-selected = ["default.black","default.gray3"] 90 | packet-list-row-focus = ["default.black","default.cyan"] 91 | packet-list-row-selected = ["default.black","default.gray4"] 92 | packet-struct-focus = ["default.black","default.cyan"] 93 | packet-struct-selected = ["default.black","default.gray4"] 94 | progress-complete = ["default.white","default.purple"] 95 | progress-default = ["default.white","default.black"] 96 | progress-spinner = ["default.yellow","default.black"] 97 | spinner = ["default.yellow","default.white"] 98 | stream-client = ["default.white","default.red"] 99 | stream-match = ["default.white","default.yellow"] 100 | stream-search = ["default.white","default.black"] 101 | stream-server = ["default.cyan","default.blue"] 102 | title = ["default.red","unused"] 103 | -------------------------------------------------------------------------------- /assets/themes/default-8.toml: -------------------------------------------------------------------------------- 1 | 2 | unused = "Color00" 3 | 4 | [default] 5 | black = "Color00" 6 | white = "Color07" 7 | red = "Color01" 8 | yellow = "Color03" 9 | green = "Color02" 10 | cyan = "Color06" 11 | blue = "Color04" 12 | purple = "Color05" 13 | 14 | [dark] 15 | button = ["default.white","default.black"] 16 | button-focus = ["default.white","default.purple"] 17 | button-selected = ["default.white","default.black"] 18 | cmdline = ["default.black","default.yellow"] 19 | cmdline-button = ["default.yellow","default.black"] 20 | cmdline-border = ["default.black","default.yellow"] 21 | copy-mode = ["default.black","default.yellow"] 22 | copy-mode-alt = ["default.yellow","default.black"] 23 | copy-mode-label = ["default.white","default.red"] 24 | current-capture = ["default.white","themes.unused"] 25 | dialog = ["default.black","default.yellow"] 26 | dialog-button = ["default.yellow","default.black"] 27 | default = ["default.white","default.black"] 28 | filter-empty = ["default.black","default.cyan"] 29 | filter-intermediate = ["default.black","default.yellow"] 30 | filter-invalid = ["default.black","default.red"] 31 | filter-menu = ["default.white","default.black"] 32 | filter-valid = ["default.black","default.green"] 33 | hex-byte-selected = ["default.black","default.purple"] 34 | hex-byte-unselected = ["default.black","default.purple"] 35 | hex-field-selected = ["default.black","default.cyan"] 36 | hex-field-unselected = ["default.white","default.black"] 37 | hex-interval-selected = ["default.black","default.white"] 38 | hex-interval-unselected = ["default.black","default.white"] 39 | hex-layer-selected = ["default.black","default.white"] 40 | hex-layer-unselected = ["default.black","default.white"] 41 | packet-list-cell-focus = ["default.black","default.purple"] 42 | packet-list-cell-selected = ["default.white","default.black"] 43 | packet-list-row-focus = ["default.black","default.cyan"] 44 | packet-list-row-selected = ["default.white","default.black"] 45 | packet-struct-focus = ["default.black","default.cyan"] 46 | packet-struct-selected = ["default.black","default.white"] 47 | progress-complete = ["default.white","default.purple"] 48 | progress-default = ["default.white","default.black"] 49 | progress-spinner = ["default.yellow","default.purple"] 50 | spinner = ["default.yellow","default.black"] 51 | stream-client = ["default.black","default.red"] 52 | stream-match = ["default.black","default.yellow"] 53 | stream-search = ["default.black","default.white"] 54 | stream-server = ["default.black","default.blue"] 55 | title = ["default.red","unused"] 56 | 57 | [light] 58 | button = ["default.black","default.white"] 59 | button-focus = ["default.white","default.purple"] 60 | button-selected = ["default.black","default.white"] 61 | cmdline = ["default.black","default.yellow"] 62 | cmdline-button = ["default.yellow","default.black"] 63 | cmdline-border = ["default.black","default.yellow"] 64 | copy-mode = ["default.white","default.yellow"] 65 | copy-mode-alt = ["default.yellow","default.white"] 66 | copy-mode-label = ["default.black","default.red"] 67 | current-capture = ["default.black","unused"] 68 | dialog = ["default.black","default.yellow"] 69 | dialog-button = ["default.yellow","default.black"] 70 | default = ["default.black","default.white"] 71 | filter-empty = ["default.black","default.cyan"] 72 | filter-intermediate = ["default.black","default.yellow"] 73 | filter-invalid = ["default.black","default.red"] 74 | filter-menu = ["default.black","default.white"] 75 | filter-valid = ["default.black","default.green"] 76 | hex-byte-selected = ["default.black","default.purple"] 77 | hex-byte-unselected = ["default.black","default.white"] 78 | hex-field-selected = ["default.black","default.cyan"] 79 | hex-field-unselected = ["default.white","default.black"] 80 | hex-interval-selected = ["default.white","default.black"] 81 | hex-interval-unselected = ["default.white","default.black"] 82 | hex-layer-selected = ["default.white","default.black"] 83 | hex-layer-unselected = ["default.white","default.black"] 84 | packet-list-cell-focus = ["default.black","default.purple"] 85 | packet-list-cell-selected = ["default.white","default.black"] 86 | packet-list-row-focus = ["default.black","default.cyan"] 87 | packet-list-row-selected = ["default.white","default.black"] 88 | packet-struct-focus = ["default.black","default.cyan"] 89 | packet-struct-selected = ["default.white","default.black"] 90 | progress-complete = ["default.white","default.purple"] 91 | progress-default = ["default.white","default.black"] 92 | progress-spinner = ["default.yellow","default.black"] 93 | spinner = ["default.yellow","default.white"] 94 | stream-client = ["default.black","default.red"] 95 | stream-match = ["default.white","default.yellow"] 96 | stream-search = ["default.white","default.black"] 97 | stream-server = ["default.black","default.blue"] 98 | title = ["default.red","unused"] 99 | -------------------------------------------------------------------------------- /assets/themes/dracula-256.toml: -------------------------------------------------------------------------------- 1 | 2 | unused = "#79e11a" 3 | 4 | [dracula] 5 | gray1 = "#464752" 6 | gray2 = "#565761" 7 | gray3 = "#b6b6b2" 8 | gray4 = "#ccccc7" 9 | black = "#282a36" 10 | blue = "#6272a4" 11 | cyan = "#8be9fd" 12 | green = "#50fa7b" 13 | magenta = "#ff79c6" 14 | orange = "#ffb86c" 15 | purple = "#bd93f9" 16 | red = "#ff5555" 17 | white = "#f8f8f2" 18 | yellow = "#f1fa8c" 19 | 20 | [dark] 21 | button = ["dracula.black","dracula.gray3"] 22 | button-focus = ["dracula.black","dracula.magenta"] 23 | button-selected = ["dracula.white","dracula.gray3"] 24 | cmdline = ["dracula.black","dracula.yellow"] 25 | cmdline-button = ["dracula.yellow","dracula.black"] 26 | cmdline-border = ["dracula.black","dracula.yellow"] 27 | copy-mode = ["dracula.black","dracula.yellow"] 28 | copy-mode-alt = ["dracula.yellow","dracula.black"] 29 | copy-mode-label = ["dracula.white","dracula.red"] 30 | current-capture = ["dracula.white","themes.unused"] 31 | dialog = ["dracula.black","dracula.yellow"] 32 | dialog-button = ["dracula.yellow","dracula.black"] 33 | default = ["dracula.white","dracula.black"] 34 | filter-empty = ["dracula.black","dracula.cyan"] 35 | filter-intermediate = ["dracula.black","dracula.orange"] 36 | filter-invalid = ["dracula.black","dracula.red"] 37 | filter-menu = ["dracula.white","dracula.black"] 38 | filter-valid = ["dracula.black","dracula.green"] 39 | hex-byte-selected = ["dracula.white","dracula.purple"] 40 | hex-byte-unselected = ["dracula.white","dracula.black"] 41 | hex-field-selected = ["dracula.black","dracula.cyan"] 42 | hex-field-unselected = ["dracula.black","dracula.white"] 43 | hex-interval-selected = ["dracula.white","dracula.gray2"] 44 | hex-interval-unselected = ["dracula.white","dracula.gray1"] 45 | hex-layer-selected = ["dracula.white","dracula.gray2"] 46 | hex-layer-unselected = ["dracula.white","dracula.gray1"] 47 | packet-list-cell-focus = ["dracula.white","dracula.purple"] 48 | packet-list-cell-selected = ["dracula.white","dracula.gray2"] 49 | packet-list-row-focus = ["dracula.white","dracula.cyan"] 50 | packet-list-row-selected = ["dracula.white","dracula.gray1"] 51 | packet-struct-focus = ["dracula.black","dracula.cyan"] 52 | packet-struct-selected = ["dracula.black","dracula.gray2"] 53 | progress-complete = ["dracula.white","dracula.purple"] 54 | progress-default = ["dracula.white","dracula.black"] 55 | progress-spinner = ["dracula.yellow","dracula.purple"] 56 | spinner = ["dracula.yellow","dracula.black"] 57 | stream-client = ["dracula.black","dracula.red"] 58 | stream-match = ["dracula.black","dracula.yellow"] 59 | stream-search = ["dracula.black","dracula.white"] 60 | stream-server = ["dracula.white","dracula.blue"] 61 | title = ["dracula.red","unused"] 62 | 63 | [light] 64 | button = ["dracula.black","dracula.white"] 65 | button-focus = ["dracula.black","dracula.purple"] 66 | button-selected = ["dracula.black","dracula.gray3"] 67 | cmdline = ["dracula.black","dracula.yellow"] 68 | cmdline-button = ["dracula.yellow","dracula.black"] 69 | cmdline-border = ["dracula.black","dracula.yellow"] 70 | copy-mode = ["dracula.white","dracula.yellow"] 71 | copy-mode-alt = ["dracula.yellow","dracula.white"] 72 | copy-mode-label = ["dracula.black","dracula.red"] 73 | current-capture = ["dracula.black","unused"] 74 | dialog = ["dracula.black","dracula.yellow"] 75 | dialog-button = ["dracula.yellow","dracula.black"] 76 | default = ["dracula.black","dracula.white"] 77 | filter-empty = ["dracula.black","dracula.cyan"] 78 | filter-intermediate = ["dracula.black","dracula.orange"] 79 | filter-invalid = ["dracula.black","dracula.red"] 80 | filter-menu = ["dracula.black","dracula.white"] 81 | filter-valid = ["dracula.black","dracula.green"] 82 | hex-byte-selected = ["dracula.white","dracula.purple"] 83 | hex-byte-unselected = ["dracula.black","dracula.white"] 84 | hex-field-selected = ["dracula.black","dracula.cyan"] 85 | hex-field-unselected = ["dracula.black","dracula.gray2"] 86 | hex-interval-selected = ["dracula.white","dracula.gray2"] 87 | hex-interval-unselected = ["dracula.black","dracula.gray4"] 88 | hex-layer-selected = ["dracula.white","dracula.gray2"] 89 | hex-layer-unselected = ["dracula.black","dracula.gray4"] 90 | packet-list-cell-focus = ["dracula.white","dracula.purple"] 91 | packet-list-cell-selected = ["dracula.black","dracula.gray3"] 92 | packet-list-row-focus = ["dracula.white","dracula.cyan"] 93 | packet-list-row-selected = ["dracula.black","dracula.gray4"] 94 | packet-struct-focus = ["dracula.black","dracula.cyan"] 95 | packet-struct-selected = ["dracula.black","dracula.gray4"] 96 | progress-complete = ["dracula.white","dracula.purple"] 97 | progress-default = ["dracula.white","dracula.black"] 98 | progress-spinner = ["dracula.yellow","dracula.black"] 99 | spinner = ["dracula.yellow","dracula.white"] 100 | stream-client = ["dracula.white","dracula.red"] 101 | stream-match = ["dracula.white","dracula.yellow"] 102 | stream-search = ["dracula.white","dracula.black"] 103 | stream-server = ["dracula.white","dracula.blue"] 104 | title = ["dracula.red","unused"] 105 | -------------------------------------------------------------------------------- /configs/termshark-dd01307f2423.json.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcla/termshark/e8a1ec6e4f2d517f646d3bba35a2ec4926e0202c/configs/termshark-dd01307f2423.json.enc -------------------------------------------------------------------------------- /docs/Maintainer.md: -------------------------------------------------------------------------------- 1 | # How to Package Termshark for Release 2 | 3 | ## Termux (Android) 4 | 5 | I've been building use the termux docker builder. 6 | 7 | ```bash 8 | docker pull termux/package-builder 9 | ``` 10 | 11 | Clone the `termux-packages` and `termux-root-packages` repos: 12 | 13 | ```bash 14 | cd source/ 15 | git clone https://github.com/termux/termux-packages 16 | cd termux-packages 17 | git clone https://github.com/termux/termux-root-packages 18 | ``` 19 | 20 | Open `termux-packages/termux-root-packages/packages/termshark/build.sh` in an editor. Change 21 | 22 | ```bash 23 | cd $TERMUX_PKG_BUILDDIR 24 | go get -d -v github.com/gcla/termshark/v2/cmd/termshark@e185fa59d87c06fe1bafb83ce6dc15591434ccc8 25 | go install github.com/gcla/termshark/v2/cmd/termshark 26 | ``` 27 | 28 | to use the correct uuid - I am using the uuid for v2.0.3 29 | 30 | ```bash 31 | cd $TERMUX_PKG_BUILDDIR 32 | go get -d -v github.com/gcla/termshark/v2/cmd/termshark@73dfd1f6cb8c553eb524ebc27d991f637c1ac5ea 33 | go install github.com/gcla/termshark/v2/cmd/termshark 34 | ``` 35 | 36 | Change `TERMUX_PKG_VERSION` too. 37 | 38 | Save. Start docker and build (from `termux-packages` dir): 39 | 40 | ```bash 41 | gcla@elgin:~/source/termux-packages$ ./scripts/run-docker.sh 42 | Running container 'termux-package-builder' from image 'termux/package-builder'... 43 | builder@201c39983bf8:~/termux-packages$ rm /data/data/.built-packages/termshark 44 | builder@201c39983bf8:~/termux-packages$ ./clean.sh # to rebuild everything! 45 | builder@201c39983bf8:~/termux-packages$ ./build-package.sh termux-root-packages/packages/termshark/ 46 | ... 47 | ``` 48 | 49 | This will take several minutes. You'll probably see an error like this: 50 | 51 | ``` 52 | Wrong checksum for https://termshark.io: 53 | Expected: 36e45dfeb97f89379bda5be6bfe69c46e5c4211674120977e7b0033f5d90321a 54 | Actual: c05a64f1e502d406cc149c6e8b92720ad6310aecd1dd206e05713fd8a2247a84 55 | ``` 56 | 57 | Open `termux-packages/termux-root-packages/packages/termshark/build.sh` again and change `TERMUX_PKG_SHA256`. Rebuild. 58 | 59 | Submit a PR to `termux-root-packages`. 60 | 61 | To edit files in use by a docker container, you can use tramp + emacs with a path like this: `/docker:builder@201c39983bf8:/home/builder/termux-packages/termux-root-packages/packages/termshark/build.sh` 62 | 63 | ## Snapcraft 64 | 65 | Fork Mario's termshark-snap repository: https://github.com/mharjac/termshark-snap (@mharjac) and clone it to a recentish Linux. Edit `snapcraft.yaml`. Change `version:` and edit this section to use the correct hash - this one corresponds to v2.0.3: 66 | 67 | ``` 68 | go get github.com/gcla/termshark/v2/cmd/termshark@73dfd1f6cb8c553eb524ebc27d991f637c1ac5ea 69 | ``` 70 | 71 | From a shell, type 72 | 73 | ``` 74 | snapcraft 75 | ``` 76 | 77 | If you have prior snapcraft builds of termshark, you might need 78 | 79 | ``` 80 | snapcraft clean 81 | ``` 82 | 83 | first. On my 19.10 machine, I ran into snapcraft failures that resolved when I simply ran `snapcraft` again... 84 | 85 | When this succeeds, the working directory should have a file `termshark_2.0.3_amd64.snap`. To install this - to test it out - try this: 86 | 87 | ``` 88 | snap install --dangerous ./termshark_2.0.3_amd64.snap 89 | ``` 90 | 91 | then to run it: 92 | 93 | ``` 94 | /snap/bin/termshark -h 95 | ``` 96 | 97 | Check your changes in and submit a PR to Mario (@mharjac). 98 | 99 | 100 | -------------------------------------------------------------------------------- /docs/Packages.md: -------------------------------------------------------------------------------- 1 | # Install Packages 2 | 3 | Here's how to install termshark on various OSes and with various package managers. 4 | 5 | ## Arch Linux 6 | 7 | - [termshark](https://archlinux.org/packages/community/x86_64/termshark/): The 8 | official package. 9 | - [termshark-git](https://aur.archlinux.org/packages/termshark-git): Compiles 10 | from source, made by [Thann](https://github.com/Thann) 11 | 12 | ## Debian 13 | 14 | Termshark is only available in unstable/sid at the moment. 15 | 16 | ```bash 17 | apt update 18 | apt install termshark 19 | ``` 20 | 21 | ## FreeBSD 22 | 23 | Thanks to [Ryan Steinmetz](https://github.com/zi0r) 24 | 25 | Termshark is in the FreeBSD ports tree! To install the package, run: 26 | 27 | `pkg install termshark` 28 | 29 | To build/install the port, run: 30 | 31 | `cd /usr/ports/net/termshark/ && make install clean` 32 | 33 | ## Homebrew 34 | 35 | ```bash 36 | brew update 37 | brew install termshark 38 | ``` 39 | 40 | ## MacPorts 41 | 42 | ```bash 43 | sudo port selfupdate 44 | sudo port install termshark 45 | ``` 46 | 47 | ## Kali Linux 48 | 49 | ```bash 50 | apt update 51 | apt install termshark 52 | ``` 53 | 54 | ## NixOS 55 | 56 | Thanks to [Patrick Winter](https://github.com/winpat) 57 | 58 | ```bash 59 | nix-channel --add https://nixos.org/channels/nixpkgs-unstable 60 | nix-channel --update 61 | nix-env -iA nixpkgs.termshark 62 | ``` 63 | 64 | ## SnapCraft 65 | 66 | Thanks to [mharjac](https://github.com/mharjac) 67 | 68 | Termshark can be easily installed on almost all major distros just by issuing: 69 | 70 | ```bash 71 | snap install termshark 72 | ``` 73 | 74 | Note there is a big caveat with Snap and the architecture of Wireshark that prevents termshark being able to read network interfaces. If installed via Snap, termshark will only be able to work with pcap files. See [this explanation](https://forum.snapcraft.io/t/wireshark-and-setcap/9629/6). 75 | 76 | ## Termux (Android) 77 | 78 | ```bash 79 | pkg install root-repo 80 | pkg install termshark 81 | ``` 82 | 83 | Note that termshark does not require a rooted phone to inspect a pcap, but it does depend on tshark which is itself in Termux's root-repo for programs that do work best on a rooted phone. 84 | 85 | If you would like to use termshark's copy-mode to copy sections of packets to your Android clipboard, you will also need [Termux:API](https://play.google.com/store/apps/details?id=com.termux.api&hl=en_US). Install from the Play Store, then from termux, type: 86 | 87 | ```bash 88 | pkg install termux-api 89 | ``` 90 | 91 | ![device art](/../gh-pages/images/device art.png?raw=true) 92 | 93 | ## Ubuntu 94 | 95 | If you are running Ubuntu 19.10 (eoan) or higher, termshark can be installed like this: 96 | 97 | ```bash 98 | sudo apt install termshark 99 | ``` 100 | 101 | For Ubuntu < 19.10, you can use the PPA _nicolais/termshark_ to install termshark: 102 | 103 | ```bash 104 | sudo add-apt-repository --update ppa:nicolais/termshark 105 | sudo apt install termshark 106 | ``` 107 | 108 | Thanks to [Nicolai Søberg](https://github.com/NicolaiSoeborg) 109 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gcla/termshark/v2 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/adam-hanna/arrayOperations v0.2.6 7 | github.com/antchfx/xmlquery v1.3.3 8 | github.com/antchfx/xpath v1.1.11 // indirect 9 | github.com/blang/semver v3.5.1+incompatible 10 | github.com/flytam/filenamify v1.1.0 11 | github.com/gcla/deep v1.0.2 12 | github.com/gcla/gowid v1.4.1-0.20221101015339-ce29e21d2804 13 | github.com/gcla/tail v1.0.1-0.20190505190527-650e90873359 14 | github.com/gcla/term v0.0.0-20220601234708-3e6af2ebff27 15 | github.com/gdamore/tcell/v2 v2.5.0 16 | github.com/gin-gonic/gin v1.7.0 // indirect 17 | github.com/go-test/deep v1.0.2 // indirect 18 | github.com/hashicorp/golang-lru v0.5.4 19 | github.com/jessevdk/go-flags v1.4.0 20 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 21 | github.com/mattn/go-isatty v0.0.14 22 | github.com/mitchellh/go-homedir v1.1.0 23 | github.com/mreiferson/go-snappystream v0.2.3 24 | github.com/pkg/errors v0.9.1 25 | github.com/psanford/wormhole-william v1.0.6-0.20210402190004-049df45b8d5a 26 | github.com/rakyll/statik v0.1.7 27 | github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 28 | github.com/sirupsen/logrus v1.7.0 29 | github.com/spf13/viper v1.12.0 30 | github.com/stretchr/testify v1.7.1 31 | github.com/tevino/abool v1.2.0 32 | gitlab.com/jonas.jasas/condchan v0.0.0-20190210165812-36637ad2b5bc 33 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a 34 | gopkg.in/fsnotify/fsnotify.v1 v1.4.7 35 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /pkg/cli/all.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | // 5 | 6 | package cli 7 | 8 | import "github.com/jessevdk/go-flags" 9 | 10 | //====================================================================== 11 | 12 | // Used to determine if we should run tshark instead e.g. stdout is not a tty 13 | type Tshark struct { 14 | PassThru string `long:"pass-thru" default:"auto" optional:"true" optional-value:"true" choice:"yes" choice:"no" choice:"auto" choice:"true" choice:"false" description:"Run tshark instead (auto => if stdout is not a tty)."` 15 | Profile string `long:"profile" short:"C" description:"Start with this configuration profile." value-name:""` 16 | PrintIfaces bool `short:"D" optional:"true" optional-value:"true" description:"Print a list of the interfaces on which termshark can capture."` 17 | TailSwitch 18 | } 19 | 20 | // Termshark's own command line arguments. Used if we don't pass through to tshark. 21 | type Termshark struct { 22 | Ifaces []string `value-name:"" short:"i" description:"Interface(s) to read."` 23 | Pcap flags.Filename `value-name:"" short:"r" description:"Pcap file/fifo to read. Use - for stdin."` 24 | WriteTo flags.Filename `value-name:"" short:"w" description:"Write raw packet data to outfile."` 25 | DecodeAs []string `short:"d" description:"Specify dissection of layer type." value-name:"==,"` 26 | PrintIfaces bool `short:"D" optional:"true" optional-value:"true" description:"Print a list of the interfaces on which termshark can capture."` 27 | DisplayFilter string `short:"Y" description:"Apply display filter." value-name:""` 28 | CaptureFilter string `short:"f" description:"Apply capture filter." value-name:""` 29 | TimestampFormat string `short:"t" description:"Set the format of the packet timestamp printed in summary lines." choice:"a" choice:"ad" choice:"adoy" choice:"d" choice:"dd" choice:"e" choice:"r" choice:"u" choice:"ud" choice:"udoy" value-name:""` 30 | PlatformSwitches 31 | Profile string `long:"profile" short:"C" description:"Start with this configuration profile." value-name:""` 32 | PassThru string `long:"pass-thru" default:"auto" optional:"true" optional-value:"true" choice:"auto" choice:"true" choice:"false" description:"Run tshark instead (auto => if stdout is not a tty)."` 33 | LogTty bool `long:"log-tty" optional:"true" optional-value:"true" choice:"true" choice:"false" description:"Log to the terminal."` 34 | Debug TriState `long:"debug" default:"unset" hidden:"true" optional:"true" optional-value:"true" description:"Enable termshark debugging. See https://termshark.io/userguide."` 35 | Help bool `long:"help" short:"h" optional:"true" optional-value:"true" description:"Show this help message."` 36 | Version []bool `long:"version" short:"v" optional:"true" optional-value:"true" description:"Show version information."` 37 | 38 | Args struct { 39 | FilterOrPcap string `value-name:"" description:"Filter (capture for iface, display for pcap), or pcap to read."` 40 | } `positional-args:"yes"` 41 | } 42 | 43 | // If args are passed through to tshark (e.g. stdout not a tty), then 44 | // strip these out so tshark doesn't fail. 45 | var TermsharkOnly = []string{"--pass-thru", "--profile", "--log-tty", "--debug", "--tail"} 46 | 47 | func FlagIsTrue(val string) bool { 48 | return val == "true" || val == "yes" 49 | } 50 | 51 | //====================================================================== 52 | // Local Variables: 53 | // mode: Go 54 | // fill-column: 78 55 | // End: 56 | -------------------------------------------------------------------------------- /pkg/cli/flags.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | // 5 | // +build !windows 6 | 7 | package cli 8 | 9 | //====================================================================== 10 | 11 | // Embedded in the CLI options struct. 12 | type PlatformSwitches struct { 13 | Tty string `long:"tty" description:"Display the UI on this terminal." value-name:""` 14 | } 15 | 16 | func (p PlatformSwitches) TtyValue() string { 17 | return p.Tty 18 | } 19 | 20 | //====================================================================== 21 | 22 | type TailSwitch struct{} 23 | 24 | func (t TailSwitch) TailFileValue() string { 25 | return "" 26 | } 27 | 28 | //====================================================================== 29 | // Local Variables: 30 | // mode: Go 31 | // fill-column: 78 32 | // End: 33 | -------------------------------------------------------------------------------- /pkg/cli/flags_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package cli 6 | 7 | import "github.com/jessevdk/go-flags" 8 | 9 | //====================================================================== 10 | 11 | type PlatformSwitches struct{} 12 | 13 | func (p PlatformSwitches) TtyValue() string { 14 | return "" 15 | } 16 | 17 | //====================================================================== 18 | 19 | type TailSwitch struct { 20 | Tail flags.Filename `value-name:"" long:"tail" hidden:"true" description:"Tail a file (private)."` 21 | } 22 | 23 | func (t TailSwitch) TailFileValue() string { 24 | return string(t.Tail) 25 | } 26 | 27 | //====================================================================== 28 | // Local Variables: 29 | // mode: Go 30 | // fill-column: 78 31 | // End: 32 | -------------------------------------------------------------------------------- /pkg/cli/tristate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | // 5 | 6 | package cli 7 | 8 | //====================================================================== 9 | 10 | type TriState struct { 11 | Set bool 12 | Val bool 13 | } 14 | 15 | func (b *TriState) UnmarshalFlag(value string) error { 16 | switch value { 17 | case "true", "TRUE", "t", "T", "1", "y", "Y", "yes", "Yes", "YES": 18 | b.Set = true 19 | b.Val = true 20 | case "false", "FALSE", "f", "F", "0", "n", "N", "no", "No", "NO": 21 | b.Set = true 22 | b.Val = false 23 | default: 24 | b.Set = false 25 | } 26 | return nil 27 | } 28 | 29 | func (b TriState) MarshalFlag() string { 30 | if b.Set { 31 | if b.Val { 32 | return "true" 33 | } else { 34 | return "false" 35 | } 36 | } else { 37 | return "unset" 38 | } 39 | } 40 | 41 | //====================================================================== 42 | // Local Variables: 43 | // mode: Go 44 | // fill-column: 78 45 | // End: 46 | -------------------------------------------------------------------------------- /pkg/confwatcher/confwatcher.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package confwatcher 6 | 7 | import ( 8 | "os" 9 | "sync" 10 | 11 | "github.com/gcla/termshark/v2" 12 | log "github.com/sirupsen/logrus" 13 | fsnotify "gopkg.in/fsnotify/fsnotify.v1" 14 | ) 15 | 16 | //====================================================================== 17 | 18 | type ConfigWatcher struct { 19 | watcher *fsnotify.Watcher 20 | change chan struct{} 21 | closech chan struct{} 22 | closeWait sync.WaitGroup 23 | } 24 | 25 | func New() (*ConfigWatcher, error) { 26 | watcher, err := fsnotify.NewWatcher() 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | change := make(chan struct{}) 32 | closech := make(chan struct{}) 33 | 34 | res := &ConfigWatcher{ 35 | change: change, 36 | closech: closech, 37 | } 38 | 39 | res.closeWait.Add(1) 40 | 41 | termshark.TrackedGo(func() { 42 | defer func() { 43 | res.watcher.Close() 44 | close(change) 45 | res.closeWait.Done() 46 | }() 47 | Loop: 48 | for { 49 | select { 50 | case <-watcher.Events: 51 | res.change <- struct{}{} 52 | 53 | case err := <-watcher.Errors: 54 | log.Debugf("Error from config watcher: %v", err) 55 | 56 | case <-closech: 57 | break Loop 58 | } 59 | } 60 | }, Goroutinewg) 61 | 62 | if err := watcher.Add(termshark.ConfFile("termshark.toml")); err != nil && !os.IsNotExist(err) { 63 | return nil, err 64 | } 65 | 66 | res.watcher = watcher 67 | 68 | return res, nil 69 | } 70 | 71 | func (c *ConfigWatcher) Close() { 72 | // drain the change channel to ensure the goroutine above can process the close. This 73 | // is safe because I know, at this point, there are no other readers because termshark 74 | // has exited its select loop. 75 | termshark.TrackedGo(func() { 76 | // This might block because the goroutine above might not be blocked sending 77 | // to c.change. But then that means the goroutine's for loop above will terminate, 78 | // c.change will be closed, and then this goroutine will end. If the above 79 | // goroutine is blocked sending to c.change, then this will drain that value, 80 | // and again the goroutine above will end. 81 | <-c.change 82 | }, Goroutinewg) 83 | 84 | c.closech <- struct{}{} 85 | c.closeWait.Wait() 86 | } 87 | 88 | func (c *ConfigWatcher) ConfigChanged() <-chan struct{} { 89 | return c.change 90 | } 91 | 92 | //====================================================================== 93 | 94 | var Goroutinewg *sync.WaitGroup 95 | 96 | //====================================================================== 97 | // Local Variables: 98 | // mode: Go 99 | // fill-column: 78 100 | // End: 101 | -------------------------------------------------------------------------------- /pkg/convs/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package convs 6 | 7 | import "fmt" 8 | 9 | type Ethernet struct{} 10 | type IPv4 struct{} 11 | type IPv6 struct{} 12 | type UDP struct{} 13 | type TCP struct{} 14 | 15 | var OfficialNameToType = map[string]string{ 16 | Ethernet{}.String(): Ethernet{}.Short(), 17 | IPv4{}.String(): IPv4{}.Short(), 18 | IPv6{}.String(): IPv6{}.Short(), 19 | UDP{}.String(): UDP{}.Short(), 20 | TCP{}.String(): TCP{}.Short(), 21 | } 22 | 23 | //====================================================================== 24 | 25 | func (t Ethernet) String() string { 26 | return "Ethernet" 27 | } 28 | 29 | func (t Ethernet) Short() string { 30 | return "eth" 31 | } 32 | 33 | func (t Ethernet) FilterTo(vals ...string) string { 34 | return fmt.Sprintf("eth.dst == %s", vals[0]) 35 | } 36 | 37 | func (t Ethernet) FilterFrom(vals ...string) string { 38 | return fmt.Sprintf("eth.src == %s", vals[0]) 39 | } 40 | 41 | func (t Ethernet) FilterAny(vals ...string) string { 42 | return fmt.Sprintf("eth.addr == %s", vals[0]) 43 | } 44 | 45 | func (t Ethernet) AIndex() []int { 46 | return []int{0} 47 | } 48 | 49 | func (t Ethernet) BIndex() []int { 50 | return []int{1} 51 | } 52 | 53 | //====================================================================== 54 | 55 | func (t IPv4) String() string { 56 | return "IPv4" 57 | } 58 | 59 | func (t IPv4) Short() string { 60 | return "ip" 61 | } 62 | 63 | func (t IPv4) FilterTo(vals ...string) string { 64 | return fmt.Sprintf("ip.dst == %s", vals[0]) 65 | } 66 | 67 | func (t IPv4) FilterFrom(vals ...string) string { 68 | return fmt.Sprintf("ip.src == %s", vals[0]) 69 | } 70 | 71 | func (t IPv4) FilterAny(vals ...string) string { 72 | return fmt.Sprintf("ip.addr == %s", vals[0]) 73 | } 74 | 75 | func (t IPv4) AIndex() []int { 76 | return []int{0} 77 | } 78 | 79 | func (t IPv4) BIndex() []int { 80 | return []int{1} 81 | } 82 | 83 | //====================================================================== 84 | 85 | func (t IPv6) String() string { 86 | return "IPv6" 87 | } 88 | 89 | func (t IPv6) Short() string { 90 | return "ipv6" 91 | } 92 | 93 | func (t IPv6) FilterTo(vals ...string) string { 94 | return fmt.Sprintf("ipv6.dst == %s", vals[0]) 95 | } 96 | 97 | func (t IPv6) FilterFrom(vals ...string) string { 98 | return fmt.Sprintf("ipv6.src == %s", vals[0]) 99 | } 100 | 101 | func (t IPv6) FilterAny(vals ...string) string { 102 | return fmt.Sprintf("ipv6.addr == %s", vals[0]) 103 | } 104 | 105 | func (t IPv6) AIndex() []int { 106 | return []int{0} 107 | } 108 | 109 | func (t IPv6) BIndex() []int { 110 | return []int{1} 111 | } 112 | 113 | //====================================================================== 114 | 115 | func (t UDP) String() string { 116 | return "UDP" 117 | } 118 | 119 | func (t UDP) Short() string { 120 | return "udp" 121 | } 122 | 123 | func (t UDP) FilterTo(vals ...string) string { 124 | return fmt.Sprintf("%s && udp.dstport == %s", IPv4{}.FilterTo(vals[0]), vals[1]) 125 | } 126 | 127 | func (t UDP) FilterFrom(vals ...string) string { 128 | return fmt.Sprintf("%s && udp.srcport == %s", IPv4{}.FilterFrom(vals[0]), vals[1]) 129 | } 130 | 131 | func (t UDP) FilterAny(vals ...string) string { 132 | return fmt.Sprintf("%s && udp.port == %s", IPv4{}.FilterAny(vals[0]), vals[1]) 133 | } 134 | 135 | func (t UDP) AIndex() []int { 136 | return []int{0, 1} 137 | } 138 | 139 | func (t UDP) BIndex() []int { 140 | return []int{2, 3} 141 | } 142 | 143 | //====================================================================== 144 | 145 | func (t TCP) String() string { 146 | return "TCP" 147 | } 148 | 149 | func (t TCP) Short() string { 150 | return "tcp" 151 | } 152 | 153 | func (t TCP) FilterTo(vals ...string) string { 154 | return fmt.Sprintf("%s && tcp.dstport == %s", IPv4{}.FilterTo(vals[0]), vals[1]) 155 | } 156 | 157 | func (t TCP) FilterFrom(vals ...string) string { 158 | return fmt.Sprintf("%s && tcp.srcport == %s", IPv4{}.FilterFrom(vals[0]), vals[1]) 159 | } 160 | 161 | func (t TCP) FilterAny(vals ...string) string { 162 | return fmt.Sprintf("%s && tcp.port == %s", IPv4{}.FilterAny(vals[0]), vals[1]) 163 | } 164 | 165 | func (t TCP) AIndex() []int { 166 | return []int{0, 1} 167 | } 168 | 169 | func (t TCP) BIndex() []int { 170 | return []int{2, 3} 171 | } 172 | 173 | //====================================================================== 174 | // Local Variables: 175 | // mode: Go 176 | // fill-column: 78 177 | // End: 178 | -------------------------------------------------------------------------------- /pkg/fields/fields_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license 2 | // that can be found in the LICENSE file. 3 | 4 | package fields 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | //====================================================================== 13 | 14 | func TestFields1(t *testing.T) { 15 | 16 | fields := New() 17 | err := fields.InitNoCache() 18 | assert.NoError(t, err) 19 | 20 | m1, ok := fields.ser.Fields.(map[string]interface{})["tcp"] 21 | assert.Equal(t, true, ok) 22 | 23 | m2, ok := m1.(map[string]interface{})["port"] 24 | assert.Equal(t, true, ok) 25 | 26 | assert.IsType(t, Field{}, m2) 27 | assert.Equal(t, m2.(Field).Type, FT_UINT16) 28 | } 29 | 30 | //====================================================================== 31 | // Local Variables: 32 | // mode: Go 33 | // fill-column: 78 34 | // End: 35 | -------------------------------------------------------------------------------- /pkg/format/hexdump.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package format implements useful string/byte formatting functions. 6 | package format 7 | 8 | import ( 9 | "encoding/hex" 10 | "fmt" 11 | "regexp" 12 | "strings" 13 | ) 14 | 15 | type Options struct { 16 | LeftAsciiDelimiter string 17 | RightAsciiDelimiter string 18 | } 19 | 20 | var re *regexp.Regexp 21 | 22 | func init() { 23 | re = regexp.MustCompile(`(?m)^(.{60})\|(.+?)\|$`) // do each line 24 | } 25 | 26 | // HexDump produces a wireshark-like hexdump, with an option to set the left and 27 | // right delimiter used for the ascii section. This is a cheesy implementation using 28 | // a regex to change the golang hexdump output. 29 | func HexDump(data []byte, opts ...Options) string { 30 | var opt Options 31 | if len(opts) > 0 { 32 | opt = opts[0] 33 | } 34 | // Output: 35 | // 00000000 47 6f 20 69 73 20 61 6e 20 6f 70 65 6e 20 73 6f |Go is an open so| 36 | // 00000010 75 72 63 65 20 70 72 6f 67 72 61 6d 6d 69 6e 67 |urce programming| 37 | // 00000020 20 6c 61 6e 67 75 61 67 65 2e | language.| 38 | res := hex.Dump(data) 39 | res = re.ReplaceAllString(res, fmt.Sprintf(`${1}%s${2}%s`, opt.LeftAsciiDelimiter, opt.RightAsciiDelimiter)) 40 | 41 | return strings.TrimRight(res, "\n") 42 | } 43 | 44 | //====================================================================== 45 | // Local Variables: 46 | // mode: Go 47 | // fill-column: 110 48 | // End: 49 | -------------------------------------------------------------------------------- /pkg/format/hexdump_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package format 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | //====================================================================== 14 | 15 | func TestHexDump1(t *testing.T) { 16 | var tests = []struct { 17 | in string 18 | out string 19 | }{ 20 | { 21 | "Go is an open source programming language.", 22 | "00000000 47 6f 20 69 73 20 61 6e 20 6f 70 65 6e 20 73 6f QGo is an open so\n" + 23 | "00000010 75 72 63 65 20 70 72 6f 67 72 61 6d 6d 69 6e 67 Qurce programming\n" + 24 | "00000020 20 6c 61 6e 67 75 61 67 65 2e Q language.", 25 | }, 26 | } 27 | 28 | for _, test := range tests { 29 | assert.Equal(t, true, (HexDump([]byte(test.in), Options{ 30 | LeftAsciiDelimiter: "Q", 31 | }) == test.out)) 32 | } 33 | } 34 | 35 | //====================================================================== 36 | // Local Variables: 37 | // mode: Go 38 | // fill-column: 110 39 | // End: 40 | -------------------------------------------------------------------------------- /pkg/format/printable.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package format implements useful string/byte formatting functions. 6 | package format 7 | 8 | import ( 9 | "bytes" 10 | "encoding/hex" 11 | "fmt" 12 | "regexp" 13 | "strings" 14 | "unicode" 15 | ) 16 | 17 | func MakePrintableString(data []byte) string { 18 | var buffer bytes.Buffer 19 | for i := 0; i < len(data); i++ { 20 | if unicode.IsPrint(rune(data[i])) { 21 | buffer.WriteString(string(rune(data[i]))) 22 | } 23 | } 24 | return buffer.String() 25 | } 26 | 27 | func MakePrintableStringWithNewlines(data []byte) string { 28 | var buffer bytes.Buffer 29 | for i := 0; i < len(data); i++ { 30 | if (data[i] >= 32 && data[i] < 127) || data[i] == '\n' { 31 | buffer.WriteString(string(rune(data[i]))) 32 | } else { 33 | buffer.WriteRune('.') 34 | } 35 | } 36 | return buffer.String() 37 | } 38 | 39 | func MakeEscapedString(data []byte) string { 40 | res := make([]string, 0) 41 | var buffer bytes.Buffer 42 | for i := 0; i < len(data); i++ { 43 | buffer.WriteString(fmt.Sprintf("\\x%02x", data[i])) 44 | if i%16 == 16-1 || i+1 == len(data) { 45 | res = append(res, fmt.Sprintf("\"%s\"", buffer.String())) 46 | buffer.Reset() 47 | } 48 | } 49 | return strings.Join(res, " \\\n") 50 | } 51 | 52 | func MakeHexStream(data []byte) string { 53 | var buffer bytes.Buffer 54 | for i := 0; i < len(data); i++ { 55 | buffer.WriteString(fmt.Sprintf("%02x", data[i])) 56 | } 57 | return buffer.String() 58 | } 59 | 60 | var hexRe = regexp.MustCompile(`\\x[0-9a-fA-F][0-9a-fA-F]`) 61 | 62 | // TranslateHexCodes will change instances of "\x41" in the input to the 63 | // byte 'A' in the output, passing through other characters. This is a small 64 | // subset of strconv.Unquote() for wireshark PSML data. 65 | func TranslateHexCodes(s []byte) []byte { 66 | return hexRe.ReplaceAllFunc(s, func(m []byte) []byte { 67 | r, err := hex.DecodeString(string(m[2:])) 68 | if err != nil { 69 | panic(err) 70 | } 71 | return []byte{r[0]} 72 | }) 73 | } 74 | 75 | //====================================================================== 76 | // Local Variables: 77 | // mode: Go 78 | // fill-column: 110 79 | // End: 80 | -------------------------------------------------------------------------------- /pkg/noroot/noroot.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package noroot 6 | 7 | import ( 8 | "github.com/gcla/gowid/widgets/list" 9 | "github.com/gcla/gowid/widgets/tree" 10 | ) 11 | 12 | //====================================================================== 13 | 14 | type Walker struct { 15 | *tree.TreeWalker 16 | } 17 | 18 | func NewWalker(w *tree.TreeWalker) *Walker { 19 | return &Walker{ 20 | TreeWalker: w, 21 | } 22 | } 23 | 24 | // for omitting top level node 25 | func (f *Walker) Next(pos list.IWalkerPosition) list.IWalkerPosition { 26 | return tree.WalkerNext(f, pos) 27 | } 28 | 29 | func (f *Walker) Previous(pos list.IWalkerPosition) list.IWalkerPosition { 30 | fc := pos.(tree.IPos) 31 | pp := tree.PreviousPosition(fc, f.Tree()) 32 | if pp.Equal(tree.NewPos()) { 33 | return nil 34 | } 35 | return tree.WalkerPrevious(f, pos) 36 | } 37 | 38 | //====================================================================== 39 | // Local Variables: 40 | // mode: Go 41 | // fill-column: 110 42 | // End: 43 | -------------------------------------------------------------------------------- /pkg/pcap/cmds_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // +build !windows 6 | 7 | package pcap 8 | 9 | import ( 10 | "syscall" 11 | 12 | "github.com/kballard/go-shellquote" 13 | "github.com/pkg/errors" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | func (c *Command) PutInNewGroupOnUnix() { 18 | c.Cmd.SysProcAttr = &syscall.SysProcAttr{ 19 | Setpgid: true, 20 | Pgid: 0, 21 | } 22 | } 23 | 24 | func (c *Command) Kill() error { 25 | c.Lock() 26 | defer c.Unlock() 27 | if c.Cmd.Process == nil { 28 | return errors.WithStack(ProcessNotStarted{Command: c.Cmd}) 29 | } 30 | log.Infof("Sending SIGKILL to %v: %v", c.Cmd.Process.Pid, shellquote.Join(c.Cmd.Args...)) 31 | // The PSML tshark process doesn't reliably die with a SIGTERM - not sure why 32 | return syscall.Kill(-c.Cmd.Process.Pid, syscall.SIGKILL) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/pcap/cmds_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package pcap 6 | 7 | import "github.com/pkg/errors" 8 | 9 | func (c *Command) PutInNewGroupOnUnix() {} 10 | 11 | func (c *Command) Kill() error { 12 | c.Lock() 13 | defer c.Unlock() 14 | if c.Cmd.Process == nil { 15 | return errors.WithStack(ProcessNotStarted{Command: c.Cmd}) 16 | } 17 | return c.Cmd.Process.Kill() 18 | } 19 | -------------------------------------------------------------------------------- /pkg/pcap/handlers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package pcap 6 | 7 | import "github.com/gcla/gowid" 8 | 9 | //====================================================================== 10 | 11 | type HandlerCode int 12 | 13 | const ( 14 | NoneCode HandlerCode = 1 << iota 15 | PdmlCode 16 | PsmlCode 17 | TailCode 18 | IfaceCode 19 | ConvCode 20 | StreamCode 21 | CapinfoCode 22 | ) 23 | 24 | type IClear interface { 25 | OnClear(code HandlerCode, app gowid.IApp) 26 | } 27 | 28 | type INewSource interface { 29 | OnNewSource(code HandlerCode, app gowid.IApp) 30 | } 31 | 32 | type IOnError interface { 33 | OnError(code HandlerCode, app gowid.IApp, err error) 34 | } 35 | 36 | type IBeforeBegin interface { 37 | BeforeBegin(code HandlerCode, app gowid.IApp) 38 | } 39 | 40 | type IAfterEnd interface { 41 | AfterEnd(code HandlerCode, app gowid.IApp) 42 | } 43 | 44 | type IPsmlHeader interface { 45 | OnPsmlHeader(code HandlerCode, app gowid.IApp) 46 | } 47 | 48 | type IUnpack interface { 49 | Unpack() []interface{} 50 | } 51 | 52 | type HandlerList []interface{} 53 | 54 | func (h HandlerList) Unpack() []interface{} { 55 | return h 56 | } 57 | 58 | type unpackedHandlerFunc func(HandlerCode, gowid.IApp, interface{}) bool 59 | 60 | func HandleUnpack(code HandlerCode, cb interface{}, handler unpackedHandlerFunc, app gowid.IApp) bool { 61 | if c, ok := cb.(IUnpack); ok { 62 | handlers := c.Unpack() 63 | for _, cb := range handlers { 64 | handler(code, app, cb) // will wait on channel if it has to, doesn't matter if not 65 | } 66 | return true 67 | } 68 | return false 69 | } 70 | 71 | func HandleBegin(code HandlerCode, app gowid.IApp, cb interface{}) bool { 72 | res := false 73 | if !HandleUnpack(code, cb, HandleBegin, app) { 74 | if c, ok := cb.(IBeforeBegin); ok { 75 | c.BeforeBegin(code, app) 76 | res = true 77 | } 78 | } 79 | return res 80 | } 81 | 82 | func HandleEnd(code HandlerCode, app gowid.IApp, cb interface{}) bool { 83 | res := false 84 | if !HandleUnpack(code, cb, HandleEnd, app) { 85 | if c, ok := cb.(IAfterEnd); ok { 86 | c.AfterEnd(code, app) 87 | res = true 88 | } 89 | } 90 | return res 91 | } 92 | 93 | func HandleError(code HandlerCode, app gowid.IApp, err error, cb interface{}) bool { 94 | res := false 95 | if !HandleUnpack(code, cb, func(code HandlerCode, app gowid.IApp, cb2 interface{}) bool { 96 | return HandleError(code, app, err, cb2) 97 | }, app) { 98 | if ec, ok := cb.(IOnError); ok { 99 | ec.OnError(code, app, err) 100 | res = true 101 | } 102 | } 103 | return res 104 | } 105 | 106 | func handlePsmlHeader(code HandlerCode, app gowid.IApp, cb interface{}) bool { 107 | res := false 108 | if !HandleUnpack(code, cb, handlePsmlHeader, app) { 109 | if c, ok := cb.(IPsmlHeader); ok { 110 | c.OnPsmlHeader(code, app) 111 | res = true 112 | } 113 | } 114 | return res 115 | } 116 | 117 | func handleClear(code HandlerCode, app gowid.IApp, cb interface{}) bool { 118 | res := false 119 | if !HandleUnpack(code, cb, handleClear, app) { 120 | if c, ok := cb.(IClear); ok { 121 | c.OnClear(code, app) 122 | res = true 123 | } 124 | } 125 | return res 126 | } 127 | 128 | func handleNewSource(code HandlerCode, app gowid.IApp, cb interface{}) bool { 129 | res := false 130 | if !HandleUnpack(code, cb, handleNewSource, app) { 131 | if c, ok := cb.(INewSource); ok { 132 | c.OnNewSource(code, app) 133 | res = true 134 | } 135 | } 136 | return res 137 | } 138 | 139 | //====================================================================== 140 | // Local Variables: 141 | // mode: Go 142 | // fill-column: 78 143 | // End: 144 | -------------------------------------------------------------------------------- /pkg/pcap/pdml.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package pcap 6 | 7 | import ( 8 | "bytes" 9 | "compress/gzip" 10 | "encoding/gob" 11 | "encoding/xml" 12 | "io" 13 | 14 | "github.com/mreiferson/go-snappystream" 15 | ) 16 | 17 | //====================================================================== 18 | 19 | type IPdmlPacket interface { 20 | Packet() PdmlPacket 21 | } 22 | 23 | type PdmlPacket struct { 24 | XMLName xml.Name `xml:"packet"` 25 | Content []byte `xml:",innerxml"` 26 | } 27 | 28 | var _ IPdmlPacket = PdmlPacket{} 29 | 30 | func (p PdmlPacket) Packet() PdmlPacket { 31 | return p 32 | } 33 | 34 | //====================================================================== 35 | 36 | type GzippedPdmlPacket struct { 37 | Data bytes.Buffer 38 | } 39 | 40 | var _ IPdmlPacket = GzippedPdmlPacket{} 41 | 42 | func (p GzippedPdmlPacket) Packet() PdmlPacket { 43 | return p.Uncompress() 44 | } 45 | 46 | func (p GzippedPdmlPacket) Uncompress() PdmlPacket { 47 | greader, err := gzip.NewReader(&p.Data) 48 | if err != nil { 49 | panic(err) 50 | } 51 | decoder := gob.NewDecoder(greader) 52 | var res PdmlPacket 53 | err = decoder.Decode(&res) 54 | if err != nil { 55 | panic(err) 56 | } 57 | 58 | return res 59 | } 60 | 61 | func GzipPdmlPacket(p PdmlPacket) IPdmlPacket { 62 | res := GzippedPdmlPacket{} 63 | gwriter := gzip.NewWriter(&res.Data) 64 | encoder := gob.NewEncoder(gwriter) 65 | err := encoder.Encode(p) 66 | if err != nil { 67 | panic(err) 68 | } 69 | gwriter.Close() 70 | return res 71 | } 72 | 73 | //====================================================================== 74 | 75 | type SnappiedPdmlPacket struct { 76 | Data bytes.Buffer 77 | } 78 | 79 | var _ IPdmlPacket = SnappiedPdmlPacket{} 80 | 81 | func (p SnappiedPdmlPacket) Packet() PdmlPacket { 82 | return p.Uncompress() 83 | } 84 | 85 | func (p SnappiedPdmlPacket) Uncompress() PdmlPacket { 86 | var res PdmlPacket 87 | UnsnappyMe(&res, &p.Data) 88 | return res 89 | } 90 | 91 | func SnappyPdmlPacket(p PdmlPacket) IPdmlPacket { 92 | res := SnappiedPdmlPacket{} 93 | SnappyMe(p, &res.Data) 94 | return res 95 | } 96 | 97 | //====================================================================== 98 | 99 | // SnappyMe compresses the object within interface p to the 100 | // writer w. 101 | func SnappyMe(p interface{}, w io.Writer) { 102 | gwriter := snappystream.NewBufferedWriter(w) 103 | encoder := gob.NewEncoder(gwriter) 104 | if err := encoder.Encode(p); err != nil { 105 | panic(err) 106 | } 107 | gwriter.Close() 108 | } 109 | 110 | // UnsnappyMe decompresses from reader r into res. Afterwards, 111 | // res will be an interface whose type is a pointer to whatever 112 | // was serialized in the first place. 113 | func UnsnappyMe(res interface{}, r io.Reader) { 114 | greader := snappystream.NewReader(r, false) 115 | decoder := gob.NewDecoder(greader) 116 | if err := decoder.Decode(res); err != nil { 117 | panic(err) 118 | } 119 | } 120 | 121 | //====================================================================== 122 | // Local Variables: 123 | // mode: Go 124 | // fill-column: 78 125 | // End: 126 | -------------------------------------------------------------------------------- /pkg/pcap/source.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package pcap 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | "strings" 11 | 12 | "github.com/gcla/termshark/v2/pkg/system" 13 | ) 14 | 15 | //====================================================================== 16 | 17 | type IPacketSource interface { 18 | Name() string 19 | IsFile() bool 20 | IsInterface() bool 21 | IsFifo() bool 22 | IsPipe() bool 23 | } 24 | 25 | //====================================================================== 26 | 27 | func FileSystemSources(srcs []IPacketSource) []IPacketSource { 28 | res := make([]IPacketSource, 0) 29 | for _, src := range srcs { 30 | if src.IsFile() { 31 | res = append(res, src) 32 | } 33 | } 34 | return res 35 | } 36 | 37 | func SourcesString(srcs []IPacketSource) string { 38 | return strings.Join(SourcesNames(srcs), " + ") 39 | } 40 | 41 | func SourcesNames(srcs []IPacketSource) []string { 42 | names := make([]string, 0, len(srcs)) 43 | for _, psrc := range srcs { 44 | names = append(names, psrc.Name()) 45 | } 46 | return names 47 | } 48 | 49 | func UIName(src IPacketSource) string { 50 | if src.IsPipe() { 51 | return "" 52 | } else { 53 | return src.Name() 54 | } 55 | } 56 | 57 | func CanRestart(src IPacketSource) bool { 58 | return src.IsFile() || src.IsInterface() 59 | } 60 | 61 | //====================================================================== 62 | 63 | type FileSource struct { 64 | Filename string 65 | } 66 | 67 | var _ IPacketSource = FileSource{} 68 | 69 | func (p FileSource) Name() string { 70 | return p.Filename 71 | } 72 | 73 | func (p FileSource) IsFile() bool { 74 | return true 75 | } 76 | 77 | func (p FileSource) IsInterface() bool { 78 | return false 79 | } 80 | 81 | func (p FileSource) IsFifo() bool { 82 | return false 83 | } 84 | 85 | func (p FileSource) IsPipe() bool { 86 | return false 87 | } 88 | 89 | func (p FileSource) String() string { 90 | return fmt.Sprintf("File:%s", p.Filename) 91 | } 92 | 93 | //====================================================================== 94 | 95 | type TemporaryFileSource struct { 96 | FileSource 97 | } 98 | 99 | type ISourceRemover interface { 100 | Remove() error 101 | } 102 | 103 | func (p TemporaryFileSource) Remove() error { 104 | return os.Remove(p.Filename) 105 | } 106 | 107 | func (p TemporaryFileSource) String() string { 108 | return fmt.Sprintf("TempFile:%s", p.Filename) 109 | } 110 | 111 | //====================================================================== 112 | 113 | type InterfaceSource struct { 114 | Iface string 115 | } 116 | 117 | var _ IPacketSource = InterfaceSource{} 118 | 119 | func (p InterfaceSource) Name() string { 120 | return p.Iface 121 | } 122 | 123 | func (p InterfaceSource) IsFile() bool { 124 | return false 125 | } 126 | 127 | func (p InterfaceSource) IsInterface() bool { 128 | return true 129 | } 130 | 131 | func (p InterfaceSource) IsFifo() bool { 132 | return false 133 | } 134 | 135 | func (p InterfaceSource) IsPipe() bool { 136 | return false 137 | } 138 | 139 | func (p InterfaceSource) String() string { 140 | return fmt.Sprintf("Interface:%s", p.Iface) 141 | } 142 | 143 | //====================================================================== 144 | 145 | type FifoSource struct { 146 | Filename string 147 | } 148 | 149 | var _ IPacketSource = FifoSource{} 150 | 151 | func (p FifoSource) Name() string { 152 | return p.Filename 153 | } 154 | 155 | func (p FifoSource) IsFile() bool { 156 | return false 157 | } 158 | 159 | func (p FifoSource) IsInterface() bool { 160 | return false 161 | } 162 | 163 | func (p FifoSource) IsFifo() bool { 164 | return true 165 | } 166 | 167 | func (p FifoSource) IsPipe() bool { 168 | return false 169 | } 170 | 171 | func (p FifoSource) String() string { 172 | return fmt.Sprintf("Fifo:%s", p.Filename) 173 | } 174 | 175 | //====================================================================== 176 | 177 | type PipeSource struct { 178 | Descriptor string 179 | Fd int 180 | } 181 | 182 | var _ IPacketSource = PipeSource{} 183 | 184 | func (p PipeSource) Name() string { 185 | return p.Descriptor 186 | } 187 | 188 | func (p PipeSource) IsFile() bool { 189 | return false 190 | } 191 | 192 | func (p PipeSource) IsInterface() bool { 193 | return false 194 | } 195 | 196 | func (p PipeSource) IsFifo() bool { 197 | return false 198 | } 199 | 200 | func (p PipeSource) IsPipe() bool { 201 | return true 202 | } 203 | 204 | func (p PipeSource) Close() error { 205 | system.CloseDescriptor(p.Fd) 206 | return nil 207 | } 208 | 209 | func (p PipeSource) String() string { 210 | return fmt.Sprintf("Pipe:%s(%d)", p.Descriptor, p.Fd) 211 | } 212 | 213 | //====================================================================== 214 | // Local Variables: 215 | // mode: Go 216 | // fill-column: 78 217 | // End: 218 | -------------------------------------------------------------------------------- /pkg/pcap/testdata/1.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcla/termshark/e8a1ec6e4f2d517f646d3bba35a2ec4926e0202c/pkg/pcap/testdata/1.pcap -------------------------------------------------------------------------------- /pkg/pcap/testdata/2.hexdump-body: -------------------------------------------------------------------------------- 1 | 0000 10 40 00 20 35 01 2b 59 00 06 29 17 93 f8 aa aa .@. 5.+Y..)..... 2 | 0010 03 00 00 00 08 00 45 00 00 37 f9 39 00 00 40 11 ......E..7.9..@. 3 | 0020 a6 db c0 a8 2c 7b c0 a8 2c d5 f9 39 00 45 00 23 ....,{..,..9.E.# 4 | 0030 8d 73 00 01 43 3a 5c 49 42 4d 54 43 50 49 50 5c .s..C:\IBMTCPIP\ 5 | 0040 6c 63 63 6d 2e 31 00 6f 63 74 65 74 00 lccm.1.octet. 6 | 7 | -------------------------------------------------------------------------------- /pkg/pcap/testdata/2.hexdump-footer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcla/termshark/e8a1ec6e4f2d517f646d3bba35a2ec4926e0202c/pkg/pcap/testdata/2.hexdump-footer -------------------------------------------------------------------------------- /pkg/pcap/testdata/2.hexdump-header: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcla/termshark/e8a1ec6e4f2d517f646d3bba35a2ec4926e0202c/pkg/pcap/testdata/2.hexdump-header -------------------------------------------------------------------------------- /pkg/pcap/testdata/2.pcap-body: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcla/termshark/e8a1ec6e4f2d517f646d3bba35a2ec4926e0202c/pkg/pcap/testdata/2.pcap-body -------------------------------------------------------------------------------- /pkg/pcap/testdata/2.pcap-footer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcla/termshark/e8a1ec6e4f2d517f646d3bba35a2ec4926e0202c/pkg/pcap/testdata/2.pcap-footer -------------------------------------------------------------------------------- /pkg/pcap/testdata/2.pcap-header: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcla/termshark/e8a1ec6e4f2d517f646d3bba35a2ec4926e0202c/pkg/pcap/testdata/2.pcap-header -------------------------------------------------------------------------------- /pkg/pcap/testdata/2.pdml-footer: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pkg/pcap/testdata/2.pdml-header: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pkg/pcap/testdata/2.psml-body: -------------------------------------------------------------------------------- 1 | 2 |
1
3 |
0.000000
4 |
192.168.44.123
5 |
192.168.44.213
6 |
TFTP
7 |
77
8 |
Read Request, File: C:\IBMTCPIP\lccm.1, Transfer type: octet
9 |
10 | -------------------------------------------------------------------------------- /pkg/pcap/testdata/2.psml-footer: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /pkg/pcap/testdata/2.psml-header: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
No.
5 |
Time
6 |
Source
7 |
Destination
8 |
Protocol
9 |
Length
10 |
Info
11 |
12 | 13 | -------------------------------------------------------------------------------- /pkg/pcap/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package pcap 6 | 7 | import ( 8 | "github.com/gcla/gowid/gwutil" 9 | ) 10 | 11 | //====================================================================== 12 | 13 | type averageTracker struct { 14 | count uint64 15 | total uint64 16 | } 17 | 18 | func (a averageTracker) average() gwutil.IntOption { 19 | if a.count == 0 { 20 | return gwutil.NoneInt() 21 | } 22 | return gwutil.SomeInt(int(a.total / a.count)) 23 | } 24 | 25 | func (a *averageTracker) update(more int) { 26 | a.count += 1 27 | a.total += uint64(more) 28 | } 29 | 30 | type maxTracker struct { 31 | cur int 32 | } 33 | 34 | func (a maxTracker) max() int { 35 | return a.cur 36 | } 37 | 38 | func (a *maxTracker) update(candidate int) { 39 | if candidate > a.cur { 40 | a.cur = candidate 41 | } 42 | } 43 | 44 | //====================================================================== 45 | // Local Variables: 46 | // mode: Go 47 | // fill-column: 78 48 | // End: 49 | -------------------------------------------------------------------------------- /pkg/psmlmodel/model.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package psmlmodel 6 | 7 | import ( 8 | "sort" 9 | 10 | "github.com/gcla/gowid" 11 | "github.com/gcla/gowid/widgets/button" 12 | "github.com/gcla/gowid/widgets/columns" 13 | "github.com/gcla/gowid/widgets/holder" 14 | "github.com/gcla/gowid/widgets/isselected" 15 | "github.com/gcla/gowid/widgets/styled" 16 | "github.com/gcla/gowid/widgets/table" 17 | "github.com/gcla/gowid/widgets/text" 18 | "github.com/gcla/termshark/v2/widgets/expander" 19 | ) 20 | 21 | //====================================================================== 22 | 23 | // Model is a table model that provides a widget that will render 24 | // in one row only when not selected. 25 | type Model struct { 26 | *table.SimpleModel 27 | styler gowid.ICellStyler 28 | } 29 | 30 | func New(m *table.SimpleModel, st gowid.ICellStyler) *Model { 31 | return &Model{ 32 | SimpleModel: m, 33 | styler: st, 34 | } 35 | } 36 | 37 | // Provides the ith "cell" widget, upstream makes the "row" 38 | func (c *Model) CellWidget(i int, s string) gowid.IWidget { 39 | w := table.SimpleCellWidget(c, i, s) 40 | if w != nil { 41 | w = expander.New(w) 42 | } 43 | return w 44 | } 45 | 46 | func (c *Model) CellWidgets(row table.RowId) []gowid.IWidget { 47 | return table.SimpleCellWidgets(c, row) 48 | } 49 | 50 | // table.ITable2 51 | func (c *Model) HeaderWidget(ws []gowid.IWidget, focus int) gowid.IWidget { 52 | hws := c.HeaderWidgets() 53 | hw := c.SimpleModel.HeaderWidget(hws, focus).(*columns.Widget) 54 | hw2 := isselected.NewExt( 55 | hw, 56 | styled.New(hw, c.styler), 57 | styled.New(hw, c.styler), 58 | ) 59 | return hw2 60 | } 61 | 62 | func (c *Model) HeaderWidgets() []gowid.IWidget { 63 | var res []gowid.IWidget 64 | if c.Headers != nil { 65 | 66 | res = make([]gowid.IWidget, 0, len(c.Headers)) 67 | bhs := make([]*holder.Widget, len(c.Headers)) 68 | bms := make([]*button.Widget, len(c.Headers)) 69 | for i, s := range c.Headers { 70 | i2 := i 71 | var all, label gowid.IWidget 72 | label = text.New(s + " ") 73 | 74 | sorters := c.Comparators 75 | if sorters != nil { 76 | sorteri := sorters[i2] 77 | if sorteri != nil { 78 | bmid := button.NewBare(text.New("-")) 79 | bfor := button.NewBare(text.New("^")) 80 | brev := button.NewBare(text.New("v")) 81 | bh := holder.New(bmid) 82 | bhs[i] = bh 83 | bms[i] = bmid 84 | 85 | action := func(rev bool, next *button.Widget, app gowid.IApp) { 86 | sorter := &table.SimpleTableByColumn{ 87 | SimpleModel: c.SimpleModel, 88 | Column: i2, 89 | } 90 | if rev { 91 | sort.Sort(sort.Reverse(sorter)) 92 | } else { 93 | sort.Sort(sorter) 94 | } 95 | bh.SetSubWidget(next, app) 96 | for j, bhj := range bhs { 97 | if j != i2 { 98 | bhj.SetSubWidget(bms[j], app) 99 | } 100 | } 101 | } 102 | 103 | bmid.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { 104 | action(false, bfor, app) 105 | })) 106 | 107 | bfor.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { 108 | action(true, brev, app) 109 | })) 110 | 111 | brev.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { 112 | action(false, bfor, app) 113 | })) 114 | 115 | all = columns.NewFixed(label, styled.NewFocus(bh, gowid.MakeStyledAs(gowid.StyleReverse))) 116 | } 117 | } 118 | var w gowid.IWidget 119 | if c.Style.HeaderStyleProvided { 120 | w = isselected.New( 121 | styled.New( 122 | all, 123 | c.GetStyle().HeaderStyleNoFocus, 124 | ), 125 | styled.New( 126 | all, 127 | c.GetStyle().HeaderStyleSelected, 128 | ), 129 | styled.New( 130 | all, 131 | c.GetStyle().HeaderStyleFocus, 132 | ), 133 | ) 134 | } else { 135 | w = styled.NewExt( 136 | all, 137 | nil, 138 | gowid.MakeStyledAs(gowid.StyleReverse), 139 | ) 140 | } 141 | res = append(res, w) 142 | } 143 | } 144 | return res 145 | } 146 | 147 | //====================================================================== 148 | // Local Variables: 149 | // mode: Go 150 | // fill-column: 110 151 | // End: 152 | -------------------------------------------------------------------------------- /pkg/shark/columnformat_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license 2 | // that can be found in the LICENSE file. 3 | 4 | package shark 5 | 6 | import ( 7 | "fmt" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | //====================================================================== 14 | 15 | func TestCF1(t *testing.T) { 16 | 17 | fields := &ColumnsFromTshark{} 18 | err := fields.InitNoCache() 19 | assert.NoError(t, err) 20 | 21 | cfmap := make(map[string]PsmlColumnSpec) 22 | for _, f := range fields.fields { 23 | fmt.Printf("GCLA: adding %v\n", f) 24 | cfmap[f.Field.Token] = f 25 | } 26 | 27 | m1, ok := cfmap["%At"] 28 | assert.Equal(t, true, ok) 29 | assert.Equal(t, "Absolute time", m1.Name) 30 | 31 | m2, ok := cfmap["%rs"] 32 | assert.Equal(t, true, ok) 33 | assert.Equal(t, "Src addr (resolved)", m2.Name) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/shark/wiresharkcfg/cfg.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package wiresharkcfg 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | "path" 11 | "strings" 12 | 13 | homedir "github.com/mitchellh/go-homedir" 14 | "github.com/shibukawa/configdir" 15 | ) 16 | 17 | //====================================================================== 18 | 19 | var NotFoundError = fmt.Errorf("Could not find wireshark preferences") 20 | var NotParsedError = fmt.Errorf("Could not parse wireshark preferences") 21 | 22 | type Config struct { 23 | Strings map[string]string 24 | Lists map[string][]string 25 | } 26 | 27 | func NewDefault() (*Config, error) { 28 | // See https://www.wireshark.org/docs/wsug_html_chunked/ChAppFilesConfigurationSection.html 29 | // Wireshark had a ~/.wireshark directory before adopting XDG 30 | tryXDG := true 31 | cpath, err := homedir.Expand("~/.wireshark/preferences") 32 | if err == nil { 33 | _, err = os.Stat(cpath) 34 | if err == nil { 35 | tryXDG = false 36 | } 37 | } 38 | if tryXDG { 39 | stdConf := configdir.New("", "wireshark") 40 | dirs := stdConf.QueryFolders(configdir.All) 41 | cpath = path.Join(dirs[0].Path, "preferences") 42 | _, err = os.Stat(cpath) 43 | if os.IsNotExist(err) { 44 | return nil, err 45 | } 46 | } 47 | 48 | res := &Config{} 49 | err = res.PopulateFrom(cpath) 50 | return res, err 51 | } 52 | 53 | func (c *Config) PopulateFrom(filename string) error { 54 | file, err := os.Open(filename) 55 | if err != nil { 56 | return err 57 | } 58 | parsed, err := ParseReader("", file) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | *c = *(parsed.(*Config)) 64 | return nil 65 | } 66 | 67 | func (c *Config) GetList(key string) []string { 68 | if c == nil { 69 | return nil 70 | } 71 | if val, ok := c.Lists[key]; ok { 72 | return val 73 | } 74 | return nil 75 | } 76 | 77 | func (c *Config) ColumnFormat() []string { 78 | return c.GetList("gui.column.format") 79 | } 80 | 81 | func (c *Config) merge(other *Config) { 82 | for k, v := range other.Strings { 83 | c.Strings[k] = v 84 | } 85 | for k, v := range other.Lists { 86 | c.Lists[k] = v 87 | } 88 | } 89 | 90 | func (c *Config) String() string { 91 | res := make([]string, 0, len(c.Strings)+len(c.Lists)) 92 | for k, v := range c.Strings { 93 | res = append(res, fmt.Sprintf("%s: %s", k, v)) 94 | } 95 | for k, v := range c.Lists { 96 | v2 := strings.Join(v, ", ") 97 | res = append(res, fmt.Sprintf("%s: %s", k, v2)) 98 | } 99 | return strings.Join(res, "\n") 100 | } 101 | 102 | //====================================================================== 103 | // Local Variables: 104 | // mode: Go 105 | // fill-column: 110 106 | // End: 107 | -------------------------------------------------------------------------------- /pkg/shark/wiresharkcfg/parser.peg: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | // 5 | // This peg file should be compiled with something like this: 6 | // 7 | // go get github.com/mna/pigeon@f3db42a 8 | // cd termshark/share/wiresharkcfg/ 9 | // pigeon parser.peg > parser.go 10 | // 11 | 12 | { 13 | package wiresharkcfg 14 | 15 | import ( 16 | "io" 17 | "unicode" 18 | "strings" 19 | "os" 20 | "fmt" 21 | "strconv" 22 | "errors" 23 | "io/ioutil" 24 | "bytes" 25 | "unicode/utf8" 26 | log "github.com/sirupsen/logrus" 27 | ) 28 | 29 | } 30 | 31 | // 32 | // Parse input that looks roughly like: 33 | // 34 | // # Packet list hidden columns 35 | // # List all columns to hide in the packet list. 36 | // gui.column.hidden: 37 | // 38 | // # Packet list column format 39 | // # Each pair of strings consists of a column title and its format 40 | // gui.column.format: 41 | // "No.", "%m", 42 | // "gcla3", "%Yt", 43 | // "Time", "%t", 44 | // "Source", "%s", 45 | // "Destination", "%d", 46 | // "Protocol", "%p", 47 | // "Length", "%L", 48 | // "Info", "%i", 49 | // "gcla", "%V", 50 | // "gcla2", "%B", 51 | // "utc", "%Aut" 52 | // 53 | // ####### User Interface: Font ######## 54 | // 55 | // # Font name for packet list, protocol tree, and hex dump panes. (Qt) 56 | // # A string 57 | // gui.qt.font_name: Liberation Mono,11,-1,5,50,0,0,0,0,0 58 | // ... 59 | 60 | Input <- e:OneEntry es:OneEntry* { 61 | res := e.(*Config) 62 | for _, e2 := range es.([]interface{}) { 63 | e2.(*Config).merge(res) 64 | res = e2.(*Config) 65 | } 66 | return res, nil 67 | } 68 | 69 | OneEntry <- sv:(ListKeyValue / StringKeyValue) { 70 | return sv.(*Config), nil 71 | } 72 | 73 | Comment <- _nl+ / (_nl* "#" [^\r\n]* _nl) #{ 74 | return nil 75 | } 76 | 77 | StringKeyValue <- Comment* k:StringKey ":" v:StringValue _nl _nl { 78 | strs := make(map[string]string) 79 | strs[k.(string)] = v.(string) 80 | res := &Config{ 81 | Lists: make(map[string][]string), 82 | Strings: strs, 83 | } 84 | return res, nil 85 | } 86 | 87 | StringKey <- [a-zA-z0-9._]+ { 88 | return string(c.text), nil 89 | } 90 | 91 | StringValue <- [^\n\t\r]+ { 92 | return string(c.text), nil 93 | } 94 | 95 | ListKeyValue <- Comment* k:ListKey ':' _ lv:ListValue { 96 | lists := make(map[string][]string) 97 | lists[string(k.([]uint8))] = lv.([]string) 98 | res := &Config{ 99 | Lists: lists, 100 | Strings: make(map[string]string), 101 | } 102 | return res, nil 103 | } 104 | 105 | ListKey <- "gui.column.format" / "gui.column.hidden" { 106 | return c.text, nil 107 | } 108 | 109 | ListItem <- [^\n\t\r,]+ { 110 | return string(c.text), nil 111 | } 112 | 113 | ListValue <- li:ListItem _ lv:( ',' _ ListValue )* { 114 | res := make([]string, 0) 115 | res = append(res, li.(string)) 116 | noneOrSome := lv.([]interface{}) 117 | if len(noneOrSome) > 0 { 118 | some := noneOrSome[0].([]interface{}) 119 | // [[44] [] [[...]]] 120 | if len(some) == 3 { 121 | vals := some[2].([]string) 122 | res = append(res, vals...) 123 | } 124 | } 125 | return res, nil 126 | } 127 | 128 | _nl "newline" <- [\r]?[\n] 129 | 130 | _ "whitespace" <- [ \n\t\r]* 131 | 132 | __ "mandatory whitespace" <- [ \n\t\r]+ 133 | 134 | EOF <- !. 135 | -------------------------------------------------------------------------------- /pkg/streams/parse.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package streams 6 | 7 | import ( 8 | "encoding/hex" 9 | "fmt" 10 | "strings" 11 | ) 12 | 13 | //====================================================================== 14 | 15 | type IChunk interface { 16 | Direction() Direction 17 | StreamData() []byte 18 | } 19 | 20 | type IOnStreamChunk interface { 21 | OnStreamChunk(chunk IChunk) 22 | } 23 | 24 | type IOnStreamHeader interface { 25 | OnStreamHeader(header FollowHeader) 26 | } 27 | 28 | //====================================================================== 29 | 30 | type parseContext interface { 31 | Err() error 32 | } 33 | 34 | type StreamParseError struct{} 35 | 36 | func (e StreamParseError) Error() string { 37 | return "Stream reassembly parse error" 38 | } 39 | 40 | var _ error = StreamParseError{} 41 | 42 | //====================================================================== 43 | 44 | type Protocol int 45 | 46 | const ( 47 | Unspecified Protocol = 0 48 | TCP Protocol = iota 49 | UDP Protocol = iota 50 | ) 51 | 52 | var _ fmt.Stringer = Protocol(0) 53 | 54 | func (p Protocol) String() string { 55 | switch p { 56 | case Unspecified: 57 | return "Unspecified" 58 | case TCP: 59 | return "TCP" 60 | case UDP: 61 | return "UDP" 62 | default: 63 | panic(nil) 64 | } 65 | } 66 | 67 | //====================================================================== 68 | 69 | type Direction int 70 | 71 | const ( 72 | Client Direction = 0 73 | Server Direction = iota 74 | ) 75 | 76 | func (d Direction) String() string { 77 | switch d { 78 | case Client: 79 | return "Client" 80 | case Server: 81 | return "Server" 82 | default: 83 | return "Unknown!" 84 | } 85 | } 86 | 87 | //====================================================================== 88 | 89 | type Bytes struct { 90 | Dirn Direction 91 | Data []byte 92 | } 93 | 94 | var _ fmt.Stringer = Bytes{} 95 | var _ IChunk = Bytes{} 96 | 97 | func (b Bytes) Direction() Direction { 98 | return b.Dirn 99 | } 100 | 101 | func (b Bytes) StreamData() []byte { 102 | return b.Data 103 | } 104 | 105 | func (b Bytes) String() string { 106 | return fmt.Sprintf("Direction: %v\n%s", b.Dirn, hex.Dump(b.Data)) 107 | } 108 | 109 | //====================================================================== 110 | 111 | type FollowHeader struct { 112 | Follow string 113 | Filter string 114 | Node0 string 115 | Node1 string 116 | } 117 | 118 | func (h FollowHeader) String() string { 119 | return fmt.Sprintf("[client:%s server:%s follow:%s filter:%s]", h.Node0, h.Node1, h.Follow, h.Filter) 120 | } 121 | 122 | type FollowStream struct { 123 | FollowHeader 124 | Bytes []Bytes 125 | } 126 | 127 | var _ fmt.Stringer = FollowStream{} 128 | 129 | func (f FollowStream) String() string { 130 | datastrs := make([]string, 0, len(f.Bytes)) 131 | for _, b := range f.Bytes { 132 | datastrs = append(datastrs, b.String()) 133 | } 134 | data := strings.Join(datastrs, "\n") 135 | return fmt.Sprintf("Follow: %s\nFilter: %s\nNode0: %s\nNode1: %s\nData:\n%s", f.Follow, f.Filter, f.Node0, f.Node1, data) 136 | } 137 | 138 | //====================================================================== 139 | // Local Variables: 140 | // mode: Go 141 | // fill-column: 110 142 | // End: 143 | -------------------------------------------------------------------------------- /pkg/summary/summary.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package summary 6 | 7 | import ( 8 | "bufio" 9 | "io" 10 | "sync" 11 | 12 | "github.com/gcla/termshark/v2" 13 | ) 14 | 15 | //====================================================================== 16 | 17 | // Reader maintains the first two and last two lines read from a source io.Reader. 18 | // At any point, the Summary() function can be called to extract a summary of 19 | // what's been read so far. I'm using this to create a summary of the stderr of 20 | // a termshark command. 21 | type Reader struct { 22 | source io.Reader 23 | first *string 24 | second *string 25 | penultimate *string 26 | last *string 27 | num int 28 | lock sync.Mutex 29 | } 30 | 31 | func New(source io.Reader) *Reader { 32 | res := &Reader{ 33 | source: source, 34 | } 35 | 36 | termshark.TrackedGo(func() { 37 | res.start() 38 | }, Goroutinewg) 39 | 40 | return res 41 | } 42 | 43 | func (h *Reader) Summary() []string { 44 | h.lock.Lock() 45 | defer h.lock.Unlock() 46 | 47 | res := make([]string, 0, 5) 48 | if h.num >= 1 { 49 | res = append(res, *h.first) 50 | } 51 | if h.num >= 2 { 52 | res = append(res, *h.second) 53 | } 54 | if h.num >= 5 { 55 | res = append(res, "...") 56 | } 57 | if h.num >= 4 { 58 | res = append(res, *h.penultimate) 59 | } 60 | if h.num >= 3 { 61 | res = append(res, *h.last) 62 | } 63 | 64 | return res 65 | } 66 | 67 | func (h *Reader) start() { 68 | scanner := bufio.NewScanner(h.source) 69 | for scanner.Scan() { 70 | line := scanner.Text() 71 | h.lock.Lock() 72 | h.num += 1 73 | if h.first == nil { 74 | h.first = &line 75 | } else if h.second == nil { 76 | h.second = &line 77 | } 78 | 79 | h.penultimate = h.last 80 | h.last = &line 81 | h.lock.Unlock() 82 | } 83 | } 84 | 85 | //====================================================================== 86 | 87 | // This is a debugging aid - I use it to ensure goroutines stop as expected. If they don't 88 | // the main program will hang at termination. 89 | var Goroutinewg *sync.WaitGroup 90 | 91 | //====================================================================== 92 | // Local Variables: 93 | // mode: Go 94 | // fill-column: 78 95 | // End: 96 | -------------------------------------------------------------------------------- /pkg/system/dumpcapext.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // +build !windows 6 | // +build !darwin 7 | 8 | package system 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | "os/exec" 14 | "regexp" 15 | "strconv" 16 | "syscall" 17 | ) 18 | 19 | //====================================================================== 20 | 21 | var fdre *regexp.Regexp = regexp.MustCompile(`/dev/fd/([[:digit:]]+)`) 22 | 23 | // DumpcapExt will run dumpcap first, but if it fails, run tshark. Intended as 24 | // a special case to allow termshark -i to use dumpcap if possible, 25 | // but if it fails (e.g. iface==randpkt), fall back to tshark. dumpcap is more 26 | // efficient than tshark at just capturing, and will drop fewer packets, but 27 | // tshark supports extcap interfaces. 28 | func DumpcapExt(dumpcapBin string, tsharkBin string, args ...string) error { 29 | var err error 30 | 31 | // If the first argument is /dev/fd/X, it means the process should have 32 | // descriptor X open and will expect packet data to be readable on it. 33 | // This /dev/fd feature does not work on tshark when run on freebsd, meaning 34 | // tshark will fail if you do something like 35 | // 36 | // cat foo.pcap | tshark -r /dev/fd/0 37 | // 38 | // The fix here is to replace /dev/fd/X with the arg "-", which tshark will 39 | // interpret as stdin, then dup descriptor X to 0 before starting dumpcap/tshark 40 | // 41 | if len(args) >= 2 { 42 | if os.Getenv("TERMSHARK_REPLACE_DEVFD") != "0" { 43 | fdnum := fdre.FindStringSubmatch(args[1]) 44 | if len(fdnum) == 2 { 45 | fd, err := strconv.Atoi(fdnum[1]) 46 | if err != nil { 47 | fmt.Fprintf(os.Stderr, "Unexpected error parsing %s: %v\n", args[1], err) 48 | } else { 49 | err = Dup2(fd, 0) 50 | if err != nil { 51 | fmt.Fprintf(os.Stderr, "Problem duplicating fd %d to 0: %v\n", fd, err) 52 | fmt.Fprintf(os.Stderr, "Will not try to replace argument %s to tshark\n", args[1]) 53 | } else { 54 | fmt.Fprintf(os.Stderr, "Replacing argument %s with - for tshark compatibility\n", args[1]) 55 | args[1] = "-" 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | fmt.Fprintf(os.Stderr, "Starting termshark's custom live capture procedure.\n") 63 | dumpcapCmd := exec.Command(dumpcapBin, args...) 64 | fmt.Fprintf(os.Stderr, "Trying dumpcap command %v\n", dumpcapCmd) 65 | dumpcapCmd.Stdin = os.Stdin 66 | dumpcapCmd.Stdout = os.Stdout 67 | dumpcapCmd.Stderr = os.Stderr 68 | if dumpcapCmd.Run() != nil { 69 | var tshark string 70 | tshark, err = exec.LookPath(tsharkBin) 71 | if err == nil { 72 | fmt.Fprintf(os.Stderr, "Retrying with capture command %v\n", append([]string{tshark}, args...)) 73 | err = syscall.Exec(tshark, append([]string{tshark}, args...), os.Environ()) 74 | } 75 | } 76 | 77 | return err 78 | } 79 | -------------------------------------------------------------------------------- /pkg/system/dumpcapext_arm64.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // +build !darwin 6 | // +build !linux 7 | 8 | package system 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | "os/exec" 14 | "syscall" 15 | ) 16 | 17 | //====================================================================== 18 | 19 | // DumpcapExt will run dumpcap first, but if it fails, run tshark. Intended as 20 | // a special case to allow termshark -i to use dumpcap if possible, 21 | // but if it fails (e.g. iface==randpkt), fall back to tshark. dumpcap is more 22 | // efficient than tshark at just capturing, and will drop fewer packets, but 23 | // tshark supports extcap interfaces. 24 | func DumpcapExt(dumpcapBin string, tsharkBin string, args ...string) error { 25 | var err error 26 | 27 | dumpcapCmd := exec.Command(dumpcapBin, args...) 28 | fmt.Fprintf(os.Stderr, "Starting termshark's custom live capture procedure.\n") 29 | fmt.Fprintf(os.Stderr, "Trying dumpcap command %v\n", dumpcapCmd) 30 | dumpcapCmd.Stdin = os.Stdin 31 | dumpcapCmd.Stdout = os.Stdout 32 | dumpcapCmd.Stderr = os.Stderr 33 | if dumpcapCmd.Run() != nil { 34 | var tshark string 35 | tshark, err = exec.LookPath(tsharkBin) 36 | if err == nil { 37 | fmt.Fprintf(os.Stderr, "Retrying with dumpcap command %v\n", append([]string{tshark}, args...)) 38 | err = syscall.Exec(tshark, append([]string{tshark}, args...), os.Environ()) 39 | } 40 | } 41 | 42 | return err 43 | } 44 | -------------------------------------------------------------------------------- /pkg/system/dumpcapext_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package system 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | "os/exec" 11 | "syscall" 12 | ) 13 | 14 | //====================================================================== 15 | 16 | // DumpcapExt will run dumpcap first, but if it fails, run tshark. Intended as 17 | // a special case to allow termshark -i to use dumpcap if possible, 18 | // but if it fails (e.g. iface==randpkt), fall back to tshark. dumpcap is more 19 | // efficient than tshark at just capturing, and will drop fewer packets, but 20 | // tshark supports extcap interfaces. 21 | func DumpcapExt(dumpcapBin string, tsharkBin string, args ...string) error { 22 | var err error 23 | 24 | dumpcapCmd := exec.Command(dumpcapBin, args...) 25 | fmt.Fprintf(os.Stderr, "Starting termshark's custom live capture procedure.\n") 26 | fmt.Fprintf(os.Stderr, "Trying dumpcap command %v\n", dumpcapCmd) 27 | dumpcapCmd.Stdin = os.Stdin 28 | dumpcapCmd.Stdout = os.Stdout 29 | dumpcapCmd.Stderr = os.Stderr 30 | if dumpcapCmd.Run() != nil { 31 | var tshark string 32 | tshark, err = exec.LookPath(tsharkBin) 33 | if err == nil { 34 | fmt.Fprintf(os.Stderr, "Retrying with dumpcap command %v\n", append([]string{tshark}, args...)) 35 | err = syscall.Exec(tshark, append([]string{tshark}, args...), os.Environ()) 36 | } 37 | } 38 | 39 | return err 40 | } 41 | -------------------------------------------------------------------------------- /pkg/system/dumpcapext_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package system 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | "os/exec" 11 | ) 12 | 13 | //====================================================================== 14 | 15 | // DumpcapExt will run dumpcap first, but if it fails, run tshark. Intended as 16 | // a special case to allow termshark -i to use dumpcap if possible, 17 | // but if it fails (e.g. iface==randpkt), fall back to tshark. dumpcap is more 18 | // efficient than tshark at just capturing, and will drop fewer packets, but 19 | // tshark supports extcap interfaces. 20 | func DumpcapExt(dumpcapBin string, tsharkBin string, args ...string) error { 21 | dumpcapCmd := exec.Command(dumpcapBin, args...) 22 | fmt.Fprintf(os.Stderr, "Starting termshark's custom live capture procedure.\n") 23 | fmt.Fprintf(os.Stderr, "Trying dumpcap command %v\n", dumpcapCmd) 24 | dumpcapCmd.Stdin = os.Stdin 25 | dumpcapCmd.Stdout = os.Stdout 26 | dumpcapCmd.Stderr = os.Stderr 27 | if dumpcapCmd.Run() == nil { 28 | return nil 29 | } 30 | 31 | tsharkCmd := exec.Command(tsharkBin, args...) 32 | fmt.Fprintf(os.Stderr, "Retrying with dumpcap command %v\n", tsharkCmd) 33 | tsharkCmd.Stdin = os.Stdin 34 | tsharkCmd.Stdout = os.Stdout 35 | tsharkCmd.Stderr = os.Stderr 36 | return tsharkCmd.Run() 37 | } 38 | -------------------------------------------------------------------------------- /pkg/system/dup.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // +build !windows 6 | // +build !linux !arm64 7 | // +build !linux !riscv64 8 | 9 | package system 10 | 11 | import "syscall" 12 | 13 | func Dup2(fd int, fd2 int) error { 14 | return syscall.Dup2(fd, fd2) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/system/dup_linux_arm64.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package system 6 | 7 | import "syscall" 8 | 9 | func Dup2(fd int, fd2 int) error { 10 | return syscall.Dup3(fd, fd2, 0) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/system/dup_linux_riscv64.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package system 6 | 7 | import "syscall" 8 | 9 | func Dup2(fd int, fd2 int) error { 10 | return syscall.Dup3(fd, fd2, 0) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/system/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package system 6 | 7 | //====================================================================== 8 | 9 | type NotImplementedError struct{} 10 | 11 | var _ error = NotImplementedError{} 12 | 13 | func (e NotImplementedError) Error() string { 14 | return "Feature not implemented" 15 | } 16 | 17 | var NotImplemented = NotImplementedError{} 18 | 19 | //====================================================================== 20 | // Local Variables: 21 | // mode: Go 22 | // fill-column: 78 23 | // End: 24 | -------------------------------------------------------------------------------- /pkg/system/extcmds.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // +build !darwin,!android,!windows 6 | 7 | package system 8 | 9 | var CopyToClipboard = []string{"xsel", "-i", "-b"} 10 | 11 | var OpenURL = []string{"xdg-open"} 12 | -------------------------------------------------------------------------------- /pkg/system/extcmds_android.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package system 6 | 7 | var CopyToClipboard = []string{"termux-clipboard-set"} 8 | 9 | var OpenURL = []string{"am", "start", "-a", "android.intent.action.VIEW", "-d"} 10 | -------------------------------------------------------------------------------- /pkg/system/extcmds_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package system 6 | 7 | var CopyToClipboard = []string{"pbcopy"} 8 | 9 | var OpenURL = []string{"open"} 10 | -------------------------------------------------------------------------------- /pkg/system/extcmds_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package system 6 | 7 | var CopyToClipboard = []string{"clip"} 8 | 9 | var OpenURL = []string{"explorer"} 10 | -------------------------------------------------------------------------------- /pkg/system/fd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // +build !windows 6 | 7 | package system 8 | 9 | import ( 10 | "os" 11 | "syscall" 12 | ) 13 | 14 | //====================================================================== 15 | 16 | func CloseDescriptor(fd int) { 17 | syscall.Close(fd) 18 | } 19 | 20 | func FileRegularOrLink(filename string) bool { 21 | fi, err := os.Stat(filename) 22 | if err != nil { 23 | return false 24 | } 25 | 26 | return fi.Mode().IsRegular() || (fi.Mode()&os.ModeSymlink != 0) 27 | } 28 | 29 | //====================================================================== 30 | // Local Variables: 31 | // mode: Go 32 | // fill-column: 78 33 | // End: 34 | -------------------------------------------------------------------------------- /pkg/system/fd_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // +build windows 6 | 7 | package system 8 | 9 | func CloseDescriptor(fd int) { 10 | } 11 | 12 | func FileRegularOrLink(filename string) bool { 13 | return true 14 | } 15 | 16 | //====================================================================== 17 | // Local Variables: 18 | // mode: Go 19 | // fill-column: 78 20 | // End: 21 | -------------------------------------------------------------------------------- /pkg/system/fdinfo.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package system 6 | 7 | import ( 8 | "fmt" 9 | "io/ioutil" 10 | "os" 11 | "path/filepath" 12 | "regexp" 13 | "strconv" 14 | 15 | "github.com/gcla/gowid" 16 | ) 17 | 18 | //====================================================================== 19 | 20 | var re *regexp.Regexp = regexp.MustCompile(`^pos:\s*([0-9]+)`) 21 | 22 | var FileNotOpenError = fmt.Errorf("Could not find file among descriptors") 23 | var ParseError = fmt.Errorf("Could not match file position") 24 | 25 | // current, max 26 | func ProcessProgress(pid int, filename string) (int64, int64, error) { 27 | filename, err := filepath.EvalSymlinks(filename) 28 | if err != nil { 29 | return -1, -1, err 30 | } 31 | fi, err := os.Stat(filename) 32 | if err != nil { 33 | return -1, -1, err 34 | } 35 | finfo, err := ioutil.ReadDir(fmt.Sprintf("/proc/%d/fd", pid)) 36 | if err != nil { 37 | return -1, -1, err 38 | } 39 | fd := -1 40 | for _, f := range finfo { 41 | lname, err := os.Readlink(fmt.Sprintf("/proc/%d/fd/%s", pid, f.Name())) 42 | if err == nil && lname == filename { 43 | fd, _ = strconv.Atoi(f.Name()) 44 | break 45 | } 46 | } 47 | if fd == -1 { 48 | return -1, -1, gowid.WithKVs(FileNotOpenError, map[string]interface{}{"filename": filename}) 49 | } 50 | info, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/fdinfo/%d", pid, fd)) 51 | 52 | matches := re.FindStringSubmatch(string(info)) 53 | if len(matches) <= 1 { 54 | return -1, -1, gowid.WithKVs(ParseError, map[string]interface{}{"fdinfo": finfo}) 55 | } 56 | pos, err := strconv.ParseUint(matches[1], 10, 64) 57 | if err != nil { 58 | return -1, -1, err 59 | } 60 | return int64(pos), fi.Size(), nil 61 | } 62 | 63 | //====================================================================== 64 | // Local Variables: 65 | // mode: Go 66 | // fill-column: 78 67 | // End: 68 | -------------------------------------------------------------------------------- /pkg/system/have_fdinfo.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // +build !linux,!android 6 | 7 | package system 8 | 9 | const HaveFdinfo = false 10 | 11 | //====================================================================== 12 | // Local Variables: 13 | // mode: Go 14 | // fill-column: 110 15 | // End: 16 | -------------------------------------------------------------------------------- /pkg/system/have_fdinfo_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package system 6 | 7 | const HaveFdinfo = true 8 | 9 | //====================================================================== 10 | // Local Variables: 11 | // mode: Go 12 | // fill-column: 110 13 | // End: 14 | -------------------------------------------------------------------------------- /pkg/system/picker.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // +build !android 6 | 7 | package system 8 | 9 | import ( 10 | "fmt" 11 | ) 12 | 13 | var NoPicker error = fmt.Errorf("No file picker available") 14 | 15 | func PickFile() (string, error) { 16 | return "", NoPicker 17 | } 18 | 19 | func PickFileError(e string) error { 20 | fmt.Println(e) 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /pkg/system/picker_android.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package system 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | "os/exec" 11 | "path" 12 | 13 | fsnotify "gopkg.in/fsnotify/fsnotify.v1" 14 | ) 15 | 16 | var NoPicker error = fmt.Errorf("No file picker available") // not running on termux 17 | var NoTermuxApi error = fmt.Errorf("Could not launch file picker. Please install termux-api:\npkg install termux-api\n") 18 | 19 | func PickFile() (string, error) { 20 | tsdir := "/data/data/com.termux/files/home" 21 | tsfile := "termux" 22 | tsabs := path.Join(tsdir, tsfile) 23 | 24 | if err := os.Remove(tsabs); err != nil && !os.IsNotExist(err) { 25 | return "", fmt.Errorf("Could not remove previous temporary termux file %s: %v", tsabs, err) 26 | } 27 | 28 | if _, err := exec.Command("termux-storage-get", tsabs).Output(); err != nil { 29 | exerr, ok := err.(*exec.Error) 30 | if ok && (exerr.Err == exec.ErrNotFound) { 31 | return "", NoTermuxApi 32 | } else { 33 | return "", fmt.Errorf("Could not select input for termshark: %v", err) 34 | } 35 | } 36 | 37 | if iwatcher, err := fsnotify.NewWatcher(); err != nil { 38 | return "", fmt.Errorf("Could not start filesystem watcher: %v\n", err) 39 | } else { 40 | defer iwatcher.Close() 41 | 42 | if err := iwatcher.Add(tsdir); err != nil { //&& !os.IsNotExist(err) { 43 | return "", fmt.Errorf("Could not set up file watcher for %s: %v\n", tsfile, err) 44 | } 45 | 46 | // Don't time it - the user might be tied up with the file picker for a while. No real way to tell... 47 | //tmr := time.NewTimer(time.Duration(10000) * time.Millisecond) 48 | //defer tmr.Close() 49 | 50 | Loop: 51 | for { 52 | select { 53 | case we := <-iwatcher.Events: 54 | if path.Base(we.Name) == tsfile { 55 | break Loop 56 | } 57 | 58 | case err := <-iwatcher.Errors: 59 | return "", fmt.Errorf("File watcher error for %s: %v", tsfile, err) 60 | } 61 | } 62 | 63 | return tsabs, nil 64 | } 65 | } 66 | 67 | func PickFileError(e string) error { 68 | if _, err := exec.Command("termux-toast", e).Output(); err != nil { 69 | exerr, ok := err.(*exec.Error) 70 | if ok && (exerr.Err == exec.ErrNotFound) { 71 | return NoTermuxApi 72 | } else { 73 | return fmt.Errorf("Error running termux-toast: %v", err) 74 | } 75 | } 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /pkg/system/signals.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | // 5 | // +build !windows 6 | 7 | package system 8 | 9 | import ( 10 | "os" 11 | "os/signal" 12 | "syscall" 13 | ) 14 | 15 | //====================================================================== 16 | 17 | func RegisterForSignals(ch chan<- os.Signal) { 18 | signal.Notify(ch, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGTSTP, syscall.SIGCONT, syscall.SIGUSR1, syscall.SIGUSR2) 19 | } 20 | 21 | func IsSigUSR1(sig os.Signal) bool { 22 | return isUnixSig(sig, syscall.SIGUSR1) 23 | } 24 | 25 | func IsSigUSR2(sig os.Signal) bool { 26 | return isUnixSig(sig, syscall.SIGUSR2) 27 | } 28 | 29 | func IsSigTSTP(sig os.Signal) bool { 30 | return isUnixSig(sig, syscall.SIGTSTP) 31 | } 32 | 33 | func IsSigCont(sig os.Signal) bool { 34 | return isUnixSig(sig, syscall.SIGCONT) 35 | } 36 | 37 | func StopMyself() error { 38 | return syscall.Kill(syscall.Getpid(), syscall.SIGSTOP) 39 | } 40 | 41 | func isUnixSig(sig os.Signal, usig syscall.Signal) bool { 42 | if ssig, ok := sig.(syscall.Signal); ok && ssig == usig { 43 | return true 44 | } 45 | return false 46 | } 47 | 48 | //====================================================================== 49 | // Local Variables: 50 | // mode: Go 51 | // fill-column: 78 52 | // End: 53 | -------------------------------------------------------------------------------- /pkg/system/signals_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package system 6 | 7 | import ( 8 | "os" 9 | "os/signal" 10 | 11 | "github.com/gcla/gowid" 12 | ) 13 | 14 | //====================================================================== 15 | 16 | func RegisterForSignals(ch chan<- os.Signal) { 17 | signal.Notify(ch, os.Interrupt) 18 | } 19 | 20 | func IsSigUSR1(sig os.Signal) bool { 21 | return false 22 | } 23 | 24 | func IsSigUSR2(sig os.Signal) bool { 25 | return false 26 | } 27 | 28 | func IsSigTSTP(sig os.Signal) bool { 29 | return false 30 | } 31 | 32 | func IsSigCont(sig os.Signal) bool { 33 | return false 34 | } 35 | 36 | func StopMyself() error { 37 | return gowid.WithKVs(NotImplemented, map[string]interface{}{"feature": "SIGSTOP"}) 38 | } 39 | 40 | //====================================================================== 41 | // Local Variables: 42 | // mode: Go 43 | // fill-column: 78 44 | // End: 45 | -------------------------------------------------------------------------------- /pkg/tailfile/tailfile.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | //+build !windows 6 | 7 | package tailfile 8 | 9 | import ( 10 | "os" 11 | "os/exec" 12 | ) 13 | 14 | //====================================================================== 15 | 16 | func Tail(file string) error { 17 | cmd := exec.Command("tail", "-f", file) 18 | cmd.Stdout = os.Stdout 19 | return cmd.Run() 20 | } 21 | 22 | //====================================================================== 23 | // Local Variables: 24 | // mode: Go 25 | // fill-column: 78 26 | // End: 27 | -------------------------------------------------------------------------------- /pkg/tailfile/tailfile_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package tailfile 6 | 7 | import ( 8 | "os" 9 | 10 | "github.com/gcla/tail" 11 | ) 12 | 13 | //====================================================================== 14 | 15 | func Tail(file string) error { 16 | t, err := tail.TailFile(file, tail.Config{ 17 | Follow: true, 18 | ReOpen: true, 19 | Poll: true, 20 | Logger: tail.DiscardingLogger, 21 | }) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | for chunk := range t.Bytes { 27 | os.Stdout.Write([]byte(chunk.Text)) 28 | } 29 | return nil 30 | } 31 | 32 | //====================================================================== 33 | // Local Variables: 34 | // mode: Go 35 | // fill-column: 78 36 | // End: 37 | -------------------------------------------------------------------------------- /pkg/theme/modeswap/modeswap.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // package modeswap provides an IColor-conforming type Color that renders differently 6 | // if in low-color mode 7 | package modeswap 8 | 9 | import ( 10 | "github.com/gcla/gowid" 11 | ) 12 | 13 | //====================================================================== 14 | 15 | type Color struct { 16 | modeHi gowid.IColor 17 | mode256 gowid.IColor 18 | mode16 gowid.IColor 19 | } 20 | 21 | var _ gowid.IColor = (*Color)(nil) 22 | 23 | func New(hi, mid, lo gowid.IColor) *Color { 24 | return &Color{ 25 | modeHi: hi, 26 | mode256: mid, 27 | mode16: lo, 28 | } 29 | } 30 | 31 | func (c *Color) ToTCellColor(mode gowid.ColorMode) (gowid.TCellColor, bool) { 32 | var col gowid.IColor = c.mode16 33 | switch mode { 34 | case gowid.Mode24BitColors: 35 | col = c.modeHi 36 | case gowid.Mode256Colors: 37 | col = c.mode256 38 | default: 39 | col = c.mode16 40 | } 41 | return col.ToTCellColor(mode) 42 | } 43 | 44 | //====================================================================== 45 | // Local Variables: 46 | // mode: Go 47 | // fill-column: 110 48 | // End: 49 | -------------------------------------------------------------------------------- /pkg/theme/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // package theme provides utilities for customizing the styling of termshark. 6 | package theme 7 | 8 | import ( 9 | "fmt" 10 | "io" 11 | "os" 12 | "path" 13 | "path/filepath" 14 | 15 | "github.com/gcla/gowid" 16 | "github.com/rakyll/statik/fs" 17 | "github.com/shibukawa/configdir" 18 | log "github.com/sirupsen/logrus" 19 | "github.com/spf13/viper" 20 | 21 | _ "github.com/gcla/termshark/v2/assets/statik" 22 | ) 23 | 24 | //====================================================================== 25 | 26 | type Layer int 27 | 28 | const ( 29 | Foreground Layer = 0 30 | Background Layer = iota 31 | ) 32 | 33 | var theme *viper.Viper 34 | 35 | // MakeColorSafe extends gowid's MakeColorSafe function, preferring to interpret 36 | // its string argument as a toml file config key lookup first; if this fails, then 37 | // fall back to gowid.MakeColorSafe, which will then read colors as urwid color names, 38 | // #-prefixed hex digits, grayscales, etc. 39 | func MakeColorSafe(s string, l Layer) (gowid.Color, error) { 40 | loops := 10 41 | cur := s 42 | if theme != nil { 43 | for { 44 | next := theme.GetString(cur) 45 | if next != "" { 46 | cur = next 47 | } else { 48 | next := theme.GetStringSlice(cur) 49 | if next == nil || len(next) != 2 { 50 | break 51 | } else { 52 | cur = next[l] 53 | } 54 | } 55 | loops -= 1 56 | if loops == 0 { 57 | break 58 | } 59 | } 60 | } 61 | col, err := gowid.MakeColorSafe(cur) 62 | if err == nil { 63 | return gowid.Color{IColor: col, Id: s}, nil 64 | } 65 | return gowid.MakeColorSafe(s) 66 | } 67 | 68 | type Mode gowid.ColorMode 69 | 70 | func (m Mode) String() string { 71 | switch gowid.ColorMode(m) { 72 | case gowid.Mode256Colors: 73 | return "256" 74 | case gowid.Mode88Colors: 75 | return "88" 76 | case gowid.Mode16Colors: 77 | return "16" 78 | case gowid.Mode8Colors: 79 | return "8" 80 | case gowid.ModeMonochrome: 81 | return "mono" 82 | case gowid.Mode24BitColors: 83 | return "truecolor" 84 | default: 85 | return "unknown" 86 | } 87 | } 88 | 89 | // Load will set the package-level theme object to a viper object representing the 90 | // toml file either (a) read from disk, or failing that (b) built-in to termshark. 91 | // Disk themes are preferred and take precedence. 92 | func Load(name string, app gowid.IApp) error { 93 | var err error 94 | 95 | theme = viper.New() 96 | defer func() { 97 | if err != nil { 98 | theme = nil 99 | } 100 | }() 101 | 102 | theme.SetConfigType("toml") 103 | stdConf := configdir.New("", "termshark") 104 | dirs := stdConf.QueryFolders(configdir.Global) 105 | 106 | mode := Mode(app.GetColorMode()).String() 107 | 108 | log.Infof("Loading theme %s in terminal mode %v", name, app.GetColorMode()) 109 | 110 | // If there's not a truecolor theme, we assume the user wants the best alternative to be loaded, 111 | // and if a terminal has truecolor support, it'll surely have 256-color support. 112 | modes := []string{mode} 113 | if mode == "truecolor" { 114 | modes = append(modes, Mode(gowid.Mode256Colors).String()) 115 | } 116 | 117 | for _, m := range modes { 118 | // Prefer to load from disk 119 | themeFileName := filepath.Join(dirs[0].Path, "themes", fmt.Sprintf("%s-%s.toml", name, m)) 120 | log.Infof("Trying to load user theme %s", themeFileName) 121 | var file io.ReadCloser 122 | file, err = os.Open(themeFileName) 123 | if err == nil { 124 | defer file.Close() 125 | log.Infof("Loaded user theme %s", themeFileName) 126 | return theme.ReadConfig(file) 127 | } 128 | } 129 | 130 | // Fall back to built-in themes 131 | statikFS, err := fs.New() 132 | if err != nil { 133 | return fmt.Errorf("in mode %v: %v", app.GetColorMode(), err) 134 | } 135 | 136 | for _, m := range modes { 137 | themeFileName := path.Join("/themes", fmt.Sprintf("%s-%s.toml", name, m)) 138 | log.Infof("Trying to load built-in theme %s", themeFileName) 139 | var file io.ReadCloser 140 | file, err = statikFS.Open(themeFileName) 141 | if err == nil { 142 | defer file.Close() 143 | log.Infof("Loaded built-in theme %s", themeFileName) 144 | return theme.ReadConfig(file) 145 | } 146 | } 147 | 148 | return err 149 | } 150 | 151 | //====================================================================== 152 | // Local Variables: 153 | // mode: Go 154 | // fill-column: 110 155 | // End: 156 | -------------------------------------------------------------------------------- /pkg/tty/tty.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | // 5 | // +build !windows 6 | 7 | package tty 8 | 9 | import ( 10 | "os" 11 | "syscall" 12 | 13 | "github.com/gcla/term/termios" 14 | "golang.org/x/sys/unix" 15 | ) 16 | 17 | //====================================================================== 18 | 19 | type TerminalSignals struct { 20 | tiosp *unix.Termios 21 | out *os.File 22 | set bool 23 | } 24 | 25 | func (t *TerminalSignals) IsSet() bool { 26 | return t.set 27 | } 28 | 29 | func (t *TerminalSignals) Restore() { 30 | if t.out != nil { 31 | fd := uintptr(t.out.Fd()) 32 | termios.Tcsetattr(fd, termios.TCSANOW, t.tiosp) 33 | 34 | t.out.Close() 35 | t.out = nil 36 | } 37 | t.set = false 38 | } 39 | 40 | func (t *TerminalSignals) Set(outtty string) error { 41 | var e error 42 | var newtios unix.Termios 43 | var fd uintptr 44 | 45 | if t.out, e = os.OpenFile(outtty, os.O_WRONLY, 0); e != nil { 46 | goto failed 47 | } 48 | 49 | fd = uintptr(t.out.Fd()) 50 | 51 | if t.tiosp, e = termios.Tcgetattr(fd); e != nil { 52 | goto failed 53 | } 54 | 55 | newtios = *t.tiosp 56 | newtios.Lflag |= syscall.ISIG 57 | 58 | // Enable ctrl-z for suspending the foreground process group via the 59 | // line discipline. Ctrl-c and Ctrl-\ are not handled, so the terminal 60 | // app will receive these keypresses. 61 | newtios.Cc[syscall.VSUSP] = 032 62 | newtios.Cc[syscall.VINTR] = 0 63 | newtios.Cc[syscall.VQUIT] = 0 64 | 65 | if e = termios.Tcsetattr(fd, termios.TCSANOW, &newtios); e != nil { 66 | goto failed 67 | } 68 | 69 | t.set = true 70 | 71 | return nil 72 | 73 | failed: 74 | if t.out != nil { 75 | t.out.Close() 76 | t.out = nil 77 | } 78 | return e 79 | } 80 | 81 | //====================================================================== 82 | // Local Variables: 83 | // mode: Go 84 | // fill-column: 78 85 | // End: 86 | -------------------------------------------------------------------------------- /pkg/tty/tty_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package tty 6 | 7 | //====================================================================== 8 | 9 | type TerminalSignals struct { 10 | set bool 11 | } 12 | 13 | func (t *TerminalSignals) IsSet() bool { 14 | return t.set 15 | } 16 | 17 | func (t *TerminalSignals) Restore() { 18 | t.set = false 19 | } 20 | 21 | func (t *TerminalSignals) Set(tty string) error { 22 | t.set = true 23 | return nil 24 | } 25 | 26 | //====================================================================== 27 | // Local Variables: 28 | // mode: Go 29 | // fill-column: 78 30 | // End: 31 | -------------------------------------------------------------------------------- /scripts/do-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # For Travis, so that git describe gives something useful 4 | git fetch --tags . 5 | 6 | export TERMSHARK_GIT_DESCRIBE="$(git describe --tags HEAD)" 7 | 8 | curl -sL https://git.io/goreleaser > /tmp/goreleaser.sh 9 | # testing 10 | bash /tmp/goreleaser.sh --snapshot --skip-sign --rm-dist 11 | # release 12 | # bash /tmp/goreleaser.sh --skip-sign --rm-dist 13 | 14 | -------------------------------------------------------------------------------- /scripts/pcaps/telnet-cooked.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcla/termshark/e8a1ec6e4f2d517f646d3bba35a2ec4926e0202c/scripts/pcaps/telnet-cooked.pcap -------------------------------------------------------------------------------- /scripts/simple-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # Safe enough... 6 | PCAP=$(mktemp -u /tmp/testXXXX.pcap) 7 | FIFO=$(mktemp -u /tmp/fifoXXXX) 8 | 9 | cleanup() { 10 | set +e 11 | rm "${PCAP}" 12 | rm "${FIFO}" 13 | true 14 | } 15 | 16 | trap cleanup EXIT 17 | 18 | echo Started some simple termshark tests. 19 | 20 | echo Installing termshark for test use. 21 | 22 | go install ./... 23 | 24 | echo Making a test pcap. 25 | 26 | cat < "${PCAP}" 27 | d4c3b2a102000400 28 | 0000000000000000 29 | 0000040006000000 30 | f32a395200000000 31 | 4d0000004d000000 32 | 1040002035012b59 33 | 0006291793f8aaaa 34 | 0300000008004500 35 | 0037f93900004011 36 | a6dbc0a82c7bc0a8 37 | 2cd5f93900450023 38 | 8d730001433a5c49 39 | 424d54435049505c 40 | 6c63636d2e31006f 41 | 6374657400f32a39 42 | 52000000004d0000 43 | 004d000000104000 44 | 2035012b59000629 45 | 1793f8aaaa030000 46 | 00080045000037f9 47 | 3900004011a6dbc0 48 | a82c7bc0a82cd5f9 49 | 39004500238d7300 50 | 01433a5c49424d54 51 | 435049505c6c6363 52 | 6d2e31006f637465 53 | 7400 54 | EOF 55 | 56 | echo Running termshark cli tests. 57 | 58 | # if timeout is invoked because termshark is stuck, the exit code will be non-zero 59 | export TS="$GOPATH/bin/termshark" 60 | 61 | # stdout is not a tty, so falls back to tshark 62 | $TS -r "${PCAP}" | grep '192.168.44.213 TFTP 77' 63 | 64 | # prove that options provided are passed through to tshark 65 | [[ $($TS -r "${PCAP}" -T psml -n | grep '' | wc -l) == 2 ]] 66 | 67 | # Must choose either a file or an interface 68 | ! $TS -r "${PCAP}" -i eth0 69 | 70 | # only display the second line via tshark 71 | [[ $($TS -r "${PCAP}" 'frame.number == 2' | wc -l) == 1 ]] 72 | 73 | # test fifos 74 | mkfifo "${FIFO}" 75 | cat "${PCAP}" > "${FIFO}" & 76 | $TS -r "${FIFO}" | grep '192.168.44.213 TFTP 77' 77 | wait 78 | rm "${FIFO}" 79 | 80 | # Check pass-thru option works. Make termshark run in a tty to ensure it's taking effect 81 | [[ $(script -q -e -c "$TS -r "${PCAP}" --pass-thru" | wc -l) == 2 ]] 82 | 83 | [[ $(script -q -e -c "$TS -r "${PCAP}" --pass-thru=true" | wc -l) == 2 ]] 84 | 85 | # run in script so termshark thinks it's in a tty 86 | cat version.go | grep -o -E "v[0-9]+\.[0-9]+(\.[0-9]+)?" | \ 87 | xargs -i bash -c "script -q -e -c \"$TS -v\" | grep {}" 88 | 89 | echo Running termshark UI tests. 90 | 91 | in_tty() { 92 | ARGS=$@ # make into one token 93 | socat - EXEC:"bash -c \\\"stty rows 50 cols 80 && TERM=xterm && $ARGS\\\"",pty,setsid,ctty 94 | } 95 | 96 | wait_for_load() { 97 | rm ~/.cache/termshark/termshark.log > /dev/null 2>&1 98 | tail -F ~/.cache/termshark/termshark.log 2> /dev/null | while [ 1 ] ; do read ; echo Log: $REPLY 1>&2 ; grep "Load operation complete" <<<$REPLY && break ; done 99 | } 100 | 101 | echo UI test 1 102 | # Load a pcap, quit 103 | { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty $TS -r "${PCAP}" > /dev/null 104 | 105 | echo Tests disabled for now until I understand whats going on with Travis... 106 | exit 0 107 | 108 | echo UI test 2 109 | # Run with stdout not a tty, but disable the pass-thru to tshark 110 | { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "$TS -r "${PCAP}" --pass-thru=false | cat" > /dev/null 111 | 112 | echo UI test 3 113 | # Load a pcap, very rudimentary scrape for an IP, quit 114 | { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "$TS -r "${PCAP}"" | grep -a 192.168.44.123 > /dev/null 115 | 116 | # Ensure -r flag isn't needed 117 | { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "$TS "${PCAP}"" | grep -a 192.168.44.123 > /dev/null 118 | 119 | echo UI test 4 120 | # Load a pcap from stdin 121 | { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "cat "${PCAP}" | TERM=xterm $TS -i -" > /dev/null 122 | { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "cat "${PCAP}" | TERM=xterm $TS -r -" > /dev/null 123 | { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "cat "${PCAP}" | TERM=xterm $TS" > /dev/null 124 | 125 | echo UI test 5 126 | # Display filter at end of command line 127 | { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "$TS -r scripts/pcaps/telnet-cooked.pcap \'frame.number == 2\'" | grep -a "Frame 2: 74 bytes" > /dev/null 128 | 129 | echo UI test 6 130 | mkfifo "${FIFO}" 131 | cat "${PCAP}" > "${FIFO}" & 132 | { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "$TS -r "${FIFO}"" > /dev/null 133 | wait 134 | cat "${PCAP}" > "${FIFO}" & 135 | { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "$TS -i "${FIFO}"" > /dev/null 136 | wait 137 | cat "${PCAP}" > "${FIFO}" & 138 | { wait_for_load ; sleep 0.5s ; echo q ; sleep 0.5s ; echo ; } | in_tty "$TS "${FIFO}"" > /dev/null 139 | #{ sleep 5s ; echo q ; echo ; } | in_tty "$TS "${FIFO}" \'frame.number == 2\'" | grep -a "Frame 2: 74 bytes" > /dev/null 140 | wait 141 | 142 | echo Tests were successful. 143 | -------------------------------------------------------------------------------- /ui/capinfoui.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package ui contains user-interface functions and helpers for termshark. 6 | package ui 7 | 8 | import ( 9 | "os" 10 | "strings" 11 | "time" 12 | 13 | "github.com/gcla/gowid" 14 | "github.com/gcla/termshark/v2" 15 | "github.com/gcla/termshark/v2/pkg/capinfo" 16 | "github.com/gcla/termshark/v2/pkg/pcap" 17 | log "github.com/sirupsen/logrus" 18 | ) 19 | 20 | var CapinfoLoader *capinfo.Loader 21 | 22 | var CapinfoData string 23 | var CapinfoTime time.Time 24 | 25 | //====================================================================== 26 | 27 | func startCapinfo(app gowid.IApp) { 28 | if Loader.PcapPdml == "" { 29 | OpenError("No pcap loaded.", app) 30 | return 31 | } 32 | 33 | fi, err := os.Stat(Loader.PcapPdml) 34 | if err != nil || CapinfoTime.Before(fi.ModTime()) { 35 | CapinfoLoader = capinfo.NewLoader(capinfo.MakeCommands(), Loader.Context()) 36 | 37 | handler := capinfoParseHandler{} 38 | 39 | CapinfoLoader.StartLoad( 40 | Loader.PcapPdml, 41 | app, 42 | &handler, 43 | ) 44 | } else { 45 | OpenMessageForCopy(CapinfoData, appView, app) 46 | } 47 | } 48 | 49 | //====================================================================== 50 | 51 | type capinfoParseHandler struct { 52 | tick *time.Ticker // for updating the spinner 53 | stop chan struct{} 54 | pleaseWaitClosed bool 55 | } 56 | 57 | var _ capinfo.ICapinfoCallbacks = (*capinfoParseHandler)(nil) 58 | var _ pcap.IBeforeBegin = (*capinfoParseHandler)(nil) 59 | var _ pcap.IAfterEnd = (*capinfoParseHandler)(nil) 60 | 61 | func (t *capinfoParseHandler) OnCapinfoData(data string) { 62 | CapinfoData = strings.Replace(data, "\r\n", "\n", -1) // For windows... 63 | fi, err := os.Stat(Loader.PcapPdml) 64 | if err != nil { 65 | log.Warnf("Could not read mtime from pcap %s: %v", Loader.PcapPdml, err) 66 | } else { 67 | CapinfoTime = fi.ModTime() 68 | } 69 | } 70 | 71 | func (t *capinfoParseHandler) AfterCapinfoEnd(success bool) { 72 | } 73 | 74 | func (t *capinfoParseHandler) BeforeBegin(code pcap.HandlerCode, app gowid.IApp) { 75 | if code&pcap.CapinfoCode == 0 { 76 | return 77 | } 78 | app.Run(gowid.RunFunction(func(app gowid.IApp) { 79 | OpenPleaseWait(appView, app) 80 | })) 81 | 82 | t.tick = time.NewTicker(time.Duration(200) * time.Millisecond) 83 | t.stop = make(chan struct{}) 84 | 85 | termshark.TrackedGo(func() { 86 | Loop: 87 | for { 88 | select { 89 | case <-t.tick.C: 90 | app.Run(gowid.RunFunction(func(app gowid.IApp) { 91 | pleaseWaitSpinner.Update() 92 | })) 93 | case <-t.stop: 94 | break Loop 95 | } 96 | } 97 | }, Goroutinewg) 98 | } 99 | 100 | func (t *capinfoParseHandler) AfterEnd(code pcap.HandlerCode, app gowid.IApp) { 101 | if code&pcap.CapinfoCode == 0 { 102 | return 103 | } 104 | app.Run(gowid.RunFunction(func(app gowid.IApp) { 105 | if !t.pleaseWaitClosed { 106 | t.pleaseWaitClosed = true 107 | ClosePleaseWait(app) 108 | } 109 | 110 | OpenMessageForCopy(CapinfoData, appView, app) 111 | })) 112 | close(t.stop) 113 | } 114 | 115 | //====================================================================== 116 | 117 | func clearCapinfoState() { 118 | CapinfoTime = time.Time{} 119 | } 120 | 121 | //====================================================================== 122 | 123 | type ManageCapinfoCache struct{} 124 | 125 | var _ pcap.INewSource = ManageCapinfoCache{} 126 | var _ pcap.IClear = ManageCapinfoCache{} 127 | 128 | // Make sure that existing stream widgets are discarded if the user loads a new pcap. 129 | func (t ManageCapinfoCache) OnNewSource(pcap.HandlerCode, gowid.IApp) { 130 | clearCapinfoState() 131 | } 132 | 133 | func (t ManageCapinfoCache) OnClear(pcap.HandlerCode, gowid.IApp) { 134 | clearCapinfoState() 135 | } 136 | 137 | //====================================================================== 138 | // Local Variables: 139 | // mode: Go 140 | // fill-column: 110 141 | // End: 142 | -------------------------------------------------------------------------------- /ui/convscallbacks.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package ui contains user-interface functions and helpers for termshark. 6 | package ui 7 | 8 | import ( 9 | "strings" 10 | "time" 11 | 12 | "github.com/gcla/gowid" 13 | "github.com/gcla/termshark/v2" 14 | "github.com/gcla/termshark/v2/pkg/pcap" 15 | ) 16 | 17 | //====================================================================== 18 | 19 | type IOnDataSync interface { 20 | OnData(data string, app gowid.IApp) 21 | OnCancel(gowid.IApp) 22 | } 23 | 24 | type convsParseHandler struct { 25 | app gowid.IApp 26 | tick *time.Ticker // for updating the spinner 27 | stop chan struct{} 28 | ondata IOnDataSync 29 | pleaseWaitClosed bool 30 | } 31 | 32 | var _ pcap.IBeforeBegin = (*convsParseHandler)(nil) 33 | var _ pcap.IAfterEnd = (*convsParseHandler)(nil) 34 | 35 | func (t *convsParseHandler) OnData(data string) { 36 | data = strings.Replace(data, "\r\n", "\n", -1) // For windows... 37 | 38 | if t.ondata != nil { 39 | t.app.Run(gowid.RunFunction(func(app gowid.IApp) { 40 | t.ondata.OnData(data, app) 41 | })) 42 | } 43 | } 44 | 45 | func (t *convsParseHandler) AfterDataEnd(success bool) { 46 | if t.ondata != nil && !success { 47 | t.app.Run(gowid.RunFunction(func(app gowid.IApp) { 48 | t.ondata.OnCancel(app) 49 | })) 50 | } 51 | } 52 | 53 | func (t *convsParseHandler) BeforeBegin(code pcap.HandlerCode, app gowid.IApp) { 54 | if code&pcap.ConvCode == 0 { 55 | return 56 | } 57 | app.Run(gowid.RunFunction(func(app gowid.IApp) { 58 | OpenPleaseWait(appView, t.app) 59 | })) 60 | 61 | t.tick = time.NewTicker(time.Duration(200) * time.Millisecond) 62 | t.stop = make(chan struct{}) 63 | 64 | termshark.TrackedGo(func() { 65 | Loop: 66 | for { 67 | select { 68 | case <-t.tick.C: 69 | app.Run(gowid.RunFunction(func(app gowid.IApp) { 70 | pleaseWaitSpinner.Update() 71 | })) 72 | case <-t.stop: 73 | break Loop 74 | } 75 | } 76 | }, Goroutinewg) 77 | } 78 | 79 | func (t *convsParseHandler) AfterEnd(code pcap.HandlerCode, app gowid.IApp) { 80 | if code&pcap.ConvCode == 0 { 81 | return 82 | } 83 | t.app.Run(gowid.RunFunction(func(app gowid.IApp) { 84 | if !t.pleaseWaitClosed { 85 | t.pleaseWaitClosed = true 86 | ClosePleaseWait(t.app) 87 | } 88 | })) 89 | close(t.stop) 90 | } 91 | 92 | //====================================================================== 93 | // Local Variables: 94 | // mode: Go 95 | // fill-column: 110 96 | // End: 97 | -------------------------------------------------------------------------------- /ui/convsui_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package ui contains user-interface functions and helpers for termshark. 6 | package ui 7 | 8 | import ( 9 | "fmt" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestScan1(t *testing.T) { 17 | line := `127.0.0.1:47416 <-> 127.0.0.1:9191 0 0 43549 9951808 43549 9951808 4.160565000 9.4522` 18 | 19 | var ( 20 | addra string 21 | addrb string 22 | framesto int 23 | bytesto int 24 | framesfrom int 25 | bytesfrom int 26 | frames int 27 | bytes int 28 | start string 29 | durn string 30 | ) 31 | 32 | r := strings.NewReader(line) 33 | n, err := fmt.Fscanf(r, "%s <-> %s %d %d %d %d %d %d %s %s", 34 | &addra, 35 | &addrb, 36 | &framesto, 37 | &bytesto, 38 | &framesfrom, 39 | &bytesfrom, 40 | &frames, 41 | &bytes, 42 | &start, 43 | &durn, 44 | ) 45 | 46 | assert.NoError(t, err) 47 | assert.Equal(t, 10, n) 48 | assert.Equal(t, "4.160565000", start) 49 | assert.Equal(t, "127.0.0.1:9191", addrb) 50 | } 51 | 52 | //====================================================================== 53 | // Local Variables: 54 | // mode: Go 55 | // fill-column: 110 56 | // End: 57 | -------------------------------------------------------------------------------- /ui/darkmode.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package ui 6 | 7 | import "github.com/gcla/gowid" 8 | 9 | //====================================================================== 10 | 11 | type PaletteSwitcher struct { 12 | P1 gowid.IPalette 13 | P2 gowid.IPalette 14 | ChooseOne *bool 15 | } 16 | 17 | var _ gowid.IPalette = (*PaletteSwitcher)(nil) 18 | 19 | func (p PaletteSwitcher) CellStyler(name string) (gowid.ICellStyler, bool) { 20 | if *p.ChooseOne { 21 | return p.P1.CellStyler(name) 22 | } else { 23 | return p.P2.CellStyler(name) 24 | } 25 | } 26 | 27 | func (p PaletteSwitcher) RangeOverPalette(f func(key string, value gowid.ICellStyler) bool) { 28 | if *p.ChooseOne { 29 | p.P1.RangeOverPalette(f) 30 | } else { 31 | p.P2.RangeOverPalette(f) 32 | } 33 | } 34 | 35 | //====================================================================== 36 | // Local Variables: 37 | // mode: Go 38 | // fill-column: 78 39 | // End: 40 | -------------------------------------------------------------------------------- /ui/filterconvs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package ui contains user-interface functions and helpers for termshark. 6 | package ui 7 | 8 | import ( 9 | "github.com/gcla/gowid" 10 | "github.com/gcla/gowid/widgets/columns" 11 | "github.com/gcla/gowid/widgets/holder" 12 | "github.com/gcla/gowid/widgets/menu" 13 | "github.com/gcla/termshark/v2/ui/menuutil" 14 | "github.com/gdamore/tcell/v2" 15 | ) 16 | 17 | //====================================================================== 18 | 19 | var filterConvsMenu1 *menu.Widget 20 | var filterConvsMenu1Site *menu.SiteWidget 21 | var filterConvsMenu2 *menu.Widget 22 | 23 | type indirect struct { 24 | *holder.Widget 25 | } 26 | 27 | type iFilterMenuActor interface { 28 | HandleFilterMenuSelection(FilterCombinator, gowid.IApp) 29 | } 30 | 31 | type convsFilterMenuActor struct{} 32 | 33 | var _ iFilterMenuActor = convsFilterMenuActor{} 34 | 35 | func (c convsFilterMenuActor) HandleFilterMenuSelection(conv FilterCombinator, app gowid.IApp) { 36 | convsUi.filterSelectedIndex = conv 37 | filterConvsMenu2.Open(filterConvsMenu1Site, app) 38 | } 39 | 40 | func buildFilterConvsMenu() { 41 | filterConvsMenu1Holder := &indirect{} 42 | filterConvsMenu2Holder := &indirect{} 43 | 44 | filterConvsMenu1 = menu.New("filterconvs1", filterConvsMenu1Holder, fixed, menu.Options{ 45 | Modal: true, 46 | CloseKeysProvided: true, 47 | CloseKeys: []gowid.IKey{ 48 | gowid.MakeKey('q'), 49 | gowid.MakeKeyExt(tcell.KeyLeft), 50 | gowid.MakeKeyExt(tcell.KeyEscape), 51 | gowid.MakeKeyExt(tcell.KeyCtrlC), 52 | }, 53 | }) 54 | 55 | filterConvsMenu2 = menu.New("filterconvs2", filterConvsMenu2Holder, fixed, menu.Options{ 56 | Modal: true, 57 | CloseKeysProvided: true, 58 | CloseKeys: []gowid.IKey{ 59 | gowid.MakeKey('q'), 60 | gowid.MakeKeyExt(tcell.KeyLeft), 61 | gowid.MakeKeyExt(tcell.KeyEscape), 62 | gowid.MakeKeyExt(tcell.KeyCtrlC), 63 | }, 64 | }) 65 | 66 | w := makeFilterCombineMenuWidget(convsFilterMenuActor{}) 67 | filterConvsMenu1Site = menu.NewSite(menu.SiteOptions{ 68 | XOffset: -3, 69 | YOffset: -3, 70 | }) 71 | cols := columns.New([]gowid.IContainerWidget{ 72 | &gowid.ContainerWidget{IWidget: w, D: fixed}, 73 | &gowid.ContainerWidget{IWidget: filterConvsMenu1Site, D: fixed}, 74 | }) 75 | filterConvsMenu1Holder.Widget = holder.New(cols) 76 | 77 | w2 := makeFilterConvs2MenuWidget() 78 | filterConvsMenu2Holder.Widget = holder.New(w2) 79 | } 80 | 81 | func makeFilterCombineMenuWidget(handler iFilterMenuActor) gowid.IWidget { 82 | menuItems := make([]menuutil.SimpleMenuItem, 0) 83 | 84 | for i, s := range []string{ 85 | "Selected", 86 | "Not Selected", 87 | "...and Selected", 88 | "...or Selected", 89 | "...and not Selected", 90 | "...or not Selected", 91 | } { 92 | i2 := i 93 | menuItems = append(menuItems, 94 | menuutil.SimpleMenuItem{ 95 | Txt: s, 96 | Key: gowid.MakeKey('1' + rune(i)), 97 | CB: func(app gowid.IApp, w2 gowid.IWidget) { 98 | handler.HandleFilterMenuSelection(FilterCombinator(i2), app) 99 | }, 100 | }, 101 | ) 102 | } 103 | 104 | lb, _ := menuutil.MakeMenuWithHotKeys(menuItems, nil) 105 | return lb 106 | } 107 | 108 | func makeFilterConvs2MenuWidget() gowid.IWidget { 109 | menuItems := make([]menuutil.SimpleMenuItem, 0) 110 | 111 | for i, s := range []string{ 112 | "A ↔ B", 113 | "A → B", 114 | "B → A", 115 | "A ↔ Any", 116 | "A → Any", 117 | "Any → A", 118 | "Any ↔ B", 119 | "Any → B", 120 | "B → Any", 121 | } { 122 | i2 := i 123 | menuItems = append(menuItems, 124 | menuutil.SimpleMenuItem{ 125 | Txt: s, 126 | Key: gowid.MakeKey('1' + rune(i)), 127 | CB: func(app gowid.IApp, w2 gowid.IWidget) { 128 | filterConvsMenu1.Close(app) 129 | filterConvsMenu2.Close(app) 130 | convsUi.doFilterMenuOp(FilterMask(i2), app) 131 | }, 132 | }, 133 | ) 134 | } 135 | 136 | convListBox, _ := menuutil.MakeMenuWithHotKeys(menuItems, nil) 137 | 138 | return convListBox 139 | } 140 | 141 | //====================================================================== 142 | // Local Variables: 143 | // mode: Go 144 | // fill-column: 110 145 | // End: 146 | -------------------------------------------------------------------------------- /ui/logsui.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // +build !windows 6 | 7 | // Package ui contains user-interface functions and helpers for termshark. 8 | package ui 9 | 10 | import ( 11 | "fmt" 12 | "io/ioutil" 13 | "os" 14 | 15 | "github.com/gcla/gowid" 16 | "github.com/gcla/gowid/widgets/holder" 17 | "github.com/gcla/gowid/widgets/terminal" 18 | "github.com/gcla/termshark/v2" 19 | "github.com/gcla/termshark/v2/configs/profiles" 20 | "github.com/gcla/termshark/v2/widgets/fileviewer" 21 | log "github.com/sirupsen/logrus" 22 | ) 23 | 24 | //====================================================================== 25 | 26 | func pager() string { 27 | res := profiles.ConfString("main.pager", "") 28 | if res == "" { 29 | res = os.Getenv("PAGER") 30 | } 31 | return res 32 | } 33 | 34 | // Dynamically load conv. If the convs window was last opened with a different filter, and the "limit to 35 | // filter" checkbox is checked, then the data needs to be reloaded. 36 | func openLogsUi(app gowid.IApp) { 37 | openFileUi(termshark.CacheFile("termshark.log"), false, fileviewer.Options{ 38 | Name: "Logs", 39 | GoToBottom: true, 40 | Pager: pager(), 41 | }, app) 42 | } 43 | 44 | func openConfigUi(app gowid.IApp) { 45 | tmp, err := ioutil.TempFile("", "termshark-*.toml") 46 | if err != nil { 47 | OpenError(fmt.Sprintf("Could not create temp file: %v", err), app) 48 | return 49 | } 50 | tmp.Close() 51 | 52 | err = profiles.WriteConfigAs(tmp.Name()) 53 | if err != nil { 54 | OpenError(fmt.Sprintf("Could not run config viewer\n\n%v", err), app) 55 | } else { 56 | openFileUi(tmp.Name(), true, fileviewer.Options{ 57 | Name: "Config", 58 | Pager: pager(), 59 | }, app) 60 | } 61 | } 62 | 63 | func openFileUi(file string, delete bool, opt fileviewer.Options, app gowid.IApp) { 64 | logsUi, err := fileviewer.New(file, 65 | gowid.WidgetCallback{"cb", 66 | func(app gowid.IApp, w gowid.IWidget) { 67 | t := w.(*terminal.Widget) 68 | ecode := t.Cmd.ProcessState.ExitCode() 69 | // -1 for signals - don't show an error for that 70 | if ecode != 0 && ecode != -1 { 71 | d := OpenError(fmt.Sprintf("Could not run file viewer\n\n%s", t.Cmd.ProcessState), app) 72 | d.OnOpenClose(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { 73 | closeFileUi(app) 74 | })) 75 | } else { 76 | closeFileUi(app) 77 | } 78 | if delete && false { 79 | err := os.Remove(file) 80 | if err != nil { 81 | log.Warnf("Problem deleting %s: %v", file, err) 82 | } 83 | } 84 | }, 85 | }, 86 | opt, 87 | ) 88 | if err != nil { 89 | OpenError(fmt.Sprintf("Error launching terminal: %v", err), app) 90 | return 91 | } 92 | 93 | logsView := holder.New(logsUi) 94 | 95 | appViewNoKeys.SetSubWidget(logsView, app) 96 | } 97 | 98 | func closeFileUi(app gowid.IApp) { 99 | appViewNoKeys.SetSubWidget(mainView, app) 100 | } 101 | 102 | //====================================================================== 103 | // Local Variables: 104 | // mode: Go 105 | // fill-column: 110 106 | // End: 107 | -------------------------------------------------------------------------------- /ui/logsui_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package ui contains user-interface functions and helpers for termshark. 6 | package ui 7 | 8 | import ( 9 | "github.com/gcla/gowid" 10 | ) 11 | 12 | // Dynamically load conv. If the convs window was last opened with a different filter, and the "limit to 13 | // filter" checkbox is checked, then the data needs to be reloaded. 14 | func openLogsUi(app gowid.IApp) { 15 | } 16 | 17 | func openConfigUi(app gowid.IApp) { 18 | } 19 | 20 | //====================================================================== 21 | 22 | //====================================================================== 23 | // Local Variables: 24 | // mode: Go 25 | // fill-column: 110 26 | // End: 27 | -------------------------------------------------------------------------------- /ui/searchalg.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package ui contains user-interface functions and helpers for termshark. 6 | package ui 7 | 8 | import ( 9 | "fmt" 10 | "time" 11 | 12 | "github.com/gcla/gowid" 13 | "github.com/gcla/termshark/v2" 14 | "github.com/gcla/termshark/v2/pkg/pcap" 15 | "github.com/gcla/termshark/v2/widgets/search" 16 | ) 17 | 18 | //====================================================================== 19 | 20 | // PacketSearcher coordinates a packet search and communicates results back from the 21 | // search implementations via resultChan. 22 | type PacketSearcher struct { 23 | resultChan chan search.IntermediateResult 24 | } 25 | 26 | var _ search.IAlgorithm = (*PacketSearcher)(nil) 27 | 28 | //====================================================================== 29 | 30 | // SearchPackets looks for the given search term in the currently loaded packets. It 31 | // is written generically, with the specifics of the packet details to be searched provided 32 | // by a set of callbacks. These give the search algorithm the starting position, the mechanics 33 | // of the search, and so on. An instance of a search can return a matching position, or a 34 | // value indicating that the algorithm needs to wait until packet data is available (e.g. 35 | // if PDML data needs to be searched but is not currently loaded). If a match is found, the 36 | // callbacks also determine how to update the UI to represent the match. 37 | func (w *PacketSearcher) SearchPackets(term search.INeedle, cbs search.ICallbacks, app gowid.IApp) { 38 | 39 | if packetListView == nil { 40 | cbs.OnError(fmt.Errorf("No packets to search"), app) 41 | return 42 | } 43 | 44 | cbs.Reset(app) 45 | 46 | currentPos, err := cbs.StartingPosition() 47 | startPos := currentPos 48 | // currentPacket will be 1-based 49 | if err != nil { 50 | cbs.OnError(err, app) 51 | return 52 | } 53 | 54 | stopCurrentSearch = cbs 55 | progressOwner = SearchOwns 56 | 57 | searchStop.RemoveOnClick(gowid.CallbackID{ 58 | Name: "searchstop", 59 | }) 60 | searchStop.OnClick(gowid.MakeWidgetCallback("searchstop", func(app gowid.IApp, _ gowid.IWidget) { 61 | cbs.RequestStop(app) 62 | })) 63 | 64 | tickInterval := time.Duration(200) * time.Millisecond 65 | tick := time.NewTicker(tickInterval) 66 | 67 | resumeAt := -1 68 | var resAt interface{} 69 | 70 | // Computationally bound searching goroutine - may have to terminate if it runs out of 71 | // packets to search while they're loaded 72 | termshark.TrackedGo(func() { 73 | cbs.SearchPacketsFrom(currentPos, startPos, term, app) 74 | }, Goroutinewg) 75 | 76 | // This goroutine exists so that at a regular interval, I can update progress. I want 77 | // the main searching goroutine to be doing the computation and not having to cooperate 78 | // with a timer interrupt 79 | termshark.TrackedGo(func() { 80 | 81 | res := search.Result{} 82 | 83 | defer func() { 84 | stopCurrentSearch = nil 85 | cbs.SearchPacketsResult(res, app) 86 | }() 87 | 88 | Loop: 89 | for { 90 | select { 91 | case <-tick.C: 92 | cbs.OnTick(app) 93 | 94 | if resumeAt != -1 { 95 | termshark.TrackedGo(func() { 96 | cbs.SearchPacketsFrom(resAt, startPos, term, app) 97 | }, Goroutinewg) 98 | resumeAt = -1 99 | } 100 | 101 | case sres := <-w.resultChan: 102 | if sres.ResumeAt == nil { 103 | // Search is finished 104 | res = sres.Res 105 | break Loop 106 | } else { 107 | resumeAt = sres.ResumeAt.PacketNumber() 108 | resAt = sres.ResumeAt 109 | 110 | // go to 0-based for cache lookup 111 | resumeAtZeroBased := resumeAt - 1 112 | app.Run(gowid.RunFunction(func(app gowid.IApp) { 113 | pktsPerLoad := Loader.PacketsPerLoad() 114 | 115 | CacheRequests = append(CacheRequests, pcap.LoadPcapSlice{ 116 | Row: (resumeAtZeroBased / pktsPerLoad) * pktsPerLoad, 117 | CancelCurrent: true, 118 | }) 119 | CacheRequestsChan <- struct{}{} 120 | })) 121 | } 122 | } 123 | } 124 | }, Goroutinewg) 125 | 126 | } 127 | 128 | //====================================================================== 129 | // Local Variables: 130 | // mode: Go 131 | // fill-column: 110 132 | // End: 133 | -------------------------------------------------------------------------------- /ui/searchcommon.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package ui contains user-interface functions and helpers for termshark. 6 | package ui 7 | 8 | import ( 9 | "sync" 10 | 11 | "github.com/gcla/gowid" 12 | ) 13 | 14 | //====================================================================== 15 | 16 | type commonSearchCallbacks struct { 17 | ticks int 18 | } 19 | 20 | func (s *commonSearchCallbacks) OnTick(app gowid.IApp) { 21 | s.ticks += 1 22 | if s.ticks >= 2 { 23 | app.Run(gowid.RunFunction(func(app gowid.IApp) { 24 | SetProgressIndeterminateFor(app, SearchOwns) 25 | SetSearchProgressWidget(app) 26 | loadSpinner.Update() 27 | })) 28 | } 29 | } 30 | 31 | func (s *commonSearchCallbacks) OnError(err error, app gowid.IApp) { 32 | app.Run(gowid.RunFunction(func(app gowid.IApp) { 33 | OpenError(err.Error(), app) 34 | })) 35 | } 36 | 37 | //====================================================================== 38 | 39 | type SearchStopper struct { 40 | RequestedMutex sync.Mutex 41 | Requested bool 42 | } 43 | 44 | func (s *SearchStopper) RequestStop(app gowid.IApp) { 45 | s.RequestedMutex.Lock() 46 | defer s.RequestedMutex.Unlock() 47 | s.Requested = true 48 | } 49 | 50 | func (s *SearchStopper) DoIfStopped(f func()) { 51 | s.RequestedMutex.Lock() 52 | defer s.RequestedMutex.Unlock() 53 | if s.Requested { 54 | f() 55 | s.Requested = false 56 | } 57 | } 58 | 59 | //====================================================================== 60 | // Local Variables: 61 | // mode: Go 62 | // fill-column: 110 63 | // End: 64 | -------------------------------------------------------------------------------- /ui/tableutil/tableutil.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package tableutil contains user-interface functions and helpers for termshark's 6 | // tables - in particular, helpers for vim key sequences like 5gg and G 7 | package tableutil 8 | 9 | import ( 10 | "github.com/gcla/gowid" 11 | "github.com/gcla/gowid/widgets/table" 12 | "github.com/gcla/termshark/v2" 13 | "github.com/gcla/termshark/v2/widgets/appkeys" 14 | "github.com/gdamore/tcell/v2" 15 | ) 16 | 17 | //====================================================================== 18 | 19 | type GoToAdapter struct { 20 | *table.BoundedWidget 21 | *termshark.KeyState 22 | } 23 | 24 | var _ IGoToLineRequested = (*GoToAdapter)(nil) 25 | 26 | func (t *GoToAdapter) GoToLineOrTop(evk *tcell.EventKey) (bool, int) { 27 | num := -1 28 | if t.NumberPrefix != -1 { 29 | num = t.NumberPrefix - 1 30 | } 31 | return evk.Key() == tcell.KeyRune && evk.Rune() == 'g' && t.PartialgCmd, num 32 | } 33 | 34 | func (t *GoToAdapter) GoToLineOrBottom(evk *tcell.EventKey) (bool, int) { 35 | num := -1 36 | if t.NumberPrefix != -1 { 37 | num = t.NumberPrefix - 1 38 | } 39 | return evk.Key() == tcell.KeyRune && evk.Rune() == 'G', num 40 | } 41 | 42 | type IGoToLineRequested interface { 43 | GoToLineOrTop(evk *tcell.EventKey) (bool, int) // -1 means top 44 | GoToLineOrBottom(evk *tcell.EventKey) (bool, int) // -1 means bottom 45 | GoToFirst(gowid.IApp) bool 46 | GoToLast(gowid.IApp) bool 47 | GoToNth(gowid.IApp, int) bool 48 | } 49 | 50 | // GotoHander retrusn a function suitable for the appkeys widget - it will 51 | // check to see if the key represents a supported action on the table and 52 | // then runs the action if so. 53 | func GotoHandler(t IGoToLineRequested) appkeys.KeyInputFn { 54 | return func(evk *tcell.EventKey, app gowid.IApp) bool { 55 | handled := false 56 | if t != nil { 57 | handled = true 58 | if doit, line := t.GoToLineOrTop(evk); doit { 59 | if line == -1 { 60 | t.GoToFirst(app) 61 | } else { 62 | // psml starts counting at 1 63 | t.GoToNth(app, line) 64 | } 65 | } else if doit, line := t.GoToLineOrBottom(evk); doit { 66 | if line == -1 { 67 | t.GoToLast(app) 68 | } else { 69 | // psml starts counting at 1 70 | t.GoToNth(app, line) 71 | } 72 | } else { 73 | handled = false 74 | } 75 | } 76 | return handled 77 | } 78 | } 79 | 80 | //====================================================================== 81 | // Local Variables: 82 | // mode: Go 83 | // fill-column: 110 84 | // End: 85 | -------------------------------------------------------------------------------- /ui/wormhole.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2020 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package ui contains user-interface functions and helpers for termshark. 6 | package ui 7 | 8 | import ( 9 | "fmt" 10 | 11 | "github.com/gcla/gowid" 12 | "github.com/gcla/gowid/widgets/dialog" 13 | "github.com/gcla/gowid/widgets/framed" 14 | "github.com/gcla/termshark/v2/configs/profiles" 15 | "github.com/gcla/termshark/v2/widgets/wormhole" 16 | log "github.com/sirupsen/logrus" 17 | ) 18 | 19 | //====================================================================== 20 | 21 | var CurrentWormholeWidget *wormhole.Widget 22 | 23 | func openWormhole(app gowid.IApp) { 24 | 25 | var numWords int 26 | if CurrentWormholeWidget == nil { 27 | numWords = profiles.ConfInt("main.wormhole-length", 2) 28 | } else { 29 | numWords = CurrentWormholeWidget.CodeLength() 30 | } 31 | 32 | if CurrentWormholeWidget == nil { 33 | var err error 34 | CurrentWormholeWidget, err = wormhole.New(Loader.PcapPdml, app, wormhole.Options{ 35 | ErrorHandler: func(err error, app gowid.IApp) { 36 | msg := fmt.Sprintf("Problem sending pcap: %v", err) 37 | log.Error(msg) 38 | OpenError(msg, app) 39 | }, 40 | CodeLength: numWords, 41 | TransitRelayAddress: profiles.ConfString("main.wormhole-transit-relay", ""), 42 | RendezvousURL: profiles.ConfString("main.wormhole-rendezvous-url", ""), 43 | }) 44 | if err != nil { 45 | msg := fmt.Sprintf("%v", err) 46 | log.Error(msg) 47 | OpenError(msg, app) 48 | return 49 | } 50 | } 51 | 52 | wormholeDialog := dialog.New( 53 | framed.NewSpace( 54 | CurrentWormholeWidget, 55 | ), 56 | dialog.Options{ 57 | Buttons: []dialog.Button{dialog.CloseD}, 58 | NoShadow: true, 59 | BackgroundStyle: gowid.MakePaletteRef("dialog"), 60 | BorderStyle: gowid.MakePaletteRef("dialog"), 61 | ButtonStyle: gowid.MakePaletteRef("dialog-button"), 62 | }, 63 | ) 64 | 65 | // space for the frame; then XXX-word1-word2-... - max length of word in 66 | // pgp word list is 11. Yuck. 67 | maxl := (2 * 3) + len(" - cancelled!") + wormhole.UpperBoundOnLength(numWords) 68 | 69 | wormholeDialog.Open(appView, ratioupto(0.8, maxl), app) 70 | } 71 | 72 | //====================================================================== 73 | // Local Variables: 74 | // mode: Go 75 | // fill-column: 110 76 | // End: 77 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package termshark 6 | 7 | var Version string = "v2.4.0+" 8 | 9 | //====================================================================== 10 | // Local Variables: 11 | // indent-tabs-mode: nil 12 | // tab-width: 4 13 | // fill-column: 78 14 | // End: 15 | -------------------------------------------------------------------------------- /widgets/appkeys/appkeys.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package appkeys provides a widget which responds to keyboard input. 6 | package appkeys 7 | 8 | import ( 9 | "fmt" 10 | 11 | "github.com/gcla/gowid" 12 | "github.com/gdamore/tcell/v2" 13 | ) 14 | 15 | //====================================================================== 16 | 17 | type IWidget interface { 18 | gowid.ICompositeWidget 19 | } 20 | 21 | type IAppInput interface { 22 | gowid.IComposite 23 | ApplyBefore() bool 24 | } 25 | 26 | type IAppKeys interface { 27 | KeyInput(ev *tcell.EventKey, app gowid.IApp) bool 28 | } 29 | 30 | type IAppMouse interface { 31 | MouseInput(ev *tcell.EventMouse, app gowid.IApp) bool 32 | } 33 | 34 | type KeyInputFn func(ev *tcell.EventKey, app gowid.IApp) bool 35 | type MouseInputFn func(ev *tcell.EventMouse, app gowid.IApp) bool 36 | 37 | type Options struct { 38 | ApplyBefore bool 39 | } 40 | 41 | type Widget struct { 42 | gowid.IWidget 43 | opt Options 44 | } 45 | 46 | type KeyWidget struct { 47 | *Widget 48 | fn KeyInputFn 49 | } 50 | 51 | type MouseWidget struct { 52 | *Widget 53 | fn MouseInputFn 54 | } 55 | 56 | func New(inner gowid.IWidget, fn KeyInputFn, opts ...Options) *KeyWidget { 57 | var opt Options 58 | if len(opts) > 0 { 59 | opt = opts[0] 60 | } 61 | 62 | res := &KeyWidget{ 63 | Widget: &Widget{ 64 | IWidget: inner, 65 | opt: opt, 66 | }, 67 | fn: fn, 68 | } 69 | 70 | return res 71 | } 72 | 73 | var _ gowid.ICompositeWidget = (*KeyWidget)(nil) 74 | var _ IWidget = (*KeyWidget)(nil) 75 | var _ IAppKeys = (*KeyWidget)(nil) 76 | 77 | func NewMouse(inner gowid.IWidget, fn MouseInputFn, opts ...Options) *MouseWidget { 78 | var opt Options 79 | if len(opts) > 0 { 80 | opt = opts[0] 81 | } 82 | 83 | res := &MouseWidget{ 84 | Widget: &Widget{ 85 | IWidget: inner, 86 | opt: opt, 87 | }, 88 | fn: fn, 89 | } 90 | 91 | return res 92 | } 93 | 94 | var _ gowid.ICompositeWidget = (*MouseWidget)(nil) 95 | var _ IWidget = (*MouseWidget)(nil) 96 | var _ IAppMouse = (*MouseWidget)(nil) 97 | 98 | func (w *Widget) String() string { 99 | return fmt.Sprintf("appkeys[%v]", w.SubWidget()) 100 | } 101 | 102 | func (w *Widget) ApplyBefore() bool { 103 | return w.opt.ApplyBefore 104 | } 105 | 106 | func (w *KeyWidget) KeyInput(k *tcell.EventKey, app gowid.IApp) bool { 107 | return w.fn(k, app) 108 | } 109 | 110 | func (w *MouseWidget) MouseInput(k *tcell.EventMouse, app gowid.IApp) bool { 111 | return w.fn(k, app) 112 | } 113 | 114 | func (w *Widget) SubWidget() gowid.IWidget { 115 | return w.IWidget 116 | } 117 | 118 | func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { 119 | w.IWidget = wi 120 | } 121 | 122 | func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { 123 | return SubWidgetSize(w, size, focus, app) 124 | } 125 | 126 | func (w *KeyWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { 127 | return UserInput(w, ev, size, focus, app) 128 | } 129 | 130 | func (w *MouseWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { 131 | return UserInput(w, ev, size, focus, app) 132 | } 133 | 134 | //====================================================================== 135 | 136 | func SubWidgetSize(w gowid.ICompositeWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { 137 | return size 138 | } 139 | 140 | func RenderSize(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { 141 | return gowid.RenderSize(w.SubWidget(), size, focus, app) 142 | } 143 | 144 | func UserInput(w IAppInput, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { 145 | var res bool 146 | 147 | if w.ApplyBefore() { 148 | switch ev := ev.(type) { 149 | case *tcell.EventKey: 150 | if wk, ok := w.(IAppKeys); ok { 151 | res = wk.KeyInput(ev, app) 152 | } 153 | case *tcell.EventMouse: 154 | if wm, ok := w.(IAppMouse); ok { 155 | res = wm.MouseInput(ev, app) 156 | } 157 | } 158 | if !res { 159 | res = w.SubWidget().UserInput(ev, size, focus, app) 160 | } 161 | } else { 162 | res = w.SubWidget().UserInput(ev, size, focus, app) 163 | if !res { 164 | switch ev := ev.(type) { 165 | case *tcell.EventKey: 166 | if wk, ok := w.(IAppKeys); ok { 167 | res = wk.KeyInput(ev, app) 168 | } 169 | case *tcell.EventMouse: 170 | if wm, ok := w.(IAppMouse); ok { 171 | res = wm.MouseInput(ev, app) 172 | } 173 | } 174 | } 175 | } 176 | return res 177 | } 178 | 179 | func Render(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { 180 | return w.SubWidget().Render(size, focus, app) 181 | } 182 | 183 | //====================================================================== 184 | // Local Variables: 185 | // mode: Go 186 | // fill-column: 110 187 | // End: 188 | -------------------------------------------------------------------------------- /widgets/copymodetree/copymodetree.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package copymodetree provides a wrapper around a tree that supports copy mode. 6 | // It assumes the underlying tree is a termshark PDML tree and allows copying 7 | // the PDML substructure or a serialized representation of the substructure. 8 | package copymodetree 9 | 10 | import ( 11 | "bytes" 12 | "fmt" 13 | "strings" 14 | 15 | "github.com/gcla/gowid" 16 | "github.com/gcla/gowid/widgets/list" 17 | "github.com/gcla/gowid/widgets/tree" 18 | "github.com/gcla/termshark/v2" 19 | "github.com/gcla/termshark/v2/pkg/pdmltree" 20 | ) 21 | 22 | //====================================================================== 23 | 24 | type Widget struct { 25 | *list.Widget 26 | clip gowid.IClipboardSelected 27 | } 28 | 29 | type ITreeAndListWalker interface { 30 | list.IWalker 31 | Decorator() tree.IDecorator 32 | Maker() tree.IWidgetMaker 33 | Tree() tree.IModel 34 | } 35 | 36 | // Note that tree.New() returns a *list.Widget - that's how it's implemented. So this 37 | // uses a list widget too. 38 | func New(l *list.Widget, clip gowid.IClipboardSelected) *Widget { 39 | return &Widget{ 40 | Widget: l, 41 | clip: clip, 42 | } 43 | } 44 | 45 | func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { 46 | if app.InCopyMode() && app.CopyModeClaimedBy().ID() == w.ID() && focus.Focus { 47 | diff := w.CopyModeLevels() - (app.CopyModeClaimedAt() - app.CopyLevel()) 48 | 49 | walk := w.Walker().(ITreeAndListWalker) 50 | w.SetWalker(NewWalker(walk, walk.Focus().(tree.IPos), diff, w.clip), app) 51 | 52 | res := w.Widget.Render(size, focus, app) 53 | w.SetWalker(walk, app) 54 | return res 55 | } else { 56 | return w.Widget.Render(size, focus, app) 57 | } 58 | } 59 | 60 | func (w *Widget) SubWidget() gowid.IWidget { 61 | return w.Widget 62 | } 63 | 64 | func (w *Widget) CopyModeLevels() int { 65 | pos := w.Walker().Focus().(tree.IPos) 66 | return len(pos.Indices()) 67 | } 68 | 69 | func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { 70 | return gowid.CopyModeUserInput(w, ev, size, focus, app) 71 | } 72 | 73 | func (w *Widget) Clips(app gowid.IApp) []gowid.ICopyResult { 74 | walker := w.Walker().(tree.ITreeWalker) 75 | pos := walker.Focus().(tree.IPos) 76 | lvls := w.CopyModeLevels() 77 | 78 | diff := lvls - (app.CopyModeClaimedAt() - app.CopyLevel()) 79 | 80 | npos := pos 81 | for i := 0; i < diff; i++ { 82 | npos = tree.ParentPosition(npos) 83 | } 84 | 85 | tr := npos.GetSubStructure(walker.Tree()) 86 | ptr := tr.(*pdmltree.Model) 87 | 88 | atts := make([]string, 0) 89 | atts = append(atts, string(ptr.NodeName)) 90 | for k, v := range ptr.Attrs { 91 | atts = append(atts, fmt.Sprintf("%s=\"%s\"", k, v)) 92 | } 93 | 94 | var tidyxmlstr string 95 | messyxmlstr := fmt.Sprintf("<%s>%s", strings.Join(atts, " "), ptr.Content, string(ptr.NodeName)) 96 | buf := bytes.Buffer{} 97 | if termshark.IndentPdml(bytes.NewReader([]byte(messyxmlstr)), &buf) != nil { 98 | tidyxmlstr = messyxmlstr 99 | } else { 100 | tidyxmlstr = buf.String() 101 | } 102 | 103 | return []gowid.ICopyResult{ 104 | gowid.CopyResult{ 105 | Name: "Selected subtree", 106 | Val: ptr.String(), 107 | }, 108 | gowid.CopyResult{ 109 | Name: "Selected subtree PDML", 110 | Val: tidyxmlstr, 111 | }, 112 | } 113 | } 114 | 115 | //====================================================================== 116 | 117 | type Walker struct { 118 | ITreeAndListWalker 119 | pos tree.IPos 120 | diff int 121 | gowid.IClipboardSelected 122 | } 123 | 124 | func NewWalker(walker ITreeAndListWalker, pos tree.IPos, diff int, clip gowid.IClipboardSelected) *Walker { 125 | return &Walker{ 126 | ITreeAndListWalker: walker, 127 | pos: pos, 128 | diff: diff, 129 | IClipboardSelected: clip, 130 | } 131 | } 132 | 133 | func (f *Walker) At(lpos list.IWalkerPosition) gowid.IWidget { 134 | if lpos == nil { 135 | return nil 136 | } 137 | 138 | pos := lpos.(tree.IPos) 139 | w := tree.WidgetAt(f, pos) 140 | 141 | npos := f.pos 142 | for i := 0; i < f.diff; i++ { 143 | npos = tree.ParentPosition(npos) 144 | } 145 | 146 | if tree.IsSubPosition(npos, pos) { 147 | return f.AlterWidget(w, nil) 148 | } else { 149 | return w 150 | } 151 | } 152 | 153 | //====================================================================== 154 | // Local Variables: 155 | // mode: Go 156 | // fill-column: 110 157 | // End: 158 | -------------------------------------------------------------------------------- /widgets/enableselected/enableselected.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package enableselected provides a widget that turns on focus.Selected. 6 | // It can be used to wrap container widgets (pile, columns) which may 7 | // change their look according to the selected state. One use for this is 8 | // highlighting selected rows or columns when the widget itself is not in 9 | // focus. 10 | package enableselected 11 | 12 | import ( 13 | "github.com/gcla/gowid" 14 | ) 15 | 16 | //====================================================================== 17 | 18 | // Widget turns on the selected field in the Widget when operations are done on this widget. Then 19 | // children widgets that respond to the selected state will be activated. 20 | type Widget struct { 21 | gowid.IWidget 22 | } 23 | 24 | var _ gowid.IWidget = (*Widget)(nil) 25 | var _ gowid.IComposite = (*Widget)(nil) 26 | 27 | func New(w gowid.IWidget) *Widget { 28 | return &Widget{w} 29 | } 30 | 31 | func (w *Widget) SubWidget() gowid.IWidget { 32 | return w.IWidget 33 | } 34 | 35 | func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { 36 | focus.Selected = true 37 | return gowid.RenderSize(w.IWidget, size, focus, app) 38 | } 39 | 40 | func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { 41 | focus.Selected = true 42 | return w.IWidget.Render(size, focus, app) 43 | } 44 | 45 | func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { 46 | focus.Selected = true 47 | return w.IWidget.UserInput(ev, size, focus, app) 48 | } 49 | 50 | //====================================================================== 51 | // Local Variables: 52 | // mode: Go 53 | // fill-column: 110 54 | // End: 55 | -------------------------------------------------------------------------------- /widgets/expander/expander.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package expander provides a widget that renders in one line when not in focus 6 | // but that may render using more than one line when in focus. This is useful for 7 | // showing an item in full when needed, but otherwise saving screen real-estate. 8 | package expander 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/gcla/gowid" 14 | "github.com/gcla/gowid/widgets/boxadapter" 15 | ) 16 | 17 | //====================================================================== 18 | 19 | // Widget will render in one row when not selected, and then using 20 | // however many rows required when selected. 21 | type Widget struct { 22 | orig gowid.IWidget 23 | w *boxadapter.Widget 24 | } 25 | 26 | var _ gowid.IWidget = (*Widget)(nil) 27 | var _ gowid.IComposite = (*Widget)(nil) 28 | 29 | func New(w gowid.IWidget) *Widget { 30 | b := boxadapter.New(w, 1) 31 | return &Widget{w, b} 32 | } 33 | 34 | func (w *Widget) SubWidget() gowid.IWidget { 35 | return w.orig 36 | } 37 | 38 | func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { 39 | if focus.Selected { 40 | return gowid.RenderSize(w.orig, size, focus, app) 41 | } else { 42 | return gowid.RenderSize(w.w, size, focus, app) 43 | } 44 | } 45 | 46 | func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { 47 | if focus.Selected { 48 | return w.orig.Render(size, focus, app) 49 | } else { 50 | return w.w.Render(size, focus, app) 51 | } 52 | } 53 | 54 | func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { 55 | if focus.Selected { 56 | return w.orig.UserInput(ev, size, focus, app) 57 | } else { 58 | return w.w.UserInput(ev, size, focus, app) 59 | } 60 | } 61 | 62 | func (w *Widget) Selectable() bool { 63 | return w.w.Selectable() 64 | } 65 | 66 | func (w *Widget) String() string { 67 | return fmt.Sprintf("expander[%v]", w.w.IWidget) 68 | } 69 | 70 | //====================================================================== 71 | // Local Variables: 72 | // mode: Go 73 | // fill-column: 110 74 | // End: 75 | -------------------------------------------------------------------------------- /widgets/fileviewer/fileviewer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // +build !windows 6 | 7 | // Package fileviewer provides a widget to view a text file in a terminal 8 | // via a pager program. 9 | package fileviewer 10 | 11 | import ( 12 | "fmt" 13 | 14 | "github.com/gcla/gowid" 15 | "github.com/gcla/gowid/widgets/hpadding" 16 | "github.com/gcla/gowid/widgets/null" 17 | "github.com/gcla/gowid/widgets/pile" 18 | "github.com/gcla/gowid/widgets/terminal" 19 | "github.com/gcla/gowid/widgets/text" 20 | ) 21 | 22 | //====================================================================== 23 | 24 | type Options struct { 25 | Name string 26 | GoToBottom bool 27 | Pager string 28 | } 29 | 30 | type Widget struct { 31 | gowid.IWidget 32 | opt Options 33 | } 34 | 35 | // New - a bit clumsy, UI will always be legit, but error represents terminal failure 36 | func New(vfile string, cb gowid.IWidgetChangedCallback, opts ...Options) (*Widget, error) { 37 | var opt Options 38 | if len(opts) > 0 { 39 | opt = opts[0] 40 | } 41 | 42 | var args []string 43 | 44 | if opt.Pager == "" { 45 | if opt.GoToBottom { 46 | args = []string{"less", "+G", vfile} 47 | } else { 48 | args = []string{"less", vfile} 49 | } 50 | } else { 51 | args = []string{"sh", "-c", fmt.Sprintf("%s %s", opt.Pager, vfile)} 52 | } 53 | 54 | var term gowid.IWidget 55 | var termC *terminal.Widget 56 | var errTerm error 57 | termC, errTerm = terminal.New(args) 58 | if errTerm != nil { 59 | term = null.New() 60 | } else { 61 | termC.OnProcessExited(cb) 62 | term = termC 63 | } 64 | 65 | header := hpadding.New( 66 | text.New(fmt.Sprintf("%s - %s", opt.Name, vfile)), 67 | gowid.HAlignMiddle{}, 68 | gowid.RenderFixed{}, 69 | ) 70 | 71 | main := pile.New([]gowid.IContainerWidget{ 72 | &gowid.ContainerWidget{ 73 | IWidget: header, 74 | D: gowid.RenderWithUnits{U: 2}, 75 | }, 76 | &gowid.ContainerWidget{ 77 | IWidget: term, 78 | D: gowid.RenderWithWeight{W: 1.0}, 79 | }, 80 | }) 81 | 82 | res := &Widget{ 83 | IWidget: main, 84 | opt: opt, 85 | } 86 | 87 | return res, errTerm 88 | } 89 | 90 | //====================================================================== 91 | // Local Variables: 92 | // mode: Go 93 | // fill-column: 110 94 | // End: 95 | -------------------------------------------------------------------------------- /widgets/framefocus/framefocus.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package framefocus provides a very specific widget to apply a frame around the widget in focus 6 | // and an empty frame if not. 7 | package framefocus 8 | 9 | import ( 10 | "runtime" 11 | 12 | "github.com/gcla/gowid" 13 | "github.com/gcla/gowid/widgets/framed" 14 | "github.com/gcla/gowid/widgets/holder" 15 | "github.com/gcla/gowid/widgets/isselected" 16 | ) 17 | 18 | //====================================================================== 19 | 20 | type Widget struct { 21 | *isselected.Widget 22 | h *holder.Widget 23 | } 24 | 25 | func New(w gowid.IWidget) *Widget { 26 | h := holder.New(w) 27 | 28 | noFocusFrame := framed.SpaceFrame 29 | noFocusFrame.T = 0 30 | noFocusFrame.B = 0 31 | 32 | return &Widget{ 33 | Widget: isselected.New( 34 | framed.New(h, framed.Options{ 35 | Frame: noFocusFrame, 36 | }), 37 | framed.NewUnicodeAlt2(h), 38 | framed.NewUnicode(h), 39 | ), 40 | h: h, 41 | } 42 | } 43 | 44 | func NewSlim(w gowid.IWidget) *Widget { 45 | h := holder.New(w) 46 | 47 | noFocusFrame := framed.SpaceFrame 48 | selectedFrame := framed.UnicodeAlt2Frame 49 | focusFrame := framed.UnicodeFrame 50 | if runtime.GOOS == "windows" { 51 | selectedFrame = framed.UnicodeFrame 52 | focusFrame = framed.UnicodeAlt2Frame 53 | } 54 | noFocusFrame.T = 0 55 | noFocusFrame.B = 0 56 | selectedFrame.T = 0 57 | selectedFrame.B = 0 58 | focusFrame.T = 0 59 | focusFrame.B = 0 60 | 61 | return &Widget{ 62 | Widget: isselected.New( 63 | framed.New(h, framed.Options{ 64 | Frame: noFocusFrame, 65 | }), 66 | framed.New(h, framed.Options{ 67 | Frame: selectedFrame, 68 | }), 69 | framed.New(h, framed.Options{ 70 | Frame: focusFrame, 71 | }), 72 | ), 73 | h: h, 74 | } 75 | } 76 | 77 | func (w *Widget) SubWidget() gowid.IWidget { 78 | return w.h.SubWidget() 79 | } 80 | 81 | func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { 82 | w.h.SetSubWidget(wi, app) 83 | } 84 | 85 | //====================================================================== 86 | // Local Variables: 87 | // mode: Go 88 | // fill-column: 110 89 | // End: 90 | -------------------------------------------------------------------------------- /widgets/hexdumper/hexdumper_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license 2 | // that can be found in the LICENSE file. 3 | 4 | package hexdumper 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/gcla/gowid" 10 | "github.com/gcla/gowid/gwtest" 11 | log "github.com/sirupsen/logrus" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | //====================================================================== 16 | 17 | func TestDump1(t *testing.T) { 18 | widget1 := New([]byte("abcdefghijklmnopqrstuvwxyz0123456789 abcdefghijklmnopqrstuvwxyz0123456789")) 19 | //stylers: []LayerStyler{styler}, 20 | canvas1 := widget1.Render(gowid.RenderFlowWith{C: 80}, gowid.NotSelected, gwtest.D) 21 | log.Infof("Canvas1 is %s", canvas1.String()) 22 | assert.Equal(t, 5, canvas1.BoxRows()) 23 | } 24 | 25 | func TestDump2(t *testing.T) { 26 | widget1 := New([]byte("")) 27 | canvas2 := widget1.Render(gowid.RenderFlowWith{C: 60}, gowid.NotSelected, gwtest.D) 28 | log.Infof("Canvas2 is %s", canvas2.String()) 29 | assert.Equal(t, 1, canvas2.BoxRows()) 30 | } 31 | 32 | //====================================================================== 33 | // Local Variables: 34 | // mode: Go 35 | // fill-column: 110 36 | // End: 37 | -------------------------------------------------------------------------------- /widgets/ifwidget/ifwidget.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package ifwidget provides a simple widget that behaves differently depending on the condition 6 | // supplied. 7 | package ifwidget 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/gcla/gowid" 13 | ) 14 | 15 | //====================================================================== 16 | 17 | type Widget struct { 18 | wtrue gowid.IWidget 19 | wfalse gowid.IWidget 20 | pred Predicate 21 | } 22 | 23 | var _ gowid.IWidget = (*Widget)(nil) 24 | var _ gowid.ICompositeWidget = (*Widget)(nil) 25 | 26 | type Predicate func() bool 27 | 28 | func New(wtrue gowid.IWidget, wfalse gowid.IWidget, pred Predicate) *Widget { 29 | res := &Widget{ 30 | wtrue: wtrue, 31 | wfalse: wfalse, 32 | pred: pred, 33 | } 34 | return res 35 | } 36 | 37 | func (w *Widget) String() string { 38 | return fmt.Sprintf("ifwidget[%v]", w.SubWidget()) 39 | } 40 | 41 | func (w *Widget) SubWidget() gowid.IWidget { 42 | if w.pred() { 43 | return w.wtrue 44 | } else { 45 | return w.wfalse 46 | } 47 | } 48 | 49 | func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { 50 | if w.pred() { 51 | w.wtrue = wi 52 | } else { 53 | w.wfalse = wi 54 | } 55 | } 56 | 57 | func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { 58 | return size 59 | } 60 | 61 | func (w *Widget) Selectable() bool { 62 | if w.pred() { 63 | return w.wtrue.Selectable() 64 | } else { 65 | return w.wfalse.Selectable() 66 | } 67 | } 68 | 69 | func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { 70 | if w.pred() { 71 | return w.wtrue.UserInput(ev, size, focus, app) 72 | } else { 73 | return w.wfalse.UserInput(ev, size, focus, app) 74 | } 75 | } 76 | 77 | func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { 78 | if w.pred() { 79 | return gowid.RenderSize(w.wtrue, size, focus, app) 80 | } else { 81 | return gowid.RenderSize(w.wfalse, size, focus, app) 82 | } 83 | } 84 | 85 | func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { 86 | if w.pred() { 87 | return w.wtrue.Render(size, focus, app) 88 | } else { 89 | return w.wfalse.Render(size, focus, app) 90 | } 91 | } 92 | 93 | //====================================================================== 94 | // Local Variables: 95 | // mode: Go 96 | // fill-column: 110 97 | // End: 98 | -------------------------------------------------------------------------------- /widgets/keepselected/keepselected.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package keepselected turns on the selected bit when Render or UserInput is called. 6 | package keepselected 7 | 8 | import "github.com/gcla/gowid" 9 | 10 | // A widget to ensure that its subwidget is always rendered as "selected", even if it's 11 | // not in focus. This allows a composite widget to style its selected child even without 12 | // focus so the user can see which child is active. 13 | type Widget struct { 14 | sub gowid.IWidget 15 | } 16 | 17 | func New(w gowid.IWidget) *Widget { 18 | return &Widget{ 19 | sub: w, 20 | } 21 | } 22 | 23 | func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { 24 | return w.sub.Render(size, focus.SelectIf(true), app) 25 | } 26 | 27 | func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { 28 | return w.sub.RenderSize(size, focus.SelectIf(true), app) 29 | } 30 | 31 | func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { 32 | return w.sub.UserInput(ev, size, focus.SelectIf(true), app) 33 | } 34 | 35 | func (w *Widget) Selectable() bool { 36 | return w.sub.Selectable() 37 | } 38 | 39 | func (w *Widget) SubWidget() gowid.IWidget { 40 | return w.sub 41 | } 42 | 43 | func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { 44 | w.sub = wi 45 | } 46 | 47 | //====================================================================== 48 | // Local Variables: 49 | // mode: Go 50 | // fill-column: 110 51 | // End: 52 | -------------------------------------------------------------------------------- /widgets/mapkeys/mapkeys.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package mapkeys provides a widget that can map one keypress to a sequence of 6 | // keypresses. If the user pnovides as input a key that is mapped, the sequence of 7 | // resulting keypresses is played to the subwidget before control returns. If the key is 8 | // not mapped, it is passed through as normal. I'm going to use this to provide a vim-like 9 | // macro feature in termshark. 10 | package mapkeys 11 | 12 | import ( 13 | "github.com/gcla/gowid" 14 | "github.com/gcla/gowid/vim" 15 | "github.com/gdamore/tcell/v2" 16 | ) 17 | 18 | //====================================================================== 19 | 20 | type Widget struct { 21 | gowid.IWidget 22 | kmap map[vim.KeyPress]vim.KeySequence 23 | } 24 | 25 | var _ gowid.IWidget = (*Widget)(nil) 26 | 27 | func New(w gowid.IWidget) *Widget { 28 | res := &Widget{ 29 | IWidget: w, 30 | kmap: make(map[vim.KeyPress]vim.KeySequence), 31 | } 32 | return res 33 | } 34 | 35 | func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { 36 | switch ev := ev.(type) { 37 | case *tcell.EventKey: 38 | kp := vim.KeyPressFromTcell(ev) 39 | if seq, ok := w.kmap[kp]; ok { 40 | var res bool 41 | for _, vk := range seq { 42 | k := gowid.Key(vk) 43 | // What should the handled value be?? 44 | res = w.IWidget.UserInput(tcell.NewEventKey(k.Key(), k.Rune(), k.Modifiers()), size, focus, app) 45 | } 46 | return res 47 | } else { 48 | return w.IWidget.UserInput(ev, size, focus, app) 49 | } 50 | default: 51 | return w.IWidget.UserInput(ev, size, focus, app) 52 | } 53 | } 54 | 55 | func (w *Widget) AddMapping(from vim.KeyPress, to vim.KeySequence, app gowid.IApp) { 56 | w.kmap[from] = to 57 | } 58 | 59 | func (w *Widget) RemoveMapping(from vim.KeyPress, app gowid.IApp) { 60 | delete(w.kmap, from) 61 | } 62 | 63 | // ClearMappings will remove all mappings. I deliberately preserve the same dictionary, 64 | // though in case I decide in the future it's useful to let clients have direct access to 65 | // the map (and so maybe store it somewhere). 66 | func (w *Widget) ClearMappings(app gowid.IApp) { 67 | for k := range w.kmap { 68 | delete(w.kmap, k) 69 | } 70 | } 71 | 72 | //====================================================================== 73 | // Local Variables: 74 | // mode: Go 75 | // fill-column: 90 76 | // End: 77 | -------------------------------------------------------------------------------- /widgets/number/number.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package hexdumper provides a numeric widget with a couple of buttons that increase or decrease its value. 6 | package number 7 | 8 | import ( 9 | "fmt" 10 | 11 | "github.com/gcla/gowid" 12 | "github.com/gcla/gowid/gwutil" 13 | "github.com/gcla/gowid/widgets/button" 14 | "github.com/gcla/gowid/widgets/columns" 15 | "github.com/gcla/gowid/widgets/fill" 16 | "github.com/gcla/gowid/widgets/holder" 17 | "github.com/gcla/gowid/widgets/hpadding" 18 | "github.com/gcla/gowid/widgets/text" 19 | ) 20 | 21 | //====================================================================== 22 | 23 | type Options struct { 24 | Value int 25 | Max gwutil.IntOption 26 | Min gwutil.IntOption 27 | Styler func(gowid.IWidget) gowid.IWidget 28 | } 29 | 30 | type Widget struct { 31 | gowid.IWidget 32 | up *button.Widget 33 | down *button.Widget 34 | valHolder *holder.Widget 35 | Value int 36 | Opt Options 37 | } 38 | 39 | var _ gowid.IWidget = (*Widget)(nil) 40 | 41 | var blank *hpadding.Widget 42 | var upArrow *text.Widget 43 | var downArrow *text.Widget 44 | var leftbr *text.Widget 45 | var rightbr *text.Widget 46 | 47 | func init() { 48 | blank = hpadding.New( 49 | fill.New(' '), 50 | gowid.HAlignLeft{}, 51 | gowid.RenderWithUnits{U: 1}, 52 | ) 53 | 54 | leftbr = text.New("[") 55 | rightbr = text.New("]") 56 | upArrow = text.New("^") 57 | downArrow = text.New("v") 58 | } 59 | 60 | func New(opts ...Options) *Widget { 61 | 62 | var opt Options 63 | if len(opts) > 0 { 64 | opt = opts[0] 65 | } 66 | 67 | res := &Widget{ 68 | valHolder: holder.New(text.New(fmt.Sprintf("%d", opt.Value))), 69 | Value: opt.Value, 70 | } 71 | 72 | up := button.NewBare(upArrow) 73 | 74 | up.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { 75 | if !res.Opt.Max.IsNone() && res.Value >= res.Opt.Max.Val() { 76 | res.Value = res.Opt.Max.Val() 77 | } else { 78 | res.Value += 1 79 | } 80 | res.valHolder.SetSubWidget(text.New(fmt.Sprintf("%d", res.Value)), app) 81 | })) 82 | 83 | down := button.NewBare(downArrow) 84 | 85 | down.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, widget gowid.IWidget) { 86 | if !res.Opt.Min.IsNone() && res.Value <= res.Opt.Min.Val() { 87 | res.Value = res.Opt.Min.Val() 88 | } else { 89 | res.Value -= 1 90 | } 91 | res.valHolder.SetSubWidget(text.New(fmt.Sprintf("%d", res.Value)), app) 92 | })) 93 | 94 | styler := opt.Styler 95 | if styler == nil { 96 | styler = func(w gowid.IWidget) gowid.IWidget { 97 | return w 98 | } 99 | } 100 | 101 | cols := columns.New([]gowid.IContainerWidget{ 102 | &gowid.ContainerWidget{ 103 | IWidget: res.valHolder, 104 | D: gowid.RenderFixed{}, 105 | //D: gowid.RenderWithWeight{W: 1}, 106 | }, 107 | &gowid.ContainerWidget{ 108 | IWidget: blank, 109 | D: gowid.RenderWithUnits{U: 1}, 110 | }, 111 | &gowid.ContainerWidget{ 112 | IWidget: leftbr, 113 | D: gowid.RenderFixed{}, 114 | }, 115 | &gowid.ContainerWidget{ 116 | IWidget: styler(up), 117 | D: gowid.RenderFixed{}, 118 | }, 119 | &gowid.ContainerWidget{ 120 | IWidget: styler(down), 121 | D: gowid.RenderFixed{}, 122 | }, 123 | &gowid.ContainerWidget{ 124 | IWidget: rightbr, 125 | D: gowid.RenderFixed{}, 126 | }, 127 | }) 128 | 129 | res.IWidget = cols 130 | res.Opt = opt 131 | res.up = up 132 | res.down = down 133 | 134 | return res 135 | } 136 | 137 | //====================================================================== 138 | // Local Variables: 139 | // mode: Go 140 | // fill-column: 110 141 | // End: 142 | -------------------------------------------------------------------------------- /widgets/number/number_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license 2 | // that can be found in the LICENSE file. 3 | 4 | package number 5 | 6 | import ( 7 | "testing" 8 | "time" 9 | 10 | "github.com/gcla/gowid" 11 | "github.com/gcla/gowid/gwtest" 12 | "github.com/gdamore/tcell/v2" 13 | log "github.com/sirupsen/logrus" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | //====================================================================== 18 | 19 | func evclick(x, y int) *tcell.EventMouse { 20 | return tcell.NewEventMouse(x, y, tcell.Button1, 0) 21 | } 22 | 23 | func evunclick(x, y int) *tcell.EventMouse { 24 | return tcell.NewEventMouse(x, y, tcell.ButtonNone, 0) 25 | } 26 | 27 | func TestNumber1(t *testing.T) { 28 | v := 2 29 | 30 | w := New(Options{ 31 | Value: v, 32 | }) 33 | sz := gowid.RenderFixed{} 34 | 35 | c1 := w.Render(sz, gowid.NotSelected, gwtest.D) 36 | log.Infof("Canvas is %s", c1.String()) 37 | // "0 [^v]" 38 | assert.Equal(t, 1, c1.BoxRows()) 39 | 40 | clickat := func(x, y int) { 41 | w.UserInput(evclick(x, y), sz, gowid.Focused, gwtest.D) 42 | gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false, time.Now()}) 43 | w.UserInput(evunclick(x, y), sz, gowid.Focused, gwtest.D) 44 | gwtest.D.SetLastMouseState(gowid.MouseState{false, false, false, time.Now()}) 45 | } 46 | 47 | clickat(2, 0) 48 | assert.Equal(t, v, w.Value) 49 | 50 | clickat(3, 0) 51 | assert.Equal(t, v+1, w.Value) 52 | 53 | clickat(4, 0) 54 | clickat(4, 0) 55 | assert.Equal(t, v+1-2, w.Value) 56 | } 57 | 58 | //====================================================================== 59 | // Local Variables: 60 | // mode: Go 61 | // fill-column: 110 62 | // End: 63 | -------------------------------------------------------------------------------- /widgets/regexstyle/regexstyle.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package regexstyle provides a widget that highlights the content of its subwidget according to a regular 6 | // expression. The widget is also given an occurrence parameter which determines which instance of the regex 7 | // match is highlighted, or if -1 is supplied, all instances are highlighted. The widget currently wraps a 8 | // text widget only since it depends on that widget being able to clone its content. 9 | package regexstyle 10 | 11 | import ( 12 | "regexp" 13 | 14 | "github.com/gcla/gowid" 15 | "github.com/gcla/gowid/widgets/text" 16 | ) 17 | 18 | //====================================================================== 19 | 20 | // This is the type of subwidget supported by regexstyle 21 | type ContentWidget interface { 22 | gowid.IWidget 23 | Content() text.IContent 24 | SetContent(gowid.IApp, text.IContent) 25 | } 26 | 27 | type Widget struct { 28 | ContentWidget 29 | Highlight 30 | } 31 | 32 | type Highlight struct { 33 | Re *regexp.Regexp 34 | Occ int 35 | Style gowid.ICellStyler 36 | } 37 | 38 | func New(w ContentWidget, hl Highlight) *Widget { 39 | res := &Widget{ 40 | ContentWidget: w, 41 | Highlight: hl, 42 | } 43 | return res 44 | } 45 | 46 | func (w *Widget) SetRegexOccurrence(i int) { 47 | w.Occ = i 48 | } 49 | 50 | func (w *Widget) SetRegex(re *regexp.Regexp) { 51 | w.Re = re 52 | } 53 | 54 | func (w *Widget) RegexMatches() int { 55 | return len(w.regexMatches(w.Content())) 56 | } 57 | 58 | func (w *Widget) regexMatches(content text.IContent) [][]int { 59 | if w.Re == nil || w.Style == nil { 60 | return [][]int{} 61 | } 62 | 63 | runes := make([]rune, 0, content.Length()) 64 | 65 | for i := 0; i < w.Content().Length(); i++ { 66 | runes = append(runes, w.Content().ChrAt(i)) 67 | } 68 | 69 | return w.Re.FindAllStringIndex(string(runes), -1) 70 | } 71 | 72 | func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { 73 | if w.Re == nil || w.Style == nil { 74 | return w.ContentWidget.Render(size, focus, app) 75 | } 76 | 77 | // save orig so it can be restored before end of render 78 | content := w.Content() 79 | if clonableContent, ok := content.(text.ICloneContent); !ok { 80 | return w.ContentWidget.Render(size, focus, app) 81 | } else { 82 | dup := clonableContent.Clone() 83 | 84 | if textContent, ok := content.(*text.Content); !ok { 85 | return w.ContentWidget.Render(size, focus, app) 86 | } else { 87 | 88 | indices := w.regexMatches(content) 89 | 90 | if len(indices) == 0 { 91 | return w.ContentWidget.Render(size, focus, app) 92 | } 93 | 94 | for i := 0; i < len(indices); i++ { 95 | if w.Occ == i || w.Occ == -1 { 96 | for j := indices[i][0]; j < indices[i][1]; j++ { 97 | (*textContent)[j].Attr = w.Style 98 | } 99 | } 100 | } 101 | 102 | // Let the underlying text widget layout the text in the way it's configured to 103 | // (line breaks, justification, etc) 104 | canvas := w.ContentWidget.Render(size, focus, app) 105 | 106 | w.SetContent(app, dup) 107 | 108 | return canvas 109 | } 110 | } 111 | } 112 | 113 | //====================================================================== 114 | // Local Variables: 115 | // mode: Go 116 | // fill-column: 110 117 | // End: 118 | -------------------------------------------------------------------------------- /widgets/renderfocused/renderfocused.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package renderfocused will render a widget with focus true 6 | package renderfocused 7 | 8 | import ( 9 | "github.com/gcla/gowid" 10 | ) 11 | 12 | //====================================================================== 13 | 14 | type Widget struct { 15 | gowid.IWidget 16 | } 17 | 18 | var _ gowid.IWidget = (*Widget)(nil) 19 | var _ gowid.ICompositeWidget = (*Widget)(nil) 20 | 21 | func New(w gowid.IWidget) *Widget { 22 | return &Widget{ 23 | IWidget: w, 24 | } 25 | } 26 | 27 | func (w *Widget) SubWidget() gowid.IWidget { 28 | return w.IWidget 29 | } 30 | 31 | func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { 32 | return w.SubWidget().RenderSize(size, focus, app) 33 | } 34 | 35 | func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { 36 | return gowid.RenderSize(w.IWidget, size, gowid.Focused, app) 37 | } 38 | 39 | func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { 40 | return w.IWidget.Render(size, gowid.Focused, app) 41 | } 42 | 43 | func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { 44 | return w.IWidget.UserInput(ev, size, focus, app) 45 | } 46 | 47 | // TODO - this isn't right. Should Selectable be conditioned on focus? 48 | func (w *Widget) Selectable() bool { 49 | return w.IWidget.Selectable() 50 | } 51 | 52 | //====================================================================== 53 | // Local Variables: 54 | // mode: Go 55 | // fill-column: 110 56 | // End: 57 | -------------------------------------------------------------------------------- /widgets/resizable/resizable_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license 2 | // that can be found in the LICENSE file. 3 | 4 | package resizable 5 | 6 | import ( 7 | "encoding/json" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | //====================================================================== 14 | 15 | func TestOffset1(t *testing.T) { 16 | off1 := Offset{2, 4, 7} 17 | off1m, err := json.Marshal(off1) 18 | assert.NoError(t, err) 19 | assert.Equal(t, "{\"col1\":2,\"col2\":4,\"adjust\":7}", string(off1m)) 20 | 21 | off2 := Offset{3, 1, 15} 22 | offs := []Offset{off1, off2} 23 | offsm, err := json.Marshal(offs) 24 | assert.NoError(t, err) 25 | assert.Equal(t, "[{\"col1\":2,\"col2\":4,\"adjust\":7},{\"col1\":3,\"col2\":1,\"adjust\":15}]", string(offsm)) 26 | 27 | offs2 := make([]Offset, 0) 28 | err = json.Unmarshal(offsm, &offs2) 29 | assert.NoError(t, err) 30 | assert.Equal(t, offs, offs2) 31 | } 32 | 33 | //====================================================================== 34 | // Local Variables: 35 | // mode: Go 36 | // fill-column: 110 37 | // End: 38 | -------------------------------------------------------------------------------- /widgets/rossshark/rossshark.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package rossshark provides a widget that draws a hi-tech shark fin over the 6 | // background and allows it to move across the screen. I hope this is faithful 7 | // to Ross Jacobs' vision :-) 8 | package rossshark 9 | 10 | import ( 11 | "math/rand" 12 | "time" 13 | 14 | "github.com/gcla/gowid" 15 | "github.com/gcla/gowid/gwutil" 16 | ) 17 | 18 | //====================================================================== 19 | 20 | var maskF []string 21 | var maskB = []string{ 22 | "00000000000000000000000000111111111", 23 | "00000000000000000000011111111111110", 24 | "00000000000000000111111111111111100", 25 | "00000000000000111111111111111111000", 26 | "00000000000011111111111111111110000", 27 | "00000000001111111111111111111110000", 28 | "00000000111111111111111111111100000", 29 | "00000001111111111111111111111100000", 30 | "00000011111111111111111111111100000", 31 | "00000111111111111111111111111100000", 32 | "00001111111111111111111111111100000", 33 | "00011111111111111111111111111100000", 34 | "00111111111111111111111111111100000", 35 | "01111111111111111111111111111100000", 36 | "01111111111111111111111111111110000", 37 | "11111111111111111111111111111110000", 38 | "11111111111111111111111111111111000", 39 | "11111111111111111111111111111111100", 40 | } 41 | 42 | func init() { 43 | maskF = make([]string, 0, len(maskB)) 44 | for _, line := range maskB { 45 | maskF = append(maskF, reverseString(line)) 46 | } 47 | } 48 | 49 | type Direction int 50 | 51 | const ( 52 | Backward Direction = 0 53 | Forward Direction = iota 54 | ) 55 | 56 | type Widget struct { 57 | gowid.IWidget 58 | Dir Direction 59 | active bool 60 | xOffset int 61 | mask [][]string 62 | backg []string 63 | ticker *time.Ticker 64 | } 65 | 66 | var _ gowid.IWidget = (*Widget)(nil) 67 | 68 | func New(w gowid.IWidget) *Widget { 69 | backg := make([]string, 0, 48) 70 | for i := 0; i < cap(backg); i++ { 71 | backg = append(backg, randomString(110)) 72 | } 73 | res := &Widget{ 74 | IWidget: w, 75 | mask: [][]string{maskB, maskF}, 76 | backg: backg, 77 | xOffset: 100000, 78 | } 79 | return res 80 | } 81 | 82 | func (w *Widget) Advance() { 83 | switch w.Dir { 84 | case Backward: 85 | w.xOffset -= 1 86 | if w.xOffset <= -len(w.mask[0]) { 87 | w.xOffset = 100000 // big enough 88 | } 89 | case Forward: 90 | w.xOffset += 1 91 | } 92 | } 93 | 94 | func (w *Widget) Activate() { 95 | w.ticker = time.NewTicker(time.Duration(150) * time.Millisecond) 96 | } 97 | 98 | func (w *Widget) Deactivate() { 99 | w.ticker = nil 100 | } 101 | 102 | func (w *Widget) Active() bool { 103 | return w.ticker != nil 104 | } 105 | 106 | func (w *Widget) C() <-chan time.Time { 107 | return w.ticker.C 108 | } 109 | 110 | func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { 111 | c := w.IWidget.Render(size, focus, app) 112 | if w.Active() { 113 | // Adjust here to account for the fact the screen can be resized 114 | if w.xOffset >= c.BoxColumns() { 115 | switch w.Dir { 116 | case Backward: 117 | w.xOffset = c.BoxColumns() - 1 118 | case Forward: 119 | w.xOffset = -len(w.mask[0]) 120 | } 121 | } 122 | mask := w.mask[w.Dir] 123 | yOffset := c.BoxRows()/2 - len(mask)/2 // in the middle 124 | for y, sy := gwutil.Max(0, yOffset), gwutil.Max(0, -yOffset); y < c.BoxRows() && sy < len(mask); y, sy = y+1, sy+1 { 125 | for x, sx := gwutil.Max(0, w.xOffset), gwutil.Max(0, -w.xOffset); x < c.BoxColumns() && sx < len(mask[0]); x, sx = x+1, sx+1 { 126 | if mask[sy][sx] == '1' { 127 | cell := c.CellAt(x, y) 128 | r := w.backg[y%len(w.backg)][x%len(w.backg[0])] 129 | c.SetCellAt(x, y, cell.WithRune(rune(r))) 130 | } 131 | } 132 | } 133 | } 134 | return c 135 | } 136 | 137 | //====================================================================== 138 | 139 | // Use charset [a-f0-9] to mirror tshark -x/xxd hex output 140 | const charset = "abcdef0123456789" 141 | 142 | var seededRand *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano())) 143 | 144 | func randomStringWithCharset(length int, charset string) string { 145 | b := make([]byte, length) 146 | for i := range b { 147 | b[i] = charset[seededRand.Intn(len(charset))] 148 | } 149 | return string(b) 150 | } 151 | 152 | func randomString(length int) string { 153 | return randomStringWithCharset(length, charset) 154 | } 155 | 156 | // Plagiarized from https://stackoverflow.com/a/4965535 - the most straightforward answer 157 | func reverseString(s string) (result string) { 158 | for _, v := range s { 159 | result = string(v) + result 160 | } 161 | return 162 | } 163 | 164 | //====================================================================== 165 | // Local Variables: 166 | // mode: Go 167 | // fill-column: 110 168 | // End: 169 | -------------------------------------------------------------------------------- /widgets/scrollabletable/scrollabletable.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package scrollabletable makes a widget that some scrollbar interfaces 6 | // suitable for passing to withscrollbar.New() 7 | package scrollabletable 8 | 9 | import ( 10 | "github.com/gcla/gowid" 11 | "github.com/gcla/gowid/widgets/table" 12 | "github.com/gcla/termshark/v2/widgets/withscrollbar" 13 | ) 14 | 15 | //====================================================================== 16 | 17 | type IScrollableTable interface { 18 | gowid.IWidget 19 | withscrollbar.IScrollOneLine 20 | withscrollbar.IScrollOnePage 21 | CurrentRow() int 22 | Model() table.IModel 23 | } 24 | 25 | // To implement withscrollbar.IScrollValues 26 | type Widget struct { 27 | IScrollableTable 28 | } 29 | 30 | // makes a IScrollableTable suitable for passing to withscrollbar.New() 31 | var _ withscrollbar.IScrollSubWidget = Widget{} 32 | var _ withscrollbar.IScrollSubWidget = (*Widget)(nil) 33 | 34 | func New(t IScrollableTable) *Widget { 35 | return &Widget{ 36 | IScrollableTable: t, 37 | } 38 | } 39 | 40 | func (s Widget) ScrollLength() int { 41 | return s.Model().(table.IBoundedModel).Rows() 42 | } 43 | 44 | func (s Widget) ScrollPosition() int { 45 | return s.CurrentRow() 46 | } 47 | 48 | //====================================================================== 49 | // Local Variables: 50 | // mode: Go 51 | // fill-column: 110 52 | // End: 53 | -------------------------------------------------------------------------------- /widgets/scrollabletext/scrollabletext.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package scrollabletext provides a text widget that can be placed inside 6 | // withscrollbar.Widget 7 | package scrollabletext 8 | 9 | import ( 10 | "strings" 11 | 12 | "github.com/gcla/gowid" 13 | "github.com/gcla/gowid/gwutil" 14 | "github.com/gcla/gowid/widgets/selectable" 15 | "github.com/gcla/gowid/widgets/text" 16 | "github.com/gdamore/tcell/v2" 17 | ) 18 | 19 | //====================================================================== 20 | 21 | // Widget constructs a text widget and allows it to be scrolled. But this widget is limited - it assumes no 22 | // line will wrap. To make this happen it ensures that any lines that are too long are clipped. It makes this 23 | // assumption because my scrollbar APIs are not well designed, and functions like ScrollPosition and 24 | // ScrollLength don't understand the current rendering context. That means if the app is resized, and a line 25 | // now takes two screen lines to render and not one, the scrollbar can't be built accurately. Until I design 26 | // a better scrollbar API, this will work - I'm only using it for limited information dialogs at the moment. 27 | type Widget struct { 28 | *selectable.Widget 29 | splitText []string 30 | linesFromTop int // how many lines down we are 31 | cachedLength int 32 | } 33 | 34 | var _ gowid.IWidget = (*Widget)(nil) 35 | 36 | func New(txt string) *Widget { 37 | splitText := strings.Split(txt, "\n") 38 | res := &Widget{ 39 | splitText: splitText, 40 | cachedLength: len(splitText), 41 | } 42 | res.makeText() 43 | return res 44 | } 45 | 46 | func (w *Widget) makeText() { 47 | w.Widget = selectable.New( 48 | text.New( 49 | strings.Join(w.splitText[w.linesFromTop:], "\n"), 50 | text.Options{ 51 | Wrap: text.WrapClip, 52 | ClipIndicator: "...", 53 | }, 54 | ), 55 | ) 56 | } 57 | 58 | func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { 59 | handled := true 60 | linesFromTop := w.linesFromTop 61 | switch ev := ev.(type) { 62 | case *tcell.EventKey: 63 | switch ev.Key() { 64 | case tcell.KeyPgUp: 65 | w.UpPage(1, size, app) 66 | case tcell.KeyUp, tcell.KeyCtrlP: 67 | w.Up(1, size, app) 68 | case tcell.KeyDown, tcell.KeyCtrlN: 69 | w.Down(1, size, app) 70 | case tcell.KeyPgDn: 71 | w.DownPage(1, size, app) 72 | default: 73 | handled = false 74 | } 75 | } 76 | 77 | if handled && linesFromTop == w.linesFromTop { 78 | handled = false 79 | } 80 | 81 | if !handled { 82 | handled = w.Widget.UserInput(ev, size, focus, app) 83 | } 84 | 85 | return handled 86 | } 87 | 88 | // Implement functions for withscrollbar.Widget 89 | func (w *Widget) ScrollPosition() int { 90 | return w.linesFromTop 91 | } 92 | 93 | func (w *Widget) ScrollLength() int { 94 | return w.cachedLength 95 | } 96 | 97 | func (w *Widget) Up(lines int, size gowid.IRenderSize, app gowid.IApp) { 98 | pos := w.linesFromTop 99 | w.linesFromTop = gwutil.Max(0, w.linesFromTop-lines) 100 | if pos != w.linesFromTop { 101 | w.makeText() 102 | } 103 | } 104 | 105 | func (w *Widget) Down(lines int, size gowid.IRenderSize, app gowid.IApp) { 106 | pos := w.linesFromTop 107 | w.linesFromTop = gwutil.Min(w.cachedLength-1, w.linesFromTop+lines) 108 | if pos != w.linesFromTop { 109 | w.makeText() 110 | } 111 | } 112 | 113 | func (w *Widget) UpPage(num int, size gowid.IRenderSize, app gowid.IApp) { 114 | pos := w.linesFromTop 115 | pg := 1 116 | if size, ok := size.(gowid.IRows); ok { 117 | pg = size.Rows() 118 | } 119 | w.linesFromTop = gwutil.Max(0, w.linesFromTop-(pg*num)) 120 | if pos != w.linesFromTop { 121 | w.makeText() 122 | } 123 | } 124 | 125 | func (w *Widget) DownPage(num int, size gowid.IRenderSize, app gowid.IApp) { 126 | pos := w.linesFromTop 127 | pg := 1 128 | if size, ok := size.(gowid.IRows); ok { 129 | pg = size.Rows() 130 | } 131 | w.linesFromTop = gwutil.Min(w.cachedLength-1, w.linesFromTop+(pg*num)) 132 | if pos != w.linesFromTop { 133 | w.makeText() 134 | } 135 | } 136 | 137 | //====================================================================== 138 | // Local Variables: 139 | // mode: Go 140 | // fill-column: 110 141 | // End: 142 | -------------------------------------------------------------------------------- /widgets/trackfocus/trackfocus.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | // Package trackfocus provides a widget that issues callbacks when a widget loses or gains the focus. 6 | package trackfocus 7 | 8 | import ( 9 | "github.com/gcla/gowid" 10 | ) 11 | 12 | //====================================================================== 13 | 14 | type Widget struct { 15 | gowid.IWidget 16 | init bool 17 | last bool 18 | cb *gowid.Callbacks 19 | } 20 | 21 | func New(w gowid.IWidget) *Widget { 22 | return &Widget{ 23 | IWidget: w, 24 | cb: gowid.NewCallbacks(), 25 | } 26 | } 27 | 28 | // Markers to track the callbacks being added. These just need to be distinct 29 | // from other markers. 30 | type FocusLostCB struct{} 31 | type FocusGainedCB struct{} 32 | 33 | // Boilerplate to make the widget provide methods to add and remove callbacks. 34 | func (w *Widget) OnFocusLost(f gowid.IWidgetChangedCallback) { 35 | gowid.AddWidgetCallback(w.cb, FocusLostCB{}, f) 36 | } 37 | 38 | func (w *Widget) RemoveOnFocusLost(f gowid.IIdentity) { 39 | gowid.RemoveWidgetCallback(w.cb, FocusLostCB{}, f) 40 | } 41 | 42 | func (w *Widget) OnFocusGained(f gowid.IWidgetChangedCallback) { 43 | gowid.AddWidgetCallback(w.cb, FocusGainedCB{}, f) 44 | } 45 | 46 | func (w *Widget) RemoveOnFocusGained(f gowid.IIdentity) { 47 | gowid.RemoveWidgetCallback(w.cb, FocusGainedCB{}, f) 48 | } 49 | 50 | func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { 51 | res := w.IWidget.Render(size, focus, app) 52 | if w.init && focus.Focus != w.last { 53 | if focus.Focus { 54 | gowid.RunWidgetCallbacks(w.cb, FocusGainedCB{}, app, w) 55 | } else { 56 | gowid.RunWidgetCallbacks(w.cb, FocusLostCB{}, app, w) 57 | } 58 | } 59 | w.init = true 60 | w.last = focus.Focus 61 | return res 62 | } 63 | 64 | // Provide IComposite and ISettableComposite. This makes the widget cooperate with general 65 | // utilities that walk the widget hierarchy, like FocusPath(). 66 | func (w *Widget) SubWidget() gowid.IWidget { 67 | return w.IWidget 68 | } 69 | 70 | func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { 71 | w.IWidget = wi 72 | } 73 | 74 | //====================================================================== 75 | // Local Variables: 76 | // mode: Go 77 | // fill-column: 110 78 | // End: 79 | -------------------------------------------------------------------------------- /widgets/trackfocus/trackfocus_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license 2 | // that can be found in the LICENSE file. 3 | 4 | package trackfocus 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/gcla/gowid" 10 | "github.com/gcla/gowid/gwtest" 11 | "github.com/gcla/gowid/widgets/text" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestTrackFocus1(t *testing.T) { 16 | tw := text.New("foobar") 17 | ftw := New(tw) 18 | 19 | c := ftw.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) 20 | 21 | cbran := false 22 | ftw.OnFocusLost(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) { 23 | cbran = true 24 | })) 25 | 26 | assert.Equal(t, "foobar", c.String()) 27 | 28 | ftw.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) 29 | assert.Equal(t, false, cbran) 30 | 31 | ftw.Render(gowid.RenderFixed{}, gowid.NotSelected, gwtest.D) 32 | assert.Equal(t, true, cbran) 33 | 34 | cbran = false 35 | ftw.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) 36 | assert.Equal(t, false, cbran) 37 | 38 | ftw.RemoveOnFocusLost(gowid.CallbackID{"cb"}) 39 | ftw.Render(gowid.RenderFixed{}, gowid.NotSelected, gwtest.D) 40 | assert.Equal(t, false, cbran) 41 | } 42 | 43 | //====================================================================== 44 | // Local Variables: 45 | // mode: Go 46 | // fill-column: 110 47 | // End: 48 | -------------------------------------------------------------------------------- /widgets/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source 2 | // code is governed by the MIT license that can be found in the LICENSE 3 | // file. 4 | 5 | package widgets 6 | 7 | import ( 8 | "github.com/gcla/gowid" 9 | "github.com/gcla/gowid/widgets/menu" 10 | "github.com/gdamore/tcell/v2" 11 | ) 12 | 13 | //====================================================================== 14 | 15 | func SwallowMouseScroll(ev *tcell.EventMouse, app gowid.IApp) bool { 16 | res := false 17 | switch ev.Buttons() { 18 | case tcell.WheelDown: 19 | res = true 20 | case tcell.WheelUp: 21 | res = true 22 | } 23 | return res 24 | } 25 | 26 | func SwallowMovementKeys(ev *tcell.EventKey, app gowid.IApp) bool { 27 | res := false 28 | switch ev.Key() { 29 | case tcell.KeyDown, tcell.KeyCtrlN, tcell.KeyUp, tcell.KeyCtrlP, tcell.KeyRight, tcell.KeyCtrlF, tcell.KeyLeft, tcell.KeyCtrlB: 30 | res = true 31 | case tcell.KeyRune: 32 | switch ev.Rune() { 33 | case 'h', 'j', 'k', 'l': 34 | res = true 35 | } 36 | } 37 | return res 38 | } 39 | 40 | //====================================================================== 41 | 42 | // Return false if it was already open 43 | type MenuOpenerFunc func(bool, *menu.Widget, menu.ISite, gowid.IApp) bool 44 | 45 | func (m MenuOpenerFunc) OpenMenu(mu *menu.Widget, site *menu.SiteWidget, app gowid.IApp) bool { 46 | return m(true, mu, site, app) 47 | } 48 | 49 | func (m MenuOpenerFunc) CloseMenu(mu *menu.Widget, app gowid.IApp) { 50 | m(false, mu, nil, app) 51 | } 52 | 53 | func OpenSimpleMenu(open bool, mu *menu.Widget, site menu.ISite, app gowid.IApp) bool { 54 | if open { 55 | mu.Open(site, app) 56 | return true 57 | } else { 58 | mu.Close(app) 59 | return true 60 | } 61 | } 62 | 63 | //====================================================================== 64 | // Local Variables: 65 | // mode: Go 66 | // fill-column: 78 67 | // End: 68 | -------------------------------------------------------------------------------- /widgets/withscrollbar/withscrollbar_test.go: -------------------------------------------------------------------------------- 1 | package withscrollbar 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/gcla/gowid" 9 | "github.com/gcla/gowid/gwtest" 10 | "github.com/gcla/gowid/widgets/button" 11 | "github.com/gcla/gowid/widgets/list" 12 | "github.com/gcla/gowid/widgets/text" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | type scrollingListBox struct { 17 | *list.Widget 18 | } 19 | 20 | func (t *scrollingListBox) Up(lines int, size gowid.IRenderSize, app gowid.IApp) {} 21 | func (t *scrollingListBox) Down(lines int, size gowid.IRenderSize, app gowid.IApp) {} 22 | func (t *scrollingListBox) UpPage(num int, size gowid.IRenderSize, app gowid.IApp) {} 23 | func (t *scrollingListBox) DownPage(num int, size gowid.IRenderSize, app gowid.IApp) {} 24 | 25 | func (t *scrollingListBox) ScrollLength() int { 26 | return 8 27 | } 28 | 29 | func (t *scrollingListBox) ScrollPosition() int { 30 | return 0 31 | } 32 | 33 | func Test1(t *testing.T) { 34 | bws := make([]gowid.IWidget, 8) 35 | for i := 0; i < len(bws); i++ { 36 | bws[i] = button.NewBare(text.New(fmt.Sprintf("%03d", i))) 37 | } 38 | 39 | walker := list.NewSimpleListWalker(bws) 40 | lbox := &scrollingListBox{Widget: list.New(walker)} 41 | sbox := New(lbox) 42 | 43 | canvas1 := sbox.Render(gowid.MakeRenderBox(4, 8), gowid.NotSelected, gwtest.D) 44 | res := strings.Join([]string{ 45 | "000▲", 46 | "001█", 47 | "002 ", 48 | "003 ", 49 | "004 ", 50 | "005 ", 51 | "006 ", 52 | "007▼", 53 | }, "\n") 54 | assert.Equal(t, res, canvas1.String()) 55 | 56 | sbox = New(lbox, Options{ 57 | HideIfContentFits: true, 58 | }) 59 | 60 | canvas1 = sbox.Render(gowid.MakeRenderBox(4, 8), gowid.NotSelected, gwtest.D) 61 | res = strings.Join([]string{ 62 | "000 ", 63 | "001 ", 64 | "002 ", 65 | "003 ", 66 | "004 ", 67 | "005 ", 68 | "006 ", 69 | "007 ", 70 | }, "\n") 71 | assert.Equal(t, res, canvas1.String()) 72 | 73 | canvas1 = sbox.Render(gowid.MakeRenderBox(4, 5), gowid.NotSelected, gwtest.D) 74 | res = strings.Join([]string{ 75 | "000▲", 76 | "001█", 77 | "002 ", 78 | "003 ", 79 | "004▼", 80 | }, "\n") 81 | assert.Equal(t, res, canvas1.String()) 82 | } 83 | --------------------------------------------------------------------------------