From 6cbc721e9a8152d85fcb7c510a1907570896ac90 Mon Sep 17 00:00:00 2001 From: Sean Whitton Date: Sat, 7 Apr 2018 10:24:51 -0700 Subject: single dotfiles archive --- archive/bin/apple-setup.sh | 103 ++ archive/bin/backuptom3 | 14 + archive/bin/bashmount | 761 +++++++++ archive/bin/bitlbee_startup | 8 + archive/bin/build | 144 ++ archive/bin/build_rpi_sd_card.sh | 195 +++ archive/bin/cabal-install-exec | 16 + archive/bin/cabal-link-bins | 9 + archive/bin/caffeinate-zenity | 3 + archive/bin/capture-mail | 13 + archive/bin/clean-github-pr.py | 106 ++ archive/bin/coldbkup | 131 ++ archive/bin/ctrlnocaps.ahk | 1 + archive/bin/ctrlswapcaps-nonuk.ahk | 37 + archive/bin/ctrlswapcaps.ahk | 30 + archive/bin/dasl-setup.bat | 7 + archive/bin/develacc | 10 + archive/bin/develacc-inner | 33 + archive/bin/develacc-push | 22 + archive/bin/develacc-push-all | 22 + archive/bin/dionysusbk | 51 + archive/bin/doc_post_receive_hook | 61 + archive/bin/doccheckin.bat | 10 + archive/bin/e | 21 + archive/bin/ed | 5 + archive/bin/emacs-pkg-subtree | 54 + archive/bin/es | 1 + archive/bin/extract_url.pl | 967 ++++++++++++ archive/bin/firejail-skype | 10 + archive/bin/fmdavsetup | 27 + archive/bin/fmr-sync-suspend | 14 + archive/bin/gbampersat.ahk | 2 + archive/bin/git-wip | 328 ++++ archive/bin/goodmorning | 34 + archive/bin/grbk | 39 + archive/bin/greypdf | 77 + archive/bin/hestia-checkup | 19 + archive/bin/hestia-startup | 16 + archive/bin/httphtmltitle.py | 15 + archive/bin/idlesshclear | 8 + archive/bin/imap-pass | 23 + archive/bin/imap-password | 46 + archive/bin/its-all-text-wrapper | 8 + archive/bin/jnest | 10 + archive/bin/kill-ssh-and-umount | 7 + archive/bin/laptopinput | 109 ++ archive/bin/latesteconomist | 9 + archive/bin/m | 1 + archive/bin/ma_org_publish | 64 + archive/bin/ma_reboot_check | 14 + archive/bin/mdns-do | 6 + archive/bin/mutt_bgrun | 118 ++ archive/bin/normalise-artemis | 42 + archive/bin/offlineimap-run | 4 + archive/bin/offlineimap.py | 47 + archive/bin/org-mairix-el-store-link | 3 + archive/bin/orgblosxom2ikiwiki.py | 190 +++ archive/bin/planetnewspipe | 5 + archive/bin/podcastsup | 12 + archive/bin/pomodoro | 83 + archive/bin/privoxy-blocklist.sh | 189 +++ archive/bin/rdate.py | 141 ++ archive/bin/rdate.py-dir | 13 + archive/bin/reading.py | 100 ++ archive/bin/readme-pull-request | 19 + archive/bin/sariulahk.ahk | 130 ++ archive/bin/sdfweb-post-update | 21 + archive/bin/searchmail | 65 + archive/bin/sendmyip | 9 + archive/bin/smtptun | 76 + archive/bin/spwd20 | 180 +++ archive/bin/spwd20-roll | 113 ++ archive/bin/sscan | 253 +++ archive/bin/ssleep | 82 + archive/bin/sync-docs | 27 + archive/bin/update-recoll-db | 6 + archive/bin/urxvtma | 6 + archive/bin/urxvttmux | 41 + archive/bin/urxvttmux-prompt | 9 + archive/bin/weekly-backups | 58 + archive/bin/win32setup.bat | 23 + archive/bin/workstation-uninstallable | 43 + archive/bin/workstation-uninstallable-alt | 30 + archive/bin/xlaunch | 20 + archive/bin/yankfmailpw | 29 + archive/bin/yt | 3 + archive/texmf/bibtex/bst/spwchicago/spwchicago.bst | 1662 ++++++++++++++++++++ archive/texmf/tex/latex/spwdnd/spwdnd.cls | 46 + archive/texmf/tex/latex/spwdoc/spwdoc.cls | 126 ++ archive/texmf/tex/latex/spwessay/spwessay.cls | 186 +++ archive/texmf/tex/latex/spworg/spworg.sty | 50 + archive/texmf/tex/latex/spwoutline/spwoutline.cls | 172 ++ archive/texmf/tex/latex/spwpaper/spwpaper.cls | 198 +++ archive/texmf/tex/latex/spwtitle/spwtitle.sty | 26 + 94 files changed, 8377 insertions(+) create mode 100755 archive/bin/apple-setup.sh create mode 100755 archive/bin/backuptom3 create mode 100755 archive/bin/bashmount create mode 100755 archive/bin/bitlbee_startup create mode 100755 archive/bin/build create mode 100755 archive/bin/build_rpi_sd_card.sh create mode 100755 archive/bin/cabal-install-exec create mode 100755 archive/bin/cabal-link-bins create mode 100755 archive/bin/caffeinate-zenity create mode 100755 archive/bin/capture-mail create mode 100755 archive/bin/clean-github-pr.py create mode 100755 archive/bin/coldbkup create mode 100644 archive/bin/ctrlnocaps.ahk create mode 100644 archive/bin/ctrlswapcaps-nonuk.ahk create mode 100644 archive/bin/ctrlswapcaps.ahk create mode 100755 archive/bin/dasl-setup.bat create mode 100755 archive/bin/develacc create mode 100755 archive/bin/develacc-inner create mode 100755 archive/bin/develacc-push create mode 100755 archive/bin/develacc-push-all create mode 100755 archive/bin/dionysusbk create mode 100755 archive/bin/doc_post_receive_hook create mode 100644 archive/bin/doccheckin.bat create mode 100755 archive/bin/e create mode 100755 archive/bin/ed create mode 100755 archive/bin/emacs-pkg-subtree create mode 120000 archive/bin/es create mode 100755 archive/bin/extract_url.pl create mode 100755 archive/bin/firejail-skype create mode 100755 archive/bin/fmdavsetup create mode 100755 archive/bin/fmr-sync-suspend create mode 100644 archive/bin/gbampersat.ahk create mode 100755 archive/bin/git-wip create mode 100755 archive/bin/goodmorning create mode 100755 archive/bin/grbk create mode 100755 archive/bin/greypdf create mode 100755 archive/bin/hestia-checkup create mode 100755 archive/bin/hestia-startup create mode 100755 archive/bin/httphtmltitle.py create mode 100755 archive/bin/idlesshclear create mode 100755 archive/bin/imap-pass create mode 100755 archive/bin/imap-password create mode 100755 archive/bin/its-all-text-wrapper create mode 100755 archive/bin/jnest create mode 100755 archive/bin/kill-ssh-and-umount create mode 100755 archive/bin/laptopinput create mode 100755 archive/bin/latesteconomist create mode 120000 archive/bin/m create mode 100755 archive/bin/ma_org_publish create mode 100755 archive/bin/ma_reboot_check create mode 100755 archive/bin/mdns-do create mode 100755 archive/bin/mutt_bgrun create mode 100755 archive/bin/normalise-artemis create mode 100755 archive/bin/offlineimap-run create mode 100755 archive/bin/offlineimap.py create mode 100755 archive/bin/org-mairix-el-store-link create mode 100755 archive/bin/orgblosxom2ikiwiki.py create mode 100755 archive/bin/planetnewspipe create mode 100755 archive/bin/podcastsup create mode 100755 archive/bin/pomodoro create mode 100755 archive/bin/privoxy-blocklist.sh create mode 100755 archive/bin/rdate.py create mode 100755 archive/bin/rdate.py-dir create mode 100755 archive/bin/reading.py create mode 100755 archive/bin/readme-pull-request create mode 100644 archive/bin/sariulahk.ahk create mode 100755 archive/bin/sdfweb-post-update create mode 100755 archive/bin/searchmail create mode 100755 archive/bin/sendmyip create mode 100755 archive/bin/smtptun create mode 100755 archive/bin/spwd20 create mode 100755 archive/bin/spwd20-roll create mode 100755 archive/bin/sscan create mode 100755 archive/bin/ssleep create mode 100755 archive/bin/sync-docs create mode 100755 archive/bin/update-recoll-db create mode 100755 archive/bin/urxvtma create mode 100755 archive/bin/urxvttmux create mode 100755 archive/bin/urxvttmux-prompt create mode 100755 archive/bin/weekly-backups create mode 100644 archive/bin/win32setup.bat create mode 100755 archive/bin/workstation-uninstallable create mode 100755 archive/bin/workstation-uninstallable-alt create mode 100755 archive/bin/xlaunch create mode 100755 archive/bin/yankfmailpw create mode 100755 archive/bin/yt create mode 100644 archive/texmf/bibtex/bst/spwchicago/spwchicago.bst create mode 100644 archive/texmf/tex/latex/spwdnd/spwdnd.cls create mode 100644 archive/texmf/tex/latex/spwdoc/spwdoc.cls create mode 100644 archive/texmf/tex/latex/spwessay/spwessay.cls create mode 100644 archive/texmf/tex/latex/spworg/spworg.sty create mode 100644 archive/texmf/tex/latex/spwoutline/spwoutline.cls create mode 100644 archive/texmf/tex/latex/spwpaper/spwpaper.cls create mode 100644 archive/texmf/tex/latex/spwtitle/spwtitle.sty (limited to 'archive') diff --git a/archive/bin/apple-setup.sh b/archive/bin/apple-setup.sh new file mode 100755 index 00000000..71101fc2 --- /dev/null +++ b/archive/bin/apple-setup.sh @@ -0,0 +1,103 @@ +#!/bin/sh + +USB=/Volumes/SPWHITTON + +key() +{ + local k="$1" + osascript -e "tell application \"System Events\" to keystroke \"$k\"" +} + +code() +{ + local k="$1" + osascript -e "tell application \"System Events\" to key code $k" +} + +# activate my keyboard preferences + +osascript -e 'tell application "System Preferences" to activate' +osascript -e 'tell application "System Preferences" to set current pane to pane "com.apple.preference.keyboard"' +osascript -e 'tell application "System Events" to key code 98 using {control down}' # ctrl-f7 to activate tabbing between controls +sleep 1 +code 48 +code 48 +code 48 +code 48 +code 49 +code 48 +code 48 +code 48 +code 49 +code 125 +code 125 +code 52 +code 48 +code 48 +code 49 +code 125 +code 52 +code 48 +code 49 +code 126 +code 52 +code 48 +code 48 +code 48 +code 49 +osascript -e 'tell application "System Events" to key code 98 using {control down}' +osascript -e 'tell application "System Events" to keystroke "q" using {command down}' + +# setup + +if ! [ -d "$HOME/src/dotfiles/.git" ]; then + git clone https://git.spwhitton.name/dotfiles $HOME/src/dotfiles + # this is currently out of action because GNU stow installs but + # doesn't seem to actually do anything on Mac OS + # $HOME/src/dotfiles/bin/bstraph.sh + cp $HOME/src/dotfiles/{.zshrc,.shenv} $HOME # instead +fi +pkill firefox +/Applications/Firefox.app/Contents/MacOS/firefox -private-window http://u.arizona.edu/~spwhitton/bookmarks.shtml >/dev/null 2>/dev/null & +mkdir -p $HOME/.ssh +cp $USB/lib/{id_putty,known_hosts,config} $HOME/.ssh +chmod 600 $HOME/.ssh/id_putty + +# run the shell + +cd $HOME +/bin/zsh + +# cleanup + +pkill firefox +pkill ssh # cached connection! +echo "Please wait, securely deleting files..." +rm -rfP $HOME/.ssh/{id_putty,config} $HOME/Downloads/* $HOME/.Trash/* $HOME/.zshist $HOME/.bash_history + +# reset keyboard preferences + +osascript -e 'tell application "System Events" to key code 98 using {control down}' +osascript -e 'tell application "System Preferences" to activate' +osascript -e 'tell application "System Preferences" to set current pane to pane "com.apple.preference.keyboard"' +sleep 1 +code 48 +code 48 +code 48 +code 48 +code 49 +code 48 +code 48 +code 48 +code 49 +code 48 +code 48 +code 48 +code 48 +code 49 +code 48 +code 48 +code 49 +osascript -e 'tell application "System Events" to key code 98 using {control down}' +osascript -e 'tell application "System Events" to keystroke "q" using {command down}' + diff --git a/archive/bin/backuptom3 b/archive/bin/backuptom3 new file mode 100755 index 00000000..80a0ecc2 --- /dev/null +++ b/archive/bin/backuptom3 @@ -0,0 +1,14 @@ +#!/bin/bash + +# backs up git repositories to m3 portable drive, excluding git annex + +if ! mount | grep -q "on /media/m3 type ext4"; then + echo "m3 drive not mounted" +else + for dir in /media/m3/git/*; do + cd $dir + echo \> $(basename $dir): + git pull + done + sudo /root/local/bin/duply $(hostname -s)-seven backup +fi diff --git a/archive/bin/bashmount b/archive/bin/bashmount new file mode 100755 index 00000000..d12840aa --- /dev/null +++ b/archive/bin/bashmount @@ -0,0 +1,761 @@ +#!/bin/bash + +#=============================================================================# +# FILE: bashmount # +# VERSION: 3.2.0 # +# DESCRIPTION: bashmount is a menu-driven bash script that can use different # +# backends to easily mount, unmount or eject removable devices # +# without dependencies on udisks or any GUI. An extensive # +# configuration file allows many aspects of the script to be # +# modified and custom commands to be run on devices. # +# LICENSE: GPLv2 # +# AUTHORS: Jamie Nguyen # +# Lukas B. # +#=============================================================================# + +# Copyright (C) 2013-2014 Jamie Nguyen +# Copyright (C) 2014 Lukas B. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License v2 as published by the +# Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +declare -r VERSION='3.2.0' + +if (( $# > 0 )); then + if [[ "${1}" = '-V' || "${1}" = '--version' ]]; then + cat << EOF +bashmount ${VERSION} +Copyright (C) 2013-2014 Jamie Nguyen +Copyright (C) 2014 Lukas B. +License GPLv2: GNU GPL version 2 . +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. + +Written by Jamie Nguyen and Lukas B. +EOF + exit 0 + else + printf '%s\n' 'bashmount: invalid option.' + exit 64 + fi +fi + +#-------------------------------------# +# CONFIGURATION # +#-------------------------------------# +# {{{ +# Make sure that user defined options will not interfere with grep. +unset GREP_OPTIONS + +# Set defaults. +declare udisks='auto' +declare default_mount_options='--options nosuid,noexec,noatime' +declare -i show_internal=1 +declare -i show_removable=1 +declare -i show_optical=1 +declare -i show_commands=1 +declare -i show_full_device_names=0 +declare -i colourize=1 +declare -i custom4_show=0 +declare -i custom5_show=0 +declare -i custom6_show=0 +declare -i run_post_mount=0 +declare -i run_post_unmount=0 +declare -a blacklist=() + +mount_command() { + if (( udisks == 0 )); then + read -r -e -p 'Choose the mountpoint directory: ' dir + while [[ ! -d "${dir}" ]] || findmnt "${dir}" >/dev/null 2>&1; do + error 'No such directory, or mountpoint is already in use.' + read -r -e -p 'Choose the mountpoint directory: ' dir + done + mount ${mount_options} "${1}" "${dir}" + else + udisksctl mount ${mount_options} --block-device "${1}" + fi +} + +unmount_command() { + if (( udisks == 0 )); then + umount "${1}" + else + udisksctl unmount --block-device "${1}" + fi +} + +filemanager() { + cd "${1}" && "$SHELL" + exit 0 +} + +post_mount() { + error "No command specified in 'bashmount.conf'." + return 1 +} + +post_unmount() { + error "No command specified in 'bashmount.conf'." + return 1 +} + +# Load configuration file. +declare CONFIGFILE= + +if [[ -z "${XDG_CONFIG_HOME}" ]]; then + CONFIGFILE="${HOME}/.config/bashmount/config" +else + CONFIGFILE="${XDG_CONFIG_HOME}/bashmount/config" +fi + +if [[ ! -f "${CONFIGFILE}" ]]; then + CONFIGFILE='/etc/bashmount.conf' +fi + +if [[ -f "${CONFIGFILE}" ]]; then + if ! . "${CONFIGFILE}"; then + printf '%s\n' 'bashmount: Failed to source configuration file.' + exit 78 + fi +fi + +if [[ "${udisks}" == "auto" ]]; then + type -p udisksctl >/dev/null 2>&1 && udisks=1 || udisks=0 +elif (( udisks == 1 )); then + if ! type -p udisksctl >/dev/null 2>&1; then + printf '%s\n' "bashmount: 'udisksctl': command not found" + exit 69 + fi +fi + +if ! type -p lsblk >/dev/null 2>&1; then + printf '%s\n' "bashmount: 'lsblk': command not found" + exit 69 +fi + +declare mount_options="${default_mount_options}" +# }}} + +#-------------------------------------# +# GENERAL FUNCTIONS # +#-------------------------------------# +# {{{ +unset ALL_OFF BOLD BLUE GREEN RED +if (( colourize )); then + if tput setaf 0 >/dev/null 2>&1; then + ALL_OFF="$(tput sgr0)" + BOLD="$(tput bold)" + BLUE="${BOLD}$(tput setaf 4)" + GREEN="${BOLD}$(tput setaf 2)" + RED="${BOLD}$(tput setaf 1)" + else + ALL_OFF='\e[1;0m' + BOLD='\e[1;1m' + BLUE="${BOLD}\e[1;34m" + GREEN="${BOLD}\e[1;32m" + RED="${BOLD}\e[1;31m" + fi + readonly ALL_OFF BOLD BLUE GREEN RED +fi + +msg() { + printf '%s\n' "${GREEN}==>${ALL_OFF}${BOLD} ${@}${ALL_OFF}" >&2 +} + +error() { + printf '%s\n' "${RED}==>${ALL_OFF}${BOLD} ERROR: ${@}${ALL_OFF}" >&2 +} + +print_commands() { + print_separator_commands + printf '%s' "${BLUE}e${ALL_OFF}: eject ${BLUE}i${ALL_OFF}: info" + printf '%s' " ${BLUE}m${ALL_OFF}: mount ${BLUE}o${ALL_OFF}: open" + printf '%s\n\n' " ${BLUE}u${ALL_OFF}: unmount" + printf '%s' "${BLUE}a${ALL_OFF}: unmount all" + printf '%s' " ${BLUE}r${ALL_OFF}: refresh" + printf '%s\n\n' " ${BLUE}q${ALL_OFF}: quit ${BLUE}?${ALL_OFF}: help" +} + +print_submenu_commands() { + print_separator_commands + printf '%s' "${BLUE}e${ALL_OFF}: eject ${BLUE}i${ALL_OFF}: info" + if info_mounted "${devname}"; then + printf '%s' " ${BLUE}u${ALL_OFF}: unmount" + else + printf '%s' " ${BLUE}m${ALL_OFF}: mount" + fi + printf '%s\n\n' " ${BLUE}o${ALL_OFF}: open" + printf '%s' "${BLUE}b${ALL_OFF}: back ${BLUE}r${ALL_OFF}: refresh" + printf '%s\n' " ${BLUE}q${ALL_OFF}: quit ${BLUE}?${ALL_OFF}: help" + + printf '\n' + printf '%s' "${BLUE}1${ALL_OFF}: read-only" + printf '%s' " ${BLUE}2${ALL_OFF}: luksOpen" + printf '%s' " ${BLUE}3${ALL_OFF}: luksClose" + printf '\n' + + if (( custom4_show )) || (( custom5_show )) || (( custom6_show )); then + printf '\n' + fi + + if (( custom4_show )) && [[ -n "${custom4_desc}" ]]; then + printf '%s' "${BLUE}4${ALL_OFF}: ${custom4_desc}" + fi + + if (( custom5_show )) && [[ -n "${custom5_desc}" ]]; then + printf '%s' " ${BLUE}5${ALL_OFF}: ${custom5_desc}" + fi + + if (( custom6_show )) && [[ -n "${custom6_desc}" ]]; then + printf '%s' " ${BLUE}6${ALL_OFF}: ${custom6_desc}" + fi + + if (( custom4_show )) || (( custom5_show )) || (( custom6_show )); then + printf '\n' + fi +} + +enter_to_continue() { + printf '\n' + read -r -e -p "Press [${BLUE}enter${ALL_OFF}] to continue: " null +} + +invalid_command() { + printf '\n' + error 'Invalid command. See the help menu.' + enter_to_continue +} + +print_separator() { + printf '%s\n\n' '=====================================================' +} + +print_separator_commands() { + printf '%s\n\n' '===================== COMMANDS ======================' +} + +print_separator_device() { + printf '%s\n\n' '==================== DEVICE MENU ====================' +} + +print_separator_optical() { + printf '%s\n\n' '=================== OPTICAL MEDIA ===================' +} + +print_separator_removable() { + printf '%s\n\n' '================== REMOVABLE MEDIA ==================' +} + +print_separator_internal() { + printf '%s\n\n' '================== INTERNAL MEDIA ===================' +} + +print_help() { + clear + print_commands + print_separator + printf '%s' "${GREEN}==>${ALL_OFF} " + printf '%s' "${BOLD}To mount the first device, enter ${ALL_OFF}" + printf '%s' "${BLUE}1m${ALL_OFF}" + printf '%s\n\n' "${BOLD}.${ALL_OFF}" + printf '%s' "${GREEN}==>${ALL_OFF} " + printf '%s\n\n' "${BOLD}To open the mountpath directory of the first${ALL_OFF}" + printf '%s' "${BOLD} device (mounting if required), enter " + printf '%s' "${BLUE}1o${ALL_OFF}" + printf '%s\n\n' "${BOLD}.${ALL_OFF}" + printf '%s' "${GREEN}==>${ALL_OFF} " + printf '%s' "${BOLD}To view a device sub-menu, " + printf '%s\n\n' "just enter the number.${ALL_OFF}" + printf '%s' "${GREEN}==>${ALL_OFF} " + printf '%s' "${BLUE}a${ALL_OFF}" + printf '%s' "${BOLD}, " + printf '%s' "${BLUE}r${ALL_OFF}" + printf '%s' "${BOLD}, " + printf '%s' "${BLUE}q${ALL_OFF} " + printf '%s' "${BOLD}and " + printf '%s' "${BLUE}?${ALL_OFF} " + printf '%s\n\n' "${BOLD}do not require a number.${ALL_OFF}" + print_separator + enter_to_continue +} + +print_help_sub() { + clear + print_submenu_commands + printf '\n' + print_separator + printf '%s' "${GREEN}==>${ALL_OFF} " + printf '%s\n\n' "${BOLD}To perform a command, enter a character.${ALL_OFF}" + printf '%s' "${GREEN}==>${ALL_OFF} " + printf '%s' "${BOLD}For example, to mount this device, enter ${ALL_OFF}" + printf '%s' "${BLUE}m${ALL_OFF}" + printf '%s\n\n' "${BOLD}.${ALL_OFF}" + print_separator + enter_to_continue +} + +print_device_name() { + # The padding between device location and device label. + local -i padding=22 + # For device names that are too long, this defines how many characters from + # the end of the string we will show. + local -i post_length=6 + + info_label="$(info_fslabel "${devname}")" + if [[ -z "${info_label}" ]]; then + if [[ "${1}" == 'optical' ]]; then + info_label="$(lsblk -dno MODEL "${devname}")" + else + info_label="$(info_partlabel "${devname}")" + fi + [[ -z "${info_label}" ]] && info_label='No label' + fi + + listed[device_number]="${devname}" + (( device_number++ )) + + printf '%s' " ${BLUE}${device_number})${ALL_OFF}" + devnameshort="${devname##*/}" + + if (( !show_full_device_names )) && (( ${#devnameshort} > padding )); then + pre_length=$(( padding - post_length - 3 )) + devnamepre="${devnameshort:0:pre_length}" + devnamepost="${devnameshort:${#devnameshort}-post_length}" + devnameshort="${devnamepre}...${devnamepost}" + fi + + printf '%s' " ${devnameshort}:" + + # Add padding between device location and device label. + devname_length="${#devnameshort}" + for (( i=padding ; i>devname_length ; i-- )); do + printf '%s' " " + done + + printf '%s' " ${info_label}" + if info_mounted "${devname}"; then + printf '%s' " ${GREEN}[mounted]${ALL_OFF}" + mounted[${#mounted[*]}]="${devname}" + fi + printf '\n' +} + +# }}} + +#-------------------------------------# +# INFORMATION RETRIEVAL # +#-------------------------------------# +# {{{ +# Returns 0 if the device is registered as removable device in the kernel, +# otherwise it returns 1. +info_removable() { + [[ "$(lsblk -drno RM "${1}")" == '1' ]] +} + +# Prints the device type, for example partition or disk. +info_type() { + lsblk -drno TYPE "${1}" +} + +# Prints the filesystem label, if present. +info_fslabel() { + lsblk -drno LABEL "${1}" +} + +# Prints the partition label, if present. +info_partlabel() { + lsblk -drno PARTLABEL "${1}" +} + +# Prints the mountpath, if mounted. +info_mountpath() { + findmnt -no TARGET "${1}" +} + +# Returns 0 if the device is mounted, 1 otherwise. +info_mounted() { + findmnt -no TARGET "${1}" >/dev/null 2>&1 +} + +# Prints the filesystem type. +info_fstype() { + lsblk -drno FSTYPE "${1}" +} + +# Prints the device size. +info_size() { + lsblk -drno SIZE "${1}" +} +# }}} + +#-------------------------------------# +# DEVICE MANIPULATION # +#-------------------------------------# +# {{{ +check_device() { + if [[ ! -b "${1}" ]]; then + printf '\n' + error "${1} is no longer available." + enter_to_continue + return 1 + fi + return 0 +} + +action_eject() { + check_device "${1}" || return 1 + info_mounted "${1}" && action_unmount "${1}" + if ! info_mounted "${1}"; then + printf '\n' + msg "Ejecting ${1} ..." + printf '\n' + eject "${1}" + # Give the device some time to eject. If we don't then sometimes the ejected + # device will still be present when returning to the main menu. + enter_to_continue + sleep 2 + fi +} + +action_info() { + check_device "${1}" || return 1 + lsblk -o NAME,FSTYPE,MOUNTPOINT,SIZE "${1}" | less +} + +action_mount() { + check_device "${1}" || return 1 + printf '\n' + if info_mounted "${1}"; then + error "${1} is already mounted." + else + msg "Mounting ${1} ..." + if mount_command "${1}"; then + msg "${1} mounted succesfullly." + (( run_post_mount )) && post_mount "${1}" + else + printf '\n' + error "${1} could not be mounted." + fi + fi + enter_to_continue +} + +action_open() { + if ! info_mounted "${1}"; then + printf '\n' + msg "Mounting ${1} ..." + if mount_command "${1}"; then + msg "${1} mounted succesfullly." + (( run_post_mount )) && post_mount "${1}" + else + printf '\n' + error "${1} could not be mounted." + enter_to_continue + return 1 + fi + fi + printf '\n' + msg "Opening ${1} ..." + printf '\n' + filemanager "$(info_mountpath "${1}")" + enter_to_continue +} + +action_unmount() { + printf '\n' + if info_mounted "${1}"; then + msg "Unmounting ${1} ..." + printf '\n' + if unmount_command "${1}"; then + msg "${1} unmounted successfully." + (( run_post_unmount )) && post_unmount "${1}" + else + printf '\n' + error "${1} could not be unmounted." + fi + else + error "${1} is not mounted." + fi + enter_to_continue +} +# }}} + +#-------------------------------------# +# MENU FUNCTIONS # +#-------------------------------------# +# {{{ +list_devices() { + local -a all=() removable=() internal=() optical=() + # The array "all" contains the sorted list of devices returned by lsblk. + all=( $(lsblk -plno NAME) ) + # The array "listed" contains all devices that are shown to the user. + listed=() + # The array "mounted" contains all devices that are listed and mounted. + mounted=() + # "device_number" is the total number of devices listed and equals ${#listed[*]}. + device_number=0 + + for devname in ${all[@]}; do + local info_type= + # Hide blacklisted devices. + for string in ${blacklist[@]}; do + lsblk -dPno NAME,TYPE,FSTYPE,LABEL,MOUNTPOINT,PARTLABEL "${devname}" \ + | grep -E "${string}" >/dev/null 2>&1 + (( $? )) || continue 2 + done + info_type=$(info_type "${devname}") + # Sort devices into arrays removable, internal, and optical. + if [[ "${info_type}" == 'part' || "${info_type}" == 'crypt' ]]; then + if info_removable "${devname}"; then + removable[${#removable[*]}]="${devname}" + else + internal[${#internal[*]}]="${devname}" + fi + # Normally we don't want to see a 'disk', but if it has no partitions + # (eg, internal storage on some portable media devices) then it should + # be visible. + elif [[ "${info_type}" == 'disk' ]]; then + [[ "${all[@]}" =~ ${devname}1 ]] && continue + if info_removable "${devname}"; then + removable[${#removable[*]}]="${devname}" + else + internal[${#internal[*]}]="${devname}" + fi + elif [[ "${info_type}" == 'rom' ]]; then + optical[${#optical[*]}]="${devname}" + else + continue + fi + done + # Print output. + # List internal media. + if (( show_internal )) && (( ${#internal[*]} )); then + print_separator_internal + for devname in ${internal[@]}; do + print_device_name + done + printf '\n' + fi + # List removable media. + if (( show_removable )) && (( ${#removable[*]} )); then + print_separator_removable + for devname in ${removable[@]}; do + print_device_name + done + printf '\n' + fi + # List optical media. + if (( show_optical )) && (( ${#optical[*]} )); then + print_separator_optical + for devname in ${optical[@]}; do + print_device_name optical + done + printf '\n' + fi + (( device_number )) || printf '%s\n' 'No devices.' +} + +submenu() { + check_device "${devname}" || return 1 + local info_label= info_fstype= info_size= + info_label="$(info_fslabel "${devname}")" + if [[ -z "${info_label}" ]]; then + info_label="$(info_partlabel "${devname}")" + if [[ -z "${info_label}" ]]; then + info_label='-' + fi + fi + info_fstype="$(info_fstype "${devname}")" + info_size="$(info_size "${devname}")" + clear + print_separator_device + printf '%s\n' "device : ${devname}" + printf '%s\n' "label : ${info_label}" + printf '%s' 'mounted : ' + if info_mounted "${devname}"; then + printf '%s\n' "${GREEN}yes${ALL_OFF}" + printf '%s\n' "mountpath : $(info_mountpath "${devname}")" + else + printf '%s\n' "${RED}no${ALL_OFF}" + fi + printf '%s\n' "fstype : ${info_fstype}" + printf '%s\n' "size : ${info_size}" + if (( show_commands )); then + printf '\n' + print_submenu_commands + fi + printf '\n' + print_separator + read -r -e -p 'Command: ' action + case "${action}" in + 'e') action_eject "${devname}";; + 'i') action_info "${devname}";; + 'm') action_mount "${devname}";; + 'o') action_open "${devname}";; + 'u') action_unmount "${devname}";; + 'b') return 1;; + 'r') return 0;; + 'q') exit;; + '?') + print_help_sub + return 0;; + '1') + printf '\n' + msg 'Mounting read-only ...' + printf '\n' + mount_options="${default_mount_options}"' --read-only' + mount_command "${devname}" + mount_options="${default_mount_options}" + enter_to_continue + return 0;; + '2') + printf '\n' + msg 'Opening luks volume ...' + printf '\n' + if (( udisks == 0 )); then + cryptsetup open --type luks -v "${devname}" "luks-${devname##*/}" + else + udisksctl unlock --block-device "${devname}" + fi + enter_to_continue + return 0;; + '3') + printf '\n' + msg 'Closing luks volume ...' + printf '\n' + if (( udisks == 0 )); then + cryptsetup close --type luks "${devname}" + else + udisksctl lock --block-device "${devname}" + fi + enter_to_continue + return 0;; + '4') + if (( custom4_show )); then + printf '\n' + msg "Running custom command ${custom4_desc} ..." + printf '\n' + custom4_command "${devname}" + enter_to_continue + else + invalid_command + fi + return 0;; + '5') + if (( custom5_show )); then + printf '\n' + msg "Running custom command ${custom5_desc} ..." + printf '\n' + custom5_command "${devname}" + enter_to_continue + else + invalid_command + fi + return 0;; + '6') + if (( custom6_show )); then + printf '\n' + msg "Running custom command ${custom6_desc} ..." + printf '\n' + custom6_command "${devname}" + enter_to_continue + else + invalid_command + fi + return 0;; + *) invalid_command + return 0;; + esac +} + +select_action() { + local devname= letter= + local -i number= + print_separator + read -r -e -p 'Command: ' action + if [[ "${action}" =~ ^[1-9] ]]; then + if [[ "${action}" =~ ^[1-9][0-9]*$ ]]; then + number="$(( action - 1 ))" + if (( number >= device_number )); then + invalid_command + return 1 + fi + devname=${listed[number]} + while :; do + submenu || break + done + elif [[ "${action}" =~ ^[1-9][0-9]*[eimou]$ ]]; then + number="$(( ${action%?} - 1 ))" + letter="${action: -1}" + if (( number >= device_number )); then + invalid_command + return 1 + fi + devname="${listed[number]}" + case "${letter}" in + 'e') action_eject "${devname}";; + 'i') action_info "${devname}";; + 'm') action_mount "${devname}";; + 'o') action_open "${devname}";; + 'u') action_unmount "${devname}";; + *) return 1;; + esac + return 0 + else + invalid_command + return 1 + fi + else + case "${action}" in + 'a') + printf '\n' + if (( ! ${#mounted[*]} )); then + error 'No devices mounted.' + enter_to_continue + return 1 + fi + read -r -e -p 'Unmount all devices [y/N]?: ' unmount + if [[ "${unmount}" != 'y' ]] && [[ "${unmount}" != 'Y' ]]; then + return 0 + fi + clear + for devname in ${mounted[@]}; do + action_unmount "${devname}" || continue + done + enter_to_contine + return 1;; + 'r'|"") + return 0;; + 'q'|'b') + exit 0;; + '?') + print_help + return 0;; + *) + invalid_command + return 1;; + esac + fi +} +# }}} + +declare -i device_number= +declare -a mounted=() +declare -a listed=() + +while true; do + clear + list_devices + (( show_commands )) && print_commands + select_action +done diff --git a/archive/bin/bitlbee_startup b/archive/bin/bitlbee_startup new file mode 100755 index 00000000..aaabbf7e --- /dev/null +++ b/archive/bin/bitlbee_startup @@ -0,0 +1,8 @@ +#!/bin/bash + +. $HOME/.shenv + +unset PERL5LIB PKG_CONFIG_PATH LD_LIBRARY_PATH C_INCLUDE_PATH MODULEBUILDRC MODULEPATH MODULESHOME PERL_MM_OPT +export PERL5LIB PKG_CONFIG_PATH LD_LIBRARY_PATH C_INCLUDE_PATH MODULEBUILDRC MODULEPATH MODULESHOME PERL_MM_OPT + +junest /usr/sbin/bitlbee -n >/dev/null 2>/dev/null & diff --git a/archive/bin/build b/archive/bin/build new file mode 100755 index 00000000..40669696 --- /dev/null +++ b/archive/bin/build @@ -0,0 +1,144 @@ +#!/bin/sh + + + +# Rewrite of this script is a WIP because I think I can get what I +# want by just running debuild instead of dgit/gbp when I want to +# ignore uncommitted changes. dpkg-buildpackage should unapply the +# patches if it applied them. The only problem is if I applied some +# with quilt. However, the only time I actually *need* to do that is +# when refreshing patches; otherwise I should use gbp-pq, and then +# test with `debuild -b`. After refreshing patches I can delete the +# .pc directory and it should all work. + + + + +# Copyright (C) 2016 Sean Whitton + +# Released under the GPL version 3 + +# Description: + +# The purpose of this script is to give the right magic invocations to +# build a Debian package, abstracting from the differences between +# +# - patches-applied repos intended to be built with dgit +# - patches-unapplied repos intended to be built with gbp +# - plain source packages not kept in git +# +# We do not consider patches-unapplied dgit repos (unreleased dgit +# functionality) and patches-applied gbp repos (not popular, and +# depends on d/source/local-options, which is to be discouraged as it +# prevents dgit adoption). The former might make this script +# obsolete. +# +# The following options are accepted, provided they come before any +# other options, which are passed on to whatever we invoked to build +# the package. +# +# - --ignore -- bypass checks for a dirty git working tree +# - -b -- do a binary-only build (no .dsc) +# - -S -- do a source-only build (no .deb) +# - --sbuild -- use sbuild +# +# I recommend the following shell aliases: +# +# alias buildi="build --ignore" +# alias buildib="build --ignore -b" +# alias sbuild="build --sbuild" +# alias sbuildi="build --ignore --sbuild" + +# Process arguments. We use -nc because we want to run the clean +# ourselves before letting gbp or dgit check for uncommitted changes +while "$1" in "--ignore --binary -S --sbuild"; do + case "$1" in + --ignore) + ignore=yes + ;; + -b) + buildtype="-nc -b" + ;; + -S) + buildtype="-nc -S" + ;; + --sbuild) + sbuild=yes + ;; + esac + shift +done + +# default to a full build (mainly for full Lintian output) +if [ "$buildtype" = "" ]; then + buildtype="-nc -F" +fi + +# We try to run a clean unless this is an --ignore build. Basically, +# --ignore builds are for iteratively hacking on stuff, but once we +# don't pass --ignore, we want all the safety checks to fire +if [ ! "$ignore" = "yes" ]; then + fakeroot debian/rules clean +fi + +if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + if git branch -a | grep -q "remotes/dgit"; then + # ---- build with dgit + + if [ "$ignore" = "yes" ]; then + arg="--ignore-dirty" + else + arg="" + fi + if [ "$sbuild" = "yes" ]; then + arg="$arg sbuild" + elif [ "$"] # TODO continue processing args + + # safety check before git clean if we're not ignoring + if [ ! "$ignore" = "yes" ]; then + if git status --porcelain 2>/dev/null | grep -q "^??"; then + echo >$2 "abort: untracked files present (to override, try \`buildi')" + exit 1 + fi + fi + dgit $arg "$@" + lintian + else + # ---- (assume) build with gbp + ( + # gbp must be invoked from the root of the repository + cd "$(git rev-parse --show-toplevel)" + + if [ -d ".pc" ]; then + quilt pop -a + fi + # handle ignore + if [ "$ignore" = "yes" ]; then + arg="--git-ignore-new" + else + arg="" + fi + fakeroot debian/rules clean + # safety check before git clean if we're not ignoring + if [ ! "$ignore" = "yes" ]; then + if git status --porcelain 2>/dev/null | grep -q "^??"; then + echo >&2 "abort: untracked files present (to override, try \`buildi')" + exit 1 + fi + fi + git clean -xfd --exclude="debian/patches/*" # newly created patches + gbp buildpackage -F -nc -us -uc --source-options=--unapply-patches $arg "$@" + ) + fi +else + # ---- build with debuild + debuild $buildtype -us -uc +fi + +# do a post-clean if we did a pre-clean +if [ ! "$ignore" = "yes" ]; then + fakeroot debian/rules clean +fi + +# TODO should we be doing so many cleans? There is probably a better way. +# TODO similarly, we are popping and reapplying all the patches over and over. diff --git a/archive/bin/build_rpi_sd_card.sh b/archive/bin/build_rpi_sd_card.sh new file mode 100755 index 00000000..a8a90bbc --- /dev/null +++ b/archive/bin/build_rpi_sd_card.sh @@ -0,0 +1,195 @@ +#!/bin/bash + +# build your own Raspberry Pi SD card +# +# by Klaus M Pfeiffer, http://blog.kmp.or.at/ , 2012-06-24 + +# 2012-06-24 +# just checking for how partitions are called on the system (thanks to Ricky Birtles and Luke Wilkinson) +# using http.debian.net as debian mirror, see http://rgeissert.blogspot.co.at/2012/06/introducing-httpdebiannet-debians.html +# tested successfully in debian squeeze and wheezy VirtualBox +# added hint for lvm2 +# added debconf-set-selections for kezboard +# corrected bug in writing to etc/modules +# 2012-06-16 +# improoved handling of local debian mirror +# added hint for dosfstools (thanks to Mike) +# added vchiq & snd_bcm2835 to /etc/modules (thanks to Tony Jones) +# take the value fdisk suggests for the boot partition to start (thanks to Mike) +# 2012-06-02 +# improoved to directly generate an image file with the help of kpartx +# added deb_local_mirror for generating images with correct sources.list +# 2012-05-27 +# workaround for https://github.com/Hexxeh/rpi-update/issues/4 just touching /boot/start.elf before running rpi-update +# 2012-05-20 +# back to wheezy, http://bugs.debian.org/672851 solved, http://packages.qa.debian.org/i/ifupdown/news/20120519T163909Z.html +# 2012-05-19 +# stage3: remove eth* from /lib/udev/rules.d/75-persistent-net-generator.rules +# initial + +# you need at least +# apt-get install binfmt-support qemu qemu-user-static debootstrap kpartx lvm2 dosfstools + +deb_mirror="http://http.debian.net/debian" +#deb_local_mirror="http://debian.kmp.or.at:3142/debian" + +bootsize="64M" +deb_release="wheezy" + +device=$1 +buildenv="/root/rpi" +rootfs="${buildenv}/rootfs" +bootfs="${rootfs}/boot" + +mydate=`date +%Y%m%d` + +if [ "$deb_local_mirror" == "" ]; then + deb_local_mirror=$deb_mirror +fi + +image="" + + +if [ $EUID -ne 0 ]; then + echo "this tool must be run as root" + exit 1 +fi + +if ! [ -b $device ]; then + echo "$device is not a block device" + exit 1 +fi + +if [ "$device" == "" ]; then + echo "no block device given, just creating an image" + mkdir -p $buildenv + image="${buildenv}/rpi_basic_${deb_release}_${mydate}.img" + dd if=/dev/zero of=$image bs=1MB count=1000 + device=`losetup -f --show $image` + echo "image $image created and mounted as $device" +else + dd if=/dev/zero of=$device bs=512 count=1 +fi + +fdisk $device << EOF +n +p +1 + ++$bootsize +t +c +n +p +2 + + +w +EOF + + +if [ "$image" != "" ]; then + losetup -d $device + device=`kpartx -va $image | sed -E 's/.*(loop[0-9])p.*/\1/g' | head -1` + device="/dev/mapper/${device}" + bootp=${device}p1 + rootp=${device}p2 +else + if ! [ -b ${device}1 ]; then + bootp=${device}p1 + rootp=${device}p2 + if ! [ -b ${bootp} ]; then + echo "uh, oh, something went wrong, can't find bootpartition neither as ${device}1 nor as ${device}p1, exiting." + exit 1 + fi + else + bootp=${device}1 + rootp=${device}2 + fi +fi + +mkfs.vfat $bootp +mkfs.ext4 $rootp + +mkdir -p $rootfs + +mount $rootp $rootfs + +cd $rootfs + +debootstrap --foreign --arch armel $deb_release $rootfs $deb_local_mirror +cp /usr/bin/qemu-arm-static usr/bin/ +LANG=C chroot $rootfs /debootstrap/debootstrap --second-stage + +mount $bootp $bootfs + +echo "deb $deb_local_mirror $deb_release main contrib non-free +" > etc/apt/sources.list + +echo "dwc_otg.lpm_enable=0 console=ttyAMA0,115200 kgdboc=ttyAMA0,115200 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 rootwait" > boot/cmdline.txt + +echo "proc /proc proc defaults 0 0 +/dev/mmcblk0p1 /boot vfat defaults 0 0 +" > etc/fstab + +echo "raspberrypi" > etc/hostname + +echo "auto lo +iface lo inet loopback + +auto eth0 +iface eth0 inet dhcp +" > etc/network/interfaces + +echo "vchiq +snd_bcm2835 +" >> etc/modules + +echo "console-common console-data/keymap/policy select Select keymap from full list +console-common console-data/keymap/full select de-latin1-nodeadkeys +" > debconf.set + +echo "#!/bin/bash +debconf-set-selections /debconf.set +rm -f /debconf.set +apt-get update +apt-get -y install git-core binutils ca-certificates +wget http://goo.gl/1BOfJ -O /usr/bin/rpi-update +chmod +x /usr/bin/rpi-update +mkdir -p /lib/modules/3.1.9+ +touch /boot/start.elf +rpi-update +apt-get -y install locales console-common ntp openssh-server less vim +echo \"root:raspberry\" | chpasswd +sed -i -e 's/KERNEL\!=\"eth\*|/KERNEL\!=\"/' /lib/udev/rules.d/75-persistent-net-generator.rules +rm -f /etc/udev/rules.d/70-persistent-net.rules +rm -f third-stage +" > third-stage +chmod +x third-stage +LANG=C chroot $rootfs /third-stage + +echo "deb $deb_mirror $deb_release main contrib non-free +" > etc/apt/sources.list + +echo "#!/bin/bash +aptitude update +aptitude clean +apt-get clean +rm -f cleanup +" > cleanup +chmod +x cleanup +LANG=C chroot $rootfs /cleanup + +cd + +umount $bootp +umount $rootp + +if [ "$image" != "" ]; then + kpartx -d $image + echo "created image $image" +fi + + +echo "done." + diff --git a/archive/bin/cabal-install-exec b/archive/bin/cabal-install-exec new file mode 100755 index 00000000..fa4e2716 --- /dev/null +++ b/archive/bin/cabal-install-exec @@ -0,0 +1,16 @@ +#!/bin/sh + +cabal --no-require-sandbox update +for var in "$@"; do + if [ -d "$HOME/local/src/${var}" ]; then + echo >&2 "$(basename $0): a version of $var is already installed; skipping" + else + mkdir -p $HOME/local/src/${var} + cd $HOME/local/src/${var} + touch .duplicity-ignore + cabal sandbox init + wget https://www.stackage.org/lts/cabal.config + cabal install $var --force-reinstalls + cabal-link-bins + fi +done diff --git a/archive/bin/cabal-link-bins b/archive/bin/cabal-link-bins new file mode 100755 index 00000000..15a5af5a --- /dev/null +++ b/archive/bin/cabal-link-bins @@ -0,0 +1,9 @@ +#!/bin/sh + +if ! [ -d .cabal-sandbox/bin ]; then + echo >&2 "$(basename $0): looks like you don't have a cabal sandbox yet" + exit 1 +fi + +mkdir -p $HOME/local/bin +ln -rfs -t $HOME/local/bin .cabal-sandbox/bin/* diff --git a/archive/bin/caffeinate-zenity b/archive/bin/caffeinate-zenity new file mode 100755 index 00000000..cbeec673 --- /dev/null +++ b/archive/bin/caffeinate-zenity @@ -0,0 +1,3 @@ +#!/bin/sh + +WINDOWID= caffeinate zenity --info --text="Press okay to resume screensaver and/or autosuspend" diff --git a/archive/bin/capture-mail b/archive/bin/capture-mail new file mode 100755 index 00000000..a34b894e --- /dev/null +++ b/archive/bin/capture-mail @@ -0,0 +1,13 @@ +#!/bin/zsh + +msg=$(cat /dev/stdin) + +id=$(echo $msg | grep -i "^Message-ID:" | sed "s/^Message-I[dD]: //") +from=$(echo $msg | grep -m 1 -i "^From:" | sed "s/^From: //" | sed "s/<.*$//" | sed 's/"//g' | sed 's/ $//') +subject=$(echo $msg | grep -m 1 -i "^Subject:" | sed "s/^Subject: //") + +save-org-buffers + +echo "* TODO E-mail \"$subject\" from $from" >>$HOME/doc/org/refile.org +echo "# Message-Id: $id" >>$HOME/doc/org/refile.org +#emacsclient -t -e '(spw/end-of-refile)' diff --git a/archive/bin/clean-github-pr.py b/archive/bin/clean-github-pr.py new file mode 100755 index 00000000..34834958 --- /dev/null +++ b/archive/bin/clean-github-pr.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python + +# clean-github-pr --- Create tidy repositories for pull requests +# +# Copyright (C) 2016 Sean Whitton +# +# clean-github-pr 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. +# +# clean-github-pr 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 clean-github-pr. If not, see . + +import github + +import sys +import time +import tempfile +import shutil +import subprocess +import os + +CREDS_FILE = os.getenv("HOME") + "/.cache/clean-github-pr-creds" + +def main(): + # check arguments + if len(sys.argv) != 2: + print sys.argv[0] + ": usage: " + sys.argv[0] + " USER/REPO" + sys.exit(1) + + # check creds file + try: + f = open(CREDS_FILE, 'r') + except IOError: + print sys.argv[0] + ": please put your github username and password, separated by a colon, in the file ~/.cache/clean-github-pr-creds" + sys.exit(1) + + # just to be sure + os.chmod(CREDS_FILE, 0600) + + # make the fork + creds = f.readline() + username = creds.split(":")[0] + pword = creds.split(":")[1].strip() + token = f.readline().strip() + + if len(token) != 0: + g = github.Github(token) + else: + g = github.Github(username, pword) + + u = g.get_user() + + source = sys.argv[1] + if '/' in source: + fork = sys.argv[1].split("/")[1] + print "forking repo " + source + u.create_fork(g.get_repo(source)) + else: + fork = sys.argv[1] + + while True: + try: + r = u.get_repo(fork) + except github.UnknownObjectException: + print "still waiting" + time.sleep(5) + else: + break + + # set up & push github branch + user_work_dir = os.getcwd() + work_area = tempfile.mkdtemp() + os.chdir(work_area) + subprocess.call(["git", "clone", "https://github.com/" + username + "/" + fork]) + os.chdir(work_area + "/" + fork) + subprocess.call(["git", "checkout", "--orphan", "github"]) + subprocess.call(["git", "rm", "-rf", "."]) + with open("README.md", 'w') as f: + f.write("This repository is just a fork made in order to submit a pull request; please ignore.") + subprocess.call(["git", "add", "README.md"]) + subprocess.call(["git", "commit", "-m", "fork for a pull request; please ignore"]) + subprocess.call(["git", "push", "origin", "+github"]) + os.chdir(user_work_dir) + shutil.rmtree(work_area) + + # make sure the branch has been pushed + time.sleep(5) + + # set clean repository settings + r.edit(fork, + has_wiki=False, + description="Fork for a pull request; please ignore", + homepage="", + has_issues=False, + has_downloads=False, + default_branch="github") + +if __name__ == "__main__": + main() diff --git a/archive/bin/coldbkup b/archive/bin/coldbkup new file mode 100755 index 00000000..a98b2936 --- /dev/null +++ b/archive/bin/coldbkup @@ -0,0 +1,131 @@ +#!/bin/bash + +# backup to removable, offline media + +. $HOME/.shenv +. $HOME/lib/tputfs.sh + +set -e + +# TODO syncing annexes should be more systematic. This first part of +# the script is the worst. Detect if a git repo is an annex, and do +# some sensible backup sync. So that, like with regular git repos, +# coldbkup can also set up a new backup drive without me having to do +# a lot of annex setting up (maybe a function: gitannexbk, or just +# auto-detect (better)). We can init the annex and put it in the +# archive or backup group so we get a sensible default preferred +# content expression (and need to git remote add in $HOME) + +# determine removable media available and if it's m3, sync annex content +# TODO do this by looking at size of removable media? +if mount | grep -q "/media/${USER}/m3"; then + HDD=/media/${USER}/m3 + status syncing annex content + # we sync both ma and m3 here so that ma knows everything that got + # put onto m3 + cd $HOME/lib/annex + git annex sync --content origin m3 + cd $HOME/lib/wikiannex + git annex sync --content origin athena m3 + # TODO + # cd $HOME/lib/dionysus + # git annex sync --content m3 + cd $HOME +elif mount | grep -q "/media/${USER}/bkupsd"; then + HDD=/media/${USER}/bkupsd + + status syncing wikiannex content + cd $HOME/lib/wikiannex + git annex sync --content origin athena bkupsd + + status syncing dionysus annex content + cd $HOME/lib/dionysus + git annex sync --content # origin athena bkupsd + cd $HOME +else + echo "coldbkup: removable media not mounted" >&2 + exit 1 +fi +DEST=${HDD}/git +mkdir -p $DEST + +# function to backup a repo: first arg is ssh path to repo, second arg +# is where to put it +gitbk () +{ + local long=$1 + local short=$(basename $long) + local dest=$2 + if [ -e "$dest/$short" ]; then + cd $dest/$short + git fetch origin "+refs/heads/*:refs/heads/*" --prune --tags + else + mkdir -p $dest + cd $dest + git clone --mirror $long $short + fi +} + +# backup a repo from github +githubbk () +{ + status processing $1 from github + gitbk git@github.com:$1 $DEST/github +} + +# backup a repo from alioth +aliothbk () +{ + status processing $1 from alioth + gitbk alioth:/git/$1 $DEST/alioth +} + +# Stage 1 : Backup repos hosted on athena + +# TODO: don't use ls here (see http://mywiki.wooledge.org/ParsingLs) + +repos=$(ssh athena ls /home/git) +for repo in $repos; do + status processing $repo from athena + # TODO look in git-remote-gcrypt to find how it detects a gcrypt + # repo; there must be a way + if [ "$repo" = "priv.git" -o "$repo" = "annex.git" -o "$repo" = "rt.git" ]; then + # might need a ssh:// and a /~/ in here to work with gcrypt + gitbk gcrypt::git@spwhitton.name:/home/git/$repo $DEST/athena + else + gitbk git@spwhitton.name:/home/git/$repo $DEST/athena + fi +done + +# Stage 2 : Debian repos + +aliothbk pkg-emacsen/pkg/aggressive-indent-mode.git +aliothbk pkg-emacsen/pkg/f-el.git +aliothbk pkg-emacsen/pkg/emacs-async.git +aliothbk pkg-emacsen/pkg/emacs-noflet.git +aliothbk pkg-emacsen/pkg/perspective-el.git +aliothbk pkg-emacsen/pkg/helm.git +aliothbk pkg-emacsen/pkg/epl.git +aliothbk pkg-emacsen/pkg/pkg-info-el.git +aliothbk pkg-emacsen/pkg/flx.git +aliothbk pkg-emacsen/pkg/projectile.git +aliothbk pkg-emacsen/pkg/let-alist.git +aliothbk pkg-emacsen/pkg/seq-el.git +aliothbk pkg-emacsen/pkg/shut-up.git +aliothbk pkg-emacsen/pkg/popup-el.git +aliothbk pkg-emacsen/pkg/paredit-el.git +aliothbk pkg-mozext/ublock-origin.git +aliothbk pkg-mozext/y-u-no-validate.git +aliothbk pkg-mozext/classic-theme-restorer.git +aliothbk pkg-emacsen/pkg/flycheck.git +aliothbk pkg-mozext/keysnail.git + +# Stage 3 : Starred repos on github + +# TODO Commented as downloading so many forks will be slow. Maybe run +# it on athena instead? + +# ( +# cd $DEST/github +# github-backup spwhitton +# ) diff --git a/archive/bin/ctrlnocaps.ahk b/archive/bin/ctrlnocaps.ahk new file mode 100644 index 00000000..0651fa49 --- /dev/null +++ b/archive/bin/ctrlnocaps.ahk @@ -0,0 +1 @@ +Capslock::Ctrl \ No newline at end of file diff --git a/archive/bin/ctrlswapcaps-nonuk.ahk b/archive/bin/ctrlswapcaps-nonuk.ahk new file mode 100644 index 00000000..b4927523 --- /dev/null +++ b/archive/bin/ctrlswapcaps-nonuk.ahk @@ -0,0 +1,37 @@ +; original source: http://lifehacker.com/5468862/create-a-shortcut-key-for-restoring-a-specific-window +; but I've added TheExe parameter +ToggleWinMinimize(TheWindowTitle, TheExe) +{ + SetTitleMatchMode,2 + DetectHiddenWindows, Off + IfWinActive, %TheWindowTitle% + { + WinMinimize, %TheWindowTitle% + } + Else + { + IfWinExist, %TheWindowTitle% + { + WinGet, winid, ID, %TheWindowTitle% + DllCall("SwitchToThisWindow", "UInt", winid, "UInt", 1) + } + Else + { + Run, %TheExe% + } + } + Return +} + +F11::Send !{F4} +F12::ToggleWinMinimize("Mozilla Firefox", "Firefox") + +; for Emacs + +Capslock::Ctrl +LCtrl::Capslock + +; some British keyboard layout conventions + +@::" +"::@ diff --git a/archive/bin/ctrlswapcaps.ahk b/archive/bin/ctrlswapcaps.ahk new file mode 100644 index 00000000..16fd4416 --- /dev/null +++ b/archive/bin/ctrlswapcaps.ahk @@ -0,0 +1,30 @@ +; original source: http://lifehacker.com/5468862/create-a-shortcut-key-for-restoring-a-specific-window +; but I've added TheExe parameter +ToggleWinMinimize(TheWindowTitle, TheExe) +{ + SetTitleMatchMode,2 + DetectHiddenWindows, Off + IfWinActive, %TheWindowTitle% + { + WinMinimize, %TheWindowTitle% + } + Else + { + IfWinExist, %TheWindowTitle% + { + WinGet, winid, ID, %TheWindowTitle% + DllCall("SwitchToThisWindow", "UInt", winid, "UInt", 1) + } + Else + { + Run, %TheExe% + } + } + Return +} + +F11::Send !{F4} +F12::ToggleWinMinimize("Mozilla Firefox", "Firefox") + +Capslock::Ctrl +LCtrl::Capslock diff --git a/archive/bin/dasl-setup.bat b/archive/bin/dasl-setup.bat new file mode 100755 index 00000000..afe02117 --- /dev/null +++ b/archive/bin/dasl-setup.bat @@ -0,0 +1,7 @@ +@echo off +mkdir C:\%HOMEPATH%\SPW +copy /y ..\lib\putty.exe.reg C:\%HOMEPATH%\SPW +copy /y ..\lib\putty.exe-empty.reg C:\%HOMEPATH%\SPW +copy /y ..\lib\spwhitton@putty.ppk C:\%HOMEPATH%\SPW +copy /y ..\bin\ctrlswapcaps.exe C:\%HOMEPATH%\SPW +explorer "C:\%HOMEPATH%\SPW" diff --git a/archive/bin/develacc b/archive/bin/develacc new file mode 100755 index 00000000..32e71367 --- /dev/null +++ b/archive/bin/develacc @@ -0,0 +1,10 @@ +#!/bin/sh + +# TODO use `sudo machinectl shell spw@develacc $shell` instead because +# it sets up the environment better afaict +# shell=$(sudo enter-develacc "getent passwd spw | cut -d: -f5") +# (^ enter-devleacc probably still best for non-interactive usage) + +# perhaps just a shell alias + +sudo $HOME/bin/develacc-inner diff --git a/archive/bin/develacc-inner b/archive/bin/develacc-inner new file mode 100755 index 00000000..cb094e4b --- /dev/null +++ b/archive/bin/develacc-inner @@ -0,0 +1,33 @@ +#!/usr/bin/perl + +# Config + +my $machine = "develacc"; +my $user = "spw"; + +# Code, based on enter-foo script from Propellor's systemd-nspawn +# support + +# default command: calling user's login shell +push @ARGV, $ENV{'SHELL'} unless (@ARGV); + +# get args +my $pid=`machinectl show $machine -p Leader | cut -d= -f2`; +chomp $pid; +my $home=`echo ~$user`; +chomp $home; +my $uid=`stat --printf="%u" $home`; +chomp $uid; +my $gid=`stat --printf="%g" $home`; +chomp $gid; + +# nsenter time +if (length $pid) { + foreach my $var (keys %ENV) { + delete $ENV{$var} unless $var eq 'PATH' || $var eq 'TERM'; + } + exec('nsenter', '-S', $uid, '-G', $gid, "--wd=$home", '-p', '-u', '-n', '-i', '-m', '-t', $pid, @ARGV); +} else { + die 'container not running'; +} +exit(1); diff --git a/archive/bin/develacc-push b/archive/bin/develacc-push new file mode 100755 index 00000000..7c9b5f1f --- /dev/null +++ b/archive/bin/develacc-push @@ -0,0 +1,22 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use lib "$ENV{HOME}/lib/perl5"; + +use ScriptStatus; +use Term::UI; + +my $term = Term::ReadLine->new('brand'); + +@ARGV or die "tell me which repo to back up"; +my $repo = shift @ARGV; +chdir "/home/spw/src/$repo" or die "repo does not exist"; +system "cat .git/config | grep \"url =\""; +system "cat .git/config | grep \"insteadOf\""; +system "cat .git/config | grep \"pushInsteadOf\""; +exit unless $term->ask_yn( + prompt => "Intend to push to these URIs?", + default => 'n', + ); +system "git push --no-verify @ARGV"; diff --git a/archive/bin/develacc-push-all b/archive/bin/develacc-push-all new file mode 100755 index 00000000..2d1dbd19 --- /dev/null +++ b/archive/bin/develacc-push-all @@ -0,0 +1,22 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use lib "$ENV{HOME}/lib/perl5"; + +use ScriptStatus; +use Term::UI; + +my $term = Term::ReadLine->new('brand'); + +@ARGV or die "tell me which repo to back up"; +my $repo = shift @ARGV; +chdir "/home/spw/src/$repo" or die "repo does not exist"; +system "cat .git/config | grep \"url =\""; +system "cat .git/config | grep \"insteadOf\""; +system "cat .git/config | grep \"pushInsteadOf\""; +exit unless $term->ask_yn( + prompt => "Intend to push to these URIs?", + default => 'n', + ); +system "git push-all --no-verify"; diff --git a/archive/bin/dionysusbk b/archive/bin/dionysusbk new file mode 100755 index 00000000..9c6ad0d6 --- /dev/null +++ b/archive/bin/dionysusbk @@ -0,0 +1,51 @@ +#!/bin/bash + +DIONYSUS="/media/usb0" +DEST="$HOME/lib/annex/old/androidbk" + +# check we can go ahead setup the temporary directory + +if ! [ -d "$DIONYSUS/DCIM" ]; then + echo "$(basename $0): phone microsd doesn't look to be mounted" + exit 1 +fi + +cd $(TMPDIR=~/tmp mktemp -d) +mkdir temp-tar + +# backup photos + +mkdir incoming-photos +if rsync -qavc $DIONYSUS/DCIM/ incoming-photos; then + rm -r $DIONYSUS/DCIM/* +fi + +mkdir incoming-img +if rsync -qavc $DIONYSUS/Pictures/ incoming-img; then + rm -r $DIONYSUS/Pictures +fi + +# backup Kakao + +if rsync -qavc $DIONYSUS/Chats/ temp-tar; then + rm -r $DIONYSUS/Chats +fi + +# backup contacts CSV file + +if rsync -qavc $DIONYSUS/Backup.Contacts.*.csv temp-tar; then + rm $DIONYSUS/Backup.Contacts.*.csv +fi + +# tar it all up +cd temp-tar +tar cf $DEST/$(date +dionysus_%F.tar) * +cd .. + +# conclusion: output instructions for backup procedure + +echo "Done in $(pwd). Now see backup procedure notes in Emacs." + +# echo "Go there and open up the tar file and check everything is there." +# echo "Then move the tar file into annex, check through and move photos," +# echo "into annex and nuke $(pwd)/temp-tar and Dropbox photos." diff --git a/archive/bin/doc_post_receive_hook b/archive/bin/doc_post_receive_hook new file mode 100755 index 00000000..fefe4734 --- /dev/null +++ b/archive/bin/doc_post_receive_hook @@ -0,0 +1,61 @@ +#!/bin/sh + +# TODO if one of the calls to `org-publish-project' coughs on some +# syntax errors, it stops further files in that project being +# published. This script could e-mail me a warning about that, so I +# can try to publish interactively (not batch mode) to find the +# problem (and an easy way to test that interactive publishing would +# be good, in comproc.org) + +HOME=/home/swhitton + +. $HOME/.shenv + +# 1. prepare the union mount + +if ! [ -d "$HOME/local/src/org-publish/doc" ]; then + mkdir -p $HOME/local/src/org-publish + git clone /home/git/doc.git $HOME/local/src/org-publish/doc +else + cd $HOME/local/src/org-publish/doc + git pull -f +fi +#if ! mount | grep "lib/fm" >/dev/null; then +# mount $HOME/lib/fm +#fi +mkdir -p /var/www/spw/org /tmp/dionysus/Agenda "/tmp/dionysus/Org docs" "/tmp/dionysus/Philos notes" + +#TEMP=$(mktemp -d) +if [ -e "/tmp/org-work" ]; then + # we cannot use our own `mktemp -d' because Org hardcodes paths to + # source files in its timestamps cache + echo "another instance of the Org publishing script is running or crashed" + exit +else + mkdir /tmp/org-work + TEMP="/tmp/org-work" +fi +unionfs-fuse $HOME/local/src/org-publish=RW:$HOME=RW $TEMP + +# 2. change to the union mount and run Emacs + +HOME=$TEMP +export HOME +lisp=$(cat < +# Instructions for use : https://spwhitton.name/blog/entry/emacs-pkg-subtree/ + +# Copyright (C) 2015 Sean Whitton. Released under the GNU GPL 3. + +DEST="$HOME/src/dotfiles/.emacs.d/pkg" + +set -e + +if [ "$3" = "" ]; then + echo "$(basename $0): usage: $(basename $0) add|pull git_clone_uri ref" >&2 + exit 1 +fi + +cd $DEST + +op="$1" +uri="$2" +repo="$(basename $2)" +pkg="${repo%%\.git}" +ref="$3" +top="$(git rev-parse --show-toplevel)" +prefix="${DEST##$top/}/$pkg" + +cd $top +clean="$(git status --porcelain)" +if [ ! -z "$clean" ]; then + echo "commit first" >&2 + exit 1 +fi + +if [ "$op" = "add" ]; then + if [ ! -e "$DEST/$pkg" ]; then + git subtree add --squash --prefix $prefix $uri $ref + echo "$uri $ref" >> $DEST/subtrees + git add $DEST/subtrees + git commit -m "updated Emacs packages record" + else + echo "you already have a subtree by that name" >&2 + exit 1 + fi +elif [ "$op" = "pull" ]; then + git subtree pull --squash --prefix $prefix $uri $ref + sed -i -e "s|^${uri} .*$|${uri} ${ref}|" $DEST/subtrees + git add $DEST/subtrees + git commit -m "updated Emacs packages record" +else + echo "$(basename $0): usage: $(basename $0) add|pull git_clone_uri ref" >&2 + exit 1 +fi diff --git a/archive/bin/es b/archive/bin/es new file mode 120000 index 00000000..9cbe6ea5 --- /dev/null +++ b/archive/bin/es @@ -0,0 +1 @@ +e \ No newline at end of file diff --git a/archive/bin/extract_url.pl b/archive/bin/extract_url.pl new file mode 100755 index 00000000..3d32a081 --- /dev/null +++ b/archive/bin/extract_url.pl @@ -0,0 +1,967 @@ +#!/usr/bin/env perl + +# License: BSD-2-Clause (simplified) +# URL: http://spdx.org/licenses/BSD-2-Clause +# +# Copyright (C) 2011-2013 Kyle Wheeler +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY KYLE WHEELER "AS IS" AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +# EVENT SHALL KYLE WHEELER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use MIME::Parser; +use HTML::Parser; +use Getopt::Std; +use Pod::Usage; +use Env; +use strict; +use warnings; + +my $LICENSE = "BSD-2-Clause"; +my $NAME = "extract_url"; +my $version = "1.5.8"; +my $txtonly = 0; +my $list = ''; +my $help = ''; + +sub HELP_MESSAGE { + pod2usage(0); +} +sub VERSION_MESSAGE { + print "$NAME $version License:$LICENSE\n"; +} + +my %options; +eval "use Getopt::Long"; +if ($@) { + $Getopt::Std::STANDARD_HELP_VERSION = 1; + &getopts("hltV",\%options) or pod2usage(-exitval=>2,-verbose=>1); +} else { + &GetOptions('Version' => sub { VERSION_MESSAGE(); exit; }, + 'help' => sub { pod2usage(-exitval=>0,-verbose=>1); }, + 'man' => sub { pod2usage(-exitval=>0, -verbose=>99); }, + 'text' => \$txtonly, + 'list!' => \$list) or pod2usage(-exitval=>2,-verbose=>1); +} +my $fancymenu = 1; +if ($options{'l'} || length $list) { $fancymenu = 0; } +if ($options{'V'}) { &VERSION_MESSAGE(); exit; } +if ($options{'h'}) { &HELP_MESSAGE(); } + +# create a hash of html tag names that may have links +my %link_attr = ( + 'a' => {'href'=>1}, + 'applet' => {'archive'=>1,'codebase'=>1,'code'=>1}, + 'area' => {'href'=>1}, + 'blockquote' => {'cite'=>1}, + #'body' => {'background'=>1}, + 'embed' => {'pluginspage'=>1, 'src'=>1}, + 'form' => {'action'=>1}, + 'frame' => {'src'=>1, 'longdesc'=>1}, + 'iframe' => {'src'=>1, 'longdesc'=>1}, + #'ilayer' => {'background'=>1}, + #'img' => {'src'=>1}, + 'input' => {'src'=>1, 'usemap'=>1}, + 'ins' => {'cite'=>1}, + 'isindex' => {'action'=>1}, + 'head' => {'profile'=>1}, + #'layer' => {'background'=>1, 'src'=>1}, + 'layer' => {'src'=>1}, + 'link' => {'href'=>1}, + 'object' => {'classid'=>1, 'codebase'=>1, 'data'=>1, 'archive'=>1, + 'usemap'=>1}, + 'q' => {'cite'=>1}, + 'script' => {'src'=>1, 'for'=>1}, + #'table' => {'background'=>1}, + #'td' => {'background'=>1}, + #'th' => {'background'=>1}, + #'tr' => {'background'=>1}, + 'xmp' => {'href'=>1}, +); + +# find out the URLVIEW command +my $urlviewcommand=""; +my $displaysanitized = 0; # means to display the pre-sanitized URL instead of the pretty one +my $shortcut = 0; # means open it without checking if theres only 1 URL +my $noreview = 0; # means don't display overly-long URLs to be checked before opening +my $persist = 0; # means don't exit after viewing a URL (ignored if $shortcut == 0) +my $ignore_empty = 0; # means to throw out URLs that don't have text in HTML +my $default_view = "url"; # means what shows up in the list by default: urls or contexts +my $alt_select_key = 'k'; +sub getprefs +{ + if (open(PREFFILE,'<',$ENV{'HOME'}."/.extract_urlview")) { + while () { + my $lineread = $_; + if ($lineread =~ /^ALTSELECT [A-Za-fh-z0-9,.<>?;:{}|!@#$%^&*()_=+-`~]$/) { + $lineread =~ /ALTSELECT (.)/; $alt_select_key = $1; + } elsif ($lineread =~ /^SHORTCUT$/) { $shortcut = 1; + } elsif ($lineread =~ /^NOREVIEW$/) { $noreview = 1; + } elsif ($lineread =~ /^PERSISTENT$/) { $persist = 1; + } elsif ($lineread =~ /^DISPLAY_SANITIZED$/) { $displaysanitized = 1; + } elsif ($lineread =~ /^IGNORE_EMPTY_TAGS$/) { $ignore_empty = 1; + } elsif ($lineread =~ /^COMMAND (.*)/) { + $lineread =~ /^COMMAND (.*)/; + $urlviewcommand=$1; + chomp $urlviewcommand; + } elsif ($lineread =~ /^DEFAULT_VIEW (.*)/) { + $lineread =~ /^DEFAULT_VIEW (.*)/; + if ($1 =~ /^context$/) { + $default_view = "context"; + } else { + $default_view = "url"; + } + } elsif ($lineread =~ /^HTML_TAGS (.*)/) { + $lineread =~ /^HTML_TAGS (.*)/; + my @tags = split(',', $1); + my %tags_hash; + foreach my $tag (@tags) { + $tags_hash{lc $tag} = 1; + } + foreach my $tag (keys %link_attr) { + delete $link_attr{$tag} if (! exists($tags_hash{$tag})); + } + } + } + close PREFFILE; + } elsif (open(URLVIEW,'<',$ENV{'HOME'}."/.urlview")) { + while () { + if (/^COMMAND (.*)/) { + $urlviewcommand=$1; + chomp $urlviewcommand; + last; + } + } + close URLVIEW; + } + if ($urlviewcommand eq "") { + if (exists $ENV{BROWSER}) { + $urlviewcommand=$ENV{BROWSER}; + } else { + $urlviewcommand = "open"; + } + } +} + +my %link_hash; +my %orig_text; +my $newlink = 1; +sub foundurl { + my ($uri) = @_; + #$uri =~ s/mailto:(.*)/$1/; + if (! $link_hash{$uri}) { + $link_hash{$uri} = $newlink++; + } +} +my $foundurl_text_curindex = 0; +my $foundurl_text_lastindex = 0; +my $foundurl_text_prevurl = ""; +my $foundurl_text_text; + +sub foundurl_text { + my ($uri,$orig) = @_; + $uri = &renderuri($uri); + $foundurl_text_curindex = index($$foundurl_text_text, $orig, $foundurl_text_lastindex); + my $sincelast; + if ($foundurl_text_curindex >= 0) { + # this is the expected behavior + $sincelast = &tidytext(substr($$foundurl_text_text,$foundurl_text_lastindex,($foundurl_text_curindex-$foundurl_text_lastindex))); + } else { + # something odd is going on. What's happened is that our URL finder has + # found a URL that isn't in the text following the last URL it found. + # It *may* be doing things out of order... but that's really strange. + # We rely on it finding URLs in order of appearance in order to get + # context information. I'll try to recover but whatever happens, we + # can't get context information for this URL, and our context info for + # other URLs may be seriously messed up! + $foundurl_text_curindex = index($$foundurl_text_text, $orig); + if ($foundurl_text_curindex >= 0) { + # okay, we can recover... we'll just pretend that *everything* is + # the sincelast text + $sincelast = &tidytext(substr($$foundurl_text_text, 0, $foundurl_text_curindex)); + } else { + # Very strange... I can't even find the URL! The best we can do is + # continue without *any* context... but there's *SERIOUS* weirdness + # going on, and expectations have been *majorly* violated. Let's + # just hope the URL is already closed (and so already has context + # information). I'm setting the curindex so that it'll be zero for + # the next URL (i.e. we can pretend that everything up to the next + # url is "sincelast") + $foundurl_text_curindex = 0 - length($orig); + } + $sincelast = ""; + } + $sincelast =~ s/<$//; + $sincelast =~ s/^>//; + &foundurl($uri); + &process_sincelast($uri, $foundurl_text_prevurl, $sincelast); + $foundurl_text_lastindex = $foundurl_text_curindex + length($orig); + $foundurl_text_prevurl = $uri; +} +sub unfindurl { + my($uri) = @_; + delete($link_hash{$uri}); + delete($orig_text{$uri}); +} +sub renderuri { + my($uri) = @_; + $uri =~ s/&/&/gs; + $uri =~ s/%([0-7][a-fA-F0-9])/chr(hex($1))/egs; + return $uri; +} +sub sanitizeuri { + my($uri) = @_; + $uri =~ s/([^a-zA-Z0-9_.!*()\@:=\?\/%~+-])/sprintf("%%%X",ord($1))/egs; + return $uri; +} + +my $parser = new MIME::Parser; + +my %closedurls; + +sub process_sincelast +{ + my($url,$prev,$sincelast) = @_; + if (length($prev) > 0 && ! exists($closedurls{$prev})) { + $orig_text{$prev} .= " ".substr($sincelast,0,30); + $closedurls{$prev} = 1; + #print "URL(".$link_hash{$prev}.":".$newlink."): $prev ->\n\t".$orig_text{$prev}."\n\n"; + } + if (! exists($closedurls{$url})) { + my $beforetext = substr $sincelast, -30; + if (length($beforetext)) { + $orig_text{$url} = "$beforetext =>URL<="; + } else { + $orig_text{$url} = "=>URL<="; + } + } +} + +sub extract_url_from_text { + ($foundurl_text_text) = @_; + # The idea here is to eliminate duplicate URLs - I want the + # %link_hash to be full of URLs. My regex (in the else statement) + # is decent, but imperfect. URI::Find is better. + my $fancyfind=1; + eval "use URI::Find::Schemeless"; + $fancyfind=0 if ($@); + if ($fancyfind == 1) { + my $finder = URI::Find::Schemeless->new(\&foundurl_text); + $finder->find($foundurl_text_text); + } else { + $$foundurl_text_text =~ s{(((mms|ftp|http|https)://|news:)[][A-Za-z0-9_.~!*'();:@&=+,/?%#-]+[^](,.'">;[:space:]]|(mailto:)?[-a-zA-Z_0-9.+]+@[-a-zA-Z_0-9.]+)}{ + &foundurl_text($1,$1); + }eg; + } +} + +my $seenstart = 0; +my $seenurl = ""; +my $beforetext = ""; +my $extendedskipped = ""; +my $last10words = ""; +my $words_since_link_end = ""; + +sub tidytext +{ + my ($text) = @_; + my %rendermap = ( + '[\n]' => ' ', + '[\r]' => ' ', + '&#[0-9]+;' => '', + '&#x[0-9a-f]+;' => '', + ' ' => ' ', + '©' => '(c)', + '—' => '---', + '"' => '"', + ''' => "'", + '<' => '<', + '>' => '>', + '&([ACEINOUY])(grave|acute|circ|tilde|uml|ring|cedil);' => '$1', + '&' => '&', + '\s\s+' => ' ', + ); + foreach my $entity (keys %rendermap) { + my $construct = '$text =~ s/$entity/'.$rendermap{$entity}.'/ig'; + eval $construct; + } + $text =~ s/^\s+//; + $text =~ s/\s+$//; + return $text; +} + +sub subwords +{ + my ($string, $minlen) = @_; + my @words = split(/\s+/, $string); + return "" if @words == 0; + my $retstr = $words[0]; + my $wordcount = 1; + while (length($retstr) < $minlen && $wordcount < @words) { + $retstr .= " " . $words[$wordcount]; + $wordcount++; + } + return $retstr; +} + +sub sublastwords +{ + my ($string, $minlen) = @_; + my @words = split(/\s+/, $string); + return "" if @words == 0; + my $retstr = $words[@words-1]; + my $wordcount = 1; + while (length($retstr) < $minlen && $wordcount < @words) { + $wordcount++; + $retstr = $words[@words - $wordcount] . " $retstr"; + } + return $retstr; +} + +sub find_urls_rec +{ + my($ent) = @_; + #print "type: " . $ent->mime_type . " ... parts: ".$ent->parts."\n"; + if ($ent->parts >= 1 or $ent->mime_type eq "multipart/mixed") { + for (my $i=0;$i<$ent->parts;$i++) { + find_urls_rec($ent->parts($i)); + } + } else { + #print "type: " . $ent->mime_type . "\n"; + if ($ent->mime_type eq "message/rfc822") { &find_urls_rec($ent->parts()); } + elsif ($ent->mime_type eq "text/html" ) { + my $parser = HTML::Parser->new(api_version=>3); + my $skipped_text = ""; + #$parser->unbroken_text(1); + $parser->handler(start => sub { + my($tagname,$pos,$text) = @_; + if (my $link_attr = $link_attr{$tagname}) { + while (4 <= @$pos) { + my($k_offset, $k_len, $v_offset, $v_len) = splice(@$pos,-4); + my $attrname = lc(substr($text, $k_offset, $k_len)); + next unless exists($link_attr->{$attrname}); + next unless $v_offset; # 0 v_offset means no value + + # This is REALLY hack-ish and fragile, but can + # sometimes be invaluable + &extract_url_from_text(\$skipped_text) if (length($skipped_text) > 0); + + my $v = substr($text, $v_offset, $v_len); + $v =~ s/^([\'\"])(.*)\1$/$2/; + $v = &renderuri($v); + &foundurl($v); + + $words_since_link_end .= " $skipped_text"; + $last10words = &tidytext("$last10words $skipped_text"); + $last10words = &sublastwords($last10words, 50); + $skipped_text = ""; + + $words_since_link_end = &tidytext($words_since_link_end); + if (length($seenurl) > 0 && ! exists($closedurls{$seenurl})) { + my $since_words = &subwords($words_since_link_end, 40); + if (length($since_words) > 0) { + my $space = " "; + $space = "" if ($since_words =~ /^[.,;!?)-]/); + $orig_text{$seenurl} .= "$space$since_words"; + } + $closedurls{$seenurl} = 1; + } + + $beforetext = &sublastwords($last10words, 30); + $seenstart = 1; + $seenurl = $v; + } + } + }, + "tagname, tokenpos, text"); + $parser->handler(end => sub { + my ($text) = @_; + $last10words = &tidytext("$last10words $skipped_text"); + $last10words = &sublastwords($last10words, 50); + if ($seenstart == 1) { + if (! exists($closedurls{$seenurl})) { + my $mtext = "=>$skipped_text<="; + if (length($beforetext)) { + my $space = " "; + $space = "" if ($beforetext =~ /[(-]$/); + $orig_text{$seenurl} = "$beforetext$space$mtext"; + } else { + $orig_text{$seenurl} = "$mtext"; + } + } + if (length($skipped_text) == 0 && $ignore_empty == 1 && ! exists($closedurls{$seenurl})) { + &unfindurl($seenurl); + } + $seenstart = 0; + $extendedskipped .= " $skipped_text"; + $words_since_link_end = ""; + } else { + $words_since_link_end .= " $skipped_text"; + } + $skipped_text = ""; + },"text"); + # the "text" handler is used, rather than skipped_text because + # otherwise blocks of text at the beginning of a "lightly html-ified" + # document can be ignored. + $parser->handler(text => sub { + my ($text) = @_; + $skipped_text = &tidytext("$skipped_text $text"); + }, "text"); + $parser->parse($ent->bodyhandle->as_string); + $parser->eof; + if (length($words_since_link_end) > 0) { + # This is REALLY hack-ish and fragile, but can + # sometimes be invaluable + &extract_url_from_text(\$words_since_link_end); + } + if (length($skipped_text) > 0) { + &extract_url_from_text(\$skipped_text); + } + } elsif ($ent->mime_type =~ /text\/.*/) { + $ent->head->unfold; + my $ctype = $ent->head->get('Content-type'); + if (defined($ctype) and $ctype =~ m/format=flowed/) { + my @lines = $ent->bodyhandle->as_lines; + chomp(@lines); + my $body = ""; + my $delsp; + if ($ctype =~ /delsp=yes/) { + #print "delsp=yes!\n"; + $delsp=1; + } else { + #print "delsp=no!\n"; + $delsp=0; + } + for (my $i=0;$i<@lines;$i++) { + my $col = 0; + my $quotetext = ""; + #print "=> " . $lines[$i] . "\n"; + while (substr($lines[$i],$col,1) eq ">") { + $quotetext .= ">"; + $col++; + } + if ($col > 0) { $body .= "$quotetext "; } + while ($lines[$i] =~ / $/ && $lines[$i] =~ /^$quotetext[^>]/ && $lines[$i+1] =~ /^$quotetext[^>]/) { + my $line; + if ($delsp) { + $line = substr($lines[$i],$col,length($lines[$i])-$col-1); + } else { + $line = substr($lines[$i],$col); + } + $line =~ s/^\s+//; + $body .= $line; + $i++; + } + if ($lines[$i] =~ /^$quotetext[^>]/) { + my $line = substr($lines[$i],$col); + $line =~ s/^\s+//; + $body .= $line."\n"; + } + } + &extract_url_from_text(\$body); + } else { + &extract_url_from_text(\$ent->bodyhandle->as_string); + } + } + } +} + +sub urlwrap { + my($subseq,$text,$linelen,$breaker) = @_; + my $len = length($text); + my $i = 0; + my $output = ""; + while ($len > $linelen) { + if ($i > 0) { $output .= $subseq; } + my $breakpoint = -1; + my $chunk = substr($text,$i,$linelen); + my @chars = ("!","*","'","(",")",";",":","@","&","=","+",",","/","?","%","#","[","]","-","_"); + foreach my $chr ( @chars ) { + my $pt = rindex($chunk,$chr); + if ($breakpoint < $pt) { $breakpoint = $pt; } + } + if ($breakpoint == -1) { $breakpoint = $linelen; } + else { $breakpoint += 1; } + $output .= substr($text,$i,$breakpoint) . $breaker; + if ($i == 0) { $linelen -= length($subseq); } + $len -= $breakpoint; + $i += $breakpoint; + } + if ($i > 0) { $output .= $subseq; } + $output .= substr($text,$i); + return $output; +} + +sub isOutputScreen { + use POSIX; + return 0 if POSIX::isatty( \*STDOUT) eq "" ; # pipe + return 1; # screen +} # end of isOutputScreen + +&getprefs(); +$parser->output_to_core(1); +my $filecontents; +if ($#ARGV == 0) { + open(INPUT, "<$ARGV[0]") or die "Couldn't open input file $ARGV[0]: $!"; + $filecontents = join('',); + close(INPUT); +} else { + die "no input provided!\n" if POSIX::isatty( \*STDIN) ne "" ; # pipe + $filecontents = join('',); +} + +if (not $txtonly) { + my $entity = $parser->parse_data($filecontents); + &find_urls_rec($entity); + if (scalar(keys %link_hash) == 0) { + &extract_url_from_text(\$filecontents); + } +} else { + my @lines = ; # slurp in the whole file + my $filebody = join("", @lines); # generate a single string from those lines + &extract_url_from_text(\$filebody); +} + +if (&isOutputScreen) { + eval "use Curses::UI"; + $fancymenu = 0 if ($@); +} else { + $fancymenu = 0; +} + +if ($fancymenu == 1) { + #use strict; + + # This is the shortcut... + if ($shortcut == 1 && 1 == scalar keys %link_hash) { + my ($url) = each %link_hash; + $url = &sanitizeuri($url); + if ($urlviewcommand =~ m/%s/) { + $urlviewcommand =~ s/%s/'$url'/g; + } else { + $urlviewcommand .= " $url"; + } + system $urlviewcommand; + exit 0; + } + + # Curses support really REALLY wants to own STDIN + close(STDIN); + open(STDIN,"/dev/tty"); # looks like a hack, smells like a hack... + + my $cui = new Curses::UI( + -color_support => 1, + -clear_on_exit => 1 + ); + my $wrapwidth = $cui->width() - 2; + my %listhash_url; + my %listhash_context; + my @listvals; + # $link_hash{url} = ordering of the urls in the document as first-seen + foreach my $url (sort {$link_hash{$a} <=> $link_hash{$b} } keys(%link_hash)) { + push(@listvals,$link_hash{$url}); + if ($displaysanitized) { + $listhash_url{$link_hash{$url}} = &sanitizeuri($url); + } else { + $listhash_url{$link_hash{$url}} = $url; + } + $listhash_context{$link_hash{$url}} = $orig_text{$url}; + } + + my @menu = ( + { -label => 'Keys: q=quit m=menu s=switch-view c=context g=top G=bottom', + -submenu => [ + { -label => 'About a', -value => \&about }, + { -label => 'Show Command C', -value => \&show_command }, + { -label => 'Switch List View s', -value => \&switch_list }, + { -label => 'Exit ^q', -value => \&exit_dialog } + ], + }, + ); + my $menu = $cui->add( + 'menu','Menubar', + -menu => \@menu, + ); + my $win1 = $cui->add( + 'win1', 'Window', + -border => 1, + -y => 1, + -bfg => 'green', + ); + sub about() + { + $cui->dialog( + -message => "$NAME $version License:$LICENSE" + ); + } + sub show_command() + { + # This extra sprintf work is to ensure that the title + # is fully displayed even if $urlviewcommand is short + my $title = "The configured URL viewing command is:"; + my $len = length($title); + my $cmd = sprintf("%-${len}s",$urlviewcommand); + $cui->dialog( + -title => "The configured URL viewing command is:", + -message => $cmd, + ); + } + sub exit_dialog() + { + my $return = $cui->dialog( + -message => "Do you really want to quit?", + -buttons => ['yes', 'no'], + ); + exit(0) if $return; + } + + my $listbox_labels; + if ($default_view eq "url") { + $listbox_labels = \%listhash_url; + } else { + $listbox_labels = \%listhash_context; + } + my $listbox = $win1->add( + 'mylistbox', 'Listbox', + -values => \@listvals, + -labels => $listbox_labels, + ); + $cui->set_binding(sub {$menu->focus()}, "\cX"); + $cui->set_binding(sub {$menu->focus()}, "m"); + $cui->set_binding( sub{exit}, "q" ); + $cui->set_binding( \&exit_dialog , "\cQ"); + $cui->set_binding( sub{exit} , "\cc"); + $cui->set_binding(\&switch_list, "s"); + $cui->set_binding(\&about, "a"); + $cui->set_binding(\&show_command, "C"); + $listbox->set_binding( 'option-last', "G"); + $listbox->set_binding( 'option-first', "g"); + sub switch_list() + { + if ($listbox_labels == \%listhash_url) { + $listbox->labels(\%listhash_context); + $listbox_labels = \%listhash_context; + } elsif ($listbox_labels == \%listhash_context) { + $listbox->labels(\%listhash_url); + $listbox_labels = \%listhash_url; + } + $listbox->focus(); + } + sub madeselection_sub { + my ($stayopen) = @_; + my $rawurl = $listhash_url{$listbox->get_active_value()}; + my $url = &sanitizeuri($rawurl); + my $command = $urlviewcommand; + if ($command =~ m/%s/) { + $command =~ s/%s/'$url'/g; + } else { + $command .= " $url"; + } + my $return = 1; + if ($noreview != 1 && length($rawurl) > ($cui->width()-2)) { + $return = $cui->dialog( + -message => &urlwrap(" ",$rawurl,$cui->width()-8,"\n"), + -title => "Your Choice:", + -buttons => ['ok', 'cancel'], + ); + } + if ($return) { + system $command; + if ($stayopen == 0) { + exit 0 if ($persist == 0); + } else { + exit 0 unless ($persist == 0); + } + } + } + sub madeselection { &madeselection_sub(0); } + sub altexit_madeselection { &madeselection_sub(1); } + $cui->set_binding( \&madeselection, " "); + $listbox->set_routine('option-select',\&madeselection); + $cui->set_binding( \&altexit_madeselection, $alt_select_key); + use Text::Wrap; + sub contextual { + my $rawurl = $listhash_url{$listbox->get_active_value()}; + $Text::Wrap::columns = $cui->width()-8; + if (exists($orig_text{$rawurl}) && length($orig_text{$rawurl}) > 1) { + $cui->dialog( + -message => wrap('','',$orig_text{$rawurl}), + -title => "Context:", + -buttons => ['ok'], + ); + } else { + $cui->error( + -message => "Sorry, I don't have any context for this link", + -buttons => ['ok'], + -bfg => 'red', + -tfg => 'red', + -fg => 'red', + ); + } + } + $cui->set_binding( \&contextual, "c"); + + $listbox->focus(); + $cui->mainloop(); +} else { + # using this as a pass-thru to URLVIEW + foreach my $value (sort {$link_hash{$a} <=> $link_hash{$b} } keys %link_hash) + { + $value = &sanitizeuri($value); + print "$value\n"; + } +} + +__END__ + +=pod + +=head1 NAME + +extract_url -- extract URLs from email messages + +=head1 SYNOPSIS + +extract_url [options] I + +=head1 DESCRIPTION + +This is a Perl script that extracts URLs from correctly-encoded +I email messages. This can be used either as a pre-parser for +I, or to replace I entirely. + +I is a great program, but has some deficiencies. In particular, +it isn't particularly configurable, and cannot handle URLs that have +been broken over several lines in I email +messages. Nor can it handle I email messages. Also, +I doesn't eliminate duplicate URLs. This Perl script handles +all of that. It also sanitizes URLs so that they can't break out of the +command shell. + +This is designed primarily for use with the I emailer. The idea is +that if you want to access a URL in an email, you pipe the email to a +URL extractor (like this one) which then lets you select a URL to view +in some third program (such as Firefox). An alternative design is to +access URLs from within mutt's pager by defining macros and tagging the +URLs in the display to indicate which macro to use. A script you can use +to do that is I. + +=head1 OPTIONS + +=over 4 + +=item B<-h, --help> + +Display this help and exit. + +=item B<-m, --man> + +Display the full man page documentation. + +=item B<-l, --list> + +Prevent use of Ncurses, and simply output a list of extracted URLs. + +=item B<-t, --text> + +Prevent MIME handling; treat the input as plain text. + +=item B<-V, --version> + +Output version information and exit. + +=back + +=head1 DEPENDENCIES + +Mandatory dependencies are B and B. These +usually come with Perl. + +Optional dependencies are B (recognizes more exotic URL +variations in plain text (without HTML tags)), B (allows it +to fully replace I), and B (if present, +B recognizes long options --version and --list). + +=head1 EXAMPLES + +This Perl script expects a valid email to be either piped in via STDIN or in a +file listed as the script's only argument. Its STDOUT can be a pipe into +I (it will detect this). Here's how you can use it: + + cat message.txt | extract_url.pl + cat message.txt | extract_url.pl | urlview + extract_url.pl message.txt + extract_url.pl message.txt | urlview + +For use with B, here's a macro you can use: + + macro index,pager \cb "\ + \ + unset pipe_decode\ + extract_url.pl" \ + "get URLs" + +For use with B, here's a more complicated macro you can use: + + macro index,pager \cb "\ + set my_pdsave=\$pipe_decode\ + unset pipe_decode\ + extract_url.pl\ + set pipe_decode=\$my_pdsave" \ + "get URLs" + +Here's a suggestion for how to handle I: + + macro index,pager ,b "\ + set my_pdsave=\$pipe_decode\ + unset pipe_decode\ + extract_url.pl\ + set pipe_decode=\$my_pdsave" \ + "get URLs" + + macro index,pager ,B "\ + set my_pdsave=\$pipe_decode\ + set pipe_decode\ + extract_url.pl\ + set pipe_decode=\$my_pdsave" \ + "decrypt message, then get URLs" + + message-hook . 'macro index,pager \cb ,b "URL viewer"' + message-hook ~G 'macro index,pager \cb ,B "URL viewer"' + +=head1 CONFIGURATION + +If you're using it with B (i.e. as a standalone URL +selector), this Perl script will try and figure out what command to use +based on the contents of your F<~/.urlview> file. However, it also has +its own configuration file (F<~/.extract_urlview>) that will be used +instead, if it exists. So far, there are eight kinds of lines you can +have in this file: + +=over 8 + +=item COMMAND ... + +This line specifies the command that will be used to view URLs. This +command CAN contain a I<%s>, which will be replaced by the URL inside +single-quotes. If it does not contain a I<%s>, the URL will simply be +appended to the command. If this line is not present, the command is +taken from the environment variable $BROWSER. If BROWSER is not set, the +command is assumed to be "open", which is the correct command for MacOS X +systems. + +=item SHORTCUT + +This line specifies that if an email contains only 1 URL, that URL will +be opened without prompting. The default (without this line) is to +always prompt. + +=item NOREVIEW + +Normally, if a URL is too long to display on screen in the menu, the +user will be prompted with the full URL before opening it, just to make +sure it's correct. This line turns that behavior off. + +=item PERSISTENT + +By default, when a URL has been selected and viewed from the menu, +B will exit. If you would like it to be ready to view +another URL without re-parsing the email (i.e. much like standard +I behavior), add this line to the config file. + +=item IGNORE_EMPTY_TAGS + +By default, the script collects all the URLs it can find. Sometimes, +though, HTML messages contain links that don't correspond to any text +(and aren't normally rendered or accessible). This tells the script to +ignore these links. + +=item HTML_TAGS ... + +This line specifies which HTML tags will be examined for URLs. By +default, the script is very generous, looking in I, I, +I, I
, I, I
, I, I