#!/bin/bash # # cheatsheet_viewer.sh # # Copyright (C) 2025, Branko Majic # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # Treat unset variables as errors. set -u PROGRAM="cheatsheet_viewer.sh" VERSION="0.0.1" function usage() { cat <. EOF } function version() { cat < | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU General Public License as published by | | the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU General Public License for more details. | | | | You should have received a copy of the GNU General Public License | | along with this program. If not, see . | +-----------------------------------------------------------------------+ EOF } # Commands # ======== # # Lists available profiles. # # Arguments: # # $1 (config_dir) # Base directory under which all configuration files are stored. # # Returns: # 0 on success, 1 otherwise. # function command_list() { local config_dir="$1" local profiles_dir="$config_dir/profiles" shopt -s nullglob local profiles=("$profiles_dir"/*) shopt -u nullglob if (( ${#profiles[@]} == 0 )); then echo "No profiles defined under $profiles_dir" return 0 fi echo "Available profiles:" echo for profile in "${profiles[@]}"; do echo " - $(basename "$profile")" done } # # Displays profile information. # # Arguments: # # $1 (config_dir) # Base directory under which all configuration files are stored. # # $2 (profile) # Profile name. # # Returns: # 0 on success, 1 otherwise. # function command_info() { local config_dir="$1" local profile="$2" local profile_dir="$config_dir/profiles/$profile" local cheatsheets_file="$profile_dir/cheatsheets" local cheatsheets=() if [[ ! -d $profile_dir ]]; then error "No such profile found under $profile_dir" return 1 fi if [[ -e $cheatsheets_file ]]; then readarray -t cheatsheets < <(grep --invert-match --extended-regexp "(^[[:blank:]]*#|^[[:blank:]]*$)" "$cheatsheets_file") fi echo "Profile $profile" echo "========${profile//?/=}" echo echo "Base directory: $profile_dir" echo "Cheatsheets list: $cheatsheets_file" echo if (( ${#cheatsheets[@]} == 0 )); then echo "No configured cheatsheets." else echo "Configured cheatsheets:" echo for cheatsheet in "${cheatsheets[@]}"; do echo " - $cheatsheet" done fi } # # Displays configured cheatsheets from a profile. # # Arguments: # # $1 (config_dir) # Base directory under which all configuration files are stored. # # $2 (profile) # Profile name. # # $3 (overlay_mode) # Specify if cheatsheets should be shown in overlay mode. # # Returns: # 0 on success, 1 otherwise. # function command_display() { local config_dir="$1" local profile="$2" local overlay_mode="$3" local profile_dir="$config_dir/profiles/$profile" local cheatsheets_file="$profile_dir/cheatsheets" local caller_window_id local cheatsheets=() if [[ ! -d $profile_dir ]]; then error "No such profile found under $profile_dir" return 1 fi if [[ -e $cheatsheets_file ]]; then readarray -t cheatsheets < <(grep --invert-match --extended-regexp "(^[[:blank:]]*#|^[[:blank:]]*$)" "$cheatsheets_file") fi if (( ${#cheatsheets[@]} == 0 )); then warning "No cheatsheets are defined under $cheatsheets_file" return 0 fi if (( overlay_mode )); then caller_window_id=$(xprop -root | grep '^_NET_ACTIVE_WINDOW(WINDOW)' | sed -e 's/.*window id # //') fi local pqiv_options=( "--scale-mode-screen-fraction=1.0" "--zoom-level=1.0" "--transparent-background" "--hide-info-box" "--window-title=cheatsheet-viewer-$profile" ) pqiv "${pqiv_options[@]}" "${cheatsheets[@]}" & if (( overlay_mode )); then local counter=0 while (( counter < 10 )) && ! wmctrl -l | grep -q "cheatsheet-viewer-$profile"; do sleep 0.05 (( counter+=1 )) done wmctrl -F -r "cheatsheet-viewer-$profile" -b add,above wmctrl -i -a "$caller_window_id" fi } # # Toggles display of configured cheatsheets from a profile. # # Arguments: # # $1 (config_dir) # Base directory under which all configuration files are stored. # # $2 (profile) # Profile name. # # $3 (overlay_mode) # Specify if cheatsheets should be shown in overlay mode. # # Returns: # 0 on success, 1 otherwise. # function command_toggle() { local config_dir="$1" local profile="$2" local overlay_mode="$3" local profile_dir="$config_dir/profiles/$profile" local cheatsheets_file="$profile_dir/cheatsheets" local last_cheatsheet_file="$profile_dir/last" local current_process_id current_profile last_cheatsheet caller_window_id local cheatsheets=() if [[ ! -d $profile_dir ]]; then error "No such profile found under $profile_dir" return 1 fi if [[ -e $cheatsheets_file ]]; then readarray -t cheatsheets < <(grep --invert-match --extended-regexp "(^[[:blank:]]*#|^[[:blank:]]*$)" "$cheatsheets_file") fi if (( ${#cheatsheets[@]} == 0 )); then warning "No cheatsheets are defined under $cheatsheets_file" return 0 fi if [[ -e $last_cheatsheet_file ]]; then last_cheatsheet=$(< "$last_cheatsheet_file") else last_cheatsheet="" fi # Get information about existing running cheatsheet viewer pqiv instance (if any). current_process_id=$(wmctrl -lp | grep 'cheatsheet-viewer-' | awk '{print $3}') current_profile=$(wmctrl -lp | grep 'cheatsheet-viewer-' | awk '{print $5}' | sed -e 's/cheatsheet-viewer-//') # Kill off the existing running process. if [[ -n $current_process_id ]]; then kill "$current_process_id" fi # If requested profile is not the same as current one, we want to # show cheatsheets from requested profile. if [[ $current_profile != "$profile" ]]; then if (( overlay_mode )); then caller_window_id=$(xprop -root | grep '^_NET_ACTIVE_WINDOW(WINDOW)' | sed -e 's/.*window id # //') fi local pqiv_options=( "--scale-mode-screen-fraction=1.0" "--zoom-level=1.0" "--transparent-background" "--hide-info-box" "--window-title=cheatsheet-viewer-$profile" ) if [[ -n $last_cheatsheet && -f $last_cheatsheet ]]; then pqiv_options+=("--action=goto_file_byname($last_cheatsheet); set_status_output(1)") else pqiv_options+=("--action=set_status_output(1)") fi ( while read -r line; do eval "$line" echo "$CURRENT_FILE_NAME" > "$profile_dir/last" done < <(pqiv "${pqiv_options[@]}" "${cheatsheets[@]}" | grep --line-buffered '^CURRENT_FILE_NAME=') ) & if (( overlay_mode )); then local counter=0 while (( counter < 10 )) && ! wmctrl -l | grep -q "cheatsheet-viewer-$profile"; do sleep 0.05 (( counter+=1 )) done wmctrl -F -r "cheatsheet-viewer-$profile" -b add,above wmctrl -i -a "$caller_window_id" fi fi } # Set-up colours for message printing if we're not piping and terminal is # capable of outputting the colors. _COLOR_TERMINAL=$(tput colors 2>&1) if [[ -t 1 ]] && (( _COLOR_TERMINAL > 0 )); then _TEXT_BOLD=$(tput bold) _TEXT_WHITE=$(tput setaf 7) _TEXT_BLUE=$(tput setaf 6) _TEXT_GREEN=$(tput setaf 2) _TEXT_YELLOW=$(tput setaf 3) _TEXT_RED=$(tput setaf 1) _TEXT_RESET=$(tput sgr0) else _TEXT_BOLD="" _TEXT_WHITE="" _TEXT_BLUE="" _TEXT_GREEN="" _TEXT_YELLOW="" _TEXT_RED="" _TEXT_RESET="" fi # Set-up functions for printing coloured messages. function debug() { if [[ $DEBUG != 0 ]]; then echo "${_TEXT_BOLD}${_TEXT_BLUE}[DEBUG]${_TEXT_RESET}" "$@" fi } function info() { if [[ $QUIET == 0 ]]; then echo "${_TEXT_BOLD}${_TEXT_WHITE}[INFO] ${_TEXT_RESET}" "$@" fi } function success() { if [[ $QUIET == 0 ]]; then echo "${_TEXT_BOLD}${_TEXT_GREEN}[OK] ${_TEXT_RESET}" "$@" fi } function warning() { echo "${_TEXT_BOLD}${_TEXT_YELLOW}[WARN] ${_TEXT_RESET}" "$@" >&2 } function error() { echo "${_TEXT_BOLD}${_TEXT_RED}[ERROR]${_TEXT_RESET}" "$@" >&2 } # Define error codes. SUCCESS=0 ERROR_ARGUMENTS=1 # Disable debug and quiet modes by default. DEBUG=0 QUIET=0 # Default paths, directories etc. CONFIG_DIR="$HOME/.config/cheatsheet_viewer" # Default option values. OVERLAY_MODE=0 # If no arguments were given, just show usage help. if [[ -z ${1-} ]]; then short_help exit "$SUCCESS" fi # Parse the arguments while getopts "oqdvh" opt; do case "$opt" in o) OVERLAY_MODE=1;; q) QUIET=1;; d) DEBUG=1;; v) version exit "$SUCCESS";; h) long_help exit "$SUCCESS";; *) short_help exit "$ERROR_ARGUMENTS";; esac done i=$OPTIND shift $(( i-1 )) # Quiet and debug are mutually exclusive. if [[ $QUIET != 0 && $DEBUG != 0 ]]; then error "Quiet and debug options are mutually exclusive." exit "$ERROR_ARGUMENTS" fi COMMAND="${1-}" shift if [[ $COMMAND == list ]]; then command_list "$CONFIG_DIR" elif [[ $COMMAND == info ]]; then PROFILE="${1-}" shift if [[ -z $PROFILE ]]; then error "Profile name must be specified." exit "$ERROR_ARGUMENTS" fi command_info "$CONFIG_DIR" "$PROFILE" elif [[ $COMMAND == display ]]; then PROFILE="${1-default}" shift command_display "$CONFIG_DIR" "$PROFILE" "$OVERLAY_MODE" elif [[ $COMMAND == toggle ]]; then PROFILE="${1-default}" shift command_toggle "$CONFIG_DIR" "$PROFILE" "$OVERLAY_MODE" else error "Unsupported command: $COMMAND" exit "$ERROR_ARGUMENTS" fi