├── .gitignore ├── resources └── sbp_screenshot.png ├── CONTRIBUTING.md ├── src ├── segments │ ├── prompt_ready.bash │ ├── direnv.bash │ ├── aws.bash │ ├── conda.bash │ ├── timestamp.bash │ ├── exit_code.bash │ ├── path_ro.bash │ ├── host.bash │ ├── nix.bash │ ├── python_env.bash │ ├── command.bash │ ├── path.bash │ ├── k8s.bash │ ├── load.bash │ ├── terraform.bash │ ├── wttr.bash │ ├── rescuetime.bash │ ├── git.bash │ └── README.md ├── colors │ ├── xresources.bash │ ├── default-256.bash │ ├── apathy.bash │ ├── default.bash │ ├── atlas.bash │ ├── solarized.bash │ ├── gruvebox.bash │ ├── monokai.bash │ ├── eighties.bash │ ├── ocean.bash │ ├── README.md │ └── template.ejs ├── hooks │ ├── README.md │ └── alert.bash ├── layouts │ ├── README.md │ ├── plain.bash │ ├── lines.bash │ └── powerline.bash ├── debug.bash ├── execute.bash ├── decorate.bash ├── main.bash ├── configure.bash ├── interact_themed.bash └── interact.bash ├── bin ├── run_tests ├── ci_test ├── try_me ├── ci_test_wrapper └── install ├── test ├── asserts.bash ├── segments │ ├── timestamp.bats │ ├── prompt_ready.bats │ ├── exit_code.bats │ ├── aws.bats │ ├── conda.bats │ ├── path_ro.bats │ ├── path.bats │ ├── python_env.bats │ ├── k8s.bats │ ├── load.bats │ ├── command.bats │ ├── wttr.bats │ ├── segment_helper.bash │ ├── rescuetime.bats │ ├── host.bats │ └── git.bats ├── src_helper.bash ├── hooks │ ├── hook_helper.bash │ └── alert.bats ├── decorate.bats ├── execute.bats └── configure.bats ├── config ├── colors.conf.template ├── settings.conf ├── settings.conf.template └── colors.conf ├── .shellcheckrc ├── Dockerfile ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── pull_request_verify.yaml ├── .pre-commit-config.yaml ├── LICENSE ├── sbp.bash └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | test-reports/ 2 | target/ 3 | tests/bauta 4 | -------------------------------------------------------------------------------- /resources/sbp_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brujoand/sbp/HEAD/resources/sbp_screenshot.png -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to SBP 2 | 3 | Fork, make change, run tests, make pr, profit! 4 | 5 | SBP is intended to be fast, adaptive and configurable. 6 | 7 | -------------------------------------------------------------------------------- /src/segments/prompt_ready.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | segments::prompt_ready() { 4 | print_themed_segment 'prompt_ready' "$SEGMENTS_PROMPT_READY_ICON" 5 | } 6 | -------------------------------------------------------------------------------- /bin/run_tests: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -e 4 | 5 | SBP_PATH=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) 6 | export SBP_PATH 7 | 8 | bats "${SBP_PATH}/test/" 9 | -------------------------------------------------------------------------------- /src/segments/direnv.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | segments::direnv() { 4 | if [[ -v DIRENV_DIR ]]; then 5 | print_themed_segment 'normal' "direnv" 6 | fi 7 | } 8 | -------------------------------------------------------------------------------- /src/segments/aws.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | segments::aws() { 4 | if [[ -n $AWS_DEFAULT_PROFILE ]]; then 5 | print_themed_segment 'normal' "$AWS_DEFAULT_PROFILE" 6 | fi 7 | } 8 | -------------------------------------------------------------------------------- /src/segments/conda.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | segments::conda() { 4 | if [[ -n $CONDA_DEFAULT_ENV ]]; then 5 | print_themed_segment 'normal' "$CONDA_DEFAULT_ENV" 6 | fi 7 | } 8 | -------------------------------------------------------------------------------- /bin/ci_test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | apk -U add git 4 | 5 | git clone https://github.com/bats-core/bats-core.git 6 | cd bats-core || exit 1 7 | ./install.sh /usr/local 8 | 9 | /bash/bin/run_tests 10 | -------------------------------------------------------------------------------- /src/segments/timestamp.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | segments::timestamp() { 4 | local timestamp_format=${SEGMENTS_TIMESTAMP_FORMAT:-'%H:%M:%S'} 5 | print_themed_segment 'normal' "$(date +"$timestamp_format")" 6 | } 7 | -------------------------------------------------------------------------------- /bin/try_me: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -e 4 | 5 | base_path=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) 6 | 7 | sudo docker build -t brujoand/sbp:local "$base_path" 8 | sudo docker run -it brujoand/sbp:local bash 9 | -------------------------------------------------------------------------------- /src/segments/exit_code.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | segments::exit_code() { 4 | if [[ $COMMAND_EXIT_CODE -ne 0 && $COMMAND_EXIT_CODE -ne 130 ]]; then 5 | print_themed_segment 'highlight' "$COMMAND_EXIT_CODE" 6 | fi 7 | } 8 | -------------------------------------------------------------------------------- /src/colors/xresources.bash: -------------------------------------------------------------------------------- 1 | color0="0" 2 | color1="8" 3 | color2="1" 4 | color3="9" 5 | color4="2" 6 | color5="10" 7 | color6="3" 8 | color7="11" 9 | color8="4" 10 | color9="12" 11 | color10="5" 12 | color11="13" 13 | color12="6" 14 | color13="14" 15 | color14="7" 16 | color15="15" 17 | -------------------------------------------------------------------------------- /src/colors/default-256.bash: -------------------------------------------------------------------------------- 1 | color0="0" 2 | color1="238" 3 | color2="8" 4 | color3="244" 5 | color4="250" 6 | color5="251" 7 | color6="252" 8 | color7="15" 9 | color8="196" 10 | color9="172" 11 | color10="148" 12 | color11="22" 13 | color12="99" 14 | color13="99" 15 | color14="31" 16 | color15="99" 17 | -------------------------------------------------------------------------------- /test/asserts.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | assert() { 4 | if ! "$@"; then 5 | echo "Assertion failed: $*" 6 | false 7 | fi 8 | } 9 | 10 | assert_equal() { 11 | if [[ $1 != "$2" ]]; then 12 | echo "expected: '$2'" 13 | echo "actual: '$1'" 14 | false 15 | fi 16 | } 17 | -------------------------------------------------------------------------------- /src/segments/path_ro.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | segment_value="${SEGMENT_PATH_RO_ICON:-}" 4 | 5 | segments::path_ro() { 6 | #TODO the character needs to be a setting 7 | if [[ ! -w $PWD ]]; then 8 | segment_value="" 9 | print_themed_segment 'normal' "$segment_value" 10 | fi 11 | } 12 | -------------------------------------------------------------------------------- /test/segments/timestamp.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load segment_helper 4 | 5 | @test "test a normal timestamp" { 6 | SEGMENTS_TIMESTAMP_FORMAT='%H:%M:%S' 7 | mapfile -t result <<<"$(execute_segment)" 8 | echo "${result[@]}" 9 | 10 | assert_equal "${#result[@]}" 2 11 | assert_equal "${result[0]}" 'normal' 12 | } 13 | -------------------------------------------------------------------------------- /config/colors.conf.template: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Color configuration for the segments. 4 | # See src/colors/*.bash for the color values 5 | # All segments colors have default values in src/config/colors.conf 6 | 7 | # This is how you override them: 8 | # SEGMENTS_TIMESTAMP_COLOR_PRIMARY=$color2 9 | # SEGMENTS_TIMESTAMP_COLOR_SECONDARY=$color5 10 | -------------------------------------------------------------------------------- /bin/ci_test_wrapper: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | bash_version=$1 4 | 5 | if [[ -z $bash_version ]]; then 6 | echo 'No bash version to test on' 7 | exit 1 8 | else 9 | echo "Ready to test bash version $bash_version" 10 | fi 11 | 12 | docker pull "bash:${bash_version}" 13 | docker run "bash:${bash_version}" --version 14 | docker run -v "${PWD}:/bash" "bash:${bash_version}" /bash/bin/ci_test 15 | -------------------------------------------------------------------------------- /src/colors/apathy.bash: -------------------------------------------------------------------------------- 1 | color0="3;26;22" 2 | color1="11;52;45" 3 | color2="24;78;69" 4 | color3="43;104;94" 5 | color4="95;156;146" 6 | color5="129;181;172" 7 | color6="167;206;200" 8 | color7="210;231;228" 9 | color8="62;150;136" 10 | color9="62;121;150" 11 | color10="62;76;150" 12 | color11="136;62;150" 13 | color12="150;62;76" 14 | color13="150;136;62" 15 | color14="76;150;62" 16 | color15="62;150;91" 17 | -------------------------------------------------------------------------------- /src/colors/default.bash: -------------------------------------------------------------------------------- 1 | color0="40;40;40" 2 | color1="60;56;54" 3 | color2="80;73;69" 4 | color3="102;92;84" 5 | color4="189;174;147" 6 | color5="213;196;161" 7 | color6="235;219;178" 8 | color7="251;241;199" 9 | color8="255;0;0" 10 | color9="215;135;0" 11 | color10="175;215;0" 12 | color11="0;130;0" 13 | color12="135;135;255" 14 | color13="135;95;255" 15 | color14="0;135;175" 16 | color15="161;105;70" 17 | -------------------------------------------------------------------------------- /src/colors/atlas.bash: -------------------------------------------------------------------------------- 1 | color0="0;38;53" 2 | color1="0;56;77" 3 | color2="81;127;141" 4 | color3="108;139;145" 5 | color4="134;150;150" 6 | color5="161;161;154" 7 | color6="230;230;220" 8 | color7="250;250;248" 9 | color8="255;90;103" 10 | color9="240;142;72" 11 | color10="255;204;27" 12 | color11="127;192;110" 13 | color12="20;116;126" 14 | color13="93;215;185" 15 | color14="154;112;164" 16 | color15="196;48;96" 17 | -------------------------------------------------------------------------------- /src/colors/solarized.bash: -------------------------------------------------------------------------------- 1 | color0="0;43;54" 2 | color1="7;54;66" 3 | color2="88;110;117" 4 | color3="101;123;131" 5 | color4="131;148;150" 6 | color5="147;161;161" 7 | color6="238;232;213" 8 | color7="253;246;227" 9 | color8="220;50;47" 10 | color9="203;75;22" 11 | color10="181;137;0" 12 | color11="133;153;0" 13 | color12="42;161;152" 14 | color13="38;139;210" 15 | color14="108;113;196" 16 | color15="211;54;130" 17 | -------------------------------------------------------------------------------- /src/colors/gruvebox.bash: -------------------------------------------------------------------------------- 1 | color0="40;40;40" 2 | color1="60;56;54" 3 | color2="80;73;69" 4 | color3="102;92;84" 5 | color4="189;174;147" 6 | color5="213;196;161" 7 | color6="235;219;178" 8 | color7="251;241;199" 9 | color8="251;73;52" 10 | color9="254;128;25" 11 | color10="250;189;47" 12 | color11="184;187;38" 13 | color12="142;192;124" 14 | color13="131;165;152" 15 | color14="211;134;155" 16 | color15="214;93;14" 17 | -------------------------------------------------------------------------------- /src/colors/monokai.bash: -------------------------------------------------------------------------------- 1 | color0="39;40;34" 2 | color1="56;56;48" 3 | color2="73;72;62" 4 | color3="117;113;94" 5 | color4="165;159;133" 6 | color5="248;248;242" 7 | color6="245;244;241" 8 | color7="249;248;245" 9 | color8="249;38;114" 10 | color9="253;151;31" 11 | color10="244;191;117" 12 | color11="166;226;46" 13 | color12="161;239;228" 14 | color13="102;217;239" 15 | color14="174;129;255" 16 | color15="204;102;51" 17 | -------------------------------------------------------------------------------- /src/segments/host.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | user_id="$(id -u)" 4 | 5 | segments::host() { 6 | 7 | if [[ -n $SSH_CLIENT ]]; then 8 | host_value="${USER}@${HOSTNAME}" 9 | else 10 | host_value="$USER" 11 | fi 12 | 13 | if [[ $user_id -eq 0 ]]; then 14 | print_themed_segment 'highlight' "$host_value" 15 | else 16 | print_themed_segment 'normal' "$host_value" 17 | fi 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/colors/eighties.bash: -------------------------------------------------------------------------------- 1 | color0="45;45;45" 2 | color1="57;57;57" 3 | color2="81;81;81" 4 | color3="116;115;105" 5 | color4="160;159;147" 6 | color5="211;208;200" 7 | color6="232;230;223" 8 | color7="242;240;236" 9 | color8="242;119;122" 10 | color9="249;145;87" 11 | color10="255;204;102" 12 | color11="153;204;153" 13 | color12="102;204;204" 14 | color13="102;153;204" 15 | color14="204;153;204" 16 | color15="210;123;83" 17 | -------------------------------------------------------------------------------- /src/colors/ocean.bash: -------------------------------------------------------------------------------- 1 | color0="43;48;59" 2 | color1="52;61;70" 3 | color2="79;91;102" 4 | color3="101;115;126" 5 | color4="167;173;186" 6 | color5="192;197;206" 7 | color6="223;225;232" 8 | color7="239;241;245" 9 | color8="191;97;106" 10 | color9="208;135;112" 11 | color10="235;203;139" 12 | color11="163;190;140" 13 | color12="150;181;180" 14 | color13="143;161;179" 15 | color14="180;142;173" 16 | color15="171;121;103" 17 | -------------------------------------------------------------------------------- /src/segments/nix.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | segments::nix() { 4 | # lorri also sets IN_NIX_SHELL but a real nix-shell sets bunch more variables we can use to differentiate the two 5 | # and only display the segment when we are in a real nix-shell which opens a nested bash which can be closed 6 | if [[ -n $IN_NIX_SHELL && -v name && -v system ]]; then 7 | print_themed_segment 'normal' "nix-shell" 8 | fi 9 | } 10 | -------------------------------------------------------------------------------- /test/segments/prompt_ready.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load segment_helper 4 | 5 | @test "test a normal prompt_ready segment" { 6 | export SEGMENTS_PROMPT_READY_VI_MODE 7 | export SEGMENTS_PROMPT_READY_ICON='x' 8 | mapfile -t result <<<"$(execute_segment)" 9 | 10 | assert_equal "${#result[@]}" 2 11 | assert_equal "${result[0]}" 'prompt_ready' 12 | assert_equal "${result[1]}" "$SEGMENTS_PROMPT_READY_ICON" 13 | } 14 | -------------------------------------------------------------------------------- /test/segments/exit_code.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load segment_helper 4 | 5 | @test "test a good exit_code segment" { 6 | result="$(execute_segment)" 7 | assert_equal "$result" '' 8 | } 9 | 10 | @test "test a bad command segment" { 11 | export COMMAND_EXIT_CODE=1 12 | mapfile -t result <<<"$(execute_segment)" 13 | assert_equal "${#result[@]}" 2 14 | assert_equal "${result[0]}" 'highlight' 15 | assert_equal "${result[1]}" '1' 16 | } 17 | -------------------------------------------------------------------------------- /test/src_helper.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | source "${SBP_PATH}/src/debug.bash" 3 | source "${SBP_PATH}/test/asserts.bash" 4 | 5 | SRC_NAME="$(basename "$BATS_TEST_FILENAME" .bats)" 6 | TMP_DIR=$(mktemp -d) && trap 'command rm -rf "$TMP_DIR"' EXIT 7 | 8 | source_src() { 9 | src_source="${SBP_PATH}/src/${SRC_NAME}.bash" 10 | 11 | if [[ ! -f $src_source ]]; then 12 | debug::log "Could not find $src_source" 13 | exit 1 14 | fi 15 | source "$src_source" 16 | } 17 | 18 | source_src 19 | -------------------------------------------------------------------------------- /src/segments/python_env.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | segments::python_env() { 4 | if [[ -n $VIRTUAL_ENV ]]; then 5 | segment_value="${VIRTUAL_ENV##*/}" 6 | else 7 | path=${PWD} 8 | while [[ $path ]]; do 9 | if [[ -f "${path}/.python-version" ]]; then 10 | read -r segment_value <"${path}/.python-version" 11 | break 12 | fi 13 | path=${path%/*} 14 | done 15 | fi 16 | 17 | if [[ -n $segment_value ]]; then 18 | print_themed_segment 'normal' "$segment_value" 19 | fi 20 | } 21 | -------------------------------------------------------------------------------- /test/segments/aws.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load segment_helper 4 | 5 | @test "test that we recognize an AWS profile" { 6 | export AWS_DEFAULT_PROFILE='my_account' 7 | mapfile -t result <<<"$(execute_segment)" 8 | assert_equal "${#result[@]}" 2 9 | assert_equal "${result[0]}" 'normal' 10 | assert_equal "${result[1]}" "$AWS_DEFAULT_PROFILE" 11 | } 12 | 13 | @test "test that we do nothing without an AWS profile" { 14 | unset AWS_DEFAULT_PROFILE 15 | output="$(execute_segment)" 16 | assert_equal "$output" '' 17 | } 18 | -------------------------------------------------------------------------------- /test/segments/conda.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load segment_helper 4 | 5 | @test "test that we recognize a Conda profile" { 6 | export CONDA_DEFAULT_ENV='my_env' 7 | mapfile -t result <<<"$(execute_segment)" 8 | assert_equal "${#result[@]}" 2 9 | assert_equal "${result[0]}" 'normal' 10 | assert_equal "${result[1]}" "$CONDA_DEFAULT_ENV" 11 | } 12 | 13 | @test "test that we do nothing without a Conda profile" { 14 | unset CONDA_DEFAULT_ENV 15 | local result 16 | result="$(execute_segment)" 17 | assert_equal "$result" '' 18 | } 19 | -------------------------------------------------------------------------------- /test/hooks/hook_helper.bash: -------------------------------------------------------------------------------- 1 | source "${SBP_PATH}/src/debug.bash" 2 | source "${SBP_PATH}/test/asserts.bash" 3 | 4 | export COMMAND_EXIT_CODE=0 5 | export COMMAND_DURATION=0 6 | HOOK_NAME="$(basename "$BATS_TEST_FILENAME" .bats)" 7 | TMP_DIR=$(mktemp -d) && trap 'command rm -rf "$TMP_DIR"' EXIT 8 | 9 | source_hook() { 10 | hook_source="${SBP_PATH}/src/hooks/${HOOK_NAME}.bash" 11 | 12 | if [[ ! -f $hook_source ]]; then 13 | debug::log "Could not find $hook_source" 14 | exit 1 15 | fi 16 | source "$hook_source" 17 | } 18 | 19 | execute_hook() { 20 | "hooks::${HOOK_NAME}" 21 | } 22 | 23 | source_hook 24 | -------------------------------------------------------------------------------- /test/segments/path_ro.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load segment_helper 4 | 5 | setup() { 6 | cd "$TMP_DIR" || exit 1 7 | } 8 | 9 | @test "test a normal path segment" { 10 | result="$(execute_segment)" 11 | assert_equal "$result" '' 12 | } 13 | 14 | @test "test a read only path segment" { 15 | # We need to run tests as non root first 16 | skip 17 | mkdir ro 18 | chmod 0555 ro 19 | cd ro 20 | export SEGMENT_PATH_RO_ICON=x 21 | mapfile -t result <<<"$(execute_segment)" 22 | assert_equal "${#result[@]}" 2 23 | assert_equal "${result[0]}" 'normal' 24 | assert_equal "${result[1]}" "$SEGMENT_PATH_RO_ICON" 25 | } 26 | -------------------------------------------------------------------------------- /.shellcheckrc: -------------------------------------------------------------------------------- 1 | # SC1091: Not following 2 | # https://github.com/koalaman/shellcheck/wiki/SC1091 3 | disable=SC1091 4 | 5 | # SC2034: foo appears unused. Verify it or export it. 6 | # https://www.shellcheck.net/wiki/SC2034 7 | disable=SC2034 8 | 9 | # SC1090: Can't follow non-constant source. Use a directive to specify location. 10 | # https://www.shellcheck.net/wiki/SC1090 11 | disable=SC1090 12 | 13 | # SC2030: Modification of var is local 14 | # https://www.shellcheck.net/wiki/SC2030 15 | disable=SC2030 16 | 17 | # SC2031: var was modified in a subshell. 18 | # https://github.com/koalaman/shellcheck/wiki/SC2031 19 | disable=SC2031 20 | -------------------------------------------------------------------------------- /test/hooks/alert.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load hook_helper 4 | 5 | hooks::alert_notify() { 6 | for argument in "${@}"; do 7 | [[ -z $argument ]] && continue 8 | printf '%s\n' "$argument" 9 | done 10 | } 11 | 12 | @test "test an okay alert hook" { 13 | HOOKS_ALERT_THRESHOLD=1 14 | COMMAND_DURATION=20 15 | COMMAND_EXIT_CODE=0 16 | 17 | mapfile -t result <<<"$(execute_hook)" 18 | expected_title="Command Succeded" 19 | expected_message="Time spent was ${COMMAND_DURATION}s" 20 | 21 | assert_equal "${#result[@]}" 2 22 | assert_equal "${result[0]}" "$expected_title" 23 | assert_equal "${result[1]}" "$expected_message" 24 | } 25 | -------------------------------------------------------------------------------- /src/segments/command.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | segments::command() { 4 | local timer_m=0 5 | local timer_s=0 6 | 7 | if [[ $COMMAND_EXIT_CODE -lt 0 || $COMMAND_EXIT_CODE -eq 130 ]]; then 8 | timer_m=0 9 | timer_s=0 10 | fi 11 | 12 | if [[ $COMMAND_DURATION -gt 0 ]]; then 13 | timer_m=$((COMMAND_DURATION / 60)) 14 | timer_s=$((COMMAND_DURATION % 60)) 15 | fi 16 | 17 | command_value="last: ${timer_m}m ${timer_s}s" 18 | 19 | if [[ $COMMAND_EXIT_CODE -gt 0 && $COMMAND_EXIT_CODE -ne 130 ]]; then 20 | print_themed_segment 'highlight' "$command_value" 21 | else 22 | print_themed_segment 'normal' "$command_value" 23 | fi 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | 5 | 6 | RUN apt-get update && \ 7 | echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections && \ 8 | apt-get install -y git bash curl apt-utils dialog vim 9 | 10 | RUN adduser --system --shell /bin/bash --disabled-password sbp && \ 11 | apt-get install -y locales && \ 12 | locale-gen en_US.UTF-8 && \ 13 | dpkg-reconfigure locales && \ 14 | update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 15 | 16 | copy . /sbp 17 | 18 | RUN chown -R sbp /sbp 19 | 20 | USER sbp 21 | 22 | ENV USER sbp 23 | ENV LC_ALL en_US.UTF-8 24 | 25 | WORKDIR /home/sbp 26 | 27 | RUN touch .bashrc && /sbp/bin/install 28 | -------------------------------------------------------------------------------- /test/segments/path.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load segment_helper 4 | 5 | setup() { 6 | cd "$TMP_DIR" || exit 1 7 | } 8 | 9 | @test "test a normal path segment" { 10 | mapfile -t result <<<"$(execute_segment)" 11 | dir_slashes="${TMP_DIR//[^\/]/}" 12 | dir_count="${#dir_slashes}" 13 | assert_equal "${#result[@]}" $((dir_count + 1)) 14 | assert_equal "${result[0]}" 'normal' 15 | } 16 | 17 | @test "test a non-split path segment" { 18 | export SEGMENTS_PATH_SPLITTER_DISABLE=1 19 | export SEGMENTS_MAX_LENGTH=99 20 | mapfile -t result <<<"$(execute_segment)" 21 | assert_equal "${#result[@]}" 2 22 | assert_equal "${result[0]}" 'normal' 23 | assert_equal "${result[1]}" "$TMP_DIR" 24 | } 25 | -------------------------------------------------------------------------------- /test/segments/python_env.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load segment_helper 4 | 5 | setup() { 6 | cd "$TMP_DIR" || exit 1 7 | } 8 | 9 | @test "test a variable based python_env segment" { 10 | VIRTUAL_ENV='3.5' 11 | mapfile -t result <<<"$(execute_segment)" 12 | 13 | assert_equal "${#result[@]}" 2 14 | assert_equal "${result[0]}" 'normal' 15 | assert_equal "${result[1]}" "$VIRTUAL_ENV" 16 | } 17 | 18 | @test "test a file based python_env segment" { 19 | version='3.5' 20 | echo "$version" >.python-version 21 | mapfile -t result <<<"$(execute_segment)" 22 | 23 | assert_equal "${#result[@]}" 2 24 | assert_equal "${result[0]}" 'normal' 25 | assert_equal "${result[1]}" "$version" 26 | } 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: local 4 | hooks: 5 | - id: shellcheck 6 | name: Test shell scripts with shellcheck 7 | description: Shell scripts conform to shellcheck 8 | entry: shellcheck 9 | language: system 10 | types: [shell] 11 | args: [-e, SC1091, -e, SC2034, -e, SC1090, -e, SC2030, -e, SC2031] 12 | - id: shfmt 13 | name: Check shell style with shfmt 14 | language: system 15 | entry: shfmt 16 | types: [shell] 17 | exclude_types: [csh, perl, python, ruby, tcsh, zsh] 18 | args: ['-i', '2', '-ci', '-s', '-w'] 19 | - repo: https://github.com/zricethezav/gitleaks 20 | rev: v8.13.0 21 | hooks: 22 | - id: gitleaks 23 | -------------------------------------------------------------------------------- /config/settings.conf: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Default themeing 4 | SBP_THEME_COLOR=${SBP_THEME_COLOR:-'default-256'} 5 | SBP_THEME_LAYOUT=${SBP_THEME_LAYOUT:-'plain'} 6 | 7 | # Default segment settings 8 | SEGMENTS_K8S_DEFAULT_USER=${SEGMENTS_K8S_DEFAULT_USER:-"$USER"} 9 | SEGMENTS_K8S_HIDE_CLUSTER=${SEGMENTS_K8S_HIDE_CLUSTER:-0} 10 | SEGMENTS_LOAD_THRESHOLD=${SEGMENTS_LOAD_THRESHOLD:-50} 11 | SEGMENTS_LOAD_THRESHOLD_HIGH=${SEGMENTS_LOAD_THRESHOLD_HIGH:-80} 12 | SEGMENTS_RESCUETIME_REFRESH_RATE=${SEGMENTS_RESCUETIME_REFRESH_RATE:-600} 13 | SEGMENTS_TIMESTAMP_FORMAT=${SEGMENTS_TIMESTAMP_FORMAT:-"%H:-%M:-%S"} 14 | SEGMENTS_WTTR_LOCATION=${SEGMENTS_WTTR_LOCATION:-'Oslo'} 15 | SEGMENTS_WTTR_FORMAT=${SEGMENTS_WTTR_FORMAT:-'%p;%t;%w'} 16 | -------------------------------------------------------------------------------- /bin/install: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | base_path=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) 4 | bashrc_path="$HOME"/.bashrc 5 | 6 | if [[ -z $SBP_PATH ]]; then 7 | echo "SBP: adding sbp_path=${base_path} to ~/.bashrc" 8 | echo "SBP_PATH=${base_path}" >>"$bashrc_path" 9 | else 10 | # shellcheck disable=SC2154 11 | echo "SBP: sbp_path has already been set to '$SBP_PATH'" 12 | fi 13 | 14 | if [[ -z $SBP_SOURCED ]]; then 15 | echo "SBP: adding 'source ${base_path}/sbp.bash' to ~/.bashrc" 16 | echo "source ${base_path}/sbp.bash" >>"$bashrc_path" 17 | else 18 | echo "${base_path}/sbp.bash has already been set to be sourced" 19 | fi 20 | 21 | echo "SBP: You'll have to reload bash or source ~/.bashrc for changes to take effect" 22 | -------------------------------------------------------------------------------- /src/colors/README.md: -------------------------------------------------------------------------------- 1 | # Colors 2 | 3 | ## Creating your own theme colors 4 | You can create your own theme layout by placing a file in: 5 | ``` 6 | ${HOME}/.config/sbp/themes/colors/${your_color_theme_name}.bash 7 | ``` 8 | 9 | Theme colors are basically 16 RGB values. And I've added a template 10 | `template.ejs` that can be used with [Base16 Builder](https://github.com/base16-builder/base16-builder) to generate a colors 11 | file from your favorite color scheme. For instance like so: 12 | ``` 13 | base16-builder --scheme 3024 --template themes/template.ejs --brightness dark > themes/colors/3024.bash 14 | ``` 15 | You can also set the colors to be `xresources` which will use whatever theme is 16 | set in your terminal/xresources config. 17 | -------------------------------------------------------------------------------- /test/segments/k8s.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load segment_helper 4 | 5 | setup() { 6 | export KUBE_CONFIG="${TMP_DIR}/config" 7 | cat <"$KUBE_CONFIG" 8 | current-context: project/k8s:443/sbp 9 | kind: Config 10 | preferences: {} 11 | users: 12 | - name: sbp/k8s:443 13 | user: 14 | token: some_token 15 | EOF 16 | } 17 | 18 | @test "test no config k8s segment" { 19 | command rm "$KUBE_CONFIG" 20 | result="$(execute_segment)" 21 | assert_equal "$result" '' 22 | } 23 | 24 | @test "test normal config k8s segment" { 25 | mapfile -t result <<<"$(execute_segment)" 26 | 27 | assert_equal "${#result[@]}" 2 28 | assert_equal "${result[0]}" 'normal' 29 | assert_equal "${result[1]}" 'sbp@k8s/project' 30 | } 31 | -------------------------------------------------------------------------------- /test/segments/load.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load segment_helper 4 | 5 | setup() { 6 | cd "$TMP_DIR" || exit 1 7 | } 8 | 9 | @test "that we can get cpu_cores" { 10 | result="$(segments::load_get_cpu_count)" 11 | 12 | [[ $result -gt 0 ]] 13 | } 14 | 15 | @test "that we can get load_avg" { 16 | result="$(segments::load_get_load_avg)" 17 | 18 | [[ -n $result ]] 19 | } 20 | 21 | @test "test a file based python_env segment" { 22 | segments::load_get_load_avg() { 23 | echo 0.25 24 | } 25 | 26 | segments::load_get_cpu_count() { 27 | echo 4 28 | } 29 | 30 | mapfile -t result <<<"$(execute_segment)" 31 | 32 | assert_equal "${#result[@]}" 2 33 | assert_equal "${result[0]}" 'normal' 34 | assert_equal "${result[1]}" "6" 35 | } 36 | -------------------------------------------------------------------------------- /src/segments/path.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | segments::path() { 4 | local segment_max_length=$SEGMENTS_MAX_LENGTH 5 | 6 | local wdir=${PWD/${HOME}/\~} 7 | 8 | if [[ ${#wdir} -gt $segment_max_length ]]; then 9 | folder=${wdir##*/} 10 | IFS='/' wdir=$(for p in ${wdir}; do printf '%s/' "${p:0:1}"; done) 11 | wdir="${wdir%/*}${folder:1}" 12 | fi 13 | 14 | IFS=/ read -r -a wdir_array <<<"${wdir}" 15 | if [[ $SEGMENTS_PATH_SPLITTER_DISABLE -ne 1 && ${#wdir_array[@]} -gt 1 ]]; then 16 | declare -a segments 17 | for dir in "${wdir_array[@]}"; do 18 | if [[ -n $dir ]]; then 19 | segments+=("$dir") 20 | fi 21 | done 22 | print_themed_segment 'normal' "${segments[@]}" 23 | else 24 | print_themed_segment 'normal' "$wdir" 25 | fi 26 | } 27 | -------------------------------------------------------------------------------- /src/segments/k8s.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | [[ -z $KUBECONFIG ]] && KUBECONFIG="${HOME}/.kube/config" 3 | 4 | segments::k8s() { 5 | [[ -f $KUBECONFIG ]] || return 0 6 | context="$(sed -n 's/.*current-context: \(.*\)/\1/p' "$KUBECONFIG")" 7 | [[ -z $context ]] && return 0 8 | namespace="$(pcregrep -M -- "- context:\n(^\s\s\w*.*\n)* name: ${context}" "${KUBECONFIG}" | sed -n 's/ *namespace: \(\w*\)/\1/p')" 9 | if [[ -z $namespace ]]; then 10 | namespace='default' 11 | fi 12 | 13 | local segment_icon_char="${SEGMENTS_K8S_ICON:-⎈}" 14 | 15 | if [[ -z $namespace || $SEGMENTS_K8S_HIDE_CLUSTER -eq 1 ]]; then 16 | segment="${segment_icon_char} ${context}" 17 | else 18 | segment="${segment_icon_char} ${context}/${namespace}" 19 | fi 20 | 21 | print_themed_segment 'normal' "${segment,,}" 22 | } 23 | -------------------------------------------------------------------------------- /test/segments/command.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load segment_helper 4 | 5 | @test "test a good command segment" { 6 | mapfile -t result <<<"$(execute_segment)" 7 | assert_equal "${#result[@]}" 2 8 | assert_equal "${result[0]}" 'normal' 9 | assert_equal "${result[1]}" 'last: 0m 0s' 10 | } 11 | 12 | @test "test a bad command segment" { 13 | export COMMAND_EXIT_CODE=1 14 | mapfile -t result <<<"$(execute_segment)" 15 | assert_equal "${#result[@]}" 2 16 | assert_equal "${result[0]}" 'highlight' 17 | assert_equal "${result[1]}" 'last: 0m 0s' 18 | } 19 | 20 | @test "test a long command segment" { 21 | export COMMAND_DURATION=99 22 | mapfile -t result <<<"$(execute_segment)" 23 | assert_equal "${#result[@]}" 2 24 | assert_equal "${result[0]}" 'normal' 25 | assert_equal "${result[1]}" 'last: 1m 39s' 26 | } 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment (please complete the following information):** 27 | - OS: [e.g. Linux] 28 | - Terminal [e.g. kitty, iTerm] 29 | - Version [e.g. 22] 30 | 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/workflows/pull_request_verify.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Pull request: verify" 3 | 4 | on: 5 | pull_request: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | pr-lint: 11 | name: "Lint check" 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: "Execute shell linting" 16 | uses: luizm/action-sh-checker@master 17 | env: 18 | SHFMT_OPTS: -i 2 -ci -s 19 | 20 | pr-test: 21 | name: "Pull request checks" 22 | runs-on: ubuntu-latest 23 | strategy: 24 | matrix: 25 | version: 26 | - rc 27 | - 5.0.17 28 | - 4.4.23 29 | - 4.3.48 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: "Execute unit tests" 33 | run: ./bin/ci_test_wrapper ${{ matrix.version }} 34 | shell: bash 35 | -------------------------------------------------------------------------------- /test/segments/wttr.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load segment_helper 4 | 5 | execute::execute_nohup_function() { 6 | "$@" 7 | } 8 | 9 | segments::wttr_fetch_changes() { 10 | echo -e '0.1mm\n+16°C\n↓13km/h' 11 | } 12 | 13 | @test "test parsing the wttr segment" { 14 | SEGMENT_CACHE="${TMP_DIR}/wttr" 15 | stats="$(segments::wttr_fetch_changes)" 16 | echo "$stats" >"$SEGMENT_CACHE" 17 | mapfile -t result <<<"$(execute_segment)" 18 | 19 | assert_equal "${#result[@]}" 4 20 | assert_equal "${result[0]}" 'normal' 21 | assert_equal "${result[1]}" "0.1mm" 22 | assert_equal "${result[2]}" "+16°C" 23 | assert_equal "${result[3]}" "↓13km/h" 24 | } 25 | 26 | @test "test refreshing the wttr segment" { 27 | SEGMENT_CACHE="${TMP_DIR}/wttr" 28 | command rm -rf "$SEGMENT_CACHE" 29 | execute_segment 30 | [[ -f $SEGMENT_CACHE ]] 31 | 32 | } 33 | -------------------------------------------------------------------------------- /test/segments/segment_helper.bash: -------------------------------------------------------------------------------- 1 | source "${SBP_PATH}/src/debug.bash" 2 | source "${SBP_PATH}/test/asserts.bash" 3 | 4 | export COMMAND_EXIT_CODE=0 5 | export COMMAND_DURATION=0 6 | SEGMENT_NAME="$(basename "$BATS_TEST_FILENAME" .bats)" 7 | TMP_DIR=$(mktemp -d) && trap 'command rm -rf "$TMP_DIR"' EXIT 8 | 9 | source_segment() { 10 | segment_source="${SBP_PATH}/src/segments/${SEGMENT_NAME}.bash" 11 | 12 | if [[ ! -f $segment_source ]]; then 13 | debug::log "Could not find $segment_source" 14 | return 1 15 | fi 16 | source "$segment_source" 17 | } 18 | 19 | execute_segment() { 20 | "segments::${SEGMENT_NAME}" 21 | } 22 | 23 | print_themed_segment() { 24 | for argument in "${@}"; do 25 | [[ -z $argument ]] && continue 26 | printf '%s\n' "$argument" 27 | done 28 | } 29 | 30 | export -f print_themed_segment 31 | 32 | source_segment 33 | -------------------------------------------------------------------------------- /test/segments/rescuetime.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load segment_helper 4 | 5 | RESCUETIME_API_KEY=password 6 | 7 | execute::execute_nohup_function() { 8 | "$@" 9 | } 10 | 11 | segments::rescuetime_fetch_changes() { 12 | cat <"$SEGMENT_CACHE" 24 | mapfile -t result <<<"$(execute_segment)" 25 | 26 | assert_equal "${#result[@]}" 3 27 | assert_equal "${result[0]}" 'normal' 28 | assert_equal "${result[1]}" "77%" 29 | assert_equal "${result[2]}" "3h:20m" 30 | } 31 | 32 | @test "test a refreshing the rescuetime segment" { 33 | SEGMENT_CACHE="${TMP_DIR}/rescuetime" 34 | command rm -rf "$SEGMENT_CACHE" 35 | RESCUETIME_ENDPOINT="http://localhost:8080" 36 | 37 | execute_segment 38 | [[ -f $SEGMENT_CACHE ]] 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/hooks/README.md: -------------------------------------------------------------------------------- 1 | # Hooks 2 | 3 | Hooks are executed asynchronously and are meant to provide a way of either 4 | alerting the user, or running other non segment related tasks. 5 | 6 | ## Creating your own hooks 7 | You can create your own hooks by placing a file in: 8 | ``` 9 | ${HOME}/.config/sbp/hooks/${your_hooks_name}.bash 10 | ``` 11 | 12 | You'll also want to add your hooks in the ´SBP_HOOKS´ variable, in the settings. 13 | 14 | Your script will be sourced and executed with the following env variables: 15 | ``` 16 | - COMMAND_EXIT_CODE, the exit code of the privous shell command 17 | - COMMAND_DURATION, the duration of the shell command 18 | - SBP_TMP, a tmp folder which is local to your shell PID and cleaned upon exit 19 | - SBP_CACHE, a cache folder which is global to all SBP processes 20 | - SBP_PATH, the path to the SBP diectory 21 | ``` 22 | 23 | These are the provided hooks: 24 | - Alert; Creates a notification if the previous command took longer than x 25 | seconds. 26 | -------------------------------------------------------------------------------- /test/segments/host.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load segment_helper 4 | 5 | @test "test that we recognize a normal user" { 6 | unset SSH_CLIENT 7 | export user_id=1000 8 | USER=${USER:-travis} 9 | mapfile -t result <<<"$(execute_segment)" 10 | assert_equal "${#result[@]}" 2 11 | assert_equal "${result[0]}" 'normal' 12 | assert_equal "${result[1]}" "$USER" 13 | } 14 | 15 | @test "test that we recognize an ssh session" { 16 | export SSH_CLIENT=yes 17 | export user_id=1000 18 | USER=${USER:-travis} 19 | mapfile -t result <<<"$(execute_segment)" 20 | assert_equal "${#result[@]}" 2 21 | assert_equal "${result[0]}" 'normal' 22 | assert_equal "${result[1]}" "${USER}@${HOSTNAME}" 23 | } 24 | 25 | @test "test that we recognize the root user" { 26 | unset SSH_CLIENT 27 | export user_id=0 28 | USER=${USER:-travis} 29 | mapfile -t result <<<"$(execute_segment)" 30 | assert_equal "${#result[@]}" 2 31 | assert_equal "${result[0]}" 'highlight' 32 | assert_equal "${result[1]}" "$USER" 33 | } 34 | -------------------------------------------------------------------------------- /src/layouts/README.md: -------------------------------------------------------------------------------- 1 | # Layouts 2 | 3 | Layouts are the last step to handle the segments before they are concatenated 4 | into the PS1 prompt variable. 5 | 6 | Layouts will receive a ´segment_type´ as the first argument, this can be: 7 | - normal; a regular segment 8 | - highlight; a segment that will use the highlighted colors 9 | - filler; The segment separating left from right side of the prompt 10 | - prompt_ready; The segment printed immediately before the cursor 11 | 12 | Each layout is responsible for applying colors, spacing and other formatting 13 | and it should be completely detached from the main logic so a layout change 14 | should never break a working prompt. 15 | 16 | ## Creating your own layout 17 | You can create your own hooks by placing a file in: 18 | ``` 19 | ${HOME}/.config/sbp/layouts/${your_layouts_name}.bash 20 | ``` 21 | 22 | Your script will be sourced and executed with the following env variables: 23 | ``` 24 | - PRIMARY_COLOR 25 | - SECONDARY_COLOR 26 | - PRIMARY_COLOR_HIGHLIGHT 27 | - SECONDARY_COLOR_HIGHLIGHT 28 | - SPLITTER_COLOR 29 | ``` 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Anders Brujordet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/settings.conf.template: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | SBP_THEME_COLOR='default-256' 3 | SBP_THEME_LAYOUT='plain' 4 | 5 | # Hooks will run once before every prompt 6 | # Run 'sbp list hooks' to list all available hooks 7 | SBP_HOOKS=('alert') 8 | 9 | # Segments are generated before each prompt and can 10 | # be added, removed and reordered 11 | # Run 'sbp list segments' to list all available segments 12 | # Maybe you don't want to run all segments when in 13 | # a small window? 14 | 15 | if [[ "$COLUMNS" -le 120 ]]; then 16 | # Let's adjust to the smaller screen 17 | SBP_THEME_LAYOUT='lines' 18 | SBP_SEGMENTS_LEFT=('path' 'python_env' 'git' 'command') 19 | else 20 | SBP_SEGMENTS_LEFT=('host' 'path' 'python_env' 'k8s' 'git' 'nix') 21 | SBP_SEGMENTS_RIGHT=('command' 'timestamp') 22 | SBP_SEGMENTS_LINE_TWO=('prompt_ready') 23 | fi 24 | 25 | # Segment specific settings 26 | SEGMENTS_K8S_DEFAULT_USER="$USER" 27 | SEGMENTS_K8S_HIDE_CLUSTER=0 28 | SEGMENTS_LOAD_THRESHOLD=50 29 | SEGMENTS_LOAD_THRESHOLD_HIGH=80 30 | SEGMENTS_RESCUETIME_REFRESH_RATE=600 31 | SEGMENTS_TIMESTAMP_FORMAT="%H:%M:%S" 32 | SEGMENTS_WTTR_LOCATION='Oslo' 33 | SEGMENTS_WTTR_FORMAT='%p;%t;%w' 34 | 35 | # vim: set ft=bash: 36 | -------------------------------------------------------------------------------- /src/debug.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | debug::log() { 4 | local timestamp file function 5 | printf -v timestamp '%(%y.%m.%d %H:%M:%S)T' -1 6 | file="${BASH_SOURCE[1]##*/}" 7 | function="${FUNCNAME[1]}" 8 | printf >&2 '\n[%s] [%s - %s]: \e[31m%s\e[0m\n' "$timestamp" "$file" "$function" "${*}" 9 | } 10 | 11 | if [[ ${EPOCHREALTIME-} ]]; then 12 | date_cmd=EPOCHREALTIME 13 | else 14 | if [[ $OSTYPE == "darwin"* ]]; then 15 | if type -P gdate &>/dev/null; then 16 | date_cmd='gdate' 17 | fi 18 | else 19 | date_cmd='date' 20 | fi 21 | fi 22 | 23 | debug::get_clock() { 24 | if [[ $date_cmd == EPOCHREALTIME ]]; then 25 | printf -v "$1" %s "${EPOCHREALTIME//[!0-9]/}" 26 | printf -v "$1" %s "${!1%???}" # strip 3 digits of microsec resolution 27 | else 28 | printf -v "$1" %s "$("$date_cmd" +'%s%3N')" 29 | fi 30 | } 31 | 32 | debug::start_timer() { 33 | debug::get_clock timer_start 34 | } 35 | 36 | debug::tick_timer() { 37 | [[ -z $date_cmd ]] && return 0 38 | local timer_stop timer_spent 39 | debug::get_clock timer_stop 40 | # shellcheck disable=SC2154 41 | timer_spent=$((timer_stop - timer_start)) 42 | echo >&2 "${timer_spent}ms: $1" 43 | debug::get_clock timer_start 44 | } 45 | -------------------------------------------------------------------------------- /src/hooks/alert.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | HOOKS_ALERT_THRESHOLD="${HOOKS_ALERT_THRESHOLD:-60}" 4 | 5 | hooks::alert_notify() { 6 | [[ -z $2 ]] && return 7 | 8 | title=$1 9 | message=$2 10 | 11 | if type terminal-notifier &>/dev/null; then 12 | (terminal-notifier -title "$title" -message "$message" &) 13 | elif [[ "$(uname -s)" == "Darwin" ]]; then 14 | osascript -e "display notification \"$message\" with title \"$title\"" 15 | elif type ntfy &>/dev/null; then 16 | if [[ -z $HOOKS_ALERT_NTFY_BACKEND ]]; then 17 | (ntfy -t "$title" send "$message" &) 18 | else 19 | (ntfy -b "$HOOKS_ALERT_NTFY_BACKEND" -t "$title" send "$message" &) 20 | fi 21 | elif type notify-send &>/dev/null; then 22 | (notify-send --icon=terminal "$title" "$message" &) 23 | fi 24 | } 25 | 26 | hooks::alert() { 27 | [[ $COMMAND_EXIT_CODE -lt 0 ]] && return 28 | if [[ $HOOKS_ALERT_THRESHOLD -le $COMMAND_DURATION ]]; then 29 | local title message 30 | 31 | if [[ $COMMAND_EXIT_CODE -eq "0" ]]; then 32 | title="Command Succeded" 33 | message="Time spent was ${COMMAND_DURATION}s" 34 | else 35 | title="Command Failed" 36 | message="Time spent was ${COMMAND_DURATION}s" 37 | fi 38 | 39 | hooks::alert_notify "$title" "$message" 40 | fi 41 | } 42 | -------------------------------------------------------------------------------- /src/segments/load.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | shopt -s extglob 4 | 5 | segments::load_get_cpu_count() { 6 | [[ -z $cpu_count ]] && cpu_count=$(nproc 2>/dev/null) 7 | [[ -z $cpu_count ]] && cpu_count=$(sysctl -n hw.ncpu) 8 | [[ -z $cpu_count ]] && cpu_count=$(\grep -c '^[Pp]rocessor' /proc/cpuinfo) 9 | 10 | if [[ $cpu_count -gt 0 ]]; then 11 | printf '%s\n' "$cpu_count" 12 | fi 13 | } 14 | 15 | segments::load_get_load_avg() { 16 | load_avg="$(LC_ALL=C sysctl -n vm.loadavg 2>/dev/null | cut -d ' ' -f 2)" 17 | if [[ -n $load_avg ]]; then 18 | printf '%s\n' "$load_avg" 19 | return 0 20 | fi 21 | 22 | local eol 23 | read -r load_avg eol 2 | color00="<%- base["00"]["rgb"][0] %>;<%- base["00"]["rgb"][1] %>;<%- base["00"]["rgb"][2] %>" 3 | color01="<%- base["01"]["rgb"][0] %>;<%- base["01"]["rgb"][1] %>;<%- base["01"]["rgb"][2] %>" 4 | color02="<%- base["02"]["rgb"][0] %>;<%- base["02"]["rgb"][1] %>;<%- base["02"]["rgb"][2] %>" 5 | color03="<%- base["03"]["rgb"][0] %>;<%- base["03"]["rgb"][1] %>;<%- base["03"]["rgb"][2] %>" 6 | color04="<%- base["04"]["rgb"][0] %>;<%- base["04"]["rgb"][1] %>;<%- base["04"]["rgb"][2] %>" 7 | color05="<%- base["05"]["rgb"][0] %>;<%- base["05"]["rgb"][1] %>;<%- base["05"]["rgb"][2] %>" 8 | color06="<%- base["06"]["rgb"][0] %>;<%- base["06"]["rgb"][1] %>;<%- base["06"]["rgb"][2] %>" 9 | color07="<%- base["07"]["rgb"][0] %>;<%- base["07"]["rgb"][1] %>;<%- base["07"]["rgb"][2] %>" 10 | color08="<%- base["08"]["rgb"][0] %>;<%- base["08"]["rgb"][1] %>;<%- base["08"]["rgb"][2] %>" 11 | color09="<%- base["09"]["rgb"][0] %>;<%- base["09"]["rgb"][1] %>;<%- base["09"]["rgb"][2] %>" 12 | color0A="<%- base["0A"]["rgb"][0] %>;<%- base["0A"]["rgb"][1] %>;<%- base["0A"]["rgb"][2] %>" 13 | color0B="<%- base["0B"]["rgb"][0] %>;<%- base["0B"]["rgb"][1] %>;<%- base["0B"]["rgb"][2] %>" 14 | color0C="<%- base["0C"]["rgb"][0] %>;<%- base["0C"]["rgb"][1] %>;<%- base["0C"]["rgb"][2] %>" 15 | color0D="<%- base["0D"]["rgb"][0] %>;<%- base["0D"]["rgb"][1] %>;<%- base["0D"]["rgb"][2] %>" 16 | color0E="<%- base["0E"]["rgb"][0] %>;<%- base["0E"]["rgb"][1] %>;<%- base["0E"]["rgb"][2] %>" 17 | color0F="<%- base["0F"]["rgb"][0] %>;<%- base["0F"]["rgb"][1] %>;<%- base["0F"]["rgb"][2] %>" 18 | -------------------------------------------------------------------------------- /test/segments/git.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load segment_helper 4 | 5 | setup() { 6 | export SEGMENTS_MAX_LENGTH=99 7 | export SEGMENTS_GIT_ICON='' 8 | 9 | cd "$TMP_DIR" || exit 1 10 | git init &>/dev/null 11 | git config user.name sbp 12 | git config user.email sbp@sbp.sbp 13 | touch readme 14 | git add readme 15 | git commit -am "inital commit" &>/dev/null 16 | } 17 | 18 | @test "test a clean master" { 19 | mapfile -t result <<<"$(execute_segment)" 20 | assert_equal "${#result[@]}" 2 21 | assert_equal "${result[0]}" 'normal' 22 | assert_equal "${result[1]}" 'master' 23 | } 24 | 25 | @test "test untracked git segment" { 26 | touch this and that 27 | mapfile -t result <<<"$(execute_segment)" 28 | assert_equal "${#result[@]}" 3 29 | assert_equal "${result[0]}" 'normal' 30 | assert_equal "${result[1]}" '?3' 31 | assert_equal "${result[2]}" 'master' 32 | } 33 | 34 | @test "test commited git segment" { 35 | touch this and that 36 | git add . &>/dev/null 37 | 38 | mapfile -t result <<<"$(execute_segment)" 39 | echo "${result[@]}" 40 | assert_equal "${#result[@]}" 3 41 | assert_equal "${result[0]}" 'normal' 42 | assert_equal "${result[1]}" '+3' 43 | assert_equal "${result[2]}" 'master' 44 | } 45 | 46 | @test "test we can use an icon with git segment" { 47 | export SEGMENTS_GIT_ICON='@' 48 | touch this and that 49 | git add . &>/dev/null 50 | 51 | mapfile -t result <<<"$(execute_segment)" 52 | echo "${result[@]}" 53 | assert_equal "${#result[@]}" 4 54 | assert_equal "${result[0]}" 'normal' 55 | assert_equal "${result[1]}" '+3' 56 | assert_equal "${result[2]}" "$SEGMENTS_GIT_ICON" 57 | assert_equal "${result[3]}" 'master' 58 | } 59 | -------------------------------------------------------------------------------- /src/segments/terraform.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | if [[ ${SEGMENTS_USE_NERDFONTS} == 1 ]]; then 4 | # use Terraform icon from Nerdfonts 5 | tf_icon="" 6 | else 7 | tf_icon="${SEGMENTS_TF_ICON:-⊤}" 8 | fi 9 | 10 | tf_extension="${TF_FILE_EXTENSION:-tf}" 11 | declare tf_version="" 12 | declare -a scanned_dirs=() 13 | 14 | # To configure more than one dir to be scanned 15 | # an array is necessary so we get values expanded 16 | # to real arguments when calling find later 17 | if [[ -n ${SEGMENTS_TF_SCANNED_DIRS} ]]; then 18 | scanned_dirs=("${SEGMENTS_TF_SCANNED_DIRS[@]}") 19 | else 20 | scanned_dirs=("$PWD") 21 | fi 22 | 23 | _tf_available() { 24 | command -v terraform 25 | } 26 | 27 | _get_terraform_version() { 28 | # when version managers are available, parse their 29 | # config files first before calling 'terraform version' 30 | 31 | # check for version files created by 'tfenv' 32 | if [[ $(command -v tfenv) ]] && [[ -f $PWD/.terraform-version ]]; then 33 | tf_version="$(tr -d '\n' <.terraform-version)" 34 | 35 | # check for version files created by 'asdf' 36 | elif [[ $(command -v asdf) ]] && [[ -f $PWD/.tool-versions ]] && [[ "$(<.tool-versions)" =~ terraform.([0-9.]+) ]]; then 37 | tf_version="${BASH_REMATCH[1]}" 38 | 39 | # get version from terraform directly (slowest) 40 | elif [[ $(_tf_available) ]] && [[ "$(terraform -v)" =~ v([0-9.]+) ]]; then 41 | tf_version="${BASH_REMATCH[1]}" 42 | fi 43 | } 44 | 45 | segments::terraform() { 46 | if [[ $(_tf_available) ]] && [[ -n $(find "${scanned_dirs[@]}" -maxdepth 1 -name "*.${tf_extension}" -print -quit 2>/dev/null) ]]; then 47 | _get_terraform_version 48 | segment="$tf_icon $tf_version" 49 | print_themed_segment 'normal' "$segment" 50 | fi 51 | } 52 | -------------------------------------------------------------------------------- /src/segments/wttr.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | location=${SEGMENTS_WTTR_LOCATION:-'Oslo'} 4 | format=${SEGMENTS_WTTR_FORMAT:-'%p;%t;%w'} 5 | refresh_rate="${SEGMENTS_WTTR_REFRESH_RATE:-600}" 6 | 7 | segments::wttr_fetch_changes() { 8 | web=$( 9 | curl -s \ 10 | -H "Accept-Language: ${LANG%_*}" \ 11 | --compressed "wttr.in/${location}?format=${format}" 12 | exit $? 13 | ) 14 | rtn_code=$? 15 | 16 | debug::log "wttr fetch return code $rtn_code" 17 | debug::log "wttr fetch content $web" 18 | 19 | # shellcheck disable=SC2181 20 | if [[ $rtn_code == 0 && ! $web =~ "500 Internal Server Error" && ! $web =~ "502 Bad Gateway" ]]; then 21 | echo "$web" | tr -d '\n' | tr ';' '\n' 22 | fi 23 | } 24 | 25 | segments::wttr_refresh() { 26 | if [[ ! -f $SEGMENT_CACHE ]]; then 27 | debug::log "No cache folder" 28 | fi 29 | 30 | if [[ -f $SEGMENT_CACHE ]]; then 31 | if [[ $OSTYPE =~ darwin || $(uname) == Darwin ]]; then 32 | last_update=$(stat -f "%m" "$SEGMENT_CACHE") 33 | else 34 | last_update=$(stat -c "%Y" "$SEGMENT_CACHE") 35 | fi 36 | else 37 | last_update=0 38 | fi 39 | 40 | local current_time 41 | _sbp_get_current_time current_time 42 | time_since_update=$((current_time - last_update)) 43 | 44 | if [[ $time_since_update -lt $refresh_rate ]]; then 45 | return 0 46 | fi 47 | 48 | weather_data=$(segments::wttr_fetch_changes) 49 | 50 | if [[ -n $weather_data ]]; then 51 | echo "$weather_data" >"$SEGMENT_CACHE" 52 | fi 53 | } 54 | 55 | segments::wttr() { 56 | if [[ -f $SEGMENT_CACHE ]]; then 57 | mapfile -t result <"$SEGMENT_CACHE" 58 | print_themed_segment 'normal' "${result[@]}" 59 | fi 60 | execute::execute_nohup_function segments::wttr_refresh 61 | } 62 | -------------------------------------------------------------------------------- /test/execute.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load src_helper 4 | 5 | setup() { 6 | export SBP_CONFIG="${TMP_DIR}/local_config" 7 | export SBP_STATE="${TMP_DIR}/local_state" 8 | mkdir -p "$SBP_CONFIG"/{hooks,segments,peekabo} 9 | export SBP_STATE="${TMP_DIR}/local_state" 10 | mkdir -p "$SBP_STATE/log" 11 | } 12 | 13 | # Helpers that override SBP functions 14 | configure::get_feature_file() { 15 | local -n get_feature_file_result=$1 16 | local feature_type=$2 17 | local feature_name=$3 18 | 19 | get_feature_file_result="${SBP_CONFIG}/${feature_type}s/${feature_name}.bash" 20 | } 21 | 22 | segments::test_segment() { 23 | printf '%s\n%s\n%s\n%s\n%s' "$PRIMARY_COLOR" "$SECONDARY_COLOR" "$SPLITTER_COLOR" "$SEGMENTS_MAX_LENGTH" "$SEGMENT_POSITION" 24 | } 25 | 26 | @test "test that we can execute prompt hooks" { 27 | pipe_name="${SBP_CONFIG}/pipe" 28 | mkfifo "$pipe_name" 29 | 30 | export SBP_HOOKS=('alert') 31 | echo "hooks::alert() { echo 'success' > $pipe_name; }" >"${SBP_CONFIG}/hooks/alert.bash" 32 | execute::execute_prompt_hooks 33 | read -r result <"$pipe_name" 34 | assert_equal "$result" 'success' 35 | 36 | # Check that the hook log file was created 37 | assert [ -f "${SBP_STATE}/log/hook.log" ] 38 | } 39 | 40 | @test "test that we pass the correct environment variables to segments" { 41 | # Segment configuration variables 42 | SEGMENTS_TEST_SEGMENT_COLOR_PRIMARY=0 43 | SEGMENTS_TEST_SEGMENT_COLOR_SECONDARY=1 44 | SEGMENTS_TEST_SEGMENT_COLOR_SPLITTER=2 45 | SEGMENTS_TEST_SEGMENT_MAX_LENGTH=3 46 | 47 | echo "true" >"${SBP_CONFIG}/segments/test_segment.bash" 48 | mapfile -t result <<<"$(execute::execute_prompt_segment 'test_segment' 'left')" 49 | assert_equal "${result[0]}" '0' 50 | assert_equal "${result[1]}" '1' 51 | assert_equal "${result[2]}" '2' 52 | assert_equal "${result[3]}" '3' 53 | assert_equal "${result[4]}" 'left' 54 | } 55 | -------------------------------------------------------------------------------- /src/execute.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # TODO replace with call to configure::get feature file 4 | # and check for peekaboo in the segment execution 5 | execute::get_script() { 6 | local -n get_script_result=$1 7 | local feature_type=$2 8 | local feature_name=$3 9 | 10 | if [[ -f "${SBP_CONFIG}/peekaboo/${feature_name}" ]]; then 11 | return 0 12 | fi 13 | 14 | local feature_file 15 | configure::get_feature_file 'feature_file' "$feature_type" "$feature_name" 16 | get_script_result="$feature_file" 17 | } 18 | 19 | execute::execute_nohup_function() { 20 | ( 21 | trap '' HUP INT 22 | "$@" 23 | ) >"${SBP_STATE}/log/hook.log" & 24 | } 25 | 26 | execute::execute_prompt_hooks() { 27 | local hook_script 28 | for hook in "${SBP_HOOKS[@]}"; do 29 | execute::get_script 'hook_script' 'hook' "$hook" 30 | 31 | if [[ -f $hook_script ]]; then 32 | source "$hook_script" 33 | execute::execute_nohup_function "hooks::${hook}" 34 | fi 35 | done 36 | } 37 | 38 | execute::execute_prompt_segment() { 39 | local segment=$1 40 | local SEGMENT_POSITION=$2 41 | local SEGMENT_CACHE="${SBP_CACHE}/${segment}" 42 | 43 | local segment_script 44 | execute::get_script 'segment_script' 'segment' "$segment" 45 | 46 | if [[ -f $segment_script ]]; then 47 | source "$segment_script" 48 | 49 | local -n PRIMARY_COLOR="SEGMENTS_${segment^^}_COLOR_PRIMARY" 50 | local -n SECONDARY_COLOR="SEGMENTS_${segment^^}_COLOR_SECONDARY" 51 | 52 | local -n PRIMARY_COLOR_HIGHLIGHT="SEGMENTS_${segment^^}_COLOR_PRIMARY_HIGHLIGHT" 53 | local -n SECONDARY_COLOR_HIGHLIGHT="SEGMENTS_${segment^^}_COLOR_SECONDARY_HIGHLIGHT" 54 | 55 | local -n SPLITTER_COLOR="SEGMENTS_${segment^^}_COLOR_SPLITTER" 56 | 57 | local -n max_length_override="SEGMENTS_${segment^^}_MAX_LENGTH" 58 | if [[ -n $max_length_override ]]; then 59 | SEGMENTS_MAX_LENGTH="$max_length_override" 60 | fi 61 | 62 | "segments::${segment}" 63 | fi 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/decorate.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | decorate::calculate_complementing_color() { 4 | local -n calculate_complementing_color_result=$1 5 | local source_color=$2 6 | input_colors=() 7 | 8 | if [[ -z ${source_color//[0123456789]/} ]]; then 9 | # This is not accurate 10 | calculate_complementing_color_result="$((255 - source_color))" 11 | else 12 | mapfile -t input_colors < <(tr ';' '\n' <<<"$source_color") 13 | local red=${input_colors[0]} 14 | local green=${input_colors[1]} 15 | local blue=${input_colors[2]} 16 | 17 | red=$((red * 2126)) 18 | green=$((green * 7152)) 19 | blue=$((blue * 722)) 20 | 21 | lum=$((red + green + blue)) 22 | if [[ $lum -gt 1400000 ]]; then 23 | calculate_complementing_color_result='0;0;0' 24 | else 25 | calculate_complementing_color_result='255;255;255' 26 | fi 27 | fi 28 | } 29 | 30 | decorate::print_colors() { 31 | local -n print_colors_result=$1 32 | local fg_code=$2 33 | local bg_code=$3 34 | local escaped=${4} 35 | local fg_color bg_color 36 | 37 | decorate::print_fg_color 'fg_color' "$fg_code" "$escaped" 38 | decorate::print_bg_color 'bg_color' "$bg_code" "$escaped" 39 | print_colors_result="${fg_color}${bg_color}" 40 | } 41 | 42 | decorate::print_bg_color() { 43 | local -n print_bg_color_result=$1 44 | local bg_code=$2 45 | local escaped=${3:-'true'} 46 | 47 | decorate::format_color 'print_bg_color_result' 48 "$bg_code" "$escaped" 48 | } 49 | 50 | decorate::print_fg_color() { 51 | local -n print_fg_color_result=$1 52 | local fg_code=$2 53 | local escaped=${3:-'true'} 54 | 55 | decorate::format_color 'print_fg_color_result' 38 "$fg_code" "$escaped" 56 | } 57 | 58 | decorate::format_color() { 59 | local -n format_color_result=$1 60 | local color_code=$2 61 | local color_value=$3 62 | local escaped=$4 63 | local color_reset_code=$((color_code + 1)) 64 | 65 | if [[ -z $color_value ]]; then 66 | format_color_result="\e[${color_reset_code}m" 67 | elif [[ -z ${color_value//[0123456789]/} ]]; then 68 | format_color_result="\e[${color_code};5;${color_value}m" 69 | else 70 | format_color_result="\e[${color_code};2;${color_value}m" 71 | fi 72 | 73 | if [[ $escaped == 'true' ]]; then 74 | format_color_result="\[${format_color_result}\]" 75 | fi 76 | } 77 | -------------------------------------------------------------------------------- /src/segments/rescuetime.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | RESCUETIME_ENDPOINT="https://www.rescuetime.com/anapi/data?key=${RESCUETIME_API_KEY}&format=csv&rs=day&rk=productivity" 4 | 5 | segments::rescuetime_fetch_changes() { 6 | result=$(curl -s "$RESCUETIME_ENDPOINT" | grep -v '^Rank') 7 | exit_code=$? 8 | if [[ $exit_code -gt 0 ]]; then 9 | debug::log "Could not reach rescuetime RESCUETIME_ENDPOINT" 10 | return 0 11 | fi 12 | echo "$result" 13 | } 14 | 15 | segments::rescuetime_refresh() { 16 | refresh_rate="${SEGMENTS_RESCUETIME_REFRESH_RATE:-600}" 17 | if [[ ! -f $SEGMENT_CACHE ]]; then 18 | debug::log "No cache folder" 19 | fi 20 | 21 | if [[ -f $SEGMENT_CACHE ]]; then 22 | last_update=$(stat -f "%m" "$SEGMENT_CACHE") 23 | else 24 | last_update=0 25 | fi 26 | 27 | local current_time 28 | _sbp_get_current_time current_time 29 | time_since_update=$((current_time - last_update)) 30 | 31 | if [[ $time_since_update -lt $refresh_rate ]]; then 32 | return 0 33 | fi 34 | 35 | if [[ -z $RESCUETIME_API_KEY ]]; then 36 | debug::log "RESCUETIME_API_KEY not set" 37 | return 1 38 | fi 39 | 40 | result="$(segments::rescuetime_fetch_changes)" 41 | 42 | if [[ -z $result ]]; then 43 | # No data, so no logging of time today 44 | command rm -f "$SEGMENT_CACHE" 45 | return 0 46 | fi 47 | 48 | for line in $result; do 49 | seconds=$(cut -d ',' -f 2 <<<"$line") 50 | total_seconds=$((seconds + total_seconds)) 51 | value=$(cut -d ',' -f 4 <<<"$line") 52 | 53 | productivity_value=$((value + 2)) 54 | score=$((seconds * productivity_value)) 55 | productive_score=$((score + productive_score)) 56 | done 57 | 58 | max_score=$((total_seconds * 4)) 59 | pulse="$((productive_score * 100 / max_score))%" 60 | hours=$((total_seconds / 60 / 60)) 61 | hour_seconds=$((hours * 60 * 60)) 62 | remaining_seconds=$((total_seconds - hour_seconds)) 63 | minutes=$((remaining_seconds / 60)) 64 | time="${hours}h:${minutes}m" 65 | 66 | printf '%s;%s' "$pulse" "$time" >"$SEGMENT_CACHE" 67 | } 68 | 69 | segments::rescuetime() { 70 | if [[ -f $SEGMENT_CACHE ]]; then 71 | read -r cache <"$SEGMENT_CACHE" 72 | pulse="${cache/;*/}" 73 | time="${cache/*;/}" 74 | print_themed_segment 'normal' "$pulse" "$time" 75 | fi 76 | execute::execute_nohup_function segments::rescuetime_refresh 77 | } 78 | -------------------------------------------------------------------------------- /src/main.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # shellcheck source=src/debug.bash 4 | source "${SBP_PATH}/src/debug.bash" 5 | # shellcheck source=src/decorate.bash 6 | source "${SBP_PATH}/src/decorate.bash" 7 | # shellcheck source=src/execute.bash 8 | source "${SBP_PATH}/src/execute.bash" 9 | # shellcheck source=src/configure.bash 10 | source "${SBP_PATH}/src/configure.bash" 11 | 12 | configure::load_config 13 | 14 | readonly COMMAND_EXIT_CODE=$1 15 | readonly COMMAND_DURATION=$2 16 | 17 | main::main() { 18 | execute::execute_prompt_hooks 19 | 20 | # Execute the segments 21 | segment_groups=('left' 'right' 'line_two') 22 | 23 | declare -a pids_left 24 | declare -a pids_right 25 | declare -a pids_line_two 26 | 27 | left_segment_count=${#SBP_SEGMENTS_LEFT[@]} 28 | right_segment_count=${#SBP_SEGMENTS_RIGHT[@]} 29 | total_segment_count=$((left_segment_count + right_segment_count)) 30 | SEGMENTS_MAX_LENGTH=$((COLUMNS / total_segment_count)) 31 | 32 | for group in "${segment_groups[@]}"; do 33 | # Bash doesn't support array -> array, so we use pointers instead :( 34 | local -n segment_list="SBP_SEGMENTS_${group^^}" 35 | local -n pids="pids_${group}" 36 | 37 | for i in "${!segment_list[@]}"; do 38 | local segment_name="${segment_list[$i]}" 39 | execute::execute_prompt_segment "$segment_name" "$group" >"${SBP_TMP}/${group}.${i}" & 40 | pids["$i"]=$! 41 | done 42 | done 43 | 44 | declare -A segments_output 45 | declare -A segments_length 46 | 47 | # Collect the segments 48 | for group in "${segment_groups[@]}"; do 49 | # Bash doesn't support array -> array, so we use pointers instead :( 50 | # shellcheck disable=SC2178 51 | local -n pids="pids_${group}" 52 | 53 | for i in "${!pids[@]}"; do 54 | pid=${pids[$i]} 55 | wait "$pid" 56 | mapfile -t segment_data <"${SBP_TMP}/${group}.${i}" 57 | 58 | segment_size=${segment_data[0]} 59 | segment_value=${segment_data[1]} 60 | if [[ -n $segment_value ]]; then 61 | segments_length["$group"]=$((${segments_length["$group"]} + segment_size)) 62 | segments_output["$group"]="${segments_output["$group"]}${segment_value}" 63 | fi 64 | done 65 | done 66 | 67 | local prompt_gap_size=$((COLUMNS - segments_length['left'] - segments_length['right'])) 68 | print_themed_prompt "${segments_output['left']}" "${segments_output['right']}" "${segments_output[line_two]}" "$prompt_gap_size" 69 | } 70 | 71 | main::main 72 | -------------------------------------------------------------------------------- /sbp.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | ################################# 4 | # Simple Bash Prompt (SBP) # 5 | ################################# 6 | 7 | # Check the Bash version. 8 | if [ -z "${BASH_VERSION-}" ]; then 9 | printf 'sbp: This is not a Bash session. Bash 4.3 or higher is required by sbp.\n' >&2 10 | return 1 2>/dev/null 11 | elif [ -z "${BASH_VERSINFO-}" ] || ((BASH_VERSINFO[0] < 4 || BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 3)); then 12 | printf 'sbp: This is Bash %s. Bash 4.3 or higher is required by sbp.\n' "$BASH_VERSION" >&2 13 | return 1 2>/dev/null 14 | fi 15 | 16 | # Do not set up prompts when it is not an interactive session. 17 | if [[ $- != *i* ]] && ! return 0 2>/dev/null; then 18 | printf 'sbp: This is not an interactive session of Bash.\n' >&2 19 | exit 1 20 | fi 21 | 22 | # shellcheck source=src/interact.bash 23 | source "${SBP_PATH}/src/interact.bash" 24 | # shellcheck source=src/debug.bash 25 | source "${SBP_PATH}/src/debug.bash" 26 | 27 | if [[ -w "/run/user/${UID}" ]]; then 28 | SBP_TMP=$(mktemp -d --tmpdir="/run/user/${UID}") && trap 'command rm -rf "$SBP_TMP"' EXIT 29 | else 30 | SBP_TMP=$(mktemp -d) && trap 'command rm -rf "$SBP_TMP"' EXIT 31 | fi 32 | 33 | export SBP_TMP 34 | export SBP_PATH 35 | export COLUMNS 36 | 37 | _sbp_get_current_time() { 38 | if [[ ${EPOCHSECONDS-} ]]; then 39 | printf -v "$1" %s "$EPOCHSECONDS" 40 | else 41 | printf -v "$1" '%(%s)T' -1 42 | fi 43 | } 44 | export -f _sbp_get_current_time 45 | 46 | _sbp_set_prompt() { 47 | local command_status=$? 48 | local command_status current_time command_start command_duration 49 | [[ -n ${SBP_DEBUG-} ]] && debug::start_timer 50 | _sbp_get_current_time current_time 51 | if [[ -f "${SBP_TMP}/execution" ]]; then 52 | command_start=$(<"${SBP_TMP}/execution") 53 | command_duration=$((current_time - command_start)) 54 | command rm "${SBP_TMP}/execution" 55 | else 56 | command_duration=0 57 | command_status=0 58 | fi 59 | 60 | # TODO move this somewhere else 61 | title="${PWD##*/}" 62 | if [[ -n $SSH_CLIENT ]]; then 63 | title="${HOSTNAME:-ssh}:${title}" 64 | fi 65 | printf '\e]2;%s\007' "$title" 66 | 67 | PS1=$(bash "${SBP_PATH}/src/main.bash" "$command_status" "$command_duration") 68 | [[ -n ${SBP_DEBUG-} ]] && debug::tick_timer "Done" 69 | } 70 | 71 | _sbp_pre_exec() { 72 | local time 73 | _sbp_get_current_time time 74 | echo "$time" >"${SBP_TMP}/execution" 75 | } 76 | 77 | # shellcheck disable=SC2034,SC2016 78 | PS0='$(_sbp_pre_exec)' 79 | 80 | [[ $PROMPT_COMMAND =~ _sbp_set_prompt ]] || PROMPT_COMMAND="_sbp_set_prompt${PROMPT_COMMAND:+;}$PROMPT_COMMAND" 81 | SBP_SOURCED=1 82 | -------------------------------------------------------------------------------- /test/configure.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load src_helper 4 | 5 | setup() { 6 | SBP_CONFIG="${TMP_DIR}/local_config" 7 | mkdir -p "$SBP_CONFIG"/{hooks,layouts,segments,colors} 8 | SBP_PATH="${TMP_DIR}/default_source" 9 | mkdir -p "$SBP_PATH"/src/{hooks,layouts,segments,colors} 10 | } 11 | 12 | # List feature files in all various combinations 13 | 14 | @test "test that configure can list hook files" { 15 | touch "${SBP_CONFIG}/hooks/alert.bash" 16 | mapfile -t result <<<"$(configure::list_feature_files 'hooks')" 17 | assert_equal "${#result[@]}" 1 18 | assert_equal "${result[0]}" "${SBP_CONFIG}/hooks/alert.bash" 19 | } 20 | 21 | @test "test that configure can list segment files" { 22 | touch "$SBP_PATH"/src/segments/{1..12}.bash 23 | mapfile -t result <<<"$(configure::list_feature_files 'segments')" 24 | assert_equal "${#result[@]}" 12 25 | } 26 | 27 | @test "test that configure can list layout files" { 28 | touch "$SBP_PATH"/src/layouts/{1..4}.bash 29 | touch "$SBP_CONFIG"/layouts/{1..4}.bash 30 | mapfile -t result <<<"$(configure::list_feature_files 'layouts')" 31 | assert_equal "${#result[@]}" 8 32 | } 33 | 34 | @test "test that configure can list color files" { 35 | touch "$SBP_PATH"/src/colors/{1..4}.bash 36 | touch "$SBP_CONFIG"/colors/{1..4}.bash 37 | mapfile -t result <<<"$(configure::list_feature_files 'colors')" 38 | assert_equal "${#result[@]}" 8 39 | } 40 | 41 | # List feature names depends very much on listing files, so just test that it works for one 42 | 43 | @test "test that we can list feature names" { 44 | touch "$SBP_CONFIG"/colors/sbp.bash 45 | mapfile -t result <<<"$(configure::list_feature_names 'colors')" 46 | assert_equal "${#result[@]}" 1 47 | assert_equal "${result[0]}" 'sbp' 48 | } 49 | 50 | @test "test that we can set the correct colors" { 51 | local local_config="${SBP_CONFIG}/colors/local.bash" 52 | local global_config="${SBP_PATH}/src/colors/global.bash" 53 | local result 54 | touch "$global_config" 55 | touch "$local_config" 56 | # shellcheck disable=SC2317 57 | source() { echo "$@"; } 58 | result="$(configure::set_colors 'local')" 59 | assert_equal "$result" "$local_config" 60 | result="$(configure::set_colors 'global')" 61 | assert_equal "$result" "$global_config" 62 | } 63 | 64 | @test "test that we can set the correct layouts" { 65 | local local_config="${SBP_CONFIG}/layouts/local.bash" 66 | local global_config="${SBP_PATH}/src/layouts/global.bash" 67 | local result 68 | touch "$global_config" 69 | touch "$local_config" 70 | source() { echo "$@"; } 71 | result="$(configure::set_layout 'local')" 72 | assert_equal "$result" "$local_config" 73 | result="$(configure::set_layout 'global')" 74 | assert_equal "$result" "$global_config" 75 | } 76 | -------------------------------------------------------------------------------- /src/layouts/plain.bash: -------------------------------------------------------------------------------- 1 | SEGMENTS_PROMPT_READY_ICON=${LAYOUTS_PLAIN_PROMPT_READY_ICON:-'➜'} 2 | SEGMENTS_GIT_ICON=${LAYOUTS_PLAIN_GIT_ICON:-' '} 3 | SEGMENTS_GIT_INCOMING_ICON=${LAYOUTS_PLAIN_GIT_INCOMING_ICON:-'↓'} 4 | SEGMENTS_GIT_OUTGOING_ICON=${LAYOUTS_PLAIN_GIT_OUTGOING_ICON:-'↑'} 5 | SEGMENTS_PATH_SPLITTER_DISABLE=${LAYOUTS_PLAIN_PATH_SPLITTER_DISABLE:-1} 6 | PROMPT_COMPACT=${SBP_PROMPT_COMPACT:-false} 7 | 8 | print_themed_prompt() { 9 | local left_segments=$1 10 | local right_segments=$2 11 | local line_two_segments=$3 12 | local prompt_gap_size=$4 13 | 14 | local reset_color 15 | decorate::print_colors 'reset_color' 16 | 17 | if [[ -n $right_segments ]]; then 18 | right_segments="${right_segments} " 19 | prompt_gap_size=$((prompt_gap_size - 1)) 20 | right_segments="${right_segments}${reset_color}" 21 | fi 22 | 23 | if [[ $PROMPT_COMPACT == false ]]; then 24 | left_segments="\n${left_segments}" 25 | fi 26 | 27 | local filler_segment 28 | if [[ -n $right_segments || -n $line_two_segments ]]; then 29 | print_themed_filler 'filler_segment' "$prompt_gap_size" 30 | right_segments="${right_segments}\n" 31 | fi 32 | 33 | line_two_segments="${line_two_segments}${reset_color}" 34 | 35 | printf '%s' "${left_segments}${filler_segment}${right_segments}${line_two_segments}" 36 | } 37 | 38 | print_themed_filler() { 39 | local -n print_themed_filler_result=$1 40 | local filler_size=$2 41 | # Account for seperator and padding 42 | # shellcheck disable=SC2183 43 | padding=$(printf "%*s" "$filler_size") 44 | # shellcheck disable=SC2034 45 | SEGMENT_LINE_POSITION=2 46 | prompt_filler_output="$(print_themed_segment 'filler' "$padding")" 47 | mapfile -t segment_output <<<"$prompt_filler_output" 48 | 49 | # shellcheck disable=SC2034 50 | print_themed_filler_result=${segment_output[1]} 51 | } 52 | 53 | print_themed_segment() { 54 | local segment_type=$1 55 | shift 56 | local segment_parts=("${@}") 57 | local segment_length=0 58 | local themed_parts 59 | 60 | if [[ $segment_type == 'highlight' ]]; then 61 | PRIMARY_COLOR="$PRIMARY_COLOR_HIGHLIGHT" 62 | fi 63 | 64 | if [[ $segment_type == 'filler' ]]; then 65 | themed_parts="${segment_parts[0]}" 66 | else 67 | for part in "${segment_parts[@]}"; do 68 | [[ -z ${part// /} ]] && continue 69 | part_length="${#part}" 70 | 71 | themed_parts="${themed_parts} ${part}" 72 | segment_length=$((segment_length + part_length + 1)) 73 | done 74 | fi 75 | 76 | local color 77 | decorate::print_fg_color 'color' "$PRIMARY_COLOR" 78 | 79 | full_output="${color}${themed_parts}" 80 | 81 | if [[ $segment_type == 'prompt_ready' ]]; then 82 | full_output="${full_output} " 83 | fi 84 | 85 | printf '%s\n%s' "$segment_length" "$full_output" 86 | } 87 | -------------------------------------------------------------------------------- /src/layouts/lines.bash: -------------------------------------------------------------------------------- 1 | SEPERATOR_LEFT=${LAYOUT_LINES_SEPARATOR_LEFT:-'['} 2 | SEPERATOR_RIGHT=${LAYOUT_LINES_SEPARATOR_RIGHT:-']'} 3 | PROMPT_PREFIX_UPPER=${LAYOUT_LINES_PROMPT_PREFIX_UPPER:-'┍'} 4 | PROMPT_PREFIX_LOWER=${LAYOUT_LINES_PROMPT_PREFIX_LOWER:-'└'} 5 | SEGMENTS_PROMPT_READY_ICON=${LAYOUT_LINES_PROMPT_READY_ICON:-'➜'} 6 | SEGMENTS_GIT_ICON=${LAYOUT_LINES_GIT_ICON:-' '} 7 | SEGMENTS_PATH_SPLITTER_DISABLE=1 8 | PROMPT_COMPACT=${SBP_PROMPT_COMPACT:-false} 9 | 10 | print_themed_prompt() { 11 | local left_segments=$1 12 | local right_segments=$2 13 | local line_two_segments=$3 14 | local prompt_gap_size=$4 15 | 16 | local reset_color 17 | decorate::print_colors 'reset_color' 18 | 19 | if [[ -n $right_segments || -n $line_two_segments ]]; then 20 | prefix_upper_size="${#PROMPT_PREFIX_UPPER}" 21 | left_segments="${PROMPT_PREFIX_UPPER}${left_segments}" 22 | prompt_gap_size=$((prefix_upper_size - prompt_gap_size)) 23 | line_two_segments="${PROMPT_PREFIX_LOWER}${line_two_segments}" 24 | right_segments="${right_segments}\n" 25 | 26 | local filler_segment 27 | print_themed_filler 'filler_segment' "$prompt_gap_size" 28 | fi 29 | 30 | if [[ $PROMPT_COMPACT == false ]]; then 31 | left_segments="\n${left_segments}" 32 | fi 33 | 34 | right_segments="${right_segments}${reset_color}" 35 | prompt_ready="${prompt_ready}${reset_color}" 36 | 37 | printf '%s' "${left_segments}${filler_segment}${right_segments}${line_two_segments} " 38 | } 39 | 40 | print_themed_filler() { 41 | local -n print_themed_filler_result=$1 42 | local filler_size=$2 43 | # Account for seperator and padding 44 | # shellcheck disable=SC2183 45 | padding=$(printf "%*s" "$filler_size") 46 | prompt_filler_output="$(print_themed_segment 'filler' "$padding")" 47 | mapfile -t segment_output <<<"$prompt_filler_output" 48 | 49 | # shellcheck disable=SC2034 50 | print_themed_filler_result=${segment_output[1]} 51 | } 52 | 53 | print_themed_segment() { 54 | local segment_type=$1 55 | shift 56 | local segment_parts=("${@}") 57 | local themed_parts 58 | local segment_length=0 59 | 60 | if [[ $segment_type == 'highlight' ]]; then 61 | PRIMARY_COLOR="$PRIMARY_COLOR_HIGHLIGHT" 62 | fi 63 | 64 | if [[ $segment_type == 'filler' || $segment_type == 'prompt_ready' ]]; then 65 | SEPERATOR_RIGHT='' 66 | SEPERATOR_LEFT='' 67 | fi 68 | 69 | seperator_size=$((${#SEPERATOR_RIGHT} + ${#SEPERATOR_LEFT})) 70 | 71 | for part in "${segment_parts[@]}"; do 72 | [[ -z $part ]] && continue 73 | part_length="${#part}" 74 | 75 | if [[ -n $themed_parts ]]; then 76 | themed_parts="${themed_parts} ${part}" 77 | segment_length=$((segment_length + part_length + 1)) 78 | else 79 | segment_length="$((segment_length + part_length))" 80 | themed_parts="${part}" 81 | fi 82 | done 83 | 84 | local color 85 | decorate::print_fg_color 'color' "$PRIMARY_COLOR" 86 | 87 | local color_reset 88 | decorate::print_colors 'color_reset' 89 | 90 | full_output="${color_reset}${color}${SEPERATOR_LEFT}${themed_parts}${SEPERATOR_RIGHT}" 91 | segment_length=$((segment_length + seperator_size)) 92 | 93 | printf '%s\n%s' "$segment_length" "$full_output" 94 | } 95 | -------------------------------------------------------------------------------- /src/segments/git.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | segments::git() { 4 | local max_length=$SEGMENTS_MAX_LENGTH 5 | 6 | local incoming_icon="${SEGMENTS_GIT_INCOMING_ICON:-↓}" 7 | local outgoing_icon="${SEGMENTS_GIT_OUTGOING_ICON:-↑}" 8 | 9 | local branch_only="${SEGMENTS_GIT_BRANCH_ONLY:-false}" 10 | 11 | local path=${PWD} 12 | while [[ $path ]]; do 13 | if [[ -d "${path}/.git" ]]; then 14 | local git_folder="${path}/.git" 15 | break 16 | fi 17 | path=${path%/*} 18 | done 19 | 20 | [[ -z $git_folder ]] && exit 0 21 | if [[ $PWD == "$git_folder" ]]; then 22 | print_themed_segment 'normal' '.git/' 23 | return 0 24 | fi 25 | 26 | if [[ $branch_only == false ]]; then 27 | local git_status 28 | git_status="$(git status --porcelain --branch 2>/dev/null)" 29 | 30 | local additions=0 31 | local modifications=0 32 | local deletions=0 33 | local untracked=0 34 | 35 | while read -r line; do 36 | local compacted=${line// /} 37 | local action=${compacted:0:1} 38 | case $action in 39 | A) 40 | additions_icon=' +' 41 | additions=$((additions + 1)) 42 | ;; 43 | M | R) 44 | modifications_icon=' ~' 45 | modifications=$((modifications + 1)) 46 | ;; 47 | D) 48 | deletions_icon=' -' 49 | deletions=$((deletions + 1)) 50 | ;; 51 | \?) 52 | untracked_icon=' ?' 53 | untracked=$((untracked + 1)) 54 | ;; 55 | \#) 56 | branch_line=${line/\#\# /} 57 | branch_data=${branch_line/% */} 58 | branch="${branch_data/...*/}" 59 | upstream_data="${branch_line#* }" 60 | upstream_stripped="${upstream_data//[\[|\]]/}" 61 | if [[ $upstream_data != "$upstream_stripped" ]]; then 62 | outgoing_filled="${upstream_stripped/ahead / ${outgoing_icon}}" 63 | upstream_status="${outgoing_filled/behind / ${incoming_icon}}" 64 | fi 65 | ;; 66 | esac 67 | done <<<"$git_status" 68 | 69 | local git_state="${additions_icon}${additions#0}${modifications_icon}${modifications#0}${deletions_icon}${deletions#0}${untracked_icon}${untracked#0}" 70 | 71 | # git status does not support detached head 72 | if [[ $branch != 'HEAD' ]]; then 73 | git_head="$branch" 74 | else 75 | git_head=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null) 76 | fi 77 | else 78 | git_head=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null) 79 | fi 80 | 81 | git_size=$((${#git_state} + ${#SEGMENTS_GIT_ICON} + ${#git_head} + ${#upstream_status})) 82 | 83 | if [[ $git_size -gt $max_length && $max_length -ne -1 ]]; then 84 | available_space=$((max_length - ${#git_state} - ${#SEGMENTS_GIT_ICON} + ${#upstream_status})) 85 | if [[ $available_space -gt 0 ]]; then 86 | git_head="${git_head:0:available_space}.." 87 | else 88 | git_head="" 89 | fi 90 | fi 91 | 92 | SPLITTER_LEFT='' 93 | SPLITTER_RIGHT='' 94 | print_themed_segment 'normal' "${git_state/ /}" "$SEGMENTS_GIT_ICON" "${git_head/ /}" "${upstream_status/ /}" 95 | } 96 | -------------------------------------------------------------------------------- /src/configure.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | SBP_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/sbp" 4 | mkdir -p "${SBP_CONFIG}" 5 | config_file="${SBP_CONFIG}/settings.conf" 6 | colors_file="${SBP_CONFIG}/colors.conf" 7 | config_template="${SBP_PATH}/config/settings.conf.template" 8 | colors_template="${SBP_PATH}/config/colors.conf.template" 9 | default_colors="${SBP_PATH}/config/colors.conf" 10 | default_config="${SBP_PATH}/config/settings.conf" 11 | SBP_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/sbp" 12 | SBP_STATE="${XDG_STATE_HOME:-$HOME/.local/state}/sbp" 13 | 14 | configure::list_feature_files() { 15 | local feature_type=$1 16 | IFS=" " read -r -a features <<<"$( 17 | shopt -s nullglob 18 | echo "${SBP_PATH}/src/${feature_type}"/*.bash \ 19 | "${SBP_CONFIG}/${feature_type}"/*.bash 20 | )" 21 | 22 | for file in "${features[@]}"; do 23 | printf '%s\n' "$file" 24 | done 25 | } 26 | 27 | configure::get_feature_file() { 28 | local -n get_feature_file_result=$1 29 | local feature_type=$2 30 | local feature_name=$3 31 | 32 | local local_file="${SBP_PATH}/src/${feature_type}s/${feature_name}.bash" 33 | local global_file="${SBP_CONFIG}/${feature_type}s/${feature_name}.bash" 34 | 35 | if [[ -f $local_file ]]; then 36 | get_feature_file_result="$local_file" 37 | elif [[ -f $global_file ]]; then 38 | get_feature_file_result="$global_file" 39 | else 40 | debug::log "Could not find $local_file" 41 | debug::log "Could not find $global_file" 42 | debug::log "Make sure at least on of them exists" 43 | fi 44 | 45 | } 46 | 47 | configure::list_feature_names() { 48 | local feature_type=$1 49 | for file in $(configure::list_feature_files "$feature_type"); do 50 | file_name="${file##*/}" 51 | name="${file_name/.bash/}" 52 | printf '%s\n' "$name" 53 | done 54 | } 55 | 56 | configure::set_colors() { 57 | local color_name=$1 58 | local colors_file 59 | configure::get_feature_file 'colors_file' 'color' "$color_name" 60 | 61 | if [[ -n $colors_file ]]; then 62 | source "$colors_file" 63 | else 64 | debug::log "Using the default color config" 65 | source "${SBP_PATH}/src/colors/default.bash" 66 | fi 67 | } 68 | 69 | configure::set_layout() { 70 | local layout_name=$1 71 | local layout_file 72 | 73 | configure::get_feature_file 'layout_file' 'layout' "$layout_name" 74 | 75 | if [[ -n $layout_file ]]; then 76 | source "$layout_file" 77 | else 78 | debug::log "Using the default layout" 79 | source "${SBP_PATH}/src/layouts/plain.bash" 80 | fi 81 | } 82 | 83 | configure::load_config() { 84 | [[ -d ${SBP_CACHE} ]] || mkdir -p "${SBP_CACHE}" 85 | [[ -d ${SBP_STATE}/log ]] || mkdir -p "${SBP_STATE}/log" 86 | 87 | if [[ ! -f $config_file ]]; then 88 | debug::log "Config file not found: ${config_file}" 89 | debug::log "Creating it.." 90 | cp "$config_template" "$config_file" 91 | fi 92 | 93 | if [[ ! -f $colors_file ]]; then 94 | debug::log "Color config file not found: ${colors_file}" 95 | debug::log "Creating it.." 96 | cp "$colors_template" "$colors_file" 97 | fi 98 | 99 | # shellcheck source=/dev/null 100 | source "$config_file" 101 | source "$default_config" 102 | configure::set_layout "${SBP_THEME_LAYOUT_OVERRIDE:-$SBP_THEME_LAYOUT}" 103 | configure::set_colors "${SBP_THEME_COLOR_OVERRIDE:-$SBP_THEME_COLOR}" 104 | # shellcheck source=/dev/null 105 | source "$colors_file" 106 | source "$default_colors" 107 | } 108 | -------------------------------------------------------------------------------- /src/segments/README.md: -------------------------------------------------------------------------------- 1 | # Segments 2 | 3 | Segments are the parts that make up the prompt. They can be added and removed in 4 | any order. They are executed asynchronously so they cannot be dependent on other 5 | segments. 6 | 7 | ## Creating your own segments 8 | You can create your own segments by placing a file in: 9 | ``` 10 | ${HOME}/.config/sbp/segments/${your_segment_name}.bash 11 | ``` 12 | 13 | This script should contain at least a function called 14 | `segments::${your_segment_name}` and it will have the following variables 15 | available upon execution: 16 | ``` 17 | - COMMAND_EXIT_CODE, the exit code of the privous shell command 18 | - COMMAND_DURATION, the duration of the shell command 19 | - SBP_TMP, a tmp folder which is local to your shell PID and cleaned upon exit 20 | - SBP_CACHE, a cache folder which is global to all SBP processes 21 | - SBP_PATH, the path to the SBP diectory 22 | - SEGMENTS_MAX_LENGTH tells your segment how much space it should use. This is 23 | not a hard limit, but a suggestion for when to start trimming/compacting the 24 | segment. 25 | ``` 26 | 27 | When you have defined the parts you want to use in your segment you need to 28 | theme the parts by issuing the following command: 29 | ``` 30 | print_themed_segment 'normal/higlight' "${segment_pars[@]}" 31 | ``` 32 | 33 | ## Configuration 34 | All segments should adhere to the SEGMENTS_MAX_LENGTH variable, but you can set 35 | SEGMENTS_${SEGMENT}_MAX_LENGTH to a specific value or -1 to disable 36 | truncation/compacting for that specific segment. 37 | 38 | ## aws 39 | Shows the current active aws profile 40 | 41 | ## command 42 | shows the time spent on the last command, and turns red if it failed 43 | 44 | ## exit_code 45 | shows the value of the last exitcode 46 | 47 | ## git 48 | shows the git branch and current status, set ´SEGMENTS_GIT_BRANCH_ONLY=true´ 49 | to speed up execution on large repos. The default value is ´false´. 50 | 51 | ## host 52 | shows the ${USER} and ${HOSTNAME} if you are logged in through ssh 53 | 54 | ## k8s 55 | shows the current user/cluster/project 56 | Setting ´SEGMENTS_K8S_DEFAULT_USER´ will hide the user if it's the default user 57 | Setting ´SEGMENTS_K8S_HIDE_CLUSTER´ to 1 will hide the cluster name. The 58 | default values are '' and 0 respectively. 59 | 60 | ## load 61 | shows the average load of the machine 62 | 63 | ## nix 64 | shows wether you are in a nix-shell or not 65 | 66 | ## path 67 | shows the current path 68 | 69 | ## path_ro 70 | shows a lock if current path is read only 71 | 72 | ## prompt_ready 73 | Shows a simple character before the end of the prompt 74 | 75 | ## python_env 76 | shows the virtual env settings for current folder 77 | 78 | ## rescuetime 79 | Shows Productivity score and logged time for the day. Requires 80 | ´RESCUETIME_API_KEY´ to be set in the environment 81 | 82 | ## terraform 83 | Shows the Terraform version in directories with Terraform files. Supports version managers 'tfenv' and 'asdf'. 84 | Directories to be scanned can be configured with array `SEGMENTS_TF_SCANNED_DIRS` (default: *$PWD*), 85 | file extension to look for with `TF_FILE_EXTENSION` (default: *tf*). 86 | Nerdfont icon will be used when setting `SEGMENTS_USE_NERDFONTS` to 1. Overriding the default icon is possible with `SEGMENTS_TF_ICON`. 87 | 88 | ## timestamp 89 | shows a timestamp generated by date formatted by ´SEGMENTS_TIMESTAMP_FORMAT´, 90 | the default value is '%H:%M:%S' and the output is generated by the unix 91 | ´date´ command. 92 | 93 | ## wttr 94 | Shows the weather based on ´SEGMENTS_WTTR_LOCATION´ and 95 | ´SEGMENTS_WTTR_FORMAT´, the default values are ´Oslo´ and ´%p;%t;%w´ 96 | respectively. 97 | -------------------------------------------------------------------------------- /src/interact_themed.bash: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # shellcheck source=src/decorate.bash 4 | source "${SBP_PATH}/src/decorate.bash" 5 | # shellcheck source=src/configure.bash 6 | source "${SBP_PATH}/src/configure.bash" 7 | # shellcheck source=src/execute.bash 8 | source "${SBP_PATH}/src/execute.bash" 9 | # shellcheck source=src/debug.bash 10 | source "${SBP_PATH}/src/debug.bash" 11 | 12 | configure::load_config 13 | 14 | list_config() { 15 | for setting in $(compgen -A variable | grep '^SEGMENTS_'); do 16 | local -n value="$setting" 17 | printf '%s %s\n' "$setting" "$value" 18 | done | column -t 19 | } 20 | 21 | list_segments() { 22 | for segment_path in $(configure::list_feature_files 'segments'); do 23 | local status='disabled' 24 | local segment_file="${segment_path##*/}" 25 | local segment_name="${segment_file/.bash/}" 26 | SBP_SEGMENTS=("${SBP_SEGMENTS_LEFT[@]}" "${SBP_SEGMENTS_RIGHT[@]}" "${SBP_SEGMENTS_LINE_TWO[@]}") 27 | if printf '%s.bash\n' "${SBP_SEGMENTS[@]}" | grep -qo "${segment_name}"; then 28 | if [[ -f "${SBP_CONFIG}/peekaboo/${segment_name/.bash/}" ]]; then 29 | status='hidden' 30 | else 31 | status='enabled' 32 | fi 33 | fi 34 | 35 | debug::start_timer 36 | (execute::execute_prompt_segment "$segment_name" &>/dev/null) 37 | # shellcheck disable=SC2119 38 | duration=$(debug::tick_timer 2>&1 | tr -d ':') 39 | 40 | echo "${segment_name}: ${status}" "$duration" 41 | done | column -t 42 | } 43 | 44 | list_hooks() { 45 | for hook in $(configure::list_feature_files 'hooks'); do 46 | hook_file="${hook##*/}" 47 | hook_name="${hook_file/.bash/}" 48 | status='disabled' 49 | if printf '%s.bash\n' "${SBP_HOOKS[@]}" | grep -qo "${hook_name}"; then 50 | if [[ -f "${SBP_CONFIG}/peekaboo/${hook_name}" ]]; then 51 | status='paused' 52 | else 53 | status='enabled' 54 | fi 55 | fi 56 | echo "${hook_name}: ${status}" | column -t 57 | done 58 | } 59 | 60 | list_layouts() { 61 | for layout in $(configure::list_feature_files 'layouts'); do 62 | file="${layout##*/}" 63 | printf ' %s\n' "${file/.bash/}" 64 | done 65 | } 66 | 67 | show_current_colors() { 68 | printf ' ' 69 | for n in {0..15}; do 70 | color="color${n}" 71 | local text_color_value 72 | decorate::calculate_complementing_color 'text_color_value' "${!color}" 73 | local text_color 74 | decorate::print_fg_color 'text_color' "$text_color_value" 'false' 75 | local bg_color 76 | decorate::print_bg_color 'bg_color' "${!color}" 'false' 77 | printf '%b%b %b%b ' "$bg_color" "$text_color" " $n " "\e[00m" 78 | done 79 | printf '\n' 80 | } 81 | 82 | list_colors() { 83 | for color in $(configure::list_feature_files 'colors'); do 84 | source "$color" 85 | file="${color##*/}" 86 | printf '\n %s \n' "${file/.bash/}" 87 | show_current_colors 88 | done 89 | } 90 | 91 | list_themes() { 92 | printf '\n%s:\n' "Colors" 93 | list_colors 94 | printf '\n%s:\n' "Layouts" 95 | list_layouts 96 | } 97 | 98 | list_words() { 99 | configure::list_feature_names "$1" 100 | } 101 | 102 | show_status() { 103 | read -r -d '' splash <<'EOF' 104 | 105 | - _____________________________ 106 | / _____/\______ \______ \ 107 | \_____ \ | | _/| ___/ 108 | / \ | | \| | 109 | /_______ / |______ /|____| 110 | \/ \/ 111 | EOF 112 | printf '%s\n' "${splash}" 113 | printf '%s: %s\n' 'Color' "${SBP_THEME_COLOR:-$SETTINGS_THEME_COLOR}" 114 | printf '%s: %s\n' 'Layout' "${SBP_THEME_LAYOUT:-$SETTINGS_THEME_LAYOUT}" 115 | printf '\n%s\n' "Current colors:" 116 | show_current_colors 117 | } 118 | 119 | "$@" 120 | -------------------------------------------------------------------------------- /config/colors.conf: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Color configuration for the segments. 4 | # See src/colors/*.bash for the color values 5 | 6 | SEGMENTS_AWS_COLOR_PRIMARY=${SEGMENTS_AWS_COLOR_PRIMARY:-$color8} 7 | SEGMENTS_AWS_COLOR_SECONDARY=${SEGMENTS_AWS_COLOR_SECONDARY:-$color9} 8 | 9 | SEGMENTS_COMMAND_COLOR_PRIMARY=${SEGMENTS_COMMAND_COLOR_PRIMARY:-$color4} 10 | SEGMENTS_COMMAND_COLOR_SECONDARY=${SEGMENTS_COMMAND_COLOR_SECONDARY:-$color1} 11 | SEGMENTS_COMMAND_COLOR_PRIMARY_HIGHLIGHT=${SEGMENTS_COMMAND_COLOR_PRIMARY_HIGHLIGHT:-$color8} 12 | SEGMENTS_COMMAND_COLOR_SECONDARY_HIGHLIGHT=${SEGMENTS_COMMAND_COLOR_SECONDARY_HIGHLIGHT:-$color4} 13 | 14 | SEGMENTS_CONDA_COLOR_PRIMARY=${SEGMENTS_CONDA_COLOR_PRIMARY:-$color8} 15 | SEGMENTS_CONDA_COLOR_SECONDARY=${SEGMENTS_CONDA_COLOR_SECONDARY:-$color9} 16 | 17 | SEGMENTS_DIRENV_COLOR_PRIMARY=${SEGMENTS_DIRENV_COLOR_PRIMARY:-$color14} 18 | SEGMENTS_DIRENV_COLOR_SECONDARY=${SEGMENTS_DIRENV_COLOR_SECONDARY:-$color7} 19 | 20 | SEGMENTS_GIT_COLOR_PRIMARY=${SEGMENTS_GIT_COLOR_PRIMARY:-$color10} 21 | SEGMENTS_GIT_COLOR_SECONDARY=${SEGMENTS_GIT_COLOR_SECONDARY:-$color1} 22 | 23 | SEGMENTS_HOST_COLOR_PRIMARY=${SEGMENTS_HOST_COLOR_PRIMARY:-$color2} 24 | SEGMENTS_HOST_COLOR_SECONDARY=${SEGMENTS_HOST_COLOR_SECONDARY:-$color5} 25 | SEGMENTS_HOST_COLOR_PRIMARY_HIGHLIGHT=${SEGMENTS_HOST_COLOR_PRIMARY_HIGHLIGHT:-$color9} 26 | SEGMENTS_HOST_COLOR_SECONDARY_HIGHLIGHT=${SEGMENTS_HOST_COLOR_SECONDARY_HIGHLIGHT:-$color0} 27 | 28 | SEGMENTS_K8S_COLOR_PRIMARY=${SEGMENTS_K8S_COLOR_PRIMARY:-$color3} 29 | SEGMENTS_K8S_COLOR_SECONDARY=${SEGMENTS_K8S_COLOR_SECONDARY:-$color7} 30 | 31 | SEGMENTS_LOAD_COLOR_PRIMARY=${SEGMENTS_LOAD_COLOR_PRIMARY:-$color9} 32 | SEGMENTS_LOAD_COLOR_SECONDARY=${SEGMENTS_LOAD_COLOR_SECONDARY:-$color1} 33 | SEGMENTS_LOAD_COLOR_PRIMARY_HIGHLIGHT=${SEGMENTS_LOAD_COLOR_PRIMARY_HIGHLIGHT:-$color8} 34 | SEGMENTS_LOAD_COLOR_SECONDARY_HIGHLIGHT=${SEGMENTS_LOAD_COLOR_SECONDARY_HIGHLIGHT:-$color7} 35 | 36 | SEGMENTS_NIX_COLOR_PRIMARY=${SEGMENTS_NIX_COLOR_PRIMARY:-$color13} 37 | SEGMENTS_NIX_COLOR_SECONDARY=${SEGMENTS_NIX_COLOR_SECONDARY:-$color7} 38 | 39 | SEGMENTS_PATH_COLOR_PRIMARY=${SEGMENTS_PATH_COLOR_PRIMARY:-$color14} 40 | SEGMENTS_PATH_COLOR_SECONDARY=${SEGMENTS_PATH_COLOR_SECONDARY:-$color7} 41 | SEGMENTS_PATH_COLOR_SPLITTER=${SEGMENTS_PATH_COLOR_SPLITTER:-$color4} 42 | 43 | SEGMENTS_PATH_RO_COLOR_SECONDARY=${SEGMENTS_PATH_RO_COLOR_SECONDARY:-$color15} 44 | SEGMENTS_PATH_RO_COLOR_PRIMARY=${SEGMENTS_PATH_RO_COLOR_PRIMARY:-$color1} 45 | 46 | SEGMENTS_PROMPT_READY_COLOR_PRIMARY=${SEGMENTS_PROMPT_READY_COLOR_PRIMARY:-''} 47 | SEGMENTS_PROMPT_READY_COLOR_SECONDARY=${SEGMENTS_PROMPT_READY_COLOR_SECONDARY:-$color4} 48 | 49 | SEGMENTS_PYTHON_ENV_COLOR_PRIMARY=${SEGMENTS_PYTHON_ENV_COLOR_PRIMARY:-$color9} 50 | SEGMENTS_PYTHON_ENV_COLOR_SECONDARY=${SEGMENTS_PYTHON_ENV_COLOR_SECONDARY:-$color15} 51 | 52 | SEGMENTS_RESCUETIME_COLOR_PRIMARY=${SEGMENTS_RESCUETIME_COLOR_PRIMARY:-$color11} 53 | SEGMENTS_RESCUETIME_COLOR_SECONDARY=${SEGMENTS_RESCUETIME_COLOR_SECONDARY:-$color4} 54 | SEGMENTS_RESCUETIME_COLOR_SPLITTER=${SEGMENTS_RESCUETIME_COLOR_SPLITTER:-$color7} 55 | 56 | SEGMENTS_EXIT_CODE_COLOR_PRIMARY_HIGHLIGHT=${SEGMENTS_EXIT_CODE_COLOR_PRIMARY_HIGHLIGHT:-$color1} 57 | SEGMENTS_EXIT_CODE_COLOR_SECONDARY_HIGHLIGHT=${SEGMENTS_EXIT_CODE_COLOR_SECONDARY_HIGHLIGHT:-$color15} 58 | 59 | SEGMENTS_TERRAFORM_COLOR_PRIMARY=${SEGMENTS_HOST_COLOR_PRIMARY:-$color2} 60 | SEGMENTS_TERRAFORM_COLOR_SECONDARY=${SEGMENTS_HOST_COLOR_SECONDARY:-$color5} 61 | 62 | SEGMENTS_TIMESTAMP_COLOR_PRIMARY=${SEGMENTS_TIMESTAMP_COLOR_PRIMARY:-$color2} 63 | SEGMENTS_TIMESTAMP_COLOR_SECONDARY=${SEGMENTS_TIMESTAMP_COLOR_SECONDARY:-$color5} 64 | 65 | SEGMENTS_WTTR_COLOR_PRIMARY=${SEGMENTS_WTTR_COLOR_PRIMARY:-$color11} 66 | SEGMENTS_WTTR_COLOR_SECONDARY=${SEGMENTS_WTTR_COLOR_SECONDARY:-$color4} 67 | SEGMENTS_WTTR_COLOR_SPLITTER=${SEGMENTS_WTTR_COLOR_SPLITTER:-$color7} 68 | -------------------------------------------------------------------------------- /src/layouts/powerline.bash: -------------------------------------------------------------------------------- 1 | SEPERATOR_LEFT=${LAYOUTS_POWERLINE_SEPARATOR_LEFT:-''} 2 | SEPERATOR_RIGHT=${LAYOUTS_POWERLINE_SEPARATOR_RIGHT:-''} 3 | SPLITTER_LEFT=${LAYOUTS_POWERLINE_SPLITTER_LEFT:-''} 4 | SPLITTER_RIGHT=${LAYOUTS_POWERLINE_SPLITTER_RIGHT:-''} 5 | SEGMENTS_PROMPT_READY_ICON=${LAYOUTS_POWERLINE_PROMPT_READY_ICON:-'➜'} 6 | SEGMENTS_GIT_ICON=${LAYOUTS_POWERLINE_GIT_ICON:-''} 7 | SEGMENTS_GIT_INCOMING_ICON=${LAYOUTS_POWERLINE_GIT_INCOMING_ICON:-'↓'} 8 | SEGMENTS_GIT_OUTGOING_ICON=${LAYOUTS_POWERLINE_GIT_OUTGOING_ICON:-'↑'} 9 | PROMPT_COMPACT=${SBP_PROMPT_COMPACT:-false} 10 | 11 | print_themed_prompt() { 12 | local left_segments=$1 13 | local right_segments=$2 14 | local line_two_segments=$3 15 | local prompt_gap_size=$4 16 | 17 | # Remove the first seperator as it's not ending a previous segment 18 | local seperator_left_size=${#SEPERATOR_LEFT} 19 | left_segments=${left_segments#*"$SEPERATOR_LEFT"} 20 | prompt_gap_size=$((seperator_left_size + prompt_gap_size)) 21 | # Remove the first seperator as it's not ending a previous segment 22 | [[ -n $line_two_segments ]] && line_two_segments=${line_two_segments#*"$SEPERATOR_LEFT"} 23 | 24 | local reset_color 25 | decorate::print_colors 'reset_color' 26 | 27 | if [[ $PROMPT_COMPACT == false ]]; then 28 | left_segments="\n${left_segments}" 29 | fi 30 | 31 | local filler_segment 32 | if [[ -n $right_segments || -n $line_two_segments ]]; then 33 | print_themed_filler 'filler_segment' "$prompt_gap_size" 34 | right_segments="${right_segments}${reset_color}\n" 35 | elif [[ ${SBP_SEGMENTS_LEFT[-1]} != 'prompt_ready' ]]; then 36 | # We need to finish the last segment in this case 37 | print_themed_filler 'filler_segment' 1 38 | filler_segment="${filler_segment// /}${reset_color} " 39 | fi 40 | 41 | line_two_segments="${line_two_segments}${reset_color}" 42 | 43 | printf '%s' "${left_segments}${filler_segment}${right_segments}${line_two_segments}" 44 | 45 | } 46 | 47 | print_themed_filler() { 48 | local -n print_themed_filler_result=$1 49 | local seperator_size=${#SEPERATOR_LEFT} 50 | # Account for seperator and padding 51 | local filler_size=$(($2 - seperator_size - 2)) 52 | # shellcheck disable=SC2183 53 | printf -v padding '%*s' "$filler_size" 54 | SEGMENT_POSITION='left' 55 | prompt_filler_output="$(print_themed_segment 'normal' "$padding")" 56 | mapfile -t segment_output <<<"$prompt_filler_output" 57 | 58 | # shellcheck disable=SC2034 59 | print_themed_filler_result=${segment_output[1]} 60 | } 61 | 62 | print_themed_segment() { 63 | local segment_type=$1 64 | shift 65 | local segment_parts=("${@}") 66 | local segment_length=0 67 | local part_length=0 68 | local themed_segment 69 | local seperator_themed 70 | local part_splitter 71 | 72 | if [[ $segment_type == 'highlight' ]]; then 73 | PRIMARY_COLOR="$PRIMARY_COLOR_HIGHLIGHT" 74 | SECONDARY_COLOR="$SECONDARY_COLOR_HIGHLIGHT" 75 | fi 76 | 77 | if [[ $SEGMENT_POSITION == 'left' ]]; then 78 | part_splitter="$SPLITTER_LEFT" 79 | seperator="$SEPERATOR_LEFT" 80 | local seperator_color 81 | decorate::print_bg_color 'seperator_color' "$PRIMARY_COLOR" 82 | seperator_themed="${seperator_color}${seperator}" 83 | local prepare_color= 84 | decorate::print_fg_color 'prepare_color' "$PRIMARY_COLOR" 85 | elif [[ $SEGMENT_POSITION == 'right' ]]; then 86 | part_splitter="$SPLITTER_RIGHT" 87 | seperator="$SEPERATOR_RIGHT" 88 | local seperator_color 89 | decorate::print_fg_color 'seperator_color' "$PRIMARY_COLOR" 90 | seperator_themed="${seperator_color}${seperator}" 91 | local prepare_color= 92 | decorate::print_fg_color 'prepare_color' "$PRIMARY_COLOR" 93 | fi 94 | 95 | local segment_colors 96 | decorate::print_colors 'segment_colors' "$SECONDARY_COLOR" "$PRIMARY_COLOR" 97 | 98 | if [[ -n ${part_splitter/ /} ]]; then 99 | part_splitter=" ${part_splitter} " 100 | else 101 | part_splitter=' ' 102 | fi 103 | 104 | local part_splitter_length="${#part_splitter}" 105 | segment_length="${#seperator}" 106 | themed_segment="$seperator_themed" 107 | themed_segment="${themed_segment}${segment_colors}" 108 | 109 | if [[ ${#segment_parts[@]} -gt 1 ]]; then 110 | local splitter_color_on 111 | decorate::print_fg_color 'splitter_color_on' "$SPLITTER_COLOR" 112 | local splitter_color_off 113 | decorate::print_fg_color 'splitter_color_off' "$SECONDARY_COLOR" 114 | part_splitter_themed="${splitter_color_on}${part_splitter}${splitter_color_off}" 115 | fi 116 | 117 | local themed_parts 118 | 119 | for part in "${segment_parts[@]}"; do 120 | [[ -z $part ]] && continue 121 | part_length="${#part}" 122 | 123 | if [[ -n $themed_parts ]]; then 124 | themed_parts="${themed_parts}${part_splitter_themed}${part}" 125 | segment_length=$((segment_length + part_length + part_splitter_length)) 126 | else 127 | segment_length="$((segment_length + part_length))" 128 | themed_parts="${part}" 129 | fi 130 | done 131 | 132 | themed_segment="${themed_segment} ${themed_parts} " 133 | segment_length=$((segment_length + 2)) 134 | 135 | themed_segment="${themed_segment}${prepare_color}" 136 | printf '%s\n%s' "$segment_length" "$themed_segment" 137 | } 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SBP - Simple Bash Prompt 2 | [![Build Status](https://travis-ci.org/brujoand/sbp.svg?branch=master)](https://travis-ci.org/brujoand/sbp) 3 | 4 | Simple Bash Prompt (SBP) is a bash prompt, which was simple once. 5 | This started out as a pure ripoff from powerline-shell, which is great, but written in python. 6 | SBP is all bash, which makes it fast and fun. 7 | 8 | I've tried making the code as readable and extensible as possible. 9 | If something seems wrong, lacking or bad in some way; feel free to rant, review and create a pull request. 10 | 11 | ![Screenshot](/resources/sbp_screenshot.png) 12 | 13 | For a live demo of this magic [head over 14 | here](https://asciinema.org/a/JuTQxC1wfoUr269Tzw8SMejVl) 15 | 16 | ## Hard Requirements 17 | - Bash 4.3+ 18 | 19 | ## Soft Requirements 20 | If you want the fancy pointy segment separators, you need the powerline fonts _installed_ and _enabled_. Both. 21 | You can get them [here](https://github.com/powerline/fonts) which also has 22 | installation instructions 23 | Now the hard_to_remember part. Change the settings of your terminal emulator. 24 | Something like "Settings" and then "Fonts" will probably be the right place. 25 | If you don't like powerline then use the 'plain' or 'lines' theme or create your 26 | own. If you are using Kitty as a terminal then everything should work out of the 27 | box. 28 | 29 | ## Developer requirements 30 | For local development [pre-commit](https://pre-commit.com/), 31 | [shellcheck](https://www.shellcheck.net/), 32 | [bats](https://bats-core.readthedocs.io/en/stable/installation.html) and 33 | [shfmt](https://github.com/mvdan/sh) are needed. 34 | 35 | ## Installing 36 | 37 | ### With git and the install script 38 | When you clone this repo, there is an install script located at ´bin/install´. 39 | It will add two lines to `$HOME/.bashrc`: 40 | ``` 41 | SBP_PATH=/the/path/to/sbp 42 | source ${SBP_PATH}/sbp.bash 43 | ``` 44 | You could also just add these two lines to some bash config file of your own 45 | choosing manually. Keep in mind that this approach will use the master branch 46 | by default, so expect less stability. 47 | 48 | ## Usage 49 | So you're ready to go. Now you do nothing. Just use it. But you could. If you want. Change stuff up a bit. 50 | Edit your config by running `sbp edit config` and run `sbp reload` if you changed 51 | something substantial. Most changes will be effective immediately. 52 | You can use the `sbp` command for a lot of things: 53 | ``` 54 | Usage: sbp [command] 55 | 56 | Commands: 57 | reload - Reload SBP and user settings 58 | status - Show the current configuration 59 | help - Show this help text 60 | list 61 | config - List all current settings 62 | segments - List all available segments 63 | hooks - List all available hooks 64 | themes - List all available color themes and layouts 65 | edit 66 | config - Opens the sbp config in $EDITOR 67 | colors - Opens the colors config in $EDITOR 68 | set 69 | color - Set [color] for the current session 70 | layout - Set [layout] for the current session 71 | toggle 72 | peekaboo - Toggle execution of [segment] or [hook] 73 | debug - Toggle debug mode 74 | sbp 75 | ``` 76 | 77 | ## Features 78 | ### Segments 79 | Segments can be configured, moved, and hidden depending on your mood, or 80 | environment. Read more about those and how to make your own in the [Segments 81 | Folder](/src/segments). 82 | 83 | ### Hooks 84 | Hooks let's you execute scripts asynchronously to either alert you, or prepare 85 | data in some way. Whatever you want really. Read more about those and how to 86 | make your own in the [Hooks Folder](/src/hooks). 87 | 88 | ### Colors and Layouts 89 | Colors and layouts let you decide how the prompt is drawn. Read more about those 90 | and how to make your own in the [Colors](/src/colors) and 91 | [Layouts](/src/layouts). SBP supports both truecolors through RGB values and 256 colors 92 | by using ansi codes. Many will probably just want to rely on the configuration 93 | set in Xresources, by using the xresources color setting. 94 | 95 | #### Beta - VI mode 96 | ~~The setting `settings_prompt_ready_vi_mode=1` will use the `prompt_ready` icon 97 | with the configured colors and change it's color depending on the current VI 98 | mode if enabled. The cursor will also change from blinking to solid block if 99 | your terminal supports it.~~ 100 | The VI mode support has been removed as it is not possible to predictably place 101 | the VI mode indicator on a multiline prompt. PR's are very welcome if you find a 102 | way to do this. 103 | 104 | ### FAQ 105 | 106 | #### Is this really just bash? 107 | Yes, but actually no. At the time of writing the main implementation has 108 | just a few calls do date, while some segments touch grep and sed but these 109 | are being removed. Sometimes we need to talk to other CLI applications though like 110 | git. 111 | 112 | #### My prompt doesn't show any colors, whats wrong? 113 | You are using a terminal that doesn't support truecolors, the OSX Terminal.app maybe? 114 | You can write your own ansi theme, or use one of the two provided ones, default-256 or xresources. 115 | 116 | #### I don't want to install any fancy fonts, can I still have nice things? 117 | Why yes! Simply use the 'plain' layout. No fonts needed. Or use the 118 | [Kitty](https://sw.kovidgoyal.net/kitty/) terminal which will draw most of the 119 | missing characters for you. 120 | 121 | #### The git segment is too slow 122 | If you're working on a large repository you can speed up git by issuing: 123 | ``` 124 | $ git config core.fsmonitor true 125 | $ git config core.untrackedcache true 126 | ``` 127 | -------------------------------------------------------------------------------- /src/interact.bash: -------------------------------------------------------------------------------- 1 | _sbp_themed_helper="${SBP_PATH}/src/interact_themed.bash" 2 | 3 | _sbp_print_usage() { 4 | cat <