heymodule - BeOS Application Scripting from Python

Version 1.0
October 31, 1998

BeOS R3 introduced a new BMessage based scripting system, that lets you remote control applications by sending them special scripting messages.

In theory, you can use anything you want to script your applications, as long as it can send and receive BMessage objects. In practice, you either have to write a C++ application, or use Attila Mezei's hey utility from the command-line.

Until now.

heymodule lets you do BeOS application scripting from Python. As far as I know, this is the very first third-party programming language to support BeOS's scripting!


Note: Currently, Python only runs on PowerPC systems; Steven Black (one of Be's QA engineers) is helping to change this, but it's not ready for prime time yet.

Installing heymodule

If you've already got Python 1.5.1 installed, you can grab the py-heymodule-ppc.zip or py-heymodule-ppc.pkg from BeWare's Languages section, or from my software pages.

If you haven't already installed Python 1.5.1, grab the python+heymodule-ppc.zip or python+heymodule-ppc.pkg from BeWare's Languages section, or from my software pages.

If you've downloaded one of the .pkg files, install it with SoftwareValet in the usual way.

If you've downloaded one of the .zip files, install it by unzipping it from the root of your boot drive:

$ cd /boot
$ unzip /path/to/py-heymodule-ppc.zip

Installing heymodule puts all the magic bits (the heymodule.so shared library) into the right places (/boot/home/config/lib/python1.5/BeOS). It also installs a heymodule-1.0 directory in /boot/apps; look inside for this documentation, and a few example scripts.

Now what?

Well, now you can do application scripting from Python.

This isn't saying much though; scripting isn't well documented yet, and there isn't really a whole lot you can do that's useful.

The BeOS Bible will have an excellent chapter on Scripting when it's published in the next month or two. Sorry for the plug, I wrote that chapter. :-)

Using heymodule

Importing heymodule

Before you can use heymodule, you've got to import it into Python:

import BeOS.hey

heymodule contains one Python object, so it's probably going to be easier to use if you import that object directly into your namespace:

from BeOS.hey import Hey

This will let you refer to the Hey object directly, instead of with a qualified name, BeOS.hey.Hey. Much nicer if you're using heymodule from an interactive session; you can do whatever you want in a Python program.


Note: heymodule creates a BApplication object; do not attempt to use it with any BeOS-specific module that also creates a BApplication. Something bad will happen. I don't know what, exactly, but you probably won't like it.

Creating a Hey object

The Hey object is your window onto the BeOS scripting world; you need one of these to do anything useful with heymodule.

Hey's constructor requires a string; this string can be:

Creating a Hey object from an application signature or MIME type will attempt to launch the appropriate application if it's not already running.


Note: Creating a Hey object from an application signature or MIME type is reliable; using a thread name may fail for no apparent reason (not all threads are named equally).

Hey object methods

The Hey object supports the following methods:

Count( specifier )
Return a count of the objects specified by the specifier. See Specifier, below.
Create( specifier )
Create an new instance of a property. This is untested. See Specifier, below.
Delete( specifier )
Delete an instance of a property. This is untested. See Specifier, below.
Get( specifier )
Return the given specifier's data. See Specifier, below.
GetSuites( specifier )
Return a dictionary detailing the suites supported by the given specifier. Call with no specifier to get the application's supported suites. The most useful part of this message will be the names in the list with the key "suites". See Specifier, below.
Load( path )
Tell the application to load the file specified by path. In hacker terms, this sends a B_REFS_RECEIVED message to the application.
Quit( specifier )
Tell the window specified by the given specifier to close; call with no specifier to tell the application to quit. Using a specifier doesn't currently work properly; quitting an entire application works fine. See Specifier, below.
Save( specifier )
Tell the application to save the document specified by the given specifier. Call with no specifier to tell the application to save all of its documents. This is untested. See Specifier, below.
Send( message )
Send an arbitrary message to the application. Currently you can only specify a "what" code for the message, you can't include extra data. message can be a number (such as 0x5f414252) or a four-character string (such as "_ABR"). Send() will return anything that's appropriate for the sent message.
SetBool( specifier, value )
Set the given specifier to the Boolean value. The value should probably be one of 0, "true", "yes", etc. Try your favourites. See Specifier, below.
SetColor( specifier, value )
Set the given specifier to the colour value. The value can be a tuple of red/green/blue/alpha values (such as ( 255, 255, 255, 127 )), or you can specify the read/green/blue/alpha values separately (such as 255, 255, 203, 255). In either case, the alpha is optional. See Specifier, below.
SetColour( specifier, value )
SetColor() for those of us who can spell English words properly.
SetDouble( specifier, value )
Set the given specifier to the double-precision floating-point value. Your application's documentation will tell you when you need to use a double instead of a float. See Specifier, below.
SetFloat( specifier, value )
Set the given specifier to the floating-point value. See Specifier, below.
SetInt( specifier, value )
Set the given specifier to the integer value. See Specifier, below.
SetInt8( specifier, value )
Set the given specifier to the 8-bit integer value. Your application's documentation will tell you when you need to use an int8 instead of a normal int. See Specifier, below.
SetInt16( specifier, value )
Set the given specifier to the 16-bit integer value. Your application's documentation will tell you when you need to use an int16 instead of a normal int. See Specifier, below.
SetInt32( specifier, value )
Set the given specifier to the 32-bit integer value. Your application's documentation will tell you when you need to use an int32 instead of a normal int. See Specifier, below.
SetPath( specifier, value )
Set the given specifier to the entry_ref that represents the path in value. You probably wanted to use Load( path ) instead. See Specifier, below.
SetPoint( specifier, value )
Set the given specifier to the BPoint value. The value can be specified by a tuple of X and Y values (such as ( 53, 192 )) or you can specify the X and Y values directly (such as 192.0, 53.0). You can also use integers or floating-point if you're feeling sassy. See Specifier, below.
SetRect( specifier, value )
Set the given specifier to the BRect value. The value can be specified by a tuple of left, top, right, bottom values (such as ( 5, 10, 15, 20 )) or you can specify them directly (such as 25, 30, 35, 40). Use integers or floating-point at will. See Specifier, below.
SetString( specifier, value )
Set the given specifier to the string value. See Specifier, below.
Specifier( specifier )
Create a Specifier object used by many of these methods. If you call it with no arguments, you'll have to populate it using the Specifier Stack style with Specifier's Add() method. If you don't like that, you can pass in a string matching the hey command-line tool's specifier syntax. See Specifier, below.

Unless stated otherwise, these methods will all return something reasonable for the given specifier. For example, if you're after the Frame of a Window, you're going to get back a rectangle (which will be a four-item tuple in Pythonese). If you ask for the Title, you'll get back a string.

If the Hey object doesn't know what the hell the other app was talking about in its reply, you'll get the entire message back; this is a Python dictionary, with keys matching the BMessage item names. Each key indexes a tuple, since BMessage can have multiple items under a single name.

This, if you got back a dictionary from one of the Hey methods, you could check the reply's error value like this:

if reply.has_key( "error" ):
    # Get the first item out of the tuple... there isn't likely to be
    # more than one "error" entry anyway.
    value = reply["error"][0]
    
    if value != 0:
        print "It's the end of the world as we know it!"

Note: BRect object and rgb_color objects both look like four-item tuples to Python. The BRect will have floating-point values though.

Specifier objects

The Specifier object is used by Hey's methods to target a specific part of an application, such as a view inside a window.

Creating a Specifier - the hey way

You can create a full, ready-to-use Specifier by using Hey's Specifier() method with a string:

spec = hey_object.Specifier( "Title of Window 0" )

The string uses exactly the same syntax as the hey command-line tool. Specifically:

specifier_1 [of specifier_2 ... specifier_n]

Where a specifier is a property name (such as Title or Window; see your application's documentation for more information about the properties it supports) optionally followed by a qualifier:

Qualifier Description
number This is an index specifier.
-number This isn a reverse index specifier.
[ start to end ] This is a range specifier.
[ -start to -end ] This is a reverse range specifier.
string This is a name specifier.

Properties without extra qualifiers are direct specifiers.

Consult your application's docs to find out what kind of specifiers it supports, and look in The Be Book's scripting topic for more information about properties and specifiers.

Creating a Specifier - the Specifier Stack way

The other way to create a usable Specifier is to create what The Be Book's scripting topic calls a Specifier Stack. Doing it this way is handy if you've got some docs that talk about application scripting for a C++ using audience; you can use the same syntax.

Step one is to create an empty Specifier using Hey's Specifier() method:

spec = hey_object.Specifier()

Now you use the Specifier object's Add() method to populate the Specifier:

# We're after the title of the first window:
spec.Add( "Title" )
spec.Add( "Window", 0 )

title = hey_object.Get( spec )
print "The title is:", title

The specifier stack is created from the most specific item (in this case, the Title) to the least specific (the Window).

Specifier's Add() method

The Add() method lets you add any kind of specifier to the stack:

Add( property )
Add a direct specifier for property.
Add( property, name )
Add a name specifier for the property named name.
Add( property, number )
Add an index or reverse index (if number is negative) specifier for the property.
Add( property, start, run )
Add a range or reverse range (if start is negative) specifier covering run items for the property.

Note: Do not mix Specifier's Add() method with a Specifier that was created from a hey-style string. I don't think it will work the way you expect.

Examples

Hiding and showing windows

Say you start campus from your UserBootscript, and you'd like to hide it's status window:

from BeOS.hey import Hey
x = Hey( "campus" )
s = x.Specifier( "Hidden of Window 0" )
reply = x.SetBool( s, 1 )

To make it reappear:

from BeOS.hey import Hey
x = Hey( "campus" )
s = x.Specifier( "Hidden of Window 0" )
reply = x.SetBool( s, "false" )

Getting the Title and Frame of a window

Let's start the preferred handler for text/plain files, and find the Title and Frame rectangle of its first window:

from BeOS.hey import Hey

x = Hey( "text/plain" )

# Set up a specifier for the frame, using the hey style:
frame_spec = x.Specifier( "Frame of Window 0" )

# Set up a specifier for the title, using the specifier stack:
title_spec = x.Specifier()
title_spec.Add( "Title" )
title_spec.Add( "Window", 0 )

# Get the frame and title:
frame_rect = x.Get( frame_spec )
title = x.Get( title_spec )

# Now say what we got:
print "The window %s has a frame rectangle of %s." % ( title, frame_rect )

# Now tell the text/plain handler to quit:
x.Quit()

On my system, StyledEdit is the text/plain handler, and this prints:

The window Untitled 1 has a frame rectangle of (7.0, 26.0, 507.0, 426.0).

Moving windows around

Let's start the preferred handler for text/plain files again and move its window around.

from BeOS.hey import Hey
from time import sleep

x = Hey( "text/plain" )

frame_spec = x.Specifier( "Frame of Window 0" )

(left, top, right, bottom) = x.Get( frame_spec )

for foo in range( 5, 105, 5 ):
    # Looks like you need to refresh the specifier every time you want
    # to use it... bug or feature?
    frame_spec = x.Specifier( "Frame of Window 0" )

    # Move the window 5 pixels at a time:
    reply = x.SetRect( frame_spec,
        ( ( left + foo ), ( top + foo ), ( right + foo ), ( bottom + foo ) ) )

    # Just to show that this is the same as using the "hey" style:
    frame_rect = x.Specifier()
    frame_rect.Add( "Frame" )
    frame_rect.Add( "Window", 0 )

    # Get and print the current frame rectangle:
    now_rect = x.Get( frame_rect )
    print "rect is now '%s'" % ( now_rect, )

    # Snooze for a second so we can actually see the window moving.
    sleep( 1 )

# Tell us where the script left the window.
frame_rect = x.Specifier( "Frame of Window 0" )
new_rect = x.Get( frame_rect )
print "The window is now at %s." % ( new_rect, )

Listing an application's windows

Say you want to know the titles of every window in the currently running Pe session:

from BeOS.hey import Hey

# Create a Hey object to talk to Pe.
pe = Hey( "pe" )

# Count its windows.
wind = pe.Specifier( "Window" )
num = pe.Count( wind )

print "Pe has %d windows..." % ( num )

# For every window, find its title.
for i in range( num ):
    s = pe.Specifier()
    s.Add( "Title" )
    s.Add( "Window", i )

    name = pe.Get( s )

    print "Pe's window %d is named '%s'" % ( i, name )

Right now on my system I get something like this:

Pe has 7 windows...
Pe's window 0 is named 'Open'
Pe's window 1 is named 'Find'
Pe's window 2 is named '/boot/home/config/settings/pe/Worksheet'
Pe's window 3 is named 'Preferences'
Pe's window 4 is named '/boot/src/Python/Python-1.5.1/Modules/heymodule.cpp'
Pe's window 5 is named '/boot/src/Python/Python-1.5.1/Modules/heymodule.html'
Pe's window 6 is named '/boot/src/Python/Python-1.5.1/Modules/testhey.py'

Since I can't see windows 0, 1, and 3 on my screen, Pe must keep them around as hidden windows. As an exercise for the reader, change this to make all of the windows visible (or if you're feeling saucy, show all the hidden windows and hide all the visible windows).


Copyright © 1998, Chris Herborth (chrish@qnx.com)