├── .flake8 ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .idea ├── .gitignore ├── PathPicker.iml ├── inspectionProfiles │ └── profiles_settings.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── ZeroClipboard.min.js ├── ZeroClipboard.swf ├── bootstrap.min.css ├── font-awesome.css ├── font │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ └── fontawesome-webfont.woff ├── fpp-favicon.ico ├── heading@2x.png ├── launch_page.css └── og-image.png ├── debian ├── DEBIAN │ └── control ├── package.sh └── usr │ └── share │ ├── doc │ └── pathpicker │ │ ├── changelog │ │ └── copyright │ └── man │ └── man1 │ └── fpp.1 ├── fpp ├── fpp.rb ├── index.html ├── mypy.ini ├── poetry.lock ├── pyproject.toml ├── scripts ├── makeDist.sh ├── makeManpage.sh └── runTests.sh └── src ├── choose.py ├── pathpicker ├── __init__.py ├── char_code_mapping.py ├── color_printer.py ├── curses_api.py ├── formatted_text.py ├── key_bindings.py ├── line_format.py ├── logger.py ├── output.py ├── parse.py ├── repos.py ├── screen.py ├── screen_control.py ├── screen_flags.py ├── state_files.py └── usage_strings.py ├── print_help.py ├── process_input.py ├── tests ├── __init__.py ├── expected │ ├── abbreviatedLineSelect.txt │ ├── allInputBranch.txt │ ├── dontWipeChrome.txt │ ├── executeKeysEndKeySelectLast.txt │ ├── fileNameWithSpacesDescription.txt │ ├── gitAbbreviatedFiles.txt │ ├── gitDiffWithPageDown.txt │ ├── gitDiffWithPageDownColor.txt │ ├── gitDiffWithScroll.txt │ ├── gitDiffWithScrollUp.txt │ ├── gitDiffWithValidation.txt │ ├── longFileNames.txt │ ├── longFileNamesWithBeforeTextBug.txt │ ├── longFileTruncation.txt │ ├── longListEndKey.txt │ ├── longListHomeKey.txt │ ├── longListPageUpAndDown.txt │ ├── selectAllBug.txt │ ├── selectAllFromArg.txt │ ├── selectCommandWithPassedCommand.txt │ ├── selectDownSelect.txt │ ├── selectDownSelectInverse.txt │ ├── selectFirst.txt │ ├── selectFirstWithDown.txt │ ├── selectTwoCommandMode.txt │ ├── selectWithDownSelect.txt │ ├── selectWithDownSelectInverse.txt │ ├── simpleGitDiff.txt │ ├── simpleLoadAndQuit.txt │ ├── simpleSelectWithAttributes.txt │ ├── simpleSelectWithColor.txt │ ├── simpleWithAttributes.txt │ ├── tallLoadAndQuit.txt │ ├── tonsOfFiles.txt │ └── xModeWithSelect.txt ├── inputs │ ├── .DS_KINDA_STORE │ ├── NSArray+Utils.h │ ├── absoluteGitDiff.txt │ ├── annoying Spaces Folder │ │ └── evilFile With Space2.txt │ ├── annoying-hyphen-dir │ │ └── Package Control.system-bundle │ ├── annoyingTildeExtension.txt~ │ ├── blogredesign.sublime-workspace │ ├── evilFile No Prepend.txt │ ├── evilFile With Space.txt │ ├── file-from-yocto_%.bbappend │ ├── file-from-yocto_3.1%.bbappend │ ├── fileNamesWithSpaces.txt │ ├── gitAbbreviatedFiles.txt │ ├── gitBranch.txt │ ├── gitDiff.txt │ ├── gitDiffColor.txt │ ├── gitDiffNoStat.txt │ ├── gitDiffSomeExist.txt │ ├── gitLongDiff.txt │ ├── gitLongDiffColor.txt │ ├── longFileNames.txt │ ├── longFileNamesWithBeforeText.txt │ ├── longLineAbbreviated.txt │ ├── longList.txt │ ├── superLongFileNames.txt │ ├── svo (install the zip not me).xml │ ├── svo (install the zip, not me).xml │ ├── svo install the zip not me.xml │ ├── svo install the zip, not me.xml │ └── tonsOfFiles.txt ├── lib │ ├── __init__.py │ ├── curses_api.py │ ├── key_bindings.py │ ├── screen.py │ └── screen_test_runner.py ├── test_key_bindings_parsing.py ├── test_parsing.py └── test_screen.py └── version.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | extend-ignore = E203, W503 4 | copyright-check = True 5 | copyright-min-file-size = 1 6 | copyright-regexp = Copyright \(c\) Facebook\, Inc\. and its affiliates\. 7 | select = E,F,W,B,I,C 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Run tests 8 | strategy: 9 | matrix: 10 | os: [ubuntu-20.04, macos-latest] 11 | python-version: [3.6] 12 | poetry-version: [1.1.4] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Run image 22 | uses: abatilo/actions-poetry@v2.0.0 23 | with: 24 | poetry-version: ${{ matrix.poetry-version }} 25 | - name: Install dependencies 26 | run: poetry install 27 | - name: Run mypy 28 | run: poetry run mypy src 29 | - name: Run flake8 30 | run: poetry run flake8 src 31 | - name: Run pylint 32 | run: poetry run pylint src 33 | - name: Run tests 34 | run: poetry run pytest src/tests 35 | - name: Run vulture 36 | run: poetry run vulture src 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Vim swaps 2 | *.swo 3 | *.swp 4 | # python compiled files 5 | *.pyc 6 | # hg merge conflicts 7 | *.orig 8 | dist/* 9 | fpp.deb 10 | fpp*.deb 11 | debian/usr/ 12 | pathpicker*.deb 13 | # man page leftovers 14 | manpage.adoc 15 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/PathPicker.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Open Source Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | Using welcoming and inclusive language 12 | Being respectful of differing viewpoints and experiences 13 | Gracefully accepting constructive criticism 14 | Focusing on what is best for the community 15 | Showing empathy towards other community members 16 | Examples of unacceptable behavior by participants include: 17 | 18 | The use of sexualized language or imagery and unwelcome sexual attention or advances 19 | Trolling, insulting/derogatory comments, and personal or political attacks 20 | Public or private harassment 21 | Publishing others’ private information, such as a physical or electronic address, without explicit permission 22 | Other conduct which could reasonably be considered inappropriate in a professional setting 23 | 24 | ## Our Responsibilities 25 | 26 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 27 | 28 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 29 | 30 | ## Scope 31 | 32 | This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 33 | 34 | ## Enforcement 35 | 36 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at opensource-conduct@fb.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 37 | 38 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project’s leadership. 39 | 40 | ## Attribution 41 | 42 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 43 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 44 | 45 | [homepage]: https://www.contributor-covenant.org 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Facebook PathPicker 2 | 3 | Welcome to the community and thanks for stopping by! Facebook PathPicker is actually quite 4 | easy to contribute to -- it has minimal external and internal dependencies and just some 5 | simple input-output tests to run. 6 | 7 | The easiest way to get set up is to: 8 | * First, clone the repo with: 9 | * `git clone https://github.com/facebook/PathPicker.git` 10 | * Second, ensure you have Python 3 installed: 11 | * `python3 --version` 12 | * Go ahead and execute the script! 13 | * `cd PathPicker; ./fpp` 14 | 15 | The three areas to contribute to are: 16 | * Regex parsing (for detecting filenames and ignoring normal code) 17 | * UI functionality (either bug fixes with curses or new functionality) 18 | * General pipeline work related to our bash scripts. 19 | 20 | ### Pull Requests 21 | 22 | Send them over! Our bot will ask you to sign the CLA and after that someone 23 | from the team will start reviewing. 24 | 25 | Before sending them over, make sure the tests pass with: 26 | `./scripts/runTests.sh` 27 | 28 | ### Test dependencies 29 | 30 | * Install [poetry](https://github.com/python-poetry/poetry). 31 | * Select poetry environment with `poetry env use 3.6`. Some linters depend on Python version and it's better to check on the same version as we use in CI. 32 | 33 | ### PyCharm project 34 | You can open PathPicker in PyCharm. You will also need to install [poetry plugin](https://plugins.jetbrains.com/plugin/14307-poetry) for using poetry environment. 35 | 36 | ### Contributor License Agreement ("CLA") 37 | 38 | In order to accept your pull request, we need you to submit a CLA. You only need to do this once, so if you've done this for another Facebook open source project, you're good to go. If you are submitting a pull request for the first time, just let us know that you have completed the CLA and we can cross-check with your GitHub username. 39 | [Complete your CLA here](https://code.facebook.com/cla) 40 | 41 | ## Bugs 42 | 43 | ### Where to Find Known Issues 44 | 45 | We will be using GitHub Issues for our public bugs. It's worth checking there before reporting your own issue. 46 | 47 | ### Reporting New Issues 48 | 49 | Always try to provide a minimal test case that repros the bug. 50 | 51 | ### Documentation 52 | 53 | * Do not wrap lines at 80 characters - configure your editor to soft-wrap when editing documentation. 54 | 55 | ## License 56 | 57 | By contributing to Facebook PathPicker, you agree that your contributions will be licensed under its MIT license. 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Facebook, Inc. and its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PathPicker 2 | 3 | [![tests](https://github.com/facebook/PathPicker/workflows/tests/badge.svg)](https://github.com/facebook/PathPicker/actions) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | 5 | Facebook PathPicker is a simple command line tool that solves the perpetual 6 | problem of selecting files out of bash output. PathPicker will: 7 | * Parse all incoming lines for entries that look like files 8 | * Present the piped input in a convenient selector UI 9 | * Allow you to either: 10 | * Edit the selected files in your favorite `$EDITOR` 11 | * Execute an arbitrary command with them 12 | 13 | It is easiest to understand by watching a simple demo: 14 | 15 | 16 | 17 | ## Examples 18 | 19 | After installing PathPicker, using it is as easy as piping into `fpp`. It takes 20 | a wide variety of input -- try it with all the options below: 21 | 22 | * `git status | fpp` 23 | * `hg status | fpp` 24 | * `git grep "FooBar" | fpp` 25 | * `grep -r "FooBar" . | fpp` 26 | * `git diff HEAD~1 --stat | fpp` 27 | * `find . -iname "*.js" | fpp` 28 | * `arc inlines | fpp` 29 | 30 | and anything else you can dream up! 31 | 32 | ## Requirements 33 | 34 | PathPicker requires Python 3. 35 | 36 | ### Supported Shells 37 | 38 | * Bash is fully supported and works the best. 39 | * ZSH is supported as well, but won't have a few features like alias expansion in command line mode. 40 | * csh/fish/rc are supported in the latest version, but might have quirks or issues in older versions of PathPicker. Note: if your default shell and current shell is not in the same family (bash/zsh... v.s. fish/rc), you need to manually export environment variable `$SHELL` to your current shell. 41 | 42 | ## Installing PathPicker 43 | 44 | ### Homebrew 45 | 46 | Installing PathPicker is easiest with [Homebrew for mac](http://brew.sh/): 47 | 48 | * `brew update` (to pull down the recipe since it is new) 49 | * `brew install fpp` 50 | 51 | ### Linux 52 | 53 | On Debian-based systems, run these steps: 54 | [fakeroot](https://wiki.debian.org/FakeRoot): 55 | 56 | ``` 57 | $ git clone https://github.com/facebook/PathPicker.git 58 | $ cd PathPicker/debian 59 | $ ./package.sh 60 | $ ls ../pathpicker_*_all.deb 61 | ``` 62 | 63 | On Arch Linux, PathPicker can be installed from Arch User Repository (AUR). 64 | ([The AUR fpp-git package](https://aur.archlinux.org/packages/fpp-git/).) 65 | 66 | If you are on another system, or prefer manual installation, please 67 | follow the instructions given below. 68 | 69 | ### Manual Installation 70 | 71 | If you are on a system without Homebrew, it's still quite easy to install 72 | PathPicker, since it's essentially just a bash script that calls some Python. These 73 | steps more-or-less outline the process: 74 | 75 | * `cd /usr/local/ # or wherever you install apps` 76 | * `git clone https://github.com/facebook/PathPicker.git` 77 | * `cd PathPicker/` 78 | 79 | Here we create a symbolic link from the bash script in the repo 80 | to `/usr/local/bin/` which is assumed to be in the current 81 | `$PATH`: 82 | 83 | * `ln -s "$(pwd)/fpp" /usr/local/bin/fpp` 84 | * `fpp --help # should work!` 85 | 86 | ### Add-ons 87 | 88 | For tmux users, you can additionally install `tmux-fpp` which adds a key combination to run PathPicker on the last received `stdout`. 89 | This makes jumping into file selection mode even easier. ([Check it out here!](https://github.com/tmux-plugins/tmux-fpp)) 90 | 91 | 92 | ## Advanced Functionality 93 | 94 | As mentioned above, PathPicker allows you to also execute arbitrary commands using the specified files. 95 | Here is an example showing a `git checkout` command executed against the selected files: 96 | 97 | 98 | 99 | The selected files are appended to the command prefix to form the final command. If you need the files 100 | in the middle of your command, you can use the `$F` token instead, like: 101 | 102 | `cat $F | wc -l` 103 | 104 | Another important note is that PathPicker, by default, only selects files that exist on the filesystem. If you 105 | want to skip this (perhaps to selected deleted files in `git status`), just run PathPicker with the `--no-file-checks` (or `-nfc`, for short) flag. 106 | 107 | ## How PathPicker works 108 | 109 | PathPicker is a combination of a bash script and some small Python modules. 110 | It essentially has three steps: 111 | 112 | * Firstly, the bash script redirects all standards out into a python module that 113 | parses and extracts out filename candidates. These candidates are extracted with a series of 114 | regular expressions, since the input to PathPicker can be any `stdout` from another program. Rather 115 | than make specialized parsers for each program, we treat everything as noisy input, and select candidates via 116 | regexes. To limit the number of calls to the filesystem (to check existence), we are fairly restrictive on the 117 | candidates we extract. 118 | 119 | The downside to this is that files that are single words, with no extension (like `test`), that are not prepended by 120 | a directory will fail to match. This is a known limitation to PathPicker, and means that it will sometimes fail to find valid files in the input. 121 | 122 | * Next, a selector UI built with `curses` is presented to the user. At this point you can select a few files to edit, or input a command 123 | to execute. 124 | 125 | * Lastly, the python script outputs a command to a bash file that is later 126 | executed by the original bash script. 127 | 128 | It's not the most elegant architecture in the world but, in our opinion, it provides a lot of utility. 129 | 130 | ## Documentation & Configuration 131 | 132 | For all documentation and configuration options, see the output of `fpp --help`. 133 | 134 | ## Join the PathPicker community 135 | 136 | See the [CONTRIBUTING.md](https://github.com/facebook/PathPicker/blob/master/CONTRIBUTING.md) file for how to help out. 137 | 138 | ## License 139 | 140 | PathPicker is MIT licensed. 141 | -------------------------------------------------------------------------------- /assets/ZeroClipboard.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/PathPicker/c1dd1f7fc4dae5aa807218cf086942f1c9241783/assets/ZeroClipboard.swf -------------------------------------------------------------------------------- /assets/font/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/PathPicker/c1dd1f7fc4dae5aa807218cf086942f1c9241783/assets/font/fontawesome-webfont.eot -------------------------------------------------------------------------------- /assets/font/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/PathPicker/c1dd1f7fc4dae5aa807218cf086942f1c9241783/assets/font/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /assets/font/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/PathPicker/c1dd1f7fc4dae5aa807218cf086942f1c9241783/assets/font/fontawesome-webfont.woff -------------------------------------------------------------------------------- /assets/fpp-favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/PathPicker/c1dd1f7fc4dae5aa807218cf086942f1c9241783/assets/fpp-favicon.ico -------------------------------------------------------------------------------- /assets/heading@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/PathPicker/c1dd1f7fc4dae5aa807218cf086942f1c9241783/assets/heading@2x.png -------------------------------------------------------------------------------- /assets/launch_page.css: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) Facebook, Inc. and its affiliates. 3 | This source code is licensed under the MIT license found in the 4 | LICENSE file in the root directory of this source tree. 5 | **/ 6 | 7 | html { 8 | background-color:#fff; 9 | text-rendering: optimizelegibility; 10 | -webkit-text-size-adjust: none; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | } 14 | 15 | #brew-install-button { 16 | background-color: #4EC099; 17 | color: white; 18 | border: none; 19 | padding: 4px 8px; 20 | margin-left: 4px; 21 | font-family: "HelveticaNeue-Medium", "Helvetica Neue Medium", "Segoe UI", Helvetica, Arial, "Lucida Grande", sans-serif; 22 | font-size: 14px; 23 | line-height: 20px; 24 | } 25 | 26 | #brew-install-button i { 27 | position: relative; 28 | top: 1px; 29 | } 30 | 31 | body { 32 | font-family: "HelveticaNeue", "Helvetica Neue", "Segoe UI", Helvetica, Arial, "Lucida Grande", sans-serif; 33 | } 34 | 35 | .jumbotron.pathPickerHeader { 36 | background-color: #003945; 37 | color: white; 38 | font-family: "HelveticaNeue-Light","Helvetica Neue Light", "Helvetica Neue", "Segoe UI", Helvetica, Arial, "Lucida Grande", sans-serif; 39 | font-weight: 100; 40 | font-weight: lighter; 41 | padding: 0; 42 | } 43 | 44 | .heading p.lead { 45 | font-family: "HelveticaNeue-Medium", "Helvetica Neue Medium", "Segoe UI", Helvetica, Arial, "Lucida Grande", sans-serif; 46 | font-size: 14px; 47 | color: #809CA2; 48 | line-height: 24px; 49 | } 50 | 51 | .navbar-inverse .navbar-brand { 52 | font-weight: 600; 53 | font-size: 16px; 54 | line-height: 18px; 55 | color: #fff; 56 | } 57 | 58 | #logo { 59 | height: 40px; 60 | margin: 20px auto 20px auto; 61 | } 62 | 63 | .jumbotron .heading { 64 | text-align: center; 65 | padding: 0px 16px; 66 | padding-bottom: 60px; 67 | padding-top: 108px; 68 | } 69 | 70 | .container .row .asciiDemo { 71 | height: 540px; 72 | text-align: center; 73 | } 74 | 75 | .callToAction { 76 | text-align: center; 77 | background-color: #1F242A; 78 | padding: 24px 0; 79 | } 80 | 81 | .callToAction p { 82 | font-family: "Menlo-Regular", "Menlo Regular", "Menlo"; 83 | font-size: 12px; 84 | color: #809CA2; 85 | line-height: 18px; 86 | margin-bottom: 0; 87 | } 88 | 89 | .callToAction pre { 90 | display: inline-block; 91 | margin: 0; 92 | } 93 | 94 | div.row a.btn.btn-success { 95 | background-color: #4EC099; 96 | border-radius: 4px; 97 | border-style: none; 98 | font-family: "HelveticaNeue-Medium", "Helvetica Neue Medium", "Segoe UI", Helvetica, Arial, "Lucida Grande", sans-serif; 99 | font-size: 12px; 100 | color: #FFFFFF; 101 | line-height: 18px; 102 | padding: 8px 16px; 103 | margin-top: 20px; 104 | } 105 | 106 | #brew-install-button.zeroclipboard-is-hover, 107 | div.row a.btn.btn-success:hover { 108 | background-color: #4AB691; 109 | } 110 | 111 | .jumbotron { 112 | margin-bottom: 0; 113 | } 114 | 115 | h1 { 116 | font-family: "HelveticaNeue-Thin","Helvetica Neue Thin", "Helvetica Neue", "Segoe UI", Helvetica, Arial, "Lucida Grande", sans-serif; 117 | font-size: 64px; 118 | color: #FFFFFF; 119 | line-height: 80px; 120 | } 121 | 122 | h2 { 123 | font-family: "HelveticaNeue-Light","Helvetica Neue Light", "Helvetica Neue", "Segoe UI", Helvetica, Arial, "Lucida Grande", sans-serif; 124 | font-size: 36px; 125 | color: #1F242A; 126 | line-height: 54px; 127 | margin-bottom: 20px; 128 | } 129 | 130 | p { 131 | font-size: 16px; 132 | color: #787B7F; 133 | line-height: 24px; 134 | margin-bottom: 20px; 135 | } 136 | 137 | .row { 138 | margin: 80px auto; 139 | } 140 | 141 | .row:last-of-type { 142 | border-style: 1px solid red; 143 | margin-bottom: 80px; 144 | } 145 | 146 | .content { 147 | padding: 0px 36px 60px 36px; 148 | } 149 | 150 | hr { 151 | margin: 60px auto; 152 | border-top: 1px solid #EEE; 153 | } 154 | 155 | .navbar { 156 | margin-bottom: 0; 157 | } 158 | 159 | #navbar { 160 | padding-right: 36px; 161 | background-color: #1F242A; 162 | border: none; 163 | border-top: 1px solid #111; 164 | } 165 | 166 | .navbar-inverse { 167 | background-color: #1F242A; 168 | border: none; 169 | } 170 | 171 | .navbar-inverse .navbar-collapse { 172 | border: none; 173 | border-color: none; 174 | } 175 | 176 | .navbar-collapse { 177 | border-top: none; 178 | -webkit-box-shadow: none; 179 | box-shadow: none; 180 | } 181 | 182 | .navbar-inverse .navbar-toggle { 183 | border: none; 184 | } 185 | 186 | .navbar-toggle { 187 | float: right; 188 | margin-left: 5px; 189 | } 190 | 191 | .navbar-inverse .navbar-toggle:focus, .navbar-inverse .navbar-toggle:hover { 192 | background-color: #1F242A; 193 | } 194 | 195 | .navbar-inverse .navbar-nav>.active>a, .navbar-inverse .navbar-nav>.active>a:focus, .navbar-inverse .navbar-nav>.active>a:hover { 196 | background-color: rgba(0,0,0,0); 197 | display: none; 198 | } 199 | 200 | .navbar-right { 201 | float: left; 202 | } 203 | 204 | .navbar-nav { 205 | margin: 0 0px; 206 | } 207 | 208 | .nav>li>a { 209 | padding-left: 0; 210 | } 211 | 212 | .github-counter { 213 | margin-left: 0px; 214 | padding-top: 20px; 215 | padding-bottom: 48px; 216 | } 217 | 218 | .navbar-inverse .navbar-nav>li>a { 219 | padding-top: 20px; 220 | padding-bottom: 12px; 221 | color: #fff; 222 | } 223 | 224 | 225 | @media (min-width: 768px) { 226 | .navbar-inverse { 227 | background-color: #003945; 228 | border-style: none; 229 | } 230 | 231 | .nav iframe { 232 | padding: 15px auto; 233 | } 234 | 235 | .navbar { 236 | margin-bottom: 0; 237 | border-radius: 0; 238 | } 239 | 240 | #navbar { 241 | background-color: #003945; 242 | padding-left: 36px; 243 | padding-right: 36px; 244 | } 245 | 246 | .navbar-inverse .navbar-brand { 247 | display: none; 248 | } 249 | 250 | .navbar-inverse .navbar-nav>li>a { 251 | font-weight: 600; 252 | font-size: 14px; 253 | line-height: 18px; 254 | color: #809CA2; 255 | padding-top: 44px; 256 | padding-bottom: 44px; 257 | padding-right: 24px; 258 | } 259 | 260 | .navbar-inverse .navbar-nav>.active>a, .navbar-inverse .navbar-nav>.active>a:focus, .navbar-inverse .navbar-nav>.active>a:hover { 261 | background-color: #003945; 262 | display: inline-block; 263 | } 264 | 265 | .navbar-inverse .navbar-nav>li>a:first-of-type { 266 | padding-left: 0; 267 | } 268 | 269 | .github-counter { 270 | padding-top: 44px; 271 | padding-bottom: 44px; 272 | } 273 | 274 | .jumbotron .heading { 275 | padding-top: 0; 276 | } 277 | 278 | .container { 279 | padding-left: 0px; 280 | padding-right: 0px; 281 | } 282 | } 283 | /* Regarding video demo missing */ 284 | .mobile-show{ 285 | display: none; 286 | } 287 | .mobile-show img { 288 | width:300px; 289 | height: auto; 290 | } 291 | 292 | @media only screen 293 | and (min-device-width : 320px) 294 | and (max-device-width : 480px){ 295 | .mobile-show {display: inline; 296 | width: 50pxpx; 297 | height: 50px;} 298 | .mobile-hide {display: none;} 299 | } -------------------------------------------------------------------------------- /assets/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/PathPicker/c1dd1f7fc4dae5aa807218cf086942f1c9241783/assets/og-image.png -------------------------------------------------------------------------------- /debian/DEBIAN/control: -------------------------------------------------------------------------------- 1 | Package: pathpicker 2 | Version: __version__ 3 | Architecture: all 4 | Maintainer: Peter Cottle 5 | Installed-Size: 209 6 | Section: misc 7 | Priority: optional 8 | Description: Bash Output File Picker 9 | PathPicker parses piped input for files and presents it in a convenient UI. 10 | -------------------------------------------------------------------------------- /debian/package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # Copyright (c) Facebook, Inc. and its affiliates. 3 | # 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | PTH="$(pwd)" 7 | VERSION="$(python3 "$PTH/../src/version.py")" 8 | DATETIME=$(date '+%a, %d %b %Y %H:%M:%S %z') 9 | 10 | echo "Building fpp version $VERSION at $DATETIME" 11 | mkdir -p "$PTH/usr/bin" && 12 | mkdir -p "$PTH/usr/share/pathpicker/src/" 13 | 14 | sed s#__version__#"$VERSION"# < "$PTH/DEBIAN/control" > "$PTH/DEBIAN/control.modif" 15 | mv "$PTH/DEBIAN/control.modif" "$PTH/DEBIAN/control" 16 | 17 | echo "====================" 18 | echo "Control file is:" 19 | echo "====================" 20 | cat "$PTH/DEBIAN/control" 21 | echo "====================" 22 | 23 | cp -R "$PTH/../src" "$PTH/usr/share/pathpicker" && 24 | cp "$PTH/../fpp" "$PTH/usr/share/pathpicker/fpp" && 25 | cd "$PTH/usr/bin/" 26 | 27 | echo "Creating symlink..." 28 | ln -f -s ../share/pathpicker/fpp fpp 29 | sed s#__version__#"$VERSION"# < "$PTH/usr/share/doc/pathpicker/changelog" > "$PTH/usr/share/doc/pathpicker/changelog.modif" 30 | sed s#__date_timestamp__#"$DATETIME"# < "$PTH/usr/share/doc/pathpicker/changelog.modif" > "$PTH/usr/share/doc/pathpicker/changelog" 31 | 32 | echo "====================" 33 | echo "Changelog is:" 34 | echo "====================" 35 | cat "$PTH/usr/share/doc/pathpicker/changelog" 36 | echo "====================" 37 | 38 | echo "Gziping..." 39 | gzip -9 "$PTH/usr/share/doc/pathpicker/changelog" && 40 | rm "$PTH/usr/share/doc/pathpicker/changelog.modif" 41 | 42 | echo "Setting permissions..." 43 | cd "$PTH" 44 | find . -type d -exec chmod 755 {} \; 45 | find . -type f -exec chmod 644 {} \; 46 | 47 | echo "Building package..." 48 | rm "$PTH/package.sh" 49 | chmod 755 usr/share/pathpicker/fpp 50 | fakeroot -- sh -c "chown -R root:root * && dpkg --build ./ ../pathpicker_${VERSION}_all.deb ;" 51 | echo "Restoring template files..." 52 | cd - 53 | git checkout HEAD -- "$PTH/DEBIAN/control" "$PTH/usr/share/doc/pathpicker/changelog" "$PTH/package.sh" 54 | chmod 777 "$PTH/package.sh" 55 | 56 | echo 'Done! Check out fpp.deb' 57 | -------------------------------------------------------------------------------- /debian/usr/share/doc/pathpicker/changelog: -------------------------------------------------------------------------------- 1 | pathpicker (__version__) UNRELEASED; urgency=low 2 | 3 | * Release version __version__. 4 | 5 | -- Peter Cottle __date_timestamp__ 6 | -------------------------------------------------------------------------------- /debian/usr/share/doc/pathpicker/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: PathPicker 3 | Upstream-Contact: Peter Cottle 4 | License: BSD License 5 | 6 | For PathPicker software 7 | 8 | Copyright (c) 2015-present, Facebook, Inc. All rights reserved. 9 | 10 | Redistribution and use in source and binary forms, with or without modification, 11 | are permitted provided that the following conditions are met: 12 | 13 | * Redistributions of source code must retain the above copyright notice, this 14 | list of conditions and the following disclaimer. 15 | 16 | * Redistributions in binary form must reproduce the above copyright notice, 17 | this list of conditions and the following disclaimer in the documentation 18 | and/or other materials provided with the distribution. 19 | 20 | * Neither the name Facebook nor the names of its contributors may be used to 21 | endorse or promote products derived from this software without specific 22 | prior written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 25 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 26 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 28 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 30 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 31 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 33 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | -------------------------------------------------------------------------------- /debian/usr/share/man/man1/fpp.1: -------------------------------------------------------------------------------- 1 | '\" t 2 | .\" Title: fpp 3 | .\" Author: [FIXME: author] [see http://www.docbook.org/tdg5/en/html/author] 4 | .\" Generator: DocBook XSL Stylesheets vsnapshot 5 | .\" Date: 07/01/2021 6 | .\" Manual: \ \& 7 | .\" Source: \ \& 8 | .\" Language: English 9 | .\" 10 | .TH "FPP" "1" "07/01/2021" "\ \&" "\ \&" 11 | .\" ----------------------------------------------------------------- 12 | .\" * Define some portability stuff 13 | .\" ----------------------------------------------------------------- 14 | .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 15 | .\" http://bugs.debian.org/507673 16 | .\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html 17 | .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 18 | .ie \n(.g .ds Aq \(aq 19 | .el .ds Aq ' 20 | .\" ----------------------------------------------------------------- 21 | .\" * set default formatting 22 | .\" ----------------------------------------------------------------- 23 | .\" disable hyphenation 24 | .nh 25 | .\" disable justification (adjust text to left margin only) 26 | .ad l 27 | .\" ----------------------------------------------------------------- 28 | .\" * MAIN CONTENT STARTS HERE * 29 | .\" ----------------------------------------------------------------- 30 | .SH "NAME" 31 | fpp \- Facebook PathPicker; a command line tool for selecting files out of bash output 32 | .SH "SYNOPSIS" 33 | .sp 34 | .nf 35 | usage: fpp [\-h] [\-r] [\-\-version] [\-\-clean] [\-ko] [\-c COMMAND [COMMAND \&.\&.\&.]] 36 | [\-e EXECUTE_KEYS [EXECUTE_KEYS \&.\&.\&.]] [\-nfc] [\-ai] [\-ni] [\-a] 37 | 38 | optional arguments: 39 | \-h, \-\-help show this help message and exit 40 | \-r, \-\-record Record input and output\&. This is largely used for 41 | testing, but you may find it useful for scripting\&. 42 | \-\-version Print the version of fpp and exit\&. 43 | \-\-clean Remove the state files that fpp uses when starting up, 44 | including the previous input used and selection 45 | pickle\&. Useful when using fpp in a script context 46 | where the previous state should be discarded\&. 47 | \-ko, \-\-keep\-open keep PathPicker open once a file selection or command 48 | is performed\&. This will loop the program until Ctrl\-C 49 | is used to terminate the process\&. 50 | \-c COMMAND [COMMAND \&.\&.\&.], \-\-command COMMAND [COMMAND \&.\&.\&.] 51 | You may specify a command while invoking fpp that will 52 | be run once files have been selected\&. Normally, fpp 53 | opens your editor (see discussion of $EDITOR, $VISUAL, 54 | and $FPP_EDITOR) when you press enter\&. If you specify 55 | a command here, it will be invoked instead\&. 56 | \-e EXECUTE_KEYS [EXECUTE_KEYS \&.\&.\&.], \-\-execute\-keys EXECUTE_KEYS [EXECUTE_KEYS \&.\&.\&.] 57 | Automatically execute the given keys when the file 58 | list shows up\&. This is useful on certain cases, e\&.g\&. 59 | using "END" in order to automatically go to the last 60 | entry when there is a long list\&. 61 | \-nfc, \-\-no\-file\-checks 62 | You may want to turn off file system validation for a 63 | particular instance of PathPicker; this flag disables 64 | our internal logic for checking if a regex match is an 65 | actual file on the system\&. This is particularly useful 66 | when using PathPicker for an input of, say, deleted 67 | files in git status that you would like to restore to 68 | a given revision\&. It enables you to select the deleted 69 | files even though they do not exist on the system 70 | anymore\&. 71 | \-ai, \-\-all\-input You may force PathPicker to recognize all lines as 72 | acceptable input\&. Typically, PathPicker will scan the 73 | input for references to file paths\&. Passing this 74 | option will disable those scans and the program will 75 | assume that every input line is a match\&. In practice, 76 | this option allows for input selection for a variety 77 | of sources that would otherwise be unsupported \-\- git 78 | branches, mercurial bookmarks, etc\&. 79 | \-ni, \-\-non\-interactive 80 | Normally, the command that runs after you\*(Aqve chosen 81 | files to operate on is spawned in an interactive 82 | subshell\&. This allows you to use aliases and have 83 | access to environment variables defined in your 84 | startup files, but can have strange side\-effects when 85 | starting and stopping jobs and redirecting inputs\&. 86 | Using this flag runs your commands in a non\- 87 | interactive subshell, like a normal shell script\&. 88 | \-a, \-\-all Automatically select all available lines once the 89 | interactive editor has been entered\&. 90 | .fi 91 | .SH "INTRO" 92 | .sp 93 | To get started with fpp, pipe some kind of terminal output into the program\&. Examples include: 94 | .sp 95 | .RS 4 96 | .ie n \{\ 97 | \h'-04'\(bu\h'+03'\c 98 | .\} 99 | .el \{\ 100 | .sp -1 101 | .IP \(bu 2.3 102 | .\} 103 | git status | fpp 104 | .RE 105 | .sp 106 | .RS 4 107 | .ie n \{\ 108 | \h'-04'\(bu\h'+03'\c 109 | .\} 110 | .el \{\ 111 | .sp -1 112 | .IP \(bu 2.3 113 | .\} 114 | git show | fpp 115 | .RE 116 | .sp 117 | .RS 4 118 | .ie n \{\ 119 | \h'-04'\(bu\h'+03'\c 120 | .\} 121 | .el \{\ 122 | .sp -1 123 | .IP \(bu 2.3 124 | .\} 125 | git diff HEAD master | fpp 126 | .RE 127 | .sp 128 | .RS 4 129 | .ie n \{\ 130 | \h'-04'\(bu\h'+03'\c 131 | .\} 132 | .el \{\ 133 | .sp -1 134 | .IP \(bu 2.3 135 | .\} 136 | git diff HEAD~10 \-\-numstat | fpp 137 | .RE 138 | .sp 139 | .RS 4 140 | .ie n \{\ 141 | \h'-04'\(bu\h'+03'\c 142 | .\} 143 | .el \{\ 144 | .sp -1 145 | .IP \(bu 2.3 146 | .\} 147 | grep \-r "Banana" \&. | fpp 148 | .RE 149 | .sp 150 | .RS 4 151 | .ie n \{\ 152 | \h'-04'\(bu\h'+03'\c 153 | .\} 154 | .el \{\ 155 | .sp -1 156 | .IP \(bu 2.3 157 | .\} 158 | find \&. \-iname "*\&.js" | fpp 159 | .RE 160 | .sp 161 | Once fpp parses your input (and something that looks like a file matches), it will put you inside a pager that will allow you to select files with the following commands: 162 | .SH "NAVIGATION" 163 | .sp 164 | .RS 4 165 | .ie n \{\ 166 | \h'-04'\(bu\h'+03'\c 167 | .\} 168 | .el \{\ 169 | .sp -1 170 | .IP \(bu 2.3 171 | .\} 172 | [f] toggle the selection of a file 173 | .RE 174 | .sp 175 | .RS 4 176 | .ie n \{\ 177 | \h'-04'\(bu\h'+03'\c 178 | .\} 179 | .el \{\ 180 | .sp -1 181 | .IP \(bu 2.3 182 | .\} 183 | [F] toggle and move downward by 1 184 | .RE 185 | .sp 186 | .RS 4 187 | .ie n \{\ 188 | \h'-04'\(bu\h'+03'\c 189 | .\} 190 | .el \{\ 191 | .sp -1 192 | .IP \(bu 2.3 193 | .\} 194 | [A] toggle selection of all (unique) files 195 | .RE 196 | .sp 197 | .RS 4 198 | .ie n \{\ 199 | \h'-04'\(bu\h'+03'\c 200 | .\} 201 | .el \{\ 202 | .sp -1 203 | .IP \(bu 2.3 204 | .\} 205 | [down arrow|j] move downward by 1 206 | .RE 207 | .sp 208 | .RS 4 209 | .ie n \{\ 210 | \h'-04'\(bu\h'+03'\c 211 | .\} 212 | .el \{\ 213 | .sp -1 214 | .IP \(bu 2.3 215 | .\} 216 | [up arrow|k] move upward by 1 217 | .RE 218 | .sp 219 | .RS 4 220 | .ie n \{\ 221 | \h'-04'\(bu\h'+03'\c 222 | .\} 223 | .el \{\ 224 | .sp -1 225 | .IP \(bu 2.3 226 | .\} 227 | [] page down 228 | .RE 229 | .sp 230 | .RS 4 231 | .ie n \{\ 232 | \h'-04'\(bu\h'+03'\c 233 | .\} 234 | .el \{\ 235 | .sp -1 236 | .IP \(bu 2.3 237 | .\} 238 | [b] page up 239 | .RE 240 | .sp 241 | .RS 4 242 | .ie n \{\ 243 | \h'-04'\(bu\h'+03'\c 244 | .\} 245 | .el \{\ 246 | .sp -1 247 | .IP \(bu 2.3 248 | .\} 249 | [x] quick select mode 250 | .RE 251 | .sp 252 | .RS 4 253 | .ie n \{\ 254 | \h'-04'\(bu\h'+03'\c 255 | .\} 256 | .el \{\ 257 | .sp -1 258 | .IP \(bu 2.3 259 | .\} 260 | [d] describe file 261 | .RE 262 | .sp 263 | Once you have your files selected, you can either open them in your favorite text editor or execute commands with them via command mode: 264 | .sp 265 | .RS 4 266 | .ie n \{\ 267 | \h'-04'\(bu\h'+03'\c 268 | .\} 269 | .el \{\ 270 | .sp -1 271 | .IP \(bu 2.3 272 | .\} 273 | [] open all selected files (or file under cursor if none selected) in $EDITOR 274 | .RE 275 | .sp 276 | .RS 4 277 | .ie n \{\ 278 | \h'-04'\(bu\h'+03'\c 279 | .\} 280 | .el \{\ 281 | .sp -1 282 | .IP \(bu 2.3 283 | .\} 284 | [c] enter command mode 285 | .RE 286 | .SH "COMMAND MODE" 287 | .sp 288 | Command mode is helpful when you want to execute bash commands with the filenames you have selected\&. By default the filenames are appended automatically to command you enter before it is executed, so all you have to do is type the prefix\&. Some examples: 289 | .sp 290 | .RS 4 291 | .ie n \{\ 292 | \h'-04'\(bu\h'+03'\c 293 | .\} 294 | .el \{\ 295 | .sp -1 296 | .IP \(bu 2.3 297 | .\} 298 | git add 299 | .RE 300 | .sp 301 | .RS 4 302 | .ie n \{\ 303 | \h'-04'\(bu\h'+03'\c 304 | .\} 305 | .el \{\ 306 | .sp -1 307 | .IP \(bu 2.3 308 | .\} 309 | git checkout HEAD~1 \-\- 310 | .RE 311 | .sp 312 | .RS 4 313 | .ie n \{\ 314 | \h'-04'\(bu\h'+03'\c 315 | .\} 316 | .el \{\ 317 | .sp -1 318 | .IP \(bu 2.3 319 | .\} 320 | rm \-rf 321 | .RE 322 | .sp 323 | These commands get formatted into: * git add file1 file2 # etc * git checkout HEAD~1 \(em file1 file2 * rm \-rf file1 file2 # etc 324 | .sp 325 | If your command needs filenames in the middle, the token "$F" will be replaced with your selected filenames if it is found in the command string\&. Examples include: 326 | .sp 327 | .RS 4 328 | .ie n \{\ 329 | \h'-04'\(bu\h'+03'\c 330 | .\} 331 | .el \{\ 332 | .sp -1 333 | .IP \(bu 2.3 334 | .\} 335 | scp $F dev:~/backup 336 | .RE 337 | .sp 338 | .RS 4 339 | .ie n \{\ 340 | \h'-04'\(bu\h'+03'\c 341 | .\} 342 | .el \{\ 343 | .sp -1 344 | .IP \(bu 2.3 345 | .\} 346 | mv $F \&.\&./over/here 347 | .RE 348 | .sp 349 | Which format to: * scp file1 file2 dev:~/backup * mv file1 file2 \&.\&./over/here 350 | .SH "CONFIGURATION" 351 | .sp 352 | PathPicker offers a bit of configuration currently with more to come in the future\&. 353 | .sp 354 | Editor 355 | .sp 356 | The $FPP_EDITOR environment variable can be set to tell PathPicker which editor to open the selected files with\&. If that variable is not set, $VISUAL and then $EDITOR are used as fallbacks, with "vim" as a last resort\&. 357 | .sp 358 | The $FPP_DISABLE_SPLIT environment variable will disable splitting files into panes for vim clients (aka sequential editing)\&. 359 | .sp 360 | Directory 361 | .sp 362 | PathPicker saves state files for use when starting up, including the previous input used and selection pickle\&. By default, these files are saved in $XDG_CACHE_HOME/fpp, but the $FPP_DIR environment variable can be used to tell PathPicker to use another directory\&. 363 | .sp 364 | Colors 365 | .sp 366 | FPP will understand colors if the piped input uses them\&. In general, most tools do not unless requested to do so\&. 367 | .sp 368 | For git, try git config \-\-global color\&.ui always or use the command line option \-\-color\&. 369 | .sp 370 | For built in commands like ls, try \-G (on Mac, additionally export CLICOLOR_FORCE in your environment to anything\&.) 371 | -------------------------------------------------------------------------------- /fpp: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) Facebook, Inc. and its affiliates. 3 | # 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | 7 | # get the directory of this script so we can execute the related python 8 | # http://stackoverflow.com/a/246128/212110 9 | SOURCE=$0 10 | # resolve $SOURCE until the file is no longer a symlink 11 | while [ -h "$SOURCE" ]; do 12 | BASEDIR="$(cd -P "$(dirname "$SOURCE")" && pwd)" 13 | SOURCE="$(readlink "$SOURCE")" 14 | # if $SOURCE was a relative symlink, we need to resolve it relative to 15 | # the path where the symlink file was located 16 | [[ $SOURCE != /* ]] && SOURCE="$BASEDIR/$SOURCE" 17 | done 18 | BASEDIR="$(cd -P "$(dirname "$SOURCE")" && pwd)" 19 | 20 | PYTHONCMD="python3" 21 | NONINTERACTIVE=false 22 | 23 | # Setup according to XDG/Freedesktop standards as specified by 24 | # https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html 25 | if [ -z "$FPP_DIR" ]; then 26 | if [ -z "$XDG_CACHE_HOME" ]; then 27 | FPP_DIR="$HOME/.cache/fpp" 28 | else 29 | FPP_DIR="$XDG_CACHE_HOME/fpp" 30 | fi 31 | fi 32 | 33 | function doProgram { 34 | # process input from pipe and store as pickled file 35 | $PYTHONCMD "$BASEDIR/src/process_input.py" "$@" 36 | # if it failed, just fail now and exit the script 37 | # this works for the looping -ko case as well 38 | if [[ $? != 0 ]]; then exit $?; fi 39 | # now close stdin and choose input... 40 | exec 0<&- 41 | 42 | $PYTHONCMD "$BASEDIR/src/choose.py" "$@" < /dev/tty 43 | # Determine if running from within vim shell 44 | IFLAG="" 45 | if [ -z "$VIMRUNTIME" -a "$NONINTERACTIVE" = false ]; then 46 | IFLAG="-i" 47 | fi 48 | # execute the output bash script. For zsh or bash 49 | # shells, we delegate to $SHELL, but for all others 50 | # (fish, csh, etc) we delegate to bash. 51 | # 52 | # We use the following heuristics from 53 | # http://stackoverflow.com/questions/3327013/ 54 | # in order to determine which shell we are on 55 | if [ -n "$BASH" -o -n "$ZSH_NAME" ]; then 56 | if [ -e "${SHELL}" ]; then 57 | $SHELL $IFLAG "$FPP_DIR/.fpp.sh" < /dev/tty 58 | else 59 | (>&2 echo "Your SHELL bash variable ${SHELL} does not exist, please export it explicitly"); 60 | $BASH $IFLAG "$FPP_DIR/.fpp.sh" < /dev/tty 61 | fi 62 | else 63 | /bin/bash $IFLAG "$FPP_DIR/.fpp.sh" < /dev/tty 64 | fi 65 | } 66 | 67 | # we need to handle the --help option outside the python 68 | # flow since otherwise we will move into input selection... 69 | for opt in "$@"; do 70 | if [ "$opt" == "--debug" ]; then 71 | echo "Executing from '$BASEDIR'" 72 | elif [ "$opt" == "--version" ]; then 73 | VERSION="$($PYTHONCMD "$BASEDIR/src/version.py")" 74 | echo "fpp version $VERSION" 75 | exit 0 76 | elif [ "$opt" == "--python2" ]; then 77 | echo "Python 2 is no longer supported. Please use Python 3." 78 | exit 1 79 | elif [ "$opt" == "--help" -o "$opt" == "-h" ]; then 80 | $PYTHONCMD "$BASEDIR/src/print_help.py" 81 | exit 0 82 | elif [ "$opt" == "--record" -o "$opt" == "-r" ]; then 83 | echo "Recording input and output..." 84 | elif [ "$opt" == "--non-interactive" -o "$opt" == "-ni" ]; then 85 | NONINTERACTIVE=true 86 | elif [ "$opt" == "--keep-open" -o "$opt" == "-ko" ]; then 87 | # allow control-c to exit the loop 88 | # http://unix.stackexchange.com/a/48432 89 | trap "exit" INT 90 | while true; do 91 | doProgram "$@" 92 | # connect tty back to stdin since we closed it 93 | # earlier. this also works since we will only read 94 | # from stdin once and then go permanent interactive mode 95 | # http://stackoverflow.com/a/1992967/948126 96 | exec 0 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Facebook PathPicker 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 | Support Ukraine 🇺🇦 40 | 41 | Help Provide Humanitarian Aid to Ukraine 42 | 43 | . 44 |
45 |
46 | 76 | 77 |
78 |
79 |
80 | 81 |

Why Pipe when you can Pick?

82 |
83 |
84 |
85 |

86 | brew update; brew install fpp; fpp 87 | 96 |

97 |
98 |
99 |
100 |
101 |
102 |

Watch Introductory Video

103 | 107 |
108 |
109 |
110 |
111 |
112 |

Any Input

113 |

114 | PathPicker accepts a wide range of input -- output from git commands, grep results, searches -- pretty much anything. 115 |

116 |

117 | After parsing the input, PathPicker presents you with a nice UI to select which files you're interested in. After that 118 | you can open them in your favorite editor or execute arbitrary commands. 119 |

120 |

More on GitHub

121 |
122 |
123 | 124 | 125 | 126 |
127 | 128 |
129 |
130 |
131 |
132 |
133 |
134 |

Arbitrary Commands

135 |

136 | Besides opening files in your editor, PathPicker also allows you to execute arbitrary commands with the selected 137 | files. 138 |

139 |

140 | You can use your selection to add files to version control, change permissions, scp, delete -- pretty much 141 | whatever you want! 142 |

143 | More on GitHub 144 |
145 |
146 | 147 | 148 |
149 | 150 |
151 |
152 |
153 |
154 | 159 | 160 | 161 | 162 | 191 | 196 | 205 | 220 | 225 | 226 | 227 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | check_untyped_defs = true 3 | disallow_any_explicit = true 4 | disallow_any_unimported = true 5 | disallow_subclassing_any = true 6 | no_implicit_optional = true 7 | warn_no_return = true 8 | warn_redundant_casts = true 9 | warn_return_any = true 10 | warn_unreachable = true 11 | warn_unused_ignores = true 12 | disallow_untyped_calls = true 13 | disallow_incomplete_defs = true 14 | disallow_untyped_defs = true 15 | disallow_untyped_decorators = true 16 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.isort] 2 | profile = "black" 3 | multi_line_output = 3 4 | 5 | [tool.pylint.messages_control] 6 | disable = [ 7 | "fixme", 8 | "missing-class-docstring", 9 | "missing-function-docstring", 10 | "missing-module-docstring", 11 | "no-self-use", 12 | "too-many-arguments", 13 | "too-many-branches", 14 | "too-many-instance-attributes", 15 | "too-many-public-methods", 16 | "too-many-return-statements", 17 | ] 18 | 19 | [tool.pylint.format] 20 | max-line-length = "88" 21 | 22 | [tool.poetry] 23 | name = "pathpicker" 24 | version = "0.9.5" 25 | description = "PathPicker accepts a wide range of input -- output from git commands, grep results, searches -- pretty much anything. After parsing the input, PathPicker presents you with a nice UI to select which files you're interested in. After that you can open them in your favorite editor or execute arbitrary commands." 26 | authors = ["Peter Cottle "] 27 | license = "MIT" 28 | 29 | [tool.poetry.dependencies] 30 | python = "^3.6" 31 | 32 | [tool.poetry.dev-dependencies] 33 | black = "^20.8b1" 34 | flake8 = "^3.8.4" 35 | flake8-black = "^0.2.1" 36 | flake8-bugbear = "^20.11.1" 37 | flake8-comprehensions = "^3.3.1" 38 | flake8-copyright = "^0.2.2" 39 | flake8-eradicate = "^1.0.0" 40 | flake8-isort = "^4.0.0" 41 | flake8-use-fstring = "^1.1" 42 | isort = "^5.7.0" 43 | mypy = "^0.800" 44 | pylint = "^2.6.2" 45 | pytest = "^6.2.2" 46 | vulture = "^2.3" 47 | 48 | [build-system] 49 | requires = ["poetry-core>=1.0.0"] 50 | build-backend = "poetry.core.masonry.api" 51 | -------------------------------------------------------------------------------- /scripts/makeDist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) Facebook, Inc. and its affiliates. 3 | # 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | VERSION="$(python3 ./src/version.py)" 7 | DEST="./dist/fpp.$VERSION.tar.gz" 8 | mkdir -p ./dist/ 9 | tar -czf $DEST src/*.py fpp 10 | 11 | sed -i '' -e "s#url .*#url \"https://github.com/facebook/PathPicker/releases/download/$VERSION/fpp.$VERSION.tar.gz\"#g" ./fpp.rb 12 | HASH=$(cat $DEST | shasum -a 256 | cut -d " " -f 1) 13 | sed -i '' -e "s#^ sha256 .*# sha256 \"$HASH\"#g" ./fpp.rb 14 | 15 | echo "Recipe updated with hash from $DEST" 16 | -------------------------------------------------------------------------------- /scripts/makeManpage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) Facebook, Inc. and its affiliates. 3 | # 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | command -v a2x >/dev/null 2>&1 || { echo >&2 "I require a2x provided by asciidoc, but it's not installed. Aborting."; exit 1; } 7 | (cd src && python3 -m pathpicker.usage_strings > manpage.adoc) 8 | a2x --format manpage "src/manpage.adoc" --destination-dir debian/usr/share/man/man1/ 9 | -------------------------------------------------------------------------------- /scripts/runTests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) Facebook, Inc. and its affiliates. 3 | # 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | 7 | set -e 8 | 9 | # get the directory of this script so we can execute the related python 10 | # http://stackoverflow.com/a/246128/212110 11 | SOURCE=$0 12 | # resolve $SOURCE until the file is no longer a symlink 13 | while [ -h "$SOURCE" ]; do 14 | BASEDIR="$(cd -P "$(dirname "$SOURCE")" && pwd)" 15 | SOURCE="$(readlink "$SOURCE")" 16 | # if $SOURCE was a relative symlink, we need to resolve it relative to 17 | # the path where the symlink file was located 18 | [[ $SOURCE != /* ]] && SOURCE="$BASEDIR/$SOURCE" 19 | done 20 | BASEDIR="$(cd -P "$(dirname "$SOURCE")" && pwd)" 21 | REPO_ROOT="$BASEDIR/.." 22 | 23 | cd "$REPO_ROOT" 24 | 25 | poetry install 26 | poetry run mypy src 27 | poetry run flake8 src 28 | poetry run pylint src 29 | poetry run pytest src/tests 30 | poetry run vulture src 31 | -------------------------------------------------------------------------------- /src/choose.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | import curses 6 | import os 7 | import pickle 8 | import sys 9 | from typing import Dict, List, Optional 10 | 11 | from pathpicker import logger, output, screen_control, state_files 12 | from pathpicker.curses_api import CursesApi, CursesApiBase 13 | from pathpicker.key_bindings import KeyBindings, read_key_bindings 14 | from pathpicker.line_format import LineBase, LineMatch 15 | from pathpicker.screen import CursesScreen, ScreenBase 16 | from pathpicker.screen_flags import ScreenFlags 17 | 18 | LOAD_SELECTION_WARNING = """ 19 | WARNING! Loading the standard input and previous selection 20 | failed. This is probably due to a backwards compatibility issue 21 | with upgrading PathPicker or an internal error. Please pipe 22 | a new set of input to PathPicker to start fresh (after which 23 | this error will go away) 24 | """ 25 | 26 | 27 | def do_program( 28 | stdscr: ScreenBase, 29 | flags: ScreenFlags, 30 | key_bindings: Optional[KeyBindings] = None, 31 | curses_api: Optional[CursesApiBase] = None, 32 | line_objs: Optional[Dict[int, LineBase]] = None, 33 | ) -> None: 34 | # curses and lineObjs get dependency injected for 35 | # our tests, so init these if they are not provided 36 | if not key_bindings: 37 | key_bindings = read_key_bindings() 38 | if not curses_api: 39 | curses_api = CursesApi() 40 | if not line_objs: 41 | line_objs = get_line_objs() 42 | output.clear_file() 43 | logger.clear_file() 44 | screen = screen_control.Controller( 45 | flags, key_bindings, stdscr, line_objs, curses_api 46 | ) 47 | screen.control() 48 | 49 | 50 | def get_line_objs() -> Dict[int, LineBase]: 51 | file_path = state_files.get_pickle_file_path() 52 | try: 53 | line_objs: Dict[int, LineBase] = pickle.load(open(file_path, "rb")) 54 | except (OSError, KeyError, pickle.PickleError): 55 | output.append_error(LOAD_SELECTION_WARNING) 56 | output.append_exit() 57 | sys.exit(1) 58 | logger.add_event("total_num_files", len(line_objs)) 59 | 60 | selection_path = state_files.get_selection_file_path() 61 | if os.path.isfile(selection_path): 62 | set_selections_from_pickle(selection_path, line_objs) 63 | 64 | matches = [ 65 | line_obj for line_obj in line_objs.values() if isinstance(line_obj, LineMatch) 66 | ] 67 | if not matches: 68 | output.write_to_file('echo "No lines matched!";') 69 | output.append_exit() 70 | sys.exit(0) 71 | return line_objs 72 | 73 | 74 | def set_selections_from_pickle( 75 | selection_path: str, line_objs: Dict[int, LineBase] 76 | ) -> None: 77 | try: 78 | selected_indices = pickle.load(open(selection_path, "rb")) 79 | except (OSError, KeyError, pickle.PickleError): 80 | output.append_error(LOAD_SELECTION_WARNING) 81 | output.append_exit() 82 | sys.exit(1) 83 | for index in selected_indices: 84 | if index >= len(line_objs.items()): 85 | error = f"Found index {index} more than total matches" 86 | output.append_error(error) 87 | continue 88 | to_select = line_objs[index] 89 | if isinstance(to_select, LineMatch): 90 | to_select.set_select(True) 91 | else: 92 | error = f"Line {index} was selected but is not LineMatch" 93 | output.append_error(error) 94 | 95 | 96 | def main(argv: List[str]) -> int: 97 | file_path = state_files.get_pickle_file_path() 98 | if not os.path.exists(file_path): 99 | print("Nothing to do!") 100 | output.write_to_file('echo ":D";') 101 | output.append_exit() 102 | return 0 103 | output.clear_file() 104 | # we initialize our args *before* we move into curses 105 | # so we can benefit from the default argparse 106 | # behavior: 107 | flags = ScreenFlags.init_from_args(argv[1:]) 108 | curses.wrapper(lambda x: do_program(CursesScreen(x), flags)) 109 | return 0 110 | 111 | 112 | if __name__ == "__main__": 113 | sys.exit(main(sys.argv)) 114 | -------------------------------------------------------------------------------- /src/pathpicker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/PathPicker/c1dd1f7fc4dae5aa807218cf086942f1c9241783/src/pathpicker/__init__.py -------------------------------------------------------------------------------- /src/pathpicker/char_code_mapping.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | import curses 7 | from typing import Dict 8 | 9 | CODE_TO_CHAR: Dict[int, str] = {i: chr(i) for i in range(256)} 10 | CODE_TO_CHAR.update( 11 | (value, name[4:]) for name, value in vars(curses).items() if name.startswith("KEY_") 12 | ) 13 | # special exceptions 14 | CODE_TO_CHAR[10] = "ENTER" 15 | 16 | CHAR_TO_CODE: Dict[str, int] = {v: k for k, v in CODE_TO_CHAR.items()} 17 | -------------------------------------------------------------------------------- /src/pathpicker/color_printer.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | from typing import Optional 6 | 7 | from pathpicker.curses_api import CursesApiBase 8 | from pathpicker.screen import ScreenBase 9 | 10 | 11 | class ColorPrinter: 12 | 13 | """A thin wrapper over screens in ncurses that caches colors and 14 | attribute state""" 15 | 16 | DEFAULT_COLOR_INDEX = 1 17 | CURRENT_COLORS = -1 18 | 19 | def __init__(self, screen: ScreenBase, curses_api: CursesApiBase): 20 | self.colors = {} 21 | self.colors[(0, 0)] = 0 # 0,0 = white on black is hardcoded 22 | # in general, we want to use -1,-1 for most "normal" text printing 23 | self.colors[(-1, -1)] = self.DEFAULT_COLOR_INDEX 24 | self.curses_api = curses_api 25 | self.curses_api.init_pair(self.DEFAULT_COLOR_INDEX, -1, -1) 26 | self.screen = screen 27 | self.current_attributes = 0 # initialized in set_attributes 28 | 29 | def set_attributes(self, fg_color: int, bg_color: int, other: int) -> None: 30 | self.current_attributes = self.get_attributes(fg_color, bg_color, other) 31 | 32 | def get_attributes(self, fg_color: int, bg_color: int, other: int) -> int: 33 | color_index = -1 34 | color_pair = (fg_color, bg_color) 35 | if color_pair not in self.colors: 36 | new_index = len(self.colors) 37 | if new_index < self.curses_api.get_color_pairs(): 38 | self.curses_api.init_pair(new_index, fg_color, bg_color) 39 | self.colors[color_pair] = new_index 40 | color_index = new_index 41 | else: 42 | color_index = self.colors[color_pair] 43 | 44 | attr = self.curses_api.color_pair(color_index) 45 | 46 | attr = attr | other 47 | 48 | return attr 49 | 50 | def addstr( 51 | self, y_pos: int, x_pos: int, text: str, attr: Optional[int] = None 52 | ) -> None: 53 | if attr is None: 54 | attr = self.curses_api.color_pair(self.DEFAULT_COLOR_INDEX) 55 | elif attr == self.CURRENT_COLORS: 56 | attr = self.current_attributes 57 | 58 | self.screen.addstr(y_pos, x_pos, text, attr) 59 | 60 | def clear_square( 61 | self, top_y: int, bottom_y: int, left_x: int, right_x: int 62 | ) -> None: 63 | # clear out square from top to bottom 64 | for i in range(top_y, bottom_y): 65 | self.clear_segment(i, left_x, right_x) 66 | 67 | # perhaps there's a more elegant way to do this 68 | def clear_segment(self, y_pos: int, start_x: int, end_x: int) -> None: 69 | space_str = " " * (end_x - start_x) 70 | attr = self.curses_api.color_pair(self.DEFAULT_COLOR_INDEX) 71 | 72 | self.screen.addstr(y_pos, start_x, space_str, attr) 73 | -------------------------------------------------------------------------------- /src/pathpicker/curses_api.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | import curses 6 | import sys 7 | from abc import ABC, abstractmethod 8 | 9 | 10 | class CursesApiBase(ABC): 11 | @abstractmethod 12 | def use_default_colors(self) -> None: 13 | pass 14 | 15 | @abstractmethod 16 | def echo(self) -> None: 17 | pass 18 | 19 | @abstractmethod 20 | def noecho(self) -> None: 21 | pass 22 | 23 | @abstractmethod 24 | def init_pair(self, pair_number: int, fg_color: int, bg_color: int) -> None: 25 | pass 26 | 27 | @abstractmethod 28 | def color_pair(self, color_number: int) -> int: 29 | pass 30 | 31 | @abstractmethod 32 | def get_color_pairs(self) -> int: 33 | pass 34 | 35 | @abstractmethod 36 | def exit(self) -> None: 37 | pass 38 | 39 | @abstractmethod 40 | def allow_file_output(self) -> bool: 41 | pass 42 | 43 | 44 | class CursesApi(CursesApiBase): 45 | 46 | """A dummy curses wrapper that allows us to intercept these 47 | calls when in a test environment""" 48 | 49 | def use_default_colors(self) -> None: 50 | curses.use_default_colors() 51 | 52 | def echo(self) -> None: 53 | curses.echo() 54 | 55 | def noecho(self) -> None: 56 | curses.noecho() 57 | 58 | def init_pair(self, pair_number: int, fg_color: int, bg_color: int) -> None: 59 | curses.init_pair(pair_number, fg_color, bg_color) 60 | 61 | def color_pair(self, color_number: int) -> int: 62 | return curses.color_pair(color_number) 63 | 64 | def get_color_pairs(self) -> int: 65 | assert hasattr(curses, "COLOR_PAIRS"), "curses is not initialized!" 66 | return curses.COLOR_PAIRS 67 | 68 | def exit(self) -> None: 69 | sys.exit(0) 70 | 71 | def allow_file_output(self) -> bool: 72 | return True 73 | -------------------------------------------------------------------------------- /src/pathpicker/formatted_text.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | import curses 6 | import re 7 | from collections import namedtuple 8 | from typing import Optional, Tuple 9 | 10 | from pathpicker.color_printer import ColorPrinter 11 | 12 | 13 | class FormattedText: 14 | 15 | """A piece of ANSI escape formatted text which responds 16 | to str() returning the plain text and knows how to print 17 | itself out using ncurses""" 18 | 19 | ANSI_ESCAPE_FORMATTING = r"\x1b\[([^mK]*)[mK]" 20 | BOLD_ATTRIBUTE = 1 21 | UNDERLINE_ATTRIBUTE = 4 22 | Range = namedtuple("Range", "bottom top") 23 | FOREGROUND_RANGE = Range(30, 39) 24 | BACKGROUND_RANGE = Range(40, 49) 25 | 26 | def __init__(self, text: Optional[str] = None): 27 | self.text = text 28 | 29 | if self.text is not None: 30 | self.segments = re.split(self.ANSI_ESCAPE_FORMATTING, self.text) 31 | # re.split will insert a empty string if there is a match at the beginning 32 | # or it will return [string] if there is no match 33 | # create the invariant that every segment has a formatting segment, e.g 34 | # we will always have FORMAT, TEXT, FORMAT, TEXT 35 | self.segments.insert(0, "") 36 | self.plain_text = "".join(self.segments[1::2]) 37 | 38 | def __str__(self) -> str: 39 | return self.plain_text 40 | 41 | @classmethod 42 | def parse_formatting(cls, formatting: str) -> Tuple[int, int, int]: 43 | """Parse ANSI formatting; the formatting passed in should be 44 | stripped of the control characters and ending character""" 45 | fg_color = -1 # -1 default means "use default", not "use white/black" 46 | bg_color = -1 47 | other = 0 48 | int_values = [int(value) for value in formatting.split(";") if value] 49 | for code in int_values: 50 | if cls.FOREGROUND_RANGE.bottom <= code <= cls.FOREGROUND_RANGE.top: 51 | fg_color = code - cls.FOREGROUND_RANGE.bottom 52 | elif cls.BACKGROUND_RANGE.bottom <= code <= cls.BACKGROUND_RANGE.top: 53 | bg_color = code - cls.BACKGROUND_RANGE.bottom 54 | elif code == cls.BOLD_ATTRIBUTE: 55 | other = other | curses.A_BOLD 56 | elif code == cls.UNDERLINE_ATTRIBUTE: 57 | other = other | curses.A_UNDERLINE 58 | 59 | return fg_color, bg_color, other 60 | 61 | @classmethod 62 | def get_sequence_for_attributes( 63 | cls, fg_color: int, bg_color: int, attr: int 64 | ) -> str: 65 | """Return a fully formed escape sequence for the color pair 66 | and additional attributes""" 67 | return ( 68 | "\x1b[" 69 | + str(cls.FOREGROUND_RANGE.bottom + fg_color) 70 | + ";" 71 | + str(cls.BACKGROUND_RANGE.bottom + bg_color) 72 | + ";" 73 | + str(attr) 74 | + "m" 75 | ) 76 | 77 | def print_text( 78 | self, y_pos: int, x_pos: int, printer: ColorPrinter, max_len: int 79 | ) -> None: 80 | """Print out using ncurses. Note that if any formatting changes 81 | occur, the attribute set is changed and not restored""" 82 | printed_so_far = 0 83 | for index, val in enumerate(self.segments): 84 | if printed_so_far >= max_len: 85 | break 86 | if index % 2 == 1: 87 | # text 88 | to_print = val[0 : max_len - printed_so_far] 89 | printer.addstr( 90 | y_pos, x_pos + printed_so_far, to_print, ColorPrinter.CURRENT_COLORS 91 | ) 92 | printed_so_far += len(to_print) 93 | else: 94 | # formatting 95 | printer.set_attributes(*self.parse_formatting(val)) 96 | 97 | def find_segment_place(self, to_go: int) -> Tuple[int, int]: 98 | index = 1 99 | 100 | while index < len(self.segments): 101 | to_go -= len(self.segments[index]) 102 | if to_go < 0: 103 | return index, to_go 104 | 105 | index += 2 106 | 107 | if to_go == 0: 108 | # we could reach here if the requested place is equal 109 | # to the very end of the string (as we do a <0 above). 110 | return index - 2, len(self.segments[index - 2]) 111 | raise AssertionError("Unreachable") 112 | 113 | def breakat(self, where: int) -> Tuple["FormattedText", "FormattedText"]: 114 | """Break the formatted text at the point given and return 115 | a new tuple of two FormattedText representing the before and 116 | after""" 117 | # FORMAT, TEXT, FORMAT, TEXT, FORMAT, TEXT 118 | # --before----, segF, seg, ----after-- 119 | # 120 | # to 121 | # 122 | # FORMAT, TEXT, FORMAT, TEXTBEFORE, FORMAT, TEXTAFTER, FORMAT, TEXT 123 | # --before----, segF, [before], segF, [after], -----after-- 124 | # ----index---------------/ 125 | (index, split_point) = self.find_segment_place(where) 126 | text_segment = self.segments[index] 127 | before_text = text_segment[:split_point] 128 | after_text = text_segment[split_point:] 129 | before_segments = self.segments[:index] 130 | after_segments = self.segments[index + 1 :] 131 | 132 | formatting_for_segment = self.segments[index - 1] 133 | 134 | before_formatted_text = FormattedText() 135 | after_formatted_text = FormattedText() 136 | before_formatted_text.segments = before_segments + [before_text] 137 | after_formatted_text.segments = ( 138 | [formatting_for_segment] + [after_text] + after_segments 139 | ) 140 | before_formatted_text.plain_text = self.plain_text[:where] 141 | after_formatted_text.plain_text = self.plain_text[where:] 142 | 143 | return before_formatted_text, after_formatted_text 144 | -------------------------------------------------------------------------------- /src/pathpicker/key_bindings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | import configparser 6 | import os 7 | from typing import List, NewType, Tuple 8 | 9 | from pathpicker.state_files import FPP_DIR 10 | 11 | KEY_BINDINGS_FILE = os.path.join(FPP_DIR, ".fpp.keys") 12 | 13 | 14 | KeyBindings = NewType("KeyBindings", List[Tuple[str, str]]) 15 | 16 | 17 | def read_key_bindings(key_bindings_file: str = KEY_BINDINGS_FILE) -> KeyBindings: 18 | """Returns configured key bindings, in the format [(key, command), ...]. 19 | The ordering of the entries is not guaranteed, although it's irrelevant 20 | to the purpose. 21 | """ 22 | config_file_path = os.path.expanduser(key_bindings_file) 23 | parser = configparser.ConfigParser() 24 | parser.read(config_file_path) 25 | 26 | bindings = KeyBindings([]) 27 | if parser.has_section("bindings"): 28 | bindings = KeyBindings(parser.items("bindings")) 29 | return bindings 30 | -------------------------------------------------------------------------------- /src/pathpicker/line_format.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | import curses 6 | import os 7 | import subprocess 8 | import time 9 | from abc import ABC, abstractmethod 10 | from pathlib import Path 11 | from typing import TYPE_CHECKING, Optional, Tuple 12 | 13 | from pathpicker import parse 14 | from pathpicker.color_printer import ColorPrinter 15 | from pathpicker.formatted_text import FormattedText 16 | from pathpicker.parse import MatchResult 17 | 18 | if TYPE_CHECKING: 19 | from pathpicker.screen_control import Controller 20 | 21 | 22 | class LineBase(ABC): 23 | def __init__(self) -> None: 24 | self.controller: Optional["Controller"] = None 25 | 26 | def set_controller(self, controller: "Controller") -> None: 27 | self.controller = controller 28 | 29 | @abstractmethod 30 | def output(self, printer: ColorPrinter) -> None: 31 | pass 32 | 33 | 34 | class SimpleLine(LineBase): 35 | def __init__(self, formatted_line: FormattedText, index: int): 36 | super().__init__() 37 | self.formatted_line = formatted_line 38 | self.index = index 39 | 40 | def output(self, printer: ColorPrinter) -> None: 41 | assert self.controller is not None 42 | (min_x, min_y, max_x, max_y) = self.controller.get_chrome_boundaries() 43 | max_len = min(max_x - min_x, len(str(self))) 44 | y_pos = min_y + self.index + self.controller.get_scroll_offset() 45 | 46 | if y_pos < min_y or y_pos >= max_y: 47 | # won't be displayed! 48 | return 49 | 50 | self.formatted_line.print_text(y_pos, min_x, printer, max_len) 51 | 52 | def __str__(self) -> str: 53 | return str(self.formatted_line) 54 | 55 | 56 | class LineMatch(LineBase): 57 | ARROW_DECORATOR = "|===>" 58 | # this is inserted between long files, so it looks like 59 | # ./src/foo/bar/something|...|baz/foo.py 60 | TRUNCATE_DECORATOR = "|...|" 61 | 62 | def __init__( 63 | self, 64 | formatted_line: FormattedText, 65 | result: MatchResult, 66 | index: int, 67 | validate_file_exists: bool = False, 68 | all_input: bool = False, 69 | ): 70 | super().__init__() 71 | 72 | self.formatted_line = formatted_line 73 | self.index = index 74 | self.all_input = all_input 75 | 76 | path, num, matches = result 77 | 78 | self.path = ( 79 | path 80 | if all_input 81 | else parse.prepend_dir(path, with_file_inspection=validate_file_exists) 82 | ) 83 | self.num = num 84 | 85 | line = str(self.formatted_line) 86 | # save a bunch of stuff so we can 87 | # pickle 88 | self.start = matches.start() 89 | self.end = min(matches.end(), len(line)) 90 | self.group: str = matches.group() 91 | 92 | # this is a bit weird but we need to strip 93 | # off the whitespace for the matches we got, 94 | # since matches like README are aggressive 95 | # about including whitespace. For most lines 96 | # this will be a no-op, but for lines like 97 | # "README " we will reset end to 98 | # earlier 99 | string_subset = line[self.start : self.end] 100 | stripped_subset = string_subset.strip() 101 | trailing_whitespace = len(string_subset) - len(stripped_subset) 102 | self.end -= trailing_whitespace 103 | self.group = self.group[0 : len(self.group) - trailing_whitespace] 104 | 105 | self.selected = False 106 | self.hovered = False 107 | self.is_truncated = False 108 | 109 | # precalculate the pre, post, and match strings 110 | (self.before_text, _) = self.formatted_line.breakat(self.start) 111 | (_, self.after_text) = self.formatted_line.breakat(self.end) 112 | 113 | self.decorated_match = FormattedText() 114 | self.update_decorated_match() 115 | 116 | def toggle_select(self) -> None: 117 | self.set_select(not self.selected) 118 | 119 | def set_select(self, val: bool) -> None: 120 | self.selected = val 121 | self.update_decorated_match() 122 | 123 | def set_hover(self, val: bool) -> None: 124 | self.hovered = val 125 | self.update_decorated_match() 126 | 127 | def get_screen_index(self) -> int: 128 | return self.index 129 | 130 | def get_path(self) -> str: 131 | return self.path 132 | 133 | def get_file_size(self) -> str: 134 | size = os.path.getsize(self.path) 135 | for unit in ["B", "K", "M", "G", "T", "P", "E", "Z"]: 136 | if size < 1024: 137 | return f"size: {size}{unit}" 138 | size //= 1024 139 | raise AssertionError("Unreachable") 140 | 141 | def get_length_in_lines(self) -> str: 142 | output = subprocess.check_output(["wc", "-l", self.path]) 143 | lines_count = output.strip().split()[0].decode("utf-8") 144 | lines_caption = "lines" if int(lines_count) > 1 else "line" 145 | return f"length: {lines_count} {lines_caption}" 146 | 147 | def get_time_last_accessed(self) -> str: 148 | time_accessed = time.strftime( 149 | "%m/%d/%Y %H:%M:%S", time.localtime(os.stat(self.path).st_atime) 150 | ) 151 | return f"last accessed: {time_accessed}" 152 | 153 | def get_time_last_modified(self) -> str: 154 | time_modified = time.strftime( 155 | "%m/%d/%Y %H:%M:%S", time.localtime(os.stat(self.path).st_mtime) 156 | ) 157 | return f"last modified: {time_modified}" 158 | 159 | def get_owner_user(self) -> str: 160 | user_owner_name = Path(self.path).owner() 161 | user_owner_id = os.stat(self.path).st_uid 162 | return f"owned by user: {user_owner_name}, {user_owner_id}" 163 | 164 | def get_owner_group(self) -> str: 165 | group_owner_name = Path(self.path).group() 166 | group_owner_id = os.stat(self.path).st_gid 167 | return f"owned by group: {group_owner_name}, {group_owner_id}" 168 | 169 | def get_dir(self) -> str: 170 | return os.path.dirname(self.path) 171 | 172 | def is_resolvable(self) -> bool: 173 | return not self.is_git_abbreviated_path() 174 | 175 | def is_git_abbreviated_path(self) -> bool: 176 | # this method mainly serves as a warning for when we get 177 | # git-abbreviated paths like ".../" that confuse users. 178 | parts = self.path.split(os.path.sep) 179 | return len(parts) > 0 and parts[0] == "..." 180 | 181 | def get_line_num(self) -> int: 182 | return self.num 183 | 184 | def get_selected(self) -> bool: 185 | return self.selected 186 | 187 | def get_before(self) -> str: 188 | return str(self.before_text) 189 | 190 | def get_after(self) -> str: 191 | return str(self.after_text) 192 | 193 | def get_match(self) -> str: 194 | return self.group 195 | 196 | def __str__(self) -> str: 197 | return ( 198 | self.get_before() 199 | + "||" 200 | + self.get_match() 201 | + "||" 202 | + self.get_after() 203 | + "||" 204 | + str(self.num) 205 | ) 206 | 207 | def update_decorated_match(self, max_len: Optional[int] = None) -> None: 208 | """Update the cached decorated match formatted string, and 209 | dirty the line, if needed""" 210 | if self.hovered and self.selected: 211 | attributes = ( 212 | curses.COLOR_WHITE, 213 | curses.COLOR_RED, 214 | FormattedText.BOLD_ATTRIBUTE, 215 | ) 216 | elif self.hovered: 217 | attributes = ( 218 | curses.COLOR_WHITE, 219 | curses.COLOR_BLUE, 220 | FormattedText.BOLD_ATTRIBUTE, 221 | ) 222 | elif self.selected: 223 | attributes = ( 224 | curses.COLOR_WHITE, 225 | curses.COLOR_GREEN, 226 | FormattedText.BOLD_ATTRIBUTE, 227 | ) 228 | elif not self.all_input: 229 | attributes = (0, 0, FormattedText.UNDERLINE_ATTRIBUTE) 230 | else: 231 | attributes = (0, 0, 0) 232 | 233 | decorator_text = self.get_decorator() 234 | 235 | # we may not be connected to a controller (during process_input, 236 | # for example) 237 | if self.controller: 238 | self.controller.dirty_line(self.index) 239 | 240 | plain_text = decorator_text + self.get_match() 241 | if max_len and len(plain_text + str(self.before_text)) > max_len: 242 | # alright, we need to chop the ends off of our 243 | # decorated match and glue them together with our 244 | # truncation decorator. We subtract the length of the 245 | # before text since we consider that important too. 246 | space_allowed = ( 247 | max_len 248 | - len(self.TRUNCATE_DECORATOR) 249 | - len(decorator_text) 250 | - len(str(self.before_text)) 251 | ) 252 | mid_point = int(space_allowed / 2) 253 | begin_match = plain_text[0:mid_point] 254 | end_match = plain_text[-mid_point : len(plain_text)] 255 | plain_text = begin_match + self.TRUNCATE_DECORATOR + end_match 256 | 257 | self.decorated_match = FormattedText( 258 | FormattedText.get_sequence_for_attributes(*attributes) + plain_text 259 | ) 260 | 261 | def get_decorator(self) -> str: 262 | if self.selected: 263 | return self.ARROW_DECORATOR 264 | return "" 265 | 266 | def print_up_to( 267 | self, 268 | text: FormattedText, 269 | printer: ColorPrinter, 270 | y_pos: int, 271 | x_pos: int, 272 | max_len: int, 273 | ) -> Tuple[int, int]: 274 | """Attempt to print maxLen characters, returning a tuple 275 | (x, maxLen) updated with the actual number of characters 276 | printed""" 277 | if max_len <= 0: 278 | return x_pos, max_len 279 | 280 | max_printable = min(len(str(text)), max_len) 281 | text.print_text(y_pos, x_pos, printer, max_printable) 282 | return x_pos + max_printable, max_len - max_printable 283 | 284 | def output(self, printer: ColorPrinter) -> None: 285 | assert self.controller is not None 286 | (min_x, min_y, max_x, max_y) = self.controller.get_chrome_boundaries() 287 | y_pos = min_y + self.index + self.controller.get_scroll_offset() 288 | 289 | if y_pos < min_y or y_pos >= max_y: 290 | # won't be displayed! 291 | return 292 | 293 | # we dont care about the after text, but we should be able to see 294 | # all of the decorated match (which means we need to see up to 295 | # the end of the decoratedMatch, aka include beforeText) 296 | important_text_length = len(str(self.before_text)) + len( 297 | str(self.decorated_match) 298 | ) 299 | space_for_printing = max_x - min_x 300 | if important_text_length > space_for_printing: 301 | # hrm, we need to update our decorated match to show 302 | # a truncated version since right now we will print off 303 | # the screen. lets also dump the beforeText for more 304 | # space 305 | self.update_decorated_match(max_len=space_for_printing) 306 | self.is_truncated = True 307 | else: 308 | # first check what our expanded size would be: 309 | expanded_size = len(str(self.before_text)) + len(self.get_match()) 310 | if expanded_size < space_for_printing and self.is_truncated: 311 | # if the screen gets resized, we might be truncated 312 | # from a previous render but **now** we have room. 313 | # in that case lets expand back out 314 | self.update_decorated_match() 315 | self.is_truncated = False 316 | 317 | max_len = max_x - min_x 318 | so_far = (min_x, max_len) 319 | 320 | so_far = self.print_up_to(self.before_text, printer, y_pos, *so_far) 321 | so_far = self.print_up_to(self.decorated_match, printer, y_pos, *so_far) 322 | so_far = self.print_up_to(self.after_text, printer, y_pos, *so_far) 323 | -------------------------------------------------------------------------------- /src/pathpicker/logger.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | import getpass 6 | import json 7 | from typing import List, NamedTuple, Optional, Tuple 8 | 9 | from pathpicker import state_files 10 | 11 | # This file just outputs some simple log events that are consumed by 12 | # another service for internal logging at Facebook. Use it if you want 13 | # to, or disable it if you want. 14 | 15 | 16 | class LoggingEvent(NamedTuple): # TypedDict from Python 3.8 needs to be used here. 17 | unixname: str 18 | num: Optional[int] 19 | eventname: str 20 | 21 | 22 | def write_to_file(content: str) -> None: 23 | file = open(state_files.get_logger_file_path(), "w") 24 | file.write(content) 25 | file.close() 26 | 27 | 28 | def clear_file() -> None: 29 | write_to_file("") 30 | 31 | 32 | events: List[Tuple[str, Optional[int]]] = [] 33 | 34 | 35 | def add_event(event: str, number: Optional[int] = None) -> None: 36 | events.append((event, number)) 37 | 38 | 39 | def get_logging_dicts() -> List[LoggingEvent]: 40 | unixname = getpass.getuser() 41 | dicts = [] 42 | for event, number in events: 43 | dicts.append(LoggingEvent(unixname, number, event)) 44 | return dicts 45 | 46 | 47 | def output() -> None: 48 | dicts = get_logging_dicts() 49 | json_output = json.dumps( 50 | [ 51 | {"unixname": e.unixname, "num": e.num, "eventname": e.eventname} 52 | for e in dicts 53 | ] 54 | ) 55 | write_to_file(json_output) 56 | -------------------------------------------------------------------------------- /src/pathpicker/output.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | import os 6 | import pickle 7 | from typing import List, Tuple 8 | 9 | from pathpicker import logger, state_files 10 | from pathpicker.line_format import LineMatch 11 | 12 | RED_COLOR = "\033[0;31m" 13 | NO_COLOR = "\033[0m" 14 | 15 | INVALID_FILE_WARNING = """ 16 | Warning! Some invalid or unresolvable files were detected. 17 | """ 18 | 19 | GIT_ABBREVIATION_WARNING = """ 20 | It looks like one of these is a git abbreviated file with 21 | a triple dot path (.../). Try to turn off git's abbreviation 22 | with --numstat so we get actual paths (not abbreviated 23 | versions which cannot be resolved. 24 | """ 25 | 26 | CONTINUE_WARNING = "Are you sure you want to continue? Ctrl-C to quit" 27 | 28 | 29 | # The two main entry points into this module: 30 | # 31 | 32 | 33 | def exec_composed_command(command: str, line_objs: List[LineMatch]) -> None: 34 | if not command: 35 | edit_files(line_objs) 36 | return 37 | 38 | logger.add_event("command_on_num_files", len(line_objs)) 39 | command = compose_command(command, line_objs) 40 | append_alias_expansion() 41 | append_if_invalid(line_objs) 42 | append_friendly_command(command) 43 | append_exit() 44 | 45 | 46 | def edit_files(line_objs: List[LineMatch]) -> None: 47 | logger.add_event("editing_num_files", len(line_objs)) 48 | files_and_line_numbers = [ 49 | (line_obj.get_path(), line_obj.get_line_num()) for line_obj in line_objs 50 | ] 51 | command = join_files_into_command(files_and_line_numbers) 52 | append_if_invalid(line_objs) 53 | append_to_file(command) 54 | append_exit() 55 | 56 | 57 | # Private helpers 58 | def append_if_invalid(line_objs: List[LineMatch]) -> None: 59 | # lastly lets check validity and actually output an 60 | # error if any files are invalid 61 | invalid_lines = [line for line in line_objs if not line.is_resolvable()] 62 | if not invalid_lines: 63 | return 64 | append_error(INVALID_FILE_WARNING) 65 | if any(map(LineMatch.is_git_abbreviated_path, invalid_lines)): 66 | append_error(GIT_ABBREVIATION_WARNING) 67 | append_to_file(f'read -p "{CONTINUE_WARNING}" -r') 68 | 69 | 70 | def output_selection(line_objs: List[LineMatch]) -> None: 71 | file_path = state_files.get_selection_file_path() 72 | indices = [line.index for line in line_objs] 73 | file = open(file_path, "wb") 74 | pickle.dump(indices, file) 75 | file.close() 76 | 77 | 78 | def get_editor_and_path() -> Tuple[str, str]: 79 | editor_path = ( 80 | os.environ.get("FPP_EDITOR") 81 | or os.environ.get("VISUAL") 82 | or os.environ.get("EDITOR") 83 | ) 84 | if editor_path: 85 | editor = os.path.basename(editor_path) 86 | logger.add_event(f"using_editor_{editor}") 87 | return editor, editor_path 88 | return "vim", "vim" 89 | 90 | 91 | def join_files_into_command(files_and_line_numbers: List[Tuple[str, int]]) -> str: 92 | editor, editor_path = get_editor_and_path() 93 | cmd = editor_path + " " 94 | if editor == "vim -p": 95 | first_file_path, first_line_num = files_and_line_numbers[0] 96 | cmd += f" +{first_line_num} {first_file_path}" 97 | for (file_path, line_num) in files_and_line_numbers[1:]: 98 | cmd += f' +"tabnew +{line_num} {file_path}"' 99 | elif editor in ["vim", "mvim", "nvim"] and not os.environ.get("FPP_DISABLE_SPLIT"): 100 | first_file_path, first_line_num = files_and_line_numbers[0] 101 | cmd += f" +{first_line_num} {first_file_path}" 102 | for (file_path, line_num) in files_and_line_numbers[1:]: 103 | cmd += f' +"vsp +{line_num} {file_path}"' 104 | else: 105 | for (file_path, line_num) in files_and_line_numbers: 106 | editor_without_args = editor.split()[0] 107 | if ( 108 | editor_without_args 109 | in ["vi", "nvim", "nano", "joe", "emacs", "emacsclient", "micro"] 110 | and line_num != 0 111 | ): 112 | cmd += f" +{line_num} '{file_path}'" 113 | elif ( 114 | editor_without_args in ["subl", "sublime", "atom", "hx"] 115 | and line_num != 0 116 | ): 117 | cmd += f" '{file_path}:{line_num}'" 118 | elif line_num != 0 and os.environ.get("FPP_LINENUM_SEP"): 119 | cmd += f" '{file_path}{os.environ.get('FPP_LINENUM_SEP')}{line_num}'" 120 | else: 121 | cmd += f" '{file_path}'" 122 | return cmd 123 | 124 | 125 | def compose_cd_command(_command: str, line_objs: List[LineMatch]) -> str: 126 | file_path = os.path.expanduser(line_objs[0].get_dir()) 127 | file_path = os.path.abspath(file_path) 128 | # now copy it into clipboard for cdp-ing 129 | # TODO -- this is pretty specific to 130 | # pcottles workflow 131 | return f'echo "{file_path}" > ~/.dircopy' 132 | 133 | 134 | def is_cd_command(command: str) -> bool: 135 | return command[0:3] in ["cd ", "cd"] 136 | 137 | 138 | def compose_command(command: str, line_objs: List[LineMatch]) -> str: 139 | if is_cd_command(command): 140 | return compose_cd_command(command, line_objs) 141 | return compose_file_command(command, line_objs) 142 | 143 | 144 | def compose_file_command(command: str, line_objs: List[LineMatch]) -> str: 145 | command = command.encode().decode("utf-8") 146 | paths = [f"'{line_obj.get_path()}'" for line_obj in line_objs] 147 | path_str = " ".join(paths) 148 | if "$F" in command: 149 | command = command.replace("$F", path_str) 150 | else: 151 | command = f"{command} {path_str}" 152 | return command 153 | 154 | 155 | def output_nothing() -> None: 156 | append_to_file('echo "nothing to do!"; exit 1') 157 | 158 | 159 | def clear_file() -> None: 160 | write_to_file("") 161 | 162 | 163 | def append_alias_expansion() -> None: 164 | # zsh by default expands aliases when running in interactive mode 165 | # (see ../fpp). bash (on this author's Yosemite box) seems to have 166 | # alias expansion off when run with -i present and -c absent, 167 | # despite documentation hinting otherwise. 168 | # 169 | # so here we must ask bash to turn on alias expansion. 170 | shell = os.environ.get("SHELL") 171 | if shell is None or "fish" not in shell: 172 | append_to_file( 173 | """ 174 | if type shopt > /dev/null; then 175 | shopt -s expand_aliases 176 | fi 177 | """ 178 | ) 179 | 180 | 181 | def append_friendly_command(command: str) -> None: 182 | header = 'echo "executing command:"\necho "' + command.replace('"', '\\"') + '"' 183 | append_to_file(header) 184 | append_to_file(command) 185 | 186 | 187 | def append_error(text: str) -> None: 188 | append_to_file(f'printf "{RED_COLOR}{text}{NO_COLOR}\n"') 189 | 190 | 191 | def append_to_file(command: str) -> None: 192 | file = open(state_files.get_script_output_file_path(), "a") 193 | file.write(command + "\n") 194 | file.close() 195 | logger.output() 196 | 197 | 198 | def append_exit() -> None: 199 | # The `$SHELL` environment variable points to the default shell, 200 | # not the current shell. But they are often the same. And there 201 | # is no other simple and reliable way to detect the current shell. 202 | shell = os.environ["SHELL"] 203 | # ``csh``, fish`` and, ``rc`` uses ``$status`` instead of ``$?``. 204 | if shell.endswith("csh") or shell.endswith("fish") or shell.endswith("rc"): 205 | exit_status = "$status" 206 | # Otherwise we assume a Bournal-like shell, e.g. bash and zsh. 207 | else: 208 | exit_status = "$?" 209 | append_to_file(f"exit {exit_status};") 210 | 211 | 212 | def write_to_file(command: str) -> None: 213 | file = open(state_files.get_script_output_file_path(), "w") 214 | file.write(command + "\n") 215 | file.close() 216 | logger.output() 217 | -------------------------------------------------------------------------------- /src/pathpicker/repos.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | # If you are using a code grep query service and want to resolve 7 | # certain global symbols to local directories, 8 | # add them as REPOS below. We will essentially replace a global 9 | # match against something like: 10 | # www/myFile.py 11 | # to: 12 | # ~/www/myFile.py 13 | REPOS = ["www", "configerator", "fbcode", "configerator-dsi"] 14 | -------------------------------------------------------------------------------- /src/pathpicker/screen.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | from abc import ABC, abstractmethod 6 | from typing import TYPE_CHECKING, Tuple 7 | 8 | if TYPE_CHECKING: 9 | import curses 10 | 11 | 12 | class ScreenBase(ABC): 13 | @abstractmethod 14 | def getmaxyx(self) -> Tuple[int, int]: 15 | pass 16 | 17 | @abstractmethod 18 | def refresh(self) -> None: 19 | pass 20 | 21 | @abstractmethod 22 | def erase(self) -> None: 23 | pass 24 | 25 | @abstractmethod 26 | def move(self, y_pos: int, x_pos: int) -> None: 27 | pass 28 | 29 | @abstractmethod 30 | def addstr(self, y_pos: int, x_pos: int, string: str, attr: int) -> None: 31 | pass 32 | 33 | @abstractmethod 34 | def delch(self, y_pos: int, x_pos: int) -> None: 35 | pass 36 | 37 | @abstractmethod 38 | def getch(self) -> int: 39 | pass 40 | 41 | @abstractmethod 42 | def getstr(self, y_pos: int, x_pos: int, max_len: int) -> str: 43 | pass 44 | 45 | 46 | class CursesScreen(ScreenBase): 47 | def __init__(self, screen: "curses._CursesWindow"): 48 | self.screen = screen 49 | 50 | def getmaxyx(self) -> Tuple[int, int]: 51 | return self.screen.getmaxyx() 52 | 53 | def refresh(self) -> None: 54 | self.screen.refresh() 55 | 56 | def erase(self) -> None: 57 | self.screen.erase() 58 | 59 | def move(self, y_pos: int, x_pos: int) -> None: 60 | self.screen.move(y_pos, x_pos) 61 | 62 | def addstr(self, y_pos: int, x_pos: int, string: str, attr: int) -> None: 63 | self.screen.addstr(y_pos, x_pos, string, attr) 64 | 65 | def delch(self, y_pos: int, x_pos: int) -> None: 66 | self.screen.delch(y_pos, x_pos) 67 | 68 | def getch(self) -> int: 69 | return self.screen.getch() 70 | 71 | def getstr(self, y_pos: int, x_pos: int, max_len: int) -> str: 72 | result = self.screen.getstr(y_pos, x_pos, max_len) 73 | if isinstance(result, str): 74 | return result 75 | if isinstance(result, int): 76 | return str(result) 77 | return result.decode("utf-8") 78 | -------------------------------------------------------------------------------- /src/pathpicker/screen_flags.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | import argparse 6 | from typing import List 7 | 8 | 9 | class ScreenFlags: 10 | 11 | """A class that just represents the total set of flags 12 | available to FPP. Note that some of these are actually 13 | processed by the fpp batch file, and not by python. 14 | However, they are documented here because argsparse is 15 | clean and easy to use for that purpose. 16 | 17 | The flags that are actually processed and are meaningful 18 | are 19 | 20 | * c (command) 21 | * r (record) 22 | 23 | """ 24 | 25 | def __init__(self, args: argparse.Namespace): 26 | self.args = args 27 | 28 | def get_preset_command(self) -> str: 29 | return " ".join(self.args.command) 30 | 31 | def get_execute_keys(self) -> List[str]: 32 | return list(self.args.execute_keys) 33 | 34 | def get_is_clean_mode(self) -> bool: 35 | return bool(self.args.clean) 36 | 37 | def get_disable_file_checks(self) -> bool: 38 | return bool(self.args.no_file_checks) or bool(self.args.all_input) 39 | 40 | def get_all_input(self) -> bool: 41 | return bool(self.args.all_input) 42 | 43 | def get_keep_open(self) -> bool: 44 | return bool(self.args.keep_open) 45 | 46 | @staticmethod 47 | def get_arg_parser() -> argparse.ArgumentParser: 48 | parser = argparse.ArgumentParser(prog="fpp") 49 | parser.add_argument( 50 | "-r", 51 | "--record", 52 | help=""" 53 | Record input and output. This is 54 | largely used for testing, but you may find it useful for scripting.""", 55 | default=False, 56 | action="store_true", 57 | ) 58 | parser.add_argument( 59 | "--version", 60 | default=False, 61 | help=""" 62 | Print the version of fpp and exit.""", 63 | action="store_true", 64 | ) 65 | parser.add_argument( 66 | "--clean", 67 | default=False, 68 | action="store_true", 69 | help=""" 70 | Remove the state files that fpp uses when starting up, including 71 | the previous input used and selection pickle. Useful when using fpp 72 | in a script context where the previous state should be discarded.""", 73 | ) 74 | parser.add_argument( 75 | "-ko", 76 | "--keep-open", 77 | default=False, 78 | action="store_true", 79 | help="""keep PathPicker open once 80 | a file selection or command is performed. This will loop the program 81 | until Ctrl-C is used to terminate the process.""", 82 | ) 83 | parser.add_argument( 84 | "-c", 85 | "--command", 86 | help="""You may specify a command while 87 | invoking fpp that will be run once files have been selected. Normally, 88 | fpp opens your editor (see discussion of $EDITOR, $VISUAL, and 89 | $FPP_EDITOR) when you press enter. If you specify a command here, 90 | it will be invoked instead.""", 91 | default="", 92 | action="store", 93 | nargs="+", 94 | ) 95 | 96 | parser.add_argument( 97 | "-e", 98 | "--execute-keys", 99 | help="""Automatically execute the given keys when 100 | the file list shows up. 101 | This is useful on certain cases, e.g. using "END" in order to automatically 102 | go to the last entry when there is a long list.""", 103 | default="", 104 | action="store", 105 | nargs="+", 106 | ) 107 | parser.add_argument( 108 | "-nfc", 109 | "--no-file-checks", 110 | default=False, 111 | action="store_true", 112 | help="""You may want to turn off file 113 | system validation for a particular instance of PathPicker; this flag 114 | disables our internal logic for checking if a regex match is an actual file 115 | on the system. This is particularly useful when using PathPicker for an input 116 | of, say, deleted files in git status that you would like to restore to a given 117 | revision. It enables you to select the deleted files even though they 118 | do not exist on the system anymore.""", 119 | ) 120 | parser.add_argument( 121 | "-ai", 122 | "--all-input", 123 | default=False, 124 | action="store_true", 125 | help="""You may force PathPicker to recognize all 126 | lines as acceptable input. Typically, PathPicker will scan the input for references 127 | to file paths. Passing this option will disable those scans and the program will 128 | assume that every input line is a match. In practice, this option allows for input 129 | selection for a variety of sources that would otherwise be unsupported -- git branches, 130 | mercurial bookmarks, etc.""", 131 | ) 132 | parser.add_argument( 133 | "-ni", 134 | "--non-interactive", 135 | default=False, 136 | action="store_true", 137 | help="""Normally, the command that runs after you've 138 | chosen files to operate on is spawned in an interactive subshell. This allows you 139 | to use aliases and have access to environment variables defined in your startup 140 | files, but can have strange side-effects when starting and stopping jobs 141 | and redirecting inputs. Using this flag runs your commands in a non-interactive 142 | subshell, like a normal shell script.""", 143 | ) 144 | parser.add_argument( 145 | "-a", 146 | "--all", 147 | default=False, 148 | action="store_true", 149 | help="""Automatically select all available lines 150 | once the interactive editor has been entered.""", 151 | ) 152 | return parser 153 | 154 | @staticmethod 155 | def init_from_args(argv: List[str]) -> "ScreenFlags": 156 | (args, _chars) = ScreenFlags.get_arg_parser().parse_known_args(argv) 157 | return ScreenFlags(args) 158 | -------------------------------------------------------------------------------- /src/pathpicker/state_files.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | import os 6 | from typing import List 7 | 8 | FPP_DIR = os.environ.get("FPP_DIR") or "~/.cache/fpp" 9 | PICKLE_FILE = ".pickle" 10 | SELECTION_PICKLE = ".selection.pickle" 11 | OUTPUT_FILE = ".fpp.sh" 12 | LOGGER_FILE = ".fpp.log" 13 | 14 | 15 | def assert_dir_created() -> None: 16 | path = os.path.expanduser(FPP_DIR) 17 | if os.path.isdir(path): 18 | return 19 | try: 20 | os.makedirs(path) 21 | except OSError: 22 | if not os.path.isdir(path): 23 | raise 24 | 25 | 26 | def get_pickle_file_path() -> str: 27 | assert_dir_created() 28 | return os.path.expanduser(os.path.join(FPP_DIR, PICKLE_FILE)) 29 | 30 | 31 | def get_selection_file_path() -> str: 32 | assert_dir_created() 33 | return os.path.expanduser(os.path.join(FPP_DIR, SELECTION_PICKLE)) 34 | 35 | 36 | def get_script_output_file_path() -> str: 37 | assert_dir_created() 38 | return os.path.expanduser(os.path.join(FPP_DIR, OUTPUT_FILE)) 39 | 40 | 41 | def get_logger_file_path() -> str: 42 | assert_dir_created() 43 | return os.path.expanduser(os.path.join(FPP_DIR, LOGGER_FILE)) 44 | 45 | 46 | def get_all_state_files() -> List[str]: 47 | # keep this update to date! We do not include 48 | # the script output path since that gets cleaned automatically 49 | return [ 50 | get_pickle_file_path(), 51 | get_selection_file_path(), 52 | get_logger_file_path(), 53 | get_script_output_file_path(), 54 | ] 55 | -------------------------------------------------------------------------------- /src/pathpicker/usage_strings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | from pathpicker.screen_flags import ScreenFlags 6 | 7 | MANPAGE_HEADER = """= fpp(1) 8 | """ 9 | 10 | MANPAGE_NAME_SECTION = """ 11 | == NAME 12 | 13 | fpp - Facebook PathPicker; a command line tool for selecting files out of bash output 14 | """ 15 | 16 | USAGE_INTRO_PRE = """ 17 | Welcome to fpp, the Facebook PathPicker! We hope your stay 18 | with us is enjoyable. 19 | """ 20 | 21 | MANPAGE_INTRO_PRE = """ 22 | == INTRO 23 | """ 24 | 25 | INTRO = """ 26 | To get started with fpp, pipe some kind of terminal output into the program. 27 | Examples include: 28 | 29 | * git status | fpp 30 | * git show | fpp 31 | * git diff HEAD master | fpp 32 | * git diff HEAD~10 --numstat | fpp 33 | * grep -r "Banana" . | fpp 34 | * find . -iname "*.js" | fpp 35 | 36 | Once fpp parses your input (and something that looks like a file matches), it 37 | will put you inside a pager that will allow you to select files with the 38 | following commands: 39 | """ 40 | 41 | USAGE_INTRO = USAGE_INTRO_PRE + INTRO 42 | 43 | MANPAGE_SYNOPSIS = """ 44 | == SYNOPSIS 45 | 46 | """ 47 | 48 | USAGE_PAGE_HEADER = """ 49 | == Navigation == 50 | 51 | """ 52 | 53 | USAGE_PAGE = """ 54 | * [f] toggle the selection of a file 55 | * [F] toggle and move downward by 1 56 | * [A] toggle selection of all (unique) files 57 | * [down arrow|j] move downward by 1 58 | * [up arrow|k] move upward by 1 59 | * [] page down 60 | * [b] page up 61 | * [x] quick select mode 62 | * [d] describe file 63 | 64 | 65 | Once you have your files selected, you can 66 | either open them in your favorite 67 | text editor or execute commands with 68 | them via command mode: 69 | 70 | * [] open all selected files 71 | (or file under cursor if none selected) 72 | in $EDITOR 73 | * [c] enter command mode 74 | """ 75 | 76 | USAGE_COMMAND_HEADER = """ 77 | == Command Mode == 78 | 79 | """ 80 | 81 | USAGE_COMMAND = """ 82 | Command mode is helpful when you want to 83 | execute bash commands with the filenames 84 | you have selected. By default the filenames 85 | are appended automatically to command you 86 | enter before it is executed, so all you have 87 | to do is type the prefix. Some examples: 88 | 89 | * git add 90 | * git checkout HEAD~1 -- 91 | * rm -rf 92 | 93 | These commands get formatted into: 94 | * git add file1 file2 # etc 95 | * git checkout HEAD~1 -- file1 file2 96 | * rm -rf file1 file2 # etc 97 | 98 | If your command needs filenames in the middle, 99 | the token "$F" will be replaced with your 100 | selected filenames if it is found in the command 101 | string. Examples include: 102 | 103 | * scp $F dev:~/backup 104 | * mv $F ../over/here 105 | 106 | Which format to: 107 | * scp file1 file2 dev:~/backup 108 | * mv file1 file2 ../over/here 109 | """ 110 | 111 | USAGE_CONFIGURATION = """ 112 | == Configuration == 113 | 114 | 115 | PathPicker offers a bit of configuration currently with more to come 116 | in the future. 117 | 118 | ~ Editor ~ 119 | 120 | The $FPP_EDITOR environment variable can be set to tell PathPicker 121 | which editor to open the selected files with. If that variable 122 | is not set, $VISUAL and then $EDITOR are used as fallbacks, 123 | with "vim" as a last resort. 124 | 125 | The $FPP_DISABLE_SPLIT environment variable will disable splitting 126 | files into panes for vim clients (aka sequential editing). 127 | 128 | ~ Directory ~ 129 | 130 | PathPicker saves state files for use when starting up, including the 131 | previous input used and selection pickle. By default, these files are saved 132 | in $XDG_CACHE_HOME/fpp, but the $FPP_DIR environment variable can be used to tell 133 | PathPicker to use another directory. 134 | 135 | ~ Colors ~ 136 | 137 | FPP will understand colors if the piped input uses them. In general, most 138 | tools do not unless requested to do so. 139 | 140 | For git, try `git config --global color.ui always` or use the command 141 | line option --color. 142 | 143 | For built in commands like `ls`, try `-G` (on Mac, additionally export 144 | CLICOLOR_FORCE in your environment to anything.) 145 | 146 | """ 147 | 148 | USAGE_COMMAND_LINE = """ 149 | == Command line arguments == 150 | 151 | 152 | PathPicker supports some command line arguments, as well. 153 | 154 | """ 155 | 156 | USAGE_TAIL = """ 157 | That's a fairly in-depth overview of Facebook PathPicker. 158 | We also provide help along the way as you 159 | use the app, so don't worry and jump on in! 160 | """ 161 | 162 | USAGE_STR = ( 163 | USAGE_INTRO 164 | + USAGE_PAGE_HEADER 165 | + USAGE_PAGE 166 | + USAGE_COMMAND_HEADER 167 | + USAGE_COMMAND 168 | + USAGE_CONFIGURATION 169 | + USAGE_COMMAND_LINE 170 | + ScreenFlags.get_arg_parser().format_help() 171 | + USAGE_TAIL 172 | ) 173 | 174 | DECORATOR = "*" * 80 175 | USAGE_STR = DECORATOR + "\n" + USAGE_STR + "\n" + DECORATOR 176 | 177 | 178 | MANPAGE_STR = "\n\n".join( 179 | [ 180 | MANPAGE_HEADER, 181 | MANPAGE_NAME_SECTION, 182 | MANPAGE_SYNOPSIS, 183 | "--------------------------------------", 184 | ScreenFlags.get_arg_parser().format_help(), 185 | "--------------------------------------", 186 | MANPAGE_INTRO_PRE, 187 | INTRO, 188 | USAGE_PAGE_HEADER, 189 | USAGE_PAGE, 190 | USAGE_COMMAND_HEADER, 191 | USAGE_COMMAND, 192 | USAGE_CONFIGURATION, 193 | ] 194 | ) 195 | 196 | if __name__ == "__main__": 197 | print(MANPAGE_STR) 198 | -------------------------------------------------------------------------------- /src/print_help.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | import process_input 6 | 7 | process_input.usage() 8 | -------------------------------------------------------------------------------- /src/process_input.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | import os 6 | import pickle 7 | import sys 8 | from typing import Dict, List 9 | 10 | from pathpicker import parse, state_files 11 | from pathpicker.formatted_text import FormattedText 12 | from pathpicker.line_format import LineBase, LineMatch, SimpleLine 13 | from pathpicker.screen_flags import ScreenFlags 14 | from pathpicker.usage_strings import USAGE_STR 15 | 16 | 17 | def get_line_objs(flags: ScreenFlags) -> Dict[int, LineBase]: 18 | input_lines = sys.stdin.readlines() 19 | return get_line_objs_from_lines( 20 | input_lines, 21 | validate_file_exists=not flags.get_disable_file_checks(), 22 | all_input=flags.get_all_input(), 23 | ) 24 | 25 | 26 | def get_line_objs_from_lines( 27 | input_lines: List[str], validate_file_exists: bool = True, all_input: bool = False 28 | ) -> Dict[int, LineBase]: 29 | line_objs: Dict[int, LineBase] = {} 30 | for index, line in enumerate(input_lines): 31 | line = line.replace("\t", " " * 4) 32 | # remove the new line as we place the cursor ourselves for each 33 | # line. this avoids curses errors when we newline past the end of the 34 | # screen 35 | line = line.replace("\n", "") 36 | formatted_line = FormattedText(line) 37 | result = parse.match_line( 38 | str(formatted_line), 39 | validate_file_exists=validate_file_exists, 40 | all_input=all_input, 41 | ) 42 | 43 | if not result: 44 | line_obj: LineBase = SimpleLine(formatted_line, index) 45 | else: 46 | line_obj = LineMatch( 47 | formatted_line, 48 | result, 49 | index, 50 | validate_file_exists=validate_file_exists, 51 | all_input=all_input, 52 | ) 53 | 54 | line_objs[index] = line_obj 55 | 56 | return line_objs 57 | 58 | 59 | def do_program(flags: ScreenFlags) -> None: 60 | file_path = state_files.get_pickle_file_path() 61 | line_objs = get_line_objs(flags) 62 | # pickle it so the next program can parse it 63 | pickle.dump(line_objs, open(file_path, "wb")) 64 | 65 | 66 | def usage() -> None: 67 | print(USAGE_STR) 68 | 69 | 70 | def main(argv: List[str]) -> int: 71 | flags = ScreenFlags.init_from_args(argv[1:]) 72 | if flags.get_is_clean_mode(): 73 | print("Cleaning out state files...") 74 | for file_path in state_files.get_all_state_files(): 75 | if os.path.isfile(file_path): 76 | os.remove(file_path) 77 | print(f"Done! Removed {len(state_files.get_all_state_files())} files ") 78 | return 0 79 | if sys.stdin.isatty(): 80 | # don't keep the old selection if the --keep-open option is used; 81 | # otherwise you need to manually clear the old selection every 82 | # time fpp is reopened. 83 | if flags.get_keep_open(): 84 | # delete the old selection 85 | selection_path = state_files.get_selection_file_path() 86 | if os.path.isfile(selection_path): 87 | os.remove(selection_path) 88 | if os.path.isfile(state_files.get_pickle_file_path()): 89 | print("Using previous input piped to fpp...") 90 | else: 91 | usage() 92 | # let the next stage parse the old version 93 | else: 94 | # delete the old selection 95 | selection_path = state_files.get_selection_file_path() 96 | if os.path.isfile(selection_path): 97 | os.remove(selection_path) 98 | do_program(flags) 99 | return 0 100 | 101 | 102 | if __name__ == "__main__": 103 | sys.exit(main(sys.argv)) 104 | -------------------------------------------------------------------------------- /src/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/PathPicker/c1dd1f7fc4dae5aa807218cf086942f1c9241783/src/tests/__init__.py -------------------------------------------------------------------------------- /src/tests/expected/abbreviatedLineSelect.txt: -------------------------------------------------------------------------------- 1 | src/__tests__/expected/allInputBranch.txt | 30 ++++++++ 2 | src/__tests__/expected/selectTwoCommandMode.txt | 4 +- 3 | |===>src/__tests__/baz/bar/banana/|...|/inputs/annoyingTildeExtension.txt | 4 | src/__tests__/inputs/gitBranch.txt | 5 ++ 5 | src/__tests__/inputs/gitLongDiff.txt | 2 +- 6 | src/__tests__/inputs/gitLongDiffColor.txt | 2 +- 7 | src/__tests__/screenTestRunner.py | 9 ++- 8 | src/__tests__/testParsing.py | 79 ++++++++++++++++++++ 9 | src/__tests__/testScreen.py | 11 ++- 10 | src/choose.py | 8 +- 11 | src/format.py | 23 +++--- 12 | src/output.py | 29 +++++--- 13 | src/parse.py | 25 +++++-- 14 | src/processInput.py | 11 ++- 15 | src/screenControl.py | 87 ++++++++++++++-------- 16 | src/screenFlags.py | 12 ++- 17 | src/usageStrings.py | 3 + 18 | src/version.py | 2 +- 19 | 26 files changed, 286 insertions(+), 88 deletions(-) 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ________________________________________________________________________________ 30 | [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick selec -------------------------------------------------------------------------------- /src/tests/expected/allInputBranch.txt: -------------------------------------------------------------------------------- 1 | fix3 2 | |===>gh-pages 3 | * master 4 | testAllInput 5 | trunk 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ________________________________________________________________________________ 30 | [f|A] selection, [down|j|up|k|space|b] navigation, [x] quick select mode, [c] co -------------------------------------------------------------------------------- /src/tests/expected/executeKeysEndKeySelectLast.txt: -------------------------------------------------------------------------------- 1 | README.md | 8 ++++- 2 | fpp | 6 ++-- 3 | src/__tests__/__init__.py | 0 4 | src/__tests__/cursesForTest.py | 45 ++++++++++++++++++++++++++++ 5 | src/__tests__/initTest.py | 28 ++++++++++++++++++ 6 | src/__tests__/screenForTest.py | 67 ++++++++++++++++++++++++++++++++++++++++++ 7 | src/charCodeMapping.py | 20 +++++++++++++ 8 | src/choose.py | 15 ++++++++-- 9 | src/colorPrinter.py | 21 ++++++++----- 10 | src/cursesAPI.py | 40 +++++++++++++++++++++++++ 11 | src/format.py | 4 +-- 12 | src/processInput.py | 7 +++++ 13 | src/screenControl.py | 28 +++++++----------- 14 | |===>src/screenFlags.py | 34 +++++++++++++++++++++ 15 | 14 files changed, 290 insertions(+), 33 deletions(-) 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ________________________________________________________________________________ 30 | [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick selec -------------------------------------------------------------------------------- /src/tests/expected/fileNameWithSpacesDescription.txt: -------------------------------------------------------------------------------- 1 | inputs/annoying-hyphen-dir/Package\ Control.system-bundle| 2 | inputs/annoying-hyphen-dir/Package Control.system-bundle| * [f] toggle the selection of a file 3 | inputs/annoying\ Spaces\ Folder/evilFile\ With\ Space2.txt| * [F] toggle and move downward by 1 4 | inputs/annoying Spaces Folder/evilFile With Space2.txt| * [A] toggle selection of all (unique) files 5 | | * [down arrow|j] move downward by 1 6 | | * [up arrow|k] move upward by 1 7 | | * [] page down 8 | | * [b] page up 9 | | * [x] quick select mode 10 | | * [d] describe file 11 | | 12 | | 13 | |Once you have your files selected, you can 14 | |either open them in your favorite 15 | |text editor or execute commands with 16 | |them via command mode: 17 | | 18 | | * [] open all selected files 19 | | (or file under cursor if none selected) 20 | | in $EDITOR 21 | | * [c] enter command mode 22 | | 23 | |Description for ./inputs/annoying-hyphen-dir/Pac 24 | | 25 | | * last accessed: */*/* *:*:* (glob) 26 | | * last modified: */*/* *:*:* (glob) 27 | | * owned by user: *, * (glob) 28 | | * owned by group: *, * (glob) 29 | | * size: 21B 30 | | * length: 1 line -------------------------------------------------------------------------------- /src/tests/expected/gitAbbreviatedFiles.txt: -------------------------------------------------------------------------------- 1 | |===>.../fpp | 6 ++-- 2 | //////////// 3 | .../src/__tests__/__init__.py | 0 4 | ***************************** 5 | .../src/__tests__/cursesForTest.py | 45 ++++++++++++++++++++++++++++ 6 | __________________________________ 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ________________________________________________________________________________ 58 | 59 | [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick selec 60 | -------------------------------------------------------------------------------- /src/tests/expected/gitDiffWithPageDown.txt: -------------------------------------------------------------------------------- 1 | === + def getMaxDecoratorLength(self): 2 | . + return len(self.ARROW_DECORATOR) 3 | . + 4 | . def printUpTo(self, text, printer, y, x, maxLen): 5 | . '''Attempt to print maxLen characters, returning a tuple 6 | . (x, maxLen) updated with the actual number of characters 7 | . @@ -189,3 +207,6 @@ class LineMatch(object): 8 | . soFar = self.printUpTo(self.beforeText, printer, y, *soFar) 9 | /-\ soFar = self.printUpTo(self.decoratedMatch, printer, y, *soFar) 10 | \-/ soFar = self.printUpTo(self.afterText, printer, y, *soFar) 11 | . + if self.needsUnselectedPrint: 12 | . + self.needsUnselectedPrint = False 13 | . + self.printUpTo(self.endingClearText, printer, y, *soFar) 14 | . diff --git a/src/formattedText.py b/src/formattedText.py 15 | . index f02d1ca..e495352 100644 16 | . --- a/src/formattedText.py 17 | . +++ b/src/formattedText.py 18 | . @@ -23,6 +23,9 @@ class FormattedText(object): 19 | . FOREGROUND_RANGE = Range(30, 39) 20 | . BACKGROUND_RANGE = Range(40, 49) 21 | . 22 | . + DEFAULT_COLOR_FOREGROUND = -1 23 | . + DEFAULT_COLOR_BACKGROUND = -1 24 | . + 25 | . def __init__(self, text=None): 26 | . self.text = text 27 | . 28 | . 29 | . ___________________________________________________________________________ 30 | === [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick -------------------------------------------------------------------------------- /src/tests/expected/gitDiffWithPageDownColor.txt: -------------------------------------------------------------------------------- 1 | === + def getMaxDecoratorLength(self): 2 | RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR 3 | . + return len(self.ARROW_DECORATOR) 4 | RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR 5 | . + 6 | R 7 | . def printUpTo(self, text, printer, y, x, maxLen): 8 | 9 | . '''Attempt to print maxLen characters, returning a tuple 10 | 11 | . (x, maxLen) updated with the actual number of characters 12 | 13 | . @@ -189,3 +207,6 @@ class LineMatch(object): 14 | GGGGGGGGGGGGGGGGGGG 15 | . soFar = self.printUpTo(self.beforeText, printer, y, *soFar) 16 | 17 | /-\ soFar = self.printUpTo(self.decoratedMatch, printer, y, *soFar) 18 | 19 | \-/ soFar = self.printUpTo(self.afterText, printer, y, *soFar) 20 | 21 | . + if self.needsUnselectedPrint: 22 | RRRRRRRRRRRRRRRR______________________ 23 | . + self.needsUnselectedPrint = False 24 | RRRRRRRRRRRRRRRRR_____________________RRRRRRRR 25 | . + self.printUpTo(self.endingClearText, printer, y, *soFar) 26 | RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR 27 | . diff --git a/src/formattedText.py b/src/formattedText.py 28 | WWWWWWWWWWW**********************WWWWWWWWWWWWWWWWWWWWWWW 29 | . index f02d1ca..e495352 100644 30 | WWWWWWWWWWWWWW________WWWWWWW 31 | . --- a/src/formattedText.py 32 | WWWW______________________ 33 | . +++ b/src/formattedText.py 34 | WWWW______________________ 35 | . @@ -23,6 +23,9 @@ class FormattedText(object): 36 | GGGGGGGGGGGGGGGGG 37 | . FOREGROUND_RANGE = Range(30, 39) 38 | 39 | . BACKGROUND_RANGE = Range(40, 49) 40 | 41 | . 42 | 43 | . + DEFAULT_COLOR_FOREGROUND = -1 44 | RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR 45 | . + DEFAULT_COLOR_BACKGROUND = -1 46 | RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR 47 | . + 48 | R 49 | . def __init__(self, text=None): 50 | 51 | . self.text = text 52 | _________ 53 | . 54 | 55 | . 56 | 57 | . ___________________________________________________________________________ 58 | 59 | === [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick 60 | -------------------------------------------------------------------------------- /src/tests/expected/gitDiffWithScroll.txt: -------------------------------------------------------------------------------- 1 | === diff --git |===>a/src/__tests__/screenTestRunner.py b/src/__tests__/screenT 2 | /-\ index f1f6f7a..5c71551 100644 3 | |-| --- a/src/__tests__/screenTestRunner.py 4 | |-| +++ b/src/__tests__/screenTestRunner.py 5 | |-| @@ -36,24 +36,31 @@ def getRowsFromScreenRun( 6 | |-| charInputs, 7 | |-| screenConfig={}, 8 | |-| printScreen=True, 9 | |-| - pastScreen=None): 10 | |-| + pastScreen=None, 11 | |-| + args=[]): 12 | |-| + 13 | |-| lineObjs = getLineObjsFromFile(inputFile) 14 | |-| screen = ScreenForTest( 15 | |-| charInputs, 16 | |-| maxX=screenConfig.get('maxX', 80), 17 | |-| maxY=screenConfig.get('maxY', 30), 18 | |-| ) 19 | |-| - # mock our flags with an empty command line for now 20 | |-| - flags = ScreenFlags.initFromArgs([]) 21 | \-/ + 22 | . + # mock our flags with the passed arg list 23 | . + flags = ScreenFlags.initFromArgs(args) 24 | . + # we run our program and throw a StopIteration exception 25 | . + # instead of sys.exit-ing 26 | . try: 27 | . 28 | . 29 | . ___________________________________________________________________________ 30 | === [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick -------------------------------------------------------------------------------- /src/tests/expected/gitDiffWithScrollUp.txt: -------------------------------------------------------------------------------- 1 | === We also provide help along the way as you 2 | . @@ -131,6 +133,8 @@ USAGE_STR = USAGE_INTRO + \ 3 | . USAGE_COMMAND_HEADER + \ 4 | . USAGE_COMMAND + \ 5 | . USAGE_CONFIGURATION + \ 6 | . + USAGE_COMMAND_LINE + \ 7 | . + ScreenFlags.getArgParser().format_help() + \ 8 | . USAGE_TAIL 9 | . 10 | . decorator = '*' * 80 11 | . diff --git a/src/version.py b/src/version.py 12 | . new file mode 100755 13 | . index 0000000..aee174d 14 | . --- /dev/null 15 | . +++ b/src/version.py 16 | . @@ -0,0 +1,15 @@ 17 | . +# Copyright (c) 2015-present, Facebook, Inc. 18 | . +# All rights reserved. 19 | . +# 20 | . +# This source code is licensed under the BSD-style license found in the 21 | . +# LICENSE file in the root directory of this source tree. An additional gr 22 | . +# of patent rights can be found in the PATENTS file in the same directory. 23 | . +# 24 | . +from __future__ import print_function 25 | . + 26 | . + 27 | /-\ 28 | \-/ 29 | . ___________________________________________________________________________ 30 | === [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick -------------------------------------------------------------------------------- /src/tests/expected/gitDiffWithValidation.txt: -------------------------------------------------------------------------------- 1 | src/pathpicker/char_code_mapping.py | 20 +++++++++++++ 2 | *********************************** 3 | src/choose.py | 15 ++++++++-- 4 | _____________ 5 | src/pathpicker/color_printer.py | 21 ++++++++----- 6 | _______________________________ 7 | src/pathpicker/curses_api.py | 40 +++++++++++++++++++++++++ 8 | ____________________________ 9 | src/pathpicker/line_format.py | 4 +-- 10 | _____________________________ 11 | src/process_input.py | 7 +++++ 12 | ____________________ 13 | /foo/bar/src/pathpicker/char_code_mapping.py | 20 +++++++++++++ 14 | 15 | /foo/bar/src/choose.py | 15 ++++++++-- 16 | 17 | /foo/bar/src/pathpicker/color_printer.py | 21 ++++++++----- 18 | 19 | /foo/bar/src/pathpicker/curses_api.py | 40 +++++++++++++++++++++++ 20 | 21 | /foo/bar/src/pathpicker/format.py | 4 +-- 22 | 23 | /foo/bar/src/process_input.py | 7 +++++ 24 | 25 | /foo/bar/src/pathpicker/screen_control.py | 28 +++++++----------- 26 | 27 | /foo/bar/src/pathpicker/screen_flags.py | 34 +++++++++++++++++++++ 28 | 29 | 14 files changed, 290 insertions(+), 33 deletions(-) 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ________________________________________________________________________________ 58 | 59 | [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick selec 60 | -------------------------------------------------------------------------------- /src/tests/expected/longFileNames.txt: -------------------------------------------------------------------------------- 1 | longfilenamereallylo 2 | longfilenamereallylo 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ____________________ 30 | [f|A] selection, [do -------------------------------------------------------------------------------- /src/tests/expected/longFileNamesWithBeforeTextBug.txt: -------------------------------------------------------------------------------- 1 | -rw-r--r--@ 1 pcottle THEFACEBOOK\Domain Users 401 Jun 4 2015 |===>lon|...|ding.txt 2 | -rw-r--r--@ 1 pcottle THEFACEBOOK\Domain Users 401 Jun 4 2015 foo.txt 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | _______________________________________________________________________________________________ 40 | [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick select mode, [c] com -------------------------------------------------------------------------------- /src/tests/expected/longFileTruncation.txt: -------------------------------------------------------------------------------- 1 | ./hadoop-mapreduce-project/|...|eNamereallylongfileName.txt 2 | ___________________________________________________________ 3 | |===>./hadoop-mapreduce-p|...|some/deeper/dir/source.py 4 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||| 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ____________________________________________________________ 38 | 39 | [f|A] selection, [down|j|up|k|space|b] navigation, [enter] o 40 | -------------------------------------------------------------------------------- /src/tests/expected/longListEndKey.txt: -------------------------------------------------------------------------------- 1 | === dummy97.txt 2 | ___________ 3 | . dummy98.txt 4 | ___________ 5 | . dummy99.txt 6 | ___________ 7 | . dummy100.txt 8 | ************ 9 | . 10 | 11 | . 12 | 13 | . 14 | 15 | /-\ 16 | 17 | \-/ ___________________________________________________________________________ 18 | 19 | === [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick 20 | -------------------------------------------------------------------------------- /src/tests/expected/longListHomeKey.txt: -------------------------------------------------------------------------------- 1 | === dummy1.txt 2 | ********** 3 | \-/ dummy2.txt 4 | __________ 5 | . dummy3.txt 6 | __________ 7 | . dummy4.txt 8 | __________ 9 | . dummy5.txt 10 | __________ 11 | . dummy6.txt 12 | __________ 13 | . 14 | 15 | . 16 | 17 | . ___________________________________________________________________________ 18 | 19 | === [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick 20 | -------------------------------------------------------------------------------- /src/tests/expected/longListPageUpAndDown.txt: -------------------------------------------------------------------------------- 1 | === dummy14.txt 2 | . dummy15.txt 3 | . dummy16.txt 4 | . dummy17.txt 5 | /-\ dummy18.txt 6 | |-| dummy19.txt 7 | |-| dummy20.txt 8 | |-| dummy21.txt 9 | |-| dummy22.txt 10 | |-| dummy23.txt 11 | |-| dummy24.txt 12 | |-| dummy25.txt 13 | \-/ dummy26.txt 14 | . dummy27.txt 15 | . dummy28.txt 16 | . dummy29.txt 17 | . dummy30.txt 18 | . dummy31.txt 19 | . dummy32.txt 20 | . dummy33.txt 21 | . dummy34.txt 22 | . dummy35.txt 23 | . dummy36.txt 24 | . dummy37.txt 25 | . dummy38.txt 26 | . dummy39.txt 27 | . 28 | . 29 | . ___________________________________________________________________________ 30 | === [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick -------------------------------------------------------------------------------- /src/tests/expected/selectAllBug.txt: -------------------------------------------------------------------------------- 1 | === diff --git |===>a/src/choose.py b/src/choose.py 2 | /-\ index d184f91.|===>.5d7f2f9 100755 3 | \-/ --- a/src/choose.py 4 | . +++ b/src/choose.py 5 | . @@ -11,6 +11,7 @@ import curses 6 | . import pickle 7 | . import sys 8 | . import os 9 | . +import argparse 10 | . 11 | . import output 12 | . import screenControl 13 | . @@ -29,18 +30,16 @@ this error will go away) 14 | . ''' 15 | . 16 | . 17 | . -def doProgram(stdscr, cursesAPI=None, lineObjs=None, flags=None): 18 | . +def doProgram(stdscr, flags, cursesAPI=None, lineObjs=None): 19 | . # curses and lineObjs get dependency injected for 20 | . # our tests, so init these if they are not provided 21 | . if not cursesAPI: 22 | . cursesAPI = CursesAPI() 23 | . if not lineObjs: 24 | . lineObjs = getLineObjs() 25 | . - if not flags: 26 | . - flags = ScreenFlags.initFromArgs() 27 | . 28 | . 29 | . ___________________________________________________________________________ 30 | === [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick -------------------------------------------------------------------------------- /src/tests/expected/selectAllFromArg.txt: -------------------------------------------------------------------------------- 1 | |===>/foo/bar/README.md | 8 ++++- 2 | |===>/foo/bar/fpp | 6 ++-- 3 | |===>/foo/bar/src/__tests__/__init__.py | 0 4 | |===>/foo/bar/src/__tests__/cursesForTest.py | 45 ++++++++++++++++++++++++++++ 5 | |===>/foo/bar/src/__tests__/initTest.py | 28 ++++++++++++++++++ 6 | |===>/foo/bar/src/__tests__/screenForTest.py | 67 +++++++++++++++++++++++++++++ 7 | |===>/foo/bar/src/charCodeMapping.py | 20 +++++++++++++ 8 | |===>/foo/bar/src/choose.py | 15 ++++++++-- 9 | |===>/foo/bar/src/colorPrinter.py | 21 ++++++++----- 10 | |===>/foo/bar/src/cursesAPI.py | 40 +++++++++++++++++++++++++ 11 | |===>/foo/bar/src/format.py | 4 +-- 12 | |===>/foo/bar/src/processInput.py | 7 +++++ 13 | |===>/foo/bar/src/screenControl.py | 28 +++++++----------- 14 | |===>/foo/bar/src/screenFlags.py | 34 +++++++++++++++++++++ 15 | 14 files changed, 290 insertions(+), 33 deletions(-) 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ________________________________________________________________________________ 30 | [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick selec -------------------------------------------------------------------------------- /src/tests/expected/selectCommandWithPassedCommand.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Oh no! You already provided a command so you cannot enter command mode. 22 | GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG 23 | The command you provided was " 'git add'" 24 | 25 | Press any key to go back to selecting paths. 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ________________________________________________________________________________ 58 | 59 | [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick selec 60 | -------------------------------------------------------------------------------- /src/tests/expected/selectDownSelect.txt: -------------------------------------------------------------------------------- 1 | |===>README.md | 8 ++++- 2 | fpp | 6 ++-- 3 | |===>src/__tests__/__init__.py | 0 4 | src/__tests__/cursesForTest.py | 45 ++++++++++++++++++++++++++++ 5 | src/__tests__/initTest.py | 28 ++++++++++++++++++ 6 | src/__tests__/screenForTest.py | 67 ++++++++++++++++++++++++++++++++++++++++++ 7 | src/charCodeMapping.py | 20 +++++++++++++ 8 | src/choose.py | 15 ++++++++-- 9 | src/colorPrinter.py | 21 ++++++++----- 10 | src/cursesAPI.py | 40 +++++++++++++++++++++++++ 11 | src/format.py | 4 +-- 12 | src/processInput.py | 7 +++++ 13 | src/screenControl.py | 28 +++++++----------- 14 | src/screenFlags.py | 34 +++++++++++++++++++++ 15 | 14 files changed, 290 insertions(+), 33 deletions(-) 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ________________________________________________________________________________ 30 | [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick selec -------------------------------------------------------------------------------- /src/tests/expected/selectDownSelectInverse.txt: -------------------------------------------------------------------------------- 1 | README.md | 8 ++++- 2 | fpp | 6 ++-- 3 | src/__tests__/__init__.py | 0 4 | |===>src/__tests__/cursesForTest.py | 45 ++++++++++++++++++++++++++++ 5 | |===>src/__tests__/initTest.py | 28 ++++++++++++++++++ 6 | |===>src/__tests__/screenForTest.py | 67 ++++++++++++++++++++++++++++++++++++++ 7 | |===>src/charCodeMapping.py | 20 +++++++++++++ 8 | |===>src/choose.py | 15 ++++++++-- 9 | |===>src/colorPrinter.py | 21 ++++++++----- 10 | |===>src/cursesAPI.py | 40 +++++++++++++++++++++++++ 11 | |===>src/format.py | 4 +-- 12 | |===>src/processInput.py | 7 +++++ 13 | |===>src/screenControl.py | 28 +++++++----------- 14 | |===>src/screenFlags.py | 34 +++++++++++++++++++++ 15 | 14 files changed, 290 insertions(+), 33 deletions(-) 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ________________________________________________________________________________ 30 | [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick selec -------------------------------------------------------------------------------- /src/tests/expected/selectFirst.txt: -------------------------------------------------------------------------------- 1 | |===>README.md | 8 ++++- 2 | fpp | 6 ++-- 3 | src/__tests__/__init__.py | 0 4 | src/__tests__/cursesForTest.py | 45 ++++++++++++++++++++++++++++ 5 | src/__tests__/initTest.py | 28 ++++++++++++++++++ 6 | src/__tests__/screenForTest.py | 67 ++++++++++++++++++++++++++++++++++++++++++ 7 | src/charCodeMapping.py | 20 +++++++++++++ 8 | src/choose.py | 15 ++++++++-- 9 | src/colorPrinter.py | 21 ++++++++----- 10 | src/cursesAPI.py | 40 +++++++++++++++++++++++++ 11 | src/format.py | 4 +-- 12 | src/processInput.py | 7 +++++ 13 | src/screenControl.py | 28 +++++++----------- 14 | src/screenFlags.py | 34 +++++++++++++++++++++ 15 | 14 files changed, 290 insertions(+), 33 deletions(-) 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ________________________________________________________________________________ 30 | [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick selec -------------------------------------------------------------------------------- /src/tests/expected/selectFirstWithDown.txt: -------------------------------------------------------------------------------- 1 | |===>README.md | 8 ++++- 2 | fpp | 6 ++-- 3 | src/__tests__/__init__.py | 0 4 | src/__tests__/cursesForTest.py | 45 ++++++++++++++++++++++++++++ 5 | src/__tests__/initTest.py | 28 ++++++++++++++++++ 6 | src/__tests__/screenForTest.py | 67 ++++++++++++++++++++++++++++++++++++++++++ 7 | src/charCodeMapping.py | 20 +++++++++++++ 8 | src/choose.py | 15 ++++++++-- 9 | src/colorPrinter.py | 21 ++++++++----- 10 | src/cursesAPI.py | 40 +++++++++++++++++++++++++ 11 | src/format.py | 4 +-- 12 | src/processInput.py | 7 +++++ 13 | src/screenControl.py | 28 +++++++----------- 14 | src/screenFlags.py | 34 +++++++++++++++++++++ 15 | 14 files changed, 290 insertions(+), 33 deletions(-) 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ________________________________________________________________________________ 30 | [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick selec -------------------------------------------------------------------------------- /src/tests/expected/selectTwoCommandMode.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ========================================================== 10 | Paths you have selected: 11 | ========================================================== 12 | /foo/bar/README.md 13 | /foo/bar/fpp 14 | ========================================================== 15 | Type a command below! Paths will be appended or replace $F 16 | Enter a blank line to go back to the selection process 17 | ========================================================== 18 | .......................................................... 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ________________________________________________________________________________ 30 | command examples: | git add | git checkout HEAD~1 -- | mv $F ../here/ | -------------------------------------------------------------------------------- /src/tests/expected/selectWithDownSelect.txt: -------------------------------------------------------------------------------- 1 | |===>README.md | 8 ++++- 2 | fpp | 6 ++-- 3 | |===>src/__tests__/__init__.py | 0 4 | src/__tests__/cursesForTest.py | 45 ++++++++++++++++++++++++++++ 5 | src/__tests__/initTest.py | 28 ++++++++++++++++++ 6 | src/__tests__/screenForTest.py | 67 ++++++++++++++++++++++++++++++++++++++++++ 7 | src/charCodeMapping.py | 20 +++++++++++++ 8 | src/choose.py | 15 ++++++++-- 9 | src/colorPrinter.py | 21 ++++++++----- 10 | src/cursesAPI.py | 40 +++++++++++++++++++++++++ 11 | src/format.py | 4 +-- 12 | src/processInput.py | 7 +++++ 13 | src/screenControl.py | 28 +++++++----------- 14 | src/screenFlags.py | 34 +++++++++++++++++++++ 15 | 14 files changed, 290 insertions(+), 33 deletions(-) 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ________________________________________________________________________________ 30 | [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick selec -------------------------------------------------------------------------------- /src/tests/expected/selectWithDownSelectInverse.txt: -------------------------------------------------------------------------------- 1 | README.md | 8 ++++- 2 | fpp | 6 ++-- 3 | src/__tests__/__init__.py | 0 4 | |===>src/__tests__/cursesForTest.py | 45 ++++++++++++++++++++++++++++ 5 | |===>src/__tests__/initTest.py | 28 ++++++++++++++++++ 6 | |===>src/__tests__/screenForTest.py | 67 ++++++++++++++++++++++++++++++++++++++ 7 | |===>src/charCodeMapping.py | 20 +++++++++++++ 8 | |===>src/choose.py | 15 ++++++++-- 9 | |===>src/colorPrinter.py | 21 ++++++++----- 10 | |===>src/cursesAPI.py | 40 +++++++++++++++++++++++++ 11 | |===>src/format.py | 4 +-- 12 | |===>src/processInput.py | 7 +++++ 13 | |===>src/screenControl.py | 28 +++++++----------- 14 | |===>src/screenFlags.py | 34 +++++++++++++++++++++ 15 | 14 files changed, 290 insertions(+), 33 deletions(-) 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ________________________________________________________________________________ 30 | [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick selec -------------------------------------------------------------------------------- /src/tests/expected/simpleGitDiff.txt: -------------------------------------------------------------------------------- 1 | README.md | 8 ++++- 2 | fpp | 6 ++-- 3 | src/__tests__/__init__.py | 0 4 | src/__tests__/cursesForTest.py | 45 ++++++++++++++++++++++++++++ 5 | src/__tests__/initTest.py | 28 ++++++++++++++++++ 6 | src/__tests__/screenForTest.py | 67 ++++++++++++++++++++++++++++++++++++++++++ 7 | src/charCodeMapping.py | 20 +++++++++++++ 8 | src/choose.py | 15 ++++++++-- 9 | src/colorPrinter.py | 21 ++++++++----- 10 | src/cursesAPI.py | 40 +++++++++++++++++++++++++ 11 | src/format.py | 4 +-- 12 | src/processInput.py | 7 +++++ 13 | src/screenControl.py | 28 +++++++----------- 14 | src/screenFlags.py | 34 +++++++++++++++++++++ 15 | 14 files changed, 290 insertions(+), 33 deletions(-) 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ________________________________________________________________________________ 30 | [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [c] command mod -------------------------------------------------------------------------------- /src/tests/expected/simpleLoadAndQuit.txt: -------------------------------------------------------------------------------- 1 | README.md | 8 ++++- 2 | fpp | 6 ++-- 3 | src/__tests__/__init__.py | 0 4 | src/__tests__/cursesForTest.py | 45 ++++++++++++++++++++++++++++ 5 | src/__tests__/initTest.py | 28 ++++++++++++++++++ 6 | src/__tests__/screenForTest.py | 67 ++++++++++++++++++++++++++++++++++++++++++ 7 | src/charCodeMapping.py | 20 +++++++++++++ 8 | src/choose.py | 15 ++++++++-- 9 | src/colorPrinter.py | 21 ++++++++----- 10 | src/cursesAPI.py | 40 +++++++++++++++++++++++++ 11 | src/format.py | 4 +-- 12 | src/processInput.py | 7 +++++ 13 | src/screenControl.py | 28 +++++++----------- 14 | src/screenFlags.py | 34 +++++++++++++++++++++ 15 | 14 files changed, 290 insertions(+), 33 deletions(-) 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ________________________________________________________________________________ 30 | [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick selec -------------------------------------------------------------------------------- /src/tests/expected/simpleSelectWithAttributes.txt: -------------------------------------------------------------------------------- 1 | |===>README.md | 8 ++++- 2 | ////////////// 3 | fpp | 6 ++-- 4 | 5 | src/__tests__/__init__.py | 0 6 | ************************* 7 | src/__tests__/cursesForTest.py | 45 ++++++++++++++++++++++++++++ 8 | ______________________________ 9 | src/__tests__/initTest.py | 28 ++++++++++++++++++ 10 | _________________________ 11 | src/__tests__/screenForTest.py | 67 ++++++++++++++++++++++++++++++++++++++++++ 12 | ______________________________ 13 | src/charCodeMapping.py | 20 +++++++++++++ 14 | ______________________ 15 | src/choose.py | 15 ++++++++-- 16 | _____________ 17 | src/colorPrinter.py | 21 ++++++++----- 18 | ___________________ 19 | src/cursesAPI.py | 40 +++++++++++++++++++++++++ 20 | ________________ 21 | src/format.py | 4 +-- 22 | _____________ 23 | src/processInput.py | 7 +++++ 24 | ___________________ 25 | src/screenControl.py | 28 +++++++----------- 26 | ____________________ 27 | src/screenFlags.py | 34 +++++++++++++++++++++ 28 | __________________ 29 | 14 files changed, 290 insertions(+), 33 deletions(-) 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ________________________________________________________________________________ 58 | 59 | [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick selec 60 | -------------------------------------------------------------------------------- /src/tests/expected/simpleSelectWithColor.txt: -------------------------------------------------------------------------------- 1 | |===>assets/launch_page.css | 4 + 2 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ G 3 | fpp | 4 + 4 | G 5 | fpp.rb | 4 +- 6 | ****** GR 7 | index.html | 11 ++- 8 | __________ GGR 9 | scripts/makeDist.sh | 2 +- 10 | ___________________ GR 11 | .../expected/selectCommandWithPassedCommand.txt | 30 +++++++ 12 | _______________________________________________ GGGGGGG 13 | src/__tests__/expected/selectDownSelectInverse.txt | 4 +- 14 | __________________________________________________ GR 15 | .../expected/simpleSelectWithAttributes.txt | 60 ++++++++++++++ 16 | ___________________________________________ GGGGGGGGGGGGGG 17 | src/__tests__/expected/simpleWithAttributes.txt | 60 ++++++++++++++ 18 | _______________________________________________ GGGGGGGGGGGGGG 19 | src/__tests__/screenForTest.py | 40 +++++++-- 20 | ______________________________ GGGGGGGRR 21 | src/__tests__/screenTestRunner.py | 17 ++-- 22 | _________________________________ GGRR 23 | src/__tests__/testScreen.py | 96 ++++++++++++++++++---- 24 | ___________________________ GGGGGGGGGGGGGGGGGGRRRR 25 | src/format.py | 29 ++++++- 26 | _____________ GGGGGGR 27 | src/formattedText.py | 3 + 28 | ____________________ G 29 | src/processInput.py | 10 +++ 30 | ___________________ GGG 31 | src/screenControl.py | 23 ++++++ 32 | ____________________ GGGGGG 33 | src/screenFlags.py | 18 +++- 34 | __________________ GGGR 35 | src/stateFiles.py | 11 +++ 36 | _________________ GGG 37 | src/version.py | 15 ++++ 38 | ______________ GGGG 39 | 19 files changed, 404 insertions(+), 37 deletions(-) 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | ________________________________________________________________________________________________________________________________________________________________________________________________________ 78 | 79 | [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick select mode, [c] command mode 80 | -------------------------------------------------------------------------------- /src/tests/expected/simpleWithAttributes.txt: -------------------------------------------------------------------------------- 1 | README.md | 8 ++++- 2 | ********* 3 | fpp | 6 ++-- 4 | 5 | src/__tests__/__init__.py | 0 6 | _________________________ 7 | src/__tests__/cursesForTest.py | 45 ++++++++++++++++++++++++++++ 8 | ______________________________ 9 | src/__tests__/initTest.py | 28 ++++++++++++++++++ 10 | _________________________ 11 | src/__tests__/screenForTest.py | 67 ++++++++++++++++++++++++++++++++++++++++++ 12 | ______________________________ 13 | src/charCodeMapping.py | 20 +++++++++++++ 14 | ______________________ 15 | src/choose.py | 15 ++++++++-- 16 | _____________ 17 | src/colorPrinter.py | 21 ++++++++----- 18 | ___________________ 19 | src/cursesAPI.py | 40 +++++++++++++++++++++++++ 20 | ________________ 21 | src/format.py | 4 +-- 22 | _____________ 23 | src/processInput.py | 7 +++++ 24 | ___________________ 25 | src/screenControl.py | 28 +++++++----------- 26 | ____________________ 27 | src/screenFlags.py | 34 +++++++++++++++++++++ 28 | __________________ 29 | 14 files changed, 290 insertions(+), 33 deletions(-) 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ________________________________________________________________________________ 58 | 59 | [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick selec 60 | -------------------------------------------------------------------------------- /src/tests/expected/tallLoadAndQuit.txt: -------------------------------------------------------------------------------- 1 | README.md | 8 ++++- 2 | fpp | 6 ++-- 3 | src/__tests__/__init__.py | 0 4 | src/__tests__/cursesForTest.py | 45 ++++++++++++++++++++++++++++ 5 | src/__tests__/initTest.py | 28 ++++++++++++++++++ 6 | src/__tests__/screenForTest.py | 67 ++++++++++++++++++++++++++++++++++++++++++ 7 | src/charCodeMapping.py | 20 +++++++++++++ 8 | src/choose.py | 15 ++++++++-- 9 | src/colorPrinter.py | 21 ++++++++----- 10 | src/cursesAPI.py | 40 +++++++++++++++++++++++++ 11 | src/format.py | 4 +-- 12 | src/processInput.py | 7 +++++ 13 | src/screenControl.py | 28 +++++++----------- 14 | src/screenFlags.py | 34 +++++++++++++++++++++ 15 | 14 files changed, 290 insertions(+), 33 deletions(-) 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | ____________________________________________________________________________________________________________________________________________ 60 | [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick select mode, [c] command mode -------------------------------------------------------------------------------- /src/tests/expected/tonsOfFiles.txt: -------------------------------------------------------------------------------- 1 | ./drivers/acpi/acpica/utxferror.c 2 | ./drivers/acpi/acpica/utascii.c 3 | ./drivers/acpi/acpica/tbinstal.c 4 | ./drivers/acpi/acpica/utprint.c 5 | ./drivers/acpi/acpica/evevent.c 6 | ./drivers/acpi/acpica/dsdebug.c 7 | ./drivers/acpi/acpica/psloop.c 8 | ./drivers/acpi/acpica/utstrtoul64.c 9 | ./drivers/acpi/acpica/utaddress.c 10 | ./drivers/acpi/acpica/evgpeutil.c 11 | ./drivers/acpi/acpica/dsobject.c 12 | ./drivers/acpi/acpica/exregion.c 13 | ./drivers/acpi/acpica/nsload.c 14 | ./drivers/acpi/acpica/utnonansi.c 15 | ./drivers/acpi/acpica/utxfinit.c 16 | ./drivers/acpi/acpica/evxface.c 17 | ./drivers/acpi/acpica/exconvrt.c 18 | ./drivers/acpi/acpica/hwtimer.c 19 | ./drivers/acpi/acpica/utdebug.c 20 | ./drivers/acpi/acpica/exoparg1.c 21 | ./drivers/acpi/acpica/utexcep.c 22 | ./drivers/acpi/acpica/nsxfobj.c 23 | ./drivers/acpi/acpica/hwgpe.c 24 | ========================================================== 25 | Type a command below! Paths will be appended or replace $F 26 | Enter a blank line to go back to the selection process 27 | ========================================================== 28 | .......................................................... 29 | ________________________________________________________________________________ 30 | command examples: | git add | git checkout HEAD~1 -- | mv $F ../here/ | -------------------------------------------------------------------------------- /src/tests/expected/xModeWithSelect.txt: -------------------------------------------------------------------------------- 1 | B README.md | 8 ++++- 2 | ********* 3 | C fpp | 6 ++-- 4 | 5 | D src/__tests__/__init__.py | 0 6 | _________________________ 7 | E src/__tests__/cursesForTest.py | 45 ++++++++++++++++++++++++++++ 8 | ______________________________ 9 | G |===>src/__tests__/initTest.py | 28 ++++++++++++++++++ 10 | |||||||||||||||||||||||||||||| 11 | H src/__tests__/screenForTest.py | 67 ++++++++++++++++++++++++++++++++++++++ 12 | ______________________________ 13 | I src/charCodeMapping.py | 20 +++++++++++++ 14 | ______________________ 15 | J |===>src/choose.py | 15 ++++++++-- 16 | |||||||||||||||||| 17 | K src/colorPrinter.py | 21 ++++++++----- 18 | ___________________ 19 | L src/cursesAPI.py | 40 +++++++++++++++++++++++++ 20 | ________________ 21 | M src/format.py | 4 +-- 22 | _____________ 23 | N src/processInput.py | 7 +++++ 24 | ___________________ 25 | O src/screenControl.py | 28 +++++++----------- 26 | ____________________ 27 | P src/screenFlags.py | 34 +++++++++++++++++++++ 28 | __________________ 29 | Q 14 files changed, 290 insertions(+), 33 deletions(-) 30 | 31 | R 32 | 33 | S 34 | 35 | T 36 | 37 | U 38 | 39 | V 40 | 41 | W 42 | 43 | X 44 | 45 | Y 46 | 47 | Z 48 | 49 | 1 50 | 51 | 2 52 | 53 | 3 54 | 55 | 4 56 | 57 | 5___________________________________________________________________________ 58 | 59 | [f|A] selection, [down|j|up|k|space|b] navigation, [enter] open, [x] quick 60 | -------------------------------------------------------------------------------- /src/tests/inputs/.DS_KINDA_STORE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/PathPicker/c1dd1f7fc4dae5aa807218cf086942f1c9241783/src/tests/inputs/.DS_KINDA_STORE -------------------------------------------------------------------------------- /src/tests/inputs/NSArray+Utils.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/PathPicker/c1dd1f7fc4dae5aa807218cf086942f1c9241783/src/tests/inputs/NSArray+Utils.h -------------------------------------------------------------------------------- /src/tests/inputs/absoluteGitDiff.txt: -------------------------------------------------------------------------------- 1 | /foo/bar/README.md | 8 ++++- 2 | /foo/bar/fpp | 6 ++-- 3 | /foo/bar/src/__tests__/__init__.py | 0 4 | /foo/bar/src/__tests__/cursesForTest.py | 45 ++++++++++++++++++++++++++++ 5 | /foo/bar/src/__tests__/initTest.py | 28 ++++++++++++++++++ 6 | /foo/bar/src/__tests__/screenForTest.py | 67 ++++++++++++++++++++++++++++++++++++++++++ 7 | /foo/bar/src/charCodeMapping.py | 20 +++++++++++++ 8 | /foo/bar/src/choose.py | 15 ++++++++-- 9 | /foo/bar/src/colorPrinter.py | 21 ++++++++----- 10 | /foo/bar/src/cursesAPI.py | 40 +++++++++++++++++++++++++ 11 | /foo/bar/src/format.py | 4 +-- 12 | /foo/bar/src/processInput.py | 7 +++++ 13 | /foo/bar/src/screenControl.py | 28 +++++++----------- 14 | /foo/bar/src/screenFlags.py | 34 +++++++++++++++++++++ 15 | 14 files changed, 290 insertions(+), 33 deletions(-) 16 | -------------------------------------------------------------------------------- /src/tests/inputs/annoying Spaces Folder/evilFile With Space2.txt: -------------------------------------------------------------------------------- 1 | im a file with spaces, rawrrr. 2 | -------------------------------------------------------------------------------- /src/tests/inputs/annoying-hyphen-dir/Package Control.system-bundle: -------------------------------------------------------------------------------- 1 | CCCCCC-COMBO BREAKER 2 | -------------------------------------------------------------------------------- /src/tests/inputs/annoyingTildeExtension.txt~: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/PathPicker/c1dd1f7fc4dae5aa807218cf086942f1c9241783/src/tests/inputs/annoyingTildeExtension.txt~ -------------------------------------------------------------------------------- /src/tests/inputs/blogredesign.sublime-workspace: -------------------------------------------------------------------------------- 1 | hrmmm this is something 2 | -------------------------------------------------------------------------------- /src/tests/inputs/evilFile No Prepend.txt: -------------------------------------------------------------------------------- 1 | ahhhhh 2 | -------------------------------------------------------------------------------- /src/tests/inputs/evilFile With Space.txt: -------------------------------------------------------------------------------- 1 | im a file with spaces, rawrrr. 2 | -------------------------------------------------------------------------------- /src/tests/inputs/file-from-yocto_%.bbappend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/PathPicker/c1dd1f7fc4dae5aa807218cf086942f1c9241783/src/tests/inputs/file-from-yocto_%.bbappend -------------------------------------------------------------------------------- /src/tests/inputs/file-from-yocto_3.1%.bbappend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/PathPicker/c1dd1f7fc4dae5aa807218cf086942f1c9241783/src/tests/inputs/file-from-yocto_3.1%.bbappend -------------------------------------------------------------------------------- /src/tests/inputs/fileNamesWithSpaces.txt: -------------------------------------------------------------------------------- 1 | inputs/annoying-hyphen-dir/Package\ Control.system-bundle 2 | inputs/annoying-hyphen-dir/Package Control.system-bundle 3 | inputs/annoying\ Spaces\ Folder/evilFile\ With\ Space2.txt 4 | inputs/annoying Spaces Folder/evilFile With Space2.txt 5 | -------------------------------------------------------------------------------- /src/tests/inputs/gitAbbreviatedFiles.txt: -------------------------------------------------------------------------------- 1 | .../fpp | 6 ++-- 2 | .../src/__tests__/__init__.py | 0 3 | .../src/__tests__/cursesForTest.py | 45 ++++++++++++++++++++++++++++ 4 | -------------------------------------------------------------------------------- /src/tests/inputs/gitBranch.txt: -------------------------------------------------------------------------------- 1 | fix3 2 | gh-pages 3 | * master 4 | testAllInput 5 | trunk 6 | -------------------------------------------------------------------------------- /src/tests/inputs/gitDiff.txt: -------------------------------------------------------------------------------- 1 | README.md | 8 ++++- 2 | fpp | 6 ++-- 3 | src/__tests__/__init__.py | 0 4 | src/__tests__/cursesForTest.py | 45 ++++++++++++++++++++++++++++ 5 | src/__tests__/initTest.py | 28 ++++++++++++++++++ 6 | src/__tests__/screenForTest.py | 67 ++++++++++++++++++++++++++++++++++++++++++ 7 | src/charCodeMapping.py | 20 +++++++++++++ 8 | src/choose.py | 15 ++++++++-- 9 | src/colorPrinter.py | 21 ++++++++----- 10 | src/cursesAPI.py | 40 +++++++++++++++++++++++++ 11 | src/format.py | 4 +-- 12 | src/processInput.py | 7 +++++ 13 | src/screenControl.py | 28 +++++++----------- 14 | src/screenFlags.py | 34 +++++++++++++++++++++ 15 | 14 files changed, 290 insertions(+), 33 deletions(-) 16 | -------------------------------------------------------------------------------- /src/tests/inputs/gitDiffColor.txt: -------------------------------------------------------------------------------- 1 | assets/launch_page.css | 4 + 2 | fpp | 4 + 3 | fpp.rb | 4 +- 4 | index.html | 11 ++- 5 | scripts/makeDist.sh | 2 +- 6 | .../expected/selectCommandWithPassedCommand.txt | 30 +++++++ 7 | src/__tests__/expected/selectDownSelectInverse.txt | 4 +- 8 | .../expected/simpleSelectWithAttributes.txt | 60 ++++++++++++++ 9 | src/__tests__/expected/simpleWithAttributes.txt | 60 ++++++++++++++ 10 | src/__tests__/screenForTest.py | 40 +++++++-- 11 | src/__tests__/screenTestRunner.py | 17 ++-- 12 | src/__tests__/testScreen.py | 96 ++++++++++++++++++---- 13 | src/format.py | 29 ++++++- 14 | src/formattedText.py | 3 + 15 | src/processInput.py | 10 +++ 16 | src/screenControl.py | 23 ++++++ 17 | src/screenFlags.py | 18 +++- 18 | src/stateFiles.py | 11 +++ 19 | src/version.py | 15 ++++ 20 | 19 files changed, 404 insertions(+), 37 deletions(-) 21 | -------------------------------------------------------------------------------- /src/tests/inputs/gitDiffNoStat.txt: -------------------------------------------------------------------------------- 1 | diff --git a/src/__tests__/screenTestRunner.py b/src/__tests__/screenTestRunner.py 2 | index f1f6f7a..5c71551 100644 3 | --- a/src/__tests__/screenTestRunner.py 4 | +++ b/src/__tests__/screenTestRunner.py 5 | @@ -36,24 +36,31 @@ def getRowsFromScreenRun( 6 | charInputs, 7 | screenConfig={}, 8 | printScreen=True, 9 | - pastScreen=None): 10 | + pastScreen=None, 11 | + args=[]): 12 | + 13 | lineObjs = getLineObjsFromFile(inputFile) 14 | screen = ScreenForTest( 15 | charInputs, 16 | maxX=screenConfig.get('maxX', 80), 17 | maxY=screenConfig.get('maxY', 30), 18 | ) 19 | - # mock our flags with an empty command line for now 20 | - flags = ScreenFlags.initFromArgs([]) 21 | + 22 | + # mock our flags with the passed arg list 23 | + flags = ScreenFlags.initFromArgs(args) 24 | + # we run our program and throw a StopIteration exception 25 | + # instead of sys.exit-ing 26 | try: 27 | choose.doProgram(screen, flags, CursesForTest(), lineObjs) 28 | except StopIteration: 29 | pass 30 | + 31 | if printScreen: 32 | screen.printOldScreens() 33 | + 34 | if pastScreen: 35 | - return screen.getRowsForPastScreen(pastScreen) 36 | - return screen.getRows() 37 | + return screen.getRowsWithAttributesForPastScreen(pastScreen) 38 | + return screen.getRowsWithAttributes() 39 | 40 | if __name__ == '__main__': 41 | getRowsFromScreenRun( 42 | -------------------------------------------------------------------------------- /src/tests/inputs/gitDiffSomeExist.txt: -------------------------------------------------------------------------------- 1 | src/pathpicker/char_code_mapping.py | 20 +++++++++++++ 2 | src/choose.py | 15 ++++++++-- 3 | src/pathpicker/color_printer.py | 21 ++++++++----- 4 | src/pathpicker/curses_api.py | 40 +++++++++++++++++++++++++ 5 | src/pathpicker/line_format.py | 4 +-- 6 | src/process_input.py | 7 +++++ 7 | /foo/bar/src/pathpicker/char_code_mapping.py | 20 +++++++++++++ 8 | /foo/bar/src/choose.py | 15 ++++++++-- 9 | /foo/bar/src/pathpicker/color_printer.py | 21 ++++++++----- 10 | /foo/bar/src/pathpicker/curses_api.py | 40 +++++++++++++++++++++++++ 11 | /foo/bar/src/pathpicker/format.py | 4 +-- 12 | /foo/bar/src/process_input.py | 7 +++++ 13 | /foo/bar/src/pathpicker/screen_control.py | 28 +++++++----------- 14 | /foo/bar/src/pathpicker/screen_flags.py | 34 +++++++++++++++++++++ 15 | 14 files changed, 290 insertions(+), 33 deletions(-) 16 | -------------------------------------------------------------------------------- /src/tests/inputs/longFileNames.txt: -------------------------------------------------------------------------------- 1 | longfilenamereallylongfilenameitssuperlongareyoutiredoftryingtoreadthisyet 2 | longfilenamereallylongfilenameitssuperlongareyoutiredoftryingtoreadthisyet.reallyafile 3 | -------------------------------------------------------------------------------- /src/tests/inputs/longFileNamesWithBeforeText.txt: -------------------------------------------------------------------------------- 1 | -rw-r--r--@ 1 pcottle THEFACEBOOK\Domain Users 401 Jun 4 2015 longfilenamereallylongfilenameitssuperlongareyoutiredoftryingtoreadthisyetding.txt 2 | -rw-r--r--@ 1 pcottle THEFACEBOOK\Domain Users 401 Jun 4 2015 foo.txt 3 | -------------------------------------------------------------------------------- /src/tests/inputs/longLineAbbreviated.txt: -------------------------------------------------------------------------------- 1 | src/__tests__/expected/allInputBranch.txt | 30 ++++++++ 2 | src/__tests__/expected/selectTwoCommandMode.txt | 4 +- 3 | src/__tests__/baz/bar/banana/fooo/fooooooo/woohoo/foo/bar/baz/banana/happa/inputs/annoyingTildeExtension.txt | 0 4 | src/__tests__/inputs/gitBranch.txt | 5 ++ 5 | src/__tests__/inputs/gitLongDiff.txt | 2 +- 6 | src/__tests__/inputs/gitLongDiffColor.txt | 2 +- 7 | src/__tests__/screenTestRunner.py | 9 ++- 8 | src/__tests__/testParsing.py | 79 ++++++++++++++++++++ 9 | src/__tests__/testScreen.py | 11 ++- 10 | src/choose.py | 8 +- 11 | src/format.py | 23 +++--- 12 | src/output.py | 29 +++++--- 13 | src/parse.py | 25 +++++-- 14 | src/processInput.py | 11 ++- 15 | src/screenControl.py | 87 ++++++++++++++-------- 16 | src/screenFlags.py | 12 ++- 17 | src/usageStrings.py | 3 + 18 | src/version.py | 2 +- 19 | 26 files changed, 286 insertions(+), 88 deletions(-) 20 | -------------------------------------------------------------------------------- /src/tests/inputs/longList.txt: -------------------------------------------------------------------------------- 1 | dummy1.txt 2 | dummy2.txt 3 | dummy3.txt 4 | dummy4.txt 5 | dummy5.txt 6 | dummy6.txt 7 | dummy7.txt 8 | dummy8.txt 9 | dummy9.txt 10 | dummy10.txt 11 | dummy11.txt 12 | dummy12.txt 13 | dummy13.txt 14 | dummy14.txt 15 | dummy15.txt 16 | dummy16.txt 17 | dummy17.txt 18 | dummy18.txt 19 | dummy19.txt 20 | dummy20.txt 21 | dummy21.txt 22 | dummy22.txt 23 | dummy23.txt 24 | dummy24.txt 25 | dummy25.txt 26 | dummy26.txt 27 | dummy27.txt 28 | dummy28.txt 29 | dummy29.txt 30 | dummy30.txt 31 | dummy31.txt 32 | dummy32.txt 33 | dummy33.txt 34 | dummy34.txt 35 | dummy35.txt 36 | dummy36.txt 37 | dummy37.txt 38 | dummy38.txt 39 | dummy39.txt 40 | dummy40.txt 41 | dummy41.txt 42 | dummy42.txt 43 | dummy43.txt 44 | dummy44.txt 45 | dummy45.txt 46 | dummy46.txt 47 | dummy47.txt 48 | dummy48.txt 49 | dummy49.txt 50 | dummy50.txt 51 | dummy51.txt 52 | dummy52.txt 53 | dummy53.txt 54 | dummy54.txt 55 | dummy55.txt 56 | dummy56.txt 57 | dummy57.txt 58 | dummy58.txt 59 | dummy59.txt 60 | dummy60.txt 61 | dummy61.txt 62 | dummy62.txt 63 | dummy63.txt 64 | dummy64.txt 65 | dummy65.txt 66 | dummy66.txt 67 | dummy67.txt 68 | dummy68.txt 69 | dummy69.txt 70 | dummy70.txt 71 | dummy71.txt 72 | dummy72.txt 73 | dummy73.txt 74 | dummy74.txt 75 | dummy75.txt 76 | dummy76.txt 77 | dummy77.txt 78 | dummy78.txt 79 | dummy79.txt 80 | dummy80.txt 81 | dummy81.txt 82 | dummy82.txt 83 | dummy83.txt 84 | dummy84.txt 85 | dummy85.txt 86 | dummy86.txt 87 | dummy87.txt 88 | dummy88.txt 89 | dummy89.txt 90 | dummy90.txt 91 | dummy91.txt 92 | dummy92.txt 93 | dummy93.txt 94 | dummy94.txt 95 | dummy95.txt 96 | dummy96.txt 97 | dummy97.txt 98 | dummy98.txt 99 | dummy99.txt 100 | dummy100.txt 101 | -------------------------------------------------------------------------------- /src/tests/inputs/superLongFileNames.txt: -------------------------------------------------------------------------------- 1 | ./hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-client/hadoop-mapreduce-project/hadoop-mapreduce-client/LongFileNamereallylongfileName.txt 2 | ./hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-client/hadoop-mapreduce-project/hadoop-mapreduce-client/some/deeper/dir/source.py 3 | -------------------------------------------------------------------------------- /src/tests/inputs/svo (install the zip not me).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/PathPicker/c1dd1f7fc4dae5aa807218cf086942f1c9241783/src/tests/inputs/svo (install the zip not me).xml -------------------------------------------------------------------------------- /src/tests/inputs/svo (install the zip, not me).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/PathPicker/c1dd1f7fc4dae5aa807218cf086942f1c9241783/src/tests/inputs/svo (install the zip, not me).xml -------------------------------------------------------------------------------- /src/tests/inputs/svo install the zip not me.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/PathPicker/c1dd1f7fc4dae5aa807218cf086942f1c9241783/src/tests/inputs/svo install the zip not me.xml -------------------------------------------------------------------------------- /src/tests/inputs/svo install the zip, not me.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/PathPicker/c1dd1f7fc4dae5aa807218cf086942f1c9241783/src/tests/inputs/svo install the zip, not me.xml -------------------------------------------------------------------------------- /src/tests/inputs/tonsOfFiles.txt: -------------------------------------------------------------------------------- 1 | 2 | ./tests/tests.c 3 | ./arch/x86/kernel/cpu.c 4 | ./arch/x86/kernel/ioapic.c 5 | ./arch/x86/kernel/apic.c 6 | ./arch/x86/kernel/gdt.c 7 | ./arch/x86/kernel/idt.c 8 | ./drivers/timer/i8254.c 9 | ./drivers/input/kbd.c 10 | ./drivers/acpi/acpica/rsinfo.c 11 | ./drivers/acpi/acpica/psxface.c 12 | ./drivers/acpi/acpica/evmisc.c 13 | ./drivers/acpi/acpica/nseval.c 14 | ./drivers/acpi/acpica/hwsleep.c 15 | ./drivers/acpi/acpica/nsprepkg.c 16 | ./drivers/acpi/acpica/nsaccess.c 17 | ./drivers/acpi/acpica/tbutils.c 18 | ./drivers/acpi/acpica/uteval.c 19 | ./drivers/acpi/acpica/psscope.c 20 | ./drivers/acpi/acpica/dsinit.c 21 | ./drivers/acpi/acpica/nswalk.c 22 | ./drivers/acpi/acpica/utxferror.c 23 | ./drivers/acpi/acpica/utascii.c 24 | ./drivers/acpi/acpica/tbinstal.c 25 | ./drivers/acpi/acpica/utprint.c 26 | ./drivers/acpi/acpica/evevent.c 27 | ./drivers/acpi/acpica/dsdebug.c 28 | ./drivers/acpi/acpica/psloop.c 29 | ./drivers/acpi/acpica/utstrtoul64.c 30 | ./drivers/acpi/acpica/utaddress.c 31 | ./drivers/acpi/acpica/evgpeutil.c 32 | ./drivers/acpi/acpica/dsobject.c 33 | ./drivers/acpi/acpica/exregion.c 34 | ./drivers/acpi/acpica/nsload.c 35 | ./drivers/acpi/acpica/utnonansi.c 36 | ./drivers/acpi/acpica/utxfinit.c 37 | ./drivers/acpi/acpica/evxface.c 38 | ./drivers/acpi/acpica/exconvrt.c 39 | ./drivers/acpi/acpica/hwtimer.c 40 | ./drivers/acpi/acpica/utdebug.c 41 | ./drivers/acpi/acpica/exoparg1.c 42 | ./drivers/acpi/acpica/utexcep.c 43 | ./drivers/acpi/acpica/nsxfobj.c 44 | ./drivers/acpi/acpica/hwgpe.c 45 | 46 | -------------------------------------------------------------------------------- /src/tests/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebook/PathPicker/c1dd1f7fc4dae5aa807218cf086942f1c9241783/src/tests/lib/__init__.py -------------------------------------------------------------------------------- /src/tests/lib/curses_api.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | from pathpicker.curses_api import CursesApiBase 6 | 7 | 8 | class CursesForTest(CursesApiBase): 9 | 10 | """The dependency-injected curses wrapper which simply 11 | stores some state in test runs of the UI""" 12 | 13 | def __init__(self) -> None: 14 | self.color_pairs = {} 15 | self.current_color = (0, 0) 16 | # The (0, 0) is hardcoded. 17 | self.color_pairs[0] = self.current_color 18 | 19 | def use_default_colors(self) -> None: 20 | pass 21 | 22 | def echo(self) -> None: 23 | pass 24 | 25 | def noecho(self) -> None: 26 | pass 27 | 28 | def init_pair(self, pair_number: int, fg_color: int, bg_color: int) -> None: 29 | self.color_pairs[pair_number] = (fg_color, bg_color) 30 | 31 | def color_pair(self, color_number: int) -> int: 32 | self.current_color = self.color_pairs[color_number] 33 | # TODO -- find a better return than this? 34 | return color_number 35 | 36 | def get_color_pairs(self) -> int: 37 | # pretend we are on 256 color 38 | return 256 39 | 40 | def exit(self) -> None: 41 | raise StopIteration("stopping program") 42 | 43 | def allow_file_output(self) -> bool: 44 | # do not output selection pickle 45 | return False 46 | -------------------------------------------------------------------------------- /src/tests/lib/key_bindings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | from pathpicker.key_bindings import KeyBindings 6 | 7 | KEY_BINDINGS_FOR_TEST_CONFIG_CONTENT: str = "[bindings]\nr = rspec\ns = subl\n" 8 | KEY_BINDINGS_FOR_TEST: KeyBindings = KeyBindings( 9 | [ 10 | ("r", "rspec"), 11 | ("s", "subl"), 12 | ] 13 | ) 14 | -------------------------------------------------------------------------------- /src/tests/lib/screen.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | from copy import copy 6 | from typing import Dict, List, NewType, Optional, Tuple 7 | 8 | from pathpicker.char_code_mapping import CHAR_TO_CODE 9 | from pathpicker.screen import ScreenBase 10 | 11 | ATTRIBUTE_SYMBOL_MAPPING: Dict[int, str] = { 12 | 0: " ", 13 | 1: " ", 14 | 2: "B", 15 | 2097154: "*", # bold white 16 | 131072: "_", 17 | 3: "G", 18 | 4: "R", 19 | 5: "?", 20 | 6: "!", 21 | 2097153: "W", 22 | 2097155: "|", # bold 23 | 2097156: "/", # bold 24 | 2097158: "~", # bold 25 | 2097157: "@", # bold 26 | 7: "?", 27 | } 28 | 29 | 30 | ScreenType = NewType("ScreenType", Dict[Tuple[int, int], Tuple[str, int]]) 31 | 32 | 33 | class ScreenForTest(ScreenBase): 34 | """A dummy object that is dependency-injected in place 35 | of curses standard screen. Allows us to unit-test parts 36 | of the UI code""" 37 | 38 | def __init__(self, char_inputs: List[str], max_x: int, max_y: int): 39 | self.max_x = max_x 40 | self.max_y = max_y 41 | self.output = ScreenType({}) 42 | self.past_screens: List[ScreenType] = [] 43 | self.char_inputs = char_inputs 44 | self.erase() 45 | self.current_attribute = 0 46 | 47 | def getmaxyx(self) -> Tuple[int, int]: 48 | return self.max_y, self.max_x 49 | 50 | def refresh(self) -> None: 51 | if self.contains_content(self.output): 52 | # we have an old screen, so add it 53 | self.past_screens.append(copy(self.output)) 54 | 55 | def contains_content(self, screen: ScreenType) -> bool: 56 | for _coord, pair in screen.items(): 57 | (char, _attr) = pair 58 | if char: 59 | return True 60 | return False 61 | 62 | def erase(self) -> None: 63 | self.output = ScreenType({}) 64 | for x_pos in range(self.max_x): 65 | for y_pos in range(self.max_y): 66 | coord = (x_pos, y_pos) 67 | self.output[coord] = ("", 1) 68 | 69 | def move(self, _y_pos: int, _x_pos: int) -> None: 70 | pass 71 | 72 | def attrset(self, attr: int) -> None: 73 | self.current_attribute = attr 74 | 75 | def addstr( 76 | self, y_pos: int, x_pos: int, string: str, attr: Optional[int] = None 77 | ) -> None: 78 | if attr: 79 | self.attrset(attr) 80 | for delta_x, value in enumerate(string): 81 | coord = (x_pos + delta_x, y_pos) 82 | self.output[coord] = (value, self.current_attribute) 83 | 84 | def delch(self, y_pos: int, x_pos: int) -> None: 85 | """Delete a character. We implement this by removing the output, 86 | NOT by printing a space""" 87 | self.output[(x_pos, y_pos)] = ("", 1) 88 | 89 | def getch(self) -> int: 90 | return CHAR_TO_CODE[self.char_inputs.pop(0)] 91 | 92 | def getstr(self, _y: int, _x: int, _max_len: int) -> str: 93 | # TODO -- enable editing this 94 | return "" 95 | 96 | def print_screen(self) -> None: 97 | for index, row in enumerate(self.get_rows()): 98 | print(f"Row {index:02}:{row}") 99 | 100 | def print_old_screens(self) -> None: 101 | for old_screen in range(self.get_num_past_screens()): 102 | for index, row in enumerate(self.get_rows_for_past_screen(old_screen)): 103 | print(f"Screen {old_screen:02} Row {index:02}:{row}") 104 | 105 | def get_num_past_screens(self) -> int: 106 | return len(self.past_screens) 107 | 108 | def get_rows_for_past_screen(self, past_screen: int) -> List[str]: 109 | return self.get_rows(screen=self.past_screens[past_screen]) 110 | 111 | def get_rows_with_attributes_for_past_screen( 112 | self, past_screen: int 113 | ) -> Tuple[List[str], List[str]]: 114 | return self.get_rows_with_attributes(screen=self.past_screens[past_screen]) 115 | 116 | def get_rows_with_attributes_for_past_screens( 117 | self, past_screens: List[int] 118 | ) -> Tuple[List[str], List[str]]: 119 | """Get the rows & attributes for the array of screens as one stream 120 | (there is no extra new line or extra space between pages)""" 121 | pages = map( 122 | lambda screen_index: self.get_rows_with_attributes( 123 | screen=self.past_screens[screen_index] 124 | ), 125 | past_screens, 126 | ) 127 | 128 | # join the pages together into one stream 129 | lines, attributes = zip(*pages) 130 | return ( 131 | [line for page in lines for line in page], 132 | [line for page in attributes for line in page], 133 | ) 134 | 135 | def get_rows_with_attributes( 136 | self, screen: Optional[ScreenType] = None 137 | ) -> Tuple[List[str], List[str]]: 138 | if not screen: 139 | screen = self.output 140 | 141 | rows: List[str] = [] 142 | attribute_rows: List[str] = [] 143 | for y_pos in range(self.max_y): 144 | row = "" 145 | attribute_row = "" 146 | for x_pos in range(self.max_x): 147 | coord = (x_pos, y_pos) 148 | (char, attr) = screen[coord] 149 | row += char 150 | attribute_row += self.get_attribute_symbol_for_code(attr) 151 | rows.append(row) 152 | attribute_rows.append(attribute_row) 153 | return rows, attribute_rows 154 | 155 | def get_rows(self, screen: Optional[ScreenType] = None) -> List[str]: 156 | (rows, _) = self.get_rows_with_attributes(screen) 157 | return rows 158 | 159 | def get_attribute_symbol_for_code(self, code: int) -> str: 160 | symbol = ATTRIBUTE_SYMBOL_MAPPING.get(code, None) 161 | if symbol is None: 162 | raise ValueError(f"{code} not mapped") 163 | return symbol 164 | -------------------------------------------------------------------------------- /src/tests/lib/screen_test_runner.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | import os 6 | from typing import Dict, List, Optional, Tuple 7 | 8 | import choose 9 | import process_input 10 | from pathpicker.line_format import LineBase 11 | from pathpicker.screen_flags import ScreenFlags 12 | from tests.lib.curses_api import CursesForTest 13 | from tests.lib.key_bindings import KEY_BINDINGS_FOR_TEST 14 | from tests.lib.screen import ScreenForTest 15 | 16 | INPUT_DIR = "./inputs/" 17 | 18 | 19 | def get_line_objs_from_file( 20 | input_file: str, validate_file_exists: bool, all_input: bool 21 | ) -> Dict[int, LineBase]: 22 | input_file = os.path.join(INPUT_DIR, input_file) 23 | file = open(input_file) 24 | lines = file.read().split("\n") 25 | file.close() 26 | return process_input.get_line_objs_from_lines( 27 | lines, validate_file_exists=validate_file_exists, all_input=all_input 28 | ) 29 | 30 | 31 | def get_rows_from_screen_run( 32 | input_file: str, 33 | char_inputs: List[str], 34 | screen_config: Dict[str, int], 35 | print_screen: bool, 36 | past_screen: Optional[int], 37 | past_screens: Optional[List[int]], 38 | args: List[str], 39 | validate_file_exists: bool, 40 | all_input: bool, 41 | ) -> Tuple[List[str], List[str]]: 42 | line_objs = get_line_objs_from_file( 43 | input_file, validate_file_exists=validate_file_exists, all_input=all_input 44 | ) 45 | screen = ScreenForTest( 46 | char_inputs, 47 | max_x=screen_config.get("maxX", 80), 48 | max_y=screen_config.get("maxY", 30), 49 | ) 50 | 51 | # mock our flags with the passed arg list 52 | flags = ScreenFlags.init_from_args(args) 53 | # we run our program and throw a StopIteration exception 54 | # instead of sys.exit-ing 55 | try: 56 | choose.do_program( 57 | screen, flags, KEY_BINDINGS_FOR_TEST, CursesForTest(), line_objs 58 | ) 59 | except StopIteration: 60 | pass 61 | 62 | if print_screen: 63 | screen.print_old_screens() 64 | 65 | if past_screen: 66 | return screen.get_rows_with_attributes_for_past_screen(past_screen) 67 | if past_screens: 68 | return screen.get_rows_with_attributes_for_past_screens(past_screens) 69 | return screen.get_rows_with_attributes() 70 | -------------------------------------------------------------------------------- /src/tests/test_key_bindings_parsing.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | import tempfile 6 | import unittest 7 | 8 | from pathpicker.key_bindings import read_key_bindings 9 | from tests.lib.key_bindings import ( 10 | KEY_BINDINGS_FOR_TEST, 11 | KEY_BINDINGS_FOR_TEST_CONFIG_CONTENT, 12 | ) 13 | 14 | 15 | class TestKeyBindingsParser(unittest.TestCase): 16 | def test_ignore_non_existing_configuration_file(self) -> None: 17 | file = tempfile.NamedTemporaryFile(delete=True) 18 | file.close() 19 | 20 | bindings = read_key_bindings(file.name) 21 | 22 | self.assertEqual( 23 | bindings, 24 | [], 25 | ( 26 | "The parser did not return an empty list, " 27 | f"when initialized with a non-existent file: {bindings}" 28 | ), 29 | ) 30 | 31 | def test_standard_parsing(self) -> None: 32 | file = tempfile.NamedTemporaryFile(mode="wt", delete=False) 33 | file.write(KEY_BINDINGS_FOR_TEST_CONFIG_CONTENT) 34 | file.close() 35 | 36 | bindings = read_key_bindings(file.name) 37 | 38 | actual_result = sorted(bindings) 39 | expected_result = KEY_BINDINGS_FOR_TEST 40 | 41 | self.assertEqual( 42 | actual_result, 43 | expected_result, 44 | ( 45 | "The parser did not properly parse the test file\n\n" 46 | f'Expected:"{expected_result}"\nActual :"{actual_result}"' 47 | ), 48 | ) 49 | 50 | 51 | if __name__ == "__main__": 52 | unittest.main() 53 | -------------------------------------------------------------------------------- /src/version.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | VERSION = "0.9.5" 7 | 8 | 9 | if __name__ == "__main__": 10 | print(VERSION) 11 | --------------------------------------------------------------------------------