mirror of
https://github.com/TECHNOFAB11/tmux-copyrat.git
synced 2025-12-10 23:20:06 +01:00
chore: a new start
This commit is contained in:
commit
34d0bb5a35
21 changed files with 2319 additions and 0 deletions
37
.github/workflows/rust.yml
vendored
Normal file
37
.github/workflows/rust.yml
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
name: Rust
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build-mac:
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release
|
||||
|
||||
- uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: thumbs-macos.zip
|
||||
path: ./target/release/thumbs
|
||||
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update -y
|
||||
sudo apt-get install -y curl gnupg ca-certificates git gcc-multilib g++-multilib cmake libssl-dev pkg-config python
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release
|
||||
|
||||
- uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: thumbs-linux.zip
|
||||
path: ./target/release/thumbs
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
**/*.rs.bk
|
||||
2
.rustfmt.toml
Normal file
2
.rustfmt.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
tab_spaces = 2
|
||||
max_width = 120
|
||||
21
.travis.yml
Normal file
21
.travis.yml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
language: rust
|
||||
cache: cargo
|
||||
dist: trusty
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
rust:
|
||||
- nightly
|
||||
- stable
|
||||
|
||||
before_script:
|
||||
- rustup component add rustfmt
|
||||
- cargo install --force cargo-audit
|
||||
- cargo generate-lockfile
|
||||
|
||||
script:
|
||||
- cargo fmt --all -- --check
|
||||
- cargo build
|
||||
- cargo test
|
||||
- cargo audit
|
||||
198
Cargo.lock
generated
Normal file
198
Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"hermit-abi 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "2.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "numtoa"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.1.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "redox_termios"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "termion"
|
||||
version = "1.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thumbs"
|
||||
version = "0.4.1"
|
||||
dependencies = [
|
||||
"clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"termion 1.5.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "vec_map"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[metadata]
|
||||
"checksum aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)" = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada"
|
||||
"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
|
||||
"checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||
"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
||||
"checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9"
|
||||
"checksum hermit-abi 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "61565ff7aaace3525556587bd2dc31d4a07071957be715e63ce7b1eccf51a8f4"
|
||||
"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
"checksum libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)" = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005"
|
||||
"checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
|
||||
"checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
|
||||
"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
|
||||
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
|
||||
"checksum regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692"
|
||||
"checksum regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)" = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae"
|
||||
"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||
"checksum termion 1.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "c22cec9d8978d906be5ac94bceb5a010d885c626c4c8855721a4dbd20e3ac905"
|
||||
"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
|
||||
"checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
|
||||
"checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"
|
||||
"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a"
|
||||
"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
|
||||
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
22
Cargo.toml
Normal file
22
Cargo.toml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "thumbs"
|
||||
version = "0.4.1"
|
||||
authors = ["Ferran Basora <fcsonline@gmail.com>"]
|
||||
edition = "2018"
|
||||
description = "A lightning fast version copy/pasting like vimium/vimperator"
|
||||
repository = "https://github.com/fcsonline/tmux-thumbs"
|
||||
keywords = ["rust", "tmux", "tmux-plugin", "vimium", "vimperator"]
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
termion = "1.5"
|
||||
regex = "1.3.1"
|
||||
clap = "2.33.0"
|
||||
|
||||
[[bin]]
|
||||
name = "thumbs"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "tmux-thumbs"
|
||||
path = "src/swapper.rs"
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019 Ferran Basora
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
427
README.md
Normal file
427
README.md
Normal file
|
|
@ -0,0 +1,427 @@
|
|||
# tmux-thumbs
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
## Usage
|
||||
|
||||
Press ( <kbd>prefix</kbd> + <kbd>Space</kbd> ) to highlist in you current tmux
|
||||
visible pane all text that match specific pattern. Then press the highlighted
|
||||
letter hint to yank the text in your tmux buffer.
|
||||
|
||||
### Matched patterns
|
||||
|
||||
- File paths
|
||||
- File in diff
|
||||
- Git SHAs
|
||||
- IPFS CID's
|
||||
- Colors in hex
|
||||
- Numbers ( 4+ digits )
|
||||
- Hex numbers
|
||||
- Markdown urls
|
||||
- IP4 addresses
|
||||
- Docker images
|
||||
- kubernetes resources
|
||||
- UUIDs
|
||||
|
||||
These are the list of matched patterns that will be highlighted by default. If
|
||||
you want to highlight a pattern that is not in this list you can add one or
|
||||
more with `--regexp` parameter.
|
||||
|
||||
## Demo
|
||||
|
||||
[](https://asciinema.org/a/232775?autoplay=1)
|
||||
|
||||
## Tmux integration
|
||||
|
||||
Clone the repo:
|
||||
|
||||
```
|
||||
git clone https://github.com/fcsonline/tmux-thumbs ~/.tmux/plugins/tmux-thumbs
|
||||
```
|
||||
|
||||
Compile it with [cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html):
|
||||
|
||||
```
|
||||
cd ~/.tmux/plugins/tmux-thumbs
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
Source it in your `.tmux.conf`:
|
||||
|
||||
```
|
||||
run-shell ~/.tmux/plugins/tmux-thumbs/tmux-thumbs.tmux
|
||||
```
|
||||
|
||||
Reload TMUX conf by running:
|
||||
|
||||
```
|
||||
tmux source-file ~/.tmux.conf
|
||||
```
|
||||
|
||||
## Using Tmux Plugin Manager
|
||||
|
||||
You can add this line to your list of [TPM](https://github.com/tmux-plugins/tpm) plugins in `.tmux.conf`:
|
||||
|
||||
```
|
||||
set -g @plugin 'fcsonline/tmux-thumbs'
|
||||
```
|
||||
|
||||
To be able to install the plugin just hit <kbd>prefix</kbd> + <kbd>I</kbd>. You should now be able to use
|
||||
the plugin!
|
||||
|
||||
## Configuration
|
||||
|
||||
If you want to customize how is shown your tmux-thumbs hints those all available
|
||||
parameters to set your perfect profile.
|
||||
|
||||
NOTE: for changes to take effect, you'll need to source again your `.tmux.conf` file.
|
||||
|
||||
* [@thumbs-key](#thumbs-key)
|
||||
* [@thumbs-alphabet](#thumbs-alphabet)
|
||||
* [@thumbs-reverse](#thumbs-reverse)
|
||||
* [@thumbs-unique](#thumbs-unique)
|
||||
* [@thumbs-position](#thumbs-position)
|
||||
* [@thumbs-regexp-N](#thumbs-regexp-N)
|
||||
* [@thumbs-command](#thumbs-command)
|
||||
* [@thumbs-upcase-command](#thumbs-upcase-command)
|
||||
* [@thumbs-bg-color](#thumbs-bg-color)
|
||||
* [@thumbs-fg-color](#thumbs-fg-color)
|
||||
* [@thumbs-hint-bg-color](#thumbs-hint-bg-color)
|
||||
* [@thumbs-hint-fg-color](#thumbs-hint-fg-color)
|
||||
* [@thumbs-select-fg-color](#thumbs-select-fg-color)
|
||||
* [@thumbs-select-bg-color](#thumbs-select-bg-color)
|
||||
* [@thumbs-contrast](#thumbs-contrast)
|
||||
|
||||
### @thumbs-key
|
||||
|
||||
`default: space`
|
||||
|
||||
Choose which key is used to enter in thumbs mode.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
set -g @thumbs-key F
|
||||
```
|
||||
|
||||
### @thumbs-alphabet
|
||||
|
||||
`default: qwerty`
|
||||
|
||||
Choose which set of characters is used to build hints. Review all [available alphabets](#Alphabets)
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
set -g @thumbs-alphabet dvorak-homerow
|
||||
```
|
||||
|
||||
### @thumbs-reverse
|
||||
|
||||
`default: disabled`
|
||||
|
||||
Choose in which direction you want to assign hints. Useful to get shorter hints closer to the cursor.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
set -g @thumbs-reverse
|
||||
```
|
||||
|
||||
### @thumbs-unique
|
||||
|
||||
`default: disabled`
|
||||
|
||||
Choose if you want to assign the same hint for the same matched strings.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
set -g @thumbs-unique
|
||||
```
|
||||
|
||||
### @thumbs-position
|
||||
|
||||
`default: left`
|
||||
|
||||
Choose where do you want to show the hint in the matched string. Options (left, right).
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
set -g @thumbs-position right
|
||||
```
|
||||
|
||||
### @thumbs-regexp-N
|
||||
|
||||
Add extra patterns to match. This parameter can have multiple instances.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
set -g @thumbs-regexp-1 '[a-z]+@[a-z]+.com' # Match emails
|
||||
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
|
||||
```
|
||||
|
||||
### @thumbs-command
|
||||
|
||||
`default: 'tmux set-buffer {}'`
|
||||
|
||||
Choose which command execute when you press a hint. `tmux-thumbs` will replace `{}` with the picked hint.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
set -g @thumbs-command 'echo -n {} | pbcopy'
|
||||
```
|
||||
|
||||
### @thumbs-upcase-command
|
||||
|
||||
`default: 'tmux set-buffer {} && tmux paste-buffer'`
|
||||
|
||||
Choose which command execute when you press a upcase hint. `tmux-thumbs` will replace `{}` with the picked hint.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
set -g @thumbs-upcase-command 'echo -n {} | pbcopy'
|
||||
```
|
||||
|
||||
### @thumbs-bg-color
|
||||
|
||||
`default: black`
|
||||
|
||||
Sets the background color for matches
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
set -g @thumbs-bg-color blue
|
||||
```
|
||||
|
||||
### @thumbs-fg-color
|
||||
|
||||
`default: green`
|
||||
|
||||
Sets the foreground color for matches
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
set -g @thumbs-fg-color green
|
||||
```
|
||||
|
||||
### @thumbs-hint-bg-color
|
||||
|
||||
`default: black`
|
||||
|
||||
Sets the background color for hints
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
set -g @thumbs-hint-bg-color blue
|
||||
```
|
||||
|
||||
### @thumbs-hint-fg-color
|
||||
|
||||
`default: yellow`
|
||||
|
||||
Sets the foreground color for hints
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
set -g @thumbs-hint-fg-color green
|
||||
```
|
||||
|
||||
### @thumbs-select-fg-color
|
||||
|
||||
`default: blue`
|
||||
|
||||
Sets the foreground color for selection
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
set -g @thumbs-select-fg-color red
|
||||
```
|
||||
|
||||
### @thumbs-select-bg-color
|
||||
|
||||
`default: black`
|
||||
|
||||
Sets the background color for selection
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
set -g @thumbs-select-bg-color red
|
||||
```
|
||||
|
||||
### @thumbs-contrast
|
||||
|
||||
`default: 0`
|
||||
|
||||
Displays hint character in square brackets for extra visibility.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
set -g @thumbs-contrast 1
|
||||
```
|
||||
|
||||
#### Colors
|
||||
|
||||
This is the list of available colors:
|
||||
|
||||
- black
|
||||
- red
|
||||
- green
|
||||
- yellow
|
||||
- blue
|
||||
- magenta
|
||||
- cyan
|
||||
- white
|
||||
- default
|
||||
|
||||
#### Alphabets
|
||||
|
||||
This is the list of available alphabets:
|
||||
|
||||
- `numeric`: 1234567890
|
||||
- `abcd`: abcd
|
||||
- `qwerty`: asdfqwerzxcvjklmiuopghtybn
|
||||
- `qwerty-homerow`: asdfjklgh
|
||||
- `qwerty-left-hand`: asdfqwerzcxv
|
||||
- `qwerty-right-hand`: jkluiopmyhn
|
||||
- `azerty`: qsdfazerwxcvjklmuiopghtybn
|
||||
- `azerty-homerow`: qsdfjkmgh
|
||||
- `azerty-left-hand`: qsdfazerwxcv
|
||||
- `azerty-right-hand`: jklmuiophyn
|
||||
- `qwertz`: asdfqweryxcvjkluiopmghtzbn
|
||||
- `qwertz-homerow`: asdfghjkl
|
||||
- `qwertz-left-hand`: asdfqweryxcv
|
||||
- `qwertz-right-hand`: jkluiopmhzn
|
||||
- `dvorak`: aoeuqjkxpyhtnsgcrlmwvzfidb
|
||||
- `dvorak-homerow`: aoeuhtnsid
|
||||
- `dvorak-left-hand`: aoeupqjkyix
|
||||
- `dvorak-right-hand`: htnsgcrlmwvz
|
||||
- `colemak`: arstqwfpzxcvneioluymdhgjbk
|
||||
- `colemak-homerow`: arstneiodh
|
||||
- `colemak-left-hand`: arstqwfpzxcv
|
||||
- `colemak-right-hand`: neioluymjhk
|
||||
|
||||
## Extra features
|
||||
|
||||
- **Arrow navigation:** You can use the arrows to move around between all matched items.
|
||||
- **Auto paste:** If your last typed hint character is uppercase, you are going to pick and paste the desired hint.
|
||||
- **Multi selection:** If you run thumb with multi selection mode you will be able to choose multiple hints pressing the desired letter and `Space` to finalize the selection.
|
||||
|
||||
## Tmux compatibility
|
||||
|
||||
This is the known list of versions of `tmux` compatible with `tmux-thumbs`:
|
||||
|
||||
| Version | Compatible |
|
||||
|:-------:|:----------:|
|
||||
| 3.0a | ✅ |
|
||||
| 2.9a | ✅ |
|
||||
| 2.8 | ❓ |
|
||||
| 2.7 | ❓ |
|
||||
| 2.6 | ✅ |
|
||||
| 2.5 | ❓ |
|
||||
| 2.4 | ❓ |
|
||||
| 2.3 | ❓ |
|
||||
| 1.8 | ❓ |
|
||||
| 1.7 | ❓ |
|
||||
|
||||
If you can check hat `tmux-thumbs` is or is not compatible with some specific version of `tmux`, let me know.
|
||||
|
||||
## Standalone `thumbs`
|
||||
|
||||
This project started as a `tmux` plugin but after reviewing it with some
|
||||
friends we decided to explore all the possibilities of decoupling thumbs from
|
||||
`tmux`. You can install it with a simple command:
|
||||
|
||||
```
|
||||
cargo install thumbs
|
||||
```
|
||||
|
||||
And those are all available options:
|
||||
|
||||
```
|
||||
thumbs 0.4.1
|
||||
A lightning fast version copy/pasting like vimium/vimperator
|
||||
|
||||
USAGE:
|
||||
thumbs [FLAGS] [OPTIONS]
|
||||
|
||||
FLAGS:
|
||||
-c, --contrast Put square brackets around hint for visibility
|
||||
-h, --help Prints help information
|
||||
-m, --multi Enable multi-selection
|
||||
-r, --reverse Reverse the order for assigned hints
|
||||
-u, --unique Don't show duplicated hints for the same match
|
||||
-V, --version Prints version information
|
||||
|
||||
OPTIONS:
|
||||
-a, --alphabet <alphabet> Sets the alphabet [default: qwerty]
|
||||
--bg-color <background_color> Sets the background color for matches [default: black]
|
||||
--fg-color <foreground_color> Sets the foregroud color for matches [default: green]
|
||||
-f, --format <format>
|
||||
Specifies the out format for the picked hint. (%U: Upcase, %H: Hint) [default: %H]
|
||||
|
||||
--hint-bg-color <hint_background_color> Sets the background color for hints [default: black]
|
||||
--hint-fg-color <hint_foreground_color> Sets the foregroud color for hints [default: yellow]
|
||||
-p, --position <position> Hint position [default: left]
|
||||
-x, --regexp <regexp>... Use this regexp as extra pattern to match
|
||||
--select-bg-color <select_background_color> Sets the background color for selection [default: black]
|
||||
--select-fg-color <select_foreground_color> Sets the foreground color for selection [default: blue]
|
||||
-t, --target <target> Stores the hint in the specified path
|
||||
```
|
||||
|
||||
|
||||
If you want to enjoy terminal hints, you can do things like this without `tmux`:
|
||||
|
||||
```
|
||||
> alias pick='thumbs -u -r | xsel --clipboard -i'
|
||||
> git log | pick
|
||||
```
|
||||
|
||||
Or multi selection:
|
||||
|
||||
```
|
||||
> git log | thumbs -m
|
||||
1df9fa69c8831ac042c6466af81e65402ee2a007
|
||||
4897dc4ecbd2ac90b17de95e00e9e75bb540e37f
|
||||
```
|
||||
|
||||
Standalone `thumbs` has some similarities to [FZF](https://github.com/junegunn/fzf).
|
||||
|
||||
## Background
|
||||
|
||||
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).
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [X] Support multi selection
|
||||
- [X] Decouple `tmux-thumbs` from `tmux`
|
||||
- [ ] Code [Kitty](https://github.com/kovidgoyal/kitty) plugin, now that `thumbs` can run standalone
|
||||
|
||||
## Contribute
|
||||
|
||||
This project started as a side project to learn Rust, so I'm sure that is full
|
||||
of mistakes and areas to be improve. If you think you can tweak the code to
|
||||
make it better, I'll really appreaciate a pull request. ;)
|
||||
|
||||
# License
|
||||
|
||||
[MIT](https://github.com/fcsonline/tmux-thumbs/blob/master/LICENSE)
|
||||
43
samples/test1
Normal file
43
samples/test1
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
colors: [31ma[m[32mb[m[33mc[m[34md[m[35me[m 30.6.23.42 lorem 135.2.4.4 lorem 235.23.33.34
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam et aliquet
|
||||
mauris, et ullamcorper enim. Nullam vitae orci eget tellus porta ullamcorper
|
||||
ut et lacus. 127.0.0.1 Proin sagittis, tortor nec egestas volutpat,
|
||||
255.255.255.255 sapien velit accumsan ligula, at consectetur risus tortor
|
||||
vitae massa. In commodo vitae sem eu semper. Suspendisse orci tellus, aliquet
|
||||
sit amet elit sit amet, https://en.wikipedia.org/wiki/ vehicula porttitor
|
||||
dolor 😋. In 😏, rhoncus some UTF(😁) dignissim iaculis ac. Ip 10.0.3.4
|
||||
Morbi eu elit sed eros f79010f2 ultricies 67ef359ea8 consequat. Morbi mattis
|
||||
dolor mi, ac faucibus justo maximus eu. Donec vestibulum lorem semper,
|
||||
ultricies libero 5734d923afb8 efc9061663 ac, #ff00FF ex. Morbi
|
||||
123e4567-e89b-12d3-a456-426655440000 tempor mollis condimentum.
|
||||
|
||||
lorem /var/log/123e4567/999.log lorem /etc/system.conf lorem
|
||||
|
||||
cat ▶ cat ▶ cat ▶ cat ▶ cat 30.6.23.42 lorem 135.2.4.4 lorem 235.23.33.34
|
||||
|
||||
ipsum [fcsonline](https://github.com/fcsonline) lorem
|
||||
|
||||
path: /var/log/nginx.log[m
|
||||
|
||||
path: ./folder/.neomake@04fd.log
|
||||
|
||||
home: ~/.gnu/.config.txt
|
||||
|
||||
10.3.23.42 lorem 123.2.3.4 lorem 230.23.33.34
|
||||
10.3.23.42 lorem 123.2.3.4 lorem 230.23.33.34
|
||||
10.3.23.42 lorem 123.2.3.4 lorem 230.23.33.34
|
||||
10.3.23.42 lorem 123.2.3.4 lorem 230.23.33.34
|
||||
10.3.23.42 lorem 123.2.3.4 lorem 230.23.33.34
|
||||
10.3.23.42 lorem 123.2.3.4 lorem 230.23.33.34
|
||||
10.3.23.42 lorem 123.2.3.4 lorem 230.23.33.34
|
||||
10.3.23.42 lorem 123.2.3.4 lorem 230.23.33.34
|
||||
10.3.23.42 lorem 123.2.3.4 lorem 230.23.33.34
|
||||
10.3.23.42 lorem 123.2.3.4 lorem 230.23.33.34
|
||||
10.3.23.42 lorem 123.2.3.4 lorem 230.23.33.34
|
||||
|
||||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||
<none> <none> sha256:77af4d6b9913e693e8d0b4b294fa62ade6054e6b2f1ffb617ac955dd63fb0182 19 hours ago 1.089 GB
|
||||
committest latest sha256:b6fa739cedf5ea12a620a439402b6004d057da800f91c7524b5086a5e4749c9f 19 hours ago 1.089 GB
|
||||
<none> <none> sha256:78a85c484f71509adeaace20e72e941f6bdd2b25b4c75da8693efd9f61a37921 19 hours ago 1.089 GB
|
||||
docker latest sha256:30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4 20 hours ago 1.089 GB
|
||||
41
samples/test2
Normal file
41
samples/test2
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
127.10.0.1 192.168.10.1 127.0.0.1 192.168.0.1
|
||||
127.10.0.2 192.168.10.2 127.0.0.2 192.168.0.2
|
||||
127.10.0.3 192.168.10.3 127.0.0.3 192.168.0.3
|
||||
127.10.0.4 192.168.10.4 127.0.0.4 192.168.0.4
|
||||
127.10.0.5 192.168.10.5 127.0.0.5 192.168.0.5
|
||||
127.10.0.6 192.168.10.6 127.0.0.6 192.168.0.6
|
||||
127.10.0.7 192.168.10.7 127.0.0.7 192.168.0.7
|
||||
127.10.0.8 192.168.10.8 127.0.0.8 192.168.0.8
|
||||
127.10.0.9 192.168.10.9 127.0.0.9 192.168.0.9
|
||||
127.10.0.10 192.168.10.10 127.0.0.10 192.168.0.10
|
||||
127.10.0.11 192.168.10.11 127.0.0.11 192.168.0.11
|
||||
127.10.0.12 192.168.10.12 127.0.0.12 192.168.0.12
|
||||
127.10.0.13 192.168.10.13 127.0.0.13 192.168.0.13
|
||||
127.10.0.14 192.168.10.14 127.0.0.14 192.168.0.14
|
||||
127.10.0.15 192.168.10.15 127.0.0.15 192.168.0.15
|
||||
127.10.0.16 192.168.10.16 127.0.0.16 192.168.0.16
|
||||
127.10.0.17 192.168.10.17 127.0.0.17 192.168.0.17
|
||||
127.10.0.18 192.168.10.18 127.0.0.18 192.168.0.18
|
||||
127.10.0.19 192.168.10.19 127.0.0.19 192.168.0.19
|
||||
127.10.0.20 192.168.10.20 127.0.0.20 192.168.0.20
|
||||
|
||||
128.10.0.1 192.169.10.1 127.0.11.1 193.168.0.1
|
||||
128.10.0.2 192.169.10.2 127.0.11.2 193.168.0.2
|
||||
128.10.0.3 192.169.10.3 127.0.11.3 193.168.0.3
|
||||
128.10.0.4 192.169.10.4 127.0.11.4 193.168.0.4
|
||||
128.10.0.5 192.169.10.5 127.0.11.5 193.168.0.5
|
||||
128.10.0.6 192.169.10.6 127.0.11.6 193.168.0.6
|
||||
128.10.0.7 192.169.10.7 127.0.11.7 193.168.0.7
|
||||
128.10.0.8 192.169.10.8 127.0.11.8 193.168.0.8
|
||||
128.10.0.9 192.169.10.9 127.0.11.9 193.168.0.9
|
||||
128.10.0.10 192.169.10.10 127.0.11.10 193.168.0.10
|
||||
128.10.0.11 192.169.10.11 127.0.11.11 193.168.0.11
|
||||
128.10.0.12 192.169.10.12 127.0.11.12 193.168.0.12
|
||||
128.10.0.13 192.169.10.13 127.0.11.13 193.168.0.13
|
||||
128.10.0.14 192.169.10.14 127.0.11.14 193.168.0.14
|
||||
128.10.0.15 192.169.10.15 127.0.11.15 193.168.0.15
|
||||
128.10.0.16 192.169.10.16 127.0.11.16 193.168.0.16
|
||||
128.10.0.17 192.169.10.17 127.0.11.17 193.168.0.17
|
||||
128.10.0.18 192.169.10.18 127.0.11.18 193.168.0.18
|
||||
128.10.0.19 192.169.10.19 127.0.11.19 193.168.0.19
|
||||
128.10.0.20 192.169.10.20 127.0.11.20 193.168.0.20
|
||||
6
samples/test3
Normal file
6
samples/test3
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[01;33mcommit[m: 92329857fabcf913e61351b4e311c53d8c18c1af other: [33m12329857fabcf913e61351b4e311c53d8c18c1ae[m
|
||||
commit: [33md89189228a4b34ef5478bb24dc04ea3443dd73e0[m
|
||||
path: [32m/var/log/nginx.log[m
|
||||
path: [32mtest/log/nginx.log[m
|
||||
|
||||
lorem /var/log/nginx.log lorem
|
||||
2
samples/test4
Normal file
2
samples/test4
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[1m[32mfoo@bar[0m[39m[49m [1m[34m~/dev/tmux-thumbs[33m master[31m ▶[0m[39m[49m tmux capture-pane -e -J
|
||||
[1m[32mfoo@bar[0m[39m[49m [1m[34m~/dev/tmux-thumbs[33m master[31m ▶[0m[39m[49m tmux capture-pane -e -J -p >> samples/test4
|
||||
1
samples/test5
Normal file
1
samples/test5
Normal file
|
|
@ -0,0 +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 [01;33mcommit[m: 92329857fabcf913e61351b4e311c53d8c18c1af other: [33m12329857fabcf913e61351b4e311c53d8c18c1ae[m commit: [33md89189228a4b34ef5478bb24dc04ea3443dd73e0[m path: [32m/var/log/nginx.log[m path: [32mtest/log/nginx.log[m lorem /var/log/nginx.log lorem
|
||||
108
src/alphabets.rs
Normal file
108
src/alphabets.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
const ALPHABETS: [(&'static str, &'static str); 22] = [
|
||||
("numeric", "1234567890"),
|
||||
("abcd", "abcd"),
|
||||
("qwerty", "asdfqwerzxcvjklmiuopghtybn"),
|
||||
("qwerty-homerow", "asdfjklgh"),
|
||||
("qwerty-left-hand", "asdfqwerzcxv"),
|
||||
("qwerty-right-hand", "jkluiopmyhn"),
|
||||
("azerty", "qsdfazerwxcvjklmuiopghtybn"),
|
||||
("azerty-homerow", "qsdfjkmgh"),
|
||||
("azerty-left-hand", "qsdfazerwxcv"),
|
||||
("azerty-right-hand", "jklmuiophyn"),
|
||||
("qwertz", "asdfqweryxcvjkluiopmghtzbn"),
|
||||
("qwertz-homerow", "asdfghjkl"),
|
||||
("qwertz-left-hand", "asdfqweryxcv"),
|
||||
("qwertz-right-hand", "jkluiopmhzn"),
|
||||
("dvorak", "aoeuqjkxpyhtnsgcrlmwvzfidb"),
|
||||
("dvorak-homerow", "aoeuhtnsid"),
|
||||
("dvorak-left-hand", "aoeupqjkyix"),
|
||||
("dvorak-right-hand", "htnsgcrlmwvz"),
|
||||
("colemak", "arstqwfpzxcvneioluymdhgjbk"),
|
||||
("colemak-homerow", "arstneiodh"),
|
||||
("colemak-left-hand", "arstqwfpzxcv"),
|
||||
("colemak-right-hand", "neioluymjhk"),
|
||||
];
|
||||
|
||||
pub struct Alphabet<'a> {
|
||||
letters: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Alphabet<'a> {
|
||||
fn new(letters: &'a str) -> Alphabet {
|
||||
Alphabet { letters }
|
||||
}
|
||||
|
||||
pub fn hints(&self, matches: usize) -> Vec<String> {
|
||||
let letters: Vec<String> = self.letters.chars().map(|s| s.to_string()).collect();
|
||||
|
||||
let mut expansion = letters.clone();
|
||||
let mut expanded: Vec<String> = Vec::new();
|
||||
|
||||
loop {
|
||||
if expansion.len() + expanded.len() >= matches {
|
||||
break;
|
||||
}
|
||||
if expansion.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let prefix = expansion.pop().expect("Ouch!");
|
||||
let sub_expansion: Vec<String> = letters
|
||||
.iter()
|
||||
.take(matches - expansion.len() - expanded.len())
|
||||
.map(|s| prefix.clone() + s)
|
||||
.collect();
|
||||
|
||||
expanded.splice(0..0, sub_expansion);
|
||||
}
|
||||
|
||||
expansion = expansion.iter().take(matches - expanded.len()).cloned().collect();
|
||||
expansion.append(&mut expanded);
|
||||
expansion
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_alphabet(alphabet_name: &str) -> Alphabet {
|
||||
let alphabets: HashMap<&str, &str> = ALPHABETS.iter().cloned().collect();
|
||||
|
||||
alphabets
|
||||
.get(alphabet_name)
|
||||
.expect(format!("Unknown alphabet: {}", alphabet_name).as_str()); // FIXME
|
||||
|
||||
Alphabet::new(alphabets[alphabet_name])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn simple_matches() {
|
||||
let alphabet = Alphabet::new("abcd");
|
||||
let hints = alphabet.hints(3);
|
||||
assert_eq!(hints, ["a", "b", "c"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composed_matches() {
|
||||
let alphabet = Alphabet::new("abcd");
|
||||
let hints = alphabet.hints(6);
|
||||
assert_eq!(hints, ["a", "b", "c", "da", "db", "dc"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composed_matches_multiple() {
|
||||
let alphabet = Alphabet::new("abcd");
|
||||
let hints = alphabet.hints(8);
|
||||
assert_eq!(hints, ["a", "b", "ca", "cb", "da", "db", "dc", "dd"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composed_matches_max() {
|
||||
let alphabet = Alphabet::new("ab");
|
||||
let hints = alphabet.hints(8);
|
||||
assert_eq!(hints, ["aa", "ab", "ba", "bb"]);
|
||||
}
|
||||
}
|
||||
35
src/colors.rs
Normal file
35
src/colors.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
use termion::color;
|
||||
|
||||
pub fn get_color(color_name: &str) -> Box<&dyn color::Color> {
|
||||
match color_name {
|
||||
"black" => Box::new(&color::Black),
|
||||
"red" => Box::new(&color::Red),
|
||||
"green" => Box::new(&color::Green),
|
||||
"yellow" => Box::new(&color::Yellow),
|
||||
"blue" => Box::new(&color::Blue),
|
||||
"magenta" => Box::new(&color::Magenta),
|
||||
"cyan" => Box::new(&color::Cyan),
|
||||
"white" => Box::new(&color::White),
|
||||
"default" => Box::new(&color::Reset),
|
||||
_ => panic!("Unknown color: {}", color_name),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn match_color() {
|
||||
let text1 = println!("{}{}", color::Fg(*get_color("green")), "foo");
|
||||
let text2 = println!("{}{}", color::Fg(color::Green), "foo");
|
||||
|
||||
assert_eq!(text1, text2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn no_match_color() {
|
||||
println!("{}{}", color::Fg(*get_color("wat")), "foo");
|
||||
}
|
||||
}
|
||||
200
src/main.rs
Normal file
200
src/main.rs
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
extern crate clap;
|
||||
extern crate termion;
|
||||
|
||||
mod alphabets;
|
||||
mod colors;
|
||||
mod state;
|
||||
mod view;
|
||||
|
||||
use self::clap::{App, Arg};
|
||||
use clap::crate_version;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::prelude::*;
|
||||
use std::io::{self, Read};
|
||||
|
||||
fn app_args<'a>() -> clap::ArgMatches<'a> {
|
||||
App::new("thumbs")
|
||||
.version(crate_version!())
|
||||
.about("A lightning fast version copy/pasting like vimium/vimperator")
|
||||
.arg(
|
||||
Arg::with_name("alphabet")
|
||||
.help("Sets the alphabet")
|
||||
.long("alphabet")
|
||||
.short("a")
|
||||
.default_value("qwerty"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("format")
|
||||
.help("Specifies the out format for the picked hint. (%U: Upcase, %H: Hint)")
|
||||
.long("format")
|
||||
.short("f")
|
||||
.default_value("%H"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("foreground_color")
|
||||
.help("Sets the foregroud color for matches")
|
||||
.long("fg-color")
|
||||
.default_value("green"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("background_color")
|
||||
.help("Sets the background color for matches")
|
||||
.long("bg-color")
|
||||
.default_value("black"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("hint_foreground_color")
|
||||
.help("Sets the foregroud color for hints")
|
||||
.long("hint-fg-color")
|
||||
.default_value("yellow"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("hint_background_color")
|
||||
.help("Sets the background color for hints")
|
||||
.long("hint-bg-color")
|
||||
.default_value("black"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("select_foreground_color")
|
||||
.help("Sets the foreground color for selection")
|
||||
.long("select-fg-color")
|
||||
.default_value("blue"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("select_background_color")
|
||||
.help("Sets the background color for selection")
|
||||
.long("select-bg-color")
|
||||
.default_value("black"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("multi")
|
||||
.help("Enable multi-selection")
|
||||
.long("multi")
|
||||
.short("m"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("reverse")
|
||||
.help("Reverse the order for assigned hints")
|
||||
.long("reverse")
|
||||
.short("r"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("unique")
|
||||
.help("Don't show duplicated hints for the same match")
|
||||
.long("unique")
|
||||
.short("u"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("position")
|
||||
.help("Hint position")
|
||||
.long("position")
|
||||
.default_value("left")
|
||||
.short("p"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("regexp")
|
||||
.help("Use this regexp as extra pattern to match")
|
||||
.long("regexp")
|
||||
.short("x")
|
||||
.takes_value(true)
|
||||
.multiple(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("contrast")
|
||||
.help("Put square brackets around hint for visibility")
|
||||
.long("contrast")
|
||||
.short("c"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("target")
|
||||
.help("Stores the hint in the specified path")
|
||||
.long("target")
|
||||
.short("t")
|
||||
.takes_value(true),
|
||||
)
|
||||
.get_matches()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = app_args();
|
||||
let format = args.value_of("format").unwrap();
|
||||
let alphabet = args.value_of("alphabet").unwrap();
|
||||
let position = args.value_of("position").unwrap();
|
||||
let target = args.value_of("target");
|
||||
let multi = args.is_present("multi");
|
||||
let reverse = args.is_present("reverse");
|
||||
let unique = args.is_present("unique");
|
||||
let contrast = args.is_present("contrast");
|
||||
let regexp = if let Some(items) = args.values_of("regexp") {
|
||||
items.collect::<Vec<_>>()
|
||||
} else {
|
||||
[].to_vec()
|
||||
};
|
||||
|
||||
let foreground_color = colors::get_color(args.value_of("foreground_color").unwrap());
|
||||
let background_color = colors::get_color(args.value_of("background_color").unwrap());
|
||||
let hint_foreground_color = colors::get_color(args.value_of("hint_foreground_color").unwrap());
|
||||
let hint_background_color = colors::get_color(args.value_of("hint_background_color").unwrap());
|
||||
let select_foreground_color = colors::get_color(args.value_of("select_foreground_color").unwrap());
|
||||
let select_background_color = colors::get_color(args.value_of("select_background_color").unwrap());
|
||||
|
||||
let stdin = io::stdin();
|
||||
let mut handle = stdin.lock();
|
||||
let mut output = String::new();
|
||||
|
||||
handle.read_to_string(&mut output).unwrap();
|
||||
|
||||
let lines = output.split('\n').collect::<Vec<&str>>();
|
||||
|
||||
let mut state = state::State::new(&lines, alphabet, ®exp);
|
||||
|
||||
let selected = {
|
||||
let mut viewbox = view::View::new(
|
||||
&mut state,
|
||||
multi,
|
||||
reverse,
|
||||
unique,
|
||||
contrast,
|
||||
position,
|
||||
select_foreground_color,
|
||||
select_background_color,
|
||||
foreground_color,
|
||||
background_color,
|
||||
hint_foreground_color,
|
||||
hint_background_color,
|
||||
);
|
||||
|
||||
viewbox.present()
|
||||
};
|
||||
|
||||
if !selected.is_empty() {
|
||||
let output = selected
|
||||
.iter()
|
||||
.map(|(text, upcase)| {
|
||||
let upcase_value = if *upcase { "true" } else { "false" };
|
||||
|
||||
let mut output = format.to_string();
|
||||
|
||||
output = str::replace(&output, "%U", upcase_value);
|
||||
output = str::replace(&output, "%H", text.as_str());
|
||||
output
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
if let Some(target) = target {
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.open(target)
|
||||
.expect("Unable to open the target file");
|
||||
|
||||
file.write(output.as_bytes()).unwrap();
|
||||
} else {
|
||||
print!("{}", output);
|
||||
}
|
||||
} else {
|
||||
::std::process::exit(1);
|
||||
}
|
||||
}
|
||||
422
src/state.rs
Normal file
422
src/state.rs
Normal file
|
|
@ -0,0 +1,422 @@
|
|||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
|
||||
const EXCLUDE_PATTERNS: [(&'static str, &'static str); 1] = [("bash", r"[[:cntrl:]]\[([0-9]{1,2};)?([0-9]{1,2})?m")];
|
||||
|
||||
const PATTERNS: [(&'static str, &'static str); 14] = [
|
||||
("markdown_url", r"\[[^]]*\]\(([^)]+)\)"),
|
||||
("url", r"((https?://|git@|git://|ssh://|ftp://|file:///)[^ ]+)"),
|
||||
("diff_a", r"--- a/([^ ]+)"),
|
||||
("diff_b", r"\+\+\+ b/([^ ]+)"),
|
||||
("docker", r"sha256:([0-9a-f]{64})"),
|
||||
("path", r"(([.\w\-@~]+)?(/[.\w\-@]+)+)"),
|
||||
("color", r"#[0-9a-fA-F]{6}"),
|
||||
("uid", r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"),
|
||||
("ipfs", r"Qm[0-9a-zA-Z]{44}"),
|
||||
("sha", r"[0-9a-f]{7,40}"),
|
||||
("ip", r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"),
|
||||
("ipv6", r"[A-f0-9:]+:+[A-f0-9:]+[%\w\d]+"),
|
||||
("address", r"0x[0-9a-fA-F]+"),
|
||||
("number", r"[0-9]{4,}"),
|
||||
];
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Match<'a> {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub pattern: &'a str,
|
||||
pub text: &'a str,
|
||||
pub hint: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> fmt::Debug for Match<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Match {{ x: {}, y: {}, pattern: {}, text: {}, hint: <{}> }}",
|
||||
self.x,
|
||||
self.y,
|
||||
self.pattern,
|
||||
self.text,
|
||||
self.hint.clone().unwrap_or("<undefined>".to_string())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialEq for Match<'a> {
|
||||
fn eq(&self, other: &Match) -> bool {
|
||||
self.x == other.x && self.y == other.y
|
||||
}
|
||||
}
|
||||
|
||||
pub struct State<'a> {
|
||||
pub lines: &'a Vec<&'a str>,
|
||||
alphabet: &'a str,
|
||||
regexp: &'a Vec<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> State<'a> {
|
||||
pub fn new(lines: &'a Vec<&'a str>, alphabet: &'a str, regexp: &'a Vec<&'a str>) -> State<'a> {
|
||||
State {
|
||||
lines,
|
||||
alphabet,
|
||||
regexp,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matches(&self, reverse: bool, unique: bool) -> Vec<Match<'a>> {
|
||||
let mut matches = Vec::new();
|
||||
|
||||
let exclude_patterns = EXCLUDE_PATTERNS
|
||||
.iter()
|
||||
.map(|tuple| (tuple.0, Regex::new(tuple.1).unwrap()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let custom_patterns = self
|
||||
.regexp
|
||||
.iter()
|
||||
.map(|regexp| ("custom", Regex::new(regexp).expect("Invalid custom regexp")))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let patterns = PATTERNS
|
||||
.iter()
|
||||
.map(|tuple| (tuple.0, Regex::new(tuple.1).unwrap()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let all_patterns = [exclude_patterns, custom_patterns, patterns].concat();
|
||||
|
||||
for (index, line) in self.lines.iter().enumerate() {
|
||||
let mut chunk: &str = line;
|
||||
let mut offset: i32 = 0;
|
||||
|
||||
loop {
|
||||
let submatches = all_patterns
|
||||
.iter()
|
||||
.filter_map(|tuple| match tuple.1.find_iter(chunk).nth(0) {
|
||||
Some(m) => Some((tuple.0, tuple.1.clone(), m)),
|
||||
None => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let first_match_option = submatches.iter().min_by(|x, y| x.2.start().cmp(&y.2.start()));
|
||||
|
||||
if let Some(first_match) = first_match_option {
|
||||
let (name, pattern, matching) = first_match;
|
||||
let text = matching.as_str();
|
||||
|
||||
if let Some(captures) = pattern.captures(text) {
|
||||
let (subtext, substart) = if let Some(capture) = captures.get(1) {
|
||||
(capture.as_str(), capture.start())
|
||||
} else {
|
||||
(matching.as_str(), 0)
|
||||
};
|
||||
|
||||
// Never hint or broke bash color sequences
|
||||
if *name != "bash" {
|
||||
matches.push(Match {
|
||||
x: offset + matching.start() as i32 + substart as i32,
|
||||
y: index as i32,
|
||||
pattern: name,
|
||||
text: subtext,
|
||||
hint: None,
|
||||
});
|
||||
}
|
||||
|
||||
chunk = chunk.get(matching.end()..).expect("Unknown chunk");
|
||||
offset += matching.end() as i32;
|
||||
} else {
|
||||
panic!("No matching?");
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let alphabet = super::alphabets::get_alphabet(self.alphabet);
|
||||
let mut hints = alphabet.hints(matches.len());
|
||||
|
||||
// This looks wrong but we do a pop after
|
||||
if !reverse {
|
||||
hints.reverse();
|
||||
} else {
|
||||
matches.reverse();
|
||||
hints.reverse();
|
||||
}
|
||||
|
||||
if unique {
|
||||
let mut previous: HashMap<&str, String> = HashMap::new();
|
||||
|
||||
for mat in &mut matches {
|
||||
if let Some(previous_hint) = previous.get(mat.text) {
|
||||
mat.hint = Some(previous_hint.clone());
|
||||
} else if let Some(hint) = hints.pop() {
|
||||
mat.hint = Some(hint.to_string().clone());
|
||||
previous.insert(mat.text, hint.to_string().clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for mat in &mut matches {
|
||||
if let Some(hint) = hints.pop() {
|
||||
mat.hint = Some(hint.to_string().clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if reverse {
|
||||
matches.reverse();
|
||||
}
|
||||
|
||||
matches
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn split(output: &str) -> Vec<&str> {
|
||||
output.split("\n").collect::<Vec<&str>>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_reverse() {
|
||||
let lines = split("lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 3);
|
||||
assert_eq!(results.first().unwrap().hint.clone().unwrap(), "a");
|
||||
assert_eq!(results.last().unwrap().hint.clone().unwrap(), "c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_unique() {
|
||||
let lines = split("lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, true);
|
||||
|
||||
assert_eq!(results.len(), 3);
|
||||
assert_eq!(results.first().unwrap().hint.clone().unwrap(), "a");
|
||||
assert_eq!(results.last().unwrap().hint.clone().unwrap(), "a");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_docker() {
|
||||
let lines = split("latest sha256:30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4 20 hours ago");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(
|
||||
results.get(0).unwrap().text,
|
||||
"30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_bash() {
|
||||
let lines = split("path: [32m/var/log/nginx.log[m\npath: [32mtest/log/nginx-2.log:32[mfolder/.nginx@4df2.log");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 3);
|
||||
assert_eq!(results.get(0).unwrap().text, "/var/log/nginx.log");
|
||||
assert_eq!(results.get(1).unwrap().text, "test/log/nginx-2.log");
|
||||
assert_eq!(results.get(2).unwrap().text, "folder/.nginx@4df2.log");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_paths() {
|
||||
let lines = split("Lorem /tmp/foo/bar_lol, lorem\n Lorem /var/log/boot-strap.log lorem ../log/kern.log lorem");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 3);
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "/tmp/foo/bar_lol");
|
||||
assert_eq!(results.get(1).unwrap().text.clone(), "/var/log/boot-strap.log");
|
||||
assert_eq!(results.get(2).unwrap().text.clone(), "../log/kern.log");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_home() {
|
||||
let lines = split("Lorem ~/.gnu/.config.txt, lorem");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "~/.gnu/.config.txt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_uids() {
|
||||
let lines = split("Lorem ipsum 123e4567-e89b-12d3-a456-426655440000 lorem\n Lorem lorem lorem");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_shas() {
|
||||
let lines = split("Lorem fd70b5695 5246ddf f924213 lorem\n Lorem 973113963b491874ab2e372ee60d4b4cb75f717c lorem");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 4);
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "fd70b5695");
|
||||
assert_eq!(results.get(1).unwrap().text.clone(), "5246ddf");
|
||||
assert_eq!(results.get(2).unwrap().text.clone(), "f924213");
|
||||
assert_eq!(
|
||||
results.get(3).unwrap().text.clone(),
|
||||
"973113963b491874ab2e372ee60d4b4cb75f717c"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_ips() {
|
||||
let lines = split("Lorem ipsum 127.0.0.1 lorem\n Lorem 255.255.10.255 lorem 127.0.0.1 lorem");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 3);
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "127.0.0.1");
|
||||
assert_eq!(results.get(1).unwrap().text.clone(), "255.255.10.255");
|
||||
assert_eq!(results.get(2).unwrap().text.clone(), "127.0.0.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_ipv6s() {
|
||||
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");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 4);
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "fe80::2:202:fe4");
|
||||
assert_eq!(
|
||||
results.get(1).unwrap().text.clone(),
|
||||
"2001:67c:670:202:7ba8:5e41:1591:d723"
|
||||
);
|
||||
assert_eq!(results.get(2).unwrap().text.clone(), "fe80::2:1");
|
||||
assert_eq!(results.get(3).unwrap().text.clone(), "fe80:22:312:fe::1%eth0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_markdown_urls() {
|
||||
let lines = split("Lorem ipsum [link](https://github.io?foo=bar)  lorem");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 2);
|
||||
assert_eq!(results.get(0).unwrap().pattern.clone(), "markdown_url");
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "https://github.io?foo=bar");
|
||||
assert_eq!(results.get(1).unwrap().pattern.clone(), "markdown_url");
|
||||
assert_eq!(results.get(1).unwrap().text.clone(), "http://cdn.com/img.jpg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_urls() {
|
||||
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");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 4);
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "https://www.rust-lang.org/tools");
|
||||
assert_eq!(results.get(0).unwrap().pattern.clone(), "url");
|
||||
assert_eq!(results.get(1).unwrap().text.clone(), "https://crates.io");
|
||||
assert_eq!(results.get(1).unwrap().pattern.clone(), "url");
|
||||
assert_eq!(results.get(2).unwrap().text.clone(), "https://github.io?foo=bar");
|
||||
assert_eq!(results.get(2).unwrap().pattern.clone(), "url");
|
||||
assert_eq!(results.get(3).unwrap().text.clone(), "ssh://github.io");
|
||||
assert_eq!(results.get(3).unwrap().pattern.clone(), "url");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_addresses() {
|
||||
let lines = split("Lorem 0xfd70b5695 0x5246ddf lorem\n Lorem 0x973113tlorem");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 3);
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "0xfd70b5695");
|
||||
assert_eq!(results.get(1).unwrap().text.clone(), "0x5246ddf");
|
||||
assert_eq!(results.get(2).unwrap().text.clone(), "0x973113");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_hex_colors() {
|
||||
let lines = split("Lorem #fd7b56 lorem #FF00FF\n Lorem #00fF05 lorem #abcd00 lorem #afRR00");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 4);
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "#fd7b56");
|
||||
assert_eq!(results.get(1).unwrap().text.clone(), "#FF00FF");
|
||||
assert_eq!(results.get(2).unwrap().text.clone(), "#00fF05");
|
||||
assert_eq!(results.get(3).unwrap().text.clone(), "#abcd00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_ipfs() {
|
||||
let lines = split("Lorem QmRdbNSxDJBXmssAc9fvTtux4duptMvfSGiGuq6yHAQVKQ lorem Qmfoobar");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(
|
||||
results.get(0).unwrap().text.clone(),
|
||||
"QmRdbNSxDJBXmssAc9fvTtux4duptMvfSGiGuq6yHAQVKQ"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_process_port() {
|
||||
let lines =
|
||||
split("Lorem 5695 52463 lorem\n Lorem 973113 lorem 99999 lorem 8888 lorem\n 23456 lorem 5432 lorem 23444");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_diff_a() {
|
||||
let lines = split("Lorem lorem\n--- a/src/main.rs");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "src/main.rs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_diff_b() {
|
||||
let lines = split("Lorem lorem\n+++ b/src/main.rs");
|
||||
let custom = [].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "src/main.rs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn priority() {
|
||||
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");
|
||||
let custom = ["CUSTOM-[0-9]{4,}", "ISSUE-[0-9]{3}"].to_vec();
|
||||
let results = State::new(&lines, "abcd", &custom).matches(false, false);
|
||||
|
||||
assert_eq!(results.len(), 9);
|
||||
assert_eq!(results.get(0).unwrap().text.clone(), "http://foo.bar");
|
||||
assert_eq!(results.get(1).unwrap().text.clone(), "CUSTOM-52463");
|
||||
assert_eq!(results.get(2).unwrap().text.clone(), "ISSUE-123");
|
||||
assert_eq!(results.get(3).unwrap().text.clone(), "/var/fd70b569/9999.log");
|
||||
assert_eq!(results.get(4).unwrap().text.clone(), "52463");
|
||||
assert_eq!(results.get(5).unwrap().text.clone(), "973113");
|
||||
assert_eq!(
|
||||
results.get(6).unwrap().text.clone(),
|
||||
"123e4567-e89b-12d3-a456-426655440000"
|
||||
);
|
||||
assert_eq!(results.get(7).unwrap().text.clone(), "8888");
|
||||
assert_eq!(results.get(8).unwrap().text.clone(), "https://crates.io/23456/fd70b569");
|
||||
}
|
||||
}
|
||||
384
src/swapper.rs
Normal file
384
src/swapper.rs
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
extern crate clap;
|
||||
|
||||
use self::clap::{App, Arg};
|
||||
use clap::crate_version;
|
||||
use regex::Regex;
|
||||
use std::process::Command;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
trait Executor {
|
||||
fn execute(&mut self, args: Vec<String>) -> String;
|
||||
fn last_executed(&self) -> Option<Vec<String>>;
|
||||
}
|
||||
|
||||
struct RealShell {
|
||||
executed: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl RealShell {
|
||||
fn new() -> RealShell {
|
||||
RealShell { executed: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl Executor for RealShell {
|
||||
fn execute(&mut self, args: Vec<String>) -> String {
|
||||
let execution = Command::new(args[0].as_str())
|
||||
.args(&args[1..])
|
||||
.output()
|
||||
.expect("Couldn't run it");
|
||||
|
||||
self.executed = Some(args);
|
||||
|
||||
let output: String = String::from_utf8_lossy(&execution.stdout).into();
|
||||
|
||||
output.trim_end().to_string()
|
||||
}
|
||||
|
||||
fn last_executed(&self) -> Option<Vec<String>> {
|
||||
self.executed.clone()
|
||||
}
|
||||
}
|
||||
|
||||
const TMP_FILE: &str = "/tmp/thumbs-last";
|
||||
|
||||
pub struct Swapper<'a> {
|
||||
executor: Box<&'a mut dyn Executor>,
|
||||
dir: String,
|
||||
command: String,
|
||||
upcase_command: String,
|
||||
active_pane_id: Option<String>,
|
||||
active_pane_height: Option<i32>,
|
||||
active_pane_scroll_position: Option<i32>,
|
||||
active_pane_in_copy_mode: Option<String>,
|
||||
thumbs_pane_id: Option<String>,
|
||||
content: Option<String>,
|
||||
signal: String,
|
||||
}
|
||||
|
||||
impl<'a> Swapper<'a> {
|
||||
fn new(executor: Box<&'a mut dyn Executor>, dir: String, command: String, upcase_command: String) -> Swapper {
|
||||
let since_the_epoch = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards");
|
||||
let signal = format!("thumbs-finished-{}", since_the_epoch.as_secs());
|
||||
|
||||
Swapper {
|
||||
executor,
|
||||
dir,
|
||||
command,
|
||||
upcase_command,
|
||||
active_pane_id: None,
|
||||
active_pane_height: None,
|
||||
active_pane_scroll_position: None,
|
||||
active_pane_in_copy_mode: None,
|
||||
thumbs_pane_id: None,
|
||||
content: None,
|
||||
signal,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn capture_active_pane(&mut self) {
|
||||
let active_command = vec![
|
||||
"tmux",
|
||||
"list-panes",
|
||||
"-F",
|
||||
"#{pane_id}:#{?pane_in_mode,1,0}:#{pane_height}:#{scroll_position}:#{?pane_active,active,nope}",
|
||||
];
|
||||
|
||||
let output = self
|
||||
.executor
|
||||
.execute(active_command.iter().map(|arg| arg.to_string()).collect());
|
||||
|
||||
let lines: Vec<&str> = output.split('\n').collect();
|
||||
let chunks: Vec<Vec<&str>> = lines.into_iter().map(|line| line.split(':').collect()).collect();
|
||||
|
||||
let active_pane = chunks
|
||||
.iter()
|
||||
.find(|&chunks| *chunks.get(4).unwrap() == "active")
|
||||
.expect("Unable to find active pane");
|
||||
|
||||
let pane_id = active_pane.get(0).unwrap();
|
||||
let pane_in_copy_mode = active_pane.get(1).unwrap().to_string();
|
||||
|
||||
self.active_pane_id = Some(pane_id.to_string());
|
||||
self.active_pane_in_copy_mode = Some(pane_in_copy_mode);
|
||||
|
||||
if self.active_pane_in_copy_mode.clone().unwrap() == "1" {
|
||||
let pane_height = active_pane
|
||||
.get(2)
|
||||
.unwrap()
|
||||
.parse()
|
||||
.expect("Unable to retrieve pane height");
|
||||
let pane_scroll_position = active_pane
|
||||
.get(3)
|
||||
.unwrap()
|
||||
.parse()
|
||||
.expect("Unable to retrieve pane scroll");
|
||||
|
||||
self.active_pane_height = Some(pane_height);
|
||||
self.active_pane_scroll_position = Some(pane_scroll_position);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute_thumbs(&mut self) {
|
||||
let options_command = vec!["tmux", "show", "-g"];
|
||||
let params: Vec<String> = options_command.iter().map(|arg| arg.to_string()).collect();
|
||||
let options = self.executor.execute(params);
|
||||
let lines: Vec<&str> = options.split('\n').collect();
|
||||
|
||||
let pattern = Regex::new(r#"@thumbs-([\w\-0-9]+) "?(\w+)"?"#).unwrap();
|
||||
|
||||
let args = lines
|
||||
.iter()
|
||||
.flat_map(|line| {
|
||||
if let Some(captures) = pattern.captures(line) {
|
||||
let name = captures.get(1).unwrap().as_str();
|
||||
let value = captures.get(2).unwrap().as_str();
|
||||
|
||||
let boolean_params = vec!["reverse", "unique", "contrast"];
|
||||
|
||||
if boolean_params.iter().any(|&x| x == name) {
|
||||
return vec![format!("--{}", name)];
|
||||
}
|
||||
|
||||
let string_params = vec![
|
||||
"position",
|
||||
"fg-color",
|
||||
"bg-color",
|
||||
"hint-bg-color",
|
||||
"hint-fg-color",
|
||||
"select-fg-color",
|
||||
"select-bg-color",
|
||||
];
|
||||
|
||||
if string_params.iter().any(|&x| x == name) {
|
||||
return vec![format!("--{}", name), format!("'{}'", value)];
|
||||
}
|
||||
|
||||
if name.starts_with("regexp") {
|
||||
return vec!["--regexp".to_string(), format!("'{}'", value)];
|
||||
}
|
||||
|
||||
vec![]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let active_pane_id = self.active_pane_id.as_mut().unwrap().clone();
|
||||
|
||||
let scroll_params = if self.active_pane_in_copy_mode.is_some() {
|
||||
if let (Some(pane_height), Some(scroll_position)) =
|
||||
(self.active_pane_scroll_position, self.active_pane_scroll_position)
|
||||
{
|
||||
format!(" -S {} -E {}", -scroll_position, pane_height - scroll_position - 1)
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
// NOTE: For debugging add echo $PWD && sleep 5 after tee
|
||||
let pane_command = format!(
|
||||
"tmux capture-pane -t {} -p{} | {}/target/release/thumbs -f '%U:%H' -t {} {}; tmux swap-pane -t {}; tmux wait-for -S {}",
|
||||
active_pane_id,
|
||||
scroll_params,
|
||||
self.dir,
|
||||
TMP_FILE,
|
||||
args.join(" "),
|
||||
active_pane_id,
|
||||
self.signal
|
||||
);
|
||||
|
||||
let thumbs_command = vec![
|
||||
"tmux",
|
||||
"new-window",
|
||||
"-P",
|
||||
"-d",
|
||||
"-n",
|
||||
"[thumbs]",
|
||||
pane_command.as_str(),
|
||||
];
|
||||
|
||||
let params: Vec<String> = thumbs_command.iter().map(|arg| arg.to_string()).collect();
|
||||
|
||||
self.thumbs_pane_id = Some(self.executor.execute(params));
|
||||
}
|
||||
|
||||
pub fn swap_panes(&mut self) {
|
||||
let active_pane_id = self.active_pane_id.as_mut().unwrap().clone();
|
||||
let thumbs_pane_id = self.thumbs_pane_id.as_mut().unwrap().clone();
|
||||
|
||||
let swap_command = vec![
|
||||
"tmux",
|
||||
"swap-pane",
|
||||
"-d",
|
||||
"-s",
|
||||
active_pane_id.as_str(),
|
||||
"-t",
|
||||
thumbs_pane_id.as_str(),
|
||||
];
|
||||
let params = swap_command.iter().map(|arg| arg.to_string()).collect();
|
||||
|
||||
self.executor.execute(params);
|
||||
}
|
||||
|
||||
pub fn wait_thumbs(&mut self) {
|
||||
let wait_command = vec!["tmux", "wait-for", self.signal.as_str()];
|
||||
let params = wait_command.iter().map(|arg| arg.to_string()).collect();
|
||||
|
||||
self.executor.execute(params);
|
||||
}
|
||||
|
||||
pub fn retrieve_content(&mut self) {
|
||||
let retrieve_command = vec!["cat", TMP_FILE];
|
||||
let params = retrieve_command.iter().map(|arg| arg.to_string()).collect();
|
||||
|
||||
self.content = Some(self.executor.execute(params));
|
||||
}
|
||||
|
||||
pub fn destroy_content(&mut self) {
|
||||
let retrieve_command = vec!["rm", TMP_FILE];
|
||||
let params = retrieve_command.iter().map(|arg| arg.to_string()).collect();
|
||||
|
||||
self.executor.execute(params);
|
||||
}
|
||||
|
||||
pub fn execute_command(&mut self) {
|
||||
let content = self.content.clone().unwrap();
|
||||
let mut splitter = content.splitn(2, ':');
|
||||
|
||||
if let Some(upcase) = splitter.next() {
|
||||
if let Some(text) = splitter.next() {
|
||||
let execute_command = if upcase.trim_end() == "true" {
|
||||
self.upcase_command.clone()
|
||||
} else {
|
||||
self.command.clone()
|
||||
};
|
||||
|
||||
let final_command = str::replace(execute_command.as_str(), "{}", text.trim_end());
|
||||
let retrieve_command = vec!["bash", "-c", final_command.as_str()];
|
||||
let params = retrieve_command.iter().map(|arg| arg.to_string()).collect();
|
||||
|
||||
self.executor.execute(params);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
struct TestShell {
|
||||
outputs: Vec<String>,
|
||||
executed: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl TestShell {
|
||||
fn new(outputs: Vec<String>) -> TestShell {
|
||||
TestShell {
|
||||
executed: None,
|
||||
outputs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Executor for TestShell {
|
||||
fn execute(&mut self, args: Vec<String>) -> String {
|
||||
self.executed = Some(args);
|
||||
self.outputs.pop().unwrap()
|
||||
}
|
||||
|
||||
fn last_executed(&self) -> Option<Vec<String>> {
|
||||
self.executed.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retrieve_active_pane() {
|
||||
let last_command_outputs = vec!["%97:100:24:1:active\n%106:100:24:1:nope\n%107:100:24:1:nope\n".to_string()];
|
||||
let mut executor = TestShell::new(last_command_outputs);
|
||||
let mut swapper = Swapper::new(Box::new(&mut executor), "".to_string(), "".to_string(), "".to_string());
|
||||
|
||||
swapper.capture_active_pane();
|
||||
|
||||
assert_eq!(swapper.active_pane_id.unwrap(), "%97");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_panes() {
|
||||
let last_command_outputs = vec![
|
||||
"".to_string(),
|
||||
"%100".to_string(),
|
||||
"".to_string(),
|
||||
"%106:100:24:1:nope\n%98:100:24:1:active\n%107:100:24:1:nope\n".to_string(),
|
||||
];
|
||||
let mut executor = TestShell::new(last_command_outputs);
|
||||
let mut swapper = Swapper::new(Box::new(&mut executor), "".to_string(), "".to_string(), "".to_string());
|
||||
|
||||
swapper.capture_active_pane();
|
||||
swapper.execute_thumbs();
|
||||
swapper.swap_panes();
|
||||
|
||||
let expectation = vec!["tmux", "swap-pane", "-d", "-s", "%98", "-t", "%100"];
|
||||
|
||||
assert_eq!(executor.last_executed().unwrap(), expectation);
|
||||
}
|
||||
}
|
||||
|
||||
fn app_args<'a>() -> clap::ArgMatches<'a> {
|
||||
App::new("tmux-thumbs")
|
||||
.version(crate_version!())
|
||||
.about("A lightning fast version of tmux-fingers, copy/pasting tmux like vimium/vimperator")
|
||||
.arg(
|
||||
Arg::with_name("dir")
|
||||
.help("Directory where to execute thumbs")
|
||||
.long("dir")
|
||||
.default_value(""),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("command")
|
||||
.help("Pick command")
|
||||
.long("command")
|
||||
.default_value("tmux set-buffer {}"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("upcase_command")
|
||||
.help("Upcase command")
|
||||
.long("upcase-command")
|
||||
.default_value("tmux set-buffer {} && tmux paste-buffer"),
|
||||
)
|
||||
.get_matches()
|
||||
}
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
let args = app_args();
|
||||
let dir = args.value_of("dir").unwrap();
|
||||
let command = args.value_of("command").unwrap();
|
||||
let upcase_command = args.value_of("upcase_command").unwrap();
|
||||
|
||||
if dir.is_empty() {
|
||||
panic!("Invalid tmux-thumbs execution. Are you trying to execute tmux-thumbs directly?")
|
||||
}
|
||||
|
||||
let mut executor = RealShell::new();
|
||||
let mut swapper = Swapper::new(
|
||||
Box::new(&mut executor),
|
||||
dir.to_string(),
|
||||
command.to_string(),
|
||||
upcase_command.to_string(),
|
||||
);
|
||||
|
||||
swapper.capture_active_pane();
|
||||
swapper.execute_thumbs();
|
||||
swapper.swap_panes();
|
||||
swapper.wait_thumbs();
|
||||
swapper.retrieve_content();
|
||||
swapper.destroy_content();
|
||||
swapper.execute_command();
|
||||
Ok(())
|
||||
}
|
||||
304
src/view.rs
Normal file
304
src/view.rs
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
use super::*;
|
||||
use std::char;
|
||||
use std::io::{stdout, Read, Write};
|
||||
use termion::async_stdin;
|
||||
use termion::event::Key;
|
||||
use termion::input::TermRead;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::AlternateScreen;
|
||||
use termion::{color, cursor};
|
||||
|
||||
pub struct View<'a> {
|
||||
state: &'a mut state::State<'a>,
|
||||
skip: usize,
|
||||
multi: bool,
|
||||
contrast: bool,
|
||||
position: &'a str,
|
||||
matches: Vec<state::Match<'a>>,
|
||||
select_foreground_color: Box<&'a dyn color::Color>,
|
||||
select_background_color: Box<&'a dyn color::Color>,
|
||||
foreground_color: Box<&'a dyn color::Color>,
|
||||
background_color: Box<&'a dyn color::Color>,
|
||||
hint_background_color: Box<&'a dyn color::Color>,
|
||||
hint_foreground_color: Box<&'a dyn color::Color>,
|
||||
}
|
||||
|
||||
enum CaptureEvent {
|
||||
Exit,
|
||||
Hint(Vec<(String, bool)>),
|
||||
}
|
||||
|
||||
impl<'a> View<'a> {
|
||||
pub fn new(
|
||||
state: &'a mut state::State<'a>,
|
||||
multi: bool,
|
||||
reverse: bool,
|
||||
unique: bool,
|
||||
contrast: bool,
|
||||
position: &'a str,
|
||||
select_foreground_color: Box<&'a dyn color::Color>,
|
||||
select_background_color: Box<&'a dyn color::Color>,
|
||||
foreground_color: Box<&'a dyn color::Color>,
|
||||
background_color: Box<&'a dyn color::Color>,
|
||||
hint_foreground_color: Box<&'a dyn color::Color>,
|
||||
hint_background_color: Box<&'a dyn color::Color>,
|
||||
) -> View<'a> {
|
||||
let matches = state.matches(reverse, unique);
|
||||
let skip = if reverse { matches.len() - 1 } else { 0 };
|
||||
|
||||
View {
|
||||
state,
|
||||
skip,
|
||||
multi,
|
||||
contrast,
|
||||
position,
|
||||
matches,
|
||||
select_foreground_color,
|
||||
select_background_color,
|
||||
foreground_color,
|
||||
background_color,
|
||||
hint_foreground_color,
|
||||
hint_background_color,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prev(&mut self) {
|
||||
if self.skip > 0 {
|
||||
self.skip -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
if self.skip < self.matches.len() - 1 {
|
||||
self.skip += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn make_hint_text(&self, hint: &str) -> String {
|
||||
if self.contrast {
|
||||
format!("[{}]", hint)
|
||||
} else {
|
||||
hint.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&self, stdout: &mut dyn Write) -> () {
|
||||
write!(stdout, "{}", cursor::Hide).unwrap();
|
||||
|
||||
for (index, line) in self.state.lines.iter().enumerate() {
|
||||
let clean = line.trim_end_matches(|c: char| c.is_whitespace());
|
||||
|
||||
if !clean.is_empty() {
|
||||
let text = self.make_hint_text(line);
|
||||
|
||||
print!("{goto}{text}", goto = cursor::Goto(1, index as u16 + 1), text = &text);
|
||||
}
|
||||
}
|
||||
|
||||
let selected = self.matches.get(self.skip);
|
||||
|
||||
for mat in self.matches.iter() {
|
||||
let selected_color = if selected == Some(mat) {
|
||||
&self.select_foreground_color
|
||||
} else {
|
||||
&self.foreground_color
|
||||
};
|
||||
let selected_background_color = if selected == Some(mat) {
|
||||
&self.select_background_color
|
||||
} else {
|
||||
&self.background_color
|
||||
};
|
||||
|
||||
// Find long utf sequences and extract it from mat.x
|
||||
let line = &self.state.lines[mat.y as usize];
|
||||
let prefix = &line[0..mat.x as usize];
|
||||
let extra = prefix.len() - prefix.chars().count();
|
||||
let offset = (mat.x as u16) - (extra as u16);
|
||||
let text = self.make_hint_text(mat.text);
|
||||
|
||||
print!(
|
||||
"{goto}{background}{foregroud}{text}{resetf}{resetb}",
|
||||
goto = cursor::Goto(offset + 1, mat.y as u16 + 1),
|
||||
foregroud = color::Fg(**selected_color),
|
||||
background = color::Bg(**selected_background_color),
|
||||
resetf = color::Fg(color::Reset),
|
||||
resetb = color::Bg(color::Reset),
|
||||
text = &text
|
||||
);
|
||||
|
||||
if let Some(ref hint) = mat.hint {
|
||||
let extra_position = if self.position == "left" {
|
||||
0
|
||||
} else {
|
||||
text.len() - mat.hint.clone().unwrap().len()
|
||||
};
|
||||
|
||||
let text = self.make_hint_text(hint.as_str());
|
||||
|
||||
print!(
|
||||
"{goto}{background}{foregroud}{text}{resetf}{resetb}",
|
||||
goto = cursor::Goto(offset + extra_position as u16 + 1, mat.y as u16 + 1),
|
||||
foregroud = color::Fg(*self.hint_foreground_color),
|
||||
background = color::Bg(*self.hint_background_color),
|
||||
resetf = color::Fg(color::Reset),
|
||||
resetb = color::Bg(color::Reset),
|
||||
text = &text
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
stdout.flush().unwrap();
|
||||
}
|
||||
|
||||
fn listen(&mut self, stdin: &mut dyn Read, stdout: &mut dyn Write) -> CaptureEvent {
|
||||
if self.matches.is_empty() {
|
||||
return CaptureEvent::Exit
|
||||
}
|
||||
|
||||
let mut chosen = vec![];
|
||||
let mut typed_hint: String = "".to_owned();
|
||||
let longest_hint = self
|
||||
.matches
|
||||
.iter()
|
||||
.filter_map(|m| m.hint.clone())
|
||||
.max_by(|x, y| x.len().cmp(&y.len()))
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
self.render(stdout);
|
||||
|
||||
loop {
|
||||
match stdin.keys().next() {
|
||||
Some(key) => {
|
||||
match key {
|
||||
Ok(key) => {
|
||||
match key {
|
||||
Key::Esc => {
|
||||
if self.multi && !typed_hint.is_empty() {
|
||||
typed_hint.clear();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Key::Insert => match self.matches.iter().enumerate().find(|&h| h.0 == self.skip) {
|
||||
Some(hm) => {
|
||||
chosen.push((hm.1.text.to_string(), false));
|
||||
|
||||
if !self.multi {
|
||||
return CaptureEvent::Hint(chosen);
|
||||
}
|
||||
}
|
||||
_ => panic!("Match not found?"),
|
||||
},
|
||||
Key::Up => {
|
||||
self.prev();
|
||||
}
|
||||
Key::Down => {
|
||||
self.next();
|
||||
}
|
||||
Key::Left => {
|
||||
self.prev();
|
||||
}
|
||||
Key::Right => {
|
||||
self.next();
|
||||
}
|
||||
Key::Char(ch) => {
|
||||
if ch == ' ' && self.multi {
|
||||
return CaptureEvent::Hint(chosen);
|
||||
}
|
||||
|
||||
let key = ch.to_string();
|
||||
let lower_key = key.to_lowercase();
|
||||
|
||||
typed_hint.push_str(lower_key.as_str());
|
||||
|
||||
let selection = self.matches.iter().find(|mat| mat.hint == Some(typed_hint.clone()));
|
||||
|
||||
match selection {
|
||||
Some(mat) => {
|
||||
chosen.push((mat.text.to_string(), key != lower_key));
|
||||
|
||||
if self.multi {
|
||||
typed_hint.clear();
|
||||
} else {
|
||||
return CaptureEvent::Hint(chosen);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if !self.multi && typed_hint.len() >= longest_hint.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Unknown key
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => panic!(err),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Nothing in the buffer. Wait for a bit...
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
|
||||
self.render(stdout);
|
||||
}
|
||||
|
||||
CaptureEvent::Exit
|
||||
}
|
||||
|
||||
pub fn present(&mut self) -> Vec<(String, bool)> {
|
||||
let mut stdin = async_stdin();
|
||||
let mut stdout = AlternateScreen::from(stdout().into_raw_mode().unwrap());
|
||||
|
||||
let hints = match self.listen(&mut stdin, &mut stdout) {
|
||||
CaptureEvent::Exit => vec![],
|
||||
CaptureEvent::Hint(chosen) => chosen,
|
||||
};
|
||||
|
||||
write!(stdout, "{}", cursor::Show).unwrap();
|
||||
|
||||
hints
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn split(output: &str) -> Vec<&str> {
|
||||
output.split("\n").collect::<Vec<&str>>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hint_text() {
|
||||
let lines = split("lorem 127.0.0.1 lorem");
|
||||
let custom = [].to_vec();
|
||||
let mut state = state::State::new(&lines, "abcd", &custom);
|
||||
let mut view = View {
|
||||
state: &mut state,
|
||||
skip: 0,
|
||||
multi: false,
|
||||
contrast: false,
|
||||
position: &"",
|
||||
matches: vec![],
|
||||
select_foreground_color: colors::get_color("default"),
|
||||
select_background_color: colors::get_color("default"),
|
||||
foreground_color: colors::get_color("default"),
|
||||
background_color: colors::get_color("default"),
|
||||
hint_background_color: colors::get_color("default"),
|
||||
hint_foreground_color: colors::get_color("default"),
|
||||
};
|
||||
|
||||
let result = view.make_hint_text("a");
|
||||
assert_eq!(result, "a".to_string());
|
||||
|
||||
view.contrast = true;
|
||||
let result = view.make_hint_text("a");
|
||||
assert_eq!(result, "[a]".to_string());
|
||||
}
|
||||
}
|
||||
28
tmux-thumbs.sh
Executable file
28
tmux-thumbs.sh
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
[ -f ~/.bash_profile ] && source ~/.bash_profile
|
||||
|
||||
PARAMS=()
|
||||
|
||||
function add-option-param {
|
||||
VALUE=$(tmux show -vg @thumbs-$1 2> /dev/null)
|
||||
|
||||
if [[ ${VALUE} ]]; then
|
||||
PARAMS+=("--$1=${VALUE}")
|
||||
fi
|
||||
}
|
||||
|
||||
add-option-param "command"
|
||||
add-option-param "upcase-command"
|
||||
|
||||
# Remove empty arguments from PARAMS.
|
||||
# Otherwise, they would choke up tmux-thumbs when passed to it.
|
||||
for i in "${!PARAMS[@]}"; do
|
||||
[ -n "${PARAMS[$i]}" ] || unset "PARAMS[$i]"
|
||||
done
|
||||
|
||||
CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
${CURRENT_DIR}/target/release/tmux-thumbs --dir "${CURRENT_DIR}" "${PARAMS[@]}"
|
||||
|
||||
true
|
||||
15
tmux-thumbs.tmux
Executable file
15
tmux-thumbs.tmux
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
DEFAULT_THUMBS_KEY="space"
|
||||
THUMBS_KEY=$(tmux show-option -gqv @thumbs-key)
|
||||
THUMBS_KEY=${THUMBS_KEY:-$DEFAULT_THUMBS_KEY}
|
||||
|
||||
tmux bind-key $THUMBS_KEY run-shell -b "${CURRENT_DIR}/tmux-thumbs.sh"
|
||||
|
||||
BINARY="${CURRENT_DIR}/target/release/thumbs"
|
||||
|
||||
if [ ! -f "$BINARY" ]; then
|
||||
cd "${CURRENT_DIR}" && cargo build --release
|
||||
fi
|
||||
Loading…
Add table
Add a link
Reference in a new issue