nix: Make nix the root

Which means now I'll have to think about real prefixes in commit names.
This commit is contained in:
Geoffrey Frogeye 2023-11-26 23:58:22 +01:00
parent 550eed06e0
commit ee178b7d57
Signed by: geoffrey
GPG key ID: C72403E7F82E6AD8
190 changed files with 5 additions and 6 deletions

View file

@ -0,0 +1,6 @@
#!/usr/bin/env bash
# Make sur eto install zlib-flate from the qpdf package
# Works on unencrypted only
tail -c +25 "$1" | zlib-flate -uncompress | tar xvf -

View file

@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euxo pipefail
# Set variables
name="${1:-linux}"
kernel="/boot/vmlinuz-$name"
initrd="/boot/initramfs-$name.img"
# Test if required files are present
[ -f $kernel ]
[ -f $initrd ]
command -v kexec &> /dev/null
# Configure the next kernel to load
sudo kexec -l $kernel --initrd=$initrd --reuse-cmdline
# Gracefully restart on the next kernel
sudo systemctl kexec

View file

@ -0,0 +1,12 @@
#!/usr/bin/env bash
if [ -z $DISPLAY ]
then
sudo tee /proc/acpi/bbswitch <<< ON
"$@"
rmmod nvidia_uvm
rmmod nvidia
sudo tee /proc/acpi/bbswitch <<< OFF
else
PATH="/opt/cuda/bin:$PATH" LD_LIBRARY_PATH="/opt/cuda/lib64:$LD_LIBRARY_PATH" VBLANK=0 VGL_READBACK=pbo optirun -c yuv "$@"
fi

View file

@ -0,0 +1,8 @@
#!/usr/bin/env bash
# Downloads a font from dafont.com and
# extracts it in the user's font dir
wget "http://dl.dafont.com/dl/?f=$1" -O /tmp/dafont.zip
unzip /tmp/dafont.zip -d ~/.local/share/fonts -x *.txt
rm -rf /tmp/dafont.zip

455
unprocessed/config/scripts/debloc Executable file
View file

@ -0,0 +1,455 @@
#!/usr/bin/env bash
# Installs Debian packages on a Debian system
# with no root access, in the user home
# CONFIGURATION
# Verifications
if [[ -z $DEBIAN_MIRROR && ! -f /etc/apt/sources.list ]]; then
echo "Unable to find a mirror. Try setting DEBIAN_MIRROR (see help)."
exit 1
fi
if [[ -z $DEBIAN_DB && ! $(which apt &> /dev/null) ]]; then
echo "Unable to find a database for packages to install. Try setting DEBIAN_DB (see help)."
exit 1
fi
# Overrides
[ -z $DEBLOC_PREFIX ] && DEBLOC_PREFIX=$(dpkg --print-architecture)
[ -z $DEBLOC_DB ] && DEBLOC_DB=${XDG_CONFIG_HOME:-$HOME/.config}/debloc/$DEBLOC_PREFIX
[ -z $DEBLOC_ROOT ] && DEBLOC_ROOT=$HOME/.debloc/$DEBLOC_PREFIX
DEBLOC_LD=$DEBLOC_ROOT/ld
if [ -z "$DEBIAN_MIRROR" ]; then
DEBIAN_MIRROR="$(cat /etc/apt/sources.list | grep '^deb ' | grep main | grep -v backports)"
DEBIAN_MIRROR="$(echo -e "$DEBIAN_MIRROR" | cut -d ' ' -f 2 | sed 's/\/$//' | sort | uniq)"
fi
# Preparation
mkdir -p $DEBLOC_DB &> /dev/null
mkdir -p $DEBLOC_ROOT &> /dev/null
# PRIVATE FUNCTIONS
# Tell if a package exists
function _debloc-exists { # package
if [[ -n $DEBIAN_DB && -f $DEBIAN_DB ]]; then
grep "^Package: $1\$" $DEBIAN_DB --quiet
else
LANG=C apt-cache show $1 &> /dev/null
fi
if [ $? == 0 ]; then
return 1
else
return 0
fi
}
# Return the real package associated with a virtual package
# If not a virtual package, return the input
function _debloc-filterVirtual { # package
pkg=$1
if [[ -n $DEBIAN_DB && -f $DEBIAN_DB ]]; then
echo $pkg
else
LANG=C apt-cache policy $1 | grep "Candidate" | grep "(none)" > /dev/null
if [ $? == 0 ]; then
# TODO This is not really accurate
LANG=C apt-cache showpkg $pkg | tail -1 | cut -d ' ' -f 1
else
echo $pkg
fi
fi
return 0
}
# Tell if a package is installed via debloc
function _debloc-locallyInstalled { # package
if [ -f $DEBLOC_DB/$1 ]; then
return 1
else
return 0
fi
}
# Tell if a package is installed system-wide
function _debloc-globallyInstalled { # package
STATUS=$(mktemp)
LANG=C dpkg --list $1 &> $STATUS
if [ $? != 0 ]; then
rm -f $STATUS > /dev/null
return 0
fi
cat $STATUS | grep '^Status:' | grep ' installed' --quiet
if [ $? != 0 ]; then
rm -f $STATUS > /dev/null
return 0
else
rm -f $STATUS > /dev/null
return 1
fi
}
# Get informations about a package
function _debloc-packageShow { # package
pkg=$1
if [[ -n $DEBIAN_DB && -f $DEBIAN_DB ]]; then
startline=$(grep "^Package: ${pkg}\$" $DEBIAN_DB --line-number | tail -1 | cut -d ':' -f 1)
if [ -z "$startline" ]; then
return 0
fi
sed -n "$startline,$(expr $startline + 100)p" $DEBIAN_DB | while read line; do
if [ -z "$line" ]; then
return 0
fi
echo $line
done
return 1
else
LANG=C apt-cache show $pkg | while read line; do
if [ -z "$line" ]; then
return 0
fi
echo "$line"
done
return 0
fi
}
# Get the path of a package
function _debloc-packagePath { # package
_debloc-packageShow $1 | grep "^Filename:" | head -1 | cut -d ':' -f 2 | sed -e 's/^[[:space:]]*//'
return 0
}
# Get the md5sum of a package
function _debloc-packageMd5sum { # package
_debloc-packageShow $1 | grep "^MD5sum:" | cut -d ':' -f 2 | sed -e 's/^[[:space:]]*//'
return 0
}
# Update symbolics links in $DEBLOC_ROOT/lib
function _debloc-ldconfig {
mkdir -p $DEBLOC_LD &> /dev/null
rm -f $DEBLOC_LD &> /dev/null
find $DEBLOC_ROOT{/usr,}/lib -type f -name "*.so*" | while read lib; do
ln --symbolic --force "$lib" "$DEBLOC_LD/$(basename $lib)"
done &> /dev/null
find $DEBLOC_ROOT{/usr,}/lib -type l -name "*.so*" | while read link; do
yes | cp --force --no-dereference --preserve=links "$link" "$DEBLOC_LD" &> /dev/null
done &> /dev/null
}
# Fix absolute symbolic links
function _debloc-fixRootSymlinks {
find $DEBLOC_ROOT -type l | while read src
do
dst="$(readlink "$src")"
if echo "$dst" | grep '^/' | grep -q -v "^$DEBLOC_ROOT"
then
newDst="$DEBLOC_ROOT$dst"
if [ -f "$newDst" ]
then
echo "$src → $newDst"
rm "$src"
ln -s "$newDst" "$src"
else
echo "Ignoring $src pointing to $dst"
fi
fi
done
}
function _debloc-fixPkgconfPrefix {
sed "s|^prefix=/usr$|prefix=$DEBLOC_ROOT/usr|" $(find $DEBLOC_ROOT -type f -name "*.pc") -i
}
function debloc_fix {
echo "Fixing absolute symbolic links..."
_debloc-fixRootSymlinks
echo "Linking libraries in /ld"
_debloc-ldconfig
echo "Fixing prefix in pkg-config files"
_debloc-fixPkgconfPrefix
}
# Install debian archive
function _debloc-installDeb { # path
TMP_DIR=$(mktemp -d) &> /dev/null
$(cd $TMP_DIR; ar x "$1")
TAR_FILE=$(find $TMP_DIR -type f -name "data.tar.*" | head -1)
if [ -e "$TAR_FILE" ]; then
# Output for DB saving
tar tf $TAR_FILE
tar xf $TAR_FILE -C $DEBLOC_ROOT
# _debloc-ldconfig
mkdir -p $DEBLOC_LD &> /dev/null
tar tf $TAR_FILE | grep '^.\(/usr\)\?/lib/' | grep '\.so' | while read file; do
lib=$(readlink -f $DEBLOC_ROOT/$file)
if [ -f $lib ]; then
ln --symbolic --force "$lib" "$DEBLOC_LD/$(basename $file)"
fi
if [ -h $lib ]; then
yes | cp --force --no-dereference --preserve=links "$(basename $link)" "$DEBLOC_LD/" &> /dev/null
fi
done
fi
rm -rf $TMP_DIR &> /dev/null
return 0
}
# Install package
function _debloc-install { # package
pkg=$1
DEB_FILE=$(mktemp) &> /dev/null
path=$(_debloc-packagePath $pkg)
echo -e "${DEBIAN_MIRROR}" | while read mirror; do
if [ -z "$mirror" ]; then
continue
fi
url=${mirror}/${path}
echo "→ Downloading $url"
wget "$url" --quiet -O $DEB_FILE
if [ $? == 0 ]; then
break
fi
done
if [ ! -s $DEB_FILE ]; then
echo "→ Failed (no deb file)!"
rm $DEBLOC_DB/$pkg &> /dev/null
return 4
fi
echo "→ Verifying sums"
theo=$(_debloc-packageMd5sum $pkg)
real=$(md5sum $DEB_FILE | cut -d ' ' -f 1)
if [ "$theo" != "$real" ]; then
rm -f $DEB_FILE &> /dev/null
echo "→ Failed (sum doesn't match)!"
rm $DEBLOC_DB/$pkg &> /dev/null
return 5
fi
echo "→ Installing"
_debloc-installDeb $DEB_FILE > $DEBLOC_DB/$pkg
echo "→ Done!"
rm -f $DEB_FILE &> /dev/null
return 0
}
# Get the dependencies of a package
function _debloc-packageDeps { # package
_debloc-packageShow $1 | grep '^Depends:' | sed 's/Depends: //' | sed 's/, /\n/g' | cut -d ' ' -f 1
return 0
}
# Install package with dependencies
function _debloc-installDeps { # package
pkg=$1
echo "Installing $pkg"
touch $DEBLOC_DB/$pkg # To prevent cyclic deps
_debloc-packageDeps $pkg | while read dep; do
dep=$(_debloc-filterVirtual $dep)
_debloc-locallyInstalled $dep
if [ $? == 1 ]; then
echo "- Dependency $dep is already installed with Debloc"
continue
fi
_debloc-globallyInstalled $dep
if [ $? == 1 ]; then
echo "- Dependency $dep is already installed on the system"
continue
fi
_debloc-installDeps $dep | while read line; do echo "- $line"; done
done
_debloc-install $pkg
return 0
}
# PUBLIC FUNCTIONS
function proxy_set_help {
echo "Usage: $0 env"
echo
echo "Examples:"
echo ' eval "$(debloc env)"'
return 0
}
function debloc_env {
echo "export PATH=\"$DEBLOC_ROOT/usr/bin:$DEBLOC_ROOT/usr/games/:$DEBLOC_ROOT/usr/lib/git-core:\$PATH\""
echo "export LIBRARY_PATH=\"$DEBLOC_LD:\$LIBRARY_PATH\""
echo "export C_INCLUDE_PATH=\"$DEBLOC_ROOT/usr/include:\$C_INCLUDE_PATH\""
echo "export CPLUS_INCLUDE_PATH=\"$DEBLOC_ROOT/usr/include:$DEBLOC_ROOT/usr/include/python2.7/:$DEBLOC_ROOT/usr/include/x86_64-linux-gnu/python2.7/:\$CPLUS_INCLUDE_PATH\""
echo "export LD_LIBRARY_PATH=\"$DEBLOC_LD:\$LD_LIBRARY_PATH\""
echo "export PYTHONPATH=\"$DEBLOC_ROOT/usr/lib/python2/dist-packages:$DEBLOC_ROOT/usr/lib/python3/dist-packages:$DEBLOC_ROOT/usr/lib/python2.7/dist-packages:$DEBLOC_ROOT/usr/lib/python3.5/dist-packages:\$PYTHONPATH\""
echo "export QT_QPA_PLATFORM_PLUGIN_PATH=\"$DEBLOC_ROOT/usr/lib/x86_64-linux-gnu/qt5/plugins/platforms\""
echo "export PKG_CONFIG_PATH=\"$DEBLOC_ROOT/usr/share/pkgconfig/:$DEBLOC_ROOT/usr/lib/x86_64-linux-gnu/pkgconfig/:$DEBLOC_ROOT/usr/lib/pkgconfig/:\$PKG_CONFIG_PATH\""
}
function debloc_info {
echo "DEBLOC_PREFIX=$DEBLOC_PREFIX"
echo "DEBLOC_ROOT=$DEBLOC_ROOT"
echo "DEBLOC_DB=$DEBLOC_DB"
echo "DEBLOC_LD=$DEBLOC_LD"
echo "DEBIAN_MIRROR='$DEBIAN_MIRROR'"
echo "DEBIAN_DB=$DEBIAN_DB"
}
function debloc_install_help {
echo "Usage: $0 install PACKAGE"
echo
echo "Arguments:"
echo " PACKAGE Package name"
return 0
}
function debloc_install { # package
if [ -z $1 ]; then
debloc_deb_help
fi
for pkg in $*
do
if [ $pkg == '--force' ] || [ $pkg == '-f' ]; then
force=0
fi
done
for pkg in $*; do
if [ $pkg == '--force' ] || [ $pkg == '-f' ]; then
continue
fi
pkg=$(_debloc-filterVirtual $pkg)
_debloc-exists $pkg
if [ $? == 0 ]; then
echo "Unknown package $pkg"
continue
fi
if [ ! -v force ]; then
_debloc-locallyInstalled $pkg
if [ $? == 1 ]; then
echo "Package $pkg is already installed with Debloc"
continue
fi
_debloc-globallyInstalled $pkg
if [ $? == 1 ]; then
echo "Package $pkg is already installed on the system"
continue
fi
fi
_debloc-installDeps $pkg
done
return 0
}
function debloc_deb_help {
echo "Usage: $0 deb PATH"
echo
echo "Arguments:"
echo " PATH Path to the .deb file"
return 0
}
function debloc_deb { # path
if [ -z $1 ]; then
debloc_deb_help
fi
for path in $*; do
if [ ! -f "$path" ]; then
echo "$path is not a file"
return 6
fi
echo "Installing $(basename $path)"
_debloc-installDeb "$(readlink -f $path)" > $DEBLOC_DB/$(basename $path)
done
return 0
}
function debloc_altern_help {
echo "Usage: $0 altern PROGRAM ALTERNATIVE"
echo
echo "Arguments:"
echo " PROGRAM Program to set the alternative for"
echo " ALTERNATIVE Alternative to set"
echo
echo "Examples:"
echo " $0 altern vim nox"
echo " $0 altern dmenu xft"
return 0
}
function debloc_altern { # program alternative
if [[ -z $1 || -z $2 ]]; then
debloc_altern_help
exit 1
fi
if [ -f "$DEBLOC_ROOT/usr/bin/$1.$2" ]; then
dest="$DEBLOC_ROOT/usr/bin/$1"
alte="$DEBLOC_ROOT/usr/bin/$1.$2"
elif [ -f "$DEBLOC_ROOT/bin/$1.$2" ]; then
dest="$DEBLOC_ROOT/bin/$1"
alte="$DEBLOC_ROOT/bin/$1.$2"
else
echo "Unknown alternative for $1 : $2"
exit 1
fi
if [ -e "$dest" ]; then
rm $dest
fi
ln -s "$alte" "$dest"
}
function debloc_flush {
rm -rf $DEBLOC_ROOT/* &> /dev/null
rm -f $DEBLOC_DB/* &> /dev/null
}
# TODO Other word for 'fake filesystem' and/or explain what this is
function debloc_help {
command="$1"
if [ -n "$command" ]; then
if type "debloc_${command}_help" &> /dev/null; then
shift
"debloc_${command}_help" "$@"
return $?
fi
fi
echo "Usage: $0 COMMAND"
echo
echo "Commands:"
echo " env Provides the environment variables required to run applications from the fake filesystem"
echo " info Gives some information about the fake filesystem"
echo " install Install a debian package in the fake filesystem"
echo " deb Install from a .deb file in the fake filesystem"
echo " altern Update alternative"
echo " fix Apply some fixes in the fake filesystem"
echo " flush Remove every package installed from the fake filesystem"
echo " help Get help with commands"
echo
echo "Environment variables:"
echo " DEBLOC_PREFIX Name of the fake filesystem to use (default: uses dpkg architecture)"
echo " DEBLOC_ROOT Path of the fake filesystem (default: ~/.debloc/\$DEBLOC_PREFIX/)"
echo " DEBLOC_DB Database of the fake filesystem (default: \$XDG_CONFIG_HOME/debloc/\$DEBLOC_PREFIX)"
echo " DEBIAN_MIRROR Multiline list of debian mirror (default: uses /etc/apt/sources.list)"
echo " DEBIAN_DB Path to a file with all packages description (default: uses apt-cache showpkg)"
echo " help Get help with commands"
return 0
}
# MAIN
command="$1"
shift
if type "debloc_$command" &> /dev/null; then
"debloc_$command" "$@"
else
debloc_help
fi

View file

@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Don't forget:
# --duration 15m # Specify expected duration of presentation
# --half-screen --geometry 2048x768+0+1 # If presenting with beamer notes on the right, and you have two screens with 1024x768 resolution
/home/geoffrey/Documents/Programmation/Impressive/OutOfTree/impressive.py \
--transition WipeRight \
--bind lmb:=box-zoom --bind lmb=zoom-exit \
--bind rmb:=box-add --bind rmb=box-clear \
--bind ctrl+p:=overview-enter --bind ctrl+a:=overview-confirm \
--bind escape:=time-reset \
--bind e:=goto-last \
--bind b-=fade-to-black \
--bind escape-=quit \
--cursor default \
--fontsize 26 \
--transtime 200 \
--mousedelay 1000 \
--page-progress \
--time-display \
--tracking \
--zoomdarkness 75 \
"$@"

2
unprocessed/config/scripts/hc Executable file
View file

@ -0,0 +1,2 @@
#!/usr/bin/env bash
highlight -O ansi "$@"

2
unprocessed/config/scripts/hl Executable file
View file

@ -0,0 +1,2 @@
#!/usr/bin/env bash
highlight -O ansi "$@" | less -R

View file

@ -0,0 +1,56 @@
#!/usr/bin/env node
// Imports
var fs = require('fs');
var pdf = require('html-pdf');
var yargs = require('yargs');
// Understanding
var argv = yargs
.usage("Usage: $0 -o out.pdf [options]")
.example('$0 -i doc.pdf -o doc.pdf', 'Convert doc.html to PDF using the default values')
.help('h')
.alias('h', 'help')
.describe('i', 'Input file')
.alias('i', 'input')
.default('i', '/dev/stdin')
.describe('o', 'Output file')
.alias('o', 'output')
.describe('t', 'Title of file')
.alias('t', 'title')
.default('t', 'Sans titre')
.describe('b', 'Border')
.alias('b', 'border')
.default('b', '2cm')
.demandOption(['o'])
.argv;
// Settings
options = {
"base": "file://" + process.cwd() + '/',
"format": "A4",
"orientation": "portrait",
"border": argv.border,
"footer": {
"height": "10mm",
"contents": {
default: '<div style="text-align: left; float: left;">' + argv.title + '</div> <div style="text-align:right; float: right;">{{page}}/{{pages}}</div>',
}
},
}
// Reading
htmlString = fs.readFileSync(argv.i, "utf8");
// Conversion
pdf.create(htmlString, options).toFile(argv.o, function(err, res) {
if (err) console.error(err);
});

View file

@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Setups a WSL system the way I like it
# Remember to use the alwsl script on GitHub to install Arch Linux on WSL
# Use github:cbucher/console as a console and github:mintty/wsltty as a shell
# (%LOCALAPPDATA%/wsltty/bin/wsl-bridge) to avoid arrow keys bypassing
(cd /usr/share/i18n/charmaps/; sudo gunzip -k UTF8.gz)
echo "fr_FR.UTF-8 UTF-8" | sudo tee -a /etc/locale.gen
sudo locale-gen

View file

@ -0,0 +1,6 @@
#!/usr/bin/env bash
serv="$1"
shift
ssh "$serv" 'sudo tail --follow=name --retry --quiet $(sudo find $(echo /var/log/$([ -d /var/log/httpd/ ] && echo httpd || echo apache2)) -type f -name *access.log)' | logstalgia --sync "$@"

View file

@ -0,0 +1,527 @@
#!/usr/bin/env bash
# Handles indexing and SSH keys of machines I
# have access on
MACHINES_HOME=$HOME
MACHINES_CONFIG=$HOME/.config/machines
MACHINES_API=https://machines.frogeye.fr
mkdir -p "$MACHINES_HOME" &> /dev/null
mkdir -p "$MACHINES_CONFIG" &> /dev/null
# COMMON
function prompt { # text
while true
do
read -r -p "$1 [yn] " yn
case $yn in
[Yy]* ) return 1;;
[Nn]* ) return 0;;
* ) echo "Please answer y or n.";;
esac
done
}
# From https://gist.github.com/cdown/1163649
urlencode() { # string
old_lc_collate=$LC_COLLATE
LC_COLLATE=C
local length="${#1}"
for (( i = 0; i < length; i++ )); do
local c="${1:i:1}"
case $c in
[a-zA-Z0-9.~_-]) printf "%s" "$c" ;;
*) printf '%%%02X' "'$c" ;;
esac
done
LC_COLLATE=$old_lc_collate
}
urldecode() { # string
local url_encoded="${1//+/ }"
printf '%b' "${url_encoded//%/\\x}"
}
# API ACCESS
function _machines-api {
route=$1
shift
temp=$(mktemp)
wget "$MACHINES_API/$route" --content-on-error --quiet --output-document=$temp "$@"
result=$?
if [ $result != 0 ]; then
echo "[ERROR] wget returned $result for route $route" 1>&2;
cat $temp 1>&2;
rm $temp
exit 2
fi
cat $temp
rm $temp
}
function _machines-apiToken {
read -r -p 'TOTP token: ' token
_machines-api "$@" --header="X-TOTP: $token"
}
function _machines-apiSigned {
_machines-ensureAdmin
_machines-api "$@" --certificate="$MACHINES_CONFIG/machines.crt" --private-key="$MACHINES_CONFIG/machines.key"
}
# APPLICATION KEYS & CERTIFICATE
function _machines-pubFromCrt {
openssl x509 -in "$MACHINES_CONFIG/machines.crt" -pubkey -noout > "$MACHINES_CONFIG/machines.pub"
}
function _machines-verifyCertificate {
return
if openssl verify "$MACHINES_CONFIG/machines.crt" | grep -v 'error 18' | grep 'error' --quiet; then
echo "[ERROR] Invalid certificate" 1>&2;
exit 1
fi
}
function _machines-ensurePub {
if [ ! -f "$MACHINES_CONFIG/machines.crt" ]; then
CERT_FILE=$(mktemp)
echo "[INFO] Downloading certificate..."
_machines-api cert > "$CERT_FILE"
openssl x509 -fingerprint -in "$CERT_FILE" | grep Fingerprint --color=never
prompt "Is this correct ?"
if [ $? == 1 ]; then
mv "$CERT_FILE" "$MACHINES_CONFIG/machines.crt" &> /dev/null
else
echo "[ERROR] Certificate rejected." 1>&2;
exit 1
fi
fi
_machines-verifyCertificate
if [ ! -f "$MACHINES_CONFIG/machines.pub" ]; then
_machines-pubFromCrt
fi
return 0
}
function _machines-ensureAdmin {
if [ ! -f "$MACHINES_CONFIG/machines.key" ]; then
echo "[ERROR] You need have to have the private key to do that" 1>&2;
exit 1
fi
}
# SSH ACCESS KEYS
function _machines-signAkey { # network
KEY_FILE=$(mktemp)
SIGN_FILE=$(mktemp)
_machines-apiSigned "akey/$1?unsigned" > "$KEY_FILE"
openssl dgst -sha256 -sign "$MACHINES_CONFIG/machines.key" -out "$SIGN_FILE" "$KEY_FILE"
_machines-apiSigned "akey/$1" --method=PUT --body-file="$SIGN_FILE"
rm "$KEY_FILE" "$SIGN_FILE" &> /dev/null
}
function _machines-getAkey { # network
_machines-ensurePub
KEY_FILE=$(mktemp)
SIGN_FILE=$(mktemp)
_machines-api "akey/$1" > "$KEY_FILE"
_machines-api "akey/$1?signature" > "$SIGN_FILE"
if openssl dgst -sha256 -verify "$MACHINES_CONFIG/machines.pub" -signature "$SIGN_FILE" "$KEY_FILE" &> /dev/null
then
cat "$KEY_FILE"
\rm "$KEY_FILE" "$SIGN_FILE"
return 0
else
\rm "$KEY_FILE" "$SIGN_FILE"
exit 1
fi
}
function _machines-updateAkey {
MYKEY_FILE=$(mktemp)
network=$(grep '^network=' "$MACHINES_CONFIG/this" | cut -d '=' -f 2)
if _machines-getAkey "$network" > "$MYKEY_FILE"
then
\mv -f "$MYKEY_FILE" "$MACHINES_HOME/.ssh/authorized_keys"
if [ -f "$MACHINES_HOME/.ssh/authorized_keys.tail" ]
then
cat "$MACHINES_HOME/.ssh/authorized_keys.tail" >> "$MACHINES_HOME/.ssh/authorized_keys"
fi
return 0
else
cat "$MYKEY_FILE"
echo "[ERROR] Authorized keys are not properly signed" 1>&2;
\rm "$MYKEY_FILE"
exit 1
fi
}
function _machines-postFile { # filename
cat $1 | while read -r line; do
parameter=$(echo "$line" | cut -d '=' -f 1)
value="$(echo "$line" | sed 's/^[a-zA-Z0-9]\+\(\[\]\)\?=//')"
echo -n "&$parameter=$(urlencode "$value")"
done
}
function _machines-addElement { # element elementType default
FILE=$(mktemp)
echo -e "$3" > "$FILE"
$EDITOR "$FILE"
data=$(_machines-postFile "$FILE")
\rm "$FILE"
_machines-apiSigned "$2" --post-data "name=$1$data"
}
function _machines-viewElement { # element elementType
_machines-apiSigned "$2/$1"
}
function _machines-editElement { # element elementType
FILE=$(mktemp)
_machines-apiSigned "$2/$1" > "$FILE"
$EDITOR "$FILE"
data=$(_machines-postFile "$FILE")
rm "$FILE" &> /dev/null
err=$(_machines-apiSigned "$2/$1" --post-data "$data")
}
function _machines-deleteElement { # element elementType
err=$(_machines-apiSigned "$2/$1" --method=DELETE)
}
# USER ADMIN FUNCTIONS
function machines_history {
if [ -f "$MACHINES_CONFIG/lastVerifiedLog" ]; then
from=$(<"$MACHINES_CONFIG/lastVerifiedLog")
else
from=0
fi
d=$(date +%s)
_machines-apiSigned log?from=$from | less
if prompt "Is this OK?"
then
exit 1
else
echo "$d" > "$MACHINES_CONFIG/lastVerifiedLog"
return 0
fi
}
function machines_sign {
machines_history
echo "Signing default network authorized_keys..."
_machines-signAkey
_machines-apiSigned network | while read -r network; do
echo "Signing network $network authorized_keys..."
_machines-signAkey $network
done
}
function machines_machine_list {
_machines-apiSigned machine
}
function machines_network_list {
_machines-apiSigned network
}
function machines_machine_add_help {
echo "Usage: $0 machine|mac|m add MACHINE"
echo
echo "Arguments:"
echo " MACHINE machine to add"
return 0
}
function machines_machine_add { # machine
if [ -z "$1" ]; then
machines_machine_add_help
exit 1
fi
_machines-addElement "$1" machine "host[]=\nnetwork=\nuserkey=\nhostkey=\nuser="
}
function machines_network_add_help {
echo "Usage: $0 network|net|n add NETWORK"
echo
echo "Arguments:"
echo " NETWORK Network to add"
return 0
}
function machines_network_add { # network
if [ -z "$1" ]; then
machines_network_add_help
exit 1
fi
_machines-addElement "$1" network "allowed[]=\nsecure=false"
}
function machines_machine_view_help {
echo "Usage: $0 machine|mac|m view MACHINE"
echo
echo "Arguments:"
echo " MACHINE machine to view"
return 0
}
function machines_machine_view { # machine
if [ -z "$1" ]; then
machines_machine_view_help
exit 1
fi
_machines-viewElement "$1" machine
}
function machines_network_view_help {
echo "Usage: $0 network|net|n view NETWORK"
echo
echo "Arguments:"
echo " NETWORK Network to view"
return 0
}
function machines_network_view { # network
if [ -z "$1" ]; then
machines_network_view_help
exit 1
fi
_machines-viewElement "$1" network
}
function machines_machine_edit_help {
echo "Usage: $0 machine|mac|m edit MACHINE"
echo
echo "Arguments:"
echo " MACHINE machine to edit"
return 0
}
function machines_machine_edit { # machine
if [ -z "$1" ]; then
machines_machine_edit_help
exit 1
fi
_machines-editElement "$1" machine
}
function machines_network_edit_help {
echo "Usage: $0 network|net|n edit NETWORK"
echo
echo "Arguments:"
echo " NETWORK Network to edit"
return 0
}
function machines_network_edit { # network
if [ -z "$1" ]; then
machines_network_edit_help
exit 1
fi
_machines-editElement "$1" network
}
function machines_machine_delete_help {
echo "Usage: $0 machine|mac|m delete machine"
echo
echo "Arguments:"
echo " MACHINE machine to remove"
return 0
}
function machines_machine_delete { # machine
if [ -z "$1" ]; then
machines_machine_delete_help
exit 1
fi
_machines-deleteElement "$1" machine
}
function machines_network_delete_help {
echo "Usage: $0 network|net|n delete NETWORK"
echo
echo "Arguments:"
echo " NETWORK Network to remove"
return 0
}
function machines_network_delete { # network
if [ -z "$1" ]; then
machines_network_delete_help
exit 1
fi
_machines-deleteElement "$1" network
}
function machines_machine_help {
echo "Usage: $0 machine|mac|m COMMAND"
echo
echo "Commands:"
echo " list List all machines"
echo " add Interactively add a machine"
echo " view Display a machine"
echo " edit Interactively edit a specified machine"
echo " delete Remove a specified machine"
echo " help Get help with commands"
return 0
}
function machines_machine {
command="$1"
shift
if type "machines_machine_$command" &> /dev/null; then
"machines_machine_$command" "$@"
else
machines_machine_help
fi
}
function machines_network_help {
echo "Usage: $0 network|net|n COMMAND"
echo
echo "Commands:"
echo " list List all networks"
echo " add Interactively add a network"
echo " view Display a network"
echo " edit Interactively edit a specified network"
echo " delete Remove a specified network"
echo " help Get help with commands"
return 0
}
function machines_network {
command="$1"
shift
if type "machines_network_$command" &> /dev/null; then
"machines_network_$command" "$@"
else
machines_network_help
fi
}
machines_mac() { machines_machine "$@"; }
machines_m() { machines_machine "$@"; }
machines_net() { machines_network "$@"; }
machines_n() { machines_network "$@"; }
machines_mac_help() { machines_machine_help "$@"; }
machines_m_help() { machines_machine_help "$@"; }
machines_net_help() { machines_network_help "$@"; }
machines_n_help() { machines_network_help "$@"; }
function machines_update-all {
machines_machine_list | while read -r machine; do
echo "Updating $machine..."
if [ $machine = $(cat "$MACHINES_CONFIG/this.name") ]; then
machines_update
continue
fi
ssh -n "$machine" 'cd .dotfiles; git pull; ./config/scripts/machines update'
done
}
function machines_regen-keys {
if [[ -e $MACHINES_CONFIG/machines.key || -e $MACHINES_CONFIG/machines.pub || -e $MACHINES_CONFIG/machines.crt ]]; then
echo "[ERROR] Please delete the pem files manually to prove you know what you're doing." 1>&2;
exit 1
else
openssl genrsa -out "$MACHINES_CONFIG/machines.key" 4096
chmod 600 "$MACHINES_CONFIG/machines.key"
openssl req -key "$MACHINES_CONFIG/machines.key" -new -out "$MACHINES_CONFIG/machines.csr"
openssl x509 -req -days 1826 -in "$MACHINES_CONFIG/machines.csr" -signkey "$MACHINES_CONFIG/machines.key" -out "$MACHINES_CONFIG/machines.crt"
_machines-pubFromCrt
fi
}
# USER FUNCTIONS
function machines_setup {
if [ -e "$MACHINES_CONFIG/this.name" ]; then
echo "[ERROR] This machine is already set up" 1>&2;
exit 1
fi
_machines-ensurePub
# Variables
read -r -p 'Machine name? ' name
read -r -p 'Hosts (separated by spaces)? ' hosts
# User key
mkdir -p "$MACHINES_HOME/.ssh" &> /dev/null
if [[ ! -f $MACHINES_HOME/.ssh/id_rsa || ! -f $MACHINES_HOME/.ssh/id_rsa.pub ]]; then
ssh-keygen -b 4096 -C "$name@machines.frogeye.fr" -f "$MACHINES_HOME/.ssh/id_rsa" -t rsa
fi
userkey=$(<"$MACHINES_HOME/.ssh/id_rsa.pub")
# Host key
for type in ecdsa ed25519 rsa dsa; do
if [ -f "/etc/ssh/ssh_host_${type}_key.pub" ]; then
hostkey=$(<"/etc/ssh/ssh_host_${type}_key.pub")
break
fi
done
# Subscription
data="name=$(urlencode "$name")&userkey=$(urlencode "$userkey")&hostkey=$(urlencode "$hostkey")&user=$(urlencode "$USER")"
for host in $hosts; do
data="$data&host[]=$(urlencode "$host")"
done
_machines-apiToken machine --post-data "$data"
echo "$name" > "$MACHINES_CONFIG/this.name"
machines_update
}
function machines_update {
_machines-api "machine/$(cat "$MACHINES_CONFIG/this.name")" > "$MACHINES_CONFIG/this"
_machines-updateAkey
}
function machines_totp {
url=$(_machines-apiSigned totp)
echo "URL : $url"
echo "$url" | qrencode -o - | feh -
}
function machines_help {
command="$1"
if [ -n "$command" ]; then
if type "machines_${command}_help" &> /dev/null; then
shift
"machines_${command}_help" "$@"
return $?
fi
fi
echo "Usage: $0 COMMAND"
echo
echo "User commands:"
echo " setup Interactive initial setup for new machine"
echo " update Update this machine"
echo " help Get help with commands"
echo
echo "Admin commands:"
echo " machine|mac|m Modify machines"
echo " network|net|n Modify networks"
echo " update-all Update all machines available via SSH"
echo " regen-keys Regenerate system keys"
echo " sign Sign recent transactions for propagation"
echo " totp Get TOTP generating QR code / URL"
return 0
}
# MAIN
command="$1"
shift
if type "machines_$command" &> /dev/null; then
"machines_$command" "$@"
else
machines_help "$@"
fi

View file

@ -0,0 +1,128 @@
#!/usr/bin/env node
// Imports
var fs = require('fs');
var marked = require('marked');
var highlight = require('highlight.js');
var katex = require('katex');
var yargs = require('yargs');
var extend = require('util')._extend;
// Constants
var template = '<!DOCTYPE html> <html lang="fr"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta charset="UTF-8"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.11.0/styles/xcode.min.css" integrity="sha256-OED7Gmqde0cMVVeo1zVd+3fBD4EST32D4h9YT7KY0aY=" crossorigin="anonymous" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.7.1/katex.min.css" integrity="sha384-wITovz90syo1dJWVh32uuETPVEtGigN07tkttEqPv+uR2SE/mbQcG7ATL28aI9H0" crossorigin="anonymous"> <style type="text/css"> image { max-width: 100 % } </style> <title>%TITLE%</title> </head> <body> <main class="page-content" aria-label="Content"> %BODY% </main> </body> </html>'
// Understanding
var argv = yargs
.usage("Usage: $0 [options]")
.example('$0 -i doc.md -o doc.html', 'Convert doc.md to HTML using the default template')
.help('h')
.alias('h', 'help')
.describe('i', 'Input file')
.alias('i', 'input')
.default('i', '/dev/stdin')
.describe('o', 'Output file')
.alias('o', 'output')
.default('o', '/dev/stdout')
.describe('t', 'Template file (%BODY% is replaced by the text)')
.alias('t', 'template')
.argv;
if (argv.t) {
template = fs.readFileSync(argv.t, "utf8");
}
var latex = true;
// TODO Arg
// Settings
var extraLangages = {
avrpseudo: function(hljs) {
lang = extend({}, highlight.getLanguage('avrasm'));
lang.keywords.keyword += ' Si Alors Sinon FinSi TantQue FinTantQue Pour FinPour allant de à ←';
lang.keywords.keyword += ' Lire Sortir sur Appeler Retourner';
lang.keywords.keyword += ' DecalerDroite DecalerGauche';
lang.keywords.keyword += ' Incrementer Decrementer';
lang.keywords.built_in += ' vrai faux';
lang.contains.push({
className: 'meta',
begin: /Configurer.+/,
end: /\n/,
});
return lang;
},
avrasmplus: function(hljs) {
lang = extend({}, highlight.getLanguage('avrasm'));
lang.keywords.keyword += ' si saut alors et ou if then goto && || <-';
lang.contains.push({
className: 'meta',
begin: /@\w+/,
});
return lang;
},
};
for (lang in extraLangages) {
// This must be done before any call to highlight.highlight :/
highlight.registerLanguage(lang, extraLangages[lang]);
}
var renderer = new marked.Renderer();
marked.setOptions({
highlight: function(code, lang) {
if (lang == 'raw') {
return code;
} else if (highlight.getLanguage(lang)) {
return highlight.highlight(lang, code).value;
} else {
// if (extraLangages[lang]) {
// highlight.registerLanguage(lang, extraLangages[lang]);
// return highlight.highlight(lang, code).value;
// } else {
// }
console.warn("Unknown language: " + lang);
return highlight.highlightAuto(code).value;
}
}
});
// Processing
markdownString = fs.readFileSync(argv.i, "utf8");
// TeX
if (latex) {
markdownString = markdownString.replace(/\\\$/g, '&dollar;')
markdownString = markdownString.replace(/\$\$([\s\S]+)\$\$/gm, function(glob, formula) {
return katex.renderToString(formula, {
displayMode: true
});
});
markdownString = markdownString.replace(/\$([^$]+)\$/g, function(glob, formula) {
return katex.renderToString(formula, {
displayMode: false
});
});
}
// Conversion
htmlString = marked(markdownString, {
renderer: renderer,
breaks: false
});
// fullHtmlString = htmlString;
fullHtmlString = template.replace('%BODY%', () => { return htmlString });
// Saving
if (argv.o == '/dev/stdout') {
console.log(fullHtmlString);
} else {
fs.writeFileSync(argv.o, fullHtmlString);
}

961
unprocessed/config/scripts/mel Executable file
View file

@ -0,0 +1,961 @@
#!/usr/bin/env python3
# pylint: disable=E1101
"""
Meh mail client
A dumb Python scripts that leverages notmuch, mbsync, and msmtp
to become a fully-functional extremly-opinonated mail client.
"""
# TODO Features
# TODO Implement initial command set
# TODO Lockfiles for write operations on mail files (mbsync,
# tags→maildir operations)
# TODO OPTI Lockfile per account and process everything in parallel
# (if implemented, this should be optional since while it may speed up
# the mail fetching process, its multi-threading nature would cause a
# lot of cache flushes and be not very efficient on battery)
# TODO Handle true character width
# TODO IMAP IDLE watches?
# TODO GPG
# TODO (only then) Refactor
# TODO Merge file with melConf
# TODO Config system revamp
import argparse
import configparser
import datetime
import email.message
import email.parser
import html
import logging
import mailcap
import os
import pdb
import re
import shutil
import subprocess
import sys
import traceback
import typing
import colorama
import coloredlogs
import notmuch
import progressbar
import xdg.BaseDirectory
MailLocation = typing.NewType("MailLocation", typing.Tuple[str, str, str])
# MessageAction = typing.Callable[[notmuch.Message], None]
class MelEngine:
"""
Class with all the functions for manipulating the database / mails.
"""
def load_config(self, config_path: str) -> configparser.ConfigParser:
"""
Load the configuration file into MelEngine
"""
self.log.info("Loading config file: %s", config_path)
if not os.path.isfile(config_path):
self.log.fatal("Config file not found!")
sys.exit(1)
# TODO Create it, maybe?
config = configparser.ConfigParser()
config.read(config_path)
# NOTE An empty/inexistant file while give an empty config
return config
def generate_aliases(self) -> None:
"""
Populate MelEngine.aliases and MelEngine.accounts
"""
assert self.config
for name in self.config.sections():
if not name.islower():
continue
section = self.config[name]
self.aliases.add(section["from"])
if "alternatives" in section:
for alt in section["alternatives"].split(";"):
self.aliases.add(alt)
self.accounts[name] = section
def __init__(self, config_path: str) -> None:
self.log = logging.getLogger("MelEngine")
self.config = self.load_config(config_path)
self.database = None
# Caches
self.accounts: typing.Dict[str, configparser.SectionProxy] = dict()
# All the emails the user is represented as:
self.aliases: typing.Set[str] = set()
# TODO If the user send emails to himself, maybe that wont cut it.
self.generate_aliases()
def notmuch_new(self) -> None:
"""
Runs `notmuch new`, which basically update the database
to match the mail folder.
"""
assert not self.database
self.log.info("Indexing mails")
notmuch_config_file = os.path.expanduser(
"~/.config/notmuch-config"
) # TODO Better
cmd = ["notmuch", "--config", notmuch_config_file, "new"]
self.log.debug(" ".join(cmd))
subprocess.run(cmd, check=True)
def list_folders(self) -> typing.List[typing.Tuple[str, ...]]:
"""
List all the folders of the mail dir.
"""
assert self.config
storage_path = os.path.realpath(
os.path.expanduser(self.config["GENERAL"]["storage"])
)
folders = list()
for account in self.accounts:
storage_path_account = os.path.join(storage_path, account)
for root, dirs, _ in os.walk(storage_path_account):
if "cur" not in dirs or "new" not in dirs or "tmp" not in dirs:
continue
assert root.startswith(storage_path)
path = root[len(storage_path) :]
path_split = path.split("/")
if path_split[0] == "":
path_split = path_split[1:]
folders.append(tuple(path_split))
return folders
def open_database(self, write: bool = False) -> None:
"""
Open an access notmuch database in read or read+write mode.
Be sure to require only in the mode you want to avoid deadlocks.
"""
assert self.config
mode = (
notmuch.Database.MODE.READ_WRITE
if write
else notmuch.Database.MODE.READ_ONLY
)
if self.database:
# If the requested mode is the one already present,
# or we request read when it's already write, do nothing
if mode in (self.database.mode, notmuch.Database.MODE.READ_ONLY):
return
self.log.info("Current database not in mode %s, closing", mode)
self.close_database()
self.log.info("Opening database in mode %s", mode)
db_path = os.path.realpath(
os.path.expanduser(self.config["GENERAL"]["storage"])
)
self.database = notmuch.Database(mode=mode, path=db_path)
def close_database(self) -> None:
"""
Close the access notmuch database.
"""
if self.database:
self.log.info("Closing database")
self.database.close()
self.database = None
def get_location(self, msg: notmuch.Message) -> MailLocation:
"""
Return the filesystem location (relative to the mail directory)
of the given message.
"""
path = msg.get_filename()
path = os.path.dirname(path)
assert self.database
base = self.database.get_path()
assert path.startswith(base)
path = path[len(base) :]
path_split = path.split("/")
mailbox = path_split[1]
assert mailbox in self.accounts
state = path_split[-1]
folder = tuple(path_split[2:-1])
assert state in {"cur", "tmp", "new"}
return (mailbox, folder, state)
@staticmethod
def is_uid(uid: typing.Any) -> bool:
"""
Tells if the provided string is a valid UID.
"""
return (
isinstance(uid, str)
and len(uid) == 12
and bool(re.match("^[a-zA-Z0-9+/]{12}$", uid))
)
@staticmethod
def extract_email(field: str) -> str:
"""
Extract the email adress from a To: or From: field
(usually the whole field or between < >)
"""
# TODO Can be made better (extract name and email)
# Also what happens with multiple dests?
try:
sta = field.index("<")
sto = field.index(">")
return field[sta + 1 : sto]
except ValueError:
return field
def retag_msg(self, msg: notmuch.Message) -> None:
"""
Update automatic tags for message.
"""
_, folder, _ = self.get_location(msg)
# Search-friendly folder name
slug_folder_list = list()
for fold_index, fold in [
(fold_index, folder[fold_index]) for fold_index in range(len(folder))
]:
if fold_index == 0 and len(folder) > 1 and fold == "INBOX":
continue
slug_folder_list.append(fold.upper())
slug_folder = tuple(slug_folder_list)
tags = set(msg.get_tags())
def tag_if(tag: str, condition: bool) -> None:
"""
Ensure the presence/absence of tag depending on the condition.
"""
nonlocal msg
if condition and tag not in tags:
msg.add_tag(tag)
elif not condition and tag in tags:
msg.remove_tag(tag)
expeditor = MelEngine.extract_email(msg.get_header("from"))
tag_if("inbox", slug_folder[0] == "INBOX")
tag_if("spam", slug_folder[0] in ("JUNK", "SPAM"))
tag_if("deleted", slug_folder[0] == "TRASH")
tag_if("draft", slug_folder[0] == "DRAFTS")
tag_if("sent", expeditor in self.aliases)
tag_if("unprocessed", False)
# UID
uid = msg.get_header("X-TUID")
if not MelEngine.is_uid(uid):
# TODO Happens to sent mails but should it?
print(f"{msg.get_filename()} has no UID!")
return
uidtag = "tuid{}".format(uid)
# Remove eventual others UID
for tag in tags:
if tag.startswith("tuid") and tag != uidtag:
msg.remove_tag(tag)
msg.add_tag(uidtag)
def apply_msgs(
self,
query_str: str,
action: typing.Callable,
*args: typing.Any,
show_progress: bool = False,
write: bool = False,
close_db: bool = True,
**kwargs: typing.Any,
) -> int:
"""
Run a function on the messages selected by the given query.
"""
self.open_database(write=write)
self.log.info("Querying %s", query_str)
query = notmuch.Query(self.database, query_str)
query.set_sort(notmuch.Query.SORT.OLDEST_FIRST)
elements = query.search_messages()
nb_msgs = query.count_messages()
iterator = (
progressbar.progressbar(elements, max_value=nb_msgs)
if show_progress and nb_msgs
else elements
)
self.log.info("Executing %s", action)
for msg in iterator:
self.log.debug("On mail %s", msg)
if write:
msg.freeze()
action(msg, *args, **kwargs)
if write:
msg.thaw()
msg.tags_to_maildir_flags()
if close_db:
self.close_database()
return nb_msgs
class MelOutput:
"""
All functions that print mail stuff onto the screen.
"""
WIDTH_FIXED = 31
WIDTH_RATIO_DEST_SUBJECT = 0.3
def compute_line_format(
self,
) -> typing.Tuple[typing.Optional[int], typing.Optional[int]]:
"""
Based on the terminal width, assign the width of flexible columns.
"""
if self.is_tty:
columns, _ = shutil.get_terminal_size((80, 20))
remain = columns - MelOutput.WIDTH_FIXED - 1
dest_width = int(remain * MelOutput.WIDTH_RATIO_DEST_SUBJECT)
subject_width = remain - dest_width
return (dest_width, subject_width)
return (None, None)
def __init__(self, engine: MelEngine) -> None:
colorama.init()
self.log = logging.getLogger("MelOutput")
self.engine = engine
self.light_background = True
self.is_tty = sys.stdout.isatty()
self.dest_width, self.subject_width = self.compute_line_format()
self.mailbox_colors: typing.Dict[str, str] = dict()
# TODO Allow custom path
self.caps = mailcap.getcaps()
@staticmethod
def format_date(date: datetime.datetime) -> str:
"""
Format the given date as a 9-characters width string.
Show the time if the mail is less than 24h old,
else show the date.
"""
now = datetime.datetime.now()
if now - date < datetime.timedelta(days=1):
return date.strftime("%H:%M:%S")
if now - date < datetime.timedelta(days=28):
return date.strftime("%d %H:%M")
if now - date < datetime.timedelta(days=365):
return date.strftime("%m-%d %H")
return date.strftime("%y-%m-%d")
@staticmethod
def clip_text(size: typing.Optional[int], text: str) -> str:
"""
Fit text into the given character size,
fill with spaces if shorter,
clip with … if larger.
"""
if size is None:
return text
length = len(text)
if length == size:
return text
if length > size:
return text[: size - 1] + "…"
return text + " " * (size - length)
@staticmethod
def chunks(iterable: str, chunk_size: int) -> typing.Iterable[str]:
"""Yield successive chunk_size-sized chunks from iterable."""
# From https://stackoverflow.com/a/312464
for i in range(0, len(iterable), chunk_size):
yield iterable[i : i + chunk_size]
@staticmethod
def sizeof_fmt(num: int, suffix: str = "B") -> str:
"""
Print the given size in a human-readable format.
"""
remainder = float(num)
# From https://stackoverflow.com/a/1094933
for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
if abs(remainder) < 1024.0:
return "%3.1f %s%s" % (remainder, unit, suffix)
remainder /= 1024.0
return "%.1f %s%s" % (remainder, "Yi", suffix)
def get_mailbox_color(self, mailbox: str) -> str:
"""
Return the color of the given mailbox in a ready to print
string with ASCII escape codes.
"""
if not self.is_tty:
return ""
if mailbox not in self.mailbox_colors:
# RGB colors (not supported everywhere)
# color_str = self.config[mailbox]["color"]
# color_str = color_str[1:] if color_str[0] == '#' else color_str
# R = int(color_str[0:2], 16)
# G = int(color_str[2:4], 16)
# B = int(color_str[4:6], 16)
# self.mailbox_colors[mailbox] = f"\x1b[38;2;{R};{G};{B}m"
color_int = int(self.engine.config[mailbox]["color16"])
self.mailbox_colors[mailbox] = f"\x1b[38;5;{color_int}m"
return self.mailbox_colors[mailbox]
def print_msg(self, msg: notmuch.Message) -> None:
"""
Print the given message header on one line.
"""
if not self.dest_width:
self.compute_line_format()
sep = " " if self.is_tty else "\t"
line = ""
tags = set(msg.get_tags())
mailbox, _, _ = self.engine.get_location(msg)
if "unread" in tags or "flagged" in tags:
line += colorama.Style.BRIGHT
# if 'flagged' in tags:
# line += colorama.Style.BRIGHT
# if 'unread' not in tags:
# line += colorama.Style.DIM
line += (
colorama.Back.LIGHTBLACK_EX
if self.light_background
else colorama.Back.BLACK
)
self.light_background = not self.light_background
line += self.get_mailbox_color(mailbox)
# UID
uid = None
for tag in tags:
if tag.startswith("tuid"):
uid = tag[4:]
assert uid, f"No UID for message: {msg}."
assert MelEngine.is_uid(uid), f"{uid} {type(uid)} is not a valid UID."
line += uid
# Date
line += sep + colorama.Fore.MAGENTA
date = datetime.datetime.fromtimestamp(msg.get_date())
line += self.format_date(date)
# Icons
line += sep + colorama.Fore.RED
def tags2col1(
tag1: str, tag2: str, characters: typing.Tuple[str, str, str, str]
) -> None:
"""
Show the presence/absence of two tags with one character.
"""
nonlocal line
both, first, second, none = characters
if tag1 in tags:
if tag2 in tags:
line += both
else:
line += first
else:
if tag2 in tags:
line += second
else:
line += none
tags2col1("spam", "draft", ("?", "S", "D", " "))
tags2col1("attachment", "encrypted", ("E", "A", "E", " "))
tags2col1("unread", "flagged", ("!", "U", "F", " "))
tags2col1("sent", "replied", ("?", "↑", "↪", " "))
# Opposed
line += sep + colorama.Fore.BLUE
if "sent" in tags:
dest = msg.get_header("to")
else:
dest = msg.get_header("from")
line += MelOutput.clip_text(self.dest_width, dest)
# Subject
line += sep + colorama.Fore.WHITE
subject = msg.get_header("subject")
line += MelOutput.clip_text(self.subject_width, subject)
if self.is_tty:
line += colorama.Style.RESET_ALL
print(line)
def notify_msg(self, msg: notmuch.Message) -> None:
"""
Send a notification for the given message.
"""
self.log.info("Sending notification for %s", msg)
subject = msg.get_header("subject")
expd = msg.get_header("from")
account, _, _ = self.engine.get_location(msg)
summary = "{} (<i>{}</i>)".format(html.escape(expd), account)
body = html.escape(subject)
cmd = ["notify-send", "-u", "low", "-i", "mail-message-new", summary, body]
print(" ".join(cmd))
subprocess.run(cmd, check=False)
def notify_all(self) -> None:
"""
Send a notification for unprocessed and unread message.
Basically should only send a notification for a given message once
since it should be marked as processed right after.
"""
nb_msgs = self.engine.apply_msgs(
"tag:unread and tag:unprocessed", self.notify_msg
)
if nb_msgs > 0:
self.log.info("Playing notification sound (%d new message(s))", nb_msgs)
cmd = [
"play",
"-n",
"synth",
"sine",
"E4",
"sine",
"A5",
"remix",
"1-2",
"fade",
"0.5",
"1.2",
"0.5",
"2",
]
subprocess.run(cmd, check=False)
@staticmethod
def format_header_value(val: str) -> str:
"""
Return split header values in a contiguous string.
"""
return val.replace("\n", "").replace("\t", "").strip()
PART_MULTI_FORMAT = (
colorama.Fore.BLUE + "{count} {indent}+ {typ}" + colorama.Style.RESET_ALL
)
PART_LEAF_FORMAT = (
colorama.Fore.BLUE
+ "{count} {indent}→ {desc} ({typ}; {size})"
+ colorama.Style.RESET_ALL
)
def show_parts_tree(
self, part: email.message.Message, depth: int = 0, count: int = 1
) -> int:
"""
Show a tree of the parts contained in a message.
Return the number of parts of the mesage.
"""
indent = depth * "\t"
typ = part.get_content_type()
if part.is_multipart():
print(
MelOutput.PART_MULTI_FORMAT.format(count=count, indent=indent, typ=typ)
)
payl = part.get_payload()
assert isinstance(payl, list)
size = 1
for obj in payl:
size += self.show_parts_tree(obj, depth=depth + 1, count=count + size)
return size
payl = part.get_payload(decode=True)
assert isinstance(payl, bytes)
size = len(payl)
desc = part.get("Content-Description", "<no description>")
print(
MelOutput.PART_LEAF_FORMAT.format(
count=count,
indent=indent,
typ=typ,
desc=desc,
size=MelOutput.sizeof_fmt(size),
)
)
return 1
INTERESTING_HEADERS = ["Date", "From", "Subject", "To", "Cc", "Message-Id"]
HEADER_FORMAT = (
colorama.Fore.BLUE
+ colorama.Style.BRIGHT
+ "{}:"
+ colorama.Style.NORMAL
+ " {}"
+ colorama.Style.RESET_ALL
)
def read_msg(self, msg: notmuch.Message) -> None:
"""
Display the content of a mail.
"""
# Parse
filename = msg.get_filename()
parser = email.parser.BytesParser()
with open(filename, "rb") as filedesc:
mail = parser.parse(filedesc)
# Defects
if mail.defects:
self.log.warning("Defects found in the mail:")
for defect in mail.defects:
self.log.warning(defect)
# Headers
for key in MelOutput.INTERESTING_HEADERS:
val = mail.get(key)
if val:
assert isinstance(val, str)
val = self.format_header_value(val)
print(MelOutput.HEADER_FORMAT.format(key, val))
# TODO Show all headers
# TODO BONUS Highlight failed verifications
self.show_parts_tree(mail)
print()
# Show text/plain
# TODO Consider alternative
for part in mail.walk():
if part.is_multipart():
continue
payl = part.get_payload(decode=True)
assert isinstance(payl, bytes)
if part.get_content_type() == "text/plain":
print(payl.decode())
else:
# TODO Use nametemplate from mailcap
temp_file = "/tmp/melcap.html" # TODO Real temporary file
# TODO FIFO if possible
with open(temp_file, "wb") as temp_filedesc:
temp_filedesc.write(payl)
command, _ = mailcap.findmatch(
self.caps, part.get_content_type(), key="view", filename=temp_file
)
if command:
os.system(command)
def print_dir_list(self) -> None:
"""
Print a colored directory list.
Every line is easilly copiable.
"""
for arb in self.engine.list_folders():
line = colorama.Fore.LIGHTBLACK_EX + "'"
line += self.get_mailbox_color(arb[0])
line += arb[0].replace("'", "\\'")
line += colorama.Fore.LIGHTBLACK_EX
for inter in arb[1:-1]:
line += "/" + inter.replace("'", "\\'")
line += "/" + colorama.Fore.WHITE + arb[-1].replace("'", "\\'")
line += colorama.Fore.LIGHTBLACK_EX + "'"
line += colorama.Style.RESET_ALL
print(line)
class MelCLI:
"""
Handles the user input and run asked operations.
"""
VERBOSITY_LEVELS = ["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
def apply_msgs_input(
self,
argmessages: typing.List[str],
action: typing.Callable,
write: bool = False,
) -> None:
"""
Run a function on the message given by the user.
"""
# TODO First argument might be unecessary
if not argmessages:
from_stdin = not sys.stdin.isatty()
if argmessages:
from_stdin = len(argmessages) == 1 and argmessages == "-"
messages = list()
if from_stdin:
for line in sys.stdin:
uid = line[:12]
if not MelEngine.is_uid(uid):
self.log.error("Not an UID: %s", uid)
continue
messages.append(uid)
else:
for uids in argmessages:
if len(uids) > 12:
self.log.warning(
"Might have forgotten some spaces "
"between the UIDs. Don't worry, I'll "
"split them for you"
)
for uid in MelOutput.chunks(uids, 12):
if not MelEngine.is_uid(uid):
self.log.error("Not an UID: %s", uid)
continue
messages.append(uid)
for message in messages:
query_str = f"tag:tuid{message}"
nb_msgs = self.engine.apply_msgs(
query_str, action, write=write, close_db=False
)
if nb_msgs < 1:
self.log.error("Couldn't execute function for message %s", message)
self.engine.close_database()
def operation_default(self) -> None:
"""
Default operation: list all message in the inbox
"""
self.engine.apply_msgs("tag:inbox", self.output.print_msg)
def operation_inbox(self) -> None:
"""
Inbox operation: list all message in the inbox,
possibly only the unread ones.
"""
query_str = "tag:unread" if self.args.only_unread else "tag:inbox"
self.engine.apply_msgs(query_str, self.output.print_msg)
def operation_flag(self) -> None:
"""
Flag operation: Flag user selected messages.
"""
def flag_msg(msg: notmuch.Message) -> None:
"""
Flag given message.
"""
msg.add_tag("flagged")
self.apply_msgs_input(self.args.message, flag_msg, write=True)
def operation_unflag(self) -> None:
"""
Unflag operation: Flag user selected messages.
"""
def unflag_msg(msg: notmuch.Message) -> None:
"""
Unflag given message.
"""
msg.remove_tag("flagged")
self.apply_msgs_input(self.args.message, unflag_msg, write=True)
def operation_read(self) -> None:
"""
Read operation: show full content of selected message
"""
self.apply_msgs_input(self.args.message, self.output.read_msg)
def operation_fetch(self) -> None:
"""
Fetch operation: Sync remote databases with the local one.
"""
# Fetch mails
self.log.info("Fetching mails")
mbsync_config_file = os.path.expanduser("~/.config/mbsyncrc") # TODO Better
cmd = ["mbsync", "--config", mbsync_config_file, "--all"]
subprocess.run(cmd, check=False)
# Index new mails
self.engine.notmuch_new()
# Notify
self.output.notify_all()
# Tag new mails
self.engine.apply_msgs(
"tag:unprocessed", self.engine.retag_msg, show_progress=True, write=True
)
def operation_list(self) -> None:
"""
List operation: Print all folders.
"""
self.output.print_dir_list()
def operation_debug(self) -> None:
"""
DEBUG
"""
print("UwU")
def operation_retag(self) -> None:
"""
Retag operation: Manually retag all the mails in the database.
Mostly debug I suppose.
"""
self.engine.apply_msgs(
"*", self.engine.retag_msg, show_progress=True, write=True
)
def operation_all(self) -> None:
"""
All operation: list every single message.
"""
self.engine.apply_msgs("*", self.output.print_msg)
def add_subparsers(self) -> None:
"""
Add the operation parser to the main parser.
"""
# TODO If the only operation to the parser done are adding argument,
# we should automate this.
subparsers = self.parser.add_subparsers(help="Action to execute")
# List messages
self.parser.set_defaults(operation=self.operation_default)
# inbox (default)
parser_inbox = subparsers.add_parser(
"inbox", help="Show unread, unsorted and flagged messages"
)
parser_inbox.add_argument(
"-u", "--only-unread", action="store_true", help="Show unread messages only"
)
# TODO Make this more relevant
parser_inbox.set_defaults(operation=self.operation_inbox)
# list folder [--recurse]
# List actions
parser_list = subparsers.add_parser("list", help="List all folders")
# parser_list.add_argument('message', nargs='*', help="Messages")
parser_list.set_defaults(operation=self.operation_list)
# flag msg...
parser_flag = subparsers.add_parser("flag", help="Mark messages as flagged")
parser_flag.add_argument("message", nargs="*", help="Messages")
parser_flag.set_defaults(operation=self.operation_flag)
# unflag msg...
parser_unflag = subparsers.add_parser(
"unflag", help="Mark messages as not-flagged"
)
parser_unflag.add_argument("message", nargs="*", help="Messages")
parser_unflag.set_defaults(operation=self.operation_unflag)
# delete msg...
# spam msg...
# move dest msg...
# Read message
# read msg [--html] [--plain] [--browser]
parser_read = subparsers.add_parser("read", help="Read message")
parser_read.add_argument("message", nargs=1, help="Messages")
parser_read.set_defaults(operation=self.operation_read)
# attach msg [id] [--save] (list if no id, xdg-open else)
# Redaction
# new account
# reply msg [--all]
# Folder management
# tree [folder]
# mkdir folder
# rmdir folder (prevent if folder isn't empty (mail/subfolder))
# (yeah that should do)
# Meta
# setup (interactive thing maybe)
# fetch (mbsync, notmuch new, retag, notify; called by greater gods)
parser_fetch = subparsers.add_parser(
"fetch", help="Fetch mail, tag them, and run notifications"
)
parser_fetch.set_defaults(operation=self.operation_fetch)
# Debug
# debug (various)
parser_debug = subparsers.add_parser(
"debug", help="Who know what this holds..."
)
parser_debug.set_defaults(verbosity="DEBUG")
parser_debug.set_defaults(operation=self.operation_debug)
# retag (all or unprocessed)
parser_retag = subparsers.add_parser(
"retag", help="Retag all mails (when you changed configuration)"
)
parser_retag.set_defaults(operation=self.operation_retag)
# all
parser_all = subparsers.add_parser("all", help="Show ALL messages")
parser_all.set_defaults(operation=self.operation_all)
def create_parser(self) -> argparse.ArgumentParser:
"""
Create the main parser that will handle the user arguments.
"""
parser = argparse.ArgumentParser(description="Meh mail client")
parser.add_argument(
"-v",
"--verbosity",
choices=MelCLI.VERBOSITY_LEVELS,
default="WARNING",
help="Verbosity of self.log messages",
)
# parser.add_argument('-n', '--dry-run', action='store_true',
# help="Don't do anything") # DEBUG
default_config_file = os.path.join(
xdg.BaseDirectory.xdg_config_home, "mel", "accounts.conf"
)
parser.add_argument(
"-c", "--config", default=default_config_file, help="Accounts config file"
)
return parser
def __init__(self) -> None:
self.log = logging.getLogger("MelCLI")
self.parser = self.create_parser()
self.add_subparsers()
self.args = self.parser.parse_args()
coloredlogs.install(
level=self.args.verbosity, fmt="%(levelname)s %(name)s %(message)s"
)
self.engine = MelEngine(self.args.config)
self.output = MelOutput(self.engine)
if self.args.operation:
self.log.info("Executing operation %s", self.args.operation)
self.args.operation()
if __name__ == "__main__":
if not os.environ.get("MEL_DEBUG"):
CLI = MelCLI()
else:
try:
CLI = MelCLI()
except:
EXTYPE, VALUE, TB = sys.exc_info()
traceback.print_exc()
pdb.post_mortem(TB)

View file

@ -0,0 +1,344 @@
#!/usr/bin/env python3
"""
Meh mail client conf generator for other things
"""
import configparser
import os
import sys
# TODO Find config file from XDG
# TODO Signature file
# TODO Write ~/.mail/[mailbox]/color file if required by sth?
# TODO Write in .config or .cache /mel
# TODO Fix IMAPS with mbsync
configPath = os.path.join(os.path.expanduser("~"), ".config", "mel", "accounts.conf")
config = configparser.ConfigParser()
config.read(configPath)
storageFull = os.path.realpath(os.path.expanduser(config["GENERAL"]["storage"]))
config["GENERAL"]["storage"] = storageFull
SERVER_DEFAULTS = {
"imap": {"port": 143, "starttls": True},
"smtp": {"port": 587, "starttls": True},
}
SERVER_ITEMS = {"host", "port", "user", "pass", "starttls"}
ACCOUNT_DEFAULTS = {
"color": "#FFFFFF",
"color16": "0",
# "colormutt": "white",
"inboxfolder": "INBOX",
"archivefolder": "Archive",
"draftsfolder": "Drafts",
"sentfolder": "Sent",
"spamfolder": "Spam",
"trashfolder": "Trash",
}
# Reading sections
accounts = dict()
mails = set()
for name in config.sections():
if not name.islower():
continue
section = config[name]
data = dict()
for server in SERVER_DEFAULTS.keys():
for item in SERVER_ITEMS:
key = server + item
try:
val = (
section.get(key)
or section.get(item)
or SERVER_DEFAULTS[server][item]
)
except KeyError:
raise KeyError("{}.{}".format(name, key))
if isinstance(val, str):
if val == "True":
val = True
elif val == "False":
val = False
elif val.isnumeric():
val = int(val)
data[key] = val
for key in section.keys():
if key in SERVER_ITEMS:
continue
data[key] = section[key]
for k, v in config["DEFAULT"].items():
if k not in data:
data[k] = v
for k, v in ACCOUNT_DEFAULTS.items():
if k not in data:
data[k] = v
mails.add(section["from"])
if "alternatives" in section:
for alt in section["alternatives"].split(";"):
mails.add(alt)
data["account"] = name
data["storage"] = os.path.join(config["GENERAL"]["storage"], name)
data["storageInbox"] = os.path.join(data["storage"], "INBOX")
accounts[name] = data
general = dict()
section = config["GENERAL"]
for key in section.keys():
general[key] = section[key]
general["main"] = accounts[general["main"]]
# OfflineIMAP
OFFLINEIMAP_BEGIN = """[general]
# List of accounts to be synced, separated by a comma.
accounts = {}
maxsyncaccounts = {}
stocktimeout = 60
pythonfile = ~/.config/offlineimap.py
[mbnames]
enabled = yes
filename = ~/.mutt/mailboxes
header = "mailboxes "
peritem = "+%(accountname)s/%(foldername)s"
sep = " "
footer = "\\n"
"""
OFFLINEIMAP_ACCOUNT = """[Account {account}]
localrepository = {account}-local
remoterepository = {account}-remote
autorefresh = 0.5
quick = 10
utf8foldernames = yes
postsynchook = ~/.mutt/postsync
[Repository {account}-local]
type = Maildir
localfolders = {storage}
[Repository {account}-remote]
type = IMAP
{secconf}
keepalive = 60
holdconnectionopen = yes
remotehost = {imaphost}
remoteport = {imapport}
remoteuser = {imapuser}
remotepass = {imappass}
"""
offlineIMAPstr = OFFLINEIMAP_BEGIN.format(",".join(accounts), len(accounts))
for name, account in accounts.items():
if account["imapstarttls"]:
secconf = "ssl = no"
else:
secconf = "sslcacertfile = /etc/ssl/certs/ca-certificates.crt"
offlineIMAPstr += OFFLINEIMAP_ACCOUNT.format(**account, secconf=secconf)
# TODO Write
# mbsync
MBSYNC_ACCOUNT = """IMAPAccount {account}
Host {imaphost}
Port {imapport}
User {imapuser}
Pass "{imappassEscaped}"
{secconf}
IMAPStore {account}-remote
Account {account}
MaildirStore {account}-local
Subfolders Verbatim
Path {storage}/
Inbox {storageInbox}/
Channel {account}
Master :{account}-remote:
Slave :{account}-local:
Patterns *
Create Both
SyncState *
"""
mbsyncStr = ""
for name, account in accounts.items():
if account["imapstarttls"]:
secconf = "SSLType STARTTLS"
else:
secconf = "SSLType IMAPS"
if "certificate" in account:
secconf += "\nCertificateFile {certificate}".format(**account)
imappassEscaped = account["imappass"].replace("\\", "\\\\")
mbsyncStr += MBSYNC_ACCOUNT.format(
**account, secconf=secconf, imappassEscaped=imappassEscaped
)
mbsyncFilepath = os.path.join(os.path.expanduser("~"), ".config/mel/mbsyncrc")
with open(mbsyncFilepath, "w") as f:
f.write(mbsyncStr)
# msmtp
MSMTP_BEGIN = """defaults
protocol smtp
auth on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
"""
MSMTP_ACCOUNT = """account {account}
from {from}
user {smtpuser}
password {smtppass}
host {smtphost}
port {smtpport}
tls on
"""
msmtpStr = MSMTP_BEGIN
for name, account in accounts.items():
msmtpStr += MSMTP_ACCOUNT.format(**account)
mbsyncFilepath = os.path.join(os.path.expanduser("~"), ".config/msmtp/config")
with open(mbsyncFilepath, "w") as f:
f.write(msmtpStr)
# notmuch
NOTMUCH_BEGIN = """[database]
path={storage}
[user]
name={main[name]}
primary_email={main[from]}
other_email={other_email}
[new]
tags=unprocessed;unread;
ignore=
[search]
exclude_tags=deleted;spam;
[maildir]
synchronize_flags=true
[crypto]
gpg_path=gpg
"""
other_email = mails.copy()
other_email.remove(general["main"]["from"])
other_email = ";".join(other_email)
notmuchStr = NOTMUCH_BEGIN.format(**general, other_email=other_email)
mbsyncFilepath = os.path.join(os.path.expanduser("~"), ".config/notmuch-config")
with open(mbsyncFilepath, "w") as f:
f.write(notmuchStr)
# mutt (temp)
## mailboxes
MAILBOXES_BEGIN = "mailboxes"
mailboxesStr = MAILBOXES_BEGIN
for name, account in accounts.items():
lines = "-" * (20 - len(name))
mailboxesStr += f' "+{name}{lines}"'
for root, dirs, files in os.walk(account["storage"]):
if "cur" not in dirs or "new" not in dirs or "tmp" not in dirs:
continue
assert root.startswith(storageFull)
path = root[len(storageFull) + 1 :]
mailboxesStr += f' "+{path}"'
mailboxesStr += "\n"
mailboxesFilepath = os.path.join(os.path.expanduser("~"), ".mutt/mailboxes")
with open(mailboxesFilepath, "w") as f:
f.write(mailboxesStr)
## accounts
# TODO html mails
MUTT_ACCOUNT = """set from = "{from}"
set sendmail = "/usr/bin/msmtp -a {account}"
set realname = "{name}"
set spoolfile = "+{account}/{inboxfolder}"
set mbox = "+{account}/{archivefolder}"
set postponed = "+{account}/{draftsfolder}"
set record = "+{account}/{sentfolder}"
set trash = "+{account}/{trashfolder}"
set signature = "~/.mutt/accounts/{account}.sig"
set content_type = "text/plain"
set sig_dashes = yes
color status {colormutt} default
macro index D \\
"<clear-flag>N<save-message>+{account}/{trashfolder}<enter>" \\
"move message to the trash"
macro index S \\
"<clear-flag>N<save-message>+{account}/{spamfolder}<enter>" \\
"mark message as spam"
# vim: syntax=muttrc
"""
for name, account in accounts.items():
muttStr = MUTT_ACCOUNT.format(**account)
# Config
muttFilepath = os.path.join(os.path.expanduser("~"), f".mutt/accounts/{name}")
with open(muttFilepath, "w") as f:
f.write(muttStr)
# Signature
sigStr = account.get("sig", account.get("name", ""))
sigFilepath = os.path.join(os.path.expanduser("~"), f".mutt/accounts/{name}.sig")
with open(sigFilepath, "w") as f:
f.write(sigStr)
MUTT_SELECTOR = """
set folder = "{storage}"
source ~/.mutt/mailboxes
source ~/.mutt/accounts/{main[account]}
{hooks}
source ~/.mutt/custom
# vim: syntax=muttrc
"""
selectStr = ""
hooks = ""
for name, account in accounts.items():
hooks += f"folder-hook {name}/* source ~/.mutt/accounts/{name}\n"
selectStr += MUTT_SELECTOR.format(**general, hooks=hooks)
selectFilepath = os.path.join(os.path.expanduser("~"), ".mutt/muttrc")
with open(selectFilepath, "w") as f:
f.write(selectStr)
## Color
for name, account in accounts.items():
# Config
colorFilepath = os.path.join(
os.path.expanduser("~"), f'{general["storage"]}/{name}/color'
)
with open(colorFilepath, "w") as f:
f.write(account["color"])

View file

@ -0,0 +1,39 @@
#!/usr/bin/env python3
import logging
import os
import shutil
import sys
import coloredlogs
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
log = logging.getLogger()
MUSICS_FOLDER = os.path.join(os.path.expanduser("~"), "Musique")
BOF_FOLDER = os.path.join(os.path.expanduser("~"), ".MusiqueBof")
for f in sys.argv[1:]:
src = os.path.realpath(f)
if not os.path.isfile(src):
log.error("{} does not exists".format(src))
continue
srcBase = None
if src.startswith(MUSICS_FOLDER):
srcBase = MUSICS_FOLDER
dstBase = BOF_FOLDER
elif src.startswith(BOF_FOLDER):
srcBase = BOF_FOLDER
dstBase = MUSIC_FOLDER
else:
log.error("{} not in any music folder".format(src))
continue
common = os.path.relpath(src, srcBase)
dst = os.path.join(dstBase, common)
dstFolder = os.path.dirname(dst)
log.info("{} → {}".format(src, dst))
os.makedirs(dstFolder, exist_ok=True)
shutil.move(src, dst)

83
unprocessed/config/scripts/nv Executable file
View file

@ -0,0 +1,83 @@
#!/usr/bin/env bash
# Extracted frm nvidia-xrun
DRY_RUN=0
function execute {
if [[ ${DRY_RUN} -eq 1 ]]
then
echo ">>Dry run. Command: $*"
else
eval $*
fi
}
function turn_off_gpu {
if [[ "$REMOVE_DEVICE" == '1' ]]; then
echo 'Removing Nvidia bus from the kernel'
execute "sudo tee /sys/bus/pci/devices/${DEVICE_BUS_ID}/remove <<<1"
else
echo 'Enabling powersave for the graphic card'
execute "sudo tee /sys/bus/pci/devices/${DEVICE_BUS_ID}/power/control <<<auto"
fi
echo 'Enabling powersave for the PCIe controller'
execute "sudo tee /sys/bus/pci/devices/${CONTROLLER_BUS_ID}/power/control <<<auto"
}
function turn_on_gpu {
echo 'Turning the PCIe controller on to allow card rescan'
execute "sudo tee /sys/bus/pci/devices/${CONTROLLER_BUS_ID}/power/control <<<on"
echo 'Waiting 1 second'
execute "sleep 1"
if [[ ! -d /sys/bus/pci/devices/${DEVICE_BUS_ID} ]]; then
echo 'Rescanning PCI devices'
execute "sudo tee /sys/bus/pci/rescan <<<1"
echo "Waiting ${BUS_RESCAN_WAIT_SEC} second for rescan"
execute "sleep ${BUS_RESCAN_WAIT_SEC}"
fi
echo 'Turning the card on'
execute "sudo tee /sys/bus/pci/devices/${DEVICE_BUS_ID}/power/control <<<on"
}
function load_modules {
for module in "${MODULES_LOAD[@]}"
do
echo "Loading module ${module}"
execute "sudo modprobe ${module}"
done
}
function unload_modules {
for module in "${MODULES_UNLOAD[@]}"
do
echo "Unloading module ${module}"
execute "sudo modprobe -r ${module}"
done
}
if [[ "$1" == "-d" ]]
then
DRY_RUN=1
shift 1
fi
# load config file
. /etc/default/nvidia-xrun
if [ "$1" == "on" ]
then
turn_on_gpu
load_modules
elif [ "$1" == "off" ]
then
unload_modules
turn_off_gpu
else
echo "Usage: $0 [on|off]"
fi

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,22 @@
{
"name": "geoffreyfrogeye-dotfiles-scripts",
"version": "1.0.0",
"description": "Stores dependencies used for GeoffreyFrogeye's dotfiles scripts.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://git.frogeye.fr/geoffrey/dotfiles"
},
"author": "GeoffreyFrogeye",
"license": "GPL-3.0",
"dependencies": {
"highlight.js": "^9.11.0",
"html-pdf": "^2.1.0",
"katex": "^0.7.1",
"marked": "^0.3.6",
"yargs": "^8.0.1"
}
}

103
unprocessed/config/scripts/proxy Executable file
View file

@ -0,0 +1,103 @@
#!/usr/bin/env bash
# Allows easy manipulation of the proxy variables
function proxy_set_help {
echo "Usage: $0 set ADDRESS"
echo
echo "Arguments:"
echo " ADDRESS Address of the proxy"
echo
echo "Examples:"
echo ' eval "$(proxy set http://proxy.mycompany.com:3128/)"'
return 0
}
function proxy_set {
if [ -z $1 ]; then
proxy_set_help
return 1
fi
echo "export http_proxy='$1'"
echo "export https_proxy='$1'"
echo "export ftp_proxy='$1'"
echo "export rsync_proxy='$1'"
exit 0
}
function proxy_setup_help {
echo "Usage: $0 setup"
echo
echo "Examples:"
echo " proxy_set # Then eval the output"
return 0
}
function proxy_setup {
export no_proxy="localhost,127.0.0.1,localaddress,.localdomain.com"
if (( $# > 0 )); then
valid=$(echo $@ | sed -n 's/\([0-9]\{1,3\}.\)\{4\}:\([0-9]\+\)/&/p')
if [[ $valid != $@ ]]; then
>&2 echo "Invalid address"
return 1
fi
proxy_set "http://$1/"
return 0
fi
echo -n "User: "; read username
if [[ $username != "" ]]; then
echo -n "Password: "
read -es password
local pre="$username:$password@"
fi
echo -n "Server: "; read server
echo -n "Port: "; read port
proxy_set "http://$pre$server:$port/"
return 0
}
function proxy_off_help {
echo "Usage: $0 off"
echo
echo "Examples:"
echo ' eval $(proxy off)'
return 0
}
function proxy_off {
echo 'unset http_proxy'
echo 'unset https_proxy'
echo 'unset ftp_proxy'
echo 'unset rsync_proxy'
return 0
}
function proxy_help {
command="$1"
if [ -n "$command" ]; then
if type "proxy_${command}_help" &> /dev/null; then
shift
"proxy_${command}_help" "$@"
return $?
fi
fi
echo "Usage: $0 COMMAND"
echo
echo "Commands:"
echo " setup Interactively setup proxy"
echo " set Set proxy from address"
echo " off Turn off proxy"
echo " help Get help with commands"
return 0
}
# MAIN
command="$1"
shift
if type "proxy_$command" &> /dev/null; then
"proxy_$command" "$@"
else
proxy_help
fi

View file

@ -0,0 +1,14 @@
#!/usr/bin/env bash
#! nix-shell -i bash --pure
#! nix-shell -p bash
# Removes CRLF (^M or \r) from a file
#sed -e "s/^M//" "$1" -i
tmpfile=$(mktemp)
cp "$1" "$tmpfile"
tr -d '\r' < "$tmpfile" > "$1"
rm "$tmpfile"

View file

@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Dynamically determines if the ssh connection
# is to be proxied through `proxytunnel`
# To be used with ssh_config ProxyCommand
host="$1"
port="$2"
if [ -z "$http_proxy" ]; then
socat "TCP:$host:$port" -
else
proxy=$(echo "$http_proxy" | sed 's/^https\?:\/\///' | sed 's/\/$//')
port=443 # Most won't want this
echo "$proxy" | grep '@'
if [ $? == 0 ]; then
user=$(echo $proxy | cut -d '@' -f 2)
proxy=$(echo $proxy | cut -d '@' -f 1)
proxytunnel -p $proxy -P $user -d $host:$port
else
proxytunnel -p $proxy -d $host:$port
fi
fi

193
unprocessed/config/scripts/tvshow Executable file
View file

@ -0,0 +1,193 @@
#!/usr/bin/env python3
# pylint: disable=C0103,W0621
# pip install tmdbv3api
import os
import re
import subprocess
import sys
import typing
import tmdbv3api
# TODO Override files without warning
# TODO Dry run mode (just comment the last line ^^)
# Typing
Episode = typing.Any # TODO
# Constants
API_KEY_PASS_PATH = "http/themoviedb.org"
VIDEO_EXTENSIONS = {"mp4", "mkv", "avi", "webm"}
# Functions
def get_pass_data(path: str) -> typing.Dict[str, str]:
"""
Returns the data stored in the Unix password manager
given its path.
"""
run = subprocess.run(["pass", path], stdout=subprocess.PIPE, check=True)
lines = run.stdout.decode().split("\n")
data = dict()
data["pass"] = lines[0]
for line in lines[1:]:
match = re.match(r"(\w+): ?(.+)", line)
if match:
data[match[1]] = match[2]
return data
def confirm(text: str) -> bool:
res = input(text + " [yn] ")
while res not in ("y", "n"):
res = input("Please answer with y or n: ")
return res == "y"
def episode_identifier(episode: typing.Any) -> str:
return (
f"S{episode['season_number']:02d}E"
+ f"{episode['episode_number']:02d} {episode['name']}"
)
dryrun = "-n" in sys.argv
if dryrun:
dryrun = True
sys.argv.remove("-n")
# Connecting to TMBDB
tmdb = tmdbv3api.TMDb()
tmdb.api_key = get_pass_data(API_KEY_PASS_PATH)["api"]
tmdb.language = sys.argv[1]
# Searching the TV show name (by current directory name)
tv = tmdbv3api.TV()
season = tmdbv3api.Season()
if len(sys.argv) >= 3:
show_name = sys.argv[2]
else:
show_name = os.path.split(os.path.realpath(os.path.curdir))[1]
if "(" in show_name:
show_name = show_name.split("(")[0].strip()
search = tv.search(show_name)
# Asking the user to select the one
show = None
for res in search:
print(f"#{res.id} {res.name} ({res.first_air_date[:4]}): {res.overview}")
if confirm("Is this the show for this folder?"):
show = tv.details(res.id)
break
if not show:
print("Could not find a matching " + f"show on TheMovieDatabase for {show_name}.")
sys.exit(1)
# Retrieving all the episode of the show
episodes: typing.List[Episode] = list()
print(f"List of episodes for {show.name}:")
for season_number in range(0, show.number_of_seasons + 1):
season_details = season.details(show.id, season_number)
try:
season_details.episodes
except AttributeError:
continue
for episode in season_details.episodes:
episodes.append(episode)
print(f"- {episode_identifier(episode)}")
# Finding movie files in the folder
print("List of video files in this folder")
videos: typing.List[typing.Tuple[str, str]] = list()
for root, dirs, files in os.walk(os.path.curdir):
for filename in files:
basename, ext = os.path.splitext(filename)
real_ext = ext[1:].lower()
if real_ext not in VIDEO_EXTENSIONS:
continue
videos.append((root, filename))
print(f"- {filename}")
def get_episode(season_number: int, episode_number: int) -> typing.Optional[Episode]:
# TODO Make more efficient using indexing
for episode in episodes:
if (
episode["season_number"] == season_number
and episode["episode_number"] == episode_number
):
return episode
return None
# Matching movie files to episode
associations: typing.List[typing.Tuple[typing.Tuple[str, str], Episode]] = list()
for video in videos:
root, filename = video
match = re.search(r"S(\d+)E(\d+)", filename)
print(f"Treating file: {root}/{filename}")
episode = None
season_number = 0
episode_number = 0
while not episode:
if match:
season_number = int(match[1])
episode_number = int(match[2])
else:
try:
season_number = int(input("Season number ?"))
episode_number = int(input("Episode number ?"))
except ValueError:
continue
if season_number < 0 and episode_number < 0:
break
match = None
episode = get_episode(season_number, episode_number)
if not episode:
print(
f" could not find episode S{season_number:02d}E{episode_number:02d} in TMBD"
)
# Skip
if not episode:
if season_number < -1 and episode_number < -1:
# Skip all
break
# Skip one
continue
associations.append((video, episode))
print(f" associated to: {episode_identifier(episode)}")
# Rename video files
for association in associations:
video, episode = association
root, filename = video
basename, ext = os.path.splitext(filename)
new_name = f"{show.name} ({show.first_air_date[:4]}) {episode_identifier(episode)}"
# Rename all file with the same base name as the original file so we
# can rename nfo files and subtitles (only one though)
for a_filename in os.listdir(root):
a_basename, a_ext = os.path.splitext(a_filename)
if a_basename == basename:
old_path = os.path.join(root, a_filename)
new_path = os.path.join(root, new_name + a_ext)
if old_path == new_path:
continue
print(old_path, "->", new_path)
if not dryrun:
os.rename(old_path, new_path)

View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
echo ssh "$1" sudo date --set="'$(date -R)'"