by Marko Riedel, from an idea by Martin Brecher
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
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.
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.
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.
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.
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.
A PercentageView has a white background and a black boundary, which we draw first.
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
which works like PSmoveto() and PSshow().
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.
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.
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.
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.
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.
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.
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.
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.
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.
We turn the data into a string, split it into lines and obtain an enumerator of those lines.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.