ftp.nice.ch/users/felix/FileSpy.1.1.src/SpyTextFieldCell.m

This is SpyTextFieldCell.m in view mode; [Download] [Up]

/*
 *   This sourcecode is part of FileSpy, a logfile observing utility.
 *   Copyright (C) 1996  Felix Rauch
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *   
 *   Notice that this program may not be possessed, used, copied,
 *   distributed or modified by people having to do with nuclear
 *   weapons. See the file CONDITIONS for details.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 *   To contact the original author, try:
 *   e-mail: Felix.Rauch@nice.ch
 *   Traditional mail:	Felix Rauch
 *			Sempacherstrasse 33
 *			8032 Zurich
 *			Switzerland
 */

#import <appkit/Text.h>
#import <appkit/ScrollView.h>
#import <appkit/graphics.h>
#import "SpyTextFieldCell.h"

#define MAXSIZE 1.0e38	// Maximum size of a text object
#define MAXSLOWDOWN 32	// maximum of file updates to ignore in case of nfs-errors (could be a default-value). Must be power of 2!

@implementation SpyTextFieldCell

static List *tmpList = nil;		// list for selected cells in exMatrix

+ (void)setSharedTmpList:newList
{
    tmpList = newList;
}

+ sharedTmpList
{
    return tmpList;
}

- init
{

    [super init];
    fileLenght = lastFileLenght = 0;
    fileex = oldfileex = NO;
    textloaded = NO;
    dirloaded = NO;
    firstTime = 2;
    permissionOK = oldpermissionOK = YES;
    maxLines = 0;
    zeroFlag = NO;
    nrEINVAL = 0;

// init font object
    myFont = [Font newFont:"Ohlfs"
		    size:10
		    style:0
		    matrix:NX_FLIPPEDMATRIX];
    [Text setDefaultFont:myFont];
    myDoc = [Document new:self];
    myWindow = [[myDoc scrollView] window];
    myScrollView = [myDoc scrollView];
    myText = [[myDoc scrollView] docView];
    [myWindow setFreeWhenClosed:NO];
    color = NX_COLORBLACK;

    return self;
}

- delete
{
    if(exCell) {
	int row, col;
	
	[exMatrix getRow:&row andCol:&col ofCell:exCell];
	[exMatrix removeRowAt:row andFree:YES];
    }
    [myDoc free];
    [myWindow setDelegate:nil];
    [myWindow setFreeWhenClosed:YES];
    [myWindow close];
    return self;
}

- free
{
    return [super free];
}

- (long int)lastFileLenght
{
    return lastFileLenght;
}

- setLastFileLenght:(long int)lenght
{
    lastFileLenght = lenght;
    return self;
}

- (BOOL)fileExists
{
    return fileex;
}

- updateFile
// This is important. It checks the file and looks whether something has changed with it
{
    if(nrEINVAL > 0) {		// if we had nfs errors recently then wait for next check with
    				// exponentially increasing time between two checks (proposed by mwa)
	unsigned int i = MAXSLOWDOWN;
	
	while((i > 0) && (i >= nrEINVAL)) {
	    if(nrEINVAL % i == 0)
		break;
	    i /= 2;
	}
	if(nrEINVAL % i != 0) {
	    nrEINVAL++;
	    return self;
	}
    }
    oldfileex = fileex;
    oldpermissionOK = permissionOK;
    if(firstTime > 0) {			// if we're here for the first or second time
    					// firstTime has the following meanings: 2: first time here
					//					 1: second time here
					//					 0: nth time here, no more special cases
	struct stat st;
	
	if(stat([self stringValue], &st) < 0) {
	    switch(errno) {
		case ENOENT:		// file doesn't exist
		case ENOTDIR:		// part of path-prefix is not a directory, so file doesn't exist
		    fileType = FILETYPE_NOTHING;
		    break;
		case EACCES:
		    fileType = FILETYPE_NOTHING;
		    if((firstTime == 2) && !(spytype & SPY_EXISTENCE))
			printf("filespy: no permission for %s\n", [self stringValue]);
		    permissionOK = NO;
		    break;
		case EINVAL:			// in case of 'nfs-server blafasel not responding still trying'
		    if(nrEINVAL++ == 0) {
			perror("filespy: stat");
		    }
		    break;
		default:
		    perror("filespy: stat");
		    fileex = NO;
	    }
	    firstTime = 1;		// next time we will be here for the second time
	} else {			// stat() was ok
	    fileex = YES;		// yes, the file exists
	    permissionOK = YES;		// yes, we can read the file
	    nrEINVAL = 0;		// no more nfs errors
	    if(st.st_mode & S_IFDIR) {	// the file is a deriectory
		fileType = FILETYPE_DIR;
		lastmtime = (time_t)0;	// as we're here for the first or second time, we haven't checked the file before
		if((firstTime == 1) && !(spytype & SPY_EXISTENCE)) {	// if dir was created and we're not spying for ex.
		    printf("filespy: created directory %s\n", [self stringValue]);	// (if we were spying for existence,
					// then this would be noted in the existence-log, so we wouldn't need a printf)
		}
	    } else if(st.st_mode & S_IFREG) {	// the file is a regular file
		fileType = FILETYPE_FILE;
		fileLenght = st.st_size;	// remember its current size
		if(fileMode == RADIO_ENTIRE)	// if we will have to read the whole content, 
		    lastFileLenght = 0;		// just pretend that the file grew from 0 length.
		else if(fileMode == RADIO_NEW)	// otherwise, this is the initial file length
		    lastFileLenght = fileLenght;
		if((firstTime == 1) && (fileLenght == 0) && !(spytype & SPY_EXISTENCE)) {
			// if we're here for the second time (i.e. the file didn't exist before) and the new file's length
			// is 0 bytes and we're not spying for existence, we have to make the user aware of the new file.
		    printf("filespy: created empty file %s\n", [self stringValue]);
		}
	    } else {
		printf("filespy: not supported filetype in file %s\n", [self stringValue]);	// what should I do with other filetypes?
	    }
	    firstTime = 0;		// now we can handle this file in the regular way
	}
    } else if(fileType == FILETYPE_FILE) {	// this is a regular file
	struct stat st;

	if(stat([self stringValue], &st) < 0) {
	    switch (errno) {
		case ENOENT: 	// no such file or directory
		case ENOTDIR:	// some part of the path changed from directory to file -> file can't exist anymore
		    [self fileRemoved];
		    lastFileLenght = fileLenght;
		    fileLenght = 0;
		    firstTime = 2;	// well, the file doesn't exist anymore, so it's a special case again
		    if(!permissionOK) {		// if we didn't have permission before...
			if(!(spytype & SPY_EXISTENCE)) {
			    printf("filespy: regained permission for not existing file %s\n", [self stringValue]);
				    // I'm sure whether this message makes much sense...
			}
			permissionOK = YES;	// we have permissions (well, sort of..)
		    }
		    break;
		case EACCES:		// access denied
		    if(permissionOK) {
			if(!(spytype & SPY_EXISTENCE))
			    printf("filespy: lost permission for file %s\n", [self stringValue]);
			permissionOK = NO;
		    }
		    break;
		case EINVAL:		// nfs server not responding
		    if(nrEINVAL++ == 0) {	// do not report nfs-error each time the file is checked
			perror("filespy: stat");
		    }
		    break;
		default:
		    perror("filespy: stat");
	    }
	} else {			// stat() was ok
	    nrEINVAL = 0;		// no more nfs errors
	    if(!permissionOK) {		// if we didn't have permission before
		if(!(spytype & SPY_EXISTENCE))
		    printf("filespy: regained permission for file %s\n", [self stringValue]);
		permissionOK = YES;
	    }
	    if(!(st.st_mode & S_IFREG)) {	// the file is no longer a regular file
		[self fileRemoved];
		lastFileLenght = fileLenght;
		fileLenght = 0;
		firstTime = 2;
	    } else {
		lastFileLenght = fileLenght;
		fileLenght = st.st_size;
		fileex = YES;		// yes, the file exists
	    }
	}	
    } else if(fileType == FILETYPE_DIR) {	// this file is a directory
	struct stat st;
	
	if(stat([self stringValue], &st) < 0) {
	    switch(errno) {
		    case ENOENT:
		    case ENOTDIR:
			[self fileRemoved];
			lastmtime = mtime;
			firstTime = 2;
			if(!permissionOK) {
			    if(!(spytype & SPY_EXISTENCE))
				printf("filespy: regained permission for not existing directory %s\n", [self stringValue]);
			    permissionOK = YES;
			}
			break;
		    case EACCES:
			if(permissionOK) {
			    if(!(spytype & SPY_EXISTENCE))
				printf("filespy: lost permission for directory %s\n", [self stringValue]);
			    permissionOK = NO;
			}
			break;
		    case EINVAL:		// nfs server not responding
			if(nrEINVAL++ == 0) {
			    perror("filespy: stat");
			}
			break;
		    default:
			perror("filespy: stat");
	    }
	} else {
	    nrEINVAL = 0;		// no more nfs errors
	    if(!permissionOK) {
		if(!(spytype & SPY_EXISTENCE))
		    printf("filespy: regained permission for directory %s\n", [self stringValue]);
		permissionOK = YES;
	    }
	    if(!(st.st_mode & S_IFDIR)) {	// this file is no longer a directory
		[self fileRemoved];
		lastmtime = mtime;
		firstTime = 2;
	    } else {
		oldfileex = fileex;
		lastmtime = mtime;
		mtime = st.st_mtime;	// when was the directory last modified?
		if(!dirStore) {		// if we don't have the directory's contents yet, then read them
		    DIR *dirp;
		    struct direct *dp;
					// read all filenames and put them in dirStore
		    dirp = opendir([self stringValue]);
		    if(dirp != NULL) {
			dirStore = [[Storage alloc]
					initCount:0 elementSize:MAXFILENAMELENGHT
					description:"[MAXFILENAMELENGHT c]"];
			for (dp = readdir(dirp); dp != NULL; dp = readdir(dirp))
			    [dirStore addElement:dp->d_name];
			closedir(dirp);
		    }
		}
	    }
	}
    }
    if((spytype & SPY_EXISTENCE) && fileex) {
	time_t myTime = time(NULL);
	[exCell changeTime:myTime];
    }
    return self;
}

- updateText
// This one checks whether it is necessary to read the contents of the file or update the existencewindow
{
    if((fileType == FILETYPE_FILE) && (spytype & SPY_CONTENTS))
	[self readText];
    else if((fileType == FILETYPE_DIR) && (spytype & SPY_CONTENTS))
	[self readDir];
    if(spytype & SPY_EXISTENCE)	
	[self updateExistence];
    return self;
}

- readText
// read contents of changed regular file
{
    if(fileLenght < lastFileLenght) {		// if the file shrunk
	if((fileLenght > 0) || zeroFlag) {	// if there's still something in it or it had 0 length last time we checked...
			// (we do not report that a file has shrunken to 0 bytes the first time we notice it. there are
			// commands in /usr/adm/daily which produce length-0-files for a very short time (e.g. with 'tail'))
	    printf("filespy: file %s reduced its size from %ld to %ld bytes\n", [self stringValue], lastFileLenght, fileLenght);
	    switch(fileMode) {
		case RADIO_NEW: lastFileLenght = fileLenght; break;
		case RADIO_ENTIRE: lastFileLenght = 0; break;
	    }
	    zeroFlag = NO;
	} else {
	    zeroFlag = YES;			// first check that this file has length 0 for a longer time
	    fileLenght = lastFileLenght;	// ignore new filelength it until next check
	}
	[self contentChanged];			// however, something did change
    }
    if(fileLenght > lastFileLenght) {		// if the file grew then get the new text and send it to addText
	int diff = fileLenght - lastFileLenght;
	char *newString = malloc(diff + 2);
	FILE *fp;

	if((fp = fopen([self stringValue], "r")) == (FILE *)NULL) {
	    printf("filespy: couldn't open file %s\n", [self stringValue]);
	} else {
	    if(fseek(fp, lastFileLenght, SEEK_SET) == 0) {
		if(fread(newString, sizeof(char), diff, fp) != 0) {
		    newString[diff] = '\000';
		    newString[diff+1] = '\000';
		    [self addText:newString];
		} else {
		    perror("filespy: fread");
		}
	    } else {
		perror("filespy: fseek");
	    }
	    fclose(fp);
	}
	free(newString);
    } else if(fileLenght == lastFileLenght)
	textloaded = YES;
    return self;
}

- readDir
// the contents of a directory and remember it
{
    DIR *dirp;
    struct direct *dp;
    unsigned int pos = 0, count = [dirStore count];
    node root, actnode, tmpnode;	// pointers to nodedescs
    
    if((dirp = opendir([self stringValue])) == NULL) {
	if(errno == EACCES)
	    printf("filespy: no permission to read directory %s\n", [self stringValue]);
	else
	    perror("filespy: opendir");
	return self;
    }
    actnode = root = NULL;
    for (dp = readdir(dirp); dp != NULL; dp = readdir(dirp)) {
	if((pos >= count) || (strcmp(dp->d_name, [dirStore elementAt:pos]) != 0)) {
	    unsigned int i = pos;
	    while(i < count && (strcmp(dp->d_name, (char *)[dirStore elementAt:i]) != 0))
		i++;
	    if(i >= count) {		// not found, so it's a new file
	    				// remember all new files in a linked list so that we can print them later
		char str[MAXFILENAMELENGHT + 10] = "new file: ";

		[dirStore insertElement:dp->d_name at:pos];	// remember new file
		count++;
		strcat(str, dp->d_name);
		strcat(str, "\n");
		tmpnode = (node)malloc(sizeof(nodedesc));
		if(actnode != NULL) {
		    actnode->next = tmpnode;
		} else {
		    root = tmpnode;
		}
		tmpnode->string = (char *)malloc(strlen(str)+1);
		strcpy(tmpnode->string, str);
		tmpnode->next = NULL;
		actnode = tmpnode;
	    } else {			// found, so files between pos and i are deleted
	    				// remember the deleted files so that we can print them later
		unsigned int j;
		for(j = pos; j < i; j++) {
		    char str[MAXFILENAMELENGHT + 14] = "removed file: ";
		    strcat(str, (char *)[dirStore elementAt:j]);
		    strcat(str, "\n");
		    tmpnode = (node)malloc(sizeof(nodedesc));
		    if(actnode != NULL) {
			actnode->next = tmpnode;
		    } else {
			root = tmpnode;
		    }
		    tmpnode->string = (char *)malloc(strlen(str)+1);
		    strcpy(tmpnode->string, str);
		    tmpnode->next = NULL;
		    actnode = tmpnode;
		    [dirStore removeElementAt:j];
		    count--;
		}
	    }
	}
	pos++;
    }
    closedir(dirp);
    if(pos < count) {		// rest of the files in dirStore have been deleted
	unsigned int i, max = [dirStore count];
	for(i = pos; i < max; i++) {
	    char str[MAXFILENAMELENGHT + 14] = "removed file: ";
	    strcat(str, (char *)[dirStore elementAt:pos]);
	    strcat(str, "\n");
	    tmpnode = (node)malloc(sizeof(nodedesc));
	    if(actnode != NULL) {
		actnode->next = tmpnode;
	    } else {
		root = tmpnode;
	    }
	    tmpnode->string = (char *)malloc(strlen(str)+1);
	    strcpy(tmpnode->string, str);
	    tmpnode->next = NULL;
	    actnode = tmpnode;
	    [dirStore removeElementAt:pos];
	}
    }
    if(root != NULL) {
	    		// contents of the directory changed (linked list exists), so put all changes together in
			// a single string and pass it to addText
	unsigned int len = 0;
	char *totstr, *actstr;

	actnode = root;
	while(actnode != NULL) {
	    len += strlen(actnode->string);
	    actnode = actnode->next;
	}
	actstr = totstr = (char *)malloc(len+1);
	actnode = root;
	while(actnode != NULL) {
	    strcpy(actstr, actnode->string);
	    actstr = actstr + strlen(actnode->string);
	    actnode = actnode->next;
	}
	[self addText:totstr];
	free(totstr);
	actnode = root;			// remove the linked list
	while(actnode != NULL) {
	    tmpnode = actnode;
	    actnode = actnode->next;
	    free(tmpnode->string);
	    free(tmpnode);
	}
    }
    return self;
}

- addText:(char *)givenString
// output the text givenString depending on the settings and preferences of the user
{
    BOOL changed = YES, filterMode, mode = NO;
// Bugreport Nr. 5 and 6 by Karsten Heinze: malloc()ed memory was too small (I forgot the terminating 0-Byte)
    char *newString = (char *)malloc(strlen(givenString)+1);
    int textlenght;

    if(maxLines > 0)		// if a maximum number of lines is set, then cut the old parts of the existing text off
	[self shortText];
    filterMode = [myDelegate filterMode];
    strcpy(newString, givenString);
    textlenght = [myText textLength];
    if(useFilter) {		// if the filter is active, the filter the text first
	switch(filterMode) {
	    case FILTER_DONTCOPY: mode = YES; break;
	    case FILTER_DONTBEEP: mode = NO; break;
	}
	changed = [myDelegate filterString:newString andRemove:mode who:self];
	if(timeStamp && (changed || (filterMode == FILTER_DONTBEEP))) {		// insert timestamp in front of each line
	    [self addTimeStamp:&newString];
	}
	if(changed || (filterMode == FILTER_DONTBEEP)) {
	    [self appendText:newString unfilteredString:givenString didChange:changed];
	}								
    } else {		// without filter, just add the timeStamp if that feature is active and append the text
	if(timeStamp) {
	    [self addTimeStamp:&newString];
	}
	[self appendText:newString unfilteredString:givenString didChange:changed];
    }
    if(fileType == FILETYPE_FILE)
	textloaded = YES;
    else if(fileType == FILETYPE_DIR)
	dirloaded = YES;
    free(newString);
    return self;
}

- appendText:(char *)string unfilteredString:(char *)ufString didChange:(BOOL)changed
// append string to the text-object in myWindow. changed is YES if something passed the filter (or if the filter is not active)
{
    int textlenght;
    
    textlenght = [myText textLength];
    [myText setSel :textlenght :textlenght];
    [myText replaceSel:string];
    [myText setSel :textlenght :[myText textLength]];
    [[[myText sizeToFit] scrollSelToVisible] display];

    if(!useFilter || changed) {
	if(autoPopUp && !logToSuperlog)
	    [self displayWindow];
	if(beepOnChange && !logToSuperlog)
	    [myDelegate nxbeep];
    }
    [self contentChanged];
    if(logToSuperlog && (fileex || !dontCopySuperlog)) {
    	if([myDelegate superUsesFilter]) {
	    [myDelegate updateSuperlog:self :string :changed :beepOnChange :autoPopUp];
	} else {		// if the superlog doesn't use the filter, then send the unchanged string
	    // changed is YES here, because nothing could have been filtered out if the multilog doesn't filter
	    [myDelegate updateSuperlog:self :ufString :YES :beepOnChange :autoPopUp];
	}
    }
    return self;
}

- (BOOL)changedState
// checks whether anything changed with this file
{
    int ret = 0;
    
    if((spytype & SPY_CONTENTS) != 0) {
	if((fileType == FILETYPE_FILE) && (fileLenght != lastFileLenght)) {
	    ret++;
	} else if((fileType == FILETYPE_DIR) && (mtime != lastmtime)) {
	    ret++;
	}
    }
    if((spytype & SPY_EXISTENCE) != 0) {
	if((fileex != oldfileex) || (permissionOK != oldpermissionOK)) {
	    ret++;
	}
    }
    return ret > 0;
}

- displayWindow
{
    if(![myWindow isVisible]) {
	[myWindow orderFrontRegardless];
	[myDelegate addWindow:self];
    }
    if([myDelegate becomeKey]) {
	[myWindow makeKeyWindow];
	[myDelegate unhideApp:self];
    }
    return self;
}

- moveWindowTo:(NXCoord)x:(NXCoord)y
{
    [myDoc moveWindowTo:x:y];
    return self;
}

- setFileMode:(int)mode
{
    fileMode = mode;
    return self;
}

- (int)fileMode
{
    return fileMode;
}

- setAutoPopUp:(BOOL)state
{
    autoPopUp = state;
    return self;
}

- (BOOL)autoPopUp
{
    return autoPopUp;
}

- window
{
    return myWindow;
}

- setMyDelegate:sender
{
    myDelegate = sender;
    exMatrix = [myDelegate existenceMatrix];
    return self;
}

- myDelegate
{
    return myDelegate;
}

- setBeepOnChange:(BOOL)state
{
    beepOnChange = state;
    return self;
}

- (BOOL)beepOnChange
{
    return beepOnChange;
}

- setLogToSuperlog:(BOOL)state;
{
    logToSuperlog = state;
    return self;
}

- (BOOL)logToSuperlog
{
    return logToSuperlog;
}

- setUseFilter:(BOOL)state
{
    useFilter = state;
    return self;
}

- (BOOL)useFilter
{
    return useFilter;
}

- setDontCopy:(BOOL)state
{
    dontCopySuperlog = state;
    return self;
}

- (BOOL)dontCopy
{
    return dontCopySuperlog;
}

- setTimeStamp:(BOOL)state
{
    timeStamp = state;
    return self;
}

- (BOOL)timeStamp;
{
    return timeStamp;
}

- setSpytype:(unsigned short int)type
{
    int rowCount, colCount;
    if((type & SPY_EXISTENCE) != (spytype & SPY_EXISTENCE)) {		// if the type changed in existence
	if(!exCell) {						// we haven't spyed for existence yet
			// so insert new exCell in existence-matrix
	    [exMatrix addRow];
	    [exMatrix getNumRows:&rowCount numCols:&colCount];
	    exCell = [exMatrix cellAt:rowCount-1 :0];
	    [exCell setStringValue:[self stringValue]];
	    [exCell setMyDelegate:self];
	    [exCell setTime:time(NULL)];
	    [exMatrix sizeToCells];
	    [exMatrix scrollCellToVisible:rowCount-1 :0];
	    [exMatrix display];
	} else {
			// we were spying for this file, so we aren't any longer from now on -> remove the exCell
	    [exMatrix getRow:&rowCount andCol:&colCount ofCell:exCell];
	    [exMatrix removeRowAt:rowCount andFree:YES];
	    [exMatrix display];
	    exCell = nil;
	}
    }
    spytype = type;
    return self;
}

- (unsigned short int)spytype
{
    return spytype;
}

- updateExistence
{
    time_t myTime = time(NULL);
    List *tmpList = [SpyTextFieldCell sharedTmpList];

    if((fileex != oldfileex) || (permissionOK != oldpermissionOK)) {		// existence or permission changed
	[tmpList addObject:exCell];		// remember cell to update later in controller
	if(fileex)
	    [exCell setTime:myTime];
	else
	    [exCell changeTime:myTime];
	if(beepOnChange)
	    [myDelegate nxbeep];
	if(autoPopUp)
	    [myDelegate showExistencelogRegardless];
    }
    return self;
}

- setSuperCopyFilename:(unsigned int)state
{
    superCopyFilename = state;
    return self;
}

- (unsigned int)superCopyFilename;
{
    return superCopyFilename;
}

- setStringValue:(const char *)str
{
    [myWindow setTitleAsFilename:str];
    return [super setStringValue:str];
}

- clearBuffer:sender
{
    [myText setSel :0 :[myText textLength]];
    [myText replaceSel:""];
    [[myText sizeToFit] scrollSelToVisible];
    [self contentChanged];
    return self;
}

- contentChanged
{
    if(![myWindow isKeyWindow] && ![myWindow isDocEdited])
	[myWindow setDocEdited:YES];
    return self;
}

- fileRemoved
// prepares string that says which file/directories have been removed
{
    char str[MAXFILENAMELENGHT + 14] = "removed ";

    if(fileType == FILETYPE_FILE) {	// this file has been removed
	strcat(str, "file: ");
    } else if(fileType == FILETYPE_DIR) { // the whole directory has been removed, so output the names of all the files in it
	if([dirStore count] > 2) {
	    unsigned int i, max = [dirStore count];
	    node root, actnode, tmpnode;
    
	    actnode = root = NULL;
	    for(i = 2; i < max; i++) {
		char str[MAXFILENAMELENGHT + 14] = "removed file: ";
		strcat(str, (char *)[dirStore elementAt:2]);
		strcat(str, "\n");
		tmpnode = (node)malloc(sizeof(nodedesc));
		if(actnode != NULL) {
		    actnode->next = tmpnode;
		} else {
		    root = tmpnode;
		}
		tmpnode->string = (char *)malloc(strlen(str)+1);
		strcpy(tmpnode->string, str);
		tmpnode->next = NULL;
		actnode = tmpnode;
		[dirStore removeElementAt:2];
	    }
	    if(root != NULL) {
		unsigned int len = 0;
		char *totstr, *actstr;

		actnode = root;
		while(actnode != NULL) {
		    len += strlen(actnode->string);
		    actnode = actnode->next;
		}
		actstr = totstr = (char *)malloc(len+1);
		actnode = root;
		while(actnode != NULL) {
		    strcpy(actstr, actnode->string);
		    actstr = actstr + strlen(actnode->string);
		    actnode = actnode->next;
		}
		[self addText:totstr];
		free(totstr);
		actnode = root;
		while(actnode != NULL) {
		    tmpnode = actnode;
		    actnode = actnode->next;
		    free(tmpnode->string);
		    free(tmpnode);
		}
	    }
	}
	strcat(str, "dir: ");
    }
    strcat(str, [self stringValue]);
    if(!(spytype & SPY_EXISTENCE))		// only print message if we're not spying for existence
	puts(str);				// otherwise we have to update the existence-log
    fileType = FILETYPE_NOTHING;
    fileex = NO;
    return self;
}

- windowDidBecomeKey:sender
{
    if([sender isDocEdited])
	[sender setDocEdited:NO];
    [myDelegate windowDidBecomeKey:sender];
    return self;
}

- windowWillClose:sender
{
    [myDelegate windowWillClose:sender];
    return self;
}

- text
{
    return myText;
}

- (NXColor)color
{
    return color;
}

- setColor:(NXColor)newColor
{
    color = newColor;
    [myText setTextColor:color];
    [myText display];
    return self;
}

- setSpyFont:newFont
{
    myFont = newFont;
    [myText setFont:myFont];
    [myText scrollSelToVisible];
    return self;
}

- addTimeStamp:(char **)string
// add current time in front of each line in string
{
#define TIMELEN 19		// length of timestring
    time_t mytime = time((time_t *)NULL);
    struct tm *tp = localtime(&mytime);
    char tstr[TIMELEN], *tmpText, *p, *cp;
    unsigned int n = 0, tlen;
    BOOL copied;
    
    strftime(tstr, (size_t)TIMELEN, "{%b %d %H:%M:%S} ", tp);
    tlen = strlen(tstr);
    p = *string;
    while(*p != '\000') {
	if(*p++ == '\n')
	    n++;	// count number of lines
    }
    tmpText = (char *)malloc(strlen(*string) + n*TIMELEN + 1);
    tmpText[0] = '\000';
    p = *string;
    cp = tmpText;
    copied = NO;
    while(*p != '\000') {
	if(!copied) {
	    strcpy(cp, tstr);
	    cp += tlen;
	    copied = YES;
	}
	*cp++ = *p;
	if(*p++ == '\n')
	    copied = NO;
    }
    *cp = '\000';
    free(*string);
    *string = tmpText;
    return self;
}

- spyFont
{
    return myFont;
}

- setMaxLines:(int)lines
{
    maxLines = lines;
    return self;
}

- shortText
// remove lines from the beginning of the text so that the text has not more than maxLines lines
{
    int textLen = [myText textLength];
    int textLines = [myText lineFromPosition:textLen];

    if(textLines > maxLines) {
	[[myText setSel:0 :[myText positionFromLine:(textLines - maxLines)]] replaceSel:""];
    }
    return self;
}

- (BOOL)permissionOk
{
    return permissionOK;
}

- setShowTime:(BOOL)state
{
//    [exCell setTime:time(NULL)];
    showTime = state;
    return self;
}

- (BOOL)showTime
{
    return showTime;
}

- (BOOL)askUpdateTime
{
    return (spytype & SPY_EXISTENCE) && fileex;	// need to update time in existenceTextFieldCell
}

- drawInside:(const NXRect *)cellFrame inView:controlView	// taken from 'ScrollDoodScroll' and modified
{
#define IMAGEMARGIN 4.0
    /* every CustomCell needs these two */
    static id sharedTextCell = nil;
    NXRect rect = *cellFrame;
    NXRect boxRect;
    NXCoord size;
    NXSize ESize;
    NXPoint imageOrigin;

  /* erase the cell */
    PSsetgray((cFlags1.state || cFlags1.highlighted) ? NX_WHITE : NX_LTGRAY);
    NXRectFill(cellFrame);
    size = NX_HEIGHT(cellFrame) - 2*IMAGEMARGIN;
    if(spytype & SPY_CONTENTS) {	// If we're only spying for existence, we don't need a colorbox
	NXSetColor(color);
	NXSetRect(&boxRect, NX_X(cellFrame) + IMAGEMARGIN, NX_Y(cellFrame) + IMAGEMARGIN,
	    size, size);
	NXRectFill(&boxRect);
    } else {
	if(!EImage) {
	    EImage = [NXImage findImageNamed:"E"];
	}
	[EImage getSize:&ESize];
	imageOrigin.x = NX_X(cellFrame) + IMAGEMARGIN;
	imageOrigin.y = NX_Y(cellFrame) + NX_HEIGHT(cellFrame) -
			(NX_HEIGHT(cellFrame) - ESize.height) / 2.0;
    
	[EImage composite:NX_SOVER toPoint:&imageOrigin];
    }
    NX_WIDTH(&rect) -= (size + IMAGEMARGIN * 2.0 - NX_X(&rect));
    NX_X(&rect) += size + IMAGEMARGIN * 2.0;

    if (!sharedTextCell) {
        sharedTextCell = [[Cell alloc] init];
	[sharedTextCell setWrap:NO];
    }
    [sharedTextCell setFont:[self font]];
    [sharedTextCell setStringValue:[self stringValue]];
    [sharedTextCell drawInside:&rect inView:controlView];
    
  /* all drawing from now on will be in dark gray */
    PSsetgray(NX_DKGRAY);
    
  /* draw the two dark gray lines above and below the cell */
    if (cFlags1.state || cFlags1.highlighted) {
        NXRect rectArray[2];
      /*
       * draw 1-pixel tall rectangles instead of lines (this is faster than
       * PSmoveto(); PSlineto()).
       */
	NXSetRect(&(rectArray[0]), NX_X(cellFrame), NX_Y(cellFrame),
		NX_WIDTH(cellFrame), 1.0);
	NXSetRect(&(rectArray[1]), NX_X(cellFrame), NX_MAXY(cellFrame) - 1.0,
		NX_WIDTH(cellFrame), 1.0);

      /* using NXRectFillList is faster than separate calls to NXRectFill */
	NXRectFillList(rectArray, 2);
    }

    return self;
}

- highlight:(const NXRect *)cellFrame inView:controlView lit:(BOOL)flag
{
    if (cFlags1.highlighted != flag) {
	cFlags1.highlighted = flag;
	[self drawInside:cellFrame inView:controlView];
    }
    return self;
}

@end

These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.