forked from michael.heier/citadel-core
feat: add Citadel CLI (#33)
add commands for: - Show status for all services - Run a command inside a (app) container - Switch branch / update channel - Switch Bitcoin/Electrum/Lightning implementation - app commands - debug command - Fix update / backup / starting stuck - Edit node configs (Bitcoin Core, LND) - Edit app configs (Nextcloud etc.) - Memory usage / System info - Start - Stop - Restart - Reboot - Shutdown - List all installed apps & services - Logs - Version - Help
This commit is contained in:
parent
a8d224f597
commit
07aa73b59e
1
bin/citadel
Symbolic link
1
bin/citadel
Symbolic link
|
@ -0,0 +1 @@
|
|||
../cli/citadel
|
487
cli/citadel
Executable file
487
cli/citadel
Executable file
|
@ -0,0 +1,487 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# SPDX-FileCopyrightText: 2022 Citadel and contributors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CITADEL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/..)"
|
||||
CLI_NAME="$(basename $0)"
|
||||
CLI_VERSION="0.0.1"
|
||||
CLI_DIR="$(dirname "$(readlink -f "$0")")"
|
||||
SERVICE_NAME="citadel-startup"
|
||||
EDITOR="${EDITOR:-micro}"
|
||||
|
||||
source $CLI_DIR/utils/functions.sh
|
||||
source $CLI_DIR/utils/multiselect.sh
|
||||
source $CLI_DIR/utils/spinner.sh
|
||||
source $CLI_DIR/utils/helpers.sh
|
||||
|
||||
if [ -z ${1+x} ]; then
|
||||
command=""
|
||||
else
|
||||
command="$1"
|
||||
fi
|
||||
|
||||
# Check Citadel Status
|
||||
if [[ "$command" = "status" ]]; then
|
||||
POSITIONAL_ARGS=()
|
||||
|
||||
long=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-l | --long)
|
||||
long=true
|
||||
shift
|
||||
;;
|
||||
-* | --*)
|
||||
echo "Unknown option $1"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
POSITIONAL_ARGS+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
set -- "${POSITIONAL_ARGS[@]}"
|
||||
|
||||
free -m | awk 'NR==2{printf "Memory Usage: %s/%sMB (%.2f%%)\n", $3,$2,$3*100/$2 }'
|
||||
df -h | awk '$NF=="/"{printf "Disk Usage: %d/%dGB (%s)\n", $3,$2,$5}'
|
||||
top -bn1 | grep load | awk '{printf "CPU Load: %.2f\n", $(NF-2)}'
|
||||
|
||||
echo
|
||||
|
||||
if $long; then
|
||||
docker container ls --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"
|
||||
else
|
||||
docker container ls --format "table {{.Names}}\t{{.Status}}"
|
||||
fi
|
||||
|
||||
if [[ $(pgrep -f karen) ]]; then
|
||||
printf "\nKaren is listening.\n"
|
||||
else
|
||||
printf "\nERROR: Karen is not listening.\n"
|
||||
fi
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
# Update Citadel
|
||||
if [[ "$command" = "update" ]]; then
|
||||
POSITIONAL_ARGS=()
|
||||
|
||||
branch=$(get_update_channel)
|
||||
force=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-b | --branch)
|
||||
branch="$2"
|
||||
shift # past argument
|
||||
shift # past value
|
||||
;;
|
||||
--force)
|
||||
force=true
|
||||
shift # past argument
|
||||
;;
|
||||
-* | --*)
|
||||
echo "Unknown option $1"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
POSITIONAL_ARGS+=("$1") # save positional arg
|
||||
shift # past argument
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters
|
||||
|
||||
if $force; then
|
||||
sudo rm -f $CITADEL_ROOT/statuses/update-in-progress
|
||||
fi
|
||||
|
||||
sudo $CITADEL_ROOT/scripts/update/update --repo runcitadel/core#$branch
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
# Start Citadel
|
||||
if [[ "$command" = "start" ]]; then
|
||||
if $(is_managed_by_systemd); then
|
||||
if $(is_service_active); then
|
||||
echo 'Citadel is already running.'
|
||||
else
|
||||
sudo systemctl start citadel-startup
|
||||
fi
|
||||
else
|
||||
sudo $CITADEL_ROOT/scripts/start
|
||||
fi
|
||||
exit
|
||||
fi
|
||||
|
||||
# Stop Citadel
|
||||
if [[ "$command" = "stop" ]]; then
|
||||
active=$(is_service_active)
|
||||
|
||||
if $(is_managed_by_systemd) && $active; then
|
||||
if $(is_service_active); then
|
||||
sudo systemctl stop citadel-startup
|
||||
else
|
||||
echo 'Citadel is not running.'
|
||||
fi
|
||||
else
|
||||
sudo $CITADEL_ROOT/scripts/stop
|
||||
fi
|
||||
exit
|
||||
fi
|
||||
|
||||
# Restart Citadel
|
||||
if [[ "$command" = "restart" ]]; then
|
||||
shift
|
||||
|
||||
# TODO: enable restarting services
|
||||
|
||||
if [ ! -z ${1+x} ]; then
|
||||
echo "Too many arguments."
|
||||
echo "Usage: \`$CLI_NAME $command\`"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if $(is_managed_by_systemd); then
|
||||
sudo systemctl restart $SERVICE_NAME
|
||||
else
|
||||
sudo $CITADEL_ROOT/scripts/stop
|
||||
sudo $CITADEL_ROOT/scripts/start
|
||||
fi
|
||||
exit
|
||||
fi
|
||||
|
||||
# Reboot the system
|
||||
if [[ "$command" = "reboot" ]]; then
|
||||
$CLI_NAME stop || true
|
||||
sudo reboot
|
||||
exit
|
||||
fi
|
||||
|
||||
# Shutdown the system
|
||||
if [[ "$command" = "shutdown" ]]; then
|
||||
$CLI_NAME stop || true
|
||||
sudo shutdown now
|
||||
exit
|
||||
fi
|
||||
|
||||
# List all installed services apps
|
||||
if [[ "$command" = "list" ]]; then
|
||||
echo 'karen'
|
||||
docker ps --format "{{.Names}}"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Run a command inside a container
|
||||
if [[ "$command" = "run" ]]; then
|
||||
shift
|
||||
|
||||
if [ -z ${1+x} ]; then
|
||||
echo "Specify an app or service."
|
||||
echo "Usage: \`$CLI_NAME $command <service> \"<command>\"\`"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z ${2+x} ]; then
|
||||
echo "Specify a command to run."
|
||||
echo "Usage: \`$CLI_NAME $command <service> \"<command>\"\`"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker exec -t $1 sh -c "$2" || {
|
||||
echo "To see all installed services & apps use \`$CLI_NAME list\`"
|
||||
echo "Usage: \`$CLI_NAME $command <service> \"<command>\"\`"
|
||||
exit 1
|
||||
}
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
# Configure Citadel
|
||||
if [[ "$command" = "set" ]]; then
|
||||
shift
|
||||
|
||||
if [ -z ${1+x} ]; then
|
||||
echo "Missing subcommand."
|
||||
echo "Usage: \`$CLI_NAME $command <subcommand>\`"
|
||||
exit 1
|
||||
else
|
||||
subcommand="$1"
|
||||
fi
|
||||
|
||||
# Switch update channel
|
||||
if [[ "$subcommand" = "update-channel" ]]; then
|
||||
if [ -z ${2+x} ]; then
|
||||
echo "Specify an update channel to switch to."
|
||||
echo "Usage: \`$CLI_NAME $subcommand <stable|beta|c-lightning>\`"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case $2 in "stable" | "beta" | "c-lightning")
|
||||
# continue
|
||||
;;
|
||||
*)
|
||||
echo "Not a valid update channel: \"$2\""
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
sudo $CITADEL_ROOT/scripts/set-update-channel $2
|
||||
$CLI_NAME update
|
||||
exit
|
||||
fi
|
||||
|
||||
# Switch Bitcoin/Electrum implementation
|
||||
if [[ "$subcommand" = "implementation" ]] || [[ "$subcommand" = "impl" ]]; then
|
||||
shift
|
||||
sudo $CITADEL_ROOT/services/manage.py set $@
|
||||
$CLI_NAME restart
|
||||
exit
|
||||
fi
|
||||
|
||||
# Switch Bitcoin network
|
||||
if [[ "$subcommand" = "network" ]]; then
|
||||
shift
|
||||
|
||||
if [ -z ${1+x} ]; then
|
||||
echo "Specify a network to switch to."
|
||||
echo "Usage: \`$CLI_NAME $subcommand <mainnet|signet|testnet|regtest>\`"
|
||||
else
|
||||
case $1 in
|
||||
"mainnet" | "testnet" | "signet" | "regtest")
|
||||
sudo $CITADEL_ROOT/scripts/stop
|
||||
sudo OVERWRITE_NETWORK=$1 $CITADEL_ROOT/scripts/configure
|
||||
sudo $CITADEL_ROOT/scripts/start
|
||||
;;
|
||||
*)
|
||||
echo "Not a valid value for network"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
echo "\"$subcommand\" is not a valid subcommand."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# App commands
|
||||
if [[ "$command" = "app" ]]; then
|
||||
shift
|
||||
sudo $CITADEL_ROOT/scripts/app $@
|
||||
exit
|
||||
fi
|
||||
|
||||
# Edit common app configuration files
|
||||
if [[ "$command" = "configure" ]]; then
|
||||
if [ -z ${2+x} ]; then
|
||||
echo "Specify an app or service to configure."
|
||||
echo "Usage: \`$CLI_NAME $command <service>\`"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
POSITIONAL_ARGS=()
|
||||
|
||||
persist=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--persist)
|
||||
persist=true
|
||||
shift # past argument
|
||||
;;
|
||||
-* | --*)
|
||||
echo "Unknown option $1"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
POSITIONAL_ARGS+=("$1") # save positional arg
|
||||
shift # past argument
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters
|
||||
|
||||
# These service and app configs are already persisted
|
||||
# TODO: add more apps
|
||||
|
||||
if [[ "$2" = "nextcloud" ]]; then
|
||||
edit_file --priviledged $CITADEL_ROOT/app-data/nextcloud/data/nextcloud/config/config.php
|
||||
prompt_apply_config nextcloud-web-1 false
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ "$2" = "nginx" ]]; then
|
||||
edit_file $CITADEL_ROOT/nginx/nginx.conf
|
||||
prompt_apply_config nginx false
|
||||
exit
|
||||
fi
|
||||
|
||||
if $persist; then
|
||||
echo "NOTE: As of now persisted config changes will not be kept when updating Citadel."
|
||||
else
|
||||
echo "NOTE: Some changes to this configuration file may be overwritten the next time you start Citadel."
|
||||
echo "To persist the changes run the command again with \`$CLI_NAME configure $2 --persist\`"
|
||||
fi
|
||||
|
||||
read -p "Continue? [Y/n] " should_continue
|
||||
echo
|
||||
if [[ $should_continue =~ [Nn]$ ]]; then
|
||||
exit
|
||||
fi
|
||||
|
||||
# Service and app configs below may be overwritten
|
||||
# TODO: check which implementation is running
|
||||
# and do "bitcoin" / "lightning" / "electrum"
|
||||
|
||||
if [[ "$2" = "bitcoin" ]]; then
|
||||
if $persist; then
|
||||
edit_file $CITADEL_ROOT/templates/bitcoin-sample.conf
|
||||
prompt_apply_config bitcoin true
|
||||
else
|
||||
edit_file $CITADEL_ROOT/bitcoin/bitcoin.conf
|
||||
prompt_apply_config bitcoin false
|
||||
fi
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ "$2" = "lnd" ]]; then
|
||||
if $persist; then
|
||||
edit_file $CITADEL_ROOT/templates/lnd-sample.conf
|
||||
prompt_apply_config lightning true
|
||||
else
|
||||
edit_file $CITADEL_ROOT/lnd/lnd.conf
|
||||
prompt_apply_config lightning false
|
||||
fi
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ "$2" = "electrs" ]]; then
|
||||
edit_file $CITADEL_ROOT/templates/electrs-sample.toml
|
||||
prompt_apply_config electrum true
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ "$2" = "fulcrumx" ]]; then
|
||||
if $persist; then
|
||||
edit_file $CITADEL_ROOT/templates/fulcrumx-sample.conf
|
||||
prompt_apply_config electrum true
|
||||
else
|
||||
edit_file $CITADEL_ROOT/fulcrumx/fulcrumx.conf
|
||||
prompt_apply_config electrum false
|
||||
fi
|
||||
exit
|
||||
fi
|
||||
|
||||
echo "No service or app \"$2\" not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Show logs for apps & services
|
||||
if [[ "$command" = "logs" ]]; then
|
||||
shift
|
||||
|
||||
POSITIONAL_ARGS=()
|
||||
|
||||
follow=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-f | --follow)
|
||||
follow=true
|
||||
shift # past argument
|
||||
;;
|
||||
-n | --tail)
|
||||
number_of_lines="$2"
|
||||
shift # past argument
|
||||
shift # past value
|
||||
;;
|
||||
-* | --*)
|
||||
echo "Unknown option $1"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
POSITIONAL_ARGS+=("$1") # save positional arg
|
||||
shift # past argument
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters
|
||||
|
||||
# Set default number_of_lines if not set by user
|
||||
if [ -z ${number_of_lines+x} ]; then
|
||||
if [[ ${#POSITIONAL_ARGS[@]} == 0 ]] || [[ ${#POSITIONAL_ARGS[@]} == 1 ]]; then
|
||||
number_of_lines=40
|
||||
else
|
||||
number_of_lines=10
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z ${1+x} ] || [[ "$1" = "karen" ]]; then
|
||||
if [[ ${#POSITIONAL_ARGS[@]} == 2 ]]; then
|
||||
echo "Karen logs cannot be viewed together with other services."
|
||||
echo "Usage: \`$CLI_NAME $command karen\`"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tail $($follow && echo "-f") -n $number_of_lines $CITADEL_ROOT/logs/karen.log
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ ${#POSITIONAL_ARGS[@]} == 1 ]]; then
|
||||
docker logs $($follow && echo "-f") --tail $number_of_lines $@ || {
|
||||
echo "To see all installed services & apps use \`$CLI_NAME list\`"
|
||||
echo "Usage: \`$CLI_NAME $command <service>\`"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
# TODO: can only show logs for services in docker-compose.yml
|
||||
docker compose logs $($follow && echo "-f") --tail $number_of_lines $@ || {
|
||||
echo "To see all installed services & apps use \`$CLI_NAME list\`"
|
||||
echo "Usage: \`$CLI_NAME $command <service>\`"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
# Debug Citadel
|
||||
if [[ "$command" = "debug" ]]; then
|
||||
shift
|
||||
sudo $CITADEL_ROOT/scripts/debug $@
|
||||
exit
|
||||
fi
|
||||
|
||||
# Show version information for this CLI
|
||||
if [[ "$command" = "--version" ]] || [[ "$command" = "-v" ]]; then
|
||||
citadel_version=$(jq -r '.version' $CITADEL_ROOT/info.json)
|
||||
echo "Citadel v$citadel_version"
|
||||
echo "citadel-cli v$CLI_VERSION"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Show usage information for this CLI
|
||||
if [[ "$command" = "--help" ]] || [[ "$command" = "-h" ]]; then
|
||||
show_help
|
||||
exit
|
||||
fi
|
||||
|
||||
# If we get here it means no valid command was supplied
|
||||
# Show help and exit
|
||||
show_help
|
||||
exit
|
111
cli/utils/functions.sh
Normal file
111
cli/utils/functions.sh
Normal file
|
@ -0,0 +1,111 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
${CLI_NAME}-cli v${CLI_VERSION}
|
||||
Manage your Citadel.
|
||||
|
||||
Usage: ${CLI_NAME} <command> [options]
|
||||
|
||||
Flags:
|
||||
-h, --help Show this help message
|
||||
-v, --version Show version information for this CLI
|
||||
|
||||
Commands:
|
||||
status Check the status of all services
|
||||
start Start the Citadel service
|
||||
stop Stop the Citadel service safely
|
||||
restart Restart the Citadel service
|
||||
reboot Reboot the system
|
||||
shutdown Shutdown the system
|
||||
update Update Citadel
|
||||
list List all installed services apps
|
||||
run <service> "<command>" Run a command inside a container
|
||||
set <command> Switch between Bitcoin & Lightning implementations
|
||||
app <command> Install, update or restart apps
|
||||
configure <service> Edit service & app configuration files
|
||||
logs <service> Show logs for an app or service
|
||||
debug View logs for troubleshooting
|
||||
EOF
|
||||
}
|
||||
|
||||
is_managed_by_systemd() {
|
||||
if systemctl --all --type service | grep -q "$SERVICE_NAME"; then
|
||||
echo true
|
||||
else
|
||||
echo false
|
||||
fi
|
||||
}
|
||||
|
||||
is_service_active() {
|
||||
service_status=$(systemctl is-active $SERVICE_NAME)
|
||||
if [[ "$service_status" = "active" ]]; then
|
||||
echo true
|
||||
else
|
||||
echo false
|
||||
fi
|
||||
}
|
||||
|
||||
edit_file() {
|
||||
if [[ $1 = "--priviledged" ]]; then
|
||||
echo "Editing this file requires elevated priviledges."
|
||||
|
||||
if ! sudo test -f $2; then
|
||||
echo "File not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if sudo test -w $2; then
|
||||
sudo $EDITOR $2
|
||||
else
|
||||
echo "File not writable."
|
||||
fi
|
||||
else
|
||||
if ! test -f $1; then
|
||||
echo "File not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if test -w $1; then
|
||||
$EDITOR $1
|
||||
else
|
||||
echo "File not writable."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
get_update_channel() {
|
||||
update_channel_line=$(cat $CITADEL_ROOT/.env | grep UPDATE_CHANNEL)
|
||||
update_channel=(${update_channel_line//=/ })
|
||||
|
||||
if [ -z ${update_channel[1]+x} ]; then
|
||||
# fall back to stable
|
||||
echo "stable"
|
||||
else
|
||||
echo ${update_channel[1]}
|
||||
fi
|
||||
}
|
||||
|
||||
prompt_apply_config() {
|
||||
service=$1
|
||||
persisted=$2
|
||||
|
||||
read -p "Do you want to apply the changes now? [y/N] " should_restart
|
||||
echo
|
||||
if [[ $should_restart =~ [Yy]$ ]]; then
|
||||
if $persisted; then
|
||||
sudo $CITADEL_ROOT/scripts/configure
|
||||
fi
|
||||
|
||||
printf "\nRestarting service \"$service\"...\n"
|
||||
docker restart $service
|
||||
echo "Done."
|
||||
else
|
||||
if $persisted; then
|
||||
echo "To apply the changes, restart Citadel by running \`$CLI_NAME restart\`."
|
||||
else
|
||||
echo "To apply the changes, restart service \"$service\" by running \`docker restart $service\`."
|
||||
fi
|
||||
fi
|
||||
}
|
11
cli/utils/helpers.sh
Normal file
11
cli/utils/helpers.sh
Normal file
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
trim() {
|
||||
local var="$*"
|
||||
# remove leading whitespace characters
|
||||
var="${var#"${var%%[![:space:]]*}"}"
|
||||
# remove trailing whitespace characters
|
||||
var="${var%"${var##*[![:space:]]}"}"
|
||||
printf '%s' "$var"
|
||||
}
|
123
cli/utils/multiselect.sh
Normal file
123
cli/utils/multiselect.sh
Normal file
|
@ -0,0 +1,123 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
function multiselect() {
|
||||
# little helpers for terminal print control and key input
|
||||
ESC=$(printf "\033")
|
||||
cursor_blink_on() { printf "$ESC[?25h"; }
|
||||
cursor_blink_off() { printf "$ESC[?25l"; }
|
||||
cursor_to() { printf "$ESC[$1;${2:-1}H"; }
|
||||
print_inactive() { printf "$2 $1 "; }
|
||||
print_active() { printf "$2 $ESC[7m $1 $ESC[27m"; }
|
||||
get_cursor_row() {
|
||||
IFS=';' read -sdR -p $'\E[6n' ROW COL
|
||||
echo ${ROW#*[}
|
||||
}
|
||||
|
||||
local return_value=$1
|
||||
local -n options=$2
|
||||
local -n defaults=$3
|
||||
|
||||
local selected=()
|
||||
for ((i = 0; i < ${#options[@]}; i++)); do
|
||||
if [[ ${defaults[i]} = "true" ]]; then
|
||||
selected+=("true")
|
||||
else
|
||||
selected+=("false")
|
||||
fi
|
||||
printf "\n"
|
||||
done
|
||||
|
||||
# determine current screen position for overwriting the options
|
||||
local lastrow=$(get_cursor_row)
|
||||
local startrow=$(($lastrow - ${#options[@]}))
|
||||
|
||||
# ensure cursor and input echoing back on upon a ctrl+c during read -s
|
||||
trap "cursor_blink_on; stty echo; printf '\n'; exit" 2
|
||||
cursor_blink_off
|
||||
|
||||
key_input() {
|
||||
local key
|
||||
IFS= read -rsn1 key 2>/dev/null >&2
|
||||
if [[ $key = "" ]]; then echo enter; fi
|
||||
if [[ $key = $'\x20' ]]; then echo space; fi
|
||||
if [[ $key = "k" ]]; then echo up; fi
|
||||
if [[ $key = "j" ]]; then echo down; fi
|
||||
if [[ $key = $'\x1b' ]]; then
|
||||
read -rsn2 key
|
||||
if [[ $key = [A || $key = k ]]; then echo up; fi
|
||||
if [[ $key = [B || $key = j ]]; then echo down; fi
|
||||
fi
|
||||
}
|
||||
|
||||
toggle_option() {
|
||||
local option=$1
|
||||
if [[ ${selected[option]} == true ]]; then
|
||||
selected[option]=false
|
||||
else
|
||||
selected[option]=true
|
||||
fi
|
||||
}
|
||||
|
||||
print_options() {
|
||||
# print options by overwriting the last lines
|
||||
local idx=0
|
||||
|
||||
for option in "${options[@]}"; do
|
||||
local prefix="[ ]"
|
||||
|
||||
if [[ ${selected[idx]} == true ]]; then
|
||||
prefix="[\e[38;5;46m✔\e[0m]"
|
||||
fi
|
||||
|
||||
cursor_to $(($startrow + $idx))
|
||||
if [ $idx -eq $1 ]; then
|
||||
print_active "$option" "$prefix"
|
||||
else
|
||||
print_inactive "$option" "$prefix"
|
||||
fi
|
||||
|
||||
idx=$((idx + 1))
|
||||
done
|
||||
}
|
||||
|
||||
get_result() {
|
||||
local result=()
|
||||
for ((i = 0; i < ${#options[@]}; i++)); do
|
||||
if ${selected[i]}; then
|
||||
result+=(${options[i]})
|
||||
fi
|
||||
done
|
||||
|
||||
echo "${result[@]}"
|
||||
}
|
||||
|
||||
local active=0
|
||||
while true; do
|
||||
print_options $active
|
||||
|
||||
# user key control
|
||||
case $(key_input) in
|
||||
space) toggle_option $active ;;
|
||||
enter)
|
||||
print_options -1
|
||||
break
|
||||
;;
|
||||
up)
|
||||
active=$((active - 1))
|
||||
if [ $active -lt 0 ]; then active=$((${#options[@]} - 1)); fi
|
||||
;;
|
||||
down)
|
||||
active=$((active + 1))
|
||||
if [ $active -ge ${#options[@]} ]; then active=0; fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# cursor position back to normal
|
||||
cursor_to $lastrow
|
||||
printf "\n"
|
||||
cursor_blink_on
|
||||
|
||||
eval $return_value='("$(get_result)")'
|
||||
}
|
87
cli/utils/spinner.sh
Normal file
87
cli/utils/spinner.sh
Normal file
|
@ -0,0 +1,87 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Author: Tasos Latsas
|
||||
|
||||
# spinner.sh
|
||||
#
|
||||
# Display an awesome 'spinner' while running your long shell commands
|
||||
#
|
||||
# Do *NOT* call _spinner function directly.
|
||||
# Use {start,stop}_spinner wrapper functions
|
||||
|
||||
# usage:
|
||||
# 1. source this script in your's
|
||||
# 2. start the spinner:
|
||||
# start_spinner [display-message-here]
|
||||
# 3. run your command
|
||||
# 4. stop the spinner:
|
||||
# stop_spinner [your command's exit status]
|
||||
#
|
||||
# Also see: test.sh
|
||||
|
||||
function _spinner() {
|
||||
# $1 start/stop
|
||||
#
|
||||
# on start: $2 display message
|
||||
# on stop : $2 process exit status
|
||||
# $3 spinner function pid (supplied from stop_spinner)
|
||||
|
||||
local on_success="DONE"
|
||||
local on_fail="FAIL"
|
||||
local white="\e[1;37m"
|
||||
local green="\e[1;32m"
|
||||
local red="\e[1;31m"
|
||||
local nc="\e[0m"
|
||||
|
||||
case $1 in
|
||||
start)
|
||||
# display message with some space
|
||||
echo -ne "${2} "
|
||||
|
||||
# start spinner
|
||||
i=1
|
||||
sp='\|/-'
|
||||
delay=${SPINNER_DELAY:-0.15}
|
||||
|
||||
while :; do
|
||||
printf "\b${sp:i++%${#sp}:1}"
|
||||
sleep $delay
|
||||
done
|
||||
;;
|
||||
stop)
|
||||
if [[ -z ${3} ]]; then
|
||||
echo "spinner is not running.."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
kill $3 >/dev/null 2>&1
|
||||
|
||||
# inform the user upon success or failure
|
||||
echo -en "\b["
|
||||
if [[ $2 -eq 0 ]]; then
|
||||
echo -en "${green}${on_success}${nc}"
|
||||
else
|
||||
echo -en "${red}${on_fail}${nc}"
|
||||
fi
|
||||
echo -e "]"
|
||||
;;
|
||||
*)
|
||||
echo "invalid argument, try {start/stop}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function start_spinner() {
|
||||
# $1 : msg to display
|
||||
_spinner "start" "${1}" &
|
||||
# set global spinner pid
|
||||
_sp_pid=$!
|
||||
disown
|
||||
}
|
||||
|
||||
function stop_spinner() {
|
||||
# $1 : command exit status
|
||||
_spinner "stop" $1 $_sp_pid
|
||||
unset _sp_pid
|
||||
}
|
Loading…
Reference in New Issue
Block a user