<!--{{{-->
<link rel='alternate' type='application/rss+xml' title='RSS' href='index.xml'/>
<!--}}}-->
Background: #fff
Foreground: #000
PrimaryPale: #8cf
PrimaryLight: #18f
PrimaryMid: #04b
PrimaryDark: #014
SecondaryPale: #ffc
SecondaryLight: #fe8
SecondaryMid: #db4
SecondaryDark: #841
TertiaryPale: #eee
TertiaryLight: #ccc
TertiaryMid: #999
TertiaryDark: #666
Error: #f88
/*{{{*/
body {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}

a {color:[[ColorPalette::PrimaryMid]];}
a:hover {background-color:[[ColorPalette::PrimaryMid]]; color:[[ColorPalette::Background]];}
a img {border:0;}

h1,h2,h3,h4,h5,h6 {color:[[ColorPalette::SecondaryDark]]; background:transparent;}
h1 {border-bottom:2px solid [[ColorPalette::TertiaryLight]];}
h2,h3 {border-bottom:1px solid [[ColorPalette::TertiaryLight]];}

.button {color:[[ColorPalette::PrimaryDark]]; border:1px solid [[ColorPalette::Background]];}
.button:hover {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::SecondaryLight]]; border-color:[[ColorPalette::SecondaryMid]];}
.button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::SecondaryDark]];}

.header {background:[[ColorPalette::PrimaryMid]];}
.headerShadow {color:[[ColorPalette::Foreground]];}
.headerShadow a {font-weight:normal; color:[[ColorPalette::Foreground]];}
.headerForeground {color:[[ColorPalette::Background]];}
.headerForeground a {font-weight:normal; color:[[ColorPalette::PrimaryPale]];}

.tabSelected{color:[[ColorPalette::PrimaryDark]];
	background:[[ColorPalette::TertiaryPale]];
	border-left:1px solid [[ColorPalette::TertiaryLight]];
	border-top:1px solid [[ColorPalette::TertiaryLight]];
	border-right:1px solid [[ColorPalette::TertiaryLight]];
}
.tabUnselected {color:[[ColorPalette::Background]]; background:[[ColorPalette::TertiaryMid]];}
.tabContents {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::TertiaryPale]]; border:1px solid [[ColorPalette::TertiaryLight]];}
.tabContents .button {border:0;}

#sidebar {}
#sidebarOptions input {border:1px solid [[ColorPalette::PrimaryMid]];}
#sidebarOptions .sliderPanel {background:[[ColorPalette::PrimaryPale]];}
#sidebarOptions .sliderPanel a {border:none;color:[[ColorPalette::PrimaryMid]];}
#sidebarOptions .sliderPanel a:hover {color:[[ColorPalette::Background]]; background:[[ColorPalette::PrimaryMid]];}
#sidebarOptions .sliderPanel a:active {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::Background]];}

.wizard {background:[[ColorPalette::PrimaryPale]]; border:1px solid [[ColorPalette::PrimaryMid]];}
.wizard h1 {color:[[ColorPalette::PrimaryDark]]; border:none;}
.wizard h2 {color:[[ColorPalette::Foreground]]; border:none;}
.wizardStep {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];
	border:1px solid [[ColorPalette::PrimaryMid]];}
.wizardStep.wizardStepDone {background:[[ColorPalette::TertiaryLight]];}
.wizardFooter {background:[[ColorPalette::PrimaryPale]];}
.wizardFooter .status {background:[[ColorPalette::PrimaryDark]]; color:[[ColorPalette::Background]];}
.wizard .button {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryLight]]; border: 1px solid;
	border-color:[[ColorPalette::SecondaryPale]] [[ColorPalette::SecondaryDark]] [[ColorPalette::SecondaryDark]] [[ColorPalette::SecondaryPale]];}
.wizard .button:hover {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Background]];}
.wizard .button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::Foreground]]; border: 1px solid;
	border-color:[[ColorPalette::PrimaryDark]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryDark]];}

#messageArea {border:1px solid [[ColorPalette::SecondaryMid]]; background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]];}
#messageArea .button {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::SecondaryPale]]; border:none;}

.popupTiddler {background:[[ColorPalette::TertiaryPale]]; border:2px solid [[ColorPalette::TertiaryMid]];}

.popup {background:[[ColorPalette::TertiaryPale]]; color:[[ColorPalette::TertiaryDark]]; border-left:1px solid [[ColorPalette::TertiaryMid]]; border-top:1px solid [[ColorPalette::TertiaryMid]]; border-right:2px solid [[ColorPalette::TertiaryDark]]; border-bottom:2px solid [[ColorPalette::TertiaryDark]];}
.popup hr {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::PrimaryDark]]; border-bottom:1px;}
.popup li.disabled {color:[[ColorPalette::TertiaryMid]];}
.popup li a, .popup li a:visited {color:[[ColorPalette::Foreground]]; border: none;}
.popup li a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; border: none;}
.popup li a:active {background:[[ColorPalette::SecondaryPale]]; color:[[ColorPalette::Foreground]]; border: none;}
.popupHighlight {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
.listBreak div {border-bottom:1px solid [[ColorPalette::TertiaryDark]];}

.tiddler .defaultCommand {font-weight:bold;}

.shadow .title {color:[[ColorPalette::TertiaryDark]];}

.title {color:[[ColorPalette::SecondaryDark]];}
.subtitle {color:[[ColorPalette::TertiaryDark]];}

.toolbar {color:[[ColorPalette::PrimaryMid]];}
.toolbar a {color:[[ColorPalette::TertiaryLight]];}
.selected .toolbar a {color:[[ColorPalette::TertiaryMid]];}
.selected .toolbar a:hover {color:[[ColorPalette::Foreground]];}

.tagging, .tagged {border:1px solid [[ColorPalette::TertiaryPale]]; background-color:[[ColorPalette::TertiaryPale]];}
.selected .tagging, .selected .tagged {background-color:[[ColorPalette::TertiaryLight]]; border:1px solid [[ColorPalette::TertiaryMid]];}
.tagging .listTitle, .tagged .listTitle {color:[[ColorPalette::PrimaryDark]];}
.tagging .button, .tagged .button {border:none;}

.footer {color:[[ColorPalette::TertiaryLight]];}
.selected .footer {color:[[ColorPalette::TertiaryMid]];}

.sparkline {background:[[ColorPalette::PrimaryPale]]; border:0;}
.sparktick {background:[[ColorPalette::PrimaryDark]];}

.error, .errorButton {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Error]];}
.warning {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryPale]];}
.lowlight {background:[[ColorPalette::TertiaryLight]];}

.zoomer {background:none; color:[[ColorPalette::TertiaryMid]]; border:3px solid [[ColorPalette::TertiaryMid]];}

.imageLink, #displayArea .imageLink {background:transparent;}

.annotation {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; border:2px solid [[ColorPalette::SecondaryMid]];}

.viewer .listTitle {list-style-type:none; margin-left:-2em;}
.viewer .button {border:1px solid [[ColorPalette::SecondaryMid]];}
.viewer blockquote {border-left:3px solid [[ColorPalette::TertiaryDark]];}

.viewer table, table.twtable {border:2px solid [[ColorPalette::TertiaryDark]];}
.viewer th, .viewer thead td, .twtable th, .twtable thead td {background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::Background]];}
.viewer td, .viewer tr, .twtable td, .twtable tr {border:1px solid [[ColorPalette::TertiaryDark]];}

.viewer pre {border:1px solid [[ColorPalette::SecondaryLight]]; background:[[ColorPalette::SecondaryPale]];}
.viewer code {color:[[ColorPalette::SecondaryDark]];}
.viewer hr {border:0; border-top:dashed 1px [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::TertiaryDark]];}

.highlight, .marked {background:[[ColorPalette::SecondaryLight]];}

.editor input {border:1px solid [[ColorPalette::PrimaryMid]];}
.editor textarea {border:1px solid [[ColorPalette::PrimaryMid]]; width:100%;}
.editorFooter {color:[[ColorPalette::TertiaryMid]];}

#backstageArea {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::TertiaryMid]];}
#backstageArea a {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
#backstageArea a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; }
#backstageArea a.backstageSelTab {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
#backstageButton a {background:none; color:[[ColorPalette::Background]]; border:none;}
#backstageButton a:hover {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
#backstagePanel {background:[[ColorPalette::Background]]; border-color: [[ColorPalette::Background]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]];}
.backstagePanelFooter .button {border:none; color:[[ColorPalette::Background]];}
.backstagePanelFooter .button:hover {color:[[ColorPalette::Foreground]];}
#backstageCloak {background:[[ColorPalette::Foreground]]; opacity:0.6; filter:'alpha(opacity:60)';}
/*}}}*/
/*{{{*/
* html .tiddler {height:1%;}

body {font-size:.75em; font-family:arial,helvetica; margin:0; padding:0;}

h1,h2,h3,h4,h5,h6 {font-weight:bold; text-decoration:none;}
h1,h2,h3 {padding-bottom:1px; margin-top:1.2em;margin-bottom:0.3em;}
h4,h5,h6 {margin-top:1em;}
h1 {font-size:1.35em;}
h2 {font-size:1.25em;}
h3 {font-size:1.1em;}
h4 {font-size:1em;}
h5 {font-size:.9em;}

hr {height:1px;}

a {text-decoration:none;}

dt {font-weight:bold;}

ol {list-style-type:decimal;}
ol ol {list-style-type:lower-alpha;}
ol ol ol {list-style-type:lower-roman;}
ol ol ol ol {list-style-type:decimal;}
ol ol ol ol ol {list-style-type:lower-alpha;}
ol ol ol ol ol ol {list-style-type:lower-roman;}
ol ol ol ol ol ol ol {list-style-type:decimal;}

.txtOptionInput {width:11em;}

#contentWrapper .chkOptionInput {border:0;}

.externalLink {text-decoration:underline;}

.indent {margin-left:3em;}
.outdent {margin-left:3em; text-indent:-3em;}
code.escaped {white-space:nowrap;}

.tiddlyLinkExisting {font-weight:bold;}
.tiddlyLinkNonExisting {font-style:italic;}

/* the 'a' is required for IE, otherwise it renders the whole tiddler in bold */
a.tiddlyLinkNonExisting.shadow {font-weight:bold;}

#mainMenu .tiddlyLinkExisting,
	#mainMenu .tiddlyLinkNonExisting,
	#sidebarTabs .tiddlyLinkNonExisting {font-weight:normal; font-style:normal;}
#sidebarTabs .tiddlyLinkExisting {font-weight:bold; font-style:normal;}

.header {position:relative;}
.header a:hover {background:transparent;}
.headerShadow {position:relative; padding:4.5em 0em 1em 1em; left:-1px; top:-1px;}
.headerForeground {position:absolute; padding:4.5em 0em 1em 1em; left:0px; top:0px;}

.siteTitle {font-size:3em;}
.siteSubtitle {font-size:1.2em;}

#mainMenu {position:absolute; left:0; width:10em; text-align:right; line-height:1.6em; padding:1.5em 0.5em 0.5em 0.5em; font-size:1.1em;}

#sidebar {position:absolute; right:3px; width:16em; font-size:.9em;}
#sidebarOptions {padding-top:0.3em;}
#sidebarOptions a {margin:0em 0.2em; padding:0.2em 0.3em; display:block;}
#sidebarOptions input {margin:0.4em 0.5em;}
#sidebarOptions .sliderPanel {margin-left:1em; padding:0.5em; font-size:.85em;}
#sidebarOptions .sliderPanel a {font-weight:bold; display:inline; padding:0;}
#sidebarOptions .sliderPanel input {margin:0 0 .3em 0;}
#sidebarTabs .tabContents {width:15em; overflow:hidden;}

.wizard {padding:0.1em 1em 0em 2em;}
.wizard h1 {font-size:2em; font-weight:bold; background:none; padding:0em 0em 0em 0em; margin:0.4em 0em 0.2em 0em;}
.wizard h2 {font-size:1.2em; font-weight:bold; background:none; padding:0em 0em 0em 0em; margin:0.4em 0em 0.2em 0em;}
.wizardStep {padding:1em 1em 1em 1em;}
.wizard .button {margin:0.5em 0em 0em 0em; font-size:1.2em;}
.wizardFooter {padding:0.8em 0.4em 0.8em 0em;}
.wizardFooter .status {padding:0em 0.4em 0em 0.4em; margin-left:1em;}
.wizard .button {padding:0.1em 0.2em 0.1em 0.2em;}

#messageArea {position:fixed; top:2em; right:0em; margin:0.5em; padding:0.5em; z-index:2000; _position:absolute;}
.messageToolbar {display:block; text-align:right; padding:0.2em 0.2em 0.2em 0.2em;}
#messageArea a {text-decoration:underline;}

.tiddlerPopupButton {padding:0.2em 0.2em 0.2em 0.2em;}
.popupTiddler {position: absolute; z-index:300; padding:1em 1em 1em 1em; margin:0;}

.popup {position:absolute; z-index:300; font-size:.9em; padding:0; list-style:none; margin:0;}
.popup .popupMessage {padding:0.4em;}
.popup hr {display:block; height:1px; width:auto; padding:0; margin:0.2em 0em;}
.popup li.disabled {padding:0.4em;}
.popup li a {display:block; padding:0.4em; font-weight:normal; cursor:pointer;}
.listBreak {font-size:1px; line-height:1px;}
.listBreak div {margin:2px 0;}

.tabset {padding:1em 0em 0em 0.5em;}
.tab {margin:0em 0em 0em 0.25em; padding:2px;}
.tabContents {padding:0.5em;}
.tabContents ul, .tabContents ol {margin:0; padding:0;}
.txtMainTab .tabContents li {list-style:none;}
.tabContents li.listLink { margin-left:.75em;}

#contentWrapper {display:block;}
#splashScreen {display:none;}

#displayArea {margin:1em 17em 0em 14em;}

.toolbar {text-align:right; font-size:.9em;}

.tiddler {padding:1em 1em 0em 1em;}

.missing .viewer,.missing .title {font-style:italic;}

.title {font-size:1.6em; font-weight:bold;}

.missing .subtitle {display:none;}
.subtitle {font-size:1.1em;}

.tiddler .button {padding:0.2em 0.4em;}

.tagging {margin:0.5em 0.5em 0.5em 0; float:left; display:none;}
.isTag .tagging {display:block;}
.tagged {margin:0.5em; float:right;}
.tagging, .tagged {font-size:0.9em; padding:0.25em;}
.tagging ul, .tagged ul {list-style:none; margin:0.25em; padding:0;}
.tagClear {clear:both;}

.footer {font-size:.9em;}
.footer li {display:inline;}

.annotation {padding:0.5em; margin:0.5em;}

* html .viewer pre {width:99%; padding:0 0 1em 0;}
.viewer {line-height:1.4em; padding-top:0.5em;}
.viewer .button {margin:0em 0.25em; padding:0em 0.25em;}
.viewer blockquote {line-height:1.5em; padding-left:0.8em;margin-left:2.5em;}
.viewer ul, .viewer ol {margin-left:0.5em; padding-left:1.5em;}

.viewer table, table.twtable {border-collapse:collapse; margin:0.8em 1.0em;}
.viewer th, .viewer td, .viewer tr,.viewer caption,.twtable th, .twtable td, .twtable tr,.twtable caption {padding:3px;}
table.listView {font-size:0.85em; margin:0.8em 1.0em;}
table.listView th, table.listView td, table.listView tr {padding:0px 3px 0px 3px;}

.viewer pre {padding:0.5em; margin-left:0.5em; font-size:1.2em; line-height:1.4em; overflow:auto;}
.viewer code {font-size:1.2em; line-height:1.4em;}

.editor {font-size:1.1em;}
.editor input, .editor textarea {display:block; width:100%; font:inherit;}
.editorFooter {padding:0.25em 0em; font-size:.9em;}
.editorFooter .button {padding-top:0px; padding-bottom:0px;}

.fieldsetFix {border:0; padding:0; margin:1px 0px 1px 0px;}

.sparkline {line-height:1em;}
.sparktick {outline:0;}

.zoomer {font-size:1.1em; position:absolute; overflow:hidden;}
.zoomer div {padding:1em;}

* html #backstage {width:99%;}
* html #backstageArea {width:99%;}
#backstageArea {display:none; position:relative; overflow: hidden; z-index:150; padding:0.3em 0.5em 0.3em 0.5em;}
#backstageToolbar {position:relative;}
#backstageArea a {font-weight:bold; margin-left:0.5em; padding:0.3em 0.5em 0.3em 0.5em;}
#backstageButton {display:none; position:absolute; z-index:175; top:0em; right:0em;}
#backstageButton a {padding:0.1em 0.4em 0.1em 0.4em; margin:0.1em 0.1em 0.1em 0.1em;}
#backstage {position:relative; width:100%; z-index:50;}
#backstagePanel {display:none; z-index:100; position:absolute; margin:0em 3em 0em 3em; padding:1em 1em 1em 1em;}
.backstagePanelFooter {padding-top:0.2em; float:right;}
.backstagePanelFooter a {padding:0.2em 0.4em 0.2em 0.4em;}
#backstageCloak {display:none; z-index:20; position:absolute; width:100%; height:100px;}

.whenBackstage {display:none;}
.backstageVisible .whenBackstage {display:block;}
/*}}}*/
/***
StyleSheet for use when a translation requires any css style changes.
This StyleSheet can be used directly by languages such as Chinese, Japanese and Korean which need larger font sizes.
***/
/*{{{*/
body {font-size:0.8em;}
#sidebarOptions {font-size:1.05em;}
#sidebarOptions a {font-style:normal;}
#sidebarOptions .sliderPanel {font-size:0.95em;}
.subtitle {font-size:0.8em;}
.viewer table.listView {font-size:0.95em;}
/*}}}*/
/*{{{*/
@media print {
#mainMenu, #sidebar, #messageArea, .toolbar, #backstageButton, #backstageArea {display: none ! important;}
#displayArea {margin: 1em 1em 0em 1em;}
/* Fixes a feature in Firefox 1.5.0.2 where print preview displays the noscript content */
noscript {display:none;}
}
/*}}}*/
<!--{{{-->
<div class='header' macro='gradient vert [[ColorPalette::PrimaryLight]] [[ColorPalette::PrimaryMid]]'>
<div class='headerShadow'>
<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>&nbsp;
<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
</div>
<div class='headerForeground'>
<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>&nbsp;
<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
</div>
</div>
<div id='mainMenu' refresh='content' tiddler='MainMenu'></div>
<div id='sidebar'>
<div id='sidebarOptions' refresh='content' tiddler='SideBarOptions'></div>
<div id='sidebarTabs' refresh='content' force='true' tiddler='SideBarTabs'></div>
</div>
<div id='displayArea'>
<div id='messageArea'></div>
<div id='tiddlerDisplay'></div>
</div>
<!--}}}-->
<!--{{{-->
<div class='toolbar' macro='toolbar closeTiddler closeOthers +editTiddler > fields syncing permalink references jump'></div>
<div class='title' macro='view title'></div>
<div class='subtitle'><span macro='view modifier link'></span>, <span macro='view modified date'></span> (<span macro='message views.wikified.createdPrompt'></span> <span macro='view created date'></span>)</div>
<div class='tagging' macro='tagging'></div>
<div class='tagged' macro='tags'></div>
<div class='viewer' macro='view text wikified'></div>
<div class='tagClear'></div>
<!--}}}-->
<!--{{{-->
<div class='toolbar' macro='toolbar +saveTiddler -cancelTiddler deleteTiddler'></div>
<div class='title' macro='view title'></div>
<div class='editor' macro='edit title'></div>
<div macro='annotations'></div>
<div class='editor' macro='edit text'></div>
<div class='editor' macro='edit tags'></div><div class='editorFooter'><span macro='message views.editor.tagPrompt'></span><span macro='tagChooser'></span></div>
<!--}}}-->
To get started with this blank TiddlyWiki, you'll need to modify the following tiddlers:
* SiteTitle & SiteSubtitle: The title and subtitle of the site, as shown above (after saving, they will also appear in the browser title bar)
* MainMenu: The menu (usually on the left)
* DefaultTiddlers: Contains the names of the tiddlers that you want to appear when the TiddlyWiki is opened
You'll also need to enter your username for signing your edits: <<option txtUserName>>
These InterfaceOptions for customising TiddlyWiki are saved in your browser

Your username for signing your edits. Write it as a WikiWord (eg JoeBloggs)

<<option txtUserName>>
<<option chkSaveBackups>> SaveBackups
<<option chkAutoSave>> AutoSave
<<option chkRegExpSearch>> RegExpSearch
<<option chkCaseSensitiveSearch>> CaseSensitiveSearch
<<option chkAnimate>> EnableAnimations

----
Also see AdvancedOptions
<<importTiddlers>>
[img[Tom Purl's Avatar|http://www.tompurl.com-a.googlepages.com/ddr_avatar.jpg]]

My name is Tom Purl and I am a father, husband, and a bunch of other things.  Professionally, I'm a middleware administrator specializing in BPM and Content Management, and my hobbies include writing, playing on my computer, and cooking.  

I promise that, in real life, I'm not nearly as lame as this short description would make you think.  

If you would like to contact me for any reason, then please feel free to send me an e-mail message using the following address:
* tom@tompurl.com

Here's some more information that people seem to care about these days:

* My [[Blog Roll|http://www.bloglines.com/public/tompurl]]
* My [[Technorati Profile]]
* My [[Bookmarks|http://del.icio.us/tpurl]]
I'm a "middleware systems administrator" for a living (I know, how do I live with all of the excitement), which means that I spend a lot of time behind a computer.  Over the years, I've put together a list of guiding principles when it comes to maintaining my "toolbox" of work applications:

# Minimize the number of tools that you use
#* I try to squeeze as much usefulness as possible out of each application.  For example, I use Vim to do everything from Java programming to blog documentation authoring.
# Be open to new software since it can make your life much easier

I therefore try a //lot// of new software, but I only end up using a small fraction.  That fraction is composed of applications that are so good that I like to share its contents with others.  

This is a list of my favorite new (to me) applications of 2007.  I highly recommend each one, and have written a review of each application on my web page.  

<<forEachTiddler
 where
 'tiddler.tags.contains("best-apps-of-2007")'
 sortBy
 'tiddler.created'
 descending
 write
 '(index < 10) ? "* [["+tiddler.title+"]]\n" : ""'>>
//This article is part of a series called "[[Best Apps Of 2007]]".//

!!What Is It?

I have become a big fan of the [[Getting Things Done|http://en.wikipedia.org/wiki/Gtd]] process, otherwise known as GTD.  Basically, it's a system for organizing all of your tasks that is simple, flexible, and very popular right now.

One of the key tenets of GTD is that you need one or more "trusted systems" that you can use to track things like ideas, appointments, and action items.  After finishing this book, I realized that I already had trusted systems for ideas and appointments ([[potwiki|http://www.vim.org/scripts/script.php?script_id=1018]] Vim wiki and Lotus Notes Calendar, respectively), but nothing to help me manage my actions items and projects from start to finish.  

I therefore tried //lots// of different tools to help me manage my action items, including the following:

* [[GTDTiddlyWiki|http://shared.snapgrid.com/]]
* [[php-gtd|http://www.gtd-php.com/]]
* [[todo.txt|http://todotxt.com/]]
* [[PyGTD|http://96db.com/pyGTD/]]
* etc.

All of these tools certainly are very useful, and they all have very large user bases, but none of them worked for me because they all required too much "housekeeping".  For example, another tenet of GTD is that you always have a "next actions" list, which is a list of one or more actions from each project that you need to accomplish "next" to keep the project moving forward.  Most GTD programs assume that you'll compile this list yourself each time that you complete an action, but I find this task to be tedious and time-consuming, especially when you're trying to complete a bunch of action items in a hurry.  I wanted a program that would handle more of the simple logic so I could focus on my actions, not operating the program that organizes them.

For me, this problem was solved when I found [[ThinkingRock|http://todo]].  It is a robust, simple, and stable program that is designed to be used with a GTD system, and it has been incredibly valuable to me since the day I started using it.

!!What's So Cool About It?

!!!It's Smart

Like I was saying above, the killer feature for me is that ThinkingRock handles a lot of simple logic for me, such as automatically updating my "next actions" list when I complete an action.  Here's an example:

Let's say that I create a new project called "cleaning the garage".  This project would have the following steps:

# Pick up all trash
# Store or throw away all loose items
# wash floor of garage

After creating this project in ThinkingRock, it would look like this:

[img[project example|http://www.tompurl.com/thinkingrock-garage-project-1.png]]

Note the following:

# The only tasks that has the "running man" icon next to it is the first task.  This signifies that the task has a status of "Do ASAP", which means that it will appear on my "Next Actions" list.
# The tasks are in the order of when I would like to perform them.

Assuming that this is my only project, this is how my "Next Actions" list would look:

[img[project example|http://www.tompurl.com/thinkingrock-garage-project-2.png]]

I can now only focus on the next necessary action for cleaning my garage.  This feature is nice, but what's even nicer is that when I'm done with that first task, ThinkingRock automatically the status of the next task ("Store or throw away all loose items") to "Do ASAP", meaning that it's now on my "Next Actions" list.  I can now move on to the next task without thinking about either my project or ThinkingRock.

!!!GTD-Centric Workflow

Another cool thing about ThinkingRock is that it is build around a GTD-centric workflow.  This workflow has the following steps:

# Collecting your thoughts
# Processing your thoughts
# Processing Action List

ThinkingRock has some very robust interfaces for accomplishing each of these tasks, and makes it very easy to take ideas and convert them into action items.  

In addition to providing this workflow, however, ThinkingRock also makes it very easy to circumvent it if you want to create your projects and tasks in a less-structured way.  For example, your projects don't //have// to start their lives as "collected thoughts".  You can just right click on the Project node of the Projects view and choose "New Project".

!!!Extends GTD In Intelligent Ways

Yet another nice thing about ThinkingRock is that it adds some nice features to the GTD process that can make you life much easier.  For example, one rule of GTD is that if a action item takes more than two minutes to complete, it should be added to a project.  In most cases, I agree that a long-running task should probably belong to some sort of project, but it's certainly not always the case.  Most GTD-centric apps though enforce this rule, however.  If you want to have a "bucket" for action items that don't fit in a particular project, then you typically end up creating a "Non-Project" project.  This works for a lot of people, but it certainly is ugly.

ThinkingRock, on the other hand, allows you to create action items that aren't associated with a task.  It also lets you do some other non-GTD things, such as the following:

# Assigning a "priority" to an action item
# Assigning time criterion to an action item

These nice little features really make the app easier to use, whether or not you're a strict GTD-er.

!!!Cross-Platform And Free

Not only does ThinkingRock run on any platform that supports the JRE (i.e. 99.999% of desktop operating systems), but it's also financially free //and// [[free software|http://en.wikipedia.org/wiki/Free_software]].  The authors do take donations, however, and since I received so much utility from this application, I was happy to do so.

!!Gripes

The only real gripe that I have with this program is that it can be a resource hog.  This problem can be controlled somewhat, but it has to be considered.  It runs relatively well on my laptop (2 GB of RAM with a dual-core chip), but it's much slower on my desktop machine (512 MB of RAM with a 1.6 ghz Sempron chip).

!!Conclusion

Not only is ThinkingRock the best GTD-centric application available, but it's also cross-platform and free.  It's made me //much// more efficient, effective, and focused, and I //highly// recommend it to anyone who's looking for a program to help them implement a GTD process.
//This article is part of a series called "[[Best Apps Of 2007]]".//

Out of all of the new applications that I started using in 2007, [[Total Commander|http://www.ghisler.com/]] is by ''far'' the most boring and old-fashioned.  However, it's also probably ''the most valuable when it comes to programming and sysadmin work'', so I highly recommend it.

!!What Is It?

Simply put, Total Commander is a two-paned file explorer.  It is similar to the now-discontinued [[Norton Commander|http://en.wikipedia.org/wiki/Norton_Commander]] on Windows, and is still under very active development.

Here's a quick screen shot:

[img[total command screen shot|http://www.tompurl.com/total-command-screenshot-web.png]]

I know, this is still incredibly boring, but ''if you're a programmer or a sysadmin, you'll want to stay with me''.

!!What's So Cool About It?

First, I guess I never realized this before I started using a two-paned file explorer, but I copy and move a //lot// of files around.  This process used to involve two Windows Explorer windows or {{{xcopy}}}, neither of which is very fast in my opinion.  Now I can move files around very quickly with a trivial amount of effort.

Next, ''there are a //ton// of plugins available for Total Commander'' that mostly allow you to perform "transformations" on a file or folder when you move it from one directory to another.  My most-used set of plugins are the "Packer" plugins, which allow you to compress a file or folder on-the-fly as you move it to a new directory.  This interface for compressing files is //much// faster and more intuitive than either the WinZip GUI interface or a command-line interface.

Finally, what I really like about Total Commander is that it couples a ''rich GUI interface with a very robust keyboard interface''.  I've even gotten to the point where I only need to use the keyboard for 98% of the operations that I perform using Total Commander.  This feature makes me //much// more efficient.  For the other 2% of operations that aren't very familiar to me, however, it's nice to have a simple and intuitive GUI interface. 

!!Conclusion

Total Commander is an incredibly useful application for people who spend a lot of time moving and manipulating files and folders on Windows.  It's small, stable, robust, and actively developed.  It definitely made me more efficient and effective in 2007, and I highly recommend it.
//This article is part of a series called "[[Best Apps Of 2007]]".//

Out of all of the great new applications that I started using in 2007, the only one that I would take with me to the proverbial desert island is [[ScrapBook|http://amb.vis.ne.jp/mozilla/scrapbook/]].  I honestly don't know how I used to live without this little-known plugin for Firefox.

!!What Is It?

The ScrapBook plugin for Firefox basically allows you to do the following things:

# Save an offline copy of a web page, including offline copies of the linked-to pages if you prefer.
# Make annotations on the saved web pages.

From a 30,000 foot view, this is all the app really does.  This might not seem very interesting, and tools like [[httrack|http://www.httrack.com/]] have had this functionality for a long time.  However, where ScrapBook really shines is in it's simple-yet-powerful user interface.  For example, you can capture the following types of web content using ScrapBook:

* Whole web pages
* Pages that are referenced by links on the current page
* Fragments of your current page
* Child links of all of the above, down to 5 levels

ScrapBook does all of this from within Firefox, using simple right-click or menu commands.

!!What's So Cool About It

There are basically two ways that I use ScrapBook

# I use it to capture, store, and organize web content that I can quickly search and retrieve later
# I use it as an offline "internet newspaper"

!!!Archived Web Content Library

I'm a sysadmin for a living, and I play with computers a bit in my free time, so I do a lot of research on the web.  In the old days, when I found web content that was truly useful, I would bookmark the site using [[del.icio.us|http://del.icio.us]] and tag it with what I thought would be important keywords.  If I needed the content again later, I would do a tag-based search in del.icio.us.  This type of search would typically return more than one link, so I would then manually choose the link that I needed.

This process worked pretty well for me, but it had the following disadvantages:

# ''It's not a full-content search''.  It's just a keyword search, and I might not tag the bookmark with the right tags.
#* Let's say that I read an article on installing [[Tomcat|http://tomcat.apache.org/]] on Ubuntu Linux, which happened to have a great section on managing JRE's on Ubuntu.  Now let's say that when I tagged the article, I only used the keywords "ubuntu" and "tomcat".  6 months from now, if I tried to do a tag-based search for all of my bookmarks that include "java" and "ubuntu" tags, I wouldn't find the article I actually needed.
# ''The web content might disappear''.
#* I think a lot of us have experienced this.  We find a great article or blog entry, bookmark, and then find that it no longer exists the next time you need it.
# ''It's relatively slow''.
#* Tag-based searches are non-intuitive and require more thinking, which makes them slow compared to searching based on a web page's content.  

Now, instead of using bookmarks to store references to important web sites, I use ScrapBook to capture web pages that I find to be useful.  There's no tagging required; just right-click, capture, and move on to the next web page.  When I want to use the content again later, I use ScrapBook's excellent full-text search engine, which is both intuitive for my brain and fast on my computer.  The search engine returns all of the possible matches using an bookmark-like interface, which is also very easy to use.  This new process has ''revolutionized how I store and retrieve valuable web content'', and has ''saved me tons of time and effort''.  This is true in both professional and personal contexts.

!!Offline Internet Newspaper

I'm a big fan of online RSS readers like [[Google Reader|http://www.google.com/reader]] and [[Bloglines|http://www.bloglines.com]].  For me, they're like "internet newspapers", giving me all of the news that I truly care about each day in a quick and inexpensive way.  I have trouble keeping up with the articles that I really like, however, because I don't have a lot of free time to read them.  Often the free time that I do have is when I'm commuting on a train, but I don't have an internet connection available then.

Thank goodness ScrapBook makes it much easier for me to organize and read my favorite articles.  

# First, I carve out two 10-minute portions of my day where I "process" my RSS reader.  
#* During this time, I find the articles that look interesting based on their title and summary, and capture then locally in my "Read-Review" folder in ScrapBook.  
# Then, when I finally do have time to read the articles, I can use the ScrapBook sidebar to browse all of my captured content, all without an internet connection.  

I can honestly say that ScrapBook has not only ''made it easier for me to process more RSS feeds each day, but it also makes it easier to read those articles later''.  "Life-long learning" is always a high priority for me, and ScrapBook really makes this task easier for me.

!!Conclusion

If you ''do a lot of internet research'', or ''wish that you could be reading more web content while you're away from an internet connection'', then ScrapBook is an excellent Firefox plugin that you should check out.  It's robust, easy-to-use, and incredibly useful.
Well, it's a new year, and what's a new year without a "get your financial shtuff  in order" resolution. Every year, I blame part of my bookkeeping incompetence on my favorite accounting app, GnuCash  , so I'm usually looking for something newer and better every January. So here's my options this year:
!Choices
!!GnuCash
* http://www.gnucash.org
This is always what I come back to, so why even look, right?
* Advantages
** FOSS!
** The new 2.0 version is so much better than the previous 1.8 version from usability and stability perspectives.
** Fairly robust and intuitive
** The standard on Linux (did I mention that I'm a Linux user?)
** Will probably be around forever, so I don't have to worry about being forced to convert all of my historical financial data to a new app any time soon.
** New budgeting interface!
** Tons of great reporting features
** Reporting API using a variant of Lisp. Now if only I knew how to program in Lisp :)
* Disadvantages
** Can still be a little flaky at times
** Only really installs easily on Linux. OS X and Windows users have a much more difficult time.
*** Please note that you can install GnuCash on both Mac OS X and Windows. It's just hard to do. Also, the Windows port of GnuCash is very new, and would be considered alpha software in my opinion.
** Since it's a "real" accounting app, it may be more than I need, meaning that I sometimes have to deal with complexity that is unnecessary for my needs.
!!Quicken
This only runs on Linux is I use Crossover or Wine, and apparently it doesn't run very well. The same thing is true with MS Money, so no thanks.
Wesabe
* http://www.wesabe.com
This is a online-only app bookkeeping app with some cool folksonomy features and a slick, AJAX-y interface.
* Advantages * Seems very easy-to-use, especially when it comes to budgeting and report-generating
** There's a free version, but I don't know if that will work for me.
** Multi-user: very easy for myself and my wife to use this app at the same time.
** Very collaborative, which is nice if you're fairly ignorant about the world of personal finance.
* Disadvantages
** Seems to force you to use a very non-intuitive interface (if you're used to apps like Quickbooks or Quicken).
** You can't add new transactions manually - you have to download a qif file from your bank and upload it into Wesabe.
** You can't use Expense accounts or categories in Wesabe, just "tags". This, to me, is very non-intuitive.
** I spend a lot of time on a train each day, and I can't use this app offline, so that's a big disadvantage for me.
** Very limited reporting functionality.
*** You can basically get three types of reports. This may be enough for most people, but seems a bit limiting to me.
!!Moneydance
* http://www.moneydance.com
This is a Quicken-like bookkeeping app that isn't quite as bloated and can run on nearly any OS that supports Java.
* Advantages
** Will pretty much run on any OS.
** Fairly robust
** Seems to have a plugin architecture that is compatible with Java and Python. Nice!
* Disadvantages
** Not free (bad), but inexpensive (good!)
** The budgeting interface seems a bit overly-simplistic. I'm hesitant to switch to a new bookkeeping app if this particular feature doesn't really wow me.
** How long will this non-FOSS app survive?
*** Please note that you can export all of your data to a qif format, so it's fairly portable, even if MoneyDance were to fold tomorrow.
!!jGnash
* http://jgnash.sourceforge.net/wiki/index.php
This seems to be someone's copy of GnuCash, but with a simpler interface and written in Java.
* Advantages
** FOSS!
** Will pretty much run on any OS.
** Beanshell scripting interface, which is incredibly cool
** Stable and somewhat robust
* Disadvantages
** No budgeting module, which really is a show-stopper for me
** That's really it. This is definitely my second choice.
!Conclusion
In the end, you should choose a program that best compliments your bookkeeping process, not one that "just takes care of it automatically". The latter type of applications often impose a process on you that, more often than not, doesn't really "fit". Ideally, you should be able to fulfill your process completely using pencil and paper; your bookkeeping app should simply make your process more efficient.
I'd love to therefore say that I have defined my bookkeeping process from A-Z and therefore can confidently choose one of the apps above, but my process is still a work-in-progress. So since I need to make a decision soon, I'll go with the app that imposes the fewest constraints on my process while still making me more efficient that I would be otherwise. For me, that app is still GnuCash, although jGnash comes in a close second.
I've never really been able to find a calendar program that really works for me, which can make it really difficult to stay organized.  I'm a Linux user, so naturally I've tried tools like Evolution (buggy and bloated) and Mozilla Sunbird (much more buggy).  Lately, I've been using pal, running it on my Linux workstation at home.  Since I can't access that workstation from work, I also wrote a small script that invokes pal and sends me a list of all of my events/appointments via e-mail.  I automated this script using cron so that it sends the list to me on a daily basis.

This hodge-podge has worked relatively well for me, but it obviously has a lot of holes in it.  I needed to, yet again, try something new.  Since the open source world of calendar software still isn't delivering, I thought I might try one of the new, free web- based calendars that seem to be all the rage.  Coincidentally, Slashdot just posted [[an article on this subject|http://slashdot.org/articles/06/04/16/1333258.shtml]], which started me thinking on what my requirements would be.

!! Necessary Requirements

!!! iCalendar Support
According to the [[WikiPedia iCalendar page|http://en.wikipedia.org/wiki/ICalendar]], "iCalendar is a standard for calendar data exchange".  This sort of feature is designed to make it easier to schedule meetings with people in a very loosely-coupled way.  However, since I'm looking for a personal calendar and not a work calendar, I doubt that I'll be setting up meetings to watch the football game any time soon.
 
However, there are a few reasons why I might want to insist on a calendar app that supports the iCalendar standard:
* I would like to be able to edit my web calendar with one or more fat-client calendar apps.
* If the calendar site supports iCalendar, then I can export my entire calendar to a file.  This is a nice feature if I ever want to use a different calendar application.
* The iCalendar standard may make it easier in the future for me to share events and such, but that feature is pretty low on the totem pole right now.  

!!! Email Integration
I hadn't thought of this feature before, but it seems terribly useful.  [[http://spongecell.com/|Spongecell]] allows you to do this as a beta feature, and it would be a nice way to do bulk imports (//much// easier than writing an import script).  

!!! Web Services API
I sometimes find myself needing to do bulk imports and changes to my personal calendar.  It would be awfully nice if I could just put all of that information in some sort of comma-delimited file and run a small script to import it into my calendar.  A web services API would make this very easy and quick.  Also, it would help spur others to create useful applications (like Konfabulator widgets) that extend the functionality of the web site, making it easier to manage my events.

!!! Event Publication Via RSS 
I'm a huge fan of RSS, and even though most of my friends and family members aren't using it yet, they will be within the next couple of years.  Creating an RSS feed of my public events seems like a very cheap and easy way of notifying my friends about them.  It's also an easy way for me to view my upcoming events.  Who needs a Konfabulator widget designed to work with Kiko or Spongecell when you can just view your upcoming events vai BlogLines?

!!! Ajax Support
Ha, just kidding.  I don't mean to sound like an old fogie, but most of the Ajax web apps that I've used that everyone is raving about have been underwhelming.  For the most part, they haven't been much easier to use or saved me any time.  Of course, I'm not saying that this feature would be a disadvantage, it's just not on the requirements radar.

!! Options
* [[Yahoo! Calendar|http://calendar.yahoo.com]] - This app has been around the longest and seems very stable and useful, but it lacks most of the features listed above.  This seems especially odd when you consider that there are a lot of new, free, and full-featured web calendars that have been recently introduced.

* [[Google Calendar|http://calendar.google.com]] - I've only played with this for a few minutes, but it seems to be adequate (which is a word that a lot of people have been using with Google's non-search-based apps lately).  I really see no compelling reason to use it yet.

* [[Kiko|http://www.kiko.com]] - This calendar app seems to have a lot of intelligent people and smart money behind it according to the Slashdot article.  I like the fact that it integrates with a contact list, and that they explicitly support the Firefox web browser.

* [[Spongcell|http://www.spongecell.com]] - This web calendar seems to be the integration king, with their web serivces and e-mail API's.  I'm especially impressed with the e-mail API, and really think that I would get a lot of utility out of it.

!! Conclusion
The good news for me and bad news for the companies behind these web apps is that standards such as iCalendar and RSS make it easy to switch calendaring services.  So it doesn't really matter which provider I choose in the short term as long as they support these standards.  
[[The Show With Ze Frank|http://www.zefrank.com/theshow/]], a very-cool video podcast, is just the type of low-budget and brilliant satire that inspires me to be a more creative and entertaining blogger. If you haven't seen it yet, then I highly recommend it. It's difficult to describe, but it reminds me of portions of some of my favorite episodes of "The Daily Show".

Other cool things about "the show":

* Zefrank allows people to reply to his video podcasts on his blog, and then publicly humiliates some of the people who posts dumb comments. This is surprisingly cathartic for me.
* The smart, dorky humor on this site is hard to find in mainstream outlets.
* This guy has such a loyal underground following, that there's even a community-maintained wiki.

Basically, the site is everything that great internet TV should be, and therefore probably won't be around for very long. Check out the Brain Crack episode if you can. Also, please note that this show is probably not work safe due to language.
I'm a big fan of using [[GNU Screen|http://http//www.gnu.org/software/screen/]], a terminal multiplexer. At any given time, I could be interacting with a dozen different Unix servers at work. I therefore group servers into logical, named groups and jump back and forth between them using screen.

I find the following one-liner functions to be incredibly helpful to me when it comes to managing multiple screen sessions simultaneously. Hopefully someone else will also benefit from them. I currently run these one-liners using the Bash shell, and store them in my ~/.bashrc file. Each function should be runnable from a shell prompt.

!!getscreenpids

{{{function getscreenpids() { ps auxww | grep screen | grep -v grep | awk '{print $2}'; } }}}

This function simply returns a list of process id's (pid's) for each screen process. This is nice info to have if you want to kill a screen session, since each session uses it's own screen process.

!!clearscreen

{{{function clearscreen() { for pid in $(getscreenpids); do kill -9 $pid; done; screen -wipe; } }}}

This function kills every screen session into which I am logged. This is nice if things get flaky and I just want to kill all of my screen processes quickly.

!!killscreen

{{{function killscreen() { screen -ls | grep $1 | awk '{print $1}' | cut -f1 -d. | xargs kill -9; screen -wipe; } }}}

This function kills every screen session that uses the title that you pass as the first parameter. So if you have one or more screen sessions titled "envA", and you pass that string to this function, all of those screen sessions will be killed.
I've been a big fan of [[VIM|http://vim.org]] for quite a while.  It's light-weight, has a wealth of features, and is, once you get over the learning curve, very easy to use.  I'm even using it to edit this article.

I use Linux at home and get to spend a bit of time using it at work, so I can use VIM for tons of my daily tasks.  Using it's GUI, non-terminal based cousin GVIM on Windows, however, has not always been so easy.  The parent VIM project does offer GVIM for Windows, and it has been adequate for me for years.  It's just that it hasn't been //nearly// as good as the terminal-based Linux version.  

For example, setting up spell-checking support in the stock Windows version of GVIM proved to be impossible for me.  I spent well over two hours one day trying to troubleshoot the spellchecking plugin.  After I visited the 15th web site that suggested that I either install a second instance of Cygwin or just use the Linux version, I finally quit.  

Luckily for me, I found out today that [[the Cream project|http://cream.sourceforge.net]] offers their own souped-up version of GVIM for Windows that includes extra patches and other cool add-ons like built-in spell checking.  They even have a Windows binary for the 7.0e beta version of GVIM, complete with a Nullsoft installation package. 

The only possible downside of using the Cream version of GVIM instead of the stock version is that Cream also distributes their GVIM binaries with a set of plugins that "dumb-down" GVIM.  As any {{{vi}}} user knows, it has an unusual (to put it mildly) keyboard mapping that encourages you to not use a mouse.  The Cream project tries to bring GVIM to the masses by replacing this keyboard mapping with a more intuitive, Notepad-style interface.  This is a boon for people who want to use a powerful text editor without experiencing a learning curve, but it can be a PITA for experienced GVIM users who like all of the features that the stock keyboard mapping provides.  

The good news is that you can turn some of these features off using one of the following two methods:

# ''Expert Mode'' - This mode allows you to use a lot of Cream's cool features while also letting you use most of the "stock" vi commands.  It allows you to switch between modes, which the regular Cream plugin doesn't allow.  I'm a big fan of the stock GVIM keybindings, so I really thought that I would hate Cream, even while using the "Expert Mode".  I'm surprised to see how useful I'm finding it to be, however.  
# ''Regular GVIM'' - If you want to use Cream for some tasks and plain-old GVIM for others, you can do this also.  Cream is just a plugin for GVIM:  if you find yourself in a situation where you have to use the stock version of GVIM, you can just execute that without loading the cream plugin.  

If you like to use VIM, but you're also forced to use Windows on occasion, it would be a good idea to give Cream a little bit of your time.  
[[Hello!]]
[[Latest Articles]]
Sweet!  Someone developed an open source ([[definition|http://en.wikipedia.org/wiki/Open_source]]) version of the [[del.icio.us]] server's software called [[de.lerio.us|http://de.lirio.us/rubric/]].  del.icio.us has become a fantastic tool for me to store my "public" bookmarks, but I have a ton of intranet bookmarks that I use at work that I wouldn't dream of saving in del.icio.us.  Hopefully, someone in our intranet department has the foresight to install de.lerio.us on the intranet and [[foxylicious|http://dietrich.ganx4.com/foxylicious/]] on all of our browsers so that we have an easier time of finding obscure, company-related web pages.In the mean time, I can at least use [[rubric|http://search.cpan.org/dist/Rubric/]], the software on which de.lerio.us is built, to back up my del.icio.us bookmarks from the command-line.
One of the top 5 rules for naming a blog article is to use a descriptive title, which this one admittedly isn't.  However, it's the only three words that keep crossing my mind every time I use the Windows application [[Dexpot|http://www.dexpot.de/index.php?id=home]].

!!Why You, Joe Windows User, Need Virtual Desktops 

I know, I know, Windows is already perfect, and there's nothing from the Linux desktop world that could possible enhance the experience of using Windows, right?  Well, I disagree and here's why.

As a long-time Linux-on-the-desktop user, I've always been a big fan of the virtual desktop feature of X windows (which is dominant window manager of Unix and Linux systems, often referred to as simply "X").   Virtual desktops allow you to have separate desktops, each with their own background image, open applications, and other properties.  You can easily "jump" from desktop to desktop using your mouse or keyboard, as if you had multiple instances of your window manager running in a single monitor.  

I really love this feature of X.  It allows me to group all of the windows related to a ''single'' project in a ''single'' window, helping me be a more [[practice mindfulness|http://headrush.typepad.com/creating_passionate_users/2005/03/your_brain_on_m.html]], while also allowing me to easily switch between projects.

On my Linux workstation at home, I basically use the following virtual desktop arrangement:

* Desktop 1 is devoted to time management/[[GTD|http://en.wikipedia.org/wiki/Gtd]] apps like my [[e-mail manager|http://www.mutt.org "home page for the mutt e-mail client"]], [[todo list|http://www.96db.com/pyGTD/]], and [[calendar app|http://palcal.sourceforge.net/]].
* Desktop 2 is devoted to all of the apps that I use to rip and archive my DVD collection
* Desktop 3 is devoted to [[MythTV|http://www.mythtv.org]]
* Desktop 4 is devoted to bookkeeping and financial management
* Desktop 5 is a scratch desktop for new projects
* Desktop 6 is too

Using virtual desktops, I'm able to jump back-and-forth between these projects and contexts, quickly and easily, without cluttering my taskbar with open application buttons or my desktop with dozens of windows.  In the end, it helps you organize your life and get things done more quickly.

!!Dexpot and Virtual Desktops 

Dexpot does a //wonderful// job managing all of the virtual desktops on Windows laptop, and it may even do a better job than the X windows alternative.  Here are some of the features that really stood out for me:

* The ability to move windows between desktops
* The ability to share windows on multiple (or all) desktops simultaneously
* The ability to specify the backdrop, screen saver, and video resolution on each desktop'
* etc.

!!Additional Features Worth Mentioning 

So the virtual desktop thing was really all I needed, but Dexpot comes with a lot more than that, including:

* Fullscreen Preview - An [[Expose|http://en.wikipedia.org/wiki/Expos%C3%A9_%28Mac_OS_X%29]]-like feature, finally on Windows
* Desktop Rules - Apparently this is a rules engine that fires events based on gui actions.  I haven't tried this yet, but it sounds incredibly cool.
* Session management, allowing you to choose between multiple preference sets
* Enough customization and shortcut options to gag a goat.

!!Conclusion 

So yes, in short, Dexpot is probably the coolest app that I've seen for Windows in at least a couple of years, and has already made me much more effective and efficient at work.  I ''highly'' recommend it, especially considering that it's fully functional shareware.  If you do like it and decide to purchase a license (which isn't necessary but a good idea if you like it), it's on 10 euros (which is around $13 USD).  

//This article is part of a series called "[[Best Apps Of 2007]]".  
I've really been getting a lot of use out of Eclipse lately.  I've been meaning to publish a bunch of little posts regarding all of the cool things that I've been using, but I haven't had the time.  Instead, I'm going to put it all together in this one post. At the very least, it will hopefully define a toolbox of Eclipse programs that will help others do some real work with Eclipse.

!!EPIC - Eclipse Perl Integration
First of all, I am ''very'' impressed by the Perl editing plugin for Eclipse called [[EPIC|http://e-p-i-c.sourceforge.net]].  It has easily made me 3 or 4 times more productive when I'm writing Perl.  It has code completion, error warnings, perldoc integration, and even debugging.

Before using this plugin, I thought that Eclipse only worked as a Java editor.  EPIC definitely proves that Eclipse can work for many different types of languages.

!!Subclipse - Subvesrion + Eclipse
If you're not familiar with [[Subversion|http://subversion.tigris.org]], it's a [[version control system|http://en.wikipedia.org/wiki/Version_control_system]] (or VCS), similar to CVS. It's a tool that I've learned to depend on more and more as of late, and it's very nice to be able to use it within Eclipse.

Subclipse still has a few noticeable bugs in it and is considered to be beta quality, but I find it to be very useable, even useful.  If you find yourself working on more and more software from within Eclipse, and Subversion is your VCS, then you'll definitely want to take a look at Subclipse.

!!ShellEd - Shell Scripting Using Eclipse
Traditionally, I'm a big fan of using the venerable VIM + shell combination when it comes to coding with scripting languages like Perl, Python, Korn shell, etc. It's cheap, easy to set up, and it works on almost any platform.  However, once I started editing my Perl scripts with Eclipse, I found myself only using VIM for Korn shell scripts.

Naturally, I had the following thought:
<<<
"Wouldn't it be nice if I could edit, organize, test, and debug *all* of my scrips/programs using Eclipse? I mean, if EPIC made me 3 times more efficient than I was with VIM, then maybe I could see the same results with other programming languages"
<<<
To test my hypothesis, I found ShellEd_, the shell script plugin for Eclipse.

Of course, reality won out and [[ShellEd|http://www.eclipse-plugins.info/eclipse/plugin_details.jsp?id=821]] did not make me more efficient when writing Korn shell scripts. It's not nearly as robust as EPIC.  For example, it currently does not support warnings, code completion, or debugging.  It does, however, have pretty good syntax highlighting (not as good as VIM, but not bad) and man page integration, even on Windows.

Not that I'm complaining!  It's a very good first step, and I have found it very useful.  Also, it's **free**.  If I truly don't like how it works, then I'm free to change it or pay someone else to do so. Hopefully, more people will start working on this project and it will continue to improve.

!!Conclusion
Three years ago, when I first found out about Eclipse, I thought that it was unlikely that it would live up to it's goal of becoming **the** IDE for all programming languages. These days, things are very different.  JBuilder is going to start using Eclipse as the base for its IDE (http://it.slashdot.org/article.pl?sid=05/04/23/1753237), and most languages seem to have a pretty decent plugin into Eclipse. I wouldn't be surprised if within five years Eclipse is the new VIM for budding programmers.
Here's the easy way to do it:

{{{$ grep -r "^Delivered-To:.*\@yourdomainname.com" ~/.thunderbird | cut -f3 -d: | sort | uniq}}}
  
Please note the following:

* This command calls a lot of Linux/Unix programs that aren't available on Windows XP by default. You could probably run the command above however with Cygwin.
* If you use any IMAP mailboxes, then you //must// sync them with your client before running this command.  Please see the "File -> Offline" menu for more details.
* Please replace {{{yourdomainname.com}}} with your actual domain name.  
* You may have to search more than one Thunderbird folder.  For example, on my Gentoo Linux system, I have to search the following folders:
    
  * {{{~/.thunderbird}}}
  * {{{~/.mozilla-thunderbird}}}
      
* Please note that this command simply returns a list of unique e-mail addresses that include {{{@yourdomainname.com}}}.  It will not show you the clever person was who came up with that e-mail address.
* Isn't Linux cool? 
    
This is a terribly easy way to search text files in a hurry. I wish that Windows came with tools like this.

I've been blessed with friends and family members who are both creative and terribly funny. The reason why I came up with that command above was because I wanted to see a list of all of the funny e-mail addresses that they were sending to me.  Here are a few of my favorites:

* commietommypinko@tompurl.com
* destrocodpiece@tompurl.com (???)
* smelly@tompurl.com
So, once again I've decided to change to a different web application.  For a while, I thought that it would be nice to do a bit of work to make my content easier to find for my "regular" readers.  I then realized that the only regular reader was me, so I decided just to do what I wanted :)

If you are someone who used to read my old WordPress site and are looking for old content, then please feel free to use my search engine or various navigational links.  If it isn't there, then it probably wasn't very useful anyways, so I didn't convert it.

I also thought that it may be a bad idea to use TiddlyWiki as my web site engine because it uses a lot of cool AJAX-y stuff that can sometimes give web browsers a fit.  TiddlyWiki has it's share of UI bugs, so you may notice a little weirdness as you navigate this site.  The good news is that very few people navigate this site, and that browsers are catching up to easily support the type of functionality that TiddlyWiki uses.  

So yes, hopefully this TiddlyWiki thing works out for a while.  I'm really tired of converting my content between various web apps, and really just want to have a nice little corner where I can share my life with the rest of the world.
//~~(Part of the [[ForEachTiddlerPlugin]])~~//

Create customizable lists, tables etc. for your selections of tiddlers. Specify the tiddlers to include and their order through a powerful language.

''Syntax:'' 
|>|{{{<<}}}''forEachTiddler'' [''in'' //tiddlyWikiPath//] [''where'' //whereCondition//] [''sortBy'' //sortExpression// [''ascending'' //or// ''descending'']] [''script'' //scriptText//] [//action// [//actionParameters//]]{{{>>}}}|
|//tiddlyWikiPath//|The filepath to the TiddlyWiki the macro should work on. When missing the current TiddlyWiki is used.|
|//whereCondition//|(quoted) JavaScript boolean expression. May refer to the build-in variables {{{tiddler}}} and {{{context}}}.|
|//sortExpression//|(quoted) JavaScript expression returning "comparable" objects (using '{{{<}}}','{{{>}}}','{{{==}}}'. May refer to the build-in variables {{{tiddler}}} and {{{context}}}.|
|//scriptText//|(quoted) JavaScript text. Typically defines JavaScript functions that are called by the various JavaScript expressions (whereClause, sortClause, action arguments,...)|
|//action//|The action that should be performed on every selected tiddler, in the given order. By default the actions [[addToList|AddToListAction]] and [[write|WriteAction]] are supported. When no action is specified [[addToList|AddToListAction]] is used.|
|//actionParameters//|(action specific) parameters the action may refer while processing the tiddlers (see action descriptions for details). <<tiddler [[JavaScript in actionParameters]]>>|
|>|~~Syntax formatting: Keywords in ''bold'', optional parts in [...]. 'or' means that exactly one of the two alternatives must exist.~~|


''Using JavaScript''

To give you a lot of flexibility the [[ForEachTiddlerMacro]] uses JavaScript in its arguments. Even if you are not that familiar with JavaScript you may find forEachTiddler useful. Just have a look at the various ready-to-use [[ForEachTiddlerExamples]] and adapt them to your needs.

''The Elements of the Macro''

The arguments of the ForEachTiddlerMacro consist of multiple parts, each of them being optional.

<<slider chkFETInClause [[inClause]] "inClause" "inClause">>
<<slider chkFETWhereClause [[whereClause]] "whereClause" "whereClause">>
<<slider chkFETSortClause [[sortClause]] "sortClause" "sortClause">>
<<slider chkFETScriptClause [[scriptClause]] "scriptClause" "scriptClause">>
<<slider chkFETActions [[Action Specification]] "Action Specification" "Action Specification">>

''Using Macros and ">" inside the forEachTiddler Macro''

You may use other macro calls into the expression, especially in the actionParameters. To avoid that the {{{>>}}} of such a macro call is misinterpreted as the end of the {{{<<forEachTiddler...>>}}} macro you must escape the {{{>>}}} of the inner macro with {{{$))}}} E.g. if you want to use {{{<<tiddler ...>>}}} inside the {{{forEachTiddler}}} macro you have to write {{{<<tiddler ...$))}}}.

In addition it is necessary to escape single {{{>}}} with the text {{{$)}}}.

''Using {{{<<tiddler ... with: ...>>}}} to re-use ForEachTiddler definitions''

Sometimes you may want to use a certain ForEachTiddler definition in slight variations. E.g. you may want to list either the tiddlers tagged with "ToDo" and in the other case with "Done". To do so you may use "Tiddler parameters". Here an example:

Replace the variable part of the ForEachTiddler definition with $1 ($2,... $9 are supported). E.g. you may create the tiddler "ListTaggedTiddlers" like this
{{{
<<forEachTiddler 
 where 
 'tiddler.tags.contains("$1")'
>>
}}}

Now you can use the ListTaggedTiddlers for various specific tags, using the {{{<<tiddler ...>>}}} macro:
{{{
<<tiddler ListTaggedTiddlers with: "systemConfig">>
}}}
{{{
<<tiddler ListTaggedTiddlers with: "Plugin">>
}}}


See also [[ForEachTiddlerExamples]].
/***
|''Name:''|ForEachTiddlerPlugin|
|''Version:''|1.0.8 (2007-04-12)|
|''Source:''|http://tiddlywiki.abego-software.de/#ForEachTiddlerPlugin|
|''Author:''|UdoBorkowski (ub [at] abego-software [dot] de)|
|''Licence:''|[[BSD open source license (abego Software)|http://www.abego-software.de/legal/apl-v10.html]]|
|''Copyright:''|&copy; 2005-2007 [[abego Software|http://www.abego-software.de]]|
|''TiddlyWiki:''|1.2.38+, 2.0|
|''Browser:''|Firefox 1.0.4+; Firefox 1.5; InternetExplorer 6.0|
!Description

Create customizable lists, tables etc. for your selections of tiddlers. Specify the tiddlers to include and their order through a powerful language.

''Syntax:'' 
|>|{{{<<}}}''forEachTiddler'' [''in'' //tiddlyWikiPath//] [''where'' //whereCondition//] [''sortBy'' //sortExpression// [''ascending'' //or// ''descending'']] [''script'' //scriptText//] [//action// [//actionParameters//]]{{{>>}}}|
|//tiddlyWikiPath//|The filepath to the TiddlyWiki the macro should work on. When missing the current TiddlyWiki is used.|
|//whereCondition//|(quoted) JavaScript boolean expression. May refer to the build-in variables {{{tiddler}}} and  {{{context}}}.|
|//sortExpression//|(quoted) JavaScript expression returning "comparable" objects (using '{{{<}}}','{{{>}}}','{{{==}}}'. May refer to the build-in variables {{{tiddler}}} and  {{{context}}}.|
|//scriptText//|(quoted) JavaScript text. Typically defines JavaScript functions that are called by the various JavaScript expressions (whereClause, sortClause, action arguments,...)|
|//action//|The action that should be performed on every selected tiddler, in the given order. By default the actions [[addToList|AddToListAction]] and [[write|WriteAction]] are supported. When no action is specified [[addToList|AddToListAction]]  is used.|
|//actionParameters//|(action specific) parameters the action may refer while processing the tiddlers (see action descriptions for details). <<tiddler [[JavaScript in actionParameters]]>>|
|>|~~Syntax formatting: Keywords in ''bold'', optional parts in [...]. 'or' means that exactly one of the two alternatives must exist.~~|

See details see [[ForEachTiddlerMacro]] and [[ForEachTiddlerExamples]].

!Revision history
* v1.0.8 (2007-04-12)
** Adapted to latest TiddlyWiki 2.2 Beta importTiddlyWiki API (introduced with changeset 2004). TiddlyWiki 2.2 Beta builds prior to changeset 2004 are no longer supported (but TiddlyWiki 2.1 and earlier, of cause)
* v1.0.7 (2007-03-28)
** Also support "pre" formatted TiddlyWikis (introduced with TW 2.2) (when using "in" clause to work on external tiddlers)
* v1.0.6 (2006-09-16)
** Context provides "viewerTiddler", i.e. the tiddler used to view the macro. Most times this is equal to the "inTiddler", but when using the "tiddler" macro both may be different.
** Support "begin", "end" and "none" expressions in "write" action
* v1.0.5 (2006-02-05)
** Pass tiddler containing the macro with wikify, context object also holds reference to tiddler containing the macro ("inTiddler"). Thanks to SimonBaird.
** Support Firefox 1.5.0.1
** Internal
*** Make "JSLint" conform
*** "Only install once"
* v1.0.4 (2006-01-06)
** Support TiddlyWiki 2.0
* v1.0.3 (2005-12-22)
** Features: 
*** Write output to a file supports multi-byte environments (Thanks to Bram Chen) 
*** Provide API to access the forEachTiddler functionality directly through JavaScript (see getTiddlers and performMacro)
** Enhancements:
*** Improved error messages on InternetExplorer.
* v1.0.2 (2005-12-10)
** Features: 
*** context object also holds reference to store (TiddlyWiki)
** Fixed Bugs: 
*** ForEachTiddler 1.0.1 has broken support on win32 Opera 8.51 (Thanks to BrunoSabin for reporting)
* v1.0.1 (2005-12-08)
** Features: 
*** Access tiddlers stored in separated TiddlyWikis through the "in" option. I.e. you are no longer limited to only work on the "current TiddlyWiki".
*** Write output to an external file using the "toFile" option of the "write" action. With this option you may write your customized tiddler exports.
*** Use the "script" section to define "helper" JavaScript functions etc. to be used in the various JavaScript expressions (whereClause, sortClause, action arguments,...).
*** Access and store context information for the current forEachTiddler invocation (through the build-in "context" object) .
*** Improved script evaluation (for where/sort clause and write scripts).
* v1.0.0 (2005-11-20)
** initial version

!Code
***/
//{{{

	
//============================================================================
//============================================================================
//		   ForEachTiddlerPlugin
//============================================================================
//============================================================================

// Only install once
if (!version.extensions.ForEachTiddlerPlugin) {

if (!window.abego) window.abego = {};

version.extensions.ForEachTiddlerPlugin = {
	major: 1, minor: 0, revision: 8, 
	date: new Date(2007,3,12), 
	source: "http://tiddlywiki.abego-software.de/#ForEachTiddlerPlugin",
	licence: "[[BSD open source license (abego Software)|http://www.abego-software.de/legal/apl-v10.html]]",
	copyright: "Copyright (c) abego Software GmbH, 2005-2007 (www.abego-software.de)"
};

// For backward compatibility with TW 1.2.x
//
if (!TiddlyWiki.prototype.forEachTiddler) {
	TiddlyWiki.prototype.forEachTiddler = function(callback) {
		for(var t in this.tiddlers) {
			callback.call(this,t,this.tiddlers[t]);
		}
	};
}

//============================================================================
// forEachTiddler Macro
//============================================================================

version.extensions.forEachTiddler = {
	major: 1, minor: 0, revision: 8, date: new Date(2007,3,12), provider: "http://tiddlywiki.abego-software.de"};

// ---------------------------------------------------------------------------
// Configurations and constants 
// ---------------------------------------------------------------------------

config.macros.forEachTiddler = {
	 // Standard Properties
	 label: "forEachTiddler",
	 prompt: "Perform actions on a (sorted) selection of tiddlers",

	 // actions
	 actions: {
		 addToList: {},
		 write: {}
	 }
};

// ---------------------------------------------------------------------------
//  The forEachTiddler Macro Handler 
// ---------------------------------------------------------------------------

config.macros.forEachTiddler.getContainingTiddler = function(e) {
	while(e && !hasClass(e,"tiddler"))
		e = e.parentNode;
	var title = e ? e.getAttribute("tiddler") : null; 
	return title ? store.getTiddler(title) : null;
};

config.macros.forEachTiddler.handler = function(place,macroName,params,wikifier,paramString,tiddler) {
	// config.macros.forEachTiddler.traceMacroCall(place,macroName,params,wikifier,paramString,tiddler);

	if (!tiddler) tiddler = config.macros.forEachTiddler.getContainingTiddler(place);
	// --- Parsing ------------------------------------------

	var i = 0; // index running over the params
	// Parse the "in" clause
	var tiddlyWikiPath = undefined;
	if ((i < params.length) && params[i] == "in") {
		i++;
		if (i >= params.length) {
			this.handleError(place, "TiddlyWiki path expected behind 'in'.");
			return;
		}
		tiddlyWikiPath = this.paramEncode((i < params.length) ? params[i] : "");
		i++;
	}

	// Parse the where clause
	var whereClause ="true";
	if ((i < params.length) && params[i] == "where") {
		i++;
		whereClause = this.paramEncode((i < params.length) ? params[i] : "");
		i++;
	}

	// Parse the sort stuff
	var sortClause = null;
	var sortAscending = true; 
	if ((i < params.length) && params[i] == "sortBy") {
		i++;
		if (i >= params.length) {
			this.handleError(place, "sortClause missing behind 'sortBy'.");
			return;
		}
		sortClause = this.paramEncode(params[i]);
		i++;

		if ((i < params.length) && (params[i] == "ascending" || params[i] == "descending")) {
			 sortAscending = params[i] == "ascending";
			 i++;
		}
	}

	// Parse the script
	var scriptText = null;
	if ((i < params.length) && params[i] == "script") {
		i++;
		scriptText = this.paramEncode((i < params.length) ? params[i] : "");
		i++;
	}

	// Parse the action. 
	// When we are already at the end use the default action
	var actionName = "addToList";
	if (i < params.length) {
	   if (!config.macros.forEachTiddler.actions[params[i]]) {
			this.handleError(place, "Unknown action '"+params[i]+"'.");
			return;
		} else {
			actionName = params[i]; 
			i++;
		}
	} 
	
	// Get the action parameter
	// (the parsing is done inside the individual action implementation.)
	var actionParameter = params.slice(i);


	// --- Processing ------------------------------------------
	try {
		this.performMacro({
				place: place, 
				inTiddler: tiddler,
				whereClause: whereClause, 
				sortClause: sortClause, 
				sortAscending: sortAscending, 
				actionName: actionName, 
				actionParameter: actionParameter, 
				scriptText: scriptText, 
				tiddlyWikiPath: tiddlyWikiPath});

	} catch (e) {
		this.handleError(place, e);
	}
};

// Returns an object with properties "tiddlers" and "context".
// tiddlers holds the (sorted) tiddlers selected by the parameter,
// context the context of the execution of the macro.
//
// The action is not yet performed.
//
// @parameter see performMacro
//
config.macros.forEachTiddler.getTiddlersAndContext = function(parameter) {

	var context = config.macros.forEachTiddler.createContext(parameter.place, parameter.whereClause, parameter.sortClause, parameter.sortAscending, parameter.actionName, parameter.actionParameter, parameter.scriptText, parameter.tiddlyWikiPath, parameter.inTiddler);

	var tiddlyWiki = parameter.tiddlyWikiPath ? this.loadTiddlyWiki(parameter.tiddlyWikiPath) : store;
	context["tiddlyWiki"] = tiddlyWiki;
	
	// Get the tiddlers, as defined by the whereClause
	var tiddlers = this.findTiddlers(parameter.whereClause, context, tiddlyWiki);
	context["tiddlers"] = tiddlers;

	// Sort the tiddlers, when sorting is required.
	if (parameter.sortClause) {
		this.sortTiddlers(tiddlers, parameter.sortClause, parameter.sortAscending, context);
	}

	return {tiddlers: tiddlers, context: context};
};

// Returns the (sorted) tiddlers selected by the parameter.
//
// The action is not yet performed.
//
// @parameter see performMacro
//
config.macros.forEachTiddler.getTiddlers = function(parameter) {
	return this.getTiddlersAndContext(parameter).tiddlers;
};

// Performs the macros with the given parameter.
//
// @param parameter holds the parameter of the macro as separate properties.
//				  The following properties are supported:
//
//						place
//						whereClause
//						sortClause
//						sortAscending
//						actionName
//						actionParameter
//						scriptText
//						tiddlyWikiPath
//
//					All properties are optional. 
//					For most actions the place property must be defined.
//
config.macros.forEachTiddler.performMacro = function(parameter) {
	var tiddlersAndContext = this.getTiddlersAndContext(parameter);

	// Perform the action
	var actionName = parameter.actionName ? parameter.actionName : "addToList";
	var action = config.macros.forEachTiddler.actions[actionName];
	if (!action) {
		this.handleError(parameter.place, "Unknown action '"+actionName+"'.");
		return;
	}

	var actionHandler = action.handler;
	actionHandler(parameter.place, tiddlersAndContext.tiddlers, parameter.actionParameter, tiddlersAndContext.context);
};

// ---------------------------------------------------------------------------
//  The actions 
// ---------------------------------------------------------------------------

// Internal.
//
// --- The addToList Action -----------------------------------------------
//
config.macros.forEachTiddler.actions.addToList.handler = function(place, tiddlers, parameter, context) {
	// Parse the parameter
	var p = 0;

	// Check for extra parameters
	if (parameter.length > p) {
		config.macros.forEachTiddler.createExtraParameterErrorElement(place, "addToList", parameter, p);
		return;
	}

	// Perform the action.
	var list = document.createElement("ul");
	place.appendChild(list);
	for (var i = 0; i < tiddlers.length; i++) {
		var tiddler = tiddlers[i];
		var listItem = document.createElement("li");
		list.appendChild(listItem);
		createTiddlyLink(listItem, tiddler.title, true);
	}
};

abego.parseNamedParameter = function(name, parameter, i) {
	var beginExpression = null;
	if ((i < parameter.length) && parameter[i] == name) {
		i++;
		if (i >= parameter.length) {
			throw "Missing text behind '%0'".format([name]);
		}
		
		return config.macros.forEachTiddler.paramEncode(parameter[i]);
	}
	return null;
}

// Internal.
//
// --- The write Action ---------------------------------------------------
//
config.macros.forEachTiddler.actions.write.handler = function(place, tiddlers, parameter, context) {
	// Parse the parameter
	var p = 0;
	if (p >= parameter.length) {
		this.handleError(place, "Missing expression behind 'write'.");
		return;
	}

	var textExpression = config.macros.forEachTiddler.paramEncode(parameter[p]);
	p++;

	// Parse the "begin" option
	var beginExpression = abego.parseNamedParameter("begin", parameter, p);
	if (beginExpression !== null) 
		p += 2;
	var endExpression = abego.parseNamedParameter("end", parameter, p);
	if (endExpression !== null) 
		p += 2;
	var noneExpression = abego.parseNamedParameter("none", parameter, p);
	if (noneExpression !== null) 
		p += 2;

	// Parse the "toFile" option
	var filename = null;
	var lineSeparator = undefined;
	if ((p < parameter.length) && parameter[p] == "toFile") {
		p++;
		if (p >= parameter.length) {
			this.handleError(place, "Filename expected behind 'toFile' of 'write' action.");
			return;
		}
		
		filename = config.macros.forEachTiddler.getLocalPath(config.macros.forEachTiddler.paramEncode(parameter[p]));
		p++;
		if ((p < parameter.length) && parameter[p] == "withLineSeparator") {
			p++;
			if (p >= parameter.length) {
				this.handleError(place, "Line separator text expected behind 'withLineSeparator' of 'write' action.");
				return;
			}
			lineSeparator = config.macros.forEachTiddler.paramEncode(parameter[p]);
			p++;
		}
	}
	
	// Check for extra parameters
	if (parameter.length > p) {
		config.macros.forEachTiddler.createExtraParameterErrorElement(place, "write", parameter, p);
		return;
	}

	// Perform the action.
	var func = config.macros.forEachTiddler.getEvalTiddlerFunction(textExpression, context);
	var count = tiddlers.length;
	var text = "";
	if (count > 0 && beginExpression)
		text += config.macros.forEachTiddler.getEvalTiddlerFunction(beginExpression, context)(undefined, context, count, undefined);
	
	for (var i = 0; i < count; i++) {
		var tiddler = tiddlers[i];
		text += func(tiddler, context, count, i);
	}
	
	if (count > 0 && endExpression)
		text += config.macros.forEachTiddler.getEvalTiddlerFunction(endExpression, context)(undefined, context, count, undefined);

	if (count == 0 && noneExpression) 
		text += config.macros.forEachTiddler.getEvalTiddlerFunction(noneExpression, context)(undefined, context, count, undefined);
		

	if (filename) {
		if (lineSeparator !== undefined) {
			lineSeparator = lineSeparator.replace(/\\n/mg, "\n").replace(/\\r/mg, "\r");
			text = text.replace(/\n/mg,lineSeparator);
		}
		saveFile(filename, convertUnicodeToUTF8(text));
	} else {
		var wrapper = createTiddlyElement(place, "span");
		wikify(text, wrapper, null/* highlightRegExp */, context.inTiddler);
	}
};


// ---------------------------------------------------------------------------
//  Helpers
// ---------------------------------------------------------------------------

// Internal.
//
config.macros.forEachTiddler.createContext = function(placeParam, whereClauseParam, sortClauseParam, sortAscendingParam, actionNameParam, actionParameterParam, scriptText, tiddlyWikiPathParam, inTiddlerParam) {
	return {
		place : placeParam, 
		whereClause : whereClauseParam, 
		sortClause : sortClauseParam, 
		sortAscending : sortAscendingParam, 
		script : scriptText,
		actionName : actionNameParam, 
		actionParameter : actionParameterParam,
		tiddlyWikiPath : tiddlyWikiPathParam,
		inTiddler : inTiddlerParam, // the tiddler containing the <<forEachTiddler ...>> macro call.
		viewerTiddler : config.macros.forEachTiddler.getContainingTiddler(placeParam) // the tiddler showing the forEachTiddler result
	};
};

// Internal.
//
// Returns a TiddlyWiki with the tiddlers loaded from the TiddlyWiki of 
// the given path.
//
config.macros.forEachTiddler.loadTiddlyWiki = function(path, idPrefix) {
	if (!idPrefix) {
		idPrefix = "store";
	}
	var lenPrefix = idPrefix.length;
	
	// Read the content of the given file
	var content = loadFile(this.getLocalPath(path));
	if(content === null) {
		throw "TiddlyWiki '"+path+"' not found.";
	}
	
	var tiddlyWiki = new TiddlyWiki();

	// Starting with TW 2.2 there is a helper function to import the tiddlers
	if (tiddlyWiki.importTiddlyWiki) {
		if (!tiddlyWiki.importTiddlyWiki(content))
			throw "File '"+path+"' is not a TiddlyWiki.";
		tiddlyWiki.dirty = false;
		return tiddlyWiki;
	}
	
	// The legacy code, for TW < 2.2
	
	// Locate the storeArea div's
	var posOpeningDiv = content.indexOf(startSaveArea);
	var posClosingDiv = content.lastIndexOf(endSaveArea);
	if((posOpeningDiv == -1) || (posClosingDiv == -1)) {
		throw "File '"+path+"' is not a TiddlyWiki.";
	}
	var storageText = content.substr(posOpeningDiv + startSaveArea.length, posClosingDiv);
	
	// Create a "div" element that contains the storage text
	var myStorageDiv = document.createElement("div");
	myStorageDiv.innerHTML = storageText;
	myStorageDiv.normalize();
	
	// Create all tiddlers in a new TiddlyWiki
	// (following code is modified copy of TiddlyWiki.prototype.loadFromDiv)
	var store = myStorageDiv.childNodes;
	for(var t = 0; t < store.length; t++) {
		var e = store[t];
		var title = null;
		if(e.getAttribute)
			title = e.getAttribute("tiddler");
		if(!title && e.id && e.id.substr(0,lenPrefix) == idPrefix)
			title = e.id.substr(lenPrefix);
		if(title && title !== "") {
			var tiddler = tiddlyWiki.createTiddler(title);
			tiddler.loadFromDiv(e,title);
		}
	}
	tiddlyWiki.dirty = false;

	return tiddlyWiki;
};


	
// Internal.
//
// Returns a function that has a function body returning the given javaScriptExpression.
// The function has the parameters:
// 
//	 (tiddler, context, count, index)
//
config.macros.forEachTiddler.getEvalTiddlerFunction = function (javaScriptExpression, context) {
	var script = context["script"];
	var functionText = "var theFunction = function(tiddler, context, count, index) { return "+javaScriptExpression+"}";
	var fullText = (script ? script+";" : "")+functionText+";theFunction;";
	return eval(fullText);
};

// Internal.
//
config.macros.forEachTiddler.findTiddlers = function(whereClause, context, tiddlyWiki) {
	var result = [];
	var func = config.macros.forEachTiddler.getEvalTiddlerFunction(whereClause, context);
	tiddlyWiki.forEachTiddler(function(title,tiddler) {
		if (func(tiddler, context, undefined, undefined)) {
			result.push(tiddler);
		}
	});
	return result;
};

// Internal.
//
config.macros.forEachTiddler.createExtraParameterErrorElement = function(place, actionName, parameter, firstUnusedIndex) {
	var message = "Extra parameter behind '"+actionName+"':";
	for (var i = firstUnusedIndex; i < parameter.length; i++) {
		message += " "+parameter[i];
	}
	this.handleError(place, message);
};

// Internal.
//
config.macros.forEachTiddler.sortAscending = function(tiddlerA, tiddlerB) {
	var result = 
		(tiddlerA.forEachTiddlerSortValue == tiddlerB.forEachTiddlerSortValue) 
			? 0
			: (tiddlerA.forEachTiddlerSortValue < tiddlerB.forEachTiddlerSortValue)
			   ? -1 
			   : +1; 
	return result;
};

// Internal.
//
config.macros.forEachTiddler.sortDescending = function(tiddlerA, tiddlerB) {
	var result = 
		(tiddlerA.forEachTiddlerSortValue == tiddlerB.forEachTiddlerSortValue) 
			? 0
			: (tiddlerA.forEachTiddlerSortValue < tiddlerB.forEachTiddlerSortValue)
			   ? +1 
			   : -1; 
	return result;
};

// Internal.
//
config.macros.forEachTiddler.sortTiddlers = function(tiddlers, sortClause, ascending, context) {
	// To avoid evaluating the sortClause whenever two items are compared 
	// we pre-calculate the sortValue for every item in the array and store it in a 
	// temporary property ("forEachTiddlerSortValue") of the tiddlers.
	var func = config.macros.forEachTiddler.getEvalTiddlerFunction(sortClause, context);
	var count = tiddlers.length;
	var i;
	for (i = 0; i < count; i++) {
		var tiddler = tiddlers[i];
		tiddler.forEachTiddlerSortValue = func(tiddler,context, undefined, undefined);
	}

	// Do the sorting
	tiddlers.sort(ascending ? this.sortAscending : this.sortDescending);

	// Delete the temporary property that holds the sortValue.	
	for (i = 0; i < tiddlers.length; i++) {
		delete tiddlers[i].forEachTiddlerSortValue;
	}
};


// Internal.
//
config.macros.forEachTiddler.trace = function(message) {
	displayMessage(message);
};

// Internal.
//
config.macros.forEachTiddler.traceMacroCall = function(place,macroName,params) {
	var message ="<<"+macroName;
	for (var i = 0; i < params.length; i++) {
		message += " "+params[i];
	}
	message += ">>";
	displayMessage(message);
};


// Internal.
//
// Creates an element that holds an error message
// 
config.macros.forEachTiddler.createErrorElement = function(place, exception) {
	var message = (exception.description) ? exception.description : exception.toString();
	return createTiddlyElement(place,"span",null,"forEachTiddlerError","<<forEachTiddler ...>>: "+message);
};

// Internal.
//
// @param place [may be null]
//
config.macros.forEachTiddler.handleError = function(place, exception) {
	if (place) {
		this.createErrorElement(place, exception);
	} else {
		throw exception;
	}
};

// Internal.
//
// Encodes the given string.
//
// Replaces 
//	 "$))" to ">>"
//	 "$)" to ">"
//
config.macros.forEachTiddler.paramEncode = function(s) {
	var reGTGT = new RegExp("\\$\\)\\)","mg");
	var reGT = new RegExp("\\$\\)","mg");
	return s.replace(reGTGT, ">>").replace(reGT, ">");
};

// Internal.
//
// Returns the given original path (that is a file path, starting with "file:")
// as a path to a local file, in the systems native file format.
//
// Location information in the originalPath (i.e. the "#" and stuff following)
// is stripped.
// 
config.macros.forEachTiddler.getLocalPath = function(originalPath) {
	// Remove any location part of the URL
	var hashPos = originalPath.indexOf("#");
	if(hashPos != -1)
		originalPath = originalPath.substr(0,hashPos);
	// Convert to a native file format assuming
	// "file:///x:/path/path/path..." - pc local file --> "x:\path\path\path..."
	// "file://///server/share/path/path/path..." - FireFox pc network file --> "\\server\share\path\path\path..."
	// "file:///path/path/path..." - mac/unix local file --> "/path/path/path..."
	// "file://server/share/path/path/path..." - pc network file --> "\\server\share\path\path\path..."
	var localPath;
	if(originalPath.charAt(9) == ":") // pc local file
		localPath = unescape(originalPath.substr(8)).replace(new RegExp("/","g"),"\\");
	else if(originalPath.indexOf("file://///") === 0) // FireFox pc network file
		localPath = "\\\\" + unescape(originalPath.substr(10)).replace(new RegExp("/","g"),"\\");
	else if(originalPath.indexOf("file:///") === 0) // mac/unix local file
		localPath = unescape(originalPath.substr(7));
	else if(originalPath.indexOf("file:/") === 0) // mac/unix local file
		localPath = unescape(originalPath.substr(5));
	else // pc network file
		localPath = "\\\\" + unescape(originalPath.substr(7)).replace(new RegExp("/","g"),"\\");	
	return localPath;
};

// ---------------------------------------------------------------------------
// Stylesheet Extensions (may be overridden by local StyleSheet)
// ---------------------------------------------------------------------------
//
setStylesheet(
	".forEachTiddlerError{color: #ffffff;background-color: #880000;}",
	"forEachTiddler");

//============================================================================
// End of forEachTiddler Macro
//============================================================================


//============================================================================
// String.startsWith Function
//============================================================================
//
// Returns true if the string starts with the given prefix, false otherwise.
//
version.extensions["String.startsWith"] = {major: 1, minor: 0, revision: 0, date: new Date(2005,11,20), provider: "http://tiddlywiki.abego-software.de"};
//
String.prototype.startsWith = function(prefix) {
	var n =  prefix.length;
	return (this.length >= n) && (this.slice(0, n) == prefix);
};



//============================================================================
// String.endsWith Function
//============================================================================
//
// Returns true if the string ends with the given suffix, false otherwise.
//
version.extensions["String.endsWith"] = {major: 1, minor: 0, revision: 0, date: new Date(2005,11,20), provider: "http://tiddlywiki.abego-software.de"};
//
String.prototype.endsWith = function(suffix) {
	var n = suffix.length;
	return (this.length >= n) && (this.right(n) == suffix);
};


//============================================================================
// String.contains Function
//============================================================================
//
// Returns true when the string contains the given substring, false otherwise.
//
version.extensions["String.contains"] = {major: 1, minor: 0, revision: 0, date: new Date(2005,11,20), provider: "http://tiddlywiki.abego-software.de"};
//
String.prototype.contains = function(substring) {
	return this.indexOf(substring) >= 0;
};

//============================================================================
// Array.indexOf Function
//============================================================================
//
// Returns the index of the first occurance of the given item in the array or 
// -1 when no such item exists.
//
// @param item [may be null]
//
version.extensions["Array.indexOf"] = {major: 1, minor: 0, revision: 0, date: new Date(2005,11,20), provider: "http://tiddlywiki.abego-software.de"};
//
Array.prototype.indexOf = function(item) {
	for (var i = 0; i < this.length; i++) {
		if (this[i] == item) {
			return i;
		}
	}
	return -1;
};

//============================================================================
// Array.contains Function
//============================================================================
//
// Returns true when the array contains the given item, otherwise false. 
//
// @param item [may be null]
//
version.extensions["Array.contains"] = {major: 1, minor: 0, revision: 0, date: new Date(2005,11,20), provider: "http://tiddlywiki.abego-software.de"};
//
Array.prototype.contains = function(item) {
	return (this.indexOf(item) >= 0);
};

//============================================================================
// Array.containsAny Function
//============================================================================
//
// Returns true when the array contains at least one of the elements 
// of the item. Otherwise (or when items contains no elements) false is returned.
//
version.extensions["Array.containsAny"] = {major: 1, minor: 0, revision: 0, date: new Date(2005,11,20), provider: "http://tiddlywiki.abego-software.de"};
//
Array.prototype.containsAny = function(items) {
	for(var i = 0; i < items.length; i++) {
		if (this.contains(items[i])) {
			return true;
		}
	}
	return false;
};


//============================================================================
// Array.containsAll Function
//============================================================================
//
// Returns true when the array contains all the items, otherwise false.
// 
// When items is null false is returned (even if the array contains a null).
//
// @param items [may be null] 
//
version.extensions["Array.containsAll"] = {major: 1, minor: 0, revision: 0, date: new Date(2005,11,20), provider: "http://tiddlywiki.abego-software.de"};
//
Array.prototype.containsAll = function(items) {
	for(var i = 0; i < items.length; i++) {
		if (!this.contains(items[i])) {
			return false;
		}
	}
	return true;
};


} // of "install only once"

// Used Globals (for JSLint) ==============
// ... DOM
/*global 	document */
// ... TiddlyWiki Core
/*global 	convertUnicodeToUTF8, createTiddlyElement, createTiddlyLink, 
			displayMessage, endSaveArea, hasClass, loadFile, saveFile, 
			startSaveArea, store, wikify */
//}}}


/***
!Licence and Copyright
Copyright (c) abego Software ~GmbH, 2005 ([[www.abego-software.de|http://www.abego-software.de]])

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

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.

Neither the name of abego Software nor the names of its contributors may be
used to endorse or promote products derived from this software without specific
prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT OWNER 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.
***/
Most people don't back up their data, and one popular excuse is that it's too expensive.  While there are costs associated with any type of backup, I hope to prove the following points in this article:

# //Not// backing up your data can be incredibly expensive
# Backups can be affordable if they're done correctly

''Note'': There are a few links below that point at commercial hardware, software, and web sites.  I'm not receiving a kickback from any of the companies associated with these links.  I mention them because they have all worked well for me.

!!The High Costs Of Data Loss

In strictly monetary terms, it can be //very// expensive to retrieve data from a broken disk.  A friend of mine once lost a very important disc, and the estimate for retrieving the data was over $2000.  As you'll see later in this article, that price is significantly higher than the cost of a simple backup process.

A much more important cost to me, however, are the "costs" associated with losing irreplaceable items such as photos, home videos, and other one-of-a-kind files.  Past generations had to worry about a major catastrophe like a fire or flood to destroy their family heirlooms.  Today, all it takes is a power surge or a little too much static electricity, and you're at risk for losing all of your digital heirlooms.  

!!Computer Backups Can Be A Frugal Choice

I've tried multiple backups processes over the last 7 or 8 years, and am pretty happy with how frugal my current system is.  Here are some of the lessons that I've learned:

!!!1. Your Backup Process Should Be As Automatic As Possible

Automating your backup process has two major benefits

# If you don't actually have to //do// anything to execute your backup, it's much more likely to occur
#* Trust me on this one.  In the world of IT, we spend tons of time, money, and effort to automate as much as we possibly can.  Not only does it improve the quality of our processes, but it allows us to execute them on a much more frequent basis (if necessary).
# You will save tons of time
#* Your time is much too valuable to be spent doing things like swapping DVD-R's or waiting for one process to end so you can start a new one.

!!!2. Backup Using A Hard Disk Instead Of Optical Media 

Backing up your data to a hard disk instead of CD-R's or DVD-R's has the following advantages:

# Hard disk storage is cheaper, byte for byte, when you take the rewritable nature of hard disks into account.
#* In my opinion, CD-RW's and DVD-RW's don't really count as a reliable format for backups.  I have yet to meet someone who has been able to rewrite a CD-RW more than 10 times without any data corruption.
# Data corruption is more easily visible on a hard disk
#* Hard disks and optical media both degrade and, with their moving parts, hard drives typically have a shorter usable life than DVD's.  The big advantage of hard drives, however, is that you will know if it's broken the next time you backup your data (which should be on a daily basis at least).  Optical media could sit on a shelf for //years// after it has degraded past the point of usefulness.  You wouldn't know that your backup data was gone until you actually needed it.
# Hard drives, simply put, are easy
#* The older I get, them more I appreciate simplicity and ease-of-use.  It's hard to beat the ease and simplicity of backing up your data to a separate hard drive.

!!!3. Err On The Side Of Backing Up Too Much Data

On my home computer, I backup my entire "Home" partition (which is more-or-less equivalent to the "My Documents" folder on Windows).  Is there stuff in that folder that I don't need to back up on a daily basis?  Of course.  My fear, however, is that I'll lose something that I didn't explicitly add to my list of folders or files to backup.

For example, let's say that I have a very brittle backup process in place where I have to explicitly specify each top-level folder that I want to backup.  Later on, I start using a great new program like [[Thinking Rock|http://www.thinkingrock.com.au/]], and choose to save my data under {{{C:\Documents And Settings\Tom\My Documents\ThinkingRock"}}}.  If I forget to add the "ThinkingRock" folder to my backup process, I would then lose some pretty important information if I lost my disk.

Obviously, I prefer to sleep better at night by erring on the side of unnecessary backups.  It might cost a little more, but it's well worth it when you think about the downside potential.  

!!Recommendations

Now that I've (hopefully) convinced you to institute some sort of regular, automated backup process, I'll leave it up to you to find the best tools to satisfy your needs.  There are tons of tutorials online, but if you need some help getting started, here are some recommendations:

!!!Software

* [[SyncBack|http://www.2brightsparks.com/freeware/freeware-hub.html]] (Windows)
** I've been using the free version of this software for over a year now and it has worked flawlessly.  The non-free version ([[SyncBackSE|http://www.2brightsparks.com/syncback/sbse.html]]) gives you a lot more functionality and flexibility, such as backup snapshots, built-in encryption, and built-in compression.
* [[SuperDuper!|http://www.shirt-pocket.com/SuperDuper/SuperDuperDescription.html]] (Mac)
** I've tried Unix-based backup solutions for the Mac, but they haven't worked well with the Mac's "eccentricities".  SuperDuper makes backing up your Mac very easy and automatic.  There is a free version, which is nice for evaluating the software. In my opinion, it's not very useful for day-to-day use since it doesn't make it possible to do incremental backups.
** I can also vouch for this product's user forums.  They are very helpful, and the author of the software posts to them on a very regular basis.  
* [[rsnapshot|http://www.rsnapshot.org]] (Linux)
** I'm assuming that most people reading this tutorial don't use Linux, but it's my "main" OS at home, and the absolute best tool for local backups is rsnapshot.  It is robust, rock-solid, and, best of all, free-as-in-speech.  I not only use it to backup my home partition, but I also use it to do automated cold backups of some of my Mysql databases.  

!!!Hardware

The absolute best hardware that I've found for storing my backups inexpensively is the [[Western Digital My Book Essential|http://www.newegg.com/product/product.aspx?item=N82E16822136025]] external hard drive.  Here's my reasoning:

* ''It's fairly inexpensive'':  It's typically only $20 more than a plain hard drive without the external enclosure.  This premium is well worth it in my opinion, since third-party external enclosures can be a real pain to use.  
** Please note that there are a ''lot'' of "My Book" models available, some of which costs several hundred dollars.  If you don't have a strong reason to purchase one of the higher-end drives, then I don't recommend that you upgrade.  It's my opinion that this drive is ''more than good enough'' for backups.
* ''It's relatively energy efficient'':  I've used multiple types of external drives in the past, and one of my chief gripes is that they run all of the time, which wastes energy.  This drive "spins down" its platters after a certain period of time, and quickly brings them back up when needed.  This feature not only saves energy, but it also keeps the drive cooler.
* ''Cool and quiet'':  Cool, quiet hardware is usually reliable, long-lasting hardware.  
* ''It's fast'':  I know, it only has a USB 2.0 interface, and you can't imagine doing //any// file IO over USB.  Well, for my backup needs, it's been more than fast enough.  Actually, I'm so happy with the speed that I use one of my old USB My Books as a regular hard drive too.
* ''It can be as "dumb" as you need it to be'':  The My Book has a feature that allows you to initiate a backup by pressing a button on the hard drive, making it a somewhat "smart" piece of hardware.  This feature only works on Windows however (I'm a Linux user), and it's hard to automate the physical act of pressing a button, so I don't use this feature.  Thankfully, this drive still works very well with my Ubuntu Workstation.

!!Conclusion

Sooner or later, all hard drives fail, so you need a reliable backup plan if you can't live without the data.  Hopefully, this tutorial will help some people move closer to this goal.
The page itself really isn't that funny, but its definitely one of the funnier url's that I have ever seen:

* http://dontbejealousthativebeenchattingonlinewithbabesallday.com
Since I'm a big fan of [[Python|http://python.org]] and cross-platform programming, I've been checking in on the [[PyGame|http://pygame.org]] project for a while. The PyGame software allows you to create sophisticated video games using Python.  These libraries are cross platform, and Python can make game creation much easier.A lot of changes have occurred over the last year, and I was very happy to see during my last visit that someone has created a free version of Dance Dance Revolution called [[PyDance|http://icculus.org/pyddr/]]. Not only does it look nice and play well with a keyboard, but it will also work with a dance pad. Not bad for a free program written in a high-level language.I definitely recommend the game, even if you don't have a dance pad. No matter what, you should definitely install the PyGame libraries and try out a few of their games.
I've been using Java since 2000 for a variety of tasks, mostly related to software administration and maintenance. A common request from co-workers and some of my geekier friends is a list of recommended resources for Java training.

I've built this list a couple of times, and I keep losing the old lists, so I thought that I would put it on my wiki so it would be a little more permanent.  I hope that a few people find it helpful.

!! Dead-Tree Book Resources
* [[Just Java|http://www.amazon.com/exec/obidos/tg/detail/-/0131482114/qid=1120576682/sr=8-1/ref=sr_8_xs_ap_i1_xgl14/102-1756582-3155308?v=glance&amp;s=books&amp;n=507846]] - //The// Java bible.  No matter what you do with Java, you'll need this book eventually.  Not only is it very informative, it is entertaining and very well written.  It's one of those truly excellent technical books that you see so very rarely in a jungle of crap.
* [[Sun Certified Programmer & Developer for Java 2 Study Guide|http://www.amazon.com/exec/obidos/tg/detail/-/0072226846/qid=1120577095/sr=8-1/ref=sr_8_xs_ap_i1_xgl14/102-1756582-3155308?v=glance&amp;s=books&amp;n=507846]] - The only book you'll really need to become a Sun Certified Java Programmer.  Also very well written.
* [[Head First Java|http://www.amazon.com/exec/obidos/tg/detail/-/0596009208/qid=1120577361/sr=8-1/ref=sr_8_xs_ap_i1_xgl14/102-1756582-3155308?v=glance&amp;s=books&amp;n=507846]] - I haven't actually read this book, but I've heard wonderful things about it from multiple sources, and it was written by the same authors of the previous bullet point.  I look forward to reading the other books from the succesful [[Head First series|http://headfirst.oreilly.com/]].
* [[Core Servlets and Javaserver Pages|http://www.amazon.com/exec/obidos/tg/detail/-/0130092290/qid=1120577790/sr=8-1/ref=sr_8_xs_ap_i1_xgl14/102-1756582-3155308?v=glance&amp;s=books&amp;n=507846]] - A good book for learning the basics of JSP's and servlets.  I recommend checking this book out from the library because it's a fairly quick read.  Another cool thing about this book is that there are a lot of excerpts available at http://www.coreservlets.com.  Definitely check out the web site if you're interested in this book.
  
!! Online Book Resources
* [[How To Think Like A Computer Scientist|http://www.ibiblio.org/obp/thinkCS/java/english/]] - This is a good, free start to programming with Java.
* [[Thinking in Java|http://www.mindview.net/Books/TIJ/]] - This is an ''excellent'' free book for programming in Java.  It's quite possibly the best free programming book available for any language.
  
!! Web Sites
* [[JavaRanch|http://javaranch.com]] - I ''highly'' recommend this web site if you are a beginning Java programmer.  I feel that the following sections are particularly good:
** [[Forum|http://saloon.javaranch.com/cgi-bin/ubb/ultimatebb.cgi]] - I received some great support from this forum on numerous occasions, but it was especially useful when I was studying for the SCJP test.
** [[Campfire Stories|http://www.javaranch.com/campfire.jsp]] - Tutorials and tips 
** [[Bunkhouse|http://www.javaranch.com/books.jsp]] - The best place for unbiased reviews of Java books.
** [[Cattle Drive|http://www.javaranch.com/cattledrive.jsp]] - I am a former pupil in their Cattle Drive class, and found it to be a very good educational value for beginners.  
* [[Roedy Green's Java & Internet Glossary|http://mindprod.com/jgloss/jgloss.html]] - His [[exception page|http://mindprod.com/jgloss/exception.html]] is my favorite place to go when I'm stuck on a particular error and need a little guidance.  Also, it includes a treasure trove of documentation and tutorials to help you with most basic tasks. It's an absolutely essential resource.  
  
''Note'': Please note that I linked to Amazon for all of the books listed above, but I'm not getting a kickback for doing so.  You should be able to find a lot of these books at your local library, and they should all be available from discount book sites such as [[half.com|http://half.com]].

Good luck and have fun!
/***
http://tiddlystyles.com/#theme:Flickr
!General Rules
***/

/*{{{*/
* {
 margin: 0px;
}

#displayArea {
 margin: 1em 17em 0em 2em;
}

a,
.button{
 color: #0063dc;
 text-decoration: none;
 background: transparent;
 border: 0;
}

a:hover, a:active, a.button:hover {
 color: #ff0084;
 background: transparent;
}
/*}}}*/
/***
!Header /%====================================================================================%/
***/
/*{{{*/
#header {
 color: #ff0084;
 padding: 0px;
}

#titleline{
 background-color: transparent;
 padding:0;
 border-bottom: 2px dotted #ccc;
}

#siteTitle {
 font-size: 2.5em;
 color: #0063dc;
}

#siteSubtitle {
 color: #ff0084;
}

#titleLine a{
 color: #0063dc;
}

#titleLine a:hover{
 color: #ff0084;
}
/*}}}*/
/***
!Popup /%====================================================================================%/
***/
/*{{{*/
#popup{
 background-color: #fff;
}

#popup hr{
 color: #e6e6e6;
}

#popup a{
 color: #ff0084;
 background-color: #fff;
}

#popup a:hover{
 color: #0063dc;
 background-color: #fff;
}
/*}}}*/
* http://googleblog.blogspot.com/2006/03/writely-so.html

Google recently leaked a presentation where they stated that they're developing a service called "GDrive".  Speculators are saying that Google will offer a service where they store the sum of all of the data on your computer.  Some co-workers of mine calculated that it would take something like 1,000,000 terabytes of storage capacity to do this, which would make it larger than any other storage array in the world by a very large order of magnitude.  

I don't agree with the assumption that Google wants you to upload the entirety of your hard drive(s) to their system.  The main reason is that there's no money in it.  A lot of companies have tried and failed to offer this type of service before, and none of them have lasted for more than a year or so.  Google may be smarter and have bigger data centers than anyone else, but that doesn't mean that they can make this sort of service profitable.  

What's much more likely is that Google wants to act as a centralized storage repository for things like, oh I don't know, word processing and spreadsheet documents.  These files are cheaper to store because they're realtively small compared to mp3 and divx files.  It's also cheaper to serve and retrieve those files when users want to edit them.

Now Google just needs a way to get you to store your Word and Excel docs on their servers.  This is where Writely comes in.  Google will make Writely free to use, and you will be encouraged to save your files on your GDrive.  You'll probably have the option of saving them on your desktop, but most people will find that to be an unecessary hassle the vast majority of the time.  

If the Writely + GDrive model works for Google, I wouldn't be surprised if they leveraged a few more of their services in order to get you to store more of your hard drive on your GDrive.  For example, it would be fairly simple for Google to "transfer" movies from their Google Video service to your GDrive.  Of course, they wouldn't really transfer anything to your GDrive.  They would just need as many copies as would be necessary to satisfy the maximum number of predicted concurrent users.  Or, in other words, it may look like there's a copy of "Capote" in yours and a couple thousand other people's GDrives, but in reality it's just a pointer to a shared copy of the digital video.  

It's not hard to see how Google could extend this model to include digital images, mp3's, PowerPoint presentations, you name it.  Which OS you choose to run would be pointless, because the majority of apps that you use and their associated files would be stored on a free, centralized, redundant, and virus-free repository.  For most people, it would actually be a pretty great service.

So yes, Google doesn't want you to back up your entire hard drive to your GDrive, or at least not all at once.
I've been using the [[Groovy|http://groovy.codehaus.org/]] programming language a lot lately at work to programmatically interact with systems that have Java interfaces. Simply put, it's great, and it really makes my job as a sysadmin of Java middleware easier. Also, if you've tried other Java-based scripting languages like [[Jython|http://www.jython.org/]] in the past but have been disappointed (like me), then I definitely recommend that you check Groovy out.

I think that the admin angle of the language is lost on most potential users, however. Groovy is a great language for writing small-to-medium-sized administrative scripts, and it's even a great language for non-"real" programmers who want to start dipping their toes into the pool of Java. For those who are considering whether they want to put in the time to learn a little about Groovy, here are some of the advantages that I see as a sysadmin:

!!No More Compiling & Building

First of all, if you're still compiling your Java apps using javac, then stop reading this article and start using [[Apache Ant|http://ant.apache.org/]]. You're wasting your time with javac, even if most of your scripts are pretty small. Ant is a great tool that has saved me a ton of time, but it's definitely not without it's own learning curve and occasional problems, however.

With Groovy, there is no explicit compilation step, just like with Perl and the Korn Shell. You just write your script, run it, fix and problems, run your script again, and then rinse and repeat if necessary. This is a much more intuitive programming workflow for most sysadmins.

!!Easier Deployments

If your script is going to eventually run on a server, all you have to do to deploy it is to upload it to the server. From there, you can run it like any other Perl or shell script. You don't have to worry about writing or deploying wrapper scripts for your class or jar files that actually invoke your program.

!!Simpler Syntax

Here are some of the syntactic niceties offered by Groovy:

* Like Python and the Korn Shell, Groovy doesn't force you to end every line with a semi-colon if it only contains a single statement.
* Loose typing, so you don't have to declare a variable's type when you create it.
* Fewer brackets
* No forced classes
** In Java, every file is explicitly defined as being a class. You don't have to do this with Groovy.
* Multiple Classes In One File
** Tired of following OO design standards that force you to create 8 separate class files for a relatively simple program? Put them all in a single file in Groovy and simplify your life.
* More features that I'm just now discovering like closures and Javabean-like accessors.

!!Text Editor Friendly

I was reading a blog article by a Sun developer the other day where he said that he didn't like Groovy because it didn't integrate very well in Netbeans (an IDE). He mentioned that there was no decent code completion module for Groovy yet like there is for Java. It therefore took him longer to write the following print statement in Groovy:

{{{println "hello"}}}

...than it did to write the equivalent (and much longer) statement in Java with the help of code completion:

{{{System.out.println("Hello");}}}

Although this statement is a little short-sighted in my opinion ("I don't like cars because there's no place to hook up my horse"), it points out one of Groovy's strengths: it can be used very well with only a text editor and a command prompt. Groovy, like Perl and shell languages, was never designed with whiz-bang IDE's in mind. It's brief, powerful syntax and development tools were designed to make programming with it so easy that you could do it using Notepad and the command prompt if you wanted and still be very efficient and effective.

Groovy's ease-of-use outside of an IDE is good news for most sysadmins for a couple of reasons. First, they spend a good portion of their days using text editors to do things like view log files, change config files, et cetera. Being able to use the same text editor to also write scripts lowers the learning curve and is very synergistic. Also, most sysadmins usually don't have IDE's like Eclipse or Netbeans installed on their system. For the type of Java programming that they typically do, they view IDE's as being bloated and having an unnecessarily high learning curve.

!!Groovy Shell

One of [[Python's|http://www.python.org]] greatest strengths is the fact that it comes with an interactive interpreter. Groovy also includes an interactive interpreter, and its major benefit is that it allows you to evaluate small snippets of code quickly and easily. This comes in handy when you're having problems getting a chunk of code to work, and also when you're "kicking the tires" on a new third-party library or language feature.

!!Conclusion

There are a lot of new scripting languages out there that are Java-based, and their potential benefits are numerous. However, it can be difficult to pick one that is worth the time and attention necessary to achieve proficiency. In my opinion, Groovy fits the bill and then some, and is especially good for people who need to write small-to-medium-sized programs that interact with Java systems.
Welcome to my web site!  This is my personal scratchpad for articles related to ''technology'', ''frugality'', and (very ra