aboutsummaryrefslogtreecommitdiff
path: root/bin
diff options
context:
space:
mode:
authorRaul Benencia <rul@kalgan.cc>2020-05-24 13:44:14 -0700
committerRaul Benencia <rul@kalgan.cc>2020-05-24 13:47:59 -0700
commit546a3a211a0d59f14edbd97c1990476e84a46247 (patch)
treec5709cc4a8f7b0f39e1cb023f0f6b03bf749272e /bin
parent3589f3e66769e87831e9f31fb3493064dd3ce73e (diff)
Add bin/ repo from home-bin/ repo
Diffstat (limited to 'bin')
-rwxr-xr-xbin/dim-screen.sh51
-rwxr-xr-xbin/emacs-pkg-install45
-rwxr-xr-xbin/i3lock-wrapper5
-rwxr-xr-xbin/mutt-displayfilter-gpg52
-rwxr-xr-xbin/mutt-fetchbug161
-rwxr-xr-xbin/mutt-open76
-rwxr-xr-xbin/mutt-remember-mail23
-rwxr-xr-xbin/pycombine240
-rwxr-xr-xbin/touchpad-enable-tap7
-rwxr-xr-xbin/xrandr-rul21
10 files changed, 681 insertions, 0 deletions
diff --git a/bin/dim-screen.sh b/bin/dim-screen.sh
new file mode 100755
index 0000000..949ed85
--- /dev/null
+++ b/bin/dim-screen.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+# Debian package: xbacklight
+
+# Brightness will be lowered to this value.
+min_brightness=0
+
+# If your video driver works with xbacklight, set -time and -steps for fading
+# to $min_brightness here. Setting steps to 1 disables fading.
+fade_time=2000
+fade_steps=200
+
+# Time to sleep (in seconds) between increments when using sysfs. If unset or
+# empty, fading is disabled.
+fade_step_time=0.05
+
+
+get_brightness() {
+ if [[ -z $sysfs_path ]]; then
+ xbacklight -get
+ else
+ cat $sysfs_path
+ fi
+}
+
+set_brightness() {
+ if [[ -z $sysfs_path ]]; then
+ xbacklight -steps 1 -set $1
+ else
+ echo $1 > $sysfs_path
+ fi
+}
+
+fade_brightness() {
+ if [[ -z $sysfs_path ]]; then
+ xbacklight -time $fade_time -steps $fade_steps -set $1
+ elif [[ -z $fade_step_time ]]; then
+ set_brightness $1
+ else
+ local level
+ for level in $(eval echo {$(get_brightness)..$1}); do
+ set_brightness $level
+ sleep $fade_step_time
+ done
+ fi
+}
+
+trap 'exit 0' TERM INT
+trap "set_brightness $(get_brightness); kill %%" EXIT
+fade_brightness $min_brightness
+sleep 2147483647 &
+wait
diff --git a/bin/emacs-pkg-install b/bin/emacs-pkg-install
new file mode 100755
index 0000000..93b747f
--- /dev/null
+++ b/bin/emacs-pkg-install
@@ -0,0 +1,45 @@
+#!/bin/bash
+# Gotten from: https://gist.github.com/padawanphysicist/d6299870de4ef8ad892f
+#
+# I wrapped the code constructed in
+#
+# http://hacks-galore.org/aleix/blog/archives/2013/01/08/install-emacs-packages-from-command-line
+#
+# in a single bash script, so I would a single code snippet.
+#
+
+
+# Elisp script is created as a temporary file, to be removed after installing
+# the package
+elisp_script_name=$(mktemp /tmp/emacs-pkg-install-el.XXXXXX)
+elisp_code="
+;;
+;; Install package from command line. Example:
+;;
+;; $ emacs --batch --expr \"(define pkg-to-install 'smex)\" -l emacs-pkg-install.el
+;;
+(require 'package)
+(package-initialize)
+(add-to-list 'package-archives
+ '(\"melpa\" . \"http://melpa.milkbox.net/packages/\") t)
+;;(add-to-list 'package-archives
+;; '(\"marmalade\" . \"http://marmalade-repo.org/packages/\") t)
+;; Fix HTTP1/1.1 problems
+(setq url-http-attempt-keepalives nil)
+(package-refresh-contents)
+(package-install pkg-to-install)"
+
+echo "$elisp_code" > $elisp_script_name
+
+if [ $# -lt 1 ]
+then
+ echo "Usage: `basename $0` <package 1> <package 2> ..."
+ exit 1
+fi
+
+for pkg_name in $@; do
+ emacs --batch --eval "(defconst pkg-to-install '$pkg_name)" -l $elisp_script_name
+done
+
+# Remove tmp file
+rm "$elisp_script_name"
diff --git a/bin/i3lock-wrapper b/bin/i3lock-wrapper
new file mode 100755
index 0000000..7c8d8e2
--- /dev/null
+++ b/bin/i3lock-wrapper
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+killall -SIGUSR1 dunst
+i3lock --nofork -c "#000000" -e -f
+killall -SIGUSR2 dunst
diff --git a/bin/mutt-displayfilter-gpg b/bin/mutt-displayfilter-gpg
new file mode 100755
index 0000000..ccd99d1
--- /dev/null
+++ b/bin/mutt-displayfilter-gpg
@@ -0,0 +1,52 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+Copyright 2017 Sebastian Schmittner
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+import re
+import sys
+
+
+def shorten_tofu_blocks(input):
+ pattern = r"(?s)gpg: Verified ([0-9]+) .*(ago|as being bad)\."
+ replace = r"gpg: TOFU \1"
+ return re.sub(pattern, replace, input)
+
+
+def remove_aka_list(input):
+ pattern = r"gpg:[ \t]+aka [^\n]*\n"
+ replace = ""
+ return re.sub(pattern, replace, input)
+
+
+def shorten_untrusted_warning(input):
+ pattern = r"(?s)gpg: WARNING: This key is not certified with sufficiently trusted signatures!.*key fingerprint: [^\n]+"
+ replace = "gpg: WARNING: untrusted signature"
+ return re.sub(pattern, replace, input)
+
+
+def main():
+ input = sys.stdin.read()
+ input = shorten_tofu_blocks(input)
+ input = remove_aka_list(input)
+ input = shorten_untrusted_warning(input)
+ sys.stdout.write(input)
+
+
+# goto main
+if __name__ == "__main__":
+ main()
diff --git a/bin/mutt-fetchbug b/bin/mutt-fetchbug
new file mode 100755
index 0000000..93ffc58
--- /dev/null
+++ b/bin/mutt-fetchbug
@@ -0,0 +1,161 @@
+#!/usr/bin/perl -w
+#
+# mutt-fetchbug, extensively based off of
+# mutt-notmuch - notmuch (of a) helper for Mutt
+#
+# Copyright: © 2011 Stefano Zacchiroli <zack@upsilon.cc>
+# License: GNU General Public License (GPL), version 3 or above
+#
+# Differences between mutt-notmuch and mutt-fetchbug are
+# Copyright: © 2012 Ryan Kavanagh <rak@debian.org>
+# License: GNU General Public License (GPL), version 3 or above
+#
+# See the bottom of this file for more documentation.
+# A manpage can be obtained by running "pod2man mutt-fetchbug > mutt-fetchbug.1"
+
+use strict;
+use warnings;
+
+use File::Path;
+use Getopt::Long;
+use Pod::Usage;
+
+# search($btsmbox, $query)
+# Fetch bugs matching $query with bts; store results in $btsmbox
+sub search($$) {
+ my ($btsmbox, $query) = @_;
+
+ system("bts --cache-mode=mbox cache $query"
+ . " && ln -fs ~/.cache/devscripts/bts/$query.mbox $btsmbox");
+}
+
+sub search_action($$@) {
+ my ($interactive, $btsmbox, @params) = @_;
+
+ if (! $interactive) {
+ fetch($btsmbox, join(' ', @params));
+ } else {
+ my $query = "";
+ my $done = 0;
+ while (! $done) {
+ print "bug number ('?' for man): ";
+ chomp($query = <STDIN>);
+ if ($query eq "?") {
+ system("man bts");
+ } elsif ($query eq "") {
+ $done = 1; # quit doing nothing
+ } else {
+ search($btsmbox, $query);
+ $done = 1;
+ }
+ }
+ }
+}
+
+sub die_usage() {
+ my %podflags = ( "verbose" => 1,
+ "exitval" => 2 );
+ pod2usage(%podflags);
+}
+
+sub main() {
+ my $btsmbox = "$ENV{HOME}/.cache/mutt_btsresults";
+ my $interactive = 0;
+ my $help_needed = 0;
+
+ my $getopt = GetOptions(
+ "h|help" => \$help_needed,
+ "o|output-mbox=s" => \$btsmbox,
+ "p|prompt" => \$interactive);
+ if (! $getopt || $#ARGV < 0) { die_usage() };
+ my ($action, @params) = ($ARGV[0], @ARGV[1..$#ARGV]);
+
+ if ($help_needed) {
+ die_usage();
+ } elsif ($action eq "search" && $#ARGV == 0 && ! $interactive) {
+ print STDERR "Error: no search term provided\n\n";
+ die_usage();
+ } elsif ($action eq "search") {
+ search_action($interactive, $btsmbox, @params);
+ } else {
+ die_usage();
+ }
+}
+
+main();
+
+__END__
+
+=head1 NAME
+
+mutt-fetchbug - 'bts show' frontend for Mutt
+
+=head1 SYNOPSIS
+
+=over
+
+=item B<mutt-fetchbug> [I<OPTION>]... search [I<SEARCH-TERM>]...
+
+=back
+
+=head1 DESCRIPTION
+
+mutt-fetchbug is a frontend to the 'bts show' command (Debian package:
+devscripts) designed to fetch bugs and place them in a predefined mbox. The
+search term should typically be a bug number.
+
+=head1 OPTIONS
+
+=over 4
+
+=item -o DIR
+
+=item --output-mbox DIR
+
+Store search results as (symlink) mbox MBOX. Beware: MBOX will be overwritten.
+(Default: F<~/.cache/mutt_btsresults/>)
+
+=item -p
+
+=item --prompt
+
+Instead of using command line search terms, prompt the user for them (only for
+"search").
+
+=item -h
+
+=item --help
+
+Show usage information and exit.
+
+=back
+
+=head1 INTEGRATION WITH MUTT
+
+mutt-fetchbug can be used to integrate 'bts show' with the Mutt mail user agent
+(unsurprisingly, given the name). To that end, you should define the following
+macros in your F<~/.muttrc> (replacing F<~/bin/mutt-fetchbug> for the actual
+location of mutt-fetchbug on your system):
+
+ macro index <F7> \
+ "<enter-command>unset wait_key<enter><shell-escape>~/bin/mutt-fetchbug --prompt search<enter><change-folder-readonly>~/.cache/mutt_btsresults<enter><enter-command>set wait_key<enter>" \
+ "fetch bug(s) (using bts show)"
+
+The macro (activated by <F7>) will prompt the user for a bug number and then
+jump to a temporary mbox showing the fetched bug.
+
+=head1 SEE ALSO
+
+mutt(1), bts(1)
+
+=head1 AUTHOR
+
+mutt-fetchbug is extensively based off of 'mutt-notmuch', which is
+Copyright: (C) 2011 Stefano Zacchiroli <zack@upsilon.cc>.
+
+All differences between mutt-fetchbug and mutt-notmuch are
+Copyright (C) 2012 Ryan Kavanagh <rak@debian.org>
+
+License: GNU General Public License (GPL), version 3 or higher
+
+=cut
diff --git a/bin/mutt-open b/bin/mutt-open
new file mode 100755
index 0000000..0b21261
--- /dev/null
+++ b/bin/mutt-open
@@ -0,0 +1,76 @@
+#!/bin/bash
+#
+# Fire up mutt on a given mail, located in some Maildir
+# Mail can be specified either by path or by Messsage-ID; in the latter case
+# file lookup is performed using some mail indexing tool.
+#
+# Copyright: © 2009-2014 Stefano Zacchiroli <zack@upsilon.cc>
+# License: GNU General Public License (GPL), version 3 or above
+
+# requires: notmuch | maildir-utils >= 0.7
+
+MUTT="neomutt"
+MAIL_INDEXER="notmuch" # one of "notmuch", "mu"
+export NOTMUCH_CONFIG=$HOME/te/etc/notmuch-config
+
+MUTT_FLAGS="-R -F ~/te/etc/muttrc"
+HIDE_SIDEBAR_CMD="B" # set to empty string if sidebar is not used
+
+# Sample output of lookup command, which gets passed to mutt-open
+# /home/zack/Maildir/INBOX/cur/1256673179_0.8700.usha,U=37420,FMD5=7e33429f656f1e6e9d79b29c3f82c57e:2,S
+
+die_usage () {
+ echo "Usage: mutt-open FILE" 1>&2
+ echo " mutt-open MESSAGE-ID" 1>&2
+ echo 'E.g.: mutt-open `notmuch search --output=files id:MESSAGE-ID`' 1>&2
+ echo ' mutt-open `mu find -f l i:MESSAGE-ID`' 1>&2
+ echo ' mutt-open 20091030112543.GA4230@usha.takhisis.invalid' 1>&2
+ exit 3
+}
+
+# Lookup: Message-ID -> mail path. Store results in global $fname
+lookup_msgid () {
+ msgid_query="$1"
+ case "$MAIL_INDEXER" in
+ notmuch)
+ fname=$(notmuch search --output=files id:"$msgid_query" | head -n 1)
+ ;;
+ mu)
+ fname=$(mu find -f l i:"$msgid_query" | head -n 1)
+ ;;
+ esac
+}
+
+dump_info () {
+ echo "fname: $fname"
+ echo "msgid: $msgid"
+}
+
+if [ -z "$1" -o "$1" = "-h" -o "$1" = "-help" -o "$1" = "--help" ] ; then
+ die_usage
+fi
+if (echo "$1" | grep -q /) && test -f "$1" ; then # arg is a file
+ fname="$1"
+ msgid=$(egrep -i '^message-id:' "$fname" | cut -f 2 -d':' | sed 's/[ <>]//g')
+elif ! (echo "$1" | grep -q /) ; then # arg is a Message-ID
+ msgid="$1"
+ lookup_msgid "$msgid" # side-effect: set $fname
+fi
+# dump_info ; exit 3
+if ! dirname "$fname" | egrep -q '/(cur|new|tmp)$' ; then
+ echo "Path not pointing inside a maildir: $fname" 1>&2
+ exit 2
+fi
+maildir=$(dirname $(dirname "$fname"))
+
+if ! [ -d "$maildir" ] ; then
+ echo "Not a (mail)dir: $maildir" 1>&1
+ exit 2
+fi
+
+# UGLY HACK: without sleep, push keys do not reach mutt, I _guess_ that there
+# might be some terminal-related issue here, since also waiting for an input
+# with "read" similarly "solves" the problem
+sleep 0.5
+mutt_keys="/=i$msgid\n\n$HIDE_SIDEBAR_CMD"
+exec $MUTT $MUTT_FLAGS -f "$maildir/" -e "push $mutt_keys"
diff --git a/bin/mutt-remember-mail b/bin/mutt-remember-mail
new file mode 100755
index 0000000..e9898f5
--- /dev/null
+++ b/bin/mutt-remember-mail
@@ -0,0 +1,23 @@
+#!/usr/bin/perl -w
+#
+# Helper for mutt to remember mails in Emacs' Org mode
+#
+# Copyright: © 2009-2010 Stefano Zacchiroli <zack@upsilon.cc>
+# License: GNU General Public License (GPL), version 3 or above
+#
+# Example of mutt macro to invoke this hitting ESC-R (to be put in ~/.muttrc):
+# macro index \eR "|~/bin/remember-mail\n"
+
+use strict;
+use Mail::Internet;
+use URI::Escape;
+
+my $msg = Mail::Internet->new(\*STDIN);
+$msg->head->get('message-id') =~ /^<(.*)>$/;
+my $mid = $1;
+my $subject = $msg->head->get('subject') || "";
+my $from = $msg->head->get('from') || "";
+chomp ($subject, $from);
+my $note_body = uri_escape(" Subject: $subject\n From: $from");
+
+exec "emacsclient", "-c", "org-protocol:/capture:/m/mutt:$mid/mail/$note_body";
diff --git a/bin/pycombine b/bin/pycombine
new file mode 100755
index 0000000..a9d2338
--- /dev/null
+++ b/bin/pycombine
@@ -0,0 +1,240 @@
+#!/usr/bin/env python3
+
+"""
+Copyright 2019 Raúl Benencia
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+
+
+import argparse
+import os
+import sys
+import re
+
+
+class DirCombiner():
+ ignore_files = ['.known', '.gitignore']
+ ignore_dirs = ['.git', '.svn', '_darcs']
+
+ def __init__(self, include, exclude, status_filename, verbose):
+ self.include = re.compile(include)
+ self.exclude = re.compile(exclude)
+ self.status_filename = status_filename
+ self.verbose = verbose
+
+ def _filter_files(self, filenames):
+ return [
+ fn for fn in filenames
+ if fn not in DirCombiner.ignore_files
+ and self.include.match(fn)
+ and not self.exclude.match(fn)
+ ]
+
+ def _filter_dirs(self, directories):
+ return [
+ sd for sd in directories
+ if sd not in DirCombiner.ignore_dirs
+ and self.include.match(sd)
+ and not self.exclude.match(sd)
+ ]
+
+ def _create_symlinks(self, status, dest, dirpath, filenames):
+ # Create symlinks for each file
+ for f in filenames:
+ src = os.path.abspath(os.path.normpath(os.path.join(dirpath, f)))
+ dst = os.path.normpath(os.path.join(dest, dirpath, f))
+
+ status.mark_as_seen(dst)
+ replace_msg = ""
+ if os.path.lexists(dst):
+ if not os.path.islink(dst):
+ sys.stderr.write(
+ "{} in {} is also in {}\n".format(f, dirpath, dest)
+ )
+ continue
+ elif os.path.realpath(dst) != src:
+ replace_msg = "(previously pointing to: {})".format(
+ os.path.realpath(dst))
+ try:
+ os.remove(dst)
+ except IOError:
+ raise DeleteFileError(dst)
+ else:
+ # Symlink is already pointing to the current file
+ continue
+
+ try:
+ os.symlink(src, dst)
+ except OSError:
+ raise CreateSymlinkError(dst)
+
+ if replace_msg == "" or (replace_msg != "" and self.verbose):
+ sys.stdout.write("{} -> {} {}\n".format(src, dst, replace_msg))
+
+ def _create_directories(self, dest, dirpath, subdirs):
+ for sd in subdirs:
+ dst = os.path.normpath(os.path.join(dest, dirpath, sd))
+ if os.path.lexists(dst):
+ if os.path.isdir(dst):
+ continue
+ else:
+ raise DirIsFileError(dst)
+
+ try:
+ os.mkdir(dst)
+ except OSError:
+ raise CreateDirError(dst)
+
+ sys.stdout.write(f"{dst} created\n")
+
+ def combine(self, dest, directories):
+ # Loop through all the directories. The later ones can override
+ # links from the previous ones.
+ for directory in directories:
+ try:
+ os.chdir(directory)
+ except OSError:
+ raise ChangeDirError(directory)
+
+ status = DirCombinerStatus(self.status_filename)
+ for dirpath, subdirs, filenames in os.walk('.'):
+ # Modify filenames and subdirs in place to reduce the
+ # scope of the search.
+ subdirs[:] = self._filter_dirs(subdirs)
+ filenames[:] = self._filter_files(filenames)
+
+ self._create_symlinks(status, dest, dirpath, filenames)
+ self._create_directories(dest, dirpath, subdirs)
+
+ status.finish()
+
+
+class DirCombinerStatus():
+ def __init__(self, status_filename):
+ self._seen = []
+ self._status_filename = status_filename
+
+ try:
+ if os.path.exists(self._status_filename):
+ if not os.path.isfile(self._status_filename):
+ raise StatusIsNotFileError(self._status_filename)
+ else:
+ with open(self._status_filename, 'r') as f:
+ self._known = f.read().split('\n')[:-1]
+ else:
+ self._known = []
+ except IOError:
+ raise StatusFileError(self._status_filename)
+
+ def _clean_old(self):
+ old = {f for f in self._known if f not in self._seen}
+ for f in old:
+ try:
+ if os.path.lexists(f):
+ rp = os.path.realpath(f)
+ if not os.path.islink(f) or os.path.exists(rp):
+ sys.stderr.write(
+ f"Not deleting {f} as it's a valid file\n")
+ else:
+ os.remove(f)
+ sys.stdout.write(f"Deleted old file {f}\n")
+ except IOError:
+ raise DeleteFileError(f)
+
+ def mark_as_seen(self, filename):
+ self._seen.append(filename)
+
+ def finish(self):
+ self._clean_old()
+ with open(self._status_filename, 'w') as f:
+ for filename in self._seen:
+ f.write(filename + '\n')
+
+
+class Error(Exception):
+ """Base class for exceptions in this program """
+ msg = ''
+
+ def __init__(self, param):
+ self.param = param
+
+ def message(self):
+ return self.msg.format(self.param) + '\n'
+
+
+class DirIsFileError(Error):
+ msg = 'Target directory {} already exists as a non-dir file.'
+
+
+class CreateDirError(Error):
+ msg = 'Failure creating directory {}. Do you have appropriate permissions?'
+
+
+class CreateSymlinkError(Error):
+ msg = 'Failure creating symlink {}. Do you have appropriate permissions?'
+
+
+class ChangeDirError(Error):
+ msg = 'Failure changing to dir {}. Do you have appropriate permissions?'
+
+
+class StatusFileError(Error):
+ msg = 'Error while opening status file {}. ' + \
+ 'Do you have appropriate permissions?'
+
+
+class StatusIsNotFile(Error):
+ msg = 'The status path {} does not have a file. Maybe it holds an old dir?'
+
+
+class DeleteFileError(Error):
+ msg = 'Unable to delete file {}. Do you have appropriate permissions?'
+
+
+def main():
+ parser = argparse.ArgumentParser()
+
+ parser.add_argument('-i', '--include', default='.*',
+ help='regex for filenames to include')
+
+ parser.add_argument('-e', '--exclude', default='^$',
+ help='regex for filenames to exclude')
+
+ parser.add_argument('-s', '--status-filename', default='.known',
+ help='regex for filenames to exclude')
+
+ parser.add_argument('dest', metavar='dest', type=str,
+ help='directory where to install links and filenames')
+
+ parser.add_argument('dirs', metavar='dir', type=str, nargs='+',
+ help='directories to retrieve filenames from')
+
+ parser.add_argument('-v', '--verbose', default=False,
+ help='Verbose output')
+
+
+ args = parser.parse_args()
+ dc = DirCombiner(args.include, args.exclude, args.status_filename, args.verbose)
+
+ try:
+ dc.combine(args.dest, args.dirs)
+
+ except Error as e:
+ sys.stderr.write(e.message())
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/bin/touchpad-enable-tap b/bin/touchpad-enable-tap
new file mode 100755
index 0000000..dcd530f
--- /dev/null
+++ b/bin/touchpad-enable-tap
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+if xinput list | grep -iq "touchpad"; then
+ device_id=$(xinput list | grep -i touchpad | awk '{print $6}' | sed 's/id=//')
+ option_id=$(xinput list-props $device_id | grep -i 'Tapping Enabled' | head -n1 | awk '{print $4}' | tr -d '():')
+ xinput set-prop $device_id $option_id 1
+fi
diff --git a/bin/xrandr-rul b/bin/xrandr-rul
new file mode 100755
index 0000000..0b76a98
--- /dev/null
+++ b/bin/xrandr-rul
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+INT="eDP-1"
+
+export DISPLAY="${DISPLAY:-:0}"
+
+xrandr --output ${INT} --mode 1920x1080 # 1920x1440 #2560x1440
+
+for EXT in DP-1 DP-2 HDMI-2
+do
+ if xrandr -q | grep -qs "^${EXT} connected"
+ then
+ xrandr --output ${EXT} --mode 3440x1440
+ xrandr --output ${EXT} --left-of ${INT}
+ else
+ xrandr --output ${EXT} --off
+ fi
+done
+
+# If we have a script for sorting out the workspaces, use it.
+eval "$(command -v i3-fix-workspaces)"
nihil fit ex nihilo