├── .gitignore ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── tests ├── mocks │ └── adb_mock.sh ├── res │ └── adb_log_example.sh └── validators │ └── file_validator.sh ├── src ├── serial │ ├── serial_interface.sh │ ├── serial_init.sh │ └── serial_picker.sh ├── shell │ ├── shell_utils.sh │ ├── shell_env_init.sh │ ├── shell_env_check.sh │ └── argument_parsing.sh ├── threads │ ├── thread_controller.sh │ ├── thread_interface.sh │ ├── thread_background.sh │ └── thread_setup.sh ├── fzf_env │ ├── load_copy_program.sh │ └── fzf_default_state.sh ├── bindings │ ├── navigation_bindings.sh │ ├── command_suites.sh │ ├── input_trim_bindings.sh │ ├── query_bindings.sh │ ├── copy_bindings.sh │ ├── mode_bindings.sh │ ├── history_bindings.sh │ ├── misc_bindings.sh │ ├── stream_bindings.sh │ └── preview_bindings.sh ├── execution_loop.sh └── ui │ ├── ui_utils.sh │ └── ui_strings.sh ├── res └── header │ └── purr_header.txt ├── bundled └── osc52_copy.sh ├── docs ├── contributing.md └── code-of-conduct.md ├── makefile ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | g3upt/ 2 | g3doc/ 3 | out/ 4 | obj/ 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Expected Behavior 2 | 3 | 4 | ## Actual Behavior 5 | 6 | 7 | ## Steps to Reproduce the Problem 8 | 9 | 1. 10 | 1. 11 | 1. 12 | 13 | ## Specifications 14 | 15 | - Version: 16 | - Platform: -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # 2 | 3 | > It's a good idea to open an issue first for discussion. 4 | 5 | - [ ] Appropriate changes to README are included in PR 6 | - [ ] Basic testing of all relevant hotkeys and modes has been completed. -------------------------------------------------------------------------------- /tests/mocks/adb_mock.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | if grep -q "devices" <<<$@; then 4 | echo "List of devices attached" 5 | echo "emulator-5554 device" 6 | elif grep -q "wait-for-device" <<<$@; then 7 | sleep 0.01 8 | elif grep -q "logcat" <<<$@; then 9 | echo $mocked_adb_output 10 | sleep 10 11 | fi 12 | -------------------------------------------------------------------------------- /src/serial/serial_interface.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | update_serial_cmd="serial=\$(cat $purr_serial_cache);" 18 | 19 | __purr_update_serial() { 20 | serial=$(cat $purr_serial_cache 2>/dev/null) 21 | } 22 | -------------------------------------------------------------------------------- /src/serial/serial_init.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Grabs a serial number from adb. 18 | serial=$(pick_serial) 19 | if [ $? -ne 0 ]; then 20 | echo $serial 21 | __purr_cleanup $dir_name 22 | exit 61 23 | fi 24 | 25 | # Load the serial number into the serial cache. 26 | echo $serial >$purr_serial_cache 27 | -------------------------------------------------------------------------------- /src/shell/shell_utils.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # We need this because Macs don't have "timeout" as a default command. 18 | # However, both Mac and Linux machines (typically) have perl... 19 | purr_timeout() { 20 | PERL_BADLANG=0 perl -e 'alarm shift; exec @ARGV' "$@" 21 | } 22 | 23 | # Inefficient, but portable on Linux and Mac. 24 | wait_for_file() { 25 | while ! [ -s $1 ]; do 26 | sleep 0.01 27 | done 28 | } 29 | -------------------------------------------------------------------------------- /res/header/purr_header.txt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Error code bands: 18 | # 10-19: User-induced error. 19 | # 18 - User did not select a serial. 20 | # 50-59: Irrecoverable environment-based error. 21 | # 51 - Shell is not zsh. 22 | # 52 - No valid adb installation found. 23 | # 53 - No valid fzf installation found. 24 | # 54 - No valid perl installation found. 25 | # 60-69: Irrecoverable ADB-related error. 26 | # 61 - Could not get a serial number. 27 | -------------------------------------------------------------------------------- /bundled/osc52_copy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | OSC_52_MAX_SEQUENCE="100000" 4 | 5 | die() { 6 | echo "ERROR: $*" 7 | exit 1 8 | } 9 | 10 | tmux_dcs() { 11 | printf '\033Ptmux;\033%s\033\\' "$1" 12 | } 13 | 14 | screen_dcs() { 15 | local limit=256 16 | echo "$1" | 17 | sed -E "s:.{$((limit - 4))}:&\n:g" | 18 | sed -E -e 's:^:\x1bP:' -e 's:$:\x1b\\:' | 19 | tr -d '\n' 20 | } 21 | 22 | print_seq() { 23 | local seq="$1" 24 | case ${TERM-} in 25 | screen*) 26 | if [ -n "${TMUX-}" ]; then 27 | tmux_dcs "${seq}" 28 | else 29 | screen_dcs "${seq}" 30 | fi 31 | ;; 32 | tmux*) 33 | tmux_dcs "${seq}" 34 | ;; 35 | *) 36 | echo "${seq}" 37 | ;; 38 | esac 39 | } 40 | 41 | b64enc() { 42 | base64 | tr -d '\n' 43 | } 44 | 45 | osc_52_copy() { 46 | local str 47 | if [ $# -eq 0 ]; then 48 | str="$(b64enc)" 49 | else 50 | str="$(echo "$@" | b64enc)" 51 | fi 52 | if [ ${OSC_52_MAX_SEQUENCE} -gt 0 ]; then 53 | local len=${#str} 54 | if [ ${len} -gt ${OSC_52_MAX_SEQUENCE} ]; then 55 | die "selection too long to send to terminal:" \ 56 | "${OSC_52_MAX_SEQUENCE} limit, ${len} attempted" 57 | fi 58 | fi 59 | print_seq "$(printf '\033]52;c;%s\a' "${str}")" 60 | } 61 | 62 | set -e 63 | 64 | osc_52_copy "$@" 65 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. 4 | 5 | ## Before you begin 6 | 7 | ### Sign our Contributor License Agreement 8 | 9 | Contributions to this project must be accompanied by a 10 | [Contributor License Agreement](https://cla.developers.google.com/about) (CLA). 11 | You (or your employer) retain the copyright to your contribution; this simply 12 | gives us permission to use and redistribute your contributions as part of the 13 | project. 14 | 15 | If you or your current employer have already signed the Google CLA (even if it 16 | was for a different project), you probably don't need to do it again. 17 | 18 | Visit to see your current agreements or to 19 | sign a new one. 20 | 21 | ### Review our Community Guidelines 22 | 23 | This project follows [Google's Open Source Community 24 | Guidelines](https://opensource.google/conduct/). 25 | 26 | ## Contribution process 27 | 28 | ### Code Reviews 29 | 30 | All submissions, including submissions by project members, require review. We 31 | use GitHub pull requests for this purpose. Consult 32 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 33 | information on using pull requests. 34 | -------------------------------------------------------------------------------- /src/shell/shell_env_init.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | if [ -z $dir_name ]; then 18 | dir_name=$(mktemp -d /tmp/purr.XXXXXXXXXX) 19 | else 20 | mkdir -p $dir_name 21 | fi 22 | 23 | # Run cleanup if we exit abnormally. 24 | trap "__purr_cleanup $dir_name" INT TERM 25 | 26 | # Tell fzf to run commands in zsh. 27 | SHELL=$(which zsh) 28 | 29 | # Create the cache files we'll use to communicate with fzf. 30 | __purr_create_files $dir_name 31 | 32 | # If we are in TMUX or SSH, $TTY might not be the TTY we want to route to. 33 | if [ -n "${TMUX-}" ]; then 34 | pane_active_tty=$(tmux list-panes -F "#{pane_active} #{pane_tty}" | awk '$1=="1" { print $2 }') 35 | if [ ! -z $SSH_TTY ]; then 36 | target_tty="${SSH_TTY:-$pane_active_tty}" 37 | else 38 | target_tty="${TTY:-$pane_active_tty}" 39 | fi 40 | else 41 | target_tty="$TTY" 42 | fi 43 | -------------------------------------------------------------------------------- /src/threads/thread_controller.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | PURR_THREAD_CLEANUP="purr_thread_cleanup" 18 | PURR_THREAD_START="purr_thread_start" 19 | PURR_THREAD_STOP="purr_thread_stop" 20 | 21 | # The background handler is responsible for starting and killing 22 | # the stream threads. It uses the thread_io_pipe 23 | # as a one-way communication between the core process and itself, 24 | # and then handles asynchronous processes related to the stream threads. 25 | __purr_background_handler() { 26 | __purr_start_streams 27 | 28 | while true; do 29 | if read line <$thread_io_pipe; then 30 | if [ $line = "$PURR_THREAD_STOP" ]; then 31 | __purr_cleanup_streams 32 | elif [ $line = "$PURR_THREAD_START" ]; then 33 | __purr_start_streams 34 | elif [ $line = "$PURR_THREAD_CLEANUP" ]; then 35 | __purr_cleanup_streams 36 | exit 0 37 | fi 38 | fi 39 | done 40 | } 41 | -------------------------------------------------------------------------------- /src/fzf_env/load_copy_program.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | if [ ! -z $COPY_PROGRAM ]; then 18 | purr_copy_program="$COPY_PROGRAM" 19 | elif command -v pbcopy &>/dev/null && [ -z $SSH_TTY ]; then 20 | purr_copy_program="$(which pbcopy)" 21 | elif command -v wl-copy &>/dev/null && [ $XDG_SESSION_TYPE = "wayland" ]; then 22 | purr_copy_program="wl-copy" 23 | elif command -v xsel &>/dev/null && [ ! -z $DISPLAY ]; then 24 | purr_copy_program="xsel --clipboard --input" 25 | elif command -v osc52_copy &>/dev/null; then 26 | purr_copy_program="osc52_copy" 27 | fi 28 | 29 | if [ -z $purr_copy_program ]; then 30 | echo >&2 "Could not identify a known copy program!" 31 | echo >&2 "You can set a copy program by exporting the 'COPY_PROGRAM' variable." 32 | echo >&2 "If your terminal supports it, an OSC52 program is bundled in bundled/osc52_copy." 33 | echo >&2 "purr will continue, but copy commands will not work." 34 | fi 35 | -------------------------------------------------------------------------------- /src/bindings/navigation_bindings.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Bind: ctrl-f, brings the user to the currently selected entry. 18 | bind_commands+=('--bind' "ctrl-f:track+clear-query+hide-preview+execute-silent(echo 'hidden' >| $purr_preview_visible_cache;)") 19 | rebind_in_default_command_suite "ctrl-f" 20 | rebind_in_adb_command_suite "ctrl-f" 21 | rebind_in_history_command_suite "ctrl-f" 22 | rebind_in_serial_command_suite "ctrl-f" 23 | 24 | # Bind: home/end, similar to page-up/page-down for preview. 25 | bind_commands+=('--bind' "home:preview-page-up") 26 | rebind_in_default_command_suite "home" 27 | rebind_in_adb_command_suite "home" 28 | rebind_in_history_command_suite "home" 29 | rebind_in_serial_command_suite "home" 30 | 31 | bind_commands+=('--bind' "end:preview-page-down") 32 | rebind_in_default_command_suite "end" 33 | rebind_in_adb_command_suite "end" 34 | rebind_in_history_command_suite "end" 35 | rebind_in_serial_command_suite "end" 36 | -------------------------------------------------------------------------------- /src/threads/thread_interface.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | __purr_start_stream() { 18 | echo "$PURR_THREAD_START" >$thread_io_pipe 19 | } 20 | 21 | __purr_thread_stop_stream() { 22 | echo "$PURR_THREAD_STOP" >$thread_io_pipe 23 | } 24 | 25 | __purr_cleanup() { 26 | local dir_name=$1 27 | 28 | # Send a message to the background threads that they need to die. 29 | if [ -p $thread_io_pipe ]; then 30 | echo "$PURR_THREAD_CLEANUP" >$thread_io_pipe 31 | fi 32 | 33 | # Delete all of the cached state files. 34 | if [ -d $dir_name ] && [[ $delete_dir_flag = "true" ]]; then 35 | rm -r $dir_name &>/dev/null 36 | fi 37 | } 38 | 39 | # We want to avoid the case where we don't have any input coming through, 40 | # causing a block until an error is thrown. 41 | __wait_for_input_streams() { 42 | purr_timeout 0.1 wait_for_file $purr_verbose_input_cache 43 | purr_timeout 0.1 wait_for_file $purr_info_input_cache 44 | purr_timeout 0.1 wait_for_file $purr_warning_input_cache 45 | purr_timeout 0.1 wait_for_file $purr_error_input_cache 46 | } 47 | -------------------------------------------------------------------------------- /src/bindings/command_suites.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Double-click is bound by default, just unbind it here. 18 | default_command_suite="unbind(double-click)" 19 | adb_command_suite="unbind(double-click)" 20 | history_command_suite="unbind(double-click)" 21 | serial_command_suite="unbind(double-click)" 22 | 23 | rebind_in_default_command_suite() { 24 | default_command_suite="$default_command_suite+rebind($1)" 25 | } 26 | 27 | rebind_in_adb_command_suite() { 28 | adb_command_suite="$adb_command_suite+rebind($1)" 29 | } 30 | 31 | rebind_in_history_command_suite() { 32 | history_command_suite="$history_command_suite+rebind($1)" 33 | } 34 | 35 | rebind_in_serial_command_suite() { 36 | serial_command_suite="$serial_command_suite+rebind($1)" 37 | } 38 | 39 | unbind_in_default_command_suite() { 40 | default_command_suite="$default_command_suite+unbind($1)" 41 | } 42 | 43 | unbind_in_adb_command_suite() { 44 | adb_command_suite="$adb_command_suite+unbind($1)" 45 | } 46 | 47 | unbind_in_history_command_suite() { 48 | history_command_suite="$history_command_suite+unbind($1)" 49 | } 50 | 51 | unbind_in_serial_command_suite() { 52 | serial_command_suite="$serial_command_suite+unbind($1)" 53 | } 54 | -------------------------------------------------------------------------------- /src/shell/shell_env_check.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Ensures the user's environment can support running purr. 18 | __purr_check_env() { 19 | 20 | # Ensures that the calling shell in zsh. 21 | if [ -n "$ZSH_VERSION" ]; then 22 | : # Left blank for clarity. 23 | elif [ -n "$BASH_VERSION" ]; then 24 | echo >&2 "Bash detected. Purr will malfunction. Aborting." 25 | exit 51 26 | else 27 | echo >&2 "Unsure of shell. Purr may malfunction. Aborting." 28 | exit 51 29 | fi 30 | 31 | # Ensures that the user has adb installed. 32 | if ! type $adb_cmd_loc &>/dev/null; then 33 | echo >&2 "purr requires ADB to be installed for correct operation. Aborting." 34 | exit 52 35 | fi 36 | 37 | # Ensures that the user has fzf 0.40.0 or later installed. 38 | autoload is-at-least 39 | if ! command -v fzf &>/dev/null || ! is-at-least $REQUIRED_FZF_VERSION $(fzf --version | cut -d' ' -f1); then 40 | echo >&2 "purr requires fzf 0.40.0 or higher to be installed for correct operation. Aborting." 41 | exit 53 42 | fi 43 | 44 | # Ensures that the user has perl installed. 45 | if ! command -v perl &>/dev/null; then 46 | echo >&2 "purr requires perl to be installed for correct operation. Aborting." 47 | exit 54 48 | fi 49 | } 50 | 51 | __purr_check_env 52 | -------------------------------------------------------------------------------- /src/shell/argument_parsing.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | REQUIRED_FZF_VERSION="0.40.0" 18 | 19 | VERSION="2.0.5" 20 | 21 | USAGE=("purr" 22 | "\n[-q: Sets the default query]" 23 | "\n[-i: Disables the instruction header]" 24 | "\n[-a: Set custom adb parameters as a string]" 25 | "\n[-f: Set custom fzf parameters as a string]" 26 | "\n[-v: Get version number]" 27 | "\n[-V: Get version of all dependencies]") 28 | 29 | __purr_get_composite_version() { 30 | composite_version="Purr: $VERSION" 31 | 32 | if ! command -v fzf &>/dev/null; then 33 | composite_version="$composite_version fzf: Not Installed" 34 | else 35 | composite_version="$composite_version fzf: $(fzf --version)" 36 | fi 37 | 38 | if [ -z $ZSH_VERSION ]; then 39 | composite_version="$composite_version zsh: Not Installed" 40 | else 41 | composite_version="$composite_version zsh: $ZSH_VERSION" 42 | fi 43 | 44 | echo $composite_version 45 | } 46 | 47 | # Parse argument flags. 48 | instruction_flag=true 49 | adb_cmd_loc="adb" 50 | delete_dir_flag=true 51 | fzf_exec_flag=true 52 | while getopts ':XA:D:a:f:ivVq:' flags; do 53 | case $flags in 54 | q) query_string="--query=${OPTARG}" ;; 55 | a) custom_adb_params=${OPTARG} ;; 56 | A) adb_cmd_loc=${OPTARG} ;; 57 | D) 58 | delete_dir_flag=false 59 | dir_name=${OPTARG} ;; 60 | f) custom_fzf_params=${OPTARG} ;; 61 | i) instruction_flag=false ;; 62 | X) fzf_exec_flag=false ;; 63 | v) 64 | echo $VERSION 65 | exit 0 66 | ;; 67 | V) 68 | echo "$(__purr_get_composite_version)" 69 | exit 0 70 | ;; 71 | *) 72 | echo $USAGE 73 | exit 1 74 | ;; 75 | esac 76 | done 77 | -------------------------------------------------------------------------------- /tests/res/adb_log_example.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | mocked_adb_output=$(cat <<-END 18 | --------- beginning of system 19 | 11-30 11:51:17.111 520 565 V DisplayPowerController2[0]: Brightness [0.39763778] reason changing to: 'manual', previous reason: 'manual [ dim ]'. 20 | 11-30 11:51:17.111 520 565 I DisplayPowerController2[0]: BrightnessEvent: disp=0, physDisp=local:4619827259835644672, brt=0.39763778, initBrt=0.05, rcmdBrt=NaN, preBrt=NaN, lux=0.0, preLux=0.0, hbmMax=1.0, hbmMode=off, rbcStrength=0, thrmMax=1.0, powerFactor=1.0, wasShortTermModelActive=false, flags=, reason=manual, autoBrightness=false, strategy=InvalidBrightnessStrategy 21 | --------- beginning of main 22 | 11-30 11:51:17.159 397 397 I android.hardware.lights-service.example: Lights setting state for id=1 to color ff101010 23 | 11-30 11:51:17.186 397 397 I android.hardware.lights-service.example: Lights setting state for id=1 to color ff111111 24 | 11-30 11:51:17.197 397 397 I android.hardware.lights-service.example: Lights setting state for id=1 to color ff121212 25 | 11-30 11:51:17.214 397 397 I android.hardware.lights-service.example: Lights setting state for id=1 to color ff131313 26 | 11-30 11:51:17.231 397 397 I android.hardware.lights-service.example: Lights setting state for id=1 to color ff141414 27 | 11-30 11:51:17.247 397 397 I android.hardware.lights-service.example: Lights setting state for id=1 to color ff151515 28 | 11-30 11:51:17.264 397 397 I android.hardware.lights-service.example: Lights setting state for id=1 to color ff161616 29 | 11-30 11:51:17.281 397 397 I android.hardware.lights-service.example: Lights setting state for id=1 to color ff171717 30 | END 31 | ) 32 | -------------------------------------------------------------------------------- /src/bindings/input_trim_bindings.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Bind: ctrl-w, send a cache wipe request to logcat. 18 | wipe_cmd=( 19 | 'execute-silent(' 20 | $stop_stream 21 | $update_serial_cmd 22 | "$adb_cmd_loc -s \$serial logcat -c;" 23 | $set_slock_off 24 | "echo 'wipe' > $purr_accept_command_cache;" 25 | $save_current_query 26 | $start_stream 27 | ')+accept' 28 | ) 29 | bind_commands+=('--bind' "ctrl-w:$wipe_cmd") 30 | rebind_in_default_command_suite "ctrl-w" 31 | unbind_in_adb_command_suite "ctrl-w" 32 | unbind_in_history_command_suite "ctrl-w" 33 | unbind_in_serial_command_suite "ctrl-w" 34 | 35 | # Bind: ctrl-t, trim input to the end of the file. 36 | trim_cmd=( 37 | 'execute-silent(' 38 | "trimmed_time=\$(echo {} | cut -d' ' -f1-2);" 39 | 'if [ ! -z $trimmed_time ]; then' 40 | $stop_stream 41 | $update_serial_cmd 42 | $set_slock_off 43 | "echo 'trim' > $purr_accept_command_cache;" 44 | # Grab the timestamp from the selected message. 45 | "echo \$trimmed_time > $purr_time_start_cache;" 46 | $save_current_query 47 | $start_stream 48 | 'fi;' 49 | ')+accept-non-empty' 50 | ) 51 | bind_commands+=('--bind' "ctrl-t:$trim_cmd") 52 | rebind_in_default_command_suite "ctrl-t" 53 | unbind_in_adb_command_suite "ctrl-t" 54 | unbind_in_history_command_suite "ctrl-t" 55 | unbind_in_serial_command_suite "ctrl-t" 56 | 57 | # Bind: ctrl-alt-t, de-trim input. 58 | untrim_cmd=( 59 | 'execute-silent(' 60 | $stop_stream 61 | $update_serial_cmd 62 | $set_slock_off 63 | "echo 'trim' > $purr_accept_command_cache;" 64 | "echo '' > $purr_time_start_cache;" 65 | $save_current_query 66 | $start_stream 67 | ')+accept' 68 | ) 69 | bind_commands+=('--bind' "ctrl-alt-t:$untrim_cmd") 70 | rebind_in_default_command_suite "ctrl-alt-t" 71 | unbind_in_adb_command_suite "ctrl-alt-t" 72 | unbind_in_history_command_suite "ctrl-alt-t" 73 | unbind_in_serial_command_suite "ctrl-alt-t" 74 | -------------------------------------------------------------------------------- /src/bindings/query_bindings.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Bind: ctrl-alt-s, get tag from line and add it to query. 18 | get_tag_cmd=( 19 | 'execute-silent(' 20 | 'tag=$(echo "{}" | xargs | xargs | cut -d" " -f6);' 21 | 'cur_query=$(echo "{q}" | xargs | xargs);' 22 | 'if echo $cur_query | grep -w -q -- "$tag"; then' 23 | "echo \"\$cur_query\" > $purr_query_cache;" 24 | 'elif [ -z $cur_query ]; then' 25 | "echo \"\$tag\" > $purr_query_cache;" 26 | 'else;' 27 | "echo \"\$cur_query \$tag\" > $purr_query_cache;" 28 | 'fi;' 29 | ')+transform-query(' 30 | "cat $purr_query_cache;" 31 | "echo "" > $purr_query_cache;" 32 | ')' 33 | ) 34 | bind_commands+=('--bind' "ctrl-alt-s:$get_tag_cmd") 35 | rebind_in_default_command_suite "ctrl-alt-s" 36 | unbind_in_adb_command_suite "ctrl-alt-s" 37 | unbind_in_history_command_suite "ctrl-alt-s" 38 | unbind_in_serial_command_suite "ctrl-alt-s" 39 | 40 | 41 | # Bind: ctrl-alt-d, get tag from line and add it as negative query. 42 | remove_tag_cmd=( 43 | 'execute-silent(' 44 | 'tag=$(echo "{}" | xargs | xargs | cut -d" " -f6);' 45 | 'negative_tag="!$tag";' 46 | 'cur_query=$(echo "{q}" | xargs | xargs);' 47 | 'if echo $cur_query | grep -w -q -- "$tag"; then' 48 | 'untagged_query=$(echo "$cur_query" | sed "s/\b$tag\b//g" | sed "s/\b$tag//g" | xargs);' 49 | "echo \"\$untagged_query\" > $purr_query_cache;" 50 | 'elif [ -z $cur_query ]; then' 51 | "echo \"\$negative_tag\" > $purr_query_cache;" 52 | 'else;' 53 | "echo \"\$cur_query \$negative_tag\" > $purr_query_cache;" 54 | 'fi;' 55 | ')+transform-query(' 56 | "cat $purr_query_cache;" 57 | "echo "" > $purr_query_cache;" 58 | ')' 59 | ) 60 | bind_commands+=('--bind' "ctrl-alt-d:$remove_tag_cmd") 61 | rebind_in_default_command_suite "ctrl-alt-d" 62 | unbind_in_adb_command_suite "ctrl-alt-d" 63 | unbind_in_history_command_suite "ctrl-alt-d" 64 | unbind_in_serial_command_suite "ctrl-alt-d" 65 | -------------------------------------------------------------------------------- /src/bindings/copy_bindings.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Bind: ctrl-y, print to clipboard. 18 | cmd_clipboard=( 19 | "execute-silent(" 20 | "lines=\"{+}\";" 21 | 'eval "line_array=($lines)";' 22 | 'new_line_array=$(printf "%s\n" "${line_array[@]}");' 23 | "echo \"\$new_line_array[@]\" | $purr_copy_program > $target_tty;" 24 | ')+clear-selection' 25 | ) 26 | bind_commands+=('--bind' "ctrl-y:$cmd_clipboard") 27 | rebind_in_default_command_suite "ctrl-y" 28 | rebind_in_adb_command_suite "ctrl-y" 29 | unbind_in_history_command_suite "ctrl-y" 30 | unbind_in_serial_command_suite "ctrl-y" 31 | 32 | # Bind: ctrl-\, prints device information to the clipboard. 33 | cmd_bug_report=( 34 | 'execute-silent(' 35 | $update_serial_cmd 36 | 'info_array=("\`\`\`");' 37 | 'info_array+=("\nFingerprint:" $(' "$adb_cmd_loc" ' -s $serial shell getprop | grep "ro.build.fingerprint"));' 38 | 'info_array+=("\nSDK Version:" $(' "$adb_cmd_loc" ' -s $serial shell getprop | grep "ro.build.version.sdk"));' 39 | 'info_array+=("\nGMS Version:" $(' "$adb_cmd_loc" ' -s $serial shell dumpsys package com.google.android.gms | grep "versionName" | head -n 1));' 40 | 'info_array+=("\n\`\`\`");' 41 | "echo \"\$info_array\" | $purr_copy_program > $target_tty;" 42 | '{' 43 | 'bug_report_name="/tmp/bugreport-$(' "$adb_cmd_loc" ' -s $serial shell getprop ro.product.vendor.name)-$(' "$adb_cmd_loc" ' -s $serial shell getprop ro.product.vendor.device)-$(' "$adb_cmd_loc" ' -s $serial shell getprop ro.vendor.build.version.sdk)-$(date +"%d-%m-%Y::%H:%M:%S")";' 44 | 'bug_report_error=$(' "$adb_cmd_loc" ' -s $serial bugreport $bug_report_name);' 45 | 'if [ ! -f "${bug_report_name}.zip" ]; then ' 46 | 'echo $bug_report_error > "${bug_report_name}_err";' 47 | 'fi;' 48 | '} &' 49 | ')+clear-selection' 50 | ) 51 | bind_commands+=('--bind' "ctrl-\:$cmd_bug_report") 52 | rebind_in_default_command_suite "ctrl-\\" 53 | rebind_in_adb_command_suite "ctrl-\\" 54 | rebind_in_history_command_suite "ctrl-\\" 55 | rebind_in_serial_command_suite "ctrl-\\" 56 | -------------------------------------------------------------------------------- /src/execution_loop.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Creates background workers to stream the logcat input streams for us. 18 | (__purr_background_handler &) 19 | 20 | # Some actions in purr exit fzf, but shouldn't exit purr, so we need an execution loop. 21 | while true; do 22 | 23 | # Grabs the last query from the user before fzf exited. 24 | cached_query="$(cat $purr_query_cache)" 25 | "" >$purr_query_cache &>/dev/null 26 | 27 | __purr_update_serial 28 | __purr_update_prompt 29 | 30 | __purr_set_start_command 31 | 32 | __purr_set_start_preview 33 | 34 | if [[ "$fzf_exec_flag" = "false" ]]; then # Don't exec fzf if in a testing session. 35 | sleep 5 36 | accepted="" 37 | ret=$? 38 | elif [ ! -z $cached_query ]; then # Starts and runs the actual fzf process. 39 | accepted=$(FZF_DEFAULT_COMMAND="$load_input_stream" fzf $starter_preview_command $fzfp $fzf_prompt $bind_commands $start_command --query=$cached_query) 40 | ret=$? 41 | else 42 | accepted=$(FZF_DEFAULT_COMMAND="$load_input_stream" fzf $starter_preview_command $fzfp $fzf_prompt $bind_commands $start_command) 43 | ret=$? 44 | fi 45 | 46 | # fzf returns 0 when it normally exits, and returns 1 when it normally exists but does 47 | # not return an "accepted" string. These are the only codes on which we want break. 48 | if [ "$ret" -ne 0 ] && [ "$ret" -ne 1 ]; then 49 | break 50 | fi 51 | 52 | cached_query="" 53 | accept_cmd="" 54 | 55 | # The async processes might take a bit of time to process the accept command. 56 | purr_timeout 1 "wait_for_file $purr_accept_command_cache" 57 | 58 | # We'll use this to figure out the user input before fzf stopped. 59 | accept_cmd=$(cat $purr_accept_command_cache) 60 | "" >$purr_accept_command_cache &>/dev/null 61 | 62 | if [ "$accept_cmd" = "wipe" ] || [ "$accept_cmd" = "serial" ] || [ "$accept_cmd" = "trim" ]; then 63 | __wait_for_input_streams 64 | elif [ "$accept_cmd" = "adb_cmd" ] || [ "$accept_cmd" = "history" ]; then 65 | : # Left blank for clarity. 66 | elif [ "$accept_cmd" = "editor" ]; then 67 | __purr_start_editor 68 | else 69 | break 70 | fi 71 | done 72 | 73 | __purr_cleanup $dir_name 74 | -------------------------------------------------------------------------------- /src/fzf_env/fzf_default_state.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Load the instructions into the instruction cache. 18 | __purr_print_instructions $purr_instruction_cache 19 | 20 | # Set the default program state. 21 | echo "\x1b[1;32m $serial => \x1b[1;0m" >$purr_connection_state_cache 22 | echo $stream_verbose_msg >|$purr_stream_header_cache 23 | echo $slock_off_msg >|$purr_slock_cache 24 | echo $unique_off_msg >|$purr_unique_cache 25 | echo $sorting_chronological >|$purr_sort_header_cache 26 | echo "tail -F -n 99999999 $purr_verbose_input_cache $teecmd" >|$purr_input_stream_cache 27 | echo -1 >|$purr_history_pointer_cache 28 | echo 0 >|$purr_history_counter_cache 29 | 30 | bind_commands=() 31 | 32 | cat <<- "END" > $purr_spc_purpose_cache 33 | _ ) 34 | ( \_ | 35 | _,-'/'_ , ;-. \ 36 | . ,-' O ( ` .<= `. \ 37 | `. ,'o O o 0 o , ,'-. ,-' 38 | `. `. ,'o O 0/ 0 )___,.--','"""`--._,,--' 39 | __>. \ ,: ' o O/. o/ __,`--'-. _,-' 40 | -._ `-``._/o 0 0(),| o/-'' `-.__ __,-'| 41 | )_. ` / ~':o ,-.\o \ `----' | 42 | `-.-| 0 o.`.\ /o|`.| | 43 | /o . ;.'`o|'|o \ / 44 | |`,,' `._/0|\',\ / 45 | |`.|`._/ \o'||o | | 46 | /,`/ )./|.'| / 47 | ---------|.|--------\,\`--'-------------------------\ 48 | |O| \``. | 49 | |o| `._) \ 50 | |(| \ 51 | |O| | 52 | hh |)| | 53 | /(| | 54 | (o/ | 55 | \) / 56 | 57 | Cats who can purr can't roar, and vice-versa. 58 | Cheetahs can't roar, but they can meow and purr. 59 | This is because cheetahs are the cutest kittens at heart. 60 | This has been Cat Facts with Alfred. 61 | END 62 | -------------------------------------------------------------------------------- /src/bindings/mode_bindings.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Bind: ctrl-u, toggle unique mode. 18 | unique_cmd=( 19 | 'execute-silent(' 20 | "if $is_unique_on; then" 21 | $set_unique_off 22 | "if grep -q 'verbose-unique' $purr_input_stream_cache; then" 23 | $set_stream_verbose 24 | "elif grep -q 'info-unique' $purr_input_stream_cache; then" 25 | $set_stream_info 26 | "elif grep -q 'warning-unique' $purr_input_stream_cache; then" 27 | $set_stream_warning 28 | "elif grep -q 'error-unique' $purr_input_stream_cache; then" 29 | $set_stream_error 30 | "fi;" 31 | "else;" 32 | $set_unique_on 33 | "if grep -q 'verbose' $purr_input_stream_cache; then" 34 | $set_stream_verbose_unique 35 | "elif grep -q 'info' $purr_input_stream_cache; then" 36 | $set_stream_info_unique 37 | "elif grep -q 'warning' $purr_input_stream_cache; then" 38 | $set_stream_warning_unique 39 | "elif grep -q 'error' $purr_input_stream_cache; then" 40 | $set_stream_error_unique 41 | "fi;" 42 | "fi;" 43 | ')+reload(' 44 | $inject_empty_line 45 | $load_input_stream 46 | ")+transform-header(" 47 | $load_generic_header 48 | ")+first+enable-search+$default_command_suite" 49 | ) 50 | bind_commands+=('--bind' "ctrl-u:$unique_cmd") 51 | rebind_in_default_command_suite "ctrl-u" 52 | unbind_in_adb_command_suite "ctrl-u" 53 | unbind_in_history_command_suite "ctrl-u" 54 | unbind_in_serial_command_suite "ctrl-u" 55 | 56 | # Bind: ctrl-s, toggle scroll lock. 57 | stop_cmd=( 58 | 'execute-silent(' 59 | '{' 60 | "if $is_slock_on; then" 61 | $set_slock_off 62 | "else" 63 | $set_slock_on 64 | "fi" 65 | '} &' 66 | ')+toggle-track+transform-header(' 67 | $load_generic_header 68 | ")+$default_command_suite" 69 | ) 70 | bind_commands+=('--bind' "ctrl-s:$stop_cmd") 71 | rebind_in_default_command_suite "ctrl-s" 72 | rebind_in_adb_command_suite "ctrl-s" 73 | unbind_in_history_command_suite "ctrl-s" 74 | unbind_in_serial_command_suite "ctrl-s" 75 | 76 | # Bind: ctrl-j, toggle chronological/relevance sort. 77 | cmd_sort=( 78 | "toggle-sort+execute-silent(" 79 | '{' 80 | "if $is_sort_chrono; then" 81 | $set_sort_relevance 82 | "else" 83 | $set_sort_chrono 84 | "fi" 85 | '} &' 86 | ")+transform-header(" 87 | $load_generic_header 88 | ")+$default_command_suite" 89 | ) 90 | bind_commands+=('--bind' "ctrl-j:$cmd_sort") 91 | rebind_in_default_command_suite "ctrl-j" 92 | rebind_in_adb_command_suite "ctrl-j" 93 | rebind_in_history_command_suite "ctrl-j" 94 | rebind_in_serial_command_suite "ctrl-j" 95 | -------------------------------------------------------------------------------- /tests/validators/file_validator.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | validate_runtime_purr_files() { 18 | dir_name=$1 19 | 20 | # Did we load the serial? 21 | grep -q -- "emulator-5554" $dir_name/serial-cache.purr 22 | grep -q -- "emulator-5554" $dir_name/connection-state.purr 23 | echo "Serial was loaded." 24 | 25 | # Did the input stream load to verbose? 26 | grep -q -- "verbose-input-cache.purr" $dir_name/input-stream.purr 27 | grep -q -- "Verbose" $dir_name/stream-header.purr 28 | echo "Input stream is verbose." 29 | 30 | # Did we start on the instruction preview? And it is visible? 31 | grep -q -- "instruction" $dir_name/preview-command-cache.purr 32 | grep -q -- "nohidden" $dir_name/preview-visibility-cache.purr 33 | echo "Preview set to instruction." 34 | 35 | # Did we start with our modes in the correct states? 36 | grep -q -- "Off" $dir_name/purr_unique_cache.purr 37 | grep -q -- "Off" $dir_name/scroll-lock-header.purr 38 | grep -q -- "Chronological" $dir_name/sort-header.purr 39 | echo "Modes set to correct starting states." 40 | 41 | # Did purr correctly pick up the adb output? 42 | verbose_cache_contents=$(cat $dir_name/verbose-input-cache.purr) 43 | if [[ "$verbose_cache_contents" = "$mocked_adb_output" ]]; then 44 | echo "Verbose cache is correct." 45 | else 46 | return 1 47 | fi 48 | } 49 | 50 | validate_exit_time_purr_files() { 51 | dir_name=$1 52 | 53 | # Did the handler actually do clean up? 54 | if [ ! -f $dir_name/verbose-input-cache.purr ]; then 55 | echo "Threads did actually cleanup." 56 | else 57 | return 1 58 | fi 59 | } 60 | 61 | USAGE=("purr_file_validator" 62 | "-a: Set the ADB binary path; likely the bundled adb_mock." 63 | "-p: Set the purr binary path.") 64 | 65 | while getopts ':p:a:' flags; do 66 | case $flags in 67 | a) adb_mock_path=${OPTARG} ;; 68 | p) purr_binary_path=${OPTARG} ;; 69 | *) echo $USAGE ;; 70 | esac 71 | done 72 | 73 | if [ -z $purr_binary_path ]; then 74 | echo >&2 "Please provide the path to the purr binary through -p." 75 | exit 1 76 | elif [ -z $adb_mock_path ]; then 77 | echo >&2 "Please provide the path to the adb mock binary through -a." 78 | exit 1 79 | fi 80 | 81 | # We need to specify this so we know where purr is going to put 82 | # the files. 83 | dir_name=$(mktemp -d /tmp/purr.XXXXXXXXXX) 84 | 85 | # Fail on any error. 86 | set -e 87 | 88 | # Run purr in the background with the given ADB binary. 89 | # -X makes sure we don't actually launch fzf and only do file validation. 90 | # When in -X mode, purr sleeps for 5 seconds after reaching fzf. 91 | { 92 | eval "$purr_binary_path -A $adb_mock_path -D $dir_name -X" 93 | } & 94 | 95 | # Wait for purr to start. 96 | sleep 1 97 | 98 | # Check that purr is OK during runtime. 99 | validate_runtime_purr_files $dir_name 100 | 101 | # Wait for purr to exit. 102 | sleep 5 103 | 104 | # Check that purr exited OK. 105 | validate_exit_time_purr_files $dir_name 106 | 107 | rm -r $dir_name 108 | 109 | echo "Files looks valid!" 110 | -------------------------------------------------------------------------------- /src/bindings/history_bindings.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # This code is a bit arcane, but here's basically what it's doing. 18 | # When the query changes, we write an integer and start a timer. 19 | # Once the timer ends, we check the integer; if it's not the same, 20 | # we don't write to history. If it is, we check if we can find the 21 | # query in the history file. We'll then either write it or move it 22 | # to the top. This holistically allows us to write to history even 23 | # though queries are never "submitted". 24 | history_input=( 25 | 'execute-silent(' 26 | '{' 27 | "seen_counter=\$(cat $purr_history_counter_cache);" 28 | '$(( seen_counter += 1 ));' 29 | "echo \$seen_counter >| $purr_history_counter_cache;" 30 | 'sleep 3.5;' 31 | "cur_counter=\$(cat $purr_history_counter_cache);" 32 | 'if [ $seen_counter -eq $cur_counter ]; then' 33 | 'query={q};' 34 | "query=\$(echo \"\$query\" | xargs | tr -s ' ');" 35 | 'if [ -z "$query" ]; then' 36 | ':;' 37 | "elif grep -cim1 -x \" *\$query *\" $purr_history_cache; then" 38 | "line=\$(grep -n -x \" *\$query *\" $purr_history_cache | cut -d : -f 1);" 39 | "sed -i \"\${line}d\" $purr_history_cache;" 40 | "echo \$query >> $purr_history_cache;" 41 | "echo 0 >| $purr_history_pointer_cache;" 42 | 'else;' 43 | "echo \$query >> $purr_history_cache;" 44 | "echo 0 >| $purr_history_pointer_cache;" 45 | 'fi;' 46 | 'fi;' 47 | '} &' 48 | ')' 49 | ) 50 | bind_commands+=('--bind' "change:$history_input") 51 | 52 | # This just allows the user to use alt-shift-up/down to traverse 53 | # history. We just keep track in the file of where they are. We 54 | # wipe this pointer wheneever we add a new history entry. 55 | history_up=( 56 | 'transform-query(' 57 | "line_count=\$(wc -l < $purr_history_cache);" 58 | "cur_pointer=\$(cat $purr_history_pointer_cache);" 59 | '$(( cur_pointer += 1 ));' 60 | 'if [ $cur_pointer -lt $line_count ]; then' 61 | "echo \$cur_pointer >| $purr_history_pointer_cache;" 62 | 'else;' 63 | '$(( cur_pointer -= 1));' 64 | 'fi;' 65 | 'line_to_get="$((line_count - cur_pointer))";' 66 | "sed -n -e \${line_to_get}p $purr_history_cache;" 67 | ')' 68 | ) 69 | bind_commands+=('--bind' "alt-shift-up:$history_up") 70 | rebind_in_default_command_suite "alt-shift-up" 71 | rebind_in_adb_command_suite "alt-shift-up" 72 | rebind_in_history_command_suite "alt-shift-up" 73 | rebind_in_serial_command_suite "alt-shift-up" 74 | 75 | # See above. 76 | history_down=( 77 | 'transform-query(' 78 | "line_count=\$(wc -l < $purr_history_cache);" 79 | "cur_pointer=\$(cat $purr_history_pointer_cache);" 80 | '$(( cur_pointer -= 1 ));' 81 | 'if [ $cur_pointer -ge -1 ]; then' 82 | "echo \$cur_pointer >| $purr_history_pointer_cache;" 83 | 'else;' 84 | '$(( cur_pointer += 1));' 85 | 'fi;' 86 | 'line_to_get="$((line_count - cur_pointer))";' 87 | "sed -n -e \${line_to_get}p $purr_history_cache;" 88 | ')' 89 | ) 90 | bind_commands+=('--bind' "alt-shift-down:$history_down") 91 | rebind_in_default_command_suite "alt-shift-down" 92 | rebind_in_adb_command_suite "alt-shift-down" 93 | rebind_in_history_command_suite "alt-shift-down" 94 | rebind_in_serial_command_suite "alt-shift-down" 95 | -------------------------------------------------------------------------------- /src/serial/serial_picker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Queries the user to choose between all connected adb devices. 18 | # Output: Serial number of the chosen adb device, to be used in adb -s $(pick_serial). 19 | pick_serial() { 20 | 21 | # Checks if we want to return just the $ANDROID_SERIAL serial. 22 | if [ ! -z $ANDROID_SERIAL ]; then 23 | __pick_serial_wait $ANDROID_SERIAL 24 | echo $ANDROID_SERIAL 25 | return 26 | fi 27 | 28 | # Check if any ADB devices are found. 29 | local adb_devices="" 30 | local device_count="" 31 | while [ -z $adb_devices ]; do 32 | __pick_serial_wait 33 | 34 | adb_devices=$(eval "$adb_cmd_loc devices | tail -n +2 | sed '/^\s*$/d' | sort") 35 | device_count=$(echo $adb_devices | wc -l) 36 | 37 | # Checks how many devices are in a non-connected state. 38 | local disconnected_devices=$(grep ".*offline.*" <<<$adb_devices | sort) 39 | local disconnected_count=$(grep -c ".*offline.*" <<<$adb_devices | sort) 40 | 41 | # Checks whether there are any devices left that can be connected to. 42 | if [ $disconnected_count -eq $device_count ]; then 43 | echo >&2 "All available devices are disconnected. Waiting..." 44 | sleep 2 45 | __pick_serial_wait 46 | 47 | adb_devices="" 48 | device_count="" 49 | elif [ $disconnected_count -ne 0 ]; then 50 | echo >&2 "Skipped offline devices:" 51 | echo >&2 $disconnected_devices 52 | adb_devices=$(grep -v ".*offline.*" <<<$adb_devices | sort) 53 | fi 54 | done 55 | 56 | # Check if only one device is found. If it is, we'll just use it. 57 | if [ $device_count -eq 1 ]; then 58 | local stripped_device=$(echo "$adb_devices" | xargs) 59 | else 60 | if command -v rg &>/dev/null; then 61 | local stripped_device=$(FZF_DEFAULT_COMMAND="echo \"$adb_devices\"" fzf $fzfpnh "--height=25%" --preview-window=right,50%,wrap \ 62 | "--preview=$adb_cmd_loc -s \$(cut -f1 <<< {}) shell getprop | rg '(ro.bootimage.build.date]|ro.product.name|ro.bootimage.build.version.incremental])' | awk -F ']:' '{print \$2}' | sed 's/]//g' | sed 's/\[//g' " | xargs) 63 | else 64 | local stripped_device=$(FZF_DEFAULT_COMMAND="echo \"$adb_devices\"" fzf $fzfpnh "--height=25%" | xargs) 65 | fi 66 | fi 67 | 68 | # If the user exits fzf without picking. 69 | if [ -z $stripped_device ]; then 70 | echo >&2 "No serial number selected." 71 | exit 18 72 | fi 73 | 74 | # Cuts the serial down to just the number. 75 | echo "$(cut -d' ' -f1 <<<$stripped_device)" # Grab the serial from selection. 76 | } 77 | 78 | # If we know which serial to look for, or we can't see any, we need to wait. ADB device connections 79 | # can be fickle, and ADB sometimes reports devices before it really should. 80 | __pick_serial_wait() { 81 | if [ ! -z $1 ]; then 82 | local serial_stmt="-s $1" 83 | fi 84 | 85 | # See if any devices are immediately connected before printing a connect message. 86 | purr_timeout 1 "$adb_cmd_loc $serial_stmt wait-for-device" 2>/dev/null 87 | ret=$? 88 | 89 | # If we can't find any immediate connections, go into a longer waiting period. 90 | if [ $ret -eq 124 ] || [ $ret -eq 142 ]; then 91 | echo >&2 "Waiting on a device to connect..." 92 | 93 | eval "$adb_cmd_loc $serial_stmt wait-for-device" 94 | 95 | echo >&2 "Device connected." 96 | fi 97 | } 98 | -------------------------------------------------------------------------------- /src/ui/ui_utils.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | __purr_set_start_preview() { 18 | prev_preview=$(cat $purr_preview_command_cache) 19 | prev_preview_vis=$(cat $purr_preview_visible_cache) 20 | 21 | if [ "$prev_preview" = "instruction" ]; then 22 | if [ "$prev_preview_vis" = "hidden" ]; then 23 | starter_preview_command=($instruction_preview_starter_hidden) 24 | elif [ "$prev_preview_vis" = "nohidden" ]; then 25 | starter_preview_command=($instruction_preview_starter) 26 | fi 27 | elif [ "$prev_preview" = "verbose" ]; then 28 | if [ "$prev_preview_vis" = "hidden" ]; then 29 | starter_preview_command=($verbose_hint_preview_starter_hidden) 30 | elif [ "$prev_preview_vis" = "nohidden" ]; then 31 | starter_preview_command=($verbose_hint_preview_starter) 32 | fi 33 | elif [ "$prev_preview" = "current" ]; then 34 | if [ "$prev_preview_vis" = "hidden" ]; then 35 | starter_preview_command=($current_hint_preview_starter_hidden) 36 | elif [ "$prev_preview_vis" = "nohidden" ]; then 37 | starter_preview_command=($current_hint_preview_starter) 38 | fi 39 | elif [ $instruction_flag = "true" ]; then 40 | starter_preview_command=($instruction_preview_starter) 41 | echo "instruction" >$purr_preview_command_cache 42 | echo "nohidden" >$purr_preview_visible_cache 43 | else 44 | starter_preview_command=($verbose_hint_preview_starter_hidden) 45 | echo "verbose" >$purr_preview_command_cache 46 | echo "hidden" >$purr_preview_visible_cache 47 | fi 48 | } 49 | 50 | __purr_set_start_command() { 51 | if grep -q "History" $purr_stream_header_cache; then 52 | start_command=('--bind' "start:hide-preview+transform-header($load_generic_header)+$history_command_suite") 53 | elif grep -q "ADB" $purr_stream_header_cache; then 54 | start_command=('--bind' "start:hide-preview+transform-header($load_generic_header)+$adb_command_suite") 55 | 56 | # Enables the cat facts easter egg. 57 | if [ "$cached_query" = "Give me cat facts!" ]; then 58 | adb_query_cmd="cat $purr_spc_purpose_cache" 59 | else 60 | adb_query_cmd="$adb_cmd_loc -s $serial shell $cached_query" 61 | fi 62 | 63 | cached_query="" 64 | 65 | echo $adb_query_cmd >$purr_input_stream_cache 66 | else 67 | start_command=('--bind' "start:transform-header($load_generic_header)+$default_command_suite") 68 | fi 69 | } 70 | 71 | __purr_start_editor() { 72 | 73 | # If there's nothing to edit, we just move on. 74 | if [ -z $accepted ]; then 75 | continue 76 | fi 77 | 78 | # Grab the context around the accepted line. 79 | if [ $(echo $accepted | wc -l) -eq 1 ]; then 80 | if command -v rg &>/dev/null; then 81 | rg --color=always -F "$accepted" $purr_input_cache -C 500 >$purr_editor_input_cache 82 | else 83 | grep --color=always -F "$accepted" $purr_input_cache -C 500 >$purr_editor_input_cache 84 | fi 85 | else 86 | echo $accepted >$purr_editor_input_cache 87 | fi 88 | 89 | # Preferentially grab the editor from $EDITOR_PURR, then $EDITOR, then just use vim. 90 | if [ $EDITOR_PURR ]; then 91 | eval "$EDITOR_PURR $purr_editor_input_cache" 92 | elif [ $EDITOR ]; then 93 | eval "$EDITOR $purr_editor_input_cache" 94 | else 95 | echo "No editor detected. Overriding to vim." 96 | echo "Purr will read from \$EDITOR_PURR, then \$EDITOR, then default to vim." 97 | vim +501 $purr_editor_input_cache 98 | fi 99 | } 100 | 101 | __purr_update_prompt() { 102 | fzf_prompt=('--prompt' " $serial -> ") 103 | } 104 | -------------------------------------------------------------------------------- /src/bindings/misc_bindings.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Bind: Ctrl-h, ask for help in ADB command mode. 18 | adb_help_cmd=( 19 | 'execute-silent(' 20 | '{' 21 | $set_stream_adb 22 | $set_header_adb 23 | $set_slock_off 24 | '} &' 25 | $update_serial_cmd 26 | 'query={q};' 27 | 'for string in "--help" "-h" "help"; do' 28 | 'adb_help=$( { eval' "$adb_cmd_loc" '-s $serial shell $query $string" } 2>&1 );' 29 | 'ret=$?;' 30 | # We are trying best-guesses on what a help prompt might look like here. There isn't 31 | # a standard for shell commands, so we're going to try the most common ones, and we 32 | # assume that if a request returns a bunch of lines, maybe it's just dumping help. 33 | 'if { [ $ret -eq 0 ] && [ ! -s $adb_help ] } || [ $(wc -l <<< $adb_help) -ge 15 ]; then' 34 | "echo \"Ran \\\"adb -s \$serial shell \$query \$string\\\"\" > $purr_adb_cache;" 35 | "echo \$adb_help >> $purr_adb_cache;" 36 | "exit;" 37 | 'fi;' 38 | 'done;' 39 | "echo \"Could not find help for \\\"\$query\\\"\" >> $purr_adb_cache;" 40 | ')+reload(' 41 | $load_input_stream 42 | ')+disable-search' 43 | ) 44 | bind_commands+=('--bind' "ctrl-h:$adb_help_cmd") 45 | unbind_in_default_command_suite "ctrl-h" 46 | rebind_in_adb_command_suite "ctrl-h" 47 | unbind_in_history_command_suite "ctrl-h" 48 | unbind_in_serial_command_suite "ctrl-h" 49 | 50 | # We overload the hell of this command... 51 | # Bind: enter, select history command or serial selection. 52 | enter_cmd=( 53 | 'transform-query(' 54 | "if grep -q \"History\" $purr_stream_header_cache; then" 55 | 'echo {};' 56 | 'else;' 57 | 'echo {q};' 58 | 'fi;' 59 | ')+execute-silent(' 60 | "if grep -q \"History\" $purr_stream_header_cache; then" 61 | "echo 'history' > $purr_accept_command_cache;" 62 | $set_stream_verbose 63 | $set_header_verbose 64 | $set_slock_off 65 | $save_current_query 66 | "elif grep -q \"Serial\" $purr_stream_header_cache; then" 67 | $stop_stream 68 | "accepted=\$(echo {});" 69 | 'if [ ! -z $accepted ]; then' 70 | "echo {} > $purr_serial_cache;" 71 | 'fi;' 72 | "echo 'serial' > $purr_accept_command_cache;" 73 | $set_stream_verbose 74 | $set_header_verbose 75 | $set_slock_off 76 | $save_current_query 77 | $start_stream 78 | "elif grep -q \"ADB\" $purr_stream_header_cache; then" 79 | $set_slock_off 80 | "echo 'adb_cmd' > $purr_accept_command_cache;" 81 | $save_current_query 82 | "fi;" 83 | ')+accept' 84 | ) 85 | bind_commands+=('--bind' "enter:$enter_cmd") 86 | unbind_in_default_command_suite "enter" 87 | rebind_in_adb_command_suite "enter" 88 | rebind_in_history_command_suite "enter" 89 | rebind_in_serial_command_suite "enter" 90 | 91 | # Adds logic to handle the prompt change to track serial numbers. 92 | focus_cmd=( 93 | 'transform-prompt(' 94 | " connection_state=\$(cat $purr_connection_state_cache);" 95 | 'echo $connection_state;' 96 | ')' 97 | ) 98 | bind_commands+=('--bind' "focus:$focus_cmd") 99 | 100 | # Command to exit purr. 101 | esc_cmd=( 102 | 'execute-silent(' 103 | "echo 'escape' > $purr_accept_command_cache;" 104 | ')+accept' 105 | ) 106 | bind_commands+=('--bind' "esc:$esc_cmd") 107 | rebind_in_default_command_suite "esc" 108 | rebind_in_adb_command_suite "esc" 109 | rebind_in_history_command_suite "esc" 110 | rebind_in_serial_command_suite "esc" 111 | 112 | # Bind: ctrl-v, for going to the text editor. 113 | cmd_editor=( 114 | "execute-silent(" 115 | "accepted=\$(echo {});" 116 | 'if [ ! -z $accepted ]; then' 117 | "echo 'editor' > $purr_accept_command_cache;" 118 | $save_current_query 119 | 'fi;' 120 | ')+accept-non-empty' 121 | ) 122 | bind_commands+=('--bind' "ctrl-v:$cmd_editor") 123 | rebind_in_default_command_suite "ctrl-v" 124 | rebind_in_adb_command_suite "ctrl-v" 125 | unbind_in_history_command_suite "ctrl-v" 126 | unbind_in_serial_command_suite "ctrl-v" 127 | -------------------------------------------------------------------------------- /docs/code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, education, socio-economic status, nationality, personal appearance, 10 | race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | This Code of Conduct also applies outside the project spaces when the Project 56 | Steward has a reasonable belief that an individual's behavior may have a 57 | negative impact on the project or its community. 58 | 59 | ## Conflict Resolution 60 | 61 | We do not believe that all conflict is bad; healthy debate and disagreement 62 | often yield positive results. However, it is never okay to be disrespectful or 63 | to engage in behavior that violates the project’s code of conduct. 64 | 65 | If you see someone violating the code of conduct, you are encouraged to address 66 | the behavior directly with those involved. Many issues can be resolved quickly 67 | and easily, and this gives people more control over the outcome of their 68 | dispute. If you are unable to resolve the matter for any reason, or if the 69 | behavior is threatening or harassing, report it. We are dedicated to providing 70 | an environment where participants feel welcome and safe. 71 | 72 | Reports should be directed to *[PROJECT STEWARD NAME(s) AND EMAIL(s)]*, the 73 | Project Steward(s) for *[PROJECT NAME]*. It is the Project Steward’s duty to 74 | receive and address reported violations of the code of conduct. They will then 75 | work with a committee consisting of representatives from the Open Source 76 | Programs Office and the Google Open Source Strategy team. If for any reason you 77 | are uncomfortable reaching out to the Project Steward, please email 78 | opensource@google.com. 79 | 80 | We will investigate every complaint, but you may not receive a direct response. 81 | We will use our discretion in determining when and how to follow up on reported 82 | incidents, which may range from not taking action to permanent expulsion from 83 | the project and project-sponsored spaces. We will notify the accused of the 84 | report and provide them an opportunity to discuss it before any action is taken. 85 | The identity of the reporter will be omitted from the details of the report 86 | supplied to the accused. In potentially harmful situations, such as ongoing 87 | harassment or threats to anyone's safety, we may take action without notice. 88 | 89 | ## Attribution 90 | 91 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 92 | available at 93 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 94 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | OUTDIR?=$(CURDIR)/out 2 | OBJDIR?=$(CURDIR)/obj 3 | SRCDIR?=$(CURDIR)/src 4 | RESDIR?=$(CURDIR)/res 5 | TESTDIR?=$(CURDIR)/tests 6 | 7 | PURRFILE ?=$(OUTDIR)/purr 8 | PURRFILE_TEMP ?=$(OBJDIR)/purr 9 | 10 | ADBMOCKFILE ?=$(OUTDIR)/adb_mock 11 | ADBMOCKFILE_TEMP ?=$(OBJDIR)/adb_mock 12 | 13 | FILETESTERFILE ?=$(OUTDIR)/file_tester 14 | FILETESTERFILE_TEMP ?=$(OBJDIR)/file_tester 15 | 16 | all: purr adb_mock file_tester 17 | 18 | .PHONY: purr 19 | 20 | purr: 21 | mkdir -p $(OUTDIR) 22 | mkdir -p $(OBJDIR) 23 | 24 | echo "" > $(PURRFILE) 25 | echo "" > $(PURRFILE_TEMP) 26 | 27 | # We first need the threading functions, since we use them as part of shell 28 | # env setup, specifically to create the cache files. 29 | cat $(SRCDIR)/threads/thread_background.sh >> "$(PURRFILE_TEMP)" 30 | cat $(SRCDIR)/threads/thread_controller.sh >> "$(PURRFILE_TEMP)" 31 | cat $(SRCDIR)/threads/thread_interface.sh >> "$(PURRFILE_TEMP)" 32 | cat $(SRCDIR)/threads/thread_setup.sh >> "$(PURRFILE_TEMP)" 33 | 34 | # We can then cleanly do shell setup. 35 | cat $(SRCDIR)/shell/shell_utils.sh >> "$(PURRFILE_TEMP)" 36 | cat $(SRCDIR)/shell/argument_parsing.sh >> "$(PURRFILE_TEMP)" 37 | cat $(SRCDIR)/shell/shell_env_check.sh >> "$(PURRFILE_TEMP)" 38 | cat $(SRCDIR)/shell/shell_env_init.sh >> "$(PURRFILE_TEMP)" 39 | 40 | # After we've validated the shell, we want to query for a serial number. 41 | cat $(SRCDIR)/serial/serial_picker.sh >> "$(PURRFILE_TEMP)" 42 | cat $(SRCDIR)/serial/serial_interface.sh >> "$(PURRFILE_TEMP)" 43 | cat $(SRCDIR)/serial/serial_init.sh >> "$(PURRFILE_TEMP)" 44 | 45 | # Load in all the UI utilities. We'll need these for setting up the fzf 46 | # environment in the next step. 47 | cat $(SRCDIR)/ui/ui_strings.sh >> "$(PURRFILE_TEMP)" 48 | cat $(SRCDIR)/ui/ui_utils.sh >> "$(PURRFILE_TEMP)" 49 | 50 | # Finalizes the fzf environment; program is in a ready state at this point. 51 | # We just need to load in all the UI/UX elements from bindings. 52 | cat $(SRCDIR)/fzf_env/load_copy_program.sh >> "$(PURRFILE_TEMP)" 53 | cat $(SRCDIR)/fzf_env/fzf_default_state.sh >> "$(PURRFILE_TEMP)" 54 | 55 | # Load the binding libraries; this is the main block of code for purr. 56 | # Order here isn't super important, but the stream bindings need to be 57 | # at the very bottom after the command suites are fully initialized. 58 | cat $(SRCDIR)/bindings/command_suites.sh >> "$(PURRFILE_TEMP)" 59 | cat $(SRCDIR)/bindings/copy_bindings.sh >> "$(PURRFILE_TEMP)" 60 | cat $(SRCDIR)/bindings/history_bindings.sh >> "$(PURRFILE_TEMP)" 61 | cat $(SRCDIR)/bindings/input_trim_bindings.sh >> "$(PURRFILE_TEMP)" 62 | cat $(SRCDIR)/bindings/misc_bindings.sh >> "$(PURRFILE_TEMP)" 63 | cat $(SRCDIR)/bindings/mode_bindings.sh >> "$(PURRFILE_TEMP)" 64 | cat $(SRCDIR)/bindings/navigation_bindings.sh >> "$(PURRFILE_TEMP)" 65 | cat $(SRCDIR)/bindings/preview_bindings.sh >> "$(PURRFILE_TEMP)" 66 | cat $(SRCDIR)/bindings/query_bindings.sh >> "$(PURRFILE_TEMP)" 67 | cat $(SRCDIR)/bindings/stream_bindings.sh >> "$(PURRFILE_TEMP)" 68 | 69 | # Load the execution loop that actually makes things happen. 70 | cat $(SRCDIR)/execution_loop.sh >> "$(PURRFILE_TEMP)" 71 | 72 | # Remove comments/shebangs. 73 | sed -e '/^[ \t]*#/d' "$(PURRFILE_TEMP)" > $(PURRFILE) 74 | mv $(PURRFILE) "$(PURRFILE_TEMP)" 75 | 76 | # Add one shebang and the copyright notice. 77 | # We need a temp file since cat can't do it in-place. 78 | cat $(RESDIR)/header/purr_header.txt $(PURRFILE_TEMP) > $(PURRFILE) 79 | 80 | # Grant execution permission. 81 | chmod +rwx $(PURRFILE) 82 | 83 | .PHONY: adb_mock 84 | 85 | adb_mock: 86 | mkdir -p $(OUTDIR) 87 | mkdir -p $(OBJDIR) 88 | 89 | echo "" > $(ADBMOCKFILE) 90 | echo "" > $(ADBMOCKFILE_TEMP) 91 | 92 | cat $(TESTDIR)/res/adb_log_example.sh >> "$(ADBMOCKFILE_TEMP)" 93 | cat $(TESTDIR)/mocks/adb_mock.sh >> "$(ADBMOCKFILE_TEMP)" 94 | 95 | # Remove comments/shebangs. 96 | sed -e '/^[ \t]*#/d' "$(ADBMOCKFILE_TEMP)" > $(ADBMOCKFILE) 97 | mv $(ADBMOCKFILE) "$(ADBMOCKFILE_TEMP)" 98 | 99 | # Add one shebang and the copyright notice. 100 | # We need a temp file since cat can't do it in-place. 101 | cat $(RESDIR)/header/purr_header.txt $(ADBMOCKFILE_TEMP) > $(ADBMOCKFILE) 102 | 103 | # Grant execution permission. 104 | chmod +rwx $(ADBMOCKFILE) 105 | 106 | .PHONY: file_tester 107 | 108 | file_tester: 109 | mkdir -p $(OUTDIR) 110 | mkdir -p $(OBJDIR) 111 | 112 | echo "" > $(FILETESTERFILE) 113 | echo "" > $(FILETESTERFILE_TEMP) 114 | 115 | cat $(TESTDIR)/res/adb_log_example.sh >> "$(FILETESTERFILE_TEMP)" 116 | cat $(TESTDIR)/validators/file_validator.sh >> "$(FILETESTERFILE_TEMP)" 117 | 118 | # Remove comments/shebangs. 119 | sed -e '/^[ \t]*#/d' "$(FILETESTERFILE_TEMP)" > $(FILETESTERFILE) 120 | mv $(FILETESTERFILE) "$(FILETESTERFILE_TEMP)" 121 | 122 | # Add one shebang and the copyright notice. 123 | # We need a temp file since cat can't do it in-place. 124 | cat $(RESDIR)/header/purr_header.txt $(FILETESTERFILE_TEMP) > $(FILETESTERFILE) 125 | 126 | # Grant execution permission. 127 | chmod +rwx $(FILETESTERFILE) 128 | 129 | .PHONY: clean 130 | 131 | clean: 132 | [ -e $(OUTDIR) ] && rm -r $(OUTDIR) || true 133 | [ -e $(OBJDIR) ] && rm -r $(OBJDIR) || true 134 | -------------------------------------------------------------------------------- /src/bindings/stream_bindings.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | rebind_in_default_command_suite "F1" 18 | rebind_in_adb_command_suite "F1" 19 | rebind_in_history_command_suite "F1" 20 | rebind_in_serial_command_suite "F1" 21 | 22 | rebind_in_default_command_suite "F2" 23 | rebind_in_adb_command_suite "F2" 24 | rebind_in_history_command_suite "F2" 25 | rebind_in_serial_command_suite "F2" 26 | 27 | rebind_in_default_command_suite "F3" 28 | rebind_in_adb_command_suite "F3" 29 | rebind_in_history_command_suite "F3" 30 | rebind_in_serial_command_suite "F3" 31 | 32 | rebind_in_default_command_suite "F4" 33 | rebind_in_adb_command_suite "F4" 34 | rebind_in_history_command_suite "F4" 35 | rebind_in_serial_command_suite "F4" 36 | 37 | rebind_in_default_command_suite "F5" 38 | rebind_in_adb_command_suite "F5" 39 | rebind_in_history_command_suite "F5" 40 | rebind_in_serial_command_suite "F5" 41 | 42 | rebind_in_default_command_suite "F6" 43 | rebind_in_adb_command_suite "F6" 44 | rebind_in_history_command_suite "F6" 45 | rebind_in_serial_command_suite "F6" 46 | 47 | rebind_in_default_command_suite "ctrl-r" 48 | unbind_in_adb_command_suite "ctrl-r" 49 | unbind_in_history_command_suite "ctrl-r" 50 | unbind_in_serial_command_suite "ctrl-r" 51 | 52 | # Bind: F1, show logcat error stream. 53 | error_cmd=( 54 | 'execute-silent(' 55 | '{' 56 | "if $is_unique_on; then" 57 | $set_stream_error_unique 58 | "else;" 59 | $set_stream_error 60 | "fi;" 61 | $set_header_error 62 | $set_slock_off 63 | '} &' 64 | ')+reload(' 65 | $inject_empty_line 66 | $load_input_stream 67 | ")+transform-header(" 68 | $load_generic_header 69 | ")+first+enable-search+$default_command_suite" 70 | ) 71 | bind_commands+=('--bind' "F1:$error_cmd") 72 | 73 | # Bind: F2, show logcat warning stream. 74 | warn_cmd=( 75 | 'execute-silent(' 76 | '{' 77 | "if $is_unique_on; then" 78 | $set_stream_warning_unique 79 | "else;" 80 | $set_stream_warning 81 | "fi;" 82 | $set_header_warning 83 | $set_slock_off 84 | '} &' 85 | ')+reload(' 86 | $inject_empty_line 87 | $load_input_stream 88 | ")+transform-header(" 89 | $load_generic_header 90 | ")+first+enable-search+$default_command_suite" 91 | ) 92 | bind_commands+=('--bind' "f2:$warn_cmd") 93 | 94 | # Bind: F3, show logcat info stream. 95 | info_cmd=( 96 | 'execute-silent(' 97 | '{' 98 | "if $is_unique_on; then" 99 | $set_stream_info_unique 100 | "else;" 101 | $set_stream_info 102 | "fi;" 103 | $set_header_info 104 | $set_slock_off 105 | '} &' 106 | ')+reload(' 107 | $inject_empty_line 108 | $load_input_stream 109 | ")+transform-header(" 110 | $load_generic_header 111 | ")+first+enable-search+$default_command_suite" 112 | ) 113 | bind_commands+=('--bind' "f3:$info_cmd") 114 | 115 | # Bind: F4, show logcat verbose stream. 116 | verb_cmd=( 117 | 'execute-silent(' 118 | '{' 119 | "if $is_unique_on; then" 120 | $set_stream_verbose_unique 121 | "else;" 122 | $set_stream_verbose 123 | "fi;" 124 | $set_header_verbose 125 | $set_slock_off 126 | '} &' 127 | ')+reload(' 128 | $inject_emppty_line 129 | $load_input_stream 130 | ")+transform-header(" 131 | $load_generic_header 132 | ")+first+enable-search+$default_command_suite" 133 | ) 134 | bind_commands+=('--bind' "f4:$verb_cmd") 135 | 136 | # Bind: F5, show serial stream. 137 | serial_cmd=( 138 | 'execute-silent(' 139 | '{' 140 | $set_stream_serial 141 | $set_header_serial 142 | $set_slock_off 143 | '} &' 144 | ')+reload(' 145 | $load_input_stream 146 | ")+transform-header(" 147 | $load_generic_header 148 | ")+first+enable-search+hide-preview+$serial_command_suite+execute-silent(echo 'hidden' >| $purr_preview_visible_cache;)" 149 | ) 150 | bind_commands+=('--bind' "f5:$serial_cmd") 151 | 152 | # Bind: F6, open adb command mode. 153 | adb_stream_cmd=( 154 | 'execute-silent(' 155 | '{' 156 | $set_stream_adb 157 | $set_header_adb 158 | $set_slock_off 159 | '} &' 160 | ')+reload(' 161 | $inject_empty_line 162 | $load_input_stream 163 | ")+transform-header(" 164 | $load_generic_header 165 | ")+first+disable-search+hide-preview+$adb_command_suite+execute-silent(echo 'hidden' >| $purr_preview_visible_cache;)" 166 | ) 167 | bind_commands+=('--bind' "f6:$adb_stream_cmd") 168 | 169 | # Bind: ctrl-r, show history file. 170 | history_cmd=( 171 | 'execute-silent(' 172 | '{' 173 | $set_stream_history 174 | $set_header_history 175 | $set_slock_off 176 | '} &' 177 | ')+reload(' 178 | $load_input_stream 179 | ")+transform-header(" 180 | $load_generic_header 181 | ")+clear-query+first+hide-preview+enable-search+$history_command_suite+execute-silent(echo 'hidden' >| $purr_preview_visible_cache;)" 182 | ) 183 | bind_commands+=('--bind' "ctrl-r:$history_cmd") 184 | -------------------------------------------------------------------------------- /src/ui/ui_strings.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Prompt for each input stream. 18 | stream_error_msg="\x1b[1;31mError\x1b[1;0m\t\t" 19 | stream_warn_msg="\x1b[1;33mWarning\x1b[1;0m\t\t" 20 | stream_info_msg="\x1b[1;32mInfo\x1b[1;0m\t\t" 21 | stream_verbose_msg="\x1b[1;34mVerbose\x1b[1;0m\t\t" 22 | stream_focus_msg="\x1b[1;35mFocus\x1b[1;0m\t\t" 23 | stream_adb_msg="\x1b[1;35mADB\x1b[1;0m\t\t" 24 | 25 | # Prompt for scroll lock on/off. 26 | slock_on_msg="\x1b[1;31mOn \x1b[1;0m" 27 | slock_off_msg="\x1b[1;32mOff \x1b[1;0m" 28 | 29 | # Prompt for scroll lock on/off. 30 | unique_on_msg="\x1b[1;31mOn \x1b[1;0m" 31 | unique_off_msg="\x1b[1;32mOff \x1b[1;0m" 32 | 33 | # Prompt for each sorting mode. 34 | sorting_chronological="\x1b[1;35mChronological\x1b[1;0m\t" 35 | sorting_relevance="\x1b[1;36mRelevance\x1b[1;0m\t\t" 36 | 37 | # Some reasonable defaults for FZF. 38 | fzf_params=('--exact' '--ansi' '--tac' '--no-sort' '--multi' $query_string) 39 | fzf_pretty=('--info' 'inline' '--pointer' '>') 40 | fzf_header=('--header-first') 41 | label_header=" Date $(date +"%Z %z") PID TID" 42 | fzf_label=('--border-label' $label_header '--border-label-pos' '1:top') 43 | fzf_gui=('--border' '--margin' '0.5%' '--padding' '0.5%' '--height' '100%') 44 | 45 | fzfpnh=($fzf_params $fzf_pretty $fzf_gui $custom_fzf_params $fzf_label) 46 | fzfp=($fzfpnh $fzf_header) 47 | 48 | # Some reasonable defaults for adb logcat. 49 | teecmd=("|" 'tee' $purr_input_cache) 50 | check_adb_status=("|" 'grep' '-q' '-v' '"Lost connection to device."') 51 | 52 | load_generic_header=( 53 | "echo -n \"Stream: \";" 54 | "cat $purr_stream_header_cache | tr -d \"\n\";" 55 | "echo -n \"Sort: \";" 56 | "cat $purr_sort_header_cache | tr -d \"\n\";" 57 | "echo -n \"Scroll Lock: \";" 58 | "cat $purr_slock_cache | tr -d \"\n\";" 59 | "echo -n \"Unique: \";" 60 | "cat $purr_unique_cache | tr -d \"\n\";" 61 | "echo \"\x1b[1;2;37mPurr: Happy Logcat\x1b[1;0m\";" 62 | ) 63 | 64 | # Handles the UI logic of reloading the header. 65 | load_input_stream=( 66 | "if grep -q \"History\" $purr_stream_header_cache; then" 67 | "cat $purr_input_stream_cache | zsh;" 68 | "elif grep -q \"Serial\" $purr_stream_header_cache; then" 69 | "cat $purr_input_stream_cache | zsh;" 70 | "elif grep -q \"ADB\" $purr_stream_header_cache; then" 71 | # If the user runs a command, put the full command as the first line. 72 | "if grep -q \"$adb_cmd_loc -s\" $purr_input_stream_cache; then" 73 | "echo \"Command: \$(cat $purr_input_stream_cache)\";" 74 | "fi;" 75 | "cat $purr_input_stream_cache | zsh |& tee;" # Prints all streams to stdout. 76 | 'else;' 77 | $update_serial_cmd 78 | "if $adb_cmd_loc devices | grep \$serial &> /dev/null; then" 79 | "echo \"\x1b[1;32m \$serial => \x1b[1;0m\" >| $purr_connection_state_cache;" 80 | 'else;' 81 | "echo \"\x1b[1;31m \$serial != \x1b[1;0m\" >| $purr_connection_state_cache;" 82 | 'fi;' 83 | 84 | "cat $purr_input_stream_cache | zsh;" 85 | "fi;" 86 | ) 87 | 88 | instruction_preview_command="cat $purr_instruction_cache" 89 | instruction_preview_window="right,50%,nohidden,nofollow,wrap,<55(up,50%,nohidden,nofollow,wrap)" 90 | instruction_preview_window_hidden="right,50%,hidden,nofollow,wrap,<55(up,50%,hidden,nofollow,wrap)" 91 | hint_preview=("change-preview($instruction_preview_command)+change-preview-window($instruction_preview_window)+change-preview-label()+refresh-preview") 92 | 93 | set_stream_error="echo \"tail -F -n 99999999 $purr_error_input_cache $teecmd\" >| $purr_input_stream_cache;" 94 | set_stream_warning="echo \"tail -F -n 99999999 $purr_warning_input_cache $teecmd\" >| $purr_input_stream_cache;" 95 | set_stream_info="echo \"tail -F -n 99999999 $purr_info_input_cache $teecmd\" >| $purr_input_stream_cache;" 96 | set_stream_verbose="echo \"tail -F -n 99999999 $purr_verbose_input_cache $teecmd\" >| $purr_input_stream_cache;" 97 | 98 | set_stream_error_unique="echo \"tail -F -n 99999999 $purr_error_input_cache_unique $teecmd\" >| $purr_input_stream_cache;" 99 | set_stream_warning_unique="echo \"tail -F -n 99999999 $purr_warning_input_cache_unique $teecmd\" >| $purr_input_stream_cache;" 100 | set_stream_info_unique="echo \"tail -F -n 99999999 $purr_info_input_cache_unique $teecmd\" >| $purr_input_stream_cache;" 101 | set_stream_verbose_unique="echo \"tail -F -n 99999999 $purr_verbose_input_cache_unique $teecmd\" >| $purr_input_stream_cache;" 102 | 103 | set_stream_adb="echo \"tail -F -n 99999999 $purr_adb_cache $teecmd\" >| $purr_input_stream_cache;" 104 | 105 | # Sets the stream header to a given message. 106 | set_header_error="echo \"$stream_error_msg\" >| $purr_stream_header_cache;" 107 | set_header_warning="echo \"$stream_warn_msg\" >| $purr_stream_header_cache;" 108 | set_header_info="echo \"$stream_info_msg\" >| $purr_stream_header_cache;" 109 | set_header_verbose="echo \"$stream_verbose_msg\" >| $purr_stream_header_cache;" 110 | set_header_focus="echo \"$stream_focus_msg\" >| $purr_stream_header_cache;" 111 | set_header_adb="echo \"$stream_adb_msg\" >| $purr_stream_header_cache;" 112 | 113 | # Sets the sort header to a given message. 114 | set_sort_chrono="echo \"$sorting_chronological\" >| $purr_sort_header_cache;" 115 | set_sort_relevance="echo \"$sorting_relevance\" >| $purr_sort_header_cache;" 116 | 117 | # Saves the current query for an fzf reboot. 118 | save_current_query="echo {q} > $purr_query_cache;" 119 | 120 | # Sets the scroll lock header to a given state. 121 | set_slock_on="echo \"$slock_on_msg\" >| $purr_slock_cache;" 122 | set_slock_off="echo \"$slock_off_msg\" >| $purr_slock_cache;" 123 | 124 | # Sets the scroll lock header to a given state. 125 | set_unique_on="echo \"$unique_on_msg\" >| $purr_unique_cache;" 126 | set_unique_off="echo \"$unique_off_msg\" >| $purr_unique_cache;" 127 | 128 | # Boolean to determine state of scroll lock. 129 | is_slock_on="grep -q \"On\" \"$purr_slock_cache\"" 130 | 131 | # Boolean to determine state of unique mode. 132 | is_unique_on="grep -q \"On\" \"$purr_unique_cache\"" 133 | 134 | # Boolean to determine state of sort order. 135 | is_sort_chrono="grep -q \"Chronological\" \"$purr_sort_header_cache\"" 136 | 137 | # Injects an empty line to force fzf to refresh the input stream. 138 | inject_empty_line="echo \"\";" 139 | 140 | # Adds logic for handling history and serial selection. 141 | stream_history_msg="\x1b[1;36mHistory\x1b[1;0m\t\t" 142 | stream_serial_msg="\x1b[1;36mSerial\x1b[1;0m\t\t" 143 | 144 | set_stream_history="echo \"cat $purr_history_cache\" >| $purr_input_stream_cache;" 145 | set_header_history="echo \"$stream_history_msg\" >| $purr_stream_header_cache;" 146 | 147 | set_stream_serial="echo \"$adb_cmd_loc devices | tail -n +2 |sed '/^\s*$/d' | sort | awk '{print \$ 1}'\" >| $purr_input_stream_cache;" 148 | set_header_serial="echo \"$stream_serial_msg\" >| $purr_stream_header_cache;" 149 | 150 | hint_preview_window="top,70%,nohidden,wrap,+200/2" 151 | hint_preview_window_hidden="top,70%,hidden,wrap,+200/2" 152 | 153 | start_stream="echo \"$PURR_THREAD_START\" >$thread_io_pipe;" 154 | stop_stream="echo \"$PURR_THREAD_STOP\" >$thread_io_pipe;" 155 | -------------------------------------------------------------------------------- /src/bindings/preview_bindings.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Bind: F7, set preview to the instruction menu.. 18 | instruction_preview_cache_command=$(cat <<-END 19 | echo 'nohidden' >| $purr_preview_visible_cache; 20 | echo 'instruction' >| $purr_preview_command_cache; 21 | END 22 | ) 23 | bind_commands+=('--bind' "F7:$hint_preview+execute-silent($instruction_preview_command)") 24 | rebind_in_default_command_suite "F7" 25 | unbind_in_adb_command_suite "F7" 26 | unbind_in_history_command_suite "F7" 27 | unbind_in_serial_command_suite "F7" 28 | 29 | 30 | # Bind: F9, highlight selected line and show 200 lines of context around it in current stream. 31 | current_hint_preview_command=$(cat <<-END 32 | # Grab the line number from the input cache. This may not be unique if multiple lines are exact matches. 33 | line_number="\$(grep -F -n -- {} $purr_input_cache | cut -d':' -f1)"; 34 | 35 | if [ -z "\$line_number" ]; then 36 | echo "Could not identify selected line in input buffer!"; 37 | echo "This should never happen; please report me as a bug!"; 38 | else; 39 | 40 | # Get only the first line to display. 41 | first_line_number="\$(head -n 1 <<< "\$line_number")"; 42 | 43 | # Get the number of lines in the input file for pointer math! 44 | lines_in_input_file="\$(wc -l $purr_input_cache | xargs | cut -d' ' -f1)"; 45 | 46 | # Get the lines we need to put into the preview file. 47 | line_number_min=\$((first_line_number-200)); 48 | line_number_max=\$((first_line_number+200)); 49 | 50 | # Try not to overflow from the input file. 51 | if [ \$line_number_max -ge \$lines_in_input_file ]; then 52 | line_number_max=\$lines_in_input_file; 53 | fi; 54 | 55 | # Try not to underflow from the input file. 56 | if [ \$line_number_min -le 0 ]; then 57 | pad_top_number=\$((200 - first_line_number)) 58 | line_number_min=0; 59 | fi; 60 | 61 | # Get the number we need to tail for. 62 | tail_line_numbers=\$((\$line_number_max - \$line_number_min)); 63 | 64 | # Get the lines that need to be printed from the input cache. 65 | full_lines=\$(head -n \$line_number_max $purr_input_cache | tail -n \$tail_line_numbers -q;); 66 | 67 | # If we don't have enough lines on the top, we'll pad with new lines so that 68 | # fzf can still send us to above the correct line in the preview. 69 | if [ ! -z "\$pad_top_number" ]; then 70 | for i in {1..\$pad_top_number}; do 71 | padding_string="\$padding_string\n"; 72 | done; 73 | full_lines="\$padding_string""\$full_lines"; 74 | fi; 75 | 76 | # Start building the preview. We need to do this piecemeal to make sure we can 77 | # highlight the relevant lines. 78 | preview_top=\$(echo \$full_lines | head -n 199); 79 | 80 | # Since we aren't guarenteed that the selected line is unique, let's tell the user so 81 | # they understand why the line might seem different. 82 | if [ "\$(wc -l <<< "\$line_number")" -ne 1 ]; then 83 | info_panel="\\n----------------"; 84 | info_panel+="\\nSeeing multiple exact matches; highlighting first instance." 85 | info_panel+="\\nFirst Instance on line \$first_line_number"; 86 | info_panel+="\\nDuplicates on line(s) \$(echo \$line_number | tail -n +2 | tr '\n' ' ')"; 87 | info_panel+="\\n----------------"; 88 | fi; 89 | 90 | # Highlight the line the user selected. 91 | highlighted_line="\$(echo -- \$full_lines | head -n 200 -- | tail -n 1 -q -- | cat -v -- | sed -e "s/\^\[\[[0-9;]*m/\x1b[1;36m/g")"; 92 | 93 | # We might not have lines at the bottom to print, so we need to check 94 | # how big the padded buffer is. 95 | full_lines_size=\$(echo \$full_lines | wc -l); 96 | bottom_line_numbers=\$((\$full_lines_size - 200)); 97 | 98 | # Load in the bottom of the preview. 99 | preview_bottom=\$(echo \$full_lines | tail -n \$bottom_line_numbers); 100 | 101 | # Construct and print the preview. 102 | constructed_preview="\$preview_top\$info_panel\\n\$highlighted_line\\n\$preview_bottom"; 103 | echo "\$constructed_preview"; 104 | fi; 105 | END 106 | ) 107 | current_hint_preview_cache_command=$(cat <<-END 108 | echo 'nohidden' >| $purr_preview_visible_cache; 109 | echo 'current' >| $purr_preview_command_cache; 110 | END 111 | ) 112 | bind_commands+=('--bind' "F9:change-preview($current_hint_preview_command)+change-preview-label(F9: Current Stream)+change-preview-window($hint_preview_window)+refresh-preview+execute-silent($current_hint_preview_cache_command)") 113 | rebind_in_default_command_suite "F9" 114 | unbind_in_adb_command_suite "F9" 115 | unbind_in_history_command_suite "F9" 116 | unbind_in_serial_command_suite "F9" 117 | 118 | # Bind: F10, highlight selected line and show 200 lines of context around it in verbose stream. 119 | verbose_hint_preview_command=$(echo "$current_hint_preview_command" | sed "s:$purr_input_cache:$purr_verbose_input_cache:g") 120 | verbose_hint_preview_cache_command=$(cat <<-END 121 | echo 'nohidden' >| $purr_preview_visible_cache; 122 | echo 'verbose' >| $purr_preview_command_cache; 123 | END 124 | ) 125 | bind_commands+=('--bind' "F10:change-preview($verbose_hint_preview_command)+change-preview-label(F10: Verbose Stream)+change-preview-window($hint_preview_window)+refresh-preview+execute-silent($verbose_hint_preview_cache_command)") 126 | rebind_in_default_command_suite "F10" 127 | unbind_in_adb_command_suite "F10" 128 | unbind_in_history_command_suite "F10" 129 | unbind_in_serial_command_suite "F10" 130 | 131 | # Bind: Ctrl-P, toggle preview on/off. 132 | toggle_preview_cache_command=$(cat <<-END 133 | if [ \$(cat $purr_preview_visible_cache) = 'nohidden' ]; then; 134 | echo 'hidden' >| $purr_preview_visible_cache; 135 | else; 136 | echo 'nohidden' >| $purr_preview_visible_cache; 137 | fi 138 | END 139 | ) 140 | bind_commands+=('--bind' "ctrl-p:toggle-preview+execute-silent($toggle_preview_cache_command)") 141 | rebind_in_default_command_suite "ctrl-p" 142 | unbind_in_adb_command_suite "ctrl-p" 143 | unbind_in_history_command_suite "ctrl-p" 144 | unbind_in_serial_command_suite "ctrl-p" 145 | 146 | instruction_preview_starter=("--preview" 147 | "$instruction_preview_command" 148 | "--preview-window" 149 | "$instruction_preview_window") 150 | 151 | current_hint_preview_starter=("--preview" 152 | "$current_hint_preview_command" 153 | "--preview-window" 154 | "$hint_preview_window" 155 | "--preview-label" 156 | "Current Stream") 157 | 158 | verbose_hint_preview_starter=("--preview" 159 | "$verbose_hint_preview_command" 160 | "--preview-window" 161 | "$hint_preview_window" 162 | "--preview-label" 163 | "Verbose Stream") 164 | 165 | instruction_preview_starter_hidden=("--preview" 166 | "$instruction_preview_command" 167 | "--preview-window" 168 | "$instruction_preview_window_hidden") 169 | 170 | current_hint_preview_starter_hidden=("--preview" 171 | "$current_hint_preview_command" 172 | "--preview-window" 173 | "$hint_preview_window_hidden" 174 | "--preview-label" 175 | "Current Stream") 176 | 177 | verbose_hint_preview_starter_hidden=("--preview" 178 | "$verbose_hint_preview_command" 179 | "--preview-window" 180 | "$hint_preview_window_hidden" 181 | "--preview-label" 182 | "Verbose Stream") 183 | -------------------------------------------------------------------------------- /src/threads/thread_background.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Spawns 8 background threads; four to stream standard error/warning/info/verbose, and another 18 | # four to stream the unique versions of the streams. 19 | __purr_start_streams() { 20 | __purr_stream_background_file $purr_error_input_cache "*:E" $purr_serial_cache & 21 | __purr_stream_background_file $purr_warning_input_cache "*:W" $purr_serial_cache & 22 | __purr_stream_background_file $purr_info_input_cache "*:I" $purr_serial_cache & 23 | __purr_stream_background_file $purr_verbose_input_cache "*:V" $purr_serial_cache & 24 | 25 | __purr_stream_unique_file $purr_error_input_cache $purr_error_input_cache_unique $purr_error_unique_checksum_cache & 26 | __purr_stream_unique_file $purr_warning_input_cache $purr_warning_input_cache_unique $purr_warning_unique_checksum_cache & 27 | __purr_stream_unique_file $purr_info_input_cache $purr_info_input_cache_unique $purr_info_unique_checksum_cache & 28 | __purr_stream_unique_file $purr_verbose_input_cache $purr_verbose_input_cache_unique $purr_verbose_unique_checksum_cache & 29 | } 30 | 31 | # We can just kill the stream threads; they only interact with 32 | # the stream file, so we don't care about is happening when they die. 33 | __purr_cleanup_streams() { 34 | kill ${${(v)jobstates##*:*:}%=*} 35 | for pid in ${${(v)jobstates##*:*:}%=*}; do 36 | wait $pid 37 | done 38 | 39 | rm $purr_error_input_cache &> /dev/null 40 | rm $purr_warning_input_cache &> /dev/null 41 | rm $purr_info_input_cache &> /dev/null 42 | rm $purr_verbose_input_cache &> /dev/null 43 | 44 | rm $purr_error_input_cache_unique &> /dev/null 45 | rm $purr_warning_input_cache_unique &> /dev/null 46 | rm $purr_info_input_cache_unique &> /dev/null 47 | rm $purr_verbose_input_cache_unique &> /dev/null 48 | } 49 | 50 | # The function that runs the stream threads. Each should run an 51 | # adb process that feeds information into the given file. We also 52 | # want to make sure that only one thread is handling shared logic, 53 | # so the verbose thread is responsible for this, as it has the most 54 | # context. Since these threads are the only ones who directly interact 55 | # with adb logcat, they are also responsible for monitoring the connection 56 | # state of the device and ensuring they can cleanly resume if we lose 57 | # the device. 58 | __purr_stream_background_file() { 59 | local stream_file=$1 60 | local stream_command=$2 61 | local purr_serial_cache=$3 62 | 63 | # We only want the verbose background stream to perform certain actions. 64 | if grep -q "verbose" <<< "$stream_file"; then 65 | local am_verbose="true" 66 | else 67 | local am_verbose="false" 68 | fi 69 | 70 | # If the directory doesn't exist anymore, it's likely that we have exited. 71 | touch $stream_file &> /dev/null 72 | if [ $? -ne 0 ]; then 73 | exit 74 | fi 75 | 76 | # We don't know anything about the connection yet, even 77 | # though we should be in a good state. 78 | conn_status="unknown" 79 | 80 | while true; do 81 | 82 | # Make sure we have the most up to date serial. 83 | __purr_update_serial 84 | if [ -z $serial ]; then 85 | exit 86 | fi 87 | 88 | # If we disconnected, we want to make sure that the device is still offline. 89 | if [[ $conn_status = "potential" ]]; then 90 | purr_timeout 2 "$adb_cmd_loc -s $serial wait-for-device" &> /dev/null 91 | 92 | # The device seems to have found itself, we can just reconnect. 93 | if [ $? -eq 0 ]; then 94 | echo "\x1b[1;36mPURR STATUS: Device Responding.\x1b[1;0m" >> $stream_file 95 | echo "\x1b[1;36mPURR STATUS: Restarting input from last seen timestamp.\x1b[1;0m" >> $stream_file 96 | conn_status="alive" 97 | if [ $am_verbose = "true" ]; then 98 | echo "\x1b[1;32m $serial => \x1b[1;0m" > $purr_connection_state_cache 99 | fi 100 | else # Otherwise, we'll enter a dead state and start polling for the device. 101 | echo "\x1b[1;36mPURR STATUS: Device Not Responding.\x1b[1;0m" >> $stream_file 102 | echo "\x1b[1;36mPURR STATUS: Locking input streams until device found.\x1b[1;0m" >> $stream_file 103 | conn_status="dead" 104 | if [ $am_verbose = "true" ]; then 105 | echo "\x1b[1;31m $serial != \x1b[1;0m" > $purr_connection_state_cache 106 | fi 107 | fi 108 | else # If we aren't in potential mode, we poll for the device until we find it. 109 | eval "$adb_cmd_loc -s $serial wait-for-device &> /dev/null" 110 | 111 | if [ $? -eq 0 ]; then 112 | if [ $conn_status != "unknown" ]; then 113 | echo "\x1b[1;36mPURR STATUS: Device Responding.\x1b[1;0m" >> $stream_file 114 | echo "\x1b[1;36mPURR STATUS: Restarting input from last seen timestamp.\x1b[1;0m" >> $stream_file 115 | fi 116 | 117 | conn_status="alive" 118 | if [ $am_verbose = "true" ]; then 119 | echo "\x1b[1;32m $serial => \x1b[1;0m" > $purr_connection_state_cache 120 | fi 121 | fi 122 | fi 123 | 124 | # Once we've established a device connection, we'll start streaming from logcat. 125 | if [ $conn_status = "alive" ]; then 126 | 127 | # Handles trimming logcat input to a specific timestamp. 128 | trim_time=$(cat $purr_time_start_cache) 129 | if [ -z $trim_time ]; then 130 | eval "$adb_cmd_loc -s $serial logcat -v color $custom_adb_params '$stream_command' >> $stream_file" 131 | else 132 | 133 | # We don't want the trim time to survive a thread cleanup, 134 | # so once the verbose thread is sure all the other threads 135 | # have read it, we wipe the state cache. 136 | if [ $am_verbose = "true" ]; then 137 | { 138 | sleep 1.5 139 | if [ -f $purr_time_start_cache ]; then 140 | echo "" > $purr_time_start_cache &> /dev/null 141 | fi 142 | } & 143 | fi 144 | eval "$adb_cmd_loc -s $serial logcat -v color $custom_adb_params -T '$trim_time' '$stream_command' >> $stream_file" 145 | fi 146 | 147 | # We've been disconnected, and we're not sure what the state is. We'll 148 | # grab the last message sent so we can restart at the same timestamp. 149 | if [ $am_verbose = "true" ]; then 150 | trimmed_time=$(echo $(tail -1 $stream_file) | cut -d' ' -f1-2 | sed -e 's/\x1b\[[0-9;]*m//g'); 151 | echo $trimmed_time > $purr_time_start_cache; 152 | fi 153 | 154 | echo "\x1b[1;36mPURR STATUS: Potential Connection Lost.\x1b[1;0m" >> $stream_file 155 | conn_status="potential" 156 | fi 157 | 158 | sleep 0.5 159 | done 160 | } 161 | 162 | __purr_stream_unique_file() { 163 | local stream_input_file=$1 164 | local stream_output_file=$2 165 | local stream_checksum_file=$3 166 | 167 | # If the directory doesn't exist anymore, it's likely that we have exited. 168 | touch $stream_output_file &> /dev/null 169 | if [ $? -ne 0 ]; then 170 | exit 171 | fi 172 | 173 | # If the directory doesn't exist anymore, it's likely that we have exited. 174 | touch $stream_checksum_file &> /dev/null 175 | if [ $? -ne 0 ]; then 176 | exit 177 | fi 178 | 179 | # MacOS doesn't have a -s parameter for tail, so unique mode breaks... 180 | if [ "$(uname)" = "Darwin" ]; then 181 | tail_sleep_interval="" 182 | else 183 | tail_sleep_interval="-s 0.1" 184 | fi 185 | 186 | tail $tail_sleep_interval -F --lines=+0 -- $stream_input_file 2> /dev/null | while read -r line; do 187 | trimmed_line_cksum=$(echo $line | awk '{$1=$1};1' | cut -d' ' -f5- -- | cksum | cut -d' ' -f1) 188 | if command -v rg &>/dev/null; then 189 | if ! rg -q "$trimmed_line_cksum" $stream_checksum_file 2> /dev/null; then 190 | echo "$line" >> $stream_output_file 191 | echo "$trimmed_line_cksum" >> $stream_checksum_file 192 | else 193 | echo "Erasing duplicate line..." >> $stream_output_file 194 | fi 195 | else 196 | if ! grep -q "$trimmed_line_cksum" $stream_checksum_file 2> /dev/null; then 197 | echo "$line" >> $stream_output_file 198 | echo "$trimmed_line_cksum" >> $stream_checksum_file 199 | else 200 | echo "Erasing duplicate line..." >> $stream_output_file 201 | fi 202 | fi 203 | done 204 | } 205 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # purr 2 | 3 | ### Introduction 4 | purr is a zsh CLI tool for viewing and searching through Android logcat output. It leverages [fzf](https://github.com/junegunn/fzf) to provide a simple yet powerful user interface, fuzzy-finding capabilities, and much more. 5 | 6 | ### Motivation 7 | While Android Studio's logcat viewer is sufficient for most app development, it breaks down when exposed to situations such as terminal-only access or when multiple devices need to be accessed quickly. When performing development on the Android operating system itself, developers revert to using raw `adb logcat` in shell. 8 | 9 | This is sub-optimal and wastes a lot of time on writing `grep` statements and rooting through uncolored, unfiltered text with poor user experience. `purr` is meant as a solution to this; a powerful logcat viewer running entirely on the shell, capable of going through millions of logs quickly, while leveraging other shell-based solutions for common problems. 10 | 11 | Purr can be used for simple app debugging, in which it provides a quicker interface than standard Logcat. 12 | 13 | https://github.com/google/purr/assets/126256142/527bb85d-bf50-4ea8-b74f-7751098a3162 14 | 15 | For more complex diagnosis, purr shines in quickly jumping around and isolating relevant logs. 16 | 17 | https://github.com/google/purr/assets/126256142/fa490ac4-4f20-4049-87df-ffb2b10c3fb2 18 | 19 | There's even a mode to search through `adb shell` results; no more grepping through dumpsys! 20 | 21 | https://github.com/google/purr/assets/126256142/fb41ec9d-f5a7-43be-98be-9d04ed7b536e 22 | 23 | ### Installation 24 | 1. Download the latest release version. 25 | 2. Place the script in your PATH. 26 | 3. If you need a copy program, also put the bundled osc52_copy program in your PATH. 27 | 28 | ### Compilation 29 | 1. Clone the repo 30 | 2. Run "make" 31 | 32 | ### Dependencies 33 | 34 | `purr` currently functions on Ubuntu Linux and Mac on `zsh`, and requires a local install of `fzf` with version `0.40.0` or higher, `perl`, and `adb`. 35 | 36 | Some `purr` commands require a program to copy to clipboard. `purr` will check automatically for installations of `pbcopy`, `xsel`, and `wl-copy`. If the `COPY_PROGRAM` variable is set, purr will attempt to use it. If you are unsure of which copy program to use, a version of OSC52 copy is bundled and can be used if it is added to your path (and your terminal supports OSC52). 37 | 38 | Support for Windows may be provided in future, but is not a current priority. 39 | 40 | * [fzf](https://github.com/junegunn/fzf) [0.40.0+] 41 | * [zsh](https://github.com/zsh-users/zsh) 42 | * [adb](https://developer.android.com/studio/command-line/adb) 43 | * [perl](https://www.perl.org/) 44 | 45 | ### Guide 46 | `purr` includes a simple tool to help select the device serial from `adb devices`, or can read from the `$ANDROID_SERIAL` environment variable if set. Otherwise, `purr` has the following command-line parameters: 47 | 48 | * -a: Sets custom parameters for `adb` that will be used as well as the defaults whenever an input stream is selected. 49 | * -f: Sets custom parameters for `fzf`. Used on top of default parameters. 50 | * -i: Disables the instruction preview on launch. 51 | * -q: Set the default query string upon `purr` being opened. 52 | * -v: Shows the `purr` version. 53 | * -V: Shows a composite version of `purr` and dependencies. 54 | 55 | There are also the following special command line parameters that should only be used for testing and debugging: 56 | 57 | * -A: Replace all calls to `adb` with the given binary. This can be used in conjunction with the bundled `adb_mock` binary to perform basic testing on purr. 58 | * -D: Use the given directory to store all generated files instead of the standard /tmp dir. 59 | * -X: Do not execute `fzf` when reached in the execution loop; return as if `fzf` executed correctly. 60 | 61 | Any other command-line parameters will print the help dialog. 62 | 63 | Note that both `-a` and `-f` are read without validation; there is no guarantee that setting either parameter will not break `purr`. 64 | 65 | ### Hotkeys 66 | 67 | #### General 68 | * Escape: Exits `purr`. Ctrl-c and other methods also work, but may take longer, and may not gracefully exit. 69 | 70 | #### Stream Modes 71 | * F1/F2/F3/F4: Sets the logcat stream between Error/Warning/Info/Verbose, respectively. 72 | * F5: Opens the serial selection menu. Pressing Enter will select and load the highlighted device. 73 | * F6: Opens ADB command mode. Pressing Enter will execute the current query as an ADB shell command on the current device, and print the output to the finder window. 74 | * Ctrl-r: Opens the history menu. Pressing Enter will load the selected history item into the query and return you to the verbose stream. 75 | 76 | #### Preview Window 77 | * Ctrl-p: Toggles the preview window on/off. By default, the window is on, but you can use the -i flag to make it default to off. 78 | * F7: Show the instruction preview (on by default). 79 | * F9: Shows context around the selected line in the currently selected input stream. 80 | * F10: Shows context around the selected line in the verbose input stream. 81 | * Shift-up/down: Scrolls one item up or down the preview window, respectively. 82 | * Home/End: Scrolls one page up or down the preview window, respectively. 83 | 84 | #### Navigation 85 | * Ctrl-s: Enables scroll lock. While in scroll lock mode, your cursor will remain bound to the selected item as long as it remains in the search filter. 86 | * Ctrl-f: Shorthand for enabling scroll lock and clearing the query. This allows you to go to the surrounding context of a selected item. Scroll lock will end once you move your cursor. 87 | * Ctrl-j: Changes search modes between Chronological (default) and Relevance. This may be useful for fuzzy queries. 88 | 89 | #### Querry 90 | * Ctrl-alt-s: Adds the selected tag to your query. If the tag already exists in your query, do nothing. 91 | * Ctrl-alt-d: Adds the inverse of the selected tag to your query. If the tag already exists in your query, remove it instead. Note that the inverse of the selected tag may also match non-tag lines in your log output. 92 | 93 | #### ADB Shorthands 94 | * Ctrl-t: Trims the logs to any logs after the currently selected items. Useful if attempting to isolate a specific issue after a certain point. 95 | * Ctrl-alt-t: Untrims logs, reverting to showing all logs from the device. 96 | * Ctrl-w: Wipes the logcat logs from the device. 97 | 98 | #### Misc 99 | * Tab: Select a line. Multiple lines can be selected simultaneously. 100 | * Ctrl-y: Yanks selected lines into the system clipboard. 101 | * Ctrl-v: Opens the text editor; see below. 102 | * Ctrl-\\: Prints some basic device information into the system clipboard, and starts a background process to capture a bug report. The bug report is saved to /tmp/bugreport-$target-$device-$sdk-$date 103 | 104 | #### History 105 | `purr` saves a query string to history once it has not been changed for more than 3.5 seconds. You can use the following hotkeys to access history: 106 | 107 | * Ctrl-r: Show the full history menu. 108 | * Alt-shift-up: Move to the next entry in history. 109 | * Alt-shift-down: Move to the previous entry in history. 110 | 111 | When scrolling through history with alt-shift-up or alt-shift-down, your position in the history will reset once a string has been in the query for 3.5 seconds. 112 | 113 | #### Editor 114 | When you select a single line and press Ctrl-V, `purr` will open the selected line and surrounding context in a text editor. You can specify the text editor through the `$EDITOR` or `$EDITOR_PURR` environment variables; if no text editor is specified, `purr` will use `vim`. 115 | 116 | Note that logcat uses ANSI color codes to display color, so an editor that supports these codes is recommended; for example, [AnsiEsc](https://www.vim.org/scripts/script.php?script_id=302) for `Vim`. 117 | 118 | If multiple lines are selected, only those selected lines will be opened in the text editor. 119 | 120 | ### Development 121 | 1. Clone the repo 122 | 2. Open in your favorite IDE/editor 123 | 124 | ### Support 125 | 126 | If you've found an error, please file an issue: 127 | 128 | https://github.com/google/purr/issues 129 | 130 | Patches are encouraged, and may be submitted by forking this project and 131 | submitting a pull request through GitHub. 132 | 133 | License 134 | ======= 135 | 136 | Copyright 2022 Google LLC 137 | 138 | Licensed under the Apache License, Version 2.0 (the "License"); 139 | you may not use this file except in compliance with the License. 140 | You may obtain a copy of the License at 141 | 142 | https://www.apache.org/licenses/LICENSE-2.0 143 | 144 | Unless required by applicable law or agreed to in writing, software 145 | distributed under the License is distributed on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 147 | See the License for the specific language governing permissions and 148 | limitations under the License. 149 | -------------------------------------------------------------------------------- /src/threads/thread_setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Copyright 2023 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | __purr_create_files() { 18 | local dir_name=$1 19 | 20 | if [ -z $dir_name ]; then 21 | echo "Can't parse directory name." 22 | exit 1 23 | fi 24 | 25 | # File to print input into. 26 | purr_input_cache="$dir_name/purr-input-cache.purr" 27 | touch $purr_input_cache 28 | 29 | # File to print input into. 30 | purr_instruction_cache="$dir_name/instructions.purr" 31 | touch $purr_instruction_cache 32 | 33 | # File to use for editor input. 34 | purr_editor_input_cache="$dir_name/editor-input-cache.purr" 35 | touch $purr_editor_input_cache 36 | 37 | # File to use to keep track of input stream. 38 | purr_input_stream_cache="$dir_name/input-stream.purr" 39 | touch $purr_input_stream_cache 40 | 41 | # File to use to keep track of the stream header message. 42 | purr_stream_header_cache="$dir_name/stream-header.purr" 43 | touch $purr_stream_header_cache 44 | 45 | # File to use to keep track of the query during aborting commands. 46 | purr_query_cache="$dir_name/query-cache.purr" 47 | touch $purr_query_cache 48 | 49 | # File to use to keep track of the sort header message. 50 | purr_sort_header_cache="$dir_name/sort-header.purr" 51 | touch $purr_sort_header_cache 52 | 53 | # File to use to keep track of whether scroll lock is on. 54 | purr_slock_cache="$dir_name/scroll-lock-header.purr" 55 | touch $purr_slock_cache 56 | 57 | # File to use to keep track of the serial number. 58 | purr_serial_cache="$dir_name/serial-cache.purr" 59 | touch $purr_serial_cache 60 | 61 | # File to use to keep track of the state of the device connection. 62 | purr_connection_state_cache="$dir_name/connection-state.purr" 63 | touch $purr_connection_state_cache 64 | 65 | # File to use to keep track of the state of the fzf preview window's visibility. 66 | purr_preview_visible_cache="$dir_name/preview-visibility-cache.purr" 67 | touch $purr_preview_visible_cache 68 | 69 | # File to use to keep track of the state of the fzf preview window's command. 70 | purr_preview_command_cache="$dir_name/preview-command-cache.purr" 71 | touch $purr_preview_command_cache 72 | 73 | # Pipe to communicate with the background handler. 74 | thread_io_pipe=$dir_name/thread_io_pipe 75 | mkfifo $thread_io_pipe 76 | 77 | # File to communicate what the accept command intent was. 78 | purr_accept_command_cache="$dir_name/purr_accept_command_cache.purr" 79 | touch $purr_accept_command_cache 80 | 81 | # File to keep track of what time we want to start at. 82 | purr_time_start_cache="$dir_name/purr_time_start_cache.purr" 83 | touch $purr_time_start_cache 84 | 85 | purr_unique_cache="$dir_name/purr_unique_cache.purr" 86 | touch $purr_unique_cache 87 | 88 | # Files to input cache the streams into. 89 | purr_error_input_cache="$dir_name/error-input-cache.purr" 90 | purr_warning_input_cache="$dir_name/warning-input-cache.purr" 91 | purr_info_input_cache="$dir_name/info-input-cache.purr" 92 | purr_verbose_input_cache="$dir_name/verbose-input-cache.purr" 93 | 94 | # Files to input cache the unique version of the streams into. 95 | purr_error_unique_checksum_cache="$dir_name/error-unique-checksum-cache.purr" 96 | purr_warning_unique_checksum_cache="$dir_name/warning-unique-checksum-cache.purr" 97 | purr_info_unique_checksum_cache="$dir_name/info-unique-checksum-cache.purr" 98 | purr_verbose_unique_checksum_cache="$dir_name/verbose-unique-checksum-cache.purr" 99 | 100 | # Files to input cache the unique version of the streams into. 101 | purr_error_input_cache_unique="$dir_name/error-unique-input-cache.purr" 102 | purr_warning_input_cache_unique="$dir_name/warning-unique-input-cache.purr" 103 | purr_info_input_cache_unique="$dir_name/info-unique-input-cache.purr" 104 | purr_verbose_input_cache_unique="$dir_name/verbose-unique-input-cache.purr" 105 | 106 | # File to use for purr history. This file is not removed between sessions. 107 | purr_history_cache="/var/tmp/history.purr" 108 | touch $purr_history_cache 109 | 110 | # File to use for the purr history counter. 111 | purr_history_counter_cache="$dir_name/history-counter.purr" 112 | touch $purr_history_counter_cache 113 | 114 | # File to use for purr history location. 115 | purr_history_pointer_cache="$dir_name/history-pointer.purr" 116 | touch $purr_history_pointer_cache 117 | 118 | purr_spc_purpose_cache="$dir_name/spc-purpose-cache.purr" 119 | touch $purr_spc_purpose_cache 120 | 121 | # We'll use this to cache the result of ADB commands. 122 | purr_adb_cache="$dir_name/adb-cache.purr" 123 | touch $purr_adb_cache 124 | } 125 | 126 | # Writes the instruction file which will be displayed in the 127 | # instruction header when purr starts. 128 | __purr_print_instructions() { 129 | local purr_instruction_cache=$1 130 | 131 | echo "\x1b[1;1m\x1b[1;4mInstructions\x1b[1;0m" >>$purr_instruction_cache 132 | echo " \x1b[1;1mCtrl-p:\x1b[1;0m Toggle this preview window on and off" >>$purr_instruction_cache 133 | echo " \x1b[1;1mShift-up/down:\x1b[1;0m Scroll up/down in the instruction window" >>$purr_instruction_cache 134 | echo " \x1b[1;1mEscape:\x1b[1;0m Exit" >>$purr_instruction_cache 135 | echo "" >>$purr_instruction_cache 136 | echo "\x1b[1;1m\x1b[1;4mStream Modes\x1b[1;0m" >>$purr_instruction_cache 137 | echo " \x1b[1;1mF1/2/3/4:\x1b[1;0m Show Error/Warning/Info/Verbose streams, respectively" >>$purr_instruction_cache 138 | echo " \x1b[1;1mF5:\x1b[1;0m Show the serial selection menu" >>$purr_instruction_cache 139 | echo " \x1b[1;1mF6:\x1b[1;0m Enter ADB command mode" >>$purr_instruction_cache 140 | echo " \x1b[1;1mCtrl-r:\x1b[1;0m Show the history menu" >>$purr_instruction_cache 141 | echo "" >>$purr_instruction_cache 142 | echo "\x1b[1;1m\x1b[1;4mPreview\x1b[1;0m" >>$purr_instruction_cache 143 | echo " \x1b[1;1mCtrl-p:\x1b[1;0m Toggles the preview window on/off" >>$purr_instruction_cache 144 | echo " \x1b[1;1mF7:\x1b[1;0m Shows the instruction preview" >>$purr_instruction_cache 145 | echo " \x1b[1;1mF9:\x1b[1;0m Shows line context in the current stream" >>$purr_instruction_cache 146 | echo " \x1b[1;1mF10:\x1b[1;0m Shows line context in the verbose stream" >>$purr_instruction_cache 147 | echo " \x1b[1;1mShift-up/down:\x1b[1;0m Scroll up/down in the preview window" >>$purr_instruction_cache 148 | echo " \x1b[1;1mHome/End:\x1b[1;0m Scroll one page in the preview window" >>$purr_instruction_cache 149 | echo "" >>$purr_instruction_cache 150 | echo "\x1b[1;1m\x1b[1;4mNavigation\x1b[1;0m" >>$purr_instruction_cache 151 | echo " \x1b[1;1mCtrl-s:\x1b[1;0m Enable Scroll Lock" >>$purr_instruction_cache 152 | echo " \x1b[1;1mCtrl-j:\x1b[1;0m Toggle between Chronological and Relevance sort" >>$purr_instruction_cache 153 | echo " \x1b[1;1mCtrl-f:\x1b[1;0m Go to selected line" >>$purr_instruction_cache 154 | echo " \x1b[1;1mCtrl-alt-s:\x1b[1;0m Add selected lines tag to the query" >>$purr_instruction_cache 155 | echo " \x1b[1;1mCtrl-alt-d:\x1b[1;0m Remove selected lines tag to the query, or adds inverse tag if it doesn't exist" >>$purr_instruction_cache 156 | echo "" >>$purr_instruction_cache 157 | echo "\x1b[1;1m\x1b[1;4mADB Controls\x1b[1;0m" >>$purr_instruction_cache 158 | echo " \x1b[1;1mCtrl-w:\x1b[1;0m Issues 'adb logcat -c' to permanently wipe device logs" >>$purr_instruction_cache 159 | echo " \x1b[1;1mCtrl-t:\x1b[1;0m Trims logs to the selected entries timestamp" >>$purr_instruction_cache 160 | echo " \x1b[1;1mCtrl-alt-t:\x1b[1;0m Removes any applied trim" >>$purr_instruction_cache 161 | echo " \x1b[1;1mCtrl-h:\x1b[1;0m In ADB command mode, attempts to get the help menu of a command" >>$purr_instruction_cache 162 | echo " \x1b[1;1mEnter:\x1b[1;0m In ADB command mode, execute the current query as:" >>$purr_instruction_cache 163 | echo " 'adb -s \$serial shell \$query'" >>$purr_instruction_cache 164 | echo "" >>$purr_instruction_cache 165 | echo "\x1b[1;1m\x1b[1;4mHistory\x1b[1;0m" >>$purr_instruction_cache 166 | echo " \x1b[1;1mAlt-shift-up/down:\x1b[1;0m Scroll up/down through history items" >>$purr_instruction_cache 167 | echo " \x1b[1;1mEnter:\x1b[1;0m Select an item from the history menu" >>$purr_instruction_cache 168 | echo "" >>$purr_instruction_cache 169 | echo "\x1b[1;1m\x1b[1;4mSerial\x1b[1;0m" >>$purr_instruction_cache 170 | echo " \x1b[1;1mEnter:\x1b[1;0m Select an item from the serial menu" >>$purr_instruction_cache 171 | echo "" >>$purr_instruction_cache 172 | echo "\x1b[1;1m\x1b[1;4mMisc.\x1b[1;0m" >>$purr_instruction_cache 173 | echo " \x1b[1;1mCtrl-v:\x1b[1;0m Open selected line in text editor" >>$purr_instruction_cache 174 | echo " \x1b[1;1mCtrl-y:\x1b[1;0m Copy selected lines" >>$purr_instruction_cache 175 | echo " \x1b[1;1mTab:\x1b[1;0m Select multiple lines" >>$purr_instruction_cache 176 | echo " \x1b[1;1mCtrl-\:\x1b[1;0m Print simple device information to the clipboard and start a background process to save a bug report to /tmp/bug-report-\$device-\$date.zip." >>$purr_instruction_cache 177 | } 178 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | IGNORE_COPYRIGHT: Google License 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "[]" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright [yyyy] [name of copyright owner] 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. --------------------------------------------------------------------------------