From 76a6480b6241814acb6c23e04a5885a21a2ddbfc Mon Sep 17 00:00:00 2001 From: Christian Nieves Date: Tue, 19 Apr 2022 15:50:47 +0000 Subject: [PATCH] Populate repo --- config/.config/nvim/coc-settings.json | 56 + config/.config/nvim/init.vim | 1 + config/.config/nvim/lua/diagnostics.lua | 55 + config/.config/nvim/lua/lsp.lua | 176 + fzf/fzf-at-google.zsh | 255 ++ fzf/fzf-relevant-files.zsh | 48 + fzf/fzf/.github/FUNDING.yml | 1 + fzf/fzf/.github/ISSUE_TEMPLATE.md | 22 + fzf/fzf/.github/dependabot.yml | 6 + fzf/fzf/.github/workflows/codeql-analysis.yml | 37 + fzf/fzf/.github/workflows/linux.yml | 45 + fzf/fzf/.github/workflows/macos.yml | 45 + fzf/fzf/.gitignore | 14 + fzf/fzf/.goreleaser.yml | 119 + fzf/fzf/.rubocop.yml | 28 + fzf/fzf/ADVANCED.md | 569 +++ fzf/fzf/BUILD.md | 49 + fzf/fzf/CHANGELOG.md | 1206 ++++++ fzf/fzf/Dockerfile | 11 + fzf/fzf/LICENSE | 21 + fzf/fzf/Makefile | 166 + fzf/fzf/README-VIM.md | 486 +++ fzf/fzf/README.md | 715 ++++ fzf/fzf/bin/fzf-tmux | 233 ++ fzf/fzf/doc/fzf.txt | 512 +++ fzf/fzf/go.mod | 17 + fzf/fzf/go.sum | 31 + fzf/fzf/install | 377 ++ fzf/fzf/install.ps1 | 65 + fzf/fzf/main.go | 14 + fzf/fzf/man/man1/fzf-tmux.1 | 68 + fzf/fzf/man/man1/fzf.1 | 1018 +++++ fzf/fzf/plugin/fzf.vim | 1054 +++++ fzf/fzf/shell/completion.bash | 381 ++ fzf/fzf/shell/completion.zsh | 329 ++ fzf/fzf/shell/key-bindings.bash | 96 + fzf/fzf/shell/key-bindings.fish | 172 + fzf/fzf/shell/key-bindings.zsh | 120 + fzf/fzf/src/LICENSE | 21 + fzf/fzf/src/algo/algo.go | 884 ++++ fzf/fzf/src/algo/algo_test.go | 197 + fzf/fzf/src/algo/normalize.go | 492 +++ fzf/fzf/src/ansi.go | 409 ++ fzf/fzf/src/ansi_test.go | 427 ++ fzf/fzf/src/cache.go | 81 + fzf/fzf/src/cache_test.go | 39 + fzf/fzf/src/chunklist.go | 89 + fzf/fzf/src/chunklist_test.go | 80 + fzf/fzf/src/constants.go | 85 + fzf/fzf/src/core.go | 351 ++ fzf/fzf/src/history.go | 96 + fzf/fzf/src/history_test.go | 68 + fzf/fzf/src/item.go | 44 + fzf/fzf/src/item_test.go | 23 + fzf/fzf/src/matcher.go | 235 ++ fzf/fzf/src/merger.go | 120 + fzf/fzf/src/merger_test.go | 88 + fzf/fzf/src/options.go | 1734 ++++++++ fzf/fzf/src/options_test.go | 457 +++ fzf/fzf/src/pattern.go | 425 ++ fzf/fzf/src/pattern_test.go | 209 + fzf/fzf/src/protector/protector.go | 8 + fzf/fzf/src/protector/protector_openbsd.go | 10 + fzf/fzf/src/reader.go | 201 + fzf/fzf/src/reader_test.go | 63 + fzf/fzf/src/result.go | 243 ++ fzf/fzf/src/result_others.go | 16 + fzf/fzf/src/result_test.go | 159 + fzf/fzf/src/result_x86.go | 16 + fzf/fzf/src/terminal.go | 2890 +++++++++++++ fzf/fzf/src/terminal_test.go | 638 +++ fzf/fzf/src/terminal_unix.go | 26 + fzf/fzf/src/terminal_windows.go | 45 + fzf/fzf/src/tokenizer.go | 253 ++ fzf/fzf/src/tokenizer_test.go | 112 + fzf/fzf/src/tui/dummy.go | 46 + fzf/fzf/src/tui/light.go | 987 +++++ fzf/fzf/src/tui/light_unix.go | 110 + fzf/fzf/src/tui/light_windows.go | 145 + fzf/fzf/src/tui/tcell.go | 721 ++++ fzf/fzf/src/tui/tcell_test.go | 392 ++ fzf/fzf/src/tui/ttyname_unix.go | 47 + fzf/fzf/src/tui/ttyname_windows.go | 14 + fzf/fzf/src/tui/tui.go | 625 +++ fzf/fzf/src/tui/tui_test.go | 20 + fzf/fzf/src/util/atomicbool.go | 34 + fzf/fzf/src/util/atomicbool_test.go | 17 + fzf/fzf/src/util/chars.go | 198 + fzf/fzf/src/util/chars_test.go | 46 + fzf/fzf/src/util/eventbox.go | 96 + fzf/fzf/src/util/eventbox_test.go | 61 + fzf/fzf/src/util/slab.go | 12 + fzf/fzf/src/util/util.go | 138 + fzf/fzf/src/util/util_test.go | 56 + fzf/fzf/src/util/util_unix.go | 47 + fzf/fzf/src/util/util_windows.go | 83 + fzf/fzf/test/fzf.vader | 175 + fzf/fzf/test/test_go.rb | 2692 ++++++++++++ fzf/fzf/uninstall | 117 + tmux/.tmux.conf | 104 + tmux/.tmux/osiris-theme.conf | 88 + .../.tmux/plugins/tmux-battery/.gitattributes | 3 + tmux/.tmux/plugins/tmux-battery/CHANGELOG.md | 44 + tmux/.tmux/plugins/tmux-battery/LICENSE.md | 19 + tmux/.tmux/plugins/tmux-battery/README.md | 262 ++ tmux/.tmux/plugins/tmux-battery/battery.tmux | 62 + .../screenshots/battery_charging_tier1.png | Bin 0 -> 7485 bytes .../screenshots/battery_charging_tier2.png | Bin 0 -> 7772 bytes .../screenshots/battery_charging_tier3.png | Bin 0 -> 7915 bytes .../screenshots/battery_charging_tier4.png | Bin 0 -> 7931 bytes .../screenshots/battery_charging_tier5.png | Bin 0 -> 7743 bytes .../screenshots/battery_charging_tier6.png | Bin 0 -> 7791 bytes .../screenshots/battery_charging_tier7.png | Bin 0 -> 7819 bytes .../screenshots/battery_charging_tier8.png | Bin 0 -> 7799 bytes .../screenshots/battery_discharging_tier1.png | Bin 0 -> 7425 bytes .../screenshots/battery_discharging_tier2.png | Bin 0 -> 7951 bytes .../screenshots/battery_discharging_tier3.png | Bin 0 -> 8287 bytes .../screenshots/battery_discharging_tier4.png | Bin 0 -> 8247 bytes .../screenshots/battery_discharging_tier5.png | Bin 0 -> 8096 bytes .../screenshots/battery_discharging_tier6.png | Bin 0 -> 8109 bytes .../screenshots/battery_discharging_tier7.png | Bin 0 -> 8253 bytes .../screenshots/battery_discharging_tier8.png | Bin 0 -> 7478 bytes .../screenshots/battery_status_attached.png | Bin 0 -> 7534 bytes .../screenshots/battery_status_unknown.png | Bin 0 -> 7299 bytes .../tmux-battery/scripts/battery_color.sh | 23 + .../scripts/battery_color_charge.sh | 98 + .../scripts/battery_color_status.sh | 74 + .../tmux-battery/scripts/battery_graph.sh | 45 + .../tmux-battery/scripts/battery_icon.sh | 21 + .../scripts/battery_icon_charge.sh | 66 + .../scripts/battery_icon_status.sh | 61 + .../scripts/battery_percentage.sh | 45 + .../tmux-battery/scripts/battery_remain.sh | 130 + .../tmux-battery/scripts/battery_status_bg.sh | 50 + .../tmux-battery/scripts/battery_status_fg.sh | 50 + .../plugins/tmux-battery/scripts/helpers.sh | 63 + tmux/.tmux/plugins/tmux-cowboy/LICENSE.md | 19 + tmux/.tmux/plugins/tmux-cowboy/README.md | 24 + tmux/.tmux/plugins/tmux-cowboy/cowboy.tmux | 9 + .../.tmux/plugins/tmux-cowboy/scripts/kill.sh | 26 + tmux/.tmux/plugins/tmux-cpu/.editorconfig | 16 + tmux/.tmux/plugins/tmux-cpu/.mailmap | 2 + tmux/.tmux/plugins/tmux-cpu/LICENSE | 22 + tmux/.tmux/plugins/tmux-cpu/README.md | 170 + tmux/.tmux/plugins/tmux-cpu/cpu.tmux | 85 + .../plugins/tmux-cpu/screenshots/high_bg.png | Bin 0 -> 992 bytes .../plugins/tmux-cpu/screenshots/high_fg.png | Bin 0 -> 919 bytes .../tmux-cpu/screenshots/high_icon.png | Bin 0 -> 1069 bytes .../plugins/tmux-cpu/screenshots/low_bg.png | Bin 0 -> 908 bytes .../plugins/tmux-cpu/screenshots/low_fg.png | Bin 0 -> 855 bytes .../plugins/tmux-cpu/screenshots/low_icon.png | Bin 0 -> 1016 bytes .../tmux-cpu/screenshots/medium_bg.png | Bin 0 -> 1314 bytes .../tmux-cpu/screenshots/medium_fg.png | Bin 0 -> 1162 bytes .../tmux-cpu/screenshots/medium_icon.png | Bin 0 -> 1188 bytes .../plugins/tmux-cpu/scripts/cpu_bg_color.sh | 37 + .../plugins/tmux-cpu/scripts/cpu_fg_color.sh | 37 + .../plugins/tmux-cpu/scripts/cpu_icon.sh | 39 + .../tmux-cpu/scripts/cpu_percentage.sh | 40 + .../plugins/tmux-cpu/scripts/cpu_temp.sh | 21 + .../tmux-cpu/scripts/cpu_temp_bg_color.sh | 37 + .../tmux-cpu/scripts/cpu_temp_fg_color.sh | 37 + .../plugins/tmux-cpu/scripts/cpu_temp_icon.sh | 39 + .../plugins/tmux-cpu/scripts/gpu_bg_color.sh | 37 + .../plugins/tmux-cpu/scripts/gpu_fg_color.sh | 37 + .../plugins/tmux-cpu/scripts/gpu_icon.sh | 39 + .../tmux-cpu/scripts/gpu_percentage.sh | 26 + .../plugins/tmux-cpu/scripts/gpu_temp.sh | 33 + .../tmux-cpu/scripts/gpu_temp_bg_color.sh | 37 + .../tmux-cpu/scripts/gpu_temp_fg_color.sh | 37 + .../plugins/tmux-cpu/scripts/gpu_temp_icon.sh | 39 + .../plugins/tmux-cpu/scripts/gram_bg_color.sh | 37 + .../plugins/tmux-cpu/scripts/gram_fg_color.sh | 37 + .../plugins/tmux-cpu/scripts/gram_icon.sh | 39 + .../tmux-cpu/scripts/gram_percentage.sh | 26 + .../.tmux/plugins/tmux-cpu/scripts/helpers.sh | 132 + .../plugins/tmux-cpu/scripts/ram_bg_color.sh | 37 + .../plugins/tmux-cpu/scripts/ram_fg_color.sh | 37 + .../plugins/tmux-cpu/scripts/ram_icon.sh | 39 + .../tmux-cpu/scripts/ram_percentage.sh | 48 + .../plugins/tmux-sensible/.gitattributes | 2 + tmux/.tmux/plugins/tmux-sensible/CHANGELOG.md | 43 + tmux/.tmux/plugins/tmux-sensible/LICENSE.md | 19 + tmux/.tmux/plugins/tmux-sensible/README.md | 114 + .../.tmux/plugins/tmux-sensible/sensible.tmux | 161 + tmux/.tmux/plugins/tmux-yank/.editorconfig | 24 + tmux/.tmux/plugins/tmux-yank/.gitattributes | 11 + tmux/.tmux/plugins/tmux-yank/.gitignore | 1 + tmux/.tmux/plugins/tmux-yank/.travis.yml | 17 + tmux/.tmux/plugins/tmux-yank/CHANGELOG.md | 132 + tmux/.tmux/plugins/tmux-yank/LICENSE.md | 20 + tmux/.tmux/plugins/tmux-yank/README.md | 288 ++ tmux/.tmux/plugins/tmux-yank/Vagrantfile | 10 + tmux/.tmux/plugins/tmux-yank/_config.yml | 1 + tmux/.tmux/plugins/tmux-yank/citest | 30 + .../plugins/tmux-yank/scripts/copy_line.sh | 111 + .../tmux-yank/scripts/copy_pane_pwd.sh | 25 + .../plugins/tmux-yank/scripts/helpers.sh | 205 + .../plugins/tmux-yank/vagrant_provisioning.sh | 13 + tmux/.tmux/plugins/tmux-yank/video/README.md | 7 + .../tmux-yank/video/screencast_img.png | Bin 0 -> 52912 bytes tmux/.tmux/plugins/tmux-yank/video/script.md | 204 + tmux/.tmux/plugins/tmux-yank/yank.tmux | 92 + tmux/.tmux/plugins/tpm/.gitattributes | 9 + tmux/.tmux/plugins/tpm/.gitignore | 4 + tmux/.tmux/plugins/tpm/.gitmodules | 3 + tmux/.tmux/plugins/tpm/.travis.yml | 19 + tmux/.tmux/plugins/tpm/CHANGELOG.md | 83 + tmux/.tmux/plugins/tpm/HOW_TO_PLUGIN.md | 2 + tmux/.tmux/plugins/tpm/LICENSE.md | 20 + tmux/.tmux/plugins/tpm/README.md | 101 + tmux/.tmux/plugins/tpm/bin/clean_plugins | 14 + tmux/.tmux/plugins/tpm/bin/install_plugins | 14 + tmux/.tmux/plugins/tpm/bin/update_plugins | 24 + tmux/.tmux/plugins/tpm/bindings/clean_plugins | 19 + .../plugins/tpm/bindings/install_plugins | 19 + .../.tmux/plugins/tpm/bindings/update_plugins | 49 + .../tpm/docs/automatic_tpm_installation.md | 12 + .../tpm/docs/changing_plugins_install_dir.md | 16 + .../plugins/tpm/docs/how_to_create_plugin.md | 108 + .../tpm/docs/managing_plugins_via_cmd_line.md | 36 + .../.tmux/plugins/tpm/docs/tpm_not_working.md | 96 + .../plugins/tpm/scripts/check_tmux_version.sh | 78 + .../plugins/tpm/scripts/clean_plugins.sh | 41 + .../tpm/scripts/helpers/plugin_functions.sh | 104 + .../scripts/helpers/shell_echo_functions.sh | 7 + .../scripts/helpers/tmux_echo_functions.sh | 28 + .../plugins/tpm/scripts/helpers/tmux_utils.sh | 6 + .../plugins/tpm/scripts/helpers/utility.sh | 17 + .../plugins/tpm/scripts/install_plugins.sh | 75 + .../plugins/tpm/scripts/source_plugins.sh | 42 + .../plugins/tpm/scripts/update_plugin.sh | 73 + .../scripts/update_plugin_prompt_handler.sh | 18 + tmux/.tmux/plugins/tpm/scripts/variables.sh | 13 + .../tpm/tests/expect_failed_plugin_download | 36 + .../tpm/tests/expect_successful_clean_plugins | 35 + ...xpect_successful_multiple_plugins_download | 44 + .../tests/expect_successful_plugin_download | 50 + ...xpect_successful_update_of_a_single_plugin | 55 + .../expect_successful_update_of_all_plugins | 59 + tmux/.tmux/plugins/tpm/tests/helpers/tpm.sh | 13 + .../plugins/tpm/tests/test_plugin_clean.sh | 67 + .../tpm/tests/test_plugin_installation.sh | 284 ++ .../tests/test_plugin_installation_legacy.sh | 100 + .../plugins/tpm/tests/test_plugin_sourcing.sh | 78 + .../plugins/tpm/tests/test_plugin_update.sh | 60 + tmux/.tmux/plugins/tpm/tpm | 81 + .../plugins/vim-tmux-navigator/.gitignore | 1 + .../plugins/vim-tmux-navigator/License.md | 21 + .../plugins/vim-tmux-navigator/README.md | 305 ++ .../vim-tmux-navigator/doc/tmux-navigator.txt | 39 + .../plugins/vim-tmux-navigator/pattern-check | 42 + .../plugin/tmux_navigator.vim | 131 + .../vim-tmux-navigator.tmux | 25 + tmux/.tmux/tmux-migrate-options.py | 139 + tmux/.tmuxinator/dev.yml | 36 + tmux/.tmuxinator/second.yaml | 27 + vim/.vim/after/syntax/java.vim | 348 ++ vim/.vim/autoload/plug.vim | 2802 +++++++++++++ vim/.vim/prefs/ale.vim | 21 + vim/.vim/prefs/asynclsp.vim | 53 + vim/.vim/prefs/cmp.vim | 5 + vim/.vim/prefs/coc.vim | 172 + vim/.vim/prefs/golang.vim | 63 + vim/.vim/prefs/google.vim | 169 + vim/.vim/prefs/init.vim | 64 + vim/.vim/prefs/leader.vim | 183 + vim/.vim/prefs/mappings.vim | 70 + vim/.vim/prefs/plug_prefs.vim | 185 + vim/.vim/prefs/plugins.vim | 116 + vim/.vim/prefs/ui.vim | 96 + vim/.vim/prefs/ultisnips.vim | 4 + vim/.vim/prefs/ycm.vim | 36 + vim/.vimrc | 62 + vim/.vimrc.local | 0 zsh/.aliases.sh | 104 + zsh/.bash-powerline.sh | 110 + zsh/.bash_profile | 40 + zsh/.bash_profile.local | 58 + zsh/.oh-my-zsh/.editorconfig | 8 + zsh/.oh-my-zsh/.github/CODEOWNERS | 10 + zsh/.oh-my-zsh/.github/FUNDING.yml | 2 + .../.github/ISSUE_TEMPLATE/bug_report.yml | 69 + .../.github/ISSUE_TEMPLATE/config.yml | 8 + .../ISSUE_TEMPLATE/feature_request.yml | 32 + .../.github/PULL_REQUEST_TEMPLATE.md | 19 + zsh/.oh-my-zsh/.github/workflows/main.yml | 36 + zsh/.oh-my-zsh/.gitignore | 8 + zsh/.oh-my-zsh/.gitpod.Dockerfile | 5 + zsh/.oh-my-zsh/.gitpod.yml | 9 + zsh/.oh-my-zsh/CODE_OF_CONDUCT.md | 76 + zsh/.oh-my-zsh/CONTRIBUTING.md | 224 + zsh/.oh-my-zsh/LICENSE.txt | 21 + zsh/.oh-my-zsh/README.md | 314 ++ zsh/.oh-my-zsh/lib/bzr.zsh | 10 + zsh/.oh-my-zsh/lib/cli.zsh | 776 ++++ zsh/.oh-my-zsh/lib/clipboard.zsh | 107 + zsh/.oh-my-zsh/lib/compfix.zsh | 44 + zsh/.oh-my-zsh/lib/completion.zsh | 78 + zsh/.oh-my-zsh/lib/correction.zsh | 15 + zsh/.oh-my-zsh/lib/diagnostics.zsh | 353 ++ zsh/.oh-my-zsh/lib/directories.zsh | 38 + zsh/.oh-my-zsh/lib/functions.zsh | 256 ++ zsh/.oh-my-zsh/lib/git.zsh | 281 ++ zsh/.oh-my-zsh/lib/grep.zsh | 41 + zsh/.oh-my-zsh/lib/history.zsh | 40 + zsh/.oh-my-zsh/lib/key-bindings.zsh | 138 + zsh/.oh-my-zsh/lib/misc.zsh | 35 + zsh/.oh-my-zsh/lib/nvm.zsh | 6 + zsh/.oh-my-zsh/lib/prompt_info_functions.zsh | 43 + zsh/.oh-my-zsh/lib/spectrum.zsh | 35 + zsh/.oh-my-zsh/lib/termsupport.zsh | 137 + zsh/.oh-my-zsh/lib/theme-and-appearance.zsh | 59 + zsh/.oh-my-zsh/oh-my-zsh.sh | 179 + zsh/.oh-my-zsh/plugins/adb/README.md | 8 + zsh/.oh-my-zsh/plugins/adb/_adb | 67 + zsh/.oh-my-zsh/plugins/ag/README.md | 13 + zsh/.oh-my-zsh/plugins/ag/_ag | 66 + zsh/.oh-my-zsh/plugins/alias-finder/README.md | 46 + .../alias-finder/alias-finder.plugin.zsh | 47 + zsh/.oh-my-zsh/plugins/aliases/README.md | 21 + .../plugins/aliases/aliases.plugin.zsh | 10 + zsh/.oh-my-zsh/plugins/aliases/cheatsheet.py | 55 + zsh/.oh-my-zsh/plugins/aliases/termcolor.py | 168 + zsh/.oh-my-zsh/plugins/ansible/README.md | 34 + .../plugins/ansible/ansible.plugin.zsh | 28 + zsh/.oh-my-zsh/plugins/ant/README.md | 12 + zsh/.oh-my-zsh/plugins/ant/ant.plugin.zsh | 16 + .../plugins/apache2-macports/README.md | 21 + .../apache2-macports.plugin.zsh | 6 + zsh/.oh-my-zsh/plugins/arcanist/README.md | 41 + .../plugins/arcanist/arcanist.plugin.zsh | 37 + zsh/.oh-my-zsh/plugins/archlinux/README.md | 172 + .../plugins/archlinux/archlinux.plugin.zsh | 174 + zsh/.oh-my-zsh/plugins/asdf/README.md | 27 + zsh/.oh-my-zsh/plugins/asdf/asdf.plugin.zsh | 19 + zsh/.oh-my-zsh/plugins/autoenv/README.md | 20 + .../plugins/autoenv/autoenv.plugin.zsh | 71 + zsh/.oh-my-zsh/plugins/autojump/README.md | 11 + .../plugins/autojump/autojump.plugin.zsh | 35 + zsh/.oh-my-zsh/plugins/autopep8/README.md | 8 + zsh/.oh-my-zsh/plugins/autopep8/_autopep8 | 32 + zsh/.oh-my-zsh/plugins/aws/README.md | 75 + zsh/.oh-my-zsh/plugins/aws/aws.plugin.zsh | 206 + zsh/.oh-my-zsh/plugins/battery/README.md | 29 + .../plugins/battery/battery.plugin.zsh | 262 ++ zsh/.oh-my-zsh/plugins/bazel/README.md | 5 + zsh/.oh-my-zsh/plugins/bazel/_bazel | 341 ++ zsh/.oh-my-zsh/plugins/bbedit/README.md | 20 + .../plugins/bbedit/bbedit.plugin.zsh | 21 + zsh/.oh-my-zsh/plugins/bedtools/README.md | 5 + zsh/.oh-my-zsh/plugins/bedtools/_bedtools | 64 + zsh/.oh-my-zsh/plugins/bgnotify/README.md | 54 + .../plugins/bgnotify/bgnotify.plugin.zsh | 77 + zsh/.oh-my-zsh/plugins/boot2docker/README.md | 6 + .../plugins/boot2docker/_boot2docker | 73 + zsh/.oh-my-zsh/plugins/bower/README.md | 17 + zsh/.oh-my-zsh/plugins/bower/_bower | 58 + zsh/.oh-my-zsh/plugins/bower/bower.plugin.zsh | 84 + zsh/.oh-my-zsh/plugins/branch/README.md | 33 + .../plugins/branch/branch.plugin.zsh | 31 + zsh/.oh-my-zsh/plugins/brew/README.md | 30 + zsh/.oh-my-zsh/plugins/brew/brew.plugin.zsh | 9 + zsh/.oh-my-zsh/plugins/bundler/README.md | 74 + zsh/.oh-my-zsh/plugins/bundler/_bundler | 104 + .../plugins/bundler/bundler.plugin.zsh | 130 + zsh/.oh-my-zsh/plugins/cabal/README.md | 9 + zsh/.oh-my-zsh/plugins/cabal/cabal.plugin.zsh | 93 + zsh/.oh-my-zsh/plugins/cake/README.md | 15 + zsh/.oh-my-zsh/plugins/cake/cake.plugin.zsh | 33 + zsh/.oh-my-zsh/plugins/cakephp3/README.md | 16 + .../plugins/cakephp3/cakephp3.plugin.zsh | 38 + zsh/.oh-my-zsh/plugins/capistrano/README.md | 14 + zsh/.oh-my-zsh/plugins/capistrano/_capistrano | 49 + .../plugins/capistrano/capistrano.plugin.zsh | 11 + zsh/.oh-my-zsh/plugins/cargo/README.md | 11 + zsh/.oh-my-zsh/plugins/cargo/cargo.plugin.zsh | 23 + zsh/.oh-my-zsh/plugins/cask/README.md | 15 + zsh/.oh-my-zsh/plugins/cask/cask.plugin.zsh | 26 + zsh/.oh-my-zsh/plugins/catimg/README.md | 23 + .../plugins/catimg/catimg.plugin.zsh | 17 + zsh/.oh-my-zsh/plugins/catimg/catimg.sh | 88 + zsh/.oh-my-zsh/plugins/catimg/colors.png | Bin 0 -> 353 bytes zsh/.oh-my-zsh/plugins/celery/README.md | 9 + zsh/.oh-my-zsh/plugins/celery/_celery | 129 + zsh/.oh-my-zsh/plugins/chruby/README.md | 20 + .../plugins/chruby/chruby.plugin.zsh | 121 + zsh/.oh-my-zsh/plugins/chucknorris/.gitignore | 1 + zsh/.oh-my-zsh/plugins/chucknorris/README.md | 38 + .../chucknorris/chucknorris.plugin.zsh | 24 + .../plugins/chucknorris/fortunes/chucknorris | 568 +++ zsh/.oh-my-zsh/plugins/cloudfoundry/README.md | 58 + .../cloudfoundry/cloudfoundry.plugin.zsh | 34 + zsh/.oh-my-zsh/plugins/codeclimate/README.md | 8 + .../plugins/codeclimate/_codeclimate | 82 + zsh/.oh-my-zsh/plugins/coffee/README.md | 31 + zsh/.oh-my-zsh/plugins/coffee/_coffee | 81 + .../plugins/coffee/coffee.plugin.zsh | 16 + zsh/.oh-my-zsh/plugins/colemak/.gitignore | 1 + zsh/.oh-my-zsh/plugins/colemak/README.md | 48 + zsh/.oh-my-zsh/plugins/colemak/colemak-less | 6 + .../plugins/colemak/colemak.plugin.zsh | 33 + .../plugins/colored-man-pages/README.md | 32 + .../colored-man-pages.plugin.zsh | 48 + .../plugins/colored-man-pages/nroff | 12 + zsh/.oh-my-zsh/plugins/colorize/README.md | 56 + .../plugins/colorize/colorize.plugin.zsh | 114 + .../plugins/command-not-found/README.md | 33 + .../command-not-found.plugin.zsh | 62 + .../plugins/common-aliases/README.md | 123 + .../common-aliases/common-aliases.plugin.zsh | 88 + zsh/.oh-my-zsh/plugins/compleat/README.md | 9 + .../plugins/compleat/compleat.plugin.zsh | 20 + zsh/.oh-my-zsh/plugins/composer/README.md | 31 + .../plugins/composer/composer.plugin.zsh | 70 + zsh/.oh-my-zsh/plugins/copybuffer/README.md | 11 + .../plugins/copybuffer/copybuffer.plugin.zsh | 16 + zsh/.oh-my-zsh/plugins/copydir/README.md | 10 + .../plugins/copydir/copydir.plugin.zsh | 5 + zsh/.oh-my-zsh/plugins/copyfile/README.md | 11 + .../plugins/copyfile/copyfile.plugin.zsh | 7 + zsh/.oh-my-zsh/plugins/cp/README.md | 32 + zsh/.oh-my-zsh/plugins/cp/cp.plugin.zsh | 4 + zsh/.oh-my-zsh/plugins/cpanm/README.md | 9 + zsh/.oh-my-zsh/plugins/cpanm/_cpanm | 64 + zsh/.oh-my-zsh/plugins/dash/README.md | 28 + zsh/.oh-my-zsh/plugins/dash/dash.plugin.zsh | 80 + zsh/.oh-my-zsh/plugins/debian/README.md | 85 + .../plugins/debian/debian.plugin.zsh | 224 + zsh/.oh-my-zsh/plugins/deno/README.md | 18 + zsh/.oh-my-zsh/plugins/deno/deno.plugin.zsh | 35 + zsh/.oh-my-zsh/plugins/dircycle/README.md | 78 + .../plugins/dircycle/dircycle.plugin.zsh | 54 + zsh/.oh-my-zsh/plugins/direnv/README.md | 15 + .../plugins/direnv/direnv.plugin.zsh | 16 + zsh/.oh-my-zsh/plugins/dirhistory/README.md | 42 + .../plugins/dirhistory/dirhistory.plugin.zsh | 202 + zsh/.oh-my-zsh/plugins/dirpersist/README.md | 10 + .../plugins/dirpersist/dirpersist.plugin.zsh | 21 + zsh/.oh-my-zsh/plugins/django/README.md | 12 + .../plugins/django/django.plugin.zsh | 407 ++ zsh/.oh-my-zsh/plugins/dnf/README.md | 29 + zsh/.oh-my-zsh/plugins/dnf/dnf.plugin.zsh | 15 + zsh/.oh-my-zsh/plugins/dnote/README.md | 51 + zsh/.oh-my-zsh/plugins/dnote/_dnote | 39 + .../plugins/docker-compose/README.md | 32 + .../plugins/docker-compose/_docker-compose | 421 ++ .../docker-compose/docker-compose.plugin.zsh | 24 + .../plugins/docker-machine/README.md | 19 + .../plugins/docker-machine/_docker-machine | 359 ++ .../docker-machine/docker-machine.plugin.zsh | 33 + zsh/.oh-my-zsh/plugins/docker/README.md | 34 + zsh/.oh-my-zsh/plugins/docker/_docker | 3143 ++++++++++++++ zsh/.oh-my-zsh/plugins/doctl/README.md | 9 + zsh/.oh-my-zsh/plugins/doctl/doctl.plugin.zsh | 9 + zsh/.oh-my-zsh/plugins/dotenv/README.md | 92 + .../plugins/dotenv/dotenv.plugin.zsh | 64 + zsh/.oh-my-zsh/plugins/dotnet/README.md | 23 + .../plugins/dotnet/dotnet.plugin.zsh | 32 + zsh/.oh-my-zsh/plugins/droplr/README.md | 19 + .../plugins/droplr/droplr.plugin.zsh | 15 + zsh/.oh-my-zsh/plugins/drush/README.md | 83 + .../plugins/drush/drush.complete.sh | 50 + zsh/.oh-my-zsh/plugins/drush/drush.plugin.zsh | 104 + zsh/.oh-my-zsh/plugins/eecms/README.md | 11 + zsh/.oh-my-zsh/plugins/eecms/eecms.plugin.zsh | 20 + zsh/.oh-my-zsh/plugins/emacs/README.md | 30 + zsh/.oh-my-zsh/plugins/emacs/emacs.plugin.zsh | 63 + zsh/.oh-my-zsh/plugins/emacs/emacsclient.sh | 38 + zsh/.oh-my-zsh/plugins/ember-cli/README.md | 22 + .../plugins/ember-cli/ember-cli.plugin.zsh | 17 + zsh/.oh-my-zsh/plugins/emoji-clock/README.md | 14 + .../emoji-clock/emoji-clock.plugin.zsh | 33 + zsh/.oh-my-zsh/plugins/emoji/README.md | 135 + .../plugins/emoji/emoji-char-definitions.zsh | 1303 ++++++ zsh/.oh-my-zsh/plugins/emoji/emoji-data.txt | 1308 ++++++ zsh/.oh-my-zsh/plugins/emoji/emoji.plugin.zsh | 288 ++ zsh/.oh-my-zsh/plugins/emoji/update_emoji.pl | 113 + zsh/.oh-my-zsh/plugins/emotty/README.md | 39 + .../plugins/emotty/emotty.plugin.zsh | 50 + .../plugins/emotty/emotty_emoji_set.zsh | 24 + .../plugins/emotty/emotty_floral_set.zsh | 18 + .../plugins/emotty/emotty_love_set.zsh | 34 + .../plugins/emotty/emotty_nature_set.zsh | 58 + .../plugins/emotty/emotty_stellar_set.zsh | 25 + .../plugins/emotty/emotty_zodiac_set.zsh | 29 + zsh/.oh-my-zsh/plugins/encode64/README.md | 58 + .../plugins/encode64/encode64.plugin.zsh | 17 + zsh/.oh-my-zsh/plugins/extract/README.md | 60 + zsh/.oh-my-zsh/plugins/extract/_extract | 7 + .../plugins/extract/extract.plugin.zsh | 85 + zsh/.oh-my-zsh/plugins/fabric/README.md | 9 + zsh/.oh-my-zsh/plugins/fabric/_fab | 69 + .../plugins/fabric/fabric.plugin.zsh | 0 zsh/.oh-my-zsh/plugins/fancy-ctrl-z/README.md | 14 + .../fancy-ctrl-z/fancy-ctrl-z.plugin.zsh | 12 + zsh/.oh-my-zsh/plugins/fasd/README.md | 21 + zsh/.oh-my-zsh/plugins/fasd/fasd.plugin.zsh | 16 + zsh/.oh-my-zsh/plugins/fastfile/README.md | 84 + .../plugins/fastfile/fastfile.plugin.zsh | 127 + zsh/.oh-my-zsh/plugins/fbterm/README.md | 10 + .../plugins/fbterm/fbterm.plugin.zsh | 7 + zsh/.oh-my-zsh/plugins/fd/README.md | 13 + zsh/.oh-my-zsh/plugins/fd/_fd | 83 + zsh/.oh-my-zsh/plugins/firewalld/README.md | 22 + .../plugins/firewalld/firewalld.plugin.zsh | 17 + zsh/.oh-my-zsh/plugins/flutter/README.md | 21 + zsh/.oh-my-zsh/plugins/flutter/_flutter | 37 + .../plugins/flutter/flutter.plugin.zsh | 7 + zsh/.oh-my-zsh/plugins/fnm/README.md | 9 + zsh/.oh-my-zsh/plugins/fnm/fnm.plugin.zsh | 23 + zsh/.oh-my-zsh/plugins/forklift/README.md | 23 + .../plugins/forklift/forklift.plugin.zsh | 122 + zsh/.oh-my-zsh/plugins/fossil/README.md | 7 + .../plugins/fossil/fossil.plugin.zsh | 89 + .../plugins/frontend-search/README.md | 75 + .../frontend-search/_frontend-search.sh | 161 + .../frontend-search.plugin.zsh | 114 + zsh/.oh-my-zsh/plugins/fzf/README.md | 52 + zsh/.oh-my-zsh/plugins/fzf/fzf.plugin.zsh | 174 + zsh/.oh-my-zsh/plugins/gas/README.md | 10 + zsh/.oh-my-zsh/plugins/gas/_gas | 39 + zsh/.oh-my-zsh/plugins/gatsby/README.md | 7 + zsh/.oh-my-zsh/plugins/gatsby/_gatsby | 24 + zsh/.oh-my-zsh/plugins/gb/README.md | 21 + zsh/.oh-my-zsh/plugins/gb/_gb | 111 + zsh/.oh-my-zsh/plugins/gcloud/README.md | 24 + .../plugins/gcloud/gcloud.plugin.zsh | 34 + zsh/.oh-my-zsh/plugins/geeknote/README.md | 10 + zsh/.oh-my-zsh/plugins/geeknote/_geeknote | 157 + .../plugins/geeknote/geeknote.plugin.zsh | 2 + zsh/.oh-my-zsh/plugins/gem/README.md | 17 + zsh/.oh-my-zsh/plugins/gem/_gem | 72 + zsh/.oh-my-zsh/plugins/gem/gem.plugin.zsh | 7 + zsh/.oh-my-zsh/plugins/genpass/README.md | 66 + zsh/.oh-my-zsh/plugins/genpass/genpass-apple | 79 + zsh/.oh-my-zsh/plugins/genpass/genpass-monkey | 32 + zsh/.oh-my-zsh/plugins/genpass/genpass-xkcd | 68 + .../plugins/genpass/genpass.plugin.zsh | 1 + zsh/.oh-my-zsh/plugins/gh/README.md | 23 + zsh/.oh-my-zsh/plugins/gh/gh.plugin.zsh | 24 + .../plugins/git-auto-fetch/README.md | 50 + .../git-auto-fetch/git-auto-fetch.plugin.zsh | 63 + .../plugins/git-escape-magic/README.md | 16 + .../plugins/git-escape-magic/git-escape-magic | 135 + .../git-escape-magic.plugin.zsh | 9 + zsh/.oh-my-zsh/plugins/git-extras/README.md | 17 + .../plugins/git-extras/git-extras.plugin.zsh | 498 +++ zsh/.oh-my-zsh/plugins/git-flow-avh/README.md | 19 + .../git-flow-avh/git-flow-avh.plugin.zsh | 526 +++ zsh/.oh-my-zsh/plugins/git-flow/README.md | 31 + .../plugins/git-flow/git-flow.plugin.zsh | 369 ++ zsh/.oh-my-zsh/plugins/git-hubflow/README.md | 24 + .../git-hubflow/git-hubflow.plugin.zsh | 333 ++ zsh/.oh-my-zsh/plugins/git-lfs/README.md | 24 + .../plugins/git-lfs/git-lfs.plugin.zsh | 17 + zsh/.oh-my-zsh/plugins/git-prompt/README.md | 66 + .../plugins/git-prompt/git-prompt.plugin.zsh | 96 + .../plugins/git-prompt/gitstatus.py | 100 + zsh/.oh-my-zsh/plugins/git/README.md | 244 ++ zsh/.oh-my-zsh/plugins/git/git.plugin.zsh | 336 ++ zsh/.oh-my-zsh/plugins/gitfast/README.md | 15 + zsh/.oh-my-zsh/plugins/gitfast/_git | 292 ++ .../plugins/gitfast/git-completion.bash | 3653 +++++++++++++++++ zsh/.oh-my-zsh/plugins/gitfast/git-prompt.sh | 585 +++ .../plugins/gitfast/gitfast.plugin.zsh | 6 + zsh/.oh-my-zsh/plugins/gitfast/update | 8 + zsh/.oh-my-zsh/plugins/github/README.md | 46 + zsh/.oh-my-zsh/plugins/github/_hub | 174 + .../plugins/github/github.plugin.zsh | 76 + zsh/.oh-my-zsh/plugins/gitignore/README.md | 17 + .../plugins/gitignore/gitignore.plugin.zsh | 12 + zsh/.oh-my-zsh/plugins/glassfish/README.md | 9 + zsh/.oh-my-zsh/plugins/glassfish/_asadmin | 1150 ++++++ .../plugins/glassfish/glassfish.plugin.zsh | 0 zsh/.oh-my-zsh/plugins/globalias/README.md | 79 + .../plugins/globalias/globalias.plugin.zsh | 23 + zsh/.oh-my-zsh/plugins/gnu-utils/README.md | 38 + .../plugins/gnu-utils/gnu-utils.plugin.zsh | 83 + zsh/.oh-my-zsh/plugins/golang/README.md | 32 + .../plugins/golang/golang.plugin.zsh | 276 ++ .../plugins/golang/templates/package.txt | 29 + .../plugins/golang/templates/search.txt | 0 zsh/.oh-my-zsh/plugins/gpg-agent/README.md | 9 + .../plugins/gpg-agent/gpg-agent.plugin.zsh | 17 + zsh/.oh-my-zsh/plugins/gradle/README.md | 30 + zsh/.oh-my-zsh/plugins/gradle/_gradle | 420 ++ .../plugins/gradle/gradle.plugin.zsh | 26 + zsh/.oh-my-zsh/plugins/grails/README.md | 16 + .../plugins/grails/grails.plugin.zsh | 60 + zsh/.oh-my-zsh/plugins/grc/README.md | 15 + zsh/.oh-my-zsh/plugins/grc/grc.plugin.zsh | 17 + zsh/.oh-my-zsh/plugins/grunt/README.md | 37 + zsh/.oh-my-zsh/plugins/grunt/grunt.plugin.zsh | 255 ++ zsh/.oh-my-zsh/plugins/gulp/README.md | 8 + zsh/.oh-my-zsh/plugins/gulp/gulp.plugin.zsh | 29 + zsh/.oh-my-zsh/plugins/hanami/README.md | 45 + .../plugins/hanami/hanami.plugin.zsh | 19 + zsh/.oh-my-zsh/plugins/helm/README.md | 9 + zsh/.oh-my-zsh/plugins/helm/helm.plugin.zsh | 7 + zsh/.oh-my-zsh/plugins/heroku/README.md | 9 + .../plugins/heroku/heroku.plugin.zsh | 9 + .../history-substring-search/README.md | 198 + .../history-substring-search.plugin.zsh | 15 + .../history-substring-search.zsh | 759 ++++ .../update-from-upstream.zsh | 129 + zsh/.oh-my-zsh/plugins/history/README.md | 17 + .../plugins/history/history.plugin.zsh | 3 + zsh/.oh-my-zsh/plugins/hitchhiker/.gitignore | 1 + zsh/.oh-my-zsh/plugins/hitchhiker/README.md | 44 + .../plugins/hitchhiker/fortunes/hitchhiker | 275 ++ .../plugins/hitchhiker/hitchhiker.plugin.zsh | 23 + zsh/.oh-my-zsh/plugins/hitokoto/README.md | 15 + .../plugins/hitokoto/hitokoto.plugin.zsh | 14 + zsh/.oh-my-zsh/plugins/homestead/README.md | 9 + .../plugins/homestead/homestead.plugin.zsh | 10 + zsh/.oh-my-zsh/plugins/httpie/README.md | 20 + zsh/.oh-my-zsh/plugins/httpie/_httpie | 181 + .../plugins/httpie/httpie.plugin.zsh | 7 + zsh/.oh-my-zsh/plugins/invoke/README.md | 10 + .../plugins/invoke/invoke.plugin.zsh | 5 + zsh/.oh-my-zsh/plugins/ionic/README.md | 30 + zsh/.oh-my-zsh/plugins/ionic/ionic.plugin.zsh | 15 + zsh/.oh-my-zsh/plugins/ipfs/LICENSE | 22 + zsh/.oh-my-zsh/plugins/ipfs/README.md | 17 + zsh/.oh-my-zsh/plugins/ipfs/_ipfs | 717 ++++ zsh/.oh-my-zsh/plugins/isodate/README.md | 22 + .../plugins/isodate/isodate.plugin.zsh | 7 + zsh/.oh-my-zsh/plugins/iterm2/README.md | 29 + .../plugins/iterm2/iterm2.plugin.zsh | 68 + zsh/.oh-my-zsh/plugins/jake-node/README.md | 9 + .../plugins/jake-node/jake-node.plugin.zsh | 14 + zsh/.oh-my-zsh/plugins/jenv/README.md | 27 + zsh/.oh-my-zsh/plugins/jenv/jenv.plugin.zsh | 30 + zsh/.oh-my-zsh/plugins/jfrog/README.md | 11 + zsh/.oh-my-zsh/plugins/jfrog/jfrog.plugin.zsh | 10 + zsh/.oh-my-zsh/plugins/jhbuild/README.md | 34 + .../plugins/jhbuild/jhbuild.plugin.zsh | 32 + zsh/.oh-my-zsh/plugins/jira/README.md | 70 + zsh/.oh-my-zsh/plugins/jira/_jira | 24 + zsh/.oh-my-zsh/plugins/jira/jira.plugin.zsh | 133 + zsh/.oh-my-zsh/plugins/jruby/README.md | 21 + zsh/.oh-my-zsh/plugins/jruby/jruby.plugin.zsh | 4 + zsh/.oh-my-zsh/plugins/jsontools/README.md | 79 + .../plugins/jsontools/jsontools.plugin.zsh | 113 + zsh/.oh-my-zsh/plugins/juju/README.md | 117 + zsh/.oh-my-zsh/plugins/juju/juju.plugin.zsh | 127 + zsh/.oh-my-zsh/plugins/jump/README.md | 31 + zsh/.oh-my-zsh/plugins/jump/jump.plugin.zsh | 59 + zsh/.oh-my-zsh/plugins/kate/README.md | 20 + zsh/.oh-my-zsh/plugins/kate/kate.plugin.zsh | 9 + zsh/.oh-my-zsh/plugins/keychain/README.md | 45 + .../plugins/keychain/keychain.plugin.zsh | 32 + zsh/.oh-my-zsh/plugins/kitchen/README.md | 9 + zsh/.oh-my-zsh/plugins/kitchen/_kitchen | 85 + zsh/.oh-my-zsh/plugins/knife/README.md | 25 + zsh/.oh-my-zsh/plugins/knife/_knife | 257 ++ zsh/.oh-my-zsh/plugins/knife_ssh/README.md | 14 + .../plugins/knife_ssh/knife_ssh.plugin.zsh | 18 + zsh/.oh-my-zsh/plugins/kops/README.md | 12 + zsh/.oh-my-zsh/plugins/kops/kops.plugin.zsh | 3 + zsh/.oh-my-zsh/plugins/kube-ps1/README.md | 238 ++ .../plugins/kube-ps1/kube-ps1.plugin.zsh | 371 ++ zsh/.oh-my-zsh/plugins/kubectl/README.md | 130 + .../plugins/kubectl/kubectl.plugin.zsh | 180 + zsh/.oh-my-zsh/plugins/kubectx/README.md | 26 + .../plugins/kubectx/kubectx.plugin.zsh | 9 + zsh/.oh-my-zsh/plugins/kubectx/prod.png | Bin 0 -> 3834 bytes zsh/.oh-my-zsh/plugins/kubectx/stage.png | Bin 0 -> 3829 bytes zsh/.oh-my-zsh/plugins/lando/LICENSE | 21 + zsh/.oh-my-zsh/plugins/lando/README.md | 37 + zsh/.oh-my-zsh/plugins/lando/lando.plugin.zsh | 41 + zsh/.oh-my-zsh/plugins/laravel/README.md | 57 + zsh/.oh-my-zsh/plugins/laravel/_artisan | 40 + .../plugins/laravel/laravel.plugin.zsh | 41 + zsh/.oh-my-zsh/plugins/laravel4/README.md | 18 + .../plugins/laravel4/laravel4.plugin.zsh | 20 + zsh/.oh-my-zsh/plugins/laravel5/README.md | 18 + .../plugins/laravel5/laravel5.plugin.zsh | 19 + .../plugins/last-working-dir/README.md | 33 + .../last-working-dir.plugin.zsh | 28 + zsh/.oh-my-zsh/plugins/lein/README.md | 9 + zsh/.oh-my-zsh/plugins/lein/_lein | 69 + zsh/.oh-my-zsh/plugins/lighthouse/README.md | 29 + .../plugins/lighthouse/lighthouse.plugin.zsh | 12 + zsh/.oh-my-zsh/plugins/lol/README.md | 71 + zsh/.oh-my-zsh/plugins/lol/lol.plugin.zsh | 51 + zsh/.oh-my-zsh/plugins/lxd/README.md | 9 + zsh/.oh-my-zsh/plugins/lxd/lxd.plugin.zsh | 26 + zsh/.oh-my-zsh/plugins/macports/README.md | 21 + zsh/.oh-my-zsh/plugins/macports/_port | 92 + .../plugins/macports/macports.plugin.zsh | 6 + zsh/.oh-my-zsh/plugins/magic-enter/README.md | 17 + .../magic-enter/magic-enter.plugin.zsh | 38 + zsh/.oh-my-zsh/plugins/man/README.md | 13 + zsh/.oh-my-zsh/plugins/man/man.plugin.zsh | 37 + zsh/.oh-my-zsh/plugins/marked2/README.md | 13 + .../plugins/marked2/marked2.plugin.zsh | 12 + zsh/.oh-my-zsh/plugins/mercurial/README.md | 67 + .../plugins/mercurial/mercurial.plugin.zsh | 74 + zsh/.oh-my-zsh/plugins/meteor/README.md | 46 + zsh/.oh-my-zsh/plugins/meteor/_meteor | 67 + .../plugins/meteor/meteor.plugin.zsh | 33 + zsh/.oh-my-zsh/plugins/microk8s/README.md | 24 + .../plugins/microk8s/microk8s.plugin.zsh | 82 + zsh/.oh-my-zsh/plugins/minikube/README.md | 9 + .../plugins/minikube/minikube.plugin.zsh | 13 + zsh/.oh-my-zsh/plugins/mix-fast/README.md | 28 + .../plugins/mix-fast/mix-fast.plugin.zsh | 30 + zsh/.oh-my-zsh/plugins/mix/README.md | 19 + zsh/.oh-my-zsh/plugins/mix/_mix | 129 + zsh/.oh-my-zsh/plugins/mongocli/README.md | 19 + .../plugins/mongocli/mongocli.plugin.zsh | 4 + zsh/.oh-my-zsh/plugins/mosh/README.md | 9 + zsh/.oh-my-zsh/plugins/mosh/mosh.plugin.zsh | 2 + zsh/.oh-my-zsh/plugins/mvn/README.md | 61 + zsh/.oh-my-zsh/plugins/mvn/mvn.plugin.zsh | 342 ++ .../plugins/mysql-macports/README.md | 20 + .../mysql-macports/mysql-macports.plugin.zsh | 8 + zsh/.oh-my-zsh/plugins/n98-magerun/README.md | 21 + .../n98-magerun/n98-magerun.plugin.zsh | 42 + zsh/.oh-my-zsh/plugins/nanoc/README.md | 20 + zsh/.oh-my-zsh/plugins/nanoc/_nanoc | 92 + zsh/.oh-my-zsh/plugins/nanoc/nanoc.plugin.zsh | 6 + zsh/.oh-my-zsh/plugins/ng/README.md | 10 + zsh/.oh-my-zsh/plugins/ng/ng.plugin.zsh | 78 + zsh/.oh-my-zsh/plugins/nmap/README.md | 27 + zsh/.oh-my-zsh/plugins/nmap/nmap.plugin.zsh | 32 + zsh/.oh-my-zsh/plugins/node/README.md | 19 + zsh/.oh-my-zsh/plugins/node/node.plugin.zsh | 6 + zsh/.oh-my-zsh/plugins/nomad/README.md | 15 + zsh/.oh-my-zsh/plugins/nomad/_nomad | 153 + zsh/.oh-my-zsh/plugins/npm/README.md | 31 + zsh/.oh-my-zsh/plugins/npm/npm.plugin.zsh | 71 + zsh/.oh-my-zsh/plugins/npx/README.md | 39 + zsh/.oh-my-zsh/plugins/npx/npx.plugin.zsh | 7 + zsh/.oh-my-zsh/plugins/nvm/README.md | 32 + zsh/.oh-my-zsh/plugins/nvm/_nvm | 34 + zsh/.oh-my-zsh/plugins/nvm/nvm.plugin.zsh | 77 + zsh/.oh-my-zsh/plugins/oc/README.md | 13 + zsh/.oh-my-zsh/plugins/oc/oc.plugin.zsh | 7 + zsh/.oh-my-zsh/plugins/octozen/README.md | 12 + .../plugins/octozen/octozen.plugin.zsh | 11 + zsh/.oh-my-zsh/plugins/osx/README.md | 62 + zsh/.oh-my-zsh/plugins/osx/_security | 90 + zsh/.oh-my-zsh/plugins/osx/music | 170 + zsh/.oh-my-zsh/plugins/osx/osx.plugin.zsh | 240 ++ zsh/.oh-my-zsh/plugins/osx/spotify | 478 +++ zsh/.oh-my-zsh/plugins/otp/README.md | 22 + zsh/.oh-my-zsh/plugins/otp/otp.plugin.zsh | 45 + zsh/.oh-my-zsh/plugins/pass/README.md | 22 + zsh/.oh-my-zsh/plugins/pass/_pass | 153 + zsh/.oh-my-zsh/plugins/paver/README.md | 12 + zsh/.oh-my-zsh/plugins/paver/paver.plugin.zsh | 16 + zsh/.oh-my-zsh/plugins/pep8/README.md | 8 + zsh/.oh-my-zsh/plugins/pep8/_pep8 | 34 + .../plugins/per-directory-history/README.md | 48 + .../per-directory-history.plugin.zsh | 1 + .../per-directory-history.zsh | 174 + zsh/.oh-my-zsh/plugins/percol/README.md | 20 + .../plugins/percol/percol.plugin.zsh | 22 + zsh/.oh-my-zsh/plugins/perl/README.md | 37 + zsh/.oh-my-zsh/plugins/perl/perl.plugin.zsh | 56 + zsh/.oh-my-zsh/plugins/perms/README.md | 15 + zsh/.oh-my-zsh/plugins/perms/perms.plugin.zsh | 82 + zsh/.oh-my-zsh/plugins/phing/README.md | 9 + zsh/.oh-my-zsh/plugins/phing/phing.plugin.zsh | 7 + zsh/.oh-my-zsh/plugins/pip/README.md | 28 + zsh/.oh-my-zsh/plugins/pip/_pip | 100 + zsh/.oh-my-zsh/plugins/pip/pip.plugin.zsh | 97 + zsh/.oh-my-zsh/plugins/pipenv/README.md | 28 + .../plugins/pipenv/pipenv.plugin.zsh | 44 + zsh/.oh-my-zsh/plugins/pj/README.md | 45 + zsh/.oh-my-zsh/plugins/pj/pj.plugin.zsh | 37 + zsh/.oh-my-zsh/plugins/please/README.md | 26 + .../plugins/please/please.plugin.zsh | 7 + zsh/.oh-my-zsh/plugins/pm2/README.md | 19 + zsh/.oh-my-zsh/plugins/pm2/_pm2 | 168 + zsh/.oh-my-zsh/plugins/pm2/pm2.plugin.zsh | 6 + zsh/.oh-my-zsh/plugins/pod/README.md | 10 + zsh/.oh-my-zsh/plugins/pod/_pod | 682 +++ zsh/.oh-my-zsh/plugins/postgres/README.md | 22 + .../plugins/postgres/postgres.plugin.zsh | 8 + zsh/.oh-my-zsh/plugins/pow/README.md | 21 + zsh/.oh-my-zsh/plugins/pow/pow.plugin.zsh | 85 + zsh/.oh-my-zsh/plugins/powder/README.md | 8 + zsh/.oh-my-zsh/plugins/powder/_powder | 4 + zsh/.oh-my-zsh/plugins/powify/README.md | 10 + zsh/.oh-my-zsh/plugins/powify/_powify | 55 + zsh/.oh-my-zsh/plugins/profiles/README.md | 25 + .../plugins/profiles/profiles.plugin.zsh | 12 + zsh/.oh-my-zsh/plugins/pyenv/README.md | 24 + zsh/.oh-my-zsh/plugins/pyenv/pyenv.plugin.zsh | 96 + zsh/.oh-my-zsh/plugins/pylint/README.md | 16 + zsh/.oh-my-zsh/plugins/pylint/_pylint | 31 + .../plugins/pylint/pylint.plugin.zsh | 1 + zsh/.oh-my-zsh/plugins/python/README.md | 19 + .../plugins/python/python.plugin.zsh | 50 + zsh/.oh-my-zsh/plugins/rails/README.md | 82 + zsh/.oh-my-zsh/plugins/rails/_rails | 66 + zsh/.oh-my-zsh/plugins/rails/rails.plugin.zsh | 90 + zsh/.oh-my-zsh/plugins/rake-fast/README.md | 35 + .../plugins/rake-fast/rake-fast.plugin.zsh | 43 + zsh/.oh-my-zsh/plugins/rake/README.md | 37 + zsh/.oh-my-zsh/plugins/rake/rake.plugin.zsh | 10 + zsh/.oh-my-zsh/plugins/rand-quote/README.md | 15 + .../plugins/rand-quote/rand-quote.plugin.zsh | 14 + zsh/.oh-my-zsh/plugins/rbenv/README.md | 26 + zsh/.oh-my-zsh/plugins/rbenv/rbenv.plugin.zsh | 68 + zsh/.oh-my-zsh/plugins/rbfu/README.md | 17 + zsh/.oh-my-zsh/plugins/rbfu/rbfu.plugin.zsh | 42 + zsh/.oh-my-zsh/plugins/react-native/README.md | 76 + .../plugins/react-native/_react-native | 32 + .../react-native/react-native.plugin.zsh | 67 + zsh/.oh-my-zsh/plugins/rebar/README.md | 9 + zsh/.oh-my-zsh/plugins/rebar/_rebar | 79 + zsh/.oh-my-zsh/plugins/redis-cli/README.md | 15 + zsh/.oh-my-zsh/plugins/redis-cli/_redis-cli | 142 + zsh/.oh-my-zsh/plugins/repo/README.md | 25 + zsh/.oh-my-zsh/plugins/repo/_repo | 270 ++ zsh/.oh-my-zsh/plugins/repo/repo.plugin.zsh | 10 + zsh/.oh-my-zsh/plugins/ripgrep/README.md | 13 + zsh/.oh-my-zsh/plugins/ripgrep/_ripgrep | 612 +++ zsh/.oh-my-zsh/plugins/ros/README.md | 10 + zsh/.oh-my-zsh/plugins/ros/_ros | 64 + zsh/.oh-my-zsh/plugins/rsync/README.md | 16 + zsh/.oh-my-zsh/plugins/rsync/rsync.plugin.zsh | 4 + zsh/.oh-my-zsh/plugins/ruby/README.md | 20 + zsh/.oh-my-zsh/plugins/ruby/ruby.plugin.zsh | 14 + zsh/.oh-my-zsh/plugins/rust/README.md | 9 + zsh/.oh-my-zsh/plugins/rust/_rust | 228 + zsh/.oh-my-zsh/plugins/rustup/README.md | 9 + .../plugins/rustup/rustup.plugin.zsh | 22 + zsh/.oh-my-zsh/plugins/rvm/README.md | 20 + zsh/.oh-my-zsh/plugins/rvm/rvm.plugin.zsh | 74 + zsh/.oh-my-zsh/plugins/safe-paste/README.md | 9 + .../plugins/safe-paste/safe-paste.plugin.zsh | 100 + zsh/.oh-my-zsh/plugins/salt/README.md | 5 + zsh/.oh-my-zsh/plugins/salt/_salt | 279 ++ zsh/.oh-my-zsh/plugins/samtools/README.md | 5 + zsh/.oh-my-zsh/plugins/samtools/_samtools | 40 + zsh/.oh-my-zsh/plugins/sbt/README.md | 32 + zsh/.oh-my-zsh/plugins/sbt/_sbt | 56 + zsh/.oh-my-zsh/plugins/sbt/sbt.plugin.zsh | 25 + zsh/.oh-my-zsh/plugins/scala/README.md | 16 + zsh/.oh-my-zsh/plugins/scala/_scala | 249 ++ zsh/.oh-my-zsh/plugins/scd/README.md | 159 + zsh/.oh-my-zsh/plugins/scd/_scd | 60 + zsh/.oh-my-zsh/plugins/scd/scd | 533 +++ zsh/.oh-my-zsh/plugins/scd/scd.plugin.zsh | 17 + zsh/.oh-my-zsh/plugins/screen/README.md | 10 + .../plugins/screen/screen.plugin.zsh | 54 + zsh/.oh-my-zsh/plugins/scw/README.md | 7 + zsh/.oh-my-zsh/plugins/scw/_scw | 333 ++ zsh/.oh-my-zsh/plugins/sdk/README.md | 14 + zsh/.oh-my-zsh/plugins/sdk/sdk.plugin.zsh | 58 + zsh/.oh-my-zsh/plugins/sfdx/README.md | 11 + zsh/.oh-my-zsh/plugins/sfdx/_sfdx | 1110 +++++ zsh/.oh-my-zsh/plugins/sfffe/README.md | 17 + zsh/.oh-my-zsh/plugins/sfffe/sfffe.plugin.zsh | 28 + zsh/.oh-my-zsh/plugins/shell-proxy/README.md | 52 + zsh/.oh-my-zsh/plugins/shell-proxy/proxy.py | 73 + .../shell-proxy/shell-proxy.plugin.zsh | 16 + .../plugins/shell-proxy/ssh-agent.py | 16 + .../plugins/shell-proxy/ssh-proxy.py | 18 + zsh/.oh-my-zsh/plugins/shrink-path/README.md | 116 + .../shrink-path/shrink-path.plugin.zsh | 182 + zsh/.oh-my-zsh/plugins/singlechar/README.md | 118 + .../plugins/singlechar/singlechar.plugin.zsh | 123 + zsh/.oh-my-zsh/plugins/spring/README.md | 25 + zsh/.oh-my-zsh/plugins/spring/_spring | 29 + zsh/.oh-my-zsh/plugins/sprunge/README.md | 32 + .../plugins/sprunge/sprunge.plugin.zsh | 56 + zsh/.oh-my-zsh/plugins/ssh-agent/README.md | 96 + .../plugins/ssh-agent/ssh-agent.plugin.zsh | 105 + zsh/.oh-my-zsh/plugins/stack/README.md | 9 + zsh/.oh-my-zsh/plugins/stack/stack.plugin.zsh | 4 + .../plugins/sublime-merge/README.md | 17 + .../sublime-merge/sublime-merge.plugin.zsh | 55 + zsh/.oh-my-zsh/plugins/sublime/README.md | 37 + .../plugins/sublime/sublime.plugin.zsh | 124 + zsh/.oh-my-zsh/plugins/sudo/README.md | 45 + zsh/.oh-my-zsh/plugins/sudo/sudo.plugin.zsh | 95 + zsh/.oh-my-zsh/plugins/supervisor/README.md | 13 + .../plugins/supervisor/_supervisorctl | 143 + .../plugins/supervisor/_supervisord | 33 + .../plugins/supervisor/supervisor.plugin.zsh | 14 + zsh/.oh-my-zsh/plugins/suse/README.md | 96 + zsh/.oh-my-zsh/plugins/suse/suse.plugin.zsh | 59 + zsh/.oh-my-zsh/plugins/svcat/README.md | 9 + zsh/.oh-my-zsh/plugins/svcat/svcat.plugin.zsh | 6 + .../plugins/svn-fast-info/README.md | 56 + .../svn-fast-info/svn-fast-info.plugin.zsh | 72 + zsh/.oh-my-zsh/plugins/svn/README.md | 67 + zsh/.oh-my-zsh/plugins/svn/svn.plugin.zsh | 87 + zsh/.oh-my-zsh/plugins/swiftpm/README.md | 22 + zsh/.oh-my-zsh/plugins/swiftpm/_swift | 474 +++ .../plugins/swiftpm/swiftpm.plugin.zsh | 8 + zsh/.oh-my-zsh/plugins/symfony/README.md | 9 + .../plugins/symfony/symfony.plugin.zsh | 13 + zsh/.oh-my-zsh/plugins/symfony2/README.md | 28 + .../plugins/symfony2/symfony2.plugin.zsh | 34 + zsh/.oh-my-zsh/plugins/systemadmin/README.md | 51 + .../systemadmin/systemadmin.plugin.zsh | 155 + zsh/.oh-my-zsh/plugins/systemd/README.md | 94 + .../plugins/systemd/systemd.plugin.zsh | 90 + zsh/.oh-my-zsh/plugins/taskwarrior/README.md | 18 + zsh/.oh-my-zsh/plugins/taskwarrior/_task | 285 ++ .../taskwarrior/taskwarrior.plugin.zsh | 7 + zsh/.oh-my-zsh/plugins/term_tab/README | 16 + .../plugins/term_tab/term_tab.plugin.zsh | 41 + zsh/.oh-my-zsh/plugins/terminitor/README.md | 9 + zsh/.oh-my-zsh/plugins/terminitor/_terminitor | 38 + zsh/.oh-my-zsh/plugins/terraform/README.md | 29 + zsh/.oh-my-zsh/plugins/terraform/_terraform | 411 ++ .../plugins/terraform/terraform.plugin.zsh | 11 + zsh/.oh-my-zsh/plugins/textastic/README.md | 15 + .../plugins/textastic/textastic.plugin.zsh | 17 + zsh/.oh-my-zsh/plugins/textmate/README.md | 17 + .../plugins/textmate/textmate.plugin.zsh | 14 + zsh/.oh-my-zsh/plugins/thefuck/README.md | 13 + .../plugins/thefuck/thefuck.plugin.zsh | 21 + zsh/.oh-my-zsh/plugins/themes/README.md | 18 + .../plugins/themes/themes.plugin.zsh | 35 + zsh/.oh-my-zsh/plugins/thor/README.md | 10 + zsh/.oh-my-zsh/plugins/thor/_thor | 4 + zsh/.oh-my-zsh/plugins/tig/README.md | 16 + zsh/.oh-my-zsh/plugins/tig/tig.plugin.zsh | 3 + zsh/.oh-my-zsh/plugins/timer/README.md | 18 + zsh/.oh-my-zsh/plugins/timer/timer.plugin.zsh | 35 + zsh/.oh-my-zsh/plugins/tmux-cssh/README.md | 10 + zsh/.oh-my-zsh/plugins/tmux-cssh/_tmux-cssh | 25 + zsh/.oh-my-zsh/plugins/tmux/README.md | 41 + zsh/.oh-my-zsh/plugins/tmux/tmux.extra.conf | 2 + zsh/.oh-my-zsh/plugins/tmux/tmux.only.conf | 1 + zsh/.oh-my-zsh/plugins/tmux/tmux.plugin.zsh | 99 + zsh/.oh-my-zsh/plugins/tmuxinator/README.md | 19 + zsh/.oh-my-zsh/plugins/tmuxinator/_tmuxinator | 23 + .../plugins/tmuxinator/tmuxinator.plugin.zsh | 5 + zsh/.oh-my-zsh/plugins/torrent/README.md | 13 + .../plugins/torrent/torrent.plugin.zsh | 17 + zsh/.oh-my-zsh/plugins/transfer/README.md | 24 + .../plugins/transfer/transfer.plugin.zsh | 69 + zsh/.oh-my-zsh/plugins/tugboat/README.md | 12 + zsh/.oh-my-zsh/plugins/tugboat/_tugboat | 106 + zsh/.oh-my-zsh/plugins/ubuntu/README.md | 60 + .../plugins/ubuntu/ubuntu.plugin.zsh | 127 + zsh/.oh-my-zsh/plugins/ufw/README.md | 18 + zsh/.oh-my-zsh/plugins/ufw/_ufw | 115 + .../plugins/universalarchive/README.md | 46 + .../universalarchive/_universalarchive | 6 + .../universalarchive.plugin.zsh | 70 + zsh/.oh-my-zsh/plugins/urltools/README.md | 29 + .../plugins/urltools/urltools.plugin.zsh | 42 + .../plugins/vagrant-prompt/README.md | 6 + .../vagrant-prompt/vagrant-prompt.plugin.zsh | 33 + zsh/.oh-my-zsh/plugins/vagrant/README.md | 40 + zsh/.oh-my-zsh/plugins/vagrant/_vagrant | 133 + .../plugins/vagrant/vagrant.plugin.zsh | 33 + zsh/.oh-my-zsh/plugins/vault/README.md | 15 + zsh/.oh-my-zsh/plugins/vault/_vault | 400 ++ zsh/.oh-my-zsh/plugins/vi-mode/README.md | 127 + .../plugins/vi-mode/vi-mode.plugin.zsh | 145 + .../plugins/vim-interaction/README.md | 82 + .../vim-interaction.plugin.zsh | 69 + zsh/.oh-my-zsh/plugins/virtualenv/README.md | 15 + .../plugins/virtualenv/virtualenv.plugin.zsh | 7 + .../plugins/virtualenvwrapper/README.md | 38 + .../virtualenvwrapper.plugin.zsh | 85 + zsh/.oh-my-zsh/plugins/vscode/README.md | 78 + .../plugins/vscode/vscode.plugin.zsh | 41 + zsh/.oh-my-zsh/plugins/vundle/README.md | 19 + .../plugins/vundle/vundle.plugin.zsh | 27 + zsh/.oh-my-zsh/plugins/wakeonlan/README.md | 43 + zsh/.oh-my-zsh/plugins/wakeonlan/_wake | 4 + .../plugins/wakeonlan/wakeonlan.plugin.zsh | 14 + zsh/.oh-my-zsh/plugins/wd/LICENSE | 21 + zsh/.oh-my-zsh/plugins/wd/README.md | 260 ++ zsh/.oh-my-zsh/plugins/wd/_wd.sh | 98 + zsh/.oh-my-zsh/plugins/wd/wd.plugin.zsh | 10 + zsh/.oh-my-zsh/plugins/wd/wd.sh | 501 +++ zsh/.oh-my-zsh/plugins/web-search/README.md | 79 + .../plugins/web-search/web-search.plugin.zsh | 82 + zsh/.oh-my-zsh/plugins/wp-cli/README.md | 109 + .../plugins/wp-cli/wp-cli.plugin.zsh | 123 + zsh/.oh-my-zsh/plugins/xcode/README.md | 88 + zsh/.oh-my-zsh/plugins/xcode/_xcselv | 19 + zsh/.oh-my-zsh/plugins/xcode/xcode.plugin.zsh | 211 + zsh/.oh-my-zsh/plugins/yarn/README.md | 47 + zsh/.oh-my-zsh/plugins/yarn/_yarn | 465 +++ zsh/.oh-my-zsh/plugins/yarn/yarn.plugin.zsh | 39 + zsh/.oh-my-zsh/plugins/yii/README.md | 15 + zsh/.oh-my-zsh/plugins/yii/yii.plugin.zsh | 17 + zsh/.oh-my-zsh/plugins/yii2/README.md | 7 + zsh/.oh-my-zsh/plugins/yii2/yii2.plugin.zsh | 29 + zsh/.oh-my-zsh/plugins/yum/README.md | 27 + zsh/.oh-my-zsh/plugins/yum/yum.plugin.zsh | 16 + zsh/.oh-my-zsh/plugins/z/Makefile | 4 + zsh/.oh-my-zsh/plugins/z/README | 148 + zsh/.oh-my-zsh/plugins/z/README.md | 23 + zsh/.oh-my-zsh/plugins/z/z.1 | 173 + zsh/.oh-my-zsh/plugins/z/z.plugin.zsh | 1 + zsh/.oh-my-zsh/plugins/z/z.sh | 267 ++ zsh/.oh-my-zsh/plugins/zbell/README.md | 30 + zsh/.oh-my-zsh/plugins/zbell/zbell.plugin.zsh | 83 + zsh/.oh-my-zsh/plugins/zeus/README.md | 50 + zsh/.oh-my-zsh/plugins/zeus/_zeus | 98 + zsh/.oh-my-zsh/plugins/zeus/zeus.plugin.zsh | 70 + zsh/.oh-my-zsh/plugins/zoxide/README.md | 14 + .../plugins/zoxide/zoxide.plugin.zsh | 5 + .../plugins/zsh-interactive-cd/README.md | 23 + .../zsh-interactive-cd.plugin.zsh | 148 + .../.config/znt/README.txt | 1 + .../.config/znt/n-aliases.conf | 33 + .../.config/znt/n-cd.conf | 68 + .../.config/znt/n-env.conf | 38 + .../.config/znt/n-functions.conf | 41 + .../.config/znt/n-history.conf | 43 + .../.config/znt/n-kill.conf | 46 + .../.config/znt/n-list.conf | 55 + .../.config/znt/n-options.conf | 34 + .../.config/znt/n-panelize.conf | 34 + .../plugins/zsh-navigation-tools/LICENSE | 700 ++++ .../plugins/zsh-navigation-tools/Makefile | 35 + .../plugins/zsh-navigation-tools/NEWS | 17 + .../plugins/zsh-navigation-tools/README.md | 431 ++ .../plugins/zsh-navigation-tools/_n-kill | 41 + .../plugins/zsh-navigation-tools/n-aliases | 47 + .../plugins/zsh-navigation-tools/n-cd | 71 + .../plugins/zsh-navigation-tools/n-env | 47 + .../plugins/zsh-navigation-tools/n-functions | 54 + .../plugins/zsh-navigation-tools/n-help | 135 + .../plugins/zsh-navigation-tools/n-history | 371 ++ .../plugins/zsh-navigation-tools/n-kill | 96 + .../plugins/zsh-navigation-tools/n-list | 517 +++ .../plugins/zsh-navigation-tools/n-list-draw | 133 + .../plugins/zsh-navigation-tools/n-list-input | 377 ++ .../plugins/zsh-navigation-tools/n-options | 84 + .../plugins/zsh-navigation-tools/n-panelize | 68 + .../zsh-navigation-tools/znt-cd-widget | 8 + .../zsh-navigation-tools/znt-history-widget | 22 + .../zsh-navigation-tools/znt-kill-widget | 8 + .../plugins/zsh-navigation-tools/znt-tmux.zsh | 50 + .../zsh-navigation-tools/znt-usetty-wrapper | 40 + .../zsh-navigation-tools.plugin.zsh | 76 + zsh/.oh-my-zsh/plugins/zsh_reload/README.md | 3 + .../plugins/zsh_reload/zsh_reload.plugin.zsh | 7 + zsh/.oh-my-zsh/templates/zshrc.zsh-template | 101 + zsh/.oh-my-zsh/themes/3den.zsh-theme | 7 + zsh/.oh-my-zsh/themes/Soliah.zsh-theme | 87 + zsh/.oh-my-zsh/themes/adben.zsh-theme | 126 + zsh/.oh-my-zsh/themes/af-magic.zsh-theme | 47 + zsh/.oh-my-zsh/themes/afowler.zsh-theme | 10 + zsh/.oh-my-zsh/themes/agnoster.zsh-theme | 259 ++ zsh/.oh-my-zsh/themes/alanpeabody.zsh-theme | 24 + zsh/.oh-my-zsh/themes/amuse.zsh-theme | 18 + zsh/.oh-my-zsh/themes/apple.zsh-theme | 28 + zsh/.oh-my-zsh/themes/arrow.zsh-theme | 14 + zsh/.oh-my-zsh/themes/aussiegeek.zsh-theme | 8 + zsh/.oh-my-zsh/themes/avit.zsh-theme | 85 + zsh/.oh-my-zsh/themes/awesomepanda.zsh-theme | 16 + zsh/.oh-my-zsh/themes/bira.zsh-theme | 32 + zsh/.oh-my-zsh/themes/blinks.zsh-theme | 30 + zsh/.oh-my-zsh/themes/bureau.zsh-theme | 123 + zsh/.oh-my-zsh/themes/candy-kingdom.zsh-theme | 32 + zsh/.oh-my-zsh/themes/candy.zsh-theme | 7 + zsh/.oh-my-zsh/themes/clean.zsh-theme | 14 + zsh/.oh-my-zsh/themes/cloud.zsh-theme | 10 + zsh/.oh-my-zsh/themes/crcandy.zsh-theme | 8 + zsh/.oh-my-zsh/themes/crunch.zsh-theme | 39 + zsh/.oh-my-zsh/themes/cypher.zsh-theme | 4 + zsh/.oh-my-zsh/themes/dallas.zsh-theme | 27 + zsh/.oh-my-zsh/themes/darkblood.zsh-theme | 9 + zsh/.oh-my-zsh/themes/daveverwer.zsh-theme | 7 + zsh/.oh-my-zsh/themes/dieter.zsh-theme | 56 + zsh/.oh-my-zsh/themes/dogenpunk.zsh-theme | 79 + zsh/.oh-my-zsh/themes/dpoggi.zsh-theme | 14 + zsh/.oh-my-zsh/themes/dst.zsh-theme | 16 + zsh/.oh-my-zsh/themes/dstufft.zsh-theme | 19 + zsh/.oh-my-zsh/themes/duellj.zsh-theme | 7 + zsh/.oh-my-zsh/themes/eastwood.zsh-theme | 23 + zsh/.oh-my-zsh/themes/edvardm.zsh-theme | 6 + zsh/.oh-my-zsh/themes/emotty.zsh-theme | 103 + zsh/.oh-my-zsh/themes/essembeh.zsh-theme | 50 + zsh/.oh-my-zsh/themes/evan.zsh-theme | 2 + zsh/.oh-my-zsh/themes/fino-time.zsh-theme | 36 + zsh/.oh-my-zsh/themes/fino.zsh-theme | 45 + zsh/.oh-my-zsh/themes/fishy.zsh-theme | 35 + zsh/.oh-my-zsh/themes/flazz.zsh-theme | 19 + zsh/.oh-my-zsh/themes/fletcherm.zsh-theme | 12 + zsh/.oh-my-zsh/themes/fox.zsh-theme | 9 + zsh/.oh-my-zsh/themes/frisk.zsh-theme | 12 + zsh/.oh-my-zsh/themes/frontcube.zsh-theme | 13 + zsh/.oh-my-zsh/themes/funky.zsh-theme | 14 + zsh/.oh-my-zsh/themes/fwalch.zsh-theme | 6 + zsh/.oh-my-zsh/themes/gallifrey.zsh-theme | 11 + zsh/.oh-my-zsh/themes/gallois.zsh-theme | 24 + .../themes/garyblessington.zsh-theme | 6 + zsh/.oh-my-zsh/themes/gentoo.zsh-theme | 28 + zsh/.oh-my-zsh/themes/geoffgarside.zsh-theme | 5 + zsh/.oh-my-zsh/themes/gianu.zsh-theme | 6 + zsh/.oh-my-zsh/themes/gnzh.zsh-theme | 43 + zsh/.oh-my-zsh/themes/gozilla.zsh-theme | 15 + zsh/.oh-my-zsh/themes/half-life.zsh-theme | 93 + zsh/.oh-my-zsh/themes/humza.zsh-theme | 26 + zsh/.oh-my-zsh/themes/imajes.zsh-theme | 5 + zsh/.oh-my-zsh/themes/intheloop.zsh-theme | 23 + zsh/.oh-my-zsh/themes/itchy.zsh-theme | 18 + zsh/.oh-my-zsh/themes/jaischeema.zsh-theme | 12 + zsh/.oh-my-zsh/themes/jbergantine.zsh-theme | 6 + zsh/.oh-my-zsh/themes/jispwoso.zsh-theme | 10 + zsh/.oh-my-zsh/themes/jnrowe.zsh-theme | 38 + zsh/.oh-my-zsh/themes/jonathan.zsh-theme | 153 + zsh/.oh-my-zsh/themes/josh.zsh-theme | 43 + zsh/.oh-my-zsh/themes/jreese.zsh-theme | 14 + zsh/.oh-my-zsh/themes/jtriley.zsh-theme | 2 + zsh/.oh-my-zsh/themes/juanghurtado.zsh-theme | 41 + zsh/.oh-my-zsh/themes/junkfood.zsh-theme | 30 + zsh/.oh-my-zsh/themes/kafeitu.zsh-theme | 6 + zsh/.oh-my-zsh/themes/kardan.zsh-theme | 12 + zsh/.oh-my-zsh/themes/kennethreitz.zsh-theme | 15 + zsh/.oh-my-zsh/themes/kiwi.zsh-theme | 10 + zsh/.oh-my-zsh/themes/kolo.zsh-theme | 22 + zsh/.oh-my-zsh/themes/kphoen.zsh-theme | 43 + zsh/.oh-my-zsh/themes/lambda.zsh-theme | 4 + zsh/.oh-my-zsh/themes/linuxonly.zsh-theme | 59 + zsh/.oh-my-zsh/themes/lukerandall.zsh-theme | 24 + zsh/.oh-my-zsh/themes/macovsky-ruby.zsh-theme | 1 + zsh/.oh-my-zsh/themes/macovsky.zsh-theme | 12 + zsh/.oh-my-zsh/themes/maran.zsh-theme | 6 + zsh/.oh-my-zsh/themes/mgutz.zsh-theme | 6 + zsh/.oh-my-zsh/themes/mh.zsh-theme | 24 + .../themes/michelebologna.zsh-theme | 75 + zsh/.oh-my-zsh/themes/mikeh.zsh-theme | 21 + zsh/.oh-my-zsh/themes/miloshadzic.zsh-theme | 8 + zsh/.oh-my-zsh/themes/minimal.zsh-theme | 24 + zsh/.oh-my-zsh/themes/mira.zsh-theme | 23 + zsh/.oh-my-zsh/themes/mlh.zsh-theme | 97 + zsh/.oh-my-zsh/themes/mortalscumbag.zsh-theme | 65 + zsh/.oh-my-zsh/themes/mrtazz.zsh-theme | 7 + zsh/.oh-my-zsh/themes/murilasso.zsh-theme | 14 + zsh/.oh-my-zsh/themes/muse.zsh-theme | 16 + zsh/.oh-my-zsh/themes/nanotech.zsh-theme | 7 + zsh/.oh-my-zsh/themes/nebirhos.zsh-theme | 21 + zsh/.oh-my-zsh/themes/nicoulaj.zsh-theme | 43 + zsh/.oh-my-zsh/themes/norm.zsh-theme | 7 + zsh/.oh-my-zsh/themes/obraun.zsh-theme | 10 + zsh/.oh-my-zsh/themes/peepcode.zsh-theme | 47 + zsh/.oh-my-zsh/themes/philips.zsh-theme | 14 + zsh/.oh-my-zsh/themes/pmcgee.zsh-theme | 16 + .../themes/pygmalion-virtualenv.zsh-theme | 53 + zsh/.oh-my-zsh/themes/pygmalion.zsh-theme | 32 + zsh/.oh-my-zsh/themes/random.zsh-theme | 47 + zsh/.oh-my-zsh/themes/re5et.zsh-theme | 15 + zsh/.oh-my-zsh/themes/refined.zsh-theme | 107 + zsh/.oh-my-zsh/themes/rgm.zsh-theme | 8 + zsh/.oh-my-zsh/themes/risto.zsh-theme | 6 + zsh/.oh-my-zsh/themes/rixius.zsh-theme | 21 + zsh/.oh-my-zsh/themes/rkj-repos.zsh-theme | 35 + zsh/.oh-my-zsh/themes/rkj.zsh-theme | 9 + zsh/.oh-my-zsh/themes/robbyrussell.zsh-theme | 7 + zsh/.oh-my-zsh/themes/sammy.zsh-theme | 6 + zsh/.oh-my-zsh/themes/simonoff.zsh-theme | 138 + zsh/.oh-my-zsh/themes/simple.zsh-theme | 6 + zsh/.oh-my-zsh/themes/skaro.zsh-theme | 7 + zsh/.oh-my-zsh/themes/smt.zsh-theme | 83 + zsh/.oh-my-zsh/themes/sonicradish.zsh-theme | 37 + zsh/.oh-my-zsh/themes/sorin.zsh-theme | 42 + zsh/.oh-my-zsh/themes/sporty_256.zsh-theme | 13 + zsh/.oh-my-zsh/themes/steeef.zsh-theme | 103 + zsh/.oh-my-zsh/themes/strug.zsh-theme | 25 + zsh/.oh-my-zsh/themes/sunaku.zsh-theme | 25 + zsh/.oh-my-zsh/themes/sunrise.zsh-theme | 93 + zsh/.oh-my-zsh/themes/superjarin.zsh-theme | 18 + zsh/.oh-my-zsh/themes/suvash.zsh-theme | 21 + .../themes/takashiyoshida.zsh-theme | 27 + zsh/.oh-my-zsh/themes/terminalparty.zsh-theme | 8 + zsh/.oh-my-zsh/themes/theunraveler.zsh-theme | 16 + zsh/.oh-my-zsh/themes/tjkirch.zsh-theme | 15 + zsh/.oh-my-zsh/themes/tjkirch_mod.zsh-theme | 13 + zsh/.oh-my-zsh/themes/tonotdo.zsh-theme | 12 + zsh/.oh-my-zsh/themes/trapd00r.zsh-theme | 131 + zsh/.oh-my-zsh/themes/wedisagree.zsh-theme | 111 + zsh/.oh-my-zsh/themes/wezm+.zsh-theme | 7 + zsh/.oh-my-zsh/themes/wezm.zsh-theme | 7 + zsh/.oh-my-zsh/themes/wuffers.zsh-theme | 5 + .../themes/xiong-chiamiov-plus.zsh-theme | 6 + .../themes/xiong-chiamiov.zsh-theme | 6 + zsh/.oh-my-zsh/themes/ys.zsh-theme | 72 + zsh/.oh-my-zsh/themes/zhann.zsh-theme | 23 + zsh/.oh-my-zsh/tools/changelog.sh | 441 ++ zsh/.oh-my-zsh/tools/check_for_upgrade.sh | 153 + zsh/.oh-my-zsh/tools/install.sh | 420 ++ zsh/.oh-my-zsh/tools/require_tool.sh | 161 + zsh/.oh-my-zsh/tools/theme_chooser.sh | 98 + zsh/.oh-my-zsh/tools/uninstall.sh | 40 + zsh/.oh-my-zsh/tools/upgrade.sh | 211 + zsh/.zshrc | 115 + zsh/fig_prompt | 225 + zsh/g3path.zsh | 22 + zsh/goog_prompt.zsh | 422 ++ 1200 files changed, 108582 insertions(+) create mode 100644 config/.config/nvim/coc-settings.json create mode 100644 config/.config/nvim/init.vim create mode 100644 config/.config/nvim/lua/diagnostics.lua create mode 100644 config/.config/nvim/lua/lsp.lua create mode 100644 fzf/fzf-at-google.zsh create mode 100755 fzf/fzf-relevant-files.zsh create mode 100644 fzf/fzf/.github/FUNDING.yml create mode 100644 fzf/fzf/.github/ISSUE_TEMPLATE.md create mode 100644 fzf/fzf/.github/dependabot.yml create mode 100644 fzf/fzf/.github/workflows/codeql-analysis.yml create mode 100644 fzf/fzf/.github/workflows/linux.yml create mode 100644 fzf/fzf/.github/workflows/macos.yml create mode 100644 fzf/fzf/.gitignore create mode 100644 fzf/fzf/.goreleaser.yml create mode 100644 fzf/fzf/.rubocop.yml create mode 100644 fzf/fzf/ADVANCED.md create mode 100644 fzf/fzf/BUILD.md create mode 100644 fzf/fzf/CHANGELOG.md create mode 100644 fzf/fzf/Dockerfile create mode 100644 fzf/fzf/LICENSE create mode 100644 fzf/fzf/Makefile create mode 100644 fzf/fzf/README-VIM.md create mode 100644 fzf/fzf/README.md create mode 100755 fzf/fzf/bin/fzf-tmux create mode 100644 fzf/fzf/doc/fzf.txt create mode 100644 fzf/fzf/go.mod create mode 100644 fzf/fzf/go.sum create mode 100755 fzf/fzf/install create mode 100644 fzf/fzf/install.ps1 create mode 100644 fzf/fzf/main.go create mode 100644 fzf/fzf/man/man1/fzf-tmux.1 create mode 100644 fzf/fzf/man/man1/fzf.1 create mode 100644 fzf/fzf/plugin/fzf.vim create mode 100644 fzf/fzf/shell/completion.bash create mode 100644 fzf/fzf/shell/completion.zsh create mode 100644 fzf/fzf/shell/key-bindings.bash create mode 100644 fzf/fzf/shell/key-bindings.fish create mode 100644 fzf/fzf/shell/key-bindings.zsh create mode 100644 fzf/fzf/src/LICENSE create mode 100644 fzf/fzf/src/algo/algo.go create mode 100644 fzf/fzf/src/algo/algo_test.go create mode 100644 fzf/fzf/src/algo/normalize.go create mode 100644 fzf/fzf/src/ansi.go create mode 100644 fzf/fzf/src/ansi_test.go create mode 100644 fzf/fzf/src/cache.go create mode 100644 fzf/fzf/src/cache_test.go create mode 100644 fzf/fzf/src/chunklist.go create mode 100644 fzf/fzf/src/chunklist_test.go create mode 100644 fzf/fzf/src/constants.go create mode 100644 fzf/fzf/src/core.go create mode 100644 fzf/fzf/src/history.go create mode 100644 fzf/fzf/src/history_test.go create mode 100644 fzf/fzf/src/item.go create mode 100644 fzf/fzf/src/item_test.go create mode 100644 fzf/fzf/src/matcher.go create mode 100644 fzf/fzf/src/merger.go create mode 100644 fzf/fzf/src/merger_test.go create mode 100644 fzf/fzf/src/options.go create mode 100644 fzf/fzf/src/options_test.go create mode 100644 fzf/fzf/src/pattern.go create mode 100644 fzf/fzf/src/pattern_test.go create mode 100644 fzf/fzf/src/protector/protector.go create mode 100644 fzf/fzf/src/protector/protector_openbsd.go create mode 100644 fzf/fzf/src/reader.go create mode 100644 fzf/fzf/src/reader_test.go create mode 100644 fzf/fzf/src/result.go create mode 100644 fzf/fzf/src/result_others.go create mode 100644 fzf/fzf/src/result_test.go create mode 100644 fzf/fzf/src/result_x86.go create mode 100644 fzf/fzf/src/terminal.go create mode 100644 fzf/fzf/src/terminal_test.go create mode 100644 fzf/fzf/src/terminal_unix.go create mode 100644 fzf/fzf/src/terminal_windows.go create mode 100644 fzf/fzf/src/tokenizer.go create mode 100644 fzf/fzf/src/tokenizer_test.go create mode 100644 fzf/fzf/src/tui/dummy.go create mode 100644 fzf/fzf/src/tui/light.go create mode 100644 fzf/fzf/src/tui/light_unix.go create mode 100644 fzf/fzf/src/tui/light_windows.go create mode 100644 fzf/fzf/src/tui/tcell.go create mode 100644 fzf/fzf/src/tui/tcell_test.go create mode 100644 fzf/fzf/src/tui/ttyname_unix.go create mode 100644 fzf/fzf/src/tui/ttyname_windows.go create mode 100644 fzf/fzf/src/tui/tui.go create mode 100644 fzf/fzf/src/tui/tui_test.go create mode 100644 fzf/fzf/src/util/atomicbool.go create mode 100644 fzf/fzf/src/util/atomicbool_test.go create mode 100644 fzf/fzf/src/util/chars.go create mode 100644 fzf/fzf/src/util/chars_test.go create mode 100644 fzf/fzf/src/util/eventbox.go create mode 100644 fzf/fzf/src/util/eventbox_test.go create mode 100644 fzf/fzf/src/util/slab.go create mode 100644 fzf/fzf/src/util/util.go create mode 100644 fzf/fzf/src/util/util_test.go create mode 100644 fzf/fzf/src/util/util_unix.go create mode 100644 fzf/fzf/src/util/util_windows.go create mode 100644 fzf/fzf/test/fzf.vader create mode 100755 fzf/fzf/test/test_go.rb create mode 100755 fzf/fzf/uninstall create mode 100644 tmux/.tmux.conf create mode 100644 tmux/.tmux/osiris-theme.conf create mode 100644 tmux/.tmux/plugins/tmux-battery/.gitattributes create mode 100644 tmux/.tmux/plugins/tmux-battery/CHANGELOG.md create mode 100644 tmux/.tmux/plugins/tmux-battery/LICENSE.md create mode 100644 tmux/.tmux/plugins/tmux-battery/README.md create mode 100755 tmux/.tmux/plugins/tmux-battery/battery.tmux create mode 100644 tmux/.tmux/plugins/tmux-battery/screenshots/battery_charging_tier1.png create mode 100644 tmux/.tmux/plugins/tmux-battery/screenshots/battery_charging_tier2.png create mode 100644 tmux/.tmux/plugins/tmux-battery/screenshots/battery_charging_tier3.png create mode 100644 tmux/.tmux/plugins/tmux-battery/screenshots/battery_charging_tier4.png create mode 100644 tmux/.tmux/plugins/tmux-battery/screenshots/battery_charging_tier5.png create mode 100644 tmux/.tmux/plugins/tmux-battery/screenshots/battery_charging_tier6.png create mode 100644 tmux/.tmux/plugins/tmux-battery/screenshots/battery_charging_tier7.png create mode 100644 tmux/.tmux/plugins/tmux-battery/screenshots/battery_charging_tier8.png create mode 100644 tmux/.tmux/plugins/tmux-battery/screenshots/battery_discharging_tier1.png create mode 100644 tmux/.tmux/plugins/tmux-battery/screenshots/battery_discharging_tier2.png create mode 100644 tmux/.tmux/plugins/tmux-battery/screenshots/battery_discharging_tier3.png create mode 100644 tmux/.tmux/plugins/tmux-battery/screenshots/battery_discharging_tier4.png create mode 100644 tmux/.tmux/plugins/tmux-battery/screenshots/battery_discharging_tier5.png create mode 100644 tmux/.tmux/plugins/tmux-battery/screenshots/battery_discharging_tier6.png create mode 100644 tmux/.tmux/plugins/tmux-battery/screenshots/battery_discharging_tier7.png create mode 100644 tmux/.tmux/plugins/tmux-battery/screenshots/battery_discharging_tier8.png create mode 100644 tmux/.tmux/plugins/tmux-battery/screenshots/battery_status_attached.png create mode 100644 tmux/.tmux/plugins/tmux-battery/screenshots/battery_status_unknown.png create mode 100755 tmux/.tmux/plugins/tmux-battery/scripts/battery_color.sh create mode 100755 tmux/.tmux/plugins/tmux-battery/scripts/battery_color_charge.sh create mode 100755 tmux/.tmux/plugins/tmux-battery/scripts/battery_color_status.sh create mode 100755 tmux/.tmux/plugins/tmux-battery/scripts/battery_graph.sh create mode 100755 tmux/.tmux/plugins/tmux-battery/scripts/battery_icon.sh create mode 100755 tmux/.tmux/plugins/tmux-battery/scripts/battery_icon_charge.sh create mode 100755 tmux/.tmux/plugins/tmux-battery/scripts/battery_icon_status.sh create mode 100755 tmux/.tmux/plugins/tmux-battery/scripts/battery_percentage.sh create mode 100755 tmux/.tmux/plugins/tmux-battery/scripts/battery_remain.sh create mode 100755 tmux/.tmux/plugins/tmux-battery/scripts/battery_status_bg.sh create mode 100755 tmux/.tmux/plugins/tmux-battery/scripts/battery_status_fg.sh create mode 100644 tmux/.tmux/plugins/tmux-battery/scripts/helpers.sh create mode 100644 tmux/.tmux/plugins/tmux-cowboy/LICENSE.md create mode 100644 tmux/.tmux/plugins/tmux-cowboy/README.md create mode 100755 tmux/.tmux/plugins/tmux-cowboy/cowboy.tmux create mode 100755 tmux/.tmux/plugins/tmux-cowboy/scripts/kill.sh create mode 100644 tmux/.tmux/plugins/tmux-cpu/.editorconfig create mode 100644 tmux/.tmux/plugins/tmux-cpu/.mailmap create mode 100644 tmux/.tmux/plugins/tmux-cpu/LICENSE create mode 100644 tmux/.tmux/plugins/tmux-cpu/README.md create mode 100755 tmux/.tmux/plugins/tmux-cpu/cpu.tmux create mode 100644 tmux/.tmux/plugins/tmux-cpu/screenshots/high_bg.png create mode 100644 tmux/.tmux/plugins/tmux-cpu/screenshots/high_fg.png create mode 100644 tmux/.tmux/plugins/tmux-cpu/screenshots/high_icon.png create mode 100644 tmux/.tmux/plugins/tmux-cpu/screenshots/low_bg.png create mode 100644 tmux/.tmux/plugins/tmux-cpu/screenshots/low_fg.png create mode 100644 tmux/.tmux/plugins/tmux-cpu/screenshots/low_icon.png create mode 100644 tmux/.tmux/plugins/tmux-cpu/screenshots/medium_bg.png create mode 100644 tmux/.tmux/plugins/tmux-cpu/screenshots/medium_fg.png create mode 100644 tmux/.tmux/plugins/tmux-cpu/screenshots/medium_icon.png create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/cpu_bg_color.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/cpu_fg_color.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/cpu_icon.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/cpu_percentage.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/cpu_temp.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/cpu_temp_bg_color.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/cpu_temp_fg_color.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/cpu_temp_icon.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/gpu_bg_color.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/gpu_fg_color.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/gpu_icon.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/gpu_percentage.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/gpu_temp.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/gpu_temp_bg_color.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/gpu_temp_fg_color.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/gpu_temp_icon.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/gram_bg_color.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/gram_fg_color.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/gram_icon.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/gram_percentage.sh create mode 100644 tmux/.tmux/plugins/tmux-cpu/scripts/helpers.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/ram_bg_color.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/ram_fg_color.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/ram_icon.sh create mode 100755 tmux/.tmux/plugins/tmux-cpu/scripts/ram_percentage.sh create mode 100644 tmux/.tmux/plugins/tmux-sensible/.gitattributes create mode 100644 tmux/.tmux/plugins/tmux-sensible/CHANGELOG.md create mode 100644 tmux/.tmux/plugins/tmux-sensible/LICENSE.md create mode 100644 tmux/.tmux/plugins/tmux-sensible/README.md create mode 100644 tmux/.tmux/plugins/tmux-sensible/sensible.tmux create mode 100644 tmux/.tmux/plugins/tmux-yank/.editorconfig create mode 100644 tmux/.tmux/plugins/tmux-yank/.gitattributes create mode 100644 tmux/.tmux/plugins/tmux-yank/.gitignore create mode 100644 tmux/.tmux/plugins/tmux-yank/.travis.yml create mode 100644 tmux/.tmux/plugins/tmux-yank/CHANGELOG.md create mode 100644 tmux/.tmux/plugins/tmux-yank/LICENSE.md create mode 100644 tmux/.tmux/plugins/tmux-yank/README.md create mode 100644 tmux/.tmux/plugins/tmux-yank/Vagrantfile create mode 100644 tmux/.tmux/plugins/tmux-yank/_config.yml create mode 100644 tmux/.tmux/plugins/tmux-yank/citest create mode 100644 tmux/.tmux/plugins/tmux-yank/scripts/copy_line.sh create mode 100644 tmux/.tmux/plugins/tmux-yank/scripts/copy_pane_pwd.sh create mode 100644 tmux/.tmux/plugins/tmux-yank/scripts/helpers.sh create mode 100644 tmux/.tmux/plugins/tmux-yank/vagrant_provisioning.sh create mode 100644 tmux/.tmux/plugins/tmux-yank/video/README.md create mode 100644 tmux/.tmux/plugins/tmux-yank/video/screencast_img.png create mode 100644 tmux/.tmux/plugins/tmux-yank/video/script.md create mode 100644 tmux/.tmux/plugins/tmux-yank/yank.tmux create mode 100644 tmux/.tmux/plugins/tpm/.gitattributes create mode 100644 tmux/.tmux/plugins/tpm/.gitignore create mode 100644 tmux/.tmux/plugins/tpm/.gitmodules create mode 100644 tmux/.tmux/plugins/tpm/.travis.yml create mode 100644 tmux/.tmux/plugins/tpm/CHANGELOG.md create mode 100644 tmux/.tmux/plugins/tpm/HOW_TO_PLUGIN.md create mode 100644 tmux/.tmux/plugins/tpm/LICENSE.md create mode 100644 tmux/.tmux/plugins/tpm/README.md create mode 100755 tmux/.tmux/plugins/tpm/bin/clean_plugins create mode 100755 tmux/.tmux/plugins/tpm/bin/install_plugins create mode 100755 tmux/.tmux/plugins/tpm/bin/update_plugins create mode 100755 tmux/.tmux/plugins/tpm/bindings/clean_plugins create mode 100755 tmux/.tmux/plugins/tpm/bindings/install_plugins create mode 100755 tmux/.tmux/plugins/tpm/bindings/update_plugins create mode 100644 tmux/.tmux/plugins/tpm/docs/automatic_tpm_installation.md create mode 100644 tmux/.tmux/plugins/tpm/docs/changing_plugins_install_dir.md create mode 100644 tmux/.tmux/plugins/tpm/docs/how_to_create_plugin.md create mode 100644 tmux/.tmux/plugins/tpm/docs/managing_plugins_via_cmd_line.md create mode 100644 tmux/.tmux/plugins/tpm/docs/tpm_not_working.md create mode 100755 tmux/.tmux/plugins/tpm/scripts/check_tmux_version.sh create mode 100755 tmux/.tmux/plugins/tpm/scripts/clean_plugins.sh create mode 100644 tmux/.tmux/plugins/tpm/scripts/helpers/plugin_functions.sh create mode 100644 tmux/.tmux/plugins/tpm/scripts/helpers/shell_echo_functions.sh create mode 100644 tmux/.tmux/plugins/tpm/scripts/helpers/tmux_echo_functions.sh create mode 100644 tmux/.tmux/plugins/tpm/scripts/helpers/tmux_utils.sh create mode 100644 tmux/.tmux/plugins/tpm/scripts/helpers/utility.sh create mode 100755 tmux/.tmux/plugins/tpm/scripts/install_plugins.sh create mode 100755 tmux/.tmux/plugins/tpm/scripts/source_plugins.sh create mode 100755 tmux/.tmux/plugins/tpm/scripts/update_plugin.sh create mode 100755 tmux/.tmux/plugins/tpm/scripts/update_plugin_prompt_handler.sh create mode 100644 tmux/.tmux/plugins/tpm/scripts/variables.sh create mode 100755 tmux/.tmux/plugins/tpm/tests/expect_failed_plugin_download create mode 100755 tmux/.tmux/plugins/tpm/tests/expect_successful_clean_plugins create mode 100755 tmux/.tmux/plugins/tpm/tests/expect_successful_multiple_plugins_download create mode 100755 tmux/.tmux/plugins/tpm/tests/expect_successful_plugin_download create mode 100755 tmux/.tmux/plugins/tpm/tests/expect_successful_update_of_a_single_plugin create mode 100755 tmux/.tmux/plugins/tpm/tests/expect_successful_update_of_all_plugins create mode 100644 tmux/.tmux/plugins/tpm/tests/helpers/tpm.sh create mode 100755 tmux/.tmux/plugins/tpm/tests/test_plugin_clean.sh create mode 100755 tmux/.tmux/plugins/tpm/tests/test_plugin_installation.sh create mode 100755 tmux/.tmux/plugins/tpm/tests/test_plugin_installation_legacy.sh create mode 100755 tmux/.tmux/plugins/tpm/tests/test_plugin_sourcing.sh create mode 100755 tmux/.tmux/plugins/tpm/tests/test_plugin_update.sh create mode 100755 tmux/.tmux/plugins/tpm/tpm create mode 100644 tmux/.tmux/plugins/vim-tmux-navigator/.gitignore create mode 100644 tmux/.tmux/plugins/vim-tmux-navigator/License.md create mode 100644 tmux/.tmux/plugins/vim-tmux-navigator/README.md create mode 100644 tmux/.tmux/plugins/vim-tmux-navigator/doc/tmux-navigator.txt create mode 100644 tmux/.tmux/plugins/vim-tmux-navigator/pattern-check create mode 100644 tmux/.tmux/plugins/vim-tmux-navigator/plugin/tmux_navigator.vim create mode 100755 tmux/.tmux/plugins/vim-tmux-navigator/vim-tmux-navigator.tmux create mode 100644 tmux/.tmux/tmux-migrate-options.py create mode 100644 tmux/.tmuxinator/dev.yml create mode 100644 tmux/.tmuxinator/second.yaml create mode 100644 vim/.vim/after/syntax/java.vim create mode 100644 vim/.vim/autoload/plug.vim create mode 100644 vim/.vim/prefs/ale.vim create mode 100644 vim/.vim/prefs/asynclsp.vim create mode 100644 vim/.vim/prefs/cmp.vim create mode 100644 vim/.vim/prefs/coc.vim create mode 100644 vim/.vim/prefs/golang.vim create mode 100644 vim/.vim/prefs/google.vim create mode 100644 vim/.vim/prefs/init.vim create mode 100644 vim/.vim/prefs/leader.vim create mode 100644 vim/.vim/prefs/mappings.vim create mode 100644 vim/.vim/prefs/plug_prefs.vim create mode 100644 vim/.vim/prefs/plugins.vim create mode 100644 vim/.vim/prefs/ui.vim create mode 100644 vim/.vim/prefs/ultisnips.vim create mode 100644 vim/.vim/prefs/ycm.vim create mode 100644 vim/.vimrc create mode 100644 vim/.vimrc.local create mode 100644 zsh/.aliases.sh create mode 100644 zsh/.bash-powerline.sh create mode 100644 zsh/.bash_profile create mode 100644 zsh/.bash_profile.local create mode 100644 zsh/.oh-my-zsh/.editorconfig create mode 100644 zsh/.oh-my-zsh/.github/CODEOWNERS create mode 100644 zsh/.oh-my-zsh/.github/FUNDING.yml create mode 100644 zsh/.oh-my-zsh/.github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 zsh/.oh-my-zsh/.github/ISSUE_TEMPLATE/config.yml create mode 100644 zsh/.oh-my-zsh/.github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 zsh/.oh-my-zsh/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 zsh/.oh-my-zsh/.github/workflows/main.yml create mode 100644 zsh/.oh-my-zsh/.gitignore create mode 100644 zsh/.oh-my-zsh/.gitpod.Dockerfile create mode 100644 zsh/.oh-my-zsh/.gitpod.yml create mode 100644 zsh/.oh-my-zsh/CODE_OF_CONDUCT.md create mode 100644 zsh/.oh-my-zsh/CONTRIBUTING.md create mode 100644 zsh/.oh-my-zsh/LICENSE.txt create mode 100644 zsh/.oh-my-zsh/README.md create mode 100644 zsh/.oh-my-zsh/lib/bzr.zsh create mode 100644 zsh/.oh-my-zsh/lib/cli.zsh create mode 100644 zsh/.oh-my-zsh/lib/clipboard.zsh create mode 100644 zsh/.oh-my-zsh/lib/compfix.zsh create mode 100644 zsh/.oh-my-zsh/lib/completion.zsh create mode 100644 zsh/.oh-my-zsh/lib/correction.zsh create mode 100644 zsh/.oh-my-zsh/lib/diagnostics.zsh create mode 100644 zsh/.oh-my-zsh/lib/directories.zsh create mode 100644 zsh/.oh-my-zsh/lib/functions.zsh create mode 100644 zsh/.oh-my-zsh/lib/git.zsh create mode 100644 zsh/.oh-my-zsh/lib/grep.zsh create mode 100644 zsh/.oh-my-zsh/lib/history.zsh create mode 100644 zsh/.oh-my-zsh/lib/key-bindings.zsh create mode 100644 zsh/.oh-my-zsh/lib/misc.zsh create mode 100644 zsh/.oh-my-zsh/lib/nvm.zsh create mode 100644 zsh/.oh-my-zsh/lib/prompt_info_functions.zsh create mode 100644 zsh/.oh-my-zsh/lib/spectrum.zsh create mode 100644 zsh/.oh-my-zsh/lib/termsupport.zsh create mode 100644 zsh/.oh-my-zsh/lib/theme-and-appearance.zsh create mode 100644 zsh/.oh-my-zsh/oh-my-zsh.sh create mode 100644 zsh/.oh-my-zsh/plugins/adb/README.md create mode 100644 zsh/.oh-my-zsh/plugins/adb/_adb create mode 100644 zsh/.oh-my-zsh/plugins/ag/README.md create mode 100644 zsh/.oh-my-zsh/plugins/ag/_ag create mode 100644 zsh/.oh-my-zsh/plugins/alias-finder/README.md create mode 100644 zsh/.oh-my-zsh/plugins/alias-finder/alias-finder.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/aliases/README.md create mode 100644 zsh/.oh-my-zsh/plugins/aliases/aliases.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/aliases/cheatsheet.py create mode 100644 zsh/.oh-my-zsh/plugins/aliases/termcolor.py create mode 100644 zsh/.oh-my-zsh/plugins/ansible/README.md create mode 100644 zsh/.oh-my-zsh/plugins/ansible/ansible.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/ant/README.md create mode 100644 zsh/.oh-my-zsh/plugins/ant/ant.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/apache2-macports/README.md create mode 100644 zsh/.oh-my-zsh/plugins/apache2-macports/apache2-macports.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/arcanist/README.md create mode 100644 zsh/.oh-my-zsh/plugins/arcanist/arcanist.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/archlinux/README.md create mode 100644 zsh/.oh-my-zsh/plugins/archlinux/archlinux.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/asdf/README.md create mode 100644 zsh/.oh-my-zsh/plugins/asdf/asdf.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/autoenv/README.md create mode 100644 zsh/.oh-my-zsh/plugins/autoenv/autoenv.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/autojump/README.md create mode 100644 zsh/.oh-my-zsh/plugins/autojump/autojump.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/autopep8/README.md create mode 100644 zsh/.oh-my-zsh/plugins/autopep8/_autopep8 create mode 100644 zsh/.oh-my-zsh/plugins/aws/README.md create mode 100644 zsh/.oh-my-zsh/plugins/aws/aws.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/battery/README.md create mode 100644 zsh/.oh-my-zsh/plugins/battery/battery.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/bazel/README.md create mode 100644 zsh/.oh-my-zsh/plugins/bazel/_bazel create mode 100644 zsh/.oh-my-zsh/plugins/bbedit/README.md create mode 100644 zsh/.oh-my-zsh/plugins/bbedit/bbedit.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/bedtools/README.md create mode 100644 zsh/.oh-my-zsh/plugins/bedtools/_bedtools create mode 100644 zsh/.oh-my-zsh/plugins/bgnotify/README.md create mode 100644 zsh/.oh-my-zsh/plugins/bgnotify/bgnotify.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/boot2docker/README.md create mode 100644 zsh/.oh-my-zsh/plugins/boot2docker/_boot2docker create mode 100644 zsh/.oh-my-zsh/plugins/bower/README.md create mode 100644 zsh/.oh-my-zsh/plugins/bower/_bower create mode 100644 zsh/.oh-my-zsh/plugins/bower/bower.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/branch/README.md create mode 100644 zsh/.oh-my-zsh/plugins/branch/branch.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/brew/README.md create mode 100644 zsh/.oh-my-zsh/plugins/brew/brew.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/bundler/README.md create mode 100644 zsh/.oh-my-zsh/plugins/bundler/_bundler create mode 100644 zsh/.oh-my-zsh/plugins/bundler/bundler.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/cabal/README.md create mode 100644 zsh/.oh-my-zsh/plugins/cabal/cabal.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/cake/README.md create mode 100644 zsh/.oh-my-zsh/plugins/cake/cake.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/cakephp3/README.md create mode 100644 zsh/.oh-my-zsh/plugins/cakephp3/cakephp3.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/capistrano/README.md create mode 100644 zsh/.oh-my-zsh/plugins/capistrano/_capistrano create mode 100644 zsh/.oh-my-zsh/plugins/capistrano/capistrano.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/cargo/README.md create mode 100644 zsh/.oh-my-zsh/plugins/cargo/cargo.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/cask/README.md create mode 100644 zsh/.oh-my-zsh/plugins/cask/cask.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/catimg/README.md create mode 100644 zsh/.oh-my-zsh/plugins/catimg/catimg.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/catimg/catimg.sh create mode 100644 zsh/.oh-my-zsh/plugins/catimg/colors.png create mode 100644 zsh/.oh-my-zsh/plugins/celery/README.md create mode 100644 zsh/.oh-my-zsh/plugins/celery/_celery create mode 100644 zsh/.oh-my-zsh/plugins/chruby/README.md create mode 100644 zsh/.oh-my-zsh/plugins/chruby/chruby.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/chucknorris/.gitignore create mode 100644 zsh/.oh-my-zsh/plugins/chucknorris/README.md create mode 100644 zsh/.oh-my-zsh/plugins/chucknorris/chucknorris.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/chucknorris/fortunes/chucknorris create mode 100644 zsh/.oh-my-zsh/plugins/cloudfoundry/README.md create mode 100644 zsh/.oh-my-zsh/plugins/cloudfoundry/cloudfoundry.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/codeclimate/README.md create mode 100644 zsh/.oh-my-zsh/plugins/codeclimate/_codeclimate create mode 100644 zsh/.oh-my-zsh/plugins/coffee/README.md create mode 100644 zsh/.oh-my-zsh/plugins/coffee/_coffee create mode 100644 zsh/.oh-my-zsh/plugins/coffee/coffee.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/colemak/.gitignore create mode 100644 zsh/.oh-my-zsh/plugins/colemak/README.md create mode 100644 zsh/.oh-my-zsh/plugins/colemak/colemak-less create mode 100644 zsh/.oh-my-zsh/plugins/colemak/colemak.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/colored-man-pages/README.md create mode 100644 zsh/.oh-my-zsh/plugins/colored-man-pages/colored-man-pages.plugin.zsh create mode 100755 zsh/.oh-my-zsh/plugins/colored-man-pages/nroff create mode 100644 zsh/.oh-my-zsh/plugins/colorize/README.md create mode 100644 zsh/.oh-my-zsh/plugins/colorize/colorize.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/command-not-found/README.md create mode 100644 zsh/.oh-my-zsh/plugins/command-not-found/command-not-found.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/common-aliases/README.md create mode 100644 zsh/.oh-my-zsh/plugins/common-aliases/common-aliases.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/compleat/README.md create mode 100644 zsh/.oh-my-zsh/plugins/compleat/compleat.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/composer/README.md create mode 100644 zsh/.oh-my-zsh/plugins/composer/composer.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/copybuffer/README.md create mode 100644 zsh/.oh-my-zsh/plugins/copybuffer/copybuffer.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/copydir/README.md create mode 100644 zsh/.oh-my-zsh/plugins/copydir/copydir.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/copyfile/README.md create mode 100644 zsh/.oh-my-zsh/plugins/copyfile/copyfile.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/cp/README.md create mode 100644 zsh/.oh-my-zsh/plugins/cp/cp.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/cpanm/README.md create mode 100644 zsh/.oh-my-zsh/plugins/cpanm/_cpanm create mode 100644 zsh/.oh-my-zsh/plugins/dash/README.md create mode 100644 zsh/.oh-my-zsh/plugins/dash/dash.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/debian/README.md create mode 100644 zsh/.oh-my-zsh/plugins/debian/debian.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/deno/README.md create mode 100644 zsh/.oh-my-zsh/plugins/deno/deno.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/dircycle/README.md create mode 100644 zsh/.oh-my-zsh/plugins/dircycle/dircycle.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/direnv/README.md create mode 100644 zsh/.oh-my-zsh/plugins/direnv/direnv.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/dirhistory/README.md create mode 100644 zsh/.oh-my-zsh/plugins/dirhistory/dirhistory.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/dirpersist/README.md create mode 100644 zsh/.oh-my-zsh/plugins/dirpersist/dirpersist.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/django/README.md create mode 100644 zsh/.oh-my-zsh/plugins/django/django.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/dnf/README.md create mode 100644 zsh/.oh-my-zsh/plugins/dnf/dnf.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/dnote/README.md create mode 100644 zsh/.oh-my-zsh/plugins/dnote/_dnote create mode 100644 zsh/.oh-my-zsh/plugins/docker-compose/README.md create mode 100644 zsh/.oh-my-zsh/plugins/docker-compose/_docker-compose create mode 100644 zsh/.oh-my-zsh/plugins/docker-compose/docker-compose.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/docker-machine/README.md create mode 100644 zsh/.oh-my-zsh/plugins/docker-machine/_docker-machine create mode 100644 zsh/.oh-my-zsh/plugins/docker-machine/docker-machine.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/docker/README.md create mode 100644 zsh/.oh-my-zsh/plugins/docker/_docker create mode 100644 zsh/.oh-my-zsh/plugins/doctl/README.md create mode 100644 zsh/.oh-my-zsh/plugins/doctl/doctl.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/dotenv/README.md create mode 100644 zsh/.oh-my-zsh/plugins/dotenv/dotenv.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/dotnet/README.md create mode 100644 zsh/.oh-my-zsh/plugins/dotnet/dotnet.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/droplr/README.md create mode 100644 zsh/.oh-my-zsh/plugins/droplr/droplr.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/drush/README.md create mode 100644 zsh/.oh-my-zsh/plugins/drush/drush.complete.sh create mode 100644 zsh/.oh-my-zsh/plugins/drush/drush.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/eecms/README.md create mode 100644 zsh/.oh-my-zsh/plugins/eecms/eecms.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/emacs/README.md create mode 100644 zsh/.oh-my-zsh/plugins/emacs/emacs.plugin.zsh create mode 100755 zsh/.oh-my-zsh/plugins/emacs/emacsclient.sh create mode 100644 zsh/.oh-my-zsh/plugins/ember-cli/README.md create mode 100644 zsh/.oh-my-zsh/plugins/ember-cli/ember-cli.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/emoji-clock/README.md create mode 100644 zsh/.oh-my-zsh/plugins/emoji-clock/emoji-clock.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/emoji/README.md create mode 100644 zsh/.oh-my-zsh/plugins/emoji/emoji-char-definitions.zsh create mode 100644 zsh/.oh-my-zsh/plugins/emoji/emoji-data.txt create mode 100644 zsh/.oh-my-zsh/plugins/emoji/emoji.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/emoji/update_emoji.pl create mode 100644 zsh/.oh-my-zsh/plugins/emotty/README.md create mode 100644 zsh/.oh-my-zsh/plugins/emotty/emotty.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/emotty/emotty_emoji_set.zsh create mode 100644 zsh/.oh-my-zsh/plugins/emotty/emotty_floral_set.zsh create mode 100644 zsh/.oh-my-zsh/plugins/emotty/emotty_love_set.zsh create mode 100644 zsh/.oh-my-zsh/plugins/emotty/emotty_nature_set.zsh create mode 100644 zsh/.oh-my-zsh/plugins/emotty/emotty_stellar_set.zsh create mode 100644 zsh/.oh-my-zsh/plugins/emotty/emotty_zodiac_set.zsh create mode 100644 zsh/.oh-my-zsh/plugins/encode64/README.md create mode 100644 zsh/.oh-my-zsh/plugins/encode64/encode64.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/extract/README.md create mode 100644 zsh/.oh-my-zsh/plugins/extract/_extract create mode 100644 zsh/.oh-my-zsh/plugins/extract/extract.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/fabric/README.md create mode 100644 zsh/.oh-my-zsh/plugins/fabric/_fab create mode 100644 zsh/.oh-my-zsh/plugins/fabric/fabric.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/fancy-ctrl-z/README.md create mode 100644 zsh/.oh-my-zsh/plugins/fancy-ctrl-z/fancy-ctrl-z.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/fasd/README.md create mode 100644 zsh/.oh-my-zsh/plugins/fasd/fasd.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/fastfile/README.md create mode 100644 zsh/.oh-my-zsh/plugins/fastfile/fastfile.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/fbterm/README.md create mode 100644 zsh/.oh-my-zsh/plugins/fbterm/fbterm.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/fd/README.md create mode 100644 zsh/.oh-my-zsh/plugins/fd/_fd create mode 100644 zsh/.oh-my-zsh/plugins/firewalld/README.md create mode 100644 zsh/.oh-my-zsh/plugins/firewalld/firewalld.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/flutter/README.md create mode 100644 zsh/.oh-my-zsh/plugins/flutter/_flutter create mode 100644 zsh/.oh-my-zsh/plugins/flutter/flutter.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/fnm/README.md create mode 100644 zsh/.oh-my-zsh/plugins/fnm/fnm.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/forklift/README.md create mode 100644 zsh/.oh-my-zsh/plugins/forklift/forklift.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/fossil/README.md create mode 100644 zsh/.oh-my-zsh/plugins/fossil/fossil.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/frontend-search/README.md create mode 100644 zsh/.oh-my-zsh/plugins/frontend-search/_frontend-search.sh create mode 100644 zsh/.oh-my-zsh/plugins/frontend-search/frontend-search.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/fzf/README.md create mode 100644 zsh/.oh-my-zsh/plugins/fzf/fzf.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/gas/README.md create mode 100644 zsh/.oh-my-zsh/plugins/gas/_gas create mode 100644 zsh/.oh-my-zsh/plugins/gatsby/README.md create mode 100644 zsh/.oh-my-zsh/plugins/gatsby/_gatsby create mode 100644 zsh/.oh-my-zsh/plugins/gb/README.md create mode 100644 zsh/.oh-my-zsh/plugins/gb/_gb create mode 100644 zsh/.oh-my-zsh/plugins/gcloud/README.md create mode 100644 zsh/.oh-my-zsh/plugins/gcloud/gcloud.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/geeknote/README.md create mode 100644 zsh/.oh-my-zsh/plugins/geeknote/_geeknote create mode 100644 zsh/.oh-my-zsh/plugins/geeknote/geeknote.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/gem/README.md create mode 100644 zsh/.oh-my-zsh/plugins/gem/_gem create mode 100644 zsh/.oh-my-zsh/plugins/gem/gem.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/genpass/README.md create mode 100755 zsh/.oh-my-zsh/plugins/genpass/genpass-apple create mode 100755 zsh/.oh-my-zsh/plugins/genpass/genpass-monkey create mode 100755 zsh/.oh-my-zsh/plugins/genpass/genpass-xkcd create mode 100644 zsh/.oh-my-zsh/plugins/genpass/genpass.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/gh/README.md create mode 100644 zsh/.oh-my-zsh/plugins/gh/gh.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/git-auto-fetch/README.md create mode 100644 zsh/.oh-my-zsh/plugins/git-auto-fetch/git-auto-fetch.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/git-escape-magic/README.md create mode 100644 zsh/.oh-my-zsh/plugins/git-escape-magic/git-escape-magic create mode 100644 zsh/.oh-my-zsh/plugins/git-escape-magic/git-escape-magic.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/git-extras/README.md create mode 100644 zsh/.oh-my-zsh/plugins/git-extras/git-extras.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/git-flow-avh/README.md create mode 100644 zsh/.oh-my-zsh/plugins/git-flow-avh/git-flow-avh.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/git-flow/README.md create mode 100644 zsh/.oh-my-zsh/plugins/git-flow/git-flow.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/git-hubflow/README.md create mode 100644 zsh/.oh-my-zsh/plugins/git-hubflow/git-hubflow.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/git-lfs/README.md create mode 100644 zsh/.oh-my-zsh/plugins/git-lfs/git-lfs.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/git-prompt/README.md create mode 100644 zsh/.oh-my-zsh/plugins/git-prompt/git-prompt.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/git-prompt/gitstatus.py create mode 100644 zsh/.oh-my-zsh/plugins/git/README.md create mode 100644 zsh/.oh-my-zsh/plugins/git/git.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/gitfast/README.md create mode 100644 zsh/.oh-my-zsh/plugins/gitfast/_git create mode 100644 zsh/.oh-my-zsh/plugins/gitfast/git-completion.bash create mode 100644 zsh/.oh-my-zsh/plugins/gitfast/git-prompt.sh create mode 100644 zsh/.oh-my-zsh/plugins/gitfast/gitfast.plugin.zsh create mode 100755 zsh/.oh-my-zsh/plugins/gitfast/update create mode 100644 zsh/.oh-my-zsh/plugins/github/README.md create mode 100644 zsh/.oh-my-zsh/plugins/github/_hub create mode 100644 zsh/.oh-my-zsh/plugins/github/github.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/gitignore/README.md create mode 100644 zsh/.oh-my-zsh/plugins/gitignore/gitignore.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/glassfish/README.md create mode 100644 zsh/.oh-my-zsh/plugins/glassfish/_asadmin create mode 100644 zsh/.oh-my-zsh/plugins/glassfish/glassfish.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/globalias/README.md create mode 100644 zsh/.oh-my-zsh/plugins/globalias/globalias.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/gnu-utils/README.md create mode 100644 zsh/.oh-my-zsh/plugins/gnu-utils/gnu-utils.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/golang/README.md create mode 100644 zsh/.oh-my-zsh/plugins/golang/golang.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/golang/templates/package.txt create mode 100644 zsh/.oh-my-zsh/plugins/golang/templates/search.txt create mode 100644 zsh/.oh-my-zsh/plugins/gpg-agent/README.md create mode 100644 zsh/.oh-my-zsh/plugins/gpg-agent/gpg-agent.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/gradle/README.md create mode 100644 zsh/.oh-my-zsh/plugins/gradle/_gradle create mode 100644 zsh/.oh-my-zsh/plugins/gradle/gradle.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/grails/README.md create mode 100644 zsh/.oh-my-zsh/plugins/grails/grails.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/grc/README.md create mode 100644 zsh/.oh-my-zsh/plugins/grc/grc.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/grunt/README.md create mode 100644 zsh/.oh-my-zsh/plugins/grunt/grunt.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/gulp/README.md create mode 100644 zsh/.oh-my-zsh/plugins/gulp/gulp.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/hanami/README.md create mode 100644 zsh/.oh-my-zsh/plugins/hanami/hanami.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/helm/README.md create mode 100644 zsh/.oh-my-zsh/plugins/helm/helm.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/heroku/README.md create mode 100644 zsh/.oh-my-zsh/plugins/heroku/heroku.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/history-substring-search/README.md create mode 100644 zsh/.oh-my-zsh/plugins/history-substring-search/history-substring-search.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/history-substring-search/history-substring-search.zsh create mode 100755 zsh/.oh-my-zsh/plugins/history-substring-search/update-from-upstream.zsh create mode 100644 zsh/.oh-my-zsh/plugins/history/README.md create mode 100644 zsh/.oh-my-zsh/plugins/history/history.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/hitchhiker/.gitignore create mode 100644 zsh/.oh-my-zsh/plugins/hitchhiker/README.md create mode 100644 zsh/.oh-my-zsh/plugins/hitchhiker/fortunes/hitchhiker create mode 100644 zsh/.oh-my-zsh/plugins/hitchhiker/hitchhiker.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/hitokoto/README.md create mode 100644 zsh/.oh-my-zsh/plugins/hitokoto/hitokoto.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/homestead/README.md create mode 100644 zsh/.oh-my-zsh/plugins/homestead/homestead.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/httpie/README.md create mode 100644 zsh/.oh-my-zsh/plugins/httpie/_httpie create mode 100644 zsh/.oh-my-zsh/plugins/httpie/httpie.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/invoke/README.md create mode 100644 zsh/.oh-my-zsh/plugins/invoke/invoke.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/ionic/README.md create mode 100644 zsh/.oh-my-zsh/plugins/ionic/ionic.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/ipfs/LICENSE create mode 100644 zsh/.oh-my-zsh/plugins/ipfs/README.md create mode 100644 zsh/.oh-my-zsh/plugins/ipfs/_ipfs create mode 100644 zsh/.oh-my-zsh/plugins/isodate/README.md create mode 100644 zsh/.oh-my-zsh/plugins/isodate/isodate.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/iterm2/README.md create mode 100644 zsh/.oh-my-zsh/plugins/iterm2/iterm2.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/jake-node/README.md create mode 100644 zsh/.oh-my-zsh/plugins/jake-node/jake-node.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/jenv/README.md create mode 100644 zsh/.oh-my-zsh/plugins/jenv/jenv.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/jfrog/README.md create mode 100644 zsh/.oh-my-zsh/plugins/jfrog/jfrog.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/jhbuild/README.md create mode 100644 zsh/.oh-my-zsh/plugins/jhbuild/jhbuild.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/jira/README.md create mode 100644 zsh/.oh-my-zsh/plugins/jira/_jira create mode 100644 zsh/.oh-my-zsh/plugins/jira/jira.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/jruby/README.md create mode 100644 zsh/.oh-my-zsh/plugins/jruby/jruby.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/jsontools/README.md create mode 100644 zsh/.oh-my-zsh/plugins/jsontools/jsontools.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/juju/README.md create mode 100644 zsh/.oh-my-zsh/plugins/juju/juju.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/jump/README.md create mode 100644 zsh/.oh-my-zsh/plugins/jump/jump.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/kate/README.md create mode 100644 zsh/.oh-my-zsh/plugins/kate/kate.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/keychain/README.md create mode 100644 zsh/.oh-my-zsh/plugins/keychain/keychain.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/kitchen/README.md create mode 100644 zsh/.oh-my-zsh/plugins/kitchen/_kitchen create mode 100644 zsh/.oh-my-zsh/plugins/knife/README.md create mode 100644 zsh/.oh-my-zsh/plugins/knife/_knife create mode 100644 zsh/.oh-my-zsh/plugins/knife_ssh/README.md create mode 100644 zsh/.oh-my-zsh/plugins/knife_ssh/knife_ssh.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/kops/README.md create mode 100644 zsh/.oh-my-zsh/plugins/kops/kops.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/kube-ps1/README.md create mode 100644 zsh/.oh-my-zsh/plugins/kube-ps1/kube-ps1.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/kubectl/README.md create mode 100644 zsh/.oh-my-zsh/plugins/kubectl/kubectl.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/kubectx/README.md create mode 100644 zsh/.oh-my-zsh/plugins/kubectx/kubectx.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/kubectx/prod.png create mode 100644 zsh/.oh-my-zsh/plugins/kubectx/stage.png create mode 100644 zsh/.oh-my-zsh/plugins/lando/LICENSE create mode 100644 zsh/.oh-my-zsh/plugins/lando/README.md create mode 100644 zsh/.oh-my-zsh/plugins/lando/lando.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/laravel/README.md create mode 100644 zsh/.oh-my-zsh/plugins/laravel/_artisan create mode 100644 zsh/.oh-my-zsh/plugins/laravel/laravel.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/laravel4/README.md create mode 100644 zsh/.oh-my-zsh/plugins/laravel4/laravel4.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/laravel5/README.md create mode 100644 zsh/.oh-my-zsh/plugins/laravel5/laravel5.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/last-working-dir/README.md create mode 100644 zsh/.oh-my-zsh/plugins/last-working-dir/last-working-dir.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/lein/README.md create mode 100644 zsh/.oh-my-zsh/plugins/lein/_lein create mode 100644 zsh/.oh-my-zsh/plugins/lighthouse/README.md create mode 100644 zsh/.oh-my-zsh/plugins/lighthouse/lighthouse.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/lol/README.md create mode 100644 zsh/.oh-my-zsh/plugins/lol/lol.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/lxd/README.md create mode 100644 zsh/.oh-my-zsh/plugins/lxd/lxd.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/macports/README.md create mode 100644 zsh/.oh-my-zsh/plugins/macports/_port create mode 100644 zsh/.oh-my-zsh/plugins/macports/macports.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/magic-enter/README.md create mode 100644 zsh/.oh-my-zsh/plugins/magic-enter/magic-enter.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/man/README.md create mode 100644 zsh/.oh-my-zsh/plugins/man/man.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/marked2/README.md create mode 100644 zsh/.oh-my-zsh/plugins/marked2/marked2.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/mercurial/README.md create mode 100644 zsh/.oh-my-zsh/plugins/mercurial/mercurial.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/meteor/README.md create mode 100644 zsh/.oh-my-zsh/plugins/meteor/_meteor create mode 100644 zsh/.oh-my-zsh/plugins/meteor/meteor.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/microk8s/README.md create mode 100644 zsh/.oh-my-zsh/plugins/microk8s/microk8s.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/minikube/README.md create mode 100644 zsh/.oh-my-zsh/plugins/minikube/minikube.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/mix-fast/README.md create mode 100644 zsh/.oh-my-zsh/plugins/mix-fast/mix-fast.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/mix/README.md create mode 100644 zsh/.oh-my-zsh/plugins/mix/_mix create mode 100644 zsh/.oh-my-zsh/plugins/mongocli/README.md create mode 100644 zsh/.oh-my-zsh/plugins/mongocli/mongocli.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/mosh/README.md create mode 100644 zsh/.oh-my-zsh/plugins/mosh/mosh.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/mvn/README.md create mode 100644 zsh/.oh-my-zsh/plugins/mvn/mvn.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/mysql-macports/README.md create mode 100644 zsh/.oh-my-zsh/plugins/mysql-macports/mysql-macports.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/n98-magerun/README.md create mode 100644 zsh/.oh-my-zsh/plugins/n98-magerun/n98-magerun.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/nanoc/README.md create mode 100644 zsh/.oh-my-zsh/plugins/nanoc/_nanoc create mode 100644 zsh/.oh-my-zsh/plugins/nanoc/nanoc.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/ng/README.md create mode 100644 zsh/.oh-my-zsh/plugins/ng/ng.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/nmap/README.md create mode 100644 zsh/.oh-my-zsh/plugins/nmap/nmap.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/node/README.md create mode 100644 zsh/.oh-my-zsh/plugins/node/node.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/nomad/README.md create mode 100644 zsh/.oh-my-zsh/plugins/nomad/_nomad create mode 100644 zsh/.oh-my-zsh/plugins/npm/README.md create mode 100644 zsh/.oh-my-zsh/plugins/npm/npm.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/npx/README.md create mode 100644 zsh/.oh-my-zsh/plugins/npx/npx.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/nvm/README.md create mode 100644 zsh/.oh-my-zsh/plugins/nvm/_nvm create mode 100644 zsh/.oh-my-zsh/plugins/nvm/nvm.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/oc/README.md create mode 100644 zsh/.oh-my-zsh/plugins/oc/oc.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/octozen/README.md create mode 100644 zsh/.oh-my-zsh/plugins/octozen/octozen.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/osx/README.md create mode 100644 zsh/.oh-my-zsh/plugins/osx/_security create mode 100644 zsh/.oh-my-zsh/plugins/osx/music create mode 100644 zsh/.oh-my-zsh/plugins/osx/osx.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/osx/spotify create mode 100644 zsh/.oh-my-zsh/plugins/otp/README.md create mode 100644 zsh/.oh-my-zsh/plugins/otp/otp.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/pass/README.md create mode 100644 zsh/.oh-my-zsh/plugins/pass/_pass create mode 100644 zsh/.oh-my-zsh/plugins/paver/README.md create mode 100644 zsh/.oh-my-zsh/plugins/paver/paver.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/pep8/README.md create mode 100644 zsh/.oh-my-zsh/plugins/pep8/_pep8 create mode 100644 zsh/.oh-my-zsh/plugins/per-directory-history/README.md create mode 120000 zsh/.oh-my-zsh/plugins/per-directory-history/per-directory-history.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/per-directory-history/per-directory-history.zsh create mode 100644 zsh/.oh-my-zsh/plugins/percol/README.md create mode 100644 zsh/.oh-my-zsh/plugins/percol/percol.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/perl/README.md create mode 100644 zsh/.oh-my-zsh/plugins/perl/perl.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/perms/README.md create mode 100644 zsh/.oh-my-zsh/plugins/perms/perms.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/phing/README.md create mode 100644 zsh/.oh-my-zsh/plugins/phing/phing.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/pip/README.md create mode 100644 zsh/.oh-my-zsh/plugins/pip/_pip create mode 100644 zsh/.oh-my-zsh/plugins/pip/pip.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/pipenv/README.md create mode 100644 zsh/.oh-my-zsh/plugins/pipenv/pipenv.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/pj/README.md create mode 100644 zsh/.oh-my-zsh/plugins/pj/pj.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/please/README.md create mode 100644 zsh/.oh-my-zsh/plugins/please/please.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/pm2/README.md create mode 100644 zsh/.oh-my-zsh/plugins/pm2/_pm2 create mode 100644 zsh/.oh-my-zsh/plugins/pm2/pm2.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/pod/README.md create mode 100644 zsh/.oh-my-zsh/plugins/pod/_pod create mode 100644 zsh/.oh-my-zsh/plugins/postgres/README.md create mode 100644 zsh/.oh-my-zsh/plugins/postgres/postgres.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/pow/README.md create mode 100644 zsh/.oh-my-zsh/plugins/pow/pow.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/powder/README.md create mode 100644 zsh/.oh-my-zsh/plugins/powder/_powder create mode 100644 zsh/.oh-my-zsh/plugins/powify/README.md create mode 100644 zsh/.oh-my-zsh/plugins/powify/_powify create mode 100644 zsh/.oh-my-zsh/plugins/profiles/README.md create mode 100644 zsh/.oh-my-zsh/plugins/profiles/profiles.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/pyenv/README.md create mode 100644 zsh/.oh-my-zsh/plugins/pyenv/pyenv.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/pylint/README.md create mode 100644 zsh/.oh-my-zsh/plugins/pylint/_pylint create mode 100644 zsh/.oh-my-zsh/plugins/pylint/pylint.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/python/README.md create mode 100644 zsh/.oh-my-zsh/plugins/python/python.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/rails/README.md create mode 100644 zsh/.oh-my-zsh/plugins/rails/_rails create mode 100644 zsh/.oh-my-zsh/plugins/rails/rails.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/rake-fast/README.md create mode 100644 zsh/.oh-my-zsh/plugins/rake-fast/rake-fast.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/rake/README.md create mode 100644 zsh/.oh-my-zsh/plugins/rake/rake.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/rand-quote/README.md create mode 100644 zsh/.oh-my-zsh/plugins/rand-quote/rand-quote.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/rbenv/README.md create mode 100644 zsh/.oh-my-zsh/plugins/rbenv/rbenv.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/rbfu/README.md create mode 100644 zsh/.oh-my-zsh/plugins/rbfu/rbfu.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/react-native/README.md create mode 100644 zsh/.oh-my-zsh/plugins/react-native/_react-native create mode 100644 zsh/.oh-my-zsh/plugins/react-native/react-native.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/rebar/README.md create mode 100644 zsh/.oh-my-zsh/plugins/rebar/_rebar create mode 100644 zsh/.oh-my-zsh/plugins/redis-cli/README.md create mode 100644 zsh/.oh-my-zsh/plugins/redis-cli/_redis-cli create mode 100644 zsh/.oh-my-zsh/plugins/repo/README.md create mode 100644 zsh/.oh-my-zsh/plugins/repo/_repo create mode 100644 zsh/.oh-my-zsh/plugins/repo/repo.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/ripgrep/README.md create mode 100644 zsh/.oh-my-zsh/plugins/ripgrep/_ripgrep create mode 100644 zsh/.oh-my-zsh/plugins/ros/README.md create mode 100644 zsh/.oh-my-zsh/plugins/ros/_ros create mode 100644 zsh/.oh-my-zsh/plugins/rsync/README.md create mode 100644 zsh/.oh-my-zsh/plugins/rsync/rsync.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/ruby/README.md create mode 100644 zsh/.oh-my-zsh/plugins/ruby/ruby.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/rust/README.md create mode 100644 zsh/.oh-my-zsh/plugins/rust/_rust create mode 100644 zsh/.oh-my-zsh/plugins/rustup/README.md create mode 100644 zsh/.oh-my-zsh/plugins/rustup/rustup.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/rvm/README.md create mode 100644 zsh/.oh-my-zsh/plugins/rvm/rvm.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/safe-paste/README.md create mode 100644 zsh/.oh-my-zsh/plugins/safe-paste/safe-paste.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/salt/README.md create mode 100644 zsh/.oh-my-zsh/plugins/salt/_salt create mode 100644 zsh/.oh-my-zsh/plugins/samtools/README.md create mode 100644 zsh/.oh-my-zsh/plugins/samtools/_samtools create mode 100644 zsh/.oh-my-zsh/plugins/sbt/README.md create mode 100644 zsh/.oh-my-zsh/plugins/sbt/_sbt create mode 100644 zsh/.oh-my-zsh/plugins/sbt/sbt.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/scala/README.md create mode 100644 zsh/.oh-my-zsh/plugins/scala/_scala create mode 100644 zsh/.oh-my-zsh/plugins/scd/README.md create mode 100644 zsh/.oh-my-zsh/plugins/scd/_scd create mode 100755 zsh/.oh-my-zsh/plugins/scd/scd create mode 100644 zsh/.oh-my-zsh/plugins/scd/scd.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/screen/README.md create mode 100644 zsh/.oh-my-zsh/plugins/screen/screen.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/scw/README.md create mode 100644 zsh/.oh-my-zsh/plugins/scw/_scw create mode 100644 zsh/.oh-my-zsh/plugins/sdk/README.md create mode 100644 zsh/.oh-my-zsh/plugins/sdk/sdk.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/sfdx/README.md create mode 100644 zsh/.oh-my-zsh/plugins/sfdx/_sfdx create mode 100644 zsh/.oh-my-zsh/plugins/sfffe/README.md create mode 100644 zsh/.oh-my-zsh/plugins/sfffe/sfffe.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/shell-proxy/README.md create mode 100755 zsh/.oh-my-zsh/plugins/shell-proxy/proxy.py create mode 100644 zsh/.oh-my-zsh/plugins/shell-proxy/shell-proxy.plugin.zsh create mode 100755 zsh/.oh-my-zsh/plugins/shell-proxy/ssh-agent.py create mode 100755 zsh/.oh-my-zsh/plugins/shell-proxy/ssh-proxy.py create mode 100644 zsh/.oh-my-zsh/plugins/shrink-path/README.md create mode 100644 zsh/.oh-my-zsh/plugins/shrink-path/shrink-path.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/singlechar/README.md create mode 100644 zsh/.oh-my-zsh/plugins/singlechar/singlechar.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/spring/README.md create mode 100644 zsh/.oh-my-zsh/plugins/spring/_spring create mode 100644 zsh/.oh-my-zsh/plugins/sprunge/README.md create mode 100644 zsh/.oh-my-zsh/plugins/sprunge/sprunge.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/ssh-agent/README.md create mode 100644 zsh/.oh-my-zsh/plugins/ssh-agent/ssh-agent.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/stack/README.md create mode 100644 zsh/.oh-my-zsh/plugins/stack/stack.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/sublime-merge/README.md create mode 100644 zsh/.oh-my-zsh/plugins/sublime-merge/sublime-merge.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/sublime/README.md create mode 100644 zsh/.oh-my-zsh/plugins/sublime/sublime.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/sudo/README.md create mode 100644 zsh/.oh-my-zsh/plugins/sudo/sudo.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/supervisor/README.md create mode 100644 zsh/.oh-my-zsh/plugins/supervisor/_supervisorctl create mode 100644 zsh/.oh-my-zsh/plugins/supervisor/_supervisord create mode 100644 zsh/.oh-my-zsh/plugins/supervisor/supervisor.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/suse/README.md create mode 100644 zsh/.oh-my-zsh/plugins/suse/suse.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/svcat/README.md create mode 100644 zsh/.oh-my-zsh/plugins/svcat/svcat.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/svn-fast-info/README.md create mode 100644 zsh/.oh-my-zsh/plugins/svn-fast-info/svn-fast-info.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/svn/README.md create mode 100644 zsh/.oh-my-zsh/plugins/svn/svn.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/swiftpm/README.md create mode 100644 zsh/.oh-my-zsh/plugins/swiftpm/_swift create mode 100644 zsh/.oh-my-zsh/plugins/swiftpm/swiftpm.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/symfony/README.md create mode 100644 zsh/.oh-my-zsh/plugins/symfony/symfony.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/symfony2/README.md create mode 100644 zsh/.oh-my-zsh/plugins/symfony2/symfony2.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/systemadmin/README.md create mode 100644 zsh/.oh-my-zsh/plugins/systemadmin/systemadmin.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/systemd/README.md create mode 100644 zsh/.oh-my-zsh/plugins/systemd/systemd.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/taskwarrior/README.md create mode 100644 zsh/.oh-my-zsh/plugins/taskwarrior/_task create mode 100644 zsh/.oh-my-zsh/plugins/taskwarrior/taskwarrior.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/term_tab/README create mode 100644 zsh/.oh-my-zsh/plugins/term_tab/term_tab.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/terminitor/README.md create mode 100644 zsh/.oh-my-zsh/plugins/terminitor/_terminitor create mode 100644 zsh/.oh-my-zsh/plugins/terraform/README.md create mode 100644 zsh/.oh-my-zsh/plugins/terraform/_terraform create mode 100644 zsh/.oh-my-zsh/plugins/terraform/terraform.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/textastic/README.md create mode 100644 zsh/.oh-my-zsh/plugins/textastic/textastic.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/textmate/README.md create mode 100644 zsh/.oh-my-zsh/plugins/textmate/textmate.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/thefuck/README.md create mode 100644 zsh/.oh-my-zsh/plugins/thefuck/thefuck.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/themes/README.md create mode 100644 zsh/.oh-my-zsh/plugins/themes/themes.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/thor/README.md create mode 100644 zsh/.oh-my-zsh/plugins/thor/_thor create mode 100644 zsh/.oh-my-zsh/plugins/tig/README.md create mode 100644 zsh/.oh-my-zsh/plugins/tig/tig.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/timer/README.md create mode 100644 zsh/.oh-my-zsh/plugins/timer/timer.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/tmux-cssh/README.md create mode 100644 zsh/.oh-my-zsh/plugins/tmux-cssh/_tmux-cssh create mode 100644 zsh/.oh-my-zsh/plugins/tmux/README.md create mode 100644 zsh/.oh-my-zsh/plugins/tmux/tmux.extra.conf create mode 100644 zsh/.oh-my-zsh/plugins/tmux/tmux.only.conf create mode 100644 zsh/.oh-my-zsh/plugins/tmux/tmux.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/tmuxinator/README.md create mode 100644 zsh/.oh-my-zsh/plugins/tmuxinator/_tmuxinator create mode 100644 zsh/.oh-my-zsh/plugins/tmuxinator/tmuxinator.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/torrent/README.md create mode 100644 zsh/.oh-my-zsh/plugins/torrent/torrent.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/transfer/README.md create mode 100644 zsh/.oh-my-zsh/plugins/transfer/transfer.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/tugboat/README.md create mode 100644 zsh/.oh-my-zsh/plugins/tugboat/_tugboat create mode 100644 zsh/.oh-my-zsh/plugins/ubuntu/README.md create mode 100644 zsh/.oh-my-zsh/plugins/ubuntu/ubuntu.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/ufw/README.md create mode 100644 zsh/.oh-my-zsh/plugins/ufw/_ufw create mode 100644 zsh/.oh-my-zsh/plugins/universalarchive/README.md create mode 100644 zsh/.oh-my-zsh/plugins/universalarchive/_universalarchive create mode 100644 zsh/.oh-my-zsh/plugins/universalarchive/universalarchive.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/urltools/README.md create mode 100644 zsh/.oh-my-zsh/plugins/urltools/urltools.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/vagrant-prompt/README.md create mode 100644 zsh/.oh-my-zsh/plugins/vagrant-prompt/vagrant-prompt.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/vagrant/README.md create mode 100644 zsh/.oh-my-zsh/plugins/vagrant/_vagrant create mode 100644 zsh/.oh-my-zsh/plugins/vagrant/vagrant.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/vault/README.md create mode 100644 zsh/.oh-my-zsh/plugins/vault/_vault create mode 100644 zsh/.oh-my-zsh/plugins/vi-mode/README.md create mode 100644 zsh/.oh-my-zsh/plugins/vi-mode/vi-mode.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/vim-interaction/README.md create mode 100644 zsh/.oh-my-zsh/plugins/vim-interaction/vim-interaction.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/virtualenv/README.md create mode 100644 zsh/.oh-my-zsh/plugins/virtualenv/virtualenv.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/virtualenvwrapper/README.md create mode 100644 zsh/.oh-my-zsh/plugins/virtualenvwrapper/virtualenvwrapper.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/vscode/README.md create mode 100644 zsh/.oh-my-zsh/plugins/vscode/vscode.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/vundle/README.md create mode 100644 zsh/.oh-my-zsh/plugins/vundle/vundle.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/wakeonlan/README.md create mode 100644 zsh/.oh-my-zsh/plugins/wakeonlan/_wake create mode 100644 zsh/.oh-my-zsh/plugins/wakeonlan/wakeonlan.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/wd/LICENSE create mode 100644 zsh/.oh-my-zsh/plugins/wd/README.md create mode 100644 zsh/.oh-my-zsh/plugins/wd/_wd.sh create mode 100644 zsh/.oh-my-zsh/plugins/wd/wd.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/wd/wd.sh create mode 100644 zsh/.oh-my-zsh/plugins/web-search/README.md create mode 100644 zsh/.oh-my-zsh/plugins/web-search/web-search.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/wp-cli/README.md create mode 100644 zsh/.oh-my-zsh/plugins/wp-cli/wp-cli.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/xcode/README.md create mode 100644 zsh/.oh-my-zsh/plugins/xcode/_xcselv create mode 100644 zsh/.oh-my-zsh/plugins/xcode/xcode.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/yarn/README.md create mode 100644 zsh/.oh-my-zsh/plugins/yarn/_yarn create mode 100644 zsh/.oh-my-zsh/plugins/yarn/yarn.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/yii/README.md create mode 100644 zsh/.oh-my-zsh/plugins/yii/yii.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/yii2/README.md create mode 100644 zsh/.oh-my-zsh/plugins/yii2/yii2.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/yum/README.md create mode 100644 zsh/.oh-my-zsh/plugins/yum/yum.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/z/Makefile create mode 100644 zsh/.oh-my-zsh/plugins/z/README create mode 100644 zsh/.oh-my-zsh/plugins/z/README.md create mode 100644 zsh/.oh-my-zsh/plugins/z/z.1 create mode 100644 zsh/.oh-my-zsh/plugins/z/z.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/z/z.sh create mode 100644 zsh/.oh-my-zsh/plugins/zbell/README.md create mode 100644 zsh/.oh-my-zsh/plugins/zbell/zbell.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/zeus/README.md create mode 100644 zsh/.oh-my-zsh/plugins/zeus/_zeus create mode 100644 zsh/.oh-my-zsh/plugins/zeus/zeus.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/zoxide/README.md create mode 100644 zsh/.oh-my-zsh/plugins/zoxide/zoxide.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/zsh-interactive-cd/README.md create mode 100644 zsh/.oh-my-zsh/plugins/zsh-interactive-cd/zsh-interactive-cd.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/.config/znt/README.txt create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/.config/znt/n-aliases.conf create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/.config/znt/n-cd.conf create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/.config/znt/n-env.conf create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/.config/znt/n-functions.conf create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/.config/znt/n-history.conf create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/.config/znt/n-kill.conf create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/.config/znt/n-list.conf create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/.config/znt/n-options.conf create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/.config/znt/n-panelize.conf create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/LICENSE create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/Makefile create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/NEWS create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/README.md create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/_n-kill create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/n-aliases create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/n-cd create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/n-env create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/n-functions create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/n-help create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/n-history create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/n-kill create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/n-list create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/n-list-draw create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/n-list-input create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/n-options create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/n-panelize create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/znt-cd-widget create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/znt-history-widget create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/znt-kill-widget create mode 100755 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/znt-tmux.zsh create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/znt-usetty-wrapper create mode 100644 zsh/.oh-my-zsh/plugins/zsh-navigation-tools/zsh-navigation-tools.plugin.zsh create mode 100644 zsh/.oh-my-zsh/plugins/zsh_reload/README.md create mode 100644 zsh/.oh-my-zsh/plugins/zsh_reload/zsh_reload.plugin.zsh create mode 100644 zsh/.oh-my-zsh/templates/zshrc.zsh-template create mode 100644 zsh/.oh-my-zsh/themes/3den.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/Soliah.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/adben.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/af-magic.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/afowler.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/agnoster.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/alanpeabody.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/amuse.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/apple.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/arrow.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/aussiegeek.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/avit.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/awesomepanda.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/bira.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/blinks.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/bureau.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/candy-kingdom.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/candy.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/clean.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/cloud.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/crcandy.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/crunch.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/cypher.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/dallas.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/darkblood.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/daveverwer.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/dieter.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/dogenpunk.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/dpoggi.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/dst.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/dstufft.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/duellj.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/eastwood.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/edvardm.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/emotty.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/essembeh.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/evan.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/fino-time.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/fino.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/fishy.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/flazz.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/fletcherm.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/fox.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/frisk.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/frontcube.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/funky.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/fwalch.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/gallifrey.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/gallois.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/garyblessington.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/gentoo.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/geoffgarside.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/gianu.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/gnzh.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/gozilla.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/half-life.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/humza.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/imajes.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/intheloop.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/itchy.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/jaischeema.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/jbergantine.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/jispwoso.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/jnrowe.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/jonathan.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/josh.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/jreese.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/jtriley.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/juanghurtado.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/junkfood.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/kafeitu.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/kardan.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/kennethreitz.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/kiwi.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/kolo.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/kphoen.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/lambda.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/linuxonly.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/lukerandall.zsh-theme create mode 120000 zsh/.oh-my-zsh/themes/macovsky-ruby.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/macovsky.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/maran.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/mgutz.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/mh.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/michelebologna.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/mikeh.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/miloshadzic.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/minimal.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/mira.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/mlh.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/mortalscumbag.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/mrtazz.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/murilasso.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/muse.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/nanotech.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/nebirhos.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/nicoulaj.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/norm.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/obraun.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/peepcode.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/philips.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/pmcgee.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/pygmalion-virtualenv.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/pygmalion.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/random.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/re5et.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/refined.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/rgm.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/risto.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/rixius.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/rkj-repos.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/rkj.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/robbyrussell.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/sammy.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/simonoff.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/simple.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/skaro.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/smt.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/sonicradish.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/sorin.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/sporty_256.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/steeef.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/strug.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/sunaku.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/sunrise.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/superjarin.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/suvash.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/takashiyoshida.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/terminalparty.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/theunraveler.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/tjkirch.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/tjkirch_mod.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/tonotdo.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/trapd00r.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/wedisagree.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/wezm+.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/wezm.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/wuffers.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/xiong-chiamiov-plus.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/xiong-chiamiov.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/ys.zsh-theme create mode 100644 zsh/.oh-my-zsh/themes/zhann.zsh-theme create mode 100755 zsh/.oh-my-zsh/tools/changelog.sh create mode 100644 zsh/.oh-my-zsh/tools/check_for_upgrade.sh create mode 100755 zsh/.oh-my-zsh/tools/install.sh create mode 100755 zsh/.oh-my-zsh/tools/require_tool.sh create mode 100755 zsh/.oh-my-zsh/tools/theme_chooser.sh create mode 100644 zsh/.oh-my-zsh/tools/uninstall.sh create mode 100755 zsh/.oh-my-zsh/tools/upgrade.sh create mode 100644 zsh/.zshrc create mode 100644 zsh/fig_prompt create mode 100644 zsh/g3path.zsh create mode 100755 zsh/goog_prompt.zsh diff --git a/config/.config/nvim/coc-settings.json b/config/.config/nvim/coc-settings.json new file mode 100644 index 0000000..2f571ed --- /dev/null +++ b/config/.config/nvim/coc-settings.json @@ -0,0 +1,56 @@ +{ + "languageserver": { + "ciderlsp": { + "command": "/google/bin/releases/cider/ciderlsp/ciderlsp", + "args": [ + "--tooltag=coc-nvim", + "--noforward_sync_responses", + "-hub_addr=blade:languageservices-staging" + + ], + "filetypes": [ + "borg", + "c", + "cpp", + "go", + "java", + "kotlin", + "proto", + "python", + "textproto" + ] + } + }, + "diagnostic-languageserver.filetypes": { + "borg":"/google/bin/releases/cider/ciderlsp/ciderlsp", + "c":"/google/bin/releases/cider/ciderlsp/ciderlsp", + "cpp":"/google/bin/releases/cider/ciderlsp/ciderlsp", + "go":"/google/bin/releases/cider/ciderlsp/ciderlsp", + "java":"/google/bin/releases/cider/ciderlsp/ciderlsp", + "kt":"/google/bin/releases/cider/ciderlsp/ciderlsp", + "kotlin":"/google/bin/releases/cider/ciderlsp/ciderlsp", + "proto":"/google/bin/releases/cider/ciderlsp/ciderlsp", + "python":"/google/bin/releases/cider/ciderlsp/ciderlsp", + "textproto": "/google/bin/releases/cider/ciderlsp/ciderlsp" + }, + "diagnostic-languageserver.linters": { + "borg":"/google/bin/releases/cider/ciderlsp/ciderlsp", + "c":"/google/bin/releases/cider/ciderlsp/ciderlsp", + "cpp":"/google/bin/releases/cider/ciderlsp/ciderlsp", + "go":"/google/bin/releases/cider/ciderlsp/ciderlsp", + "java":"/google/bin/releases/cider/ciderlsp/ciderlsp", + "kt":"/google/bin/releases/cider/ciderlsp/ciderlsp", + "kotlin":"/google/bin/releases/cider/ciderlsp/ciderlsp", + "proto":"/google/bin/releases/cider/ciderlsp/ciderlsp", + "python":"/google/bin/releases/cider/ciderlsp/ciderlsp", + "textproto": "/google/bin/releases/cider/ciderlsp/ciderlsp" + }, + "java.completion.guessMethodArguments": true, + "java.progressReports.enabled": true, + "coc.preferences.currentFunctionSymbolAutoUpdate": true, + "coc.preferences.willSaveHandlerTimeout":1000, + "codeLens.enable": true, + "diagnostic.checkCurrentLine": true, + "diagnostic.virtualText": true, + "diagnostic.virtualTextCurrentLineOnly": false +} diff --git a/config/.config/nvim/init.vim b/config/.config/nvim/init.vim new file mode 100644 index 0000000..aea6d95 --- /dev/null +++ b/config/.config/nvim/init.vim @@ -0,0 +1 @@ +source ~/.vimrc diff --git a/config/.config/nvim/lua/diagnostics.lua b/config/.config/nvim/lua/diagnostics.lua new file mode 100644 index 0000000..8cfb740 --- /dev/null +++ b/config/.config/nvim/lua/diagnostics.lua @@ -0,0 +1,55 @@ +-- Diagnostics +require("trouble").setup({ + position = "bottom", -- position of the list can be: bottom, top, left, right + height = 10, -- height of the trouble list when position is top or bottom + width = 50, -- width of the list when position is left or right + icons = true, -- use devicons for filenames + mode = "workspace_diagnostics", -- "workspace_diagnostics", "document_diagnostics", "quickfix", "lsp_references", "loclist" + fold_open = "", -- icon used for open folds + fold_closed = "", -- icon used for closed folds + group = true, -- group results by file + padding = true, -- add an extra new line on top of the list + action_keys = { -- key mappings for actions in the trouble list + -- map to {} to remove a mapping, for example: + -- close = {}, + close = "q", -- close the list + cancel = "", -- cancel the preview and get back to your last window / buffer / cursor + refresh = "r", -- manually refresh + jump = { "", "" }, -- jump to the diagnostic or open / close folds + open_split = { "" }, -- open buffer in new split + open_vsplit = { "" }, -- open buffer in new vsplit + open_tab = { "" }, -- open buffer in new tab + jump_close = { "o" }, -- jump to the diagnostic and close the list + toggle_mode = "m", -- toggle between "workspace" and "document" diagnostics mode + toggle_preview = "P", -- toggle auto_preview + hover = "L", -- opens a small popup with the full multiline message + preview = "p", -- preview the diagnostic location + close_folds = { "zM", "zm" }, -- close all folds + open_folds = { "zR", "zr" }, -- open all folds + toggle_fold = { "zA", "za" }, -- toggle fold of current file + previous = "k", -- preview item + next = "j", -- next item + }, + indent_lines = true, -- add an indent guide below the fold icons + auto_open = false, -- automatically open the list when you have diagnostics + auto_close = false, -- automatically close the list when you have no diagnostics + auto_preview = true, -- automatically preview the location of the diagnostic. to close preview and go back to last window + auto_fold = false, -- automatically fold a file trouble list at creation + auto_jump = { "lsp_definitions" }, -- for the given modes, automatically jump if there is only a single result + signs = { + -- icons / text used for a diagnostic + error = "", + warning = "", + hint = "", + information = "", + other = "﫠", + }, + use_diagnostic_signs = false, -- enabling this will use the signs defined in your lsp client +}) + +-- Mappings +vim.api.nvim_set_keymap("n", "xx", "Trouble", { silent = true, noremap = true }) +vim.api.nvim_set_keymap("n", "xw", "Trouble workspace_diagnostics", { silent = true, noremap = true }) +vim.api.nvim_set_keymap("n", "xd", "Trouble document_diagnostics", { silent = true, noremap = true }) +vim.api.nvim_set_keymap("n", "xl", "Trouble loclist", { silent = true, noremap = true }) +vim.api.nvim_set_keymap("n", "xq", "Trouble quickfix", { silent = true, noremap = true }) diff --git a/config/.config/nvim/lua/lsp.lua b/config/.config/nvim/lua/lsp.lua new file mode 100644 index 0000000..4538e6b --- /dev/null +++ b/config/.config/nvim/lua/lsp.lua @@ -0,0 +1,176 @@ +-- 1. Configure CiderLSP +local nvim_lsp = require("lspconfig") +local configs = require("lspconfig.configs") +configs.ciderlsp = { + default_config = { + cmd = { "/google/bin/releases/cider/ciderlsp/ciderlsp", "--tooltag=nvim-lsp", "--noforward_sync_responses" }, + filetypes = { "c", "cpp", "java", "kotlin", "proto", "textproto", "go", "python", "bzl" }, + root_dir = nvim_lsp.util.root_pattern("BUILD"), + settings = {}, + }, +} + +-- 2. Configure CMP +vim.opt.completeopt = { "menu", "menuone", "noselect" } + +-- Don't show the dumb matching stuff +vim.opt.shortmess:append("c") + +local lspkind = require("lspkind") +lspkind.init() + +local cmp = require("cmp") + +cmp.setup({ + mapping = { + [""] = cmp.mapping.scroll_docs(-4), + [""] = cmp.mapping.scroll_docs(4), + [""] = cmp.mapping.close(), + [""] = cmp.mapping(cmp.mapping.complete(), { "i", "c" }), + [""] = cmp.mapping.confirm({ select = true }), + [""] = cmp.mapping(function(fallback) + if cmp.visible() then + cmp.select_next_item() + elseif vim.fn["vsnip#available"](1) == 1 then + feedkey("(vsnip-expand-or-jump)", "") + elseif has_words_before() then + cmp.complete() + else + fallback() -- The fallback function sends a already mapped key. In this case, it's probably ``. + end + end, { "i", "s" }), + + [""] = cmp.mapping(function() + if cmp.visible() then + cmp.select_prev_item() + elseif vim.fn["vsnip#jumpable"](-1) == 1 then + feedkey("(vsnip-jump-prev)", "") + end + end, { "i", "s" }), + + [""] = cmp.mapping(function(fallback) + if cmp.visible() then + cmp.select_prev_item() + elseif vim.fn["vsnip#available"](1) == 1 then + feedkey("(vsnip-jump-prev)", "") + else + fallback() -- The fallback function sends a already mapped key. In this case, it's probably ``. + end + end), + + [""] = cmp.mapping(function(fallback) + if cmp.visible() then + cmp.select_next_item() + elseif vim.fn["vsnip#available"](1) == 1 then + feedkey("(vsnip-expand-or-jump)", "") + else + fallback() -- The fallback function sends a already mapped key. In this case, it's probably ``. + end + end), + }, + + sources = { + { name = "nvim_lua" }, + { name = "nvim_lsp" }, + { name = "path" }, + { name = "vim_vsnip" }, + { name = "buffer", keyword_length = 5 }, + }, + + sorting = { + comparators = { + cmp.config.compare.offset, + cmp.config.compare.exact, + cmp.config.compare.score, + + function(entry1, entry2) + local _, entry1_under = entry1.completion_item.label:find("^_+") + local _, entry2_under = entry2.completion_item.label:find("^_+") + entry1_under = entry1_under or 0 + entry2_under = entry2_under or 0 + if entry1_under > entry2_under then + return false + elseif entry1_under < entry2_under then + return true + end + end, + + cmp.config.compare.kind, + cmp.config.compare.sort_text, + cmp.config.compare.length, + cmp.config.compare.order, + }, + }, + + snippet = { + expand = function(args) + vim.fn["vsnip#anonymous"](args.body) + end, + }, + + formatting = { + format = lspkind.cmp_format({ + with_text = true, + maxwidth = 40, -- half max width + menu = { + buffer = "[buffer]", + nvim_lsp = "[CiderLSP]", + nvim_lua = "[API]", + path = "[path]", + vim_vsnip = "[snip]", + }, + }), + }, + + experimental = { + native_menu = false, + ghost_text = true, + }, +}) + +vim.cmd([[ + augroup CmpZsh + au! + autocmd Filetype zsh lua require'cmp'.setup.buffer { sources = { { name = "zsh" }, } } + augroup END +]]) + +-- 3. Set up CiderLSP +local on_attach = function(client, bufnr) + vim.api.nvim_buf_set_option(bufnr, "omnifunc", "v:lua.vim.lsp.omnifunc") + if vim.lsp.formatexpr then -- Neovim v0.6.0+ only. + vim.api.nvim_buf_set_option(bufnr, "formatexpr", "v:lua.vim.lsp.formatexpr") + end + if vim.lsp.tagfunc then + vim.api.nvim_buf_set_option(bufnr, "tagfunc", "v:lua.vim.lsp.tagfunc") + end + + local opts = { noremap = true, silent = true } + vim.api.nvim_buf_set_keymap(bufnr, "n", "rn", "lua vim.lsp.buf.rename()", opts) + vim.api.nvim_buf_set_keymap(bufnr, "n", "ca", "lua vim.lsp.buf.code_action()", opts) + vim.api.nvim_buf_set_keymap(bufnr, "n", "L", "lua vim.lsp.buf.hover()", opts) + vim.api.nvim_buf_set_keymap(bufnr, "n", "g0", "lua vim.lsp.buf.document_symbol()", opts) + vim.api.nvim_buf_set_keymap(bufnr, "n", "gW", "lua vim.lsp.buf.workspace_symbol()", opts) + vim.api.nvim_buf_set_keymap(bufnr, "n", "gd", "lua vim.lsp.buf.definition()", opts) + vim.api.nvim_buf_set_keymap(bufnr, "n", "gD", "lua vim.lsp.buf.declaration()", opts) + vim.api.nvim_buf_set_keymap(bufnr, "n", "gi", "lua vim.lsp.buf.implementation()", opts) + vim.api.nvim_buf_set_keymap(bufnr, "n", "gr", "lua vim.lsp.buf.references()", opts) + vim.api.nvim_buf_set_keymap(bufnr, "n", "", "lua vim.lsp.buf.signature_help()", opts) + vim.api.nvim_buf_set_keymap(bufnr, "n", "gt", "lua vim.lsp.buf.type_definition()", opts) + vim.api.nvim_buf_set_keymap(bufnr, "n", "[g", "lua vim.diagnostic.goto_prev()", opts) + vim.api.nvim_buf_set_keymap(bufnr, "n", "]g", "lua vim.diagnostic.goto_next()", opts) + + vim.api.nvim_command("augroup LSP") + vim.api.nvim_command("autocmd!") + if client.resolved_capabilities.document_highlight then + vim.api.nvim_command("autocmd CursorHold lua vim.lsp.buf.document_highlight()") + vim.api.nvim_command("autocmd CursorHoldI lua vim.lsp.buf.document_highlight()") + vim.api.nvim_command("autocmd CursorMoved lua vim.lsp.util.buf_clear_references()") + end + vim.api.nvim_command("augroup END") +end + +nvim_lsp.ciderlsp.setup({ + capabilities = require("cmp_nvim_lsp").update_capabilities(vim.lsp.protocol.make_client_capabilities()), + on_attach = on_attach, +}) diff --git a/fzf/fzf-at-google.zsh b/fzf/fzf-at-google.zsh new file mode 100644 index 0000000..c7fe189 --- /dev/null +++ b/fzf/fzf-at-google.zsh @@ -0,0 +1,255 @@ +# See go/fzf-at-google for more on this script. + +export FZF_SOURCE="${HOME}/fzf-relevant-files.zsh" + +function create_fzf_command() { + rg --files $(${FZF_SOURCE}) +} + +# These are weird because they're invoked via `sh -c` or something, so we need +# to pass in the functions we use. Man this is ugly af. Also can't set one to +# the other, they both need to be as-is. I am bad at zsh. +export FZF_DEFAULT_COMMAND="$(functions create_fzf_command); create_fzf_command" +export FZF_CTRL_T_COMMAND="$(functions create_fzf_command); create_fzf_command" +export FZF_ALT_C_COMMAND="fdfind -t d . $(${FZF_SOURCE})" + +_find_fig_workspaces() { + hg citc -l +} + +_find_blaze_targets() { + # Our tool outputs space-separated directories of interest. Conver these to a + # list, which we'll refer to later. + local pkg + local cleandir + local DIRS=( $(${FZF_SOURCE}) ) + # for dir in "${DIRS[@]}"; do + # echo "$dir" + # done + for dir in "${DIRS[@]}"; do + # Here we want: + # .* - match all targets + # //foo/bar/baz/... - the blaze package we're searching under + # We do a trailing '&' so that we run all of these in parallel. + # We shunt 2>/dev/null to silence its info output, which for some reason + # comes out on stderr. This isn't ideal, because we'll swallow real errors. + # Looking at `blaze help query`, I can't find any options to turn this off. + # `--logging=0` doesn't do it; `--show_loading_progress=false` doesn't do + # it. + # Strip any leading // and trailing /. This allows more flexibility in how + # people want to define their targets in their FZF_SOURCE file. + cleandir=`echo $dir | sed 's/^\/\///; s/\/$//'` + pkg="//${cleandir}/..." + blaze query "filter(.*, ${pkg})" 2>/dev/null & + done +} + + +# fzf completion for blaze targets beneath the current directory. eg: +# +# blaze build ** +# +# Syntax for this style of completion is taken from: +# https://github.com/junegunn/fzf/wiki/Examples-(completion)#writing-custom-fuzzy-completion +_fzf_complete_blaze() { + _fzf_complete "" "$@" < <(_find_blaze_targets) +} + +# fzf completion for rabbit targets beneath the current directory. eg: +# +# rabbit test ** +# +_fzf_complete_rabbit() { + _fzf_complete "" "$@" < <(_find_blaze_targets) +} + +_fzf_complete_hgd() { + _fzf_complete "" "$@" < <(_find_fig_workspaces) +} + +# fzf completion of blaze targets for `hg blaze` and `rabbit` commands. eg: +# +# hg blaze -r . -- test ** +# +_fzf_complete_hg() { + ARGS="$@" + if [[ $ARGS == 'hg blaze'* ]] || [[ $ARGS == 'hg rabbit'* ]]; then + _fzf_complete "" "$@" < <(_find_blaze_targets) + elif [[ $ARGS == 'hg citc -d'* ]]; then + _fzf_complete "" "$@" < <(_find_fig_workspaces) + else + eval "zle ${fzf_default_completion:-expand-or-complete}" + fi +} + +# fzf completion for chrome build targets. eg: +# +# autoninja -C out/Debug ** +# +_fzf_complete_autoninja() { + # ${BUFFER} contains all the arguments passed to the command. We assume that + # one will be the directory containing the ninja files, and that it begins + # with `out/`. It might not be the last command, so we can't use positional + # arguments. We'll use rg. No line numbers (`--no-line-number`), print only + # the matching part (`-o`). + NINJA_DIR="$(echo ${BUFFER} | rg --no-line-number -o 'out/\w*')" + + # We assume that the file is then in `${NINJA_DIR}/build.ninja`. Note that + # we're assuming we're not passing with a trailing slash. + BUILD_FILE=${NINJA_DIR}/build.ninja + + _fzf_complete "" "$@" < <( + # The lines we want look like: + # + # build target_name: blah something/else.stamp + # + # Grab those lines. + rg --no-line-number "^build \w*:" ${BUILD_FILE} | \ + # Print the second token, which is `target_name:`. + awk ' { print $2 } ' | \ + # Strip the colon. + sed s/://g + ) +} + + +# This generates a list of flags that have previously been used in history +# commands, trying to intelligently parse values for reuse on the prompt. For +# example, with the following history entry files (as shown on zsh): +# +# 1 foo --test --bar=val +# 2 foo -f 123 --bar val +# +# `_find_flags foo` should return: +# +# --test +# --bar +# --bar=val +# -f +# -f 123 +# --bar val +_find_flags() { + # $1 is passed to the function and should be the command. + local match_prefix=$1 + fc -rl 1 | \ + # strip leading command number and trailing slashes. Trailing slashes + # somehow confuse fzf or the do while. + sed -e 's/^\s*[0-9]*\*\?\s*//' -e 's/\\\+$//' | \ + rg "^${match_prefix}" --color=never --no-line-number | +awk -v match_prefix=${match_prefix} ' { for (i = 1; i <= NF; i++) { + flag = "" + is_value = "" + maybe_value = "" + + if ($i ~ /^\\\\n/) { + # Then this begins might be the first line after a continuation and begin + # like "\\n--foo". We want this to be interpreted as if fresh, without a new + # line. + $i = gensub(/^\\\\n/, "", "g", $i) + } + + if ($i ~ /^--?[a-zA-Z0-9]/) { + # Then it looks like a flag. + split($i, parts, "=") + if (parts[2] != "") { + # It is something like --flag=value + flag = parts[1] + is_value = parts[2] + } else { + # It is something like -f, and might be -f val. + flag = $i + maybe_value = $(i+1) + if (maybe_value ~ /^\\\\n/) { + # Then we probably consumed the next line in a line continuation. + # Newlines in output will confuse a later process, so remove this. + maybe_value = gensub(/^\\\\n/, "", "g", maybe_value) + } + if (maybe_value ~ /^--?[a-zA-Z0-9]/) { + # Then we probably consumed another flag, eg `--foo` from + # `--foo --bar=val`. Reset to empty string so we do not print that as an + # option, as if `-foo` took the value `--bar=val`. + maybe_value = "" + } + } + + # Colorize the part that we will not match in the output to make clear + # we are matching only the leading flags. + cmd_with_color = "\033[0;35m" $0 "\033[0m" + + # A note on the \xC2\xA0 strings here: we want a nbsp before the command so + # that we can split easily and pull out the flag rather than the entire + # line. This also allows us to tell fzf --nth and select only the first + # column to search on. This is the nbsp notation that awk is able to output. + # Fzf wants a \u00a0 format, which we use elsehwere, but note that these are + # the same character. + if (flag != "") { + # Then we parsed a flag. + + # Print the flag itself. + if (seen_arr[flag] != 1) { + seen_arr[flag] = 1 + print flag "\xC2\xA0" cmd_with_color + } + + if (is_value != "") { + # The whole token is a valid value. + if (seen_arr[$i] != 1) { + seen_arr[$i] = 1 + print $i "\xC2\xA0" cmd_with_color + } + } + if (maybe_value != "") { + # We guessed at a value. + output_with_guessed_value = flag " " maybe_value + if (seen_arr[output_with_guessed_value] != 1) { + seen_arr[output_with_guessed_value] = 1 + print flag " " maybe_value "\xC2\xA0 " cmd_with_color + } + } + } + } else { + continue + } +} +}' + +} + +# CTRL-Q - Paste the selected flags into the command line. Copied from CTRL-T +# bindings shown here: +# https://github.com/junegunn/fzf/blob/master/shell/key-bindings.zsh +__flagsel() { + # Normally, BUFFER is adequate. However, if we're in a line continutation, as + # indicated by CONTEXT=cont, we want PREBUFFER: + # https://linux.die.net/man/1/zshzle. + local buffer_with_start_of_cmd=${BUFFER} + if [[ $CONTEXT == "cont" ]]; then + buffer_with_start_of_cmd=${PREBUFFER} + fi + # echo "bwsoc: |${buffer_with_start_of_cmd}|" + # First we tr to remove new lines. and whitespace, which can trip us up on + # multiline input like continuations. Then we use sed to replace white space + # with space, which we will use as our delimiter to cut. + local match_prefix=`echo ${buffer_with_start_of_cmd} | tr "\n" " " | sed -e "s/[[:space:]]\+/ /g" -e "s/\n/ /g" | cut -d ' ' -f 1` + local cmd="_find_flags ${match_prefix}" + setopt localoptions pipefail no_aliases 2> /dev/null + local item + # Need to set this outside the surrounding string to enable $'' notation so + # fzf is able to interpret the delimiter correctly. + local delimiter_arg=$'--delimiter \u00a0' + eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --prompt='${match_prefix}> ' ${delimiter_arg} --nth 1 --reverse --multi --ansi $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read item; do + echo -n "${item}" | awk -F '\xC2\xA0' '{ if (NF > 1) { printf "%s ", $1 } }' + done + local ret=$? + echo + return $ret +} + +fzf-flag-widget() { + LBUFFER="${LBUFFER}$(__flagsel)" + local ret=$? + zle reset-prompt + return $ret +} +zle -N fzf-flag-widget +bindkey '^Q' fzf-flag-widget diff --git a/fzf/fzf-relevant-files.zsh b/fzf/fzf-relevant-files.zsh new file mode 100755 index 0000000..e741a8f --- /dev/null +++ b/fzf/fzf-relevant-files.zsh @@ -0,0 +1,48 @@ +#!/usr/bin/zsh +setopt extended_glob + +# See go/fzf-at-google for more on this script. +# +# Thanks to yoongu@, smithian@ and others for help with the approach. See this +# discussion for more history: +# +# https://groups.google.com/a/google.com/g/zsh-users/c/gLVYmwSy_lg/m/x6wnesDFCAAJ + +# This prints paths we care about. Source it with $(${path_to_file}) and then +# feed it to FZF. This assumes that the content will be passed to `rg`, which +# means it does things like recursively search the current working directory by +# default. + +# These are relative to google3. +# +# NOTE: These are examples relevant to my own projects. Change replace them with +# your own. +RELEVANT_FILE_PATHS=( + "experimental/users/$USER" + "java/com/google/android/gmscore/integ" + "javatests/com/google/android/gmscore/integ" +) + +SEARCH_REL_PATHS=() + +# If we are in a google3 directory, use our relevant files. Otherwise, assume +# we're in something like ~/dotfiles and return everything since we don't need +# to be smart. +if [[ $PWD == (#b)(/google/src/cloud/${USER}/[^/]##/google3)* ]]; then + # Get the google3 absolute path as a backreference (#b). + GOOGLE3_ABS_PATH=${match[1]} + # Prepend google3 to get absolute whitelists. + WHITELIST_ABS_PATHS=(${RELEVANT_FILE_PATHS/#/${GOOGLE3_ABS_PATH}/}) + # We only search a path if it's either the PWD or descended from it. + SEARCH_ABS_PATH=(${(M)WHITELIST_ABS_PATHS:#${PWD}(|/*)}) + # Remove hte leading / as well + SEARCH_REL_PATHS=(${SEARCH_ABS_PATH/${PWD}\//}) +fi + +# Default to searching the PWD. +if [[ ${#SEARCH_REL_PATHS} == 0 ]]; then + SEARCH_REL_PATHS=("") +fi + +echo $SEARCH_REL_PATHS + diff --git a/fzf/fzf/.github/FUNDING.yml b/fzf/fzf/.github/FUNDING.yml new file mode 100644 index 0000000..21ef360 --- /dev/null +++ b/fzf/fzf/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: ["https://paypal.me/junegunn", "https://www.buymeacoffee.com/junegunn"] diff --git a/fzf/fzf/.github/ISSUE_TEMPLATE.md b/fzf/fzf/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..1ba2978 --- /dev/null +++ b/fzf/fzf/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,22 @@ + + + + +- [ ] I have read through the manual page (`man fzf`) +- [ ] I have the latest version of fzf +- [ ] I have searched through the existing issues + +## Info + +- OS + - [ ] Linux + - [ ] Mac OS X + - [ ] Windows + - [ ] Etc. +- Shell + - [ ] bash + - [ ] zsh + - [ ] fish + +## Problem / Steps to reproduce + diff --git a/fzf/fzf/.github/dependabot.yml b/fzf/fzf/.github/dependabot.yml new file mode 100644 index 0000000..f1b219b --- /dev/null +++ b/fzf/fzf/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" diff --git a/fzf/fzf/.github/workflows/codeql-analysis.yml b/fzf/fzf/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..2856960 --- /dev/null +++ b/fzf/fzf/.github/workflows/codeql-analysis.yml @@ -0,0 +1,37 @@ +# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning +name: CodeQL + +on: + push: + branches: [ master, devel ] + pull_request: + branches: [ master ] + workflow_dispatch: + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: ['go'] + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/fzf/fzf/.github/workflows/linux.yml b/fzf/fzf/.github/workflows/linux.yml new file mode 100644 index 0000000..3558a83 --- /dev/null +++ b/fzf/fzf/.github/workflows/linux.yml @@ -0,0 +1,45 @@ +--- +name: Test fzf on Linux + +on: + push: + branches: [ master, devel ] + pull_request: + branches: [ master ] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + go: [1.14, 1.16] + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go }} + + - name: Setup Ruby + uses: ruby/setup-ruby@v1.62.0 + with: + ruby-version: 3.0.0 + + - name: Install packages + run: sudo apt-get install --yes zsh fish tmux + + - name: Install Ruby gems + run: sudo gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1 + + - name: Rubocop + run: rubocop --require rubocop-minitest --require rubocop-performance + + - name: Unit test + run: make test + + - name: Integration test + run: make install && ./install --all && LC_ALL=C tmux new-session -d && ruby test/test_go.rb --verbose diff --git a/fzf/fzf/.github/workflows/macos.yml b/fzf/fzf/.github/workflows/macos.yml new file mode 100644 index 0000000..08b0fe2 --- /dev/null +++ b/fzf/fzf/.github/workflows/macos.yml @@ -0,0 +1,45 @@ +--- +name: Test fzf on macOS + +on: + push: + branches: [ master, devel ] + pull_request: + branches: [ master ] + workflow_dispatch: + +jobs: + build: + runs-on: macos-latest + strategy: + matrix: + go: [1.14, 1.16] + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go }} + + - name: Setup Ruby + uses: ruby/setup-ruby@v1.62.0 + with: + ruby-version: 3.0.0 + + - name: Install packages + run: HOMEBREW_NO_INSTALL_CLEANUP=1 brew install fish zsh tmux + + - name: Install Ruby gems + run: gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1 + + - name: Rubocop + run: rubocop --require rubocop-minitest --require rubocop-performance + + - name: Unit test + run: make test + + - name: Integration test + run: make install && ./install --all && LC_ALL=C tmux new-session -d && ruby test/test_go.rb --verbose diff --git a/fzf/fzf/.gitignore b/fzf/fzf/.gitignore new file mode 100644 index 0000000..8bde085 --- /dev/null +++ b/fzf/fzf/.gitignore @@ -0,0 +1,14 @@ +bin/fzf +bin/fzf.exe +dist +target +pkg +Gemfile.lock +.DS_Store +doc/tags +vendor +gopath +*.zwc +fzf +tmp +*.patch diff --git a/fzf/fzf/.goreleaser.yml b/fzf/fzf/.goreleaser.yml new file mode 100644 index 0000000..bab275d --- /dev/null +++ b/fzf/fzf/.goreleaser.yml @@ -0,0 +1,119 @@ +--- +project_name: fzf + +before: + hooks: + - go mod download + +builds: + - id: fzf-macos + binary: fzf + goos: + - darwin + goarch: + - amd64 + ldflags: + - "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}" + hooks: + post: | + sh -c ' + cat > /tmp/fzf-gon-amd64.hcl << EOF + source = ["./dist/fzf-macos_darwin_amd64/fzf"] + bundle_id = "kr.junegunn.fzf" + apple_id { + username = "junegunn.c@gmail.com" + password = "@env:AC_PASSWORD" + } + sign { + application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)" + } + zip { + output_path = "./dist/fzf-{{ .Version }}-darwin_amd64.zip" + } + EOF + gon /tmp/fzf-gon-amd64.hcl + ' + + - id: fzf-macos-arm + binary: fzf + goos: + - darwin + goarch: + - arm64 + ldflags: + - "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}" + hooks: + post: | + sh -c ' + cat > /tmp/fzf-gon-arm64.hcl << EOF + source = ["./dist/fzf-macos-arm_darwin_arm64/fzf"] + bundle_id = "kr.junegunn.fzf" + apple_id { + username = "junegunn.c@gmail.com" + password = "@env:AC_PASSWORD" + } + sign { + application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)" + } + zip { + output_path = "./dist/fzf-{{ .Version }}-darwin_arm64.zip" + } + EOF + gon /tmp/fzf-gon-arm64.hcl + ' + + - id: fzf + goos: + - linux + - windows + - freebsd + - openbsd + goarch: + - amd64 + - arm + - arm64 + goarm: + - 5 + - 6 + - 7 + ldflags: + - "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}" + ignore: + - goos: freebsd + goarch: arm + - goos: openbsd + goarch: arm + - goos: freebsd + goarch: arm64 + - goos: openbsd + goarch: arm64 + +archives: + - name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" + builds: + - fzf + format: tar.gz + format_overrides: + - goos: windows + format: zip + files: + - non-existent* + +release: + github: + owner: junegunn + name: fzf + prerelease: auto + name_template: '{{ .Tag }}' + extra_files: + - glob: ./dist/fzf-*darwin*.zip + +snapshot: + name_template: "{{ .Tag }}-devel" + +changelog: + sort: asc + filters: + exclude: + - README + - test diff --git a/fzf/fzf/.rubocop.yml b/fzf/fzf/.rubocop.yml new file mode 100644 index 0000000..c131deb --- /dev/null +++ b/fzf/fzf/.rubocop.yml @@ -0,0 +1,28 @@ +Layout/LineLength: + Enabled: false +Metrics: + Enabled: false +Lint/ShadowingOuterLocalVariable: + Enabled: false +Style/MethodCallWithArgsParentheses: + Enabled: true + IgnoredMethods: + - assert + - exit + - paste + - puts + - raise + - refute + - require + - send_keys + IgnoredPatterns: + - ^assert_ + - ^refute_ +Style/NumericPredicate: + Enabled: false +Style/StringConcatenation: + Enabled: false +Style/OptionalBooleanParameter: + Enabled: false +Style/WordArray: + MinSize: 1 diff --git a/fzf/fzf/ADVANCED.md b/fzf/fzf/ADVANCED.md new file mode 100644 index 0000000..111a1ae --- /dev/null +++ b/fzf/fzf/ADVANCED.md @@ -0,0 +1,569 @@ +Advanced fzf examples +====================== + +*(Last update: 2021/05/22)* + + + +* [Introduction](#introduction) +* [Screen Layout](#screen-layout) + * [`--height`](#--height) + * [`fzf-tmux`](#fzf-tmux) + * [Popup window support](#popup-window-support) +* [Dynamic reloading of the list](#dynamic-reloading-of-the-list) + * [Updating the list of processes by pressing CTRL-R](#updating-the-list-of-processes-by-pressing-ctrl-r) + * [Toggling between data sources](#toggling-between-data-sources) +* [Ripgrep integration](#ripgrep-integration) + * [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter) + * [Using fzf as interative Ripgrep launcher](#using-fzf-as-interative-ripgrep-launcher) + * [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode) +* [Log tailing](#log-tailing) +* [Key bindings for git objects](#key-bindings-for-git-objects) + * [Files listed in `git status`](#files-listed-in-git-status) + * [Branches](#branches) + * [Commit hashes](#commit-hashes) +* [Color themes](#color-themes) + * [Generating fzf color theme from Vim color schemes](#generating-fzf-color-theme-from-vim-color-schemes) + + + +Introduction +------------ + +fzf is an interactive [Unix filter][filter] program that is designed to be +used with other Unix tools. It reads a list of items from the standard input, +allows you to select a subset of the items, and prints the selected ones to +the standard output. You can think of it as an interactive version of *grep*, +and it's already useful even if you don't know any of its options. + +```sh +# 1. ps: Feed the list of processes to fzf +# 2. fzf: Interactively select a process using fuzzy matching algorithm +# 3. awk: Take the PID from the selected line +# 3. kill: Kill the process with the PID +ps -ef | fzf | awk '{print $2}' | xargs kill -9 +``` + +[filter]: https://en.wikipedia.org/wiki/Filter_(software) + +While the above example succinctly summarizes the fundamental concept of fzf, +you can build much more sophisticated interactive workflows using fzf once you +learn its wide variety of features. + +- To see the full list of options and features, see `man fzf` +- To see the latest additions, see [CHANGELOG.md](CHANGELOG.md) + +This document will guide you through some examples that will familiarize you +with the advanced features of fzf. + +Screen Layout +------------- + +### `--height` + +fzf by default opens in fullscreen mode, but it's not always desirable. +Oftentimes, you want to see the current context of the terminal while using +fzf. `--height` is an option for opening fzf below the cursor in +non-fullscreen mode so you can still see the previous commands and their +results above it. + +```sh +fzf --height=40% +``` + +![image](https://user-images.githubusercontent.com/700826/113379893-c184c680-93b5-11eb-9676-c7c0a2f01748.png) + +You might also want to experiment with other layout options such as +`--layout=reverse`, `--info=inline`, `--border`, `--margin`, etc. + +```sh +fzf --height=40% --layout=reverse +fzf --height=40% --layout=reverse --info=inline +fzf --height=40% --layout=reverse --info=inline --border +fzf --height=40% --layout=reverse --info=inline --border --margin=1 +fzf --height=40% --layout=reverse --info=inline --border --margin=1 --padding=1 +``` + +![image](https://user-images.githubusercontent.com/700826/113379932-dfeac200-93b5-11eb-9e28-df1a2ee71f90.png) + +*(See `Layout` section of the man page to see the full list of options)* + +But you definitely don't want to repeat `--height=40% --layout=reverse +--info=inline --border --margin=1 --padding=1` every time you use fzf. You +could write a wrapper script or shell alias, but there is an easier option. +Define `$FZF_DEFAULT_OPTS` like so: + +```sh +export FZF_DEFAULT_OPTS="--height=40% --layout=reverse --info=inline --border --margin=1 --padding=1" +``` + +### `fzf-tmux` + +Before fzf had `--height` option, we would open fzf in a tmux split pane not +to take up the whole screen. This is done using `fzf-tmux` script. + +```sh +# Open fzf on a tmux split pane below the current pane. +# Takes the same set of options. +fzf-tmux --layout=reverse +``` + +![image](https://user-images.githubusercontent.com/700826/113379973-f1cc6500-93b5-11eb-8860-c9bc4498aadf.png) + +The limitation of `fzf-tmux` is that it only works when you're on tmux unlike +`--height` option. But the advantage of it is that it's more flexible. +(See `man fzf-tmux` for available options.) + +```sh +# On the right (50%) +fzf-tmux -r + +# On the left (30%) +fzf-tmux -l30% + +# Above the cursor +fzf-tmux -u30% +``` + +![image](https://user-images.githubusercontent.com/700826/113379983-fa24a000-93b5-11eb-93eb-8a3d39b2f163.png) + +![image](https://user-images.githubusercontent.com/700826/113380001-0577cb80-93b6-11eb-95d0-2ba453866882.png) + +![image](https://user-images.githubusercontent.com/700826/113380040-1d4f4f80-93b6-11eb-9bef-737fb120aafe.png) + +#### Popup window support + +But here's the really cool part; tmux 3.2 added support for popup windows. So +you can open fzf in a popup window, which is quite useful if you frequently +use split panes. + +```sh +# Open tmux in a tmux popup window (default size: 50% of the screen) +fzf-tmux -p + +# 80% width, 60% height +fzf-tmux -p 80%,60% +``` + +![image](https://user-images.githubusercontent.com/700826/113380106-4a9bfd80-93b6-11eb-8cee-aeb1c4ce1a1f.png) + +> You might also want to check out my tmux plugins which support this popup +> window layout. +> +> - https://github.com/junegunn/tmux-fzf-url +> - https://github.com/junegunn/tmux-fzf-maccy + +Dynamic reloading of the list +----------------------------- + +fzf can dynamically update the candidate list using an arbitrary program with +`reload` bindings (The design document for `reload` can be found +[here][reload]). + +[reload]: https://github.com/junegunn/fzf/issues/1750 + +### Updating the list of processes by pressing CTRL-R + +This example shows how you can set up a binding for dynamically updating the +list without restarting fzf. + +```sh +(date; ps -ef) | + fzf --bind='ctrl-r:reload(date; ps -ef)' \ + --header=$'Press CTRL-R to reload\n\n' --header-lines=2 \ + --preview='echo {}' --preview-window=down,3,wrap \ + --layout=reverse --height=80% | awk '{print $2}' | xargs kill -9 +``` + +![image](https://user-images.githubusercontent.com/700826/113465047-200c7c00-946c-11eb-918c-268f37a900c8.png) + +- The initial command is `(date; ps -ef)`. It prints the current date and + time, and the list of the processes. +- With `--header` option, you can show any message as the fixed header. +- To disallow selecting the first two lines (`date` and `ps` header), we use + `--header-lines=2` option. +- `--bind='ctrl-r:reload(date; ps -ef)'` binds CTRL-R to `reload` action that + runs `date; ps -ef`, so we can update the list of the processes by pressing + CTRL-R. +- We use simple `echo {}` preview option, so we can see the entire line on the + preview window below even if it's too long + +### Toggling between data sources + +You're not limited to just one reload binding. Set up multiple bindings so +you can switch between data sources. + +```sh +find * | fzf --prompt 'All> ' \ + --header 'CTRL-D: Directories / CTRL-F: Files' \ + --bind 'ctrl-d:change-prompt(Directories> )+reload(find * -type d)' \ + --bind 'ctrl-f:change-prompt(Files> )+reload(find * -type f)' +``` + +![image](https://user-images.githubusercontent.com/700826/113465073-4af6d000-946c-11eb-858f-2372c0955f67.png) + +![image](https://user-images.githubusercontent.com/700826/113465072-46321c00-946c-11eb-9b6f-cda3951df579.png) + +Ripgrep integration +------------------- + +### Using fzf as the secondary filter + +* Requires [bat][bat] +* Requires [Ripgrep][rg] + +[bat]: https://github.com/sharkdp/bat +[rg]: https://github.com/BurntSushi/ripgrep + +fzf is pretty fast for filtering a list that you will rarely have to think +about its performance. But it is not the right tool for searching for text +inside many large files, and in that case you should definitely use something +like [Ripgrep][rg]. + +In the next example, Ripgrep is the primary filter that searches for the given +text in files, and fzf is used as the secondary fuzzy filter that adds +interactivity to the workflow. And we use [bat][bat] to show the matching line in +the preview window. + +This is a bash script and it will not run as expected on other non-compliant +shells. To avoid the compatibility issue, let's save this snippet as a script +file called `rfv`. + +```bash +#!/usr/bin/env bash + +# 1. Search for text in files using Ripgrep +# 2. Interactively narrow down the list using fzf +# 3. Open the file in Vim +IFS=: read -ra selected < <( + rg --color=always --line-number --no-heading --smart-case "${*:-}" | + fzf --ansi \ + --color "hl:-1:underline,hl+:-1:underline:reverse" \ + --delimiter : \ + --preview 'bat --color=always {1} --highlight-line {2}' \ + --preview-window 'up,60%,border-bottom,+{2}+3/3,~3' +) +[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}" +``` + +And run it with an initial query string. + +```sh +# Make the script executable +chmod +x rfv + +# Run it with the initial query "algo" +./rfv algo +``` + +> Ripgrep will perform the initial search and list all the lines that contain +`algo`. Then we further narrow down the list on fzf. + +![image](https://user-images.githubusercontent.com/700826/113683873-a42a6200-96ff-11eb-9666-26ce4091b0e4.png) + +I know it's a lot to digest, let's try to break down the code. + +- Ripgrep prints the matching lines in the following format + ``` + man/man1/fzf.1:54:.BI "--algo=" TYPE + man/man1/fzf.1:55:Fuzzy matching algorithm (default: v2) + man/man1/fzf.1:58:.BR v2 " Optimal scoring algorithm (quality)" + src/pattern_test.go:7: "github.com/junegunn/fzf/src/algo" + ``` + The first token delimited by `:` is the file path, and the second token is + the line number of the matching line. They respectively correspond to `{1}` + and `{2}` in the preview command. + - `--preview 'bat --color=always {1} --highlight-line {2}'` +- As we run `rg` with `--color=always` option, we should tell fzf to parse + ANSI color codes in the input by setting `--ansi`. +- We customize how fzf colors various text elements using `--color` option. + `-1` tells fzf to keep the original color from the input. See `man fzf` for + available color options. +- The value of `--preview-window` option consists of 5 components delimited + by `,` + 1. `up` — Position of the preview window + 1. `60%` — Size of the preview window + 1. `border-bottom` — Preview window border only on the bottom side + 1. `+{2}+3/3` — Scroll offset of the preview contents + 1. `~3` — Fixed header +- Let's break down the latter two. We want to display the bat output in the + preview window with a certain scroll offset so that the matching line is + positioned near the center of the preview window. + - `+{2}` — The base offset is extracted from the second token + - `+3` — We add 3 lines to the base offset to compensate for the header + part of `bat` output + - ``` + ───────┬────────────────────────────────────────────────────────── + │ File: CHANGELOG.md + ───────┼────────────────────────────────────────────────────────── + 1 │ CHANGELOG + 2 │ ========= + 3 │ + 4 │ 0.26.0 + 5 │ ------ + ``` + - `/3` adjusts the offset so that the matching line is shown at a third + position in the window + - `~3` makes the top three lines fixed header so that they are always + visible regardless of the scroll offset +- Once we selected a line, we open the file with `vim` (`vim + "${selected[0]}"`) and move the cursor to the line (`+${selected[1]}`). + +### Using fzf as interative Ripgrep launcher + +We have learned that we can bind `reload` action to a key (e.g. +`--bind=ctrl-r:execute(ps -ef)`). In the next example, we are going to **bind +`reload` action to `change` event** so that whenever the user *changes* the +query string on fzf, `reload` action is triggered. + +Here is a variation of the above `rfv` script. fzf will restart Ripgrep every +time the user updates the query string on fzf. Searching and filtering is +completely done by Ripgrep, and fzf merely provides the interactive interface. +So we lose the "fuzziness", but the performance will be better on larger +projects, and it will free up memory as you narrow down the results. + +```bash +#!/usr/bin/env bash + +# 1. Search for text in files using Ripgrep +# 2. Interactively restart Ripgrep with reload action +# 3. Open the file in Vim +RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " +INITIAL_QUERY="${*:-}" +IFS=: read -ra selected < <( + FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \ + fzf --ansi \ + --disabled --query "$INITIAL_QUERY" \ + --bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \ + --delimiter : \ + --preview 'bat --color=always {1} --highlight-line {2}' \ + --preview-window 'up,60%,border-bottom,+{2}+3/3,~3' +) +[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}" +``` + +![image](https://user-images.githubusercontent.com/700826/113684212-f9ff0a00-96ff-11eb-8737-7bb571d320cc.png) + +- Instead of starting fzf in `rg ... | fzf` form, we start fzf without an + explicit input, but with a custom `FZF_DEFAULT_COMMAND` variable. This way + fzf can kill the initial Ripgrep process it starts with the initial query. + Otherwise, the initial Ripgrep process will keep consuming system resources + even after `reload` is triggered. +- Filtering is no longer a responsibility of fzf; hence `--disabled` +- `{q}` in the reload command evaluates to the query string on fzf prompt. +- `sleep 0.1` in the reload command is for "debouncing". This small delay will + reduce the number of intermediate Ripgrep processes while we're typing in + a query. + +### Switching to fzf-only search mode + +*(Requires fzf 0.27.1 or above)* + +In the previous example, we lost fuzzy matching capability as we completely +delegated search functionality to Ripgrep. But we can dynamically switch to +fzf-only search mode by *"unbinding"* `reload` action from `change` event. + +```sh +#!/usr/bin/env bash + +# Two-phase filtering with Ripgrep and fzf +# +# 1. Search for text in files using Ripgrep +# 2. Interactively restart Ripgrep with reload action +# * Press alt-enter to switch to fzf-only filtering +# 3. Open the file in Vim +RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " +INITIAL_QUERY="${*:-}" +IFS=: read -ra selected < <( + FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \ + fzf --ansi \ + --color "hl:-1:underline,hl+:-1:underline:reverse" \ + --disabled --query "$INITIAL_QUERY" \ + --bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \ + --bind "alt-enter:unbind(change,alt-enter)+change-prompt(2. fzf> )+enable-search+clear-query" \ + --prompt '1. ripgrep> ' \ + --delimiter : \ + --preview 'bat --color=always {1} --highlight-line {2}' \ + --preview-window 'up,60%,border-bottom,+{2}+3/3,~3' +) +[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}" +``` + +* Phase 1. Filtering with Ripgrep +![image](https://user-images.githubusercontent.com/700826/119213880-735e8a80-bafd-11eb-8493-123e4be24fbc.png) +* Phase 2. Filtering with fzf +![image](https://user-images.githubusercontent.com/700826/119213887-7e191f80-bafd-11eb-98c9-71a1af9d7aab.png) + +- We added `--prompt` option to show that fzf is initially running in "Ripgrep + launcher mode". +- We added `alt-enter` binding that + 1. unbinds `change` event, so Ripgrep is no longer restarted on key press + 2. changes the prompt to `2. fzf>` + 3. enables search functionality of fzf + 4. clears the current query string that was used to start Ripgrep process + 5. and unbinds `alt-enter` itself as this is a one-off event +- We reverted `--color` option for customizing how the matching chunks are + displayed in the second phase + +Log tailing +----------- + +fzf can run long-running preview commands and render partial results before +completion. And when you specify `follow` flag in `--preview-window` option, +fzf will "`tail -f`" the result, automatically scrolling to the bottom. + +```bash +# With "follow", preview window will automatically scroll to the bottom. +# "\033[2J" is an ANSI escape sequence for clearing the screen. +# When fzf reads this code it clears the previous preview contents. +fzf --preview-window follow --preview 'for i in $(seq 100000); do + echo "$i" + sleep 0.01 + (( i % 300 == 0 )) && printf "\033[2J" +done' +``` + +![image](https://user-images.githubusercontent.com/700826/113473303-dd669600-94a3-11eb-88a9-1f61b996bb0e.png) + +Admittedly, that was a silly example. Here's a practical one for browsing +Kubernetes pods. + +```bash +pods() { + FZF_DEFAULT_COMMAND="kubectl get pods --all-namespaces" \ + fzf --info=inline --layout=reverse --header-lines=1 \ + --prompt "$(kubectl config current-context | sed 's/-context$//')> " \ + --header $'╱ Enter (kubectl exec) ╱ CTRL-O (open log in editor) ╱ CTRL-R (reload) ╱\n\n' \ + --bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \ + --bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \ + --bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \ + --bind 'ctrl-r:reload:$FZF_DEFAULT_COMMAND' \ + --preview-window up:follow \ + --preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@" +} +``` + +![image](https://user-images.githubusercontent.com/700826/113473547-1d7a4880-94a5-11eb-98ef-9aa6f0ed215a.png) + +- The preview window will *"log tail"* the pod + - Holding on to a large amount of log will consume a lot of memory. So we + limited the initial log amount with `--tail=10000`. +- `execute` bindings allow you to run any command without leaving fzf + - Press enter key on a pod to `kubectl exec` into it + - Press CTRL-O to open the log in your editor +- Press CTRL-R to reload the pod list +- Press CTRL-/ repeatedly to to rotate through a different sets of preview + window options + 1. `80%,border-bottom` + 1. `hidden` + 1. Empty string after `|` translates to the default options from `--preview-window` + +Key bindings for git objects +---------------------------- + +I have [blogged](https://junegunn.kr/2016/07/fzf-git) about my fzf+git key +bindings a few years ago. I'm going to show them here again, because they are +seriously useful. + +### Files listed in `git status` + +CTRL-GCTRL-F + +![image](https://user-images.githubusercontent.com/700826/113473779-a9d93b00-94a6-11eb-87b5-f62a8d0a0efc.png) + +### Branches + +CTRL-GCTRL-B + +![image](https://user-images.githubusercontent.com/700826/113473758-87dfb880-94a6-11eb-82f4-9218103f10bd.png) + +### Commit hashes + +CTRL-GCTRL-H + +![image](https://user-images.githubusercontent.com/700826/113473765-91692080-94a6-11eb-8d38-ed4d41f27ac1.png) + + +The full source code can be found [here](https://gist.github.com/junegunn/8b572b8d4b5eddd8b85e5f4d40f17236). + +Color themes +------------ + +You can customize how fzf colors the text elements with `--color` option. Here +are a few color themes. Note that you need a terminal emulator that can +display 24-bit colors. + +```sh +# junegunn/seoul256.vim (dark) +export FZF_DEFAULT_OPTS='--color=bg+:#3F3F3F,bg:#4B4B4B,border:#6B6B6B,spinner:#98BC99,hl:#719872,fg:#D9D9D9,header:#719872,info:#BDBB72,pointer:#E12672,marker:#E17899,fg+:#D9D9D9,preview-bg:#3F3F3F,prompt:#98BEDE,hl+:#98BC99' +``` + +![seoul256](https://user-images.githubusercontent.com/700826/113475011-2c192d80-94ae-11eb-9d17-1e5867bae01f.png) + +```sh +# junegunn/seoul256.vim (light) +export FZF_DEFAULT_OPTS='--color=bg+:#D9D9D9,bg:#E1E1E1,border:#C8C8C8,spinner:#719899,hl:#719872,fg:#616161,header:#719872,info:#727100,pointer:#E12672,marker:#E17899,fg+:#616161,preview-bg:#D9D9D9,prompt:#0099BD,hl+:#719899' +``` + +![seoul256-light](https://user-images.githubusercontent.com/700826/113475022-389d8600-94ae-11eb-905f-0939dd535837.png) + +```sh +# morhetz/gruvbox +export FZF_DEFAULT_OPTS='--color=bg+:#3c3836,bg:#32302f,spinner:#fb4934,hl:#928374,fg:#ebdbb2,header:#928374,info:#8ec07c,pointer:#fb4934,marker:#fb4934,fg+:#ebdbb2,prompt:#fb4934,hl+:#fb4934' +``` + +![gruvbox](https://user-images.githubusercontent.com/700826/113475042-494dfc00-94ae-11eb-9322-cd03a027305a.png) + +```sh +# arcticicestudio/nord-vim +export FZF_DEFAULT_OPTS='--color=bg+:#3B4252,bg:#2E3440,spinner:#81A1C1,hl:#616E88,fg:#D8DEE9,header:#616E88,info:#81A1C1,pointer:#81A1C1,marker:#81A1C1,fg+:#D8DEE9,prompt:#81A1C1,hl+:#81A1C1' +``` + +![nord](https://user-images.githubusercontent.com/700826/113475063-67b3f780-94ae-11eb-9b24-5f0d22b63399.png) + +```sh +# tomasr/molokai +export FZF_DEFAULT_OPTS='--color=bg+:#293739,bg:#1B1D1E,border:#808080,spinner:#E6DB74,hl:#7E8E91,fg:#F8F8F2,header:#7E8E91,info:#A6E22E,pointer:#A6E22E,marker:#F92672,fg+:#F8F8F2,prompt:#F92672,hl+:#F92672' +``` + +![molokai](https://user-images.githubusercontent.com/700826/113475085-8619f300-94ae-11eb-85e4-2766fc3246bf.png) + +### Generating fzf color theme from Vim color schemes + +The Vim plugin of fzf can generate `--color` option from the current color +scheme according to `g:fzf_colors` variable. You can find the detailed +explanation [here](https://github.com/junegunn/fzf/blob/master/README-VIM.md#explanation-of-gfzf_colors). + +Here is an example. Add this to your Vim configuration file. + +```vim +let g:fzf_colors = +\ { 'fg': ['fg', 'Normal'], + \ 'bg': ['bg', 'Normal'], + \ 'preview-bg': ['bg', 'NormalFloat'], + \ 'hl': ['fg', 'Comment'], + \ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'], + \ 'bg+': ['bg', 'CursorLine', 'CursorColumn'], + \ 'hl+': ['fg', 'Statement'], + \ 'info': ['fg', 'PreProc'], + \ 'border': ['fg', 'Ignore'], + \ 'prompt': ['fg', 'Conditional'], + \ 'pointer': ['fg', 'Exception'], + \ 'marker': ['fg', 'Keyword'], + \ 'spinner': ['fg', 'Label'], + \ 'header': ['fg', 'Comment'] } +``` + +Then you can see how the `--color` option is generated by printing the result +of `fzf#wrap()`. + +```vim +:echo fzf#wrap() +``` + +Use this command to append `export FZF_DEFAULT_OPTS="..."` line to the end of +the current file. + +```vim +:call append('$', printf('export FZF_DEFAULT_OPTS="%s"', matchstr(fzf#wrap().options, "--color[^']*"))) +``` diff --git a/fzf/fzf/BUILD.md b/fzf/fzf/BUILD.md new file mode 100644 index 0000000..8c318f4 --- /dev/null +++ b/fzf/fzf/BUILD.md @@ -0,0 +1,49 @@ +Building fzf +============ + +Build instructions +------------------ + +### Prerequisites + +- Go 1.13 or above + +### Using Makefile + +```sh +# Build fzf binary for your platform in target +make + +# Build fzf binary and copy it to bin directory +make install + +# Build fzf binaries and archives for all platforms using goreleaser +make build + +# Publish GitHub release +make release +``` + +> :warning: Makefile uses git commands to determine the version and the +> revision information for `fzf --version`. So if you're building fzf from an +> environment where its git information is not available, you have to manually +> set `$FZF_VERSION` and `$FZF_REVISION`. +> +> e.g. `FZF_VERSION=0.24.0 FZF_REVISION=tarball make` + +Third-party libraries used +-------------------------- + +- [mattn/go-runewidth](https://github.com/mattn/go-runewidth) + - Licensed under [MIT](http://mattn.mit-license.org) +- [mattn/go-shellwords](https://github.com/mattn/go-shellwords) + - Licensed under [MIT](http://mattn.mit-license.org) +- [mattn/go-isatty](https://github.com/mattn/go-isatty) + - Licensed under [MIT](http://mattn.mit-license.org) +- [tcell](https://github.com/gdamore/tcell) + - Licensed under [Apache License 2.0](https://github.com/gdamore/tcell/blob/master/LICENSE) + +License +------- + +[MIT](LICENSE) diff --git a/fzf/fzf/CHANGELOG.md b/fzf/fzf/CHANGELOG.md new file mode 100644 index 0000000..cbcbea9 --- /dev/null +++ b/fzf/fzf/CHANGELOG.md @@ -0,0 +1,1206 @@ +CHANGELOG +========= + +0.29.0 +------ +- Added `change-preview(...)` action to change the `--preview` command + - cf. `preview(...)` is a one-off action that doesn't change the default + preview command +- Added `change-preview-window(...)` action + - You can rotate through the different options separated by `|` + ```sh + fzf --preview 'cat {}' --preview-window right:40% \ + --bind 'ctrl-/:change-preview-window(right,70%|down,40%,border-top|hidden|)' + ``` +- Fixed rendering of the prompt line when overflow occurs with `--info=inline` + +0.28.0 +------ +- Added `--header-first` option to print header before the prompt line + ```sh + fzf --header $'Welcome to fzf\n▔▔▔▔▔▔▔▔▔▔▔▔▔▔' --reverse --height 30% --border --header-first + ``` +- Added `--scroll-off=LINES` option (similar to `scrolloff` option of Vim) + - You can set it to a very large number so that the cursor stays in the + middle of the screen while scrolling + ```sh + fzf --scroll-off=5 + fzf --scroll-off=999 + ``` +- Fixed bug where preview window is not updated on `reload` (#2644) +- fzf on Windows will also use `$SHELL` to execute external programs + - See #2638 and #2647 + - Thanks to @rashil2000, @vovcacik, and @janlazo + +0.27.3 +------ +- Preview window is `hidden` by default when there are `preview` bindings but + `--preview` command is not given +- Fixed bug where `{n}` is not properly reset on `reload` +- Fixed bug where spinner is not displayed on `reload` +- Enhancements in tcell renderer for Windows (#2616) +- Vim plugin + - `sinklist` is added as a synonym to `sink*` so that it's easier to add + a function to a spec dictionary + ```vim + let spec = { 'source': 'ls', 'options': ['--multi', '--preview', 'cat {}'] } + function spec.sinklist(matches) + echom string(a:matches) + endfunction + + call fzf#run(fzf#wrap(spec)) + ``` + - Vim 7 compatibility + +0.27.2 +------ +- 16 base ANSI colors can be specified by their names + ```sh + fzf --color fg:3,fg+:11 + fzf --color fg:yellow,fg+:bright-yellow + ``` +- Fix bug where `--read0` not properly displaying long lines + +0.27.1 +------ +- Added `unbind` action. In the following Ripgrep launcher example, you can + use `unbind(reload)` to switch to fzf-only filtering mode. + - See https://github.com/junegunn/fzf/blob/master/ADVANCED.md#switching-to-fzf-only-search-mode +- Vim plugin + - Vim plugin will stop immediately even when the source command hasn't finished + ```vim + " fzf will read the stream file while allowing other processes to append to it + call fzf#run({'source': 'cat /dev/null > /tmp/stream; tail -f /tmp/stream'}) + ``` + - It is now possible to open popup window relative to the current window + ```vim + let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true, 'yoffset': 1.0 } } + ``` + +0.27.0 +------ +- More border options for `--preview-window` + ```sh + fzf --preview 'cat {}' --preview-window border-left + fzf --preview 'cat {}' --preview-window border-left --border horizontal + fzf --preview 'cat {}' --preview-window top:border-bottom + fzf --preview 'cat {}' --preview-window top:border-horizontal + ``` +- Automatically set `/dev/tty` as STDIN on execute action + ```sh + # Redirect /dev/tty to suppress "Vim: Warning: Input is not from a terminal" + # ls | fzf --bind "enter:execute(vim {} < /dev/tty)" + + # "< /dev/tty" part is no longer needed + ls | fzf --bind "enter:execute(vim {})" + ``` +- Bug fixes and improvements +- Signed and notarized macOS binaries + (Huge thanks to [BACKERS.md](https://github.com/junegunn/junegunn/blob/main/BACKERS.md)!) + +0.26.0 +------ +- Added support for fixed header in preview window + ```sh + # Display top 3 lines as the fixed header + fzf --preview 'bat --style=header,grid --color=always {}' --preview-window '~3' + ``` +- More advanced preview offset expression to better support the fixed header + ```sh + # Preview with bat, matching line in the middle of the window below + # the fixed header of the top 3 lines + # + # ~3 Top 3 lines as the fixed header + # +{2} Base scroll offset extracted from the second field + # +3 Extra offset to compensate for the 3-line header + # /2 Put in the middle of the preview area + # + git grep --line-number '' | + fzf --delimiter : \ + --preview 'bat --style=full --color=always --highlight-line {2} {1}' \ + --preview-window '~3:+{2}+3/2' + ``` +- Added `select` and `deselect` action for unconditionally selecting or + deselecting a single item in `--multi` mode. Complements `toggle` action. +- Significant performance improvement in ANSI code processing +- Bug fixes and improvements +- Built with Go 1.16 + +0.25.1 +------ +- Added `close` action + - Close preview window if open, abort fzf otherwise +- Bug fixes and improvements + +0.25.0 +------ +- Text attributes set in `--color` are not reset when fzf sees another + `--color` option for the same element. This allows you to put custom text + attributes in your `$FZF_DEFAULT_OPTS` and still have those attributes + even when you override the colors. + + ```sh + # Default colors and attributes + fzf + + # Apply custom text attributes + export FZF_DEFAULT_OPTS='--color fg+:italic,hl:-1:underline,hl+:-1:reverse:underline' + + fzf + + # Different colors but you still have the attributes + fzf --color hl:176,hl+:177 + + # Write "regular" if you want to clear the attributes + fzf --color hl:176:regular,hl+:177:regular + ``` +- Renamed `--phony` to `--disabled` +- You can dynamically enable and disable the search functionality using the + new `enable-search`, `disable-search`, and `toggle-search` actions +- You can assign a different color to the query string for when search is disabled + ```sh + fzf --color query:#ffffff,disabled:#999999 --bind space:toggle-search + ``` +- Added `last` action to move the cursor to the last match + - The opposite action `top` is renamed to `first`, but `top` is still + recognized as a synonym for backward compatibility +- Added `preview-top` and `preview-bottom` actions +- Extended support for alt key chords: alt with any case-sensitive single character + ```sh + fzf --bind alt-,:first,alt-.:last + ``` + +0.24.4 +------ +- Added `--preview-window` option `follow` + ```sh + # Preview window will automatically scroll to the bottom + fzf --preview-window follow --preview 'for i in $(seq 100000); do + echo "$i" + sleep 0.01 + (( i % 300 == 0 )) && printf "\033[2J" + done' + ``` +- Added `change-prompt` action + ```sh + fzf --prompt 'foo> ' --bind $'a:change-prompt:\x1b[31mbar> ' + ``` +- Bug fixes and improvements + +0.24.3 +------ +- Added `--padding` option + ```sh + fzf --margin 5% --padding 5% --border --preview 'cat {}' \ + --color bg:#222222,preview-bg:#333333 + ``` + +0.24.2 +------ +- Bug fixes and improvements + +0.24.1 +------ +- Fixed broken `--color=[bw|no]` option + +0.24.0 +------ +- Real-time rendering of preview window + ```sh + # fzf can render preview window before the command completes + fzf --preview 'sleep 1; for i in $(seq 100); do echo $i; sleep 0.01; done' + + # Preview window can process ANSI escape sequence (CSI 2 J) for clearing the display + fzf --preview 'for i in $(seq 100000); do + (( i % 200 == 0 )) && printf "\033[2J" + echo "$i" + sleep 0.01 + done' + ``` +- Updated `--color` option to support text styles + - `regular` / `bold` / `dim` / `underline` / `italic` / `reverse` / `blink` + ```sh + # * Set -1 to keep the original color + # * Multiple style attributes can be combined + # * Italic style may not be supported by some terminals + rg --line-number --no-heading --color=always "" | + fzf --ansi --prompt "Rg: " \ + --color fg+:italic,hl:underline:-1,hl+:italic:underline:reverse:-1 \ + --color pointer:reverse,prompt:reverse,input:159 \ + --pointer ' ' + ``` +- More `--border` options + - `vertical`, `top`, `bottom`, `left`, `right` + - Updated Vim plugin to use these new `--border` options + ```vim + " Floating popup window in the center of the screen + let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } + + " Popup with 100% width + let g:fzf_layout = { 'window': { 'width': 1.0, 'height': 0.5, 'border': 'horizontal' } } + + " Popup with 100% height + let g:fzf_layout = { 'window': { 'width': 0.5, 'height': 1.0, 'border': 'vertical' } } + + " Similar to 'down' layout, but it uses a popup window and doesn't affect the window layout + let g:fzf_layout = { 'window': { 'width': 1.0, 'height': 0.5, 'yoffset': 1.0, 'border': 'top' } } + + " Opens on the right; + " 'highlight' option is still supported but it will only take the foreground color of the group + let g:fzf_layout = { 'window': { 'width': 0.5, 'height': 1.0, 'xoffset': 1.0, 'border': 'left', 'highlight': 'Comment' } } + ``` +- To indicate if `--multi` mode is enabled, fzf will print the number of + selected items even when no item is selected + ```sh + seq 100 | fzf + # 100/100 + seq 100 | fzf --multi + # 100/100 (0) + seq 100 | fzf --multi 5 + # 100/100 (0/5) + ``` +- Since 0.24.0, release binaries will be uploaded to https://github.com/junegunn/fzf/releases + +0.23.1 +------ +- Added `--preview-window` options for disabling flags + - `nocycle` + - `nohidden` + - `nowrap` + - `default` +- Built with Go 1.14.9 due to performance regression + - https://github.com/golang/go/issues/40727 + +0.23.0 +------ +- Support preview scroll offset relative to window height + ```sh + git grep --line-number '' | + fzf --delimiter : \ + --preview 'bat --style=numbers --color=always --highlight-line {2} {1}' \ + --preview-window +{2}-/2 + ``` +- Added `--preview-window` option for sharp edges (`--preview-window sharp`) +- Added `--preview-window` option for cyclic scrolling (`--preview-window cycle`) +- Reduced vertical padding around the preview window when `--preview-window + noborder` is used +- Added actions for preview window + - `preview-half-page-up` + - `preview-half-page-down` +- Vim + - Popup width and height can be given in absolute integer values + - Added `fzf#exec()` function for getting the path of fzf executable + - It also downloads the latest binary if it's not available by running + `./install --bin` +- Built with Go 1.15.2 + - We no longer provide 32-bit binaries + +0.22.0 +------ +- Added more options for `--bind` + - `backward-eof` event + ```sh + # Aborts when you delete backward when the query prompt is already empty + fzf --bind backward-eof:abort + ``` + - `refresh-preview` action + ```sh + # Rerun preview command when you hit '?' + fzf --preview 'echo $RANDOM' --bind '?:refresh-preview' + ``` + - `preview` action + ```sh + # Default preview command with an extra preview binding + fzf --preview 'file {}' --bind '?:preview:cat {}' + + # A preview binding with no default preview command + # (Preview window is initially empty) + fzf --bind '?:preview:cat {}' + + # Preview window hidden by default, it appears when you first hit '?' + fzf --bind '?:preview:cat {}' --preview-window hidden + ``` +- Added preview window option for setting the initial scroll offset + ```sh + # Initial scroll offset is set to the line number of each line of + # git grep output *minus* 5 lines + git grep --line-number '' | + fzf --delimiter : --preview 'nl {1}' --preview-window +{2}-5 + ``` +- Added support for ANSI colors in `--prompt` string +- Smart match of accented characters + - An unaccented character in the query string will match both accented and + unaccented characters, while an accented character will only match + accented characters. This is similar to how "smart-case" match works. +- Vim plugin + - `tmux` layout option for using fzf-tmux + ```vim + let g:fzf_layout = { 'tmux': '-p90%,60%' } + ``` + +0.21.1 +------ +- Shell extension + - CTRL-R will remove duplicate commands +- fzf-tmux + - Supports tmux popup window (require tmux 3.2 or above) + - ```sh + # 50% width and height + fzf-tmux -p + + # 80% width and height + fzf-tmux -p 80% + + # 80% width and 40% height + fzf-tmux -p 80%,40% + fzf-tmux -w 80% -h 40% + + # Window position + fzf-tmux -w 80% -h 40% -x 0 -y 0 + fzf-tmux -w 80% -h 40% -y 1000 + + # Write ordinary fzf options after -- + fzf-tmux -p -- --reverse --info=inline --margin 2,4 --border + ``` + - On macOS, you can build the latest tmux from the source with + `brew install tmux --HEAD` +- Bug fixes + - Fixed Windows file traversal not to include directories + - Fixed ANSI colors with `--keep-right` + - Fixed _fzf_complete for zsh +- Built with Go 1.14.1 + +0.21.0 +------ +- `--height` option is now available on Windows as well (@kelleyma49) +- Added `--pointer` and `--marker` options +- Added `--keep-right` option that keeps the right end of the line visible + when it's too long +- Style changes + - `--border` will now print border with rounded corners around the + finder instead of printing horizontal lines above and below it. + The previous style is available via `--border=horizontal` + - Unicode spinner +- More keys and actions for `--bind` +- Added PowerShell script for downloading Windows binary +- Vim plugin: Built-in floating windows support + ```vim + let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } + ``` +- bash: Various improvements in key bindings (CTRL-T, CTRL-R, ALT-C) + - CTRL-R will start with the current command-line as the initial query + - CTRL-R properly supports multi-line commands +- Fuzzy completion API changed + ```sh + # Previous: fzf arguments given as a single string argument + # - This style is still supported, but it's deprecated + _fzf_complete "--multi --reverse --prompt=\"doge> \"" "$@" < <( + echo foo + ) + + # New API: multiple fzf arguments before "--" + # - Easier to write multiple options + _fzf_complete --multi --reverse --prompt="doge> " -- "$@" < <( + echo foo + ) + ``` +- Bug fixes and improvements + +0.20.0 +------ +- Customizable preview window color (`preview-fg` and `preview-bg` for `--color`) + ```sh + fzf --preview 'cat {}' \ + --color 'fg:#bbccdd,fg+:#ddeeff,bg:#334455,preview-bg:#223344,border:#778899' \ + --border --height 20 --layout reverse --info inline + ``` +- Removed the immediate flicking of the screen on `reload` action. + ```sh + : | fzf --bind 'change:reload:seq {q}' --phony + ``` +- Added `clear-query` and `clear-selection` actions for `--bind` +- It is now possible to split a composite bind action over multiple `--bind` + expressions by prefixing the later ones with `+`. + ```sh + fzf --bind 'ctrl-a:up+up' + + # Can be now written as + fzf --bind 'ctrl-a:up' --bind 'ctrl-a:+up' + + # This is useful when you need to write special execute/reload form (i.e. `execute:...`) + # to avoid parse errors and add more actions to the same key + fzf --multi --bind 'ctrl-l:select-all+execute:less {+f}' --bind 'ctrl-l:+deselect-all' + ``` +- Fixed parse error of `--bind` expression where concatenated execute/reload + action contains `+` character. + ```sh + fzf --multi --bind 'ctrl-l:select-all+execute(less {+f})+deselect-all' + ``` +- Fixed bugs of reload action + - Not triggered when there's no match even when the command doesn't have + any placeholder expressions + - Screen not properly cleared when `--header-lines` not filled on reload + +0.19.0 +------ + +- Added `--phony` option which completely disables search functionality. + Useful when you want to use fzf only as a selector interface. See below. +- Added "reload" action for dynamically updating the input list without + restarting fzf. See https://github.com/junegunn/fzf/issues/1750 to learn + more about it. + ```sh + # Using fzf as the selector interface for ripgrep + RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " + INITIAL_QUERY="foo" + FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY' || true" \ + fzf --bind "change:reload:$RG_PREFIX {q} || true" \ + --ansi --phony --query "$INITIAL_QUERY" + ``` +- `--multi` now takes an optional integer argument which indicates the maximum + number of items that can be selected + ```sh + seq 100 | fzf --multi 3 --reverse --height 50% + ``` +- If a placeholder expression for `--preview` and `execute` action (and the + new `reload` action) contains `f` flag, it is replaced to the + path of a temporary file that holds the evaluated list. This is useful + when you multi-select a large number of items and the length of the + evaluated string may exceed [`ARG_MAX`][argmax]. + ```sh + # Press CTRL-A to select 100K items and see the sum of all the numbers + seq 100000 | fzf --multi --bind ctrl-a:select-all \ + --preview "awk '{sum+=\$1} END {print sum}' {+f}" + ``` +- `deselect-all` no longer deselects unmatched items. It is now consistent + with `select-all` and `toggle-all` in that it only affects matched items. +- Due to the limitation of bash, fuzzy completion is enabled by default for + a fixed set of commands. A helper function for easily setting up fuzzy + completion for any command is now provided. + ```sh + # usage: _fzf_setup_completion path|dir COMMANDS... + _fzf_setup_completion path git kubectl + ``` +- Info line style can be changed by `--info=STYLE` + - `--info=default` + - `--info=inline` (same as old `--inline-info`) + - `--info=hidden` +- Preview window border can be disabled by adding `noborder` to + `--preview-window`. +- When you transform the input with `--with-nth`, the trailing white spaces + are removed. +- `ctrl-\`, `ctrl-]`, `ctrl-^`, and `ctrl-/` can now be used with `--bind` +- See https://github.com/junegunn/fzf/milestone/15?closed=1 for more details + +[argmax]: https://unix.stackexchange.com/questions/120642/what-defines-the-maximum-size-for-a-command-single-argument + +0.18.0 +------ + +- Added placeholder expression for zero-based item index: `{n}` and `{+n}` + - `fzf --preview 'echo {n}: {}'` +- Added color option for the gutter: `--color gutter:-1` +- Added `--no-unicode` option for drawing borders in non-Unicode, ASCII + characters +- `FZF_PREVIEW_LINES` and `FZF_PREVIEW_COLUMNS` are exported to preview process + - fzf still overrides `LINES` and `COLUMNS` as before, but they may be + reset by the default shell. +- Bug fixes and improvements + - See https://github.com/junegunn/fzf/milestone/14?closed=1 +- Built with Go 1.12.1 + +0.17.5 +------ + +- Bug fixes and improvements + - See https://github.com/junegunn/fzf/milestone/13?closed=1 +- Search query longer than the screen width is allowed (up to 300 chars) +- Built with Go 1.11.1 + +0.17.4 +------ + +- Added `--layout` option with a new layout called `reverse-list`. + - `--layout=reverse` is a synonym for `--reverse` + - `--layout=default` is a synonym for `--no-reverse` +- Preview window will be updated even when there is no match for the query + if any of the placeholder expressions (e.g. `{q}`, `{+}`) evaluates to + a non-empty string. +- More keys for binding: `shift-{up,down}`, `alt-{up,down,left,right}` +- fzf can now start even when `/dev/tty` is not available by making an + educated guess. +- Updated the default command for Windows. +- Fixes and improvements on bash/zsh completion +- install and uninstall scripts now supports generating files under + `XDG_CONFIG_HOME` on `--xdg` flag. + +See https://github.com/junegunn/fzf/milestone/12?closed=1 for the full list of +changes. + +0.17.3 +------ +- `$LINES` and `$COLUMNS` are exported to preview command so that the command + knows the exact size of the preview window. +- Better error messages when the default command or `$FZF_DEFAULT_COMMAND` + fails. +- Reverted #1061 to avoid having duplicate entries in the list when find + command detected a file system loop (#1120). The default command now + requires that find supports `-fstype` option. +- fzf now distinguishes mouse left click and right click (#1130) + - Right click is now bound to `toggle` action by default + - `--bind` understands `left-click` and `right-click` +- Added `replace-query` action (#1137) + - Replaces query string with the current selection +- Added `accept-non-empty` action (#1162) + - Same as accept, except that it prevents fzf from exiting without any + selection + +0.17.1 +------ + +- Fixed custom background color of preview window (#1046) +- Fixed background color issues of Windows binary +- Fixed Windows binary to execute command using cmd.exe with no parsing and + escaping (#1072) +- Added support for `window` layout on Vim 8 using Vim 8 terminal (#1055) + +0.17.0-2 +-------- + +A maintenance release for auxiliary scripts. fzf binaries are not updated. + +- Experimental support for the builtin terminal of Vim 8 + - fzf can now run inside GVim +- Updated Vim plugin to better handle `&shell` issue on fish +- Fixed a bug of fzf-tmux where invalid output is generated +- Fixed fzf-tmux to work even when `tput` does not work + +0.17.0 +------ +- Performance optimization +- One can match literal spaces in extended-search mode with a space prepended + by a backslash. +- `--expect` is now additive and can be specified multiple times. + +0.16.11 +------- +- Performance optimization +- Fixed missing preview update + +0.16.10 +------- +- Fixed invalid handling of ANSI colors in preview window +- Further improved `--ansi` performance + +0.16.9 +------ +- Memory and performance optimization + - Around 20% performance improvement for general use cases + - Up to 5x faster processing of `--ansi` + - Up to 50% reduction of memory usage +- Bug fixes and usability improvements + - Fixed handling of bracketed paste mode + - [ERROR] on info line when the default command failed + - More efficient rendering of preview window + - `--no-clear` updated for repetitive relaunching scenarios + +0.16.8 +------ +- New `change` event and `top` action for `--bind` + - `fzf --bind change:top` + - Move cursor to the top result whenever the query string is changed + - `fzf --bind 'ctrl-w:unix-word-rubout+top,ctrl-u:unix-line-discard+top'` + - `top` combined with `unix-word-rubout` and `unix-line-discard` +- Fixed inconsistent tiebreak scores when `--nth` is used +- Proper display of tab characters in `--prompt` +- Fixed not to `--cycle` on page-up/page-down to prevent overshoot +- Git revision in `--version` output +- Basic support for Cygwin environment +- Many fixes in Vim plugin on Windows/Cygwin (thanks to @janlazo) + +0.16.7 +------ +- Added support for `ctrl-alt-[a-z]` key chords +- CTRL-Z (SIGSTOP) now works with fzf +- fzf will export `$FZF_PREVIEW_WINDOW` so that the scripts can use it +- Bug fixes and improvements in Vim plugin and shell extensions + +0.16.6 +------ +- Minor bug fixes and improvements +- Added `--no-clear` option for scripting purposes + +0.16.5 +------ +- Minor bug fixes +- Added `toggle-preview-wrap` action +- Built with Go 1.8 + +0.16.4 +------ +- Added `--border` option to draw border above and below the finder +- Bug fixes and improvements + +0.16.3 +------ +- Fixed a bug where fzf incorrectly display the lines when straddling tab + characters are trimmed +- Placeholder expression used in `--preview` and `execute` action can + optionally take `+` flag to be used with multiple selections + - e.g. `git log --oneline | fzf --multi --preview 'git show {+1}'` +- Added `execute-silent` action for executing a command silently without + switching to the alternate screen. This is useful when the process is + short-lived and you're not interested in its output. + - e.g. `fzf --bind 'ctrl-y:execute!(echo -n {} | pbcopy)'` +- `ctrl-space` is allowed in `--bind` + +0.16.2 +------ +- Dropped ncurses dependency +- Binaries for freebsd, openbsd, arm5, arm6, arm7, and arm8 +- Official 24-bit color support +- Added support for composite actions in `--bind`. Multiple actions can be + chained using `+` separator. + - e.g. `fzf --bind 'ctrl-y:execute(echo -n {} | pbcopy)+abort'` +- `--preview-window` with size 0 is allowed. This is used to make fzf execute + preview command in the background without displaying the result. +- Minor bug fixes and improvements + +0.16.1 +------ +- Fixed `--height` option to properly fill the window with the background + color +- Added `half-page-up` and `half-page-down` actions +- Added `-L` flag to the default find command + +0.16.0 +------ +- *Added `--height HEIGHT[%]` option* + - fzf can now display finder without occupying the full screen +- Preview window will truncate long lines by default. Line wrap can be enabled + by `:wrap` flag in `--preview-window`. +- Latin script letters will be normalized before matching so that it's easier + to match against accented letters. e.g. `sodanco` can match `Só Danço Samba`. + - Normalization can be disabled via `--literal` +- Added `--filepath-word` to make word-wise movements/actions (`alt-b`, + `alt-f`, `alt-bs`, `alt-d`) respect path separators + +0.15.9 +------ +- Fixed rendering glitches introduced in 0.15.8 +- The default escape delay is reduced to 50ms and is configurable via + `$ESCDELAY` +- Scroll indicator at the top-right corner of the preview window is always + displayed when there's overflow +- Can now be built with ncurses 6 or tcell to support extra features + - *ncurses 6* + - Supports more than 256 color pairs + - Supports italics + - *tcell* + - 24-bit color support + - See https://github.com/junegunn/fzf/blob/master/BUILD.md + +0.15.8 +------ +- Updated ANSI processor to handle more VT-100 escape sequences +- Added `--no-bold` (and `--bold`) option +- Improved escape sequence processing for WSL +- Added support for `alt-[0-9]`, `f11`, and `f12` for `--bind` and `--expect` + +0.15.7 +------ +- Fixed panic when color is disabled and header lines contain ANSI colors + +0.15.6 +------ +- Windows binaries! (@kelleyma49) +- Fixed the bug where header lines are cleared when preview window is toggled +- Fixed not to display ^N and ^O on screen +- Fixed cursor keys (or any key sequence that starts with ESC) on WSL by + making fzf wait for additional keystrokes after ESC for up to 100ms + +0.15.5 +------ +- Setting foreground color will no longer set background color to black + - e.g. `fzf --color fg:153` +- `--tiebreak=end` will consider relative position instead of absolute distance +- Updated `fzf#wrap` function to respect `g:fzf_colors` + +0.15.4 +------ +- Added support for range expression in preview and execute action + - e.g. `ls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1` + - `{q}` will be replaced to the single-quoted string of the current query +- Fixed to properly handle unicode whitespace characters +- Display scroll indicator in preview window +- Inverse search term will use exact matcher by default + - This is a breaking change, but I believe it makes much more sense. It is + almost impossible to predict which entries will be filtered out due to + a fuzzy inverse term. You can still perform inverse-fuzzy-match by + prepending `!'` to the term. + +0.15.3 +------ +- Added support for more ANSI attributes: dim, underline, blink, and reverse +- Fixed race condition in `toggle-preview` + +0.15.2 +------ +- Preview window is now scrollable + - With mouse scroll or with bindable actions + - `preview-up` + - `preview-down` + - `preview-page-up` + - `preview-page-down` +- Updated ANSI processor to support high intensity colors and ignore + some VT100-related escape sequences + +0.15.1 +------ +- Fixed panic when the pattern occurs after 2^15-th column +- Fixed rendering delay when displaying extremely long lines + +0.15.0 +------ +- Improved fuzzy search algorithm + - Added `--algo=[v1|v2]` option so one can still choose the old algorithm + which values the search performance over the quality of the result +- Advanced scoring criteria +- `--read0` to read input delimited by ASCII NUL character +- `--print0` to print output delimited by ASCII NUL character + +0.13.5 +------ +- Memory and performance optimization + - Up to 2x performance with half the amount of memory + +0.13.4 +------ +- Performance optimization + - Memory footprint for ascii string is reduced by 60% + - 15 to 20% improvement of query performance + - Up to 45% better performance of `--nth` with non-regex delimiters +- Fixed invalid handling of `hidden` property of `--preview-window` + +0.13.3 +------ +- Fixed duplicate rendering of the last line in preview window + +0.13.2 +------ +- Fixed race condition where preview window is not properly cleared + +0.13.1 +------ +- Fixed UI issue with large `--preview` output with many ANSI codes + +0.13.0 +------ +- Added preview feature + - `--preview CMD` + - `--preview-window POS[:SIZE][:hidden]` +- `{}` in execute action is now replaced to the single-quoted (instead of + double-quoted) string of the current line +- Fixed to ignore control characters for bracketed paste mode + +0.12.2 +------ + +- 256-color capability detection does not require `256` in `$TERM` +- Added `print-query` action +- More named keys for binding; F1 ~ F10, + ALT-/, ALT-space, and ALT-enter +- Added `jump` and `jump-accept` actions that implement [EasyMotion][em]-like + movement + ![][jump] + +[em]: https://github.com/easymotion/vim-easymotion +[jump]: https://cloud.githubusercontent.com/assets/700826/15367574/b3999dc4-1d64-11e6-85da-28ceeb1a9bc2.png + +0.12.1 +------ + +- Ranking algorithm introduced in 0.12.0 is now universally applied +- Fixed invalid cache reference in exact mode +- Fixes and improvements in Vim plugin and shell extensions + +0.12.0 +------ + +- Enhanced ranking algorithm +- Minor bug fixes + +0.11.4 +------ + +- Added `--hscroll-off=COL` option (default: 10) (#513) +- Some fixes in Vim plugin and shell extensions + +0.11.3 +------ + +- Graceful exit on SIGTERM (#482) +- `$SHELL` instead of `sh` for `execute` action and `$FZF_DEFAULT_COMMAND` (#481) +- Changes in fuzzy completion API + - [`_fzf_compgen_{path,dir}`](https://github.com/junegunn/fzf/commit/9617647) + - [`_fzf_complete_COMMAND_post`](https://github.com/junegunn/fzf/commit/8206746) + for post-processing + +0.11.2 +------ + +- `--tiebreak` now accepts comma-separated list of sort criteria + - Each criterion should appear only once in the list + - `index` is only allowed at the end of the list + - `index` is implicitly appended to the list when not specified + - Default is `length` (or equivalently `length,index`) +- `begin` criterion will ignore leading whitespaces when calculating the index +- Added `toggle-in` and `toggle-out` actions + - Switch direction depending on `--reverse`-ness + - `export FZF_DEFAULT_OPTS="--bind tab:toggle-out,shift-tab:toggle-in"` +- Reduced the initial delay when `--tac` is not given + - fzf defers the initial rendering of the screen up to 100ms if the input + stream is ongoing to prevent unnecessary redraw during the initial + phase. However, 100ms delay is quite noticeable and might give the + impression that fzf is not snappy enough. This commit reduces the + maximum delay down to 20ms when `--tac` is not specified, in which case + the input list quickly fills the entire screen. + +0.11.1 +------ + +- Added `--tabstop=SPACES` option + +0.11.0 +------ + +- Added OR operator for extended-search mode +- Added `--execute-multi` action +- Fixed incorrect cursor position when unicode wide characters are used in + `--prompt` +- Fixes and improvements in shell extensions + +0.10.9 +------ + +- Extended-search mode is now enabled by default + - `--extended-exact` is deprecated and instead we have `--exact` for + orthogonally controlling "exactness" of search +- Fixed not to display non-printable characters +- Added `double-click` for `--bind` option +- More robust handling of SIGWINCH + +0.10.8 +------ + +- Fixed panic when trying to set colors after colors are disabled (#370) + +0.10.7 +------ + +- Fixed unserialized interrupt handling during execute action which often + caused invalid memory access and crash +- Changed `--tiebreak=length` (default) to use trimmed length when `--nth` is + used + +0.10.6 +------ + +- Replaced `--header-file` with `--header` option +- `--header` and `--header-lines` can be used together +- Changed exit status + - 0: Okay + - 1: No match + - 2: Error + - 130: Interrupted +- 64-bit linux binary is statically-linked with ncurses to avoid + compatibility issues. + +0.10.5 +------ + +- `'`-prefix to unquote the term in `--extended-exact` mode +- Backward scan when `--tiebreak=end` is set + +0.10.4 +------ + +- Fixed to remove ANSI code from output when `--with-nth` is set + +0.10.3 +------ + +- Fixed slow performance of `--with-nth` when used with `--delimiter` + - Regular expression engine of Golang as of now is very slow, so the fixed + version will treat the given delimiter pattern as a plain string instead + of a regular expression unless it contains special characters and is + a valid regular expression. + - Simpler regular expression for delimiter for better performance + +0.10.2 +------ + +### Fixes and improvements + +- Improvement in perceived response time of queries + - Eager, efficient rune array conversion +- Graceful exit when failed to initialize ncurses (invalid $TERM) +- Improved ranking algorithm when `--nth` option is set +- Changed the default command not to fail when there are files whose names + start with dash + +0.10.1 +------ + +### New features + +- Added `--margin` option +- Added options for sticky header + - `--header-file` + - `--header-lines` +- Added `cancel` action which clears the input or closes the finder when the + input is already empty + - e.g. `export FZF_DEFAULT_OPTS="--bind esc:cancel"` +- Added `delete-char/eof` action to differentiate `CTRL-D` and `DEL` + +### Minor improvements/fixes + +- Fixed to allow binding colon and comma keys +- Fixed ANSI processor to handle color regions spanning multiple lines + +0.10.0 +------ + +### New features + +- More actions for `--bind` + - `select-all` + - `deselect-all` + - `toggle-all` + - `ignore` +- `execute(...)` action for running arbitrary command without leaving fzf + - `fzf --bind "ctrl-m:execute(less {})"` + - `fzf --bind "ctrl-t:execute(tmux new-window -d 'vim {}')"` + - If the command contains parentheses, use any of the follows alternative + notations to avoid parse errors + - `execute[...]` + - `execute~...~` + - `execute!...!` + - `execute@...@` + - `execute#...#` + - `execute$...$` + - `execute%...%` + - `execute^...^` + - `execute&...&` + - `execute*...*` + - `execute;...;` + - `execute/.../` + - `execute|...|` + - `execute:...` + - This is the special form that frees you from parse errors as it + does not expect the closing character + - The catch is that it should be the last one in the + comma-separated list +- Added support for optional search history + - `--history HISTORY_FILE` + - When used, `CTRL-N` and `CTRL-P` are automatically remapped to + `next-history` and `previous-history` + - `--history-size MAX_ENTRIES` (default: 1000) +- Cyclic scrolling can be enabled with `--cycle` +- Fixed the bug where the spinner was not spinning on idle input stream + - e.g. `sleep 100 | fzf` + +### Minor improvements/fixes + +- Added synonyms for key names that can be specified for `--bind`, + `--toggle-sort`, and `--expect` +- Fixed the color of multi-select marker on the current line +- Fixed to allow `^pattern$` in extended-search mode + + +0.9.13 +------ + +### New features + +- Color customization with the extended `--color` option + +### Bug fixes + +- Fixed premature termination of Reader in the presence of a long line which + is longer than 64KB + +0.9.12 +------ + +### New features + +- Added `--bind` option for custom key bindings + +### Bug fixes + +- Fixed to update "inline-info" immediately after terminal resize +- Fixed ANSI code offset calculation + +0.9.11 +------ + +### New features + +- Added `--inline-info` option for saving screen estate (#202) + - Useful inside Neovim + - e.g. `let $FZF_DEFAULT_OPTS = $FZF_DEFAULT_OPTS.' --inline-info'` + +### Bug fixes + +- Invalid mutation of input on case conversion (#209) +- Smart-case for each term in extended-search mode (#208) +- Fixed double-click result when scroll offset is positive + +0.9.10 +------ + +### Improvements + +- Performance optimization +- Less aggressive memoization to limit memory usage + +### New features + +- Added color scheme for light background: `--color=light` + +0.9.9 +----- + +### New features + +- Added `--tiebreak` option (#191) +- Added `--no-hscroll` option (#193) +- Visual indication of `--toggle-sort` (#194) + +0.9.8 +----- + +### Bug fixes + +- Fixed Unicode case handling (#186) +- Fixed to terminate on RuneError (#185) + +0.9.7 +----- + +### New features + +- Added `--toggle-sort` option (#173) + - `--toggle-sort=ctrl-r` is applied to `CTRL-R` shell extension + +### Bug fixes + +- Fixed to print empty line if `--expect` is set and fzf is completed by + `--select-1` or `--exit-0` (#172) +- Fixed to allow comma character as an argument to `--expect` option + +0.9.6 +----- + +### New features + +#### Added `--expect` option (#163) + +If you provide a comma-separated list of keys with `--expect` option, fzf will +allow you to select the match and complete the finder when any of the keys is +pressed. Additionally, fzf will print the name of the key pressed as the first +line of the output so that your script can decide what to do next based on the +information. + +```sh +fzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@ +``` + +The updated vim plugin uses this option to implement +[ctrlp](https://github.com/kien/ctrlp.vim)-compatible key bindings. + +### Bug fixes + +- Fixed to ignore ANSI escape code `\e[K` (#162) + +0.9.5 +----- + +### New features + +#### Added `--ansi` option (#150) + +If you give `--ansi` option to fzf, fzf will interpret ANSI color codes from +the input, display the item with the ANSI colors (true colors are not +supported), and strips the codes from the output. This option is off by +default as it entails some overhead. + +### Improvements + +#### Reduced initial memory footprint (#151) + +By removing unnecessary copy of pointers, fzf will use significantly smaller +amount of memory when it's started. The difference is hugely noticeable when +the input is extremely large. (e.g. `locate / | fzf`) + +### Bug fixes + +- Fixed panic on `--no-sort --filter ''` (#149) + +0.9.4 +----- + +### New features + +#### Added `--tac` option to reverse the order of the input. + +One might argue that this option is unnecessary since we can already put `tac` +or `tail -r` in the command pipeline to achieve the same result. However, the +advantage of `--tac` is that it does not block until the input is complete. + +### *Backward incompatible changes* + +#### Changed behavior on `--no-sort` + +`--no-sort` option will no longer reverse the display order within finder. You +may want to use the new `--tac` option with `--no-sort`. + +``` +history | fzf +s --tac +``` + +### Improvements + +#### `--filter` will not block when sort is disabled + +When fzf works in filtering mode (`--filter`) and sort is disabled +(`--no-sort`), there's no need to block until input is complete. The new +version of fzf will print the matches on-the-fly when the following condition +is met: + + --filter TERM --no-sort [--no-tac --no-sync] + +or simply: + + -f TERM +s + +This change removes unnecessary delay in the use cases like the following: + + fzf -f xxx +s | head -5 + +However, in this case, fzf processes the lines sequentially, so it cannot +utilize multiple cores, and fzf will run slightly slower than the previous +mode of execution where filtering is done in parallel after the entire input +is loaded. If the user is concerned about this performance problem, one can +add `--sync` option to re-enable buffering. + +0.9.3 +----- + +### New features +- Added `--sync` option for multi-staged filtering + +### Improvements +- `--select-1` and `--exit-0` will start finder immediately when the condition + cannot be met diff --git a/fzf/fzf/Dockerfile b/fzf/fzf/Dockerfile new file mode 100644 index 0000000..9cccaaa --- /dev/null +++ b/fzf/fzf/Dockerfile @@ -0,0 +1,11 @@ +FROM archlinux +RUN pacman -Sy && pacman --noconfirm -S awk git tmux zsh fish ruby procps go make gcc +RUN gem install --no-document -v 5.14.2 minitest +RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc +RUN echo '. ~/.bashrc' >> ~/.bash_profile + +# Do not set default PS1 +RUN rm -f /etc/bash.bashrc +COPY . /fzf +RUN cd /fzf && make install && ./install --all +CMD tmux new 'set -o pipefail; ruby /fzf/test/test_go.rb | tee out && touch ok' && cat out && [ -e ok ] diff --git a/fzf/fzf/LICENSE b/fzf/fzf/LICENSE new file mode 100644 index 0000000..50aa5d9 --- /dev/null +++ b/fzf/fzf/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013-2021 Junegunn Choi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/fzf/fzf/Makefile b/fzf/fzf/Makefile new file mode 100644 index 0000000..91c5711 --- /dev/null +++ b/fzf/fzf/Makefile @@ -0,0 +1,166 @@ +SHELL := bash +GO ?= go +GOOS ?= $(word 1, $(subst /, " ", $(word 4, $(shell go version)))) + +MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST))) +ROOT_DIR := $(shell dirname $(MAKEFILE)) +SOURCES := $(wildcard *.go src/*.go src/*/*.go) $(MAKEFILE) + +ifdef FZF_VERSION +VERSION := $(FZF_VERSION) +else +VERSION := $(shell git describe --abbrev=0 2> /dev/null) +endif +ifeq ($(VERSION),) +$(error Not on git repository; cannot determine $$FZF_VERSION) +endif +VERSION_TRIM := $(shell sed "s/-.*//" <<< $(VERSION)) +VERSION_REGEX := $(subst .,\.,$(VERSION_TRIM)) + +ifdef FZF_REVISION +REVISION := $(FZF_REVISION) +else +REVISION := $(shell git log -n 1 --pretty=format:%h -- $(SOURCES) 2> /dev/null) +endif +ifeq ($(REVISION),) +$(error Not on git repository; cannot determine $$FZF_REVISION) +endif +BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision=$(REVISION)" -tags "$(TAGS)" + +BINARY32 := fzf-$(GOOS)_386 +BINARY64 := fzf-$(GOOS)_amd64 +BINARYARM5 := fzf-$(GOOS)_arm5 +BINARYARM6 := fzf-$(GOOS)_arm6 +BINARYARM7 := fzf-$(GOOS)_arm7 +BINARYARM8 := fzf-$(GOOS)_arm8 +BINARYPPC64LE := fzf-$(GOOS)_ppc64le +BINARYRISCV64 := fzf-$(GOOS)_riscv64 + +# https://en.wikipedia.org/wiki/Uname +UNAME_M := $(shell uname -m) +ifeq ($(UNAME_M),x86_64) + BINARY := $(BINARY64) +else ifeq ($(UNAME_M),amd64) + BINARY := $(BINARY64) +else ifeq ($(UNAME_M),i686) + BINARY := $(BINARY32) +else ifeq ($(UNAME_M),i386) + BINARY := $(BINARY32) +else ifeq ($(UNAME_M),armv5l) + BINARY := $(BINARYARM5) +else ifeq ($(UNAME_M),armv6l) + BINARY := $(BINARYARM6) +else ifeq ($(UNAME_M),armv7l) + BINARY := $(BINARYARM7) +else ifeq ($(UNAME_M),armv8l) + BINARY := $(BINARYARM8) +else ifeq ($(UNAME_M),arm64) + BINARY := $(BINARYARM8) +else ifeq ($(UNAME_M),aarch64) + BINARY := $(BINARYARM8) +else ifeq ($(UNAME_M),ppc64le) + BINARY := $(BINARYPPC64LE) +else ifeq ($(UNAME_M),riscv64) + BINARY := $(BINARYRISCV64) +else +$(error Build on $(UNAME_M) is not supported, yet.) +endif + +all: target/$(BINARY) + +test: $(SOURCES) + [ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1) + SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \ + github.com/junegunn/fzf/src \ + github.com/junegunn/fzf/src/algo \ + github.com/junegunn/fzf/src/tui \ + github.com/junegunn/fzf/src/util + +bench: + cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem + +install: bin/fzf + +build: + goreleaser --rm-dist --snapshot + +release: +ifndef GITHUB_TOKEN + $(error GITHUB_TOKEN is not defined) +endif + + # Check if we are on master branch +ifneq ($(shell git symbolic-ref --short HEAD),master) + $(error Not on master branch) +endif + + # Check if version numbers are properly updated + grep -q ^$(VERSION_REGEX)$$ CHANGELOG.md + grep -qF '"fzf $(VERSION_TRIM)"' man/man1/fzf.1 + grep -qF '"fzf $(VERSION_TRIM)"' man/man1/fzf-tmux.1 + grep -qF $(VERSION) install + grep -qF $(VERSION) install.ps1 + + # Make release note out of CHANGELOG.md + mkdir -p tmp + sed -n '/^$(VERSION_REGEX)$$/,/^[0-9]/p' CHANGELOG.md | tail -r | \ + sed '1,/^ *$$/d' | tail -r | sed 1,2d | tee tmp/release-note + + # Push to temp branch first so that install scripts always works on master branch + git checkout -B temp master + git push origin temp --follow-tags --force + + # Make a GitHub release + goreleaser --rm-dist --release-notes tmp/release-note + + # Push to master + git checkout master + git push origin master + + # Delete temp branch + git push origin --delete temp + +clean: + $(RM) -r dist target + +target/$(BINARY32): $(SOURCES) + GOARCH=386 $(GO) build $(BUILD_FLAGS) -o $@ + +target/$(BINARY64): $(SOURCES) + GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@ + +# https://github.com/golang/go/wiki/GoArm +target/$(BINARYARM5): $(SOURCES) + GOARCH=arm GOARM=5 $(GO) build $(BUILD_FLAGS) -o $@ + +target/$(BINARYARM6): $(SOURCES) + GOARCH=arm GOARM=6 $(GO) build $(BUILD_FLAGS) -o $@ + +target/$(BINARYARM7): $(SOURCES) + GOARCH=arm GOARM=7 $(GO) build $(BUILD_FLAGS) -o $@ + +target/$(BINARYARM8): $(SOURCES) + GOARCH=arm64 $(GO) build $(BUILD_FLAGS) -o $@ + +target/$(BINARYPPC64LE): $(SOURCES) + GOARCH=ppc64le $(GO) build $(BUILD_FLAGS) -o $@ + +target/$(BINARYRISCV64): $(SOURCES) + GOARCH=riscv64 $(GO) build $(BUILD_FLAGS) -o $@ + +bin/fzf: target/$(BINARY) | bin + cp -f target/$(BINARY) bin/fzf + +docker: + docker build -t fzf-arch . + docker run -it fzf-arch tmux + +docker-test: + docker build -t fzf-arch . + docker run -it fzf-arch + +update: + $(GO) get -u + $(GO) mod tidy + +.PHONY: all build release test bench install clean docker docker-test update diff --git a/fzf/fzf/README-VIM.md b/fzf/fzf/README-VIM.md new file mode 100644 index 0000000..425bf67 --- /dev/null +++ b/fzf/fzf/README-VIM.md @@ -0,0 +1,486 @@ +FZF Vim integration +=================== + +Installation +------------ + +Once you have fzf installed, you can enable it inside Vim simply by adding the +directory to `&runtimepath` in your Vim configuration file. The path may +differ depending on the package manager. + +```vim +" If installed using Homebrew +set rtp+=/usr/local/opt/fzf + +" If installed using git +set rtp+=~/.fzf +``` + +If you use [vim-plug](https://github.com/junegunn/vim-plug), the same can be +written as: + +```vim +" If installed using Homebrew +Plug '/usr/local/opt/fzf' + +" If installed using git +Plug '~/.fzf' +``` + +But if you want the latest Vim plugin file from GitHub rather than the one +included in the package, write: + +```vim +Plug 'junegunn/fzf' +``` + +The Vim plugin will pick up fzf binary available on the system. If fzf is not +found on `$PATH`, it will ask you if it should download the latest binary for +you. + +To make sure that you have the latest version of the binary, set up +post-update hook like so: + +```vim +Plug 'junegunn/fzf', { 'do': { -> fzf#install() } } +``` + +Summary +------- + +The Vim plugin of fzf provides two core functions, and `:FZF` command which is +the basic file selector command built on top of them. + +1. **`fzf#run([spec dict])`** + - Starts fzf inside Vim with the given spec + - `:call fzf#run({'source': 'ls'})` +2. **`fzf#wrap([spec dict]) -> (dict)`** + - Takes a spec for `fzf#run` and returns an extended version of it with + additional options for addressing global preferences (`g:fzf_xxx`) + - `:echo fzf#wrap({'source': 'ls'})` + - We usually *wrap* a spec with `fzf#wrap` before passing it to `fzf#run` + - `:call fzf#run(fzf#wrap({'source': 'ls'}))` +3. **`:FZF [fzf_options string] [path string]`** + - Basic fuzzy file selector + - A reference implementation for those who don't want to write VimScript + to implement custom commands + - If you're looking for more such commands, check out [fzf.vim](https://github.com/junegunn/fzf.vim) project. + +The most important of all is `fzf#run`, but it would be easier to understand +the whole if we start off with `:FZF` command. + +`:FZF[!]` +--------- + +```vim +" Look for files under current directory +:FZF + +" Look for files under your home directory +:FZF ~ + +" With fzf command-line options +:FZF --reverse --info=inline /tmp + +" Bang version starts fzf in fullscreen mode +:FZF! +``` + +Similarly to [ctrlp.vim](https://github.com/kien/ctrlp.vim), use enter key, +`CTRL-T`, `CTRL-X` or `CTRL-V` to open selected files in the current window, +in new tabs, in horizontal splits, or in vertical splits respectively. + +Note that the environment variables `FZF_DEFAULT_COMMAND` and +`FZF_DEFAULT_OPTS` also apply here. + +### Configuration + +- `g:fzf_action` + - Customizable extra key bindings for opening selected files in different ways +- `g:fzf_layout` + - Determines the size and position of fzf window +- `g:fzf_colors` + - Customizes fzf colors to match the current color scheme +- `g:fzf_history_dir` + - Enables history feature + +#### Examples + +```vim +" This is the default extra key bindings +let g:fzf_action = { + \ 'ctrl-t': 'tab split', + \ 'ctrl-x': 'split', + \ 'ctrl-v': 'vsplit' } + +" An action can be a reference to a function that processes selected lines +function! s:build_quickfix_list(lines) + call setqflist(map(copy(a:lines), '{ "filename": v:val }')) + copen + cc +endfunction + +let g:fzf_action = { + \ 'ctrl-q': function('s:build_quickfix_list'), + \ 'ctrl-t': 'tab split', + \ 'ctrl-x': 'split', + \ 'ctrl-v': 'vsplit' } + +" Default fzf layout +" - Popup window (center of the screen) +let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } + +" - Popup window (center of the current window) +let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true } } + +" - Popup window (anchored to the bottom of the current window) +let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true, 'yoffset': 1.0 } } + +" - down / up / left / right +let g:fzf_layout = { 'down': '40%' } + +" - Window using a Vim command +let g:fzf_layout = { 'window': 'enew' } +let g:fzf_layout = { 'window': '-tabnew' } +let g:fzf_layout = { 'window': '10new' } + +" Customize fzf colors to match your color scheme +" - fzf#wrap translates this to a set of `--color` options +let g:fzf_colors = +\ { 'fg': ['fg', 'Normal'], + \ 'bg': ['bg', 'Normal'], + \ 'hl': ['fg', 'Comment'], + \ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'], + \ 'bg+': ['bg', 'CursorLine', 'CursorColumn'], + \ 'hl+': ['fg', 'Statement'], + \ 'info': ['fg', 'PreProc'], + \ 'border': ['fg', 'Ignore'], + \ 'prompt': ['fg', 'Conditional'], + \ 'pointer': ['fg', 'Exception'], + \ 'marker': ['fg', 'Keyword'], + \ 'spinner': ['fg', 'Label'], + \ 'header': ['fg', 'Comment'] } + +" Enable per-command history +" - History files will be stored in the specified directory +" - When set, CTRL-N and CTRL-P will be bound to 'next-history' and +" 'previous-history' instead of 'down' and 'up'. +let g:fzf_history_dir = '~/.local/share/fzf-history' +``` + +##### Explanation of `g:fzf_colors` + +`g:fzf_colors` is a dictionary mapping fzf elements to a color specification +list: + + element: [ component, group1 [, group2, ...] ] + +- `element` is an fzf element to apply a color to: + + | Element | Description | + | --- | --- | + | `fg` / `bg` / `hl` | Item (foreground / background / highlight) | + | `fg+` / `bg+` / `hl+` | Current item (foreground / background / highlight) | + | `preview-fg` / `preview-bg` | Preview window text and background | + | `hl` / `hl+` | Highlighted substrings (normal / current) | + | `gutter` | Background of the gutter on the left | + | `pointer` | Pointer to the current line (`>`) | + | `marker` | Multi-select marker (`>`) | + | `border` | Border around the window (`--border` and `--preview`) | + | `header` | Header (`--header` or `--header-lines`) | + | `info` | Info line (match counters) | + | `spinner` | Streaming input indicator | + | `query` | Query string | + | `disabled` | Query string when search is disabled | + | `prompt` | Prompt before query (`> `) | + | `pointer` | Pointer to the current line (`>`) | + +- `component` specifies the component (`fg` / `bg`) from which to extract the + color when considering each of the following highlight groups + +- `group1 [, group2, ...]` is a list of highlight groups that are searched (in + order) for a matching color definition + +For example, consider the following specification: + +```vim + 'prompt': ['fg', 'Conditional', 'Comment'], +``` + +This means we color the **prompt** +- using the `fg` attribute of the `Conditional` if it exists, +- otherwise use the `fg` attribute of the `Comment` highlight group if it exists, +- otherwise fall back to the default color settings for the **prompt**. + +You can examine the color option generated according the setting by printing +the result of `fzf#wrap()` function like so: + +```vim +:echo fzf#wrap() +``` + +`fzf#run` +--------- + +`fzf#run()` function is the core of Vim integration. It takes a single +dictionary argument, *a spec*, and starts fzf process accordingly. At the very +least, specify `sink` option to tell what it should do with the selected +entry. + +```vim +call fzf#run({'sink': 'e'}) +``` + +We haven't specified the `source`, so this is equivalent to starting fzf on +command line without standard input pipe; fzf will use find command (or +`$FZF_DEFAULT_COMMAND` if defined) to list the files under the current +directory. When you select one, it will open it with the sink, `:e` command. +If you want to open it in a new tab, you can pass `:tabedit` command instead +as the sink. + +```vim +call fzf#run({'sink': 'tabedit'}) +``` + +Instead of using the default find command, you can use any shell command as +the source. The following example will list the files managed by git. It's +equivalent to running `git ls-files | fzf` on shell. + +```vim +call fzf#run({'source': 'git ls-files', 'sink': 'e'}) +``` + +fzf options can be specified as `options` entry in spec dictionary. + +```vim +call fzf#run({'sink': 'tabedit', 'options': '--multi --reverse'}) +``` + +You can also pass a layout option if you don't want fzf window to take up the +entire screen. + +```vim +" up / down / left / right / window are allowed +call fzf#run({'source': 'git ls-files', 'sink': 'e', 'left': '40%'}) +call fzf#run({'source': 'git ls-files', 'sink': 'e', 'window': '30vnew'}) +``` + +`source` doesn't have to be an external shell command, you can pass a Vim +array as the source. In the next example, we pass the names of color +schemes as the source to implement a color scheme selector. + +```vim +call fzf#run({'source': map(split(globpath(&rtp, 'colors/*.vim')), + \ 'fnamemodify(v:val, ":t:r")'), + \ 'sink': 'colo', 'left': '25%'}) +``` + +The following table summarizes the available options. + +| Option name | Type | Description | +| -------------------------- | ------------- | ---------------------------------------------------------------- | +| `source` | string | External command to generate input to fzf (e.g. `find .`) | +| `source` | list | Vim list as input to fzf | +| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) | +| `sink` | funcref | Reference to function to process each selected item | +| `sinklist` (or `sink*`) | funcref | Similar to `sink`, but takes the list of output lines at once | +| `options` | string/list | Options to fzf | +| `dir` | string | Working directory | +| `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) | +| `tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%`) | +| `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new`) | +| `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}`) | + +`options` entry can be either a string or a list. For simple cases, string +should suffice, but prefer to use list type to avoid escaping issues. + +```vim +call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'}) +call fzf#run({'options': ['--reverse', '--prompt', 'C:\Program Files\']}) +``` + +When `window` entry is a dictionary, fzf will start in a popup window. The +following options are allowed: + +- Required: + - `width` [float range [0 ~ 1]] or [integer range [8 ~ ]] + - `height` [float range [0 ~ 1]] or [integer range [4 ~ ]] +- Optional: + - `yoffset` [float default 0.5 range [0 ~ 1]] + - `xoffset` [float default 0.5 range [0 ~ 1]] + - `relative` [boolean default v:false] + - `border` [string default `rounded`]: Border style + - `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]` + +`fzf#wrap` +---------- + +We have seen that several aspects of `:FZF` command can be configured with +a set of global option variables; different ways to open files +(`g:fzf_action`), window position and size (`g:fzf_layout`), color palette +(`g:fzf_colors`), etc. + +So how can we make our custom `fzf#run` calls also respect those variables? +Simply by *"wrapping"* the spec dictionary with `fzf#wrap` before passing it +to `fzf#run`. + +- **`fzf#wrap([name string], [spec dict], [fullscreen bool]) -> (dict)`** + - All arguments are optional. Usually we only need to pass a spec dictionary. + - `name` is for managing history files. It is ignored if + `g:fzf_history_dir` is not defined. + - `fullscreen` can be either `0` or `1` (default: 0). + +`fzf#wrap` takes a spec and returns an extended version of it (also +a dictionary) with additional options for addressing global preferences. You +can examine the return value of it like so: + +```vim +echo fzf#wrap({'source': 'ls'}) +``` + +After we *"wrap"* our spec, we pass it to `fzf#run`. + +```vim +call fzf#run(fzf#wrap({'source': 'ls'})) +``` + +Now it supports `CTRL-T`, `CTRL-V`, and `CTRL-X` key bindings (configurable +via `g:fzf_action`) and it opens fzf window according to `g:fzf_layout` +setting. + +To make it easier to use, let's define `LS` command. + +```vim +command! LS call fzf#run(fzf#wrap({'source': 'ls'})) +``` + +Type `:LS` and see how it works. + +We would like to make `:LS!` (bang version) open fzf in fullscreen, just like +`:FZF!`. Add `-bang` to command definition, and use `` value to set +the last `fullscreen` argument of `fzf#wrap` (see `:help `). + +```vim +" On :LS!, evaluates to '!', and '!0' becomes 1 +command! -bang LS call fzf#run(fzf#wrap({'source': 'ls'}, 0)) +``` + +Our `:LS` command will be much more useful if we can pass a directory argument +to it, so that something like `:LS /tmp` is possible. + +```vim +command! -bang -complete=dir -nargs=? LS + \ call fzf#run(fzf#wrap({'source': 'ls', 'dir': }, 0)) +``` + +Lastly, if you have enabled `g:fzf_history_dir`, you might want to assign +a unique name to our command and pass it as the first argument to `fzf#wrap`. + +```vim +" The query history for this command will be stored as 'ls' inside g:fzf_history_dir. +" The name is ignored if g:fzf_history_dir is not defined. +command! -bang -complete=dir -nargs=? LS + \ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': }, 0)) +``` + +### Global options supported by `fzf#wrap` + +- `g:fzf_layout` +- `g:fzf_action` + - **Works only when no custom `sink` (or `sinklist`) is provided** + - Having custom sink usually means that each entry is not an ordinary + file path (e.g. name of color scheme), so we can't blindly apply the + same strategy (i.e. `tabedit some-color-scheme` doesn't make sense) +- `g:fzf_colors` +- `g:fzf_history_dir` + +Tips +---- + +### fzf inside terminal buffer + +On the latest versions of Vim and Neovim, fzf will start in a terminal buffer. +If you find the default ANSI colors to be different, consider configuring the +colors using `g:terminal_ansi_colors` in regular Vim or `g:terminal_color_x` +in Neovim. + +```vim +" Terminal colors for seoul256 color scheme +if has('nvim') + let g:terminal_color_0 = '#4e4e4e' + let g:terminal_color_1 = '#d68787' + let g:terminal_color_2 = '#5f865f' + let g:terminal_color_3 = '#d8af5f' + let g:terminal_color_4 = '#85add4' + let g:terminal_color_5 = '#d7afaf' + let g:terminal_color_6 = '#87afaf' + let g:terminal_color_7 = '#d0d0d0' + let g:terminal_color_8 = '#626262' + let g:terminal_color_9 = '#d75f87' + let g:terminal_color_10 = '#87af87' + let g:terminal_color_11 = '#ffd787' + let g:terminal_color_12 = '#add4fb' + let g:terminal_color_13 = '#ffafaf' + let g:terminal_color_14 = '#87d7d7' + let g:terminal_color_15 = '#e4e4e4' +else + let g:terminal_ansi_colors = [ + \ '#4e4e4e', '#d68787', '#5f865f', '#d8af5f', + \ '#85add4', '#d7afaf', '#87afaf', '#d0d0d0', + \ '#626262', '#d75f87', '#87af87', '#ffd787', + \ '#add4fb', '#ffafaf', '#87d7d7', '#e4e4e4' + \ ] +endif +``` + +### Starting fzf in a popup window + +```vim +" Required: +" - width [float range [0 ~ 1]] or [integer range [8 ~ ]] +" - height [float range [0 ~ 1]] or [integer range [4 ~ ]] +" +" Optional: +" - xoffset [float default 0.5 range [0 ~ 1]] +" - yoffset [float default 0.5 range [0 ~ 1]] +" - relative [boolean default v:false] +" - border [string default 'rounded']: Border style +" - 'rounded' / 'sharp' / 'horizontal' / 'vertical' / 'top' / 'bottom' / 'left' / 'right' +let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } +``` + +Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2 +or above) by putting fzf-tmux options in `tmux` key. + +```vim +" See `man fzf-tmux` for available options +if exists('$TMUX') + let g:fzf_layout = { 'tmux': '-p90%,60%' } +else + let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } +endif +``` + +### Hide statusline + +When fzf starts in a terminal buffer, the file type of the buffer is set to +`fzf`. So you can set up `FileType fzf` autocmd to customize the settings of +the window. + +For example, if you open fzf on the bottom on the screen (e.g. `{'down': +'40%'}`), you might want to temporarily disable the statusline for a cleaner +look. + +```vim +let g:fzf_layout = { 'down': '30%' } +autocmd! FileType fzf +autocmd FileType fzf set laststatus=0 noshowmode noruler + \| autocmd BufLeave set laststatus=2 showmode ruler +``` + +[License](LICENSE) +------------------ + +The MIT License (MIT) + +Copyright (c) 2013-2021 Junegunn Choi diff --git a/fzf/fzf/README.md b/fzf/fzf/README.md new file mode 100644 index 0000000..92eab8b --- /dev/null +++ b/fzf/fzf/README.md @@ -0,0 +1,715 @@ +fzf - a command-line fuzzy finder [![github-actions](https://github.com/junegunn/fzf/workflows/Test%20fzf%20on%20Linux/badge.svg)](https://github.com/junegunn/fzf/actions) +=== + +fzf is a general-purpose command-line fuzzy finder. + + + +It's an interactive Unix filter for command-line that can be used with any +list; files, command history, processes, hostnames, bookmarks, git commits, +etc. + +Pros +---- + +- Portable, no dependencies +- Blazingly fast +- The most comprehensive feature set +- Flexible layout +- Batteries included + - Vim/Neovim plugin, key bindings, and fuzzy auto-completion + +Table of Contents +----------------- + + + +* [Installation](#installation) + * [Using Homebrew](#using-homebrew) + * [Using git](#using-git) + * [Using Linux package managers](#using-linux-package-managers) + * [Windows](#windows) + * [As Vim plugin](#as-vim-plugin) +* [Upgrading fzf](#upgrading-fzf) +* [Building fzf](#building-fzf) +* [Usage](#usage) + * [Using the finder](#using-the-finder) + * [Layout](#layout) + * [Search syntax](#search-syntax) + * [Environment variables](#environment-variables) + * [Options](#options) + * [Demo](#demo) +* [Examples](#examples) +* [`fzf-tmux` script](#fzf-tmux-script) +* [Key bindings for command-line](#key-bindings-for-command-line) +* [Fuzzy completion for bash and zsh](#fuzzy-completion-for-bash-and-zsh) + * [Files and directories](#files-and-directories) + * [Process IDs](#process-ids) + * [Host names](#host-names) + * [Environment variables / Aliases](#environment-variables--aliases) + * [Settings](#settings) + * [Supported commands](#supported-commands) + * [Custom fuzzy completion](#custom-fuzzy-completion) +* [Vim plugin](#vim-plugin) +* [Advanced topics](#advanced-topics) + * [Performance](#performance) + * [Executing external programs](#executing-external-programs) + * [Reloading the candidate list](#reloading-the-candidate-list) + * [1. Update the list of processes by pressing CTRL-R](#1-update-the-list-of-processes-by-pressing-ctrl-r) + * [2. Switch between sources by pressing CTRL-D or CTRL-F](#2-switch-between-sources-by-pressing-ctrl-d-or-ctrl-f) + * [3. Interactive ripgrep integration](#3-interactive-ripgrep-integration) + * [Preview window](#preview-window) +* [Tips](#tips) + * [Respecting `.gitignore`](#respecting-gitignore) + * [Fish shell](#fish-shell) +* [Related projects](#related-projects) +* [License](#license) + + + +Installation +------------ + +fzf project consists of the following components: + +- `fzf` executable +- `fzf-tmux` script for launching fzf in a tmux pane +- Shell extensions + - Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish) + - Fuzzy auto-completion (bash, zsh) +- Vim/Neovim plugin + +You can [download fzf executable][bin] alone if you don't need the extra +stuff. + +[bin]: https://github.com/junegunn/fzf/releases + +### Using Homebrew + +You can use [Homebrew](https://brew.sh/) (on macOS or Linux) +to install fzf. + +```sh +brew install fzf + +# To install useful key bindings and fuzzy completion: +$(brew --prefix)/opt/fzf/install +``` + +fzf is also available [via MacPorts][portfile]: `sudo port install fzf` + +[portfile]: https://github.com/macports/macports-ports/blob/master/sysutils/fzf/Portfile + +### Using git + +Alternatively, you can "git clone" this repository to any directory and run +[install](https://github.com/junegunn/fzf/blob/master/install) script. + +```sh +git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf +~/.fzf/install +``` + +### Using Linux package managers + +| Package Manager | Linux Distribution | Command | +| --- | --- | --- | +| APK | Alpine Linux | `sudo apk add fzf` | +| APT | Debian 9+/Ubuntu 19.10+ | `sudo apt-get install fzf` | +| Conda | | `conda install -c conda-forge fzf` | +| DNF | Fedora | `sudo dnf install fzf` | +| Nix | NixOS, etc. | `nix-env -iA nixpkgs.fzf` | +| Pacman | Arch Linux | `sudo pacman -S fzf` | +| pkg | FreeBSD | `pkg install fzf` | +| pkgin | NetBSD | `pkgin install fzf` | +| pkg_add | OpenBSD | `pkg_add fzf` | +| XBPS | Void Linux | `sudo xbps-install -S fzf` | +| Zypper | openSUSE | `sudo zypper install fzf` | + +> :warning: **Key bindings (CTRL-T / CTRL-R / ALT-C) and fuzzy auto-completion +> may not be enabled by default.** +> +> Refer to the package documentation for more information. (e.g. `apt-cache show fzf`) + +[![Packaging status](https://repology.org/badge/vertical-allrepos/fzf.svg)](https://repology.org/project/fzf/versions) + +### Windows + +Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also +available via [Chocolatey][choco] and [Scoop][scoop]: + +| Package manager | Command | +| --- | --- | +| Chocolatey | `choco install fzf` | +| Scoop | `scoop install fzf` | + +[choco]: https://chocolatey.org/packages/fzf +[scoop]: https://github.com/ScoopInstaller/Main/blob/master/bucket/fzf.json + +Known issues and limitations on Windows can be found on [the wiki +page][windows-wiki]. + +[windows-wiki]: https://github.com/junegunn/fzf/wiki/Windows + +### As Vim plugin + +If you use +[vim-plug](https://github.com/junegunn/vim-plug), add this line to your Vim +configuration file: + +```vim +Plug 'junegunn/fzf', { 'do': { -> fzf#install() } } +``` + +`fzf#install()` makes sure that you have the latest binary, but it's optional, +so you can omit it if you use a plugin manager that doesn't support hooks. + +For more installation options, see [README-VIM.md](README-VIM.md). + +Upgrading fzf +------------- + +fzf is being actively developed, and you might want to upgrade it once in a +while. Please follow the instruction below depending on the installation +method used. + +- git: `cd ~/.fzf && git pull && ./install` +- brew: `brew update; brew upgrade fzf` +- macports: `sudo port upgrade fzf` +- chocolatey: `choco upgrade fzf` +- vim-plug: `:PlugUpdate fzf` + +Building fzf +------------ + +See [BUILD.md](BUILD.md). + +Usage +----- + +fzf will launch interactive finder, read the list from STDIN, and write the +selected item to STDOUT. + +```sh +find * -type f | fzf > selected +``` + +Without STDIN pipe, fzf will use find command to fetch the list of +files excluding hidden ones. (You can override the default command with +`FZF_DEFAULT_COMMAND`) + +```sh +vim $(fzf) +``` + +#### Using the finder + +- `CTRL-K` / `CTRL-J` (or `CTRL-P` / `CTRL-N`) to move cursor up and down +- `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit +- On multi-select mode (`-m`), `TAB` and `Shift-TAB` to mark multiple items +- Emacs style key bindings +- Mouse: scroll, click, double-click; shift-click and shift-scroll on + multi-select mode + +#### Layout + +fzf by default starts in fullscreen mode, but you can make it start below the +cursor with `--height` option. + +```sh +vim $(fzf --height 40%) +``` + +Also, check out `--reverse` and `--layout` options if you prefer +"top-down" layout instead of the default "bottom-up" layout. + +```sh +vim $(fzf --height 40% --reverse) +``` + +You can add these options to `$FZF_DEFAULT_OPTS` so that they're applied by +default. For example, + +```sh +export FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border' +``` + +#### Search syntax + +Unless otherwise specified, fzf starts in "extended-search mode" where you can +type in multiple search terms delimited by spaces. e.g. `^music .mp3$ sbtrkt +!fire` + +| Token | Match type | Description | +| --------- | -------------------------- | ------------------------------------ | +| `sbtrkt` | fuzzy-match | Items that match `sbtrkt` | +| `'wild` | exact-match (quoted) | Items that include `wild` | +| `^music` | prefix-exact-match | Items that start with `music` | +| `.mp3$` | suffix-exact-match | Items that end with `.mp3` | +| `!fire` | inverse-exact-match | Items that do not include `fire` | +| `!^music` | inverse-prefix-exact-match | Items that do not start with `music` | +| `!.mp3$` | inverse-suffix-exact-match | Items that do not end with `.mp3` | + +If you don't prefer fuzzy matching and do not wish to "quote" every word, +start fzf with `-e` or `--exact` option. Note that when `--exact` is set, +`'`-prefix "unquotes" the term. + +A single bar character term acts as an OR operator. For example, the following +query matches entries that start with `core` and end with either `go`, `rb`, +or `py`. + +``` +^core go$ | rb$ | py$ +``` + +#### Environment variables + +- `FZF_DEFAULT_COMMAND` + - Default command to use when input is tty + - e.g. `export FZF_DEFAULT_COMMAND='fd --type f'` + - > :warning: This variable is not used by shell extensions due to the + > slight difference in requirements. + > + > (e.g. `CTRL-T` runs `$FZF_CTRL_T_COMMAND` instead, `vim **` runs + > `_fzf_compgen_path()`, and `cd **` runs `_fzf_compgen_dir()`) + > + > The available options are described later in this document. +- `FZF_DEFAULT_OPTS` + - Default options + - e.g. `export FZF_DEFAULT_OPTS="--layout=reverse --inline-info"` + +#### Options + +See the man page (`man fzf`) for the full list of options. + +#### Demo +If you learn by watching videos, check out this screencast by [@samoshkin](https://github.com/samoshkin) to explore `fzf` features. + + + + + +Examples +-------- + +* [Wiki page of examples](https://github.com/junegunn/fzf/wiki/examples) + * *Disclaimer: The examples on this page are maintained by the community + and are not thoroughly tested* +* [Advanced fzf examples](https://github.com/junegunn/fzf/blob/master/ADVANCED.md) + +`fzf-tmux` script +----------------- + +[fzf-tmux](bin/fzf-tmux) is a bash script that opens fzf in a tmux pane. + +```sh +# usage: fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS] + +# See available options +fzf-tmux --help + +# select git branches in horizontal split below (15 lines) +git branch | fzf-tmux -d 15 + +# select multiple words in vertical split on the left (20% of screen width) +cat /usr/share/dict/words | fzf-tmux -l 20% --multi --reverse +``` + +It will still work even when you're not on tmux, silently ignoring `-[pudlr]` +options, so you can invariably use `fzf-tmux` in your scripts. + +Alternatively, you can use `--height HEIGHT[%]` option not to start fzf in +fullscreen mode. + +```sh +fzf --height 40% +``` + +Key bindings for command-line +----------------------------- + +The install script will setup the following key bindings for bash, zsh, and +fish. + +- `CTRL-T` - Paste the selected files and directories onto the command-line + - Set `FZF_CTRL_T_COMMAND` to override the default command + - Set `FZF_CTRL_T_OPTS` to pass additional options +- `CTRL-R` - Paste the selected command from history onto the command-line + - If you want to see the commands in chronological order, press `CTRL-R` + again which toggles sorting by relevance + - Set `FZF_CTRL_R_OPTS` to pass additional options +- `ALT-C` - cd into the selected directory + - Set `FZF_ALT_C_COMMAND` to override the default command + - Set `FZF_ALT_C_OPTS` to pass additional options + +If you're on a tmux session, you can start fzf in a tmux split-pane or in +a tmux popup window by setting `FZF_TMUX_OPTS` (e.g. `-d 40%`). +See `fzf-tmux --help` for available options. + +More tips can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/Configuring-shell-key-bindings). + +Fuzzy completion for bash and zsh +--------------------------------- + +#### Files and directories + +Fuzzy completion for files and directories can be triggered if the word before +the cursor ends with the trigger sequence, which is by default `**`. + +- `COMMAND [DIRECTORY/][FUZZY_PATTERN]**` + +```sh +# Files under the current directory +# - You can select multiple items with TAB key +vim ** + +# Files under parent directory +vim ../** + +# Files under parent directory that match `fzf` +vim ../fzf** + +# Files under your home directory +vim ~/** + + +# Directories under current directory (single-selection) +cd ** + +# Directories under ~/github that match `fzf` +cd ~/github/fzf** +``` + +#### Process IDs + +Fuzzy completion for PIDs is provided for kill command. In this case, +there is no trigger sequence; just press the tab key after the kill command. + +```sh +# Can select multiple processes with or keys +kill -9 +``` + +#### Host names + +For ssh and telnet commands, fuzzy completion for hostnames is provided. The +names are extracted from /etc/hosts and ~/.ssh/config. + +```sh +ssh ** +telnet ** +``` + +#### Environment variables / Aliases + +```sh +unset ** +export ** +unalias ** +``` + +#### Settings + +```sh +# Use ~~ as the trigger sequence instead of the default ** +export FZF_COMPLETION_TRIGGER='~~' + +# Options to fzf command +export FZF_COMPLETION_OPTS='--border --info=inline' + +# Use fd (https://github.com/sharkdp/fd) instead of the default find +# command for listing path candidates. +# - The first argument to the function ($1) is the base path to start traversal +# - See the source code (completion.{bash,zsh}) for the details. +_fzf_compgen_path() { + fd --hidden --follow --exclude ".git" . "$1" +} + +# Use fd to generate the list for directory completion +_fzf_compgen_dir() { + fd --type d --hidden --follow --exclude ".git" . "$1" +} + +# (EXPERIMENTAL) Advanced customization of fzf options via _fzf_comprun function +# - The first argument to the function is the name of the command. +# - You should make sure to pass the rest of the arguments to fzf. +_fzf_comprun() { + local command=$1 + shift + + case "$command" in + cd) fzf "$@" --preview 'tree -C {} | head -200' ;; + export|unset) fzf "$@" --preview "eval 'echo \$'{}" ;; + ssh) fzf "$@" --preview 'dig {}' ;; + *) fzf "$@" ;; + esac +} +``` + +#### Supported commands + +On bash, fuzzy completion is enabled only for a predefined set of commands +(`complete | grep _fzf` to see the list). But you can enable it for other +commands as well by using `_fzf_setup_completion` helper function. + +```sh +# usage: _fzf_setup_completion path|dir|var|alias|host COMMANDS... +_fzf_setup_completion path ag git kubectl +_fzf_setup_completion dir tree +``` + +#### Custom fuzzy completion + +_**(Custom completion API is experimental and subject to change)**_ + +For a command named _"COMMAND"_, define `_fzf_complete_COMMAND` function using +`_fzf_complete` helper. + +```sh +# Custom fuzzy completion for "doge" command +# e.g. doge ** +_fzf_complete_doge() { + _fzf_complete --multi --reverse --prompt="doge> " -- "$@" < <( + echo very + echo wow + echo such + echo doge + ) +} +``` + +- The arguments before `--` are the options to fzf. +- After `--`, simply pass the original completion arguments unchanged (`"$@"`). +- Then, write a set of commands that generates the completion candidates and + feed its output to the function using process substitution (`< <(...)`). + +zsh will automatically pick up the function using the naming convention but in +bash you have to manually associate the function with the command using the +`complete` command. + +```sh +[ -n "$BASH" ] && complete -F _fzf_complete_doge -o default -o bashdefault doge +``` + +If you need to post-process the output from fzf, define +`_fzf_complete_COMMAND_post` as follows. + +```sh +_fzf_complete_foo() { + _fzf_complete --multi --reverse --header-lines=3 -- "$@" < <( + ls -al + ) +} + +_fzf_complete_foo_post() { + awk '{print $NF}' +} + +[ -n "$BASH" ] && complete -F _fzf_complete_foo -o default -o bashdefault foo +``` + +Vim plugin +---------- + +See [README-VIM.md](README-VIM.md). + +Advanced topics +--------------- + +### Performance + +fzf is fast and is [getting even faster][perf]. Performance should not be +a problem in most use cases. However, you might want to be aware of the +options that affect performance. + +- `--ansi` tells fzf to extract and parse ANSI color codes in the input, and it + makes the initial scanning slower. So it's not recommended that you add it + to your `$FZF_DEFAULT_OPTS`. +- `--nth` makes fzf slower because it has to tokenize each line. +- `--with-nth` makes fzf slower as fzf has to tokenize and reassemble each + line. +- If you absolutely need better performance, you can consider using + `--algo=v1` (the default being `v2`) to make fzf use a faster greedy + algorithm. However, this algorithm is not guaranteed to find the optimal + ordering of the matches and is not recommended. + +[perf]: https://junegunn.kr/images/fzf-0.17.0.png + +### Executing external programs + +You can set up key bindings for starting external processes without leaving +fzf (`execute`, `execute-silent`). + +```bash +# Press F1 to open the file with less without leaving fzf +# Press CTRL-Y to copy the line to clipboard and aborts fzf (requires pbcopy) +fzf --bind 'f1:execute(less -f {}),ctrl-y:execute-silent(echo {} | pbcopy)+abort' +``` + +See *KEY BINDINGS* section of the man page for details. + +### Reloading the candidate list + +By binding `reload` action to a key or an event, you can make fzf dynamically +reload the candidate list. See https://github.com/junegunn/fzf/issues/1750 for +more details. + +#### 1. Update the list of processes by pressing CTRL-R + +```sh +FZF_DEFAULT_COMMAND='ps -ef' \ + fzf --bind 'ctrl-r:reload($FZF_DEFAULT_COMMAND)' \ + --header 'Press CTRL-R to reload' --header-lines=1 \ + --height=50% --layout=reverse +``` + +#### 2. Switch between sources by pressing CTRL-D or CTRL-F + +```sh +FZF_DEFAULT_COMMAND='find . -type f' \ + fzf --bind 'ctrl-d:reload(find . -type d),ctrl-f:reload($FZF_DEFAULT_COMMAND)' \ + --height=50% --layout=reverse +``` + +#### 3. Interactive ripgrep integration + +The following example uses fzf as the selector interface for ripgrep. We bound +`reload` action to `change` event, so every time you type on fzf, the ripgrep +process will restart with the updated query string denoted by the placeholder +expression `{q}`. Also, note that we used `--disabled` option so that fzf +doesn't perform any secondary filtering. + +```sh +INITIAL_QUERY="" +RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " +FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \ + fzf --bind "change:reload:$RG_PREFIX {q} || true" \ + --ansi --disabled --query "$INITIAL_QUERY" \ + --height=50% --layout=reverse +``` + +If ripgrep doesn't find any matches, it will exit with a non-zero exit status, +and fzf will warn you about it. To suppress the warning message, we added +`|| true` to the command, so that it always exits with 0. + +See ["Using fzf as interative Ripgrep launcher"](https://github.com/junegunn/fzf/blob/master/ADVANCED.md#using-fzf-as-interative-ripgrep-launcher) +for a fuller example with preview window options. + +### Preview window + +When the `--preview` option is set, fzf automatically starts an external process +with the current line as the argument and shows the result in the split window. +Your `$SHELL` is used to execute the command with `$SHELL -c COMMAND`. +The window can be scrolled using the mouse or custom key bindings. + +```bash +# {} is replaced with the single-quoted string of the focused line +fzf --preview 'cat {}' +``` + +Preview window supports ANSI colors, so you can use any program that +syntax-highlights the content of a file, such as +[Bat](https://github.com/sharkdp/bat) or +[Highlight](http://www.andre-simon.de/doku/highlight/en/highlight.php): + +```bash +fzf --preview 'bat --style=numbers --color=always --line-range :500 {}' +``` + +You can customize the size, position, and border of the preview window using +`--preview-window` option, and the foreground and background color of it with +`--color` option. For example, + +```bash +fzf --height 40% --layout reverse --info inline --border \ + --preview 'file {}' --preview-window up,1,border-horizontal \ + --color 'fg:#bbccdd,fg+:#ddeeff,bg:#334455,preview-bg:#223344,border:#778899' +``` + +See the man page (`man fzf`) for the full list of options. + +For more advanced examples, see [Key bindings for git with fzf][fzf-git] +([code](https://gist.github.com/junegunn/8b572b8d4b5eddd8b85e5f4d40f17236)). + +[fzf-git]: https://junegunn.kr/2016/07/fzf-git/ + +---- + +Since fzf is a general-purpose text filter rather than a file finder, **it is +not a good idea to add `--preview` option to your `$FZF_DEFAULT_OPTS`**. + +```sh +# ********************* +# ** DO NOT DO THIS! ** +# ********************* +export FZF_DEFAULT_OPTS='--preview "bat --style=numbers --color=always --line-range :500 {}"' + +# bat doesn't work with any input other than the list of files +ps -ef | fzf +seq 100 | fzf +history | fzf +``` + +Tips +---- + +#### Respecting `.gitignore` + +You can use [fd](https://github.com/sharkdp/fd), +[ripgrep](https://github.com/BurntSushi/ripgrep), or [the silver +searcher](https://github.com/ggreer/the_silver_searcher) instead of the +default find command to traverse the file system while respecting +`.gitignore`. + +```sh +# Feed the output of fd into fzf +fd --type f | fzf + +# Setting fd as the default source for fzf +export FZF_DEFAULT_COMMAND='fd --type f' + +# Now fzf (w/o pipe) will use fd instead of find +fzf + +# To apply the command to CTRL-T as well +export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND" +``` + +If you want the command to follow symbolic links and don't want it to exclude +hidden files, use the following command: + +```sh +export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git' +``` + +#### Fish shell + +`CTRL-T` key binding of fish, unlike those of bash and zsh, will use the last +token on the command-line as the root directory for the recursive search. For +instance, hitting `CTRL-T` at the end of the following command-line + +```sh +ls /var/ +``` + +will list all files and directories under `/var/`. + +When using a custom `FZF_CTRL_T_COMMAND`, use the unexpanded `$dir` variable to +make use of this feature. `$dir` defaults to `.` when the last token is not a +valid directory. Example: + +```sh +set -g FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'" +``` + +Related projects +---------------- + +https://github.com/junegunn/fzf/wiki/Related-projects + +[License](LICENSE) +------------------ + +The MIT License (MIT) + +Copyright (c) 2013-2021 Junegunn Choi diff --git a/fzf/fzf/bin/fzf-tmux b/fzf/fzf/bin/fzf-tmux new file mode 100755 index 0000000..6a18cf8 --- /dev/null +++ b/fzf/fzf/bin/fzf-tmux @@ -0,0 +1,233 @@ +#!/usr/bin/env bash +# fzf-tmux: starts fzf in a tmux pane +# usage: fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS] + +fail() { + >&2 echo "$1" + exit 2 +} + +fzf="$(command -v fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf" +[[ -x "$fzf" ]] || fail 'fzf executable not found' + +tmux_args=() +args=() +opt="" +skip="" +swap="" +close="" +term="" +[[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines) || lines=$(tmux display-message -p "#{pane_height}") +[[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols) || columns=$(tmux display-message -p "#{pane_width}") + +help() { + >&2 echo 'usage: fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS] + + LAYOUT OPTIONS: + (default layout: -d 50%) + + Popup window (requires tmux 3.2 or above): + -p [WIDTH[%][,HEIGHT[%]]] (default: 50%) + -w WIDTH[%] + -h HEIGHT[%] + -x COL + -y ROW + + Split pane: + -u [HEIGHT[%]] Split above (up) + -d [HEIGHT[%]] Split below (down) + -l [WIDTH[%]] Split left + -r [WIDTH[%]] Split right +' + exit +} + +while [[ $# -gt 0 ]]; do + arg="$1" + shift + [[ -z "$skip" ]] && case "$arg" in + -) + term=1 + ;; + --help) + help + ;; + --version) + echo "fzf-tmux (with fzf $("$fzf" --version))" + exit + ;; + -p*|-w*|-h*|-x*|-y*|-d*|-u*|-r*|-l*) + if [[ "$arg" =~ ^-[pwhxy] ]]; then + [[ "$opt" =~ "-K -E" ]] || opt="-K -E" + elif [[ "$arg" =~ ^.[lr] ]]; then + opt="-h" + if [[ "$arg" =~ ^.l ]]; then + opt="$opt -d" + swap="; swap-pane -D ; select-pane -L" + close="; tmux swap-pane -D" + fi + else + opt="" + if [[ "$arg" =~ ^.u ]]; then + opt="$opt -d" + swap="; swap-pane -D ; select-pane -U" + close="; tmux swap-pane -D" + fi + fi + if [[ ${#arg} -gt 2 ]]; then + size="${arg:2}" + else + if [[ "$1" =~ ^[0-9%,]+$ ]] || [[ "$1" =~ ^[A-Z]$ ]]; then + size="$1" + shift + else + continue + fi + fi + + if [[ "$arg" =~ ^-p ]]; then + if [[ -n "$size" ]]; then + w=${size%%,*} + h=${size##*,} + opt="$opt -w$w -h$h" + fi + elif [[ "$arg" =~ ^-[whxy] ]]; then + opt="$opt ${arg:0:2}$size" + elif [[ "$size" =~ %$ ]]; then + size=${size:0:((${#size}-1))} + if [[ -n "$swap" ]]; then + opt="$opt -p $(( 100 - size ))" + else + opt="$opt -p $size" + fi + else + if [[ -n "$swap" ]]; then + if [[ "$arg" =~ ^.l ]]; then + max=$columns + else + max=$lines + fi + size=$(( max - size )) + [[ $size -lt 0 ]] && size=0 + opt="$opt -l $size" + else + opt="$opt -l $size" + fi + fi + ;; + --) + # "--" can be used to separate fzf-tmux options from fzf options to + # avoid conflicts + skip=1 + tmux_args=("${args[@]}") + args=() + continue + ;; + *) + args+=("$arg") + ;; + esac + [[ -n "$skip" ]] && args+=("$arg") +done + +if [[ -z "$TMUX" ]]; then + "$fzf" "${args[@]}" + exit $? +fi + +# --height option is not allowed. CTRL-Z is also disabled. +args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore") + +# Handle zoomed tmux pane without popup options by moving it to a temp window +if [[ ! "$opt" =~ "-K -E" ]] && tmux list-panes -F '#F' | grep -q Z; then + zoomed_without_popup=1 + original_window=$(tmux display-message -p "#{window_id}") + tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - '\\;' do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'") + tmux swap-pane -t $tmp_window \; select-window -t $tmp_window +fi + +set -e + +# Clean up named pipes on exit +id=$RANDOM +argsf="${TMPDIR:-/tmp}/fzf-args-$id" +fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id" +fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id" +fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id" +tmux_win_opts=( $(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/') ) +cleanup() { + \rm -f $argsf $fifo1 $fifo2 $fifo3 + + # Restore tmux window options + if [[ "${#tmux_win_opts[@]}" -gt 0 ]]; then + eval "tmux ${tmux_win_opts[*]}" + fi + + # Remove temp window if we were zoomed without popup options + if [[ -n "$zoomed_without_popup" ]]; then + tmux display-message -p "#{window_id}" > /dev/null + tmux swap-pane -t $original_window \; \ + select-window -t $original_window \; \ + kill-window -t $tmp_window \; \ + resize-pane -Z + fi + + if [[ $# -gt 0 ]]; then + trap - EXIT + exit 130 + fi +} +trap 'cleanup 1' SIGUSR1 +trap 'cleanup' EXIT + +envs="export TERM=$TERM " +[[ "$opt" =~ "-K -E" ]] && FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS" +[[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" +[[ -n "$FZF_DEFAULT_COMMAND" ]] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" +echo "$envs;" > "$argsf" + +# Build arguments to fzf +opts=$(printf "%q " "${args[@]}") + +pppid=$$ +echo -n "trap 'kill -SIGUSR1 -$pppid' EXIT SIGINT SIGTERM;" >> $argsf +close="; trap - EXIT SIGINT SIGTERM $close" + +export TMUX=$(cut -d , -f 1,2 <<< "$TMUX") +mkfifo -m o+w $fifo2 +if [[ "$opt" =~ "-K -E" ]]; then + cat $fifo2 & + if [[ -n "$term" ]] || [[ -t 0 ]]; then + cat <<< "\"$fzf\" $opts > $fifo2; out=\$? $close; exit \$out" >> $argsf + else + mkfifo $fifo1 + cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; out=\$? $close; exit \$out" >> $argsf + cat <&0 > $fifo1 & + fi + + # tmux dropped the support for `-K`, `-R` to popup command + # TODO: We can remove this once tmux 3.2 is released + if [[ ! "$(tmux popup --help 2>&1)" =~ '-R shell-command' ]]; then + opt="${opt/-K/}" + else + opt="${opt} -R" + fi + + tmux popup -d "$PWD" "${tmux_args[@]}" $opt "bash $argsf" > /dev/null 2>&1 + exit $? +fi + +mkfifo -m o+w $fifo3 +if [[ -n "$term" ]] || [[ -t 0 ]]; then + cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" >> $argsf +else + mkfifo $fifo1 + cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" >> $argsf + cat <&0 > $fifo1 & +fi +tmux set-window-option synchronize-panes off \;\ + set-window-option remain-on-exit off \;\ + split-window -c "$PWD" $opt "${tmux_args[@]}" "bash -c 'exec -a fzf bash $argsf'" $swap \ + > /dev/null 2>&1 || { "$fzf" "${args[@]}"; exit $?; } +cat $fifo2 +exit "$(cat $fifo3)" diff --git a/fzf/fzf/doc/fzf.txt b/fzf/fzf/doc/fzf.txt new file mode 100644 index 0000000..9485723 --- /dev/null +++ b/fzf/fzf/doc/fzf.txt @@ -0,0 +1,512 @@ +fzf.txt fzf Last change: May 19 2021 +FZF - TABLE OF CONTENTS *fzf* *fzf-toc* +============================================================================== + + FZF Vim integration |fzf-vim-integration| + Installation |fzf-installation| + Summary |fzf-summary| + :FZF[!] |:FZF| + Configuration |fzf-configuration| + Examples |fzf-examples| + Explanation of g:fzf_colors |fzf-explanation-of-gfzfcolors| + fzf#run |fzf#run| + fzf#wrap |fzf#wrap| + Global options supported by fzf#wrap |fzf-global-options-supported-by-fzf#wrap| + Tips |fzf-tips| + fzf inside terminal buffer |fzf-inside-terminal-buffer| + Starting fzf in a popup window |fzf-starting-fzf-in-a-popup-window| + Hide statusline |fzf-hide-statusline| + License |fzf-license| + +FZF VIM INTEGRATION *fzf-vim-integration* +============================================================================== + + +INSTALLATION *fzf-installation* +============================================================================== + +Once you have fzf installed, you can enable it inside Vim simply by adding the +directory to 'runtimepath' in your Vim configuration file. The path may differ +depending on the package manager. +> + " If installed using Homebrew + set rtp+=/usr/local/opt/fzf + + " If installed using git + set rtp+=~/.fzf +< +If you use {vim-plug}{1}, the same can be written as: +> + " If installed using Homebrew + Plug '/usr/local/opt/fzf' + + " If installed using git + Plug '~/.fzf' +< +But if you want the latest Vim plugin file from GitHub rather than the one +included in the package, write: +> + Plug 'junegunn/fzf' +< +The Vim plugin will pick up fzf binary available on the system. If fzf is not +found on `$PATH`, it will ask you if it should download the latest binary for +you. + +To make sure that you have the latest version of the binary, set up +post-update hook like so: + + *fzf#install* +> + Plug 'junegunn/fzf', { 'do': { -> fzf#install() } } +< + {1} https://github.com/junegunn/vim-plug + + +SUMMARY *fzf-summary* +============================================================================== + +The Vim plugin of fzf provides two core functions, and `:FZF` command which is +the basic file selector command built on top of them. + + 1. `fzf#run([spec dict])` + - Starts fzf inside Vim with the given spec + - `:call fzf#run({'source': 'ls'})` + 2. `fzf#wrap([spec dict]) -> (dict)` + - Takes a spec for `fzf#run` and returns an extended version of it with + additional options for addressing global preferences (`g:fzf_xxx`) + - `:echo fzf#wrap({'source': 'ls'})` + - We usually wrap a spec with `fzf#wrap` before passing it to `fzf#run` + - `:call fzf#run(fzf#wrap({'source': 'ls'}))` + 3. `:FZF [fzf_options string] [path string]` + - Basic fuzzy file selector + - A reference implementation for those who don't want to write VimScript to + implement custom commands + - If you're looking for more such commands, check out {fzf.vim}{2} project. + +The most important of all is `fzf#run`, but it would be easier to understand +the whole if we start off with `:FZF` command. + + {2} https://github.com/junegunn/fzf.vim + + +:FZF[!] +============================================================================== + + *:FZF* +> + " Look for files under current directory + :FZF + + " Look for files under your home directory + :FZF ~ + + " With fzf command-line options + :FZF --reverse --info=inline /tmp + + " Bang version starts fzf in fullscreen mode + :FZF! +< +Similarly to {ctrlp.vim}{3}, use enter key, CTRL-T, CTRL-X or CTRL-V to open +selected files in the current window, in new tabs, in horizontal splits, or in +vertical splits respectively. + +Note that the environment variables `FZF_DEFAULT_COMMAND` and +`FZF_DEFAULT_OPTS` also apply here. + + {3} https://github.com/kien/ctrlp.vim + + +< Configuration >_____________________________________________________________~ + *fzf-configuration* + + *g:fzf_action* *g:fzf_layout* *g:fzf_colors* *g:fzf_history_dir* + + - `g:fzf_action` + - Customizable extra key bindings for opening selected files in different + ways + - `g:fzf_layout` + - Determines the size and position of fzf window + - `g:fzf_colors` + - Customizes fzf colors to match the current color scheme + - `g:fzf_history_dir` + - Enables history feature + + +Examples~ + *fzf-examples* +> + " This is the default extra key bindings + let g:fzf_action = { + \ 'ctrl-t': 'tab split', + \ 'ctrl-x': 'split', + \ 'ctrl-v': 'vsplit' } + + " An action can be a reference to a function that processes selected lines + function! s:build_quickfix_list(lines) + call setqflist(map(copy(a:lines), '{ "filename": v:val }')) + copen + cc + endfunction + + let g:fzf_action = { + \ 'ctrl-q': function('s:build_quickfix_list'), + \ 'ctrl-t': 'tab split', + \ 'ctrl-x': 'split', + \ 'ctrl-v': 'vsplit' } + + " Default fzf layout + " - Popup window (center of the screen) + let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } + + " - Popup window (center of the current window) + let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true } } + + " - Popup window (anchored to the bottom of the current window) + let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true, 'yoffset': 1.0 } } + + " - down / up / left / right + let g:fzf_layout = { 'down': '40%' } + + " - Window using a Vim command + let g:fzf_layout = { 'window': 'enew' } + let g:fzf_layout = { 'window': '-tabnew' } + let g:fzf_layout = { 'window': '10new' } + + " Customize fzf colors to match your color scheme + " - fzf#wrap translates this to a set of `--color` options + let g:fzf_colors = + \ { 'fg': ['fg', 'Normal'], + \ 'bg': ['bg', 'Normal'], + \ 'hl': ['fg', 'Comment'], + \ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'], + \ 'bg+': ['bg', 'CursorLine', 'CursorColumn'], + \ 'hl+': ['fg', 'Statement'], + \ 'info': ['fg', 'PreProc'], + \ 'border': ['fg', 'Ignore'], + \ 'prompt': ['fg', 'Conditional'], + \ 'pointer': ['fg', 'Exception'], + \ 'marker': ['fg', 'Keyword'], + \ 'spinner': ['fg', 'Label'], + \ 'header': ['fg', 'Comment'] } + + " Enable per-command history + " - History files will be stored in the specified directory + " - When set, CTRL-N and CTRL-P will be bound to 'next-history' and + " 'previous-history' instead of 'down' and 'up'. + let g:fzf_history_dir = '~/.local/share/fzf-history' +< + +Explanation of g:fzf_colors~ + *fzf-explanation-of-gfzfcolors* + +`g:fzf_colors` is a dictionary mapping fzf elements to a color specification +list: +> + element: [ component, group1 [, group2, ...] ] +< + - `element` is an fzf element to apply a color to: + + ----------------------------+------------------------------------------------------ + Element | Description ~ + ----------------------------+------------------------------------------------------ + `fg` / `bg` / `hl` | Item (foreground / background / highlight) + `fg+` / `bg+` / `hl+` | Current item (foreground / background / highlight) + `preview-fg` / `preview-bg` | Preview window text and background + `hl` / `hl+` | Highlighted substrings (normal / current) + `gutter` | Background of the gutter on the left + `pointer` | Pointer to the current line ( `>` ) + `marker` | Multi-select marker ( `>` ) + `border` | Border around the window ( `--border` and `--preview` ) + `header` | Header ( `--header` or `--header-lines` ) + `info` | Info line (match counters) + `spinner` | Streaming input indicator + `query` | Query string + `disabled` | Query string when search is disabled + `prompt` | Prompt before query ( `> ` ) + `pointer` | Pointer to the current line ( `>` ) + ----------------------------+------------------------------------------------------ + - `component` specifies the component (`fg` / `bg`) from which to extract the + color when considering each of the following highlight groups + - `group1 [, group2, ...]` is a list of highlight groups that are searched (in + order) for a matching color definition + +For example, consider the following specification: +> + 'prompt': ['fg', 'Conditional', 'Comment'], +< +This means we color the prompt - using the `fg` attribute of the `Conditional` +if it exists, - otherwise use the `fg` attribute of the `Comment` highlight +group if it exists, - otherwise fall back to the default color settings for +the prompt. + +You can examine the color option generated according the setting by printing +the result of `fzf#wrap()` function like so: +> + :echo fzf#wrap() +< + +FZF#RUN +============================================================================== + + *fzf#run* + +`fzf#run()` function is the core of Vim integration. It takes a single +dictionary argument, a spec, and starts fzf process accordingly. At the very +least, specify `sink` option to tell what it should do with the selected +entry. +> + call fzf#run({'sink': 'e'}) +< +We haven't specified the `source`, so this is equivalent to starting fzf on +command line without standard input pipe; fzf will use find command (or +`$FZF_DEFAULT_COMMAND` if defined) to list the files under the current +directory. When you select one, it will open it with the sink, `:e` command. +If you want to open it in a new tab, you can pass `:tabedit` command instead +as the sink. +> + call fzf#run({'sink': 'tabedit'}) +< +Instead of using the default find command, you can use any shell command as +the source. The following example will list the files managed by git. It's +equivalent to running `git ls-files | fzf` on shell. +> + call fzf#run({'source': 'git ls-files', 'sink': 'e'}) +< +fzf options can be specified as `options` entry in spec dictionary. +> + call fzf#run({'sink': 'tabedit', 'options': '--multi --reverse'}) +< +You can also pass a layout option if you don't want fzf window to take up the +entire screen. +> + " up / down / left / right / window are allowed + call fzf#run({'source': 'git ls-files', 'sink': 'e', 'left': '40%'}) + call fzf#run({'source': 'git ls-files', 'sink': 'e', 'window': '30vnew'}) +< +`source` doesn't have to be an external shell command, you can pass a Vim +array as the source. In the next example, we pass the names of color schemes +as the source to implement a color scheme selector. +> + call fzf#run({'source': map(split(globpath(&rtp, 'colors/*.vim')), + \ 'fnamemodify(v:val, ":t:r")'), + \ 'sink': 'colo', 'left': '25%'}) +< +The following table summarizes the available options. + + ---------------------------+---------------+---------------------------------------------------------------------- + Option name | Type | Description ~ + ---------------------------+---------------+---------------------------------------------------------------------- + `source` | string | External command to generate input to fzf (e.g. `find .` ) + `source` | list | Vim list as input to fzf + `sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` ) + `sink` | funcref | Reference to function to process each selected item + `sinklist` (or `sink*` ) | funcref | Similar to `sink` , but takes the list of output lines at once + `options` | string/list | Options to fzf + `dir` | string | Working directory + `up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` ) + `tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%` ) + `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` ) + `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}` ) + ---------------------------+---------------+---------------------------------------------------------------------- + +`options` entry can be either a string or a list. For simple cases, string +should suffice, but prefer to use list type to avoid escaping issues. +> + call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'}) + call fzf#run({'options': ['--reverse', '--prompt', 'C:\Program Files\']}) +< +When `window` entry is a dictionary, fzf will start in a popup window. The +following options are allowed: + + - Required: + - `width` [float range [0 ~ 1]] or [integer range [8 ~ ]] + - `height` [float range [0 ~ 1]] or [integer range [4 ~ ]] + - Optional: + - `yoffset` [float default 0.5 range [0 ~ 1]] + - `xoffset` [float default 0.5 range [0 ~ 1]] + - `relative` [boolean default v:false] + - `border` [string default `rounded`]: Border style + - `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]` + + +FZF#WRAP +============================================================================== + + *fzf#wrap* + +We have seen that several aspects of `:FZF` command can be configured with a +set of global option variables; different ways to open files (`g:fzf_action`), +window position and size (`g:fzf_layout`), color palette (`g:fzf_colors`), +etc. + +So how can we make our custom `fzf#run` calls also respect those variables? +Simply by "wrapping" the spec dictionary with `fzf#wrap` before passing it to +`fzf#run`. + + - `fzf#wrap([name string], [spec dict], [fullscreen bool]) -> (dict)` + - All arguments are optional. Usually we only need to pass a spec + dictionary. + - `name` is for managing history files. It is ignored if `g:fzf_history_dir` + is not defined. + - `fullscreen` can be either `0` or `1` (default: 0). + +`fzf#wrap` takes a spec and returns an extended version of it (also a +dictionary) with additional options for addressing global preferences. You can +examine the return value of it like so: +> + echo fzf#wrap({'source': 'ls'}) +< +After we "wrap" our spec, we pass it to `fzf#run`. +> + call fzf#run(fzf#wrap({'source': 'ls'})) +< +Now it supports CTRL-T, CTRL-V, and CTRL-X key bindings (configurable via +`g:fzf_action`) and it opens fzf window according to `g:fzf_layout` setting. + +To make it easier to use, let's define `LS` command. +> + command! LS call fzf#run(fzf#wrap({'source': 'ls'})) +< +Type `:LS` and see how it works. + +We would like to make `:LS!` (bang version) open fzf in fullscreen, just like +`:FZF!`. Add `-bang` to command definition, and use value to set the +last `fullscreen` argument of `fzf#wrap` (see :help ). +> + " On :LS!, evaluates to '!', and '!0' becomes 1 + command! -bang LS call fzf#run(fzf#wrap({'source': 'ls'}, 0)) +< +Our `:LS` command will be much more useful if we can pass a directory argument +to it, so that something like `:LS /tmp` is possible. +> + command! -bang -complete=dir -nargs=? LS + \ call fzf#run(fzf#wrap({'source': 'ls', 'dir': }, 0)) +< +Lastly, if you have enabled `g:fzf_history_dir`, you might want to assign a +unique name to our command and pass it as the first argument to `fzf#wrap`. +> + " The query history for this command will be stored as 'ls' inside g:fzf_history_dir. + " The name is ignored if g:fzf_history_dir is not defined. + command! -bang -complete=dir -nargs=? LS + \ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': }, 0)) +< + +< Global options supported by fzf#wrap >______________________________________~ + *fzf-global-options-supported-by-fzf#wrap* + + - `g:fzf_layout` + - `g:fzf_action` + - Works only when no custom `sink` (or `sink*`) is provided + - Having custom sink usually means that each entry is not an ordinary + file path (e.g. name of color scheme), so we can't blindly apply the + same strategy (i.e. `tabedit some-color-scheme` doesn't make sense) + - `g:fzf_colors` + - `g:fzf_history_dir` + + +TIPS *fzf-tips* +============================================================================== + + +< fzf inside terminal buffer >________________________________________________~ + *fzf-inside-terminal-buffer* + +The latest versions of Vim and Neovim include builtin terminal emulator +(`:terminal`) and fzf will start in a terminal buffer in the following cases: + + - On Neovim + - On GVim + - On Terminal Vim with a non-default layout + - `call fzf#run({'left': '30%'})` or `let g:fzf_layout = {'left': '30%'}` + +On the latest versions of Vim and Neovim, fzf will start in a terminal buffer. +If you find the default ANSI colors to be different, consider configuring the +colors using `g:terminal_ansi_colors` in regular Vim or `g:terminal_color_x` +in Neovim. + + *g:terminal_color_15* *g:terminal_color_14* *g:terminal_color_13* +*g:terminal_color_12* *g:terminal_color_11* *g:terminal_color_10* *g:terminal_color_9* + *g:terminal_color_8* *g:terminal_color_7* *g:terminal_color_6* *g:terminal_color_5* + *g:terminal_color_4* *g:terminal_color_3* *g:terminal_color_2* *g:terminal_color_1* + *g:terminal_color_0* +> + " Terminal colors for seoul256 color scheme + if has('nvim') + let g:terminal_color_0 = '#4e4e4e' + let g:terminal_color_1 = '#d68787' + let g:terminal_color_2 = '#5f865f' + let g:terminal_color_3 = '#d8af5f' + let g:terminal_color_4 = '#85add4' + let g:terminal_color_5 = '#d7afaf' + let g:terminal_color_6 = '#87afaf' + let g:terminal_color_7 = '#d0d0d0' + let g:terminal_color_8 = '#626262' + let g:terminal_color_9 = '#d75f87' + let g:terminal_color_10 = '#87af87' + let g:terminal_color_11 = '#ffd787' + let g:terminal_color_12 = '#add4fb' + let g:terminal_color_13 = '#ffafaf' + let g:terminal_color_14 = '#87d7d7' + let g:terminal_color_15 = '#e4e4e4' + else + let g:terminal_ansi_colors = [ + \ '#4e4e4e', '#d68787', '#5f865f', '#d8af5f', + \ '#85add4', '#d7afaf', '#87afaf', '#d0d0d0', + \ '#626262', '#d75f87', '#87af87', '#ffd787', + \ '#add4fb', '#ffafaf', '#87d7d7', '#e4e4e4' + \ ] + endif +< + +< Starting fzf in a popup window >____________________________________________~ + *fzf-starting-fzf-in-a-popup-window* +> + " Required: + " - width [float range [0 ~ 1]] or [integer range [8 ~ ]] + " - height [float range [0 ~ 1]] or [integer range [4 ~ ]] + " + " Optional: + " - xoffset [float default 0.5 range [0 ~ 1]] + " - yoffset [float default 0.5 range [0 ~ 1]] + " - relative [boolean default v:false] + " - border [string default 'rounded']: Border style + " - 'rounded' / 'sharp' / 'horizontal' / 'vertical' / 'top' / 'bottom' / 'left' / 'right' + let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } +< +Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2 +or above) by putting fzf-tmux options in `tmux` key. +> + " See `man fzf-tmux` for available options + if exists('$TMUX') + let g:fzf_layout = { 'tmux': '-p90%,60%' } + else + let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } + endif +< + +< Hide statusline >___________________________________________________________~ + *fzf-hide-statusline* + +When fzf starts in a terminal buffer, the file type of the buffer is set to +`fzf`. So you can set up `FileType fzf` autocmd to customize the settings of +the window. + +For example, if you open fzf on the bottom on the screen (e.g. `{'down': +'40%'}`), you might want to temporarily disable the statusline for a cleaner +look. +> + let g:fzf_layout = { 'down': '30%' } + autocmd! FileType fzf + autocmd FileType fzf set laststatus=0 noshowmode noruler + \| autocmd BufLeave set laststatus=2 showmode ruler +< + +LICENSE *fzf-license* +============================================================================== + +The MIT License (MIT) + +Copyright (c) 2013-2021 Junegunn Choi + +============================================================================== +vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap: diff --git a/fzf/fzf/go.mod b/fzf/fzf/go.mod new file mode 100644 index 0000000..d7c3b3c --- /dev/null +++ b/fzf/fzf/go.mod @@ -0,0 +1,17 @@ +module github.com/junegunn/fzf + +require ( + github.com/gdamore/tcell v1.4.0 + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.14 + github.com/mattn/go-runewidth v0.0.13 + github.com/mattn/go-shellwords v1.0.12 + github.com/rivo/uniseg v0.2.0 + github.com/saracen/walker v0.1.2 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c + golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 + golang.org/x/text v0.3.6 // indirect +) + +go 1.13 diff --git a/fzf/fzf/go.sum b/fzf/fzf/go.sum new file mode 100644 index 0000000..f1bb671 --- /dev/null +++ b/fzf/fzf/go.sum @@ -0,0 +1,31 @@ +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU= +github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/saracen/walker v0.1.2 h1:/o1TxP82n8thLvmL4GpJXduYaRmJ7qXp8u9dSlV0zmo= +github.com/saracen/walker v0.1.2/go.mod h1:0oKYMsKVhSJ+ful4p/XbjvXbMgLEkLITZaxozsl4CGE= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs= +golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/fzf/fzf/install b/fzf/fzf/install new file mode 100755 index 0000000..59115fd --- /dev/null +++ b/fzf/fzf/install @@ -0,0 +1,377 @@ +#!/usr/bin/env bash + +set -u + +version=0.29.0 +auto_completion= +key_bindings= +update_config=2 +shells="bash zsh fish" +prefix='~/.fzf' +prefix_expand=~/.fzf +fish_dir=${XDG_CONFIG_HOME:-$HOME/.config}/fish + +help() { + cat << EOF +usage: $0 [OPTIONS] + + --help Show this message + --bin Download fzf binary only; Do not generate ~/.fzf.{bash,zsh} + --all Download fzf binary and update configuration files + to enable key bindings and fuzzy completion + --xdg Generate files under \$XDG_CONFIG_HOME/fzf + --[no-]key-bindings Enable/disable key bindings (CTRL-T, CTRL-R, ALT-C) + --[no-]completion Enable/disable fuzzy completion (bash & zsh) + --[no-]update-rc Whether or not to update shell configuration files + + --no-bash Do not set up bash configuration + --no-zsh Do not set up zsh configuration + --no-fish Do not set up fish configuration +EOF +} + +for opt in "$@"; do + case $opt in + --help) + help + exit 0 + ;; + --all) + auto_completion=1 + key_bindings=1 + update_config=1 + ;; + --xdg) + prefix='"${XDG_CONFIG_HOME:-$HOME/.config}"/fzf/fzf' + prefix_expand=${XDG_CONFIG_HOME:-$HOME/.config}/fzf/fzf + mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/fzf" + ;; + --key-bindings) key_bindings=1 ;; + --no-key-bindings) key_bindings=0 ;; + --completion) auto_completion=1 ;; + --no-completion) auto_completion=0 ;; + --update-rc) update_config=1 ;; + --no-update-rc) update_config=0 ;; + --bin) ;; + --no-bash) shells=${shells/bash/} ;; + --no-zsh) shells=${shells/zsh/} ;; + --no-fish) shells=${shells/fish/} ;; + *) + echo "unknown option: $opt" + help + exit 1 + ;; + esac +done + +cd "$(dirname "${BASH_SOURCE[0]}")" +fzf_base=$(pwd) +fzf_base_esc=$(printf %q "$fzf_base") + +ask() { + while true; do + read -p "$1 ([y]/n) " -r + REPLY=${REPLY:-"y"} + if [[ $REPLY =~ ^[Yy]$ ]]; then + return 1 + elif [[ $REPLY =~ ^[Nn]$ ]]; then + return 0 + fi + done +} + +check_binary() { + echo -n " - Checking fzf executable ... " + local output + output=$("$fzf_base"/bin/fzf --version 2>&1) + if [ $? -ne 0 ]; then + echo "Error: $output" + binary_error="Invalid binary" + else + output=${output/ */} + if [ "$version" != "$output" ]; then + echo "$output != $version" + binary_error="Invalid version" + else + echo "$output" + binary_error="" + return 0 + fi + fi + rm -f "$fzf_base"/bin/fzf + return 1 +} + +link_fzf_in_path() { + if which_fzf="$(command -v fzf)"; then + echo " - Found in \$PATH" + echo " - Creating symlink: bin/fzf -> $which_fzf" + (cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf) + check_binary && return + fi + return 1 +} + +try_curl() { + command -v curl > /dev/null && + if [[ $1 =~ tar.gz$ ]]; then + curl -fL $1 | tar -xzf - + else + local temp=${TMPDIR:-/tmp}/fzf.zip + curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp" + fi +} + +try_wget() { + command -v wget > /dev/null && + if [[ $1 =~ tar.gz$ ]]; then + wget -O - $1 | tar -xzf - + else + local temp=${TMPDIR:-/tmp}/fzf.zip + wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp" + fi +} + +download() { + echo "Downloading bin/fzf ..." + if [ -x "$fzf_base"/bin/fzf ]; then + echo " - Already exists" + check_binary && return + fi + link_fzf_in_path && return + mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin + if [ $? -ne 0 ]; then + binary_error="Failed to create bin directory" + return + fi + + local url + url=https://github.com/junegunn/fzf/releases/download/$version/${1} + set -o pipefail + if ! (try_curl $url || try_wget $url); then + set +o pipefail + binary_error="Failed to download with curl and wget" + return + fi + set +o pipefail + + if [ ! -f fzf ]; then + binary_error="Failed to download ${1}" + return + fi + + chmod +x fzf && check_binary +} + +# Try to download binary executable +archi=$(uname -sm) +binary_available=1 +binary_error="" +case "$archi" in + Darwin\ arm64) download fzf-$version-darwin_arm64.zip ;; + Darwin\ x86_64) download fzf-$version-darwin_amd64.zip ;; + Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;; + Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;; + Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;; + Linux\ armv8*) download fzf-$version-linux_arm64.tar.gz ;; + Linux\ aarch64*) download fzf-$version-linux_arm64.tar.gz ;; + Linux\ *64) download fzf-$version-linux_amd64.tar.gz ;; + FreeBSD\ *64) download fzf-$version-freebsd_amd64.tar.gz ;; + OpenBSD\ *64) download fzf-$version-openbsd_amd64.tar.gz ;; + CYGWIN*\ *64) download fzf-$version-windows_amd64.zip ;; + MINGW*\ *64) download fzf-$version-windows_amd64.zip ;; + MSYS*\ *64) download fzf-$version-windows_amd64.zip ;; + Windows*\ *64) download fzf-$version-windows_amd64.zip ;; + *) binary_available=0 binary_error=1 ;; +esac + +cd "$fzf_base" +if [ -n "$binary_error" ]; then + if [ $binary_available -eq 0 ]; then + echo "No prebuilt binary for $archi ..." + else + echo " - $binary_error !!!" + fi + if command -v go > /dev/null; then + echo -n "Building binary (go get -u github.com/junegunn/fzf) ... " + if [ -z "${GOPATH-}" ]; then + export GOPATH="${TMPDIR:-/tmp}/fzf-gopath" + mkdir -p "$GOPATH" + fi + if go get -ldflags "-s -w -X main.version=$version -X main.revision=go-get" github.com/junegunn/fzf; then + echo "OK" + cp "$GOPATH/bin/fzf" "$fzf_base/bin/" + else + echo "Failed to build binary. Installation failed." + exit 1 + fi + else + echo "go executable not found. Installation failed." + exit 1 + fi +fi + +[[ "$*" =~ "--bin" ]] && exit 0 + +for s in $shells; do + if ! command -v "$s" > /dev/null; then + shells=${shells/$s/} + fi +done + +if [[ ${#shells} -lt 3 ]]; then + echo "No shell configuration to be updated." + exit 0 +fi + +# Auto-completion +if [ -z "$auto_completion" ]; then + ask "Do you want to enable fuzzy auto-completion?" + auto_completion=$? +fi + +# Key-bindings +if [ -z "$key_bindings" ]; then + ask "Do you want to enable key bindings?" + key_bindings=$? +fi + +echo +for shell in $shells; do + [[ "$shell" = fish ]] && continue + src=${prefix_expand}.${shell} + echo -n "Generate $src ... " + + fzf_completion="[[ \$- == *i* ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null" + if [ $auto_completion -eq 0 ]; then + fzf_completion="# $fzf_completion" + fi + + fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\"" + if [ $key_bindings -eq 0 ]; then + fzf_key_bindings="# $fzf_key_bindings" + fi + + cat > "$src" << EOF +# Setup fzf +# --------- +if [[ ! "\$PATH" == *$fzf_base_esc/bin* ]]; then + export PATH="\${PATH:+\${PATH}:}$fzf_base/bin" +fi + +# Auto-completion +# --------------- +$fzf_completion + +# Key bindings +# ------------ +$fzf_key_bindings +EOF + echo "OK" +done + +# fish +if [[ "$shells" =~ fish ]]; then + echo -n "Update fish_user_paths ... " + fish << EOF + echo \$fish_user_paths | \grep "$fzf_base"/bin > /dev/null + or set --universal fish_user_paths \$fish_user_paths "$fzf_base"/bin +EOF + [ $? -eq 0 ] && echo "OK" || echo "Failed" + + mkdir -p "${fish_dir}/functions" + fish_binding="${fish_dir}/functions/fzf_key_bindings.fish" + if [ $key_bindings -ne 0 ]; then + echo -n "Symlink $fish_binding ... " + ln -sf "$fzf_base/shell/key-bindings.fish" \ + "$fish_binding" && echo "OK" || echo "Failed" + else + echo -n "Removing $fish_binding ... " + rm -f "$fish_binding" + echo "OK" + fi +fi + +append_line() { + set -e + + local update line file pat lno + update="$1" + line="$2" + file="$3" + pat="${4:-}" + lno="" + + echo "Update $file:" + echo " - $line" + if [ -f "$file" ]; then + if [ $# -lt 4 ]; then + lno=$(\grep -nF "$line" "$file" | sed 's/:.*//' | tr '\n' ' ') + else + lno=$(\grep -nF "$pat" "$file" | sed 's/:.*//' | tr '\n' ' ') + fi + fi + if [ -n "$lno" ]; then + echo " - Already exists: line #$lno" + else + if [ $update -eq 1 ]; then + [ -f "$file" ] && echo >> "$file" + echo "$line" >> "$file" + echo " + Added" + else + echo " ~ Skipped" + fi + fi + echo + set +e +} + +create_file() { + local file="$1" + shift + echo "Create $file:" + for line in "$@"; do + echo " $line" + echo "$line" >> "$file" + done + echo +} + +if [ $update_config -eq 2 ]; then + echo + ask "Do you want to update your shell configuration files?" + update_config=$? +fi +echo +for shell in $shells; do + [[ "$shell" = fish ]] && continue + [ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc + append_line $update_config "[ -f ${prefix}.${shell} ] && source ${prefix}.${shell}" "$dest" "${prefix}.${shell}" +done + +if [ $key_bindings -eq 1 ] && [[ "$shells" =~ fish ]]; then + bind_file="${fish_dir}/functions/fish_user_key_bindings.fish" + if [ ! -e "$bind_file" ]; then + create_file "$bind_file" \ + 'function fish_user_key_bindings' \ + ' fzf_key_bindings' \ + 'end' + else + append_line $update_config "fzf_key_bindings" "$bind_file" + fi +fi + +if [ $update_config -eq 1 ]; then + echo 'Finished. Restart your shell or reload config file.' + if [[ "$shells" =~ bash ]]; then + echo -n ' source ~/.bashrc # bash' + [[ "$archi" =~ Darwin ]] && echo -n ' (.bashrc should be loaded from .bash_profile)' + echo + fi + [[ "$shells" =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh" + [[ "$shells" =~ fish ]] && [ $key_bindings -eq 1 ] && echo ' fzf_key_bindings # fish' + echo + echo 'Use uninstall script to remove fzf.' + echo +fi +echo 'For more information, see: https://github.com/junegunn/fzf' diff --git a/fzf/fzf/install.ps1 b/fzf/fzf/install.ps1 new file mode 100644 index 0000000..2a1549f --- /dev/null +++ b/fzf/fzf/install.ps1 @@ -0,0 +1,65 @@ +$version="0.29.0" + +$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition + +function check_binary () { + Write-Host " - Checking fzf executable ... " -NoNewline + $output=cmd /c $fzf_base\bin\fzf.exe --version 2>&1 + if (-not $?) { + Write-Host "Error: $output" + $binary_error="Invalid binary" + } else { + $output=(-Split $output)[0] + if ($version -ne $output) { + Write-Host "$output != $version" + $binary_error="Invalid version" + } else { + Write-Host "$output" + $binary_error="" + return 1 + } + } + Remove-Item "$fzf_base\bin\fzf.exe" + return 0 +} + +function download { + param($file) + Write-Host "Downloading bin/fzf ..." + if (Test-Path "$fzf_base\bin\fzf.exe") { + Write-Host " - Already exists" + if (check_binary) { + return + } + } + if (-not (Test-Path "$fzf_base\bin")) { + md "$fzf_base\bin" + } + if (-not $?) { + $binary_error="Failed to create bin directory" + return + } + cd "$fzf_base\bin" + $url="https://github.com/junegunn/fzf/releases/download/$version/$file" + $temp=$env:TMP + "\fzf.zip" + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + if ($PSVersionTable.PSVersion.Major -ge 3) { + Invoke-WebRequest -Uri $url -OutFile $temp + } else { + (New-Object Net.WebClient).DownloadFile($url, $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath("$temp")) + } + if ($?) { + (Microsoft.PowerShell.Archive\Expand-Archive -Path $temp -DestinationPath .); (Remove-Item $temp) + } else { + $binary_error="Failed to download with powershell" + } + if (-not (Test-Path fzf.exe)) { + $binary_error="Failed to download $file" + return + } + echo y | icacls $fzf_base\bin\fzf.exe /grant Administrator:F ; check_binary >$null +} + +download "fzf-$version-windows_amd64.zip" + +Write-Host 'For more information, see: https://github.com/junegunn/fzf' diff --git a/fzf/fzf/main.go b/fzf/fzf/main.go new file mode 100644 index 0000000..0343f9c --- /dev/null +++ b/fzf/fzf/main.go @@ -0,0 +1,14 @@ +package main + +import ( + fzf "github.com/junegunn/fzf/src" + "github.com/junegunn/fzf/src/protector" +) + +var version string = "0.29" +var revision string = "devel" + +func main() { + protector.Protect() + fzf.Run(fzf.ParseOptions(), version, revision) +} diff --git a/fzf/fzf/man/man1/fzf-tmux.1 b/fzf/fzf/man/man1/fzf-tmux.1 new file mode 100644 index 0000000..2601d5b --- /dev/null +++ b/fzf/fzf/man/man1/fzf-tmux.1 @@ -0,0 +1,68 @@ +.ig +The MIT License (MIT) + +Copyright (c) 2013-2021 Junegunn Choi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +.. +.TH fzf-tmux 1 "Dec 2021" "fzf 0.29.0" "fzf-tmux - open fzf in tmux split pane" + +.SH NAME +fzf-tmux - open fzf in tmux split pane + +.SH SYNOPSIS +.B fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS] + +.SH DESCRIPTION +fzf-tmux is a wrapper script for fzf that opens fzf in a tmux split pane or in +a tmux popup window. It is designed to work just like fzf except that it does +not take up the whole screen. You can safely use fzf-tmux instead of fzf in +your scripts as the extra options will be silently ignored if you're not on +tmux. + +.SH LAYOUT OPTIONS + +(default layout: \fB-d 50%\fR) + +.SS Popup window +(requires tmux 3.2 or above) +.TP +.B "-p [WIDTH[%][,HEIGHT[%]]]" +.TP +.B "-w WIDTH[%]" +.TP +.B "-h WIDTH[%]" +.TP +.B "-x COL" +.TP +.B "-y ROW" + +.SS Split pane +.TP +.B "-u [height[%]]" +Split above (up) +.TP +.B "-d [height[%]]" +Split below (down) +.TP +.B "-l [width[%]]" +Split left +.TP +.B "-r [width[%]]" +Split right diff --git a/fzf/fzf/man/man1/fzf.1 b/fzf/fzf/man/man1/fzf.1 new file mode 100644 index 0000000..4920dba --- /dev/null +++ b/fzf/fzf/man/man1/fzf.1 @@ -0,0 +1,1018 @@ +.ig +The MIT License (MIT) + +Copyright (c) 2013-2021 Junegunn Choi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +.. +.TH fzf 1 "Dec 2021" "fzf 0.29.0" "fzf - a command-line fuzzy finder" + +.SH NAME +fzf - a command-line fuzzy finder + +.SH SYNOPSIS +fzf [options] + +.SH DESCRIPTION +fzf is a general-purpose command-line fuzzy finder. + +.SH OPTIONS +.SS Search mode +.TP +.B "-x, --extended" +Extended-search mode. Since 0.10.9, this is enabled by default. You can disable +it with \fB+x\fR or \fB--no-extended\fR. +.TP +.B "-e, --exact" +Enable exact-match +.TP +.B "-i" +Case-insensitive match (default: smart-case match) +.TP +.B "+i" +Case-sensitive match +.TP +.B "--literal" +Do not normalize latin script letters for matching. +.TP +.BI "--algo=" TYPE +Fuzzy matching algorithm (default: v2) + +.br +.BR v2 " Optimal scoring algorithm (quality)" +.br +.BR v1 " Faster but not guaranteed to find the optimal result (performance)" +.br + +.TP +.BI "-n, --nth=" "N[,..]" +Comma-separated list of field index expressions for limiting search scope. +See \fBFIELD INDEX EXPRESSION\fR for the details. +.TP +.BI "--with-nth=" "N[,..]" +Transform the presentation of each line using field index expressions +.TP +.BI "-d, --delimiter=" "STR" +Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style) +.TP +.BI "--disabled" +Do not perform search. With this option, fzf becomes a simple selector +interface rather than a "fuzzy finder". You can later enable the search using +\fBenable-search\fR or \fBtoggle-search\fR action. +.SS Search result +.TP +.B "+s, --no-sort" +Do not sort the result +.TP +.B "--tac" +Reverse the order of the input + +.RS +e.g. + \fBhistory | fzf --tac --no-sort\fR +.RE +.TP +.BI "--tiebreak=" "CRI[,..]" +Comma-separated list of sort criteria to apply when the scores are tied. +.br + +.br +.BR length " Prefers line with shorter length" +.br +.BR begin " Prefers line with matched substring closer to the beginning" +.br +.BR end " Prefers line with matched substring closer to the end" +.br +.BR index " Prefers line that appeared earlier in the input stream" +.br + +.br +- Each criterion should appear only once in the list +.br +- \fBindex\fR is only allowed at the end of the list +.br +- \fBindex\fR is implicitly appended to the list when not specified +.br +- Default is \fBlength\fR (or equivalently \fBlength\fR,index) +.br +- If \fBend\fR is found in the list, fzf will scan each line backwards +.SS Interface +.TP +.B "-m, --multi" +Enable multi-select with tab/shift-tab. It optionally takes an integer argument +which denotes the maximum number of items that can be selected. +.TP +.B "+m, --no-multi" +Disable multi-select +.TP +.B "--no-mouse" +Disable mouse +.TP +.BI "--bind=" "KEYBINDS" +Comma-separated list of custom key bindings. See \fBKEY/EVENT BINDINGS\fR for +the details. +.TP +.B "--cycle" +Enable cyclic scroll +.TP +.B "--keep-right" +Keep the right end of the line visible when it's too long. Effective only when +the query string is empty. +.TP +.BI "--scroll-off=" "LINES" +Number of screen lines to keep above or below when scrolling to the top or to +the bottom (default: 0). +.TP +.B "--no-hscroll" +Disable horizontal scroll +.TP +.BI "--hscroll-off=" "COLS" +Number of screen columns to keep to the right of the highlighted substring +(default: 10). Setting it to a large value will cause the text to be positioned +on the center of the screen. +.TP +.B "--filepath-word" +Make word-wise movements and actions respect path separators. The following +actions are affected: + +\fBbackward-kill-word\fR +.br +\fBbackward-word\fR +.br +\fBforward-word\fR +.br +\fBkill-word\fR +.TP +.BI "--jump-labels=" "CHARS" +Label characters for \fBjump\fR and \fBjump-accept\fR +.SS Layout +.TP +.BI "--height=" "HEIGHT[%]" +Display fzf window below the cursor with the given height instead of using +the full screen. +.TP +.BI "--min-height=" "HEIGHT" +Minimum height when \fB--height\fR is given in percent (default: 10). +Ignored when \fB--height\fR is not specified. +.TP +.BI "--layout=" "LAYOUT" +Choose the layout (default: default) + +.br +.BR default " Display from the bottom of the screen" +.br +.BR reverse " Display from the top of the screen" +.br +.BR reverse-list " Display from the top of the screen, prompt at the bottom" +.br + +.TP +.B "--reverse" +A synonym for \fB--layout=reverse\fB + +.TP +.BI "--border" [=STYLE] +Draw border around the finder + +.br +.BR rounded " Border with rounded corners (default)" +.br +.BR sharp " Border with sharp corners" +.br +.BR horizontal " Horizontal lines above and below the finder" +.br +.BR vertical " Vertical lines on each side of the finder" +.br +.BR top +.br +.BR bottom +.br +.BR left +.br +.BR right +.br +.BR none +.br + +.TP +.B "--no-unicode" +Use ASCII characters instead of Unicode box drawing characters to draw border + +.TP +.BI "--margin=" MARGIN +Comma-separated expression for margins around the finder. +.br + +.br +.RS +.BR TRBL " Same margin for top, right, bottom, and left" +.br +.BR TB,RL " Vertical, horizontal margin" +.br +.BR T,RL,B " Top, horizontal, bottom margin" +.br +.BR T,R,B,L " Top, right, bottom, left margin" +.br + +.br +Each part can be given in absolute number or in percentage relative to the +terminal size with \fB%\fR suffix. +.br + +.br +e.g. + \fBfzf --margin 10% + fzf --margin 1,5%\fR +.RE +.TP +.BI "--padding=" PADDING +Comma-separated expression for padding inside the border. Padding is +distinguishable from margin only when \fB--border\fR option is used. +.br + +.br +e.g. + \fBfzf --margin 5% --padding 5% --border --preview 'cat {}' \\ + --color bg:#222222,preview-bg:#333333\fR + +.br +.RS +.BR TRBL " Same padding for top, right, bottom, and left" +.br +.BR TB,RL " Vertical, horizontal padding" +.br +.BR T,RL,B " Top, horizontal, bottom padding" +.br +.BR T,R,B,L " Top, right, bottom, left padding" +.br +.RE + +.TP +.BI "--info=" "STYLE" +Determines the display style of finder info. + +.br +.BR default " Display on the next line to the prompt" +.br +.BR inline " Display on the same line" +.br +.BR hidden " Do not display finder info" +.br + +.TP +.B "--no-info" +A synonym for \fB--info=hidden\fB + +.TP +.BI "--prompt=" "STR" +Input prompt (default: '> ') +.TP +.BI "--pointer=" "STR" +Pointer to the current line (default: '>') +.TP +.BI "--marker=" "STR" +Multi-select marker (default: '>') +.TP +.BI "--header=" "STR" +The given string will be printed as the sticky header. The lines are displayed +in the given order from top to bottom regardless of \fB--layout\fR option, and +are not affected by \fB--with-nth\fR. ANSI color codes are processed even when +\fB--ansi\fR is not set. +.TP +.BI "--header-lines=" "N" +The first N lines of the input are treated as the sticky header. When +\fB--with-nth\fR is set, the lines are transformed just like the other +lines that follow. +.TP +.B "--header-first" +Print header before the prompt line +.SS Display +.TP +.B "--ansi" +Enable processing of ANSI color codes +.TP +.BI "--tabstop=" SPACES +Number of spaces for a tab character (default: 8) +.TP +.BI "--color=" "[BASE_SCHEME][,COLOR_NAME[:ANSI_COLOR][:ANSI_ATTRIBUTES]]..." +Color configuration. The name of the base color scheme is followed by custom +color mappings. + +.RS +.B BASE SCHEME: + (default: dark on 256-color terminal, otherwise 16) + + \fBdark \fRColor scheme for dark 256-color terminal + \fBlight \fRColor scheme for light 256-color terminal + \fB16 \fRColor scheme for 16-color terminal + \fBbw \fRNo colors (equivalent to \fB--no-color\fR) + +.B COLOR NAMES: + \fBfg \fRText + \fBbg \fRBackground + \fBpreview-fg \fRPreview window text + \fBpreview-bg \fRPreview window background + \fBhl \fRHighlighted substrings + \fBfg+ \fRText (current line) + \fBbg+ \fRBackground (current line) + \fBgutter \fRGutter on the left (defaults to \fBbg+\fR) + \fBhl+ \fRHighlighted substrings (current line) + \fBquery \fRQuery string + \fBdisabled \fRQuery string when search is disabled + \fBinfo \fRInfo line (match counters) + \fBborder \fRBorder around the window (\fB--border\fR and \fB--preview\fR) + \fBprompt \fRPrompt + \fBpointer \fRPointer to the current line + \fBmarker \fRMulti-select marker + \fBspinner \fRStreaming input indicator + \fBheader \fRHeader + +.B ANSI COLORS: + \fB-1 \fRDefault terminal foreground/background color + \fB \fR(or the original color of the text) + \fB0 ~ 15 \fR16 base colors + \fBblack\fR + \fBred\fR + \fBgreen\fR + \fByellow\fR + \fBblue\fR + \fBmagenta\fR + \fBcyan\fR + \fBwhite\fR + \fBbright-black\fR (gray | grey) + \fBbright-red\fR + \fBbright-green\fR + \fBbright-yellow\fR + \fBbright-blue\fR + \fBbright-magenta\fR + \fBbright-cyan\fR + \fBbright-white\fR + \fB16 ~ 255 \fRANSI 256 colors + \fB#rrggbb \fR24-bit colors + +.B ANSI ATTRIBUTES: (Only applies to foreground colors) + \fBregular \fRClears previously set attributes; should precede the other ones + \fBbold\fR + \fBunderline\fR + \fBreverse\fR + \fBdim\fR + \fBitalic\fR + +.B EXAMPLES: + + \fB# Seoul256 theme with 8-bit colors + # (https://github.com/junegunn/seoul256.vim) + fzf --color='bg:237,bg+:236,info:143,border:240,spinner:108' \\ + --color='hl:65,fg:252,header:65,fg+:252' \\ + --color='pointer:161,marker:168,prompt:110,hl+:108' + + # Seoul256 theme with 24-bit colors + fzf --color='bg:#4B4B4B,bg+:#3F3F3F,info:#BDBB72,border:#6B6B6B,spinner:#98BC99' \\ + --color='hl:#719872,fg:#D9D9D9,header:#719872,fg+:#D9D9D9' \\ + --color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'\fR +.RE +.TP +.B "--no-bold" +Do not use bold text +.TP +.B "--black" +Use black background +.SS History +.TP +.BI "--history=" "HISTORY_FILE" +Load search history from the specified file and update the file on completion. +When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically remapped to +\fBnext-history\fR and \fBprevious-history\fR. +.TP +.BI "--history-size=" "N" +Maximum number of entries in the history file (default: 1000). The file is +automatically truncated when the number of the lines exceeds the value. +.SS Preview +.TP +.BI "--preview=" "COMMAND" +Execute the given command for the current line and display the result on the +preview window. \fB{}\fR in the command is the placeholder that is replaced to +the single-quoted string of the current line. To transform the replacement +string, specify field index expressions between the braces (See \fBFIELD INDEX +EXPRESSION\fR for the details). + +.RS +e.g. + \fBfzf --preview='head -$LINES {}' + ls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR + +fzf exports \fB$FZF_PREVIEW_LINES\fR and \fB$FZF_PREVIEW_COLUMNS\fR so that +they represent the exact size of the preview window. (It also overrides +\fB$LINES\fR and \fB$COLUMNS\fR with the same values but they can be reset +by the default shell, so prefer to refer to the ones with \fBFZF_PREVIEW_\fR +prefix.) + +A placeholder expression starting with \fB+\fR flag will be replaced to the +space-separated list of the selected lines (or the current line if no selection +was made) individually quoted. + +e.g. + \fBfzf --multi --preview='head -10 {+}' + git log --oneline | fzf --multi --preview 'git show {+1}'\fR + +When using a field index expression, leading and trailing whitespace is stripped +from the replacement string. To preserve the whitespace, use the \fBs\fR flag. + +Also, \fB{q}\fR is replaced to the current query string, and \fB{n}\fR is +replaced to zero-based ordinal index of the line. Use \fB{+n}\fR if you want +all index numbers when multiple lines are selected. + +A placeholder expression with \fBf\fR flag is replaced to the path of +a temporary file that holds the evaluated list. This is useful when you +multi-select a large number of items and the length of the evaluated string may +exceed \fBARG_MAX\fR. + +e.g. + \fB# Press CTRL-A to select 100K items and see the sum of all the numbers. + # This won't work properly without 'f' flag due to ARG_MAX limit. + seq 100000 | fzf --multi --bind ctrl-a:select-all \\ + --preview "awk '{sum+=\$1} END {print sum}' {+f}"\fR + +Note that you can escape a placeholder pattern by prepending a backslash. + +Preview window will be updated even when there is no match for the current +query if any of the placeholder expressions evaluates to a non-empty string. + +Since 0.24.0, fzf can render partial preview content before the preview command +completes. ANSI escape sequence for clearing the display (\fBCSI 2 J\fR) is +supported, so you can use it to implement preview window that is constantly +updating. + +e.g. + \fBfzf --preview 'for i in $(seq 100000); do + (( i % 200 == 0 )) && printf "\\033[2J" + echo "$i" + sleep 0.01 + done'\fR +.RE +.TP +.BI "--preview-window=" "[POSITION][,SIZE[%]][,border-BORDER_OPT][,[no]wrap][,[no]follow][,[no]cycle][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]" + +.RS +.B POSITION: (default: right) + \fBup + \fBdown + \fBleft + \fBright + +\fRDetermines the layout of the preview window. + +* If the argument contains \fB:hidden\fR, the preview window will be hidden by +default until \fBtoggle-preview\fR action is triggered. + +* If size is given as 0, preview window will not be visible, but fzf will still +execute the command in the background. + +* Long lines are truncated by default. Line wrap can be enabled with +\fB:wrap\fR flag. + +* Preview window will automatically scroll to the bottom when \fB:follow\fR +flag is set, similarly to how \fBtail -f\fR works. + +.RS +e.g. + \fBfzf --preview-window follow --preview 'for i in $(seq 100000); do + echo "$i" + sleep 0.01 + (( i % 300 == 0 )) && printf "\\033[2J" + done'\fR +.RE + +* Cyclic scrolling is enabled with \fB:cycle\fR flag. + +* To change the style of the border of the preview window, specify one of +the options for \fB--border\fR with \fBborder-\fR prefix. +e.g. \fBborder-rounded\fR (border with rounded edges, default), +\fBborder-sharp\fR (border with sharp edges), \fBborder-left\fR, +\fBborder-none\fR, etc. + +* \fB[:+SCROLL[OFFSETS][/DENOM]]\fR determines the initial scroll offset of the +preview window. + + - \fBSCROLL\fR can be either a numeric integer or a single-field index expression that refers to a numeric integer. + + - The optional \fBOFFSETS\fR part is for adjusting the base offset. It should be given as a series of signed integers (\fB-INTEGER\fR or \fB+INTEGER\fR). + + - The final \fB/DENOM\fR part is for specifying a fraction of the preview window height. + +* \fB~HEADER_LINES\fR keeps the top N lines as the fixed header so that they +are always visible. + +* \fBdefault\fR resets all options previously set to the default. + +.RS +e.g. + \fB# Non-default scroll window positions and sizes + fzf --preview="head {}" --preview-window=up,30% + fzf --preview="file {}" --preview-window=down,1 + + # Initial scroll offset is set to the line number of each line of + # git grep output *minus* 5 lines (-5) + git grep --line-number '' | + fzf --delimiter : --preview 'nl {1}' --preview-window '+{2}-5' + + # Preview with bat, matching line in the middle of the window below + # the fixed header of the top 3 lines + # + # ~3 Top 3 lines as the fixed header + # +{2} Base scroll offset extracted from the second field + # +3 Extra offset to compensate for the 3-line header + # /2 Put in the middle of the preview area + # + git grep --line-number '' | + fzf --delimiter : \\ + --preview 'bat --style=full --color=always --highlight-line {2} {1}' \\ + --preview-window '~3,+{2}+3/2' + + # Display top 3 lines as the fixed header + fzf --preview 'bat --style=full --color=always {}' --preview-window '~3'\fR +.RE + +.SS Scripting +.TP +.BI "-q, --query=" "STR" +Start the finder with the given query +.TP +.B "-1, --select-1" +If there is only one match for the initial query (\fB--query\fR), do not start +interactive finder and automatically select the only match +.TP +.B "-0, --exit-0" +If there is no match for the initial query (\fB--query\fR), do not start +interactive finder and exit immediately +.TP +.BI "-f, --filter=" "STR" +Filter mode. Do not start interactive finder. When used with \fB--no-sort\fR, +fzf becomes a fuzzy-version of grep. +.TP +.B "--print-query" +Print query as the first line +.TP +.BI "--expect=" "KEY[,..]" +Comma-separated list of keys that can be used to complete fzf in addition to +the default enter key. When this option is set, fzf will print the name of the +key pressed as the first line of its output (or as the second line if +\fB--print-query\fR is also used). The line will be empty if fzf is completed +with the default enter key. If \fB--expect\fR option is specified multiple +times, fzf will expect the union of the keys. \fB--no-expect\fR will clear the +list. + +.RS +e.g. + \fBfzf --expect=ctrl-v,ctrl-t,alt-s --expect=f1,f2,~,@\fR +.RE +.TP +.B "--read0" +Read input delimited by ASCII NUL characters instead of newline characters +.TP +.B "--print0" +Print output delimited by ASCII NUL characters instead of newline characters +.TP +.B "--no-clear" +Do not clear finder interface on exit. If fzf was started in full screen mode, +it will not switch back to the original screen, so you'll have to manually run +\fBtput rmcup\fR to return. This option can be used to avoid flickering of the +screen when your application needs to start fzf multiple times in order. +.TP +.B "--sync" +Synchronous search for multi-staged filtering. If specified, fzf will launch +ncurses finder only after the input stream is complete. + +.RS +e.g. \fBfzf --multi | fzf --sync\fR +.RE +.TP +.B "--version" +Display version information and exit + +.TP +Note that most options have the opposite versions with \fB--no-\fR prefix. + +.SH ENVIRONMENT VARIABLES +.TP +.B FZF_DEFAULT_COMMAND +Default command to use when input is tty. On *nix systems, fzf runs the command +with \fB$SHELL -c\fR if \fBSHELL\fR is set, otherwise with \fBsh -c\fR, so in +this case make sure that the command is POSIX-compliant. +.TP +.B FZF_DEFAULT_OPTS +Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR + +.SH EXIT STATUS +.BR 0 " Normal exit" +.br +.BR 1 " No match" +.br +.BR 2 " Error" +.br +.BR 130 " Interrupted with \fBCTRL-C\fR or \fBESC\fR" + +.SH FIELD INDEX EXPRESSION + +A field index expression can be a non-zero integer or a range expression +([BEGIN]..[END]). \fB--nth\fR and \fB--with-nth\fR take a comma-separated list +of field index expressions. + +.SS Examples +.BR 1 " The 1st field" +.br +.BR 2 " The 2nd field" +.br +.BR -1 " The last field" +.br +.BR -2 " The 2nd to last field" +.br +.BR 3..5 " From the 3rd field to the 5th field" +.br +.BR 2.. " From the 2nd field to the last field" +.br +.BR ..-3 " From the 1st field to the 3rd to the last field" +.br +.BR .. " All the fields" +.br + +.SH EXTENDED SEARCH MODE + +Unless specified otherwise, fzf will start in "extended-search mode". In this +mode, you can specify multiple patterns delimited by spaces, such as: \fB'wild +^music .mp3$ sbtrkt !rmx\fR + +You can prepend a backslash to a space (\fB\\ \fR) to match a literal space +character. + +.SS Exact-match (quoted) +A term that is prefixed by a single-quote character (\fB'\fR) is interpreted as +an "exact-match" (or "non-fuzzy") term. fzf will search for the exact +occurrences of the string. + +.SS Anchored-match +A term can be prefixed by \fB^\fR, or suffixed by \fB$\fR to become an +anchored-match term. Then fzf will search for the lines that start with or end +with the given string. An anchored-match term is also an exact-match term. + +.SS Negation +If a term is prefixed by \fB!\fR, fzf will exclude the lines that satisfy the +term from the result. In this case, fzf performs exact match by default. + +.SS Exact-match by default +If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with +\fB'\fR) every word, start fzf with \fB-e\fR or \fB--exact\fR option. Note that +when \fB--exact\fR is set, \fB'\fR-prefix "unquotes" the term. + +.SS OR operator +A single bar character term acts as an OR operator. For example, the following +query matches entries that start with \fBcore\fR and end with either \fBgo\fR, +\fBrb\fR, or \fBpy\fR. + +e.g. \fB^core go$ | rb$ | py$\fR + +.SH KEY/EVENT BINDINGS +\fB--bind\fR option allows you to bind \fBa key\fR or \fBan event\fR to one or +more \fBactions\fR. You can use it to customize key bindings or implement +dynamic behaviors. + +\fB--bind\fR takes a comma-separated list of binding expressions. Each binding +expression is \fBKEY:ACTION\fR or \fBEVENT:ACTION\fR. + +e.g. + \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR + +.SS AVAILABLE KEYS: (SYNONYMS) +\fIctrl-[a-z]\fR +.br +\fIctrl-space\fR +.br +\fIctrl-\\\fR +.br +\fIctrl-]\fR +.br +\fIctrl-^\fR (\fIctrl-6\fR) +.br +\fIctrl-/\fR (\fIctrl-_\fR) +.br +\fIctrl-alt-[a-z]\fR +.br +\fIalt-[*]\fR (Any case-sensitive single character is allowed) +.br +\fIf[1-12]\fR +.br +\fIenter\fR (\fIreturn\fR \fIctrl-m\fR) +.br +\fIspace\fR +.br +\fIbspace\fR (\fIbs\fR) +.br +\fIalt-up\fR +.br +\fIalt-down\fR +.br +\fIalt-left\fR +.br +\fIalt-right\fR +.br +\fIalt-enter\fR +.br +\fIalt-space\fR +.br +\fIalt-bspace\fR (\fIalt-bs\fR) +.br +\fItab\fR +.br +\fIbtab\fR (\fIshift-tab\fR) +.br +\fIesc\fR +.br +\fIdel\fR +.br +\fIup\fR +.br +\fIdown\fR +.br +\fIleft\fR +.br +\fIright\fR +.br +\fIhome\fR +.br +\fIend\fR +.br +\fIinsert\fR +.br +\fIpgup\fR (\fIpage-up\fR) +.br +\fIpgdn\fR (\fIpage-down\fR) +.br +\fIshift-up\fR +.br +\fIshift-down\fR +.br +\fIshift-left\fR +.br +\fIshift-right\fR +.br +\fIalt-shift-up\fR +.br +\fIalt-shift-down\fR +.br +\fIalt-shift-left\fR +.br +\fIalt-shift-right\fR +.br +\fIleft-click\fR +.br +\fIright-click\fR +.br +\fIdouble-click\fR +.br +or any single character + +.SS AVAILABLE EVENTS: +\fIchange\fR +.RS +Triggered whenever the query string is changed + +e.g. + \fB# Move cursor to the first entry whenever the query is changed + fzf --bind change:first\fR +.RE + +\fIbackward-eof\fR +.RS +Triggered when the query string is already empty and you try to delete it +backward. + +e.g. + \fBfzf --bind backward-eof:abort\fR +.RE + +.SS AVAILABLE ACTIONS: +A key or an event can be bound to one or more of the following actions. + + \fBACTION: DEFAULT BINDINGS (NOTES): + \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR + \fBaccept\fR \fIenter double-click\fR + \fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection) + \fBbackward-char\fR \fIctrl-b left\fR + \fBbackward-delete-char\fR \fIctrl-h bspace\fR + \fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty) + \fBbackward-kill-word\fR \fIalt-bs\fR + \fBbackward-word\fR \fIalt-b shift-left\fR + \fBbeginning-of-line\fR \fIctrl-a home\fR + \fBcancel\fR (clear query string if not empty, abort fzf otherwise) + \fBchange-preview(...)\fR (change \fB--preview\fR option) + \fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|') + \fBchange-prompt(...)\fR (change prompt to the given string) + \fBclear-screen\fR \fIctrl-l\fR + \fBclear-selection\fR (clear multi-selection) + \fBclose\fR (close preview window if open, abort fzf otherwise) + \fBclear-query\fR (clear query string) + \fBdelete-char\fR \fIdel\fR + \fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty) + \fBdeselect\fR + \fBdeselect-all\fR (deselect all matches) + \fBdisable-search\fR (disable search functionality) + \fBdown\fR \fIctrl-j ctrl-n down\fR + \fBenable-search\fR (enable search functionality) + \fBend-of-line\fR \fIctrl-e end\fR + \fBexecute(...)\fR (see below for the details) + \fBexecute-silent(...)\fR (see below for the details) + \fBfirst\fR (move to the first match) + \fBforward-char\fR \fIctrl-f right\fR + \fBforward-word\fR \fIalt-f shift-right\fR + \fBignore\fR + \fBjump\fR (EasyMotion-like 2-keystroke movement) + \fBjump-accept\fR (jump and accept) + \fBkill-line\fR + \fBkill-word\fR \fIalt-d\fR + \fBlast\fR (move to the last match) + \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR) + \fBpage-down\fR \fIpgdn\fR + \fBpage-up\fR \fIpgup\fR + \fBhalf-page-down\fR + \fBhalf-page-up\fR + \fBpreview(...)\fR (see below for the details) + \fBpreview-down\fR \fIshift-down\fR + \fBpreview-up\fR \fIshift-up\fR + \fBpreview-page-down\fR + \fBpreview-page-up\fR + \fBpreview-half-page-down\fR + \fBpreview-half-page-up\fR + \fBpreview-bottom\fR + \fBpreview-top\fR + \fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR) + \fBprint-query\fR (print query and exit) + \fBput\fR (put the character to the prompt) + \fBrefresh-preview\fR + \fBreload(...)\fR (see below for the details) + \fBreplace-query\fR (replace query string with the current selection) + \fBselect\fR + \fBselect-all\fR (select all matches) + \fBtoggle\fR (\fIright-click\fR) + \fBtoggle-all\fR (toggle all matches) + \fBtoggle+down\fR \fIctrl-i (tab)\fR + \fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR) + \fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR) + \fBtoggle-preview\fR + \fBtoggle-preview-wrap\fR + \fBtoggle-search\fR (toggle search functionality) + \fBtoggle-sort\fR + \fBtoggle+up\fR \fIbtab (shift-tab)\fR + \fBunbind(...)\fR (unbind bindings) + \fBunix-line-discard\fR \fIctrl-u\fR + \fBunix-word-rubout\fR \fIctrl-w\fR + \fBup\fR \fIctrl-k ctrl-p up\fR + \fByank\fR \fIctrl-y\fR + +.SS ACTION COMPOSITION + +Multiple actions can be chained using \fB+\fR separator. + +e.g. + \fBfzf --multi --bind 'ctrl-a:select-all+accept'\fR + \fBfzf --multi --bind 'ctrl-a:select-all' --bind 'ctrl-a:+accept'\fR + +.SS ACTION ARGUMENT + +An action denoted with \fB(...)\fR suffix takes an argument. + +e.g. + \fBfzf --bind 'ctrl-a:change-prompt(NewPrompt> )'\fR + \fBfzf --bind 'ctrl-v:preview(cat {})' --preview-window hidden\fR + +If the argument contains parentheses, fzf may fail to parse the expression. In +that case, you can use any of the following alternative notations to avoid +parse errors. + + \fBaction-name[...]\fR + \fBaction-name~...~\fR + \fBaction-name!...!\fR + \fBaction-name@...@\fR + \fBaction-name#...#\fR + \fBaction-name$...$\fR + \fBaction-name%...%\fR + \fBaction-name^...^\fR + \fBaction-name&...&\fR + \fBaction-name*...*\fR + \fBaction-name;...;\fR + \fBaction-name/.../\fR + \fBaction-name|...|\fR + \fBaction-name:...\fR +.RS +The last one is the special form that frees you from parse errors as it does +not expect the closing character. The catch is that it should be the last one +in the comma-separated list of key-action pairs. +.RE + +.SS COMMAND EXECUTION + +With \fBexecute(...)\fR action, you can execute arbitrary commands without +leaving fzf. For example, you can turn fzf into a simple file browser by +binding \fBenter\fR key to \fBless\fR command like follows. + + \fBfzf --bind "enter:execute(less {})"\fR + +You can use the same placeholder expressions as in \fB--preview\fR. + +fzf switches to the alternate screen when executing a command. However, if the +command is expected to complete quickly, and you are not interested in its +output, you might want to use \fBexecute-silent\fR instead, which silently +executes the command without the switching. Note that fzf will not be +responsive until the command is complete. For asynchronous execution, start +your command as a background process (i.e. appending \fB&\fR). + +On *nix systems, fzf runs the command with \fB$SHELL -c\fR if \fBSHELL\fR is +set, otherwise with \fBsh -c\fR, so in this case make sure that the command is +POSIX-compliant. + +.SS RELOAD INPUT + +\fBreload(...)\fR action is used to dynamically update the input list +without restarting fzf. It takes the same command template with placeholder +expressions as \fBexecute(...)\fR. + +See \fIhttps://github.com/junegunn/fzf/issues/1750\fR for more info. + +e.g. + \fB# Update the list of processes by pressing CTRL-R + ps -ef | fzf --bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \\ + --header-lines=1 --layout=reverse + + # Integration with ripgrep + RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " + INITIAL_QUERY="foobar" + FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \\ + fzf --bind "change:reload:$RG_PREFIX {q} || true" \\ + --ansi --disabled --query "$INITIAL_QUERY"\fR + +.SS PREVIEW BINDING + +With \fBpreview(...)\fR action, you can specify multiple different preview +commands in addition to the default preview command given by \fB--preview\fR +option. + +e.g. + # Default preview command with an extra preview binding + fzf --preview 'file {}' --bind '?:preview:cat {}' + + # A preview binding with no default preview command + # (Preview window is initially empty) + fzf --bind '?:preview:cat {}' + + # Preview window hidden by default, it appears when you first hit '?' + fzf --bind '?:preview:cat {}' --preview-window hidden + +.SS CHANGE PREVIEW WINDOW ATTRIBUTES + +\fBchange-preview-window\fR action can be used to change the properties of the +preview window. Unlike the \fB--preview-window\fR option, you can specify +multiple sets of options separated by '|' characters. + +e.g. + # Rotate through the options using CTRL-/ + fzf --preview 'cat {}' --bind 'ctrl-/:change-preview-window(right,70%|down,40%,border-horizontal|hidden|right)' + + # The default properties given by `--preview-window` are inherited, so an empty string in the list is interpreted as the default + fzf --preview 'cat {}' --preview-window 'right,40%,border-left' --bind 'ctrl-/:change-preview-window(70%|down,border-top|hidden|)' + + # This is equivalent to toggle-preview action + fzf --preview 'cat {}' --bind 'ctrl-/:change-preview-window(hidden|)' + +.SH AUTHOR +Junegunn Choi (\fIjunegunn.c@gmail.com\fR) + +.SH SEE ALSO +.B Project homepage: +.RS +.I https://github.com/junegunn/fzf +.RE +.br + +.br +.B Extra Vim plugin: +.RS +.I https://github.com/junegunn/fzf.vim +.RE + +.SH LICENSE +MIT diff --git a/fzf/fzf/plugin/fzf.vim b/fzf/fzf/plugin/fzf.vim new file mode 100644 index 0000000..3c705e9 --- /dev/null +++ b/fzf/fzf/plugin/fzf.vim @@ -0,0 +1,1054 @@ +" Copyright (c) 2017 Junegunn Choi +" +" MIT License +" +" Permission is hereby granted, free of charge, to any person obtaining +" a copy of this software and associated documentation files (the +" "Software"), to deal in the Software without restriction, including +" without limitation the rights to use, copy, modify, merge, publish, +" distribute, sublicense, and/or sell copies of the Software, and to +" permit persons to whom the Software is furnished to do so, subject to +" the following conditions: +" +" The above copyright notice and this permission notice shall be +" included in all copies or substantial portions of the Software. +" +" THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +" EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +" MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +" NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +" LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +" OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +" WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +if exists('g:loaded_fzf') + finish +endif +let g:loaded_fzf = 1 + +let s:is_win = has('win32') || has('win64') +if s:is_win && &shellslash + set noshellslash + let s:base_dir = expand(':h:h') + set shellslash +else + let s:base_dir = expand(':h:h') +endif +if s:is_win + let s:term_marker = '&::FZF' + + function! s:fzf_call(fn, ...) + let shellslash = &shellslash + try + set noshellslash + return call(a:fn, a:000) + finally + let &shellslash = shellslash + endtry + endfunction + + " Use utf-8 for fzf.vim commands + " Return array of shell commands for cmd.exe + function! s:enc_to_cp(str) + if !has('iconv') + return a:str + endif + if !exists('s:codepage') + let s:codepage = libcallnr('kernel32.dll', 'GetACP', 0) + endif + return iconv(a:str, &encoding, 'cp'.s:codepage) + endfunction + function! s:wrap_cmds(cmds) + return map([ + \ '@echo off', + \ 'setlocal enabledelayedexpansion'] + \ + (has('gui_running') ? ['set TERM= > nul'] : []) + \ + (type(a:cmds) == type([]) ? a:cmds : [a:cmds]) + \ + ['endlocal'], + \ 'enc_to_cp(v:val."\r")') + endfunction +else + let s:term_marker = ";#FZF" + + function! s:fzf_call(fn, ...) + return call(a:fn, a:000) + endfunction + + function! s:wrap_cmds(cmds) + return a:cmds + endfunction + + function! s:enc_to_cp(str) + return a:str + endfunction +endif + +function! s:shellesc_cmd(arg) + let escaped = substitute(a:arg, '[&|<>()@^]', '^&', 'g') + let escaped = substitute(escaped, '%', '%%', 'g') + let escaped = substitute(escaped, '"', '\\^&', 'g') + let escaped = substitute(escaped, '\(\\\+\)\(\\^\)', '\1\1\2', 'g') + return '^"'.substitute(escaped, '\(\\\+\)$', '\1\1', '').'^"' +endfunction + +function! fzf#shellescape(arg, ...) + let shell = get(a:000, 0, s:is_win ? 'cmd.exe' : 'sh') + if shell =~# 'cmd.exe$' + return s:shellesc_cmd(a:arg) + endif + return s:fzf_call('shellescape', a:arg) +endfunction + +function! s:fzf_getcwd() + return s:fzf_call('getcwd') +endfunction + +function! s:fzf_fnamemodify(fname, mods) + return s:fzf_call('fnamemodify', a:fname, a:mods) +endfunction + +function! s:fzf_expand(fmt) + return s:fzf_call('expand', a:fmt, 1) +endfunction + +function! s:fzf_tempname() + return s:fzf_call('tempname') +endfunction + +let s:layout_keys = ['window', 'tmux', 'up', 'down', 'left', 'right'] +let s:fzf_go = s:base_dir.'/bin/fzf' +let s:fzf_tmux = s:base_dir.'/bin/fzf-tmux' + +let s:cpo_save = &cpo +set cpo&vim + +function! s:popup_support() + return has('nvim') ? has('nvim-0.4') : has('popupwin') && has('patch-8.2.191') +endfunction + +function! s:default_layout() + return s:popup_support() + \ ? { 'window' : { 'width': 0.9, 'height': 0.6 } } + \ : { 'down': '~40%' } +endfunction + +function! fzf#install() + if s:is_win && !has('win32unix') + let script = s:base_dir.'/install.ps1' + if !filereadable(script) + throw script.' not found' + endif + let script = 'powershell -ExecutionPolicy Bypass -file ' . script + else + let script = s:base_dir.'/install' + if !executable(script) + throw script.' not found' + endif + let script .= ' --bin' + endif + + call s:warn('Running fzf installer ...') + call system(script) + if v:shell_error + throw 'Failed to download fzf: '.script + endif +endfunction + +let s:versions = {} +function s:get_version(bin) + if has_key(s:versions, a:bin) + return s:versions[a:bin] + end + let command = a:bin . ' --version --no-height' + let output = systemlist(command) + if v:shell_error || empty(output) + return '' + endif + let ver = matchstr(output[-1], '[0-9.]\+') + let s:versions[a:bin] = ver + return ver +endfunction + +function! s:compare_versions(a, b) + let a = split(a:a, '\.') + let b = split(a:b, '\.') + for idx in range(0, max([len(a), len(b)]) - 1) + let v1 = str2nr(get(a, idx, 0)) + let v2 = str2nr(get(b, idx, 0)) + if v1 < v2 | return -1 + elseif v1 > v2 | return 1 + endif + endfor + return 0 +endfunction + +function! s:compare_binary_versions(a, b) + return s:compare_versions(s:get_version(a:a), s:get_version(a:b)) +endfunction + +let s:checked = {} +function! fzf#exec(...) + if !exists('s:exec') + let binaries = [] + if executable('fzf') + call add(binaries, 'fzf') + endif + if executable(s:fzf_go) + call add(binaries, s:fzf_go) + endif + + if empty(binaries) + if input('fzf executable not found. Download binary? (y/n) ') =~? '^y' + redraw + call fzf#install() + return fzf#exec() + else + redraw + throw 'fzf executable not found' + endif + elseif len(binaries) > 1 + call sort(binaries, 's:compare_binary_versions') + endif + + let s:exec = binaries[-1] + endif + + if a:0 && !has_key(s:checked, a:1) + let fzf_version = s:get_version(s:exec) + if empty(fzf_version) + let message = printf('Failed to run "%s --version"', s:exec) + unlet s:exec + throw message + end + + if s:compare_versions(fzf_version, a:1) >= 0 + let s:checked[a:1] = 1 + return s:exec + elseif a:0 < 2 && input(printf('You need fzf %s or above. Found: %s. Download binary? (y/n) ', a:1, fzf_version)) =~? '^y' + let s:versions = {} + unlet s:exec + redraw + call fzf#install() + return fzf#exec(a:1, 1) + else + throw printf('You need to upgrade fzf (required: %s or above)', a:1) + endif + endif + + return s:exec +endfunction + +function! s:tmux_enabled() + if has('gui_running') || !exists('$TMUX') + return 0 + endif + + if exists('s:tmux') + return s:tmux + endif + + let s:tmux = 0 + if !executable(s:fzf_tmux) + if executable('fzf-tmux') + let s:fzf_tmux = 'fzf-tmux' + else + return 0 + endif + endif + + let output = system('tmux -V') + let s:tmux = !v:shell_error && output >= 'tmux 1.7' + return s:tmux +endfunction + +function! s:escape(path) + let path = fnameescape(a:path) + return s:is_win ? escape(path, '$') : path +endfunction + +function! s:error(msg) + echohl ErrorMsg + echom a:msg + echohl None +endfunction + +function! s:warn(msg) + echohl WarningMsg + echom a:msg + echohl None +endfunction + +function! s:has_any(dict, keys) + for key in a:keys + if has_key(a:dict, key) + return 1 + endif + endfor + return 0 +endfunction + +function! s:open(cmd, target) + if stridx('edit', a:cmd) == 0 && s:fzf_fnamemodify(a:target, ':p') ==# s:fzf_expand('%:p') + return + endif + execute a:cmd s:escape(a:target) +endfunction + +function! s:common_sink(action, lines) abort + if len(a:lines) < 2 + return + endif + let key = remove(a:lines, 0) + let Cmd = get(a:action, key, 'e') + if type(Cmd) == type(function('call')) + return Cmd(a:lines) + endif + if len(a:lines) > 1 + augroup fzf_swap + autocmd SwapExists * let v:swapchoice='o' + \| call s:warn('fzf: E325: swap file exists: '.s:fzf_expand('')) + augroup END + endif + try + let empty = empty(s:fzf_expand('%')) && line('$') == 1 && empty(getline(1)) && !&modified + " Preserve the current working directory in case it's changed during + " the execution (e.g. `set autochdir` or `autocmd BufEnter * lcd ...`) + let cwd = exists('w:fzf_pushd') ? w:fzf_pushd.dir : expand('%:p:h') + for item in a:lines + if item[0] != '~' && item !~ (s:is_win ? '^[A-Z]:\' : '^/') + let sep = s:is_win ? '\' : '/' + let item = join([cwd, item], cwd[len(cwd)-1] == sep ? '' : sep) + endif + if empty + execute 'e' s:escape(item) + let empty = 0 + else + call s:open(Cmd, item) + endif + if !has('patch-8.0.0177') && !has('nvim-0.2') && exists('#BufEnter') + \ && isdirectory(item) + doautocmd BufEnter + endif + endfor + catch /^Vim:Interrupt$/ + finally + silent! autocmd! fzf_swap + endtry +endfunction + +function! s:get_color(attr, ...) + let gui = !s:is_win && !has('win32unix') && has('termguicolors') && &termguicolors + let fam = gui ? 'gui' : 'cterm' + let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$' + for group in a:000 + let code = synIDattr(synIDtrans(hlID(group)), a:attr, fam) + if code =~? pat + return code + endif + endfor + return '' +endfunction + +function! s:defaults() + let rules = copy(get(g:, 'fzf_colors', {})) + let colors = join(map(items(filter(map(rules, 'call("s:get_color", v:val)'), '!empty(v:val)')), 'join(v:val, ":")'), ',') + return empty(colors) ? '' : fzf#shellescape('--color='.colors) +endfunction + +function! s:validate_layout(layout) + for key in keys(a:layout) + if index(s:layout_keys, key) < 0 + throw printf('Invalid entry in g:fzf_layout: %s (allowed: %s)%s', + \ key, join(s:layout_keys, ', '), key == 'options' ? '. Use $FZF_DEFAULT_OPTS.' : '') + endif + endfor + return a:layout +endfunction + +function! s:evaluate_opts(options) + return type(a:options) == type([]) ? + \ join(map(copy(a:options), 'fzf#shellescape(v:val)')) : a:options +endfunction + +" [name string,] [opts dict,] [fullscreen boolean] +function! fzf#wrap(...) + let args = ['', {}, 0] + let expects = map(copy(args), 'type(v:val)') + let tidx = 0 + for arg in copy(a:000) + let tidx = index(expects, type(arg) == 6 ? type(0) : type(arg), tidx) + if tidx < 0 + throw 'Invalid arguments (expected: [name string] [opts dict] [fullscreen boolean])' + endif + let args[tidx] = arg + let tidx += 1 + unlet arg + endfor + let [name, opts, bang] = args + + if len(name) + let opts.name = name + end + + " Layout: g:fzf_layout (and deprecated g:fzf_height) + if bang + for key in s:layout_keys + if has_key(opts, key) + call remove(opts, key) + endif + endfor + elseif !s:has_any(opts, s:layout_keys) + if !exists('g:fzf_layout') && exists('g:fzf_height') + let opts.down = g:fzf_height + else + let opts = extend(opts, s:validate_layout(get(g:, 'fzf_layout', s:default_layout()))) + endif + endif + + " Colors: g:fzf_colors + let opts.options = s:defaults() .' '. s:evaluate_opts(get(opts, 'options', '')) + + " History: g:fzf_history_dir + if len(name) && len(get(g:, 'fzf_history_dir', '')) + let dir = s:fzf_expand(g:fzf_history_dir) + if !isdirectory(dir) + call mkdir(dir, 'p') + endif + let history = fzf#shellescape(dir.'/'.name) + let opts.options = join(['--history', history, opts.options]) + endif + + " Action: g:fzf_action + if !s:has_any(opts, ['sink', 'sinklist', 'sink*']) + let opts._action = get(g:, 'fzf_action', s:default_action) + let opts.options .= ' --expect='.join(keys(opts._action), ',') + function! opts.sinklist(lines) abort + return s:common_sink(self._action, a:lines) + endfunction + let opts['sink*'] = opts.sinklist " For backward compatibility + endif + + return opts +endfunction + +function! s:use_sh() + let [shell, shellslash, shellcmdflag, shellxquote] = [&shell, &shellslash, &shellcmdflag, &shellxquote] + if s:is_win + set shell=cmd.exe + set noshellslash + let &shellcmdflag = has('nvim') ? '/s /c' : '/c' + let &shellxquote = has('nvim') ? '"' : '(' + else + set shell=sh + endif + return [shell, shellslash, shellcmdflag, shellxquote] +endfunction + +function! s:writefile(...) + if call('writefile', a:000) == -1 + throw 'Failed to write temporary file. Check if you can write to the path tempname() returns.' + endif +endfunction + +function! fzf#run(...) abort +try + let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh() + + let dict = exists('a:1') ? copy(a:1) : {} + let temps = { 'result': s:fzf_tempname() } + let optstr = s:evaluate_opts(get(dict, 'options', '')) + try + let fzf_exec = fzf#shellescape(fzf#exec()) + catch + throw v:exception + endtry + + if !s:present(dict, 'dir') + let dict.dir = s:fzf_getcwd() + endif + if has('win32unix') && s:present(dict, 'dir') + let dict.dir = fnamemodify(dict.dir, ':p') + endif + + if has_key(dict, 'source') + let source = remove(dict, 'source') + let type = type(source) + if type == 1 + let source_command = source + elseif type == 3 + let temps.input = s:fzf_tempname() + call s:writefile(source, temps.input) + let source_command = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input) + else + throw 'Invalid source type' + endif + else + let source_command = '' + endif + + let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0) || has_key(dict, 'tmux') + let use_height = has_key(dict, 'down') && !has('gui_running') && + \ !(has('nvim') || s:is_win || has('win32unix') || s:present(dict, 'up', 'left', 'right', 'window')) && + \ executable('tput') && filereadable('/dev/tty') + let has_vim8_term = has('terminal') && has('patch-8.0.995') + let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win + let use_term = has_nvim_term || + \ has_vim8_term && !has('win32unix') && (has('gui_running') || s:is_win || s:present(dict, 'down', 'up', 'left', 'right', 'window')) + let use_tmux = (has_key(dict, 'tmux') || (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:splittable(dict)) && s:tmux_enabled() + if prefer_tmux && use_tmux + let use_height = 0 + let use_term = 0 + endif + if use_term + let optstr .= ' --no-height' + elseif use_height + let height = s:calc_size(&lines, dict.down, dict) + let optstr .= ' --height='.height + endif + let optstr .= s:border_opt(get(dict, 'window', 0)) + let prev_default_command = $FZF_DEFAULT_COMMAND + if len(source_command) + let $FZF_DEFAULT_COMMAND = source_command + endif + let command = (use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result + + if use_term + return s:execute_term(dict, command, temps) + endif + + let lines = use_tmux ? s:execute_tmux(dict, command, temps) + \ : s:execute(dict, command, use_height, temps) + call s:callback(dict, lines) + return lines +finally + if exists('source_command') && len(source_command) + if len(prev_default_command) + let $FZF_DEFAULT_COMMAND = prev_default_command + else + let $FZF_DEFAULT_COMMAND = '' + silent! execute 'unlet $FZF_DEFAULT_COMMAND' + endif + endif + let [&shell, &shellslash, &shellcmdflag, &shellxquote] = [shell, shellslash, shellcmdflag, shellxquote] +endtry +endfunction + +function! s:present(dict, ...) + for key in a:000 + if !empty(get(a:dict, key, '')) + return 1 + endif + endfor + return 0 +endfunction + +function! s:fzf_tmux(dict) + let size = get(a:dict, 'tmux', '') + if empty(size) + for o in ['up', 'down', 'left', 'right'] + if s:present(a:dict, o) + let spec = a:dict[o] + if (o == 'up' || o == 'down') && spec[0] == '~' + let size = '-'.o[0].s:calc_size(&lines, spec, a:dict) + else + " Legacy boolean option + let size = '-'.o[0].(spec == 1 ? '' : substitute(spec, '^\~', '', '')) + endif + break + endif + endfor + endif + return printf('LINES=%d COLUMNS=%d %s %s - --', + \ &lines, &columns, fzf#shellescape(s:fzf_tmux), size) +endfunction + +function! s:splittable(dict) + return s:present(a:dict, 'up', 'down') && &lines > 15 || + \ s:present(a:dict, 'left', 'right') && &columns > 40 +endfunction + +function! s:pushd(dict) + if s:present(a:dict, 'dir') + let cwd = s:fzf_getcwd() + let w:fzf_pushd = { + \ 'command': haslocaldir() ? 'lcd' : (exists(':tcd') && haslocaldir(-1) ? 'tcd' : 'cd'), + \ 'origin': cwd, + \ 'bufname': bufname('') + \ } + execute 'lcd' s:escape(a:dict.dir) + let cwd = s:fzf_getcwd() + let w:fzf_pushd.dir = cwd + let a:dict.pushd = w:fzf_pushd + return cwd + endif + return '' +endfunction + +augroup fzf_popd + autocmd! + autocmd WinEnter * call s:dopopd() +augroup END + +function! s:dopopd() + if !exists('w:fzf_pushd') + return + endif + + " FIXME: We temporarily change the working directory to 'dir' entry + " of options dictionary (set to the current working directory if not given) + " before running fzf. + " + " e.g. call fzf#run({'dir': '/tmp', 'source': 'ls', 'sink': 'e'}) + " + " After processing the sink function, we have to restore the current working + " directory. But doing so may not be desirable if the function changed the + " working directory on purpose. + " + " So how can we tell if we should do it or not? A simple heuristic we use + " here is that we change directory only if the current working directory + " matches 'dir' entry. However, it is possible that the sink function did + " change the directory to 'dir'. In that case, the user will have an + " unexpected result. + if s:fzf_getcwd() ==# w:fzf_pushd.dir && (!&autochdir || w:fzf_pushd.bufname ==# bufname('')) + execute w:fzf_pushd.command s:escape(w:fzf_pushd.origin) + endif + unlet! w:fzf_pushd +endfunction + +function! s:xterm_launcher() + let fmt = 'xterm -T "[fzf]" -bg "%s" -fg "%s" -geometry %dx%d+%d+%d -e bash -ic %%s' + if has('gui_macvim') + let fmt .= '&& osascript -e "tell application \"MacVim\" to activate"' + endif + return printf(fmt, + \ escape(synIDattr(hlID("Normal"), "bg"), '#'), escape(synIDattr(hlID("Normal"), "fg"), '#'), + \ &columns, &lines/2, getwinposx(), getwinposy()) +endfunction +unlet! s:launcher +if s:is_win || has('win32unix') + let s:launcher = '%s' +else + let s:launcher = function('s:xterm_launcher') +endif + +function! s:exit_handler(code, command, ...) + if a:code == 130 + return 0 + elseif has('nvim') && a:code == 129 + " When deleting the terminal buffer while fzf is still running, + " Nvim sends SIGHUP. + return 0 + elseif a:code > 1 + call s:error('Error running ' . a:command) + if !empty(a:000) + sleep + endif + return 0 + endif + return 1 +endfunction + +function! s:execute(dict, command, use_height, temps) abort + call s:pushd(a:dict) + if has('unix') && !a:use_height + silent! !clear 2> /dev/null + endif + let escaped = (a:use_height || s:is_win) ? a:command : escape(substitute(a:command, '\n', '\\n', 'g'), '%#!') + if has('gui_running') + let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher))) + let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher + if has('unix') + let escaped = "'".substitute(escaped, "'", "'\"'\"'", 'g')."'" + endif + let command = printf(fmt, escaped) + else + let command = escaped + endif + if s:is_win + let batchfile = s:fzf_tempname().'.bat' + call s:writefile(s:wrap_cmds(command), batchfile) + let command = batchfile + let a:temps.batchfile = batchfile + if has('nvim') + let fzf = {} + let fzf.dict = a:dict + let fzf.temps = a:temps + function! fzf.on_exit(job_id, exit_status, event) dict + call s:pushd(self.dict) + let lines = s:collect(self.temps) + call s:callback(self.dict, lines) + endfunction + let cmd = 'start /wait cmd /c '.command + call jobstart(cmd, fzf) + return [] + endif + elseif has('win32unix') && $TERM !=# 'cygwin' + let shellscript = s:fzf_tempname() + call s:writefile([command], shellscript) + let command = 'cmd.exe /C '.fzf#shellescape('set "TERM=" & start /WAIT sh -c '.shellscript) + let a:temps.shellscript = shellscript + endif + if a:use_height + call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s < /dev/tty 2> /dev/tty', &lines, command)) + else + execute 'silent !'.command + endif + let exit_status = v:shell_error + redraw! + let lines = s:collect(a:temps) + return s:exit_handler(exit_status, command) ? lines : [] +endfunction + +function! s:execute_tmux(dict, command, temps) abort + let command = a:command + let cwd = s:pushd(a:dict) + if len(cwd) + " -c '#{pane_current_path}' is only available on tmux 1.9 or above + let command = join(['cd', fzf#shellescape(cwd), '&&', command]) + endif + + call system(command) + let exit_status = v:shell_error + redraw! + let lines = s:collect(a:temps) + return s:exit_handler(exit_status, command) ? lines : [] +endfunction + +function! s:calc_size(max, val, dict) + let val = substitute(a:val, '^\~', '', '') + if val =~ '%$' + let size = a:max * str2nr(val[:-2]) / 100 + else + let size = min([a:max, str2nr(val)]) + endif + + let srcsz = -1 + if type(get(a:dict, 'source', 0)) == type([]) + let srcsz = len(a:dict.source) + endif + + let opts = $FZF_DEFAULT_OPTS.' '.s:evaluate_opts(get(a:dict, 'options', '')) + if opts =~ 'preview' + return size + endif + let margin = match(opts, '--inline-info\|--info[^-]\{-}inline') > match(opts, '--no-inline-info\|--info[^-]\{-}\(default\|hidden\)') ? 1 : 2 + let margin += stridx(opts, '--border') > stridx(opts, '--no-border') ? 2 : 0 + if stridx(opts, '--header') > stridx(opts, '--no-header') + let margin += len(split(opts, "\n")) + endif + return srcsz >= 0 ? min([srcsz + margin, size]) : size +endfunction + +function! s:getpos() + return {'tab': tabpagenr(), 'win': winnr(), 'winid': win_getid(), 'cnt': winnr('$'), 'tcnt': tabpagenr('$')} +endfunction + +function! s:border_opt(window) + if type(a:window) != type({}) + return '' + endif + + " Border style + let style = tolower(get(a:window, 'border', 'rounded')) + if !has_key(a:window, 'border') && !get(a:window, 'rounded', 1) + let style = 'sharp' + endif + if style == 'none' || style == 'no' + return '' + endif + + " For --border styles, we need fzf 0.24.0 or above + call fzf#exec('0.24.0') + let opt = ' --border=' . style + if has_key(a:window, 'highlight') + let color = s:get_color('fg', a:window.highlight) + if len(color) + let opt .= ' --color=border:' . color + endif + endif + return opt +endfunction + +function! s:split(dict) + let directions = { + \ 'up': ['topleft', 'resize', &lines], + \ 'down': ['botright', 'resize', &lines], + \ 'left': ['vertical topleft', 'vertical resize', &columns], + \ 'right': ['vertical botright', 'vertical resize', &columns] } + let ppos = s:getpos() + let is_popup = 0 + try + if s:present(a:dict, 'window') + if type(a:dict.window) == type({}) + if !s:popup_support() + throw 'Nvim 0.4+ or Vim 8.2.191+ with popupwin feature is required for pop-up window' + end + call s:popup(a:dict.window) + let is_popup = 1 + else + execute 'keepalt' a:dict.window + endif + elseif !s:splittable(a:dict) + execute (tabpagenr()-1).'tabnew' + else + for [dir, triple] in items(directions) + let val = get(a:dict, dir, '') + if !empty(val) + let [cmd, resz, max] = triple + if (dir == 'up' || dir == 'down') && val[0] == '~' + let sz = s:calc_size(max, val, a:dict) + else + let sz = s:calc_size(max, val, {}) + endif + execute cmd sz.'new' + execute resz sz + return [ppos, {}, is_popup] + endif + endfor + endif + return [ppos, is_popup ? {} : { '&l:wfw': &l:wfw, '&l:wfh': &l:wfh }, is_popup] + finally + if !is_popup + setlocal winfixwidth winfixheight + endif + endtry +endfunction + +nnoremap (fzf-insert) i +nnoremap (fzf-normal) +if exists(':tnoremap') + tnoremap (fzf-insert) i + tnoremap (fzf-normal) +endif + +function! s:execute_term(dict, command, temps) abort + let winrest = winrestcmd() + let pbuf = bufnr('') + let [ppos, winopts, is_popup] = s:split(a:dict) + call s:use_sh() + let b:fzf = a:dict + let fzf = { 'buf': bufnr(''), 'pbuf': pbuf, 'ppos': ppos, 'dict': a:dict, 'temps': a:temps, + \ 'winopts': winopts, 'winrest': winrest, 'lines': &lines, + \ 'columns': &columns, 'command': a:command } + function! fzf.switch_back(inplace) + if a:inplace && bufnr('') == self.buf + if bufexists(self.pbuf) + execute 'keepalt keepjumps b' self.pbuf + endif + " No other listed buffer + if bufnr('') == self.buf + enew + endif + endif + endfunction + function! fzf.on_exit(id, code, ...) + if s:getpos() == self.ppos " {'window': 'enew'} + for [opt, val] in items(self.winopts) + execute 'let' opt '=' val + endfor + call self.switch_back(1) + else + if bufnr('') == self.buf + " We use close instead of bd! since Vim does not close the split when + " there's no other listed buffer (nvim +'set nobuflisted') + close + endif + silent! execute 'tabnext' self.ppos.tab + silent! execute self.ppos.win.'wincmd w' + endif + + if bufexists(self.buf) + execute 'bd!' self.buf + endif + + if &lines == self.lines && &columns == self.columns && s:getpos() == self.ppos + execute self.winrest + endif + + let lines = s:collect(self.temps) + if !s:exit_handler(a:code, self.command, 1) + return + endif + + call s:pushd(self.dict) + call s:callback(self.dict, lines) + call self.switch_back(s:getpos() == self.ppos) + + if &buftype == 'terminal' + call feedkeys(&filetype == 'fzf' ? "\(fzf-insert)" : "\(fzf-normal)") + endif + endfunction + + try + call s:pushd(a:dict) + if s:is_win + let fzf.temps.batchfile = s:fzf_tempname().'.bat' + call s:writefile(s:wrap_cmds(a:command), fzf.temps.batchfile) + let command = fzf.temps.batchfile + else + let command = a:command + endif + let command .= s:term_marker + if has('nvim') + call termopen(command, fzf) + else + let term_opts = {'exit_cb': function(fzf.on_exit)} + if v:version >= 802 + let term_opts.term_kill = 'term' + endif + if is_popup + let term_opts.hidden = 1 + else + let term_opts.curwin = 1 + endif + let fzf.buf = term_start([&shell, &shellcmdflag, command], term_opts) + if is_popup && exists('#TerminalWinOpen') + doautocmd TerminalWinOpen + endif + if !has('patch-8.0.1261') && !s:is_win + call term_wait(fzf.buf, 20) + endif + endif + tnoremap + if exists('&termwinkey') && (empty(&termwinkey) || &termwinkey =~? '') + tnoremap . + endif + finally + call s:dopopd() + endtry + setlocal nospell bufhidden=wipe nobuflisted nonumber + setf fzf + startinsert + return [] +endfunction + +function! s:collect(temps) abort + try + return filereadable(a:temps.result) ? readfile(a:temps.result) : [] + finally + for tf in values(a:temps) + silent! call delete(tf) + endfor + endtry +endfunction + +function! s:callback(dict, lines) abort + let popd = has_key(a:dict, 'pushd') + if popd + let w:fzf_pushd = a:dict.pushd + endif + + try + if has_key(a:dict, 'sink') + for line in a:lines + if type(a:dict.sink) == 2 + call a:dict.sink(line) + else + execute a:dict.sink s:escape(line) + endif + endfor + endif + if has_key(a:dict, 'sink*') + call a:dict['sink*'](a:lines) + elseif has_key(a:dict, 'sinklist') + call a:dict['sinklist'](a:lines) + endif + catch + if stridx(v:exception, ':E325:') < 0 + echoerr v:exception + endif + endtry + + " We may have opened a new window or tab + if popd + let w:fzf_pushd = a:dict.pushd + call s:dopopd() + endif +endfunction + +if has('nvim') + function s:create_popup(hl, opts) abort + let buf = nvim_create_buf(v:false, v:true) + let opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts) + let win = nvim_open_win(buf, v:true, opts) + call setwinvar(win, '&winhighlight', 'NormalFloat:'..a:hl) + call setwinvar(win, '&colorcolumn', '') + return buf + endfunction +else + function! s:create_popup(hl, opts) abort + let s:popup_create = {buf -> popup_create(buf, #{ + \ line: a:opts.row, + \ col: a:opts.col, + \ minwidth: a:opts.width, + \ maxwidth: a:opts.width, + \ minheight: a:opts.height, + \ maxheight: a:opts.height, + \ zindex: 1000, + \ })} + autocmd TerminalOpen * ++once call s:popup_create(str2nr(expand(''))) + endfunction +endif + +function! s:popup(opts) abort + let xoffset = get(a:opts, 'xoffset', 0.5) + let yoffset = get(a:opts, 'yoffset', 0.5) + let relative = get(a:opts, 'relative', 0) + + " Use current window size for positioning relatively positioned popups + let columns = relative ? winwidth(0) : &columns + let lines = relative ? winheight(0) : (&lines - has('nvim')) + + " Size and position + let width = min([max([8, a:opts.width > 1 ? a:opts.width : float2nr(columns * a:opts.width)]), columns]) + let height = min([max([4, a:opts.height > 1 ? a:opts.height : float2nr(lines * a:opts.height)]), lines]) + let row = float2nr(yoffset * (lines - height)) + (relative ? win_screenpos(0)[0] - 1 : 0) + let col = float2nr(xoffset * (columns - width)) + (relative ? win_screenpos(0)[1] - 1 : 0) + + " Managing the differences + let row = min([max([0, row]), &lines - has('nvim') - height]) + let col = min([max([0, col]), &columns - width]) + let row += !has('nvim') + let col += !has('nvim') + + call s:create_popup('Normal', { + \ 'row': row, 'col': col, 'width': width, 'height': height + \ }) +endfunction + +let s:default_action = { + \ 'ctrl-t': 'tab split', + \ 'ctrl-x': 'split', + \ 'ctrl-v': 'vsplit' } + +function! s:shortpath() + let short = fnamemodify(getcwd(), ':~:.') + if !has('win32unix') + let short = pathshorten(short) + endif + let slash = (s:is_win && !&shellslash) ? '\' : '/' + return empty(short) ? '~'.slash : short . (short =~ escape(slash, '\').'$' ? '' : slash) +endfunction + +function! s:cmd(bang, ...) abort + let args = copy(a:000) + let opts = { 'options': ['--multi'] } + if len(args) && isdirectory(expand(args[-1])) + let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '') + if s:is_win && !&shellslash + let opts.dir = substitute(opts.dir, '/', '\\', 'g') + endif + let prompt = opts.dir + else + let prompt = s:shortpath() + endif + let prompt = strwidth(prompt) < &columns - 20 ? prompt : '> ' + call extend(opts.options, ['--prompt', prompt]) + call extend(opts.options, args) + call fzf#run(fzf#wrap('FZF', opts, a:bang)) +endfunction + +command! -nargs=* -complete=dir -bang FZF call s:cmd(0, ) + +let &cpo = s:cpo_save +unlet s:cpo_save diff --git a/fzf/fzf/shell/completion.bash b/fzf/fzf/shell/completion.bash new file mode 100644 index 0000000..21aa450 --- /dev/null +++ b/fzf/fzf/shell/completion.bash @@ -0,0 +1,381 @@ +# ____ ____ +# / __/___ / __/ +# / /_/_ / / /_ +# / __/ / /_/ __/ +# /_/ /___/_/ completion.bash +# +# - $FZF_TMUX (default: 0) +# - $FZF_TMUX_OPTS (default: empty) +# - $FZF_COMPLETION_TRIGGER (default: '**') +# - $FZF_COMPLETION_OPTS (default: empty) + +if [[ $- =~ i ]]; then + +# To use custom commands instead of find, override _fzf_compgen_{path,dir} +if ! declare -f _fzf_compgen_path > /dev/null; then + _fzf_compgen_path() { + echo "$1" + command find -L "$1" \ + -name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \ + -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' + } +fi + +if ! declare -f _fzf_compgen_dir > /dev/null; then + _fzf_compgen_dir() { + command find -L "$1" \ + -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \ + -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' + } +fi + +########################################################### + +# To redraw line after fzf closes (printf '\e[5n') +bind '"\e[0n": redraw-current-line' 2> /dev/null + +__fzf_comprun() { + if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then + _fzf_comprun "$@" + elif [[ -n "$TMUX_PANE" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "$FZF_TMUX_OPTS" ]]; }; then + shift + fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- "$@" + else + shift + fzf "$@" + fi +} + +__fzf_orig_completion() { + local l comp f cmd + while read -r l; do + if [[ "$l" =~ ^(.*\ -F)\ *([^ ]*).*\ ([^ ]*)$ ]]; then + comp="${BASH_REMATCH[1]}" + f="${BASH_REMATCH[2]}" + cmd="${BASH_REMATCH[3]}" + [[ "$f" = _fzf_* ]] && continue + printf -v "_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" "%s" "${comp} %s ${cmd} #${f}" + if [[ "$l" = *" -o nospace "* ]] && [[ ! "$__fzf_nospace_commands" = *" $cmd "* ]]; then + __fzf_nospace_commands="$__fzf_nospace_commands $cmd " + fi + fi + done +} + +_fzf_opts_completion() { + local cur prev opts + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + opts=" + -x --extended + -e --exact + --algo + -i +i + -n --nth + --with-nth + -d --delimiter + +s --no-sort + --tac + --tiebreak + -m --multi + --no-mouse + --bind + --cycle + --no-hscroll + --jump-labels + --height + --literal + --reverse + --margin + --inline-info + --prompt + --pointer + --marker + --header + --header-lines + --ansi + --tabstop + --color + --no-bold + --history + --history-size + --preview + --preview-window + -q --query + -1 --select-1 + -0 --exit-0 + -f --filter + --print-query + --expect + --sync" + + case "${prev}" in + --tiebreak) + COMPREPLY=( $(compgen -W "length begin end index" -- "$cur") ) + return 0 + ;; + --color) + COMPREPLY=( $(compgen -W "dark light 16 bw" -- "$cur") ) + return 0 + ;; + --history) + COMPREPLY=() + return 0 + ;; + esac + + if [[ "$cur" =~ ^-|\+ ]]; then + COMPREPLY=( $(compgen -W "${opts}" -- "$cur") ) + return 0 + fi + + return 0 +} + +_fzf_handle_dynamic_completion() { + local cmd orig_var orig ret orig_cmd orig_complete + cmd="$1" + shift + orig_cmd="$1" + orig_var="_fzf_orig_completion_$cmd" + orig="${!orig_var##*#}" + if [[ -n "$orig" ]] && type "$orig" > /dev/null 2>&1; then + $orig "$@" + elif [[ -n "$_fzf_completion_loader" ]]; then + orig_complete=$(complete -p "$orig_cmd" 2> /dev/null) + _completion_loader "$@" + ret=$? + # _completion_loader may not have updated completion for the command + if [[ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]]; then + __fzf_orig_completion < <(complete -p "$orig_cmd" 2> /dev/null) + if [[ "$__fzf_nospace_commands" = *" $orig_cmd "* ]]; then + eval "${orig_complete/ -F / -o nospace -F }" + else + eval "$orig_complete" + fi + fi + return $ret + fi +} + +__fzf_generic_path_completion() { + local cur base dir leftover matches trigger cmd + cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}" + COMPREPLY=() + trigger=${FZF_COMPLETION_TRIGGER-'**'} + cur="${COMP_WORDS[COMP_CWORD]}" + if [[ "$cur" == *"$trigger" ]]; then + base=${cur:0:${#cur}-${#trigger}} + eval "base=$base" + + [[ $base = *"/"* ]] && dir="$base" + while true; do + if [[ -z "$dir" ]] || [[ -d "$dir" ]]; then + leftover=${base/#"$dir"} + leftover=${leftover/#\/} + [[ -z "$dir" ]] && dir='.' + [[ "$dir" != "/" ]] && dir="${dir/%\//}" + matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $2" __fzf_comprun "$4" -q "$leftover" | while read -r item; do + printf "%q$3 " "$item" + done) + matches=${matches% } + [[ -z "$3" ]] && [[ "$__fzf_nospace_commands" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches " + if [[ -n "$matches" ]]; then + COMPREPLY=( "$matches" ) + else + COMPREPLY=( "$cur" ) + fi + printf '\e[5n' + return 0 + fi + dir=$(dirname "$dir") + [[ "$dir" =~ /$ ]] || dir="$dir"/ + done + else + shift + shift + shift + _fzf_handle_dynamic_completion "$cmd" "$@" + fi +} + +_fzf_complete() { + # Split arguments around -- + local args rest str_arg i sep + args=("$@") + sep= + for i in "${!args[@]}"; do + if [[ "${args[$i]}" = -- ]]; then + sep=$i + break + fi + done + if [[ -n "$sep" ]]; then + str_arg= + rest=("${args[@]:$((sep + 1)):${#args[@]}}") + args=("${args[@]:0:$sep}") + else + str_arg=$1 + args=() + shift + rest=("$@") + fi + + local cur selected trigger cmd post + post="$(caller 0 | awk '{print $2}')_post" + type -t "$post" > /dev/null 2>&1 || post=cat + + cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}" + trigger=${FZF_COMPLETION_TRIGGER-'**'} + cur="${COMP_WORDS[COMP_CWORD]}" + if [[ "$cur" == *"$trigger" ]]; then + cur=${cur:0:${#cur}-${#trigger}} + + selected=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $str_arg" __fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | tr '\n' ' ') + selected=${selected% } # Strip trailing space not to repeat "-o nospace" + if [[ -n "$selected" ]]; then + COMPREPLY=("$selected") + else + COMPREPLY=("$cur") + fi + printf '\e[5n' + return 0 + else + _fzf_handle_dynamic_completion "$cmd" "${rest[@]}" + fi +} + +_fzf_path_completion() { + __fzf_generic_path_completion _fzf_compgen_path "-m" "" "$@" +} + +# Deprecated. No file only completion. +_fzf_file_completion() { + _fzf_path_completion "$@" +} + +_fzf_dir_completion() { + __fzf_generic_path_completion _fzf_compgen_dir "" "/" "$@" +} + +_fzf_complete_kill() { + local trigger=${FZF_COMPLETION_TRIGGER-'**'} + local cur="${COMP_WORDS[COMP_CWORD]}" + if [[ -z "$cur" ]]; then + COMP_WORDS[$COMP_CWORD]=$trigger + elif [[ "$cur" != *"$trigger" ]]; then + return 1 + fi + + _fzf_proc_completion "$@" +} + +_fzf_proc_completion() { + _fzf_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <( + command ps -ef | sed 1d + ) +} + +_fzf_proc_completion_post() { + awk '{print $2}' +} + +_fzf_host_completion() { + _fzf_complete +m -- "$@" < <( + command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \ + <(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \ + <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | + awk '{if (length($2) > 0) {print $2}}' | sort -u + ) +} + +_fzf_var_completion() { + _fzf_complete -m -- "$@" < <( + declare -xp | sed 's/=.*//' | sed 's/.* //' + ) +} + +_fzf_alias_completion() { + _fzf_complete -m -- "$@" < <( + alias | sed 's/=.*//' | sed 's/.* //' + ) +} + +# fzf options +complete -o default -F _fzf_opts_completion fzf + +d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}" +a_cmds=" + awk cat diff diff3 + emacs emacsclient ex file ftp g++ gcc gvim head hg java + javac ld less more mvim nvim patch perl python ruby + sed sftp sort source tail tee uniq vi view vim wc xdg-open + basename bunzip2 bzip2 chmod chown curl cp dirname du + find git grep gunzip gzip hg jar + ln ls mv open rm rsync scp + svn tar unzip zip" + +# Preserve existing completion +__fzf_orig_completion < <(complete -p $d_cmds $a_cmds 2> /dev/null) + +if type _completion_loader > /dev/null 2>&1; then + _fzf_completion_loader=1 +fi + +__fzf_defc() { + local cmd func opts orig_var orig def + cmd="$1" + func="$2" + opts="$3" + orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" + orig="${!orig_var}" + if [[ -n "$orig" ]]; then + printf -v def "$orig" "$func" + eval "$def" + else + complete -F "$func" $opts "$cmd" + fi +} + +# Anything +for cmd in $a_cmds; do + __fzf_defc "$cmd" _fzf_path_completion "-o default -o bashdefault" +done + +# Directory +for cmd in $d_cmds; do + __fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames" +done + +# Kill completion (supports empty completion trigger) +complete -F _fzf_complete_kill -o default -o bashdefault kill + +unset cmd d_cmds a_cmds + +_fzf_setup_completion() { + local kind fn cmd + kind=$1 + fn=_fzf_${1}_completion + if [[ $# -lt 2 ]] || ! type -t "$fn" > /dev/null; then + echo "usage: ${FUNCNAME[0]} path|dir|var|alias|host|proc COMMANDS..." + return 1 + fi + shift + __fzf_orig_completion < <(complete -p "$@" 2> /dev/null) + for cmd in "$@"; do + case "$kind" in + dir) __fzf_defc "$cmd" "$fn" "-o nospace -o dirnames" ;; + var) __fzf_defc "$cmd" "$fn" "-o default -o nospace -v" ;; + alias) __fzf_defc "$cmd" "$fn" "-a" ;; + *) __fzf_defc "$cmd" "$fn" "-o default -o bashdefault" ;; + esac + done +} + +# Environment variables / Aliases / Hosts +_fzf_setup_completion 'var' export unset +_fzf_setup_completion 'alias' unalias +_fzf_setup_completion 'host' ssh telnet + +fi diff --git a/fzf/fzf/shell/completion.zsh b/fzf/fzf/shell/completion.zsh new file mode 100644 index 0000000..f12afca --- /dev/null +++ b/fzf/fzf/shell/completion.zsh @@ -0,0 +1,329 @@ +# ____ ____ +# / __/___ / __/ +# / /_/_ / / /_ +# / __/ / /_/ __/ +# /_/ /___/_/ completion.zsh +# +# - $FZF_TMUX (default: 0) +# - $FZF_TMUX_OPTS (default: '-d 40%') +# - $FZF_COMPLETION_TRIGGER (default: '**') +# - $FZF_COMPLETION_OPTS (default: empty) + +# Both branches of the following `if` do the same thing -- define +# __fzf_completion_options such that `eval $__fzf_completion_options` sets +# all options to the same values they currently have. We'll do just that at +# the bottom of the file after changing options to what we prefer. +# +# IMPORTANT: Until we get to the `emulate` line, all words that *can* be quoted +# *must* be quoted in order to prevent alias expansion. In addition, code must +# be written in a way works with any set of zsh options. This is very tricky, so +# careful when you change it. +# +# Start by loading the builtin zsh/parameter module. It provides `options` +# associative array that stores current shell options. +if 'zmodload' 'zsh/parameter' 2>'/dev/null' && (( ${+options} )); then + # This is the fast branch and it gets taken on virtually all Zsh installations. + # + # ${(kv)options[@]} expands to array of keys (option names) and values ("on" + # or "off"). The subsequent expansion# with (j: :) flag joins all elements + # together separated by spaces. __fzf_completion_options ends up with a value + # like this: "options=(shwordsplit off aliases on ...)". + __fzf_completion_options="options=(${(j: :)${(kv)options[@]}})" +else + # This branch is much slower because it forks to get the names of all + # zsh options. It's possible to eliminate this fork but it's not worth the + # trouble because this branch gets taken only on very ancient or broken + # zsh installations. + () { + # That `()` above defines an anonymous function. This is essentially a scope + # for local parameters. We use it to avoid polluting global scope. + 'local' '__fzf_opt' + __fzf_completion_options="setopt" + # `set -o` prints one line for every zsh option. Each line contains option + # name, some spaces, and then either "on" or "off". We just want option names. + # Expansion with (@f) flag splits a string into lines. The outer expansion + # removes spaces and everything that follow them on every line. __fzf_opt + # ends up iterating over option names: shwordsplit, aliases, etc. + for __fzf_opt in "${(@)${(@f)$(set -o)}%% *}"; do + if [[ -o "$__fzf_opt" ]]; then + # Option $__fzf_opt is currently on, so remember to set it back on. + __fzf_completion_options+=" -o $__fzf_opt" + else + # Option $__fzf_opt is currently off, so remember to set it back off. + __fzf_completion_options+=" +o $__fzf_opt" + fi + done + # The value of __fzf_completion_options here looks like this: + # "setopt +o shwordsplit -o aliases ..." + } +fi + +# Enable the default zsh options (those marked with in `man zshoptions`) +# but without `aliases`. Aliases in functions are expanded when functions are +# defined, so if we disable aliases here, we'll be sure to have no pesky +# aliases in any of our functions. This way we won't need prefix every +# command with `command` or to quote every word to defend against global +# aliases. Note that `aliases` is not the only option that's important to +# control. There are several others that could wreck havoc if they are set +# to values we don't expect. With the following `emulate` command we +# sidestep this issue entirely. +'emulate' 'zsh' '-o' 'no_aliases' + +# This brace is the start of try-always block. The `always` part is like +# `finally` in lesser languages. We use it to *always* restore user options. +{ + +# Bail out if not interactive shell. +[[ -o interactive ]] || return 0 + +# To use custom commands instead of find, override _fzf_compgen_{path,dir} +if ! declare -f _fzf_compgen_path > /dev/null; then + _fzf_compgen_path() { + echo "$1" + command find -L "$1" \ + -name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \ + -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' + } +fi + +if ! declare -f _fzf_compgen_dir > /dev/null; then + _fzf_compgen_dir() { + command find -L "$1" \ + -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \ + -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' + } +fi + +########################################################### + +__fzf_comprun() { + if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then + _fzf_comprun "$@" + elif [ -n "$TMUX_PANE" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "$FZF_TMUX_OPTS" ]; }; then + shift + if [ -n "$FZF_TMUX_OPTS" ]; then + fzf-tmux ${(Q)${(Z+n+)FZF_TMUX_OPTS}} -- "$@" + else + fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%} -- "$@" + fi + else + shift + fzf "$@" + fi +} + +# Extract the name of the command. e.g. foo=1 bar baz** +__fzf_extract_command() { + local token tokens + tokens=(${(z)1}) + for token in $tokens; do + token=${(Q)token} + if [[ "$token" =~ [[:alnum:]] && ! "$token" =~ "=" ]]; then + echo "$token" + return + fi + done + echo "${tokens[1]}" +} + +__fzf_generic_path_completion() { + local base lbuf cmd compgen fzf_opts suffix tail dir leftover matches + base=$1 + lbuf=$2 + cmd=$(__fzf_extract_command "$lbuf") + compgen=$3 + fzf_opts=$4 + suffix=$5 + tail=$6 + + setopt localoptions nonomatch + eval "base=$base" + [[ $base = *"/"* ]] && dir="$base" + while [ 1 ]; do + if [[ -z "$dir" || -d ${dir} ]]; then + leftover=${base/#"$dir"} + leftover=${leftover/#\/} + [ -z "$dir" ] && dir='.' + [ "$dir" != "/" ] && dir="${dir/%\//}" + matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" | while read item; do + echo -n "${(q)item}$suffix " + done) + matches=${matches% } + if [ -n "$matches" ]; then + LBUFFER="$lbuf$matches$tail" + fi + zle reset-prompt + break + fi + dir=$(dirname "$dir") + dir=${dir%/}/ + done +} + +_fzf_path_completion() { + __fzf_generic_path_completion "$1" "$2" _fzf_compgen_path \ + "-m" "" " " +} + +_fzf_dir_completion() { + __fzf_generic_path_completion "$1" "$2" _fzf_compgen_dir \ + "" "/" "" +} + +_fzf_feed_fifo() ( + command rm -f "$1" + mkfifo "$1" + cat <&0 > "$1" & +) + +_fzf_complete() { + setopt localoptions ksh_arrays + # Split arguments around -- + local args rest str_arg i sep + args=("$@") + sep= + for i in {0..${#args[@]}}; do + if [[ "${args[$i]}" = -- ]]; then + sep=$i + break + fi + done + if [[ -n "$sep" ]]; then + str_arg= + rest=("${args[@]:$((sep + 1)):${#args[@]}}") + args=("${args[@]:0:$sep}") + else + str_arg=$1 + args=() + shift + rest=("$@") + fi + + local fifo lbuf cmd matches post + fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$" + lbuf=${rest[0]} + cmd=$(__fzf_extract_command "$lbuf") + post="${funcstack[1]}_post" + type $post > /dev/null 2>&1 || post=cat + + _fzf_feed_fifo "$fifo" + matches=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $str_arg" __fzf_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ') + if [ -n "$matches" ]; then + LBUFFER="$lbuf$matches" + fi + command rm -f "$fifo" +} + +_fzf_complete_telnet() { + _fzf_complete +m -- "$@" < <( + command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' | + awk '{if (length($2) > 0) {print $2}}' | sort -u + ) +} + +_fzf_complete_ssh() { + _fzf_complete +m -- "$@" < <( + setopt localoptions nonomatch + command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \ + <(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \ + <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | + awk '{if (length($2) > 0) {print $2}}' | sort -u + ) +} + +_fzf_complete_export() { + _fzf_complete -m -- "$@" < <( + declare -xp | sed 's/=.*//' | sed 's/.* //' + ) +} + +_fzf_complete_unset() { + _fzf_complete -m -- "$@" < <( + declare -xp | sed 's/=.*//' | sed 's/.* //' + ) +} + +_fzf_complete_unalias() { + _fzf_complete +m -- "$@" < <( + alias | sed 's/=.*//' + ) +} + +_fzf_complete_kill() { + _fzf_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <( + command ps -ef | sed 1d + ) +} + +_fzf_complete_kill_post() { + awk '{print $2}' +} + +fzf-completion() { + local tokens cmd prefix trigger tail matches lbuf d_cmds + setopt localoptions noshwordsplit noksh_arrays noposixbuiltins + + # http://zsh.sourceforge.net/FAQ/zshfaq03.html + # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags + tokens=(${(z)LBUFFER}) + if [ ${#tokens} -lt 1 ]; then + zle ${fzf_default_completion:-expand-or-complete} + return + fi + + cmd=$(__fzf_extract_command "$LBUFFER") + + # Explicitly allow for empty trigger. + trigger=${FZF_COMPLETION_TRIGGER-'**'} + [ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("") + + # When the trigger starts with ';', it becomes a separate token + if [[ ${LBUFFER} = *"${tokens[-2]}${tokens[-1]}" ]]; then + tokens[-2]="${tokens[-2]}${tokens[-1]}" + tokens=(${tokens[0,-2]}) + fi + + lbuf=$LBUFFER + tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))} + # Kill completion (do not require trigger sequence) + if [ "$cmd" = kill -a ${LBUFFER[-1]} = ' ' ]; then + tail=$trigger + tokens+=$trigger + lbuf="$lbuf$trigger" + fi + + # Trigger sequence given + if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then + d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}) + + [ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}} + [ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}} + + if eval "type _fzf_complete_${cmd} > /dev/null"; then + prefix="$prefix" eval _fzf_complete_${cmd} ${(q)lbuf} + zle reset-prompt + elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then + _fzf_dir_completion "$prefix" "$lbuf" + else + _fzf_path_completion "$prefix" "$lbuf" + fi + # Fall back to default completion + else + zle ${fzf_default_completion:-expand-or-complete} + fi +} + +[ -z "$fzf_default_completion" ] && { + binding=$(bindkey '^I') + [[ $binding =~ 'undefined-key' ]] || fzf_default_completion=$binding[(s: :w)2] + unset binding +} + +zle -N fzf-completion +bindkey '^I' fzf-completion + +} always { + # Restore the original options. + eval $__fzf_completion_options + 'unset' '__fzf_completion_options' +} diff --git a/fzf/fzf/shell/key-bindings.bash b/fzf/fzf/shell/key-bindings.bash new file mode 100644 index 0000000..e10117e --- /dev/null +++ b/fzf/fzf/shell/key-bindings.bash @@ -0,0 +1,96 @@ +# ____ ____ +# / __/___ / __/ +# / /_/_ / / /_ +# / __/ / /_/ __/ +# /_/ /___/_/ key-bindings.bash +# +# - $FZF_TMUX_OPTS +# - $FZF_CTRL_T_COMMAND +# - $FZF_CTRL_T_OPTS +# - $FZF_CTRL_R_OPTS +# - $FZF_ALT_C_COMMAND +# - $FZF_ALT_C_OPTS + +# Key bindings +# ------------ +__fzf_select__() { + local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ + -o -type f -print \ + -o -type d -print \ + -o -type l -print 2> /dev/null | cut -b3-"}" + eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read -r item; do + printf '%q ' "$item" + done + echo +} + +if [[ $- =~ i ]]; then + +__fzfcmd() { + [[ -n "$TMUX_PANE" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "$FZF_TMUX_OPTS" ]]; } && + echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf" +} + +fzf-file-widget() { + local selected="$(__fzf_select__)" + READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}" + READLINE_POINT=$(( READLINE_POINT + ${#selected} )) +} + +__fzf_cd__() { + local cmd dir + cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ + -o -type d -print 2> /dev/null | cut -b3-"}" + dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m) && printf 'cd -- %q' "$dir" +} + +__fzf_history__() { + local output + output=$( + builtin fc -lnr -2147483648 | + last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -n -l0 -e 'BEGIN { getc; $/ = "\n\t"; $HISTCMD = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCMD - $. . "\t$_" if !$seen{$_}++' | + FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS +m --read0" $(__fzfcmd) --query "$READLINE_LINE" + ) || return + READLINE_LINE=${output#*$'\t'} + if [[ -z "$READLINE_POINT" ]]; then + echo "$READLINE_LINE" + else + READLINE_POINT=0x7fffffff + fi +} + +# Required to refresh the prompt after fzf +bind -m emacs-standard '"\er": redraw-current-line' + +bind -m vi-command '"\C-z": emacs-editing-mode' +bind -m vi-insert '"\C-z": emacs-editing-mode' +bind -m emacs-standard '"\C-z": vi-editing-mode' + +if (( BASH_VERSINFO[0] < 4 )); then + # CTRL-T - Paste the selected file path into the command line + bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"' + bind -m vi-command '"\C-t": "\C-z\C-t\C-z"' + bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"' + + # CTRL-R - Paste the selected command from history into the command line + bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u"$(__fzf_history__)"\e\C-e\er"' + bind -m vi-command '"\C-r": "\C-z\C-r\C-z"' + bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"' +else + # CTRL-T - Paste the selected file path into the command line + bind -m emacs-standard -x '"\C-t": fzf-file-widget' + bind -m vi-command -x '"\C-t": fzf-file-widget' + bind -m vi-insert -x '"\C-t": fzf-file-widget' + + # CTRL-R - Paste the selected command from history into the command line + bind -m emacs-standard -x '"\C-r": __fzf_history__' + bind -m vi-command -x '"\C-r": __fzf_history__' + bind -m vi-insert -x '"\C-r": __fzf_history__' +fi + +# ALT-C - cd into the selected directory +bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"' +bind -m vi-command '"\ec": "\C-z\ec\C-z"' +bind -m vi-insert '"\ec": "\C-z\ec\C-z"' + +fi diff --git a/fzf/fzf/shell/key-bindings.fish b/fzf/fzf/shell/key-bindings.fish new file mode 100644 index 0000000..370ef1b --- /dev/null +++ b/fzf/fzf/shell/key-bindings.fish @@ -0,0 +1,172 @@ +# ____ ____ +# / __/___ / __/ +# / /_/_ / / /_ +# / __/ / /_/ __/ +# /_/ /___/_/ key-bindings.fish +# +# - $FZF_TMUX_OPTS +# - $FZF_CTRL_T_COMMAND +# - $FZF_CTRL_T_OPTS +# - $FZF_CTRL_R_OPTS +# - $FZF_ALT_C_COMMAND +# - $FZF_ALT_C_OPTS + +# Key bindings +# ------------ +function fzf_key_bindings + + # Store current token in $dir as root for the 'find' command + function fzf-file-widget -d "List files and folders" + set -l commandline (__fzf_parse_commandline) + set -l dir $commandline[1] + set -l fzf_query $commandline[2] + set -l prefix $commandline[3] + + # "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not + # $dir itself, even if hidden. + test -n "$FZF_CTRL_T_COMMAND"; or set -l FZF_CTRL_T_COMMAND " + command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ + -o -type f -print \ + -o -type d -print \ + -o -type l -print 2> /dev/null | sed 's@^\./@@'" + + test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% + begin + set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" + eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end + end + if [ -z "$result" ] + commandline -f repaint + return + else + # Remove last token from commandline. + commandline -t "" + end + for i in $result + commandline -it -- $prefix + commandline -it -- (string escape $i) + commandline -it -- ' ' + end + commandline -f repaint + end + + function fzf-history-widget -d "Show command history" + test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% + begin + set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --tiebreak=index --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS +m" + + set -l FISH_MAJOR (echo $version | cut -f1 -d.) + set -l FISH_MINOR (echo $version | cut -f2 -d.) + + # history's -z flag is needed for multi-line support. + # history's -z flag was added in fish 2.4.0, so don't use it for versions + # before 2.4.0. + if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ]; + history -z | eval (__fzfcmd) --read0 --print0 -q '(commandline)' | read -lz result + and commandline -- $result + else + history | eval (__fzfcmd) -q '(commandline)' | read -l result + and commandline -- $result + end + end + commandline -f repaint + end + + function fzf-cd-widget -d "Change directory" + set -l commandline (__fzf_parse_commandline) + set -l dir $commandline[1] + set -l fzf_query $commandline[2] + set -l prefix $commandline[3] + + test -n "$FZF_ALT_C_COMMAND"; or set -l FZF_ALT_C_COMMAND " + command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ + -o -type d -print 2> /dev/null | sed 's@^\./@@'" + test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% + begin + set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" + eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result + + if [ -n "$result" ] + cd -- $result + + # Remove last token from commandline. + commandline -t "" + commandline -it -- $prefix + end + end + + commandline -f repaint + end + + function __fzfcmd + test -n "$FZF_TMUX"; or set FZF_TMUX 0 + test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% + if [ -n "$FZF_TMUX_OPTS" ] + echo "fzf-tmux $FZF_TMUX_OPTS -- " + else if [ $FZF_TMUX -eq 1 ] + echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- " + else + echo "fzf" + end + end + + bind \ct fzf-file-widget + bind \cr fzf-history-widget + bind \ec fzf-cd-widget + + if bind -M insert > /dev/null 2>&1 + bind -M insert \ct fzf-file-widget + bind -M insert \cr fzf-history-widget + bind -M insert \ec fzf-cd-widget + end + + function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix' + set -l commandline (commandline -t) + + # strip -option= from token if present + set -l prefix (string match -r -- '^-[^\s=]+=' $commandline) + set commandline (string replace -- "$prefix" '' $commandline) + + # eval is used to do shell expansion on paths + eval set commandline $commandline + + if [ -z $commandline ] + # Default to current directory with no --query + set dir '.' + set fzf_query '' + else + set dir (__fzf_get_dir $commandline) + + if [ "$dir" = "." -a (string sub -l 1 -- $commandline) != '.' ] + # if $dir is "." but commandline is not a relative path, this means no file path found + set fzf_query $commandline + else + # Also remove trailing slash after dir, to "split" input properly + set fzf_query (string replace -r "^$dir/?" -- '' "$commandline") + end + end + + echo $dir + echo $fzf_query + echo $prefix + end + + function __fzf_get_dir -d 'Find the longest existing filepath from input string' + set dir $argv + + # Strip all trailing slashes. Ignore if $dir is root dir (/) + if [ (string length -- $dir) -gt 1 ] + set dir (string replace -r '/*$' -- '' $dir) + end + + # Iteratively check if dir exists and strip tail end of path + while [ ! -d "$dir" ] + # If path is absolute, this can keep going until ends up at / + # If path is relative, this can keep going until entire input is consumed, dirname returns "." + set dir (dirname -- "$dir") + end + + echo $dir + end + +end diff --git a/fzf/fzf/shell/key-bindings.zsh b/fzf/fzf/shell/key-bindings.zsh new file mode 100644 index 0000000..032cd43 --- /dev/null +++ b/fzf/fzf/shell/key-bindings.zsh @@ -0,0 +1,120 @@ +# ____ ____ +# / __/___ / __/ +# / /_/_ / / /_ +# / __/ / /_/ __/ +# /_/ /___/_/ key-bindings.zsh +# +# - $FZF_TMUX_OPTS +# - $FZF_CTRL_T_COMMAND +# - $FZF_CTRL_T_OPTS +# - $FZF_CTRL_R_OPTS +# - $FZF_ALT_C_COMMAND +# - $FZF_ALT_C_OPTS + +# Key bindings +# ------------ + +# The code at the top and the bottom of this file is the same as in completion.zsh. +# Refer to that file for explanation. +if 'zmodload' 'zsh/parameter' 2>'/dev/null' && (( ${+options} )); then + __fzf_key_bindings_options="options=(${(j: :)${(kv)options[@]}})" +else + () { + __fzf_key_bindings_options="setopt" + 'local' '__fzf_opt' + for __fzf_opt in "${(@)${(@f)$(set -o)}%% *}"; do + if [[ -o "$__fzf_opt" ]]; then + __fzf_key_bindings_options+=" -o $__fzf_opt" + else + __fzf_key_bindings_options+=" +o $__fzf_opt" + fi + done + } +fi + +'emulate' 'zsh' '-o' 'no_aliases' + +{ + +[[ -o interactive ]] || return 0 + +# CTRL-T - Paste the selected file path(s) into the command line +__fsel() { + local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ + -o -type f -print \ + -o -type d -print \ + -o -type l -print 2> /dev/null | cut -b3-"}" + setopt localoptions pipefail no_aliases 2> /dev/null + local item + eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read item; do + echo -n "${(q)item} " + done + local ret=$? + echo + return $ret +} + +__fzfcmd() { + [ -n "$TMUX_PANE" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "$FZF_TMUX_OPTS" ]; } && + echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf" +} + +fzf-file-widget() { + LBUFFER="${LBUFFER}$(__fsel)" + local ret=$? + zle reset-prompt + return $ret +} +zle -N fzf-file-widget +bindkey -M emacs '^T' fzf-file-widget +bindkey -M vicmd '^T' fzf-file-widget +bindkey -M viins '^T' fzf-file-widget + +# ALT-C - cd into the selected directory +fzf-cd-widget() { + local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ + -o -type d -print 2> /dev/null | cut -b3-"}" + setopt localoptions pipefail no_aliases 2> /dev/null + local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m)" + if [[ -z "$dir" ]]; then + zle redisplay + return 0 + fi + zle push-line # Clear buffer. Auto-restored on next prompt. + BUFFER="cd -- ${(q)dir}" + zle accept-line + local ret=$? + unset dir # ensure this doesn't end up appearing in prompt expansion + zle reset-prompt + return $ret +} +zle -N fzf-cd-widget +bindkey -M emacs '\ec' fzf-cd-widget +bindkey -M vicmd '\ec' fzf-cd-widget +bindkey -M viins '\ec' fzf-cd-widget + +# CTRL-R - Paste the selected command from history into the command line +fzf-history-widget() { + local selected num + setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null + selected=( $(fc -rl 1 | perl -ne 'print if !$seen{(/^\s*[0-9]+\**\s+(.*)/, $1)}++' | + FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) ) + local ret=$? + if [ -n "$selected" ]; then + num=$selected[1] + if [ -n "$num" ]; then + zle vi-fetch-history -n $num + fi + fi + zle reset-prompt + return $ret +} +zle -N fzf-history-widget +bindkey -M emacs '^R' fzf-history-widget +bindkey -M vicmd '^R' fzf-history-widget +bindkey -M viins '^R' fzf-history-widget + +} always { + eval $__fzf_key_bindings_options + 'unset' '__fzf_key_bindings_options' +} diff --git a/fzf/fzf/src/LICENSE b/fzf/fzf/src/LICENSE new file mode 100644 index 0000000..50aa5d9 --- /dev/null +++ b/fzf/fzf/src/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013-2021 Junegunn Choi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/fzf/fzf/src/algo/algo.go b/fzf/fzf/src/algo/algo.go new file mode 100644 index 0000000..40fb2af --- /dev/null +++ b/fzf/fzf/src/algo/algo.go @@ -0,0 +1,884 @@ +package algo + +/* + +Algorithm +--------- + +FuzzyMatchV1 finds the first "fuzzy" occurrence of the pattern within the given +text in O(n) time where n is the length of the text. Once the position of the +last character is located, it traverses backwards to see if there's a shorter +substring that matches the pattern. + + a_____b___abc__ To find "abc" + *-----*-----*> 1. Forward scan + <*** 2. Backward scan + +The algorithm is simple and fast, but as it only sees the first occurrence, +it is not guaranteed to find the occurrence with the highest score. + + a_____b__c__abc + *-----*--* *** + +FuzzyMatchV2 implements a modified version of Smith-Waterman algorithm to find +the optimal solution (highest score) according to the scoring criteria. Unlike +the original algorithm, omission or mismatch of a character in the pattern is +not allowed. + +Performance +----------- + +The new V2 algorithm is slower than V1 as it examines all occurrences of the +pattern instead of stopping immediately after finding the first one. The time +complexity of the algorithm is O(nm) if a match is found and O(n) otherwise +where n is the length of the item and m is the length of the pattern. Thus, the +performance overhead may not be noticeable for a query with high selectivity. +However, if the performance is more important than the quality of the result, +you can still choose v1 algorithm with --algo=v1. + +Scoring criteria +---------------- + +- We prefer matches at special positions, such as the start of a word, or + uppercase character in camelCase words. + +- That is, we prefer an occurrence of the pattern with more characters + matching at special positions, even if the total match length is longer. + e.g. "fuzzyfinder" vs. "fuzzy-finder" on "ff" + ```````````` +- Also, if the first character in the pattern appears at one of the special + positions, the bonus point for the position is multiplied by a constant + as it is extremely likely that the first character in the typed pattern + has more significance than the rest. + e.g. "fo-bar" vs. "foob-r" on "br" + `````` +- But since fzf is still a fuzzy finder, not an acronym finder, we should also + consider the total length of the matched substring. This is why we have the + gap penalty. The gap penalty increases as the length of the gap (distance + between the matching characters) increases, so the effect of the bonus is + eventually cancelled at some point. + e.g. "fuzzyfinder" vs. "fuzzy-blurry-finder" on "ff" + ``````````` +- Consequently, it is crucial to find the right balance between the bonus + and the gap penalty. The parameters were chosen that the bonus is cancelled + when the gap size increases beyond 8 characters. + +- The bonus mechanism can have the undesirable side effect where consecutive + matches are ranked lower than the ones with gaps. + e.g. "foobar" vs. "foo-bar" on "foob" + ``````` +- To correct this anomaly, we also give extra bonus point to each character + in a consecutive matching chunk. + e.g. "foobar" vs. "foo-bar" on "foob" + `````` +- The amount of consecutive bonus is primarily determined by the bonus of the + first character in the chunk. + e.g. "foobar" vs. "out-of-bound" on "oob" + ```````````` +*/ + +import ( + "bytes" + "fmt" + "strings" + "unicode" + "unicode/utf8" + + "github.com/junegunn/fzf/src/util" +) + +var DEBUG bool + +func indexAt(index int, max int, forward bool) int { + if forward { + return index + } + return max - index - 1 +} + +// Result contains the results of running a match function. +type Result struct { + // TODO int32 should suffice + Start int + End int + Score int +} + +const ( + scoreMatch = 16 + scoreGapStart = -3 + scoreGapExtension = -1 + + // We prefer matches at the beginning of a word, but the bonus should not be + // too great to prevent the longer acronym matches from always winning over + // shorter fuzzy matches. The bonus point here was specifically chosen that + // the bonus is cancelled when the gap between the acronyms grows over + // 8 characters, which is approximately the average length of the words found + // in web2 dictionary and my file system. + bonusBoundary = scoreMatch / 2 + + // Although bonus point for non-word characters is non-contextual, we need it + // for computing bonus points for consecutive chunks starting with a non-word + // character. + bonusNonWord = scoreMatch / 2 + + // Edge-triggered bonus for matches in camelCase words. + // Compared to word-boundary case, they don't accompany single-character gaps + // (e.g. FooBar vs. foo-bar), so we deduct bonus point accordingly. + bonusCamel123 = bonusBoundary + scoreGapExtension + + // Minimum bonus point given to characters in consecutive chunks. + // Note that bonus points for consecutive matches shouldn't have needed if we + // used fixed match score as in the original algorithm. + bonusConsecutive = -(scoreGapStart + scoreGapExtension) + + // The first character in the typed pattern usually has more significance + // than the rest so it's important that it appears at special positions where + // bonus points are given, e.g. "to-go" vs. "ongoing" on "og" or on "ogo". + // The amount of the extra bonus should be limited so that the gap penalty is + // still respected. + bonusFirstCharMultiplier = 2 +) + +type charClass int + +const ( + charNonWord charClass = iota + charLower + charUpper + charLetter + charNumber +) + +func posArray(withPos bool, len int) *[]int { + if withPos { + pos := make([]int, 0, len) + return &pos + } + return nil +} + +func alloc16(offset int, slab *util.Slab, size int) (int, []int16) { + if slab != nil && cap(slab.I16) > offset+size { + slice := slab.I16[offset : offset+size] + return offset + size, slice + } + return offset, make([]int16, size) +} + +func alloc32(offset int, slab *util.Slab, size int) (int, []int32) { + if slab != nil && cap(slab.I32) > offset+size { + slice := slab.I32[offset : offset+size] + return offset + size, slice + } + return offset, make([]int32, size) +} + +func charClassOfAscii(char rune) charClass { + if char >= 'a' && char <= 'z' { + return charLower + } else if char >= 'A' && char <= 'Z' { + return charUpper + } else if char >= '0' && char <= '9' { + return charNumber + } + return charNonWord +} + +func charClassOfNonAscii(char rune) charClass { + if unicode.IsLower(char) { + return charLower + } else if unicode.IsUpper(char) { + return charUpper + } else if unicode.IsNumber(char) { + return charNumber + } else if unicode.IsLetter(char) { + return charLetter + } + return charNonWord +} + +func charClassOf(char rune) charClass { + if char <= unicode.MaxASCII { + return charClassOfAscii(char) + } + return charClassOfNonAscii(char) +} + +func bonusFor(prevClass charClass, class charClass) int16 { + if prevClass == charNonWord && class != charNonWord { + // Word boundary + return bonusBoundary + } else if prevClass == charLower && class == charUpper || + prevClass != charNumber && class == charNumber { + // camelCase letter123 + return bonusCamel123 + } else if class == charNonWord { + return bonusNonWord + } + return 0 +} + +func bonusAt(input *util.Chars, idx int) int16 { + if idx == 0 { + return bonusBoundary + } + return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx))) +} + +func normalizeRune(r rune) rune { + if r < 0x00C0 || r > 0x2184 { + return r + } + + n := normalized[r] + if n > 0 { + return n + } + return r +} + +// Algo functions make two assumptions +// 1. "pattern" is given in lowercase if "caseSensitive" is false +// 2. "pattern" is already normalized if "normalize" is true +type Algo func(caseSensitive bool, normalize bool, forward bool, input *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) + +func trySkip(input *util.Chars, caseSensitive bool, b byte, from int) int { + byteArray := input.Bytes()[from:] + idx := bytes.IndexByte(byteArray, b) + if idx == 0 { + // Can't skip any further + return from + } + // We may need to search for the uppercase letter again. We don't have to + // consider normalization as we can be sure that this is an ASCII string. + if !caseSensitive && b >= 'a' && b <= 'z' { + if idx > 0 { + byteArray = byteArray[:idx] + } + uidx := bytes.IndexByte(byteArray, b-32) + if uidx >= 0 { + idx = uidx + } + } + if idx < 0 { + return -1 + } + return from + idx +} + +func isAscii(runes []rune) bool { + for _, r := range runes { + if r >= utf8.RuneSelf { + return false + } + } + return true +} + +func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) int { + // Can't determine + if !input.IsBytes() { + return 0 + } + + // Not possible + if !isAscii(pattern) { + return -1 + } + + firstIdx, idx := 0, 0 + for pidx := 0; pidx < len(pattern); pidx++ { + idx = trySkip(input, caseSensitive, byte(pattern[pidx]), idx) + if idx < 0 { + return -1 + } + if pidx == 0 && idx > 0 { + // Step back to find the right bonus point + firstIdx = idx - 1 + } + idx++ + } + return firstIdx +} + +func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []int16) { + width := lastIdx - int(F[0]) + 1 + + for i, f := range F { + I := i * width + if i == 0 { + fmt.Print(" ") + for j := int(f); j <= lastIdx; j++ { + fmt.Printf(" " + string(T[j]) + " ") + } + fmt.Println() + } + fmt.Print(string(pattern[i]) + " ") + for idx := int(F[0]); idx < int(f); idx++ { + fmt.Print(" 0 ") + } + for idx := int(f); idx <= lastIdx; idx++ { + fmt.Printf("%2d ", H[i*width+idx-int(F[0])]) + } + fmt.Println() + + fmt.Print(" ") + for idx, p := range C[I : I+width] { + if idx+int(F[0]) < int(F[i]) { + p = 0 + } + if p > 0 { + fmt.Printf("%2d ", p) + } else { + fmt.Print(" ") + } + } + fmt.Println() + } +} + +func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { + // Assume that pattern is given in lowercase if case-insensitive. + // First check if there's a match and calculate bonus for each position. + // If the input string is too long, consider finding the matching chars in + // this phase as well (non-optimal alignment). + M := len(pattern) + if M == 0 { + return Result{0, 0, 0}, posArray(withPos, M) + } + N := input.Length() + + // Since O(nm) algorithm can be prohibitively expensive for large input, + // we fall back to the greedy algorithm. + if slab != nil && N*M > cap(slab.I16) { + return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab) + } + + // Phase 1. Optimized search for ASCII string + idx := asciiFuzzyIndex(input, pattern, caseSensitive) + if idx < 0 { + return Result{-1, -1, 0}, nil + } + + // Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages + offset16 := 0 + offset32 := 0 + offset16, H0 := alloc16(offset16, slab, N) + offset16, C0 := alloc16(offset16, slab, N) + // Bonus point for each position + offset16, B := alloc16(offset16, slab, N) + // The first occurrence of each character in the pattern + offset32, F := alloc32(offset32, slab, M) + // Rune array + _, T := alloc32(offset32, slab, N) + input.CopyRunes(T) + + // Phase 2. Calculate bonus for each point + maxScore, maxScorePos := int16(0), 0 + pidx, lastIdx := 0, 0 + pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), charNonWord, false + Tsub := T[idx:] + H0sub, C0sub, Bsub := H0[idx:][:len(Tsub)], C0[idx:][:len(Tsub)], B[idx:][:len(Tsub)] + for off, char := range Tsub { + var class charClass + if char <= unicode.MaxASCII { + class = charClassOfAscii(char) + if !caseSensitive && class == charUpper { + char += 32 + } + } else { + class = charClassOfNonAscii(char) + if !caseSensitive && class == charUpper { + char = unicode.To(unicode.LowerCase, char) + } + if normalize { + char = normalizeRune(char) + } + } + + Tsub[off] = char + bonus := bonusFor(prevClass, class) + Bsub[off] = bonus + prevClass = class + + if char == pchar { + if pidx < M { + F[pidx] = int32(idx + off) + pidx++ + pchar = pattern[util.Min(pidx, M-1)] + } + lastIdx = idx + off + } + + if char == pchar0 { + score := scoreMatch + bonus*bonusFirstCharMultiplier + H0sub[off] = score + C0sub[off] = 1 + if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) { + maxScore, maxScorePos = score, idx+off + if forward && bonus == bonusBoundary { + break + } + } + inGap = false + } else { + if inGap { + H0sub[off] = util.Max16(prevH0+scoreGapExtension, 0) + } else { + H0sub[off] = util.Max16(prevH0+scoreGapStart, 0) + } + C0sub[off] = 0 + inGap = true + } + prevH0 = H0sub[off] + } + if pidx != M { + return Result{-1, -1, 0}, nil + } + if M == 1 { + result := Result{maxScorePos, maxScorePos + 1, int(maxScore)} + if !withPos { + return result, nil + } + pos := []int{maxScorePos} + return result, &pos + } + + // Phase 3. Fill in score matrix (H) + // Unlike the original algorithm, we do not allow omission. + f0 := int(F[0]) + width := lastIdx - f0 + 1 + offset16, H := alloc16(offset16, slab, width*M) + copy(H, H0[f0:lastIdx+1]) + + // Possible length of consecutive chunk at each position. + _, C := alloc16(offset16, slab, width*M) + copy(C, C0[f0:lastIdx+1]) + + Fsub := F[1:] + Psub := pattern[1:][:len(Fsub)] + for off, f := range Fsub { + f := int(f) + pchar := Psub[off] + pidx := off + 1 + row := pidx * width + inGap := false + Tsub := T[f : lastIdx+1] + Bsub := B[f:][:len(Tsub)] + Csub := C[row+f-f0:][:len(Tsub)] + Cdiag := C[row+f-f0-1-width:][:len(Tsub)] + Hsub := H[row+f-f0:][:len(Tsub)] + Hdiag := H[row+f-f0-1-width:][:len(Tsub)] + Hleft := H[row+f-f0-1:][:len(Tsub)] + Hleft[0] = 0 + for off, char := range Tsub { + col := off + f + var s1, s2, consecutive int16 + + if inGap { + s2 = Hleft[off] + scoreGapExtension + } else { + s2 = Hleft[off] + scoreGapStart + } + + if pchar == char { + s1 = Hdiag[off] + scoreMatch + b := Bsub[off] + consecutive = Cdiag[off] + 1 + // Break consecutive chunk + if b == bonusBoundary { + consecutive = 1 + } else if consecutive > 1 { + b = util.Max16(b, util.Max16(bonusConsecutive, B[col-int(consecutive)+1])) + } + if s1+b < s2 { + s1 += Bsub[off] + consecutive = 0 + } else { + s1 += b + } + } + Csub[off] = consecutive + + inGap = s1 < s2 + score := util.Max16(util.Max16(s1, s2), 0) + if pidx == M-1 && (forward && score > maxScore || !forward && score >= maxScore) { + maxScore, maxScorePos = score, col + } + Hsub[off] = score + } + } + + if DEBUG { + debugV2(T, pattern, F, lastIdx, H, C) + } + + // Phase 4. (Optional) Backtrace to find character positions + pos := posArray(withPos, M) + j := f0 + if withPos { + i := M - 1 + j = maxScorePos + preferMatch := true + for { + I := i * width + j0 := j - f0 + s := H[I+j0] + + var s1, s2 int16 + if i > 0 && j >= int(F[i]) { + s1 = H[I-width+j0-1] + } + if j > int(F[i]) { + s2 = H[I+j0-1] + } + + if s > s1 && (s > s2 || s == s2 && preferMatch) { + *pos = append(*pos, j) + if i == 0 { + break + } + i-- + } + preferMatch = C[I+j0] > 1 || I+width+j0+1 < len(C) && C[I+width+j0+1] > 0 + j-- + } + } + // Start offset we return here is only relevant when begin tiebreak is used. + // However finding the accurate offset requires backtracking, and we don't + // want to pay extra cost for the option that has lost its importance. + return Result{j, maxScorePos + 1, int(maxScore)}, pos +} + +// Implement the same sorting criteria as V2 +func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) { + pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0) + pos := posArray(withPos, len(pattern)) + prevClass := charNonWord + if sidx > 0 { + prevClass = charClassOf(text.Get(sidx - 1)) + } + for idx := sidx; idx < eidx; idx++ { + char := text.Get(idx) + class := charClassOf(char) + if !caseSensitive { + if char >= 'A' && char <= 'Z' { + char += 32 + } else if char > unicode.MaxASCII { + char = unicode.To(unicode.LowerCase, char) + } + } + // pattern is already normalized + if normalize { + char = normalizeRune(char) + } + if char == pattern[pidx] { + if withPos { + *pos = append(*pos, idx) + } + score += scoreMatch + bonus := bonusFor(prevClass, class) + if consecutive == 0 { + firstBonus = bonus + } else { + // Break consecutive chunk + if bonus == bonusBoundary { + firstBonus = bonus + } + bonus = util.Max16(util.Max16(bonus, firstBonus), bonusConsecutive) + } + if pidx == 0 { + score += int(bonus * bonusFirstCharMultiplier) + } else { + score += int(bonus) + } + inGap = false + consecutive++ + pidx++ + } else { + if inGap { + score += scoreGapExtension + } else { + score += scoreGapStart + } + inGap = true + consecutive = 0 + firstBonus = 0 + } + prevClass = class + } + return score, pos +} + +// FuzzyMatchV1 performs fuzzy-match +func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { + if len(pattern) == 0 { + return Result{0, 0, 0}, nil + } + if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 { + return Result{-1, -1, 0}, nil + } + + pidx := 0 + sidx := -1 + eidx := -1 + + lenRunes := text.Length() + lenPattern := len(pattern) + + for index := 0; index < lenRunes; index++ { + char := text.Get(indexAt(index, lenRunes, forward)) + // This is considerably faster than blindly applying strings.ToLower to the + // whole string + if !caseSensitive { + // Partially inlining `unicode.ToLower`. Ugly, but makes a noticeable + // difference in CPU cost. (Measured on Go 1.4.1. Also note that the Go + // compiler as of now does not inline non-leaf functions.) + if char >= 'A' && char <= 'Z' { + char += 32 + } else if char > unicode.MaxASCII { + char = unicode.To(unicode.LowerCase, char) + } + } + if normalize { + char = normalizeRune(char) + } + pchar := pattern[indexAt(pidx, lenPattern, forward)] + if char == pchar { + if sidx < 0 { + sidx = index + } + if pidx++; pidx == lenPattern { + eidx = index + 1 + break + } + } + } + + if sidx >= 0 && eidx >= 0 { + pidx-- + for index := eidx - 1; index >= sidx; index-- { + tidx := indexAt(index, lenRunes, forward) + char := text.Get(tidx) + if !caseSensitive { + if char >= 'A' && char <= 'Z' { + char += 32 + } else if char > unicode.MaxASCII { + char = unicode.To(unicode.LowerCase, char) + } + } + + pidx_ := indexAt(pidx, lenPattern, forward) + pchar := pattern[pidx_] + if char == pchar { + if pidx--; pidx < 0 { + sidx = index + break + } + } + } + + if !forward { + sidx, eidx = lenRunes-eidx, lenRunes-sidx + } + + score, pos := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, withPos) + return Result{sidx, eidx, score}, pos + } + return Result{-1, -1, 0}, nil +} + +// ExactMatchNaive is a basic string searching algorithm that handles case +// sensitivity. Although naive, it still performs better than the combination +// of strings.ToLower + strings.Index for typical fzf use cases where input +// strings and patterns are not very long. +// +// Since 0.15.0, this function searches for the match with the highest +// bonus point, instead of stopping immediately after finding the first match. +// The solution is much cheaper since there is only one possible alignment of +// the pattern. +func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { + if len(pattern) == 0 { + return Result{0, 0, 0}, nil + } + + lenRunes := text.Length() + lenPattern := len(pattern) + + if lenRunes < lenPattern { + return Result{-1, -1, 0}, nil + } + + if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 { + return Result{-1, -1, 0}, nil + } + + // For simplicity, only look at the bonus at the first character position + pidx := 0 + bestPos, bonus, bestBonus := -1, int16(0), int16(-1) + for index := 0; index < lenRunes; index++ { + index_ := indexAt(index, lenRunes, forward) + char := text.Get(index_) + if !caseSensitive { + if char >= 'A' && char <= 'Z' { + char += 32 + } else if char > unicode.MaxASCII { + char = unicode.To(unicode.LowerCase, char) + } + } + if normalize { + char = normalizeRune(char) + } + pidx_ := indexAt(pidx, lenPattern, forward) + pchar := pattern[pidx_] + if pchar == char { + if pidx_ == 0 { + bonus = bonusAt(text, index_) + } + pidx++ + if pidx == lenPattern { + if bonus > bestBonus { + bestPos, bestBonus = index, bonus + } + if bonus == bonusBoundary { + break + } + index -= pidx - 1 + pidx, bonus = 0, 0 + } + } else { + index -= pidx + pidx, bonus = 0, 0 + } + } + if bestPos >= 0 { + var sidx, eidx int + if forward { + sidx = bestPos - lenPattern + 1 + eidx = bestPos + 1 + } else { + sidx = lenRunes - (bestPos + 1) + eidx = lenRunes - (bestPos - lenPattern + 1) + } + score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false) + return Result{sidx, eidx, score}, nil + } + return Result{-1, -1, 0}, nil +} + +// PrefixMatch performs prefix-match +func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { + if len(pattern) == 0 { + return Result{0, 0, 0}, nil + } + + trimmedLen := 0 + if !unicode.IsSpace(pattern[0]) { + trimmedLen = text.LeadingWhitespaces() + } + + if text.Length()-trimmedLen < len(pattern) { + return Result{-1, -1, 0}, nil + } + + for index, r := range pattern { + char := text.Get(trimmedLen + index) + if !caseSensitive { + char = unicode.ToLower(char) + } + if normalize { + char = normalizeRune(char) + } + if char != r { + return Result{-1, -1, 0}, nil + } + } + lenPattern := len(pattern) + score, _ := calculateScore(caseSensitive, normalize, text, pattern, trimmedLen, trimmedLen+lenPattern, false) + return Result{trimmedLen, trimmedLen + lenPattern, score}, nil +} + +// SuffixMatch performs suffix-match +func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { + lenRunes := text.Length() + trimmedLen := lenRunes + if len(pattern) == 0 || !unicode.IsSpace(pattern[len(pattern)-1]) { + trimmedLen -= text.TrailingWhitespaces() + } + if len(pattern) == 0 { + return Result{trimmedLen, trimmedLen, 0}, nil + } + diff := trimmedLen - len(pattern) + if diff < 0 { + return Result{-1, -1, 0}, nil + } + + for index, r := range pattern { + char := text.Get(index + diff) + if !caseSensitive { + char = unicode.ToLower(char) + } + if normalize { + char = normalizeRune(char) + } + if char != r { + return Result{-1, -1, 0}, nil + } + } + lenPattern := len(pattern) + sidx := trimmedLen - lenPattern + eidx := trimmedLen + score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false) + return Result{sidx, eidx, score}, nil +} + +// EqualMatch performs equal-match +func EqualMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { + lenPattern := len(pattern) + if lenPattern == 0 { + return Result{-1, -1, 0}, nil + } + + // Strip leading whitespaces + trimmedLen := 0 + if !unicode.IsSpace(pattern[0]) { + trimmedLen = text.LeadingWhitespaces() + } + + // Strip trailing whitespaces + trimmedEndLen := 0 + if !unicode.IsSpace(pattern[lenPattern-1]) { + trimmedEndLen = text.TrailingWhitespaces() + } + + if text.Length()-trimmedLen-trimmedEndLen != lenPattern { + return Result{-1, -1, 0}, nil + } + match := true + if normalize { + runes := text.ToRunes() + for idx, pchar := range pattern { + char := runes[trimmedLen+idx] + if !caseSensitive { + char = unicode.To(unicode.LowerCase, char) + } + if normalizeRune(pchar) != normalizeRune(char) { + match = false + break + } + } + } else { + runes := text.ToRunes() + runesStr := string(runes[trimmedLen : len(runes)-trimmedEndLen]) + if !caseSensitive { + runesStr = strings.ToLower(runesStr) + } + match = runesStr == string(pattern) + } + if match { + return Result{trimmedLen, trimmedLen + lenPattern, (scoreMatch+bonusBoundary)*lenPattern + + (bonusFirstCharMultiplier-1)*bonusBoundary}, nil + } + return Result{-1, -1, 0}, nil +} diff --git a/fzf/fzf/src/algo/algo_test.go b/fzf/fzf/src/algo/algo_test.go new file mode 100644 index 0000000..218ca1f --- /dev/null +++ b/fzf/fzf/src/algo/algo_test.go @@ -0,0 +1,197 @@ +package algo + +import ( + "math" + "sort" + "strings" + "testing" + + "github.com/junegunn/fzf/src/util" +) + +func assertMatch(t *testing.T, fun Algo, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, score int) { + assertMatch2(t, fun, caseSensitive, false, forward, input, pattern, sidx, eidx, score) +} + +func assertMatch2(t *testing.T, fun Algo, caseSensitive, normalize, forward bool, input, pattern string, sidx int, eidx int, score int) { + if !caseSensitive { + pattern = strings.ToLower(pattern) + } + chars := util.ToChars([]byte(input)) + res, pos := fun(caseSensitive, normalize, forward, &chars, []rune(pattern), true, nil) + var start, end int + if pos == nil || len(*pos) == 0 { + start = res.Start + end = res.End + } else { + sort.Ints(*pos) + start = (*pos)[0] + end = (*pos)[len(*pos)-1] + 1 + } + if start != sidx { + t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", start, sidx, input, pattern) + } + if end != eidx { + t.Errorf("Invalid end index: %d (expected: %d, %s / %s)", end, eidx, input, pattern) + } + if res.Score != score { + t.Errorf("Invalid score: %d (expected: %d, %s / %s)", res.Score, score, input, pattern) + } +} + +func TestFuzzyMatch(t *testing.T) { + for _, fn := range []Algo{FuzzyMatchV1, FuzzyMatchV2} { + for _, forward := range []bool{true, false} { + assertMatch(t, fn, false, forward, "fooBarbaz1", "oBZ", 2, 9, + scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3) + assertMatch(t, fn, false, forward, "foo bar baz", "fbb", 0, 9, + scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+ + bonusBoundary*2+2*scoreGapStart+4*scoreGapExtension) + assertMatch(t, fn, false, forward, "/AutomatorDocument.icns", "rdoc", 9, 13, + scoreMatch*4+bonusCamel123+bonusConsecutive*2) + assertMatch(t, fn, false, forward, "/man1/zshcompctl.1", "zshc", 6, 10, + scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3) + assertMatch(t, fn, false, forward, "/.oh-my-zsh/cache", "zshc", 8, 13, + scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3+scoreGapStart) + assertMatch(t, fn, false, forward, "ab0123 456", "12356", 3, 10, + scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtension) + assertMatch(t, fn, false, forward, "abc123 456", "12356", 3, 10, + scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtension) + assertMatch(t, fn, false, forward, "foo/bar/baz", "fbb", 0, 9, + scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+ + bonusBoundary*2+2*scoreGapStart+4*scoreGapExtension) + assertMatch(t, fn, false, forward, "fooBarBaz", "fbb", 0, 7, + scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+ + bonusCamel123*2+2*scoreGapStart+2*scoreGapExtension) + assertMatch(t, fn, false, forward, "foo barbaz", "fbb", 0, 8, + scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary+ + scoreGapStart*2+scoreGapExtension*3) + assertMatch(t, fn, false, forward, "fooBar Baz", "foob", 0, 4, + scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3) + assertMatch(t, fn, false, forward, "xFoo-Bar Baz", "foo-b", 1, 6, + scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+ + bonusNonWord+bonusBoundary) + + assertMatch(t, fn, true, forward, "fooBarbaz", "oBz", 2, 9, + scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3) + assertMatch(t, fn, true, forward, "Foo/Bar/Baz", "FBB", 0, 9, + scoreMatch*3+bonusBoundary*(bonusFirstCharMultiplier+2)+ + scoreGapStart*2+scoreGapExtension*4) + assertMatch(t, fn, true, forward, "FooBarBaz", "FBB", 0, 7, + scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusCamel123*2+ + scoreGapStart*2+scoreGapExtension*2) + assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4, + scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+ + util.Max(bonusCamel123, bonusBoundary)) + + // Consecutive bonus updated + assertMatch(t, fn, true, forward, "foo-bar", "o-ba", 2, 6, + scoreMatch*4+bonusBoundary*3) + + // Non-match + assertMatch(t, fn, true, forward, "fooBarbaz", "oBZ", -1, -1, 0) + assertMatch(t, fn, true, forward, "Foo Bar Baz", "fbb", -1, -1, 0) + assertMatch(t, fn, true, forward, "fooBarbaz", "fooBarbazz", -1, -1, 0) + } + } +} + +func TestFuzzyMatchBackward(t *testing.T) { + assertMatch(t, FuzzyMatchV1, false, true, "foobar fb", "fb", 0, 4, + scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+ + scoreGapStart+scoreGapExtension) + assertMatch(t, FuzzyMatchV1, false, false, "foobar fb", "fb", 7, 9, + scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary) +} + +func TestExactMatchNaive(t *testing.T) { + for _, dir := range []bool{true, false} { + assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "oBA", -1, -1, 0) + assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "fooBarbazz", -1, -1, 0) + + assertMatch(t, ExactMatchNaive, false, dir, "fooBarbaz", "oBA", 2, 5, + scoreMatch*3+bonusCamel123+bonusConsecutive) + assertMatch(t, ExactMatchNaive, false, dir, "/AutomatorDocument.icns", "rdoc", 9, 13, + scoreMatch*4+bonusCamel123+bonusConsecutive*2) + assertMatch(t, ExactMatchNaive, false, dir, "/man1/zshcompctl.1", "zshc", 6, 10, + scoreMatch*4+bonusBoundary*(bonusFirstCharMultiplier+3)) + assertMatch(t, ExactMatchNaive, false, dir, "/.oh-my-zsh/cache", "zsh/c", 8, 13, + scoreMatch*5+bonusBoundary*(bonusFirstCharMultiplier+4)) + } +} + +func TestExactMatchNaiveBackward(t *testing.T) { + assertMatch(t, ExactMatchNaive, false, true, "foobar foob", "oo", 1, 3, + scoreMatch*2+bonusConsecutive) + assertMatch(t, ExactMatchNaive, false, false, "foobar foob", "oo", 8, 10, + scoreMatch*2+bonusConsecutive) +} + +func TestPrefixMatch(t *testing.T) { + score := (scoreMatch+bonusBoundary)*3 + bonusBoundary*(bonusFirstCharMultiplier-1) + + for _, dir := range []bool{true, false} { + assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1, 0) + assertMatch(t, PrefixMatch, false, dir, "fooBarBaz", "baz", -1, -1, 0) + assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3, score) + assertMatch(t, PrefixMatch, false, dir, "foOBarBaZ", "foo", 0, 3, score) + assertMatch(t, PrefixMatch, false, dir, "f-oBarbaz", "f-o", 0, 3, score) + + assertMatch(t, PrefixMatch, false, dir, " fooBar", "foo", 1, 4, score) + assertMatch(t, PrefixMatch, false, dir, " fooBar", " fo", 0, 3, score) + assertMatch(t, PrefixMatch, false, dir, " fo", "foo", -1, -1, 0) + } +} + +func TestSuffixMatch(t *testing.T) { + for _, dir := range []bool{true, false} { + assertMatch(t, SuffixMatch, true, dir, "fooBarbaz", "Baz", -1, -1, 0) + assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "Foo", -1, -1, 0) + + assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "baz", 6, 9, + scoreMatch*3+bonusConsecutive*2) + assertMatch(t, SuffixMatch, false, dir, "fooBarBaZ", "baz", 6, 9, + (scoreMatch+bonusCamel123)*3+bonusCamel123*(bonusFirstCharMultiplier-1)) + + // Strip trailing white space from the string + assertMatch(t, SuffixMatch, false, dir, "fooBarbaz ", "baz", 6, 9, + scoreMatch*3+bonusConsecutive*2) + // Only when the pattern doesn't end with a space + assertMatch(t, SuffixMatch, false, dir, "fooBarbaz ", "baz ", 6, 10, + scoreMatch*4+bonusConsecutive*2+bonusNonWord) + } +} + +func TestEmptyPattern(t *testing.T) { + for _, dir := range []bool{true, false} { + assertMatch(t, FuzzyMatchV1, true, dir, "foobar", "", 0, 0, 0) + assertMatch(t, FuzzyMatchV2, true, dir, "foobar", "", 0, 0, 0) + assertMatch(t, ExactMatchNaive, true, dir, "foobar", "", 0, 0, 0) + assertMatch(t, PrefixMatch, true, dir, "foobar", "", 0, 0, 0) + assertMatch(t, SuffixMatch, true, dir, "foobar", "", 6, 6, 0) + } +} + +func TestNormalize(t *testing.T) { + caseSensitive := false + normalize := true + forward := true + test := func(input, pattern string, sidx, eidx, score int, funs ...Algo) { + for _, fun := range funs { + assertMatch2(t, fun, caseSensitive, normalize, forward, + input, pattern, sidx, eidx, score) + } + } + test("Só Danço Samba", "So", 0, 2, 56, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, ExactMatchNaive) + test("Só Danço Samba", "sodc", 0, 7, 89, FuzzyMatchV1, FuzzyMatchV2) + test("Danço", "danco", 0, 5, 128, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, SuffixMatch, ExactMatchNaive, EqualMatch) +} + +func TestLongString(t *testing.T) { + bytes := make([]byte, math.MaxUint16*2) + for i := range bytes { + bytes[i] = 'x' + } + bytes[math.MaxUint16] = 'z' + assertMatch(t, FuzzyMatchV2, true, true, string(bytes), "zx", math.MaxUint16, math.MaxUint16+2, scoreMatch*2+bonusConsecutive) +} diff --git a/fzf/fzf/src/algo/normalize.go b/fzf/fzf/src/algo/normalize.go new file mode 100644 index 0000000..9324790 --- /dev/null +++ b/fzf/fzf/src/algo/normalize.go @@ -0,0 +1,492 @@ +// Normalization of latin script letters +// Reference: http://www.unicode.org/Public/UCD/latest/ucd/Index.txt + +package algo + +var normalized map[rune]rune = map[rune]rune{ + 0x00E1: 'a', // WITH ACUTE, LATIN SMALL LETTER + 0x0103: 'a', // WITH BREVE, LATIN SMALL LETTER + 0x01CE: 'a', // WITH CARON, LATIN SMALL LETTER + 0x00E2: 'a', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x00E4: 'a', // WITH DIAERESIS, LATIN SMALL LETTER + 0x0227: 'a', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1EA1: 'a', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0201: 'a', // WITH DOUBLE GRAVE, LATIN SMALL LETTER + 0x00E0: 'a', // WITH GRAVE, LATIN SMALL LETTER + 0x1EA3: 'a', // WITH HOOK ABOVE, LATIN SMALL LETTER + 0x0203: 'a', // WITH INVERTED BREVE, LATIN SMALL LETTER + 0x0101: 'a', // WITH MACRON, LATIN SMALL LETTER + 0x0105: 'a', // WITH OGONEK, LATIN SMALL LETTER + 0x1E9A: 'a', // WITH RIGHT HALF RING, LATIN SMALL LETTER + 0x00E5: 'a', // WITH RING ABOVE, LATIN SMALL LETTER + 0x1E01: 'a', // WITH RING BELOW, LATIN SMALL LETTER + 0x00E3: 'a', // WITH TILDE, LATIN SMALL LETTER + 0x0363: 'a', // , COMBINING LATIN SMALL LETTER + 0x0250: 'a', // , LATIN SMALL LETTER TURNED + 0x1E03: 'b', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E05: 'b', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0253: 'b', // WITH HOOK, LATIN SMALL LETTER + 0x1E07: 'b', // WITH LINE BELOW, LATIN SMALL LETTER + 0x0180: 'b', // WITH STROKE, LATIN SMALL LETTER + 0x0183: 'b', // WITH TOPBAR, LATIN SMALL LETTER + 0x0107: 'c', // WITH ACUTE, LATIN SMALL LETTER + 0x010D: 'c', // WITH CARON, LATIN SMALL LETTER + 0x00E7: 'c', // WITH CEDILLA, LATIN SMALL LETTER + 0x0109: 'c', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x0255: 'c', // WITH CURL, LATIN SMALL LETTER + 0x010B: 'c', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x0188: 'c', // WITH HOOK, LATIN SMALL LETTER + 0x023C: 'c', // WITH STROKE, LATIN SMALL LETTER + 0x0368: 'c', // , COMBINING LATIN SMALL LETTER + 0x0297: 'c', // , LATIN LETTER STRETCHED + 0x2184: 'c', // , LATIN SMALL LETTER REVERSED + 0x010F: 'd', // WITH CARON, LATIN SMALL LETTER + 0x1E11: 'd', // WITH CEDILLA, LATIN SMALL LETTER + 0x1E13: 'd', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER + 0x0221: 'd', // WITH CURL, LATIN SMALL LETTER + 0x1E0B: 'd', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E0D: 'd', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0257: 'd', // WITH HOOK, LATIN SMALL LETTER + 0x1E0F: 'd', // WITH LINE BELOW, LATIN SMALL LETTER + 0x0111: 'd', // WITH STROKE, LATIN SMALL LETTER + 0x0256: 'd', // WITH TAIL, LATIN SMALL LETTER + 0x018C: 'd', // WITH TOPBAR, LATIN SMALL LETTER + 0x0369: 'd', // , COMBINING LATIN SMALL LETTER + 0x00E9: 'e', // WITH ACUTE, LATIN SMALL LETTER + 0x0115: 'e', // WITH BREVE, LATIN SMALL LETTER + 0x011B: 'e', // WITH CARON, LATIN SMALL LETTER + 0x0229: 'e', // WITH CEDILLA, LATIN SMALL LETTER + 0x1E19: 'e', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER + 0x00EA: 'e', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x00EB: 'e', // WITH DIAERESIS, LATIN SMALL LETTER + 0x0117: 'e', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1EB9: 'e', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0205: 'e', // WITH DOUBLE GRAVE, LATIN SMALL LETTER + 0x00E8: 'e', // WITH GRAVE, LATIN SMALL LETTER + 0x1EBB: 'e', // WITH HOOK ABOVE, LATIN SMALL LETTER + 0x025D: 'e', // WITH HOOK, LATIN SMALL LETTER REVERSED OPEN + 0x0207: 'e', // WITH INVERTED BREVE, LATIN SMALL LETTER + 0x0113: 'e', // WITH MACRON, LATIN SMALL LETTER + 0x0119: 'e', // WITH OGONEK, LATIN SMALL LETTER + 0x0247: 'e', // WITH STROKE, LATIN SMALL LETTER + 0x1E1B: 'e', // WITH TILDE BELOW, LATIN SMALL LETTER + 0x1EBD: 'e', // WITH TILDE, LATIN SMALL LETTER + 0x0364: 'e', // , COMBINING LATIN SMALL LETTER + 0x029A: 'e', // , LATIN SMALL LETTER CLOSED OPEN + 0x025E: 'e', // , LATIN SMALL LETTER CLOSED REVERSED OPEN + 0x025B: 'e', // , LATIN SMALL LETTER OPEN + 0x0258: 'e', // , LATIN SMALL LETTER REVERSED + 0x025C: 'e', // , LATIN SMALL LETTER REVERSED OPEN + 0x01DD: 'e', // , LATIN SMALL LETTER TURNED + 0x1D08: 'e', // , LATIN SMALL LETTER TURNED OPEN + 0x1E1F: 'f', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x0192: 'f', // WITH HOOK, LATIN SMALL LETTER + 0x01F5: 'g', // WITH ACUTE, LATIN SMALL LETTER + 0x011F: 'g', // WITH BREVE, LATIN SMALL LETTER + 0x01E7: 'g', // WITH CARON, LATIN SMALL LETTER + 0x0123: 'g', // WITH CEDILLA, LATIN SMALL LETTER + 0x011D: 'g', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x0121: 'g', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x0260: 'g', // WITH HOOK, LATIN SMALL LETTER + 0x1E21: 'g', // WITH MACRON, LATIN SMALL LETTER + 0x01E5: 'g', // WITH STROKE, LATIN SMALL LETTER + 0x0261: 'g', // , LATIN SMALL LETTER SCRIPT + 0x1E2B: 'h', // WITH BREVE BELOW, LATIN SMALL LETTER + 0x021F: 'h', // WITH CARON, LATIN SMALL LETTER + 0x1E29: 'h', // WITH CEDILLA, LATIN SMALL LETTER + 0x0125: 'h', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x1E27: 'h', // WITH DIAERESIS, LATIN SMALL LETTER + 0x1E23: 'h', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E25: 'h', // WITH DOT BELOW, LATIN SMALL LETTER + 0x02AE: 'h', // WITH FISHHOOK, LATIN SMALL LETTER TURNED + 0x0266: 'h', // WITH HOOK, LATIN SMALL LETTER + 0x1E96: 'h', // WITH LINE BELOW, LATIN SMALL LETTER + 0x0127: 'h', // WITH STROKE, LATIN SMALL LETTER + 0x036A: 'h', // , COMBINING LATIN SMALL LETTER + 0x0265: 'h', // , LATIN SMALL LETTER TURNED + 0x2095: 'h', // , LATIN SUBSCRIPT SMALL LETTER + 0x00ED: 'i', // WITH ACUTE, LATIN SMALL LETTER + 0x012D: 'i', // WITH BREVE, LATIN SMALL LETTER + 0x01D0: 'i', // WITH CARON, LATIN SMALL LETTER + 0x00EE: 'i', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x00EF: 'i', // WITH DIAERESIS, LATIN SMALL LETTER + 0x1ECB: 'i', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0209: 'i', // WITH DOUBLE GRAVE, LATIN SMALL LETTER + 0x00EC: 'i', // WITH GRAVE, LATIN SMALL LETTER + 0x1EC9: 'i', // WITH HOOK ABOVE, LATIN SMALL LETTER + 0x020B: 'i', // WITH INVERTED BREVE, LATIN SMALL LETTER + 0x012B: 'i', // WITH MACRON, LATIN SMALL LETTER + 0x012F: 'i', // WITH OGONEK, LATIN SMALL LETTER + 0x0268: 'i', // WITH STROKE, LATIN SMALL LETTER + 0x1E2D: 'i', // WITH TILDE BELOW, LATIN SMALL LETTER + 0x0129: 'i', // WITH TILDE, LATIN SMALL LETTER + 0x0365: 'i', // , COMBINING LATIN SMALL LETTER + 0x0131: 'i', // , LATIN SMALL LETTER DOTLESS + 0x1D09: 'i', // , LATIN SMALL LETTER TURNED + 0x1D62: 'i', // , LATIN SUBSCRIPT SMALL LETTER + 0x2071: 'i', // , SUPERSCRIPT LATIN SMALL LETTER + 0x01F0: 'j', // WITH CARON, LATIN SMALL LETTER + 0x0135: 'j', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x029D: 'j', // WITH CROSSED-TAIL, LATIN SMALL LETTER + 0x0249: 'j', // WITH STROKE, LATIN SMALL LETTER + 0x025F: 'j', // WITH STROKE, LATIN SMALL LETTER DOTLESS + 0x0237: 'j', // , LATIN SMALL LETTER DOTLESS + 0x1E31: 'k', // WITH ACUTE, LATIN SMALL LETTER + 0x01E9: 'k', // WITH CARON, LATIN SMALL LETTER + 0x0137: 'k', // WITH CEDILLA, LATIN SMALL LETTER + 0x1E33: 'k', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0199: 'k', // WITH HOOK, LATIN SMALL LETTER + 0x1E35: 'k', // WITH LINE BELOW, LATIN SMALL LETTER + 0x029E: 'k', // , LATIN SMALL LETTER TURNED + 0x2096: 'k', // , LATIN SUBSCRIPT SMALL LETTER + 0x013A: 'l', // WITH ACUTE, LATIN SMALL LETTER + 0x019A: 'l', // WITH BAR, LATIN SMALL LETTER + 0x026C: 'l', // WITH BELT, LATIN SMALL LETTER + 0x013E: 'l', // WITH CARON, LATIN SMALL LETTER + 0x013C: 'l', // WITH CEDILLA, LATIN SMALL LETTER + 0x1E3D: 'l', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER + 0x0234: 'l', // WITH CURL, LATIN SMALL LETTER + 0x1E37: 'l', // WITH DOT BELOW, LATIN SMALL LETTER + 0x1E3B: 'l', // WITH LINE BELOW, LATIN SMALL LETTER + 0x0140: 'l', // WITH MIDDLE DOT, LATIN SMALL LETTER + 0x026B: 'l', // WITH MIDDLE TILDE, LATIN SMALL LETTER + 0x026D: 'l', // WITH RETROFLEX HOOK, LATIN SMALL LETTER + 0x0142: 'l', // WITH STROKE, LATIN SMALL LETTER + 0x2097: 'l', // , LATIN SUBSCRIPT SMALL LETTER + 0x1E3F: 'm', // WITH ACUTE, LATIN SMALL LETTER + 0x1E41: 'm', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E43: 'm', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0271: 'm', // WITH HOOK, LATIN SMALL LETTER + 0x0270: 'm', // WITH LONG LEG, LATIN SMALL LETTER TURNED + 0x036B: 'm', // , COMBINING LATIN SMALL LETTER + 0x1D1F: 'm', // , LATIN SMALL LETTER SIDEWAYS TURNED + 0x026F: 'm', // , LATIN SMALL LETTER TURNED + 0x2098: 'm', // , LATIN SUBSCRIPT SMALL LETTER + 0x0144: 'n', // WITH ACUTE, LATIN SMALL LETTER + 0x0148: 'n', // WITH CARON, LATIN SMALL LETTER + 0x0146: 'n', // WITH CEDILLA, LATIN SMALL LETTER + 0x1E4B: 'n', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER + 0x0235: 'n', // WITH CURL, LATIN SMALL LETTER + 0x1E45: 'n', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E47: 'n', // WITH DOT BELOW, LATIN SMALL LETTER + 0x01F9: 'n', // WITH GRAVE, LATIN SMALL LETTER + 0x0272: 'n', // WITH LEFT HOOK, LATIN SMALL LETTER + 0x1E49: 'n', // WITH LINE BELOW, LATIN SMALL LETTER + 0x019E: 'n', // WITH LONG RIGHT LEG, LATIN SMALL LETTER + 0x0273: 'n', // WITH RETROFLEX HOOK, LATIN SMALL LETTER + 0x00F1: 'n', // WITH TILDE, LATIN SMALL LETTER + 0x2099: 'n', // , LATIN SUBSCRIPT SMALL LETTER + 0x00F3: 'o', // WITH ACUTE, LATIN SMALL LETTER + 0x014F: 'o', // WITH BREVE, LATIN SMALL LETTER + 0x01D2: 'o', // WITH CARON, LATIN SMALL LETTER + 0x00F4: 'o', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x00F6: 'o', // WITH DIAERESIS, LATIN SMALL LETTER + 0x022F: 'o', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1ECD: 'o', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0151: 'o', // WITH DOUBLE ACUTE, LATIN SMALL LETTER + 0x020D: 'o', // WITH DOUBLE GRAVE, LATIN SMALL LETTER + 0x00F2: 'o', // WITH GRAVE, LATIN SMALL LETTER + 0x1ECF: 'o', // WITH HOOK ABOVE, LATIN SMALL LETTER + 0x01A1: 'o', // WITH HORN, LATIN SMALL LETTER + 0x020F: 'o', // WITH INVERTED BREVE, LATIN SMALL LETTER + 0x014D: 'o', // WITH MACRON, LATIN SMALL LETTER + 0x01EB: 'o', // WITH OGONEK, LATIN SMALL LETTER + 0x00F8: 'o', // WITH STROKE, LATIN SMALL LETTER + 0x1D13: 'o', // WITH STROKE, LATIN SMALL LETTER SIDEWAYS + 0x00F5: 'o', // WITH TILDE, LATIN SMALL LETTER + 0x0366: 'o', // , COMBINING LATIN SMALL LETTER + 0x0275: 'o', // , LATIN SMALL LETTER BARRED + 0x1D17: 'o', // , LATIN SMALL LETTER BOTTOM HALF + 0x0254: 'o', // , LATIN SMALL LETTER OPEN + 0x1D11: 'o', // , LATIN SMALL LETTER SIDEWAYS + 0x1D12: 'o', // , LATIN SMALL LETTER SIDEWAYS OPEN + 0x1D16: 'o', // , LATIN SMALL LETTER TOP HALF + 0x1E55: 'p', // WITH ACUTE, LATIN SMALL LETTER + 0x1E57: 'p', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x01A5: 'p', // WITH HOOK, LATIN SMALL LETTER + 0x209A: 'p', // , LATIN SUBSCRIPT SMALL LETTER + 0x024B: 'q', // WITH HOOK TAIL, LATIN SMALL LETTER + 0x02A0: 'q', // WITH HOOK, LATIN SMALL LETTER + 0x0155: 'r', // WITH ACUTE, LATIN SMALL LETTER + 0x0159: 'r', // WITH CARON, LATIN SMALL LETTER + 0x0157: 'r', // WITH CEDILLA, LATIN SMALL LETTER + 0x1E59: 'r', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E5B: 'r', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0211: 'r', // WITH DOUBLE GRAVE, LATIN SMALL LETTER + 0x027E: 'r', // WITH FISHHOOK, LATIN SMALL LETTER + 0x027F: 'r', // WITH FISHHOOK, LATIN SMALL LETTER REVERSED + 0x027B: 'r', // WITH HOOK, LATIN SMALL LETTER TURNED + 0x0213: 'r', // WITH INVERTED BREVE, LATIN SMALL LETTER + 0x1E5F: 'r', // WITH LINE BELOW, LATIN SMALL LETTER + 0x027C: 'r', // WITH LONG LEG, LATIN SMALL LETTER + 0x027A: 'r', // WITH LONG LEG, LATIN SMALL LETTER TURNED + 0x024D: 'r', // WITH STROKE, LATIN SMALL LETTER + 0x027D: 'r', // WITH TAIL, LATIN SMALL LETTER + 0x036C: 'r', // , COMBINING LATIN SMALL LETTER + 0x0279: 'r', // , LATIN SMALL LETTER TURNED + 0x1D63: 'r', // , LATIN SUBSCRIPT SMALL LETTER + 0x015B: 's', // WITH ACUTE, LATIN SMALL LETTER + 0x0161: 's', // WITH CARON, LATIN SMALL LETTER + 0x015F: 's', // WITH CEDILLA, LATIN SMALL LETTER + 0x015D: 's', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x0219: 's', // WITH COMMA BELOW, LATIN SMALL LETTER + 0x1E61: 's', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E9B: 's', // WITH DOT ABOVE, LATIN SMALL LETTER LONG + 0x1E63: 's', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0282: 's', // WITH HOOK, LATIN SMALL LETTER + 0x023F: 's', // WITH SWASH TAIL, LATIN SMALL LETTER + 0x017F: 's', // , LATIN SMALL LETTER LONG + 0x00DF: 's', // , LATIN SMALL LETTER SHARP + 0x209B: 's', // , LATIN SUBSCRIPT SMALL LETTER + 0x0165: 't', // WITH CARON, LATIN SMALL LETTER + 0x0163: 't', // WITH CEDILLA, LATIN SMALL LETTER + 0x1E71: 't', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER + 0x021B: 't', // WITH COMMA BELOW, LATIN SMALL LETTER + 0x0236: 't', // WITH CURL, LATIN SMALL LETTER + 0x1E97: 't', // WITH DIAERESIS, LATIN SMALL LETTER + 0x1E6B: 't', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E6D: 't', // WITH DOT BELOW, LATIN SMALL LETTER + 0x01AD: 't', // WITH HOOK, LATIN SMALL LETTER + 0x1E6F: 't', // WITH LINE BELOW, LATIN SMALL LETTER + 0x01AB: 't', // WITH PALATAL HOOK, LATIN SMALL LETTER + 0x0288: 't', // WITH RETROFLEX HOOK, LATIN SMALL LETTER + 0x0167: 't', // WITH STROKE, LATIN SMALL LETTER + 0x036D: 't', // , COMBINING LATIN SMALL LETTER + 0x0287: 't', // , LATIN SMALL LETTER TURNED + 0x209C: 't', // , LATIN SUBSCRIPT SMALL LETTER + 0x0289: 'u', // BAR, LATIN SMALL LETTER + 0x00FA: 'u', // WITH ACUTE, LATIN SMALL LETTER + 0x016D: 'u', // WITH BREVE, LATIN SMALL LETTER + 0x01D4: 'u', // WITH CARON, LATIN SMALL LETTER + 0x1E77: 'u', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER + 0x00FB: 'u', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x1E73: 'u', // WITH DIAERESIS BELOW, LATIN SMALL LETTER + 0x00FC: 'u', // WITH DIAERESIS, LATIN SMALL LETTER + 0x1EE5: 'u', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0171: 'u', // WITH DOUBLE ACUTE, LATIN SMALL LETTER + 0x0215: 'u', // WITH DOUBLE GRAVE, LATIN SMALL LETTER + 0x00F9: 'u', // WITH GRAVE, LATIN SMALL LETTER + 0x1EE7: 'u', // WITH HOOK ABOVE, LATIN SMALL LETTER + 0x01B0: 'u', // WITH HORN, LATIN SMALL LETTER + 0x0217: 'u', // WITH INVERTED BREVE, LATIN SMALL LETTER + 0x016B: 'u', // WITH MACRON, LATIN SMALL LETTER + 0x0173: 'u', // WITH OGONEK, LATIN SMALL LETTER + 0x016F: 'u', // WITH RING ABOVE, LATIN SMALL LETTER + 0x1E75: 'u', // WITH TILDE BELOW, LATIN SMALL LETTER + 0x0169: 'u', // WITH TILDE, LATIN SMALL LETTER + 0x0367: 'u', // , COMBINING LATIN SMALL LETTER + 0x1D1D: 'u', // , LATIN SMALL LETTER SIDEWAYS + 0x1D1E: 'u', // , LATIN SMALL LETTER SIDEWAYS DIAERESIZED + 0x1D64: 'u', // , LATIN SUBSCRIPT SMALL LETTER + 0x1E7F: 'v', // WITH DOT BELOW, LATIN SMALL LETTER + 0x028B: 'v', // WITH HOOK, LATIN SMALL LETTER + 0x1E7D: 'v', // WITH TILDE, LATIN SMALL LETTER + 0x036E: 'v', // , COMBINING LATIN SMALL LETTER + 0x028C: 'v', // , LATIN SMALL LETTER TURNED + 0x1D65: 'v', // , LATIN SUBSCRIPT SMALL LETTER + 0x1E83: 'w', // WITH ACUTE, LATIN SMALL LETTER + 0x0175: 'w', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x1E85: 'w', // WITH DIAERESIS, LATIN SMALL LETTER + 0x1E87: 'w', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E89: 'w', // WITH DOT BELOW, LATIN SMALL LETTER + 0x1E81: 'w', // WITH GRAVE, LATIN SMALL LETTER + 0x1E98: 'w', // WITH RING ABOVE, LATIN SMALL LETTER + 0x028D: 'w', // , LATIN SMALL LETTER TURNED + 0x1E8D: 'x', // WITH DIAERESIS, LATIN SMALL LETTER + 0x1E8B: 'x', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x036F: 'x', // , COMBINING LATIN SMALL LETTER + 0x00FD: 'y', // WITH ACUTE, LATIN SMALL LETTER + 0x0177: 'y', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x00FF: 'y', // WITH DIAERESIS, LATIN SMALL LETTER + 0x1E8F: 'y', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1EF5: 'y', // WITH DOT BELOW, LATIN SMALL LETTER + 0x1EF3: 'y', // WITH GRAVE, LATIN SMALL LETTER + 0x1EF7: 'y', // WITH HOOK ABOVE, LATIN SMALL LETTER + 0x01B4: 'y', // WITH HOOK, LATIN SMALL LETTER + 0x0233: 'y', // WITH MACRON, LATIN SMALL LETTER + 0x1E99: 'y', // WITH RING ABOVE, LATIN SMALL LETTER + 0x024F: 'y', // WITH STROKE, LATIN SMALL LETTER + 0x1EF9: 'y', // WITH TILDE, LATIN SMALL LETTER + 0x028E: 'y', // , LATIN SMALL LETTER TURNED + 0x017A: 'z', // WITH ACUTE, LATIN SMALL LETTER + 0x017E: 'z', // WITH CARON, LATIN SMALL LETTER + 0x1E91: 'z', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x0291: 'z', // WITH CURL, LATIN SMALL LETTER + 0x017C: 'z', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E93: 'z', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0225: 'z', // WITH HOOK, LATIN SMALL LETTER + 0x1E95: 'z', // WITH LINE BELOW, LATIN SMALL LETTER + 0x0290: 'z', // WITH RETROFLEX HOOK, LATIN SMALL LETTER + 0x01B6: 'z', // WITH STROKE, LATIN SMALL LETTER + 0x0240: 'z', // WITH SWASH TAIL, LATIN SMALL LETTER + 0x0251: 'a', // , latin small letter script + 0x00C1: 'A', // WITH ACUTE, LATIN CAPITAL LETTER + 0x00C2: 'A', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER + 0x00C4: 'A', // WITH DIAERESIS, LATIN CAPITAL LETTER + 0x00C0: 'A', // WITH GRAVE, LATIN CAPITAL LETTER + 0x00C5: 'A', // WITH RING ABOVE, LATIN CAPITAL LETTER + 0x023A: 'A', // WITH STROKE, LATIN CAPITAL LETTER + 0x00C3: 'A', // WITH TILDE, LATIN CAPITAL LETTER + 0x1D00: 'A', // , LATIN LETTER SMALL CAPITAL + 0x0181: 'B', // WITH HOOK, LATIN CAPITAL LETTER + 0x0243: 'B', // WITH STROKE, LATIN CAPITAL LETTER + 0x0299: 'B', // , LATIN LETTER SMALL CAPITAL + 0x1D03: 'B', // , LATIN LETTER SMALL CAPITAL BARRED + 0x00C7: 'C', // WITH CEDILLA, LATIN CAPITAL LETTER + 0x023B: 'C', // WITH STROKE, LATIN CAPITAL LETTER + 0x1D04: 'C', // , LATIN LETTER SMALL CAPITAL + 0x018A: 'D', // WITH HOOK, LATIN CAPITAL LETTER + 0x0189: 'D', // , LATIN CAPITAL LETTER AFRICAN + 0x1D05: 'D', // , LATIN LETTER SMALL CAPITAL + 0x00C9: 'E', // WITH ACUTE, LATIN CAPITAL LETTER + 0x00CA: 'E', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER + 0x00CB: 'E', // WITH DIAERESIS, LATIN CAPITAL LETTER + 0x00C8: 'E', // WITH GRAVE, LATIN CAPITAL LETTER + 0x0246: 'E', // WITH STROKE, LATIN CAPITAL LETTER + 0x0190: 'E', // , LATIN CAPITAL LETTER OPEN + 0x018E: 'E', // , LATIN CAPITAL LETTER REVERSED + 0x1D07: 'E', // , LATIN LETTER SMALL CAPITAL + 0x0193: 'G', // WITH HOOK, LATIN CAPITAL LETTER + 0x029B: 'G', // WITH HOOK, LATIN LETTER SMALL CAPITAL + 0x0262: 'G', // , LATIN LETTER SMALL CAPITAL + 0x029C: 'H', // , LATIN LETTER SMALL CAPITAL + 0x00CD: 'I', // WITH ACUTE, LATIN CAPITAL LETTER + 0x00CE: 'I', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER + 0x00CF: 'I', // WITH DIAERESIS, LATIN CAPITAL LETTER + 0x0130: 'I', // WITH DOT ABOVE, LATIN CAPITAL LETTER + 0x00CC: 'I', // WITH GRAVE, LATIN CAPITAL LETTER + 0x0197: 'I', // WITH STROKE, LATIN CAPITAL LETTER + 0x026A: 'I', // , LATIN LETTER SMALL CAPITAL + 0x0248: 'J', // WITH STROKE, LATIN CAPITAL LETTER + 0x1D0A: 'J', // , LATIN LETTER SMALL CAPITAL + 0x1D0B: 'K', // , LATIN LETTER SMALL CAPITAL + 0x023D: 'L', // WITH BAR, LATIN CAPITAL LETTER + 0x1D0C: 'L', // WITH STROKE, LATIN LETTER SMALL CAPITAL + 0x029F: 'L', // , LATIN LETTER SMALL CAPITAL + 0x019C: 'M', // , LATIN CAPITAL LETTER TURNED + 0x1D0D: 'M', // , LATIN LETTER SMALL CAPITAL + 0x019D: 'N', // WITH LEFT HOOK, LATIN CAPITAL LETTER + 0x0220: 'N', // WITH LONG RIGHT LEG, LATIN CAPITAL LETTER + 0x00D1: 'N', // WITH TILDE, LATIN CAPITAL LETTER + 0x0274: 'N', // , LATIN LETTER SMALL CAPITAL + 0x1D0E: 'N', // , LATIN LETTER SMALL CAPITAL REVERSED + 0x00D3: 'O', // WITH ACUTE, LATIN CAPITAL LETTER + 0x00D4: 'O', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER + 0x00D6: 'O', // WITH DIAERESIS, LATIN CAPITAL LETTER + 0x00D2: 'O', // WITH GRAVE, LATIN CAPITAL LETTER + 0x019F: 'O', // WITH MIDDLE TILDE, LATIN CAPITAL LETTER + 0x00D8: 'O', // WITH STROKE, LATIN CAPITAL LETTER + 0x00D5: 'O', // WITH TILDE, LATIN CAPITAL LETTER + 0x0186: 'O', // , LATIN CAPITAL LETTER OPEN + 0x1D0F: 'O', // , LATIN LETTER SMALL CAPITAL + 0x1D10: 'O', // , LATIN LETTER SMALL CAPITAL OPEN + 0x1D18: 'P', // , LATIN LETTER SMALL CAPITAL + 0x024A: 'Q', // WITH HOOK TAIL, LATIN CAPITAL LETTER SMALL + 0x024C: 'R', // WITH STROKE, LATIN CAPITAL LETTER + 0x0280: 'R', // , LATIN LETTER SMALL CAPITAL + 0x0281: 'R', // , LATIN LETTER SMALL CAPITAL INVERTED + 0x1D19: 'R', // , LATIN LETTER SMALL CAPITAL REVERSED + 0x1D1A: 'R', // , LATIN LETTER SMALL CAPITAL TURNED + 0x023E: 'T', // WITH DIAGONAL STROKE, LATIN CAPITAL LETTER + 0x01AE: 'T', // WITH RETROFLEX HOOK, LATIN CAPITAL LETTER + 0x1D1B: 'T', // , LATIN LETTER SMALL CAPITAL + 0x0244: 'U', // BAR, LATIN CAPITAL LETTER + 0x00DA: 'U', // WITH ACUTE, LATIN CAPITAL LETTER + 0x00DB: 'U', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER + 0x00DC: 'U', // WITH DIAERESIS, LATIN CAPITAL LETTER + 0x00D9: 'U', // WITH GRAVE, LATIN CAPITAL LETTER + 0x1D1C: 'U', // , LATIN LETTER SMALL CAPITAL + 0x01B2: 'V', // WITH HOOK, LATIN CAPITAL LETTER + 0x0245: 'V', // , LATIN CAPITAL LETTER TURNED + 0x1D20: 'V', // , LATIN LETTER SMALL CAPITAL + 0x1D21: 'W', // , LATIN LETTER SMALL CAPITAL + 0x00DD: 'Y', // WITH ACUTE, LATIN CAPITAL LETTER + 0x0178: 'Y', // WITH DIAERESIS, LATIN CAPITAL LETTER + 0x024E: 'Y', // WITH STROKE, LATIN CAPITAL LETTER + 0x028F: 'Y', // , LATIN LETTER SMALL CAPITAL + 0x1D22: 'Z', // , LATIN LETTER SMALL CAPITAL + + 'Ắ': 'A', + 'Ấ': 'A', + 'Ằ': 'A', + 'Ầ': 'A', + 'Ẳ': 'A', + 'Ẩ': 'A', + 'Ẵ': 'A', + 'Ẫ': 'A', + 'Ặ': 'A', + 'Ậ': 'A', + + 'ắ': 'a', + 'ấ': 'a', + 'ằ': 'a', + 'ầ': 'a', + 'ẳ': 'a', + 'ẩ': 'a', + 'ẵ': 'a', + 'ẫ': 'a', + 'ặ': 'a', + 'ậ': 'a', + + 'Ế': 'E', + 'Ề': 'E', + 'Ể': 'E', + 'Ễ': 'E', + 'Ệ': 'E', + + 'ế': 'e', + 'ề': 'e', + 'ể': 'e', + 'ễ': 'e', + 'ệ': 'e', + + 'Ố': 'O', + 'Ớ': 'O', + 'Ồ': 'O', + 'Ờ': 'O', + 'Ổ': 'O', + 'Ở': 'O', + 'Ỗ': 'O', + 'Ỡ': 'O', + 'Ộ': 'O', + 'Ợ': 'O', + + 'ố': 'o', + 'ớ': 'o', + 'ồ': 'o', + 'ờ': 'o', + 'ổ': 'o', + 'ở': 'o', + 'ỗ': 'o', + 'ỡ': 'o', + 'ộ': 'o', + 'ợ': 'o', + + 'Ứ': 'U', + 'Ừ': 'U', + 'Ử': 'U', + 'Ữ': 'U', + 'Ự': 'U', + + 'ứ': 'u', + 'ừ': 'u', + 'ử': 'u', + 'ữ': 'u', + 'ự': 'u', +} + +// NormalizeRunes normalizes latin script letters +func NormalizeRunes(runes []rune) []rune { + ret := make([]rune, len(runes)) + copy(ret, runes) + for idx, r := range runes { + if r < 0x00C0 || r > 0x2184 { + continue + } + n := normalized[r] + if n > 0 { + ret[idx] = normalized[r] + } + } + return ret +} diff --git a/fzf/fzf/src/ansi.go b/fzf/fzf/src/ansi.go new file mode 100644 index 0000000..698bf89 --- /dev/null +++ b/fzf/fzf/src/ansi.go @@ -0,0 +1,409 @@ +package fzf + +import ( + "strconv" + "strings" + "unicode/utf8" + + "github.com/junegunn/fzf/src/tui" +) + +type ansiOffset struct { + offset [2]int32 + color ansiState +} + +type ansiState struct { + fg tui.Color + bg tui.Color + attr tui.Attr + lbg tui.Color +} + +func (s *ansiState) colored() bool { + return s.fg != -1 || s.bg != -1 || s.attr > 0 || s.lbg >= 0 +} + +func (s *ansiState) equals(t *ansiState) bool { + if t == nil { + return !s.colored() + } + return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr && s.lbg == t.lbg +} + +func (s *ansiState) ToString() string { + if !s.colored() { + return "" + } + + ret := "" + if s.attr&tui.Bold > 0 { + ret += "1;" + } + if s.attr&tui.Dim > 0 { + ret += "2;" + } + if s.attr&tui.Italic > 0 { + ret += "3;" + } + if s.attr&tui.Underline > 0 { + ret += "4;" + } + if s.attr&tui.Blink > 0 { + ret += "5;" + } + if s.attr&tui.Reverse > 0 { + ret += "7;" + } + ret += toAnsiString(s.fg, 30) + toAnsiString(s.bg, 40) + + return "\x1b[" + strings.TrimSuffix(ret, ";") + "m" +} + +func toAnsiString(color tui.Color, offset int) string { + col := int(color) + ret := "" + if col == -1 { + ret += strconv.Itoa(offset + 9) + } else if col < 8 { + ret += strconv.Itoa(offset + col) + } else if col < 16 { + ret += strconv.Itoa(offset - 30 + 90 + col - 8) + } else if col < 256 { + ret += strconv.Itoa(offset+8) + ";5;" + strconv.Itoa(col) + } else if col >= (1 << 24) { + r := strconv.Itoa((col >> 16) & 0xff) + g := strconv.Itoa((col >> 8) & 0xff) + b := strconv.Itoa(col & 0xff) + ret += strconv.Itoa(offset+8) + ";2;" + r + ";" + g + ";" + b + } + return ret + ";" +} + +func isPrint(c uint8) bool { + return '\x20' <= c && c <= '\x7e' +} + +func matchOperatingSystemCommand(s string) int { + // `\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)` + // ^ match starting here + // + i := 5 // prefix matched in nextAnsiEscapeSequence() + for ; i < len(s) && isPrint(s[i]); i++ { + } + if i < len(s) { + if s[i] == '\x07' { + return i + 1 + } + if s[i] == '\x1b' && i < len(s)-1 && s[i+1] == '\\' { + return i + 2 + } + } + return -1 +} + +func matchControlSequence(s string) int { + // `\x1b[\\[()][0-9;?]*[a-zA-Z@]` + // ^ match starting here + // + i := 2 // prefix matched in nextAnsiEscapeSequence() + for ; i < len(s) && (isNumeric(s[i]) || s[i] == ';' || s[i] == '?'); i++ { + } + if i < len(s) { + c := s[i] + if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '@' { + return i + 1 + } + } + return -1 +} + +func isCtrlSeqStart(c uint8) bool { + return c == '\\' || c == '[' || c == '(' || c == ')' +} + +// nextAnsiEscapeSequence returns the ANSI escape sequence and is equivalent to +// calling FindStringIndex() on the below regex (which was originally used): +// +// "(?:\x1b[\\[()][0-9;?]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)" +// +func nextAnsiEscapeSequence(s string) (int, int) { + // fast check for ANSI escape sequences + i := 0 + for ; i < len(s); i++ { + switch s[i] { + case '\x0e', '\x0f', '\x1b', '\x08': + // We ignore the fact that '\x08' cannot be the first char + // in the string and be an escape sequence for the sake of + // speed and simplicity. + goto Loop + } + } + return -1, -1 + +Loop: + for ; i < len(s); i++ { + switch s[i] { + case '\x08': + // backtrack to match: `.\x08` + if i > 0 && s[i-1] != '\n' { + if s[i-1] < utf8.RuneSelf { + return i - 1, i + 1 + } + _, n := utf8.DecodeLastRuneInString(s[:i]) + return i - n, i + 1 + } + case '\x1b': + // match: `\x1b[\\[()][0-9;?]*[a-zA-Z@]` + if i+2 < len(s) && isCtrlSeqStart(s[i+1]) { + if j := matchControlSequence(s[i:]); j != -1 { + return i, i + j + } + } + + // match: `\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)` + if i+5 < len(s) && s[i+1] == ']' && isNumeric(s[i+2]) && + s[i+3] == ';' && isPrint(s[i+4]) { + + if j := matchOperatingSystemCommand(s[i:]); j != -1 { + return i, i + j + } + } + + // match: `\x1b.` + if i+1 < len(s) && s[i+1] != '\n' { + if s[i+1] < utf8.RuneSelf { + return i, i + 2 + } + _, n := utf8.DecodeRuneInString(s[i+1:]) + return i, i + n + 1 + } + case '\x0e', '\x0f': + // match: `[\x0e\x0f]` + return i, i + 1 + } + } + return -1, -1 +} + +func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) { + // We append to a stack allocated variable that we'll + // later copy and return, to save on allocations. + offsets := make([]ansiOffset, 0, 32) + + if state != nil { + offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state}) + } + + var ( + pstate *ansiState // lazily allocated + output strings.Builder + prevIdx int + runeCount int + ) + for idx := 0; idx < len(str); { + // Make sure that we found an ANSI code + start, end := nextAnsiEscapeSequence(str[idx:]) + if start == -1 { + break + } + start += idx + idx += end + + // Check if we should continue + prev := str[prevIdx:start] + if proc != nil && !proc(prev, state) { + return "", nil, nil + } + prevIdx = idx + + if len(prev) != 0 { + runeCount += utf8.RuneCountInString(prev) + // Grow the buffer size to the maximum possible length (string length + // containing ansi codes) to avoid repetitive allocation + if output.Cap() == 0 { + output.Grow(len(str)) + } + output.WriteString(prev) + } + + newState := interpretCode(str[start:idx], state) + if !newState.equals(state) { + if state != nil { + // Update last offset + (&offsets[len(offsets)-1]).offset[1] = int32(runeCount) + } + + if newState.colored() { + // Append new offset + if pstate == nil { + pstate = &ansiState{} + } + *pstate = newState + state = pstate + offsets = append(offsets, ansiOffset{ + [2]int32{int32(runeCount), int32(runeCount)}, + newState, + }) + } else { + // Discard state + state = nil + } + } + } + + var rest string + var trimmed string + if prevIdx == 0 { + // No ANSI code found + rest = str + trimmed = str + } else { + rest = str[prevIdx:] + output.WriteString(rest) + trimmed = output.String() + } + if proc != nil { + proc(rest, state) + } + if len(offsets) > 0 { + if len(rest) > 0 && state != nil { + // Update last offset + runeCount += utf8.RuneCountInString(rest) + (&offsets[len(offsets)-1]).offset[1] = int32(runeCount) + } + // Return a copy of the offsets slice + a := make([]ansiOffset, len(offsets)) + copy(a, offsets) + return trimmed, &a, state + } + return trimmed, nil, state +} + +func parseAnsiCode(s string) (int, string) { + var remaining string + if i := strings.IndexByte(s, ';'); i >= 0 { + remaining = s[i+1:] + s = s[:i] + } + + if len(s) > 0 { + // Inlined version of strconv.Atoi() that only handles positive + // integers and does not allocate on error. + code := 0 + for _, ch := range []byte(s) { + ch -= '0' + if ch > 9 { + return -1, remaining + } + code = code*10 + int(ch) + } + return code, remaining + } + + return -1, remaining +} + +func interpretCode(ansiCode string, prevState *ansiState) ansiState { + var state ansiState + if prevState == nil { + state = ansiState{-1, -1, 0, -1} + } else { + state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg} + } + if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' { + if prevState != nil && strings.HasSuffix(ansiCode, "0K") { + state.lbg = prevState.bg + } + return state + } + + if len(ansiCode) <= 3 { + state.fg = -1 + state.bg = -1 + state.attr = 0 + return state + } + ansiCode = ansiCode[2 : len(ansiCode)-1] + + state256 := 0 + ptr := &state.fg + + for len(ansiCode) != 0 { + var num int + if num, ansiCode = parseAnsiCode(ansiCode); num != -1 { + switch state256 { + case 0: + switch num { + case 38: + ptr = &state.fg + state256++ + case 48: + ptr = &state.bg + state256++ + case 39: + state.fg = -1 + case 49: + state.bg = -1 + case 1: + state.attr = state.attr | tui.Bold + case 2: + state.attr = state.attr | tui.Dim + case 3: + state.attr = state.attr | tui.Italic + case 4: + state.attr = state.attr | tui.Underline + case 5: + state.attr = state.attr | tui.Blink + case 7: + state.attr = state.attr | tui.Reverse + case 23: // tput rmso + state.attr = state.attr &^ tui.Italic + case 24: // tput rmul + state.attr = state.attr &^ tui.Underline + case 0: + state.fg = -1 + state.bg = -1 + state.attr = 0 + state256 = 0 + default: + if num >= 30 && num <= 37 { + state.fg = tui.Color(num - 30) + } else if num >= 40 && num <= 47 { + state.bg = tui.Color(num - 40) + } else if num >= 90 && num <= 97 { + state.fg = tui.Color(num - 90 + 8) + } else if num >= 100 && num <= 107 { + state.bg = tui.Color(num - 100 + 8) + } + } + case 1: + switch num { + case 2: + state256 = 10 // MAGIC + case 5: + state256++ + default: + state256 = 0 + } + case 2: + *ptr = tui.Color(num) + state256 = 0 + case 10: + *ptr = tui.Color(1<<24) | tui.Color(num<<16) + state256++ + case 11: + *ptr = *ptr | tui.Color(num<<8) + state256++ + case 12: + *ptr = *ptr | tui.Color(num) + state256 = 0 + } + } + } + + if state256 > 0 { + *ptr = -1 + } + return state +} diff --git a/fzf/fzf/src/ansi_test.go b/fzf/fzf/src/ansi_test.go new file mode 100644 index 0000000..cdccc10 --- /dev/null +++ b/fzf/fzf/src/ansi_test.go @@ -0,0 +1,427 @@ +package fzf + +import ( + "math/rand" + "regexp" + "strings" + "testing" + "unicode/utf8" + + "github.com/junegunn/fzf/src/tui" +) + +// The following regular expression will include not all but most of the +// frequently used ANSI sequences. This regex is used as a reference for +// testing nextAnsiEscapeSequence(). +// +// References: +// - https://github.com/gnachman/iTerm2 +// - https://web.archive.org/web/20090204053813/http://ascii-table.com/ansi-escape-sequences.php +// (archived from http://ascii-table.com/ansi-escape-sequences.php) +// - https://web.archive.org/web/20090227051140/http://ascii-table.com/ansi-escape-sequences-vt-100.php +// (archived from http://ascii-table.com/ansi-escape-sequences-vt-100.php) +// - http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html +// - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html +var ansiRegexReference = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)") + +func testParserReference(t testing.TB, str string) { + t.Helper() + + toSlice := func(start, end int) []int { + if start == -1 { + return nil + } + return []int{start, end} + } + + s := str + for i := 0; ; i++ { + got := toSlice(nextAnsiEscapeSequence(s)) + exp := ansiRegexReference.FindStringIndex(s) + + equal := len(got) == len(exp) + if equal { + for i := 0; i < len(got); i++ { + if got[i] != exp[i] { + equal = false + break + } + } + } + if !equal { + var exps, gots []rune + if len(got) == 2 { + gots = []rune(s[got[0]:got[1]]) + } + if len(exp) == 2 { + exps = []rune(s[exp[0]:exp[1]]) + } + t.Errorf("%d: %q: got: %v (%q) want: %v (%q)", i, s, got, gots, exp, exps) + return + } + if len(exp) == 0 { + return + } + s = s[exp[1]:] + } +} + +func TestNextAnsiEscapeSequence(t *testing.T) { + testStrs := []string{ + "\x1b[0mhello world", + "\x1b[1mhello world", + "椙\x1b[1m椙", + "椙\x1b[1椙m椙", + "\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d", + "\x1b[1mhello \x1b[Kworld", + "hello \x1b[34;45;1mworld", + "hello \x1b[34;45;1mwor\x1b[34;45;1mld", + "hello \x1b[34;45;1mwor\x1b[0mld", + "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md", + "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md", + "hello \x1b[32;1mworld", + "hello world", + "hello \x1b[0;38;5;200;48;5;100mworld", + "\x1b椙", + "椙\x08", + "\n\x08", + "X\x08", + "", + "\x1b]4;3;rgb:aa/bb/cc\x07 ", + "\x1b]4;3;rgb:aa/bb/cc\x1b\\ ", + ansiBenchmarkString, + } + + for _, s := range testStrs { + testParserReference(t, s) + } +} + +func TestNextAnsiEscapeSequence_Fuzz_Modified(t *testing.T) { + t.Parallel() + if testing.Short() { + t.Skip("short test") + } + + testStrs := []string{ + "\x1b[0mhello world", + "\x1b[1mhello world", + "椙\x1b[1m椙", + "椙\x1b[1椙m椙", + "\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d", + "\x1b[1mhello \x1b[Kworld", + "hello \x1b[34;45;1mworld", + "hello \x1b[34;45;1mwor\x1b[34;45;1mld", + "hello \x1b[34;45;1mwor\x1b[0mld", + "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md", + "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md", + "hello \x1b[32;1mworld", + "hello world", + "hello \x1b[0;38;5;200;48;5;100mworld", + ansiBenchmarkString, + } + + replacementBytes := [...]rune{'\x0e', '\x0f', '\x1b', '\x08'} + + modifyString := func(s string, rr *rand.Rand) string { + n := rr.Intn(len(s)) + b := []rune(s) + for ; n >= 0 && len(b) != 0; n-- { + i := rr.Intn(len(b)) + switch x := rr.Intn(4); x { + case 0: + b = append(b[:i], b[i+1:]...) + case 1: + j := rr.Intn(len(replacementBytes) - 1) + b[i] = replacementBytes[j] + case 2: + x := rune(rr.Intn(utf8.MaxRune)) + for !utf8.ValidRune(x) { + x = rune(rr.Intn(utf8.MaxRune)) + } + b[i] = x + case 3: + b[i] = rune(rr.Intn(utf8.MaxRune)) // potentially invalid + default: + t.Fatalf("unsupported value: %d", x) + } + } + return string(b) + } + + rr := rand.New(rand.NewSource(1)) + for _, s := range testStrs { + for i := 1_000; i >= 0; i-- { + testParserReference(t, modifyString(s, rr)) + } + } +} + +func TestNextAnsiEscapeSequence_Fuzz_Random(t *testing.T) { + t.Parallel() + + if testing.Short() { + t.Skip("short test") + } + + randomString := func(rr *rand.Rand) string { + numChars := rand.Intn(50) + codePoints := make([]rune, numChars) + for i := 0; i < len(codePoints); i++ { + var r rune + for n := 0; n < 1000; n++ { + r = rune(rr.Intn(utf8.MaxRune)) + // Allow 10% of runes to be invalid + if utf8.ValidRune(r) || rr.Float64() < 0.10 { + break + } + } + codePoints[i] = r + } + return string(codePoints) + } + + rr := rand.New(rand.NewSource(1)) + for i := 0; i < 100_000; i++ { + testParserReference(t, randomString(rr)) + } +} + +func TestExtractColor(t *testing.T) { + assert := func(offset ansiOffset, b int32, e int32, fg tui.Color, bg tui.Color, bold bool) { + var attr tui.Attr + if bold { + attr = tui.Bold + } + if offset.offset[0] != b || offset.offset[1] != e || + offset.color.fg != fg || offset.color.bg != bg || offset.color.attr != attr { + t.Error(offset, b, e, fg, bg, attr) + } + } + + src := "hello world" + var state *ansiState + clean := "\x1b[0m" + check := func(assertion func(ansiOffsets *[]ansiOffset, state *ansiState)) { + output, ansiOffsets, newState := extractColor(src, state, nil) + state = newState + if output != "hello world" { + t.Errorf("Invalid output: %s %v", output, []rune(output)) + } + t.Log(src, ansiOffsets, clean) + assertion(ansiOffsets, state) + } + + check(func(offsets *[]ansiOffset, state *ansiState) { + if offsets != nil { + t.Fail() + } + }) + + state = nil + src = "\x1b[0mhello world" + check(func(offsets *[]ansiOffset, state *ansiState) { + if offsets != nil { + t.Fail() + } + }) + + state = nil + src = "\x1b[1mhello world" + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { + t.Fail() + } + assert((*offsets)[0], 0, 11, -1, -1, true) + }) + + state = nil + src = "\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d" + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { + t.Fail() + } + assert((*offsets)[0], 0, 6, -1, -1, true) + }) + + state = nil + src = "\x1b[1mhello \x1b[Kworld" + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { + t.Fail() + } + assert((*offsets)[0], 0, 11, -1, -1, true) + }) + + state = nil + src = "hello \x1b[34;45;1mworld" + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { + t.Fail() + } + assert((*offsets)[0], 6, 11, 4, 5, true) + }) + + state = nil + src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld" + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { + t.Fail() + } + assert((*offsets)[0], 6, 11, 4, 5, true) + }) + + state = nil + src = "hello \x1b[34;45;1mwor\x1b[0mld" + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { + t.Fail() + } + assert((*offsets)[0], 6, 9, 4, 5, true) + }) + + state = nil + src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md" + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 3 { + t.Fail() + } + assert((*offsets)[0], 6, 8, 4, 233, true) + assert((*offsets)[1], 8, 9, 161, 233, true) + assert((*offsets)[2], 10, 11, 161, -1, false) + }) + + // {38,48};5;{38,48} + state = nil + src = "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md" + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 2 { + t.Fail() + } + assert((*offsets)[0], 6, 9, 38, 48, true) + assert((*offsets)[1], 9, 10, 48, 38, true) + }) + + src = "hello \x1b[32;1mworld" + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { + t.Fail() + } + if state.fg != 2 || state.bg != -1 || state.attr == 0 { + t.Fail() + } + assert((*offsets)[0], 6, 11, 2, -1, true) + }) + + src = "hello world" + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { + t.Fail() + } + if state.fg != 2 || state.bg != -1 || state.attr == 0 { + t.Fail() + } + assert((*offsets)[0], 0, 11, 2, -1, true) + }) + + src = "hello \x1b[0;38;5;200;48;5;100mworld" + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 2 { + t.Fail() + } + if state.fg != 200 || state.bg != 100 || state.attr > 0 { + t.Fail() + } + assert((*offsets)[0], 0, 6, 2, -1, true) + assert((*offsets)[1], 6, 11, 200, 100, false) + }) +} + +func TestAnsiCodeStringConversion(t *testing.T) { + assert := func(code string, prevState *ansiState, expected string) { + state := interpretCode(code, prevState) + if expected != state.ToString() { + t.Errorf("expected: %s, actual: %s", + strings.Replace(expected, "\x1b[", "\\x1b[", -1), + strings.Replace(state.ToString(), "\x1b[", "\\x1b[", -1)) + } + } + assert("\x1b[m", nil, "") + assert("\x1b[m", &ansiState{attr: tui.Blink, lbg: -1}, "") + + assert("\x1b[31m", nil, "\x1b[31;49m") + assert("\x1b[41m", nil, "\x1b[39;41m") + + assert("\x1b[92m", nil, "\x1b[92;49m") + assert("\x1b[102m", nil, "\x1b[39;102m") + + assert("\x1b[31m", &ansiState{fg: 4, bg: 4, lbg: -1}, "\x1b[31;44m") + assert("\x1b[1;2;31m", &ansiState{fg: 2, bg: -1, attr: tui.Reverse, lbg: -1}, "\x1b[1;2;7;31;49m") + assert("\x1b[38;5;100;48;5;200m", nil, "\x1b[38;5;100;48;5;200m") + assert("\x1b[48;5;100;38;5;200m", nil, "\x1b[38;5;200;48;5;100m") + assert("\x1b[48;5;100;38;2;10;20;30;1m", nil, "\x1b[1;38;2;10;20;30;48;5;100m") + assert("\x1b[48;5;100;38;2;10;20;30;7m", + &ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1}, + "\x1b[2;3;7;38;2;10;20;30;48;5;100m") +} + +func TestParseAnsiCode(t *testing.T) { + tests := []struct { + In, Exp string + N int + }{ + {"123", "", 123}, + {"1a", "", -1}, + {"1a;12", "12", -1}, + {"12;a", "a", 12}, + {"-2", "", -1}, + } + for _, x := range tests { + n, s := parseAnsiCode(x.In) + if n != x.N || s != x.Exp { + t.Fatalf("%q: got: (%d %q) want: (%d %q)", x.In, n, s, x.N, x.Exp) + } + } +} + +// kernel/bpf/preload/iterators/README +const ansiBenchmarkString = "\x1b[38;5;81m\x1b[01;31m\x1b[Kkernel/\x1b[0m\x1b[38;5;81mbpf/" + + "\x1b[0m\x1b[38;5;81mpreload/\x1b[0m\x1b[38;5;81miterators/" + + "\x1b[0m\x1b[38;5;149mMakefile\x1b[m\x1b[K\x1b[0m" + +func BenchmarkNextAnsiEscapeSequence(b *testing.B) { + b.SetBytes(int64(len(ansiBenchmarkString))) + for i := 0; i < b.N; i++ { + s := ansiBenchmarkString + for { + _, o := nextAnsiEscapeSequence(s) + if o == -1 { + break + } + s = s[o:] + } + } +} + +// Baseline test to compare the speed of nextAnsiEscapeSequence() to the +// previously used regex based implementation. +func BenchmarkNextAnsiEscapeSequence_Regex(b *testing.B) { + b.SetBytes(int64(len(ansiBenchmarkString))) + for i := 0; i < b.N; i++ { + s := ansiBenchmarkString + for { + a := ansiRegexReference.FindStringIndex(s) + if len(a) == 0 { + break + } + s = s[a[1]:] + } + } +} + +func BenchmarkExtractColor(b *testing.B) { + b.SetBytes(int64(len(ansiBenchmarkString))) + for i := 0; i < b.N; i++ { + extractColor(ansiBenchmarkString, nil, nil) + } +} diff --git a/fzf/fzf/src/cache.go b/fzf/fzf/src/cache.go new file mode 100644 index 0000000..df1a6ab --- /dev/null +++ b/fzf/fzf/src/cache.go @@ -0,0 +1,81 @@ +package fzf + +import "sync" + +// queryCache associates strings to lists of items +type queryCache map[string][]Result + +// ChunkCache associates Chunk and query string to lists of items +type ChunkCache struct { + mutex sync.Mutex + cache map[*Chunk]*queryCache +} + +// NewChunkCache returns a new ChunkCache +func NewChunkCache() ChunkCache { + return ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)} +} + +// Add adds the list to the cache +func (cc *ChunkCache) Add(chunk *Chunk, key string, list []Result) { + if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax { + return + } + + cc.mutex.Lock() + defer cc.mutex.Unlock() + + qc, ok := cc.cache[chunk] + if !ok { + cc.cache[chunk] = &queryCache{} + qc = cc.cache[chunk] + } + (*qc)[key] = list +} + +// Lookup is called to lookup ChunkCache +func (cc *ChunkCache) Lookup(chunk *Chunk, key string) []Result { + if len(key) == 0 || !chunk.IsFull() { + return nil + } + + cc.mutex.Lock() + defer cc.mutex.Unlock() + + qc, ok := cc.cache[chunk] + if ok { + list, ok := (*qc)[key] + if ok { + return list + } + } + return nil +} + +func (cc *ChunkCache) Search(chunk *Chunk, key string) []Result { + if len(key) == 0 || !chunk.IsFull() { + return nil + } + + cc.mutex.Lock() + defer cc.mutex.Unlock() + + qc, ok := cc.cache[chunk] + if !ok { + return nil + } + + for idx := 1; idx < len(key); idx++ { + // [---------| ] | [ |---------] + // [--------| ] | [ |--------] + // [-------| ] | [ |-------] + prefix := key[:len(key)-idx] + suffix := key[idx:] + for _, substr := range [2]string{prefix, suffix} { + if cached, found := (*qc)[substr]; found { + return cached + } + } + } + return nil +} diff --git a/fzf/fzf/src/cache_test.go b/fzf/fzf/src/cache_test.go new file mode 100644 index 0000000..0242534 --- /dev/null +++ b/fzf/fzf/src/cache_test.go @@ -0,0 +1,39 @@ +package fzf + +import "testing" + +func TestChunkCache(t *testing.T) { + cache := NewChunkCache() + chunk1p := &Chunk{} + chunk2p := &Chunk{count: chunkSize} + items1 := []Result{{}} + items2 := []Result{{}, {}} + cache.Add(chunk1p, "foo", items1) + cache.Add(chunk2p, "foo", items1) + cache.Add(chunk2p, "bar", items2) + + { // chunk1 is not full + cached := cache.Lookup(chunk1p, "foo") + if cached != nil { + t.Error("Cached disabled for non-empty chunks", cached) + } + } + { + cached := cache.Lookup(chunk2p, "foo") + if cached == nil || len(cached) != 1 { + t.Error("Expected 1 item cached", cached) + } + } + { + cached := cache.Lookup(chunk2p, "bar") + if cached == nil || len(cached) != 2 { + t.Error("Expected 2 items cached", cached) + } + } + { + cached := cache.Lookup(chunk1p, "foobar") + if cached != nil { + t.Error("Expected 0 item cached", cached) + } + } +} diff --git a/fzf/fzf/src/chunklist.go b/fzf/fzf/src/chunklist.go new file mode 100644 index 0000000..cd635c2 --- /dev/null +++ b/fzf/fzf/src/chunklist.go @@ -0,0 +1,89 @@ +package fzf + +import "sync" + +// Chunk is a list of Items whose size has the upper limit of chunkSize +type Chunk struct { + items [chunkSize]Item + count int +} + +// ItemBuilder is a closure type that builds Item object from byte array +type ItemBuilder func(*Item, []byte) bool + +// ChunkList is a list of Chunks +type ChunkList struct { + chunks []*Chunk + mutex sync.Mutex + trans ItemBuilder +} + +// NewChunkList returns a new ChunkList +func NewChunkList(trans ItemBuilder) *ChunkList { + return &ChunkList{ + chunks: []*Chunk{}, + mutex: sync.Mutex{}, + trans: trans} +} + +func (c *Chunk) push(trans ItemBuilder, data []byte) bool { + if trans(&c.items[c.count], data) { + c.count++ + return true + } + return false +} + +// IsFull returns true if the Chunk is full +func (c *Chunk) IsFull() bool { + return c.count == chunkSize +} + +func (cl *ChunkList) lastChunk() *Chunk { + return cl.chunks[len(cl.chunks)-1] +} + +// CountItems returns the total number of Items +func CountItems(cs []*Chunk) int { + if len(cs) == 0 { + return 0 + } + return chunkSize*(len(cs)-1) + cs[len(cs)-1].count +} + +// Push adds the item to the list +func (cl *ChunkList) Push(data []byte) bool { + cl.mutex.Lock() + + if len(cl.chunks) == 0 || cl.lastChunk().IsFull() { + cl.chunks = append(cl.chunks, &Chunk{}) + } + + ret := cl.lastChunk().push(cl.trans, data) + cl.mutex.Unlock() + return ret +} + +// Clear clears the data +func (cl *ChunkList) Clear() { + cl.mutex.Lock() + cl.chunks = nil + cl.mutex.Unlock() +} + +// Snapshot returns immutable snapshot of the ChunkList +func (cl *ChunkList) Snapshot() ([]*Chunk, int) { + cl.mutex.Lock() + + ret := make([]*Chunk, len(cl.chunks)) + copy(ret, cl.chunks) + + // Duplicate the last chunk + if cnt := len(ret); cnt > 0 { + newChunk := *ret[cnt-1] + ret[cnt-1] = &newChunk + } + + cl.mutex.Unlock() + return ret, CountItems(ret) +} diff --git a/fzf/fzf/src/chunklist_test.go b/fzf/fzf/src/chunklist_test.go new file mode 100644 index 0000000..6c1d09e --- /dev/null +++ b/fzf/fzf/src/chunklist_test.go @@ -0,0 +1,80 @@ +package fzf + +import ( + "fmt" + "testing" + + "github.com/junegunn/fzf/src/util" +) + +func TestChunkList(t *testing.T) { + // FIXME global + sortCriteria = []criterion{byScore, byLength} + + cl := NewChunkList(func(item *Item, s []byte) bool { + item.text = util.ToChars(s) + return true + }) + + // Snapshot + snapshot, count := cl.Snapshot() + if len(snapshot) > 0 || count > 0 { + t.Error("Snapshot should be empty now") + } + + // Add some data + cl.Push([]byte("hello")) + cl.Push([]byte("world")) + + // Previously created snapshot should remain the same + if len(snapshot) > 0 { + t.Error("Snapshot should not have changed") + } + + // But the new snapshot should contain the added items + snapshot, count = cl.Snapshot() + if len(snapshot) != 1 && count != 2 { + t.Error("Snapshot should not be empty now") + } + + // Check the content of the ChunkList + chunk1 := snapshot[0] + if chunk1.count != 2 { + t.Error("Snapshot should contain only two items") + } + if chunk1.items[0].text.ToString() != "hello" || + chunk1.items[1].text.ToString() != "world" { + t.Error("Invalid data") + } + if chunk1.IsFull() { + t.Error("Chunk should not have been marked full yet") + } + + // Add more data + for i := 0; i < chunkSize*2; i++ { + cl.Push([]byte(fmt.Sprintf("item %d", i))) + } + + // Previous snapshot should remain the same + if len(snapshot) != 1 { + t.Error("Snapshot should stay the same") + } + + // New snapshot + snapshot, count = cl.Snapshot() + if len(snapshot) != 3 || !snapshot[0].IsFull() || + !snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 { + t.Error("Expected two full chunks and one more chunk") + } + if snapshot[2].count != 2 { + t.Error("Unexpected number of items") + } + + cl.Push([]byte("hello")) + cl.Push([]byte("world")) + + lastChunkCount := snapshot[len(snapshot)-1].count + if lastChunkCount != 2 { + t.Error("Unexpected number of items:", lastChunkCount) + } +} diff --git a/fzf/fzf/src/constants.go b/fzf/fzf/src/constants.go new file mode 100644 index 0000000..96d9821 --- /dev/null +++ b/fzf/fzf/src/constants.go @@ -0,0 +1,85 @@ +package fzf + +import ( + "math" + "os" + "time" + + "github.com/junegunn/fzf/src/util" +) + +const ( + // Core + coordinatorDelayMax time.Duration = 100 * time.Millisecond + coordinatorDelayStep time.Duration = 10 * time.Millisecond + + // Reader + readerBufferSize = 64 * 1024 + readerPollIntervalMin = 10 * time.Millisecond + readerPollIntervalStep = 5 * time.Millisecond + readerPollIntervalMax = 50 * time.Millisecond + + // Terminal + initialDelay = 20 * time.Millisecond + initialDelayTac = 100 * time.Millisecond + spinnerDuration = 100 * time.Millisecond + previewCancelWait = 500 * time.Millisecond + previewChunkDelay = 100 * time.Millisecond + previewDelayed = 500 * time.Millisecond + maxPatternLength = 300 + maxMulti = math.MaxInt32 + + // Matcher + numPartitionsMultiplier = 8 + maxPartitions = 32 + progressMinDuration = 200 * time.Millisecond + + // Capacity of each chunk + chunkSize int = 100 + + // Pre-allocated memory slices to minimize GC + slab16Size int = 100 * 1024 // 200KB * 32 = 12.8MB + slab32Size int = 2048 // 8KB * 32 = 256KB + + // Do not cache results of low selectivity queries + queryCacheMax int = chunkSize / 5 + + // Not to cache mergers with large lists + mergerCacheMax int = 100000 + + // History + defaultHistoryMax int = 1000 + + // Jump labels + defaultJumpLabels string = "asdfghjklqwertyuiopzxcvbnm1234567890ASDFGHJKLQWERTYUIOPZXCVBNM`~;:,<.>/?'\"!@#$%^&*()[{]}-_=+" +) + +var defaultCommand string + +func init() { + if !util.IsWindows() { + defaultCommand = `set -o pipefail; command find -L . -mindepth 1 \( -path '*/\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \) -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-` + } else if os.Getenv("TERM") == "cygwin" { + defaultCommand = `sh -c "command find -L . -mindepth 1 -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-"` + } +} + +// fzf events +const ( + EvtReadNew util.EventType = iota + EvtReadFin + EvtSearchNew + EvtSearchProgress + EvtSearchFin + EvtHeader + EvtReady + EvtQuit +) + +const ( + exitCancel = -1 + exitOk = 0 + exitNoMatch = 1 + exitError = 2 + exitInterrupt = 130 +) diff --git a/fzf/fzf/src/core.go b/fzf/fzf/src/core.go new file mode 100644 index 0000000..6244c99 --- /dev/null +++ b/fzf/fzf/src/core.go @@ -0,0 +1,351 @@ +/* +Package fzf implements fzf, a command-line fuzzy finder. + +The MIT License (MIT) + +Copyright (c) 2013-2021 Junegunn Choi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package fzf + +import ( + "fmt" + "os" + "time" + + "github.com/junegunn/fzf/src/util" +) + +/* +Reader -> EvtReadFin +Reader -> EvtReadNew -> Matcher (restart) +Terminal -> EvtSearchNew:bool -> Matcher (restart) +Matcher -> EvtSearchProgress -> Terminal (update info) +Matcher -> EvtSearchFin -> Terminal (update list) +Matcher -> EvtHeader -> Terminal (update header) +*/ + +// Run starts fzf +func Run(opts *Options, version string, revision string) { + sort := opts.Sort > 0 + sortCriteria = opts.Criteria + + if opts.Version { + if len(revision) > 0 { + fmt.Printf("%s (%s)\n", version, revision) + } else { + fmt.Println(version) + } + os.Exit(exitOk) + } + + // Event channel + eventBox := util.NewEventBox() + + // ANSI code processor + ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) { + return util.ToChars(data), nil + } + + var lineAnsiState, prevLineAnsiState *ansiState + if opts.Ansi { + if opts.Theme.Colored { + ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) { + prevLineAnsiState = lineAnsiState + trimmed, offsets, newState := extractColor(string(data), lineAnsiState, nil) + lineAnsiState = newState + return util.ToChars([]byte(trimmed)), offsets + } + } else { + // When color is disabled but ansi option is given, + // we simply strip out ANSI codes from the input + ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) { + trimmed, _, _ := extractColor(string(data), nil, nil) + return util.ToChars([]byte(trimmed)), nil + } + } + } + + // Chunk list + var chunkList *ChunkList + var itemIndex int32 + header := make([]string, 0, opts.HeaderLines) + if len(opts.WithNth) == 0 { + chunkList = NewChunkList(func(item *Item, data []byte) bool { + if len(header) < opts.HeaderLines { + header = append(header, string(data)) + eventBox.Set(EvtHeader, header) + return false + } + item.text, item.colors = ansiProcessor(data) + item.text.Index = itemIndex + itemIndex++ + return true + }) + } else { + chunkList = NewChunkList(func(item *Item, data []byte) bool { + tokens := Tokenize(string(data), opts.Delimiter) + if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 { + var ansiState *ansiState + if prevLineAnsiState != nil { + ansiStateDup := *prevLineAnsiState + ansiState = &ansiStateDup + } + for _, token := range tokens { + prevAnsiState := ansiState + _, _, ansiState = extractColor(token.text.ToString(), ansiState, nil) + if prevAnsiState != nil { + token.text.Prepend("\x1b[m" + prevAnsiState.ToString()) + } else { + token.text.Prepend("\x1b[m") + } + } + } + trans := Transform(tokens, opts.WithNth) + transformed := joinTokens(trans) + if len(header) < opts.HeaderLines { + header = append(header, transformed) + eventBox.Set(EvtHeader, header) + return false + } + item.text, item.colors = ansiProcessor([]byte(transformed)) + item.text.TrimTrailingWhitespaces() + item.text.Index = itemIndex + item.origText = &data + itemIndex++ + return true + }) + } + + // Reader + streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync + var reader *Reader + if !streamingFilter { + reader = NewReader(func(data []byte) bool { + return chunkList.Push(data) + }, eventBox, opts.ReadZero, opts.Filter == nil) + go reader.ReadSource() + } + + // Matcher + forward := true + for _, cri := range opts.Criteria[1:] { + if cri == byEnd { + forward = false + break + } + if cri == byBegin { + break + } + } + patternBuilder := func(runes []rune) *Pattern { + return BuildPattern( + opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, + opts.Filter == nil, opts.Nth, opts.Delimiter, runes) + } + matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) + + // Filtering mode + if opts.Filter != nil { + if opts.PrintQuery { + opts.Printer(*opts.Filter) + } + + pattern := patternBuilder([]rune(*opts.Filter)) + matcher.sort = pattern.sortable + + found := false + if streamingFilter { + slab := util.MakeSlab(slab16Size, slab32Size) + reader := NewReader( + func(runes []byte) bool { + item := Item{} + if chunkList.trans(&item, runes) { + if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil { + opts.Printer(item.text.ToString()) + found = true + } + } + return false + }, eventBox, opts.ReadZero, false) + reader.ReadSource() + } else { + eventBox.Unwatch(EvtReadNew) + eventBox.WaitFor(EvtReadFin) + + snapshot, _ := chunkList.Snapshot() + merger, _ := matcher.scan(MatchRequest{ + chunks: snapshot, + pattern: pattern}) + for i := 0; i < merger.Length(); i++ { + opts.Printer(merger.Get(i).item.AsString(opts.Ansi)) + found = true + } + } + if found { + os.Exit(exitOk) + } + os.Exit(exitNoMatch) + } + + // Synchronous search + if opts.Sync { + eventBox.Unwatch(EvtReadNew) + eventBox.WaitFor(EvtReadFin) + } + + // Go interactive + go matcher.Loop() + + // Terminal I/O + terminal := NewTerminal(opts, eventBox) + deferred := opts.Select1 || opts.Exit0 + go terminal.Loop() + if !deferred { + terminal.startChan <- true + } + + // Event coordination + reading := true + clearCache := util.Once(false) + clearSelection := util.Once(false) + ticks := 0 + var nextCommand *string + restart := func(command string) { + reading = true + clearCache = util.Once(true) + clearSelection = util.Once(true) + chunkList.Clear() + itemIndex = 0 + header = make([]string, 0, opts.HeaderLines) + go reader.restart(command) + } + eventBox.Watch(EvtReadNew) + query := []rune{} + for { + delay := true + ticks++ + input := func() []rune { + paused, input := terminal.Input() + if !paused { + query = input + } + return query + } + eventBox.Wait(func(events *util.Events) { + if _, fin := (*events)[EvtReadFin]; fin { + delete(*events, EvtReadNew) + } + for evt, value := range *events { + switch evt { + case EvtQuit: + if reading { + reader.terminate() + } + os.Exit(value.(int)) + case EvtReadNew, EvtReadFin: + if evt == EvtReadFin && nextCommand != nil { + restart(*nextCommand) + nextCommand = nil + break + } else { + reading = reading && evt == EvtReadNew + } + snapshot, count := chunkList.Snapshot() + terminal.UpdateCount(count, !reading, value.(*string)) + if opts.Sync { + opts.Sync = false + terminal.UpdateList(PassMerger(&snapshot, opts.Tac), false) + } + matcher.Reset(snapshot, input(), false, !reading, sort, clearCache()) + + case EvtSearchNew: + var command *string + switch val := value.(type) { + case searchRequest: + sort = val.sort + command = val.command + } + if command != nil { + if reading { + reader.terminate() + nextCommand = command + } else { + restart(*command) + } + break + } + snapshot, _ := chunkList.Snapshot() + matcher.Reset(snapshot, input(), true, !reading, sort, clearCache()) + delay = false + + case EvtSearchProgress: + switch val := value.(type) { + case float32: + terminal.UpdateProgress(val) + } + + case EvtHeader: + headerPadded := make([]string, opts.HeaderLines) + copy(headerPadded, value.([]string)) + terminal.UpdateHeader(headerPadded) + + case EvtSearchFin: + switch val := value.(type) { + case *Merger: + if deferred { + count := val.Length() + if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 { + deferred = false + terminal.startChan <- true + } else if val.final { + if opts.Exit0 && count == 0 || opts.Select1 && count == 1 { + if opts.PrintQuery { + opts.Printer(opts.Query) + } + if len(opts.Expect) > 0 { + opts.Printer("") + } + for i := 0; i < count; i++ { + opts.Printer(val.Get(i).item.AsString(opts.Ansi)) + } + if count > 0 { + os.Exit(exitOk) + } + os.Exit(exitNoMatch) + } + deferred = false + terminal.startChan <- true + } + } + terminal.UpdateList(val, clearSelection()) + } + } + } + events.Clear() + }) + if delay && reading { + dur := util.DurWithin( + time.Duration(ticks)*coordinatorDelayStep, + 0, coordinatorDelayMax) + time.Sleep(dur) + } + } +} diff --git a/fzf/fzf/src/history.go b/fzf/fzf/src/history.go new file mode 100644 index 0000000..45728d4 --- /dev/null +++ b/fzf/fzf/src/history.go @@ -0,0 +1,96 @@ +package fzf + +import ( + "errors" + "io/ioutil" + "os" + "strings" +) + +// History struct represents input history +type History struct { + path string + lines []string + modified map[int]string + maxSize int + cursor int +} + +// NewHistory returns the pointer to a new History struct +func NewHistory(path string, maxSize int) (*History, error) { + fmtError := func(e error) error { + if os.IsPermission(e) { + return errors.New("permission denied: " + path) + } + return errors.New("invalid history file: " + e.Error()) + } + + // Read history file + data, err := ioutil.ReadFile(path) + if err != nil { + // If it doesn't exist, check if we can create a file with the name + if os.IsNotExist(err) { + data = []byte{} + if err := ioutil.WriteFile(path, data, 0600); err != nil { + return nil, fmtError(err) + } + } else { + return nil, fmtError(err) + } + } + // Split lines and limit the maximum number of lines + lines := strings.Split(strings.Trim(string(data), "\n"), "\n") + if len(lines[len(lines)-1]) > 0 { + lines = append(lines, "") + } + return &History{ + path: path, + maxSize: maxSize, + lines: lines, + modified: make(map[int]string), + cursor: len(lines) - 1}, nil +} + +func (h *History) append(line string) error { + // We don't append empty lines + if len(line) == 0 { + return nil + } + + lines := append(h.lines[:len(h.lines)-1], line) + if len(lines) > h.maxSize { + lines = lines[len(lines)-h.maxSize:] + } + h.lines = append(lines, "") + return ioutil.WriteFile(h.path, []byte(strings.Join(h.lines, "\n")), 0600) +} + +func (h *History) override(str string) { + // You can update the history but they're not written to the file + if h.cursor == len(h.lines)-1 { + h.lines[h.cursor] = str + } else if h.cursor < len(h.lines)-1 { + h.modified[h.cursor] = str + } +} + +func (h *History) current() string { + if str, prs := h.modified[h.cursor]; prs { + return str + } + return h.lines[h.cursor] +} + +func (h *History) previous() string { + if h.cursor > 0 { + h.cursor-- + } + return h.current() +} + +func (h *History) next() string { + if h.cursor < len(h.lines)-1 { + h.cursor++ + } + return h.current() +} diff --git a/fzf/fzf/src/history_test.go b/fzf/fzf/src/history_test.go new file mode 100644 index 0000000..6294bde --- /dev/null +++ b/fzf/fzf/src/history_test.go @@ -0,0 +1,68 @@ +package fzf + +import ( + "io/ioutil" + "os" + "runtime" + "testing" +) + +func TestHistory(t *testing.T) { + maxHistory := 50 + + // Invalid arguments + var paths []string + if runtime.GOOS == "windows" { + // GOPATH should exist, so we shouldn't be able to override it + paths = []string{os.Getenv("GOPATH")} + } else { + paths = []string{"/etc", "/proc"} + } + + for _, path := range paths { + if _, e := NewHistory(path, maxHistory); e == nil { + t.Error("Error expected for: " + path) + } + } + + f, _ := ioutil.TempFile("", "fzf-history") + f.Close() + + { // Append lines + h, _ := NewHistory(f.Name(), maxHistory) + for i := 0; i < maxHistory+10; i++ { + h.append("foobar") + } + } + { // Read lines + h, _ := NewHistory(f.Name(), maxHistory) + if len(h.lines) != maxHistory+1 { + t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines)) + } + for i := 0; i < maxHistory; i++ { + if h.lines[i] != "foobar" { + t.Error("Expected: foobar, actual: " + h.lines[i]) + } + } + } + { // Append lines + h, _ := NewHistory(f.Name(), maxHistory) + h.append("barfoo") + h.append("") + h.append("foobarbaz") + } + { // Read lines again + h, _ := NewHistory(f.Name(), maxHistory) + if len(h.lines) != maxHistory+1 { + t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines)) + } + compare := func(idx int, exp string) { + if h.lines[idx] != exp { + t.Errorf("Expected: %s, actual: %s\n", exp, h.lines[idx]) + } + } + compare(maxHistory-3, "foobar") + compare(maxHistory-2, "barfoo") + compare(maxHistory-1, "foobarbaz") + } +} diff --git a/fzf/fzf/src/item.go b/fzf/fzf/src/item.go new file mode 100644 index 0000000..cb778cb --- /dev/null +++ b/fzf/fzf/src/item.go @@ -0,0 +1,44 @@ +package fzf + +import ( + "github.com/junegunn/fzf/src/util" +) + +// Item represents each input line. 56 bytes. +type Item struct { + text util.Chars // 32 = 24 + 1 + 1 + 2 + 4 + transformed *[]Token // 8 + origText *[]byte // 8 + colors *[]ansiOffset // 8 +} + +// Index returns ordinal index of the Item +func (item *Item) Index() int32 { + return item.text.Index +} + +var minItem = Item{text: util.Chars{Index: -1}} + +func (item *Item) TrimLength() uint16 { + return item.text.TrimLength() +} + +// Colors returns ansiOffsets of the Item +func (item *Item) Colors() []ansiOffset { + if item.colors == nil { + return []ansiOffset{} + } + return *item.colors +} + +// AsString returns the original string +func (item *Item) AsString(stripAnsi bool) string { + if item.origText != nil { + if stripAnsi { + trimmed, _, _ := extractColor(string(*item.origText), nil, nil) + return trimmed + } + return string(*item.origText) + } + return item.text.ToString() +} diff --git a/fzf/fzf/src/item_test.go b/fzf/fzf/src/item_test.go new file mode 100644 index 0000000..1efb5f1 --- /dev/null +++ b/fzf/fzf/src/item_test.go @@ -0,0 +1,23 @@ +package fzf + +import ( + "testing" + + "github.com/junegunn/fzf/src/util" +) + +func TestStringPtr(t *testing.T) { + orig := []byte("\x1b[34mfoo") + text := []byte("\x1b[34mbar") + item := Item{origText: &orig, text: util.ToChars(text)} + if item.AsString(true) != "foo" || item.AsString(false) != string(orig) { + t.Fail() + } + if item.AsString(true) != "foo" { + t.Fail() + } + item.origText = nil + if item.AsString(true) != string(text) || item.AsString(false) != string(text) { + t.Fail() + } +} diff --git a/fzf/fzf/src/matcher.go b/fzf/fzf/src/matcher.go new file mode 100644 index 0000000..22aa819 --- /dev/null +++ b/fzf/fzf/src/matcher.go @@ -0,0 +1,235 @@ +package fzf + +import ( + "fmt" + "runtime" + "sort" + "sync" + "time" + + "github.com/junegunn/fzf/src/util" +) + +// MatchRequest represents a search request +type MatchRequest struct { + chunks []*Chunk + pattern *Pattern + final bool + sort bool + clearCache bool +} + +// Matcher is responsible for performing search +type Matcher struct { + patternBuilder func([]rune) *Pattern + sort bool + tac bool + eventBox *util.EventBox + reqBox *util.EventBox + partitions int + slab []*util.Slab + mergerCache map[string]*Merger +} + +const ( + reqRetry util.EventType = iota + reqReset +) + +// NewMatcher returns a new Matcher +func NewMatcher(patternBuilder func([]rune) *Pattern, + sort bool, tac bool, eventBox *util.EventBox) *Matcher { + partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions) + return &Matcher{ + patternBuilder: patternBuilder, + sort: sort, + tac: tac, + eventBox: eventBox, + reqBox: util.NewEventBox(), + partitions: partitions, + slab: make([]*util.Slab, partitions), + mergerCache: make(map[string]*Merger)} +} + +// Loop puts Matcher in action +func (m *Matcher) Loop() { + prevCount := 0 + + for { + var request MatchRequest + + m.reqBox.Wait(func(events *util.Events) { + for _, val := range *events { + switch val := val.(type) { + case MatchRequest: + request = val + default: + panic(fmt.Sprintf("Unexpected type: %T", val)) + } + } + events.Clear() + }) + + if request.sort != m.sort || request.clearCache { + m.sort = request.sort + m.mergerCache = make(map[string]*Merger) + clearChunkCache() + } + + // Restart search + patternString := request.pattern.AsString() + var merger *Merger + cancelled := false + count := CountItems(request.chunks) + + foundCache := false + if count == prevCount { + // Look up mergerCache + if cached, found := m.mergerCache[patternString]; found { + foundCache = true + merger = cached + } + } else { + // Invalidate mergerCache + prevCount = count + m.mergerCache = make(map[string]*Merger) + } + + if !foundCache { + merger, cancelled = m.scan(request) + } + + if !cancelled { + if merger.cacheable() { + m.mergerCache[patternString] = merger + } + merger.final = request.final + m.eventBox.Set(EvtSearchFin, merger) + } + } +} + +func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk { + partitions := m.partitions + perSlice := len(chunks) / partitions + + if perSlice == 0 { + partitions = len(chunks) + perSlice = 1 + } + + slices := make([][]*Chunk, partitions) + for i := 0; i < partitions; i++ { + start := i * perSlice + end := start + perSlice + if i == partitions-1 { + end = len(chunks) + } + slices[i] = chunks[start:end] + } + return slices +} + +type partialResult struct { + index int + matches []Result +} + +func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { + startedAt := time.Now() + + numChunks := len(request.chunks) + if numChunks == 0 { + return EmptyMerger, false + } + pattern := request.pattern + if pattern.IsEmpty() { + return PassMerger(&request.chunks, m.tac), false + } + + cancelled := util.NewAtomicBool(false) + + slices := m.sliceChunks(request.chunks) + numSlices := len(slices) + resultChan := make(chan partialResult, numSlices) + countChan := make(chan int, numChunks) + waitGroup := sync.WaitGroup{} + + for idx, chunks := range slices { + waitGroup.Add(1) + if m.slab[idx] == nil { + m.slab[idx] = util.MakeSlab(slab16Size, slab32Size) + } + go func(idx int, slab *util.Slab, chunks []*Chunk) { + defer func() { waitGroup.Done() }() + count := 0 + allMatches := make([][]Result, len(chunks)) + for idx, chunk := range chunks { + matches := request.pattern.Match(chunk, slab) + allMatches[idx] = matches + count += len(matches) + if cancelled.Get() { + return + } + countChan <- len(matches) + } + sliceMatches := make([]Result, 0, count) + for _, matches := range allMatches { + sliceMatches = append(sliceMatches, matches...) + } + if m.sort { + if m.tac { + sort.Sort(ByRelevanceTac(sliceMatches)) + } else { + sort.Sort(ByRelevance(sliceMatches)) + } + } + resultChan <- partialResult{idx, sliceMatches} + }(idx, m.slab[idx], chunks) + } + + wait := func() bool { + cancelled.Set(true) + waitGroup.Wait() + return true + } + + count := 0 + matchCount := 0 + for matchesInChunk := range countChan { + count++ + matchCount += matchesInChunk + + if count == numChunks { + break + } + + if m.reqBox.Peek(reqReset) { + return nil, wait() + } + + if time.Since(startedAt) > progressMinDuration { + m.eventBox.Set(EvtSearchProgress, float32(count)/float32(numChunks)) + } + } + + partialResults := make([][]Result, numSlices) + for range slices { + partialResult := <-resultChan + partialResults[partialResult.index] = partialResult.matches + } + return NewMerger(pattern, partialResults, m.sort, m.tac), false +} + +// Reset is called to interrupt/signal the ongoing search +func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, clearCache bool) { + pattern := m.patternBuilder(patternRunes) + + var event util.EventType + if cancel { + event = reqReset + } else { + event = reqRetry + } + m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, clearCache}) +} diff --git a/fzf/fzf/src/merger.go b/fzf/fzf/src/merger.go new file mode 100644 index 0000000..8e6a884 --- /dev/null +++ b/fzf/fzf/src/merger.go @@ -0,0 +1,120 @@ +package fzf + +import "fmt" + +// EmptyMerger is a Merger with no data +var EmptyMerger = NewMerger(nil, [][]Result{}, false, false) + +// Merger holds a set of locally sorted lists of items and provides the view of +// a single, globally-sorted list +type Merger struct { + pattern *Pattern + lists [][]Result + merged []Result + chunks *[]*Chunk + cursors []int + sorted bool + tac bool + final bool + count int +} + +// PassMerger returns a new Merger that simply returns the items in the +// original order +func PassMerger(chunks *[]*Chunk, tac bool) *Merger { + mg := Merger{ + pattern: nil, + chunks: chunks, + tac: tac, + count: 0} + + for _, chunk := range *mg.chunks { + mg.count += chunk.count + } + return &mg +} + +// NewMerger returns a new Merger +func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merger { + mg := Merger{ + pattern: pattern, + lists: lists, + merged: []Result{}, + chunks: nil, + cursors: make([]int, len(lists)), + sorted: sorted, + tac: tac, + final: false, + count: 0} + + for _, list := range mg.lists { + mg.count += len(list) + } + return &mg +} + +// Length returns the number of items +func (mg *Merger) Length() int { + return mg.count +} + +// Get returns the pointer to the Result object indexed by the given integer +func (mg *Merger) Get(idx int) Result { + if mg.chunks != nil { + if mg.tac { + idx = mg.count - idx - 1 + } + chunk := (*mg.chunks)[idx/chunkSize] + return Result{item: &chunk.items[idx%chunkSize]} + } + + if mg.sorted { + return mg.mergedGet(idx) + } + + if mg.tac { + idx = mg.count - idx - 1 + } + for _, list := range mg.lists { + numItems := len(list) + if idx < numItems { + return list[idx] + } + idx -= numItems + } + panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count)) +} + +func (mg *Merger) cacheable() bool { + return mg.count < mergerCacheMax +} + +func (mg *Merger) mergedGet(idx int) Result { + for i := len(mg.merged); i <= idx; i++ { + minRank := minRank() + minIdx := -1 + for listIdx, list := range mg.lists { + cursor := mg.cursors[listIdx] + if cursor < 0 || cursor == len(list) { + mg.cursors[listIdx] = -1 + continue + } + if cursor >= 0 { + rank := list[cursor] + if minIdx < 0 || compareRanks(rank, minRank, mg.tac) { + minRank = rank + minIdx = listIdx + } + } + } + + if minIdx >= 0 { + chosen := mg.lists[minIdx] + mg.merged = append(mg.merged, chosen[mg.cursors[minIdx]]) + mg.cursors[minIdx]++ + } else { + panic(fmt.Sprintf("Index out of bounds (sorted, %d/%d)", i, mg.count)) + } + } + return mg.merged[idx] +} diff --git a/fzf/fzf/src/merger_test.go b/fzf/fzf/src/merger_test.go new file mode 100644 index 0000000..c6af4f6 --- /dev/null +++ b/fzf/fzf/src/merger_test.go @@ -0,0 +1,88 @@ +package fzf + +import ( + "fmt" + "math/rand" + "sort" + "testing" + + "github.com/junegunn/fzf/src/util" +) + +func assert(t *testing.T, cond bool, msg ...string) { + if !cond { + t.Error(msg) + } +} + +func randResult() Result { + str := fmt.Sprintf("%d", rand.Uint32()) + chars := util.ToChars([]byte(str)) + chars.Index = rand.Int31() + return Result{item: &Item{text: chars}} +} + +func TestEmptyMerger(t *testing.T) { + assert(t, EmptyMerger.Length() == 0, "Not empty") + assert(t, EmptyMerger.count == 0, "Invalid count") + assert(t, len(EmptyMerger.lists) == 0, "Invalid lists") + assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list") +} + +func buildLists(partiallySorted bool) ([][]Result, []Result) { + numLists := 4 + lists := make([][]Result, numLists) + cnt := 0 + for i := 0; i < numLists; i++ { + numResults := rand.Int() % 20 + cnt += numResults + lists[i] = make([]Result, numResults) + for j := 0; j < numResults; j++ { + item := randResult() + lists[i][j] = item + } + if partiallySorted { + sort.Sort(ByRelevance(lists[i])) + } + } + items := []Result{} + for _, list := range lists { + items = append(items, list...) + } + return lists, items +} + +func TestMergerUnsorted(t *testing.T) { + lists, items := buildLists(false) + cnt := len(items) + + // Not sorted: same order + mg := NewMerger(nil, lists, false, false) + assert(t, cnt == mg.Length(), "Invalid Length") + for i := 0; i < cnt; i++ { + assert(t, items[i] == mg.Get(i), "Invalid Get") + } +} + +func TestMergerSorted(t *testing.T) { + lists, items := buildLists(true) + cnt := len(items) + + // Sorted sorted order + mg := NewMerger(nil, lists, true, false) + assert(t, cnt == mg.Length(), "Invalid Length") + sort.Sort(ByRelevance(items)) + for i := 0; i < cnt; i++ { + if items[i] != mg.Get(i) { + t.Error("Not sorted", items[i], mg.Get(i)) + } + } + + // Inverse order + mg2 := NewMerger(nil, lists, true, false) + for i := cnt - 1; i >= 0; i-- { + if items[i] != mg2.Get(i) { + t.Error("Not sorted", items[i], mg2.Get(i)) + } + } +} diff --git a/fzf/fzf/src/options.go b/fzf/fzf/src/options.go new file mode 100644 index 0000000..b3bcdea --- /dev/null +++ b/fzf/fzf/src/options.go @@ -0,0 +1,1734 @@ +package fzf + +import ( + "fmt" + "os" + "regexp" + "strconv" + "strings" + "unicode" + + "github.com/junegunn/fzf/src/algo" + "github.com/junegunn/fzf/src/tui" + + "github.com/mattn/go-runewidth" + "github.com/mattn/go-shellwords" +) + +const usage = `usage: fzf [options] + + Search + -x, --extended Extended-search mode + (enabled by default; +x or --no-extended to disable) + -e, --exact Enable Exact-match + --algo=TYPE Fuzzy matching algorithm: [v1|v2] (default: v2) + -i Case-insensitive match (default: smart-case match) + +i Case-sensitive match + --literal Do not normalize latin script letters before matching + -n, --nth=N[,..] Comma-separated list of field index expressions + for limiting search scope. Each can be a non-zero + integer or a range expression ([BEGIN]..[END]). + --with-nth=N[,..] Transform the presentation of each line using + field index expressions + -d, --delimiter=STR Field delimiter regex (default: AWK-style) + +s, --no-sort Do not sort the result + --tac Reverse the order of the input + --disabled Do not perform search + --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply + when the scores are tied [length|begin|end|index] + (default: length) + + Interface + -m, --multi[=MAX] Enable multi-select with tab/shift-tab + --no-mouse Disable mouse + --bind=KEYBINDS Custom key bindings. Refer to the man page. + --cycle Enable cyclic scroll + --keep-right Keep the right end of the line visible on overflow + --scroll-off=LINES Number of screen lines to keep above or below when + scrolling to the top or to the bottom (default: 0) + --no-hscroll Disable horizontal scroll + --hscroll-off=COLS Number of screen columns to keep to the right of the + highlighted substring (default: 10) + --filepath-word Make word-wise movements respect path separators + --jump-labels=CHARS Label characters for jump and jump-accept + + Layout + --height=HEIGHT[%] Display fzf window below the cursor with the given + height instead of using fullscreen + --min-height=HEIGHT Minimum height when --height is given in percent + (default: 10) + --layout=LAYOUT Choose layout: [default|reverse|reverse-list] + --border[=STYLE] Draw border around the finder + [rounded|sharp|horizontal|vertical| + top|bottom|left|right|none] (default: rounded) + --margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L) + --padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L) + --info=STYLE Finder info style [default|inline|hidden] + --prompt=STR Input prompt (default: '> ') + --pointer=STR Pointer to the current line (default: '>') + --marker=STR Multi-select marker (default: '>') + --header=STR String to print as header + --header-lines=N The first N lines of the input are treated as header + --header-first Print header before the prompt line + + Display + --ansi Enable processing of ANSI color codes + --tabstop=SPACES Number of spaces for a tab character (default: 8) + --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors + --no-bold Do not use bold text + + History + --history=FILE History file + --history-size=N Maximum number of history entries (default: 1000) + + Preview + --preview=COMMAND Command to preview highlighted line ({}) + --preview-window=OPT Preview window layout (default: right:50%) + [up|down|left|right][,SIZE[%]] + [,[no]wrap][,[no]cycle][,[no]follow][,[no]hidden] + [,border-BORDER_OPT] + [,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES] + [,default] + + Scripting + -q, --query=STR Start the finder with the given query + -1, --select-1 Automatically select the only match + -0, --exit-0 Exit immediately when there's no match + -f, --filter=STR Filter mode. Do not start interactive finder. + --print-query Print query as the first line + --expect=KEYS Comma-separated list of keys to complete fzf + --read0 Read input delimited by ASCII NUL characters + --print0 Print output delimited by ASCII NUL characters + --sync Synchronous search for multi-staged filtering + --version Display version information and exit + + Environment variables + FZF_DEFAULT_COMMAND Default command to use when input is tty + FZF_DEFAULT_OPTS Default options + (e.g. '--layout=reverse --inline-info') + +` + +// Case denotes case-sensitivity of search +type Case int + +// Case-sensitivities +const ( + CaseSmart Case = iota + CaseIgnore + CaseRespect +) + +// Sort criteria +type criterion int + +const ( + byScore criterion = iota + byLength + byBegin + byEnd +) + +type sizeSpec struct { + size float64 + percent bool +} + +func defaultMargin() [4]sizeSpec { + return [4]sizeSpec{} +} + +type windowPosition int + +const ( + posUp windowPosition = iota + posDown + posLeft + posRight +) + +type layoutType int + +const ( + layoutDefault layoutType = iota + layoutReverse + layoutReverseList +) + +type infoStyle int + +const ( + infoDefault infoStyle = iota + infoInline + infoHidden +) + +type previewOpts struct { + command string + position windowPosition + size sizeSpec + scroll string + hidden bool + wrap bool + cycle bool + follow bool + border tui.BorderShape + headerLines int +} + +func (a previewOpts) sameLayout(b previewOpts) bool { + return a.size == b.size && a.position == b.position && a.border == b.border && a.hidden == b.hidden +} + +func (a previewOpts) sameContentLayout(b previewOpts) bool { + return a.wrap == b.wrap && a.headerLines == b.headerLines +} + +// Options stores the values of command-line options +type Options struct { + Fuzzy bool + FuzzyAlgo algo.Algo + Extended bool + Phony bool + Case Case + Normalize bool + Nth []Range + WithNth []Range + Delimiter Delimiter + Sort int + Tac bool + Criteria []criterion + Multi int + Ansi bool + Mouse bool + Theme *tui.ColorTheme + Black bool + Bold bool + Height sizeSpec + MinHeight int + Layout layoutType + Cycle bool + KeepRight bool + Hscroll bool + HscrollOff int + ScrollOff int + FileWord bool + InfoStyle infoStyle + JumpLabels string + Prompt string + Pointer string + Marker string + Query string + Select1 bool + Exit0 bool + Filter *string + ToggleSort bool + Expect map[tui.Event]string + Keymap map[tui.Event][]*action + Preview previewOpts + PrintQuery bool + ReadZero bool + Printer func(string) + PrintSep string + Sync bool + History *History + Header []string + HeaderLines int + HeaderFirst bool + Margin [4]sizeSpec + Padding [4]sizeSpec + BorderShape tui.BorderShape + Unicode bool + Tabstop int + ClearOnExit bool + Version bool +} + +func defaultPreviewOpts(command string) previewOpts { + return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded, 0} +} + +func defaultOptions() *Options { + return &Options{ + Fuzzy: true, + FuzzyAlgo: algo.FuzzyMatchV2, + Extended: true, + Phony: false, + Case: CaseSmart, + Normalize: true, + Nth: make([]Range, 0), + WithNth: make([]Range, 0), + Delimiter: Delimiter{}, + Sort: 1000, + Tac: false, + Criteria: []criterion{byScore, byLength}, + Multi: 0, + Ansi: false, + Mouse: true, + Theme: tui.EmptyTheme(), + Black: false, + Bold: true, + MinHeight: 10, + Layout: layoutDefault, + Cycle: false, + KeepRight: false, + Hscroll: true, + HscrollOff: 10, + ScrollOff: 0, + FileWord: false, + InfoStyle: infoDefault, + JumpLabels: defaultJumpLabels, + Prompt: "> ", + Pointer: ">", + Marker: ">", + Query: "", + Select1: false, + Exit0: false, + Filter: nil, + ToggleSort: false, + Expect: make(map[tui.Event]string), + Keymap: make(map[tui.Event][]*action), + Preview: defaultPreviewOpts(""), + PrintQuery: false, + ReadZero: false, + Printer: func(str string) { fmt.Println(str) }, + PrintSep: "\n", + Sync: false, + History: nil, + Header: make([]string, 0), + HeaderLines: 0, + HeaderFirst: false, + Margin: defaultMargin(), + Padding: defaultMargin(), + Unicode: true, + Tabstop: 8, + ClearOnExit: true, + Version: false} +} + +func help(code int) { + os.Stdout.WriteString(usage) + os.Exit(code) +} + +func errorExit(msg string) { + os.Stderr.WriteString(msg + "\n") + os.Exit(exitError) +} + +func optString(arg string, prefixes ...string) (bool, string) { + for _, prefix := range prefixes { + if strings.HasPrefix(arg, prefix) { + return true, arg[len(prefix):] + } + } + return false, "" +} + +func nextString(args []string, i *int, message string) string { + if len(args) > *i+1 { + *i++ + } else { + errorExit(message) + } + return args[*i] +} + +func optionalNextString(args []string, i *int) (bool, string) { + if len(args) > *i+1 && !strings.HasPrefix(args[*i+1], "-") && !strings.HasPrefix(args[*i+1], "+") { + *i++ + return true, args[*i] + } + return false, "" +} + +func atoi(str string) int { + num, err := strconv.Atoi(str) + if err != nil { + errorExit("not a valid integer: " + str) + } + return num +} + +func atof(str string) float64 { + num, err := strconv.ParseFloat(str, 64) + if err != nil { + errorExit("not a valid number: " + str) + } + return num +} + +func nextInt(args []string, i *int, message string) int { + if len(args) > *i+1 { + *i++ + } else { + errorExit(message) + } + return atoi(args[*i]) +} + +func optionalNumeric(args []string, i *int, defaultValue int) int { + if len(args) > *i+1 { + if strings.IndexAny(args[*i+1], "0123456789") == 0 { + *i++ + return atoi(args[*i]) + } + } + return defaultValue +} + +func splitNth(str string) []Range { + if match, _ := regexp.MatchString("^[0-9,-.]+$", str); !match { + errorExit("invalid format: " + str) + } + + tokens := strings.Split(str, ",") + ranges := make([]Range, len(tokens)) + for idx, s := range tokens { + r, ok := ParseRange(&s) + if !ok { + errorExit("invalid format: " + str) + } + ranges[idx] = r + } + return ranges +} + +func delimiterRegexp(str string) Delimiter { + // Special handling of \t + str = strings.Replace(str, "\\t", "\t", -1) + + // 1. Pattern does not contain any special character + if regexp.QuoteMeta(str) == str { + return Delimiter{str: &str} + } + + rx, e := regexp.Compile(str) + // 2. Pattern is not a valid regular expression + if e != nil { + return Delimiter{str: &str} + } + + // 3. Pattern as regular expression. Slow. + return Delimiter{regex: rx} +} + +func isAlphabet(char uint8) bool { + return char >= 'a' && char <= 'z' +} + +func isNumeric(char uint8) bool { + return char >= '0' && char <= '9' +} + +func parseAlgo(str string) algo.Algo { + switch str { + case "v1": + return algo.FuzzyMatchV1 + case "v2": + return algo.FuzzyMatchV2 + default: + errorExit("invalid algorithm (expected: v1 or v2)") + } + return algo.FuzzyMatchV2 +} + +func parseBorder(str string, optional bool) tui.BorderShape { + switch str { + case "rounded": + return tui.BorderRounded + case "sharp": + return tui.BorderSharp + case "horizontal": + return tui.BorderHorizontal + case "vertical": + return tui.BorderVertical + case "top": + return tui.BorderTop + case "bottom": + return tui.BorderBottom + case "left": + return tui.BorderLeft + case "right": + return tui.BorderRight + case "none": + return tui.BorderNone + default: + if optional && str == "" { + return tui.BorderRounded + } + errorExit("invalid border style (expected: rounded|sharp|horizontal|vertical|top|bottom|left|right|none)") + } + return tui.BorderNone +} + +func parseKeyChords(str string, message string) map[tui.Event]string { + if len(str) == 0 { + errorExit(message) + } + + str = regexp.MustCompile("(?i)(alt-),").ReplaceAllString(str, "$1"+string([]rune{escapedComma})) + tokens := strings.Split(str, ",") + if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Contains(str, ",,,") { + tokens = append(tokens, ",") + } + + chords := make(map[tui.Event]string) + for _, key := range tokens { + if len(key) == 0 { + continue // ignore + } + key = strings.ReplaceAll(key, string([]rune{escapedComma}), ",") + lkey := strings.ToLower(key) + add := func(e tui.EventType) { + chords[e.AsEvent()] = key + } + switch lkey { + case "up": + add(tui.Up) + case "down": + add(tui.Down) + case "left": + add(tui.Left) + case "right": + add(tui.Right) + case "enter", "return": + add(tui.CtrlM) + case "space": + chords[tui.Key(' ')] = key + case "bspace", "bs": + add(tui.BSpace) + case "ctrl-space": + add(tui.CtrlSpace) + case "ctrl-^", "ctrl-6": + add(tui.CtrlCaret) + case "ctrl-/", "ctrl-_": + add(tui.CtrlSlash) + case "ctrl-\\": + add(tui.CtrlBackSlash) + case "ctrl-]": + add(tui.CtrlRightBracket) + case "change": + add(tui.Change) + case "backward-eof": + add(tui.BackwardEOF) + case "alt-enter", "alt-return": + chords[tui.CtrlAltKey('m')] = key + case "alt-space": + chords[tui.AltKey(' ')] = key + case "alt-bs", "alt-bspace": + add(tui.AltBS) + case "alt-up": + add(tui.AltUp) + case "alt-down": + add(tui.AltDown) + case "alt-left": + add(tui.AltLeft) + case "alt-right": + add(tui.AltRight) + case "tab": + add(tui.Tab) + case "btab", "shift-tab": + add(tui.BTab) + case "esc": + add(tui.ESC) + case "del": + add(tui.Del) + case "home": + add(tui.Home) + case "end": + add(tui.End) + case "insert": + add(tui.Insert) + case "pgup", "page-up": + add(tui.PgUp) + case "pgdn", "page-down": + add(tui.PgDn) + case "alt-shift-up", "shift-alt-up": + add(tui.AltSUp) + case "alt-shift-down", "shift-alt-down": + add(tui.AltSDown) + case "alt-shift-left", "shift-alt-left": + add(tui.AltSLeft) + case "alt-shift-right", "shift-alt-right": + add(tui.AltSRight) + case "shift-up": + add(tui.SUp) + case "shift-down": + add(tui.SDown) + case "shift-left": + add(tui.SLeft) + case "shift-right": + add(tui.SRight) + case "left-click": + add(tui.LeftClick) + case "right-click": + add(tui.RightClick) + case "double-click": + add(tui.DoubleClick) + case "f10": + add(tui.F10) + case "f11": + add(tui.F11) + case "f12": + add(tui.F12) + default: + runes := []rune(key) + if len(key) == 10 && strings.HasPrefix(lkey, "ctrl-alt-") && isAlphabet(lkey[9]) { + chords[tui.CtrlAltKey(rune(key[9]))] = key + } else if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { + add(tui.EventType(tui.CtrlA.Int() + int(lkey[5]) - 'a')) + } else if len(runes) == 5 && strings.HasPrefix(lkey, "alt-") { + r := runes[4] + switch r { + case escapedColon: + r = ':' + case escapedComma: + r = ',' + case escapedPlus: + r = '+' + } + chords[tui.AltKey(r)] = key + } else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' { + add(tui.EventType(tui.F1.Int() + int(key[1]) - '1')) + } else if len(runes) == 1 { + chords[tui.Key(runes[0])] = key + } else { + errorExit("unsupported key: " + key) + } + } + } + return chords +} + +func parseTiebreak(str string) []criterion { + criteria := []criterion{byScore} + hasIndex := false + hasLength := false + hasBegin := false + hasEnd := false + check := func(notExpected *bool, name string) { + if *notExpected { + errorExit("duplicate sort criteria: " + name) + } + if hasIndex { + errorExit("index should be the last criterion") + } + *notExpected = true + } + for _, str := range strings.Split(strings.ToLower(str), ",") { + switch str { + case "index": + check(&hasIndex, "index") + case "length": + check(&hasLength, "length") + criteria = append(criteria, byLength) + case "begin": + check(&hasBegin, "begin") + criteria = append(criteria, byBegin) + case "end": + check(&hasEnd, "end") + criteria = append(criteria, byEnd) + default: + errorExit("invalid sort criterion: " + str) + } + } + return criteria +} + +func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme { + dupe := *theme + return &dupe +} + +func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { + theme := dupeTheme(defaultTheme) + rrggbb := regexp.MustCompile("^#[0-9a-fA-F]{6}$") + for _, str := range strings.Split(strings.ToLower(str), ",") { + switch str { + case "dark": + theme = dupeTheme(tui.Dark256) + case "light": + theme = dupeTheme(tui.Light256) + case "16": + theme = dupeTheme(tui.Default16) + case "bw", "no": + theme = tui.NoColorTheme() + default: + fail := func() { + errorExit("invalid color specification: " + str) + } + // Color is disabled + if theme == nil { + continue + } + + components := strings.Split(str, ":") + if len(components) < 2 { + fail() + } + + mergeAttr := func(cattr *tui.ColorAttr) { + for _, component := range components[1:] { + switch component { + case "regular": + cattr.Attr = tui.AttrRegular + case "bold", "strong": + cattr.Attr |= tui.Bold + case "dim": + cattr.Attr |= tui.Dim + case "italic": + cattr.Attr |= tui.Italic + case "underline": + cattr.Attr |= tui.Underline + case "blink": + cattr.Attr |= tui.Blink + case "reverse": + cattr.Attr |= tui.Reverse + case "black": + cattr.Color = tui.Color(0) + case "red": + cattr.Color = tui.Color(1) + case "green": + cattr.Color = tui.Color(2) + case "yellow": + cattr.Color = tui.Color(3) + case "blue": + cattr.Color = tui.Color(4) + case "magenta": + cattr.Color = tui.Color(5) + case "cyan": + cattr.Color = tui.Color(6) + case "white": + cattr.Color = tui.Color(7) + case "bright-black", "gray", "grey": + cattr.Color = tui.Color(8) + case "bright-red": + cattr.Color = tui.Color(9) + case "bright-green": + cattr.Color = tui.Color(10) + case "bright-yellow": + cattr.Color = tui.Color(11) + case "bright-blue": + cattr.Color = tui.Color(12) + case "bright-magenta": + cattr.Color = tui.Color(13) + case "bright-cyan": + cattr.Color = tui.Color(14) + case "bright-white": + cattr.Color = tui.Color(15) + case "": + default: + if rrggbb.MatchString(component) { + cattr.Color = tui.HexToColor(component) + } else { + ansi32, err := strconv.Atoi(component) + if err != nil || ansi32 < -1 || ansi32 > 255 { + fail() + } + cattr.Color = tui.Color(ansi32) + } + } + } + } + switch components[0] { + case "query", "input": + mergeAttr(&theme.Input) + case "disabled": + mergeAttr(&theme.Disabled) + case "fg": + mergeAttr(&theme.Fg) + case "bg": + mergeAttr(&theme.Bg) + case "preview-fg": + mergeAttr(&theme.PreviewFg) + case "preview-bg": + mergeAttr(&theme.PreviewBg) + case "fg+": + mergeAttr(&theme.Current) + case "bg+": + mergeAttr(&theme.DarkBg) + case "gutter": + mergeAttr(&theme.Gutter) + case "hl": + mergeAttr(&theme.Match) + case "hl+": + mergeAttr(&theme.CurrentMatch) + case "border": + mergeAttr(&theme.Border) + case "prompt": + mergeAttr(&theme.Prompt) + case "spinner": + mergeAttr(&theme.Spinner) + case "info": + mergeAttr(&theme.Info) + case "pointer": + mergeAttr(&theme.Cursor) + case "marker": + mergeAttr(&theme.Selected) + case "header": + mergeAttr(&theme.Header) + default: + fail() + } + } + } + return theme +} + +var executeRegexp *regexp.Regexp + +func firstKey(keymap map[tui.Event]string) tui.Event { + for k := range keymap { + return k + } + return tui.EventType(0).AsEvent() +} + +const ( + escapedColon = 0 + escapedComma = 1 + escapedPlus = 2 +) + +func init() { + // Backreferences are not supported. + // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') + executeRegexp = regexp.MustCompile( + `(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|unbind):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|unbind)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`) +} + +func parseKeymap(keymap map[tui.Event][]*action, str string) { + masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { + symbol := ":" + if strings.HasPrefix(src, "+") { + symbol = "+" + } + prefix := symbol + "execute" + if strings.HasPrefix(src[1:], "reload") { + prefix = symbol + "reload" + } else if strings.HasPrefix(src[1:], "change-preview-window") { + prefix = symbol + "change-preview-window" + } else if strings.HasPrefix(src[1:], "change-preview") { + prefix = symbol + "change-preview" + } else if strings.HasPrefix(src[1:], "preview") { + prefix = symbol + "preview" + } else if strings.HasPrefix(src[1:], "unbind") { + prefix = symbol + "unbind" + } else if strings.HasPrefix(src[1:], "change-prompt") { + prefix = symbol + "change-prompt" + } else if src[len(prefix)] == '-' { + c := src[len(prefix)+1] + if c == 's' || c == 'S' { + prefix += "-silent" + } else { + prefix += "-multi" + } + } + return prefix + "(" + strings.Repeat(" ", len(src)-len(prefix)-2) + ")" + }) + masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1) + masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1) + masked = strings.Replace(masked, "+:", string([]rune{escapedPlus, ':'}), -1) + + idx := 0 + for _, pairStr := range strings.Split(masked, ",") { + origPairStr := str[idx : idx+len(pairStr)] + idx += len(pairStr) + 1 + + pair := strings.SplitN(pairStr, ":", 2) + if len(pair) < 2 { + errorExit("bind action not specified: " + origPairStr) + } + var key tui.Event + if len(pair[0]) == 1 && pair[0][0] == escapedColon { + key = tui.Key(':') + } else if len(pair[0]) == 1 && pair[0][0] == escapedComma { + key = tui.Key(',') + } else if len(pair[0]) == 1 && pair[0][0] == escapedPlus { + key = tui.Key('+') + } else { + keys := parseKeyChords(pair[0], "key name required") + key = firstKey(keys) + } + + idx2 := len(pair[0]) + 1 + specs := strings.Split(pair[1], "+") + actions := make([]*action, 0, len(specs)) + appendAction := func(types ...actionType) { + actions = append(actions, toActions(types...)...) + } + prevSpec := "" + for specIndex, maskedSpec := range specs { + spec := origPairStr[idx2 : idx2+len(maskedSpec)] + idx2 += len(maskedSpec) + 1 + spec = prevSpec + spec + specLower := strings.ToLower(spec) + switch specLower { + case "ignore": + appendAction(actIgnore) + case "beginning-of-line": + appendAction(actBeginningOfLine) + case "abort": + appendAction(actAbort) + case "accept": + appendAction(actAccept) + case "accept-non-empty": + appendAction(actAcceptNonEmpty) + case "print-query": + appendAction(actPrintQuery) + case "refresh-preview": + appendAction(actRefreshPreview) + case "replace-query": + appendAction(actReplaceQuery) + case "backward-char": + appendAction(actBackwardChar) + case "backward-delete-char": + appendAction(actBackwardDeleteChar) + case "backward-delete-char/eof": + appendAction(actBackwardDeleteCharEOF) + case "backward-word": + appendAction(actBackwardWord) + case "clear-screen": + appendAction(actClearScreen) + case "delete-char": + appendAction(actDeleteChar) + case "delete-char/eof": + appendAction(actDeleteCharEOF) + case "deselect": + appendAction(actDeselect) + case "end-of-line": + appendAction(actEndOfLine) + case "cancel": + appendAction(actCancel) + case "clear-query": + appendAction(actClearQuery) + case "clear-selection": + appendAction(actClearSelection) + case "forward-char": + appendAction(actForwardChar) + case "forward-word": + appendAction(actForwardWord) + case "jump": + appendAction(actJump) + case "jump-accept": + appendAction(actJumpAccept) + case "kill-line": + appendAction(actKillLine) + case "kill-word": + appendAction(actKillWord) + case "unix-line-discard", "line-discard": + appendAction(actUnixLineDiscard) + case "unix-word-rubout", "word-rubout": + appendAction(actUnixWordRubout) + case "yank": + appendAction(actYank) + case "backward-kill-word": + appendAction(actBackwardKillWord) + case "toggle-down": + appendAction(actToggle, actDown) + case "toggle-up": + appendAction(actToggle, actUp) + case "toggle-in": + appendAction(actToggleIn) + case "toggle-out": + appendAction(actToggleOut) + case "toggle-all": + appendAction(actToggleAll) + case "toggle-search": + appendAction(actToggleSearch) + case "select": + appendAction(actSelect) + case "select-all": + appendAction(actSelectAll) + case "deselect-all": + appendAction(actDeselectAll) + case "close": + appendAction(actClose) + case "toggle": + appendAction(actToggle) + case "down": + appendAction(actDown) + case "up": + appendAction(actUp) + case "first", "top": + appendAction(actFirst) + case "last": + appendAction(actLast) + case "page-up": + appendAction(actPageUp) + case "page-down": + appendAction(actPageDown) + case "half-page-up": + appendAction(actHalfPageUp) + case "half-page-down": + appendAction(actHalfPageDown) + case "previous-history": + appendAction(actPreviousHistory) + case "next-history": + appendAction(actNextHistory) + case "toggle-preview": + appendAction(actTogglePreview) + case "toggle-preview-wrap": + appendAction(actTogglePreviewWrap) + case "toggle-sort": + appendAction(actToggleSort) + case "preview-top": + appendAction(actPreviewTop) + case "preview-bottom": + appendAction(actPreviewBottom) + case "preview-up": + appendAction(actPreviewUp) + case "preview-down": + appendAction(actPreviewDown) + case "preview-page-up": + appendAction(actPreviewPageUp) + case "preview-page-down": + appendAction(actPreviewPageDown) + case "preview-half-page-up": + appendAction(actPreviewHalfPageUp) + case "preview-half-page-down": + appendAction(actPreviewHalfPageDown) + case "enable-search": + appendAction(actEnableSearch) + case "disable-search": + appendAction(actDisableSearch) + case "put": + if key.Type == tui.Rune && unicode.IsGraphic(key.Char) { + appendAction(actRune) + } else { + errorExit("unable to put non-printable character: " + pair[0]) + } + default: + t := isExecuteAction(specLower) + if t == actIgnore { + if specIndex == 0 && specLower == "" { + actions = append(keymap[key], actions...) + } else { + errorExit("unknown action: " + spec) + } + } else { + var offset int + switch t { + case actReload: + offset = len("reload") + case actPreview: + offset = len("preview") + case actChangePreviewWindow: + offset = len("change-preview-window") + case actChangePreview: + offset = len("change-preview") + case actChangePrompt: + offset = len("change-prompt") + case actUnbind: + offset = len("unbind") + case actExecuteSilent: + offset = len("execute-silent") + case actExecuteMulti: + offset = len("execute-multi") + default: + offset = len("execute") + } + var actionArg string + if spec[offset] == ':' { + if specIndex == len(specs)-1 { + actionArg = spec[offset+1:] + actions = append(actions, &action{t: t, a: actionArg}) + } else { + prevSpec = spec + "+" + continue + } + } else { + actionArg = spec[offset+1 : len(spec)-1] + actions = append(actions, &action{t: t, a: actionArg}) + } + if t == actUnbind { + parseKeyChords(actionArg, "unbind target required") + } else if t == actChangePreviewWindow { + opts := previewOpts{} + for _, arg := range strings.Split(actionArg, "|") { + parsePreviewWindow(&opts, arg) + } + } + } + } + prevSpec = "" + } + keymap[key] = actions + } +} + +func isExecuteAction(str string) actionType { + matches := executeRegexp.FindAllStringSubmatch(":"+str, -1) + if matches == nil || len(matches) != 1 { + return actIgnore + } + prefix := matches[0][1] + if len(prefix) == 0 { + prefix = matches[0][2] + } + switch prefix { + case "reload": + return actReload + case "unbind": + return actUnbind + case "preview": + return actPreview + case "change-preview-window": + return actChangePreviewWindow + case "change-preview": + return actChangePreview + case "change-prompt": + return actChangePrompt + case "execute": + return actExecute + case "execute-silent": + return actExecuteSilent + case "execute-multi": + return actExecuteMulti + } + return actIgnore +} + +func parseToggleSort(keymap map[tui.Event][]*action, str string) { + keys := parseKeyChords(str, "key name required") + if len(keys) != 1 { + errorExit("multiple keys specified") + } + keymap[firstKey(keys)] = toActions(actToggleSort) +} + +func strLines(str string) []string { + return strings.Split(strings.TrimSuffix(str, "\n"), "\n") +} + +func parseSize(str string, maxPercent float64, label string) sizeSpec { + var val float64 + percent := strings.HasSuffix(str, "%") + if percent { + val = atof(str[:len(str)-1]) + if val < 0 { + errorExit(label + " must be non-negative") + } + if val > maxPercent { + errorExit(fmt.Sprintf("%s too large (max: %d%%)", label, int(maxPercent))) + } + } else { + if strings.Contains(str, ".") { + errorExit(label + " (without %) must be a non-negative integer") + } + + val = float64(atoi(str)) + if val < 0 { + errorExit(label + " must be non-negative") + } + } + return sizeSpec{val, percent} +} + +func parseHeight(str string) sizeSpec { + size := parseSize(str, 100, "height") + return size +} + +func parseLayout(str string) layoutType { + switch str { + case "default": + return layoutDefault + case "reverse": + return layoutReverse + case "reverse-list": + return layoutReverseList + default: + errorExit("invalid layout (expected: default / reverse / reverse-list)") + } + return layoutDefault +} + +func parseInfoStyle(str string) infoStyle { + switch str { + case "default": + return infoDefault + case "inline": + return infoInline + case "hidden": + return infoHidden + default: + errorExit("invalid info style (expected: default / inline / hidden)") + } + return infoDefault +} + +func parsePreviewWindow(opts *previewOpts, input string) { + delimRegex := regexp.MustCompile("[:,]") // : for backward compatibility + sizeRegex := regexp.MustCompile("^[0-9]+%?$") + offsetRegex := regexp.MustCompile(`^(\+{-?[0-9]+})?([+-][0-9]+)*(-?/[1-9][0-9]*)?$`) + headerRegex := regexp.MustCompile("^~(0|[1-9][0-9]*)$") + tokens := delimRegex.Split(input, -1) + for _, token := range tokens { + switch token { + case "": + case "default": + *opts = defaultPreviewOpts(opts.command) + case "hidden": + opts.hidden = true + case "nohidden": + opts.hidden = false + case "wrap": + opts.wrap = true + case "nowrap": + opts.wrap = false + case "cycle": + opts.cycle = true + case "nocycle": + opts.cycle = false + case "up", "top": + opts.position = posUp + case "down", "bottom": + opts.position = posDown + case "left": + opts.position = posLeft + case "right": + opts.position = posRight + case "rounded", "border", "border-rounded": + opts.border = tui.BorderRounded + case "sharp", "border-sharp": + opts.border = tui.BorderSharp + case "noborder", "border-none": + opts.border = tui.BorderNone + case "border-horizontal": + opts.border = tui.BorderHorizontal + case "border-vertical": + opts.border = tui.BorderVertical + case "border-top": + opts.border = tui.BorderTop + case "border-bottom": + opts.border = tui.BorderBottom + case "border-left": + opts.border = tui.BorderLeft + case "border-right": + opts.border = tui.BorderRight + case "follow": + opts.follow = true + case "nofollow": + opts.follow = false + default: + if headerRegex.MatchString(token) { + opts.headerLines = atoi(token[1:]) + } else if sizeRegex.MatchString(token) { + opts.size = parseSize(token, 99, "window size") + } else if offsetRegex.MatchString(token) { + opts.scroll = token + } else { + errorExit("invalid preview window option: " + token) + } + } + } +} + +func parseMargin(opt string, margin string) [4]sizeSpec { + margins := strings.Split(margin, ",") + checked := func(str string) sizeSpec { + return parseSize(str, 49, opt) + } + switch len(margins) { + case 1: + m := checked(margins[0]) + return [4]sizeSpec{m, m, m, m} + case 2: + tb := checked(margins[0]) + rl := checked(margins[1]) + return [4]sizeSpec{tb, rl, tb, rl} + case 3: + t := checked(margins[0]) + rl := checked(margins[1]) + b := checked(margins[2]) + return [4]sizeSpec{t, rl, b, rl} + case 4: + return [4]sizeSpec{ + checked(margins[0]), checked(margins[1]), + checked(margins[2]), checked(margins[3])} + default: + errorExit("invalid " + opt + ": " + margin) + } + return defaultMargin() +} + +func parseOptions(opts *Options, allArgs []string) { + var historyMax int + if opts.History == nil { + historyMax = defaultHistoryMax + } else { + historyMax = opts.History.maxSize + } + setHistory := func(path string) { + h, e := NewHistory(path, historyMax) + if e != nil { + errorExit(e.Error()) + } + opts.History = h + } + setHistoryMax := func(max int) { + historyMax = max + if historyMax < 1 { + errorExit("history max must be a positive integer") + } + if opts.History != nil { + opts.History.maxSize = historyMax + } + } + validateJumpLabels := false + validatePointer := false + validateMarker := false + for i := 0; i < len(allArgs); i++ { + arg := allArgs[i] + switch arg { + case "-h", "--help": + help(exitOk) + case "-x", "--extended": + opts.Extended = true + case "-e", "--exact": + opts.Fuzzy = false + case "--extended-exact": + // Note that we now don't have --no-extended-exact + opts.Fuzzy = false + opts.Extended = true + case "+x", "--no-extended": + opts.Extended = false + case "+e", "--no-exact": + opts.Fuzzy = true + case "-q", "--query": + opts.Query = nextString(allArgs, &i, "query string required") + case "-f", "--filter": + filter := nextString(allArgs, &i, "query string required") + opts.Filter = &filter + case "--literal": + opts.Normalize = false + case "--no-literal": + opts.Normalize = true + case "--algo": + opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)")) + case "--expect": + for k, v := range parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") { + opts.Expect[k] = v + } + case "--no-expect": + opts.Expect = make(map[tui.Event]string) + case "--enabled", "--no-phony": + opts.Phony = false + case "--disabled", "--phony": + opts.Phony = true + case "--tiebreak": + opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) + case "--bind": + parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required")) + case "--color": + _, spec := optionalNextString(allArgs, &i) + if len(spec) == 0 { + opts.Theme = tui.EmptyTheme() + } else { + opts.Theme = parseTheme(opts.Theme, spec) + } + case "--toggle-sort": + parseToggleSort(opts.Keymap, nextString(allArgs, &i, "key name required")) + case "-d", "--delimiter": + opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required")) + case "-n", "--nth": + opts.Nth = splitNth(nextString(allArgs, &i, "nth expression required")) + case "--with-nth": + opts.WithNth = splitNth(nextString(allArgs, &i, "nth expression required")) + case "-s", "--sort": + opts.Sort = optionalNumeric(allArgs, &i, 1) + case "+s", "--no-sort": + opts.Sort = 0 + case "--tac": + opts.Tac = true + case "--no-tac": + opts.Tac = false + case "-i": + opts.Case = CaseIgnore + case "+i": + opts.Case = CaseRespect + case "-m", "--multi": + opts.Multi = optionalNumeric(allArgs, &i, maxMulti) + case "+m", "--no-multi": + opts.Multi = 0 + case "--ansi": + opts.Ansi = true + case "--no-ansi": + opts.Ansi = false + case "--no-mouse": + opts.Mouse = false + case "+c", "--no-color": + opts.Theme = tui.NoColorTheme() + case "+2", "--no-256": + opts.Theme = tui.Default16 + case "--black": + opts.Black = true + case "--no-black": + opts.Black = false + case "--bold": + opts.Bold = true + case "--no-bold": + opts.Bold = false + case "--layout": + opts.Layout = parseLayout( + nextString(allArgs, &i, "layout required (default / reverse / reverse-list)")) + case "--reverse": + opts.Layout = layoutReverse + case "--no-reverse": + opts.Layout = layoutDefault + case "--cycle": + opts.Cycle = true + case "--no-cycle": + opts.Cycle = false + case "--keep-right": + opts.KeepRight = true + case "--no-keep-right": + opts.KeepRight = false + case "--hscroll": + opts.Hscroll = true + case "--no-hscroll": + opts.Hscroll = false + case "--hscroll-off": + opts.HscrollOff = nextInt(allArgs, &i, "hscroll offset required") + case "--scroll-off": + opts.ScrollOff = nextInt(allArgs, &i, "scroll offset required") + case "--filepath-word": + opts.FileWord = true + case "--no-filepath-word": + opts.FileWord = false + case "--info": + opts.InfoStyle = parseInfoStyle( + nextString(allArgs, &i, "info style required")) + case "--no-info": + opts.InfoStyle = infoHidden + case "--inline-info": + opts.InfoStyle = infoInline + case "--no-inline-info": + opts.InfoStyle = infoDefault + case "--jump-labels": + opts.JumpLabels = nextString(allArgs, &i, "label characters required") + validateJumpLabels = true + case "-1", "--select-1": + opts.Select1 = true + case "+1", "--no-select-1": + opts.Select1 = false + case "-0", "--exit-0": + opts.Exit0 = true + case "+0", "--no-exit-0": + opts.Exit0 = false + case "--read0": + opts.ReadZero = true + case "--no-read0": + opts.ReadZero = false + case "--print0": + opts.Printer = func(str string) { fmt.Print(str, "\x00") } + opts.PrintSep = "\x00" + case "--no-print0": + opts.Printer = func(str string) { fmt.Println(str) } + opts.PrintSep = "\n" + case "--print-query": + opts.PrintQuery = true + case "--no-print-query": + opts.PrintQuery = false + case "--prompt": + opts.Prompt = nextString(allArgs, &i, "prompt string required") + case "--pointer": + opts.Pointer = nextString(allArgs, &i, "pointer sign string required") + validatePointer = true + case "--marker": + opts.Marker = nextString(allArgs, &i, "selected sign string required") + validateMarker = true + case "--sync": + opts.Sync = true + case "--no-sync": + opts.Sync = false + case "--async": + opts.Sync = false + case "--no-history": + opts.History = nil + case "--history": + setHistory(nextString(allArgs, &i, "history file path required")) + case "--history-size": + setHistoryMax(nextInt(allArgs, &i, "history max size required")) + case "--no-header": + opts.Header = []string{} + case "--no-header-lines": + opts.HeaderLines = 0 + case "--header": + opts.Header = strLines(nextString(allArgs, &i, "header string required")) + case "--header-lines": + opts.HeaderLines = atoi( + nextString(allArgs, &i, "number of header lines required")) + case "--header-first": + opts.HeaderFirst = true + case "--no-header-first": + opts.HeaderFirst = false + case "--preview": + opts.Preview.command = nextString(allArgs, &i, "preview command required") + case "--no-preview": + opts.Preview.command = "" + case "--preview-window": + parsePreviewWindow(&opts.Preview, + nextString(allArgs, &i, "preview window layout required: [up|down|left|right][,SIZE[%]][,border-BORDER_OPT][,wrap][,cycle][,hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]")) + case "--height": + opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]")) + case "--min-height": + opts.MinHeight = nextInt(allArgs, &i, "height required: HEIGHT") + case "--no-height": + opts.Height = sizeSpec{} + case "--no-margin": + opts.Margin = defaultMargin() + case "--no-padding": + opts.Padding = defaultMargin() + case "--no-border": + opts.BorderShape = tui.BorderNone + case "--border": + hasArg, arg := optionalNextString(allArgs, &i) + opts.BorderShape = parseBorder(arg, !hasArg) + case "--no-unicode": + opts.Unicode = false + case "--unicode": + opts.Unicode = true + case "--margin": + opts.Margin = parseMargin( + "margin", + nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)")) + case "--padding": + opts.Padding = parseMargin( + "padding", + nextString(allArgs, &i, "padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)")) + case "--tabstop": + opts.Tabstop = nextInt(allArgs, &i, "tab stop required") + case "--clear": + opts.ClearOnExit = true + case "--no-clear": + opts.ClearOnExit = false + case "--version": + opts.Version = true + default: + if match, value := optString(arg, "--algo="); match { + opts.FuzzyAlgo = parseAlgo(value) + } else if match, value := optString(arg, "-q", "--query="); match { + opts.Query = value + } else if match, value := optString(arg, "-f", "--filter="); match { + opts.Filter = &value + } else if match, value := optString(arg, "-d", "--delimiter="); match { + opts.Delimiter = delimiterRegexp(value) + } else if match, value := optString(arg, "--border="); match { + opts.BorderShape = parseBorder(value, false) + } else if match, value := optString(arg, "--prompt="); match { + opts.Prompt = value + } else if match, value := optString(arg, "--pointer="); match { + opts.Pointer = value + validatePointer = true + } else if match, value := optString(arg, "--marker="); match { + opts.Marker = value + validateMarker = true + } else if match, value := optString(arg, "-n", "--nth="); match { + opts.Nth = splitNth(value) + } else if match, value := optString(arg, "--with-nth="); match { + opts.WithNth = splitNth(value) + } else if match, _ := optString(arg, "-s", "--sort="); match { + opts.Sort = 1 // Don't care + } else if match, value := optString(arg, "-m", "--multi="); match { + opts.Multi = atoi(value) + } else if match, value := optString(arg, "--height="); match { + opts.Height = parseHeight(value) + } else if match, value := optString(arg, "--min-height="); match { + opts.MinHeight = atoi(value) + } else if match, value := optString(arg, "--layout="); match { + opts.Layout = parseLayout(value) + } else if match, value := optString(arg, "--info="); match { + opts.InfoStyle = parseInfoStyle(value) + } else if match, value := optString(arg, "--toggle-sort="); match { + parseToggleSort(opts.Keymap, value) + } else if match, value := optString(arg, "--expect="); match { + for k, v := range parseKeyChords(value, "key names required") { + opts.Expect[k] = v + } + } else if match, value := optString(arg, "--tiebreak="); match { + opts.Criteria = parseTiebreak(value) + } else if match, value := optString(arg, "--color="); match { + opts.Theme = parseTheme(opts.Theme, value) + } else if match, value := optString(arg, "--bind="); match { + parseKeymap(opts.Keymap, value) + } else if match, value := optString(arg, "--history="); match { + setHistory(value) + } else if match, value := optString(arg, "--history-size="); match { + setHistoryMax(atoi(value)) + } else if match, value := optString(arg, "--header="); match { + opts.Header = strLines(value) + } else if match, value := optString(arg, "--header-lines="); match { + opts.HeaderLines = atoi(value) + } else if match, value := optString(arg, "--preview="); match { + opts.Preview.command = value + } else if match, value := optString(arg, "--preview-window="); match { + parsePreviewWindow(&opts.Preview, value) + } else if match, value := optString(arg, "--margin="); match { + opts.Margin = parseMargin("margin", value) + } else if match, value := optString(arg, "--padding="); match { + opts.Padding = parseMargin("padding", value) + } else if match, value := optString(arg, "--tabstop="); match { + opts.Tabstop = atoi(value) + } else if match, value := optString(arg, "--hscroll-off="); match { + opts.HscrollOff = atoi(value) + } else if match, value := optString(arg, "--scroll-off="); match { + opts.ScrollOff = atoi(value) + } else if match, value := optString(arg, "--jump-labels="); match { + opts.JumpLabels = value + validateJumpLabels = true + } else { + errorExit("unknown option: " + arg) + } + } + } + + if opts.HeaderLines < 0 { + errorExit("header lines must be a non-negative integer") + } + + if opts.HscrollOff < 0 { + errorExit("hscroll offset must be a non-negative integer") + } + + if opts.ScrollOff < 0 { + errorExit("scroll offset must be a non-negative integer") + } + + if opts.Tabstop < 1 { + errorExit("tab stop must be a positive integer") + } + + if len(opts.JumpLabels) == 0 { + errorExit("empty jump labels") + } + + if validateJumpLabels { + for _, r := range opts.JumpLabels { + if r < 32 || r > 126 { + errorExit("non-ascii jump labels are not allowed") + } + } + } + + if validatePointer { + if err := validateSign(opts.Pointer, "pointer"); err != nil { + errorExit(err.Error()) + } + } + + if validateMarker { + if err := validateSign(opts.Marker, "marker"); err != nil { + errorExit(err.Error()) + } + } +} + +func validateSign(sign string, signOptName string) error { + if sign == "" { + return fmt.Errorf("%v cannot be empty", signOptName) + } + for _, r := range sign { + if !unicode.IsGraphic(r) { + return fmt.Errorf("invalid character in %v", signOptName) + } + } + if runewidth.StringWidth(sign) > 2 { + return fmt.Errorf("%v display width should be up to 2", signOptName) + } + return nil +} + +func postProcessOptions(opts *Options) { + if !opts.Version && !tui.IsLightRendererSupported() && opts.Height.size > 0 { + errorExit("--height option is currently not supported on this platform") + } + // Default actions for CTRL-N / CTRL-P when --history is set + if opts.History != nil { + if _, prs := opts.Keymap[tui.CtrlP.AsEvent()]; !prs { + opts.Keymap[tui.CtrlP.AsEvent()] = toActions(actPreviousHistory) + } + if _, prs := opts.Keymap[tui.CtrlN.AsEvent()]; !prs { + opts.Keymap[tui.CtrlN.AsEvent()] = toActions(actNextHistory) + } + } + + // Extend the default key map + keymap := defaultKeymap() + for key, actions := range opts.Keymap { + var lastChangePreviewWindow *action + for _, act := range actions { + switch act.t { + case actToggleSort: + // To display "+S"/"-S" on info line + opts.ToggleSort = true + case actChangePreviewWindow: + lastChangePreviewWindow = act + } + } + // Re-organize actions so that we only keep the last change-preview-window + // and it comes first in the list. + // * change-preview-window(up,+10)+preview(sleep 3; cat {})+change-preview-window(up,+20) + // -> change-preview-window(up,+20)+preview(sleep 3; cat {}) + if lastChangePreviewWindow != nil { + reordered := []*action{lastChangePreviewWindow} + for _, act := range actions { + if act.t != actChangePreviewWindow { + reordered = append(reordered, act) + } + } + actions = reordered + } + keymap[key] = actions + } + opts.Keymap = keymap + + // If we're not using extended search mode, --nth option becomes irrelevant + // if it contains the whole range + if !opts.Extended || len(opts.Nth) == 1 { + for _, r := range opts.Nth { + if r.begin == rangeEllipsis && r.end == rangeEllipsis { + opts.Nth = make([]Range, 0) + return + } + } + } + + if opts.Bold { + theme := opts.Theme + boldify := func(c tui.ColorAttr) tui.ColorAttr { + dup := c + if !theme.Colored { + dup.Attr |= tui.Bold + } else if (c.Attr & tui.AttrRegular) == 0 { + dup.Attr |= tui.Bold + } + return dup + } + theme.Current = boldify(theme.Current) + theme.CurrentMatch = boldify(theme.CurrentMatch) + theme.Prompt = boldify(theme.Prompt) + theme.Input = boldify(theme.Input) + theme.Cursor = boldify(theme.Cursor) + theme.Spinner = boldify(theme.Spinner) + } +} + +// ParseOptions parses command-line options +func ParseOptions() *Options { + opts := defaultOptions() + + // Options from Env var + words, _ := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS")) + if len(words) > 0 { + parseOptions(opts, words) + } + + // Options from command-line arguments + parseOptions(opts, os.Args[1:]) + + postProcessOptions(opts) + return opts +} diff --git a/fzf/fzf/src/options_test.go b/fzf/fzf/src/options_test.go new file mode 100644 index 0000000..bb94623 --- /dev/null +++ b/fzf/fzf/src/options_test.go @@ -0,0 +1,457 @@ +package fzf + +import ( + "fmt" + "io/ioutil" + "testing" + + "github.com/junegunn/fzf/src/tui" +) + +func TestDelimiterRegex(t *testing.T) { + // Valid regex + delim := delimiterRegexp(".") + if delim.regex == nil || delim.str != nil { + t.Error(delim) + } + // Broken regex -> string + delim = delimiterRegexp("[0-9") + if delim.regex != nil || *delim.str != "[0-9" { + t.Error(delim) + } + // Valid regex + delim = delimiterRegexp("[0-9]") + if delim.regex.String() != "[0-9]" || delim.str != nil { + t.Error(delim) + } + // Tab character + delim = delimiterRegexp("\t") + if delim.regex != nil || *delim.str != "\t" { + t.Error(delim) + } + // Tab expression + delim = delimiterRegexp("\\t") + if delim.regex != nil || *delim.str != "\t" { + t.Error(delim) + } + // Tabs -> regex + delim = delimiterRegexp("\t+") + if delim.regex == nil || delim.str != nil { + t.Error(delim) + } +} + +func TestDelimiterRegexString(t *testing.T) { + delim := delimiterRegexp("*") + tokens := Tokenize("-*--*---**---", delim) + if delim.regex != nil || + tokens[0].text.ToString() != "-*" || + tokens[1].text.ToString() != "--*" || + tokens[2].text.ToString() != "---*" || + tokens[3].text.ToString() != "*" || + tokens[4].text.ToString() != "---" { + t.Errorf("%s %v %d", delim, tokens, len(tokens)) + } +} + +func TestDelimiterRegexRegex(t *testing.T) { + delim := delimiterRegexp("--\\*") + tokens := Tokenize("-*--*---**---", delim) + if delim.str != nil || + tokens[0].text.ToString() != "-*--*" || + tokens[1].text.ToString() != "---*" || + tokens[2].text.ToString() != "*---" { + t.Errorf("%s %d", tokens, len(tokens)) + } +} + +func TestSplitNth(t *testing.T) { + { + ranges := splitNth("..") + if len(ranges) != 1 || + ranges[0].begin != rangeEllipsis || + ranges[0].end != rangeEllipsis { + t.Errorf("%v", ranges) + } + } + { + ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1") + if len(ranges) != 10 || + ranges[0].begin != rangeEllipsis || ranges[0].end != 3 || + ranges[1].begin != rangeEllipsis || ranges[1].end != rangeEllipsis || + ranges[2].begin != 2 || ranges[2].end != 3 || + ranges[3].begin != 4 || ranges[3].end != rangeEllipsis || + ranges[4].begin != -3 || ranges[4].end != -2 || + ranges[5].begin != rangeEllipsis || ranges[5].end != rangeEllipsis || + ranges[6].begin != 2 || ranges[6].end != 2 || + ranges[7].begin != -2 || ranges[7].end != -2 || + ranges[8].begin != 2 || ranges[8].end != -2 || + ranges[9].begin != rangeEllipsis || ranges[9].end != rangeEllipsis { + t.Errorf("%v", ranges) + } + } +} + +func TestIrrelevantNth(t *testing.T) { + { + opts := defaultOptions() + words := []string{"--nth", "..", "-x"} + parseOptions(opts, words) + postProcessOptions(opts) + if len(opts.Nth) != 0 { + t.Errorf("nth should be empty: %v", opts.Nth) + } + } + for _, words := range [][]string{{"--nth", "..,3", "+x"}, {"--nth", "3,1..", "+x"}, {"--nth", "..-1,1", "+x"}} { + { + opts := defaultOptions() + parseOptions(opts, words) + postProcessOptions(opts) + if len(opts.Nth) != 0 { + t.Errorf("nth should be empty: %v", opts.Nth) + } + } + { + opts := defaultOptions() + words = append(words, "-x") + parseOptions(opts, words) + postProcessOptions(opts) + if len(opts.Nth) != 2 { + t.Errorf("nth should not be empty: %v", opts.Nth) + } + } + } +} + +func TestParseKeys(t *testing.T) { + pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "") + checkEvent := func(e tui.Event, s string) { + if pairs[e] != s { + t.Errorf("%s != %s", pairs[e], s) + } + } + check := func(et tui.EventType, s string) { + checkEvent(et.AsEvent(), s) + } + if len(pairs) != 12 { + t.Error(12) + } + check(tui.CtrlZ, "ctrl-z") + check(tui.F2, "f2") + check(tui.CtrlG, "ctrl-G") + checkEvent(tui.AltKey('z'), "alt-z") + checkEvent(tui.Key('@'), "@") + checkEvent(tui.AltKey('a'), "Alt-a") + checkEvent(tui.Key('!'), "!") + checkEvent(tui.Key('J'), "J") + checkEvent(tui.Key('g'), "g") + checkEvent(tui.CtrlAltKey('a'), "ctrl-alt-a") + checkEvent(tui.CtrlAltKey('m'), "ALT-enter") + checkEvent(tui.AltKey(' '), "alt-SPACE") + + // Synonyms + pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "") + if len(pairs) != 9 { + t.Error(9) + } + check(tui.CtrlM, "Return") + checkEvent(tui.Key(' '), "space") + check(tui.Tab, "tab") + check(tui.BTab, "btab") + check(tui.ESC, "esc") + check(tui.Up, "up") + check(tui.Down, "down") + check(tui.Left, "left") + check(tui.Right, "right") + + pairs = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "") + if len(pairs) != 11 { + t.Error(11) + } + check(tui.Tab, "Ctrl-I") + check(tui.PgUp, "page-up") + check(tui.PgDn, "Page-Down") + check(tui.Home, "Home") + check(tui.End, "End") + check(tui.AltBS, "Alt-BSpace") + check(tui.SLeft, "shift-left") + check(tui.SRight, "shift-right") + check(tui.BTab, "shift-tab") + check(tui.CtrlM, "Enter") + check(tui.BSpace, "bspace") +} + +func TestParseKeysWithComma(t *testing.T) { + checkN := func(a int, b int) { + if a != b { + t.Errorf("%d != %d", a, b) + } + } + check := func(pairs map[tui.Event]string, e tui.Event, s string) { + if pairs[e] != s { + t.Errorf("%s != %s", pairs[e], s) + } + } + + pairs := parseKeyChords(",", "") + checkN(len(pairs), 1) + check(pairs, tui.Key(','), ",") + + pairs = parseKeyChords(",,a,b", "") + checkN(len(pairs), 3) + check(pairs, tui.Key('a'), "a") + check(pairs, tui.Key('b'), "b") + check(pairs, tui.Key(','), ",") + + pairs = parseKeyChords("a,b,,", "") + checkN(len(pairs), 3) + check(pairs, tui.Key('a'), "a") + check(pairs, tui.Key('b'), "b") + check(pairs, tui.Key(','), ",") + + pairs = parseKeyChords("a,,,b", "") + checkN(len(pairs), 3) + check(pairs, tui.Key('a'), "a") + check(pairs, tui.Key('b'), "b") + check(pairs, tui.Key(','), ",") + + pairs = parseKeyChords("a,,,b,c", "") + checkN(len(pairs), 4) + check(pairs, tui.Key('a'), "a") + check(pairs, tui.Key('b'), "b") + check(pairs, tui.Key('c'), "c") + check(pairs, tui.Key(','), ",") + + pairs = parseKeyChords(",,,", "") + checkN(len(pairs), 1) + check(pairs, tui.Key(','), ",") + + pairs = parseKeyChords(",ALT-,,", "") + checkN(len(pairs), 1) + check(pairs, tui.AltKey(','), "ALT-,") +} + +func TestBind(t *testing.T) { + keymap := defaultKeymap() + check := func(event tui.Event, arg1 string, types ...actionType) { + if len(keymap[event]) != len(types) { + t.Errorf("invalid number of actions for %v (%d != %d)", + event, len(types), len(keymap[event])) + return + } + for idx, action := range keymap[event] { + if types[idx] != action.t { + t.Errorf("invalid action type (%d != %d)", types[idx], action.t) + } + } + if len(arg1) > 0 && keymap[event][0].a != arg1 { + t.Errorf("invalid action argument: (%s != %s)", arg1, keymap[event][0].a) + } + } + check(tui.CtrlA.AsEvent(), "", actBeginningOfLine) + parseKeymap(keymap, + "ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+ + "f1:execute(ls {+})+abort+execute(echo {+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ + "alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+ + "x:Execute(foo+bar),X:execute/bar+baz/"+ + ",f1:+first,f1:+top"+ + ",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up") + check(tui.CtrlA.AsEvent(), "", actKillLine) + check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown) + check(tui.Key('c'), "", actPageUp) + check(tui.Key(','), "", actAbort) + check(tui.Key(':'), "", actAccept) + check(tui.AltKey('z'), "", actPageDown) + check(tui.F1.AsEvent(), "ls {+}", actExecute, actAbort, actExecute, actSelectAll, actFirst, actFirst) + check(tui.F2.AsEvent(), "echo {}, {}, {}", actExecute) + check(tui.F3.AsEvent(), "echo '({})'", actExecute) + check(tui.F4.AsEvent(), "less {}", actExecute) + check(tui.Key('x'), "foo+bar", actExecute) + check(tui.Key('X'), "bar+baz", actExecute) + check(tui.AltKey('a'), "echo (,),[,],/,:,;,%,{}", actExecuteMulti) + check(tui.AltKey('b'), "echo (,),[,],/,:,@,%,{}", actExecute) + check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute) + + for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { + parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char)) + check(tui.Key([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute) + } + + parseKeymap(keymap, "f1:abort") + check(tui.F1.AsEvent(), "", actAbort) +} + +func TestColorSpec(t *testing.T) { + theme := tui.Dark256 + dark := parseTheme(theme, "dark") + if *dark != *theme { + t.Errorf("colors should be equivalent") + } + if dark == theme { + t.Errorf("point should not be equivalent") + } + + light := parseTheme(theme, "dark,light") + if *light == *theme { + t.Errorf("should not be equivalent") + } + if *light != *tui.Light256 { + t.Errorf("colors should be equivalent") + } + if light == theme { + t.Errorf("point should not be equivalent") + } + + customized := parseTheme(theme, "fg:231,bg:232") + if customized.Fg.Color != 231 || customized.Bg.Color != 232 { + t.Errorf("color not customized") + } + if *tui.Dark256 == *customized { + t.Errorf("colors should not be equivalent") + } + customized.Fg = tui.Dark256.Fg + customized.Bg = tui.Dark256.Bg + if *tui.Dark256 != *customized { + t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized) + } + + customized = parseTheme(theme, "fg:231,dark,bg:232") + if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg { + t.Errorf("color not customized") + } +} + +func TestDefaultCtrlNP(t *testing.T) { + check := func(words []string, et tui.EventType, expected actionType) { + e := et.AsEvent() + opts := defaultOptions() + parseOptions(opts, words) + postProcessOptions(opts) + if opts.Keymap[e][0].t != expected { + t.Error() + } + } + check([]string{}, tui.CtrlN, actDown) + check([]string{}, tui.CtrlP, actUp) + + check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept) + check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept) + + f, _ := ioutil.TempFile("", "fzf-history") + f.Close() + hist := "--history=" + f.Name() + check([]string{hist}, tui.CtrlN, actNextHistory) + check([]string{hist}, tui.CtrlP, actPreviousHistory) + + check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlN, actAccept) + check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlP, actPreviousHistory) + + check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlN, actNextHistory) + check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlP, actAccept) +} + +func optsFor(words ...string) *Options { + opts := defaultOptions() + parseOptions(opts, words) + postProcessOptions(opts) + return opts +} + +func TestToggle(t *testing.T) { + opts := optsFor() + if opts.ToggleSort { + t.Error() + } + + opts = optsFor("--bind=a:toggle-sort") + if !opts.ToggleSort { + t.Error() + } + + opts = optsFor("--bind=a:toggle-sort", "--bind=a:up") + if opts.ToggleSort { + t.Error() + } +} + +func TestPreviewOpts(t *testing.T) { + opts := optsFor() + if !(opts.Preview.command == "" && + opts.Preview.hidden == false && + opts.Preview.wrap == false && + opts.Preview.position == posRight && + opts.Preview.size.percent == true && + opts.Preview.size.size == 50) { + t.Error() + } + opts = optsFor("--preview", "cat {}", "--preview-window=left:15,hidden,wrap:+{1}-/2") + if !(opts.Preview.command == "cat {}" && + opts.Preview.hidden == true && + opts.Preview.wrap == true && + opts.Preview.position == posLeft && + opts.Preview.scroll == "+{1}-/2" && + opts.Preview.size.percent == false && + opts.Preview.size.size == 15) { + t.Error(opts.Preview) + } + opts = optsFor("--preview-window=up,15,wrap,hidden,+{1}+3-1-2/2", "--preview-window=down", "--preview-window=cycle") + if !(opts.Preview.command == "" && + opts.Preview.hidden == true && + opts.Preview.wrap == true && + opts.Preview.cycle == true && + opts.Preview.position == posDown && + opts.Preview.scroll == "+{1}+3-1-2/2" && + opts.Preview.size.percent == false && + opts.Preview.size.size == 15) { + t.Error(opts.Preview.size.size) + } + opts = optsFor("--preview-window=up:15:wrap:hidden") + if !(opts.Preview.command == "" && + opts.Preview.hidden == true && + opts.Preview.wrap == true && + opts.Preview.position == posUp && + opts.Preview.size.percent == false && + opts.Preview.size.size == 15) { + t.Error(opts.Preview) + } + opts = optsFor("--preview=foo", "--preview-window=up", "--preview-window=default:70%") + if !(opts.Preview.command == "foo" && + opts.Preview.position == posRight && + opts.Preview.size.percent == true && + opts.Preview.size.size == 70) { + t.Error(opts.Preview) + } +} + +func TestAdditiveExpect(t *testing.T) { + opts := optsFor("--expect=a", "--expect", "b", "--expect=c") + if len(opts.Expect) != 3 { + t.Error(opts.Expect) + } +} + +func TestValidateSign(t *testing.T) { + testCases := []struct { + inputSign string + isValid bool + }{ + {"> ", true}, + {"아", true}, + {"😀", true}, + {"", false}, + {">>>", false}, + {"\n", false}, + {"\t", false}, + } + + for _, testCase := range testCases { + err := validateSign(testCase.inputSign, "") + if testCase.isValid && err != nil { + t.Errorf("Input sign `%s` caused error", testCase.inputSign) + } + + if !testCase.isValid && err == nil { + t.Errorf("Input sign `%s` did not cause error", testCase.inputSign) + } + } +} diff --git a/fzf/fzf/src/pattern.go b/fzf/fzf/src/pattern.go new file mode 100644 index 0000000..4a7a87a --- /dev/null +++ b/fzf/fzf/src/pattern.go @@ -0,0 +1,425 @@ +package fzf + +import ( + "fmt" + "regexp" + "strings" + + "github.com/junegunn/fzf/src/algo" + "github.com/junegunn/fzf/src/util" +) + +// fuzzy +// 'exact +// ^prefix-exact +// suffix-exact$ +// !inverse-exact +// !'inverse-fuzzy +// !^inverse-prefix-exact +// !inverse-suffix-exact$ + +type termType int + +const ( + termFuzzy termType = iota + termExact + termPrefix + termSuffix + termEqual +) + +type term struct { + typ termType + inv bool + text []rune + caseSensitive bool + normalize bool +} + +// String returns the string representation of a term. +func (t term) String() string { + return fmt.Sprintf("term{typ: %d, inv: %v, text: []rune(%q), caseSensitive: %v}", t.typ, t.inv, string(t.text), t.caseSensitive) +} + +type termSet []term + +// Pattern represents search pattern +type Pattern struct { + fuzzy bool + fuzzyAlgo algo.Algo + extended bool + caseSensitive bool + normalize bool + forward bool + text []rune + termSets []termSet + sortable bool + cacheable bool + cacheKey string + delimiter Delimiter + nth []Range + procFun map[termType]algo.Algo +} + +var ( + _patternCache map[string]*Pattern + _splitRegex *regexp.Regexp + _cache ChunkCache +) + +func init() { + _splitRegex = regexp.MustCompile(" +") + clearPatternCache() + clearChunkCache() +} + +func clearPatternCache() { + // We can uniquely identify the pattern for a given string since + // search mode and caseMode do not change while the program is running + _patternCache = make(map[string]*Pattern) +} + +func clearChunkCache() { + _cache = NewChunkCache() +} + +// BuildPattern builds Pattern object from the given arguments +func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool, + cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern { + + var asString string + if extended { + asString = strings.TrimLeft(string(runes), " ") + for strings.HasSuffix(asString, " ") && !strings.HasSuffix(asString, "\\ ") { + asString = asString[:len(asString)-1] + } + } else { + asString = string(runes) + } + + cached, found := _patternCache[asString] + if found { + return cached + } + + caseSensitive := true + sortable := true + termSets := []termSet{} + + if extended { + termSets = parseTerms(fuzzy, caseMode, normalize, asString) + // We should not sort the result if there are only inverse search terms + sortable = false + Loop: + for _, termSet := range termSets { + for idx, term := range termSet { + if !term.inv { + sortable = true + } + // If the query contains inverse search terms or OR operators, + // we cannot cache the search scope + if !cacheable || idx > 0 || term.inv || fuzzy && term.typ != termFuzzy || !fuzzy && term.typ != termExact { + cacheable = false + if sortable { + // Can't break until we see at least one non-inverse term + break Loop + } + } + } + } + } else { + lowerString := strings.ToLower(asString) + normalize = normalize && + lowerString == string(algo.NormalizeRunes([]rune(lowerString))) + caseSensitive = caseMode == CaseRespect || + caseMode == CaseSmart && lowerString != asString + if !caseSensitive { + asString = lowerString + } + } + + ptr := &Pattern{ + fuzzy: fuzzy, + fuzzyAlgo: fuzzyAlgo, + extended: extended, + caseSensitive: caseSensitive, + normalize: normalize, + forward: forward, + text: []rune(asString), + termSets: termSets, + sortable: sortable, + cacheable: cacheable, + nth: nth, + delimiter: delimiter, + procFun: make(map[termType]algo.Algo)} + + ptr.cacheKey = ptr.buildCacheKey() + ptr.procFun[termFuzzy] = fuzzyAlgo + ptr.procFun[termEqual] = algo.EqualMatch + ptr.procFun[termExact] = algo.ExactMatchNaive + ptr.procFun[termPrefix] = algo.PrefixMatch + ptr.procFun[termSuffix] = algo.SuffixMatch + + _patternCache[asString] = ptr + return ptr +} + +func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet { + str = strings.Replace(str, "\\ ", "\t", -1) + tokens := _splitRegex.Split(str, -1) + sets := []termSet{} + set := termSet{} + switchSet := false + afterBar := false + for _, token := range tokens { + typ, inv, text := termFuzzy, false, strings.Replace(token, "\t", " ", -1) + lowerText := strings.ToLower(text) + caseSensitive := caseMode == CaseRespect || + caseMode == CaseSmart && text != lowerText + normalizeTerm := normalize && + lowerText == string(algo.NormalizeRunes([]rune(lowerText))) + if !caseSensitive { + text = lowerText + } + if !fuzzy { + typ = termExact + } + + if len(set) > 0 && !afterBar && text == "|" { + switchSet = false + afterBar = true + continue + } + afterBar = false + + if strings.HasPrefix(text, "!") { + inv = true + typ = termExact + text = text[1:] + } + + if text != "$" && strings.HasSuffix(text, "$") { + typ = termSuffix + text = text[:len(text)-1] + } + + if strings.HasPrefix(text, "'") { + // Flip exactness + if fuzzy && !inv { + typ = termExact + text = text[1:] + } else { + typ = termFuzzy + text = text[1:] + } + } else if strings.HasPrefix(text, "^") { + if typ == termSuffix { + typ = termEqual + } else { + typ = termPrefix + } + text = text[1:] + } + + if len(text) > 0 { + if switchSet { + sets = append(sets, set) + set = termSet{} + } + textRunes := []rune(text) + if normalizeTerm { + textRunes = algo.NormalizeRunes(textRunes) + } + set = append(set, term{ + typ: typ, + inv: inv, + text: textRunes, + caseSensitive: caseSensitive, + normalize: normalizeTerm}) + switchSet = true + } + } + if len(set) > 0 { + sets = append(sets, set) + } + return sets +} + +// IsEmpty returns true if the pattern is effectively empty +func (p *Pattern) IsEmpty() bool { + if !p.extended { + return len(p.text) == 0 + } + return len(p.termSets) == 0 +} + +// AsString returns the search query in string type +func (p *Pattern) AsString() string { + return string(p.text) +} + +func (p *Pattern) buildCacheKey() string { + if !p.extended { + return p.AsString() + } + cacheableTerms := []string{} + for _, termSet := range p.termSets { + if len(termSet) == 1 && !termSet[0].inv && (p.fuzzy || termSet[0].typ == termExact) { + cacheableTerms = append(cacheableTerms, string(termSet[0].text)) + } + } + return strings.Join(cacheableTerms, "\t") +} + +// CacheKey is used to build string to be used as the key of result cache +func (p *Pattern) CacheKey() string { + return p.cacheKey +} + +// Match returns the list of matches Items in the given Chunk +func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result { + // ChunkCache: Exact match + cacheKey := p.CacheKey() + if p.cacheable { + if cached := _cache.Lookup(chunk, cacheKey); cached != nil { + return cached + } + } + + // Prefix/suffix cache + space := _cache.Search(chunk, cacheKey) + + matches := p.matchChunk(chunk, space, slab) + + if p.cacheable { + _cache.Add(chunk, cacheKey, matches) + } + return matches +} + +func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result { + matches := []Result{} + + if space == nil { + for idx := 0; idx < chunk.count; idx++ { + if match, _, _ := p.MatchItem(&chunk.items[idx], false, slab); match != nil { + matches = append(matches, *match) + } + } + } else { + for _, result := range space { + if match, _, _ := p.MatchItem(result.item, false, slab); match != nil { + matches = append(matches, *match) + } + } + } + return matches +} + +// MatchItem returns true if the Item is a match +func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) { + if p.extended { + if offsets, bonus, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) { + result := buildResult(item, offsets, bonus) + return &result, offsets, pos + } + return nil, nil, nil + } + offset, bonus, pos := p.basicMatch(item, withPos, slab) + if sidx := offset[0]; sidx >= 0 { + offsets := []Offset{offset} + result := buildResult(item, offsets, bonus) + return &result, offsets, pos + } + return nil, nil, nil +} + +func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) { + var input []Token + if len(p.nth) == 0 { + input = []Token{{text: &item.text, prefixLength: 0}} + } else { + input = p.transformInput(item) + } + if p.fuzzy { + return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab) + } + return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab) +} + +func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, *[]int) { + var input []Token + if len(p.nth) == 0 { + input = []Token{{text: &item.text, prefixLength: 0}} + } else { + input = p.transformInput(item) + } + offsets := []Offset{} + var totalScore int + var allPos *[]int + if withPos { + allPos = &[]int{} + } + for _, termSet := range p.termSets { + var offset Offset + var currentScore int + matched := false + for _, term := range termSet { + pfun := p.procFun[term.typ] + off, score, pos := p.iter(pfun, input, term.caseSensitive, term.normalize, p.forward, term.text, withPos, slab) + if sidx := off[0]; sidx >= 0 { + if term.inv { + continue + } + offset, currentScore = off, score + matched = true + if withPos { + if pos != nil { + *allPos = append(*allPos, *pos...) + } else { + for idx := off[0]; idx < off[1]; idx++ { + *allPos = append(*allPos, int(idx)) + } + } + } + break + } else if term.inv { + offset, currentScore = Offset{0, 0}, 0 + matched = true + continue + } + } + if matched { + offsets = append(offsets, offset) + totalScore += currentScore + } + } + return offsets, totalScore, allPos +} + +func (p *Pattern) transformInput(item *Item) []Token { + if item.transformed != nil { + return *item.transformed + } + + tokens := Tokenize(item.text.ToString(), p.delimiter) + ret := Transform(tokens, p.nth) + item.transformed = &ret + return ret +} + +func (p *Pattern) iter(pfun algo.Algo, tokens []Token, caseSensitive bool, normalize bool, forward bool, pattern []rune, withPos bool, slab *util.Slab) (Offset, int, *[]int) { + for _, part := range tokens { + if res, pos := pfun(caseSensitive, normalize, forward, part.text, pattern, withPos, slab); res.Start >= 0 { + sidx := int32(res.Start) + part.prefixLength + eidx := int32(res.End) + part.prefixLength + if pos != nil { + for idx := range *pos { + (*pos)[idx] += int(part.prefixLength) + } + } + return Offset{sidx, eidx}, res.Score, pos + } + } + return Offset{-1, -1}, 0, nil +} diff --git a/fzf/fzf/src/pattern_test.go b/fzf/fzf/src/pattern_test.go new file mode 100644 index 0000000..b95d151 --- /dev/null +++ b/fzf/fzf/src/pattern_test.go @@ -0,0 +1,209 @@ +package fzf + +import ( + "reflect" + "testing" + + "github.com/junegunn/fzf/src/algo" + "github.com/junegunn/fzf/src/util" +) + +var slab *util.Slab + +func init() { + slab = util.MakeSlab(slab16Size, slab32Size) +} + +func TestParseTermsExtended(t *testing.T) { + terms := parseTerms(true, CaseSmart, false, + "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ | ^iii$ ^xxx | 'yyy | zzz$ | !ZZZ |") + if len(terms) != 9 || + terms[0][0].typ != termFuzzy || terms[0][0].inv || + terms[1][0].typ != termExact || terms[1][0].inv || + terms[2][0].typ != termPrefix || terms[2][0].inv || + terms[3][0].typ != termSuffix || terms[3][0].inv || + terms[4][0].typ != termExact || !terms[4][0].inv || + terms[5][0].typ != termFuzzy || !terms[5][0].inv || + terms[6][0].typ != termPrefix || !terms[6][0].inv || + terms[7][0].typ != termSuffix || !terms[7][0].inv || + terms[7][1].typ != termEqual || terms[7][1].inv || + terms[8][0].typ != termPrefix || terms[8][0].inv || + terms[8][1].typ != termExact || terms[8][1].inv || + terms[8][2].typ != termSuffix || terms[8][2].inv || + terms[8][3].typ != termExact || !terms[8][3].inv { + t.Errorf("%v", terms) + } + for _, termSet := range terms[:8] { + term := termSet[0] + if len(term.text) != 3 { + t.Errorf("%v", term) + } + } +} + +func TestParseTermsExtendedExact(t *testing.T) { + terms := parseTerms(false, CaseSmart, false, + "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") + if len(terms) != 8 || + terms[0][0].typ != termExact || terms[0][0].inv || len(terms[0][0].text) != 3 || + terms[1][0].typ != termFuzzy || terms[1][0].inv || len(terms[1][0].text) != 3 || + terms[2][0].typ != termPrefix || terms[2][0].inv || len(terms[2][0].text) != 3 || + terms[3][0].typ != termSuffix || terms[3][0].inv || len(terms[3][0].text) != 3 || + terms[4][0].typ != termExact || !terms[4][0].inv || len(terms[4][0].text) != 3 || + terms[5][0].typ != termFuzzy || !terms[5][0].inv || len(terms[5][0].text) != 3 || + terms[6][0].typ != termPrefix || !terms[6][0].inv || len(terms[6][0].text) != 3 || + terms[7][0].typ != termSuffix || !terms[7][0].inv || len(terms[7][0].text) != 3 { + t.Errorf("%v", terms) + } +} + +func TestParseTermsEmpty(t *testing.T) { + terms := parseTerms(true, CaseSmart, false, "' ^ !' !^") + if len(terms) != 0 { + t.Errorf("%v", terms) + } +} + +func TestExact(t *testing.T) { + defer clearPatternCache() + clearPatternCache() + pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, + []Range{}, Delimiter{}, []rune("'abc")) + chars := util.ToChars([]byte("aabbcc abc")) + res, pos := algo.ExactMatchNaive( + pattern.caseSensitive, pattern.normalize, pattern.forward, &chars, pattern.termSets[0][0].text, true, nil) + if res.Start != 7 || res.End != 10 { + t.Errorf("%v / %d / %d", pattern.termSets, res.Start, res.End) + } + if pos != nil { + t.Errorf("pos is expected to be nil") + } +} + +func TestEqual(t *testing.T) { + defer clearPatternCache() + clearPatternCache() + pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("^AbC$")) + + match := func(str string, sidxExpected int, eidxExpected int) { + chars := util.ToChars([]byte(str)) + res, pos := algo.EqualMatch( + pattern.caseSensitive, pattern.normalize, pattern.forward, &chars, pattern.termSets[0][0].text, true, nil) + if res.Start != sidxExpected || res.End != eidxExpected { + t.Errorf("%v / %d / %d", pattern.termSets, res.Start, res.End) + } + if pos != nil { + t.Errorf("pos is expected to be nil") + } + } + match("ABC", -1, -1) + match("AbC", 0, 3) + match("AbC ", 0, 3) + match(" AbC ", 1, 4) + match(" AbC", 2, 5) +} + +func TestCaseSensitivity(t *testing.T) { + defer clearPatternCache() + clearPatternCache() + pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("abc")) + clearPatternCache() + pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("Abc")) + clearPatternCache() + pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("abc")) + clearPatternCache() + pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("Abc")) + clearPatternCache() + pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, true, []Range{}, Delimiter{}, []rune("abc")) + clearPatternCache() + pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, true, []Range{}, Delimiter{}, []rune("Abc")) + + if string(pat1.text) != "abc" || pat1.caseSensitive != false || + string(pat2.text) != "Abc" || pat2.caseSensitive != true || + string(pat3.text) != "abc" || pat3.caseSensitive != false || + string(pat4.text) != "abc" || pat4.caseSensitive != false || + string(pat5.text) != "abc" || pat5.caseSensitive != true || + string(pat6.text) != "Abc" || pat6.caseSensitive != true { + t.Error("Invalid case conversion") + } +} + +func TestOrigTextAndTransformed(t *testing.T) { + pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg")) + tokens := Tokenize("junegunn", Delimiter{}) + trans := Transform(tokens, []Range{{1, 1}}) + + origBytes := []byte("junegunn.choi") + for _, extended := range []bool{false, true} { + chunk := Chunk{count: 1} + chunk.items[0] = Item{ + text: util.ToChars([]byte("junegunn")), + origText: &origBytes, + transformed: &trans} + pattern.extended = extended + matches := pattern.matchChunk(&chunk, nil, slab) // No cache + if !(matches[0].item.text.ToString() == "junegunn" && + string(*matches[0].item.origText) == "junegunn.choi" && + reflect.DeepEqual(*matches[0].item.transformed, trans)) { + t.Error("Invalid match result", matches) + } + + match, offsets, pos := pattern.MatchItem(&chunk.items[0], true, slab) + if !(match.item.text.ToString() == "junegunn" && + string(*match.item.origText) == "junegunn.choi" && + offsets[0][0] == 0 && offsets[0][1] == 5 && + reflect.DeepEqual(*match.item.transformed, trans)) { + t.Error("Invalid match result", match, offsets, extended) + } + if !((*pos)[0] == 4 && (*pos)[1] == 0) { + t.Error("Invalid pos array", *pos) + } + } +} + +func TestCacheKey(t *testing.T) { + test := func(extended bool, patStr string, expected string, cacheable bool) { + clearPatternCache() + pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune(patStr)) + if pat.CacheKey() != expected { + t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) + } + if pat.cacheable != cacheable { + t.Errorf("Expected: %t, actual: %t (%s)", cacheable, pat.cacheable, patStr) + } + clearPatternCache() + } + test(false, "foo !bar", "foo !bar", true) + test(false, "foo | bar !baz", "foo | bar !baz", true) + test(true, "foo bar baz", "foo\tbar\tbaz", true) + test(true, "foo !bar", "foo", false) + test(true, "foo !bar baz", "foo\tbaz", false) + test(true, "foo | bar baz", "baz", false) + test(true, "foo | bar | baz", "", false) + test(true, "foo | bar !baz", "", false) + test(true, "| | foo", "", false) + test(true, "| | | foo", "foo", false) +} + +func TestCacheable(t *testing.T) { + test := func(fuzzy bool, str string, expected string, cacheable bool) { + clearPatternCache() + pat := BuildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, true, []Range{}, Delimiter{}, []rune(str)) + if pat.CacheKey() != expected { + t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) + } + if cacheable != pat.cacheable { + t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable) + } + clearPatternCache() + } + test(true, "foo bar", "foo\tbar", true) + test(true, "foo 'bar", "foo\tbar", false) + test(true, "foo !bar", "foo", false) + + test(false, "foo bar", "foo\tbar", true) + test(false, "foo 'bar", "foo", false) + test(false, "foo '", "foo", true) + test(false, "foo 'bar", "foo", false) + test(false, "foo !bar", "foo", false) +} diff --git a/fzf/fzf/src/protector/protector.go b/fzf/fzf/src/protector/protector.go new file mode 100644 index 0000000..2739c01 --- /dev/null +++ b/fzf/fzf/src/protector/protector.go @@ -0,0 +1,8 @@ +// +build !openbsd + +package protector + +// Protect calls OS specific protections like pledge on OpenBSD +func Protect() { + return +} diff --git a/fzf/fzf/src/protector/protector_openbsd.go b/fzf/fzf/src/protector/protector_openbsd.go new file mode 100644 index 0000000..84a5ded --- /dev/null +++ b/fzf/fzf/src/protector/protector_openbsd.go @@ -0,0 +1,10 @@ +// +build openbsd + +package protector + +import "golang.org/x/sys/unix" + +// Protect calls OS specific protections like pledge on OpenBSD +func Protect() { + unix.PledgePromises("stdio rpath tty proc exec") +} diff --git a/fzf/fzf/src/reader.go b/fzf/fzf/src/reader.go new file mode 100644 index 0000000..06e9b73 --- /dev/null +++ b/fzf/fzf/src/reader.go @@ -0,0 +1,201 @@ +package fzf + +import ( + "bufio" + "context" + "io" + "os" + "os/exec" + "path/filepath" + "sync" + "sync/atomic" + "time" + + "github.com/junegunn/fzf/src/util" + "github.com/saracen/walker" +) + +// Reader reads from command or standard input +type Reader struct { + pusher func([]byte) bool + eventBox *util.EventBox + delimNil bool + event int32 + finChan chan bool + mutex sync.Mutex + exec *exec.Cmd + command *string + killed bool + wait bool +} + +// NewReader returns new Reader object +func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool, wait bool) *Reader { + return &Reader{pusher, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait} +} + +func (r *Reader) startEventPoller() { + go func() { + ptr := &r.event + pollInterval := readerPollIntervalMin + for { + if atomic.CompareAndSwapInt32(ptr, int32(EvtReadNew), int32(EvtReady)) { + r.eventBox.Set(EvtReadNew, (*string)(nil)) + pollInterval = readerPollIntervalMin + } else if atomic.LoadInt32(ptr) == int32(EvtReadFin) { + if r.wait { + r.finChan <- true + } + return + } else { + pollInterval += readerPollIntervalStep + if pollInterval > readerPollIntervalMax { + pollInterval = readerPollIntervalMax + } + } + time.Sleep(pollInterval) + } + }() +} + +func (r *Reader) fin(success bool) { + atomic.StoreInt32(&r.event, int32(EvtReadFin)) + if r.wait { + <-r.finChan + } + + r.mutex.Lock() + ret := r.command + if success || r.killed { + ret = nil + } + r.mutex.Unlock() + + r.eventBox.Set(EvtReadFin, ret) +} + +func (r *Reader) terminate() { + r.mutex.Lock() + defer func() { r.mutex.Unlock() }() + + r.killed = true + if r.exec != nil && r.exec.Process != nil { + util.KillCommand(r.exec) + } else if defaultCommand != "" { + os.Stdin.Close() + } +} + +func (r *Reader) restart(command string) { + r.event = int32(EvtReady) + r.startEventPoller() + success := r.readFromCommand(nil, command) + r.fin(success) +} + +// ReadSource reads data from the default command or from standard input +func (r *Reader) ReadSource() { + r.startEventPoller() + var success bool + if util.IsTty() { + // The default command for *nix requires bash + shell := "bash" + cmd := os.Getenv("FZF_DEFAULT_COMMAND") + if len(cmd) == 0 { + if defaultCommand != "" { + success = r.readFromCommand(&shell, defaultCommand) + } else { + success = r.readFiles() + } + } else { + success = r.readFromCommand(nil, cmd) + } + } else { + success = r.readFromStdin() + } + r.fin(success) +} + +func (r *Reader) feed(src io.Reader) { + delim := byte('\n') + if r.delimNil { + delim = '\000' + } + reader := bufio.NewReaderSize(src, readerBufferSize) + for { + // ReadBytes returns err != nil if and only if the returned data does not + // end in delim. + bytea, err := reader.ReadBytes(delim) + byteaLen := len(bytea) + if byteaLen > 0 { + if err == nil { + // get rid of carriage return if under Windows: + if util.IsWindows() && byteaLen >= 2 && bytea[byteaLen-2] == byte('\r') { + bytea = bytea[:byteaLen-2] + } else { + bytea = bytea[:byteaLen-1] + } + } + if r.pusher(bytea) { + atomic.StoreInt32(&r.event, int32(EvtReadNew)) + } + } + if err != nil { + break + } + } +} + +func (r *Reader) readFromStdin() bool { + r.feed(os.Stdin) + return true +} + +func (r *Reader) readFiles() bool { + r.killed = false + fn := func(path string, mode os.FileInfo) error { + path = filepath.Clean(path) + if path != "." { + isDir := mode.Mode().IsDir() + if isDir && filepath.Base(path)[0] == '.' { + return filepath.SkipDir + } + if !isDir && r.pusher([]byte(path)) { + atomic.StoreInt32(&r.event, int32(EvtReadNew)) + } + } + r.mutex.Lock() + defer r.mutex.Unlock() + if r.killed { + return context.Canceled + } + return nil + } + cb := walker.WithErrorCallback(func(pathname string, err error) error { + return nil + }) + return walker.Walk(".", fn, cb) == nil +} + +func (r *Reader) readFromCommand(shell *string, command string) bool { + r.mutex.Lock() + r.killed = false + r.command = &command + if shell != nil { + r.exec = util.ExecCommandWith(*shell, command, true) + } else { + r.exec = util.ExecCommand(command, true) + } + out, err := r.exec.StdoutPipe() + if err != nil { + r.mutex.Unlock() + return false + } + err = r.exec.Start() + r.mutex.Unlock() + if err != nil { + return false + } + r.feed(out) + return r.exec.Wait() == nil +} diff --git a/fzf/fzf/src/reader_test.go b/fzf/fzf/src/reader_test.go new file mode 100644 index 0000000..feb45fc --- /dev/null +++ b/fzf/fzf/src/reader_test.go @@ -0,0 +1,63 @@ +package fzf + +import ( + "testing" + "time" + + "github.com/junegunn/fzf/src/util" +) + +func TestReadFromCommand(t *testing.T) { + strs := []string{} + eb := util.NewEventBox() + reader := NewReader( + func(s []byte) bool { strs = append(strs, string(s)); return true }, + eb, false, true) + + reader.startEventPoller() + + // Check EventBox + if eb.Peek(EvtReadNew) { + t.Error("EvtReadNew should not be set yet") + } + + // Normal command + reader.fin(reader.readFromCommand(nil, `echo abc&&echo def`)) + if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" { + t.Errorf("%s", strs) + } + + // Check EventBox again + eb.WaitFor(EvtReadFin) + + // Wait should return immediately + eb.Wait(func(events *util.Events) { + events.Clear() + }) + + // EventBox is cleared + if eb.Peek(EvtReadNew) { + t.Error("EvtReadNew should not be set yet") + } + + // Make sure that event poller is finished + time.Sleep(readerPollIntervalMax) + + // Restart event poller + reader.startEventPoller() + + // Failing command + reader.fin(reader.readFromCommand(nil, `no-such-command`)) + strs = []string{} + if len(strs) > 0 { + t.Errorf("%s", strs) + } + + // Check EventBox again + if eb.Peek(EvtReadNew) { + t.Error("Command failed. EvtReadNew should not be set") + } + if !eb.Peek(EvtReadFin) { + t.Error("EvtReadFin should be set") + } +} diff --git a/fzf/fzf/src/result.go b/fzf/fzf/src/result.go new file mode 100644 index 0000000..8abe0d3 --- /dev/null +++ b/fzf/fzf/src/result.go @@ -0,0 +1,243 @@ +package fzf + +import ( + "math" + "sort" + "unicode" + + "github.com/junegunn/fzf/src/tui" + "github.com/junegunn/fzf/src/util" +) + +// Offset holds two 32-bit integers denoting the offsets of a matched substring +type Offset [2]int32 + +type colorOffset struct { + offset [2]int32 + color tui.ColorPair +} + +type Result struct { + item *Item + points [4]uint16 +} + +func buildResult(item *Item, offsets []Offset, score int) Result { + if len(offsets) > 1 { + sort.Sort(ByOrder(offsets)) + } + + result := Result{item: item} + numChars := item.text.Length() + minBegin := math.MaxUint16 + minEnd := math.MaxUint16 + maxEnd := 0 + validOffsetFound := false + for _, offset := range offsets { + b, e := int(offset[0]), int(offset[1]) + if b < e { + minBegin = util.Min(b, minBegin) + minEnd = util.Min(e, minEnd) + maxEnd = util.Max(e, maxEnd) + validOffsetFound = true + } + } + + for idx, criterion := range sortCriteria { + val := uint16(math.MaxUint16) + switch criterion { + case byScore: + // Higher is better + val = math.MaxUint16 - util.AsUint16(score) + case byLength: + val = item.TrimLength() + case byBegin, byEnd: + if validOffsetFound { + whitePrefixLen := 0 + for idx := 0; idx < numChars; idx++ { + r := item.text.Get(idx) + whitePrefixLen = idx + if idx == minBegin || !unicode.IsSpace(r) { + break + } + } + if criterion == byBegin { + val = util.AsUint16(minEnd - whitePrefixLen) + } else { + val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength())) + } + } + } + result.points[3-idx] = val + } + + return result +} + +// Sort criteria to use. Never changes once fzf is started. +var sortCriteria []criterion + +// Index returns ordinal index of the Item +func (result *Result) Index() int32 { + return result.item.Index() +} + +func minRank() Result { + return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}} +} + +func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, current bool) []colorOffset { + itemColors := result.item.Colors() + + // No ANSI codes + if len(itemColors) == 0 { + var offsets []colorOffset + for _, off := range matchOffsets { + offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch}) + } + return offsets + } + + // Find max column + var maxCol int32 + for _, off := range matchOffsets { + if off[1] > maxCol { + maxCol = off[1] + } + } + for _, ansi := range itemColors { + if ansi.offset[1] > maxCol { + maxCol = ansi.offset[1] + } + } + + cols := make([]int, maxCol) + for colorIndex, ansi := range itemColors { + for i := ansi.offset[0]; i < ansi.offset[1]; i++ { + cols[i] = colorIndex + 1 // 1-based index of itemColors + } + } + + for _, off := range matchOffsets { + for i := off[0]; i < off[1]; i++ { + // Negative of 1-based index of itemColors + // - The extra -1 means highlighted + cols[i] = cols[i]*-1 - 1 + } + } + + // sort.Sort(ByOrder(offsets)) + + // Merge offsets + // ------------ ---- -- ---- + // ++++++++ ++++++++++ + // --++++++++-- --++++++++++--- + curr := 0 + start := 0 + ansiToColorPair := func(ansi ansiOffset, base tui.ColorPair) tui.ColorPair { + fg := ansi.color.fg + bg := ansi.color.bg + if fg == -1 { + if current { + fg = theme.Current.Color + } else { + fg = theme.Fg.Color + } + } + if bg == -1 { + if current { + bg = theme.DarkBg.Color + } else { + bg = theme.Bg.Color + } + } + return tui.NewColorPair(fg, bg, ansi.color.attr).MergeAttr(base) + } + var colors []colorOffset + add := func(idx int) { + if curr != 0 && idx > start { + if curr < 0 { + color := colMatch + if curr < -1 && theme.Colored { + origColor := ansiToColorPair(itemColors[-curr-2], colMatch) + // hl or hl+ only sets the foreground color, so colMatch is the + // combination of either [hl and bg] or [hl+ and bg+]. + // + // If the original text already has background color, and the + // foreground color of colMatch is -1, we shouldn't only apply the + // background color of colMatch. + // e.g. echo -e "\x1b[32;7mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline + // echo -e "\x1b[42mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline + if color.Fg().IsDefault() && origColor.HasBg() { + color = origColor + } else { + color = origColor.MergeNonDefault(color) + } + } + colors = append(colors, colorOffset{ + offset: [2]int32{int32(start), int32(idx)}, color: color}) + } else { + ansi := itemColors[curr-1] + colors = append(colors, colorOffset{ + offset: [2]int32{int32(start), int32(idx)}, + color: ansiToColorPair(ansi, colBase)}) + } + } + } + for idx, col := range cols { + if col != curr { + add(idx) + start = idx + curr = col + } + } + add(int(maxCol)) + return colors +} + +// ByOrder is for sorting substring offsets +type ByOrder []Offset + +func (a ByOrder) Len() int { + return len(a) +} + +func (a ByOrder) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a ByOrder) Less(i, j int) bool { + ioff := a[i] + joff := a[j] + return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1]) +} + +// ByRelevance is for sorting Items +type ByRelevance []Result + +func (a ByRelevance) Len() int { + return len(a) +} + +func (a ByRelevance) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a ByRelevance) Less(i, j int) bool { + return compareRanks(a[i], a[j], false) +} + +// ByRelevanceTac is for sorting Items +type ByRelevanceTac []Result + +func (a ByRelevanceTac) Len() int { + return len(a) +} + +func (a ByRelevanceTac) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a ByRelevanceTac) Less(i, j int) bool { + return compareRanks(a[i], a[j], true) +} diff --git a/fzf/fzf/src/result_others.go b/fzf/fzf/src/result_others.go new file mode 100644 index 0000000..e3363a8 --- /dev/null +++ b/fzf/fzf/src/result_others.go @@ -0,0 +1,16 @@ +// +build !386,!amd64 + +package fzf + +func compareRanks(irank Result, jrank Result, tac bool) bool { + for idx := 3; idx >= 0; idx-- { + left := irank.points[idx] + right := jrank.points[idx] + if left < right { + return true + } else if left > right { + return false + } + } + return (irank.item.Index() <= jrank.item.Index()) != tac +} diff --git a/fzf/fzf/src/result_test.go b/fzf/fzf/src/result_test.go new file mode 100644 index 0000000..4084fdb --- /dev/null +++ b/fzf/fzf/src/result_test.go @@ -0,0 +1,159 @@ +// +build !tcell + +package fzf + +import ( + "math" + "sort" + "testing" + + "github.com/junegunn/fzf/src/tui" + "github.com/junegunn/fzf/src/util" +) + +func withIndex(i *Item, index int) *Item { + (*i).text.Index = int32(index) + return i +} + +func TestOffsetSort(t *testing.T) { + offsets := []Offset{ + {3, 5}, {2, 7}, + {1, 3}, {2, 9}} + sort.Sort(ByOrder(offsets)) + + if offsets[0][0] != 1 || offsets[0][1] != 3 || + offsets[1][0] != 2 || offsets[1][1] != 7 || + offsets[2][0] != 2 || offsets[2][1] != 9 || + offsets[3][0] != 3 || offsets[3][1] != 5 { + t.Error("Invalid order:", offsets) + } +} + +func TestRankComparison(t *testing.T) { + rank := func(vals ...uint16) Result { + return Result{ + points: [4]uint16{vals[0], vals[1], vals[2], vals[3]}, + item: &Item{text: util.Chars{Index: int32(vals[4])}}} + } + if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), false) || + !compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) || + !compareRanks(rank(1, 2, 0, 0, 3), rank(1, 3, 0, 0, 2), false) || + !compareRanks(rank(0, 0, 0, 0, 0), rank(0, 0, 0, 0, 0), false) { + t.Error("Invalid order") + } + + if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), true) || + !compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) || + !compareRanks(rank(1, 2, 0, 0, 3), rank(1, 3, 0, 0, 2), true) || + !compareRanks(rank(0, 0, 0, 0, 0), rank(0, 0, 0, 0, 0), false) { + t.Error("Invalid order (tac)") + } +} + +// Match length, string length, index +func TestResultRank(t *testing.T) { + // FIXME global + sortCriteria = []criterion{byScore, byLength} + + strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")} + item1 := buildResult( + withIndex(&Item{text: util.RunesToChars(strs[0])}, 1), []Offset{}, 2) + if item1.points[3] != math.MaxUint16-2 || // Bonus + item1.points[2] != 3 || // Length + item1.points[1] != 0 || // Unused + item1.points[0] != 0 || // Unused + item1.item.Index() != 1 { + t.Error(item1) + } + // Only differ in index + item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2) + + items := []Result{item1, item2} + sort.Sort(ByRelevance(items)) + if items[0] != item2 || items[1] != item1 { + t.Error(items) + } + + items = []Result{item2, item1, item1, item2} + sort.Sort(ByRelevance(items)) + if items[0] != item2 || items[1] != item2 || + items[2] != item1 || items[3] != item1 { + t.Error(items, item1, item1.item.Index(), item2, item2.item.Index()) + } + + // Sort by relevance + item3 := buildResult( + withIndex(&Item{}, 2), []Offset{{1, 3}, {5, 7}}, 3) + item4 := buildResult( + withIndex(&Item{}, 2), []Offset{{1, 2}, {6, 7}}, 4) + item5 := buildResult( + withIndex(&Item{}, 2), []Offset{{1, 3}, {5, 7}}, 5) + item6 := buildResult( + withIndex(&Item{}, 2), []Offset{{1, 2}, {6, 7}}, 6) + items = []Result{item1, item2, item3, item4, item5, item6} + sort.Sort(ByRelevance(items)) + if !(items[0] == item6 && items[1] == item5 && + items[2] == item4 && items[3] == item3 && + items[4] == item2 && items[5] == item1) { + t.Error(items, item1, item2, item3, item4, item5, item6) + } +} + +func TestColorOffset(t *testing.T) { + // ------------ 20 ---- -- ---- + // ++++++++ ++++++++++ + // --++++++++-- --++++++++++--- + + offsets := []Offset{{5, 15}, {25, 35}} + item := Result{ + item: &Item{ + colors: &[]ansiOffset{ + {[2]int32{0, 20}, ansiState{1, 5, 0, -1}}, + {[2]int32{22, 27}, ansiState{2, 6, tui.Bold, -1}}, + {[2]int32{30, 32}, ansiState{3, 7, 0, -1}}, + {[2]int32{33, 40}, ansiState{4, 8, tui.Bold, -1}}}}} + + colBase := tui.NewColorPair(89, 189, tui.AttrUndefined) + colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined) + colors := item.colorOffsets(offsets, tui.Dark256, colBase, colMatch, true) + assert := func(idx int, b int32, e int32, c tui.ColorPair) { + o := colors[idx] + if o.offset[0] != b || o.offset[1] != e || o.color != c { + t.Error(o, b, e, c) + } + } + // [{[0 5] {1 5 0}} {[5 15] {99 199 0}} {[15 20] {1 5 0}} + // {[22 25] {2 6 1}} {[25 27] {99 199 1}} {[27 30] {99 199 0}} + // {[30 32] {99 199 0}} {[32 33] {99 199 0}} {[33 35] {99 199 1}} + // {[35 40] {4 8 1}}] + assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined)) + assert(1, 5, 15, colMatch) + assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined)) + assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold)) + assert(4, 25, 27, colMatch.WithAttr(tui.Bold)) + assert(5, 27, 30, colMatch) + assert(6, 30, 32, colMatch) + assert(7, 32, 33, colMatch) // TODO: Should we merge consecutive blocks? + assert(8, 33, 35, colMatch.WithAttr(tui.Bold)) + assert(9, 35, 40, tui.NewColorPair(4, 8, tui.Bold)) + + colRegular := tui.NewColorPair(-1, -1, tui.AttrUndefined) + colUnderline := tui.NewColorPair(-1, -1, tui.Underline) + colors = item.colorOffsets(offsets, tui.Dark256, colRegular, colUnderline, true) + + // [{[0 5] {1 5 0}} {[5 15] {1 5 8}} {[15 20] {1 5 0}} + // {[22 25] {2 6 1}} {[25 27] {2 6 9}} {[27 30] {-1 -1 8}} + // {[30 32] {3 7 8}} {[32 33] {-1 -1 8}} {[33 35] {4 8 9}} + // {[35 40] {4 8 1}}] + assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined)) + assert(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline)) + assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined)) + assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold)) + assert(4, 25, 27, tui.NewColorPair(2, 6, tui.Bold|tui.Underline)) + assert(5, 27, 30, colUnderline) + assert(6, 30, 32, tui.NewColorPair(3, 7, tui.Underline)) + assert(7, 32, 33, colUnderline) + assert(8, 33, 35, tui.NewColorPair(4, 8, tui.Bold|tui.Underline)) + assert(9, 35, 40, tui.NewColorPair(4, 8, tui.Bold)) +} diff --git a/fzf/fzf/src/result_x86.go b/fzf/fzf/src/result_x86.go new file mode 100644 index 0000000..60e26e9 --- /dev/null +++ b/fzf/fzf/src/result_x86.go @@ -0,0 +1,16 @@ +// +build 386 amd64 + +package fzf + +import "unsafe" + +func compareRanks(irank Result, jrank Result, tac bool) bool { + left := *(*uint64)(unsafe.Pointer(&irank.points[0])) + right := *(*uint64)(unsafe.Pointer(&jrank.points[0])) + if left < right { + return true + } else if left > right { + return false + } + return (irank.item.Index() <= jrank.item.Index()) != tac +} diff --git a/fzf/fzf/src/terminal.go b/fzf/fzf/src/terminal.go new file mode 100644 index 0000000..e4823ad --- /dev/null +++ b/fzf/fzf/src/terminal.go @@ -0,0 +1,2890 @@ +package fzf + +import ( + "bufio" + "fmt" + "io/ioutil" + "math" + "os" + "os/signal" + "regexp" + "sort" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/mattn/go-runewidth" + "github.com/rivo/uniseg" + + "github.com/junegunn/fzf/src/tui" + "github.com/junegunn/fzf/src/util" +) + +// import "github.com/pkg/profile" + +/* + Placeholder regex is used to extract placeholders from fzf's template + strings. Acts as input validation for parsePlaceholder function. + Describes the syntax, but it is fairly lenient. + + The following pseudo regex has been reverse engineered from the + implementation. It is overly strict, but better describes whats possible. + As such it is not useful for validation, but rather to generate test + cases for example. + + \\?(?: # escaped type + {\+?s?f?RANGE(?:,RANGE)*} # token type + |{q} # query type + |{\+?n?f?} # item type (notice no mandatory element inside brackets) + ) + RANGE = (?: + (?:-?[0-9]+)?\.\.(?:-?[0-9]+)? # ellipsis syntax for token range (x..y) + |-?[0-9]+ # shorthand syntax (x..x) + ) +*/ +var placeholder *regexp.Regexp +var whiteSuffix *regexp.Regexp +var offsetComponentRegex *regexp.Regexp +var offsetTrimCharsRegex *regexp.Regexp +var activeTempFiles []string + +const ellipsis string = ".." +const clearCode string = "\x1b[2J" + +func init() { + placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\+?f?nf?})`) + whiteSuffix = regexp.MustCompile(`\s*$`) + offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`) + offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`) + activeTempFiles = []string{} +} + +type jumpMode int + +const ( + jumpDisabled jumpMode = iota + jumpEnabled + jumpAcceptEnabled +) + +type previewer struct { + version int64 + lines []string + offset int + enabled bool + scrollable bool + final bool + following bool + spinner string +} + +type previewed struct { + version int64 + numLines int + offset int + filled bool +} + +type eachLine struct { + line string + err error +} + +type itemLine struct { + current bool + selected bool + label string + queryLen int + width int + result Result +} + +var emptyLine = itemLine{} + +// Terminal represents terminal input/output +type Terminal struct { + initDelay time.Duration + infoStyle infoStyle + spinner []string + prompt func() + promptLen int + pointer string + pointerLen int + pointerEmpty string + marker string + markerLen int + markerEmpty string + queryLen [2]int + layout layoutType + fullscreen bool + keepRight bool + hscroll bool + hscrollOff int + scrollOff int + wordRubout string + wordNext string + cx int + cy int + offset int + xoffset int + yanked []rune + input []rune + multi int + sort bool + toggleSort bool + delimiter Delimiter + expect map[tui.Event]string + keymap map[tui.Event][]*action + pressed string + printQuery bool + history *History + cycle bool + headerFirst bool + headerLines int + header []string + header0 []string + ansi bool + tabstop int + margin [4]sizeSpec + padding [4]sizeSpec + strong tui.Attr + unicode bool + borderShape tui.BorderShape + cleanExit bool + paused bool + border tui.Window + window tui.Window + pborder tui.Window + pwindow tui.Window + count int + progress int + reading bool + running bool + failed *string + jumping jumpMode + jumpLabels string + printer func(string) + printsep string + merger *Merger + selected map[int32]selectedItem + version int64 + reqBox *util.EventBox + initialPreviewOpts previewOpts + previewOpts previewOpts + previewer previewer + previewed previewed + previewBox *util.EventBox + eventBox *util.EventBox + mutex sync.Mutex + initFunc func() + prevLines []itemLine + suppress bool + sigstop bool + startChan chan bool + killChan chan int + slab *util.Slab + theme *tui.ColorTheme + tui tui.Renderer + executing *util.AtomicBool +} + +type selectedItem struct { + at time.Time + item *Item +} + +type byTimeOrder []selectedItem + +func (a byTimeOrder) Len() int { + return len(a) +} + +func (a byTimeOrder) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a byTimeOrder) Less(i, j int) bool { + return a[i].at.Before(a[j].at) +} + +const ( + reqPrompt util.EventType = iota + reqInfo + reqHeader + reqList + reqJump + reqRefresh + reqReinit + reqRedraw + reqFullRedraw + reqClose + reqPrintQuery + reqPreviewEnqueue + reqPreviewDisplay + reqPreviewRefresh + reqPreviewDelayed + reqQuit +) + +type action struct { + t actionType + a string +} + +type actionType int + +const ( + actIgnore actionType = iota + actInvalid + actRune + actMouse + actBeginningOfLine + actAbort + actAccept + actAcceptNonEmpty + actBackwardChar + actBackwardDeleteChar + actBackwardDeleteCharEOF + actBackwardWord + actCancel + actChangePrompt + actClearScreen + actClearQuery + actClearSelection + actClose + actDeleteChar + actDeleteCharEOF + actEndOfLine + actForwardChar + actForwardWord + actKillLine + actKillWord + actUnixLineDiscard + actUnixWordRubout + actYank + actBackwardKillWord + actSelectAll + actDeselectAll + actToggle + actToggleSearch + actToggleAll + actToggleDown + actToggleUp + actToggleIn + actToggleOut + actDown + actUp + actPageUp + actPageDown + actHalfPageUp + actHalfPageDown + actJump + actJumpAccept + actPrintQuery + actRefreshPreview + actReplaceQuery + actToggleSort + actTogglePreview + actTogglePreviewWrap + actPreview + actChangePreview + actChangePreviewWindow + actPreviewTop + actPreviewBottom + actPreviewUp + actPreviewDown + actPreviewPageUp + actPreviewPageDown + actPreviewHalfPageUp + actPreviewHalfPageDown + actPreviousHistory + actNextHistory + actExecute + actExecuteSilent + actExecuteMulti // Deprecated + actSigStop + actFirst + actLast + actReload + actDisableSearch + actEnableSearch + actSelect + actDeselect + actUnbind +) + +type placeholderFlags struct { + plus bool + preserveSpace bool + number bool + query bool + file bool +} + +type searchRequest struct { + sort bool + command *string +} + +type previewRequest struct { + template string + pwindow tui.Window + scrollOffset int + list []*Item +} + +type previewResult struct { + version int64 + lines []string + offset int + spinner string +} + +func toActions(types ...actionType) []*action { + actions := make([]*action, len(types)) + for idx, t := range types { + actions[idx] = &action{t: t, a: ""} + } + return actions +} + +func defaultKeymap() map[tui.Event][]*action { + keymap := make(map[tui.Event][]*action) + add := func(e tui.EventType, a actionType) { + keymap[e.AsEvent()] = toActions(a) + } + addEvent := func(e tui.Event, a actionType) { + keymap[e] = toActions(a) + } + + add(tui.Invalid, actInvalid) + add(tui.Resize, actClearScreen) + add(tui.CtrlA, actBeginningOfLine) + add(tui.CtrlB, actBackwardChar) + add(tui.CtrlC, actAbort) + add(tui.CtrlG, actAbort) + add(tui.CtrlQ, actAbort) + add(tui.ESC, actAbort) + add(tui.CtrlD, actDeleteCharEOF) + add(tui.CtrlE, actEndOfLine) + add(tui.CtrlF, actForwardChar) + add(tui.CtrlH, actBackwardDeleteChar) + add(tui.BSpace, actBackwardDeleteChar) + add(tui.Tab, actToggleDown) + add(tui.BTab, actToggleUp) + add(tui.CtrlJ, actDown) + add(tui.CtrlK, actUp) + add(tui.CtrlL, actClearScreen) + add(tui.CtrlM, actAccept) + add(tui.CtrlN, actDown) + add(tui.CtrlP, actUp) + add(tui.CtrlU, actUnixLineDiscard) + add(tui.CtrlW, actUnixWordRubout) + add(tui.CtrlY, actYank) + if !util.IsWindows() { + add(tui.CtrlZ, actSigStop) + } + + addEvent(tui.AltKey('b'), actBackwardWord) + add(tui.SLeft, actBackwardWord) + addEvent(tui.AltKey('f'), actForwardWord) + add(tui.SRight, actForwardWord) + addEvent(tui.AltKey('d'), actKillWord) + add(tui.AltBS, actBackwardKillWord) + + add(tui.Up, actUp) + add(tui.Down, actDown) + add(tui.Left, actBackwardChar) + add(tui.Right, actForwardChar) + + add(tui.Home, actBeginningOfLine) + add(tui.End, actEndOfLine) + add(tui.Del, actDeleteChar) + add(tui.PgUp, actPageUp) + add(tui.PgDn, actPageDown) + + add(tui.SUp, actPreviewUp) + add(tui.SDown, actPreviewDown) + + add(tui.Mouse, actMouse) + add(tui.DoubleClick, actAccept) + add(tui.LeftClick, actIgnore) + add(tui.RightClick, actToggle) + return keymap +} + +func trimQuery(query string) []rune { + return []rune(strings.Replace(query, "\t", " ", -1)) +} + +func hasPreviewAction(opts *Options) bool { + for _, actions := range opts.Keymap { + for _, action := range actions { + if action.t == actPreview || action.t == actChangePreview { + return true + } + } + } + return false +} + +func makeSpinner(unicode bool) []string { + if unicode { + return []string{`⠋`, `⠙`, `⠹`, `⠸`, `⠼`, `⠴`, `⠦`, `⠧`, `⠇`, `⠏`} + } + return []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`} +} + +// NewTerminal returns new Terminal object +func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { + input := trimQuery(opts.Query) + var header []string + switch opts.Layout { + case layoutDefault, layoutReverseList: + header = reverseStringArray(opts.Header) + default: + header = opts.Header + } + var delay time.Duration + if opts.Tac { + delay = initialDelayTac + } else { + delay = initialDelay + } + var previewBox *util.EventBox + showPreviewWindow := len(opts.Preview.command) > 0 && !opts.Preview.hidden + if len(opts.Preview.command) > 0 || hasPreviewAction(opts) { + previewBox = util.NewEventBox() + } + strongAttr := tui.Bold + if !opts.Bold { + strongAttr = tui.AttrRegular + } + var renderer tui.Renderer + fullscreen := opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100 + if fullscreen { + if tui.HasFullscreenRenderer() { + renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse) + } else { + renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, + true, func(h int) int { return h }) + } + } else { + maxHeightFunc := func(termHeight int) int { + var maxHeight int + if opts.Height.percent { + maxHeight = util.Max(int(opts.Height.size*float64(termHeight)/100.0), opts.MinHeight) + } else { + maxHeight = int(opts.Height.size) + } + + effectiveMinHeight := minHeight + if previewBox != nil && (opts.Preview.position == posUp || opts.Preview.position == posDown) { + effectiveMinHeight *= 2 + } + if opts.InfoStyle != infoDefault { + effectiveMinHeight-- + } + if opts.BorderShape != tui.BorderNone { + effectiveMinHeight += 2 + } + return util.Min(termHeight, util.Max(maxHeight, effectiveMinHeight)) + } + renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, false, maxHeightFunc) + } + wordRubout := "[^\\pL\\pN][\\pL\\pN]" + wordNext := "[\\pL\\pN][^\\pL\\pN]|(.$)" + if opts.FileWord { + sep := regexp.QuoteMeta(string(os.PathSeparator)) + wordRubout = fmt.Sprintf("%s[^%s]", sep, sep) + wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep) + } + t := Terminal{ + initDelay: delay, + infoStyle: opts.InfoStyle, + spinner: makeSpinner(opts.Unicode), + queryLen: [2]int{0, 0}, + layout: opts.Layout, + fullscreen: fullscreen, + keepRight: opts.KeepRight, + hscroll: opts.Hscroll, + hscrollOff: opts.HscrollOff, + scrollOff: opts.ScrollOff, + wordRubout: wordRubout, + wordNext: wordNext, + cx: len(input), + cy: 0, + offset: 0, + xoffset: 0, + yanked: []rune{}, + input: input, + multi: opts.Multi, + sort: opts.Sort > 0, + toggleSort: opts.ToggleSort, + delimiter: opts.Delimiter, + expect: opts.Expect, + keymap: opts.Keymap, + pressed: "", + printQuery: opts.PrintQuery, + history: opts.History, + margin: opts.Margin, + padding: opts.Padding, + unicode: opts.Unicode, + borderShape: opts.BorderShape, + cleanExit: opts.ClearOnExit, + paused: opts.Phony, + strong: strongAttr, + cycle: opts.Cycle, + headerFirst: opts.HeaderFirst, + headerLines: opts.HeaderLines, + header: header, + header0: header, + ansi: opts.Ansi, + tabstop: opts.Tabstop, + reading: true, + running: true, + failed: nil, + jumping: jumpDisabled, + jumpLabels: opts.JumpLabels, + printer: opts.Printer, + printsep: opts.PrintSep, + merger: EmptyMerger, + selected: make(map[int32]selectedItem), + reqBox: util.NewEventBox(), + initialPreviewOpts: opts.Preview, + previewOpts: opts.Preview, + previewer: previewer{0, []string{}, 0, showPreviewWindow, false, true, false, ""}, + previewed: previewed{0, 0, 0, false}, + previewBox: previewBox, + eventBox: eventBox, + mutex: sync.Mutex{}, + suppress: true, + sigstop: false, + slab: util.MakeSlab(slab16Size, slab32Size), + theme: opts.Theme, + startChan: make(chan bool, 1), + killChan: make(chan int), + tui: renderer, + initFunc: func() { renderer.Init() }, + executing: util.NewAtomicBool(false)} + t.prompt, t.promptLen = t.parsePrompt(opts.Prompt) + t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0) + t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0) + // Pre-calculated empty pointer and marker signs + t.pointerEmpty = strings.Repeat(" ", t.pointerLen) + t.markerEmpty = strings.Repeat(" ", t.markerLen) + + return &t +} + +func (t *Terminal) parsePrompt(prompt string) (func(), int) { + var state *ansiState + trimmed, colors, _ := extractColor(prompt, state, nil) + item := &Item{text: util.ToChars([]byte(trimmed)), colors: colors} + + // "Prompt> " + // ------- // Do not apply ANSI attributes to the trailing whitespaces + // // unless the part has a non-default ANSI state + loc := whiteSuffix.FindStringIndex(trimmed) + if loc != nil { + blankState := ansiOffset{[2]int32{int32(loc[0]), int32(loc[1])}, ansiState{-1, -1, tui.AttrClear, -1}} + if item.colors != nil { + lastColor := (*item.colors)[len(*item.colors)-1] + if lastColor.offset[1] < int32(loc[1]) { + blankState.offset[0] = lastColor.offset[1] + colors := append(*item.colors, blankState) + item.colors = &colors + } + } else { + colors := []ansiOffset{blankState} + item.colors = &colors + } + } + output := func() { + t.printHighlighted( + Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false) + } + _, promptLen := t.processTabs([]rune(trimmed), 0) + + return output, promptLen +} + +func (t *Terminal) noInfoLine() bool { + return t.infoStyle != infoDefault +} + +// Input returns current query string +func (t *Terminal) Input() (bool, []rune) { + t.mutex.Lock() + defer t.mutex.Unlock() + return t.paused, copySlice(t.input) +} + +// UpdateCount updates the count information +func (t *Terminal) UpdateCount(cnt int, final bool, failedCommand *string) { + t.mutex.Lock() + t.count = cnt + t.reading = !final + t.failed = failedCommand + t.mutex.Unlock() + t.reqBox.Set(reqInfo, nil) + if final { + t.reqBox.Set(reqRefresh, nil) + } +} + +func reverseStringArray(input []string) []string { + size := len(input) + reversed := make([]string, size) + for idx, str := range input { + reversed[size-idx-1] = str + } + return reversed +} + +// UpdateHeader updates the header +func (t *Terminal) UpdateHeader(header []string) { + t.mutex.Lock() + t.header = append(append([]string{}, t.header0...), header...) + t.mutex.Unlock() + t.reqBox.Set(reqHeader, nil) +} + +// UpdateProgress updates the search progress +func (t *Terminal) UpdateProgress(progress float32) { + t.mutex.Lock() + newProgress := int(progress * 100) + changed := t.progress != newProgress + t.progress = newProgress + t.mutex.Unlock() + + if changed { + t.reqBox.Set(reqInfo, nil) + } +} + +// UpdateList updates Merger to display the list +func (t *Terminal) UpdateList(merger *Merger, reset bool) { + t.mutex.Lock() + t.progress = 100 + t.merger = merger + if reset { + t.selected = make(map[int32]selectedItem) + } + t.mutex.Unlock() + t.reqBox.Set(reqInfo, nil) + t.reqBox.Set(reqList, nil) +} + +func (t *Terminal) output() bool { + if t.printQuery { + t.printer(string(t.input)) + } + if len(t.expect) > 0 { + t.printer(t.pressed) + } + found := len(t.selected) > 0 + if !found { + current := t.currentItem() + if current != nil { + t.printer(current.AsString(t.ansi)) + found = true + } + } else { + for _, sel := range t.sortSelected() { + t.printer(sel.item.AsString(t.ansi)) + } + } + return found +} + +func (t *Terminal) sortSelected() []selectedItem { + sels := make([]selectedItem, 0, len(t.selected)) + for _, sel := range t.selected { + sels = append(sels, sel) + } + sort.Sort(byTimeOrder(sels)) + return sels +} + +func (t *Terminal) displayWidth(runes []rune) int { + width, _ := util.RunesWidth(runes, 0, t.tabstop, math.MaxInt32) + return width +} + +const ( + minWidth = 4 + minHeight = 4 +) + +func calculateSize(base int, size sizeSpec, occupied int, minSize int, pad int) int { + max := base - occupied + if size.percent { + return util.Constrain(int(float64(base)*0.01*size.size), minSize, max) + } + return util.Constrain(int(size.size)+pad, minSize, max) +} + +func (t *Terminal) resizeWindows() { + screenWidth := t.tui.MaxX() + screenHeight := t.tui.MaxY() + t.prevLines = make([]itemLine, screenHeight) + + marginInt := [4]int{} // TRBL + paddingInt := [4]int{} // TRBL + sizeSpecToInt := func(index int, spec sizeSpec) int { + if spec.percent { + var max float64 + if index%2 == 0 { + max = float64(screenHeight) + } else { + max = float64(screenWidth) + } + return int(max * spec.size * 0.01) + } + return int(spec.size) + } + for idx, sizeSpec := range t.padding { + paddingInt[idx] = sizeSpecToInt(idx, sizeSpec) + } + + extraMargin := [4]int{} // TRBL + for idx, sizeSpec := range t.margin { + switch t.borderShape { + case tui.BorderHorizontal: + extraMargin[idx] += 1 - idx%2 + case tui.BorderVertical: + extraMargin[idx] += 2 * (idx % 2) + case tui.BorderTop: + if idx == 0 { + extraMargin[idx]++ + } + case tui.BorderRight: + if idx == 1 { + extraMargin[idx] += 2 + } + case tui.BorderBottom: + if idx == 2 { + extraMargin[idx]++ + } + case tui.BorderLeft: + if idx == 3 { + extraMargin[idx] += 2 + } + case tui.BorderRounded, tui.BorderSharp: + extraMargin[idx] += 1 + idx%2 + } + marginInt[idx] = sizeSpecToInt(idx, sizeSpec) + extraMargin[idx] + } + + adjust := func(idx1 int, idx2 int, max int, min int) { + if max >= min { + margin := marginInt[idx1] + marginInt[idx2] + paddingInt[idx1] + paddingInt[idx2] + if max-margin < min { + desired := max - min + paddingInt[idx1] = desired * paddingInt[idx1] / margin + paddingInt[idx2] = desired * paddingInt[idx2] / margin + marginInt[idx1] = util.Max(extraMargin[idx1], desired*marginInt[idx1]/margin) + marginInt[idx2] = util.Max(extraMargin[idx2], desired*marginInt[idx2]/margin) + } + } + } + + previewVisible := t.isPreviewEnabled() && t.previewOpts.size.size > 0 + minAreaWidth := minWidth + minAreaHeight := minHeight + if previewVisible { + switch t.previewOpts.position { + case posUp, posDown: + minAreaHeight *= 2 + case posLeft, posRight: + minAreaWidth *= 2 + } + } + adjust(1, 3, screenWidth, minAreaWidth) + adjust(0, 2, screenHeight, minAreaHeight) + if t.border != nil { + t.border.Close() + } + if t.window != nil { + t.window.Close() + } + if t.pborder != nil { + t.pborder.Close() + } + if t.pwindow != nil { + t.pwindow.Close() + } + // Reset preview version so that full redraw occurs + t.previewed.version = 0 + + width := screenWidth - marginInt[1] - marginInt[3] + height := screenHeight - marginInt[0] - marginInt[2] + switch t.borderShape { + case tui.BorderHorizontal: + t.border = t.tui.NewWindow( + marginInt[0]-1, marginInt[3], width, height+2, + false, tui.MakeBorderStyle(tui.BorderHorizontal, t.unicode)) + case tui.BorderVertical: + t.border = t.tui.NewWindow( + marginInt[0], marginInt[3]-2, width+4, height, + false, tui.MakeBorderStyle(tui.BorderVertical, t.unicode)) + case tui.BorderTop: + t.border = t.tui.NewWindow( + marginInt[0]-1, marginInt[3], width, height+1, + false, tui.MakeBorderStyle(tui.BorderTop, t.unicode)) + case tui.BorderBottom: + t.border = t.tui.NewWindow( + marginInt[0], marginInt[3], width, height+1, + false, tui.MakeBorderStyle(tui.BorderBottom, t.unicode)) + case tui.BorderLeft: + t.border = t.tui.NewWindow( + marginInt[0], marginInt[3]-2, width+2, height, + false, tui.MakeBorderStyle(tui.BorderLeft, t.unicode)) + case tui.BorderRight: + t.border = t.tui.NewWindow( + marginInt[0], marginInt[3], width+2, height, + false, tui.MakeBorderStyle(tui.BorderRight, t.unicode)) + case tui.BorderRounded, tui.BorderSharp: + t.border = t.tui.NewWindow( + marginInt[0]-1, marginInt[3]-2, width+4, height+2, + false, tui.MakeBorderStyle(t.borderShape, t.unicode)) + } + + // Add padding + for idx, val := range paddingInt { + marginInt[idx] += val + } + width = screenWidth - marginInt[1] - marginInt[3] + height = screenHeight - marginInt[0] - marginInt[2] + + noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode) + if previewVisible { + createPreviewWindow := func(y int, x int, w int, h int) { + pwidth := w + pheight := h + var previewBorder tui.BorderStyle + if t.previewOpts.border == tui.BorderNone { + previewBorder = tui.MakeTransparentBorder() + } else { + previewBorder = tui.MakeBorderStyle(t.previewOpts.border, t.unicode) + } + t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder) + switch t.previewOpts.border { + case tui.BorderSharp, tui.BorderRounded: + pwidth -= 4 + pheight -= 2 + x += 2 + y += 1 + case tui.BorderLeft: + pwidth -= 2 + x += 2 + case tui.BorderRight: + pwidth -= 2 + case tui.BorderTop: + pheight -= 1 + y += 1 + case tui.BorderBottom: + pheight -= 1 + case tui.BorderHorizontal: + pheight -= 2 + y += 1 + case tui.BorderVertical: + pwidth -= 4 + x += 2 + } + t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, true, noBorder) + } + verticalPad := 2 + minPreviewHeight := 3 + switch t.previewOpts.border { + case tui.BorderNone, tui.BorderVertical, tui.BorderLeft, tui.BorderRight: + verticalPad = 0 + minPreviewHeight = 1 + case tui.BorderTop, tui.BorderBottom: + verticalPad = 1 + minPreviewHeight = 2 + } + switch t.previewOpts.position { + case posUp: + pheight := calculateSize(height, t.previewOpts.size, minHeight, minPreviewHeight, verticalPad) + t.window = t.tui.NewWindow( + marginInt[0]+pheight, marginInt[3], width, height-pheight, false, noBorder) + createPreviewWindow(marginInt[0], marginInt[3], width, pheight) + case posDown: + pheight := calculateSize(height, t.previewOpts.size, minHeight, minPreviewHeight, verticalPad) + t.window = t.tui.NewWindow( + marginInt[0], marginInt[3], width, height-pheight, false, noBorder) + createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight) + case posLeft: + pwidth := calculateSize(width, t.previewOpts.size, minWidth, 5, 4) + t.window = t.tui.NewWindow( + marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false, noBorder) + createPreviewWindow(marginInt[0], marginInt[3], pwidth, height) + case posRight: + pwidth := calculateSize(width, t.previewOpts.size, minWidth, 5, 4) + t.window = t.tui.NewWindow( + marginInt[0], marginInt[3], width-pwidth, height, false, noBorder) + createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height) + } + } else { + t.window = t.tui.NewWindow( + marginInt[0], + marginInt[3], + width, + height, false, noBorder) + } + for i := 0; i < t.window.Height(); i++ { + t.window.MoveAndClear(i, 0) + } +} + +func (t *Terminal) move(y int, x int, clear bool) { + h := t.window.Height() + + switch t.layout { + case layoutDefault: + y = h - y - 1 + case layoutReverseList: + n := 2 + len(t.header) + if t.noInfoLine() { + n-- + } + if y < n { + y = h - y - 1 + } else { + y -= n + } + } + + if clear { + t.window.MoveAndClear(y, x) + } else { + t.window.Move(y, x) + } +} + +func (t *Terminal) truncateQuery() { + t.input, _ = t.trimRight(t.input, maxPatternLength) + t.cx = util.Constrain(t.cx, 0, len(t.input)) +} + +func (t *Terminal) updatePromptOffset() ([]rune, []rune) { + maxWidth := util.Max(1, t.window.Width()-t.promptLen-1) + + _, overflow := t.trimLeft(t.input[:t.cx], maxWidth) + minOffset := int(overflow) + maxOffset := util.Min(util.Min(len(t.input), minOffset+maxWidth), t.cx) + + t.xoffset = util.Constrain(t.xoffset, minOffset, maxOffset) + before, _ := t.trimLeft(t.input[t.xoffset:t.cx], maxWidth) + beforeLen := t.displayWidth(before) + after, _ := t.trimRight(t.input[t.cx:], maxWidth-beforeLen) + afterLen := t.displayWidth(after) + t.queryLen = [2]int{beforeLen, afterLen} + return before, after +} + +func (t *Terminal) promptLine() int { + if t.headerFirst { + max := t.window.Height() - 1 + if !t.noInfoLine() { + max-- + } + return util.Min(len(t.header0)+t.headerLines, max) + } + return 0 +} + +func (t *Terminal) placeCursor() { + t.move(t.promptLine(), t.promptLen+t.queryLen[0], false) +} + +func (t *Terminal) printPrompt() { + t.move(t.promptLine(), 0, true) + t.prompt() + + before, after := t.updatePromptOffset() + color := tui.ColInput + if t.paused { + color = tui.ColDisabled + } + t.window.CPrint(color, string(before)) + t.window.CPrint(color, string(after)) +} + +func (t *Terminal) trimMessage(message string, maxWidth int) string { + if len(message) <= maxWidth { + return message + } + runes, _ := t.trimRight([]rune(message), maxWidth-2) + return string(runes) + strings.Repeat(".", util.Constrain(maxWidth, 0, 2)) +} + +func (t *Terminal) printInfo() { + pos := 0 + line := t.promptLine() + switch t.infoStyle { + case infoDefault: + t.move(line+1, 0, true) + if t.reading { + duration := int64(spinnerDuration) + idx := (time.Now().UnixNano() % (duration * int64(len(t.spinner)))) / duration + t.window.CPrint(tui.ColSpinner, t.spinner[idx]) + } + t.move(line+1, 2, false) + pos = 2 + case infoInline: + pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1 + if pos+len(" < ") > t.window.Width() { + return + } + t.move(line, pos, true) + if t.reading { + t.window.CPrint(tui.ColSpinner, " < ") + } else { + t.window.CPrint(tui.ColPrompt, " < ") + } + pos += len(" < ") + case infoHidden: + return + } + + found := t.merger.Length() + total := util.Max(found, t.count) + output := fmt.Sprintf("%d/%d", found, total) + if t.toggleSort { + if t.sort { + output += " +S" + } else { + output += " -S" + } + } + if t.multi > 0 { + if t.multi == maxMulti { + output += fmt.Sprintf(" (%d)", len(t.selected)) + } else { + output += fmt.Sprintf(" (%d/%d)", len(t.selected), t.multi) + } + } + if t.progress > 0 && t.progress < 100 { + output += fmt.Sprintf(" (%d%%)", t.progress) + } + if t.failed != nil && t.count == 0 { + output = fmt.Sprintf("[Command failed: %s]", *t.failed) + } + output = t.trimMessage(output, t.window.Width()-pos) + t.window.CPrint(tui.ColInfo, output) +} + +func (t *Terminal) printHeader() { + if len(t.header) == 0 { + return + } + max := t.window.Height() + if t.headerFirst { + max-- + if !t.noInfoLine() { + max-- + } + } + var state *ansiState + for idx, lineStr := range t.header { + line := idx + if !t.headerFirst { + line++ + if !t.noInfoLine() { + line++ + } + } + if line >= max { + continue + } + trimmed, colors, newState := extractColor(lineStr, state, nil) + state = newState + item := &Item{ + text: util.ToChars([]byte(trimmed)), + colors: colors} + + t.move(line, 2, true) + t.printHighlighted(Result{item: item}, + tui.ColHeader, tui.ColHeader, false, false) + } +} + +func (t *Terminal) printList() { + t.constrain() + + maxy := t.maxItems() + count := t.merger.Length() - t.offset + for j := 0; j < maxy; j++ { + i := j + if t.layout == layoutDefault { + i = maxy - 1 - j + } + line := i + 2 + len(t.header) + if t.noInfoLine() { + line-- + } + if i < count { + t.printItem(t.merger.Get(i+t.offset), line, i, i == t.cy-t.offset) + } else if t.prevLines[i] != emptyLine { + t.prevLines[i] = emptyLine + t.move(line, 0, true) + } + } +} + +func (t *Terminal) printItem(result Result, line int, i int, current bool) { + item := result.item + _, selected := t.selected[item.Index()] + label := "" + if t.jumping != jumpDisabled { + if i < len(t.jumpLabels) { + // Striped + current = i%2 == 0 + label = t.jumpLabels[i:i+1] + strings.Repeat(" ", t.pointerLen-1) + } + } else if current { + label = t.pointer + } + + // Avoid unnecessary redraw + newLine := itemLine{current: current, selected: selected, label: label, + result: result, queryLen: len(t.input), width: 0} + prevLine := t.prevLines[i] + if prevLine.current == newLine.current && + prevLine.selected == newLine.selected && + prevLine.label == newLine.label && + prevLine.queryLen == newLine.queryLen && + prevLine.result == newLine.result { + return + } + + t.move(line, 0, false) + if current { + if len(label) == 0 { + t.window.CPrint(tui.ColCurrentCursorEmpty, t.pointerEmpty) + } else { + t.window.CPrint(tui.ColCurrentCursor, label) + } + if selected { + t.window.CPrint(tui.ColCurrentSelected, t.marker) + } else { + t.window.CPrint(tui.ColCurrentSelectedEmpty, t.markerEmpty) + } + newLine.width = t.printHighlighted(result, tui.ColCurrent, tui.ColCurrentMatch, true, true) + } else { + if len(label) == 0 { + t.window.CPrint(tui.ColCursorEmpty, t.pointerEmpty) + } else { + t.window.CPrint(tui.ColCursor, label) + } + if selected { + t.window.CPrint(tui.ColSelected, t.marker) + } else { + t.window.Print(t.markerEmpty) + } + newLine.width = t.printHighlighted(result, tui.ColNormal, tui.ColMatch, false, true) + } + fillSpaces := prevLine.width - newLine.width + if fillSpaces > 0 { + t.window.Print(strings.Repeat(" ", fillSpaces)) + } + t.prevLines[i] = newLine +} + +func (t *Terminal) trimRight(runes []rune, width int) ([]rune, bool) { + // We start from the beginning to handle tab characters + _, overflowIdx := util.RunesWidth(runes, 0, t.tabstop, width) + if overflowIdx >= 0 { + return runes[:overflowIdx], true + } + return runes, false +} + +func (t *Terminal) displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int { + width, _ := util.RunesWidth(runes, prefixWidth, t.tabstop, limit) + return width +} + +func (t *Terminal) trimLeft(runes []rune, width int) ([]rune, int32) { + width = util.Max(0, width) + var trimmed int32 + // Assume that each rune takes at least one column on screen + if len(runes) > width+2 { + diff := len(runes) - width - 2 + trimmed = int32(diff) + runes = runes[diff:] + } + + currentWidth := t.displayWidth(runes) + + for currentWidth > width && len(runes) > 0 { + runes = runes[1:] + trimmed++ + currentWidth = t.displayWidthWithLimit(runes, 2, width) + } + return runes, trimmed +} + +func (t *Terminal) overflow(runes []rune, max int) bool { + return t.displayWidthWithLimit(runes, 0, max) > max +} + +func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMatch tui.ColorPair, current bool, match bool) int { + item := result.item + + // Overflow + text := make([]rune, item.text.Length()) + copy(text, item.text.ToRunes()) + matchOffsets := []Offset{} + var pos *[]int + if match && t.merger.pattern != nil { + _, matchOffsets, pos = t.merger.pattern.MatchItem(item, true, t.slab) + } + charOffsets := matchOffsets + if pos != nil { + charOffsets = make([]Offset, len(*pos)) + for idx, p := range *pos { + offset := Offset{int32(p), int32(p + 1)} + charOffsets[idx] = offset + } + sort.Sort(ByOrder(charOffsets)) + } + var maxe int + for _, offset := range charOffsets { + maxe = util.Max(maxe, int(offset[1])) + } + + offsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current) + maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1) + maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) + displayWidth := t.displayWidthWithLimit(text, 0, maxWidth) + if displayWidth > maxWidth { + transformOffsets := func(diff int32) { + for idx, offset := range offsets { + b, e := offset.offset[0], offset.offset[1] + b += 2 - diff + e += 2 - diff + b = util.Max32(b, 2) + offsets[idx].offset[0] = b + offsets[idx].offset[1] = util.Max32(b, e) + } + } + if t.hscroll { + if t.keepRight && pos == nil { + trimmed, diff := t.trimLeft(text, maxWidth-2) + transformOffsets(diff) + text = append([]rune(ellipsis), trimmed...) + } else if !t.overflow(text[:maxe], maxWidth-2) { + // Stri.. + text, _ = t.trimRight(text, maxWidth-2) + text = append(text, []rune(ellipsis)...) + } else { + // Stri.. + if t.overflow(text[maxe:], 2) { + text = append(text[:maxe], []rune(ellipsis)...) + } + // ..ri.. + var diff int32 + text, diff = t.trimLeft(text, maxWidth-2) + + // Transform offsets + transformOffsets(diff) + text = append([]rune(ellipsis), text...) + } + } else { + text, _ = t.trimRight(text, maxWidth-2) + text = append(text, []rune(ellipsis)...) + + for idx, offset := range offsets { + offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-2)) + offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth)) + } + } + displayWidth = t.displayWidthWithLimit(text, 0, displayWidth) + } + + var index int32 + var substr string + var prefixWidth int + maxOffset := int32(len(text)) + for _, offset := range offsets { + b := util.Constrain32(offset.offset[0], index, maxOffset) + e := util.Constrain32(offset.offset[1], index, maxOffset) + + substr, prefixWidth = t.processTabs(text[index:b], prefixWidth) + t.window.CPrint(colBase, substr) + + if b < e { + substr, prefixWidth = t.processTabs(text[b:e], prefixWidth) + t.window.CPrint(offset.color, substr) + } + + index = e + if index >= maxOffset { + break + } + } + if index < maxOffset { + substr, _ = t.processTabs(text[index:], prefixWidth) + t.window.CPrint(colBase, substr) + } + return displayWidth +} + +func (t *Terminal) renderPreviewSpinner() { + numLines := len(t.previewer.lines) + spin := t.previewer.spinner + if len(spin) > 0 || t.previewer.scrollable { + maxWidth := t.pwindow.Width() + if !t.previewer.scrollable { + if maxWidth > 0 { + t.pwindow.Move(0, maxWidth-1) + t.pwindow.CPrint(tui.ColSpinner, spin) + } + } else { + offsetString := fmt.Sprintf("%d/%d", t.previewer.offset+1, numLines) + if len(spin) > 0 { + spin += " " + maxWidth -= 2 + } + offsetRunes, _ := t.trimRight([]rune(offsetString), maxWidth) + pos := maxWidth - t.displayWidth(offsetRunes) + t.pwindow.Move(0, pos) + if maxWidth > 0 { + t.pwindow.CPrint(tui.ColSpinner, spin) + t.pwindow.CPrint(tui.ColInfo.WithAttr(tui.Reverse), string(offsetRunes)) + } + } + } +} + +func (t *Terminal) renderPreviewArea(unchanged bool) { + if unchanged { + t.pwindow.MoveAndClear(0, 0) // Clear scroll offset display + } else { + t.previewed.filled = false + t.pwindow.Erase() + } + + height := t.pwindow.Height() + header := []string{} + body := t.previewer.lines + headerLines := t.previewOpts.headerLines + // Do not enable preview header lines if it's value is too large + if headerLines > 0 && headerLines < util.Min(len(body), height) { + header = t.previewer.lines[0:headerLines] + body = t.previewer.lines[headerLines:] + // Always redraw header + t.renderPreviewText(height, header, 0, false) + t.pwindow.MoveAndClear(t.pwindow.Y(), 0) + } + t.renderPreviewText(height, body, -t.previewer.offset+headerLines, unchanged) + + if !unchanged { + t.pwindow.FinishFill() + } +} + +func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unchanged bool) { + maxWidth := t.pwindow.Width() + var ansi *ansiState + for _, line := range lines { + var lbg tui.Color = -1 + if ansi != nil { + ansi.lbg = -1 + } + line = strings.TrimSuffix(line, "\n") + if lineNo >= height || t.pwindow.Y() == height-1 && t.pwindow.X() > 0 { + t.previewed.filled = true + break + } else if lineNo >= 0 { + var fillRet tui.FillReturn + prefixWidth := 0 + _, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool { + trimmed := []rune(str) + isTrimmed := false + if !t.previewOpts.wrap { + trimmed, isTrimmed = t.trimRight(trimmed, maxWidth-t.pwindow.X()) + } + str, width := t.processTabs(trimmed, prefixWidth) + prefixWidth += width + if t.theme.Colored && ansi != nil && ansi.colored() { + lbg = ansi.lbg + fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str) + } else { + fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str) + } + return !isTrimmed && + (fillRet == tui.FillContinue || t.previewOpts.wrap && fillRet == tui.FillNextLine) + }) + t.previewer.scrollable = t.previewer.scrollable || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width() + if fillRet == tui.FillNextLine { + continue + } else if fillRet == tui.FillSuspend { + t.previewed.filled = true + break + } + if unchanged && lineNo == 0 { + break + } + if lbg >= 0 { + t.pwindow.CFill(-1, lbg, tui.AttrRegular, + strings.Repeat(" ", t.pwindow.Width()-t.pwindow.X())+"\n") + } else { + t.pwindow.Fill("\n") + } + } + lineNo++ + } +} + +func (t *Terminal) printPreview() { + if !t.hasPreviewWindow() { + return + } + numLines := len(t.previewer.lines) + height := t.pwindow.Height() + unchanged := (t.previewed.filled || numLines == t.previewed.numLines) && + t.previewer.version == t.previewed.version && + t.previewer.offset == t.previewed.offset + t.previewer.scrollable = t.previewer.offset > 0 || numLines > height + t.renderPreviewArea(unchanged) + t.renderPreviewSpinner() + t.previewed.numLines = numLines + t.previewed.version = t.previewer.version + t.previewed.offset = t.previewer.offset +} + +func (t *Terminal) printPreviewDelayed() { + if !t.hasPreviewWindow() || len(t.previewer.lines) > 0 && t.previewed.version == t.previewer.version { + return + } + + t.previewer.scrollable = false + t.renderPreviewArea(true) + + message := t.trimMessage("Loading ..", t.pwindow.Width()) + pos := t.pwindow.Width() - len(message) + t.pwindow.Move(0, pos) + t.pwindow.CPrint(tui.ColInfo.WithAttr(tui.Reverse), message) +} + +func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) { + var strbuf strings.Builder + l := prefixWidth + gr := uniseg.NewGraphemes(string(runes)) + for gr.Next() { + rs := gr.Runes() + str := string(rs) + var w int + if len(rs) == 1 && rs[0] == '\t' { + w = t.tabstop - l%t.tabstop + strbuf.WriteString(strings.Repeat(" ", w)) + } else { + w = runewidth.StringWidth(str) + strbuf.WriteString(str) + } + l += w + } + return strbuf.String(), l +} + +func (t *Terminal) printAll() { + t.resizeWindows() + t.printList() + t.printPrompt() + t.printInfo() + t.printHeader() + t.printPreview() +} + +func (t *Terminal) refresh() { + t.placeCursor() + if !t.suppress { + windows := make([]tui.Window, 0, 4) + if t.borderShape != tui.BorderNone { + windows = append(windows, t.border) + } + if t.hasPreviewWindow() { + if t.pborder != nil { + windows = append(windows, t.pborder) + } + windows = append(windows, t.pwindow) + } + windows = append(windows, t.window) + t.tui.RefreshWindows(windows) + } +} + +func (t *Terminal) delChar() bool { + if len(t.input) > 0 && t.cx < len(t.input) { + t.input = append(t.input[:t.cx], t.input[t.cx+1:]...) + return true + } + return false +} + +func findLastMatch(pattern string, str string) int { + rx, err := regexp.Compile(pattern) + if err != nil { + return -1 + } + locs := rx.FindAllStringIndex(str, -1) + if locs == nil { + return -1 + } + prefix := []rune(str[:locs[len(locs)-1][0]]) + return len(prefix) +} + +func findFirstMatch(pattern string, str string) int { + rx, err := regexp.Compile(pattern) + if err != nil { + return -1 + } + loc := rx.FindStringIndex(str) + if loc == nil { + return -1 + } + prefix := []rune(str[:loc[0]]) + return len(prefix) +} + +func copySlice(slice []rune) []rune { + ret := make([]rune, len(slice)) + copy(ret, slice) + return ret +} + +func (t *Terminal) rubout(pattern string) { + pcx := t.cx + after := t.input[t.cx:] + t.cx = findLastMatch(pattern, string(t.input[:t.cx])) + 1 + t.yanked = copySlice(t.input[t.cx:pcx]) + t.input = append(t.input[:t.cx], after...) +} + +func keyMatch(key tui.Event, event tui.Event) bool { + return event.Type == key.Type && event.Char == key.Char || + key.Type == tui.DoubleClick && event.Type == tui.Mouse && event.MouseEvent.Double +} + +func parsePlaceholder(match string) (bool, string, placeholderFlags) { + flags := placeholderFlags{} + + if match[0] == '\\' { + // Escaped placeholder pattern + return true, match[1:], flags + } + + skipChars := 1 + for _, char := range match[1:] { + switch char { + case '+': + flags.plus = true + skipChars++ + case 's': + flags.preserveSpace = true + skipChars++ + case 'n': + flags.number = true + skipChars++ + case 'f': + flags.file = true + skipChars++ + case 'q': + flags.query = true + // query flag is not skipped + default: + break + } + } + + matchWithoutFlags := "{" + match[skipChars:] + + return false, matchWithoutFlags, flags +} + +func hasPreviewFlags(template string) (slot bool, plus bool, query bool) { + for _, match := range placeholder.FindAllString(template, -1) { + _, _, flags := parsePlaceholder(match) + if flags.plus { + plus = true + } + if flags.query { + query = true + } + slot = true + } + return +} + +func writeTemporaryFile(data []string, printSep string) string { + f, err := ioutil.TempFile("", "fzf-preview-*") + if err != nil { + errorExit("Unable to create temporary file") + } + defer f.Close() + + f.WriteString(strings.Join(data, printSep)) + f.WriteString(printSep) + activeTempFiles = append(activeTempFiles, f.Name()) + return f.Name() +} + +func cleanTemporaryFiles() { + for _, filename := range activeTempFiles { + os.Remove(filename) + } + activeTempFiles = []string{} +} + +func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) string { + return replacePlaceholder( + template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list) +} + +func (t *Terminal) evaluateScrollOffset() int { + if t.pwindow == nil { + return 0 + } + + // We only need the current item to calculate the scroll offset + offsetExpr := offsetTrimCharsRegex.ReplaceAllString( + t.replacePlaceholder(t.previewOpts.scroll, false, "", []*Item{t.currentItem(), nil}), "") + + atoi := func(s string) int { + n, e := strconv.Atoi(s) + if e != nil { + return 0 + } + return n + } + + base := -1 + height := util.Max(0, t.pwindow.Height()-t.previewOpts.headerLines) + for _, component := range offsetComponentRegex.FindAllString(offsetExpr, -1) { + if strings.HasPrefix(component, "-/") { + component = component[1:] + } + if component[0] == '/' { + denom := atoi(component[1:]) + if denom != 0 { + base -= height / denom + } + break + } + base += atoi(component) + } + return util.Max(0, base) +} + +func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string { + current := allItems[:1] + selected := allItems[1:] + if current[0] == nil { + current = []*Item{} + } + if selected[0] == nil { + selected = []*Item{} + } + + // replace placeholders one by one + return placeholder.ReplaceAllStringFunc(template, func(match string) string { + escaped, match, flags := parsePlaceholder(match) + + // this function implements the effects a placeholder has on items + var replace func(*Item) string + + // placeholder types (escaped, query type, item type, token type) + switch { + case escaped: + return match + case match == "{q}": + return quoteEntry(query) + case match == "{}": + replace = func(item *Item) string { + switch { + case flags.number: + n := int(item.text.Index) + if n < 0 { + return "" + } + return strconv.Itoa(n) + case flags.file: + return item.AsString(stripAnsi) + default: + return quoteEntry(item.AsString(stripAnsi)) + } + } + default: + // token type and also failover (below) + rangeExpressions := strings.Split(match[1:len(match)-1], ",") + ranges := make([]Range, len(rangeExpressions)) + for idx, s := range rangeExpressions { + r, ok := ParseRange(&s) // ellipsis (x..y) and shorthand (x..x) range syntax + if !ok { + // Invalid expression, just return the original string in the template + return match + } + ranges[idx] = r + } + + replace = func(item *Item) string { + tokens := Tokenize(item.AsString(stripAnsi), delimiter) + trans := Transform(tokens, ranges) + str := joinTokens(trans) + + // trim the last delimiter + if delimiter.str != nil { + str = strings.TrimSuffix(str, *delimiter.str) + } else if delimiter.regex != nil { + delims := delimiter.regex.FindAllStringIndex(str, -1) + // make sure the delimiter is at the very end of the string + if len(delims) > 0 && delims[len(delims)-1][1] == len(str) { + str = str[:delims[len(delims)-1][0]] + } + } + + if !flags.preserveSpace { + str = strings.TrimSpace(str) + } + if !flags.file { + str = quoteEntry(str) + } + return str + } + } + + // apply 'replace' function over proper set of items and return result + + items := current + if flags.plus || forcePlus { + items = selected + } + replacements := make([]string, len(items)) + + for idx, item := range items { + replacements[idx] = replace(item) + } + + if flags.file { + return writeTemporaryFile(replacements, printsep) + } + return strings.Join(replacements, " ") + }) +} + +func (t *Terminal) redraw(clear bool) { + if clear { + t.tui.Clear() + } + t.tui.Refresh() + t.printAll() +} + +func (t *Terminal) executeCommand(template string, forcePlus bool, background bool) { + valid, list := t.buildPlusList(template, forcePlus) + if !valid { + return + } + command := t.replacePlaceholder(template, forcePlus, string(t.input), list) + cmd := util.ExecCommand(command, false) + t.executing.Set(true) + if !background { + cmd.Stdin = tui.TtyIn() + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + t.tui.Pause(true) + cmd.Run() + t.tui.Resume(true, false) + t.redraw(true) + t.refresh() + } else { + t.tui.Pause(false) + cmd.Run() + t.tui.Resume(false, false) + } + t.executing.Set(false) + cleanTemporaryFiles() +} + +func (t *Terminal) hasPreviewer() bool { + return t.previewBox != nil +} + +func (t *Terminal) isPreviewEnabled() bool { + return t.hasPreviewer() && t.previewer.enabled +} + +func (t *Terminal) hasPreviewWindow() bool { + return t.pwindow != nil && t.isPreviewEnabled() +} + +func (t *Terminal) currentItem() *Item { + cnt := t.merger.Length() + if t.cy >= 0 && cnt > 0 && cnt > t.cy { + return t.merger.Get(t.cy).item + } + return nil +} + +func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item) { + current := t.currentItem() + slot, plus, query := hasPreviewFlags(template) + if !(!slot || query && len(t.input) > 0 || (forcePlus || plus) && len(t.selected) > 0) { + return current != nil, []*Item{current, current} + } + + // We would still want to update preview window even if there is no match if + // 1. command template contains {q} and the query string is not empty + // 2. or it contains {+} and we have more than one item already selected. + // To do so, we pass an empty Item instead of nil to trigger an update. + if current == nil { + current = &minItem + } + + var sels []*Item + if len(t.selected) == 0 { + sels = []*Item{current, current} + } else { + sels = make([]*Item, len(t.selected)+1) + sels[0] = current + for i, sel := range t.sortSelected() { + sels[i+1] = sel.item + } + } + return true, sels +} + +func (t *Terminal) selectItem(item *Item) bool { + if len(t.selected) >= t.multi { + return false + } + if _, found := t.selected[item.Index()]; found { + return true + } + + t.selected[item.Index()] = selectedItem{time.Now(), item} + t.version++ + + return true +} + +func (t *Terminal) selectItemChanged(item *Item) bool { + if _, found := t.selected[item.Index()]; found { + return false + } + return t.selectItem(item) +} + +func (t *Terminal) deselectItem(item *Item) { + delete(t.selected, item.Index()) + t.version++ +} + +func (t *Terminal) deselectItemChanged(item *Item) bool { + if _, found := t.selected[item.Index()]; found { + t.deselectItem(item) + return true + } + return false +} + +func (t *Terminal) toggleItem(item *Item) bool { + if _, found := t.selected[item.Index()]; !found { + return t.selectItem(item) + } + t.deselectItem(item) + return true +} + +func (t *Terminal) killPreview(code int) { + select { + case t.killChan <- code: + default: + if code != exitCancel { + t.eventBox.Set(EvtQuit, code) + } + } +} + +func (t *Terminal) cancelPreview() { + t.killPreview(exitCancel) +} + +// Loop is called to start Terminal I/O +func (t *Terminal) Loop() { + // prof := profile.Start(profile.ProfilePath("/tmp/")) + <-t.startChan + { // Late initialization + intChan := make(chan os.Signal, 1) + signal.Notify(intChan, os.Interrupt, syscall.SIGTERM) + go func() { + for s := range intChan { + // Don't quit by SIGINT while executing because it should be for the executing command and not for fzf itself + if !(s == os.Interrupt && t.executing.Get()) { + t.reqBox.Set(reqQuit, nil) + } + } + }() + + contChan := make(chan os.Signal, 1) + notifyOnCont(contChan) + go func() { + for { + <-contChan + t.reqBox.Set(reqReinit, nil) + } + }() + + resizeChan := make(chan os.Signal, 1) + notifyOnResize(resizeChan) // Non-portable + go func() { + for { + <-resizeChan + t.reqBox.Set(reqFullRedraw, nil) + } + }() + + t.mutex.Lock() + t.initFunc() + t.resizeWindows() + t.printPrompt() + t.printInfo() + t.printHeader() + t.refresh() + t.mutex.Unlock() + go func() { + timer := time.NewTimer(t.initDelay) + <-timer.C + t.reqBox.Set(reqRefresh, nil) + }() + + // Keep the spinner spinning + go func() { + for { + t.mutex.Lock() + reading := t.reading + t.mutex.Unlock() + time.Sleep(spinnerDuration) + if reading { + t.reqBox.Set(reqInfo, nil) + } + } + }() + } + + if t.hasPreviewer() { + go func() { + var version int64 + for { + var items []*Item + var commandTemplate string + var pwindow tui.Window + initialOffset := 0 + t.previewBox.Wait(func(events *util.Events) { + for req, value := range *events { + switch req { + case reqPreviewEnqueue: + request := value.(previewRequest) + commandTemplate = request.template + initialOffset = request.scrollOffset + items = request.list + pwindow = request.pwindow + } + } + events.Clear() + }) + version++ + // We don't display preview window if no match + if items[0] != nil { + _, query := t.Input() + command := t.replacePlaceholder(commandTemplate, false, string(query), items) + cmd := util.ExecCommand(command, true) + if pwindow != nil { + height := pwindow.Height() + env := os.Environ() + lines := fmt.Sprintf("LINES=%d", height) + columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width()) + env = append(env, lines) + env = append(env, "FZF_PREVIEW_"+lines) + env = append(env, columns) + env = append(env, "FZF_PREVIEW_"+columns) + cmd.Env = env + } + + out, _ := cmd.StdoutPipe() + cmd.Stderr = cmd.Stdout + reader := bufio.NewReader(out) + eofChan := make(chan bool) + finishChan := make(chan bool, 1) + err := cmd.Start() + if err == nil { + reapChan := make(chan bool) + lineChan := make(chan eachLine) + // Goroutine 1 reads process output + go func() { + for { + line, err := reader.ReadString('\n') + lineChan <- eachLine{line, err} + if err != nil { + break + } + } + eofChan <- true + }() + + // Goroutine 2 periodically requests rendering + rendered := util.NewAtomicBool(false) + go func(version int64) { + lines := []string{} + spinner := makeSpinner(t.unicode) + spinnerIndex := -1 // Delay initial rendering by an extra tick + ticker := time.NewTicker(previewChunkDelay) + offset := initialOffset + Loop: + for { + select { + case <-ticker.C: + if len(lines) > 0 && len(lines) >= initialOffset { + if spinnerIndex >= 0 { + spin := spinner[spinnerIndex%len(spinner)] + t.reqBox.Set(reqPreviewDisplay, previewResult{version, lines, offset, spin}) + rendered.Set(true) + offset = -1 + } + spinnerIndex++ + } + case eachLine := <-lineChan: + line := eachLine.line + err := eachLine.err + if len(line) > 0 { + clearIndex := strings.Index(line, clearCode) + if clearIndex >= 0 { + lines = []string{} + line = line[clearIndex+len(clearCode):] + version-- + offset = 0 + } + lines = append(lines, line) + } + if err != nil { + t.reqBox.Set(reqPreviewDisplay, previewResult{version, lines, offset, ""}) + rendered.Set(true) + break Loop + } + } + } + ticker.Stop() + reapChan <- true + }(version) + + // Goroutine 3 is responsible for cancelling running preview command + go func(version int64) { + timer := time.NewTimer(previewDelayed) + Loop: + for { + select { + case <-timer.C: + t.reqBox.Set(reqPreviewDelayed, version) + case code := <-t.killChan: + if code != exitCancel { + util.KillCommand(cmd) + t.eventBox.Set(EvtQuit, code) + } else { + // We can immediately kill a long-running preview program + // once we started rendering its partial output + delay := previewCancelWait + if rendered.Get() { + delay = 0 + } + timer := time.NewTimer(delay) + select { + case <-timer.C: + util.KillCommand(cmd) + case <-finishChan: + } + timer.Stop() + } + break Loop + case <-finishChan: + break Loop + } + } + timer.Stop() + reapChan <- true + }(version) + + <-eofChan // Goroutine 1 finished + cmd.Wait() // NOTE: We should not call Wait before EOF + finishChan <- true // Tell Goroutine 3 to stop + <-reapChan // Goroutine 2 and 3 finished + <-reapChan + } else { + // Failed to start the command. Report the error immediately. + t.reqBox.Set(reqPreviewDisplay, previewResult{version, []string{err.Error()}, 0, ""}) + } + + cleanTemporaryFiles() + } else { + t.reqBox.Set(reqPreviewDisplay, previewResult{version, nil, 0, ""}) + } + } + }() + } + + refreshPreview := func(command string) { + if len(command) > 0 && t.isPreviewEnabled() { + _, list := t.buildPlusList(command, false) + t.cancelPreview() + t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.evaluateScrollOffset(), list}) + } + } + + go func() { + var focusedIndex int32 = minItem.Index() + var version int64 = -1 + running := true + code := exitError + exit := func(getCode func() int) { + t.tui.Close() + code = getCode() + if code <= exitNoMatch && t.history != nil { + t.history.append(string(t.input)) + } + running = false + t.mutex.Unlock() + } + + for running { + t.reqBox.Wait(func(events *util.Events) { + defer events.Clear() + t.mutex.Lock() + for req, value := range *events { + switch req { + case reqPrompt: + t.printPrompt() + if t.noInfoLine() { + t.printInfo() + } + case reqInfo: + t.printInfo() + case reqList: + t.printList() + var currentIndex int32 = minItem.Index() + currentItem := t.currentItem() + if currentItem != nil { + currentIndex = currentItem.Index() + } + if focusedIndex != currentIndex || version != t.version { + version = t.version + focusedIndex = currentIndex + refreshPreview(t.previewOpts.command) + } + case reqJump: + if t.merger.Length() == 0 { + t.jumping = jumpDisabled + } + t.printList() + case reqHeader: + t.printHeader() + case reqRefresh: + t.suppress = false + case reqReinit: + t.tui.Resume(t.fullscreen, t.sigstop) + t.redraw(true) + case reqRedraw: + t.redraw(false) + case reqFullRedraw: + t.redraw(true) + case reqClose: + exit(func() int { + if t.output() { + return exitOk + } + return exitNoMatch + }) + return + case reqPreviewDisplay: + result := value.(previewResult) + if t.previewer.version != result.version { + t.previewer.version = result.version + t.previewer.following = t.previewOpts.follow + } + t.previewer.lines = result.lines + t.previewer.spinner = result.spinner + if t.previewer.following { + t.previewer.offset = len(t.previewer.lines) - t.pwindow.Height() + } else if result.offset >= 0 { + t.previewer.offset = util.Constrain(result.offset, t.previewOpts.headerLines, len(t.previewer.lines)-1) + } + t.printPreview() + case reqPreviewRefresh: + t.printPreview() + case reqPreviewDelayed: + t.previewer.version = value.(int64) + t.printPreviewDelayed() + case reqPrintQuery: + exit(func() int { + t.printer(string(t.input)) + return exitOk + }) + return + case reqQuit: + exit(func() int { return exitInterrupt }) + return + } + } + t.refresh() + t.mutex.Unlock() + }) + } + // prof.Stop() + t.killPreview(code) + }() + + looping := true + for looping { + var newCommand *string + changed := false + beof := false + queryChanged := false + + event := t.tui.GetChar() + + t.mutex.Lock() + previousInput := t.input + previousCx := t.cx + events := []util.EventType{} + req := func(evts ...util.EventType) { + for _, event := range evts { + events = append(events, event) + if event == reqClose || event == reqQuit { + looping = false + } + } + } + togglePreview := func(enabled bool) bool { + if t.previewer.enabled != enabled { + t.previewer.enabled = enabled + // We need to immediately update t.pwindow so we don't use reqRedraw + t.resizeWindows() + req(reqPrompt, reqList, reqInfo, reqHeader) + return true + } + return false + } + toggle := func() bool { + current := t.currentItem() + if current != nil && t.toggleItem(current) { + req(reqInfo) + return true + } + return false + } + scrollPreviewTo := func(newOffset int) { + if !t.previewer.scrollable { + return + } + t.previewer.following = false + numLines := len(t.previewer.lines) + if t.previewOpts.cycle { + newOffset = (newOffset + numLines) % numLines + } + newOffset = util.Constrain(newOffset, t.previewOpts.headerLines, numLines-1) + if t.previewer.offset != newOffset { + t.previewer.offset = newOffset + req(reqPreviewRefresh) + } + } + scrollPreviewBy := func(amount int) { + scrollPreviewTo(t.previewer.offset + amount) + } + for key, ret := range t.expect { + if keyMatch(key, event) { + t.pressed = ret + t.reqBox.Set(reqClose, nil) + t.mutex.Unlock() + return + } + } + + actionsFor := func(eventType tui.EventType) []*action { + return t.keymap[eventType.AsEvent()] + } + + var doAction func(*action) bool + doActions := func(actions []*action) bool { + for _, action := range actions { + if !doAction(action) { + return false + } + } + return true + } + doAction = func(a *action) bool { + switch a.t { + case actIgnore: + case actExecute, actExecuteSilent: + t.executeCommand(a.a, false, a.t == actExecuteSilent) + case actExecuteMulti: + t.executeCommand(a.a, true, false) + case actInvalid: + t.mutex.Unlock() + return false + case actTogglePreview: + if t.hasPreviewer() { + togglePreview(!t.previewer.enabled) + if t.previewer.enabled { + valid, list := t.buildPlusList(t.previewOpts.command, false) + if valid { + t.cancelPreview() + t.previewBox.Set(reqPreviewEnqueue, + previewRequest{t.previewOpts.command, t.pwindow, t.evaluateScrollOffset(), list}) + } + } + } + case actTogglePreviewWrap: + if t.hasPreviewWindow() { + t.previewOpts.wrap = !t.previewOpts.wrap + // Reset preview version so that full redraw occurs + t.previewed.version = 0 + req(reqPreviewRefresh) + } + case actToggleSort: + t.sort = !t.sort + changed = true + case actPreviewTop: + if t.hasPreviewWindow() { + scrollPreviewTo(0) + } + case actPreviewBottom: + if t.hasPreviewWindow() { + scrollPreviewTo(len(t.previewer.lines) - t.pwindow.Height()) + } + case actPreviewUp: + if t.hasPreviewWindow() { + scrollPreviewBy(-1) + } + case actPreviewDown: + if t.hasPreviewWindow() { + scrollPreviewBy(1) + } + case actPreviewPageUp: + if t.hasPreviewWindow() { + scrollPreviewBy(-t.pwindow.Height()) + } + case actPreviewPageDown: + if t.hasPreviewWindow() { + scrollPreviewBy(t.pwindow.Height()) + } + case actPreviewHalfPageUp: + if t.hasPreviewWindow() { + scrollPreviewBy(-t.pwindow.Height() / 2) + } + case actPreviewHalfPageDown: + if t.hasPreviewWindow() { + scrollPreviewBy(t.pwindow.Height() / 2) + } + case actBeginningOfLine: + t.cx = 0 + case actBackwardChar: + if t.cx > 0 { + t.cx-- + } + case actPrintQuery: + req(reqPrintQuery) + case actChangePrompt: + t.prompt, t.promptLen = t.parsePrompt(a.a) + req(reqPrompt) + case actPreview: + togglePreview(true) + refreshPreview(a.a) + case actRefreshPreview: + refreshPreview(t.previewOpts.command) + case actReplaceQuery: + current := t.currentItem() + if current != nil { + t.input = current.text.ToRunes() + t.cx = len(t.input) + } + case actAbort: + req(reqQuit) + case actDeleteChar: + t.delChar() + case actDeleteCharEOF: + if !t.delChar() && t.cx == 0 { + req(reqQuit) + } + case actEndOfLine: + t.cx = len(t.input) + case actCancel: + if len(t.input) == 0 { + req(reqQuit) + } else { + t.yanked = t.input + t.input = []rune{} + t.cx = 0 + } + case actBackwardDeleteCharEOF: + if len(t.input) == 0 { + req(reqQuit) + } else if t.cx > 0 { + t.input = append(t.input[:t.cx-1], t.input[t.cx:]...) + t.cx-- + } + case actForwardChar: + if t.cx < len(t.input) { + t.cx++ + } + case actBackwardDeleteChar: + beof = len(t.input) == 0 + if t.cx > 0 { + t.input = append(t.input[:t.cx-1], t.input[t.cx:]...) + t.cx-- + } + case actSelectAll: + if t.multi > 0 { + for i := 0; i < t.merger.Length(); i++ { + if !t.selectItem(t.merger.Get(i).item) { + break + } + } + req(reqList, reqInfo) + } + case actDeselectAll: + if t.multi > 0 { + for i := 0; i < t.merger.Length() && len(t.selected) > 0; i++ { + t.deselectItem(t.merger.Get(i).item) + } + req(reqList, reqInfo) + } + case actClose: + if t.isPreviewEnabled() { + togglePreview(false) + } else { + req(reqQuit) + } + case actSelect: + current := t.currentItem() + if t.multi > 0 && current != nil && t.selectItemChanged(current) { + req(reqList, reqInfo) + } + case actDeselect: + current := t.currentItem() + if t.multi > 0 && current != nil && t.deselectItemChanged(current) { + req(reqList, reqInfo) + } + case actToggle: + if t.multi > 0 && t.merger.Length() > 0 && toggle() { + req(reqList) + } + case actToggleAll: + if t.multi > 0 { + prevIndexes := make(map[int]struct{}) + for i := 0; i < t.merger.Length() && len(t.selected) > 0; i++ { + item := t.merger.Get(i).item + if _, found := t.selected[item.Index()]; found { + prevIndexes[i] = struct{}{} + t.deselectItem(item) + } + } + + for i := 0; i < t.merger.Length(); i++ { + if _, found := prevIndexes[i]; !found { + item := t.merger.Get(i).item + if !t.selectItem(item) { + break + } + } + } + req(reqList, reqInfo) + } + case actToggleIn: + if t.layout != layoutDefault { + return doAction(&action{t: actToggleUp}) + } + return doAction(&action{t: actToggleDown}) + case actToggleOut: + if t.layout != layoutDefault { + return doAction(&action{t: actToggleDown}) + } + return doAction(&action{t: actToggleUp}) + case actToggleDown: + if t.multi > 0 && t.merger.Length() > 0 && toggle() { + t.vmove(-1, true) + req(reqList) + } + case actToggleUp: + if t.multi > 0 && t.merger.Length() > 0 && toggle() { + t.vmove(1, true) + req(reqList) + } + case actDown: + t.vmove(-1, true) + req(reqList) + case actUp: + t.vmove(1, true) + req(reqList) + case actAccept: + req(reqClose) + case actAcceptNonEmpty: + if len(t.selected) > 0 || t.merger.Length() > 0 || !t.reading && t.count == 0 { + req(reqClose) + } + case actClearScreen: + req(reqFullRedraw) + case actClearQuery: + t.input = []rune{} + t.cx = 0 + case actClearSelection: + if t.multi > 0 { + t.selected = make(map[int32]selectedItem) + t.version++ + req(reqList, reqInfo) + } + case actFirst: + t.vset(0) + req(reqList) + case actLast: + t.vset(t.merger.Length() - 1) + req(reqList) + case actUnixLineDiscard: + beof = len(t.input) == 0 + if t.cx > 0 { + t.yanked = copySlice(t.input[:t.cx]) + t.input = t.input[t.cx:] + t.cx = 0 + } + case actUnixWordRubout: + beof = len(t.input) == 0 + if t.cx > 0 { + t.rubout("\\s\\S") + } + case actBackwardKillWord: + beof = len(t.input) == 0 + if t.cx > 0 { + t.rubout(t.wordRubout) + } + case actYank: + suffix := copySlice(t.input[t.cx:]) + t.input = append(append(t.input[:t.cx], t.yanked...), suffix...) + t.cx += len(t.yanked) + case actPageUp: + t.vmove(t.maxItems()-1, false) + req(reqList) + case actPageDown: + t.vmove(-(t.maxItems() - 1), false) + req(reqList) + case actHalfPageUp: + t.vmove(t.maxItems()/2, false) + req(reqList) + case actHalfPageDown: + t.vmove(-(t.maxItems() / 2), false) + req(reqList) + case actJump: + t.jumping = jumpEnabled + req(reqJump) + case actJumpAccept: + t.jumping = jumpAcceptEnabled + req(reqJump) + case actBackwardWord: + t.cx = findLastMatch(t.wordRubout, string(t.input[:t.cx])) + 1 + case actForwardWord: + t.cx += findFirstMatch(t.wordNext, string(t.input[t.cx:])) + 1 + case actKillWord: + ncx := t.cx + + findFirstMatch(t.wordNext, string(t.input[t.cx:])) + 1 + if ncx > t.cx { + t.yanked = copySlice(t.input[t.cx:ncx]) + t.input = append(t.input[:t.cx], t.input[ncx:]...) + } + case actKillLine: + if t.cx < len(t.input) { + t.yanked = copySlice(t.input[t.cx:]) + t.input = t.input[:t.cx] + } + case actRune: + prefix := copySlice(t.input[:t.cx]) + t.input = append(append(prefix, event.Char), t.input[t.cx:]...) + t.cx++ + case actPreviousHistory: + if t.history != nil { + t.history.override(string(t.input)) + t.input = trimQuery(t.history.previous()) + t.cx = len(t.input) + } + case actNextHistory: + if t.history != nil { + t.history.override(string(t.input)) + t.input = trimQuery(t.history.next()) + t.cx = len(t.input) + } + case actToggleSearch: + t.paused = !t.paused + changed = !t.paused + req(reqPrompt) + case actEnableSearch: + t.paused = false + changed = true + req(reqPrompt) + case actDisableSearch: + t.paused = true + req(reqPrompt) + case actSigStop: + p, err := os.FindProcess(os.Getpid()) + if err == nil { + t.sigstop = true + t.tui.Clear() + t.tui.Pause(t.fullscreen) + notifyStop(p) + t.mutex.Unlock() + return false + } + case actMouse: + me := event.MouseEvent + mx, my := me.X, me.Y + if me.S != 0 { + // Scroll + if t.window.Enclose(my, mx) && t.merger.Length() > 0 { + if t.multi > 0 && me.Mod { + toggle() + } + t.vmove(me.S, true) + req(reqList) + } else if t.hasPreviewWindow() && t.pwindow.Enclose(my, mx) { + scrollPreviewBy(-me.S) + } + } else if t.window.Enclose(my, mx) { + mx -= t.window.Left() + my -= t.window.Top() + mx = util.Constrain(mx-t.promptLen, 0, len(t.input)) + min := 2 + len(t.header) + if t.noInfoLine() { + min-- + } + h := t.window.Height() + switch t.layout { + case layoutDefault: + my = h - my - 1 + case layoutReverseList: + if my < h-min { + my += min + } else { + my = h - my - 1 + } + } + if me.Double { + // Double-click + if my >= min { + if t.vset(t.offset+my-min) && t.cy < t.merger.Length() { + return doActions(actionsFor(tui.DoubleClick)) + } + } + } else if me.Down { + if my == t.promptLine() && mx >= 0 { + // Prompt + t.cx = mx + t.xoffset + } else if my >= min { + // List + if t.vset(t.offset+my-min) && t.multi > 0 && me.Mod { + toggle() + } + req(reqList) + if me.Left { + return doActions(actionsFor(tui.LeftClick)) + } + return doActions(actionsFor(tui.RightClick)) + } + } + } + case actReload: + t.failed = nil + + valid, list := t.buildPlusList(a.a, false) + if !valid { + // We run the command even when there's no match + // 1. If the template doesn't have any slots + // 2. If the template has {q} + slot, _, query := hasPreviewFlags(a.a) + valid = !slot || query + } + if valid { + command := t.replacePlaceholder(a.a, false, string(t.input), list) + newCommand = &command + t.reading = true + t.version++ + } + case actUnbind: + keys := parseKeyChords(a.a, "PANIC") + for key := range keys { + delete(t.keymap, key) + } + case actChangePreview: + if t.previewOpts.command != a.a { + togglePreview(len(a.a) > 0) + t.previewOpts.command = a.a + refreshPreview(t.previewOpts.command) + } + case actChangePreviewWindow: + currentPreviewOpts := t.previewOpts + + // Reset preview options and apply the additional options + t.previewOpts = t.initialPreviewOpts + + // Split window options + tokens := strings.Split(a.a, "|") + parsePreviewWindow(&t.previewOpts, tokens[0]) + if len(tokens) > 1 { + a.a = strings.Join(append(tokens[1:], tokens[0]), "|") + } + + if t.previewOpts.hidden { + togglePreview(false) + } else { + // Full redraw + if !currentPreviewOpts.sameLayout(t.previewOpts) { + if togglePreview(true) { + refreshPreview(t.previewOpts.command) + } else { + req(reqRedraw) + } + } else if !currentPreviewOpts.sameContentLayout(t.previewOpts) { + t.previewed.version = 0 + req(reqPreviewRefresh) + } + + // Adjust scroll offset + if t.hasPreviewWindow() && currentPreviewOpts.scroll != t.previewOpts.scroll { + scrollPreviewTo(t.evaluateScrollOffset()) + } + } + } + return true + } + + if t.jumping == jumpDisabled { + actions := t.keymap[event.Comparable()] + if len(actions) == 0 && event.Type == tui.Rune { + doAction(&action{t: actRune}) + } else if !doActions(actions) { + continue + } + t.truncateQuery() + queryChanged = string(previousInput) != string(t.input) + changed = changed || queryChanged + if onChanges, prs := t.keymap[tui.Change.AsEvent()]; queryChanged && prs { + if !doActions(onChanges) { + continue + } + } + if onEOFs, prs := t.keymap[tui.BackwardEOF.AsEvent()]; beof && prs { + if !doActions(onEOFs) { + continue + } + } + } else { + if event.Type == tui.Rune { + if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() { + t.cy = idx + t.offset + if t.jumping == jumpAcceptEnabled { + req(reqClose) + } + } + } + t.jumping = jumpDisabled + req(reqList) + } + + if queryChanged { + if t.isPreviewEnabled() { + _, _, q := hasPreviewFlags(t.previewOpts.command) + if q { + t.version++ + } + } + } + + if queryChanged || t.cx != previousCx { + req(reqPrompt) + } + + t.mutex.Unlock() // Must be unlocked before touching reqBox + + if changed || newCommand != nil { + t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, command: newCommand}) + } + for _, event := range events { + t.reqBox.Set(event, nil) + } + } +} + +func (t *Terminal) constrain() { + // count of items to display allowed by filtering + count := t.merger.Length() + // count of lines can be displayed + height := t.maxItems() + + t.cy = util.Constrain(t.cy, 0, count-1) + + minOffset := util.Max(t.cy-height+1, 0) + maxOffset := util.Max(util.Min(count-height, t.cy), 0) + t.offset = util.Constrain(t.offset, minOffset, maxOffset) + if t.scrollOff == 0 { + return + } + + scrollOff := util.Min(height/2, t.scrollOff) + for { + prevOffset := t.offset + if t.cy-t.offset < scrollOff { + t.offset = util.Max(minOffset, t.offset-1) + } + if t.cy-t.offset >= height-scrollOff { + t.offset = util.Min(maxOffset, t.offset+1) + } + if t.offset == prevOffset { + break + } + } +} + +func (t *Terminal) vmove(o int, allowCycle bool) { + if t.layout != layoutDefault { + o *= -1 + } + dest := t.cy + o + if t.cycle && allowCycle { + max := t.merger.Length() - 1 + if dest > max { + if t.cy == max { + dest = 0 + } + } else if dest < 0 { + if t.cy == 0 { + dest = max + } + } + } + t.vset(dest) +} + +func (t *Terminal) vset(o int) bool { + t.cy = util.Constrain(o, 0, t.merger.Length()-1) + return t.cy == o +} + +func (t *Terminal) maxItems() int { + max := t.window.Height() - 2 - len(t.header) + if t.noInfoLine() { + max++ + } + return util.Max(max, 0) +} diff --git a/fzf/fzf/src/terminal_test.go b/fzf/fzf/src/terminal_test.go new file mode 100644 index 0000000..ee19b67 --- /dev/null +++ b/fzf/fzf/src/terminal_test.go @@ -0,0 +1,638 @@ +package fzf + +import ( + "bytes" + "io" + "os" + "regexp" + "strings" + "testing" + "text/template" + + "github.com/junegunn/fzf/src/util" +) + +func TestReplacePlaceholder(t *testing.T) { + item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m") + items1 := []*Item{item1, item1} + items2 := []*Item{ + newItem("foo'bar \x1b[31mbaz\x1b[m"), + newItem("foo'bar \x1b[31mbaz\x1b[m"), + newItem("FOO'BAR \x1b[31mBAZ\x1b[m")} + + delim := "'" + var regex *regexp.Regexp + + var result string + check := func(expected string) { + if result != expected { + t.Errorf("expected: %s, actual: %s", expected, result) + } + } + // helper function that converts template format into string and carries out the check() + checkFormat := func(format string) { + type quotes struct{ O, I, S string } // outer, inner quotes, print separator + unixStyle := quotes{`'`, `'\''`, "\n"} + windowsStyle := quotes{`^"`, `'`, "\n"} + var effectiveStyle quotes + + if util.IsWindows() { + effectiveStyle = windowsStyle + } else { + effectiveStyle = unixStyle + } + + expected := templateToString(format, effectiveStyle) + check(expected) + } + printsep := "\n" + + /* + Test multiple placeholders and the function parameters. + */ + + // {}, preserve ansi + result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1) + checkFormat("echo {{.O}} foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}") + + // {}, strip ansi + result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1) + checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}") + + // {}, with multiple items + result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2) + checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}") + + // {..}, strip leading whitespaces, preserve ansi + result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1) + checkFormat("echo {{.O}}foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}") + + // {..}, strip leading whitespaces, strip ansi + result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1) + checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}") + + // {q} + result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1) + checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}} {{.O}}query{{.O}}") + + // {q}, multiple items + result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2) + checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}") + + result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2) + checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}}") + + result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1) + checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}bazfoo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}") + + result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2) + checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}") + + result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2) + checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}") + + // forcePlus + result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2) + checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}") + + // Whitespace preserving flag with "'" delimiter + result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1) + checkFormat("echo {{.O}} foo{{.O}}") + + result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1) + checkFormat("echo {{.O}}bar baz{{.O}}") + + result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1) + checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}") + + result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1) + checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}") + + // Whitespace preserving flag with regex delimiter + regex = regexp.MustCompile(`\w+`) + + result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1) + checkFormat("echo {{.O}} {{.O}}") + + result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1) + checkFormat("echo {{.O}}{{.I}}{{.O}}") + + result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1) + checkFormat("echo {{.O}} {{.O}}") + + // No match + result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil}) + check("echo /") + + // No match, but with selections + result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1}) + checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}") + + // String delimiter + result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1) + checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.O}}/{{.O}}bar baz{{.O}}") + + // Regex delimiter + regex = regexp.MustCompile("[oa]+") + // foo'bar baz + result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1) + checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}f{{.O}}/{{.O}}r b{{.O}}/{{.O}}{{.I}}bar b{{.O}}") + + /* + Test single placeholders, but focus on the placeholders' parameters (e.g. flags). + see: TestParsePlaceholder + */ + items3 := []*Item{ + // single line + newItem("1a 1b 1c 1d 1e 1f"), + // multi line + newItem("1a 1b 1c 1d 1e 1f"), + newItem("2a 2b 2c 2d 2e 2f"), + newItem("3a 3b 3c 3d 3e 3f"), + newItem("4a 4b 4c 4d 4e 4f"), + newItem("5a 5b 5c 5d 5e 5f"), + newItem("6a 6b 6c 6d 6e 6f"), + newItem("7a 7b 7c 7d 7e 7f"), + } + stripAnsi := false + printsep = "\n" + forcePlus := false + query := "sample query" + + templateToOutput := make(map[string]string) + templateToFile := make(map[string]string) // same as above, but the file contents will be matched + // I. item type placeholder + templateToOutput[`{}`] = `{{.O}}1a 1b 1c 1d 1e 1f{{.O}}` + templateToOutput[`{+}`] = `{{.O}}1a 1b 1c 1d 1e 1f{{.O}} {{.O}}2a 2b 2c 2d 2e 2f{{.O}} {{.O}}3a 3b 3c 3d 3e 3f{{.O}} {{.O}}4a 4b 4c 4d 4e 4f{{.O}} {{.O}}5a 5b 5c 5d 5e 5f{{.O}} {{.O}}6a 6b 6c 6d 6e 6f{{.O}} {{.O}}7a 7b 7c 7d 7e 7f{{.O}}` + templateToOutput[`{n}`] = `0` + templateToOutput[`{+n}`] = `0 0 0 0 0 0 0` + templateToFile[`{f}`] = `1a 1b 1c 1d 1e 1f{{.S}}` + templateToFile[`{+f}`] = `1a 1b 1c 1d 1e 1f{{.S}}2a 2b 2c 2d 2e 2f{{.S}}3a 3b 3c 3d 3e 3f{{.S}}4a 4b 4c 4d 4e 4f{{.S}}5a 5b 5c 5d 5e 5f{{.S}}6a 6b 6c 6d 6e 6f{{.S}}7a 7b 7c 7d 7e 7f{{.S}}` + templateToFile[`{nf}`] = `0{{.S}}` + templateToFile[`{+nf}`] = `0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}` + + // II. token type placeholders + templateToOutput[`{..}`] = templateToOutput[`{}`] + templateToOutput[`{1..}`] = templateToOutput[`{}`] + templateToOutput[`{..2}`] = `{{.O}}1a 1b{{.O}}` + templateToOutput[`{1..2}`] = templateToOutput[`{..2}`] + templateToOutput[`{-2..-1}`] = `{{.O}}1e 1f{{.O}}` + // shorthand for x..x range + templateToOutput[`{1}`] = `{{.O}}1a{{.O}}` + templateToOutput[`{1..1}`] = templateToOutput[`{1}`] + templateToOutput[`{-6}`] = templateToOutput[`{1}`] + // multiple ranges + templateToOutput[`{1,2}`] = templateToOutput[`{1..2}`] + templateToOutput[`{1,2,4}`] = `{{.O}}1a 1b 1d{{.O}}` + templateToOutput[`{1,2..4}`] = `{{.O}}1a 1b 1c 1d{{.O}}` + templateToOutput[`{1..2,-4..-3}`] = `{{.O}}1a 1b 1c 1d{{.O}}` + // flags + templateToOutput[`{+1}`] = `{{.O}}1a{{.O}} {{.O}}2a{{.O}} {{.O}}3a{{.O}} {{.O}}4a{{.O}} {{.O}}5a{{.O}} {{.O}}6a{{.O}} {{.O}}7a{{.O}}` + templateToOutput[`{+-1}`] = `{{.O}}1f{{.O}} {{.O}}2f{{.O}} {{.O}}3f{{.O}} {{.O}}4f{{.O}} {{.O}}5f{{.O}} {{.O}}6f{{.O}} {{.O}}7f{{.O}}` + templateToOutput[`{s1}`] = `{{.O}}1a {{.O}}` + templateToFile[`{f1}`] = `1a{{.S}}` + templateToOutput[`{+s1..2}`] = `{{.O}}1a 1b {{.O}} {{.O}}2a 2b {{.O}} {{.O}}3a 3b {{.O}} {{.O}}4a 4b {{.O}} {{.O}}5a 5b {{.O}} {{.O}}6a 6b {{.O}} {{.O}}7a 7b {{.O}}` + templateToFile[`{+sf1..2}`] = `1a 1b {{.S}}2a 2b {{.S}}3a 3b {{.S}}4a 4b {{.S}}5a 5b {{.S}}6a 6b {{.S}}7a 7b {{.S}}` + + // III. query type placeholder + // query flag is not removed after parsing, so it gets doubled + // while the double q is invalid, it is useful here for testing purposes + templateToOutput[`{q}`] = "{{.O}}" + query + "{{.O}}" + + // IV. escaping placeholder + templateToOutput[`\{}`] = `{}` + templateToOutput[`\{++}`] = `{++}` + templateToOutput[`{++}`] = templateToOutput[`{+}`] + + for giveTemplate, wantOutput := range templateToOutput { + result = replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3) + checkFormat(wantOutput) + } + for giveTemplate, wantOutput := range templateToFile { + path := replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3) + + data, err := readFile(path) + if err != nil { + t.Errorf("Cannot read the content of the temp file %s.", path) + } + result = string(data) + + checkFormat(wantOutput) + } +} + +func TestQuoteEntry(t *testing.T) { + type quotes struct{ E, O, SQ, DQ, BS string } // standalone escape, outer, single and double quotes, backslash + unixStyle := quotes{``, `'`, `'\''`, `"`, `\`} + windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`} + var effectiveStyle quotes + + if util.IsWindows() { + effectiveStyle = windowsStyle + } else { + effectiveStyle = unixStyle + } + + tests := map[string]string{ + `'`: `{{.O}}{{.SQ}}{{.O}}`, + `"`: `{{.O}}{{.DQ}}{{.O}}`, + `\`: `{{.O}}{{.BS}}{{.O}}`, + `\"`: `{{.O}}{{.BS}}{{.DQ}}{{.O}}`, + `"\\\"`: `{{.O}}{{.DQ}}{{.BS}}{{.BS}}{{.BS}}{{.DQ}}{{.O}}`, + + `$`: `{{.O}}${{.O}}`, + `$HOME`: `{{.O}}$HOME{{.O}}`, + `'$HOME'`: `{{.O}}{{.SQ}}$HOME{{.SQ}}{{.O}}`, + + `&`: `{{.O}}{{.E}}&{{.O}}`, + `|`: `{{.O}}{{.E}}|{{.O}}`, + `<`: `{{.O}}{{.E}}<{{.O}}`, + `>`: `{{.O}}{{.E}}>{{.O}}`, + `(`: `{{.O}}{{.E}}({{.O}}`, + `)`: `{{.O}}{{.E}}){{.O}}`, + `@`: `{{.O}}{{.E}}@{{.O}}`, + `^`: `{{.O}}{{.E}}^{{.O}}`, + `%`: `{{.O}}{{.E}}%{{.O}}`, + `!`: `{{.O}}{{.E}}!{{.O}}`, + `%USERPROFILE%`: `{{.O}}{{.E}}%USERPROFILE{{.E}}%{{.O}}`, + `C:\Program Files (x86)\`: `{{.O}}C:{{.BS}}Program Files {{.E}}(x86{{.E}}){{.BS}}{{.O}}`, + `"C:\Program Files"`: `{{.O}}{{.DQ}}C:{{.BS}}Program Files{{.DQ}}{{.O}}`, + } + + for input, expected := range tests { + escaped := quoteEntry(input) + expected = templateToString(expected, effectiveStyle) + if escaped != expected { + t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped) + } + } +} + +// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Unix +func TestUnixCommands(t *testing.T) { + if util.IsWindows() { + t.SkipNow() + } + tests := []testCase{ + // reference: give{template, query, items}, want{output OR match} + + // 1) working examples + + // paths that does not have to evaluated will work fine, when quoted + {give{`grep foo {}`, ``, newItems(`test`)}, want{output: `grep foo 'test'`}}, + {give{`grep foo {}`, ``, newItems(`/home/user/test`)}, want{output: `grep foo '/home/user/test'`}}, + {give{`grep foo {}`, ``, newItems(`./test`)}, want{output: `grep foo './test'`}}, + + // only placeholders are escaped as data, this will lookup tilde character in a test file in your home directory + // quoting the tilde is required (to be treated as string) + {give{`grep {} ~/test`, ``, newItems(`~`)}, want{output: `grep '~' ~/test`}}, + + // 2) problematic examples + // (not necessarily unexpected) + + // paths that need to expand some part of it won't work (special characters and variables) + {give{`cat {}`, ``, newItems(`~/test`)}, want{output: `cat '~/test'`}}, + {give{`cat {}`, ``, newItems(`$HOME/test`)}, want{output: `cat '$HOME/test'`}}, + } + testCommands(t, tests) +} + +// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows +func TestWindowsCommands(t *testing.T) { + if !util.IsWindows() { + t.SkipNow() + } + tests := []testCase{ + // reference: give{template, query, items}, want{output OR match} + + // 1) working examples + + // example of redundantly escaped backslash in the output, besides looking bit ugly, it won't cause any issue + {give{`type {}`, ``, newItems(`C:\test.txt`)}, want{output: `type ^"C:\\test.txt^"`}}, + {give{`rg -- "package" {}`, ``, newItems(`.\test.go`)}, want{output: `rg -- "package" ^".\\test.go^"`}}, + // example of mandatorily escaped backslash in the output, otherwise `rg -- "C:\test.txt"` is matching for tabulator + {give{`rg -- {}`, ``, newItems(`C:\test.txt`)}, want{output: `rg -- ^"C:\\test.txt^"`}}, + // example of mandatorily escaped double quote in the output, otherwise `rg -- ""C:\\test.txt""` is not matching for the double quotes around the path + {give{`rg -- {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `rg -- ^"\^"C:\\test.txt\^"^"`}}, + + // 2) problematic examples + // (not necessarily unexpected) + + // notepad++'s parser can't handle `-n"12"` generate by fzf, expects `-n12` + {give{`notepad++ -n{1} {2}`, ``, newItems(`12 C:\Work\Test Folder\File.txt`)}, want{output: `notepad++ -n^"12^" ^"C:\\Work\\Test Folder\\File.txt^"`}}, + + // cat is parsing `\"` as a part of the file path, double quote is illegal character for paths on Windows + // cat: "C:\\test.txt: Invalid argument + {give{`cat {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `cat ^"\^"C:\\test.txt\^"^"`}}, + // cat: "C:\\test.txt": Invalid argument + {give{`cmd /c {}`, ``, newItems(`cat "C:\test.txt"`)}, want{output: `cmd /c ^"cat \^"C:\\test.txt\^"^"`}}, + + // the "file" flag in the pattern won't create *.bat or *.cmd file so the command in the output tries to edit the file, instead of executing it + // the temp file contains: `cat "C:\test.txt"` + // TODO this should actually work + {give{`cmd /c {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^cmd /c .*\fzf-preview-[0-9]{9}$`}}, + } + testCommands(t, tests) +} + +// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows in Powershell +func TestPowershellCommands(t *testing.T) { + if !util.IsWindows() { + t.SkipNow() + } + + tests := []testCase{ + // reference: give{template, query, items}, want{output OR match} + + /* + You can read each line in the following table as a pipeline that + consist of series of parsers that act upon your input (col. 1) and + each cell represents the output value. + + For example: + - exec.Command("program.exe", `\''`) + - goes to win32 api which will process it transparently as it contains no special characters, see [CommandLineToArgvW][]. + - powershell command will receive it as is, that is two arguments: a literal backslash and empty string in single quotes + - native command run via/from powershell will receive only one argument: a literal backslash. Because extra parsing rules apply, see [NativeCallsFromPowershell][]. + - some¹ apps have internal parser, that requires one more level of escaping (yes, this is completely application-specific, but see terminal_test.go#TestWindowsCommands) + + Character⁰ CommandLineToArgvW Powershell commands Native commands from Powershell Apps requiring escapes¹ | Being tested below + ---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------ + " empty string² missing argument error ... ... | + \" literal " unbalanced quote error ... ... | + '\"' literal '"' literal " empty string empty string (match all) | yes + '\\\"' literal '\"' literal \" literal " literal " | + ---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------ + \ transparent transparent transparent regex error | + '\' transparent literal \ literal \ regex error | yes + \\ transparent transparent transparent literal \ | + '\\' transparent literal \\ literal \\ literal \ | + ---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------ + ' transparent unbalanced quote error ... ... | + \' transparent literal \ and unb. quote error ... ... | + \'' transparent literal \ and empty string literal \ regex error | no, but given as example above + ''' transparent unbalanced quote error ... ... | + '''' transparent literal ' literal ' literal ' | yes + ---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------ + + ⁰: charatecter or characters 'x' as an argument to a program in go's call: exec.Command("program.exe", `x`) + ¹: native commands like grep, git grep, ripgrep + ²: interpreted as a grouping quote, affects argument parser and gets removed from the result + + [CommandLineToArgvW]: https://docs.microsoft.com/en-gb/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw#remarks + [NativeCallsFromPowershell]: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing?view=powershell-7.1#passing-arguments-that-contain-quote-characters + */ + + // 1) working examples + + {give{`Get-Content {}`, ``, newItems(`C:\test.txt`)}, want{output: `Get-Content 'C:\test.txt'`}}, + {give{`rg -- "package" {}`, ``, newItems(`.\test.go`)}, want{output: `rg -- "package" '.\test.go'`}}, + + // example of escaping single quotes + {give{`rg -- {}`, ``, newItems(`'foobar'`)}, want{output: `rg -- '''foobar'''`}}, + + // chaining powershells + {give{`powershell -NoProfile -Command {}`, ``, newItems(`cat "C:\test.txt"`)}, want{output: `powershell -NoProfile -Command 'cat \"C:\test.txt\"'`}}, + + // 2) problematic examples + // (not necessarily unexpected) + + // looking for a path string will only work with escaped backslashes + {give{`rg -- {}`, ``, newItems(`C:\test.txt`)}, want{output: `rg -- 'C:\test.txt'`}}, + // looking for a literal double quote will only work with triple escaped double quotes + {give{`rg -- {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `rg -- '\"C:\test.txt\"'`}}, + + // Get-Content (i.e. cat alias) is parsing `"` as a part of the file path, returns an error: + // Get-Content : Cannot find drive. A drive with the name '"C:' does not exist. + {give{`cat {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `cat '\"C:\test.txt\"'`}}, + + // the "file" flag in the pattern won't create *.ps1 file so the powershell will offload this "unknown" filetype + // to explorer, which will prompt user to pick editing program for the fzf-preview file + // the temp file contains: `cat "C:\test.txt"` + // TODO this should actually work + {give{`powershell -NoProfile -Command {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^powershell -NoProfile -Command .*\fzf-preview-[0-9]{9}$`}}, + } + + // to force powershell-style escaping we temporarily set environment variable that fzf honors + shellBackup := os.Getenv("SHELL") + os.Setenv("SHELL", "powershell") + testCommands(t, tests) + os.Setenv("SHELL", shellBackup) +} + +/* + Test typical valid placeholders and parsing of them. + + Also since the parser assumes the input is matched with `placeholder` regex, + the regex is tested here as well. +*/ +func TestParsePlaceholder(t *testing.T) { + // give, want pairs + templates := map[string]string{ + // I. item type placeholder + `{}`: `{}`, + `{+}`: `{+}`, + `{n}`: `{n}`, + `{+n}`: `{+n}`, + `{f}`: `{f}`, + `{+nf}`: `{+nf}`, + + // II. token type placeholders + `{..}`: `{..}`, + `{1..}`: `{1..}`, + `{..2}`: `{..2}`, + `{1..2}`: `{1..2}`, + `{-2..-1}`: `{-2..-1}`, + // shorthand for x..x range + `{1}`: `{1}`, + `{1..1}`: `{1..1}`, + `{-6}`: `{-6}`, + // multiple ranges + `{1,2}`: `{1,2}`, + `{1,2,4}`: `{1,2,4}`, + `{1,2..4}`: `{1,2..4}`, + `{1..2,-4..-3}`: `{1..2,-4..-3}`, + // flags + `{+1}`: `{+1}`, + `{+-1}`: `{+-1}`, + `{s1}`: `{s1}`, + `{f1}`: `{f1}`, + `{+s1..2}`: `{+s1..2}`, + `{+sf1..2}`: `{+sf1..2}`, + + // III. query type placeholder + // query flag is not removed after parsing, so it gets doubled + // while the double q is invalid, it is useful here for testing purposes + `{q}`: `{qq}`, + + // IV. escaping placeholder + `\{}`: `{}`, + `\{++}`: `{++}`, + `{++}`: `{+}`, + } + + for giveTemplate, wantTemplate := range templates { + if !placeholder.MatchString(giveTemplate) { + t.Errorf(`given placeholder %s does not match placeholder regex, so attempt to parse it is unexpected`, giveTemplate) + continue + } + + _, placeholderWithoutFlags, flags := parsePlaceholder(giveTemplate) + gotTemplate := placeholderWithoutFlags[:1] + flags.encodePlaceholder() + placeholderWithoutFlags[1:] + + if gotTemplate != wantTemplate { + t.Errorf(`parsed placeholder "%s" into "%s", but want "%s"`, giveTemplate, gotTemplate, wantTemplate) + } + } +} + +/* utilities section */ + +// Item represents one line in fzf UI. Usually it is relative path to files and folders. +func newItem(str string) *Item { + bytes := []byte(str) + trimmed, _, _ := extractColor(str, nil, nil) + return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))} +} + +// Functions tested in this file require array of items (allItems). The array needs +// to consist of at least two nils. This is helper function. +func newItems(str ...string) []*Item { + result := make([]*Item, util.Max(len(str), 2)) + for i, s := range str { + result[i] = newItem(s) + } + return result +} + +// (for logging purposes) +func (item *Item) String() string { + return item.AsString(true) +} + +// Helper function to parse, execute and convert "text/template" to string. Panics on error. +func templateToString(format string, data interface{}) string { + bb := &bytes.Buffer{} + + err := template.Must(template.New("").Parse(format)).Execute(bb, data) + if err != nil { + panic(err) + } + + return bb.String() +} + +// ad hoc types for test cases +type give struct { + template string + query string + allItems []*Item +} +type want struct { + /* + Unix: + The `want.output` string is supposed to be formatted for evaluation by + `sh -c command` system call. + + Windows: + The `want.output` string is supposed to be formatted for evaluation by + `cmd.exe /s /c "command"` system call. The `/s` switch enables so called old + behaviour, which is more favourable for nesting (possibly escaped) + special characters. This is the relevant section of `help cmd`: + + ...old behavior is to see if the first character is + a quote character and if so, strip the leading character and + remove the last quote character on the command line, preserving + any text after the last quote character. + */ + output string // literal output + match string // output is matched against this regex (when output is empty string) +} +type testCase struct { + give + want +} + +func testCommands(t *testing.T, tests []testCase) { + // common test parameters + delim := "\t" + delimiter := Delimiter{str: &delim} + printsep := "" + stripAnsi := false + forcePlus := false + + // evaluate the test cases + for idx, test := range tests { + gotOutput := replacePlaceholder( + test.give.template, stripAnsi, delimiter, printsep, forcePlus, + test.give.query, + test.give.allItems) + switch { + case test.want.output != "": + if gotOutput != test.want.output { + t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'", + idx, + test.give.template, test.give.query, test.give.allItems, + gotOutput, test.want.output) + } + case test.want.match != "": + wantMatch := strings.ReplaceAll(test.want.match, `\`, `\\`) + wantRegex := regexp.MustCompile(wantMatch) + if !wantRegex.MatchString(gotOutput) { + t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'", + idx, + test.give.template, test.give.query, test.give.allItems, + gotOutput, test.want.match) + } + default: + t.Errorf("tests[%v]: test case does not describe 'want' property", idx) + } + } +} + +// naive encoder of placeholder flags +func (flags placeholderFlags) encodePlaceholder() string { + encoded := "" + if flags.plus { + encoded += "+" + } + if flags.preserveSpace { + encoded += "s" + } + if flags.number { + encoded += "n" + } + if flags.file { + encoded += "f" + } + if flags.query { + encoded += "q" + } + return encoded +} + +// can be replaced with os.ReadFile() in go 1.16+ +func readFile(path string) ([]byte, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + data := make([]byte, 0, 128) + for { + if len(data) >= cap(data) { + d := append(data[:cap(data)], 0) + data = d[:len(data)] + } + + n, err := file.Read(data[len(data):cap(data)]) + data = data[:len(data)+n] + if err != nil { + if err == io.EOF { + err = nil + } + return data, err + } + } +} diff --git a/fzf/fzf/src/terminal_unix.go b/fzf/fzf/src/terminal_unix.go new file mode 100644 index 0000000..b14cd68 --- /dev/null +++ b/fzf/fzf/src/terminal_unix.go @@ -0,0 +1,26 @@ +// +build !windows + +package fzf + +import ( + "os" + "os/signal" + "strings" + "syscall" +) + +func notifyOnResize(resizeChan chan<- os.Signal) { + signal.Notify(resizeChan, syscall.SIGWINCH) +} + +func notifyStop(p *os.Process) { + p.Signal(syscall.SIGSTOP) +} + +func notifyOnCont(resizeChan chan<- os.Signal) { + signal.Notify(resizeChan, syscall.SIGCONT) +} + +func quoteEntry(entry string) string { + return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'" +} diff --git a/fzf/fzf/src/terminal_windows.go b/fzf/fzf/src/terminal_windows.go new file mode 100644 index 0000000..5e74873 --- /dev/null +++ b/fzf/fzf/src/terminal_windows.go @@ -0,0 +1,45 @@ +// +build windows + +package fzf + +import ( + "os" + "regexp" + "strings" +) + +func notifyOnResize(resizeChan chan<- os.Signal) { + // TODO +} + +func notifyStop(p *os.Process) { + // NOOP +} + +func notifyOnCont(resizeChan chan<- os.Signal) { + // NOOP +} + +func quoteEntry(entry string) string { + shell := os.Getenv("SHELL") + if len(shell) == 0 { + shell = "cmd" + } + + if strings.Contains(shell, "cmd") { + // backslash escaping is done here for applications + // (see ripgrep test case in terminal_test.go#TestWindowsCommands) + escaped := strings.Replace(entry, `\`, `\\`, -1) + escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"` + // caret is the escape character for cmd shell + r, _ := regexp.Compile(`[&|<>()@^%!"]`) + return r.ReplaceAllStringFunc(escaped, func(match string) string { + return "^" + match + }) + } else if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") { + escaped := strings.Replace(entry, `"`, `\"`, -1) + return "'" + strings.Replace(escaped, "'", "''", -1) + "'" + } else { + return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'" + } +} diff --git a/fzf/fzf/src/tokenizer.go b/fzf/fzf/src/tokenizer.go new file mode 100644 index 0000000..26f42d2 --- /dev/null +++ b/fzf/fzf/src/tokenizer.go @@ -0,0 +1,253 @@ +package fzf + +import ( + "bytes" + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/junegunn/fzf/src/util" +) + +const rangeEllipsis = 0 + +// Range represents nth-expression +type Range struct { + begin int + end int +} + +// Token contains the tokenized part of the strings and its prefix length +type Token struct { + text *util.Chars + prefixLength int32 +} + +// String returns the string representation of a Token. +func (t Token) String() string { + return fmt.Sprintf("Token{text: %s, prefixLength: %d}", t.text, t.prefixLength) +} + +// Delimiter for tokenizing the input +type Delimiter struct { + regex *regexp.Regexp + str *string +} + +// String returns the string representation of a Delimiter. +func (d Delimiter) String() string { + return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str) +} + +func newRange(begin int, end int) Range { + if begin == 1 { + begin = rangeEllipsis + } + if end == -1 { + end = rangeEllipsis + } + return Range{begin, end} +} + +// ParseRange parses nth-expression and returns the corresponding Range object +func ParseRange(str *string) (Range, bool) { + if (*str) == ".." { + return newRange(rangeEllipsis, rangeEllipsis), true + } else if strings.HasPrefix(*str, "..") { + end, err := strconv.Atoi((*str)[2:]) + if err != nil || end == 0 { + return Range{}, false + } + return newRange(rangeEllipsis, end), true + } else if strings.HasSuffix(*str, "..") { + begin, err := strconv.Atoi((*str)[:len(*str)-2]) + if err != nil || begin == 0 { + return Range{}, false + } + return newRange(begin, rangeEllipsis), true + } else if strings.Contains(*str, "..") { + ns := strings.Split(*str, "..") + if len(ns) != 2 { + return Range{}, false + } + begin, err1 := strconv.Atoi(ns[0]) + end, err2 := strconv.Atoi(ns[1]) + if err1 != nil || err2 != nil || begin == 0 || end == 0 { + return Range{}, false + } + return newRange(begin, end), true + } + + n, err := strconv.Atoi(*str) + if err != nil || n == 0 { + return Range{}, false + } + return newRange(n, n), true +} + +func withPrefixLengths(tokens []string, begin int) []Token { + ret := make([]Token, len(tokens)) + + prefixLength := begin + for idx := range tokens { + chars := util.ToChars([]byte(tokens[idx])) + ret[idx] = Token{&chars, int32(prefixLength)} + prefixLength += chars.Length() + } + return ret +} + +const ( + awkNil = iota + awkBlack + awkWhite +) + +func awkTokenizer(input string) ([]string, int) { + // 9, 32 + ret := []string{} + prefixLength := 0 + state := awkNil + begin := 0 + end := 0 + for idx := 0; idx < len(input); idx++ { + r := input[idx] + white := r == 9 || r == 32 + switch state { + case awkNil: + if white { + prefixLength++ + } else { + state, begin, end = awkBlack, idx, idx+1 + } + case awkBlack: + end = idx + 1 + if white { + state = awkWhite + } + case awkWhite: + if white { + end = idx + 1 + } else { + ret = append(ret, input[begin:end]) + state, begin, end = awkBlack, idx, idx+1 + } + } + } + if begin < end { + ret = append(ret, input[begin:end]) + } + return ret, prefixLength +} + +// Tokenize tokenizes the given string with the delimiter +func Tokenize(text string, delimiter Delimiter) []Token { + if delimiter.str == nil && delimiter.regex == nil { + // AWK-style (\S+\s*) + tokens, prefixLength := awkTokenizer(text) + return withPrefixLengths(tokens, prefixLength) + } + + if delimiter.str != nil { + return withPrefixLengths(strings.SplitAfter(text, *delimiter.str), 0) + } + + // FIXME performance + var tokens []string + if delimiter.regex != nil { + for len(text) > 0 { + loc := delimiter.regex.FindStringIndex(text) + if len(loc) < 2 { + loc = []int{0, len(text)} + } + last := util.Max(loc[1], 1) + tokens = append(tokens, text[:last]) + text = text[last:] + } + } + return withPrefixLengths(tokens, 0) +} + +func joinTokens(tokens []Token) string { + var output bytes.Buffer + for _, token := range tokens { + output.WriteString(token.text.ToString()) + } + return output.String() +} + +// Transform is used to transform the input when --with-nth option is given +func Transform(tokens []Token, withNth []Range) []Token { + transTokens := make([]Token, len(withNth)) + numTokens := len(tokens) + for idx, r := range withNth { + parts := []*util.Chars{} + minIdx := 0 + if r.begin == r.end { + idx := r.begin + if idx == rangeEllipsis { + chars := util.ToChars([]byte(joinTokens(tokens))) + parts = append(parts, &chars) + } else { + if idx < 0 { + idx += numTokens + 1 + } + if idx >= 1 && idx <= numTokens { + minIdx = idx - 1 + parts = append(parts, tokens[idx-1].text) + } + } + } else { + var begin, end int + if r.begin == rangeEllipsis { // ..N + begin, end = 1, r.end + if end < 0 { + end += numTokens + 1 + } + } else if r.end == rangeEllipsis { // N.. + begin, end = r.begin, numTokens + if begin < 0 { + begin += numTokens + 1 + } + } else { + begin, end = r.begin, r.end + if begin < 0 { + begin += numTokens + 1 + } + if end < 0 { + end += numTokens + 1 + } + } + minIdx = util.Max(0, begin-1) + for idx := begin; idx <= end; idx++ { + if idx >= 1 && idx <= numTokens { + parts = append(parts, tokens[idx-1].text) + } + } + } + // Merge multiple parts + var merged util.Chars + switch len(parts) { + case 0: + merged = util.ToChars([]byte{}) + case 1: + merged = *parts[0] + default: + var output bytes.Buffer + for _, part := range parts { + output.WriteString(part.ToString()) + } + merged = util.ToChars(output.Bytes()) + } + + var prefixLength int32 + if minIdx < numTokens { + prefixLength = tokens[minIdx].prefixLength + } else { + prefixLength = 0 + } + transTokens[idx] = Token{&merged, prefixLength} + } + return transTokens +} diff --git a/fzf/fzf/src/tokenizer_test.go b/fzf/fzf/src/tokenizer_test.go new file mode 100644 index 0000000..985cef9 --- /dev/null +++ b/fzf/fzf/src/tokenizer_test.go @@ -0,0 +1,112 @@ +package fzf + +import ( + "testing" +) + +func TestParseRange(t *testing.T) { + { + i := ".." + r, _ := ParseRange(&i) + if r.begin != rangeEllipsis || r.end != rangeEllipsis { + t.Errorf("%v", r) + } + } + { + i := "3.." + r, _ := ParseRange(&i) + if r.begin != 3 || r.end != rangeEllipsis { + t.Errorf("%v", r) + } + } + { + i := "3..5" + r, _ := ParseRange(&i) + if r.begin != 3 || r.end != 5 { + t.Errorf("%v", r) + } + } + { + i := "-3..-5" + r, _ := ParseRange(&i) + if r.begin != -3 || r.end != -5 { + t.Errorf("%v", r) + } + } + { + i := "3" + r, _ := ParseRange(&i) + if r.begin != 3 || r.end != 3 { + t.Errorf("%v", r) + } + } +} + +func TestTokenize(t *testing.T) { + // AWK-style + input := " abc: def: ghi " + tokens := Tokenize(input, Delimiter{}) + if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 { + t.Errorf("%s", tokens) + } + + // With delimiter + tokens = Tokenize(input, delimiterRegexp(":")) + if tokens[0].text.ToString() != " abc:" || tokens[0].prefixLength != 0 { + t.Error(tokens[0].text.ToString(), tokens[0].prefixLength) + } + + // With delimiter regex + tokens = Tokenize(input, delimiterRegexp("\\s+")) + if tokens[0].text.ToString() != " " || tokens[0].prefixLength != 0 || + tokens[1].text.ToString() != "abc: " || tokens[1].prefixLength != 2 || + tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 8 || + tokens[3].text.ToString() != "ghi " || tokens[3].prefixLength != 14 { + t.Errorf("%s", tokens) + } +} + +func TestTransform(t *testing.T) { + input := " abc: def: ghi: jkl" + { + tokens := Tokenize(input, Delimiter{}) + { + ranges := splitNth("1,2,3") + tx := Transform(tokens, ranges) + if joinTokens(tx) != "abc: def: ghi: " { + t.Errorf("%s", tx) + } + } + { + ranges := splitNth("1..2,3,2..,1") + tx := Transform(tokens, ranges) + if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " || + len(tx) != 4 || + tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 || + tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 || + tx[2].text.ToString() != "def: ghi: jkl" || tx[2].prefixLength != 8 || + tx[3].text.ToString() != "abc: " || tx[3].prefixLength != 2 { + t.Errorf("%s", tx) + } + } + } + { + tokens := Tokenize(input, delimiterRegexp(":")) + { + ranges := splitNth("1..2,3,2..,1") + tx := Transform(tokens, ranges) + if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" || + len(tx) != 4 || + tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 || + tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 || + tx[2].text.ToString() != " def: ghi: jkl" || tx[2].prefixLength != 6 || + tx[3].text.ToString() != " abc:" || tx[3].prefixLength != 0 { + t.Errorf("%s", tx) + } + } + } +} + +func TestTransformIndexOutOfBounds(t *testing.T) { + Transform([]Token{}, splitNth("1")) +} diff --git a/fzf/fzf/src/tui/dummy.go b/fzf/fzf/src/tui/dummy.go new file mode 100644 index 0000000..af7e759 --- /dev/null +++ b/fzf/fzf/src/tui/dummy.go @@ -0,0 +1,46 @@ +// +build !ncurses +// +build !tcell +// +build !windows + +package tui + +type Attr int32 + +func HasFullscreenRenderer() bool { + return false +} + +func (a Attr) Merge(b Attr) Attr { + return a | b +} + +const ( + AttrUndefined = Attr(0) + AttrRegular = Attr(1 << 7) + AttrClear = Attr(1 << 8) + + Bold = Attr(1) + Dim = Attr(1 << 1) + Italic = Attr(1 << 2) + Underline = Attr(1 << 3) + Blink = Attr(1 << 4) + Blink2 = Attr(1 << 5) + Reverse = Attr(1 << 6) +) + +func (r *FullscreenRenderer) Init() {} +func (r *FullscreenRenderer) Pause(bool) {} +func (r *FullscreenRenderer) Resume(bool, bool) {} +func (r *FullscreenRenderer) Clear() {} +func (r *FullscreenRenderer) Refresh() {} +func (r *FullscreenRenderer) Close() {} + +func (r *FullscreenRenderer) GetChar() Event { return Event{} } +func (r *FullscreenRenderer) MaxX() int { return 0 } +func (r *FullscreenRenderer) MaxY() int { return 0 } + +func (r *FullscreenRenderer) RefreshWindows(windows []Window) {} + +func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window { + return nil +} diff --git a/fzf/fzf/src/tui/light.go b/fzf/fzf/src/tui/light.go new file mode 100644 index 0000000..d3e3fab --- /dev/null +++ b/fzf/fzf/src/tui/light.go @@ -0,0 +1,987 @@ +package tui + +import ( + "bytes" + "fmt" + "os" + "regexp" + "strconv" + "strings" + "time" + "unicode/utf8" + + "github.com/mattn/go-runewidth" + "github.com/rivo/uniseg" + + "golang.org/x/term" +) + +const ( + defaultWidth = 80 + defaultHeight = 24 + + defaultEscDelay = 100 + escPollInterval = 5 + offsetPollTries = 10 + maxInputBuffer = 10 * 1024 +) + +const consoleDevice string = "/dev/tty" + +var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R") +var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R") + +func (r *LightRenderer) stderr(str string) { + r.stderrInternal(str, true) +} + +// FIXME: Need better handling of non-displayable characters +func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) { + bytes := []byte(str) + runes := []rune{} + for len(bytes) > 0 { + r, sz := utf8.DecodeRune(bytes) + nlcr := r == '\n' || r == '\r' + if r >= 32 || r == '\x1b' || nlcr { + if r == utf8.RuneError || nlcr && !allowNLCR { + runes = append(runes, ' ') + } else { + runes = append(runes, r) + } + } + bytes = bytes[sz:] + } + r.queued.WriteString(string(runes)) +} + +func (r *LightRenderer) csi(code string) { + r.stderr("\x1b[" + code) +} + +func (r *LightRenderer) flush() { + if r.queued.Len() > 0 { + fmt.Fprint(os.Stderr, r.queued.String()) + r.queued.Reset() + } +} + +// Light renderer +type LightRenderer struct { + theme *ColorTheme + mouse bool + forceBlack bool + clearOnExit bool + prevDownTime time.Time + clickY []int + ttyin *os.File + buffer []byte + origState *term.State + width int + height int + yoffset int + tabstop int + escDelay int + fullscreen bool + upOneLine bool + queued strings.Builder + y int + x int + maxHeightFunc func(int) int + + // Windows only + ttyinChannel chan byte + inHandle uintptr + outHandle uintptr + origStateInput uint32 + origStateOutput uint32 +} + +type LightWindow struct { + renderer *LightRenderer + colored bool + preview bool + border BorderStyle + top int + left int + width int + height int + posx int + posy int + tabstop int + fg Color + bg Color +} + +func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) Renderer { + r := LightRenderer{ + theme: theme, + forceBlack: forceBlack, + mouse: mouse, + clearOnExit: clearOnExit, + ttyin: openTtyIn(), + yoffset: 0, + tabstop: tabstop, + fullscreen: fullscreen, + upOneLine: false, + maxHeightFunc: maxHeightFunc} + return &r +} + +func repeat(r rune, times int) string { + if times > 0 { + return strings.Repeat(string(r), times) + } + return "" +} + +func atoi(s string, defaultValue int) int { + value, err := strconv.Atoi(s) + if err != nil { + return defaultValue + } + return value +} + +func (r *LightRenderer) Init() { + r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay) + + if err := r.initPlatform(); err != nil { + errorExit(err.Error()) + } + r.updateTerminalSize() + initTheme(r.theme, r.defaultTheme(), r.forceBlack) + + if r.fullscreen { + r.smcup() + } else { + // We assume that --no-clear is used for repetitive relaunching of fzf. + // So we do not clear the lower bottom of the screen. + if r.clearOnExit { + r.csi("J") + } + y, x := r.findOffset() + r.mouse = r.mouse && y >= 0 + // When --no-clear is used for repetitive relaunching, there is a small + // time frame between fzf processes where the user keystrokes are not + // captured by either of fzf process which can cause x offset to be + // increased and we're left with unwanted extra new line. + if x > 0 && r.clearOnExit { + r.upOneLine = true + r.makeSpace() + } + for i := 1; i < r.MaxY(); i++ { + r.makeSpace() + } + } + + if r.mouse { + r.csi("?1000h") + } + r.csi(fmt.Sprintf("%dA", r.MaxY()-1)) + r.csi("G") + r.csi("K") + if !r.clearOnExit && !r.fullscreen { + r.csi("s") + } + if !r.fullscreen && r.mouse { + r.yoffset, _ = r.findOffset() + } +} + +func (r *LightRenderer) makeSpace() { + r.stderr("\n") + r.csi("G") +} + +func (r *LightRenderer) move(y int, x int) { + // w.csi("u") + if r.y < y { + r.csi(fmt.Sprintf("%dB", y-r.y)) + } else if r.y > y { + r.csi(fmt.Sprintf("%dA", r.y-y)) + } + r.stderr("\r") + if x > 0 { + r.csi(fmt.Sprintf("%dC", x)) + } + r.y = y + r.x = x +} + +func (r *LightRenderer) origin() { + r.move(0, 0) +} + +func getEnv(name string, defaultValue int) int { + env := os.Getenv(name) + if len(env) == 0 { + return defaultValue + } + return atoi(env, defaultValue) +} + +func (r *LightRenderer) getBytes() []byte { + return r.getBytesInternal(r.buffer, false) +} + +func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte { + c, ok := r.getch(nonblock) + if !nonblock && !ok { + r.Close() + errorExit("Failed to read " + consoleDevice) + } + + retries := 0 + if c == ESC.Int() || nonblock { + retries = r.escDelay / escPollInterval + } + buffer = append(buffer, byte(c)) + + pc := c + for { + c, ok = r.getch(true) + if !ok { + if retries > 0 { + retries-- + time.Sleep(escPollInterval * time.Millisecond) + continue + } + break + } else if c == ESC.Int() && pc != c { + retries = r.escDelay / escPollInterval + } else { + retries = 0 + } + buffer = append(buffer, byte(c)) + pc = c + + // This should never happen under normal conditions, + // so terminate fzf immediately. + if len(buffer) > maxInputBuffer { + r.Close() + panic(fmt.Sprintf("Input buffer overflow (%d): %v", len(buffer), buffer)) + } + } + + return buffer +} + +func (r *LightRenderer) GetChar() Event { + if len(r.buffer) == 0 { + r.buffer = r.getBytes() + } + if len(r.buffer) == 0 { + panic("Empty buffer") + } + + sz := 1 + defer func() { + r.buffer = r.buffer[sz:] + }() + + switch r.buffer[0] { + case CtrlC.Byte(): + return Event{CtrlC, 0, nil} + case CtrlG.Byte(): + return Event{CtrlG, 0, nil} + case CtrlQ.Byte(): + return Event{CtrlQ, 0, nil} + case 127: + return Event{BSpace, 0, nil} + case 0: + return Event{CtrlSpace, 0, nil} + case 28: + return Event{CtrlBackSlash, 0, nil} + case 29: + return Event{CtrlRightBracket, 0, nil} + case 30: + return Event{CtrlCaret, 0, nil} + case 31: + return Event{CtrlSlash, 0, nil} + case ESC.Byte(): + ev := r.escSequence(&sz) + // Second chance + if ev.Type == Invalid { + r.buffer = r.getBytes() + ev = r.escSequence(&sz) + } + return ev + } + + // CTRL-A ~ CTRL-Z + if r.buffer[0] <= CtrlZ.Byte() { + return Event{EventType(r.buffer[0]), 0, nil} + } + char, rsz := utf8.DecodeRune(r.buffer) + if char == utf8.RuneError { + return Event{ESC, 0, nil} + } + sz = rsz + return Event{Rune, char, nil} +} + +func (r *LightRenderer) escSequence(sz *int) Event { + if len(r.buffer) < 2 { + return Event{ESC, 0, nil} + } + + loc := offsetRegexpBegin.FindIndex(r.buffer) + if loc != nil && loc[0] == 0 { + *sz = loc[1] + return Event{Invalid, 0, nil} + } + + *sz = 2 + if r.buffer[1] >= 1 && r.buffer[1] <= 'z'-'a'+1 { + return CtrlAltKey(rune(r.buffer[1] + 'a' - 1)) + } + alt := false + if len(r.buffer) > 2 && r.buffer[1] == ESC.Byte() { + r.buffer = r.buffer[1:] + alt = true + } + switch r.buffer[1] { + case ESC.Byte(): + return Event{ESC, 0, nil} + case 127: + return Event{AltBS, 0, nil} + case '[', 'O': + if len(r.buffer) < 3 { + return Event{Invalid, 0, nil} + } + *sz = 3 + switch r.buffer[2] { + case 'D': + if alt { + return Event{AltLeft, 0, nil} + } + return Event{Left, 0, nil} + case 'C': + if alt { + // Ugh.. + return Event{AltRight, 0, nil} + } + return Event{Right, 0, nil} + case 'B': + if alt { + return Event{AltDown, 0, nil} + } + return Event{Down, 0, nil} + case 'A': + if alt { + return Event{AltUp, 0, nil} + } + return Event{Up, 0, nil} + case 'Z': + return Event{BTab, 0, nil} + case 'H': + return Event{Home, 0, nil} + case 'F': + return Event{End, 0, nil} + case 'M': + return r.mouseSequence(sz) + case 'P': + return Event{F1, 0, nil} + case 'Q': + return Event{F2, 0, nil} + case 'R': + return Event{F3, 0, nil} + case 'S': + return Event{F4, 0, nil} + case '1', '2', '3', '4', '5', '6': + if len(r.buffer) < 4 { + return Event{Invalid, 0, nil} + } + *sz = 4 + switch r.buffer[2] { + case '2': + if r.buffer[3] == '~' { + return Event{Insert, 0, nil} + } + if len(r.buffer) > 4 && r.buffer[4] == '~' { + *sz = 5 + switch r.buffer[3] { + case '0': + return Event{F9, 0, nil} + case '1': + return Event{F10, 0, nil} + case '3': + return Event{F11, 0, nil} + case '4': + return Event{F12, 0, nil} + } + } + // Bracketed paste mode: \e[200~ ... \e[201~ + if len(r.buffer) > 5 && r.buffer[3] == '0' && (r.buffer[4] == '0' || r.buffer[4] == '1') && r.buffer[5] == '~' { + // Immediately discard the sequence from the buffer and reread input + r.buffer = r.buffer[6:] + *sz = 0 + return r.GetChar() + } + return Event{Invalid, 0, nil} // INS + case '3': + return Event{Del, 0, nil} + case '4': + return Event{End, 0, nil} + case '5': + return Event{PgUp, 0, nil} + case '6': + return Event{PgDn, 0, nil} + case '1': + switch r.buffer[3] { + case '~': + return Event{Home, 0, nil} + case '1', '2', '3', '4', '5', '7', '8', '9': + if len(r.buffer) == 5 && r.buffer[4] == '~' { + *sz = 5 + switch r.buffer[3] { + case '1': + return Event{F1, 0, nil} + case '2': + return Event{F2, 0, nil} + case '3': + return Event{F3, 0, nil} + case '4': + return Event{F4, 0, nil} + case '5': + return Event{F5, 0, nil} + case '7': + return Event{F6, 0, nil} + case '8': + return Event{F7, 0, nil} + case '9': + return Event{F8, 0, nil} + } + } + return Event{Invalid, 0, nil} + case ';': + if len(r.buffer) < 6 { + return Event{Invalid, 0, nil} + } + *sz = 6 + switch r.buffer[4] { + case '1', '2', '3', '5': + alt := r.buffer[4] == '3' + altShift := r.buffer[4] == '1' && r.buffer[5] == '0' + char := r.buffer[5] + if altShift { + if len(r.buffer) < 7 { + return Event{Invalid, 0, nil} + } + *sz = 7 + char = r.buffer[6] + } + switch char { + case 'A': + if alt { + return Event{AltUp, 0, nil} + } + if altShift { + return Event{AltSUp, 0, nil} + } + return Event{SUp, 0, nil} + case 'B': + if alt { + return Event{AltDown, 0, nil} + } + if altShift { + return Event{AltSDown, 0, nil} + } + return Event{SDown, 0, nil} + case 'C': + if alt { + return Event{AltRight, 0, nil} + } + if altShift { + return Event{AltSRight, 0, nil} + } + return Event{SRight, 0, nil} + case 'D': + if alt { + return Event{AltLeft, 0, nil} + } + if altShift { + return Event{AltSLeft, 0, nil} + } + return Event{SLeft, 0, nil} + } + } // r.buffer[4] + } // r.buffer[3] + } // r.buffer[2] + } // r.buffer[2] + } // r.buffer[1] + rest := bytes.NewBuffer(r.buffer[1:]) + c, size, err := rest.ReadRune() + if err == nil { + *sz = 1 + size + return AltKey(c) + } + return Event{Invalid, 0, nil} +} + +func (r *LightRenderer) mouseSequence(sz *int) Event { + if len(r.buffer) < 6 || !r.mouse { + return Event{Invalid, 0, nil} + } + *sz = 6 + switch r.buffer[3] { + case 32, 34, 36, 40, 48, // mouse-down / shift / cmd / ctrl + 35, 39, 43, 51: // mouse-up / shift / cmd / ctrl + mod := r.buffer[3] >= 36 + left := r.buffer[3] == 32 + down := r.buffer[3]%2 == 0 + x := int(r.buffer[4] - 33) + y := int(r.buffer[5]-33) - r.yoffset + double := false + if down { + now := time.Now() + if !left { // Right double click is not allowed + r.clickY = []int{} + } else if now.Sub(r.prevDownTime) < doubleClickDuration { + r.clickY = append(r.clickY, y) + } else { + r.clickY = []int{y} + } + r.prevDownTime = now + } else { + if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] && + time.Since(r.prevDownTime) < doubleClickDuration { + double = true + } + } + + return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}} + case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl + 97, 101, 105, 113: // scroll-down / shift / cmd / ctrl + mod := r.buffer[3] >= 100 + s := 1 - int(r.buffer[3]%2)*2 + x := int(r.buffer[4] - 33) + y := int(r.buffer[5]-33) - r.yoffset + return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, false, mod}} + } + return Event{Invalid, 0, nil} +} + +func (r *LightRenderer) smcup() { + r.csi("?1049h") +} + +func (r *LightRenderer) rmcup() { + r.csi("?1049l") +} + +func (r *LightRenderer) Pause(clear bool) { + r.restoreTerminal() + if clear { + if r.fullscreen { + r.rmcup() + } else { + r.smcup() + r.csi("H") + } + r.flush() + } +} + +func (r *LightRenderer) Resume(clear bool, sigcont bool) { + r.setupTerminal() + if clear { + if r.fullscreen { + r.smcup() + } else { + r.rmcup() + } + r.flush() + } else if sigcont && !r.fullscreen && r.mouse { + // NOTE: SIGCONT (Coming back from CTRL-Z): + // It's highly likely that the offset we obtained at the beginning is + // no longer correct, so we simply disable mouse input. + r.csi("?1000l") + r.mouse = false + } +} + +func (r *LightRenderer) Clear() { + if r.fullscreen { + r.csi("H") + } + // r.csi("u") + r.origin() + r.csi("J") + r.flush() +} + +func (r *LightRenderer) RefreshWindows(windows []Window) { + r.flush() +} + +func (r *LightRenderer) Refresh() { + r.updateTerminalSize() +} + +func (r *LightRenderer) Close() { + // r.csi("u") + if r.clearOnExit { + if r.fullscreen { + r.rmcup() + } else { + r.origin() + if r.upOneLine { + r.csi("A") + } + r.csi("J") + } + } else if !r.fullscreen { + r.csi("u") + } + if r.mouse { + r.csi("?1000l") + } + r.flush() + r.closePlatform() + r.restoreTerminal() +} + +func (r *LightRenderer) MaxX() int { + return r.width +} + +func (r *LightRenderer) MaxY() int { + return r.height +} + +func (r *LightRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window { + w := &LightWindow{ + renderer: r, + colored: r.theme.Colored, + preview: preview, + border: borderStyle, + top: top, + left: left, + width: width, + height: height, + tabstop: r.tabstop, + fg: colDefault, + bg: colDefault} + if preview { + w.fg = r.theme.PreviewFg.Color + w.bg = r.theme.PreviewBg.Color + } else { + w.fg = r.theme.Fg.Color + w.bg = r.theme.Bg.Color + } + w.drawBorder() + return w +} + +func (w *LightWindow) drawBorder() { + switch w.border.shape { + case BorderRounded, BorderSharp: + w.drawBorderAround() + case BorderHorizontal: + w.drawBorderHorizontal(true, true) + case BorderVertical: + w.drawBorderVertical(true, true) + case BorderTop: + w.drawBorderHorizontal(true, false) + case BorderBottom: + w.drawBorderHorizontal(false, true) + case BorderLeft: + w.drawBorderVertical(true, false) + case BorderRight: + w.drawBorderVertical(false, true) + } +} + +func (w *LightWindow) drawBorderHorizontal(top, bottom bool) { + color := ColBorder + if w.preview { + color = ColPreviewBorder + } + if top { + w.Move(0, 0) + w.CPrint(color, repeat(w.border.horizontal, w.width)) + } + if bottom { + w.Move(w.height-1, 0) + w.CPrint(color, repeat(w.border.horizontal, w.width)) + } +} + +func (w *LightWindow) drawBorderVertical(left, right bool) { + width := w.width - 2 + if !left || !right { + width++ + } + color := ColBorder + if w.preview { + color = ColPreviewBorder + } + for y := 0; y < w.height; y++ { + w.Move(y, 0) + if left { + w.CPrint(color, string(w.border.vertical)) + } + w.CPrint(color, repeat(' ', width)) + if right { + w.CPrint(color, string(w.border.vertical)) + } + } +} + +func (w *LightWindow) drawBorderAround() { + w.Move(0, 0) + color := ColBorder + if w.preview { + color = ColPreviewBorder + } + w.CPrint(color, string(w.border.topLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.topRight)) + for y := 1; y < w.height-1; y++ { + w.Move(y, 0) + w.CPrint(color, string(w.border.vertical)) + w.CPrint(color, repeat(' ', w.width-2)) + w.CPrint(color, string(w.border.vertical)) + } + w.Move(w.height-1, 0) + w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.bottomRight)) +} + +func (w *LightWindow) csi(code string) { + w.renderer.csi(code) +} + +func (w *LightWindow) stderrInternal(str string, allowNLCR bool) { + w.renderer.stderrInternal(str, allowNLCR) +} + +func (w *LightWindow) Top() int { + return w.top +} + +func (w *LightWindow) Left() int { + return w.left +} + +func (w *LightWindow) Width() int { + return w.width +} + +func (w *LightWindow) Height() int { + return w.height +} + +func (w *LightWindow) Refresh() { +} + +func (w *LightWindow) Close() { +} + +func (w *LightWindow) X() int { + return w.posx +} + +func (w *LightWindow) Y() int { + return w.posy +} + +func (w *LightWindow) Enclose(y int, x int) bool { + return x >= w.left && x < (w.left+w.width) && + y >= w.top && y < (w.top+w.height) +} + +func (w *LightWindow) Move(y int, x int) { + w.posx = x + w.posy = y + + w.renderer.move(w.Top()+y, w.Left()+x) +} + +func (w *LightWindow) MoveAndClear(y int, x int) { + w.Move(y, x) + // We should not delete preview window on the right + // csi("K") + w.Print(repeat(' ', w.width-x)) + w.Move(y, x) +} + +func attrCodes(attr Attr) []string { + codes := []string{} + if (attr & AttrClear) > 0 { + return codes + } + if (attr & Bold) > 0 { + codes = append(codes, "1") + } + if (attr & Dim) > 0 { + codes = append(codes, "2") + } + if (attr & Italic) > 0 { + codes = append(codes, "3") + } + if (attr & Underline) > 0 { + codes = append(codes, "4") + } + if (attr & Blink) > 0 { + codes = append(codes, "5") + } + if (attr & Reverse) > 0 { + codes = append(codes, "7") + } + return codes +} + +func colorCodes(fg Color, bg Color) []string { + codes := []string{} + appendCode := func(c Color, offset int) { + if c == colDefault { + return + } + if c.is24() { + r := (c >> 16) & 0xff + g := (c >> 8) & 0xff + b := (c) & 0xff + codes = append(codes, fmt.Sprintf("%d;2;%d;%d;%d", 38+offset, r, g, b)) + } else if c >= colBlack && c <= colWhite { + codes = append(codes, fmt.Sprintf("%d", int(c)+30+offset)) + } else if c > colWhite && c < 16 { + codes = append(codes, fmt.Sprintf("%d", int(c)+90+offset-8)) + } else if c >= 16 && c < 256 { + codes = append(codes, fmt.Sprintf("%d;5;%d", 38+offset, c)) + } + } + appendCode(fg, 0) + appendCode(bg, 10) + return codes +} + +func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) bool { + codes := append(attrCodes(attr), colorCodes(fg, bg)...) + w.csi(";" + strings.Join(codes, ";") + "m") + return len(codes) > 0 +} + +func (w *LightWindow) Print(text string) { + w.cprint2(colDefault, w.bg, AttrRegular, text) +} + +func cleanse(str string) string { + return strings.Replace(str, "\x1b", "", -1) +} + +func (w *LightWindow) CPrint(pair ColorPair, text string) { + w.csiColor(pair.Fg(), pair.Bg(), pair.Attr()) + w.stderrInternal(cleanse(text), false) + w.csi("m") +} + +func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) { + if w.csiColor(fg, bg, attr) { + defer w.csi("m") + } + w.stderrInternal(cleanse(text), false) +} + +type wrappedLine struct { + text string + displayWidth int +} + +func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLine { + lines := []wrappedLine{} + width := 0 + line := "" + gr := uniseg.NewGraphemes(input) + for gr.Next() { + rs := gr.Runes() + str := string(rs) + var w int + if len(rs) == 1 && rs[0] == '\t' { + w = tabstop - (prefixLength+width)%tabstop + str = repeat(' ', w) + } else { + w = runewidth.StringWidth(str) + } + width += w + + if prefixLength+width <= max { + line += str + } else { + lines = append(lines, wrappedLine{string(line), width - w}) + line = str + prefixLength = 0 + width = w + } + } + lines = append(lines, wrappedLine{string(line), width}) + return lines +} + +func (w *LightWindow) fill(str string, onMove func()) FillReturn { + allLines := strings.Split(str, "\n") + for i, line := range allLines { + lines := wrapLine(line, w.posx, w.width, w.tabstop) + for j, wl := range lines { + w.stderrInternal(wl.text, false) + w.posx += wl.displayWidth + + // Wrap line + if j < len(lines)-1 || i < len(allLines)-1 { + if w.posy+1 >= w.height { + return FillSuspend + } + w.MoveAndClear(w.posy, w.posx) + w.Move(w.posy+1, 0) + onMove() + } + } + } + if w.posx+1 >= w.Width() { + if w.posy+1 >= w.height { + return FillSuspend + } + w.Move(w.posy+1, 0) + onMove() + return FillNextLine + } + return FillContinue +} + +func (w *LightWindow) setBg() { + if w.bg != colDefault { + w.csiColor(colDefault, w.bg, AttrRegular) + } +} + +func (w *LightWindow) Fill(text string) FillReturn { + w.Move(w.posy, w.posx) + w.setBg() + return w.fill(text, w.setBg) +} + +func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn { + w.Move(w.posy, w.posx) + if fg == colDefault { + fg = w.fg + } + if bg == colDefault { + bg = w.bg + } + if w.csiColor(fg, bg, attr) { + defer w.csi("m") + return w.fill(text, func() { w.csiColor(fg, bg, attr) }) + } + return w.fill(text, w.setBg) +} + +func (w *LightWindow) FinishFill() { + w.MoveAndClear(w.posy, w.posx) + for y := w.posy + 1; y < w.height; y++ { + w.MoveAndClear(y, 0) + } +} + +func (w *LightWindow) Erase() { + w.drawBorder() + // We don't erase the window here to avoid flickering during scroll + w.Move(0, 0) +} diff --git a/fzf/fzf/src/tui/light_unix.go b/fzf/fzf/src/tui/light_unix.go new file mode 100644 index 0000000..936a13e --- /dev/null +++ b/fzf/fzf/src/tui/light_unix.go @@ -0,0 +1,110 @@ +// +build !windows + +package tui + +import ( + "fmt" + "os" + "os/exec" + "strings" + "syscall" + + "github.com/junegunn/fzf/src/util" + "golang.org/x/term" +) + +func IsLightRendererSupported() bool { + return true +} + +func (r *LightRenderer) defaultTheme() *ColorTheme { + if strings.Contains(os.Getenv("TERM"), "256") { + return Dark256 + } + colors, err := exec.Command("tput", "colors").Output() + if err == nil && atoi(strings.TrimSpace(string(colors)), 16) > 16 { + return Dark256 + } + return Default16 +} + +func (r *LightRenderer) fd() int { + return int(r.ttyin.Fd()) +} + +func (r *LightRenderer) initPlatform() error { + fd := r.fd() + origState, err := term.GetState(fd) + if err != nil { + return err + } + r.origState = origState + term.MakeRaw(fd) + return nil +} + +func (r *LightRenderer) closePlatform() { + // NOOP +} + +func openTtyIn() *os.File { + in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) + if err != nil { + tty := ttyname() + if len(tty) > 0 { + if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil { + return in + } + } + fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice) + os.Exit(2) + } + return in +} + +func (r *LightRenderer) setupTerminal() { + term.MakeRaw(r.fd()) +} + +func (r *LightRenderer) restoreTerminal() { + term.Restore(r.fd(), r.origState) +} + +func (r *LightRenderer) updateTerminalSize() { + width, height, err := term.GetSize(r.fd()) + + if err == nil { + r.width = width + r.height = r.maxHeightFunc(height) + } else { + r.width = getEnv("COLUMNS", defaultWidth) + r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight)) + } +} + +func (r *LightRenderer) findOffset() (row int, col int) { + r.csi("6n") + r.flush() + bytes := []byte{} + for tries := 0; tries < offsetPollTries; tries++ { + bytes = r.getBytesInternal(bytes, tries > 0) + offsets := offsetRegexp.FindSubmatch(bytes) + if len(offsets) > 3 { + // Add anything we skipped over to the input buffer + r.buffer = append(r.buffer, offsets[1]...) + return atoi(string(offsets[2]), 0) - 1, atoi(string(offsets[3]), 0) - 1 + } + } + return -1, -1 +} + +func (r *LightRenderer) getch(nonblock bool) (int, bool) { + b := make([]byte, 1) + fd := r.fd() + util.SetNonblock(r.ttyin, nonblock) + _, err := util.Read(fd, b) + if err != nil { + return 0, false + } + return int(b[0]), true +} diff --git a/fzf/fzf/src/tui/light_windows.go b/fzf/fzf/src/tui/light_windows.go new file mode 100644 index 0000000..875bf6f --- /dev/null +++ b/fzf/fzf/src/tui/light_windows.go @@ -0,0 +1,145 @@ +//+build windows + +package tui + +import ( + "os" + "syscall" + "time" + + "github.com/junegunn/fzf/src/util" + "golang.org/x/sys/windows" +) + +const ( + timeoutInterval = 10 +) + +var ( + consoleFlagsInput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_EXTENDED_FLAGS) + consoleFlagsOutput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING | windows.ENABLE_PROCESSED_OUTPUT | windows.DISABLE_NEWLINE_AUTO_RETURN) +) + +// IsLightRendererSupported checks to see if the Light renderer is supported +func IsLightRendererSupported() bool { + var oldState uint32 + // enable vt100 emulation (https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences) + if windows.GetConsoleMode(windows.Stderr, &oldState) != nil { + return false + } + // attempt to set mode to determine if we support VT 100 codes. This will work on newer Windows 10 + // version: + canSetVt100 := windows.SetConsoleMode(windows.Stderr, oldState|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) == nil + var checkState uint32 + if windows.GetConsoleMode(windows.Stderr, &checkState) != nil || + (checkState&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING { + return false + } + windows.SetConsoleMode(windows.Stderr, oldState) + return canSetVt100 +} + +func (r *LightRenderer) defaultTheme() *ColorTheme { + // the getenv check is borrowed from here: https://github.com/gdamore/tcell/commit/0c473b86d82f68226a142e96cc5a34c5a29b3690#diff-b008fcd5e6934bf31bc3d33bf49f47d8R178: + if !IsLightRendererSupported() || os.Getenv("ConEmuPID") != "" || os.Getenv("TCELL_TRUECOLOR") == "disable" { + return Default16 + } + return Dark256 +} + +func (r *LightRenderer) initPlatform() error { + //outHandle := windows.Stdout + outHandle, _ := syscall.Open("CONOUT$", syscall.O_RDWR, 0) + // enable vt100 emulation (https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences) + if err := windows.GetConsoleMode(windows.Handle(outHandle), &r.origStateOutput); err != nil { + return err + } + r.outHandle = uintptr(outHandle) + inHandle, _ := syscall.Open("CONIN$", syscall.O_RDWR, 0) + if err := windows.GetConsoleMode(windows.Handle(inHandle), &r.origStateInput); err != nil { + return err + } + r.inHandle = uintptr(inHandle) + + r.setupTerminal() + + // channel for non-blocking reads. Buffer to make sure + // we get the ESC sets: + r.ttyinChannel = make(chan byte, 1024) + + // the following allows for non-blocking IO. + // syscall.SetNonblock() is a NOOP under Windows. + go func() { + fd := int(r.inHandle) + b := make([]byte, 1) + for { + // HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT. + _ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput) + + _, err := util.Read(fd, b) + if err == nil { + r.ttyinChannel <- b[0] + } + } + }() + + return nil +} + +func (r *LightRenderer) closePlatform() { + windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput) + windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput) +} + +func openTtyIn() *os.File { + // not used + return nil +} + +func (r *LightRenderer) setupTerminal() error { + if err := windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput); err != nil { + return err + } + return windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput) +} + +func (r *LightRenderer) restoreTerminal() error { + if err := windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput); err != nil { + return err + } + return windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput) +} + +func (r *LightRenderer) updateTerminalSize() { + var bufferInfo windows.ConsoleScreenBufferInfo + if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil { + r.width = getEnv("COLUMNS", defaultWidth) + r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight)) + + } else { + r.width = int(bufferInfo.Window.Right - bufferInfo.Window.Left) + r.height = r.maxHeightFunc(int(bufferInfo.Window.Bottom - bufferInfo.Window.Top)) + } +} + +func (r *LightRenderer) findOffset() (row int, col int) { + var bufferInfo windows.ConsoleScreenBufferInfo + if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil { + return -1, -1 + } + return int(bufferInfo.CursorPosition.X), int(bufferInfo.CursorPosition.Y) +} + +func (r *LightRenderer) getch(nonblock bool) (int, bool) { + if nonblock { + select { + case bc := <-r.ttyinChannel: + return int(bc), true + case <-time.After(timeoutInterval * time.Millisecond): + return 0, false + } + } else { + bc := <-r.ttyinChannel + return int(bc), true + } +} diff --git a/fzf/fzf/src/tui/tcell.go b/fzf/fzf/src/tui/tcell.go new file mode 100644 index 0000000..82c7566 --- /dev/null +++ b/fzf/fzf/src/tui/tcell.go @@ -0,0 +1,721 @@ +// +build tcell windows + +package tui + +import ( + "os" + "time" + + "runtime" + + "github.com/gdamore/tcell" + "github.com/gdamore/tcell/encoding" + + "github.com/mattn/go-runewidth" + "github.com/rivo/uniseg" +) + +func HasFullscreenRenderer() bool { + return true +} + +func (p ColorPair) style() tcell.Style { + style := tcell.StyleDefault + return style.Foreground(tcell.Color(p.Fg())).Background(tcell.Color(p.Bg())) +} + +type Attr tcell.Style + +type TcellWindow struct { + color bool + preview bool + top int + left int + width int + height int + normal ColorPair + lastX int + lastY int + moveCursor bool + borderStyle BorderStyle +} + +func (w *TcellWindow) Top() int { + return w.top +} + +func (w *TcellWindow) Left() int { + return w.left +} + +func (w *TcellWindow) Width() int { + return w.width +} + +func (w *TcellWindow) Height() int { + return w.height +} + +func (w *TcellWindow) Refresh() { + if w.moveCursor { + _screen.ShowCursor(w.left+w.lastX, w.top+w.lastY) + w.moveCursor = false + } + w.lastX = 0 + w.lastY = 0 + + w.drawBorder() +} + +func (w *TcellWindow) FinishFill() { + // NO-OP +} + +const ( + Bold Attr = Attr(tcell.AttrBold) + Dim = Attr(tcell.AttrDim) + Blink = Attr(tcell.AttrBlink) + Reverse = Attr(tcell.AttrReverse) + Underline = Attr(tcell.AttrUnderline) + Italic = Attr(tcell.AttrItalic) +) + +const ( + AttrUndefined = Attr(0) + AttrRegular = Attr(1 << 7) + AttrClear = Attr(1 << 8) +) + +func (r *FullscreenRenderer) defaultTheme() *ColorTheme { + if _screen.Colors() >= 256 { + return Dark256 + } + return Default16 +} + +var ( + _colorToAttribute = []tcell.Color{ + tcell.ColorBlack, + tcell.ColorRed, + tcell.ColorGreen, + tcell.ColorYellow, + tcell.ColorBlue, + tcell.ColorDarkMagenta, + tcell.ColorLightCyan, + tcell.ColorWhite, + } +) + +func (c Color) Style() tcell.Color { + if c <= colDefault { + return tcell.ColorDefault + } else if c >= colBlack && c <= colWhite { + return _colorToAttribute[int(c)] + } else { + return tcell.Color(c) + } +} + +func (a Attr) Merge(b Attr) Attr { + return a | b +} + +// handle the following as private members of FullscreenRenderer instance +// they are declared here to prevent introducing tcell library in non-windows builds +var ( + _screen tcell.Screen + _prevMouseButton tcell.ButtonMask +) + +func (r *FullscreenRenderer) initScreen() { + s, e := tcell.NewScreen() + if e != nil { + errorExit(e.Error()) + } + if e = s.Init(); e != nil { + errorExit(e.Error()) + } + if r.mouse { + s.EnableMouse() + } else { + s.DisableMouse() + } + _screen = s +} + +func (r *FullscreenRenderer) Init() { + if os.Getenv("TERM") == "cygwin" { + os.Setenv("TERM", "") + } + encoding.Register() + + r.initScreen() + initTheme(r.theme, r.defaultTheme(), r.forceBlack) +} + +func (r *FullscreenRenderer) MaxX() int { + ncols, _ := _screen.Size() + return int(ncols) +} + +func (r *FullscreenRenderer) MaxY() int { + _, nlines := _screen.Size() + return int(nlines) +} + +func (w *TcellWindow) X() int { + return w.lastX +} + +func (w *TcellWindow) Y() int { + return w.lastY +} + +func (r *FullscreenRenderer) Clear() { + _screen.Sync() + _screen.Clear() +} + +func (r *FullscreenRenderer) Refresh() { + // noop +} + +func (r *FullscreenRenderer) GetChar() Event { + ev := _screen.PollEvent() + switch ev := ev.(type) { + case *tcell.EventResize: + return Event{Resize, 0, nil} + + // process mouse events: + case *tcell.EventMouse: + // mouse down events have zeroed buttons, so we can't use them + // mouse up event consists of two events, 1. (main) event with modifier and other metadata, 2. event with zeroed buttons + // so mouse click is three consecutive events, but the first and last are indistinguishable from movement events (with released buttons) + // dragging has same structure, it only repeats the middle (main) event appropriately + x, y := ev.Position() + mod := ev.Modifiers() != 0 + + // since we dont have mouse down events (unlike LightRenderer), we need to track state in prevButton + prevButton, button := _prevMouseButton, ev.Buttons() + _prevMouseButton = button + drag := prevButton == button + + switch { + case button&tcell.WheelDown != 0: + return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}} + case button&tcell.WheelUp != 0: + return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}} + case button&tcell.Button1 != 0 && !drag: + // all potential double click events put their 'line' coordinate in the clickY array + // double click event has two conditions, temporal and spatial, the first is checked here + now := time.Now() + if now.Sub(r.prevDownTime) < doubleClickDuration { + r.clickY = append(r.clickY, y) + } else { + r.clickY = []int{y} + } + r.prevDownTime = now + + // detect double clicks (also check for spatial condition) + n := len(r.clickY) + double := n > 1 && r.clickY[n-2] == r.clickY[n-1] + if double { + // make sure two consecutive double clicks require four clicks + r.clickY = []int{} + } + + // fire single or double click event + return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, mod}} + case button&tcell.Button2 != 0 && !drag: + return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, mod}} + case runtime.GOOS != "windows": + + // double and single taps on Windows don't quite work due to + // the console acting on the events and not allowing us + // to consume them. + + left := button&tcell.Button1 != 0 + down := left || button&tcell.Button3 != 0 + double := false + if down { + now := time.Now() + if !left { + r.clickY = []int{} + } else if now.Sub(r.prevDownTime) < doubleClickDuration { + r.clickY = append(r.clickY, x) + } else { + r.clickY = []int{x} + r.prevDownTime = now + } + } else { + if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] && + time.Now().Sub(r.prevDownTime) < doubleClickDuration { + double = true + } + } + + return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}} + } + + // process keyboard: + case *tcell.EventKey: + mods := ev.Modifiers() + none := mods == tcell.ModNone + alt := (mods & tcell.ModAlt) > 0 + ctrl := (mods & tcell.ModCtrl) > 0 + shift := (mods & tcell.ModShift) > 0 + ctrlAlt := ctrl && alt + altShift := alt && shift + + keyfn := func(r rune) Event { + if alt { + return CtrlAltKey(r) + } + return EventType(CtrlA.Int() - 'a' + int(r)).AsEvent() + } + switch ev.Key() { + // section 1: Ctrl+(Alt)+[a-z] + case tcell.KeyCtrlA: + return keyfn('a') + case tcell.KeyCtrlB: + return keyfn('b') + case tcell.KeyCtrlC: + return keyfn('c') + case tcell.KeyCtrlD: + return keyfn('d') + case tcell.KeyCtrlE: + return keyfn('e') + case tcell.KeyCtrlF: + return keyfn('f') + case tcell.KeyCtrlG: + return keyfn('g') + case tcell.KeyCtrlH: + switch ev.Rune() { + case 0: + if ctrl { + return Event{BSpace, 0, nil} + } + case rune(tcell.KeyCtrlH): + switch { + case ctrl: + return keyfn('h') + case alt: + return Event{AltBS, 0, nil} + case none, shift: + return Event{BSpace, 0, nil} + } + } + case tcell.KeyCtrlI: + return keyfn('i') + case tcell.KeyCtrlJ: + return keyfn('j') + case tcell.KeyCtrlK: + return keyfn('k') + case tcell.KeyCtrlL: + return keyfn('l') + case tcell.KeyCtrlM: + return keyfn('m') + case tcell.KeyCtrlN: + return keyfn('n') + case tcell.KeyCtrlO: + return keyfn('o') + case tcell.KeyCtrlP: + return keyfn('p') + case tcell.KeyCtrlQ: + return keyfn('q') + case tcell.KeyCtrlR: + return keyfn('r') + case tcell.KeyCtrlS: + return keyfn('s') + case tcell.KeyCtrlT: + return keyfn('t') + case tcell.KeyCtrlU: + return keyfn('u') + case tcell.KeyCtrlV: + return keyfn('v') + case tcell.KeyCtrlW: + return keyfn('w') + case tcell.KeyCtrlX: + return keyfn('x') + case tcell.KeyCtrlY: + return keyfn('y') + case tcell.KeyCtrlZ: + return keyfn('z') + // section 2: Ctrl+[ \]_] + case tcell.KeyCtrlSpace: + return Event{CtrlSpace, 0, nil} + case tcell.KeyCtrlBackslash: + return Event{CtrlBackSlash, 0, nil} + case tcell.KeyCtrlRightSq: + return Event{CtrlRightBracket, 0, nil} + case tcell.KeyCtrlCarat: + return Event{CtrlCaret, 0, nil} + case tcell.KeyCtrlUnderscore: + return Event{CtrlSlash, 0, nil} + // section 3: (Alt)+Backspace2 + case tcell.KeyBackspace2: + if alt { + return Event{AltBS, 0, nil} + } + return Event{BSpace, 0, nil} + + // section 4: (Alt+Shift)+Key(Up|Down|Left|Right) + case tcell.KeyUp: + if altShift { + return Event{AltSUp, 0, nil} + } + if shift { + return Event{SUp, 0, nil} + } + if alt { + return Event{AltUp, 0, nil} + } + return Event{Up, 0, nil} + case tcell.KeyDown: + if altShift { + return Event{AltSDown, 0, nil} + } + if shift { + return Event{SDown, 0, nil} + } + if alt { + return Event{AltDown, 0, nil} + } + return Event{Down, 0, nil} + case tcell.KeyLeft: + if altShift { + return Event{AltSLeft, 0, nil} + } + if shift { + return Event{SLeft, 0, nil} + } + if alt { + return Event{AltLeft, 0, nil} + } + return Event{Left, 0, nil} + case tcell.KeyRight: + if altShift { + return Event{AltSRight, 0, nil} + } + if shift { + return Event{SRight, 0, nil} + } + if alt { + return Event{AltRight, 0, nil} + } + return Event{Right, 0, nil} + + // section 5: (Insert|Home|Delete|End|PgUp|PgDn|BackTab|F1-F12) + case tcell.KeyInsert: + return Event{Insert, 0, nil} + case tcell.KeyHome: + return Event{Home, 0, nil} + case tcell.KeyDelete: + return Event{Del, 0, nil} + case tcell.KeyEnd: + return Event{End, 0, nil} + case tcell.KeyPgUp: + return Event{PgUp, 0, nil} + case tcell.KeyPgDn: + return Event{PgDn, 0, nil} + case tcell.KeyBacktab: + return Event{BTab, 0, nil} + case tcell.KeyF1: + return Event{F1, 0, nil} + case tcell.KeyF2: + return Event{F2, 0, nil} + case tcell.KeyF3: + return Event{F3, 0, nil} + case tcell.KeyF4: + return Event{F4, 0, nil} + case tcell.KeyF5: + return Event{F5, 0, nil} + case tcell.KeyF6: + return Event{F6, 0, nil} + case tcell.KeyF7: + return Event{F7, 0, nil} + case tcell.KeyF8: + return Event{F8, 0, nil} + case tcell.KeyF9: + return Event{F9, 0, nil} + case tcell.KeyF10: + return Event{F10, 0, nil} + case tcell.KeyF11: + return Event{F11, 0, nil} + case tcell.KeyF12: + return Event{F12, 0, nil} + + // section 6: (Ctrl+Alt)+'rune' + case tcell.KeyRune: + r := ev.Rune() + + switch { + // translate native key events to ascii control characters + case r == ' ' && ctrl: + return Event{CtrlSpace, 0, nil} + // handle AltGr characters + case ctrlAlt: + return Event{Rune, r, nil} // dropping modifiers + // simple characters (possibly with modifier) + case alt: + return AltKey(r) + default: + return Event{Rune, r, nil} + } + + // section 7: Esc + case tcell.KeyEsc: + return Event{ESC, 0, nil} + } + } + + // section 8: Invalid + return Event{Invalid, 0, nil} +} + +func (r *FullscreenRenderer) Pause(clear bool) { + if clear { + _screen.Fini() + } +} + +func (r *FullscreenRenderer) Resume(clear bool, sigcont bool) { + if clear { + r.initScreen() + } +} + +func (r *FullscreenRenderer) Close() { + _screen.Fini() +} + +func (r *FullscreenRenderer) RefreshWindows(windows []Window) { + // TODO + for _, w := range windows { + w.Refresh() + } + _screen.Show() +} + +func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window { + normal := ColNormal + if preview { + normal = ColPreview + } + return &TcellWindow{ + color: r.theme.Colored, + preview: preview, + top: top, + left: left, + width: width, + height: height, + normal: normal, + borderStyle: borderStyle} +} + +func (w *TcellWindow) Close() { + // TODO +} + +func fill(x, y, w, h int, n ColorPair, r rune) { + for ly := 0; ly <= h; ly++ { + for lx := 0; lx <= w; lx++ { + _screen.SetContent(x+lx, y+ly, r, nil, n.style()) + } + } +} + +func (w *TcellWindow) Erase() { + fill(w.left-1, w.top, w.width+1, w.height, w.normal, ' ') +} + +func (w *TcellWindow) Enclose(y int, x int) bool { + return x >= w.left && x < (w.left+w.width) && + y >= w.top && y < (w.top+w.height) +} + +func (w *TcellWindow) Move(y int, x int) { + w.lastX = x + w.lastY = y + w.moveCursor = true +} + +func (w *TcellWindow) MoveAndClear(y int, x int) { + w.Move(y, x) + for i := w.lastX; i < w.width; i++ { + _screen.SetContent(i+w.left, w.lastY+w.top, rune(' '), nil, w.normal.style()) + } + w.lastX = x +} + +func (w *TcellWindow) Print(text string) { + w.printString(text, w.normal) +} + +func (w *TcellWindow) printString(text string, pair ColorPair) { + lx := 0 + a := pair.Attr() + + style := pair.style() + if a&AttrClear == 0 { + style = style. + Reverse(a&Attr(tcell.AttrReverse) != 0). + Underline(a&Attr(tcell.AttrUnderline) != 0). + Italic(a&Attr(tcell.AttrItalic) != 0). + Blink(a&Attr(tcell.AttrBlink) != 0). + Dim(a&Attr(tcell.AttrDim) != 0) + } + + gr := uniseg.NewGraphemes(text) + for gr.Next() { + rs := gr.Runes() + + if len(rs) == 1 { + r := rs[0] + if r < rune(' ') { // ignore control characters + continue + } else if r == '\n' { + w.lastY++ + lx = 0 + continue + } else if r == '\u000D' { // skip carriage return + continue + } + } + var xPos = w.left + w.lastX + lx + var yPos = w.top + w.lastY + if xPos < (w.left+w.width) && yPos < (w.top+w.height) { + _screen.SetContent(xPos, yPos, rs[0], rs[1:], style) + } + lx += runewidth.StringWidth(string(rs)) + } + w.lastX += lx +} + +func (w *TcellWindow) CPrint(pair ColorPair, text string) { + w.printString(text, pair) +} + +func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn { + lx := 0 + a := pair.Attr() + + var style tcell.Style + if w.color { + style = pair.style() + } else { + style = w.normal.style() + } + style = style. + Blink(a&Attr(tcell.AttrBlink) != 0). + Bold(a&Attr(tcell.AttrBold) != 0). + Dim(a&Attr(tcell.AttrDim) != 0). + Reverse(a&Attr(tcell.AttrReverse) != 0). + Underline(a&Attr(tcell.AttrUnderline) != 0). + Italic(a&Attr(tcell.AttrItalic) != 0) + + gr := uniseg.NewGraphemes(text) + for gr.Next() { + rs := gr.Runes() + if len(rs) == 1 && rs[0] == '\n' { + w.lastY++ + w.lastX = 0 + lx = 0 + continue + } + + // word wrap: + xPos := w.left + w.lastX + lx + if xPos >= (w.left + w.width) { + w.lastY++ + w.lastX = 0 + lx = 0 + xPos = w.left + } + + yPos := w.top + w.lastY + if yPos >= (w.top + w.height) { + return FillSuspend + } + + _screen.SetContent(xPos, yPos, rs[0], rs[1:], style) + lx += runewidth.StringWidth(string(rs)) + } + w.lastX += lx + if w.lastX == w.width { + w.lastY++ + w.lastX = 0 + return FillNextLine + } + + return FillContinue +} + +func (w *TcellWindow) Fill(str string) FillReturn { + return w.fillString(str, w.normal) +} + +func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn { + if fg == colDefault { + fg = w.normal.Fg() + } + if bg == colDefault { + bg = w.normal.Bg() + } + return w.fillString(str, NewColorPair(fg, bg, a)) +} + +func (w *TcellWindow) drawBorder() { + shape := w.borderStyle.shape + if shape == BorderNone { + return + } + + left := w.left + right := left + w.width + top := w.top + bot := top + w.height + + var style tcell.Style + if w.color { + if w.preview { + style = ColPreviewBorder.style() + } else { + style = ColBorder.style() + } + } else { + style = w.normal.style() + } + + switch shape { + case BorderRounded, BorderSharp, BorderHorizontal, BorderTop: + for x := left; x < right; x++ { + _screen.SetContent(x, top, w.borderStyle.horizontal, nil, style) + } + } + switch shape { + case BorderRounded, BorderSharp, BorderHorizontal, BorderBottom: + for x := left; x < right; x++ { + _screen.SetContent(x, bot-1, w.borderStyle.horizontal, nil, style) + } + } + switch shape { + case BorderRounded, BorderSharp, BorderVertical, BorderLeft: + for y := top; y < bot; y++ { + _screen.SetContent(left, y, w.borderStyle.vertical, nil, style) + } + } + switch shape { + case BorderRounded, BorderSharp, BorderVertical, BorderRight: + for y := top; y < bot; y++ { + _screen.SetContent(right-1, y, w.borderStyle.vertical, nil, style) + } + } + switch shape { + case BorderRounded, BorderSharp: + _screen.SetContent(left, top, w.borderStyle.topLeft, nil, style) + _screen.SetContent(right-1, top, w.borderStyle.topRight, nil, style) + _screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style) + _screen.SetContent(right-1, bot-1, w.borderStyle.bottomRight, nil, style) + } +} diff --git a/fzf/fzf/src/tui/tcell_test.go b/fzf/fzf/src/tui/tcell_test.go new file mode 100644 index 0000000..aa63b72 --- /dev/null +++ b/fzf/fzf/src/tui/tcell_test.go @@ -0,0 +1,392 @@ +// +build tcell windows + +package tui + +import ( + "testing" + + "github.com/gdamore/tcell" + "github.com/junegunn/fzf/src/util" +) + +func assert(t *testing.T, context string, got interface{}, want interface{}) bool { + if got == want { + return true + } else { + t.Errorf("%s = (%T)%v, want (%T)%v", context, got, got, want, want) + return false + } +} + +// Test the handling of the tcell keyboard events. +func TestGetCharEventKey(t *testing.T) { + if util.ToTty() { + // This test is skipped when output goes to terminal, because it causes + // some glitches: + // - output lines may not start at the beginning of a row which makes + // the output unreadable + // - terminal may get cleared which prevents you from seeing results of + // previous tests + // Good ways to prevent the glitches are piping the output to a pager + // or redirecting to a file. I've found `less +G` to be trouble-free. + t.Skip("Skipped because this test misbehaves in terminal, pipe to a pager or redirect output to a file to run it safely.") + } else if testing.Verbose() { + // I have observed a behaviour when this test outputted more than 8192 + // bytes (32*256) into the 'less' pager, both the go's test executable + // and the pager hanged. The go's executable was blocking on printing. + // I was able to create minimal working example of that behaviour, but + // that example hanged after 12256 bytes (32*(256+127)). + t.Log("If you are piping this test to a pager and it hangs, make the pager greedy for input, e.g. 'less +G'.") + } + + if !HasFullscreenRenderer() { + t.Skip("Can't test FullscreenRenderer.") + } + + // construct test cases + type giveKey struct { + Type tcell.Key + Char rune + Mods tcell.ModMask + } + type wantKey = Event + type testCase struct { + giveKey + wantKey + } + /* + Some test cases are marked "fabricated". It means that giveKey value + is valid, but it is not what you get when you press the keys. For + example Ctrl+C will NOT give you tcell.KeyCtrlC, but tcell.KeyETX + (End-Of-Text character, causing SIGINT). + I was trying to accompany the fabricated test cases with real ones. + + Some test cases are marked "unhandled". It means that giveKey.Type + is not present in tcell.go source code. It can still be handled via + implicit or explicit alias. + + If not said otherwise, test cases are for US keyboard. + + (tabstop=44) + */ + tests := []testCase{ + + // section 1: Ctrl+(Alt)+[a-z] + {giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl}, wantKey{CtrlA, 0, nil}}, + {giveKey{tcell.KeyCtrlC, rune(tcell.KeyCtrlC), tcell.ModCtrl}, wantKey{CtrlC, 0, nil}}, // fabricated + {giveKey{tcell.KeyETX, rune(tcell.KeyETX), tcell.ModCtrl}, wantKey{CtrlC, 0, nil}}, // this is SIGINT (Ctrl+C) + {giveKey{tcell.KeyCtrlZ, rune(tcell.KeyCtrlZ), tcell.ModCtrl}, wantKey{CtrlZ, 0, nil}}, // fabricated + // KeyTab is alias for KeyTAB + {giveKey{tcell.KeyCtrlI, rune(tcell.KeyCtrlI), tcell.ModCtrl}, wantKey{Tab, 0, nil}}, // fabricated + {giveKey{tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, wantKey{Tab, 0, nil}}, // unhandled, actual "Tab" keystroke + {giveKey{tcell.KeyTAB, rune(tcell.KeyTAB), tcell.ModNone}, wantKey{Tab, 0, nil}}, // fabricated, unhandled + // KeyEnter is alias for KeyCR + {giveKey{tcell.KeyCtrlM, rune(tcell.KeyCtrlM), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // actual "Enter" keystroke + {giveKey{tcell.KeyCR, rune(tcell.KeyCR), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled + {giveKey{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled + // Ctrl+Alt keys + {giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'a', nil}}, // fabricated + {giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAlt, 'a', nil}}, // fabricated + + // section 2: Ctrl+[ \]_] + {giveKey{tcell.KeyCtrlSpace, rune(tcell.KeyCtrlSpace), tcell.ModCtrl}, wantKey{CtrlSpace, 0, nil}}, // fabricated + {giveKey{tcell.KeyNUL, rune(tcell.KeyNUL), tcell.ModNone}, wantKey{CtrlSpace, 0, nil}}, // fabricated, unhandled + {giveKey{tcell.KeyRune, ' ', tcell.ModCtrl}, wantKey{CtrlSpace, 0, nil}}, // actual Ctrl+' ' + {giveKey{tcell.KeyCtrlBackslash, rune(tcell.KeyCtrlBackslash), tcell.ModCtrl}, wantKey{CtrlBackSlash, 0, nil}}, + {giveKey{tcell.KeyCtrlRightSq, rune(tcell.KeyCtrlRightSq), tcell.ModCtrl}, wantKey{CtrlRightBracket, 0, nil}}, + {giveKey{tcell.KeyCtrlCarat, rune(tcell.KeyCtrlCarat), tcell.ModShift | tcell.ModCtrl}, wantKey{CtrlCaret, 0, nil}}, // fabricated + {giveKey{tcell.KeyRS, rune(tcell.KeyRS), tcell.ModShift | tcell.ModCtrl}, wantKey{CtrlCaret, 0, nil}}, // actual Ctrl+Shift+6 (i.e. Ctrl+^) keystroke + {giveKey{tcell.KeyCtrlUnderscore, rune(tcell.KeyCtrlUnderscore), tcell.ModShift | tcell.ModCtrl}, wantKey{CtrlSlash, 0, nil}}, + + // section 3: (Alt)+Backspace2 + // KeyBackspace2 is alias for KeyDEL = 0x7F (ASCII) (allegedly unused by Windows) + // KeyDelete = 0x2E (VK_DELETE constant in Windows) + // KeyBackspace is alias for KeyBS = 0x08 (ASCII) (implicit alias with KeyCtrlH) + {giveKey{tcell.KeyBackspace2, 0, tcell.ModNone}, wantKey{BSpace, 0, nil}}, // fabricated + {giveKey{tcell.KeyBackspace2, 0, tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // fabricated + {giveKey{tcell.KeyDEL, 0, tcell.ModNone}, wantKey{BSpace, 0, nil}}, // fabricated, unhandled + {giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Del, 0, nil}}, + {giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{Del, 0, nil}}, + {giveKey{tcell.KeyBackspace, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled + {giveKey{tcell.KeyBS, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled + {giveKey{tcell.KeyCtrlH, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled + {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModNone}, wantKey{BSpace, 0, nil}}, // actual "Backspace" keystroke + {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // actual "Alt+Backspace" keystroke + {giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Backspace" keystroke + {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Shift+Backspace" keystroke + {giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke + {giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Shift+Backspace" keystroke + {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift | tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // actual "Shift+Alt+Backspace" keystroke + {giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke + {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+H" keystroke + {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Alt+H" keystroke + {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+Shift+H" keystroke + {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Shift+Alt+H" keystroke + + // section 4: (Alt+Shift)+Key(Up|Down|Left|Right) + {giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}}, + {giveKey{tcell.KeyDown, 0, tcell.ModAlt}, wantKey{AltDown, 0, nil}}, + {giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{SLeft, 0, nil}}, + {giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltSRight, 0, nil}}, + {giveKey{tcell.KeyUpLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled + {giveKey{tcell.KeyUpRight, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled + {giveKey{tcell.KeyDownLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled + {giveKey{tcell.KeyDownRight, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled + {giveKey{tcell.KeyCenter, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled + // section 5: (Insert|Home|Delete|End|PgUp|PgDn|BackTab|F1-F12) + {giveKey{tcell.KeyInsert, 0, tcell.ModNone}, wantKey{Insert, 0, nil}}, + {giveKey{tcell.KeyF1, 0, tcell.ModNone}, wantKey{F1, 0, nil}}, + // section 6: (Ctrl+Alt)+'rune' + {giveKey{tcell.KeyRune, 'a', tcell.ModNone}, wantKey{Rune, 'a', nil}}, + {giveKey{tcell.KeyRune, 'a', tcell.ModCtrl}, wantKey{Rune, 'a', nil}}, // fabricated + {giveKey{tcell.KeyRune, 'a', tcell.ModAlt}, wantKey{Alt, 'a', nil}}, + {giveKey{tcell.KeyRune, 'A', tcell.ModAlt}, wantKey{Alt, 'A', nil}}, + {giveKey{tcell.KeyRune, '`', tcell.ModAlt}, wantKey{Alt, '`', nil}}, + /* + "Input method" in Windows Language options: + US: "US Keyboard" does not generate any characters (and thus any events) in Ctrl+Alt+[a-z] range + CS: "Czech keyboard" + DE: "German keyboard" + + Note that right Alt is not just `tcell.ModAlt` on foreign language keyboards, but it is the AltGr `tcell.ModCtrl|tcell.ModAlt`. + */ + {giveKey{tcell.KeyRune, '{', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '{', nil}}, // CS: Ctrl+Alt+b = "{" // Note that this does not interfere with CtrlB, since the "b" is replaced with "{" on OS level + {giveKey{tcell.KeyRune, '$', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '$', nil}}, // CS: Ctrl+Alt+ů = "$" + {giveKey{tcell.KeyRune, '~', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '~', nil}}, // CS: Ctrl+Alt++ = "~" + {giveKey{tcell.KeyRune, '`', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '`', nil}}, // CS: Ctrl+Alt+ý,Space = "`" // this is dead key, space is required to emit the char + + {giveKey{tcell.KeyRune, '{', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '{', nil}}, // DE: Ctrl+Alt+7 = "{" + {giveKey{tcell.KeyRune, '@', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '@', nil}}, // DE: Ctrl+Alt+q = "@" + {giveKey{tcell.KeyRune, 'µ', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, 'µ', nil}}, // DE: Ctrl+Alt+m = "µ" + + // section 7: Esc + // KeyEsc and KeyEscape are aliases for KeyESC + {giveKey{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone}, wantKey{ESC, 0, nil}}, // fabricated + {giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModNone}, wantKey{ESC, 0, nil}}, // unhandled + {giveKey{tcell.KeyEscape, rune(tcell.KeyEscape), tcell.ModNone}, wantKey{ESC, 0, nil}}, // fabricated, unhandled + {giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModCtrl}, wantKey{ESC, 0, nil}}, // actual Ctrl+[ keystroke + {giveKey{tcell.KeyCtrlLeftSq, rune(tcell.KeyCtrlLeftSq), tcell.ModCtrl}, wantKey{ESC, 0, nil}}, // fabricated, unhandled + + // section 8: Invalid + {giveKey{tcell.KeyRune, 'a', tcell.ModMeta}, wantKey{Rune, 'a', nil}}, // fabricated + {giveKey{tcell.KeyF24, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, + {giveKey{tcell.KeyHelp, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled + {giveKey{tcell.KeyExit, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled + {giveKey{tcell.KeyClear, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // unhandled, actual keystroke Numpad_5 with Numlock OFF + {giveKey{tcell.KeyCancel, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled + {giveKey{tcell.KeyPrint, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled + {giveKey{tcell.KeyPause, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // unhandled + + } + r := NewFullscreenRenderer(&ColorTheme{}, false, false) + r.Init() + + // run and evaluate the tests + for _, test := range tests { + // generate key event + giveEvent := tcell.NewEventKey(test.giveKey.Type, test.giveKey.Char, test.giveKey.Mods) + _screen.PostEventWait(giveEvent) + t.Logf("giveEvent = %T{key: %v, ch: %q (%[3]v), mod: %#04b}\n", giveEvent, giveEvent.Key(), giveEvent.Rune(), giveEvent.Modifiers()) + + // process the event in fzf and evaluate the test + gotEvent := r.GetChar() + // skip Resize events, those are sometimes put in the buffer outside of this test + for gotEvent.Type == Resize { + t.Logf("Resize swallowed") + gotEvent = r.GetChar() + } + t.Logf("wantEvent = %T{Type: %v, Char: %q (%[3]v)}\n", test.wantKey, test.wantKey.Type, test.wantKey.Char) + t.Logf("gotEvent = %T{Type: %v, Char: %q (%[3]v)}\n", gotEvent, gotEvent.Type, gotEvent.Char) + + assert(t, "r.GetChar().Type", gotEvent.Type, test.wantKey.Type) + assert(t, "r.GetChar().Char", gotEvent.Char, test.wantKey.Char) + } + + r.Close() +} + +/* +Quick reference +--------------- + +(tabstop=18) +(this is not mapping table, it merely puts multiple constants ranges in one table) + +¹) the two columns are each other implicit alias +²) explicit aliases here + +%v section # tcell ctrl key¹ tcell ctrl char¹ tcell alias² tui constants tcell named keys tcell mods +-- --------- -------------- --------------- ----------- ------------- ---------------- ---------- +0 2 KeyCtrlSpace KeyNUL = ^@ Rune ModNone +1 1 KeyCtrlA KeySOH = ^A CtrlA ModShift +2 1 KeyCtrlB KeySTX = ^B CtrlB ModCtrl +3 1 KeyCtrlC KeyETX = ^C CtrlC +4 1 KeyCtrlD KeyEOT = ^D CtrlD ModAlt +5 1 KeyCtrlE KeyENQ = ^E CtrlE +6 1 KeyCtrlF KeyACK = ^F CtrlF +7 1 KeyCtrlG KeyBEL = ^G CtrlG +8 1 KeyCtrlH KeyBS = ^H KeyBackspace CtrlH ModMeta +9 1 KeyCtrlI KeyTAB = ^I KeyTab Tab +10 1 KeyCtrlJ KeyLF = ^J CtrlJ +11 1 KeyCtrlK KeyVT = ^K CtrlK +12 1 KeyCtrlL KeyFF = ^L CtrlL +13 1 KeyCtrlM KeyCR = ^M KeyEnter CtrlM +14 1 KeyCtrlN KeySO = ^N CtrlN +15 1 KeyCtrlO KeySI = ^O CtrlO +16 1 KeyCtrlP KeyDLE = ^P CtrlP +17 1 KeyCtrlQ KeyDC1 = ^Q CtrlQ +18 1 KeyCtrlR KeyDC2 = ^R CtrlR +19 1 KeyCtrlS KeyDC3 = ^S CtrlS +20 1 KeyCtrlT KeyDC4 = ^T CtrlT +21 1 KeyCtrlU KeyNAK = ^U CtrlU +22 1 KeyCtrlV KeySYN = ^V CtrlV +23 1 KeyCtrlW KeyETB = ^W CtrlW +24 1 KeyCtrlX KeyCAN = ^X CtrlX +25 1 KeyCtrlY KeyEM = ^Y CtrlY +26 1 KeyCtrlZ KeySUB = ^Z CtrlZ +27 7 KeyCtrlLeftSq KeyESC = ^[ KeyEsc, KeyEscape ESC +28 2 KeyCtrlBackslash KeyFS = ^\ CtrlSpace +29 2 KeyCtrlRightSq KeyGS = ^] CtrlBackSlash +30 2 KeyCtrlCarat KeyRS = ^^ CtrlRightBracket +31 2 KeyCtrlUnderscore KeyUS = ^_ CtrlCaret +32 CtrlSlash +33 Invalid +34 Resize +35 Mouse +36 DoubleClick +37 LeftClick +38 RightClick +39 BTab +40 BSpace +41 Del +42 PgUp +43 PgDn +44 Up +45 Down +46 Left +47 Right +48 Home +49 End +50 Insert +51 SUp +52 SDown +53 SLeft +54 SRight +55 F1 +56 F2 +57 F3 +58 F4 +59 F5 +60 F6 +61 F7 +62 F8 +63 F9 +64 F10 +65 F11 +66 F12 +67 Change +68 BackwardEOF +69 AltBS +70 AltUp +71 AltDown +72 AltLeft +73 AltRight +74 AltSUp +75 AltSDown +76 AltSLeft +77 AltSRight +78 Alt +79 CtrlAlt +.. +127 3 KeyDEL KeyBackspace2 +.. +256 6 KeyRune +257 4 KeyUp +258 4 KeyDown +259 4 KeyRight +260 4 KeyLeft +261 8 KeyUpLeft +262 8 KeyUpRight +263 8 KeyDownLeft +264 8 KeyDownRight +265 8 KeyCenter +266 5 KeyPgUp +267 5 KeyPgDn +268 5 KeyHome +269 5 KeyEnd +270 5 KeyInsert +271 5 KeyDelete +272 8 KeyHelp +273 8 KeyExit +274 8 KeyClear +275 8 KeyCancel +276 8 KeyPrint +277 8 KeyPause +278 5 KeyBacktab +279 5 KeyF1 +280 5 KeyF2 +281 5 KeyF3 +282 5 KeyF4 +283 5 KeyF5 +284 5 KeyF6 +285 5 KeyF7 +286 5 KeyF8 +287 5 KeyF9 +288 5 KeyF10 +289 5 KeyF11 +290 5 KeyF12 +291 8 KeyF13 +292 8 KeyF14 +293 8 KeyF15 +294 8 KeyF16 +295 8 KeyF17 +296 8 KeyF18 +297 8 KeyF19 +298 8 KeyF20 +299 8 KeyF21 +300 8 KeyF22 +301 8 KeyF23 +302 8 KeyF24 +303 8 KeyF25 +304 8 KeyF26 +305 8 KeyF27 +306 8 KeyF28 +307 8 KeyF29 +308 8 KeyF30 +309 8 KeyF31 +310 8 KeyF32 +311 8 KeyF33 +312 8 KeyF34 +313 8 KeyF35 +314 8 KeyF36 +315 8 KeyF37 +316 8 KeyF38 +317 8 KeyF39 +318 8 KeyF40 +319 8 KeyF41 +320 8 KeyF42 +321 8 KeyF43 +322 8 KeyF44 +323 8 KeyF45 +324 8 KeyF46 +325 8 KeyF47 +326 8 KeyF48 +327 8 KeyF49 +328 8 KeyF50 +329 8 KeyF51 +330 8 KeyF52 +331 8 KeyF53 +332 8 KeyF54 +333 8 KeyF55 +334 8 KeyF56 +335 8 KeyF57 +336 8 KeyF58 +337 8 KeyF59 +338 8 KeyF60 +339 8 KeyF61 +340 8 KeyF62 +341 8 KeyF63 +342 8 KeyF64 +-- --------- -------------- --------------- ----------- ------------- ---------------- ---------- +%v section # tcell ctrl key tcell ctrl char tcell alias tui constants tcell named keys tcell mods +*/ diff --git a/fzf/fzf/src/tui/ttyname_unix.go b/fzf/fzf/src/tui/ttyname_unix.go new file mode 100644 index 0000000..68298cd --- /dev/null +++ b/fzf/fzf/src/tui/ttyname_unix.go @@ -0,0 +1,47 @@ +// +build !windows + +package tui + +import ( + "io/ioutil" + "os" + "syscall" +) + +var devPrefixes = [...]string{"/dev/pts/", "/dev/"} + +func ttyname() string { + var stderr syscall.Stat_t + if syscall.Fstat(2, &stderr) != nil { + return "" + } + + for _, prefix := range devPrefixes { + files, err := ioutil.ReadDir(prefix) + if err != nil { + continue + } + + for _, file := range files { + if stat, ok := file.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev { + return prefix + file.Name() + } + } + } + return "" +} + +// TtyIn returns terminal device to be used as STDIN, falls back to os.Stdin +func TtyIn() *os.File { + in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) + if err != nil { + tty := ttyname() + if len(tty) > 0 { + if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil { + return in + } + } + return os.Stdin + } + return in +} diff --git a/fzf/fzf/src/tui/ttyname_windows.go b/fzf/fzf/src/tui/ttyname_windows.go new file mode 100644 index 0000000..8db490a --- /dev/null +++ b/fzf/fzf/src/tui/ttyname_windows.go @@ -0,0 +1,14 @@ +// +build windows + +package tui + +import "os" + +func ttyname() string { + return "" +} + +// TtyIn on Windows returns os.Stdin +func TtyIn() *os.File { + return os.Stdin +} diff --git a/fzf/fzf/src/tui/tui.go b/fzf/fzf/src/tui/tui.go new file mode 100644 index 0000000..eb09da4 --- /dev/null +++ b/fzf/fzf/src/tui/tui.go @@ -0,0 +1,625 @@ +package tui + +import ( + "fmt" + "os" + "strconv" + "time" +) + +// Types of user action +type EventType int + +const ( + Rune EventType = iota + + CtrlA + CtrlB + CtrlC + CtrlD + CtrlE + CtrlF + CtrlG + CtrlH + Tab + CtrlJ + CtrlK + CtrlL + CtrlM + CtrlN + CtrlO + CtrlP + CtrlQ + CtrlR + CtrlS + CtrlT + CtrlU + CtrlV + CtrlW + CtrlX + CtrlY + CtrlZ + ESC + CtrlSpace + + // https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal + CtrlBackSlash + CtrlRightBracket + CtrlCaret + CtrlSlash + + Invalid + Resize + Mouse + DoubleClick + LeftClick + RightClick + + BTab + BSpace + + Del + PgUp + PgDn + + Up + Down + Left + Right + Home + End + Insert + + SUp + SDown + SLeft + SRight + + F1 + F2 + F3 + F4 + F5 + F6 + F7 + F8 + F9 + F10 + F11 + F12 + + Change + BackwardEOF + + AltBS + + AltUp + AltDown + AltLeft + AltRight + + AltSUp + AltSDown + AltSLeft + AltSRight + + Alt + CtrlAlt +) + +func (t EventType) AsEvent() Event { + return Event{t, 0, nil} +} + +func (t EventType) Int() int { + return int(t) +} + +func (t EventType) Byte() byte { + return byte(t) +} + +func (e Event) Comparable() Event { + // Ignore MouseEvent pointer + return Event{e.Type, e.Char, nil} +} + +func Key(r rune) Event { + return Event{Rune, r, nil} +} + +func AltKey(r rune) Event { + return Event{Alt, r, nil} +} + +func CtrlAltKey(r rune) Event { + return Event{CtrlAlt, r, nil} +} + +const ( + doubleClickDuration = 500 * time.Millisecond +) + +type Color int32 + +func (c Color) IsDefault() bool { + return c == colDefault +} + +func (c Color) is24() bool { + return c > 0 && (c&(1<<24)) > 0 +} + +type ColorAttr struct { + Color Color + Attr Attr +} + +func NewColorAttr() ColorAttr { + return ColorAttr{Color: colUndefined, Attr: AttrUndefined} +} + +const ( + colUndefined Color = -2 + colDefault Color = -1 +) + +const ( + colBlack Color = iota + colRed + colGreen + colYellow + colBlue + colMagenta + colCyan + colWhite +) + +type FillReturn int + +const ( + FillContinue FillReturn = iota + FillNextLine + FillSuspend +) + +type ColorPair struct { + fg Color + bg Color + attr Attr +} + +func HexToColor(rrggbb string) Color { + r, _ := strconv.ParseInt(rrggbb[1:3], 16, 0) + g, _ := strconv.ParseInt(rrggbb[3:5], 16, 0) + b, _ := strconv.ParseInt(rrggbb[5:7], 16, 0) + return Color((1 << 24) + (r << 16) + (g << 8) + b) +} + +func NewColorPair(fg Color, bg Color, attr Attr) ColorPair { + return ColorPair{fg, bg, attr} +} + +func (p ColorPair) Fg() Color { + return p.fg +} + +func (p ColorPair) Bg() Color { + return p.bg +} + +func (p ColorPair) Attr() Attr { + return p.attr +} + +func (p ColorPair) HasBg() bool { + return p.attr&Reverse == 0 && p.bg != colDefault || + p.attr&Reverse > 0 && p.fg != colDefault +} + +func (p ColorPair) merge(other ColorPair, except Color) ColorPair { + dup := p + dup.attr = dup.attr.Merge(other.attr) + if other.fg != except { + dup.fg = other.fg + } + if other.bg != except { + dup.bg = other.bg + } + return dup +} + +func (p ColorPair) WithAttr(attr Attr) ColorPair { + dup := p + dup.attr = dup.attr.Merge(attr) + return dup +} + +func (p ColorPair) MergeAttr(other ColorPair) ColorPair { + return p.WithAttr(other.attr) +} + +func (p ColorPair) Merge(other ColorPair) ColorPair { + return p.merge(other, colUndefined) +} + +func (p ColorPair) MergeNonDefault(other ColorPair) ColorPair { + return p.merge(other, colDefault) +} + +type ColorTheme struct { + Colored bool + Input ColorAttr + Disabled ColorAttr + Fg ColorAttr + Bg ColorAttr + PreviewFg ColorAttr + PreviewBg ColorAttr + DarkBg ColorAttr + Gutter ColorAttr + Prompt ColorAttr + Match ColorAttr + Current ColorAttr + CurrentMatch ColorAttr + Spinner ColorAttr + Info ColorAttr + Cursor ColorAttr + Selected ColorAttr + Header ColorAttr + Border ColorAttr +} + +type Event struct { + Type EventType + Char rune + MouseEvent *MouseEvent +} + +type MouseEvent struct { + Y int + X int + S int + Left bool + Down bool + Double bool + Mod bool +} + +type BorderShape int + +const ( + BorderNone BorderShape = iota + BorderRounded + BorderSharp + BorderHorizontal + BorderVertical + BorderTop + BorderBottom + BorderLeft + BorderRight +) + +type BorderStyle struct { + shape BorderShape + horizontal rune + vertical rune + topLeft rune + topRight rune + bottomLeft rune + bottomRight rune +} + +type BorderCharacter int + +func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle { + if unicode { + if shape == BorderRounded { + return BorderStyle{ + shape: shape, + horizontal: '─', + vertical: '│', + topLeft: '╭', + topRight: '╮', + bottomLeft: '╰', + bottomRight: '╯', + } + } + return BorderStyle{ + shape: shape, + horizontal: '─', + vertical: '│', + topLeft: '┌', + topRight: '┐', + bottomLeft: '└', + bottomRight: '┘', + } + } + return BorderStyle{ + shape: shape, + horizontal: '-', + vertical: '|', + topLeft: '+', + topRight: '+', + bottomLeft: '+', + bottomRight: '+', + } +} + +func MakeTransparentBorder() BorderStyle { + return BorderStyle{ + shape: BorderRounded, + horizontal: ' ', + vertical: ' ', + topLeft: ' ', + topRight: ' ', + bottomLeft: ' ', + bottomRight: ' '} +} + +type Renderer interface { + Init() + Pause(clear bool) + Resume(clear bool, sigcont bool) + Clear() + RefreshWindows(windows []Window) + Refresh() + Close() + + GetChar() Event + + MaxX() int + MaxY() int + + NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window +} + +type Window interface { + Top() int + Left() int + Width() int + Height() int + + Refresh() + FinishFill() + Close() + + X() int + Y() int + Enclose(y int, x int) bool + + Move(y int, x int) + MoveAndClear(y int, x int) + Print(text string) + CPrint(color ColorPair, text string) + Fill(text string) FillReturn + CFill(fg Color, bg Color, attr Attr, text string) FillReturn + Erase() +} + +type FullscreenRenderer struct { + theme *ColorTheme + mouse bool + forceBlack bool + prevDownTime time.Time + clickY []int +} + +func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer { + r := &FullscreenRenderer{ + theme: theme, + mouse: mouse, + forceBlack: forceBlack, + prevDownTime: time.Unix(0, 0), + clickY: []int{}} + return r +} + +var ( + Default16 *ColorTheme + Dark256 *ColorTheme + Light256 *ColorTheme + + ColPrompt ColorPair + ColNormal ColorPair + ColInput ColorPair + ColDisabled ColorPair + ColMatch ColorPair + ColCursor ColorPair + ColCursorEmpty ColorPair + ColSelected ColorPair + ColCurrent ColorPair + ColCurrentMatch ColorPair + ColCurrentCursor ColorPair + ColCurrentCursorEmpty ColorPair + ColCurrentSelected ColorPair + ColCurrentSelectedEmpty ColorPair + ColSpinner ColorPair + ColInfo ColorPair + ColHeader ColorPair + ColBorder ColorPair + ColPreview ColorPair + ColPreviewBorder ColorPair +) + +func EmptyTheme() *ColorTheme { + return &ColorTheme{ + Colored: true, + Input: ColorAttr{colUndefined, AttrUndefined}, + Disabled: ColorAttr{colUndefined, AttrUndefined}, + Fg: ColorAttr{colUndefined, AttrUndefined}, + Bg: ColorAttr{colUndefined, AttrUndefined}, + PreviewFg: ColorAttr{colUndefined, AttrUndefined}, + PreviewBg: ColorAttr{colUndefined, AttrUndefined}, + DarkBg: ColorAttr{colUndefined, AttrUndefined}, + Gutter: ColorAttr{colUndefined, AttrUndefined}, + Prompt: ColorAttr{colUndefined, AttrUndefined}, + Match: ColorAttr{colUndefined, AttrUndefined}, + Current: ColorAttr{colUndefined, AttrUndefined}, + CurrentMatch: ColorAttr{colUndefined, AttrUndefined}, + Spinner: ColorAttr{colUndefined, AttrUndefined}, + Info: ColorAttr{colUndefined, AttrUndefined}, + Cursor: ColorAttr{colUndefined, AttrUndefined}, + Selected: ColorAttr{colUndefined, AttrUndefined}, + Header: ColorAttr{colUndefined, AttrUndefined}, + Border: ColorAttr{colUndefined, AttrUndefined}} +} + +func NoColorTheme() *ColorTheme { + return &ColorTheme{ + Colored: false, + Input: ColorAttr{colDefault, AttrRegular}, + Disabled: ColorAttr{colDefault, AttrRegular}, + Fg: ColorAttr{colDefault, AttrRegular}, + Bg: ColorAttr{colDefault, AttrRegular}, + PreviewFg: ColorAttr{colDefault, AttrRegular}, + PreviewBg: ColorAttr{colDefault, AttrRegular}, + DarkBg: ColorAttr{colDefault, AttrRegular}, + Gutter: ColorAttr{colDefault, AttrRegular}, + Prompt: ColorAttr{colDefault, AttrRegular}, + Match: ColorAttr{colDefault, Underline}, + Current: ColorAttr{colDefault, Reverse}, + CurrentMatch: ColorAttr{colDefault, Reverse | Underline}, + Spinner: ColorAttr{colDefault, AttrRegular}, + Info: ColorAttr{colDefault, AttrRegular}, + Cursor: ColorAttr{colDefault, AttrRegular}, + Selected: ColorAttr{colDefault, AttrRegular}, + Header: ColorAttr{colDefault, AttrRegular}, + Border: ColorAttr{colDefault, AttrRegular}} +} + +func errorExit(message string) { + fmt.Fprintln(os.Stderr, message) + os.Exit(2) +} + +func init() { + Default16 = &ColorTheme{ + Colored: true, + Input: ColorAttr{colDefault, AttrUndefined}, + Disabled: ColorAttr{colUndefined, AttrUndefined}, + Fg: ColorAttr{colDefault, AttrUndefined}, + Bg: ColorAttr{colDefault, AttrUndefined}, + PreviewFg: ColorAttr{colUndefined, AttrUndefined}, + PreviewBg: ColorAttr{colUndefined, AttrUndefined}, + DarkBg: ColorAttr{colBlack, AttrUndefined}, + Gutter: ColorAttr{colUndefined, AttrUndefined}, + Prompt: ColorAttr{colBlue, AttrUndefined}, + Match: ColorAttr{colGreen, AttrUndefined}, + Current: ColorAttr{colYellow, AttrUndefined}, + CurrentMatch: ColorAttr{colGreen, AttrUndefined}, + Spinner: ColorAttr{colGreen, AttrUndefined}, + Info: ColorAttr{colWhite, AttrUndefined}, + Cursor: ColorAttr{colRed, AttrUndefined}, + Selected: ColorAttr{colMagenta, AttrUndefined}, + Header: ColorAttr{colCyan, AttrUndefined}, + Border: ColorAttr{colBlack, AttrUndefined}} + Dark256 = &ColorTheme{ + Colored: true, + Input: ColorAttr{colDefault, AttrUndefined}, + Disabled: ColorAttr{colUndefined, AttrUndefined}, + Fg: ColorAttr{colDefault, AttrUndefined}, + Bg: ColorAttr{colDefault, AttrUndefined}, + PreviewFg: ColorAttr{colUndefined, AttrUndefined}, + PreviewBg: ColorAttr{colUndefined, AttrUndefined}, + DarkBg: ColorAttr{236, AttrUndefined}, + Gutter: ColorAttr{colUndefined, AttrUndefined}, + Prompt: ColorAttr{110, AttrUndefined}, + Match: ColorAttr{108, AttrUndefined}, + Current: ColorAttr{254, AttrUndefined}, + CurrentMatch: ColorAttr{151, AttrUndefined}, + Spinner: ColorAttr{148, AttrUndefined}, + Info: ColorAttr{144, AttrUndefined}, + Cursor: ColorAttr{161, AttrUndefined}, + Selected: ColorAttr{168, AttrUndefined}, + Header: ColorAttr{109, AttrUndefined}, + Border: ColorAttr{59, AttrUndefined}} + Light256 = &ColorTheme{ + Colored: true, + Input: ColorAttr{colDefault, AttrUndefined}, + Disabled: ColorAttr{colUndefined, AttrUndefined}, + Fg: ColorAttr{colDefault, AttrUndefined}, + Bg: ColorAttr{colDefault, AttrUndefined}, + PreviewFg: ColorAttr{colUndefined, AttrUndefined}, + PreviewBg: ColorAttr{colUndefined, AttrUndefined}, + DarkBg: ColorAttr{251, AttrUndefined}, + Gutter: ColorAttr{colUndefined, AttrUndefined}, + Prompt: ColorAttr{25, AttrUndefined}, + Match: ColorAttr{66, AttrUndefined}, + Current: ColorAttr{237, AttrUndefined}, + CurrentMatch: ColorAttr{23, AttrUndefined}, + Spinner: ColorAttr{65, AttrUndefined}, + Info: ColorAttr{101, AttrUndefined}, + Cursor: ColorAttr{161, AttrUndefined}, + Selected: ColorAttr{168, AttrUndefined}, + Header: ColorAttr{31, AttrUndefined}, + Border: ColorAttr{145, AttrUndefined}} +} + +func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) { + if forceBlack { + theme.Bg = ColorAttr{colBlack, AttrUndefined} + } + + o := func(a ColorAttr, b ColorAttr) ColorAttr { + c := a + if b.Color != colUndefined { + c.Color = b.Color + } + if b.Attr != AttrUndefined { + c.Attr = b.Attr + } + return c + } + theme.Input = o(baseTheme.Input, theme.Input) + theme.Disabled = o(theme.Input, o(baseTheme.Disabled, theme.Disabled)) + theme.Fg = o(baseTheme.Fg, theme.Fg) + theme.Bg = o(baseTheme.Bg, theme.Bg) + theme.PreviewFg = o(theme.Fg, o(baseTheme.PreviewFg, theme.PreviewFg)) + theme.PreviewBg = o(theme.Bg, o(baseTheme.PreviewBg, theme.PreviewBg)) + theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg) + theme.Gutter = o(theme.DarkBg, o(baseTheme.Gutter, theme.Gutter)) + theme.Prompt = o(baseTheme.Prompt, theme.Prompt) + theme.Match = o(baseTheme.Match, theme.Match) + theme.Current = o(baseTheme.Current, theme.Current) + theme.CurrentMatch = o(baseTheme.CurrentMatch, theme.CurrentMatch) + theme.Spinner = o(baseTheme.Spinner, theme.Spinner) + theme.Info = o(baseTheme.Info, theme.Info) + theme.Cursor = o(baseTheme.Cursor, theme.Cursor) + theme.Selected = o(baseTheme.Selected, theme.Selected) + theme.Header = o(baseTheme.Header, theme.Header) + theme.Border = o(baseTheme.Border, theme.Border) + + initPalette(theme) +} + +func initPalette(theme *ColorTheme) { + pair := func(fg, bg ColorAttr) ColorPair { + if fg.Color == colDefault && (fg.Attr&Reverse) > 0 { + bg.Color = colDefault + } + return ColorPair{fg.Color, bg.Color, fg.Attr} + } + blank := theme.Fg + blank.Attr = AttrRegular + + ColPrompt = pair(theme.Prompt, theme.Bg) + ColNormal = pair(theme.Fg, theme.Bg) + ColInput = pair(theme.Input, theme.Bg) + ColDisabled = pair(theme.Disabled, theme.Bg) + ColMatch = pair(theme.Match, theme.Bg) + ColCursor = pair(theme.Cursor, theme.Gutter) + ColCursorEmpty = pair(blank, theme.Gutter) + ColSelected = pair(theme.Selected, theme.Gutter) + ColCurrent = pair(theme.Current, theme.DarkBg) + ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg) + ColCurrentCursor = pair(theme.Cursor, theme.DarkBg) + ColCurrentCursorEmpty = pair(blank, theme.DarkBg) + ColCurrentSelected = pair(theme.Selected, theme.DarkBg) + ColCurrentSelectedEmpty = pair(blank, theme.DarkBg) + ColSpinner = pair(theme.Spinner, theme.Bg) + ColInfo = pair(theme.Info, theme.Bg) + ColHeader = pair(theme.Header, theme.Bg) + ColBorder = pair(theme.Border, theme.Bg) + ColPreview = pair(theme.PreviewFg, theme.PreviewBg) + ColPreviewBorder = pair(theme.Border, theme.PreviewBg) +} diff --git a/fzf/fzf/src/tui/tui_test.go b/fzf/fzf/src/tui/tui_test.go new file mode 100644 index 0000000..3ba9bf3 --- /dev/null +++ b/fzf/fzf/src/tui/tui_test.go @@ -0,0 +1,20 @@ +package tui + +import "testing" + +func TestHexToColor(t *testing.T) { + assert := func(expr string, r, g, b int) { + color := HexToColor(expr) + if !color.is24() || + int((color>>16)&0xff) != r || + int((color>>8)&0xff) != g || + int((color)&0xff) != b { + t.Fail() + } + } + + assert("#ff0000", 255, 0, 0) + assert("#010203", 1, 2, 3) + assert("#102030", 16, 32, 48) + assert("#ffffff", 255, 255, 255) +} diff --git a/fzf/fzf/src/util/atomicbool.go b/fzf/fzf/src/util/atomicbool.go new file mode 100644 index 0000000..c5c7e69 --- /dev/null +++ b/fzf/fzf/src/util/atomicbool.go @@ -0,0 +1,34 @@ +package util + +import ( + "sync/atomic" +) + +func convertBoolToInt32(b bool) int32 { + if b { + return 1 + } + return 0 +} + +// AtomicBool is a boxed-class that provides synchronized access to the +// underlying boolean value +type AtomicBool struct { + state int32 // "1" is true, "0" is false +} + +// NewAtomicBool returns a new AtomicBool +func NewAtomicBool(initialState bool) *AtomicBool { + return &AtomicBool{state: convertBoolToInt32(initialState)} +} + +// Get returns the current boolean value synchronously +func (a *AtomicBool) Get() bool { + return atomic.LoadInt32(&a.state) == 1 +} + +// Set updates the boolean value synchronously +func (a *AtomicBool) Set(newState bool) bool { + atomic.StoreInt32(&a.state, convertBoolToInt32(newState)) + return newState +} diff --git a/fzf/fzf/src/util/atomicbool_test.go b/fzf/fzf/src/util/atomicbool_test.go new file mode 100644 index 0000000..1feff79 --- /dev/null +++ b/fzf/fzf/src/util/atomicbool_test.go @@ -0,0 +1,17 @@ +package util + +import "testing" + +func TestAtomicBool(t *testing.T) { + if !NewAtomicBool(true).Get() || NewAtomicBool(false).Get() { + t.Error("Invalid initial value") + } + + ab := NewAtomicBool(true) + if ab.Set(false) { + t.Error("Invalid return value") + } + if ab.Get() { + t.Error("Invalid state") + } +} diff --git a/fzf/fzf/src/util/chars.go b/fzf/fzf/src/util/chars.go new file mode 100644 index 0000000..41de924 --- /dev/null +++ b/fzf/fzf/src/util/chars.go @@ -0,0 +1,198 @@ +package util + +import ( + "fmt" + "unicode" + "unicode/utf8" + "unsafe" +) + +const ( + overflow64 uint64 = 0x8080808080808080 + overflow32 uint32 = 0x80808080 +) + +type Chars struct { + slice []byte // or []rune + inBytes bool + trimLengthKnown bool + trimLength uint16 + + // XXX Piggybacking item index here is a horrible idea. But I'm trying to + // minimize the memory footprint by not wasting padded spaces. + Index int32 +} + +func checkAscii(bytes []byte) (bool, int) { + i := 0 + for ; i <= len(bytes)-8; i += 8 { + if (overflow64 & *(*uint64)(unsafe.Pointer(&bytes[i]))) > 0 { + return false, i + } + } + for ; i <= len(bytes)-4; i += 4 { + if (overflow32 & *(*uint32)(unsafe.Pointer(&bytes[i]))) > 0 { + return false, i + } + } + for ; i < len(bytes); i++ { + if bytes[i] >= utf8.RuneSelf { + return false, i + } + } + return true, 0 +} + +// ToChars converts byte array into rune array +func ToChars(bytes []byte) Chars { + inBytes, bytesUntil := checkAscii(bytes) + if inBytes { + return Chars{slice: bytes, inBytes: inBytes} + } + + runes := make([]rune, bytesUntil, len(bytes)) + for i := 0; i < bytesUntil; i++ { + runes[i] = rune(bytes[i]) + } + for i := bytesUntil; i < len(bytes); { + r, sz := utf8.DecodeRune(bytes[i:]) + i += sz + runes = append(runes, r) + } + return RunesToChars(runes) +} + +func RunesToChars(runes []rune) Chars { + return Chars{slice: *(*[]byte)(unsafe.Pointer(&runes)), inBytes: false} +} + +func (chars *Chars) IsBytes() bool { + return chars.inBytes +} + +func (chars *Chars) Bytes() []byte { + return chars.slice +} + +func (chars *Chars) optionalRunes() []rune { + if chars.inBytes { + return nil + } + return *(*[]rune)(unsafe.Pointer(&chars.slice)) +} + +func (chars *Chars) Get(i int) rune { + if runes := chars.optionalRunes(); runes != nil { + return runes[i] + } + return rune(chars.slice[i]) +} + +func (chars *Chars) Length() int { + if runes := chars.optionalRunes(); runes != nil { + return len(runes) + } + return len(chars.slice) +} + +// String returns the string representation of a Chars object. +func (chars *Chars) String() string { + return fmt.Sprintf("Chars{slice: []byte(%q), inBytes: %v, trimLengthKnown: %v, trimLength: %d, Index: %d}", chars.slice, chars.inBytes, chars.trimLengthKnown, chars.trimLength, chars.Index) +} + +// TrimLength returns the length after trimming leading and trailing whitespaces +func (chars *Chars) TrimLength() uint16 { + if chars.trimLengthKnown { + return chars.trimLength + } + chars.trimLengthKnown = true + var i int + len := chars.Length() + for i = len - 1; i >= 0; i-- { + char := chars.Get(i) + if !unicode.IsSpace(char) { + break + } + } + // Completely empty + if i < 0 { + return 0 + } + + var j int + for j = 0; j < len; j++ { + char := chars.Get(j) + if !unicode.IsSpace(char) { + break + } + } + chars.trimLength = AsUint16(i - j + 1) + return chars.trimLength +} + +func (chars *Chars) LeadingWhitespaces() int { + whitespaces := 0 + for i := 0; i < chars.Length(); i++ { + char := chars.Get(i) + if !unicode.IsSpace(char) { + break + } + whitespaces++ + } + return whitespaces +} + +func (chars *Chars) TrailingWhitespaces() int { + whitespaces := 0 + for i := chars.Length() - 1; i >= 0; i-- { + char := chars.Get(i) + if !unicode.IsSpace(char) { + break + } + whitespaces++ + } + return whitespaces +} + +func (chars *Chars) TrimTrailingWhitespaces() { + whitespaces := chars.TrailingWhitespaces() + chars.slice = chars.slice[0 : len(chars.slice)-whitespaces] +} + +func (chars *Chars) ToString() string { + if runes := chars.optionalRunes(); runes != nil { + return string(runes) + } + return string(chars.slice) +} + +func (chars *Chars) ToRunes() []rune { + if runes := chars.optionalRunes(); runes != nil { + return runes + } + bytes := chars.slice + runes := make([]rune, len(bytes)) + for idx, b := range bytes { + runes[idx] = rune(b) + } + return runes +} + +func (chars *Chars) CopyRunes(dest []rune) { + if runes := chars.optionalRunes(); runes != nil { + copy(dest, runes) + return + } + for idx, b := range chars.slice[:len(dest)] { + dest[idx] = rune(b) + } +} + +func (chars *Chars) Prepend(prefix string) { + if runes := chars.optionalRunes(); runes != nil { + runes = append([]rune(prefix), runes...) + chars.slice = *(*[]byte)(unsafe.Pointer(&runes)) + } else { + chars.slice = append([]byte(prefix), chars.slice...) + } +} diff --git a/fzf/fzf/src/util/chars_test.go b/fzf/fzf/src/util/chars_test.go new file mode 100644 index 0000000..b7983f3 --- /dev/null +++ b/fzf/fzf/src/util/chars_test.go @@ -0,0 +1,46 @@ +package util + +import "testing" + +func TestToCharsAscii(t *testing.T) { + chars := ToChars([]byte("foobar")) + if !chars.inBytes || chars.ToString() != "foobar" || !chars.inBytes { + t.Error() + } +} + +func TestCharsLength(t *testing.T) { + chars := ToChars([]byte("\tabc한글 ")) + if chars.inBytes || chars.Length() != 8 || chars.TrimLength() != 5 { + t.Error() + } +} + +func TestCharsToString(t *testing.T) { + text := "\tabc한글 " + chars := ToChars([]byte(text)) + if chars.ToString() != text { + t.Error() + } +} + +func TestTrimLength(t *testing.T) { + check := func(str string, exp uint16) { + chars := ToChars([]byte(str)) + trimmed := chars.TrimLength() + if trimmed != exp { + t.Errorf("Invalid TrimLength result for '%s': %d (expected %d)", + str, trimmed, exp) + } + } + check("hello", 5) + check("hello ", 5) + check("hello ", 5) + check(" hello", 5) + check(" hello", 5) + check(" hello ", 5) + check(" hello ", 5) + check("h o", 5) + check(" h o ", 5) + check(" ", 0) +} diff --git a/fzf/fzf/src/util/eventbox.go b/fzf/fzf/src/util/eventbox.go new file mode 100644 index 0000000..b710cf1 --- /dev/null +++ b/fzf/fzf/src/util/eventbox.go @@ -0,0 +1,96 @@ +package util + +import "sync" + +// EventType is the type for fzf events +type EventType int + +// Events is a type that associates EventType to any data +type Events map[EventType]interface{} + +// EventBox is used for coordinating events +type EventBox struct { + events Events + cond *sync.Cond + ignore map[EventType]bool +} + +// NewEventBox returns a new EventBox +func NewEventBox() *EventBox { + return &EventBox{ + events: make(Events), + cond: sync.NewCond(&sync.Mutex{}), + ignore: make(map[EventType]bool)} +} + +// Wait blocks the goroutine until signaled +func (b *EventBox) Wait(callback func(*Events)) { + b.cond.L.Lock() + + if len(b.events) == 0 { + b.cond.Wait() + } + + callback(&b.events) + b.cond.L.Unlock() +} + +// Set turns on the event type on the box +func (b *EventBox) Set(event EventType, value interface{}) { + b.cond.L.Lock() + b.events[event] = value + if _, found := b.ignore[event]; !found { + b.cond.Broadcast() + } + b.cond.L.Unlock() +} + +// Clear clears the events +// Unsynchronized; should be called within Wait routine +func (events *Events) Clear() { + for event := range *events { + delete(*events, event) + } +} + +// Peek peeks at the event box if the given event is set +func (b *EventBox) Peek(event EventType) bool { + b.cond.L.Lock() + _, ok := b.events[event] + b.cond.L.Unlock() + return ok +} + +// Watch deletes the events from the ignore list +func (b *EventBox) Watch(events ...EventType) { + b.cond.L.Lock() + for _, event := range events { + delete(b.ignore, event) + } + b.cond.L.Unlock() +} + +// Unwatch adds the events to the ignore list +func (b *EventBox) Unwatch(events ...EventType) { + b.cond.L.Lock() + for _, event := range events { + b.ignore[event] = true + } + b.cond.L.Unlock() +} + +// WaitFor blocks the execution until the event is received +func (b *EventBox) WaitFor(event EventType) { + looping := true + for looping { + b.Wait(func(events *Events) { + for evt := range *events { + switch evt { + case event: + looping = false + return + } + } + }) + } +} diff --git a/fzf/fzf/src/util/eventbox_test.go b/fzf/fzf/src/util/eventbox_test.go new file mode 100644 index 0000000..5a9dc30 --- /dev/null +++ b/fzf/fzf/src/util/eventbox_test.go @@ -0,0 +1,61 @@ +package util + +import "testing" + +// fzf events +const ( + EvtReadNew EventType = iota + EvtReadFin + EvtSearchNew + EvtSearchProgress + EvtSearchFin + EvtClose +) + +func TestEventBox(t *testing.T) { + eb := NewEventBox() + + // Wait should return immediately + ch := make(chan bool) + + go func() { + eb.Set(EvtReadNew, 10) + ch <- true + <-ch + eb.Set(EvtSearchNew, 10) + eb.Set(EvtSearchNew, 15) + eb.Set(EvtSearchNew, 20) + eb.Set(EvtSearchProgress, 30) + ch <- true + <-ch + eb.Set(EvtSearchFin, 40) + ch <- true + <-ch + }() + + count := 0 + sum := 0 + looping := true + for looping { + <-ch + eb.Wait(func(events *Events) { + for _, value := range *events { + switch val := value.(type) { + case int: + sum += val + looping = sum < 100 + } + } + events.Clear() + }) + ch <- true + count++ + } + + if count != 3 { + t.Error("Invalid number of events", count) + } + if sum != 100 { + t.Error("Invalid sum", sum) + } +} diff --git a/fzf/fzf/src/util/slab.go b/fzf/fzf/src/util/slab.go new file mode 100644 index 0000000..0c49d2d --- /dev/null +++ b/fzf/fzf/src/util/slab.go @@ -0,0 +1,12 @@ +package util + +type Slab struct { + I16 []int16 + I32 []int32 +} + +func MakeSlab(size16 int, size32 int) *Slab { + return &Slab{ + I16: make([]int16, size16), + I32: make([]int32, size32)} +} diff --git a/fzf/fzf/src/util/util.go b/fzf/fzf/src/util/util.go new file mode 100644 index 0000000..c3995bf --- /dev/null +++ b/fzf/fzf/src/util/util.go @@ -0,0 +1,138 @@ +package util + +import ( + "math" + "os" + "strings" + "time" + + "github.com/mattn/go-isatty" + "github.com/mattn/go-runewidth" + "github.com/rivo/uniseg" +) + +// RunesWidth returns runes width +func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) { + width := 0 + gr := uniseg.NewGraphemes(string(runes)) + idx := 0 + for gr.Next() { + rs := gr.Runes() + var w int + if len(rs) == 1 && rs[0] == '\t' { + w = tabstop - (prefixWidth+width)%tabstop + } else { + s := string(rs) + w = runewidth.StringWidth(s) + strings.Count(s, "\n") + } + width += w + if width > limit { + return width, idx + } + idx += len(rs) + } + return width, -1 +} + +// Max returns the largest integer +func Max(first int, second int) int { + if first >= second { + return first + } + return second +} + +// Max16 returns the largest integer +func Max16(first int16, second int16) int16 { + if first >= second { + return first + } + return second +} + +// Max32 returns the largest 32-bit integer +func Max32(first int32, second int32) int32 { + if first > second { + return first + } + return second +} + +// Min returns the smallest integer +func Min(first int, second int) int { + if first <= second { + return first + } + return second +} + +// Min32 returns the smallest 32-bit integer +func Min32(first int32, second int32) int32 { + if first <= second { + return first + } + return second +} + +// Constrain32 limits the given 32-bit integer with the upper and lower bounds +func Constrain32(val int32, min int32, max int32) int32 { + if val < min { + return min + } + if val > max { + return max + } + return val +} + +// Constrain limits the given integer with the upper and lower bounds +func Constrain(val int, min int, max int) int { + if val < min { + return min + } + if val > max { + return max + } + return val +} + +func AsUint16(val int) uint16 { + if val > math.MaxUint16 { + return math.MaxUint16 + } else if val < 0 { + return 0 + } + return uint16(val) +} + +// DurWithin limits the given time.Duration with the upper and lower bounds +func DurWithin( + val time.Duration, min time.Duration, max time.Duration) time.Duration { + if val < min { + return min + } + if val > max { + return max + } + return val +} + +// IsTty returns true if stdin is a terminal +func IsTty() bool { + return isatty.IsTerminal(os.Stdin.Fd()) +} + +// ToTty returns true if stdout is a terminal +func ToTty() bool { + return isatty.IsTerminal(os.Stdout.Fd()) +} + +// Once returns a function that returns the specified boolean value only once +func Once(nextResponse bool) func() bool { + state := nextResponse + return func() bool { + prevState := state + state = false + return prevState + } +} diff --git a/fzf/fzf/src/util/util_test.go b/fzf/fzf/src/util/util_test.go new file mode 100644 index 0000000..45a5a2d --- /dev/null +++ b/fzf/fzf/src/util/util_test.go @@ -0,0 +1,56 @@ +package util + +import "testing" + +func TestMax(t *testing.T) { + if Max(-2, 5) != 5 { + t.Error("Invalid result") + } +} + +func TestContrain(t *testing.T) { + if Constrain(-3, -1, 3) != -1 { + t.Error("Expected", -1) + } + if Constrain(2, -1, 3) != 2 { + t.Error("Expected", 2) + } + + if Constrain(5, -1, 3) != 3 { + t.Error("Expected", 3) + } +} + +func TestOnce(t *testing.T) { + o := Once(false) + if o() { + t.Error("Expected: false") + } + if o() { + t.Error("Expected: false") + } + + o = Once(true) + if !o() { + t.Error("Expected: true") + } + if o() { + t.Error("Expected: false") + } +} + +func TestRunesWidth(t *testing.T) { + for _, args := range [][]int{ + {100, 5, -1}, + {3, 4, 3}, + {0, 1, 0}, + } { + width, overflowIdx := RunesWidth([]rune("hello"), 0, 0, args[0]) + if width != args[1] { + t.Errorf("Expected width: %d, actual: %d", args[1], width) + } + if overflowIdx != args[2] { + t.Errorf("Expected overflow index: %d, actual: %d", args[2], overflowIdx) + } + } +} diff --git a/fzf/fzf/src/util/util_unix.go b/fzf/fzf/src/util/util_unix.go new file mode 100644 index 0000000..6331275 --- /dev/null +++ b/fzf/fzf/src/util/util_unix.go @@ -0,0 +1,47 @@ +// +build !windows + +package util + +import ( + "os" + "os/exec" + "syscall" +) + +// ExecCommand executes the given command with $SHELL +func ExecCommand(command string, setpgid bool) *exec.Cmd { + shell := os.Getenv("SHELL") + if len(shell) == 0 { + shell = "sh" + } + return ExecCommandWith(shell, command, setpgid) +} + +// ExecCommandWith executes the given command with the specified shell +func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd { + cmd := exec.Command(shell, "-c", command) + if setpgid { + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + } + return cmd +} + +// KillCommand kills the process for the given command +func KillCommand(cmd *exec.Cmd) error { + return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) +} + +// IsWindows returns true on Windows +func IsWindows() bool { + return false +} + +// SetNonblock executes syscall.SetNonblock on file descriptor +func SetNonblock(file *os.File, nonblock bool) { + syscall.SetNonblock(int(file.Fd()), nonblock) +} + +// Read executes syscall.Read on file descriptor +func Read(fd int, b []byte) (int, error) { + return syscall.Read(int(fd), b) +} diff --git a/fzf/fzf/src/util/util_windows.go b/fzf/fzf/src/util/util_windows.go new file mode 100644 index 0000000..e4e0437 --- /dev/null +++ b/fzf/fzf/src/util/util_windows.go @@ -0,0 +1,83 @@ +// +build windows + +package util + +import ( + "fmt" + "os" + "os/exec" + "strings" + "sync/atomic" + "syscall" +) + +var shellPath atomic.Value + +// ExecCommand executes the given command with $SHELL +func ExecCommand(command string, setpgid bool) *exec.Cmd { + var shell string + if cached := shellPath.Load(); cached != nil { + shell = cached.(string) + } else { + shell = os.Getenv("SHELL") + if len(shell) == 0 { + shell = "cmd" + } else if strings.Contains(shell, "/") { + out, err := exec.Command("cygpath", "-w", shell).Output() + if err == nil { + shell = strings.Trim(string(out), "\n") + } + } + shellPath.Store(shell) + } + return ExecCommandWith(shell, command, setpgid) +} + +// ExecCommandWith executes the given command with the specified shell +// FIXME: setpgid is unused. We set it in the Unix implementation so that we +// can kill preview process with its child processes at once. +// NOTE: For "powershell", we should ideally set output encoding to UTF8, +// but it is left as is now because no adverse effect has been observed. +func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd { + var cmd *exec.Cmd + if strings.Contains(shell, "cmd") { + cmd = exec.Command(shell) + cmd.SysProcAttr = &syscall.SysProcAttr{ + HideWindow: false, + CmdLine: fmt.Sprintf(` /v:on/s/c "%s"`, command), + CreationFlags: 0, + } + return cmd + } + + if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") { + cmd = exec.Command(shell, "-NoProfile", "-Command", command) + } else { + cmd = exec.Command(shell, "-c", command) + } + cmd.SysProcAttr = &syscall.SysProcAttr{ + HideWindow: false, + CreationFlags: 0, + } + return cmd +} + +// KillCommand kills the process for the given command +func KillCommand(cmd *exec.Cmd) error { + return cmd.Process.Kill() +} + +// IsWindows returns true on Windows +func IsWindows() bool { + return true +} + +// SetNonblock executes syscall.SetNonblock on file descriptor +func SetNonblock(file *os.File, nonblock bool) { + syscall.SetNonblock(syscall.Handle(file.Fd()), nonblock) +} + +// Read executes syscall.Read on file descriptor +func Read(fd int, b []byte) (int, error) { + return syscall.Read(syscall.Handle(fd), b) +} diff --git a/fzf/fzf/test/fzf.vader b/fzf/fzf/test/fzf.vader new file mode 100644 index 0000000..07f0c8d --- /dev/null +++ b/fzf/fzf/test/fzf.vader @@ -0,0 +1,175 @@ +Execute (Setup): + let g:dir = fnamemodify(g:vader_file, ':p:h') + unlet! g:fzf_layout g:fzf_action g:fzf_history_dir + Log 'Test directory: ' . g:dir + Save &acd + +Execute (fzf#run with dir option): + let cwd = getcwd() + let result = fzf#run({ 'source': 'git ls-files', 'options': '--filter=vdr', 'dir': g:dir }) + AssertEqual ['fzf.vader'], result + AssertEqual 0, haslocaldir() + AssertEqual getcwd(), cwd + + execute 'lcd' fnameescape(cwd) + let result = sort(fzf#run({ 'source': 'git ls-files', 'options': '--filter e', 'dir': g:dir })) + AssertEqual ['fzf.vader', 'test_go.rb'], result + AssertEqual 1, haslocaldir() + AssertEqual getcwd(), cwd + +Execute (fzf#run with Funcref command): + let g:ret = [] + function! g:FzfTest(e) + call add(g:ret, a:e) + endfunction + let result = sort(fzf#run({ 'source': 'git ls-files', 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir })) + AssertEqual ['fzf.vader', 'test_go.rb'], result + AssertEqual ['fzf.vader', 'test_go.rb'], sort(g:ret) + +Execute (fzf#run with string source): + let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' })) + AssertEqual ['hi'], result + +Execute (fzf#run with list source): + let result = sort(fzf#run({ 'source': ['hello', 'world'], 'options': '-f e' })) + AssertEqual ['hello'], result + let result = sort(fzf#run({ 'source': ['hello', 'world'], 'options': '-f o' })) + AssertEqual ['hello', 'world'], result + +Execute (fzf#run with string source): + let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' })) + AssertEqual ['hi'], result + +Execute (fzf#run with dir option and noautochdir): + set noacd + let cwd = getcwd() + call fzf#run({'source': ['/foobar'], 'sink': 'e', 'dir': '/tmp', 'options': '-1'}) + " No change in working directory + AssertEqual cwd, getcwd() + + call fzf#run({'source': ['/foobar'], 'sink': 'tabe', 'dir': '/tmp', 'options': '-1'}) + AssertEqual cwd, getcwd() + tabclose + AssertEqual cwd, getcwd() + +Execute (Incomplete fzf#run with dir option and autochdir): + set acd + let cwd = getcwd() + call fzf#run({'source': [], 'sink': 'e', 'dir': '/tmp', 'options': '-0'}) + " No change in working directory even if &acd is set + AssertEqual cwd, getcwd() + +Execute (FIXME: fzf#run with dir option and autochdir): + set acd + call fzf#run({'source': ['/foobar'], 'sink': 'e', 'dir': '/tmp', 'options': '-1'}) + " Working directory changed due to &acd + AssertEqual '/foobar', expand('%') + AssertEqual '/', getcwd() + +Execute (fzf#run with dir option and autochdir when final cwd is same as dir): + set acd + cd /tmp + call fzf#run({'source': ['/foobar'], 'sink': 'e', 'dir': '/', 'options': '-1'}) + " Working directory changed due to &acd + AssertEqual '/', getcwd() + +Execute (fzf#wrap): + AssertThrows fzf#wrap({'foo': 'bar'}) + + let opts = fzf#wrap('foobar') + Log opts + AssertEqual '~40%', opts.down + Assert opts.options =~ '--expect=' + Assert !has_key(opts, 'sink') + Assert has_key(opts, 'sink*') + + let opts = fzf#wrap('foobar', {}, 0) + Log opts + AssertEqual '~40%', opts.down + + let opts = fzf#wrap('foobar', {}, 1) + Log opts + Assert !has_key(opts, 'down') + + let opts = fzf#wrap('foobar', {'down': '50%'}) + Log opts + AssertEqual '50%', opts.down + + let opts = fzf#wrap('foobar', {'down': '50%'}, 1) + Log opts + Assert !has_key(opts, 'down') + + let opts = fzf#wrap('foobar', {'sink': 'e'}) + Log opts + AssertEqual 'e', opts.sink + Assert !has_key(opts, 'sink*') + + let opts = fzf#wrap('foobar', {'options': '--reverse'}) + Log opts + Assert opts.options =~ '--expect=' + Assert opts.options =~ '--reverse' + + let g:fzf_layout = {'window': 'enew'} + let opts = fzf#wrap('foobar') + Log opts + AssertEqual 'enew', opts.window + + let opts = fzf#wrap('foobar', {}, 1) + Log opts + Assert !has_key(opts, 'window') + + let opts = fzf#wrap('foobar', {'right': '50%'}) + Log opts + Assert !has_key(opts, 'window') + AssertEqual '50%', opts.right + + let opts = fzf#wrap('foobar', {'right': '50%'}, 1) + Log opts + Assert !has_key(opts, 'window') + Assert !has_key(opts, 'right') + + let g:fzf_action = {'a': 'tabe'} + let opts = fzf#wrap('foobar') + Log opts + Assert opts.options =~ '--expect=a' + Assert !has_key(opts, 'sink') + Assert has_key(opts, 'sink*') + + let opts = fzf#wrap('foobar', {'sink': 'e'}) + Log opts + AssertEqual 'e', opts.sink + Assert !has_key(opts, 'sink*') + + let g:fzf_history_dir = '/tmp' + let opts = fzf#wrap('foobar', {'options': '--color light'}) + Log opts + Assert opts.options =~ "--history '/tmp/foobar'" + Assert opts.options =~ '--color light' + + let g:fzf_colors = { 'fg': ['fg', 'Error'] } + let opts = fzf#wrap({}) + Assert opts.options =~ '^--color=fg:' + +Execute (fzf#shellescape with sh): + AssertEqual '''''', fzf#shellescape('', 'sh') + AssertEqual '''\''', fzf#shellescape('\', 'sh') + AssertEqual '''""''', fzf#shellescape('""', 'sh') + AssertEqual '''foobar>''', fzf#shellescape('foobar>', 'sh') + AssertEqual '''\\\"\\\''', fzf#shellescape('\\\"\\\', 'sh') + AssertEqual '''echo ''\''''a''\'''' && echo ''\''''b''\''''''', fzf#shellescape('echo ''a'' && echo ''b''', 'sh') + +Execute (fzf#shellescape with cmd.exe): + AssertEqual '^"^"', fzf#shellescape('', 'cmd.exe') + AssertEqual '^"\\^"', fzf#shellescape('\', 'cmd.exe') + AssertEqual '^"\^"\^"^"', fzf#shellescape('""', 'cmd.exe') + AssertEqual '^"foobar^>^"', fzf#shellescape('foobar>', 'cmd.exe') + AssertEqual '^"\\\\\\\^"\\\\\\^"', fzf#shellescape('\\\"\\\', 'cmd.exe') + AssertEqual '^"echo ''a'' ^&^& echo ''b''^"', fzf#shellescape('echo ''a'' && echo ''b''', 'cmd.exe') + + AssertEqual '^"C:\Program Files ^(x86^)\\^"', fzf#shellescape('C:\Program Files (x86)\', 'cmd.exe') + AssertEqual '^"C:/Program Files ^(x86^)/^"', fzf#shellescape('C:/Program Files (x86)/', 'cmd.exe') + AssertEqual '^"%%USERPROFILE%%^"', fzf#shellescape('%USERPROFILE%', 'cmd.exe') + +Execute (Cleanup): + unlet g:dir + Restore diff --git a/fzf/fzf/test/test_go.rb b/fzf/fzf/test/test_go.rb new file mode 100755 index 0000000..95759bf --- /dev/null +++ b/fzf/fzf/test/test_go.rb @@ -0,0 +1,2692 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'minitest/autorun' +require 'fileutils' +require 'English' +require 'shellwords' +require 'erb' +require 'tempfile' + +TEMPLATE = DATA.read +UNSETS = %w[ + FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS + FZF_TMUX FZF_TMUX_OPTS + FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS + FZF_ALT_C_COMMAND + FZF_ALT_C_OPTS FZF_CTRL_R_OPTS + fish_history +].freeze +DEFAULT_TIMEOUT = 10 + +FILE = File.expand_path(__FILE__) +BASE = File.expand_path('..', __dir__) +Dir.chdir(BASE) +FZF = "FZF_DEFAULT_OPTS= FZF_DEFAULT_COMMAND= #{BASE}/bin/fzf" + +def wait + since = Time.now + begin + yield or raise Minitest::Assertion, 'Assertion failure' + rescue Minitest::Assertion + raise if Time.now - since > DEFAULT_TIMEOUT + + sleep(0.05) + retry + end +end + +class Shell + class << self + def bash + @bash ||= + begin + bashrc = '/tmp/fzf.bash' + File.open(bashrc, 'w') do |f| + f.puts ERB.new(TEMPLATE).result(binding) + end + + "bash --rcfile #{bashrc}" + end + end + + def zsh + @zsh ||= + begin + zdotdir = '/tmp/fzf-zsh' + FileUtils.rm_rf(zdotdir) + FileUtils.mkdir_p(zdotdir) + File.open("#{zdotdir}/.zshrc", 'w') do |f| + f.puts ERB.new(TEMPLATE).result(binding) + end + "ZDOTDIR=#{zdotdir} zsh" + end + end + + def fish + UNSETS.map { |v| v + '= ' }.join + 'fish' + end + end +end + +class Tmux + attr_reader :win + + def initialize(shell = :bash) + @win = go(%W[new-window -d -P -F #I #{Shell.send(shell)}]).first + go(%W[set-window-option -t #{@win} pane-base-index 0]) + return unless shell == :fish + + send_keys 'function fish_prompt; end; clear', :Enter + self.until(&:empty?) + end + + def kill + go(%W[kill-window -t #{win}]) + end + + def focus + go(%W[select-window -t #{win}]) + end + + def send_keys(*args) + go(%W[send-keys -t #{win}] + args.map(&:to_s)) + end + + def paste(str) + system('tmux', 'setb', str, ';', 'pasteb', '-t', win, ';', 'send-keys', '-t', win, 'Enter') + end + + def capture + go(%W[capture-pane -p -J -t #{win}]).map(&:rstrip).reverse.drop_while(&:empty?).reverse + end + + def until(refresh = false) + lines = nil + begin + wait do + lines = capture + class << lines + def counts + lazy + .map { |l| l.scan(%r{^. ([0-9]+)/([0-9]+)( \(([0-9]+)\))?}) } + .reject(&:empty?) + .first&.first&.map(&:to_i)&.values_at(0, 1, 3) || [0, 0, 0] + end + + def match_count + counts[0] + end + + def item_count + counts[1] + end + + def select_count + counts[2] + end + + def any_include?(val) + method = val.is_a?(Regexp) ? :match : :include? + find { |line| line.send(method, val) } + end + end + yield(lines).tap do |ok| + send_keys 'C-l' if refresh && !ok + end + end + rescue Minitest::Assertion + puts $ERROR_INFO.backtrace + puts '>' * 80 + puts lines + puts '<' * 80 + raise + end + lines + end + + def prepare + tries = 0 + begin + self.until(true) do |lines| + message = "Prepare[#{tries}]" + send_keys ' ', 'C-u', :Enter, message, :Left, :Right + lines[-1] == message + end + rescue Minitest::Assertion + (tries += 1) < 5 ? retry : raise + end + send_keys 'C-u', 'C-l' + end + + private + + def go(args) + IO.popen(%w[tmux] + args) { |io| io.readlines(chomp: true) } + end +end + +class TestBase < Minitest::Test + TEMPNAME = '/tmp/output' + + attr_reader :tmux + + def tempname + @temp_suffix ||= 0 + [TEMPNAME, + caller_locations.map(&:label).find { |l| l.start_with?('test_') }, + @temp_suffix].join('-') + end + + def writelines(path, lines) + File.unlink(path) while File.exist?(path) + File.open(path, 'w') { |f| f.puts lines } + end + + def readonce + wait { assert_path_exists tempname } + File.read(tempname) + ensure + File.unlink(tempname) while File.exist?(tempname) + @temp_suffix += 1 + tmux.prepare + end + + def fzf(*opts) + fzf!(*opts) + " > #{tempname}.tmp; mv #{tempname}.tmp #{tempname}" + end + + def fzf!(*opts) + opts = opts.map do |o| + case o + when Symbol + o = o.to_s + o.length > 1 ? "--#{o.tr('_', '-')}" : "-#{o}" + when String, Numeric + o.to_s + end + end.compact + "#{FZF} #{opts.join(' ')}" + end +end + +class TestGoFZF < TestBase + def setup + super + @tmux = Tmux.new + end + + def teardown + @tmux.kill + end + + def test_vanilla + tmux.send_keys "seq 1 100000 | #{fzf}", :Enter + tmux.until do |lines| + assert_equal '>', lines.last + assert_equal ' 100000/100000', lines[-2] + end + lines = tmux.capture + assert_equal ' 2', lines[-4] + assert_equal '> 1', lines[-3] + assert_equal ' 100000/100000', lines[-2] + assert_equal '>', lines[-1] + + # Testing basic key bindings + tmux.send_keys '99', 'C-a', '1', 'C-f', '3', 'C-b', 'C-h', 'C-u', 'C-e', 'C-y', 'C-k', 'Tab', 'BTab' + tmux.until do |lines| + assert_equal '> 3910', lines[-4] + assert_equal ' 391', lines[-3] + assert_equal ' 856/100000', lines[-2] + assert_equal '> 391', lines[-1] + end + + tmux.send_keys :Enter + assert_equal '3910', readonce.chomp + end + + def test_fzf_default_command + tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND='echo hello'"), :Enter + tmux.until { |lines| assert_equal '> hello', lines[-3] } + + tmux.send_keys :Enter + assert_equal 'hello', readonce.chomp + end + + def test_fzf_default_command_failure + tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', 'FZF_DEFAULT_COMMAND=false'), :Enter + tmux.until { |lines| assert_equal ' [Command failed: false]', lines[-2] } + tmux.send_keys :Enter + end + + def test_key_bindings + tmux.send_keys "#{FZF} -q 'foo bar foo-bar'", :Enter + tmux.until { |lines| assert_equal '> foo bar foo-bar', lines.last } + + # CTRL-A + tmux.send_keys 'C-A', '(' + tmux.until { |lines| assert_equal '> (foo bar foo-bar', lines.last } + + # META-F + tmux.send_keys :Escape, :f, ')' + tmux.until { |lines| assert_equal '> (foo) bar foo-bar', lines.last } + + # CTRL-B + tmux.send_keys 'C-B', 'var' + tmux.until { |lines| assert_equal '> (foovar) bar foo-bar', lines.last } + + # Left, CTRL-D + tmux.send_keys :Left, :Left, 'C-D' + tmux.until { |lines| assert_equal '> (foovr) bar foo-bar', lines.last } + + # META-BS + tmux.send_keys :Escape, :BSpace + tmux.until { |lines| assert_equal '> (r) bar foo-bar', lines.last } + + # CTRL-Y + tmux.send_keys 'C-Y', 'C-Y' + tmux.until { |lines| assert_equal '> (foovfoovr) bar foo-bar', lines.last } + + # META-B + tmux.send_keys :Escape, :b, :Space, :Space + tmux.until { |lines| assert_equal '> ( foovfoovr) bar foo-bar', lines.last } + + # CTRL-F / Right + tmux.send_keys 'C-F', :Right, '/' + tmux.until { |lines| assert_equal '> ( fo/ovfoovr) bar foo-bar', lines.last } + + # CTRL-H / BS + tmux.send_keys 'C-H', :BSpace + tmux.until { |lines| assert_equal '> ( fovfoovr) bar foo-bar', lines.last } + + # CTRL-E + tmux.send_keys 'C-E', 'baz' + tmux.until { |lines| assert_equal '> ( fovfoovr) bar foo-barbaz', lines.last } + + # CTRL-U + tmux.send_keys 'C-U' + tmux.until { |lines| assert_equal '>', lines.last } + + # CTRL-Y + tmux.send_keys 'C-Y' + tmux.until { |lines| assert_equal '> ( fovfoovr) bar foo-barbaz', lines.last } + + # CTRL-W + tmux.send_keys 'C-W', 'bar-foo' + tmux.until { |lines| assert_equal '> ( fovfoovr) bar bar-foo', lines.last } + + # META-D + tmux.send_keys :Escape, :b, :Escape, :b, :Escape, :d, 'C-A', 'C-Y' + tmux.until { |lines| assert_equal '> bar( fovfoovr) bar -foo', lines.last } + + # CTRL-M + tmux.send_keys 'C-M' + tmux.until { |lines| refute_equal '>', lines.last } + end + + def test_file_word + tmux.send_keys "#{FZF} -q '--/foo bar/foo-bar/baz' --filepath-word", :Enter + tmux.until { |lines| assert_equal '> --/foo bar/foo-bar/baz', lines.last } + + tmux.send_keys :Escape, :b + tmux.send_keys :Escape, :b + tmux.send_keys :Escape, :b + tmux.send_keys :Escape, :d + tmux.send_keys :Escape, :f + tmux.send_keys :Escape, :BSpace + tmux.until { |lines| assert_equal '> --///baz', lines.last } + end + + def test_multi_order + tmux.send_keys "seq 1 10 | #{fzf(:multi)}", :Enter + tmux.until { |lines| assert_equal '>', lines.last } + + tmux.send_keys :Tab, :Up, :Up, :Tab, :Tab, :Tab, # 3, 2 + 'C-K', 'C-K', 'C-K', 'C-K', :BTab, :BTab, # 5, 6 + :PgUp, 'C-J', :Down, :Tab, :Tab # 8, 7 + tmux.until { |lines| assert_equal ' 10/10 (6)', lines[-2] } + tmux.send_keys 'C-M' + assert_equal %w[3 2 5 6 8 7], readonce.lines(chomp: true) + end + + def test_multi_max + tmux.send_keys "seq 1 10 | #{FZF} -m 3 --bind A:select-all,T:toggle-all --preview 'echo [{+}]/{}'", :Enter + + tmux.until { |lines| assert_equal 10, lines.item_count } + + tmux.send_keys '1' + tmux.until do |lines| + assert_includes lines[1], ' [1]/1 ' + assert lines[-2]&.start_with?(' 2/10 ') + end + + tmux.send_keys 'A' + tmux.until do |lines| + assert_includes lines[1], ' [1 10]/1 ' + assert lines[-2]&.start_with?(' 2/10 (2/3)') + end + + tmux.send_keys :BSpace + tmux.until { |lines| assert lines[-2]&.start_with?(' 10/10 (2/3)') } + + tmux.send_keys 'T' + tmux.until do |lines| + assert_includes lines[1], ' [2 3 4]/1 ' + assert lines[-2]&.start_with?(' 10/10 (3/3)') + end + + %w[T A].each do |key| + tmux.send_keys key + tmux.until do |lines| + assert_includes lines[1], ' [1 5 6]/1 ' + assert lines[-2]&.start_with?(' 10/10 (3/3)') + end + end + + tmux.send_keys :BTab + tmux.until do |lines| + assert_includes lines[1], ' [5 6]/2 ' + assert lines[-2]&.start_with?(' 10/10 (2/3)') + end + + [:BTab, :BTab, 'A'].each do |key| + tmux.send_keys key + tmux.until do |lines| + assert_includes lines[1], ' [5 6 2]/3 ' + assert lines[-2]&.start_with?(' 10/10 (3/3)') + end + end + + tmux.send_keys '2' + tmux.until { |lines| assert lines[-2]&.start_with?(' 1/10 (3/3)') } + + tmux.send_keys 'T' + tmux.until do |lines| + assert_includes lines[1], ' [5 6]/2 ' + assert lines[-2]&.start_with?(' 1/10 (2/3)') + end + + tmux.send_keys :BSpace + tmux.until { |lines| assert lines[-2]&.start_with?(' 10/10 (2/3)') } + + tmux.send_keys 'A' + tmux.until do |lines| + assert_includes lines[1], ' [5 6 1]/1 ' + assert lines[-2]&.start_with?(' 10/10 (3/3)') + end + end + + def test_with_nth + [true, false].each do |multi| + tmux.send_keys "(echo ' 1st 2nd 3rd/'; + echo ' first second third/') | + #{fzf(multi && :multi, :x, :nth, 2, :with_nth, '2,-1,1')}", + :Enter + tmux.until { |lines| assert_equal multi ? ' 2/2 (0)' : ' 2/2', lines[-2] } + + # Transformed list + lines = tmux.capture + assert_equal ' second third/first', lines[-4] + assert_equal '> 2nd 3rd/1st', lines[-3] + + # However, the output must not be transformed + if multi + tmux.send_keys :BTab, :BTab + tmux.until { |lines| assert_equal ' 2/2 (2)', lines[-2] } + tmux.send_keys :Enter + assert_equal [' 1st 2nd 3rd/', ' first second third/'], readonce.lines(chomp: true) + else + tmux.send_keys '^', '3' + tmux.until { |lines| assert_equal ' 1/2', lines[-2] } + tmux.send_keys :Enter + assert_equal [' 1st 2nd 3rd/'], readonce.lines(chomp: true) + end + end + end + + def test_scroll + [true, false].each do |rev| + tmux.send_keys "seq 1 100 | #{fzf(rev && :reverse)}", :Enter + tmux.until { |lines| assert_includes lines, ' 100/100' } + tmux.send_keys(*Array.new(110) { rev ? :Down : :Up }) + tmux.until { |lines| assert_includes lines, '> 100' } + tmux.send_keys :Enter + assert_equal '100', readonce.chomp + end + end + + def test_select_1 + tmux.send_keys "seq 1 100 | #{fzf(:with_nth, '..,..', :print_query, :q, 5555, :'1')}", :Enter + assert_equal %w[5555 55], readonce.lines(chomp: true) + end + + def test_exit_0 + tmux.send_keys "seq 1 100 | #{fzf(:with_nth, '..,..', :print_query, :q, 555_555, :'0')}", :Enter + assert_equal %w[555555], readonce.lines(chomp: true) + end + + def test_select_1_exit_0_fail + [:'0', :'1', %i[1 0]].each do |opt| + tmux.send_keys "seq 1 100 | #{fzf(:print_query, :multi, :q, 5, *opt)}", :Enter + tmux.until { |lines| assert_equal '> 5', lines.last } + tmux.send_keys :BTab, :BTab, :BTab + tmux.until { |lines| assert_equal ' 19/100 (3)', lines[-2] } + tmux.send_keys :Enter + assert_equal %w[5 5 50 51], readonce.lines(chomp: true) + end + end + + def test_query_unicode + tmux.paste "(echo abc; echo $'\\352\\260\\200\\353\\202\\230\\353\\213\\244') | #{fzf(:query, "$'\\352\\260\\200\\353\\213\\244'")}" + tmux.until { |lines| assert_equal ' 1/2', lines[-2] } + tmux.send_keys :Enter + assert_equal %w[가나다], readonce.lines(chomp: true) + end + + def test_sync + tmux.send_keys "seq 1 100 | #{fzf!(:multi)} | awk '{print $1 $1}' | #{fzf(:sync)}", :Enter + tmux.until { |lines| assert_equal '>', lines[-1] } + tmux.send_keys 9 + tmux.until { |lines| assert_equal ' 19/100 (0)', lines[-2] } + tmux.send_keys :BTab, :BTab, :BTab + tmux.until { |lines| assert_equal ' 19/100 (3)', lines[-2] } + tmux.send_keys :Enter + tmux.until { |lines| assert_equal '>', lines[-1] } + tmux.send_keys 'C-K', :Enter + assert_equal %w[9090], readonce.lines(chomp: true) + end + + def test_tac + tmux.send_keys "seq 1 1000 | #{fzf(:tac, :multi)}", :Enter + tmux.until { |lines| assert_equal ' 1000/1000 (0)', lines[-2] } + tmux.send_keys :BTab, :BTab, :BTab + tmux.until { |lines| assert_equal ' 1000/1000 (3)', lines[-2] } + tmux.send_keys :Enter + assert_equal %w[1000 999 998], readonce.lines(chomp: true) + end + + def test_tac_sort + tmux.send_keys "seq 1 1000 | #{fzf(:tac, :multi)}", :Enter + tmux.until { |lines| assert_equal ' 1000/1000 (0)', lines[-2] } + tmux.send_keys '99' + tmux.until { |lines| assert_equal ' 28/1000 (0)', lines[-2] } + tmux.send_keys :BTab, :BTab, :BTab + tmux.until { |lines| assert_equal ' 28/1000 (3)', lines[-2] } + tmux.send_keys :Enter + assert_equal %w[99 999 998], readonce.lines(chomp: true) + end + + def test_tac_nosort + tmux.send_keys "seq 1 1000 | #{fzf(:tac, :no_sort, :multi)}", :Enter + tmux.until { |lines| assert_equal ' 1000/1000 (0)', lines[-2] } + tmux.send_keys '00' + tmux.until { |lines| assert_equal ' 10/1000 (0)', lines[-2] } + tmux.send_keys :BTab, :BTab, :BTab + tmux.until { |lines| assert_equal ' 10/1000 (3)', lines[-2] } + tmux.send_keys :Enter + assert_equal %w[1000 900 800], readonce.lines(chomp: true) + end + + def test_expect + test = lambda do |key, feed, expected = key| + tmux.send_keys "seq 1 100 | #{fzf(:expect, key)}", :Enter + tmux.until { |lines| assert_equal ' 100/100', lines[-2] } + tmux.send_keys '55' + tmux.until { |lines| assert_equal ' 1/100', lines[-2] } + tmux.send_keys(*feed) + tmux.prepare + assert_equal [expected, '55'], readonce.lines(chomp: true) + end + test.call('ctrl-t', 'C-T') + test.call('ctrl-t', 'Enter', '') + test.call('alt-c', %i[Escape c]) + test.call('f1', 'f1') + test.call('f2', 'f2') + test.call('f3', 'f3') + test.call('f2,f4', 'f2', 'f2') + test.call('f2,f4', 'f4', 'f4') + test.call('alt-/', %i[Escape /]) + %w[f5 f6 f7 f8 f9 f10].each do |key| + test.call('f5,f6,f7,f8,f9,f10', key, key) + end + test.call('@', '@') + end + + def test_expect_print_query + tmux.send_keys "seq 1 100 | #{fzf('--expect=alt-z', :print_query)}", :Enter + tmux.until { |lines| assert_equal ' 100/100', lines[-2] } + tmux.send_keys '55' + tmux.until { |lines| assert_equal ' 1/100', lines[-2] } + tmux.send_keys :Escape, :z + assert_equal %w[55 alt-z 55], readonce.lines(chomp: true) + end + + def test_expect_printable_character_print_query + tmux.send_keys "seq 1 100 | #{fzf('--expect=z --print-query')}", :Enter + tmux.until { |lines| assert_equal ' 100/100', lines[-2] } + tmux.send_keys '55' + tmux.until { |lines| assert_equal ' 1/100', lines[-2] } + tmux.send_keys 'z' + assert_equal %w[55 z 55], readonce.lines(chomp: true) + end + + def test_expect_print_query_select_1 + tmux.send_keys "seq 1 100 | #{fzf('-q55 -1 --expect=alt-z --print-query')}", :Enter + assert_equal ['55', '', '55'], readonce.lines(chomp: true) + end + + def test_toggle_sort + ['--toggle-sort=ctrl-r', '--bind=ctrl-r:toggle-sort'].each do |opt| + tmux.send_keys "seq 1 111 | #{fzf("-m +s --tac #{opt} -q11")}", :Enter + tmux.until { |lines| assert_equal '> 111', lines[-3] } + tmux.send_keys :Tab + tmux.until { |lines| assert_equal ' 4/111 -S (1)', lines[-2] } + tmux.send_keys 'C-R' + tmux.until { |lines| assert_equal '> 11', lines[-3] } + tmux.send_keys :Tab + tmux.until { |lines| assert_equal ' 4/111 +S (2)', lines[-2] } + tmux.send_keys :Enter + assert_equal %w[111 11], readonce.lines(chomp: true) + end + end + + def test_unicode_case + writelines(tempname, %w[строКА1 СТРОКА2 строка3 Строка4]) + assert_equal %w[СТРОКА2 Строка4], `#{FZF} -fС < #{tempname}`.lines(chomp: true) + assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `#{FZF} -fс < #{tempname}`.lines(chomp: true) + end + + def test_tiebreak + input = %w[ + --foobar-------- + -----foobar--- + ----foobar-- + -------foobar- + ] + writelines(tempname, input) + + assert_equal input, `#{FZF} -ffoobar --tiebreak=index < #{tempname}`.lines(chomp: true) + + by_length = %w[ + ----foobar-- + -----foobar--- + -------foobar- + --foobar-------- + ] + assert_equal by_length, `#{FZF} -ffoobar < #{tempname}`.lines(chomp: true) + assert_equal by_length, `#{FZF} -ffoobar --tiebreak=length < #{tempname}`.lines(chomp: true) + + by_begin = %w[ + --foobar-------- + ----foobar-- + -----foobar--- + -------foobar- + ] + assert_equal by_begin, `#{FZF} -ffoobar --tiebreak=begin < #{tempname}`.lines(chomp: true) + assert_equal by_begin, `#{FZF} -f"!z foobar" -x --tiebreak begin < #{tempname}`.lines(chomp: true) + + assert_equal %w[ + -------foobar- + ----foobar-- + -----foobar--- + --foobar-------- + ], `#{FZF} -ffoobar --tiebreak end < #{tempname}`.lines(chomp: true) + + assert_equal input, `#{FZF} -f"!z" -x --tiebreak end < #{tempname}`.lines(chomp: true) + end + + def test_tiebreak_index_begin + writelines(tempname, [ + 'xoxxxxxoxx', + 'xoxxxxxox', + 'xxoxxxoxx', + 'xxxoxoxxx', + 'xxxxoxox', + ' xxoxoxxx' + ]) + + assert_equal [ + 'xxxxoxox', + ' xxoxoxxx', + 'xxxoxoxxx', + 'xxoxxxoxx', + 'xoxxxxxox', + 'xoxxxxxoxx' + ], `#{FZF} -foo < #{tempname}`.lines(chomp: true) + + assert_equal [ + 'xxxoxoxxx', + 'xxxxoxox', + ' xxoxoxxx', + 'xxoxxxoxx', + 'xoxxxxxoxx', + 'xoxxxxxox' + ], `#{FZF} -foo --tiebreak=index < #{tempname}`.lines(chomp: true) + + # Note that --tiebreak=begin is now based on the first occurrence of the + # first character on the pattern + assert_equal [ + ' xxoxoxxx', + 'xxxoxoxxx', + 'xxxxoxox', + 'xxoxxxoxx', + 'xoxxxxxoxx', + 'xoxxxxxox' + ], `#{FZF} -foo --tiebreak=begin < #{tempname}`.lines(chomp: true) + + assert_equal [ + ' xxoxoxxx', + 'xxxoxoxxx', + 'xxxxoxox', + 'xxoxxxoxx', + 'xoxxxxxox', + 'xoxxxxxoxx' + ], `#{FZF} -foo --tiebreak=begin,length < #{tempname}`.lines(chomp: true) + end + + def test_tiebreak_begin_algo_v2 + writelines(tempname, [ + 'baz foo bar', + 'foo bar baz' + ]) + assert_equal [ + 'foo bar baz', + 'baz foo bar' + ], `#{FZF} -fbar --tiebreak=begin --algo=v2 < #{tempname}`.lines(chomp: true) + end + + def test_tiebreak_end + writelines(tempname, [ + 'xoxxxxxxxx', + 'xxoxxxxxxx', + 'xxxoxxxxxx', + 'xxxxoxxxx', + 'xxxxxoxxx', + ' xxxxoxxx' + ]) + + assert_equal [ + ' xxxxoxxx', + 'xxxxoxxxx', + 'xxxxxoxxx', + 'xoxxxxxxxx', + 'xxoxxxxxxx', + 'xxxoxxxxxx' + ], `#{FZF} -fo < #{tempname}`.lines(chomp: true) + + assert_equal [ + 'xxxxxoxxx', + ' xxxxoxxx', + 'xxxxoxxxx', + 'xxxoxxxxxx', + 'xxoxxxxxxx', + 'xoxxxxxxxx' + ], `#{FZF} -fo --tiebreak=end < #{tempname}`.lines(chomp: true) + + assert_equal [ + 'xxxxxoxxx', + ' xxxxoxxx', + 'xxxxoxxxx', + 'xxxoxxxxxx', + 'xxoxxxxxxx', + 'xoxxxxxxxx' + ], `#{FZF} -fo --tiebreak=end,length,begin < #{tempname}`.lines(chomp: true) + end + + def test_tiebreak_length_with_nth + input = %w[ + 1:hell + 123:hello + 12345:he + 1234567:h + ] + writelines(tempname, input) + + output = %w[ + 1:hell + 12345:he + 123:hello + 1234567:h + ] + assert_equal output, `#{FZF} -fh < #{tempname}`.lines(chomp: true) + + # Since 0.16.8, --nth doesn't affect --tiebreak + assert_equal output, `#{FZF} -fh -n2 -d: < #{tempname}`.lines(chomp: true) + end + + def test_invalid_cache + tmux.send_keys "(echo d; echo D; echo x) | #{fzf('-q d')}", :Enter + tmux.until { |lines| assert_equal ' 2/3', lines[-2] } + tmux.send_keys :BSpace + tmux.until { |lines| assert_equal ' 3/3', lines[-2] } + tmux.send_keys :D + tmux.until { |lines| assert_equal ' 1/3', lines[-2] } + tmux.send_keys :Enter + end + + def test_invalid_cache_query_type + command = %[(echo 'foo$bar'; echo 'barfoo'; echo 'foo^bar'; echo "foo'1-2"; seq 100) | #{fzf}] + + # Suffix match + tmux.send_keys command, :Enter + tmux.until { |lines| assert_equal 104, lines.match_count } + tmux.send_keys 'foo$' + tmux.until { |lines| assert_equal 1, lines.match_count } + tmux.send_keys 'bar' + tmux.until { |lines| assert_equal 1, lines.match_count } + tmux.send_keys :Enter + + # Prefix match + tmux.prepare + tmux.send_keys command, :Enter + tmux.until { |lines| assert_equal 104, lines.match_count } + tmux.send_keys '^bar' + tmux.until { |lines| assert_equal 1, lines.match_count } + tmux.send_keys 'C-a', 'foo' + tmux.until { |lines| assert_equal 1, lines.match_count } + tmux.send_keys :Enter + + # Exact match + tmux.prepare + tmux.send_keys command, :Enter + tmux.until { |lines| assert_equal 104, lines.match_count } + tmux.send_keys "'12" + tmux.until { |lines| assert_equal 1, lines.match_count } + tmux.send_keys 'C-a', 'foo' + tmux.until { |lines| assert_equal 1, lines.match_count } + end + + def test_smart_case_for_each_term + assert_equal 1, `echo Foo bar | #{FZF} -x -f "foo Fbar" | wc -l`.to_i + end + + def test_bind + tmux.send_keys "seq 1 1000 | #{fzf('-m --bind=ctrl-j:accept,u:up,T:toggle-up,t:toggle')}", :Enter + tmux.until { |lines| assert_equal ' 1000/1000 (0)', lines[-2] } + tmux.send_keys 'uuu', 'TTT', 'tt', 'uu', 'ttt', 'C-j' + assert_equal %w[4 5 6 9], readonce.lines(chomp: true) + end + + def test_bind_print_query + tmux.send_keys "seq 1 1000 | #{fzf('-m --bind=ctrl-j:print-query')}", :Enter + tmux.until { |lines| assert_equal ' 1000/1000 (0)', lines[-2] } + tmux.send_keys 'print-my-query', 'C-j' + assert_equal %w[print-my-query], readonce.lines(chomp: true) + end + + def test_bind_replace_query + tmux.send_keys "seq 1 1000 | #{fzf('--print-query --bind=ctrl-j:replace-query')}", :Enter + tmux.send_keys '1' + tmux.until { |lines| assert_equal ' 272/1000', lines[-2] } + tmux.send_keys 'C-k', 'C-j' + tmux.until { |lines| assert_equal ' 29/1000', lines[-2] } + tmux.until { |lines| assert_equal '> 10', lines[-1] } + end + + def test_long_line + data = '.' * 256 * 1024 + File.open(tempname, 'w') do |f| + f << data + end + assert_equal data, `#{FZF} -f . < #{tempname}`.chomp + end + + def test_read0 + lines = `find .`.lines(chomp: true) + assert_equal lines.last, `find . | #{FZF} -e -f "^#{lines.last}$"`.chomp + assert_equal \ + lines.last, + `find . -print0 | #{FZF} --read0 -e -f "^#{lines.last}$"`.chomp + end + + def test_select_all_deselect_all_toggle_all + tmux.send_keys "seq 100 | #{fzf('--bind ctrl-a:select-all,ctrl-d:deselect-all,ctrl-t:toggle-all --multi')}", :Enter + tmux.until { |lines| assert_equal ' 100/100 (0)', lines[-2] } + tmux.send_keys :BTab, :BTab, :BTab + tmux.until { |lines| assert_equal ' 100/100 (3)', lines[-2] } + tmux.send_keys 'C-t' + tmux.until { |lines| assert_equal ' 100/100 (97)', lines[-2] } + tmux.send_keys 'C-a' + tmux.until { |lines| assert_equal ' 100/100 (100)', lines[-2] } + tmux.send_keys :Tab, :Tab + tmux.until { |lines| assert_equal ' 100/100 (98)', lines[-2] } + tmux.send_keys '100' + tmux.until { |lines| assert_equal 1, lines.match_count } + tmux.send_keys 'C-d' + tmux.until { |lines| assert_equal ' 1/100 (97)', lines[-2] } + tmux.send_keys 'C-u' + tmux.until { |lines| assert_equal 100, lines.match_count } + tmux.send_keys 'C-d' + tmux.until { |lines| assert_equal ' 100/100 (0)', lines[-2] } + tmux.send_keys :BTab, :BTab + tmux.until { |lines| assert_equal ' 100/100 (2)', lines[-2] } + tmux.send_keys 0 + tmux.until { |lines| assert_equal ' 10/100 (2)', lines[-2] } + tmux.send_keys 'C-a' + tmux.until { |lines| assert_equal ' 10/100 (12)', lines[-2] } + tmux.send_keys :Enter + assert_equal %w[1 2 10 20 30 40 50 60 70 80 90 100], + readonce.lines(chomp: true) + end + + def test_history + history_file = '/tmp/fzf-test-history' + + # History with limited number of entries + begin + File.unlink(history_file) + rescue StandardError + nil + end + opts = "--history=#{history_file} --history-size=4" + input = %w[00 11 22 33 44] + input.each do |keys| + tmux.prepare + tmux.send_keys "seq 100 | #{fzf(opts)}", :Enter + tmux.until { |lines| assert_equal ' 100/100', lines[-2] } + tmux.send_keys keys + tmux.until { |lines| assert_equal ' 1/100', lines[-2] } + tmux.send_keys :Enter + end + wait do + assert_path_exists history_file + assert_equal input[1..-1], File.readlines(history_file, chomp: true) + end + + # Update history entries (not changed on disk) + tmux.send_keys "seq 100 | #{fzf(opts)}", :Enter + tmux.until { |lines| assert_equal ' 100/100', lines[-2] } + tmux.send_keys 'C-p' + tmux.until { |lines| assert_equal '> 44', lines[-1] } + tmux.send_keys 'C-p' + tmux.until { |lines| assert_equal '> 33', lines[-1] } + tmux.send_keys :BSpace + tmux.until { |lines| assert_equal '> 3', lines[-1] } + tmux.send_keys 1 + tmux.until { |lines| assert_equal '> 31', lines[-1] } + tmux.send_keys 'C-p' + tmux.until { |lines| assert_equal '> 22', lines[-1] } + tmux.send_keys 'C-n' + tmux.until { |lines| assert_equal '> 31', lines[-1] } + tmux.send_keys 0 + tmux.until { |lines| assert_equal '> 310', lines[-1] } + tmux.send_keys :Enter + wait do + assert_path_exists history_file + assert_equal %w[22 33 44 310], File.readlines(history_file, chomp: true) + end + + # Respect --bind option + tmux.send_keys "seq 100 | #{fzf(opts + ' --bind ctrl-p:next-history,ctrl-n:previous-history')}", :Enter + tmux.until { |lines| assert_equal ' 100/100', lines[-2] } + tmux.send_keys 'C-n', 'C-n', 'C-n', 'C-n', 'C-p' + tmux.until { |lines| assert_equal '> 33', lines[-1] } + tmux.send_keys :Enter + ensure + File.unlink(history_file) + end + + def test_execute + output = '/tmp/fzf-test-execute' + opts = %[--bind "alt-a:execute(echo /{}/ >> #{output}),alt-b:execute[echo /{}{}/ >> #{output}],C:execute:echo /{}{}{}/ >> #{output}"] + writelines(tempname, %w[foo'bar foo"bar foo$bar]) + tmux.send_keys "cat #{tempname} | #{fzf(opts)}", :Enter + tmux.until { |lines| assert_equal ' 3/3', lines[-2] } + tmux.send_keys :Escape, :a + tmux.send_keys :Escape, :a + tmux.send_keys :Up + tmux.send_keys :Escape, :b + tmux.send_keys :Escape, :b + tmux.send_keys :Up + tmux.send_keys :C + tmux.send_keys 'barfoo' + tmux.until { |lines| assert_equal ' 0/3', lines[-2] } + tmux.send_keys :Escape, :a + tmux.send_keys :Escape, :b + wait do + assert_path_exists output + assert_equal %w[ + /foo'bar/ /foo'bar/ + /foo"barfoo"bar/ /foo"barfoo"bar/ + /foo$barfoo$barfoo$bar/ + ], File.readlines(output, chomp: true) + end + ensure + begin + File.unlink(output) + rescue StandardError + nil + end + end + + def test_execute_multi + output = '/tmp/fzf-test-execute-multi' + opts = %[--multi --bind "alt-a:execute-multi(echo {}/{+} >> #{output})"] + writelines(tempname, %w[foo'bar foo"bar foo$bar foobar]) + tmux.send_keys "cat #{tempname} | #{fzf(opts)}", :Enter + tmux.until { |lines| assert_equal ' 4/4 (0)', lines[-2] } + tmux.send_keys :Escape, :a + tmux.send_keys :BTab, :BTab, :BTab + tmux.until { |lines| assert_equal ' 4/4 (3)', lines[-2] } + tmux.send_keys :Escape, :a + tmux.send_keys :Tab, :Tab + tmux.until { |lines| assert_equal ' 4/4 (3)', lines[-2] } + tmux.send_keys :Escape, :a + wait do + assert_path_exists output + assert_equal [ + %(foo'bar/foo'bar), + %(foo'bar foo"bar foo$bar/foo'bar foo"bar foo$bar), + %(foo'bar foo"bar foobar/foo'bar foo"bar foobar) + ], File.readlines(output, chomp: true) + end + ensure + begin + File.unlink(output) + rescue StandardError + nil + end + end + + def test_execute_plus_flag + output = tempname + '.tmp' + begin + File.unlink(output) + rescue StandardError + nil + end + writelines(tempname, ['foo bar', '123 456']) + + tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute-silent(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter + + tmux.until { |lines| assert_equal ' 2/2 (0)', lines[-2] } + tmux.send_keys 'xy' + tmux.until { |lines| assert_equal ' 0/2 (0)', lines[-2] } + tmux.send_keys :BSpace + tmux.until { |lines| assert_equal ' 2/2 (0)', lines[-2] } + + tmux.send_keys :Up + tmux.send_keys :Tab + tmux.send_keys 'xy' + tmux.until { |lines| assert_equal ' 0/2 (1)', lines[-2] } + tmux.send_keys :BSpace + tmux.until { |lines| assert_equal ' 2/2 (1)', lines[-2] } + + tmux.send_keys :Tab + tmux.send_keys 'xy' + tmux.until { |lines| assert_equal ' 0/2 (2)', lines[-2] } + tmux.send_keys :BSpace + tmux.until { |lines| assert_equal ' 2/2 (2)', lines[-2] } + + wait do + assert_path_exists output + assert_equal [ + %(foo bar/foo bar/bar/bar), + %(123 456/foo bar/456/bar), + %(123 456 foo bar/foo bar/456 bar/bar) + ], File.readlines(output, chomp: true) + end + rescue StandardError + begin + File.unlink(output) + rescue StandardError + nil + end + end + + def test_execute_shell + # Custom script to use as $SHELL + output = tempname + '.out' + begin + File.unlink(output) + rescue StandardError + nil + end + writelines(tempname, + ['#!/usr/bin/env bash', "echo $1 / $2 > #{output}"]) + system("chmod +x #{tempname}") + + tmux.send_keys "echo foo | SHELL=#{tempname} fzf --bind 'enter:execute:{}bar'", :Enter + tmux.until { |lines| assert_equal ' 1/1', lines[-2] } + tmux.send_keys :Enter + tmux.until { |lines| assert_equal ' 1/1', lines[-2] } + wait do + assert_path_exists output + assert_equal ["-c / 'foo'bar"], File.readlines(output, chomp: true) + end + ensure + begin + File.unlink(output) + rescue StandardError + nil + end + end + + def test_cycle + tmux.send_keys "seq 8 | #{fzf(:cycle)}", :Enter + tmux.until { |lines| assert_equal ' 8/8', lines[-2] } + tmux.send_keys :Down + tmux.until { |lines| assert_equal '> 8', lines[-10] } + tmux.send_keys :Down + tmux.until { |lines| assert_equal '> 7', lines[-9] } + tmux.send_keys :Up + tmux.until { |lines| assert_equal '> 8', lines[-10] } + tmux.send_keys :PgUp + tmux.until { |lines| assert_equal '> 8', lines[-10] } + tmux.send_keys :Up + tmux.until { |lines| assert_equal '> 1', lines[-3] } + tmux.send_keys :PgDn + tmux.until { |lines| assert_equal '> 1', lines[-3] } + tmux.send_keys :Down + tmux.until { |lines| assert_equal '> 8', lines[-10] } + end + + def test_header_lines + tmux.send_keys "seq 100 | #{fzf('--header-lines=10 -q 5')}", :Enter + 2.times do + tmux.until do |lines| + assert_equal ' 18/90', lines[-2] + assert_equal ' 1', lines[-3] + assert_equal ' 2', lines[-4] + assert_equal '> 50', lines[-13] + end + tmux.send_keys :Down + end + tmux.send_keys :Enter + assert_equal '50', readonce.chomp + end + + def test_header_lines_reverse + tmux.send_keys "seq 100 | #{fzf('--header-lines=10 -q 5 --reverse')}", :Enter + 2.times do + tmux.until do |lines| + assert_equal ' 18/90', lines[1] + assert_equal ' 1', lines[2] + assert_equal ' 2', lines[3] + assert_equal '> 50', lines[12] + end + tmux.send_keys :Up + end + tmux.send_keys :Enter + assert_equal '50', readonce.chomp + end + + def test_header_lines_reverse_list + tmux.send_keys "seq 100 | #{fzf('--header-lines=10 -q 5 --layout=reverse-list')}", :Enter + 2.times do + tmux.until do |lines| + assert_equal '> 50', lines[0] + assert_equal ' 2', lines[-4] + assert_equal ' 1', lines[-3] + assert_equal ' 18/90', lines[-2] + end + tmux.send_keys :Up + end + tmux.send_keys :Enter + assert_equal '50', readonce.chomp + end + + def test_header_lines_overflow + tmux.send_keys "seq 100 | #{fzf('--header-lines=200')}", :Enter + tmux.until do |lines| + assert_equal ' 0/0', lines[-2] + assert_equal ' 1', lines[-3] + end + tmux.send_keys :Enter + assert_equal '', readonce.chomp + end + + def test_header_lines_with_nth + tmux.send_keys "seq 100 | #{fzf('--header-lines 5 --with-nth 1,1,1,1,1')}", :Enter + tmux.until do |lines| + assert_equal ' 95/95', lines[-2] + assert_equal ' 11111', lines[-3] + assert_equal ' 55555', lines[-7] + assert_equal '> 66666', lines[-8] + end + tmux.send_keys :Enter + assert_equal '6', readonce.chomp + end + + def test_header + tmux.send_keys "seq 100 | #{fzf("--header \"$(head -5 #{FILE})\"")}", :Enter + header = File.readlines(FILE, chomp: true).take(5) + tmux.until do |lines| + assert_equal ' 100/100', lines[-2] + assert_equal header.map { |line| " #{line}".rstrip }, lines[-7..-3] + assert_equal '> 1', lines[-8] + end + end + + def test_header_reverse + tmux.send_keys "seq 100 | #{fzf("--header \"$(head -5 #{FILE})\" --reverse")}", :Enter + header = File.readlines(FILE, chomp: true).take(5) + tmux.until do |lines| + assert_equal ' 100/100', lines[1] + assert_equal header.map { |line| " #{line}".rstrip }, lines[2..6] + assert_equal '> 1', lines[7] + end + end + + def test_header_reverse_list + tmux.send_keys "seq 100 | #{fzf("--header \"$(head -5 #{FILE})\" --layout=reverse-list")}", :Enter + header = File.readlines(FILE, chomp: true).take(5) + tmux.until do |lines| + assert_equal ' 100/100', lines[-2] + assert_equal header.map { |line| " #{line}".rstrip }, lines[-7..-3] + assert_equal '> 1', lines[0] + end + end + + def test_header_and_header_lines + tmux.send_keys "seq 100 | #{fzf("--header-lines 10 --header \"$(head -5 #{FILE})\"")}", :Enter + header = File.readlines(FILE, chomp: true).take(5) + tmux.until do |lines| + assert_equal ' 90/90', lines[-2] + assert_equal header.map { |line| " #{line}".rstrip }, lines[-7...-2] + assert_equal (' 1'..' 10').to_a.reverse, lines[-17...-7] + end + end + + def test_header_and_header_lines_reverse + tmux.send_keys "seq 100 | #{fzf("--reverse --header-lines 10 --header \"$(head -5 #{FILE})\"")}", :Enter + header = File.readlines(FILE, chomp: true).take(5) + tmux.until do |lines| + assert_equal ' 90/90', lines[1] + assert_equal header.map { |line| " #{line}".rstrip }, lines[2...7] + assert_equal (' 1'..' 10').to_a, lines[7...17] + end + end + + def test_header_and_header_lines_reverse_list + tmux.send_keys "seq 100 | #{fzf("--layout=reverse-list --header-lines 10 --header \"$(head -5 #{FILE})\"")}", :Enter + header = File.readlines(FILE, chomp: true).take(5) + tmux.until do |lines| + assert_equal ' 90/90', lines[-2] + assert_equal header.map { |line| " #{line}".rstrip }, lines[-7...-2] + assert_equal (' 1'..' 10').to_a.reverse, lines[-17...-7] + end + end + + def test_cancel + tmux.send_keys "seq 10 | #{fzf('--bind 2:cancel')}", :Enter + tmux.until { |lines| assert_equal ' 10/10', lines[-2] } + tmux.send_keys '123' + tmux.until do |lines| + assert_equal '> 3', lines[-1] + assert_equal ' 1/10', lines[-2] + end + tmux.send_keys 'C-y', 'C-y' + tmux.until { |lines| assert_equal '> 311', lines[-1] } + tmux.send_keys 2 + tmux.until { |lines| assert_equal '>', lines[-1] } + tmux.send_keys 2 + tmux.prepare + end + + def test_margin + tmux.send_keys "yes | head -1000 | #{fzf('--margin 5,3')}", :Enter + tmux.until do |lines| + assert_equal '', lines[4] + assert_equal ' y', lines[5] + end + tmux.send_keys :Enter + end + + def test_margin_reverse + tmux.send_keys "seq 1000 | #{fzf('--margin 7,5 --reverse')}", :Enter + tmux.until { |lines| assert_equal ' 1000/1000', lines[1 + 7] } + tmux.send_keys :Enter + end + + def test_margin_reverse_list + tmux.send_keys "yes | head -1000 | #{fzf('--margin 5,3 --layout=reverse-list')}", :Enter + tmux.until do |lines| + assert_equal '', lines[4] + assert_equal ' > y', lines[5] + end + tmux.send_keys :Enter + end + + def test_tabstop + writelines(tempname, %W[f\too\tba\tr\tbaz\tbarfooq\tux]) + { + 1 => '> f oo ba r baz barfooq ux', + 2 => '> f oo ba r baz barfooq ux', + 3 => '> f oo ba r baz barfooq ux', + 4 => '> f oo ba r baz barfooq ux', + 5 => '> f oo ba r baz barfooq ux', + 6 => '> f oo ba r baz barfooq ux', + 7 => '> f oo ba r baz barfooq ux', + 8 => '> f oo ba r baz barfooq ux', + 9 => '> f oo ba r baz barfooq ux' + }.each do |ts, exp| + tmux.prepare + tmux.send_keys %(cat #{tempname} | fzf --tabstop=#{ts}), :Enter + tmux.until(true) do |lines| + assert_equal exp, lines[-3] + end + tmux.send_keys :Enter + end + end + + def test_with_nth_basic + writelines(tempname, ['hello world ', 'byebye']) + assert_equal \ + 'hello world ', + `#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp + end + + def test_with_nth_ansi + writelines(tempname, ["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye']) + assert_equal \ + 'hello world ', + `#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi < #{tempname}`.chomp + end + + def test_with_nth_no_ansi + src = "\x1b[33mhello \x1b[34;1mworld\x1b[m " + writelines(tempname, [src, 'byebye']) + assert_equal \ + src, + `#{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi < #{tempname}`.chomp + end + + def test_exit_0_exit_code + `echo foo | #{FZF} -q bar -0` + assert_equal 1, $CHILD_STATUS.exitstatus + end + + def test_invalid_option + lines = `#{FZF} --foobar 2>&1` + assert_equal 2, $CHILD_STATUS.exitstatus + assert_includes lines, 'unknown option: --foobar' + end + + def test_filter_exitstatus + # filter / streaming filter + ['', '--no-sort'].each do |opts| + assert_includes `echo foo | #{FZF} -f foo #{opts}`, 'foo' + assert_equal 0, $CHILD_STATUS.exitstatus + + assert_empty `echo foo | #{FZF} -f bar #{opts}` + assert_equal 1, $CHILD_STATUS.exitstatus + end + end + + def test_exitstatus_empty + { '99' => '0', '999' => '1' }.each do |query, status| + tmux.send_keys "seq 100 | #{FZF} -q #{query}; echo --$?--", :Enter + tmux.until { |lines| assert_match %r{ [10]/100}, lines[-2] } + tmux.send_keys :Enter + tmux.until { |lines| assert_equal "--#{status}--", lines.last } + end + end + + def test_default_extended + assert_equal '100', `seq 100 | #{FZF} -f "1 00$"`.chomp + assert_equal '', `seq 100 | #{FZF} -f "1 00$" +x`.chomp + end + + def test_exact + assert_equal 4, `seq 123 | #{FZF} -f 13`.lines.length + assert_equal 2, `seq 123 | #{FZF} -f 13 -e`.lines.length + assert_equal 4, `seq 123 | #{FZF} -f 13 +e`.lines.length + end + + def test_or_operator + assert_equal %w[1 5 10], `seq 10 | #{FZF} -f "1 | 5"`.lines(chomp: true) + assert_equal %w[1 10 2 3 4 5 6 7 8 9], + `seq 10 | #{FZF} -f '1 | !1'`.lines(chomp: true) + end + + def test_hscroll_off + writelines(tempname, ['=' * 10_000 + '0123456789']) + [0, 3, 6].each do |off| + tmux.prepare + tmux.send_keys "#{FZF} --hscroll-off=#{off} -q 0 < #{tempname}", :Enter + tmux.until { |lines| assert lines[-3]&.end_with?((0..off).to_a.join + '..') } + tmux.send_keys '9' + tmux.until { |lines| assert lines[-3]&.end_with?('789') } + tmux.send_keys :Enter + end + end + + def test_partial_caching + tmux.send_keys 'seq 1000 | fzf -e', :Enter + tmux.until { |lines| assert_equal ' 1000/1000', lines[-2] } + tmux.send_keys 11 + tmux.until { |lines| assert_equal ' 19/1000', lines[-2] } + tmux.send_keys 'C-a', "'" + tmux.until { |lines| assert_equal ' 28/1000', lines[-2] } + tmux.send_keys :Enter + end + + def test_jump + tmux.send_keys "seq 1000 | #{fzf("--multi --jump-labels 12345 --bind 'ctrl-j:jump'")}", :Enter + tmux.until { |lines| assert_equal ' 1000/1000 (0)', lines[-2] } + tmux.send_keys 'C-j' + tmux.until { |lines| assert_equal '5 5', lines[-7] } + tmux.until { |lines| assert_equal ' 6', lines[-8] } + tmux.send_keys '5' + tmux.until { |lines| assert_equal '> 5', lines[-7] } + tmux.send_keys :Tab + tmux.until { |lines| assert_equal ' >5', lines[-7] } + tmux.send_keys 'C-j' + tmux.until { |lines| assert_equal '5>5', lines[-7] } + tmux.send_keys '2' + tmux.until { |lines| assert_equal '> 2', lines[-4] } + tmux.send_keys :Tab + tmux.until { |lines| assert_equal ' >2', lines[-4] } + tmux.send_keys 'C-j' + tmux.until { |lines| assert_equal '5>5', lines[-7] } + + # Press any key other than jump labels to cancel jump + tmux.send_keys '6' + tmux.until { |lines| assert_equal '> 1', lines[-3] } + tmux.send_keys :Tab + tmux.until { |lines| assert_equal '>>1', lines[-3] } + tmux.send_keys :Enter + assert_equal %w[5 2 1], readonce.lines(chomp: true) + end + + def test_jump_accept + tmux.send_keys "seq 1000 | #{fzf("--multi --jump-labels 12345 --bind 'ctrl-j:jump-accept'")}", :Enter + tmux.until { |lines| assert_equal ' 1000/1000 (0)', lines[-2] } + tmux.send_keys 'C-j' + tmux.until { |lines| assert_equal '5 5', lines[-7] } + tmux.send_keys '3' + assert_equal '3', readonce.chomp + end + + def test_pointer + tmux.send_keys "seq 10 | #{fzf("--pointer '>>'")}", :Enter + # Assert that specified pointer is displayed + tmux.until { |lines| assert_equal '>> 1', lines[-3] } + end + + def test_pointer_with_jump + tmux.send_keys "seq 10 | #{fzf("--multi --jump-labels 12345 --bind 'ctrl-j:jump' --pointer '>>'")}", :Enter + tmux.until { |lines| assert_equal ' 10/10 (0)', lines[-2] } + tmux.send_keys 'C-j' + # Correctly padded jump label should appear + tmux.until { |lines| assert_equal '5 5', lines[-7] } + tmux.until { |lines| assert_equal ' 6', lines[-8] } + tmux.send_keys '5' + # Assert that specified pointer is displayed + tmux.until { |lines| assert_equal '>> 5', lines[-7] } + end + + def test_marker + tmux.send_keys "seq 10 | #{fzf("--multi --marker '>>'")}", :Enter + tmux.until { |lines| assert_equal ' 10/10 (0)', lines[-2] } + tmux.send_keys :BTab + # Assert that specified marker is displayed + tmux.until { |lines| assert_equal ' >>1', lines[-3] } + end + + def test_preview + tmux.send_keys %(seq 1000 | sed s/^2$// | #{FZF} -m --preview 'sleep 0.2; echo {{}-{+}}' --bind ?:toggle-preview), :Enter + tmux.until { |lines| assert_includes lines[1], ' {1-1} ' } + tmux.send_keys :Up + tmux.until { |lines| assert_includes lines[1], ' {-} ' } + tmux.send_keys '555' + tmux.until { |lines| assert_includes lines[1], ' {555-555} ' } + tmux.send_keys '?' + tmux.until { |lines| refute_includes lines[1], ' {555-555} ' } + tmux.send_keys '?' + tmux.until { |lines| assert_includes lines[1], ' {555-555} ' } + tmux.send_keys :BSpace + tmux.until { |lines| assert lines[-2]&.start_with?(' 28/1000 ') } + tmux.send_keys 'foobar' + tmux.until { |lines| refute_includes lines[1], ' {55-55} ' } + tmux.send_keys 'C-u' + tmux.until { |lines| assert_equal 1000, lines.match_count } + tmux.until { |lines| assert_includes lines[1], ' {1-1} ' } + tmux.send_keys :BTab + tmux.until { |lines| assert_includes lines[1], ' {-1} ' } + tmux.send_keys :BTab + tmux.until { |lines| assert_includes lines[1], ' {3-1 } ' } + tmux.send_keys :BTab + tmux.until { |lines| assert_includes lines[1], ' {4-1 3} ' } + tmux.send_keys :BTab + tmux.until { |lines| assert_includes lines[1], ' {5-1 3 4} ' } + end + + def test_preview_hidden + tmux.send_keys %(seq 1000 | #{FZF} --preview 'echo {{}-{}-$FZF_PREVIEW_LINES-$FZF_PREVIEW_COLUMNS}' --preview-window down:1:hidden --bind ?:toggle-preview), :Enter + tmux.until { |lines| assert_equal '>', lines[-1] } + tmux.send_keys '?' + tmux.until { |lines| assert_match(/ {1-1-1-[0-9]+}/, lines[-2]) } + tmux.send_keys '555' + tmux.until { |lines| assert_match(/ {555-555-1-[0-9]+}/, lines[-2]) } + tmux.send_keys '?' + tmux.until { |lines| assert_equal '> 555', lines[-1] } + end + + def test_preview_size_0 + begin + File.unlink(tempname) + rescue StandardError + nil + end + tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0), :Enter + tmux.until do |lines| + assert_equal 100, lines.item_count + assert_equal ' 100/100', lines[1] + assert_equal '> 1', lines[2] + end + wait do + assert_path_exists tempname + assert_equal %w[1], File.readlines(tempname, chomp: true) + end + tmux.send_keys :Down + tmux.until { |lines| assert_equal '> 2', lines[3] } + wait do + assert_path_exists tempname + assert_equal %w[1 2], File.readlines(tempname, chomp: true) + end + tmux.send_keys :Down + tmux.until { |lines| assert_equal '> 3', lines[4] } + wait do + assert_path_exists tempname + assert_equal %w[1 2 3], File.readlines(tempname, chomp: true) + end + end + + def test_preview_flags + tmux.send_keys %(seq 10 | sed 's/^/:: /; s/$/ /' | + #{FZF} --multi --preview 'echo {{2}/{s2}/{+2}/{+s2}/{q}/{n}/{+n}}'), :Enter + tmux.until { |lines| assert_includes lines[1], ' {1/1 /1/1 //0/0} ' } + tmux.send_keys '123' + tmux.until { |lines| assert_includes lines[1], ' {////123//} ' } + tmux.send_keys 'C-u', '1' + tmux.until { |lines| assert_equal 2, lines.match_count } + tmux.until { |lines| assert_includes lines[1], ' {1/1 /1/1 /1/0/0} ' } + tmux.send_keys :BTab + tmux.until { |lines| assert_includes lines[1], ' {10/10 /1/1 /1/9/0} ' } + tmux.send_keys :BTab + tmux.until { |lines| assert_includes lines[1], ' {10/10 /1 10/1 10 /1/9/0 9} ' } + tmux.send_keys '2' + tmux.until { |lines| assert_includes lines[1], ' {//1 10/1 10 /12//0 9} ' } + tmux.send_keys '3' + tmux.until { |lines| assert_includes lines[1], ' {//1 10/1 10 /123//0 9} ' } + end + + def test_preview_file + tmux.send_keys %[(echo foo bar; echo bar foo) | #{FZF} --multi --preview 'cat {+f} {+f2} {+nf} {+fn}' --print0], :Enter + tmux.until { |lines| assert_includes lines[1], ' foo barbar00 ' } + tmux.send_keys :BTab + tmux.until { |lines| assert_includes lines[1], ' foo barbar00 ' } + tmux.send_keys :BTab + tmux.until { |lines| assert_includes lines[1], ' foo barbar foobarfoo0101 ' } + end + + def test_preview_q_no_match + tmux.send_keys %(: | #{FZF} --preview 'echo foo {q}'), :Enter + tmux.until { |lines| assert_equal 0, lines.match_count } + tmux.until { |lines| refute_includes lines[1], ' foo ' } + tmux.send_keys 'bar' + tmux.until { |lines| assert_includes lines[1], ' foo bar ' } + tmux.send_keys 'C-u' + tmux.until { |lines| refute_includes lines[1], ' foo ' } + end + + def test_preview_q_no_match_with_initial_query + tmux.send_keys %(: | #{FZF} --preview 'echo foo {q}{q}' --query foo), :Enter + tmux.until { |lines| assert_equal 0, lines.match_count } + tmux.until { |lines| assert_includes lines[1], ' foofoo ' } + end + + def test_no_clear + tmux.send_keys "seq 10 | fzf --no-clear --inline-info --height 5 > #{tempname}", :Enter + prompt = '> < 10/10' + tmux.until { |lines| assert_equal prompt, lines[-1] } + tmux.send_keys :Enter + wait do + assert_path_exists tempname + assert_equal %w[1], File.readlines(tempname, chomp: true) + end + tmux.until { |lines| assert_equal prompt, lines[-1] } + end + + def test_info_hidden + tmux.send_keys 'seq 10 | fzf --info=hidden', :Enter + tmux.until { |lines| assert_equal '> 1', lines[-2] } + end + + def test_change_first_last + tmux.send_keys %(seq 1000 | #{FZF} --bind change:first,alt-Z:last), :Enter + tmux.until { |lines| assert_equal 1000, lines.match_count } + tmux.send_keys :Up + tmux.until { |lines| assert_equal '> 2', lines[-4] } + tmux.send_keys 1 + tmux.until { |lines| assert_equal '> 1', lines[-3] } + tmux.send_keys :Up + tmux.until { |lines| assert_equal '> 10', lines[-4] } + tmux.send_keys 1 + tmux.until { |lines| assert_equal '> 11', lines[-3] } + tmux.send_keys 'C-u' + tmux.until { |lines| assert_equal '> 1', lines[-3] } + tmux.send_keys :Escape, 'Z' + tmux.until { |lines| assert_equal '> 1000', lines[0] } + tmux.send_keys :Enter + end + + def test_accept_non_empty + tmux.send_keys %(seq 1000 | #{fzf('--print-query --bind enter:accept-non-empty')}), :Enter + tmux.until { |lines| assert_equal 1000, lines.match_count } + tmux.send_keys 'foo' + tmux.until { |lines| assert_equal ' 0/1000', lines[-2] } + # fzf doesn't exit since there's no selection + tmux.send_keys :Enter + tmux.until { |lines| assert_equal ' 0/1000', lines[-2] } + tmux.send_keys 'C-u' + tmux.until { |lines| assert_equal ' 1000/1000', lines[-2] } + tmux.send_keys '999' + tmux.until { |lines| assert_equal ' 1/1000', lines[-2] } + tmux.send_keys :Enter + assert_equal %w[999 999], readonce.lines(chomp: true) + end + + def test_accept_non_empty_with_multi_selection + tmux.send_keys %(seq 1000 | #{fzf('-m --print-query --bind enter:accept-non-empty')}), :Enter + tmux.until { |lines| assert_equal 1000, lines.match_count } + tmux.send_keys :Tab + tmux.until { |lines| assert_equal ' 1000/1000 (1)', lines[-2] } + tmux.send_keys 'foo' + tmux.until { |lines| assert_equal ' 0/1000 (1)', lines[-2] } + # fzf will exit in this case even though there's no match for the current query + tmux.send_keys :Enter + assert_equal %w[foo 1], readonce.lines(chomp: true) + end + + def test_accept_non_empty_with_empty_list + tmux.send_keys %(: | #{fzf('-q foo --print-query --bind enter:accept-non-empty')}), :Enter + tmux.until { |lines| assert_equal ' 0/0', lines[-2] } + tmux.send_keys :Enter + # fzf will exit anyway since input list is empty + assert_equal %w[foo], readonce.lines(chomp: true) + end + + def test_preview_update_on_select + tmux.send_keys %(seq 10 | fzf -m --preview 'echo {+}' --bind a:toggle-all), + :Enter + tmux.until { |lines| assert_equal 10, lines.item_count } + tmux.send_keys 'a' + tmux.until { |lines| assert(lines.any? { |line| line.include?(' 1 2 3 4 5 ') }) } + tmux.send_keys 'a' + tmux.until { |lines| lines.each { |line| refute_includes line, ' 1 2 3 4 5 ' } } + end + + def test_escaped_meta_characters + input = [ + 'foo^bar', + 'foo$bar', + 'foo!bar', + "foo'bar", + 'foo bar', + 'bar foo' + ] + writelines(tempname, input) + + assert_equal input.length, `#{FZF} -f'foo bar' < #{tempname}`.lines.length + assert_equal input.length - 1, `#{FZF} -f'^foo bar$' < #{tempname}`.lines.length + assert_equal ['foo bar'], `#{FZF} -f'foo\\ bar' < #{tempname}`.lines(chomp: true) + assert_equal ['foo bar'], `#{FZF} -f'^foo\\ bar$' < #{tempname}`.lines(chomp: true) + assert_equal input.length - 1, `#{FZF} -f'!^foo\\ bar$' < #{tempname}`.lines.length + end + + def test_inverse_only_search_should_not_sort_the_result + # Filter + assert_equal %w[aaaaa b ccc], + `printf '%s\n' aaaaa b ccc BAD | #{FZF} -f '!bad'`.lines(chomp: true) + + # Interactive + tmux.send_keys %(printf '%s\n' aaaaa b ccc BAD | #{FZF} -q '!bad'), :Enter + tmux.until do |lines| + assert_equal 4, lines.item_count + assert_equal 3, lines.match_count + end + tmux.until { |lines| assert_equal '> aaaaa', lines[-3] } + tmux.until { |lines| assert_equal ' b', lines[-4] } + tmux.until { |lines| assert_equal ' ccc', lines[-5] } + end + + def test_preview_correct_tab_width_after_ansi_reset_code + writelines(tempname, ["\x1b[31m+\x1b[m\t\x1b[32mgreen"]) + tmux.send_keys "#{FZF} --preview 'cat #{tempname}'", :Enter + tmux.until { |lines| assert_includes lines[1], ' + green ' } + end + + def test_disabled + tmux.send_keys %(seq 1000 | #{FZF} --query 333 --disabled --bind a:enable-search,b:disable-search,c:toggle-search --preview 'echo {} {q}'), :Enter + tmux.until { |lines| assert_equal 1000, lines.match_count } + tmux.until { |lines| assert_includes lines[1], ' 1 333 ' } + tmux.send_keys 'foo' + tmux.until { |lines| assert_equal 1000, lines.match_count } + tmux.until { |lines| assert_includes lines[1], ' 1 333foo ' } + + # Already disabled, no change + tmux.send_keys 'b' + tmux.until { |lines| assert_equal 1000, lines.match_count } + + # Enable search + tmux.send_keys 'a' + tmux.until { |lines| assert_equal 0, lines.match_count } + tmux.send_keys :BSpace, :BSpace, :BSpace + tmux.until { |lines| assert_equal 1, lines.match_count } + tmux.until { |lines| assert_includes lines[1], ' 333 333 ' } + + # Toggle search -> disabled again, but retains the previous result + tmux.send_keys 'c' + tmux.send_keys 'foo' + tmux.until { |lines| assert_includes lines[1], ' 333 333foo ' } + tmux.until { |lines| assert_equal 1, lines.match_count } + + # Enabled, no match + tmux.send_keys 'c' + tmux.until { |lines| assert_equal 0, lines.match_count } + tmux.until { |lines| assert_includes lines[1], ' 333foo ' } + end + + def test_reload + tmux.send_keys %(seq 1000 | #{FZF} --bind 'change:reload(seq {q}),a:reload(seq 100),b:reload:seq 200' --header-lines 2 --multi 2), :Enter + tmux.until { |lines| assert_equal 998, lines.match_count } + tmux.send_keys 'a' + tmux.until do |lines| + assert_equal 98, lines.item_count + assert_equal 98, lines.match_count + end + tmux.send_keys 'b' + tmux.until do |lines| + assert_equal 198, lines.item_count + assert_equal 198, lines.match_count + end + tmux.send_keys :Tab + tmux.until { |lines| assert_equal ' 198/198 (1/2)', lines[-2] } + tmux.send_keys '555' + tmux.until { |lines| assert_equal ' 1/553 (0/2)', lines[-2] } + end + + def test_reload_even_when_theres_no_match + tmux.send_keys %(: | #{FZF} --bind 'space:reload(seq 10)'), :Enter + tmux.until { |lines| assert_equal 0, lines.item_count } + tmux.send_keys :Space + tmux.until { |lines| assert_equal 10, lines.item_count } + end + + def test_clear_list_when_header_lines_changed_due_to_reload + tmux.send_keys %(seq 10 | #{FZF} --header 0 --header-lines 3 --bind 'space:reload(seq 1)'), :Enter + tmux.until { |lines| assert_includes lines, ' 9' } + tmux.send_keys :Space + tmux.until { |lines| refute_includes lines, ' 9' } + end + + def test_clear_query + tmux.send_keys %(: | #{FZF} --query foo --bind space:clear-query), :Enter + tmux.until { |lines| assert_equal 0, lines.item_count } + tmux.until { |lines| assert_equal '> foo', lines.last } + tmux.send_keys 'C-a', 'bar' + tmux.until { |lines| assert_equal '> barfoo', lines.last } + tmux.send_keys :Space + tmux.until { |lines| assert_equal '>', lines.last } + end + + def test_clear_selection + tmux.send_keys %(seq 100 | #{FZF} --multi --bind space:clear-selection), :Enter + tmux.until { |lines| assert_equal 100, lines.match_count } + tmux.send_keys :Tab + tmux.until { |lines| assert_equal ' 100/100 (1)', lines[-2] } + tmux.send_keys 'foo' + tmux.until { |lines| assert_equal ' 0/100 (1)', lines[-2] } + tmux.send_keys :Space + tmux.until { |lines| assert_equal ' 0/100 (0)', lines[-2] } + end + + def test_backward_delete_char_eof + tmux.send_keys "seq 1000 | #{fzf("--bind 'bs:backward-delete-char/eof'")}", :Enter + tmux.until { |lines| assert_equal ' 1000/1000', lines[-2] } + tmux.send_keys '11' + tmux.until { |lines| assert_equal '> 11', lines[-1] } + tmux.send_keys :BSpace + tmux.until { |lines| assert_equal '> 1', lines[-1] } + tmux.send_keys :BSpace + tmux.until { |lines| assert_equal '>', lines[-1] } + tmux.send_keys :BSpace + tmux.prepare + end + + def test_strip_xterm_osc_sequence + %W[\x07 \x1b\\].each do |esc| + writelines(tempname, [%(printf $1"\e]4;3;rgb:aa/bb/cc#{esc} "$2)]) + File.chmod(0o755, tempname) + tmux.prepare + tmux.send_keys \ + %(echo foo bar | #{FZF} --preview '#{tempname} {2} {1}'), :Enter + + tmux.until { |lines| assert lines.any_include?('bar foo') } + tmux.send_keys :Enter + end + end + + def test_keep_right + tmux.send_keys "seq 10000 | #{FZF} --read0 --keep-right", :Enter + tmux.until { |lines| assert lines.any_include?('9999 10000') } + end + + def test_backward_eof + tmux.send_keys "echo foo | #{FZF} --bind 'backward-eof:reload(seq 100)'", :Enter + tmux.until { |lines| lines.item_count == 1 && lines.match_count == 1 } + tmux.send_keys 'x' + tmux.until { |lines| lines.item_count == 1 && lines.match_count == 0 } + tmux.send_keys :BSpace + tmux.until { |lines| lines.item_count == 1 && lines.match_count == 1 } + tmux.send_keys :BSpace + tmux.until { |lines| lines.item_count == 100 && lines.match_count == 100 } + end + + def test_preview_bindings_with_default_preview + tmux.send_keys "seq 10 | #{FZF} --preview 'echo [{}]' --bind 'a:preview(echo [{}{}]),b:preview(echo [{}{}{}]),c:refresh-preview'", :Enter + tmux.until { |lines| lines.item_count == 10 } + tmux.until { |lines| assert_includes lines[1], '[1]' } + tmux.send_keys 'a' + tmux.until { |lines| assert_includes lines[1], '[11]' } + tmux.send_keys 'c' + tmux.until { |lines| assert_includes lines[1], '[1]' } + tmux.send_keys 'b' + tmux.until { |lines| assert_includes lines[1], '[111]' } + tmux.send_keys :Up + tmux.until { |lines| assert_includes lines[1], '[2]' } + end + + def test_preview_bindings_without_default_preview + tmux.send_keys "seq 10 | #{FZF} --bind 'a:preview(echo [{}{}]),b:preview(echo [{}{}{}]),c:refresh-preview'", :Enter + tmux.until { |lines| lines.item_count == 10 } + tmux.until { |lines| refute_includes lines[1], '1' } + tmux.send_keys 'a' + tmux.until { |lines| assert_includes lines[1], '[11]' } + tmux.send_keys 'c' # does nothing + tmux.until { |lines| assert_includes lines[1], '[11]' } + tmux.send_keys 'b' + tmux.until { |lines| assert_includes lines[1], '[111]' } + tmux.send_keys 9 + tmux.until { |lines| lines.match_count == 1 } + tmux.until { |lines| refute_includes lines[1], '2' } + tmux.until { |lines| assert_includes lines[1], '[111]' } + end + + def test_preview_scroll_begin_constant + tmux.send_keys "echo foo 123 321 | #{FZF} --preview 'seq 1000' --preview-window left:+123", :Enter + tmux.until { |lines| assert_match %r{1/1}, lines[-2] } + tmux.until { |lines| assert_match %r{123.*123/1000}, lines[1] } + end + + def test_preview_scroll_begin_expr + tmux.send_keys "echo foo 123 321 | #{FZF} --preview 'seq 1000' --preview-window left:+{3}", :Enter + tmux.until { |lines| assert_match %r{1/1}, lines[-2] } + tmux.until { |lines| assert_match %r{321.*321/1000}, lines[1] } + end + + def test_preview_scroll_begin_and_offset + ['echo foo 123 321', 'echo foo :123: 321'].each do |input| + tmux.send_keys "#{input} | #{FZF} --preview 'seq 1000' --preview-window left:+{2}-2", :Enter + tmux.until { |lines| assert_match %r{1/1}, lines[-2] } + tmux.until { |lines| assert_match %r{121.*121/1000}, lines[1] } + tmux.send_keys 'C-c' + end + end + + def test_normalized_match + echoes = '(echo a; echo á; echo A; echo Á;)' + assert_equal %w[a á A Á], `#{echoes} | #{FZF} -f a`.lines.map(&:chomp) + assert_equal %w[á Á], `#{echoes} | #{FZF} -f á`.lines.map(&:chomp) + assert_equal %w[A Á], `#{echoes} | #{FZF} -f A`.lines.map(&:chomp) + assert_equal %w[Á], `#{echoes} | #{FZF} -f Á`.lines.map(&:chomp) + end + + def test_preview_clear_screen + tmux.send_keys %{seq 100 | #{FZF} --preview 'for i in $(seq 300); do (( i % 200 == 0 )) && printf "\\033[2J"; echo "[$i]"; sleep 0.001; done'}, :Enter + tmux.until { |lines| lines.item_count == 100 } + tmux.until { |lines| lines[1]&.include?('[200]') } + end + + def test_change_prompt + tmux.send_keys "#{FZF} --bind 'a:change-prompt(a> ),b:change-prompt:b> ' --query foo", :Enter + tmux.until { |lines| assert_equal '> foo', lines[-1] } + tmux.send_keys 'a' + tmux.until { |lines| assert_equal 'a> foo', lines[-1] } + tmux.send_keys 'b' + tmux.until { |lines| assert_equal 'b> foo', lines[-1] } + end + + def test_preview_window_follow + tmux.send_keys "#{FZF} --preview 'seq 1000 | nl' --preview-window down:noborder:follow", :Enter + tmux.until { |lines| assert_equal '1000 1000', lines[-1].strip } + end + + def test_toggle_preview_wrap + tmux.send_keys "#{FZF} --preview 'for i in $(seq $FZF_PREVIEW_COLUMNS); do echo -n .; done; echo wrapped; echo 2nd line' --bind ctrl-w:toggle-preview-wrap", :Enter + 2.times do + tmux.until { |lines| assert_includes lines[2], '2nd line' } + tmux.send_keys 'C-w' + tmux.until do |lines| + assert_includes lines[2], 'wrapped' + assert_includes lines[3], '2nd line' + end + tmux.send_keys 'C-w' + end + end + + def test_close + tmux.send_keys "seq 100 | #{FZF} --preview 'echo foo' --bind ctrl-c:close", :Enter + tmux.until { |lines| assert_equal 100, lines.match_count } + tmux.until { |lines| assert_includes lines[1], 'foo' } + tmux.send_keys 'C-c' + tmux.until { |lines| refute_includes lines[1], 'foo' } + tmux.send_keys '10' + tmux.until { |lines| assert_equal 2, lines.match_count } + tmux.send_keys 'C-c' + tmux.send_keys 'C-l', 'closed' + tmux.until { |lines| assert_includes lines[0], 'closed' } + end + + def test_select_deselect + tmux.send_keys "seq 3 | #{FZF} --multi --bind up:deselect+up,down:select+down", :Enter + tmux.until { |lines| assert_equal 3, lines.match_count } + tmux.send_keys :Tab + tmux.until { |lines| assert_equal 1, lines.select_count } + tmux.send_keys :Up + tmux.until { |lines| assert_equal 0, lines.select_count } + tmux.send_keys :Down, :Down + tmux.until { |lines| assert_equal 2, lines.select_count } + tmux.send_keys :Tab + tmux.until { |lines| assert_equal 1, lines.select_count } + tmux.send_keys :Down, :Down + tmux.until { |lines| assert_equal 2, lines.select_count } + tmux.send_keys :Up + tmux.until { |lines| assert_equal 1, lines.select_count } + tmux.send_keys :Down + tmux.until { |lines| assert_equal 1, lines.select_count } + tmux.send_keys :Down + tmux.until { |lines| assert_equal 2, lines.select_count } + end + + def test_interrupt_execute + tmux.send_keys "seq 100 | #{FZF} --bind 'ctrl-l:execute:echo executing {}; sleep 100'", :Enter + tmux.until { |lines| assert_equal 100, lines.item_count } + tmux.send_keys 'C-l' + tmux.until { |lines| assert lines.any_include?('executing 1') } + tmux.send_keys 'C-c' + tmux.until { |lines| assert_equal 100, lines.item_count } + tmux.send_keys 99 + tmux.until { |lines| assert_equal 1, lines.match_count } + end + + def test_kill_default_command_on_abort + script = tempname + '.sh' + writelines(script, + ['#!/usr/bin/env bash', + "echo 'Started'", + 'while :; do sleep 1; done']) + system("chmod +x #{script}") + + tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{script}"), :Enter + tmux.until { |lines| assert_equal 1, lines.item_count } + tmux.send_keys 'C-c' + tmux.send_keys 'C-l', 'closed' + tmux.until { |lines| assert_includes lines[0], 'closed' } + wait { refute system("pgrep -f #{script}") } + ensure + system("pkill -9 -f #{script}") + begin + File.unlink(script) + rescue StandardError + nil + end + end + + def test_kill_default_command_on_accept + script = tempname + '.sh' + writelines(script, + ['#!/usr/bin/env bash', + "echo 'Started'", + 'while :; do sleep 1; done']) + system("chmod +x #{script}") + + tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{script}"), :Enter + tmux.until { |lines| assert_equal 1, lines.item_count } + tmux.send_keys :Enter + assert_equal 'Started', readonce.chomp + wait { refute system("pgrep -f #{script}") } + ensure + system("pkill -9 -f #{script}") + begin + File.unlink(script) + rescue StandardError + nil + end + end + + def test_kill_reload_command_on_abort + script = tempname + '.sh' + writelines(script, + ['#!/usr/bin/env bash', + "echo 'Started'", + 'while :; do sleep 1; done']) + system("chmod +x #{script}") + + tmux.send_keys "seq 1 3 | #{fzf("--bind 'ctrl-r:reload(#{script})'")}", :Enter + tmux.until { |lines| assert_equal 3, lines.item_count } + tmux.send_keys 'C-r' + tmux.until { |lines| assert_equal 1, lines.item_count } + tmux.send_keys 'C-c' + tmux.send_keys 'C-l', 'closed' + tmux.until { |lines| assert_includes lines[0], 'closed' } + wait { refute system("pgrep -f #{script}") } + ensure + system("pkill -9 -f #{script}") + begin + File.unlink(script) + rescue StandardError + nil + end + end + + def test_kill_reload_command_on_accept + script = tempname + '.sh' + writelines(script, + ['#!/usr/bin/env bash', + "echo 'Started'", + 'while :; do sleep 1; done']) + system("chmod +x #{script}") + + tmux.send_keys "seq 1 3 | #{fzf("--bind 'ctrl-r:reload(#{script})'")}", :Enter + tmux.until { |lines| assert_equal 3, lines.item_count } + tmux.send_keys 'C-r' + tmux.until { |lines| assert_equal 1, lines.item_count } + tmux.send_keys :Enter + assert_equal 'Started', readonce.chomp + wait { refute system("pgrep -f #{script}") } + ensure + system("pkill -9 -f #{script}") + begin + File.unlink(script) + rescue StandardError + nil + end + end + + def test_preview_header + tmux.send_keys "seq 100 | #{FZF} --bind ctrl-k:preview-up+preview-up,ctrl-j:preview-down+preview-down+preview-down --preview 'seq 1000' --preview-window 'top:+{1}:~3'", :Enter + tmux.until { |lines| assert_equal 100, lines.item_count } + top5 = ->(lines) { lines.drop(1).take(5).map { |s| s[/[0-9]+/] } } + tmux.until do |lines| + assert_includes lines[1], '4/1000' + assert_equal(%w[1 2 3 4 5], top5[lines]) + end + tmux.send_keys '55' + tmux.until do |lines| + assert_equal 1, lines.match_count + assert_equal(%w[1 2 3 55 56], top5[lines]) + end + tmux.send_keys 'C-J' + tmux.until do |lines| + assert_equal(%w[1 2 3 58 59], top5[lines]) + end + tmux.send_keys :BSpace + tmux.until do |lines| + assert_equal 19, lines.match_count + assert_equal(%w[1 2 3 5 6], top5[lines]) + end + tmux.send_keys 'C-K' + tmux.until { |lines| assert_equal(%w[1 2 3 4 5], top5[lines]) } + end + + def test_unbind + tmux.send_keys "seq 100 | #{FZF} --bind 'c:clear-query,d:unbind(c,d)'", :Enter + tmux.until { |lines| assert_equal 100, lines.item_count } + tmux.send_keys 'ab' + tmux.until { |lines| assert_equal '> ab', lines[-1] } + tmux.send_keys 'c' + tmux.until { |lines| assert_equal '>', lines[-1] } + tmux.send_keys 'dabcd' + tmux.until { |lines| assert_equal '> abcd', lines[-1] } + end + + def test_item_index_reset_on_reload + tmux.send_keys "seq 10 | #{FZF} --preview 'echo [[{n}]]' --bind 'up:last,down:first,space:reload:seq 100'", :Enter + tmux.until { |lines| assert_includes lines[1], '[[0]]' } + tmux.send_keys :Up + tmux.until { |lines| assert_includes lines[1], '[[9]]' } + tmux.send_keys :Down + tmux.until { |lines| assert_includes lines[1], '[[0]]' } + tmux.send_keys :Space + tmux.until do |lines| + assert_equal 100, lines.item_count + assert_includes lines[1], '[[0]]' + end + tmux.send_keys :Up + tmux.until { |lines| assert_includes lines[1], '[[99]]' } + end + + def test_reload_should_update_preview + tmux.send_keys "seq 3 | #{FZF} --bind 'ctrl-t:reload:echo 4' --preview 'echo {}' --preview-window 'nohidden'", :Enter + tmux.until { |lines| assert_includes lines[1], '1' } + tmux.send_keys 'C-t' + tmux.until { |lines| assert_includes lines[1], '4' } + end + + def test_scroll_off + tmux.send_keys "seq 1000 | #{FZF} --scroll-off=3 --bind l:last", :Enter + tmux.until { |lines| assert_equal 1000, lines.item_count } + height = tmux.until { |lines| lines }.first.to_i + tmux.send_keys :PgUp + tmux.until do |lines| + assert_equal height + 3, lines.first.to_i + assert_equal "> #{height}", lines[3].strip + end + tmux.send_keys :Up + tmux.until { |lines| assert_equal "> #{height + 1}", lines[3].strip } + tmux.send_keys 'l' + tmux.until { |lines| assert_equal '> 1000', lines.first.strip } + tmux.send_keys :PgDn + tmux.until { |lines| assert_equal "> #{1000 - height + 1}", lines.reverse[5].strip } + tmux.send_keys :Down + tmux.until { |lines| assert_equal "> #{1000 - height}", lines.reverse[5].strip } + end + + def test_scroll_off_large + tmux.send_keys "seq 1000 | #{FZF} --scroll-off=9999", :Enter + tmux.until { |lines| assert_equal 1000, lines.item_count } + height = tmux.until { |lines| lines }.first.to_i + tmux.send_keys :PgUp + tmux.until { |lines| assert_equal "> #{height}", lines[height / 2].strip } + tmux.send_keys :Up + tmux.until { |lines| assert_equal "> #{height + 1}", lines[height / 2].strip } + tmux.send_keys :Up + tmux.until { |lines| assert_equal "> #{height + 2}", lines[height / 2].strip } + tmux.send_keys :Down + tmux.until { |lines| assert_equal "> #{height + 1}", lines[height / 2].strip } + end + + def test_header_first + tmux.send_keys "seq 1000 | #{FZF} --header foobar --header-lines 3 --header-first", :Enter + tmux.until do |lines| + expected = <<~OUTPUT + > 4 + 997/997 + > + 3 + 2 + 1 + foobar + OUTPUT + + assert_equal expected.chomp, lines.reverse.take(7).reverse.join("\n") + end + end + + def test_header_first_reverse + tmux.send_keys "seq 1000 | #{FZF} --header foobar --header-lines 3 --header-first --reverse --inline-info", :Enter + tmux.until do |lines| + expected = <<~OUTPUT + foobar + 1 + 2 + 3 + > < 997/997 + > 4 + OUTPUT + + assert_equal expected.chomp, lines.take(6).join("\n") + end + end + + def test_change_preview_window + tmux.send_keys "seq 1000 | #{FZF} --preview 'echo [[{}]]' --preview-window border-none --bind '" \ + 'a:change-preview(echo __{}__),' \ + 'b:change-preview-window(down)+change-preview(echo =={}==)+change-preview-window(up),' \ + 'c:change-preview(),d:change-preview-window(hidden),' \ + "e:preview(printf ::%${FZF_PREVIEW_COLUMNS}s{})+change-preview-window(up),f:change-preview-window(up,wrap)'", :Enter + tmux.until { |lines| assert_equal 1000, lines.item_count } + tmux.until { |lines| assert_includes lines[0], '[[1]]' } + + # change-preview action permanently changes the preview command set by --preview + tmux.send_keys 'a' + tmux.until { |lines| assert_includes lines[0], '__1__' } + tmux.send_keys :Up + tmux.until { |lines| assert_includes lines[0], '__2__' } + + # When multiple change-preview-window actions are bound to a single key, + # the last one wins and the updated options are immediately applied to the new preview + tmux.send_keys 'b' + tmux.until { |lines| assert_equal '==2==', lines[0] } + tmux.send_keys :Up + tmux.until { |lines| assert_equal '==3==', lines[0] } + + # change-preview with an empty preview command closes the preview window + tmux.send_keys 'c' + tmux.until { |lines| refute_includes lines[0], '==' } + + # change-preview again to re-open the preview window + tmux.send_keys 'a' + tmux.until { |lines| assert_equal '__3__', lines[0] } + + # Hide the preview window with hidden flag + tmux.send_keys 'd' + tmux.until { |lines| refute_includes lines[0], '__3__' } + + # One-off preview + tmux.send_keys 'e' + tmux.until do |lines| + assert_equal '::', lines[0] + refute_includes lines[1], '3' + end + + # Wrapped + tmux.send_keys 'f' + tmux.until do |lines| + assert_equal '::', lines[0] + assert_equal ' 3', lines[1] + end + end + + def test_change_preview_window_rotate + tmux.send_keys "seq 100 | #{FZF} --preview-window left,border-none --preview 'echo hello' --bind '" \ + "a:change-preview-window(right|down|up|hidden|)'", :Enter + 3.times do + tmux.until { |lines| lines[0].start_with?('hello') } + tmux.send_keys 'a' + tmux.until { |lines| lines[0].end_with?('hello') } + tmux.send_keys 'a' + tmux.until { |lines| lines[-1].start_with?('hello') } + tmux.send_keys 'a' + tmux.until { |lines| assert_equal 'hello', lines[0] } + tmux.send_keys 'a' + tmux.until { |lines| refute_includes lines[0], 'hello' } + tmux.send_keys 'a' + end + end +end + +module TestShell + def setup + @tmux = Tmux.new(shell) + tmux.prepare + end + + def teardown + @tmux.kill + end + + def set_var(name, val) + tmux.prepare + tmux.send_keys "export #{name}='#{val}'", :Enter + tmux.prepare + end + + def unset_var(name) + tmux.prepare + tmux.send_keys "unset #{name}", :Enter + tmux.prepare + end + + def test_ctrl_t + set_var('FZF_CTRL_T_COMMAND', 'seq 100') + + tmux.prepare + tmux.send_keys 'C-t' + tmux.until { |lines| assert_equal 100, lines.item_count } + tmux.send_keys :Tab, :Tab, :Tab + tmux.until { |lines| assert lines.any_include?(' (3)') } + tmux.send_keys :Enter + tmux.until { |lines| assert lines.any_include?('1 2 3') } + tmux.send_keys 'C-c' + end + + def test_ctrl_t_unicode + writelines(tempname, ['fzf-unicode 테스트1', 'fzf-unicode 테스트2']) + set_var('FZF_CTRL_T_COMMAND', "cat #{tempname}") + + tmux.prepare + tmux.send_keys 'echo ', 'C-t' + tmux.until { |lines| assert_equal 2, lines.item_count } + tmux.send_keys 'fzf-unicode' + tmux.until { |lines| assert_equal 2, lines.match_count } + + tmux.send_keys '1' + tmux.until { |lines| assert_equal 1, lines.match_count } + tmux.send_keys :Tab + tmux.until { |lines| assert_equal 1, lines.select_count } + + tmux.send_keys :BSpace + tmux.until { |lines| assert_equal 2, lines.match_count } + + tmux.send_keys '2' + tmux.until { |lines| assert_equal 1, lines.match_count } + tmux.send_keys :Tab + tmux.until { |lines| assert_equal 2, lines.select_count } + + tmux.send_keys :Enter + tmux.until { |lines| assert_match(/echo .*fzf-unicode.*1.* .*fzf-unicode.*2/, lines.join) } + tmux.send_keys :Enter + tmux.until { |lines| assert_equal 'fzf-unicode 테스트1 fzf-unicode 테스트2', lines[-1] } + end + + def test_alt_c + tmux.prepare + tmux.send_keys :Escape, :c + lines = tmux.until { |lines| assert_operator lines.match_count, :>, 0 } + expected = lines.reverse.find { |l| l.start_with?('> ') }[2..-1] + tmux.send_keys :Enter + tmux.prepare + tmux.send_keys :pwd, :Enter + tmux.until { |lines| assert lines[-1]&.end_with?(expected) } + end + + def test_alt_c_command + set_var('FZF_ALT_C_COMMAND', 'echo /tmp') + + tmux.prepare + tmux.send_keys 'cd /', :Enter + + tmux.prepare + tmux.send_keys :Escape, :c + tmux.until { |lines| assert_equal 1, lines.item_count } + tmux.send_keys :Enter + + tmux.prepare + tmux.send_keys :pwd, :Enter + tmux.until { |lines| assert_equal '/tmp', lines[-1] } + end + + def test_ctrl_r + tmux.prepare + tmux.send_keys 'echo 1st', :Enter + tmux.prepare + tmux.send_keys 'echo 2nd', :Enter + tmux.prepare + tmux.send_keys 'echo 3d', :Enter + tmux.prepare + 3.times do + tmux.send_keys 'echo 3rd', :Enter + tmux.prepare + end + tmux.send_keys 'echo 4th', :Enter + tmux.prepare + tmux.send_keys 'C-r' + tmux.until { |lines| assert_operator lines.match_count, :>, 0 } + tmux.send_keys 'e3d' + # Duplicates removed: 3d (1) + 3rd (1) => 2 matches + tmux.until { |lines| assert_equal 2, lines.match_count } + tmux.until { |lines| assert lines[-3]&.end_with?(' echo 3d') } + tmux.send_keys 'C-r' + tmux.until { |lines| assert lines[-3]&.end_with?(' echo 3rd') } + tmux.send_keys :Enter + tmux.until { |lines| assert_equal 'echo 3rd', lines[-1] } + tmux.send_keys :Enter + tmux.until { |lines| assert_equal '3rd', lines[-1] } + end + + def test_ctrl_r_multiline + tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter + tmux.until { |lines| assert_equal %w[foo bar], lines[-2..-1] } + tmux.prepare + tmux.send_keys 'C-r' + tmux.until { |lines| assert_equal '>', lines[-1] } + tmux.send_keys 'foo bar' + tmux.until { |lines| assert lines[-3]&.end_with?('bar"') } + tmux.send_keys :Enter + tmux.until { |lines| assert lines[-1]&.end_with?('bar"') } + tmux.send_keys :Enter + tmux.until { |lines| assert_equal %w[foo bar], lines[-2..-1] } + end + + def test_ctrl_r_abort + skip("doesn't restore the original line when search is aborted pre Bash 4") if shell == :bash && `#{Shell.bash} --version`[/(?<= version )\d+/].to_i < 4 + %w[foo ' "].each do |query| + tmux.prepare + tmux.send_keys :Enter, query + tmux.until { |lines| assert lines[-1]&.start_with?(query) } + tmux.send_keys 'C-r' + tmux.until { |lines| assert_equal "> #{query}", lines[-1] } + tmux.send_keys 'C-g' + tmux.until { |lines| assert lines[-1]&.start_with?(query) } + end + end +end + +module CompletionTest + def test_file_completion + FileUtils.mkdir_p('/tmp/fzf-test') + FileUtils.mkdir_p('/tmp/fzf test') + (1..100).each { |i| FileUtils.touch("/tmp/fzf-test/#{i}") } + ['no~such~user', '/tmp/fzf test/foobar'].each do |f| + FileUtils.touch(File.expand_path(f)) + end + tmux.prepare + tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab + tmux.until { |lines| assert_operator lines.match_count, :>, 0 } + tmux.send_keys ' !d' + tmux.until { |lines| assert_equal 2, lines.match_count } + tmux.send_keys :Tab, :Tab + tmux.until { |lines| assert_equal 2, lines.select_count } + tmux.send_keys :Enter + tmux.until(true) do |lines| + assert_equal 'cat /tmp/fzf-test/10 /tmp/fzf-test/100', lines[-1] + end + + # ~USERNAME** + user = `whoami`.chomp + tmux.send_keys 'C-u' + tmux.send_keys "cat ~#{user}**", :Tab + tmux.until { |lines| assert_operator lines.match_count, :>, 0 } + tmux.send_keys "/#{user}" + tmux.until { |lines| assert(lines.any? { |l| l.end_with?("/#{user}") }) } + tmux.send_keys :Enter + tmux.until(true) do |lines| + assert_match %r{cat .*/#{user}}, lines[-1] + end + + # ~INVALID_USERNAME** + tmux.send_keys 'C-u' + tmux.send_keys 'cat ~such**', :Tab + tmux.until(true) { |lines| assert lines.any_include?('no~such~user') } + tmux.send_keys :Enter + tmux.until(true) { |lines| assert_equal 'cat no~such~user', lines[-1] } + + # /tmp/fzf\ test** + tmux.send_keys 'C-u' + tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab + tmux.until { |lines| assert_operator lines.match_count, :>, 0 } + tmux.send_keys 'foobar$' + tmux.until { |lines| assert_equal 1, lines.match_count } + tmux.send_keys :Enter + tmux.until(true) { |lines| assert_equal 'cat /tmp/fzf\ test/foobar', lines[-1] } + + # Should include hidden files + (1..100).each { |i| FileUtils.touch("/tmp/fzf-test/.hidden-#{i}") } + tmux.send_keys 'C-u' + tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab + tmux.until(true) do |lines| + assert_equal 100, lines.match_count + assert lines.any_include?('/tmp/fzf-test/.hidden-') + end + tmux.send_keys :Enter + ensure + ['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f| + FileUtils.rm_rf(File.expand_path(f)) + end + end + + def test_file_completion_root + tmux.send_keys 'ls /**', :Tab + tmux.until { |lines| assert_operator lines.match_count, :>, 0 } + tmux.send_keys :Enter + end + + def test_dir_completion + (1..100).each do |idx| + FileUtils.mkdir_p("/tmp/fzf-test/d#{idx}") + end + FileUtils.touch('/tmp/fzf-test/d55/xxx') + tmux.prepare + tmux.send_keys 'cd /tmp/fzf-test/**', :Tab + tmux.until { |lines| assert_operator lines.match_count, :>, 0 } + tmux.send_keys :Tab, :Tab # Tab does not work here + tmux.send_keys 55 + tmux.until { |lines| assert_equal 1, lines.match_count } + tmux.send_keys :Enter + tmux.until(true) { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] } + tmux.send_keys :xx + tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/xx', lines[-1] } + + # Should not match regular files (bash-only) + if instance_of?(TestBash) + tmux.send_keys :Tab + tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/xx', lines[-1] } + end + + # Fail back to plusdirs + tmux.send_keys :BSpace, :BSpace, :BSpace + tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55', lines[-1] } + tmux.send_keys :Tab + tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] } + end + + def test_process_completion + tmux.send_keys 'sleep 12345 &', :Enter + lines = tmux.until { |lines| assert lines[-1]&.start_with?('[1] ') } + pid = lines[-1]&.split&.last + tmux.prepare + tmux.send_keys 'C-L' + tmux.send_keys 'kill ', :Tab + tmux.until { |lines| assert_operator lines.match_count, :>, 0 } + tmux.send_keys 'sleep12345' + tmux.until { |lines| assert lines.any_include?('sleep 12345') } + tmux.send_keys :Enter + tmux.until(true) { |lines| assert_equal "kill #{pid}", lines[-1] } + ensure + if pid + begin + Process.kill('KILL', pid.to_i) + rescue StandardError + nil + end + end + end + + def test_custom_completion + tmux.send_keys '_fzf_compgen_path() { echo "$1"; seq 10; }', :Enter + tmux.prepare + tmux.send_keys 'ls /tmp/**', :Tab + tmux.until { |lines| assert_equal 11, lines.match_count } + tmux.send_keys :Tab, :Tab, :Tab + tmux.until { |lines| assert_equal 3, lines.select_count } + tmux.send_keys :Enter + tmux.until(true) { |lines| assert_equal 'ls /tmp 1 2', lines[-1] } + end + + def test_unset_completion + tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter + tmux.prepare + + # Using tmux + tmux.send_keys 'unset FZFFOOBR**', :Tab + tmux.until { |lines| assert_equal 1, lines.match_count } + tmux.send_keys :Enter + tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] } + tmux.send_keys 'C-c' + + # FZF_TMUX=1 + new_shell + tmux.focus + tmux.send_keys 'unset FZFFOOBR**', :Tab + tmux.until { |lines| assert_equal 1, lines.match_count } + tmux.send_keys :Enter + tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] } + end + + def test_file_completion_unicode + FileUtils.mkdir_p('/tmp/fzf-test') + tmux.paste "cd /tmp/fzf-test; echo test3 > $'fzf-unicode \\355\\205\\214\\354\\212\\244\\355\\212\\2701'; echo test4 > $'fzf-unicode \\355\\205\\214\\354\\212\\244\\355\\212\\2702'" + tmux.prepare + tmux.send_keys 'cat fzf-unicode**', :Tab + tmux.until { |lines| assert_equal 2, lines.match_count } + + tmux.send_keys '1' + tmux.until { |lines| assert_equal 1, lines.match_count } + tmux.send_keys :Tab + tmux.until { |lines| assert_equal 1, lines.select_count } + + tmux.send_keys :BSpace + tmux.until { |lines| assert_equal 2, lines.match_count } + + tmux.send_keys '2' + tmux.until { |lines| assert_equal 1, lines.select_count } + tmux.send_keys :Tab + tmux.until { |lines| assert_equal 2, lines.select_count } + + tmux.send_keys :Enter + tmux.until(true) { |lines| assert_match(/cat .*fzf-unicode.*1.* .*fzf-unicode.*2/, lines[-1]) } + tmux.send_keys :Enter + tmux.until { |lines| assert_equal %w[test3 test4], lines[-2..-1] } + end + + def test_custom_completion_api + tmux.send_keys 'eval "_fzf$(declare -f _comprun)"', :Enter + %w[f g].each do |command| + tmux.prepare + tmux.send_keys "#{command} b**", :Tab + tmux.until do |lines| + assert_equal 2, lines.item_count + assert_equal 1, lines.match_count + assert lines.any_include?("prompt-#{command}") + assert lines.any_include?("preview-#{command}-bar") + end + tmux.send_keys :Enter + tmux.until { |lines| assert_equal "#{command} #{command}barbar", lines[-1] } + tmux.send_keys 'C-u' + end + ensure + tmux.prepare + tmux.send_keys 'unset -f _fzf_comprun', :Enter + end +end + +class TestBash < TestBase + include TestShell + include CompletionTest + + def shell + :bash + end + + def new_shell + tmux.prepare + tmux.send_keys "FZF_TMUX=1 #{Shell.bash}", :Enter + tmux.prepare + end + + def test_dynamic_completion_loader + tmux.paste 'touch /tmp/foo; _fzf_completion_loader=1' + tmux.paste '_completion_loader() { complete -o default fake; }' + tmux.paste 'complete -F _fzf_path_completion -o default -o bashdefault fake' + tmux.send_keys 'fake /tmp/foo**', :Tab + tmux.until { |lines| assert_operator lines.match_count, :>, 0 } + tmux.send_keys 'C-c' + + tmux.prepare + tmux.send_keys 'fake /tmp/foo' + tmux.send_keys :Tab, 'C-u' + + tmux.prepare + tmux.send_keys 'fake /tmp/foo**', :Tab + tmux.until { |lines| assert_operator lines.match_count, :>, 0 } + end +end + +class TestZsh < TestBase + include TestShell + include CompletionTest + + def shell + :zsh + end + + def new_shell + tmux.send_keys "FZF_TMUX=1 #{Shell.zsh}", :Enter + tmux.prepare + end + + def test_complete_quoted_command + tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter + ['unset', '\unset', "'unset'"].each do |command| + tmux.prepare + tmux.send_keys "#{command} FZFFOOBR**", :Tab + tmux.until { |lines| assert_equal 1, lines.match_count } + tmux.send_keys :Enter + tmux.until { |lines| assert_equal "#{command} FZFFOOBAR", lines[-1] } + tmux.send_keys 'C-c' + end + end +end + +class TestFish < TestBase + include TestShell + + def shell + :fish + end + + def new_shell + tmux.send_keys 'env FZF_TMUX=1 fish', :Enter + tmux.send_keys 'function fish_prompt; end; clear', :Enter + tmux.until { |lines| assert_empty lines } + end + + def set_var(name, val) + tmux.prepare + tmux.send_keys "set -g #{name} '#{val}'", :Enter + tmux.prepare + end +end + +__END__ +PS1= PROMPT_COMMAND= HISTFILE= HISTSIZE=100 +unset <%= UNSETS.join(' ') %> +unset $(env | sed -n /^_fzf_orig/s/=.*//p) +unset $(declare -F | sed -n "/_fzf/s/.*-f //p") + +# Setup fzf +# --------- +if [[ ! "$PATH" == *<%= BASE %>/bin* ]]; then + export PATH="${PATH:+${PATH}:}<%= BASE %>/bin" +fi + +# Auto-completion +# --------------- +[[ $- == *i* ]] && source "<%= BASE %>/shell/completion.<%= __method__ %>" 2> /dev/null + +# Key bindings +# ------------ +source "<%= BASE %>/shell/key-bindings.<%= __method__ %>" + +# Old API +_fzf_complete_f() { + _fzf_complete "+m --multi --prompt \"prompt-f> \"" "$@" < <( + echo foo + echo bar + ) +} + +# New API +_fzf_complete_g() { + _fzf_complete +m --multi --prompt "prompt-g> " -- "$@" < <( + echo foo + echo bar + ) +} + +_fzf_complete_f_post() { + awk '{print "f" $0 $0}' +} + +_fzf_complete_g_post() { + awk '{print "g" $0 $0}' +} + +[ -n "$BASH" ] && complete -F _fzf_complete_f -o default -o bashdefault f +[ -n "$BASH" ] && complete -F _fzf_complete_g -o default -o bashdefault g + +_comprun() { + local command=$1 + shift + + case "$command" in + f) fzf "$@" --preview 'echo preview-f-{}' ;; + g) fzf "$@" --preview 'echo preview-g-{}' ;; + *) fzf "$@" ;; + esac +} diff --git a/fzf/fzf/uninstall b/fzf/fzf/uninstall new file mode 100755 index 0000000..094d689 --- /dev/null +++ b/fzf/fzf/uninstall @@ -0,0 +1,117 @@ +#!/usr/bin/env bash + +xdg=0 +prefix='~/.fzf' +prefix_expand=~/.fzf +fish_dir=${XDG_CONFIG_HOME:-$HOME/.config}/fish + +help() { + cat << EOF +usage: $0 [OPTIONS] + + --help Show this message + --xdg Remove files generated under \$XDG_CONFIG_HOME/fzf +EOF +} + +for opt in "$@"; do + case $opt in + --help) + help + exit 0 + ;; + --xdg) + xdg=1 + prefix='"${XDG_CONFIG_HOME:-$HOME/.config}"/fzf/fzf' + prefix_expand=${XDG_CONFIG_HOME:-$HOME/.config}/fzf/fzf + ;; + *) + echo "unknown option: $opt" + help + exit 1 + ;; + esac +done + +ask() { + while true; do + read -p "$1 ([y]/n) " -r + REPLY=${REPLY:-"y"} + if [[ $REPLY =~ ^[Yy]$ ]]; then + return 0 + elif [[ $REPLY =~ ^[Nn]$ ]]; then + return 1 + fi + done +} + +remove() { + echo "Remove $1" + rm -f "$1" +} + +remove_line() { + src=$(readlink "$1") + if [ $? -eq 0 ]; then + echo "Remove from $1 ($src):" + else + src=$1 + echo "Remove from $1:" + fi + + shift + line_no=1 + match=0 + while [ -n "$1" ]; do + line=$(sed -n "$line_no,\$p" "$src" | \grep -m1 -nF "$1") + if [ $? -ne 0 ]; then + shift + line_no=1 + continue + fi + line_no=$(( $(sed 's/:.*//' <<< "$line") + line_no - 1 )) + content=$(sed 's/^[0-9]*://' <<< "$line") + match=1 + echo " - Line #$line_no: $content" + [ "$content" = "$1" ] || ask " - Remove?" + if [ $? -eq 0 ]; then + awk -v n=$line_no 'NR == n {next} {print}' "$src" > "$src.bak" && + mv "$src.bak" "$src" || break + echo " - Removed" + else + echo " - Skipped" + line_no=$(( line_no + 1 )) + fi + done + [ $match -eq 0 ] && echo " - Nothing found" + echo +} + +for shell in bash zsh; do + shell_config=${prefix_expand}.${shell} + remove "${shell_config}" + remove_line ~/.${shell}rc \ + "[ -f ${prefix}.${shell} ] && source ${prefix}.${shell}" \ + "source ${prefix}.${shell}" +done + +bind_file="${fish_dir}/functions/fish_user_key_bindings.fish" +if [ -f "$bind_file" ]; then + remove_line "$bind_file" "fzf_key_bindings" +fi + +if [ -d "${fish_dir}/functions" ]; then + remove "${fish_dir}/functions/fzf.fish" + remove "${fish_dir}/functions/fzf_key_bindings.fish" + + if [ -z "$(ls -A "${fish_dir}/functions")" ]; then + rmdir "${fish_dir}/functions" + else + echo "Can't delete non-empty directory: \"${fish_dir}/functions\"" + fi +fi + +config_dir=$(dirname "$prefix_expand") +if [[ "$xdg" = 1 ]] && [[ "$config_dir" = */fzf ]] && [[ -d "$config_dir" ]]; then + rmdir "$config_dir" +fi diff --git a/tmux/.tmux.conf b/tmux/.tmux.conf new file mode 100644 index 0000000..7c22e21 --- /dev/null +++ b/tmux/.tmux.conf @@ -0,0 +1,104 @@ +unbind C-b +set -g prefix C-a +bind C-a send-prefix + +set-option -g history-limit 100000000 +set-option -g status-position bottom + +set-option -g bell-action other +set-option -g visual-bell on + +set -g base-index 1 +set -g pane-base-index 1 +set-option -g base-index 1 +set-window-option -g pane-base-index 1 + +set -g default-terminal "screen-256color" +set -ga terminal-overrides ",xterm-256color*:Tc" # tell Tmux that outside terminal supports true color +set -g default-shell zsh + +# force a reload of the config file +unbind r +bind r source-file ~/.tmux.conf \; display "Reloaded!" + +bind -n C-b send-keys -R \; clear-history \; send-keys C-l + +set -g status on +set-option -g status-interval 2 +bind Escape confirm-before "kill-server" + +# Automatically set window title +set-window-option -g automatic-rename off +set-window-option -g allow-rename on +#set-option -g set-titles on + +set-window-option -g xterm-keys on +set-option -g xterm-keys on +set -g status-keys vi + +# Use vim keybindings in copy mode +setw -g mode-keys vi + +# Setup 'v' to begin selection as in Vim +bind-key -Tcopy-mode-vi 'v' send -X begin-selection +bind-key -Tcopy-mode-vi 'y' send -X copy-pipe-and-cancel "xclip -sel clip -i" + +# Bind ']' to use pbpaste +#bind ] run "pbpaste | tmux load-buffer - && tmux paste-buffer" +setw -g monitor-activity on + +bind-key v split-window -h -c '#{pane_current_path}' +bind-key s split-window -v -c '#{pane_current_path}' + +bind -n M-s set-window-option synchronize-panes\; display-message "synchronize-panes is now #{?pane_synchronized,on,off}" + +bind -n M-z resize-pane -Z + +bind -n M-j resize-pane -D 5 +bind -n M-k resize-pane -U 5 +bind -n M-h resize-pane -L 5 +bind -n M-l resize-pane -R 5 + +# smart pane switching with awareness of vim splits +bind -n C-h run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys C-h) || tmux select-pane -L" +bind -n C-j run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys C-j) || tmux select-pane -D" +bind -n C-k run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys C-k) || tmux select-pane -U" +bind -n C-l run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys C-l) || tmux select-pane -R" +#bind -n C-\ run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys 'C-\\') || tmux select-pane -l" + +# allowd pane-navigation while in copy-mode +bind-key -T copy-mode-vi C-h select-pane -L +bind-key -T copy-mode-vi C-j select-pane -D +bind-key -T copy-mode-vi C-k select-pane -U +bind-key -T copy-mode-vi C-l select-pane -R + +# Shift arrow to switch windows +bind -n S-Left previous-window +bind -n S-Right next-window + +# Ctrl-Shift arrow to swap windows +bind-key -n C-S-Left swap-window -t -1 +bind-key -n C-S-Right swap-window -t +1 + +# No delay for escape key press +set -sg escape-time 0 + +#set -g @continuum-restore 'off' +#set -g @resurrect-save-shell-history 'on' +#set -g @resurrect-strategy-vim 'session' + +#set -g @themepack 'powerline/double/cyan' +source-file "${HOME}/.tmux/osiris-theme.conf" + +# List of plugins +set -g @plugin 'tmux-plugins/tpm' +set -g @plugin 'tmux-plugins/tmux-sensible' +set -g @plugin 'tmux-plugins/tmux-yank' +set -g @plugin 'christoomey/vim-tmux-navigator' +set -g @plugin 'tmux-plugins/tmux-cpu' +set -g @plugin 'tmux-plugins/tmux-battery' +set -g @plugin 'tmux-plugins/tmux-cowboy' # Kill process in pane w/ prefix+* +#set -g @plugin 'odedlaz/tmux-onedark-theme' + +# Initialize TMUX plugin manager (keep this line at the very bottom of tmux.conf) +run '~/.tmux/plugins/tpm/tpm' diff --git a/tmux/.tmux/osiris-theme.conf b/tmux/.tmux/osiris-theme.conf new file mode 100644 index 0000000..76a6874 --- /dev/null +++ b/tmux/.tmux/osiris-theme.conf @@ -0,0 +1,88 @@ +###################### +### DESIGN CHANGES ### +###################### + +# Variables +left_sep='' +right_sep='' +left_alt_sep='' +right_alt_sep='' + +set -g @cpu_low_icon "=" # icon when cpu is low +set -g @cpu_medium_icon "≡" # icon when cpu is medium +set -g @cpu_high_icon "≣" # icon when cpu is high + +set -g @ram_low_fg_color "#[fg=#000000]" # foreground color when ram is low +set -g @ram_medium_fg_color "#[fg=#000000]" # foreground color when ram is medium +set -g @ram_high_fg_color "#[fg=#000000]" # foreground color when ram is high + +set -g @cpu_low_fg_color "#[fg=#000000]" # foreground color when cpu is low +set -g @cpu_medium_fg_color "#[fg=#000000]" # foreground color when cpu is medium +set -g @cpu_high_fg_color "#[fg=#000000]" # foreground color when cpu is high + +set -g @cpu_low_bg_color "#[bg=green]" # background color when cpu is low +set -g @cpu_medium_bg_color "#[bg=yellow]" # background color when cpu is medium +set -g @cpu_high_bg_color "#[bg=red]" # background color when cpu is high + +# set -g @cpu_low_fg_color "#[fg=#83bd68]" +# set -g @cpu_medium_fg_color "#[fg=#f0c674]" +# set -g @cpu_high_fg_color "#[fg=#cc6666]" + +# panes + + +## Status Basr +set -g status-position bottom +set -g status-style bg=colour234,fg=colour238,dim +set -g status-left-length 20 +set -g status-left '' +set -g status-interval 2 + +set -g status-right "#{cpu_bg_color}#{cpu_fg_color} CPU:#{cpu_percentage} #{ram_fg_color}#{ram_bg_color} RAM:#{ram_percentage} #[fg=default]#[bg=default] %a %h-%d %H:%M" +set -g status-right-length 55 + + +# Messages +set -g message-style bold,fg=colour232,bg=colour81 + +# Window Mode + + +# Window Status +set -g window-status-separator '' + +setw -g window-status-current-style bold,bg=colour238,fg=colour46 +setw -g window-status-current-format " #W " + +setw -g window-status-style none,bg=colour235,fg=colour138 +setw -g window-status-format " #W " + +set -g status-style fg=colour137,bg=colour234,dim + +setw -g window-status-current-style fg=colour81,bg=colour238,bold +setw -g window-status-current-format ' #W ' + +setw -g window-status-style fg=colour138,bg=colour235 +setw -g window-status-format ' #W ' + +setw -g window-status-bell-style fg=colour255,bg=colour1,bold +# Bells +setw -g window-status-bell-style bold,fg=colour255,bg=colour1 + +set-option -g visual-activity off +set-option -g visual-bell off +set-option -g visual-silence off +set-window-option -g monitor-activity off +set-option -g bell-action none + + +# Modes +setw -g clock-mode-colour colour135 +setw -g mode-style bg=colour6,fg=colour0,bold,fg=colour46,bg=colour238 + + +# Panes +set -g pane-border-style fg=black,bg=colour232,fg=colour237 +set -g pane-active-border-style fg=brightred,bg=colour232,fg=colour46 + +# Custom styling, http://www.hamvocke.com/blog/a-guide-to-customizing-your-tmux-conf/ diff --git a/tmux/.tmux/plugins/tmux-battery/.gitattributes b/tmux/.tmux/plugins/tmux-battery/.gitattributes new file mode 100644 index 0000000..162dd30 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-battery/.gitattributes @@ -0,0 +1,3 @@ +# Force text files to have unix eols, so Windows/Cygwin does not break them +*.* eol=lf +*.png -text diff --git a/tmux/.tmux/plugins/tmux-battery/CHANGELOG.md b/tmux/.tmux/plugins/tmux-battery/CHANGELOG.md new file mode 100644 index 0000000..dacbe45 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-battery/CHANGELOG.md @@ -0,0 +1,44 @@ +# Changelog + +### master +- Fixed `#{battery_graph}` to actually display graph (@rux616) +- High-granularity icons and colors added. (@rux616) +- Changed preferred order of utility applications to be `pmset` -> `acpi` -> `upower` -> `termux-battery-status` due to CPU usage issues with `upower` (2019-03-05) (@rux616) +- Added `#{battery_status_bg}` feature (@RyanFrantz) +- Added multibattery output support for `upower` (@futuro) +- Added Chromebook support (@forkjoseph) +- Added battery graph, simplify interpolation (@levens) + +### v1.2.0, 2016-09-24 +- show output for `#{battery_remain}` interpolation only if the battery is + discharging +- prevent displaying "(No" for `#{battery_remain}` interpolation (when battery + status is "No estimate" +- display all batteries that upower knows about (@JanAhrens) +- acpi battery status (@cpb) +- fix issue with status-right and status-left whitespace being cut out +- fix issue with the `pmset -g batt` command output for macOS Sierra and further + +### v1.1.0, 2015-03-14 +- change the default icon for "attached" battery state from :snail: to :warning: +- add support for OS X "attached" battery state (@m1foley) +- add `#{battery_remain}` feature (@asethwright) + +### v1.0.0, 2014-08-31 +- update readme to reflect github organization change +- bring in linux support +- small refactoring +- rename plugin to tmux-battery +- add contributors to the readme + +### v0.0.2, 2014-06-03 +- switch to tab indentation +- do not automatically prepend battery status +- change format interpolation strings to more Tmux-idiomatic + `#{battery_percentage}` and `#{battery_icon}` +- refactoring for simplicity +- support interpolation in `status-left` option too +- README update + +### v0.0.1, 2014-06-03 +- tag version 0.0.1 diff --git a/tmux/.tmux/plugins/tmux-battery/LICENSE.md b/tmux/.tmux/plugins/tmux-battery/LICENSE.md new file mode 100644 index 0000000..40f6ddd --- /dev/null +++ b/tmux/.tmux/plugins/tmux-battery/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (C) 2014 Bruno Sutic + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tmux/.tmux/plugins/tmux-battery/README.md b/tmux/.tmux/plugins/tmux-battery/README.md new file mode 100644 index 0000000..5bf4775 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-battery/README.md @@ -0,0 +1,262 @@ +# Tmux battery status + +Enables displaying battery percentage and status icon in tmux status-right. + +## Installation + +In order to read the battery status, this plugin depends on having one of the following applications installed: +- pmset (MacOS only) +- acpi +- upower +- termux-battery-status +- apm + +In a normal situation, one of the above should be installed on your system by default and thus it should not be necessary to specifically install one of them. That being said, the `acpi` utility is currently recommended for use over `upower` where possible due to ongoing CPU usage issues. + +### Installation with [Tmux Plugin Manager](https://github.com/tmux-plugins/tpm) (recommended) + +Add plugin to the list of TPM plugins in `.tmux.conf`: + +```tmux +set -g @plugin 'tmux-plugins/tmux-battery' +``` + +Hit ` + I` to fetch the plugin and source it. + +If format strings are added to `status-right`, they should now be visible. + +### Manual Installation + +Clone the repo: + +```shell +git clone https://github.com/tmux-plugins/tmux-battery ~/clone/path +``` + +Add this line to the bottom of `.tmux.conf`: + +```tmux +run-shell ~/clone/path/battery.tmux +``` + +From the terminal, reload TMUX environment: + +```shell +tmux source-file ~/.tmux.conf +``` + +If format strings are added to `status-right`, they should now be visible. + +## Usage + +Add any of the supported format strings (see below) to the `status-right` tmux option in `.tmux.conf`. Example: + +```tmux +set -g status-right '#{battery_status_bg} Batt: #{battery_icon} #{battery_percentage} #{battery_remain} | %a %h-%d %H:%M ' +``` + +### Supported Format Strings + + - `#{battery_color_bg}` - will set the background color of the status bar based on the battery charge level if discharging and status otherwise + - `#{battery_color_fg}` - will set the foreground color of the status bar based on the battery charge level if discharging and status otherwise + - `#{battery_color_charge_bg}` - will set the background color of the status bar based solely on the battery charge level + - `#{battery_color_charge_fg}` - will set the foreground color of the status bar based solely on the battery charge level + - `#{battery_color_status_bg}` - will set the background color of the status bar based solely on the battery status + - `#{battery_color_status_fg}` - will set the foreground color of the status bar based solely on the battery status + - `#{battery_graph}` - will show battery percentage as a bar graph: ▁▂▄▆█ + - `#{battery_icon}` - will display a battery status/charge icon + - `#{battery_icon_charge}` - will display a battery charge icon + - `#{battery_icon_status}` - will display a battery status icon + - `#{battery_percentage}` - will show battery percentage + - `#{battery_remain}` - will show remaining time of battery charge\* + +\* These format strings can be further customized via options as described below. + +#### Options + +`#{battery_remain}` + + - `@batt_remain_short`: 'true' / 'false' - This will shorten the time remaining (when charging or discharging) to `~H:MM`. + +### Defaults + +#### Options + + - `@batt_remain_short`: 'false' + +#### Icons/Colors + +By default, the following colors and icons are used. (The exact colors displayed depends on your terminal / X11 config.) + +Please be aware that the 'level of charge' as noted below (e.g. `[80%-95%)`) uses interval notation. If you are unfamiliar with it, check it out here. + +Also, a note about the `@batt_color_...` options: `@batt_color_..._primary_...` options are what will be displayed in the main `bg` or `fg` format strings you choose - e.g. if you use `#{battery_color_bg}`, the `@batt_color_..._primary_...` colors you choose will be the background. Likewise, the corresponding `@batt_color_..._secondary_...` color will be the foreground. + +Level of Charge Colors: + + - primary tier 8 \[95%-100%] (`@batt_color_charge_primary_tier8`): '#00ff00' + - primary tier 7 \[80%-95%) (`@batt_color_charge_primary_tier7`): '#55ff00' + - primary tier 6 \[65%-80%) (`@batt_color_charge_primary_tier6`): '#aaff00' + - primary tier 5 \[50%-65%) (`@batt_color_charge_primary_tier5`): '#ffff00' + - primary tier 4 \[35%-50%) (`@batt_color_charge_primary_tier4`): '#ffc000' + - primary tier 3 \[20%-35%) (`@batt_color_charge_primary_tier3`): '#ff8000' + - primary tier 2 (5%-20%) (`@batt_color_charge_primary_tier2`): '#ff4000' + - primary tier 1 \[0%-5%] (`@batt_color_charge_primary_tier1`): '#ff0000' + - secondary tier 8 \[95%-100%] (`@batt_color_charge_secondary_tier8`): 'colour0' + - secondary tier 7 \[80%-95%) (`@batt_color_charge_secondary_tier7`): 'colour0' + - secondary tier 6 \[65%-80%) (`@batt_color_charge_secondary_tier6`): 'colour0' + - secondary tier 5 \[50%-65%) (`@batt_color_charge_secondary_tier5`): 'colour0' + - secondary tier 4 \[35%-50%) (`@batt_color_charge_secondary_tier4`): 'colour0' + - secondary tier 3 \[20%-35%) (`@batt_color_charge_secondary_tier3`): 'colour0' + - secondary tier 2 (5%-20%) (`@batt_color_charge_secondary_tier2`): 'colour0' + - secondary tier 1 \[0%-5%] (`@batt_color_charge_secondary_tier1`): 'colour0' + +Status Colors: + + - primary charged (`@batt_color_status_primary_charged`): 'colour33' + - primary charging (`@batt_color_status_primary_charging`): 'colour33' + - primary discharging (`@batt_color_status_primary_discharging`): 'colour14' + - primary attached (`@batt_color_status_primary_attached`): 'colour201' + - primary unknown (`@batt_color_status_primary_unknown`): 'colour7' + - secondary charged (`@batt_color_status_secondary_charged`): 'colour0' + - secondary charging (`@batt_color_status_secondary_charging`): 'colour0' + - secondary discharging (`@batt_color_status_secondary_discharging`): 'colour0' + - secondary attached (`@batt_color_status_secondary_attached`): 'colour0' + - secondary unknown (`@batt_color_status_secondary_unknown`): 'colour0' + +Level of Charge Icons: + + - tier 8 \[95%-100%] (`@batt_icon_charge_tier8`): '█' + - tier 7 \[80%-95%) (`@batt_icon_charge_tier7`): '▇' + - tier 6 \[65%-80%) (`@batt_icon_charge_tier6`): '▆' + - tier 5 \[50%-65%) (`@batt_icon_charge_tier5`): '▅' + - tier 4 \[35%-50%) (`@batt_icon_charge_tier4`): '▄' + - tier 3 \[20%-35%) (`@batt_icon_charge_tier3`): '▃' + - tier 2 (5%-20%) (`@batt_icon_charge_tier2`): '▂' + - tier 1 \[0%-5%] (`@batt_icon_charge_tier1`): '▁' + +Status Icons: + + - charged (`@batt_icon_status_charged`): '🔌' + - charged - OS X (`@batt_icon_status_charged`): '🔌' + - charging (`@batt_icon_status_charging`): '🔌' + - discharging (`@batt_icon_status_discharging`): '🔋' + - attached (`@batt_icon_status_attached`): '⚠️' + - unknown (`@batt_icon_status_unknown`): '?' + +#### Changing the Defaults + +All efforts have been made to make sane defaults, but if you wish to change any of them, add the option to `.tmux.conf`. For example: + +```tmux +set -g @batt_icon_charge_tier8 '🌕' +set -g @batt_icon_charge_tier7 '🌖' +set -g @batt_icon_charge_tier6 '🌖' +set -g @batt_icon_charge_tier5 '🌗' +set -g @batt_icon_charge_tier4 '🌗' +set -g @batt_icon_charge_tier3 '🌘' +set -g @batt_icon_charge_tier2 '🌘' +set -g @batt_icon_charge_tier1 '🌑' +set -g @batt_icon_status_charged '🔋' +set -g @batt_icon_status_charging '⚡' +set -g @batt_icon_status_discharging '👎' +set -g @batt_color_status_primary_charged '#3daee9' +set -g @batt_color_status_primary_charging '#3daee9' +``` + +Don't forget to reload the tmux environment after you do this by either hitting ` + I` if tmux battery is installed via the tmux plugin manager, or by typing `tmux source-file ~/.tmux.conf` in the terminal if tmux battery is manually installed. + +*Warning*: The battery icon change most likely will not be instant. When you un-plug the power cord, it will take some time (15 - 60 seconds) for the icon to change. This depends on the `status-interval` tmux option. Setting it to 15 seconds should be good enough. + +## Examples + +These are all examples of the default plugin color and icon schemes paired with the default tmux color scheme using the following `status-right` and `status-right-length` settings in `.tmux.conf` + +```tmux +set -g status-right 'Colors: #{battery_color_bg}bg#[default] #{battery_color_fg}fg#[default] #{battery_color_charge_bg}charge_bg#[default] #{battery_color_charge_fg}charge_fg#[default] #{battery_color_status_bg}status_bg#[default] #{battery_color_status_fg}status_fg#[default] | Graph: #{battery_graph} | Icon: #{battery_icon} | Charge Icon: #{battery_icon_charge} | Status Icon: #{battery_icon_status} | Percent: #{battery_percentage} | Remain: #{battery_remain}' +set -g status-right-length '150' +``` + +Battery charging at tier 8 \[95%-100%]:
+![battery-charging-tier8](./screenshots/battery_charging_tier8.png) + +Battery charging at tier 7 \[80%-95%):
+![battery-charging-tier7](./screenshots/battery_charging_tier7.png) + +Battery charging at tier 6 \[65%-80%):
+![battery-charging-tier6](./screenshots/battery_charging_tier6.png) + +Battery charging at tier 5 \[50%-65%):
+![battery-charging-tier5](./screenshots/battery_charging_tier5.png) + +Battery charging at tier 4 \[35%-50%):
+![battery-charging-tier4](./screenshots/battery_charging_tier4.png) + +Battery charging at tier 3 \[20%-35%):
+![battery-charging-tier3](./screenshots/battery_charging_tier3.png) + +Battery charging at tier 2 (5%-20%):
+![battery-charging-tier2](./screenshots/battery_charging_tier2.png) + +Battery charging at tier 1 \[0%-5%]:
+![battery-charging-tier1](./screenshots/battery_charging_tier1.png) + +Battery discharging at tier 8 \[95%-100%]:
+![battery-discharging-tier8](./screenshots/battery_discharging_tier8.png) + +Battery discharging at tier 7 \[80%-95%):
+![battery-discharging-tier7](./screenshots/battery_discharging_tier7.png) + +Battery discharging at tier 6 \[65%-80%):
+![battery-discharging-tier6](./screenshots/battery_discharging_tier6.png) + +Battery discharging at tier 5 \[50%-65%):
+![battery-discharging-tier5](./screenshots/battery_discharging_tier5.png) + +Battery discharging at tier 4 \[35%-50%):
+![battery-discharging-tier4](./screenshots/battery_discharging_tier4.png) + +Battery discharging at tier 3 \[20%-35%):
+![battery-discharging-tier3](./screenshots/battery_discharging_tier3.png) + +Battery discharging at tier 2 (5%-20%):
+![battery-discharging-tier2](./screenshots/battery_discharging_tier2.png) + +Battery discharging at tier 1 \[0%-5%]:
+![battery-discharging-tier1](./screenshots/battery_discharging_tier1.png) + +Battery in 'attached' status:
+![battery-status-attached](./screenshots/battery_status_attached.png) + +Battery in an unknown status:
+![battery-status-unknown](./screenshots/battery_status_unknown.png) + +### Tmux Plugins + +This plugin is part of the [tmux-plugins](https://github.com/tmux-plugins) organisation. Checkout plugins as [resurrect](https://github.com/tmux-plugins/tmux-resurrect), [logging](https://github.com/tmux-plugins/tmux-logging), [online status](https://github.com/tmux-plugins/tmux-online-status), and many more over at the [tmux-plugins](https://github.com/tmux-plugins) organisation page. + +### Maintainer + + - [Martin Beentjes](https://github.com/martinbeentjes) + +### Contributors + + - Adam Biggs + - Aleksandar Djurdjic + - Bruno Sutic + - Caleb + - Dan Cassidy + - Diego Ximenes + - Evan N-D + - Jan Ahrens + - Joey Geralnik + - HyunJong (Joseph) Lee + - Martin Beentjes + - Mike Foley + - Ryan Frantz + - Seth Wright + - Tom Levens + +### License + +[MIT](LICENSE.md) diff --git a/tmux/.tmux/plugins/tmux-battery/battery.tmux b/tmux/.tmux/plugins/tmux-battery/battery.tmux new file mode 100755 index 0000000..0c666b8 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-battery/battery.tmux @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source "$CURRENT_DIR/scripts/helpers.sh" + +battery_interpolation=( + "\#{battery_color_bg}" + "\#{battery_color_fg}" + "\#{battery_color_charge_bg}" + "\#{battery_color_charge_fg}" + "\#{battery_color_status_bg}" + "\#{battery_color_status_fg}" + "\#{battery_graph}" + "\#{battery_icon}" + "\#{battery_icon_charge}" + "\#{battery_icon_status}" + "\#{battery_percentage}" + "\#{battery_remain}" +) + +battery_commands=( + "#($CURRENT_DIR/scripts/battery_color.sh bg)" + "#($CURRENT_DIR/scripts/battery_color.sh fg)" + "#($CURRENT_DIR/scripts/battery_color_charge.sh bg)" + "#($CURRENT_DIR/scripts/battery_color_charge.sh fg)" + "#($CURRENT_DIR/scripts/battery_color_status.sh bg)" + "#($CURRENT_DIR/scripts/battery_color_status.sh fg)" + "#($CURRENT_DIR/scripts/battery_graph.sh)" + "#($CURRENT_DIR/scripts/battery_icon.sh)" + "#($CURRENT_DIR/scripts/battery_icon_charge.sh)" + "#($CURRENT_DIR/scripts/battery_icon_status.sh)" + "#($CURRENT_DIR/scripts/battery_percentage.sh)" + "#($CURRENT_DIR/scripts/battery_remain.sh)" +) + +set_tmux_option() { + local option="$1" + local value="$2" + tmux set-option -gq "$option" "$value" +} + +do_interpolation() { + local all_interpolated="$1" + for ((i=0; i<${#battery_commands[@]}; i++)); do + all_interpolated=${all_interpolated//${battery_interpolation[$i]}/${battery_commands[$i]}} + done + echo "$all_interpolated" +} + +update_tmux_option() { + local option="$1" + local option_value="$(get_tmux_option "$option")" + local new_option_value="$(do_interpolation "$option_value")" + set_tmux_option "$option" "$new_option_value" +} + +main() { + update_tmux_option "status-right" + update_tmux_option "status-left" +} +main diff --git a/tmux/.tmux/plugins/tmux-battery/screenshots/battery_charging_tier1.png b/tmux/.tmux/plugins/tmux-battery/screenshots/battery_charging_tier1.png new file mode 100644 index 0000000000000000000000000000000000000000..c45862770629c61450e9b0ef20717449f1ad2132 GIT binary patch literal 7485 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 literal 0 HcmV?d00001 diff --git a/tmux/.tmux/plugins/tmux-battery/screenshots/battery_charging_tier5.png b/tmux/.tmux/plugins/tmux-battery/screenshots/battery_charging_tier5.png new file mode 100644 index 0000000000000000000000000000000000000000..ee8f54d9978edf9b55c82ddec99956e9d49453d4 GIT binary patch literal 7743 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 literal 0 HcmV?d00001 diff --git a/tmux/.tmux/plugins/tmux-battery/screenshots/battery_charging_tier6.png b/tmux/.tmux/plugins/tmux-battery/screenshots/battery_charging_tier6.png new file mode 100644 index 0000000000000000000000000000000000000000..02a5241079f2a435bbf35fcf490190913284a5b8 GIT binary patch literal 7791 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 literal 0 HcmV?d00001 diff --git a/tmux/.tmux/plugins/tmux-battery/screenshots/battery_charging_tier7.png b/tmux/.tmux/plugins/tmux-battery/screenshots/battery_charging_tier7.png new file mode 100644 index 0000000000000000000000000000000000000000..c756c41849f8868211b13015c9c610fc94c6cc17 GIT binary patch literal 7819 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 QO_%Ys)b-UGRUD%K3m4&@6aWAK literal 0 HcmV?d00001 diff --git a/tmux/.tmux/plugins/tmux-battery/screenshots/battery_charging_tier8.png b/tmux/.tmux/plugins/tmux-battery/screenshots/battery_charging_tier8.png new file mode 100644 index 0000000000000000000000000000000000000000..faa2475ad623905ab08ac1fe207f0905bf5b8880 GIT binary patch literal 7799 zcmYLu1yCHp)-D>{Ay{B>*Wf`G_XJ3Af@>f_g6p!lyK8`8!3i4(?gV#-;1*=(#8rhJzia=>v_ zHF8HmA?*Lpg__PyNQ;6( &;r|XkkYq=J`x#&(ZGC)V zY&k!BeR=#|dQv8POu{@H>fd!!a2uF7*U+wDjb84)w~{mCJy!5u0ud_dGz#)n*?T1V z!HbA~H&mjxQaAXVeW;t>MKd(1SU!E(r&ylzNG&Y3tkq~+r`8ze+v-d1WNlGzX&DDZ zM@pz&zhNN$|Aa;!s^vdwmdZn#3doU)fd~pL)e8(3f{`L8#FXfX`G52N|G*~W`9c0V zZ$;r2XWfH-Et*E>IQ{g{ZIu4}f;XPu{@**-{We!OssvCmY>#4z_U`GB!od3mMY+g- zq!CTr8h^BoQW6!4W@$k_;IbxU{GZ+ST$6JA5YTZC z>IAxghTX5m&jXsHV-ac76*nTg{#NyjiCxhXJ_Y9ZIiaw>UDK>Oy^G;Ptu(ac=xea| z{AE!a0QOrY!eQ2}r6KTvcw%kbinFr50N8~FLRQ?H~9{PxN#;kr(?!l$_OvMP7 zd#P!$x*Tz9!=GZx#aT3ict_UxChk_3|HkyRf+YRw_~@%GOHhtJ+ZLv46Z#E3^X zF)*~DSnZ(e&{QV{q_qtkO|;DjU(1we1Y{5$I%zZc88n@U48Ly8{qq&$CFT@){L~)f z*oSeSgj^)0tOT1JRwJSCbdB_+ho! zmie}&r)8o73O7&DBFzc*_f$lec)RI-c+wSa=}U#yVSIRkM>lG!6R_!t`0vmWnQ9Xw zoR7q^M&-oq3{h)*J8P3TvfFhy+eAaKr$nMr9z6BtkQ0go?Pl5mSkHo*| zK4*zhKek@;q)w3oj9^u4^tFYXa=nmjR2eJMw;iJkA}@ZiwE_X#FYQE=c5v6(q^9* z9uLilp6fWBKQfdNl649N^f@U7pWSY)EEC`3{#wE!G{v9LcjJ9z<&mcE^r2?^LQo?l zE7}(#dA;`(>Tj7UvAIFMzk!eS5!1AdpJf2yP}@U zY@!6AJr-|V^9M_V7lTc~Siz5zJMT6fEkvf!#;|HJ9%zw+-QJ}yjw-4*x;vO3yaUbz z*MO<9CEFw~KkalgZEgj$K7nab>z)gT;N71$xh((|Ze0%s+~!2?;L%`SFpI17d2Z06 z7;1q^OH&7S0Dzh))I9ddD0criEF zh#xVW3$d12So}7BP&19c8|nk`k`|h`kri~&Jm`m(VA@SaLOd%e!_v*0fC zWS7_-i^-ooK!OxG!{7MNUVOXnA7%>M(f*p@^6v+F-AVw;g3!+j%sYbosN(Nk!W?ko z`%7@z?R{dk_IZVP4DImOl5~v=QzluW`D05wIRDU7ufAFhzOK2=GaIAL)mh-Iz@fMt zvow_IaYqv5ZnO++|gS!l|-v+Vkd!XQS9ntTdt67|=O6kj2Qs{)RZ zOkBer&w9K?%k4^6$dkr-J3|I9kBxynQf1c4ThHUJG~V$2g}amaNkw`L!QAow{UPNg zs_XKVXA-VaZwL%MeW-Azgt${i^QMny|Ls_o*y~XI&cYb4h4(bmZ6)8$HA`$oK1=Qu z_WzUaPO!eGc4`V!KAU}ogz_I5nEz+V<(S3vcj24ia!qj&G&)BzMtQ-n-051n_z8>B zcNMfAHhtwz+E(4@vw(B!{MBe^v1ll*la#j9Ll=Vd`L9%<#l^LMY=V|6bOywDnqbh6j85;2W2Xm&IL?A_>$|KjsE8hU8M0jtw9G(E8){h5QFPF&YzS~BVVU4Ig< z?e&||B4{XKrKE{0+RR+1&CSe4EJ#2o$k^(=Nl(r_@8}?Z_l)Jk)+Zg=Yi-{X!n(Z@ ziq|7aF#!$nw7^kZ=-x}UxcID-=$-Lz5&`U% z>o&!<)uHmSo08T54}nF)diU7EW^lt+`wNM2Gw3aVVWj`I<$Tv-w96hbqxMEo4Y4&b zTZ7ZPn}eUn-MP*s&T%)DIm9L~oM#baM!1jYS#jEfd6+!YMnNK8DD;>KN{=#tdgwQQ zLM%errpejvPuQgt!Dx@zMVn6O1&SO+eDrBUYEjP;q|=1q4ec0)=$i5zE6>S@4oqid zjwCPTCU|}~q&xZz$lUDF2WVa=w`~AG=~ruopv#n^9GUrM;v$lh0M;3hojQu%RYMhkSc=OHS)A3hscj+q12Cx9DhYO zOj>3s(p;Z(D17czw!I}*H^bO1q3&BpvDc+_r9plUSC0nh%ZZH+)C8-)e*L)3Feyyx z@s1!G#m940Wc9I3x>ImlQ^{CpMn>}*0|?VH>qF3@iia9Gw33xK;Ga1s8YLzY~MX6B@W0Gj02BDj( z&O_X%Fbq*iVUg!8B6(hUD7Hsn>+PBh-GMK5_ybI-K2$|_ucLk~JP4byY@T_f|1A+R zQ#0ikPX*dK?o)J*--Npbrb&$R0IlP9$+310Pj&Jx^9`Gum?YG@dgd&P1Ks?mO)CT4 z;1AilNkpbcRo`v{1mEWv$>anE84(erOtK~Okd_e^vCK(!j*bKL;3_K5dk^;F7e=#p z^l3cJ${eV36j5kcnl;~W-SoVf-EUSs;#o^81F%9HidD%IobWGIHrqEyyh%n=kbNYrCTb@;zTF4~y+?FA!6C!STdTg3?bYbA)Fca+njbGm{Gu?ISkO*MNrw{=q~Sg#ZP^j}$F! z8wU_?FN4=;Z8_ z)_rK~14mu5`QMy6((5dQqwL!#C4-BY0TDlgwB1=Xlob~zfGZVevy8;^s7|{I;h&}3 zdOXB!(9&%a^RklJgp1@++Ygsu(vgN{^h;g$hP<@{i5DMcA4KIJYEQU9ZdWxowDK3j zEWac~PulPX!cWp0pxFjihihF+w;b1Xp1&ZqPvt8woblg!kr`Db)u|P}?Y%7^G9rOD zi}=?t?$b$gNBn>fNu}?aOx|&Rr(IINpN67;ukM?lEx1uC1EVBFg_`~+8{R;!GX9xZ5M~8f8 zMH&hWp)?=d=ypj3Yo->T%hcGA@y<7fT7rRjnRPcfj#M|vWVdw;5KjXN4@!AOI((3t zRi4w}?8JL*o_-9;AR-2dC-}BfA1~;?=T8T4^ z-VzdIRWegz0WTxisO)@rK=&_8lkqQ?WqXm`B0h)eBlgcbkXaSu*u;6aYQ{Ucu6wx{ zm^j~^pm%n}5A+HfH3A=iM@gIwaeK#|${8<~tfHV7;|g8-mds%$G}w3?1b2+AUpjD? zh%T&y{bHdbva<4%m9(Am2jWDXK^hAcC20d0!s~ zb;`FWh13V91Ou5@Nc#PGXQO^qUE31Qk1X~T*yDNsh9{8n8Jp0ttkr&g^TZ4EH*1-? zt|ir%K8n)bpNlm<#){7e6m%K4jMBN)xsn7xbqu|JIABR@ZhUB&f=m$W+ltoxZ=yl8 z-xx^5!jZx$;pKc18tECPbM>krj_E6|_~zaY+lBJ65qucd_U^*IECns_@+G>M+tb@- z)HzApGJ;u)M<__hPJ41IDi!VdoG_N%+&d*b`I!w2^fVP0~fV+Gq6qZ+5{lE z^-x>(YoZ2cNc=)G8d@_^WicW>(EKL^68-_gJFQ|BG3KMVw7gihfx#UY zomiXMRJLIlHDXQrUBA&VOKU5~70-UYLRwP*5C%poij`m5U&8)KDy!iX=7K-#?Fo1; zgkMeyvI@93vt4?XRp6W@o+KxarDTzp$3SE3oS!(aj=pt7on2{@e=7?5W?zIgDNm%H zD1W$39NoqGBPW2Jm}I>)t_6Z=k_13t#F+ONUd#lfzYw9428*M^dZKe0%- zD~<2{49BN%MWvUB1#{l17%EnFho|ujJ_ybACHF#e*>ezpL<3!GG|ASq(Q&Rn`ak+19G3U=2Cy*bVlkA&@)BX$lze{m8nSZLsqRLGPE8F#kh|Qgh;-^>{ z+sl0h$i&q8{|r};;kRfV>BJPyo9S_s=Ioxj(Y`_Yu?9K15X)0@nlLMULkVwIp|?Uu zx$`Qwx-iB5%k0QAx>g)We9^IH`jfTDRRX}`56s{Yi}kyOR}4W6scsDJObF!dW@GR( z6|nF7@?Lqi03^rt4Ei0~9LmEp$^&iPE9t$FZB%#oJu}d|1@Yvd$pX@BjvHHI-HI@6(uk%#zLtn1u;^pPUb$3}3Z8`^LNCgrv0ni7^jN0YhAR z;fe9paYY<0mnTme0!Ef2UiEw6zR?|)O%TbMjZqjy(%&R5&M>{X3GC=YC5*-ejh_7D zMcEfVWLsYP-SJUsSJvOi1WD@b0==6Z*W$IC27;Z4`9l4WF1DuomiR30<$qPkcp+7A zV`J)amr`l^akP*S*Hg0Cp3h zH+5hZIZ<%^G(b2}X(6idF_-Z1?`KN&7JhS*7&d$E&%iL03=JZt!-uBz0sA(hUj@L& z{{u6F1nbAhmZ-nfte4I&cuNQ-(`4|FMda&QrnNW4jxbIe(0hZuFy&Qpe`_c?QToZ1(Gck-P?$MqmiZ6^e0P6*qzKlD|q3Cr|%|(Rb5kUN{lExCW|{38vDZ+TRQP0lWz*GU9h6} zaOS?LpT)i!woe(z3^@F41_|H=vhOuRjFlBeV>*#JMY*-F3TzTo02t<*ZfLib#+=i> zuhRL?slS@<$2825CbHL$z5MjHh7F12?IR3heFcqA$ehHofGy2INiXM%3JHnKjcnW> zD6^{br(i(oEjd4u%Vlcp!Z~SMcH=cfaDt&8g#wTc!8msidjXkLpSn3==?BKI?ieCG zVKbe%bO+6gAnBr+(m1Ic#3E()dZerm!B`d^mYH7bFN5!SMIpVgmMO z=|Zp;owJ-F)@e*svoI?N;cAHTDg^tRG$kxTtYgn$2NLi%lL4)X@^30DE)MGuW``;*)gN_ew@0(L4#r8VLJ^WY+a#1x67nJ%+)888AIVBp| zHxY`a4{=6qLy$Y?pr_z?$*5t_H^al6)}$lBP(r^B*+N{9ZVt2vK{G+EuIjxT{LZ)4bl7hIvgoe@_2r>=m0{3wncl_7qc^R1mt}S*ZA)0yh`+P#{b%vh@SkRL zQT4CgCgyMboU+6(gzR>|K@9?t)c-K6<@!R*Z4 z?9HHycO86(iUnRhwqRY73;jC%!qJoRS{8a={EAFTCdV_VQn_ZQ%@Hw|1=P)$L}Nbf zLRK^|ujZ(TPd{)rvOBF#xKSoyOm{xFiLJ?+q{#F8Xqf@Y!;y?YCFO^!yuYC*nJ~l} z@T7GP{n$%?Y<9f=EOG0}#pUg3o&7U*0VTP5dloJivYo)N%z|XQMXUW1n-FV5EjlG! z>*XFu(&!aj!ga#^U#D5ky`w+>sWgX&MKObu$J6Zej<1K`BCAnU7htlPg$a*Bf_knP~M2+=6mFGPy z+Nh*fD@YZgTww(FZaTld%euJ|e$%xaE#h2y-J!2f|0^Z$)191aq*ZZ~%Uy)PSJ^@L zxblZ_vOLF~RVvU6%((l3Z!hDXMX315cGpEqx>v+xe_?Y+PL(qWK&Oco`4OC}p*a-# zDBy9$&0T-O8uRD)eS$FB)z6zBHg&!wjL%-}Esoq^i;bjr+ab9#2QP$SCh+g4t4D!p zYUrgo+0Qfh@rYS2N5(r^+^t^%RR^_f=W;0XKQ4OY0CT6o;swJ&DN3(3vcyD6sz0c9 zr3@lQ8+>D2ECkF1_bHL>#p!-+`6Jdw{g!r{;!SL9v2REIbNnDE`BsX}wd`w_H5(bY z{nLeuuoDU2CmjZqkNAgQ3N$#O5A!ebrZ+Eg!JOEcszk){bLK9$=BmDnee|2R1bm?O z%dyfu)2@$S>Z=G*KVLmgjh*;gBER|&wO1&h91A0sy&Cia^&TSzwd*c;-^)lTeXzQ! z6X%Fnr6HqqX#Z75^FDg*m7Ip7DjyEgq^nI#s@R38Uhe40;9psf@xNdmI5ZF4vTE7U zX_M3`S^qLvDrrV0$hQv5!S1806UFy#U!h8nEa-WtNXBh|d>=GXumz4}ba4nVc!eXI qrGdlkrdzz)gLgs#NTFhW2EZO|7DNn*!;zh26g8!Hina0F!1Zk&~q$R@BZvK9W7-%Y$|LdBqTgl6$L#cB(yZdJs~DK;{2&mG6-?O@>DVP zK|*>x^xuJ;_5Sr6BqZu)RRvjtz`}n;LAfMLofkmEUmpCD{E|GUY>ezo-xxl}6Jp6F zej5r0Gsi1sy#LMZ)om3^5y>_e0v4p=MJrPg;L62kcG0oB=t@w~r4&Y!&9=!sDux7i zbyuI!{%|ZQt}8Asf=bRvkoX;QghK8YMQ@jbHqzOPYi318R|AavWFKQBjpv*Jei3`d zyKKF)cliDQGpWcTbEt6W8OYLQtZz5zc2G=H_@>^+`=})1|1a5d`g=cA-2c6x($R!t zegA9Gv95nqsJ1Yeh@hqY3axcnOkJ}WzN1=*zz8la9+M#@K(jpk0;S4B{=1QI>nHq+ zxx~#4t?qab+MNKJ?~R!5tT1h%gD1Z9A53GTd+C}!^4FTGj$2A{oA_} zczFXGDo(lePn?i3nI9BFOGTo zgFlC^Cq`oXOu(5SS_GkdYS03KboBw=g@^;9l)BF$??Wn)v+}&fkDCp`=SbMhaeqfDR(^lZ!?7rGYX%%t4@^uWdbKzkQ} z`^<*>U5Os1le^2R+R_po)IvF<__kKrw4hEktg!s0_k{B5FqLab*Xc!$LIdj6dB6ZV zFNhpD+ftaBORS+8tG>vW`ji0&hoyf=M^e$knxMcrOISByHS^g$mA;7%b8$}UvRW*q z)}eqd>c}Minnk5w1du*yh}SinOBw-{E9tZHX&s869C)(w8?@VZKr2c1*??BgyDI;d zXbApwEwiB9dpRbE8eJYUwGVY5*Hw5qw=3<&oF(JO8#9A4zN&wbfYTp<_(8ff0o;q1$r6?7>RXx8xnm=suOm}M4X4Im&<64XV zjtjC#9q zn91j2o*`i5@G<9s$LD?8%4kEm_wr2A8lSOFSywMPZs<&J@jN9TQ74A+%tXJYLM6Ur z&tF)c42zA$QyF5dlQTSe5ee|%32#}P4)5}}R*xVI(f<;+T@!zOqPWR#&Co!ei&cv@ z&ga}6p&k(sfhEH{fN~!@-9(F8fl?Mh+DFs()EAU@lh!ww=UN^9NrtL}Ddnyg1txYa zPTj{HZq=p&M;D=7!7W6RSlYgqMEQ$#il!D3)b~$j;{M_~{4&oQf=tv$Wlh%k#EQ*< z{E|HuAm&M{lR0vJqWYNnNYjp_jO4Xxxf#GYuZ&#JXg&ZzpMG`B=bVECMz%weZg_2y z`0z8S78Nv_$&WYJPENm@t^UGgE$XSovXRgSN4|k@0nFffx3@jO$35zm4nhOcdz%>E zPyfsc%ajP$K}BH48Kt-p=gOaZv{_k5iJdok$>vRH;q|mZQQT)e87tF8ChF>xydG~i zx}N8HdT7A+=F0Jr?dY5S}3 zER%^S^xpyJ9~ZvoYy4T>?bim)OT0Cy`89uJ&?z_<>T5 z9-w}|!_q?7ym~J6Yd0y{jX!O8;YIk;lRqYMPL{>pAX z-v`}Gltv_{G3H%e3}$K-sa00TkG6esIua=0>Qpd({PJ~J!1lzB#KLg#X8*6AbHCmy zKz4o=Mgn&%JnUjqOZt48)~MOqn!j;eS3f8*pV(w2n^cvnod52{)T`!IlebKN?~1m| z2kZ$#vYy@>hG4VcLUO)dn#EihK2?<_G(MWT4~PU51t(Hk{TWVb?%3)1tkbhJ)f#rW zu{pvwE$8Bh?a=$V>aRfMZjNgxX3>d9-6e8S3io#-`aqA=Me=u(Ws|uv1+vlGHHy+M zSVZoT-EaH_bK~0zKa3aAGl4C26sLeV2dccq5l)k2S_%Q`KTEKCN`MV6SiH`244# zDq>ci-aaOA`Ri8h5gpWx%xE$2a)V~8;1o{M&CNr+N8-*)+K0c| zA)60%kq^>4no8fyk2;Ur6WgsRJ;tEhEXIJ+y$`+Xw#r}8oAg--+4uWJN2yfilE)-_@&ujNZ0=2!F;>_U{unX*$Het1|k4_B>g z28e0}6<3x*&2pkenVS~LCqkLynKyapG*QrL5l6yCaADwbsI`HP@Ax*L(kbrfnu#!F&#_vwfO4oPEYkz4x_bI(OWC34U_^ALDxdspZBGeX~}o(1HHe-eXR>a?w( z74%Q=u69ZpwOi|Wg2YU3&_9#&pmlYh)t(JhO`-n%m0rixp+M<^Zq^I6JC6110c2tWUl~UL7&MD5k66BW8A0Q#n_| zDeI4$x<8+dCm2Wm($qclfDL)W)WWWCInzBtVRxwP&-5cIC^PS(WKiTlwDGNtqPGAX z9Z{67vi3C3r1suV{2`x@3p*SyLH#v@Z{a|96jNPoVA!kbuqsepy`f;Z^l}@=jJI9? zJB@>=0^4SHK^)Y)F6}+8to*pTn~@UcZyJV;c8;_CSoOnV(q@MFEuzlGN>KGnEhL|& z$H4^U00C6@L1&g5S>@=4dLQ&xl3gve_l~VN>vRED?;;$K^~Ciu;Z=qC39dTN?A}#{ zJ}-z+)Rk&2MEp9JuRFPul7gVcuklm{jfsr}K+$A#4!gH@EN%1^?|;oguS-*}%4QVf zq-YfJYDT&8zLmta*6=V43))z)k9)JuechTuS99l*^;Sa&GjJZ)x0z!BhP|{6|H7YR z_(o0Yxuf5;Huwxz&X{5Ub(q7Phs(U2#4BUUf9ZJqn;&Snlz-^q^FM53} ziu$0nzf$x#u_R5n$&HCRKRemU9BZNbNRbB6laf}XFe;vg^^yfPO$h>#rDmw5Q&uh) zRn$|iLSro~7sM(Lf;NGCBWaUXT#wy^O$8{YGld_LS4h8uV;3pcOM#<&V^F|=Rfz1R zuCguwb{XVA>!aI@z7~ULx7&HJ4gA4~cgpapt-dko%u5b+#43QsW2R&#bE!skc}e&B zxMuS4Mqk5UoH>J)|8!D9P{*(l@GJ0l3hTq1^20_jcm1h2SisfVc0L*r1uw@6?b%zCOv61 zihpUw)b9ipNda!lCJsZw4%tyvrRSZqpiVR?paA&=_z^~Fj4k)?SdYZAVS!zuyzP^O zw1nNB-vMopo&1l7oK`6z-gAbJig2=FiRDDU&sjs_Kdm5n(fMqhol%I!0uexQrzx9- zJ3|s(0ZGaTzfYA-s_fYB^0Z>}{h$rckIqfyAlkS2xJyJi#zmhq$`}<Zm*-wCyQte3&*u`%35?=1>lWr*jawblBk( zQyI<8=Qm(}OjUO<=mpg`>}6~S)b6N`+@g}3=`jX65_~u6_wPyCRvcbG<`%D=@R{o62dTS8yqp-xfOsduuc^MY;YWGZ?0Jb1(bFWQudoWtO7)nc{u_+RxH$p$ArvQZ z#$f+tie$9UBHLY^)eJ@gSsM0{kI79@2p>Y!l_6}(;~^*2?ft)RtLLYB5^QWYMI{ct zx8?jV<0n^XOp2PB$g`*Y*GfKTXZ6j?T!*{%2u;8;|K>A64Ki;)0+j>9gA#SO40U_U z&SQ*m!tvPK(a4&0bVw#cuL&X{vQ@ebi6;;Jdgo9@|JKwJ=ZjHpFtp*QOY6pX<{>6 zObAW+O*`~#3*oReH#gUVGV}O~b5UEBjiD~^S@{tS!dhcQ(ra6GhX6+YpOg01|MmcA z=da-Kj)z$-+W=gG;|~{fYd4zoe1iznelVb&W81#9t8miQ&Dbjc@eWi$ac#nXiRVRh z&XkW^-U%g-dQVNaESKk=ZLcc9HZOS}Q=D#P2Y2yGns?X^BL@mXe5H(`GNp5Du2*>~ zz#^6wK7~u}c>eA5zK}}O2b_TL)3mfny_y2c1)=6_f$^3pGx>n9(XO9*P3nPUfQ#q; zm~(uodGYz7m$_dhnd{*FmB?5{ehzg&0Qk?cN;QR4mtnQ77Cj~>`t)wq?U~gFf zdFO8L`|EQKg`NsVg2t*45_g$-{oc2e?!p_OXaA2L>*Iyh?8ohHHcyfGZCy&~4gx=a z8CBuo^>{<#0Np!>a?5co-bRS(p~$>&oW42dNMm2(O9k%820ogc84lb~d$yVJP3 z(|_tM_(pcE{6JoeXlrLUA;@`KK}aJvJYb&b^{?%CBd}BT8swZN^^;IV?*l{kTVNrO z+Mj|uk!GST_$)#Q{BZ*R>(Gu$_P%2F_m11q#RX8^ynU3bUHJ*;B;%Bdf86f!B-n;p z@6Ljprf^x0Dxv^&MY~b|WyE{jIBQ3uCbUoCyOFb)(TRbL!8~R9;8#apJl?ohE*QJ5 zVU&rxCbau<=MP9?)K*E)wP^VxDcc@W?tlO3d8=cANTm>1CR5sEI(4Lro2|Z-7RsBI z=UIHl)cR{m3pwvKc~;jgqk0$tZ+wfdkL}K&vpxV{S{j$>S%(yl`Dfe139S;-7>j>N zivXQ$@s7B9jz3F@dpFScTgjJHNU>6ter0Dv^l@`_75tN=nOqqHS2Z%)w@{vQ{}P$; zOs`e@w)ie{-6A@uHt(=`?#x4n%swS?%yA&J^vxJOVW`rgg$~>$Xqq-O+?<1ve4^Z6 zXyY7FJ2b`ny}#qD*b|n$_hIuaIcIrMD}1Q26))8*BM9d8$;j&S|D1HluiKJ~AVhI5 zl569vG*Luj%zR@hYaBwp`{#+#_*5$As*x=2LMpzM;RxD=2Ha>>J`u!}*FfvQ*_wYf zbtcu0tZI!jz**Jp|CFQMAv^q0xb+zc@=w zWM74*Q+0f6ZK5uD+kZRBqT$^a`4Z9LstocYw_ZD289V!8NNgMT%C(ujD;9{eaonwm zuu%3^>zMW@3}e!0VvoySy!umJjPg@<&^uSLLo>>dsJ=CYJnO+2#i8Ug*+Q&T!3GSh zLVx{7Hi-cu;Zn^4v1bu%#NmkC5x~GZ7f21{M^;mc_V{SOf_IXmgkB93vp~4>!NX`Y zqLqH@zzp>z;pF@*(hmFEg`h=~o&=V8aWSQFMHq45Td>WcvQ5+r1w*UFP`3VSHU!aZ z^`Yd?id(lENB-{ju?U(bD7~#HbF6=vS~1+>&m|2gp>5)ey}dIILVFPv;LP039M2%3` z4I*CAZrL%Lm72A^oQWScT7LR0q8+gP*vMKBTh*y1MR3JR+0x8A;(sFAC!3!@qb@_>-f_Z+n1i`rjfZ3mpkzNJLJ9!QKOtYgar;fvP2pIinItR6 z1$kZu&ue@_C2Ven27g1xytULC;9E3zxe(Givc`qId5Q6u;;fl{ja6YrltkgMB29c* zh0O~`0;MK^g7rVFA_o_Fu170m4*3w$>6jJC8_&0Uk%Y+4gcYb@VEE!)PFIK6r5XYE@i6UCZVLkIn-uFfqclf~AU*Av?z!S5gKA47v zN8|WBFVK@9$9@#cVgalG2WI(?Q!cG7J?8gW3`S1IXpfZf+4Z_L(TBPv0O^9~>b$k1q#9NB@M;>Nxz99%D)?Xq7`ZY;u+XgYgXEw+XbZ8PS8zUbVe+)kZ z;ox!FYL3gS*=jftW=n|C5}K8-17X?-!{W0sjf=|ZmFM(4ylrhZ1w~c<+2cg_nH@p%0hw9f-hb$h zFJRZ`nD*8U2&GBq*vkFa7Yh2&9%es*zX^-TfNm`5KV6$m4o@HY~Q-FT-ir`gLAM~0&oA)!h|1mT}T#VvDulVRT);9LVy>0OJ{ZCBcW+; z1Z%xF<55+}utumc5ZZYi!F9C5GKb(N`xg{X2j|L7>bRjUm323A<2f2cW7-)CB?;dY z1rggli*KK#Bk3Wc``M1$j<)H%_PFQTJesG210xzS*g0xlvS5SLN6Fe^>7RNh# zIIl{FS^%|2fBbNM2M5(j=e%f^{3)6)ij2JH?#2yPe6?W)DimH0ILlw6y{)eoJu8h- zF)+R!OJ^3nt!GAzOc3zTYib4<0m)dju!`|%BOY-J#fPBNELiP{>rik!lfyy@Iu(BV zr2VKb;H#wIfZC)MEgZiWT@=KvS=HhzvJUp8PiFlxfZFDul$_U+^K19fecgD`F;FAElfY9LQFZ1B5^AjkB*ez{XtXrdwfEU9Y zjrdA?m?SIg6P>g5#+D~>Hit2?mIlSPdk(kfc3)`Hsw>aG462Fv8a2iqscVC4cZcT2DiK@CGfL!i#X)x?eq6 zM{mpt?V$nZ2Xs^OAGK1nS;l+IeM6v7SnwRf{1(r0IuQ$pMZ5=8X1=eEf2*Q^F5$$o zsV)1{{lT)#Q-ck4x9zKsDKYkRGKc_P1f}(&uT`vihsFQ3-t^`fZ!gnE7EY6dDaGI} zzUZT}my*)AbVTo_BlN{@u~OQQoE}}aN>z!h55VQuVZA`D#H+#H_8M@6hi&?XRB Z!sS@@rz-CYVy20ts;H$7RaEDT?!M#{;NFd3d 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 H=HLDY%B-8p literal 0 HcmV?d00001 diff --git a/tmux/.tmux/plugins/tmux-battery/screenshots/battery_discharging_tier4.png b/tmux/.tmux/plugins/tmux-battery/screenshots/battery_discharging_tier4.png new file mode 100644 index 0000000000000000000000000000000000000000..9228b9383dc9fe6832b8a00f4af2b2b8e2a10d35 GIT binary patch literal 8247 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 Sfcg^vO-)H#v0nbem;V9xNrZ|3 literal 0 HcmV?d00001 diff --git a/tmux/.tmux/plugins/tmux-battery/screenshots/battery_discharging_tier5.png b/tmux/.tmux/plugins/tmux-battery/screenshots/battery_discharging_tier5.png new file mode 100644 index 0000000000000000000000000000000000000000..9ecfd53f505d03e31e45e1719f37955787bb47c6 GIT binary patch literal 8096 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 literal 0 HcmV?d00001 diff --git a/tmux/.tmux/plugins/tmux-battery/screenshots/battery_discharging_tier7.png b/tmux/.tmux/plugins/tmux-battery/screenshots/battery_discharging_tier7.png new file mode 100644 index 0000000000000000000000000000000000000000..ae4a887656fe3e470f37893c8f1827e41e73f64d GIT binary patch literal 8253 zcmZ8nbx<2`w57Dg-3yf9?oNSFihFU27WbmX-QC>+6(~}IyR^6m*8;_g1&08EgqPpU zoA<}NGrRlE>Nm4{?>YCJjeoDHgpWgwgNBBNuc9okgNF7ZA2lY%!a%Js}Ute9$otdQN$5V%ca1B3Wzx zfzla6Y-$a2?M-GnH*-cOA`ymoR%+4<{Uk<#i(g&r7r#ujrHIJMyTs8WaK}$4rh-3)V`yv_s{MeDE$9Ic2oBB1c&PX$93pL zx7oAj{~M7h-+am&*v3B6+2QT=AL|B3rUe&X^3=}djNzE9Br=^lkT-u$3-x4(NXx^7UWU{!+8uO4}$M+ zNQC&#n81(Ex}-jHGV!*BY3IT+&@i#~NZAA(_f%=+Vy4?dq`{z#ZuINT(_yLkB{ zlkpSBuBCu_ki)QlOk->HY{elM#uF3s}qPQzHDabA0-PEQoKv&ujJEJ=EWZN z#w@{=?!c)sHi$mbtpr(YGLgS#r~S+X^`jEyIXo$#3VMC0fj9}>Bi{htn)WodItLPw z7xwn*249Y2E>fS>1!dx0v60Sb*DSLqa`XlO9|5={*<8vK~AAJt<|W+wYIaBW_=+ z1QoqibIgk^yqLK9C_TFhFC0+9E(qBiD8_Xu73Rb!uN>0b z`%h`|0q(YGcX08skeBVILlWaFM_4*X!?7{Ignr*d!eb*YeP8|XR~aCJn!x?Hv$1>)C7(A^#oK}yC4*=#6dE^s37Kmp zc%`nSe^_QuZ_3?$x)eA7N2V1a8jCwK%fD+F5w_}t1WSwoU!L8V?>b%75tc>^tMYRF zmYPuaRLVe|p-i`xgtGw{kXVcV96`SM8R(+47QhnKvfWB!Z3n6HyUVqxAR8q5<)wVhaWk?h~Ri>x* znLe--2^IgC(?h*Q9A2^99(8?I4DgsK&oSz{;D)?XSAFAyEus`%T5(Mew2z(z?je%v zXio20+_l06FtPc=5%Q8H_jJtFJu%ano4$!{;am^o86cLK3~F1$Y|G-03i#D8PVv(2 z*q>475eRCx)Zyvsmsdaniz#)J<7c^WEC}|W$V#XP)4`=aZ)igampK)TZN$1lh=sfG zzweaV;3?bX!NYnQDx_a5Wd0VRE$5E3c8ebJ-CFQc=${Muy_PXo<*)SmK+v zc8>*i-G**YJB#W9K5xJeS?&%iq{Kp`X23mG)7OXUUHf!3 z+3&(;);m|+XB7N4LkcrIcHC26McRl89fo6iXS;H6?JVz&R|4;C(rn*Z1pPMg+9$cc zR5SgmC~(rg&o4o{Tva9RdTfB^vk@u-`s0a<#>Cf^R&QvL>@|QzV$K(60RFcVjjL@X zV0Nz+utyH7F&|^9svoVU8L87nh*fLZ9gF>2lj=Xd%9NlV zU64_*NFp(RtypQ8CN0=9+LDC)c~PJ^AvekXKsFzj;Qu3kc3e#eV;WV?&ff8j{#4

TlN<~tSwQKDzjp%C1P#hcm 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 literal 0 HcmV?d00001 diff --git a/tmux/.tmux/plugins/tmux-battery/screenshots/battery_status_attached.png b/tmux/.tmux/plugins/tmux-battery/screenshots/battery_status_attached.png new file mode 100644 index 0000000000000000000000000000000000000000..463f3c64a442e6170f0eb9ed2afc307e13e5d406 GIT binary patch literal 7534 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 literal 0 HcmV?d00001 diff --git a/tmux/.tmux/plugins/tmux-battery/screenshots/battery_status_unknown.png b/tmux/.tmux/plugins/tmux-battery/screenshots/battery_status_unknown.png new file mode 100644 index 0000000000000000000000000000000000000000..95347914f846fce80125038e2dff07ff9225ea7f GIT binary patch literal 7299 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/dev/null 2>&1; then + if $short; then + echo '~?:??' + else + echo '- Calculating estimate...' + fi + else + local remaining_time="$(echo $status | grep -o '[0-9]\{1,2\}:[0-9]\{1,2\}')" + if battery_discharging; then + if $short; then + echo $remaining_time | awk '{printf "~%s", $1}' + else + echo $remaining_time | awk '{printf "- %s left", $1}' + fi + elif battery_charged; then + if $short; then + echo $remaining_time | awk '{printf "charged", $1}' + else + echo $remaining_time | awk '{printf "fully charged", $1}' + fi + else + if $short; then + echo $remaining_time | awk '{printf "~%s", $1}' + else + echo $remaining_time | awk '{printf "- %s till full", $1}' + fi + fi + fi +} + +upower_battery_remaining_time() { + battery=$(upower -e | grep -E 'battery|DisplayDevice'| tail -n1) + if battery_discharging; then + local remaining_time + remaining_time=$(upower -i "$battery" | grep -E '(remain|time to empty)') + if $short; then + echo "$remaining_time" | awk '{printf "%s %s", $(NF-1), $(NF)}' + else + echo "$remaining_time" | awk '{printf "%s %s left", $(NF-1), $(NF)}' + fi + elif battery_charged; then + if $short; then + echo "" + else + echo "charged" + fi + else + local remaining_time + remaining_time=$(upower -i "$battery" | grep -E 'time to full') + if $short; then + echo "$remaining_time" | awk '{printf "%s %s", $(NF-1), $(NF)}' + else + echo "$remaining_time" | awk '{printf "%s %s to full", $(NF-1), $(NF)}' + fi + fi +} + +acpi_battery_remaining_time() { + acpi -b | grep -m 1 -Eo "[0-9]+:[0-9]+:[0-9]+" +} + +print_battery_remain() { + if is_wsl; then + echo "?" # currently unsupported on WSL + elif command_exists "pmset"; then + pmset_battery_remaining_time + elif command_exists "acpi"; then + acpi_battery_remaining_time + elif command_exists "upower"; then + upower_battery_remaining_time + elif command_exists "apm"; then + apm_battery_remaining_time + fi +} + +main() { + get_remain_settings + print_battery_remain +} +main diff --git a/tmux/.tmux/plugins/tmux-battery/scripts/battery_status_bg.sh b/tmux/.tmux/plugins/tmux-battery/scripts/battery_status_bg.sh new file mode 100755 index 0000000..42d1626 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-battery/scripts/battery_status_bg.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source "$CURRENT_DIR/helpers.sh" + +color_full_charge_default="#[bg=green]" +color_high_charge_default="#[bg=yellow]" +color_medium_charge_default="#[bg=colour208]" # orange +color_low_charge_default="#[bg=red]" +color_charging_default="#[bg=green]" + +color_full_charge="" +color_high_charge="" +color_medium_charge="" +color_low_charge="" +color_charging="" + +get_charge_color_settings() { + color_full_charge=$(get_tmux_option "@batt_color_full_charge" "$color_full_charge_default") + color_high_charge=$(get_tmux_option "@batt_color_high_charge" "$color_high_charge_default") + color_medium_charge=$(get_tmux_option "@batt_color_medium_charge" "$color_medium_charge_default") + color_low_charge=$(get_tmux_option "@batt_color_low_charge" "$color_low_charge_default") + color_charging=$(get_tmux_option "@batt_color_charging" "$color_charging_default") +} + +print_battery_status_bg() { + # Call `battery_percentage.sh`. + percentage=$($CURRENT_DIR/battery_percentage.sh | sed -e 's/%//') + status=$(battery_status | awk '{print $1;}') + if [ $status == 'charging' ]; then + printf $color_charging + elif [ $percentage -eq 100 ]; then + printf $color_full_charge + elif [ $percentage -le 99 -a $percentage -ge 51 ];then + printf $color_high_charge + elif [ $percentage -le 50 -a $percentage -ge 16 ];then + printf $color_medium_charge + elif [ "$percentage" == "" ];then + printf $color_full_charge_default # assume it's a desktop + else + printf $color_low_charge + fi +} + +main() { + get_charge_color_settings + print_battery_status_bg +} +main diff --git a/tmux/.tmux/plugins/tmux-battery/scripts/battery_status_fg.sh b/tmux/.tmux/plugins/tmux-battery/scripts/battery_status_fg.sh new file mode 100755 index 0000000..2418b8c --- /dev/null +++ b/tmux/.tmux/plugins/tmux-battery/scripts/battery_status_fg.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source "$CURRENT_DIR/helpers.sh" + +color_full_charge_default="#[fg=green]" +color_high_charge_default="#[fg=yellow]" +color_medium_charge_default="#[fg=colour208]" # orange +color_low_charge_default="#[fg=red]" +color_charging_default="#[fg=green]" + +color_full_charge="" +color_high_charge="" +color_medium_charge="" +color_low_charge="" +color_charging="" + +get_charge_color_settings() { + color_full_charge=$(get_tmux_option "@batt_color_full_charge" "$color_full_charge_default") + color_high_charge=$(get_tmux_option "@batt_color_high_charge" "$color_high_charge_default") + color_medium_charge=$(get_tmux_option "@batt_color_medium_charge" "$color_medium_charge_default") + color_low_charge=$(get_tmux_option "@batt_color_low_charge" "$color_low_charge_default") + color_charging=$(get_tmux_option "@batt_color_charging" "$color_charging_default") +} + +print_battery_status_fg() { + # Call `battery_percentage.sh`. + percentage=$($CURRENT_DIR/battery_percentage.sh | sed -e 's/%//') + status=$(battery_status | awk '{print $1;}') + if [ $status == 'charging' ]; then + printf $color_charging + elif [ $percentage -eq 100 ]; then + printf $color_full_charge + elif [ $percentage -le 99 -a $percentage -ge 51 ];then + printf $color_high_charge + elif [ $percentage -le 50 -a $percentage -ge 16 ];then + printf $color_medium_charge + elif [ "$percentage" == "" ];then + printf $color_full_charge_default # assume it's a desktop + else + printf $color_low_charge + fi +} + +main() { + get_charge_color_settings + print_battery_status_fg +} +main diff --git a/tmux/.tmux/plugins/tmux-battery/scripts/helpers.sh b/tmux/.tmux/plugins/tmux-battery/scripts/helpers.sh new file mode 100644 index 0000000..270e688 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-battery/scripts/helpers.sh @@ -0,0 +1,63 @@ +get_tmux_option() { + local option="$1" + local default_value="$2" + local option_value="$(tmux show-option -gqv "$option")" + if [ -z "$option_value" ]; then + echo "$default_value" + else + echo "$option_value" + fi +} + +is_osx() { + [ $(uname) == "Darwin" ] +} + +is_chrome() { + chrome="/sys/class/chromeos/cros_ec" + if [ -d "$chrome" ]; then + return 0 + else + return 1 + fi +} + +is_wsl() { + version=$(/dev/null 2>&1 +} + +battery_status() { + if is_wsl; then + local battery + battery=$(find /sys/class/power_supply/*/status | tail -n1) + awk '{print tolower($0);}' "$battery" + elif command_exists "pmset"; then + pmset -g batt | awk -F '; *' 'NR==2 { print $2 }' + elif command_exists "acpi"; then + acpi -b | awk '{gsub(/,/, ""); print tolower($3); exit}' + elif command_exists "upower"; then + local battery + battery=$(upower -e | grep -E 'battery|DisplayDevice'| tail -n1) + upower -i $battery | awk '/state/ {print $2}' + elif command_exists "termux-battery-status"; then + termux-battery-status | jq -r '.status' | awk '{printf("%s%", tolower($1))}' + elif command_exists "apm"; then + local battery + battery=$(apm -a) + if [ $battery -eq 0 ]; then + echo "discharging" + elif [ $battery -eq 1 ]; then + echo "charging" + fi + fi +} diff --git a/tmux/.tmux/plugins/tmux-cowboy/LICENSE.md b/tmux/.tmux/plugins/tmux-cowboy/LICENSE.md new file mode 100644 index 0000000..e6e7350 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-cowboy/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (C) Bruno Sutic + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tmux/.tmux/plugins/tmux-cowboy/README.md b/tmux/.tmux/plugins/tmux-cowboy/README.md new file mode 100644 index 0000000..1f39d39 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-cowboy/README.md @@ -0,0 +1,24 @@ +# tmux-cowboy + +~~Just kill that damned stale process!~~ Send a signal to a process running +inside a current pane. + +Useful when you're annoyed by the stale program and just want to get rid of it. + +NOTE: this plugin calls a `kill -9 ` command and that's potentially +dangerous. Use this plugin at your own responsibility. That said, I'm using +this on my personal computer. If there are bugs I'll be the first to know. + +### Key bindings + +- prefix * - end the process running in the current + pane with `kill -9` + +### FAQ + +Q: What's with the name? Why "cowboy"?
+A: Because you go pew-pew killing those bad processes. + +### License + +[MIT](LICENSE.md) diff --git a/tmux/.tmux/plugins/tmux-cowboy/cowboy.tmux b/tmux/.tmux/plugins/tmux-cowboy/cowboy.tmux new file mode 100755 index 0000000..94de13f --- /dev/null +++ b/tmux/.tmux/plugins/tmux-cowboy/cowboy.tmux @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPTS_DIR="${CURRENT_DIR}/scripts" + +main() { + tmux bind-key "*" run-shell "$SCRIPTS_DIR/kill.sh KILL" +} +main diff --git a/tmux/.tmux/plugins/tmux-cowboy/scripts/kill.sh b/tmux/.tmux/plugins/tmux-cowboy/scripts/kill.sh new file mode 100755 index 0000000..65b23a6 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-cowboy/scripts/kill.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +SIGNAL="${1:-KILL}" + +pane_pid() { + tmux display-message -p "#{pane_pid}" +} + +pid() { + local pane_pid="$(pane_pid)" + + ps -ao "ppid pid" | + sed "s/^ *//" | + grep "^${pane_pid}" | + cut -d' ' -f2- | + head -n 1 +} + +main() { + local pid="$(pid)" + + if [ -n "$pid" ]; then + kill -${SIGNAL} $pid + fi +} +main diff --git a/tmux/.tmux/plugins/tmux-cpu/.editorconfig b/tmux/.tmux/plugins/tmux-cpu/.editorconfig new file mode 100644 index 0000000..94dcf5a --- /dev/null +++ b/tmux/.tmux/plugins/tmux-cpu/.editorconfig @@ -0,0 +1,16 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true + +# 2 space indentation +[*.{sh,tmux}] +indent_style = space +indent_size = 2 diff --git a/tmux/.tmux/plugins/tmux-cpu/.mailmap b/tmux/.tmux/plugins/tmux-cpu/.mailmap new file mode 100644 index 0000000..c71f097 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-cpu/.mailmap @@ -0,0 +1,2 @@ +Camille TJHOA +Yuichi Kiri diff --git a/tmux/.tmux/plugins/tmux-cpu/LICENSE b/tmux/.tmux/plugins/tmux-cpu/LICENSE new file mode 100644 index 0000000..7d4f87d --- /dev/null +++ b/tmux/.tmux/plugins/tmux-cpu/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014 ctjhoa + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/tmux/.tmux/plugins/tmux-cpu/README.md b/tmux/.tmux/plugins/tmux-cpu/README.md new file mode 100644 index 0000000..51f627c --- /dev/null +++ b/tmux/.tmux/plugins/tmux-cpu/README.md @@ -0,0 +1,170 @@ +# Tmux CPU and GPU status + +Enables displaying CPU and GPU information in Tmux `status-right` and `status-left`. +Configurable percentage and icon display. + +## Installation +### Installation with [Tmux Plugin Manager](https://github.com/tmux-plugins/tpm) (recommended) + +Add plugin to the list of TPM plugins in `.tmux.conf`: + +```shell +set -g @plugin 'tmux-plugins/tmux-cpu' +``` + +Hit `prefix + I` to fetch the plugin and source it. + +If format strings are added to `status-right`, they should now be visible. + +### Manual Installation + +Clone the repo: + +```shell +$ git clone https://github.com/tmux-plugins/tmux-cpu ~/clone/path +``` + +Add this line to the bottom of `.tmux.conf`: + +```shell +run-shell ~/clone/path/cpu.tmux +``` + +Reload TMUX environment: + +```shell +# type this in terminal +$ tmux source-file ~/.tmux.conf +``` + +If format strings are added to `status-right`, they should now be visible. + +### Optional requirements (Linux, BSD, OSX) + +- `iostat` or `sar` are the best way to get an accurate CPU percentage. +A fallback is included using `ps -aux` but could be inaccurate. +- `free` is used for obtaining system RAM status. +- `lm-sensors` is used for CPU temperature. +- `nvidia-smi` is required for GPU information. +For OSX, `cuda-smi` is required instead (but only shows GPU memory use rather than load). +If "No GPU" is displayed, it means the script was not able to find `nvidia-smi`/`cuda-smi`. +Please make sure the appropriate command is installed and in the `$PATH`. + +## Usage + +Add any of the supported format strings (see below) to the existing `status-right` tmux option. +Example: + +```shell +# in .tmux.conf +set -g status-right '#{cpu_bg_color} CPU: #{cpu_icon} #{cpu_percentage} | %a %h-%d %H:%M ' +``` + +### Supported Options + +This is done by introducing 12 new format strings that can be added to +`status-right` option: + +- `#{cpu_icon}` - will display a CPU status icon +- `#{cpu_percentage}` - will show CPU percentage (averaged across cores) +- `#{cpu_bg_color}` - will change the background color based on the CPU percentage +- `#{cpu_fg_color}` - will change the foreground color based on the CPU percentage +- `#{ram_icon}` - will display a RAM status icon +- `#{ram_percentage}` - will show RAM percentage (averaged across cores) +- `#{ram_bg_color}` - will change the background color based on the RAM percentage +- `#{ram_fg_color}` - will change the foreground color based on the RAM percentage +- `#{cpu_temp_icon}` - will display a CPU temperature status icon +- `#{cpu_temp}` - will show CPU temperature (averaged across cores) +- `#{cpu_temp_bg_color}` - will change the background color based on the CPU temperature +- `#{cpu_temp_fg_color}` - will change the foreground color based on the CPU temperature + +GPU equivalents also exist: + +- `#{gpu_icon}` - will display a GPU status icon +- `#{gpu_percentage}` - will show GPU percentage (averaged across devices) +- `#{gpu_bg_color}` - will change the background color based on the GPU percentage +- `#{gpu_fg_color}` - will change the foreground color based on the GPU percentage +- `#{gram_icon}` - will display a GPU RAM status icon +- `#{gram_percentage}` - will show GPU RAM percentage (total across devices) +- `#{gram_bg_color}` - will change the background color based on the GPU RAM percentage +- `#{gram_fg_color}` - will change the foreground color based on the GPU RAM percentage +- `#{gpu_temp_icon}` - will display a GPU temperature status icon +- `#{gpu_temp}` - will show GPU temperature (average across devices) +- `#{gpu_temp_bg_color}` - will change the background color based on the GPU temperature +- `#{gpu_temp_fg_color}` - will change the foreground color based on the GPU temperature + +## Examples + +CPU usage lower than 30%:
+![low_fg](/screenshots/low_fg.png) +![low_bg](/screenshots/low_bg.png) +![low_icon](/screenshots/low_icon.png) + +CPU usage between 30% and 80%:
+![medium_fg](/screenshots/medium_fg.png) +![medium_bg](/screenshots/medium_bg.png) +![medium_icon](/screenshots/medium_icon.png) + +CPU usage higher than 80%:
+![high_fg](/screenshots/high_fg.png) +![high_bg](/screenshots/high_bg.png) +![high_icon](/screenshots/high_icon.png) + +## Customization + +Here are all available options with their default values: + +```shell +@cpu_low_icon "=" # icon when cpu is low +@cpu_medium_icon "≡" # icon when cpu is medium +@cpu_high_icon "≣" # icon when cpu is high + +@cpu_low_fg_color "" # foreground color when cpu is low +@cpu_medium_fg_color "" # foreground color when cpu is medium +@cpu_high_fg_color "" # foreground color when cpu is high + +@cpu_low_bg_color "#[bg=green]" # background color when cpu is low +@cpu_medium_bg_color "#[bg=yellow]" # background color when cpu is medium +@cpu_high_bg_color "#[bg=red]" # background color when cpu is high + +@cpu_percentage_format "%3.1f%%" # printf format to use to display percentage + +@cpu_medium_thresh "30" # medium percentage threshold +@cpu_high_thresh "80" # high percentage threshold + +@ram_(low_icon,high_bg_color,etc...) # same defaults as above + +@cpu_temp_format "%2.0f" # printf format to use to display temperature +@cpu_temp_unit "C" # supports C & F + +@cpu_temp_medium_thresh "80" # medium temperature threshold +@cpu_temp_high_thresh "90" # high temperature threshold + +@cpu_temp_(low_icon,high_bg_color,etc...) # same defaults as above +``` + +All `@cpu_*` options are valid with `@gpu_*` (except `@cpu_*_thresh` which apply to both CPU and GPU). Additionally, `@ram_*` options become `@gram_*` for GPU equivalents. + +Note that these colors depend on your terminal / X11 config. + +You can can customize each one of these options in your `.tmux.conf`, for example: + +```shell +set -g @cpu_low_fg_color "#[fg=#00ff00]" +set -g @cpu_percentage_format "%5.1f%%" # Add left padding +``` + +Don't forget to reload the tmux environment (`$ tmux source-file ~/.tmux.conf`) after you do this. + +### Tmux Plugins + +This plugin is part of the [tmux-plugins](https://github.com/tmux-plugins) organisation. Checkout plugins as [battery](https://github.com/tmux-plugins/tmux-battery), [logging](https://github.com/tmux-plugins/tmux-logging), [online status](https://github.com/tmux-plugins/tmux-online-status), and many more over at the [tmux-plugins](https://github.com/tmux-plugins) organisation page. + +### Maintainers + +- [Camille Tjhoa](https://github.com/ctjhoa) +- [Casper da Costa-Luis](https://github.com/casperdcl) + +### License + +[MIT](LICENSE.md) diff --git a/tmux/.tmux/plugins/tmux-cpu/cpu.tmux b/tmux/.tmux/plugins/tmux-cpu/cpu.tmux new file mode 100755 index 0000000..576c206 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-cpu/cpu.tmux @@ -0,0 +1,85 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source "$CURRENT_DIR/scripts/helpers.sh" + +cpu_interpolation=( + "\#{cpu_percentage}" + "\#{cpu_icon}" + "\#{cpu_bg_color}" + "\#{cpu_fg_color}" + "\#{gpu_percentage}" + "\#{gpu_icon}" + "\#{gpu_bg_color}" + "\#{gpu_fg_color}" + "\#{ram_percentage}" + "\#{ram_icon}" + "\#{ram_bg_color}" + "\#{ram_fg_color}" + "\#{gram_percentage}" + "\#{gram_icon}" + "\#{gram_bg_color}" + "\#{gram_fg_color}" + "\#{cpu_temp}" + "\#{cpu_temp_icon}" + "\#{cpu_temp_bg_color}" + "\#{cpu_temp_fg_color}" + "\#{gpu_temp}" + "\#{gpu_temp_icon}" + "\#{gpu_temp_bg_color}" + "\#{gpu_temp_fg_color}" +) +cpu_commands=( + "#($CURRENT_DIR/scripts/cpu_percentage.sh)" + "#($CURRENT_DIR/scripts/cpu_icon.sh)" + "#($CURRENT_DIR/scripts/cpu_bg_color.sh)" + "#($CURRENT_DIR/scripts/cpu_fg_color.sh)" + "#($CURRENT_DIR/scripts/gpu_percentage.sh)" + "#($CURRENT_DIR/scripts/gpu_icon.sh)" + "#($CURRENT_DIR/scripts/gpu_bg_color.sh)" + "#($CURRENT_DIR/scripts/gpu_fg_color.sh)" + "#($CURRENT_DIR/scripts/ram_percentage.sh)" + "#($CURRENT_DIR/scripts/ram_icon.sh)" + "#($CURRENT_DIR/scripts/ram_bg_color.sh)" + "#($CURRENT_DIR/scripts/ram_fg_color.sh)" + "#($CURRENT_DIR/scripts/gram_percentage.sh)" + "#($CURRENT_DIR/scripts/gram_icon.sh)" + "#($CURRENT_DIR/scripts/gram_bg_color.sh)" + "#($CURRENT_DIR/scripts/gram_fg_color.sh)" + "#($CURRENT_DIR/scripts/cpu_temp.sh)" + "#($CURRENT_DIR/scripts/cpu_temp_icon.sh)" + "#($CURRENT_DIR/scripts/cpu_temp_bg_color.sh)" + "#($CURRENT_DIR/scripts/cpu_temp_fg_color.sh)" + "#($CURRENT_DIR/scripts/gpu_temp.sh)" + "#($CURRENT_DIR/scripts/gpu_temp_icon.sh)" + "#($CURRENT_DIR/scripts/gpu_temp_bg_color.sh)" + "#($CURRENT_DIR/scripts/gpu_temp_fg_color.sh)" +) + +set_tmux_option() { + local option=$1 + local value=$2 + tmux set-option -gq "$option" "$value" +} + +do_interpolation() { + local all_interpolated="$1" + for ((i=0; i<${#cpu_commands[@]}; i++)); do + all_interpolated=${all_interpolated//${cpu_interpolation[$i]}/${cpu_commands[$i]}} + done + echo "$all_interpolated" +} + +update_tmux_option() { + local option=$1 + local option_value=$(get_tmux_option "$option") + local new_option_value=$(do_interpolation "$option_value") + set_tmux_option "$option" "$new_option_value" +} + +main() { + update_tmux_option "status-right" + update_tmux_option "status-left" +} +main diff --git a/tmux/.tmux/plugins/tmux-cpu/screenshots/high_bg.png b/tmux/.tmux/plugins/tmux-cpu/screenshots/high_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..7426449d70be74cc4cabc11dcbb353450bfc189d GIT binary patch literal 992 zcmeAS@N?(olHy`uVBq!ia0vp^%|NWn!2~3kUiz;GQq09po*^6@9Je3(KLGM(c)B=- zRK&fV?d=f~C~^F}W%2RK8!v2Rj$iMYCG}#FgGs?fr6!Jrp?XaVGK9Bqnoo6cypbL3 zw6#m_f>MWAu8>kp+w2n@+TCvqr^S_iIp@6B^4uNE?K2IR1-_f;BYyvi`Rdd4`+lD- z|G)pUeY%9itFqmk`xq<^Fq~;nPLXjC3b@8N2}sH`7_p}@c{&s_oRXQrsls@aQKL9P zZ32r3`+_r#GYKkaaB6xLp!W0r^8-E(k>pT%4Sz6kg9)e)b{u+s?MP*93awWcl8m zd%O9}w;S*G$7XfE?3}y#ak~2R)6-^mmt1t0zN)TsPTF*zVrPEu30uXhio44HRxzeG zeRSEa6hE^^biKvv8>>(KvU~0H{=|m^87H2seE)SuVngJxt_ZF zVL^W{xVf7DZ;d-JvtY$Uy{Rf*u{}>Lox11mT2fZI%lGn1F*_+e`)5VxG>q-Qn(bsf zozEAX-*lcyt;J~ShpDR-uTMF}cir3gWO-!r9!;6tf2GCiid@g;Z!K8A9;7)45}9@{|hJUH%{LUfjWd@!P4(%BH>Tt16p~F3#QbJ-mudcV%pFQTm-7 z6O}$*^Vp*H-goJmhl{co`d<#(L7NnYXo^4YhQ4Bt!rGUCeGfBVWM(SlW$Mu+Sqd0vKh zEw$GVb?NAR?Rl)nzcXgL{SJ)>J}TaSeoNV0eB(Ut-n*n(MW5ep=bO9zRQ&G0jmEn7 zENw5EN!u(}*>u`>iT-St#jdW8HK%>=O?z7vBsJG6K*-|G8K09kIt@c#EKaF^Hqq;| z$BO7PHTN2L{=G{{IHl&b>*TjPnc-n&Ya%$xb=LX`PP*Z8+x*I^1?M|?Z7sLV*8I1d zn?vXfi-5q7_j(!%6IdoOJZ6~U_+bu4`qogXW6D+D6PWv?#sQdR89ZJ6T-G@yGywn? C`ppUe literal 0 HcmV?d00001 diff --git a/tmux/.tmux/plugins/tmux-cpu/screenshots/high_fg.png b/tmux/.tmux/plugins/tmux-cpu/screenshots/high_fg.png new file mode 100644 index 0000000000000000000000000000000000000000..29080247d3aded787c6896999ddfe8301407dd80 GIT binary patch literal 919 zcmV;I18Dq-P)2%*~u&2t-61LAz-O5;dT8^D+ zcI6{eJrQN~Y;ZbmM9uiz>G`0+wf|Cc)1SoOixR>m6YJG*T$1AlILeraL8@c`lsX$% z{O&qG3lJ;36@qV~@mjGqh|_T+YQ_tE!PZ4*Be3?n66HC>q_nMq+{svnLgh-ju#g_( z9ixnP<7$A@>`W^eU*7o3j1GtAJ6Z9)*-S+&;nf_kiIB3=`B<8>{&(D=Cis6ZeaqDf7)wHWNf6Y<1ScK3*(GpPFqmizp z%+NGcmBLQN=(zTn_^gS4v9I}#M_7|zjwd>NTC*=7KR({YpG;$@4o*9R1evc{)h=ENHZZG3R@afrK8Z`l%h)o~m$B4|)= z6^Mn+S9aDxu^#8U8h4w>cX<*4Ep?4b?xA~}@4IyN#e8|T(RV(X~X3I_uV{g^BT zBnBArQ2P$IUTxDKs^K`pa^TZM#Ibr#&ws}uD|R2*Jp|}s_nyAM?6Z+j4KVthz9*s) zsv$VNI1_SYvmOSPT=0$Ksf45c!}4tZD9jt|ue@C8bF++6E`27FPn1a9ibC7ULQ21Y zwzf9x;!ausdFf<4EtZeGXyu1Y+MBW(j{7h8BgXPgRG;dbJhX9cGI=M50sZwL+2z+A tN5)NmJxIt+2d9H0B|w;`-{T0u{R212yEF%(a-RSI002ovPDHLkV1ntkwY&fT literal 0 HcmV?d00001 diff --git a/tmux/.tmux/plugins/tmux-cpu/screenshots/high_icon.png b/tmux/.tmux/plugins/tmux-cpu/screenshots/high_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c5961ca16840632259950b1741deb6c02d1696b1 GIT binary patch literal 1069 zcmV+|1k(G7P)Mb?>w+~=(OMA{A%fymR6r1cqFrWj3J4vY%Z(UFjAve&od3+3 zbHDrFJDto>27>{?fhR!YB~U^H01&|k00;mP2oPj&5C9?&Ajse#07M`_kikI!h(LfK zgM$DNfdD}U2LT`g0fGz;0zd=;1Q{FzfCvN#GHWuK2r2)c%yC@*pkav-<{tnN0I>vp zsB*-#tei!&;^Sh+P%p0RI{%vgSx9TN;oU6oMR_B#^BwQTVfLcBiJu7Qv z=9E!fK<^^Q4f&qR^f;lav<=wOTprp}hHACBs4ltoGPakfKZ^jTXEHq1r+cHkw(ijJCE&r zjF8Sej|z4MWaQb_S<*f|Eh~N80ZGCy zOKwB~N!_2n_{K)Z=+MXS@=IOA#2D0C*DfMlTzy#bCWEQNH@h4c%jM;yOWv3n={k3) zF49(*o6emsYf{^;8KbRWRf=+x7T(=Ee71HZmKa4d3>}&tDl|_F5=CB=c&_@Tcm0CQ zg!_um+T;1yth$0eqrIpBnJHg6RE*~ z7T(`Idg8uvg(2RrTw;{fYQ>!YS7B!Q+U=X?_HTfa*L7jf)dsnJZJZ@9-4BTOGb$Ywj7_IqhQYUG}j+%Q{9dARE@cY3gu2H|u zUwb)lqaQ9nzqj)bMc`TN4<-wq#R#w{_`+;(Lg1jm`M?)u1U>)=0Fej~bl8Ic5P<+e n1_uEk0s(>y4gx>~0tER78?^CVho8ra00000NkvXXu0mjfh&Jo$ literal 0 HcmV?d00001 diff --git a/tmux/.tmux/plugins/tmux-cpu/screenshots/low_bg.png b/tmux/.tmux/plugins/tmux-cpu/screenshots/low_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..6c3dc289635d78f246689ff48ea40e718aad28c8 GIT binary patch literal 908 zcmV;719SX|P)RI$q5X5>CMNmO6J$SHCgy!H2q!*=#VDW+JD@nWBn$2_5Y?JJ+8&cCI-AoCD6x!L- z?)RVh&&==pW_A}c3=;?h0KT*cK%VD)KA)C^V}QU65CkwC1RPvDC^(o7rh_Da67ji% z>7WRpG+aBF4vGLu!?lCypa`Her~m~j5Y=QGYj|g&l8@=2=#VI}ewuFF<#}p1|1i)> zeCGXZoc?dUWONLATv*-&t>Ih@iZ5{n3n2yP9Wz}KRUpe z6?(Yf)r3^$Nmw3v-2ovq@^#|LqB7!bc|~n7=6WB6wBK)rmnMHc4`)8?c23og4FC%}h4#1k(=gjukq;PE^#VUY}E_Chm`E7Lf-)!}iY5S^Sg1&YYR&noP`NwkaefFLP0E8G%w?Dkcl3;>&AtKn=Hh-U!p@@)iD@ zNe5;K0;n0L%zl>LD@2aUK#gdxNCQhLq_oE79wz9|cE_mG&86c#^TBqhb{mZ$su!_F z<2r{Fqbx&0mlqeGO?6(4kci2oi zfDuItWtb_>0qUpppjBMft)Juu5qV!Ed?(0VNan}gyJGy*(66~R+nym~?VKwIY7`wA z!Ym|;`#M{=FHD3^$4=xeFI<*5B!Y~~PYGR#)k8-zx%v(rylpGPrRhT|EBi>bHA}L( zUCj!Ssl(D5bnd{9p3Z)jlnYF}$LxURC;g!mB@0((xVtTFXPr7KE1aJVb!S)8r$F*I zO`FOVS;c3ilXUz~WUOU>FQK>++(5eO(UUaUhRL;`MY=B(I8s?S>hrez&q^D2yfqb5 zRYH9s34@D9 z7fm!K4z8M*ZW>J|9ZYNLzoe6ii-|Fzi9@wgT4_NQul98n^&l04@L*)CvF>fD6C{bt>Sz09*hr z02hD@Y6XA`zy;ufIu&qU04@L*fD39a=t7~8$Kz37?y~PdN1$WX^13#AznHxF_Typs z*D8fx?gYO*QfEzay*!wVT>P?b$A;%F_X87Mov+SBEe*9>h0$+cH_(0*iWQS$(nAb73^NdaTzwL++P+m8{sGTt&?Ep!Y7*vo7cN$0HYl zp~A5DNssnSsJT3p|z!> zY4NFDr-?U2tjD|K;Zp)V$2x8~bEhMpuP5URcGnGuF;l0t7KS=`UO=V5xAOXVtsZ}H zxG#IShZfzwXq2_@9Onm?Q{nNfrkE`#P%dq=3T7cSlFm9s$t30uMWZ8<>7}`51X_sQ znSXaRCH>yCR(*-Dm6{%z%Fwuouz-pNMebW{TUFlXL|`#`AA+Kp`t=k=zvy=yTv?bZqL|fn&7_G2i;xbv2QIk>)+Yb}002ovPDHLkV1j#_e>VUC literal 0 HcmV?d00001 diff --git a/tmux/.tmux/plugins/tmux-cpu/screenshots/low_icon.png b/tmux/.tmux/plugins/tmux-cpu/screenshots/low_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1c39fe0d84ce395f83f9f927f62bbbcaa04106c6 GIT binary patch literal 1016 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 literal 0 HcmV?d00001 diff --git a/tmux/.tmux/plugins/tmux-cpu/screenshots/medium_icon.png b/tmux/.tmux/plugins/tmux-cpu/screenshots/medium_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3bad91036099fba366424d81ec4180b882a524d1 GIT binary patch literal 1188 zcmV;V1Y7%wP)KWYvjcJXsD4BYAobvqP`wBt;iZbO_*c; zKG-{H_UZTkf%dS78aR48X-f04{QLIi=HcDjjz9l_+j>SpHD?cVZ3Pj2PKIwU@1NPV z_u%c%{~4HtbsfBH<+%U6y|iQ3u}63ez-=^&%peIhpqu`^+&^W>;a8xbV)*~}513{Y zF$oA%`?zsY*SSxu^0u*w0nRUHZ@u>im*<#xc-dZ@+qD15t1nEF7M`I|-rpvzJoQu4 z&7S$#+y#$#&0~U$@6I~<@*ge(h!h_@p~mq4-ya|VhZYPl@$vC9JU?;e!*2$L-;ZzJ z{a_+3!p?v@)c!s`yX`Sl^PR&N9@x6ch%uc4iZC)V0^@?=|9=n-0?h2}j6Z*34}NAv z-;zLmPN+(V`oH)0OkN1B3xGKQ?!tj)FbbK)Wtg%2|MKGA={<+eJOSmSzmF~+f9R#D zD|GYxM>YiwWv*uz?|mcEp{$aUV*g$o1E#BQ*LNM1^iE80{r%>`*8M+ZbsUY9g@4^S zxZ~>Uzt{}>`*`n+xrfn8{NLZdBGe9&Q2YDx%-VGapMUtl%&%nS?4IEBckL3P&KOe7cS$*aWN@B$3X|Nc#l6!#W$2}|0e*mig`}WfAMK|~r^$ql` z90VTUIJ;xhtM9nYVV1IvcGqM_O>2)2FIWQ~Xy-jT)oAg#5ZH}|dB<>DTkQpwHo@8^fd zm$t4wf+*M+S-1p*1b(q05(T@evAGN{i%fW_EyRO=Z?B!Q_$(~VG5ol;@4Q-ITrS(c zkJk^Ze}Xlk0gJ)=n>+h9;|Nkn=$|`xE;%_FpOR5ISO|>vLn%rNqy12Ff^T%*fSgbp zO>3i}MotKjqh~a&jfNUIAwZ6v(X=)iYUG3fIeGv(q`#m4UjNhp0000 /dev/null +} + +is_linux_iostat() { + # Bug in early versions of linux iostat -V return error code + iostat -c &> /dev/null +} + +# is second float bigger or equal? +fcomp() { + awk -v n1=$1 -v n2=$2 'BEGIN {if (n1<=n2) exit 0; exit 1}' +} + +load_status() { + local percentage=$1 + cpu_medium_thresh=$(get_tmux_option "@cpu_medium_thresh" "30") + cpu_high_thresh=$(get_tmux_option "@cpu_high_thresh" "80") + if fcomp $cpu_high_thresh $percentage; then + echo "high" + elif fcomp $cpu_medium_thresh $percentage && fcomp $percentage $cpu_high_thresh; then + echo "medium" + else + echo "low" + fi +} + +temp_status() { + local temp=$1 + cpu_temp_medium_thresh=$(get_tmux_option "@cpu_temp_medium_thresh" "80") + cpu_temp_high_thresh=$(get_tmux_option "@cpu_temp_high_thresh" "90") + if fcomp $cpu_temp_high_thresh $temp; then + echo "high" + elif fcomp $cpu_temp_medium_thresh $temp && fcomp $temp $cpu_temp_high_thresh; then + echo "medium" + else + echo "low" + fi +} + +cpus_number() { + if is_linux; then + if command_exists "nproc"; then + nproc + else + echo "$(( $(sed -n 's/^processor.*:\s*\([0-9]\+\)/\1/p' /proc/cpuinfo | tail -n 1) + 1 ))" + fi + else + sysctl -n hw.ncpu + fi +} + +command_exists() { + local command="$1" + command -v "$command" &> /dev/null +} + +get_tmp_dir() { + local tmpdir="${TMPDIR:-${TMP:-${TEMP:-/tmp}}}" + [ -d "$tmpdir" ] || local tmpdir=~/tmp + echo "$tmpdir/tmux-$EUID-cpu" +} + +get_time() { + date +%s.%N +} + +get_cache_val(){ + local key="$1" + # seconds after which cache is invalidated + local timeout="${2:-2}" + local cache="$(get_tmp_dir)/$key" + if [ -f "$cache" ]; then + awk -v cache="$(head -n1 "$cache")" -v timeout=$timeout -v now=$(get_time) \ + 'BEGIN {if (now - timeout < cache) exit 0; exit 1}' \ + && tail -n+2 "$cache" + fi +} + +put_cache_val(){ + local key="$1" + local val="${@:2}" + local tmpdir="$(get_tmp_dir)" + [ ! -d "$tmpdir" ] && mkdir -p "$tmpdir" && chmod 0700 "$tmpdir" + echo "$(get_time)" > "$tmpdir/$key" + echo -n "$val" >> "$tmpdir/$key" + echo -n "$val" +} + +cached_eval(){ + local command="$1" + local key="$(basename "$command")" + local val="$(get_cache_val "$key")" + if [ -z "$val" ]; then + put_cache_val "$key" "$($command "${@:2}")" + else + echo -n "$val" + fi +} diff --git a/tmux/.tmux/plugins/tmux-cpu/scripts/ram_bg_color.sh b/tmux/.tmux/plugins/tmux-cpu/scripts/ram_bg_color.sh new file mode 100755 index 0000000..e864d23 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-cpu/scripts/ram_bg_color.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source "$CURRENT_DIR/helpers.sh" + +ram_low_bg_color="" +ram_medium_bg_color="" +ram_high_bg_color="" + +ram_low_default_bg_color="#[bg=green]" +ram_medium_default_bg_color="#[bg=yellow]" +ram_high_default_bg_color="#[bg=red]" + +get_bg_color_settings() { + ram_low_bg_color=$(get_tmux_option "@ram_low_bg_color" "$ram_low_default_bg_color") + ram_medium_bg_color=$(get_tmux_option "@ram_medium_bg_color" "$ram_medium_default_bg_color") + ram_high_bg_color=$(get_tmux_option "@ram_high_bg_color" "$ram_high_default_bg_color") +} + +print_bg_color() { + local ram_percentage=$($CURRENT_DIR/ram_percentage.sh | sed -e 's/%//') + local ram_load_status=$(load_status $ram_percentage) + if [ $ram_load_status == "low" ]; then + echo "$ram_low_bg_color" + elif [ $ram_load_status == "medium" ]; then + echo "$ram_medium_bg_color" + elif [ $ram_load_status == "high" ]; then + echo "$ram_high_bg_color" + fi +} + +main() { + get_bg_color_settings + print_bg_color +} +main diff --git a/tmux/.tmux/plugins/tmux-cpu/scripts/ram_fg_color.sh b/tmux/.tmux/plugins/tmux-cpu/scripts/ram_fg_color.sh new file mode 100755 index 0000000..9102870 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-cpu/scripts/ram_fg_color.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source "$CURRENT_DIR/helpers.sh" + +ram_low_fg_color="" +ram_medium_fg_color="" +ram_high_fg_color="" + +ram_low_default_fg_color="#[fg=green]" +ram_medium_default_fg_color="#[fg=yellow]" +ram_high_default_fg_color="#[fg=red]" + +get_fg_color_settings() { + ram_low_fg_color=$(get_tmux_option "@ram_low_fg_color" "$ram_low_default_fg_color") + ram_medium_fg_color=$(get_tmux_option "@ram_medium_fg_color" "$ram_medium_default_fg_color") + ram_high_fg_color=$(get_tmux_option "@ram_high_fg_color" "$ram_high_default_fg_color") +} + +print_fg_color() { + local ram_percentage=$($CURRENT_DIR/ram_percentage.sh | sed -e 's/%//') + local ram_load_status=$(load_status $ram_percentage) + if [ $ram_load_status == "low" ]; then + echo "$ram_low_fg_color" + elif [ $ram_load_status == "medium" ]; then + echo "$ram_medium_fg_color" + elif [ $ram_load_status == "high" ]; then + echo "$ram_high_fg_color" + fi +} + +main() { + get_fg_color_settings + print_fg_color +} +main diff --git a/tmux/.tmux/plugins/tmux-cpu/scripts/ram_icon.sh b/tmux/.tmux/plugins/tmux-cpu/scripts/ram_icon.sh new file mode 100755 index 0000000..e27c64d --- /dev/null +++ b/tmux/.tmux/plugins/tmux-cpu/scripts/ram_icon.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source "$CURRENT_DIR/helpers.sh" + +# script global variables +ram_low_icon="" +ram_medium_icon="" +ram_high_icon="" + +ram_low_default_icon="=" +ram_medium_default_icon="≡" +ram_high_default_icon="≣" + +# icons are set as script global variables +get_icon_settings() { + ram_low_icon=$(get_tmux_option "@ram_low_icon" "$ram_low_default_icon") + ram_medium_icon=$(get_tmux_option "@ram_medium_icon" "$ram_medium_default_icon") + ram_high_icon=$(get_tmux_option "@ram_high_icon" "$ram_high_default_icon") +} + +print_icon() { + local ram_percentage=$($CURRENT_DIR/ram_percentage.sh | sed -e 's/%//') + local ram_load_status=$(load_status $ram_percentage) + if [ $ram_load_status == "low" ]; then + echo "$ram_low_icon" + elif [ $ram_load_status == "medium" ]; then + echo "$ram_medium_icon" + elif [ $ram_load_status == "high" ]; then + echo "$ram_high_icon" + fi +} + +main() { + get_icon_settings + print_icon "$1" +} +main diff --git a/tmux/.tmux/plugins/tmux-cpu/scripts/ram_percentage.sh b/tmux/.tmux/plugins/tmux-cpu/scripts/ram_percentage.sh new file mode 100755 index 0000000..df31c2c --- /dev/null +++ b/tmux/.tmux/plugins/tmux-cpu/scripts/ram_percentage.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source "$CURRENT_DIR/helpers.sh" + +ram_percentage_format="%3.1f%%" + +sum_macos_vm_stats() { + grep -Eo '[0-9]+' \ + | awk '{ a += $1 * 4096 } END { print a }' +} + +print_ram_percentage() { + ram_percentage_format=$(get_tmux_option "@ram_percentage_format" "$ram_percentage_format") + + if command_exists "free"; then + cached_eval free | awk -v format="$ram_percentage_format" '$1 ~ /Mem/ {printf(format, 100*$3/$2)}' + elif command_exists "vm_stat"; then + # page size of 4096 bytes + stats="$(cached_eval vm_stat)" + + used_and_cached=$(echo "$stats" \ + | grep -E "(Pages active|Pages inactive|Pages speculative|Pages wired down|Pages occupied by compressor)" \ + | sum_macos_vm_stats \ + ) + + cached=$(echo "$stats" \ + | grep -E "(Pages purgeable|File-backed pages)" \ + | sum_macos_vm_stats \ + ) + + free=$(echo "$stats" \ + | grep -E "(Pages free)" \ + | sum_macos_vm_stats \ + ) + + used=$(($used_and_cached - $cached)) + total=$(($used_and_cached + $free)) + + echo "$used $total" | awk -v format="$ram_percentage_format" '{printf(format, 100*$1/$2)}' + fi +} + +main() { + print_ram_percentage +} +main diff --git a/tmux/.tmux/plugins/tmux-sensible/.gitattributes b/tmux/.tmux/plugins/tmux-sensible/.gitattributes new file mode 100644 index 0000000..4cde323 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-sensible/.gitattributes @@ -0,0 +1,2 @@ +# Force text files to have unix eols, so Windows/Cygwin does not break them +*.* eol=lf diff --git a/tmux/.tmux/plugins/tmux-sensible/CHANGELOG.md b/tmux/.tmux/plugins/tmux-sensible/CHANGELOG.md new file mode 100644 index 0000000..579c0db --- /dev/null +++ b/tmux/.tmux/plugins/tmux-sensible/CHANGELOG.md @@ -0,0 +1,43 @@ +# Changelog + +### master +- remove `detach-on-destroy` +- do not set `aggressive-resize` on iTerm terminal +- disable `detach-on-destroy` + +### v3.0.0, 2015-06-24 +- remove 'almost sensible' feature + +### v2.3.0, 2015-06-24 +- update to support \*THE\* latest tmux version +- bugfix for `prefix + R` key binding +- fix for tmux 2.0 `default-terminal` option (thanks @kwbr) + +### v2.2.0, 2015-02-10 +- bugfix in `key_binding_not_set`: the regex is now properly detecting key + bindings with `-r` flag. +- enable `aggressive-resize` + +### v2.1.0, 2014-12-12 +- check before binding `prefix + prefix` (@m1foley) +- enable `focus-events` +- deprecate 'almost sensible' feature. The reason for this is to focus the + plugin on doing just one thing. + +### v2.0.0, 2014-10-03 +- bugfix: prevent exiting tmux if 'reattach-to-user-namespace' is not installed +- remove all mouse-related options +- introduce 'almost sensible' setting and options + +### v1.1.0, 2014-08-30 +- bugfix: determine the default shell from the $SHELL env var on OS X +- set `mode-mouse on` by default +- do not make any decision about the prefix, just enhance it +- update `README.md`. List options set in the plugin. +- do *not* set `mode-mouse on` by default because some users don't like it +- if a user changes default prefix but binds `C-b` to something else, do not + unbind `C-b` + +### v1.0.0, 2014-07-30 +- initial work on the plugin +- add readme diff --git a/tmux/.tmux/plugins/tmux-sensible/LICENSE.md b/tmux/.tmux/plugins/tmux-sensible/LICENSE.md new file mode 100644 index 0000000..40f6ddd --- /dev/null +++ b/tmux/.tmux/plugins/tmux-sensible/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (C) 2014 Bruno Sutic + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tmux/.tmux/plugins/tmux-sensible/README.md b/tmux/.tmux/plugins/tmux-sensible/README.md new file mode 100644 index 0000000..7185b67 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-sensible/README.md @@ -0,0 +1,114 @@ +# Tmux sensible + +A set of tmux options that should be acceptable to everyone. + +Inspired by [vim-sensible](https://github.com/tpope/vim-sensible). + +Tested and working on Linux, OSX and Cygwin. + +### Principles + +- `tmux-sensible` options should be acceptable to **every** tmux user!
+ If any of the options bothers you, please open an issue and it will probably + be updated (or removed). +- if you think a new option should be added, feel free to open a pull request. +- **no overriding** of user defined settings.
+ Your existing `.tmux.conf` settings are respected and they won't be changed. + That way you can use `tmux-sensible` if you have a few specific options. + +### Goals + +- group standard tmux community options in one place +- remove clutter from your `.tmux.conf` +- educate new tmux users about basic options + +### Options + + # utf8 is on + set -g utf8 on + set -g status-utf8 on + + # address vim mode switching delay (http://superuser.com/a/252717/65504) + set -s escape-time 0 + + # increase scrollback buffer size + set -g history-limit 50000 + + # tmux messages are displayed for 4 seconds + set -g display-time 4000 + + # refresh 'status-left' and 'status-right' more often + set -g status-interval 5 + + # set only on OS X where it's required + set -g default-command "reattach-to-user-namespace -l $SHELL" + + # upgrade $TERM + set -g default-terminal "screen-256color" + + # emacs key bindings in tmux command prompt (prefix + :) are better than + # vi keys, even for vim users + set -g status-keys emacs + + # focus events enabled for terminals that support them + set -g focus-events on + + # super useful when using "grouped sessions" and multi-monitor setup + setw -g aggressive-resize on + +### Key bindings + + # easier and faster switching between next/prev window + bind C-p previous-window + bind C-n next-window + +Above bindings enhance the default `prefix + p` and `prefix + n` bindings by +allowing you to hold `Ctrl` and repeat `a + p`/`a + n` (if your prefix is +`C-a`), which is a lot quicker. + + # source .tmux.conf as suggested in `man tmux` + bind R source-file '~/.tmux.conf' + +"Adaptable" key bindings that build upon your `prefix` value: + + # if prefix is 'C-a' + bind C-a send-prefix + bind a last-window + +If prefix is `C-b`, above keys will be `C-b` and `b`.
+If prefix is `C-z`, above keys will be `C-z` and `z`... you get the idea. + +### Installation with [Tmux Plugin Manager](https://github.com/tmux-plugins/tpm) (recommended) + +Add plugin to the list of TPM plugins in `.tmux.conf`: + + set -g @plugin 'tmux-plugins/tmux-sensible' + +Hit `prefix + I` to fetch the plugin and source it. That's it! + +### Manual Installation + +Clone the repo: + + $ git clone https://github.com/tmux-plugins/tmux-sensible ~/clone/path + +Add this line to the bottom of `.tmux.conf`: + + run-shell ~/clone/path/sensible.tmux + +Reload TMUX environment with `$ tmux source-file ~/.tmux.conf`, and that's it. + +### Other goodies + +You might also find these useful: + +- [copycat](https://github.com/tmux-plugins/tmux-copycat) + improve tmux search and reduce mouse usage +- [pain control](https://github.com/tmux-plugins/tmux-pain-control) + useful standard bindings for controlling panes +- [resurrect](https://github.com/tmux-plugins/tmux-resurrect) + persists tmux environment across system restarts + +### License + +[MIT](LICENSE.md) diff --git a/tmux/.tmux/plugins/tmux-sensible/sensible.tmux b/tmux/.tmux/plugins/tmux-sensible/sensible.tmux new file mode 100644 index 0000000..5cf2af6 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-sensible/sensible.tmux @@ -0,0 +1,161 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# used to match output from `tmux list-keys` +KEY_BINDING_REGEX="bind-key[[:space:]]\+\(-r[[:space:]]\+\)\?\(-T prefix[[:space:]]\+\)\?" + +is_osx() { + local platform=$(uname) + [ "$platform" == "Darwin" ] +} + +iterm_terminal() { + [[ "$TERM_PROGRAM" =~ ^iTerm ]] +} + +command_exists() { + local command="$1" + type "$command" >/dev/null 2>&1 +} + +# returns prefix key, e.g. 'C-a' +prefix() { + tmux show-option -gv prefix +} + +# if prefix is 'C-a', this function returns 'a' +prefix_without_ctrl() { + local prefix="$(prefix)" + echo "$prefix" | cut -d '-' -f2 +} + +option_value_not_changed() { + local option="$1" + local default_value="$2" + local option_value=$(tmux show-option -gv "$option") + [ "$option_value" == "$default_value" ] +} + +server_option_value_not_changed() { + local option="$1" + local default_value="$2" + local option_value=$(tmux show-option -sv "$option") + [ "$option_value" == "$default_value" ] +} + +key_binding_not_set() { + local key="$1" + if $(tmux list-keys | grep -q "${KEY_BINDING_REGEX}${key}[[:space:]]"); then + return 1 + else + return 0 + fi +} + +key_binding_not_changed() { + local key="$1" + local default_value="$2" + if $(tmux list-keys | grep -q "${KEY_BINDING_REGEX}${key}[[:space:]]\+${default_value}"); then + # key still has the default binding + return 0 + else + return 1 + fi +} + +main() { + # OPTIONS + + # enable utf8 (option removed in tmux 2.2) + tmux set-option -g utf8 on 2>/dev/null + + # enable utf8 in tmux status-left and status-right (option removed in tmux 2.2) + tmux set-option -g status-utf8 on 2>/dev/null + + # address vim mode switching delay (http://superuser.com/a/252717/65504) + if server_option_value_not_changed "escape-time" "500"; then + tmux set-option -s escape-time 0 + fi + + # increase scrollback buffer size + if option_value_not_changed "history-limit" "2000"; then + tmux set-option -g history-limit 50000 + fi + + # tmux messages are displayed for 4 seconds + if option_value_not_changed "display-time" "750"; then + tmux set-option -g display-time 4000 + fi + + # refresh 'status-left' and 'status-right' more often + if option_value_not_changed "status-interval" "15"; then + tmux set-option -g status-interval 5 + fi + + # required (only) on OS X + if is_osx && command_exists "reattach-to-user-namespace" && option_value_not_changed "default-command" ""; then + tmux set-option -g default-command "reattach-to-user-namespace -l $SHELL" + fi + + # upgrade $TERM, tmux 1.9 + if option_value_not_changed "default-terminal" "screen"; then + tmux set-option -g default-terminal "screen-256color" + fi + # upgrade $TERM, tmux 2.0+ + if server_option_value_not_changed "default-terminal" "screen"; then + tmux set-option -s default-terminal "screen-256color" + fi + + # emacs key bindings in tmux command prompt (prefix + :) are better than + # vi keys, even for vim users + tmux set-option -g status-keys emacs + + # focus events enabled for terminals that support them + tmux set-option -g focus-events on + + # super useful when using "grouped sessions" and multi-monitor setup + if ! iterm_terminal; then + tmux set-window-option -g aggressive-resize on + fi + + # DEFAULT KEY BINDINGS + + local prefix="$(prefix)" + local prefix_without_ctrl="$(prefix_without_ctrl)" + + # if C-b is not prefix + if [ $prefix != "C-b" ]; then + # unbind obsolete default binding + if key_binding_not_changed "C-b" "send-prefix"; then + tmux unbind-key C-b + fi + + # pressing `prefix + prefix` sends to the shell + if key_binding_not_set "$prefix"; then + tmux bind-key "$prefix" send-prefix + fi + fi + + # If Ctrl-a is prefix then `Ctrl-a + a` switches between alternate windows. + # Works for any prefix character. + if key_binding_not_set "$prefix_without_ctrl"; then + tmux bind-key "$prefix_without_ctrl" last-window + fi + + # easier switching between next/prev window + if key_binding_not_set "C-p"; then + tmux bind-key C-p previous-window + fi + if key_binding_not_set "C-n"; then + tmux bind-key C-n next-window + fi + + # source `.tmux.conf` file - as suggested in `man tmux` + if key_binding_not_set "R"; then + tmux bind-key R run-shell ' \ + tmux source-file ~/.tmux.conf > /dev/null; \ + tmux display-message "Sourced .tmux.conf!"' + fi +} +main diff --git a/tmux/.tmux/plugins/tmux-yank/.editorconfig b/tmux/.tmux/plugins/tmux-yank/.editorconfig new file mode 100644 index 0000000..d65b947 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-yank/.editorconfig @@ -0,0 +1,24 @@ +# EditorConfig: http://EditorConfig.org + +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +[*.md] +max_line_length = 76 +indent_size = 4 +trim_trailing_whitespace = true + +[Vagrantfile] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + + +[{*.sh,*.tmux}] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true diff --git a/tmux/.tmux/plugins/tmux-yank/.gitattributes b/tmux/.tmux/plugins/tmux-yank/.gitattributes new file mode 100644 index 0000000..07c6d8e --- /dev/null +++ b/tmux/.tmux/plugins/tmux-yank/.gitattributes @@ -0,0 +1,11 @@ +# The linguist directives are for https://github.com/github/linguist + +*.md text eol=lf whitespace=blank-at-eol +*.sh text eol=lf whitespace=blank-at-eol diff=php +*.tmux text eol=lf whitespace=blank-at-eol diff=php + +Vagrantfile text eol=lf linguist-vendored +video/* linguist-documentation + +# Binary Types +*.png binary diff --git a/tmux/.tmux/plugins/tmux-yank/.gitignore b/tmux/.tmux/plugins/tmux-yank/.gitignore new file mode 100644 index 0000000..a977916 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-yank/.gitignore @@ -0,0 +1 @@ +.vagrant/ diff --git a/tmux/.tmux/plugins/tmux-yank/.travis.yml b/tmux/.tmux/plugins/tmux-yank/.travis.yml new file mode 100644 index 0000000..cc4b3ac --- /dev/null +++ b/tmux/.tmux/plugins/tmux-yank/.travis.yml @@ -0,0 +1,17 @@ +sudo: required +language: bash +services: + - docker + +script: +- "./citest" + +notifications: + email: false + pushover: + on_success: change + on_failure: always + api_key: + secure: gWoqAqGyBbO6mcKbHkt29jJZ7ElOfct2C5WPfliBARl8ImCE0HE6CEGdK25i29mjfIxXWp3HITsRawuauZDN8nCZ9srO+0wr7OWAcZuhDW6mDmKNLX2y4eR4lK21MsMpLIHqA48hYXkHVKSHR7TDG88A/0MRXoTb5gcuPDJMqPk= + users: + secure: dIUBBbi8R7cOcwBQ8guLsq+M8vBXcSAu9vKUVEqToSHoWap4fTl4QSZpyhzxLy6uSNRwg1u20xVSlEAPee2Z+efzZQtA0I9bRTkcAMbzH65+sWKgMsEMJoHrqlCr7FvX4c+UMW9sWlRLoH52oN3ilCQNy2beI8mWqE4gAGxD4aA= diff --git a/tmux/.tmux/plugins/tmux-yank/CHANGELOG.md b/tmux/.tmux/plugins/tmux-yank/CHANGELOG.md new file mode 100644 index 0000000..ca1740c --- /dev/null +++ b/tmux/.tmux/plugins/tmux-yank/CHANGELOG.md @@ -0,0 +1,132 @@ +Change Log +========== + +[master] +-------- + +### Added + +- Mouse support, controlled by `yank_with_mouse` and `yank_selection_mouse` + (@keidax) + +[v2.3.0] 2018-02-01 +------------------- + +### Added + +- Tmux 2.4 support (@docwhat, @edi9999) +- Windows Subsystem for Linux (WSL) support via `clip.exe` (@lukewang1024) +- "copy pane current directory" feature (@bruno-) +- `yank_line` and `yank_pane_pwd` fork to prevent xclip from hanging Tmux (@leoalekseyev) +* `yank_line` no longer cares if you use emacs or vi in copy-mode. + +### Fixed + +- Detect git builds of tmux version ≥ 2.4 (@maximbaz PR#89) + +[v2.2.0] 2015-10-12 +------------------- + +### Added + +- Support for custom copy command (if `xclip` and others aren't + accessible, and you want to have your custom copy command) +- Cygwin support via `putclip` command + +[v2.1.0] 2015-06-17 +------------------- + +### Added + +- Add support for `xsel` on Linux (@ctjhoa) +- Support for shell `vi` mode (@xnaveira) + +### Updated + +- Make `reattach-to-user-namespace` on OS X optional (@bosr) +- Deprecate Alty + +[v2.0.0] 2014-12-06 +------------------- + +### Fixed + +- Change copy mode "put selection" key binding to Y so that vi + mode Controly is not overridden. + +[v1.0.0] 2014-12-06 +------------------- + +### Added + +- Show error message if plugin dependencies aren't installed. +- Vagrant setup for manually testing Linux. + +### Updated + +- `README` + - Related plugin list + - Instructions on updating `xclip` for Linux. + +### Removed + +- The screen-cast is moved into `screencast` branch. + +[v0.0.4] 2014-07-29 +------------------- + +### Updated + +- `README` documentation; including a screen-cast. + +[v0.0.3] 2014-06-29 +------------------- + +### Added + +- Wait when doing "yank line" when using a remote shell (`ssh`, `mosh`) to + ensure screen is updated. + +### Fixed + +- Handle `yank-line` when used on the last line of buffer: copy multiple + lines. +- `yank-line` never yanks 'newline' char for multiple-line commands in + shell (this is actually tmux/bash bug). + +### Updated + +- Code cleanup. + +[v0.0.2] 2014-06-25 +------------------- + +### Updated + + - `README` + +### Added + + - In OS X: Check if `reattach-to-user-namespace` is installed. + - "copy current command line" feature. + +[v0.0.1] 2014-06-24 +------------------- + +- First working version. + +Notes +----- + +This change log is kept in format. + + [master]: https://github.com/tmux-plugins/tmux-yank/compare/v2.3.0...HEAD + [v2.3.0]: https://github.com/tmux-plugins/tmux-yank/compare/v2.2.0...v2.3.0 + [v2.2.0]: https://github.com/tmux-plugins/tmux-yank/compare/v2.1.0...v2.2.0 + [v2.1.0]: https://github.com/tmux-plugins/tmux-yank/compare/v2.0.0...v2.1.0 + [v2.0.0]: https://github.com/tmux-plugins/tmux-yank/compare/v1.0.0...v2.0.0 + [v1.0.0]: https://github.com/tmux-plugins/tmux-yank/compare/v0.0.4...v1.0.0 + [v0.0.4]: https://github.com/tmux-plugins/tmux-yank/compare/v0.0.3...v0.0.4 + [v0.0.3]: https://github.com/tmux-plugins/tmux-yank/compare/v0.0.2...v0.0.3 + [v0.0.2]: https://github.com/tmux-plugins/tmux-yank/compare/v0.0.1...v0.0.2 + [v0.0.1]: https://github.com/tmux-plugins/tmux-yank/commits/v0.0.1 diff --git a/tmux/.tmux/plugins/tmux-yank/LICENSE.md b/tmux/.tmux/plugins/tmux-yank/LICENSE.md new file mode 100644 index 0000000..a898835 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-yank/LICENSE.md @@ -0,0 +1,20 @@ +Copyright (C) 2014, 2017 Bruno Sutic +Copyright (C) 2017 Christian Höltje + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tmux/.tmux/plugins/tmux-yank/README.md b/tmux/.tmux/plugins/tmux-yank/README.md new file mode 100644 index 0000000..b6d8916 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-yank/README.md @@ -0,0 +1,288 @@ +[![Build +Status](https://travis-ci.org/tmux-plugins/tmux-yank.svg?branch=master)](https://travis-ci.org/tmux-plugins/tmux-yank) +[![GitHub +release](https://img.shields.io/github/release/tmux-plugins/tmux-yank.svg)](https://github.com/tmux-plugins/tmux-yank/releases) +[![GitHub +issues](https://img.shields.io/github/issues/tmux-plugins/tmux-yank.svg)](https://github.com/tmux-plugins/tmux-yank/issues) + +tmux-yank +========= + +Copy to the system clipboard in [`tmux`](https://tmux.github.io/). + +Supports: + +- Linux +- macOS +- Cygwin +- Windows Subsystem for Linux (WSL) + +Installing +---------- + +### Via TPM (recommended) + +The easiest way to install `tmux-yank` is via the [Tmux Plugin +Manager](https://github.com/tmux-plugins/tpm). + +1. Add plugin to the list of TPM plugins in `.tmux.conf`: + + ``` tmux + set -g @plugin 'tmux-plugins/tmux-yank' + ``` + +2. Use prefixI install `tmux-yank`. You should now + be able to `tmux-yank` immediately. +3. When you want to update `tmux-yank` use prefixU. + +### Manual Installation + +1. Clone the repository + + ``` sh + $ git clone https://github.com/tmux-plugins/tmux-yank ~/clone/path + ``` + +2. Add this line to the bottom of `.tmux.conf` + + ``` tmux + run-shell ~/clone/path/yank.tmux + ``` + +3. Reload the `tmux` environment + + ``` sh + # type this inside tmux + $ tmux source-file ~/.tmux.conf + ``` + +You should now be able to use `tmux-yank` immediately. + +Requirements +------------ + +In order for `tmux-yank` to work, there must be a program that store data in +the system clipboard. + +### macOS + +- [`reattach-to-user-namespace`](https://github.com/ChrisJohnsen/tmux-MacOSX-pasteboard) + +**Note**: Some versions of macOS (aka OS X) have been reported to work +without `reattach-to-user-namespace`. It doesn't hurt to have it installed. + +- OS X 10.8: Mountain Lion – *required* +- OS X 10.9: Mavericks – *required* +- OS X 10.10: Yosemite – *not required* +- OS X 10.11: El Capitan – *not required* +- macOS 10.12: Sierra – *required* + +The easiest way to use `reattach-to-user-namespace` with `tmux` is use to +use the [`tmux-sensible`](https://github.com/tmux-plugins/tmux-sensible) +plugin. + +To use it manually, use: + +``` tmux +# ~/.tmux.conf +set-option -g default-command "reattach-to-user-namespace -l $SHELL" +``` + +If you have `tmux` 1.5 or newer and are using +[iTerm2](https://www.iterm2.com/) version 3 or newer then the y +in `copy-mode` and mouse selection will work without `tmux-yank`. + +To enable this: + +1. Go into iTerm2's preferences. +2. Go to the "General" tab. +3. Check "Applications in terminal may access clipboard" +4. In `tmux`, ensure `set-clipboard` is turned on: + + ``` sh + $ tmux show-options -g -s set-clipboard + set-clipboard on + ``` + +#### [HomeBrew](https://brew.sh/) (recommended) + +``` sh +$ brew install reattach-to-user-namespace +``` + +#### MacPorts + +``` sh +$ sudo port install tmux-pasteboard +``` + +### Linux + +- `xsel` (recommended) or `xclip` (for X). +- `wl-copy` from [wl-clipboard](https://github.com/bugaevc/wl-clipboard) (for Wayland) + +If you have `tmux` 1.5 or newer and are using `xterm`, the y in +`copy-mode` and mouse selection will work without `tmux-yank`. See the +`tmux(1)` man page entry for the `set-clipboard` option. + +#### Debian & Ubuntu + +``` sh +$ sudo apt-get install xsel # or xclip +``` + +#### RedHat & CentOS + +``` sh +$ sudo yum install xsel # or xclip +``` + +### Cygwin + +- (*optional*) `putclip` which is part of the `cygutils-extra` package. + +### Windows Subsystem for Linux (WSL) + +- `clip.exe` is shipped with Windows Subsystem for Linux. + +Configuration +------------- + +### Key bindings + +- Normal Mode + - prefixy — copies text from the command line + to the clipboard. + + Works with all popular shells/repls. Tested with: + + - shells: `bash`, `zsh` (with `bindkey -e`), `tcsh` + - repls: `irb`, `pry`, `node`, `psql`, `python`, `php -a`, + `coffee` + - remote shells: `ssh`, [mosh](http://mosh.mit.edu/) + - vim/neovim command line (requires + [vim-husk](https://github.com/bruno-/vim-husk) or + [vim-rsi](https://github.com/tpope/vim-rsi) plugin) + + - prefixY — copy the current pane's current + working directory to the clipboard. + +- Copy Mode + - y — copy selection to system clipboard. + - Y (shift-y) — "put" selection. Equivalent to copying a + selection, and pasting it to the command line. + + +### Default and Preferred Clipboard Programs + +tmux-yank does its best to detect a reasonable choice for a clipboard +program on your OS. + +If tmux-yank can't detect a known clipboard program then it uses the +`@custom_copy_command` tmux option as your clipboard program if set. + +If you need to always override tmux-yank's choice for a clipboard program, +then you can set `@override_copy_command` to force tmux-yank to use whatever +you want. + +Note that both programs _must_ accept `STDIN` for the text to be copied. + +An example of setting `@override_copy_command`: + +``` tmux +# ~/.tmux.conf + +set -g @custom_copy_command 'my-clipboard-copy --some-arg' +# or +set -g @override_copy_command 'my-clipboard-copy --some-arg' +``` + +### Linux Clipboards + +Linux has several cut-and-paste clipboards: `primary`, `secondary`, and +`clipboard` (default in tmux-yank is `clipboard`). + +You can change this by setting `@yank_selection`: + +``` tmux +# ~/.tmux.conf + +set -g @yank_selection 'primary' # or 'secondary' or 'clipboard' +``` + +With mouse support turned on (see below) the default clipboard for mouse +selections is `primary`. + +You can change this by setting `@yank_selection_mouse`: + +``` tmux +# ~/.tmux.conf + +set -g @yank_selection_mouse 'clipboard' # or 'primary' or 'secondary' +``` + +### Controlling Yank Behavior + +By default, `tmux-yank` will exit copy mode after yanking text. If you wish to +remain in copy mode, you can set `@yank_action`: + +``` tmux +# ~/.tmux.conf + +set -g @yank_action 'copy-pipe' # or 'copy-pipe-and-cancel' for the default +``` + +### Mouse Support + +`tmux-yank` has mouse support enabled by default. It will only work if `tmux`'s +built-in mouse support is also enabled (with `mouse on` since `tmux` 2.1, or +`mode-mouse on` in older versions). + +To yank with the mouse, click and drag with the primary button to begin +selection, and release to yank. + +If you would prefer to disable this behavior, or provide your own bindings for +the `MouseDragEnd1Pane` event, you can do so with: + +``` tmux +# ~/.tmux.conf + +set -g @yank_with_mouse off # or 'on' +``` + +If you want to remain in copy mode after making a mouse selection, set +`@yank_action` as described above. + +### vi mode support + +If using `tmux` 2.3 or older *and* using vi keys then you'll have add the +following configuration setting: + +``` tmux +# ~/.tmux.conf + +set -g @shell_mode 'vi' +``` + +This isn't needed with `tmux` 2.4 or newer. + +### Screen-cast + +[![screencast +screenshot](/video/screencast_img.png)](https://vimeo.com/102039099) + +**Note**: The screen-cast uses Controly for +"put selection". Use Y in `v2.0.0` and later. + +### Other tmux plugins + +- [tmux-copycat](https://github.com/tmux-plugins/tmux-copycat) - a plugin + for regular expression searches in tmux and fast match selection +- [tmux-open](https://github.com/tmux-plugins/tmux-open) - a plugin for + quickly opening highlighted file or a URL +- [tmux-continuum](https://github.com/tmux-plugins/tmux-continuum) - + automatic restoring and continuous saving of tmux environment. + +### License + +[MIT](LICENSE.md) diff --git a/tmux/.tmux/plugins/tmux-yank/Vagrantfile b/tmux/.tmux/plugins/tmux-yank/Vagrantfile new file mode 100644 index 0000000..778b77a --- /dev/null +++ b/tmux/.tmux/plugins/tmux-yank/Vagrantfile @@ -0,0 +1,10 @@ +VAGRANTFILE_API_VERSION = '2' + +Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| + config.vm.box = 'precise32' + config.vm.box_url = 'http://files.vagrantup.com/precise32.box' + + config.vm.provision 'shell', path: 'vagrant_provisioning.sh' + + config.ssh.forward_x11 = true +end diff --git a/tmux/.tmux/plugins/tmux-yank/_config.yml b/tmux/.tmux/plugins/tmux-yank/_config.yml new file mode 100644 index 0000000..c741881 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-yank/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-slate \ No newline at end of file diff --git a/tmux/.tmux/plugins/tmux-yank/citest b/tmux/.tmux/plugins/tmux-yank/citest new file mode 100644 index 0000000..f8e17f5 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-yank/citest @@ -0,0 +1,30 @@ +#!/bin/bash + +set -euo pipefail + +cd "$(dirname "$0")" + +bash_scripts=( + yank.tmux + scripts/*.sh +) + +set -x +docker run \ + --rm \ + --volume="${PWD}:/mnt:ro" \ + --workdir="/mnt" \ + bash:latest \ + bash -Dn "${bash_scripts[@]}" + +docker run \ + --rm \ + --volume="${PWD}:/mnt:ro" \ + --workdir="/mnt" \ + koalaman/shellcheck:stable \ + --shell=bash \ + --external-sources \ + --color=always \ + "${bash_scripts[@]}" + +# EOF diff --git a/tmux/.tmux/plugins/tmux-yank/scripts/copy_line.sh b/tmux/.tmux/plugins/tmux-yank/scripts/copy_line.sh new file mode 100644 index 0000000..20a70e1 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-yank/scripts/copy_line.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +HELPERS_DIR="$CURRENT_DIR" +TMUX_COPY_MODE="" + +REMOTE_SHELL_WAIT_TIME="0.4" + +# shellcheck source=scripts/helpers.sh +source "${HELPERS_DIR}/helpers.sh" + +# sets a TMUX_COPY_MODE that is used as a global variable +get_tmux_copy_mode() { + TMUX_COPY_MODE="$(tmux show-option -gwv mode-keys)" +} + +# The command when on ssh with latency. To make it work in this case too, +# sleep is added. +add_sleep_for_remote_shells() { + local pane_command + pane_command="$(tmux display-message -p '#{pane_current_command}')" + if [[ $pane_command =~ (ssh|mosh) ]]; then + sleep "$REMOTE_SHELL_WAIT_TIME" + fi +} + +go_to_the_beginning_of_current_line() { + if [ "$(shell_mode)" == "emacs" ]; then + tmux send-key 'C-a' + else + tmux send-key 'Escape' '0' + fi +} + +enter_tmux_copy_mode() { + tmux copy-mode +} + +start_tmux_selection() { + if tmux_is_at_least 2.4; then + tmux send -X begin-selection + elif [ "$TMUX_COPY_MODE" == "vi" ]; then + # vi copy mode + tmux send-key 'Space' + else + # emacs copy mode + tmux send-key 'C-Space' + fi +} + +# works when command spans accross multiple lines +end_of_line_in_copy_mode() { + if tmux_is_at_least 2.4; then + tmux send -X -N 150 'cursor-down' # 'down' key. 'vi' mode is faster so we're + # jumping more lines than emacs. + tmux send -X 'end-of-line' # End of line (just in case we are already at the last line). + tmux send -X 'previous-word' # Beginning of the previous word. + tmux send -X 'next-word-end' # End of next word. + elif [ "$TMUX_COPY_MODE" == "vi" ]; then + # vi copy mode + # This sequence of keys consistently selects multiple lines + tmux send-key '150' # Go to the bottom of scrollback buffer by using + tmux send-key 'j' # 'down' key. 'vi' mode is faster so we're + # jumping more lines than emacs. + tmux send-key '$' # End of line (just in case we are already at the last line). + tmux send-key 'b' # Beginning of the previous word. + tmux send-key 'e' # End of next word. + else + # emacs copy mode + for ((c = 1; c <= '30'; c++)); do # go to the bottom of scrollback buffer + tmux send-key 'C-n' + done + tmux send-key 'C-e' + tmux send-key 'M-b' + tmux send-key 'M-f' + fi +} + +yank_to_clipboard() { + if tmux_is_at_least 2.4; then + # shellcheck disable=SC2119 + tmux send -X copy-pipe-and-cancel "$(clipboard_copy_command)" + else + tmux send-key "$(yank_wo_newline_key)" + fi +} + +go_to_the_end_of_current_line() { + if [ "$(shell_mode)" == "emacs" ]; then + tmux send-keys 'C-e' + else + tmux send-keys '$' 'a' + fi +} + +yank_current_line() { + go_to_the_beginning_of_current_line + add_sleep_for_remote_shells + enter_tmux_copy_mode + start_tmux_selection + end_of_line_in_copy_mode + yank_to_clipboard + go_to_the_end_of_current_line + display_message 'Line copied to clipboard!' +} + +main() { + get_tmux_copy_mode + yank_current_line +} +main diff --git a/tmux/.tmux/plugins/tmux-yank/scripts/copy_pane_pwd.sh b/tmux/.tmux/plugins/tmux-yank/scripts/copy_pane_pwd.sh new file mode 100644 index 0000000..07b6f95 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-yank/scripts/copy_pane_pwd.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +HELPERS_DIR="$CURRENT_DIR" + +# shellcheck source=scripts/helpers.sh +source "${HELPERS_DIR}/helpers.sh" + +pane_current_path() { + tmux display -p -F "#{pane_current_path}" +} + +display_notice() { + display_message 'PWD copied to clipboard!' +} + +main() { + local copy_command + # shellcheck disable=SC2119 + copy_command="$(clipboard_copy_command)" + # $copy_command below should not be quoted + pane_current_path | tr -d '\n' | $copy_command + display_notice +} +main diff --git a/tmux/.tmux/plugins/tmux-yank/scripts/helpers.sh b/tmux/.tmux/plugins/tmux-yank/scripts/helpers.sh new file mode 100644 index 0000000..c80a93f --- /dev/null +++ b/tmux/.tmux/plugins/tmux-yank/scripts/helpers.sh @@ -0,0 +1,205 @@ +#!bash +# shellcheck disable=SC2239 + +yank_line="y" +yank_line_option="@yank_line" + +yank_pane_pwd="Y" +yank_pane_pwd_option="@yank_pane_pwd" + +yank_default="y" +yank_option="@copy_mode_yank" + +put_default="Y" +put_option="@copy_mode_put" + +yank_put_default="M-y" +yank_put_option="@copy_mode_yank_put" + +yank_wo_newline_default="!" +yank_wo_newline_option="@copy_mode_yank_wo_newline" + +yank_selection_default="clipboard" +yank_selection_option="@yank_selection" + +yank_selection_mouse_default="primary" +yank_selection_mouse_option="@yank_selection_mouse" + +yank_with_mouse_default="on" +yank_with_mouse_option="@yank_with_mouse" + +yank_action_default="copy-pipe-and-cancel" +yank_action_option="@yank_action" + +shell_mode_default="emacs" +shell_mode_option="@shell_mode" + +custom_copy_command_default="" +custom_copy_command_option="@custom_copy_command" + +override_copy_command_default="" +override_copy_command_option="@override_copy_command" + +# helper functions +get_tmux_option() { + local option="$1" + local default_value="$2" + local option_value + option_value=$(tmux show-option -gqv "$option") + if [ -z "$option_value" ]; then + echo "$default_value" + else + echo "$option_value" + fi +} + +yank_line_key() { + get_tmux_option "$yank_line_option" "$yank_line" +} + +yank_pane_pwd_key() { + get_tmux_option "$yank_pane_pwd_option" "$yank_pane_pwd" +} + +yank_key() { + get_tmux_option "$yank_option" "$yank_default" +} + +put_key() { + get_tmux_option "$put_option" "$put_default" +} + +yank_put_key() { + get_tmux_option "$yank_put_option" "$yank_put_default" +} + +yank_wo_newline_key() { + get_tmux_option "$yank_wo_newline_option" "$yank_wo_newline_default" +} + +yank_selection() { + get_tmux_option "$yank_selection_option" "$yank_selection_default" +} + +yank_selection_mouse() { + get_tmux_option "$yank_selection_mouse_option" "$yank_selection_mouse_default" +} + +yank_with_mouse() { + get_tmux_option "$yank_with_mouse_option" "$yank_with_mouse_default" +} + +yank_action() { + get_tmux_option "$yank_action_option" "$yank_action_default" +} + +shell_mode() { + get_tmux_option "$shell_mode_option" "$shell_mode_default" +} + +custom_copy_command() { + get_tmux_option "$custom_copy_command_option" "$custom_copy_command_default" +} + +override_copy_command() { + get_tmux_option "$override_copy_command_option" "$override_copy_command_default" +} +# Ensures a message is displayed for 5 seconds in tmux prompt. +# Does not override the 'display-time' tmux option. +display_message() { + local message="$1" + + # display_duration defaults to 5 seconds, if not passed as an argument + if [ "$#" -eq 2 ]; then + local display_duration="$2" + else + local display_duration="5000" + fi + + # saves user-set 'display-time' option + local saved_display_time + saved_display_time=$(get_tmux_option "display-time" "750") + + # sets message display time to 5 seconds + tmux set-option -gq display-time "$display_duration" + + # displays message + tmux display-message "$message" + + # restores original 'display-time' value + tmux set-option -gq display-time "$saved_display_time" +} + +command_exists() { + local command="$1" + type "$command" >/dev/null 2>&1 +} + +clipboard_copy_command() { + local mouse="${1:-false}" + # installing reattach-to-user-namespace is recommended on OS X + if [ -n "$(override_copy_command)" ]; then + override_copy_command + elif command_exists "pbcopy"; then + if command_exists "reattach-to-user-namespace"; then + echo "reattach-to-user-namespace pbcopy" + else + echo "pbcopy" + fi + elif command_exists "clip.exe"; then # WSL clipboard command + echo "cat | clip.exe" + elif command_exists "wl-copy"; then # wl-clipboard: Wayland clipboard utilities + echo "wl-copy" + elif command_exists "xsel"; then + local xsel_selection + if [[ $mouse == "true" ]]; then + xsel_selection="$(yank_selection_mouse)" + else + xsel_selection="$(yank_selection)" + fi + echo "xsel -i --$xsel_selection" + elif command_exists "xclip"; then + local xclip_selection + if [[ $mouse == "true" ]]; then + xclip_selection="$(yank_selection_mouse)" + else + xclip_selection="$(yank_selection)" + fi + echo "xclip -selection $xclip_selection" + elif command_exists "putclip"; then # cygwin clipboard command + echo "putclip" + elif [ -n "$(custom_copy_command)" ]; then + custom_copy_command + fi +} + +# Cache the TMUX version for speed. +tmux_version="$(tmux -V | cut -d ' ' -f 2)" + +tmux_is_at_least() { + if [[ $tmux_version == "$1" ]] || [[ $tmux_version == master ]]; then + return 0 + fi + + local i + local -a current_version wanted_version + IFS='.' read -ra current_version <<<"$tmux_version" + IFS='.' read -ra wanted_version <<<"$1" + + # fill empty fields in current_version with zeros + for ((i = ${#current_version[@]}; i < ${#wanted_version[@]}; i++)); do + current_version[i]=0 + done + + # fill empty fields in wanted_version with zeros + for ((i = ${#wanted_version[@]}; i < ${#current_version[@]}; i++)); do + wanted_version[i]=0 + done + + for ((i = 0; i < ${#current_version[@]}; i++)); do + if ((10#${current_version[i]} < 10#${wanted_version[i]})); then + return 1 + fi + done + return 0 +} diff --git a/tmux/.tmux/plugins/tmux-yank/vagrant_provisioning.sh b/tmux/.tmux/plugins/tmux-yank/vagrant_provisioning.sh new file mode 100644 index 0000000..c093d29 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-yank/vagrant_provisioning.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +sudo apt-get update +sudo apt-get install -y git-core expect vim xclip +sudo apt-get install -y python-software-properties software-properties-common + +# install latest Tmux 1.9a +sudo add-apt-repository -y ppa:pi-rho/dev +sudo apt-get update +sudo apt-get install -y tmux=1.9a-1~ppa1~p + +# configure X11 for xclip testing +echo "export DISPLAY='IP:0.0'" >>/home/vagrant/.bashrc diff --git a/tmux/.tmux/plugins/tmux-yank/video/README.md b/tmux/.tmux/plugins/tmux-yank/video/README.md new file mode 100644 index 0000000..97f3056 --- /dev/null +++ b/tmux/.tmux/plugins/tmux-yank/video/README.md @@ -0,0 +1,7 @@ +## Tmux yank screencast + +This directory contains docs used for creating +[tmux yank screencast](https://vimeo.com/102039099). + +- `script.md` - this file contains a script and a voiceover used to produce the + screencast diff --git a/tmux/.tmux/plugins/tmux-yank/video/screencast_img.png b/tmux/.tmux/plugins/tmux-yank/video/screencast_img.png new file mode 100644 index 0000000000000000000000000000000000000000..3bbedd2eaa37c3ed6078deec7043d5cbb80292f3 GIT binary patch literal 52912 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

x=!IK4i(vJ}`&#lE3lC0=%UjG+BIsdA*; zYp~?tM#53fsPM|5ES?wjM5gmSeX?}Os*Z@3XyS%ecwjKckT4-+ zdVDIzUX{N4L^u`CE%%S?R+!}OkMiY4+P52#HxS}ZS)e!DXiC1oUNJTEh`m=XsMy=t z!jhyAAyxbH=()Q{;LBw;*g&%)$xufD4R!^Pg|X$zbu?jN6@@nqzG>qJEj}Y)>&k8I zkgyvGYC&=k+ky<9fRE0Gvlx)=*1_H<3XhbO8ifqS$bF-~s+T|;mHfTe zu0?4{IQfG^`tWHtlWX1E(kS5agZudBS`!k|I{-MY;1k7ifx##JN=(o>O|k+ln$nJ3$PKC6>Y%+BYPVo!d? z^TXM()ZT#m^o0Ay`^6)7A8DkTu3-*ki4u8Obx_9{f4l*EF1E|nFMT0fYnIcQrenB) z!58nVcOlnp^|m%pWXpoc7YFw}u=~U+FJ5T4DaZ1Uw==q)?cd@50vp}`j5S|+&(#d= z$77LM!uB1gF zE*ljQDI49NHW6w;6q#5mJGi72n+` zRsjln?#xI+8oeD=JCw>~zjxvNs4BhK_;^~U)hq6)tY!xPJwVXtGSHvd+EsUuYGm`x zDfYVo4W^SPs_T(Bi}Kj)XPh}~-g{Y?`hZW&ZkSfvS~EHJ9+%r!aPD}pshbP4H%-ZJ zu_KCk%wBx+lPo=k9gC3}vK=>MTWOL`Ax=!I!bp!c)3S`1Uel$d^8I#8Vzw@ny?hDd z_xz*s!Qd$g`UciUe`5m$f@^-;jj(vJ3>EtD&1psxzwjn?AzrT7ND{GMFr55Wp`02{ zmg_8_m6Sb_S)yOp_R;OnFCMN!+$-f_6@*^cHeGTL9V->c8sYM8w3QrMu}5%(Qc9WA z-#b|r%7m&pIF+xb-OHx`KLCnAb-%ebiT6$;>8S)+=3XV{^j<@afJ}esu8n^-An=2S z0MEe^4AVT4gtk?mH&1964l*)_O|32IaT>ylP}QyYJL5Bp2O`>@Wi%j$2;AB3DgjZf zw+-M8beud?6Hu^=XD>%N#>PoFW08YR?;!bT)h;tk)HVmA5rydbY0zCAM&%TFp3npw z9!P2&wT`w6J&ww`cMe3FjLSU4m8hfaV*!yzfdC#pnETZ!_*CP0X2jo zLM7LT;HN}n5Dbx76@hB^;stf_^7ZKL7>+|ntK<0nZBe}`KQ3PAjkn%fL`TT|#4>#1 zR}SJ`H5Fg_zrIcmtnKK+Dx#=4FXl!jc<;J&&&`dJMvTuxd}rR1%2MnD8I_Uq6VHsq@NC&#&(HZT?sJ@rURIQf;|{~E(kkc+=Uvh-F} z)v%t~m|+gu16C=&E8be?oCijk2WyrJRwXL3mqa$`l~wh`IK0 zYhVNd!qQPALd6oy+{0l~%d5iSCR9l!iP(uQ-8zZ?L-!oOiJ^!&0AF&JRbZso{_`EY z$+9TRTZ^h_0QDF8F2$0hIj{W{iC1t%}oSWgyDe z5?)M(mdTH1D?E+uf4hw23CG6jXiow?xjX9wTiQn$Ci zUMEzy2jeb&^>$v~xlSVZ-Q8?_vjKr07zEbO-van$ev*TJN5cU#M(_CSwLD$#w8U(u zhv}WK(wnuCfKesxpTH{ z-SMs`}ta2m{-A5j!+6h!0W$~3??}!h5Zd2?zKqPX@QjGSG$F_U+#3w$p3q$Tw zy!=u}9Qs&uRMu#3SjOcwn_^Tqk#8jbzLvBQaIj9~O$#0X~T*`N8uQ^_^&)P)OiXut6$cv)cZPW8=2 z>TJ5Qm8qDmOaJ(N_EDFN_7`#SJ+ym!GWsfEoWQ&7e$sF>;xVQw>H-GsLj={8lYFfg zw`19ZMG*57_urG^G<4t9$>3;ReX>jGj(qCuIqoGz40Z?^i9>A$-5CdjJ&S#-AcqMcqnq1N+KlDiC zL%?q0L~w~~0Vw-P`C{N{AfOG}k^anReD%h)8}+$IUq9 zNC&^NV|HpW9(=fk;wuZdv##M< zoD&VrxwtGBAhjSIlnqB~OD*o5B`N4`-@(nXfC}YuJ9)3lGg8kK$rG#K!s~HmU13gV zuHC@!jvFy>)d64-g7Dzs{X}}Vz`2i61rdadq*YH&E^{qarO)KKfzVj4cO1{8J|qo| z!m0~3>{_w#;d>6kg*W2~HJQrRn%n=tfrChO^F(srN}gShJ^Tm};(4g+x)MP*SD2|= zESS}P@=`qXuysp_slYQsVyzuY_vsYe$mbPce4SVjb4BUM&`esE`c35Z0{XQVQ__P;F=0mr^9oV@HpsboY8lDT!J(po#V5aozQv0f>1f z>E^r7K-Rl&H{NYP;71e!NsS9$T%H_`V(^6;fjpkU_x=^29l{X=HB}COma@)8i;)K& z$-#~({fooHJ^8D|9IqBRgkRD!n^UcV?AZ(os3D6`6(b^%#FTDKPYw^PGeIBI0f<7)c zzWVtuMCXlmkZLvRk|mJ*>3I9C6De}CtK(+s^QjL%L9}?LYM@zmpMCB-@%$Hmi*fNZ z0&ze6J70)t47U2!oWF36d|p%W%FD0D>u9wJJ7IwAg~e}@2QxEca>U;X;CDN;TerkV4X zzVYpt#TB^#qxq%F*HVDx3MxRo+q54j^=Vv({U;WsfwU?&;|0n9OWciHxOt60N08<= zT>4a5wO_f+8Wy4vdqFis`y^7yEUL3*zEeWl41w?Iz7^ko;W<3578!#)Tf+nK<*$7M z-71-EAvhX+5^ux!8sqf%u&F;aeS)z!H?OzHGwef99kINum=q?{ap~OI_~Mtp2K+JB zao+$@?j%%ZpCI56u&Wo&^LHT;1Z$q$x_LeR@IQV&eXe8w&B3;QK63nc^pX0)2izEhkAC`7ARvOcrbwCyGTub0jh43UAgQ@H_3R6zL)kSp=SC1_{bAaL?5azc}t~_pZe4%;Hp~^(How#Xj3t@$q%B2Dj`l|Jh|~s zb%^k*nIi^vbnw6U&mN7IJ@s++jhjd~8huMKeh)*CO3AI&1k$GP6nV2^tq^HvCYpAZ z#b5a!ol+{q3K?dxIh}ihD1L~-W-=@nwIDqaNNAkHC!TsLaU#P63OY_M z#C$ys2!|lS)$|9(i^v3+8pha{K8)8#4&M`%wVUI~Cu)-lahSO_x8BYY+)G_|_EIkB zsmC6H7&NB*VWx>$L?Sf*mwPq~u`-af{4Z7m>s8LjXq<&{(H5j#OU0d%hfq|iQ5xMr z`j)vFJl&X=563?8gekomTcT~vIw~1Q4d6---n-E@An@Y^fm9TV=jZ*+XKGOf5sxex zs4M3$q)WDL-yV~gA~Xm1{riXan>N z95evV{6V7xtMwC9Xyo~|_G?N1t*Sy5hBbN{v7NM{V(=bqBy|u7qkX#|1}4$79IFq% zoU(R=a)Qsi_R^)xF-){|^Y&J#Cn-JPT)RPb8eeN`$%RE9J#h3Ql1kKG1I)oZMt!4$ zxcgQhRj9^VitgSqg3SnmE1(`p6$a$~ImX1mj0vN=V+O8Sqxw`dY^#LOm6GOUh2TG8 zAFwG*;0ZM~nHT5JkrS*YE9x35s0KI?&%8WMUM=fQh}_u}V7CQF1M@+RBDuYC;>{G; z=Wh}K5n>^dL?x;lV++(_c6E0rSk3~^LJZH=EgwM=sU;9duc9#!w!|q5n}~fFMZK!Z zhP4Z?UV?=r7M8#*EG$mJLmjueGi8R!Up9hD$Y8w*v}{%Eq3WV}#jG}@!(P3x8jB;v_8j@1fb zO_`LD65l#W03)LOHkb`1*=wAa&Ouzg2`pUq1mrfFgk+;_K;TCk0zL<(b6=e^3q~eE zqAQYc_{5l8wV|#qN%*E6`Un5$AH`q!`Ja!kKKl&JANlQgUd1(PA?{RR-PJ(CoMH;J zx9v+AbSLMg;Q(=}!L?O)#vD{EkQn|}!DJcLjZ-%`t^#+;X(m=eI&tjf#2w7W{?;~9 zO_Xw}-b`?qApee`A-rb_6K5$`IfE*!2v40UI9pSvKl$jx3E{l@m1m>!U@Ns9?n$a2 z)1AC__CnNSgE9C{51+CWBu5Pn#+4v1OcVJ{nOUm^4o?{qu!2fw9{1ELMkl&CYmPOL zT70$?8v}sMMj{y_P;E~s)eD()?l@d7HPII^-sbcBDz7X2<>0Zj#_K$^yNI<<6~n<3 zx4|t|z0*5N#SI4aMXg~v=MGf(10f2-pBuIfI`|Cu{eC+l5oylz0? zM-~D~5AOsLmR9h5fP;A^47%3#D+Aa^KDvYV9*uwgkN-(L`Q(%F;)^fDCqMDY*n-B# zC*gw%O5Ifgl1px(vTb{iL`WT>-Wo)u>!!xwDG-G50M$k>^I6@#Wma{qRc+vPKDI8( z#g2kBmqB<5^YNWg!TA`vMRY6Kc=MpD$RRh=B8WxLBavMZ$RX8_zy@G{O=Qy*kzK*7 z$LfA^ld3iJ6q8ZS1FKG>>e2Vv#MN?CdI>)bK?W*HR8+_E7wU_3tyO?CS@ zxn_T*A7UeBMa&r5OK>~WX%u}Og5f;o`$ zPfKP@r;>(r9{$Jvn!;XUW;z@D6Y2Y!-UIW@7|e%zow`lcl=F}<_`PRCPD<}}FL7?J z)xuxy+xKc-@3qFINGMa#BWv|6cn&fFteN>b)<)Zaz>gCI-a|lcN1~_F12BkPcrn`k z&wur=;?tl0G)^l;FlIRvocgu+)&J#xja{hy2eC)aLu6KAUjN(z34OptiXxR>)D1rN z=Afzs`CApvbl{zX!^t=-QZ+?~xJ<5FuG19Ys%_*3P41V4!6#inzpA`u7qM}Wh1Kk+ z7E}`Hh>*NT&U6gDcroz}R;E*DZpsRkKoTF3f(S|EX&nlYi9~Go)<%#asWvdMt|2XD zh!JT^@I0`OWem_+5E7fHYiZTevbk{lBEm8dVP@{hZI(Xe@N;nwr7N0LK9BWKJPG1g zP@<8_s(tS)EAxha4HB#<(gP@?i&V|y9EGXK&d7ARgaK4b_*GE@p7i6WZ)V8)I{~rH z44COyxOksyRh=aat^Q}kcOepjjJfdk8f{D^z)SdPr}DF;&zAb&)w#HOy}1U)C!?Qi z#3J1S=4(orSvI-j$S7bSpVMF^sURVM_`hvo|rC4 z|2U`29bCg163l-b@$g3P@gXt~nB!Zu(!JKbmxZR!8Pep{8*KvuKfVy~c~RZlM*tzn zH$MBh&&5Cdk~*gN zZHgcX-gX%%H}3_W2jlhp4W~P8I#k@{4si5mT^#)RmV+&j17!RZwAF zMM|!xO~PiT5&Q4Mb(vJRU|6J~&D9iQT1>%iqV5r*>G$rs4_DU%(S7rJeC2D;Mss6J z-2cG+u}A{PuYB{_SjDBxgoPqpH(E|zHI(kPDS#2%LM`*?HOO&_;Jn(BsKc$*3Wh05)qHar# zgC~S7(oWJ3uOjU`n}pzdwL~Vq_a%wI{V|ukX9&k!w{Iju<&NGaU?22wyyxbP4;v8p zF@S*2VJRU3J>cA<;E#X)PyT5<^^uPx56SO5_grGetW@~z?|dh&U%V7eZLKkl3R>r= z4{CRffcPNG?OKqi6lBzZ%cupJOk#KggZBkoG;2wMc=*UYsp?w|F$1&2BV4?6De1-K z5g!DZYn(lK{!;SCcf!`pp2yf*1+uTIsfx3wPQ`UC2lg9Mcr&h2 z&)LgYsP}OXhT{q{v|fvA1H&obm0Bw2VY0hJ#)PJwuUY3mB6A+S@$e8=c(L>3Q zZr0)pH##UNwJml$bYBX%?C$DG9$tMAVbxR;@eT~wPd#xwc`=>D?R5+T^y5U$Yw+)e zgE`Z2gPJ1yq!37eqyk|*+O@QO*gO{-$T@*xe52|98cs-9Oq?+Q=g^SUH0lX1+ z+Jh7b(v|djR~O}4cY?@qv1L{#kS3PlMR%Ptl{jJi8)v!&SMKc z{(4SbymliVdhmF(wzVZSie+v;^5A_bsBHlbxlTW3xarzNKu|^T-hjfU=0?1u@C*VG z>oWV+x$|+H{?z$J=SV#E3!fsMpdr~QD)Aip%|HAmY9CcQ7+>jk3q;@vD$SWneibg= zth0adlOK-?Jh`;>RO4AzOMayi)NdN9lmIr>)uv!Sqv9Wa3`{FC#dNy81 zm1Qxa2OfCPD6@DDtN&Li@V*HIYEnXGp5b13o0%wt0NaN|MYYa}*Itj-)|M1>_A>Wb z@8R<%FQ4|)kww* zh^*_#qh%&u`#1;~;3^r91B`0hJTjMiQZQ1rV{+;51V074lYnWf0^$IS{cx@SH;lbE{U>`7Air@In|Ab34d9Lur(gnccQ!5Y;rMjl3CWr?CMW_u8 zx*WrpX5@G|5>_q-{5J_a97am%C6fJ%U;1()R>jyAv~8%qlBjO9y8!_X{rV=~jqbjy z5BuL}|9^mhv3S-E(!0t8qF?---+`%IK??c36sL6y2LGF1{aVz5w^IEfnQPkz9Fe;w zfusRM7Pkknu#jQ!7JF)fL$waZ8Pr&9J9b1VMUFP3&g$syLjAEnA!>p|V#lHVsj$=A zs<7a)-)Xlc1OpOBhK7l=7>ovN9DDcfNd<*Wz47QrKbkmx)kICGeDqZwA#Ow>BqH@* zC8Al7`=jK(Dk&?8>o=~a?=1If+)30Fpe{2d$O;kLx6qekRs}(8JWe`$)}~EHV)7Fo zc_Ibu8Bsk0QCeXB9UUF0!YWdXgC%K2{Ellxy^+($KmJKRGY7^%q{%GLKKag+B(*iQ ziP%W&jFD+%11q@C_zEmb$(yO9tTJ{WMYUh1)GhxNu+BE5giGCn$<847p&d|iU8W=1 zQf^%7PVTTf_Z}owK`BL{hEkl$)|O_7Ku&z@u}6`*8dLfbrJutPme$tRlr><6gJuD# z4jd9xi<$Q1?3vSX06pmg4?PsGp>A7*s2lIo+_;%FBONW9<~~u6L`0azAJd%hj~k(J zP492t-x{qUVm>)r77irqy}1AHzJC{j8ECHm^6nhgp{(SYxf8aG!=eL(cdy-c7J2uM zjdwq;5Kt1a-cbV#eC^I%@jw2(zn7N!6Ce9%N(to$nLjIXzQdG$_mM(ixLdifK-Agj z?+a_{GW8XR=_n>e#jTz$Tu@8n*s-H=fZSE4sd(${x8vTUN8!vh0bLvd=bq~ey zM~W?RCQ^E+GBK5jDzMjHdldqa2T`J|D^boZZEgIv zhP`7rAxWd47g4Dh+-55D2J8sc1fcb!=l>?EGb;sdMP*fv>Skh=3V{T!)i*Up1;|`3 zy|cSJ=?vBBFXQ30gh6R*V`GZOHdycq1jI^)4Gm4$8fufLltGgMeje{GJu!bbjH9i{U#BP`Yw-!~~X67Q{Xup0l znjjoC?3u7OxNwcvMx+kaZJiLro}NC`hEys89_F30zWa{dyOSDmlq!>#DL-qhNCQ&U zR^T=W{6>**lh48Ud2&tIR)M%%7ys>oSOqH|xLp_IoywM?kUwV_uKdVy?f#M4ez|Bq>Asm+}ks^e2*L7>EX^d%tKEL&iGnl zUQiQTZv=IuG)3k^`{sB5ebD!r@3M1Hy~4lfb(ljx>u%jExS!t%fjeih@%~2}0uq6D z0}07lC<_6eR7F07ow@*x!q%ThI{KW z#KCmxmVe!hfmo@corHDRdt(a{NF`oD{iuP4XO}Q4XOb-XK!|+lfJMR;@w`bbwR_B? zzNto%G5?aTXrplbK3movF>ua=c@|^p1EFhws74AhZHlhELwFiJ`RIMIb4P1@{afFR zPNLLnAsP;#TF78JN9^X_&8VSjkz__l7ouo~*pZMS_VpCIQVJo_-BsI84RH}u5T|XV z04RWf_7efV%7ZSp$QbXb3W(iDwOj>ry9VK^0SOxbm%{_xhidO|%hovl;Qevx%@gEj zx`1~Y!Hcd5L%wTqv#U@i%`q4A_Ul$_+6_IfgiR5uxdpQNO5_Ba_nHGsHI#%lmvInZ znEN6z8(A3Ft5fB^VH^UqHR&Ai_@fU~!ges8{mu)h0$Yjf?vI*MQp7AQ<2qe}%8C-V zrX@k;RtId9!mjlVMkR2Y!46|ecB?avVPC=*FLAi*h*_HON47*P{56BRGM74k5p72D z^E*HNd9}U$NCcn)J@ZeF0hplD+3hu z;Yt$#u*N?~nT?V*_Cqy-F)d@b?apKSsR4=FQUE0c)Cbr(&NIHz)|)`Y-Xmb?ji#-<5s*@BJ6LOvXx970;0#4 zo)i*Gjog-rwP4eXF&tdGeQP7`dNZVZ8A$}Uls%F7=0JF6km8c01cFf|XYnhUgA}Xd z<}jZbxD#PqMZc_GVJHJLKheL3F&X3De{vav1*N@o&KY-K=Fgt+4`)TIjO($(?{0pd zrod0@m)to-`i_e-1c_^I-|$`6aDV1I?(q858~@7M+wADho6?u>{&VNg4zb?eC5!gx zJl22t6TZ?{32AilOMyAWB8Rx5Y~Zqr_#qL2yrgNZ51G18|Ez!W-f>*NRS~^pjUZQv z5~nM&7pcuNNrq(pOT_cNw{3j-5r;q`AQK}ob>mVLU_!_glzR7Aub-B5J~$noQ@=~+ zgx4js4-2^b>#?P}-;xVYKmD}$%8Y-1#>Qx-2G1pxd*zOOF$Xb_fS6@=lsrepq}ULF zlpzriegQWHVFB6a!wrihtWyEjRf_l%6i=fOll^=b1f;8 zE$`h?;h|w)q9cLvP}p2Us=~;7>wjmWy)zXR7g)0rf$2?zqeR#2$HqgXz(UsQHcUdy z*EkBrNEB9$6n{G}50rbA!$rhIdf?BlDq9vK^MLQKJFW1JFa_^0xp@m zaWygqUgLMdE)xvJ-qDZq5{BN(|G+Q9NTk&Fe0KfwjGxX=4tvN&8KR##ADOAsiyO^v zjx$ zEY!X9j*a%?0s(QwA_Qa-0>T5FIVX24&U15TTi^Y3p8O$mfKoQ?I|)dJ)xY!3@AJL= z8wWCn?z|4S)IGa*A`%ynKV^g@TyrT4>{q`1JQK-|&7|a*!5BLPXS;$VQi?%a^@|Up z51WWx)z<E3#fZ4K=2}1k@CR6 zL_C7quvGcnb?SGZqVkcnzdShpt=1tS30XhhVvpQ=gkQ4aVl zYl|}kT8i9^*6%jRk5&yl#CsC2M;RgDV||^Zq0*~eVrgc(R=Q@@16w1 zH{U!NgCuf%6uVOmc94q~F2(t7R8pwKM4U@$rXtR)v=6g*%<&Q31-dV?9_5Dgy|qzj|&*X z%9);#?@2t;DG>id97%tZTQV2G{&rZ+UeRvH>&0HMwNIURyKCCHt@pafeA*TV>BPkJRm1Y`pxq zK_C+zp(OgwN&c=S-gyMld(Qa>omm*A>|gn=yZ_W*$dzU|8xtx%c>giP9E{K)+8V6< z)nte@!)`wM)|$$a7(%aK!9%~9s(ia!o05vdG$%?V%#*6GxbgCsmY^~@uwz?PpyR)e zPQIBw4;|Qz9zQ2;fw0Pwtp&6V93&Pl)cbuea-4uT?PwsYFE)l5+;Wvne2BM$%uI>W zhYdrS;qb0DRBL1n#^zB0BCmz8Y~M;O2#Bzk_>@*`279PdIFA8+4CG>}l3lojanLEWmn#WzxC}RM&%K0CFoqqT9Kx2`*F)8=?a;N(H;flsScnSxna8%{U2+ zk?y2tU=-p9A){c|3j1)qy)!ByfD#ZT4iUI+xWyp%M%K5LagErY$F29CJv*bCzWYgK zuWGP$b6vDG)pOHwBEr^-7h%-^vnhPaMb-y)ZI4{w+mD^3oJjVAz_h8ZhIH;@)N-kb z{djFn;sQJae3ZUSswwgn3D#DX#K9v6NZsCmGX+_OQL70j-J!SRA!03%Yuy7;#@@*o zdp1mh*-Fil-P_xukn_;X-YHog*u5j^Yez8TD?#kV)2s~x{}QksgE6>=-)q_X9tcS} zdtJdgcQiL8tIZJhD9_x!?X7qV?u-etJ@@eU;Ql>P57RS^x1B_XZc`J%kE51sW}Q12 zN2X_RlA>cIqukp@d<;$`BV=AM#r(lN+fxp%9*DC z{~(*e3KAOUGFI|{{~=U-CBO?}l~W3#sN|eA!{Cr;61!VLK^;W6jWfdeT825}+M*(| zFXz_BD*fiLpDPj!5J~n#e)OFz2IeHg#Od#Q;FiqdH~#&Rf`Dgfg;0ZqvEig4xP5@r z8T1_fv46K$t0us55|iM?&yfP)=yc~++0$K^mHdrpKUD!zv1E3MqGz+4htYhRX0b-)naXm-Me-~ z(TO+X)QxU9Ubto+M$5uVG=K!e%o4b>ahM>w9lLh_@AZU@Z6iZ}f zLP7g&19_PEu;9Qxj8e>RaSjhEQq7kgO+@3ZQ)h9}ZRGl;I8C5sHd4`(j~`E!zb1&s zn3xoo{)mfuW~UxnS|d17e}9Na_DVQ1X9c`Ly+r5!*D z>p<$*gF!rDgJc{PX-SdrC zbK#eff7tdXYgAkb_91Mo1`=fenC=Tx5%XD#;fLV}eb9s_jRGmibXL#VJ4V^!LTa2eHZ{g;c$4hiy(@W;={=@H!9IwGXVX@RlwvLQtoV>a6aqcRXJ`29gw^d;`5r4;2HT1zIDp%M}y zi=5?{UwQHQlsl>g!cmNBV-CIdi8o#+wqq!%fYM}mKvIffxaB1t$de~daPKNy{Go&h z|MM4rhjOkn2|4WN;r)pxpNOA${81v$DfR?1*A%e?vRGui77&-!^uF|mzn@}HbVrq9 zzV+HGa9T(faGI^Ht#EXzRq|5W6WwdaAsp>QwikdX_7U``i?2kiw6rXakdLYquJx4{ zUrdz;Qy?M~2_6nE&;UAu9FZz48{32QaRv33aVExooPPTxgb=SM9^;!gZonZHKyY%I zx3&qY0TaDFGecbu%GR3X5Bvhg;0L&L^N1jI>Y;(`*0 zCt7h37uX9)z<|u#b`&{hW@(q&txYCq7F+X!_4ahdxzndWsCm)c(vl=c;js-VYg=nG zQo$`!zMR6_D?dqaIUxOcR7ctqnjsn%?K1k`pvoFttUU@kMi_?%25_6(T!*@&vP6#EG7CUrfMk@vRS#DiaCAXshkj`AqP@@SM* zPzQwyj2AAw5#s~XNP3p>EswFG+32`RED|MwcQmvjp;mEra!6BOiT7ev6p&TAtGyfV zMeLP$4bH-xtzc);?KpX55eFiS-J6ZJ0f9dW1U!;=rLF77RFTY}cedEmtgbxa~)t_%XnlB zV_O-$cnumr03`1(b`tytu5kXU80_EL;5r+eI(;^75qGg;=bku3aOKb_gyJ0T@dp&88c%bxy5q(3X9+m$hiDX}y=+7^rj2S9YpOsV{ti|0q!nzmsG>B6i9+EV7oCQyQ>Tonk~U47%NlSx88aPV;K+Oa*^ zHk5GPJL{9=bDkCFw-Aq0L}4zAm#J2i;8!4x+pi`|<8f@s5dcSn76@;BU3GFyP;%z~ zJDMMEf5A&u^1L#Ove$WlTn!#r*h5^Ho^mUAIGRmBFTzzM+ZE1>S*FKE1}Od84xp)n zGdBS-zXr_h6nhe75qz37$a^Mz#%_;#$6PkvZb0CVhQQsifpGdwxlvqM8xy%03PFfQ zCTsNVq)5pm2zy&=E9#@&sTh)KmnKv=B9E&W#np*7qtD)oh;2Zi2^i->}3713DzP^oNNYfF-H#!!nKI=CP2 z7md^;Y(zib2}iDN0hED`dmdeXGYKjQV5A~rAH=K!xGF8UK6~%F)g9xgx?179jmDor z_pTvbTTN0)f#e4u*h(Eq9}4V^Dbb^13>eQB6vrgvRI_HI;7ul}0b5wMZQqUxvMkwV z1|fu2F|>-Kc7=-EwY;8vF zt9I)klYq!{TU%S~KCl-Wf?Wwo&LGGtQpI?lv^~ji%peV@4jh8;Z$?UKq5`Hl#)dgt zt*zKDkfzy~Bq{e{7ZPsheUj^TJ@HM=?4ub77UmYCyQ@3uVJu7}>U~Ml6;ojr=|JP-QN#$)e~g|wrcZJ!qNLMkN73+Tou~u7RO_5tS5Qw<-vSp zPX+d#gLHND>k0wZ?!+<5ayvr*o%D<_ix>j z9_StYmDjL0tlAENuCJ>B7 zNA7fiHCbb$85N3orAB#ZC2r>N%Fn@wu0#ayS6ih4Glv=<*p8@|Qe9n!o_#WIK|G2X zJNf7%Wefn1zwnXA)0lGIgM5}lZ3an+w0qHS=Veg?WjPCEUWg7K&oPX(a|@)T*E0wr zIRz1)#2Zf6*90ln?c6QrYONKU1l?GUz1tdD2jyykv&5;oqKvs#k_BLMeB*_eqZJ1L zC8!DZ#Kf7VY-xq4nC3)eRtGVW=t}&%*(ZaP3{Yx-_!+f7h@|7ZEiffVZh@z2yrfiD zIqnqFs;V%(sk%8oJxHz<+%K3#v%VTU*|-T0BIb9qtA`ky1)LwsNT;$TsSA79OH}0Jd2Sy zQk6&Mo>IdT$?DmjW{>r_+YN)#fQOw#jg;ysAyUB!ly$C+I2D~nhE0|ft8 zF&QL~}z*oo>9AmiD>+(>Bj&9*#?Qy7Ass0+K0dyEoO`9Yg1BBE>4W;XF9P zF;q;Z0ja^5?&n!tD@F1?JSnmq{?h{b&o@n}Gm=#_xLMX(aO0*MI3Ra=Sb zgNb>(XF&XAl&_sM8RHxtI)y}iW4nOHprW$ZRcdrB#{RYzyuipqb)`M(vG<60MAi>U}+l!yv|T2$TeNW??0{_Lfi) zQg13_R6JjtlG}UE`9THhS(rn;s@K=n=Eg**RgYb|-jO71iB~cEYT6gqKF;173|or5 zVg;kN?N0iEK`5QWks%1k&mr|aC;P z5#`zRsU2`-nfmnno&%BKL?`&^2$2UoA#peZ6XQhr=GEc+c;mNVx;o3Es0?OkgMg$x z`EmK@M}mOlmY~XL+siRU^oAQ%-0{6c!q|>_;x|Ni1`ZmNk&pp9WT2S{Ox-$olLsvG zi)cBa4W2J;CaSs&YBTt59?t8%Pu!hbEq4n6QOBFq6!@6%;2LCyvKTc=2{9(bR&b4v zZl3snrZ<N0@fHvgfM~1t(1UCuTv6{XgFWFTcsn{ zm-9wwlk+_C?60y?;o69Hu?gsp9lM+i?pA& zZ%X$h=7Xy}F<#?2gOszE!k;nN6Cu7nuO-*-J~|OvhC#`kHG6U0!qMv;+k~1<#-Ko9 zxU9k1@3?lIfY^`Y>kchEvLG%qNJ48!CGMSu{~Cyf53ith9nMY9LTGR5pCNf=PF~=E z?=<1c`GAwCYNQZZlip@IQ2+s1&PBz_q_l>u(6{?MJ&*GVDymdocokI_=W30!n4BtH z3SF~A#OTw8K3a@{r8d0wmYt) z!l_1eF^7t01jSe9&s9pea(!;Xot72)xCJPv!pF*DL%8pVVrMjl(X zWV?L;=U6g2@w09nLq;mefQ-96Cl7UvVf|`uJcG_zI^Yl-@W&I z_ndp~d%e6PZ_Q+_k>B_M=JxJZroEx{&} zSsW*dS^kt%2}gu?6b)Ougc?8eTk-XHehJb>G##~>7KY;^N&(l>sGy-8C1?XVhXkXL zl2;P*(Luq=hum~^;E1No!nq175v*K90xJ*2QBd`lAh=ag4hd~-KGhNW5x--iC|4$E zE+Ih0hjP;-Kx7>>oj6Or<(R<#aw#Ihj&4WgRx+AoL?Ix4v=rs3nl$1W7sxHJghEE4 zM5Hp$M!!idBv`I{xT+6Lg5(~bvB4V+HgTGD7d2uC#`{+khjL{AIt78r!9hoB81v0GJ!^Y zSYcO-|H0}PX*T#b@Z<&hLOHE+NyBd?uCB2R27W6{$X6e6gv#?McKK85I*=&y(l95HbZI@bj2&7nt>Z_(^dq68zVgZ|wsYq$ zyo$DlhkD)BW5nt1_gLtZsYLIln1yJx$t#DAczS9gydmqeFT7w)tf^?z*X!&Lxpa3Z zsvh}kO+#iQ4Fo;wM-Npzu&sv|MD4PBY#lN%4KXCA1SkD<@Pl;Sg^{8aMw?rEx{|+y-XP-W7ZS6cF9U_#g=x&H(*Y4dc zfU{E{Zy*hSne?E%ARykS9sPEK5LhDGbt;pTqi79iof3Q?nM#Iv;HF=21lC00zAa(R zAdhxKlbjq2;d(W#TtD?E6M+_6Z(h3+rb`D9xOykLcIn=_el6^5?#7+8z77{cG=^4O zBju&igcr_DeAx9S!O!u^MQsp!`;FJxWR|t#PaU=WJ9_NPU-+#3=;uB|c-$xS2thR0L^?7d}MoJ-R+ydeY$Hb`)X0KwheNzmZ#&fxAY!GmjXcLqpscbCE4WfD z^L0Y?_VrpD#=0<*J%wPbsO4soLYIdYnYVC1Sr;u>t($ z;iF$l+n}RE5P4u@yQIi#&)_JmXmAzVixioOX3$O@Z)Kw?EjwTRUt7PVDz{(lxEa*= zLj0e^pUyJv$-8cvT8$Tb$?R#6CiJ-|{B~j%78|2d0yf7@9z9Po{rj1m)~zO@g*~Qa zc0|s@Lp`W!jKIS?)F(JjP*gIgMIBErq2S=<_owsfyWTHRV*BQ%dY>f$n6?_fV69TR3kn&11C zVRNaphfmF&oo>dpc^WuG&YM>k48|_OxEXz?j9-W^-d8Jii4N>N; zGU}Rlt={fz%Or#pLD%OrV!vZ6Fy}6vTx98}`K`32g@-UhalRkaISyrO(d%wy@JVojsbbvy2{-xNjuzbCl<-dMgGhg&O z`7lV{@={4ZPl+(;w_@ZJd_mo!vq4?PTMYD+B~kEdDQgSaMJ}M!&rM2~jWaSZ0V+^3&D1${_5@5>KJNFux9Vg`OwX?TUx!g#_xgv>^Bn>Lf zm}1%GaM-(4Yn_nd-qQ#YiLzAM@rXQXtQirHlTT@0dO*fqBIAjTvS76&YZpLNUQQp= z_c}gn%xo8}z&z3`K6|PBIBsk{`=oNMaH-6NTQ0X}HmxN6<^V?oBfS|+L1vMNkTdWk zN~saDGL?OJq+8H<${S+f3BKk5KDcpl%!i_?P`0559(BxvFvFG}X-O2=pq7bgXb{#k zbMUw^(xm2$b0!7FR55#dyAsqI&xqJW8JbQuE}!v0>G0*=RmMik6A@JV0Fal7bD4tg zH{#0Zc58zMjhiP?-fkf{D!#<`ePhyQ>by? zx%n6HOh{tsHbE7X2X%%?0GvFTcpK_!|HqoAmTLRZ>ECCA_HN$7D3SnjA z%LS;&=;O$;G(sYwgRmU7#!<|ET~HCO&qo7RRiAwuoh;*|_LS45D!UIew&9I;pV62r z%FP5Nb{#i9VsZWuN+$}Wq9eh@2-S*JHWNyIc>4RlS|MWSiv-6@uSF=3*YhlZq_(=q z7LmJ6Y5Bzr)5^F?YF!0`^)UAJ`F|mUfDm}|B_1!8Om>I#cbk2}w?bVWN1sdW4VzO$ zA18n&DfhY}ntq(7$^st=1`GPxdofZ3z$%atE@|FX=@6zzmHe+t4+aHb4N3Ko2yZ%2 zUY&!8VWhh-?$ZDcun%bgceekT9y1sYu%0tIQ=`!T3j1Kl`y+#JZ5Aj0nTfIQX5xB~ zzTf+!4sb%$a9JWk)SH4j|Cu>1?`F<5&h;h#zlnd&O1=qq$qEBq;``4mqg(;@?RIF|9#E_8yC>^h$)kfaQ)d4)*iYOzTSb`-U;pgb>xeWjQnLig}iaE<2awa zJ?pF2hrI1j)l{)QsNg~-GOmZw-1bjg)vsay`|){T3B&*WCXe(V$?~{STO z*0iMU(R_bDhTUhRp2fV}waC-QhKeuxb$4Nd-*Bb*da)#yq;5K0ivy>pf2-8{ zT1`o#+-2-#?U^}4sJtc0pG=25`iJ_zKTEVCJ=Xo z%J#-J?abq00xGoqjuJwHf>SL7T(EfD}AL!Tz`JOHnG2ersig-`2^t$JUSBk~ova zKilVtvDM?uRufmY0f$Y1)9fW}BuFDAaS+}GU-3QR-je|_@P-nAC`Qd-0?CK7Qq@>k zWCF94H_Uf`xAwB`@mkgn;`(J_q_-@pBHj*JYelHar>@j9C5wIUVy@|{*RI#$OOsYK z!;rCZ&hQO+T^%r}n`f!Pq*pI1BZhoFqi}n+LD=!VvVmy1ZcQlks~o}5XM&=Czuo+9 z?%;2RC>YcOQD}^Q;s`AY_aXWsK-6>i#qJt5+Yc?5O!%Y`fpvCR zEb|qh*th1G(n1OhGoAXXi&$~a$5DB0eh0oUSM~ypD*E9#gWS&7mz>?Ip-r4qQ=IX=wvOu)8~7mj zF?G(%>7u(V=t=WH1_M*;v$?u*c3D$YGeR$-eTDc3k#Szn2kP%{Ur&bi9A5Nlo!k4! z+c~xx4R$An{y3()SR?A=On#;m%jP&6qIc~3z=OE0N++&s1&E5*0CD%1Q~%X6elUZX zB?zfk3U=$|-oh!DU}%U}Lo-SeUrF2a?*4HFkolb5A)#$a5oZ1)nGU{Qb^Q(@`nIc( zwi)MjI58**&tddy@Qef0k4RLNWX7oYn|hL(!r1p4G|#@CCqH1pqp(jkE|&wHUp!f% zy#7cGg^&X+N-12Wi;7XBhV%_m=Z$DFy-gupIv*SjA_e2#=Y=}m)_AFFjCo-?BzFy@ zQi1JJM~Qh!Z8O!>!;TsKPq@DY(u}fz*Tfqd_xA!V4bKe>LrGBBhvUp*81zOX$&Sx; zBn+-uvd?-Le|*aMGf1k`lZv#fabC!S+{_++RMM9tGq9!5T_L?tf?9KFHOkpU{zhk< z9=Wx-e+TCJVGP{=r29G^kH5fqG26jP+@rLUsoHa2X_wJxi)&J?N~Xv0#H(OIvQ9-# z{-)^;2Tm8E5Xac=EuYmIomWGssqhPV%+6Cjv9TF|T=iW|_X5a@osX+$^h65f_YN3Shn3xWvAIlU*^#+~(M8P&tAx1i(@#UpDhMEF zRK)4{%7lfQMX0~h^T)@{05}x5mtzzxWYIrlGijIpNoULhj=N-y4K?}znZ(EeZ}M4(iz;)iyHpJ2llpLjf0{Cz7vSV@h2{+m_B z8O7&(iJs`kPX}b!e{kW5JwyFNly%0&J~JwMRq+ezPeH`ls|FInPN?3)?_p^XXTbC%-5E^7tQk6l25FzZl3#L@dCmwmd@}uMAis;~)6?Yfp+PGw+2hPlzsbdz31__HhPS4wjJ&K_S*TS%I32BW?Of$!2)mIpzWUe0fE7Oe_W&o?%#hNdTV7{m z1TX2C955T((bN65Ze8KFG@5^<7FOfg$CjCY01|1MA zNoau{%J5GXlXko^IH;)d)8|nhnz?pQLl1IHeyD-C6|==OCj$e)a?yG(AHBUcn8<7$nTXO;Qw-=P*r_|ma@T~keR#RRP)-GK3((aJjoE#GV zmv#K1T!2hxhEsHG1CwOVq@e2%JUsmA^UVr)ex1n5F-b#gXMHzBu+^TqPuHTR!#k{9 z5P!6cF&K~wyA|OuGAm8}#iQKjNQD!z8TKZ-^Y4T7nmD1z97on?Vx`{faYsQlt!Yfb><2HqsG z=bY0T6ctfQ`sbKJXB!ai*x~n2xlX(xf8hyDUKBYoF@}ruUf4d>2xWW<`f)VQK0HNL z!uNgsI^5-xGs7LFld=%SEcT7r8}>~iSYLXHx=Z{;TVjkM^h=jy=0Cr+2YzIwIw)_b z2-@ILelvIx)%IcZZ-Ci(IM*?$)qzL~B}Vh#klyeE5rsPJo9Efmd7O_h_b?*U=T*>n zYx=P2hkd4VmK#RhPFJnmp{sS!Y(5}D6R?idIPn6-CfEMA*Fu`kM~gI!-M)2sNK;uu zHrpQXDy2$p;HTT!vnN?Ty5JSaObZtR#cvKH)~NJ*!!9ra_zGLxn8-PWO`A1`oJp;T zCk0gb+cLmr)K`w?aUrvF;#5j6@%r6&39T&8n)SNFi&<1xE9mF}!OMqmt}bKReD&r8 zR&Y(`+*LP?E?HV6!M~(M_Q~*(7A>W)J`H!_nfaR15TBL-Ga0eqk1C0mz$SuKF^E{6 zZdLLR1>qm*TBGpyRc212KUV<~ZFIE=!gn*fgkdA@|PQ+Pjncc&L z58(LkD^ratP2S##FQ=UwM#2wK=EApoS@KF@fh&?Lox(sx!8Pm5)=b;8$E0kYMMH$< zIZa^_lcfxb5B4K`naE95Uo708u9}=xriHG2Q0)e;0PpH2V^n~jSI&sNN$uz92UU&j zjHk}iU1;XC^WiKF4U#8^hqV*2ZYy+(Tw~6`cksZ;YP}}HnuhKza>5+E=A3E;{)nlT z!A%CnO;qW%if;G2Vs(c!3os9=^88}St!<;*F~g7Kzjg1W8(4A)2Sl@S*d4;CsHi}O zwRUkDh$Rug9&xZaC;7v;e3s+3%x(g7#T2lLP%ZqpBtNdrO(=^_#A6s7Y+@v}w6qoDG_W{q-h*mfHuwzoFZrxyht)gyMGK~M6FR>Io(VTz#>fr zvx(qoUHhjdmnBmp_!tE8p_i5n5a%8nqUyM%MfCPk_XqE)x;iv5+uqM@OHCl|0RDaS zW7c64x0V%xcXgA;tn**9l3`rK4mmWsIwlibSj>tWL>7#8?usR@jhripJ%m#|zu`(p z=wSF^Ut3#NE?8DWXNUek7MD>;yn%?gx4iEqol%bC*oJMa)<$e&&cr7vg=AqCkz74+ zdTrPdbe~~(skTfJ8zE3W)C!7?wcexjiu3v>&Nvn7YUn|E+6ngL!R^uyTCqbpp(%u^=b9E z7?tVJ?`090hYMx$T04S>3`H>+?E&X-wE7ReQLeT21u*iW4YiO~clf+Cqcy{bCE8gB z@3a&>7Q*=XMF1As2GKlEuVs~m%BIyWJGC+fElhTGrCS(u6%-KL$fcBOI3qP_ymIgqZF+lP-__lRNhc z8l2;28}1hmFD<%Bl4J#VJv5`>Z&@U@`|9vMCs~cPhr*nmy|0OH*RVQkj?c=(%l2W2?BLZj(RK5F;D@@jNlNdF#Oi zfKiqQ;8!|abxKbZG476UPMVw-++<4E{La1$+b&e=HLvlzYpXb81@qaoH`^_@d*6|@ zmjpKi3+%iLi!k!TZxKDsm7Egi5IF2i=z-#be7E{a@UE@APj+R^x32`q4G}U_O4@G* z2*HtLud7ka+OfBoVF&5tXWcPgVfe>~=~^r6?Za-+H5+-vpx-ZLqzx5G;#3{KeQqxo z+$$D-SfV!o#DshHz8l<=+ZEI`ffjN7?vb|=xa@12576R3R@CSiM*#M_g4X7xJj(D7 zI4@)7{L3_s8|#W2!^=&r?nivcd3v~03SUBM4BCzT?@~8F%rDQ1VN1{M8(yP+&(fWy z;Q$!~4Aq5d-R?yDIX}6afa_sc?e*Lt4DyGGqgl7@MOH%5P5Y(yoA0!1nW1)M63Aw2 zx{V9{NsHsGzBiQUUCaTr?X_J@){oiqL}p?bQbN0mPh5xf+mC7UMn(AXV@^H`iTMJB zMM5`+sYPP5r->WZA2?xI$7zm<>RMFU!0dDSg z4MM|(yH>|l-ielR?%l4)8D`uBo(@%=PUF;%)TjNVZy1kC-3mE^_V3CZ!sD=945&g2 zOa}R#oGUE)$ts|wju!+7^XvU2FywVwWPL_c8eV$nB$iW5EJMU}AE)y7&CFzN!k>&= zzXJ$&8jAc926b*dnp}P!JQnvsdVhZQ^Q}(P8!3}!Bd6GB%PaK{u0vxZ+A@9Qd2#oH z(!nBWt2G+xn7ag11v_JCi+8Mie5UO#XHw?08S}|x2;&qpX z*e&oXGlA%I(7mY3!hG*JWAg;EJZFX70HG(CTjZkV@NZ`dVxGTo>@P+NIIS&_6S>lZ0<-}L(=!xyND|W zIXF0YNCdj!@2l1zAKbs)j%>s(R=h{ihM#Yj-(yLO?bQUI_YomHv;DQ@L+1spPfiFXO*Isp5Je>OTviCcyJ_j;?``p0E ztN~jQkak;tK@HftVDG`+Ybv1P(xD5&mxXTjkla$=XYisOS+o^&(6sgM}JKh zArJ9Il-|3UukpolYBTb_7i8nHr6F$|Ut4QRfBa0Yp04xF(sjnL1^ujeQYp1U1G0XP zLY=7%8fBt3MFY%?%v=9sv4#)2Ej^104qcFC{Mh41ReIYE^aLwBS z9wbP)KR%v5R>9bq%*-w?O9?Q128okmg%K`P|EK}pHoRRZ=I$rJ9~I%qA^_>A(GKEn ze(jP9WV=!VzQ#T#5 zwk^`w*8+x33^Fm!x-+p-X3tv7Fd21yK2hb$IWC zmTu+Kf(XdzaG2AJtg#L_E(+ykR1$b?al(->bP_=NPs^ieh!{zLhdp}4t>LdBVO~f# zTQ>CCL(NGiOLJ@;MJ`dVc)cfT4RrGRGlj}Ur6CF?fo!D|| zzoZbF?TiMZuiyva*e{?BT!|$wNpYY|n8%{WtaotLAFkd9hTn%DV6SnR;SNuO$QJ=_KFXqt(jXIsr$B zb78W%o(dKb@b+_9W-Irs{S?aK0>P}8sWQv#^edZ!ibxC0#;X7RqKYQQJ|t!qs)b8X zNq62T{J3zv7G*S3I^5O#TH|j~@-BL1JZbc39V}q!UaNszxc3>U5uPZth?Eo`HqBP$ z?Oyz#I5yxm!MIpu0c@p$7)@O1FY^LjY8AHD?2+*(HJN1P$FQ

*yt5agAm}UT$?R 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 `echo_message` +- installing plugins installs just new plugins. Already installed plugins aren't + updated. +- add 'update plugin' binding and functionality +- add test for updating a plugin + +### v0.0.2, 2014-07-17 +- run all *.tmux plugin files as executables +- fix all redirects to /dev/null +- fix bug: TPM shared path is created before sync (cloning plugins from github + is done) +- add test suite running in Vagrant +- add Tmux version check. `TPM` won't run if Tmux version is less than 1.9. + +### v0.0.1, 2014-05-21 +- get TPM up and running diff --git a/tmux/.tmux/plugins/tpm/HOW_TO_PLUGIN.md b/tmux/.tmux/plugins/tpm/HOW_TO_PLUGIN.md new file mode 100644 index 0000000..9901619 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/HOW_TO_PLUGIN.md @@ -0,0 +1,2 @@ +Instructions moved to +[docs/how_to_create_plugin.md](docs/how_to_create_plugin.md). diff --git a/tmux/.tmux/plugins/tpm/LICENSE.md b/tmux/.tmux/plugins/tpm/LICENSE.md new file mode 100644 index 0000000..1222865 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/LICENSE.md @@ -0,0 +1,20 @@ +MIT license +Copyright (C) 2014 Bruno Sutic + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tmux/.tmux/plugins/tpm/README.md b/tmux/.tmux/plugins/tpm/README.md new file mode 100644 index 0000000..fe90855 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/README.md @@ -0,0 +1,101 @@ +# Tmux Plugin Manager + +[![Build Status](https://travis-ci.org/tmux-plugins/tpm.svg?branch=master)](https://travis-ci.org/tmux-plugins/tpm) + +Installs and loads `tmux` plugins. + +Tested and working on Linux, OSX, and Cygwin. + +See list of plugins [here](https://github.com/tmux-plugins/list). + +### Installation + +Requirements: `tmux` version 1.9 (or higher), `git`, `bash`. + +Clone TPM: + +```bash +$ git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm +``` + +Put this at the bottom of `~/.tmux.conf` (`$XDG_CONFIG_HOME/tmux/tmux.conf` +works too): + +```bash +# List of plugins +set -g @plugin 'tmux-plugins/tpm' +set -g @plugin 'tmux-plugins/tmux-sensible' + +# Other examples: +# set -g @plugin 'github_username/plugin_name' +# set -g @plugin 'github_username/plugin_name#branch' +# set -g @plugin 'git@github.com:user/plugin' +# set -g @plugin 'git@bitbucket.com:user/plugin' + +# Initialize TMUX plugin manager (keep this line at the very bottom of tmux.conf) +run '~/.tmux/plugins/tpm/tpm' +``` + +Reload TMUX environment so TPM is sourced: + +```bash +# type this in terminal if tmux is already running +$ tmux source ~/.tmux.conf +``` + +That's it! + +### Installing plugins + +1. Add new plugin to `~/.tmux.conf` with `set -g @plugin '...'` +2. Press `prefix` + I (capital i, as in **I**nstall) to fetch the plugin. + +You're good to go! The plugin was cloned to `~/.tmux/plugins/` dir and sourced. + +### Uninstalling plugins + +1. Remove (or comment out) plugin from the list. +2. Press `prefix` + alt + u (lowercase u as in **u**ninstall) to remove the plugin. + +All the plugins are installed to `~/.tmux/plugins/` so alternatively you can +find plugin directory there and remove it. + +### Key bindings + +`prefix` + I +- Installs new plugins from GitHub or any other git repository +- Refreshes TMUX environment + +`prefix` + U +- updates plugin(s) + +`prefix` + alt + u +- remove/uninstall plugins not on the plugin list + +### Docs + +- [Help, tpm not working](docs/tpm_not_working.md) - problem solutions + +More advanced features and instructions, regular users probably do not need +this: + +- [How to create a plugin](docs/how_to_create_plugin.md). It's easy. +- [Managing plugins via the command line](docs/managing_plugins_via_cmd_line.md) +- [Changing plugins install dir](docs/changing_plugins_install_dir.md) +- [Automatic TPM installation on a new machine](docs/automatic_tpm_installation.md) + +### Tests + +Tests for this project run on [Travis CI](https://travis-ci.org/tmux-plugins/tpm). + +When run locally, [vagrant](https://www.vagrantup.com/) is required. +Run tests with: + +```bash +# within project directory +$ ./run_tests +``` + +### License + +[MIT](LICENSE.md) diff --git a/tmux/.tmux/plugins/tpm/bin/clean_plugins b/tmux/.tmux/plugins/tpm/bin/clean_plugins new file mode 100755 index 0000000..12f8730 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/bin/clean_plugins @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +# Script intended for use via the command line. +# +# `.tmux.conf` needs to be set for TPM. Tmux has to be installed on the system, +# but does not need to be started in order to run this script. + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +SCRIPTS_DIR="$CURRENT_DIR/../scripts" + +main() { + "$SCRIPTS_DIR/clean_plugins.sh" # has correct exit code +} +main diff --git a/tmux/.tmux/plugins/tpm/bin/install_plugins b/tmux/.tmux/plugins/tpm/bin/install_plugins new file mode 100755 index 0000000..c66b15b --- /dev/null +++ b/tmux/.tmux/plugins/tpm/bin/install_plugins @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +# Script intended for use via the command line. +# +# `.tmux.conf` needs to be set for TPM. Tmux has to be installed on the system, +# but does not need to be started in order to run this script. + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +SCRIPTS_DIR="$CURRENT_DIR/../scripts" + +main() { + "$SCRIPTS_DIR/install_plugins.sh" # has correct exit code +} +main diff --git a/tmux/.tmux/plugins/tpm/bin/update_plugins b/tmux/.tmux/plugins/tpm/bin/update_plugins new file mode 100755 index 0000000..30a5646 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/bin/update_plugins @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# Script intended for use via the command line. +# +# `.tmux.conf` needs to be set for TPM. Tmux has to be installed on the system, +# but does not need to be started in order to run this script. + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +SCRIPTS_DIR="$CURRENT_DIR/../scripts" +PROGRAM_NAME="$0" + +if [ $# -eq 0 ]; then + echo "usage:" + echo " $PROGRAM_NAME all update all plugins" + echo " $PROGRAM_NAME tmux-foo update plugin 'tmux-foo'" + echo " $PROGRAM_NAME tmux-bar tmux-baz update multiple plugins" + exit 1 +fi + +main() { + "$SCRIPTS_DIR/update_plugin.sh" --shell-echo "$*" # has correct exit code +} +main "$*" + diff --git a/tmux/.tmux/plugins/tpm/bindings/clean_plugins b/tmux/.tmux/plugins/tpm/bindings/clean_plugins new file mode 100755 index 0000000..9a0d5d7 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/bindings/clean_plugins @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +# Tmux key-binding script. +# Scripts intended to be used via the command line are in `bin/` directory. + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +SCRIPTS_DIR="$CURRENT_DIR/../scripts" +HELPERS_DIR="$SCRIPTS_DIR/helpers" + +source "$HELPERS_DIR/tmux_echo_functions.sh" +source "$HELPERS_DIR/tmux_utils.sh" + +main() { + reload_tmux_environment + "$SCRIPTS_DIR/clean_plugins.sh" --tmux-echo >/dev/null 2>&1 + reload_tmux_environment + end_message +} +main diff --git a/tmux/.tmux/plugins/tpm/bindings/install_plugins b/tmux/.tmux/plugins/tpm/bindings/install_plugins new file mode 100755 index 0000000..3ade3c4 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/bindings/install_plugins @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +# Tmux key-binding script. +# Scripts intended to be used via the command line are in `bin/` directory. + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +SCRIPTS_DIR="$CURRENT_DIR/../scripts" +HELPERS_DIR="$SCRIPTS_DIR/helpers" + +source "$HELPERS_DIR/tmux_echo_functions.sh" +source "$HELPERS_DIR/tmux_utils.sh" + +main() { + reload_tmux_environment + "$SCRIPTS_DIR/install_plugins.sh" --tmux-echo >/dev/null 2>&1 + reload_tmux_environment + end_message +} +main diff --git a/tmux/.tmux/plugins/tpm/bindings/update_plugins b/tmux/.tmux/plugins/tpm/bindings/update_plugins new file mode 100755 index 0000000..28cc281 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/bindings/update_plugins @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +# Tmux key-binding script. +# Scripts intended to be used via the command line are in `bin/` directory. + +# This script: +# - shows a list of installed plugins +# - starts a prompt to enter the name of the plugin that will be updated + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +SCRIPTS_DIR="$CURRENT_DIR/../scripts" +HELPERS_DIR="$SCRIPTS_DIR/helpers" + +source "$HELPERS_DIR/plugin_functions.sh" +source "$HELPERS_DIR/tmux_echo_functions.sh" +source "$HELPERS_DIR/tmux_utils.sh" + +display_plugin_update_list() { + local plugins="$(tpm_plugins_list_helper)" + tmux_echo "Installed plugins:" + tmux_echo "" + + for plugin in $plugins; do + # displaying only installed plugins + if plugin_already_installed "$plugin"; then + local plugin_name="$(plugin_name_helper "$plugin")" + tmux_echo " $plugin_name" + fi + done + + tmux_echo "" + tmux_echo "Type plugin name to update it." + tmux_echo "" + tmux_echo "- \"all\" - updates all plugins" + tmux_echo "- ENTER - cancels" +} + +update_plugin_prompt() { + tmux command-prompt -p 'plugin update:' " \ + send-keys C-c; \ + run-shell '$SCRIPTS_DIR/update_plugin_prompt_handler.sh %1'" +} + +main() { + reload_tmux_environment + display_plugin_update_list + update_plugin_prompt +} +main diff --git a/tmux/.tmux/plugins/tpm/docs/automatic_tpm_installation.md b/tmux/.tmux/plugins/tpm/docs/automatic_tpm_installation.md new file mode 100644 index 0000000..630573f --- /dev/null +++ b/tmux/.tmux/plugins/tpm/docs/automatic_tpm_installation.md @@ -0,0 +1,12 @@ +# Automatic tpm installation + +One of the first things we do on a new machine is cloning our dotfiles. Not everything comes with them though, so for example `tpm` most likely won't be installed. + +If you want to install `tpm` and plugins automatically when tmux is started, put the following snippet in `.tmux.conf` before the final `run '~/.tmux/plugins/tpm/tpm'`: + +``` +if "test ! -d ~/.tmux/plugins/tpm" \ + "run 'git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm && ~/.tmux/plugins/tpm/bin/install_plugins'" +``` + +This useful tip was submitted by @acr4 and narfman0. diff --git a/tmux/.tmux/plugins/tpm/docs/changing_plugins_install_dir.md b/tmux/.tmux/plugins/tpm/docs/changing_plugins_install_dir.md new file mode 100644 index 0000000..27de96d --- /dev/null +++ b/tmux/.tmux/plugins/tpm/docs/changing_plugins_install_dir.md @@ -0,0 +1,16 @@ +# Changing plugins install dir + +By default, TPM installs plugins in a subfolder named `plugins/` inside +`$XDG_CONFIG_HOME/tmux/` if a `tmux.conf` file was found at that location, or +inside `~/.tmux/` otherwise. + +You can change the install path by putting this in `.tmux.conf`: + + set-environment -g TMUX_PLUGIN_MANAGER_PATH '/some/other/path/' + +Tmux plugin manager initialization in `.tmux.conf` should also be updated: + + # initializes TMUX plugin manager in a new path + run /some/other/path/tpm/tpm + +Please make sure that the `run` line is at the very bottom of `.tmux.conf`. diff --git a/tmux/.tmux/plugins/tpm/docs/how_to_create_plugin.md b/tmux/.tmux/plugins/tpm/docs/how_to_create_plugin.md new file mode 100644 index 0000000..b1a68f9 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/docs/how_to_create_plugin.md @@ -0,0 +1,108 @@ +# How to create Tmux plugins + +Creating a new plugin is easy. + +For demonstration purposes we'll create a simple plugin that lists all +installed TPM plugins. Yes, a plugin that lists plugins :) We'll bind that to +`prefix + T`. + +The source code for this example plugin can be found +[here](https://github.com/tmux-plugins/tmux-example-plugin). + +### 1. create a new git project + +TPM depends on git for downloading and updating plugins. + +To create a new git project: + + $ mkdir tmux_my_plugin + $ cd tmux_my_plugin + $ git init + +### 2. create a `*.tmux` plugin run file + +When it sources a plugin, TPM executes all `*.tmux` files in your plugins' +directory. That's how plugins are run. + +Create a plugin run file in plugin directory: + + $ touch my_plugin.tmux + $ chmod u+x my_plugin.tmux + +You can have more than one `*.tmux` file, and all will get executed. However, usually +you'll need just one. + +### 3. create a plugin key binding + +We want the behavior of the plugin to trigger when a user hits `prefix + T`. + +Key `T` is chosen because: + - it's "kind of" a mnemonic for `TPM` + - the key is not used by Tmux natively. Tmux man page, KEY BINDINGS section + contains a list of all the bindings Tmux uses. There's plenty of unused keys + and we don't want to override any of Tmux default key bindings. + +Open the plugin run file in your favorite text editor: + + $ vim my_plugin.tmux + # or + $ subl my_plugin.tmux + +Put the following content in the file: + + #!/usr/bin/env bash + + CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + tmux bind-key T run-shell "$CURRENT_DIR/scripts/tmux_list_plugins.sh" + +As you can see, plugin run file is a simple bash script that sets up the binding. + +When pressed, `prefix + T` will execute another shell script: +`tmux_list_plugins.sh`. That script should be in `scripts/` directory - +relative to the plugin run file. + + +### 4. listing plugins + +Now that we have the binding, let's create a script that's invoked with +`prefix + T`. + + $ mkdir scripts + $ touch scripts/tmux_list_plugins.sh + $ chmod u+x scripts/tmux_list_plugins.sh + +And here's the script content: + + #!/usr/bin/env bash + + # fetching the directory where plugins are installed + plugin_path="$(tmux show-env -g TMUX_PLUGIN_MANAGER_PATH | cut -f2 -d=)" + + # listing installed plugins + ls -1 "$plugin_path" + +### 5. try it out + +To see if this works, execute the plugin run file: + + $ ./my_plugin.tmux + +That should set up the key binding. Now hit `prefix + T` and see if it works. + +### 6. publish the plugin + +When everything is ready, push the plugin to an online git repository, +preferably Github. + +Other users can install your plugin by just adding plugin git URL to the +`@plugin` list in their `.tmux.conf`. + +If the plugin is on Github, your users will be able to use the shorthand of +`github_username/repository`. + +### Conclusion + +Hopefully, that was easy. As you can see, it's mostly shell scripting. + +You can use other scripting languages (ruby, python etc) but plain old shell +is preferred because of portability. diff --git a/tmux/.tmux/plugins/tpm/docs/managing_plugins_via_cmd_line.md b/tmux/.tmux/plugins/tpm/docs/managing_plugins_via_cmd_line.md new file mode 100644 index 0000000..7aefd7d --- /dev/null +++ b/tmux/.tmux/plugins/tpm/docs/managing_plugins_via_cmd_line.md @@ -0,0 +1,36 @@ +# Managing plugins via the command line + +Aside from tmux key bindings, TPM provides shell interface for managing plugins +via scripts located in [bin/](../bin/) directory. + +Tmux does not need to be started in order to run scripts (but it's okay if it +is). If you [changed tpm install dir](../docs/changing_plugins_install_dir.md) +in `.tmux.conf` that should work fine too. + +Prerequisites: + +- tmux installed on the system (doh) +- `.tmux.conf` set up for TPM + +### Installing plugins + +As usual, plugins need to be specified in `.tmux.conf`. Run the following +command to install plugins: + + ~/.tmux/plugins/tpm/bin/install_plugins + +### Updating plugins + +To update all installed plugins: + + ~/.tmux/plugins/tpm/bin/update_plugins all + +or update a single plugin: + + ~/.tmux/plugins/tpm/bin/update_plugins tmux-sensible + +### Removing plugins + +To remove plugins not on the plugin list: + + ~/.tmux/plugins/tpm/bin/clean_plugins diff --git a/tmux/.tmux/plugins/tpm/docs/tpm_not_working.md b/tmux/.tmux/plugins/tpm/docs/tpm_not_working.md new file mode 100644 index 0000000..bfa14ac --- /dev/null +++ b/tmux/.tmux/plugins/tpm/docs/tpm_not_working.md @@ -0,0 +1,96 @@ +# Help, tpm not working! + +Here's the list of issues users had with `tpm`: + +


+ +> Nothing works. `tpm` key bindings `prefix + I`, `prefix + U` not even + defined. + +Related [issue #22](https://github.com/tmux-plugins/tpm/issues/22) + +- Do you have required `tmux` version to run `tpm`?
+ Check `tmux` version with `$ tmux -V` command and make sure it's higher or + equal to the required version for `tpm` as stated in the readme. + +- ZSH tmux plugin might be causing issues.
+ If you have it installed, try disabling it and see if `tpm` works then. + +
+ +> Help, I'm using custom config file with `tmux -f /path/to/my_tmux.conf` +to start Tmux and for some reason plugins aren't loaded!? + +Related [issue #57](https://github.com/tmux-plugins/tpm/issues/57) + +`tpm` has a known issue when using custom config file with `-f` option. +The solution is to use alternative plugin definition syntax. Here are the steps +to make it work: + +1. remove all `set -g @plugin` lines from tmux config file +2. in the config file define the plugins in the following way: + + # List of plugins + set -g @tpm_plugins ' \ + tmux-plugins/tpm \ + tmux-plugins/tmux-sensible \ + tmux-plugins/tmux-resurrect \ + ' + + # Initialize TMUX plugin manager (keep this line at the very bottom of tmux.conf) + run '~/.tmux/plugins/tpm/tpm' + +3. Reload TMUX environment so TPM is sourced: `$ tmux source /path/to/my_tmux.conf` + +The plugins should now be working. + +
+ +> Weird sequence of characters show up when installing or updating plugins + +Related: [issue #25](https://github.com/tmux-plugins/tpm/issues/25) + +- This could be caused by [tmuxline.vim](https://github.com/edkolev/tmuxline.vim) + plugin. Uninstall it and see if things work. + +
+ +> "failed to connect to server" error when sourcing .tmux.conf + +Related: [issue #48](https://github.com/tmux-plugins/tpm/issues/48) + +- Make sure `tmux source ~/.tmux.conf` command is ran from inside `tmux`. + +
+ +> tpm not working: '~/.tmux/plugins/tpm/tpm' returned 2 (Windows / Cygwin) + +Related: [issue #81](https://github.com/tmux-plugins/tpm/issues/81) + +This issue is most likely caused by Windows line endings. For example, if you +have git's `core.autocrlf` option set to `true`, git will automatically convert +all the files to Windows line endings which might cause a problem. + +The solution is to convert all line ending to Unix newline characters. This +command handles that for all files under `.tmux/` dir (skips `.git` +subdirectories): + +```bash +find ~/.tmux -type d -name '.git*' -prune -o -type f -print0 | xargs -0 dos2unix +``` + +
+ +> '~/.tmux/plugins/tpm/tpm' returned 127 (on macOS, w/ tmux installed using brew) + +Related: [issue #67](https://github.com/tmux-plugins/tpm/issues/67) + +This problem is because tmux's `run-shell` command runs a shell which doesn't read from user configs, thus tmux installed in `/usr/local/bin` will not be found. + +The solution is to insert the following line: + +``` +set-environment -g PATH "/usr/local/bin:/bin:/usr/bin" +``` + +before any `run-shell`/`run` commands in `~/.tmux.conf`. diff --git a/tmux/.tmux/plugins/tpm/scripts/check_tmux_version.sh b/tmux/.tmux/plugins/tpm/scripts/check_tmux_version.sh new file mode 100755 index 0000000..b0aedec --- /dev/null +++ b/tmux/.tmux/plugins/tpm/scripts/check_tmux_version.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +VERSION="$1" +UNSUPPORTED_MSG="$2" + +get_tmux_option() { + local option=$1 + local default_value=$2 + local option_value=$(tmux show-option -gqv "$option") + if [ -z "$option_value" ]; then + echo "$default_value" + else + echo "$option_value" + fi +} + +# Ensures a message is displayed for 5 seconds in tmux prompt. +# Does not override the 'display-time' tmux option. +display_message() { + local message="$1" + + # display_duration defaults to 5 seconds, if not passed as an argument + if [ "$#" -eq 2 ]; then + local display_duration="$2" + else + local display_duration="5000" + fi + + # saves user-set 'display-time' option + local saved_display_time=$(get_tmux_option "display-time" "750") + + # sets message display time to 5 seconds + tmux set-option -gq display-time "$display_duration" + + # displays message + tmux display-message "$message" + + # restores original 'display-time' value + tmux set-option -gq display-time "$saved_display_time" +} + +# this is used to get "clean" integer version number. Examples: +# `tmux 1.9` => `19` +# `1.9a` => `19` +get_digits_from_string() { + local string="$1" + local only_digits="$(echo "$string" | tr -dC '[:digit:]')" + echo "$only_digits" +} + +tmux_version_int() { + local tmux_version_string=$(tmux -V) + echo "$(get_digits_from_string "$tmux_version_string")" +} + +unsupported_version_message() { + if [ -n "$UNSUPPORTED_MSG" ]; then + echo "$UNSUPPORTED_MSG" + else + echo "Error, Tmux version unsupported! Please install Tmux version $VERSION or greater!" + fi +} + +exit_if_unsupported_version() { + local current_version="$1" + local supported_version="$2" + if [ "$current_version" -lt "$supported_version" ]; then + display_message "$(unsupported_version_message)" + exit 1 + fi +} + +main() { + local supported_version_int="$(get_digits_from_string "$VERSION")" + local current_version_int="$(tmux_version_int)" + exit_if_unsupported_version "$current_version_int" "$supported_version_int" +} +main diff --git a/tmux/.tmux/plugins/tpm/scripts/clean_plugins.sh b/tmux/.tmux/plugins/tpm/scripts/clean_plugins.sh new file mode 100755 index 0000000..a025524 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/scripts/clean_plugins.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +HELPERS_DIR="$CURRENT_DIR/helpers" + +source "$HELPERS_DIR/plugin_functions.sh" +source "$HELPERS_DIR/utility.sh" + +if [ "$1" == "--tmux-echo" ]; then # tmux-specific echo functions + source "$HELPERS_DIR/tmux_echo_functions.sh" +else # shell output functions + source "$HELPERS_DIR/shell_echo_functions.sh" +fi + +clean_plugins() { + local plugins plugin plugin_directory + plugins="$(tpm_plugins_list_helper)" + + for plugin_directory in "$(tpm_path)"/*; do + [ -d "${plugin_directory}" ] || continue + plugin="$(plugin_name_helper "${plugin_directory}")" + case "${plugins}" in + *"${plugin}"*) : ;; + *) + [ "${plugin}" = "tpm" ] && continue + echo_ok "Removing \"$plugin\"" + rm -rf "${plugin_directory}" >/dev/null 2>&1 + [ -d "${plugin_directory}" ] && + echo_err " \"$plugin\" clean fail" || + echo_ok " \"$plugin\" clean success" + ;; + esac + done +} + +main() { + ensure_tpm_path_exists + clean_plugins + exit_value_helper +} +main diff --git a/tmux/.tmux/plugins/tpm/scripts/helpers/plugin_functions.sh b/tmux/.tmux/plugins/tpm/scripts/helpers/plugin_functions.sh new file mode 100644 index 0000000..f33d215 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/scripts/helpers/plugin_functions.sh @@ -0,0 +1,104 @@ +# using @tpm_plugins is now deprecated in favor of using @plugin syntax +tpm_plugins_variable_name="@tpm_plugins" + +# manually expanding tilde char or `$HOME` variable. +_manual_expansion() { + local path="$1" + local expanded_tilde="${path/#\~/$HOME}" + echo "${expanded_tilde/#\$HOME/$HOME}" +} + +_tpm_path() { + local string_path="$(tmux start-server\; show-environment -g TMUX_PLUGIN_MANAGER_PATH | cut -f2 -d=)/" + _manual_expansion "$string_path" +} + +_CACHED_TPM_PATH="$(_tpm_path)" + +# Get the absolute path to the users configuration file of TMux. +# This includes a prioritized search on different locations. +# +_get_user_tmux_conf() { + # Define the different possible locations. + xdg_location="${XDG_CONFIG_HOME:-$HOME/.config}/tmux/tmux.conf" + default_location="$HOME/.tmux.conf" + + # Search for the correct configuration file by priority. + if [ -f "$xdg_location" ]; then + echo "$xdg_location" + + else + echo "$default_location" + fi +} + +_tmux_conf_contents() { + user_config=$(_get_user_tmux_conf) + cat /etc/tmux.conf "$user_config" 2>/dev/null + if [ "$1" == "full" ]; then # also output content from sourced files + local file + for file in $(_sourced_files); do + cat $(_manual_expansion "$file") 2>/dev/null + done + fi +} + +# return files sourced from tmux config files +_sourced_files() { + _tmux_conf_contents | + sed -E -n -e "s/^[[:space:]]*source(-file)?[[:space:]]+(-q+[[:space:]]+)?['\"]?([^'\"]+)['\"]?/\3/p" +} + +# Want to be able to abort in certain cases +trap "exit 1" TERM +export TOP_PID=$$ + +_fatal_error_abort() { + echo >&2 "Aborting." + kill -s TERM $TOP_PID +} + +# PUBLIC FUNCTIONS BELOW + +tpm_path() { + if [ "$_CACHED_TPM_PATH" == "/" ]; then + echo >&2 "FATAL: Tmux Plugin Manager not configured in tmux.conf" + _fatal_error_abort + fi + echo "$_CACHED_TPM_PATH" +} + +tpm_plugins_list_helper() { + # lists plugins from @tpm_plugins option + echo "$(tmux start-server\; show-option -gqv "$tpm_plugins_variable_name")" + + # read set -g @plugin "tmux-plugins/tmux-example-plugin" entries + _tmux_conf_contents "full" | + awk '/^[ \t]*set(-option)? +-g +@plugin/ { gsub(/'\''/,""); gsub(/'\"'/,""); print $4 }' +} + +# Allowed plugin name formats: +# 1. "git://github.com/user/plugin_name.git" +# 2. "user/plugin_name" +plugin_name_helper() { + local plugin="$1" + # get only the part after the last slash, e.g. "plugin_name.git" + local plugin_basename="$(basename "$plugin")" + # remove ".git" extension (if it exists) to get only "plugin_name" + local plugin_name="${plugin_basename%.git}" + echo "$plugin_name" +} + +plugin_path_helper() { + local plugin="$1" + local plugin_name="$(plugin_name_helper "$plugin")" + echo "$(tpm_path)${plugin_name}/" +} + +plugin_already_installed() { + local plugin="$1" + local plugin_path="$(plugin_path_helper "$plugin")" + [ -d "$plugin_path" ] && + cd "$plugin_path" && + git remote >/dev/null 2>&1 +} diff --git a/tmux/.tmux/plugins/tpm/scripts/helpers/shell_echo_functions.sh b/tmux/.tmux/plugins/tpm/scripts/helpers/shell_echo_functions.sh new file mode 100644 index 0000000..ecaa37e --- /dev/null +++ b/tmux/.tmux/plugins/tpm/scripts/helpers/shell_echo_functions.sh @@ -0,0 +1,7 @@ +echo_ok() { + echo "$*" +} + +echo_err() { + fail_helper "$*" +} diff --git a/tmux/.tmux/plugins/tpm/scripts/helpers/tmux_echo_functions.sh b/tmux/.tmux/plugins/tpm/scripts/helpers/tmux_echo_functions.sh new file mode 100644 index 0000000..7a6ef0a --- /dev/null +++ b/tmux/.tmux/plugins/tpm/scripts/helpers/tmux_echo_functions.sh @@ -0,0 +1,28 @@ +_has_emacs_mode_keys() { + $(tmux show -gw mode-keys | grep -q emacs) +} + +tmux_echo() { + local message="$1" + tmux run-shell "echo '$message'" +} + +echo_ok() { + tmux_echo "$*" +} + +echo_err() { + tmux_echo "$*" +} + +end_message() { + if _has_emacs_mode_keys; then + local continue_key="ESCAPE" + else + local continue_key="ENTER" + fi + tmux_echo "" + tmux_echo "TMUX environment reloaded." + tmux_echo "" + tmux_echo "Done, press $continue_key to continue." +} diff --git a/tmux/.tmux/plugins/tpm/scripts/helpers/tmux_utils.sh b/tmux/.tmux/plugins/tpm/scripts/helpers/tmux_utils.sh new file mode 100644 index 0000000..238952d --- /dev/null +++ b/tmux/.tmux/plugins/tpm/scripts/helpers/tmux_utils.sh @@ -0,0 +1,6 @@ +HELPERS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +source "$HELPERS_DIR/plugin_functions.sh" + +reload_tmux_environment() { + tmux source-file $(_get_user_tmux_conf) >/dev/null 2>&1 +} diff --git a/tmux/.tmux/plugins/tpm/scripts/helpers/utility.sh b/tmux/.tmux/plugins/tpm/scripts/helpers/utility.sh new file mode 100644 index 0000000..de6eb35 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/scripts/helpers/utility.sh @@ -0,0 +1,17 @@ +ensure_tpm_path_exists() { + mkdir -p "$(tpm_path)" +} + +fail_helper() { + local message="$1" + echo "$message" >&2 + FAIL="true" +} + +exit_value_helper() { + if [ "$FAIL" == "true" ]; then + exit 1 + else + exit 0 + fi +} diff --git a/tmux/.tmux/plugins/tpm/scripts/install_plugins.sh b/tmux/.tmux/plugins/tpm/scripts/install_plugins.sh new file mode 100755 index 0000000..81e6836 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/scripts/install_plugins.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +HELPERS_DIR="$CURRENT_DIR/helpers" + +source "$HELPERS_DIR/plugin_functions.sh" +source "$HELPERS_DIR/utility.sh" + +if [ "$1" == "--tmux-echo" ]; then # tmux-specific echo functions + source "$HELPERS_DIR/tmux_echo_functions.sh" +else # shell output functions + source "$HELPERS_DIR/shell_echo_functions.sh" +fi + +clone() { + local plugin="$1" + local branch="$2" + if [ -n "$branch" ]; then + cd "$(tpm_path)" && + GIT_TERMINAL_PROMPT=0 git clone -b "$branch" --single-branch --recursive "$plugin" >/dev/null 2>&1 + else + cd "$(tpm_path)" && + GIT_TERMINAL_PROMPT=0 git clone --single-branch --recursive "$plugin" >/dev/null 2>&1 + fi +} + +# tries cloning: +# 1. plugin name directly - works if it's a valid git url +# 2. expands the plugin name to point to a github repo and tries cloning again +clone_plugin() { + local plugin="$1" + local branch="$2" + clone "$plugin" "$branch" || + clone "https://git::@github.com/$plugin" "$branch" +} + +# clone plugin and produce output +install_plugin() { + local plugin="$1" + local branch="$2" + local plugin_name="$(plugin_name_helper "$plugin")" + + if plugin_already_installed "$plugin"; then + echo_ok "Already installed \"$plugin_name\"" + else + echo_ok "Installing \"$plugin_name\"" + clone_plugin "$plugin" "$branch" && + echo_ok " \"$plugin_name\" download success" || + echo_err " \"$plugin_name\" download fail" + fi +} + +install_plugins() { + local plugins="$(tpm_plugins_list_helper)" + for plugin in $plugins; do + IFS='#' read -ra plugin <<< "$plugin" + install_plugin "${plugin[0]}" "${plugin[1]}" + done +} + +verify_tpm_path_permissions() { + local path="$(tpm_path)" + # check the write permission flag for all users to ensure + # that we have proper access + [ -w "$path" ] || + echo_err "$path is not writable!" +} + +main() { + ensure_tpm_path_exists + verify_tpm_path_permissions + install_plugins + exit_value_helper +} +main diff --git a/tmux/.tmux/plugins/tpm/scripts/source_plugins.sh b/tmux/.tmux/plugins/tpm/scripts/source_plugins.sh new file mode 100755 index 0000000..6381d54 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/scripts/source_plugins.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +HELPERS_DIR="$CURRENT_DIR/helpers" + +source "$HELPERS_DIR/plugin_functions.sh" + +plugin_dir_exists() { + [ -d "$1" ] +} + +# Runs all *.tmux files from the plugin directory. +# Files are ran as executables. +# No errors if the plugin dir does not exist. +silently_source_all_tmux_files() { + local plugin_path="$1" + local plugin_tmux_files="$plugin_path*.tmux" + if plugin_dir_exists "$plugin_path"; then + for tmux_file in $plugin_tmux_files; do + # if the glob didn't find any files this will be the + # unexpanded glob which obviously doesn't exist + [ -f "$tmux_file" ] || continue + # runs *.tmux file as an executable + $tmux_file >/dev/null 2>&1 + done + fi +} + +source_plugins() { + local plugin plugin_path + local plugins="$(tpm_plugins_list_helper)" + for plugin in $plugins; do + IFS='#' read -ra plugin <<< "$plugin" + plugin_path="$(plugin_path_helper "${plugin[0]}")" + silently_source_all_tmux_files "$plugin_path" + done +} + +main() { + source_plugins +} +main diff --git a/tmux/.tmux/plugins/tpm/scripts/update_plugin.sh b/tmux/.tmux/plugins/tpm/scripts/update_plugin.sh new file mode 100755 index 0000000..68bf605 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/scripts/update_plugin.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +# this script handles core logic of updating plugins + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +HELPERS_DIR="$CURRENT_DIR/helpers" + +source "$HELPERS_DIR/plugin_functions.sh" +source "$HELPERS_DIR/utility.sh" + +if [ "$1" == "--tmux-echo" ]; then # tmux-specific echo functions + source "$HELPERS_DIR/tmux_echo_functions.sh" +else # shell output functions + source "$HELPERS_DIR/shell_echo_functions.sh" +fi + +# from now on ignore first script argument +shift + +pull_changes() { + local plugin="$1" + local plugin_path="$(plugin_path_helper "$plugin")" + cd "$plugin_path" && + GIT_TERMINAL_PROMPT=0 git pull && + GIT_TERMINAL_PROMPT=0 git submodule update --init --recursive +} + +update() { + local plugin="$1" + $(pull_changes "$plugin" > /dev/null 2>&1) && + echo_ok " \"$plugin\" update success" || + echo_err " \"$plugin\" update fail" +} + +update_all() { + echo_ok "Updating all plugins!" + echo_ok "" + local plugins="$(tpm_plugins_list_helper)" + for plugin in $plugins; do + IFS='#' read -ra plugin <<< "$plugin" + local plugin_name="$(plugin_name_helper "${plugin[0]}")" + # updating only installed plugins + if plugin_already_installed "$plugin_name"; then + update "$plugin_name" & + fi + done + wait +} + +update_plugins() { + local plugins="$*" + for plugin in $plugins; do + IFS='#' read -ra plugin <<< "$plugin" + local plugin_name="$(plugin_name_helper "${plugin[0]}")" + if plugin_already_installed "$plugin_name"; then + update "$plugin_name" & + else + echo_err "$plugin_name not installed!" & + fi + done + wait +} + +main() { + ensure_tpm_path_exists + if [ "$1" == "all" ]; then + update_all + else + update_plugins "$*" + fi + exit_value_helper +} +main "$*" diff --git a/tmux/.tmux/plugins/tpm/scripts/update_plugin_prompt_handler.sh b/tmux/.tmux/plugins/tpm/scripts/update_plugin_prompt_handler.sh new file mode 100755 index 0000000..5e1f7d9 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/scripts/update_plugin_prompt_handler.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +HELPERS_DIR="$CURRENT_DIR/helpers" + +if [ $# -eq 0 ]; then + exit 0 +fi + +source "$HELPERS_DIR/tmux_echo_functions.sh" +source "$HELPERS_DIR/tmux_utils.sh" + +main() { + "$CURRENT_DIR/update_plugin.sh" --tmux-echo "$*" + reload_tmux_environment + end_message +} +main "$*" diff --git a/tmux/.tmux/plugins/tpm/scripts/variables.sh b/tmux/.tmux/plugins/tpm/scripts/variables.sh new file mode 100644 index 0000000..5601a86 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/scripts/variables.sh @@ -0,0 +1,13 @@ +install_key_option="@tpm-install" +default_install_key="I" + +update_key_option="@tpm-update" +default_update_key="U" + +clean_key_option="@tpm-clean" +default_clean_key="M-u" + +SUPPORTED_TMUX_VERSION="1.9" + +DEFAULT_TPM_ENV_VAR_NAME="TMUX_PLUGIN_MANAGER_PATH" +DEFAULT_TPM_PATH="$HOME/.tmux/plugins/" diff --git a/tmux/.tmux/plugins/tpm/tests/expect_failed_plugin_download b/tmux/.tmux/plugins/tpm/tests/expect_failed_plugin_download new file mode 100755 index 0000000..b970477 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/tests/expect_failed_plugin_download @@ -0,0 +1,36 @@ +#!/usr/bin/env expect + +# disables script output +log_user 0 + +spawn tmux + +# Waiting for tmux to attach. If this is not done, next command, `send` will +# not work properly. +sleep 1 + +# this is tmux prefix + I +send "I" + +# cloning might take a while +set timeout 20 + +expect_after { + timeout { exit 1 } +} + +expect { + "Installing \"non-existing-plugin\"" +} + +expect { + "\"non-existing-plugin\" download fail" +} + +expect { + "Done, press ENTER to continue" { + exit 0 + } +} + +exit 1 diff --git a/tmux/.tmux/plugins/tpm/tests/expect_successful_clean_plugins b/tmux/.tmux/plugins/tpm/tests/expect_successful_clean_plugins new file mode 100755 index 0000000..987c49d --- /dev/null +++ b/tmux/.tmux/plugins/tpm/tests/expect_successful_clean_plugins @@ -0,0 +1,35 @@ +#!/usr/bin/env expect + +# disables script output +log_user 0 + +spawn tmux + +# Waiting for tmux to attach. If this is not done, next command, `send` will +# not work properly. +sleep 1 + +# this is tmux prefix + alt + u +send "u" + +set timeout 5 + +expect_after { + timeout { exit 1 } +} + +expect { + "Removing \"tmux-example-plugin\"" +} + +expect { + "\"tmux-example-plugin\" clean success" +} + +expect { + "Done, press ENTER to continue." { + exit 0 + } +} + +exit 1 diff --git a/tmux/.tmux/plugins/tpm/tests/expect_successful_multiple_plugins_download b/tmux/.tmux/plugins/tpm/tests/expect_successful_multiple_plugins_download new file mode 100755 index 0000000..cc87a26 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/tests/expect_successful_multiple_plugins_download @@ -0,0 +1,44 @@ +#!/usr/bin/env expect + +# disables script output +log_user 0 + +spawn tmux + +# Waiting for tmux to attach. If this is not done, next command, `send` will +# not work properly. +sleep 1 + +# this is tmux prefix + I +send "I" + +# cloning might take a while +set timeout 15 + +expect_after { + timeout { exit 1 } +} + +expect { + "Installing \"tmux-example-plugin\"" +} + +expect { + "\"tmux-example-plugin\" download success" +} + +expect { + "Installing \"tmux-copycat\"" +} + +expect { + "\"tmux-copycat\" download success" +} + +expect { + "Done, press ENTER to continue." { + exit 0 + } +} + +exit 1 diff --git a/tmux/.tmux/plugins/tpm/tests/expect_successful_plugin_download b/tmux/.tmux/plugins/tpm/tests/expect_successful_plugin_download new file mode 100755 index 0000000..388f05d --- /dev/null +++ b/tmux/.tmux/plugins/tpm/tests/expect_successful_plugin_download @@ -0,0 +1,50 @@ +#!/usr/bin/env expect + +# disables script output +log_user 0 + +spawn tmux + +# Waiting for tmux to attach. If this is not done, next command, `send` will +# not work properly. +sleep 1 + +# this is tmux prefix + I +send "I" + +# cloning might take a while +set timeout 15 + +expect_after { + timeout { exit 1 } +} + +expect { + "Installing \"tmux-example-plugin\"" +} + +expect { + "\"tmux-example-plugin\" download success" +} + +expect { + "Done, press ENTER to continue" { + send " " + } +} + +sleep 1 +# this is tmux prefix + I +send "I" + +expect { + "Already installed \"tmux-example-plugin\"" +} + +expect { + "Done, press ENTER to continue" { + exit 0 + } +} + +exit 1 diff --git a/tmux/.tmux/plugins/tpm/tests/expect_successful_update_of_a_single_plugin b/tmux/.tmux/plugins/tpm/tests/expect_successful_update_of_a_single_plugin new file mode 100755 index 0000000..bcd64fe --- /dev/null +++ b/tmux/.tmux/plugins/tpm/tests/expect_successful_update_of_a_single_plugin @@ -0,0 +1,55 @@ +#!/usr/bin/env expect + +# disables script output +log_user 0 + +spawn tmux + +# Waiting for tmux to attach. If this is not done, next command, `send` will +# not work properly. +sleep 1 + +# this is tmux prefix + U +send "U" + +set timeout 15 + +expect_after { + timeout { exit 1 } +} + +expect { + "Installed plugins" +} + +expect { + "tmux-example-plugin" +} + +expect { + "\"all\" - updates all plugins" +} + +expect { + "ENTER - cancels" +} + +# wait for tmux to display prompt before sending characters +sleep 1 +send "tmux-example-plugin\r" + +expect { + "Updating \"tmux-example-plugin\"" +} + +expect { + "\"tmux-example-plugin\" update success" +} + +expect { + "Done, press ENTER to continue." { + exit 0 + } +} + +exit 1 diff --git a/tmux/.tmux/plugins/tpm/tests/expect_successful_update_of_all_plugins b/tmux/.tmux/plugins/tpm/tests/expect_successful_update_of_all_plugins new file mode 100755 index 0000000..4f3a4a3 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/tests/expect_successful_update_of_all_plugins @@ -0,0 +1,59 @@ +#!/usr/bin/env expect + +# disables script output +log_user 0 + +spawn tmux + +# Waiting for tmux to attach. If this is not done, next command, `send` will +# not work properly. +sleep 1 + +# this is tmux prefix + U +send "U" + +set timeout 5 + +expect_after { + timeout { exit 1 } +} + +expect { + "Installed plugins" +} + +expect { + "tmux-example-plugin" +} + +expect { + "\"all\" - updates all plugins" +} + +expect { + "ENTER - cancels" +} + +# wait for tmux to display prompt before sending characters +sleep 1 +send "all\r" + +expect { + "Updating all plugins!" +} + +expect { + "Updating \"tmux-example-plugin\"" +} + +expect { + "\"tmux-example-plugin\" update success" +} + +expect { + "Done, press ENTER to continue." { + exit 0 + } +} + +exit 1 diff --git a/tmux/.tmux/plugins/tpm/tests/helpers/tpm.sh b/tmux/.tmux/plugins/tpm/tests/helpers/tpm.sh new file mode 100644 index 0000000..1594afb --- /dev/null +++ b/tmux/.tmux/plugins/tpm/tests/helpers/tpm.sh @@ -0,0 +1,13 @@ +check_dir_exists_helper() { + [ -d "$1" ] +} + +# runs the scripts and asserts it has the correct output and exit code +script_run_helper() { + local script="$1" + local expected_output="$2" + local expected_exit_code="${3:-0}" + $script 2>&1 | + grep "$expected_output" >/dev/null 2>&1 && # grep -q flag quits the script early + [ "${PIPESTATUS[0]}" -eq "$expected_exit_code" ] +} diff --git a/tmux/.tmux/plugins/tpm/tests/test_plugin_clean.sh b/tmux/.tmux/plugins/tpm/tests/test_plugin_clean.sh new file mode 100755 index 0000000..d36c468 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/tests/test_plugin_clean.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +TPM_DIR="$PWD" +PLUGINS_DIR="$HOME/.tmux/plugins" + +source "$CURRENT_DIR/helpers/helpers.sh" +source "$CURRENT_DIR/helpers/tpm.sh" + +manually_install_the_plugin() { + rm -rf "$PLUGINS_DIR" + mkdir -p "$PLUGINS_DIR" + cd "$PLUGINS_DIR" + git clone --quiet https://github.com/tmux-plugins/tmux-example-plugin +} + +# TMUX KEY-BINDING TESTS + +test_plugin_uninstallation_via_tmux_key_binding() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + run-shell "$TPM_DIR/tpm" + HERE + + manually_install_the_plugin + + "$CURRENT_DIR/expect_successful_clean_plugins" || + fail_helper "[key-binding] clean fails" + + teardown_helper +} + +# SCRIPT TESTS + +test_plugin_uninstallation_via_script() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + run-shell "$TPM_DIR/tpm" + HERE + + manually_install_the_plugin + + script_run_helper "$TPM_DIR/bin/clean_plugins" '"tmux-example-plugin" clean success' || + fail_helper "[script] plugin cleaning fails" + + teardown_helper +} + +test_unsuccessful_plugin_uninstallation_via_script() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + run-shell "$TPM_DIR/tpm" + HERE + + manually_install_the_plugin + chmod 000 "$PLUGINS_DIR/tmux-example-plugin" # disable directory deletion + + local expected_exit_code=1 + script_run_helper "$TPM_DIR/bin/clean_plugins" '"tmux-example-plugin" clean fail' "$expected_exit_code" || + fail_helper "[script] unsuccessful plugin cleaning doesn't fail" + + chmod 755 "$PLUGINS_DIR/tmux-example-plugin" # enable directory deletion + + teardown_helper +} + +run_tests diff --git a/tmux/.tmux/plugins/tpm/tests/test_plugin_installation.sh b/tmux/.tmux/plugins/tpm/tests/test_plugin_installation.sh new file mode 100755 index 0000000..94fb674 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/tests/test_plugin_installation.sh @@ -0,0 +1,284 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +PLUGINS_DIR="$HOME/.tmux/plugins" +TPM_DIR="$PWD" + +CUSTOM_PLUGINS_DIR="$HOME/foo/plugins" +ADDITIONAL_CONFIG_FILE_1="$HOME/.tmux/additional_config_file_1" +ADDITIONAL_CONFIG_FILE_2="$HOME/.tmux/additional_config_file_2" + +source "$CURRENT_DIR/helpers/helpers.sh" +source "$CURRENT_DIR/helpers/tpm.sh" + +# TMUX KEY-BINDING TESTS + +test_plugin_installation_via_tmux_key_binding() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + set -g @plugin "tmux-plugins/tmux-example-plugin" + run-shell "$TPM_DIR/tpm" + HERE + + "$CURRENT_DIR/expect_successful_plugin_download" || + fail_helper "[key-binding] plugin installation fails" + + check_dir_exists_helper "$PLUGINS_DIR/tmux-example-plugin/" || + fail_helper "[key-binding] plugin download fails" + + teardown_helper +} + +test_plugin_installation_via_tmux_key_binding_set_option() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + set-option -g @plugin "tmux-plugins/tmux-example-plugin" + run-shell "$TPM_DIR/tpm" + HERE + + "$CURRENT_DIR/expect_successful_plugin_download" || + fail_helper "[key-binding][set-option] plugin installation fails" + + check_dir_exists_helper "$PLUGINS_DIR/tmux-example-plugin/" || + fail_helper "[key-binding][set-option] plugin download fails" + + teardown_helper +} + +test_plugin_installation_custom_dir_via_tmux_key_binding() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + set-environment -g TMUX_PLUGIN_MANAGER_PATH '$CUSTOM_PLUGINS_DIR' + + set -g @plugin "tmux-plugins/tmux-example-plugin" + run-shell "$TPM_DIR/tpm" + HERE + + "$CURRENT_DIR/expect_successful_plugin_download" || + fail_helper "[key-binding][custom dir] plugin installation fails" + + check_dir_exists_helper "$CUSTOM_PLUGINS_DIR/tmux-example-plugin/" || + fail_helper "[key-binding][custom dir] plugin download fails" + + teardown_helper + rm -rf "$CUSTOM_PLUGINS_DIR" +} + +test_non_existing_plugin_installation_via_tmux_key_binding() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + set -g @plugin "tmux-plugins/non-existing-plugin" + run-shell "$TPM_DIR/tpm" + HERE + + "$CURRENT_DIR/expect_failed_plugin_download" || + fail_helper "[key-binding] non existing plugin installation doesn't fail" + + teardown_helper +} + +test_multiple_plugins_installation_via_tmux_key_binding() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + set -g @plugin "tmux-plugins/tmux-example-plugin" + \ \ set -g @plugin 'tmux-plugins/tmux-copycat' + run-shell "$TPM_DIR/tpm" + HERE + + "$CURRENT_DIR/expect_successful_multiple_plugins_download" || + fail_helper "[key-binding] multiple plugins installation fails" + + check_dir_exists_helper "$PLUGINS_DIR/tmux-example-plugin/" || + fail_helper "[key-binding] plugin download fails (tmux-example-plugin)" + + check_dir_exists_helper "$PLUGINS_DIR/tmux-copycat/" || + fail_helper "[key-binding] plugin download fails (tmux-copycat)" + + teardown_helper +} + +test_plugins_installation_from_sourced_file_via_tmux_key_binding() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + source '$ADDITIONAL_CONFIG_FILE_1' + set -g @plugin 'tmux-plugins/tmux-example-plugin' + run-shell "$TPM_DIR/tpm" + HERE + + mkdir ~/.tmux + echo "set -g @plugin 'tmux-plugins/tmux-copycat'" > "$ADDITIONAL_CONFIG_FILE_1" + + "$CURRENT_DIR/expect_successful_multiple_plugins_download" || + fail_helper "[key-binding][sourced file] plugins installation fails" + + check_dir_exists_helper "$PLUGINS_DIR/tmux-example-plugin/" || + fail_helper "[key-binding][sourced file] plugin download fails (tmux-example-plugin)" + + check_dir_exists_helper "$PLUGINS_DIR/tmux-copycat/" || + fail_helper "[key-binding][sourced file] plugin download fails (tmux-copycat)" + + teardown_helper +} + +test_plugins_installation_from_multiple_sourced_files_via_tmux_key_binding() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + \ \ source '$ADDITIONAL_CONFIG_FILE_1' + source-file '$ADDITIONAL_CONFIG_FILE_2' + run-shell "$TPM_DIR/tpm" + HERE + + mkdir ~/.tmux + echo "set -g @plugin 'tmux-plugins/tmux-example-plugin'" > "$ADDITIONAL_CONFIG_FILE_1" + echo " set -g @plugin 'tmux-plugins/tmux-copycat'" > "$ADDITIONAL_CONFIG_FILE_2" + + "$CURRENT_DIR/expect_successful_multiple_plugins_download" || + fail_helper "[key-binding][multiple sourced files] plugins installation fails" + + check_dir_exists_helper "$PLUGINS_DIR/tmux-example-plugin/" || + fail_helper "[key-binding][multiple sourced files] plugin download fails (tmux-example-plugin)" + + check_dir_exists_helper "$PLUGINS_DIR/tmux-copycat/" || + fail_helper "[key-binding][multiple sourced files] plugin download fails (tmux-copycat)" + + teardown_helper +} + +# SCRIPT TESTS + +test_plugin_installation_via_script() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + set -g @plugin "tmux-plugins/tmux-example-plugin" + run-shell "$TPM_DIR/tpm" + HERE + + script_run_helper "$TPM_DIR/bin/install_plugins" '"tmux-example-plugin" download success' || + fail_helper "[script] plugin installation fails" + + check_dir_exists_helper "$PLUGINS_DIR/tmux-example-plugin/" || + fail_helper "[script] plugin download fails" + + script_run_helper "$TPM_DIR/bin/install_plugins" 'Already installed "tmux-example-plugin"' || + fail_helper "[script] plugin already installed message fail" + + teardown_helper +} + +test_plugin_installation_custom_dir_via_script() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + set-environment -g TMUX_PLUGIN_MANAGER_PATH '$CUSTOM_PLUGINS_DIR' + + set -g @plugin "tmux-plugins/tmux-example-plugin" + run-shell "$TPM_DIR/tpm" + HERE + + script_run_helper "$TPM_DIR/bin/install_plugins" '"tmux-example-plugin" download success' || + fail_helper "[script][custom dir] plugin installation fails" + + check_dir_exists_helper "$CUSTOM_PLUGINS_DIR/tmux-example-plugin/" || + fail_helper "[script][custom dir] plugin download fails" + + script_run_helper "$TPM_DIR/bin/install_plugins" 'Already installed "tmux-example-plugin"' || + fail_helper "[script][custom dir] plugin already installed message fail" + + teardown_helper + rm -rf "$CUSTOM_PLUGINS_DIR" +} + +test_non_existing_plugin_installation_via_script() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + set -g @plugin "tmux-plugins/non-existing-plugin" + run-shell "$TPM_DIR/tpm" + HERE + + local expected_exit_code=1 + script_run_helper "$TPM_DIR/bin/install_plugins" '"non-existing-plugin" download fail' "$expected_exit_code" || + fail_helper "[script] non existing plugin installation doesn't fail" + + teardown_helper +} + +test_multiple_plugins_installation_via_script() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + set -g @plugin "tmux-plugins/tmux-example-plugin" + \ \ set -g @plugin 'tmux-plugins/tmux-copycat' + run-shell "$TPM_DIR/tpm" + HERE + + script_run_helper "$TPM_DIR/bin/install_plugins" '"tmux-example-plugin" download success' || + fail_helper "[script] multiple plugins installation fails" + + check_dir_exists_helper "$PLUGINS_DIR/tmux-example-plugin/" || + fail_helper "[script] plugin download fails (tmux-example-plugin)" + + check_dir_exists_helper "$PLUGINS_DIR/tmux-copycat/" || + fail_helper "[script] plugin download fails (tmux-copycat)" + + script_run_helper "$TPM_DIR/bin/install_plugins" 'Already installed "tmux-copycat"' || + fail_helper "[script] multiple plugins already installed message fail" + + teardown_helper +} + +test_plugins_installation_from_sourced_file_via_script() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + source '$ADDITIONAL_CONFIG_FILE_1' + set -g @plugin 'tmux-plugins/tmux-example-plugin' + run-shell "$TPM_DIR/tpm" + HERE + + mkdir ~/.tmux + echo "set -g @plugin 'tmux-plugins/tmux-copycat'" > "$ADDITIONAL_CONFIG_FILE_1" + + script_run_helper "$TPM_DIR/bin/install_plugins" '"tmux-copycat" download success' || + fail_helper "[script][sourced file] plugins installation fails" + + check_dir_exists_helper "$PLUGINS_DIR/tmux-example-plugin/" || + fail_helper "[script][sourced file] plugin download fails (tmux-example-plugin)" + + check_dir_exists_helper "$PLUGINS_DIR/tmux-copycat/" || + fail_helper "[script][sourced file] plugin download fails (tmux-copycat)" + + script_run_helper "$TPM_DIR/bin/install_plugins" 'Already installed "tmux-copycat"' || + fail_helper "[script][sourced file] plugins already installed message fail" + + teardown_helper +} + +test_plugins_installation_from_multiple_sourced_files_via_script() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + \ \ source '$ADDITIONAL_CONFIG_FILE_1' + source-file '$ADDITIONAL_CONFIG_FILE_2' + set -g @plugin 'tmux-plugins/tmux-example-plugin' + run-shell "$TPM_DIR/tpm" + HERE + + mkdir ~/.tmux + echo " set -g @plugin 'tmux-plugins/tmux-copycat'" > "$ADDITIONAL_CONFIG_FILE_1" + echo "set -g @plugin 'tmux-plugins/tmux-sensible'" > "$ADDITIONAL_CONFIG_FILE_2" + + script_run_helper "$TPM_DIR/bin/install_plugins" '"tmux-sensible" download success' || + fail_helper "[script][multiple sourced files] plugins installation fails" + + check_dir_exists_helper "$PLUGINS_DIR/tmux-example-plugin/" || + fail_helper "[script][multiple sourced files] plugin download fails (tmux-example-plugin)" + + check_dir_exists_helper "$PLUGINS_DIR/tmux-copycat/" || + fail_helper "[script][multiple sourced files] plugin download fails (tmux-copycat)" + + check_dir_exists_helper "$PLUGINS_DIR/tmux-sensible/" || + fail_helper "[script][multiple sourced files] plugin download fails (tmux-sensible)" + + script_run_helper "$TPM_DIR/bin/install_plugins" 'Already installed "tmux-sensible"' || + fail_helper "[script][multiple sourced files] plugins already installed message fail" + + teardown_helper +} + +run_tests diff --git a/tmux/.tmux/plugins/tpm/tests/test_plugin_installation_legacy.sh b/tmux/.tmux/plugins/tpm/tests/test_plugin_installation_legacy.sh new file mode 100755 index 0000000..b1d0cf6 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/tests/test_plugin_installation_legacy.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +PLUGINS_DIR="$HOME/.tmux/plugins" +TPM_DIR="$PWD" + +source "$CURRENT_DIR/helpers/helpers.sh" +source "$CURRENT_DIR/helpers/tpm.sh" + +# TMUX KEY-BINDING TESTS + +test_plugin_installation_via_tmux_key_binding() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + set -g @tpm_plugins "tmux-plugins/tmux-example-plugin" + run-shell "$TPM_DIR/tpm" + HERE + + # opens tmux and test it with `expect` + $CURRENT_DIR/expect_successful_plugin_download || + fail_helper "[key-binding] plugin installation fails" + + # check plugin dir exists after download + check_dir_exists_helper "$PLUGINS_DIR/tmux-example-plugin/" || + fail_helper "[key-binding] plugin download fails" + + teardown_helper +} + +test_legacy_and_new_syntax_for_plugin_installation_work_via_tmux_key_binding() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + set -g @tpm_plugins " \ + tmux-plugins/tmux-example-plugin \ + " + set -g @plugin 'tmux-plugins/tmux-copycat' + run-shell "$TPM_DIR/tpm" + HERE + + # opens tmux and test it with `expect` + "$CURRENT_DIR"/expect_successful_multiple_plugins_download || + fail_helper "[key-binding] multiple plugins installation fails" + + # check plugin dir exists after download + check_dir_exists_helper "$PLUGINS_DIR/tmux-example-plugin/" || + fail_helper "[key-binding] plugin download fails (tmux-example-plugin)" + + check_dir_exists_helper "$PLUGINS_DIR/tmux-copycat/" || + fail_helper "[key-binding] plugin download fails (tmux-copycat)" + + teardown_helper +} + +# SCRIPT TESTS + +test_plugin_installation_via_script() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + set -g @tpm_plugins "tmux-plugins/tmux-example-plugin" + run-shell "$TPM_DIR/tpm" + HERE + + script_run_helper "$TPM_DIR/bin/install_plugins" '"tmux-example-plugin" download success' || + fail_helper "[script] plugin installation fails" + + check_dir_exists_helper "$PLUGINS_DIR/tmux-example-plugin/" || + fail_helper "[script] plugin download fails" + + script_run_helper "$TPM_DIR/bin/install_plugins" 'Already installed "tmux-example-plugin"' || + fail_helper "[script] plugin already installed message fail" + + teardown_helper +} + +test_legacy_and_new_syntax_for_plugin_installation_work_via_script() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + set -g @tpm_plugins " \ + tmux-plugins/tmux-example-plugin \ + " + set -g @plugin 'tmux-plugins/tmux-copycat' + run-shell "$TPM_DIR/tpm" + HERE + + script_run_helper "$TPM_DIR/bin/install_plugins" '"tmux-example-plugin" download success' || + fail_helper "[script] multiple plugin installation fails" + + check_dir_exists_helper "$PLUGINS_DIR/tmux-example-plugin/" || + fail_helper "[script] plugin download fails (tmux-example-plugin)" + + check_dir_exists_helper "$PLUGINS_DIR/tmux-copycat/" || + fail_helper "[script] plugin download fails (tmux-copycat)" + + script_run_helper "$TPM_DIR/bin/install_plugins" 'Already installed "tmux-copycat"' || + fail_helper "[script] multiple plugins already installed message fail" + + teardown_helper +} + +run_tests diff --git a/tmux/.tmux/plugins/tpm/tests/test_plugin_sourcing.sh b/tmux/.tmux/plugins/tpm/tests/test_plugin_sourcing.sh new file mode 100755 index 0000000..c06f1fe --- /dev/null +++ b/tmux/.tmux/plugins/tpm/tests/test_plugin_sourcing.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +TPM_DIR="$PWD" +PLUGINS_DIR="$HOME/.tmux/plugins" + +CUSTOM_PLUGINS_DIR="$HOME/foo/plugins" + +source "$CURRENT_DIR/helpers/helpers.sh" +source "$CURRENT_DIR/helpers/tpm.sh" + +check_binding_defined() { + local binding="$1" + tmux list-keys | grep -q "$binding" +} + +create_test_plugin_helper() { + local plugin_path="$PLUGINS_DIR/tmux_test_plugin/" + rm -rf "$plugin_path" + mkdir -p "$plugin_path" + + while read line; do + echo "$line" >> "$plugin_path/test_plugin.tmux" + done + chmod +x "$plugin_path/test_plugin.tmux" +} + +check_tpm_path() { + local correct_tpm_path="$1" + local tpm_path="$(tmux start-server\; show-environment -g TMUX_PLUGIN_MANAGER_PATH | cut -f2 -d=)" + [ "$correct_tpm_path" == "$tpm_path" ] +} + +test_plugin_sourcing() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + set -g @plugin "doesnt_matter/tmux_test_plugin" + run-shell "$TPM_DIR/tpm" + HERE + + # manually creates a local tmux plugin + create_test_plugin_helper <<- HERE + tmux bind-key R run-shell foo_command + HERE + + tmux new-session -d # tmux starts detached + check_binding_defined "R run-shell foo_command" || + fail_helper "Plugin sourcing fails" + + teardown_helper +} + +test_default_tpm_path() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + run-shell "$TPM_DIR/tpm" + HERE + + check_tpm_path "${PLUGINS_DIR}/" || + fail_helper "Default TPM path not correct" + + teardown_helper +} + +test_custom_tpm_path() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + set-environment -g TMUX_PLUGIN_MANAGER_PATH '$CUSTOM_PLUGINS_DIR' + run-shell "$TPM_DIR/tpm" + HERE + + check_tpm_path "$CUSTOM_PLUGINS_DIR" || + fail_helper "Custom TPM path not correct" + + teardown_helper +} + +run_tests diff --git a/tmux/.tmux/plugins/tpm/tests/test_plugin_update.sh b/tmux/.tmux/plugins/tpm/tests/test_plugin_update.sh new file mode 100755 index 0000000..4924d16 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/tests/test_plugin_update.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +TPM_DIR="$PWD" +PLUGINS_DIR="$HOME/.tmux/plugins" + +source "$CURRENT_DIR/helpers/helpers.sh" +source "$CURRENT_DIR/helpers/tpm.sh" + +manually_install_the_plugin() { + mkdir -p "$PLUGINS_DIR" + cd "$PLUGINS_DIR" + git clone --quiet https://github.com/tmux-plugins/tmux-example-plugin +} + +# TMUX KEY-BINDING TESTS + +test_plugin_update_via_tmux_key_binding() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + set -g @plugin "tmux-plugins/tmux-example-plugin" + run-shell "$TPM_DIR/tpm" + HERE + + manually_install_the_plugin + + "$CURRENT_DIR/expect_successful_update_of_all_plugins" || + fail_helper "[key-binding] 'update all plugins' fails" + + "$CURRENT_DIR/expect_successful_update_of_a_single_plugin" || + fail_helper "[key-binding] 'update single plugin' fails" + + teardown_helper +} + +# SCRIPT TESTS + +test_plugin_update_via_script() { + set_tmux_conf_helper <<- HERE + set -g mode-keys vi + set -g @plugin "tmux-plugins/tmux-example-plugin" + run-shell "$TPM_DIR/tpm" + HERE + + manually_install_the_plugin + + local expected_exit_code=1 + script_run_helper "$TPM_DIR/bin/update_plugins" 'usage' "$expected_exit_code" || + fail_helper "[script] running update plugins without args should fail" + + script_run_helper "$TPM_DIR/bin/update_plugins tmux-example-plugin" '"tmux-example-plugin" update success' || + fail_helper "[script] plugin update fails" + + script_run_helper "$TPM_DIR/bin/update_plugins all" '"tmux-example-plugin" update success' || + fail_helper "[script] update all plugins fails" + + teardown_helper +} + +run_tests diff --git a/tmux/.tmux/plugins/tpm/tpm b/tmux/.tmux/plugins/tpm/tpm new file mode 100755 index 0000000..7ad4b99 --- /dev/null +++ b/tmux/.tmux/plugins/tpm/tpm @@ -0,0 +1,81 @@ +#!/usr/bin/env bash + +CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +BINDINGS_DIR="$CURRENT_DIR/bindings" +SCRIPTS_DIR="$CURRENT_DIR/scripts" + +source "$SCRIPTS_DIR/variables.sh" + +get_tmux_option() { + local option="$1" + local default_value="$2" + local option_value="$(tmux show-option -gqv "$option")" + if [ -z "$option_value" ]; then + echo "$default_value" + else + echo "$option_value" + fi +} + +tpm_path_set() { + tmux show-environment -g "$DEFAULT_TPM_ENV_VAR_NAME" >/dev/null 2>&1 +} + +# Check if configuration file exists at an XDG-compatible location, if so use +# that directory for TMUX_PLUGIN_MANAGER_PATH. Otherwise use $DEFAULT_TPM_PATH. +set_default_tpm_path() { + local xdg_tmux_path="${XDG_CONFIG_HOME:-$HOME/.config}/tmux" + local tpm_path="$DEFAULT_TPM_PATH" + + if [ -f "$xdg_tmux_path/tmux.conf" ]; then + tpm_path="$xdg_tmux_path/plugins/" + fi + + tmux set-environment -g "$DEFAULT_TPM_ENV_VAR_NAME" "$tpm_path" +} + +# Ensures TMUX_PLUGIN_MANAGER_PATH global env variable is set. +# +# Put this in `.tmux.conf` to override the default: +# `set-environment -g TMUX_PLUGIN_MANAGER_PATH "/some/other/path/"` +set_tpm_path() { + if ! tpm_path_set; then + set_default_tpm_path + fi +} + +# 1. Fetches plugin names from `@plugin` variables +# 2. Creates full plugin path +# 3. Sources all *.tmux files from each of the plugin directories +# - no errors raised if directory does not exist +# Files are sourced as tmux config files, not as shell scripts! +source_plugins() { + "$SCRIPTS_DIR/source_plugins.sh" >/dev/null 2>&1 +} + +# prefix + I - downloads TPM plugins and reloads TMUX environment +# prefix + U - updates a plugin (or all of them) and reloads TMUX environment +# prefix + alt + u - remove unused TPM plugins and reloads TMUX environment +set_tpm_key_bindings() { + local install_key="$(get_tmux_option "$install_key_option" "$default_install_key")" + tmux bind-key "$install_key" run-shell "$BINDINGS_DIR/install_plugins" + + local update_key="$(get_tmux_option "$update_key_option" "$default_update_key")" + tmux bind-key "$update_key" run-shell "$BINDINGS_DIR/update_plugins" + + local clean_key="$(get_tmux_option "$clean_key_option" "$default_clean_key")" + tmux bind-key "$clean_key" run-shell "$BINDINGS_DIR/clean_plugins" +} + +supported_tmux_version_ok() { + "$SCRIPTS_DIR/check_tmux_version.sh" "$SUPPORTED_TMUX_VERSION" +} + +main() { + if supported_tmux_version_ok; then + set_tpm_path + set_tpm_key_bindings + source_plugins + fi +} +main diff --git a/tmux/.tmux/plugins/vim-tmux-navigator/.gitignore b/tmux/.tmux/plugins/vim-tmux-navigator/.gitignore new file mode 100644 index 0000000..926ccaa --- /dev/null +++ b/tmux/.tmux/plugins/vim-tmux-navigator/.gitignore @@ -0,0 +1 @@ +doc/tags diff --git a/tmux/.tmux/plugins/vim-tmux-navigator/License.md b/tmux/.tmux/plugins/vim-tmux-navigator/License.md new file mode 100644 index 0000000..046c81d --- /dev/null +++ b/tmux/.tmux/plugins/vim-tmux-navigator/License.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Chris Toomey + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tmux/.tmux/plugins/vim-tmux-navigator/README.md b/tmux/.tmux/plugins/vim-tmux-navigator/README.md new file mode 100644 index 0000000..63e5e7c --- /dev/null +++ b/tmux/.tmux/plugins/vim-tmux-navigator/README.md @@ -0,0 +1,305 @@ +Vim Tmux Navigator +================== + +This plugin is a repackaging of [Mislav Marohnić's](https://mislav.net/) tmux-navigator +configuration described in [this gist][]. When combined with a set of tmux +key bindings, the plugin will allow you to navigate seamlessly between +vim and tmux splits using a consistent set of hotkeys. + +**NOTE**: This requires tmux v1.8 or higher. + +Usage +----- + +This plugin provides the following mappings which allow you to move between +Vim panes and tmux splits seamlessly. + +- `` => Left +- `` => Down +- `` => Up +- `` => Right +- `` => Previous split + +**Note** - you don't need to use your tmux `prefix` key sequence before using +the mappings. + +If you want to use alternate key mappings, see the [configuration section +below][]. + +Installation +------------ + +### Vim + +If you don't have a preferred installation method, I recommend using [Vundle][]. +Assuming you have Vundle installed and configured, the following steps will +install the plugin: + +Add the following line to your `~/.vimrc` file + +``` vim +Plugin 'christoomey/vim-tmux-navigator' +``` + +Then run + +``` +:PluginInstall +``` + +If you are using Vim 8+, you don't need any plugin manager. Simply clone this repository inside `~/.vim/pack/plugin/start/` directory and restart Vim. + +``` +git clone git@github.com:christoomey/vim-tmux-navigator.git ~/.vim/pack/plugins/start/vim-tmux-navigator +``` + + +### tmux + +To configure the tmux side of this customization there are two options: + +#### Add a snippet + +Add the following to your `~/.tmux.conf` file: + +``` tmux +# Smart pane switching with awareness of Vim splits. +# See: https://github.com/christoomey/vim-tmux-navigator +is_vim="ps -o state= -o comm= -t '#{pane_tty}' \ + | grep -iqE '^[^TXZ ]+ +(\\S+\\/)?g?(view|n?vim?x?)(diff)?$'" +bind-key -n 'C-h' if-shell "$is_vim" 'send-keys C-h' 'select-pane -L' +bind-key -n 'C-j' if-shell "$is_vim" 'send-keys C-j' 'select-pane -D' +bind-key -n 'C-k' if-shell "$is_vim" 'send-keys C-k' 'select-pane -U' +bind-key -n 'C-l' if-shell "$is_vim" 'send-keys C-l' 'select-pane -R' +tmux_version='$(tmux -V | sed -En "s/^tmux ([0-9]+(.[0-9]+)?).*/\1/p")' +if-shell -b '[ "$(echo "$tmux_version < 3.0" | bc)" = 1 ]' \ + "bind-key -n 'C-\\' if-shell \"$is_vim\" 'send-keys C-\\' 'select-pane -l'" +if-shell -b '[ "$(echo "$tmux_version >= 3.0" | bc)" = 1 ]' \ + "bind-key -n 'C-\\' if-shell \"$is_vim\" 'send-keys C-\\\\' 'select-pane -l'" + +bind-key -T copy-mode-vi 'C-h' select-pane -L +bind-key -T copy-mode-vi 'C-j' select-pane -D +bind-key -T copy-mode-vi 'C-k' select-pane -U +bind-key -T copy-mode-vi 'C-l' select-pane -R +bind-key -T copy-mode-vi 'C-\' select-pane -l +``` + +#### TPM + +If you'd prefer, you can use the Tmux Plugin Manager ([TPM][]) instead of +copying the snippet. +When using TPM, add the following lines to your ~/.tmux.conf: + +``` tmux +set -g @plugin 'christoomey/vim-tmux-navigator' +run '~/.tmux/plugins/tpm/tpm' +``` + +Thanks to Christopher Sexton who provided the updated tmux configuration in +[this blog post][]. + +Configuration +------------- + +### Custom Key Bindings + +If you don't want the plugin to create any mappings, you can use the five +provided functions to define your own custom maps. You will need to define +custom mappings in your `~/.vimrc` as well as update the bindings in tmux to +match. + +#### Vim + +Add the following to your `~/.vimrc` to define your custom maps: + +``` vim +let g:tmux_navigator_no_mappings = 1 + +nnoremap {Left-Mapping} :TmuxNavigateLeft +nnoremap {Down-Mapping} :TmuxNavigateDown +nnoremap {Up-Mapping} :TmuxNavigateUp +nnoremap {Right-Mapping} :TmuxNavigateRight +nnoremap {Previous-Mapping} :TmuxNavigatePrevious +``` + +*Note* Each instance of `{Left-Mapping}` or `{Down-Mapping}` must be replaced +in the above code with the desired mapping. Ie, the mapping for `` => +Left would be created with `nnoremap :TmuxNavigateLeft`. + +##### Autosave on leave + +You can configure the plugin to write the current buffer, or all buffers, when +navigating from Vim to tmux. This functionality is exposed via the +`g:tmux_navigator_save_on_switch` variable, which can have either of the +following values: + +Value | Behavior +------ | ------ +1 | `:update` (write the current buffer, but only if changed) +2 | `:wall` (write all buffers) + +To enable this, add the following (with the desired value) to your ~/.vimrc: + +```vim +" Write all buffers before navigating from Vim to tmux pane +let g:tmux_navigator_save_on_switch = 2 +``` + +##### Disable While Zoomed + +By default, if you zoom the tmux pane running Vim and then attempt to navigate +"past" the edge of the Vim session, tmux will unzoom the pane. This is the +default tmux behavior, but may be confusing if you've become accustomed to +navigation "wrapping" around the sides due to this plugin. + +We provide an option, `g:tmux_navigator_disable_when_zoomed`, which can be used +to disable this unzooming behavior, keeping all navigation within Vim until the +tmux pane is explicitly unzoomed. + +To disable navigation when zoomed, add the following to your ~/.vimrc: + +```vim +" Disable tmux navigator when zooming the Vim pane +let g:tmux_navigator_disable_when_zoomed = 1 +``` + +##### Preserve Zoom + +As noted above, navigating from a Vim pane to another tmux pane normally causes +the window to be unzoomed. Some users may prefer the behavior of tmux's `-Z` +option to `select-pane`, which keeps the window zoomed if it was zoomed. To +enable this behavior, set the `g:tmux_navigator_preserve_zoom` option to `1`: + +```vim +" If the tmux window is zoomed, keep it zoomed when moving from Vim to another pane +let g:tmux_navigator_preserve_zoom = 1 +``` + +Naturally, if `g:tmux_navigator_disable_when_zoomed` is enabled, this option +will have no effect. + +#### Tmux + +Alter each of the five lines of the tmux configuration listed above to use your +custom mappings. **Note** each line contains two references to the desired +mapping. + +### Additional Customization + +#### Restoring Clear Screen (C-l) + +The default key bindings include `` which is the readline key binding +for clearing the screen. The following binding can be added to your `~/.tmux.conf` file to provide an alternate mapping to `clear-screen`. + +``` tmux +bind C-l send-keys 'C-l' +``` + +With this enabled you can use ` C-l` to clear the screen. + +Thanks to [Brian Hogan][] for the tip on how to re-map the clear screen binding. + +#### Nesting +If you like to nest your tmux sessions, this plugin is not going to work +properly. It probably never will, as it would require detecting when Tmux would +wrap from one outermost pane to another and propagating that to the outer +session. + +By default this plugin works on the outermost tmux session and the vim +sessions it contains, but you can customize the behaviour by adding more +commands to the expression used by the grep command. + +When nesting tmux sessions via ssh or mosh, you could extend it to look like +`'(^|\/)g?(view|vim|ssh|mosh?)(diff)?$'`, which makes this plugin work within +the innermost tmux session and the vim sessions within that one. This works +better than the default behaviour if you use the outer Tmux sessions as relays +to different hosts and have all instances of vim on remote hosts. + +Similarly, if you like to nest tmux locally, add `|tmux` to the expression. + +This behaviour means that you can't leave the innermost session with Ctrl-hjkl +directly. These following fallback mappings can be targeted to the right Tmux +session by escaping the prefix (Tmux' `send-prefix` command). + +``` tmux +bind -r C-h run "tmux select-pane -L" +bind -r C-j run "tmux select-pane -D" +bind -r C-k run "tmux select-pane -U" +bind -r C-l run "tmux select-pane -R" +bind -r C-\ run "tmux select-pane -l" +``` + +Troubleshooting +--------------- + +### Vim -> Tmux doesn't work! + +This is likely due to conflicting key mappings in your `~/.vimrc`. You can use +the following search pattern to find conflicting mappings +`\vn(nore)?map\s+\`. Any matching lines should be deleted or +altered to avoid conflicting with the mappings from the plugin. + +Another option is that the pattern matching included in the `.tmux.conf` is +not recognizing that Vim is active. To check that tmux is properly recognizing +Vim, use the provided Vim command `:TmuxNavigatorProcessList`. The output of +that command should be a list like: + +``` +Ss -zsh +S+ vim +S+ tmux +``` + +If you encounter a different output please [open an issue][] with as much info +about your OS, Vim version, and tmux version as possible. + +[open an issue]: https://github.com/christoomey/vim-tmux-navigator/issues/new + +### Tmux Can't Tell if Vim Is Active + +This functionality requires tmux version 1.8 or higher. You can check your +version to confirm with this shell command: + +``` bash +tmux -V # should return 'tmux 1.8' +``` + +### Switching out of Vim Is Slow + +If you find that navigation within Vim (from split to split) is fine, but Vim +to a non-Vim tmux pane is delayed, it might be due to a slow shell startup. +Consider moving code from your shell's non-interactive rc file (e.g., +`~/.zshenv`) into the interactive startup file (e.g., `~/.zshrc`) as Vim only +sources the non-interactive config. + +### It doesn't work in Vim's `terminal` mode + +Terminal mode is currently unsupported as adding this plugin's mappings there +causes conflict with movement mappings for FZF (it also uses terminal mode). +There's a conversation about this in https://github.com/christoomey/vim-tmux-navigator/pull/172 + +### It Doesn't Work in tmate + +[tmate][] is a tmux fork that aids in setting up remote pair programming +sessions. It is designed to run alongside tmux without issue, but occasionally +there are hiccups. Specifically, if the versions of tmux and tmate don't match, +you can have issues. See [this +issue](https://github.com/christoomey/vim-tmux-navigator/issues/27) for more +detail. + +[tmate]: http://tmate.io/ + +### It Still Doesn't Work!!! + +The tmux configuration uses an inlined grep pattern match to help determine if +the current pane is running Vim. If you run into any issues with the navigation +not happening as expected, you can try using [Mislav's original external +script][] which has a more robust check. + +[Brian Hogan]: https://twitter.com/bphogan +[Mislav's original external script]: https://github.com/mislav/dotfiles/blob/master/bin/tmux-vim-select-pane +[Vundle]: https://github.com/gmarik/vundle +[TPM]: https://github.com/tmux-plugins/tpm +[configuration section below]: #custom-key-bindings +[this blog post]: http://www.codeography.com/2013/06/19/navigating-vim-and-tmux-splits +[this gist]: https://gist.github.com/mislav/5189704 diff --git a/tmux/.tmux/plugins/vim-tmux-navigator/doc/tmux-navigator.txt b/tmux/.tmux/plugins/vim-tmux-navigator/doc/tmux-navigator.txt new file mode 100644 index 0000000..752abb4 --- /dev/null +++ b/tmux/.tmux/plugins/vim-tmux-navigator/doc/tmux-navigator.txt @@ -0,0 +1,39 @@ +*tmux-navigator.txt* Plugin to allow seamless navigation between tmux and vim + +============================================================================== +CONTENTS *tmux-navigator-contents* + + +============================================================================== +INTRODUCTION *tmux-navigator* + +Vim-tmux-navigator is a little plugin which enables seamless navigation +between tmux panes and vim splits. This plugin is a repackaging of Mislav +Marohinc's tmux=navigator configuration. When combined with a set of tmux key +bindings, the plugin will allow you to navigate seamlessly between vim and +tmux splits using a consistent set of hotkeys. + +NOTE: This requires tmux v1.8 or higher. + +============================================================================== +CONFIGURATION *tmux-navigator-configuration* + +* Activate autoupdate on exit + let g:tmux_navigator_save_on_switch = 1 + +* Disable vim->tmux navigation when the Vim pane is zoomed in tmux + let g:tmux_navigator_disable_when_zoomed = 1 + +* If the Vim pane is zoomed, stay zoomed when moving to another tmux pane + let g:tmux_navigator_preserve_zoom = 1 + +* Custom Key Bindings + let g:tmux_navigator_no_mappings = 1 + + nnoremap {Left-mapping} :TmuxNavigateLeft + nnoremap {Down-Mapping} :TmuxNavigateDown + nnoremap {Up-Mapping} :TmuxNavigateUp + nnoremap {Right-Mapping} :TmuxNavigateRight + nnoremap {Previous-Mapping} :TmuxNavigatePrevious + + vim:tw=78:ts=8:ft=help:norl: diff --git a/tmux/.tmux/plugins/vim-tmux-navigator/pattern-check b/tmux/.tmux/plugins/vim-tmux-navigator/pattern-check new file mode 100644 index 0000000..c5ecbb6 --- /dev/null +++ b/tmux/.tmux/plugins/vim-tmux-navigator/pattern-check @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# +# Collection of various test strings that could be the output of the tmux +# 'pane_current_comamnd' message. Included as regression test for updates to +# the inline grep pattern used in the `.tmux.conf` configuration + +set -e + +RED=$(tput setaf 1) +GREEN=$(tput setaf 2) +YELLOW=$(tput setaf 3) +NORMAL=$(tput sgr0) + +vim_pattern='(^|\/)g?(view|n?vim?x?)(diff)?$' +match_tests=(vim Vim VIM vimdiff /usr/local/bin/vim vi gvim view gview nvim vimx) +no_match_tests=( /Users/christoomey/.vim/thing /usr/local/bin/start-vim ) + +display_matches() { + for process_name in "$@"; do + printf "%s %s\n" "$(matches_vim_pattern $process_name)" "$process_name" + done +} + +matches_vim_pattern() { + if echo "$1" | grep -iqE "$vim_pattern"; then + echo "${GREEN}match${NORMAL}" + else + echo "${RED}fail${NORMAL}" + fi +} + +main() { + echo "Testing against pattern: ${YELLOW}$vim_pattern${NORMAL}\n" + + echo "These should all ${GREEN}match${NORMAL}\n----------------------" + display_matches "${match_tests[@]}" + + echo "\nThese should all ${RED}fail${NORMAL}\n---------------------" + display_matches "${no_match_tests[@]}" +} + +main diff --git a/tmux/.tmux/plugins/vim-tmux-navigator/plugin/tmux_navigator.vim b/tmux/.tmux/plugins/vim-tmux-navigator/plugin/tmux_navigator.vim new file mode 100644 index 0000000..45a8f7f --- /dev/null +++ b/tmux/.tmux/plugins/vim-tmux-navigator/plugin/tmux_navigator.vim @@ -0,0 +1,131 @@ +" Maps to switch vim splits in the given direction. If there are +" no more windows in that direction, forwards the operation to tmux. +" Additionally, toggles between last active vim splits/tmux panes. + +if exists("g:loaded_tmux_navigator") || &cp || v:version < 700 + finish +endif +let g:loaded_tmux_navigator = 1 + +function! s:VimNavigate(direction) + try + execute 'wincmd ' . a:direction + catch + echohl ErrorMsg | echo 'E11: Invalid in command-line window; executes, CTRL-C quits: wincmd k' | echohl None + endtry +endfunction + +if !get(g:, 'tmux_navigator_no_mappings', 0) + nnoremap :TmuxNavigateLeft + nnoremap :TmuxNavigateDown + nnoremap :TmuxNavigateUp + nnoremap :TmuxNavigateRight + nnoremap :TmuxNavigatePrevious +endif + +if empty($TMUX) + command! TmuxNavigateLeft call s:VimNavigate('h') + command! TmuxNavigateDown call s:VimNavigate('j') + command! TmuxNavigateUp call s:VimNavigate('k') + command! TmuxNavigateRight call s:VimNavigate('l') + command! TmuxNavigatePrevious call s:VimNavigate('p') + finish +endif + +command! TmuxNavigateLeft call s:TmuxAwareNavigate('h') +command! TmuxNavigateDown call s:TmuxAwareNavigate('j') +command! TmuxNavigateUp call s:TmuxAwareNavigate('k') +command! TmuxNavigateRight call s:TmuxAwareNavigate('l') +command! TmuxNavigatePrevious call s:TmuxAwareNavigate('p') + +if !exists("g:tmux_navigator_save_on_switch") + let g:tmux_navigator_save_on_switch = 0 +endif + +if !exists("g:tmux_navigator_disable_when_zoomed") + let g:tmux_navigator_disable_when_zoomed = 0 +endif + +if !exists("g:tmux_navigator_preserve_zoom") + let g:tmux_navigator_preserve_zoom = 0 +endif + +function! s:TmuxOrTmateExecutable() + return (match($TMUX, 'tmate') != -1 ? 'tmate' : 'tmux') +endfunction + +function! s:TmuxVimPaneIsZoomed() + return s:TmuxCommand("display-message -p '#{window_zoomed_flag}'") == 1 +endfunction + +function! s:TmuxSocket() + " The socket path is the first value in the comma-separated list of $TMUX. + return split($TMUX, ',')[0] +endfunction + +function! s:TmuxCommand(args) + let cmd = s:TmuxOrTmateExecutable() . ' -S ' . s:TmuxSocket() . ' ' . a:args + let l:x=&shellcmdflag + let &shellcmdflag='-c' + let retval=system(cmd) + let &shellcmdflag=l:x + return retval +endfunction + +function! s:TmuxNavigatorProcessList() + echo s:TmuxCommand("run-shell 'ps -o state= -o comm= -t ''''#{pane_tty}'''''") +endfunction +command! TmuxNavigatorProcessList call s:TmuxNavigatorProcessList() + +let s:tmux_is_last_pane = 0 +augroup tmux_navigator + au! + autocmd WinEnter * let s:tmux_is_last_pane = 0 +augroup END + +function! s:NeedsVitalityRedraw() + return exists('g:loaded_vitality') && v:version < 704 && !has("patch481") +endfunction + +function! s:ShouldForwardNavigationBackToTmux(tmux_last_pane, at_tab_page_edge) + if g:tmux_navigator_disable_when_zoomed && s:TmuxVimPaneIsZoomed() + return 0 + endif + return a:tmux_last_pane || a:at_tab_page_edge +endfunction + +function! s:TmuxAwareNavigate(direction) + let nr = winnr() + let tmux_last_pane = (a:direction == 'p' && s:tmux_is_last_pane) + if !tmux_last_pane + call s:VimNavigate(a:direction) + endif + let at_tab_page_edge = (nr == winnr()) + " Forward the switch panes command to tmux if: + " a) we're toggling between the last tmux pane; + " b) we tried switching windows in vim but it didn't have effect. + if s:ShouldForwardNavigationBackToTmux(tmux_last_pane, at_tab_page_edge) + if g:tmux_navigator_save_on_switch == 1 + try + update " save the active buffer. See :help update + catch /^Vim\%((\a\+)\)\=:E32/ " catches the no file name error + endtry + elseif g:tmux_navigator_save_on_switch == 2 + try + wall " save all the buffers. See :help wall + catch /^Vim\%((\a\+)\)\=:E141/ " catches the no file name error + endtry + endif + let args = 'select-pane -t ' . shellescape($TMUX_PANE) . ' -' . tr(a:direction, 'phjkl', 'lLDUR') + if g:tmux_navigator_preserve_zoom == 1 + let l:args .= ' -Z' + endif + silent call s:TmuxCommand(args) + if s:NeedsVitalityRedraw() + redraw! + endif + let s:tmux_is_last_pane = 1 + else + let s:tmux_is_last_pane = 0 + endif +endfunction diff --git a/tmux/.tmux/plugins/vim-tmux-navigator/vim-tmux-navigator.tmux b/tmux/.tmux/plugins/vim-tmux-navigator/vim-tmux-navigator.tmux new file mode 100755 index 0000000..0aeac55 --- /dev/null +++ b/tmux/.tmux/plugins/vim-tmux-navigator/vim-tmux-navigator.tmux @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +version_pat='s/^tmux[^0-9]*([.0-9]+).*/\1/p' + +is_vim="ps -o state= -o comm= -t '#{pane_tty}' \ + | grep -iqE '^[^TXZ ]+ +(\\S+\\/)?g?(view|n?vim?x?)(diff)?$'" +tmux bind-key -n C-h if-shell "$is_vim" "send-keys C-h" "select-pane -L" +tmux bind-key -n C-j if-shell "$is_vim" "send-keys C-j" "select-pane -D" +tmux bind-key -n C-k if-shell "$is_vim" "send-keys C-k" "select-pane -U" +tmux bind-key -n C-l if-shell "$is_vim" "send-keys C-l" "select-pane -R" +tmux_version="$(tmux -V | sed -En "$version_pat")" +tmux setenv -g tmux_version "$tmux_version" + +#echo "{'version' : '${tmux_version}', 'sed_pat' : '${version_pat}' }" > ~/.tmux_version.json + +tmux if-shell -b '[ "$(echo "$tmux_version < 3.0" | bc)" = 1 ]' \ + "bind-key -n 'C-\\' if-shell \"$is_vim\" 'send-keys C-\\' 'select-pane -l'" +tmux if-shell -b '[ "$(echo "$tmux_version >= 3.0" | bc)" = 1 ]' \ + "bind-key -n 'C-\\' if-shell \"$is_vim\" 'send-keys C-\\\\' 'select-pane -l'" + +tmux bind-key -T copy-mode-vi C-h select-pane -L +tmux bind-key -T copy-mode-vi C-j select-pane -D +tmux bind-key -T copy-mode-vi C-k select-pane -U +tmux bind-key -T copy-mode-vi C-l select-pane -R +tmux bind-key -T copy-mode-vi C-\\ select-pane -l diff --git a/tmux/.tmux/tmux-migrate-options.py b/tmux/.tmux/tmux-migrate-options.py new file mode 100644 index 0000000..6c10ce9 --- /dev/null +++ b/tmux/.tmux/tmux-migrate-options.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 +# +# USAGE: +# Back up your tmux old config, run the script and redirect stdout to your conf +# file. Example: +# +# $ cp ~/.tmux.conf ~/.tmux.conf.orig +# $ python ./tmux-migrate-options.py ~/.tmux.conf.orig > ~/.tmux.conf +# +# +# PURPOSE: +# This script accepts the path to a tmux conf file to read, then combines the +# deprecated -fg, -bg, and -attr options into the new -style option, and +# prints the updated tmux conf to stdout. The deprecated options were flagged +# for removal in tmux v1.9, and then removed in tmux v2.9 years later. +# +# The script will place the new "-style" option at the location of the last +# matching prefix for that option family. This means if your config file had +# a triplet of "window-status-bg", "window-status-fg", and "window-status-attr" +# settings, they will be merged into one line at that location with the option +# "window-status-style bg=...,fg=...,", in that order. +# +# If your config file has duplicate settings for, say, -fg, they will all +# appear on the same -style option line, in the same order which should give +# the same effect as before. +# +# The parser used here is a naive regex which should be enough for most configs. +# There is *no effort* to handle tmux conditional lines or cases where you +# already have -style options as well as old -fg, -bg, -attr options. +# If you rely on those to set colors and styles, then you may want to avoid +# this script as it will break your config. +# --- +# ISC License (ISC) +# +# Copyright 2019 April | tbutts +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +# FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. +# +from __future__ import print_function, unicode_literals +from io import open + +import os +import re +import sys + + +if len(sys.argv) < 2: + sys.exit("[ERR] specify path to tmux config file") + +tmux_conf = sys.argv[1] + +with open(tmux_conf, mode='r', encoding='utf-8') as tmux_file: + content = tmux_file.readlines() + +if not content: + sys.exit("[ERR] tmux config file appears to be empty") + +OPTION_PREFIXES=[ + "message-command-", + "message-", + "mode-", + "pane-active-border-", + "pane-border-", + "status-left-", + "status-right-", + "status-", + "window-active-", + "window-status-activity-", + "window-status-bell-", + "window-status-current-", + "window-status-last-", + "window-status-", + "window-", +] + +SET_OPTION_RE=r""" + ^\s*[^#]* # ignore comment lines + (?Pset\S*\s+-g) # set, setw, set-option, etc. + \s+ + {prefix} # any of the above option strings + (?P