← All modules

Distribution · system

DirsOfInterest

Works

Where an application should put its data, config, cache and logs — eighteen lookups, ten of which throw on macOS.

Version
1.0.0 zef:patrickb
Depends
none beyond the core
License
Artistic-2.0
Its own test suite
no test files, so trivially green
Checked
2026-09-15 against Raku++ 3.28.0 and Rakudo 2026.08
Where it lives
raku.land · source

Install it #

$ rakupp install DirsOfInterest

zef install DirsOfInterest writes the same store; either installer leaves the module usable by both engines.

What it is for #

Every platform has its own answer to "where does an application keep its files": XDG directories on Unix, ~/Library/… on macOS, %APPDATA% on Windows. This distribution puts one set of eighteen method names over all three, mixing in a per-platform role at construction.

The lookups that work here #

File
use DirsOfInterest;

for <user-config-dir user-data-dir user-desktop-dir user-documents-dir
     user-downloads-dir user-music-dir user-pictures-dir user-videos-dir> -> $m {
    my $r = try DirsOfInterest."$m"();
    say sprintf('  %-20s %s', $m,
                $! ?? 'THROWS' !! $r.Str.subst($*HOME.Str, '~'));
}
say '';
say 'note user-videos-dir maps to ~/Movies on macOS, not ~/Videos.';
Output
  user-config-dir      ~/Library/Application Support
  user-data-dir        ~/Library/Application Support
  user-desktop-dir     ~/Desktop
  user-documents-dir   ~/Documents
  user-downloads-dir   ~/Downloads
  user-music-dir       ~/Music
  user-pictures-dir    ~/Pictures
  user-videos-dir      ~/Movies

note user-videos-dir maps to ~/Movies on macOS, not ~/Videos.

Naming your application #

File
use DirsOfInterest;

DirsOfInterest.set(app-name => 'demo', app-version => '1.2');
say 'after .set(:app-name, :app-version):';
for <user-data-dir user-config-dir user-documents-dir user-music-dir> -> $m {
    say sprintf('  %-20s %s', $m,
                DirsOfInterest."$m"().Str.subst($*HOME.Str, '~'));
}
say '';
say 'the eight working lookups split into two groups with different';
say 'semantics: data and config get your name and version appended, the';
say 'six media directories do not. Setting :app-name and assuming it';
say 'applies uniformly gets you a wrong answer with no error.';
say '';
say '.set installs a process-wide singleton; .new gives you an instance:';
my $d = DirsOfInterest.new(app-name => 'other', app-version => '0.1');
say '  instance : ', $d.user-data-dir.Str.subst($*HOME.Str, '~');
say '  singleton: ', DirsOfInterest.user-data-dir.Str.subst($*HOME.Str, '~');
Output
after .set(:app-name, :app-version):
  user-data-dir        ~/Library/Application Support/demo/1.2
  user-config-dir      ~/Library/Application Support/demo/1.2
  user-documents-dir   ~/Documents
  user-music-dir       ~/Music

the eight working lookups split into two groups with different
semantics: data and config get your name and version appended, the
six media directories do not. Setting :app-name and assuming it
applies uniformly gets you a wrong answer with no error.

.set installs a process-wide singleton; .new gives you an instance:
  instance : ~/Library/Application Support/other/0.1
  singleton: ~/Library/Application Support/demo/1.2

The one thing to know #

On macOS, ten of the eighteen lookups throw — and it is not a version or an install problem.

File
use DirsOfInterest;

my @all = <site-cache-dir site-config-dir site-config-dirs site-data-dir
           site-data-dirs site-runtime-dir user-cache-dir user-config-dir
           user-data-dir user-desktop-dir user-documents-dir user-downloads-dir
           user-log-dir user-music-dir user-pictures-dir user-runtime-dir
           user-state-dir user-videos-dir>;
my @dead = @all.grep({ my $r = try DirsOfInterest."$_"(); $!.so });
say 'lookups that throw on macOS : ', @dead.elems, ' of ', @all.elems;
say '  ', @dead.join("\n  ");
say '';
say 'DirsOfInterest::MacOS calls self!append-stuff(…) — PRIVATE-method';
say 'syntax — but `append-stuff` is declared as an ordinary public method';
say 'on DirsOfInterest. There is no such private method, and because the';
say 'role is mixed in with `does` at construction, nothing catches it at';
say 'compile time.';
say '';
say 'user-state-dir fails for a second, independent reason: it calls';
say 'self.user_data_dir, with underscores, a name that does not exist.';
say '';
say 'DirsOfInterest::Unix uses the public self.append-stuff consistently';
say 'and has none of this — the defect is macOS-only, which is exactly';
say 'where a macOS developer will meet it.';
Output
lookups that throw on macOS : 10 of 18
  site-cache-dir
  site-config-dir
  site-config-dirs
  site-data-dir
  site-data-dirs
  site-runtime-dir
  user-cache-dir
  user-log-dir
  user-runtime-dir
  user-state-dir

DirsOfInterest::MacOS calls self!append-stuff(…) — PRIVATE-method
syntax — but `append-stuff` is declared as an ordinary public method
on DirsOfInterest. There is no such private method, and because the
role is mixed in with `does` at construction, nothing catches it at
compile time.

user-state-dir fails for a second, independent reason: it calls
self.user_data_dir, with underscores, a name that does not exist.

DirsOfInterest::Unix uses the public self.append-stuff consistently
and has none of this — the defect is macOS-only, which is exactly
where a macOS developer will meet it.

The getter that writes to disk #

File
use DirsOfInterest;

my $base = $*TMPDIR.add("doi-{$*PID}");
LEAVE { $base.add('demo/1.2').rmdir; $base.add('demo').rmdir; $base.rmdir }
$base.mkdir;

my $d = DirsOfInterest.new(app-name => 'demo', app-version => '1.2',
                           ensure-exists => True);
my $made = $d.append-stuff($base);
say 'append-stuff returned : …/', $made.parent.basename, '/', $made.basename;
say 'it exists on disk     : ', $made.d;
say '';
say ':ensure-exists silently mkdirs as a side effect of a GETTER. Asking';
say 'where a file should go creates the directory.';
Output
append-stuff returned : …/demo/1.2
it exists on disk     : True

:ensure-exists silently mkdirs as a side effect of a GETTER. Asking
where a file should go creates the directory.

Where the two engines differ #

Only in the wording of the failure: Raku++ says No such private method '!append-stuff' for invocant of type …, Rakudo says No such private method 'append-stuff' on … and truncates the type name. The same ten lookups fail on both, which is why this is a module defect rather than an engine one.

One core divergence worth knowing if you write your own checks around this: IO::Path.d and .f on a path that does not exist return False under Raku++ and throw X::IO::DoesNotExist under Rakudo. .e returns False on both — test with .e first.

File
use DirsOfInterest;

# the eight that work, wrapped so a caller gets Nil rather than an exception
sub dir(Str $which) {
    my $r = try DirsOfInterest."$which"();
    $! ?? Nil !! $r
}
for <user-config-dir user-log-dir user-documents-dir> -> $m {
    my $p = dir($m);
    say sprintf('  %-20s %s', $m,
                $p.defined ?? $p.Str.subst($*HOME.Str, '~') !! 'unavailable here');
}
say '';
say 'and note `new` is a submethod, so it is not inherited — subclassing';
say 'DirsOfInterest and calling .new bypasses the role mixin entirely and';
say 'leaves you with an object that has none of the eighteen methods.';
Output
  user-config-dir      ~/Library/Application Support
  user-log-dir         unavailable here
  user-documents-dir   ~/Documents

and note `new` is a submethod, so it is not inherited — subclassing
DirsOfInterest and calling .new bypasses the role mixin entirely and
leaves you with an object that has none of the eighteen methods.