From 5417494fc3ae87f620f53258b9ae29a3bd2cd5a8 Mon Sep 17 00:00:00 2001 From: Solomon Laing Date: Sat, 11 Feb 2023 15:44:26 +1030 Subject: [PATCH] minor updates and addition of personal scripts to local/bin --- .config/bash/.bashrc | 2 +- .config/lf/lfrc | 2 + .config/zsh/.zshrc | 2 +- .local/bin/bash-status-bat | 12 + .local/bin/bash-status-git | 22 + .local/bin/bashmatrix | 70 ++ .local/bin/bw-unlock | 29 + .local/bin/checkup | 17 + .local/bin/cht.sh | 19 + .local/bin/compiler | 59 + .local/bin/cron/README.md | 11 + .local/bin/cron/checkup | 17 + .local/bin/cron/crontog | 6 + .local/bin/crontog | 6 + .local/bin/dec-sink-volume | 11 + .local/bin/dec-source-volume | 11 + .local/bin/displayselect | 83 ++ .local/bin/dmenu-bluetooth | 317 ++++++ .local/bin/dmenumount | 67 ++ .local/bin/dmenumountcifs | 19 + .local/bin/dmenupass | 6 + .local/bin/dmenuprompt | 21 + .local/bin/dmenusearch | 91 ++ .local/bin/dmenuumount | 21 + .local/bin/duck | 4 + .local/bin/ext | 45 + .local/bin/fishies | 1449 +++++++++++++++++++++++++ .local/bin/getcomproot | 12 + .local/bin/getnf | 103 ++ .local/bin/google | 4 + .local/bin/ifinstalled | 12 + .local/bin/inc-sink-volume | 11 + .local/bin/inc-source-volume | 11 + .local/bin/iommu.sh | 8 + .local/bin/isosec | 2 + .local/bin/kbswitcher | 26 + .local/bin/lfub | 24 + .local/bin/lynx | 23 + .local/bin/maimpick | 18 + .local/bin/mod_backlight | 19 + .local/bin/networkmanager_dmenu | 924 ++++++++++++++++ .local/bin/opout | 13 + .local/bin/rotdir | 12 + .local/bin/rssadd | 18 + .local/bin/screenlayout | 16 + .local/bin/screenshot | 13 + .local/bin/set-default-sink | 13 + .local/bin/set-default-source | 13 + .local/bin/setvol | 13 + .local/bin/status-bat | 44 + .local/bin/status-disk | 10 + .local/bin/status-net | 61 ++ .local/bin/status-vol | 27 + .local/bin/switchkb | 7 + .local/bin/tmux-sessioniser | 28 + .local/bin/tmux-windowiser | 13 + .local/bin/tmux-worker | 15 + .local/bin/toggle-sink-mute | 11 + .local/bin/toggle-source-mute | 11 + .local/bin/trayer-toggle | 13 + .local/bin/urlencode | 35 + .local/bin/vic | 4 + .local/bin/xmobar-status-bat | 9 + .local/bin/xmobar-status-disk | 7 + .local/bin/xmobar-status-keyboard | 32 + .local/bin/xmobar-status-net | 87 ++ .local/bin/xmobar-status-updates | 6 + .local/bin/xmobar-status-vol | 46 + .local/bin/xmobar-status-weather | 36 + .local/bin/xmobar-trayer-padding-icon | 48 + .local/bin/xmonad-keys-help | 12 + .local/bin/zk-gen-dir-md-toc | 40 + .local/bin/zk-gen-index | 14 + scripts/ganttproject | 3 + scripts/update-dmenu-bluetooth | 33 + scripts/update-networkmanager-dmenu | 33 + scripts/update-voidrice | 40 + scripts/zotero | 3 + 78 files changed, 4453 insertions(+), 2 deletions(-) create mode 100755 .local/bin/bash-status-bat create mode 100755 .local/bin/bash-status-git create mode 100755 .local/bin/bashmatrix create mode 100755 .local/bin/bw-unlock create mode 100755 .local/bin/checkup create mode 100755 .local/bin/cht.sh create mode 100755 .local/bin/compiler create mode 100644 .local/bin/cron/README.md create mode 100755 .local/bin/cron/checkup create mode 100755 .local/bin/cron/crontog create mode 100755 .local/bin/crontog create mode 100755 .local/bin/dec-sink-volume create mode 100755 .local/bin/dec-source-volume create mode 100755 .local/bin/displayselect create mode 100755 .local/bin/dmenu-bluetooth create mode 100755 .local/bin/dmenumount create mode 100755 .local/bin/dmenumountcifs create mode 100755 .local/bin/dmenupass create mode 100755 .local/bin/dmenuprompt create mode 100755 .local/bin/dmenusearch create mode 100755 .local/bin/dmenuumount create mode 100755 .local/bin/duck create mode 100755 .local/bin/ext create mode 100755 .local/bin/fishies create mode 100755 .local/bin/getcomproot create mode 100755 .local/bin/getnf create mode 100755 .local/bin/google create mode 100755 .local/bin/ifinstalled create mode 100755 .local/bin/inc-sink-volume create mode 100755 .local/bin/inc-source-volume create mode 100755 .local/bin/iommu.sh create mode 100755 .local/bin/isosec create mode 100755 .local/bin/kbswitcher create mode 100755 .local/bin/lfub create mode 100755 .local/bin/lynx create mode 100755 .local/bin/maimpick create mode 100755 .local/bin/mod_backlight create mode 100755 .local/bin/networkmanager_dmenu create mode 100755 .local/bin/opout create mode 100755 .local/bin/rotdir create mode 100755 .local/bin/rssadd create mode 100755 .local/bin/screenlayout create mode 100755 .local/bin/screenshot create mode 100755 .local/bin/set-default-sink create mode 100755 .local/bin/set-default-source create mode 100755 .local/bin/setvol create mode 100755 .local/bin/status-bat create mode 100755 .local/bin/status-disk create mode 100755 .local/bin/status-net create mode 100755 .local/bin/status-vol create mode 100755 .local/bin/switchkb create mode 100755 .local/bin/tmux-sessioniser create mode 100755 .local/bin/tmux-windowiser create mode 100755 .local/bin/tmux-worker create mode 100755 .local/bin/toggle-sink-mute create mode 100755 .local/bin/toggle-source-mute create mode 100755 .local/bin/trayer-toggle create mode 100755 .local/bin/urlencode create mode 100755 .local/bin/vic create mode 100755 .local/bin/xmobar-status-bat create mode 100755 .local/bin/xmobar-status-disk create mode 100755 .local/bin/xmobar-status-keyboard create mode 100755 .local/bin/xmobar-status-net create mode 100755 .local/bin/xmobar-status-updates create mode 100755 .local/bin/xmobar-status-vol create mode 100755 .local/bin/xmobar-status-weather create mode 100755 .local/bin/xmobar-trayer-padding-icon create mode 100755 .local/bin/xmonad-keys-help create mode 100755 .local/bin/zk-gen-dir-md-toc create mode 100755 .local/bin/zk-gen-index create mode 100755 scripts/ganttproject create mode 100755 scripts/update-dmenu-bluetooth create mode 100755 scripts/update-networkmanager-dmenu create mode 100755 scripts/update-voidrice create mode 100755 scripts/zotero diff --git a/.config/bash/.bashrc b/.config/bash/.bashrc index 2950610..dee6490 100644 --- a/.config/bash/.bashrc +++ b/.config/bash/.bashrc @@ -7,7 +7,7 @@ case $- in esac export GOPATH="$HOME/go" -export PATH="$PATH:$HOME/.local/bin:$GOPATH/bin" +export PATH="$PATH:$HOME/.local/bin:$GOPATH/bin:$HOME/scripts" # don't put duplicate lines or lines starting with space in the history. # See bash(1) for more options diff --git a/.config/lf/lfrc b/.config/lf/lfrc index c86c120..5a56a42 100644 --- a/.config/lf/lfrc +++ b/.config/lf/lfrc @@ -178,6 +178,8 @@ map gcxm cd ~/.xmonad map gcs cd ~/.config/shell map gcz cd ~/.config/zsh map gcx1 cd ~/.config/x11 +map gll cd ~/.local +map glb cd ~/.local/bin map gw cd ~/work map gNN cd ~/notes/ map gNw cd ~/notes/zk/work/ diff --git a/.config/zsh/.zshrc b/.config/zsh/.zshrc index 7025e91..9223a40 100644 --- a/.config/zsh/.zshrc +++ b/.config/zsh/.zshrc @@ -1,6 +1,6 @@ # If you come from bash you might have to change your $PATH. export GOPATH="$HOME/go" -export PATH=$HOME/.local/bin:$GOPATH/bin:$PATH +export PATH=$HOME/.local/bin:$GOPATH/bin:$PATH:$HOME/scripts # Path to your oh-my-zsh installation. export ZSH="/home/solomon/.oh-my-zsh" diff --git a/.local/bin/bash-status-bat b/.local/bin/bash-status-bat new file mode 100755 index 0000000..5990f92 --- /dev/null +++ b/.local/bin/bash-status-bat @@ -0,0 +1,12 @@ +#!/bin/bash + +[[ "$(upower -i $(upower -e | grep 'BAT'))" == "" ]] && exit + +current=$(upower -i $(upower -e | grep 'BAT') | grep -E "percentage" | sed 's/.*://' | sed 's/ *//') +state=$(upower -i $(upower -e | grep 'BAT') | grep -E "state" | sed 's/.*://' | sed 's/ *//') + +if [[ "$state" == "not charging" ]]; then + state="full" +fi + +echo "$current $state" diff --git a/.local/bin/bash-status-git b/.local/bin/bash-status-git new file mode 100755 index 0000000..68350f3 --- /dev/null +++ b/.local/bin/bash-status-git @@ -0,0 +1,22 @@ +#!/bin/bash + +if git status &>/dev/null; then + branch=$(git branch --show-current) + + if [ -z "$branch" ]; then + echo "(detached)" + exit 0 + fi + + read -ra counts <<< "$(git rev-list --left-right --count "$branch"...origin/"$branch")" + to_push=${counts[0]} + to_pull=${counts[1]} + + extras=$([[ ${counts[0]} -eq 0 && ${counts[1]} -eq 0 ]] || echo " $to_push↑ $to_pull↓") + + if [[ $(git diff --shortstat 2> /dev/null | tail -n1) != "" ]]; then + echo "($branch*$extras)" + else + echo "($branch~$extras)" + fi +fi diff --git a/.local/bin/bashmatrix b/.local/bin/bashmatrix new file mode 100755 index 0000000..02e60dc --- /dev/null +++ b/.local/bin/bashmatrix @@ -0,0 +1,70 @@ +#!/usr/bin/env bash + +# Courtesy of @bvierra and company (long ago, pre-cPanel) + +### Customization: +blue="\033[0;34m" +brightblue="\033[1;34m" +cyan="\033[0;36m" +brightcyan="\033[1;36m" +green="\033[0;32m" +brightgreen="\033[1;32m" +red="\033[0;31m" +brightred="\033[1;31m" +white="\033[1;37m" +black="\033[0;30m" +grey="\033[0;37m" +darkgrey="\033[1;30m" +# Choose the colors that will be used from the above list +# space-separated list +# e.g. `colors=($green $brightgreen $darkgrey $white)` +colors=($green $brightgreen) +### End customization + +### Do not edit below this line +spacing=${1:-100} # the likelihood of a character being left in place +scroll=${2:-0} # 0 for static, positive integer determines scroll speed +screenlines=$(expr `tput lines` - 1 + $scroll) +screencols=$(expr `tput cols` / 2 - 1) + +# chars=(a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9 ^) +# charset via Carl: +chars=(ア イ ウ エ オ カ キ ク ケ コ サ シ ス セ ソ タ チ ツ テ ト ナ ニ ヌ ネ ノ ハ ヒ フ ヘ ホ マ ミ ム メ モ ヤ ユ ヨ ラ リ ル レ ロ ワ ン) + +count=${#chars[@]} +colorcount=${#colors[@]} + +trap "tput sgr0; clear; exit" SIGTERM SIGINT + +if [[ $1 =~ '-h' ]]; then + echo "Display a Matrix(ish) screen in the terminal" + echo "Usage: matrix [SPACING [SCROLL]]" + echo "Example: matrix 100 0" + exit 0 +fi + + +clear +tput cup 0 0 +while : + do for i in $(eval echo {1..$screenlines}) + do for i in $(eval echo {1..$screencols}) + do rand=$(($RANDOM%$spacing)) + case $rand in + 0) + printf "${colors[$RANDOM%$colorcount]}${chars[$RANDOM%$count]} " + ;; + 1) + printf " " + ;; + *) + printf "\033[2C" + ;; + esac + done + printf "\n" + + # sleep .005 + done + tput cup 0 0 + done diff --git a/.local/bin/bw-unlock b/.local/bin/bw-unlock new file mode 100755 index 0000000..6454ade --- /dev/null +++ b/.local/bin/bw-unlock @@ -0,0 +1,29 @@ +#!/bin/bash + +## +# Depends: zenity, bw (bitwarden-cli), notify-send +# +# Generates bw session key and saves it in tmp file where only current user has permissions +# +# By Solomon Laing (solomonlaing@pm.me) +# Date 2020-03-26 +## + +password=$(zenity --password) + +if [ -z $password ] +then + exit 0; +fi + +session=$(bw unlock "$password" | awk 'NF{last=$NF} END{print last}') + +loc="/tmp/bw-session" + +touch $loc -f + +chmod 600 $loc + +echo "$session" > /tmp/bw-session + +notify-send "Your bitwarden vault has been unlocked until next reboot." \ No newline at end of file diff --git a/.local/bin/checkup b/.local/bin/checkup new file mode 100755 index 0000000..bd3c634 --- /dev/null +++ b/.local/bin/checkup @@ -0,0 +1,17 @@ +#!/bin/sh + +# Syncs repositories and downloads updates, meant to be run as a cronjob. + +notify-send "📦 Repository Sync" "Checking for package updates..." + +sudo pacman -Syyuw --noconfirm || notify-send "Error downloading updates. + +Check your internet connection, if pacman is already running, or run update manually to see errors." +pkill -RTMIN+8 "${STATUSBAR:-dwmblocks}" + +if pacman -Qu | grep -v "\[ignored\]" +then + notify-send "🎁 Repository Sync" "Updates available. Click statusbar icon (📦) for update." +else + notify-send "📦 Repository Sync" "Sync complete. No new packages for update." +fi diff --git a/.local/bin/cht.sh b/.local/bin/cht.sh new file mode 100755 index 0000000..d7b4532 --- /dev/null +++ b/.local/bin/cht.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +commands="rsync find man tldr sed awk tr cp ls grep xargs rg ps mv kill lsof less head tail tar cp rm rename jq cat ssh cargo git git-worktree git-status git-commit git-rebase docker docker-compose stow chmod chown make" + +languages="golang solidity vlang v nodejs javascript tmux typescript zsh cpp c lua rust python bash php haskell ArnoldC css html gdb" + +selected=$(echo "$commands $languages" | tr ' ' '\n' | fzf) +if [[ -z $selected ]]; then + exit 0 +fi + +read -p "Enter Query: " query + +if grep -qs "$selected" "$languages"; then + query=$(echo "$query" | tr ' ' '+') + tmux neww bash -c "curl cht.sh/$selected/$query & while [ : ]; do sleep 1; done" +else + tmux neww bash -c "curl -s cht.sh/$selected~$query | less" +fi diff --git a/.local/bin/compiler b/.local/bin/compiler new file mode 100755 index 0000000..8420e25 --- /dev/null +++ b/.local/bin/compiler @@ -0,0 +1,59 @@ +#!/bin/sh + +# This script will compile or run another finishing operation on a document. I +# have this script run via vim. +# +# Compiles .tex. groff (.mom, .ms), .rmd, .md, .org. Opens .sent files as sent +# presentations. Runs scripts based on extension or shebang. +# +# Note that .tex files which you wish to compile with XeLaTeX should have the +# string "xelatex" somewhere in a comment/command in the first 5 lines. + +file=$(readlink -f "$1") +dir=${file%/*} +base="${file%.*}" +ext="${file##*.}" + +cd "$dir" || exit 1 + +textype() { \ + textarget="$(getcomproot "$file" || echo "$file")" + echo "$textarget" + command="pdflatex" + ( head -n5 "$textarget" | grep -qi 'xelatex' ) && command="xelatex" + $command --output-directory="${textarget%/*}" "${textarget%.*}" + grep -qi addbibresource "$textarget" && + biber --input-directory "${textarget%/*}" "${textarget%.*}" && + $command --output-directory="${textarget%/*}" "${textarget%.*}" && + $command --output-directory="${textarget%/*}" "${textarget%.*}" +} + +case "$ext" in + # Try to keep these cases in alphabetical order. + [0-9]) preconv "$file" | refer -PS -e | groff -mandoc -T pdf > "$base".pdf ;; + c) cc "$file" -o "$base" && "$base" ;; + cpp) g++ "$file" -o "$base" && "$base" ;; + cs) mcs "$file" && mono "$base".exe ;; + go) go run "$file" ;; + h) sudo make install ;; + java) javac -d classes "$file" && java -cp classes "${1%.*}" ;; + m) octave "$file" ;; + md) if [ -x "$(command -v lowdown)" ]; then + lowdown --parse-no-intraemph "$file" -Tms | groff -mpdfmark -ms -kept -T pdf > "$base".pdf + elif [ -x "$(command -v groffdown)" ]; then + groffdown -i "$file" | groff -T pdf > "$base".pdf + else + pandoc -t ms --highlight-style=kate -s -o "$base".pdf "$file" + fi ; ;; + mom) preconv "$file" | refer -PS -e | groff -mom -kept -T pdf > "$base".pdf ;; + ms) preconv "$file" | refer -PS -e | groff -me -ms -kept -T pdf > "$base".pdf ;; + org) emacs "$file" --batch -u "$USER" -f org-latex-export-to-pdf ;; + py) python "$file" ;; + [rR]md) Rscript -e "rmarkdown::render('$file', quiet=TRUE)" ;; + rs) cargo build ;; + sass) sassc -a "$file" "$base".css ;; + scad) openscad -o "$base".stl "$file" ;; + sent) setsid -f sent "$file" 2>/dev/null ;; + tex) textype "$file" ;; + *) sed -n '/^#!/s/^#!//p; q' "$file" | xargs -r -I % "$file" ;; +esac diff --git a/.local/bin/cron/README.md b/.local/bin/cron/README.md new file mode 100644 index 0000000..fa0c354 --- /dev/null +++ b/.local/bin/cron/README.md @@ -0,0 +1,11 @@ +# Important Note + +These cronjobs have components that require information about your current display to display notifications correctly. + +When you add them as cronjobs, I recommend you precede the command with commands as those below: + +``` +export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u $USER)/bus; export DISPLAY=:0; . $HOME/.zprofile; then_command_goes_here +``` + +This ensures that notifications will display, xdotool commands will function and environmental variables will work as well. diff --git a/.local/bin/cron/checkup b/.local/bin/cron/checkup new file mode 100755 index 0000000..bd3c634 --- /dev/null +++ b/.local/bin/cron/checkup @@ -0,0 +1,17 @@ +#!/bin/sh + +# Syncs repositories and downloads updates, meant to be run as a cronjob. + +notify-send "📦 Repository Sync" "Checking for package updates..." + +sudo pacman -Syyuw --noconfirm || notify-send "Error downloading updates. + +Check your internet connection, if pacman is already running, or run update manually to see errors." +pkill -RTMIN+8 "${STATUSBAR:-dwmblocks}" + +if pacman -Qu | grep -v "\[ignored\]" +then + notify-send "🎁 Repository Sync" "Updates available. Click statusbar icon (📦) for update." +else + notify-send "📦 Repository Sync" "Sync complete. No new packages for update." +fi diff --git a/.local/bin/cron/crontog b/.local/bin/cron/crontog new file mode 100755 index 0000000..c9a640f --- /dev/null +++ b/.local/bin/cron/crontog @@ -0,0 +1,6 @@ +#!/bin/sh + +# Toggles all cronjobs off/on. +# Stores disabled crontabs in ~/.config/cronsaved until restored. + +([ -f "${XDG_CONFIG_HOME:-$HOME/.config}"/cronsaved ] && crontab - < "${XDG_CONFIG_HOME:-$HOME/.config}"/cronsaved && rm "${XDG_CONFIG_HOME:-$HOME/.config}"/cronsaved && notify-send "🕓 Cronjobs re-enabled.") || ( crontab -l > "${XDG_CONFIG_HOME:-$HOME/.config}"/cronsaved && crontab -r && notify-send "🕓 Cronjobs saved and disabled.") diff --git a/.local/bin/crontog b/.local/bin/crontog new file mode 100755 index 0000000..c9a640f --- /dev/null +++ b/.local/bin/crontog @@ -0,0 +1,6 @@ +#!/bin/sh + +# Toggles all cronjobs off/on. +# Stores disabled crontabs in ~/.config/cronsaved until restored. + +([ -f "${XDG_CONFIG_HOME:-$HOME/.config}"/cronsaved ] && crontab - < "${XDG_CONFIG_HOME:-$HOME/.config}"/cronsaved && rm "${XDG_CONFIG_HOME:-$HOME/.config}"/cronsaved && notify-send "🕓 Cronjobs re-enabled.") || ( crontab -l > "${XDG_CONFIG_HOME:-$HOME/.config}"/cronsaved && crontab -r && notify-send "🕓 Cronjobs saved and disabled.") diff --git a/.local/bin/dec-sink-volume b/.local/bin/dec-sink-volume new file mode 100755 index 0000000..27a60d9 --- /dev/null +++ b/.local/bin/dec-sink-volume @@ -0,0 +1,11 @@ +#!/bin/bash + +default_loc=/tmp/pactl-default-sink + +if [ ! -f "$default_loc" ] ; then + set-default-sink +fi + +default=$(cat $default_loc) + +pactl set-sink-volume $default -5% diff --git a/.local/bin/dec-source-volume b/.local/bin/dec-source-volume new file mode 100755 index 0000000..e984688 --- /dev/null +++ b/.local/bin/dec-source-volume @@ -0,0 +1,11 @@ +#!/bin/bash + +default_loc=/tmp/pactl-default-source + +if [ ! -f "$default_loc" ] ; then + set-default-source +fi + +default=$(cat $default_loc) + +pactl set-source-volume $default -5% diff --git a/.local/bin/displayselect b/.local/bin/displayselect new file mode 100755 index 0000000..f9e8062 --- /dev/null +++ b/.local/bin/displayselect @@ -0,0 +1,83 @@ +#!/bin/sh + +# A UI for detecting and selecting all displays. Probes xrandr for connected +# displays and lets user select one to use. User may also select "manual +# selection" which opens arandr. + +twoscreen() { # If multi-monitor is selected and there are two screens. + + mirror=$(printf "no\\nyes" | dmenu -i -p "Mirror displays?") + # Mirror displays using native resolution of external display and a scaled + # version for the internal display + if [ "$mirror" = "yes" ]; then + external=$(echo "$screens" | dmenu -i -p "Optimize resolution for:") + internal=$(echo "$screens" | grep -v "$external") + + res_external=$(xrandr --query | sed -n "/^$external/,/\+/p" | \ + tail -n 1 | awk '{print $1}') + res_internal=$(xrandr --query | sed -n "/^$internal/,/\+/p" | \ + tail -n 1 | awk '{print $1}') + + res_ext_x=$(echo "$res_external" | sed 's/x.*//') + res_ext_y=$(echo "$res_external" | sed 's/.*x//') + res_int_x=$(echo "$res_internal" | sed 's/x.*//') + res_int_y=$(echo "$res_internal" | sed 's/.*x//') + + scale_x=$(echo "$res_ext_x / $res_int_x" | bc -l) + scale_y=$(echo "$res_ext_y / $res_int_y" | bc -l) + + xrandr --output "$external" --auto --scale 1.0x1.0 \ + --output "$internal" --auto --same-as "$external" \ + --scale "$scale_x"x"$scale_y" + else + + primary=$(echo "$screens" | dmenu -i -p "Select primary display:") + secondary=$(echo "$screens" | grep -v "$primary") + direction=$(printf "left\\nright" | dmenu -i -p "What side of $primary should $secondary be on?") + xrandr --output "$primary" --auto --scale 1.0x1.0 --output "$secondary" --"$direction"-of "$primary" --auto --scale 1.0x1.0 + fi + } + +morescreen() { # If multi-monitor is selected and there are more than two screens. + primary=$(echo "$screens" | dmenu -i -p "Select primary display:") + secondary=$(echo "$screens" | grep -v "$primary" | dmenu -i -p "Select secondary display:") + direction=$(printf "left\\nright" | dmenu -i -p "What side of $primary should $secondary be on?") + tertiary=$(echo "$screens" | grep -v "$primary" | grep -v "$secondary" | dmenu -i -p "Select third display:") + xrandr --output "$primary" --auto --output "$secondary" --"$direction"-of "$primary" --auto --output "$tertiary" --"$(printf "left\\nright" | grep -v "$direction")"-of "$primary" --auto + } + +multimon() { # Multi-monitor handler. + case "$(echo "$screens" | wc -l)" in + 2) twoscreen ;; + *) morescreen ;; + esac ;} + +onescreen() { # If only one output available or chosen. + xrandr --output "$1" --auto --scale 1.0x1.0 $(echo "$allposs" | grep -v "\b$1" | awk '{print "--output", $1, "--off"}' | paste -sd ' ' -) + } + +postrun() { # Stuff to run to clean up. + setbg # Fix background if screen size/arangement has changed. + remaps # Re-remap keys if keyboard added (for laptop bases) + { killall dunst ; setsid -f dunst ;} >/dev/null 2>&1 # Restart dunst to ensure proper location on screen + } + +# Get all possible displays +allposs=$(xrandr -q | grep "connected") + +# Get all connected screens. +screens=$(echo "$allposs" | awk '/ connected/ {print $1}') + +# If there's only one screen +[ "$(echo "$screens" | wc -l)" -lt 2 ] && + { onescreen "$screens"; postrun; notify-send "💻 Only one screen detected." "Using it in its optimal settings..."; exit ;} + +# Get user choice including multi-monitor and manual selection: +chosen=$(printf "%s\\nmulti-monitor\\nmanual selection" "$screens" | dmenu -i -p "Select display arangement:") && +case "$chosen" in + "manual selection") arandr ; exit ;; + "multi-monitor") multimon ;; + *) onescreen "$chosen" ;; +esac + +postrun diff --git a/.local/bin/dmenu-bluetooth b/.local/bin/dmenu-bluetooth new file mode 100755 index 0000000..47f5d32 --- /dev/null +++ b/.local/bin/dmenu-bluetooth @@ -0,0 +1,317 @@ +#!/usr/bin/env bash +# _ _ _ _ _ _ +# __| |_ __ ___ ___ _ __ _ _ | |__ | |_ _ ___| |_ ___ ___ | |_ | |__ +# / _` | '_ ` _ \ / _ \ '_ \| | | |_____| '_ \| | | | |/ _ \ __/ _ \ / _ \| __|| '_ \ +# | (_| | | | | | | __/ | | | |_| |_____| |_) | | |_| | __/ || (_) | (_) | |_ | | | | +# \__,_|_| |_| |_|\___|_| |_|\__,_| |_.__/|_|\__,_|\___|\__\___/ \___/ \__||_| |_| +# +# Author: Nick Clyde (clydedroid) +# dmenu support by: Layerex +# +# A script that generates a dmenu menu that uses bluetoothctl to +# connect to bluetooth devices and display status info. +# +# Inspired by networkmanager-dmenu (https://github.com/firecat53/networkmanager-dmenu) +# Thanks to x70b1 (https://github.com/polybar/polybar-scripts/tree/master/polybar-scripts/system-bluetooth-bluetoothctl) +# +# Depends on: +# Arch repositories: dmenu, bluez-utils (contains bluetoothctl) + +# Constants +divider="---------" +goback="Back" + +# Checks if bluetooth controller is powered on +power_on() { + if bluetoothctl show | grep -F -q "Powered: yes"; then + return 0 + else + return 1 + fi +} + +# Toggles power state +toggle_power() { + if power_on; then + bluetoothctl power off + show_menu + else + if rfkill list bluetooth | grep -F -q 'blocked: yes'; then + rfkill unblock bluetooth && sleep 3 + fi + bluetoothctl power on + show_menu + fi +} + +# Checks if controller is scanning for new devices +scan_on() { + if bluetoothctl show | grep -F -q "Discovering: yes"; then + echo "Scan: on" + return 0 + else + echo "Scan: off" + return 1 + fi +} + +# Toggles scanning state +toggle_scan() { + if scan_on; then + kill "$(pgrep -F -f "bluetoothctl scan on")" + bluetoothctl scan off + show_menu + else + bluetoothctl scan on & + echo "Scanning..." + sleep 5 + show_menu + fi +} + +# Checks if controller is able to pair to devices +pairable_on() { + if bluetoothctl show | grep -F -q "Pairable: yes"; then + echo "Pairable: on" + return 0 + else + echo "Pairable: off" + return 1 + fi +} + +# Toggles pairable state +toggle_pairable() { + if pairable_on; then + bluetoothctl pairable off + show_menu + else + bluetoothctl pairable on + show_menu + fi +} + +# Checks if controller is discoverable by other devices +discoverable_on() { + if bluetoothctl show | grep -F -q "Discoverable: yes"; then + echo "Discoverable: on" + return 0 + else + echo "Discoverable: off" + return 1 + fi +} + +# Toggles discoverable state +toggle_discoverable() { + if discoverable_on; then + bluetoothctl discoverable off + show_menu + else + bluetoothctl discoverable on + show_menu + fi +} + +# Checks if a device is connected +device_connected() { + device_info=$(bluetoothctl info "$1") + if echo "$device_info" | grep -F -q "Connected: yes"; then + return 0 + else + return 1 + fi +} + +# Toggles device connection +toggle_connection() { + if device_connected "$1"; then + bluetoothctl disconnect "$1" + # device_menu "$device" + else + bluetoothctl connect "$1" + # device_menu "$device" + fi +} + +# Checks if a device is paired +device_paired() { + device_info=$(bluetoothctl info "$1") + if echo "$device_info" | grep -F -q "Paired: yes"; then + echo "Paired: yes" + return 0 + else + echo "Paired: no" + return 1 + fi +} + +# Toggles device paired state +toggle_paired() { + if device_paired "$1"; then + bluetoothctl remove "$1" + device_menu "$device" + else + bluetoothctl pair "$1" + device_menu "$device" + fi +} + +# Checks if a device is trusted +device_trusted() { + device_info=$(bluetoothctl info "$1") + if echo "$device_info" | grep -F -q "Trusted: yes"; then + echo "Trusted: yes" + return 0 + else + echo "Trusted: no" + return 1 + fi +} + +# Toggles device connection +toggle_trust() { + if device_trusted "$1"; then + bluetoothctl untrust "$1" + device_menu "$device" + else + bluetoothctl trust "$1" + device_menu "$device" + fi +} + +# Prints a short string with the current bluetooth status +# Useful for status bars like polybar, etc. +print_status() { + if power_on; then + printf '' + + mapfile -t paired_devices < <(bluetoothctl paired-devices | grep -F Device | cut -d ' ' -f 2) + counter=0 + + for device in "${paired_devices[@]}"; do + if device_connected "$device"; then + device_alias="$(bluetoothctl info "$device" | grep -F "Alias" | cut -d ' ' -f 2-)" + + if [ $counter -gt 0 ]; then + printf ", %s" "$device_alias" + else + printf " %s" "$device_alias" + fi + + ((counter++)) + fi + done + printf "\n" + else + echo "" + fi +} + +# A submenu for a specific device that allows connecting, pairing, and trusting +device_menu() { + device=$1 + + # Get device name and mac address + device_name="$(echo "$device" | cut -d ' ' -f 3-)" + mac="$(echo "$device" | cut -d ' ' -f 2)" + + # Build options + if device_connected "$mac"; then + connected="Connected: yes" + else + connected="Connected: no" + fi + paired=$(device_paired "$mac") + trusted=$(device_trusted "$mac") + options="$connected\n$paired\n$trusted\n$divider\n$goback\nExit" + + # Open dmenu menu, read chosen option + chosen="$(echo -e "$options" | run_dmenu "$device_name")" + + # Match chosen option to command + case $chosen in + "" | "$divider") + echo "No option chosen." + ;; + "$connected") + toggle_connection "$mac" + ;; + "$paired") + toggle_paired "$mac" + ;; + "$trusted") + toggle_trust "$mac" + ;; + "$goback") + show_menu + ;; + esac +} + +# Opens a dmenu menu with current bluetooth status and options to connect +show_menu() { + # Get menu options + if power_on; then + power="Power: on" + + # Human-readable names of devices, one per line + # If scan is off, will only list paired devices + devices=$(bluetoothctl devices | grep -F Device | cut -d ' ' -f 3-) + + # Get controller flags + scan=$(scan_on) + pairable=$(pairable_on) + discoverable=$(discoverable_on) + + # Options passed to dmenu + options="$devices\n$divider\n$power\n$scan\n$pairable\n$discoverable\nExit" + else + power="Power: off" + options="$power\nExit" + fi + + # Open dmenu menu, read chosen option + chosen="$(echo -e "$options" | run_dmenu "Bluetooth")" + + # Match chosen option to command + case $chosen in + "" | "$divider") + echo "No option chosen." + ;; + "$power") + toggle_power + ;; + "$scan") + toggle_scan + ;; + "$discoverable") + toggle_discoverable + ;; + "$pairable") + toggle_pairable + ;; + *) + device=$(bluetoothctl devices | grep -F "$chosen") + # Open a submenu if a device is selected + if [[ $device ]]; then device_menu "$device"; fi + ;; + esac +} + +original_args=("$@") + +# dmenu command to pipe into. Extra arguments to dmenu-bluetooth are passed through to dmenu. This +# allows the user to set fonts, sizes, colours, etc. +run_dmenu() { + dmenu "${original_args[@]}" -i -p "$1" +} + +case "$1" in + --status) + print_status + ;; + *) + show_menu + ;; +esac diff --git a/.local/bin/dmenumount b/.local/bin/dmenumount new file mode 100755 index 0000000..8cf4a6b --- /dev/null +++ b/.local/bin/dmenumount @@ -0,0 +1,67 @@ +#!/bin/sh + +# Gives a dmenu prompt to mount unmounted drives and Android phones. If +# they're in /etc/fstab, they'll be mounted automatically. Otherwise, you'll +# be prompted to give a mountpoint from already existsing directories. If you +# input a novel directory, it will prompt you to create that directory. + +getmount() { \ + [ -z "$chosen" ] && exit 1 + # shellcheck disable=SC2086 + mp="$(find $1 2>/dev/null | dmenu -i -p "Type in mount point.")" || exit 1 + test -z "$mp" && exit 1 + if [ ! -d "$mp" ]; then + mkdiryn=$(printf "No\\nYes" | dmenu -i -p "$mp does not exist. Create it?") || exit 1 + [ "$mkdiryn" = "Yes" ] && (mkdir -p "$mp" || sudo -A mkdir -p "$mp") + fi + } + +mountusb() { \ + chosen="$(echo "$usbdrives" | dmenu -i -p "Mount which drive?")" || exit 1 + chosen="$(echo "$chosen" | awk '{print $1}')" + sudo -A mount "$chosen" 2>/dev/null && notify-send "💻 USB mounting" "$chosen mounted." && exit 0 + alreadymounted=$(lsblk -nrpo "name,type,mountpoint" | awk '$3!~/\/boot|\/home$|SWAP/&&length($3)>1{printf "-not ( -path *%s -prune ) ",$3}') + getmount "/mnt /media /mount /home -maxdepth 5 -type d $alreadymounted" + partitiontype="$(lsblk -no "fstype" "$chosen")" + case "$partitiontype" in + "vfat") sudo -A mount -t vfat "$chosen" "$mp" -o rw,umask=0000;; + "exfat") sudo -A mount "$chosen" "$mp" -o uid="$(id -u)",gid="$(id -g)";; + *) sudo -A mount "$chosen" "$mp"; user="$(whoami)"; ug="$(groups | awk '{print $1}')"; sudo -A chown "$user":"$ug" "$mp";; + esac && notify-send "💻 USB mounting" "$chosen mounted to $mp." || + notify-send "💻 Drive failed to mount." "Probably a permissions issue or drive is already mounted." + } + +mountandroid() { \ + chosen="$(echo "$anddrives" | dmenu -i -p "Which Android device?")" || exit 1 + chosen="$(echo "$chosen" | cut -d : -f 1)" + getmount "$HOME -maxdepth 3 -type d" + echo "OK" | dmenu -i -p "Tap Allow on your phone if it asks for permission and then press enter" || exit 1 + simple-mtpfs --device "$chosen" "$mp" && + notify-send "🤖 Android Mounting" "Android device mounted to $mp." || + notify-send "🤖 Android failed mounting." "Probably a permissions issue or phone is already mounted." + } + +asktype() { \ + choice="$(printf "USB\\nAndroid" | dmenu -i -p "Mount a USB drive or Android device?")" || exit 1 + case $choice in + USB) mountusb ;; + Android) mountandroid ;; + esac + } + +anddrives=$(simple-mtpfs -l 2>/dev/null) +usbdrives="$(lsblk -rpo "name,type,size,label,mountpoint,fstype" | grep -v crypto_LUKS | grep 'part\|rom' | sed 's/ /:/g' | awk -F':' '$5==""{printf "%s (%s) %s\n",$1,$3,$4}')" + +if [ -z "$usbdrives" ]; then + [ -z "$anddrives" ] && echo "No USB drive or Android device detected" && exit + echo "Android device(s) detected." + mountandroid +else + if [ -z "$anddrives" ]; then + echo "USB drive(s) detected." + mountusb + else + echo "Mountable USB drive(s) and Android device(s) detected." + asktype + fi +fi diff --git a/.local/bin/dmenumountcifs b/.local/bin/dmenumountcifs new file mode 100755 index 0000000..46c2b57 --- /dev/null +++ b/.local/bin/dmenumountcifs @@ -0,0 +1,19 @@ +#!/bin/sh +# Gives a dmenu prompt to mount unmounted local NAS shares for read/write. +# Requirements - "%wheel ALL=(ALL) NOPASSWD: ALL" +# +# Browse for mDNS/DNS-SD services using the Avahi daemon... +srvname=$(avahi-browse _smb._tcp -t | awk '{print $4}' | dmenu -i -p "Which NAS?") || exit 1 +notify-send "Searching for network shares..." "Please wait..." +# Choose share disk... +share=$(smbclient -L "$srvname" -N | grep Disk | awk '{print $1}' | dmenu -i -p "Mount which share?") || exit 1 +# Format URL... +share2mnt=//"$srvname".local/"$share" + +sharemount() { + mounted=$(mount -v | grep "$share2mnt") || ([ ! -d /mnt/"$share" ] && sudo mkdir /mnt/"$share") + [ -z "$mounted" ] && sudo mount -t cifs "$share2mnt" -o user=nobody,password="",noperm /mnt/"$share" && notify-send "Netshare $share mounted" && exit 0 + notify-send "Netshare $share already mounted"; exit 1 +} + +sharemount diff --git a/.local/bin/dmenupass b/.local/bin/dmenupass new file mode 100755 index 0000000..2c14e6f --- /dev/null +++ b/.local/bin/dmenupass @@ -0,0 +1,6 @@ +#!/bin/sh + +# This script is the SUDO_ASKPASS variable, meaning that it will be used as a +# password prompt if needed. + +dmenu -fn Monospace-18 -P -p "$1" <&- && echo diff --git a/.local/bin/dmenuprompt b/.local/bin/dmenuprompt new file mode 100755 index 0000000..afb24a4 --- /dev/null +++ b/.local/bin/dmenuprompt @@ -0,0 +1,21 @@ +#!/bin/bash + +# A dmenu binary prompt script. +# Gives a dmenu prompt labelled with $1 to perform command $2. +# To invert the options, simply provide a thrid parameter $3. +# For example: +# `./prompt "Do you want to shutdown?" "shutdown now"` + +# Taken shamelessly from Luke Smith + +if [ -n "$3" ] +then + choice=$(echo -e "Yes\nNo" | dmenu -bw 0 -i -p "$1") +else + choice=$(echo -e "No\nYes" | dmenu -bw 0 -i -p "$1") +fi + +if [ $choice = "Yes" ] +then + $2 +fi diff --git a/.local/bin/dmenusearch b/.local/bin/dmenusearch new file mode 100755 index 0000000..015e37c --- /dev/null +++ b/.local/bin/dmenusearch @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# +# Script name: dmenusearch +# Description: Search various search engines (inspired by surfraw). +# Dependencies: dmenu and a web browser +# Usage: dmenusearch +# where engine is amazon, duckduckgo, etc. +# without engine all options will be listed + +# Pilfered from Derek Taylor @ https://www.gitlab.com/dwt1/dmscripts + +# Contributors: Derek Taylor +# Ali Furkan Yıldız + +# Defining our web browser. +DMBROWSER="${BROWSER:-surf}" + +# An array of search engines. You can edit this list to add/remove +# search engines. The format must be: "engine_name]="url". +# The url format must allow for the search keywords at the end of the url. +# For example: https://www.amazon.com/s?k=XXXX searches Amazon for 'XXXX'. +declare -A options +options[amazon]="https://www.amazon.com/s?k=" +options[archaur]="https://aur.archlinux.org/packages/?O=0&K=" +options[archpkg]="https://archlinux.org/packages/?sort=&q=" +options[archwiki]="https://wiki.archlinux.org/index.php?search=" +options[arxiv]="https://arxiv.org/search/?searchtype=all&source=header&query=" +options[bbcnews]="https://www.bbc.co.uk/search?q=" +options[bing]="https://www.bing.com/search?q=" +options[cliki]="https://www.cliki.net/site/search?query=" +options[cnn]="https://www.cnn.com/search?q=" +options[coinbase]="https://www.coinbase.com/price?query=" +options[debianpkg]="https://packages.debian.org/search?suite=default§ion=all&arch=any&searchon=names&keywords=" +options[discogs]="https://www.discogs.com/search/?&type=all&q=" +options[duckduckgo]="https://duckduckgo.com/?q=" +options[ebay]="https://www.ebay.com/sch/i.html?&_nkw=" +options[github]="https://github.com/search?q=" +options[gitlab]="https://gitlab.com/search?search=" +options[imdb]="https://www.imdb.com/find?q=" +options[lbry]="https://lbry.tv/$/search?q=" +options[odysee]="https://odysee.com/$/search?q=" +options[reddit]="https://www.reddit.com/search/?q=" +options[slashdot]="https://slashdot.org/index2.pl?fhfilter=" +options[socialblade]="https://socialblade.com/youtube/user/" +options[sourceforge]="https://sourceforge.net/directory/?q=" +options[stack]="https://stackoverflow.com/search?q=" +options[startpage]="https://www.startpage.com/do/dsearch?query=" +options[stockquote]="https://finance.yahoo.com/quote/" +options[thesaurus]="https://www.thesaurus.com/misspelling?term=" +options[translate]="https://translate.google.com/?sl=auto&tl=en&text=" +options[urban]="https://www.urbandictionary.com/define.php?term=" +options[wayback]="https://web.archive.org/web/*/" +options[webster]="https://www.merriam-webster.com/dictionary/" +options[wikipedia]="https://en.wikipedia.org/wiki/" +options[wiktionary]="https://en.wiktionary.org/wiki/" +options[wolfram]="https://www.wolframalpha.com/input/?i=" +options[youtube]="https://www.youtube.com/results?search_query=" +options[google]="https://www.google.com/search?q=" +options[googleimages]="https://www.google.com/search?hl=en&tbm=isch&q=" +options[googlenews]="https://news.google.com/search?q=" +options[googleSupport]="https://support.google.com/search?q=" +options[googleSupportAdmin]="https://support.google.com/a/search?q=" +options[googleStructuredData]="https://search.google.com/structured-data/testing-tool#url=" +options[googleRichResults]="https://search.google.com/test/rich-results??url=" +options[googlePagespeed]="https://developers.google.com/speed/pagespeed/insights/?url=" +options[googleDevelopers]="https://developers.google.com/s/results?q=" +options[googleOpenSource]="https://opensource.google/projects/search?q=" +options[googleExperimentswithGoogle]="https://experiments.withgoogle.com/search?q=" +options[googleDataset]="https://datasetsearch.research.google.com/search?query=" + +if [ -z "$1" ]; then + # Picking a search engine. + # shellcheck disable=SC2154 + while [ -z "$engine" ]; do + engine=$(printf '%s\n' "${!options[@]}" | sort | dmenu -i -p 'Choose search engine:') "$@" || exit + url="${options["${engine}"]}" || exit + done +else + engine="$1" +fi + +url="${options["${engine}"]}" || exit + +# Searching the chosen engine. +# shellcheck disable=SC2154 +while [ -z "$query" ]; do + query=$(echo "$engine" | dmenu -p 'Enter search query:') || exit +done + +# Display search results in web browser +$DMBROWSER "$url""$query" diff --git a/.local/bin/dmenuumount b/.local/bin/dmenuumount new file mode 100755 index 0000000..656d1f1 --- /dev/null +++ b/.local/bin/dmenuumount @@ -0,0 +1,21 @@ +#!/bin/sh + +# A dmenu prompt to unmount drives. +# Provides you with mounted partitions, select one to unmount. +# Drives mounted at /, /boot and /home will not be options to unmount. + +drives="$(lsblk -nrpo "name,type,size,mountpoint,label" | awk -F':' '{gsub(/ /,":")}$4!~/\/boot|\/efi|\/home$|SWAP/&&length($4)>1{printf "%s (%s) %s\n",$4,$3,$5}'; awk '/simple-mtpfs/ { print "📱", $2; }' /etc/mtab)" + +chosen="$(echo "$drives" | dmenu -i -p "Unmount which drive?")" || exit 1 + +case "$chosen" in + 📱*) + chosen="${chosen#📱 }" + sudo -A umount -l "$chosen" + ;; + *) + chosen="${chosen% (*}" + sudo -A umount -l "$chosen" + ;; +esac && notify-send "🖥️ Drive unmounted." "$chosen successfully unmounted." || + notify-send "🖥️ Drive failed to unmount." "Possibly a permissions or I/O issue." diff --git a/.local/bin/duck b/.local/bin/duck new file mode 100755 index 0000000..8569c8f --- /dev/null +++ b/.local/bin/duck @@ -0,0 +1,4 @@ +#!/bin/sh + +url="https://lite.duckduckgo.com/lite?kd=-1&kp=-1&q=$(urlencode "$*")" # 🦆 +exec lynx "$url" diff --git a/.local/bin/ext b/.local/bin/ext new file mode 100755 index 0000000..927fb5b --- /dev/null +++ b/.local/bin/ext @@ -0,0 +1,45 @@ +#!/bin/sh + +# A general, all-purpose extraction script. Not all extraction programs here +# are installed by LARBS automatically. +# +# Default behavior: Extract archive into new directory +# Behavior with `-c` option: Extract contents into current directory + +while getopts "hc" o; do case "${o}" in + c) extracthere="True" ;; + *) printf "Options:\\n -c: Extract archive into current directory rather than a new one.\\n" && exit 1 ;; +esac done + +if [ -z "$extracthere" ]; then + archive="$(readlink -f "$*")" && + directory="$(echo "$archive" | sed 's/\.[^\/.]*$//')" && + mkdir -p "$directory" && + cd "$directory" || exit 1 +else + archive="$(readlink -f "$(echo "$*" | cut -d' ' -f2)" 2>/dev/null)" +fi + +[ -z "$archive" ] && printf "Give archive to extract as argument.\\n" && exit 1 + +if [ -f "$archive" ] ; then + case "$archive" in + *.tar.bz2|*.tbz2) tar xvjf "$archive" ;; + *.tar.xz) tar -xf "$archive" ;; + *.tar.gz|*.tgz) tar xvzf "$archive" ;; + *.tar.zst) tar -I zstd -xf "$archive" ;; + *.lzma) unlzma "$archive" ;; + *.bz2) bunzip2 "$archive" ;; + *.rar) unrar x -ad "$archive" ;; + *.gz) gunzip "$archive" ;; + *.tar) tar xvf "$archive" ;; + *.zip) unzip "$archive" ;; + *.Z) uncompress "$archive" ;; + *.7z) 7z x "$archive" ;; + *.xz) unxz "$archive" ;; + *.exe) cabextract "$archive" ;; + *) printf "extract: '%s' - unknown archive method\\n" "$archive" ;; + esac +else + printf "File \"%s\" not found.\\n" "$archive" +fi diff --git a/.local/bin/fishies b/.local/bin/fishies new file mode 100755 index 0000000..64d38c7 --- /dev/null +++ b/.local/bin/fishies @@ -0,0 +1,1449 @@ +#!/usr/bin/env perl + +#`which keyoff` and `keyoff`; + +sub handle { + exit(0); +} + +$SIG{"TERM"} = \&handle; +$SIG{"INT"} = \&handle; + +# +# BUT FIRST: +# Keep in mind that you probably have to install libcurses-perl +# and Term::Animation from CPAN for this to work: +# +# sudo apt install libcurses-perl make +# sudo cpan -I Term::Animation +# +# Special thanks to @UndeadLeech for adding the transparency. This is +# the *only* fish/asciiquarium without the default black background. +# +############################################################################# +# Asciiquarium - An aquarium animation in ASCII art +# +# This program displays an aquarium/sea animation using ASCII art. +# It requires the module Term::Animation, which requires Curses. You +# can get both modules from http://search.cpan.org. Asciiquarium will +# only run on platforms with a curses library, so Windows is not supported. +# +# The current version of this program is available at: +# +# http://robobunny.com/projects/asciiquarium +# +############################################################################# +# Author: +# Kirk Baucom +# +# Contributors: +# Joan Stark: http://www.geocities.com/SoHo/7373/ +# most of the ASCII art +# +# License: +# +# Copyright (C) 2013 Kirk Baucom (kbaucom@schizoid.com) +# +# 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 2 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +############################################################################# + +use Term::Animation 2.0; +use Term::Animation::Entity; +use Data::Dumper; +use Curses; +use strict; +use warnings; + +my $version = "1.1"; + +my @random_objects = init_random_objects(); + +# the Z depth at which certain items occur +my %depth = ( + # no gui yet + guiText => 0, + gui => 1, + + # under water + shark => 2, + fish_start => 3, + fish_end => 20, + seaweed => 21, + castle => 22, + + # waterline + water_line3 => 2, + water_gap3 => 3, + water_line2 => 4, + water_gap2 => 5, + water_line1 => 6, + water_gap1 => 7, + water_line0 => 8, + water_gap0 => 9, +); + +main(); + +####################### MAIN ####################### + +sub main { + + my $anim = Term::Animation->new(); + + # set the wait time for getch + halfdelay(1); + #nodelay(1); + + $anim->color(1); + use_default_colors(); + my $cid = 1; + for my $f ('WHITE', 'RED', 'GREEN', 'BLUE', 'CYAN', 'MAGENTA', 'YELLOW', 'BLACK') { + init_pair($cid, eval "Curses::COLOR_$f", -1); + $cid++; + } + + my $start_time = time; + my $paused = 0; + while(1) { + + add_environment($anim); + add_castle($anim); + add_all_seaweed($anim); + add_all_fish($anim); + random_object(undef, $anim); + + $anim->redraw_screen(); + + my $nexttime = 0; + + while(1) { + my $in = getch(); + + if ( $in eq 'q' ) { quit(); } # Exit + elsif( $in eq 'r' || $in eq KEY_RESIZE()) { last; } # Redraw (will recreate all objects) + elsif( $in eq 'p' ) { $paused = !$paused; } + + $anim->animate() unless($paused); + } + $anim->update_term_size(); + $anim->remove_all_entities(); + + } + +} + +sub add_environment { + my ($anim) = @_; + + my @water_line_segment = ( + q{~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}, + q{^^^^ ^^^ ^^^ ^^^ ^^^^ }, + q{^^^^ ^^^^ ^^^ ^^ }, + q{^^ ^^^^ ^^^ ^^^^^^ } + ); + + # tile the segments so they stretch across the screen + my $segment_size = length($water_line_segment[0]); + my $segment_repeat = int($anim->width()/$segment_size) + 1; + foreach my $i (0..$#water_line_segment) { + $water_line_segment[$i] = $water_line_segment[$i]x$segment_repeat; + } + + foreach my $i (0..$#water_line_segment) { + $anim->new_entity( + name => "water_seg_$i", + type => "waterline", + shape => $water_line_segment[$i], + position => [ 0, $i+5, $depth{'water_line' . $i} ], + default_color => 'cyan', + depth => 22, + physical => 1, + ); + } +} + +sub add_castle { + my ($anim) = @_; + my $castle_image = q{ + T~~ + | + /^\ + / \ + _ _ _ / \ _ _ _ +[ ]_[ ]_[ ]/ _ _ \[ ]_[ ]_[ ] +|_=__-_ =_|_[ ]_[ ]_|_=-___-__| + | _- = | =_ = _ |= _= | + |= -[] |- = _ = |_-=_[] | + | =_ |= - ___ | =_ = | + |= []- |- /| |\ |=_ =[] | + |- =_ | =| | | | |- = - | + |_______|__|_|_|_|__|_______| +}; + + my $castle_mask = q{ + RR + yyy + y y + y y + y y + yyy + yy yy + y y y y + yyyyyyy +}; + + $anim->new_entity( + name => "castle", + shape => $castle_image, + color => $castle_mask, + position => [ $anim->width()-32, $anim->height()-13, $depth{'castle'} ], + default_color => 'BLACK', + ); +} + +sub add_all_seaweed { + my ($anim) = @_; + # figure out how many seaweed to add by the width of the screen + my $seaweed_count = int($anim->width() / 15); + for (1..$seaweed_count) { + add_seaweed(undef, $anim); + } +} + +sub add_seaweed { + my ($old_seaweed, $anim) = @_; + my @seaweed_image = ('',''); + my $height = int(rand(4)) + 3; + for my $i (1..$height) { + my $left_side = $i%2; + my $right_side = !$left_side; + $seaweed_image[$left_side] .= "(\n"; + $seaweed_image[$right_side] .= " )\n"; + } + my $x = int(rand($anim->width()-2)) + 1; + my $y = $anim->height() - $height; + my $anim_speed = rand(.05) + .25; + $anim->new_entity( + name => 'seaweed' . rand(1), + shape => \@seaweed_image, + position => [ $x, $y, $depth{'seaweed'} ], + callback_args => [ 0, 0, 0, $anim_speed ], + die_time => time() + int(rand(4*60)) + (8*60), # seaweed lives for 8 to 12 minutes + death_cb => \&add_seaweed, + default_color => 'green', + ); +} + +# add an air bubble to a fish +sub add_bubble { + my ($fish, $anim) = @_; + + my $cb_args = $fish->callback_args(); + my @fish_size = $fish->size(); + my @fish_pos = $fish->position(); + my @bubble_pos = @fish_pos; + + # moving right + if($cb_args->[0] > 0) { + $bubble_pos[0] += $fish_size[0]; + } + $bubble_pos[1] += int($fish_size[1] / 2); + # bubble always goes on top of the fish + $bubble_pos[2]--; + + $anim->new_entity( + shape => [ '.', 'o', 'O', 'O', 'O' ], + type => 'bubble', + position => \@bubble_pos, + callback_args => [ 0, -1, 0, .1 ], + die_offscreen => 1, + physical => 1, + coll_handler => \&bubble_collision, + default_color => 'CYAN', + ); +} + +sub bubble_collision { + my ($bubble, $anim) = @_; + my $collisions = $bubble->collisions(); + foreach my $col_obj (@{$collisions}) { + if($col_obj->type eq 'waterline') { + $bubble->kill(); + last; + } + } + +} + +sub add_all_fish { + my ($anim) = @_; + # figure out how many fish to add by the size of the screen, + # minus the stuff above the water + my $screen_size = ($anim->height() - 9) * $anim->width(); + my $fish_count = int($screen_size / 350); + for (1..$fish_count) { + add_fish(undef, $anim); + } +} + + +sub add_fish { + my ($old_fish, $anim) = @_; + my @fish_image = ( + +q{ + \ + ...\.., +\ /' \ + >= ink( ' > +/ \ / / + `"'"'/'' +}, +q{ + 2 + 1112111 +6 11 1 + 66 7777 4 5 +6 1 3 1 + 11111311 +}, +q{ + / + ,../... + / '\ / +< ' )ink =< + \ \ / \ + `'\'"'"' +}, +q{ + 2 + 1112111 + 1 11 6 +5 4 7777 66 + 1 3 1 6 + 11311111 +}, +q{ + \ +\ /--\ +>= (o> +/ \__/ + / +}, +q{ + 2 +6 1111 +66 745 +6 1111 + 3 +}, +q{ + / + /--\ / +::::::::;;\\\\\ + ''\\\\\\\\\'' ';\ +}, +q{ + 222 + 1122211 666 + 4111111111666 +51111111111666 + 113333311 666 +}, +q{ + __ +><_'> + ' +}, +q{ + 11 +61145 + 3 +}, +q{ + __ +<'_>< + ` +}, +q{ + 11 +54116 + 3 +}, +q{ + ..\, +>=' ('> + '''/'' +}, +q{ + 1121 +661 745 + 111311 +}, +q{ + ,/.. +<') `=< + ``\``` +}, +q{ + 1211 +547 166 + 113111 +}, +q{ + \ + / \ +>=_('> + \_/ + / +}, +q{ + 2 + 1 1 +661745 + 111 + 3 +}, +q{ + / + / \ +<')_=< + \_/ + \ +}, +q{ + 2 + 1 1 +547166 + 111 + 3 +}, +q{ + ,\ +>=('> + '/ +}, +q{ + 12 +66745 + 13 +}, +q{ + /, +<')=< + \` +}, +q{ + 21 +54766 + 31 +}, +q{ + __ +\/ o\ +/\__/ +}, +q{ + 11 +61 41 +61111 +}, +q{ + __ +/o \/ +\__/\ +}, +q{ + 11 +14 16 +11116 +}, +); + + # 1: body + # 2: dorsal fin + # 3: flippers + # 4: eye + # 5: mouth + # 6: tailfin + # 7: gills + + my @colors = ('c','C','r','R','y','Y','b','B','g','G','m','M'); + my $fish_num = int(rand($#fish_image/2)); + my $fish_index = $fish_num * 2; + my $speed = rand(2) + .25; + my $depth = int(rand($depth{'fish_end'} - $depth{'fish_start'})) + $depth{'fish_start'}; + my $color_mask = $fish_image[$fish_index+1]; + $color_mask =~ s/4/W/gm; + $color_mask = rand_color($color_mask); + + if($fish_num % 2) { + $speed *= -1; + } + my $fish_object = Term::Animation::Entity->new( + type => 'fish', + shape => $fish_image[$fish_index], + auto_trans => 1, + color => $color_mask, + position => [ 0, 0, $depth ], + callback => \&fish_callback, + callback_args => [ $speed, 0, 0 ], + die_offscreen => 1, + death_cb => \&add_fish, + physical => 1, + coll_handler => \&fish_collision, + ); + + my $max_height = 9; + my $min_height = $anim->height() - $fish_object->{'HEIGHT'}; + $fish_object->{'Y'} = int(rand($min_height - $max_height)) + $max_height; + if($fish_num % 2) { + $fish_object->{'X'} = $anim->width()-2; + } else { + $fish_object->{'X'} = 1 - $fish_object->{'WIDTH'}; + } + $anim->add_entity($fish_object); +} + +sub fish_callback { + my ($fish, $anim) = @_; + if(int(rand(100)) > 97) { + add_bubble($fish, $anim); + } + return $fish->move_entity($anim); +} + +sub fish_collision { + my ($fish, $anim) = @_; + my $collisions = $fish->collisions(); + foreach my $col_obj (@{$collisions}) { + if($col_obj->type eq 'teeth') { + add_splat($anim, $col_obj->position()); + $fish->kill(); + last; + } elsif($col_obj->type eq 'hook_point') { + retract($col_obj); + retract($fish); + # get the hook and line + my $hook = $anim->get_entities_of_type('fishhook')->[0]; + my $line = $anim->get_entities_of_type('fishline')->[0]; + retract($anim->entity($hook)); + retract($anim->entity($line)); + last; + } + } +} + +sub add_splat { + my ($anim, $x, $y, $z) = @_; + my @splat_image = ( +q# + . + *** + ' +#, +q# + ",*;` + "*,** + *"'~' +#, +q# + , , + " ","' + *" *'" + " ; . +#, +q# +* ' , ' ` +' ` * . ' + ' `' ",' +* ' " * . +" * ', ' +#, +); + + $anim->new_entity( + shape => \@splat_image, + position => [ $x - 4, $y - 2, $z-2 ], + default_color => 'RED', + callback_args => [ 0, 0, 0, .25 ], + transparent => ' ', + die_frame => 15, + ); +} + +sub add_shark { + my ($old_ent, $anim) = @_; + my @shark_image = ( +q# + __ + ( `\ + ,??????????????????????????) `\ +;' `.????????????????????????( `\__ + ; `.?????????????__..---'' `~~~~-._ + `. `.____...--'' (b `--._ + > _.-' .(( ._ ) + .`.-`--...__ .-' -.___.....-(|/|/|/|/' + ;.'?????????`. ...----`.___.',,,_______......---' + '???????????'-' +#, +q# + __ + /' ) + /' (??????????????????????????, + __/' )????????????????????????.' `; + _.-~~~~' ``---..__?????????????.' ; + _.--' b) ``--...____.' .' +( _. )). `-._ < + `\|\|\|\|)-.....___.- `-. __...--'-.'. + `---......_______,,,`.___.'----... .'?????????`.; + `-`???????????` +#, + ); + + + my @shark_mask = ( +q# + cR + cWWWWWWWW +#, +q# + Rc + WWWWWWWWc +#, + ); + + my $dir = int(rand(2)); + my $x = -53; + my $y = int(rand($anim->height() - (10 + 9))) + 9; + my $teeth_x = -9; + my $teeth_y = $y + 7; + my $speed = 2; + if($dir) { + $speed *= -1; + $x = $anim->width()-2; + $teeth_x = $x + 9; + } + + $anim->new_entity( + type => 'teeth', + shape => "*", + position => [ $teeth_x, $teeth_y, $depth{'shark'}+1 ], + depth => $depth{'fish_end'} - $depth{'fish_start'}, + callback_args => [ $speed, 0, 0 ], + physical => 1, + ); + + $anim->new_entity( + type => "shark", + color => $shark_mask[$dir], + shape => $shark_image[$dir], + auto_trans => 1, + position => [ $x, $y, $depth{'shark'} ], + default_color => 'WHITE', + callback_args => [ $speed, 0, 0 ], + die_offscreen => 1, + death_cb => sub { group_death(@_, 'teeth') }, + default_color => 'CYAN', + ); + +} + +# when a shark dies, kill the "teeth" too, the associated +# entity that does the actual collision +sub group_death { + my ($entity, $anim, @bound_types) = @_; + foreach my $type (@bound_types) { + my $bound_entities = $anim->get_entities_of_type($type); + foreach my $obj (@{$bound_entities}) { + $anim->del_entity($obj); + } + } + random_object($entity, $anim); +} + +# pull the fishhook, line and whatever got caught back +# to the surface +sub retract { + my ($entity) = @_; + $entity->physical(0); + if($entity->type eq 'fish') { + my @pos = $entity->position(); + $pos[2] = $depth{'water_gap2'}; + $entity->position( @pos ); + $entity->callback( \&fishhook_cb ); + } else { + $entity->callback_args( 'hooked' ); + } +} + +# move the fishhook +sub fishhook_cb { + my ($entity, $anim) = @_; + + my @pos = $entity->position; + + # this means we hooked something, reel it in + if(defined($entity->callback_args())) { + $pos[1]--; + + # otherwise, just lower until we reach 1/4 from the bottom + } else { + if( ( $pos[1] + $entity->height) < $anim->height * .75) { + $pos[1]++; + } + } + + return @pos; +} + +sub add_fishhook { + my ($old_ent, $anim) = @_; + + my $hook_image = +q{ + o + || + || +/ \ || + \__// + `--' +}; + + my $point_image = +q{ +. +\ +}; + my $line_image = "|\n"x50 . " \n"x6; + + my $x = 10 + ( int(rand($anim->width() - 20)) ); + my $y = -4; + my $point_x = $x + 1; + my $point_y = $y + 2; + + $anim->new_entity( + type => 'fishline', + shape => $line_image, + position => [ $x + 7, $y - 50, $depth{'water_line1'} ], + auto_trans => 1, + callback_args => undef, + callback => \&fishhook_cb, + ); + + $anim->new_entity( + type => 'fishhook', + shape => $hook_image, + trans_char => ' ', + position => [ $x, $y, $depth{'water_line1'} ], + auto_trans => 1, + die_offscreen => 1, + death_cb => sub { group_death(@_, 'teeth', 'fishline') }, + default_color => 'GREEN', + callback_args => undef, + callback => \&fishhook_cb, + ); + + $anim->new_entity( + type => 'hook_point', + shape => $point_image, + position => [ $point_x, $point_y, $depth{'shark'}+1 ], + depth => $depth{'fish_end'} - $depth{'fish_start'}, + callback_args => undef, + physical => 1, + default_color => 'GREEN', + callback => \&fishhook_cb, + + ); +} + +sub add_ship { + my ($old_ent, $anim) = @_; + + my @ship_image = ( +q{ + | | | + )_) )_) )_) + )___))___))___)\ + )____)____)_____)\\\ +_____|____|____|____\\\\\__ +\ / +}, +q{ + | | | + (_( (_( (_( + /(___((___((___( + //(_____(____(____( +__///____|____|____|_____ + \ / +}); + + my @ship_mask = ( +q{ + y y y + w + ww +yyyyyyyyyyyyyyyyyyyywwwyy +y y +}, +q{ + y y y + w + ww +yywwwyyyyyyyyyyyyyyyyyyyy + y y +}); + + my $dir = int(rand(2)); + my $x = -24; + my $speed = 1; + if($dir) { + $speed *= -1; + $x = $anim->width()-2; + } + + $anim->new_entity( + color => $ship_mask[$dir], + shape => $ship_image[$dir], + auto_trans => 1, + position => [ $x, 0, $depth{'water_gap1'} ], + default_color => 'WHITE', + callback_args => [ $speed, 0, 0 ], + die_offscreen => 1, + death_cb => \&random_object, + ); +} + +sub add_whale { + my ($old_ent, $anim) = @_; + my @whale_image = ( +q{ + .-----: + .' `. +,????/ (o) \ +\`._/ ,__) +}, +q{ + :-----. + .' `. + / (o) \????, +(__, \_.'/ +}); + my @whale_mask = ( +q{ + C C + CCCCCCC + C C C + BBBBBBB + BB BB +B B BWB B +BBBBB BBBB +}, +q{ + C C + CCCCCCC + C C C + BBBBBBB + BB BB + B BWB B B +BBBB BBBBB +} +); + + my @water_spout = ( +q{ + : +},q{ + : + : +},q{ + . . + -:- + : +},q{ + . . + .-:-. + : +},q{ + . . +'.-:-.` +' : ' +},q{ + .- -. +; : ; +},q{ +; ; +}); + + + my $dir = int(rand(2)); + my $x; + my $speed = 1; + my $spout_align; + my @whale_anim; + my @whale_anim_mask; + + if($dir) { + $spout_align = 1; + $speed *= -1; + $x = $anim->width()-2; + } else { + $spout_align = 11; + $x = -18; + } + + # no water spout + for (1..5) { + push(@whale_anim, "\n\n\n" . $whale_image[$dir]); + push(@whale_anim_mask, $whale_mask[$dir]); + } + + # animate water spout + foreach my $spout_frame (@water_spout) { + my $whale_frame = $whale_image[$dir]; + my $aligned_spout_frame; + $aligned_spout_frame = join("\n" . ' 'x$spout_align, split("\n", $spout_frame)); + $whale_frame = $aligned_spout_frame . $whale_image[$dir]; + push(@whale_anim, $whale_frame); + push(@whale_anim_mask, $whale_mask[$dir]); + } + + $anim->new_entity( + color => \@whale_anim_mask, + shape => \@whale_anim, + auto_trans => 1, + position => [ $x, 0, $depth{'water_gap2'} ], + default_color => 'WHITE', + callback_args => [ $speed, 0, 0, 1 ], + die_offscreen => 1, + death_cb => \&random_object, + ); + +} + +sub add_monster { + my ($old_ent, $anim) = @_; + my @monster_image = ( + [ +q{ + ____ + __??????????????????????????????????????????/ o \ + / \????????_?????????????????????_???????/ ____ > + _??????| __ |?????/ \????????_????????/ \????| | + | \?????| || |????| |?????/ \?????| |???| | +},q{ + ____ + __?????????/ o \ + _?????????????????????_???????/ \?????/ ____ > + _???????/ \????????_????????/ \????| __ |???| | + | \?????| |?????/ \?????| |???| || |???| | +},q{ + ____ + __????????????????????/ o \ + _??????????????????????_???????/ \????????_???????/ ____ > +| \??????????_????????/ \????| __ |?????/ \????| | + \ \???????/ \?????| |???| || |????| |???| | +},q{ + ____ + __???????????????????????????????/ o \ + _??????????_???????/ \????????_??????????????????/ ____ > + | \???????/ \????| __ |?????/ \????????_??????| | + \ \?????| |???| || |????| |?????/ \????| | +} + ],[ +q{ + ____ + / o \??????????????????????????????????????????__ +< ____ \???????_?????????????????????_????????/ \ + | |????/ \????????_????????/ \?????| __ |??????_ + | |???| |?????/ \?????| |????| || |?????/ | +},q{ + ____ + / o \?????????__ +< ____ \?????/ \???????_?????????????????????_ + | |???| __ |????/ \????????_????????/ \???????_ + | |???| || |???| |?????/ \?????| |?????/ | +},q{ + ____ + / o \????????????????????__ +< ____ \???????_????????/ \???????_??????????????????????_ + | |????/ \?????| __ |????/ \????????_??????????/ | + | |???| |????| || |???| |?????/ \???????/ / +},q{ + ____ + / o \???????????????????????????????__ +< ____ \??????????????????_????????/ \???????_??????????_ + | |??????_????????/ \?????| __ |????/ \???????/ | + | |????/ \?????| |????| || |???| |?????/ / +} + ]); + + my @monster_mask = ( +q{ + W +},q{ + W +}); + my $dir = int(rand(2)); + my $x; + my $speed = 2; + if($dir) { + $speed *= -1; + $x = $anim->width()-2; + } else { + $x = -64 + } + my @monster_anim_mask; + for(1..4) { push(@monster_anim_mask, $monster_mask[$dir]); } + + $anim->new_entity( + shape => $monster_image[$dir], + auto_trans => 1, + color => \@monster_anim_mask, + position => [ $x, 2, $depth{'water_gap2'} ], + callback_args => [ $speed, 0, 0, .25 ], + death_cb => \&random_object, + die_offscreen => 1, + default_color => 'GREEN', + ); +} + +sub add_big_fish { + my ($old_ent, $anim) = @_; + + my @big_fish_image = ( +q{ + ______ +`""-. `````-----.....__ + `. . . `-. + : . . `. + , : . . _ : +: `. : (@) `._ + `. `..' . =`-. .__) + ; . = ~ : .-" + .' .'`. . . =.-' `._ .' +: .' : . .' + ' .' . . . .-' + .'____....----''.'=.' + "" .'.' + ''"'` +},q{ + ______ + __.....-----''''' .-""' + .-' . . .' + .' . . : + : _ . . : , + _.' (@) : .' : +(__. .-'= . `..' .' + "-. : ~ = . ; + `. _.' `-.= . . .'`. `. + `. . : `. : + `-. . . . `. ` + `.=`.``----....____`. + `.`. "" + '`"`` +}); + + my @big_fish_mask = ( +q{ + 111111 +11111 11111111111111111 + 11 2 2 111 + 1 2 2 11 + 1 1 2 2 1 1 +1 11 1 1W1 111 + 11 1111 2 1111 1111 + 1 2 1 1 1 111 + 11 1111 2 2 1111 111 11 +1 11 1 2 11 + 1 11 2 2 2 111 + 111111111111111111111 + 11 1111 + 11111 +},q{ + 111111 + 11111111111111111 11111 + 111 2 2 11 + 11 2 2 1 + 1 1 2 2 1 1 + 111 1W1 1 11 1 +1111 1111 2 1111 11 + 111 1 1 1 2 1 + 11 111 1111 2 2 1111 11 + 11 2 1 11 1 + 111 2 2 2 11 1 + 111111111111111111111 + 1111 11 + 11111 +}); + + + my $dir = int(rand(2)); + my $x; + my $speed = 3; + if($dir) { + $x = $anim->width()-1; + $speed *= -1; + } else { + $x = -34; + } + my $max_height = 9; + my $min_height = $anim->height() - 15; + my $y = int(rand($min_height - $max_height)) + $max_height; + my $color_mask = rand_color($big_fish_mask[$dir]); + $anim->new_entity( + shape => $big_fish_image[$dir], + auto_trans => 1, + color => $color_mask, + position => [ $x, $y, $depth{'shark'} ], + callback_args => [ $speed, 0, 0 ], + death_cb => \&random_object, + die_offscreen => 1, + default_color => 'YELLOW', + ); + +} + +sub add_ducks { + my ($old_ent, $anim) = @_; + my @duck_image = ( + [ +q{ + _??????????_??????????_ +,____(')=??,____(')=??,____(')< + \~~= ')????\~~= ')????\~~= ') +},q{ + _??????????_??????????_ +,____(')=??,____(')(')____,??=(')____,??=(')____, + (` =~~/????(` =~~/????(` =~~/ +},q{ + _??????????_??????????_ +=(')____,??>(')____,??=(')____, + (` =~~/????(` =~~/????(` =~~/ +},q{ + _??????????_??????????_ +=(')____,??=(')____,??>(')____, + (` =~~/????(` =~~/????(` =~~/ +} + ] + ); + + my @duck_mask = ( +q{ + g g g +wwwwwgcgy wwwwwgcgy wwwwwgcgy + wwww Ww wwww Ww wwww Ww +},q{ + g g g +ygcgwwwww ygcgwwwww ygcgwwwww + wW wwww wW wwww wW wwww +}); + + my $dir = int(rand(2)); + my $x; + my $speed = 1; + if($dir) { + $speed *= -1; + $x = $anim->width()-2; + } else { + $x = -30 + } + + $anim->new_entity( + shape => $duck_image[$dir], + auto_trans => 1, + color => $duck_mask[$dir], + position => [ $x, 5, $depth{'water_gap3'} ], + callback_args => [ $speed, 0, 0, .25 ], + death_cb => \&random_object, + die_offscreen => 1, + default_color => 'WHITE', + ); +} + +sub add_dolphins { + my ($old_ent, $anim) = @_; + my @dolphin_image = ( + [ +q{ + , + __)\_ +(\_.-' a`-. +(/~~````(/~^^` +},q{ + , +(\__ __)\_ +(/~.'' a`-. + ````\)~^^` +} + ],[ +q{ + , + _/(__ +.-'a `-._/) +'^^~\)''''~~\) +},q{ + , + _/(__ __/) +.-'a ``.~\) +'^^~(/'''' +} + ] + ); + + + my @dolphin_mask = ( +q{ + W +},q{ + W +}); + + + my $dir = int(rand(2)); + + my $x; + my $speed = 1; + my $distance = 15; # how far apart the dolphins are + + # right to left + if($dir) { + $speed *= -1; + $distance *= -1; + $x = $anim->width()-2; + + # left to right + } else { + $x = -13 + } + + my $up = [$speed,-.5,0,.5]; + my $down = [$speed,.5,0,.5]; + my $glide = [$speed,0,0,.5]; + + my @path; + + for(1..14) { push(@path, $up); } + for(1..2) { push(@path, $glide); } + for(1..14) { push(@path, $down); } + for(1..6) { push(@path, $glide); } + + my $dolphin3 = $anim->new_entity( + shape => $dolphin_image[$dir], + auto_trans => 1, + color => $dolphin_mask[$dir], + position => [ $x - ($distance * 2), 8, $depth{'water_gap3'} ], + callback_args => [ 0, [@path] ], + death_cb => \&random_object, + die_offscreen => 0, + default_color => 'blue', + ); + + my $dolphin2 = $anim->new_entity( + shape => $dolphin_image[$dir], + auto_trans => 1, + color => $dolphin_mask[$dir], + position => [ $x - $distance, 2, $depth{'water_gap3'} ], + callback_args => [ 12, [@path] ], + die_offscreen => 0, + default_color => 'BLUE', + ); + + my $dolphin1 = $anim->new_entity( + shape => $dolphin_image[$dir], + auto_trans => 1, + color => $dolphin_mask[$dir], + position => [ $x, 5, $depth{'water_gap3'} ], + callback_args => [ 24, [@path] ], + # have the lead dolphin tell the others to die offscreen, since they start offscreen + death_cb => sub{ $dolphin2->die_offscreen(1); $dolphin3->die_offscreen(1) }, + die_offscreen => 1, + default_color => 'CYAN', + ); + +} + +sub add_swan { + my ($old_ent, $anim) = @_; + my @swan_image = ( + [ +q{ + ___ +,_ / _,\ +| \ \( \| +| \_ \\\ +(_ \_) \ +(\_ ` \ + \ -=~ / +} + ],[ +q{ + ___ +/,_ \ _, +|/ )/ / | + // _/ | + / ( / _) +/ ` _/) +\ ~=- / +} + ] + ); + + my @swan_mask = ( +q{ + g + yy +},q{ + g +yy +}); + + my $dir = int(rand(2)); + my $x; + my $speed = 1; + if($dir) { + $speed *= -1; + $x = $anim->width()-2; + } else { + $x = -10 + } + + $anim->new_entity( + shape => $swan_image[$dir], + auto_trans => 1, + color => $swan_mask[$dir], + position => [ $x, 1, $depth{'water_gap3'} ], + callback_args => [ $speed, 0, 0, .25 ], + death_cb => \&random_object, + die_offscreen => 1, + default_color => 'WHITE', + ); +} + +sub init_random_objects { + return ( + \&add_ship, + \&add_whale, + \&add_monster, + \&add_big_fish, + \&add_shark, + \&add_fishhook, + \&add_swan, + \&add_ducks, + \&add_dolphins, + ); +} + +# add one of the random objects to the screen +sub random_object { + my ($dead_object, $anim) = @_; + my $sub = int(rand(scalar(@random_objects))); + $random_objects[$sub]->($dead_object, $anim); +} + +sub dprint { + open(D, ">>", "debug"); + print D @_, "\n"; + close(D); +} + +sub sighandler { + my ($sig) = @_; + if($sig eq 'INT') { quit(); } + elsif($sig eq 'WINCH') { + # ignore SIGWINCH, only redraw when requested + } + else { quit("Exiting with SIG$sig"); } +} + +sub quit { + my ($mesg) = @_; + print STDERR $mesg, "\n" if(defined($mesg)); + exit; +} + + +sub initialize { + # this may be paranoid, but i don't want to leave + # the user's terminal in a state that they might not + # know how to fix if we die badly + foreach my $sig (keys %SIG) { + $SIG{$sig} = 'sighandler' unless(defined($SIG{$sig})); + } +} + + +sub center { + my ($width, $mesg) = @_; + my $l = length($mesg); + if($l < $width) { + return ' 'x(int(($width - length($mesg))/2)) . $mesg; + } + elsif($l > $width) { + return(substr($mesg, 0, ($width - ($l + 3))) . "..."); + } + else { + return $mesg; + } +} + +sub rand_color { + my ($color_mask) = @_; + my @colors = ('c','C','r','R','y','Y','b','B','g','G','m','M'); + foreach my $i (1..9) { + my $color = $colors[int(rand($#colors))]; + $color_mask =~ s/$i/$color/gm; + } + return $color_mask; +} diff --git a/.local/bin/getcomproot b/.local/bin/getcomproot new file mode 100755 index 0000000..d34a2e4 --- /dev/null +++ b/.local/bin/getcomproot @@ -0,0 +1,12 @@ +#!/bin/bash + +# A helper script for LaTeX/groff files used by `compiler` and `opout`. +# The user can add the root file of a larger project as a comment as below: +# % root = mainfile.tex +# And the compiler script will run on that instead of the opened file. + +texroot="$(grep -i "^.\+\s*root\s*=\s*\S\+" "$1")" +texroot="${texroot##*=}" +texroot="${texroot//[\"\' ]}" + +[ -f "$texroot" ] && readlink -f "$texroot" || exit 1 diff --git a/.local/bin/getnf b/.local/bin/getnf new file mode 100755 index 0000000..54c97e1 --- /dev/null +++ b/.local/bin/getnf @@ -0,0 +1,103 @@ +#!/bin/bash + +#defining variables +nerdfontsrepo='https://api.github.com/repos/ryanoasis/nerd-fonts' +aFontInstalled="False" +keepArchives="False" +distDir="$HOME/.local/share/fonts" +downDir="$HOME/Downloads/NerdFonts" +os=$(uname) + +# help message +usage() { + echo "getNF: A Better way to install NerdFonts" + echo "" + echo "Usage:" + echo "-h print this help message and exit" + echo "-k Keep the downloaded archives" + echo "" +} + +# setting flags +while getopts :hk option; do + case "${option}" in + h) usage && exit 0 ;; + k) keepArchives="True" ;; + *) usage && exit 0 + esac +done + +# For Macs, need to set a few different things +if [[ "$os" == 'Darwin' ]]; then + distDir="$HOME/Library/Fonts" +fi + +# Check if the distDir and downDir exists, if it doesn't, create it +[ -d "$distDir" ] || mkdir -p "$distDir" && echo "Fonts Directory exists, good" +[ -d "$downDir" ] || mkdir -p "$downDir" && echo "Download Fonts Directory exists, good" + +# get font names +nerdFonts=$(curl --silent "$nerdfontsrepo/contents/patched-fonts?ref=master" | \ + grep "name" | \ + awk -F":" '{print $2}' | \ + sed 's/["",]//g;/install\.ps1/d') + +#get the latest release number from NerdFonts github repo +release=$(curl --silent "$nerdfontsrepo/releases/latest" | \ + grep -Po '"tag_name": "\K.*?(?=")') + +# use fzf to select the fonts to be installed +listFonts=$(printf '%s\n' "${nerdFonts[@]}" | fzf -m) + +#loop over the selected fonts in listFonts, download and install them +for i in $listFonts; do + checkFont=$(fc-list | grep -i "$i") + if [ -z "$checkFont" ]; then #If the font already is installed, skip it + echo "$i font download started" && + pushd "$downDir" > /dev/null && + #download the font + curl -LJO -\# "https://github.com/ryanoasis/nerd-fonts/releases/download/$release/$i.zip" \ + -o "$i.zip" --create-dirs && + echo "$i font download finished" && + echo "$i font unziping started" && + #Unzipe the downloaded archive + # unzip -qq "$downDir/$i.zip" -d "$distDir" && + unzip -qq "$i.zip" -d "$distDir" && + echo "$i font unzipping finished" && + echo "Font $i Installed" && + installedFontName=$(curl --silent "$nerdfontsrepo/contents/patched-fonts/$i/Regular/complete?ref=master" | \ + grep ".ttf" | \ + awk -F ":" 'FNR == 1 {print $2}' | \ + awk '{print $1}' | \ + sed 's/"//g') && + #set this variable to true so that the font cache get's updated + aFontInstalled=True && # We do this before the Additional info, so even if it fails the font cache will be refreshed + #Additional info, only if we can get the real font name + if [ -n "$installedFontName" ]; then + echo "$i provides:" && + fc-list | grep -i "$installedFontName" | \ + awk -F "/" '{print $7}' | \ + sed 's/style\=//' | \ + awk -F ":" 'BEGIN {print "FONT NAME" " | " "FILE NAME" " | " "STYLE"} {print $2 " | " $1 " | " $3}' | \ + column -s "|" -t + fi + popd > /dev/null + else + echo "Font $i already installed" + fi +done + +# If a font was installed, Update the font cache and remove the archive +if [ "$aFontInstalled" = "True" ]; then + echo "Regenerating fc-cache" + fc-cache -f 2>&1 && echo "fc-cache: regeneration succeeded!" + #check if the user hasn't chooen to keep the updated, if not, remove them + if [ "$keepArchives" = "False" ]; then + echo "Removing zip font files in $downDir" && + rm $downDir/*.zip + else + echo "The archive files are in $downDir" + fi +fi + +echo "All is done!" diff --git a/.local/bin/google b/.local/bin/google new file mode 100755 index 0000000..5a1caa5 --- /dev/null +++ b/.local/bin/google @@ -0,0 +1,4 @@ +#!/bin/sh + +url="https://google.com/search?q=$(urlencode "$*")" +exec lynx "https://google.com/search?q=$url" diff --git a/.local/bin/ifinstalled b/.local/bin/ifinstalled new file mode 100755 index 0000000..c192eba --- /dev/null +++ b/.local/bin/ifinstalled @@ -0,0 +1,12 @@ +#!/bin/sh + +# Some optional functions in LARBS require programs not installed by default. I +# use this little script to check to see if a command exists and if it doesn't +# it informs the user that they need that command to continue. This is used in +# various other scripts for clarity's sake. + +for x in "$@"; do + if ! which "$x" >/dev/null 2>&1 && ! pacman -Qq "$x" >/dev/null 2>&1; then + notify-send "📦 $x" "must be installed for this function." && exit 1 ; + fi +done diff --git a/.local/bin/inc-sink-volume b/.local/bin/inc-sink-volume new file mode 100755 index 0000000..229273a --- /dev/null +++ b/.local/bin/inc-sink-volume @@ -0,0 +1,11 @@ +#!/bin/bash + +default_loc=/tmp/pactl-default-sink + +if [ ! -f "$default_loc" ] ; then + set-default-sink +fi + +default=$(cat $default_loc) + +pactl set-sink-volume $default +5% diff --git a/.local/bin/inc-source-volume b/.local/bin/inc-source-volume new file mode 100755 index 0000000..a0a086d --- /dev/null +++ b/.local/bin/inc-source-volume @@ -0,0 +1,11 @@ +#!/bin/bash + +default_loc=/tmp/pactl-default-source + +if [ ! -f "$default_loc" ] ; then + set-default-source +fi + +default=$(cat $default_loc) + +pactl set-source-volume $default +5% diff --git a/.local/bin/iommu.sh b/.local/bin/iommu.sh new file mode 100755 index 0000000..556d5aa --- /dev/null +++ b/.local/bin/iommu.sh @@ -0,0 +1,8 @@ +#!/bin/bash +shopt -s nullglob +for g in /sys/kernel/iommu_groups/*; do + echo "IOMMU Group ${g##*/}:" + for d in $g/devices/*; do + echo -e "\t$(lspci -nns ${d##*/})" + done; +done; diff --git a/.local/bin/isosec b/.local/bin/isosec new file mode 100755 index 0000000..330d368 --- /dev/null +++ b/.local/bin/isosec @@ -0,0 +1,2 @@ +#!/bin/sh +exec date -u +%Y%m%d%H%M%S "$@" diff --git a/.local/bin/kbswitcher b/.local/bin/kbswitcher new file mode 100755 index 0000000..94bca7a --- /dev/null +++ b/.local/bin/kbswitcher @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +fn="/tmp/kbswitcher.status" + +layout="" + +# first run the file won't exist +if [ ! -f "$fn" ]; then + layout="dvorak" + echo "dvorak" > "$fn" +else + case $(cat "$fn") in + us) + echo "dvorak" > "$fn" + layout="dvorak" + ;; + dvorak) + echo "us" > "$fn" + layout="us" + ;; + esac +fi + +notify-send -u normal -r 161616 "kbswitcher" "Keyboard layout set to $layout." + +setxkbmap -layout "$layout" -option ctrl:nocaps diff --git a/.local/bin/lfub b/.local/bin/lfub new file mode 100755 index 0000000..9012f50 --- /dev/null +++ b/.local/bin/lfub @@ -0,0 +1,24 @@ +#!/bin/sh + +# This is a wrapper script for lb that allows it to create image previews with +# ueberzug. This works in concert with the lf configuration file and the +# lf-cleaner script. + +set -e + +cleanup() { + exec 3>&- + rm "$FIFO_UEBERZUG" +} + +if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ]; then + lf "$@" +else + [ ! -d "$HOME/.cache/lf" ] && mkdir -p "$HOME/.cache/lf" + export FIFO_UEBERZUG="$HOME/.cache/lf/ueberzug-$$" + mkfifo "$FIFO_UEBERZUG" + ueberzug layer -s <"$FIFO_UEBERZUG" -p json & + exec 3>"$FIFO_UEBERZUG" + trap cleanup HUP INT QUIT TERM PWR EXIT + lf "$@" 3>&- +fi diff --git a/.local/bin/lynx b/.local/bin/lynx new file mode 100755 index 0000000..44d1d73 --- /dev/null +++ b/.local/bin/lynx @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +# be sure to install the ca-certificates package + +lynxpath=/usr/bin/lynx +[[ ! -x $lynxpath ]] && lynxpath=/usr/local/bin/lynx + +useragent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.79 Safari/537.1 Lynx" + +if [ -e "$HOME/.config/lynx/lynx.cfg" ];then + export LYNX_CFG="$HOME/.config/lynx/lynx.cfg" +fi + +if [ -e "$HOME/.config/lynx/lynx.lss" ];then + export LYNX_LSS="$HOME/.config/lynx/lynx.lss" +fi + +if [ ! -x "$lynxpath" ]; then + echo "Doesn't look like lynx is installed." + exit 1 +fi + +exec "$lynxpath" --useragent="$useragent" "$@" diff --git a/.local/bin/maimpick b/.local/bin/maimpick new file mode 100755 index 0000000..8ea9f5e --- /dev/null +++ b/.local/bin/maimpick @@ -0,0 +1,18 @@ +#!/bin/sh + +# This is bound to Shift+PrintScreen by default, requires maim. It lets you +# choose the kind of screenshot to take, including copying the image or even +# highlighting an area to copy. scrotcucks on suicidewatch right now. + +# variables +output="$(date '+%y%m%d-%H%M-%S').png" +xclip_cmd="xclip -sel clip -t image/png" + +case "$(printf "a selected area\\ncurrent window\\nfull screen\\na selected area (copy)\\ncurrent window (copy)\\nfull screen (copy)" | dmenu -l 6 -i -p "Screenshot which area?")" in + "a selected area") maim -s pic-selected-"${output}" ;; + "current window") maim -q -d 0.2 -i "$(xdotool getactivewindow)" pic-window-"${output}" ;; + "full screen") maim -q -d 0.2 pic-full-"${output}" ;; + "a selected area (copy)") maim -s | ${xclip_cmd} ;; + "current window (copy)") maim -q -d 0.2 -i "$(xdotool getactivewindow)" | ${xclip_cmd} ;; + "full screen (copy)") maim -q -d 0.2 | ${xclip_cmd} ;; +esac diff --git a/.local/bin/mod_backlight b/.local/bin/mod_backlight new file mode 100755 index 0000000..aca3460 --- /dev/null +++ b/.local/bin/mod_backlight @@ -0,0 +1,19 @@ +#!/bin/bash + +if command -v brillo &> /dev/null; then + function send_notification() { + brightness=$(printf "%.0f\n" $(brillo -G)) + dunstify -a "changebrightness" -u low -r 9991 -h int:value:"$brightness" -i "brightness-$1" "Brightness: $brightness%" -t 2000 +} + + case $1 in + up) + brillo -A 5 -q -u 150000 + send_notification $1 + ;; + down) + brillo -U 5 -q -u 150000 + send_notification $1 + ;; + esac +fi diff --git a/.local/bin/networkmanager_dmenu b/.local/bin/networkmanager_dmenu new file mode 100755 index 0000000..137c968 --- /dev/null +++ b/.local/bin/networkmanager_dmenu @@ -0,0 +1,924 @@ +#!/usr/bin/env python3 +# encoding:utf8 +"""NetworkManager command line dmenu script. + +To add new connections or enable/disable networking requires policykit +permissions setup per: +https://wiki.archlinux.org/index.php/NetworkManager#Set_up_PolicyKit_permissions + +OR running the script as root + +Add dmenu options and default terminal if desired to +~/.config/networkmanager-dmenu/config.ini + +""" +import pathlib +import struct +import configparser +import locale +import os +from os.path import expanduser +import shlex +from shutil import which +import sys +from time import sleep +import uuid +import subprocess + +# pylint: disable=import-error +import gi +gi.require_version('NM', '1.0') +from gi.repository import GLib, NM # noqa pylint: disable=wrong-import-position +# pylint: enable=import-error + +ENV = os.environ.copy() +ENV['LC_ALL'] = 'C' +ENC = locale.getpreferredencoding() + +CONF = configparser.ConfigParser() +CONF.read(expanduser("~/.config/networkmanager-dmenu/config.ini")) + + +def cli_args(): + """ Don't override dmenu_cmd function arguments with CLI args. Removes -l + and -p if those are passed on the command line. + + Exception: if -l is passed and dmenu_command is not defined, assume that the + user wants to switch dmenu to the vertical layout and include -l. + + Returns: List of additional CLI arguments + + """ + args = sys.argv[1:] + cmd = CONF.get('dmenu', 'dmenu_command', fallback=False) + if "-l" in args or "-p" in args: + for nope in ['-l', '-p'] if cmd is not False else ['-p']: + try: + nope_idx = args.index(nope) + del args[nope_idx] + del args[nope_idx] + except ValueError: + pass + return args + + +def dmenu_pass(command, color): + """Check if dmenu passphrase patch is applied and return the correct command + line arg list + + Args: command - string + color - obscure color string + Returns: list or None + + """ + if command != 'dmenu': + return None + try: + # Check for dmenu password patch + dm_patch = b'P' in subprocess.run(["dmenu", "-h"], + capture_output=True, + check=False).stderr + except FileNotFoundError: + dm_patch = False + return ["-P"] if dm_patch else ["-nb", color, "-nf", color] + + +def dmenu_cmd(num_lines, prompt="Networks", active_lines=None): + """Parse config.ini for menu options + + Args: args - num_lines: number of lines to display + prompt: prompt to show + active_lines: list of line numbers to tag as active + Returns: command invocation (as a list of strings) for example + ["dmenu", "-l", "", "-p", "", "-i"] + + """ + # Create command string + commands = {"dmenu": ["-p", str(prompt)], + "rofi": ["-dmenu", "-p", str(prompt), "-l", str(num_lines)], + "bemenu": ["-p", str(prompt)], + "wofi": ["-p", str(prompt)]} + command = shlex.split(CONF.get('dmenu', 'dmenu_command', fallback="dmenu")) + command.extend(cli_args()) + command.extend(commands.get(command[0], [])) + # Rofi Highlighting + rofi_highlight = CONF.getboolean('dmenu', 'rofi_highlight', fallback=False) + if rofi_highlight is True and command[0] == "rofi" and active_lines: + command.extend(["-a", ",".join([str(num) for num in active_lines])]) + # Passphrase prompts + obscure = CONF.getboolean('dmenu_passphrase', 'obscure', fallback=False) + if prompt == "Passphrase" and obscure is True: + obscure_color = CONF.get('dmenu_passphrase', 'obscure_color', fallback='#222222') + pass_prompts = {"dmenu": dmenu_pass(command[0], obscure_color), + "rofi": ['-password'], + "bemenu": ['-x'], + "wofi": ['-P']} + command.extend(pass_prompts.get(command[0], [])) + return command + + +def choose_adapter(client): + """If there is more than one wifi adapter installed, ask which one to use + + """ + devices = client.get_devices() + devices = [i for i in devices if i.get_device_type() == NM.DeviceType.WIFI] + if not devices: + return None + if len(devices) == 1: + return devices[0] + device_names = "\n".join([d.get_iface() for d in devices]) + sel = subprocess.run(dmenu_cmd(len(devices), "CHOOSE ADAPTER:"), + capture_output=True, + check=False, + env=ENV, + input=device_names, + encoding=ENC).stdout + if not sel.strip(): + sys.exit() + devices = [i for i in devices if i.get_iface() == sel.strip()] + assert len(devices) == 1 + return devices[0] + + +def is_installed(cmd): + """Check if a utility is installed""" + return which(cmd) is not None + + +def bluetooth_get_enabled(): + """Check if bluetooth is enabled via rfkill. + + Returns None if no bluetooth device was found. + """ + # See https://www.kernel.org/doc/Documentation/ABI/stable/sysfs-class-rfkill + for path in pathlib.Path('/sys/class/rfkill/').glob('rfkill*'): + if (path / 'type').read_text().strip() == 'bluetooth': + return (path / 'soft').read_text().strip() == '0' + return None + + +def create_other_actions(client): + """Return list of other actions that can be taken + + """ + networking_enabled = client.networking_get_enabled() + networking_action = "Disable" if networking_enabled else "Enable" + + wifi_enabled = client.wireless_get_enabled() + wifi_action = "Disable" if wifi_enabled else "Enable" + + bluetooth_enabled = bluetooth_get_enabled() + bluetooth_action = "Disable" if bluetooth_enabled else "Enable" + + actions = [Action(f"{wifi_action} Wifi", toggle_wifi, + not wifi_enabled), + Action(f"{networking_action} Networking", + toggle_networking, not networking_enabled)] + if bluetooth_enabled is not None: + actions.append(Action(f"{bluetooth_action} Bluetooth", + toggle_bluetooth, not bluetooth_enabled)) + actions += [Action("Launch Connection Manager", launch_connection_editor), + Action("Delete a Connection", delete_connection)] + if wifi_enabled: + actions.append(Action("Rescan Wifi Networks", rescan_wifi)) + return actions + + +def rescan_wifi(): + """ + Rescan Wifi Access Points + """ + delay = CONF.getint('nmdm', 'rescan_delay', fallback=5) + for dev in CLIENT.get_devices(): + if gi.repository.NM.DeviceWifi == type(dev): + try: + dev.request_scan_async(None, rescan_cb, None) + LOOP.run() + sleep(delay) + notify("Wifi scan complete") + main() + except gi.repository.GLib.Error as err: + # Too frequent rescan error + notify("Wifi rescan failed", urgency="critical") + if not err.code == 6: # pylint: disable=no-member + raise err + + +def rescan_cb(dev, res, data): + """Callback for rescan_wifi. Just for notifications + + """ + if dev.request_scan_finish(res) is True: + notify("Wifi scan running...") + else: + notify("Wifi scan failed", urgency="critical") + LOOP.quit() + + +def ssid_to_utf8(nm_ap): + """ Convert binary ssid to utf-8 """ + ssid = nm_ap.get_ssid() + if not ssid: + return "" + ret = NM.utils_ssid_to_utf8(ssid.get_data()) + return ret + + +def prompt_saved(saved_cons): + """Prompt for a saved connection.""" + actions = create_saved_actions(saved_cons) + sel = get_selection(actions) + sel() + + +def ap_security(nm_ap): + """Parse the security flags to return a string with 'WPA2', etc. """ + flags = nm_ap.get_flags() + wpa_flags = nm_ap.get_wpa_flags() + rsn_flags = nm_ap.get_rsn_flags() + sec_str = "" + if ((flags & getattr(NM, '80211ApFlags').PRIVACY) and + (wpa_flags == 0) and (rsn_flags == 0)): + sec_str = " WEP" + if wpa_flags: + sec_str = " WPA1" + if rsn_flags & getattr(NM, '80211ApSecurityFlags').KEY_MGMT_PSK: + sec_str += " WPA2" + if rsn_flags & getattr(NM, '80211ApSecurityFlags').KEY_MGMT_SAE: + sec_str += " WPA3" + if ((wpa_flags & getattr(NM, '80211ApSecurityFlags').KEY_MGMT_802_1X) or + (rsn_flags & getattr(NM, '80211ApSecurityFlags').KEY_MGMT_802_1X)): + sec_str += " 802.1X" + if ((wpa_flags & getattr(NM, '80211ApSecurityFlags').KEY_MGMT_OWE) or + (rsn_flags & getattr(NM, '80211ApSecurityFlags').KEY_MGMT_OWE)): + sec_str += " OWE" + + # If there is no security use "--" + if sec_str == "": + sec_str = "--" + return sec_str.lstrip() + + +class Action(): # pylint: disable=too-few-public-methods + """Helper class to execute functions from a string variable""" + def __init__(self, + name, + func, + args=None, + active=False): + self.name = name + self.func = func + self.is_active = active + if args is None: + self.args = None + elif isinstance(args, list): + self.args = args + else: + self.args = [args] + + def __str__(self): + return self.name + + def __call__(self): + if self.args is None: + self.func() + else: + self.func(*self.args) + + +def conn_matches_adapter(conn, adapter): + """Return True if the connection is applicable for the given adapter. + + There seem to be two ways for a connection specify what interface it belongs + to: + + - By setting 'mac-address' in [wifi] to the adapter's MAC + - By setting 'interface-name` in [connection] to the adapter's name. + + Depending on how the connection was added, it seems like either + 'mac-address', 'interface-name' or neither of both is set. + """ + # [wifi] mac-address + setting_wireless = conn.get_setting_wireless() + mac = setting_wireless.get_mac_address() + if mac is not None: + return mac == adapter.get_permanent_hw_address() + + # [connection] interface-name + setting_connection = conn.get_setting_connection() + interface = setting_connection.get_interface_name() + if interface is not None: + return interface == adapter.get_iface() + + # Neither is set, let's assume this connection is for multiple/all adapters. + return True + + +def process_ap(nm_ap, is_active, adapter): + """Activate/Deactivate a connection and get password if required""" + if is_active: + CLIENT.deactivate_connection_async(nm_ap, None, deactivate_cb, nm_ap) + LOOP.run() + else: + conns_cur = [i for i in CONNS if + i.get_setting_wireless() is not None and + conn_matches_adapter(i, adapter)] + con = nm_ap.filter_connections(conns_cur) + if len(con) > 1: + raise ValueError("There are multiple connections possible") + + if len(con) == 1: + CLIENT.activate_connection_async(con[0], adapter, nm_ap.get_path(), + None, activate_cb, nm_ap) + LOOP.run() + else: + if ap_security(nm_ap) != "--": + password = get_passphrase() + else: + password = "" + set_new_connection(nm_ap, password, adapter) + + +def activate_cb(dev, res, data): + """Notification if activate connection completed successfully + + """ + try: + conn = dev.activate_connection_finish(res) + except GLib.Error: + conn = None + if conn is not None: + notify(f"Activated {conn.get_id()}") + else: + notify(f"Problem activating {data.get_id()}", urgency="critical") + LOOP.quit() + + +def deactivate_cb(dev, res, data): + """Notification if deactivate connection completed successfully + + """ + if dev.deactivate_connection_finish(res) is True: + notify(f"Deactivated {data.get_id()}") + else: + notify(f"Problem deactivating {data.get_id()}", urgency="critical") + LOOP.quit() + + +def process_vpngsm(con, activate): + """Activate/deactive VPN or GSM connections""" + if activate: + CLIENT.activate_connection_async(con, None, None, + None, activate_cb, con) + else: + CLIENT.deactivate_connection_async(con, None, deactivate_cb, con) + LOOP.run() + + +def create_ap_actions(aps, active_ap, active_connection, adapter): # noqa pylint: disable=too-many-locals,line-too-long + """For each AP in a list, create the string and its attached function + (activate/deactivate) + + """ + active_ap_bssid = active_ap.get_bssid() if active_ap is not None else "" + + names = [ssid_to_utf8(ap) for ap in aps] + max_len_name = max([len(name) for name in names]) if names else 0 + secs = [ap_security(ap) for ap in aps] + max_len_sec = max([len(sec) for sec in secs]) if secs else 0 + + ap_actions = [] + + for nm_ap, name, sec in zip(aps, names, secs): + bars = NM.utils_wifi_strength_bars(nm_ap.get_strength()) + wifi_chars = CONF.get("dmenu", "wifi_chars", fallback=False) + if wifi_chars: + bars = "".join([wifi_chars[i] for i, j in enumerate(bars) if j == '*']) + is_active = nm_ap.get_bssid() == active_ap_bssid + compact = CONF.getboolean("dmenu", "compact", fallback=False) + if compact: + action_name = f"{name} {sec} {bars}" + else: + action_name = f"{name:<{max_len_name}s} {sec:<{max_len_sec}s} {bars:>4}" + if is_active: + ap_actions.append(Action(action_name, process_ap, + [active_connection, True, adapter], + active=True)) + else: + ap_actions.append(Action(action_name, process_ap, + [nm_ap, False, adapter])) + return ap_actions + + +def create_vpn_actions(vpns, active): + """Create the list of strings to display with associated function + (activate/deactivate) for VPN connections. + + """ + active_vpns = [i for i in active if i.get_vpn()] + return _create_vpngsm_actions(vpns, active_vpns, "VPN") + + +def create_wireguard_actions(wgs, active): + """Create the list of strings to display with associated function + (activate/deactivate) for Wireguard connections. + + """ + active_wgs = [i for i in active if i.get_connection_type() == "wireguard"] + return _create_vpngsm_actions(wgs, active_wgs, "Wireguard") + + +def create_eth_actions(eths, active): + """Create the list of strings to display with associated function + (activate/deactivate) for Ethernet connections. + + """ + active_eths = [i for i in active if 'ethernet' in i.get_connection_type()] + return _create_vpngsm_actions(eths, active_eths, "Eth") + + +def create_gsm_actions(gsms, active): + """Create the list of strings to display with associated function + (activate/deactivate) GSM connections.""" + active_gsms = [i for i in active if + i.get_connection() is not None and + i.get_connection().is_type(NM.SETTING_GSM_SETTING_NAME)] + return _create_vpngsm_actions(gsms, active_gsms, "GSM") + + +def create_blue_actions(blues, active): + """Create the list of strings to display with associated function + (activate/deactivate) Bluetooth connections.""" + active_blues = [i for i in active if + i.get_connection() is not None and + i.get_connection().is_type(NM.SETTING_BLUETOOTH_SETTING_NAME)] + return _create_vpngsm_actions(blues, active_blues, "Bluetooth") + + +def create_saved_actions(saved): + """Create the list of strings to display with associated function + (activate/deactivate) for VPN connections. + + """ + return _create_vpngsm_actions(saved, [], "SAVED") + + +def _create_vpngsm_actions(cons, active_cons, label): + active_con_ids = [a.get_id() for a in active_cons] + actions = [] + for con in cons: + is_active = con.get_id() in active_con_ids + action_name = f"{con.get_id()}:{label}" + if is_active: + active_connection = [a for a in active_cons + if a.get_id() == con.get_id()] + if len(active_connection) != 1: + raise ValueError(f"Multiple active connections match {con.get_id()}") + active_connection = active_connection[0] + + actions.append(Action(action_name, process_vpngsm, + [active_connection, False], active=True)) + else: + actions.append(Action(action_name, process_vpngsm, + [con, True])) + return actions + + +def create_wwan_actions(client): + """Create WWWAN actions + + """ + wwan_enabled = client.wwan_get_enabled() + wwan_action = "Disable" if wwan_enabled else "Enable" + return [Action(f"{wwan_action} WWAN", toggle_wwan, not wwan_enabled)] + + +def combine_actions(eths, aps, vpns, wgs, gsms, blues, wwan, others, saved): + # pylint: disable=too-many-arguments + """Combine all given actions into a list of actions. + + Args: args - eths: list of Actions + aps: list of Actions + vpns: list of Actions + gsms: list of Actions + blues: list of Actions + wwan: list of Actions + others: list of Actions + """ + compact = CONF.getboolean("dmenu", "compact", fallback=False) + empty_action = [Action('', None)] if not compact else [] + all_actions = [] + all_actions += eths + empty_action if eths else [] + all_actions += aps + empty_action if aps else [] + all_actions += vpns + empty_action if vpns else [] + all_actions += wgs + empty_action if wgs else [] + all_actions += gsms + empty_action if (gsms and wwan) else [] + all_actions += blues + empty_action if blues else [] + all_actions += wwan + empty_action if wwan else [] + all_actions += others + empty_action if others else [] + all_actions += saved + empty_action if saved else [] + return all_actions + + +def get_selection(all_actions): + """Spawn dmenu for selection and execute the associated action.""" + rofi_highlight = CONF.getboolean('dmenu', 'rofi_highlight', fallback=False) + inp = [] + + if rofi_highlight is True: + inp = [str(action) for action in all_actions] + else: + inp = [('== ' if action.is_active else ' ') + str(action) + for action in all_actions] + active_lines = [index for index, action in enumerate(all_actions) + if action.is_active] + + command = dmenu_cmd(len(inp), active_lines=active_lines) + sel = subprocess.run(command, + capture_output=True, + check=False, + input="\n".join(inp), + encoding=ENC, + env=ENV).stdout + + if not sel.rstrip(): + sys.exit() + + if rofi_highlight is False: + action = [i for i in all_actions + if ((str(i).strip() == str(sel.strip()) + and not i.is_active) or + ('== ' + str(i) == str(sel.rstrip('\n')) + and i.is_active))] + else: + action = [i for i in all_actions if str(i).strip() == sel.strip()] + assert len(action) == 1, f"Selection was ambiguous: '{str(sel.strip())}'" + return action[0] + + +def toggle_networking(enable): + """Enable/disable networking + + Args: enable - boolean + + """ + toggle = GLib.Variant.new_tuple(GLib.Variant.new_boolean(enable)) + try: + CLIENT.dbus_call(NM.DBUS_PATH, NM.DBUS_INTERFACE, "Enable", toggle, + None, -1, None, None, None) + except AttributeError: + # Workaround for older versions of python-gobject + CLIENT.networking_set_enabled(enable) + notify(f"Networking {'enabled' if enable is True else 'disabled'}") + + +def toggle_wifi(enable): + """Enable/disable Wifi + + Args: enable - boolean + + """ + toggle = GLib.Variant.new_boolean(enable) + try: + CLIENT.dbus_set_property(NM.DBUS_PATH, NM.DBUS_INTERFACE, "WirelessEnabled", toggle, + -1, None, None, None) + except AttributeError: + # Workaround for older versions of python-gobject + CLIENT.wireless_set_enabled(enable) + notify(f"Wifi {'enabled' if enable is True else 'disabled'}") + + +def toggle_wwan(enable): + """Enable/disable WWAN + + Args: enable - boolean + + """ + toggle = GLib.Variant.new_boolean(enable) + try: + CLIENT.dbus_set_property(NM.DBUS_PATH, NM.DBUS_INTERFACE, "WwanEnabled", toggle, + -1, None, None, None) + except AttributeError: + # Workaround for older versions of python-gobject + CLIENT.wwan_set_enabled(enable) + notify(f"Wwan {'enabled' if enable is True else 'disabled'}") + + +def toggle_bluetooth(enable): + """Enable/disable Bluetooth + + Args: enable - boolean + + References: + https://github.com/blueman-project/blueman/blob/master/blueman/plugins/mechanism/RfKill.py + https://www.kernel.org/doc/html/latest/driver-api/rfkill.html + https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/uapi/linux/rfkill.h?h=v5.8.9 + + """ + type_bluetooth = 2 + op_change_all = 3 + idx = 0 + soft_state = 0 if enable else 1 + hard_state = 0 + + data = struct.pack("IBBBB", idx, type_bluetooth, op_change_all, + soft_state, hard_state) + + try: + with open('/dev/rfkill', 'r+b', buffering=0) as rff: + rff.write(data) + except PermissionError: + notify("Lacking permission to write to /dev/rfkill.", + "Check README for configuration options.", + urgency="critical") + else: + notify(f"Bluetooth {'enabled' if enable else 'disabled'}") + + +def launch_connection_editor(): + """Launch nmtui or the gui nm-connection-editor + + """ + terminal = CONF.get("editor", "terminal", fallback="xterm") + gui_if_available = CONF.getboolean("editor", "gui_if_available", fallback=True) + guis = ["gnome-control-center", "nm-connection-editor"] + if gui_if_available is True: + for gui in guis: + if is_installed(gui): + subprocess.run(gui, check=False) + return + if is_installed("nmtui"): + subprocess.run([terminal, "-e", "nmtui"], check=False) + return + notify("No network connection editor installed", urgency="critical") + + +def get_passphrase(): + """Get a password + + Returns: string + + """ + pinentry = CONF.get("dmenu", "pinentry", fallback=None) + if pinentry: + pin = "" + out = subprocess.run(pinentry, + capture_output=True, + check=False, + encoding=ENC, + input='setdesc Get network password\ngetpin\n').stdout + if out: + res = out.split("\n")[2] + if res.startswith("D "): + pin = res.split("D ")[1] + return pin + return subprocess.run(dmenu_cmd(0, "Passphrase"), + stdin=subprocess.DEVNULL, + capture_output=True, + check=False, + encoding=ENC).stdout + + +def delete_connection(): + """Display list of NM connections and delete the selected one + + """ + conn_acts = [Action(i.get_id(), i.delete_async, args=[None, delete_cb, None]) for i in CONNS] + conn_names = "\n".join([str(i) for i in conn_acts]) + sel = subprocess.run(dmenu_cmd(len(conn_acts), "CHOOSE CONNECTION TO DELETE:"), + capture_output=True, + check=False, + input=conn_names, + encoding=ENC, + env=ENV).stdout + if not sel.strip(): + sys.exit() + action = [i for i in conn_acts if str(i) == sel.rstrip("\n")] + assert len(action) == 1, f"Selection was ambiguous: {str(sel)}" + action[0]() + LOOP.run() + + +def delete_cb(dev, res, data): + """Notification if delete completed successfully + + """ + if dev.delete_finish(res) is True: + notify(f"Deleted {dev.get_id()}") + else: + notify(f"Problem deleting {dev.get_id()}", urgency="critical") + LOOP.quit() + + +def set_new_connection(nm_ap, nm_pw, adapter): + """Setup a new NetworkManager connection + + Args: ap - NM.AccessPoint + pw - string + + """ + nm_pw = str(nm_pw).strip() + profile = create_wifi_profile(nm_ap, nm_pw, adapter) + CLIENT.add_and_activate_connection_async(profile, adapter, nm_ap.get_path(), + None, verify_conn, profile) + LOOP.run() + + +def create_wifi_profile(nm_ap, password, adapter): + # pylint: disable=line-too-long + # noqa From https://cgit.freedesktop.org/NetworkManager/NetworkManager/tree/examples/python/gi/add_connection.py + # noqa and https://cgit.freedesktop.org/NetworkManager/NetworkManager/tree/examples/python/dbus/add-wifi-psk-connection.py + # pylint: enable=line-too-long + """Create the NM profile given the AP and passphrase""" + ap_sec = ap_security(nm_ap) + profile = NM.SimpleConnection.new() + + s_con = NM.SettingConnection.new() + s_con.set_property(NM.SETTING_CONNECTION_ID, ssid_to_utf8(nm_ap)) + s_con.set_property(NM.SETTING_CONNECTION_UUID, str(uuid.uuid4())) + s_con.set_property(NM.SETTING_CONNECTION_TYPE, "802-11-wireless") + profile.add_setting(s_con) + + s_wifi = NM.SettingWireless.new() + s_wifi.set_property(NM.SETTING_WIRELESS_SSID, nm_ap.get_ssid()) + s_wifi.set_property(NM.SETTING_WIRELESS_MODE, 'infrastructure') + s_wifi.set_property(NM.SETTING_WIRELESS_MAC_ADDRESS, adapter.get_permanent_hw_address()) + profile.add_setting(s_wifi) + + s_ip4 = NM.SettingIP4Config.new() + s_ip4.set_property(NM.SETTING_IP_CONFIG_METHOD, "auto") + profile.add_setting(s_ip4) + + s_ip6 = NM.SettingIP6Config.new() + s_ip6.set_property(NM.SETTING_IP_CONFIG_METHOD, "auto") + profile.add_setting(s_ip6) + + if ap_sec != "--": + s_wifi_sec = NM.SettingWirelessSecurity.new() + if "WPA" in ap_sec: + if "WPA3" in ap_sec: + s_wifi_sec.set_property(NM.SETTING_WIRELESS_SECURITY_KEY_MGMT, + "sae") + else: + s_wifi_sec.set_property(NM.SETTING_WIRELESS_SECURITY_KEY_MGMT, + "wpa-psk") + s_wifi_sec.set_property(NM.SETTING_WIRELESS_SECURITY_AUTH_ALG, + "open") + s_wifi_sec.set_property(NM.SETTING_WIRELESS_SECURITY_PSK, password) + elif "WEP" in ap_sec: + s_wifi_sec.set_property(NM.SETTING_WIRELESS_SECURITY_KEY_MGMT, + "None") + s_wifi_sec.set_property(NM.SETTING_WIRELESS_SECURITY_WEP_KEY_TYPE, + NM.WepKeyType.PASSPHRASE) + s_wifi_sec.set_wep_key(0, password) + profile.add_setting(s_wifi_sec) + + return profile + + +def verify_conn(client, result, data): + """Callback function for add_and_activate_connection_async + + Check if connection completes successfully. Delete the connection if there + is an error. + + """ + try: + act_conn = client.add_and_activate_connection_finish(result) + conn = act_conn.get_connection() + if not all([conn.verify(), + conn.verify_secrets(), + data.verify(), + data.verify_secrets()]): + raise GLib.Error + notify(f"Added {conn.get_id()}") + except GLib.Error: + try: + notify(f"Connection to {conn.get_id()} failed", + urgency="critical") + conn.delete_async(None, None, None) + except UnboundLocalError: + pass + finally: + LOOP.quit() + + +def create_ap_list(adapter, active_connections): + """Generate list of access points. Remove duplicate APs , keeping strongest + ones and the active AP + + Args: adapter + active_connections - list of all active connections + Returns: aps - list of access points + active_ap - active AP + active_ap_con - active Connection + adapter + + """ + aps = [] + ap_names = [] + active_ap = adapter.get_active_access_point() + aps_all = sorted(adapter.get_access_points(), + key=lambda a: a.get_strength(), reverse=True) + conns_cur = [i for i in CONNS if + i.get_setting_wireless() is not None and + conn_matches_adapter(i, adapter)] + try: + ap_conns = active_ap.filter_connections(conns_cur) + active_ap_name = ssid_to_utf8(active_ap) + active_ap_con = [active_conn for active_conn in active_connections + if active_conn.get_connection() in ap_conns] + except AttributeError: + active_ap_name = None + active_ap_con = [] + if len(active_ap_con) > 1: + raise ValueError("Multiple connection profiles match" + " the wireless AP") + active_ap_con = active_ap_con[0] if active_ap_con else None + for nm_ap in aps_all: + ap_name = ssid_to_utf8(nm_ap) + if nm_ap != active_ap and ap_name == active_ap_name: + # Skip adding AP if it's not active but same name as active AP + continue + if ap_name not in ap_names: + ap_names.append(ap_name) + aps.append(nm_ap) + return aps, active_ap, active_ap_con, adapter + + +def notify(message, details=None, urgency="low"): + """Use notify-send if available for notifications + + """ + delay = CONF.getint('nmdm', 'rescan_delay', fallback=5) + args = ["-u", urgency, "-a", "networkmanager-dmenu", + "-t", str(delay * 1000), message] + if details is not None: + args.append(details) + if is_installed("notify-send"): + subprocess.run(["notify-send"] + args, check=False) + + +def run(): # pylint: disable=too-many-locals + """Main script entrypoint""" + active = CLIENT.get_active_connections() + adapter = choose_adapter(CLIENT) + if adapter: + ap_actions = create_ap_actions(*create_ap_list(adapter, active)) + else: + ap_actions = [] + + vpns = [i for i in CONNS if i.is_type(NM.SETTING_VPN_SETTING_NAME)] + try: + wgs = [i for i in CONNS if i.is_type(NM.SETTING_WIREGUARD_SETTING_NAME)] + except AttributeError: + # Workaround for older versions of python-gobject with no wireguard support + wgs = [] + eths = [i for i in CONNS if i.is_type(NM.SETTING_WIRED_SETTING_NAME)] + blues = [i for i in CONNS if i.is_type(NM.SETTING_BLUETOOTH_SETTING_NAME)] + + vpn_actions = create_vpn_actions(vpns, active) + wg_actions = create_wireguard_actions(wgs, active) + eth_actions = create_eth_actions(eths, active) + blue_actions = create_blue_actions(blues, active) + other_actions = create_other_actions(CLIENT) + wwan_installed = is_installed("ModemManager") + if wwan_installed: + gsms = [i for i in CONNS if i.is_type(NM.SETTING_GSM_SETTING_NAME)] + gsm_actions = create_gsm_actions(gsms, active) + wwan_actions = create_wwan_actions(CLIENT) + else: + gsm_actions = [] + wwan_actions = [] + + list_saved = CONF.getboolean('dmenu', 'list_saved', fallback=False) + saved_cons = [i for i in CONNS if i not in vpns + wgs + eths + blues] + if list_saved: + saved_actions = create_saved_actions(saved_cons) + else: + saved_actions = [Action("Saved connections", prompt_saved, [saved_cons])] + + actions = combine_actions(eth_actions, ap_actions, vpn_actions, wg_actions, + gsm_actions, blue_actions, wwan_actions, + other_actions, saved_actions) + sel = get_selection(actions) + sel() + + +def main(): + """Main. Enables script to be re-run after a wifi rescan + + """ + global CLIENT, CONNS, LOOP # noqa pylint: disable=global-variable-undefined + CLIENT = NM.Client.new(None) + LOOP = GLib.MainLoop() + CONNS = CLIENT.get_connections() + + run() + + +if __name__ == '__main__': + main() + +# vim: set et ts=4 sw=4 : diff --git a/.local/bin/opout b/.local/bin/opout new file mode 100755 index 0000000..24d75de --- /dev/null +++ b/.local/bin/opout @@ -0,0 +1,13 @@ +#!/bin/sh + +# opout: "open output": A general handler for opening a file's intended output, +# usually the pdf of a compiled document. I find this useful especially +# running from vim. + +basename="${1%.*}" + +case "${*}" in + *.tex|*.sil|*.m[dse]|*.[rR]md|*.mom|*.[0-9]) target="$(getcomproot "$1" || echo "$1")" ; setsid -f xdg-open "${target%.*}".pdf >/dev/null 2>&1 ;; + *.html) setsid -f "$BROWSER" "$basename".html >/dev/null 2>&1 ;; + *.sent) setsid -f sent "$1" >/dev/null 2>&1 ;; +esac diff --git a/.local/bin/rotdir b/.local/bin/rotdir new file mode 100755 index 0000000..86da6db --- /dev/null +++ b/.local/bin/rotdir @@ -0,0 +1,12 @@ +#!/bin/sh + +# When I open an image from the file manager in sxiv (the image viewer), I want +# to be able to press the next/previous keys to key through the rest of the +# images in the same directory. This script "rotates" the content of a +# directory based on the first chosen file, so that if I open the 15th image, +# if I press next, it will go to the 16th etc. Autistic, I know, but this is +# one of the reasons that sxiv is great for being able to read standard input. + +[ -z "$1" ] && echo "usage: rotdir regex 2>&1" && exit 1 +base="$(basename "$1")" +ls "$PWD" | awk -v BASE="$base" 'BEGIN { lines = ""; m = 0; } { if ($0 == BASE) { m = 1; } } { if (!m) { if (lines) { lines = lines"\n"; } lines = lines""$0; } else { print $0; } } END { print lines; }' diff --git a/.local/bin/rssadd b/.local/bin/rssadd new file mode 100755 index 0000000..910fca3 --- /dev/null +++ b/.local/bin/rssadd @@ -0,0 +1,18 @@ +#!/bin/sh + +if echo "$1" | grep -q "https*://\S\+\.[A-Za-z]\+\S*" ; then + url="$1" +else + url="$(grep -Eom1 '<[^>]+(rel="self"|application/[a-z]+\+xml)[^>]+>' "$1" | + grep -o "https?://[^\" ]")" + + echo "$url" | grep -q "https*://\S\+\.[A-Za-z]\+\S*" || + notify-send "That doesn't look like a full URL." && exit 1 +fi + +RSSFILE="${XDG_CONFIG_HOME:-$HOME/.config}/newsboat/urls" +if awk '{print $1}' "$RSSFILE" | grep "^$url$" >/dev/null; then + notify-send "You already have this RSS feed." +else + echo "$url" >> "$RSSFILE" && notify-send "RSS feed added." +fi diff --git a/.local/bin/screenlayout b/.local/bin/screenlayout new file mode 100755 index 0000000..ebd6760 --- /dev/null +++ b/.local/bin/screenlayout @@ -0,0 +1,16 @@ +#!/bin/bash + +layouts=($(ls "$HOME/.screenlayout/")) + +add="add layout..." + +layouts+=("$add") + +choice=$( printf '%s\n' "${layouts[@]}" | dmenu -i -p 'Choose a screenlayout:') "$@" || exit + +if [ "$choice" = "$add" ] +then + arandr & +else + exec "$HOME/.screenlayout/$choice" +fi diff --git a/.local/bin/screenshot b/.local/bin/screenshot new file mode 100755 index 0000000..7f2a66b --- /dev/null +++ b/.local/bin/screenshot @@ -0,0 +1,13 @@ +#!/bin/sh + +SCRIPTNAME=$(basename $0) +FILENAME="$HOME/$SCRIPTNAME-$(date +'%Y-%m-%d-%H-%M-%S').png" + +case ${1:-} in + select*|region|area) SEL="-s" ;; + *) SEL="" ;; +esac + +maim --format=png $SEL "$FILENAME" +echo -n $FILENAME | xclip -selection clipbard +notify-send "Screenshot" "$(echo -e "Screen shot saved\n$FILENAME")" diff --git a/.local/bin/set-default-sink b/.local/bin/set-default-sink new file mode 100755 index 0000000..212bae9 --- /dev/null +++ b/.local/bin/set-default-sink @@ -0,0 +1,13 @@ +#!/bin/bash + +choice=$(pactl list short sinks | awk '{print $2}' | dmenu -i -p "which sink should be default?") + +if [ -z "$choice" ] ; then + exit 0; +fi + + +rm /tmp/pactl-default-sink --force # to ignore if it isn't there +touch /tmp/pactl-default-sink +echo "$choice" > /tmp/pactl-default-sink +pactl set-default-sink $choice # make sure that the chosen sink is the \ No newline at end of file diff --git a/.local/bin/set-default-source b/.local/bin/set-default-source new file mode 100755 index 0000000..a5f6f62 --- /dev/null +++ b/.local/bin/set-default-source @@ -0,0 +1,13 @@ +#!/bin/bash + +choice=$(pactl list short sources | awk '{print $2}' | dmenu -i -p "which source should be default?") + +if [ -z "$choice" ] ; then + exit 0; +fi + + +rm /tmp/pactl-default-source --force # to ignore if it isn't there +touch /tmp/pactl-default-source +echo "$choice" > /tmp/pactl-default-source +pactl set-default-source $choice # make sure that the chosen sink is the \ No newline at end of file diff --git a/.local/bin/setvol b/.local/bin/setvol new file mode 100755 index 0000000..2b6a000 --- /dev/null +++ b/.local/bin/setvol @@ -0,0 +1,13 @@ +#!/bin/bash + +default_loc=/tmp/pactl-default-sink + +if [ ! -f "$default_loc" ] ; then + set-default-sink +fi + +default=$(cat $default_loc) + +value=$(echo -e "" | dmenu -bw 0 -i -p "Set vol to what?") + +pactl set-sink-volume $default $value% diff --git a/.local/bin/status-bat b/.local/bin/status-bat new file mode 100755 index 0000000..73b3b8f --- /dev/null +++ b/.local/bin/status-bat @@ -0,0 +1,44 @@ +#!/bin/bash + +icon="" + +status=$(bash-status-bat) +parts=($status) +current=$(echo "${parts[0]}" | sed 's/\%//') +state=${parts[1]} +status="$current%" + +if [[ "$state" == "discharging" ]]; then + case $(((current/20)+1)) in + 1) + icon=" " + ;; + 2) + icon=" " + ;; + 3) + icon=" " + ;; + 4) + icon=" " + ;; + 5) + icon=" " + ;; + esac +fi + +if [[ "$state" == "not-charging" ]]; then + icon=" " +fi + +if [[ "$state" == "fully-charged" ]]; then + icon=" " + status="Full" +fi + +if [[ "$state" == "charging" ]]; then + icon=" " +fi + +echo "$icon $status" diff --git a/.local/bin/status-disk b/.local/bin/status-disk new file mode 100755 index 0000000..8abd32a --- /dev/null +++ b/.local/bin/status-disk @@ -0,0 +1,10 @@ +#!/bin/bash + +freemb=$(df -h -B 1048576 | grep "/$" | awk -F ' ' '{ print $4 }') +freegb=$(df -h -B 1048576 | grep "/$" | awk -F ' ' '{ print $4/1024 }') + +if [ $freemb -lt 1024 ]; then + printf " %0.2fMb" $freemb +else + printf " %0.2fGb" $freegb +fi diff --git a/.local/bin/status-net b/.local/bin/status-net new file mode 100755 index 0000000..abdcc7d --- /dev/null +++ b/.local/bin/status-net @@ -0,0 +1,61 @@ +#!/bin/sh + +dev_wifi=$(cat "$HOME"/.config/net-cfg/dev_wifi) +dev_eth=$(cat "$HOME"/.config/net-cfg/dev_eth) +dev_vpn=$(cat "$HOME"/.config/net-cfg/dev_vpn) + +base03=#002b36 +base02=#073642 +base01=#586e75 +base00=#657b83 +base0=#839496 +base1=#93a1a1 +base2=#eee8d5 +base3=#fdf6e3 +yellow=#b58900 +orange=#cb4b16 +red=#dc322f +magenta=#d33682 +violet=#6c71c4 +blue=#268bd2 +cyan=#2aa198 +green=#859900 + +std_color=$magenta +wifi_icon=" " + +color=$std_color; + +eth="$(ip -o address | grep -i "$dev_eth *inet ")" +if [ -n "$eth" ] +then + speed="$(cat /sys/class/net/$dev_eth/speed)" + case $speed in + 10) speed="10Base-T" ;; + 100) speed="100Base-T" ;; + 1000) speed="Gigabit" ;; + *) speed="UNKNOWN $speed" ;; + esac + eth_status="  $speed" +fi + +ssid="$(iw dev $dev_wifi link | grep -i SSID)" +if [ -n "$ssid" ] +then + signal="$(awk '/^\s*w/ { print int($3 * 100 / 70) "%" }' /proc/net/wireless)" + wifi_status="$wifi_icon ${signal} ${ssid##*SSID: }" +fi + +vpn="$(ip -o address | grep -i "$dev_vpn *inet ")" +if [ -n "$vpn" ] +then + vpn_status="" + color=$green +else + vpn_status="" +fi + +echo "${vpn_status} $wifi_status$eth_status" + + +# vim: ft=sh:expandtab:ts=4:shiftwidth=4 diff --git a/.local/bin/status-vol b/.local/bin/status-vol new file mode 100755 index 0000000..c972067 --- /dev/null +++ b/.local/bin/status-vol @@ -0,0 +1,27 @@ +#!/bin/sh + +# Prints the current volume or  if muted. + +vol="$(wpctl get-volume @DEFAULT_AUDIO_SINK@)" + +# If muted, print 🔇 and exit. +[ "$vol" != "${vol%\[MUTED\]}" ] && echo  && exit + +vol="${vol#Volume: }" +split() { + # For ommiting the . without calling and external program. + IFS=$2 + set -- $1 + printf '%s' "$@" +} +vol="$(split "$vol" ".")" +vol="${vol##0}" + +case 1 in + $((vol >= 70)) ) icon="" ;; + $((vol >= 30)) ) icon="" ;; + $((vol >= 1)) ) icon="" ;; + * ) echo  && exit ;; +esac + +echo "$icon $vol%" diff --git a/.local/bin/switchkb b/.local/bin/switchkb new file mode 100755 index 0000000..f01fef5 --- /dev/null +++ b/.local/bin/switchkb @@ -0,0 +1,7 @@ +#!/bin/bash + +## +# Switches to the given keyboard (argument) and sets +## + +setxkbmap "$1" -option ctrl:nocaps diff --git a/.local/bin/tmux-sessioniser b/.local/bin/tmux-sessioniser new file mode 100755 index 0000000..232d5e2 --- /dev/null +++ b/.local/bin/tmux-sessioniser @@ -0,0 +1,28 @@ +#!/bin/bash + +# Simple tool that can be run to create a tmux session or +# connect to an existing one. + +if [[ $# -eq 1 ]]; then + selected=$1 +else + selected=$(find ~/repos ~/.config ~/ ~/work ~/.xmonad ~/nextcloud -mindepth 1 -maxdepth 1 -type d | fzf) +fi + +if [[ -z $selected ]]; then + exit 0 +fi + +selected_name=$(basename "$selected" | tr . _) +tmux_running=$(pgrep tmux) + +if [[ -z $TMUX ]] && [[ -z $tmux_running ]]; then + tmux new-session -s "$selected_name" -c "$selected" + exit 0 +fi + +if ! tmux has-session -t="$selected_name" 2> /dev/null; then + tmux new-session -ds "$selected_name" -c "$selected" +fi + +tmux switch-client -t "$selected_name" diff --git a/.local/bin/tmux-windowiser b/.local/bin/tmux-windowiser new file mode 100755 index 0000000..4a80fa1 --- /dev/null +++ b/.local/bin/tmux-windowiser @@ -0,0 +1,13 @@ +#!/bin/bash + +branch_name=$(basename $1) +session_name=$(tmux display-message -p "#S") +clean_name=$(echo $branch_name | tr "./" "__") +target="$session_name:$clean_name" + +if ! tmux has-session -t $target 2> /dev/null; then + tmux neww -dn $clean_name +fi + +shift +tmux send-keys -t $target "$*" Enter diff --git a/.local/bin/tmux-worker b/.local/bin/tmux-worker new file mode 100755 index 0000000..b755dd0 --- /dev/null +++ b/.local/bin/tmux-worker @@ -0,0 +1,15 @@ +#!/bin/bash + +session="system" + +# Check if the session exists, discarding output +# We can check $? for the exit status (zero for success, non-zero for failure) +tmux has-session -t $session 2>/dev/null + +if [ $? != 0 ]; then + # Set up your session + tmux new-session -s "$session" +fi + +# Attach to created session +tmux attach-session -t "$session" diff --git a/.local/bin/toggle-sink-mute b/.local/bin/toggle-sink-mute new file mode 100755 index 0000000..6103bb5 --- /dev/null +++ b/.local/bin/toggle-sink-mute @@ -0,0 +1,11 @@ +#!/bin/bash + +default_loc=/tmp/pactl-default-sink + +if [ ! -f "$default_loc" ] ; then + set-default-sink +fi + +default=$(cat $default_loc) + +pactl set-sink-mute $default toggle diff --git a/.local/bin/toggle-source-mute b/.local/bin/toggle-source-mute new file mode 100755 index 0000000..a1e8549 --- /dev/null +++ b/.local/bin/toggle-source-mute @@ -0,0 +1,11 @@ +#!/bin/bash + +default_loc=/tmp/pactl-default-source + +if [ ! -f "$default_loc" ] ; then + set-default-source +fi + +default=$(cat $default_loc) + +pactl set-source-mute $default toggle diff --git a/.local/bin/trayer-toggle b/.local/bin/trayer-toggle new file mode 100755 index 0000000..9c0e9ee --- /dev/null +++ b/.local/bin/trayer-toggle @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +test_path="/tmp/trayer.exists" + +if [ -f "$test_path" ]; then + echo "stopping trayer" + killall trayer + rm "$test_path" +else + echo "starting trayer" + touch "$test_path" + exec "$@" +fi diff --git a/.local/bin/urlencode b/.local/bin/urlencode new file mode 100755 index 0000000..ad9089c --- /dev/null +++ b/.local/bin/urlencode @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +# yeah, i totally stole this from stack exchange, no shame +# - rwxrob + +# and I stole it from rwxrob +# - inkletblot + +rawurlencode() { + local string="${1}" + local strlen=${#string} + local encoded="" + local pos c o + + for ((pos = 0; pos < strlen; pos++)); do + c=${string:$pos:1} + case "$c" in + [-_.~a-zA-Z0-9]) o="${c}" ;; + *) printf -v o '%%%02x' "'$c'" ;; + esac + encoded+="${o}" + done + echo "${encoded}" # You can either set a return variable (FASTER) + REPLY="${encoded}" #+or echo the result (EASIER)... or both... :p +} + +if test -n "$1"; then + rawurlencode "$*" + exit +fi + +IFS= +while read -r line; do + rawurlencode "$line" +done diff --git a/.local/bin/vic b/.local/bin/vic new file mode 100755 index 0000000..f5b24d5 --- /dev/null +++ b/.local/bin/vic @@ -0,0 +1,4 @@ +#!/bin/sh + +cmd=$(command -v $1) +test -n "$cmd" && exec vi "$cmd" diff --git a/.local/bin/xmobar-status-bat b/.local/bin/xmobar-status-bat new file mode 100755 index 0000000..54ca4a8 --- /dev/null +++ b/.local/bin/xmobar-status-bat @@ -0,0 +1,9 @@ +#!/bin/bash + +# a simple wrapper for inserting xmobar stuff + +status=$(status-bat) + +parts=($status) + +echo "${parts[0]} ${parts[1]}" diff --git a/.local/bin/xmobar-status-disk b/.local/bin/xmobar-status-disk new file mode 100755 index 0000000..183c771 --- /dev/null +++ b/.local/bin/xmobar-status-disk @@ -0,0 +1,7 @@ +#!/bin/bash + +status=$(status-disk) + +parts=($status) + +echo "${parts[0]} ${parts[1]}" diff --git a/.local/bin/xmobar-status-keyboard b/.local/bin/xmobar-status-keyboard new file mode 100755 index 0000000..c02f52c --- /dev/null +++ b/.local/bin/xmobar-status-keyboard @@ -0,0 +1,32 @@ +#!/bin/sh + +stdlayout=us # standard layout takes "default" color +stdname=en-us # arbitrary, descriptive only + +base03=#002b36 +base02=#073642 +base01=#586e75 +base00=#657b83 +base0=#839496 +base1=#93a1a1 +base2=#eee8d5 +base3=#fdf6e3 +yellow=#b58900 +orange=#cb4b16 +red=#dc322f +magenta=#d33682 +violet=#6c71c4 +blue=#268bd2 +cyan=#2aa198 +green=#859900 + +layout="$(xkb-switch)" + +case $layout in + ${stdlayout}) color=$green; icon=" "; name=$stdname ;; # f11c fa-keyboard-o + *) color=$magenta; icon=""; name="dvorak" ;; # f11c fa-keyboard-o +esac + +echo "$icon ${name}" + +# vim: ft=sh:expandtab:ts=4:shiftwidth=4 diff --git a/.local/bin/xmobar-status-net b/.local/bin/xmobar-status-net new file mode 100755 index 0000000..3e07dbe --- /dev/null +++ b/.local/bin/xmobar-status-net @@ -0,0 +1,87 @@ +#!/bin/sh + +dev_wifi=$(cat "$HOME"/.config/xmobar/dev_wifi) +dev_eth=$(cat "$HOME"/.config/xmobar/dev_eth) +dev_vpn=$(cat "$HOME"/.config/xmobar/dev_vpn) + +# base03=#002b36 +# base02=#073642 +# base01=#586e75 +# base00=#657b83 +# base0=#839496 +# base1=#93a1a1 +# base2=#eee8d5 +# base3=#fdf6e3 +yellow=#b58900 +# orange=#cb4b16 +red=#dc322f +magenta=#d33682 +# violet=#6c71c4 +# blue=#268bd2 +# cyan=#2aa198 +green=#859900 + +# connectivity status +# states are: +# none (no connectivity) +# portal (behind captive portal) +# limited (connected to network but no internet access) +# full (full internet connectivity) +# unknown + +std_color=$magenta +wifi_icon="" + +connectivity="$(nmcli networking connectivity)" + +case $connectivity in + none) color=$red; icon="" ;; # f056 fa-minus-circle + portal) color=$yellow; icon="" ;; # f05c fa-times-circle-o + limited) color=$yellow; icon="" ;; # f01b fa-arrow-circle-o-up + full) color=$std_color; icon="" ;; # f0aa fa-arrow-circle-up + *) color=$red; icon="" ;; # f29c fa-question-circle-o +esac + +eth="$(ip -o address | grep -i "$dev_eth *inet ")" +if [ -n "$eth" ] +then + #eth="${eth##*inet }" + speed="$(cat /sys/class/net/"$dev_eth"/speed)" + case $speed in + 10) speed="10Base-T" ;; + 100) speed="100Base-T" ;; + 1000) speed="Gigabit" ;; + *) speed="UNKNOWN $speed" ;; + esac + #eth_status="  $speed ${eth%%/*}" + eth_status="  $speed" +fi + +ssid="$(iw dev "$dev_wifi" link | grep -i SSID)" +if [ -n "$ssid" ] +then + signal="$(iw dev "$dev_wifi" station dump | grep -E '[^ ]signal avg')" + signal="${signal#*-}" + signal="${signal%% *}" + signal="$((2*(100-signal)))" + + signal=$((signal/5*5)) # get rid of some jitter + ((signal > 100)) && signal=100 + wifi_status=" $wifi_icon ${signal}% ${ssid##*SSID: }" +fi + +vpn="$(ip -o address | grep -i "$dev_vpn *inet ")" +if [ -n "$vpn" ] +then + #vpn="${vpn##*inet }" + #vpn_status=" ${vpn%%/*}" + vpn_status=" " + color=$green +else + vpn_status=" " +fi + +echo "${vpn_status}$icon ${connectivity##*full}$wifi_status$eth_status" + + +# vim: ft=sh:expandtab:ts=4:shiftwidth=4 diff --git a/.local/bin/xmobar-status-updates b/.local/bin/xmobar-status-updates new file mode 100755 index 0000000..9a57d19 --- /dev/null +++ b/.local/bin/xmobar-status-updates @@ -0,0 +1,6 @@ +#!/bin/sh + +count=$(yay -Qu | wc -l) +icon="" + +echo "$icon $count" diff --git a/.local/bin/xmobar-status-vol b/.local/bin/xmobar-status-vol new file mode 100755 index 0000000..18ff913 --- /dev/null +++ b/.local/bin/xmobar-status-vol @@ -0,0 +1,46 @@ +#!/bin/sh + +base03=#002b36 +base02=#073642 +base01=#586e75 +base00=#657b83 +base0=#839496 +base1=#93a1a1 +base2=#eee8d5 +base3=#fdf6e3 +yellow=#b58900 +orange=#cb4b16 +red=#dc322f +magenta=#d33682 +violet=#6c71c4 +blue=#268bd2 +cyan=#2aa198 +green=#859900 + +color=$cyan +vol="$(pamixer --get-volume)" + +if [ $(pamixer --get-mute) = true ]; then + vol=MUTE + color=$red + icon="" # fa-volume-off f026 + + echo "$icon $vol" +fi + +if [ "$vol" -gt "100" ]; then + icon="" + color=$orange +elif [ "$vol" -gt "70" ]; then + icon="" +elif [ "$vol" -gt "30" ]; then + icon="" +elif [ "$vol" -gt "0" ]; then + icon="" +else + vol=MUTE + color=$red + icon="" +fi + +echo "$icon $vol" diff --git a/.local/bin/xmobar-status-weather b/.local/bin/xmobar-status-weather new file mode 100755 index 0000000..95e8dc5 --- /dev/null +++ b/.local/bin/xmobar-status-weather @@ -0,0 +1,36 @@ +#!/bin/sh + +# simple script to read weather from airport code and return it for display in xmobar + +weather=$(weather-report $1 -m --no-cache -n | \ + grep -e '\[' \ + -e 'Temp' | \ + sed -e 's/\[using result //' \ + -e 's/,.*\]//' \ + -e 's/Temp.*: //' \ + -e 's/ C//' \ + -e 's/ //') + +readarray -t y <<< "$weather" + +loc=${y[0]} +temp=${y[1]} +color="" + +if ((temp > 30)) +then + color="#ff5555" +elif ((temp > 25)) +then + color="#ffb86c" +elif ((temp > 20)) +then + color="#f1fa8c" +elif ((temp > 10)) +then + color="#50fa7b" +else + color="#8be9fd" +fi + +echo -e "$loc $tempC" diff --git a/.local/bin/xmobar-trayer-padding-icon b/.local/bin/xmobar-trayer-padding-icon new file mode 100755 index 0000000..adee04a --- /dev/null +++ b/.local/bin/xmobar-trayer-padding-icon @@ -0,0 +1,48 @@ +#!/bin/sh +# Copied from https://github.com/jaor/xmobar/issues/239#issuecomment-233206552 +# Detects the width of running trayer-srg window (xprop name 'panel') +# and creates an XPM icon of that width, 1px height, and transparent. +# Outputs an -tag for use in xmobar to display the generated +# XPM icon. +# +# Run script from xmobar: +# `Run Com "/where/ever/trayer-padding-icon.sh" [] "trayerpad" 10` +# and use `%trayerpad%` in your template. + + +# Function to create a transparent Wx1 px XPM icon +create_xpm_icon () { + timestamp=$(date) + pixels=$(for i in `seq $1`; do echo -n "."; done) + + cat << EOF > "$2" +/* XPM * +static char * trayer_pad_xpm[] = { +/* This XPM icon is used for padding in xmobar to */ +/* leave room for trayer-srg. It is dynamically */ +/* updated by by trayer-padding-icon.sh which is run */ +/* by xmobar. */ +/* Created: ${timestamp} */ +/* */ +"$1 1 1 1", +/* Colors (none: transparent) */ +". c none", +/* Pixels */ +"$pixels" +}; +EOF +} + +# Width of the trayer window +width=$(xprop -name panel | grep 'program specified minimum size' | cut -d ' ' -f 5) + +# Icon file name +iconfile="/tmp/trayer-padding-${width}px.xpm" + +# If the desired icon does not exist create it +if [ ! -f $iconfile ]; then + create_xpm_icon $width $iconfile +fi + +# Output the icon tag for xmobar +echo "" diff --git a/.local/bin/xmonad-keys-help b/.local/bin/xmonad-keys-help new file mode 100755 index 0000000..1916947 --- /dev/null +++ b/.local/bin/xmonad-keys-help @@ -0,0 +1,12 @@ +#!/bin/bash + +sed -n '/--START_KEYS/,/--END_KEYS/p' $HOME/.xmonad/xmonad.hs | \ + grep -e ', ("' \ + -e '\[ ("' \ + -e '--NOTE' | \ + grep -v '\-\- , ' |\ + sed -e 's/^[ \t,]*//' \ + -e 's/\[ (/(/' \ + -e 's/--NOTE /\n/' \ + -e 's/, / --> /' | \ + yad --text-info --back=#121e32 --fore=#dfdfef --geometry=1200x800 \ No newline at end of file diff --git a/.local/bin/zk-gen-dir-md-toc b/.local/bin/zk-gen-dir-md-toc new file mode 100755 index 0000000..f3bcd47 --- /dev/null +++ b/.local/bin/zk-gen-dir-md-toc @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# Generates a simple index of the current folder and subfolders creating +# a table of contents of all of the .md files contained within. + +long_contents=$(find . | sort) + +# replace the absolute path in all lines +contents=${long_contents//\.\//} + +prev="" + +echo "$contents" | while read -r line || [[ -n $line ]]; +do + # the line contains a file, or hidden dir + if [[ $line == *"."* ]]; then + # if the line is an md, not in the .zk dir, and not a dir + if [[ $line == *".md"* && ! -d "$line" && $line != *".zk"* ]]; then + # extract the name of the file + name=$(echo "$line" | sed 's/.*\///' | sed 's/\.md//') + # create the link + name="[$name]($line)" + else + continue + fi + else + # the folders are titles + name=$(echo -e "\n## $line") + fi + + # two titles in a row means empty folder. + if [[ $prev == *"##"* && $name == *"##"* ]]; then + prev=$name + name=$(echo -e "Folder Empty.\n$prev") + else + prev=$name + fi + + echo "$name" +done diff --git a/.local/bin/zk-gen-index b/.local/bin/zk-gen-index new file mode 100755 index 0000000..13ec007 --- /dev/null +++ b/.local/bin/zk-gen-index @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +toc=$(zk-gen-dir-md-toc) + +echo -e "START_TOC$toc\nEND_TOC" > temp.md + +if [ -f index.md ]; then + perl -i -p0e 's/START_TOC.*END_TOC/`cat temp.md`/se' index.md + # sed -i "s/START_TOC.*END_TOC/\${toc}/g" index.md +else + echo -e "# Index\n\nSTART_TOC\n$toc\nEND_TOC" > index.md +fi + +rm temp.md diff --git a/scripts/ganttproject b/scripts/ganttproject new file mode 100755 index 0000000..bbe16cc --- /dev/null +++ b/scripts/ganttproject @@ -0,0 +1,3 @@ +#!/bin/bash + +/opt/ganttproject/ganttproject diff --git a/scripts/update-dmenu-bluetooth b/scripts/update-dmenu-bluetooth new file mode 100755 index 0000000..f9bcafb --- /dev/null +++ b/scripts/update-dmenu-bluetooth @@ -0,0 +1,33 @@ +#!/bin/sh + +echo "This script will clone Layerex's dmenu-bluetooth and copy the scripts from it that I use." +read -r -p "Are you sure you want to continue [Y/n]" input + +case $input in + [nN][oO] | [nN]) + echo "Okay, exiting..." + exit 0 + ;; + *) + printf "\n" + ;; +esac + +git clone https://github.com/Layerex/dmenu-bluetooth + +printf "\nCopying scripts...\n" + +dir="dmenu-bluetooth" + +if [ -d "$dir" ]; then + for script in "$HOME"/.local/bin/*; do + if [ -f "./$dir/${script##*/}" ]; then + cp "./$dir/${script##*/}" "$HOME/.local/bin/" -v + fi + done + + printf "\nRemoving %s..." "$dir" + rm -rf "./$dir" && printf "\nUpdate Complete." +else + echo "directory $dir does not exist" +fi diff --git a/scripts/update-networkmanager-dmenu b/scripts/update-networkmanager-dmenu new file mode 100755 index 0000000..c1510f1 --- /dev/null +++ b/scripts/update-networkmanager-dmenu @@ -0,0 +1,33 @@ +#!/bin/bash + +echo "This script will clone firecat53's networkmanager-dmenu and copy the scripts from it that I use." +read -r -p "Are you sure you want to continue [Y/n]" input + +case $input in + [nN][oO] | [nN]) + echo "Okay, exiting..." + exit 0 + ;; + *) + printf "\n" + ;; +esac + +git clone https://github.com/firecat53/networkmanager-dmenu + +printf "\nCopying scripts...\n" + +dir="networkmanager-dmenu" + +if [ -d "$dir" ]; then + for script in "$HOME"/.local/bin/*; do + if [ -f "./$dir/${script##*/}" ]; then + cp "./$dir/${script##*/}" "$HOME/.local/bin/" -v + fi + done + + printf "\nRemoving %s..." "$dir" + rm -rf "./$dir" && printf "\nUpdate Complete." +else + echo "directory $dir does not exist" +fi diff --git a/scripts/update-voidrice b/scripts/update-voidrice new file mode 100755 index 0000000..00a4d5a --- /dev/null +++ b/scripts/update-voidrice @@ -0,0 +1,40 @@ +#!/bin/bash + +# I use some of Luke Smiths scripts and although they don't often update, they are known to. +# The output is going to include a bunch of copy errors as I'm just copying each file in this directory from his .local/bin back to here, obviously my custom scripts won't exist so they'll fail. + +echo "This script will clone Luke Smith's voidrice and copy the scripts from it that I use." +read -r -p "Are you sure you want to continue [Y/n]" input + +case $input in + [nN][oO] | [nN]) + echo "Okay, exiting..." + exit 0 + ;; + *) + printf "\n" + ;; +esac + +git clone https://github.com/LukeSmithxyz/voidrice + +printf "\nCopying scripts...\n" + +if [ -d voidrice ]; then + for script in "$HOME"/.local/bin/*; do + if [ -f "./voidrice/.local/bin/${script##*/}" ]; then + cp "./voidrice/.local/bin/${script##*/}" "$HOME/.local/bin/" -v + fi + done + + for cron in "$HOME"/.local/bin/cron/*; do + if [ -f "./voidrice/.local/bin/cron/${cron##*/}" ]; then + cp "./voidrice/.local/bin/cron/${cron##*/}" "$HOME/.local/bin/cron/" -v + fi + done + + printf "\nRemoving voidrice..." + rm -rf ./voidrice && printf "\nUpdate Complete." +else + echo "directory voidrice does not exist" +fi diff --git a/scripts/zotero b/scripts/zotero new file mode 100755 index 0000000..7f1edc7 --- /dev/null +++ b/scripts/zotero @@ -0,0 +1,3 @@ +#!/bin/sh + +/opt/zotero/zotero