diff options
Diffstat (limited to 'bin')
| -rwxr-xr-x | bin/2av1 | 84 | ||||
| -rwxr-xr-x | bin/audm | 84 | ||||
| -rwxr-xr-x | bin/blum | 100 | ||||
| -rwxr-xr-x | bin/dots | 28 | ||||
| -rwxr-xr-x | bin/dskm | 26 | ||||
| -rwxr-xr-x | bin/genup | 91 | ||||
| -rwxr-xr-x | bin/hr | 22 | ||||
| -rw-r--r-- | bin/libfuzl.sh | 114 | ||||
| -rwxr-xr-x | bin/mailsync | 148 | ||||
| -rwxr-xr-x | bin/mktoes | 27 | ||||
| l--------- | bin/msmtp-queue | 1 | ||||
| -rwxr-xr-x | bin/msmtp-queue-stat | 17 | ||||
| l--------- | bin/msmtpq | 1 | ||||
| -rwxr-xr-x | bin/netm | 1560 | ||||
| -rwxr-xr-x | bin/pasm | 71 | ||||
| -rwxr-xr-x | bin/play-mpv | 112 | ||||
| -rwxr-xr-x | bin/play-music | 23 | ||||
| -rwxr-xr-x | bin/rsyncez | 19 | ||||
| -rwxr-xr-x | bin/susm | 30 | ||||
| -rwxr-xr-x | bin/svcm | 108 | ||||
| -rwxr-xr-x | bin/todo | 216 | ||||
| -rwxr-xr-x | bin/urlopen | 89 | ||||
| -rwxr-xr-x | bin/vpnm | 124 |
23 files changed, 3095 insertions, 0 deletions
diff --git a/bin/2av1 b/bin/2av1 new file mode 100755 index 0000000..19ace0b --- /dev/null +++ b/bin/2av1 @@ -0,0 +1,84 @@ +#!/bin/bash +set -eu +[ -n "${DEBUG:-}" ] && set -x +[ -z "${DEBUG:-}" ] && export SVT_LOG=1 + +BITRATE=${BITRATE:-128000} +EXT=${EXT:-webm} + +case "${VCODEC:-av1}" in +vp9) vcodec_args=(-c:v libvpx-vp9 -crf "${CRF:-31}" -b:v 0) ;; +av1) vcodec_args=(-c:v libsvtav1 -crf "${CRF:-30}" -svtav1-params "tune=0:fast-decode=1") ;; +esac +for f in "$@"; do + f=$(readlink -e "$f") + op=${OUTDIR:-${f%/*}} + on=${f##*/} + on=${on%.*}.${EXT} + o="${op}/${on}" + [ "$f" = "$o" ] && o="${op}/${on%.*}.new.${EXT}" + + hr + + printf '๐ท๏ธ %s\n' "$on" + + # duration + duration=$(ffprobe -v error -of csv=p=0 -show_entries format=duration -sexagesimal "$f") + + # bitrate + br=$(ffprobe -v error -of default=noprint_wrappers=1:nokey=1 \ + -select_streams a \ + -show_entries stream=bit_rate \ + "${f}") + [ -z "$br" ] && br=$BITRATE + [ $br = "N/A" ] && br=$((BITRATE + 1)) + [ $br -gt $BITRATE ] && br="$BITRATE" + + printf '๐งพ duration=%s bitrate=%d\n' \ + "$duration" \ + "$br" + + startt=$(date +%s) + + # transcode + ffmpeg -hide_banner -loglevel warning -stats \ + -i "$f" \ + ${OPTS:-} \ + -c:a libopus \ + -b:a "$br" \ + ${vcodec_args[@]} \ + -map 0 \ + -map_metadata 0 \ + "${o}" + + set +x + + # # progress + # total_frames=$(ffprobe -v error -select_streams v:0 -show_entries stream=nb_frames -of csv=p=0 -i "${1:?}") + # while [ -e "/proc/$ffmpeg_pid" ]; do + # sleep 2 + # [ -e "$vstats_file" ] || continue + # current_frames=$(tail -1 "$vstats_file" | grep -o 'frame=[ 0-9]*' | sed 's,frame=[ ]*,,') + # [ -z "$current_frames" ] && continue + # current_percent=$((100 * current_frames / total_frames)) + # printf '==> %d/%d %d%% ]]\n\n' "$current_frames" "$total_frames" "$current_percent" + # done + + # duration + endt=$(date +%s) + printf '๐ took=' + date -u -d@"$((endt - startt))" +%H:%M:%S + + # results + du -h "$f" "$o" + in_size=$(stat -c '%s' "$f") + out_size=$(stat -c '%s' "$o") + awk -v fs="$in_size" -v os="$out_size" \ + 'BEGIN{p=((100/fs) * (fs-os)); printf("โป๏ธ %.2f%% reduction\n", p);}' + + if [ $in_size -gt $out_size ]; then + trash-put -v "${f}" + else + trash-put -v "${o}" + fi || true +done diff --git a/bin/audm b/bin/audm new file mode 100755 index 0000000..58a5a91 --- /dev/null +++ b/bin/audm @@ -0,0 +1,84 @@ +#!/bin/bash +set -eu + +# shellcheck disable=2034 +description="audio/sound manager" + +export FUZL_PROMPT=audm +export FUZL_ICON=๓ฐฅ +. libfuzl.sh + +main() { + local sink source + + sink=$(inspect_node '@DEFAULT_SINK@' | awk -F\\t '{print $2}') + fuzl_action_add 'change_sink' "๓ฐ" "$sink" + + source=$(inspect_node '@DEFAULT_SOURCE@' | awk -F\\t '{print $2}') + fuzl_action_add 'change_source' "๓ฐฐ" "$source" + + fuzl_action_add 'change_profile' "๓ฐก" "Change profile" + + fuzl_action_menu || exit 1 +} + +# +# helpers +# + +inspect_node() { + local nid + nid=$(wpctl inspect "${1:?}" | awk '/^id/{match($0,/[0-9]+,/); print substr($0,RSTART,RLENGTH-1);}') + + test -n "$nid" || exit 1 # TODO handle error + pw-dump | jq -r --argjson nid "$nid" \ + '.[]|select(.id==$nid)|.info.props|[.["object.id"],.["node.description"],.["node.name"]]|join("\t")' +} + +list_nodes() { + pw-dump | jq -rc --arg class "${1:?}" \ + '.[]|select((.info.props["media.class"]//"")|test($class))|.info.props|[.["object.id"],.["node.description"],.["node.name"]]|join("\t")' +} + +# +# actions +# + +audm_change_sink() { + local id + id=$(list_nodes 'Audio/Sink' | + FUZL_PROMPT='sink' fuzl_menu $FUZL_MENU_FILTER --with-nth='{2} [{3}]' --accept-nth=1) + + test -n "$id" || main + fuzl_try -- wpctl set-default "$id" +} + +audm_change_source() { + local id + id=$(list_nodes 'Audio/Source' | + FUZL_PROMPT='source' fuzl_menu $FUZL_MENU_FILTER --with-nth='{2} [{3}]' --accept-nth=1) + + test -n "$id" || main + fuzl_try -- wpctl set-default "$id" +} + +audm_change_profile() { + local device did dname profile + + device=$(pw-dump | jq -rc --arg class "Audio/Device" \ + '.[]|select((.info.props["media.class"]//"")|test($class))|.info.props|[.["object.id"],.["device.description"],.["device.nick"]]|join("\t")' | + FUZL_PROMPT='device' fuzl_menu $FUZL_MENU_FILTER --with-nth='{2} {3}' --accept-nth='{1}\t{2}') + test -n "$device" || main + + did=$(printf %s "$device" | awk -F\\t '{print $1}') + dname=$(printf %s "$device" | awk -F\\t '{print $2}') + + profile=$(pw-dump | jq -r --argjson did "$did" \ + '.[]|select(.id==$did)|.info.params.EnumProfile[]|[.index,.description]|join("\t")' | + FUZL_PROMPT="profile ($dname)" fuzl_menu $FUZL_MENU_FILTER --with-nth=2 --accept-nth=1) + test -n "$profile" || audm_change_profile + + fuzl_try -- wpctl set-profile "$did" "$profile" +} + +main diff --git a/bin/blum b/bin/blum new file mode 100755 index 0000000..19c2aef --- /dev/null +++ b/bin/blum @@ -0,0 +1,100 @@ +#!/bin/bash +set -eu + +description="bluetooth manager" + +export FUZL_PROMPT=blum +export FUZL_ICON=๏ +. libfuzl.sh + +main() { + local connected + + if ! bt_enabled; then + fuzl_action_add "enable" "๓ฐท" "Enable Bluetooth" + else + connected=$(bt_devices Connected | bt_devices_getname) + if [ -n "$connected" ]; then + fuzl_action_add "disconnect" "๓ฐฑ" "Connected to $connected" + fi + fuzl_action_add "devices" "๓ฐพฐ" "Available devices" + fuzl_action_add "scan" "๓ฑ" "Scan" + fuzl_action_add "repl" "๎" "Run REPL" + fuzl_action_add "disable" "๓ฐท" "Disable Bluetooth" + fuzl_action_add "tether_amagiri" "๓ฐ" "Tether amagiri" + fi + + fuzl_action_menu || exit 1 +} + +bt_enabled() { + pgrep -cx bluetoothd >/dev/null 2>&1 +} + +bt_devices() { + echo "devices" "${1:-}" | bluetoothctl | + awk '/^Device /{match($0, /[0-9A-F:]{17}/); mac=substr($0,RSTART,RLENGTH); name=substr($0,RSTART+RLENGTH+1); print mac "\t" name}' +} + +bt_devices_getmac() { + awk -F '\t' '{print $1}' +} + +bt_devices_getname() { + awk -F '\t' '{print $2}' +} + +# +# actions +# + +blum_enable() { + fuzl_try -- doas -n rc-service bluetooth start + fuzl_notification "enabled" "-" + main +} + +blum_disable() { + fuzl_try -- doas -n rc-service bluetooth stop + fuzl_notification "disabled" "-" +} + +blum_disconnect() { + echo "disconnect" | fuzl_try -- bluetoothctl + fuzl_notification "disconnected" "-" +} + +blum_devices() { + device=$(bt_devices | + FUZL_PROMPT='devices' fuzl_menu $FUZL_MENU_FILTER) + + [ -n "$device" ] || main + device_mac=$(printf %s "$device" | bt_devices_getmac) + + echo "connect" "$device_mac" | fuzl_try -- bluetoothctl + fuzl_notification "connected" "$device" + +} + +blum_scan() { + fuzl_loading "scanning" + echo "scan" "on" | bluetoothctl || return + sleep 2 + fuzl_loading_cancel # TODO: this kills everything + main +} + +blum_repl() { + riverctl spawn 'footclient -- bluetoothctl' +} + +blum_tether_amagiri() { + local amagiri_mac='3C:38:F4:5D:3D:49' + local amagiri_net='e4e75ff7-b907-43b7-98b7-e0bf94a56d08' + echo "connect" "$amagiri_mac" | fuzl_try -- bluetoothctl + fuzl_notification "connected" "amagiri" + fuzl_try -- nmcli connection up "$amagiri_net" + fuzl_notification "network up" "amagiri" +} + +main diff --git a/bin/dots b/bin/dots new file mode 100755 index 0000000..cee0d73 --- /dev/null +++ b/bin/dots @@ -0,0 +1,28 @@ +#!/bin/sh +set -eu +[ -n "${DEBUG:-}" ] && set -x +case "${1:-status}" in +git) + [ $# -gt 0 ] && shift + exec /usr/bin/git --git-dir=$HOME/.dotfiles --work-tree=$HOME $* + ;; +st | status) + exec $0 git status -sb --untracked-files=no + ;; +add) + shift + exec $0 git add -f $* + ;; +commit | snapshot | record) + shift + $0 git commit --all --trailer=Automated-commit-by:dots --trailer=signoff --gpg-sign --message=* $* + echo + $0 git log --stat --format=full -1 + echo + ;; +push) + set -x + $0 git push inazuma || true + $0 git push origin || true + ;; +esac diff --git a/bin/dskm b/bin/dskm new file mode 100755 index 0000000..c5a0abf --- /dev/null +++ b/bin/dskm @@ -0,0 +1,26 @@ +#!/bin/bash +set -eu + +# shellcheck disable=2034 +description="desk height manager" + +export FUZL_PROMPT=dskm +export FUZL_ICON=๓ฑน +. libfuzl.sh + +main() { + local pos + + pos=$( + idesk config | + awk '/positions:/{ok=1; next} ok==1{print gensub(":", "", "g", $1)}' | + fuzl_menu $FUZL_MENU_FILTER + ) + test -n "$pos" || return + + fuzl_loading "moving desk to position: $pos" + idesk "$pos" + fuzl_loading_cancel +} + +main diff --git a/bin/genup b/bin/genup new file mode 100755 index 0000000..c759a10 --- /dev/null +++ b/bin/genup @@ -0,0 +1,91 @@ +#!/bin/sh +. /lib/gentoo/functions.sh + +set -e +[ -n "${DEBUG:-}" ] && set -x + +if [ "$UID" != 0 ]; then + eerror "need to be root" + exit 1 +fi + +# builtin emerge args +eargs= + +# order: +# sync -> portage update -> update -> update unstable -> clean -> flatpak + +if test $# -eq 0; then + dosync=1 + doupdate=1 + dolive=1 + doclean=1 + doflatpak=1 +fi + +printonly= +while getopts spulcfKXBh opt; do + case ${opt} in + s) dosync=1 ;; + u) doupdate=1 ;; + l) dolive=1 ;; + c) doclean=1 ;; + f) doflatpak=1 ;; + B) eargs="$eargs -X www-client/librewolf" ;; + K) eargs="$eargs -X sys-kernel/* -X virtual/dist-kernel" ;; + X) printonly=1 ;; + h | ?) + printf "usage: %s [-hsulcfBKX]\n" "$(basename "$0")" + printf "\n" + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +_() { + [ -n "$printonly" ] && printf "%s\n" "$*" && return + eval $* +} + +ebegin syncing +[ -n "${dosync:-}" ] && + _ emerge --sync && + eend + +ebegin updating portage +emerge --pretend --quiet --update sys-apps/portage | grep -qE '\[.*U.*\]' && + _ emerge -a --oneshot --getbinpkg sys-apps/portage && + eend + +ebegin updating @world +[ -n "${doupdate:-}" ] && + _ emerge -ag --update --deep --newuse --keep-going "$@" $eargs @world && + eend + +# --binpkg-respect-use=y +# _ revdep-ebuild -- "$@" $eargs && + +ebegin updating unstable +[ -n "${dolive:-}" ] && + _ emerge -a1 --keep-going @live-rebuild && + eend + +ebegin depcleaning +[ -n "${doclean:-}" ] && + _ emerge -a --depclean "$@" && + _ eclean --deep packages && + _ eclean --deep distfiles && + eend + +ebegin updating @preserved-rebuild +[ -n "${doupdate:-}" ] && + _ emerge -a --keep-going "$@" $eargs @preserved-rebuild && + eend + +ebegin updating flatpaks +[ -x "$(command -v flatpak)" ] && [ -n "${doflatpak:-}" ] && + _ flatpak upgrade && + _ flatpak uninstall --unused && + _ flatpak repair && + eend diff --git a/bin/hr b/bin/hr new file mode 100755 index 0000000..8231ae1 --- /dev/null +++ b/bin/hr @@ -0,0 +1,22 @@ +#!/bin/bash +set -eu +function print_hr() { + local str cols + local start line end + + str="${1:-}" + [[ -n $str ]] && str=" $str " + cols=$((${COLUMNS:-$(tput cols)} - ${#str} - 3)) + # start=$'\e(0'; end=$'\e(B'; line="qqq" + start=''; end=''; line="โ" + while ((${#line}<$cols)); do line+=$line; done + + printf "โพ" + printf "%s%s%s" "$start" "${line:0:$((cols/2))}" "$end" + printf "%s%s%s" "$start" "${line:0:$((cols/2))}" "$end" + printf "%s" "$str" + printf "%s%s%s" "$start" "${line:0:1}" "$end" + printf "โผ" + printf "\n" +} +print_hr "$@" diff --git a/bin/libfuzl.sh b/bin/libfuzl.sh new file mode 100644 index 0000000..a28c861 --- /dev/null +++ b/bin/libfuzl.sh @@ -0,0 +1,114 @@ +#!/bin/bash +# local is well supported +# shellcheck disable=3043 + +set -eu + +# pick up configuration +FUZL_PROMPT=${FUZL_PROMPT:-fuzl} +FUZL_ICON=${FUZL_ICON:-๓ฐฒ} + +fuzl_try() { + [ "$1" == "--" ] && shift 1 + if ! { stderr=$( + set +x + eval "$*" 2>&1 1>&3 + ); } 3>&1; then + fuzl_notification "$* failed :(" "${stderr:--}" -c error + exit 1 + fi +} + +export FUZL_MENU_FILTER=filter +export FUZL_MENU_PASSWORD=passw +export FUZL_MENU_MESSAGE=msg +export FUZL_MENU_INPUT=input + +# Shows a menu +fuzl_menu() { + local form="$1" + shift 1 + + local prompt options_list maxw + + # If a prompt_icon is provided, prepend it. + prompt="$FUZL_PROMPT " + [ -n "$FUZL_ICON" ] && prompt="$FUZL_ICON $FUZL_PROMPT " + + options_list=$(cat) + maxw=$(echo -e "$prompt\n$options_list" | wc -L) + [ "$maxw" -lt 15 ] && maxw=15 + [ "$maxw" -gt 150 ] && maxw=100 + + case "$form" in + "$FUZL_MENU_FILTER") # Selection menu with a list of options passed via STDIN + echo -ne "$options_list" | fuzzel -d -w "$maxw" -p "$prompt" "$@" + ;; + "$FUZL_MENU_PASSWORD") # Password input menu (minimalist) + echo -n | fuzzel -d -w "$maxw" -p "$prompt" --password + ;; + "$FUZL_MENU_MESSAGE") # Message box menu (no input bar) + echo -ne "$options_list" | fuzzel -d -w "$maxw" -p "$prompt" "$@" + ;; + "$FUZL_MENU_INPUT") # Text input menu (no list view) + echo -n | fuzzel -d -p "$prompt" "$@" --lines 0 + ;; + esac +} + +FUZL_NOTIFICATION_ID= + +fuzl_notification() { + local summary="${1:?missing summary}" + local body="${2:?missing body}" + shift 2 + + FUZL_NOTIFICATION_ID=$( + fyi "$@" -p -a "${FUZL_PROMPT}" "${FUZL_PROMPT}: $summary" "$body" | + cut -d= -f2 + ) +} +fuzl_notification_cancel() { + fyi --close "$FUZL_NOTIFICATION_ID" || true + FUZL_NOTIFICATION_ID= +} + +FUZL_LOADING_PID= + +fuzl_loading() { + local summary="${1:?missing summary}" + echo -e "..." | FUZL_PROMPT="$summary" fuzl_menu $FUZL_MENU_MESSAGE & + FUZL_LOADING_PID=$! +} +fuzl_loading_cancel() { + local pgid + pgid="$(ps -o pgid= "$FUZL_LOADING_PID" || return)" + kill -- -"$pgid" || true + FUZL_LOADING_PID= +} + +FUZL_ACTIONS=() + +fuzl_action_add() { + local action="${1:?}" icon="${2:?}" + shift 2 + # prevent multiple occurences + printf '%s\n' "${FUZL_ACTIONS[@]}" | grep -q "^$action" && return + FUZL_ACTIONS+=("${action}\t${icon}\t${*}") +} + +fuzl_action_menu() { + local action + + action=$(printf '%s\n' "${FUZL_ACTIONS[@]}" | + FUZL_PROMPT="$FUZL_PROMPT: $*" fuzl_menu $FUZL_MENU_FILTER --with-nth='{2} {3..}' --accept-nth=1) + + # reset actions + FUZL_ACTIONS=() + + test -n "$action" || return + eval "${FUZL_PROMPT}_${action}" "'$*'" +} + +# setup common debug helper +[ -z "${DEBUG:-}" ] || set -x diff --git a/bin/mailsync b/bin/mailsync new file mode 100755 index 0000000..2100962 --- /dev/null +++ b/bin/mailsync @@ -0,0 +1,148 @@ +#!/bin/bash +set -eu +[ -n "${DEBUG:-}" ] && set -x + +# check requirements +for tool in mbsync fyi python awk; do + if ! command -v "$tool" >/dev/null; then + printf '%s: %s not installed but required\n' "$(basename "$0")" "$tool" >&2 + exit 1 + fi +done + +maildir="${HOME}/mail" +cachefile=${XDG_CACHE_HOME:-$HOME/.cache}/mailsync.lastrun + +accounts=(gnzler) +list= +verbose= +while getopts a:lvh opt; do + case "${opt}" in + a) accounts=("${OPTARG}") ;; + l) list=1 ;; + v) verbose=1 ;; + h | ?) + printf "usage: %s [-h] [ACCOUNT...]\n" "$(basename "$0")" + printf "\n" + printf " -a ACCOUNTS, accunts to sync (default: \"%s\")\n" "${accounts[*]}" + printf " -l list mail\n" + printf " -v be verbose" + printf "\n" + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +if [ -n "${1:-}" ]; then + accounts=("$*") +fi + +notifyclose() { + fyi --close "$1" +} +notifysend() { + local summary="${1:?missing summary}" body="${2:?missing body}" + shift 2 + + fyi --print-id $@ -a mailsync \ + -i "${ico:-}" \ + "$summary" "<i>$body</i>" | cut -d= -f2 +} +try() { + [ "$1" = "--" ] && shift 1 + if ! { error=$( + set +x + eval $* 2>&3 + ); } 3>&1; then + notifysend "failed :(" "$error" -c error + return 1 + fi +} + + +decode() { + python -c 'import sys;import email.header;print(("".join(list(map(lambda s: s[0] if (type(s[0]) == str) else s[0].decode(s[1] or "utf-8"), email.header.decode_header(((sys.stdin.readlines() or [""])[0]).strip()))))))' +} + +list_msg() { + local acct=$2 + local args=(-type f -not -name '\.*') + local cachefile="${cachefile}.${acct}" + + find "${maildir}"/"${acct}"/{INBOX,lists}/new/ "${args[@]}" 2>/dev/null || true +} + +run_notify() { + local acct=$1 + local new_mail + local num + local nid + + new_mail=$(list_msg new "${acct}") + num=$(echo "$new_mail" | wc -l) + nid=$(notifysend "${acct}: new mail" "found ${num} new messages" --expire-time=6000) +} + +run_sync() { + local acct=$1 + local nid + local ret + + # exit if we're already running + pgrep mbsync >/dev/null && { + echo "mbsync is already running." + notifysend "${acct}: mailsync stopped" "mbsync is already running" + return + } + + # flush msmtp + nid=$(notifysend "${acct}: flushing queue" "running msmtp-queue" --expire-time=0) + try -- msmtp-queue -r || true + + # run sync + nid=$(notifysend "${acct}: syncing mail" "running mbsync" --expire-time=0 --replaces=$nid) + mbsync_args= + [ -n "${verbose}" ] && mbsync_args="--verbose" + try -- mbsync $mbsync_args --config="$HOME/.config/mbsyncrc" "$acct" + + # create cachefile + touch "${cachefile}.${acct}" + + # index new messages + if command -v notmuch >/dev/null 2>&1; then + nid=$(notifysend "${acct}: syncing mail" "running notmuch" --expire-time=0 --replaces=$nid) + try -- notmuch new + fi + + # update address book + if command -v maildir-rank-addr >/dev/null 2>&1; then + nid=$(notifysend "${acct}: syncing mail" "running maildir-rank-addr" --expire-time=0 --replaces=$nid) + maildir-rank-addr \ + --maildir=${maildir}/${acct} \ + --outputpath=${maildir}/addressbook \ + --addresses='(r|robert)@gnzler\.(de|io),robert(|\.)guenzler@gmail.com,therobotbase@gmail.com,robertg@balena.io,robert.gunzler@postman.com,robert@unikraft.io' \ + --filters='r\+.*@gnzler\.(de|io)' + fi + + notifyclose "$nid" +} + +# only list new mail, don't sync +if [ -z "${accounts[*]}" ]; then + echo "no account specified" >&2 + exit 1 +fi + +for acct in "${accounts[@]}"; do + ( + if [ -n "${list}" ]; then + list_msg "${list}" "${acct}" + exit 0 # exit ()& subshell + fi + run_sync "${acct}" + run_notify "${acct}" + ) & +done + +wait diff --git a/bin/mktoes b/bin/mktoes new file mode 100755 index 0000000..7eb47cf --- /dev/null +++ b/bin/mktoes @@ -0,0 +1,27 @@ +#!/bin/sh +set -eu +target=${1:?target} +size=${2:-normal} + +case $size in +normal) px=128 ;; +large) px=256 ;; +x-large) px=512 ;; +xx-large) px=1024 ;; +*) exit 1 ;; +esac + +md5=$(echo "file://$(readlink -e "$target")" | md5sum | cut -d' ' -f1) +dir=${XDG_CACHE_HOME:-$HOME/.cache}/thumbnails/$size +path=$dir/$md5.png +printf '%s' $path +[ -n "${MKTOES_NOGEN:-}" ] && exit 0 + +test -f $path && exit 0 +mkdir -p $dir +chmod 700 $dir + +tmp=$dir/.$$.$md5.png +ffmpeg -hide_banner -i "$target" -vf scale=$px:$px:force_original_aspect_ratio=decrease $tmp 2>/dev/null +mv -f $tmp $path +chmod 600 $path diff --git a/bin/msmtp-queue b/bin/msmtp-queue new file mode 120000 index 0000000..2604954 --- /dev/null +++ b/bin/msmtp-queue @@ -0,0 +1 @@ +/usr/share/msmtp/msmtpq/msmtp-queue \ No newline at end of file diff --git a/bin/msmtp-queue-stat b/bin/msmtp-queue-stat new file mode 100755 index 0000000..bb83784 --- /dev/null +++ b/bin/msmtp-queue-stat @@ -0,0 +1,17 @@ +#!/bin/sh +set -eu + +throttle_by=10 +throttle_cookie=/tmp/throttle.msmtp-queue-stat +throttle() { + last_called=$(stat -c %Y $throttle_cookie 2>/dev/null || printf 0) + now=$(date +%s) + touch $throttle_cookie + if [ $((now - last_called)) -gt $throttle_by ]; then + "$@" + else + false + fi +} + +throttle sh -c "msmtpq --q-mgmt -d | grep id= | wc -l | tee $throttle_cookie" || cat "$throttle_cookie" diff --git a/bin/msmtpq b/bin/msmtpq new file mode 120000 index 0000000..2e81615 --- /dev/null +++ b/bin/msmtpq @@ -0,0 +1 @@ +/usr/share/msmtp/msmtpq/msmtpq \ No newline at end of file diff --git a/bin/netm b/bin/netm new file mode 100755 index 0000000..fab7e95 --- /dev/null +++ b/bin/netm @@ -0,0 +1,1560 @@ +#!/usr/bin/env bash +# __ __ __ ________ ___ _ __ __ +# / / / /_ ______ _____/ / /_ __/ |/ / / | / /___/ /_ +# / /_/ / / / / __ \/ ___/ / / / / /|_/ /_____ / |/ / _ / __/ +# / __ / /_/ / /_/ / / / /___ / / / / / /_____ / /| / __/ /_ +#/_/ /_/\__, / .___/_/ /_____//_/ /_/ /_/ /_/ |_/\___/\__/ +# /____/_/ +# + +# Copyright ยฉ 2025 Djalel Oukid (sniper1720) +# +# Mods: +# - replace rofi with fuzzel +# - remove all theme references +# - fix wifi interface detection +# - hide extra search icon +# - replace close and check icons (due to problematic width) + +# --- Dependencies Check --- +if ! command -v fuzzel &>/dev/null; then + echo "Error: fuzzel is not installed. Please install it to use this script." >&2 + exit 1 +fi +if ! command -v nmcli &>/dev/null; then + echo "Error: nmcli is not installed. Please install NetworkManager and nmcli." >&2 + exit 1 +fi + +# --- Icon Variables (Nerd Fonts - Minimalist & Beautiful) --- +# --- Icons: General UI --- +icon_search="${icon_search:-""}" +icon_close="${icon_close:-"๓ฐ "}" +icon_check="${icon_check:-"๏ "}" +icon_on="${icon_on:-"๏ "}" +icon_off="${icon_off:-"๏"}" +icon_info="${icon_info:-"๓ฐ"}" +icon_refresh="${icon_refresh:-"๓ฐ"}" +icon_config="${icon_config:-"๎"}" + +# --- Icons: Network Types --- +icon_network="${icon_network:-"๓ฑ"}" +icon_wifi_prompt="${icon_wifi_prompt:-"๓ฑพ"}" +icon_ethernet="${icon_ethernet:-"๓ฐ"}" +icon_vpn="${icon_vpn:-"๓ฐ"}" +icon_wireguard="${icon_wireguard:-"๓ฐ"}" +icon_hotspot="${icon_hotspot:-"๓ฑ"}" +icon_airplane="${icon_airplane:-"๓ฐ"}" + +# --- Icons: Wi-Fi Signal --- +icon_wifi_full="${icon_wifi_full:-"๓ฐคจ"}" +icon_wifi_good="${icon_wifi_good:-"๓ฐคฅ"}" +icon_wifi_medium="${icon_wifi_medium:-"๓ฐคข"}" +icon_wifi_low="${icon_wifi_low:-"๓ฐค "}" +icon_wifi_disconnected="${icon_wifi_disconnected:-"๓ฐคฎ"}" +icon_wifi_enable="${icon_wifi_enable:-"๓ฐฉ"}" +icon_wifi_disable="${icon_wifi_disable:-"๓ฐช"}" + +# --- Icons: Security & Status --- +icon_wifi_secure="${icon_wifi_secure:-"๏ฃ"}" +icon_wifi_open="${icon_wifi_open:-"๓ฐค "}" +icon_unlock="${icon_unlock:-"๏ผ"}" +icon_password="${icon_password:-"๏"}" +icon_eye="${icon_eye:-"๓ฐ"}" +icon_eye_closed="${icon_eye_closed:-"๓ฐ"}" +icon_bookmark_saved="${icon_bookmark_saved:-"๓ฐขญ"}" +icon_saved="${icon_saved:-"๓ฐ"}" + +# --- Icons: Menus & Actions --- +icon_connect="${icon_connect:-"๓ฐซ"}" +icon_disconnect="${icon_disconnect:-"qa"}" +icon_vpn_disconnect="${icon_vpn_disconnect:-"๓ฐ"}" +icon_trash="${icon_trash:-"๏"}" +icon_pen="${icon_pen:-"๓ฐ"}" +icon_import="${icon_import:-"๓ฐบ"}" +icon_qrcode="${icon_qrcode:-"๓ฐฒ"}" + +# --- Icons: Details & Config --- +icon_active_details="${icon_active_details:-"๓ฐผ"}" +icon_status_chart="${icon_status_chart:-"๓ฑซ"}" +icon_interface="${icon_interface:-"๓ฐจ"}" +icon_devices="${icon_devices:-"๓ฐฝ"}" +icon_chip="${icon_chip:-"๓ฐขฎ"}" +icon_ipv4_config="${icon_ipv4_config:-"๓ฐ"}" +icon_ipv4_dns="${icon_ipv4_dns:-"๓ฐ"}" +icon_ipv6_config="${icon_ipv6_config:-"๓ฐ"}" # Reuse +icon_ipv6_dns="${icon_ipv6_dns:-"๓ฐ"}" # Reuse +icon_auto_ip="${icon_auto_ip:-"๓ฐ"}" +icon_auto_dns="${icon_auto_dns:-"๓ฐ"}" +icon_address="${icon_address:-"๓ฐ"}" +icon_gateway="${icon_gateway:-"๓ฐก"}" +icon_plug="${icon_plug:-"๓ฑ"}" +icon_wireless="${icon_wireless:-"๓ฐฉ"}" +icon_automatic="${icon_automatic:-"๓ฐ"}" + +# --- Icons: Miscellaneous --- +icon_hidden_network="${icon_hidden_network:-"๓ฐฒ"}" +icon_connect_wired="${icon_connect_wired:-"๓ฑ"}" +icon_wired_status="${icon_wired_status:-"๓ฐ"}" + +# --- Translatable / Customizable Messages --- +tr_checking_wifi_status='Checking Wi-Fi status... Please be patient.' +tr_scanning_networks='Scanning networks... Please be patient.' +tr_connecting_to='Connecting... Please be patient.' +tr_disconnecting_from='Disconnecting... Please be patient.' +tr_submenu_message='More options' +tr_disable_message='Disable Wi-Fi' +tr_enable_message='Enable Wi-Fi' +tr_interface_message='Interface:' +tr_known_connections_message='Known connections' # FIX: Removed colon +tr_available_networks_message='Available Networks' +tr_available_vpn_profiles_message='Available VPN Profiles' +tr_autoconnect_message='Autoconnect' +tr_ipv4_config_message='IPv4 Configuration' +tr_ipv6_config_message='IPv6 Configuration' +tr_dns4_message='DNS IPv4' +tr_dns6_message='DNS IPv6' +tr_connection_details_message='Connection Details' # New Header +tr_ip_addr='IP Address' +tr_gateway='Gateway' +tr_signal_strength='Signal Strength' +tr_speed='Link Speed' +tr_frequency='Frequency' +tr_mac_addr='MAC Address' +tr_device='Interface' +tr_autoip_message='Automatic IP' +tr_autodns_message='Automatic DNS' +tr_address_message='Addresses' +tr_gateway_message='Gateway:' +tr_forget_message='Forget connection' +tr_wireguard_enable_message='Toggle VPN' +tr_rename_connection_message='Rename connection' +tr_hidden_message='Connect to a hidden network' +tr_refresh_scan_message='Refresh Scan' # New message for refresh option +tr_no_known_wifi_connections='No known Wi-Fi connections.' # New message +tr_no_configured_vpns='No VPN connections configured.' # New message +tr_no_active_vpns='No active VPN connections.' # New message +tr_no_saved_connections='No saved connections found.' # New message +tr_no_active_connection='No active connection.' # New message +tr_no_ethernet_device='No Ethernet device found.' # New message +tr_no_wifi_networks_found='No Wi-Fi networks found.' # New message for empty scan +tr_wired_status_message='Wired Status' # New message for wired status option +tr_manage_wired_profile='Manage Active Wired Profile' # New message for wired profile management +tr_connect_wired_connection='Connect to Wired Connection:' # Refined prompt +tr_manage_wired_connections='Manage Wired Connections' # New message for wired connections management +tr_import_vpn_message='Import VPN from file' +tr_import_vpn_prompt='Enter full path to .conf or .ovpn file:' +tr_status_message='' +tr_status_connected_to='Connected to' +tr_status_connected='Connected' +tr_status_disconnected='Disconnected' + +tr_status_disabled='Disabled' +tr_connect_now_message='Connect Now' +tr_disconnect_message='Disconnect Now' +tr_notice_import_success_summary='VPN Imported' +tr_notice_import_success_body='Successfully imported VPN connection.' +tr_notice_import_error_summary='Import Error' +tr_notice_import_error_body='Failed to import VPN connection.' +tr_notice_file_not_found_body='File not found at specified path.' + +# Airplane Mode +tr_airplane_mode_message='Airplane Mode' +tr_airplane_on='Airplane Mode Enabled' +tr_airplane_off='Airplane Mode Disabled' + +# QR Code +tr_edit_password_message='Edit Password' # New translation +tr_password_prompt='Enter password for' +tr_password_updated='Password Updated' +tr_password_update_failed='Failed to update password' +tr_connection_failed_retry='Connection failed. Update password?' +tr_qrcode_message='Share via QR Code' +tr_qrcode_generating='Generating QR Code...' +tr_qrcode_error='Could not generate QR code. Is qrencode installed?' +tr_qrcode_no_password='Cannot share network without saved password.' + +# Hotspot +tr_hotspot_message='Create Hotspot' +tr_hotspot_ssid_prompt='Enter Hotspot SSID:' +tr_hotspot_password_prompt='Enter Hotspot Password (min 8 chars):' +tr_hotspot_creating='Creating Hotspot...' +tr_hotspot_success='Hotspot created successfully!' +tr_hotspot_error='Failed to create hotspot.' +tr_notice_unknown_vpn_type_body='Unknown VPN file type. Use .conf or .ovpn.' + +# Main menu prompt now includes network and search icons +tr_main_menu_prompt="$icon_network Network Manager: $icon_search" +tr_wifi_menu_prompt="$icon_wifi_prompt Wi-Fi: $icon_search" +tr_wired_menu_prompt="$icon_ethernet Wired: $icon_search" # NEW: Wired sub-menu prompt +tr_vpn_menu_prompt="$icon_vpn_disconnect VPN: $icon_search" # NEW: VPN sub-menu prompt +tr_saved_connections_menu_prompt="$icon_bookmark_saved Saved Connections: $icon_search" # NEW: Saved Connections sub-menu prompt +tr_status_menu_prompt="$icon_status_chart Status: $icon_search" # NEW: Status sub-menu prompt + +tr_select_interface_prompt='Select Interface:' +tr_ask_password_prompt='Enter password:' # Refined prompt +tr_menu_dns_prompt='Enter DNS (e.g., 8.8.8.8):' # Refined prompt +tr_menu_dns_sure_prompt_1='Remove DNS ' +tr_menu_dns_sure_prompt_2='?' +tr_menu_ip_config_addresses_prompt='Enter address (e.g., 192.168.1.10/24):' # Refined prompt +tr_menu_ip_config_gateway_prompt='Enter gateway (e.g., 192.168.1.1):' # Refined prompt +tr_menu_addresses_prompt='Type or select address (e.g., 192.168.1.10/24):' # Refined prompt +tr_menu_addresses_sure_prompt_1='Remove address ' +tr_menu_addresses_sure_prompt_2='?' +tr_forget_connection_sure_prompt_1='Forget ' +tr_forget_connection_sure_prompt_2='?' +tr_forget_connection_confirm='Yes, forget' # New confirmation text +tr_rename_connection_prompt='Enter new name for connection:' # Refined prompt +tr_connect_hidden_prompt='Enter hidden network name:' # Refined prompt + +tr_notice_connected_summary='Connected' +tr_notice_disconnected_summary='Disconnected' +tr_notice_error_summary='Connection Error' +tr_notice_connected_body='Successfully connected to' +tr_notice_disconnected_body='Successfully disconnected from' +tr_notice_error_body='Failed to connect to' +tr_notice_error_disconnect_body='Failed to disconnect from' + +# NEW: Messages for password input actions +tr_show_password_message='Show Password' +tr_hide_password_message='Hide Password' +tr_confirm_password_message='Confirm Password' +tr_edit_password_message='Edit Password' +tr_edit_password_prompt='Enter new password:' + +# --- Global Variables --- +program_name="$(basename "$0")" +LOADING_ROFI_PID="" +mapfile -t interfaces < <(nmcli --colors no -t -f TYPE,DEVICE device status | awk -F ':' '$1 == "wifi" {print $2}') +if [ -z "${interfaces[0]}" ]; then + echo "$program_name: No Wi-Fi interfaces detected." >&2 + exit 2 +fi +interface_to_use="${interfaces[0]}" + +# --- Helper Functions --- + +# Function to display a menu (form 1: selection, 2: text input with list, 3: password input, 4: message box, 5: text input no list) +# Now accepts optional extra flags to pass to rofi. +display_menu() { + local form="$1" + local prompt_text="$2" + local prompt_icon="${3:-}" # Optional: icon to prepend to prompt + local extra_flags="${4:-}" # Optional: extra flags for the fuzzel command + local rofi_prompt rofi_flags options_list + + # If a prompt_icon is provided and the prompt_text doesn't already start with it, prepend it. + if [ -n "$prompt_icon" ] && ! echo "$prompt_text" | grep -qE "^$prompt_icon"; then + rofi_prompt="$prompt_icon $prompt_text" + else + rofi_prompt="$prompt_text" + fi + + local result + + case $form in + 1 | 2 | 4) + options_list=$(cat) + maxw=$(($(wc -L <<<"$rofi_prompt\n$options_list") + 2)) + ;& + 1) # Selection menu with a list of options passed via STDIN + result=$(echo -e "$options_list" | fuzzel -di -w $maxw $extra_flags -p "$rofi_prompt") + ;; + 2) # Generic text input menu with a list + result=$(echo -e "$options_list" | fuzzel -d -w $maxw $extra_flags -p "$rofi_prompt") + ;; + 3) # Password input menu (minimalist) + result=$(fuzzel -d --password $extra_flags -p "$rofi_prompt") + ;; + 4) # Message box menu (no input bar) + result=$(echo -e "$options_list" | fuzzel -di -w $maxw --hide-prompt $extra_flags -p "$rofi_prompt") + ;; + 5) # Text input menu (no list view) + result=$(fuzzel -d $extra_flags -p "$rofi_prompt") + ;; + esac + echo "$result" +} + +# --- Rofi-based Notification System --- + +# Shows a non-blocking "loading" Rofi window and stores its PID. +show_loading_notification() { + local message="$1" + # Use dmenu mode to get the themed window, but completely override the + # mainbox layout to show only a single, centered textbox. + echo "..." | fuzzel -d -p "$message" & + LOADING_ROFI_PID=$! +} + +# Kills the loading notification Rofi window if it's running. +kill_loading_notification() { + if [ -n "$LOADING_ROFI_PID" ] && ps -p "$LOADING_ROFI_PID" >/dev/null; then + kill "$LOADING_ROFI_PID" + LOADING_ROFI_PID="" + fi +} + +# Shows a final, blocking Rofi message for results (Success, Error, etc.). +# Optionally sends a system notification if notify-send is available. +send_notification() { + local summary="$1" + local body="$2" + + # Try system notification first (non-blocking) + if command -v notify-send &>/dev/null; then + fyi -a netm "$icon_network $summary" "$body" + else + # Fallback to Rofi message + show_message "$body" "$summary" + fi +} + +# Function to show a message (stays open until dismissed, with "OK" button) +# Now accepts an optional custom_prompt and a flag to include the "OK" button. +show_message() { + local msg="$1" + local custom_prompt="${2:-Info}" # Default to "Info" if no custom prompt is provided + local include_ok="${3:-true}" # Default to true + local options="$msg" + + if [ "$include_ok" = "true" ]; then + options+="\n$icon_check OK" + fi + # Use form 4 to disable the input bar for these messages + echo -e "$options" | display_menu 4 "$custom_prompt" "" +} + +# Function to display an informational message with a "Back" button +# Now accepts an optional custom_prompt +display_info_message() { + local msg="$1" + local custom_prompt="${2:-Info}" # Default to "Info" if no custom prompt is provided + local options="$msg\n$icon_close Back" + # Use form 4 to disable the input bar for these messages + local chosen=$(echo -e "$options" | display_menu 4 "$custom_prompt" "") + if [[ "$chosen" =~ "^$icon_close Back" ]]; then + return + fi +} + +# Handles password input with a dynamic, interactive menu for showing, hiding, and editing. +ask_password() { + local password_input + local password_shown="false" # 'true' or 'false' + + # Step 1: Initial password prompt + password_input=$(echo "" | display_menu 3 "$tr_ask_password_prompt" "") + + # If user cancels the initial prompt (presses Esc or enters nothing), return empty. + if [ -z "$password_input" ]; then + return 1 + fi + + # Step 2: Interactive action menu loop + while true; do + local options="" + local password_display="" + + # Determine how to display the password + if [ "$password_shown" = "true" ]; then + password_display="$password_input" + options+="$icon_eye_closed $tr_hide_password_message: $password_display\n" + else + # Create a bullet point string of the same length as the password + password_display=$(printf 'โข%.0s' $(seq 1 ${#password_input})) + options+="$icon_eye $tr_show_password_message: $password_display\n" + fi + + # Add other actions + options+="$icon_pen $tr_edit_password_message\n" + options+="$icon_unlock $tr_confirm_password_message\n" + options+="$icon_close Back" + + # Display the interactive menu + local action_choice=$(echo -e "$options" | display_menu 1 "Password Actions" "") + + # Handle user's choice + if [ -z "$action_choice" ] || [[ "$action_choice" =~ "^$icon_close Back" ]]; then + # User cancelled at action menu + return 1 + fi + + case "$action_choice" in + *"$tr_show_password_message"* | *"$tr_hide_password_message"*) + # Toggle password visibility + if [ "$password_shown" = "true" ]; then + password_shown="false" + else + password_shown="true" + fi + # Loop continues, redrawing the menu + ;; + *"$tr_edit_password_message"*) + # Ask for a new password, pre-filling the input with the current password. + local new_password=$(echo "" | display_menu 2 "$tr_edit_password_prompt" "" "-filter \"$password_input\"") + # Only update if the user entered something and didn't cancel + if [ -n "$new_password" ]; then + password_input="$new_password" + password_shown="false" # Hide new password by default + fi + # Loop continues, redrawing the menu + ;; + *"$tr_confirm_password_message"*) + # Return the confirmed password + echo "$password_input" + return + ;; + esac + done +} + +# Show details for the active connection +show_connection_details() { + local active_ssid="$1" + local device="$2" + + show_loading_notification "Gathering details..." + + # Get details using nmcli device show + # We grep for specific fields + local info=$(nmcli -t -f GENERAL,IP4,IP6 device show "$device") + + # Extract fields (hacky but effective for standard nmcli output) + # MAC often contains colons, so we strip the field name "GENERAL.HWADDR:" + local ipv4=$(echo "$info" | grep "IP4.ADDRESS\[1\]" | cut -d':' -f2) + local gateway=$(echo "$info" | grep "IP4.GATEWAY" | cut -d':' -f2) + local hwaddr=$(echo "$info" | grep "GENERAL.HWADDR" | sed 's/^GENERAL.HWADDR://') + local state=$(echo "$info" | grep "GENERAL.STATE" | cut -d':' -f2) + + # Wifi specific details + local wifi_info=$(nmcli -t -f IN-USE,SSID,MODE,CHAN,RATE,SIGNAL,BARS,SECURITY device wifi list | grep "^\*") + # Format: *:SSID:Infra:Chan:Rate:Signal:Bars:Sec + local chan=$(echo "$wifi_info" | cut -d':' -f4) + local rate=$(echo "$wifi_info" | cut -d':' -f5) + local signal=$(echo "$wifi_info" | cut -d':' -f6) + local bars=$(echo "$wifi_info" | cut -d':' -f7) + local freq="" + + # Determine frequency band roughly by channel + if [ "$chan" -gt 14 ]; then freq="5 GHz"; else freq="2.4 GHz"; fi + + kill_loading_notification + + local details="" + details+="$icon_address $tr_ip_addr: $ipv4\n" + details+="$icon_gateway $tr_gateway: $gateway\n" + details+="$icon_wifi_full $tr_signal_strength: $signal% ($bars)\n" + details+="$icon_on $tr_speed: $rate\n" + details+="$icon_wireless $tr_frequency: $freq (Ch $chan)\n" + details+="$icon_devices $tr_mac_addr: $hwaddr\n" + details+="$icon_chip $tr_device: $device" + + display_info_message "$details" "$tr_connection_details_message" +} + +# --- Dedicated Wi-Fi Scan Function --- +perform_wifi_scan() { + show_loading_notification "$tr_scanning_networks" + + # Get list of known SSIDs, delimited by | + # We use tr to replace newline with | + local known_ssids=$(nmcli -t -f 802-11-wireless.ssid connection show | tr '\n' '|') + # Add leading/trailing pipe for exact matching + known_ssids="|$known_ssids" + + local scan_output=$(nmcli --colors no --get-values SECURITY,SIGNAL,SSID,IN-USE device wifi list --rescan auto | awk -F ':' \ + -v icon_wifi_secure="$icon_wifi_secure" \ + -v icon_wifi_open="$icon_wifi_open" \ + -v icon_wifi_full="$icon_wifi_full" \ + -v icon_wifi_good="$icon_wifi_good" \ + -v icon_wifi_medium="$icon_wifi_medium" \ + -v icon_wifi_low="$icon_wifi_low" \ + -v icon_check="$icon_check" \ + -v icon_unlock="$icon_unlock" \ + -v known_ssids="$known_ssids" \ + ' + BEGIN { x = 1 } + { + # 1. First Icon: Signal Strength (Always shown) + wifi_signal_icon = icon_wifi_low; + if ($2 > 75) wifi_signal_icon = icon_wifi_full; + else if ($2 > 50) wifi_signal_icon = icon_wifi_good; + else if ($2 > 25) wifi_signal_icon = icon_wifi_medium; + + ssid = $3; + if (ssid == "") ssid = "<hidden>"; + + # 2. Second Icon: Status (Priority: Active > Secure > Open/Known) + # User Logic: + # - Active -> Checkmark + # - Secure (Even if known) -> Lock + # - Open -> Unlock + + status_icon = icon_unlock; # Default to Unlock (Open) + + if ($4 == "*") { + status_icon = icon_check; # Active -> Check + } else if ($1 ~ /^WPA/) { + status_icon = icon_wifi_secure; # Secure -> Lock + } + # Else remains Unlock (Open) + + # Layout: [Signal] [Status] SSID (Sig%) + # Note: We purposely put two icons on the left as requested. + formatted_entry = wifi_signal_icon " " status_icon " " ssid " (" $2 "%)"; + + if ($4 == "*") { + networks[0] = formatted_entry; + } else { + networks[x++] = formatted_entry; + } + } + END { + if (networks[0] != "") { + print networks[0]; + } + for (i = 1; i < x; i++) { + print networks[i]; + } + } + ') + kill_loading_notification + echo "$scan_output" +} + +# --- Menu for Available Wi-Fi Networks --- +menu_available_wifi_networks() { + local wifi_list options chosen + while true; do + wifi_list=$(perform_wifi_scan) + + options="$icon_refresh $tr_refresh_scan_message\n" + if [ -z "$wifi_list" ]; then + options+="$icon_wifi_disconnected $tr_no_wifi_networks_found\n" + else + options+="$wifi_list\n" + fi + options+="$icon_close Back" # Always at the bottom + + # Prompt for this menu is just the search icon + chosen=$(echo -e "$options" | display_menu 1 "$tr_available_networks_message" "$icon_search") + + # Handle Esc or "Back" selection + if [ -z "$chosen" ] || [[ "$chosen" =~ "^$icon_close Back" ]]; then + return # Go back to parent menu (menu_wifi) + fi + + case "$chosen" in + "$icon_refresh $tr_refresh_scan_message") + continue # Restart the while loop to re-run scan and redisplay menu + ;; + "$icon_wifi_disconnected $tr_no_wifi_networks_found") + # Do nothing if "No Wi-Fi networks found" is selected + continue + ;; + *) + # This block should only be reached when a network is explicitly chosen + connect_wifi "$chosen" + ;; + esac + done +} + +# --- Main Wi-Fi menu (toggle, hidden, known, available networks) --- +menu_wifi() { + local connection_state options chosen + while true; do + show_loading_notification "$tr_checking_wifi_status" + connection_state=$(nmcli --colors no --get-values WIFI general) + local active_connection_info=$(nmcli -t -f NAME,DEVICE connection show --active | grep -E "wl*" | head -n 1) + kill_loading_notification + + local status_line="" + if [ "$connection_state" = "disabled" ]; then + status_line="$icon_wifi_disable $tr_status_message $tr_status_disabled" + options="$icon_wifi_enable $tr_enable_message\n" + else + if [ -n "$active_connection_info" ]; then + local active_ssid=$(echo "$active_connection_info" | cut -d':' -f1) + local active_device=$(echo "$active_connection_info" | cut -d':' -f2) + status_line="$icon_wifi_full $tr_status_message $tr_status_connected_to '$active_ssid' ($active_device)" + else + status_line="$icon_wifi_disconnected $tr_status_message $tr_status_disconnected" + fi + options="$icon_wifi_disable $tr_disable_message\n" + ${interfaces[1]:+options+="$icon_interface $tr_interface_message ${interface_to_use}\n"} + # Add "Available Networks" as a sub-menu + options+="$icon_wireless $tr_available_networks_message\n" + fi + + local full_options="$status_line\n" + full_options+="$options" + full_options+="$icon_hotspot $tr_hotspot_message\n" # NEW: Create Hotspot + full_options+="$icon_bookmark_saved $tr_known_connections_message\n" # FIX: Using icon_bookmark_saved + full_options+="$icon_hidden_network $tr_hidden_message\n" # FIX: Using icon_hidden_network + full_options+="$icon_close Back" # Always at the bottom + + chosen=$(echo -e "$full_options" | display_menu 1 "$tr_wifi_menu_prompt" "") # Pass "" for prompt_icon + + # Handle Esc or "Back" selection + if [ -z "$chosen" ] || [[ "$chosen" =~ "^$icon_close Back" ]]; then + return # Go back to parent menu (main_menu) + fi + + case "$chosen" in + "$icon_wifi_enable $tr_enable_message") + nmcli radio wifi on + ;; + "$icon_wifi_disable $tr_disable_message") + nmcli radio wifi off + ;; + *"$tr_interface_message"*) select_interface ;; + "$icon_wireless $tr_available_networks_message") menu_available_wifi_networks ;; + *"$tr_known_connections_message"*) menu_known_connections "wifi" ;; + *"$tr_hidden_message"*) connect_hidden ;; + *"$tr_hotspot_message"*) create_hotspot ;; + *"$tr_status_message"*) + # Show details if connected + if [ -n "$active_connection_info" ]; then + local active_ssid=$(echo "$active_connection_info" | cut -d':' -f1) + local active_device=$(echo "$active_connection_info" | cut -d':' -f2) + show_connection_details "$active_ssid" "$active_device" + fi + ;; + *) + show_message "Invalid option selected: $chosen" + ;; + esac + done +} + +select_interface() { + local chosen_interface=$( ( + for ((i = 0; i < ${#interfaces[@]}; i++)); do echo "$icon_interface ${interfaces[$i]}"; done + echo "$icon_close Back" + ) | display_menu 1 "$tr_select_interface_prompt" "") # Pass "" for prompt_icon + + if [ -z "$chosen_interface" ]; then + return + elif [[ "$chosen" =~ ^"$icon_close Back" ]]; then + return + else + interface_to_use="${chosen_interface:3}" + fi +} + +# Connect to a hidden Wi-Fi network +connect_hidden() { + local wifi_name=$(echo "" | display_menu 5 "$tr_connect_hidden_prompt" "") # Pass "" for prompt_icon + + if [ -z "$wifi_name" ]; then + return # User cancelled or pressed back + fi + + local wifi_password=$(ask_password) + + if [ -z "$wifi_password" ]; then + show_message "Connection cancelled. No password provided." + return + fi + + show_loading_notification "$tr_connecting_to" + if nmcli --wait 15 device wifi connect "$wifi_name" hidden yes password "$wifi_password"; then + kill_loading_notification + send_notification "$tr_notice_connected_summary" "$tr_notice_connected_body '$wifi_name'" + exit 0 + else + kill_loading_notification + send_notification "$tr_notice_error_summary" "$tr_notice_error_body '$wifi_name'" + fi +} + +# Connect to a visible Wi-Fi network +connect_wifi() { + local chosen_entry="$1" + + if [[ "$chosen_entry" == *"$icon_check"* ]]; then + local ssid_from_active=$(nmcli -t -f active,ssid dev wifi | grep "^yes" | cut -d':' -f2) + local active_uuid=$(nmcli -t -f UUID,TYPE,ACTIVE connection show | grep ":802-11-wireless:yes" | cut -d':' -f1 | head -n1) + + if [ -n "$active_uuid" ]; then + if [ -z "$ssid_from_active" ]; then + local temp_ssid=$(echo "$chosen_entry" | sed -E 's/ \([0-9]+%\).*$//') + ssid_from_active=$(echo "$temp_ssid" | sed -E 's/^(๓ฐฌ |๓ฐคซ |๓ฐคช |๓ฐคฉ |๓ฐคจ |๓ฐคง |๓ฐคฆ |๓ฐคฅ |๓ฐคค |๓ฐคฃ |๓ฐคข |๓ฐคก |๓ฐค )+//' | xargs) + fi + menu_connection "$ssid_from_active" "$active_uuid" + return + fi + fi + + local temp_ssid=$(echo "$chosen_entry" | sed -E 's/ \([0-9]+%\).*$//') + local wifi_ssid=$(echo "$temp_ssid" | sed -E 's/^(๓ฐคจ |๓ฐคฅ |๓ฐคข |๓ฐคฏ |๓ฐคซ |๓ฐคช |๏ฃ |๏ผ |๏ )+//' | xargs) + + local is_secure=$(echo "$chosen_entry" | grep -q "$icon_wifi_secure" && echo "yes" || echo "no") + + # 1. Check if already connected + local active_ssid=$(nmcli -t -f active,ssid dev wifi | grep "^yes" | cut -d':' -f2) + if [ "$wifi_ssid" = "$active_ssid" ]; then + local active_uuid=$(nmcli -t -f UUID,TYPE,ACTIVE connection show | grep ":802-11-wireless:yes" | cut -d':' -f1 | head -n1) + if [ -n "$active_uuid" ]; then + menu_connection "$wifi_ssid" "$active_uuid" + return + fi + show_message "Already connected to $wifi_ssid." + return + fi + + # 2. Check if we have a saved connection (even if not active) + local saved_uuid="" + # Use standard fields (UUID, TYPE) that are guaranteed to exist + local saved_list=$(nmcli -t -f UUID,TYPE connection show) + + while IFS=: read -r uuid type; do + # Check against both known Wi-Fi type strings + if [[ "$type" == "802-11-wireless" || "$type" == "wifi" ]]; then + # Get the SSID for this connection specifically + local ssid_check=$(nmcli -t -f 802-11-wireless.ssid connection show "$uuid" 2>/dev/null) + # FIX: output is '802-11-wireless.ssid:SSID', we must strip the prefix + ssid_check="${ssid_check#*:}" + + if [ "$ssid_check" = "$wifi_ssid" ]; then + saved_uuid="$uuid" + break + fi + fi + done <<<"$saved_list" + + if [ -n "$saved_uuid" ]; then + menu_connection "$wifi_ssid" "$saved_uuid" + return + fi + + # 3. New Connection: Ask for password if secure + local connection_result + if [ "$is_secure" = "yes" ]; then + local wifi_password=$(ask_password) + if [ -z "$wifi_password" ]; then + show_message "Connection cancelled. No password provided." + return + fi + + show_loading_notification "$tr_connecting_to" + nmcli --wait 15 device wifi connect "$wifi_ssid" ifname "$interface_to_use" password "$wifi_password" + connection_result=$? + else + show_loading_notification "$tr_connecting_to" + nmcli --wait 15 device wifi connect "$wifi_ssid" ifname "$interface_to_use" + connection_result=$? + fi + kill_loading_notification + + if [ $connection_result -eq 0 ]; then + send_notification "$tr_notice_connected_summary" "$tr_notice_connected_body '$wifi_ssid'" + exit 0 + else + send_notification "$tr_notice_error_summary" "$tr_notice_error_body '$wifi_ssid'" + fi +} + +# --- IP Configuration Menus --- + +# Manage individual IP addresses for a connection +menu_addresses() { + local connection_uuid="$1" + local ipv="$2" + local -a addresses_list + local sure chosen + + while true; do + mapfile -t addresses_list < <(nmcli --get-values ipv${ipv}.addresses connection show "$connection_uuid" | sed 's/,/\n/g') + local options=$(printf "%s\n" "${addresses_list[@]}") + options+="$icon_close Back" + + chosen=$(echo -e "$options" | display_menu 1 "$tr_menu_addresses_prompt" "") # Pass "" for prompt_icon + + if [ -z "$chosen" ] || [[ "$chosen" =~ ^"$icon_close Back" ]]; then + return # User cancelled or pressed back + fi + + local found=0 + for addr in "${addresses_list[@]}"; do + if [ "$chosen" = "$addr" ]; then + found=1 + sure=$(echo -e "$icon_check\n$icon_close Back" | display_menu 1 "$tr_menu_addresses_sure_prompt_1 ${chosen}$tr_menu_addresses_sure_prompt_2" "") # Pass "" for prompt_icon + if [ -z "$sure" ] || [[ "$sure" =~ ^"$icon_close Back" ]]; then continue; fi # User cancelled confirmation + if [[ "$sure" =~ ^"$icon_check" ]]; then + nmcli connection modify uuid "$connection_uuid" -ipv${ipv}.addresses "$chosen" + if [ ${#addresses_list[@]} -eq 1 ]; then + nmcli connection modify uuid "$connection_uuid" ipv${ipv}.gateway '' + nmcli connection modify uuid "$connection_uuid" ipv${ipv}.method auto + fi + fi + break + fi + done + + if [ "$found" -eq 0 ]; then + nmcli connection modify uuid "$connection_uuid" +ipv${ipv}.addresses "$chosen" + nmcli connection modify uuid "$connection_uuid" ipv${ipv}.method manual + fi + done +} + +# Configure IP method (auto/manual), gateway +menu_ip_config() { + local chosen_connection_name="$1" + local connection_uuid="$2" + local ipv="$3" + local autoip_state autodns_state message new_gateway chosen + + while true; do + autoip_state="$([ "$(nmcli --get-values ipv${ipv}.method connection show "$connection_uuid")" = "auto" ] && echo "$icon_on" || echo "$icon_off")" + + local current_gateway=$(nmcli --get-values ipv${ipv}.gateway connection show "$connection_uuid") + local current_addresses=$(nmcli --get-values ipv${ipv}.addresses connection show "$connection_uuid" | sed 's/,/\n/g') + + local options="" + options+="$icon_auto_ip $tr_autoip_message $autoip_state\n" + + if [ "$autoip_state" = "$icon_on" ]; then + autodns_state="$([ "$(nmcli --get-values ipv${ipv}.ignore-auto-dns connection show "$connection_uuid")" = "no" ] && echo "$icon_on" || echo "$icon_off")" + options+="$icon_auto_dns $tr_autodns_message $autodns_state\n" + else + options+="$icon_address $tr_address_message: ${current_addresses:-N/A}\n" + options+="$icon_gateway $tr_gateway_message ${current_gateway:-N/A}\n" + options+="$icon_gateway $tr_gateway_message ${current_gateway:-N/A}\n" + fi + options+="$icon_ipv4_dns DNS Configuration\n" + options+="$icon_close Back" + + chosen=$(echo -e "$options" | display_menu 1 "$chosen_connection_name (IPv$ipv)" "") # Pass "" for prompt_icon + + if [ -z "$chosen" ] || [[ "$chosen" =~ ^"$icon_close Back" ]]; then + return # User cancelled or pressed back + fi + + case "$chosen" in + *"DNS Configuration"*) + menu_dns "$connection_uuid" "$ipv" + ;; + *"$tr_autoip_message"*) + if [ "$autoip_state" = "$icon_on" ]; then + nmcli connection modify uuid "$connection_uuid" ipv${ipv}.method manual + else + nmcli connection modify uuid "$connection_uuid" ipv${ipv}.method auto + nmcli connection modify uuid "$connection_uuid" ipv${ipv}.gateway '' + nmcli connection modify uuid "$connection_uuid" ipv${ipv}.addresses '' + fi + ;; + *"$tr_autodns_message"*) + if [ "$autodns_state" = "$icon_on" ]; then + nmcli connection modify uuid "$connection_uuid" ipv${ipv}.ignore-auto-dns yes + else + nmcli connection modify uuid "$connection_uuid" ipv${ipv}.ignore-auto-dns no + fi + ;; + *"$tr_address_message"*) + menu_addresses "$connection_uuid" "$ipv" + ;; + *"$tr_gateway_message"*) + new_gateway=$(echo "$icon_close Back" | display_menu 1 "$tr_menu_ip_config_gateway_prompt" "") # Pass "" for prompt_icon + if [ -z "$new_gateway" ] || [[ "$new_gateway" =~ ^"$icon_close Back" ]]; then continue; fi + nmcli connection modify uuid "$connection_uuid" ipv${ipv}.gateway "$new_gateway" + ;; + esac + done +} + +# Configure DNS servers for a connection +menu_dns() { + local connection_uuid="$1" + local ipv="$2" + local -a dns_list + local sure chosen + + while true; do + mapfile -t dns_list < <(nmcli --get-values ipv${ipv}.dns connection show "$connection_uuid" | sed 's/,/\n/g') + local options=$(printf "%s\n" "${dns_list[@]}") + options+="$icon_close Back" + + chosen=$(echo -e "$options" | display_menu 1 "$tr_menu_dns_prompt" "") # Pass "" for prompt_icon + + if [ -z "$chosen" ] || [[ "$chosen" =~ ^"$icon_close Back" ]]; then + return # User cancelled or pressed back + fi + + local found=0 + for dns_entry in "${dns_list[@]}"; do + if [ "$chosen" = "$dns_entry" ]; then + found=1 + sure=$(echo -e "$icon_check\n$icon_close Back" | display_menu 1 "$tr_menu_dns_sure_prompt_1 ${chosen}$tr_menu_dns_sure_prompt_2" "") # Pass "" for prompt_icon + if [ -z "$sure" ] || [[ "$sure" =~ ^"$icon_close Back" ]]; then continue; fi # User cancelled confirmation + if [[ "$sure" =~ ^"$icon_check" ]]; then + nmcli connection modify uuid "$connection_uuid" -ipv${ipv}.dns "$chosen" + fi + break + fi + done + + if [ "$found" -eq 0 ]; then + nmcli connection modify uuid "$connection_uuid" +ipv${ipv}.dns "$chosen" + fi + done +} + +forget_connection() { + local chosen_connection_name="$1" + local connection_uuid="$2" + local options="$icon_check $tr_forget_connection_confirm\n$icon_close Back" + local sure=$(echo -e "$options" | display_menu 4 "$tr_forget_connection_sure_prompt_1 ${chosen_connection_name}$tr_forget_connection_sure_prompt_2" "") + + if [ -z "$sure" ] || [[ "$sure" =~ ^"$icon_close Back" ]]; then return 1; fi # User cancelled or pressed back + if [[ "$sure" =~ ^"$icon_check $tr_forget_connection_confirm" ]]; then + nmcli connection delete uuid "$connection_uuid" && return 0 + fi + return 1 +} + +rename_connection() { + local connection_uuid="$1" + local new_name=$(echo "" | display_menu 5 "$tr_rename_connection_prompt" "") # Pass "" for prompt_icon + + if [ -z "$new_name" ]; then return 1; fi # User cancelled or pressed back + + nmcli connection modify uuid "$connection_uuid" connection.id "$new_name" + return $? +} + +edit_connection_password() { + local connection_uuid="$1" + local connection_name="$2" + + # Prompt for new password + local pass=$(ask_password "$connection_name") + + if [ -z "$pass" ]; then return 1; fi # Cancelled + + # Update the connection + if nmcli connection modify uuid "$connection_uuid" wifi-sec.psk "$pass"; then + send_notification "$tr_password_updated" "$tr_notice_connected_body '$connection_name'" + return 0 + else + send_notification "$tr_password_update_failed" "$tr_notice_error_body '$connection_name'" + return 1 + fi +} + +# Menu for individual connection settings (Wi-Fi) +menu_connection() { + local chosen_connection_name="$1" + local connection_uuid="$2" + local autoconnect_state chosen + + while true; do + autoconnect_state="$([ "$(nmcli --get-values connection.autoconnect connection show "$connection_uuid")" = "yes" ] && echo "$icon_on" || echo "$icon_off")" + + # Check if this connection is currently active + local is_active=$(nmcli -t -f UUID connection show --active | grep -q "$connection_uuid" && echo "yes" || echo "no") + + local options="$icon_automatic $tr_autoconnect_message $autoconnect_state\n" + + if [ "$is_active" = "yes" ]; then + options+="$icon_wifi_disconnected $tr_disconnect_message\n" + else + options+="$icon_connect $tr_connect_now_message\n" + fi + + options+="$icon_ipv4_config $tr_ipv4_config_message\n" + options+="$icon_ipv6_config $tr_ipv6_config_message\n" + options+="$icon_trash $tr_forget_message\n" + options+="$icon_pen $tr_rename_connection_message\n" + options+="$icon_password $tr_edit_password_message\n" # NEW: Edit Password option + # Only show QR code for Wi-Fi connections + if [ "$(nmcli -g connection.type connection show "$connection_uuid")" = "802-11-wireless" ]; then + options+="$icon_qrcode $tr_qrcode_message\n" + fi + options+="$icon_close Back" + + chosen=$(echo -e "$options" | display_menu 1 "$chosen_connection_name" "") # Pass "" for prompt_icon + + if [ -z "$chosen" ] || [[ "$chosen" =~ ^"$icon_close Back" ]]; then + return # User cancelled or pressed back + fi + + case "$chosen" in + *"$tr_autoconnect_message"*) + if [ "$autoconnect_state" = "$icon_on" ]; then + nmcli connection modify uuid "$connection_uuid" autoconnect no + else + nmcli connection modify uuid "$connection_uuid" autoconnect yes + fi + ;; + "$icon_connect $tr_connect_now_message") + show_loading_notification "$tr_connecting_to" + if nmcli connection up uuid "$connection_uuid"; then + kill_loading_notification + send_notification "$tr_notice_connected_summary" "$tr_notice_connected_body '$chosen_connection_name'" + exit 0 + else + kill_loading_notification + # Smart Recovery: Prompt to update password + # Check if user wants to retry with new password + local retry_choice=$(echo -e "$icon_check Yes\n$icon_close No" | display_menu 2 "$tr_connection_failed_retry" "") + if [[ "$retry_choice" =~ ^"$icon_check" ]]; then + if edit_connection_password "$connection_uuid" "$chosen_connection_name"; then + # Retry connection + show_loading_notification "$tr_connecting_to" + if nmcli connection up uuid "$connection_uuid"; then + kill_loading_notification + send_notification "$tr_notice_connected_summary" "$tr_notice_connected_body '$chosen_connection_name'" + exit 0 + fi + kill_loading_notification + fi + fi + send_notification "$tr_notice_error_summary" "$tr_notice_error_body '$chosen_connection_name'" + fi + ;; + "$icon_wifi_disconnected $tr_disconnect_message") + # Disconnect logic + show_loading_notification "Disconnecting..." + if nmcli connection down uuid "$connection_uuid"; then + kill_loading_notification + send_notification "Disconnected" "Disconnected from '$chosen_connection_name'" + return + else + kill_loading_notification + show_message "Failed to disconnect." + fi + ;; + *"$tr_ipv4_config_message"*) menu_ip_config "$chosen_connection_name" "$connection_uuid" "4" ;; + *"$tr_ipv6_config_message"*) menu_ip_config "$chosen_connection_name" "$connection_uuid" "6" ;; + *"$tr_forget_message"*) forget_connection "$chosen_connection_name" "$connection_uuid" && return ;; + *"$tr_rename_connection_message"*) + if rename_connection "$connection_uuid"; then + show_message "Connection renamed." + chosen_connection_name=$(nmcli --get-values connection.id connection show "$connection_uuid") + else + show_message "Failed to rename connection." + fi + ;; + *"$tr_edit_password_message"*) + edit_connection_password "$connection_uuid" "$chosen_connection_name" + ;; + *"$tr_qrcode_message"*) + local ssid=$(nmcli -g 802-11-wireless.ssid connection show "$connection_uuid") + local security=$(nmcli -g 802-11-wireless-security.key-mgmt connection show "$connection_uuid" | sed 's/wpa-psk/WPA/; s/None/nopass/') + local password=$(nmcli -s -g 802-11-wireless-security.psk connection show "$connection_uuid") + show_qrcode "$ssid" "$security" "$password" + ;; + esac + done +} + +# Menu for Wireguard connection settings +menu_wireguard_connection() { + local chosen_connection_name="$1" + local connection_uuid="$2" + local state autoconnect_state chosen + + while true; do + state="$([ "$(nmcli --get-values GENERAL.STATE connection show "$connection_uuid")" = "activated" ] && echo "$icon_on" || echo "$icon_off")" + autoconnect_state="$([ "$(nmcli --get-values connection.autoconnect connection show "$connection_uuid")" = "yes" ] && echo "$icon_on" || echo "$icon_off")" + local options="$icon_plug $tr_wireguard_enable_message $state\n" + options+="$icon_automatic $tr_autoconnect_message $autoconnect_state\n" + options+="$icon_pen $tr_rename_connection_message\n" + options+="$icon_trash $tr_forget_message\n" + options+="$icon_close Back" + + chosen=$(echo -e "$options" | display_menu 1 "$chosen_connection_name" "") # Pass "" for prompt_icon + + if [ -z "$chosen" ] || [[ "$chosen" =~ ^"$icon_close Back" ]]; then + return # User cancelled or pressed back + fi + + case "$chosen" in + *"$tr_wireguard_enable_message"*) + if [ "$state" = "$icon_on" ]; then + nmcli connection down uuid "$connection_uuid" + else + nmcli connection up uuid "$connection_uuid" + fi + ;; + *"$tr_autoconnect_message"*) + if [ "$autoconnect_state" = "$icon_on" ]; then + nmcli connection modify uuid "$connection_uuid" autoconnect no + else + nmcli connection modify uuid "$connection_uuid" autoconnect yes + fi + ;; + *"$tr_rename_connection_message"*) + if rename_connection "$connection_uuid"; then + show_message "Connection renamed." + chosen_connection_name=$(nmcli --get-values connection.id connection show "$connection_uuid") + else + show_message "Failed to rename connection." + fi + ;; + *"$tr_forget_message"*) forget_connection "$chosen_connection_name" "$connection_uuid" && return ;; + esac + done +} + +# Menu for listing and managing known/saved connections +menu_known_connections() { + local connection_filter="$1" + local icon_for_type + local menu_type_function + local profiles_list_raw chosen + local prompt_to_use + + case "$connection_filter" in + "wifi") + icon_for_type="$icon_wireless" + menu_type_function="menu_connection" + profiles_list_raw=$(nmcli --colors no -t -f TYPE,UUID,NAME connection show | awk -F ':' -v icon="$icon_for_type" '$1 ~ /^(wifi|802-11-wireless).*/ {print $2 "\\0" icon " " $3}') + prompt_to_use="$tr_known_connections_message" # Use descriptive text for prompt + ;; + "wireguard") + icon_for_type="$icon_wireguard" + menu_type_function="menu_wireguard_connection" + profiles_list_raw=$(nmcli --colors no -t -f TYPE,UUID,NAME connection show | awk -F ':' -v icon="$icon_for_type" '$1 == "wireguard" {print $2 "\\0" icon " " $3}') + prompt_to_use="$tr_vpn_menu_prompt" # Use new VPN prompt + ;; + "ethernet") + icon_for_type="$icon_ethernet" + menu_type_function="menu_connection" + profiles_list_raw=$(nmcli --colors no -t -f TYPE,UUID,NAME connection show | awk -F ':' -v icon="$icon_for_type" '$1 ~ /^(ethernet|802-3-ethernet).*/ {print $2 "\\0" icon " " $3}') + prompt_to_use="$tr_manage_wired_connections" + ;; + *) # All saved connections + icon_for_type="$icon_saved" + menu_type_function="menu_connection" + profiles_list_raw=$(nmcli --colors no -t -f TYPE,UUID,NAME connection show | awk -F ':' -v icon_saved="$icon_saved" -v icon_wireless="$icon_wireless" -v icon_ethernet="$icon_ethernet" -v icon_vpn_disconnect="$icon_vpn_disconnect" '{ + icon_local = icon_saved; + if ($1 == "wifi" || $1 == "802-11-wireless") icon_local = icon_wireless; + else if ($1 == "ethernet" || $1 == "802-3-ethernet") icon_local = icon_ethernet; + else if ($1 == "vpn" || $1 == "wireguard") icon_local = icon_vpn_disconnect; + print $2 "\\0" icon_local " " $3 " (" $1 ")" + }') + prompt_to_use="$tr_saved_connections_menu_prompt" # Use new Saved Connections prompt + ;; + esac + + mapfile -t profiles_list < <(echo "$profiles_list_raw") + + if [ "${#profiles_list[@]}" -eq 0 ] || ([ "${#profiles_list[@]}" -eq 1 ] && [ -z "${profiles_list[0]}" ]); then + display_info_message "$tr_no_saved_connections" "$prompt_to_use" # Pass custom prompt + return + fi + + while true; do + local options=$(for i in "${profiles_list[@]}"; do echo -e "$i" | cut --delimiter $'\0' --fields 2; done) + options+="\n$icon_close Back" # FIX: Added newline before the Back entry + + # Prompt for this menu is explicitly set by prompt_to_use, no extra icon prepended by display_menu + chosen=$(echo -e "$options" | display_menu 1 "$prompt_to_use" "") # Pass "" for prompt_icon + + if [ -z "$chosen" ] || [[ "$chosen" =~ ^"$icon_close Back" ]]; then + return # User cancelled or pressed back + fi + + for i in "${profiles_list[@]}"; do + if [ "$chosen" = "$(echo -e "$i" | cut --delimiter $'\0' --fields 2)" ]; then + local conn_uuid=$(echo -e "$i" | cut --delimiter $'\0' --fields 1) + local conn_name_display=$(echo "$chosen" | sed -E 's/^(๓ฐ|๓ฐฉ|๓ฐ|๓ฐ) //; s/ \(.+\)$//') # Changed ๓ฐ to ๓ฐ + eval "$menu_type_function \"$(sed 's/"/\\"/g' <<<"$conn_name_display")\" \"$conn_uuid\"" + break + fi + done + done +} + +# NEW: Menu for connecting to a specific wired connection +menu_connect_wired_connection() { + local profiles_list_raw=$(nmcli --colors no -t -f TYPE,UUID,NAME connection show | awk -F ':' -v icon="$icon_ethernet" '$1 ~ /^(ethernet|802-3-ethernet).*/ {print $2 "\\0" icon " " " " $3}') + mapfile -t profiles_list < <(echo "$profiles_list_raw") + + if [ "${#profiles_list[@]}" -eq 0 ] || ([ "${#profiles_list[@]}" -eq 1 ] && [ -z "${profiles_list[0]}" ]); then + display_info_message "No saved wired connections to connect to." "$tr_connect_wired_connection" # Pass custom prompt + return + fi + + local options=$(for i in "${profiles_list[@]}"; do echo -e "$i" | cut --delimiter $'\0' --fields 2; done) + options+="\n$icon_close Back" + + # Prompt for this menu is just the search icon + local chosen=$(echo -e "$options" | display_menu 1 "$tr_connect_wired_connection" "$icon_search") + + if [ -z "$chosen" ] || [[ "$chosen" =~ ^"$icon_close Back" ]]; then + return # User cancelled or pressed back + fi + + local conn_uuid_to_connect="" + local conn_name_to_connect="" + + for i in "${profiles_list[@]}"; do + if [ "$chosen" = "$(echo -e "$i" | cut --delimiter $'\0' --fields 2)" ]; then + conn_uuid_to_connect=$(echo -e "$i" | cut --delimiter $'\0' --fields 1) + conn_name_to_connect=$(echo -e "$i" | cut --delimiter $'\0' --fields 2 | sed -E 's/^(๓ฐ) //') # Remove icon + break + fi + done + + if [ -n "$conn_uuid_to_connect" ]; then + show_loading_notification "$tr_connecting_to '$conn_name_to_connect'..." + if nmcli connection up uuid "$conn_uuid_to_connect"; then + kill_loading_notification + send_notification "$tr_notice_connected_summary" "$tr_notice_connected_body '$conn_name_to_connect'" + exit 0 + else + kill_loading_notification + send_notification "$tr_notice_error_summary" "$tr_notice_error_body '$conn_name_to_connect'" + fi + else + show_message "Could not find connection profile for $conn_name_to_connect." "$tr_connect_wired_connection" # Pass custom prompt + fi +} + +# NEW: Wired Menu +menu_wired() { + while true; do + local active_wired_conn=$(nmcli -t -f TYPE,DEVICE connection show --active | grep -vE "(wireless|vpn|wireguard)" | head -n 1) + local status_line="" + if [ -n "$active_wired_conn" ]; then + local active_device=$(echo "$active_wired_conn" | cut -d':' -f2) + status_line="$icon_ethernet $tr_status_message $tr_status_connected ($active_device)" + else + status_line="$icon_ethernet $tr_status_message $tr_status_disconnected" + fi + + local options="$status_line\n" + options+="$icon_connect_wired Connect to Wired Connection\n" # FIX: Using icon_connect_wired + options+="$icon_config $tr_manage_wired_connections\n" # Option to manage profiles + options+="$icon_close Back" + + local choice=$(echo -e "$options" | display_menu 1 "$tr_wired_menu_prompt" "") # Pass "" for prompt_icon + + if [ -z "$choice" ] || [[ "$choice" =~ ^"$icon_close Back" ]]; then + return # Go back to parent menu (main_menu) + fi + + case "$choice" in + *"Connect to Wired Connection"*) + menu_connect_wired_connection # Call the function to connect to a wired network + ;; + *"$tr_manage_wired_connections"*) + menu_known_connections "ethernet" # Reuse existing function for managing wired profiles + ;; + *"$tr_status_message"*) # Do nothing if status line is selected + ;; + *) + show_message "Invalid option selected: $choice" "$tr_wired_menu_prompt" # Pass custom prompt + ;; + esac + done +} + +# --- Status Functions --- + +show_active_connection_details() { + local device="$1" + local details_raw=$(nmcli --colors no device show "$device") + + # Display the raw list in a filterable menu + local prompt_text="$icon_active_details Active Connection Details: $icon_search" + echo -e "${details_raw}\n${icon_close} Back" | display_menu 1 "$prompt_text" "" +} + +status_menu() { + local options="$icon_active_details Active Connection Details\n$icon_devices All Device Status\n$icon_close Back" + # Use new status menu prompt + local choice=$(echo -e "$options" | display_menu 1 "$tr_status_menu_prompt" "") # Pass "" for prompt_icon + + if [ -z "$choice" ] || [[ "$choice" =~ ^"$icon_close Back" ]]; then + return # User cancelled or pressed back + fi + + case "$choice" in + *"Active Connection Details") + local active_conn_device=$(nmcli -t -f DEVICE connection show --active | head -n 1) + if [ -z "$active_conn_device" ]; then + display_info_message "$tr_no_active_connection" + else + show_active_connection_details "$active_conn_device" + fi + ;; + *"All Device Status") + local device_status=$(nmcli device status) + display_info_message "$device_status" "$tr_status_menu_prompt" # Pass custom prompt + ;; + esac +} + +# --- Airplane Mode --- +toggle_airplane_mode() { + local wifi_state=$(nmcli radio wifi) + local wwan_state=$(nmcli radio wwan 2>/dev/null || echo "disabled") # WWAN may not exist + + if [ "$wifi_state" = "enabled" ] || [ "$wwan_state" = "enabled" ]; then + # Disable all radios + nmcli radio wifi off + nmcli radio wwan off 2>/dev/null + show_message "$tr_airplane_on" "$tr_airplane_mode_message" + else + # Enable all radios + nmcli radio wifi on + nmcli radio wwan on 2>/dev/null + show_message "$tr_airplane_off" "$tr_airplane_mode_message" + fi +} + +# --- QR Code Sharing --- +show_qrcode() { + local ssid="$1" + local security="$2" + local password="$3" + + if ! command -v qrencode &>/dev/null; then + show_message "$tr_qrcode_error" "$tr_qrcode_message" + return + fi + + if [ -z "$password" ]; then + show_message "$tr_qrcode_no_password" "$tr_qrcode_message" + return + fi + + local qr_string="WIFI:T:${security};S:${ssid};P:${password};;" + local qr_file="/tmp/hyprltm-net-qr-${ssid}.png" + + show_loading_notification "$tr_qrcode_generating" + qrencode -o "$qr_file" -s 10 -m 2 "$qr_string" + kill_loading_notification + + # Display QR code inside Rofi + # -i: Enable icons + # -theme-str: Override theme to show large icon and no text input + # Display QR code inside Rofi + # -i: Enable icons + # -show-icons: Required to show icons in dmenu mode + # -theme-str: Override theme to show large icon and no text input + + # We use the successful configuration from the test script + local rofi_override=" + window { width: 500px; } + listview { lines: 1; scrollbar: false; } + element { orientation: vertical; padding: 20px; children: [ element-icon, element-text ]; } + element-icon { enabled: true; size: 300px; horizontal-align: 0.5; } + element-text { horizontal-align: 0.5; } + entry { enabled: false; } + inputbar { enabled: false; } + " + + # We pass an entry with the icon set to the QR file path + # Format: Valid Text \0icon\x1f/path/to/image.png + # We use "Scan to Connect" as the text under the QR code + echo -e "Scan to Connect\0icon\x1f${qr_file}" | + fuzzel --dmenu -i -show-icons -p "$tr_qrcode_message" + + # Cleanup after Rofi closes + rm -f "$qr_file" 2>/dev/null +} + +# Get password for a known Wi-Fi connection +get_wifi_password() { + local ssid="$1" + local uuid=$(nmcli -t -f NAME,UUID connection show | grep "^${ssid}:" | cut -d':' -f2) + if [ -n "$uuid" ]; then + nmcli -s -g 802-11-wireless-security.psk connection show "$uuid" 2>/dev/null + fi +} + +# --- Hotspot Creation --- +create_hotspot() { + local ssid=$(echo "" | display_menu 5 "$tr_hotspot_ssid_prompt" "") + if [ -z "$ssid" ]; then + return + fi + + local password=$(echo "" | display_menu 3 "$tr_hotspot_password_prompt" "") + if [ -z "$password" ] || [ ${#password} -lt 8 ]; then + show_message "Password must be at least 8 characters." "$tr_hotspot_message" + return + fi + + show_loading_notification "$tr_hotspot_creating" + if nmcli device wifi hotspot ifname "$interface_to_use" ssid "$ssid" password "$password"; then + kill_loading_notification + show_message "$tr_hotspot_success\nSSID: $ssid" "$tr_hotspot_message" + else + kill_loading_notification + show_message "$tr_hotspot_error" "$tr_hotspot_message" + fi +} + +# --- VPN Functions --- + +import_vpn() { + local vpn_file_path=$(echo "" | display_menu 5 "$tr_import_vpn_prompt" "") + + if [ -z "$vpn_file_path" ]; then + return + fi + + if [ ! -f "$vpn_file_path" ]; then + send_notification "$tr_notice_import_error_summary" "$tr_notice_file_not_found_body" + return + fi + + local vpn_type="" + case "$vpn_file_path" in + *.conf) vpn_type="wireguard" ;; + *.ovpn) vpn_type="openvpn" ;; + *) + send_notification "$tr_notice_import_error_summary" "$tr_notice_unknown_vpn_type_body" + return + ;; + esac + + if nmcli connection import type "$vpn_type" file "$vpn_file_path"; then + send_notification "$tr_notice_import_success_summary" "$tr_notice_import_success_body" + else + send_notification "$tr_notice_import_error_summary" "$tr_notice_import_error_body" + fi +} + +toggle_vpn_connection() { + local uuid="$1" + local name="$2" + local state="$3" + + if [ "$state" = "activated" ]; then + show_loading_notification "$tr_disconnecting_from '$name'..." + if nmcli connection down uuid "$uuid"; then + kill_loading_notification + send_notification "$tr_notice_disconnected_summary" "$tr_notice_disconnected_body '$name'" + exit 0 + else + kill_loading_notification + send_notification "$tr_notice_error_summary" "$tr_notice_error_disconnect_body '$name'" + fi + else + show_loading_notification "$tr_connecting_to" + if nmcli connection up uuid "$uuid"; then + kill_loading_notification + send_notification "$tr_notice_connected_summary" "$tr_notice_connected_body '$name'" + exit 0 + else + kill_loading_notification + send_notification "$tr_notice_error_summary" "$tr_notice_error_body '$name'" + fi + fi +} + +menu_available_vpns() { + while true; do + mapfile -t vpn_list < <(nmcli --colors no -t -f TYPE,UUID,NAME connection show | awk -F ':' '$1 == "vpn" || $1 == "wireguard" {print $2 "\\0" $3}') + + if [ "${#vpn_list[@]}" -eq 0 ] || [ -z "${vpn_list[0]}" ]; then + display_info_message "$tr_no_configured_vpns" "$tr_available_vpn_profiles_message" + return + fi + + local options="" + for i in "${vpn_list[@]}"; do + local uuid=$(echo -e "$i" | awk 'BEGIN{FS="\x00"}{print$1}') + local name=$(echo -e "$i" | awk 'BEGIN{FS="\x00"}{print$2}') + local state=$(nmcli --get-values GENERAL.STATE connection show uuid "$uuid") + local state_icon="$([ "$state" = "activated" ] && echo "$icon_on" || echo "$icon_off")" + options+="$state_icon $name\n" + done + options+="$icon_close Back" + + local chosen=$(echo -e "$options" | display_menu 1 "$tr_available_vpn_profiles_message" "$icon_search") + + if [ -z "$chosen" ] || [[ "$chosen" =~ ^"$icon_close Back" ]]; then + return + fi + + local chosen_name=$(echo "$chosen" | sed -E 's/^(๏ |๏) //') + for i in "${vpn_list[@]}"; do + local uuid=$(echo -e "$i" | awk 'BEGIN{FS="\x00"}{print$1}') + local name=$(echo -e "$i" | awk 'BEGIN{FS="\x00"}{print$2}') + if [ "$name" = "$chosen_name" ]; then + local state=$(nmcli --get-values GENERAL.STATE connection show uuid "$uuid") + toggle_vpn_connection "$uuid" "$name" "$state" + break + fi + done + done +} + +vpn_menu() { + while true; do + local options="$icon_vpn_disconnect $tr_available_vpn_profiles_message\n" + options+="$icon_import $tr_import_vpn_message\n" + options+="$icon_close Back" + + local choice=$(echo -e "$options" | display_menu 1 "$tr_vpn_menu_prompt" "") + + if [ -z "$choice" ] || [[ "$choice" =~ ^"$icon_close Back" ]]; then + return + fi + + case "$choice" in + *"$tr_available_vpn_profiles_message"*) + menu_available_vpns + ;; + *"$tr_import_vpn_message"*) + import_vpn + ;; + esac + done +} + +main_menu() { + local options="$icon_wifi_full Wi-Fi\n" + options+="$icon_ethernet Wired\n" + options+="$icon_vpn_disconnect VPN\n" + options+="$icon_bookmark_saved Saved Connections\n" + options+="$icon_status_chart Status\n" + options+="$icon_airplane $tr_airplane_mode_message\n" + options+="$icon_close Exit" + + while true; do # Keep looping for the main menu + local choice=$(echo -e "$options" | display_menu 1 "$tr_main_menu_prompt" "") # Pass "" for prompt_icon + + if [ -z "$choice" ]; then + # If user presses Esc on the main menu, exit the script + exit 0 + fi + + case "$choice" in + "$icon_close Exit") exit 0 ;; # Only exit if 'Exit' is explicitly chosen + *"Wi-Fi") + menu_wifi + ;; + *"Wired") + menu_wired # Call the new wired sub-menu + ;; + *"VPN") + vpn_menu + ;; + *"Saved Connections") + menu_known_connections "all" + ;; + *"Status") + status_menu + ;; + *"$tr_airplane_mode_message") + toggle_airplane_mode + ;; + esac + done # End of main menu while loop +} + +# --- Run --- +[ -n "${DEBUG:-}" ] && set -x +main_menu diff --git a/bin/pasm b/bin/pasm new file mode 100755 index 0000000..7555167 --- /dev/null +++ b/bin/pasm @@ -0,0 +1,71 @@ +#!/bin/sh +# local is well supported +# shellcheck disable=3043 +set -eux + +# shellcheck disable=2034 +description="rbw-based password manager" + +FUZL_PROMPT=pasm +FUZL_ICON=๓ฐฒ +. libfuzl.sh + +main() { + rbw unlocked 2>/dev/null || + rbw unlock || exit 1 + + local eid + eid=$(rbw_select_entry) + + fuzl_action_add "copy" "๓ฐ" "copy username/password" + rbw_has_totp "$eid" && fuzl_action_add "totp" "๓ฐ" "copy totp code" + fuzl_action_add "url" "๓ฐน" "copy url" + + fuzl_action_menu "$eid" || exit 1 +} + +# +# helpers +# + +rbw_select_entry() { + rbw list --raw | + jq -r '.[] | select(.type=="Login") | [.id,.name,.user,(.uris[0]//"-")]|join("%")' | + awk -F% '{print $1 "\t" ($2 " โ") "\t" ("๓ฐ " $3) "\t" ("๓ฐน " $4)}' | + fuzl_menu $FUZL_MENU_FILTER --with-nth='{2} {3..}' --accept-nth=1 -w 80 +} + +rbw_has_totp() { + local eid=${1:?missing eid} + # can't use fuzl_try here because we need the exit status + rbw get -l "$eid" | grep -q totp +} +rbw_entry() { + local eid=${1:?missing eid} + fuzl_try -- rbw get --raw "$eid" | jq -r '.name' +} + +# +# actions +# + +pasm_copy() { + local eid=${1:?missing eid} + fuzl_try -- rbw get -f password "$eid" | wl-copy -n + fuzl_try -- rbw get -f username "$eid" | wl-copy -p -n + fuzl_notification "$(rbw_entry "$eid")" "copied to clipboard" +} + +pasm_totp() { + local eid=${1:?missing eid} + fuzl_try -- rbw totp "$eid" | wl-copy -n + fuzl_notification "$(rbw_entry "$eid")" "copied totp to clipboard" +} + +pasm_url() { + local eid=${1:?missing eid} + fuzl_try -- rbw get -f uris "$eid" | head -n1 | wl-copy -n + fuzl_notification "$(rbw_entry "$eid")" "copied url to clipboard" +} + +main diff --git a/bin/play-mpv b/bin/play-mpv new file mode 100755 index 0000000..21f20c6 --- /dev/null +++ b/bin/play-mpv @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 + +""" +This script emulates "unique application" functionality. When starting +playback with this script, it will try to reuse an already running instance of +mpv (but only if that was started with umpv). Other mpv instances (not started +by umpv) are ignored, and the script doesn't know about them. + +This only takes filenames as arguments. Custom options can't be used; the script +interprets them as filenames. If mpv is already running, the files passed to +umpv are appended to mpv's internal playlist. If a file does not exist or is +otherwise not playable, mpv will skip the playlist entry when attempting to +play it (from the GUI perspective, it's silently ignored). + +If mpv isn't running yet, this script will start mpv and let it control the +current terminal. It will not write output to stdout/stderr, because this +will typically just fill ~/.xsession-errors with garbage. + +mpv will terminate if there are no more files to play, and running the umpv +script after that will start a new mpv instance. + +Note: you can supply custom mpv path and options with the MPV environment + variable. The environment variable will be split on whitespace, and the + first item is used as path to mpv binary and the rest is passed as options + _if_ the script starts mpv. If mpv is not started by the script (i.e. mpv + is already running), this will be ignored. +""" + +import os +import shlex +import socket +import string +import subprocess +import sys +from collections.abc import Iterable +from typing import BinaryIO + +from itertools import tee + +def notify(files: Iterable[str]) -> None: + for f in files: + subprocess.Popen([ + 'fyi', + 'play-mpv', + f, + ], start_new_session=True) + +def is_url(filename: str) -> bool: + parts = filename.split("://", 1) + if len(parts) < 2: + return False + # protocol prefix has no special characters => it's an URL + allowed_symbols = string.ascii_letters + string.digits + "_" + prefix = parts[0] + return all(c in allowed_symbols for c in prefix) + +def get_socket_path() -> str: + base_dir = ( + os.getenv("UMPV_SOCKET_DIR") or + os.getenv("XDG_RUNTIME_DIR") or + os.getenv("HOME") or + os.getenv("TMPDIR") + ) + + if not base_dir: + raise Exception("Could not determine a base directory for the socket. " + "Ensure that one of the following environment variables is set: " + "UMPV_SOCKET_DIR, XDG_RUNTIME_DIR, HOME or TMPDIR.") + + return os.path.join(base_dir, ".umpv") + +def send_files_to_mpv(conn: socket.socket | BinaryIO, files: Iterable[str]) -> None: + notify(tee(files)[0]) + + try: + send = conn.send if isinstance(conn, socket.socket) else conn.write + for f in files: + f = f.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") + send(f'raw loadfile "{f}" append-play\n'.encode()) + + except Exception: + print("mpv is terminating or the connection was lost.", file=sys.stderr) + sys.exit(1) + +def start_mpv(files: Iterable[str], socket_path: str) -> None: + [files, files_copy] = tee(files, 2) + notify(files_copy) + + mpv = "mpv" + mpv_command = shlex.split(os.getenv("MPV", mpv)) + mpv_command.extend([ + "--profile=builtin-pseudo-gui", + f"--input-ipc-server={socket_path}", + "--", + ]) + mpv_command.extend(files) + print(mpv_command) + # subprocess.Popen(mpv_command, start_new_session=True) + +def main() -> None: + files = (os.path.abspath(f) if not is_url(f) else f for f in sys.argv[1:]) + socket_path = get_socket_path() + + try: + with socket.socket(socket.AF_UNIX) as sock: + sock.connect(socket_path) + send_files_to_mpv(sock, files) + except (FileNotFoundError, ConnectionRefusedError): + start_mpv(files, socket_path) + +if __name__ == "__main__": + main() diff --git a/bin/play-music b/bin/play-music new file mode 100755 index 0000000..14e91f1 --- /dev/null +++ b/bin/play-music @@ -0,0 +1,23 @@ +#!/bin/sh +set -eux + +music_dir=/run/media/robert/fwdrive/music + +mpa() { + mpv --profile=mpa \ + --directory-filter-types=audio \ + --autocreate-playlist=filter \ + --wayland-app-id=mpv.play-music \ + "$@" +} + +case "$1" in +shuffle) + mpa --shuffle $music_dir + ;; +select) + cd $music_dir + subdir=$(find . -type d -printf "%P\n" | shuf | fuzzel -d -w 80) + mpa "$subdir" + ;; +esac diff --git a/bin/rsyncez b/bin/rsyncez new file mode 100755 index 0000000..0275971 --- /dev/null +++ b/bin/rsyncez @@ -0,0 +1,19 @@ +#!/bin/sh + +set -e +[ -n "$DEBUG" ] && set -x + +[ "$1" = "-mac" ] && shift && mac_args='--rsync-path=/usr/local/bin/rsync' # force brew version +[ "$1" = "-mac2" ] && shift && mac_args='--rsync-path=/opt/homebrew/bin/rsync' # force brew version +exec rsync \ + --verbose \ + -hhh \ + --info=flist2,name,progress \ + --partial \ + --rsh="ssh -x -T -o Compression=yes -o RemoteCommand=none" \ + --archive \ + --protect-args \ + ${mac_args} \ + "$@" + +# -c chacha20-poly1305@openssh.com diff --git a/bin/susm b/bin/susm new file mode 100755 index 0000000..197013a --- /dev/null +++ b/bin/susm @@ -0,0 +1,30 @@ +#!/bin/bash +set -eu + +# shellcheck disable=2034 +description="suspend manager" + +FUZL_PROMPT=susm +. libfuzl.sh + +main() { + local action + + action=$( ( + printf '%s\t%s\n' ๓ฐฒ 'suspend' + printf '%s\t%s\n' ๓ฐค 'reboot' + printf '%s\t%s\n' ๏ 'poweroff' + printf '%s\t%s\n' ๓ฐพ 'lock' + ) | fuzl_menu $FUZL_MENU_FILTER --with='{1} {2}' --accept-nth=2) + + case "$action" in + suspend | poweroff | reboot) + exec loginctl $action + ;; + lock) + exec waylock + ;; + esac +} + +main diff --git a/bin/svcm b/bin/svcm new file mode 100755 index 0000000..b7daf39 --- /dev/null +++ b/bin/svcm @@ -0,0 +1,108 @@ +#!/bin/bash +set -eu + +# shellcheck disable=2034 +description="service manager" + +export FUZL_PROMPT=svcm +export FUZL_ICON=๓ฐฒฝ +. libfuzl.sh + +SVCM_SCOPE=usr + +main() { + fuzl_action_add "usr" "๓ฐ" "User" + fuzl_action_add "sys" "๓ฐ" "System" + fuzl_action_menu || return + + select_service +} + +# +# helpers +# + +select_service() { + local rl + rl=$(runlevel_current) + + local srv + srv=$(get_services | + FUZL_PROMPT="$FUZL_PROMPT [$rl]" fuzl_menu $FUZL_MENU_FILTER || true) + + test -n "$srv" || main + + if [[ "$srv" = runlevel:* ]]; then + runlevel_start "$(cut -c10- <<<"$srv")" + return + fi + + fuzl_action_add "restart" "๓ฐ" "Restart" + fuzl_action_add "start" "๓ฐธ" "Start" + fuzl_action_add "stop" "๏ด" "Stop" + fuzl_action_menu "$srv" || select_service +} + +get_services() { + case "${SVCM_SCOPE:?missing scope}" in + usr) + fuzl_try -- rc-service -U -l | sort -u + find ~/.config/rc/runlevels/ -mindepth 1 -type d -printf 'runlevel:%P\n' + ;; + sys) + fuzl_try -- rc-service -l | sort -u + find /etc/runlevels/ -mindepth 1 -type d -printf 'runlevel:%P\n' + ;; + esac +} + +runlevel_current() { + case "${SVCM_SCOPE:?missing scope}" in + usr) fuzl_try -- rc-status -U -r ;; + sys) fuzl_try -- rc-status -r ;; + esac +} +runlevel_start() { + local rl=${1:?missing runlevel} + case "${SVCM_SCOPE:?missing scope}" in + usr) fuzl_try -- openrc -U "$rl" ;; + sys) fuzl_try -- doas -n openrc "$rl" ;; + esac +} + +# +# actions +# + +svcm_usr() { + SVCM_SCOPE=usr +} +svcm_sys() { + SVCM_SCOPE=sys +} + +svcm_start() { + local srv=${1:?missing service} + case "${SVCM_SCOPE:?missing scope}" in + usr) fuzl_try -- rc-service -U "$srv" start ;; + sys) fuzl_try -- doas -n rc-service "$srv" start ;; + esac +} + +svcm_stop() { + local srv=${1:?missing service} + case "${SVCM_SCOPE:?missing scope}" in + usr) fuzl_try -- rc-service -U "$srv" stop ;; + sys) fuzl_try -- doas -n rc-service "$srv" stop ;; + esac +} + +svcm_restart() { + local srv=${1:?missing service} + case "${SVCM_SCOPE:?missing scope}" in + usr) fuzl_try -- rc-service -U "$srv" restart ;; + sys) fuzl_try -- doas -n rc-service "$srv" stop ;; + esac +} + +main diff --git a/bin/todo b/bin/todo new file mode 100755 index 0000000..7436779 --- /dev/null +++ b/bin/todo @@ -0,0 +1,216 @@ +#!/bin/bash +set -e +[ -n "$DEBUG" ] && set -x + +_todo_files=("todo.txt" "TODO") +_todo_file="" + +function has_todo_file() { + cwd=$(pwd) + for f in "${_todo_files[@]}"; do + test -f "$cwd/$f" && _todo_file="$cwd/$f" && return 0 + done + return 1 +} + +function print_org() { + awk ' +function filename() { + n = split(FILENAME, a, "/") + return a[n] +} +function res() { + return "\033[0m" +} +function bold(s) { + return "\033[1m" s "\033[22m" +} +function strike(s) { + return "\033[9m" s "\033[29m" +} +function red(s) { + return "\033[31m" s res() +} +function green(s) { + return "\033[32m" s res() +} +function blue(s) { + return "\033[34m" s res() +} +function magenta(s) { + return "\033[35m" s res() +} +function osc8(uri, s) { + return "\033]8;;" uri "\033\\\\" s "\033]8;;\033\\" +} +function link(uri) { + return osc8(uri, "\033[2m" uri res()) +} +{ + if ($1 ~ /^#/) next + + // support todotxt files + if (filename() == "todo.txt") { + status = "TODO" + if ($1 ~ /^x/) { + status = "DONE" + $0 = gensub(/^[x] /, "", "1") + $0 = strike($0) + } + + $0 = gensub(/ (\+[^ ]+)/, bold(blue(" \\1")), "g") + $0 = gensub(/ (@[^ ]+)/, bold(blue(" \\1")), "g") + $0 = gensub(/(https?:\/\/[^ ]+)/, link("\\1"), "g") + tasks[status][i++] = $0 + next + } + + // support TODO files + if ($1 ~ /^(-|*)/) { + // detect status strings + match($0, /\s(TODO|WIP|WAITING|DONE)\s/, arr) + + if (filename() == "TODO" && arr[1] == "") { + $0 = gensub(/(^[\w]*(-|\*))/, "\\1 TODO", "1") + } + + $0 = gensub(/(TODO)/, red("\\1"), "1") + $0 = gensub(/(WIP)/, blue("\\1"), "1") + $0 = gensub(/(DONE)/, green("\\1"), "1") + $0 = gensub(/(\?\?\?)/, bold(magenta("\\1")), "1") + tasks[arr[1]][i++] = $0 + } +} +END { + order[1] = "TODO" + order[2] = "WIP" + order[3] = "DONE" + for (i in order) { + for (t in tasks[order[i]]) { + print " " tasks[order[i]][t] + } + } +}' $1 +} + +function print_hr() { + local str cols + local start line end + + str="$1" + [[ -n $str ]] && str=" $str " + cols=$((${COLUMNS:-$(tput cols)} - ${#str} - 3)) + start=$'\e(0' + end=$'\e(B' + line="qqq" + while ((${#line} < $cols)); do line+=$line; done + + printf "โพ" + printf "%s%s%s" "$start" "${line:0:$((cols / 2))}" "$end" + printf "%s%s%s" "$start" "${line:0:$((cols / 2))}" "$end" + printf "%s" "$str" + printf "%s%s%s" "$start" "${line:0:1}" "$end" + printf "โผ" + printf "\n" +} + +function print_shell_hook() { + case $1 in + bash) + cat <<-EOF + # Hook to run todo on directory change. + __todo_oldpwd="\$(\builtin pwd -L)" + + function __todo_hook() { + \builtin local -r retval="$?" + \builtin local pwd_tmp + pwd_tmp="\$(\builtin pwd -L)" + if [[ \${__todo_oldpwd} != "\${pwd_tmp}" ]]; then + __todo_oldpwd="\${pwd_tmp}" + \command todo + fi + return "\${retval}" + } + + # Initialize hook. + if [[ \${PROMPT_COMMAND:=} != *'__todo_hook'* ]]; then + PROMPT_COMMAND="__todo_hook;\${PROMPT_COMMAND#;}" + fi + + # Run at startup. + \command sleep .1 && \command todo + EOF + ;; + zsh) + cat <<-EOF + __todo_oldpwd="$(\builtin pwd -L)" + function __todo_hook() { + \builtin local -r retval="$?" + \builtin local pwd_tmp + if [[ \${__todo_oldpwd} != "\${pwd_tmp}" ]]; then + __todo_oldpwd="\${pwd_tmp}" + \command ${0} + fi + return "\${retval}" + } + typeset -ag precmd_functions; + if [[ -z \${precmd_functions[(r)__todo_hook]} ]]; then + precmd_functions+=__todo_hook; + fi + EOF + ;; + cd) + echo "cd() { builtin cd \"\$@\" && $0; }" + ;; + -h | --help | *) + echo "usage: todo hook <integration>" + echo "" + echo "use like this: eval \$(todo hook <integration>)" + echo "" + echo "supported integrations:" + echo " - zsh: run todo as a precmd hook" + echo " (this can get quite annoying when you're in a direcory that has a todo file)" + echo " - cd: run todo as a post-cd hook" + echo "" + exit 1 + ;; + esac +} + +function main() { + case "$1" in + hook) + shift + print_shell_hook "$1" + ;; + -h | --help) + echo "usage: todo [-r] [hook <integration>]" + echo "A quite stupid todo/agenda script that reminds you of things you need to do" + echo "" + echo " -r, --recursive walk the file tree, searching for todos" + echo " <integration> shell integration, you should eval this" + echo "" + exit 0 + ;; + -r | --recursive | -R) + while read FILE; do + print_hr "[$FILE]" + print_org $FILE + done < <(rg --files ${_todo_files[@]/#/-g }) + ;; + *) + if [ -f "$1" ]; then + print_hr "[$1]" + print_org $1 + exit 0 + fi + if has_todo_file; then + print_hr "[$(basename $_todo_file)]" + print_org $_todo_file + print_hr + fi + ;; + esac +} + +main $@ diff --git a/bin/urlopen b/bin/urlopen new file mode 100755 index 0000000..2b0479c --- /dev/null +++ b/bin/urlopen @@ -0,0 +1,89 @@ +#!/bin/bash +set -eu + +# shellcheck disable=2034 +description="url handler" + +FUZL_PROMPT=urlopen +. libfuzl.sh + +main() { + case "$#" in + 1) + fuzl_action_add "open" "๏" "open" + fuzl_action_add "ask" "๓ฐฎซ" "ask" + fuzl_action_add "clipboard" "๓ฐ " "clipboard" + fuzl_action_add "primary" "๓ฑ" "primary" + fuzl_action_menu "$(get_text "$*")" + ;; + 2) + local action + + action=${1:?missing action} + shift + eval "urlopen_$action" "'$(get_text "$*")'" + ;; + esac +} + +# +# helpers +# + +get_text() { + local text="$*" + [ -z "$text" ] && read -r text + printf '%s' "$text" +} + +get_mimetype() { + local text=${1:?missing url} + case "$text" in + http*) printf %s 'x-scheme-handler/http' ;; + *) file -bdE --mime-type "$text" ;; + esac +} +open_default() { + local text=${1:?missing url} + local application + case "$(get_mimetype "$text")" in + x-scheme-handler/http*) application=librewolf ;; + esac + case "$text" in + *linear.app*) application=linear ;; + *) application=librewolf ;; + esac + if [ -z "$application" ]; then + gio open "$text" + else + desktop_file=$(rg -Lil -e "$application" -- /usr/share/applications/ /var/lib/flatpak/exports/share/applications/ ~/.local/share/applications/ | tail -1) + gio launch "$desktop_file" "$text" + fi +} +# +# actions +# + +urlopen_ask() { + local text=${1:?missing url} + env porta open-uri "$text" ask +} + +urlopen_open() { + local text=${1:?missing url} + open_default "$text" +} + +urlopen_clipboard() { + local text=${1:?missing url} + wl-copy -n "$text" + # fuzl_notification "copied to clipboard" "$text" +} + +urlopen_primary() { + local text=${1:?missing url} + wl-copy -np "$text" + # fuzl_notification "copied to primary" "$text" +} + +main $@ diff --git a/bin/vpnm b/bin/vpnm new file mode 100755 index 0000000..f09b41a --- /dev/null +++ b/bin/vpnm @@ -0,0 +1,124 @@ +#!/bin/bash +set -eu + +# shellcheck disable=2034 +description="vpn manager" + +FUZL_PROMPT=vpnm +FUZL_ICON=๓ฐฆ +. libfuzl.sh + +main() { + local conn + + conn=$(get_current_connection) + [ -n "$conn" ] && fuzl_action_add "down" "๓ฐฆ" "Connected to $conn" + fuzl_action_add "networks" "๓ฐพฐ" "Available networks" + fuzl_action_add "profile" "๓ฑผ" "Switch profile" + + fuzl_action_menu || return +} + +# +# helpers +# + +get_emoji_flag() { + code= + # TODO: calc flag + # U\1F1E6 A + # U\200D zero-w joiner + case "$*" in + au-*) code=$'๐ฆ' ;; + be-*) code=$'๐ง๐ช' ;; + br-*) code=$'๐ง๐ท' ;; + ca-*) code=$'๐จ๐ฆ' ;; + de-*) code=$'๐ฉ๐ช' ;; + fr-*) code=$'๐ซ๐ท' ;; + gb-*) code=$'๐ฌ๐ง' ;; + jp-*) code=$'๐ฏ๐ต' ;; + no-*) code=$'๐ณ๐ด' ;; + se-*) code=$'๐ธ๐ช' ;; + us-*) code=$'๐บ๐ธ' ;; + *tail2d6ce*) code=$'๓ฐ ' ;; + *) code=$'๐' ;; + esac + echo -n $code +} + +get_vpn_connections() { + ( + tailscale exit-node list | + awk '/.tail2d6ce.ts.net/{print $2, $1}' + tailscale exit-node list | + awk -F '[ ]{2,}' '/.mullvad.ts.net/{print $2, $1, ($3 "/" $4)}' | + sort -u + ) | while read -r conn ip location; do + printf '%s\t' "$ip" + printf '%s\t' "$(get_emoji_flag "$conn")" + if grep -q 'mullvad.ts.net' <<<"$conn"; then + printf '%s\t' "$location" + else + printf '%s\t' "$conn" + fi + printf '\n' + done +} + +get_current_connection() { + tailscale exit-node list | + grep -e selected | + grep -v -e Any | ( + awk '/.tail2d6ce.ts.net/{print $2, $1}' + awk -F '[ ]{2,}' '/.mullvad.ts.net/{print $2, $1, ($3 "/" $4)}' + ) | while read -r conn ip location; do + printf '%s' "$(get_emoji_flag "$conn")" + printf ' ' + if grep -q 'mullvad.ts.net' <<<"$conn"; then + printf '%s' "$location" + else + printf '%s' "$conn" + fi + done +} + +vpnup() { + fuzl_try -- tailscale set --exit-node=${1:?} + fuzl_try -- pkill -RTMIN+2 -x waybar +} + +vpndown() { + fuzl_try -- tailscale set --exit-node= + fuzl_try -- pkill -RTMIN+2 -x waybar +} + +# +# actions +# + +vpnm_down() { + fuzl_try -- vpndown +} + +vpnm_networks() { + ip=$(get_vpn_connections | + fuzl_menu $FUZL_MENU_FILTER --with-nth='{2} {3}' --accept-nth=1) + + [ -n "$ip" ] || main + fuzl_try -- vpnup "$ip" + +} + +vpnm_profile() { + profile=$(tailscale switch --list 2>/dev/null | + tail -n+2 | + awk '{printf "%s\t%s\n", $3, $2}' | + fuzl_menu $FUZL_MENU_FILTER --accept-nth=1) + + [ -n "$profile" ] || main + fuzl_try -- tailscale switch "$profile" + sleep 2 + fuzl_try -- pkill -SIGRTMIN+2 waybar +} + +main |