Skip to content
/ scripts Public

Collection of composable, simple, and hopefully useful scripts.

License

Notifications You must be signed in to change notification settings

cmpitg/scripts

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

bin

Table of Contents

Script collection. Many of them are written in Rc shell, for Bourne-or-Bash-compatible shell suffers from many design flaws, making it difficult to maintain.

This document is written in literate programming style. To generate scripts and documentation, you need latest stable version of Ulquikit. You could also clone the repo and start using it yourself. All executables are in bin/.

TODO: Script to display a persistent notification for current desktop

TODO: Explain how commands are built the way they are built below: * When to pass as argumment? When to pass into stdin? * When to output as text? Human-readable? * What about exit code?

TODO: Help text for all commands

TODO: fzf integration; use cases: convert-to-* + fzf

TODO: GNU parallel integration

TODO: Write about shell design flaws

1. Requirements

For everything to function correctly, you need:

Some scripts depend on others. It’s best to fulfill the requirements for all of them.

2. Notes

My environment is unusual:

  • ${HOME}/Data is either a symlink or a mount point, pointing to all configuration and data belonging to the tools I use.

    If you have a separated ${HOME}, you just need to create the Data directory.

    The main reasons why I don’t use separated partition for ${HOME} is because: 1) ${HOME} itself is extremely inconsistent and cluttered (.config, .local, dot files, capticalized names vs. lower-case names, etc.); and 2) I use serveral distros, where each piece of software is slightly different in versions, thus different in configuration.

    In my main system, /home/cmpitg/Data is a symlink to /mnt/home/cmpitg, whereas /mnt/home is a mount point.

  • ${HOME}/Data/Mount-Points contains a collection of shortcuts to various directories, and /m is its symlink into /.

    I like to separate the original directories from their shortcuts and to make things globally visible. Some might argue that this is a serious security flaw. I disagree. Private things should be kept away. Your .ssh should never stay in /m.

  • /m/${USER} is a symlink back to ${HOME}/Data, so all symlinks in /m can utilize /m/${USER} itself.

    Symlinks are very useful if used appropriately (examples include the Nix package manager). For me, using /m/src is much more effective and unified than ~/src for ~ depends on what your current user is. I could also re-symlink /m/src whenever I with minimal effects on other parts of the system.

    ✗ l /m
    lrwxrwxrwx 1 root root 30 Nov 28 20:24 /m -> /home/cmpitg/Data/Mount-Points/
    
    ✗ l /m/
    total 12K
    drwxr-xr-x  4 cmpitg cmpitg 4.0K Nov 28 22:04 ./
    drwxr-xr-x 29 cmpitg cmpitg 4.0K Dec  1 23:44 ../
    dr-xr-xr-x  1 cmpitg cmpitg    0 Jan  1  1970 9p-fonts/
    drwxr-xr-x  2 cmpitg cmpitg 4.0K Nov 23 22:23 acme/
    lrwxrwxrwx  1 cmpitg cmpitg   13 Nov 28 22:01 bin -> /m/cmpitg/Bin/
    lrwxrwxrwx  1 cmpitg cmpitg   17 Nov 28 20:22 cmpitg -> /home/cmpitg/Data/
    lrwxrwxrwx  1 cmpitg cmpitg   16 Feb 15  2015 config -> /m/cmpitg/Config/
    lrwxrwxrwx  1 cmpitg cmpitg   13 Nov 28 22:02 opt -> /m/cmpitg/Opt/
    lrwxrwxrwx  1 cmpitg cmpitg   17 Aug  3  2014 scratch -> /m/cmpitg/Scratch/
    lrwxrwxrwx  1 cmpitg cmpitg   18 Nov 28 22:04 src -> /m/cmpitg/Src/
    lrwxrwxrwx  1 cmpitg cmpitg   15 Feb 15  2015 talks -> /m/cmpitg/Talks/
    lrwxrwxrwx  1 cmpitg cmpitg   17 Aug  3  2014 toolbox -> /m/cmpitg/Toolbox/
    lrwxrwxrwx  1 cmpitg cmpitg   22 Aug  3  2014 virtenvs -> /m/cmpitg/Virtual-Envs/
    lrwxrwxrwx  1 cmpitg cmpitg   18 Nov 28 20:35 www -> /m/cmpitg/WWW/

    Most directories should speak for themselves. Exceptions include:

    • /m/9p-fonts: mounted by Plan9port’s fontsrv to serve fonts, and

    • /m/acme: file system interface of Acme.

3. Installation

For installation of Plan9port, please refer to its original documentation. Below is one example session for Debian-based distros:

sudo apt install -y build-essential libfreetype6-dev libx11-dev libx11-xcb-dev git libxt-dev xorg-dev xserver-xorg-dev
cd /path/to/src/
git clone https://github.com/9fans/plan9port.git
cd plan9port
./INSTALL

# No need to add plan9port/bin to PATH as the `9` script below is used to
# invoke Plan 9 applications

For Emacs, Python, and Noto Font, please consult your distro’s documentation. Example with Debian:

sudo apt install python3 fonts-noto emacs25

4. Contents

4.1. bash-pure - runs Bash in a clean environment

file::bash-pure
#!/usr/bin/env tclsh

package require Tclx

proc removeElementsWithPattern {xs pattern} {
    set res {}
    foreach x $xs {
        if {![string match $pattern $x]} {
            lappend res $x
        }
    }
    return $res
}

foreach {var val} [array get env] {
    set newVals [removeElementsWithPattern [split $val :] *guix*]
    set ::env($var) [join $newVals :]
}

execl bash [list --login --noprofile --norc {*}$::argv]

4.2. with-env-pure - runs a command under bash-pure

file::with-env-pure
#!/usr/bin/env bash-pure

exec "$@"

4.3. bash-user - runs Bash with the current user’s environment

file::bash-user
#!/usr/bin/env bash-pure

# All hell break loose without the following!!!
# Why is the varible set to empty???
unset GDK_PIXBUF_MODULE_FILE

if [ -f ~/.env ]; then
	. ~/.env
fi
if [ -f ~/.env-prog ]; then
	. ~/.env-prog
fi

exec bash "$@"

4.4. zsh-user - runs Zsh with the current user’s environment

file::zsh-user
#!/usr/bin/env bash-user

exec zsh "$@"

4.5. with-env-user - runs a command under bash-user

TODO: Doc: For a single command, e.g. shell operators don’t work

file::with-env-user
#!/usr/bin/env bash-user

exec "$@"

4.5.1. Testing scenarios

with-env-user which with-env-user ';' aoeu ';' pwd '&&' with-pause true

4.6. with-env-user-sh - runs a shell command under bash-user

TODO: Doc: shell operators work

file::with-env-user-sh
#!/usr/bin/env bash-user

exec "${SHELL}" -c "$*"

4.6.1. Testing scenarios

The following should output: * Path to with-env-user * Command not found aoeu * Current path * Then pause and wait for input

with-env-user-sh which with-env-user ';' aoeu ';' pwd '&&' with-pause true

4.7. conda/ - utils for Python Conda

4.7.1. conda/with-env - runs a command in the Conda environment

file::conda/with-env
#!/usr/bin/env bash

CONDA_PATH="${CONDA_PATH:-/m/opt/miniconda3}"
export PATH="${CONDA_PATH}/bin:${PATH}"

if ! report-missing-executables conda Conda; then
	exit 1
fi

# eval "$(conda shell.bash hook)"

exec "$@"

4.8. guix/ - utils for the Guix package manager

4.8.1. guix/with-env - runs a command in the Guix environment

file::guix/with-env
#!/usr/bin/env bash-user

set +x

# Do not re-read user env
export _READ_USER_ENV_=0

export GUIX_LOCPATH="${HOME}/.guix-profile/lib/locale"
export GUIX_LD_WRAPPER_ALLOW_IMPURITIES=n

export PATH="${HOME}/.config/guix/current/bin:${PATH}"
export INFOPATH="${HOME}/.config/guix/current/share/info:${INFOPATH}"
export GUIX_PROFILE="${HOME}/.guix-profile"
[[ -e "${HOME}/.config/guix/current/etc/profile" ]] && . "${HOME}/.config/guix/current/etc/profile"
[[ -e "${HOME}/.guix-profile/etc/profile" ]] && . "${HOME}/.guix-profile/etc/profile"

XDG_DATA_DIRS="${XDG_DATA_DIRS:-${HOME}/.local/share}"
XDG_DATA_DIRS="${XDG_DATA_DIRS}:/usr/share:/usr/local/share:${HOME}/.local/share"
export XDG_DATA_DIRS

unset _READ_USER_ENV_

exec "$@"

4.9. chruby/ - utils for chruby

4.9.1. with-env - runs a command in chruby environment

file::chruby/with-env
#!/usr/bin/env bash

if [[ -z "${RUBY_CHRUBY_VERSION}" ]]; then
	echo "RUBY_CHRUBY_VERSION not defined, aborting..." >&2
	exit 3
fi

# Only run with Bash or Zsh
if [[ -n "${BASH}" || -n "${ZSH_NAME}" ]]; then
	if [[ -f /usr/local/share/chruby/chruby.sh && -f /usr/local/share/chruby/auto.sh ]]; then
		. /usr/local/share/chruby/chruby.sh
		. /usr/local/share/chruby/auto.sh
		chruby ${RUBY_CHRUBY_VERSION}
	fi
fi

exec "$@"

4.10. sh - utils for shell scripting

4.10.1. print-colors - print all colors

file::sh/print-colors
#!/usr/bin/env bash

for i in {0..255}; do
	printf "\x1b[38;5;${i}mcolour${i}\x1b[0m\n"
done

4.11. git/ - utils for Git version control

4.11.1. rm-orphaned - removes orphaned files

file::git/rm-orphaned
#!/usr/bin/env sh

git ls-files --deleted | xargs git rm --cached

4.11.2. show-merge-diff - `diff`s as if a merge is to be performed

TODO: * Help text * Error if missing arguments

file::git/show-merge-diff
#!/usr/bin/env sh

base_branch_="${1}"
head_branch_="${2}"

git merge-tree $(git merge-base "${base_branch_}" "${head_branch_}") "${base_branch_}" "${head_branch_}"

4.12. graalvm/ - utils for GraalVM

4.12.1. with-env - runs a command with Java from GraalVM

file::graalvm/with-env
#!/usr/bin/env bash

export JAVA_HOME=/m/opt/graalvm
export PATH="${JAVA_HOME}/bin:${PATH}"

exec "$@"

4.13. inferno/ - utils for Inferno OS

4.13.1. with-env - runs a command with Inferno OS environment

file::inferno/with-env
#!/usr/bin/env bash

set -o nounset

# export EMU='-g800x600 -c1'
export EMU='-g800x600'
export PATH="${INFERNO_OS_ROOT}/Linux/386/bin:${PATH}"

exec "$@"

4.13.2. start-emu - starts the emulator with some config

file::inferno/start-wm
#!/usr/bin/env sh

set -o nounset

export user=cmpitg

exec emu /dis/wm/wm.dis wm/logon -n "/usr/${user}/namespace" -u "${user}" "$@"

4.13.3. sh.dis - runs the shell

file::inferno/sh.dis
#!/usr/bin/env sh

set -o nounset

export user=cmpitg

exec emu /dis/sh.dis "$@"

4.14. with-pause - runs a command, then pause and wait for user to input Enter to exit

file::with-pause
#!/usr/bin/env bash

"$@"
read -p "Press Enter to exit..."

4.15. 9 - sets up the environment for Plan9port applications

  • Starts and mounts 9p font server to /m/9p-fonts

  • Creates temporary directory: /tmp/9-${USER}

  • And executes a command in a Plan9port environment in ${PLAN9}/bin. If PLAN9 variable is not set, it is set to /m/opt/plan9port by default.

TODO: Customize plumber dir TODO: Proper docs

file::9
#!/usr/bin/env bash

##
## Sets up the environment for Plan9port applications:
## * Starts plumber and font server
## * Runs the corresponding program
##

export TEMP9="${TEMP9:-/tmp/9-${USER}}"
export PLAN9="${PLAN9:-/m/opt/plan9port}"
export PATH="${PLAN9}/bin:${PATH}"
export PLAN9_FONTDIR="${PLAN9_FONTDIR:-/m/9p-fonts}"

export SHELL=rc
export TERM=9term
# export font="${PLAN9_FONTDIR}/GoMono/11a/font"

mkdir -p "${TEMP9}"
mkdir -p "${PLAN9_FONTDIR}"

plumber 2>/dev/null
nohup fontsrv -m "${PLAN9_FONTDIR}" >"${TEMP9}/fontsrv.out" 2>"${TEMP9}/fontsrv.err" &

exec ${PLAN9}/bin/9 "$@"

4.16. with-retry <timeout> <times> <cmd>…​ - runs a command, retrying on error

TODO: Help text

file::with-retry
#!/usr/bin/env tclsh

set timeout [lindex $::argv 0]
set times [lindex $::argv 1]
set cmd [lrange $::argv 2 end]

proc execCmd {cmd} {
    return [exec {*}$cmd <@ stdin >@ stdout 2>@ stderr]
}

if {[catch {execCmd $cmd}]} {
    while {$times > 0} {
        puts stderr [format "Command failed, retrying (times=%s) after %sms" $times $timeout...]
        after $timeout
        if {[catch {execCmd $cmd}]} {
            incr times -1
        } else {
            exit 0
        }
    }
    exit 1
} else {
    exit 0
}

4.17. 9-term - runs Plan9port terminal emulator within a 9 environment

file::9-term
#!/usr/bin/env sh

#
# Starts 9term within an Rc environment.
#

exec 9 9term $*

4.18. 9-rc - runs RC shell with Plan 9 Port environment

file::9-rc
#!/usr/bin/env sh

exec 9 rc "$@"

4.19. 9-acme - runs Plan9port Acme with 9

Font can be chosen by setting the font environment variable.

file::9-acme
#!/usr/bin/env 9-rc

#
# Starts Acme with font specified by variable `font'.  By default, use Go Mono.
#

if (~ $font '') {
	font='/m/9p-fonts/CascadiaCode-Regular/11a/font'
}

mkdir -p /m/acme

acme -a -m /m/acme -f $font $*

4.20. Terminal helpers

4.20.1. term-emu - wrapper for terminal emulator

If X is not running, just run the command with the default Bourne-compatible shell.

TODO: Run always ${SHELL} -c

file::term-emu
#!/usr/bin/env tclsh

package require Tclx

# Terminator has some memory leaks, throws GTK error messages to the console
# GNOME terminal doesn't handle mouse scrolling well
# XFCE4 terminal crashes randomly (under load?)
# konsole -e <cmd>
# xfce4-terminal -x <cmd>
# deep-exec guix/with-env kitty <cmd>

if {[info exists ::env(MY_TERM_EMU)]} {
	set ::TERM_EMU $::env(MY_TERM_EMU)
} else {
	set ::TERM_EMU x-terminal-emulator
}

if {[catch {exec using-x-p <@ stdin >@ stdout 2>@ stderr}]} {
	set ::args {}
	foreach x $::argv {
		lappend ::args "\"$x\""
	}
	execl $::env(SHELL) [list -c [join $::args " "]]
} else {
	execl $::TERM_EMU [list -e {*}$::argv]
}

4.20.2. with-term-emu-sh - runs a shell command in a terminal emulator

TODOs

  • Running without X?

  • Documentation

  • Doc: - <args..> → Arguments will be merged into one, executed in a shell (shell ops like && works)

file::with-term-emu-sh
#!/usr/bin/env tclsh

if {[catch {exec report-missing-executables tmux Tmux <@ stdin >@ stdout 2>@ stderr}]} {
    exit 1
}

package require Tclx

if {[lsearch $::argv "-"] == -1} {
    set ::args [list "-" {*}$::argv]
} else {
    set ::args $::argv
}

set ::withoutTermux 0
set ::detachTermux 0
set ::pauseAfterExec 0
set ::verbose 0
set ::shell $::env(SHELL)
set ::mainCmd [lrange $::argv [expr {[lsearch $::argv "-"] + 1}] end]

foreach arg $::args {
    if {$arg eq "-"} {
        break
    }

    switch $arg {
        --verbose {
            set ::verbose 1
        }
        --without-termux {
            set ::withoutTermux 1
        }
        --detach-termux {
            set ::detachTermux 1
        }
        --pause-after-exec {
            set ::pauseAfterExec 1
        }
        default {
            error "Error: Unknown argument $arg"
        }
    }
}

if {$::withoutTermux && $::detachTermux} {
    error "--without-termux and --detach-termux cannot go together"
}

if {$::verbose} {
    puts "Without termux: $::withoutTermux"
    puts "Detach termux: $::detachTermux"
    puts "Pause after execution: $::pauseAfterExec"
    puts "Shell: $::shell"
    puts "Command: $::mainCmd"
}

##############################################################################
# Main
##############################################################################

if {$::withoutTermux} {
    if {$::pauseAfterExec} {
        execl term-emu [list with-pause $::shell -c $::mainCmd]
    } else {
        execl term-emu [list $::shell -c $::mainCmd]
    }
}

set ::termuxWindowName [exec echo $::mainCmd | sed -r {s/:/COLON/g ; s/\\./DOT/g ; s/\(/OPEN_P/g ; s/\)/CLOSE_P/g ; s/\{/OPEN_C/g ; s/\}/CLOSE_C/g ; s/[[:space:]]+/_/g}]

if {$::verbose} {
    puts "Termux window name: $::termuxWindowName"
}

if {$::detachTermux} {
    if {$::pauseAfterExec} {
        execl term-emu [list with-termux -n $::termuxWindowName with-pause $::shell -c $::mainCmd \; detach]
    } else {
        execl term-emu [list with-termux -n $::termuxWindowName $::shell -c $::mainCmd \; detach]
    }
} else {
    if {$::pauseAfterExec} {
        execl term-emu [list with-termux -n $::termuxWindowName with-pause $::shell -c $::mainCmd]
    } else {
        execl term-emu [list with-termux -n $::termuxWindowName $::mainCmd]
    }
}
Testing scenarios
  • See a flash of a term emu

    with-term-emu-sh --without-termux - 'pwd && pwd'
  • See a term emu, paused

    with-term-emu-sh --without-termux - 'pwd && pwd ; with-pause true'
    with-term-emu-sh --without-termux --pause-after-exec - 'pwd && pwd'
  • See a term emu, paused, with the failed command

    with-term-emu-sh --without-termux - 'aoeu && pwd ; with-pause true'
    with-term-emu-sh --without-termux --pause-after-exec - 'aoeu && pwd'
  • See a term emu with termux, paused

    with-term-emu-sh - 'pwd && pwd ; with-pause true'
    with-term-emu-sh - 'aoeu && pwd ; with-pause true'
    with-term-emu-sh --pause-after-exec - 'aoeu && pwd'
  • See a flash of a term emu, but could find the session in termux manually

    with-term-emu-sh --detach-termux - 'pwd && pwd ; with-pause true'
    with-term-emu-sh --detach-termux --pause-after-exec - 'pwd && pwd'
    
    with-term-emu-sh --detach-termux - 'aoeu && pwd ; with-pause true'
    with-term-emu-sh --detach-termux --pause-after-exec - 'aoeu && pwd'

4.20.3. with-termux - runs a command with a terminal multiplexer

file::with-termux
#!/usr/bin/env sh

if using-x-p; then
	exec tmux new-session "$@"
else
	exec tmux new-window "$@"
fi

4.20.4. with-mux-session - runs a command in a term multiplexer session

file::with-mux-session
#!/usr/bin/env tclsh

package require Tclx

# TODO: Help text

if {[catch {exec report-missing-executables tmux Tmux <@ stdin >@ stdout 2>@ stderr}]} {
    exit 1
}

if {[lsearch $::argv "-"] == -1} {
    puts stderr "ERROR: Invalid command, needs to have -"
    exit 2
}

if {[lindex $::argv 0] eq "-"} {
    execl tmux [list new-window {*}[lrange $::argv 1 end]]
} else {
    set sessionName [lindex $::argv 0]
    execl tmux [list new-window -t $sessionName {*}[lrange $::argv 2 end]]
}

4.20.5. bring-termux-session - brings a terminal emulator session here

file::bring-termux-session
#!/usr/bin/env sh

exec with-term-emu-sh --without-termux - tmux attach -t "$@"

4.21. newline

file::newline
#!/usr/bin/env sh

printf "\n"

4.22. gnu-sed - specifically calls the GNU implementation of sed

file::gnu-sed
#!/usr/bin/env bash

report-missing-executables sed "GNU sed" || exit 1

if (/bin/sed --version 2>/dev/null | head -1 | grep sed &>/dev/null); then
	exec /bin/sed "$@"
elif (sed --version 2>/dev/null | head -1 | grep sed &>/dev/null); then
	exec sed "$@"
else
	echo You don\'t have GNU sed installed. >&2
	exit 1
fi

4.23. gnu-tar - specifically calls the GNU implementation of Tar

file::gnu-tar
#!/usr/bin/env bash

report-missing-executables tar "GNU tar" || exit 1

if (/bin/tar --version 2>/dev/null | head -1 | grep tar &>/dev/null); then
	exec /bin/tar "$@"
elif (tar --version 2>/dev/null | head -1 | grep tar &>/dev/null); then
	exec tar "$@"
else
	echo You don\'t have GNU tar installed. >&2
	exit 1
fi

4.24. gnu-tr - attempts to call the GNU implementation of tr

file::gnu-tr
#!/usr/bin/env bash

report-missing-executables tar "GNU tr" || exit 1

if (/bin/tr --version 2>/dev/null | head -1 | grep tr &>/dev/null); then
	exec /bin/tr "$@"
elif (/usr/bin/tr --version 2>/dev/null | head -1 | grep tr &>/dev/null); then
	exec /usr/bin/tr "$@"
else
	echo You don\'t have GNU tr installed. >&2
	exit 1
fi

4.25. upcase - upcases a string (read from stdin)

file::upcase
#!/usr/bin/env sh

exec gnu-tr '[:lower:]' '[:upper:]'

4.26. downcase - downcases a string (read from stdin)

file::downcase
#!/usr/bin/env sh

exec gnu-tr '[:upper:]' '[:lower:]'

4.27. view-man - views a Man page in a GUI pager (Rmacs)

file::view-man
#!/usr/bin/env tclsh

set page [lindex $::argv 0]

exec rmacs --new-frame eval "(let ((frame (selected-frame)))
  (man \"$page\")
  (delete-frame frame)
  (setq-local local/delete-frame-on-close t))" <@ stdin >@ stdout 2>@ stderr

4.28. rmacs-pager - uses Rmacs GUI as a poor man’s pager

TODO: Description and potential improvement

file::rmacs-pager
#!/usr/bin/env tclsh

package require Tcl 8
package require fileutil 1.15

set tempPath [::fileutil::tempfile]
set tempBufferName [exec random-string]

if {$::argc == 0} {
	set serverName pager
} else {
	set serverName [lindex $::argv 0]
}

exec cat > $tempPath <@ stdin
exec rmacs --name $serverName --new-frame eval "(with-current-buffer (get-buffer-create \"$tempBufferName\")
  (insert-file \"$tempPath\")
  (delete-file \"$tempPath\" nil)
  (setq-local local/delete-frame-on-close (selected-frame))
  (Man-cleanup-manpage)
  ;; (Man-fontify-manpage)
  (switch-to-buffer (current-buffer)))" >@ stdout 2>@ stderr

4.29. cat-which <executable> - cat $(which executable-1 [executable-2] […​])

file::cat-which
#!/usr/bin/env bash

#
# Finds full path executables and displays the content.
#


for exec_ in "$@"; do
	if $(which "${exec_}" &>/dev/null); then
		cat $(which "${exec_}")
	else
		echo "${exec_} not found" >&2
	fi
done
file::check-broken-symlinks
#!/usr/bin/env bash

#
# Checks for broken symlinks.
#

for file_ in "$@" ; do
	if [ -L "${file_}" ]; then
		if readlink -q "${file_}" >/dev/null ; then
			echo "Good link: ${file_}"
		else
			echo "${file_}: bad link" >/dev/stderr
		fi
	else
		echo "${file_} is not a symlink"
	fi
done
file::symlink
#!/usr/bin/env sh

if [ "$#" -eq 1 ] && [ "${1}" = "--help" ]; then
	echo "Usage: ${0} <source> <destination>

Symlink <source> to <destination>.  If <destination> ends with a slash (/), it indicates a directory and the symlink is put in the directory.  Otherwise, prompt for overwriting <destination> if exists.  In case that <source> is a symlink, it is not followed."
	exit 0
fi

if [ "$#" -ne 2 ]; then
	echo "${0} requires 2 arguments: <source> and <destination>" >&2
	exit 2
fi

if [ -d "${2}" ] && [ ! -L "${2}" ]; then
	exec ln --interactive --verbose --symbolic "${1}" "${2}"
else
	exec ln --interactive --verbose --symbolic --no-target-directory "${1}" "${2}"
fi

TODO: --help

file::list-broken-symlinks
#!/usr/bin/env sh

dir_=$(readlink -f "${1:-.}")

for file_ in "${dir_}/"*; do
    if [ ! -e "${file_}" ]; then
        echo "${file_}"
    fi
done

4.33. check-xinput - checks if a device appears in xinput list

file::check-xinput
#!/usr/bin/env bash

set -o nounset

DISPLAY=${DISPLAY:-:0}

exec xinput list | grep "$@" >/dev/null 2>&1

4.34. wait-for-xinput - waits until an xinput device is available

TODO: Docstring

file::wait-for-xinput
#!/usr/bin/env bash

set -o nounset

DISPLAY=${DISPLAY:-:0}

timeout_=${TIMEOUT:-0.1}
times_=${TIMES:-50}
counter_=0

while ! $(check-xinput "$@"); do
	counter_=$((counter_ + 1))
	if [[ "${counter_}" = "${times_}" ]]; then
		exit 1
	fi
	sleep "${timeout_}"
done

4.35. chmod-default [dir] - fixes permissions

chmod a directory recursively, 755 for files and 644 for directories. By default, dir is current working directory.

file::chmod-default
#!/usr/bin/env bash

test -z "$1" && dir_="." || dir_="$1"

find "${dir_}" -type d -print0 | xargs -0 chmod 0755
find "${dir_}" -type f -print0 | xargs -0 chmod 0644

4.36. add-line-comment - comments code, read from stdin

Comments code by prefixing them with line comment character string by the first argument passed in this script. By default, prefix code with `# `.

file::add-line-comment
#!/usr/bin/env 9-rc

#
# Comments a piece of code.
#

if (~ $1 '') {
	comment_char='#'
}
if not {
	comment_char=$1
}

prefix $comment_char^' '

4.37. Input device configuration

Notes:

  • Pressing a button → kernel generates a keycode → X receives the keycode and looks up a keysym that is mapped to that keycode

  • When using the xmodmap command to modify the keyboard layout, note that:

    • clear, add, and remove are for modifiers

    • To remap modifiers, first we need to remove the old mapping, then assign them again.

    • Swapping modifiers general involves 3 steps:

      • Removing the current mapping for the modifiers

      • Swapping the keysyms - it’s generally better to not touch the keycodes (to maintain compatibility with different vendors, e.g. one keysym might produced from different keycodes from different keyboard vendors)

      • Re-adding the same mapping for the modifiers

    • An example to demonstrate how the key mapping and translation work:

      ! To map a physical key to a targeted key
      keysym <physical-key> = <targeted-key>
      ! After this key, pressing the physical key will generate keysym for the targeted key
      
      ! When mapping a modifier, we only care about the targeted key
      add <modifier> = <targeted-key>
    • Common modifier terms:

      • control is for Control

      • mod1 is for Alt/Meta

      • mod2 is for NumLock

      • mod4 is for Super

      • mod5 is for ISO 3rd Level or Mode Switch

4.37.1. config-keymap-altgr - keyboard layout: Programmer Dvorak + Right Alt as AltGr + Ctrl-Alt swapped + CapsLock-Escape swapped

file::config-keymap-altgr
#!/usr/bin/env bash

test -z "${DISPLAY}" && exit 0

##############################################################################
# Main
##############################################################################
#
# References
# * XKB rules: /usr/share/X11/xkb/rules/
# * Arch Linux XKB page: https://wiki.archlinux.org/index.php/X_keyboard_extension
#
##############################################################################

do-notify-short "Setting cmpitg's keyboard layout"

setxkbmap us -variant dvp

if [[ ! "$(hostname)" = "ifr-l" ]]; then
    xmodmap <( cat <<EOF
    ! -*- mode: xmodmap-generic -*-
    !
    ! Notes:
    !
    ! * Press a button → keyboard sends scancode → kernel generates a keycode → keyboard layout maps to a keysym
    !
    ! * 'clear', 'add', and 'remove' commands are for modifiers
    !
    ! * 'keysym' command is to map keysym.
    !
    ! * To remap modifiers, first we need to remove the old keysyms, then assign them again.  That's why swapping is three-step:
    !   - Remove current mapping for modifiers
    !   - Swap the keysyms
    !   - Re-add the same mapping for modifiers
    !
    ! * Modifiers:
    !   - 'control' is for Control
    !   - 'mod1' is for Alt/Meta
    !   - 'mod2' is for NumLock
    !   - 'mod3' is for Hyper
    !   - 'mod4' is for Super
    !   - 'mod5' is for ISO 3rd Level or Mode Switch
    !
    ! evdev defs
    !
    !              |  Keycode |      Keysym      | XKB symbol |
    !--------------|----------|------------------|------------|
    !    Left Ctrl |       37 |        Control_L | LCTL       |
    !   Right Ctrl |      105 |        Control_R | RCTL       |
    !     Left Alt |       64 |            Alt_L | LALT       |
    !    Right Alt |      108 |            Alt_R | RALT       |
    !   Left Hyper |      207 |          Hyper_L | HYPR       |
    !  Right Hyper |      207 |          Hyper_R | HYPR       |
    !   Left Super |      206 |          Super_L | SUPR       |
    !  Right Super |      206 |          Super_R | SUPR       |
    !     Capslock |       66 |        Caps_Lock | CAPS       |
    !       Escape |        9 |           Escape | ESC        |
    !      Compose |      203 |        Multi_key | MDSW       |
    !       Level3 |       92 | ISO_Level3_Shift | LVL3       |
    !
    ! * References
    !   - Keyboard input: https://wiki.archlinux.org/index.php/Keyboard_input
    !   - Keycodes: /usr/share/X11/xkb/keycodes/
    !

    !
    ! Swap Escape and Capslock {keycode → keysym} mapping
    !

    ! Pressing Capslock emits Escape
    keycode 66 = Escape

    ! Pressing Escape emits Capslock
    keycode 9 = Caps_Lock
    ! ! Pressing Escape emits Compose
    ! keycode 9 = Multi_key

    ! Keycode 66 still triggers Lock modifier, let's rebind it
    clear Lock
    add Lock = Caps_Lock

    !
    ! Other modifiers
    !

    ! Pressing Left Alt emits Left Ctrl
    keycode 64 = Control_L

    ! Pressing Left Ctrl emits Left Alt
    keycode 37 = Alt_L

    ! Pressing Right Alt emits Right Ctrl
    ! keycode 108 = Control_R
    ! Pressing Right Alt emits Right Hyper
    keycode 108 = Hyper_R

    ! ! Pressing Right Ctrl emits Level3
    ! keycode 105 = ISO_Level3_Shift
    ! Pressing Right Ctrl emits Compose
    keycode 105 = Multi_key

    ! Now, rearrange the modifiers
    clear control
    clear mod1
    clear mod3
    clear mod4
    clear mod5
    add control = Control_L Control_R
    add mod1 = Alt_L Alt_R
    add mod3 = Hyper_L Hyper_R
    add mod4 = Super_L Super_R
    add mod5 = ISO_Level3_Shift

EOF
)
else
    # Running inside a virtual machine, where modifiers are already swapped.
    # Thus we don't swap, just assign.
    xmodmap <( cat <<EOF
    ! -*- mode: xmodmap-generic -*-
    !
    ! Notes:
    !
    ! * Press a button → keyboard sends scancode → kernel generates a keycode → keyboard layout maps to a keysym
    !
    ! * 'clear', 'add', and 'remove' commands are for modifiers
    !
    ! * 'keysym' command is to map keysym.
    !
    ! * To remap modifiers, first we need to remove the old keysyms, then assign them again.  That's why swapping is three-step:
    !   - Remove current mapping for modifiers
    !   - Swap the keysyms
    !   - Re-add the same mapping for modifiers
    !
    ! * Modifiers:
    !   - 'control' is for Control
    !   - 'mod1' is for Alt/Meta
    !   - 'mod2' is for NumLock
    !   - 'mod3' is for Hyper
    !   - 'mod4' is for Super
    !   - 'mod5' is for ISO 3rd Level or Mode Switch
    !
    ! evdev defs
    !
    !              |  Keycode |      Keysym      | XKB symbol |
    !--------------|----------|------------------|------------|
    !    Left Ctrl |       37 |        Control_L | LCTL       |
    !   Right Ctrl |      105 |        Control_R | RCTL       |
    !     Left Alt |       64 |            Alt_L | LALT       |
    !    Right Alt |      108 |            Alt_R | RALT       |
    !   Left Hyper |      207 |          Hyper_L | HYPR       |
    !  Right Hyper |      207 |          Hyper_R | HYPR       |
    !   Left Super |      206 |          Super_L | SUPR       |
    !  Right Super |      206 |          Super_R | SUPR       |
    !     Capslock |       66 |        Caps_Lock | CAPS       |
    !       Escape |        9 |           Escape | ESC        |
    !      Compose |      203 |        Multi_key | MDSW       |
    !       Level3 |       92 | ISO_Level3_Shift | LVL3       |
    !
    ! * References
    !   - Keyboard input: https://wiki.archlinux.org/index.php/Keyboard_input
    !   - Keycodes: /usr/share/X11/xkb/keycodes/
    !

    ! Pressing Right Ctrl emits Right Hyper
    keycode 105 = Hyper_R

    ! Pressing Right Alt emits Compose
    keycode 108 = Multi_key

    ! Now, rearrange the modifiers
    clear control
    clear mod3
    clear mod4
    add control = Control_L Control_R
    add mod3 = Hyper_L Hyper_R
    add mod4 = Super_L Super_R

EOF
)
fi

set_layout_for_keyboard() {
    local keyboard_name_="${1}"
    local layout_="${2}"
    local id_=$(xinput list --id-only keyboard:"${keyboard_name_}" 2>/dev/null)
    if [[ -n "${id_}" ]]; then
        echo setxkbmap -device "${id_}" -layout "${layout_}"
        setxkbmap -device "${id_}" -layout "${layout_}"
    fi
}

# set_layout_for_keyboard "SOFT/HRUF Splitography" us

xmodmap

4.37.2. config-inputs-cmpitg - config keyboards, mice, …​ all input devices

file::config-inputs-cmpitg
#!/usr/bin/env bash

# config-keymap-altgr
config-logitech-trackball-marble-righty
# config-logitech-trackball-marble-lefty
config-logitech-mx-ergo-right
config-logitech-g300-mouse
config-logitech-g502-mouse
config-logitech-g903-mouse
config-lenovo-n700-mouse
config-touchpad
config-extra-peripherals

4.37.3. config-keymap-steam - keyboard layout: Programmer Dvorak without AltGr

Because Steam doesn’t work with swapped modifiers.

file::config-keymap-steam
#!/usr/bin/env bash

test -z "${DISPLAY}" && exit 0

do-notify-short "Setting keyboard layout for Steam"
newline
setxkbmap us -variant dvp
xmodmap <( cat <<EOF
!
! No mod5 by default
!

clear mod5

!
! Swap left Ctrl and Alt
!

remove control = Control_L
remove mod1 = Alt_L Meta_L
keysym Control_L = Alt_L
keysym Alt_L = Control_L
add control = Control_L
add mod1 = Alt_L

!
! Set right Ctrl as right Alt and right Alt as ISO 3rd level
!

remove control = Control_R
remove mod1 = Alt_R Meta_R
keysym Alt_R = Control_R
keysym Control_R = ISO_Level3_Shift
add control = Control_R
add mod5 = ISO_Level3_Shift

EOF
)

config-logitech-g502-mouse
config-logitech-g903-mouse
config-logitech-mx-ergo-right

4.37.4. Config mice & extra peripherals

Enables natural scrolling and tweaks acceleration profile.

config-extra-peripherals
file::config-extra-peripherals
#!/usr/bin/env bash

check-xinput 'DELL Laser Mouse' && (
	do-notify "Setting natural scrolling for Dell mouse"
	(
		xinput set-prop 'DELL Laser Mouse' 'libinput Natural Scrolling Enabled' 1 &>/dev/null
		xinput set-prop 'DELL Laser Mouse' 'libinput Accel Speed' 0.2 &>/dev/null
	) || (
		xinput set-prop 'DELL Laser Mouse' 'Evdev Scrolling Distance' -1, -1, 1 &>/dev/null
	)
)

check-xinput 'Logitech USB Optical Mouse' && (
	do-notify 'Setting accel profile for Logitech USB Optical Mouse'
	# Polynomial - very usable, recommended
	xinput set-prop 'PS/2 Synaptics TouchPad' 'Device Accel Profile' 2
	xinput set-prop 'Logitech USB Optical Mouse' 'Device Accel Profile' 2

	do-notify "Setting natural scrolling for Logitech USB Optical Mouse"
	(
		xinput set-prop 'Logitech USB Optical Mouse' 'libinput Natural Scrolling Enabled' 1 &>/dev/null
	) || (
		xinput set-prop 'Logitech USB Optical Mouse' 'Evdev Scrolling Distance' -1, -1, 1
	)
)

check-xinput 'Kingsis Peripherals Evoluent VerticalMouse 4' && (
	do-notify "Setting natural scrolling for Evoluent Vertical 4"
	(
		xinput set-prop 'Kingsis Peripherals Evoluent VerticalMouse 4' 'libinput Natural Scrolling Enabled' 1 &>/dev/null
	) || (
		xinput set-prop 'Kingsis Peripherals Evoluent VerticalMouse 4' 'Evdev Scrolling Distance' -1, -1, 1 &>/dev/null
	)
)

check-xinput 'MOSART Semi. 2.4G Wireless Mouse' && (
	do-notify 'Setting accel profile for Anker Vertical Mouse'
	# Polynomial - very usable, recommended
	xinput set-prop 'MOSART Semi. 2.4G Wireless Mouse' 'Device Accel Profile' 2

	do-notify "Setting natural scrolling for Anker Vertical mouse"
	(
		xinput set-prop 'MOSART Semi. 2.4G Wireless Mouse' 'libinput Natural Scrolling Enabled' 1 &>/dev/null
	) || (
		xinput set-prop 'MOSART Semi. 2.4G Wireless Mouse' 'Evdev Scrolling Distance' -1, -1, 1 &>/dev/null
	)
)

check-xinput 'MOSART Semi. 2.4G Wireless Mouse Mouse' && (
	do-notify 'Setting accel profile for Anker Vertical Mouse'
	# Polynomial - very usable, recommended
	xinput set-prop 'MOSART Semi. 2.4G Wireless Mouse Mouse' 'Device Accel Profile' 2

	do-notify "Setting natural scrolling for Anker Vertical mouse"
	(
		xinput set-prop 'MOSART Semi. 2.4G Wireless Mouse Mouse' 'libinput Natural Scrolling Enabled' 1 &>/dev/null
	) || (
		xinput set-prop 'MOSART Semi. 2.4G Wireless Mouse Mouse' 'Evdev Scrolling Distance' -1, -1, 1 &>/dev/null
	)
)

check-xinput 'TPPS/2 IBM TrackPoint' && (
	do-notify "Setting natural scrolling for TPPS/2 IBM TrackPoint"
	(
		xinput set-prop 'TPPS/2 IBM TrackPoint' 'libinput Natural Scrolling Enabled' 1 &>/dev/null
	) || (
		xinput set-prop 'TPPS/2 IBM TrackPoint' 'Evdev Scrolling Distance' -1, -1, 1 &>/dev/null
	)
)

check-xinput 'PS/2 Synaptics TouchPad' && (
	do-notify 'Setting accel profile for PS/2 Synaptics TouchPad'
	# Polynomial - very usable, recommended
	xinput set-prop 'PS/2 Synaptics TouchPad' 'Device Accel Profile' 2

	do-notify 'Setting natural scrolling for PS/2 Synaptics TouchPad'
	(
		xinput set-prop 'PS/2 Synaptics TouchPad' 'libinput Natural Scrolling Enabled' 1 &>/dev/null
	) || (
		xinput set-prop 'PS/2 Synaptics TouchPad' 'Evdev Wheel Emulation' 1
		xinput set-prop 'PS/2 Synaptics TouchPad' 'Evdev Wheel Emulation Button' 2
		xinput set-prop 'PS/2 Synaptics TouchPad' 'Evdev Wheel Emulation Axes' 7, 6, 5, 4
	)
)

check-xinput 'Logitech MX Vertical Advanced Ergonomic Mouse' && (
	do-notify 'Setting accel profile for Logitech MX Vertical Advanced Ergonomic Mouse'
	# Polynomial - very usable, recommended
	xinput set-prop 'Logitech MX Vertical Advanced Ergonomic Mouse' 'Device Accel Profile' 2

	do-notify 'Setting natural scrolling for Logitech MX Vertical Advanced Ergonomic Mouse'
	(
		xinput set-prop 'Logitech MX Vertical Advanced Ergonomic Mouse' 'libinput Natural Scrolling Enabled' 1 &>/dev/null
	) || (
		xinput set-prop 'Logitech MX Vertical Advanced Ergonomic Mouse' 'Evdev Scrolling Distance' -1, -1, -1
	)
)

check-xinput 'Logitech MX Vertical' && (
	do-notify 'Setting accel profile for Logitech MX Vertical'
	# Polynomial - very usable, recommended
	xinput set-prop 'Logitech MX Vertical' 'Device Accel Profile' 2

	do-notify 'Setting natural scrolling for Logitech MX Vertical'
	(
		xinput set-prop 'Logitech MX Vertical' 'libinput Natural Scrolling Enabled' 1 &>/dev/null
	) || (
		xinput set-prop 'Logitech MX Vertical' 'Evdev Scrolling Distance' -1, -1, -1
	)
)

check-xinput 'Microsoft Microsoft® 2.4GHz Transceiver v8.0 Mouse' && (
	do-notify 'Setting accel profile for Microsoft Microsoft® 2.4GHz Transceiver v8.0 Mouse'
	# Polynomial - very usable, recommended
	xinput set-prop 'Microsoft Microsoft® 2.4GHz Transceiver v8.0 Mouse' 'Device Accel Profile' 2

	do-notify 'Setting natural scrolling for Microsoft Microsoft® 2.4GHz Transceiver v8.0 Mouse'
	(
		xinput set-prop 'Microsoft Microsoft® 2.4GHz Transceiver v8.0 Mouse' 'libinput Natural Scrolling Enabled' 1 &>/dev/null
	) || (
		xinput set-prop 'Microsoft Microsoft® 2.4GHz Transceiver v8.0 Mouse' 'Evdev Scrolling Distance' -1, -1, -1
	)
)
config-lenovo-n700-mouse
file::config-lenovo-n700-mouse
#!/bin/zsh

setopt shwordsplit

id_=$( \
	xinput list 2>/dev/null \
	| grep "Dual Mode WL Touch Mouse N700" \
	| head -1 \
	| cut -d'=' -f2 \
	| awk '{ print $1 }' \
)

test -z "${id_}" && exit 0

##############################################################################

do-notify-short "Configuring Dual Mode WL Touch Mouse N700
* Set natural scrolling
* Set pointer acceleration
"
{
	xinput set-prop "${id_}" "Evdev Scrolling Distance" -1, -1, 1 &>/dev/null
} || {
	xinput set-prop "${mouse_}" "libinput Natural Scrolling Enabled" 1 &>/dev/null
}

xinput set-prop "${id_}" "Device Accel Profile" 7
config-logitech-g300-mouse

Also, resets keyboard layout for G300 back to US QWERTY, so that Ctrl+X/C/V works as expected.

file::config-logitech-g300-mouse
#!/bin/zsh

setopt shwordsplit

mouse_=$( \
	xinput list \
	| grep "Logitech Gaming Mouse G300" \
	| head -1 \
	| cut -d'=' -f2 \
	| awk '{ print $1 }' \
)
keyboard_=$( \
	xinput list \
	| grep "Logitech Gaming Mouse G300" \
	| tail -1 \
	| cut -d'=' -f2 \
	| awk '{ print $1 }' \
)

test -z "${mouse_}"    && exit 0
test -z "${keyboard_}" && exit 0

##############################################################################

do-notify-short "Configuring Logitech G300 mouse
* Set natural scrolling
* Reset keyboard layout
"
{
	xinput set-prop "${mouse_}" "libinput Natural Scrolling Enabled" 1 &>/dev/null
} || {
	xinput set-prop "${mouse_}" "Evdev Scrolling Distance" -1, -1, 1 &>/dev/null
}
setxkbmap us -device "${keyboard_}"
config-logitech-g502-mouse
file::config-logitech-g502-mouse
#!/bin/zsh

setopt shwordsplit

# http://www.x.org/wiki/Development/Documentation/PointerAcceleration/

##############################################################################

ids_=$( \
	xinput list \
	| grep "Logitech Gaming Mouse G502" \
	| cut -d'=' -f2 \
	| awk '{ print $1 }' \
)

test -z "${ids_}" && exit 0

##############################################################################

do-notify "Configuring Logitech G502 mouse
* Set natural scrolling
* Tuning mouse movement"

for mouse_ in ${ids_}; do
	echo "${mouse_}"

	{
		xinput set-prop "${mouse_}" "libinput Natural Scrolling Enabled" 1 &>/dev/null
	} || {
		xinput set-prop "${mouse_}" "Evdev Scrolling Distance" -1, -1, 1 &>/dev/null
	}

	xinput set-prop "${mouse_}" "Device Accel Profile" 7
	xinput set-prop "${mouse_}" "Device Accel Constant Deceleration" 2
	xinput set-prop "${mouse_}" "Device Accel Adaptive Deceleration" 1
done
config-logitech-g903-mouse
file::config-logitech-g903-mouse
#!/bin/zsh

setopt shwordsplit

# http://www.x.org/wiki/Development/Documentation/PointerAcceleration/

##############################################################################

ids_=$( \
	xinput list \
	| grep "Logitech G903 LS" \
	| cut -d'=' -f2 \
	| awk '{ print $1 }' \
)

test -z "${ids_}" && exit 0

##############################################################################

do-notify "Configuring Logitech G903 mouse
* Set natural scrolling
* Tuning mouse movement"

for mouse_ in ${ids_}; do
	echo "${mouse_}"

	{
		xinput set-prop "${mouse_}" "libinput Natural Scrolling Enabled" 1 &>/dev/null
	} || {
		xinput set-prop "${mouse_}" "Evdev Scrolling Distance" -1, -1, 1 &>/dev/null
	}

#	xinput set-prop "${mouse_}" "Device Accel Profile" 3
#	xinput set-prop "${mouse_}" "Device Accel Constant Deceleration" 0.75
#	xinput set-prop "${mouse_}" "Device Accel Adaptive Deceleration" 0.75
done
config-logitech-mx-ergo-right
file::config-logitech-mx-ergo-right
#!/bin/zsh

setopt shwordsplit

# http://www.x.org/wiki/Development/Documentation/PointerAcceleration/

##############################################################################

ids_=$( \
	xinput list \
	| rg "(Logitech MX Ergo|MX Ergo Mouse)" \
	| cut -d'=' -f2 \
	| awk '{ print $1 }' \
)

test -z "${ids_}" && exit 0

##############################################################################

do-notify "Configuring Logitech MX Ergo
* Set natural scrolling
* Tuning mouse movement"

# Ref: https://www.x.org/wiki/Development/Documentation/PointerAcceleration/

for mouse_ in ${ids_}; do
	echo "${mouse_}"

	{
		xinput set-prop "${mouse_}" "libinput Natural Scrolling Enabled" 1 &>/dev/null
		xinput set-prop "${mouse_}" "Coordinate Transformation Matrix" -1 0 1 0 -1 1 0 0 1 &>/dev/null
	} || {
		xinput set-prop "${mouse_}" "Evdev Scrolling Distance" -1, -1, 1 &>/dev/null
		xinput set-prop "${mouse_}" "Evdev Axis Inversion" 1, 1 &>/dev/null
	}

	xinput set-prop "${mouse_}" "Device Accel Profile" 7
	# xinput set-prop "${mouse_}" "Device Accel Constant Deceleration" 1.05
	# xinput set-prop "${mouse_}" "Device Accel Adaptive Deceleration" 1.05
	xinput set-prop "${mouse_}" "Device Accel Constant Deceleration" 0.8
	xinput set-prop "${mouse_}" "Device Accel Adaptive Deceleration" 1.5
	# xinput set-prop "${mouse_}" "Device Accel Profile" 2
	# xinput set-prop "${mouse_}" "Device Accel Constant Deceleration" 1.7
	# xinput set-prop "${mouse_}" "Device Accel Adaptive Deceleration" 1.5
done
config-logitech-trackball-marble-lefty
file::config-logitech-trackball-marble-lefty
#!/usr/bin/env bash

# Sources:
#   https://wiki.archlinux.org/index.php/Logitech_Marble_Mouse
#   http://www.x.org/wiki/Development/Documentation/PointerAcceleration/
#   http://www.x.org/archive/X11R7.5/doc/man/man4/evdev.4.html
#   man evdev

id_=$( \
	xinput list \
	| grep "Logitech USB Trackball" \
	| head -1 \
	| cut -d'=' -f2 \
	| awk '{ print $1 }' \
)

test -z "${id_}" && exit 0

# ID        Hardware Action         Result
# 1     Large button left   normal click
# 2     Both large buttons  middle-click  †
# 3     Large button right  right-click
# 4     (not a button)  -
# 5     (not a button)  -
# 6     (not a button)  -
# 7     (not a button)  -
# 8     Small button left   browser back
# 9     Small button right  browser forward


# * big-left: Primary click
# * big-right: Secondary click
# * small-left: Scrolling
# * small-right: Middle click
do-notify-short """Config buttons for lefties:
   large-left  [1]: Right click
   large-right [3]: Left click
   small-left  [8]: Middle click
   small-right [9]: Scrolling + Middle click"""
newline
# xinput set-button-map "${id_}" 1 9 3 4 5 6 7 2 9
xinput set-button-map "${id_}" 3 9 1 4 5 6 7 2 2

# small-left
# xinput set-prop "${id_}" "Evdev Wheel Emulation Button" 8
xinput set-prop "${id_}" "Evdev Wheel Emulation Button" 9

# Enable wheel emulation
xinput set-prop "${id_}" "Evdev Wheel Emulation"        1

##############################################################################

do-notify-short "Config inverted and horizontial scrolling"

# For normal scrolling
# xinput set-prop "${id_}" "Evdev Wheel Emulation Axes" 6 7 4 5

# Inverted scrolling
xinput set-prop "${id_}" "Evdev Wheel Emulation Axes" 7 6 5 4

# Inverted direction
xinput set-prop "${id_}" "Evdev Axis Inversion" 1 1

##############################################################################

do-notify-short "Config profile: Fast movement but more control at pixel-level"
newline

# Default
# Debian
# xinput set-prop "${id_}" "Device Accel Constant Deceleration" 1.5
xinput set-prop "${id_}" "Device Accel Constant Deceleration" 1.5

# More precision
# xinput set-prop "${id_}" "Device Accel Adaptive Deceleration" 5
xinput set-prop "${id_}" "Device Accel Adaptive Deceleration" 1

# Acceleration
#   http://www.x.org/wiki/Development/Documentation/PointerAcceleration/
# xinput set-prop "${id_}" "Device Accel Profile" -1
# xinput set-prop "${id_}" "Device Accel Profile" 6
xinput set-prop "${id_}" "Device Accel Profile" 2
# Debian
xinput set-prop "${id_}" "Device Accel Velocity Scaling" 5
# xinput set-prop "${id_}" "Device Accel Velocity Scaling" 1
# xinput set-prop "${id_}" "Device Accel Velocity Scaling" 1
config-logitech-trackball-marble-righty
file::config-logitech-trackball-marble-righty
#!/usr/bin/env bash

# Sources:
#   https://wiki.archlinux.org/index.php/Logitech_Marble_Mouse
#   http://www.x.org/wiki/Development/Documentation/PointerAcceleration/
#   http://www.x.org/archive/X11R7.5/doc/man/man4/evdev.4.html
#   man evdev

id_=$( \
	xinput list \
	| grep "Logitech USB Trackball" \
	| head -1 \
	| cut -d'=' -f2 \
	| awk '{ print $1 }' \
)

test -z "${id_}" && exit 0

# ID        Hardware Action         Result
# 1     Large button left   normal click
# 2     Both large buttons  middle-click  †
# 3     Large button right  right-click
# 4     (not a button)  -
# 5     (not a button)  -
# 6     (not a button)  -
# 7     (not a button)  -
# 8     Small button left   browser back
# 9     Small button right  browser forward


# * big-left: Primary click
# * big-right: Secondary click
# * small-left: Scrolling
# * small-right: Middle click
do-notify-short """Config buttons for righties:
   large-left  [1]: Left click
   large-right [3]: Right click
   small-left  [8]: Middle click
   small-right [9]: Scrolling + Middle click"""
newline
xinput set-button-map "${id_}" 1 9 3 4 5 6 7 2 9
# xinput set-button-map "${id_}" 3 9 1 4 5 6 7 2 2

# small-left
xinput set-prop "${id_}" "Evdev Wheel Emulation Button" 8
# xinput set-prop "${id_}" "Evdev Wheel Emulation Button" 9

# Enable wheel emulation
xinput set-prop "${id_}" "Evdev Wheel Emulation"        1

##############################################################################

do-notify-short "Config inverted and horizontial scrolling"
newline

# For normal scrolling
# xinput set-prop "${id_}" "Evdev Wheel Emulation Axes" 6 7 4 5

# Inverted scrolling
xinput set-prop "${id_}" "Evdev Wheel Emulation Axes" 7 6 5 4

# Inverted direction
xinput set-prop "${id_}" "Evdev Axis Inversion" 1 1
# xinput set-prop "${id_}" "Evdev Axis Inversion" 0 1

##############################################################################

do-notify-short "Config profile: Fast movement but more control at pixel-level"
newline

# Default
# Debian
# xinput set-prop "${id_}" "Device Accel Constant Deceleration" 1.5
# xinput set-prop "${id_}" "Device Accel Constant Deceleration" 1.5

# More precision
# xinput set-prop "${id_}" "Device Accel Adaptive Deceleration" 5
# xinput set-prop "${id_}" "Device Accel Adaptive Deceleration" 1

# Acceleration
# xinput set-prop "${id_}" "Device Accel Profile" -1
# xinput set-prop "${id_}" "Device Accel Profile" 6
xinput set-prop "${id_}" "Device Accel Profile" 2
# Debian
xinput set-prop "${id_}" "Device Accel Velocity Scaling" 5
# xinput set-prop "${id_}" "Device Accel Velocity Scaling" 1.5
# xinput set-prop "${id_}" "Device Accel Velocity Scaling" 1
config-touchpad

Lots of tweaks, the code should be self-explanatory though.

file::config-touchpad
#!/usr/bin/env bash

check-xinput -i "touchpad" || exit 0

id_=$( \
	xinput list \
	| grep -i 'synaptics touchpad' \
	| cut -d'=' -f2 \
	| awk '{ print $1 }' \
)

scrolling_distance_2_=$(xinput list-props ${id_} \
	| grep 'Synaptics Scrolling Distance' \
	| gawk '{ print $NF }' \
	| sed 's/-//g' \
)
scrolling_distance_1_=$(xinput list-props ${id_} \
	| grep 'Synaptics Scrolling Distance' \
	| gawk '{ print $(NF - 1) }' \
	| cut -d',' -f1 \
	| sed 's/-//g' \
)

##############################################################################

do-notify-short """Configuring touchpad
* Setting natural scrolling
* Enabling tapping
* Enabling two-finger tapping as secondary click"""
newline

# Edge
# synclient LeftEdge=1200
# synclient RightEdge=5100
# synclient TopEdge=1000
# synclient BottomEdge=4600

# synclient LeftEdge=1000
# synclient RightEdge=5200
# synclient TopEdge=1000
# synclient BottomEdge=5000

# Palm detection
## Wed, 27 Jul 2016 23:22:03 +0300 - Disable because it's no longer relevant
# synclient PalmDetect=1

# Tap
## Wed, 27 Jul 2016 23:22:03 +0300 - Disable because it's no longer relevant
# synclient MaxTapTime=180
# synclient MaxTapMove=221
# synclient MaxDoubleTapTime=100
# synclient SingleTapTimeout=180
# synclient EmulateTwoFingerMinZ=1
# synclient EmulateTwoFingerMinW=7
# synclient VertEdgeScroll=1
# synclient HorizEdgeScroll=1

# Corner
## Wed, 27 Jul 2016 23:22:03 +0300 - Disable because it's no longer relevant
# synclient RTCornerButton=0
# synclient RBCornerButton=0
# synclient LTCornerButton=1
# synclient LBCornerButton=0
# synclient TapButton1=1
# synclient TapButton2=3
# synclient TapButton3=2
# synclient ClickFinger1=1
# synclient ClickFinger2=1
# synclient ClickFinger3=2
# synclient CircularScrolling=0

# Natural scrolling
# synclient VertScrollDelta=-111
# synclient HorizScrollDelta=-111
# synclient VertEdgeScroll=0
# synclient HorizEdgeScroll=0

##############################################################################

(
	xinput set-prop "${id_}" "libinput Tapping Enabled" 0 &>/dev/null
	xinput set-prop "${id_}" "libinput Natural Scrolling Enabled" 1 &>/dev/null
) || (
	xinput set-prop "${id_}" "Synaptics Scrolling Distance" "-${scrolling_distance_1_}" "-${scrolling_distance_2_}" &>/dev/null
	xinput set-prop "${id_}" "Synaptics Two-Finger Scrolling" 1, 1 &>/dev/null
)

# xinput get-button-map "SynPS/2 Synaptics TouchPad" 1 2 3 4 5 6 7 8 9 10 11 12

4.38. extract-audio

Extracts from a video file, creating the same file name with appropriate extension.

file::extract-audio
#!/bin/zsh

setopt shwordsplit

report-missing-executables ffmpeg Ffmpeg || exit 1

file_="$1"

ffmpeg -i "${file_}" -vn -acodec copy \
	"$file_:r.$(ffprobe ${file_} 2>&1 | grep Audio | sed -rn 's/.*Audio: ([^ ]*).*/\1/p')"

4.39. convert-to-mp3 - converts files to MP3

This script takes a list of files as arguments.

file::convert-to-mp3
#!/usr/bin/env 9-rc

report-missing-executables ffmpeg Ffmpeg || exit 1

for (f in $*) {
	new_name=`{echo $f | replace-extension mp3}
	ffmpeg -i $f -vn -aq 1 $"new_name
}

4.40. convert-to-ogg-vorbis - converts files to Ogg Vorbis

This script takes a list of files as arguments.

file::convert-to-ogg-vorbis
#!/usr/bin/env 9-rc

report-missing-executables ffmpeg Ffmpeg || exit 1

for (f in $*) {
	new_name=`{echo $f | replace-extension ogg}
	ffmpeg -i $f -vn -aq 1 $"new_name
}

4.41. count-monitors

file::count-monitors
#!/usr/bin/env bash

xrandr | grep " connected" | wc -l

4.42. get-cpu-usage

Returns the average CPU usage measured in 3 consecutive seconds, using mpstat.

file::get-cpu-usage
#!/usr/bin/env bash

#
# Using `mpstat', calculates average CPU usage in 3 seconds.
#

report-missing-executables mpstat Sysstat || exit 1

mpstat 3 1 | tail -1 | gawk '$12 ~ /[0-9.]+/ { print 100 - $12"%" }'

4.43. create-ctags

file::create-ctags
#!/usr/bin/env bash

#
# Creates a tags file named TAGS using ctags.
#

report-missing-executables tags "Ctags or Exuberant Ctags" || exit 1

if test -z "$1"; then
	cat <<EOF
Usage: $0 <directory> [ctags-options]*

Creates a tags file named TAGS using ctags.
EOF
	exit 2
fi

dir_name_="$1"
shift

ctags "$@" -f "${dir_name_}"/TAGS -R "${dir_name_}"/*

4.44. do-notify - sends a desktop notification

file::do-notify
#!/usr/bin/env bash

report-missing-executables notify-send Libnotify || exit 1

echo "$@"
# qdbus org.freedesktop.Notifications &>/dev/null && notify-send "$@"
notify-send "$@"

4.45. do-notify-short - sends a short desktop notification

file::do-notify-short
#!/usr/bin/env bash

report-missing-executables notify-send Libnotify || exit 1

echo "$@"
# qdbus org.freedesktop.Notifications &>/dev/null && notify-send -t 2000 "$@"
# qdbus org.freedesktop.Notifications &>/dev/null && notify-send "$@"
notify-send "$@"

4.46. drop-lines

file::drop-lines
#!/usr/bin/env 9-rc

#
# Drops the first $1 lines.
#

n_lines=$1
n_lines=`{echo $n_lines + 1 | bc}
tail -n +$n_lines

4.47. in_epoch2datetime - converts the epoch from read from stdin to local date time

This script is particularly helpful when using with Emacs/Acme, e.g. called with a text selection.

file::in_epoch2datetime
#!/usr/bin/env sh

epoch_=$(cat)

exec date --date="@${epoch_}" -R

4.48. en2fi - translates from English to Finnish

file::en2fi
#!/usr/bin/env 9-rc

#
# Translates from English to Finnish with Google Translate, using
# soimort/translate-shell tool.
#

report-missing-executables trans soimort/translate-shell || exit 1

TARGET_LANG=fi gtranslate $*

4.49. fi2en - translates from Finnish to English

file::fi2en
#!/usr/bin/env 9-rc

#
# Translates from Finnish to English with Google Translate, using
# soimort/translate-shell tool.
#

report-missing-executables trans soimort/translate-shell || exit 1

TARGET_LANG=en gtranslate $*

4.50. format-text - formats text from stdin using Emacs’s fill-paragraph

file::format-text
#!/usr/bin/env 9-rc

#
# Formats text from stdin using Emacs's fill-paragraph.
#

input=`{cat}
sexpr=`{echo `{cat <<EOF}}

(with-temp-buffer
  (set-fill-column 78)
  (insert "$input")
  (end-of-buffer)
  (fill-region 0 (point))
  (princ (buffer-string)))
EOF

emacs --batch --eval $"sexpr $* >[2]/dev/null

4.51. replace-extension - replaces file extension

file::replace-extension
#!/usr/bin/env 9-rc

input=`{cat}

if (~ $1 '') {
	echo Usage: $0 '<'replacement'>' >[1=2]
	exit 1
}

rev_replacement=`{echo $1 | rev}

echo $input | rev | sed 's/^[^.]*\./'$rev_replacement'./' | rev

4.52. gitserve - runs a Git server

file::gitserve
#!/usr/bin/env 9-rc

#
# Runs a Git server.
#

program=`{basename $0}

if (~ $1 '-h' '--help') {
	cat <<USAGE
	exit 0
}
Usage:

Runs a Git server.

  $program             :: Take current directory as Git repository
  $program <git-repo>  :: Take a specific Git repository

By default, the Git server is opened on port 4242.  This could be overriden by
setting the environment variable GIT_PORT.  For example: run a Git server on
port 5454, serving content from Git repo at /m/bin:

  GIT_PORT=5454 $program /m/bin

Then, you can clone the repo with: git clone git://<host>:<port>/ <repo-name>

Note that this method is a quick way to share Git repository and it's not at
all secure.  In practice, you might want to Git server behind a reverse proxy.
USAGE

(test $#GIT_PORT -eq 0) && git_port=4242 || git_port=$GIT_PORT
(test $#1        -eq 0) && git_path='.'  || git_path=$1

exec git daemon --reuseaddr '--base-path='$git_path --export-all --verbose '--port='$git_port

4.53. get-all-randr-outputs - gets all RandR outputs

file::get-all-randr-outputs
#!/usr/bin/env bash

report-missing-executables xrandr XRandR ag Ag awk "GNU Awk" || exit 1

xrandr | awk '/connected/ { print $1 }'

4.54. get-all-execs - gets all executables, optionally prefixed with a string and history support

file::get-all-execs
#!/usr/bin/env tclsh

package require Tcl 8
package require cmdline

if {[catch {exec report-missing-executables find "GNU Find" sort Coreutils parallel "GNU Parallel" with-workdir cmpitg-scripts <@ stdin >@ stdout 2>@ stderr}]} {
    exit 1
}

proc usage {{fd stdout}} {
    puts $fd {get-all-execs [<max-depth>]

Get all executables from the PATH environment variables, sort in ascending order, deduplicate, and return them one line per entry.  max-depth defines how deep we traverse from a path.  If not specified, max-depth is 1.}
}

proc getAccessiblePaths {maxDepth} {
    set rawPaths [split [string trim [exec dedup-PATH]] ":"]
    set paths {}
    foreach path $rawPaths {
        if {$path ne "." && $path ne "./" && [file exists $path] && [file isdirectory $path]} {
            # lappend cmds [list with-workdir "$path/" find . -maxdepth $maxDepth -type f,l -executable | cut -c 3-]
            lappend paths "$path/"
        }
    }
    return $paths
}

if {$::argv == "--help"} {
    usage
}
if {$::argc > 2} {
    usage stderr
    exit 1
}

if {$::argv eq {}} {
    set maxDepth 1
} else {
    set maxDepth [lindex $::argv 0]
}

set paths [getAccessiblePaths $maxDepth]

# With GNU Parallel - Slowest
# catch { puts [exec parallel --link with-workdir ::: {*}$paths ::: fdfind ::: . ::: --maxdepth ::: $maxDepth ::: --type ::: x ::: --type ::: l ::: --hidden ::: --no-ignore ::: --color ::: never | rev | cut -d/ -f 1 | rev | sort -u | rg -v {^$}]}

# Without GNU Parallel
# basename -> Slow
# catch { puts [exec fdfind . --maxdepth $maxDepth --type x --type l --hidden --no-ignore {*}$paths --color never --exec basename | sort -u | rg -v {^$}]}
# cut + rev: fast
catch { puts [exec fdfind . --maxdepth $maxDepth --type x --type l --hidden --no-ignore {*}$paths --color never | rev | cut -d/ -f 1 | rev | sort -u | rg -v {^$}]}
# Awk: fast but a bit slower
# catch { puts [exec fdfind . --maxdepth $maxDepth --type x --type l --hidden --no-ignore {*}$paths --color never | awk --field-separator=/ {{ print $NF }} | sort -u | rg -v {^$}]}

4.55. get-sensors-data - gets meaningful sensors data

file::get-sensors-data
#!/usr/bin/env tclsh

if {[catch {exec report-missing-executables sensors lm-sensors acpi acpi >@ stdout 2>@ stderr}]} {
    exit 1
}

package require Tcl 8.4
package require json 1.3.3

set deviceMapping {
    {
        label CPU
        command {::json::json2dict [exec sensors -j <@ stdin 2> /dev/null]}
        groups {
            {
                unit °C
                paths {
                    {k10temp-pci-00c3 Tdie temp1_input}
                    {k10temp-pci-00c3 Tdie temp2_input}
                    {k10temp-pci-00c3 Tccd1 temp3_input}
                    {coretemp-isa-0000 "Core 0" temp2_input}
                    {coretemp-isa-0000 "Core 1" temp3_input}
                    {coretemp-isa-0000 "Core 2" temp4_input}
                    {coretemp-isa-0000 "Core 3" temp5_input}
                }
            }
            {
                unit " RPM"
                paths {
                    {thinkpad-isa-0000 fan1 fan1_input}
                }
            }
        }
    }
    {
        label GPU
        command {::json::json2dict [exec sensors -j <@ stdin 2> /dev/null]}
        groups {
            {
                unit °C
                paths {
                    {amdgpu-pci-0500 junction temp2_input}
                    {amdgpu-pci-0600 edge temp1_input}
                }
            }
            {
                unit " RPM"
                paths {
                    {amdgpu-pci-0500 fan1 fan1_input}
                }
            }
        }
    }
    {
        label Bats
        command {join [exec acpi -b | cut -d: -f 2 | awk {BEGIN { FS="," } { print $2 $1 }} | sed {s/[:,]//g ; s/ Not charging//g ; s/ Charging/+/g ; s/ Discharging/-/g ; s/ Unknown//g} 2> /dev/null]}
    }
}

set sensorData [::json::json2dict [exec sensors -j <@ stdin 2> /dev/null]]
set resultList {}

proc getValueForPath {data path} {
    if {![dict exists $data {*}$path]} {
        return {}
    } else {
        return [format "%.1f" [dict get $data {*}$path]]
    }
}

proc getReadingForGroup {sensorData group} {
    set currentReadingList {}
    set unit [dict get $group unit]
    foreach path [dict get $group paths] {
        set number [getValueForPath $sensorData $path]
        if {$number ne {}} {
            lappend currentReadingList "$number$unit"
        }
    }
    return $currentReadingList
}

proc getReadingForDevice {dev} {
    set label [dict get $dev label]
    set sensorData [eval [dict get $dev command]]

    if {[dict exists $dev groups]} {
        set reading {}
        foreach group [dict get $dev groups] {
            set groupReading [getReadingForGroup $sensorData $group]
            if {$groupReading ne {}} {
                lappend reading [join $groupReading ", "]
            }
        }
    } else {
        # If the 'groups' key doesn't exist, the reading is $sensorData
        set reading $sensorData
    }


    if {$reading ne {}} {
        return "$label: [join $reading ", "]"
    } else {
        return {}
    }
}

foreach dev $deviceMapping {
    set reading [getReadingForDevice $dev]
    if {$reading ne {}} {
        lappend resultList $reading
    }
}

puts [join $resultList " | "]

4.56. dedup-PATH - gets a clean PATH variable (dedup’ed)

file::dedup-PATH
#!/usr/bin/env sh

exec echo "${PATH}" | awk -v RS=: -v ORS=: '!seen[$0]++' | head -1

4.57. dedup-lines - dedups lines

file::dedup-lines
#!/usr/bin/env sh

exec awk '!seen[$0]++'

4.58. html2text

file::html2text
#!/usr/bin/env bash

#
# Converts HTML to text.  HTML is read from stdin.
#

report-missing-executables lynx Lynx || exit 1

exec lynx -dump -stdin "$@"

4.59. i3-exec-command - executes an i3 command

file::i3-exec-command
#!/usr/bin/env bash

i3-input -f 'pango:Noto Sans 10' "$@"

4.60. i3-move-to-workspace - moves a window to a workspace with i3

file::i3-move-to-workspace
#!/usr/bin/env bash

i3-input \
	-f 'pango:Noto Sans 10' \
	-F 'move workspace "%s"' \
	-P 'Move window to workspace: ' %s

4.61. i3-rename-workspace - renames current workspace in i3

file::i3-rename-workspace
#!/usr/bin/env bash

i3-input \
	-f 'pango:Noto Sans 10' \
	-F 'rename workspace to "%s"' \
	-P 'Rename workspace: ' %s

4.62. switch-window - window switcher

Requirement: rofi.

file::switch-window
#!/usr/bin/env sh

report-missing-executables run-menu run-menu || exit 1

exec run-menu -modi window -show window

4.63. set-only-monitors - sets and configs only certain monitors, all others are off

file::set-only-monitors
#!/usr/bin/env tclsh

package require Tcl 8
package require Tclx

if {[catch {exec report-missing-executables get-all-randr-outputs get-all-randr-outputs >@ stdout 2>@ stderr}]} {
    exit 1
}

proc getSetMonitors {} {
    set res {}
    set takeNow 0
    foreach cmdArg $::argv {
        # We take the argument right after the --output argument
        if {$cmdArg eq "--output"} {
            set takeNow 1
        } elseif {$takeNow} {
            lappend res $cmdArg
            set takeNow 0
        }
    }
    return $res
}

proc filterMonitors {setMonitors monitors} {
    set res {}
    foreach monitor $monitors {
        if {[lsearch $setMonitors $monitor] == -1} {
            lappend res $monitor
        }
    }
    return $res
}

set setMonitors [getSetMonitors]
set allMonitors [string trim [exec get-all-randr-outputs]]
set monitors [filterMonitors $setMonitors $allMonitors]

set cmd [list {*}$::argv]
foreach monitor $monitors {
    set cmd [list {*}$cmd "--output" $monitor "--off"]
}

puts "xrandr $cmd"
execl "xrandr" $cmd

4.64. set-monitors-auto - configs all monitors with default settings from XRandR

file::set-monitors-auto
#!/usr/bin/env bash

report-missing-executables xrandr XRandR get-all-randr-outputs get-all-randr-outputs sed "GNU Sed" tr Coreutils || exit 1

get-all-randr-outputs | sed 's/$/ --auto/g; s/^/--output /g' | tr "\n" " " | xargs xrandr

4.65. set-1-monitor

file::set-1-monitor
#!/usr/bin/env tclsh

package require Tcl 8
package require Tclx

if {[catch {exec report-missing-executables set-only-monitors set-only-monitors >@ stdout 2>@ stderr}]} {
   exit 1
}

if {[info exists ::env(MY_MAIN_MONITOR_OUTPUT)]} {
    set mainMonitor $::env(MY_MAIN_MONITOR_OUTPUT)
    set mainMode $::env(MY_MAIN_MONITOR_MODE)
} else {
    set mainMonitor [lindex $monitors 0]
    set mainMode [lindex $monitors 1]
}

execl "set-only-monitors" [list "--output" $mainMonitor "--mode" $mainMode "--primary"]

4.66. Running applications

4.66.1. Volume control

file::vol
#!/usr/bin/env tclsh

set cmd [lindex $::argv 0]

proc playTestSound {} {
    exec paplay /usr/share/sounds/freedesktop/stereo/audio-volume-change.oga <@ stdin >@ stdout 2>@ stderr
}

proc setVol {amount} {
    exec pactl set-sink-volume "@DEFAULT_SINK@" $amount <@ stdin >@ stdout 2>@ stderr
}

proc setMute {flag} {
    exec pactl set-sink-mute @DEFAULT_SINK@ $flag <@ stdin >@ stdout 2>@ stderr
}

proc getVol {} {
    # return [join [exec amixer -c 1 -M -D pulse get Master | grep -o -E {[[:digit:]]+%}]]
    return [join [exec amixer -M -D pulse get Master | grep -o -E {[[:digit:]]+%}]]
}

proc showVol {} {
    set vol [getVol]
    puts $vol
    exec notify-send "Volume: $vol" <@ stdin >@ stdout 2>@ stderr
}

switch $cmd {
    get {
        showVol
    }
    up -
    + {
        setVol +5%
        playTestSound
        showVol
    }
    down -
    "-" {
        setVol -5%
        playTestSound
        showVol
    }
    toggle-mute {
        setMute toggle
        playTestSound
        showVol
    }
    mute {
        setMute 1
    }
    unmute {
        setMute 0
        playTestSound
        showVol
    }
}

4.66.2. Disk daemon

file::run-disk-daemon
#!/usr/bin/env sh

pgrep --full 'udiskie*.*tray' >/dev/null 2>&1 || exec udiskie --no-automount --tray

4.66.3. Clipboard manager

file::run-clipboard-manager
#!/usr/bin/env sh

# pidof clipit >/dev/null 2>&1 || exec clipit
pidof greenclip >/dev/null 2>&1 || exec greenclip daemon
file::display-clipboard
#!/usr/bin/env sh

exec run-menu -modi "clipboard:greenclip print" -show clipboard -run-command '{cmd}'

4.66.4. Keybind Daemon

file::run-keybind-daemon
#!/usr/bin/env sh

pidof xbindkeys >/dev/null 2>&1 || exec xbindkeys --nodaemon --poll-rc

4.66.5. Power manager

file::run-power-manager
#!/usr/bin/env sh

pkill xfce4-power-manager
pkill mate-power-manager
# exec xfce4-power-manager --no-daemon
exec mate-power-manager

4.66.6. Volume daemon

file::run-volumed
#!/usr/bin/env sh

# pkill xfce4-volumed
# exec xfce4-volumed --no-daemon

# pidof kmix >/dev/null 2>&1 || kmix

pkill pasystray ; pasystray

4.66.7. Input method daemon with Ibus

+ .file::run-ibus-daemon

#!/usr/bin/env sh

exec ibus-daemon -xvr

4.66.8. Screenshot manager

file::run-screenshot-manager
#!/usr/bin/env sh

4.66.9. Screensaver daemon

file::run-screensaverd
#!/usr/bin/env sh

pidof xscreensaver >/dev/null 2>&1 || exec xscreensaver

4.66.10. Network manager

file::run-network-manager
#!/usr/bin/env sh

pkill nm-applet
exec nm-applet --sm-disable

4.66.11. GNOME settings daemon

file::run-settings-daemon
#!/usr/bin/env bash

gnome-settings-daemon -h >/dev/null 2>&1 && (
	pidof gnome-settings-daemon >/dev/null 2>&1 || gnome-settings-daemon
)
[[ -e /usr/lib/gnome-settings-daemon/gsd-xsettings ]] && (
	pidof gsd-xsettings >/dev/null 2>&1 || /usr/lib/gnome-settings-daemon/gsd-xsettings
)

4.66.12. Insync

file::run-insync
#!/usr/bin/env sh

# pidof insync >/dev/null 2>&1 || exec insync start
insync quit
exec insync start

4.66.13. Menu program

file::run-menu
#!/usr/bin/env sh

exec rofi -l 40 \
	-width 85 \
	-i \
	-multi-select \
	-font "Cascadia Code 11" \
	-kb-row-select Tab \
	-kb-row-tab "" \
	-kb-row-left "" \
	-kb-row-right "" \
	-kb-row-up Super+c,Up,Control+p \
	-kb-row-down Super+t,Down,Control+n \
	-kb-row-left Super+h,Left,Control+b \
	-kb-row-right Super+n,Right,Control+f \
	-kb-move-front Super+d,Control+a \
	-kb-move-end Super+Shift+d,Control+e \
	-kb-move-word-back Super+g,Alt+b \
	-kb-move-word-forward Super+r,Alt+f \
	-kb-accept-custom Shift+Return \
	-kb-accept-alt Control+Return \
	"$@"

4.66.14. Context-menu program

file::run-context-menu
#!/usr/bin/env sh

# deep-exec guix/with-env report-missing-executables sawfish Sawfish || exit 1

# menu_path_=$(deep-exec guix/with-env which sawfish \
#     | xargs -n 1 -I "{}" readlink -f "{}" \
#      | xargs -n 1 -I "{}" dirname "{}" \
#      | xargs -n 1 -I "{}" readlink -f "{}/../lib/sawfish/sawfish-menu")

# exec deep-exec guix/with-env "${menu_path_}" "$@"

report-missing-executables sawfish Sawfish || exit 1

exec /usr/lib/x86_64-linux-gnu/sawfish/sawfish-menu "$@"

4.66.15. Application launcher

TODO: Note about dispatch-action in the docstring

file::run-app-launcher
#!/usr/bin/env tclsh

package require Tcl 8
package require Tclx
package require cmdline

if {[catch {exec report-missing-executables get-all-execs get-all-execs run-menu run-menu add-to-history add-to-history <@ stdin >@ stdout 2>@ stderr}]} {
    exit 1
}

try {
    array set cmdArgs [::cmdline::getoptions ::argv {
        {history-file.arg "~/.local/app-runner-history" "Application history file"}
        {max-history.arg  256                           "Maximum number of entries in the history"}
        {prefix.arg       "! "                         "String with which each executable is prefixed"}
        {max-depth.arg    10                            "How deep paths from PATHS are traversed"}
    } {[--history-file <app-runner-history>] [--prefix <prefix>]

Run a fuzzy searcher tool with all the executables found in the PATH environment variable and from a history file.  The result of the search is then executed based on predefined patterns as follows.

* '!@ <command> [args...]' :: Run the command in a terminal emulator
* '!! <command> [args...]' :: Run the command in a terminal emulator, pause and prompt for exiting when after the command finishes
* '<url>' :: Open the URL with 'web-browser-gui'
* '<file-path>' :: Open the file path with a text editor
* 'dir:<dir-path>'  :: Open the directory using 'dir-browser-gui'

TODO: More patterns are later supported using Plan9port's Plumber.
}]} trap {CMDLINE USAGE} {msg _o} {
    if {[llength $::argv] == 0} {
        puts $msg
        exit 0
    } else {
        puts stderr $msg
        exit 1
    }
}

proc readHistory {path} {
    if {[file exists $path]} {
        set fd [open $path r]
        set data [read $fd]
        close $fd
        return $data
    } else {
        return ""
    }
}

proc addToHistory {path maxHistory text} {
    set fd [open "| add-to-history --max-history $maxHistory $path" w]
    puts $fd $text
    close $fd
}

proc getChoice {history execs} {
    set fd [open "| run-menu -dmenu -p Text " r+]
    puts -nonewline $fd $history
    puts -nonewline $fd $execs
    flush $fd
    chan close $fd write
    set res [read $fd]
    catch {close $fd}
    return [string trim $res]
}

set historyFile [file normalize $cmdArgs(history-file)]
set maxDepth $cmdArgs(max-depth)
set maxHistory $cmdArgs(max-history)
set prefix $cmdArgs(prefix)

set execs [exec get-all-execs $maxDepth | sed "s/^/$prefix/g"]
set history [readHistory $historyFile]
set choice [getChoice $history $execs]

if {$choice ne ""} {
    addToHistory $historyFile $maxHistory $choice
    execl dispatch-action [list $choice]
}
file::run-rmacs-rocket
#!/usr/bin/env dash

exec wihack -type toolbar rmacs --shape utils --new-frame eval '(rocket:show-command-runner-with-dedicated-frame)'
# exec rmacs --shape utils --new-frame eval '(prog1 (rocket:show-command-runner-with-dedicated-frame) (~wmii/set-frame-floating))'
# exec run-menu -modi run,drun -show run -sidebar-mode "$@"

4.66.16. Google Chrome

file::run-chrome
#!/usr/bin/env sh

# exec google-chrome --remote-debugging-port=${CHROME_REMOTE_DEBUGGING_PORT:-9222} "$@"
exec google-chrome "$@"

4.66.17. Whatsapp

file::run-whatsapp
#!/usr/bin/env sh

# exec run-chrome --app=https://web.whatsapp.com/ "$@"
exec chromium --app=https://web.whatsapp.com/ "$@"

4.66.18. Desktop calendar

file::run-calendar
#!/usr/bin/env sh

exec run-chrome --app=https://calendar.google.com "$@"

4.67. Desktop utilities

4.67.1. run-tdrop - helper to run tdrop with default options

file::run-tdrop
#!/usr/bin/env sh

report-missing-executables tdrop tdrop || exit 1


# exec tdrop -h 60% --auto-detect-wm --monitor-aware term-emu zsh-user
exec tdrop --remember --monitor-aware "$@"

4.67.2. emacsclient-commander-for-dropdown - helper to display a commander window with Emacsclient

This executable is supposed to be used with tdrop, which, in turn, uses the executable name to perform various hacks in order to set X window properties. Hence, its name is prefixed with emacsclient.

file::emacsclient-commander-for-dropdown
#!/usr/bin/env dash

# TODO: Help text

commander_path_=${1:-/m/scratch/commander}
emacs_socket_name_=${EMACS_SOCKET_NAME:-edit}

exec emacsclient --socket-name="${emacs_socket_name_}" --no-wait --create-frame --eval "(~smart-open-file \"${commander_path_}\")"

4.67.3. toggle-dropdown-term-emu - toggles a dropdown terminal emulator

file::toggle-dropdown-term-emu
#!/usr/bin/env dash

exec run-tdrop term-emu zsh-user "$@"

4.67.4. toggle-dropdown-commander - toggles a dropdown commander window

file::toggle-dropdown-commander
#!/usr/bin/env dash

report-missing-executables emacsclient-commander-for-dropdown emacsclient-commander-for-dropdown || exit 1

exec run-tdrop emacsclient-commander-for-dropdown "$@"

4.68. disable-x-bell

file::disable-x-bell
#!/usr/bin/env sh

exec xset b off

4.69. dispatch-action - dispatches an action based on a string

file::dispatch-action
#!/usr/bin/env tclsh

# TODO: Documentation
# TODO: Help
# TODO: Read stdin?
# TODO: report-missing-executables
# TODO: Declarative configuration?

# TODO: Samples
# dispatch-action 'ssh://<foobar>!' 'w'
# dispatch-action 'ssh://<username>@<foobar>' '!' 'w'
# dispatch-action 'ssh://<username>@<foobar>:<port>' '!' 'w'
# dispatch-action 'ssh://<foobar>:/tmp/'
# dispatch-action 'ssh://<foobar>:/tmp/foobar'
# dispatch-action 'ssh://<username>@<foobar>:/tmp/'
# dispatch-action 'ssh://<username>@<foobar>:<port>:/tmp/foobar'

package require Tclx

##############################################################################
# Helpers
##############################################################################

## TODO: Documentation
proc stripPrefix {text prefix} {
    return [string range $text [string length $prefix] end]
}

proc orString {str elseStr} {
    if {[string trim $str] eq ""} {
        return $elseStr
    } else {
        return $str
    }
}

proc substEnvVars {str} {
    return [exec echo $str | envsubst]
}

## TODO: Documentation
proc splitString {text str {startIndex 0}} {
    set index [string first $str $text $startIndex]
    if {$index != -1} {
        set i1 [expr {$index - 1}]
        set i2 [expr {$index + [string length $str]}]
        return [list [string range $text 0 $i1] [string range $text $i2 end]]
    } else {
        return [list $text ""]
    }
}

proc constructEnrichedPathCmd {cmd} {
    return "deep-exec $cmd"
}

proc copyToClipboard {text} {
    set fd [open "| xsel -b" w]
    puts $fd $text
    close $fd
}

#
# Try opening a file.  TODO: Documentation for file pattern.
#
# \(~file-pattern? \"/tmp/aoeu\"\)                                        ⇒ t
# \(~file-pattern? \"/tmp/aoeu:10\"\)                                     ⇒ t
# \(~file-pattern? \"/tmp/aoeu:/hello world/\"\)                          ⇒ t
# \(~file-pattern? \"/tmp/non-existent\"\)                                ⇒ nil

# /tmp/aoeu -> /tmp/aoeu
# /tmp/aoeu:10
# /tmp/aoeu /hello/
# /tmp/aoeu:10 /hello/
# /tmp/aoeu +10 /hello/
proc tryOpeningFile {serverName inNewFrameP rest} {
    # Visit a file and return its buffer
    proc visitFile {serverName path} {
        return [exec rmacs --client-opts --alternate-editor=vim --name $serverName --no-wait visit $path <@ stdin 2>@ stderr]
    }

    proc gotoLine {serverName buffer number} {
        return [exec rmacs --client-opts --alternate-editor=vim --name $serverName --with-buffer $buffer eval "(goto-line $number)" <@ stdin >@ stdout 2>@ stderr]
    }

    proc gotoPattern {serverName buffer pattern} {
        return [exec rmacs --client-opts --alternate-editor=vim --name $serverName --with-buffer $buffer eval "(re-search-forward \"$pattern\")" <@ stdin >@ stdout 2>@ stderr]
    }

    set possiblePath [file normalize [lindex $rest 0]]
    if {[file exists $possiblePath]} {
        set buffer [visitFile $serverName $possiblePath]
    } else {
        set lastSepIndex [string last ":" $possiblePath]
        if {$lastSepIndex == -1} {
            return 0
        }

        set possibleRealPath [string range $possiblePath 0 $lastSepIndex-1]
        if {![file exists $possibleRealPath]} {
            return 0
        }
        set buffer [visitFile $serverName $possibleRealPath]
        set lineNumber [string range $possiblePath $lastSepIndex+1 end]
        set possiblePath $possibleRealPath
        catch {gotoLine $serverName $buffer $lineNumber}
    }

    foreach arg [lrange $rest 1 end] {
        switch -glob $arg {
            "+*" {
                set lineNumber [stripPrefix $arg {+}]
                catch {gotoLine $serverName $buffer $lineNumber}
            }
            "/*/" {
                set pattern [string range $arg 1 end-1]
                catch {gotoPattern $serverName $buffer $pattern}
            }
            default {
                puts stderr "ERROR: Unrecognized pattern for file path: $arg"
            }
        }
    }

    if {$inNewFrameP} {
        execl rmacs [list --client-opts --alternate-editor=vim --name $serverName --no-wait --new-frame open $possiblePath]
    } else {
        exec rmacs --client-opts --alternate-editor=vim --name $serverName open $possiblePath <@ stdin >@ stdout 2>@ stderr
    }

    return 1
}

proc callWMClientMenu {wmName client} {
    switch $wmName {
        "awesome" {
            execl awesome-client [list "display_client_menu_by_actionable_title('[string trim $client]')"]
        }
        "herbstluftwm" {
            set winID [lindex [split [string trim $client] " "] end]
            execl enrich-path [list wm herbstluft - call-menu client $winID]
        }
        default {
            execl run-menu [list "-e" "Error: Unrecognized window manager wmName=$wmName for client menu"]
        }
    }
}

proc callWMDesktopMenu {wmName desktop} {
    switch $wmName {
        "herbstluftwm" {
            execl enrich-path [list wm herbstluft - call-menu tag $desktop]
        }
        default {
            execl run-menu [list "-e" "Error: Unrecognized window manager wmName=$wmName for desktop menu"]
        }
    }
}

##############################################################################
# Main
##############################################################################

set text [string trim [join $::argv " "]]

if {[info exists ::env(RMACS_NAME)]} {
    set rmacsServerName $::env(RMACS_NAME)
} else {
    set rmacsServerName "edit"
}

if {$text ne ""} {
    switch -glob $text {
        "mux://*!!!*" {
            set rest [splitString [stripPrefix $text {mux://}] "!!!"]
            set muxSessionName [string trim [orString [substEnvVars [lindex $rest 0]] ":."]]
            set cmd [constructEnrichedPathCmd [lindex $rest 1]]
            execl with-mux-session [list $muxSessionName "-" {*}$cmd]
        }
        "mux://*!!*" {
            set rest [splitString [stripPrefix $text {mux://}] "!!"]
            set muxSessionName [string trim [orString [substEnvVars [lindex $rest 0]] ":."]]
            set cmd [constructEnrichedPathCmd [lindex $rest 1]]
            execl with-mux-session [list $muxSessionName "-" with-pause {*}$cmd]
        }
        "mux://*!*" {
            set rest [splitString [stripPrefix $text {mux://}] "!"]
            set muxSessionName [string trim [orString [substEnvVars [lindex $rest 0]] ":."]]
            set cmd [constructEnrichedPathCmd [lindex $rest 1]]
            exec tmux send-keys -l -t $muxSessionName $cmd
            execl tmux [list send-keys -t $muxSessionName Enter]
        }
        "ssh://*!*" {
            # Execute an SSH command
            set parts [splitString $text "!"]

            set hostExpr [string trim [substEnvVars [lindex $parts 0]]]
            set cmd [lindex $parts 1]

            execl ssh [list $hostExpr {*}$cmd]
        }
        "ssh://*:/*/" {
            # Expand a remote dir via SSH
            set parts [splitString [string trim $text] ":/" [string length "ssh://"]]

            set hostExpr [string trim [substEnvVars [lindex $parts 0]]]
            set path "/[lindex $parts 1]"

            # execl ssh [list $hostExpr ls -1 --indicator-style=slash --dereference --all --group-directories-first $path]
            execl ssh [list $hostExpr ls -1aHF --group-directories-first $path]
        }
        "ssh://*:/*" {
            # Edit a remote file via SSH
            # TODO: Make the edit functionality work
            set parts [splitString [string trim $text] ":/" [string length "ssh://"]]

            set hostExpr [string trim [substEnvVars [lindex $parts 0]]]
            set path "/[lindex $parts 1]"

            execl ssh [list $hostExpr cat $path]
        }
        "edit!*" {
            set path [exec which [stripPrefix $text {edit!}]]
            execl ffn [list $path]
        }
        "copy!*" {
            set text [stripPrefix $text {copy!}]
            copyToClipboard $text
        }
        "!%*" {
            set cmd [stripPrefix $text {!%}]
            execl with-env-user [list with-term-emu-sh --without-termux - $cmd]
        }
        "!@*" {
            set cmd [stripPrefix $text {!@}]
            execl with-env-user [list with-term-emu-sh --detach-termux - $cmd]
        }
        "!!!*" {
            set cmd [stripPrefix $text {!!!}]
            execl with-env-user [list with-term-emu-sh - {*}$cmd]
        }
        "!!*" {
            set cmd [stripPrefix $text {!!}]
            execl with-env-user [list with-term-emu-sh --pause-after-exec - $cmd]
        }
        "!*" {
            set cmd [stripPrefix $text {!}]
            execl with-env-user [list $::env(SHELL) -c $cmd]
        }
        "tmux :: *" {
            set tmuxSessionName [string trim [stripPrefix $text "tmux :: "]]
            execl bring-termux-session $tmuxSessionName
        }
        "wind :: *" {
            set wmName [exec deep-exec wm/get-wm-name]
            callWMClientMenu $wmName $text
        }
        "desktop :: *" {
            set wmName [exec deep-exec wm/get-wm-name]
            set desktop [stripPrefix $text {desktop :: }]
            callWMDesktopMenu $wmName $desktop
        }
        "file :: *" {
            set path [string trim [stripPrefix $text {file :: }]]
            tryOpeningFile $rmacsServerName 1 [list $path]
        }
        "*/" {
            set path [file nativename [substEnvVars [string trim $text]]]
            execl ls [list -1 --indicator-style=slash --dereference --all --group-directories-first $path]
        }
        default {
            if {![tryOpeningFile $rmacsServerName 0 $::argv]} {
                execl run-menu [list "-e" "Error: Unrecognized pattern: $text"]
            }
        }
    }
}

4.69.1. Testing scenarios

  • Run in a term emu without termux:

    dispatch-action '!% aoeu && pwd ; with-pause true ; htop'
    dispatch-action '!%' aoeu '&&' pwd ';' with-pause true ';' htop
  • Run in a detached termux → see a flash of the term emu, could find the command from a termux session

    dispatch-action '!@ aoeu && pwd ; with-pause true ; htop'
    dispatch-action '!@' aoeu '&&' pwd ';' with-pause true ';' htop
  • Run in a termux → see a term emu with a termux

    dispatch-action '!!! aoeu && pwd ; with-pause true ; htop'
    dispatch-action '!!!' aoeu '&&' pwd ';' with-pause true ';' htop
  • Run in a termux → see a term emu with a termux, paused upon exiting

    dispatch-action '!! aoeu && pwd ; with-pause true ; htop'
    dispatch-action '!!' aoeu '&&' pwd ';' with-pause true ';' htop
  • Run in a termux (subshell execution) → see a term emu with a termux, paused upon exiting

    dispatch-action '!! ( aoeu && pwd ; with-pause true ; htop)'
    dispatch-action '!!' '(' aoeu '&&' pwd ';' with-pause true ';' htop ')'

4.70. run-menu-and-dispatch - runs a menu program, allowing user to choose an item/input custom string, then dispatch an action based on the output

TODO: Documentation TODO: Support history file TODO: Make run-app-launcher depend on this

file::run-menu-and-dispatch
#!/usr/bin/env tclsh

package require Tcl 8
package require Tclx

if {[catch {exec report-missing-executables run-menu run-menu <@ stdin >@ stdout 2>@ stderr}]} {
    exit 1
}

proc spitToTempFile {input} {
    file tempfile tempPath
    set fd [open $tempPath w+]
    puts -nonewline $fd $input
    close $fd
    return $tempPath
}

proc getChoices {prompt input} {
    set fd [open "| run-menu -dmenu -p $prompt" r+]
    puts -nonewline $fd $input
    chan close $fd write
    catch {set choices [string trim [read $fd]]}
    catch {close $fd}
    return [split $choices "\n"]
}

set ::PROMPT [lindex $::argv 0]
set ::INPUT [read stdin]
set ::CHOICES [getChoices $::PROMPT $::INPUT]

if {$::CHOICES ne ""} {
    # execl dispatch-action [list $::CHOICES]
    foreach choice $::CHOICES {
        exec dispatch-action $choice
    }
}

4.71. call-omni-switcher-stdin - display a universal switcher

TODO: Help TODO: Note on standard input

file::call-omni-switcher-stdin
#!/usr/bin/env zsh

report-missing-executables grep Grep run-menu-and-dispatch run-menu-and-dispatch tmux Tmux || exit 1

rmacs_server_name_=edit
if rmacs list 2>&1 | grep -e "^${rmacs_server_name_}$" >/dev/null 2>&1; then
	rmacs_server_alive_p_=1
else
	rmacs_server_alive_p_=0
fi

exec cat --squeeze-blank - \
	<(tmux list-sessions -F "#{session_name}" | sed "s/^/tmux :: /g") \
	<([[ "${rmacs_server_alive_p_}" = 1 ]] \
		&& print $(rmacs --name "${rmacs_server_name_}" eval "(~format-opened-files)") | sed 's/^"//; s/"$//; s/^/file :: /g') \
	| grep -v -e '^$' \
	| run-menu-and-dispatch "Switch to"

4.72. call-wm-menu - calls the main window manager menu, current window manager is detected automatically

TODO: Help

file::call-wm-menu
#!/usr/bin/env tclsh

package require Tclx

set wmName [exec deep-exec wm/get-wm-name]
set menuName [lindex $::argv 0]

switch -exact $wmName {
    herbstluftwm {
        execl deep-exec [list wm/herbstluft/with-env call-menu $menuName]
    }
    default {
        execl run-menu [list -e "Menu: $menuName not supported"]
    }
}

4.73. add-to-history

TODO: Add description

file::add-to-history
#!/usr/bin/env tclsh

package require Tcl 8
package require cmdline

if {[catch [exec report-missing-executables flock util-linux]]} {
	exit 1
}

try {
	array set cmdArgs [::cmdline::getoptions ::argv {
		{max-history.arg 1000 "Maximum number of items stored in the history"}
	} {[--max-history <max-history>] <history-file-path>

TODO: Documentation
}]} trap {CMDLINE USAGE} {msg _o} {
	if {[llength $::argv] == 0} {
		puts $msg
		exit 0
	} else {
		puts stderr $msg
		exit 127
	}
}

# TODO: Handle errors or missing arguments

set maxHistory $cmdArgs(max-history)
set filePath [lindex $::argv 0]

##############################################################################
# Helpers
##############################################################################

proc slurpAndAdd {path line} {
	# Awk is to filter out blank lines and trim spaces
	set lines [split [exec echo $line | cat - $path | awk {NF { $1 = $1; print }} | dedup-lines] "\n"]
}

proc readLineFromStdin {} {
	gets stdin line
	return [string trim $line]
}

proc writeLines {path lines} {
	set fd [open $path w]
	puts $fd [join $lines "\n"]
	close $fd
}

##############################################################################
# Main
##############################################################################

if {![file exists $filePath]} {
	set baseDir [file dirname $filePath]
	if {![file exists $baseDir]} {
		file mkdir $baseDir
	}

	# Create the empty file
	close [open $filePath w]
}

# Make sure the history file is locked
if {!([info exists ::env(_FLOCKER_HISTORY_PATH_)] && $::env(_FLOCKER_HISTORY_PATH_) eq $filePath)} {
	package require Tclx
	execl flock [list --exclusive $filePath env "_FLOCKER_HISTORY_PATH_=$filePath" $::argv0 {*}$::argv]
}

set newLine [readLineFromStdin]
set newLines [slurpAndAdd $filePath $newLine]
set finalLines [lrange $newLines 0 [expr {$maxHistory - 1}]]

writeLines $filePath $finalLines

puts $newLine

4.74. normalize-filename

file::normalize-filename
#!/usr/bin/env sh

exec echo "$*" | tr -d -C '[[:alnum:]][[:space:]]'

4.75. all-dev-debs - lists all Debian -dev packages installed

file::all-dev-debs
#!/usr/bin/env bash

dpkg-query -l '*dev' | grep "^.i" | awk '{ print $2 }' | grep "\-dev$"

4.76. add-deb-repo - adds a Debian-based repository

file::add-deb-repo
#!/usr/bin/env bash

usage() {
	cat <<EOF
add-deb-repo <sources-repo.list> <dest-repo.list>

Add a Debian-compatible sources.list file to global repository.  Should there be a command to run after adding, put it as a comment on the first line of the sources.list file.
EOF
}

if [[ "${1}" = "--help" ]]; then
	usage
	exit 0
fi

if [[ "$#" -ne 2 ]]; then
	usage >&2
	exit 1
fi

repo_path_="${1}"
dest_="/etc/apt/sources.list.d/${2}"

with-sudo symlink "${repo_path_}" "${dest_}"

if [[ "$(cat ${repo_path_})" == "#"* ]]; then
	eval $(head -1 "${repo_path_}" | cut -d'#' -f2)
fi

4.77. local-tcp-open-p - checks if a local TCP port is opened

file::local-tcp-open-p
#!/usr/bin/env bash

if (test $# -eq 0); then
	cat <<EOF
Usage: `basename $0` <port>

Determines if a local TCP port is open.  Returns 0 if it is or 1 otherwise.
EOF
fi

report-missing-executables nc Netcat || exit 1

exec nc -z 127.0.0.1 "$1"

4.78. lockscreen

file::lockscreen
#!/usr/bin/env bash

# pgrep lightdm && gdmflexiserver || gnome-screensaver-command -l
xscreensaver-command -lock \
	|| gnome-screensaver-command -l \
	|| (sh -c "dbus-send --type=method_call --dest=org.gnome.ScreenSaver /org/gnome/ScreenSaver org.gnome.ScreenSaver.Lock")

4.79. monitor-off

file::monitor-off
exec xset -display :0 dpms force off

4.80. now-standardized

file::now-standardized
#!/usr/bin/env sh

report-missing-executables date Coreutils tr Coreutils || exit 1

exec date --rfc-3339=second | tr ' ' '_'

4.81. now-to-clipboard

file::now-to-clipboard
#!/usr/bin/env bash

xterm -e 'date -R | xsel -b'
file::symlink-p
#!/usr/bin/env sh

#
# Determines if a file is a symbolic link
#

test -L "$@"

TODO: --help

file::filter-broken-symlinks
#!/usr/bin/env bash

#
# Filter broken symlinks from the argument list
#

for file_ in "$@" ; do
    if [ ! -e "${file_}" ]; then
        echo "${file_}"
    fi
done

4.84. find-deep-path - returns the full path for a file/directory that is found from PATH

file::find-deep-path
#!/usr/bin/env tclsh

package require Tcl 8
package require Tclx
package require cmdline

if {[catch {exec report-missing-executables find "GNU Find" sort Coreutils <@ stdin >@ stdout 2>@ stderr}]} {
    exit 1
}

proc usage {{fd stdout}} {
    puts $fd {find-deep-path <command>

TODO}
}

proc findExec {cmd} {
    set rawPaths [exec echo $::env(PATH) | sed {s/:/\n/g} | sort | uniq]
    set res {}
    foreach path $rawPaths {
        if {[file exists "$path/$cmd"]} {
            return "$path/$cmd"
        }
    }
    return {}
}

if {$::argv == "--help"} {
    usage
}
if {$::argc != 1} {
    usage stderr
    exit 1
}

set path [findExec [lindex $::argv 0]]

if {$path eq ""} {
    exit 2
} else {
    puts $path
}

4.85. prompt-y-n - prompts for a yes/no answer

Prompts a yes/no answer, exiting with code 0 for yes and non-zero for no.

file::prompt-y-n
#!/usr/bin/env tclsh

proc getDefaultChoice {choice} {
	if {[string equal "" $choice]} {
		return "y"
	} else {
		return $choice
	}
}

proc getAnswer {default} {
	set answer [string trim [gets stdin]]
	if {[string equal "" $answer]} {
		return $default
	} else {
		return $answer
	}
}

set prompt [string trim [lindex $argv 0]]
set defaultChoice [getDefaultChoice [string trim [lindex $argv 1]]]

puts -nonewline "$prompt \[y/n\] ($defaultChoice) "
flush stdout

set answer [getAnswer $defaultChoice]
if {[string equal "y" $answer]} {
	exit 0
} else {
	exit 1
}

4.86. executable-exists <exec-file> - checks if an executable exists in PATH

Checks whether an executable exists in one of the `PATH`s, returning exit code 0 if it does and 127 otherwise.

file::executable-exists
#!/usr/bin/env sh

command -v "$@" >/dev/null 2>&1

4.87. report-missing-executables <bin-1> <software-1> <bin-2> <software-2> …​

Reports missing software by checking if their corresponding executables exist. If all executables are found, exit with status 0; otherwise, exit with status 1.

Sample usage:

report-missing-executables aria2c Aria2 wget Wget
#
# aria2c and wget not found
# Make sure Aria2, Wget are installed

report-missing-executables aria2c Aria2 wget Wget curl cURL
#
# aria2c, curl, and wget not found
# Make sure Aria2, Wget, cURL are installed

report-missing-executables aria2c Aria2 wget
#
# Invalid arguments.  Number of arguments must be even.
file::report-missing-executables
#!/usr/bin/env tclsh

proc showHelp {} {
    puts {Usage:
  report-missing-executables <exec-1> <prog-1> ...
  report-missing-executables --help

Reports missing software by checking if their corresponding executables exist.
If all executables are found, exit with status 0; otherwise, exit with status
1.

E.g.

  report-missing-executables aria2c Aria2 wget Wget
    # aria2c and wget not found
    # Make sure Aria2, Wget are installed

  report-missing-executables aria2c Aria2 wget Wget curl cURL
    # aria2c, curl, and wget not found
    # Make sure Aria2, Wget, cURL are installed

  report-missing-executables aria2c Aria2 wget
    # Invalid arguments.  Number of arguments must be even.}
}

if {$::argc == 0 || $::argv eq {--help}} {
    showHelp
    exit 0
}

if {$::argc % 2 == 1} {
    puts stderr "ERROR: Invalid arguments.  Number of arguments must be even."
    exit 2
}

proc notifyMissingExecs {execs} {
    set execStr [join $execs ", "]
    exec notify-send --urgency=critical "Missing Software" "Missing $execStr" <@ stdin >@ stdout 2>@ stderr
}

set missingExecs {}
set missingApps {}
for {set i 0} {$i < $::argc} {incr i 2} {
    set execName [lindex $::argv $i]
    set appName [lindex $::argv [expr {$i + 1}]]
    if {[catch {exec get-exec $execName > /dev/null 2> /dev/null}]} {
        set missingExecs [list {*}$missingExecs $execName]
        set missingApps [list {*}$missingApps $appName]
    }
}

switch [llength $missingApps] {
    0 {
        exit 0
    }
    1 {
        puts stderr "$missingExecs not found"
        puts stderr "Make sure $missingApps is installed"
        notifyMissingExecs $missingExecs
    }
    default {
        set execStr [join $missingExecs ", "]
        set appStr [join $missingApps ", "]
        puts stderr "$execStr not found"
        puts stderr "Make sure $appStr are installed"
        notifyMissingExecs $missingExecs
    }
}

4.88. prefix - prefixes all lines read from stdin with a string

file::prefix
#!/usr/bin/env 9-rc

#
# Prefixes all lines read from stdin.
#

prefix=$1 {
	if (test $#prefix -eq 0) {
		prefix='# '
	}
	sed 's/^/'^$prefix^'/g'
}

4.89. suffix - suffixes all lines read from stdin with a string

file::suffix
#!/usr/bin/env 9-rc

#
# Prefixes all lines read from stdin.
#

suffix=$1 {
	sed 's/$/'^$suffix^'/g'
}

4.90. query-password - queries password from a password manager

The password is printed to stdout without an end-of-line character.

TODO: Help text

file::query-password
#!/usr/bin/env tclsh

if {[catch {exec report-missing-executables kwalletcli KWallet-CLI >@ stdout 2>@ stderr <@ stdin}]} {
    exit 1
}

set folder Passwords
set entry [lindex $::argv 0]

if {[catch {exec kwalletcli -f $folder -e $entry >@ stdout <@ stdin}]} {
    # Password doesn't exist
    exec kwalletcli_getpin -p "Set password" -t "Password not yet set. Please set it now" -Y "_Set" | kwalletcli -f $folder -e $entry -P
    exec kwalletcli -f $folder -e $entry >@ stdout <@ stdin
}

4.90.1. query-sudo-password - queries Sudo password with query-password

The password is printed to stdout without an end-of-line character.

TODO: Help text

file::query-sudo-password
#!/usr/bin/env sh

report-missing-executables query-password query-password || exit 1

exec query-password Sandwich

4.91. qrcode - creates QR code from a string

file::qrcode
#!/usr/bin/env 9-rc

report-missing-executables \
	tempfile "tempfile utility" \
	qrencode Qrencode \
	|| exit 1

# FIXME: Not working
if (test $#* -eq 0) {
	echo No argument found
}

tmpfile=`{tempfile}^.png

qrencode -o $tmpfile -s 5 $*
do-notify-short $tmpfile' created'
display $tmpfile

4.92. system-temperature

file::system-temperature
#!/usr/bin/env bash

echo "-> Starting HDDTemp if necessary"
nc localhost 7634 &>/dev/null || (
	exec sudo hddtemp -d /dev/sda
)
echo ""

echo "-> HDD temperature"
nc localhost 7634

echo "-> CPU temperature"
sensors

4.93. running-p - determines if a process is running

TODO: Remove due to not being reliable

file::running-p
#!/usr/bin/env sh

#
# Determines if a process is running using pgrep.
#

exec pgrep "$@" &>/dev/null

4.94. using-x-p - determines if we’re using an X server

file::using-x-p
#!/usr/bin/env sh

report-missing-executables xset x11-xserver-utils

exec xset q >/dev/null 2>&1

4.95. show-keyboard - shows keyboard of modifiers, convenient when making screencast

file::show-keyboard
#!/usr/bin/env bash

report-missing-executables key-mon key-mon || exit 1

key-mon --decorated --meta --theme modern "$@"

4.96. with-sudo - runs sudo with some environment variables preserved

TODO: Help text

file::with-sudo
#!/usr/bin/env tclsh

if {[catch {exec report-missing-executables sudo sudo <@ stdin >@ stdout 2>@ stderr}]} {
    exit 1
}

package require Tclx

if {[lsearch $::argv "-"] == -1} {
    set args [list "-" {*}$::argv]
} else {
    set args $::argv
}

set sudoArgs {}
set cmd {}
set beforeDash 1
foreach arg $args {
    if {$arg eq "-i"} {
        execl sudo [list -i]
    }

    if {$arg eq "-"} {
        set beforeDash 0
        continue
    }

    if {$beforeDash == 1} {
        lappend sudoArgs $arg
    } else {
        lappend cmd $arg
    }
}

execl sudo [list {*}$sudoArgs -E env "PATH=$::env(PATH)" {*}$cmd]

4.97. with-sudo-wallet - runs with-sudo, uses a password manager to manage the Sudo password

TODO: Help text

file::with-sudo-wallet
#!/usr/bin/env tclsh

if {[catch {exec report-missing-executables sudo sudo query-sudo-password query-sudo-password <@ stdin >@ stdout 2>@ stderr}]} {
    exit 1
}

package require Tclx

if {[lsearch $::argv "-"] == -1} {
    set args [list "-" {*}$::argv]
} else {
    set args $::argv
}

set sudoArgs {}
set cmd {}
set beforeDash 1
foreach arg $args {
    if {$arg eq "-i"} {
        puts stderr "ERROR: Cannot run interactive sudo (-i) with this command"
        exit 2
    }

    if {$arg eq "-"} {
        set beforeDash 0
        continue
    }

    if {$beforeDash == 1} {
        lappend sudoArgs $arg
    } else {
        lappend cmd $arg
    }
}

set fd [open [list | query-sudo-password Sandwich] r]
set password [read $fd]
close $fd

exec echo $password | with-sudo -k --stdin --prompt "" {*}$sudoArgs - {*}$cmd <@ stdin >@ stdout 2>@ stderr

4.98. with-workdir <dir> <command> [args…​] - runs a command in a directory

TODO Group all shell utils in one section

TODO Help text

file::with-workdir
#!/usr/bin/env sh

cd "${1}"
shift
exec "$@"

4.99. get-all-x-displays - gets all X displays

file::get-all-x-displays
#!/usr/bin/env tclsh

proc usage {{outFD stdout}} {
    puts $outFD {get-all-x-displays

Returns all Xorg displays.}
}

if {[lindex $::argv 0] eq "--help"} {
    usage
    exit 0
}

set ::X11_SOCKET_PATH /tmp/.X11-unix

if {[file exists $::X11_SOCKET_PATH]} {
    set ::DISPLAYS [string trim [exec ls $::X11_SOCKET_PATH | tr 'X' ':' <@ stdin]]
    foreach display $::DISPLAYS {
        puts $::display
    }
}

4.100. get-all-logged-in-x-displays - gets all X displays of logged in users

file::get-all-logged-in-x-displays
#!/usr/bin/env tclsh

proc usage {{outFD stdout}} {
    puts $outFD {get-all-logged-in-x-displays

Returns all Xorg displays of logged in users.}
}

if {[lindex $::argv 0] eq "--help"} {
    usage
    exit 0
}

set ::X11_SOCKET_PATH /tmp/.X11-unix

if {[file exists $::X11_SOCKET_PATH]} {
    set ::DISPLAYS [string trim [exec w | awk {{ print $3 }} | grep -e {^:} | sort | uniq <@ stdin]]
    foreach display $::DISPLAYS {
        puts $::display
    }
}

4.101. with-all-x-displays - runs a command with all X displays

file::with-all-x-displays
#!/usr/bin/env tclsh

if {[catch {exec report-missing-executables get-all-x-displays get-all-x-displays >@ stdout 2>@ stderr}]} {
    exit 1
}

proc usage {{outFD stdout}} {
    puts $outFD {with-all-x-displays <command> [args...]

Run a command with all Xorg displays.}
}

if {$::argc == 0} {
    usage stderr
    exit 1
} elseif {[lindex $::argv 0] eq "--help"} {
    usage
    exit 0
}

set displays [string trim [exec get-all-x-displays <@ stdin]]

foreach display $displays {
    exec env "DISPLAY=$display" {*}$::argv <@ stdin >@ stdout 2>@ stderr
}

4.102. with-all-logged-in-x-displays - runs a command with all X displays

file::with-all-logged-x-displays
#!/usr/bin/env tclsh

if {[catch {exec report-missing-executables get-all-logged-in-x-displays get-all-logged-in-x-displays >@ stdout 2>@ stderr}]} {
    exit 1
}

proc usage {{outFD stdout}} {
    puts $outFD {with-all-logged-in-x-displays <command> [args...]

Run a command with all Xorg displays for logged in users.}
}

if {$::argc == 0} {
    usage stderr
    exit 1
} elseif {[lindex $::argv 0] eq "--help"} {
    usage
    exit 0
}

set displays [string trim [exec get-all-logged-in-x-displays <@ stdin]]

foreach display $displays {
    exec env "DISPLAY=$display" {*}$::argv <@ stdin >@ stdout 2>@ stderr
}

4.103. prompt-y-n-gui-all-displays - prompts a yes/no answer in all current displays

file::prompt-y-n-gui-all-displays
#!/usr/bin/env tclsh

# TODO: Doc: Works with all DISPLAYs
# TODO: Doc: One answer is enough

if {[catch {exec report-missing-executables zenity Zenity get-all-x-displays get-all-x-displays >@ stdout 2>@ stderr}]} {
    exit 1
}

proc usage {{outFD stdout}} {
    puts $outFD {prompt-gui <question>

Prompts a yes/no answer in all current displays and exits as soon as an answer is received.  Returns 0 in case of positive answer and 1 otherwise.}
}

if {$::argc == 0} {
    usage stderr
    exit 1
} elseif {[lindex $::argv 0] eq "--help"} {
    usage
    exit 0
}

set ::QUESTION [join $argv " "]
set ::DISPLAYS [exec get-all-x-displays <@ stdin]
set ::THREADS {}

package require Tcl 8.4
package require Thread 2.8

foreach display $::DISPLAYS {
    set t [thread::create {
        package require BLT
        package require Thread

        proc exitNow args {
            global ::EXIT_STATUS, ::MAIN_THREAD_ID
            lassign $::EXIT_STATUS _ _ exitCode _
            thread::send -async $::MAIN_THREAD_ID [list set ::ANSWER $exitCode]
        }

        trace add variable ::EXIT_STATUS write exitNow

        thread::wait
    }]
    thread::send $t [list set ::MAIN_THREAD_ID [thread::id]]
    thread::send -async $t [list blt::bgexec ::EXIT_STATUS zenity --question "--text=$::QUESTION" --display=$display]
    lappend ::THREADS $t
}

vwait ::ANSWER
set ::SAVED_ANSWER $::ANSWER

foreach t $::THREADS {
    if {[thread::exists $t]} {
        thread::send $t [list set ::EXIT_STATUS {0 0 0 0}]
        thread::cancel $t
        thread::release $t
    }
}

exit [expr {($::SAVED_ANSWER == 1)}]

4.104. prompt-y-n-gui-all-logged-in-displays - prompts a yes/no answer in all current displays for logged in users

file::prompt-y-n-gui-all-logged-in-displays
#!/usr/bin/env tclsh

# TODO: Doc: Works with all DISPLAYs
# TODO: Doc: One answer is enough

if {[catch {exec report-missing-executables zenity Zenity get-all-x-displays get-all-x-displays >@ stdout 2>@ stderr}]} {
    exit 1
}

proc usage {{outFD stdout}} {
    puts $outFD {prompt-gui <question>

Prompts a yes/no answer in all current displays for all logged in users and exits as soon as an answer is received.  Returns 0 in case of positive answer and 1 otherwise.}
}

if {$::argc == 0} {
    usage stderr
    exit 1
} elseif {[lindex $::argv 0] eq "--help"} {
    usage
    exit 0
}

set ::QUESTION [join $argv " "]
set ::DISPLAYS [exec get-all-logged-in-x-displays <@ stdin]
set ::THREADS {}

package require Tcl 8.4
package require Thread 2.8

foreach display $::DISPLAYS {
    set t [thread::create {
        package require BLT
        package require Thread

        proc exitNow args {
            global ::EXIT_STATUS, ::MAIN_THREAD_ID
            lassign $::EXIT_STATUS _ _ exitCode _
            thread::send -async $::MAIN_THREAD_ID [list set ::ANSWER $exitCode]
        }

        trace add variable ::EXIT_STATUS write exitNow

        thread::wait
    }]
    thread::send $t [list set ::MAIN_THREAD_ID [thread::id]]
    thread::send -async $t [list blt::bgexec ::EXIT_STATUS zenity --question "--text=$::QUESTION" --display=$display]
    lappend ::THREADS $t
}

vwait ::ANSWER
set ::SAVED_ANSWER $::ANSWER

foreach t $::THREADS {
    if {[thread::exists $t]} {
        thread::send $t [list set ::EXIT_STATUS {0 0 0 0}]
        thread::cancel $t
        thread::release $t
    }
}

exit [expr {($::SAVED_ANSWER == 1)}]

4.105. event/ - event-based triggers

Collection of executables that get triggered in case of an event

4.105.1. event/watch-change <timeout> <get-state> - <trigger>…​ - watches for a change (= 2 different states returned by <get-state>) and triggers a script

TODO: Help text and example

file::event/watch-change
#!/usr/bin/env tclsh

proc slurp {cmds} {
    set fd [open [list | {*}$cmds] r]
    set data [read $fd]
    close $fd

    return $data
}

proc lpop {listVar} {
    upvar 1 $listVar l
    set res [lindex $l 0]
    set l [lreplace $l [set l 0] 0]
    return $res
}

proc takeUntil {listVar element} {
    upvar 1 $listVar l
    set res {}

    set x [lpop l]
    while {$x ne $element && $x ne ""} {
        lappend res $x
        set x [lpop l]
    }

    return $res
}

set ::TIMEOUT [lpop ::argv]
set ::GET_STATE_CMD [takeUntil ::argv "-"]
set ::TRIGGER_CMD [lrange $::argv 0 end]

# puts "$::TIMEOUT"
# puts "$::GET_STATE_CMD"
# puts "$::TRIGGER_CMD"
# exit 0

if {$::TIMEOUT eq "" || $::GET_STATE_CMD eq "" || $::TRIGGER_CMD eq ""} {
    puts stderr "ERROR: Timeout, get-state, and trigger must exist"
    exit 2
}

set state [slurp $::GET_STATE_CMD]
while {1} {
    after $::TIMEOUT
    set newState [slurp $::GET_STATE_CMD]
    if {$newState ne $state} {
        catch {exec {*}$::TRIGGER_CMD <@ stdin >@ stdout 2>@ stderr}
    }
    set state $newState
}

4.105.2. event/get-monitors-states - TODO

file::event/get-monitors-states
#!/usr/bin/env tclsh

# /sys/class/drm/*/status

proc slurp {cmds} {
    set fd [open [list | {*}$cmds] r]
    set data [read $fd]
    close $fd

    return $data
}

proc getCurrentState {} {
    set paths [glob /sys/class/drm/*/status]
    set statuses [slurp [list cat {*}$paths]]
    return "{$paths} {$statuses}"
}

puts [getCurrentState]

4.105.3. get-connected-monitors - TODO

file::get-connected-monitors
#!/usr/bin/env python3

import gi
gi.require_version("Gdk", "3.0")


from gi.repository import Gdk
display = Gdk.Display.get_default()

for i in range(display.get_n_monitors()):
    print(display.get_monitor(i).get_model())

4.106. sudo-askpass - runs sudo with a graphical askpass program, also preserving some environment variables

file::sudo-askpass
#!/usr/bin/env sh

if ! sudo --help >/dev/null 2>&1; then
	echo "sudo not found, please install sudo" >&2
	exit 1
fi

export SUDO_ASKPASS=${SUDO_ASKPASS:-$(which ssh-askpass)}
export DISPLAY=${DISPLAY:-":0"}

exec sudo --askpass -tt -E env "PATH=${PATH}" "$@"

4.107. suspend-me - suspends computer

file::suspend-me
#!/usr/bin/env sh

# exec sudo pm-suspend && lockscreen
# exec sudo pm-suspend
exec systemctl suspend

4.108. take-from - takes all lines from stdin, starting from a pattern

file::take-from
#!/usr/bin/env 9-rc

#
# Takes all lines from a pattern (representing by $1), using GNU Awk.
#

gawk 'BEGIN {
	found = 0
}
/'^$1^'/ {
	found = 1
}
{
	if (found == 1) {
		print $0
	}
}'

4.109. take-lines - takes the first n lines

file::take-lines
#!/usr/bin/env 9-rc

#
# Takes the first $1 lines using Plan 9's seq.
#

if (test $#* -eq 0) {
	n_lines=1
}
if not {
	n_lines=$1
}
sed $n_lines^q

4.110. wget-site

file::wget-site
#!/usr/bin/env bash

wget \
	--recursive \
	--no-clobber \
	--page-requisites \
	--html-extension \
	--convert-links \
	--timestamping \
	--no-parent \
	--mirror \
	"$@"

#
# --recursive             download the entire Web site.
# --domains website.org   don't follow links outside website.org.
# --no-parent             don't follow links outside the directory tutorials/html/.
# --page-requisites       get all the elements that compose the page (images, CSS and so on).
# --html-extension        save files with the .html extension.
# --convert-links         convert links so that they work locally, off-line.
# --no-clobber            don't overwrite any existing files (used in case the download is interrupted and
#                         resumed).
# --mirror                create mirror
#

4.111. wm/ - utils for window manager (WM)

4.111.1. format-time-for-panel - formats date & time to display in a desktop panel

file::wm/format-time-for-panel
#!/usr/bin/env sh

exec date +'%A, %d %b %Y, %H:%M:%S %p'

4.111.2. get-wm-name - retrieves the current WM name

file::wm/get-wm-name
#!/usr/bin/env sh

report-missing-executables wmctrl wmctrl || exit 1

wmctrl -m | grep 'Name: ' | cut -d' ' -f2-

4.111.3. pprint-client-list - pretty-prints the list of clients with their title & IDs

file::wm/pprint-client-list
#!/usr/bin/env sh

report-missing-executables wmctrl wmctrl || exit 1

exec wmctrl -lx \
	| awk '{ for (i = 5; i <= NF; i++) printf $i" "; print "::", $3, "::", $1 }'

4.111.4. pprint-desktop-list - pretty-prints the list of desktops

file::wm/pprint-desktop-list
#!/usr/bin/env sh

report-missing-executables wmctrl wmctrl || exit 1

exec wmctrl -d \
	| awk '{ print $NF }' \
	| sort | uniq

4.111.5. get-desktop-name - gets the current desktop name

file::wm/get-desktop-name
#!/usr/bin/env sh

report-missing-executables wmctrl wmctrl || exit 1

exec wmctrl -d | awk '{ if ($2 == "*" ) print $NF }'

4.111.6. copy-client-description - copies description for a client

file::wm/copy-client-description
#!/usr/bin/env sh

report-missing-executables wmctrl wmctrl || exit 1

client_=$(pprint-client-list | run-menu -dmenu -p "Client" 2>/dev/null)

if [ -n "${client_}" ]; then
	exec dispatch-action 'copy!'"${client_}"
fi

4.111.7. copy-client-id - copies a client ID

file::wm/copy-client-id
#!/usr/bin/env sh

report-missing-executables wmctrl wmctrl || exit 1

client_id_=$(pprint-client-list | run-menu -dmenu -p "Client" 2>/dev/null | awk '{ print $NF }')

if [ -n "${client_id_}" ]; then
	exec dispatch-action 'copy!'"${client_id_}"
fi

4.111.8. call-omni-switcher - calls the omni switcher with WM patterns

file::wm/call-omni-switcher
#!/usr/bin/env zsh

report-missing-executables wmctrl wmctrl || exit 1

cat --squeeze-blank \
    <(pprint-client-list | awk '{ print "wind ::", $0 }') \
    <(pprint-desktop-list | awk '{ print "desktop ::", $0 }') \
    | grep -v -e '^$' \
    | call-omni-switcher-stdin

4.111.9. call-desktop-list - calls desktop list menu that allows interaction with current desktops

file::wm/call-desktop-list
#!/usr/bin/env zsh

report-missing-executables wmctrl wmctrl || exit 1

cat --squeeze-blank \
    <(pprint-desktop-list | awk '{ print "desktop ::", $0 }') \
    | grep -v -e '^$' \
    | run-menu-and-dispatch

4.111.10. get-menu <menu-name>

TODO: assert argv

file::wm/get-menu
#!/usr/bin/env tclsh

package require Tclx

set menuName [lindex $::argv 0]

catch {puts "(popup-menu [exec $menuName-menu 2>@ stderr])"} err opts
if {[dict get $opts -code] != 0} {
    set errStr "ERROR: Failed to get menu (menuName=$menuName): $err"
    puts stderr $errStr
    execl run-menu [list -e $errStr]
}

4.111.11. menus/ - common menus across all WMs

menus/wm-menu
file::wm/menus/wm-menu
#!/usr/bin/env -S guile -s
# -*- mode: scheme -*-
!#

(write '(("[_x] Execute client command" wm-exec-client-command)
         ("[_r] Reload" wm-reload)
         ("[_s] Restart" wm-wmexec)
         ("[_q] Quit" wm-quit)))
menus/app-menu
file::wm/menus/app-menu
#!/usr/bin/env -S guile -s
# -*- mode: scheme -*-
!#

(write '(("[_a] Config input" app-config-input)
         ("[_m] Terminal emulator" app-term-emu)
         ("[_c] Clipboard" app-clipboard)
         ("[_w] Web browser" app-web-browser)
         ("[_o] Edit TODOs" app-todo-editor)
         ("[_t] Open Toolbox" app-toolbox)
         ()
         ("[_l] Lock screen" app-lock-screen)
         ("[_s] Suspend" app-suspend)))
menus/client-menu
file::wm/menus/client-menu
#!/usr/bin/env -S guile -s
# -*- mode: scheme -*-
!#

(write '(("[_b] Bring here" bring-client-here)
         ("[_j] Jump to" jump-to-client)))

4.111.12. herbstluft/ - utils to manage HerbstluftWM

TODO: Document on a menu-driven/guiding UX

with-env - runs a command in the HerbstluftWM environment
file::wm/herbstluft/with-env
#!/usr/bin/env bash

exec enrich-path wm herbstluft menus - "$@"
call-menu - performs a high-level WM action

TODO: Report errors after catch

file::wm/herbstluft/call-menu
#!/usr/bin/env tclsh

package require Tclx

set menuName [lindex $::argv 0]
if {[catch {set action [exec get-menu $menuName 2> /dev/null | run-context-menu]} err]} {
    execl run-menu [list -e "Error when getting menu (menu=$menuName): $err"]
}

proc stripPrefix {text prefix} {
    return [string range $text [string length $prefix] end]
}

proc callHC {args} {
    return [exec herbstclient {*}$args]
}

proc getTagList {} {
    return [split [string trim [callHC object_tree tags.by-name | tail -n +2 | awk '{ print $NF }']] "\n"]
}

proc getCurrentTag {} {
    return [exec get-desktop-name]
}

switch -glob $action {
    "main-menu-call-*" {
        set menuType [stripPrefix $action "main-menu-call-"]
        execl call-menu [list $menuType]
    }
    "wm-*" {
        set cmd [stripPrefix $action "wm-"]
        if {$cmd eq "exec-client-command"} {
            catch {
                set cmd [string trim [callHC list_commands | xargs -I{} echo "{}" | sort | run-menu -dmenu -p "Herbstluft command"]]
                set output "$cmd output:\n[callHC {*}$cmd]"
                puts -nonewline $output
                execl run-menu [list -e $output]
            }
        } else {
            callHC $cmd
        }
    }
    "split-frame-*" {
        set direction [stripPrefix $action "split-frame-"]
        callHC split $direction

        switch $direction {
            "left" {
                callHC focus left
            }
            "right" {
                callHC focus right
            }
            "top" {
                callHC focus up
            }
            "bottom" {
                callHC focus down
            }
        }
    }
    "set-frame-layout-*" {
        set layout [stripPrefix $action "set-frame-layout-"]
        callHC set_layout $layout
    }
    "remove-frame" {
        callHC remove
    }
    "app-*" {
        set appName [stripPrefix $action "app-"]
        switch $appName {
            term-emu {
                callHC spawn x-terminal-emulator
            }
            toolbox {
                # TODO: Use env var
                callHC spawn ffn /m/toolbox/Toolbox
            }
            todo-editor {
                # TODO: Use env var
                callHC spawn ffn /m/toolbox/TODO.org
            }
            config-input {
                callHC spawn config-inputs-cmpitg
            }
            clipboard {
                callHC spawn display-clipboard
            }
            lock-screen {
                callHC spawn lockscreen
            }
            suspend {
                callHC spawn suspend-me
            }
        }
    }
    "toggle-focused-client-*" {
        set attr [stripPrefix $action "toggle-focused-client-"]
        switch $attr {
            floating {
                callHC attr clients.focus.floating toggle
            }
            fullscreen {
                # callHC fullscreen toggle
                execl $::env(HOME)/.guix-profile/share/doc/herbstluftwm/examples/maximize.sh
            }
        }
    }
    "move-client-to-monitor-*" {
        set direction [stripPrefix $action "move-client-to-monitor-"]
        switch $direction {
            up {
                set arg "-u"
            }
            down {
                set arg "-d"
            }
            left {
                set arg "-l"
            }
            right {
                set arg "-r"
            }
        }
        callHC shift_to_monitor $arg ""
    }
    "move-client-*" {
        set direction [stripPrefix $action "move-client-"]
        callHC shift $direction
    }
    "bring-client-here" {
        set clientID [lindex $::argv 1]
        callHC bring $clientID
    }
    "jump-to-client" {
        set clientID [lindex $::argv 1]
        callHC jumpto $clientID
    }
    "untag-focused-client" {
        callHC move default
    }
    "bring-tag-here" {
        set tagName [lindex $::argv 1]
        callHC use $tagName
    }
    "kill-tag" {
        set tagName [lindex $::argv 1]
        callHC merge_tag $tagName [getCurrentTag]
    }
    "add-tag" {
        catch {
            set tagName [string trim [exec yad {--title=Add tag} --entry {--entry-label=Tag name} 2>/dev/null]]
            if {$tagName ne ""} {
                callHC add $tagName
            }
        }
    }
    "rename-current-tag" {
        catch {
            set tagName [string trim [exec yad {--title=Rename tag} --entry {--entry-label=New name} 2>/dev/null]]
            if {$tagName ne ""} {
                callHC rename [getCurrentTag] $tagName
            }
        }
    }
    "kill-current-tag" {
        callHC merge_tag [getCurrentTag] default
    }
    "()" {}
    default {
        execl run-menu [list -e "Unknown action=$action"]
    }
}
menus/current-tag-menu
file::wm/herbstluft/menus/current-tag-menu
#!/usr/bin/env -S guile -s
# -*- mode: scheme -*-
!#

(write '(("[_r] Rename" rename-current-tag)
         ("[_k] Kill" kill-current-tag)))
menus/focused-client-menu
file::wm/herbstluft/menus/focused-client-menu
#!/usr/bin/env -S guile -s
# -*- mode: scheme -*-
!#

(write '(("[_f] Toggle fullscreen" toggle-focused-client-fullscreen)
         ("[_o] Toggle floating" toggle-focused-client-floating)
         ()
         ("[_n] Untag" untag-focused-client)
         ()
         ("[_u] Move up" move-client-up)
         ("[_d] Move down" move-client-down)
         ("[_l] Move left" move-client-left)
         ("[_r] Move right" move-client-right)
         ()
         ("[_a] Move to monitor above" move-client-to-monitor-up)
         ("[_b] Move to monitor below" move-client-to-monitor-down)
         ("[_e] Move to left monitor" move-client-to-monitor-left)
         ("[_i] Move to right monitor" move-client-to-monitor-right)))
menus/frame-menu
file::wm/herbstluft/menus/frame-menu
#!/usr/bin/env -S guile -s
# -*- mode: scheme -*-
!#

(write '(("[_u] Split (up)" split-frame-top)
         ("[_d] Split (down)" split-frame-bottom)
         ("[_l] Split (left)" split-frame-left)
         ("[_r] Split (right)" split-frame-right)
         ("[_a] Split (auto)" split-frame-auto)
         ()
         ("[_m] Layout max" set-frame-layout-max)
         ("[_v] Layout vertical" set-frame-layout-vertical)
         ("[_h] Layout horizontal" set-frame-layout-horizontal)
         ()
         ("[_k] Kill" remove-frame)))
menus/main-menu
file::wm/herbstluft/menus/main-menu
#!/usr/bin/env -S guile -s
# -*- mode: scheme -*-
!#

(use-modules (ice-9 popen))

(define (read-output cmd)
  (let* ((port (open-input-pipe cmd))
         (data (read port)))
    (close-pipe port)
    data))

(write `(("[_g] Add tag" add-tag)
         ()
         ("[_a] App" . ,(read-output "app-menu"))
         ("[_f] Frame" . ,(read-output "frame-menu"))
         ("[_c] Focused client" . ,(read-output "focused-client-menu"))
         ("[_t] Current tag" . ,(read-output "current-tag-menu"))
         ()
         ("[_w] WM" . ,(read-output "wm-menu"))))
menus/tag-menu
file::wm/herbstluft/menus/tag-menu
#!/usr/bin/env -S guile -s
# -*- mode: scheme -*-
!#

(write '(("[_b] Bring here" bring-tag-here)
         ("[_k] Kill" kill-tag)))

4.112. start-xephyr - starts a Xephyr server (for debugging window manager)

TODO: Review

TODO: Help text

file::start-xephyr
#!/usr/bin/env bash

report-missing-executables Xephyr Xephyr || exit 1

resolution_=${RESOLUTION:-800x600}

Xephyr \
	-ac \
	-br \
	-noreset \
	-screen ${resolution_} \
	:1 "$@" >/dev/null & disown

# export DISPLAY=:1.0
echo Display: $DISPLAY

4.113. openfile-dialog - creates a open-file dialog and prints the selected path to stdout

file::openfile-dialog
#!/usr/bin/env bash

report-missing-executables zenity Zenity || exit 1

zenity --file-selection --filename `pwd` "$@" 2>/dev/null

4.114. edit-which - ${EDITOR} $(which <executable>)

file::edit-which
#!/usr/bin/env dash

exec "${EDITOR}" $(which "${1}") "@$"

4.115. compute-checksum-url - computes checksum for remote file

compute-checksum-url <checksum-tool> <uri> [curl-options] …​

E.g.

file::compute-checksum-url
#!/usr/bin/env sh

checksum_tool_="${1}"
shift

report-missing-executables curl cURL "${checksum_tool_}" "${checksum_tool_}" || exit 1

exec curl --silent --location "$@" | "${checksum_tool_}" | cut -d ' ' -f 1

4.116. safe-download - downloads file, prompts overwriting, and compares checksums

Downloads a file, compares checksums, and prompts overwriting when necessary. This script returns the download destination as the last line upon a successful download with exit code 0. In case of error or checksums not matching, a non-zero exit code is returned. This script leverages the MD5 sum, SHA1 sum, SHA256 sum from Coreutils.

E.g.

  • Download to a temporary file:

    safe-download https://picsum.photos/200
  • Download to /tmp/aoeu, prompt overwriting:

    safe-download --destination /tmp/aoeu https://picsum.photos/200
  • If the current MD5 sum of /tmp/aoeu doesn’t match ABC, download and overwrite it, then compute and the check the MD5 sum for the downloaded file; otherwise, skip the download:

    safe-download --destination /tmp/aoeu --md5sum ABC https://picsum.photos/200
  • Same as the previous example except that the SHA1 sum is taken into account along with the MD5 sum:

    safe-download --destination /tmp/aoeu --md5sum ABC --sha1sum DEF https://picsum.photos/200
file::safe-download
#!/usr/bin/env tclsh

package require Tcl 8.2
package require fileutil

try {
	array set args [::cmdline::getoptions ::argv {
		{destination.arg "" "(optional) The download destination"}
		{md5sum.arg      "" "(optional) The MD5 checksum"}
		{sha1sum.arg     "" "(optional) The SHA1 checksum"}
		{sha256sum.arg   "" "(optional) The SHA256 checksum"}
		{sha512sum.arg   "" "(optional) The SHA512 checksum"}
	} {[options] <uri>

Download a file, compare checksum if necessary, and return the path to the downloaded file.  If the download destination is not specified, a temporary path is returned.
}]
} trap {CMDLINE USAGE} {msg _o} {
	puts $msg
	exit 0
}

if {[catch {exec report-missing-executables aria2c Aria2c >@ stdout 2>@ stderr}]} {
	exit 1
}

proc getDestination {dest} {
	if {$dest eq ""} {
		set path [::fileutil::tempfile]
		file delete -- $path
		return $path
	} else {
		return $dest
	}
}

proc getURI {uri} {
	if {$uri eq ""} {
		puts stderr "ERROR: Missing URI"
		exit 1
	} else {
		return $uri
	}
}

proc checksumsMatched {path checksumDict} {
	proc checksumMatched {path type checksum} {
		set checksum [string toupper $checksum]
		if {$checksum eq ""} {
			return 1
		} else {
			switch $type {
				md5 {
					return [expr {$checksum eq [string toupper [exec md5sum $path | cut -f1 -d " "]]}]
				}
				sha1 {
					return [expr {$checksum eq [string toupper [exec sha1sum $path | cut -f1 -d " "]]}]
				}
				sha256 {
					return [expr {$checksum eq [string toupper [exec sha256sum $path | cut -f1 -d " "]]}]
				}
				sha512 {
					return [expr {$checksum eq [string toupper [exec sha512sum $path | cut -f1 -d " "]]}]
				}
				default {
					return 1
				}
			}
		}
	}
	return [expr {[checksumMatched $path md5 [dict get $checksumDict md5sum]]
				  && [checksumMatched $path sha1 [dict get $checksumDict sha1sum]]
				  && [checksumMatched $path sha256 [dict get $checksumDict sha256sum]]
				  && [checksumMatched $path sha512 [dict get $checksumDict sha512sum]]}]
}

proc downloadFile {destDir destFile uri} {
	return [exec aria2c --dir $destDir --out $destFile $uri >@ stdout 2>@ stderr]
}

set dest [getDestination $args(destination)]
set destDir [file dirname $dest]
set destFile [file tail $dest]
set uri [getURI [lindex $::argv 0]]
set checksumDict [dict create md5sum $args(md5sum) sha1sum $args(sha1sum) sha256sum $args(sha256sum) sha512sum $args(sha512sum)]

if {[lindex [array get ::env VERBOSE] 1] == 1} {
	# Be verbose
	parray args
	puts "Destination: $dest"
	puts "URI: $uri"
}

if {[file exists $dest]} {
	if {[dict values $checksumDict] == {{} {} {}}} {
		try {
			exec prompt-y-n "$dest exists, would you like continue and overwrite it?" y <@ stdin >@ stdout 2>@ stderr
			file delete -- $dest
			downloadFile $destDir $destFile $uri
		} trap CHILDSTATUS {_msg _options} {}
	} elseif {![checksumsMatched $dest $checksumDict]} {
		file delete -- $dest
		downloadFile $destDir $destFile $uri
	}
} else {
	downloadFile $destDir $destFile $uri
}

if {![checksumsMatched $dest $checksumDict]} {
	exit 1
} else {
	puts $dest
}

# Local Variables:
# indent-tabs-mode: t
# End:

4.117. gen-string - generates a random alphanumeric string

file::gen-string
#!/usr/bin/env bash

length_=${1:-32}

cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w ${length_} | head -n 1

4.118. gen-names - generates random English names

TODO: docstring

file::gen-names
#!/usr/bin/env bash

set -o nounset

DICT_PATH=${DICT_PATH:-/usr/share/dict/words}

counter_=${1:-1}

rig -c $(( (${counter_} + 1) / 2 )) | awk 'NR == 1 || NR % 5 == 1' | tr ' ' '\n' | head -${counter_} | paste --serial --delimiters=' ' -

4.119. gen-filename - generates a random filename

TODO: docstring

file::gen-filename
#!/usr/bin/env sh

counter_="${1:-4}"
separator_="${2:--}"

exec normalize-filename $(gen-names ${counter_}) | downcase | tr ' ' "${separator_}"

4.120. daemontools-compatible utils

file::sv-create-user-supervisor
#!/usr/bin/env tclsh

package require Tclx

# TODO: matching -h/--help
# TODO: help text
# TODO: command line arguments

## include::tcl-helpers

##############################################################################
# Helpers
##############################################################################

proc execCmd {args} {
     return [exec {*}$args <@ stdin >@ stdout 2>@ stderr]
}

proc getRunsvdirLogContent {servicePath} {
    return [format {#!/usr/bin/env sh

mkdir -p %s/log/main
exec svlogd -ttt %s/log/main
} $servicePath $servicePath]
}

proc getRunsvdirContent {user userDaemonEnableDir} {
    return [format {#!/usr/bin/env sh

exec 2>&1
exec sudo -H -E -u %s runsvdir -P %s
} $user $userDaemonEnableDir]
}

proc createExecutableAsRoot {path content} {
    if {[file exists $path]} {
        puts -nonewline "$path exists, remove it? \[y/N\] "
        flush stdout
        set answer [string tolower [string trim [gets stdin]]]
        if {$answer eq "y"} {
            execCmd with-sudo rm -rvI $path
        } else {
            exit 0
        }
    }

    file tempfile tempFile /tmp/tempfile

    set f [open $tempFile w]
    puts $f $content
    close $f

    execCmd with-sudo mv $tempFile $path
    execCmd with-sudo chmod +x $path
}

##############################################################################
# Main
##############################################################################

if {$argc == 0} {
    set user $::env(USER)
} else {
    set user [lindex $argv 0]
}

if {[info exists ::env(MY_DAEMON_DIR)]} {
    set userDaemonDir $::env(MY_DAEMON_DIR)
} else {
    set userDaemonDir /home/$user/daemon
}

set userDaemonEnableDir $userDaemonDir/enabled
set userDaemonAvailableDir $userDaemonDir/available
set serviceName runsvdir-$user
set servicePath /etc/sv/$serviceName

puts "User: $user"
puts "Supervisor service path: $servicePath"
puts "User service path (available): $userDaemonAvailableDir"
puts "User service path (enabled): $userDaemonEnableDir"
puts ""

execCmd with-sudo mkdir -p $servicePath/log/main
file mkdir $userDaemonAvailableDir
file mkdir $userDaemonEnableDir

createExecutableAsRoot $servicePath/run     [getRunsvdirContent $user $userDaemonEnableDir]
createExecutableAsRoot $servicePath/log/run [getRunsvdirLogContent $servicePath]

exec with-sudo symlink $servicePath /etc/service/$serviceName

puts "Done, please restart the Runit service"

4.120.1. sv-enable - enables a daemon

file::sv-enable
#!/usr/bin/env tclsh

# TODO: matching -h/--help
# TODO: help text

set serviceName [lindex $argv 0]
if {[info exists ::env(MY_DAEMON_DIR)]} {
    set userDaemonPath $::env(MY_DAEMON_DIR)
} else {
    set userDaemonPath [file normalize ~/daemon]
}

set servicePath [glob -nocomplain $userDaemonPath/available/$serviceName]
set enablePath $userDaemonPath/enabled/$serviceName

if {$servicePath eq ""} {
    puts stderr "ERROR: $servicePath doesn't exist"
    exit 1
}

exec symlink $servicePath $enablePath <@ stdin >@ stdout 2>@ stderr

4.120.2. sv-disable - disables a daemon

file::sv-disable
#!/usr/bin/env tclsh

# TODO: matching -h/--help
# TODO: help text

set serviceName [lindex $argv 0]
if {[info exists ::env(MY_DAEMON_DIR)]} {
    set userDaemonPath $::env(MY_DAEMON_DIR)
} else {
    set userDaemonPath [file normalize ~/daemon]
}

set enablePath [glob -nocomplain $userDaemonPath/enabled/$serviceName]

if {$enablePath ne ""} {
    file delete $enablePath
}

4.120.3. sv-list - lists current daemons

file::sv-list
#!/usr/bin/env tclsh

# TODO: matching -h/--help
# TODO: help text
# TODO: command line arguments

if {$::env(USER) eq "root"} {
    set supervisedPath /etc/service
} else {
    if {[info exists ::env(MY_DAEMON_DIR)]} {
        set supervisedPath $::env(MY_DAEMON_DIR)/enabled
    } else {
        set supervisedPath [file normalize ~/daemon/enabled]
    }
}

foreach dir [glob -nocomplain $supervisedPath/*] {
    puts [file tail $dir]
}

4.120.4. sv-tail-log - tails a log file for a daemon

file::sv-tail-log
#!/usr/bin/env tclsh

package require Tclx

# TODO: matching -h/--help
# TODO: help text
# TODO: command line arguments

set serviceName [lindex $::argv 0]
set logFile [lindex $::argv 1]

if {$::env(USER) eq "root"} {
    set supervisedPath /etc/service
} else {
    if {[info exists ::env(MY_DAEMON_DIR)]} {
        set supervisedPath $::env(MY_DAEMON_DIR)/enabled
    } else {
        set supervisedPath [file normalize ~/daemon/enabled]
    }
}

execl tail [list "-f" $supervisedPath/$serviceName/log/main/$logFile]

5. High-level "fingertip" executables

5.1. get-exec - gets the full path of an executable from PATH

TODO: Help text

file::get-exec
#!/usr/bin/env tclsh

set execFile [string trim $::argv]
set paths [split $::env(PATH) ":"]

foreach path $paths {
    set possibleExec "$path/$execFile"
    if {[file exists $possibleExec] && [file isfile $possibleExec] && [file executable $possibleExec]} {
        puts $possibleExec
        exit 0
    }
}
exit 2

5.2. get-from-paths - gets the full path of a file/directory from PATH

TODO: Help text

file::get-from-paths
#!/usr/bin/env tclsh

set execFile [string trim $::argv]
set paths [split $::env(PATH) ":"]

foreach path $paths {
    set possibleExec "$path/$execFile"
    if {[file exists $possibleExec]} {
        puts $possibleExec
        exit 0
    }
}
exit 2

5.3. deep-exec - execs a command from PATH, the command might be a path instead of just a file name

TODO: Help text

file::deep-exec
#!/usr/bin/env sh

exec_path_=$(get-exec "$1")
shift 1

if [ "${exec_path_}" = "" ]; then
    echo "Error: $1 not found" >&2
    exit 1
fi

exec "${exec_path_}" "$@"

5.4. exec-stdin - execs from stdin

TODO: Help text

file::exec-stdin
#!/usr/bin/env tclsh

package require Tcl 8

set input [read stdin]
set fd [file tempfile tempPath]

try {
    puts -nonewline $fd $input
    close $fd

    file attribute $tempPath -permissions u+x
    puts [exec $tempPath]
} finally {
    file delete $tempPath
}

5.5. exec-and-echo-stdin - execs and echoes stdin

TODO: Help text

file::exec-and-echo-stdin
#!/usr/bin/env expect

log_user 0

spawn {*}$::argv
interact

5.6. enrich-path - enriches PATH, then executes a command

TODO: Help text

TODO: Note on the rationale of this executable - To modularize scripting/shelling

TODO: Thinking wm wm/herbstluft wm/herbstluft/menus vs. wm herbstluft menus

file::enrich-path
#!/usr/bin/env tclsh

package require Tclx

# TODO: Check argv length

# TODO: Help text

# TODO: Order of args - which overrides which

# TODO: The newly construct paths are prepended

##############################################################################
# Helper
##############################################################################

proc addSuffixToPaths {paths suffix} {
    if {[string match "/*" $suffix]} {
        return [list $suffix]
    }

    set res {}
    foreach e $paths {
        set newPath "$e/$suffix"
        if {[file exists $newPath]} {
            lappend res $newPath
        }
    }
    return $res
}

##############################################################################
# Main
##############################################################################

# Mark the end of the list of envs if needed
if {[lsearch $::argv "-"] == -1} {
    set addedPaths [lrange $::argv 0 0]
    set args [lrange $::argv 1 end]
} else {
    set index [lsearch $::argv "-"]
    set addedPaths [lrange $::argv 0 [expr {$index - 1}]]
    set args [lrange $::argv [expr {$index + 1}] end]
}

set paths [split [string trim [exec echo ".:$::env(PATH)" | awk -v RS=: {!($0 in a) {a[$0]; print}}]] "\n"]
foreach suffix $addedPaths {
    set pathsWithSuffix [addSuffixToPaths $paths $suffix]
    set paths [list {*}$pathsWithSuffix {*}$paths]
}
set ::env(PATH) [join $paths ":"]

if {[lindex $args 0] ne ""} {
    execl [lindex $args 0] [lrange $args 1 end]
}

5.7. fastls - a bit faster ls

Traditionally, ls stat`s corresponding path(s). This doesn’t work so well with remote mount with high latency. `fastls attempts to mitigate this issue.

file::fastls
#!/usr/bin/env sh

report-missing-executables find Find || exit 1

exec find "$@" -maxdepth 1

6. Legacy/Unused executables

6.1. wenv - shorthand to execute a command in an environment or in a subdirectory in PATH

file::wenv
#!/usr/bin/env tclsh

package require Tclx

# TODO: Check argv length

# TODO: Help text

set envName [lindex $::argv 0]
set args [lrange $::argv 1 end]

proc addSuffixToPaths {paths suffix} {
    set res {}
    foreach e $paths {
        lappend res "$e$suffix"
    }
    return [list {*}$res {*}$paths]
}

switch $envName {
    pure {
        execl with-env-pure $args
    }
    user -
    u {
        execl with-env-user $args
    }
    python -
    conda -
    py {
        execl with-conda $args
    }
    default {
        set paths [split $::env(PATH) ":"]
        set newPaths [addSuffixToPaths $paths "/$envName"]
        set ::env(PATH) [join $newPaths ":"]
        execl [lindex $args 0] [lrange $args 1 end]
    }
}

6.2. i3-switch-window - window switcher for i3

Requirement: dmenu.

#!/usr/bin/env python3

#
# Copyright (C) 2015-2016  Ha-Duong Nguyen <cmpitg@gmail.com>
#
# i3-switch-window is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# i3-switch-window is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# i3-switch-window.  If not, see <http://www.gnu.org/licenses/>.
#

#
# Requirements:
#   Python 3
#   dmenu with Xft patch
#

import json
import subprocess
import sys


# dmenu_options      = '-b -i -l 40 -fn "Noto Sans-10" -nf "#ffa077" -nb "#202020"'
dmenu_options      = '-p Window -i -l 40 -fn "Noto Sans-10" -nf "#ffa077" -nb "#202020"'
title_format       = "{} — {}"
cmd_get_tree       = "i3-msg -t get_tree"
cmd_switch_window  = "i3-msg '[con_id={}] focus'"


def main():
	fail_if_dmenu_not_found()

	global dmenu_options
	global cmd_get_tree
	global cmd_switch_window

	tree = json.loads(subprocess.check_output(
		cmd_get_tree,
		stderr=subprocess.STDOUT,
		shell=True
	).decode('utf-8'))

	windows       = get_all_windows(tree)
	lookup_table  = build_lookup_table(windows)
	chosen        = dmenu(itemize(windows), dmenu_options)

	switch_to_window(
		chosen=chosen,
		table=lookup_table,
		cmd=cmd_switch_window
	)


def fail_if_dmenu_not_found():
	"""Check if dmenu exists and exit if it doesn't."""
	if subprocess.call("which dmenu", shell=True) != 0:
		sys.stdout.write("dmenu not found\n")
		sys.stdout.write("Make sure you have dmenu installed\n")
		sys.exit(1)


def switch_to_window(chosen, table, cmd):
	"""Switch to the chosen window."""
	window_id = table.get(chosen, -1)
	if window_id != -1:
		subprocess.check_call(cmd.format(window_id), shell=True)


def window_as_string(with_id=False):
	global title_format

	def helper(window):
		title = title_format.format(window['class'], window['title'])
		if with_id:
			return title, window['id']
		else:
			return title

	return helper


def build_lookup_table(windows):
	stringifized = map(window_as_string(with_id=True), windows)
	return dict(stringifized)


def itemize(windows):
	"""Itemize windows list for dmenu."""
	return "\n".join(map(window_as_string(with_id=False), windows))


def get_all_windows(tree):
	"""Extracts all windows from i3 tree."""
	# Add current window
	if is_window(tree):
		result = [standardize_window(tree)]
	else:
		result = []

	# Add child windows
	children = []
	for window in tree['nodes']:
		children += get_all_windows(window)

	return result + children


def is_window(tree):
	"""Determines if a tree is a window."""
	return tree['window'] \
		and tree['window_properties']['class'].lower().find('panel') == -1


def standardize_window(window):
	"""Extracts necessary information for a window."""
	return {
		'id':       window['id'],
		'title':    window['window_properties']['title'],
		'class':    window['window_properties']['class'],
		'instance': window['window_properties']['instance']
	}


def dmenu(items, dmenu_options):
	"""Calls dmenu to display and menu for window switching."""
	cmd = subprocess.Popen(
		"dmenu {}".format(dmenu_options),
		shell=True,
		stdin=subprocess.PIPE,
		stdout=subprocess.PIPE,
		stderr=subprocess.PIPE
	)
	stdout, _ = cmd.communicate(items.encode('utf-8'))
	return stdout.decode('utf-8').strip('\n')


if __name__ == '__main__':
	main()

7. License

This project is released under the terms of the FreeBSD license. See LICENSE for further information.

About

Collection of composable, simple, and hopefully useful scripts.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published