├── .cargo └── config.toml ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ └── bug_report.md ├── dependabot.yml ├── pr-title-checker-config.json ├── pull_request_template.md ├── release-please │ ├── config.json │ └── manifest.json ├── release-plz.toml └── workflows │ ├── pr.yml │ ├── release-binaries.yml │ ├── release-pr.yml │ └── test.yml ├── .gitignore ├── .rustfmt.toml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── bin └── sk-tmux ├── e2e ├── Cargo.toml ├── src │ └── lib.rs └── tests │ ├── binds.rs │ ├── case.rs │ ├── defaults.rs │ ├── history.rs │ ├── issues.rs │ ├── keys.rs │ ├── keys_interactive.rs │ ├── options.rs │ ├── preview.rs │ ├── tiebreak.rs │ └── tmux.rs ├── install ├── man └── man1 │ ├── sk-tmux.1 │ └── sk.1 ├── plugin └── skim.vim ├── shell ├── LICENSE ├── README.md ├── completion.bash ├── completion.fish ├── completion.zsh ├── key-bindings.bash ├── key-bindings.fish ├── key-bindings.zsh └── version.txt ├── skim-common ├── Cargo.toml └── src │ ├── lib.rs │ └── spinlock.rs ├── skim-tuikit ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── examples │ ├── 256color.rs │ ├── 256color_on_screen.rs │ ├── custom-event.rs │ ├── get_keys.rs │ ├── hello-world.rs │ ├── split.rs │ ├── stack.rs │ ├── term_size.rs │ ├── termbox.rs │ ├── true_color.rs │ └── win.rs └── src │ ├── attr.rs │ ├── canvas.rs │ ├── cell.rs │ ├── color.rs │ ├── draw.rs │ ├── error.rs │ ├── event.rs │ ├── input.rs │ ├── key.rs │ ├── lib.rs │ ├── macros.rs │ ├── output.rs │ ├── prelude.rs │ ├── raw.rs │ ├── screen.rs │ ├── sys │ ├── file.rs │ ├── mod.rs │ ├── signal.rs │ └── size.rs │ ├── term.rs │ └── widget │ ├── align.rs │ ├── mod.rs │ ├── split.rs │ ├── stack.rs │ ├── util.rs │ └── win.rs ├── skim ├── Cargo.toml ├── examples │ ├── cmd_collector.rs │ ├── custom_item.rs │ ├── custom_keybinding_actions.rs │ ├── downcast.rs │ ├── nth.rs │ ├── option_builder.rs │ ├── preview_callback.rs │ ├── sample.rs │ ├── selector.rs │ └── tuikit.rs └── src │ ├── ansi.rs │ ├── bin │ └── main.rs │ ├── engine │ ├── all.rs │ ├── andor.rs │ ├── exact.rs │ ├── factory.rs │ ├── fuzzy.rs │ ├── mod.rs │ ├── regexp.rs │ └── util.rs │ ├── event.rs │ ├── field.rs │ ├── global.rs │ ├── header.rs │ ├── helper │ ├── item.rs │ ├── item_reader.rs │ ├── mod.rs │ └── selector.rs │ ├── input.rs │ ├── item.rs │ ├── lib.rs │ ├── matcher.rs │ ├── model │ ├── mod.rs │ ├── options.rs │ └── status.rs │ ├── options.rs │ ├── orderedvec.rs │ ├── output.rs │ ├── prelude.rs │ ├── previewer.rs │ ├── query.rs │ ├── reader.rs │ ├── selection.rs │ ├── theme.rs │ ├── tmux.rs │ └── util.rs └── xtask ├── Cargo.toml └── src └── main.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | xtask = "run --package xtask --" 3 | e2e = "test --package e2e" 4 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## contributor guide 2 | 3 | ### GPT/chatbot 4 | 5 | Though we tolereate GPT-assisted dev (e.g. github copilot), 6 | it is accepted but will be judged as strictly as human-only coding: 7 | - Please avoid generating PRs, PRs comments and issue with a chatbot. 8 | Please avoid generating PRs, PR comments, and issues with a chatbot. 9 | - do not submit code which you don't understand. 10 | Avoid submitting code you do not fully understand. 11 | Additionally, extensive refactoring is discouraged as it takes significant time for maintainers to review. 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug encountered using `skim` 4 | title: "[BUG] xxx" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment (please complete the following information):** 27 | - OS (`uname -a`): 28 | - `skim` version (`sk --version`): 29 | - Shell and version: 30 | - Variables (`env | grep '^SKIM'`): 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | commit-message: 8 | prefix: chore(deps) 9 | prefix-development: chore(dev-deps) 10 | -------------------------------------------------------------------------------- /.github/pr-title-checker-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "LABEL": { 3 | "name": "invalid-title", 4 | "color": "B60205" 5 | }, 6 | "CHECKS": { 7 | "prefixes": [ 8 | "feat: ", 9 | "feature: ", 10 | "fix: ", 11 | "bugfix: ", 12 | "perf: ", 13 | "refactor: ", 14 | "test: ", 15 | "tests: ", 16 | "build: ", 17 | "ci: ", 18 | "doc: ", 19 | "docs: ", 20 | "style: ", 21 | "chore: ", 22 | "other: " 23 | ], 24 | "regexp": "^\\w+(\\([a-z_-]+\\))?: ", 25 | "regexpFlags": "", 26 | "ignoreLabels": [ 27 | "skip-title-check" 28 | ] 29 | }, 30 | "MESSAGES": { 31 | "success": "PR title is valid", 32 | "failure": "PR title is invalid", 33 | "notice": "" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Checklist 2 | 3 | _check the box if it is not applicable to your changes_ 4 | - [ ] I have updated the README with the necessary documentation 5 | - [ ] I have added unit tests 6 | - [ ] I have added [end-to-end tests](test/test_skim.py) 7 | - [ ] I have linked all related issues or PRs 8 | 9 | ## Description of the changes 10 | 11 | -------------------------------------------------------------------------------- /.github/release-please/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://github.com/googleapis/release-please/raw/refs/heads/main/schemas/config.json", 3 | "release-type": "rust", 4 | "last-release-sha": "e93e65c35205290f8e1d0ed833ac6c8016d7a818", 5 | "bump-minor-pre-major": true, 6 | "separate-pull-requests": false, 7 | "tag-separator": "@", 8 | "always-update": true, 9 | "group-pull-request-title-pattern": "chore: release ${version}", 10 | "plugins": [ 11 | { 12 | "type": "cargo-workspace" 13 | } 14 | ], 15 | "packages": { 16 | ".": { 17 | "package-name": "root", 18 | "component": "root", 19 | "include-component-in-tag": false, 20 | "changelog-path": "CHANGELOG.md" 21 | }, 22 | "skim": { 23 | "skip-github-release": true 24 | }, 25 | "xtask": { 26 | "skip-github-release": true 27 | }, 28 | "e2e": { 29 | "skip-github-release": true 30 | }, 31 | "common": { 32 | "skip-github-release": true 33 | }, 34 | "tuikit": { 35 | "skip-github-release": true 36 | }, 37 | "shell": { 38 | "release-type": "simple", 39 | "skip-github-release": true, 40 | "package-name": "shell-integrations" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/release-please/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "1.16.2", 3 | "skim": "1.16.2", 4 | "e2e": "0.1.0", 5 | "xtask": "0.1.1", 6 | "tuikit": "0.6.0", 7 | "common": "0.1.0", 8 | "shell": "0.1.0" 9 | } 10 | -------------------------------------------------------------------------------- /.github/release-plz.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | allow_dirty = false 3 | changelog_update = false 4 | dependencies_update = true 5 | git_release_enable = false 6 | git_tag_enable = false 7 | pr_branch_prefix = "release-plz-" 8 | pr_labels = ["release"] 9 | pr_name = "chore(release): release" 10 | publish = false 11 | publish_no_verify = true 12 | semver_check = true 13 | 14 | features_always_increment_minor = true 15 | 16 | [[package]] 17 | name = "skim" 18 | publish = true 19 | publish_no_verify = false 20 | changelog_include = [ 21 | "skim", 22 | "skim-common", 23 | "skim-tuikit", 24 | "shell", 25 | "plugin", 26 | "bin", 27 | "xtask", 28 | "e2e" 29 | ] 30 | changelog_path = "./CHANGELOG.md" 31 | changelog_update = true 32 | git_tag_enable = true 33 | git_release_enable = true 34 | git_tag_name = "v{{ version }}" 35 | 36 | [[package]] 37 | name = "skim-common" 38 | publish = true 39 | publish_no_verify = false 40 | git_tag_enable = true 41 | git_tag_name = "common-v{{ version }}" 42 | 43 | [[package]] 44 | name = "skim-tuikit" 45 | publish = true 46 | publish_no_verify = false 47 | git_tag_enable = true 48 | git_tag_name = "tuikit-v{{ version }}" 49 | 50 | [[package]] 51 | name = "e2e" 52 | release = false 53 | publish = false 54 | [[package]] 55 | name = "xtask" 56 | release = false 57 | publish = false 58 | 59 | [changelog] 60 | header = """# Changelog 61 | 62 | ## [Unreleased] 63 | """ 64 | 65 | body = """ 66 | 67 | {%- macro username(commit) -%} 68 | {% if commit.remote.username %} (by @{{ commit.remote.username }}){% endif -%} 69 | {% endmacro -%} 70 | {% macro print_commit(commit) -%} 71 | - {% if commit.scope %}**{{ commit.scope }}:** {% endif %}\ 72 | {% if commit.breaking %}[**breaking**] {% endif %}\ 73 | {{ commit.message | upper_first }} - \ 74 | ([{{ commit.id | truncate(length=7, end="") }}]({{ remote.link }}/commit/{{ commit.id }}))\ 75 | {{ self::username(commit=commit) }}\ 76 | {% endmacro -%} 77 | 78 | {% if version %}\ 79 | {% if previous.version %}\ 80 | ## [{{ version }}]({{ release_link }}) 81 | {% else %}\ 82 | ## [{{ version }}] 83 | {% endif %}\ 84 | {% endif %}\ 85 | 86 | {% for group, commits in commits 87 | | filter(attribute="merge_commit", value=false) 88 | | unique(attribute="message") 89 | | group_by(attribute="group") %} 90 | ### {{ group | striptags | trim | upper_first }} 91 | 92 | {% for commit in commits 93 | | filter(attribute="scope") 94 | | sort(attribute="scope") %} 95 | {{ self::print_commit(commit=commit) }} 96 | {%- endfor -%} 97 | {% raw %}\n{% endraw %}\ 98 | {%- for commit in commits %} 99 | {%- if not commit.scope -%} 100 | {{ self::print_commit(commit=commit) }} 101 | {% endif -%} 102 | {% endfor -%} 103 | {% endfor %}\n 104 | 105 | {%- if remote.contributors %} 106 | ### Contributors 107 | {% for contributor in remote.contributors %} 108 | * @{{ contributor.username }} 109 | {%- endfor %} 110 | {% endif -%} 111 | """ 112 | 113 | commit_parsers = [ 114 | { message = "^feat", group = "⛰️ Features" }, 115 | { message = "^fix", group = "🐛 Bug Fixes" }, 116 | { message = "^doc", group = "📚 Documentation" }, 117 | { message = "^perf", group = "⚡ Performance" }, 118 | { message = "^refactor\\(clippy\\)", skip = true }, 119 | { message = "^refactor", group = "🚜 Refactor" }, 120 | { message = "^style", group = "🎨 Styling" }, 121 | { message = "^test", group = "🧪 Testing" }, 122 | { message = "^chore\\(release\\):", skip = true }, 123 | { message = "^chore: release", skip = true }, 124 | { message = "^chore\\(deps.*\\)", skip = true }, 125 | { message = "^chore\\(pr\\)", skip = true }, 126 | { message = "^chore\\(pull\\)", skip = true }, 127 | { message = "^chore\\(npm\\).*yarn\\.lock", skip = true }, 128 | { message = "^chore|^ci", group = "⚙️ Miscellaneous Tasks" }, 129 | { body = ".*security", group = "🛡️ Security" }, 130 | { message = "^revert", group = "◀️ Revert" }, 131 | ] 132 | 133 | link_parsers = [ 134 | { pattern = "#(\\d+)", href = "{{ remote.link }}/issues/$1" }, 135 | { pattern = "RFC(\\d+)", text = "ietf-rfc$1", href = "https://datatracker.ietf.org/doc/html/rfc$1" }, 136 | ] 137 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Requests" 2 | on: 3 | pull_request_target: 4 | types: 5 | - opened 6 | - edited 7 | - synchronize 8 | - labeled 9 | - unlabeled 10 | 11 | jobs: 12 | check: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | pull-requests: write 16 | steps: 17 | - uses: thehanimo/pr-title-checker@v1.4.3 18 | with: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | pass_on_octokit_error: false 21 | configuration_path: .github/pr-title-checker-config.json 22 | generate-files: 23 | runs-on: ubuntu-22.04 24 | permissions: 25 | contents: write 26 | steps: 27 | - uses: actions/create-github-app-token@v1 28 | id: app-token 29 | with: 30 | app-id: ${{ vars.SKIM_RS_BOT_APP_ID }} 31 | private-key: ${{ secrets.SKIM_RS_BOT_PRIVATE_KEY }} 32 | - name: Checkout Git repo 33 | uses: actions/checkout@v4 34 | with: 35 | fetch-depth: 0 36 | ref: ${{github.event.pull_request.head.ref}} 37 | repository: ${{github.event.pull_request.head.repo.full_name}} 38 | token: ${{ steps.app-token.outputs.token }} 39 | - name: Install correct toolchain 40 | uses: actions-rs/toolchain@v1 41 | with: 42 | toolchain: stable 43 | - name: Cache 44 | uses: Swatinem/rust-cache@v2 45 | - name: Generate manpage 46 | uses: actions-rs/cargo@v1 47 | with: 48 | command: run 49 | args: --package xtask mangen 50 | - name: Generate completions 51 | uses: actions-rs/cargo@v1 52 | with: 53 | command: run 54 | args: --package xtask compgen 55 | - name: Push modified files 56 | run: | 57 | git branch -v 58 | git config user.email "skim-bot@skim-rs.github.io" 59 | git config user.name "Skim bot" 60 | git commit -am 'chore: generate completions & manpage' || exit 0 61 | git push 62 | -------------------------------------------------------------------------------- /.github/workflows/release-binaries.yml: -------------------------------------------------------------------------------- 1 | name: Build and upload release binaries 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | release: 8 | types: [published] 9 | 10 | jobs: 11 | build-binaries: 12 | name: Build release binaries 13 | permissions: 14 | contents: write 15 | strategy: 16 | matrix: 17 | arch: 18 | - x86_64 19 | - aarch64 20 | - arm 21 | - armv7 22 | os: 23 | - unknown-linux-musl 24 | - apple-darwin 25 | exclude: 26 | - arch: arm 27 | os: apple-darwin 28 | - arch: armv7 29 | os: apple-darwin 30 | runs-on: ${{ contains(matrix.os, 'apple') && 'macos' || 'ubuntu' }}-latest 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v4 34 | with: 35 | fetch-depth: 1 36 | - name: Build 37 | uses: houseabsolute/actions-rust-cross@v0 38 | with: 39 | command: build 40 | target: ${{ matrix.arch }}-${{ matrix.os }}${{ contains(matrix.arch, 'arm') && 'eabi' || '' }} 41 | args: "--release --locked" 42 | - name: Create archive 43 | env: 44 | BIN_NAME: "sk" 45 | ARCHIVE_NAME: "skim-${{ matrix.arch }}-${{ matrix.os }}${{ contains(matrix.arch, 'arm') && 'eabi' || '' }}.tgz" 46 | run: | 47 | tar -C target/*/release/ -czvf "$ARCHIVE_NAME" "$BIN_NAME" 48 | - name: Upload binary 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | ARCHIVE_NAME: "skim-${{ matrix.arch }}-${{ matrix.os }}${{ contains(matrix.arch, 'arm') && 'eabi' || '' }}.tgz" 52 | run: | 53 | TAG=$(echo "$GITHUB_REF" | sed 's#refs/tags/##') 54 | gh release upload "$TAG" "$ARCHIVE_NAME" 55 | -------------------------------------------------------------------------------- /.github/workflows/release-pr.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | 6 | name: Create release PR or create github release 7 | 8 | jobs: 9 | release-pr: 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/create-github-app-token@v1 16 | id: app-token 17 | with: 18 | app-id: ${{ vars.SKIM_RS_BOT_APP_ID }} 19 | private-key: ${{ secrets.SKIM_RS_BOT_PRIVATE_KEY }} 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 1 24 | - name: Install correct toolchain 25 | uses: actions-rs/toolchain@v1 26 | with: 27 | toolchain: stable 28 | - id: release-pr 29 | uses: release-plz/action@v0.5 30 | env: 31 | GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} 32 | CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} 33 | with: 34 | config: .github/release-plz.toml 35 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | branches: 8 | - master 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | unittests: 16 | runs-on: ${{matrix.os}} 17 | strategy: 18 | matrix: 19 | build: [linux, macos] 20 | include: 21 | - build: linux 22 | os: ubuntu-latest 23 | rust: stable 24 | target: x86_64-unknown-linux-musl 25 | - build: macos 26 | os: macos-latest 27 | rust: stable 28 | target: x86_64-apple-darwin 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v2 32 | with: 33 | fetch-depth: 1 34 | - name: Install correct toolchain 35 | uses: actions-rs/toolchain@v1 36 | with: 37 | toolchain: ${{ matrix.rust }} 38 | target: ${{ matrix.target }} 39 | - name: Cache 40 | uses: Swatinem/rust-cache@v2 41 | - name: Run unit tests 42 | run: cargo test -p skim -p skim-common -p skim-tuikit 43 | 44 | e2e: 45 | runs-on: ${{matrix.os}} 46 | strategy: 47 | matrix: 48 | build: [linux, macos] 49 | include: 50 | - build: linux 51 | os: ubuntu-latest 52 | rust: stable 53 | target: x86_64-unknown-linux-musl 54 | - build: macos 55 | os: macos-latest 56 | rust: stable 57 | target: x86_64-apple-darwin 58 | steps: 59 | - name: "[linux] Install dependencies" 60 | run: | 61 | sudo apt-get install tmux 62 | tmux -V 63 | locale 64 | if: runner.os == 'Linux' 65 | - name: "[macos] Install dependencies" 66 | run: | 67 | brew install tmux 68 | tmux -V 69 | locale 70 | if: runner.os == 'macOS' 71 | env: 72 | HOMEBREW_NO_AUTO_UPDATE: 1 73 | - name: Checkout repository 74 | uses: actions/checkout@v2 75 | with: 76 | fetch-depth: 1 77 | - name: Install correct toolchain 78 | uses: actions-rs/toolchain@v1 79 | with: 80 | toolchain: ${{ matrix.rust }} 81 | target: ${{ matrix.target }} 82 | - name: Cache 83 | uses: Swatinem/rust-cache@v2 84 | - name: Run end-to-end tests 85 | run: | 86 | cargo build --release 87 | tmux new-session -d 88 | cargo e2e -j8 89 | env: 90 | LC_ALL: en_US.UTF-8 91 | TERM: xterm-256color 92 | 93 | clippy: 94 | runs-on: ubuntu-latest 95 | steps: 96 | - name: Checkout repository 97 | uses: actions/checkout@v2 98 | with: 99 | fetch-depth: 1 100 | - name: Install correct toolchain 101 | uses: actions-rs/toolchain@v1 102 | with: 103 | toolchain: stable 104 | profile: minimal 105 | components: clippy 106 | - name: Cache 107 | uses: Swatinem/rust-cache@v2 108 | - name: Clippy 109 | run: cargo clippy 110 | 111 | rustfmt: 112 | runs-on: ubuntu-latest 113 | steps: 114 | - name: Checkout repository 115 | uses: actions/checkout@v2 116 | - name: Install Rust 117 | uses: actions-rs/toolchain@v1 118 | with: 119 | toolchain: stable 120 | profile: minimal 121 | components: rustfmt 122 | - name: Check formatting 123 | run: | 124 | cargo fmt --all -- --check 125 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled files 2 | *.o 3 | *.so 4 | *.rlib 5 | *.dll 6 | 7 | # Executables 8 | *.exe 9 | 10 | # Generated by Cargo 11 | /target/ 12 | 13 | /bin/sk 14 | .idea/ 15 | .ropeproject/ 16 | 17 | # E2E 18 | __pycache__ 19 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "e2e", 4 | "skim", 5 | "skim-tuikit", 6 | "skim-common", 7 | "xtask" 8 | ] 9 | resolver = "2" 10 | default-members = ["skim"] 11 | 12 | [profile.release] 13 | lto = true 14 | 15 | [workspace.dependencies] 16 | beef = "0.5.2" 17 | bitflags = "1.3.2" 18 | chrono = "0.4.40" 19 | clap = "4.5.38" 20 | clap_complete = "4.5.50" 21 | clap_complete_fig = "4.5.2" 22 | clap_complete_nushell = "4.5.5" 23 | clap_mangen = "0.2.26" 24 | crossbeam = "0.8.2" 25 | defer-drop = "1.3.0" 26 | derive_builder = "0.20.2" 27 | env_logger = "0.11.6" 28 | fuzzy-matcher = "0.3.7" 29 | indexmap = "2.8.0" 30 | lazy_static = "1.2.0" 31 | log = "0.4.27" 32 | nix = { version = "0.29.0", default-features = false, features = ["fs"]} 33 | rand = "0.9.0" 34 | rayon = "1.5.3" 35 | regex = "1.6.0" 36 | shell-quote = "0.7.2" 37 | shlex = "1.1.0" 38 | tempfile = "3.20.0" 39 | term = "0.7" 40 | time = "0.3.41" 41 | timer = "0.2.0" 42 | unicode-width = "0.2.0" 43 | vte = "0.15.0" 44 | which = "7.0.2" 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jinzhou Zhang 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 | -------------------------------------------------------------------------------- /e2e/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "e2e" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | rand = { workspace = true } 8 | tempfile = { workspace = true } 9 | which = { workspace = true } 10 | -------------------------------------------------------------------------------- /e2e/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::{Display, Formatter}, 3 | fs::File, 4 | io::{BufReader, Error, ErrorKind, Read, Result}, 5 | path::Path, 6 | process::Command, 7 | thread::sleep, 8 | time::Duration, 9 | }; 10 | 11 | use rand::distr::{Alphanumeric, SampleString as _}; 12 | use tempfile::{NamedTempFile, TempDir, tempdir}; 13 | use which::which; 14 | 15 | pub static SK: &str = "SKIM_DEFAULT_OPTIONS= SKIM_DEFAULT_COMMAND= cargo run --package skim --release --"; 16 | 17 | pub fn sk(outfile: &str, opts: &[&str]) -> String { 18 | format!( 19 | "{} {} > {}.part; mv {}.part {}", 20 | SK, 21 | opts.join(" "), 22 | outfile, 23 | outfile, 24 | outfile 25 | ) 26 | } 27 | 28 | fn wait(pred: F) -> Result 29 | where 30 | F: Fn() -> Result, 31 | { 32 | for _ in 1..2000 { 33 | if let Ok(t) = pred() { 34 | return Ok(t); 35 | } 36 | sleep(Duration::from_millis(50)); 37 | } 38 | Err(Error::new(ErrorKind::TimedOut, "wait timed out")) 39 | } 40 | 41 | pub enum Keys<'a> { 42 | Str(&'a str), 43 | Key(char), 44 | Ctrl(&'a Keys<'a>), 45 | Alt(&'a Keys<'a>), 46 | Enter, 47 | Tab, 48 | BTab, 49 | Left, 50 | Right, 51 | BSpace, 52 | } 53 | 54 | impl Display for Keys<'_> { 55 | fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { 56 | use Keys::*; 57 | match self { 58 | Str(s) => write!(f, "{}", s), 59 | Key(c) => write!(f, "{}", c), 60 | Ctrl(k) => write!(f, "C-{}", k), 61 | Alt(k) => write!(f, "M-{}", k), 62 | Enter => write!(f, "Enter"), 63 | Tab => write!(f, "Tab"), 64 | BTab => write!(f, "BTab"), 65 | Left => write!(f, "Left"), 66 | Right => write!(f, "Right"), 67 | BSpace => write!(f, "BSpace"), 68 | } 69 | } 70 | } 71 | 72 | pub struct TmuxController { 73 | window: String, 74 | pub tempdir: TempDir, 75 | } 76 | 77 | impl TmuxController { 78 | pub fn run(args: &[&str]) -> Result> { 79 | println!("Running {:?}", args); 80 | let output = Command::new(which("tmux").expect("Please install tmux to $PATH")) 81 | .args(args) 82 | .output()? 83 | .stdout 84 | .split(|c| *c == b'\n') 85 | .map(|bytes| String::from_utf8(bytes.to_vec()).expect("Failed to parse bytes as UTF8 string")) 86 | .collect::>(); 87 | sleep(Duration::from_millis(50)); 88 | Ok(output[0..output.len() - 1].to_vec()) 89 | } 90 | 91 | pub fn new() -> Result { 92 | let unset_cmd = "unset SKIM_DEFAULT_COMMAND SKIM_DEFAULT_OPTIONS PS1 PROMPT_COMMAND"; 93 | 94 | let shell_cmd = "bash --rcfile None"; 95 | 96 | let name = Alphanumeric.sample_string(&mut rand::rng(), 16); 97 | 98 | Self::run(&[ 99 | "new-window", 100 | "-d", 101 | "-P", 102 | "-F", 103 | "#I", 104 | "-n", 105 | &name, 106 | &format!("{}; {}", unset_cmd, shell_cmd), 107 | ])?; 108 | 109 | Self::run(&["set-window-option", "-t", &name, "pane-base-index", "0"])?; 110 | 111 | Ok(Self { 112 | window: name, 113 | tempdir: tempdir()?, 114 | }) 115 | } 116 | 117 | pub fn send_keys(&self, keys: &[Keys]) -> Result<()> { 118 | for key in keys { 119 | Self::run(&["send-keys", "-t", &self.window, &key.to_string()])?; 120 | } 121 | Ok(()) 122 | } 123 | 124 | pub fn tempfile(&self) -> Result { 125 | Ok(NamedTempFile::new_in(&self.tempdir)? 126 | .path() 127 | .to_str() 128 | .unwrap() 129 | .to_string()) 130 | } 131 | 132 | // Returns the lines in reverted order 133 | pub fn capture(&self) -> Result> { 134 | let tempfile = wait(|| { 135 | let tempfile = self.tempfile()?; 136 | Self::run(&["capture-pane", "-b", &self.window, "-t", &format!("{}.0", self.window)])?; 137 | Self::run(&["save-buffer", "-b", &self.window, &tempfile])?; 138 | Ok(tempfile) 139 | })?; 140 | 141 | let mut string_lines = String::new(); 142 | BufReader::new(File::open(tempfile)?).read_to_string(&mut string_lines)?; 143 | 144 | let str_lines = string_lines.trim(); 145 | Ok(str_lines 146 | .split("\n") 147 | .map(|s| s.to_string()) 148 | .collect::>() 149 | .into_iter() 150 | .rev() 151 | .collect()) 152 | } 153 | 154 | pub fn until(&self, pred: F) -> Result<()> 155 | where 156 | F: Fn(&[String]) -> bool, 157 | { 158 | match wait(|| { 159 | let lines = self.capture()?; 160 | if pred(&lines) { 161 | return Ok(true); 162 | } 163 | Err(Error::new(ErrorKind::Other, "pred not matched")) 164 | }) { 165 | Ok(true) => Ok(()), 166 | Ok(false) => Err(Error::new(ErrorKind::Other, self.capture()?.join("\n"))), 167 | _ => Err(Error::new(ErrorKind::TimedOut, self.capture()?.join("\n"))), 168 | } 169 | } 170 | 171 | pub fn output(&self, outfile: &str) -> Result> { 172 | wait(|| { 173 | if Path::new(outfile).exists() { 174 | Ok(()) 175 | } else { 176 | Err(Error::new(ErrorKind::NotFound, "oufile does not exist yet")) 177 | } 178 | })?; 179 | let mut string_lines = String::new(); 180 | println!("{}", Path::new(outfile).exists()); 181 | println!("Reading file {outfile}"); 182 | BufReader::new(File::open(outfile)?).read_to_string(&mut string_lines)?; 183 | 184 | let str_lines = string_lines.trim(); 185 | Ok(str_lines 186 | .split("\n") 187 | .map(|s| s.to_string()) 188 | .collect::>() 189 | .into_iter() 190 | .rev() 191 | .collect()) 192 | } 193 | 194 | pub fn start_sk(&self, stdin_cmd: Option<&str>, opts: &[&str]) -> Result { 195 | let outfile = self.tempfile()?; 196 | let sk_cmd = sk(&outfile, opts); 197 | let cmd = match stdin_cmd { 198 | Some(s) => format!("{} | {}", s, sk_cmd), 199 | None => sk_cmd, 200 | }; 201 | self.send_keys(&[Keys::Str(&cmd), Keys::Enter])?; 202 | Ok(outfile) 203 | } 204 | } 205 | 206 | impl Drop for TmuxController { 207 | fn drop(&mut self) { 208 | let _ = Self::run(&["kill-window", "-t", &self.window]); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /e2e/tests/binds.rs: -------------------------------------------------------------------------------- 1 | use e2e::Keys::*; 2 | use e2e::TmuxController; 3 | use e2e::sk; 4 | use std::io::Result; 5 | 6 | fn setup(input: &str, opts: &[&str]) -> Result { 7 | let tmux = TmuxController::new()?; 8 | let _ = tmux.start_sk(Some(&format!("echo -n -e '{input}'")), opts)?; 9 | tmux.until(|l| l[0].starts_with(">"))?; 10 | Ok(tmux) 11 | } 12 | 13 | #[test] 14 | fn bind_execute_0_results() -> Result<()> { 15 | let tmux = TmuxController::new()?; 16 | let outfile = tmux.start_sk(Some("echo -n ''"), &["--bind", "'ctrl-f:execute(echo foo{})'"])?; 17 | tmux.until(|l| l[0] == ">")?; 18 | 19 | tmux.send_keys(&[Ctrl(&Key('f')), Enter])?; 20 | tmux.until(|l| l[0] != ">")?; 21 | 22 | let output = tmux.output(&outfile)?; 23 | assert_eq!(output[0], ""); 24 | 25 | Ok(()) 26 | } 27 | 28 | #[test] 29 | fn bind_execute_0_results_noref() -> Result<()> { 30 | let tmux = TmuxController::new()?; 31 | let outfile = tmux.start_sk(Some("echo -n ''"), &["--bind", "'ctrl-f:execute(echo foo)'"])?; 32 | tmux.until(|l| l[0] == ">")?; 33 | 34 | tmux.send_keys(&[Ctrl(&Key('f')), Enter])?; 35 | tmux.until(|l| l[0] != ">")?; 36 | 37 | let output = tmux.output(&outfile)?; 38 | assert_eq!(output[0], "foo"); 39 | 40 | Ok(()) 41 | } 42 | 43 | #[test] 44 | fn bind_if_non_matched() -> Result<()> { 45 | let tmux = setup( 46 | "a\nb", 47 | &["--bind", "'enter:if-non-matched(backward-delete-char)'", "-q", "ab"], 48 | )?; 49 | 50 | tmux.until(|l| l[0].starts_with(">"))?; 51 | tmux.until(|l| l[0].starts_with("> ab"))?; 52 | 53 | tmux.send_keys(&[Enter])?; 54 | tmux.until(|l| l[0] == "> a")?; 55 | 56 | tmux.send_keys(&[Enter, Key('c')])?; 57 | tmux.until(|l| l[0].starts_with("> ac"))?; 58 | 59 | Ok(()) 60 | } 61 | 62 | #[test] 63 | fn bind_append_and_select() -> Result<()> { 64 | let tmux = setup("a\\n\\nb\\nc", &["-m", "--bind", "'ctrl-f:append-and-select'"])?; 65 | 66 | tmux.send_keys(&[Str("xyz"), Ctrl(&Key('f'))])?; 67 | tmux.until(|l| l.len() > 2 && l[2] == ">>xyz")?; 68 | 69 | Ok(()) 70 | } 71 | 72 | #[test] 73 | fn bind_reload_no_arg() -> Result<()> { 74 | let tmux = TmuxController::new()?; 75 | 76 | let outfile = tmux.tempfile()?; 77 | let sk_cmd = sk(&outfile, &["--bind", "'ctrl-a:reload'"]) 78 | .replace("SKIM_DEFAULT_COMMAND=", "SKIM_DEFAULT_COMMAND='echo hello'"); 79 | tmux.send_keys(&[Str(&sk_cmd), Enter])?; 80 | tmux.until(|l| l[0].starts_with(">"))?; 81 | 82 | tmux.send_keys(&[Ctrl(&Key('a'))])?; 83 | tmux.until(|l| l.len() > 2 && l[2] == "> hello")?; 84 | 85 | Ok(()) 86 | } 87 | 88 | #[test] 89 | fn bind_reload_cmd() -> Result<()> { 90 | let tmux = setup("a\\n\\nb\\nc", &["--bind", "'ctrl-a:reload(echo hello)'"])?; 91 | 92 | tmux.until(|l| l.len() > 2 && l[2] == "> a")?; 93 | tmux.send_keys(&[Ctrl(&Key('a'))])?; 94 | tmux.until(|l| l.len() > 2 && l[2] == "> hello")?; 95 | 96 | Ok(()) 97 | } 98 | -------------------------------------------------------------------------------- /e2e/tests/case.rs: -------------------------------------------------------------------------------- 1 | use e2e::Keys::*; 2 | use e2e::TmuxController; 3 | use std::io::Result; 4 | 5 | fn setup(case: &str) -> Result { 6 | let tmux = TmuxController::new()?; 7 | let _ = tmux.start_sk(Some(&format!("echo -n -e 'aBcDeF'")), &["--case", case])?; 8 | tmux.until(|l| l[0].starts_with(">"))?; 9 | Ok(tmux) 10 | } 11 | 12 | #[test] 13 | fn case_smart_lower() -> Result<()> { 14 | let tmux = setup("smart")?; 15 | 16 | tmux.send_keys(&[Str("abc")])?; 17 | tmux.until(|l| l.len() > 1 && l[1].trim().starts_with("1/1")) 18 | } 19 | #[test] 20 | fn case_smart_exact() -> Result<()> { 21 | let tmux = setup("smart")?; 22 | 23 | tmux.send_keys(&[Str("aBc")])?; 24 | tmux.until(|l| l.len() > 1 && l[1].trim().starts_with("1/1")) 25 | } 26 | #[test] 27 | fn case_smart_no_match() -> Result<()> { 28 | let tmux = setup("smart")?; 29 | 30 | tmux.send_keys(&[Str("Abc")])?; 31 | tmux.until(|l| l.len() > 1 && l[1].trim().starts_with("0/1")) 32 | } 33 | 34 | #[test] 35 | fn case_ignore_lower() -> Result<()> { 36 | let tmux = setup("ignore")?; 37 | 38 | tmux.send_keys(&[Str("abc")])?; 39 | tmux.until(|l| l.len() > 1 && l[1].trim().starts_with("1/1")) 40 | } 41 | #[test] 42 | fn case_ignore_exact() -> Result<()> { 43 | let tmux = setup("ignore")?; 44 | 45 | tmux.send_keys(&[Str("aBc")])?; 46 | tmux.until(|l| l.len() > 1 && l[1].trim().starts_with("1/1")) 47 | } 48 | #[test] 49 | fn case_ignore_different() -> Result<()> { 50 | let tmux = setup("ignore")?; 51 | 52 | tmux.send_keys(&[Str("Abc")])?; 53 | tmux.until(|l| l.len() > 1 && l[1].trim().starts_with("1/1")) 54 | } 55 | #[test] 56 | fn case_ignore_no_match() -> Result<()> { 57 | let tmux = setup("ignore")?; 58 | 59 | tmux.send_keys(&[Str("z")])?; 60 | tmux.until(|l| l.len() > 1 && l[1].trim().starts_with("0/1")) 61 | } 62 | 63 | #[test] 64 | fn case_respect_lower() -> Result<()> { 65 | let tmux = setup("respect")?; 66 | 67 | tmux.send_keys(&[Str("abc")])?; 68 | tmux.until(|l| l.len() > 1 && l[1].trim().starts_with("0/1")) 69 | } 70 | #[test] 71 | fn case_respect_exact() -> Result<()> { 72 | let tmux = setup("respect")?; 73 | 74 | tmux.send_keys(&[Str("aBc")])?; 75 | tmux.until(|l| l.len() > 1 && l[1].trim().starts_with("1/1")) 76 | } 77 | #[test] 78 | fn case_respect_no_match() -> Result<()> { 79 | let tmux = setup("respect")?; 80 | 81 | tmux.send_keys(&[Str("Abc")])?; 82 | tmux.until(|l| l.len() > 1 && l[1].trim().starts_with("0/1")) 83 | } 84 | -------------------------------------------------------------------------------- /e2e/tests/defaults.rs: -------------------------------------------------------------------------------- 1 | use e2e::{Keys, TmuxController, sk}; 2 | use std::io::Result; 3 | 4 | #[test] 5 | fn vanilla() -> Result<()> { 6 | let tmux = TmuxController::new()?; 7 | let _ = tmux.start_sk(Some("seq 1 100000"), &[]); 8 | tmux.until(|l| l[0].starts_with(">") && l[1].starts_with(" 100000"))?; 9 | let lines = tmux.capture()?; 10 | assert_eq!(lines[3], " 2"); 11 | assert_eq!(lines[2], "> 1"); 12 | assert!(lines[1].starts_with(" 100000/100000")); 13 | assert!(lines[1].ends_with("0/0")); 14 | assert_eq!(lines[0], ">"); 15 | 16 | Ok(()) 17 | } 18 | 19 | #[test] 20 | fn default_command() -> Result<()> { 21 | let tmux = TmuxController::new()?; 22 | 23 | let outfile = tmux.tempfile()?; 24 | let sk_cmd = sk(&outfile, &[]).replace("SKIM_DEFAULT_COMMAND=", "SKIM_DEFAULT_COMMAND='echo hello'"); 25 | tmux.send_keys(&[Keys::Str(&sk_cmd), Keys::Enter])?; 26 | tmux.until(|l| l[0].starts_with(">"))?; 27 | tmux.until(|l| l.len() > 1 && l[1].starts_with(" 1/1"))?; 28 | tmux.until(|l| l.len() > 2 && l[2] == "> hello")?; 29 | 30 | tmux.send_keys(&[Keys::Enter])?; 31 | tmux.until(|l| !l[0].starts_with(">"))?; 32 | 33 | let output = tmux.output(&outfile)?; 34 | 35 | assert_eq!(output[0], "hello"); 36 | 37 | Ok(()) 38 | } 39 | 40 | #[test] 41 | fn version_long() -> Result<()> { 42 | let tmux = TmuxController::new()?; 43 | 44 | let outfile = tmux.tempfile()?; 45 | let sk_cmd = sk(&outfile, &["--version"]); 46 | tmux.send_keys(&[Keys::Str(&sk_cmd), Keys::Enter])?; 47 | 48 | let output = tmux.output(&outfile)?; 49 | 50 | assert!(output[0].starts_with("sk ")); 51 | 52 | Ok(()) 53 | } 54 | 55 | #[test] 56 | fn version_short() -> Result<()> { 57 | let tmux = TmuxController::new()?; 58 | 59 | let outfile = tmux.tempfile()?; 60 | let sk_cmd = sk(&outfile, &["-V"]); 61 | tmux.send_keys(&[Keys::Str(&sk_cmd), Keys::Enter])?; 62 | 63 | let output = tmux.output(&outfile)?; 64 | 65 | assert!(output[0].starts_with("sk ")); 66 | 67 | Ok(()) 68 | } 69 | -------------------------------------------------------------------------------- /e2e/tests/history.rs: -------------------------------------------------------------------------------- 1 | use e2e::Keys::*; 2 | use e2e::TmuxController; 3 | use std::fs::File; 4 | use std::io::Read; 5 | use std::io::Result; 6 | use std::io::Write; 7 | use std::path::Path; 8 | 9 | #[test] 10 | fn query_history() -> Result<()> { 11 | let tmux = TmuxController::new()?; 12 | let histfile = tmux.tempfile()?; 13 | 14 | File::create(&histfile)?.write(b"a\nb\nc")?; 15 | 16 | tmux.start_sk(Some("echo -e -n 'a\\nb\\nc'"), &["--history", &histfile])?; 17 | tmux.until(|l| l[0].starts_with(">"))?; 18 | 19 | tmux.send_keys(&[Ctrl(&Key('p'))])?; 20 | tmux.until(|l| l[0].trim() == "> c")?; 21 | 22 | tmux.send_keys(&[Ctrl(&Key('p'))])?; 23 | tmux.until(|l| l[0].trim() == "> b")?; 24 | 25 | tmux.send_keys(&[Ctrl(&Key('p'))])?; 26 | tmux.until(|l| l[0].trim() == "> a")?; 27 | 28 | tmux.send_keys(&[Ctrl(&Key('n'))])?; 29 | tmux.until(|l| l[0].trim() == "> b")?; 30 | 31 | tmux.send_keys(&[Key('n')])?; 32 | tmux.until(|l| l[0].trim() == "> bn")?; 33 | 34 | tmux.send_keys(&[Enter])?; 35 | 36 | tmux.until(|_| { 37 | let mut buf = String::new(); 38 | File::open(Path::new(&histfile)) 39 | .unwrap() 40 | .read_to_string(&mut buf) 41 | .unwrap(); 42 | 43 | println!("{}", buf); 44 | buf == "a\nb\nc\nbn" 45 | })?; 46 | 47 | Ok(()) 48 | } 49 | 50 | #[test] 51 | fn cmd_history() -> Result<()> { 52 | let tmux = TmuxController::new()?; 53 | let histfile = tmux.tempfile()?; 54 | 55 | File::create(&histfile)?.write(b"a\nb\nc")?; 56 | 57 | tmux.start_sk( 58 | Some("echo -e -n 'a\\nb\\nc'"), 59 | &["-i", "-c", "'echo {}'", "--cmd-history", &histfile], 60 | )?; 61 | tmux.until(|l| l[0].starts_with("c>"))?; 62 | 63 | tmux.send_keys(&[Ctrl(&Key('p'))])?; 64 | tmux.until(|l| l[0].trim() == "c> c")?; 65 | 66 | tmux.send_keys(&[Ctrl(&Key('p'))])?; 67 | tmux.until(|l| l[0].trim() == "c> b")?; 68 | 69 | tmux.send_keys(&[Ctrl(&Key('p'))])?; 70 | tmux.until(|l| l[0].trim() == "c> a")?; 71 | 72 | tmux.send_keys(&[Ctrl(&Key('n'))])?; 73 | tmux.until(|l| l[0].trim() == "c> b")?; 74 | 75 | tmux.send_keys(&[Key('n')])?; 76 | tmux.until(|l| l[0].trim() == "c> bn")?; 77 | 78 | tmux.send_keys(&[Enter])?; 79 | 80 | tmux.until(|_| { 81 | let mut buf = String::new(); 82 | File::open(Path::new(&histfile)) 83 | .unwrap() 84 | .read_to_string(&mut buf) 85 | .unwrap(); 86 | 87 | println!("{}", buf); 88 | buf == "a\nb\nc\nbn" 89 | })?; 90 | 91 | Ok(()) 92 | } 93 | -------------------------------------------------------------------------------- /e2e/tests/issues.rs: -------------------------------------------------------------------------------- 1 | use e2e::Keys::*; 2 | use e2e::TmuxController; 3 | use std::io::Result; 4 | 5 | #[test] 6 | fn issue_359_multi_regex_unicode() -> Result<()> { 7 | let tmux = TmuxController::new()?; 8 | tmux.start_sk(Some("echo 'ああa'"), &["--regex", "-q", "'a'"])?; 9 | tmux.until(|l| l[0] == "> a")?; 10 | 11 | tmux.until(|l| l.len() > 2 && l[2] == "> ああa")?; 12 | 13 | Ok(()) 14 | } 15 | 16 | #[test] 17 | fn issue_361_literal_space_control() -> Result<()> { 18 | let tmux = TmuxController::new()?; 19 | tmux.start_sk(Some("echo -ne 'foo bar\\nfoo bar'"), &["-q", "'foo\\ bar'"])?; 20 | tmux.until(|l| l[0].starts_with(">"))?; 21 | tmux.until(|l| l.len() > 2 && l[2] == "> foo bar")?; 22 | 23 | Ok(()) 24 | } 25 | #[test] 26 | fn issue_361_literal_space_invert() -> Result<()> { 27 | let tmux = TmuxController::new()?; 28 | tmux.send_keys(&[Str("set +o histexpand"), Enter])?; 29 | tmux.start_sk(Some("echo -ne 'foo bar\\nfoo bar'"), &["-q", "'!foo\\ bar'"])?; 30 | tmux.until(|l| l[0].starts_with(">"))?; 31 | tmux.until(|l| l.len() > 2 && l[2] == "> foo bar")?; 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /e2e/tests/preview.rs: -------------------------------------------------------------------------------- 1 | use e2e::TmuxController; 2 | use std::io::Result; 3 | 4 | #[test] 5 | fn preview_preserve_quotes() -> Result<()> { 6 | let tmux = TmuxController::new()?; 7 | tmux.start_sk(Some("echo \"'\\\"ABC\\\"'\""), &["--preview", "\"echo X{}X\""])?; 8 | 9 | tmux.until(|l| l.iter().any(|s| s.contains("X'\"ABC\"'"))) 10 | } 11 | 12 | #[test] 13 | fn preview_nul_char() -> Result<()> { 14 | let tmux = TmuxController::new()?; 15 | tmux.start_sk(Some("echo -ne 'a\\0b'"), &["--preview", "'echo -en {} | hexdump -C'"])?; 16 | tmux.until(|l| l[0].starts_with(">"))?; 17 | tmux.until(|l| l.iter().any(|s| s.contains("61 00 62"))) 18 | } 19 | 20 | #[test] 21 | fn preview_offset_fixed() -> Result<()> { 22 | let tmux = TmuxController::new()?; 23 | tmux.start_sk( 24 | Some("echo -ne 'a\\nb'"), 25 | &["--preview", "'seq 1000'", "--preview-window", "left:+123"], 26 | )?; 27 | tmux.until(|l| l[l.len() - 1].starts_with("123"))?; 28 | tmux.until(|l| l[l.len() - 1].contains("123/1000"))?; 29 | 30 | Ok(()) 31 | } 32 | #[test] 33 | fn preview_offset_expr() -> Result<()> { 34 | let tmux = TmuxController::new()?; 35 | tmux.start_sk( 36 | Some("echo -ne '123 321'"), 37 | &["--preview", "'seq 1000'", "--preview-window", "left:+{2}"], 38 | )?; 39 | tmux.until(|l| l[l.len() - 1].starts_with("321"))?; 40 | tmux.until(|l| l[l.len() - 1].contains("321/1000"))?; 41 | 42 | Ok(()) 43 | } 44 | #[test] 45 | fn preview_offset_fiexd_and_expr() -> Result<()> { 46 | let tmux = TmuxController::new()?; 47 | tmux.start_sk( 48 | Some("echo -ne '123 321'"), 49 | &["--preview", "'seq 1000'", "--preview-window", "left:+{2}-2"], 50 | )?; 51 | tmux.until(|l| l[l.len() - 1].starts_with("319"))?; 52 | tmux.until(|l| l[l.len() - 1].contains("319/1000"))?; 53 | 54 | Ok(()) 55 | } 56 | -------------------------------------------------------------------------------- /e2e/tests/tiebreak.rs: -------------------------------------------------------------------------------- 1 | use e2e::Keys::*; 2 | use e2e::TmuxController; 3 | use std::io::Result; 4 | 5 | fn setup(input: &str, tiebreak: &str) -> Result { 6 | let tmux = TmuxController::new()?; 7 | tmux.start_sk( 8 | Some(&format!("echo -en '{input}'")), 9 | &[&format!("--tiebreak='{tiebreak}'")], 10 | )?; 11 | tmux.until(|l| l[0].starts_with(">"))?; 12 | Ok(tmux) 13 | } 14 | 15 | #[test] 16 | fn tiebreak_default() -> Result<()> { 17 | let tmux = setup("a\\nc\\nab\\nac\\nb", "score,begin,end")?; 18 | tmux.until(|l| l.len() > 2 && l[2].starts_with("> a"))?; 19 | tmux.send_keys(&[Key('b')])?; 20 | tmux.until(|l| l.len() > 2 && l[2].starts_with("> b")) 21 | } 22 | #[test] 23 | fn tiebreak_neg_score() -> Result<()> { 24 | let tmux = setup("a\\nb\\nc\\nab\\nac", "-score")?; 25 | tmux.until(|l| l.len() > 2 && l[2].starts_with("> a"))?; 26 | tmux.send_keys(&[Key('b')])?; 27 | tmux.until(|l| l.len() > 2 && l[2].starts_with("> ab")) 28 | } 29 | 30 | #[test] 31 | fn tiebreak_index() -> Result<()> { 32 | let tmux = setup("a\\nc\\nab\\nac\\nb", "index,score")?; 33 | tmux.until(|l| l.len() > 2 && l[2].starts_with("> a"))?; 34 | tmux.send_keys(&[Key('b')])?; 35 | tmux.until(|l| l.len() > 2 && l[2].starts_with("> ab")) 36 | } 37 | #[test] 38 | fn tiebreak_neg_index() -> Result<()> { 39 | let tmux = setup("a\\nb\\nc\\nab\\nac", "-index,score")?; 40 | tmux.until(|l| l.len() > 2 && l[2].starts_with("> a"))?; 41 | tmux.send_keys(&[Key('b')])?; 42 | tmux.until(|l| l.len() > 2 && l[2].starts_with("> ab")) 43 | } 44 | 45 | #[test] 46 | fn tiebreak_begin() -> Result<()> { 47 | let tmux = setup("aaba\\nb\\nc\\naba\\nac", "begin,score")?; 48 | tmux.until(|l| l.len() > 2 && l[2].starts_with("> aaba"))?; 49 | tmux.send_keys(&[Str("ba")])?; 50 | tmux.until(|l| l.len() > 2 && l[2].starts_with("> aba")) 51 | } 52 | #[test] 53 | fn tiebreak_neg_begin() -> Result<()> { 54 | let tmux = setup("aba\\nb\\nc\\naaba\\nac", "-begin,score")?; 55 | tmux.until(|l| l.len() > 2 && l[2].starts_with("> a"))?; 56 | tmux.send_keys(&[Key('b')])?; 57 | tmux.until(|l| l.len() > 2 && l[2].starts_with("> aaba")) 58 | } 59 | 60 | #[test] 61 | fn tiebreak_end() -> Result<()> { 62 | let tmux = setup("aaba\\nb\\nc\\naba\\nac", "end,score")?; 63 | tmux.until(|l| l.len() > 2 && l[2].starts_with("> aaba"))?; 64 | tmux.send_keys(&[Str("ba")])?; 65 | tmux.until(|l| l.len() > 2 && l[2].starts_with("> aba")) 66 | } 67 | #[test] 68 | fn tiebreak_neg_end() -> Result<()> { 69 | let tmux = setup("aba\\nb\\nc\\naaba\\nac", "-end,score")?; 70 | tmux.until(|l| l.len() > 2 && l[2].starts_with("> a"))?; 71 | tmux.send_keys(&[Str("ba")])?; 72 | tmux.until(|l| l.len() > 2 && l[2].starts_with("> aaba")) 73 | } 74 | 75 | #[test] 76 | fn tiebreak_length() -> Result<()> { 77 | let tmux = setup("aaba\\nb\\nc\\naba\\nac", "length,score")?; 78 | tmux.until(|l| l.len() > 2 && l[2].starts_with("> b"))?; 79 | tmux.send_keys(&[Str("ba")])?; 80 | tmux.until(|l| l.len() > 2 && l[2].starts_with("> aba")) 81 | } 82 | #[test] 83 | fn tiebreak_neg_length() -> Result<()> { 84 | let tmux = setup("aaba\\nb\\nc\\naba\\nac", "-length,score")?; 85 | tmux.until(|l| l.len() > 2 && l[2].starts_with("> aaba"))?; 86 | tmux.send_keys(&[Key('c')])?; 87 | tmux.until(|l| l.len() > 2 && l[2].starts_with("> ac")) 88 | } 89 | -------------------------------------------------------------------------------- /e2e/tests/tmux.rs: -------------------------------------------------------------------------------- 1 | use e2e::Keys::*; 2 | use e2e::TmuxController; 3 | use std::fs::File; 4 | use std::fs::Permissions; 5 | use std::io::Read; 6 | use std::io::Result; 7 | use std::io::Write; 8 | use std::os::unix::fs::PermissionsExt; 9 | use std::path::Path; 10 | 11 | fn setup_tmux_mock(tmux: &TmuxController) -> Result { 12 | let dir = &tmux.tempdir; 13 | let path = dir.path().join("tmux"); 14 | let mock_bin = Path::new(&path); 15 | let mut writer = File::create_new(mock_bin)?; 16 | let outfile = dir.path().join("tmux-mock-cmd"); 17 | writer.write_fmt(format_args!( 18 | "#!/bin/sh 19 | 20 | echo \"$@\" > {} 21 | ", 22 | outfile.to_str().unwrap() 23 | ))?; 24 | std::fs::set_permissions(mock_bin, Permissions::from_mode(0o777))?; 25 | tmux.send_keys(&[ 26 | Str(&format!("export PATH={}:$PATH", dir.path().to_str().unwrap())), 27 | Enter, 28 | ])?; 29 | 30 | tmux.until(|_| Path::new(&tmux.tempdir.path().join("tmux")).exists())?; 31 | 32 | Ok(outfile.to_str().unwrap().to_string()) 33 | } 34 | 35 | fn get_tmux_cmd(outfile: &str) -> Result { 36 | let mut cmd = String::new(); 37 | File::open(outfile)?.read_to_string(&mut cmd)?; 38 | Ok(cmd) 39 | } 40 | 41 | #[test] 42 | fn tmux_vanilla() -> Result<()> { 43 | let tmux = TmuxController::new()?; 44 | let outfile = setup_tmux_mock(&tmux)?; 45 | tmux.start_sk(None, &["--tmux"])?; 46 | tmux.until(|_| Path::new(&outfile).exists())?; 47 | let cmd = get_tmux_cmd(&outfile)?; 48 | assert!(cmd.starts_with("display-popup")); 49 | assert!(cmd.contains("-E")); 50 | assert!(!cmd.contains("<")); 51 | 52 | Ok(()) 53 | } 54 | #[test] 55 | fn tmux_stdin() -> Result<()> { 56 | let tmux = TmuxController::new()?; 57 | let outfile = setup_tmux_mock(&tmux)?; 58 | tmux.start_sk(Some("ls"), &["--tmux"])?; 59 | tmux.until(|_| Path::new(&outfile).exists())?; 60 | let cmd = get_tmux_cmd(&outfile)?; 61 | println!("{}", cmd); 62 | assert!(cmd.contains("<")); 63 | 64 | Ok(()) 65 | } 66 | 67 | #[test] 68 | fn tmux_quote_bash() -> Result<()> { 69 | let tmux = TmuxController::new()?; 70 | let outfile = setup_tmux_mock(&tmux)?; 71 | tmux.send_keys(&[Str("export SHELL=/bin/bash"), Enter])?; 72 | tmux.start_sk(None, &["--tmux", "--bind 'ctrl-a:reload(ls /foo*)'"])?; 73 | tmux.until(|_| Path::new(&outfile).exists())?; 74 | let cmd = get_tmux_cmd(&outfile)?; 75 | assert!(cmd.starts_with("display-popup")); 76 | assert!(cmd.contains("-E")); 77 | assert!(cmd.contains("--bind $'ctrl-a:reload(ls /foo*)'")); 78 | 79 | Ok(()) 80 | } 81 | #[test] 82 | fn tmux_quote_zsh() -> Result<()> { 83 | let tmux = TmuxController::new()?; 84 | let outfile = setup_tmux_mock(&tmux)?; 85 | tmux.send_keys(&[Str("export SHELL=/bin/zsh"), Enter])?; 86 | tmux.start_sk(None, &["--tmux", "--bind 'ctrl-a:reload(ls /foo*)'"])?; 87 | tmux.until(|_| Path::new(&outfile).exists())?; 88 | let cmd = get_tmux_cmd(&outfile)?; 89 | println!("{cmd}"); 90 | assert!(cmd.starts_with("display-popup")); 91 | assert!(cmd.contains("-E")); 92 | assert!(cmd.contains("sk --bind $'ctrl-a:reload(ls /foo*)' >")); 93 | 94 | Ok(()) 95 | } 96 | #[test] 97 | fn tmux_quote_sh() -> Result<()> { 98 | let tmux = TmuxController::new()?; 99 | let outfile = setup_tmux_mock(&tmux)?; 100 | tmux.send_keys(&[Str("export SHELL=/bin/sh"), Enter])?; 101 | tmux.start_sk(None, &["--tmux", "--bind 'ctrl-a:reload(ls /foo*)'"])?; 102 | tmux.until(|_| Path::new(&outfile).exists())?; 103 | let cmd = get_tmux_cmd(&outfile)?; 104 | assert!(cmd.starts_with("display-popup")); 105 | assert!(cmd.contains("-E")); 106 | assert!(cmd.contains("--bind ctrl-a':reload(ls /foo*)'")); 107 | 108 | Ok(()) 109 | } 110 | #[test] 111 | fn tmux_quote_fish() -> Result<()> { 112 | let tmux = TmuxController::new()?; 113 | let outfile = setup_tmux_mock(&tmux)?; 114 | tmux.send_keys(&[Str("export SHELL=/bin/sh"), Enter])?; 115 | tmux.start_sk(None, &["--tmux", "--bind 'ctrl-a:reload(ls /foo*)'"])?; 116 | tmux.until(|_| Path::new(&outfile).exists())?; 117 | let cmd = get_tmux_cmd(&outfile)?; 118 | assert!(cmd.starts_with("display-popup")); 119 | assert!(cmd.contains("-E")); 120 | assert!(cmd.contains("--bind ctrl-a':reload(ls /foo*)'")); 121 | 122 | Ok(()) 123 | } 124 | -------------------------------------------------------------------------------- /install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script trys to download the correct version of binary from github. 4 | # You can download it manually and put it(i.e. `sk`) under `bin/`. 5 | # 6 | # If you know rust or have rust installed, you can build it with 7 | # `cargo build --release` 8 | 9 | set -u 10 | 11 | cd "$(dirname "${BASH_SOURCE[0]}")" 12 | skim_base="$(pwd)" 13 | 14 | version=$(curl -s "https://api.github.com/repos/skim-rs/skim/releases/latest" | grep tag_name | grep -o "[.0-9]*") 15 | 16 | check_binary() { 17 | echo -n " - Checking skim executable ... " 18 | local output 19 | output=$("$skim_base"/bin/sk --version | grep -o "[.0-9]*" 2>&1) 20 | if [ $? -ne 0 ]; then 21 | echo "Error: $output" 22 | elif [ "$version" != "$output" ]; then 23 | echo "$output != $version" 24 | else 25 | echo "Existing version is already the latest: $output" 26 | exit 0 27 | fi 28 | rm -f "$skim_base"/bin/sk 29 | return 1 30 | } 31 | 32 | 33 | # download the latest skim 34 | download() { 35 | echo "Downloading bin/sk ..." 36 | mkdir -p "$skim_base"/bin && cd "$skim_base"/bin 37 | if [ $? -ne 0 ]; then 38 | binary_error="Failed to create bin directory" 39 | return 40 | fi 41 | 42 | check_binary 43 | 44 | local url=https://github.com/skim-rs/skim/releases/download/v$version/${1}.tgz 45 | echo "Downloading: $url" 46 | if command -v curl > /dev/null; then 47 | curl -fL $url | tar xz 48 | elif command -v wget > /dev/null; then 49 | wget -O - $url | tar xz 50 | else 51 | binary_error="curl or wget not found" 52 | return 53 | fi 54 | 55 | if [ ! -f $1 ]; then 56 | binary_error="Failed to download ${1}" 57 | return 58 | fi 59 | } 60 | 61 | archi=$(uname -sm) 62 | case "$archi" in 63 | Darwin\ x86_64) download skim-${binary_arch:-x86_64}-apple-darwin;; 64 | Darwin\ arm64) download skim-${binary_arch:-aarch64}-apple-darwin;; 65 | Linux\ x86_64) download skim-${binary_arch:-x86_64}-unknown-linux-musl;; 66 | Linux\ armv7l) download skim-${binary_arch:-armv7}-unknown-linux-musleabi;; 67 | Linux\ aarch64) download skim-${binary_arch:-aarch64}-unknown-linux-musl;; 68 | *) echo "No binaries available for '$archi' yet. Try: 'cargo install skim'";; 69 | esac 70 | 71 | echo "Done :)" 72 | -------------------------------------------------------------------------------- /man/man1/sk-tmux.1: -------------------------------------------------------------------------------- 1 | .ig 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2019 Jinzhou Zhang 5 | Copyright (c) 2017 Junegunn Choi 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | .. 25 | .TH sk-tmux 1 "Oct 2018" "sk 0.10.4" "sk-tmux - open sk in tmux split pane" 26 | 27 | .SH NAME 28 | sk-tmux - open sk in tmux split pane 29 | 30 | .SH SYNOPSIS 31 | .B sk-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [sk OPTIONS] 32 | 33 | .SH DESCRIPTION 34 | sk-tmux is a wrapper script for sk that opens sk in a tmux split pane. It is 35 | designed to work just like sk except that it does not take up the whole 36 | screen. You can safely use sk-tmux instead of sk in your scripts as the extra 37 | options will be silently ignored if you're not on tmux. 38 | 39 | .SH OPTIONS 40 | .SS Layout 41 | 42 | (default: \fB-d 50%\fR) 43 | 44 | .TP 45 | .B "-u [height[%]]" 46 | Split above (up) 47 | .TP 48 | .B "-d [height[%]]" 49 | Split below (down) 50 | .TP 51 | .B "-l [width[%]]" 52 | Split left 53 | .TP 54 | .B "-r [width[%]]" 55 | Split right 56 | -------------------------------------------------------------------------------- /shell/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jinzhou Zhang 4 | Copyright (c) 2016 Junegunn Choi 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /shell/README.md: -------------------------------------------------------------------------------- 1 | This directory contains the shell bindings for skim. 2 | 3 | Note that the content in this directory is copied from 4 | [fzf](https://github.com/junegunn/fzf/tree/master/shell) and modified to fit 5 | skim. 6 | 7 | The scripts in this directory will not be maintained by skim. 8 | 9 | But you are welcome to submit PR for update the scripts to the fzf's latest 10 | scripts. Thanks! 11 | -------------------------------------------------------------------------------- /shell/completion.fish: -------------------------------------------------------------------------------- 1 | complete -c sk -s t -l tiebreak -d 'Comma-separated list of sort criteria to apply when the scores are tied' -r -f -a "score\t'' 2 | -score\t'' 3 | begin\t'' 4 | -begin\t'' 5 | end\t'' 6 | -end\t'' 7 | length\t'' 8 | -length\t'' 9 | index\t'' 10 | -index\t''" 11 | complete -c sk -s n -l nth -d 'Fields to be matched' -r 12 | complete -c sk -l with-nth -d 'Fields to be transformed' -r 13 | complete -c sk -s d -l delimiter -d 'Delimiter between fields' -r 14 | complete -c sk -l algo -d 'Fuzzy matching algorithm' -r -f -a "skim_v1\t'' 15 | skim_v2\t'' 16 | clangd\t''" 17 | complete -c sk -l case -d 'Case sensitivity' -r -f -a "respect\t'' 18 | ignore\t'' 19 | smart\t''" 20 | complete -c sk -s b -l bind -d 'Comma separated list of bindings' -r 21 | complete -c sk -s c -l cmd -d 'Command to invoke dynamically in interactive mode' -r 22 | complete -c sk -s I -d 'Replace replstr with the selected item in commands' -r 23 | complete -c sk -l color -d 'Set color theme' -r 24 | complete -c sk -l skip-to-pattern -d 'Show the matched pattern at the line start' -r 25 | complete -c sk -l layout -d 'Set layout' -r -f -a "default\t'' 26 | reverse\t'' 27 | reverse-list\t''" 28 | complete -c sk -l height -d 'Height of skim\'s window' -r 29 | complete -c sk -l min-height -d 'Minimum height of skim\'s window' -r 30 | complete -c sk -l margin -d 'Screen margin' -r 31 | complete -c sk -s p -l prompt -d 'Set prompt' -r 32 | complete -c sk -l cmd-prompt -d 'Set prompt in command mode' -r 33 | complete -c sk -l tabstop -d 'Number of spaces that make up a tab' -r 34 | complete -c sk -l info -d 'Set matching result count display position' -r -f -a "default\t'' 35 | inline\t'' 36 | hidden\t''" 37 | complete -c sk -l header -d 'Set header, displayed next to the info' -r 38 | complete -c sk -l header-lines -d 'Number of lines of the input treated as header' -r 39 | complete -c sk -l history -d 'History file' -r 40 | complete -c sk -l history-size -d 'Maximum number of query history entries to keep' -r 41 | complete -c sk -l cmd-history -d 'Command history file' -r 42 | complete -c sk -l cmd-history-size -d 'Maximum number of query history entries to keep' -r 43 | complete -c sk -l preview -d 'Preview command' -r 44 | complete -c sk -l preview-window -d 'Preview window layout' -r 45 | complete -c sk -s q -l query -d 'Initial query' -r 46 | complete -c sk -l cmd-query -d 'Initial query in interactive mode' -r 47 | complete -c sk -l expect -d '[Deprecated: Use --bind=:accept() instead] Comma separated list of keys used to complete skim' -r 48 | complete -c sk -l pre-select-n -d 'Pre-select the first n items in multi-selection mode' -r 49 | complete -c sk -l pre-select-pat -d 'Pre-select the matched items in multi-selection mode' -r 50 | complete -c sk -l pre-select-items -d 'Pre-select the items separated by newline character' -r 51 | complete -c sk -l pre-select-file -d 'Pre-select the items read from this file' -r 52 | complete -c sk -s f -l filter -d 'Query for filter mode' -r 53 | complete -c sk -l shell -d 'Generate shell completion script' -r -f -a "bash\t'' 54 | elvish\t'' 55 | fish\t'' 56 | powershell\t'' 57 | zsh\t''" 58 | complete -c sk -l tmux -d 'Run in a tmux popup' -r 59 | complete -c sk -l hscroll-off -d 'Reserved for later use' -r 60 | complete -c sk -l jump-labels -d 'Reserved for later use' -r 61 | complete -c sk -l tac -d 'Show results in reverse order' 62 | complete -c sk -l no-sort -d 'Do not sort the results' 63 | complete -c sk -s e -l exact -d 'Run in exact mode' 64 | complete -c sk -l regex -d 'Start in regex mode instead of fuzzy-match' 65 | complete -c sk -s m -l multi -d 'Enable multiple selection' 66 | complete -c sk -l no-multi -d 'Disable multiple selection' 67 | complete -c sk -l no-mouse -d 'Disable mouse' 68 | complete -c sk -s i -l interactive -d 'Run in interactive mode' 69 | complete -c sk -l no-hscroll -d 'Disable horizontal scroll' 70 | complete -c sk -l keep-right -d 'Keep the right end of the line visible on overflow' 71 | complete -c sk -l no-clear-if-empty -d 'Do not clear previous line if the command returns an empty result' 72 | complete -c sk -l no-clear-start -d 'Do not clear items on start' 73 | complete -c sk -l no-clear -d 'Do not clear screen on exit' 74 | complete -c sk -l show-cmd-error -d 'Show error message if command fails' 75 | complete -c sk -l reverse -d 'Shorthand for reverse layout' 76 | complete -c sk -l no-height -d 'Disable height feature' 77 | complete -c sk -l ansi -d 'Parse ANSI color codes in input strings' 78 | complete -c sk -l no-info -d 'Alias for --info=hidden' 79 | complete -c sk -l inline-info -d 'Alias for --info=inline' 80 | complete -c sk -l read0 -d 'Read input delimited by ASCII NUL(\\0) characters' 81 | complete -c sk -l print0 -d 'Print output delimited by ASCII NUL(\\0) characters' 82 | complete -c sk -l print-query -d 'Print the query as the first line' 83 | complete -c sk -l print-cmd -d 'Print the command as the first line (after print-query)' 84 | complete -c sk -l print-score -d 'Print the command as the first line (after print-cmd)' 85 | complete -c sk -s 1 -l select-1 -d 'Automatically select the match if there is only one' 86 | complete -c sk -s 0 -l exit-0 -d 'Automatically exit when no match is left' 87 | complete -c sk -l sync -d 'Synchronous search for multi-staged filtering' 88 | complete -c sk -s x -l extended -d 'Reserved for later use' 89 | complete -c sk -l literal -d 'Reserved for later use' 90 | complete -c sk -l cycle -d 'Reserved for later use' 91 | complete -c sk -l filepath-word -d 'Reserved for later use' 92 | complete -c sk -l border -d 'Reserved for later use' 93 | complete -c sk -l no-bold -d 'Reserved for later use' 94 | complete -c sk -l pointer -d 'Reserved for later use' 95 | complete -c sk -l marker -d 'Reserved for later use' 96 | complete -c sk -l phony -d 'Reserved for later use' 97 | complete -c sk -s h -l help -d 'Print help (see more with \'--help\')' 98 | complete -c sk -s V -l version -d 'Print version' 99 | -------------------------------------------------------------------------------- /shell/completion.zsh: -------------------------------------------------------------------------------- 1 | #compdef sk 2 | 3 | autoload -U is-at-least 4 | 5 | _sk() { 6 | typeset -A opt_args 7 | typeset -a _arguments_options 8 | local ret=1 9 | 10 | if is-at-least 5.2; then 11 | _arguments_options=(-s -S -C) 12 | else 13 | _arguments_options=(-s -C) 14 | fi 15 | 16 | local context curcontext="$curcontext" state line 17 | _arguments "${_arguments_options[@]}" : \ 18 | '*-t+[Comma-separated list of sort criteria to apply when the scores are tied]:TIEBREAK:(score -score begin -begin end -end length -length index -index)' \ 19 | '*--tiebreak=[Comma-separated list of sort criteria to apply when the scores are tied]:TIEBREAK:(score -score begin -begin end -end length -length index -index)' \ 20 | '*-n+[Fields to be matched]:NTH:_default' \ 21 | '*--nth=[Fields to be matched]:NTH:_default' \ 22 | '*--with-nth=[Fields to be transformed]:WITH_NTH:_default' \ 23 | '-d+[Delimiter between fields]:DELIMITER:_default' \ 24 | '--delimiter=[Delimiter between fields]:DELIMITER:_default' \ 25 | '--algo=[Fuzzy matching algorithm]:ALGORITHM:(skim_v1 skim_v2 clangd)' \ 26 | '--case=[Case sensitivity]:CASE:(respect ignore smart)' \ 27 | '*-b+[Comma separated list of bindings]:BIND:_default' \ 28 | '*--bind=[Comma separated list of bindings]:BIND:_default' \ 29 | '-c+[Command to invoke dynamically in interactive mode]:CMD:_default' \ 30 | '--cmd=[Command to invoke dynamically in interactive mode]:CMD:_default' \ 31 | '-I+[Replace replstr with the selected item in commands]:REPLSTR:_default' \ 32 | '--color=[Set color theme]:COLOR:_default' \ 33 | '--skip-to-pattern=[Show the matched pattern at the line start]:SKIP_TO_PATTERN:_default' \ 34 | '--layout=[Set layout]:LAYOUT:(default reverse reverse-list)' \ 35 | '--height=[Height of skim'\''s window]:HEIGHT:_default' \ 36 | '--min-height=[Minimum height of skim'\''s window]:MIN_HEIGHT:_default' \ 37 | '--margin=[Screen margin]:MARGIN:_default' \ 38 | '-p+[Set prompt]:PROMPT:_default' \ 39 | '--prompt=[Set prompt]:PROMPT:_default' \ 40 | '--cmd-prompt=[Set prompt in command mode]:CMD_PROMPT:_default' \ 41 | '--tabstop=[Number of spaces that make up a tab]:TABSTOP:_default' \ 42 | '--info=[Set matching result count display position]:INFO:(default inline hidden)' \ 43 | '--header=[Set header, displayed next to the info]:HEADER:_default' \ 44 | '--header-lines=[Number of lines of the input treated as header]:HEADER_LINES:_default' \ 45 | '--history=[History file]:HISTORY_FILE:_default' \ 46 | '--history-size=[Maximum number of query history entries to keep]:HISTORY_SIZE:_default' \ 47 | '--cmd-history=[Command history file]:CMD_HISTORY_FILE:_default' \ 48 | '--cmd-history-size=[Maximum number of query history entries to keep]:CMD_HISTORY_SIZE:_default' \ 49 | '--preview=[Preview command]:PREVIEW:_default' \ 50 | '--preview-window=[Preview window layout]:PREVIEW_WINDOW:_default' \ 51 | '-q+[Initial query]:QUERY:_default' \ 52 | '--query=[Initial query]:QUERY:_default' \ 53 | '--cmd-query=[Initial query in interactive mode]:CMD_QUERY:_default' \ 54 | '*--expect=[\[Deprecated\: Use --bind=\:accept() instead\] Comma separated list of keys used to complete skim]:EXPECT:_default' \ 55 | '--pre-select-n=[Pre-select the first n items in multi-selection mode]:PRE_SELECT_N:_default' \ 56 | '--pre-select-pat=[Pre-select the matched items in multi-selection mode]:PRE_SELECT_PAT:_default' \ 57 | '--pre-select-items=[Pre-select the items separated by newline character]:PRE_SELECT_ITEMS:_default' \ 58 | '--pre-select-file=[Pre-select the items read from this file]:PRE_SELECT_FILE:_default' \ 59 | '-f+[Query for filter mode]:FILTER:_default' \ 60 | '--filter=[Query for filter mode]:FILTER:_default' \ 61 | '--shell=[Generate shell completion script]:SHELL:(bash elvish fish powershell zsh)' \ 62 | '--tmux=[Run in a tmux popup]' \ 63 | '--hscroll-off=[Reserved for later use]:HSCROLL_OFF:_default' \ 64 | '--jump-labels=[Reserved for later use]:JUMP_LABELS:_default' \ 65 | '--tac[Show results in reverse order]' \ 66 | '--no-sort[Do not sort the results]' \ 67 | '-e[Run in exact mode]' \ 68 | '--exact[Run in exact mode]' \ 69 | '--regex[Start in regex mode instead of fuzzy-match]' \ 70 | '-m[Enable multiple selection]' \ 71 | '--multi[Enable multiple selection]' \ 72 | '(-m --multi)--no-multi[Disable multiple selection]' \ 73 | '--no-mouse[Disable mouse]' \ 74 | '-i[Run in interactive mode]' \ 75 | '--interactive[Run in interactive mode]' \ 76 | '--no-hscroll[Disable horizontal scroll]' \ 77 | '--keep-right[Keep the right end of the line visible on overflow]' \ 78 | '--no-clear-if-empty[Do not clear previous line if the command returns an empty result]' \ 79 | '--no-clear-start[Do not clear items on start]' \ 80 | '--no-clear[Do not clear screen on exit]' \ 81 | '--show-cmd-error[Show error message if command fails]' \ 82 | '--reverse[Shorthand for reverse layout]' \ 83 | '--no-height[Disable height feature]' \ 84 | '--ansi[Parse ANSI color codes in input strings]' \ 85 | '--no-info[Alias for --info=hidden]' \ 86 | '--inline-info[Alias for --info=inline]' \ 87 | '--read0[Read input delimited by ASCII NUL(\\0) characters]' \ 88 | '--print0[Print output delimited by ASCII NUL(\\0) characters]' \ 89 | '--print-query[Print the query as the first line]' \ 90 | '--print-cmd[Print the command as the first line (after print-query)]' \ 91 | '--print-score[Print the command as the first line (after print-cmd)]' \ 92 | '-1[Automatically select the match if there is only one]' \ 93 | '--select-1[Automatically select the match if there is only one]' \ 94 | '-0[Automatically exit when no match is left]' \ 95 | '--exit-0[Automatically exit when no match is left]' \ 96 | '--sync[Synchronous search for multi-staged filtering]' \ 97 | '-x[Reserved for later use]' \ 98 | '--extended[Reserved for later use]' \ 99 | '--literal[Reserved for later use]' \ 100 | '--cycle[Reserved for later use]' \ 101 | '--filepath-word[Reserved for later use]' \ 102 | '--border[Reserved for later use]' \ 103 | '--no-bold[Reserved for later use]' \ 104 | '--pointer[Reserved for later use]' \ 105 | '--marker[Reserved for later use]' \ 106 | '--phony[Reserved for later use]' \ 107 | '-h[Print help (see more with '\''--help'\'')]' \ 108 | '--help[Print help (see more with '\''--help'\'')]' \ 109 | '-V[Print version]' \ 110 | '--version[Print version]' \ 111 | && ret=0 112 | } 113 | 114 | (( $+functions[_sk_commands] )) || 115 | _sk_commands() { 116 | local commands; commands=() 117 | _describe -t commands 'sk commands' commands "$@" 118 | } 119 | 120 | if [ "$funcstack[1]" = "_sk" ]; then 121 | _sk "$@" 122 | else 123 | compdef _sk sk 124 | fi 125 | -------------------------------------------------------------------------------- /shell/key-bindings.fish: -------------------------------------------------------------------------------- 1 | #!/bin/fish 2 | # - $SKIM_TMUX_OPTS 3 | # - $SKIM_CTRL_T_COMMAND 4 | # - $SKIM_CTRL_T_OPTS 5 | # - $SKIM_CTRL_R_OPTS 6 | # - $SKIM_ALT_C_COMMAND 7 | # - $SKIM_ALT_C_OPTS 8 | # - $SKIM_COMPLETION_TRIGGER (default: '**') 9 | # - $SKIM_COMPLETION_OPTS (default: empty) 10 | 11 | # Key bindings 12 | # ------------ 13 | function skim_key_bindings 14 | 15 | # Store current token in $dir as root for the 'find' command 16 | function skim-file-widget -d "List files and folders" 17 | set -l commandline (__skim_parse_commandline) 18 | set -l dir $commandline[1] 19 | set -l skim_query $commandline[2] 20 | 21 | # "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not 22 | # $dir itself, even if hidden. 23 | test -n "$SKIM_CTRL_T_COMMAND"; or set -l SKIM_CTRL_T_COMMAND " 24 | command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ 25 | -o -type f -print \ 26 | -o -type d -print \ 27 | -o -type l -print 2> /dev/null | sed 's@^\./@@'" 28 | 29 | begin 30 | set -lx SKIM_DEFAULT_OPTIONS "--reverse $SKIM_DEFAULT_OPTIONS $SKIM_CTRL_T_OPTS" 31 | eval "$SKIM_CTRL_T_COMMAND | "(__skimcmd)' -m --query "'$skim_query'"' | while read -l r; set result $result $r; end 32 | end 33 | if [ -z "$result" ] 34 | commandline -f repaint 35 | return 36 | else 37 | # Remove last token from commandline. 38 | commandline -t "" 39 | end 40 | for i in $result 41 | commandline -it -- (string escape $i) 42 | commandline -it -- ' ' 43 | end 44 | commandline -f repaint 45 | end 46 | 47 | function skim-history-widget -d "Show command history" 48 | begin 49 | set -lx SKIM_DEFAULT_OPTIONS "$SKIM_DEFAULT_OPTIONS --bind=ctrl-r:toggle-sort $SKIM_CTRL_R_OPTS --no-multi" 50 | 51 | set -l FISH_MAJOR (echo $version | cut -f1 -d.) 52 | set -l FISH_MINOR (echo $version | cut -f2 -d.) 53 | 54 | # history's -z flag is needed for multi-line support. 55 | # history's -z flag was added in fish 2.4.0, so don't use it for versions 56 | # before 2.4.0. 57 | if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ]; 58 | history -z | eval (__skimcmd) --read0 --print0 -q '(commandline)' | read -lz result 59 | and commandline -- $result 60 | else 61 | history | eval (__skimcmd) -q '(commandline)' | read -l result 62 | and commandline -- $result 63 | end 64 | end 65 | commandline -f repaint 66 | end 67 | 68 | function skim-cd-widget -d "Change directory" 69 | set -l commandline (__skim_parse_commandline) 70 | set -l dir $commandline[1] 71 | set -l skim_query $commandline[2] 72 | 73 | test -n "$SKIM_ALT_C_COMMAND"; or set -l SKIM_ALT_C_COMMAND " 74 | command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ 75 | -o -type d -print 2> /dev/null | sed 's@^\./@@'" 76 | begin 77 | set -lx SKIM_DEFAULT_OPTIONS "--reverse $SKIM_DEFAULT_OPTIONS $SKIM_ALT_C_OPTS" 78 | eval "$SKIM_ALT_C_COMMAND | "(__skimcmd)' --no-multi --query "'$skim_query'"' | read -l result 79 | 80 | if [ -n "$result" ] 81 | cd $result 82 | 83 | # Remove last token from commandline. 84 | commandline -t "" 85 | end 86 | end 87 | 88 | commandline -f repaint 89 | end 90 | 91 | function __skimcmd 92 | test -n "$SKIM_TMUX"; or set SKIM_TMUX 0 93 | test -n "$SKIM_TMUX_HEIGHT"; or set SKIM_TMUX_HEIGHT 40% 94 | if [ -n "$SKIM_TMUX_OPTS" ] 95 | echo "sk --tmux=$SKIM_TMUX_OPTS " 96 | else if [ $SKIM_TMUX -eq 1 ] 97 | echo "sk --tmux=center,$SKIM_TMUX_HEIGHT" 98 | else 99 | echo "sk" 100 | end 101 | end 102 | 103 | bind \ct skim-file-widget 104 | bind \cr skim-history-widget 105 | bind \ec skim-cd-widget 106 | 107 | if bind -M insert > /dev/null 2>&1 108 | bind -M insert \ct skim-file-widget 109 | bind -M insert \cr skim-history-widget 110 | bind -M insert \ec skim-cd-widget 111 | end 112 | 113 | function __skim_parse_commandline -d 'Parse the current command line token and return split of existing filepath and rest of token' 114 | # eval is used to do shell expansion on paths 115 | set -l commandline (eval "printf '%s' "(commandline -t)) 116 | 117 | if [ -z $commandline ] 118 | # Default to current directory with no --query 119 | set dir '.' 120 | set skim_query '' 121 | else 122 | set dir (__skim_get_dir $commandline) 123 | 124 | if [ "$dir" = "." -a (string sub -l 1 -- $commandline) != '.' ] 125 | # if $dir is "." but commandline is not a relative path, this means no file path found 126 | set skim_query $commandline 127 | else 128 | # Also remove trailing slash after dir, to "split" input properly 129 | set skim_query (string replace -r "^$dir/?" -- '' "$commandline") 130 | end 131 | end 132 | 133 | echo $dir 134 | echo $skim_query 135 | end 136 | 137 | function __skim_get_dir -d 'Find the longest existing filepath from input string' 138 | set dir $argv 139 | 140 | # Strip all trailing slashes. Ignore if $dir is root dir (/) 141 | if [ (string length -- $dir) -gt 1 ] 142 | set dir (string replace -r '/*$' -- '' $dir) 143 | end 144 | 145 | # Iteratively check if dir exists and strip tail end of path 146 | while [ ! -d "$dir" ] 147 | # If path is absolute, this can keep going until ends up at / 148 | # If path is relative, this can keep going until entire input is consumed, dirname returns "." 149 | set dir (dirname -- "$dir") 150 | end 151 | 152 | echo $dir 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /shell/version.txt: -------------------------------------------------------------------------------- 1 | 1.16.2 2 | -------------------------------------------------------------------------------- /skim-common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "skim-common" 3 | version = "0.1.2" 4 | edition = "2024" 5 | authors = ["Zhang Jinzhou ", "Loric Andre"] 6 | description = "Fuzzy Finder in rust!" 7 | documentation = "https://docs.rs/skim" 8 | homepage = "https://github.com/skim-rs/skim" 9 | repository = "https://github.com/skim-rs/skim" 10 | readme = "../README.md" 11 | keywords = ["util"] 12 | license = "MIT" 13 | 14 | [dependencies] 15 | -------------------------------------------------------------------------------- /skim-common/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod spinlock; 2 | -------------------------------------------------------------------------------- /skim-common/src/spinlock.rs: -------------------------------------------------------------------------------- 1 | //! SpinLock implemented using AtomicBool 2 | //! Just like Mutex except: 3 | //! 4 | //! 1. It uses CAS for locking, more efficient in low contention 5 | //! 2. Use `.lock()` instead of `.lock().unwrap()` to retrieve the guard. 6 | //! 3. It doesn't handle poison so data is still available on thread panic. 7 | use std::cell::UnsafeCell; 8 | use std::ops::Deref; 9 | use std::ops::DerefMut; 10 | use std::sync::atomic::AtomicBool; 11 | use std::sync::atomic::Ordering; 12 | 13 | pub struct SpinLock { 14 | locked: AtomicBool, 15 | data: UnsafeCell, 16 | } 17 | 18 | unsafe impl Send for SpinLock {} 19 | unsafe impl Sync for SpinLock {} 20 | 21 | pub struct SpinLockGuard<'a, T: ?Sized + 'a> { 22 | // funny underscores due to how Deref/DerefMut currently work (they 23 | // disregard field privacy). 24 | __lock: &'a SpinLock, 25 | } 26 | 27 | impl<'a, T: ?Sized + 'a> SpinLockGuard<'a, T> { 28 | pub fn new(pool: &'a SpinLock) -> SpinLockGuard<'a, T> { 29 | Self { __lock: pool } 30 | } 31 | } 32 | 33 | unsafe impl Sync for SpinLockGuard<'_, T> {} 34 | 35 | impl SpinLock { 36 | pub fn new(t: T) -> SpinLock { 37 | Self { 38 | locked: AtomicBool::new(false), 39 | data: UnsafeCell::new(t), 40 | } 41 | } 42 | } 43 | 44 | impl SpinLock { 45 | pub fn lock(&self) -> SpinLockGuard { 46 | while self 47 | .locked 48 | .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) 49 | .is_err() 50 | {} 51 | SpinLockGuard::new(self) 52 | } 53 | } 54 | 55 | impl Deref for SpinLockGuard<'_, T> { 56 | type Target = T; 57 | 58 | fn deref(&self) -> &T { 59 | unsafe { &*self.__lock.data.get() } 60 | } 61 | } 62 | 63 | impl DerefMut for SpinLockGuard<'_, T> { 64 | fn deref_mut(&mut self) -> &mut T { 65 | unsafe { &mut *self.__lock.data.get() } 66 | } 67 | } 68 | 69 | impl Drop for SpinLockGuard<'_, T> { 70 | #[inline] 71 | fn drop(&mut self) { 72 | while self 73 | .__lock 74 | .locked 75 | .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst) 76 | .is_err() 77 | {} 78 | } 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use super::*; 84 | use std::sync::Arc; 85 | use std::sync::mpsc::channel; 86 | use std::thread; 87 | 88 | #[test] 89 | fn smoke() { 90 | let m = SpinLock::new(()); 91 | drop(m.lock()); 92 | drop(m.lock()); 93 | } 94 | 95 | #[test] 96 | fn lots_and_lots() { 97 | const J: u32 = 1000; 98 | const K: u32 = 3; 99 | 100 | let m = Arc::new(SpinLock::new(0)); 101 | 102 | fn inc(m: &SpinLock) { 103 | for _ in 0..J { 104 | *m.lock() += 1; 105 | } 106 | } 107 | 108 | let (tx, rx) = channel(); 109 | for _ in 0..K { 110 | let tx2 = tx.clone(); 111 | let m2 = m.clone(); 112 | thread::spawn(move || { 113 | inc(&m2); 114 | tx2.send(()).unwrap(); 115 | }); 116 | let tx2 = tx.clone(); 117 | let m2 = m.clone(); 118 | thread::spawn(move || { 119 | inc(&m2); 120 | tx2.send(()).unwrap(); 121 | }); 122 | } 123 | 124 | drop(tx); 125 | for _ in 0..2 * K { 126 | rx.recv().unwrap(); 127 | } 128 | assert_eq!(*m.lock(), J * K * 2); 129 | } 130 | 131 | #[test] 132 | fn test_mutex_unsized() { 133 | let mutex: &SpinLock<[i32]> = &SpinLock::new([1, 2, 3]); 134 | { 135 | let b = &mut *mutex.lock(); 136 | b[0] = 4; 137 | b[2] = 5; 138 | } 139 | let comp: &[i32] = &[4, 2, 5]; 140 | assert_eq!(&*mutex.lock(), comp); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /skim-tuikit/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | .idea 4 | -------------------------------------------------------------------------------- /skim-tuikit/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "skim-tuikit" 3 | version = "0.6.2" 4 | authors = ["Jinzhou Zhang "] 5 | description = "Toolkit for writing TUI applications" 6 | documentation = "https://docs.rs/skim-tuikit" 7 | homepage = "https://github.com/skim-rs/skim" 8 | repository = "https://github.com/skim-rs/skim" 9 | readme = "README.md" 10 | keywords = ["tui", "terminal", "tty", "color"] 11 | license = "MIT" 12 | edition = "2024" 13 | 14 | [dependencies] 15 | bitflags = { workspace = true } 16 | skim-common = { path = "../skim-common/", version = "0.1.2" } 17 | lazy_static = { workspace = true } 18 | log = { workspace = true } 19 | nix = { workspace = true, default-features = false, features = ["fs", "poll", "signal", "term"] } 20 | term = { workspace = true } 21 | unicode-width = { workspace = true } 22 | 23 | [dev-dependencies] 24 | env_logger = { workspace = true } 25 | -------------------------------------------------------------------------------- /skim-tuikit/README.md: -------------------------------------------------------------------------------- 1 | [![Crates.io](https://img.shields.io/crates/v/skim-tuikit.svg)](https://crates.io/crates/skim-tuikit) 2 | 3 | ## Tuikit 4 | 5 | Tuikit is a TUI library for writing terminal UI applications. Highlights: 6 | 7 | - Thread safe. 8 | - Support non-fullscreen mode as well as fullscreen mode. 9 | - Support `Alt` keys, mouse events, etc. 10 | - Buffering for efficient rendering. 11 | 12 | Tuikit is modeld after [termbox](https://github.com/nsf/termbox) which views the 13 | terminal as a table of fixed-size cells and input being a stream of structured 14 | messages. 15 | 16 | **WARNING**: The library is not stable yet, the API might change. 17 | 18 | ## Usage 19 | 20 | In your `Cargo.toml` add the following: 21 | 22 | ```toml 23 | [dependencies] 24 | skim-tuikit = "*" 25 | ``` 26 | 27 | 28 | Here is an example (could also be run by `cargo run --example hello-world`): 29 | 30 | ```rust 31 | use skim_tuikit::prelude::*; 32 | use std::cmp::{min, max}; 33 | 34 | fn main() { 35 | let term: Term<()> = Term::with_height(TermHeight::Percent(30)).unwrap(); 36 | let mut row = 1; 37 | let mut col = 0; 38 | 39 | let _ = term.print(0, 0, "press arrow key to move the text, (q) to quit"); 40 | let _ = term.present(); 41 | 42 | while let Ok(ev) = term.poll_event() { 43 | let _ = term.clear(); 44 | let _ = term.print(0, 0, "press arrow key to move the text, (q) to quit"); 45 | 46 | let (width, height) = term.term_size().unwrap(); 47 | match ev { 48 | Event::Key(Key::ESC) | Event::Key(Key::Char('q')) => break, 49 | Event::Key(Key::Up) => row = max(row-1, 1), 50 | Event::Key(Key::Down) => row = min(row+1, height-1), 51 | Event::Key(Key::Left) => col = max(col, 1)-1, 52 | Event::Key(Key::Right) => col = min(col+1, width-1), 53 | _ => {} 54 | } 55 | 56 | let attr = Attr{ fg: Color::RED, ..Attr::default() }; 57 | let _ = term.print_with_attr(row, col, "Hello World! 你好!今日は。", attr); 58 | let _ = term.set_cursor(row, col); 59 | let _ = term.present(); 60 | } 61 | } 62 | ``` 63 | 64 | ## Layout 65 | 66 | `tuikit` provides `HSplit`, `VSplit` and `Win` for managing layouts: 67 | 68 | 1. `HSplit` allow you to split area horizontally into pieces. 69 | 2. `VSplit` works just like `HSplit` but splits vertically. 70 | 3. `Win` do not split, it could have margin, padding and border. 71 | 72 | For example: 73 | 74 | ```rust 75 | use skim_tuikit::prelude::*; 76 | 77 | struct Model(String); 78 | 79 | impl Draw for Model { 80 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 81 | let (width, height) = canvas.size()?; 82 | let message_width = self.0.len(); 83 | let left = (width - message_width) / 2; 84 | let top = height / 2; 85 | let _ = canvas.print(top, left, &self.0); 86 | Ok(()) 87 | } 88 | } 89 | 90 | impl Widget for Model{} 91 | 92 | fn main() { 93 | let term: Term<()> = Term::with_height(TermHeight::Percent(50)).unwrap(); 94 | let model = Model("middle!".to_string()); 95 | 96 | while let Ok(ev) = term.poll_event() { 97 | if let Event::Key(Key::Char('q')) = ev { 98 | break; 99 | } 100 | let _ = term.print(0, 0, "press 'q' to exit"); 101 | 102 | let hsplit = HSplit::default() 103 | .split( 104 | VSplit::default() 105 | .basis(Size::Percent(30)) 106 | .split(Win::new(&model).border(true).basis(Size::Percent(30))) 107 | .split(Win::new(&model).border(true).basis(Size::Percent(30))) 108 | ) 109 | .split(Win::new(&model).border(true)); 110 | 111 | let _ = term.draw(&hsplit); 112 | let _ = term.present(); 113 | } 114 | } 115 | ``` 116 | 117 | The split algorithm is simple: 118 | 119 | 1. Both `HSplit` and `VSplit` will take several `Split` where a `Split` would 120 | contains: 121 | 1. basis, the original size 122 | 2. grow, the factor to grow if there is still enough room 123 | 3. shrink, the factor to shrink if there is not enough room 124 | 2. `HSplit/VSplit` will count the total width/height(basis) of the split items 125 | 3. Judge if the current width/height is enough or not for the split items 126 | 4. shrink/grow the split items according to their grow/shrink: `factor / sum(factors)` 127 | 5. If still not enough room, the last one(s) would be set width/height 0 128 | 129 | ## References 130 | 131 | `Tuikit` borrows ideas from lots of other projects: 132 | 133 | - [rustyline](https://github.com/kkawakam/rustyline) Readline Implementation in Rust. 134 | - How to enter the raw mode. 135 | - Part of the keycode parsing logic. 136 | - [termion](https://gitlab.redox-os.org/redox-os/termion) A bindless library for controlling terminals/TTY. 137 | - How to parse mouse events. 138 | - How to enter raw mode. 139 | - [rustbox](https://github.com/gchp/rustbox) and [termbox](https://github.com/nsf/termbox) 140 | - The idea of viewing terminal as table of fixed cells. 141 | - [termfest](https://github.com/agatan/termfest) Easy TUI library written in Rust 142 | - The buffering idea. 143 | -------------------------------------------------------------------------------- /skim-tuikit/examples/256color.rs: -------------------------------------------------------------------------------- 1 | use skim_tuikit::attr::Color; 2 | use skim_tuikit::output::Output; 3 | use std::io; 4 | 5 | fn main() { 6 | let mut output = Output::new(Box::new(io::stdout())).unwrap(); 7 | 8 | for fg in 0..=255 { 9 | output.set_fg(Color::AnsiValue(fg)); 10 | output.write(format!("{:5}", fg).as_str()); 11 | if fg % 16 == 15 { 12 | output.reset_attributes(); 13 | output.write("\n"); 14 | output.flush() 15 | } 16 | } 17 | 18 | output.reset_attributes(); 19 | 20 | for bg in 0..=255 { 21 | output.set_bg(Color::AnsiValue(bg)); 22 | output.write(format!("{:5}", bg).as_str()); 23 | if bg % 16 == 15 { 24 | output.reset_attributes(); 25 | output.write("\n"); 26 | output.flush() 27 | } 28 | } 29 | 30 | output.flush() 31 | } 32 | -------------------------------------------------------------------------------- /skim-tuikit/examples/256color_on_screen.rs: -------------------------------------------------------------------------------- 1 | use skim_tuikit::attr::{Attr, Color}; 2 | use skim_tuikit::canvas::Canvas; 3 | use skim_tuikit::output::Output; 4 | use skim_tuikit::screen::Screen; 5 | use std::io; 6 | 7 | fn main() { 8 | let mut output = Output::new(Box::new(io::stdout())).unwrap(); 9 | let (width, height) = output.terminal_size().unwrap(); 10 | let mut screen = Screen::new(width, height); 11 | 12 | for fg in 0..=255 { 13 | let _ = screen.print_with_attr( 14 | fg / 16, 15 | (fg % 16) * 5, 16 | format!("{:5}", fg).as_str(), 17 | Color::AnsiValue(fg as u8).into(), 18 | ); 19 | } 20 | 21 | let _ = screen.set_cursor(15, 80); 22 | let commands = screen.present(); 23 | 24 | commands.into_iter().for_each(|cmd| output.execute(cmd)); 25 | output.flush(); 26 | 27 | let _ = screen.print_with_attr(0, 78, "HELLO WORLD", Attr::default()); 28 | let commands = screen.present(); 29 | 30 | commands.into_iter().for_each(|cmd| output.execute(cmd)); 31 | output.flush(); 32 | 33 | for bg in 0..=255 { 34 | let _ = screen.print_with_attr( 35 | bg / 16, 36 | (bg % 16) * 5, 37 | format!("{:5}", bg).as_str(), 38 | Attr { 39 | bg: Color::AnsiValue(bg as u8), 40 | ..Attr::default() 41 | }, 42 | ); 43 | } 44 | let commands = screen.present(); 45 | commands.into_iter().for_each(|cmd| output.execute(cmd)); 46 | output.reset_attributes(); 47 | output.flush() 48 | } 49 | -------------------------------------------------------------------------------- /skim-tuikit/examples/custom-event.rs: -------------------------------------------------------------------------------- 1 | use bitflags::_core::result::Result::Ok; 2 | 3 | use skim_tuikit::prelude::*; 4 | 5 | fn main() { 6 | let term: Term = Term::with_height(TermHeight::Percent(30)).expect("term creation error"); 7 | let _ = term.print(0, 0, "Press 'q' or 'Ctrl-c' to quit!"); 8 | while let Ok(ev) = term.poll_event() { 9 | match ev { 10 | Event::Key(Key::Char('q')) | Event::Key(Key::Ctrl('c')) => break, 11 | Event::Key(key) => { 12 | let _ = term.print(1, 0, format!("get key: {:?}", key).as_str()); 13 | let _ = term.send_event(Event::User(format!("key: {:?}", key))); 14 | } 15 | Event::User(ev_str) => { 16 | let _ = term.print(2, 0, format!("user event: {}", &ev_str).as_str()); 17 | } 18 | _ => { 19 | let _ = term.print(3, 0, format!("event: {:?}", ev).as_str()); 20 | } 21 | } 22 | let _ = term.present(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /skim-tuikit/examples/get_keys.rs: -------------------------------------------------------------------------------- 1 | use skim_tuikit::input::KeyBoard; 2 | use skim_tuikit::key::Key; 3 | use skim_tuikit::output::Output; 4 | use skim_tuikit::raw::IntoRawMode; 5 | use std::time::Duration; 6 | 7 | fn main() { 8 | let _stdout = std::io::stdout().into_raw_mode().unwrap(); 9 | let mut output = Output::new(Box::new(_stdout)).unwrap(); 10 | output.enable_mouse_support(); 11 | output.flush(); 12 | 13 | println!("program will exit on pressing `q` or wait 5 seconds"); 14 | 15 | // let mut keyboard = KeyBoard::new(Box::new(std::io::stdin())); 16 | let mut keyboard = KeyBoard::new_with_tty(); 17 | while let Ok(key) = keyboard.next_key_timeout(Duration::from_secs(5)) { 18 | if key == Key::Char('q') { 19 | break; 20 | } 21 | println!("print: {:?}", key); 22 | } 23 | output.disable_mouse_support(); 24 | output.flush(); 25 | } 26 | -------------------------------------------------------------------------------- /skim-tuikit/examples/hello-world.rs: -------------------------------------------------------------------------------- 1 | use skim_tuikit::prelude::*; 2 | use std::cmp::{max, min}; 3 | 4 | fn main() { 5 | let term: Term<()> = Term::with_height(TermHeight::Percent(30)).unwrap(); 6 | let mut row = 1; 7 | let mut col = 0; 8 | 9 | let _ = term.print(0, 0, "press arrow key to move the text, (q) to quit"); 10 | let _ = term.present(); 11 | 12 | while let Ok(ev) = term.poll_event() { 13 | let _ = term.clear(); 14 | let _ = term.print(0, 0, "press arrow key to move the text, (q) to quit"); 15 | 16 | let (width, height) = term.term_size().unwrap(); 17 | match ev { 18 | Event::Key(Key::ESC) | Event::Key(Key::Char('q')) | Event::Key(Key::Ctrl('c')) => break, 19 | Event::Key(Key::Up) => row = max(row - 1, 1), 20 | Event::Key(Key::Down) => row = min(row + 1, height - 1), 21 | Event::Key(Key::Left) => col = max(col, 1) - 1, 22 | Event::Key(Key::Right) => col = min(col + 1, width - 1), 23 | _ => {} 24 | } 25 | 26 | let _ = term.print_with_attr(row, col, "Hello World! 你好!今日は。", Color::RED); 27 | let _ = term.set_cursor(row, col); 28 | let _ = term.present(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /skim-tuikit/examples/split.rs: -------------------------------------------------------------------------------- 1 | use skim_tuikit::prelude::*; 2 | 3 | struct Fit(String); 4 | 5 | impl Draw for Fit { 6 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 7 | let (_width, height) = canvas.size()?; 8 | let top = height / 2; 9 | let _ = canvas.print(top, 0, &self.0); 10 | Ok(()) 11 | } 12 | } 13 | impl Widget for Fit { 14 | fn size_hint(&self) -> (Option, Option) { 15 | (Some(self.0.len()), None) 16 | } 17 | } 18 | 19 | struct Model(String); 20 | 21 | impl Draw for Model { 22 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 23 | let (width, height) = canvas.size()?; 24 | let message_width = self.0.len(); 25 | let left = (width - message_width) / 2; 26 | let top = height / 2; 27 | let _ = canvas.print_with_attr(0, left, "press 'q' to exit", Effect::UNDERLINE.into()); 28 | let _ = canvas.print(top, left, &self.0); 29 | Ok(()) 30 | } 31 | } 32 | 33 | impl Widget for Model {} 34 | 35 | fn main() { 36 | let term: Term<()> = Term::with_height(TermHeight::Percent(50)).unwrap(); 37 | let model = Model("Hey, I'm in middle!".to_string()); 38 | let fit = Fit("Short Text That Fits".to_string()); 39 | 40 | while let Ok(ev) = term.poll_event() { 41 | match ev { 42 | Event::Key(Key::Char('q')) | Event::Key(Key::Ctrl('c')) => break, 43 | _ => (), 44 | } 45 | 46 | let hsplit = HSplit::default() 47 | .split( 48 | VSplit::default() 49 | .shrink(0) 50 | .grow(0) 51 | .split(Win::new(&fit).border(true)) 52 | .split(Win::new(&fit).border(true)), 53 | ) 54 | .split(Win::new(&model).border(true)); 55 | 56 | let _ = term.draw(&hsplit); 57 | let _ = term.present(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /skim-tuikit/examples/stack.rs: -------------------------------------------------------------------------------- 1 | use skim_tuikit::prelude::*; 2 | 3 | struct Model { 4 | win: String, 5 | } 6 | 7 | impl Draw for Model { 8 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 9 | let (width, height) = canvas.size()?; 10 | let _ = canvas.clear(); 11 | let message_width = self.win.len(); 12 | let left = (width - message_width) / 2; 13 | let top = height / 2; 14 | let _ = canvas.print(top, left, &self.win); 15 | Ok(()) 16 | } 17 | } 18 | 19 | impl Widget for Model { 20 | fn on_event(&self, event: Event, _rect: Rectangle) -> Vec { 21 | if let Event::Key(Key::SingleClick(_, _, _)) = event { 22 | vec![format!("{} clicked", self.win)] 23 | } else { 24 | vec![] 25 | } 26 | } 27 | } 28 | 29 | fn main() { 30 | let term = Term::with_options(TermOptions::default().mouse_enabled(true)).unwrap(); 31 | let (mut width, mut height) = term.term_size().unwrap(); 32 | 33 | while let Ok(ev) = term.poll_event() { 34 | match ev { 35 | Event::Key(Key::Char('q')) | Event::Key(Key::Ctrl('c')) => break, 36 | Event::Key(Key::MouseRelease(_, _)) => continue, 37 | Event::Resize { width: w, height: h } => { 38 | width = w; 39 | height = h; 40 | } 41 | _ => (), 42 | } 43 | let stack = Stack::::new() 44 | .top( 45 | Win::new(Model { 46 | win: "win floating on top".to_string(), 47 | }) 48 | .border(true) 49 | .margin(Size::Percent(30)), 50 | ) 51 | .bottom( 52 | HSplit::default() 53 | .split( 54 | Win::new(Model { 55 | win: String::from("left"), 56 | }) 57 | .border(true), 58 | ) 59 | .split( 60 | Win::new(Model { 61 | win: String::from("right"), 62 | }) 63 | .border(true), 64 | ), 65 | ); 66 | 67 | let message = stack.on_event( 68 | ev, 69 | Rectangle { 70 | width, 71 | height, 72 | top: 0, 73 | left: 0, 74 | }, 75 | ); 76 | let click_message = if message.is_empty() { "" } else { &message[0] }; 77 | let _ = term.draw(&stack); 78 | let _ = term.print(1, 1, "press 'q' to exit, try clicking on windows"); 79 | let _ = term.print(2, 1, &(String::from(click_message) + " ")); 80 | let _ = term.present(); 81 | } 82 | let _ = term.show_cursor(false); 83 | } 84 | -------------------------------------------------------------------------------- /skim-tuikit/examples/term_size.rs: -------------------------------------------------------------------------------- 1 | use skim_tuikit::output::Output; 2 | use std::io; 3 | 4 | fn main() { 5 | let output = Output::new(Box::new(io::stdout())).unwrap(); 6 | let (width, height) = output.terminal_size().unwrap(); 7 | println!("width: {}, height: {}", width, height); 8 | } 9 | -------------------------------------------------------------------------------- /skim-tuikit/examples/termbox.rs: -------------------------------------------------------------------------------- 1 | use skim_tuikit::prelude::*; 2 | use std::sync::Arc; 3 | use std::thread; 4 | use std::time::{Duration, Instant}; 5 | 6 | extern crate env_logger; 7 | 8 | /// This example is testing tuikit with multi-threads. 9 | 10 | const COL: usize = 4; 11 | 12 | fn main() { 13 | env_logger::init(); 14 | let term = Arc::new(Term::with_height(TermHeight::Fixed(10)).unwrap()); 15 | let _ = term.enable_mouse_support(); 16 | let now = Instant::now(); 17 | 18 | print_banner(&term); 19 | 20 | let th = thread::spawn(move || { 21 | while let Ok(ev) = term.poll_event() { 22 | match ev { 23 | Event::Key(Key::Char('q')) | Event::Key(Key::Ctrl('c')) => break, 24 | Event::Key(Key::Char('r')) => { 25 | let term = term.clone(); 26 | thread::spawn(move || { 27 | let _ = term.pause(); 28 | println!("restart in 2 seconds"); 29 | thread::sleep(Duration::from_secs(2)); 30 | let _ = term.restart(); 31 | let _ = term.clear(); 32 | }); 33 | } 34 | _ => (), 35 | } 36 | 37 | print_banner(&term); 38 | print_event(&term, ev, &now); 39 | } 40 | }); 41 | let _ = th.join(); 42 | } 43 | 44 | fn print_banner(term: &Term) { 45 | let (_, height) = term.term_size().unwrap_or((5, 5)); 46 | for row in 0..height { 47 | let _ = term.print(row, 0, format!("{} ", row).as_str()); 48 | } 49 | let attr = Attr { 50 | fg: Color::GREEN, 51 | effect: Effect::UNDERLINE, 52 | ..Attr::default() 53 | }; 54 | let _ = term.print_with_attr(0, COL, "How to use: (q)uit, (r)estart", attr); 55 | let _ = term.present(); 56 | } 57 | 58 | fn print_event(term: &Term, ev: Event, now: &Instant) { 59 | let elapsed = now.elapsed(); 60 | let (_, height) = term.term_size().unwrap_or((5, 5)); 61 | let _ = term.print(1, COL, format!("{:?}", ev).as_str()); 62 | let _ = term.print( 63 | height - 1, 64 | COL, 65 | format!( 66 | "time elapsed since program start: {}s + {}ms", 67 | elapsed.as_secs(), 68 | elapsed.subsec_millis() 69 | ) 70 | .as_str(), 71 | ); 72 | let _ = term.present(); 73 | } 74 | -------------------------------------------------------------------------------- /skim-tuikit/examples/true_color.rs: -------------------------------------------------------------------------------- 1 | use skim_tuikit::attr::Color; 2 | use skim_tuikit::output::Output; 3 | use std::io; 4 | 5 | // ported from: https://github.com/gnachman/iTerm2/blob/master/tests/24-bit-color.sh 6 | // should be run in terminals that supports true color 7 | 8 | // given a color idx/22 along HSV, return (r, g, b) 9 | fn rainbow_color(idx: u8) -> (u8, u8, u8) { 10 | let h = idx / 43; 11 | let f = idx - 43 * h; 12 | let t = ((f as i32 * 255) / 43) as u8; 13 | let q = 255 - t; 14 | 15 | match h { 16 | 0 => (255, t, 0), 17 | 1 => (q, 255, 0), 18 | 2 => (0, 255, t), 19 | 3 => (0, q, 255), 20 | 4 => (t, 0, 255), 21 | 5 => (255, 0, q), 22 | _ => unreachable!(), 23 | } 24 | } 25 | 26 | fn try_background(output: &mut Output, r: u8, g: u8, b: u8) { 27 | output.set_bg(Color::Rgb(r, g, b)); 28 | output.write(" ") 29 | } 30 | 31 | fn reset_output(output: &mut Output) { 32 | output.reset_attributes(); 33 | output.write("\n"); 34 | output.flush(); 35 | } 36 | 37 | fn main() { 38 | let mut output = Output::new(Box::new(io::stdout())).unwrap(); 39 | for i in 0..=127 { 40 | try_background(&mut output, i, 0, 0); 41 | } 42 | reset_output(&mut output); 43 | 44 | for i in (128..=255).rev() { 45 | try_background(&mut output, i, 0, 0); 46 | } 47 | reset_output(&mut output); 48 | 49 | for i in 0..=127 { 50 | try_background(&mut output, 0, i, 0); 51 | } 52 | reset_output(&mut output); 53 | 54 | for i in (128..=255).rev() { 55 | try_background(&mut output, 0, i, 0); 56 | } 57 | reset_output(&mut output); 58 | 59 | for i in 0..=127 { 60 | try_background(&mut output, 0, 0, i); 61 | } 62 | reset_output(&mut output); 63 | 64 | for i in (128..=255).rev() { 65 | try_background(&mut output, 0, 0, i); 66 | } 67 | reset_output(&mut output); 68 | 69 | for i in 0..=127 { 70 | let (r, g, b) = rainbow_color(i); 71 | try_background(&mut output, r, g, b); 72 | } 73 | reset_output(&mut output); 74 | 75 | for i in (128..=255).rev() { 76 | let (r, g, b) = rainbow_color(i); 77 | try_background(&mut output, r, g, b); 78 | } 79 | reset_output(&mut output); 80 | } 81 | -------------------------------------------------------------------------------- /skim-tuikit/examples/win.rs: -------------------------------------------------------------------------------- 1 | use skim_tuikit::prelude::*; 2 | 3 | struct Model(String); 4 | 5 | impl Draw for Model { 6 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 7 | let (width, height) = canvas.size()?; 8 | let message_width = self.0.len(); 9 | let left = (width - message_width) / 2; 10 | let top = height / 2; 11 | let _ = canvas.print(top, left, &self.0); 12 | Ok(()) 13 | } 14 | } 15 | 16 | impl Widget for Model {} 17 | 18 | fn main() { 19 | let term: Term<()> = Term::with_options( 20 | TermOptions::default() 21 | .height(TermHeight::Percent(50)) 22 | .disable_alternate_screen(true) 23 | .clear_on_start(false), 24 | ) 25 | .unwrap(); 26 | let model = Model("Hey, I'm in middle!".to_string()); 27 | 28 | while let Ok(ev) = term.poll_event() { 29 | match ev { 30 | Event::Key(Key::Char('q')) | Event::Key(Key::Ctrl('c')) => break, 31 | _ => (), 32 | } 33 | let _ = term.print(0, 0, "press 'q' to exit"); 34 | 35 | let inner_win = Win::new(&model) 36 | .fn_draw_header(Box::new(|canvas| { 37 | let _ = canvas.print(0, 0, "header printed with function"); 38 | Ok(()) 39 | })) 40 | .border(true); 41 | 42 | let win_bottom_title = Win::new(&inner_win) 43 | .title_align(HorizontalAlign::Center) 44 | .title("Title (at bottom) center aligned") 45 | .right_prompt("Right Prompt stays") 46 | .title_on_top(false) 47 | .border_bottom(true); 48 | 49 | let win = Win::new(&win_bottom_title) 50 | .margin(Size::Percent(10)) 51 | .padding(1) 52 | .title("Window Title") 53 | .right_prompt("Right Prompt") 54 | .border(true) 55 | .border_top_attr(Color::BLUE) 56 | .border_right_attr(Color::YELLOW) 57 | .border_bottom_attr(Color::RED) 58 | .border_left_attr(Color::GREEN); 59 | 60 | let _ = term.draw(&win); 61 | let _ = term.present(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /skim-tuikit/src/attr.rs: -------------------------------------------------------------------------------- 1 | //! attr modules defines the attributes(colors, effects) of a terminal cell 2 | 3 | use bitflags::bitflags; 4 | 5 | pub use crate::color::Color; 6 | 7 | /// `Attr` is a rendering attribute that contains fg color, bg color and text effect. 8 | /// 9 | /// ``` 10 | /// use skim_tuikit::attr::{Attr, Effect, Color}; 11 | /// 12 | /// let attr = Attr { fg: Color::RED, effect: Effect::BOLD, ..Attr::default() }; 13 | /// ``` 14 | #[derive(Debug, Clone, Copy, PartialEq)] 15 | pub struct Attr { 16 | pub fg: Color, 17 | pub bg: Color, 18 | pub effect: Effect, 19 | } 20 | 21 | impl Default for Attr { 22 | fn default() -> Self { 23 | Attr { 24 | fg: Color::default(), 25 | bg: Color::default(), 26 | effect: Effect::empty(), 27 | } 28 | } 29 | } 30 | 31 | impl Attr { 32 | /// extend the properties with the new attr's if the properties in new attr is not default. 33 | /// ``` 34 | /// use skim_tuikit::attr::{Attr, Color, Effect}; 35 | /// 36 | /// let default = Attr{fg: Color::BLUE, bg: Color::YELLOW, effect: Effect::BOLD}; 37 | /// let new = Attr{fg: Color::Default, bg: Color::WHITE, effect: Effect::REVERSE}; 38 | /// let extended = default.extend(new); 39 | /// 40 | /// assert_eq!(Color::BLUE, extended.fg); 41 | /// assert_eq!(Color::WHITE, extended.bg); 42 | /// assert_eq!(Effect::BOLD | Effect::REVERSE, extended.effect); 43 | /// ``` 44 | pub fn extend(&self, new_attr: Self) -> Attr { 45 | Attr { 46 | fg: if new_attr.fg != Color::default() { 47 | new_attr.fg 48 | } else { 49 | self.fg 50 | }, 51 | bg: if new_attr.bg != Color::default() { 52 | new_attr.bg 53 | } else { 54 | self.bg 55 | }, 56 | effect: self.effect | new_attr.effect, 57 | } 58 | } 59 | 60 | pub fn fg(mut self, fg: Color) -> Self { 61 | self.fg = fg; 62 | self 63 | } 64 | 65 | pub fn bg(mut self, bg: Color) -> Self { 66 | self.bg = bg; 67 | self 68 | } 69 | 70 | pub fn effect(mut self, effect: Effect) -> Self { 71 | self.effect = effect; 72 | self 73 | } 74 | } 75 | 76 | bitflags! { 77 | /// `Effect` is the effect of a text 78 | pub struct Effect: u8 { 79 | const BOLD = 0b00000001; 80 | const DIM = 0b00000010; 81 | const UNDERLINE = 0b00000100; 82 | const BLINK = 0b00001000; 83 | const REVERSE = 0b00010000; 84 | } 85 | } 86 | 87 | impl From for Attr { 88 | fn from(fg: Color) -> Self { 89 | Attr { 90 | fg, 91 | ..Default::default() 92 | } 93 | } 94 | } 95 | 96 | impl From for Attr { 97 | fn from(effect: Effect) -> Self { 98 | Attr { 99 | effect, 100 | ..Default::default() 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /skim-tuikit/src/canvas.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | /// A canvas is a trait defining the draw actions 3 | use crate::attr::Attr; 4 | use crate::cell::Cell; 5 | use unicode_width::UnicodeWidthChar; 6 | 7 | pub trait Canvas { 8 | /// Get the canvas size (width, height) 9 | fn size(&self) -> Result<(usize, usize)>; 10 | 11 | /// clear the canvas 12 | fn clear(&mut self) -> Result<()>; 13 | 14 | /// change a cell of position `(row, col)` to `cell` 15 | /// if `(row, col)` is out of boundary, `Ok` is returned, but no operation is taken 16 | /// return the width of the character/cell 17 | fn put_cell(&mut self, row: usize, col: usize, cell: Cell) -> Result; 18 | 19 | /// just like put_cell, except it accept (char & attr) 20 | /// return the width of the character/cell 21 | fn put_char_with_attr(&mut self, row: usize, col: usize, ch: char, attr: Attr) -> Result { 22 | self.put_cell(row, col, Cell { ch, attr }) 23 | } 24 | 25 | /// print `content` starting with position `(row, col)` with `attr` 26 | /// 27 | /// - canvas should NOT wrap to y+1 if the content is too long 28 | /// - canvas should handle wide characters 29 | /// 30 | /// returns the printed width of the content 31 | fn print_with_attr(&mut self, row: usize, col: usize, content: &str, attr: Attr) -> Result { 32 | let mut cell = Cell { 33 | attr, 34 | ..Cell::default() 35 | }; 36 | 37 | let mut width = 0; 38 | for ch in content.chars() { 39 | cell.ch = ch; 40 | width += self.put_cell(row, col + width, cell)?; 41 | } 42 | Ok(width) 43 | } 44 | 45 | /// print `content` starting with position `(row, col)` with default attribute 46 | fn print(&mut self, row: usize, col: usize, content: &str) -> Result { 47 | self.print_with_attr(row, col, content, Attr::default()) 48 | } 49 | 50 | /// move cursor position (row, col) and show cursor 51 | fn set_cursor(&mut self, row: usize, col: usize) -> Result<()>; 52 | 53 | /// show/hide cursor, set `show` to `false` to hide the cursor 54 | fn show_cursor(&mut self, show: bool) -> Result<()>; 55 | } 56 | 57 | /// A sub-area of a canvas. 58 | /// It will handle the adjustments of cursor movement, so that you could write 59 | /// to for example (0, 0) and BoundedCanvas will adjust it to real position. 60 | pub struct BoundedCanvas<'a> { 61 | canvas: &'a mut dyn Canvas, 62 | top: usize, 63 | left: usize, 64 | width: usize, 65 | height: usize, 66 | } 67 | 68 | impl<'a> BoundedCanvas<'a> { 69 | pub fn new(top: usize, left: usize, width: usize, height: usize, canvas: &'a mut dyn Canvas) -> Self { 70 | Self { 71 | canvas, 72 | top, 73 | left, 74 | width, 75 | height, 76 | } 77 | } 78 | } 79 | 80 | impl Canvas for BoundedCanvas<'_> { 81 | fn size(&self) -> Result<(usize, usize)> { 82 | Ok((self.width, self.height)) 83 | } 84 | 85 | fn clear(&mut self) -> Result<()> { 86 | for row in self.top..(self.top + self.height) { 87 | for col in self.left..(self.left + self.width) { 88 | let _ = self.canvas.put_cell(row, col, Cell::empty()); 89 | } 90 | } 91 | 92 | Ok(()) 93 | } 94 | 95 | fn put_cell(&mut self, row: usize, col: usize, cell: Cell) -> Result { 96 | if row >= self.height || col >= self.width { 97 | // do nothing 98 | Ok(cell.ch.width().unwrap_or(2)) 99 | } else { 100 | self.canvas.put_cell(row + self.top, col + self.left, cell) 101 | } 102 | } 103 | 104 | fn set_cursor(&mut self, row: usize, col: usize) -> Result<()> { 105 | if row >= self.height || col >= self.width { 106 | // do nothing 107 | Ok(()) 108 | } else { 109 | self.canvas.set_cursor(row + self.top, col + self.left) 110 | } 111 | } 112 | 113 | fn show_cursor(&mut self, show: bool) -> Result<()> { 114 | self.canvas.show_cursor(show) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /skim-tuikit/src/cell.rs: -------------------------------------------------------------------------------- 1 | /// `Cell` is a cell of the terminal. 2 | /// It has a display character and an attribute (fg and bg color, effects). 3 | use crate::attr::{Attr, Color, Effect}; 4 | 5 | const EMPTY_CHAR: char = '\0'; 6 | 7 | #[derive(Debug, Clone, Copy, PartialEq)] 8 | pub struct Cell { 9 | pub ch: char, 10 | pub attr: Attr, 11 | } 12 | 13 | impl Default for Cell { 14 | fn default() -> Self { 15 | Self { 16 | ch: ' ', 17 | attr: Attr::default(), 18 | } 19 | } 20 | } 21 | 22 | impl Cell { 23 | pub fn empty() -> Self { 24 | Self::default().ch(EMPTY_CHAR) 25 | } 26 | 27 | pub fn ch(mut self, ch: char) -> Self { 28 | self.ch = ch; 29 | self 30 | } 31 | 32 | pub fn fg(mut self, fg: Color) -> Self { 33 | self.attr.fg = fg; 34 | self 35 | } 36 | 37 | pub fn bg(mut self, bg: Color) -> Self { 38 | self.attr.bg = bg; 39 | self 40 | } 41 | 42 | pub fn effect(mut self, effect: Effect) -> Self { 43 | self.attr.effect = effect; 44 | self 45 | } 46 | 47 | pub fn attribute(mut self, attr: Attr) -> Self { 48 | self.attr = attr; 49 | self 50 | } 51 | 52 | /// check if a cell is empty 53 | pub fn is_empty(self) -> bool { 54 | self.ch == EMPTY_CHAR && self.attr == Attr::default() 55 | } 56 | } 57 | 58 | impl From for Cell { 59 | fn from(ch: char) -> Self { 60 | Cell { 61 | ch, 62 | attr: Attr::default(), 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /skim-tuikit/src/color.rs: -------------------------------------------------------------------------------- 1 | /// Color of a character, could be 8 bit(256 color) or RGB color 2 | /// 3 | /// ``` 4 | /// use skim_tuikit::attr::Color; 5 | /// Color::RED; // predefined values 6 | /// Color::Rgb(255, 0, 0); // RED 7 | /// ``` 8 | #[derive(Debug, Clone, Copy, PartialEq, Default)] 9 | #[non_exhaustive] 10 | pub enum Color { 11 | #[default] 12 | Default, 13 | AnsiValue(u8), 14 | Rgb(u8, u8, u8), 15 | } 16 | 17 | impl Color { 18 | pub const BLACK: Color = Color::AnsiValue(0); 19 | pub const RED: Color = Color::AnsiValue(1); 20 | pub const GREEN: Color = Color::AnsiValue(2); 21 | pub const YELLOW: Color = Color::AnsiValue(3); 22 | pub const BLUE: Color = Color::AnsiValue(4); 23 | pub const MAGENTA: Color = Color::AnsiValue(5); 24 | pub const CYAN: Color = Color::AnsiValue(6); 25 | pub const WHITE: Color = Color::AnsiValue(7); 26 | pub const LIGHT_BLACK: Color = Color::AnsiValue(8); 27 | pub const LIGHT_RED: Color = Color::AnsiValue(9); 28 | pub const LIGHT_GREEN: Color = Color::AnsiValue(10); 29 | pub const LIGHT_YELLOW: Color = Color::AnsiValue(11); 30 | pub const LIGHT_BLUE: Color = Color::AnsiValue(12); 31 | pub const LIGHT_MAGENTA: Color = Color::AnsiValue(13); 32 | pub const LIGHT_CYAN: Color = Color::AnsiValue(14); 33 | pub const LIGHT_WHITE: Color = Color::AnsiValue(15); 34 | } 35 | -------------------------------------------------------------------------------- /skim-tuikit/src/draw.rs: -------------------------------------------------------------------------------- 1 | /// A trait defines something that could be drawn 2 | use crate::canvas::Canvas; 3 | 4 | pub type DrawResult = std::result::Result>; 5 | 6 | /// Something that knows how to draw itself onto the canvas 7 | #[allow(unused_variables)] 8 | pub trait Draw { 9 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 10 | Ok(()) 11 | } 12 | fn draw_mut(&mut self, canvas: &mut dyn Canvas) -> DrawResult<()> { 13 | self.draw(canvas) 14 | } 15 | } 16 | 17 | impl Draw for &T { 18 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 19 | (*self).draw(canvas) 20 | } 21 | fn draw_mut(&mut self, canvas: &mut dyn Canvas) -> DrawResult<()> { 22 | (*self).draw(canvas) 23 | } 24 | } 25 | 26 | impl Draw for &mut T { 27 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 28 | (**self).draw(canvas) 29 | } 30 | fn draw_mut(&mut self, canvas: &mut dyn Canvas) -> DrawResult<()> { 31 | (**self).draw_mut(canvas) 32 | } 33 | } 34 | 35 | impl Draw for Box { 36 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 37 | self.as_ref().draw(canvas) 38 | } 39 | 40 | fn draw_mut(&mut self, canvas: &mut dyn Canvas) -> DrawResult<()> { 41 | self.as_mut().draw_mut(canvas) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /skim-tuikit/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt::{Display, Formatter}; 3 | use std::string::FromUtf8Error; 4 | use std::time::Duration; 5 | 6 | #[derive(Debug)] 7 | pub enum TuikitError { 8 | UnknownSequence(String), 9 | NoCursorReportResponse, 10 | IndexOutOfBound(usize, usize), 11 | Timeout(Duration), 12 | Interrupted, 13 | TerminalNotStarted, 14 | DrawError(Box), 15 | SendEventError(String), 16 | FromUtf8Error(std::string::FromUtf8Error), 17 | ParseIntError(std::num::ParseIntError), 18 | IOError(std::io::Error), 19 | NixError(nix::Error), 20 | ChannelReceiveError(std::sync::mpsc::RecvError), 21 | } 22 | 23 | impl Display for TuikitError { 24 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 25 | match self { 26 | TuikitError::UnknownSequence(sequence) => { 27 | write!(f, "unsupported esc sequence: {}", sequence) 28 | } 29 | TuikitError::NoCursorReportResponse => { 30 | write!(f, "buffer did not contain cursor position response") 31 | } 32 | TuikitError::IndexOutOfBound(row, col) => { 33 | write!(f, "({}, {}) is out of bound", row, col) 34 | } 35 | TuikitError::Timeout(duration) => write!(f, "timeout with duration: {:?}", duration), 36 | TuikitError::Interrupted => write!(f, "interrupted"), 37 | TuikitError::TerminalNotStarted => { 38 | write!(f, "terminal not started, call `restart` to start it") 39 | } 40 | TuikitError::DrawError(error) => write!(f, "draw error: {}", error), 41 | TuikitError::SendEventError(error) => write!(f, "send event error: {}", error), 42 | TuikitError::FromUtf8Error(error) => write!(f, "{}", error), 43 | TuikitError::ParseIntError(error) => write!(f, "{}", error), 44 | TuikitError::IOError(error) => write!(f, "{}", error), 45 | TuikitError::NixError(error) => write!(f, "{}", error), 46 | TuikitError::ChannelReceiveError(error) => write!(f, "{}", error), 47 | } 48 | } 49 | } 50 | 51 | impl Error for TuikitError {} 52 | 53 | impl From for TuikitError { 54 | fn from(error: FromUtf8Error) -> Self { 55 | TuikitError::FromUtf8Error(error) 56 | } 57 | } 58 | 59 | impl From for TuikitError { 60 | fn from(error: std::num::ParseIntError) -> Self { 61 | TuikitError::ParseIntError(error) 62 | } 63 | } 64 | 65 | impl From for TuikitError { 66 | fn from(error: nix::Error) -> Self { 67 | TuikitError::NixError(error) 68 | } 69 | } 70 | 71 | impl From for TuikitError { 72 | fn from(error: std::io::Error) -> Self { 73 | TuikitError::IOError(error) 74 | } 75 | } 76 | 77 | impl From for TuikitError { 78 | fn from(error: std::sync::mpsc::RecvError) -> Self { 79 | TuikitError::ChannelReceiveError(error) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /skim-tuikit/src/event.rs: -------------------------------------------------------------------------------- 1 | //! events a `Term` could return 2 | 3 | pub use crate::key::Key; 4 | 5 | #[derive(Eq, PartialEq, Hash, Debug, Copy, Clone)] 6 | pub enum Event { 7 | Key(Key), 8 | Resize { 9 | width: usize, 10 | height: usize, 11 | }, 12 | Restarted, 13 | /// user defined signal 1 14 | User(UserEvent), 15 | 16 | #[doc(hidden)] 17 | __Nonexhaustive, 18 | } 19 | -------------------------------------------------------------------------------- /skim-tuikit/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! ## Tuikit 3 | //! Tuikit is a TUI library for writing terminal UI applications. Highlights: 4 | //! 5 | //! - Thread safe. 6 | //! - Support non-fullscreen mode as well as fullscreen mode. 7 | //! - Support `Alt` keys, mouse events, etc. 8 | //! - Buffering for efficient rendering. 9 | //! 10 | //! Tuikit is modeld after [termbox](https://github.com/nsf/termbox) which views the 11 | //! terminal as a table of fixed-size cells and input being a stream of structured 12 | //! messages. 13 | //! 14 | //! ## Usage 15 | //! 16 | //! In your `Cargo.toml` add the following: 17 | //! 18 | //! ```toml 19 | //! [dependencies] 20 | //! tuikit = "*" 21 | //! ``` 22 | //! 23 | //! Here is an example: 24 | //! 25 | //! ```no_run 26 | //! use skim_tuikit::attr::*; 27 | //! use skim_tuikit::term::{Term, TermHeight}; 28 | //! use skim_tuikit::event::{Event, Key}; 29 | //! use std::cmp::{min, max}; 30 | //! 31 | //! let term: Term<()> = Term::with_height(TermHeight::Percent(30)).unwrap(); 32 | //! let mut row = 1; 33 | //! let mut col = 0; 34 | //! 35 | //! let _ = term.print(0, 0, "press arrow key to move the text, (q) to quit"); 36 | //! let _ = term.present(); 37 | //! 38 | //! while let Ok(ev) = term.poll_event() { 39 | //! let _ = term.clear(); 40 | //! let _ = term.print(0, 0, "press arrow key to move the text, (q) to quit"); 41 | //! 42 | //! let (width, height) = term.term_size().unwrap(); 43 | //! match ev { 44 | //! Event::Key(Key::ESC) | Event::Key(Key::Char('q')) => break, 45 | //! Event::Key(Key::Up) => row = max(row-1, 1), 46 | //! Event::Key(Key::Down) => row = min(row+1, height-1), 47 | //! Event::Key(Key::Left) => col = max(col, 1)-1, 48 | //! Event::Key(Key::Right) => col = min(col+1, width-1), 49 | //! _ => {} 50 | //! } 51 | //! 52 | //! let attr = Attr{ fg: Color::RED, ..Attr::default() }; 53 | //! let _ = term.print_with_attr(row, col, "Hello World! 你好!今日は。", attr); 54 | //! let _ = term.set_cursor(row, col); 55 | //! let _ = term.present(); 56 | //! } 57 | //! ``` 58 | pub mod attr; 59 | pub mod canvas; 60 | pub mod cell; 61 | mod color; 62 | pub mod draw; 63 | pub mod error; 64 | pub mod event; 65 | pub mod input; 66 | pub mod key; 67 | mod macros; 68 | pub mod output; 69 | pub mod prelude; 70 | pub mod raw; 71 | pub mod screen; 72 | use skim_common::spinlock; 73 | mod sys; 74 | pub mod term; 75 | pub mod widget; 76 | 77 | #[macro_use] 78 | extern crate log; 79 | 80 | use crate::error::TuikitError; 81 | 82 | pub type Result = std::result::Result; 83 | -------------------------------------------------------------------------------- /skim-tuikit/src/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! ok_or_return { 3 | ($expr:expr, $default_val:expr) => { 4 | match $expr { 5 | Ok(val) => val, 6 | Err(_) => { 7 | return $default_val; 8 | } 9 | } 10 | }; 11 | } 12 | 13 | #[macro_export] 14 | macro_rules! some_or_return { 15 | ($expr:expr, $default_val:expr) => { 16 | match $expr { 17 | Some(val) => val, 18 | None => { 19 | return $default_val; 20 | } 21 | } 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /skim-tuikit/src/prelude.rs: -------------------------------------------------------------------------------- 1 | pub use crate::Result; 2 | pub use crate::attr::{Attr, Color, Effect}; 3 | pub use crate::canvas::Canvas; 4 | pub use crate::cell::Cell; 5 | pub use crate::draw::{Draw, DrawResult}; 6 | pub use crate::event::Event; 7 | pub use crate::key::*; 8 | pub use crate::term::{Term, TermHeight, TermOptions}; 9 | pub use crate::widget::{ 10 | AlignSelf, HSplit, HorizontalAlign, Rectangle, Size, Split, Stack, VSplit, VerticalAlign, Widget, Win, 11 | }; 12 | -------------------------------------------------------------------------------- /skim-tuikit/src/raw.rs: -------------------------------------------------------------------------------- 1 | //! Managing raw mode. 2 | //! 3 | //! Raw mode is a particular state a TTY can have. It signifies that: 4 | //! 5 | //! 1. No line buffering (the input is given byte-by-byte). 6 | //! 2. The input is not written out, instead it has to be done manually by the programmer. 7 | //! 3. The output is not canonicalized (for example, `\n` means "go one line down", not "line 8 | //! break"). 9 | //! 10 | //! # Example 11 | //! 12 | //! ```rust,no_run 13 | //! use skim_tuikit::raw::IntoRawMode; 14 | //! use std::io::{Write, stdout}; 15 | //! 16 | //! let mut stdout = stdout().into_raw_mode().unwrap(); 17 | //! 18 | //! write!(stdout, "Hey there.").unwrap(); 19 | //! ``` 20 | 21 | use std::io::{self, Write}; 22 | use std::ops; 23 | use std::os::fd::{AsFd, AsRawFd, BorrowedFd}; 24 | 25 | use nix::sys::termios::{SetArg, Termios, cfmakeraw, tcgetattr, tcsetattr}; 26 | use nix::unistd::isatty; 27 | use std::fs; 28 | use std::os::unix::io::RawFd; 29 | 30 | // taken from termion 31 | /// Get the TTY device. 32 | /// 33 | /// This allows for getting stdio representing _only_ the TTY, and not other streams. 34 | pub fn get_tty() -> io::Result { 35 | fs::OpenOptions::new().read(true).write(true).open("/dev/tty") 36 | } 37 | 38 | /// A terminal restorer, which keeps the previous state of the terminal, and restores it, when 39 | /// dropped. 40 | /// 41 | /// Restoring will entirely bring back the old TTY state. 42 | pub struct RawTerminal { 43 | prev_ios: Termios, 44 | output: W, 45 | } 46 | 47 | impl Drop for RawTerminal { 48 | fn drop(&mut self) { 49 | let _ = tcsetattr(self.output.as_fd(), SetArg::TCSANOW, &self.prev_ios); 50 | } 51 | } 52 | 53 | impl ops::Deref for RawTerminal { 54 | type Target = W; 55 | 56 | fn deref(&self) -> &W { 57 | &self.output 58 | } 59 | } 60 | 61 | impl ops::DerefMut for RawTerminal { 62 | fn deref_mut(&mut self) -> &mut W { 63 | &mut self.output 64 | } 65 | } 66 | 67 | impl Write for RawTerminal { 68 | fn write(&mut self, buf: &[u8]) -> io::Result { 69 | self.output.write(buf) 70 | } 71 | 72 | fn flush(&mut self) -> io::Result<()> { 73 | self.output.flush() 74 | } 75 | } 76 | 77 | impl AsFd for RawTerminal { 78 | fn as_fd(&self) -> BorrowedFd { 79 | self.output.as_fd() 80 | } 81 | } 82 | 83 | impl AsRawFd for RawTerminal { 84 | fn as_raw_fd(&self) -> RawFd { 85 | self.output.as_raw_fd() 86 | } 87 | } 88 | 89 | /// Types which can be converted into "raw mode". 90 | /// 91 | /// # Why is this type defined on writers and not readers? 92 | /// 93 | /// TTYs has their state controlled by the writer, not the reader. You use the writer to clear the 94 | /// screen, move the cursor and so on, so naturally you use the writer to change the mode as well. 95 | pub trait IntoRawMode: Write + AsFd + AsRawFd + Sized { 96 | /// Switch to raw mode. 97 | /// 98 | /// Raw mode means that stdin won't be printed (it will instead have to be written manually by 99 | /// the program). Furthermore, the input isn't canonicalised or buffered (that is, you can 100 | /// read from stdin one byte of a time). The output is neither modified in any way. 101 | fn into_raw_mode(self) -> io::Result>; 102 | } 103 | 104 | impl IntoRawMode for W { 105 | // modified after https://github.com/kkawakam/rustyline/blob/master/src/tty/unix.rs#L668 106 | // refer: https://linux.die.net/man/3/termios 107 | fn into_raw_mode(self) -> io::Result> { 108 | use nix::errno::Errno::ENOTTY; 109 | use nix::sys::termios::OutputFlags; 110 | 111 | let istty = isatty(self.as_raw_fd()).map_err(nix_err_to_io_err)?; 112 | if !istty { 113 | Err(nix_err_to_io_err(ENOTTY))? 114 | } 115 | 116 | let prev_ios = tcgetattr(self.as_fd()).map_err(nix_err_to_io_err)?; 117 | let mut ios = prev_ios.clone(); 118 | // set raw mode 119 | cfmakeraw(&mut ios); 120 | // enable output processing (so that '\n' will issue carriage return) 121 | ios.output_flags |= OutputFlags::OPOST; 122 | 123 | tcsetattr(&self, SetArg::TCSANOW, &ios).map_err(nix_err_to_io_err)?; 124 | 125 | Ok(RawTerminal { prev_ios, output: self }) 126 | } 127 | } 128 | 129 | fn nix_err_to_io_err(err: nix::Error) -> io::Error { 130 | io::Error::from(err) 131 | } 132 | -------------------------------------------------------------------------------- /skim-tuikit/src/sys/file.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | use std::os::fd::BorrowedFd; 3 | use std::time::Duration; 4 | 5 | use crate::error::TuikitError; 6 | use nix::sys::select; 7 | use nix::sys::time::{TimeVal, TimeValLike}; 8 | 9 | fn duration_to_timeval(duration: Duration) -> TimeVal { 10 | let sec = duration.as_secs() * 1000 + (duration.subsec_millis() as u64); 11 | TimeVal::milliseconds(sec as i64) 12 | } 13 | 14 | pub fn wait_until_ready(fd: BorrowedFd, signal_fd: Option, timeout: Duration) -> Result<()> { 15 | let mut timeout_spec = if timeout == Duration::new(0, 0) { 16 | None 17 | } else { 18 | Some(duration_to_timeval(timeout)) 19 | }; 20 | 21 | let mut fdset = select::FdSet::new(); 22 | fdset.insert(fd); 23 | 24 | if let Some(f) = signal_fd { 25 | fdset.insert(f); 26 | } 27 | let n = select::select(None, &mut fdset, None, None, &mut timeout_spec)?; 28 | 29 | if n < 1 { 30 | Err(TuikitError::Timeout(timeout)) // this error message will be used in input.rs 31 | } else if fdset.contains(fd) { 32 | Ok(()) 33 | } else { 34 | Err(TuikitError::Interrupted) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /skim-tuikit/src/sys/mod.rs: -------------------------------------------------------------------------------- 1 | // copy from https://docs.rs/crate/termion/1.5.1/source/src/sys/unix/mod.rs 2 | use std::io; 3 | pub mod file; 4 | pub mod signal; 5 | pub mod size; 6 | 7 | trait IsMinusOne { 8 | fn is_minus_one(&self) -> bool; 9 | } 10 | 11 | macro_rules! impl_is_minus_one { 12 | ($($t:ident)*) => ($(impl IsMinusOne for $t { 13 | fn is_minus_one(&self) -> bool { 14 | *self == -1 15 | } 16 | })*) 17 | } 18 | 19 | impl_is_minus_one! { i8 i16 i32 i64 isize } 20 | 21 | fn cvt(t: T) -> io::Result { 22 | if t.is_minus_one() { 23 | Err(io::Error::last_os_error()) 24 | } else { 25 | Ok(t) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /skim-tuikit/src/sys/signal.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use nix::sys::signal::{SaFlags, SigAction, SigHandler, SigSet, SigmaskHow, Signal}; 3 | use nix::sys::signal::{pthread_sigmask, sigaction}; 4 | use std::collections::HashMap; 5 | use std::sync::Mutex; 6 | use std::sync::Once; 7 | use std::sync::atomic::{AtomicUsize, Ordering}; 8 | use std::sync::mpsc::{Receiver, Sender, channel}; 9 | use std::thread; 10 | 11 | lazy_static! { 12 | static ref NOTIFIER_COUNTER: AtomicUsize = AtomicUsize::new(1); 13 | static ref NOTIFIER: Mutex>> = Mutex::new(HashMap::new()); 14 | } 15 | 16 | static ONCE: Once = Once::new(); 17 | 18 | pub fn initialize_signals() { 19 | ONCE.call_once(listen_sigwinch); 20 | } 21 | 22 | pub fn notify_on_sigwinch() -> (usize, Receiver<()>) { 23 | let (tx, rx) = channel(); 24 | let new_id = NOTIFIER_COUNTER.fetch_add(1, Ordering::Relaxed); 25 | let mut notifiers = NOTIFIER.lock().unwrap(); 26 | notifiers.entry(new_id).or_insert(tx); 27 | (new_id, rx) 28 | } 29 | 30 | pub fn unregister_sigwinch(id: usize) -> Option> { 31 | let mut notifiers = NOTIFIER.lock().unwrap(); 32 | notifiers.remove(&id) 33 | } 34 | 35 | extern "C" fn handle_sigwiwnch(_: i32) {} 36 | 37 | fn listen_sigwinch() { 38 | let (tx_sig, rx_sig) = channel(); 39 | 40 | // register terminal resize event, `pthread_sigmask` should be run before any thread. 41 | let mut sigset = SigSet::empty(); 42 | sigset.add(Signal::SIGWINCH); 43 | let _ = pthread_sigmask(SigmaskHow::SIG_BLOCK, Some(&sigset), None); 44 | 45 | // SIGWINCH is ignored by mac by default, thus we need to register an empty handler 46 | let action = SigAction::new(SigHandler::Handler(handle_sigwiwnch), SaFlags::empty(), SigSet::empty()); 47 | 48 | unsafe { 49 | let _ = sigaction(Signal::SIGWINCH, &action); 50 | } 51 | 52 | thread::spawn(move || { 53 | // listen to the resize event; 54 | loop { 55 | let _errno = sigset.wait(); 56 | let _ = tx_sig.send(()); 57 | } 58 | }); 59 | 60 | thread::spawn(move || { 61 | while rx_sig.recv().is_ok() { 62 | let notifiers = NOTIFIER.lock().unwrap(); 63 | for (_, sender) in notifiers.iter() { 64 | let _ = sender.send(()); 65 | } 66 | } 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /skim-tuikit/src/sys/size.rs: -------------------------------------------------------------------------------- 1 | use std::{io, mem}; 2 | 3 | use super::cvt; 4 | use nix::libc::{TIOCGWINSZ, c_int, c_ushort, ioctl}; 5 | 6 | #[repr(C)] 7 | struct TermSize { 8 | row: c_ushort, 9 | col: c_ushort, 10 | _x: c_ushort, 11 | _y: c_ushort, 12 | } 13 | 14 | /// Get the size of the terminal. 15 | pub fn terminal_size(fd: c_int) -> io::Result<(usize, usize)> { 16 | unsafe { 17 | let mut size: TermSize = mem::zeroed(); 18 | cvt(ioctl(fd, TIOCGWINSZ, &mut size as *mut _))?; 19 | Ok((size.col as usize, size.row as usize)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /skim-tuikit/src/widget/align.rs: -------------------------------------------------------------------------------- 1 | pub trait AlignSelf { 2 | /// say horizontal align, given container's (start, end) and self's size 3 | /// Adjust the actual start position of self. 4 | /// 5 | /// Note that if the container's size < self_size, will return `start` 6 | fn adjust(&self, start: usize, end_exclusive: usize, self_size: usize) -> usize; 7 | } 8 | 9 | pub enum HorizontalAlign { 10 | Left, 11 | Center, 12 | Right, 13 | } 14 | 15 | pub enum VerticalAlign { 16 | Top, 17 | Middle, 18 | Bottom, 19 | } 20 | 21 | impl AlignSelf for HorizontalAlign { 22 | fn adjust(&self, start: usize, end: usize, self_size: usize) -> usize { 23 | if start >= end { 24 | // wrong input 25 | return start; 26 | } 27 | let container_size = end - start; 28 | if container_size <= self_size { 29 | return start; 30 | } 31 | 32 | match self { 33 | HorizontalAlign::Left => start, 34 | HorizontalAlign::Center => start + (container_size - self_size) / 2, 35 | HorizontalAlign::Right => end - self_size, 36 | } 37 | } 38 | } 39 | 40 | impl AlignSelf for VerticalAlign { 41 | fn adjust(&self, start: usize, end: usize, self_size: usize) -> usize { 42 | if start >= end { 43 | // wrong input 44 | return start; 45 | } 46 | let container_size = end - start; 47 | if container_size <= self_size { 48 | return start; 49 | } 50 | 51 | match self { 52 | VerticalAlign::Top => start, 53 | VerticalAlign::Middle => start + (container_size - self_size) / 2, 54 | VerticalAlign::Bottom => end - self_size, 55 | } 56 | } 57 | } 58 | 59 | #[cfg(test)] 60 | mod tests { 61 | use crate::widget::align::{AlignSelf, HorizontalAlign, VerticalAlign}; 62 | 63 | #[test] 64 | fn size_lt0_return_start() { 65 | assert_eq!(0, HorizontalAlign::Left.adjust(0, 0, 2)); 66 | assert_eq!(0, HorizontalAlign::Center.adjust(0, 0, 2)); 67 | assert_eq!(0, HorizontalAlign::Right.adjust(0, 0, 2)); 68 | assert_eq!(0, VerticalAlign::Top.adjust(0, 0, 2)); 69 | assert_eq!(0, VerticalAlign::Middle.adjust(0, 0, 2)); 70 | assert_eq!(0, VerticalAlign::Bottom.adjust(0, 0, 2)); 71 | 72 | assert_eq!(2, HorizontalAlign::Left.adjust(2, 0, 2)); 73 | assert_eq!(2, HorizontalAlign::Center.adjust(2, 0, 2)); 74 | assert_eq!(2, HorizontalAlign::Right.adjust(2, 0, 2)); 75 | assert_eq!(2, VerticalAlign::Top.adjust(2, 0, 2)); 76 | assert_eq!(2, VerticalAlign::Middle.adjust(2, 0, 2)); 77 | assert_eq!(2, VerticalAlign::Bottom.adjust(2, 0, 2)); 78 | } 79 | 80 | #[test] 81 | fn container_size_too_small_return_start() { 82 | assert_eq!(2, HorizontalAlign::Left.adjust(2, 3, 2)); 83 | assert_eq!(2, HorizontalAlign::Center.adjust(2, 3, 2)); 84 | assert_eq!(2, HorizontalAlign::Right.adjust(2, 3, 2)); 85 | assert_eq!(2, VerticalAlign::Top.adjust(2, 3, 2)); 86 | assert_eq!(2, VerticalAlign::Middle.adjust(2, 3, 2)); 87 | assert_eq!(2, VerticalAlign::Bottom.adjust(2, 3, 2)); 88 | } 89 | 90 | #[test] 91 | fn align_start() { 92 | assert_eq!(2, HorizontalAlign::Left.adjust(2, 8, 2)); 93 | assert_eq!(2, VerticalAlign::Top.adjust(2, 8, 2)); 94 | assert_eq!(2, HorizontalAlign::Left.adjust(2, 7, 2)); 95 | assert_eq!(2, VerticalAlign::Top.adjust(2, 7, 2)); 96 | assert_eq!(2, HorizontalAlign::Left.adjust(2, 8, 3)); 97 | assert_eq!(2, VerticalAlign::Top.adjust(2, 8, 3)); 98 | } 99 | 100 | #[test] 101 | fn align_end() { 102 | assert_eq!(6, HorizontalAlign::Right.adjust(2, 8, 2)); 103 | assert_eq!(6, VerticalAlign::Bottom.adjust(2, 8, 2)); 104 | assert_eq!(5, HorizontalAlign::Right.adjust(2, 7, 2)); 105 | assert_eq!(5, VerticalAlign::Bottom.adjust(2, 7, 2)); 106 | assert_eq!(5, HorizontalAlign::Right.adjust(2, 8, 3)); 107 | assert_eq!(5, VerticalAlign::Bottom.adjust(2, 8, 3)); 108 | } 109 | 110 | #[test] 111 | fn align_center() { 112 | assert_eq!(4, HorizontalAlign::Center.adjust(2, 8, 2)); 113 | assert_eq!(4, VerticalAlign::Middle.adjust(2, 8, 2)); 114 | assert_eq!(3, HorizontalAlign::Center.adjust(2, 7, 2)); 115 | assert_eq!(3, VerticalAlign::Middle.adjust(2, 7, 2)); 116 | assert_eq!(3, HorizontalAlign::Center.adjust(2, 8, 3)); 117 | assert_eq!(3, VerticalAlign::Middle.adjust(2, 8, 3)); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /skim-tuikit/src/widget/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::align::*; 2 | // Various pre-defined widget that implements Draw 3 | pub use self::split::*; 4 | pub use self::stack::*; 5 | pub use self::win::*; 6 | use crate::draw::Draw; 7 | use crate::event::Event; 8 | use std::cmp::min; 9 | mod align; 10 | mod split; 11 | mod stack; 12 | mod util; 13 | mod win; 14 | 15 | /// Whether fixed size or percentage 16 | #[derive(Debug, Copy, Clone, Default)] 17 | pub enum Size { 18 | Fixed(usize), 19 | Percent(usize), 20 | #[default] 21 | Default, 22 | } 23 | 24 | impl Size { 25 | pub fn calc_fixed_size(&self, total_size: usize, default_size: usize) -> usize { 26 | match *self { 27 | Size::Fixed(fixed) => min(total_size, fixed), 28 | Size::Percent(percent) => min(total_size, total_size * percent / 100), 29 | Size::Default => default_size, 30 | } 31 | } 32 | } 33 | 34 | impl From for Size { 35 | fn from(size: usize) -> Self { 36 | Size::Fixed(size) 37 | } 38 | } 39 | 40 | #[derive(Copy, Clone, Debug)] 41 | pub struct Rectangle { 42 | pub top: usize, 43 | pub left: usize, 44 | pub width: usize, 45 | pub height: usize, 46 | } 47 | 48 | impl Rectangle { 49 | /// check if the given point(row, col) lies in the rectangle 50 | pub fn contains(&self, row: usize, col: usize) -> bool { 51 | if row < self.top || row >= self.top + self.height { 52 | false 53 | } else { 54 | !(col < self.left || col >= self.left + self.width) 55 | } 56 | } 57 | 58 | /// assume the point (row, col) lies in the rectangle, adjust the origin to the rectangle's 59 | /// origin (top, left) 60 | pub fn relative_to_origin(&self, row: usize, col: usize) -> (usize, usize) { 61 | (row - self.top, col - self.left) 62 | } 63 | 64 | pub fn adjust_origin(&self) -> Rectangle { 65 | Self { 66 | top: 0, 67 | left: 0, 68 | width: self.width, 69 | height: self.height, 70 | } 71 | } 72 | } 73 | 74 | /// A widget could be recursive nested 75 | pub trait Widget: Draw { 76 | /// the (width, height) of the content 77 | /// it will be the hint for layouts to calculate the final size 78 | fn size_hint(&self) -> (Option, Option) { 79 | (None, None) 80 | } 81 | 82 | /// given a key event, emit zero or more messages 83 | /// typical usage is the mouse click event where containers would pass the event down 84 | /// to their children. 85 | fn on_event(&self, event: Event, rect: Rectangle) -> Vec { 86 | let _ = (event, rect); // avoid warning 87 | Vec::new() 88 | } 89 | 90 | /// same as `on_event` except that the self reference is mutable 91 | fn on_event_mut(&mut self, event: Event, rect: Rectangle) -> Vec { 92 | let _ = (event, rect); // avoid warning 93 | Vec::new() 94 | } 95 | } 96 | 97 | impl> Widget for &T { 98 | fn size_hint(&self) -> (Option, Option) { 99 | (*self).size_hint() 100 | } 101 | 102 | fn on_event(&self, event: Event, rect: Rectangle) -> Vec { 103 | (*self).on_event(event, rect) 104 | } 105 | 106 | fn on_event_mut(&mut self, event: Event, rect: Rectangle) -> Vec { 107 | (**self).on_event(event, rect) 108 | } 109 | } 110 | 111 | impl> Widget for &mut T { 112 | fn size_hint(&self) -> (Option, Option) { 113 | (**self).size_hint() 114 | } 115 | 116 | fn on_event(&self, event: Event, rect: Rectangle) -> Vec { 117 | (**self).on_event(event, rect) 118 | } 119 | 120 | fn on_event_mut(&mut self, event: Event, rect: Rectangle) -> Vec { 121 | (**self).on_event_mut(event, rect) 122 | } 123 | } 124 | 125 | impl + ?Sized> Widget for Box { 126 | fn size_hint(&self) -> (Option, Option) { 127 | self.as_ref().size_hint() 128 | } 129 | 130 | fn on_event(&self, event: Event, rect: Rectangle) -> Vec { 131 | self.as_ref().on_event(event, rect) 132 | } 133 | 134 | fn on_event_mut(&mut self, event: Event, rect: Rectangle) -> Vec { 135 | self.as_mut().on_event_mut(event, rect) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /skim-tuikit/src/widget/stack.rs: -------------------------------------------------------------------------------- 1 | use crate::canvas::Canvas; 2 | use crate::draw::{Draw, DrawResult}; 3 | use crate::event::Event; 4 | use crate::widget::{Rectangle, Widget}; 5 | 6 | /// A stack of widgets, will draw the including widgets back to front 7 | pub struct Stack<'a, Message = ()> { 8 | inner: Vec + 'a>>, 9 | } 10 | 11 | impl Default for Stack<'_, Message> { 12 | fn default() -> Self { 13 | Self::new() 14 | } 15 | } 16 | 17 | impl<'a, Message> Stack<'a, Message> { 18 | pub fn new() -> Self { 19 | Self { inner: vec![] } 20 | } 21 | 22 | pub fn top(mut self, widget: impl Widget + 'a) -> Self { 23 | self.inner.push(Box::new(widget)); 24 | self 25 | } 26 | 27 | pub fn bottom(mut self, widget: impl Widget + 'a) -> Self { 28 | self.inner.insert(0, Box::new(widget)); 29 | self 30 | } 31 | } 32 | 33 | impl Draw for Stack<'_, Message> { 34 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 35 | for widget in self.inner.iter() { 36 | widget.draw(canvas)? 37 | } 38 | 39 | Ok(()) 40 | } 41 | fn draw_mut(&mut self, canvas: &mut dyn Canvas) -> DrawResult<()> { 42 | for widget in self.inner.iter_mut() { 43 | widget.draw_mut(canvas)? 44 | } 45 | 46 | Ok(()) 47 | } 48 | } 49 | 50 | impl Widget for Stack<'_, Message> { 51 | fn size_hint(&self) -> (Option, Option) { 52 | // max of the inner widgets 53 | let width = self 54 | .inner 55 | .iter() 56 | .map(|widget| widget.size_hint().0) 57 | .max() 58 | .unwrap_or(None); 59 | let height = self 60 | .inner 61 | .iter() 62 | .map(|widget| widget.size_hint().1) 63 | .max() 64 | .unwrap_or(None); 65 | (width, height) 66 | } 67 | 68 | fn on_event(&self, event: Event, rect: Rectangle) -> Vec { 69 | // like javascript's capture, from top to bottom 70 | for widget in self.inner.iter().rev() { 71 | let message = widget.on_event(event, rect); 72 | if !message.is_empty() { 73 | return message; 74 | } 75 | } 76 | vec![] 77 | } 78 | 79 | fn on_event_mut(&mut self, event: Event, rect: Rectangle) -> Vec { 80 | // like javascript's capture, from top to bottom 81 | for widget in self.inner.iter_mut().rev() { 82 | let message = widget.on_event_mut(event, rect); 83 | if !message.is_empty() { 84 | return message; 85 | } 86 | } 87 | vec![] 88 | } 89 | } 90 | 91 | #[cfg(test)] 92 | #[allow(dead_code)] 93 | mod test { 94 | use super::*; 95 | use crate::cell::Cell; 96 | use std::sync::Mutex; 97 | 98 | struct WinHint { 99 | pub width_hint: Option, 100 | pub height_hint: Option, 101 | } 102 | 103 | impl Draw for WinHint { 104 | fn draw(&self, _canvas: &mut dyn Canvas) -> DrawResult<()> { 105 | unimplemented!() 106 | } 107 | } 108 | 109 | impl Widget for WinHint { 110 | fn size_hint(&self) -> (Option, Option) { 111 | (self.width_hint, self.height_hint) 112 | } 113 | } 114 | 115 | #[test] 116 | fn size_hint() { 117 | let stack = Stack::new().top(WinHint { 118 | width_hint: None, 119 | height_hint: None, 120 | }); 121 | assert_eq!((None, None), stack.size_hint()); 122 | 123 | let stack = Stack::new().top(WinHint { 124 | width_hint: Some(1), 125 | height_hint: Some(1), 126 | }); 127 | assert_eq!((Some(1), Some(1)), stack.size_hint()); 128 | 129 | let stack = Stack::new() 130 | .top(WinHint { 131 | width_hint: Some(1), 132 | height_hint: Some(2), 133 | }) 134 | .top(WinHint { 135 | width_hint: Some(2), 136 | height_hint: Some(1), 137 | }); 138 | assert_eq!((Some(2), Some(2)), stack.size_hint()); 139 | 140 | let stack = Stack::new() 141 | .top(WinHint { 142 | width_hint: None, 143 | height_hint: None, 144 | }) 145 | .top(WinHint { 146 | width_hint: Some(2), 147 | height_hint: Some(1), 148 | }); 149 | assert_eq!((Some(2), Some(1)), stack.size_hint()); 150 | } 151 | 152 | #[derive(PartialEq, Debug)] 153 | enum Called { 154 | No, 155 | Mut, 156 | Immut, 157 | } 158 | 159 | struct Drawn { 160 | called: Mutex, 161 | } 162 | 163 | impl Draw for Drawn { 164 | fn draw(&self, _canvas: &mut dyn Canvas) -> DrawResult<()> { 165 | *self.called.lock().unwrap() = Called::Immut; 166 | Ok(()) 167 | } 168 | fn draw_mut(&mut self, _canvas: &mut dyn Canvas) -> DrawResult<()> { 169 | *self.called.lock().unwrap() = Called::Mut; 170 | Ok(()) 171 | } 172 | } 173 | 174 | impl Widget for Drawn {} 175 | 176 | #[derive(Default)] 177 | struct TestCanvas {} 178 | 179 | #[allow(unused_variables)] 180 | impl Canvas for TestCanvas { 181 | fn size(&self) -> crate::Result<(usize, usize)> { 182 | Ok((100, 100)) 183 | } 184 | 185 | fn clear(&mut self) -> crate::Result<()> { 186 | unimplemented!() 187 | } 188 | 189 | fn put_cell(&mut self, row: usize, col: usize, cell: Cell) -> crate::Result { 190 | Ok(1) 191 | } 192 | 193 | fn set_cursor(&mut self, row: usize, col: usize) -> crate::Result<()> { 194 | unimplemented!() 195 | } 196 | 197 | fn show_cursor(&mut self, show: bool) -> crate::Result<()> { 198 | unimplemented!() 199 | } 200 | } 201 | 202 | #[test] 203 | fn mutable_widget() { 204 | let mut canvas = TestCanvas::default(); 205 | 206 | let mut mutable = Drawn { 207 | called: Mutex::new(Called::No), 208 | }; 209 | { 210 | let mut stack = Stack::new().top(&mut mutable); 211 | stack.draw_mut(&mut canvas).unwrap(); 212 | } 213 | assert_eq!(Called::Mut, *mutable.called.lock().unwrap()); 214 | 215 | let immutable = Drawn { 216 | called: Mutex::new(Called::No), 217 | }; 218 | let stack = Stack::new().top(&immutable); 219 | stack.draw(&mut canvas).unwrap(); 220 | assert_eq!(Called::Immut, *immutable.called.lock().unwrap()); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /skim-tuikit/src/widget/util.rs: -------------------------------------------------------------------------------- 1 | use crate::event::Event; 2 | use crate::key::Key; 3 | use crate::widget::Rectangle; 4 | 5 | pub fn adjust_event(event: Event, inner_rect: Rectangle) -> Option { 6 | match event { 7 | Event::Key(Key::MousePress(button, row, col)) => { 8 | if inner_rect.contains(row as usize, col as usize) { 9 | let (row, col) = inner_rect.relative_to_origin(row as usize, col as usize); 10 | Some(Event::Key(Key::MousePress(button, row as u16, col as u16))) 11 | } else { 12 | None 13 | } 14 | } 15 | Event::Key(Key::MouseRelease(row, col)) => { 16 | if inner_rect.contains(row as usize, col as usize) { 17 | let (row, col) = inner_rect.relative_to_origin(row as usize, col as usize); 18 | Some(Event::Key(Key::MouseRelease(row as u16, col as u16))) 19 | } else { 20 | None 21 | } 22 | } 23 | Event::Key(Key::MouseHold(row, col)) => { 24 | if inner_rect.contains(row as usize, col as usize) { 25 | let (row, col) = inner_rect.relative_to_origin(row as usize, col as usize); 26 | Some(Event::Key(Key::MouseHold(row as u16, col as u16))) 27 | } else { 28 | None 29 | } 30 | } 31 | Event::Key(Key::SingleClick(button, row, col)) => { 32 | if inner_rect.contains(row as usize, col as usize) { 33 | let (row, col) = inner_rect.relative_to_origin(row as usize, col as usize); 34 | Some(Event::Key(Key::SingleClick(button, row as u16, col as u16))) 35 | } else { 36 | None 37 | } 38 | } 39 | Event::Key(Key::DoubleClick(button, row, col)) => { 40 | if inner_rect.contains(row as usize, col as usize) { 41 | let (row, col) = inner_rect.relative_to_origin(row as usize, col as usize); 42 | Some(Event::Key(Key::DoubleClick(button, row as u16, col as u16))) 43 | } else { 44 | None 45 | } 46 | } 47 | Event::Key(Key::WheelDown(row, col, count)) => { 48 | if inner_rect.contains(row as usize, col as usize) { 49 | let (row, col) = inner_rect.relative_to_origin(row as usize, col as usize); 50 | Some(Event::Key(Key::WheelDown(row as u16, col as u16, count))) 51 | } else { 52 | None 53 | } 54 | } 55 | Event::Key(Key::WheelUp(row, col, count)) => { 56 | if inner_rect.contains(row as usize, col as usize) { 57 | let (row, col) = inner_rect.relative_to_origin(row as usize, col as usize); 58 | Some(Event::Key(Key::WheelUp(row as u16, col as u16, count))) 59 | } else { 60 | None 61 | } 62 | } 63 | ev => Some(ev), 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /skim/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "skim" 3 | version = "0.18.0" 4 | authors = ["Zhang Jinzhou ", "Loric Andre"] 5 | description = "Fuzzy Finder in rust!" 6 | documentation = "https://docs.rs/skim" 7 | homepage = "https://github.com/skim-rs/skim" 8 | repository = "https://github.com/skim-rs/skim" 9 | readme = "../README.md" 10 | keywords = ["fuzzy", "menu", "util"] 11 | license = "MIT" 12 | edition = "2024" 13 | 14 | [lib] 15 | name = "skim" 16 | path = "src/lib.rs" 17 | 18 | [[bin]] 19 | name = "sk" 20 | path = "src/bin/main.rs" 21 | 22 | [dependencies] 23 | beef = { workspace = true } 24 | bitflags = { workspace = true } 25 | chrono = { workspace = true } 26 | clap = { workspace = true, optional = true, features = ["cargo", "derive", "unstable-markdown"] } 27 | clap_complete = { workspace = true, optional = true } 28 | crossbeam = { workspace = true } 29 | defer-drop = { workspace = true } 30 | derive_builder = { workspace = true } 31 | env_logger = { workspace = true, optional = true } 32 | fuzzy-matcher = { workspace = true } 33 | indexmap = { workspace = true } 34 | log = { workspace = true } 35 | nix = { workspace = true } 36 | rand = { workspace = true } 37 | rayon = { workspace = true } 38 | regex = { workspace = true } 39 | shell-quote = { workspace = true } 40 | shlex = { workspace = true, optional = true } 41 | skim-common = { path = "../skim-common/", version = "0.1.2" } 42 | skim-tuikit = { path = "../skim-tuikit/", version = "0.6.2" } 43 | time = { workspace = true } 44 | timer = { workspace = true } 45 | unicode-width = { workspace = true } 46 | vte = { workspace = true } 47 | which = { workspace = true } 48 | 49 | [features] 50 | default = ["cli"] 51 | cli = ["dep:clap", "dep:clap_complete", "dep:shlex", "dep:env_logger"] 52 | -------------------------------------------------------------------------------- /skim/examples/cmd_collector.rs: -------------------------------------------------------------------------------- 1 | extern crate skim; 2 | use reader::CommandCollector; 3 | use skim::prelude::*; 4 | 5 | struct BasicSkimItem { 6 | value: String, 7 | } 8 | 9 | impl SkimItem for BasicSkimItem { 10 | fn text(&self) -> Cow { 11 | Cow::Borrowed(&self.value) 12 | } 13 | } 14 | 15 | struct BasicCmdCollector { 16 | pub items: Vec, 17 | } 18 | 19 | impl CommandCollector for BasicCmdCollector { 20 | fn invoke(&mut self, _cmd: &str, _components_to_stop: Arc) -> (SkimItemReceiver, Sender) { 21 | let (tx, rx) = unbounded(); 22 | let (tx_interrupt, _rx_interrupt) = unbounded(); 23 | while let Some(value) = self.items.pop() { 24 | let item = BasicSkimItem { value }; 25 | tx.send(Arc::from(item) as Arc).unwrap(); 26 | } 27 | 28 | (rx, tx_interrupt) 29 | } 30 | } 31 | 32 | pub fn main() { 33 | let cmd_collector = BasicCmdCollector { 34 | items: vec![String::from("foo"), String::from("bar"), String::from("baz")], 35 | }; 36 | let options = SkimOptionsBuilder::default() 37 | .cmd_collector(Rc::from(RefCell::from(cmd_collector))) 38 | .build() 39 | .unwrap(); 40 | 41 | let selected_items = Skim::run_with(&options, None) 42 | .map(|out| out.selected_items) 43 | .unwrap_or_default(); 44 | 45 | for item in selected_items.iter() { 46 | println!("{}", item.output()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /skim/examples/custom_item.rs: -------------------------------------------------------------------------------- 1 | extern crate skim; 2 | use skim::prelude::*; 3 | 4 | struct MyItem { 5 | inner: String, 6 | } 7 | 8 | impl SkimItem for MyItem { 9 | fn text(&self) -> Cow { 10 | Cow::Borrowed(&self.inner) 11 | } 12 | 13 | fn preview(&self, _context: PreviewContext) -> ItemPreview { 14 | if self.inner.starts_with("color") { 15 | ItemPreview::AnsiText(format!("\x1b[31mhello:\x1b[m\n{}", self.inner)) 16 | } else { 17 | ItemPreview::Text(format!("hello:\n{}", self.inner)) 18 | } 19 | } 20 | } 21 | 22 | pub fn main() { 23 | let options = SkimOptionsBuilder::default() 24 | .height(String::from("50%")) 25 | .multi(true) 26 | .preview(Some(String::new())) // preview should be specified to enable preview window 27 | .build() 28 | .unwrap(); 29 | 30 | let (tx_item, rx_item): (SkimItemSender, SkimItemReceiver) = unbounded(); 31 | let _ = tx_item.send(Arc::new(MyItem { 32 | inner: "color aaaa".to_string(), 33 | })); 34 | let _ = tx_item.send(Arc::new(MyItem { 35 | inner: "bbbb".to_string(), 36 | })); 37 | let _ = tx_item.send(Arc::new(MyItem { 38 | inner: "ccc".to_string(), 39 | })); 40 | drop(tx_item); // so that skim could know when to stop waiting for more items. 41 | 42 | let selected_items = Skim::run_with(&options, Some(rx_item)) 43 | .map(|out| out.selected_items) 44 | .unwrap_or_default(); 45 | 46 | for item in selected_items.iter() { 47 | println!("{}", item.output()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /skim/examples/custom_keybinding_actions.rs: -------------------------------------------------------------------------------- 1 | extern crate skim; 2 | use skim::prelude::*; 3 | 4 | // No action is actually performed on your filesystem! 5 | // This example only produce friendly print statements! 6 | 7 | fn fake_delete_item(item: &str) { 8 | println!("Deleting item `{}`...", item); 9 | } 10 | 11 | fn fake_create_item(item: &str) { 12 | println!("Creating a new item `{}`...", item); 13 | } 14 | 15 | pub fn main() { 16 | // Note: `accept` is a keyword used define custom actions. 17 | // For full list of accepted keywords see `parse_event` in `src/event.rs`. 18 | // `delete` and `create` are arbitrary keywords used for this example. 19 | let options = SkimOptionsBuilder::default() 20 | .multi(true) 21 | .bind(vec![String::from("bs:abort"), String::from("Enter:accept")]) 22 | .build() 23 | .unwrap(); 24 | 25 | if let Some(out) = Skim::run_with(&options, None) { 26 | match out.final_key { 27 | // Delete each selected item 28 | Key::Backspace => out.selected_items.iter().for_each(|i| fake_delete_item(&i.text())), 29 | // Create a new item based on the query 30 | Key::Enter => fake_create_item(out.query.as_ref()), 31 | _ => (), 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /skim/examples/downcast.rs: -------------------------------------------------------------------------------- 1 | extern crate skim; 2 | use skim::prelude::*; 3 | 4 | /// This example illustrates downcasting custom structs that implement 5 | /// `SkimItem` after calling `Skim::run_with`. 6 | 7 | #[derive(Debug, Clone)] 8 | struct Item { 9 | text: String, 10 | index: usize, 11 | } 12 | 13 | impl SkimItem for Item { 14 | fn text(&self) -> Cow { 15 | Cow::Borrowed(&self.text) 16 | } 17 | 18 | fn preview(&self, _context: PreviewContext) -> ItemPreview { 19 | ItemPreview::Text(self.text.to_owned()) 20 | } 21 | 22 | fn get_index(&self) -> usize { 23 | self.index 24 | } 25 | 26 | fn set_index(&mut self, index: usize) { 27 | self.index = index 28 | } 29 | } 30 | 31 | pub fn main() { 32 | let options = SkimOptionsBuilder::default() 33 | .height(String::from("50%")) 34 | .multi(true) 35 | .preview(Some(String::new())) 36 | .build() 37 | .unwrap(); 38 | 39 | let (tx, rx): (SkimItemSender, SkimItemReceiver) = unbounded(); 40 | 41 | tx.send(Arc::new(Item { 42 | text: "a".to_string(), 43 | index: 0, 44 | })) 45 | .unwrap(); 46 | tx.send(Arc::new(Item { 47 | text: "b".to_string(), 48 | index: 1, 49 | })) 50 | .unwrap(); 51 | tx.send(Arc::new(Item { 52 | text: "c".to_string(), 53 | index: 2, 54 | })) 55 | .unwrap(); 56 | 57 | drop(tx); 58 | 59 | let selected_items = Skim::run_with(&options, Some(rx)) 60 | .map(|out| out.selected_items) 61 | .unwrap_or_default() 62 | .iter() 63 | .map(|selected_item| (**selected_item).as_any().downcast_ref::().unwrap().to_owned()) 64 | .collect::>(); 65 | 66 | for item in selected_items { 67 | println!("{:?}", item); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /skim/examples/nth.rs: -------------------------------------------------------------------------------- 1 | extern crate skim; 2 | use skim::prelude::*; 3 | use std::io::Cursor; 4 | 5 | /// `nth` option is supported by SkimItemReader. 6 | /// In the example below, with `nth=2` set, only `123` could be matched. 7 | pub fn main() { 8 | let input = "foo 123"; 9 | 10 | let options = SkimOptionsBuilder::default() 11 | .query(Some(String::from("f"))) 12 | .build() 13 | .unwrap(); 14 | let item_reader = SkimItemReader::new(SkimItemReaderOption::default().nth(vec!["2"].into_iter()).build()); 15 | 16 | let items = item_reader.of_bufread(Cursor::new(input)); 17 | let selected_items = Skim::run_with(&options, Some(items)) 18 | .map(|out| out.selected_items) 19 | .unwrap_or_default(); 20 | 21 | for item in selected_items.iter() { 22 | println!("{}", item.output()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /skim/examples/option_builder.rs: -------------------------------------------------------------------------------- 1 | extern crate skim; 2 | use skim::prelude::*; 3 | use std::io::Cursor; 4 | 5 | pub fn main() { 6 | let options = SkimOptionsBuilder::default() 7 | .height(String::from("50%")) 8 | .multi(true) 9 | .build() 10 | .unwrap(); 11 | let item_reader = SkimItemReader::default(); 12 | 13 | //================================================== 14 | // first run 15 | let input = "aaaaa\nbbbb\nccc"; 16 | let items = item_reader.of_bufread(Cursor::new(input)); 17 | let selected_items = Skim::run_with(&options, Some(items)) 18 | .map(|out| out.selected_items) 19 | .unwrap_or_default(); 20 | 21 | for item in selected_items.iter() { 22 | println!("{}", item.output()); 23 | } 24 | 25 | //================================================== 26 | // second run 27 | let input = "11111\n22222\n333333333"; 28 | let items = item_reader.of_bufread(Cursor::new(input)); 29 | let selected_items = Skim::run_with(&options, Some(items)) 30 | .map(|out| out.selected_items) 31 | .unwrap_or_default(); 32 | 33 | for item in selected_items.iter() { 34 | println!("{}", item.output()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /skim/examples/preview_callback.rs: -------------------------------------------------------------------------------- 1 | use std::io::Cursor; 2 | 3 | use skim::prelude::*; 4 | 5 | pub fn main() { 6 | env_logger::init(); 7 | let options = SkimOptionsBuilder::default() 8 | .multi(true) 9 | .preview_fn(Some(PreviewCallback::from(|items: Vec>| { 10 | items 11 | .iter() 12 | .map(|s| s.text().to_ascii_uppercase().into()) 13 | .collect::>() 14 | }))) 15 | .build() 16 | .unwrap(); 17 | let item_reader = SkimItemReader::default(); 18 | 19 | let input = "aaaaa\nbbbb\nccc"; 20 | let items = item_reader.of_bufread(Cursor::new(input)); 21 | let selected_items = Skim::run_with(&options, Some(items)) 22 | .map(|out| out.selected_items) 23 | .unwrap_or_default(); 24 | 25 | for item in selected_items.iter() { 26 | println!("{}", item.output()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /skim/examples/sample.rs: -------------------------------------------------------------------------------- 1 | extern crate skim; 2 | use skim::prelude::*; 3 | 4 | pub fn main() { 5 | let options = SkimOptions::default(); 6 | 7 | let selected_items = Skim::run_with(&options, None) 8 | .map(|out| out.selected_items) 9 | .unwrap_or_default(); 10 | 11 | for item in selected_items.iter() { 12 | println!("{}", item.output()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /skim/examples/selector.rs: -------------------------------------------------------------------------------- 1 | extern crate skim; 2 | use skim::prelude::*; 3 | 4 | struct BasicSelector { 5 | pub pat: String, 6 | } 7 | 8 | impl Selector for BasicSelector { 9 | fn should_select(&self, _index: usize, item: &dyn SkimItem) -> bool { 10 | item.text().contains(&self.pat) 11 | } 12 | } 13 | 14 | pub fn main() { 15 | let selector = BasicSelector { 16 | pat: String::from("examples"), 17 | }; 18 | let options = SkimOptionsBuilder::default() 19 | .multi(true) 20 | .selector(Some(Rc::from(selector))) 21 | .query(Some(String::from("skim/"))) 22 | .build() 23 | .unwrap(); 24 | 25 | let selected_items = Skim::run_with(&options, None) 26 | .map(|out| out.selected_items) 27 | .unwrap_or_default(); 28 | 29 | for item in selected_items.iter() { 30 | println!("{}", item.output()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /skim/examples/tuikit.rs: -------------------------------------------------------------------------------- 1 | use skim::tuikit::prelude::*; 2 | use std::cmp::{max, min}; 3 | 4 | fn main() { 5 | let term: Term<()> = Term::with_height(TermHeight::Percent(30)).unwrap(); 6 | let mut row = 1; 7 | let mut col = 0; 8 | 9 | let _ = term.print(0, 0, "press arrow key to move the text, (q) to quit"); 10 | let _ = term.present(); 11 | 12 | while let Ok(ev) = term.poll_event() { 13 | let _ = term.clear(); 14 | let _ = term.print(0, 0, "press arrow key to move the text, (q) to quit"); 15 | 16 | let (width, height) = term.term_size().unwrap(); 17 | match ev { 18 | Event::Key(Key::ESC) | Event::Key(Key::Char('q')) | Event::Key(Key::Ctrl('c')) => break, 19 | Event::Key(Key::Up) => row = max(row - 1, 1), 20 | Event::Key(Key::Down) => row = min(row + 1, height - 1), 21 | Event::Key(Key::Left) => col = max(col, 1) - 1, 22 | Event::Key(Key::Right) => col = min(col + 1, width - 1), 23 | _ => {} 24 | } 25 | 26 | let _ = term.print_with_attr(row, col, "Hello World! 你好!今日は。", Color::RED); 27 | let _ = term.set_cursor(row, col); 28 | let _ = term.present(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /skim/src/engine/all.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Error, Formatter}; 2 | use std::sync::Arc; 3 | 4 | use crate::item::RankBuilder; 5 | use crate::{MatchEngine, MatchRange, MatchResult, SkimItem}; 6 | 7 | //------------------------------------------------------------------------------ 8 | #[derive(Debug)] 9 | pub struct MatchAllEngine { 10 | rank_builder: Arc, 11 | } 12 | 13 | impl MatchAllEngine { 14 | pub fn builder() -> Self { 15 | Self { 16 | rank_builder: Default::default(), 17 | } 18 | } 19 | 20 | pub fn rank_builder(mut self, rank_builder: Arc) -> Self { 21 | self.rank_builder = rank_builder; 22 | self 23 | } 24 | 25 | pub fn build(self) -> Self { 26 | self 27 | } 28 | } 29 | 30 | impl MatchEngine for MatchAllEngine { 31 | fn match_item(&self, item: Arc) -> Option { 32 | let item_len = item.text().len(); 33 | Some(MatchResult { 34 | rank: self.rank_builder.build_rank(0, 0, 0, item_len, item.get_index()), 35 | matched_range: MatchRange::ByteRange(0, 0), 36 | }) 37 | } 38 | } 39 | 40 | impl Display for MatchAllEngine { 41 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 42 | write!(f, "Noop") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /skim/src/engine/andor.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Error, Formatter}; 2 | use std::sync::Arc; 3 | 4 | use crate::{MatchEngine, MatchRange, MatchResult, SkimItem}; 5 | 6 | //------------------------------------------------------------------------------ 7 | // OrEngine, a combinator 8 | pub struct OrEngine { 9 | engines: Vec>, 10 | } 11 | 12 | impl OrEngine { 13 | pub fn builder() -> Self { 14 | Self { engines: vec![] } 15 | } 16 | 17 | pub fn engines(mut self, mut engines: Vec>) -> Self { 18 | self.engines.append(&mut engines); 19 | self 20 | } 21 | 22 | pub fn build(self) -> Self { 23 | self 24 | } 25 | } 26 | 27 | impl MatchEngine for OrEngine { 28 | fn match_item(&self, item: Arc) -> Option { 29 | for engine in &self.engines { 30 | let result = engine.match_item(Arc::clone(&item)); 31 | if result.is_some() { 32 | return result; 33 | } 34 | } 35 | 36 | None 37 | } 38 | } 39 | 40 | impl Display for OrEngine { 41 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 42 | write!( 43 | f, 44 | "(Or: {})", 45 | self.engines 46 | .iter() 47 | .map(|e| format!("{}", e)) 48 | .collect::>() 49 | .join(", ") 50 | ) 51 | } 52 | } 53 | 54 | //------------------------------------------------------------------------------ 55 | // AndEngine, a combinator 56 | pub struct AndEngine { 57 | engines: Vec>, 58 | } 59 | 60 | impl AndEngine { 61 | pub fn builder() -> Self { 62 | Self { engines: vec![] } 63 | } 64 | 65 | pub fn engines(mut self, mut engines: Vec>) -> Self { 66 | self.engines.append(&mut engines); 67 | self 68 | } 69 | 70 | pub fn build(self) -> Self { 71 | self 72 | } 73 | 74 | fn merge_matched_items(&self, items: Vec, text: &str) -> MatchResult { 75 | let rank = items[0].rank; 76 | let mut ranges = vec![]; 77 | for item in items { 78 | match item.matched_range { 79 | MatchRange::ByteRange(..) => { 80 | ranges.extend(item.range_char_indices(text)); 81 | } 82 | MatchRange::Chars(vec) => { 83 | ranges.extend(vec.iter()); 84 | } 85 | } 86 | } 87 | 88 | ranges.sort(); 89 | ranges.dedup(); 90 | MatchResult { 91 | rank, 92 | matched_range: MatchRange::Chars(ranges), 93 | } 94 | } 95 | } 96 | 97 | impl MatchEngine for AndEngine { 98 | fn match_item(&self, item: Arc) -> Option { 99 | // mock 100 | let mut results = vec![]; 101 | for engine in &self.engines { 102 | let result = engine.match_item(Arc::clone(&item))?; 103 | results.push(result); 104 | } 105 | 106 | if results.is_empty() { 107 | None 108 | } else { 109 | Some(self.merge_matched_items(results, &item.text())) 110 | } 111 | } 112 | } 113 | 114 | impl Display for AndEngine { 115 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 116 | write!( 117 | f, 118 | "(And: {})", 119 | self.engines 120 | .iter() 121 | .map(|e| format!("{}", e)) 122 | .collect::>() 123 | .join(", ") 124 | ) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /skim/src/engine/exact.rs: -------------------------------------------------------------------------------- 1 | use crate::engine::util::{contains_upper, regex_match}; 2 | use crate::item::RankBuilder; 3 | use crate::{CaseMatching, MatchEngine, MatchRange, MatchResult, SkimItem}; 4 | use regex::{Regex, escape}; 5 | use std::cmp::min; 6 | use std::fmt::{Display, Error, Formatter}; 7 | use std::sync::Arc; 8 | 9 | //------------------------------------------------------------------------------ 10 | // Exact engine 11 | #[derive(Debug, Copy, Clone, Default)] 12 | pub struct ExactMatchingParam { 13 | pub prefix: bool, 14 | pub postfix: bool, 15 | pub inverse: bool, 16 | pub case: CaseMatching, 17 | __non_exhaustive: bool, 18 | } 19 | 20 | #[derive(Debug)] 21 | pub struct ExactEngine { 22 | #[allow(dead_code)] 23 | query: String, 24 | query_regex: Option, 25 | rank_builder: Arc, 26 | inverse: bool, 27 | } 28 | 29 | impl ExactEngine { 30 | pub fn builder(query: &str, param: ExactMatchingParam) -> Self { 31 | let case_sensitive = match param.case { 32 | CaseMatching::Respect => true, 33 | CaseMatching::Ignore => false, 34 | CaseMatching::Smart => contains_upper(query), 35 | }; 36 | 37 | let mut query_builder = String::new(); 38 | if !case_sensitive { 39 | query_builder.push_str("(?i)"); 40 | } 41 | 42 | if param.prefix { 43 | query_builder.push('^'); 44 | } 45 | 46 | query_builder.push_str(&escape(query)); 47 | 48 | if param.postfix { 49 | query_builder.push('$'); 50 | } 51 | 52 | let query_regex = if query.is_empty() { 53 | None 54 | } else { 55 | Regex::new(&query_builder).ok() 56 | }; 57 | 58 | ExactEngine { 59 | query: query.to_string(), 60 | query_regex, 61 | rank_builder: Default::default(), 62 | inverse: param.inverse, 63 | } 64 | } 65 | 66 | pub fn rank_builder(mut self, rank_builder: Arc) -> Self { 67 | self.rank_builder = rank_builder; 68 | self 69 | } 70 | 71 | pub fn build(self) -> Self { 72 | self 73 | } 74 | } 75 | 76 | impl MatchEngine for ExactEngine { 77 | fn match_item(&self, item: Arc) -> Option { 78 | let mut matched_result = None; 79 | let item_text = item.text(); 80 | let default_range = [(0, item_text.len())]; 81 | for &(start, end) in item.get_matching_ranges().unwrap_or(&default_range) { 82 | let start = min(start, item_text.len()); 83 | let end = min(end, item_text.len()); 84 | if self.query_regex.is_none() { 85 | matched_result = Some((0, 0)); 86 | break; 87 | } 88 | 89 | matched_result = 90 | regex_match(&item_text[start..end], &self.query_regex).map(|(s, e)| (s + start, e + start)); 91 | 92 | if self.inverse { 93 | matched_result = matched_result.xor(Some((0, 0))) 94 | } 95 | 96 | if matched_result.is_some() { 97 | break; 98 | } 99 | } 100 | 101 | let (begin, end) = matched_result?; 102 | let score = (end - begin) as i32; 103 | let item_len = item_text.len(); 104 | Some(MatchResult { 105 | rank: self 106 | .rank_builder 107 | .build_rank(score, begin, end, item_len, item.get_index()), 108 | matched_range: MatchRange::ByteRange(begin, end), 109 | }) 110 | } 111 | } 112 | 113 | impl Display for ExactEngine { 114 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 115 | write!( 116 | f, 117 | "(Exact|{}{})", 118 | if self.inverse { "!" } else { "" }, 119 | self.query_regex.as_ref().map(|x| x.as_str()).unwrap_or("") 120 | ) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /skim/src/engine/fuzzy.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::min; 2 | use std::fmt::{Display, Error, Formatter}; 3 | use std::sync::Arc; 4 | 5 | use clap::ValueEnum; 6 | use fuzzy_matcher::FuzzyMatcher; 7 | use fuzzy_matcher::clangd::ClangdMatcher; 8 | use fuzzy_matcher::skim::SkimMatcherV2; 9 | 10 | use crate::item::RankBuilder; 11 | use crate::{CaseMatching, MatchEngine}; 12 | use crate::{MatchRange, MatchResult, SkimItem}; 13 | 14 | //------------------------------------------------------------------------------ 15 | #[derive(ValueEnum, Debug, Copy, Clone, Default)] 16 | #[clap(rename_all = "snake_case")] 17 | pub enum FuzzyAlgorithm { 18 | SkimV1, 19 | #[default] 20 | SkimV2, 21 | Clangd, 22 | } 23 | 24 | const BYTES_1M: usize = 1024 * 1024 * 1024; 25 | 26 | //------------------------------------------------------------------------------ 27 | // Fuzzy engine 28 | #[derive(Default)] 29 | pub struct FuzzyEngineBuilder { 30 | query: String, 31 | case: CaseMatching, 32 | algorithm: FuzzyAlgorithm, 33 | rank_builder: Arc, 34 | } 35 | 36 | impl FuzzyEngineBuilder { 37 | pub fn query(mut self, query: &str) -> Self { 38 | self.query = query.to_string(); 39 | self 40 | } 41 | 42 | pub fn case(mut self, case: CaseMatching) -> Self { 43 | self.case = case; 44 | self 45 | } 46 | 47 | pub fn algorithm(mut self, algorithm: FuzzyAlgorithm) -> Self { 48 | self.algorithm = algorithm; 49 | self 50 | } 51 | 52 | pub fn rank_builder(mut self, rank_builder: Arc) -> Self { 53 | self.rank_builder = rank_builder; 54 | self 55 | } 56 | 57 | #[allow(deprecated)] 58 | pub fn build(self) -> FuzzyEngine { 59 | use fuzzy_matcher::skim::SkimMatcher; 60 | let matcher: Box = match self.algorithm { 61 | FuzzyAlgorithm::SkimV1 => Box::new(SkimMatcher::default()), 62 | FuzzyAlgorithm::SkimV2 => { 63 | let matcher = SkimMatcherV2::default().element_limit(BYTES_1M); 64 | let matcher = match self.case { 65 | CaseMatching::Respect => matcher.respect_case(), 66 | CaseMatching::Ignore => matcher.ignore_case(), 67 | CaseMatching::Smart => matcher.smart_case(), 68 | }; 69 | Box::new(matcher) 70 | } 71 | FuzzyAlgorithm::Clangd => { 72 | let matcher = ClangdMatcher::default(); 73 | let matcher = match self.case { 74 | CaseMatching::Respect => matcher.respect_case(), 75 | CaseMatching::Ignore => matcher.ignore_case(), 76 | CaseMatching::Smart => matcher.smart_case(), 77 | }; 78 | Box::new(matcher) 79 | } 80 | }; 81 | 82 | FuzzyEngine { 83 | matcher, 84 | query: self.query, 85 | rank_builder: self.rank_builder, 86 | } 87 | } 88 | } 89 | 90 | pub struct FuzzyEngine { 91 | query: String, 92 | matcher: Box, 93 | rank_builder: Arc, 94 | } 95 | 96 | impl FuzzyEngine { 97 | pub fn builder() -> FuzzyEngineBuilder { 98 | FuzzyEngineBuilder::default() 99 | } 100 | 101 | fn fuzzy_match(&self, choice: &str, pattern: &str) -> Option<(i64, Vec)> { 102 | if pattern.is_empty() { 103 | return Some((0, Vec::new())); 104 | } else if choice.is_empty() { 105 | return None; 106 | } 107 | 108 | self.matcher.fuzzy_indices(choice, pattern) 109 | } 110 | } 111 | 112 | impl MatchEngine for FuzzyEngine { 113 | fn match_item(&self, item: Arc) -> Option { 114 | // iterate over all matching fields: 115 | let mut matched_result = None; 116 | let item_text = item.text(); 117 | let default_range = [(0, item_text.len())]; 118 | for &(start, end) in item.get_matching_ranges().unwrap_or(&default_range) { 119 | let start = min(start, item_text.len()); 120 | let end = min(end, item_text.len()); 121 | matched_result = self.fuzzy_match(&item_text[start..end], &self.query).map(|(s, vec)| { 122 | if start != 0 { 123 | let start_char = &item_text[..start].chars().count(); 124 | (s, vec.iter().map(|x| x + start_char).collect()) 125 | } else { 126 | (s, vec) 127 | } 128 | }); 129 | 130 | if matched_result.is_some() { 131 | break; 132 | } 133 | } 134 | 135 | matched_result.as_ref()?; 136 | 137 | let (score, matched_range) = matched_result.unwrap(); 138 | 139 | trace!("matched range {:?}", matched_range); 140 | let begin = *matched_range.first().unwrap_or(&0); 141 | let end = *matched_range.last().unwrap_or(&0); 142 | 143 | let item_len = item_text.len(); 144 | Some(MatchResult { 145 | rank: self 146 | .rank_builder 147 | .build_rank(score as i32, begin, end, item_len, item.get_index()), 148 | matched_range: MatchRange::Chars(matched_range), 149 | }) 150 | } 151 | } 152 | 153 | impl Display for FuzzyEngine { 154 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 155 | write!(f, "(Fuzzy: {})", self.query) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /skim/src/engine/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod all; 2 | pub mod andor; 3 | pub mod exact; 4 | pub mod factory; 5 | pub mod fuzzy; 6 | pub mod regexp; 7 | mod util; 8 | -------------------------------------------------------------------------------- /skim/src/engine/regexp.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Error, Formatter}; 2 | use std::sync::Arc; 3 | 4 | use regex::Regex; 5 | 6 | use crate::engine::util::regex_match; 7 | use crate::item::RankBuilder; 8 | use crate::{CaseMatching, MatchEngine}; 9 | use crate::{MatchRange, MatchResult, SkimItem}; 10 | use std::cmp::min; 11 | 12 | //------------------------------------------------------------------------------ 13 | // Regular Expression engine 14 | #[derive(Debug)] 15 | pub struct RegexEngine { 16 | query_regex: Option, 17 | rank_builder: Arc, 18 | } 19 | 20 | impl RegexEngine { 21 | pub fn builder(query: &str, case: CaseMatching) -> Self { 22 | let mut query_builder = String::new(); 23 | 24 | match case { 25 | CaseMatching::Respect => {} 26 | CaseMatching::Ignore => query_builder.push_str("(?i)"), 27 | CaseMatching::Smart => {} 28 | } 29 | 30 | query_builder.push_str(query); 31 | 32 | RegexEngine { 33 | query_regex: Regex::new(&query_builder).ok(), 34 | rank_builder: Default::default(), 35 | } 36 | } 37 | 38 | pub fn rank_builder(mut self, rank_builder: Arc) -> Self { 39 | self.rank_builder = rank_builder; 40 | self 41 | } 42 | 43 | pub fn build(self) -> Self { 44 | self 45 | } 46 | } 47 | 48 | impl MatchEngine for RegexEngine { 49 | fn match_item(&self, item: Arc) -> Option { 50 | let mut matched_result = None; 51 | let item_text = item.text(); 52 | let default_range = [(0, item_text.len())]; 53 | for &(start, end) in item.get_matching_ranges().unwrap_or(&default_range) { 54 | let start = min(start, item_text.len()); 55 | let end = min(end, item_text.len()); 56 | if self.query_regex.is_none() { 57 | matched_result = Some((0, 0)); 58 | break; 59 | } 60 | 61 | matched_result = 62 | regex_match(&item_text[start..end], &self.query_regex).map(|(s, e)| (s + start, e + start)); 63 | 64 | if matched_result.is_some() { 65 | break; 66 | } 67 | } 68 | 69 | let (begin, end) = matched_result?; 70 | let score = (end - begin) as i32; 71 | let item_len = item_text.len(); 72 | 73 | Some(MatchResult { 74 | rank: self 75 | .rank_builder 76 | .build_rank(score, begin, end, item_len, item.get_index()), 77 | matched_range: MatchRange::ByteRange(begin, end), 78 | }) 79 | } 80 | } 81 | 82 | impl Display for RegexEngine { 83 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 84 | write!( 85 | f, 86 | "(Regex: {})", 87 | self.query_regex 88 | .as_ref() 89 | .map_or("".to_string(), |re| re.as_str().to_string()) 90 | ) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /skim/src/engine/util.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | 3 | pub fn regex_match(choice: &str, pattern: &Option) -> Option<(usize, usize)> { 4 | match *pattern { 5 | Some(ref pat) => { 6 | let mat = pat.find(choice)?; 7 | Some((mat.start(), mat.end())) 8 | } 9 | None => None, 10 | } 11 | } 12 | 13 | pub fn contains_upper(string: &str) -> bool { 14 | for ch in string.chars() { 15 | if ch.is_ascii_uppercase() { 16 | return true; 17 | } 18 | } 19 | false 20 | } 21 | -------------------------------------------------------------------------------- /skim/src/global.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::atomic::{AtomicU32, Ordering}; 3 | use std::sync::{LazyLock, Mutex}; 4 | 5 | // Consider that you invoke a command with different arguments several times 6 | // If you select some items each time, how will skim remember it? 7 | // => Well, we'll give each invocation a number, i.e. RUN_NUM 8 | // What if you invoke the same command and same arguments twice? 9 | // => We use NUM_MAP to specify the same run number. 10 | static RUN_NUM: LazyLock = LazyLock::new(|| AtomicU32::new(0)); 11 | static SEQ: LazyLock = LazyLock::new(|| AtomicU32::new(1)); 12 | static NUM_MAP: LazyLock>> = LazyLock::new(|| { 13 | let mut m = HashMap::new(); 14 | m.insert("".to_string(), 0); 15 | Mutex::new(m) 16 | }); 17 | 18 | pub fn current_run_num() -> u32 { 19 | RUN_NUM.load(Ordering::SeqCst) 20 | } 21 | 22 | pub fn mark_new_run(query: &str) -> u32 { 23 | let mut map = NUM_MAP.lock().expect("failed to lock NUM_MAP"); 24 | let query = query.to_string(); 25 | let run_num = *map.entry(query).or_insert_with(|| SEQ.fetch_add(1, Ordering::SeqCst)); 26 | RUN_NUM.store(run_num, Ordering::SeqCst); 27 | run_num 28 | } 29 | 30 | #[cfg(test)] 31 | mod tests { 32 | use super::*; 33 | 34 | #[test] 35 | fn test() { 36 | assert_eq!(0, current_run_num()); 37 | mark_new_run("a"); 38 | assert_eq!(1, current_run_num()); 39 | mark_new_run("b"); 40 | assert_eq!(2, current_run_num()); 41 | mark_new_run("a"); 42 | assert_eq!(1, current_run_num()); 43 | mark_new_run(""); 44 | assert_eq!(0, current_run_num()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /skim/src/header.rs: -------------------------------------------------------------------------------- 1 | //! header of the items 2 | use crate::ansi::{ANSIParser, AnsiString}; 3 | use crate::event::UpdateScreen; 4 | use crate::event::{Event, EventHandler}; 5 | use crate::item::ItemPool; 6 | use crate::theme::ColorTheme; 7 | use crate::theme::DEFAULT_THEME; 8 | use crate::util::{LinePrinter, clear_canvas, print_item, str_lines}; 9 | use crate::{DisplayContext, Matches, SkimOptions}; 10 | use defer_drop::DeferDrop; 11 | use skim_tuikit::prelude::*; 12 | use std::cmp::max; 13 | use std::sync::Arc; 14 | 15 | pub struct Header { 16 | header: Vec>, 17 | tabstop: usize, 18 | reverse: bool, 19 | theme: Arc, 20 | 21 | // for reserved header items 22 | item_pool: Arc>, 23 | } 24 | 25 | impl Header { 26 | pub fn empty() -> Self { 27 | Self { 28 | header: vec![], 29 | tabstop: 8, 30 | reverse: false, 31 | theme: Arc::new(*DEFAULT_THEME), 32 | item_pool: Arc::new(DeferDrop::new(ItemPool::new())), 33 | } 34 | } 35 | 36 | pub fn item_pool(mut self, item_pool: Arc>) -> Self { 37 | self.item_pool = item_pool; 38 | self 39 | } 40 | 41 | pub fn theme(mut self, theme: Arc) -> Self { 42 | self.theme = theme; 43 | self 44 | } 45 | 46 | pub fn with_options(mut self, options: &SkimOptions) -> Self { 47 | self.tabstop = max(1, options.tabstop); 48 | 49 | if options.layout.starts_with("reverse") { 50 | self.reverse = true; 51 | } 52 | 53 | match &options.header { 54 | None => {} 55 | Some(header) => { 56 | let mut parser = ANSIParser::default(); 57 | if !header.is_empty() { 58 | self.header = str_lines(header).into_iter().map(|l| parser.parse_ansi(l)).collect(); 59 | } 60 | } 61 | } 62 | self 63 | } 64 | 65 | fn lines_of_header(&self) -> usize { 66 | self.header.len() + self.item_pool.reserved().len() 67 | } 68 | 69 | fn adjust_row(&self, index: usize, screen_height: usize) -> usize { 70 | if self.reverse { index } else { screen_height - index - 1 } 71 | } 72 | } 73 | 74 | impl Draw for Header { 75 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 76 | let (screen_width, screen_height) = canvas.size()?; 77 | if screen_width < 3 { 78 | return Err("screen width is too small".into()); 79 | } 80 | 81 | if screen_height < self.lines_of_header() { 82 | return Err("screen height is too small".into()); 83 | } 84 | 85 | canvas.clear()?; 86 | clear_canvas(canvas)?; 87 | 88 | for (idx, header) in self.header.iter().enumerate() { 89 | // print fixed header(specified by --header) 90 | let mut printer = LinePrinter::builder() 91 | .row(self.adjust_row(idx, screen_height)) 92 | .col(2) 93 | .tabstop(self.tabstop) 94 | .container_width(screen_width - 2) 95 | .shift(0) 96 | .text_width(screen_width - 2) 97 | .build(); 98 | 99 | for (ch, _attr) in header.iter() { 100 | printer.print_char(canvas, ch, self.theme.header(), false); 101 | } 102 | } 103 | 104 | let lines_used = self.header.len(); 105 | 106 | // print "reserved" header lines (--header-lines) 107 | for (idx, item) in self.item_pool.reserved().iter().enumerate() { 108 | let mut printer = LinePrinter::builder() 109 | .row(self.adjust_row(idx + lines_used, screen_height)) 110 | .col(2) 111 | .tabstop(self.tabstop) 112 | .container_width(screen_width - 2) 113 | .shift(0) 114 | .text_width(screen_width - 2) 115 | .build(); 116 | 117 | let context = DisplayContext { 118 | text: &item.text(), 119 | score: 0, 120 | matches: Matches::None, 121 | container_width: screen_width - 2, 122 | highlight_attr: self.theme.header(), 123 | }; 124 | 125 | print_item(canvas, &mut printer, item.display(context), self.theme.header()); 126 | } 127 | 128 | Ok(()) 129 | } 130 | } 131 | 132 | impl Widget for Header { 133 | fn size_hint(&self) -> (Option, Option) { 134 | (None, Some(self.lines_of_header())) 135 | } 136 | } 137 | 138 | impl EventHandler for Header { 139 | fn handle(&mut self, _event: &Event) -> UpdateScreen { 140 | UpdateScreen::DontRedraw 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /skim/src/helper/item.rs: -------------------------------------------------------------------------------- 1 | use crate::ansi::ANSIParser; 2 | use crate::field::{FieldRange, parse_matching_fields, parse_transform_fields}; 3 | use crate::{AnsiString, DisplayContext, Matches, SkimItem}; 4 | use regex::Regex; 5 | use skim_tuikit::prelude::Attr; 6 | use std::borrow::Cow; 7 | 8 | //------------------------------------------------------------------------------ 9 | /// An item will store everything that one line input will need to be operated and displayed. 10 | /// 11 | /// What's special about an item? 12 | /// The simplest version of an item is a line of string, but things are getting more complex: 13 | /// - The conversion of lower/upper case is slow in rust, because it involds unicode. 14 | /// - We may need to interpret the ANSI codes in the text. 15 | /// - The text can be transformed and limited while searching. 16 | /// 17 | /// About the ANSI, we made assumption that it is linewise, that means no ANSI codes will affect 18 | /// more than one line. 19 | #[derive(Debug)] 20 | pub struct DefaultSkimItem { 21 | /// The text that will be output when user press `enter` 22 | /// `Some(..)` => the original input is transformed, could not output `text` directly 23 | /// `None` => that it is safe to output `text` directly 24 | orig_text: Option, 25 | 26 | /// The text that will be shown on screen and matched. 27 | text: AnsiString<'static>, 28 | 29 | // Option> to reduce memory use in normal cases where no matching ranges are specified. 30 | #[allow(clippy::box_collection)] 31 | matching_ranges: Option>>, 32 | /// The index, for use in matching 33 | index: usize, 34 | } 35 | 36 | impl DefaultSkimItem { 37 | pub fn new( 38 | orig_text: String, 39 | ansi_enabled: bool, 40 | trans_fields: &[FieldRange], 41 | matching_fields: &[FieldRange], 42 | delimiter: &Regex, 43 | index: usize, 44 | ) -> Self { 45 | let using_transform_fields = !trans_fields.is_empty(); 46 | 47 | // transformed | ANSI | output 48 | //------------------------------------------------------ 49 | // +- T -> trans+ANSI | ANSI 50 | // | | 51 | // +- T -> trans +- F -> trans | orig 52 | // orig | | 53 | // +- F -> orig +- T -> ANSI ==| ANSI 54 | // | | 55 | // +- F -> orig | orig 56 | 57 | let mut ansi_parser: ANSIParser = Default::default(); 58 | 59 | let (orig_text, text) = if using_transform_fields && ansi_enabled { 60 | // ansi and transform 61 | let transformed = ansi_parser.parse_ansi(&parse_transform_fields(delimiter, &orig_text, trans_fields)); 62 | (Some(orig_text), transformed) 63 | } else if using_transform_fields { 64 | // transformed, not ansi 65 | let transformed = parse_transform_fields(delimiter, &orig_text, trans_fields).into(); 66 | (Some(orig_text), transformed) 67 | } else if ansi_enabled { 68 | // not transformed, ansi 69 | (None, ansi_parser.parse_ansi(&orig_text)) 70 | } else { 71 | // normal case 72 | (None, orig_text.into()) 73 | }; 74 | 75 | let matching_ranges = if !matching_fields.is_empty() { 76 | Some(Box::new(parse_matching_fields( 77 | delimiter, 78 | text.stripped(), 79 | matching_fields, 80 | ))) 81 | } else { 82 | None 83 | }; 84 | 85 | DefaultSkimItem { 86 | orig_text, 87 | text, 88 | matching_ranges, 89 | index, 90 | } 91 | } 92 | } 93 | 94 | impl SkimItem for DefaultSkimItem { 95 | #[inline] 96 | fn text(&self) -> Cow { 97 | Cow::Borrowed(self.text.stripped()) 98 | } 99 | 100 | fn output(&self) -> Cow { 101 | if self.orig_text.is_some() { 102 | if self.text.has_attrs() { 103 | let mut ansi_parser: ANSIParser = Default::default(); 104 | let text = ansi_parser.parse_ansi(self.orig_text.as_ref().unwrap()); 105 | text.into_inner() 106 | } else { 107 | Cow::Borrowed(self.orig_text.as_ref().unwrap()) 108 | } 109 | } else { 110 | Cow::Borrowed(self.text.stripped()) 111 | } 112 | } 113 | 114 | fn get_matching_ranges(&self) -> Option<&[(usize, usize)]> { 115 | self.matching_ranges.as_ref().map(|vec| vec as &[(usize, usize)]) 116 | } 117 | 118 | fn display<'a>(&'a self, context: DisplayContext<'a>) -> AnsiString<'a> { 119 | let new_fragments: Vec<(Attr, (u32, u32))> = match context.matches { 120 | Matches::CharIndices(indices) => indices 121 | .iter() 122 | .map(|&idx| (context.highlight_attr, (idx as u32, idx as u32 + 1))) 123 | .collect(), 124 | Matches::CharRange(start, end) => vec![(context.highlight_attr, (start as u32, end as u32))], 125 | Matches::ByteRange(start, end) => { 126 | let ch_start = context.text[..start].chars().count(); 127 | let ch_end = ch_start + context.text[start..end].chars().count(); 128 | vec![(context.highlight_attr, (ch_start as u32, ch_end as u32))] 129 | } 130 | Matches::None => vec![], 131 | }; 132 | let mut ret = self.text.clone(); 133 | ret.override_attrs(new_fragments); 134 | ret 135 | } 136 | 137 | fn get_index(&self) -> usize { 138 | self.index 139 | } 140 | 141 | fn set_index(&mut self, index: usize) { 142 | self.index = index; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /skim/src/helper/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod item; 2 | pub mod item_reader; 3 | pub mod selector; 4 | -------------------------------------------------------------------------------- /skim/src/helper/selector.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use regex::Regex; 4 | 5 | use crate::{Selector, SkimItem}; 6 | 7 | #[derive(Debug, Default)] 8 | pub struct DefaultSkimSelector { 9 | first_n: usize, 10 | regex: Option, 11 | preset: Option>, 12 | } 13 | 14 | impl DefaultSkimSelector { 15 | pub fn first_n(mut self, first_n: usize) -> Self { 16 | trace!("select first_n: {}", first_n); 17 | self.first_n = first_n; 18 | self 19 | } 20 | 21 | pub fn preset(mut self, preset: impl IntoIterator) -> Self { 22 | if self.preset.is_none() { 23 | self.preset = Some(HashSet::new()) 24 | } 25 | 26 | if let Some(set) = self.preset.as_mut() { 27 | set.extend(preset) 28 | } 29 | self 30 | } 31 | 32 | pub fn regex(mut self, regex: &str) -> Self { 33 | trace!("select regex: {}", regex); 34 | if !regex.is_empty() { 35 | self.regex = Regex::new(regex).ok(); 36 | } 37 | self 38 | } 39 | } 40 | 41 | impl Selector for DefaultSkimSelector { 42 | fn should_select(&self, index: usize, item: &dyn SkimItem) -> bool { 43 | if self.first_n > index { 44 | return true; 45 | } 46 | 47 | if self.preset.is_some() 48 | && self 49 | .preset 50 | .as_ref() 51 | .map(|preset| preset.contains(item.text().as_ref())) 52 | .unwrap_or(false) 53 | { 54 | return true; 55 | } 56 | 57 | if self.regex.is_some() && self.regex.as_ref().map(|re| re.is_match(&item.text())).unwrap_or(false) { 58 | return true; 59 | } 60 | 61 | false 62 | } 63 | } 64 | 65 | #[cfg(test)] 66 | mod tests { 67 | use super::*; 68 | 69 | #[test] 70 | pub fn test_first_n() { 71 | let selector = DefaultSkimSelector::default().first_n(10); 72 | assert!(selector.should_select(0, &"item")); 73 | assert!(selector.should_select(1, &"item")); 74 | assert!(selector.should_select(2, &"item")); 75 | assert!(selector.should_select(9, &"item")); 76 | assert!(!selector.should_select(10, &"item")); 77 | } 78 | 79 | #[test] 80 | pub fn test_preset() { 81 | let selector = DefaultSkimSelector::default().preset(vec!["a".to_string(), "b".to_string(), "c".to_string()]); 82 | assert!(selector.should_select(0, &"a")); 83 | assert!(selector.should_select(0, &"b")); 84 | assert!(selector.should_select(0, &"c")); 85 | assert!(!selector.should_select(0, &"d")); 86 | } 87 | 88 | #[test] 89 | pub fn test_regex() { 90 | let selector = DefaultSkimSelector::default().regex("^[0-9]"); 91 | assert!(selector.should_select(0, &"1")); 92 | assert!(selector.should_select(0, &"2")); 93 | assert!(selector.should_select(0, &"3")); 94 | assert!(selector.should_select(0, &"1a")); 95 | assert!(!selector.should_select(0, &"a")); 96 | } 97 | 98 | #[test] 99 | pub fn test_all_together() { 100 | let selector = DefaultSkimSelector::default() 101 | .first_n(1) 102 | .regex("b") 103 | .preset(vec!["c".to_string()]); 104 | assert!(selector.should_select(0, &"a")); 105 | assert!(selector.should_select(1, &"b")); 106 | assert!(selector.should_select(2, &"c")); 107 | assert!(!selector.should_select(3, &"d")); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /skim/src/matcher.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; 3 | use std::thread; 4 | use std::thread::JoinHandle; 5 | 6 | use rayon::prelude::*; 7 | 8 | use crate::item::{ItemPool, MatchedItem}; 9 | use crate::spinlock::SpinLock; 10 | use crate::{CaseMatching, MatchEngineFactory}; 11 | use defer_drop::DeferDrop; 12 | use std::rc::Rc; 13 | 14 | //============================================================================== 15 | pub struct MatcherControl { 16 | stopped: Arc, 17 | processed: Arc, 18 | matched: Arc, 19 | items: Arc>>, 20 | thread_matcher: JoinHandle<()>, 21 | } 22 | 23 | impl MatcherControl { 24 | pub fn get_num_processed(&self) -> usize { 25 | self.processed.load(Ordering::Relaxed) 26 | } 27 | 28 | pub fn get_num_matched(&self) -> usize { 29 | self.matched.load(Ordering::Relaxed) 30 | } 31 | 32 | pub fn kill(self) { 33 | self.stopped.store(true, Ordering::Relaxed); 34 | let _ = self.thread_matcher.join(); 35 | } 36 | 37 | pub fn stopped(&self) -> bool { 38 | self.stopped.load(Ordering::Relaxed) 39 | } 40 | 41 | pub fn into_items(self) -> Arc>> { 42 | while !self.stopped.load(Ordering::Relaxed) {} 43 | self.items 44 | } 45 | } 46 | 47 | //============================================================================== 48 | pub struct Matcher { 49 | engine_factory: Rc, 50 | case_matching: CaseMatching, 51 | } 52 | 53 | impl Matcher { 54 | pub fn builder(engine_factory: Rc) -> Self { 55 | Self { 56 | engine_factory, 57 | case_matching: CaseMatching::default(), 58 | } 59 | } 60 | 61 | pub fn case(mut self, case_matching: CaseMatching) -> Self { 62 | self.case_matching = case_matching; 63 | self 64 | } 65 | 66 | pub fn build(self) -> Self { 67 | self 68 | } 69 | 70 | pub fn run(&self, query: &str, item_pool: Arc>, callback: C) -> MatcherControl 71 | where 72 | C: Fn(Arc>>) + Send + 'static, 73 | { 74 | let matcher_engine = self.engine_factory.create_engine_with_case(query, self.case_matching); 75 | debug!("engine: {}", matcher_engine); 76 | let stopped = Arc::new(AtomicBool::new(false)); 77 | let stopped_clone = stopped.clone(); 78 | let processed = Arc::new(AtomicUsize::new(0)); 79 | let processed_clone = processed.clone(); 80 | let matched = Arc::new(AtomicUsize::new(0)); 81 | let matched_clone = matched.clone(); 82 | let matched_items = Arc::new(SpinLock::new(Vec::new())); 83 | let matched_items_clone = matched_items.clone(); 84 | 85 | let thread_matcher = thread::spawn(move || { 86 | let _num_taken = item_pool.num_taken(); 87 | let items = item_pool.take(); 88 | 89 | // 1. use rayon for parallel 90 | // 2. return Err to skip iteration 91 | // check https://doc.rust-lang.org/std/result/enum.Result.html#method.from_iter 92 | 93 | trace!("matcher start, total: {}", items.len()); 94 | let result: Result, _> = items 95 | .into_par_iter() 96 | .enumerate() 97 | .filter_map(|(_, item)| { 98 | let item_idx = item.get_index(); 99 | processed.fetch_add(1, Ordering::Relaxed); 100 | if stopped.load(Ordering::Relaxed) { 101 | Some(Err("matcher killed")) 102 | } else if let Some(match_result) = matcher_engine.match_item(item.clone()) { 103 | matched.fetch_add(1, Ordering::Relaxed); 104 | Some(Ok(MatchedItem { 105 | item: item.clone(), 106 | rank: match_result.rank, 107 | matched_range: Some(match_result.matched_range), 108 | item_idx: item_idx as u32, 109 | })) 110 | } else { 111 | None 112 | } 113 | }) 114 | .collect(); 115 | 116 | if let Ok(items) = result { 117 | let mut pool = matched_items.lock(); 118 | *pool = items; 119 | trace!("matcher stop, total matched: {}", pool.len()); 120 | } 121 | 122 | callback(matched_items.clone()); 123 | stopped.store(true, Ordering::Relaxed); 124 | }); 125 | 126 | MatcherControl { 127 | stopped: stopped_clone, 128 | matched: matched_clone, 129 | processed: processed_clone, 130 | items: matched_items_clone, 131 | thread_matcher, 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /skim/src/model/options.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueEnum; 2 | use clap::builder::PossibleValue; 3 | 4 | #[derive(Debug, Clone, Default, Eq, PartialEq)] 5 | pub enum InfoDisplay { 6 | #[default] 7 | Default, 8 | Inline, 9 | Hidden, 10 | } 11 | 12 | impl ValueEnum for InfoDisplay { 13 | fn value_variants<'a>() -> &'a [Self] { 14 | use InfoDisplay::*; 15 | &[Default, Inline, Hidden] 16 | } 17 | 18 | fn to_possible_value(&self) -> Option { 19 | use InfoDisplay::*; 20 | match self { 21 | Default => Some(PossibleValue::new("default")), 22 | Inline => Some(PossibleValue::new("inline")), 23 | Hidden => Some(PossibleValue::new("hidden")), 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /skim/src/model/status.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::time::Duration; 3 | 4 | use skim_tuikit::attr::{Attr, Effect}; 5 | use skim_tuikit::canvas::Canvas; 6 | use skim_tuikit::draw::{Draw, DrawResult}; 7 | use skim_tuikit::widget::Widget; 8 | 9 | use crate::event::Event; 10 | use crate::theme::ColorTheme; 11 | use crate::util::clear_canvas; 12 | 13 | use super::InfoDisplay; 14 | 15 | const SPINNER_DURATION: u32 = 200; 16 | // const SPINNERS: [char; 8] = ['-', '\\', '|', '/', '-', '\\', '|', '/']; 17 | const SPINNERS_INLINE: [char; 2] = ['-', '<']; 18 | const SPINNERS_UNICODE: [char; 10] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; 19 | 20 | #[derive(Clone)] 21 | pub(crate) struct Status { 22 | pub(crate) total: usize, 23 | pub(crate) matched: usize, 24 | pub(crate) processed: usize, 25 | pub(crate) matcher_running: bool, 26 | pub(crate) multi_selection: bool, 27 | pub(crate) selected: usize, 28 | pub(crate) current_item_idx: usize, 29 | pub(crate) hscroll_offset: i64, 30 | pub(crate) reading: bool, 31 | pub(crate) time_since_read: Duration, 32 | pub(crate) time_since_match: Duration, 33 | pub(crate) matcher_mode: String, 34 | pub(crate) theme: Arc, 35 | pub(crate) info: InfoDisplay, 36 | } 37 | 38 | #[allow(unused_assignments)] 39 | impl Draw for Status { 40 | fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { 41 | // example: 42 | // /--num_matched/num_read /-- current_item_index 43 | // [| 869580/869580 0.] 44 | // `-spinner `-- still matching 45 | 46 | // example(inline): 47 | // /--num_matched/num_read /-- current_item_index 48 | // [> - 549334/549334 0.] 49 | // `-spinner `-- still matching 50 | 51 | canvas.clear()?; 52 | let (screen_width, _) = canvas.size()?; 53 | clear_canvas(canvas)?; 54 | if self.info == InfoDisplay::Hidden { 55 | return Ok(()); 56 | } 57 | 58 | let info_attr = self.theme.info(); 59 | let info_attr_bold = Attr { 60 | effect: Effect::BOLD, 61 | ..self.theme.info() 62 | }; 63 | 64 | let a_while_since_read = self.time_since_read > Duration::from_millis(50); 65 | let a_while_since_match = self.time_since_match > Duration::from_millis(50); 66 | 67 | let mut col = 0; 68 | let spinner_set: &[char] = match self.info { 69 | InfoDisplay::Default => &SPINNERS_UNICODE, 70 | InfoDisplay::Inline => &SPINNERS_INLINE, 71 | InfoDisplay::Hidden => panic!("This should never happen"), 72 | }; 73 | 74 | if self.info == InfoDisplay::Inline { 75 | col += canvas.put_char_with_attr(0, col, ' ', info_attr)?; 76 | } 77 | 78 | // draw the spinner 79 | if self.reading && a_while_since_read { 80 | let mills = (self.time_since_read.as_secs() * 1000) as u32 + self.time_since_read.subsec_millis(); 81 | let index = (mills / SPINNER_DURATION) % (spinner_set.len() as u32); 82 | let ch = spinner_set[index as usize]; 83 | col += canvas.put_char_with_attr(0, col, ch, self.theme.spinner())?; 84 | } else { 85 | match self.info { 86 | InfoDisplay::Inline => col += canvas.put_char_with_attr(0, col, '<', self.theme.prompt())?, 87 | InfoDisplay::Default => col += canvas.put_char_with_attr(0, col, ' ', self.theme.prompt())?, 88 | InfoDisplay::Hidden => panic!("This should never happen"), 89 | } 90 | } 91 | 92 | // display matched/total number 93 | col += canvas.print_with_attr(0, col, format!(" {}/{}", self.matched, self.total).as_ref(), info_attr)?; 94 | 95 | // display the matcher mode 96 | if !self.matcher_mode.is_empty() { 97 | col += canvas.print_with_attr(0, col, format!("/{}", &self.matcher_mode).as_ref(), info_attr)?; 98 | } 99 | 100 | // display the percentage of the number of processed items 101 | if self.matcher_running && a_while_since_match { 102 | col += canvas.print_with_attr( 103 | 0, 104 | col, 105 | format!(" ({}%) ", self.processed * 100 / self.total).as_ref(), 106 | info_attr, 107 | )?; 108 | } 109 | 110 | // selected number 111 | if self.multi_selection && self.selected > 0 { 112 | col += canvas.print_with_attr(0, col, format!(" [{}]", self.selected).as_ref(), info_attr_bold)?; 113 | } 114 | 115 | // item cursor 116 | let line_num_str = format!( 117 | " {}/{}{}", 118 | self.current_item_idx, 119 | self.hscroll_offset, 120 | if self.matcher_running { '.' } else { ' ' } 121 | ); 122 | canvas.print_with_attr(0, screen_width - line_num_str.len(), &line_num_str, info_attr_bold)?; 123 | 124 | Ok(()) 125 | } 126 | } 127 | 128 | impl Widget for Status {} 129 | 130 | #[derive(PartialEq, Eq, Clone, Debug, Copy)] 131 | pub(crate) enum Direction { 132 | Up, 133 | Down, 134 | Left, 135 | Right, 136 | } 137 | 138 | #[derive(PartialEq, Eq, Clone, Debug, Copy)] 139 | pub(crate) enum ClearStrategy { 140 | DontClear, 141 | Clear, 142 | ClearIfNotNull, 143 | } 144 | -------------------------------------------------------------------------------- /skim/src/output.rs: -------------------------------------------------------------------------------- 1 | use crate::SkimItem; 2 | use crate::event::Event; 3 | use skim_tuikit::key::Key; 4 | use std::sync::Arc; 5 | 6 | pub struct SkimOutput { 7 | /// The final event that makes skim accept/quit. 8 | /// Was designed to determine if skim quit or accept. 9 | /// Typically there are only two options: `Event::EvActAbort` | `Event::EvActAccept` 10 | pub final_event: Event, 11 | 12 | /// quick pass for judging if skim aborts. 13 | pub is_abort: bool, 14 | 15 | /// The final key that makes skim accept/quit. 16 | /// Note that it might be Key::Null if it is triggered by skim. 17 | pub final_key: Key, 18 | 19 | /// The query 20 | pub query: String, 21 | 22 | /// The command query 23 | pub cmd: String, 24 | 25 | /// The selected items. 26 | pub selected_items: Vec>, 27 | } 28 | -------------------------------------------------------------------------------- /skim/src/prelude.rs: -------------------------------------------------------------------------------- 1 | pub use crate::ansi::AnsiString; 2 | pub use crate::engine::{factory::*, fuzzy::FuzzyAlgorithm}; 3 | pub use crate::event::Event; 4 | pub use crate::helper::item_reader::{SkimItemReader, SkimItemReaderOption}; 5 | pub use crate::helper::selector::DefaultSkimSelector; 6 | pub use crate::options::{SkimOptions, SkimOptionsBuilder}; 7 | pub use crate::output::SkimOutput; 8 | pub use crate::previewer::PreviewCallback; 9 | pub use crate::*; 10 | pub use crossbeam::channel::{Receiver, Sender, bounded, unbounded}; 11 | pub use skim_tuikit::event::Key; 12 | pub use std::borrow::Cow; 13 | pub use std::cell::RefCell; 14 | pub use std::rc::Rc; 15 | pub use std::sync::Arc; 16 | pub use std::sync::atomic::{AtomicUsize, Ordering}; 17 | -------------------------------------------------------------------------------- /skim/src/reader.rs: -------------------------------------------------------------------------------- 1 | //! Reader is used for reading items from datasource (e.g. stdin or command output) 2 | //! 3 | //! After reading in a line, reader will save an item into the pool(items) 4 | use crate::global::mark_new_run; 5 | use crate::options::SkimOptions; 6 | use crate::spinlock::SpinLock; 7 | use crate::{SkimItem, SkimItemReceiver}; 8 | use crossbeam::channel::{Sender, bounded, select}; 9 | use std::cell::RefCell; 10 | use std::rc::Rc; 11 | use std::sync::Arc; 12 | use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; 13 | use std::thread; 14 | 15 | const CHANNEL_SIZE: usize = 1024; 16 | 17 | pub trait CommandCollector { 18 | /// execute the `cmd` and produce a 19 | /// - skim item producer 20 | /// - a channel sender, any message send would mean to terminate the `cmd` process (for now). 21 | /// 22 | /// Internally, the command collector may start several threads(components), the collector 23 | /// should add `1` on every thread creation and sub `1` on thread termination. reader would use 24 | /// this information to determine whether the collector had stopped or not. 25 | fn invoke(&mut self, cmd: &str, components_to_stop: Arc) -> (SkimItemReceiver, Sender); 26 | } 27 | 28 | pub struct ReaderControl { 29 | tx_interrupt: Sender, 30 | tx_interrupt_cmd: Option>, 31 | components_to_stop: Arc, 32 | items: Arc>>>, 33 | } 34 | 35 | impl ReaderControl { 36 | pub fn kill(self) { 37 | debug!( 38 | "kill reader, components before: {}", 39 | self.components_to_stop.load(Ordering::SeqCst) 40 | ); 41 | 42 | let _ = self.tx_interrupt_cmd.map(|tx| tx.send(1)); 43 | let _ = self.tx_interrupt.send(1); 44 | while self.components_to_stop.load(Ordering::SeqCst) != 0 {} 45 | } 46 | 47 | pub fn take(&self) -> Vec> { 48 | let mut items = self.items.lock(); 49 | let mut ret = Vec::with_capacity(items.len()); 50 | ret.append(&mut items); 51 | ret 52 | } 53 | 54 | pub fn is_done(&self) -> bool { 55 | let items = self.items.lock(); 56 | self.components_to_stop.load(Ordering::SeqCst) == 0 && items.is_empty() 57 | } 58 | } 59 | 60 | pub struct Reader { 61 | cmd_collector: Rc>, 62 | rx_item: Option, 63 | } 64 | 65 | impl Reader { 66 | pub fn with_options(options: &SkimOptions) -> Self { 67 | Self { 68 | cmd_collector: options.cmd_collector.clone(), 69 | rx_item: None, 70 | } 71 | } 72 | 73 | pub fn source(mut self, rx_item: Option) -> Self { 74 | self.rx_item = rx_item; 75 | self 76 | } 77 | 78 | pub fn run(&mut self, cmd: &str) -> ReaderControl { 79 | mark_new_run(cmd); 80 | 81 | let components_to_stop: Arc = Arc::new(AtomicUsize::new(0)); 82 | let items = Arc::new(SpinLock::new(Vec::new())); 83 | let items_clone = items.clone(); 84 | 85 | let (rx_item, tx_interrupt_cmd) = self.rx_item.take().map(|rx| (rx, None)).unwrap_or_else(|| { 86 | let components_to_stop_clone = components_to_stop.clone(); 87 | let (rx_item, tx_interrupt_cmd) = self.cmd_collector.borrow_mut().invoke(cmd, components_to_stop_clone); 88 | (rx_item, Some(tx_interrupt_cmd)) 89 | }); 90 | 91 | let components_to_stop_clone = components_to_stop.clone(); 92 | let tx_interrupt = collect_item(components_to_stop_clone, rx_item, items_clone); 93 | 94 | ReaderControl { 95 | tx_interrupt, 96 | tx_interrupt_cmd, 97 | components_to_stop, 98 | items, 99 | } 100 | } 101 | } 102 | 103 | fn collect_item( 104 | components_to_stop: Arc, 105 | rx_item: SkimItemReceiver, 106 | items: Arc>>>, 107 | ) -> Sender { 108 | let (tx_interrupt, rx_interrupt) = bounded(CHANNEL_SIZE); 109 | 110 | let started = Arc::new(AtomicBool::new(false)); 111 | let started_clone = started.clone(); 112 | thread::spawn(move || { 113 | debug!("reader: collect_item start"); 114 | components_to_stop.fetch_add(1, Ordering::SeqCst); 115 | started_clone.store(true, Ordering::SeqCst); // notify parent that it is started 116 | 117 | loop { 118 | select! { 119 | recv(rx_item) -> new_item => match new_item { 120 | Ok(item) => { 121 | let mut vec = items.lock(); 122 | vec.push(item); 123 | } 124 | Err(_) => break, 125 | }, 126 | recv(rx_interrupt) -> _msg => break, 127 | } 128 | } 129 | 130 | components_to_stop.fetch_sub(1, Ordering::SeqCst); 131 | debug!("reader: collect_item stop"); 132 | }); 133 | 134 | while !started.load(Ordering::SeqCst) { 135 | // busy waiting for the thread to start. (components_to_stop is added) 136 | } 137 | 138 | tx_interrupt 139 | } 140 | -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask" 3 | version = "0.1.1" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | clap = { workspace = true, features = ["derive", "unstable-markdown"] } 8 | clap_complete = { workspace = true } 9 | clap_complete_fig = { workspace = true } 10 | clap_complete_nushell = { workspace = true } 11 | clap_mangen = { workspace = true } 12 | skim = { path = "../skim", features = [] } 13 | -------------------------------------------------------------------------------- /xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::pedantic, clippy::complexity)] 2 | use std::{ 3 | env, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | use clap::CommandFactory; 8 | use skim::SkimOptions; 9 | 10 | type DynError = Box; 11 | 12 | fn main() { 13 | for task in env::args().skip(1) { 14 | if let Err(e) = try_main(&task) { 15 | eprintln!("{}", e); 16 | std::process::exit(-1); 17 | } 18 | } 19 | } 20 | 21 | fn try_main(task: &str) -> Result<(), DynError> { 22 | match task { 23 | "mangen" => mangen()?, 24 | "compgen" => compgen()?, 25 | _ => print_help(), 26 | } 27 | Ok(()) 28 | } 29 | 30 | fn print_help() { 31 | eprintln!( 32 | "Tasks: 33 | 34 | mangen generate the man page 35 | compgen generate completions for popular shells 36 | " 37 | ) 38 | } 39 | 40 | fn mangen() -> Result<(), DynError> { 41 | let mut buffer: Vec = Default::default(); 42 | clap_mangen::Man::new(SkimOptions::command()).render(&mut buffer)?; 43 | let mandir = project_root().join("man").join("man1"); 44 | std::fs::create_dir_all(&mandir)?; 45 | std::fs::write(mandir.join("sk.1"), buffer)?; 46 | 47 | Ok(()) 48 | } 49 | 50 | fn compgen() -> Result<(), DynError> { 51 | let completions_dir = project_root().join("shell"); 52 | std::fs::create_dir_all(&completions_dir)?; 53 | // Bash 54 | let mut buffer: Vec = Default::default(); 55 | clap_complete::generate( 56 | clap_complete::Shell::Bash, 57 | &mut SkimOptions::command(), 58 | "sk", 59 | &mut buffer, 60 | ); 61 | std::fs::write(completions_dir.join("completion.bash"), buffer)?; 62 | 63 | // Zsh 64 | let mut buffer: Vec = Default::default(); 65 | clap_complete::generate( 66 | clap_complete::Shell::Zsh, 67 | &mut SkimOptions::command(), 68 | "sk", 69 | &mut buffer, 70 | ); 71 | std::fs::write(completions_dir.join("completion.zsh"), buffer)?; 72 | 73 | // Fish 74 | let mut buffer: Vec = Default::default(); 75 | clap_complete::generate( 76 | clap_complete::Shell::Fish, 77 | &mut SkimOptions::command(), 78 | "sk", 79 | &mut buffer, 80 | ); 81 | std::fs::write(completions_dir.join("completion.fish"), buffer)?; 82 | 83 | Ok(()) 84 | } 85 | 86 | fn project_root() -> PathBuf { 87 | Path::new(&env!("CARGO_MANIFEST_DIR")) 88 | .ancestors() 89 | .nth(1) 90 | .unwrap() 91 | .to_path_buf() 92 | } 93 | --------------------------------------------------------------------------------