3.2 Frontend to df

by Marko Riedel, from an idea by Martin Brecher

3.2.1 Idea

The program df reports file system disk space usage. It displays these statistics in human-readable form when invoked with the -h option. Its output might look like this:

Filesystem            Size  Used Avail Use% Mounted on  
/dev/hda3             7.7G  5.8G  1.6G  79% /  
/dev/hda1              61M  4.8M   53M   9% /boot  
shmfs                 126M     0  126M   0% /dev/shm  
/dev/loop0             93K   54K   34K  62% /tmp/mountpoint  
/dev/fd0              1.4M  500K  840K  38% /media/floppy


PIC

Figure 6: Graphical frontend to df displays file system disk usage.


The goal is to have a graphic frontend to df that displays the disk space usage for every disk in a graphical format like that shown in Fig.6. It should update from time to time in order to reflect dynamic changes in disk space usage.

3.2.2 Implementation

The program consists of two components: the first is the class PercentageView, which provides the functionality to display a couple of lines of data above a pie chart that represents the current percentage. We will instantiate one PercentageView for every disk. The second component is a controller that acts as the application’s delegate, runs df at one-second intervals and updates the percentages accordingly.

Start with some basic definitions that collect constants at the start of the program.

 
#import <Foundation/Foundation.h> 
#import <AppKit/AppKit.h> 
 
// skeleton by Fred Kiefer 
 
#define FONTSIZE    11 
#define SEPARATOR   4 
 
#define DFBIN @”/bin/df” 
 
#define PWIDTH     160 
#define PHEIGHT    100

These settings specify the font size to use, the distance from the pie to the margin, where df is to be found and the size of a single PercentageView.

A PercentageView needs to remember the percentage and the lines that make up the title, which we store in an NSArray.

 
@interface PercentageView : NSView 
{ 
    NSArray *titles; 
    int percentage; 
} 
 
- initWithTitles:(NSArray *)ts andFrame:(NSRect)frame; 
- setTitles:(NSArray *)newts; 
- setPercentage:(int)newpc; 
 
- (void)drawRect:(NSRect)aRect; 
 
@end

The interface declares the variables that hold the titles and the percentage. The first three methods permit to initialize a PercentageView with a given frame and a set of titles, as well as to set the percentage and the titles.

 
@implementation PercentageView 
 
- initWithTitles:(NSArray *)ts andFrame:(NSRect)frame 
{ 
    [super initWithFrame:frame]; 
    titles = ts; 
    [titles retain]; 
 
    percentage = 0; 
 
    return self; 
} 
 
- setTitles:(NSArray *)newts 
{ 
    [titles release]; 
 
    titles = newts; 
    [titles retain]; 
    [self setNeedsDisplay:YES]; 
 
    return self; 
} 
 
- setPercentage:(int)newpc 
{ 
    if(newpc==percentage){ 
        return self; 
    } 
 
    percentage = newpc; 
    [self setNeedsDisplay:YES]; 
 
    return self; 
}

The core of this class lies is the method drawRect:, which actually draws the pie chart and the titles. We read the bounds rectangle so that we can use it to compute the coordinates of the components (text, chart). We get the font of the desired size and set the dictionary that contains the attributes of the title strings to contain just one entry, namely the font that we obtained. We need to iterate over the titles so we ask for the appropriate enumerator and use the variable title to hold a single line of the title section. The variable height is decremented by the height of a line every time we draw a line. It starts at the top of the view and moves downwards. The remaining variables pertain to the pie chart: they store the radius, what angle of the pie is occupied, and where the center of the chart lies. The variable slice is used to hold three different bezier paths that are used to draw the chart.

 
- (void)drawRect:(NSRect)aRect 
{ 
    NSRect bounds = [self bounds]; 
 
    NSFont *font = 
        [NSFont userFixedPitchFontOfSize:FONTSIZE]; 
    NSDictionary *attrs = 
        [NSDictionary dictionaryWithObjectsAndKeys: 
                           font, NSFontAttributeName, nil]; 
 
    NSEnumerator *titleEnum = 
        [titles objectEnumerator]; 
    NSString *title; 
    int height = 0; 
 
    float radius; 
    int angle = 360*percentage/100; 
    NSPoint center; 
 
    NSBezierPath *slice;

A PercentageView has a white background and a black boundary, which we draw first.

 
    [[NSColor whiteColor] set]; 
    [NSBezierPath fillRect:bounds]; 
 
    [[NSColor blackColor] set]; 
    [NSBezierPath strokeRect:bounds];

The next step is to iterate over the titles starting at the top of the view and draw each line in turn. We compute the dimensions of each line and use the width to center the string and the height to decrement the variable height, i.e. the current position. Move the statement

height -= size.height;

to before the assignment of loc.x and loc.y if you have the corrected version of

drawAtPoint:withAttributes:,

which works like PSmoveto() and PSshow().

 
    while((title = [titleEnum nextObject])!=nil){ 
        NSSize size = [title sizeWithAttributes:attrs]; 
        NSPoint loc; 
 
        loc.x = (bounds.size.width-size.width)/2; 
        loc.y = height; 
        [title drawAtPoint:loc withAttributes:attrs]; 
 
        height -= size.height; 
    }

The lower part of the view forms a box that will contain the chart. It is as wide as the view itself. Its height is given by the difference between the height of the view and the height of the titles that were drawn, i.e. the current value of the variable height gives the height of the box. We use the smaller of these two to determine the radius of the pie, so that it is sure to fit inside the view. The center of the pie lies in the middle of the x-axis and radius+SEPARATOR units below the last title that was drawn.

 
    radius = (height < bounds.size.width ? 
              height : bounds.size.width)/2 
        - SEPARATOR; 
 
    center.x = bounds.size.width/2; 
    center.y = height-radius-SEPARATOR;

It remains to draw the pie chart. We draw three slices, starting with a red one that shows the part of the disk that is occupied, which is proportional to angle. The slice starts at “noon” and ends after it covers angle degrees going clockwise.

 
    slice = [NSBezierPath bezierPath]; 
    [slice moveToPoint:center]; 
    [slice appendBezierPathWithArcWithCenter:center 
           radius:radius 
           startAngle:90 endAngle:90-angle 
           clockwise:YES]; 
    [slice closePath]; 
 
    [[NSColor redColor] set]; 
    [slice fill];

The second slice shows the free space in blue. The start and end angles are the same, except that we now draw in the opposite direction.

 
    slice = [NSBezierPath bezierPath]; 
    [slice moveToPoint:center]; 
    [slice appendBezierPathWithArcWithCenter:center 
           radius:radius 
           startAngle:90 endAngle:90-angle 
           clockwise:NO]; 
    [slice closePath]; 
 
    [[NSColor blueColor] set]; 
    [slice fill];

The last step is to draw a black boundary around the chart. There are several ways to do this; we could use the code that we used to draw the red and the blue slice. Here is a different approach.

 
    slice = 
        [NSBezierPath 
            bezierPathWithOvalInRect: 
                NSMakeRect(center.x-radius, center.y-radius, 
                            2*radius, 2*radius)]; 
 
    [[NSColor blackColor] set]; 
    [slice stroke]; 
} 
 
@end

We could have used PostScript operators to draw the view, but this recipe is intended to provide examples of drawing with the application kit.

The second main component is the controller. It stores the views in a dictionary, where the keys are strings, i.e. the devices being represented.

 
@interface Controller : NSObject 
{ 
    NSMutableDictionary *devices; 
} 
 
- (NSMutableDictionary *)runDF; 
- (NSArray *)infoToTitles:(NSArray *)devInf; 
- update; 
- (void)applicationDidFinishLaunching:(NSNotification *)notif; 
 
@end

The method runDF is the heart of the controller. It runs df and returns a dictionary whose keys are the devices. Every key points to an array that contains the output from df for that device. The output entry in the dictionary is an array obtained by splitting the output line into fields, where fields are separated by spaces. The method infoToTitles turns such an array into an array of titles suitable for display in a PercentageView. The method update is invoked by a timer and invokes runDF in turn. It records and displays the changes. The last method is invoked when the application finishes launching. It assembles the window and displays an initial set of data.

The first thing runDF does is to create the dictionary that will hold the output lines for the devices. It declares an enumerator that iterates over the raw lines that we obtain from df. We will also need a character set containing spaces so that we can split lines into fields.

 
@implementation Controller 
 
- (NSMutableDictionary *)runDF 
{ 
    NSMutableDictionary *info = 
        [NSMutableDictionary dictionaryWithCapacity:1]; 
    NSEnumerator *lineEnum; 
 
    NSCharacterSet *space = 
        [NSCharacterSet whitespaceAndNewlineCharacterSet];

The next step is to declare everything that we need to run df and obtain its output. The variables length and bytes will be set to the respective values that describe the data. We need a task object for df and a pipe from its standard output. The file handle reader enables us to read from the pipe. The data is read in chunks and appended to a mutable data object. The variable line holds a single line during processing of the output. We create a pipe and obtain the file handle for reading from that pipe.

 
    unsigned length; 
    const char *bytes; 
 
    NSTask *dfTask = [NSTask new]; 
    NSPipe *outputPipe; 
    NSFileHandle *reader; 
 
    NSData *chunk; 
    NSMutableData 
        *data = [NSMutableData dataWithCapacity:1024]; 
    NSString *line; 
 
    outputPipe = [NSPipe pipe]; 
    reader = [outputPipe fileHandleForReading];

We must prepare the task before we can launch it. Therefore we set the actual binary that will be executed and add -h as an argument so that we get human-readable output from df. We set standard output and standard error to our pipe in order to read those data.

 
    [dfTask setLaunchPath:DFBIN]; 
    [dfTask setArguments:[NSArray arrayWithObject:@-h”]]; 
 
    [dfTask setStandardOutput:outputPipe]; 
    [dfTask setStandardError:outputPipe];

Now we launch the task and read chunk after chunk until there are no more data. Actually, the output from df probably fits into a single chunk. We wait for the task to exit and close the file descriptor that is associated with the read end of the pipe so that it becomes available for re-use. Recall that we will invoke this method many times so as to present an up-to-date picture of file system usage.

 
    [dfTask launch]; 
    while((chunk = [reader availableData]) && 
          [chunk length]){ 
        [data appendData:chunk]; 
    } 
    [dfTask waitUntilExit]; 
 
    close([reader fileDescriptor]);

We turn the data into a string, split it into lines and obtain an enumerator of those lines.

 
    length = [data length]; 
    bytes = [data bytes]; 
 
    lineEnum = 
        [[[NSString stringWithCString:bytes length:length] 
             componentsSeparatedByString:@\n”] 
            objectEnumerator];

The next step is to parse the data. We iterate over the array of lines and process those lines that begin with the string /dev/, i.e. refer to actual devices. We want to split the line into fields, so we create a scanner from the current line and prepare an empty array of fields. The variable field holds a single field, of which there are several per line.

 
    while((line = [lineEnum nextObject])!=nil){ 
        if([line hasPrefix:@”/dev/”]){ 
            NSScanner *scn = 
                [NSScanner scannerWithString:line]; 
            NSMutableArray *fields = 
                [NSMutableArray arrayWithCapacity:6]; 
            NSString *field;

We scan the fields in turn, until the scanner reaches the end. We skip leading whitespace, read a single field and store it in the array of fields for the current line. We record the fields in the dictionary when the entire line has been scanned.

 
            while([scn isAtEnd]==NO){ 
                [scn scanCharactersFromSet:space 
                      intoString:NULL]; 
                [scn scanUpToCharactersFromSet:space 
                      intoString:&field]; 
                [fields addObject:field]; 
            } 
 
            [info setObject:fields 
                   forKey:[fields objectAtIndex:0]]; 
        } 
    }

The routine checks if any data have successfully been read and raises an exception if there weren’t any. It returns the dictionary whose keys are the devices and whose values hold the fields from the corresponding output line produced by df.

 
    if(![info count]){ 
        [NSException raise:NSGenericException 
                      format:@”no_data_from_%@”, DFBIN]; 
    } 
 
    return info; 
}

The method infoToTitles is very simple. It creates three title lines from the array of fields: the name of the device, the mountpoint and the current usage. The latter shows the current usage, the capacity and the percentage.

 
- (NSArray *)infoToTitles:(NSArray *)devInf 
{ 
    NSString *devname, *mountpoint, *current; 
 
    devname = [devInf objectAtIndex:0]; 
    mountpoint = [devInf objectAtIndex:5]; 
 
    current = [NSString stringWithFormat:@”%@_(%@)_%@”, 
                         [devInf objectAtIndex:2], 
                         [devInf objectAtIndex:1], 
                         [devInf objectAtIndex:4]]; 
 
    return 
        [NSArray arrayWithObjects:devname, mountpoint, 
                 current, nil]; 
}

The method update is invoked by the timer. It runs df to obtain the dictionary of lines split into fields and prepares an enumerator to iterate over the keys of the dictionary. The string device holds the current device.

 
- update 
{ 
    NSMutableDictionary *info = [self runDF]; 
    NSEnumerator *devEnum; 
    NSString *device; 
 
    devEnum = [[info allKeys] objectEnumerator];

The actual iteration is next. For each device, do the following: extract the array of fields and look for the device in the dictionary that maps devices to views. If there is an entry for the device then there is a PercentageView for it that must be updated. We set the percentage from the fifth field and the set of titles as formatted by infoToTitles. This operation marks the view as needing redisplay. The method returns when all updates have been recorded.

 
    while((device = [devEnum nextObject])!=nil){ 
        NSArray *item = [info objectForKey:device]; 
        PercentageView *pview = 
            [devices objectForKey:device]; 
 
        if(pview!=nil){ 
            [pview setPercentage: 
                        [[item objectAtIndex:4] intValue]]; 
            [pview setTitles:[self infoToTitles:item]]; 
        } 
    } 
 
    return self; 
}

The last method of the controller needs to prepare everything once the application has finished launching. It declares variables to hold the window that contains the percentage views, the main menu of the application, the dimensions of the window, and its content view. It invokes runDF to obtain the data that it needs to get started. It declares an enumerator in order to iterate over the devices and a string to hold the current device name. The variable dcount tracks the number of views that have been created. There is an invocation for use with the timer.

 
- (void)applicationDidFinishLaunching:(NSNotification *)notif 
{ 
    NSWindow *dfWin; 
    NSMenu *menu = [NSMenu new]; 
 
    NSRect winRect; 
    NSView *cview; 
 
    NSMutableDictionary *info = [self runDF]; 
    NSEnumerator *devEnum; 
    NSString *device; 
    int dcount = 0; 
 
    NSInvocation *inv;

The intial setup is straightforward: the menu contains just one entry, namely the item “quit.” The width of the window’s content view is the same as that of a PercentageView. The height of the window is the number of device entries times the height of a single PercentageView. We intialize the content rectangle of the window and allocate the window, set its title, and obtain its content view, to which we’ll be adding subviews.

 
    [menu addItemWithTitle: @”Quit” 
          action:@selector(terminate:) 
          keyEquivalent:@”q”]; 
    [NSApp setMainMenu:menu]; 
 
    winRect = NSMakeRect(0, 0, PWIDTH, PHEIGHT*[info count]); 
    dfWin = [[NSWindow alloc] 
                initWithContentRect:winRect 
                styleMask: NSTitledWindowMask 
                backing: NSBackingStoreBuffered 
                defer: NO]; 
    [dfWin setTitle:@”gdf”]; 
    cview = [dfWin contentView];

The next step is important: we initialize the variable devices, which contains the dictionary that maps devices to views. Next we obtain an enumerator that iterates over the devices sorted alphabetically, but in reverse order, since we build the subviews with the lowest, i.e. last view first.

 
    devices = 
        [NSMutableDictionary 
            dictionaryWithCapacity:[info count]]; 
    devEnum = 
        [[[info allKeys] 
             sortedArrayUsingSelector:@selector(compare:)] 
            reverseObjectEnumerator];

The while loop creates the views that go into the window. It stores the array of fields for each device in the variable items and allocates the view. The frame rectangle has the standard width and height and is positioned precisely above the previous frame rectangle (we start at the bottom of the window). The titles are created with infoToTitles, as in the method update that we discussed earlier. We set the percentage once the view has been allocated and the titles have been initialized.

 
    while((device = [devEnum nextObject])!=nil){ 
        NSArray *item = [info objectForKey:device]; 
        PercentageView *pview = 
            [[PercentageView alloc] 
                initWithTitles:[self infoToTitles:item] 
                andFrame:NSMakeRect(0, dcount*PHEIGHT, 
                                      PWIDTH, PHEIGHT)]; 
        [pview setPercentage: 
                    [[item objectAtIndex:4] intValue]];

We record the new view in the dictionary devices and add it as a subview to the content view of the window. We increment the counter that determines the vertical position of the current view in the window. We center the window once we are done creating subviews and cause it to be displayed on the screen.

 
        [devices setObject:pview forKey:device]; 
        [cview addSubview:pview]; 
 
        dcount++; 
    } 
    [devices retain]; 
 
    [dfWin center]; 
    [dfWin makeKeyAndOrderFront:nil];

It remains to set up the timer. We create an invocation that captures the controller and the method update for this purpose. The invocation must persist throughout the lifetime of the application, so we retain it. The last step is to start the timer, which fires once a second.

 
    inv = [NSInvocation 
              invocationWithMethodSignature: 
                   [self methodSignatureForSelector: 
                             @selector(update)]]; 
    [inv setSelector:@selector(update)]; 
    [inv setTarget:self]; 
    [inv retain]; 
 
    [NSTimer scheduledTimerWithTimeInterval:1 
             invocation:inv 
             repeats:YES]; 
} 
 
@end

The main routine of this program is very simple. It creates an autorelease pool, an application instance and the controller, which is made the delegate of the application, and starts the application’s run loop.

 
int main(int argc, char** argv, char **env) 
{ 
    NSAutoreleasePool *pool = [NSAutoreleasePool new]; 
    NSApplication *app; 
 
    app = [NSApplication sharedApplication]; 
    [app setDelegate:[Controller new]]; 
    [app run]; 
 
    [pool release]; 
    exit(0); 
}