TOP | UP: Border Crossings | NEXT: Driving Other Applications |
UserTalk can query the operating system to find out what's happening at system level. What time is it? Has the user clicked the mouse? What programs are running? It can make system-level events occur, such as noises from the computer's speaker. Some verbs obtain system-level information about Frontier itself, such as how much free memory it has. And UserTalk can drive the computer's file system, to get or set file information, or to create, copy, or delete files.
This chapter lists the verbs that do these things.
To learn how much free memory Frontier's heap contains:1
memAvail ()
string.memAvailString ()
To learn the pathname of the Frontier application file:
frontier.getProgramPath ()
To learn the pathname of the frontmost database:
frontier.getFilePath ()
To learn the version number of the Frontier database:
frontier.version ()
To learn the version number of the Frontier application, as opposed to the database:
file.getVersion(frontier.getProgramPath())
UserTalk's system-level powers are here categorized as follows: noticing when the user holds a modifier key or manipulates the mouse; getting and setting the contents of the clipboard; learning what processes are running and which is frontmost; making sounds from the speaker and manipulating the cursor; and obtaining some miscellaneous information such as RAM memory and clock time.
To learn whether the user is holding down a modifier key:
kb.cmdKey ()
kb.controlKey ()
kb.optionKey ()
kb.shiftKey ()
The kb verbs are often used to give a menu item a slightly different functionality if a modifier key is held down when it is chosen.
To learn whether the user is holding down the mouse button:
mouse.button ()
To learn the present location of the cursor:
mouse.location ()
mouse.location() is supposed to measure from the top left corner of the frontmost window, but because of a bug, it usually measures from the top left corner of the Main Window instead. There are two workarounds. One is to call window.frontmost() immediately before calling mouse.location(); this disables the bug temporarily. The other approach is to accept the bug and compensate by calculation. For example, the following script displays the mouse's position in global coordinates continually for 20 seconds:
local(start = clock.ticks(), x, y, xx, yy)
loop
point.get(mouse.location(), @xx, @yy)
window.getPosition("Frontier.root", @x, @y)
msg((x+xx) + ", " + (y+yy))
clock.waitsixtieths(1)
if clock.ticks() - start > 60 * 20
break
To learn how long the user has been "idle":
frontier.idleTime ()
The clipboard commands move data between an object and the clipboard. They are intended primarily for use before or after a context switch: for example, to get some data into a Microsoft Word document, we might put it on the clipboard, switch to Microsoft Word, and tell Word to paste it.
This clipboard is technically known as the system scrap. It is not quite the same as the place where data is copied and pasted within Frontier; Frontier maintains what is known as a private scrap.2 The private scrap allows Frontier to maintain internally copied data in its own special formats. The system scrap's data, on the other hand, must be in a universally usable format - usually either TEXT or PICT.
When there is a context switch to or from Frontier, or when you ask for the contents of either scrap, Frontier reconciles the two scraps, so that they both contain whatever most recently went into either one of them. If the private scrap was more recently changed, Frontier reconciles the scraps by placing onto the system scrap a text version of the private scrap.
Unfortunately, there's a slight bug. If a clipboard verb changes the system scrap and there is then a context switch, Frontier sometimes fails to realize that the system scrap is more recent than the private scrap, and overwrites the system scrap with a text version of the private scrap. I have found that a reliable workaround is to read from the system scrap by saying clipboard.getValue('TEXT') just before changing it.3
To move an object's value onto the system scrap:
clipboard.put (MacOSFormat, addrObject)
Coercion from Frontier's internal formats is not performed automatically when you move data directly onto the system scrap; you must coerce explicitly beforehand. For example, this is not the way to put the text version of an outline onto the system scrap:
clipboard.put ('TEXT', @workspace.notepad) « this is not going to work!
If you say this, and then switch to Microsoft Word and paste, you'll see some extra "garbage." Instead, say this:
local (o = string(workspace.notepad)) « convert
clipboard.put ('TEXT', @o)
To move a value onto the system scrap, setting its type automatically:
clipboard.putValue (value)
clipboard.putValue() is a convenient utility, because value can be a literal or the result of a verb call - it doesn't have to be put into an object, as with clipboard.put(). So, we might rewrite the previous example:
clipboard.putValue (string(workspace.notepad))
To move the system scrap's contents into a binary object:
clipboard.get (MacOSFormat, addrObject)
To return the system scrap's contents:
clipboard.getValue (MacOSFormat)
The reason why both clipboard.get() and clipboard.getValue() require that MacOSFormat be supplied is that the system scrap can contain more than one piece of data, provided they are in different formats.4 Thus it is necessary to specify which piece of data is desired. clipboard.get() returns a binary; if (as is usually the case) the desired scrap data is text, clipboard.getValue() is usually more convenient, because its result will be text. For an example of clipboard.get() in action, see Example 28-1.
These verbs have to do with applications that are actually running; verbs that deal with applications as files are discussed later in this chapter. Starting up and quitting processes is discussed later in this chapter, and also in Chapter 32, Driving Other Applications.
Process identifier parameters are very flexible about the form of the identifier: it can be the name of the process, the pathname of the application file, or the application's string4 creator code. The creator code is frequently the most convenient, especially since an application's name can contain surprises such as trademark symbols, version numbers, and so on; for example, it is easier to type 'LAND' than "UserLand Frontier?".
To learn whether a particular process is running:
sys.appIsRunning (processIdentifier)
To learn what process is frontmost:
sys.frontmostApp ()
To learn how many processes are running:
sys.countApps ()
sys.countApps() includes faceless background applications. To obtain a count of non-faceless applications (and excluding the Finder itself), ask the Finder for its count, like this:
with finder
count (objspec(nil), process)
To learn the name of the n th process:
sys.getNthApp (n)
To bring a particular process to the front:
sys.bringAppToFront (processIdentifier)
Frontier can also be brought to the front with frontier.bringToFront(), and scriptable applications with "glue" have a bringToFront() verb of their own; these also start up the application if it isn't running (see Chapter 32).
To learn the pathname of the application file for a running process:
sys.getAppPath (processIdentifier)
To play the system alert sound:
speaker.beep ()
To play a brief, high-pitched "blip" sound:
speaker.ouch ()
To play a customized square-wave sound:
speaker.sound (duration, amplitude, frequency)
For an elaborate use of speaker.sound(), see suites.samples.basicStuff.rimshot() . There seems to be some sort of logarithmic relationship between frequency and what one normally thinks of as frequency. The following two notes are an octave apart:
speaker.sound (10, 200, 1000); speaker.sound (10, 200, 10000)
and to express a note between them, you express it as a fraction of the distance between 1000 and 10000. So, for example, the following routine plays a major scale:
theList = {0, 2, 4, 5, 7, 9, 11, 12}
for x in theList
speaker.sound(1, 200, 1000 + 9000 * x / 12)
clock.waitsixtieths(20)
speaker.playNamedSound (name)
When using speaker.playNamedSound(), the resource must be in the resource chain already; see Example 20-4.
To put up Frontier's "turning beachball" cursor:
rollBeachball ()
To learn (in bytes) how much free RAM memory there is:
sys.memAvail ()
To learn what version of the system is running:
sys.OSVersion ()
To learn or set the date and time on the system clock:
clock.now ()
clock.set (dateTime)
To learn how many "ticks" have elapsed since the computer was started up:
clock.ticks ()
clock.ticks() is useful for measuring the time elapsed between two moments of script execution:
local (startTime = clock.ticks())
« do some operation here
local (elapsedTime = clock.ticks() - startTime)
To get an accurate measurement, it is usual to repeat the operation some large number of times, since ticks are imprecise, and are a very gross unit in comparison to the speed of computer operations.
Another useful trick is to use clock.ticks() to generate identifiers that are guaranteed distinct. For example, to save several files under different names, one could name each of them "temp"+clock.ticks().
To query the system using the Gestalt Manager:5
gestalt (selector)
For verbs that read and write the resource and data forks of files, see Chapter 19, Datafiles, and Chapter 20, Resources.
The verbs described in this section drive the filing system at system level; but since the Finder is a scriptable application, and since the Finder, too, drives the filing system, Frontier can also do things with files by way of the Finder. See Chapter 32.
The pathname of a file is a string identifying the file as a series of elements - the volume name, the names of nested folders, and the filename - delimited by a file separator character, which on Mac OS is a colon.
Strictly speaking, a volume or folder pathname should end in a colon; but in actual fact, Frontier is often quite forgiving if the final colon is omitted. This is because when you supply a string pathname, it is coerced to a fileSpec before use, and the coercion process sometimes supplies the missing final colon. For the same reason, file pathnames are not case-sensitive.
Frontier maintains a pathname prefix, which it will prefix to any one-element file pathname (not ending in a colon) in an attempt to resolve the pathname. When Frontier starts up, this pathname prefix is the folder containing the Frontier application. For example, on my computer it is possible to refer to the database file as "frontier.root"; the file is in the same folder as the Frontier application, so Frontier is able to resolve the partial pathname.
To learn or set the current pathname prefix:
file.getPath ()
file.setPath (pathString)
The fact that there is a pathname prefix, and the fact that Frontier is forgiving about the final colon, can combine to give unexpected results. On the whole it is best to be strict with oneself about the colon. To learn how Frontier will interpret a pathname string, coerce it to a fileSpec in Quick Script.
See Chapter 14, Strings and Chars, on how to extract elements from a pathname string. See Chapter 25, Dialogs, for dialogs that let the user choose a file.
There are various sorts of things the system can open, and various ways in which they can be opened. The launch verbs, described in this section, take advantage of this "open" metaphor; they cause the item to open in the sense applicable to that item. See also Chapter 32.
launch.anything (pathname)
launch.controlPanel (name)
launch.appleMenu (name)
Since, under System 7, control panels and Apple menu items are double-clickables, launch.controlPanel() and launch.appleMenu() do nothing you couldn't do yourself with launch.anything().
launch.application (pathname)
To start up an application with a particular document:
launch.appWithDocument (appPathname, docPathname)
To make certain that an application is running, launch.application() and launch.appWithDoc() are much better choices than launch.anything(). They are faster, they don't bring the application to the front, and they return true immediately if the application is already running. Also, they operate interactively with the launch process so as not to return until either the application has fully opened or an error comes back from the system. launch.anything(), on the other hand, returns immediately, with no ability to learn from the system how things went or when the launch process was over. These verbs should perhaps be embedded in a try, to make absolutely certain all is well.
To start up an application given its creator code:
launch.usingID (string4)
launch.usingID() is a utility script which is not recommended, because it calls launch.anything(). You're better off resolving the ID yourself:
try
launch.application (file.findApplication (theID))
To run an FKEY or other code resource:
launch.resource (typeString4, IDnumber)
To learn how many volumes are currently mounted:
file.countVolumes ()
To obtain a list of the names of currently mounted volumes, use fileloop(), as follows:
local (theList = {}, f)
fileloop (f in "")
theList[0] = f
« now the list is in theList
To mount a remote server volume:
file.mountServerVolume (path, userID, password)
To learn whether a volume is ejectable:
file.isEjectable (volumePathname)
file.eject (volumePathname)
file.unmountVolume (volumePathname)
To learn (in bytes) the size of a volume:
file.volumeSize (volumePathname)
To learn (in bytes) how much of a volume is occupied:
file.bytesOnVolume (volumePathname)
To learn (in bytes) how much of a volume is free:
file.freespaceOnVolume (volumePathname)
As might be expected, the following is always true:
file.freespaceOnVolume() + file.bytesOnVolume() == file.volumeSize()
To learn the block size of a volume:
file.volumeBlockSize (volumePathname)
Under the Mac OS HFS filing system, the larger a volume, the larger the block size. The significance of block size is that a file takes up a whole number of blocks on disk, so the smallest amount of disk a file can occupy is file.volumeBlockSize() (twice file.volumeBlockSize() if the file has a resource fork). This wastes huge amounts of space on large disks containing many small files.6
To learn the number of files and folders on a volume:
file.filesOnVolume (volumePathname)
To learn the number of folders on a volume:
file.foldersOnVolume (volumePathname)
To learn the total number of bytes occupied by all files in a folder:
file.bytesInFolder (folderPathname)
To learn the number of files and folders in a folder:
file.filesInFolder (folderPathname, depth)
To learn the number of folders in a folder:
file.foldersInFolder (folderPathname, depth)
These verbs are utility scripts dating back to a time before the Finder was scriptable; they use a time-consuming technique of recursively traversing every file individually. It is now far faster to drive the Finder to get the same information (see Chapter 32). Here is the faster Finder equivalent of file.bytesInFolder().
on folderSize (path) |
with objectmodel, finder |
return (get (folder[path].size )) |
The following is the faster Finder equivalent of file.filesInFolder(). In this version, folders are not counted, and there are only two choices of depth.
It is easy to modify this to count folders instead of files.
To obtain the pathname of the startup volume:
file.getSystemDisk ()
To obtain the pathname of the active system folder:
file.getSystemFolderPath ()
To obtain the pathname of any system folder's standard subfolders:
file.getSpecialFolderPath (volumePathname, folder, create?)
Changes made to files with these verbs take place at system level. The Finder may take a while to hear about them, so if a Finder window containing the file is already open, it may not immediately reflect the changes; to force it to do so, call finder.update() with one parameter, the pathname of a file whose changes you want the Finder to reflect immediately.
To learn (in bytes) the size of a file:
file.size (pathname)
file.size() returns the so-called "logical size," the sum of the total bytes of data in the file's resource fork and data fork - not the "physical size," the amount of space it occupies on the disk. For the reason why these two numbers differ, see the earlier description of file.volumeBlockSize(). You can obtain a file's physical size by asking the Finder.
on physicalSize (pathname) |
with objectModel, finder |
return get (item[pathname].physicalsize) |
To obtain the size of just the data fork of a file, one way is to say this:
sizeOf (toys.readWholeFile (pathname))
To learn or set the minimum or normal RAM partition for an application:
sys.getMinAppSize (pathname)
sys.setMinAppSize (pathname, bytes)
sys.getAppSize (pathname)
sys.setAppSize (pathname, bytes)
To learn or set a file's creator code:
file.creator (pathname)
file.setCreator (pathname, string4)
To learn or set a file's type code:
file.type (pathname)
file.setType (pathname, string4)
To obtain the pathname of the application associated with a given creator code:
file.findApplication (string4)
To learn or set a file or folder's created date:
file.created (pathname)
file.setCreated (pathname, date)
To learn or set a file or folder's modified date:
file.modified (pathname)
file.setModified (pathname, date)
Modifying a file does not change its containing folder's modified date. This may not be what you want. A utility script, toys.touchPath(), changes the modified date to clock.now() on a file and all its enclosing folders.
To learn or set a file or folder's comment:
file.getComment (pathname)
file.setComment (pathname, commentString)
To learn or set a file's version string:
file.getVersion (pathname)
file.setVersion (pathname, versionString)
To learn or set a file's full version string:
file.getFullVersion (pathname)
file.setFullVersion (pathname, versionString)
To learn or set a file or folder's label:
file.getLabel (pathname)
file.setLabel (pathname, labelString)
To learn or set the state of a file or folder's visibility:
file.isVisible (pathname)
file.setVisible (pathname, visible?)
To learn or set a file or folder's locked state:
file.isLocked (pathname)
file.lock (pathname)
file.unlock (pathname)
To learn or set the state of a file's bundle bit:
sys.hasBundle (pathname)
sys.setBundle (pathname, set?)
To learn or set the state of any of a file or folder's finder flags:
finderflags.get (pathname, whichBit)
finderflags.set (pathname, whichBit)
finderflags.clear (pathname, whichBit)
It is unlikely that one will want to manipulate Finder flags directly, and the most important ones are accessible through other commands.7
To learn or set the icon position of a file or folder:
file.getIconPos (pathname, addrHoriz, addrVert)
file.setIconPos (pathname, horiz, vert)
The icon position in these commands is measured with reference to some point I can't figure out. It is easier to drive the Finder to get and set icon positions; that way, the icon position is measured from the top left of the window's content area, as expected.
Commands that can overwrite or delete a file cannot be undone and should be used with utmost care.
file.copy (source, dest)
To copy a file or folder selectively:
file.filteredCopy (source, dest, addrProc)
To move a file or folder within its volume:
file.move (source, destFolder)
To copy a file's data fork or resource fork to a new file:
file.copyDataFork (source, dest)
file.copyResourceFork (source, dest)
To "drop" a copy of a file into the system folder:
file.copyToSystemFolder (source)
To rename a file or folder or volume, in place:
file.rename (pathname, nameString)
file.delete (pathname)
To delete a folder even if it has contents:
file.deleteFolder (pathname)
file.deleteFolder() is a utility script which calls file.delete() in a simple, elegant recursive structure worthy of study.
To delete all files and folders in a folder:
toys.emptyFolder (pathname)
file.new (pathname)
To create an alias to a file or folder or volume:
file.newAlias (original, dest)
file.newFolder (pathname)
To create a new empty folder without raising an error if the folder exists:
file.sureFolder (pathname)
To create all folders along a proposed file pathname:
toys.sureFilePath (pathname)
All other verbs that create files and folders require that all the folders along the destination pathname exist already. By first passing the pathname to toys.sureFilePath(), you can proceed to create the file or folder, secure in the knowledge that all those folders now do exist.
To learn whether an existing item is a folder or volume:
file.isFolder (pathname)
file.isVolume (pathname)
To learn whether an existing item is an alias:
file.isAlias (pathname)
To test whether a file or folder or volume exists:
file.exists (pathname)
file.exists() is an important test, because many other file commands raise an error if the item doesn't exist.
To obtain the pathname of an alias's original:
file.followAlias (pathname)
Since Frontier can resolve aliases, they provide a way to maintain, in the Finder, a sort of physical analogue to a list. For an example, see suites.samples.basicStuff.nightlyBackup() , where aliases tell the script what files to back up and where to back them up.
To learn whether a file is "busy" (open for use by an application):
file.isBusy (pathname)
Some verbs in this chapter that make changes to a file cannot make them if the file is busy; the application that has it open for use locks out other users.
Testing whether a file is busy is also useful as a way of discovering whether an external process has come to an end. For example, I once used Frontier to drive a non-scriptable application to open and process several files in succession. The process on each file took about an hour. Frontier needed some way of knowing when the application was finished with each file, so that it could make the application go on to the next one; because the application was non-scriptable, it had no way of letting Frontier know this. The solution was a loop which checked once a minute to see if the file was still busy.
To learn whether the dataforks of two files are identical:
file.compare (pathname1, pathname2)
Nowhere is the lamentable hodge-podge inconsistency of Frontier's verb names more evident than in connection with the file verbs. Consider the following get/set pairs:
How simple it would have been to have named these verbs consistently:
Instead, one must deal with a different set of expressions for each function, and as a result the verb names are almost impossible to remember. Personally, I typically spend more time in the file table looking up the verb names than I do actually writing code that uses them.
1. It also possible to put a strain on Frontier's stack size, quite without regard to the value of memAvail(), which measures the heap. This might occur, for instance, if you call subroutines to a great depth, or import a table with very deep table nesting. There is no verb for directly enlarging the stack size, but a desktop script, SetFrontierStackSize, lets you do it.
2. For more about the scrap and the difference between the system scrap and a private scrap, see http://devworld.apple.com/dev/techsupport/insidemac/MoreToolbox/MoreToolbox-110.html.
3. To see the bug, select some text in a wptext, copy it, and paste it. Then, in Quick Script, execute clipboard.putValue("haha"). Switch to another application such as SimpleText, and paste. The result will be what you copied from the wptext, not "haha".
4. For example, copying text in SimpleText puts two pieces of data onto the clipboard, a 'TEXT' and a 'styl'.
5. For more information about the Gestalt Manager, see http://devworld.apple.com/dev/techsupport/insidemac/OSUtilities/OSUtilities-9.html. A number of selectors are listed there; however, the selector list is extensible, so for the most complete list, see http://www.bio.vu.nl/home/rgaros/gestalt/.
6. As this book goes to press, Apple is preparing to rectify this situation with the release of System 8.1 and HFS+.
7. The Finder flags are listed at this URL: http://devworld.apple.com/dev/techsupport/insidemac/Toolbox/Toolbox-464.html.
TOP | UP: Border Crossings | NEXT: Driving Other Applications |