fill the void

WordPress Tip: Debug Logging

Today, I needed a debug log for WordPress. Luckily, the WordPress Codex details just a couple lines of code to turn on error logging. This method is especially nice for hosted environments like DreamHost, where there’s no access to php.ini. Simply add the following to wp-config.php:

# http://codex.wordpress.org/Editing_wp-config.php#Debug
define('WP_DEBUG', true);
# http://codex.wordpress.org/Editing_wp-config.php#Configure_Error_Log
@ini_set('log_errors','On');
@ini_set('display_errors','Off');
@ini_set('error_log',dirname(__FILE__).'/logs/php_error.log');

Dedupe Files with 50 Lines of Ruby

Somehow, my iPhoto library contains duplicates. Lots of duplicates. I tried Brattoo Propaganda’s Duplicate Annihilator, but while it took care of many, many photos, there are some photos and quite a few movies duplicated. Ruby to the rescue!

I wrote a 50-line Ruby script to list out the duplicate files. The script uses SQLite3 and SHA-1 digests to identify multiple copies of each file in a directory. The end result was 2K duplicate files and 20 GB of freed disk space. Woot.

Here’s the script:

#!/usr/bin/ruby

# deduplicate_files.rb

require 'rubygems'
require 'sqlite3' # Look at 'http://github.com/luislavena/sqlite3-ruby' then do 'sudo gem install sqlite3-ruby'
require 'digest/sha1'
require 'pathname'

# Pass in the directory or assume the current one.
arg = ARGV[0] || "."
root_path = Pathname.new(arg).realpath.to_s
puts "Examining #{root_path}"

# Create a SQLite3 database in the current directory.
db = SQLite3::Database.new("deduplicate_files.db")
db.execute("create table files(digest varchar(40),path varchar(1024))")

# Recursively generate hash digests of all files.
Dir.chdir("#{root_path}")
current_file = 0
Dir['**/*.*'].each do |file|
  path = "#{root_path}/#{file}"
  # Ignore non-existent files (symbolic links) and directories.
  next if !File.exists?("#{path}") || File.directory?("#{path}")
  # Create a hash digest for the current file.
  digest = Digest::SHA1.new
  File.open(file, 'r') do |handle|
    while buffer = handle.read(1024)
      digest << buffer
    end
  end
  # Store the hash digest and full path in the database.
  db.execute("insert into files values(\"#{digest}\",\"#{path}\")")
  # Print out every Nth file.
  puts "[#{digest}] #{path} (#{current_file})" if current_file % 100 == 0
  current_file = current_file + 1
end

# Loop through digests.
db.execute("select digest,count(1) as count from files group by digest order by count desc").each do |row|
  if row[1] > 1 # Skip unique files.
    puts "Duplicates found:"
    digest = row[0]
    # List the duplicate files.
    db.execute("select digest,path from files where digest='#{digest}'").each do |dup_row|
      puts "[#{digest}] #{dup_row[1]}"
    end
  end
end

Cocoa Tip: NO vs nil for Preferences

Recently, I wanted to store a key/value pair in Info.plist or in preferences (NSUserDefaults) and know if the key existed and the value if it did. I started by using boolForKey, but this method doesn’t distinguish between NO and nil. Instead, I switched to valueForKey. Here are six lines of code to detect the three values:

NSDictionary *infoPlistDictionary = [[NSBundle mainBundle] infoDictionary];
BOOL doesKeyExistInPlist = [infoPlistDictionary valueForKey:PLIST_KEY] != nil;
BOOL isKeyTrueInPlist = [[infoPlistDictionary valueForKey:PLIST_KEY] boolValue] != NO;
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
BOOL doesKeyExistInPrefs = [defaults valueForKey:PLIST_KEY] != nil;
BOOL isKeyTrueInPrefs = [[defaults valueForKey:PLIST_KEY] boolValue] != NO;

Cocoa Tip: URLs in NSTextFields

Apple has a great Technical Q&A document for embedding a URL in an NSTextField. However, after implementing their approach, I found two issues:

Font: When I clicked the link, the URL’s font changed. To keep it consistent, I needed to set it myself. I added the following code to Apple’s NSAttributedString category from the link above:

[attrString addAttribute:NSFontAttributeName value:[NSFont fontWithName:@"Lucida Grande" size:13] range:range];

Cursor: With Apple’s code, the pointing hand cursor only appeared after I clicked the link. To get the correct cursor before the link is clicked, I followed the advice from a cocoa-dev post and subclassed NSTextField to override resetCursorRects.


Don’t make users feel bad

Matt Drance posted an interesting thought the other day about the average consumer rejecting bad user experience. When helping out my parents or other non-technical folk, I’ve never found that. At least, not exactly. Average consumers do not think about user experience. They don’t think about workflows, calls to action, marketing copy, or corner cases. Consumers just use stuff. When something goes wrong, it’s their fault. They internalize any problems. They blame themselves.

When consumers have a great user experience, they don’t notice. They don’t wonder how you designed the product so well. They just use it and move on, but that’s a good thing. They shouldn’t feel bad while using your product. Don’t make users feel bad because eventually they’ll choose a better user experience as a coping mechanism.

User experience: Because your users internalize your product’s failures.