├── .github ├── ISSUE_TEMPLATE │ └── issue.yml └── workflows │ ├── audit.yml │ ├── release.yml │ └── rust.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── samples ├── test1 ├── test2 ├── test3 ├── test4 ├── test5 ├── test6 └── test7 ├── src ├── alphabets.rs ├── colors.rs ├── main.rs ├── state.rs ├── swapper.rs └── view.rs ├── tmux-thumbs-install.sh ├── tmux-thumbs.sh └── tmux-thumbs.tmux /.github/ISSUE_TEMPLATE/issue.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | - type: textarea 11 | id: what-happened 12 | attributes: 13 | label: What happened? 14 | description: Also tell us, what did you expect to happen? 15 | placeholder: Tell us what happened! Try to be as much descriptive as possible. 16 | validations: 17 | required: true 18 | - type: dropdown 19 | id: version 20 | attributes: 21 | label: Version 22 | description: What version of `tmux-thumbs` are you running? 23 | options: 24 | - master 25 | - '0.7.*' 26 | - '0.6.*' 27 | - '0.5.*' 28 | - other 29 | validations: 30 | required: true 31 | - type: dropdown 32 | id: installation 33 | attributes: 34 | label: Installation method 35 | description: Which is your installation method to get `tmux-thumbs`? 36 | options: 37 | - Git clone 38 | - Tmux Plugin Manager (TPM) 39 | validations: 40 | required: true 41 | - type: dropdown 42 | id: build 43 | attributes: 44 | label: Build process 45 | description: Which is your build process to get the compiled binary? 46 | options: 47 | - Compiled with Rust / Cargo 48 | - Download a platform binary 49 | validations: 50 | required: true 51 | - type: dropdown 52 | id: shell 53 | attributes: 54 | label: Used shell 55 | description: Which is the shell where `tmux-thumbs` runs? 56 | options: 57 | - Sh 58 | - Bash 59 | - Zsh 60 | - Elvish 61 | validations: 62 | required: true 63 | - type: dropdown 64 | id: tmux_version 65 | attributes: 66 | label: Tmux version 67 | description: What version of our tmux you running? 68 | options: 69 | - source 70 | - 'next-3.4' 71 | - '3.4' 72 | - '3.3' 73 | - '3.2' 74 | - '3.1' 75 | - '3.0' 76 | - '2.9' 77 | - '2.8' 78 | - '2.7' 79 | - '2.6' 80 | - '2.5' 81 | - '2.4' 82 | - '2.3' 83 | - '1.8' 84 | - '1.7' 85 | - other 86 | validations: 87 | required: true 88 | - type: dropdown 89 | id: os 90 | attributes: 91 | label: Operating System 92 | description: Which operating system are you using? 93 | options: 94 | - Linux 95 | - MacOX 96 | - other 97 | validations: 98 | required: true 99 | - type: textarea 100 | id: config 101 | attributes: 102 | label: `tmux-thumbs` configuration 103 | description: Please copy and paste your `tmux-thumbs` configuration in `tmux.conf` 104 | render: shell 105 | - type: textarea 106 | id: logs 107 | attributes: 108 | label: Relevant log output 109 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 110 | render: shell 111 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Security audit 2 | on: 3 | push: 4 | paths: 5 | - '**/Cargo.toml' 6 | - '**/Cargo.lock' 7 | jobs: 8 | security_audit: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions-rs/audit-check@v1 13 | with: 14 | token: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | release: 10 | name: release ${{ matrix.target }} 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | include: 16 | - target: x86_64-unknown-linux-musl 17 | archive: tar.gz tar.xz 18 | - target: x86_64-apple-darwin 19 | archive: zip 20 | steps: 21 | - uses: actions/checkout@master 22 | - name: Compile and release 23 | uses: rust-build/rust-build.action@v1.4.0 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | RUSTTARGET: ${{ matrix.target }} 27 | EXTRA_FILES: "tmux-thumbs.sh tmux-thumbs.tmux tmux-thumbs-install.sh" 28 | ARCHIVE_TYPES: ${{ matrix.archive }} 29 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Install Tarpaulin 18 | uses: actions-rs/install@v0.1 19 | with: 20 | crate: cargo-tarpaulin 21 | version: 0.18.0 22 | use-tool-cache: true 23 | 24 | - uses: actions/checkout@v2 25 | 26 | - name: Format 27 | run: cargo fmt --all -- --check 28 | 29 | - name: Build 30 | run: cargo build --verbose 31 | 32 | - name: Run tests 33 | run: cargo test --verbose 34 | 35 | - name: Coverage 36 | run: cargo tarpaulin -o Lcov --output-dir ./coverage 37 | 38 | - name: Coveralls 39 | uses: coverallsapp/github-action@master 40 | with: 41 | github-token: ${{ secrets.GITHUB_TOKEN }} 42 | 43 | build-mac: 44 | needs: test 45 | runs-on: macos-latest 46 | 47 | steps: 48 | - uses: actions/checkout@v2 49 | 50 | - name: Build 51 | run: cargo build --release 52 | 53 | - uses: actions/upload-artifact@v1 54 | with: 55 | name: thumbs-macos.zip 56 | path: ./target/release/thumbs 57 | 58 | build-linux: 59 | needs: test 60 | runs-on: ubuntu-latest 61 | 62 | steps: 63 | - uses: actions/checkout@v2 64 | 65 | - name: Install dependencies 66 | run: | 67 | sudo apt update -y 68 | sudo apt-get install -y curl gnupg ca-certificates git gcc-multilib g++-multilib cmake libssl-dev pkg-config python3 69 | 70 | - name: Build 71 | run: cargo build --release 72 | 73 | - uses: actions/upload-artifact@v1 74 | with: 75 | name: thumbs-linux.zip 76 | path: ./target/release/thumbs 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 2 2 | max_width = 120 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "ansi_term" 16 | version = "0.12.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 19 | dependencies = [ 20 | "winapi", 21 | ] 22 | 23 | [[package]] 24 | name = "atty" 25 | version = "0.2.14" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 28 | dependencies = [ 29 | "hermit-abi", 30 | "libc", 31 | "winapi", 32 | ] 33 | 34 | [[package]] 35 | name = "base64" 36 | version = "0.13.1" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 39 | 40 | [[package]] 41 | name = "bitflags" 42 | version = "1.3.2" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 45 | 46 | [[package]] 47 | name = "clap" 48 | version = "2.34.0" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 51 | dependencies = [ 52 | "ansi_term", 53 | "atty", 54 | "bitflags", 55 | "strsim", 56 | "textwrap", 57 | "unicode-width", 58 | "vec_map", 59 | ] 60 | 61 | [[package]] 62 | name = "hermit-abi" 63 | version = "0.1.19" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 66 | dependencies = [ 67 | "libc", 68 | ] 69 | 70 | [[package]] 71 | name = "lazy_static" 72 | version = "1.4.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 75 | 76 | [[package]] 77 | name = "libc" 78 | version = "0.2.126" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" 81 | 82 | [[package]] 83 | name = "memchr" 84 | version = "2.5.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 87 | 88 | [[package]] 89 | name = "numtoa" 90 | version = "0.1.0" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" 93 | 94 | [[package]] 95 | name = "redox_syscall" 96 | version = "0.2.13" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" 99 | dependencies = [ 100 | "bitflags", 101 | ] 102 | 103 | [[package]] 104 | name = "redox_termios" 105 | version = "0.1.2" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" 108 | dependencies = [ 109 | "redox_syscall", 110 | ] 111 | 112 | [[package]] 113 | name = "regex" 114 | version = "1.7.1" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" 117 | dependencies = [ 118 | "aho-corasick", 119 | "memchr", 120 | "regex-syntax", 121 | ] 122 | 123 | [[package]] 124 | name = "regex-syntax" 125 | version = "0.6.28" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" 128 | 129 | [[package]] 130 | name = "strsim" 131 | version = "0.8.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 134 | 135 | [[package]] 136 | name = "termion" 137 | version = "1.5.6" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" 140 | dependencies = [ 141 | "libc", 142 | "numtoa", 143 | "redox_syscall", 144 | "redox_termios", 145 | ] 146 | 147 | [[package]] 148 | name = "textwrap" 149 | version = "0.11.0" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 152 | dependencies = [ 153 | "unicode-width", 154 | ] 155 | 156 | [[package]] 157 | name = "thumbs" 158 | version = "0.8.0" 159 | dependencies = [ 160 | "base64", 161 | "clap", 162 | "lazy_static", 163 | "regex", 164 | "termion", 165 | "unicode-width", 166 | ] 167 | 168 | [[package]] 169 | name = "unicode-width" 170 | version = "0.1.10" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 173 | 174 | [[package]] 175 | name = "vec_map" 176 | version = "0.8.2" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 179 | 180 | [[package]] 181 | name = "winapi" 182 | version = "0.3.9" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 185 | dependencies = [ 186 | "winapi-i686-pc-windows-gnu", 187 | "winapi-x86_64-pc-windows-gnu", 188 | ] 189 | 190 | [[package]] 191 | name = "winapi-i686-pc-windows-gnu" 192 | version = "0.4.0" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 195 | 196 | [[package]] 197 | name = "winapi-x86_64-pc-windows-gnu" 198 | version = "0.4.0" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 201 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "thumbs" 3 | version = "0.8.0" 4 | authors = ["Ferran Basora "] 5 | edition = "2018" 6 | description = "A lightning fast version copy/pasting like vimium/vimperator" 7 | repository = "https://github.com/fcsonline/tmux-thumbs" 8 | keywords = ["rust", "tmux", "tmux-plugin", "vimium", "vimperator"] 9 | license = "MIT" 10 | 11 | [dependencies] 12 | termion = "1.5.6" 13 | regex = "1.7.1" 14 | clap = "2.34.0" 15 | base64 = "0.13.1" 16 | unicode-width = "0.1.10" 17 | lazy_static = "1.4.0" 18 | 19 | [[bin]] 20 | name = "thumbs" 21 | path = "src/main.rs" 22 | 23 | [[bin]] 24 | name = "tmux-thumbs" 25 | path = "src/swapper.rs" 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ferran Basora 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tmux-thumbs 2 | 3 | [![Build Status](https://github.com/fcsonline/tmux-thumbs/workflows/Rust/badge.svg)](https://github.com/fcsonline/tmux-thumbs/actions) 4 | [![dependency status](https://deps.rs/repo/github/fcsonline/tmux-thumbs/status.svg)](https://deps.rs/repo/github/fcsonline/tmux-thumbs) 5 | [![Coverage Status](https://coveralls.io/repos/github/fcsonline/tmux-thumbs/badge.svg?branch=master)](https://coveralls.io/github/fcsonline/tmux-thumbs?branch=master) 6 | [![Maintenance](https://img.shields.io/badge/maintenance-actively%20maintained-brightgreen.svg)](https://deps.rs/repo/github/fcsonline/tmux-thumbs) 7 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) 8 | 9 | A lightning fast version of [tmux-fingers](https://github.com/Morantron/tmux-fingers) written in [Rust](https://www.rust-lang.org/) for copy pasting with vimium/vimperator like hints. 10 | 11 | ## Usage 12 | 13 | Press ( prefix + Space ) to highlight in you current tmux 14 | visible pane all text that match specific patterns. Then press the highlighted 15 | letter hint to yank the text in your tmux buffer. 16 | 17 | ### Matched patterns 18 | 19 | - File paths 20 | - File in diff 21 | - Git SHAs 22 | - IPFS CID's 23 | - Colors in hex 24 | - Numbers ( 4+ digits ) 25 | - Hex numbers 26 | - Markdown urls 27 | - IPv4, IPv6 addresses 28 | - Docker images 29 | - kubernetes resources 30 | - UUIDs 31 | 32 | These are the list of matched patterns that will be highlighted by default. If 33 | you want to highlight a pattern that is not in this list you can add one or 34 | more with `--regexp` parameter. 35 | 36 | ## Demo 37 | 38 | [![demo](https://asciinema.org/a/232775.png?ts=1)](https://asciinema.org/a/232775?autoplay=1) 39 | 40 | ## Using Tmux Plugin Manager 41 | 42 | You can add this line to your list of [TPM](https://github.com/tmux-plugins/tpm) plugins in `.tmux.conf`: 43 | 44 | ``` 45 | set -g @plugin 'fcsonline/tmux-thumbs' 46 | 47 | run-shell ~/.tmux/plugins/tmux-thumbs/tmux-thumbs.tmux 48 | ``` 49 | 50 | To be able to install the plugin just hit prefix + I. You should now be able to use 51 | the plugin! 52 | 53 | ## Installation checking out the source code 54 | 55 | `tmux-thumbs` is written in Rust. You will need `rustc` version 1.35.0 or higher. The 56 | recommended way to install Rust is from the official [download page](https://rustup.rs/). 57 | 58 | Clone the repo: 59 | 60 | ``` 61 | git clone https://github.com/fcsonline/tmux-thumbs ~/.tmux/plugins/tmux-thumbs 62 | ``` 63 | 64 | Compile it with [cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html): 65 | 66 | ``` 67 | cd ~/.tmux/plugins/tmux-thumbs 68 | cargo build --release 69 | ``` 70 | 71 | Source it in your `.tmux.conf`: 72 | 73 | ``` 74 | run-shell ~/.tmux/plugins/tmux-thumbs/tmux-thumbs.tmux 75 | ``` 76 | 77 | Reload TMUX conf by running: 78 | 79 | ``` 80 | tmux source-file ~/.tmux.conf 81 | ``` 82 | 83 | ## Configuration 84 | 85 | If you want to customize how is shown your tmux-thumbs hints those all available 86 | parameters to set your perfect profile. 87 | 88 | NOTE: for changes to take effect, you'll need to source again your `.tmux.conf` file. 89 | 90 | * [@thumbs-key](#thumbs-key) 91 | * [@thumbs-alphabet](#thumbs-alphabet) 92 | * [@thumbs-reverse](#thumbs-reverse) 93 | * [@thumbs-unique](#thumbs-unique) 94 | * [@thumbs-position](#thumbs-position) 95 | * [@thumbs-regexp-N](#thumbs-regexp-N) 96 | * [@thumbs-command](#thumbs-command) 97 | * [@thumbs-upcase-command](#thumbs-upcase-command) 98 | * [@thumbs-multi-command](#thumbs-multi-command) 99 | * [@thumbs-bg-color](#thumbs-bg-color) 100 | * [@thumbs-fg-color](#thumbs-fg-color) 101 | * [@thumbs-hint-bg-color](#thumbs-hint-bg-color) 102 | * [@thumbs-hint-fg-color](#thumbs-hint-fg-color) 103 | * [@thumbs-select-fg-color](#thumbs-select-fg-color) 104 | * [@thumbs-select-bg-color](#thumbs-select-bg-color) 105 | * [@thumbs-multi-fg-color](#thumbs-multi-fg-color) 106 | * [@thumbs-multi-bg-color](#thumbs-multi-bg-color) 107 | * [@thumbs-contrast](#thumbs-contrast) 108 | * [@thumbs-osc52](#thumbs-osc52) 109 | 110 | ### @thumbs-key 111 | 112 | `default: space` 113 | 114 | Choose which key is used to enter in thumbs mode. 115 | 116 | For example: 117 | 118 | ``` 119 | set -g @thumbs-key F 120 | ``` 121 | 122 | If you want to customize the way how `tmux-thumbs` is triggered, you can always 123 | bind whatever key to `thumbs-pick` command. For example: 124 | 125 | ``` 126 | bind-key \; thumbs-pick 127 | ``` 128 | 129 | ### @thumbs-alphabet 130 | 131 | `default: qwerty` 132 | 133 | Choose which set of characters is used to build hints. Review all [available alphabets](#Alphabets) 134 | 135 | For example: 136 | 137 | ``` 138 | set -g @thumbs-alphabet dvorak-homerow 139 | ``` 140 | 141 | ### @thumbs-reverse 142 | 143 | `default: disabled` 144 | 145 | Choose in which direction you want to assign hints. Useful to get shorter hints closer to the cursor. 146 | 147 | For example: 148 | 149 | ``` 150 | set -g @thumbs-reverse enabled 151 | ``` 152 | 153 | ### @thumbs-unique 154 | 155 | `default: disabled` 156 | 157 | Choose if you want to assign the same hint for the same matched strings. 158 | 159 | For example: 160 | 161 | ``` 162 | set -g @thumbs-unique enabled 163 | ``` 164 | 165 | ### @thumbs-position 166 | 167 | `default: left` 168 | 169 | Choose where do you want to show the hint in the matched string. Options (left, right, off_left, off_right). 170 | 171 | For example: 172 | 173 | ``` 174 | set -g @thumbs-position right 175 | ``` 176 | 177 | ### @thumbs-regexp-N 178 | 179 | Add extra patterns to match. This parameter can have multiple instances. 180 | 181 | For example: 182 | 183 | ``` 184 | set -g @thumbs-regexp-1 '[\w-\.]+@([\w-]+\.)+[\w-]{2,4}' # Match emails 185 | set -g @thumbs-regexp-2 '[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:' # Match MAC addresses 186 | set -g @thumbs-regexp-3 'Vlan\d+' # match Vlan interface on network devices 187 | set -g @thumbs-regexp-4 "Vlan\\d+" # alternative method of defining regexp 188 | set -g @thumbs-regexp-5 Vlan\\d+ # alternative method of defining regexp 189 | ``` 190 | 191 | ### @thumbs-command 192 | 193 | `default: 'tmux set-buffer -- {} && tmux display-message \"Copied {}\"'` 194 | 195 | Choose which command execute when you press a hint. `tmux-thumbs` will replace `{}` with the picked hint. 196 | 197 | For example: 198 | 199 | ``` 200 | set -g @thumbs-command 'echo -n {} | pbcopy' 201 | ``` 202 | 203 | ### @thumbs-upcase-command 204 | 205 | `default: 'tmux set-buffer -- {} && tmux paste-buffer && tmux display-message \"Copied {}\"'` 206 | 207 | Choose which command execute when you press a upcase hint. `tmux-thumbs` will replace `{}` with the picked hint. 208 | 209 | For example: 210 | 211 | ``` 212 | set -g @thumbs-upcase-command 'echo -n {} | pbcopy' 213 | ``` 214 | 215 | ### @thumbs-multi-command 216 | 217 | `default: 'tmux set-buffer -- {} && tmux paste-buffer && tmux send-keys ' ' && tmux display-message \"Copied multiple items!\"'` 218 | 219 | Choose which command execute when you select multiple items. `tmux-thumbs` will replace `{}` with the picked hint for each one. 220 | 221 | For example: 222 | 223 | ``` 224 | set -g @thumbs-multi-command 'echo -n {}' 225 | ``` 226 | 227 | ### @thumbs-bg-color 228 | 229 | `default: black` 230 | 231 | Sets the background color for matches 232 | 233 | For example: 234 | 235 | ``` 236 | set -g @thumbs-bg-color blue 237 | ``` 238 | 239 | ### @thumbs-fg-color 240 | 241 | `default: green` 242 | 243 | Sets the foreground color for matches 244 | 245 | For example: 246 | 247 | ``` 248 | set -g @thumbs-fg-color green 249 | ``` 250 | 251 | ### @thumbs-hint-bg-color 252 | 253 | `default: black` 254 | 255 | Sets the background color for hints 256 | 257 | For example: 258 | 259 | ``` 260 | set -g @thumbs-hint-bg-color blue 261 | ``` 262 | 263 | ### @thumbs-hint-fg-color 264 | 265 | `default: yellow` 266 | 267 | Sets the foreground color for hints 268 | 269 | For example: 270 | 271 | ``` 272 | set -g @thumbs-hint-fg-color green 273 | ``` 274 | 275 | ### @thumbs-select-fg-color 276 | 277 | `default: blue` 278 | 279 | Sets the foreground color for selection 280 | 281 | For example: 282 | 283 | ``` 284 | set -g @thumbs-select-fg-color red 285 | ``` 286 | 287 | ### @thumbs-select-bg-color 288 | 289 | `default: black` 290 | 291 | Sets the background color for selection 292 | 293 | For example: 294 | 295 | ``` 296 | set -g @thumbs-select-bg-color red 297 | ``` 298 | 299 | ### @thumbs-multi-fg-color 300 | 301 | `default: yellow` 302 | 303 | Sets the foreground color for multi selected item 304 | 305 | For example: 306 | 307 | ``` 308 | set -g @thumbs-multi-fg-color green 309 | ``` 310 | 311 | ### @thumbs-multi-bg-color 312 | 313 | `default: black` 314 | 315 | Sets the background color for multi selected item 316 | 317 | For example: 318 | 319 | ``` 320 | set -g @thumbs-multi-bg-color red 321 | ``` 322 | 323 | ### @thumbs-contrast 324 | 325 | `default: 0` 326 | 327 | Displays hint character in square brackets for extra visibility. 328 | 329 | For example: 330 | 331 | ``` 332 | set -g @thumbs-contrast 1 333 | ``` 334 | 335 | ### @thumbs-osc52 336 | 337 | `default: 0` 338 | 339 | If this is set to `1`, `tmux-thumbs` will print a OSC52 copy escape sequence when you select a match, in addition to running the pick command. This sequence, in terminals that support it (e.g. iTerm), allows the content to be copied into the system clipboard in addition to the tmux copy buffer. 340 | 341 | For example: 342 | 343 | ``` 344 | set -g @thumbs-osc52 1 345 | ``` 346 | 347 | #### Colors 348 | 349 | This is the list of predefined colors: 350 | 351 | - black 352 | - red 353 | - green 354 | - yellow 355 | - blue 356 | - magenta 357 | - cyan 358 | - white 359 | - default 360 | 361 | There is also support for using hex colors in the form of `#RRGGBB`. 362 | 363 | #### Alphabets 364 | 365 | This is the list of available alphabets: 366 | 367 | - `numeric`: 1234567890 368 | - `abcd`: abcd 369 | - `qwerty`: asdfqwerzxcvjklmiuopghtybn 370 | - `qwerty-homerow`: asdfjklgh 371 | - `qwerty-left-hand`: asdfqwerzcxv 372 | - `qwerty-right-hand`: jkluiopmyhn 373 | - `azerty`: qsdfazerwxcvjklmuiopghtybn 374 | - `azerty-homerow`: qsdfjkmgh 375 | - `azerty-left-hand`: qsdfazerwxcv 376 | - `azerty-right-hand`: jklmuiophyn 377 | - `qwertz`: asdfqweryxcvjkluiopmghtzbn 378 | - `qwertz-homerow`: asdfghjkl 379 | - `qwertz-left-hand`: asdfqweryxcv 380 | - `qwertz-right-hand`: jkluiopmhzn 381 | - `dvorak`: aoeuqjkxpyhtnsgcrlmwvzfidb 382 | - `dvorak-homerow`: aoeuhtnsid 383 | - `dvorak-left-hand`: aoeupqjkyix 384 | - `dvorak-right-hand`: htnsgcrlmwvz 385 | - `colemak`: arstqwfpzxcvneioluymdhgjbk 386 | - `colemak-homerow`: arstneiodh 387 | - `colemak-left-hand`: arstqwfpzxcv 388 | - `colemak-right-hand`: neioluymjhk 389 | 390 | ## Extra features 391 | 392 | - **Arrow navigation:** You can use the arrows to move around between all matched items. 393 | - **Auto paste:** If your last typed hint character is uppercase, you are going to pick and paste the desired hint. 394 | 395 | ### Multi selection 396 | 397 | If you want to enable the capability to choose multiple matches, you have to 398 | press Space. Then, choose the matches with highlighted hints or 399 | Enter (moving with cursors) and then Space again to 400 | output all of them. 401 | 402 | If you run standalone `thumbs` with multi selection mode (-m) you will be able to choose multiple hints pressing the desired letter and Space to finalize the selection. 403 | 404 | ## Tmux compatibility 405 | 406 | This is the known list of versions of `tmux` compatible with `tmux-thumbs`: 407 | 408 | | Version | Compatible | 409 | |:-------:|:----------:| 410 | | 3.0a | ✅ | 411 | | 2.9a | ✅ | 412 | | 2.8 | ❓ | 413 | | 2.7 | ❓ | 414 | | 2.6 | ✅ | 415 | | 2.5 | ❓ | 416 | | 2.4 | ❓ | 417 | | 2.3 | ❓ | 418 | | 1.8 | ❓ | 419 | | 1.7 | ❓ | 420 | 421 | If you can check hat `tmux-thumbs` is or is not compatible with some specific version of `tmux`, let me know. 422 | 423 | ## Standalone `thumbs` 424 | 425 | This project started as a `tmux` plugin but after reviewing it with some 426 | friends we decided to explore all the possibilities of decoupling thumbs from 427 | `tmux`. You can install it with a simple command: 428 | 429 | ``` 430 | cargo install thumbs 431 | ``` 432 | 433 | And those are all available options: 434 | 435 | ``` 436 | thumbs 0.7.1 437 | A lightning fast version copy/pasting like vimium/vimperator 438 | 439 | USAGE: 440 | thumbs [FLAGS] [OPTIONS] 441 | 442 | FLAGS: 443 | -c, --contrast Put square brackets around hint for visibility 444 | -h, --help Prints help information 445 | -m, --multi Enable multi-selection 446 | -r, --reverse Reverse the order for assigned hints 447 | -u, --unique Don't show duplicated hints for the same match 448 | -V, --version Prints version information 449 | 450 | OPTIONS: 451 | -a, --alphabet Sets the alphabet [default: qwerty] 452 | --bg-color Sets the background color for matches [default: black] 453 | --fg-color Sets the foregroud color for matches [default: green] 454 | -f, --format 455 | Specifies the out format for the picked hint. (%U: Upcase, %H: Hint) [default: %H] 456 | 457 | --hint-bg-color Sets the background color for hints [default: black] 458 | --hint-fg-color Sets the foregroud color for hints [default: yellow] 459 | -p, --position Hint position [default: left] 460 | -x, --regexp ... Use this regexp as extra pattern to match 461 | --select-bg-color Sets the background color for selection [default: black] 462 | --select-fg-color Sets the foreground color for selection [default: blue] 463 | --multi-bg-color Sets the background color for a multi selected item [default: black] 464 | --multi-fg-color Sets the foreground color for a multi selected item [default: cyan] 465 | -t, --target Stores the hint in the specified path 466 | ``` 467 | 468 | 469 | If you want to enjoy terminal hints, you can do things like this without `tmux`: 470 | 471 | ``` 472 | > alias pick='thumbs -u -r | xsel --clipboard -i' 473 | > git log | pick 474 | ``` 475 | 476 | Or multi selection: 477 | 478 | ``` 479 | > git log | thumbs -m 480 | 1df9fa69c8831ac042c6466af81e65402ee2a007 481 | 4897dc4ecbd2ac90b17de95e00e9e75bb540e37f 482 | ``` 483 | 484 | Standalone `thumbs` has some similarities to [FZF](https://github.com/junegunn/fzf). 485 | 486 | ## Background 487 | 488 | As I said, this project is based in [tmux-fingers](https://github.com/Morantron/tmux-fingers). Morantron did an extraordinary job, building all necessary pieces in Bash to achieve the text picker behaviour. He only deserves my gratitude for all the time I have been using [tmux-fingers](https://github.com/Morantron/tmux-fingers). 489 | 490 | During a [Fosdem](https://fosdem.org/) conf, we had the idea to rewrite it to another language. He had these thoughts many times ago but it was hard to start from scratch. So, we decided to start playing with Node.js and [react-blessed](https://github.com/Yomguithereal/react-blessed), but we detected some unacceptable latency when the program booted. We didn't investigate much about this latency. 491 | 492 | During those days another alternative appeared, called [tmux-picker](https://github.com/RTBHOUSE/tmux-picker), implemented in python and reusing many parts from [tmux-fingers](https://github.com/Morantron/tmux-fingers). It was nice, because it was fast and added original terminal color support. 493 | 494 | I was curious to know if this was possible to be written in [Rust](https://www.rust-lang.org/), and soon I realized that was something doable. The ability to implement tests for all critic parts of the application give you a great confidence about it. On the other hand, Rust has an awesome community that lets you achieve this kind of project in a short period of time. 495 | 496 | ## Roadmap 497 | 498 | - [X] Support multi selection 499 | - [X] Decouple `tmux-thumbs` from `tmux` 500 | - [ ] Code [Kitty](https://github.com/kovidgoyal/kitty) plugin, now that `thumbs` can run standalone 501 | 502 | ## Troubleshooting 503 | 504 | `tmux-thumbs` must work lighting fast. If you are facing a slow performance capturing the screen hints try to configure Tmux with these settings: 505 | 506 | ``` 507 | set -g visual-activity off 508 | set -g visual-bell off 509 | set -g visual-silence on 510 | ``` 511 | 512 | You can read a bit more about this issue here: https://github.com/fcsonline/tmux-thumbs/issues/88 513 | 514 | Every time I use `tmux-thumbs`, dead panes are created. Just review if you have 515 | this setting on: 516 | 517 | ``` 518 | set -g remain-on-exit on 519 | ``` 520 | 521 | You can read a bit more about this issue here: https://github.com/fcsonline/tmux-thumbs/issues/84 522 | 523 | ## Donations 524 | 525 | If you appreciate all the job done in this project, a small donation is always welcome: 526 | 527 | [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/fcsonline) 528 | 529 | ## Contribute 530 | 531 | This project started as a side project to learn Rust, so I'm sure that is full 532 | of mistakes and areas to be improve. If you think you can tweak the code to 533 | make it better, I'll really appreciate a pull request. ;) 534 | 535 | # License 536 | 537 | [MIT](https://github.com/fcsonline/tmux-thumbs/blob/master/LICENSE) 538 | -------------------------------------------------------------------------------- /samples/test1: -------------------------------------------------------------------------------- 1 | colors: abcde 30.6.23.42 lorem 135.2.4.4 lorem 235.23.33.34 2 | 3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam et aliquet 4 | mauris, et ullamcorper enim. Nullam vitae orci eget tellus porta ullamcorper 5 | ut et lacus. 127.0.0.1 Proin sagittis, tortor nec egestas volutpat, 6 | 255.255.255.255 sapien velit accumsan ligula, at consectetur risus tortor 7 | vitae massa. In commodo vitae sem eu semper. Suspendisse orci tellus, aliquet 8 | sit amet elit sit amet, https://en.wikipedia.org/wiki/ vehicula porttitor 9 | dolor 😋. In 😏, rhoncus some UTF(😁) dignissim iaculis ac. Ip 10.0.3.4 10 | Morbi eu elit sed eros f79010f2 ultricies 67ef359ea8 consequat. Morbi mattis 11 | dolor mi, ac faucibus justo maximus eu. Donec vestibulum lorem semper, 12 | ultricies libero 5734d923afb8 efc9061663 ac, #ff00FF ex. Morbi 13 | 123e4567-e89b-12d3-a456-426655440000 tempor mollis condimentum. 14 | 15 | lorem /var/log/123e4567/999.log lorem /etc/system.conf lorem 16 | 17 | cat ▶ cat ▶ cat ▶ cat ▶ cat 30.6.23.42 lorem 135.2.4.4 lorem 235.23.33.34 18 | 19 | ipsum [fcsonline](https://github.com/fcsonline) lorem 20 | 21 | path: /var/log/nginx.log 22 | 23 | path: ./folder/.neomake@04fd.log 24 | 25 | home: ~/.gnu/.config.txt 26 | 27 | 10.3.23.42 lorem 123.2.3.4 lorem 230.23.33.34 28 | 10.3.23.42 lorem 123.2.3.4 lorem 230.23.33.34 29 | 10.3.23.42 lorem 123.2.3.4 lorem 230.23.33.34 30 | 10.3.23.42 lorem 123.2.3.4 lorem 230.23.33.34 31 | 10.3.23.42 lorem 123.2.3.4 lorem 230.23.33.34 32 | 10.3.23.42 lorem 123.2.3.4 lorem 230.23.33.34 33 | 10.3.23.42 lorem 123.2.3.4 lorem 230.23.33.34 34 | 10.3.23.42 lorem 123.2.3.4 lorem 230.23.33.34 35 | 10.3.23.42 lorem 123.2.3.4 lorem 230.23.33.34 36 | 10.3.23.42 lorem 123.2.3.4 lorem 230.23.33.34 37 | 10.3.23.42 lorem 123.2.3.4 lorem 230.23.33.34 38 | 39 | REPOSITORY TAG IMAGE ID CREATED SIZE 40 | sha256:77af4d6b9913e693e8d0b4b294fa62ade6054e6b2f1ffb617ac955dd63fb0182 19 hours ago 1.089 GB 41 | committest latest sha256:b6fa739cedf5ea12a620a439402b6004d057da800f91c7524b5086a5e4749c9f 19 hours ago 1.089 GB 42 | sha256:78a85c484f71509adeaace20e72e941f6bdd2b25b4c75da8693efd9f61a37921 19 hours ago 1.089 GB 43 | docker latest sha256:30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4 20 hours ago 1.089 GB 44 | 45 | https://denisbider.blogspot.com/2015/09/when-monospace-fonts-arent-unicode.html 46 | 47 | より良いサポートが必要です 100.200.100.200 ユーザーはそれに値する ~/.gnu/.config.txt 😋. In 😏, Ip: 10.0.3.4 48 | 😏 😏 Ip: 10.0.3.4 49 | 50 | Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ src/components/button.jsx 51 | -------------------------------------------------------------------------------- /samples/test2: -------------------------------------------------------------------------------- 1 | 127.10.0.1 192.168.10.1 127.0.0.1 192.168.0.1 2 | 127.10.0.2 192.168.10.2 127.0.0.2 192.168.0.2 3 | 127.10.0.3 192.168.10.3 127.0.0.3 192.168.0.3 4 | 127.10.0.4 192.168.10.4 127.0.0.4 192.168.0.4 5 | 127.10.0.5 192.168.10.5 127.0.0.5 192.168.0.5 6 | 127.10.0.6 192.168.10.6 127.0.0.6 192.168.0.6 7 | 127.10.0.7 192.168.10.7 127.0.0.7 192.168.0.7 8 | 127.10.0.8 192.168.10.8 127.0.0.8 192.168.0.8 9 | 127.10.0.9 192.168.10.9 127.0.0.9 192.168.0.9 10 | 127.10.0.10 192.168.10.10 127.0.0.10 192.168.0.10 11 | 127.10.0.11 192.168.10.11 127.0.0.11 192.168.0.11 12 | 127.10.0.12 192.168.10.12 127.0.0.12 192.168.0.12 13 | 127.10.0.13 192.168.10.13 127.0.0.13 192.168.0.13 14 | 127.10.0.14 192.168.10.14 127.0.0.14 192.168.0.14 15 | 127.10.0.15 192.168.10.15 127.0.0.15 192.168.0.15 16 | 127.10.0.16 192.168.10.16 127.0.0.16 192.168.0.16 17 | 127.10.0.17 192.168.10.17 127.0.0.17 192.168.0.17 18 | 127.10.0.18 192.168.10.18 127.0.0.18 192.168.0.18 19 | 127.10.0.19 192.168.10.19 127.0.0.19 192.168.0.19 20 | 127.10.0.20 192.168.10.20 127.0.0.20 192.168.0.20 21 | 22 | 128.10.0.1 192.169.10.1 127.0.11.1 193.168.0.1 23 | 128.10.0.2 192.169.10.2 127.0.11.2 193.168.0.2 24 | 128.10.0.3 192.169.10.3 127.0.11.3 193.168.0.3 25 | 128.10.0.4 192.169.10.4 127.0.11.4 193.168.0.4 26 | 128.10.0.5 192.169.10.5 127.0.11.5 193.168.0.5 27 | 128.10.0.6 192.169.10.6 127.0.11.6 193.168.0.6 28 | 128.10.0.7 192.169.10.7 127.0.11.7 193.168.0.7 29 | 128.10.0.8 192.169.10.8 127.0.11.8 193.168.0.8 30 | 128.10.0.9 192.169.10.9 127.0.11.9 193.168.0.9 31 | 128.10.0.10 192.169.10.10 127.0.11.10 193.168.0.10 32 | 128.10.0.11 192.169.10.11 127.0.11.11 193.168.0.11 33 | 128.10.0.12 192.169.10.12 127.0.11.12 193.168.0.12 34 | 128.10.0.13 192.169.10.13 127.0.11.13 193.168.0.13 35 | 128.10.0.14 192.169.10.14 127.0.11.14 193.168.0.14 36 | 128.10.0.15 192.169.10.15 127.0.11.15 193.168.0.15 37 | 128.10.0.16 192.169.10.16 127.0.11.16 193.168.0.16 38 | 128.10.0.17 192.169.10.17 127.0.11.17 193.168.0.17 39 | 128.10.0.18 192.169.10.18 127.0.11.18 193.168.0.18 40 | 128.10.0.19 192.169.10.19 127.0.11.19 193.168.0.19 41 | 128.10.0.20 192.169.10.20 127.0.11.20 193.168.0.20 42 | -------------------------------------------------------------------------------- /samples/test3: -------------------------------------------------------------------------------- 1 | commit: 92329857fabcf913e61351b4e311c53d8c18c1af other: 12329857fabcf913e61351b4e311c53d8c18c1ae 2 | commit: d89189228a4b34ef5478bb24dc04ea3443dd73e0 3 | path: /var/log/nginx.log 4 | path: test/log/nginx.log 5 | 6 | lorem /var/log/nginx.log lorem 7 | -------------------------------------------------------------------------------- /samples/test4: -------------------------------------------------------------------------------- 1 | foo@bar ~/dev/tmux-thumbs master ▶ tmux capture-pane -e -J 2 | foo@bar ~/dev/tmux-thumbs master ▶ tmux capture-pane -e -J -p >> samples/test4 3 | -------------------------------------------------------------------------------- /samples/test5: -------------------------------------------------------------------------------- 1 | lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum commit: 92329857fabcf913e61351b4e311c53d8c18c1af other: 12329857fabcf913e61351b4e311c53d8c18c1ae commit: d89189228a4b34ef5478bb24dc04ea3443dd73e0 path: /var/log/nginx.log path: test/log/nginx.log lorem /var/log/nginx.log lorem 2 | -------------------------------------------------------------------------------- /samples/test6: -------------------------------------------------------------------------------- 1 | diff --git a/src/state.rs b/src/state.rs 2 | index 022c61f..4321097 100644 3 | --- a/src/state.rs 4 | +++ b/src/state.rs 5 | @@ -10,7 +10,7 @@ const PATTERNS: [(&'static str, &'static str); 14] = [] 6 | 7 | diff --git a/src/view.rs b/src/state.rs 8 | index 022c61f..4321097 100644 9 | --- a/src/view.rs 10 | +++ b/src/state.rs 11 | -------------------------------------------------------------------------------- /samples/test7: -------------------------------------------------------------------------------- 1 | This is a really long path: /var/log/nginx/log/nginx/log/nginx/log/nginx/log/nginx/log/nginx/log/nginx/log/nginx/log/nginx/log/nginx/log/nginx/log/nginx.log 2 | This is a another really long path: /var/log/apache/log/apache/log/apache/log/apache/log/apache/log/apache/log/apache/log/apache/log/apache/log/apache/log/apache/log/apache.log 3 | -------------------------------------------------------------------------------- /src/alphabets.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | const ALPHABETS: [(&'static str, &'static str); 22] = [ 4 | ("numeric", "1234567890"), 5 | ("abcd", "abcd"), 6 | ("qwerty", "asdfqwerzxcvjklmiuopghtybn"), 7 | ("qwerty-homerow", "asdfjklgh"), 8 | ("qwerty-left-hand", "asdfqwerzcxv"), 9 | ("qwerty-right-hand", "jkluiopmyhn"), 10 | ("azerty", "qsdfazerwxcvjklmuiopghtybn"), 11 | ("azerty-homerow", "qsdfjkmgh"), 12 | ("azerty-left-hand", "qsdfazerwxcv"), 13 | ("azerty-right-hand", "jklmuiophyn"), 14 | ("qwertz", "asdfqweryxcvjkluiopmghtzbn"), 15 | ("qwertz-homerow", "asdfghjkl"), 16 | ("qwertz-left-hand", "asdfqweryxcv"), 17 | ("qwertz-right-hand", "jkluiopmhzn"), 18 | ("dvorak", "aoeuqjkxpyhtnsgcrlmwvzfidb"), 19 | ("dvorak-homerow", "aoeuhtnsid"), 20 | ("dvorak-left-hand", "aoeupqjkyix"), 21 | ("dvorak-right-hand", "htnsgcrlmwvz"), 22 | ("colemak", "arstqwfpzxcvneioluymdhgjbk"), 23 | ("colemak-homerow", "arstneiodh"), 24 | ("colemak-left-hand", "arstqwfpzxcv"), 25 | ("colemak-right-hand", "neioluymjhk"), 26 | ]; 27 | 28 | pub struct Alphabet<'a> { 29 | letters: &'a str, 30 | } 31 | 32 | impl<'a> Alphabet<'a> { 33 | fn new(letters: &'a str) -> Alphabet { 34 | Alphabet { letters } 35 | } 36 | 37 | pub fn hints(&self, matches: usize) -> Vec { 38 | let letters: Vec = self.letters.chars().map(|s| s.to_string()).collect(); 39 | 40 | let mut expansion = letters.clone(); 41 | let mut expanded: Vec = Vec::new(); 42 | 43 | loop { 44 | if expansion.len() + expanded.len() >= matches { 45 | break; 46 | } 47 | if expansion.is_empty() { 48 | break; 49 | } 50 | 51 | let prefix = expansion.pop().expect("Ouch!"); 52 | let sub_expansion: Vec = letters 53 | .iter() 54 | .take(matches - expansion.len() - expanded.len()) 55 | .map(|s| prefix.clone() + s) 56 | .collect(); 57 | 58 | expanded.splice(0..0, sub_expansion); 59 | } 60 | 61 | expansion = expansion.iter().take(matches - expanded.len()).cloned().collect(); 62 | expansion.append(&mut expanded); 63 | expansion 64 | } 65 | } 66 | 67 | pub fn get_alphabet(alphabet_name: &str) -> Alphabet { 68 | let alphabets: HashMap<&str, &str> = ALPHABETS.iter().cloned().collect(); 69 | 70 | alphabets 71 | .get(alphabet_name) 72 | .expect(format!("Unknown alphabet: {}", alphabet_name).as_str()); // FIXME 73 | 74 | Alphabet::new(alphabets[alphabet_name]) 75 | } 76 | 77 | #[cfg(test)] 78 | mod tests { 79 | use super::*; 80 | 81 | #[test] 82 | fn simple_matches() { 83 | let alphabet = Alphabet::new("abcd"); 84 | let hints = alphabet.hints(3); 85 | assert_eq!(hints, ["a", "b", "c"]); 86 | } 87 | 88 | #[test] 89 | fn composed_matches() { 90 | let alphabet = Alphabet::new("abcd"); 91 | let hints = alphabet.hints(6); 92 | assert_eq!(hints, ["a", "b", "c", "da", "db", "dc"]); 93 | } 94 | 95 | #[test] 96 | fn composed_matches_multiple() { 97 | let alphabet = Alphabet::new("abcd"); 98 | let hints = alphabet.hints(8); 99 | assert_eq!(hints, ["a", "b", "ca", "cb", "da", "db", "dc", "dd"]); 100 | } 101 | 102 | #[test] 103 | fn composed_matches_max() { 104 | let alphabet = Alphabet::new("ab"); 105 | let hints = alphabet.hints(8); 106 | assert_eq!(hints, ["aa", "ab", "ba", "bb"]); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/colors.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use termion::color; 3 | 4 | pub fn get_color(color_name: &str) -> Box { 5 | lazy_static! { 6 | static ref RGB: Regex = Regex::new(r"#([[:xdigit:]]{2})([[:xdigit:]]{2})([[:xdigit:]]{2})").unwrap(); 7 | } 8 | 9 | if let Some(captures) = RGB.captures(color_name) { 10 | let r = u8::from_str_radix(captures.get(1).unwrap().as_str(), 16).unwrap(); 11 | let g = u8::from_str_radix(captures.get(2).unwrap().as_str(), 16).unwrap(); 12 | let b = u8::from_str_radix(captures.get(3).unwrap().as_str(), 16).unwrap(); 13 | 14 | return Box::new(color::Rgb(r, g, b)); 15 | } 16 | 17 | match color_name { 18 | "black" => Box::new(color::Black), 19 | "red" => Box::new(color::Red), 20 | "green" => Box::new(color::Green), 21 | "yellow" => Box::new(color::Yellow), 22 | "blue" => Box::new(color::Blue), 23 | "magenta" => Box::new(color::Magenta), 24 | "cyan" => Box::new(color::Cyan), 25 | "white" => Box::new(color::White), 26 | "default" => Box::new(color::Reset), 27 | _ => panic!("Unknown color: {}", color_name), 28 | } 29 | } 30 | 31 | #[cfg(test)] 32 | mod tests { 33 | use super::*; 34 | 35 | #[test] 36 | fn match_color() { 37 | let text1 = println!("{}{}", color::Fg(&*get_color("green")), "foo"); 38 | let text2 = println!("{}{}", color::Fg(color::Green), "foo"); 39 | 40 | assert_eq!(text1, text2); 41 | } 42 | 43 | #[test] 44 | fn parse_rgb() { 45 | let text1 = println!("{}{}", color::Fg(&*get_color("#1b1cbf")), "foo"); 46 | let text2 = println!("{}{}", color::Fg(color::Rgb(27, 28, 191)), "foo"); 47 | 48 | assert_eq!(text1, text2); 49 | } 50 | 51 | #[test] 52 | #[should_panic] 53 | fn parse_invalid_rgb() { 54 | println!("{}{}", color::Fg(&*get_color("#1b1cbj")), "foo"); 55 | } 56 | 57 | #[test] 58 | #[should_panic] 59 | fn no_match_color() { 60 | println!("{}{}", color::Fg(&*get_color("wat")), "foo"); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate lazy_static; 3 | extern crate base64; 4 | extern crate clap; 5 | extern crate termion; 6 | 7 | mod alphabets; 8 | mod colors; 9 | mod state; 10 | mod view; 11 | 12 | use self::clap::{App, Arg}; 13 | use clap::crate_version; 14 | use std::fs::OpenOptions; 15 | use std::io::{self, Read, Write}; 16 | 17 | #[allow(dead_code)] 18 | fn dbg(msg: &str) { 19 | let mut file = std::fs::OpenOptions::new() 20 | .append(true) 21 | .open("/tmp/thumbs.log") 22 | .expect("Unable to open log file"); 23 | 24 | writeln!(&mut file, "{}", msg).expect("Unable to write log file"); 25 | } 26 | 27 | fn app_args<'a>() -> clap::ArgMatches<'a> { 28 | App::new("thumbs") 29 | .version(crate_version!()) 30 | .about("A lightning fast version copy/pasting like vimium/vimperator") 31 | .arg( 32 | Arg::with_name("alphabet") 33 | .help("Sets the alphabet") 34 | .long("alphabet") 35 | .short("a") 36 | .default_value("qwerty"), 37 | ) 38 | .arg( 39 | Arg::with_name("format") 40 | .help("Specifies the out format for the picked hint. (%U: Upcase, %H: Hint)") 41 | .long("format") 42 | .short("f") 43 | .default_value("%H"), 44 | ) 45 | .arg( 46 | Arg::with_name("foreground_color") 47 | .help("Sets the foregroud color for matches") 48 | .long("fg-color") 49 | .default_value("green"), 50 | ) 51 | .arg( 52 | Arg::with_name("background_color") 53 | .help("Sets the background color for matches") 54 | .long("bg-color") 55 | .default_value("black"), 56 | ) 57 | .arg( 58 | Arg::with_name("hint_foreground_color") 59 | .help("Sets the foregroud color for hints") 60 | .long("hint-fg-color") 61 | .default_value("yellow"), 62 | ) 63 | .arg( 64 | Arg::with_name("hint_background_color") 65 | .help("Sets the background color for hints") 66 | .long("hint-bg-color") 67 | .default_value("black"), 68 | ) 69 | .arg( 70 | Arg::with_name("multi_foreground_color") 71 | .help("Sets the foreground color for a multi selected item") 72 | .long("multi-fg-color") 73 | .default_value("yellow"), 74 | ) 75 | .arg( 76 | Arg::with_name("multi_background_color") 77 | .help("Sets the background color for a multi selected item") 78 | .long("multi-bg-color") 79 | .default_value("black"), 80 | ) 81 | .arg( 82 | Arg::with_name("select_foreground_color") 83 | .help("Sets the foreground color for selection") 84 | .long("select-fg-color") 85 | .default_value("blue"), 86 | ) 87 | .arg( 88 | Arg::with_name("select_background_color") 89 | .help("Sets the background color for selection") 90 | .long("select-bg-color") 91 | .default_value("black"), 92 | ) 93 | .arg( 94 | Arg::with_name("multi") 95 | .help("Enable multi-selection") 96 | .long("multi") 97 | .short("m"), 98 | ) 99 | .arg( 100 | Arg::with_name("reverse") 101 | .help("Reverse the order for assigned hints") 102 | .long("reverse") 103 | .short("r"), 104 | ) 105 | .arg( 106 | Arg::with_name("unique") 107 | .help("Don't show duplicated hints for the same match") 108 | .long("unique") 109 | .short("u"), 110 | ) 111 | .arg( 112 | Arg::with_name("position") 113 | .help("Hint position") 114 | .long("position") 115 | .default_value("left") 116 | .short("p"), 117 | ) 118 | .arg( 119 | Arg::with_name("regexp") 120 | .help("Use this regexp as extra pattern to match") 121 | .long("regexp") 122 | .short("x") 123 | .takes_value(true) 124 | .multiple(true), 125 | ) 126 | .arg( 127 | Arg::with_name("contrast") 128 | .help("Put square brackets around hint for visibility") 129 | .long("contrast") 130 | .short("c"), 131 | ) 132 | .arg( 133 | Arg::with_name("target") 134 | .help("Stores the hint in the specified path") 135 | .long("target") 136 | .short("t") 137 | .takes_value(true), 138 | ) 139 | .get_matches() 140 | } 141 | 142 | fn main() { 143 | let args = app_args(); 144 | let format = args.value_of("format").unwrap(); 145 | let alphabet = args.value_of("alphabet").unwrap(); 146 | let position = args.value_of("position").unwrap(); 147 | let target = args.value_of("target"); 148 | let multi = args.is_present("multi"); 149 | let reverse = args.is_present("reverse"); 150 | let unique = args.is_present("unique"); 151 | let contrast = args.is_present("contrast"); 152 | let regexp = if let Some(items) = args.values_of("regexp") { 153 | items.collect::>() 154 | } else { 155 | [].to_vec() 156 | }; 157 | 158 | let foreground_color = colors::get_color(args.value_of("foreground_color").unwrap()); 159 | let background_color = colors::get_color(args.value_of("background_color").unwrap()); 160 | let hint_foreground_color = colors::get_color(args.value_of("hint_foreground_color").unwrap()); 161 | let hint_background_color = colors::get_color(args.value_of("hint_background_color").unwrap()); 162 | let select_foreground_color = colors::get_color(args.value_of("select_foreground_color").unwrap()); 163 | let select_background_color = colors::get_color(args.value_of("select_background_color").unwrap()); 164 | let multi_foreground_color = colors::get_color(args.value_of("multi_foreground_color").unwrap()); 165 | let multi_background_color = colors::get_color(args.value_of("multi_background_color").unwrap()); 166 | 167 | let stdin = io::stdin(); 168 | let mut handle = stdin.lock(); 169 | let mut output = String::new(); 170 | 171 | handle.read_to_string(&mut output).unwrap(); 172 | 173 | let lines = output.split('\n').collect::>(); 174 | 175 | let mut state = state::State::new(&lines, alphabet, ®exp); 176 | 177 | let selected = { 178 | let mut viewbox = view::View::new( 179 | &mut state, 180 | multi, 181 | reverse, 182 | unique, 183 | contrast, 184 | position, 185 | select_foreground_color, 186 | select_background_color, 187 | multi_foreground_color, 188 | multi_background_color, 189 | foreground_color, 190 | background_color, 191 | hint_foreground_color, 192 | hint_background_color, 193 | ); 194 | 195 | viewbox.present() 196 | }; 197 | 198 | if !selected.is_empty() { 199 | let output = selected 200 | .iter() 201 | .map(|(text, upcase)| { 202 | let upcase_value = if *upcase { "true" } else { "false" }; 203 | 204 | let mut output = format.to_string(); 205 | 206 | output = str::replace(&output, "%U", upcase_value); 207 | output = str::replace(&output, "%H", text.as_str()); 208 | output 209 | }) 210 | .collect::>() 211 | .join("\n"); 212 | 213 | if let Some(target) = target { 214 | let mut file = OpenOptions::new() 215 | .create(true) 216 | .truncate(true) 217 | .write(true) 218 | .open(target) 219 | .expect("Unable to open the target file"); 220 | 221 | file.write(output.as_bytes()).unwrap(); 222 | } else { 223 | print!("{}", output); 224 | } 225 | } else { 226 | ::std::process::exit(1); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use std::collections::HashMap; 3 | use std::fmt; 4 | 5 | const EXCLUDE_PATTERNS: [(&'static str, &'static str); 1] = [("bash", r"[[:cntrl:]]\[([0-9]{1,2};)?([0-9]{1,2})?m")]; 6 | 7 | const PATTERNS: [(&'static str, &'static str); 15] = [ 8 | ("markdown_url", r"\[[^]]*\]\(([^)]+)\)"), 9 | ("url", r"(?P(https?://|git@|git://|ssh://|ftp://|file:///)[^ ]+)"), 10 | ( 11 | "diff_summary", 12 | r"diff --git a/([.\w\-@~\[\]]+?/[.\w\-@\[\]]++) b/([.\w\-@~\[\]]+?/[.\w\-@\[\]]++)", 13 | ), 14 | ("diff_a", r"--- a/([^ ]+)"), 15 | ("diff_b", r"\+\+\+ b/([^ ]+)"), 16 | ("docker", r"sha256:([0-9a-f]{64})"), 17 | ("path", r"(?P([.\w\-@$~\[\]]+)?(/[.\w\-@$\[\]]+)+)"), 18 | ("color", r"#[0-9a-fA-F]{6}"), 19 | ("uid", r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"), 20 | ("ipfs", r"Qm[0-9a-zA-Z]{44}"), 21 | ("sha", r"[0-9a-f]{7,40}"), 22 | ("ip", r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"), 23 | ("ipv6", r"[A-f0-9:]+:+[A-f0-9:]+[%\w\d]+"), 24 | ("address", r"0x[0-9a-fA-F]+"), 25 | ("number", r"[0-9]{4,}"), 26 | ]; 27 | 28 | #[derive(Clone)] 29 | pub struct Match<'a> { 30 | pub x: i32, 31 | pub y: i32, 32 | pub pattern: &'a str, 33 | pub text: &'a str, 34 | pub hint: Option, 35 | } 36 | 37 | impl<'a> fmt::Debug for Match<'a> { 38 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 39 | write!( 40 | f, 41 | "Match {{ x: {}, y: {}, pattern: {}, text: {}, hint: <{}> }}", 42 | self.x, 43 | self.y, 44 | self.pattern, 45 | self.text, 46 | self.hint.clone().unwrap_or("".to_string()) 47 | ) 48 | } 49 | } 50 | 51 | impl<'a> PartialEq for Match<'a> { 52 | fn eq(&self, other: &Match) -> bool { 53 | self.x == other.x && self.y == other.y 54 | } 55 | } 56 | 57 | pub struct State<'a> { 58 | pub lines: &'a Vec<&'a str>, 59 | alphabet: &'a str, 60 | regexp: &'a Vec<&'a str>, 61 | } 62 | 63 | impl<'a> State<'a> { 64 | pub fn new(lines: &'a Vec<&'a str>, alphabet: &'a str, regexp: &'a Vec<&'a str>) -> State<'a> { 65 | State { 66 | lines, 67 | alphabet, 68 | regexp, 69 | } 70 | } 71 | 72 | pub fn matches(&self, reverse: bool, unique: bool) -> Vec> { 73 | let mut matches = Vec::new(); 74 | 75 | let exclude_patterns = EXCLUDE_PATTERNS 76 | .iter() 77 | .map(|tuple| (tuple.0, Regex::new(tuple.1).unwrap())) 78 | .collect::>(); 79 | 80 | let custom_patterns = self 81 | .regexp 82 | .iter() 83 | .map(|regexp| ("custom", Regex::new(regexp).expect("Invalid custom regexp"))) 84 | .collect::>(); 85 | 86 | let patterns = PATTERNS 87 | .iter() 88 | .map(|tuple| (tuple.0, Regex::new(tuple.1).unwrap())) 89 | .collect::>(); 90 | 91 | // This order determines the priority of pattern matching 92 | let all_patterns = [exclude_patterns, custom_patterns, patterns].concat(); 93 | 94 | for (index, line) in self.lines.iter().enumerate() { 95 | let mut chunk: &str = line; 96 | let mut offset: i32 = 0; 97 | 98 | loop { 99 | // For this line we search which patterns match, all of them. 100 | let submatches = all_patterns 101 | .iter() 102 | .filter_map(|tuple| match tuple.1.find_iter(chunk).nth(0) { 103 | Some(m) => Some((tuple.0, tuple.1.clone(), m)), 104 | None => None, 105 | }) 106 | .collect::>(); 107 | 108 | // Then, we search for the match with the lowest index 109 | let first_match_option = submatches.iter().min_by(|x, y| x.2.start().cmp(&y.2.start())); 110 | 111 | if let Some(first_match) = first_match_option { 112 | let (name, pattern, matching) = first_match; 113 | let text = matching.as_str(); 114 | 115 | if let Some(captures) = pattern.captures(text) { 116 | let captures: Vec<(&str, usize)> = if let Some(capture) = captures.name("match") { 117 | [(capture.as_str(), capture.start())].to_vec() 118 | } else if captures.len() > 1 { 119 | captures 120 | .iter() 121 | .skip(1) 122 | .filter_map(|capture| capture) 123 | .map(|capture| (capture.as_str(), capture.start())) 124 | .collect::>() 125 | } else { 126 | [(matching.as_str(), 0)].to_vec() 127 | }; 128 | 129 | // Never hint or broke bash color sequences, but process it 130 | if *name != "bash" { 131 | for (subtext, substart) in captures.iter() { 132 | matches.push(Match { 133 | x: offset + matching.start() as i32 + *substart as i32, 134 | y: index as i32, 135 | pattern: name, 136 | text: subtext, 137 | hint: None, 138 | }); 139 | } 140 | } 141 | 142 | chunk = chunk.get(matching.end()..).expect("Unknown chunk"); 143 | offset += matching.end() as i32; 144 | } else { 145 | panic!("No matching?"); 146 | } 147 | } else { 148 | break; 149 | } 150 | } 151 | } 152 | 153 | let alphabet = super::alphabets::get_alphabet(self.alphabet); 154 | let mut hints = alphabet.hints(matches.len()); 155 | 156 | // This looks wrong but we do a pop after 157 | if !reverse { 158 | hints.reverse(); 159 | } else { 160 | matches.reverse(); 161 | hints.reverse(); 162 | } 163 | 164 | if unique { 165 | let mut previous: HashMap<&str, String> = HashMap::new(); 166 | 167 | for mat in &mut matches { 168 | if let Some(previous_hint) = previous.get(mat.text) { 169 | mat.hint = Some(previous_hint.clone()); 170 | } else if let Some(hint) = hints.pop() { 171 | mat.hint = Some(hint.to_string().clone()); 172 | previous.insert(mat.text, hint.to_string().clone()); 173 | } 174 | } 175 | } else { 176 | for mat in &mut matches { 177 | if let Some(hint) = hints.pop() { 178 | mat.hint = Some(hint.to_string().clone()); 179 | } 180 | } 181 | } 182 | 183 | if reverse { 184 | matches.reverse(); 185 | } 186 | 187 | matches 188 | } 189 | } 190 | 191 | #[cfg(test)] 192 | mod tests { 193 | use super::*; 194 | 195 | fn split(output: &str) -> Vec<&str> { 196 | output.split("\n").collect::>() 197 | } 198 | 199 | #[test] 200 | fn match_reverse() { 201 | let lines = split("lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem"); 202 | let custom = [].to_vec(); 203 | let results = State::new(&lines, "abcd", &custom).matches(false, false); 204 | 205 | assert_eq!(results.len(), 3); 206 | assert_eq!(results.first().unwrap().hint.clone().unwrap(), "a"); 207 | assert_eq!(results.last().unwrap().hint.clone().unwrap(), "c"); 208 | } 209 | 210 | #[test] 211 | fn match_unique() { 212 | let lines = split("lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem"); 213 | let custom = [].to_vec(); 214 | let results = State::new(&lines, "abcd", &custom).matches(false, true); 215 | 216 | assert_eq!(results.len(), 3); 217 | assert_eq!(results.first().unwrap().hint.clone().unwrap(), "a"); 218 | assert_eq!(results.last().unwrap().hint.clone().unwrap(), "a"); 219 | } 220 | 221 | #[test] 222 | fn match_docker() { 223 | let lines = split("latest sha256:30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4 20 hours ago"); 224 | let custom = [].to_vec(); 225 | let results = State::new(&lines, "abcd", &custom).matches(false, false); 226 | 227 | assert_eq!(results.len(), 1); 228 | assert_eq!( 229 | results.get(0).unwrap().text, 230 | "30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4" 231 | ); 232 | } 233 | 234 | #[test] 235 | fn match_bash() { 236 | let lines = split("path: /var/log/nginx.log\npath: test/log/nginx-2.log:32folder/.nginx@4df2.log"); 237 | let custom = [].to_vec(); 238 | let results = State::new(&lines, "abcd", &custom).matches(false, false); 239 | 240 | assert_eq!(results.len(), 3); 241 | assert_eq!(results.get(0).unwrap().text, "/var/log/nginx.log"); 242 | assert_eq!(results.get(1).unwrap().text, "test/log/nginx-2.log"); 243 | assert_eq!(results.get(2).unwrap().text, "folder/.nginx@4df2.log"); 244 | } 245 | 246 | #[test] 247 | fn match_paths() { 248 | let lines = split("Lorem /tmp/foo/bar_lol, lorem\n Lorem /var/log/boot-strap.log lorem ../log/kern.log lorem"); 249 | let custom = [].to_vec(); 250 | let results = State::new(&lines, "abcd", &custom).matches(false, false); 251 | 252 | assert_eq!(results.len(), 3); 253 | assert_eq!(results.get(0).unwrap().text.clone(), "/tmp/foo/bar_lol"); 254 | assert_eq!(results.get(1).unwrap().text.clone(), "/var/log/boot-strap.log"); 255 | assert_eq!(results.get(2).unwrap().text.clone(), "../log/kern.log"); 256 | } 257 | 258 | #[test] 259 | fn match_routes() { 260 | let lines = split("Lorem /app/routes/$routeId/$objectId, lorem\n Lorem /app/routes/$sectionId"); 261 | let custom = [].to_vec(); 262 | let results = State::new(&lines, "abcd", &custom).matches(false, false); 263 | 264 | assert_eq!(results.len(), 2); 265 | assert_eq!(results.get(0).unwrap().text.clone(), "/app/routes/$routeId/$objectId"); 266 | assert_eq!(results.get(1).unwrap().text.clone(), "/app/routes/$sectionId"); 267 | } 268 | 269 | #[test] 270 | fn match_home() { 271 | let lines = split("Lorem ~/.gnu/.config.txt, lorem"); 272 | let custom = [].to_vec(); 273 | let results = State::new(&lines, "abcd", &custom).matches(false, false); 274 | 275 | assert_eq!(results.len(), 1); 276 | assert_eq!(results.get(0).unwrap().text.clone(), "~/.gnu/.config.txt"); 277 | } 278 | 279 | #[test] 280 | fn match_slugs() { 281 | let lines = split("Lorem dev/api/[slug]/foo, lorem"); 282 | let custom = [].to_vec(); 283 | let results = State::new(&lines, "abcd", &custom).matches(false, false); 284 | 285 | assert_eq!(results.len(), 1); 286 | assert_eq!(results.get(0).unwrap().text.clone(), "dev/api/[slug]/foo"); 287 | } 288 | 289 | #[test] 290 | fn match_uids() { 291 | let lines = split("Lorem ipsum 123e4567-e89b-12d3-a456-426655440000 lorem\n Lorem lorem lorem"); 292 | let custom = [].to_vec(); 293 | let results = State::new(&lines, "abcd", &custom).matches(false, false); 294 | 295 | assert_eq!(results.len(), 1); 296 | } 297 | 298 | #[test] 299 | fn match_shas() { 300 | let lines = split("Lorem fd70b5695 5246ddf f924213 lorem\n Lorem 973113963b491874ab2e372ee60d4b4cb75f717c lorem"); 301 | let custom = [].to_vec(); 302 | let results = State::new(&lines, "abcd", &custom).matches(false, false); 303 | 304 | assert_eq!(results.len(), 4); 305 | assert_eq!(results.get(0).unwrap().text.clone(), "fd70b5695"); 306 | assert_eq!(results.get(1).unwrap().text.clone(), "5246ddf"); 307 | assert_eq!(results.get(2).unwrap().text.clone(), "f924213"); 308 | assert_eq!( 309 | results.get(3).unwrap().text.clone(), 310 | "973113963b491874ab2e372ee60d4b4cb75f717c" 311 | ); 312 | } 313 | 314 | #[test] 315 | fn match_ips() { 316 | let lines = split("Lorem ipsum 127.0.0.1 lorem\n Lorem 255.255.10.255 lorem 127.0.0.1 lorem"); 317 | let custom = [].to_vec(); 318 | let results = State::new(&lines, "abcd", &custom).matches(false, false); 319 | 320 | assert_eq!(results.len(), 3); 321 | assert_eq!(results.get(0).unwrap().text.clone(), "127.0.0.1"); 322 | assert_eq!(results.get(1).unwrap().text.clone(), "255.255.10.255"); 323 | assert_eq!(results.get(2).unwrap().text.clone(), "127.0.0.1"); 324 | } 325 | 326 | #[test] 327 | fn match_ipv6s() { 328 | let lines = split("Lorem ipsum fe80::2:202:fe4 lorem\n Lorem 2001:67c:670:202:7ba8:5e41:1591:d723 lorem fe80::2:1 lorem ipsum fe80:22:312:fe::1%eth0"); 329 | let custom = [].to_vec(); 330 | let results = State::new(&lines, "abcd", &custom).matches(false, false); 331 | 332 | assert_eq!(results.len(), 4); 333 | assert_eq!(results.get(0).unwrap().text.clone(), "fe80::2:202:fe4"); 334 | assert_eq!( 335 | results.get(1).unwrap().text.clone(), 336 | "2001:67c:670:202:7ba8:5e41:1591:d723" 337 | ); 338 | assert_eq!(results.get(2).unwrap().text.clone(), "fe80::2:1"); 339 | assert_eq!(results.get(3).unwrap().text.clone(), "fe80:22:312:fe::1%eth0"); 340 | } 341 | 342 | #[test] 343 | fn match_markdown_urls() { 344 | let lines = split("Lorem ipsum [link](https://github.io?foo=bar) ![](http://cdn.com/img.jpg) lorem"); 345 | let custom = [].to_vec(); 346 | let results = State::new(&lines, "abcd", &custom).matches(false, false); 347 | 348 | assert_eq!(results.len(), 2); 349 | assert_eq!(results.get(0).unwrap().pattern.clone(), "markdown_url"); 350 | assert_eq!(results.get(0).unwrap().text.clone(), "https://github.io?foo=bar"); 351 | assert_eq!(results.get(1).unwrap().pattern.clone(), "markdown_url"); 352 | assert_eq!(results.get(1).unwrap().text.clone(), "http://cdn.com/img.jpg"); 353 | } 354 | 355 | #[test] 356 | fn match_urls() { 357 | let lines = split("Lorem ipsum https://www.rust-lang.org/tools lorem\n Lorem ipsumhttps://crates.io lorem https://github.io?foo=bar lorem ssh://github.io"); 358 | let custom = [].to_vec(); 359 | let results = State::new(&lines, "abcd", &custom).matches(false, false); 360 | 361 | assert_eq!(results.len(), 4); 362 | assert_eq!(results.get(0).unwrap().text.clone(), "https://www.rust-lang.org/tools"); 363 | assert_eq!(results.get(0).unwrap().pattern.clone(), "url"); 364 | assert_eq!(results.get(1).unwrap().text.clone(), "https://crates.io"); 365 | assert_eq!(results.get(1).unwrap().pattern.clone(), "url"); 366 | assert_eq!(results.get(2).unwrap().text.clone(), "https://github.io?foo=bar"); 367 | assert_eq!(results.get(2).unwrap().pattern.clone(), "url"); 368 | assert_eq!(results.get(3).unwrap().text.clone(), "ssh://github.io"); 369 | assert_eq!(results.get(3).unwrap().pattern.clone(), "url"); 370 | } 371 | 372 | #[test] 373 | fn match_addresses() { 374 | let lines = split("Lorem 0xfd70b5695 0x5246ddf lorem\n Lorem 0x973113tlorem"); 375 | let custom = [].to_vec(); 376 | let results = State::new(&lines, "abcd", &custom).matches(false, false); 377 | 378 | assert_eq!(results.len(), 3); 379 | assert_eq!(results.get(0).unwrap().text.clone(), "0xfd70b5695"); 380 | assert_eq!(results.get(1).unwrap().text.clone(), "0x5246ddf"); 381 | assert_eq!(results.get(2).unwrap().text.clone(), "0x973113"); 382 | } 383 | 384 | #[test] 385 | fn match_hex_colors() { 386 | let lines = split("Lorem #fd7b56 lorem #FF00FF\n Lorem #00fF05 lorem #abcd00 lorem #afRR00"); 387 | let custom = [].to_vec(); 388 | let results = State::new(&lines, "abcd", &custom).matches(false, false); 389 | 390 | assert_eq!(results.len(), 4); 391 | assert_eq!(results.get(0).unwrap().text.clone(), "#fd7b56"); 392 | assert_eq!(results.get(1).unwrap().text.clone(), "#FF00FF"); 393 | assert_eq!(results.get(2).unwrap().text.clone(), "#00fF05"); 394 | assert_eq!(results.get(3).unwrap().text.clone(), "#abcd00"); 395 | } 396 | 397 | #[test] 398 | fn match_ipfs() { 399 | let lines = split("Lorem QmRdbNSxDJBXmssAc9fvTtux4duptMvfSGiGuq6yHAQVKQ lorem Qmfoobar"); 400 | let custom = [].to_vec(); 401 | let results = State::new(&lines, "abcd", &custom).matches(false, false); 402 | 403 | assert_eq!(results.len(), 1); 404 | assert_eq!( 405 | results.get(0).unwrap().text.clone(), 406 | "QmRdbNSxDJBXmssAc9fvTtux4duptMvfSGiGuq6yHAQVKQ" 407 | ); 408 | } 409 | 410 | #[test] 411 | fn match_process_port() { 412 | let lines = 413 | split("Lorem 5695 52463 lorem\n Lorem 973113 lorem 99999 lorem 8888 lorem\n 23456 lorem 5432 lorem 23444"); 414 | let custom = [].to_vec(); 415 | let results = State::new(&lines, "abcd", &custom).matches(false, false); 416 | 417 | assert_eq!(results.len(), 8); 418 | } 419 | 420 | #[test] 421 | fn match_diff_a() { 422 | let lines = split("Lorem lorem\n--- a/src/main.rs"); 423 | let custom = [].to_vec(); 424 | let results = State::new(&lines, "abcd", &custom).matches(false, false); 425 | 426 | assert_eq!(results.len(), 1); 427 | assert_eq!(results.get(0).unwrap().text.clone(), "src/main.rs"); 428 | } 429 | 430 | #[test] 431 | fn match_diff_b() { 432 | let lines = split("Lorem lorem\n+++ b/src/main.rs"); 433 | let custom = [].to_vec(); 434 | let results = State::new(&lines, "abcd", &custom).matches(false, false); 435 | 436 | assert_eq!(results.len(), 1); 437 | assert_eq!(results.get(0).unwrap().text.clone(), "src/main.rs"); 438 | } 439 | 440 | #[test] 441 | fn match_diff_summary() { 442 | let lines = split("diff --git a/samples/test1 b/samples/test2"); 443 | let custom = [].to_vec(); 444 | let results = State::new(&lines, "abcd", &custom).matches(false, false); 445 | 446 | assert_eq!(results.len(), 2); 447 | assert_eq!(results.get(0).unwrap().text.clone(), "samples/test1"); 448 | assert_eq!(results.get(1).unwrap().text.clone(), "samples/test2"); 449 | } 450 | 451 | #[test] 452 | fn priority() { 453 | let lines = split("Lorem [link](http://foo.bar) ipsum CUSTOM-52463 lorem ISSUE-123 lorem\nLorem /var/fd70b569/9999.log 52463 lorem\n Lorem 973113 lorem 123e4567-e89b-12d3-a456-426655440000 lorem 8888 lorem\n https://crates.io/23456/fd70b569 lorem"); 454 | let custom = ["CUSTOM-[0-9]{4,}", "ISSUE-[0-9]{3}"].to_vec(); 455 | let results = State::new(&lines, "abcd", &custom).matches(false, false); 456 | 457 | assert_eq!(results.len(), 9); 458 | assert_eq!(results.get(0).unwrap().text.clone(), "http://foo.bar"); 459 | assert_eq!(results.get(1).unwrap().text.clone(), "CUSTOM-52463"); 460 | assert_eq!(results.get(2).unwrap().text.clone(), "ISSUE-123"); 461 | assert_eq!(results.get(3).unwrap().text.clone(), "/var/fd70b569/9999.log"); 462 | assert_eq!(results.get(4).unwrap().text.clone(), "52463"); 463 | assert_eq!(results.get(5).unwrap().text.clone(), "973113"); 464 | assert_eq!( 465 | results.get(6).unwrap().text.clone(), 466 | "123e4567-e89b-12d3-a456-426655440000" 467 | ); 468 | assert_eq!(results.get(7).unwrap().text.clone(), "8888"); 469 | assert_eq!(results.get(8).unwrap().text.clone(), "https://crates.io/23456/fd70b569"); 470 | } 471 | } 472 | -------------------------------------------------------------------------------- /src/swapper.rs: -------------------------------------------------------------------------------- 1 | extern crate clap; 2 | 3 | use self::clap::{App, Arg}; 4 | use clap::crate_version; 5 | use regex::Regex; 6 | use std::io::Write; 7 | use std::process::Command; 8 | use std::time::{SystemTime, UNIX_EPOCH}; 9 | 10 | trait Executor { 11 | fn execute(&mut self, args: Vec) -> String; 12 | fn last_executed(&self) -> Option>; 13 | } 14 | 15 | struct RealShell { 16 | executed: Option>, 17 | } 18 | 19 | impl RealShell { 20 | fn new() -> RealShell { 21 | RealShell { executed: None } 22 | } 23 | } 24 | 25 | impl Executor for RealShell { 26 | fn execute(&mut self, args: Vec) -> String { 27 | let execution = Command::new(args[0].as_str()) 28 | .args(&args[1..]) 29 | .output() 30 | .expect("Couldn't run it"); 31 | 32 | self.executed = Some(args); 33 | 34 | let output: String = String::from_utf8_lossy(&execution.stdout).into(); 35 | 36 | output.trim_end().to_string() 37 | } 38 | 39 | fn last_executed(&self) -> Option> { 40 | self.executed.clone() 41 | } 42 | } 43 | 44 | const TMP_FILE: &str = "/tmp/thumbs-last"; 45 | 46 | #[allow(dead_code)] 47 | fn dbg(msg: &str) { 48 | let mut file = std::fs::OpenOptions::new() 49 | .create(true) 50 | .write(true) 51 | .append(true) 52 | .open("/tmp/thumbs.log") 53 | .expect("Unable to open log file"); 54 | 55 | writeln!(&mut file, "{}", msg).expect("Unable to write log file"); 56 | } 57 | 58 | pub struct Swapper<'a> { 59 | executor: Box<&'a mut dyn Executor>, 60 | dir: String, 61 | command: String, 62 | upcase_command: String, 63 | multi_command: String, 64 | osc52: bool, 65 | active_pane_id: Option, 66 | active_pane_height: Option, 67 | active_pane_scroll_position: Option, 68 | active_pane_zoomed: Option, 69 | thumbs_pane_id: Option, 70 | content: Option, 71 | signal: String, 72 | } 73 | 74 | impl<'a> Swapper<'a> { 75 | fn new( 76 | executor: Box<&'a mut dyn Executor>, 77 | dir: String, 78 | command: String, 79 | upcase_command: String, 80 | multi_command: String, 81 | osc52: bool, 82 | ) -> Swapper { 83 | let since_the_epoch = SystemTime::now() 84 | .duration_since(UNIX_EPOCH) 85 | .expect("Time went backwards"); 86 | let signal = format!("thumbs-finished-{}", since_the_epoch.as_secs()); 87 | 88 | Swapper { 89 | executor, 90 | dir, 91 | command, 92 | upcase_command, 93 | multi_command, 94 | osc52, 95 | active_pane_id: None, 96 | active_pane_height: None, 97 | active_pane_scroll_position: None, 98 | active_pane_zoomed: None, 99 | thumbs_pane_id: None, 100 | content: None, 101 | signal, 102 | } 103 | } 104 | 105 | pub fn capture_active_pane(&mut self) { 106 | let active_command = vec![ 107 | "tmux", 108 | "list-panes", 109 | "-F", 110 | "#{pane_id}:#{?pane_in_mode,1,0}:#{pane_height}:#{scroll_position}:#{window_zoomed_flag}:#{?pane_active,active,nope}", 111 | ]; 112 | 113 | let output = self 114 | .executor 115 | .execute(active_command.iter().map(|arg| arg.to_string()).collect()); 116 | 117 | let lines: Vec<&str> = output.split('\n').collect(); 118 | let chunks: Vec> = lines.into_iter().map(|line| line.split(':').collect()).collect(); 119 | 120 | let active_pane = chunks 121 | .iter() 122 | .find(|&chunks| *chunks.get(5).unwrap() == "active") 123 | .expect("Unable to find active pane"); 124 | 125 | let pane_id = active_pane.get(0).unwrap(); 126 | 127 | self.active_pane_id = Some(pane_id.to_string()); 128 | 129 | let pane_height = active_pane 130 | .get(2) 131 | .unwrap() 132 | .parse() 133 | .expect("Unable to retrieve pane height"); 134 | 135 | self.active_pane_height = Some(pane_height); 136 | 137 | if active_pane.get(1).unwrap().to_string() == "1" { 138 | let pane_scroll_position = active_pane 139 | .get(3) 140 | .unwrap() 141 | .parse() 142 | .expect("Unable to retrieve pane scroll"); 143 | 144 | self.active_pane_scroll_position = Some(pane_scroll_position); 145 | } 146 | 147 | let zoomed_pane = *active_pane.get(4).expect("Unable to retrieve zoom pane property") == "1"; 148 | 149 | self.active_pane_zoomed = Some(zoomed_pane); 150 | } 151 | 152 | pub fn execute_thumbs(&mut self) { 153 | let options_command = vec!["tmux", "show", "-g"]; 154 | let params: Vec = options_command.iter().map(|arg| arg.to_string()).collect(); 155 | let options = self.executor.execute(params); 156 | let lines: Vec<&str> = options.split('\n').collect(); 157 | 158 | let pattern = Regex::new(r#"^@thumbs-([\w\-0-9]+)\s+"?([^"]+)"?$"#).unwrap(); 159 | 160 | let args = lines 161 | .iter() 162 | .flat_map(|line| { 163 | if let Some(captures) = pattern.captures(line) { 164 | let name = captures.get(1).unwrap().as_str(); 165 | let value = captures.get(2).unwrap().as_str(); 166 | 167 | let boolean_params = vec!["reverse", "unique", "contrast"]; 168 | 169 | if boolean_params.iter().any(|&x| x == name) { 170 | return vec![format!("--{}", name)]; 171 | } 172 | 173 | let string_params = vec![ 174 | "alphabet", 175 | "position", 176 | "fg-color", 177 | "bg-color", 178 | "hint-bg-color", 179 | "hint-fg-color", 180 | "select-fg-color", 181 | "select-bg-color", 182 | "multi-fg-color", 183 | "multi-bg-color", 184 | ]; 185 | 186 | if string_params.iter().any(|&x| x == name) { 187 | return vec![format!("--{}", name), format!("'{}'", value)]; 188 | } 189 | 190 | if name.starts_with("regexp") { 191 | return vec!["--regexp".to_string(), format!("'{}'", value.replace("\\\\", "\\"))]; 192 | } 193 | 194 | vec![] 195 | } else { 196 | vec![] 197 | } 198 | }) 199 | .collect::>(); 200 | 201 | let active_pane_id = self.active_pane_id.as_mut().unwrap().clone(); 202 | 203 | let scroll_params = 204 | if let (Some(pane_height), Some(scroll_position)) = (self.active_pane_height, self.active_pane_scroll_position) { 205 | format!(" -S {} -E {}", -scroll_position, pane_height - scroll_position - 1) 206 | } else { 207 | "".to_string() 208 | }; 209 | 210 | let active_pane_zoomed = self.active_pane_zoomed.as_mut().unwrap().clone(); 211 | let zoom_command = if active_pane_zoomed { 212 | format!("tmux resize-pane -t {} -Z;", active_pane_id) 213 | } else { 214 | "".to_string() 215 | }; 216 | 217 | let pane_command = format!( 218 | "tmux capture-pane -J -t {active_pane_id} -p{scroll_params} | tail -n {height} | {dir}/target/release/thumbs -f '%U:%H' -t {tmp} {args}; tmux swap-pane -t {active_pane_id}; {zoom_command} tmux wait-for -S {signal}", 219 | active_pane_id = active_pane_id, 220 | scroll_params = scroll_params, 221 | height = self.active_pane_height.unwrap_or(i32::MAX), 222 | dir = self.dir, 223 | tmp = TMP_FILE, 224 | args = args.join(" "), 225 | zoom_command = zoom_command, 226 | signal = self.signal 227 | ); 228 | 229 | let thumbs_command = vec![ 230 | "tmux", 231 | "new-window", 232 | "-P", 233 | "-F", 234 | "#{pane_id}", 235 | "-d", 236 | "-n", 237 | "[thumbs]", 238 | pane_command.as_str(), 239 | ]; 240 | 241 | let params: Vec = thumbs_command.iter().map(|arg| arg.to_string()).collect(); 242 | 243 | self.thumbs_pane_id = Some(self.executor.execute(params)); 244 | } 245 | 246 | pub fn swap_panes(&mut self) { 247 | let active_pane_id = self.active_pane_id.as_mut().unwrap().clone(); 248 | let thumbs_pane_id = self.thumbs_pane_id.as_mut().unwrap().clone(); 249 | 250 | let swap_command = vec![ 251 | "tmux", 252 | "swap-pane", 253 | "-d", 254 | "-s", 255 | active_pane_id.as_str(), 256 | "-t", 257 | thumbs_pane_id.as_str(), 258 | ]; 259 | 260 | let params = swap_command 261 | .iter() 262 | .filter(|&s| !s.is_empty()) 263 | .map(|arg| arg.to_string()) 264 | .collect(); 265 | 266 | self.executor.execute(params); 267 | } 268 | 269 | pub fn resize_pane(&mut self) { 270 | let active_pane_zoomed = self.active_pane_zoomed.as_mut().unwrap().clone(); 271 | 272 | if !active_pane_zoomed { 273 | return; 274 | } 275 | 276 | let thumbs_pane_id = self.thumbs_pane_id.as_mut().unwrap().clone(); 277 | 278 | let resize_command = vec!["tmux", "resize-pane", "-t", thumbs_pane_id.as_str(), "-Z"]; 279 | 280 | let params = resize_command 281 | .iter() 282 | .filter(|&s| !s.is_empty()) 283 | .map(|arg| arg.to_string()) 284 | .collect(); 285 | 286 | self.executor.execute(params); 287 | } 288 | 289 | pub fn wait_thumbs(&mut self) { 290 | let wait_command = vec!["tmux", "wait-for", self.signal.as_str()]; 291 | let params = wait_command.iter().map(|arg| arg.to_string()).collect(); 292 | 293 | self.executor.execute(params); 294 | } 295 | 296 | pub fn retrieve_content(&mut self) { 297 | let retrieve_command = vec!["cat", TMP_FILE]; 298 | let params = retrieve_command.iter().map(|arg| arg.to_string()).collect(); 299 | 300 | self.content = Some(self.executor.execute(params)); 301 | } 302 | 303 | pub fn destroy_content(&mut self) { 304 | let retrieve_command = vec!["rm", TMP_FILE]; 305 | let params = retrieve_command.iter().map(|arg| arg.to_string()).collect(); 306 | 307 | self.executor.execute(params); 308 | } 309 | 310 | pub fn send_osc52(&mut self) {} 311 | 312 | pub fn execute_command(&mut self) { 313 | let content = self.content.clone().unwrap(); 314 | let items: Vec<&str> = content.split('\n').collect(); 315 | 316 | if items.len() > 1 { 317 | let text = items 318 | .iter() 319 | .map(|item| item.splitn(2, ':').last().unwrap()) 320 | .collect::>() 321 | .join(" "); 322 | 323 | self.execute_final_command(&text, &self.multi_command.clone()); 324 | 325 | return; 326 | } 327 | 328 | // Only one item 329 | let item: &str = items.first().unwrap(); 330 | 331 | let mut splitter = item.splitn(2, ':'); 332 | 333 | if let Some(upcase) = splitter.next() { 334 | if let Some(text) = splitter.next() { 335 | if self.osc52 { 336 | let base64_text = base64::encode(text.as_bytes()); 337 | let osc_seq = format!("\x1b]52;0;{}\x07", base64_text); 338 | let tmux_seq = format!("\x1bPtmux;{}\x1b\\", osc_seq.replace("\x1b", "\x1b\x1b")); 339 | 340 | // FIXME: Review if this comment is still rellevant 341 | // 342 | // When the user selects a match: 343 | // 1. The `rustbox` object created in the `viewbox` above is dropped. 344 | // 2. During its `drop`, the `rustbox` object sends a CSI 1049 escape 345 | // sequence to tmux. 346 | // 3. This escape sequence causes the `window_pane_alternate_off` function 347 | // in tmux to be called. 348 | // 4. In `window_pane_alternate_off`, tmux sets the needs-redraw flag in the 349 | // pane. 350 | // 5. If we print the OSC copy escape sequence before the redraw is completed, 351 | // tmux will *not* send the sequence to the host terminal. See the following 352 | // call chain in tmux: `input_dcs_dispatch` -> `screen_write_rawstring` 353 | // -> `tty_write` -> `tty_client_ready`. In this case, `tty_client_ready` 354 | // will return false, thus preventing the escape sequence from being sent. 355 | // 356 | // Therefore, for now we wait a little bit here for the redraw to finish. 357 | std::thread::sleep(std::time::Duration::from_millis(100)); 358 | 359 | std::io::stdout().write_all(tmux_seq.as_bytes()).unwrap(); 360 | std::io::stdout().flush().unwrap(); 361 | } 362 | 363 | let execute_command = if upcase.trim_end() == "true" { 364 | self.upcase_command.clone() 365 | } else { 366 | self.command.clone() 367 | }; 368 | 369 | // The command we run has two arguments: 370 | // * The first arg is the (trimmed) text. This gets stored in a variable, in order to 371 | // preserve quoting and special characters. 372 | // 373 | // * The second argument is the user's command, with the '{}' token replaced with an 374 | // unquoted reference to the variable containing the text. 375 | // 376 | // The reference is unquoted, unfortunately, because the token may already have been 377 | // spliced into a string (e.g 'tmux display-message "Copied {}"'), and it's impossible (or 378 | // at least exceedingly difficult) to determine the correct quoting level. 379 | // 380 | // The alternative of literally splicing the text into the command is bad and it causes all 381 | // kinds of harmful escaping issues that the user cannot reasonable avoid. 382 | // 383 | // For example, imagine some pattern matched the text "foo;rm *" and the user's command was 384 | // an innocuous "echo {}". With literal splicing, we would run the command "echo foo;rm *". 385 | // That's BAD. Without splicing, instead we execute "echo ${THUMB}" which does mostly the 386 | // right thing regardless the contents of the text. (At worst, bash will word-separate the 387 | // unquoted variable; but it won't _execute_ those words in common scenarios). 388 | // 389 | // Ideally user commands would just use "${THUMB}" to begin with rather than having any 390 | // sort of ad-hoc string splicing here at all, and then they could specify the quoting they 391 | // want, but that would break backwards compatibility. 392 | self.execute_final_command(text.trim_end(), &execute_command); 393 | } 394 | } 395 | } 396 | 397 | pub fn execute_final_command(&mut self, text: &str, execute_command: &str) { 398 | let final_command = str::replace(execute_command, "{}", "${THUMB}"); 399 | let retrieve_command = vec![ 400 | "bash", 401 | "-c", 402 | "THUMB=\"$1\"; eval \"$2\"", 403 | "--", 404 | text, 405 | final_command.as_str(), 406 | ]; 407 | 408 | let params = retrieve_command.iter().map(|arg| arg.to_string()).collect(); 409 | 410 | self.executor.execute(params); 411 | } 412 | } 413 | 414 | #[cfg(test)] 415 | mod tests { 416 | use super::*; 417 | 418 | struct TestShell { 419 | outputs: Vec, 420 | executed: Option>, 421 | } 422 | 423 | impl TestShell { 424 | fn new(outputs: Vec) -> TestShell { 425 | TestShell { 426 | executed: None, 427 | outputs, 428 | } 429 | } 430 | } 431 | 432 | impl Executor for TestShell { 433 | fn execute(&mut self, args: Vec) -> String { 434 | self.executed = Some(args); 435 | self.outputs.pop().unwrap() 436 | } 437 | 438 | fn last_executed(&self) -> Option> { 439 | self.executed.clone() 440 | } 441 | } 442 | 443 | #[test] 444 | fn retrieve_active_pane() { 445 | let last_command_outputs = vec!["%97:100:24:1:0:active\n%106:100:24:1:0:nope\n%107:100:24:1:0:nope\n".to_string()]; 446 | let mut executor = TestShell::new(last_command_outputs); 447 | let mut swapper = Swapper::new( 448 | Box::new(&mut executor), 449 | "".to_string(), 450 | "".to_string(), 451 | "".to_string(), 452 | "".to_string(), 453 | false, 454 | ); 455 | 456 | swapper.capture_active_pane(); 457 | 458 | assert_eq!(swapper.active_pane_id.unwrap(), "%97"); 459 | } 460 | 461 | #[test] 462 | fn swap_panes() { 463 | let last_command_outputs = vec![ 464 | "".to_string(), 465 | "%100".to_string(), 466 | "".to_string(), 467 | "%106:100:24:1:0:nope\n%98:100:24:1:0:active\n%107:100:24:1:0:nope\n".to_string(), 468 | ]; 469 | let mut executor = TestShell::new(last_command_outputs); 470 | let mut swapper = Swapper::new( 471 | Box::new(&mut executor), 472 | "".to_string(), 473 | "".to_string(), 474 | "".to_string(), 475 | "".to_string(), 476 | false, 477 | ); 478 | 479 | swapper.capture_active_pane(); 480 | swapper.execute_thumbs(); 481 | swapper.swap_panes(); 482 | 483 | let expectation = vec!["tmux", "swap-pane", "-d", "-s", "%98", "-t", "%100"]; 484 | 485 | assert_eq!(executor.last_executed().unwrap(), expectation); 486 | } 487 | 488 | #[test] 489 | fn quoted_execution() { 490 | let last_command_outputs = vec!["Blah blah blah, the ignored user script output".to_string()]; 491 | let mut executor = TestShell::new(last_command_outputs); 492 | 493 | let user_command = "echo \"{}\"".to_string(); 494 | let upcase_command = "open \"{}\"".to_string(); 495 | let multi_command = "open \"{}\"".to_string(); 496 | let mut swapper = Swapper::new( 497 | Box::new(&mut executor), 498 | "".to_string(), 499 | user_command, 500 | upcase_command, 501 | multi_command, 502 | false, 503 | ); 504 | 505 | swapper.content = Some(format!( 506 | "{do_upcase}:{thumb_text}", 507 | do_upcase = false, 508 | thumb_text = "foobar;rm *", 509 | )); 510 | swapper.execute_command(); 511 | 512 | let expectation = vec![ 513 | "bash", 514 | // The actual shell command: 515 | "-c", 516 | "THUMB=\"$1\"; eval \"$2\"", 517 | // $0: The non-existent program name. 518 | "--", 519 | // $1: The value assigned to THUMB above. 520 | // Not interpreted as a shell expression! 521 | "foobar;rm *", 522 | // $2: The user script, with {} replaced with ${THUMB}, 523 | // and will be eval'd with THUMB in scope. 524 | "echo \"${THUMB}\"", 525 | ]; 526 | 527 | assert_eq!(executor.last_executed().unwrap(), expectation); 528 | } 529 | } 530 | 531 | fn app_args<'a>() -> clap::ArgMatches<'a> { 532 | App::new("tmux-thumbs") 533 | .version(crate_version!()) 534 | .about("A lightning fast version of tmux-fingers, copy/pasting tmux like vimium/vimperator") 535 | .arg( 536 | Arg::with_name("dir") 537 | .help("Directory where to execute thumbs") 538 | .long("dir") 539 | .default_value(""), 540 | ) 541 | .arg( 542 | Arg::with_name("command") 543 | .help("Command to execute after choose a hint") 544 | .long("command") 545 | .default_value("tmux set-buffer -- \"{}\" && tmux display-message \"Copied {}\""), 546 | ) 547 | .arg( 548 | Arg::with_name("upcase_command") 549 | .help("Command to execute after choose a hint, in upcase") 550 | .long("upcase-command") 551 | .default_value("tmux set-buffer -- \"{}\" && tmux paste-buffer && tmux display-message \"Copied {}\""), 552 | ) 553 | .arg( 554 | Arg::with_name("multi_command") 555 | .help("Command to execute after choose multiple hints") 556 | .long("multi-command") 557 | .default_value("tmux set-buffer -- \"{}\" && tmux paste-buffer && tmux display-message \"Multi copied {}\""), 558 | ) 559 | .arg( 560 | Arg::with_name("osc52") 561 | .help("Print OSC52 copy escape sequence in addition to running the pick command") 562 | .long("osc52") 563 | .short("o"), 564 | ) 565 | .get_matches() 566 | } 567 | 568 | fn main() -> std::io::Result<()> { 569 | let args = app_args(); 570 | let dir = args.value_of("dir").unwrap(); 571 | let command = args.value_of("command").unwrap(); 572 | let upcase_command = args.value_of("upcase_command").unwrap(); 573 | let multi_command = args.value_of("multi_command").unwrap(); 574 | let osc52 = args.is_present("osc52"); 575 | 576 | if dir.is_empty() { 577 | panic!("Invalid tmux-thumbs execution. Are you trying to execute tmux-thumbs directly?") 578 | } 579 | 580 | let mut executor = RealShell::new(); 581 | let mut swapper = Swapper::new( 582 | Box::new(&mut executor), 583 | dir.to_string(), 584 | command.to_string(), 585 | upcase_command.to_string(), 586 | multi_command.to_string(), 587 | osc52, 588 | ); 589 | 590 | swapper.capture_active_pane(); 591 | swapper.execute_thumbs(); 592 | swapper.swap_panes(); 593 | swapper.resize_pane(); 594 | swapper.wait_thumbs(); 595 | swapper.retrieve_content(); 596 | swapper.destroy_content(); 597 | swapper.execute_command(); 598 | 599 | Ok(()) 600 | } 601 | -------------------------------------------------------------------------------- /src/view.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use std::char; 3 | use std::io::{stdout, Read, Write}; 4 | use termion::async_stdin; 5 | use termion::event::Key; 6 | use termion::input::TermRead; 7 | use termion::raw::IntoRawMode; 8 | use termion::screen::AlternateScreen; 9 | use termion::{color, cursor}; 10 | 11 | use unicode_width::UnicodeWidthStr; 12 | 13 | pub struct View<'a> { 14 | state: &'a mut state::State<'a>, 15 | skip: usize, 16 | multi: bool, 17 | contrast: bool, 18 | position: &'a str, 19 | matches: Vec>, 20 | select_foreground_color: Box, 21 | select_background_color: Box, 22 | multi_foreground_color: Box, 23 | multi_background_color: Box, 24 | foreground_color: Box, 25 | background_color: Box, 26 | hint_background_color: Box, 27 | hint_foreground_color: Box, 28 | chosen: Vec<(String, bool)>, 29 | } 30 | 31 | enum CaptureEvent { 32 | Exit, 33 | Hint, 34 | } 35 | 36 | impl<'a> View<'a> { 37 | pub fn new( 38 | state: &'a mut state::State<'a>, 39 | multi: bool, 40 | reverse: bool, 41 | unique: bool, 42 | contrast: bool, 43 | position: &'a str, 44 | select_foreground_color: Box, 45 | select_background_color: Box, 46 | multi_foreground_color: Box, 47 | multi_background_color: Box, 48 | foreground_color: Box, 49 | background_color: Box, 50 | hint_foreground_color: Box, 51 | hint_background_color: Box, 52 | ) -> View<'a> { 53 | let matches = state.matches(reverse, unique); 54 | let skip = if reverse { matches.len() - 1 } else { 0 }; 55 | 56 | View { 57 | state, 58 | skip, 59 | multi, 60 | contrast, 61 | position, 62 | matches, 63 | select_foreground_color, 64 | select_background_color, 65 | multi_foreground_color, 66 | multi_background_color, 67 | foreground_color, 68 | background_color, 69 | hint_foreground_color, 70 | hint_background_color, 71 | chosen: vec![], 72 | } 73 | } 74 | 75 | pub fn prev(&mut self) { 76 | if self.skip > 0 { 77 | self.skip -= 1; 78 | } 79 | } 80 | 81 | pub fn next(&mut self) { 82 | if self.skip < self.matches.len() - 1 { 83 | self.skip += 1; 84 | } 85 | } 86 | 87 | fn make_hint_text(&self, hint: &str) -> String { 88 | if self.contrast { 89 | format!("[{}]", hint) 90 | } else { 91 | hint.to_string() 92 | } 93 | } 94 | 95 | fn render(&self, stdout: &mut dyn Write, typed_hint: &str) -> () { 96 | write!(stdout, "{}", cursor::Hide).unwrap(); 97 | 98 | for (index, line) in self.state.lines.iter().enumerate() { 99 | let clean = line.trim_end_matches(|c: char| c.is_whitespace()); 100 | 101 | if !clean.is_empty() { 102 | print!("{goto}{text}", goto = cursor::Goto(1, index as u16 + 1), text = line); 103 | } 104 | } 105 | 106 | let selected = self.matches.get(self.skip); 107 | 108 | for mat in self.matches.iter() { 109 | let chosen_hint = self.chosen.iter().any(|(hint, _)| hint == mat.text); 110 | 111 | let selected_color = if chosen_hint { 112 | &self.multi_foreground_color 113 | } else if selected == Some(mat) { 114 | &self.select_foreground_color 115 | } else { 116 | &self.foreground_color 117 | }; 118 | let selected_background_color = if chosen_hint { 119 | &self.multi_background_color 120 | } else if selected == Some(mat) { 121 | &self.select_background_color 122 | } else { 123 | &self.background_color 124 | }; 125 | 126 | // Find long utf sequences and extract it from mat.x 127 | let line = &self.state.lines[mat.y as usize]; 128 | let prefix = &line[0..mat.x as usize]; 129 | let extra = prefix.width_cjk() - prefix.chars().count(); 130 | let offset = (mat.x as u16) - (extra as u16); 131 | let text = self.make_hint_text(mat.text); 132 | 133 | print!( 134 | "{goto}{background}{foregroud}{text}{resetf}{resetb}", 135 | goto = cursor::Goto(offset + 1, mat.y as u16 + 1), 136 | foregroud = color::Fg(&**selected_color), 137 | background = color::Bg(&**selected_background_color), 138 | resetf = color::Fg(color::Reset), 139 | resetb = color::Bg(color::Reset), 140 | text = &text 141 | ); 142 | 143 | if let Some(ref hint) = mat.hint { 144 | let extra_position = match self.position { 145 | "right" => text.width_cjk() - hint.len(), 146 | "off_left" => 0 - hint.len() - if self.contrast { 2 } else { 0 }, 147 | "off_right" => text.width_cjk(), 148 | _ => 0, 149 | }; 150 | 151 | let text = self.make_hint_text(hint.as_str()); 152 | let final_position = std::cmp::max(offset as i16 + extra_position as i16, 0); 153 | 154 | print!( 155 | "{goto}{background}{foregroud}{text}{resetf}{resetb}", 156 | goto = cursor::Goto(final_position as u16 + 1, mat.y as u16 + 1), 157 | foregroud = color::Fg(&*self.hint_foreground_color), 158 | background = color::Bg(&*self.hint_background_color), 159 | resetf = color::Fg(color::Reset), 160 | resetb = color::Bg(color::Reset), 161 | text = &text 162 | ); 163 | 164 | if hint.starts_with(typed_hint) { 165 | print!( 166 | "{goto}{background}{foregroud}{text}{resetf}{resetb}", 167 | goto = cursor::Goto(final_position as u16 + 1, mat.y as u16 + 1), 168 | foregroud = color::Fg(&*self.multi_foreground_color), 169 | background = color::Bg(&*self.multi_background_color), 170 | resetf = color::Fg(color::Reset), 171 | resetb = color::Bg(color::Reset), 172 | text = &typed_hint 173 | ); 174 | } 175 | } 176 | } 177 | 178 | stdout.flush().unwrap(); 179 | } 180 | 181 | fn listen(&mut self, stdin: &mut dyn Read, stdout: &mut dyn Write) -> CaptureEvent { 182 | if self.matches.is_empty() { 183 | return CaptureEvent::Exit; 184 | } 185 | 186 | let mut typed_hint: String = "".to_owned(); 187 | let longest_hint = self 188 | .matches 189 | .iter() 190 | .filter_map(|m| m.hint.clone()) 191 | .max_by(|x, y| x.len().cmp(&y.len())) 192 | .unwrap() 193 | .clone(); 194 | 195 | self.render(stdout, &typed_hint); 196 | 197 | loop { 198 | match stdin.keys().next() { 199 | Some(key) => { 200 | match key { 201 | Ok(key) => { 202 | match key { 203 | Key::Esc => { 204 | if self.multi && !typed_hint.is_empty() { 205 | typed_hint.clear(); 206 | } else { 207 | break; 208 | } 209 | } 210 | Key::Up => { 211 | self.prev(); 212 | } 213 | Key::Down => { 214 | self.next(); 215 | } 216 | Key::Left => { 217 | self.prev(); 218 | } 219 | Key::Right => { 220 | self.next(); 221 | } 222 | Key::Backspace => { 223 | typed_hint.pop(); 224 | } 225 | Key::Char(ch) => { 226 | match ch { 227 | '\n' => match self.matches.iter().enumerate().find(|&h| h.0 == self.skip) { 228 | Some(hm) => { 229 | self.chosen.push((hm.1.text.to_string(), false)); 230 | 231 | if !self.multi { 232 | return CaptureEvent::Hint; 233 | } 234 | } 235 | _ => panic!("Match not found?"), 236 | }, 237 | ' ' => { 238 | if self.multi { 239 | // Finalize the multi selection 240 | return CaptureEvent::Hint; 241 | } else { 242 | // Enable the multi selection 243 | self.multi = true; 244 | } 245 | } 246 | key => { 247 | let key = key.to_string(); 248 | let lower_key = key.to_lowercase(); 249 | 250 | typed_hint.push_str(lower_key.as_str()); 251 | 252 | let selection = self.matches.iter().find(|mat| mat.hint == Some(typed_hint.clone())); 253 | 254 | match selection { 255 | Some(mat) => { 256 | self.chosen.push((mat.text.to_string(), key != lower_key)); 257 | 258 | if self.multi { 259 | typed_hint.clear(); 260 | } else { 261 | return CaptureEvent::Hint; 262 | } 263 | } 264 | None => { 265 | if !self.multi && typed_hint.len() >= longest_hint.len() { 266 | break; 267 | } 268 | } 269 | } 270 | } 271 | } 272 | } 273 | _ => { 274 | // Unknown key 275 | } 276 | } 277 | } 278 | Err(err) => panic!("{}", err), 279 | } 280 | 281 | stdin.keys().for_each(|_| { /* Skip the rest of stdin buffer */ }) 282 | } 283 | _ => { 284 | // Nothing in the buffer. Wait for a bit... 285 | std::thread::sleep(std::time::Duration::from_millis(50)); 286 | continue; // don't render again if nothing new to show 287 | } 288 | } 289 | 290 | self.render(stdout, &typed_hint); 291 | } 292 | 293 | CaptureEvent::Exit 294 | } 295 | 296 | pub fn present(&mut self) -> Vec<(String, bool)> { 297 | let mut stdin = async_stdin(); 298 | let mut stdout = AlternateScreen::from(stdout().into_raw_mode().unwrap()); 299 | 300 | let hints = match self.listen(&mut stdin, &mut stdout) { 301 | CaptureEvent::Exit => vec![], 302 | CaptureEvent::Hint => self.chosen.clone(), 303 | }; 304 | 305 | write!(stdout, "{}", cursor::Show).unwrap(); 306 | 307 | hints 308 | } 309 | } 310 | 311 | #[cfg(test)] 312 | mod tests { 313 | use super::*; 314 | 315 | fn split(output: &str) -> Vec<&str> { 316 | output.split("\n").collect::>() 317 | } 318 | 319 | #[test] 320 | fn hint_text() { 321 | let lines = split("lorem 127.0.0.1 lorem"); 322 | let custom = [].to_vec(); 323 | let mut state = state::State::new(&lines, "abcd", &custom); 324 | let mut view = View { 325 | state: &mut state, 326 | skip: 0, 327 | multi: false, 328 | contrast: false, 329 | position: &"", 330 | matches: vec![], 331 | select_foreground_color: colors::get_color("default"), 332 | select_background_color: colors::get_color("default"), 333 | multi_foreground_color: colors::get_color("default"), 334 | multi_background_color: colors::get_color("default"), 335 | foreground_color: colors::get_color("default"), 336 | background_color: colors::get_color("default"), 337 | hint_background_color: colors::get_color("default"), 338 | hint_foreground_color: colors::get_color("default"), 339 | chosen: vec![], 340 | }; 341 | 342 | let result = view.make_hint_text("a"); 343 | assert_eq!(result, "a".to_string()); 344 | 345 | view.contrast = true; 346 | let result = view.make_hint_text("a"); 347 | assert_eq!(result, "[a]".to_string()); 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /tmux-thumbs-install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -Eeu -o pipefail 3 | 4 | # Removing the binary to make this script idempotent 5 | rm -rf target/release/thumbs 6 | 7 | clear 8 | 9 | cat << EOF 10 | 11 | █████ █████ █████ █████ 12 | ░░███ ░░███ ░░███ ░░███ 13 | ███████ █████████████ █████ ████ █████ █████ ███████ ░███████ █████ ████ █████████████ ░███████ █████ 14 | ░░░███░ ░░███░░███░░███ ░░███ ░███ ░░███ ░░███ ██████████░░░███░ ░███░░███ ░░███ ░███ ░░███░░███░░███ ░███░░███ ███░░ 15 | ░███ ░███ ░███ ░███ ░███ ░███ ░░░█████░ ░░░░░░░░░░ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███░░█████ 16 | ░███ ███ ░███ ░███ ░███ ░███ ░███ ███░░░███ ░███ ███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░░░░███ 17 | ░░█████ █████░███ █████ ░░████████ █████ █████ ░░█████ ████ █████ ░░████████ █████░███ █████ ████████ ██████ 18 | ░░░░░ ░░░░░ ░░░ ░░░░░ ░░░░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░ ░░░░░ ░░░░░░░░ ░░░░░ ░░░ ░░░░░ ░░░░░░░░ ░░░░░░ 19 | 20 | EOF 21 | 22 | 23 | if [ "${1:-install}" == "update" ]; then 24 | 25 | cat << EOF 26 | ⚠️ UPDATE! ⚠️ 27 | 28 | It looks like you got a new version of tmux-thumbs repository but 29 | the binary version is not in sync. 30 | 31 | We are going to proceed with the new installation. 32 | 33 | Do you want to continue? 34 | 35 | Press any key to continue... 36 | EOF 37 | 38 | else 39 | 40 | cat << EOF 41 | It looks like this is the first time you are executing tmux-thumbs 42 | because the binary is not present. We are going to proceed with the 43 | installation. 44 | 45 | Do you want to continue? 46 | 47 | Press any key to continue... 48 | EOF 49 | 50 | fi 51 | 52 | read -rs -n 1 53 | 54 | cat << EOF 55 | 56 | Which format do you prefer for installation? 57 | 58 | 1) Compile: will use cargo to compile tmux-thumbs. It requires Rust. 59 | 2) Download: will download a precompiled binary for your system. 60 | 61 | EOF 62 | 63 | select opt in "Compile" "Download"; do 64 | case $opt in 65 | Compile|1) 66 | 67 | if ! [ -x "$(command -v cargo)" ]; then 68 | echo '❌ Rust is not installed!' 69 | exit 1 70 | fi 71 | 72 | echo ' Compiling tmux-thumbs, be patient:' 73 | cargo build --release --target-dir=target 74 | 75 | break;; 76 | Download|2) 77 | platform="$(uname -s)_$(uname -m)" 78 | 79 | echo " Downloading ${platform} binary..." 80 | 81 | sources=$(curl -s "https://api.github.com/repos/fcsonline/tmux-thumbs/releases/latest" | grep browser_download_url) 82 | 83 | case $platform in 84 | Darwin_x86_64) 85 | url=$(echo "${sources}" | grep -o 'https://.*darwin.zip' | uniq) 86 | curl -sL "${url}" | bsdtar -xf - thumbs tmux-thumbs 87 | 88 | ;; 89 | Linux_x86_64) 90 | url=$(echo "${sources}" | grep -o 'https://.*linux-musl.tar.gz' | uniq) 91 | curl -sL "${url}" | tar -zxf - thumbs tmux-thumbs 92 | 93 | ;; 94 | *) 95 | echo "❌ Unknown platform: ${platform}" 96 | read -rs -n 1 97 | echo " Press any key to close this pane..." 98 | exit 1 99 | ;; 100 | esac 101 | 102 | chmod +x thumbs tmux-thumbs 103 | mkdir -p target/release 104 | mv thumbs tmux-thumbs target/release 105 | 106 | break;; 107 | *) 108 | echo "❌ Ouh? Choose an available option." 109 | esac 110 | done 111 | 112 | cat << EOF 113 | Installation complete! 💯 114 | 115 | Press any key to close this pane... 116 | EOF 117 | 118 | read -rs -n 1 119 | -------------------------------------------------------------------------------- /tmux-thumbs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -Eeu -o pipefail 3 | 4 | # Setup env variables to be compatible with compiled and bundled installations 5 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 6 | RELEASE_DIR="${CURRENT_DIR}/target/release" 7 | 8 | THUMBS_BINARY="${RELEASE_DIR}/thumbs" 9 | TMUX_THUMBS_BINARY="${RELEASE_DIR}/tmux-thumbs" 10 | VERSION=$(grep 'version =' "${CURRENT_DIR}/Cargo.toml" | grep -o "\".*\"" | sed 's/"//g') 11 | 12 | if [ ! -f "$THUMBS_BINARY" ]; then 13 | tmux split-window "cd ${CURRENT_DIR} && bash ./tmux-thumbs-install.sh" 14 | exit 15 | elif [[ $(${THUMBS_BINARY} --version) != "thumbs ${VERSION}" ]]; then 16 | tmux split-window "cd ${CURRENT_DIR} && bash ./tmux-thumbs-install.sh update" 17 | exit 18 | fi 19 | 20 | function get-opt-value() { 21 | tmux show -vg "@thumbs-${1}" 2> /dev/null 22 | } 23 | 24 | function get-opt-arg() { 25 | local opt type value 26 | opt="${1}"; type="${2}" 27 | value="$(get-opt-value "${opt}")" || true 28 | 29 | if [ "${type}" = string ]; then 30 | [ -n "${value}" ] && echo "--${opt}=${value}" 31 | elif [ "${type}" = boolean ]; then 32 | [ "${value}" = 1 ] && echo "--${opt}" 33 | else 34 | return 1 35 | fi 36 | } 37 | 38 | PARAMS=(--dir "${CURRENT_DIR}") 39 | 40 | function add-param() { 41 | local type opt arg 42 | opt="${1}"; type="${2}" 43 | if arg="$(get-opt-arg "${opt}" "${type}")"; then 44 | PARAMS+=("${arg}") 45 | fi 46 | } 47 | 48 | add-param command string 49 | add-param upcase-command string 50 | add-param multi-command string 51 | add-param osc52 boolean 52 | 53 | "${TMUX_THUMBS_BINARY}" "${PARAMS[@]}" || true 54 | -------------------------------------------------------------------------------- /tmux-thumbs.tmux: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | DEFAULT_THUMBS_KEY=space 6 | 7 | THUMBS_KEY="$(tmux show-option -gqv @thumbs-key)" 8 | THUMBS_KEY=${THUMBS_KEY:-$DEFAULT_THUMBS_KEY} 9 | 10 | tmux set-option -ag command-alias "thumbs-pick=run-shell -b ${CURRENT_DIR}/tmux-thumbs.sh" 11 | tmux bind-key "${THUMBS_KEY}" thumbs-pick 12 | --------------------------------------------------------------------------------