Tuesday, January 20, 2009

Moments

you know those moments in life where you go "it works!" and then you feel really proud of yourself (or hopefully whoever made it work), i just had one of those moments. several actually.

a bit of explanation: i'm currently developing a piece of software to allow me to tag files. yeah, i know a bunch of software already exists for that, but none of it works for me and my workflow. i've got to have things work the way i need them to, not the way people think i need them to. i also make things easy to do (for me) so i've got a highly customized mbp but most of the stuff is in the background so someone else can sit down and use it like any other mac without knowing the difference.

i was using a piece of software called Punakea which fit into my workflow but it wasn't as extensible or as flexible as i needed. there were other faults as well, but the important ones were that it couldn't build multi-level tag trees(ie tag something "hello/world" and end up with the file in "root/hello/world") and it cross-referenced related tags in the tag tree(a good idea in principle, but messy on the filesystem in practice).
the guys at Punakea have a wonderful product but it just doesn't fit my needs. i set out to write my own.
(note: any command to enter into the command-line will start with a # and then space and then end with a ;)
it will follow some of the same behaviours of Punakea but improve or modify them in some ways. there will also be several different ways to interact: command-line (ie # tag somefile sometag;), drag and drop onto a drop box, choose a file in finder and right-click it, etc.
i hacked Punakea's system a bit and did add command-line, but it wasn't enough for me.

i'm building the program in 2 steps: dependent functions and the core. the dependent functions are things like getting a directory listing, linking files, getting a file's tags, setting a file's tags, etc. i've put these functions in platform.c (platform because they'll probably change depending on whether you run mac, linux, win, etc) and osxutils.c (i've copied some code for supporting getting/setting tags, file aliasing). to port the program to another platform, all you need to do is fill in the functions in platform.c with platform equivalents. right now it's setup for osx 10.5 (maybe earlier, i don't have a system to test with).
the core has yet to be written because i was working very hard on the dependent functions for a while.

the coolest thing so far(and the "moment") is my make alias function. an alias in osx is simply an empty file with a resource fork describing the target(see http://en.wikipedia.org/wiki/Alias_(Mac OS) for details). note that the data fork(the one you see in finder and the terminal) is simply an *empty file*. a plain old empty file! now take that file, add some code to it, and now you have an alias that you can execute from the command-line to resolve the original location!
afaict, finder doesn't touch the data part, nor does the rest of the system, so the data fork will always be there (unless you move it to a non-hfs filesystem in which case the alias will no longer be an alias although i've seen that finder copies the resource fork into ._filename on non-hfs systems so it just might work).
you can do cool things like # ./aliasname; or # cd "`bash -c aliasname`"; or even put the alias in your $PATH and do # aliasname; if the original is executable!
the link function is smart enough to generate code for the type of file/folder it's linking. if it has it's execute bit set, it will output code that will execute the original (with any arguments) and also allow you to do # aliasname --resolve-alias; to get the real file. if the original file isn't executable, running # bash -c aliasname; will output the real file's location. if the file is a directory, you run # some_command_to_work_with_directory "`./aliasname`"; you can also add code to your .profile that will do the cd work for you transparently so you can just do # some_commane aliasname;.
right now, i'm using hfsdata from osxutils to grab the real file's location. bash code is inserted into the alias to run it. my plan is to integrate the resolving code from hfsdata into each alias created. it would be nice if i could hardlink data but unfortunately i can't so each alias will be slightly larger than they normally would be.
it's all very cool because now i can do stuff like tagging a file "Bin" or something like that and within seconds, it will show up in my $PATH under $HOME/bin as an alias to the original file and that alias will now execute that original file.
usage case: i build product-x.y.z in my Development directory but i don't want to install it in my system directory. in fact, i'm so lazy that i don't want to do anything with the built files. i'm not lazy enough to avoid tagging the executables though! i would tag one of the executables "Bin" and then do # executable; to run it because it would then be in my path!
it's freaking awesome!

sample alias code:
#!/bin/bash
if [ $1 ]; then
if [ "$1" = "--resolve-alias" ]; then
echo "`hfsdata -e "$0"`";
else
`hfsdata -e "$0"` $@
fi;
else
`hfsdata -e "$0"` $@;
fi
end_code
code to add to .profile for cd-ing into an alias(you can even do # cd somealias/some_dir_under_original_path; ! this will work for any alias that points to a directory, you only need hfsutil from osxutils. oh, and it's kinda slow only when dealing with aliases). For all intents and purposes, assume you are using a symbolic link when using this version of cd. What this will not do is resolve the alias and echo it back, that's what hfsutil is for.

If you don't want this to replace cd, change the line that reads "cd() {" to something else like "cda() {" or "idontwantcd() {"

begin_code:

string_substring() {
if [ $3 ]; then
local to=$3;
if [ $4 ]; then
to=$(($3 - $2));
fi;
echo "${1:$2:$to}";
else
echo "${1:$2}";
fi;
}
string_lastindexof() {
local ns="`string_length "$1"`";
local nf="`string_length "$2"`";
local -i tmp=$(($ns+1-$nf));
local -i k=$tmp;
local subs="";
while [ $k -ge 1 ]; do
Delay
subs="`string_substring "$1" "$k" "$nf"`";
if [ "$subs" == "$2" ]; then
echo "$k";
return 0;
fi;
k=$(($k-1));
done;
}
string_length() {
local tmp="$1";
echo "${#tmp}";
}
aliaspath() {
if [[ ("`hfsdata -e "$1" 2>/dev/null`" != "Argument is not an alias" && "`hfsdata -e "$1" 2>/dev/null`" != "") ]]; then
echo "$1";
else
local j="`string_lastindexof "$1" "/"`";
local f="`string_substring "$1" "0" "$j"`";
aliaspath "$f";
fi
}
trailingpath() {
local f="`aliaspath "$1"`";
local l="`string_length "$f"`";
l=$(($l+1));
local g="`string_substring "$1" "l"`";
echo "$g";
}
cdalias() {
local p="`aliaspath "$1"`";
local d="`dirname "$p"`";
local b="`basename "$p"`";
local t="`trailingpath "$1"`";
if [ "`cat "$d/$b"`" != "" ]; then
builtin cd "`bash "$d/$b"`";
else
builtin cd "`hfsdata -e "$d/$b"`";
fi
if [ "$t" != "" ]; then
builtin cd "$t";
fi
}

cd() {
if [ ${#1} == 0 ]; then
builtin cd;
elif [[ -d "${1}" || -L "${1}" ]]; then
builtin cd "${1}";
else
cdalias "$1";
fi;
}

end_code

if you want an explanation of the above code, comment on this post about it. don't comment about me re-implementing some of the bash functions (string_something). it's just to make it easier for me to use.