15 September 2011

11 July 2011

Stupid Unix Tricks: Manipulating Xfwm Windows from Shell Scripts - Part I

I've been hacking with some shell scripts to manipulate windows, in lieau of using a full-fleged tiling window manager (such as Xmonad).

There are command-line utilities to help with this, such as wmctrl and xdotool that one can make use of. And I will discuss these tools in a later post.

But these tools are not aware of panels (e.g., the Xfce panel). So any scripts that use these tools needs to first query the panel configuration to determine what the margins of the desktop are.

Below is a script that does that for Xfce 4.6:

MARGIN_TOP=0
MARGIN_BOTTOM=0
MARGIN_LEFT=0
MARGIN_RIGHT=0

XFCE_CONFIG_HOME="${XDG_CONFIG_HOME}/xfce4/"

function determine_margins_xfce46 {
  i=1
  until [ $i -eq 0 ]
  do
    size=`xmlstarlet sel -t -v "/panels//panel[$i]/properties/property[@name='size']/@value" "${XFCE_CONFIG_HOME}/panel/panels.xml"`
    if [ "$size" == "" ]
    then
      i=0
    else
      pos=`xmlstarlet sel -t -v "/panels//panel[$i]/properties/property[@name='screen-position']/@value" "${XFCE_CONFIG_HOME}/panel/panels.xml"`
      case $pos in
       10|11|12)
       if (( (size > MARGIN_BOTTOM) )); then MARGIN_BOTTOM=$size; fi
       ;;
       1|2|3)
       if (( (size > MARGIN_TOP) )); then MARGIN_TOP=$size; fi
       ;;
       4|5|6)
       if (( (size > MARGIN_LEFT) )); then MARGIN_LEFT=$size; fi
       ;;
       7|8|9)
       if (( (size > MARGIN_RIGHT) )); then MARGIN_RIGHT=$size; fi
       ;;
      esac
      i=$[i+1]
    fi
  done
}

This bash function uses xmlstarlet to query the XML configuration files.

If you're using Xfce 4.8, the configuration has changed to use xfconf instead. In that case, we use the following bash function:

function determine_margins_xfce48 {
    PANEL_COUNT=`xfconf-query -c xfce4-panel -p /panels`

    i=$PANEL_COUNT
    while [ $i -gt 0 ]
    do
 i=$[i-1]
 hidden=`xfconf-query -c xfce4-panel -p /panels/panel-$i/autohide`
 if [ $hidden == "false" ]
 then
     size=`xfconf-query -c xfce4-panel -p /panels/panel-$i/size`
     pos=`xfconf-query -c xfce4-panel -p /panels/panel-$i/position`
     if [[ "${pos}" =~ ^([pxy])=([0-9]+)\;([pxy])=([0-9]+)\;([pxy])=([0-9]+)$ ]]
     then
              export ${BASH_REMATCH[1]}=${BASH_REMATCH[2]}
              export ${BASH_REMATCH[3]}=${BASH_REMATCH[4]}
              export ${BASH_REMATCH[5]}=${BASH_REMATCH[6]}
     fi
     horiz=`xfconf-query -c xfce4-panel -p /panels/panel-$i/horizontal`
     if [ $horiz == "false" ]
     then
      if (( ($p >= 1) && ($p <= 4) ))
      then
        if (( (size > MARGIN_RIGHT) )); then MARGIN_RIGHT=$size; fi
      elif (( ($p >= 5) && ($p <= 8) ))
      then
        if (( (size > MARGIN_LEFT) )); then MARGIN_LEFT=$size; fi
      fi
     else
      if (( ($p == 2) || ($p == 6) || ($p == 9) || ($p == 11) ))
      then
        if (( (size > MARGIN_TOP) )); then MARGIN_TOP=$size; fi
      elif (( ($p == 4) || ($p == 8) || ($p == 10) || ($p == 12) ))
      then
        if (( (size > MARGIN_BOTTOM) )); then MARGIN_BOTTOM=$size; fi
      fi
     fi
    fi
    done
}
And we determine which to use simply with

if ( test -e "${XFCE_CONFIG_HOME}/panel/panels.xml" ); then
  determine_margins_xfce46
else
  determine_margins_xfce48
fi

05 June 2011

Stupid Unix Tricks: Downgrading Shotwell from 0.7.2 in Ubuntu Maverick to 0.6.1 in Debian Squeeze

I recently purchased a new computer and set it up from scratch with the latest version of Debian Stable (named"Squeeze").  This wouldn't be a problem, except my previous computer was running Ubuntu Maverick--- a Debian-based distro, but one with newer but less stable (in my experience, and why I switched) versions of the software.

Generally, I don't care about running the latest and greatest. And for some applications such as Firefox or Thunderbird, newer versions are available. I also installed some non-free applications that I used, such as Skype or Flash. No problem.

Except for Shotwell, the photo manager. Ubuntu Maverick runs version 0.7.2, while Debian Squeeze ran 0.6.2. The latest version on the Shotwell web site is 0.10.1, and I didn't want to faff about installing newer libraries and risking problems with my new machine.

Since I have about 15,000 digital photos accumulated over the past 15+ years, I didn't want to lose this information.

So, I did some hacking and managed to downgrade the database using the following steps:
  1. Back up the newer shotwell database (usually in ~/.shotwell/data/photo.db).

  2. Copy the photos (usually in ~/Pictures) to the new computer, preserving the same locations. (If you're feeling adventurous, update the PhotoTable table in the database after importing it.)

  3. Copy the thumbnails from ~/.shotwell/thumbs to the new computer.

  4. Run Shotwell to create a new blank database, or create a new blank database in SQLite using the following:

    PRAGMA foreign_keys=OFF;
    BEGIN TRANSACTION;
    CREATE TABLE VersionTable (id INTEGER PRIMARY KEY, 
      schema_version INTEGER,
      app_version TEXT, user_data TEXT NULL);
    INSERT INTO "VersionTable" VALUES(1,7,'0.6.1',NULL);
    CREATE TABLE PhotoTable (id INTEGER PRIMARY KEY, 
      filename TEXT UNIQUE NOT NULL,
      width INTEGER, height INTEGER,
      filesize INTEGER,
      timestamp INTEGER, exposure_time INTEGER,
      orientation INTEGER, original_orientation INTEGER,
      import_id INTEGER, event_id INTEGER, transformations TEXT,
      md5 TEXT, thumbnail_md5 TEXT, exif_md5 TEXT,
      time_created INTEGER, flags INTEGER DEFAULT 0,
      file_format INTEGER DEFAULT 0, title TEXT, backlinks TEXT,
      time_reimported INTEGER, editable_id INTEGER DEFAULT -1);
    CREATE TABLE EventTable (id INTEGER PRIMARY KEY, name TEXT, 
      primary_photo_id INTEGER, time_created INTEGER);
    CREATE TABLE TagTable (id INTEGER PRIMARY KEY,
      name TEXT UNIQUE NOT NULL, photo_id_list TEXT,
      time_created INTEGER);
    CREATE INDEX PhotoEventIDIndex ON PhotoTable (event_id);
    COMMIT;
    
  5. Run the following script, updating the filenames of work72 and blank with the filenames of the newer shotwell database and blank older shotwell datebase:

    #!/bin/bash
    
    work72=photo72.db
    blank=photo61.db
    
    dump72=`tempfile --suffix .sql`
    
    for i in EventTable PhotoTable TagTable
    do
      echo ".dump $i"
    done |sqlite3 $work72 >  $dump72
    
    work61=`tempfile --suffix .db`
    
    cat $dump72 |sqlite3 $work61
    
    echo "
    BEGIN TRANSACTION;
    CREATE TABLE PhotoTableTemp (id INTEGER PRIMARY KEY, 
      filename TEXT UNIQUE NOT NULL,
      width INTEGER, height INTEGER,
      filesize INTEGER,
      timestamp INTEGER, exposure_time INTEGER,
      orientation INTEGER, original_orientation INTEGER,
      import_id INTEGER, event_id INTEGER, transformations TEXT,
      md5 TEXT, thumbnail_md5 TEXT, exif_md5 TEXT,
      time_created INTEGER, flags INTEGER DEFAULT 0,
      file_format INTEGER DEFAULT 0, title TEXT, backlinks TEXT,
      time_reimported INTEGER, editable_id INTEGER DEFAULT -1);
    INSERT INTO PhotoTableTemp SELECT id, filename, width, height,
      filesize, timestamp, exposure_time, orientation,
      original_orientation, import_id, event_id, transformations,
      md5, thumbnail_md5, exif_md5, time_created, flags,
      file_format, title, backlinks, time_reimported,
      editable_id FROM PhotoTable;
    DROP TABLE PhotoTable;
    CREATE TABLE PhotoTable (id INTEGER PRIMARY KEY, 
      filename TEXT UNIQUE NOT NULL,
      width INTEGER, height INTEGER,
      filesize INTEGER,
      timestamp INTEGER, exposure_time INTEGER,
      orientation INTEGER, original_orientation INTEGER,
      import_id INTEGER, event_id INTEGER, transformations TEXT,
      md5 TEXT, thumbnail_md5 TEXT, exif_md5 TEXT,
      time_created INTEGER, flags INTEGER DEFAULT 0,
      file_format INTEGER DEFAULT 0, title TEXT, backlinks TEXT,
      time_reimported INTEGER, editable_id INTEGER DEFAULT -1);
    INSERT INTO PhotoTable SELECT * FROM PhotoTableTemp;
    DROP TABLE PhotoTableTemp;
    COMMIT;" | sqlite3 $work61
    
    echo ".dump" |sqlite3 $work61 | \\
      grep -v ^CREATE |sqlite3 $blank
    
    cat /dev/null | sqlite3 $blank
    
    rm $dump72
    rm $work6

    Basically, 0.7 adds a new field (rating) to the PhotoTable table. Because SQLite's ALTER TABLE implementation does not (currently) allow the removal of fields, we have to create a new table and copy the old table to the new one, then delete the old one and re-copy the new table to the new old table. Messy, but it works.

  6. Copy the resulting database back to ~/.shotwell/data/photo.db.

  7. Run Shotwell. Well, not quite. You may get a segmentation fault. After some faffing about, it seems that after running Shotwell three or four times, it fixes whatever problems it has with the database and runs with the imported data.

    Had I more time, I would have found out what Shotwell did to the data or some other configuration setting to work with the data. (I spent way too much time experimenting with the data before realizing the various changes that I made had nothing to do with getting it to run. Had I more time, I would have compared the changes Shotwell made with the original data.)

You're mileage may vary. I'd like to hear if it works for you (and if you figure out what to do to the database so that Shotwell won't crash initially).

25 May 2011

Stupid Unix Tricks: Using the Gnome Screensaver with Xfce

I prefer to use the Xfce desktop instead of Gnome, though I use many Gnome applications from within Xfce. Unfortunately, Xfce (at least as distributed by Debian and Ubuntu) have hardcoded the use of xscreensaver. And as much as I respect JWZ's programming, the unlock screen looks like it's from the 1980s.

So I've hacked together the following workaround by creating wrappers for the xscreensaver commands:
  1. Create a file called /usr/local/bin/xscreensaver-command that contains the following:
    #!/bin/bash
    
    while [ "$1" != "" ]
    do
      arg=$1
      shift
    
      case "$arg" in
       -prefs)
         gnome-screensaver-preferences
       ;;
       -demo)
         if [[ "$1" =~ ^[0-9]+$ ]]; then
           num=$1
           shift
         else
           echo $1
         fi
         gnome-screensaver-preferences
       ;;
       -cycle)
         gnome-screensaver-command --cycle
       ;;
       -next)
         gnome-screensaver-command --cycle
       ;;
       -prev)
         gnome-screensaver-command --cycle
       ;;
       -lock)
         gnome-screensaver-command --lock
       ;;
       -activate)
         gnome-screensaver-command --activate
       ;;
       -deactivate)
         gnome-screensaver-command --deactivate
       ;;
       -time)
         gnome-screensaver-command --time
       ;;
       -exit)
         gnome-screensaver-command --exit
       ;;
       *)
         echo Unrecognized command: $arg
       ;;
      esac
    
    done
    
    
  2. Create a file called /usr/local/bin/xscreensaver-demo that contains the following:
    #!/bin/bash
    
    gnome-screensaver-preferences
    
    
  3. Create a file called /usr/local/share/applications/xscreensaver-properties.desktop that contains the following:
    [Desktop Entry]
    Exec=xscreensaver-demo
    Icon=Screensaver
    Terminal=false
    StartupNotify=true
    Name=Screensaver
    Comment=Change screensaver properties
    Type=Application
    Categories=Settings;DesktopSettings;Security;X-XFCE;
    
    
    Change the Icon entry to the name of a screensaver icon in /usr/share/icons or /usr/local/share/icons. (I downloaded a Gnome Screensaver.svg icon from here.)

I've been using this wrapper for over three years on Debian and Ubuntu distributions with no problems. (I should note, however, that Gnome Screensaver uses the Gnome Power Manager rather than Xfce Power Manager--- as I've been using the former, this has not been an issue for me.)

31 March 2011

Election Indeterminacy: Ideas for Alternative Election Coverage

While helping out at Leith FM studios during the general election in May, it occurred to me that, despite planning, everything seemed a bit random. At the same time, I was listening to recordings of John Cage's Indeterminacy, and thought that perhaps one can combine the two into a performance piece for radio about an election in progress.

The basic idea: in the studios are the players: elected officials from at least two parties, a political activist, a newsreader (to read wire stores), a reporter at a polling place who can be reached by phone, and a comedian. A moderator would use some method to randomly choose one of the players to speak about anything for no more than one minute. Most players are free to comment about what previous players said, or address general questions to the next player to speak, though that player is only determined afterwards. (The newsreader would be confined to reading only wire stories.)

My "hypothesis" is that the casual radio listener would not be able to distinguish this performance from actual election coverage that one might hear on other radio stations.

This could be extended to a live performance in front of an audience, allowing audience members to queue up to be players. (An audience member would only have one turn.)

To make this more entertaining for a live audience, one might also include a jazz band as another "player" to improvise.

(With the Scottish parliamentary elections coming up in May, I thought about organising this as an event, but it's too short notice and I am too busy with other things to do it properly, alas.)

Postscript: this technique could be applied to news coverage of any "breaking story" besides elections.

30 January 2009

A bash script implementation of the trashcan spec

I hacked together a command-line implementation of the Freedesktop.org Trash Can Specification in Bash 3.2 one evening:

#!/bin/bash

# trash (version 0.2.1)
# Robert Rothenberg

# This is a bash script implementation of the FreeDesktop.org Trash
# Specification.

# 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 2 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.

if [ -z "$1" ]; then
echo "Usage: trash FILE..."
exit 1
fi

# sed script to encode filenames

sedscript='s/ /%20/g
s/!/%21/g
s/"/%22/g
s/\#/%23/g
s/\$/%24/g
s/\&/%26/g
s/'\''/%27/g
s/(/%28/g
s/)/%29/g
s/\*/%2a/g
s/+/%2b/g
s/,/%2c/g
s/-/%2d/g
s/:/%3a/g
s/;/%3b/g
s//%3e/g
s/?/%3f/g
s/@/%40/g
s/\[/%5b/g
s/\\/%5c/g
s/\]/%5d/g
s/\^/%5e/g
s/_/%5f/g
s/`/%60/g
s/{/%7b/g
s/|/%7c/g
s/}/%7d/g
s/~/%7e/g
s/ /%09/g'

function url_encode {
echo $1 |sed -e "$sedscript"
}


function get_trashdir {
mounts=`cat /etc/fstab |grep -v \# |awk '{print $2}'`
base=/

if [ "$EUID" != "0" ]; then
mounts="$HOME $mounts"
fi

for i in $mounts
do
if [[ $1 =~ ^$i ]]
then
if [[ $i =~ ^$base ]]
then
base=$i
fi
fi
done

if [ "$base" != "$HOME" ]; then
trashdir="$base/.Trash/$UID"
if [ ! -d "$trashdir" ]; then
trashdir="$base/.Trash-$UID"
fi

mkdir -p "$trashdir"
if [ "$?" != "0" ]; then
base=$HOME
fi
fi

if [ "$base" == "$HOME" ]; then
base=$XDG_DATA_HOME
if [ -z "$base" ]; then
base="$HOME/.local/share/"
fi
trashdir="$base/Trash"
fi

echo $trashdir
}

for f in "$@"
do

# get full pathname of file
filename=$(readlink -f "$f")
dir=${filename%/*}

trashdir=`get_trashdir "$dir"`

mkdir -p "$trashdir/files"
if [ "$?" != "0" ]; then
echo "Unable to write to $trashdir" 1>&2
exit 2
fi

mkdir -p "$trashdir/info"
if [ "$?" != "0" ]; then
echo "Unable to write to $trashdir" 1>&2
exit 2
fi

trashname="${filename##*/}"
origname="${trashname%%.*}"
ext=".${trashname##*.}"
if [ "$ext" == ".$trashname" ]; then
ext=""
fi

cnt=1
while [ -e "$trashdir/files/$trashname" ] || \
[ -e "$trashdir/info/$trashname.trashinfo" ]; do
trashname="${origname}_${cnt}${ext}"
let cnt=cnt+1
done

deletedfile="$trashdir/files/$trashname"
deletedinfo="$trashdir/info/$trashname.trashinfo"
canon=`url_encode $filename`

cat > "$deletedinfo" <<END
[Trash Info]
Path=$canon
DeletionDate=`date +"%FT%H:%M:%S"`
END

mv -v "$filename" "$deletedfile"
done

exit 0

It's quick-and-dirty hack, so I won't vouch for how well it follows the spec, or even works. But it was interesting to see how closely it could be implemented in Bash using as few external commands as possible: readlink, mv, mkdir, cat, awk, sed (the URL-encoding routine comes from here).

If you're looking for a proper implementation, see trash-cli here.

UPDATE (2 March 2011) An updated version of this script is now on GitHub here.