Braving the Applescript syntax kerfuffle

Ok, I made it through. Unscathed, relatively. Applescript bowed to my power. Well, sort of.

The problem: My workflow in Omnifocus is to use the task defer date heavily to basically build up a task list for today. However, I have several “spheres” of tasks, like reading books, taking online classes, exercising.. But if I defer ‘book’ tasks for a few days, the next day contains like 8 reading tasks. Currently I keep ‘rebalancing’ these kind of pile up days by hand. But that should be scriptable.

My rules and Applescript to run my “Rebalance” script are up on github.

It turned out that I needed to form a few different ‘types’ of rules:

  • if any today task contains more than X number in a specific context, then push anything over X to the next day
  • if any today task contains more than X number in a specific PROJECT, then push anything over X to the next day
  • if any today task contains more than X number in a specific FOLDER, then push anything over X to the next day
  • if there is not a task in X project/folder today, then add a defer date to one task in X project/folder for the coming X days

These rules are realized in these examples:

  • there should be at most 1 video task per day (Project: Videos)
  • there should be at most 1 class per day (Folder: Classes / Skills Project: Tech)
  • there should be 1 interview practice thing every 4 days (if there isn’t any Interview Practice stuff today, set one for 4 days from now) (Project: Interview Practice)

And that’s it! The hardest part was definitely the Applescript syntax, but there are so many examples on the web, that you can cobble anything together. The next hardest thing was figuring out my personal rules. That just takes time and living with your own schedule and replicating what you do manually every day, into replicatable rules. What do you always find yourself always doing to your Forecast?

Full script:

property todayStr : "Today"
property missingContextStr : "" -- string when context is missing
property barStr : "--" -- BAR string after header
property preStr : "?" & tab -- PRESTRING prior to each list item
property actGroupStr : "Project Group" -- pre-string when task is action group (shows when expandGroup = false)
property sepStr : " : " -- SEPARATOR string between context and task

tell application "OmniFocus"
    tell front document
        log "STARTED Rebalancing"
        
        -- get list of today's tasks
        set nowDate to current date
        set nowHours to hours of nowDate
        
        set todayDate to current date
        set todayDate's hours to 0
        set todayDate's minutes to 0
        set todayDate's seconds to 0
        set tomorrowDate to todayDate + 1 * days
        set todaysTasks to (flattened tasks where completed is false and ((defer date ? todayDate and defer date < tomorrowDate) or (due date ? todayDate and due date < tomorrowDate)))
        
        set maxNumReadingTasks to 2
        set currentNumReadingTasks to 0
        set maxNumVideoTasks to 1
        set currentNumVideoTasks to 0
        set maxNumClassTasks to 1
        set currentNumClassTasks to 0
        set maxNumExerciseTasks to 3
        set currentNumExerciseTasks to 0
        set isInterviewPracticeToday to false
        set isProgrammingPracticeToday to false
        set isRelationshipTaskToday to false
        set isRecreationalVideoToday to false
        
        repeat with t in todaysTasks
            
            if (nowHours > 10) then
                if (name of t = "work out") then
                    set t's completed to true
                end if
                if (name of t = "run") then
                    log my logThis(t, tomorrowDate)
                    set t's defer date to tomorrowDate
                end if
                if (name of t = "do shoulder PT exercises") then
                    log my logThis(t, tomorrowDate)
                    set t's defer date to tomorrowDate
                end if
                if (name of t = "make green smoothie (add Omega 3)") then
                    set t's completed to true
                end if
                if (name of t = "write 3 pages") then
                    log my logThis(t, tomorrowDate)
                    set t's defer date to tomorrowDate
                end if
            end if
            
            if (context of t is not missing value) then
                if (name of context of t = "book") then
                    set currentNumReadingTasks to currentNumReadingTasks + 1
                    if (currentNumReadingTasks > maxNumReadingTasks) then
                        log my logThis(t, tomorrowDate)
                        set t's defer date to tomorrowDate
                        
                    end if
                end if
            end if
            
            if (containing project of t is not missing value) then
                if (name of containing project of t = "Videos") then
                    set currentNumVideoTasks to currentNumReadingTasks + 1
                    if (currentNumVideoTasks > maxNumVideoTasks) then
                        log my logThis(t, tomorrowDate)
                        set t's defer date to tomorrowDate
                        
                    end if
                end if
                
                if (name of containing project of t = "Maintenance") then
                    set currentNumExerciseTasks to currentNumExerciseTasks + 1
                    if (currentNumExerciseTasks > maxNumExerciseTasks) then
                        log my logThis(t, tomorrowDate)
                        set t's defer date to tomorrowDate
                        
                    end if
                end if
                
                if (name of containing project of t = "Interview Practice") then
                    set isInterviewPracticeToday to true
                end if
                
                if (name of containing project of t = "Practice") then
                    set isProgrammingPracticeToday to true
                end if
                
            end if
            
            if (folder of containing project of t is not missing value) then
                if (name of folder of containing project of t = "Classes / Skills" and name of containing project of t = "Tech") then
                    set currentNumClassTasks to currentNumClassTasks + 1
                    if (currentNumClassTasks > maxNumClassTasks) then
                        log my logThis(t, tomorrowDate)
                        set t's defer date to tomorrowDate
                        
                    end if
                end if
                
                if (name of folder of containing project of t = "Relationship") then
                    set isRelationshipTaskToday to true
                end if
                
                if (name of folder of containing project of t = "Recreation / Video" and name of containing project of t = "Tech") then
                    set isRecreationalVideoToday to true
                end if
                
            end if
            
        end repeat
        
        if (isInterviewPracticeToday = false) then
            my deferInProject("Interview Practice", 4, todayDate)
        end if
        
        if (isProgrammingPracticeToday = false) then
            my deferInProject("Practice", 3, todayDate)
        end if
        
        if (isRelationshipTaskToday = false) then
            my deferInFolder("Relationship", 7, todayDate)
        end if
        
        if (isRecreationalVideoToday = false) then
            my deferInFolder("Video", 4, todayDate)
        end if
        
        log "COMPLETED Rebalancing"
        
    end tell
    
end tell

on dateISOFormat(theDate)
    set y to text -4 thru -1 of ("0000" & (year of theDate))
    set m to text -2 thru -1 of ("00" & ((month of theDate) as integer))
    set d to text -2 thru -1 of ("00" & (day of theDate))
    return y & "-" & m & "-" & d
end dateISOFormat

on getTextTask(ListItem)
    using terms from application "OmniFocus"
        set theContext to missingContextStr
        if (context of ListItem is not missing value) then
            set theContext to preStr & (name of context of ListItem) & sepStr
        end if
        set theAction to (name of ListItem)
        set theProject to ""
        if (containing project of ListItem is not missing value) then
            set theProject to (name of containing project of ListItem) & sepStr
        end if
        set deferDate to ""
        if (defer date of ListItem is not missing value) then
            set deferDate to sepStr & my dateISOFormat(defer date of ListItem)
        end if
        
        -- return theContext & theProject & theAction & deferDate --
        return theAction & deferDate --
    end using terms from
end getTextTask

on deferInProject(projectName, deferDays, todayDate)
    tell application "OmniFocus"
        tell front document
            set customProject to first flattened project where its name = projectName
            set customTasks to (flattened tasks of customProject where completed is false)
            repeat with t in customTasks
                log my logThis(t, todayDate + deferDays * days)
                set t's defer date to todayDate + deferDays * days
                return
            end repeat
        end tell
    end tell
end deferInProject

on deferInFolder(folderName, deferDays, todayDate)
    tell application "OmniFocus"
        tell front document
            set customFolder to first flattened folder where its name = folderName
            set customProjects to (flattened projects of customFolder)
            repeat with p in customProjects
                set customTasks to (flattened tasks of p where completed is false)
                repeat with t in customTasks
                    log my logThis(t, todayDate + deferDays * days)
                    set t's defer date to todayDate + deferDays * days
                    return
                end repeat
            end repeat
        end tell
    end tell
end deferInFolder

on logThis(ListItem, deferDate)
    set toLog to "Deferring " & my getTextTask(ListItem) & " to " & my dateISOFormat(deferDate) & return
    return toLog
end logThis

Projects, not contexts, silly

In my last post, I outlined my projected context structure in OmniFocus. When I was just about done creating this context structure, and placing a few sample tasks that I already had, I realized that contexts such as ‘Executing Tech Talks with the team’ is not really a context. It’s a Project. So I rewrote all of my mind map contexts as projects instead, reserving contexts for very simple things:

B5CD085C-2781-439D-9A29-71C9B5C19179

My one goal here is to:
a.) Be able to “have a well-rounded day”, i.e. approx. 1 task included in my Forecast from each of my (now) Project buckets. (this is possible now with all my new projects and sub projects)
b.) Be able to quickly classify a task when I am entering things on the fly. If it’s not dead easy to add a task, I am not going to use the system. (this is possible now with my very limited set of contexts)

So that was a big paradigm shift. I just assumed contexts needed to be very intricate, but in fact my Project usage of OF2 just needed to be taken to the next level.

Next up: Applescripting OF2 to create a Perspective of 1 task from each of my Projects. Stay tuned..

Mind Mapping My Life

There are a few problems that have been nagging me about my task lists:

  1. Not having the right energy at the right time
  2. Losing perspective over what I should be doing right now for maximum effect
  3. Focusing too much on some areas of my life at the exclusion of others
  4. Having task list contexts that are not effective

The first problem seems to be answered in a few books I am aware of, like The Power of Full Engagement, and Two Awesome Hours (which I just purchased and have yet to read).

The second and third ones I think are born of the last problem, which is in regards to my use of OmniFocus 2 for all of my task tracking. I have pretty obvious @contexts like @home (for chores), @kitchen (for cooking), @store, @work, but then also non-location based ones like @read (for paper or digital books), @macbook (for really anything, it’s too general), and @watch (for Netflix, Amazon Prime, Tivo, or technical videos online).

MY GOAL: I want to feel like a well rounded person. I want to go through let’s say 2 or 3 days and feel like all the tasks that I accomplished contributed to all the major areas of my life. I’m a computer programmer, and look forward to the weekend so I could learn some new technology, but frequently Monday rolls around again and all I’ve done is cook, clean the kitchen, vacuum the floors, laundry, cleaned the bathroom. I seem to think that all of that is preparatory work to being able to settle back with a clear mind and be able to pour myself 100% into a project. But that’s the subject of another post, or a therapy session.

Back to having proper @contexts; for a while now, I’ve been trying to find some sort of better guidance around creating @contexts, but not really gotten an Aha! from anything. What is a perfect context that means “having enough energy, at a computer, on a non-recent project, while having an hour of uninterrupted time”??

My @contexts are just too generic- take the @watch for example. Technical videos and The Bachelor just shouldn’t belong in the same @context. And @macbook is also too generic. Half my life is on my computer, but I can’t have one @context for half of my life, and there are very different purposes for different computer tasks, so @macbook can’t be an effective @context.

As an experiment in just laying it all out on the table, making a mind map of everything that I have responsibility for, I did just that. My hope was that that exercise would be translatable to OmniFocus contexts, which would then be much more balanced overall and better segmented according to the ACTUAL areas of my life. Here are portions of what I came up with (using MindNode Lite):

BP3

BP2

BP1

As should be apparent, @contexts based on these categories would be much less location based, and more life area based. And watching technical videos online (@Professional:PersonalDev:WatchingVideos) will now be in a distinct @context than The Bachelor (@Recreation:TV:Tivo).

Next up: creating the OmniFocus @contexts based on this and figuring out how to track ‘balancedness’. Should each day’s set of planned tasks be one task from each of the main subdivisions in the mind map? Or something else? I’m thinking that some of these ‘leaf nodes’ should be tasks and some of them should be child contexts to store other tasks.

Spring’s RestTemplate, Jackson’s JSON deserializing and inner classes

In trying to use Spring 3’s RestTemplate that every article about says is braindead easy, I hit upon a limitation that’s taken me a few hours to iron out.

JSON I am trying to consume:

{"results":[{"id":"1","details":{"title":"First Instructions","alternateTitle":"","location":"3","beginDate":"2002-12-03 15:04:00","endDate":"2003-02-27 03:00:00","frequency":"0","teacher":"1","program":"1","description":"","fees":"","status":"0"}}]}

Hastily written service method:

public CourseResult getCourseResult(Integer id) {
	String uriTemplate = apiUrl + "?id={id}";
	RestTemplate template = new RestTemplate();
	
	List> messageConverters = template.getMessageConverters();
	List> converters = new ArrayList>(messageConverters);
		
	MappingJacksonHttpMessageConverter jsonConverter = new MappingJacksonHttpMessageConverter();
	converters.add(jsonConverter);	
	template.setMessageConverters(converters);
	    
	TclApiResponse response = template.getForObject(uriTemplate, TclApiResponse.class, String.valueOf(id));
	return response.getCourseResults()[0];
}

Domain Object:

public class TclApiResponse {
	
	private CourseResult[] courseResults;

	public CourseResult[] getCourseResults() {
		// TODO Auto-generated method stub
		return courseResults;
	}	
	
	public void setCourseResults(CourseResult[] courseResults) {
		this.courseResults = courseResults;
	}
	
	public class CourseResult {		
		Integer id;
		Details details; 
		
		[getters and setters]...
	}

	public class Details {

	...

	}
}

I was following along with the process described in http://dlinsin.blogspot.com/2009/11/playing-with-spring-resttemplate.html, with the “real” target domain object encased in a wrapper object (in this case TclApiResponse), and kept getting the consistent error:

org.springframework.web.client.RestClientException: Could not extract response: no suitable HttpMessageConverter found for response type [org.tcl.service.TclApiResponse] and content type [application/json]
	at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:77)
	at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:446)
	at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:401)
	at org.springframework.web.client.RestTemplate.getForObject(RestTemplate.java:199)
...

After much trial and error, I finally tried separating out the domain class into distinct, non-inner classes and it started working. With a 20/20 hindsight Google search, I found 1 blog that mentions this:

http://mangayaa.blogspot.com/2009/08/jackson-json-processor.html

Hopefully this saves someone some time.

Spring Roo, and mixing xml and annotated dependency injection of a Property

I used Spring Roo (with Spring 3.0) to create a simple project with a few domain objects and controllers. I wanted to experiment with using the @Component annotation and autowiring, but was having severe problems trying to autowire a String value (the url of a service) into my @Component managed bean, the string being contained in the xml of the application context. After searching several blogs, this is what I found:

  1. it is not possible to autowire a primitive value using annotations
  2. it WAS not possible before Spring 3, now you can do it with Spring Expression Language (spEL)

Here was my next to last attempt:

webmvc-config.xml:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xmlns:p="http://www.springframework.org/schema/p"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:util="http://www.springframework.org/schema/util"
	xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
		http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
				
	<!-- The controllers are autodetected POJOs labeled with the @Controller annotation. 
	<context:component-scan base-package="org.tcl" use-default-filters="false">
		<context:include-filter expression="org.springframework.stereotype.Controller" type="annotation"/>
	</context:component-scan> -->

	<!-- The controllers are autodetected POJOs labeled with the @Controller annotation. -->
	<context:component-scan base-package="org.tcl" />
	<context:annotation-config/>
	
	<!-- Turns on support for mapping requests to Spring MVC @Controller methods
	     Also registers default Formatters and Validators for use across all @Controllers -->
	<mvc:annotation-driven/>

        <util:properties id="appProperties" location="classpath:/app.properties"/> 
...

TclApiService.java:

package org.tcl.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class TclApiService implements ApiService {

	@Value("#{appProperties['tclApiUrl']}") String apiUrl;
	
	@Override
	public ApiResponse call() {
		return null; //breakpoint here, just to check out the value of the apiUrl property 
	}

	public void setApiUrl(String apiUrl) {
		this.apiUrl = apiUrl;
	}

}

app.properties:

tclApiUrl=http://www.mydomain.org/api/courses

No matter WHAT I tried, the tc server startup would fail with errors such as:

No matching bean of type [java.lang.String] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}


or, in the case of my final spEL attempt:

Field or property 'appProperties' cannot be found on object of type 'org.springframework.beans.factory.config.BeanExpressionContext'

Turns out, this is what is happening:
Since the <context:component-scan> tag occurs before the “appProperties” bean definition in webmvc-config.xml, the bean definition is not available when the @Component-ized class is being constructed. The fix is simple, just move the “appProperties” bean definition above any <context:component-scan> tag. In Roo, keep in mind, at least in my case, that there were actually 2 <context:component-scan> tags, one in applicationContext.xml and one in webmvc-config.xml. Putting my properties bean definition above the one in applicationContext.xml solved the problem. Tc Server now started successfully and the property was populated in my @Component class.

Nowhere in official spring documentation could I find the stipulation that it actually matters where the <context:component-scan> tag is placed. Initially I thought that spring might maintain a map of unresolved dependencies which are satisfied in a round robin fashion, but nope, the xml dependencies are resolved first, then the annotated ones – at least in this simple case.

oracle.sql.TIMESTAMP ClassCastException with SqlRowSet

After 2.5 days of smashing my head against Oracle hell (mixed metaphors?), I finally found a workaround for this strange ClassCastException I was having. I only found 2 or 3 Google results with this odd ClassCastException, so decided that my experience was significant enough to share!

ORIGINAL GOAL:
We had some code resembling the following:

String sqlString = “{some SQL statement that contains an ORDER BY clause}”;
Item item = itemDao.read(itemId);
getJdbcTemplate().query(sqlString,
    new Object[]{itemId},
    new RowCallbackHandler() {
        public void processRow(ResultSet rs) throws SQLException {
            ItemActivity itemActivity = new ItemActivity();
            populateItemActivity(itemActivity, rs, item);
        }
});
 

And populateItemActivity just does some stuff to the ItemActivity instance passed in, like:

public void populateItemActivity(ItemActivity itemActivity, ResultSet rs, Item item) throws SQLException{         
    itemActivity.setFileName(item.getName());
    itemActivity.setDate(rs.getTimestamp("LAST_MODIFIED_TIME"));
    itemActivity.setSize(item.getFileSize());      
    itemActivity.setAuditName(rs.getString("AUDIT_NAME"));
}

We discovered that the “AUDIT_NAME” field value was not populated for all returned rows, and in that case, the ItemActivity object should be populated with the “AUDIT_NAME” value in the record just before it, ordered by itemId and date. My fear was that the processRow(ResultSet rs) callback method was not guaranteed to process rows in the same order that they appeared in the resultset, so my attempt to copy back from the immediate prior record to copy its “AUDIT_NAME” value would be foiled and possibly incorrect.

Looking for a method that I could manipulate the entire ResultSet at one time, and so control the iteration and set a variable that I could copy between row processing, I found the JdbcTemplate.queryForRowSet(String sql, Object[] args) method, which returns a SqlRowSet I thought I could use to iterate explicitly.

We happen to be using Oracle, and a Timestamp value is one of the values in the returned SqlRowSet. This code was throwing the ClassCastException:

public void populateItemActivity(ItemActivity itemActivity, SqlRowSet rs, Item item) throws SQLException{                  
    itemActivity.setFileName(item.getName());
    oracle.sql.TIMESTAMP timestamp = (oracle.sql.TIMESTAMP) rs.getObject("LAST_MODIFIED_TIME ");  <--ClassCastException
    Date date =  (timestamp == null) ? null : new Date(timestamp.dateValue().getTime());   
    itemActivity.setDate(date);
    itemActivity.setSize(item.getFileSize());
    itemActivity.setAuditName(rs.getString("AUDIT_NAME"));
}

NOTE: the code at line 3 and 4 above I got from http://jira.springframework.org/browse/SPR-4886, who appeared to be having the same issue with SqlRowSet returning an oracle.sql.TIMESTAMP instead of a java.sql.Date with a call to rs.getObject.

No matter what I tried, I could not resolve the ClassCastException. Debugging in Eclipse showed that the Object returned from rs.getObject() was a oracle.sql.TIMESTAMP, but casting it to that same class resulted in the ClassCastException. Infuriating! Even gathering other developers, we could not figure this out.

So, NEW GOAL:
So I changed tack and found this other JdbcTemplate.query method with a different signature using a ResultSetExtractor, which allowed me to deal with the entire recordset (it's a ResultSet now instead of a SqlRowSet with the other attempt) at once (instead of a callback method for each row), letting me handle the row iteration explicitly:

getJdbcTemplate().query(sql,
        new Object[]{vaultItem.getVaultItemId()},
        new ResultSetExtractor() {                                             
          public Object extractData(ResultSet rs) throws SQLException,
            org.springframework.dao.DataAccessException {
                 String auditNameBuffer = null;
                 while (rs.next()){
                      ItemActivity itemActivity = new ItemActivity();                                                        
                      String auditName = rs.getString("AUDIT_NAME");
                      if ((null != auditName) ){
                          auditNameBuffer = auditName;
                      }                                                               
                      itemActivity.setAuditName(auditName);
                 }
                 return null;
            }      
     }
);

Project completed, but still don’t know what caused the ClassCastException.

Java Spring, Transactions and MySQL

I spent about 4 hours yesterday trying to figure out why transactions were not rolling back inserts into my mySQL database yesterday, and it turned out to be a problem in somewhere I least expected. 

There are a couple of really good and concise tutorials on how to set up transactions in Spring:

What is supposed to happen is if any exception is thrown in your base class, the transaction-wrapped proxy (TransactionProxyFactoryBean) that Spring is using will roll back your object state, which with Hibernate involved, should cancel any db operations that would have normally taken place. I turned on DEBUG logging, and the transaction was noted as being rolled back but the db was still merrily taking inserts from the frontend, even with the exceptions being thrown.

I finally found this forum that mentioned transactions with mySQL specifically:
"1. if you are using MySQL make sure you have InnoDB tables (not MyISAM or something else)."
Indeed, “show table status like 'testTable'\G;” displayed MyISAM, so a simple “alter table testTable TYPE = InnoDB2;” solved my problem. Craziness. I’m not intimately familiar with MySQL, but there are many debates going on about what engine to use, just google “mysql myisam innodb”. 

MyISAM = speed but does not support transactional rollback
InnoDB = reliability and supports transactions

The best advice I found if I were architecting a mySQL enterprise solution would be to use InnoDB on the master machines, where you might need rollback capability in case of error, and then MyISAM on any slaves, since at the point or replication from the master you should have clean data and the speed of replication would be increased by the usage of MyISAM.  

Anyway, hopefully this saves 4 hours of time from someone else who might be mystified by non-rolling back database rows in spring hibernate transactions like I was.

Google Tasks

Harkening back to my previous post on Task Reminder Services, I was watching DL.tv on TiVo recently and they showed briefly the new Tasks feature in GMail. It’s a new pane that when enabled in Google Labs preferences, allows you to store tasks and subtasks, check them off, and even convert emails to tasks. It also allows you to set a due date for that task, and I was getting excited as I searched for a “Mail me a reminder” feature.. but no such luck. I did go to their forums and request the feature though. Gmail +Tasks (should equal) Reminder Emails.

2 cooking rss feeds I can’t get enough of

There are some GReader feeds I don’t know why I’m still subscribed to. Either it updates too quickly for me to keep up with or I’m interested in maybe 1/10th of the posts. The little (856) unread next to the feed just makes me feel guilty until I poke in, look at the top couple and inevitably just mark all as read.

Not so with the 2 cooking rss feeds I discovered a short while ago:
The Kitchn: http://feeds.feedburner.com/apartmenttherapy/thekitchn
Just Bento: http://feeds.feedburner.com/justbento

The Kitchn is a very colorful feed, perfect for a food-related blog, with nice basic tips for amateurs like myself. More often than not, San Francisco gets a shoutout, so seems to be local, with local tips.

Just Bento is a curiously fascinating, DIY guide to packing your own bento lunches or whenever else you might need a well put-together portable meal.

Bye Bye Sandy

For the last year or so, I’ve been subscribed to a free service called I Want Sandy. Sandy is like a virtual calendar assistant, in that it allocates me a personal email address, which I can send a specially formatted email to. That email could be something like “remind me to pick up my dry cleaning next Wednesday at 6:45pm”, in the subject line or body of the email. At that time, I get an email back from Sandy with a reminder about my dry cleaning.  I certainly am not a power user, using it maybe once or twice a week, but enough to make it convenient for those occasional reminders.

Well, I just got a notice from them that they are shutting their virtual doors on Dec 19. Looks like Twitter has bought their technology, and their CEO is also joining Twitter.  Now, on to look for alternatives:

http://twitter.com/timer – Pros: backed by Twitter’s solid foundation Cons: Limited by Twitter’s access methods – text or browser, no email
http://gopingme.com/ – Pros: They seem to be ready for Sandy’s abandoned users, as they have a tutorial for ex-Sandy users on their home page. Just like Sandy, you can postpone event reminders (reply to reminder email with “remind me about this in 1 day”) type of thing.
http://www.rememberthemilk.com – Pros: Has email-task and receive-email-reminder functionality, but they seems to be 2 distinct operations. Cons: seems to be a full fledged task management system, which I already use Omnifocus for. I really need something just to replace Sandy with- a simple email/get-reminder-later system.

Looks like I will try http://gopingme.com, even with the cheesy service name.